diff --git a/.gitattributes b/.gitattributes index a6344aac8c09253b3b630fb776ae94478aa0275b..c21755294780162d5839feeb7f907824cca1908e 100644 --- a/.gitattributes +++ b/.gitattributes @@ -33,3 +33,37 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text *.zip filter=lfs diff=lfs merge=lfs -text *.zst filter=lfs diff=lfs merge=lfs -text *tfevents* filter=lfs diff=lfs merge=lfs -text +www/assets/cars/corvette_zr1.glb filter=lfs diff=lfs merge=lfs -text +www/assets/cars/ford_gt.glb filter=lfs diff=lfs merge=lfs -text +www/assets/cars/lambo_sc18.glb filter=lfs diff=lfs merge=lfs -text +www/assets/cars/mclaren_600lt.glb filter=lfs diff=lfs merge=lfs -text +www/assets/cars/mustang_roush.glb filter=lfs diff=lfs merge=lfs -text +www/assets/cars/porsche_gt2rs.glb filter=lfs diff=lfs merge=lfs -text +www/assets/maps/shanghai/textures/PAT_asf_out_123.png filter=lfs diff=lfs merge=lfs -text +www/assets/maps/shanghai/textures/PAT_rug_blu_112.png filter=lfs diff=lfs merge=lfs -text +www/assets/maps/shanghai/textures/advert_side_a_d_56.png filter=lfs diff=lfs merge=lfs -text +www/assets/maps/shanghai/textures/asphalt-new.png filter=lfs diff=lfs merge=lfs -text +www/assets/maps/shanghai/textures/conc_bollard_filler_a_01_d_39.png filter=lfs diff=lfs merge=lfs -text +www/assets/maps/shanghai/textures/grass-detailrealistic.png filter=lfs diff=lfs merge=lfs -text +www/assets/maps/shanghai/textures/sha_hut_marshalls_a_d_63.png filter=lfs diff=lfs merge=lfs -text +www/assets/maps/shanghai/textures/sha_rug_a_01_d_98.png filter=lfs diff=lfs merge=lfs -text +www/assets/maps/shanghai/textures/sha_sand_a_01_d_139.png filter=lfs diff=lfs merge=lfs -text +www/assets/maps/shanghai/textures/sha_transition_a_01_d_106.png filter=lfs diff=lfs merge=lfs -text +www/assets/maps/shanghai/textures/sponsor_backboard_02_48.png filter=lfs diff=lfs merge=lfs -text +www/assets/maps/shanghai/textures/textures/PAT_asf_out_123.png filter=lfs diff=lfs merge=lfs -text +www/assets/maps/shanghai/textures/textures/PAT_rug_blu_112.png filter=lfs diff=lfs merge=lfs -text +www/assets/maps/shanghai/textures/textures/advert_side_a_d_56.png filter=lfs diff=lfs merge=lfs -text +www/assets/maps/shanghai/textures/textures/asphalt-new.png filter=lfs diff=lfs merge=lfs -text +www/assets/maps/shanghai/textures/textures/conc_bollard_filler_a_01_d_39.png filter=lfs diff=lfs merge=lfs -text +www/assets/maps/shanghai/textures/textures/grass-detailrealistic.png filter=lfs diff=lfs merge=lfs -text +www/assets/maps/shanghai/textures/textures/sha_hut_marshalls_a_d_63.png filter=lfs diff=lfs merge=lfs -text +www/assets/maps/shanghai/textures/textures/sha_rug_a_01_d_98.png filter=lfs diff=lfs merge=lfs -text +www/assets/maps/shanghai/textures/textures/sha_sand_a_01_d_139.png filter=lfs diff=lfs merge=lfs -text +www/assets/maps/shanghai/textures/textures/sha_transition_a_01_d_106.png filter=lfs diff=lfs merge=lfs -text +www/assets/maps/shanghai/textures/textures/sponsor_backboard_02_48.png filter=lfs diff=lfs merge=lfs -text +www/assets/maps/shanghai/textures/textures/tree04a_127.png filter=lfs diff=lfs merge=lfs -text +www/assets/maps/shanghai/textures/textures/tree06a_128.png filter=lfs diff=lfs merge=lfs -text +www/assets/maps/shanghai/textures/tree04a_127.png filter=lfs diff=lfs merge=lfs -text +www/assets/maps/shanghai/textures/tree06a_128.png filter=lfs diff=lfs merge=lfs -text +www/assets/maps/shanghai/track.glb filter=lfs diff=lfs merge=lfs -text +www/assets/maps/track.glb filter=lfs diff=lfs merge=lfs -text diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..05c3cd7fc0331c61d8b30ce4f826dc22304e1d7b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +# Sole process binds :7860. Serves API + full HF Racing game client (www/). +FROM python:3.11-slim + +ENV PYTHONUNBUFFERED=1 \ + PORT=7860 + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY app.py lobbies.py relay.py ./ +COPY www ./www + +EXPOSE 7860 + +CMD ["python", "app.py"] diff --git a/README.md b/README.md index 0c954cbc61cb05c96dbe6ccde1180c2bc20c0c5f..6ccbad5caac350ab55cbb0a0a9bd4ce9c0dfd461 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,20 @@ --- -title: HF Racing -emoji: 🐠 -colorFrom: purple -colorTo: gray +title: HF-Racing +emoji: 🏎️ +colorFrom: red +colorTo: yellow sdk: docker +app_port: 7860 pinned: false --- -Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference +# HF Racing + +Public **match server** + lobby board + **spectate** (no HF login). + +- **Board / status:** [`/board`](./board) +- **Spectate:** Spectate button on a live lobby (no account) +- **Play:** local HF Racing only (`bash start.sh` + `HF_TOKEN`) — Space rejects anonymous seat claims + +APIs: `/api/health`, `/api/lobbies`, `/api/config` +Match WS: `/ws/match/{mode}/{lobby_id}?user=...&role=play|spectate` diff --git a/app.py b/app.py new file mode 100644 index 0000000000000000000000000000000000000000..8b3a86945cc9748fe0297056eefff3f2d26336d8 --- /dev/null +++ b/app.py @@ -0,0 +1,437 @@ +"""HF Space: FastAPI lobbies/WS + full HF Racing game client (www/).""" + +from __future__ import annotations + +import mimetypes +import os +import re +import secrets +from pathlib import Path +from typing import Optional + +import uvicorn +import httpx +from fastapi import Cookie, FastAPI, Header, Request, Response, WebSocket +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import FileResponse, HTMLResponse, JSONResponse, RedirectResponse +from fastapi.staticfiles import StaticFiles +from pydantic import BaseModel, Field + +from lobbies import board +from relay import handle_match_ws + +WWW = Path(__file__).resolve().parent / "www" +_GUEST_RE = re.compile(r"^[A-Za-z0-9_-]{2,32}$") + +mimetypes.add_type("model/gltf-binary", ".glb") +mimetypes.add_type("model/gltf+json", ".gltf") + + +class ClaimBody(BaseModel): + username: str = Field(..., min_length=1) + seat: str = Field(..., min_length=1) + avatarUrl: Optional[str] = None + + +class LeaveBody(BaseModel): + username: str = Field(..., min_length=1) + + +class HeartbeatBody(BaseModel): + username: str = Field(..., min_length=1) + + +BOARD_HTML = """ + + + + + HF-Racing · Lobby board + + + +
+

HF Racing

+

Public lobby board + spectate. Racing requires the local client with HF login (for identity / analytics).

+ + +
Loading lobbies…
+ +
+ + + +""" + + +def _ensure_guest(response: Response, racing_user: str | None) -> str: + name = (racing_user or "").strip() + if not name or not _GUEST_RE.fullmatch(name): + name = f"guest-{secrets.token_hex(3)}" + response.set_cookie( + key="racing_user", + value=name, + max_age=60 * 60 * 24 * 30, + httponly=False, + samesite="lax", + ) + return name + + +def _hf_username_from_auth(authorization: str | None) -> str | None: + if not authorization: + return None + parts = authorization.split() + if len(parts) != 2 or parts[0].lower() != "bearer": + return None + token = parts[1].strip() + if not token: + return None + try: + resp = httpx.get( + "https://huggingface.co/api/whoami-v2", + headers={"Authorization": f"Bearer {token}"}, + timeout=15.0, + ) + if resp.status_code != 200: + return None + data = resp.json() + name = data.get("name") or data.get("fullname") + return str(name) if name else None + except Exception: + return None + + +def create_app() -> FastAPI: + api = FastAPI() + api.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) + + @api.get("/api/health") + def health(): + return {"ok": True, "host": "space", "hasClient": WWW.is_dir(), "playAllowed": False} + + @api.get("/api/config") + def config(response: Response, racing_user: str | None = Cookie(default=None)): + # Guest id is for spectate only — Space never allows play. + username = _ensure_guest(response, racing_user) + return { + "ok": True, + "spaceUrl": "", + "username": username, + "avatarUrl": None, + "authError": None, + "hasToken": False, + "hasSpaceUrl": False, + "lobbyHost": "space", + "host": "space", + "playAllowed": False, + } + + @api.get("/api/lobbies") + def get_lobbies(): + return board.snapshot() + + @api.post("/api/lobbies/{mode}/{lobby_id}/claim") + def claim_seat( + mode: str, + lobby_id: str, + body: ClaimBody, + authorization: str | None = Header(default=None), + ): + # Play only via local client proxy with a real HF token (analytics identity). + hf_user = _hf_username_from_auth(authorization) + if not hf_user: + return JSONResponse( + { + "ok": False, + "error": "Play disabled on Space. Use local HF Racing with HF_TOKEN.", + }, + status_code=403, + ) + result = board.claim( + mode, lobby_id, hf_user, body.seat, avatar_url=body.avatarUrl + ) + return JSONResponse(result, status_code=200 if result.get("ok") else 400) + + @api.post("/api/lobbies/leave") + def leave_lobby(body: LeaveBody, authorization: str | None = Header(default=None)): + hf_user = _hf_username_from_auth(authorization) + # Allow leave for HF-auth players; ignore anonymous spoof leave of others + if hf_user: + return board.leave(hf_user) + if body.username and not str(body.username).startswith("guest-"): + # Unauthenticated leave of a real seat — reject + return JSONResponse({"ok": False, "error": "HF auth required"}, status_code=403) + return {"ok": True} + + @api.post("/api/lobbies/heartbeat") + def heartbeat(body: HeartbeatBody, authorization: str | None = Header(default=None)): + hf_user = _hf_username_from_auth(authorization) + if hf_user: + board.heartbeat(hf_user) + elif body.username and not str(body.username).startswith("guest-"): + return JSONResponse({"ok": False, "error": "HF auth required"}, status_code=403) + return {"ok": True} + + @api.websocket("/ws/match/{mode}/{lobby_id}") + async def match_ws( + ws: WebSocket, + mode: str, + lobby_id: str, + user: str = "", + role: str = "play", + ): + spectate = (role or "").lower() in ("spectate", "spec", "watch") + # Browser on Space: spectate only. Play sockets are for seated HF players + # (local client claims with HF_TOKEN, then connects play WS). + if not spectate: + # Allow play only if this user already holds a seat (HF-authenticated claim). + seated = board.by_user.get((user or "").strip()) + if not seated or seated[0] != lobby_id: + await ws.accept() + await ws.send_text( + '{"type":"error","error":"Spectate only in browser. Play via local HF Racing + HF_TOKEN."}' + ) + await ws.close() + return + await handle_match_ws(ws, mode, lobby_id, user, role=role) + + @api.get("/board", response_class=HTMLResponse) + def lobby_board(): + return BOARD_HTML + + @api.get("/") + def game_index(request: Request): + # Game client on Space is spectate-entry only. + q = request.query_params + if q.get("spectate") == "1" and q.get("lobby"): + index = WWW / "index.html" + if not index.is_file(): + return HTMLResponse( + "

Game client missing

Rebuild Space with www/.

", + status_code=503, + ) + return FileResponse(index) + return RedirectResponse(url="/board", status_code=302) + + if WWW.is_dir(): + for mount, folder in (("assets", "assets"), ("js", "js"), ("css", "css")): + path = WWW / folder + if path.is_dir(): + api.mount(f"/{mount}", StaticFiles(directory=str(path)), name=mount) + + return api + + +app = create_app() + + +def main() -> None: + port = int(os.environ.get("PORT") or "7860") + uvicorn.run(app, host="0.0.0.0", port=port, log_level="info") + + +if __name__ == "__main__": + main() diff --git a/lobbies.py b/lobbies.py new file mode 100644 index 0000000000000000000000000000000000000000..192f2e83ec068e148efe08d1a6f5790f19cbe5ad --- /dev/null +++ b/lobbies.py @@ -0,0 +1,283 @@ +"""Fixed lobby board: sandbox, 1v1, 4v4. Seat claims are server-authoritative.""" + +from __future__ import annotations + +import string +import time +from typing import Any, Optional + +# Lobby IDs: 0A–0X, 1A–1H, 4A–4H (category is already in the mode UI) +_LETTERS = string.ascii_uppercase +SANDBOX_IDS = [f"0{ch}" for ch in _LETTERS[:10]] # 0A … 0J +DUEL_IDS = [f"1{ch}" for ch in _LETTERS[:8]] # 1A … 1H +SQUAD_IDS = [f"4{ch}" for ch in _LETTERS[:8]] # 4A … 4H + +SANDBOX_CAP = 1 # one seat per sandbox lobby +DUEL_CAP = 2 +SQUAD_CAP = 8 # 4 + 4 + +# 4v4: start if full, or idle 60s with ≥1 per side +SQUAD_IDLE_START_SEC = 60.0 +SEAT_STALE_SEC = 8.0 +# Ghost matches: if nobody heartbeats, free the lobby (was locking seats forever). +STARTING_TIMEOUT_SEC = 40.0 +LIVE_STALE_SEC = 40.0 + + +def _now() -> float: + return time.time() + + +def _empty_sandbox(lobby_id: str) -> dict[str, Any]: + return { + "id": lobby_id, + "mode": "sandbox", + "seats": {f"S-{i}": None for i in range(1, SANDBOX_CAP + 1)}, + "status": "open", # open | starting | live + "last_change": _now(), + "match_id": None, + } + + +def _empty_duel(lobby_id: str) -> dict[str, Any]: + return { + "id": lobby_id, + "mode": "1v1", + "seats": {"X-1": None, "Y-1": None}, + "status": "open", + "last_change": _now(), + "match_id": None, + } + + +def _empty_squad(lobby_id: str) -> dict[str, Any]: + seats: dict[str, Optional[str]] = {} + for side in ("X", "Y"): + for i in range(1, 5): + seats[f"{side}-{i}"] = None + return { + "id": lobby_id, + "mode": "4v4", + "seats": seats, + "status": "open", + "last_change": _now(), + "match_id": None, + } + + +class LobbyBoard: + def __init__(self) -> None: + self.lobbies: dict[str, dict[str, Any]] = {} + for lid in SANDBOX_IDS: + self.lobbies[lid] = _empty_sandbox(lid) + for lid in DUEL_IDS: + self.lobbies[lid] = _empty_duel(lid) + for lid in SQUAD_IDS: + self.lobbies[lid] = _empty_squad(lid) + # username -> (lobby_id, seat) + self.by_user: dict[str, tuple[str, str]] = {} + + def snapshot(self) -> dict[str, Any]: + self._tick_stale_and_start() + return { + "sandbox": [self._public(self.lobbies[lid]) for lid in SANDBOX_IDS], + "duel": [self._public(self.lobbies[lid]) for lid in DUEL_IDS], + "squad": [self._public(self.lobbies[lid]) for lid in SQUAD_IDS], + "serverTime": _now(), + } + + def _public(self, lobby: dict[str, Any]) -> dict[str, Any]: + return { + "id": lobby["id"], + "mode": lobby["mode"], + "seats": dict(lobby["seats"]), + "status": lobby["status"], + "lastChange": lobby["last_change"], + "matchId": lobby["match_id"], + "filled": sum(1 for v in lobby["seats"].values() if v), + "capacity": len(lobby["seats"]), + } + + def _clear_user(self, username: str) -> None: + prev = self.by_user.pop(username, None) + if not prev: + return + lid, seat = prev + lobby = self.lobbies.get(lid) + if lobby and lobby["seats"].get(seat) == username: + lobby["seats"][seat] = None + lobby.get("_avatars", {}).pop(username, None) + lobby.get("_hb", {}).pop(username, None) + lobby["last_change"] = _now() + # Empty live/starting lobby must reset — otherwise match_id lingers + # and the next WS wait loop "starts" a ghost match with no players. + if lobby["status"] in ("live", "starting") and not any(lobby["seats"].values()): + self._reset_lobby(lobby) + + def _reset_lobby(self, lobby: dict[str, Any]) -> None: + mode = lobby["mode"] + lid = lobby["id"] + if mode == "sandbox": + self.lobbies[lid] = _empty_sandbox(lid) + elif mode == "1v1": + self.lobbies[lid] = _empty_duel(lid) + else: + self.lobbies[lid] = _empty_squad(lid) + + def leave(self, username: str) -> dict[str, Any]: + self._clear_user(username) + return {"ok": True} + + def claim( + self, + mode: str, + lobby_id: str, + username: str, + seat: str, + avatar_url: str | None = None, + ) -> dict[str, Any]: + username = (username or "").strip() + if not username: + return {"ok": False, "error": "username required"} + lobby = self.lobbies.get(lobby_id) + if not lobby or lobby["mode"] != mode: + return {"ok": False, "error": "lobby not found"} + if seat not in lobby["seats"]: + return {"ok": False, "error": "invalid seat"} + if lobby["status"] in ("live", "starting"): + return {"ok": False, "error": "match already starting"} + if lobby["seats"][seat] is not None: + return {"ok": False, "error": "seat taken"} + + # Move from previous seat if any + self._clear_user(username) + lobby["seats"][seat] = username + lobby["last_change"] = _now() + lobby.setdefault("_hb", {})[username] = _now() + avatars = lobby.setdefault("_avatars", {}) + if avatar_url and str(avatar_url).strip(): + avatars[username] = str(avatar_url).strip() + elif username not in avatars: + avatars[username] = f"https://huggingface.co/avatars/{username}" + self.by_user[username] = (lobby_id, seat) + started = self._maybe_start(lobby) + return { + "ok": True, + "lobby": self._public(lobby), + "seat": seat, + "started": started, + "matchId": lobby["match_id"], + } + + def heartbeat(self, username: str) -> None: + prev = self.by_user.get(username) + if not prev: + return + lid, seat = prev + lobby = self.lobbies.get(lid) + if not lobby: + return + # Stash last seen on seat metadata via parallel dict + lobby.setdefault("_hb", {})[username] = _now() + + def _tick_stale_and_start(self) -> None: + now = _now() + stale_users: list[str] = [] + for username, (lid, seat) in list(self.by_user.items()): + lobby = self.lobbies.get(lid) + if not lobby: + stale_users.append(username) + continue + hb = lobby.get("_hb", {}).get(username, lobby["last_change"]) + status = lobby["status"] + if status == "open" and now - hb > SEAT_STALE_SEC: + stale_users.append(username) + elif status == "live" and now - hb > LIVE_STALE_SEC: + # Drop ghost "live" seats so the board doesn't stay unclickable forever. + stale_users.append(username) + elif status == "starting" and now - lobby["last_change"] > STARTING_TIMEOUT_SEC: + stale_users.append(username) + for u in stale_users: + self._clear_user(u) + + for lobby in list(self.lobbies.values()): + if lobby["status"] == "starting" and now - lobby["last_change"] > STARTING_TIMEOUT_SEC: + self._reset_lobby(lobby) + elif lobby["status"] == "live": + seated = [u for u in lobby["seats"].values() if u] + if seated and all( + now - lobby.get("_hb", {}).get(u, 0) > LIVE_STALE_SEC for u in seated + ): + self._reset_lobby(lobby) + elif lobby["status"] == "open": + self._maybe_start(lobby) + + def _maybe_start(self, lobby: dict[str, Any]) -> bool: + if lobby["status"] != "open": + return False + mode = lobby["mode"] + seats = lobby["seats"] + filled = [s for s, u in seats.items() if u] + + if mode == "sandbox": + # Start as soon as anyone joins + if filled: + return self._mark_starting(lobby) + return False + + if mode == "1v1": + if seats.get("X-1") and seats.get("Y-1"): + return self._mark_starting(lobby) + return False + + # 4v4 + team_x = [s for s, u in seats.items() if u and s.startswith("X-")] + team_y = [s for s, u in seats.items() if u and s.startswith("Y-")] + if len(filled) >= SQUAD_CAP: + return self._mark_starting(lobby) + if team_x and team_y and (_now() - lobby["last_change"]) >= SQUAD_IDLE_START_SEC: + return self._mark_starting(lobby) + return False + + def _mark_starting(self, lobby: dict[str, Any]) -> bool: + import uuid + + lobby["status"] = "starting" + lobby["match_id"] = str(uuid.uuid4()) + lobby["last_change"] = _now() + return True + + def mark_live(self, lobby_id: str) -> None: + lobby = self.lobbies.get(lobby_id) + if lobby and lobby["status"] == "starting": + lobby["status"] = "live" + + def players_in(self, lobby_id: str) -> list[dict[str, str]]: + lobby = self.lobbies.get(lobby_id) + if not lobby: + return [] + avatars = lobby.get("_avatars") or {} + out = [] + for seat, user in lobby["seats"].items(): + if user: + side = "ffa" + if seat.startswith("X-"): + side = "X" + elif seat.startswith("Y-"): + side = "Y" + out.append( + { + "username": user, + "seat": seat, + "side": side, + "avatarUrl": avatars.get(user) + or f"https://huggingface.co/avatars/{user}", + } + ) + return out + + def get(self, lobby_id: str) -> Optional[dict[str, Any]]: + return self.lobbies.get(lobby_id) + + +board = LobbyBoard() diff --git a/relay.py b/relay.py new file mode 100644 index 0000000000000000000000000000000000000000..c8b2a2c08b592fa59174be7bbd37d27edd1da0e9 --- /dev/null +++ b/relay.py @@ -0,0 +1,237 @@ +"""WebSocket match rooms — fan-out relay, no game simulation.""" + +from __future__ import annotations + +import asyncio +import json +from typing import Any + +from fastapi import WebSocket, WebSocketDisconnect + +from lobbies import board + + +class MatchRoom: + def __init__(self, lobby_id: str, mode: str, match_id: str) -> None: + self.lobby_id = lobby_id + self.mode = mode + self.match_id = match_id + self.clients: dict[str, WebSocket] = {} # seated players + self.spectators: dict[str, WebSocket] = {} # read-only viewers + self.lock = asyncio.Lock() + self.started = False + + async def add(self, username: str, ws: WebSocket, *, spectate: bool = False) -> None: + async with self.lock: + bucket = self.spectators if spectate else self.clients + old = bucket.get(username) + if old and old is not ws: + try: + await old.close() + except Exception: + pass + bucket[username] = ws + + async def remove(self, username: str, ws: WebSocket, *, spectate: bool = False) -> None: + async with self.lock: + bucket = self.spectators if spectate else self.clients + if bucket.get(username) is ws: + del bucket[username] + + async def broadcast(self, msg: dict[str, Any], exclude: str | None = None) -> None: + data = json.dumps(msg) + dead_players: list[str] = [] + dead_specs: list[str] = [] + async with self.lock: + players = list(self.clients.items()) + specs = list(self.spectators.items()) + for user, client in players + specs: + if exclude and user == exclude: + continue + try: + await client.send_text(data) + except Exception: + if user in self.clients: + dead_players.append(user) + else: + dead_specs.append(user) + for u in dead_players: + board.leave(u) + async with self.lock: + self.clients.pop(u, None) + async with self.lock: + for u in dead_specs: + self.spectators.pop(u, None) + + +rooms: dict[str, MatchRoom] = {} +rooms_lock = asyncio.Lock() + + +async def get_or_create_room(lobby_id: str, mode: str, match_id: str) -> MatchRoom: + key = f"{mode}:{lobby_id}" + async with rooms_lock: + room = rooms.get(key) + if room is None or room.match_id != match_id: + room = MatchRoom(lobby_id, mode, match_id) + rooms[key] = room + return room + + +async def handle_match_ws( + ws: WebSocket, + mode: str, + lobby_id: str, + username: str, + role: str = "play", +) -> None: + await ws.accept() + username = (username or "").strip() + spectate = (role or "play").lower() in ("spectate", "spec", "watch") + if not username: + await ws.send_text(json.dumps({"type": "error", "error": "username required"})) + await ws.close() + return + + # Spectators use a distinct key so they never collide with a seated player name + if spectate: + username = f"spec:{username}" + + lobby = board.get(lobby_id) + if not lobby or lobby["mode"] != mode: + await ws.send_text(json.dumps({"type": "error", "error": "lobby not found"})) + await ws.close() + return + + if not spectate: + seat_info = board.by_user.get(username) + if not seat_info or seat_info[0] != lobby_id: + await ws.send_text(json.dumps({"type": "error", "error": "not seated in lobby"})) + await ws.close() + return + board.heartbeat(username) + + # Wait until lobby is starting/live with a match_id + for _ in range(180): + lobby = board.get(lobby_id) + if lobby and not spectate: + # Keep seat alive while parked in the wait loop (HTTP HB can lag). + board.heartbeat(username) + board._maybe_start(lobby) + lobby = board.get(lobby_id) + # Seat was wiped (stale / leave) — stop waiting instead of crashing later. + if not board.by_user.get(username) or board.by_user.get(username)[0] != lobby_id: + await ws.send_text(json.dumps({"type": "error", "error": "seat lost — rejoin lobby"})) + await ws.close() + return + if lobby and lobby.get("match_id") and lobby["status"] in ("starting", "live"): + # Only proceed if this player is still seated (or spectating). + if spectate or (board.by_user.get(username) and board.by_user[username][0] == lobby_id): + break + if spectate and lobby and lobby["status"] == "open" and not lobby.get("match_id"): + await ws.send_text( + json.dumps({"type": "waiting", "lobby": board._public(lobby), "spectating": True}) + ) + await asyncio.sleep(0.5) + continue + await asyncio.sleep(0.5) + try: + pub = board._public(lobby) if lobby else None + await ws.send_text( + json.dumps({"type": "waiting", "lobby": pub, "spectating": spectate}) + ) + except Exception: + return + else: + await ws.send_text(json.dumps({"type": "error", "error": "match did not start"})) + await ws.close() + return + + lobby = board.get(lobby_id) + if not lobby or not lobby.get("match_id"): + await ws.send_text(json.dumps({"type": "error", "error": "lobby reset — rejoin"})) + await ws.close() + return + + seat = None + if not spectate: + seat_info = board.by_user.get(username) + if not seat_info or seat_info[0] != lobby_id: + await ws.send_text(json.dumps({"type": "error", "error": "not seated in lobby"})) + await ws.close() + return + seat = seat_info[1] + + match_id = lobby["match_id"] + room = await get_or_create_room(lobby_id, mode, match_id) + await room.add(username, ws, spectate=spectate) + + players = board.players_in(lobby_id) + start_payload = { + "type": "match_start", + "matchId": match_id, + "mode": mode, + "lobbyId": lobby_id, + "username": username, + "seat": seat, + "players": players, + "spectating": spectate, + } + await ws.send_text(json.dumps(start_payload)) + if not spectate: + board.mark_live(lobby_id) + await room.broadcast( + { + "type": "player_joined", + "username": username, + "seat": seat, + "players": board.players_in(lobby_id), + }, + exclude=username, + ) + + try: + while True: + raw = await ws.receive_text() + if not spectate: + board.heartbeat(username) + try: + msg = json.loads(raw) + except json.JSONDecodeError: + continue + if not isinstance(msg, dict): + continue + # Spectators are receive-only (except ping) + if spectate: + if msg.get("type") == "ping": + await ws.send_text(json.dumps({"type": "pong"})) + continue + msg["from"] = username + if msg.get("type") == "ping": + await ws.send_text(json.dumps({"type": "pong"})) + continue + await room.broadcast(msg, exclude=username) + except WebSocketDisconnect: + pass + except Exception: + pass + finally: + await room.remove(username, ws, spectate=spectate) + if not spectate: + # HF WS often blips right at match_start. Instant leave was wiping BOTH + # seats and bootstrapping players back to an empty lobby. + await asyncio.sleep(3.0) + still = board.by_user.get(username) + reconnected = username in room.clients + if still and still[0] == lobby_id and not reconnected: + board.leave(username) + await room.broadcast( + { + "type": "player_left", + "username": username, + "players": board.players_in(lobby_id), + } + ) + async with rooms_lock: + if room.lobby_id == lobby_id and not room.clients and not room.spectators: + rooms.pop(f"{mode}:{lobby_id}", None) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..5f83988bb61edbf98448ef1b7d47d0a5ee1b70a0 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +fastapi==0.115.6 +uvicorn[standard]==0.32.1 +pydantic>=2.11,<2.12 +httpx==0.28.1 diff --git a/www/assets/.gitkeep b/www/assets/.gitkeep new file mode 100644 index 0000000000000000000000000000000000000000..2272bc187f9bd4aac539035d68e8a484e73909d2 --- /dev/null +++ b/www/assets/.gitkeep @@ -0,0 +1 @@ +# Shared audio / misc assets diff --git a/www/assets/cars/.gitkeep b/www/assets/cars/.gitkeep new file mode 100644 index 0000000000000000000000000000000000000000..c9608a8db08ddba15d4be57d034f356b8a1896f3 --- /dev/null +++ b/www/assets/cars/.gitkeep @@ -0,0 +1 @@ +# Drop car .glb / .gltf files here diff --git a/www/assets/cars/corvette_zr1.glb b/www/assets/cars/corvette_zr1.glb new file mode 100644 index 0000000000000000000000000000000000000000..a3ca6f6882fdb18aec07eed208805c2504b61c07 --- /dev/null +++ b/www/assets/cars/corvette_zr1.glb @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:12fbf0426979fcf0e9d7940ddee1ff23facd811e40159b6eb027f6959ad44a82 +size 9819496 diff --git a/www/assets/cars/ford_gt.glb b/www/assets/cars/ford_gt.glb new file mode 100644 index 0000000000000000000000000000000000000000..bd701b85269679a40aa2c38e31f7e2281b3fd1f0 --- /dev/null +++ b/www/assets/cars/ford_gt.glb @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:743b795e1116d1158afe2635c975486a106679e93033286c4721b7e0108dcd24 +size 7663384 diff --git a/www/assets/cars/lambo_sc18.glb b/www/assets/cars/lambo_sc18.glb new file mode 100644 index 0000000000000000000000000000000000000000..5115e93c2c6df8b56655a5854dea63f49c24285a --- /dev/null +++ b/www/assets/cars/lambo_sc18.glb @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:48fff10172b9eb7c8513448c4a6c60763d0008f00b72332cb36eaeebbae8bc3a +size 11656112 diff --git a/www/assets/cars/mclaren_600lt.glb b/www/assets/cars/mclaren_600lt.glb new file mode 100644 index 0000000000000000000000000000000000000000..a5cac4d613a9a959a1b9ef2ca5b19ca582b0c50f --- /dev/null +++ b/www/assets/cars/mclaren_600lt.glb @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:18ad77768b83c8db768cbf0a3d99b61e89e94c91707ffc289ae4e597d2f1c93c +size 5918960 diff --git a/www/assets/cars/mustang_roush.glb b/www/assets/cars/mustang_roush.glb new file mode 100644 index 0000000000000000000000000000000000000000..f10b847d3239ad2ac86a5810717343299be4e7f5 --- /dev/null +++ b/www/assets/cars/mustang_roush.glb @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7fd4c7a4499a9f1d266842af0bc21e426dc19118a759344a74f508616a88c475 +size 29595096 diff --git a/www/assets/cars/porsche_gt2rs.glb b/www/assets/cars/porsche_gt2rs.glb new file mode 100644 index 0000000000000000000000000000000000000000..9ce3dacf47afa81dbc18bfa14837b5a3ede24e0f --- /dev/null +++ b/www/assets/cars/porsche_gt2rs.glb @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:06e7a8c1106fc77148a46953bcaf0f662f9ae27dc0922bf10484e6fd4af10642 +size 14592228 diff --git a/www/assets/maps/.gitkeep b/www/assets/maps/.gitkeep new file mode 100644 index 0000000000000000000000000000000000000000..a6aeaed5be0512e35ec92d6d7a045da1d1bed353 --- /dev/null +++ b/www/assets/maps/.gitkeep @@ -0,0 +1 @@ +# Drop track/map .glb / .gltf files here diff --git a/www/assets/maps/shanghai/textures/Blocchi _dist_121.png b/www/assets/maps/shanghai/textures/Blocchi _dist_121.png new file mode 100644 index 0000000000000000000000000000000000000000..91c62d0b5425ddf556d33fe8b1ec5f1b0d5216ef Binary files /dev/null and b/www/assets/maps/shanghai/textures/Blocchi _dist_121.png differ diff --git a/www/assets/maps/shanghai/textures/Emirates_better_11.png b/www/assets/maps/shanghai/textures/Emirates_better_11.png new file mode 100644 index 0000000000000000000000000000000000000000..3ceae9ae4b32fcab1dff62e3254b16de976ae10f Binary files /dev/null and b/www/assets/maps/shanghai/textures/Emirates_better_11.png differ diff --git a/www/assets/maps/shanghai/textures/Gru_mobole_27.png b/www/assets/maps/shanghai/textures/Gru_mobole_27.png new file mode 100644 index 0000000000000000000000000000000000000000..38d94bf1d64bd21e28ec7624c9c3db8f0ca17ce3 Binary files /dev/null and b/www/assets/maps/shanghai/textures/Gru_mobole_27.png differ diff --git a/www/assets/maps/shanghai/textures/Heine_no_14.png b/www/assets/maps/shanghai/textures/Heine_no_14.png new file mode 100644 index 0000000000000000000000000000000000000000..bf9680134a2a66db3c281772a2c8d032a9c73288 Binary files /dev/null and b/www/assets/maps/shanghai/textures/Heine_no_14.png differ diff --git a/www/assets/maps/shanghai/textures/Meshesbldgobjconcrete11_diff_61.png b/www/assets/maps/shanghai/textures/Meshesbldgobjconcrete11_diff_61.png new file mode 100644 index 0000000000000000000000000000000000000000..53f507df47f6c231084254a7f0d88591149acfd7 Binary files /dev/null and b/www/assets/maps/shanghai/textures/Meshesbldgobjconcrete11_diff_61.png differ diff --git a/www/assets/maps/shanghai/textures/Meshesgrassfldgrass0541_diff_24.png b/www/assets/maps/shanghai/textures/Meshesgrassfldgrass0541_diff_24.png new file mode 100644 index 0000000000000000000000000000000000000000..22a37244834b5f9c63e92752ba7f06458456995b Binary files /dev/null and b/www/assets/maps/shanghai/textures/Meshesgrassfldgrass0541_diff_24.png differ diff --git a/www/assets/maps/shanghai/textures/New_line_PIT_126.png b/www/assets/maps/shanghai/textures/New_line_PIT_126.png new file mode 100644 index 0000000000000000000000000000000000000000..b6ce979d469485df1219229f46c27d10ebab455a Binary files /dev/null and b/www/assets/maps/shanghai/textures/New_line_PIT_126.png differ diff --git a/www/assets/maps/shanghai/textures/PAT_Einak_basso_15.png b/www/assets/maps/shanghai/textures/PAT_Einak_basso_15.png new file mode 100644 index 0000000000000000000000000000000000000000..be7bbbfad2cfec481e389bdf2a2f2b5d4ce205c7 Binary files /dev/null and b/www/assets/maps/shanghai/textures/PAT_Einak_basso_15.png differ diff --git a/www/assets/maps/shanghai/textures/PAT_asf_out_123.png b/www/assets/maps/shanghai/textures/PAT_asf_out_123.png new file mode 100644 index 0000000000000000000000000000000000000000..0f797f3f557c964afe3c3c9c784de22e43e12b03 --- /dev/null +++ b/www/assets/maps/shanghai/textures/PAT_asf_out_123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8f3c4375cfa6f28d515490188c6ba2d55c71bf1e2e9d5ede947071928b23257e +size 146897 diff --git a/www/assets/maps/shanghai/textures/PAT_gril_113.png b/www/assets/maps/shanghai/textures/PAT_gril_113.png new file mode 100644 index 0000000000000000000000000000000000000000..b527535934823643c1478e01060a0964dfdcf47c Binary files /dev/null and b/www/assets/maps/shanghai/textures/PAT_gril_113.png differ diff --git a/www/assets/maps/shanghai/textures/PAT_rug_blu_112.png b/www/assets/maps/shanghai/textures/PAT_rug_blu_112.png new file mode 100644 index 0000000000000000000000000000000000000000..024863308e2f6bcf05c5c75ac24954a7566e7fb2 --- /dev/null +++ b/www/assets/maps/shanghai/textures/PAT_rug_blu_112.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9201afd3c9283aec8f9ad8b53f050251c49c37a94cde7383933c7d8ba4e32e34 +size 186171 diff --git a/www/assets/maps/shanghai/textures/PAT_special_111.png b/www/assets/maps/shanghai/textures/PAT_special_111.png new file mode 100644 index 0000000000000000000000000000000000000000..93d29381b2a9edb6da4f7edfdd96c3a23ea02af9 Binary files /dev/null and b/www/assets/maps/shanghai/textures/PAT_special_111.png differ diff --git a/www/assets/maps/shanghai/textures/Petronas_sign_cina_17.png b/www/assets/maps/shanghai/textures/Petronas_sign_cina_17.png new file mode 100644 index 0000000000000000000000000000000000000000..14d4ae4d390089daad9528a3666fae0dda4b4d3e Binary files /dev/null and b/www/assets/maps/shanghai/textures/Petronas_sign_cina_17.png differ diff --git a/www/assets/maps/shanghai/textures/Pirelli_pan_12.png b/www/assets/maps/shanghai/textures/Pirelli_pan_12.png new file mode 100644 index 0000000000000000000000000000000000000000..2ffdc85a51de1f00f0a55e3778297de93ede8aed Binary files /dev/null and b/www/assets/maps/shanghai/textures/Pirelli_pan_12.png differ diff --git a/www/assets/maps/shanghai/textures/advert_side_a_d_56.png b/www/assets/maps/shanghai/textures/advert_side_a_d_56.png new file mode 100644 index 0000000000000000000000000000000000000000..4607d1cd318da73ba8639924a9a3c31c637867b0 --- /dev/null +++ b/www/assets/maps/shanghai/textures/advert_side_a_d_56.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:91d5b2af9c6eef1cd76db5b210e3cef8088ff17e13ce263a53945be5ca2689fd +size 111794 diff --git a/www/assets/maps/shanghai/textures/allianz_board_a_143.png b/www/assets/maps/shanghai/textures/allianz_board_a_143.png new file mode 100644 index 0000000000000000000000000000000000000000..ebee8c1561d76b733c52818c64b82397ed832fa8 Binary files /dev/null and b/www/assets/maps/shanghai/textures/allianz_board_a_143.png differ diff --git a/www/assets/maps/shanghai/textures/armco_grey_main_a_01_d_38.png b/www/assets/maps/shanghai/textures/armco_grey_main_a_01_d_38.png new file mode 100644 index 0000000000000000000000000000000000000000..a42be3193704595a0daee1665cd0a54225ca341c Binary files /dev/null and b/www/assets/maps/shanghai/textures/armco_grey_main_a_01_d_38.png differ diff --git a/www/assets/maps/shanghai/textures/asphalt-new.png b/www/assets/maps/shanghai/textures/asphalt-new.png new file mode 100644 index 0000000000000000000000000000000000000000..70f5a3784e2988fe256915baec6ee347ff978488 --- /dev/null +++ b/www/assets/maps/shanghai/textures/asphalt-new.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b9cf557a355f8616e2b3f708f752f11d8633427780aa8b8e0a26cdbd895901f0 +size 2988813 diff --git a/www/assets/maps/shanghai/textures/conc_bollard_filler_a_01_d_39.png b/www/assets/maps/shanghai/textures/conc_bollard_filler_a_01_d_39.png new file mode 100644 index 0000000000000000000000000000000000000000..a3a964c306e233fac96ced491ffcf2919e19f6c3 --- /dev/null +++ b/www/assets/maps/shanghai/textures/conc_bollard_filler_a_01_d_39.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5a9d2c20469838407280f40016475149b8f21cc9e5d364e4b4090b04a6cd15f2 +size 115981 diff --git a/www/assets/maps/shanghai/textures/distance_assets_outfield_d_79.png b/www/assets/maps/shanghai/textures/distance_assets_outfield_d_79.png new file mode 100644 index 0000000000000000000000000000000000000000..b44038516ef048fbd26d42091f5df469e26f6222 Binary files /dev/null and b/www/assets/maps/shanghai/textures/distance_assets_outfield_d_79.png differ diff --git a/www/assets/maps/shanghai/textures/distance_assets_paddock_d_80.png b/www/assets/maps/shanghai/textures/distance_assets_paddock_d_80.png new file mode 100644 index 0000000000000000000000000000000000000000..f25e324f92e40c5d1fbc1d247ea75378254cced0 Binary files /dev/null and b/www/assets/maps/shanghai/textures/distance_assets_paddock_d_80.png differ diff --git a/www/assets/maps/shanghai/textures/f1_board_a_120.png b/www/assets/maps/shanghai/textures/f1_board_a_120.png new file mode 100644 index 0000000000000000000000000000000000000000..4cf380e67a13070667996cd6e722d10da6552ab7 Binary files /dev/null and b/www/assets/maps/shanghai/textures/f1_board_a_120.png differ diff --git a/www/assets/maps/shanghai/textures/f1_board_b_16.png b/www/assets/maps/shanghai/textures/f1_board_b_16.png new file mode 100644 index 0000000000000000000000000000000000000000..d3f2ac5450554df14690ad087a6080a91d9792b6 Binary files /dev/null and b/www/assets/maps/shanghai/textures/f1_board_b_16.png differ diff --git a/www/assets/maps/shanghai/textures/fence_barbed_b_01_d_134.png b/www/assets/maps/shanghai/textures/fence_barbed_b_01_d_134.png new file mode 100644 index 0000000000000000000000000000000000000000..d8ce848d8ef190f0aea8f66453fdbdc7480131f1 Binary files /dev/null and b/www/assets/maps/shanghai/textures/fence_barbed_b_01_d_134.png differ diff --git a/www/assets/maps/shanghai/textures/fly_emirates_terrain_a_89.png b/www/assets/maps/shanghai/textures/fly_emirates_terrain_a_89.png new file mode 100644 index 0000000000000000000000000000000000000000..8830cfba96ab3f9cb0354870fa1b35ecbb56cc43 Binary files /dev/null and b/www/assets/maps/shanghai/textures/fly_emirates_terrain_a_89.png differ diff --git a/www/assets/maps/shanghai/textures/grass-detailrealistic.png b/www/assets/maps/shanghai/textures/grass-detailrealistic.png new file mode 100644 index 0000000000000000000000000000000000000000..0a40c01bef23d0d61146636ea7e3948d7a86a5c7 --- /dev/null +++ b/www/assets/maps/shanghai/textures/grass-detailrealistic.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9a733d0022596f5771eac670b11e9fd5b5a2adc6c7c1ba57e2cc7cdbabe5d443 +size 5541911 diff --git a/www/assets/maps/shanghai/textures/groove01_24.png b/www/assets/maps/shanghai/textures/groove01_24.png new file mode 100644 index 0000000000000000000000000000000000000000..644d28c4d32d6bf77f5a74c7c0820d7815046b53 Binary files /dev/null and b/www/assets/maps/shanghai/textures/groove01_24.png differ diff --git a/www/assets/maps/shanghai/textures/jumbotron_b_01_d_82.png b/www/assets/maps/shanghai/textures/jumbotron_b_01_d_82.png new file mode 100644 index 0000000000000000000000000000000000000000..f49aa5262a6c00fc7049902058e6ea14c22c4197 Binary files /dev/null and b/www/assets/maps/shanghai/textures/jumbotron_b_01_d_82.png differ diff --git a/www/assets/maps/shanghai/textures/lg_board_a_51.png b/www/assets/maps/shanghai/textures/lg_board_a_51.png new file mode 100644 index 0000000000000000000000000000000000000000..29f4565fe6d613121d3e2b8f5c854fc2ee425043 Binary files /dev/null and b/www/assets/maps/shanghai/textures/lg_board_a_51.png differ diff --git a/www/assets/maps/shanghai/textures/marquee_truck_a_02_d_74.png b/www/assets/maps/shanghai/textures/marquee_truck_a_02_d_74.png new file mode 100644 index 0000000000000000000000000000000000000000..7f9eeddb284d771cc07209f04009f8bfe5f574eb Binary files /dev/null and b/www/assets/maps/shanghai/textures/marquee_truck_a_02_d_74.png differ diff --git a/www/assets/maps/shanghai/textures/nuove barriere_19.png b/www/assets/maps/shanghai/textures/nuove barriere_19.png new file mode 100644 index 0000000000000000000000000000000000000000..8942a7de4b9f50733edbf6ad57e87f3c9f4ec41c Binary files /dev/null and b/www/assets/maps/shanghai/textures/nuove barriere_19.png differ diff --git a/www/assets/maps/shanghai/textures/rolex_board_a_6.png b/www/assets/maps/shanghai/textures/rolex_board_a_6.png new file mode 100644 index 0000000000000000000000000000000000000000..dc408b595ec3934f50ba4f1c297dc3d14fcb302a Binary files /dev/null and b/www/assets/maps/shanghai/textures/rolex_board_a_6.png differ diff --git a/www/assets/maps/shanghai/textures/rolex_board_b_13.png b/www/assets/maps/shanghai/textures/rolex_board_b_13.png new file mode 100644 index 0000000000000000000000000000000000000000..268b7e6442c31bb582a337d39f426506c0c42182 Binary files /dev/null and b/www/assets/maps/shanghai/textures/rolex_board_b_13.png differ diff --git a/www/assets/maps/shanghai/textures/rolex_clock_hd_21.png b/www/assets/maps/shanghai/textures/rolex_clock_hd_21.png new file mode 100644 index 0000000000000000000000000000000000000000..65fe0ba1fb50bcc323ad0753808c51e8da678821 Binary files /dev/null and b/www/assets/maps/shanghai/textures/rolex_clock_hd_21.png differ diff --git a/www/assets/maps/shanghai/textures/rolex_terrain_a_142.png b/www/assets/maps/shanghai/textures/rolex_terrain_a_142.png new file mode 100644 index 0000000000000000000000000000000000000000..e8b71e3582c4751f1faf2643c0502287e58be901 Binary files /dev/null and b/www/assets/maps/shanghai/textures/rolex_terrain_a_142.png differ diff --git a/www/assets/maps/shanghai/textures/sha_aqueduct_a_01_d_92.png b/www/assets/maps/shanghai/textures/sha_aqueduct_a_01_d_92.png new file mode 100644 index 0000000000000000000000000000000000000000..63be390a3b77c8d63cfd667a20311a2dd67669ca Binary files /dev/null and b/www/assets/maps/shanghai/textures/sha_aqueduct_a_01_d_92.png differ diff --git a/www/assets/maps/shanghai/textures/sha_banner_greyframe_a_d_57.png b/www/assets/maps/shanghai/textures/sha_banner_greyframe_a_d_57.png new file mode 100644 index 0000000000000000000000000000000000000000..4514d39384d0b9b6a5e7cbbc6a3aa42b8f8869ec Binary files /dev/null and b/www/assets/maps/shanghai/textures/sha_banner_greyframe_a_d_57.png differ diff --git a/www/assets/maps/shanghai/textures/sha_banner_onastick_a_d_78.png b/www/assets/maps/shanghai/textures/sha_banner_onastick_a_d_78.png new file mode 100644 index 0000000000000000000000000000000000000000..eccbeb7f8a175c74a65f7735ddefa30997f418c8 Binary files /dev/null and b/www/assets/maps/shanghai/textures/sha_banner_onastick_a_d_78.png differ diff --git a/www/assets/maps/shanghai/textures/sha_banner_startfinish_a_d_59.png b/www/assets/maps/shanghai/textures/sha_banner_startfinish_a_d_59.png new file mode 100644 index 0000000000000000000000000000000000000000..98b13eae026786ac1d72f96f6fe8572b38daf56b Binary files /dev/null and b/www/assets/maps/shanghai/textures/sha_banner_startfinish_a_d_59.png differ diff --git a/www/assets/maps/shanghai/textures/sha_barrier_armcostubyellow_a_d_47.png b/www/assets/maps/shanghai/textures/sha_barrier_armcostubyellow_a_d_47.png new file mode 100644 index 0000000000000000000000000000000000000000..3f2d209e0325106022a9cbd33e762ba372eebf22 Binary files /dev/null and b/www/assets/maps/shanghai/textures/sha_barrier_armcostubyellow_a_d_47.png differ diff --git a/www/assets/maps/shanghai/textures/sha_barrier_barbfence_a_01_d_28.png b/www/assets/maps/shanghai/textures/sha_barrier_barbfence_a_01_d_28.png new file mode 100644 index 0000000000000000000000000000000000000000..a74a95d3a71091e6f04470abed4a859a759bbea6 Binary files /dev/null and b/www/assets/maps/shanghai/textures/sha_barrier_barbfence_a_01_d_28.png differ diff --git a/www/assets/maps/shanghai/textures/sha_barrier_barbfence_a_02_d_42.png b/www/assets/maps/shanghai/textures/sha_barrier_barbfence_a_02_d_42.png new file mode 100644 index 0000000000000000000000000000000000000000..ad8e52df80a39a62c0d57ee240fe31a3328dfe2d Binary files /dev/null and b/www/assets/maps/shanghai/textures/sha_barrier_barbfence_a_02_d_42.png differ diff --git a/www/assets/maps/shanghai/textures/sha_barrier_pitwall_a_d_45.png b/www/assets/maps/shanghai/textures/sha_barrier_pitwall_a_d_45.png new file mode 100644 index 0000000000000000000000000000000000000000..9b6794c3237ab8837a55b50f8cab4e96e20193f8 Binary files /dev/null and b/www/assets/maps/shanghai/textures/sha_barrier_pitwall_a_d_45.png differ diff --git a/www/assets/maps/shanghai/textures/sha_brick_a_01_d_93.png b/www/assets/maps/shanghai/textures/sha_brick_a_01_d_93.png new file mode 100644 index 0000000000000000000000000000000000000000..5b59a8b4e93753bfaa70a7cfd24119e478b7ed45 Binary files /dev/null and b/www/assets/maps/shanghai/textures/sha_brick_a_01_d_93.png differ diff --git a/www/assets/maps/shanghai/textures/sha_building_commsplex_a_d_30.png b/www/assets/maps/shanghai/textures/sha_building_commsplex_a_d_30.png new file mode 100644 index 0000000000000000000000000000000000000000..fd01f33e1907474d8b2182618a6bce8a497c749c Binary files /dev/null and b/www/assets/maps/shanghai/textures/sha_building_commsplex_a_d_30.png differ diff --git a/www/assets/maps/shanghai/textures/sha_building_commstower_a_d_31.png b/www/assets/maps/shanghai/textures/sha_building_commstower_a_d_31.png new file mode 100644 index 0000000000000000000000000000000000000000..0786d6e71603c733937d6097b4f3d43ea592229e Binary files /dev/null and b/www/assets/maps/shanghai/textures/sha_building_commstower_a_d_31.png differ diff --git a/www/assets/maps/shanghai/textures/sha_building_curvedroof_a_d_54.png b/www/assets/maps/shanghai/textures/sha_building_curvedroof_a_d_54.png new file mode 100644 index 0000000000000000000000000000000000000000..188f0540b1e7fe705bf5d1aeeced15887eff391c Binary files /dev/null and b/www/assets/maps/shanghai/textures/sha_building_curvedroof_a_d_54.png differ diff --git a/www/assets/maps/shanghai/textures/sha_building_glasstower_a_02_d_4.png b/www/assets/maps/shanghai/textures/sha_building_glasstower_a_02_d_4.png new file mode 100644 index 0000000000000000000000000000000000000000..17d11ef2a7c8defbb1a464df7325ed848104b6ae Binary files /dev/null and b/www/assets/maps/shanghai/textures/sha_building_glasstower_a_02_d_4.png differ diff --git a/www/assets/maps/shanghai/textures/sha_building_glasstunnel_a_d_32.png b/www/assets/maps/shanghai/textures/sha_building_glasstunnel_a_d_32.png new file mode 100644 index 0000000000000000000000000000000000000000..6fb2a08000828a7e4689f9da8b62452edb786ade Binary files /dev/null and b/www/assets/maps/shanghai/textures/sha_building_glasstunnel_a_d_32.png differ diff --git a/www/assets/maps/shanghai/textures/sha_building_kartcenter_a_d_33.png b/www/assets/maps/shanghai/textures/sha_building_kartcenter_a_d_33.png new file mode 100644 index 0000000000000000000000000000000000000000..5146daf4a163710f629ab5d7589619c713e352cf Binary files /dev/null and b/www/assets/maps/shanghai/textures/sha_building_kartcenter_a_d_33.png differ diff --git a/www/assets/maps/shanghai/textures/sha_building_pitgarages_a_01_d_114.png b/www/assets/maps/shanghai/textures/sha_building_pitgarages_a_01_d_114.png new file mode 100644 index 0000000000000000000000000000000000000000..d61bc75246d8692cbca0bbedeb016dd149046a65 Binary files /dev/null and b/www/assets/maps/shanghai/textures/sha_building_pitgarages_a_01_d_114.png differ diff --git a/www/assets/maps/shanghai/textures/sha_building_pitgarages_a_02_d_46.png b/www/assets/maps/shanghai/textures/sha_building_pitgarages_a_02_d_46.png new file mode 100644 index 0000000000000000000000000000000000000000..c22be30887fb1910e0e6c0fde9e350feae842fa6 Binary files /dev/null and b/www/assets/maps/shanghai/textures/sha_building_pitgarages_a_02_d_46.png differ diff --git a/www/assets/maps/shanghai/textures/sha_building_publicwc_a_d_88.png b/www/assets/maps/shanghai/textures/sha_building_publicwc_a_d_88.png new file mode 100644 index 0000000000000000000000000000000000000000..7d21cd148ff7c98279a262676d53a5fa533c3295 Binary files /dev/null and b/www/assets/maps/shanghai/textures/sha_building_publicwc_a_d_88.png differ diff --git a/www/assets/maps/shanghai/textures/sha_building_skybridge_a_d_34.png b/www/assets/maps/shanghai/textures/sha_building_skybridge_a_d_34.png new file mode 100644 index 0000000000000000000000000000000000000000..c43c4a372e88de7642d4cb5837a269edaeadb204 Binary files /dev/null and b/www/assets/maps/shanghai/textures/sha_building_skybridge_a_d_34.png differ diff --git a/www/assets/maps/shanghai/textures/sha_cloaca_a_01_d_94.png b/www/assets/maps/shanghai/textures/sha_cloaca_a_01_d_94.png new file mode 100644 index 0000000000000000000000000000000000000000..3ea5ba8e64f866a3b2dc8bf5593bd3238aef284b Binary files /dev/null and b/www/assets/maps/shanghai/textures/sha_cloaca_a_01_d_94.png differ diff --git a/www/assets/maps/shanghai/textures/sha_colour_seating_a_01_d_132.png b/www/assets/maps/shanghai/textures/sha_colour_seating_a_01_d_132.png new file mode 100644 index 0000000000000000000000000000000000000000..923dc2db60756c3519b27ffd77b3112b3c6bcd4d Binary files /dev/null and b/www/assets/maps/shanghai/textures/sha_colour_seating_a_01_d_132.png differ diff --git a/www/assets/maps/shanghai/textures/sha_grandstand_expo_a_d_9.png b/www/assets/maps/shanghai/textures/sha_grandstand_expo_a_d_9.png new file mode 100644 index 0000000000000000000000000000000000000000..b522414b6afcbdc8275aadf3ce03bccd0bb2292d Binary files /dev/null and b/www/assets/maps/shanghai/textures/sha_grandstand_expo_a_d_9.png differ diff --git a/www/assets/maps/shanghai/textures/sha_grandstand_group_a_d_35.png b/www/assets/maps/shanghai/textures/sha_grandstand_group_a_d_35.png new file mode 100644 index 0000000000000000000000000000000000000000..03e8f09c8f4c52b6be23300822c6a5affe223b1d Binary files /dev/null and b/www/assets/maps/shanghai/textures/sha_grandstand_group_a_d_35.png differ diff --git a/www/assets/maps/shanghai/textures/sha_grandstand_main_a_d_44.png b/www/assets/maps/shanghai/textures/sha_grandstand_main_a_d_44.png new file mode 100644 index 0000000000000000000000000000000000000000..62ebf8d26d1c1660b272e26fa480b5d5bb62eaee Binary files /dev/null and b/www/assets/maps/shanghai/textures/sha_grandstand_main_a_d_44.png differ diff --git a/www/assets/maps/shanghai/textures/sha_grandstand_underbrolly_01_d_43.png b/www/assets/maps/shanghai/textures/sha_grandstand_underbrolly_01_d_43.png new file mode 100644 index 0000000000000000000000000000000000000000..cfee963ba7e257f16d678c6553b5afc656e2d982 Binary files /dev/null and b/www/assets/maps/shanghai/textures/sha_grandstand_underbrolly_01_d_43.png differ diff --git a/www/assets/maps/shanghai/textures/sha_grandstand_underbrolly_02_d_133.png b/www/assets/maps/shanghai/textures/sha_grandstand_underbrolly_02_d_133.png new file mode 100644 index 0000000000000000000000000000000000000000..275e712f32f1fde182d91cc3422add7a7215b567 Binary files /dev/null and b/www/assets/maps/shanghai/textures/sha_grandstand_underbrolly_02_d_133.png differ diff --git a/www/assets/maps/shanghai/textures/sha_hut_marshalls_a_d_63.png b/www/assets/maps/shanghai/textures/sha_hut_marshalls_a_d_63.png new file mode 100644 index 0000000000000000000000000000000000000000..2ee990f399a3c4108addeb5b0997c3e1fed4d978 --- /dev/null +++ b/www/assets/maps/shanghai/textures/sha_hut_marshalls_a_d_63.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:76bbbd6e3ae0622e8a5a553522a2abe0b2fa2f50ac756cd2801de3a57e3813cd +size 105349 diff --git a/www/assets/maps/shanghai/textures/sha_hut_pitlanetower_a_d_77.png b/www/assets/maps/shanghai/textures/sha_hut_pitlanetower_a_d_77.png new file mode 100644 index 0000000000000000000000000000000000000000..74df9f7f26025d013079e8767e66c18c638933ca Binary files /dev/null and b/www/assets/maps/shanghai/textures/sha_hut_pitlanetower_a_d_77.png differ diff --git a/www/assets/maps/shanghai/textures/sha_hut_shabby_a_d_55.png b/www/assets/maps/shanghai/textures/sha_hut_shabby_a_d_55.png new file mode 100644 index 0000000000000000000000000000000000000000..da9e487d08e5f098e3bca2cb925faf8a5f8bee7c Binary files /dev/null and b/www/assets/maps/shanghai/textures/sha_hut_shabby_a_d_55.png differ diff --git a/www/assets/maps/shanghai/textures/sha_infrastructure_arcwall_a_d_116.png b/www/assets/maps/shanghai/textures/sha_infrastructure_arcwall_a_d_116.png new file mode 100644 index 0000000000000000000000000000000000000000..35596b3376a0187a9e22ab9cd48b59f50d46d435 Binary files /dev/null and b/www/assets/maps/shanghai/textures/sha_infrastructure_arcwall_a_d_116.png differ diff --git a/www/assets/maps/shanghai/textures/sha_infrastructure_brollyroof_a_d_37.png b/www/assets/maps/shanghai/textures/sha_infrastructure_brollyroof_a_d_37.png new file mode 100644 index 0000000000000000000000000000000000000000..a8d86a2aa636c9d00eaf0f9b67c66b388bf1a7c3 Binary files /dev/null and b/www/assets/maps/shanghai/textures/sha_infrastructure_brollyroof_a_d_37.png differ diff --git a/www/assets/maps/shanghai/textures/sha_landscape_01_131.png b/www/assets/maps/shanghai/textures/sha_landscape_01_131.png new file mode 100644 index 0000000000000000000000000000000000000000..92354063e2fad08de407fb880ebd92e5bfb7a338 Binary files /dev/null and b/www/assets/maps/shanghai/textures/sha_landscape_01_131.png differ diff --git a/www/assets/maps/shanghai/textures/sha_pole_bafflelight_a_d_64.png b/www/assets/maps/shanghai/textures/sha_pole_bafflelight_a_d_64.png new file mode 100644 index 0000000000000000000000000000000000000000..93d58d975cdb1296c0cf7664ba50cc36ed463bf0 Binary files /dev/null and b/www/assets/maps/shanghai/textures/sha_pole_bafflelight_a_d_64.png differ diff --git a/www/assets/maps/shanghai/textures/sha_pole_ranking_a_d_81.png b/www/assets/maps/shanghai/textures/sha_pole_ranking_a_d_81.png new file mode 100644 index 0000000000000000000000000000000000000000..5aea99999a73fa0c89740f03b31431c356eaf456 Binary files /dev/null and b/www/assets/maps/shanghai/textures/sha_pole_ranking_a_d_81.png differ diff --git a/www/assets/maps/shanghai/textures/sha_rug_a_01_d_98.png b/www/assets/maps/shanghai/textures/sha_rug_a_01_d_98.png new file mode 100644 index 0000000000000000000000000000000000000000..df1bfa444152b5af8a5df01b6b589b51696d727b --- /dev/null +++ b/www/assets/maps/shanghai/textures/sha_rug_a_01_d_98.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dc6eee9dda8dcf0b1ff82fb206aff3d13d1fa33827115052e10c920002eb208d +size 204992 diff --git a/www/assets/maps/shanghai/textures/sha_rumble_a_01_d_99.png b/www/assets/maps/shanghai/textures/sha_rumble_a_01_d_99.png new file mode 100644 index 0000000000000000000000000000000000000000..c046590abdb8a0db095be708f6a93d1b80aeb3f2 Binary files /dev/null and b/www/assets/maps/shanghai/textures/sha_rumble_a_01_d_99.png differ diff --git a/www/assets/maps/shanghai/textures/sha_sand_a_01_d_139.png b/www/assets/maps/shanghai/textures/sha_sand_a_01_d_139.png new file mode 100644 index 0000000000000000000000000000000000000000..400128725c28610f7b4abdd791a41ca1b6dca124 --- /dev/null +++ b/www/assets/maps/shanghai/textures/sha_sand_a_01_d_139.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6147592d7e3ea3e42cb9a8bdfec5618f63190fc3db303c8e384b2e94c5e5a0ea +size 164311 diff --git a/www/assets/maps/shanghai/textures/sha_sand_a_02_d_140.png b/www/assets/maps/shanghai/textures/sha_sand_a_02_d_140.png new file mode 100644 index 0000000000000000000000000000000000000000..43af529c9d8884e0bb5614c4c1a0bcb201010089 Binary files /dev/null and b/www/assets/maps/shanghai/textures/sha_sand_a_02_d_140.png differ diff --git a/www/assets/maps/shanghai/textures/sha_tent_hospitality_a_d_36.png b/www/assets/maps/shanghai/textures/sha_tent_hospitality_a_d_36.png new file mode 100644 index 0000000000000000000000000000000000000000..bf133cbfd523bd7bfed20365c8c39635c8516154 Binary files /dev/null and b/www/assets/maps/shanghai/textures/sha_tent_hospitality_a_d_36.png differ diff --git a/www/assets/maps/shanghai/textures/sha_transition_a_01_d_106.png b/www/assets/maps/shanghai/textures/sha_transition_a_01_d_106.png new file mode 100644 index 0000000000000000000000000000000000000000..1e74e056f503c11d454101c4dd2da05031857c16 --- /dev/null +++ b/www/assets/maps/shanghai/textures/sha_transition_a_01_d_106.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c34d3cb058d94840b3310d82c66918e3f598b134dd82976591dbe91cd4b5b73d +size 100315 diff --git a/www/assets/maps/shanghai/textures/sha_wall_a_01_d_107.png b/www/assets/maps/shanghai/textures/sha_wall_a_01_d_107.png new file mode 100644 index 0000000000000000000000000000000000000000..410a3e4bb1f0db6a7cc219606670100edeccb75e Binary files /dev/null and b/www/assets/maps/shanghai/textures/sha_wall_a_01_d_107.png differ diff --git a/www/assets/maps/shanghai/textures/sha_wall_a_02_d_108.png b/www/assets/maps/shanghai/textures/sha_wall_a_02_d_108.png new file mode 100644 index 0000000000000000000000000000000000000000..39a1e9d8a3824d9f66200b69bfa1b9d444c3a9a1 Binary files /dev/null and b/www/assets/maps/shanghai/textures/sha_wall_a_02_d_108.png differ diff --git a/www/assets/maps/shanghai/textures/sha_wall_a_03_d_109.png b/www/assets/maps/shanghai/textures/sha_wall_a_03_d_109.png new file mode 100644 index 0000000000000000000000000000000000000000..80b7242e4b09364c6bc521f370a51862fb9dceb0 Binary files /dev/null and b/www/assets/maps/shanghai/textures/sha_wall_a_03_d_109.png differ diff --git a/www/assets/maps/shanghai/textures/sha_water_a_01_d_110.png b/www/assets/maps/shanghai/textures/sha_water_a_01_d_110.png new file mode 100644 index 0000000000000000000000000000000000000000..553b2777c7f29276d5e5e24112b5db9cacc48ea6 Binary files /dev/null and b/www/assets/maps/shanghai/textures/sha_water_a_01_d_110.png differ diff --git a/www/assets/maps/shanghai/textures/sha_whiteline_a_01_d_135.png b/www/assets/maps/shanghai/textures/sha_whiteline_a_01_d_135.png new file mode 100644 index 0000000000000000000000000000000000000000..d839b134b385e2cad8e5f16971f84d8918879c9b Binary files /dev/null and b/www/assets/maps/shanghai/textures/sha_whiteline_a_01_d_135.png differ diff --git a/www/assets/maps/shanghai/textures/skid_marks_a_01_d_22.png b/www/assets/maps/shanghai/textures/skid_marks_a_01_d_22.png new file mode 100644 index 0000000000000000000000000000000000000000..6f036e70cdc9d0a74e95bd2556f3f9522d03c8b0 Binary files /dev/null and b/www/assets/maps/shanghai/textures/skid_marks_a_01_d_22.png differ diff --git a/www/assets/maps/shanghai/textures/sponsor_backboard_01_52.png b/www/assets/maps/shanghai/textures/sponsor_backboard_01_52.png new file mode 100644 index 0000000000000000000000000000000000000000..f87a943769bb80d66a321ec81628eebd29f5b6ec Binary files /dev/null and b/www/assets/maps/shanghai/textures/sponsor_backboard_01_52.png differ diff --git a/www/assets/maps/shanghai/textures/sponsor_backboard_02_48.png b/www/assets/maps/shanghai/textures/sponsor_backboard_02_48.png new file mode 100644 index 0000000000000000000000000000000000000000..8a8a1f0e866162cad1f3010fd7e51a4411f56b80 --- /dev/null +++ b/www/assets/maps/shanghai/textures/sponsor_backboard_02_48.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9e881f57f2aaaa3728e777ead3386b90b11d35b016f2c0f2cadc4c3b7ef035fb +size 110239 diff --git a/www/assets/maps/shanghai/textures/sponsor_backframe_01_d_53.png b/www/assets/maps/shanghai/textures/sponsor_backframe_01_d_53.png new file mode 100644 index 0000000000000000000000000000000000000000..fc5e9f0b03088b36f185647ba5bdb1a04a0114f3 Binary files /dev/null and b/www/assets/maps/shanghai/textures/sponsor_backframe_01_d_53.png differ diff --git a/www/assets/maps/shanghai/textures/sponsorx_cats_varbv_d_41.png b/www/assets/maps/shanghai/textures/sponsorx_cats_varbv_d_41.png new file mode 100644 index 0000000000000000000000000000000000000000..b78437c3c51fc7d7bbf7b2d2d2e192e81f18e4a2 Binary files /dev/null and b/www/assets/maps/shanghai/textures/sponsorx_cats_varbv_d_41.png differ diff --git a/www/assets/maps/shanghai/textures/sponsorx_cats_varca_d_40.png b/www/assets/maps/shanghai/textures/sponsorx_cats_varca_d_40.png new file mode 100644 index 0000000000000000000000000000000000000000..2d8e65e5e673284ad007bda3607672e709be6b0c Binary files /dev/null and b/www/assets/maps/shanghai/textures/sponsorx_cats_varca_d_40.png differ diff --git a/www/assets/maps/shanghai/textures/telo_1000_7.png b/www/assets/maps/shanghai/textures/telo_1000_7.png new file mode 100644 index 0000000000000000000000000000000000000000..311f9c9219ac63dea5354f809869e40e9fd7dd29 Binary files /dev/null and b/www/assets/maps/shanghai/textures/telo_1000_7.png differ diff --git a/www/assets/maps/shanghai/textures/textures/Blocchi _dist_121.png b/www/assets/maps/shanghai/textures/textures/Blocchi _dist_121.png new file mode 100644 index 0000000000000000000000000000000000000000..91c62d0b5425ddf556d33fe8b1ec5f1b0d5216ef Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/Blocchi _dist_121.png differ diff --git a/www/assets/maps/shanghai/textures/textures/Emirates_better_11.png b/www/assets/maps/shanghai/textures/textures/Emirates_better_11.png new file mode 100644 index 0000000000000000000000000000000000000000..3ceae9ae4b32fcab1dff62e3254b16de976ae10f Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/Emirates_better_11.png differ diff --git a/www/assets/maps/shanghai/textures/textures/Gru_mobole_27.png b/www/assets/maps/shanghai/textures/textures/Gru_mobole_27.png new file mode 100644 index 0000000000000000000000000000000000000000..38d94bf1d64bd21e28ec7624c9c3db8f0ca17ce3 Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/Gru_mobole_27.png differ diff --git a/www/assets/maps/shanghai/textures/textures/Heine_no_14.png b/www/assets/maps/shanghai/textures/textures/Heine_no_14.png new file mode 100644 index 0000000000000000000000000000000000000000..bf9680134a2a66db3c281772a2c8d032a9c73288 Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/Heine_no_14.png differ diff --git a/www/assets/maps/shanghai/textures/textures/Meshesbldgobjconcrete11_diff_61.png b/www/assets/maps/shanghai/textures/textures/Meshesbldgobjconcrete11_diff_61.png new file mode 100644 index 0000000000000000000000000000000000000000..53f507df47f6c231084254a7f0d88591149acfd7 Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/Meshesbldgobjconcrete11_diff_61.png differ diff --git a/www/assets/maps/shanghai/textures/textures/Meshesgrassfldgrass0541_diff_24.png b/www/assets/maps/shanghai/textures/textures/Meshesgrassfldgrass0541_diff_24.png new file mode 100644 index 0000000000000000000000000000000000000000..22a37244834b5f9c63e92752ba7f06458456995b Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/Meshesgrassfldgrass0541_diff_24.png differ diff --git a/www/assets/maps/shanghai/textures/textures/New_line_PIT_126.png b/www/assets/maps/shanghai/textures/textures/New_line_PIT_126.png new file mode 100644 index 0000000000000000000000000000000000000000..b6ce979d469485df1219229f46c27d10ebab455a Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/New_line_PIT_126.png differ diff --git a/www/assets/maps/shanghai/textures/textures/PAT_Einak_basso_15.png b/www/assets/maps/shanghai/textures/textures/PAT_Einak_basso_15.png new file mode 100644 index 0000000000000000000000000000000000000000..be7bbbfad2cfec481e389bdf2a2f2b5d4ce205c7 Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/PAT_Einak_basso_15.png differ diff --git a/www/assets/maps/shanghai/textures/textures/PAT_asf_out_123.png b/www/assets/maps/shanghai/textures/textures/PAT_asf_out_123.png new file mode 100644 index 0000000000000000000000000000000000000000..0f797f3f557c964afe3c3c9c784de22e43e12b03 --- /dev/null +++ b/www/assets/maps/shanghai/textures/textures/PAT_asf_out_123.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8f3c4375cfa6f28d515490188c6ba2d55c71bf1e2e9d5ede947071928b23257e +size 146897 diff --git a/www/assets/maps/shanghai/textures/textures/PAT_gril_113.png b/www/assets/maps/shanghai/textures/textures/PAT_gril_113.png new file mode 100644 index 0000000000000000000000000000000000000000..b527535934823643c1478e01060a0964dfdcf47c Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/PAT_gril_113.png differ diff --git a/www/assets/maps/shanghai/textures/textures/PAT_rug_blu_112.png b/www/assets/maps/shanghai/textures/textures/PAT_rug_blu_112.png new file mode 100644 index 0000000000000000000000000000000000000000..024863308e2f6bcf05c5c75ac24954a7566e7fb2 --- /dev/null +++ b/www/assets/maps/shanghai/textures/textures/PAT_rug_blu_112.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9201afd3c9283aec8f9ad8b53f050251c49c37a94cde7383933c7d8ba4e32e34 +size 186171 diff --git a/www/assets/maps/shanghai/textures/textures/PAT_special_111.png b/www/assets/maps/shanghai/textures/textures/PAT_special_111.png new file mode 100644 index 0000000000000000000000000000000000000000..93d29381b2a9edb6da4f7edfdd96c3a23ea02af9 Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/PAT_special_111.png differ diff --git a/www/assets/maps/shanghai/textures/textures/Petronas_sign_cina_17.png b/www/assets/maps/shanghai/textures/textures/Petronas_sign_cina_17.png new file mode 100644 index 0000000000000000000000000000000000000000..14d4ae4d390089daad9528a3666fae0dda4b4d3e Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/Petronas_sign_cina_17.png differ diff --git a/www/assets/maps/shanghai/textures/textures/Pirelli_pan_12.png b/www/assets/maps/shanghai/textures/textures/Pirelli_pan_12.png new file mode 100644 index 0000000000000000000000000000000000000000..2ffdc85a51de1f00f0a55e3778297de93ede8aed Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/Pirelli_pan_12.png differ diff --git a/www/assets/maps/shanghai/textures/textures/advert_side_a_d_56.png b/www/assets/maps/shanghai/textures/textures/advert_side_a_d_56.png new file mode 100644 index 0000000000000000000000000000000000000000..4607d1cd318da73ba8639924a9a3c31c637867b0 --- /dev/null +++ b/www/assets/maps/shanghai/textures/textures/advert_side_a_d_56.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:91d5b2af9c6eef1cd76db5b210e3cef8088ff17e13ce263a53945be5ca2689fd +size 111794 diff --git a/www/assets/maps/shanghai/textures/textures/allianz_board_a_143.png b/www/assets/maps/shanghai/textures/textures/allianz_board_a_143.png new file mode 100644 index 0000000000000000000000000000000000000000..ebee8c1561d76b733c52818c64b82397ed832fa8 Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/allianz_board_a_143.png differ diff --git a/www/assets/maps/shanghai/textures/textures/armco_grey_main_a_01_d_38.png b/www/assets/maps/shanghai/textures/textures/armco_grey_main_a_01_d_38.png new file mode 100644 index 0000000000000000000000000000000000000000..a42be3193704595a0daee1665cd0a54225ca341c Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/armco_grey_main_a_01_d_38.png differ diff --git a/www/assets/maps/shanghai/textures/textures/asphalt-new.png b/www/assets/maps/shanghai/textures/textures/asphalt-new.png new file mode 100644 index 0000000000000000000000000000000000000000..70f5a3784e2988fe256915baec6ee347ff978488 --- /dev/null +++ b/www/assets/maps/shanghai/textures/textures/asphalt-new.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b9cf557a355f8616e2b3f708f752f11d8633427780aa8b8e0a26cdbd895901f0 +size 2988813 diff --git a/www/assets/maps/shanghai/textures/textures/conc_bollard_filler_a_01_d_39.png b/www/assets/maps/shanghai/textures/textures/conc_bollard_filler_a_01_d_39.png new file mode 100644 index 0000000000000000000000000000000000000000..a3a964c306e233fac96ced491ffcf2919e19f6c3 --- /dev/null +++ b/www/assets/maps/shanghai/textures/textures/conc_bollard_filler_a_01_d_39.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5a9d2c20469838407280f40016475149b8f21cc9e5d364e4b4090b04a6cd15f2 +size 115981 diff --git a/www/assets/maps/shanghai/textures/textures/distance_assets_outfield_d_79.png b/www/assets/maps/shanghai/textures/textures/distance_assets_outfield_d_79.png new file mode 100644 index 0000000000000000000000000000000000000000..b44038516ef048fbd26d42091f5df469e26f6222 Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/distance_assets_outfield_d_79.png differ diff --git a/www/assets/maps/shanghai/textures/textures/distance_assets_paddock_d_80.png b/www/assets/maps/shanghai/textures/textures/distance_assets_paddock_d_80.png new file mode 100644 index 0000000000000000000000000000000000000000..f25e324f92e40c5d1fbc1d247ea75378254cced0 Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/distance_assets_paddock_d_80.png differ diff --git a/www/assets/maps/shanghai/textures/textures/f1_board_a_120.png b/www/assets/maps/shanghai/textures/textures/f1_board_a_120.png new file mode 100644 index 0000000000000000000000000000000000000000..4cf380e67a13070667996cd6e722d10da6552ab7 Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/f1_board_a_120.png differ diff --git a/www/assets/maps/shanghai/textures/textures/f1_board_b_16.png b/www/assets/maps/shanghai/textures/textures/f1_board_b_16.png new file mode 100644 index 0000000000000000000000000000000000000000..d3f2ac5450554df14690ad087a6080a91d9792b6 Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/f1_board_b_16.png differ diff --git a/www/assets/maps/shanghai/textures/textures/fence_barbed_b_01_d_134.png b/www/assets/maps/shanghai/textures/textures/fence_barbed_b_01_d_134.png new file mode 100644 index 0000000000000000000000000000000000000000..d8ce848d8ef190f0aea8f66453fdbdc7480131f1 Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/fence_barbed_b_01_d_134.png differ diff --git a/www/assets/maps/shanghai/textures/textures/fly_emirates_terrain_a_89.png b/www/assets/maps/shanghai/textures/textures/fly_emirates_terrain_a_89.png new file mode 100644 index 0000000000000000000000000000000000000000..8830cfba96ab3f9cb0354870fa1b35ecbb56cc43 Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/fly_emirates_terrain_a_89.png differ diff --git a/www/assets/maps/shanghai/textures/textures/grass-detailrealistic.png b/www/assets/maps/shanghai/textures/textures/grass-detailrealistic.png new file mode 100644 index 0000000000000000000000000000000000000000..0a40c01bef23d0d61146636ea7e3948d7a86a5c7 --- /dev/null +++ b/www/assets/maps/shanghai/textures/textures/grass-detailrealistic.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9a733d0022596f5771eac670b11e9fd5b5a2adc6c7c1ba57e2cc7cdbabe5d443 +size 5541911 diff --git a/www/assets/maps/shanghai/textures/textures/groove01_24.png b/www/assets/maps/shanghai/textures/textures/groove01_24.png new file mode 100644 index 0000000000000000000000000000000000000000..644d28c4d32d6bf77f5a74c7c0820d7815046b53 Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/groove01_24.png differ diff --git a/www/assets/maps/shanghai/textures/textures/jumbotron_b_01_d_82.png b/www/assets/maps/shanghai/textures/textures/jumbotron_b_01_d_82.png new file mode 100644 index 0000000000000000000000000000000000000000..f49aa5262a6c00fc7049902058e6ea14c22c4197 Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/jumbotron_b_01_d_82.png differ diff --git a/www/assets/maps/shanghai/textures/textures/lg_board_a_51.png b/www/assets/maps/shanghai/textures/textures/lg_board_a_51.png new file mode 100644 index 0000000000000000000000000000000000000000..29f4565fe6d613121d3e2b8f5c854fc2ee425043 Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/lg_board_a_51.png differ diff --git a/www/assets/maps/shanghai/textures/textures/marquee_truck_a_02_d_74.png b/www/assets/maps/shanghai/textures/textures/marquee_truck_a_02_d_74.png new file mode 100644 index 0000000000000000000000000000000000000000..7f9eeddb284d771cc07209f04009f8bfe5f574eb Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/marquee_truck_a_02_d_74.png differ diff --git a/www/assets/maps/shanghai/textures/textures/nuove barriere_19.png b/www/assets/maps/shanghai/textures/textures/nuove barriere_19.png new file mode 100644 index 0000000000000000000000000000000000000000..8942a7de4b9f50733edbf6ad57e87f3c9f4ec41c Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/nuove barriere_19.png differ diff --git a/www/assets/maps/shanghai/textures/textures/rolex_board_a_6.png b/www/assets/maps/shanghai/textures/textures/rolex_board_a_6.png new file mode 100644 index 0000000000000000000000000000000000000000..dc408b595ec3934f50ba4f1c297dc3d14fcb302a Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/rolex_board_a_6.png differ diff --git a/www/assets/maps/shanghai/textures/textures/rolex_board_b_13.png b/www/assets/maps/shanghai/textures/textures/rolex_board_b_13.png new file mode 100644 index 0000000000000000000000000000000000000000..268b7e6442c31bb582a337d39f426506c0c42182 Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/rolex_board_b_13.png differ diff --git a/www/assets/maps/shanghai/textures/textures/rolex_clock_hd_21.png b/www/assets/maps/shanghai/textures/textures/rolex_clock_hd_21.png new file mode 100644 index 0000000000000000000000000000000000000000..65fe0ba1fb50bcc323ad0753808c51e8da678821 Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/rolex_clock_hd_21.png differ diff --git a/www/assets/maps/shanghai/textures/textures/rolex_terrain_a_142.png b/www/assets/maps/shanghai/textures/textures/rolex_terrain_a_142.png new file mode 100644 index 0000000000000000000000000000000000000000..e8b71e3582c4751f1faf2643c0502287e58be901 Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/rolex_terrain_a_142.png differ diff --git a/www/assets/maps/shanghai/textures/textures/sha_aqueduct_a_01_d_92.png b/www/assets/maps/shanghai/textures/textures/sha_aqueduct_a_01_d_92.png new file mode 100644 index 0000000000000000000000000000000000000000..63be390a3b77c8d63cfd667a20311a2dd67669ca Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/sha_aqueduct_a_01_d_92.png differ diff --git a/www/assets/maps/shanghai/textures/textures/sha_banner_greyframe_a_d_57.png b/www/assets/maps/shanghai/textures/textures/sha_banner_greyframe_a_d_57.png new file mode 100644 index 0000000000000000000000000000000000000000..4514d39384d0b9b6a5e7cbbc6a3aa42b8f8869ec Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/sha_banner_greyframe_a_d_57.png differ diff --git a/www/assets/maps/shanghai/textures/textures/sha_banner_onastick_a_d_78.png b/www/assets/maps/shanghai/textures/textures/sha_banner_onastick_a_d_78.png new file mode 100644 index 0000000000000000000000000000000000000000..eccbeb7f8a175c74a65f7735ddefa30997f418c8 Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/sha_banner_onastick_a_d_78.png differ diff --git a/www/assets/maps/shanghai/textures/textures/sha_banner_startfinish_a_d_59.png b/www/assets/maps/shanghai/textures/textures/sha_banner_startfinish_a_d_59.png new file mode 100644 index 0000000000000000000000000000000000000000..98b13eae026786ac1d72f96f6fe8572b38daf56b Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/sha_banner_startfinish_a_d_59.png differ diff --git a/www/assets/maps/shanghai/textures/textures/sha_barrier_armcostubyellow_a_d_47.png b/www/assets/maps/shanghai/textures/textures/sha_barrier_armcostubyellow_a_d_47.png new file mode 100644 index 0000000000000000000000000000000000000000..3f2d209e0325106022a9cbd33e762ba372eebf22 Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/sha_barrier_armcostubyellow_a_d_47.png differ diff --git a/www/assets/maps/shanghai/textures/textures/sha_barrier_barbfence_a_01_d_28.png b/www/assets/maps/shanghai/textures/textures/sha_barrier_barbfence_a_01_d_28.png new file mode 100644 index 0000000000000000000000000000000000000000..a74a95d3a71091e6f04470abed4a859a759bbea6 Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/sha_barrier_barbfence_a_01_d_28.png differ diff --git a/www/assets/maps/shanghai/textures/textures/sha_barrier_barbfence_a_02_d_42.png b/www/assets/maps/shanghai/textures/textures/sha_barrier_barbfence_a_02_d_42.png new file mode 100644 index 0000000000000000000000000000000000000000..ad8e52df80a39a62c0d57ee240fe31a3328dfe2d Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/sha_barrier_barbfence_a_02_d_42.png differ diff --git a/www/assets/maps/shanghai/textures/textures/sha_barrier_pitwall_a_d_45.png b/www/assets/maps/shanghai/textures/textures/sha_barrier_pitwall_a_d_45.png new file mode 100644 index 0000000000000000000000000000000000000000..9b6794c3237ab8837a55b50f8cab4e96e20193f8 Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/sha_barrier_pitwall_a_d_45.png differ diff --git a/www/assets/maps/shanghai/textures/textures/sha_brick_a_01_d_93.png b/www/assets/maps/shanghai/textures/textures/sha_brick_a_01_d_93.png new file mode 100644 index 0000000000000000000000000000000000000000..5b59a8b4e93753bfaa70a7cfd24119e478b7ed45 Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/sha_brick_a_01_d_93.png differ diff --git a/www/assets/maps/shanghai/textures/textures/sha_building_commsplex_a_d_30.png b/www/assets/maps/shanghai/textures/textures/sha_building_commsplex_a_d_30.png new file mode 100644 index 0000000000000000000000000000000000000000..fd01f33e1907474d8b2182618a6bce8a497c749c Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/sha_building_commsplex_a_d_30.png differ diff --git a/www/assets/maps/shanghai/textures/textures/sha_building_commstower_a_d_31.png b/www/assets/maps/shanghai/textures/textures/sha_building_commstower_a_d_31.png new file mode 100644 index 0000000000000000000000000000000000000000..0786d6e71603c733937d6097b4f3d43ea592229e Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/sha_building_commstower_a_d_31.png differ diff --git a/www/assets/maps/shanghai/textures/textures/sha_building_curvedroof_a_d_54.png b/www/assets/maps/shanghai/textures/textures/sha_building_curvedroof_a_d_54.png new file mode 100644 index 0000000000000000000000000000000000000000..188f0540b1e7fe705bf5d1aeeced15887eff391c Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/sha_building_curvedroof_a_d_54.png differ diff --git a/www/assets/maps/shanghai/textures/textures/sha_building_glasstower_a_02_d_4.png b/www/assets/maps/shanghai/textures/textures/sha_building_glasstower_a_02_d_4.png new file mode 100644 index 0000000000000000000000000000000000000000..17d11ef2a7c8defbb1a464df7325ed848104b6ae Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/sha_building_glasstower_a_02_d_4.png differ diff --git a/www/assets/maps/shanghai/textures/textures/sha_building_glasstunnel_a_d_32.png b/www/assets/maps/shanghai/textures/textures/sha_building_glasstunnel_a_d_32.png new file mode 100644 index 0000000000000000000000000000000000000000..6fb2a08000828a7e4689f9da8b62452edb786ade Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/sha_building_glasstunnel_a_d_32.png differ diff --git a/www/assets/maps/shanghai/textures/textures/sha_building_kartcenter_a_d_33.png b/www/assets/maps/shanghai/textures/textures/sha_building_kartcenter_a_d_33.png new file mode 100644 index 0000000000000000000000000000000000000000..5146daf4a163710f629ab5d7589619c713e352cf Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/sha_building_kartcenter_a_d_33.png differ diff --git a/www/assets/maps/shanghai/textures/textures/sha_building_pitgarages_a_01_d_114.png b/www/assets/maps/shanghai/textures/textures/sha_building_pitgarages_a_01_d_114.png new file mode 100644 index 0000000000000000000000000000000000000000..d61bc75246d8692cbca0bbedeb016dd149046a65 Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/sha_building_pitgarages_a_01_d_114.png differ diff --git a/www/assets/maps/shanghai/textures/textures/sha_building_pitgarages_a_02_d_46.png b/www/assets/maps/shanghai/textures/textures/sha_building_pitgarages_a_02_d_46.png new file mode 100644 index 0000000000000000000000000000000000000000..c22be30887fb1910e0e6c0fde9e350feae842fa6 Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/sha_building_pitgarages_a_02_d_46.png differ diff --git a/www/assets/maps/shanghai/textures/textures/sha_building_publicwc_a_d_88.png b/www/assets/maps/shanghai/textures/textures/sha_building_publicwc_a_d_88.png new file mode 100644 index 0000000000000000000000000000000000000000..7d21cd148ff7c98279a262676d53a5fa533c3295 Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/sha_building_publicwc_a_d_88.png differ diff --git a/www/assets/maps/shanghai/textures/textures/sha_building_skybridge_a_d_34.png b/www/assets/maps/shanghai/textures/textures/sha_building_skybridge_a_d_34.png new file mode 100644 index 0000000000000000000000000000000000000000..c43c4a372e88de7642d4cb5837a269edaeadb204 Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/sha_building_skybridge_a_d_34.png differ diff --git a/www/assets/maps/shanghai/textures/textures/sha_cloaca_a_01_d_94.png b/www/assets/maps/shanghai/textures/textures/sha_cloaca_a_01_d_94.png new file mode 100644 index 0000000000000000000000000000000000000000..3ea5ba8e64f866a3b2dc8bf5593bd3238aef284b Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/sha_cloaca_a_01_d_94.png differ diff --git a/www/assets/maps/shanghai/textures/textures/sha_colour_seating_a_01_d_132.png b/www/assets/maps/shanghai/textures/textures/sha_colour_seating_a_01_d_132.png new file mode 100644 index 0000000000000000000000000000000000000000..923dc2db60756c3519b27ffd77b3112b3c6bcd4d Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/sha_colour_seating_a_01_d_132.png differ diff --git a/www/assets/maps/shanghai/textures/textures/sha_grandstand_expo_a_d_9.png b/www/assets/maps/shanghai/textures/textures/sha_grandstand_expo_a_d_9.png new file mode 100644 index 0000000000000000000000000000000000000000..b522414b6afcbdc8275aadf3ce03bccd0bb2292d Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/sha_grandstand_expo_a_d_9.png differ diff --git a/www/assets/maps/shanghai/textures/textures/sha_grandstand_group_a_d_35.png b/www/assets/maps/shanghai/textures/textures/sha_grandstand_group_a_d_35.png new file mode 100644 index 0000000000000000000000000000000000000000..03e8f09c8f4c52b6be23300822c6a5affe223b1d Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/sha_grandstand_group_a_d_35.png differ diff --git a/www/assets/maps/shanghai/textures/textures/sha_grandstand_main_a_d_44.png b/www/assets/maps/shanghai/textures/textures/sha_grandstand_main_a_d_44.png new file mode 100644 index 0000000000000000000000000000000000000000..62ebf8d26d1c1660b272e26fa480b5d5bb62eaee Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/sha_grandstand_main_a_d_44.png differ diff --git a/www/assets/maps/shanghai/textures/textures/sha_grandstand_underbrolly_01_d_43.png b/www/assets/maps/shanghai/textures/textures/sha_grandstand_underbrolly_01_d_43.png new file mode 100644 index 0000000000000000000000000000000000000000..cfee963ba7e257f16d678c6553b5afc656e2d982 Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/sha_grandstand_underbrolly_01_d_43.png differ diff --git a/www/assets/maps/shanghai/textures/textures/sha_grandstand_underbrolly_02_d_133.png b/www/assets/maps/shanghai/textures/textures/sha_grandstand_underbrolly_02_d_133.png new file mode 100644 index 0000000000000000000000000000000000000000..275e712f32f1fde182d91cc3422add7a7215b567 Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/sha_grandstand_underbrolly_02_d_133.png differ diff --git a/www/assets/maps/shanghai/textures/textures/sha_hut_marshalls_a_d_63.png b/www/assets/maps/shanghai/textures/textures/sha_hut_marshalls_a_d_63.png new file mode 100644 index 0000000000000000000000000000000000000000..2ee990f399a3c4108addeb5b0997c3e1fed4d978 --- /dev/null +++ b/www/assets/maps/shanghai/textures/textures/sha_hut_marshalls_a_d_63.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:76bbbd6e3ae0622e8a5a553522a2abe0b2fa2f50ac756cd2801de3a57e3813cd +size 105349 diff --git a/www/assets/maps/shanghai/textures/textures/sha_hut_pitlanetower_a_d_77.png b/www/assets/maps/shanghai/textures/textures/sha_hut_pitlanetower_a_d_77.png new file mode 100644 index 0000000000000000000000000000000000000000..74df9f7f26025d013079e8767e66c18c638933ca Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/sha_hut_pitlanetower_a_d_77.png differ diff --git a/www/assets/maps/shanghai/textures/textures/sha_hut_shabby_a_d_55.png b/www/assets/maps/shanghai/textures/textures/sha_hut_shabby_a_d_55.png new file mode 100644 index 0000000000000000000000000000000000000000..da9e487d08e5f098e3bca2cb925faf8a5f8bee7c Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/sha_hut_shabby_a_d_55.png differ diff --git a/www/assets/maps/shanghai/textures/textures/sha_infrastructure_arcwall_a_d_116.png b/www/assets/maps/shanghai/textures/textures/sha_infrastructure_arcwall_a_d_116.png new file mode 100644 index 0000000000000000000000000000000000000000..35596b3376a0187a9e22ab9cd48b59f50d46d435 Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/sha_infrastructure_arcwall_a_d_116.png differ diff --git a/www/assets/maps/shanghai/textures/textures/sha_infrastructure_brollyroof_a_d_37.png b/www/assets/maps/shanghai/textures/textures/sha_infrastructure_brollyroof_a_d_37.png new file mode 100644 index 0000000000000000000000000000000000000000..a8d86a2aa636c9d00eaf0f9b67c66b388bf1a7c3 Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/sha_infrastructure_brollyroof_a_d_37.png differ diff --git a/www/assets/maps/shanghai/textures/textures/sha_landscape_01_131.png b/www/assets/maps/shanghai/textures/textures/sha_landscape_01_131.png new file mode 100644 index 0000000000000000000000000000000000000000..92354063e2fad08de407fb880ebd92e5bfb7a338 Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/sha_landscape_01_131.png differ diff --git a/www/assets/maps/shanghai/textures/textures/sha_pole_bafflelight_a_d_64.png b/www/assets/maps/shanghai/textures/textures/sha_pole_bafflelight_a_d_64.png new file mode 100644 index 0000000000000000000000000000000000000000..93d58d975cdb1296c0cf7664ba50cc36ed463bf0 Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/sha_pole_bafflelight_a_d_64.png differ diff --git a/www/assets/maps/shanghai/textures/textures/sha_pole_ranking_a_d_81.png b/www/assets/maps/shanghai/textures/textures/sha_pole_ranking_a_d_81.png new file mode 100644 index 0000000000000000000000000000000000000000..5aea99999a73fa0c89740f03b31431c356eaf456 Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/sha_pole_ranking_a_d_81.png differ diff --git a/www/assets/maps/shanghai/textures/textures/sha_rug_a_01_d_98.png b/www/assets/maps/shanghai/textures/textures/sha_rug_a_01_d_98.png new file mode 100644 index 0000000000000000000000000000000000000000..df1bfa444152b5af8a5df01b6b589b51696d727b --- /dev/null +++ b/www/assets/maps/shanghai/textures/textures/sha_rug_a_01_d_98.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dc6eee9dda8dcf0b1ff82fb206aff3d13d1fa33827115052e10c920002eb208d +size 204992 diff --git a/www/assets/maps/shanghai/textures/textures/sha_rumble_a_01_d_99.png b/www/assets/maps/shanghai/textures/textures/sha_rumble_a_01_d_99.png new file mode 100644 index 0000000000000000000000000000000000000000..c046590abdb8a0db095be708f6a93d1b80aeb3f2 Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/sha_rumble_a_01_d_99.png differ diff --git a/www/assets/maps/shanghai/textures/textures/sha_sand_a_01_d_139.png b/www/assets/maps/shanghai/textures/textures/sha_sand_a_01_d_139.png new file mode 100644 index 0000000000000000000000000000000000000000..400128725c28610f7b4abdd791a41ca1b6dca124 --- /dev/null +++ b/www/assets/maps/shanghai/textures/textures/sha_sand_a_01_d_139.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6147592d7e3ea3e42cb9a8bdfec5618f63190fc3db303c8e384b2e94c5e5a0ea +size 164311 diff --git a/www/assets/maps/shanghai/textures/textures/sha_sand_a_02_d_140.png b/www/assets/maps/shanghai/textures/textures/sha_sand_a_02_d_140.png new file mode 100644 index 0000000000000000000000000000000000000000..43af529c9d8884e0bb5614c4c1a0bcb201010089 Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/sha_sand_a_02_d_140.png differ diff --git a/www/assets/maps/shanghai/textures/textures/sha_tent_hospitality_a_d_36.png b/www/assets/maps/shanghai/textures/textures/sha_tent_hospitality_a_d_36.png new file mode 100644 index 0000000000000000000000000000000000000000..bf133cbfd523bd7bfed20365c8c39635c8516154 Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/sha_tent_hospitality_a_d_36.png differ diff --git a/www/assets/maps/shanghai/textures/textures/sha_transition_a_01_d_106.png b/www/assets/maps/shanghai/textures/textures/sha_transition_a_01_d_106.png new file mode 100644 index 0000000000000000000000000000000000000000..1e74e056f503c11d454101c4dd2da05031857c16 --- /dev/null +++ b/www/assets/maps/shanghai/textures/textures/sha_transition_a_01_d_106.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c34d3cb058d94840b3310d82c66918e3f598b134dd82976591dbe91cd4b5b73d +size 100315 diff --git a/www/assets/maps/shanghai/textures/textures/sha_wall_a_01_d_107.png b/www/assets/maps/shanghai/textures/textures/sha_wall_a_01_d_107.png new file mode 100644 index 0000000000000000000000000000000000000000..410a3e4bb1f0db6a7cc219606670100edeccb75e Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/sha_wall_a_01_d_107.png differ diff --git a/www/assets/maps/shanghai/textures/textures/sha_wall_a_02_d_108.png b/www/assets/maps/shanghai/textures/textures/sha_wall_a_02_d_108.png new file mode 100644 index 0000000000000000000000000000000000000000..39a1e9d8a3824d9f66200b69bfa1b9d444c3a9a1 Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/sha_wall_a_02_d_108.png differ diff --git a/www/assets/maps/shanghai/textures/textures/sha_wall_a_03_d_109.png b/www/assets/maps/shanghai/textures/textures/sha_wall_a_03_d_109.png new file mode 100644 index 0000000000000000000000000000000000000000..80b7242e4b09364c6bc521f370a51862fb9dceb0 Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/sha_wall_a_03_d_109.png differ diff --git a/www/assets/maps/shanghai/textures/textures/sha_water_a_01_d_110.png b/www/assets/maps/shanghai/textures/textures/sha_water_a_01_d_110.png new file mode 100644 index 0000000000000000000000000000000000000000..553b2777c7f29276d5e5e24112b5db9cacc48ea6 Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/sha_water_a_01_d_110.png differ diff --git a/www/assets/maps/shanghai/textures/textures/sha_whiteline_a_01_d_135.png b/www/assets/maps/shanghai/textures/textures/sha_whiteline_a_01_d_135.png new file mode 100644 index 0000000000000000000000000000000000000000..d839b134b385e2cad8e5f16971f84d8918879c9b Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/sha_whiteline_a_01_d_135.png differ diff --git a/www/assets/maps/shanghai/textures/textures/skid_marks_a_01_d_22.png b/www/assets/maps/shanghai/textures/textures/skid_marks_a_01_d_22.png new file mode 100644 index 0000000000000000000000000000000000000000..6f036e70cdc9d0a74e95bd2556f3f9522d03c8b0 Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/skid_marks_a_01_d_22.png differ diff --git a/www/assets/maps/shanghai/textures/textures/sponsor_backboard_01_52.png b/www/assets/maps/shanghai/textures/textures/sponsor_backboard_01_52.png new file mode 100644 index 0000000000000000000000000000000000000000..f87a943769bb80d66a321ec81628eebd29f5b6ec Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/sponsor_backboard_01_52.png differ diff --git a/www/assets/maps/shanghai/textures/textures/sponsor_backboard_02_48.png b/www/assets/maps/shanghai/textures/textures/sponsor_backboard_02_48.png new file mode 100644 index 0000000000000000000000000000000000000000..8a8a1f0e866162cad1f3010fd7e51a4411f56b80 --- /dev/null +++ b/www/assets/maps/shanghai/textures/textures/sponsor_backboard_02_48.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9e881f57f2aaaa3728e777ead3386b90b11d35b016f2c0f2cadc4c3b7ef035fb +size 110239 diff --git a/www/assets/maps/shanghai/textures/textures/sponsor_backframe_01_d_53.png b/www/assets/maps/shanghai/textures/textures/sponsor_backframe_01_d_53.png new file mode 100644 index 0000000000000000000000000000000000000000..fc5e9f0b03088b36f185647ba5bdb1a04a0114f3 Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/sponsor_backframe_01_d_53.png differ diff --git a/www/assets/maps/shanghai/textures/textures/sponsorx_cats_varbv_d_41.png b/www/assets/maps/shanghai/textures/textures/sponsorx_cats_varbv_d_41.png new file mode 100644 index 0000000000000000000000000000000000000000..b78437c3c51fc7d7bbf7b2d2d2e192e81f18e4a2 Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/sponsorx_cats_varbv_d_41.png differ diff --git a/www/assets/maps/shanghai/textures/textures/sponsorx_cats_varca_d_40.png b/www/assets/maps/shanghai/textures/textures/sponsorx_cats_varca_d_40.png new file mode 100644 index 0000000000000000000000000000000000000000..2d8e65e5e673284ad007bda3607672e709be6b0c Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/sponsorx_cats_varca_d_40.png differ diff --git a/www/assets/maps/shanghai/textures/textures/telo_1000_7.png b/www/assets/maps/shanghai/textures/textures/telo_1000_7.png new file mode 100644 index 0000000000000000000000000000000000000000..311f9c9219ac63dea5354f809869e40e9fd7dd29 Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/telo_1000_7.png differ diff --git a/www/assets/maps/shanghai/textures/textures/tree04a_127.png b/www/assets/maps/shanghai/textures/textures/tree04a_127.png new file mode 100644 index 0000000000000000000000000000000000000000..b78f5e04657b6d83c25d218fbb5711a62687d0bd --- /dev/null +++ b/www/assets/maps/shanghai/textures/textures/tree04a_127.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bcab9accaf087422841b4b58b761849481b3236cbb22f8e4c1ffac89ea05e95a +size 101973 diff --git a/www/assets/maps/shanghai/textures/textures/tree04b_129.png b/www/assets/maps/shanghai/textures/textures/tree04b_129.png new file mode 100644 index 0000000000000000000000000000000000000000..1df68c91babecea533c946af55ea28d0f73b3526 Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/tree04b_129.png differ diff --git a/www/assets/maps/shanghai/textures/textures/tree06a_128.png b/www/assets/maps/shanghai/textures/textures/tree06a_128.png new file mode 100644 index 0000000000000000000000000000000000000000..69dcf98ad88e0a97c01f10b9e22b6dfd0642dc7c --- /dev/null +++ b/www/assets/maps/shanghai/textures/textures/tree06a_128.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bc09aa3d43e6bb94d7e44055d9996c018e0f0a028fdcf1164f35ce20df292a63 +size 119571 diff --git a/www/assets/maps/shanghai/textures/textures/treeline02_136.png b/www/assets/maps/shanghai/textures/textures/treeline02_136.png new file mode 100644 index 0000000000000000000000000000000000000000..ff6c14aee490c6dc2879670dc1954b3b7c5a639d Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/treeline02_136.png differ diff --git a/www/assets/maps/shanghai/textures/textures/tyres_add1_23.png b/www/assets/maps/shanghai/textures/textures/tyres_add1_23.png new file mode 100644 index 0000000000000000000000000000000000000000..fca87264e809985cd6280b87aa8b46090e066bd2 Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/tyres_add1_23.png differ diff --git a/www/assets/maps/shanghai/textures/textures/van_safety_b_d_73.png b/www/assets/maps/shanghai/textures/textures/van_safety_b_d_73.png new file mode 100644 index 0000000000000000000000000000000000000000..236ef3898478d32bef3837d1ccf0d19706e9cc1e Binary files /dev/null and b/www/assets/maps/shanghai/textures/textures/van_safety_b_d_73.png differ diff --git a/www/assets/maps/shanghai/textures/tree04a_127.png b/www/assets/maps/shanghai/textures/tree04a_127.png new file mode 100644 index 0000000000000000000000000000000000000000..b78f5e04657b6d83c25d218fbb5711a62687d0bd --- /dev/null +++ b/www/assets/maps/shanghai/textures/tree04a_127.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bcab9accaf087422841b4b58b761849481b3236cbb22f8e4c1ffac89ea05e95a +size 101973 diff --git a/www/assets/maps/shanghai/textures/tree04b_129.png b/www/assets/maps/shanghai/textures/tree04b_129.png new file mode 100644 index 0000000000000000000000000000000000000000..1df68c91babecea533c946af55ea28d0f73b3526 Binary files /dev/null and b/www/assets/maps/shanghai/textures/tree04b_129.png differ diff --git a/www/assets/maps/shanghai/textures/tree06a_128.png b/www/assets/maps/shanghai/textures/tree06a_128.png new file mode 100644 index 0000000000000000000000000000000000000000..69dcf98ad88e0a97c01f10b9e22b6dfd0642dc7c --- /dev/null +++ b/www/assets/maps/shanghai/textures/tree06a_128.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bc09aa3d43e6bb94d7e44055d9996c018e0f0a028fdcf1164f35ce20df292a63 +size 119571 diff --git a/www/assets/maps/shanghai/textures/treeline02_136.png b/www/assets/maps/shanghai/textures/treeline02_136.png new file mode 100644 index 0000000000000000000000000000000000000000..ff6c14aee490c6dc2879670dc1954b3b7c5a639d Binary files /dev/null and b/www/assets/maps/shanghai/textures/treeline02_136.png differ diff --git a/www/assets/maps/shanghai/textures/tyres_add1_23.png b/www/assets/maps/shanghai/textures/tyres_add1_23.png new file mode 100644 index 0000000000000000000000000000000000000000..fca87264e809985cd6280b87aa8b46090e066bd2 Binary files /dev/null and b/www/assets/maps/shanghai/textures/tyres_add1_23.png differ diff --git a/www/assets/maps/shanghai/textures/van_safety_b_d_73.png b/www/assets/maps/shanghai/textures/van_safety_b_d_73.png new file mode 100644 index 0000000000000000000000000000000000000000..236ef3898478d32bef3837d1ccf0d19706e9cc1e Binary files /dev/null and b/www/assets/maps/shanghai/textures/van_safety_b_d_73.png differ diff --git a/www/assets/maps/shanghai/track.glb b/www/assets/maps/shanghai/track.glb new file mode 100644 index 0000000000000000000000000000000000000000..c7c6b75f25241ef77cb5441f606e34995d17d746 --- /dev/null +++ b/www/assets/maps/shanghai/track.glb @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e0eca9ca4e821f4582511c5562ba0b7b678ccdee13a83bae646e00806ddd28cb +size 92809048 diff --git a/www/assets/maps/track.glb b/www/assets/maps/track.glb new file mode 100644 index 0000000000000000000000000000000000000000..c7c6b75f25241ef77cb5441f606e34995d17d746 --- /dev/null +++ b/www/assets/maps/track.glb @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e0eca9ca4e821f4582511c5562ba0b7b678ccdee13a83bae646e00806ddd28cb +size 92809048 diff --git a/www/css/style.css b/www/css/style.css new file mode 100644 index 0000000000000000000000000000000000000000..efe5f008a9456152a5c86284ce3e6f9e3b834693 --- /dev/null +++ b/www/css/style.css @@ -0,0 +1,358 @@ +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html, +body { + width: 100%; + height: 100%; + overflow: hidden; + background: #0a0a0a; + font-family: "Segoe UI", "Helvetica Neue", sans-serif; + color: #fff; +} + +#game-canvas { + display: block; + position: fixed; + inset: 0; + width: 100%; + height: 100%; +} + +#hud { + position: fixed; + inset: 0; + width: 100%; + height: 100%; + pointer-events: none; + z-index: 10; +} + +#player-hud { + position: absolute; + top: 12px; + left: 12px; + z-index: 16; + display: flex; + align-items: center; + gap: 0.55rem; +} + +#player-hud[hidden] { + display: none !important; +} + +#player-avatar { + width: 40px; + height: 40px; + border-radius: 50%; + object-fit: cover; + border: 2px solid rgba(255, 255, 255, 0.85); + background: #222; +} + +#player-name { + font-family: "Arial Black", "Helvetica Neue", Impact, sans-serif; + font-size: 0.95rem; + color: #fff; + text-shadow: 0 1px 2px #000, 1px 1px 0 #000; +} + +#speed-hud { + position: absolute; + bottom: 28px; + right: 20px; + font-family: "Arial Black", "Helvetica Neue", Impact, sans-serif; + font-size: 2.2rem; + letter-spacing: 0.02em; + text-shadow: 0 2px 0 #000; +} + +.speed-unit { + margin-left: 0.25rem; + font-size: 0.9rem; + opacity: 0.75; +} + +#lap-hud { + position: absolute; + top: 14px; + right: 18px; + font-size: 0.95rem; + letter-spacing: 0.06em; + text-transform: uppercase; + color: #e8e2da; + text-shadow: 0 1px 2px #000; +} + +#spectate-bar { + position: absolute; + bottom: 36px; + left: 50%; + transform: translateX(-50%); + display: flex; + align-items: center; + gap: 0.65rem; + pointer-events: auto; + background: rgba(10, 10, 10, 0.72); + padding: 0.45rem 0.75rem; + border: 1px solid #3a322a; +} + +#spectate-bar[hidden] { + display: none !important; +} + +.spec-arrow { + pointer-events: auto; + cursor: pointer; + border: 1px solid #5a5048; + background: #1a1510; + color: #ece6df; + padding: 0.35rem 0.55rem; +} + +#spec-avatar { + width: 44px; + height: 44px; + border-radius: 50%; + object-fit: cover; + background: #222; +} + +#controls-footer { + position: fixed; + left: 12px; + bottom: 10px; + z-index: 20; + font-size: 0.75rem; + color: #9aa6b2; + pointer-events: none; +} + +.menu-root { + position: fixed; + inset: 0; + z-index: 30; + display: flex; + align-items: center; + justify-content: center; + gap: 0.75rem; + padding: 1.5rem; + background: + radial-gradient(ellipse at 50% 15%, rgba(120, 30, 20, 0.45), transparent 55%), + linear-gradient(180deg, #1a1010 0%, #0c0808 100%); + pointer-events: auto; +} + +.menu-root[hidden] { + display: none !important; +} + +.menu-brand { + font-family: "Arial Black", "Helvetica Neue", sans-serif; + font-size: clamp(2.4rem, 6vw, 4rem); + letter-spacing: 0.04em; + text-transform: uppercase; + color: #f2f4f7; + text-shadow: 0 2px 0 #000; +} + +.menu-sub { + margin: 0.25rem 0 1.25rem; + color: #9aa6b2; + font-size: 0.95rem; +} + +.menu-panel { + width: min(920px, 100%); + text-align: center; +} + +.menu-actions { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; + justify-content: center; +} + +.menu-btn { + pointer-events: auto; + cursor: pointer; + border: 2px solid #d8dde3; + background: #1a1110; + color: #f5f7fa; + padding: 0.7rem 1.4rem; + font-size: 0.95rem; + letter-spacing: 0.04em; + text-transform: uppercase; +} + +.menu-btn:hover { + background: #3a2018; +} + +.menu-btn.ghost { + border-color: #5a6570; + background: transparent; + font-size: 0.8rem; + padding: 0.45rem 0.85rem; +} + +.menu-user { + position: absolute; + top: 14px; + left: 16px; + font-size: 0.8rem; + color: #b7c2cc; +} + +.menu-error { + position: absolute; + top: 40px; + left: 16px; + right: 16px; + color: #ff8e8e; + font-size: 0.85rem; +} + +.menu-lobby { + text-align: left; + max-height: min(78vh, 720px); + display: flex; + flex-direction: column; +} + +.menu-panel[hidden], +.menu-lobby[hidden] { + display: none !important; +} + +.lobby-toolbar { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.75rem; + margin-bottom: 0.5rem; +} + +.lobby-toolbar h2 { + flex: 1; + font-size: 1.15rem; +} + +.lobby-actions { + display: flex; + gap: 0.5rem; +} + +.lobby-meta { + color: #9aa6b2; + font-size: 0.8rem; + margin-bottom: 0.75rem; +} + +.lobby-list { + overflow: auto; + display: flex; + flex-direction: column; + gap: 0.65rem; + padding-right: 0.25rem; +} + +.lobby-card { + border: 1px solid #3a322a; + background: #100e0c; + padding: 0.65rem 0.75rem; +} + +.lobby-card-head { + display: flex; + justify-content: space-between; + gap: 0.5rem; + margin-bottom: 0.5rem; + font-size: 0.85rem; + color: #ece6df; +} + +.lobby-seats { + display: flex; + flex-wrap: wrap; + gap: 0.4rem; +} + +.lobby-seats.teams { + flex-wrap: nowrap; + justify-content: space-between; + align-items: center; + gap: 0.6rem; +} + +.team-col { + display: flex; + flex-wrap: wrap; + gap: 0.35rem; + max-width: 42%; +} + +.team-col.team-x { + justify-content: flex-start; +} + +.team-col.team-y { + justify-content: flex-end; +} + +.team-mid { + flex: 0 0 auto; +} + +.seat-btn { + pointer-events: auto; + cursor: pointer; + border: 1px solid #5a5048; + background: #1a1510; + color: #ece6df; + font-size: 0.78rem; + font-weight: 700; + min-width: 2rem; + padding: 0.35rem 0.55rem; +} + +.seat-btn.team-x { + border-color: #c44b3c; + background: #3a1814; + color: #ffd4ce; +} + +.seat-btn.team-y { + border-color: #3b7ec4; + background: #142433; + color: #cfe4ff; +} + +.seat-btn .seat-who { + display: block; + font-size: 0.62rem; + font-weight: 500; + opacity: 0.85; +} + +.seat-btn.filled { + opacity: 0.9; + cursor: default; +} + +.seat-btn.spectate-btn { + border-color: #5dce8a; + color: #b8f0d0; + background: #142018; + font-weight: 600; +} + +.seat-btn:disabled { + cursor: default; +} diff --git a/www/index.html b/www/index.html new file mode 100644 index 0000000000000000000000000000000000000000..2dbcb753f64ccdab7ca24626de3785bb92448ace --- /dev/null +++ b/www/index.html @@ -0,0 +1,71 @@ + + + + + + HF Racing + + + + + + + + + + + + + diff --git a/www/js/controls.js b/www/js/controls.js new file mode 100644 index 0000000000000000000000000000000000000000..7fb625fe73c7e3825c7c0e23dcdc3a8bfef22198 --- /dev/null +++ b/www/js/controls.js @@ -0,0 +1,28 @@ +/** Keyboard driving input (WASD / arrows). */ + +export class DriveControls { + constructor(target = window) { + this.keys = new Set(); + this._down = (e) => this.keys.add(e.code); + this._up = (e) => this.keys.delete(e.code); + target.addEventListener('keydown', this._down); + target.addEventListener('keyup', this._up); + } + + dispose(target = window) { + target.removeEventListener('keydown', this._down); + target.removeEventListener('keyup', this._up); + } + + sample() { + const forward = this.keys.has('KeyW') || this.keys.has('ArrowUp'); + const back = this.keys.has('KeyS') || this.keys.has('ArrowDown'); + const left = this.keys.has('KeyA') || this.keys.has('ArrowLeft'); + const right = this.keys.has('KeyD') || this.keys.has('ArrowRight'); + return { + throttle: (forward ? 1 : 0) - (back ? 1 : 0), + steer: (left ? 1 : 0) - (right ? 1 : 0), + brake: this.keys.has('Space') ? 1 : 0, + }; + } +} diff --git a/www/js/main.js b/www/js/main.js new file mode 100644 index 0000000000000000000000000000000000000000..7aa4bebf7ecfae6ac69f25bc6f1cdcdfcf6ce075 --- /dev/null +++ b/www/js/main.js @@ -0,0 +1,203 @@ +import * as THREE from 'three'; +import { GameMenu } from './ui-menu.js'; +import { NetClient, SPAWN_OFFSETS } from './net.js'; +import { DriveControls } from './controls.js'; +import { LocalCar } from './vehicles.js'; +import { Track } from './track.js'; + +const footer = document.getElementById('footer-text'); +const hud = document.getElementById('hud'); +const speedEl = document.getElementById('speed-value'); +const playerHud = document.getElementById('player-hud'); +const playerName = document.getElementById('player-name'); +const playerAvatar = document.getElementById('player-avatar'); +const spectateBar = document.getElementById('spectate-bar'); + +const scene = new THREE.Scene(); +scene.background = new THREE.Color(0x87a0b5); +scene.fog = new THREE.Fog(0x87a0b5, 40, 140); + +const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 500); +camera.position.set(0, 6, -12); + +const renderer = new THREE.WebGLRenderer({ antialias: true }); +renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); +renderer.setSize(window.innerWidth, window.innerHeight); +renderer.shadowMap.enabled = true; +renderer.domElement.id = 'game-canvas'; +document.body.prepend(renderer.domElement); + +const hemi = new THREE.HemisphereLight(0xddeeff, 0x443322, 0.85); +scene.add(hemi); +const sun = new THREE.DirectionalLight(0xfff2dd, 1.1); +sun.position.set(30, 50, 10); +sun.castShadow = true; +scene.add(sun); + +const track = new Track(scene); +const controls = new DriveControls(); +let car = null; +let inRace = false; +let spectating = false; +let specIndex = 0; + +const net = new NetClient({ + scene, + onStatus: (msg) => { + if (footer) footer.textContent = msg; + }, + onLobbyUpdate: (board, mode, err) => menu.renderBoard(board, mode, err), + onMatchStart: enterRace, + onMatchEnd: () => { + if (footer) footer.textContent = 'Race finished — Esc for menu'; + }, + onMatchConnectionLost: () => { + leaveRaceToMenu(); + }, +}); + +const menu = new GameMenu({ + root: document.getElementById('menu-root'), + onOpenMode: (mode) => net.startLobbyPoll(mode), + onBack: async () => { + net.stopLobbyPoll(); + await net.leaveLobby(); + }, + onClaim: (mode, lobbyId, seat) => net.claim(mode, lobbyId, seat), + onLeaveSeat: () => net.leaveLobby(), + onEnterMatch: () => net.connectMatch(), + onSpectate: (mode, lobbyId) => { + menu.hide(); + hud.hidden = false; + net.connectSpectate(mode, lobbyId); + }, +}); + +function setIdentityHud(identity) { + if (!identity?.username) return; + if (playerName) playerName.textContent = identity.username; + if (playerAvatar && identity.avatarUrl) { + playerAvatar.src = identity.avatarUrl; + playerAvatar.hidden = false; + } + if (playerHud) playerHud.hidden = false; +} + +function enterRace(info) { + inRace = true; + spectating = !!info.spectating; + menu.hide(); + hud.hidden = false; + if (spectateBar) spectateBar.hidden = !spectating; + + if (!spectating) { + if (!car) { + const color = info.side === 'Y' ? 0x3b7ec4 : 0xc44b3c; + car = new LocalCar(scene, { color }); + car.tryLoadModel('car').then((ok) => { + if (footer && ok) footer.textContent = 'Loaded assets/cars/car.glb'; + }); + } + const off = SPAWN_OFFSETS[info.spawnIndex % SPAWN_OFFSETS.length]; + car.setPose(off.x, 0.35, off.z, 0); + } + + track.tryLoadMap('track').then((ok) => { + if (footer) { + footer.textContent = ok + ? `Race live · map loaded` + : `Race live · placeholder track (drop GLB in assets/maps/)`; + } + }); +} + +function leaveRaceToMenu() { + inRace = false; + spectating = false; + net.disconnectMatch(); + if (car) { + car.dispose(); + car = null; + } + hud.hidden = true; + if (spectateBar) spectateBar.hidden = true; + menu.show(); + net.startLobbyPoll(menu.mode || 'sandbox'); +} + +document.addEventListener('keydown', (e) => { + if (e.code === 'Escape' && inRace) { + leaveRaceToMenu(); + } +}); + +document.getElementById('spec-prev')?.addEventListener('click', () => { + specIndex = Math.max(0, specIndex - 1); +}); +document.getElementById('spec-next')?.addEventListener('click', () => { + specIndex += 1; +}); + +window.addEventListener('resize', () => { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + renderer.setSize(window.innerWidth, window.innerHeight); +}); + +function followCar(target, delta) { + if (!target) return; + const behind = new THREE.Vector3( + Math.sin(target.yaw || target.mesh?.rotation.y || 0) * -10, + 5, + Math.cos(target.yaw || target.mesh?.rotation.y || 0) * -10, + ); + const desired = target.position.clone().add(behind); + camera.position.lerp(desired, Math.min(1, delta * 4)); + camera.lookAt(target.position.x, target.position.y + 1, target.position.z); +} + +let last = performance.now(); +function frame(now) { + const delta = Math.min(0.05, (now - last) / 1000); + last = now; + + if (inRace && !spectating && car) { + car.update(delta, controls.sample()); + if (speedEl) speedEl.textContent = String(car.speedKmh()); + followCar(car, delta); + } else if (inRace && spectating) { + const names = net.peers.list(); + if (names.length) { + specIndex = ((specIndex % names.length) + names.length) % names.length; + const peer = net.peers.get(names[specIndex]); + if (peer) { + followCar({ position: peer.mesh.position, yaw: peer.mesh.rotation.y, mesh: peer.mesh }, delta); + const nameEl = document.getElementById('spec-name'); + if (nameEl) nameEl.textContent = names[specIndex]; + } + } + } + + net.update(delta, car); + renderer.render(scene, camera); + requestAnimationFrame(frame); +} + +async function boot() { + const identity = await net.initLocal(); + menu.setIdentity({ + username: identity.username, + spaceUrl: identity.spaceUrl, + authError: identity.ok ? null : identity.error, + playAllowed: identity.playAllowed !== false, + }); + if (identity.ok) setIdentityHud(identity); + if (footer) { + footer.textContent = identity.ok + ? 'Pick a mode — cars/maps can be dropped into assets/ later' + : (identity.error || 'Set HF_TOKEN in .env.local'); + } + requestAnimationFrame(frame); +} + +boot(); diff --git a/www/js/net.js b/www/js/net.js new file mode 100644 index 0000000000000000000000000000000000000000..6a40f9c0d4857d8585c366f3cd28eaa4d5a3c0c8 --- /dev/null +++ b/www/js/net.js @@ -0,0 +1,352 @@ +/** Lobby + match WebSocket client (racing state sync). */ + +import { PeerManager } from './peers.js'; + +const STATE_HZ = 30; +const STATE_INTERVAL = 1 / STATE_HZ; +const LOBBY_POLL_MS = 1000; +const HEARTBEAT_MS = 3000; + +function wsUrl(spaceUrl, mode, lobbyId, username, role = 'play') { + const base = (spaceUrl || window.location.origin).replace(/\/$/, ''); + const u = new URL(base); + u.protocol = u.protocol === 'https:' ? 'wss:' : 'ws:'; + u.pathname = `/ws/match/${encodeURIComponent(mode)}/${encodeURIComponent(lobbyId)}`; + u.search = `user=${encodeURIComponent(username)}&role=${encodeURIComponent(role)}`; + return u.toString(); +} + +export class NetClient { + constructor({ scene, onStatus, onMatchStart, onMatchEnd, onLobbyUpdate, onMatchConnectionLost }) { + this.scene = scene; + this.onStatus = onStatus || (() => {}); + this.onMatchStart = onMatchStart || (() => {}); + this.onMatchEnd = onMatchEnd || (() => {}); + this.onLobbyUpdate = onLobbyUpdate || (() => {}); + this.onMatchConnectionLost = onMatchConnectionLost || (() => {}); + + this.username = null; + this.avatarUrl = null; + this.spaceUrl = null; + this.playAllowed = true; + this.host = 'local'; + this.mode = null; + this.lobbyId = null; + this.seat = null; + this.side = null; + this.matchId = null; + this.inMatch = false; + this.ws = null; + this.peers = new PeerManager(scene); + this.board = null; + this._pollTimer = null; + this._hbTimer = null; + this._stateAcc = 0; + this._spawnIndex = 0; + this.players = []; + this.spectating = false; + } + + async initLocal() { + const cfg = await fetch('/api/config').then((r) => r.json()); + this.spaceUrl = cfg.spaceUrl || ''; + this.username = cfg.username || null; + this.host = cfg.host || 'local'; + this.playAllowed = cfg.host === 'space' ? false : cfg.playAllowed !== false; + this.avatarUrl = cfg.avatarUrl || (this.username && !String(this.username).startsWith('guest-') + ? `https://huggingface.co/avatars/${encodeURIComponent(this.username)}` + : null); + if (!this.username) { + return { ok: false, error: cfg.authError || 'No player identity' }; + } + if (this.host === 'local' && String(this.username).startsWith('guest-')) { + return { ok: false, error: 'Local play requires HF_TOKEN (guest ids are spectate-only on Space)' }; + } + if (this.playAllowed && cfg.hasToken === false && this.host !== 'space') { + return { ok: false, error: cfg.authError || 'Set HF_TOKEN in .env.local' }; + } + return { + ok: true, + username: this.username, + avatarUrl: this.avatarUrl, + spaceUrl: this.spaceUrl || window.location.origin, + playAllowed: this.playAllowed, + host: this.host, + }; + } + + _lobbyBase() { + return ''; + } + + async _fetchLobbies() { + const res = await fetch(`${this._lobbyBase()}/api/lobbies`); + if (!res.ok) throw new Error(`Lobby server ${res.status}`); + const board = await res.json(); + if (!board?.duel && !board?.sandbox) throw new Error('Bad lobby payload'); + return board; + } + + startLobbyPoll(mode) { + this.mode = mode; + this.stopLobbyPoll(); + const tick = async () => { + try { + const board = await this._fetchLobbies(); + this.board = board; + this.onLobbyUpdate(board, mode, null); + } catch (err) { + this.onStatus(`Lobby poll failed: ${err.message}`); + this.onLobbyUpdate(null, mode, err.message || String(err)); + } + }; + tick(); + this._pollTimer = setInterval(tick, LOBBY_POLL_MS); + this._hbTimer = setInterval(() => { + if (!this.username) return; + fetch(`${this._lobbyBase()}/api/lobbies/heartbeat`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username: this.username }), + }).catch(() => {}); + }, HEARTBEAT_MS); + } + + stopLobbyPoll() { + if (this._pollTimer) clearInterval(this._pollTimer); + if (this._hbTimer) clearInterval(this._hbTimer); + this._pollTimer = null; + this._hbTimer = null; + } + + async claim(mode, lobbyId, seat) { + if (!this.playAllowed) throw new Error('Play disabled here — spectate only'); + const res = await fetch(`/api/lobbies/${encodeURIComponent(mode)}/${encodeURIComponent(lobbyId)}/claim`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + username: this.username, + seat, + avatarUrl: this.avatarUrl || null, + }), + }); + let data; + try { + data = await res.json(); + } catch { + throw new Error(`claim failed (${res.status})`); + } + if (!res.ok || !data.ok) throw new Error(data.error || `claim failed (${res.status})`); + this.mode = mode; + this.lobbyId = lobbyId; + this.seat = seat; + this.side = seatSide(seat); + this.matchId = data.matchId || null; + return data; + } + + async leaveLobby() { + this.disconnectMatch(); + if (!this.username) return; + await fetch(`${this._lobbyBase()}/api/lobbies/leave`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username: this.username }), + }).catch(() => {}); + this.lobbyId = null; + this.seat = null; + } + + connectMatch() { + if (!this.playAllowed || this.host === 'space') { + this.onStatus('Play disabled here — spectate only'); + return; + } + if (!this.lobbyId || !this.mode) return; + this._openMatchWs('play'); + } + + connectSpectate(mode, lobbyId) { + if (!mode || !lobbyId || !this.username) return; + this.mode = mode; + this.lobbyId = lobbyId; + this.seat = null; + this.side = 'ffa'; + this.spectating = true; + this._openMatchWs('spectate'); + } + + _openMatchWs(role) { + if (this.host === 'space' || !this.playAllowed) role = 'spectate'; + this.disconnectMatch(false); + this.spectating = role === 'spectate'; + const url = wsUrl(this.spaceUrl, this.mode, this.lobbyId, this.username, role); + this.onStatus(this.spectating ? 'Connecting as spectator…' : 'Connecting to race…'); + const ws = new WebSocket(url); + this.ws = ws; + + ws.onopen = () => { + this.onStatus(this.spectating ? 'Waiting for race (spectate)…' : 'Waiting for race start…'); + this.send({ type: 'ready', username: this.username }); + }; + + ws.onmessage = (ev) => { + let msg; + try { + msg = JSON.parse(ev.data); + } catch { + return; + } + this._onMessage(msg); + }; + + ws.onclose = () => { + if (this.ws !== ws) return; + const wasInMatch = this.inMatch; + const wasSpectating = this.spectating; + this.ws = null; + this.inMatch = false; + this.onStatus('Match connection closed'); + if (wasInMatch || wasSpectating) { + this.onMatchConnectionLost({ wasInMatch, wasSpectating }); + } + }; + + ws.onerror = () => { + this.onStatus('Match WebSocket error'); + }; + } + + disconnectMatch(clearPeers = true) { + const ws = this.ws; + this.ws = null; + this.inMatch = false; + this.matchId = null; + this.spectating = false; + if (ws) { + try { + ws.close(); + } catch { + // ignore + } + } + if (clearPeers) this.peers.clear(); + } + + send(msg) { + if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return; + if (this.spectating && msg?.type && msg.type !== 'ping' && msg.type !== 'ready') return; + this.ws.send(JSON.stringify(msg)); + } + + _onMessage(msg) { + const type = msg.type; + if (type === 'waiting') { + this.onStatus('In lobby — waiting for race start…'); + return; + } + if (type === 'error') { + this.onStatus(msg.error || 'Server error'); + if (/seat|seated|rejoin|lobby reset|match did not start/i.test(msg.error || '')) { + try { + this.ws?.close(); + } catch { + // ignore + } + } + return; + } + if (type === 'match_start') { + this.inMatch = true; + this.matchId = msg.matchId; + this.players = msg.players || []; + this.spectating = + !!msg.spectating || this.spectating || !this.playAllowed || this.host === 'space'; + this.seat = this.spectating ? null : (msg.seat || this.seat); + this.side = this.spectating ? 'ffa' : seatSide(this.seat); + const hideSelf = this.spectating ? null : this.username; + this.peers.syncPlayers(this.players, hideSelf); + this._spawnIndex = this.spectating ? 0 : spawnIndexFor(this.seat, this.mode, this.players); + this.onMatchStart({ + mode: msg.mode || this.mode, + seat: this.seat, + side: this.side, + players: this.players, + spawnIndex: this._spawnIndex, + spectating: this.spectating, + }); + this.onStatus(this.spectating ? `Spectating ${this.mode} · ${this.lobbyId}` : `Race live — ${this.mode}`); + return; + } + if (type === 'player_joined' || type === 'player_left') { + this.players = msg.players || this.players; + this.peers.syncPlayers(this.players, this.spectating ? null : this.username); + return; + } + if (type === 'state' && msg.from) { + this.peers.applyState(msg.from, msg); + return; + } + if (type === 'match_end') { + this.onMatchEnd(msg); + this.onStatus(`Race over — ${msg.winner || 'finish'}`); + } + } + + /** Call each frame while in a race */ + update(delta, car) { + if (!this.inMatch) return; + this.peers.update(delta); + if (this.spectating || !car) return; + + this._stateAcc += delta; + if (this._stateAcc >= STATE_INTERVAL) { + this._stateAcc = 0; + const p = car.position; + this.send({ + type: 'state', + pos: [p.x, p.y, p.z], + rot: [car.yaw, 0, 0], + speed: car.speed || 0, + avatarUrl: this.avatarUrl || undefined, + }); + } + } +} + +function seatSide(seat) { + if (!seat) return 'ffa'; + const s = String(seat); + if (s.startsWith('X-')) return 'X'; + if (s.startsWith('Y-')) return 'Y'; + return 'ffa'; +} + +export function spawnIndexFor(seat, mode, players) { + if (mode === '1v1') { + return seat === 'Y-1' ? 1 : 0; + } + if (mode === '4v4') { + const m = String(seat).match(/^([XY])-(\d)$/i); + if (!m) return 0; + const side = m[1].toUpperCase() === 'Y' ? 4 : 0; + const n = Number(m[2]); + return side + Math.max(0, Math.min(3, n - 1)); + } + const names = (players || []).map((p) => p.username).sort(); + const me = (players || []).find((p) => p.seat === seat)?.username; + const idx = Math.max(0, names.indexOf(me)); + return idx % 8; +} + +/** Grid offsets until real track start line exists */ +export const SPAWN_OFFSETS = [ + { x: -6, z: 0 }, + { x: -2, z: 0 }, + { x: 2, z: 0 }, + { x: 6, z: 0 }, + { x: -6, z: -6 }, + { x: -2, z: -6 }, + { x: 2, z: -6 }, + { x: 6, z: -6 }, +]; diff --git a/www/js/peers.js b/www/js/peers.js new file mode 100644 index 0000000000000000000000000000000000000000..df0ec240018dca879f499b6e92fd131603c4edd1 --- /dev/null +++ b/www/js/peers.js @@ -0,0 +1,81 @@ +/** Remote racers — placeholder meshes until car GLBs land in assets/cars/. */ + +import * as THREE from 'three'; + +export class PeerManager { + constructor(scene) { + this.scene = scene; + this.peers = new Map(); // username -> { mesh, side, targetPos, targetYaw } + } + + syncPlayers(players, hideUsername = null) { + const keep = new Set(); + for (const p of players || []) { + if (!p?.username || p.username === hideUsername) continue; + keep.add(p.username); + let entry = this.peers.get(p.username); + if (!entry) { + const color = p.side === 'Y' ? 0x3b7ec4 : p.side === 'X' ? 0xc44b3c : 0xd8d0c6; + const mesh = new THREE.Mesh( + new THREE.BoxGeometry(1.6, 0.55, 3.2), + new THREE.MeshStandardMaterial({ color, metalness: 0.35, roughness: 0.45 }), + ); + mesh.castShadow = true; + mesh.position.set(0, 0.35, 0); + this.scene.add(mesh); + entry = { + mesh, + side: p.side || 'ffa', + targetPos: new THREE.Vector3(), + targetYaw: 0, + speed: 0, + }; + this.peers.set(p.username, entry); + } else { + entry.side = p.side || entry.side; + } + } + for (const [user, entry] of [...this.peers.entries()]) { + if (!keep.has(user)) { + this.scene.remove(entry.mesh); + entry.mesh.geometry?.dispose?.(); + entry.mesh.material?.dispose?.(); + this.peers.delete(user); + } + } + } + + applyState(username, msg) { + const entry = this.peers.get(username); + if (!entry || !msg?.pos) return; + entry.targetPos.set(msg.pos[0], msg.pos[1], msg.pos[2]); + entry.targetYaw = Array.isArray(msg.rot) ? msg.rot[0] : 0; + entry.speed = typeof msg.speed === 'number' ? msg.speed : entry.speed; + entry.mesh.visible = true; + } + + update(delta) { + const t = Math.min(1, delta * 12); + for (const entry of this.peers.values()) { + entry.mesh.position.lerp(entry.targetPos, t); + entry.mesh.rotation.y = THREE.MathUtils.lerp(entry.mesh.rotation.y, entry.targetYaw, t); + } + } + + clear() { + for (const entry of this.peers.values()) { + this.scene.remove(entry.mesh); + entry.mesh.geometry?.dispose?.(); + entry.mesh.material?.dispose?.(); + } + this.peers.clear(); + } + + list() { + return [...this.peers.keys()]; + } + + get(username) { + return this.peers.get(username) || null; + } +} diff --git a/www/js/track.js b/www/js/track.js new file mode 100644 index 0000000000000000000000000000000000000000..42b10154526f189c1eb1f7053dc683d8a10c4444 --- /dev/null +++ b/www/js/track.js @@ -0,0 +1,60 @@ +/** + * Track / map loader — placeholder ground until maps land in assets/maps/. + * Load path convention: assets/maps/.glb + */ + +import * as THREE from 'three'; +import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; + +export class Track { + constructor(scene) { + this.scene = scene; + this.root = new THREE.Group(); + scene.add(this.root); + this._loader = new GLTFLoader(); + this.mapId = null; + + const ground = new THREE.Mesh( + new THREE.PlaneGeometry(120, 120), + new THREE.MeshStandardMaterial({ color: 0x2a2622, roughness: 0.95 }), + ); + ground.rotation.x = -Math.PI / 2; + ground.receiveShadow = true; + this.root.add(ground); + + // Simple start grid marks + for (let i = 0; i < 8; i++) { + const mark = new THREE.Mesh( + new THREE.BoxGeometry(1.2, 0.05, 2.4), + new THREE.MeshStandardMaterial({ color: i % 2 ? 0xffffff : 0x222222 }), + ); + const col = i % 4; + const row = Math.floor(i / 4); + mark.position.set(-6 + col * 4, 0.03, -row * 6); + this.root.add(mark); + } + } + + async tryLoadMap(id = 'track') { + const url = `assets/maps/${id}.glb`; + try { + const gltf = await this._loader.loadAsync(url); + // Replace placeholder children with map + while (this.root.children.length) { + const c = this.root.children[0]; + this.root.remove(c); + } + gltf.scene.traverse((o) => { + if (o.isMesh) { + o.castShadow = true; + o.receiveShadow = true; + } + }); + this.root.add(gltf.scene); + this.mapId = id; + return true; + } catch { + return false; + } + } +} diff --git a/www/js/ui-menu.js b/www/js/ui-menu.js new file mode 100644 index 0000000000000000000000000000000000000000..47d19fb9827b2753384ab190328e243219c4a12f --- /dev/null +++ b/www/js/ui-menu.js @@ -0,0 +1,304 @@ +/** Start screen + lobby browser (DOM). */ + +export class GameMenu { + constructor({ + root, + onSandbox, + onOpenMode, + onBack, + onClaim, + onLeaveSeat, + onEnterMatch, + onSpectate, + }) { + this.root = root; + this.onSandbox = onSandbox; + this.onOpenMode = onOpenMode; + this.onBack = onBack; + this.onClaim = onClaim; + this.onLeaveSeat = onLeaveSeat; + this.onEnterMatch = onEnterMatch; + this.onSpectate = onSpectate; + this.screen = 'start'; // start | lobby + this.mode = null; + this.username = null; + this.spaceUrl = null; + this.authError = null; + this.playAllowed = true; + this.selectedLobby = null; + + this.els = { + start: root.querySelector('#menu-start'), + lobby: root.querySelector('#menu-lobby'), + lobbyTitle: root.querySelector('#lobby-title'), + lobbyList: root.querySelector('#lobby-list'), + lobbyMeta: root.querySelector('#lobby-meta'), + userLabel: root.querySelector('#menu-user'), + err: root.querySelector('#menu-error'), + btnBack: root.querySelector('#btn-lobby-back'), + btnEnter: root.querySelector('#btn-lobby-enter'), + btnLeave: root.querySelector('#btn-lobby-leave'), + }; + + root.querySelector('#btn-mode-sandbox')?.addEventListener('click', () => this._pickMode('sandbox')); + root.querySelector('#btn-mode-1v1')?.addEventListener('click', () => this._pickMode('1v1')); + root.querySelector('#btn-mode-4v4')?.addEventListener('click', () => this._pickMode('4v4')); + this.els.btnBack?.addEventListener('click', () => { + this.showStart(); + this.onBack?.(); + }); + this.els.btnEnter?.addEventListener('click', () => this.onEnterMatch?.()); + this.els.btnLeave?.addEventListener('click', () => this.onLeaveSeat?.()); + } + + setIdentity({ username, spaceUrl, authError, playAllowed = true }) { + this.username = username; + this.spaceUrl = spaceUrl; + this.authError = authError; + this.playAllowed = playAllowed !== false; + if (this.els.userLabel) { + if (!username) { + this.els.userLabel.textContent = authError || 'Not signed in — set HF_TOKEN in .env.local'; + } else if (!this.playAllowed) { + this.els.userLabel.textContent = `Spectator · ${username}`; + } else if (String(username).startsWith('guest-')) { + this.els.userLabel.textContent = `Guest · ${username}`; + } else { + this.els.userLabel.textContent = `Signed in as ${username}`; + } + } + if (this.els.btnEnter) this.els.btnEnter.hidden = !this.playAllowed; + if (this.els.btnLeave) this.els.btnLeave.hidden = !this.playAllowed; + const sub = this.root.querySelector('.menu-sub'); + if (sub) { + sub.textContent = this.playAllowed + ? 'Practice · 1v1 · 4v4 Race' + : 'Spectate only — no HF login · play is local-only' + } + // Space / spectate host: hide play mode buttons; lobbies are spectate pickers only. + for (const id of ['btn-mode-sandbox', 'btn-mode-1v1', 'btn-mode-4v4']) { + const btn = this.root.querySelector(`#${id}`); + if (btn) btn.hidden = !this.playAllowed; + } + if (!this.playAllowed && this.els.start) { + // Keep brand; modes hidden. Auto-open sandbox spectate list. + this.showLobby('sandbox'); + this.onOpenMode?.('sandbox'); + } + } + + show() { + this.root.hidden = false; + this.root.classList.add('menu-open'); + this.showStart(); + } + + hide() { + this.root.hidden = true; + this.root.classList.remove('menu-open'); + } + + showStart() { + this.screen = 'start'; + this.mode = null; + this.selectedLobby = null; + if (this.els.start) this.els.start.hidden = false; + if (this.els.lobby) this.els.lobby.hidden = true; + if (this.els.lobbyList) this.els.lobbyList.innerHTML = ''; + if (this.els.lobbyMeta) this.els.lobbyMeta.textContent = ''; + if (this.els.lobbyTitle) this.els.lobbyTitle.textContent = 'Lobbies'; + this._setError(''); + } + + showLobby(mode) { + this.screen = 'lobby'; + this.mode = mode; + if (this.els.start) this.els.start.hidden = true; + if (this.els.lobby) this.els.lobby.hidden = false; + if (this.els.lobbyTitle) { + const titles = { sandbox: 'Practice lobbies', '1v1': '1v1 lobbies', '4v4': '4v4 Race lobbies' }; + this.els.lobbyTitle.textContent = titles[mode] || mode; + } + } + + _pickMode(mode) { + if (!this.username) { + this._setError(this.authError || 'Set HF_TOKEN in .env.local'); + return; + } + if (!this.playAllowed) { + // Spectate-only host: still browse lobbies to pick Spectate. + this._setError(''); + this.showLobby(mode); + this.onOpenMode?.(mode); + return; + } + this._setError(''); + this.showLobby(mode); + this.onOpenMode?.(mode); + } + + _setError(msg) { + if (this.els.err) { + this.els.err.textContent = msg || ''; + this.els.err.hidden = !msg; + } + } + + _seatLabel(seatKey) { + const m = String(seatKey).match(/^([XYS])-(\d+)$/i); + if (!m) return { team: null, label: seatKey, cls: '' }; + const t = m[1].toUpperCase(); + return { + team: t === 'S' ? null : t, + label: m[2], + cls: t === 'X' ? 'team-x' : t === 'Y' ? 'team-y' : '', + }; + } + + _makeSeatBtn(mode, lobby, seat, user) { + const meta = this._seatLabel(seat); + const btn = document.createElement('button'); + btn.type = 'button'; + btn.className = `seat-btn ${meta.cls}${user ? ' filled' : ''}`.trim(); + const locked = + !this.playAllowed || + !!user || + lobby.status === 'live' || + lobby.status === 'starting'; + btn.disabled = locked; + btn.title = locked + ? (lobby.status === 'live' || lobby.status === 'starting' + ? `${lobby.id} is ${lobby.status} — pick another lobby or wait ~20s for it to free` + : (user ? `${user} is seated` : 'Spectate only')) + : seat; + btn.innerHTML = user + ? `${meta.label}${user}` + : meta.label; + if (this.playAllowed) { + btn.addEventListener('click', async () => { + if (btn.disabled) return; + try { + this._setError(''); + btn.disabled = true; + btn.textContent = '…'; + await this.onClaim?.(mode, lobby.id, seat); + this.selectedLobby = lobby.id; + } catch (err) { + this._setError(err.message || String(err)); + } + }); + } else if (locked) { + btn.addEventListener('click', () => { + this._setError(btn.title || 'Seat locked'); + }); + } + return btn; + } + + renderBoard(board, mode, error = null) { + const list = this.els.lobbyList; + if (!list) return; + if (error || !board) { + list.innerHTML = `
Waiting for game server… +
+ ${error || 'Lobby server not reachable.'} +
`; + this._setError(error || 'Lobby server not ready'); + return; + } + this._setError(''); + const key = mode === 'sandbox' ? 'sandbox' : mode === '1v1' ? 'duel' : 'squad'; + const lobbies = board[key] || []; + list.innerHTML = ''; + + for (const lobby of lobbies) { + const card = document.createElement('div'); + card.className = 'lobby-card'; + card.dataset.id = lobby.id; + + const head = document.createElement('div'); + head.className = 'lobby-card-head'; + head.innerHTML = `${lobby.id}${lobby.filled}/${lobby.capacity} · ${lobby.status}`; + card.appendChild(head); + + const seatEntries = Object.entries(lobby.seats || {}); + const xs = seatEntries.filter(([k]) => /^X-/i.test(k)); + const ys = seatEntries.filter(([k]) => /^Y-/i.test(k)); + + if (xs.length || ys.length) { + const row = document.createElement('div'); + row.className = 'lobby-seats teams'; + const left = document.createElement('div'); + left.className = 'team-col team-x'; + const right = document.createElement('div'); + right.className = 'team-col team-y'; + for (const [seat, user] of xs) left.appendChild(this._makeSeatBtn(mode, lobby, seat, user)); + for (const [seat, user] of ys) right.appendChild(this._makeSeatBtn(mode, lobby, seat, user)); + row.appendChild(left); + if (lobby.status === 'live' || lobby.status === 'starting') { + const mid = document.createElement('div'); + mid.className = 'team-mid'; + const spec = document.createElement('button'); + spec.type = 'button'; + spec.className = 'seat-btn spectate-btn'; + spec.textContent = 'Spectate'; + spec.addEventListener('click', async () => { + try { + this._setError(''); + this.selectedLobby = lobby.id; + await this.onSpectate?.(mode, lobby.id); + } catch (err) { + this._setError(err.message || String(err)); + } + }); + mid.appendChild(spec); + row.appendChild(mid); + } + row.appendChild(right); + card.appendChild(row); + } else { + const seats = document.createElement('div'); + seats.className = 'lobby-seats'; + for (const [seat, user] of seatEntries) { + seats.appendChild(this._makeSeatBtn(mode, lobby, seat, user)); + } + if (lobby.status === 'live' || lobby.status === 'starting') { + const spec = document.createElement('button'); + spec.type = 'button'; + spec.className = 'seat-btn spectate-btn'; + spec.textContent = 'Spectate'; + spec.addEventListener('click', async () => { + try { + this._setError(''); + this.selectedLobby = lobby.id; + await this.onSpectate?.(mode, lobby.id); + } catch (err) { + this._setError(err.message || String(err)); + } + }); + seats.appendChild(spec); + } + card.appendChild(seats); + } + + list.appendChild(card); + } + + if (this.els.lobbyMeta) { + if (!this.playAllowed) { + this.els.lobbyMeta.textContent = + 'Spectate only — click Spectate on a live lobby. No HF login required.'; + } else if (mode === '1v1') { + this.els.lobbyMeta.textContent = + 'Red vs blue — race starts when both grid seats are filled. Spectate is read-only.'; + } else if (mode === '4v4') { + this.els.lobbyMeta.textContent = + 'Red (left) vs blue (right). Starts when full, or after 60s idle with ≥1 per side.'; + } else { + this.els.lobbyMeta.textContent = + 'Practice starts when you join. Spectate watches a live race without taking a seat.'; + } + } + } +} diff --git a/www/js/vehicles.js b/www/js/vehicles.js new file mode 100644 index 0000000000000000000000000000000000000000..e1f47e00444cc2ca833996b7a8402db2c98c50aa --- /dev/null +++ b/www/js/vehicles.js @@ -0,0 +1,93 @@ +/** + * Local car — box placeholder until GLBs land in assets/cars/. + * Load path convention: assets/cars/.glb + */ + +import * as THREE from 'three'; +import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; + +const MAX_SPEED = 42; +const ACCEL = 28; +const BRAKE = 40; +const DRAG = 6; +const TURN_RATE = 2.1; + +export class LocalCar { + constructor(scene, { color = 0xc44b3c } = {}) { + this.scene = scene; + this.speed = 0; + this.yaw = 0; + this.mesh = new THREE.Mesh( + new THREE.BoxGeometry(1.6, 0.55, 3.2), + new THREE.MeshStandardMaterial({ color, metalness: 0.4, roughness: 0.4 }), + ); + this.mesh.castShadow = true; + this.mesh.position.set(0, 0.35, 0); + this.position = this.mesh.position; + scene.add(this.mesh); + this._loader = new GLTFLoader(); + this.modelId = null; + } + + /** Try loading assets/cars/.glb; keep box if missing. */ + async tryLoadModel(id = 'car') { + const url = `assets/cars/${id}.glb`; + try { + const gltf = await this._loader.loadAsync(url); + const model = gltf.scene; + model.traverse((o) => { + if (o.isMesh) { + o.castShadow = true; + o.receiveShadow = true; + } + }); + this.scene.remove(this.mesh); + this.mesh.geometry?.dispose?.(); + this.mesh.material?.dispose?.(); + this.mesh = model; + this.position = this.mesh.position; + this.scene.add(this.mesh); + this.modelId = id; + return true; + } catch { + return false; + } + } + + setPose(x, y, z, yaw = 0) { + this.mesh.position.set(x, y, z); + this.yaw = yaw; + this.mesh.rotation.y = yaw; + this.speed = 0; + } + + update(delta, input) { + const throttle = input?.throttle || 0; + const steer = input?.steer || 0; + const brake = input?.brake || 0; + + if (brake) { + this.speed = THREE.MathUtils.lerp(this.speed, 0, Math.min(1, brake * BRAKE * delta / Math.max(1, Math.abs(this.speed)))); + } else { + this.speed += throttle * ACCEL * delta; + } + this.speed -= Math.sign(this.speed) * DRAG * delta; + this.speed = THREE.MathUtils.clamp(this.speed, -MAX_SPEED * 0.35, MAX_SPEED); + + if (Math.abs(this.speed) > 0.4) { + this.yaw += steer * TURN_RATE * (this.speed / MAX_SPEED) * delta; + } + + this.mesh.rotation.y = this.yaw; + this.mesh.position.x += Math.sin(this.yaw) * this.speed * delta; + this.mesh.position.z += Math.cos(this.yaw) * this.speed * delta; + } + + speedKmh() { + return Math.round(Math.abs(this.speed) * 3.6); + } + + dispose() { + this.scene.remove(this.mesh); + } +}