diff --git a/docs/tutorial/01-introduction.md b/docs/tutorial/01-introduction.md index 70889c64..596099db 100644 --- a/docs/tutorial/01-introduction.md +++ b/docs/tutorial/01-introduction.md @@ -1,4 +1,308 @@ ---- +---mangodeer-mabar/ +├─ server/ +│ └─ server.js +├─ public/ +│ ├─ index.html (lobi + entry) +│ ├─ lobby.js (lobby UI & socket) +│ ├─ game.html (game canvas page) +│ ├─ game.js (phaser client + room support) +│ ├─ phaser.min.js +│ └─ assets/ +│ ├─ ... sprites ... +└─ package.jsonserver.jsindex.htmllobby.jsgame.htmlgame.jsphaser.min.js// server/server.js +const express = require('express'); +const http = require('http'); +const { Server } = require('socket.io'); +const app = express(); +const server = http.createServer(app); +const io = new Server(server); + +app.use(express.static(__dirname + '/../public')); + +// Room model +// rooms: { roomId: { id, mode, players: [socketId,...], maxPlayers, state: 'waiting'|'playing' } } +const rooms = {}; +const TICK = 60; // ms + +// Simple players store (authoritative state) +const players = {}; // { socketId: { x,y,vx,vy,facing,hp,roomId,anim } } + +function createRoom(mode) { + const id = 'room-' + Math.random().toString(36).slice(2,9); + const maxPlayers = mode === '1v1' ? 2 : 4; + rooms[id] = { id, mode, players: [], maxPlayers, state: 'waiting' }; + return rooms[id]; +} + +io.on('connection', socket => { + console.log('conn', socket.id); + // send current rooms list + socket.emit('lobby:rooms', Object.values(rooms).map(r => ({ + id: r.id, mode: r.mode, players: r.players.length, maxPlayers: r.maxPlayers, state: r.state + }))); + + // Create room + socket.on('lobby:create', (mode, cb) => { + if (!['1v1','2v2'].includes(mode)) return cb && cb({ ok:false, err:'bad-mode' }); + const room = createRoom(mode); + console.log('room created', room.id, mode); + io.emit('lobby:rooms', Object.values(rooms).map(r => ({ + id: r.id, mode: r.mode, players: r.players.length, maxPlayers: r.maxPlayers, state: r.state + }))); + cb && cb({ ok:true, roomId: room.id }); + }); + + // Join room + socket.on('lobby:join', (roomId, cb) => { + const room = rooms[roomId]; + if (!room) return cb && cb({ ok:false, err:'no-room' }); + if (room.players.length >= room.maxPlayers) return cb && cb({ ok:false, err:'full' }); + + // add to room + room.players.push(socket.id); + socket.join(roomId); + players[socket.id] = { + x: 200 + Math.random()*200, + y: 300, + vx: 0, vy: 0, + facing: 'right', + hp: 100, + anim: 'idle', + roomId + }; + console.log(`${socket.id} joined ${roomId}`); + + // notify room members + io.to(roomId).emit('room:update', { + id: room.id, players: room.players.slice(), maxPlayers: room.maxPlayers, mode: room.mode, state: room.state + }); + io.emit('lobby:rooms', Object.values(rooms).map(r => ({ + id: r.id, mode: r.mode, players: r.players.length, maxPlayers: r.maxPlayers, state: r.state + }))); + + // auto-start when full + if (room.players.length === room.maxPlayers) { + room.state = 'playing'; + // announce start + io.to(roomId).emit('room:start', { roomId: room.id, serverTick: Date.now() }); + console.log('room started', roomId); + } + + cb && cb({ ok:true, roomId: room.id }); + }); + + // Leave room + socket.on('lobby:leave', (roomId, cb) => { + const room = rooms[roomId]; + if (!room) return cb && cb({ ok:false, err:'no-room' }); + const idx = room.players.indexOf(socket.id); + if (idx !== -1) room.players.splice(idx,1); + socket.leave(roomId); + if (players[socket.id]) delete players[socket.id]; + // if empty, delete room + if (room.players.length === 0) { + delete rooms[roomId]; + } else { + room.state = 'waiting'; + io.to(roomId).emit('room:update', { + id: room.id, players: room.players.slice(), maxPlayers: room.maxPlayers, mode: room.mode, state: room.state + }); + } + io.emit('lobby:rooms', Object.values(rooms).map(r => ({ + id: r.id, mode: r.mode, players: r.players.length, maxPlayers: r.maxPlayers, state: r.state + }))); + cb && cb({ ok:true }); + }); + + // client input (per-room) — minimal + socket.on('input', data => { + const p = players[socket.id]; + if (!p) return; + // store last input on server-side for tick processing + p.pendingInput = data; // { left, right, up, attack, timestamp } + }); + + socket.on('disconnect', () => { + console.log('disc', socket.id); + const p = players[socket.id]; + if (p && p.roomId) { + const room = rooms[p.roomId]; + if (room) { + const idx = room.players.indexOf(socket.id); + if (idx !== -1) room.players.splice(idx,1); + io.to(room.id).emit('room:update', { + id: room.id, players: room.players.slice(), maxPlayers: room.maxPlayers, mode: room.mode, state: room.state + }); + if (room.players.length === 0) delete rooms[room.id]; + else room.state = 'waiting'; + } + } + delete players[socket.id]; + io.emit('lobby:rooms', Object.values(rooms).map(r => ({ + id: r.id, mode: r.mode, players: r.players.length, maxPlayers: r.maxPlayers, state: r.state + }))); + }); +}); + +// physics & state tick per-room +setInterval(() => { + // iterate rooms in playing state + for (const roomId in rooms) { + const room = rooms[roomId]; + if (room.state !== 'playing') continue; + + // step players that are in that room + const dt = TICK/1000; + // apply inputs + for (const sid of room.players) { + const p = players[sid]; + if (!p) continue; + const inData = p.pendingInput || {}; + // movement + const speed = 200; + p.vx = (inData.left ? -speed : 0) + (inData.right ? speed : 0); + if (p.vx < 0) p.facing = 'left'; else if (p.vx > 0) p.facing = 'right'; + // jump + if (inData.up && Math.abs(p.vy) < 0.1) p.vy = -380; + // simple attack handling + if (inData.attack) { + // mark as attack to show on client + p.anim = 'attack'; + for (const sid2 of room.players) { + if (sid2 === sid) continue; + const q = players[ssocket.id2r.stater.id200room.idroom.stateio.top.pendingInputnowp.facingp.vxp.animp.vyq.hp7012000 + +
+ +