From 05a44d1f083a3b18f7a0413d8f62fe887dab089d Mon Sep 17 00:00:00 2001 From: mangka060601-del Date: Sat, 29 Nov 2025 15:57:31 +0800 Subject: [PATCH] Update 01-introduction.md Manggoo x deer --- docs/tutorial/01-introduction.md | 306 ++++++++++++++++++++++++++++++- 1 file changed, 305 insertions(+), 1 deletion(-) 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 + + + + MangoDeer Mabar — Lobby + + + +

Mangodeer Mabar — Lobi

+
+ Buat bilik: + + +
+ +
Loading rooms...
+ + + + +process.env.PORTsocket.io/socket.io.jsconst socket = io(); +const roomsDiv = document.getElementById('rooms'); + +function renderRooms(list) { + if (!list || list.length === 0) { + roomsDiv.innerHTML = 'No rooms yet.'; + return; + } + roomsDiv.innerHTML = ''; + list.forEach(r => { + const div = document.createElement('div'); + div.className = 'room'; + div.innerHTML = `
+ ${r.id} — ${r.mode} — ${r.players}/${r.maxPlayers} — ${r.state} +
`; + const controls = document.createElement('div'); + const joinBtn = document.createElement('button'); + joinBtn.textContent = 'Join'; + joinBtn.disabled = r.players >= r.maxPlayers || r.state !== 'waiting'; + joinBtn.onclick = () => { + socket.emit('lobby:join', r.id, (res) => { + if (res && res.ok) { + // open game page with room param + location.href = `game.html?room=${r.id}`; + } else { + alert('Gagal join: ' + (res.err||'')); + } + }); + }; + controls.appendChild(joinBtn); + div.appendChild(controls); + roomsDiv.appendChild(div); + }); +} + +socket.on('lobby:rooms', (list) => { + renderRooms(list); +}); + +document.getElementById('create1v1').onclick = () => { + socket.emit('lobby:create', '1v1', (res) => { + if (res.ok) { + location.href = `game.html?room=${res.roomId}`; + } else alert('Gagal create: '+(res.err||'')); + }); +}; +document.getElementById('create2v2').onclick = () => { + socket.emit('lobby:create', '2v2', (res) => { + if (res.ok) location.href = `game.html?room=${res.roomId}`; + else alert('Gagal create: '+(res.err||'')); + }); +};list.lengthdiv.classNameroomsDiv.innerHTMLdiv.innerHTMLjoinBtn.textContentjoinBtn.disabledr.playersr.maxPlayersjoinBtn.onclicklocation.href + +Game + +
+ + +
+ + + + + +id2]; + if (!q) continue; + const dx = Math.abs(q.x - p.x); + const dy = Math.abs(q.y - p.y); + if (dx < 70 && dy < 50) { + q.hp -= 30; + if (q.hp < 0) q.hp = 0; + } + } + } else { + p.anim = Math.abs(p.vx) > 0 ? 'run' : 'idle'; + } + } + + // integrate motion & gravity & ground + for (const sid of room.players) { + const p = players[sid]; + if (!p) continue; + p.x += p.vx * dt; + p.y += p.vy * dt; + p.vy += 1200 * dt; // gravity + if (p.y > 300) { p.y = 300; p.vy = 0; } + // bounds + if (p.x < 0) p.x = 0; + if (p.x > 760) p.x = 760; + } + + // broadcast state snapshot for this room only + const snapshot = {}; + for (const sid of room.players) { + snapshot[sid] = { + x: Math.round(players[sid].x), + y: Math.round(players[sid].y), + facing: players[sid].facing, + hp: players[sid].hp, + anim: players[sid].anim + }; + } + io.to(roomId).emit('state:sync', snapshot); + } +}, TICK); + +const PORT = process.env.PORT || 3000; +server.listen(PORT, () => console.log('Server running on', PORT)); title: Tutorial - Introduction sidebar_label: Introduction slug: introduction