aboutsummaryrefslogtreecommitdiff
path: root/src/spectate
diff options
context:
space:
mode:
Diffstat (limited to 'src/spectate')
-rw-r--r--src/spectate/index.html6
-rw-r--r--src/spectate/main.ts93
-rw-r--r--src/spectate/server.rs28
3 files changed, 124 insertions, 3 deletions
diff --git a/src/spectate/index.html b/src/spectate/index.html
index 235a894..94ef698 100644
--- a/src/spectate/index.html
+++ b/src/spectate/index.html
@@ -4,8 +4,12 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>GPN Tron</title>
+ <script src="main.js"></script>
</head>
<body>
-
+ <noscript>
+ This is the GPN-Tron spectator application. You need JavaScript to
+ run it.
+ </noscript>
</body>
</html>
diff --git a/src/spectate/main.ts b/src/spectate/main.ts
new file mode 100644
index 0000000..4d508b6
--- /dev/null
+++ b/src/spectate/main.ts
@@ -0,0 +1,93 @@
+/// <reference lib="dom" />
+
+const ws = new WebSocket("/events")
+
+type Packet = "tick"
+ | { pos: { id: number, x: number, y: number } }
+ | { game: { width: number, height: number } }
+ | { player: { id: number, name: string } }
+
+class Snake {
+ parts: { x: number, y: number, dx: number, dy: number }[] = []
+ constructor(public name: string) { }
+ add_part(x: number, y: number) {
+ if (!this.parts.length) return this.parts.push({ x, y, dx: 0, dy: 0 })
+ const last = this.parts[this.parts.length - 1]
+ let dx = x - last.x, dy = y - last.y;
+ if (x > last.x + 1 || x < last.x - 1) dx *= -1, dx /= Math.abs(dx)
+ if (y > last.y + 1 || y < last.y - 1) dy *= -1, dy /= Math.abs(dy)
+ this.parts.push({ x, y, dx, dy })
+ console.log(this.parts);
+
+ }
+}
+let size = 0
+let snakes = new Map<number, Snake>()
+
+let canvas: HTMLCanvasElement
+let ctx: CanvasRenderingContext2D
+
+document.addEventListener("DOMContentLoaded", () => {
+ canvas = document.createElement("canvas")
+ ctx = canvas.getContext("2d")!
+ document.body.append(canvas)
+ canvas.width = 1000;
+ canvas.height = 1000;
+ redraw()
+})
+
+function redraw() {
+ ctx.fillStyle = "black"
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
+
+ ctx.save()
+ const scale = canvas.width / size
+ ctx.scale(scale, scale)
+ for (let x = 0; x < size; x++) {
+ for (let y = 0; y < size; y++) {
+ ctx.strokeStyle = "grey"
+ ctx.lineWidth = 0.02
+ ctx.strokeRect(x, y, 1, 1)
+ }
+ }
+ ctx.translate(0.5, 0.5)
+ for (const [xo, yo] of [[0, 0], [-size, 0], [size, 0], [0, -size], [0, size]]) {
+ ctx.save()
+ ctx.translate(xo, yo)
+ for (const snake of snakes.values()) {
+ ctx.beginPath();
+ for (let i = 0; i < snake.parts.length; i++) {
+ const p = snake.parts[i];
+ ctx.moveTo(p.x - p.dx, p.y - p.dy)
+ ctx.lineTo(p.x, p.y)
+ }
+ ctx.lineCap = "round"
+ ctx.lineJoin = "round"
+ ctx.lineWidth = 0.6;
+ ctx.strokeStyle = "red"
+ ctx.stroke()
+ }
+ ctx.restore()
+ }
+ ctx.restore()
+
+ requestAnimationFrame(redraw)
+}
+
+ws.onerror = console.error
+ws.onmessage = message => {
+ const p = JSON.parse(message.data) as Packet
+ console.log(p);
+ if (p == "tick") {
+
+ } else if ("game" in p) {
+ snakes.clear()
+ size = p.game.width
+ } else if ("player" in p) {
+ snakes.set(p.player.id, new Snake(p.player.name))
+ } else if ("pos" in p) {
+ snakes.get(p.pos.id)?.add_part(p.pos.x, p.pos.y)
+ }
+
+}
+
diff --git a/src/spectate/server.rs b/src/spectate/server.rs
index 9dbfebe..e3e9f80 100644
--- a/src/spectate/server.rs
+++ b/src/spectate/server.rs
@@ -6,6 +6,7 @@ use anyhow::Result;
use axum::extract;
use axum::extract::connect_info::ConnectInfo;
use axum::extract::ws::Message;
+use axum::http::HeaderMap;
use axum::response::Html;
use axum::{
extract::ws::{WebSocket, WebSocketUpgrade},
@@ -13,8 +14,10 @@ use axum::{
routing::get,
Router,
};
+use headers::ContentType;
use log::{info, warn};
use std::net::SocketAddr;
+use std::str::FromStr;
use std::sync::Arc;
use tokio::spawn;
use tokio::sync::{broadcast, RwLock};
@@ -32,6 +35,7 @@ pub async fn spectate_server(config: Config, state: Arc<State>) -> Result<()> {
spawn(broadcaster(sstate.clone(), state));
let app = Router::new()
.route("/", get(index))
+ .route("/main.js", get(javascript))
.route("/events", get(ws_handler))
.with_state(sstate);
let listener = tokio::net::TcpListener::bind(config.bind).await.unwrap();
@@ -47,6 +51,26 @@ pub async fn spectate_server(config: Config, state: Arc<State>) -> Result<()> {
async fn index() -> Html<&'static str> {
Html(include_str!("index.html"))
}
+#[cfg(debug_assertions)]
+async fn javascript() -> (HeaderMap, String) {
+ use headers::HeaderMapExt;
+ use tokio::fs::read_to_string;
+ let mut hm = HeaderMap::new();
+ hm.typed_insert(ContentType::from_str("application/javascript").unwrap());
+ (
+ hm,
+ read_to_string(concat!(env!("OUT_DIR"), "/main.js"))
+ .await
+ .unwrap(),
+ )
+}
+#[cfg(not(debug_assertions))]
+async fn javascript() -> (HeaderMap, &'static str) {
+ use headers::HeaderMapExt;
+ let mut hm = HeaderMap::new();
+ hm.typed_insert(ContentType::from_str("application/javascript").unwrap());
+ (hm, include_str!(concat!(env!("OUT_DIR"), "/main.js")))
+}
async fn broadcaster(sstate: Arc<SpectateState>, state: Arc<State>) {
let mut ticks = state.tick.subscribe();
@@ -59,8 +83,8 @@ async fn broadcaster(sstate: Arc<SpectateState>, state: Arc<State>) {
sstate.past_events.write().await.clear();
events.push(Packet::Game {
my_id: 0,
- width: g.size.x as usize,
- height: g.size.y as usize,
+ width: g.map.size.x as usize,
+ height: g.map.size.y as usize,
});
for (player, (_, _, name)) in &g.heads {
events.push(Packet::Player {