import express from "express"; import http from "http"; import { WebSocketServer } from "ws"; import mysql from "mysql2/promise"; import bcrypt from "bcrypt"; import jwt from "jsonwebtoken"; import path from "path"; import fs from "fs"; import { fileURLToPath } from "url"; const JWT_SECRET = "SUPER_SECRET_KEY"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // ------------------------------------------------------------- // TILE CONFIG LADEN // ------------------------------------------------------------- const tileConfig = JSON.parse( fs.readFileSync(path.join(__dirname, "tiles.json"), "utf8") ); // ------------------------------------------------------------- // OBJECT CONFIG LADEN // ------------------------------------------------------------- const objectConfig = JSON.parse( fs.readFileSync(path.join(__dirname, "objectConfig.json"), "utf8") ); const carConfig = JSON.parse( fs.readFileSync(path.join(__dirname, "carConfig.json"), "utf8") ); const cars = new Map(); // carId -> { id, ownerId, model, world, x, y, angle, speed, driverId, throttle, steer } // ------------------------------------------------------------- // MAPS LADEN // ------------------------------------------------------------- const maps = {}; function loadMaps() { const dir = path.join(__dirname, "maps"); const files = fs.readdirSync(dir).filter(f => f.endsWith(".json")); for (const file of files) { const data = JSON.parse(fs.readFileSync(path.join(dir, file), "utf8")); maps[data.name] = data; } console.log("Maps geladen:", Object.keys(maps)); } loadMaps(); // ------------------------------------------------------------- // MYSQL VERBINDUNG // ------------------------------------------------------------- export const db = await mysql.createPool({ host: "156.67.28.205", user: "game", password: "tito13101", database: "gamegta", port: "3406", connectionLimit: 10 }); // ------------------------------------------------------------- // EXPRESS SERVER // ------------------------------------------------------------- const app = express(); app.use(express.json()); app.use(express.static(__dirname)); const server = http.createServer(app); const wss = new WebSocketServer({ server }); const playersOnline = new Map(); // ------------------------------------------------------------- // REGISTER // ------------------------------------------------------------- app.post("/api/register", async (req, res) => { const { username, password } = req.body; const hash = await bcrypt.hash(password, 10); try { await db.query( "INSERT INTO players (username, password_hash, world, x, y) VALUES (?, ?, 'stadt', 112, 144)", [username, hash] ); res.json({ ok: true }); } catch { res.json({ ok: false, error: "Username vergeben" }); } }); // ------------------------------------------------------------- // LOGIN // ------------------------------------------------------------- app.post("/api/login", async (req, res) => { const { username, password } = req.body; const [rows] = await db.query( "SELECT * FROM players WHERE username = ?", [username] ); if (rows.length === 0) return res.json({ ok: false, error: "User nicht gefunden" }); const player = rows[0]; if (!await bcrypt.compare(password, player.password_hash)) return res.json({ ok: false, error: "Passwort falsch" }); const token = jwt.sign( { id: player.id, username: player.username }, JWT_SECRET ); res.json({ ok: true, token }); }); // ------------------------------------------------------------- // MAP SPEICHERN (Editor) // ------------------------------------------------------------- app.post("/api/save_map", (req, res) => { const { name, data } = req.body; if (!name || !data) { return res.json({ ok: false, error: "Name oder Daten fehlen" }); } const filePath = path.join(__dirname, "maps", name + ".json"); try { fs.writeFileSync(filePath, JSON.stringify(data, null, 2)); loadMaps(); res.json({ ok: true }); } catch { res.json({ ok: false, error: "Speichern fehlgeschlagen" }); } }); app.get("/api/get_maps", (req, res) => { res.json({ ok: true, maps }); }); // ------------------------------------------------------------- // COLLISION (Tiles + Objekte) // ------------------------------------------------------------- async function loadCars() { const [rows] = await db.query("SELECT * FROM cars"); for (const row of rows) { cars.set(row.id, { id: row.id, ownerId: row.owner_id, model: row.model, world: row.world, x: row.x, y: row.y, angle: row.angle, speed: 0, driverId: null, throttle: 0, steer: 0 }); } console.log("Autos geladen:", cars.size); } await loadCars(); function sendCarsToAll() { const list = []; for (const [id, c] of cars) { list.push({ id: c.id, model: c.model, world: c.world, x: c.x, y: c.y, angle: c.angle, driverId: c.driverId }); } const msg = JSON.stringify({ type: "cars", cars: list }); for (const [, p] of playersOnline) p.ws.send(msg); } function canMove(player, nx, ny) { const map = maps[player.state.world]; if (!map || !map.tiles) return true; const tileX = Math.floor(nx / 32); const tileY = Math.floor(ny / 32); // TILE COLLISION const tileId = map.tiles[tileY]?.[tileX]; const tile = tileConfig[tileId]; if (tile && tile.collision) return false; // OBJECT COLLISION if (map.objects) { for (const obj of map.objects) { const cfg = objectConfig[obj.type]; if (!cfg || !cfg.collision) continue; const ox = obj.x; const oy = obj.y - (cfg.height - 32); const ow = cfg.width; const oh = cfg.height; const px = nx; const py = ny; const pw = 30; const ph = 30; const hit = px < ox + ow && px + pw > ox && py < oy + oh && py + ph > oy; if (hit) return false; } } return true; } const shops = new Map(); // shopId -> { id, name, world, x, y, items: [{id, name, price, type, model, restore}] } async function loadShops() { shops.clear(); const [shopRows] = await db.query("SELECT * FROM shops"); for (const s of shopRows) { shops.set(s.id, { id: s.id, name: s.name, world: s.world, x: s.x, y: s.y, items: [] }); } const [itemRows] = await db.query(` SELECT si.shop_id, si.price, i.id AS item_id, i.name, i.type, i.model, i.restore FROM shop_items si JOIN items i ON i.id = si.item_id `); for (const row of itemRows) { const shop = shops.get(row.shop_id); if (!shop) continue; shop.items.push({ id: row.item_id, name: row.name, price: row.price, type: row.type, model: row.model, restore: row.restore }); } console.log("Shops geladen:", shops.size); } await loadShops(); // Hilfsfunktion: Spieler holen function getPlayer(playerId) { return playersOnline.get(playerId); } // ------------------------------------------------------------- // STATE AN ALLE SENDEN // ------------------------------------------------------------- function sendStateToAll() { const allPlayers = []; for (const [id, p] of playersOnline) { allPlayers.push({ id: id, x: p.state.x, y: p.state.y, world: p.state.world, money: p.state.money, bank: p.state.bank, health: p.state.health, hunger: p.state.hunger, thirst: p.state.thirst, inventory: p.state.inventory }); } const msg = JSON.stringify({ type: "state", players: allPlayers }); for (const [id, p] of playersOnline) { p.ws.send(msg); } } async function getItemData(itemId) { const [rows] = await db.query("SELECT * FROM items WHERE id=?", [itemId]); return rows[0] || null; } function round2(value) { return Math.round(value * 100) / 100; } async function useItem(p, itemId) { const invItem = p.state.inventory.find(i => i.id === itemId); if (!invItem) { return { msg: "Item nicht im Inventar gefunden." }; } const [rows] = await db.query("SELECT * FROM items WHERE id=?", [itemId]); if (rows.length === 0) { return { msg: "Item existiert nicht in der Datenbank." }; } const itemData = rows[0]; // Werte normalisieren p.state.hunger = Number(p.state.hunger) || 0; p.state.thirst = Number(p.state.thirst) || 0; p.state.health = Number(p.state.health) || 100; // ADDIEREN statt voll auffüllen const restore = Number(itemData.restore) || 0; if (itemData.type === "food") { console.log(itemData.restore) console.log(p.state.hunger) p.state.hunger = round2(Math.min(100, p.state.hunger + restore)); console.log(p.state.hunger) } if (itemData.type === "drink") { p.state.thirst = round2(Math.min(100, p.state.thirst + restore)); } if (itemData.type === "heal") { p.state.health = round2(Math.min(100, p.state.health + restore)); } // Inventar reduzieren invItem.amount--; if (invItem.amount <= 0) { p.state.inventory = p.state.inventory.filter(i => i.id !== itemId); } return { msg: `${itemData.name} benutzt.`, hunger: p.state.hunger, thirst: p.state.thirst, health: p.state.health, inventory: p.state.inventory }; } /* function handleInteraction(player, data) { const map = maps[player.state.world]; if (!map) return; const px = player.state.x; const py = player.state.y; // 1. Türen prüfen if (map.doors) { const door = map.doors.find(d => Math.abs(d.x - px) < 16 && Math.abs(d.y - py) < 16 ); if (door) { if (!maps[door.targetMap]) return; player.state.world = door.targetMap; player.state.x = door.targetX * 32; player.state.y = door.targetY * 32; db.query( "UPDATE players SET world=?, x=?, y=? WHERE id=?", [player.state.world, player.state.x, player.state.y, player.id] ); sendStateToAll(); player.ws.send(JSON.stringify({ type: "map_data", tiles: maps[player.state.world].tiles, doors: maps[player.state.world].doors, objects: maps[player.state.world].objects, shops: maps[player.state.world].shops, // <-- WICHTIG spawn: maps[player.state.world].spawn })); return; } } // 2. NPCs prüfen if (map.npcs) { const npc = map.npcs.find(n => Math.abs(n.x - px) < 32 && Math.abs(n.y - py) < 32 ); if (npc) { player.ws.send(JSON.stringify({ type: "npc_dialog", npcId: npc.id, text: npc.dialog })); return; } } // 3. Shops prüfen if (map.shops && Array.isArray(map.shops)) { const shop = map.shops.find(s => Math.abs(s.x * 32 - player.state.x) < 32 && Math.abs(s.y * 32 - player.state.y) < 32 ); if (shop) { player.ws.send(JSON.stringify({ type: "shop_open", shopId: shop.id, items: shop.items })); return; } } // 4. Interaktive Objekte (z.B. Kisten) if (map.objects) { const obj = map.objects.find(o => o.interactive && Math.abs(o.x - px) < 32 && Math.abs(o.y - py) < 32 ); if (obj) { player.ws.send(JSON.stringify({ type: "object_interact", objectId: obj.id, action: obj.action })); return; } } }*/ function handleInteraction(player, data) { const map = maps[player.state.world]; if (!map) return; const px = player.state.x; const py = player.state.y; // 1. Türen prüfen (unverändert) if (map.doors) { const door = map.doors.find(d => Math.abs(d.x - px) < 16 && Math.abs(d.y - py) < 16 ); if (door) { if (!maps[door.targetMap]) return; player.state.world = door.targetMap; player.state.x = door.targetX * 32; player.state.y = door.targetY * 32; db.query( "UPDATE players SET world=?, x=?, y=? WHERE id=?", [player.state.world, player.state.x, player.state.y, player.id] ); sendStateToAll(); player.ws.send(JSON.stringify({ type: "map_data", tiles: maps[player.state.world].tiles, doors: maps[player.state.world].doors, objects: maps[player.state.world].objects, shops: maps[player.state.world].shops, atms: maps[player.state.world].atms || [], // <-- NEU spawn: maps[player.state.world].spawn })); return; } } // 2. NPCs prüfen (unverändert) if (map.npcs) { const npc = map.npcs.find(n => Math.abs(n.x - px) < 32 && Math.abs(n.y - py) < 32 ); if (npc) { player.ws.send(JSON.stringify({ type: "npc_dialog", npcId: npc.id, text: npc.dialog })); return; } } // 3. Shops prüfen (unverändert) if (map.shops && Array.isArray(map.shops)) { const shop = map.shops.find(s => Math.abs(s.x * 32 - player.state.x) < 32 && Math.abs(s.y * 32 - player.state.y) < 32 ); if (shop) { player.ws.send(JSON.stringify({ type: "shop_open", shopId: shop.id, items: shop.items })); return; } } // 3b. ATMs prüfen (GENAU wie Shop) if (map.atms && Array.isArray(map.atms)) { const atm = map.atms.find(a => Math.abs(a.x * 32 - player.state.x) < 32 && Math.abs(a.y * 32 - player.state.y) < 32 ); if (atm) { // sende atm_open an den Client (Server entscheidet, ob PIN nötig) player.ws.send(JSON.stringify({ type: "atm_open", atmId: atm.id, money: player.state.money, bank: player.state.bank })); return; } } // 4. Interaktive Objekte (z.B. Kisten) (unverändert) if (map.objects) { const obj = map.objects.find(o => o.interactive && Math.abs(o.x - px) < 32 && Math.abs(o.y - py) < 32 ); if (obj) { player.ws.send(JSON.stringify({ type: "object_interact", objectId: obj.id, action: obj.action })); return; } } } // ------------------------------------------------------------- // WEBSOCKET MULTIPLAYER // ------------------------------------------------------------- wss.on("connection", ws => { let playerId = null; ws.on("message", async msg => { let data; try { data = JSON.parse(msg); } catch { return; } // AUTH if (data.type === "auth") { try { const payload = jwt.verify(data.token, JWT_SECRET); playerId = payload.id; ws.playerId = playerId; const [rows] = await db.query( "SELECT world, x, y, health, hunger, thirst, money,inventory FROM players WHERE id = ?", [playerId] ); const row = rows[0]; playersOnline.set(playerId, { ws, state: { world: row.world, x: row.x, y: row.y, health: row.health, hunger: row.hunger, thirst: row.thirst, inventory: JSON.parse(row.inventory || "[]"), money: row.money ?? 0, bank: row.bank ?? 0 } }); ws.send(JSON.stringify({ type: "auth_ok", id: playerId, maps })); // Map-Daten direkt nach Login senden const currentMap = maps[row.world]; ws.send(JSON.stringify({ type: "map_data", tiles: currentMap.tiles, doors: currentMap.doors, objects: currentMap.objects, shops: currentMap.shops, atms: currentMap.atms, // <-- NEU spawn: currentMap.spawn })); sendStateToAll(); } catch { ws.close(); } } if (data.type === "bank_pin_check") { // Spieler korrekt holen const player = playersOnline.get(ws.playerId); console.log(playerId) if (!player) { console.log("Kein Spieler gefunden für ws.playerId:", playerId); return; } console.log("Player ID:", playerId); const [rows] = await db.query("SELECT pin FROM players WHERE id=?", [playerId]); console.log("DB Row:", rows[0]); console.log("Client PIN:", data.pin); if (rows.length && Number(rows[0].pin) === Number(data.pin)) { player.ws.send(JSON.stringify({ type: "bank_pin_ok" })); } else { player.ws.send(JSON.stringify({ type: "bank_pin_fail" })); } } if (data.type === "bank_open") { const player = playersOnline.get(ws.playerId); if (!player) return; player.ws.send(JSON.stringify({ type: "bank_open", money: player.state.money, bank: player.state.bank })); } if (data.type === "bank_deposit") { const player = playersOnline.get(ws.playerId); // <-- NEU hinzugefügt if (!player) return; const amount = Math.max(0, Number(data.amount)); if (player.state.money >= amount) { player.state.money -= amount; player.state.bank += amount; await db.query("UPDATE players SET money=?, bank=? WHERE id=?", [player.state.money, player.state.bank, playerId]); player.ws.send(JSON.stringify({ type: "bank_update", money: player.state.money, bank: player.state.bank })); } } if (data.type === "bank_withdraw") { const player = playersOnline.get(ws.playerId); // <-- NEU hinzugefügt if (!player) return; const amount = Math.max(0, Number(data.amount)); if (player.state.bank >= amount) { player.state.bank -= amount; player.state.money += amount; await db.query("UPDATE players SET money=?, bank=? WHERE id=?", [player.state.money, player.state.bank, playerId]); player.ws.send(JSON.stringify({ type: "bank_update", money: player.state.money, bank: player.state.bank })); } } if (data.type === "bank_transfer") { const player = playersOnline.get(ws.playerId); // <-- NEU hinzugefügt if (!player) return; const targetId = Number(data.targetId); const amount = Math.max(0, Number(data.amount)); const target = playersOnline.get(targetId); if (target && player.state.bank >= amount) { player.state.bank -= amount; target.state.bank += amount; await db.query("UPDATE players SET bank=? WHERE id=?", [player.state.bank, playerId]); await db.query("UPDATE players SET bank=? WHERE id=?", [target.state.bank, targetId]); player.ws.send(JSON.stringify({ type: "bank_update", money: player.state.money, bank: player.state.bank })); target.ws.send(JSON.stringify({ type: "bank_update", money: target.state.money, bank: target.state.bank })); } } if (data.type === "car_enter") { const p = playersOnline.get(playerId); if (!p) return; let nearestCar = null; for (const [, c] of cars) { if (c.world !== p.state.world || c.driverId) continue; if (Math.abs(c.x - p.state.x) < 40 && Math.abs(c.y - p.state.y) < 40) { nearestCar = c; break; } } if (!nearestCar) return; nearestCar.driverId = playerId; p.drivingCarId = nearestCar.id; sendCarsToAll(); } if (data.type === "car_exit") { const p = playersOnline.get(playerId); if (!p || !p.drivingCarId) return; const c = cars.get(p.drivingCarId); if (c) { c.driverId = null; c.throttle = 0; c.steer = 0; p.state.x = c.x; p.state.y = c.y + 30; db.query("UPDATE cars SET x=?, y=?, angle=?, world=? WHERE id=?", [c.x, c.y, c.angle, c.world, c.id]); } p.drivingCarId = null; sendCarsToAll(); sendStateToAll(); } if (data.type === "car_control") { const p = playersOnline.get(playerId); if (!p || !p.drivingCarId) return; const c = cars.get(p.drivingCarId); if (!c) return; c.throttle = Math.max(-1, Math.min(1, Number(data.throttle) || 0)); c.steer = Math.max(-1, Math.min(1, Number(data.steer) || 0)); } if (msg.type === "shop_error") { alert(msg.msg); } if (data.type === "shop_buy") { const p = playersOnline.get(playerId); if (!p) return; const map = maps[p.state.world]; if (!map || !map.shops) return; const shop = map.shops.find(s => Math.abs(s.x * 32 - p.state.x) < 32 && Math.abs(s.y * 32 - p.state.y) < 32 ); if (!shop) return; const item = shop.items.find(i => i.id === data.itemId); if (!item) return; if (p.state.money < item.price) { p.ws.send(JSON.stringify({ type: "shop_error", msg: "Zu wenig Geld!" })); return; } p.state.money -= item.price; if (item.type === "car") { const [result] = await db.query( "INSERT INTO cars (owner_id, model, world, x, y, angle) VALUES (?, ?, ?, ?, ?, 0)", [playerId, item.model || "sedan", p.state.world, p.state.x + 40, p.state.y] ); cars.set(result.insertId, { id: result.insertId, ownerId: playerId, model: item.model || "sedan", world: p.state.world, x: p.state.x + 40, y: p.state.y, angle: 0, speed: 0, driverId: null, throttle: 0, steer: 0 }); await db.query("UPDATE players SET money=? WHERE id=?", [p.state.money, playerId]); sendCarsToAll(); p.ws.send(JSON.stringify({ type: "shop_info", msg: "Auto gekauft! Steht neben dir." })); } else { const invItem = p.state.inventory.find(i => i.id === item.id); if (invItem) invItem.amount++; else p.state.inventory.push({ id: item.id, amount: 1 }); await db.query( "UPDATE players SET money=?, inventory=? WHERE id=?", [p.state.money, JSON.stringify(p.state.inventory), playerId] ); } sendStateToAll(); } if (data.type === "state") { const player = playersOnline.get(ws.playerId); if (!player) return; const me = data.players.find(p => p.id === ws.playerId); if (!me) return; // SERVERSEITIG IMMER player.state benutzen! player.state.x = me.x; player.state.y = me.y; player.state.world = me.world; player.state.money = me.money; player.state.bank = me.bank; player.state.health = me.health; player.state.hunger = me.hunger; player.state.thirst = me.thirst; player.state.inventory = me.inventory || []; // Andere Spieler aktualisieren player.otherPlayers = data.players.filter(p => p.id !== ws.playerId); // UI aktualisieren (Client) updateStatusWindow(); renderInventoryWindow(); } if (!playerId) return; const p = playersOnline.get(playerId); if (!p) return; // TÜR BENUTZEN if (data.type === "use_door") { const currentMap = maps[p.state.world]; if (!currentMap || !currentMap.doors) return; const door = currentMap.doors.find(d => Math.abs(d.x - data.x) < 10 && Math.abs(d.y - data.y) < 10 ); if (!door) return; if (!maps[door.targetMap]) return; p.state.world = door.targetMap; p.state.x = door.targetX * 32; p.state.y = door.targetY * 32; await db.query( "UPDATE players SET world = ?, x = ?, y = ? WHERE id = ?", [p.state.world, p.state.x, p.state.y, playerId] ); sendStateToAll(); } if (data.type === "interact") { const p = playersOnline.get(playerId); if (!p) return; handleInteraction(p, data); } if (data.type === "use_item") { const p = playersOnline.get(playerId); if (!p) return; const result = await useItem(p, data.itemId); const hunger = Math.max(0, Number(p.state.hunger) || 0); const thirst = Math.max(0, Number(p.state.thirst) || 0); const health = Math.max(0, Number(p.state.health) || 100); await db.query( "UPDATE players SET inventory=?, hunger=?, thirst=?, health=? WHERE id=?", [ JSON.stringify(p.state.inventory), round2(p.state.hunger), round2(p.state.thirst), round2(p.state.health), playerId ] ); p.ws.send(JSON.stringify({ type: "item_used", msg: result.msg, hunger, thirst, health, inventory: p.state.inventory })); sendStateToAll(); } if (msg.type === "item_used") { // player.hunger = msg.hunger; //player.thirst = msg.thirst; //player.inventory = msg.inventory; renderInventoryWindow(); updateStatusUI(); } // MOVEMENT MIT COLLISION if (data.type === "move") { const nx = data.x; const ny = data.y; const oldX = p.state.x; const oldY = p.state.y; if (canMove(p, nx, ny)) { p.state.x = nx; p.state.y = ny; // Nur Position speichern – KEINE Werte vom Client übernehmen! await db.query( "UPDATE players SET x=?, y=? WHERE id=?", [p.state.x, p.state.y, playerId] ); if (oldX !== p.state.x || oldY !== p.state.y) { sendStateToAll(); } } } // MAP WECHSEL if (data.type === "change_map") { const mapName = data.map; if (!maps[mapName]) return; p.state.world = mapName; p.state.x = maps[mapName].spawn.x; p.state.y = maps[mapName].spawn.y; await db.query( "UPDATE players SET world = ?, x = ?, y = ? WHERE id = ?", [mapName, p.state.x, p.state.y, playerId] ); sendStateToAll(); player.ws.send(JSON.stringify({ type: "map_data", tiles: maps[p.state.world].tiles, doors: maps[p.state.world].doors, objects: maps[p.state.world].objects, shops: maps[p.state.world].shops, // <-- WICHTIG atms: maps[player.state.world].atms, // <-- ATM wird jetzt gesendet spawn: maps[p.state.world].spawn })); } }); ws.on("close", () => { if (playerId) { playersOnline.delete(playerId); sendStateToAll(); } }); }); function updatePlayerNeeds(p) { // Werte IMMER in Zahlen umwandeln p.state.hunger = Number(p.state.hunger) || 0; p.state.thirst = Number(p.state.thirst) || 0; p.state.health = Number(p.state.health) || 100; // Hunger / Durst sinken langsam p.state.hunger = round2(p.state.hunger - 0.002); p.state.thirst = round2(p.state.thirst - 0.004); if (p.state.hunger < 0) p.state.hunger = 0; if (p.state.thirst < 0) p.state.thirst = 0; if (p.state.hunger === 0 || p.state.thirst === 0) { p.state.health = round2(p.state.health - 0.01); if (p.state.health < 0) p.state.health = 0; } // Werte erneut absichern (gegen NaN) p.state.hunger = Math.max(0, Number(p.state.hunger) || 0); p.state.thirst = Math.max(0, Number(p.state.thirst) || 0); p.state.health = Math.max(0, Number(p.state.health) || 100); } setInterval(async () => { for (const [playerId, p] of playersOnline) { updatePlayerNeeds(p); await db.query( "UPDATE players SET health=?, hunger=?, thirst=?, inventory=? WHERE id=?", [ p.state.health, p.state.hunger, p.state.thirst, JSON.stringify(p.state.inventory), playerId ] ); sendStateToAll(); } }, 1000); // alle 1 Sekunde setInterval(() => { let changed = false; for (const [, c] of cars) { if (!c.driverId) continue; const cfg = carConfig[c.model] || carConfig.sedan; if (c.throttle > 0) c.speed += cfg.accel; else if (c.throttle < 0) c.speed -= cfg.brake; else c.speed *= (1 - cfg.friction); c.speed = Math.max(-cfg.maxSpeed / 2, Math.min(cfg.maxSpeed, c.speed)); if (Math.abs(c.speed) > 0.05) { c.angle += c.steer * cfg.turnSpeed * (c.speed > 0 ? 1 : -1); } const nx = c.x + Math.cos(c.angle) * c.speed; const ny = c.y + Math.sin(c.angle) * c.speed; if (canMove({ state: { world: c.world } }, nx, ny)) { c.x = nx; c.y = ny; } else { c.speed = 0; } const driver = playersOnline.get(c.driverId); if (driver) { driver.state.x = c.x; driver.state.y = c.y; } changed = true; } if (changed) { sendCarsToAll(); sendStateToAll(); } }, 50); // 20x pro Sekunde // ------------------------------------------------------------- // SERVER STARTEN // ------------------------------------------------------------- server.listen(5555, () => { console.log("Server läuft auf Port 5555"); });