1102 lines
33 KiB
Plaintext
1102 lines
33 KiB
Plaintext
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");
|
||
});
|