import {Logger, logSetup} from "./logger"; import {AttachedModule, gateway_connect, GatewayClient} from "./gateway"; import {Player} from "./protocol/player"; import {Planet, PlanetType} from "./protocol/planet"; import {Module, ModuleType} from "./protocol/module"; import { MessageC2SAuthenticateAndBeamOut, MessageC2SAuthenticateAndBeamOut_packetInfo, MessageC2SInput, MessageC2SInput_packetInfo, MessageC2SModuleDetach, MessageC2SModuleDetach_packetInfo, MessageC2SModuleGrabBegin, MessageC2SModuleGrabBegin_packetInfo, MessageC2SModuleGrabEnd, MessageC2SModuleGrabEnd_packetInfo, MessageC2SMouseInput, MessageC2SMouseInput_packetInfo } from "./protocol/message_c2s"; import {encode} from "./serde"; import {InputType} from "./protocol/input"; import {createParticle, tickAndDrawParticles} from "./particle"; logSetup(); const logger = new Logger("client"); export interface GlobalData { client: GatewayClient | null, players: Player[], planets: Planet[], modules: Module[], me: Player | null, canvas: HTMLCanvasElement, spritesheet_img: HTMLImageElement | null, spritesheet: object | null, context: CanvasRenderingContext2D, keys: Keys, velocity: number, x_vel: number, y_vel: number can_beam_out: boolean, direction_radians: number, mouse_x: number, mouse_y: number, tree: Map, clicked: number | null, } export interface Keys { up: boolean, left: boolean, down: boolean, right: boolean } export const global: GlobalData = { client: null, players: [], planets: [], modules: [], me: null, canvas: document.getElementById("canvas"), spritesheet_img: null, spritesheet: null, context: (document.getElementById("canvas")).getContext("2d"), keys: { up: false, down: false, left: false, right: false }, velocity: 0, x_vel: 0, y_vel: 0, can_beam_out: false, direction_radians: 0, mouse_x: 0, mouse_y: 0, tree: new Map(), clicked: null, } async function client_main(server: string, username: string, texture_quality: string) { logger.info("StarKingdoms client - starting"); if (window.localStorage.getItem("token") !== null && window.localStorage.getItem("user") !== null) { global.can_beam_out = true; document.getElementById("beamout")!.style.setProperty("display", "block"); document.getElementById("beamout")!.addEventListener("click", () => { let msg = MessageC2SAuthenticateAndBeamOut.encode({ userId: window.localStorage.getItem("user")!, token: window.localStorage.getItem("token")! }).finish(); global.client?.socket.send(encode(MessageC2SAuthenticateAndBeamOut_packetInfo.type, msg)); }) } logger.info("Loading textures"); let spritesheet_url = `/assets/dist/spritesheet-${texture_quality}.png`; let spritesheet_data_url = `/assets/dist/spritesheet-${texture_quality}.json`; let load_textures = new Promise(async (resolve) => { const image_promise: Promise = new Promise((resolve, reject) => { const image = document.createElement("img"); image.src = spritesheet_url; image.onload = () => { resolve(image); } image.onerror = err => reject(err); }); const dat_promise: Promise = fetch(spritesheet_data_url).then(res => res.json()); let image = await image_promise; let data = await dat_promise; global.spritesheet_img = image; global.spritesheet = data; resolve(); }); await load_textures; logger.info("Starting the renderer"); //let sprite = PIXI.Sprite.from(global.spritesheet?.textures["hearty.png"]); global.client = await gateway_connect(server, username); global.canvas.width = window.innerWidth; global.canvas.height = window.innerHeight; window.onresize = () => { global.canvas.width = window.innerWidth; global.canvas.height = window.innerHeight; } global.canvas.style.setProperty("background-image", `url("/assets/final/${texture_quality}/starfield.png")`); let canvas = document.getElementById("canvas")!; let thruster_counter = 0; document.onmousedown = (e) => { // convert screenspace to worldspace if (global.me !== null) { let worldX = e.clientX - window.innerWidth / 2 + global.me?.x; let worldY = e.clientY - window.innerHeight / 2 + global.me?.y; let button: InputType; if (e.button == 0) { button = InputType.Left; } else if (e.button == 1) { button = InputType.Middle; } else if (e.button == 2) { button = InputType.Right; } for (let i = 0; i < global.modules.length; i++) { if (global.modules[i].moduleType == ModuleType.LandingThrusterSuspension) { console.log("hi"); continue; } let relativeX = global.modules[i].x - worldX; let relativeY = global.modules[i].y - worldY; let rot = -global.modules[i].rotation; relativeX = relativeX*Math.cos(rot) - relativeY*Math.sin(rot); relativeY = relativeX*Math.sin(rot) + relativeY*Math.cos(rot); let bound = [-25, 25, -25, 25]; if (global.modules[i].moduleType == ModuleType.Cargo) { bound = [-18.75, 18.75, -25, 21.875]; } if (bound[0] < relativeX && relativeX < bound[1]) { if (bound[2] < relativeY && relativeY < bound[3]) { let msg = MessageC2SModuleGrabBegin.encode({ moduleId: global.modules[i].id, worldposX: worldX, worldposY: worldY, }).finish(); global.clicked = global.modules[i].id; global.client?.socket.send(encode(MessageC2SModuleGrabBegin_packetInfo.type, msg)); } } } global.tree.forEach((value: AttachedModule, key: number) => { if (value.module_type == ModuleType.LandingThrusterSuspension) { console.log("hi"); return; } let relativeX = value.x - worldX; let relativeY = value.y - worldY; let rot = -value.rotation; let adjustedX = relativeX*Math.cos(rot) - relativeY*Math.sin(rot); let adjustedY = relativeX*Math.sin(rot) + relativeY*Math.cos(rot); let bound = [-25, 25, -25, 25]; if (value.module_type == ModuleType.Cargo) { bound = [-18.75, 18.75, -25, 21.875]; } if (bound[0] < adjustedX && adjustedX < bound[1]) { if (bound[2] < adjustedY && adjustedY < bound[3]) { let msg = MessageC2SModuleDetach.encode({ moduleId: key, }).finish(); global.client?.socket.send(encode(MessageC2SModuleDetach_packetInfo.type, msg)); } } }); let msg = MessageC2SMouseInput.encode({ worldposX: worldX, worldposY: worldY, released: false, button: button! }).finish(); global.client?.socket.send(encode(MessageC2SMouseInput_packetInfo.type, msg)) } } document.onmouseup = (e) => { // convert screenspace to worldspace if (global.me !== null) { let worldX = e.clientX - window.innerWidth / 2 + global.me?.x; let worldY = e.clientY - window.innerHeight / 2 + global.me?.y; let button: InputType; if (e.button == 0) { button = InputType.Left; } else if (e.button == 1) { button = InputType.Middle; } else if (e.button == 2) { button = InputType.Right; } let msg = MessageC2SMouseInput.encode({ worldposX: worldX, worldposY: worldY, released: true, button: button! }).finish(); for (let i = 0; i < global.modules.length; i++) { if(global.clicked === global.modules[i].id) { let msg = MessageC2SModuleGrabEnd.encode({ moduleId: global.modules[i].id, worldposX: worldX, worldposY: worldY, }).finish(); global.client?.socket.send(encode(MessageC2SModuleGrabEnd_packetInfo.type, msg)) } } global.clicked = null; global.client?.socket.send(encode(MessageC2SMouseInput_packetInfo.type, msg)) } } document.onmousemove = (e) => { let canvasLeft = canvas.offsetLeft + canvas.clientLeft; let canvasTop = canvas.offsetTop + canvas.clientTop; global.mouse_x = e.pageX - canvasLeft; global.mouse_y = e.pageY - canvasTop; } document.onkeydown = (e) => { if (e.code == "ArrowLeft" || e.code == "KeyA") { // arrow-left global.keys.left = true; } else if (e.code == "ArrowRight" || e.code == "KeyD") { // arrow-right global.keys.right = true; } else if (e.code == "ArrowUp" || e.code == "KeyW") { // arrow-up global.keys.up = true; } else if (e.code == "ArrowDown" || e.code == "KeyS") { // arrow-down global.keys.down = true; } let msg = MessageC2SInput.encode({ upPressed: global.keys.up, downPressed: global.keys.down, leftPressed: global.keys.left, rightPressed: global.keys.right }).finish(); global.client?.socket.send(encode(MessageC2SInput_packetInfo.type, msg)); } document.onkeyup = (e) => { if (e.code == "ArrowLeft" || e.code == "KeyA") { // arrow-left global.keys.left = false; } else if (e.code == "ArrowRight" || e.code == "KeyD") { // arrow-right global.keys.right = false; } else if (e.code == "ArrowUp" || e.code == "KeyW") { // arrow-up global.keys.up = false; } else if (e.code == "ArrowDown" || e.code == "KeyS") { // arrow-down global.keys.down = false; } let msg = MessageC2SInput.encode({ upPressed: global.keys.up, downPressed: global.keys.down, leftPressed: global.keys.left, rightPressed: global.keys.right }).finish(); global.client?.socket.send(encode(MessageC2SInput_packetInfo.type, msg)); } let t = performance.now(); let delta = 0.0; let render = (newT: DOMHighResTimeStamp) => { delta = newT - t; t = newT; let viewer_size_x = global.canvas.width; let viewer_size_y = global.canvas.height; global.canvas.style.setProperty("background-position", `${-global.me?.x!/5}px ${-global.me?.y!/5}px`); global.context.setTransform(1, 0, 0, 1, 0, 0); global.context.clearRect(0, 0, viewer_size_x, viewer_size_y); // *dont* translate the camera. we're movign everything else around us. cameracentrism. // only translation will be to center our core module. global.context.translate(viewer_size_x / 2, viewer_size_y / 2); global.context.save(); global.context.rotate(global.direction_radians + Math.PI); // @ts-ignore let indic_tex = global.spritesheet!["frames"]["trackindicator.png"]; global.context.drawImage(global.spritesheet_img!, indic_tex.frame.x, indic_tex.frame.y, indic_tex.frame.w-1, indic_tex.frame.h-1, -indic_tex.frame.w/2, -indic_tex.frame.h/2, indic_tex.frame.w, indic_tex.frame.h ); global.context.restore(); if (global.me !== null) { document.getElementById("pos")!.innerText = `Position: ${Math.trunc(global.me.x)}, ${Math.trunc(global.me.y)}`; } document.getElementById("vel")!.innerText = `Velocity: ${Math.trunc(global.velocity)}`; document.getElementById("vel-dir")!.innerText = `Track Angle: ${global.direction_radians}` for (let i = 0; i < global.planets.length; i++) { let planet = global.planets[i]; // @ts-ignore let tex = global.spritesheet!["frames"][planet_type_to_tex_id(planet.planetType)]; global.context.drawImage(global.spritesheet_img!, tex.frame.x, // sx tex.frame.y, // sy tex.frame.w-1, // sw tex.frame.h-1, // sh (planet.x - planet.radius - global.me?.x!), // dx (planet.y - planet.radius - global.me?.y!), // dy planet.radius * 2, // dw planet.radius * 2); // dh if (planet.planetType == PlanetType.Moon) { if (global.me !== null) { global.context.beginPath(); global.context.strokeStyle = "gray"; global.context.lineWidth = 5; global.context.moveTo(global.me!.x - global.me!.x, global.me!.y - global.me!.y); global.context.lineTo(planet.x - global.me!.x, planet.y - global.me!.y); global.context.stroke(); document.getElementById("pos-moon")!.innerText = `Relative to Moon: ${Math.trunc(global.me!.x - planet.x)}, ${Math.trunc(global.me!.y - planet.y)}` } } else if (planet.planetType == PlanetType.Earth) { if (global.me !== null) { global.context.beginPath(); global.context.strokeStyle = "limegreen"; global.context.lineWidth = 5; global.context.moveTo(global.me!.x - global.me!.x, global.me!.y - global.me!.y); global.context.lineTo(planet.x - global.me!.x, planet.y - global.me!.y); global.context.stroke(); } } else if (planet.planetType == PlanetType.Mars) { if (global.me !== null) { global.context.beginPath(); global.context.strokeStyle = "orange"; global.context.lineWidth = 5; global.context.moveTo(global.me!.x - global.me!.x, global.me!.y - global.me!.y); global.context.lineTo(planet.x - global.me!.x, planet.y - global.me!.y); global.context.stroke(); } } } for (let i = 0; i < global.modules.length; i++) { if (global.me !== null) { let module = global.modules[i]; // @ts-ignore let tex = global.spritesheet!["frames"][module_type_to_tex_id(module.moduleType, (module.flags & 1) != 0)]; global.context.save(); // x_{screen} = x_{world} - player_{x_{world}} // x_{world} = x_{screen} + player_{x_{world}} global.context.translate(module.x - global.me!.x, module.y - global.me!.y); global.context.rotate(module.rotation); global.context.drawImage(global.spritesheet_img!, tex.frame.x, tex.frame.y, tex.frame.w-1, tex.frame.h-1, -25, -25, 50, 50); global.context.restore(); if (global.clicked == module.id) { global.context.save(); global.context.translate(global.mouse_x - window.innerWidth/2, global.mouse_y - window.innerHeight/2); global.context.rotate(module.rotation); global.context.globalAlpha = 0.6; global.context.drawImage(global.spritesheet_img!, tex.frame.x, tex.frame.y, tex.frame.w-1, tex.frame.h-1, -25, -25, 50, 50); global.context.globalAlpha = 1.0; global.context.restore(); } } } global.tree.forEach((value: AttachedModule, _key: number) => { if (global.me !== null) { // @ts-ignore let tex = global.spritesheet!["frames"][module_type_to_tex_id(value.module_type, true)]; global.context.save(); global.context.translate(value.x - global.me!.x, value.y - global.me!.y); global.context.rotate(value.rotation); global.context.drawImage(global.spritesheet_img!, tex.frame.x, tex.frame.y, tex.frame.w-1, tex.frame.h-1, -25, -25, 50, 50); global.context.restore(); } }); function isStarkingdomsBirthday() { let inputDate = new Date("3/20/2022"); let todaysDate = new Date(); return inputDate.getMonth() == todaysDate.getMonth() && inputDate.getDate() == todaysDate.getDate(); } function isRustBirthday() { let inputDate = new Date("5/15/2015"); let todaysDate = new Date(); return inputDate.getMonth() == todaysDate.getMonth() && inputDate.getDate() == todaysDate.getDate(); } for (let i = 0; i < global.players.length; i++) { if (global.me !== null) { let player = global.players[i]; // @ts-ignore //let tex = global.spritesheet!["frames"]["hearty.png"]; //let tex = global.spritesheet!["frames"]["hearty_party.png"]; //let tex = global.spritesheet!["frames"]["hearty_ferris.png"]; let tex; if (isStarkingdomsBirthday()) { // @ts-ignore tex = global.spritesheet!["frames"]["hearty_party.png"]; } else if (isRustBirthday()) { // @ts-ignore tex = global.spritesheet!["frames"]["hearty_ferris.png"]; } else { // @ts-ignore tex = global.spritesheet!["frames"]["hearty.png"]; } global.context.save(); global.context.translate(player.x - global.me!.x, player.y - global.me!.y); global.context.textAlign = "center"; global.context.font = "30px Segoe UI"; global.context.fillStyle = "white"; global.context.fillText(player.username, 0, -35); global.context.rotate(player.rotation); global.context.drawImage(global.spritesheet_img!, tex.frame.x, // sx tex.frame.y, // sy tex.frame.w-1, // sw tex.frame.h-1, // sh -25, -25, 50, 50); // dh global.context.restore(); } } function calculateRotated(x: number, y: number, rotation: number): [number, number] { let x2 = x * Math.cos(rotation) - y * Math.sin(rotation); let y2 = x * Math.sin(rotation) + y * Math.cos(rotation); return [x2, y2]; } const thruster_r = 52; const thruster_g = 189; const thruster_b = 235; const thruster_start_a = 80; const thruster_end_a = 0; const thruster_start_size = 4; const thruster_end_size = 50; const thruster_final_rotation = 90; function createThrusterParticle(x: number, y: number, vel_x: number, vel_y: number) { createParticle({ x: global.me!.x + calculateRotated(x, y, global.me!.rotation)[0], y: global.me!.y + calculateRotated(x, y, global.me!.rotation)[1], lifetime: 500, timer: 0, startSize: thruster_start_size, finalSize: thruster_end_size, startRotation: 0, finalRotation: thruster_final_rotation, startOpacity: thruster_start_a, endOpacity: thruster_end_a, startR: thruster_r, startG: thruster_g, startB: thruster_b, endR: thruster_r, endG: thruster_g, endB: thruster_b, velocity_x: calculateRotated(vel_x, vel_y, global.me!.rotation)[0], velocity_y: calculateRotated(vel_x, vel_y, global.me!.rotation)[1] }); } if (global.me !== null) { thruster_counter += 1; //thruster_counter = 1; // uncomment this line to disable particle limits if (thruster_counter > 3) { thruster_counter = 0; } else if (thruster_counter == 1) { if (global.keys.up) { // two backward thrusters // this one is blue createThrusterParticle(-25, 25, 0, 0.5); createThrusterParticle(25, 25, 0, 0.5); } if (global.keys.down) { // two backward thrusters // this one is blue createThrusterParticle(25, -25, 0, -0.5); createThrusterParticle(-25, -25, 0, -0.5); } if (global.keys.left) { // two backward thrusters // this one is blue createThrusterParticle(25, 25, 0, 0.5); createThrusterParticle(-25, -25, 0, -0.5); } if (global.keys.right) { // two backward thrusters // this one is blue createThrusterParticle(-25, 25, 0,0.5); createThrusterParticle(25, -25, 0, -0.5); } } tickAndDrawParticles(delta); } requestAnimationFrame(render); } requestAnimationFrame(render); } let query = new URLSearchParams(window.location.search); if (!(query.has("server") || query.has("username") || query.has("textures"))) { window.location.href = "/index.html"; } client_main(query.get("server")!, query.get("username")!, query.get("textures")!).then(() => {}); function planet_type_to_tex_id(ty: PlanetType): string { if (ty == PlanetType.Earth) { return "earth.png" } else if (ty == PlanetType.Moon) { return "moon.png" } else if (ty == PlanetType.Mars) { return "mars.png" } return "unknown.png" } function module_type_to_tex_id(ty: ModuleType, is_on: boolean): string { if (!is_on) { if (ty == ModuleType.Cargo) { return "cargo_off.png" } else if (ty == ModuleType.LandingThruster) { return "landingthruster_off.png" } else if (ty == ModuleType.LandingThrusterSuspension) { return "landingleg.png" } else if (ty == ModuleType.Hub) { return "hub_off.png" } } else { if (ty == ModuleType.Cargo) { return "cargo_on.png" } else if (ty == ModuleType.LandingThruster) { return "landingthruster_on.png" } else if (ty == ModuleType.LandingThrusterSuspension) { return "landingleg.png" } else if (ty == ModuleType.Hub) { return "hub_on.png" } } return "unknown.png" }