File size: 4,692 Bytes
c4cf09a
 
 
 
dd14326
c4cf09a
 
 
 
 
 
 
 
 
dd14326
 
 
 
c4cf09a
 
 
 
 
 
 
 
 
dd14326
 
 
 
c4cf09a
 
 
 
 
 
 
 
 
dd14326
c4cf09a
 
 
 
dd14326
 
 
 
 
c4cf09a
 
dd14326
c4cf09a
 
 
dd14326
 
c4cf09a
 
 
dd14326
c4cf09a
 
 
 
 
dd14326
c4cf09a
dd14326
 
c4cf09a
dd14326
c4cf09a
 
 
 
 
dd14326
c4cf09a
 
 
 
 
 
 
 
 
 
 
 
 
 
dd14326
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95f6015
 
 
 
 
 
 
 
 
c4cf09a
 
 
 
 
dd14326
c4cf09a
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
const express = require('express');
const http = require('http');
const WebSocket = require('ws');
const path = require('path');
const url = require('url');

const app = express();
const server = http.createServer(app);
const wss = new WebSocket.Server({ server });

app.use(express.static(path.join(__dirname, 'public')));

const PORT = process.env.PORT || 7860;

// ---- Game settings ----
const MAX_HP = 5; // 5 laser hits and you're out — no time limit, unlimited ammo

// id -> { id, name, color, position:[x,y,z], rotationY, score, hp }
const players = new Map();

const COLORS = [
  0xff5555, 0x55ff55, 0x5599ff, 0xffdd55,
  0xff55dd, 0x55ffdd, 0xffa500, 0xaa55ff,
];
let colorIndex = 0;
let nextId = 1;

function scoreList() {
  return Array.from(players.values()).map((p) => ({ id: p.id, name: p.name, score: p.score }));
}

function broadcast(data, exceptWs) {
  const msg = JSON.stringify(data);
  wss.clients.forEach((client) => {
    if (client.readyState === WebSocket.OPEN && client !== exceptWs) {
      client.send(msg);
    }
  });
}

wss.on('connection', (ws, req) => {
  const id = 'player-' + (nextId++);
  const color = COLORS[colorIndex % COLORS.length];
  colorIndex++;

  const query = url.parse(req.url, true).query;
  const name =
    (query.name || '').toString().replace(/[<>]/g, '').trim().slice(0, 16) ||
    'Player ' + (nextId - 1);

  const initialState = {
    id,
    name,
    color,
    position: [0, 1.6, 0],
    rotationY: 0,
    score: 0,
    hp: MAX_HP,
  };
  players.set(id, initialState);
  ws.playerId = id;
  ws.lastHitAt = 0;

  // Tell the new client who they are + who's already here
  ws.send(JSON.stringify({
    type: 'init',
    id,
    name,
    color,
    hp: MAX_HP,
    maxHp: MAX_HP,
    players: Array.from(players.values()).filter((p) => p.id !== id),
    scores: scoreList(),
  }));

  // Tell everyone else someone new joined
  broadcast({ type: 'join', ...initialState }, ws);

  console.log(`[+] ${id} "${name}" connected (${wss.clients.size} online)`);

  ws.on('message', (raw) => {
    let data;
    try {
      data = JSON.parse(raw);
    } catch (e) {
      return;
    }
    if (data.type === 'update' && Array.isArray(data.position)) {
      const p = players.get(id);
      if (!p) return;
      p.position = data.position;
      p.rotationY = data.rotationY || 0;
      broadcast({ type: 'update', id, position: p.position, rotationY: p.rotationY }, ws);
    } else if (data.type === 'shoot' && Array.isArray(data.from) && Array.isArray(data.to)) {
      // Relay the laser shot so everyone else can draw/hear it
      broadcast({ type: 'shoot', id, from: data.from, to: data.to }, ws);
    } else if (data.type === 'hit' && typeof data.target === 'string') {
      const shooter = players.get(id);
      const target = players.get(data.target);
      if (!shooter || !target || data.target === id) return;
      const now = Date.now();
      if (now - ws.lastHitAt < 150) return; // simple spam guard
      ws.lastHitAt = now;

      shooter.score++;
      target.hp--;

      if (target.hp <= 0) {
        // 5 hits — the player is out of the game
        broadcast({
          type: 'eliminated',
          target: data.target,
          targetName: target.name,
          by: id,
          byName: shooter.name,
          scores: scoreList(),
        });
        // Kick the eliminated player's socket shortly after so everyone sees them leave
        let targetWs = null;
        wss.clients.forEach((c) => { if (c.playerId === data.target) targetWs = c; });
        if (targetWs) {
          setTimeout(() => { try { targetWs.close(); } catch (e) {} }, 500);
        }
      } else {
        broadcast({
          type: 'tagged',
          target: data.target,
          targetName: target.name,
          targetHp: target.hp,
          by: id,
          byName: shooter.name,
          scores: scoreList(),
        });
      }
    } else if (data.type === 'signal' && data.target) {
      // Relay a WebRTC signaling message (offer/answer/ICE) to one specific peer.
      let targetWs = null;
      wss.clients.forEach((c) => {
        if (c.playerId === data.target && c.readyState === WebSocket.OPEN) targetWs = c;
      });
      if (targetWs) {
        targetWs.send(JSON.stringify({ type: 'signal', from: id, data: data.data }));
      }
    }
  });

  ws.on('close', () => {
    players.delete(id);
    broadcast({ type: 'leave', id, scores: scoreList() });
    console.log(`[-] ${id} disconnected (${wss.clients.size} online)`);
  });

  ws.on('error', () => {
    // swallow - close event will still fire
  });
});

server.listen(PORT, () => {
  console.log(`Server listening on port ${PORT}`);
});