Game development: how do I implement a camera system to follow characters (js)

This is my HTML code, and my server.js code is below it. I’m looking for any tips on how to make my code better because it’s a bit buggy in terms of actually deleting players, and more importantly a camera system so I can create a more extensive map with checkpoints. I can’t find any good guides online.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Multiplayer Game</title>
  <style>
    body { margin: 0; overflow: hidden; }
    canvas { background-color: #eee; display: block; }
    #startScreen {
      position: absolute;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
      background: white;
      padding: 20px;
      border: 1px solid #000;
      z-index: 10;
      display: flex;
      flex-direction: column;
      align-items: center;
    }
    #iconSelection img {
      width: 50px;
      height: 50px;
      margin: 5px;
      cursor: pointer;
      border: 2px solid transparent;
    }
    #iconSelection img.selected {
      border: 2px solid blue;
    }
  </style>
</head>
<body>
  <div id="startScreen">
    <h2>Enter Your Name</h2>
    <input type="text" id="playerNameInput" placeholder="Player Name" />
    <h3>Select an Icon</h3>
    <div id="iconSelection">
      <img src="image1.png" alt="Icon 1" class="icon" data-icon="image1.png">
      <img src="image2.webp" alt="Icon 2" class="icon" data-icon="image2.webp">
      <img src="image3.png" alt="Icon 3" class="icon" data-icon="image3.png">
      <!-- Add more icons as needed -->
    </div>
    <button id="startGameButton">Start Game</button>
  </div>
  <canvas id="gameCanvas"></canvas>

  <script>
    const canvas = document.getElementById('gameCanvas');
    const ctx = canvas.getContext('2d');
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;

    const GRASS_HEIGHT = 50; // Height of the grass section

    let playerName = "";
    let playerIcon = "";
    let players = {}; // Store all players keyed by their socket ID
    let myPlayerId = null; // Store the current player's ID
    let gameStarted = false;

    class Player {
      constructor(id, x, y, w, h, spritePath, name, opacity = 1) {
        this.id = id; // Unique ID for each player
        this.x = x;
        this.y = y;
        this.w = w;
        this.h = h;
        this.speedX = 0;
        this.speedY = 0;
        this.gravity = 1; // Gravity strength
        this.jumpSpeed = -15; // Jump speed
        this.sprite = new Image();
        this.sprite.src = spritePath;
        this.loaded = false;
        this.opacity = opacity; // Set opacity for players
        this.name = name; // Player's name
        this.onObject = false; // Track if the player is on a parkour object

        this.sprite.onload = () => {
          this.loaded = true;
        };
      }

      draw() {
        if (this.loaded) {
          ctx.globalAlpha = this.opacity; // Set opacity for drawing
          ctx.drawImage(this.sprite, this.x, this.y, this.w, this.h);
          ctx.globalAlpha = 1; // Reset opacity back to full

          // Draw the player's name above the character
          ctx.fillStyle = 'black';
          ctx.font = '16px Arial';
          ctx.textAlign = 'center';
          ctx.fillText(this.name, this.x + this.w / 2, this.y - 10); // Positioning the name above the player
        }
      }

      update(parkourObjects) {
        // Check horizontal collisions first
        this.x += this.speedX; // Update horizontal position

        // Handle collisions with parkour objects when moving horizontally
        for (let obj of parkourObjects) {
          if (this.collidesWith(obj)) {
            if (this.speedX > 0) { // Moving right
              this.x = obj.x - this.w; // Move player to the left of the object
            } else if (this.speedX < 0) { // Moving left
              this.x = obj.x + obj.w; // Move player to the right of the object
            }
          }
        }

        // Apply gravity
        this.y += this.speedY; // Apply current vertical speed
        this.speedY += this.gravity; // Apply gravity to vertical speed

        // Ground check
        if (this.y + this.h >= canvas.height - GRASS_HEIGHT) {
          this.y = canvas.height - this.h - GRASS_HEIGHT; // Position on the grass
          this.speedY = 0; // Reset vertical speed
          this.onObject = false; // Reset onObject flag
        }

        // Reset onObject flag before checking collisions
        this.onObject = false;

        // Check collision with parkour objects
        for (let obj of parkourObjects) {
          if (this.collidesWith(obj)) {
            // Collision response for falling onto an object
            if (this.speedY > 0 && this.y + this.h <= obj.y + obj.h) {
              this.y = obj.y - this.h; // Position on top of the object
              this.speedY = 0; // Reset vertical speed
              this.onObject = true; // Set onObject to true
            }
            // Stop upward movement if colliding with the bottom of the parkour object
            if (this.speedY < 0 && this.y <= obj.y + obj.h && this.y + this.h > obj.y) {
              this.y = obj.y + obj.h; // Position just below the object
              this.speedY = 0; // Stop vertical movement
            }
          }
        }

        this.draw();
      }

      collidesWith(obj) {
        return this.x < obj.x + obj.w &&
               this.x + this.w > obj.x &&
               this.y < obj.y + obj.h &&
               this.y + this.h > obj.y;
      }

      jump() {
        // Allow jumping if on the ground or on top of any parkour object
        const onGround = this.y + this.h >= canvas.height - GRASS_HEIGHT; // Check if on ground (grass)

        // Allow jump if on the ground or on a parkour object
        if (onGround || this.onObject) {
          this.speedY = this.jumpSpeed; // Set the jump speed
        }
      }
    }

    class ParkourObject {
      constructor(x, y, w, h, color) {
        this.x = x;
        this.y = y;
        this.w = w;
        this.h = h;
        this.color = color;
      }

      draw() {
        ctx.fillStyle = this.color;
        ctx.fillRect(this.x, this.y, this.w, this.h);
      }
    }

    class Controller {
      constructor() {
        this.up = false;
        this.right = false;
        this.left = false;

        let keyEvent = (e) => {
          if (e.code === "KeyW" || e.code === "ArrowUp") { this.up = e.type === 'keydown'; }
          if (e.code === "KeyD" || e.code === "ArrowRight") { this.right = e.type === 'keydown'; }
          if (e.code === "KeyA" || e.code === "ArrowLeft") { this.left = e.type === 'keydown'; }
        };

        addEventListener('keydown', keyEvent);
        addEventListener('keyup', keyEvent);
      }
    }

    // Create parkour objects
    const parkourObjects = [
      new ParkourObject(200, canvas.height - GRASS_HEIGHT - 100, 100, 20, 'brown'), // Parkour box 1
      new ParkourObject(400, canvas.height - GRASS_HEIGHT - 50, 100, 20, 'brown'), // Parkour box 2
    ];

    // Initialize controller
    const controller1 = new Controller();

    // WebSocket connection
       // WebSocket connection
       const socket = new WebSocket('ws://localhost:8080');

socket.addEventListener('open', () => {
  console.log('WebSocket connection established');
});

// Listen for messages
socket.onmessage = (event) => {
  event.data.text().then((text) => {
    const data = JSON.parse(text);
    if (data.type === 'positionUpdate') {
      // Update player positions
      if (!players[data.id]) {
        players[data.id] = new Player(data.id, data.x, data.y, 50, 50, data.icon, data.name, 0.5);
      } else {
        players[data.id].x = data.x;
        players[data.id].y = data.y;
      }
    } else if (data.type === 'playerDisconnected') {
      // Remove player when they disconnect
      delete players[data.id];
    }
  }).catch((error) => console.error("Error parsing message:", error));
};

// Handle page unload event to notify server of disconnection
window.addEventListener('beforeunload', () => {
  if (myPlayerId) {
    socket.send(JSON.stringify({
      type: 'disconnect',
      id: myPlayerId
    }));
  }
});

    // Function to update positions based on controls
    function updatePosition() {
      if (myPlayerId) {
        const player = players[myPlayerId];
        if (controller1.up) {
          player.jump();
        }
        if (controller1.right) {
          player.speedX = 5; // Move right
        } else if (controller1.left) {
          player.speedX = -5; // Move left
        } else {
          player.speedX = 0; // Stop moving
        }
      }
    }

    // Main game loop
    function gameLoop() {
      ctx.clearRect(0, 0, canvas.width, canvas.height); // Clear canvas
      ctx.fillStyle = 'green'; // Grass color
      ctx.fillRect(0, canvas.height - GRASS_HEIGHT, canvas.width, GRASS_HEIGHT); // Draw grass

      // Draw parkour objects
      parkourObjects.forEach(obj => obj.draw());

      // Update and draw all players
      for (const id in players) {
        players[id].update(parkourObjects);
      }

      updatePosition(); // Update my player position

      // Send position updates if WebSocket is open
      if (socket.readyState === WebSocket.OPEN && myPlayerId) {
        socket.send(JSON.stringify({
          type: 'positionUpdate',
          id: myPlayerId,
          x: players[myPlayerId].x,
          y: players[myPlayerId].y,
          icon: playerIcon,
          name: playerName
        }));
      }

      requestAnimationFrame(gameLoop); // Continue the game loop
    }

    // Start the game once the player enters their name and selects an icon
    document.getElementById('startGameButton').addEventListener('click', () => {
      playerName = document.getElementById('playerNameInput').value;
      playerIcon = document.querySelector('.icon.selected')?.dataset.icon || 'defaultIcon.png'; // Default if not selected

      // Initialize player with selected icon
      const groundLevel = canvas.height - GRASS_HEIGHT;
      myPlayerId = Date.now(); // Generate a unique ID based on timestamp
      players[myPlayerId] = new Player(myPlayerId, 50, groundLevel, 50, 50, playerIcon, playerName, 1); // Opaque for this player

      // Hide the start screen and start the game
      document.getElementById('startScreen').style.display = 'none';
      gameLoop(); // Start the game loop
    });

    // Handle icon selection
    document.querySelectorAll('.icon').forEach(icon => {
      icon.addEventListener('click', () => {
        // Deselect all icons
        document.querySelectorAll('.icon').forEach(i => i.classList.remove('selected'));
        // Select the clicked icon
        icon.classList.add('selected');
      });
    });

    // Resize canvas on window resize
    window.addEventListener('resize', () => {
      canvas.width = window.innerWidth;
      canvas.height = window.innerHeight;
    });
  </script>
</body>
</html>

server-side code

const WebSocket = require('ws');

const server = new WebSocket.Server({ port: 8080 });

const players = {}; // Object to hold connected players

server.on('connection', (socket) => {
  let playerId = Date.now(); // Generate a unique ID for the player
  players[playerId] = { socket }; // Store the player's socket

  // Notify all players of the new player
  for (const id in players) {
    if (players[id].socket.readyState === WebSocket.OPEN) {
      players[id].socket.send(JSON.stringify({
        type: 'playerConnected',
        id: playerId,
        name: "Player " + playerId, // Placeholder for player name
        icon: 'icon1.png' // Placeholder for player icon
      }));
    }
  }

  socket.on('message', (message) => {
    const data = JSON.parse(message);

    if (data.type === 'positionUpdate') {
      // Broadcast updated player position to all players
      for (const id in players) {
        if (players[id].socket.readyState === WebSocket.OPEN) {
          players[id].socket.send(JSON.stringify({
            type: 'positionUpdate',
            id: data.id,
            x: data.x,
            y: data.y,
            icon: data.icon,
            name: data.name
          }));
        }
      }
    } else if (data.type === 'disconnect') {
      // Handle player disconnection
      delete players[data.id]; // Remove player from the players object
      // Notify all remaining players about the disconnection
      for (const id in players) {
        if (players[id].socket.readyState === WebSocket.OPEN) {
          players[id].socket.send(JSON.stringify({
            type: 'playerDisconnected',
            id: data.id
          }));
        }
      }
    }
  });

  socket.on('close', () => {
    // Handle cleanup when the socket closes unexpectedly
    delete players[playerId];
  });
});