Auto-sync 2026-04-15 22:15

This commit is contained in:
root 2026-04-15 22:15:01 +02:00
parent 89788d790e
commit 9b30857065
11 changed files with 12966 additions and 4 deletions

View File

@ -0,0 +1,16 @@
-- Framework-Initialisierung (identisch zu mercyv-garage)
Framework = nil
if Config.Framework == "esx" then
Citizen.CreateThread(function()
if Config.NewESX then
while not GetResourceState('es_extended') == 'started' do Citizen.Wait(500) end
Framework = exports['es_extended']:getSharedObject()
else
while Framework == nil do
TriggerEvent('esx:getSharedObject', function(obj) Framework = obj end)
Citizen.Wait(0)
end
end
end)
end

263
mercyv-bike/client/main.lua Normal file
View File

@ -0,0 +1,263 @@
-- ═══════════════════════════════════════════════════════════════
-- MercyV Bike Client
-- ═══════════════════════════════════════════════════════════════
local NPCEntity = nil
local NPCData = nil
local IsAdmin = false
local AlreadyClaimed = false
local PanelOpen = false
-- ── Hilfsfunktionen ────────────────────────────────────────────
local function normPlate(p)
return string.lower(string.gsub(p or '', '%s+', ''))
end
local function ShowHint(text)
if Config.TextUIHandler == 'custom' then
exports['hex_4_hud']:ShowHelpNotify(text, "E")
end
end
local function HideHint()
if Config.TextUIHandler == 'custom' then
exports['hex_4_hud']:HideHelpNotify()
end
end
local function Notify(msg, type)
exports['hex_4_hud']:SendNotification(msg, type or "info")
end
-- ── NPC spawnen ────────────────────────────────────────────────
local function SpawnNPC(data)
if not data or data.x == 0 then return end
-- Alten NPC löschen
if NPCEntity and DoesEntityExist(NPCEntity) then
DeleteEntity(NPCEntity)
NPCEntity = nil
end
local model = GetHashKey(data.model or 'a_m_m_beach_01')
RequestModel(model)
local t = GetGameTimer() + 5000
while not HasModelLoaded(model) do
if GetGameTimer() > t then return end
Citizen.Wait(100)
end
NPCEntity = CreatePed(4, model, data.x, data.y, data.z - 1.0, data.heading or 0.0, false, true)
Citizen.Wait(200)
PlaceObjectOnGroundProperly(NPCEntity)
SetEntityInvincible(NPCEntity, true)
SetBlockingOfNonTemporaryEvents(NPCEntity, true)
FreezeEntityPosition(NPCEntity, true)
SetPedFleeAttributes(NPCEntity, 0, false)
SetModelAsNoLongerNeeded(model)
end
-- ── NPC-Sync vom Server ────────────────────────────────────────
RegisterNetEvent('mercyv-bike:syncNPC', function(data)
NPCData = data
SpawnNPC(data)
end)
RegisterNetEvent('mercyv-bike:setAdminStatus', function(status)
IsAdmin = status
SendNUIMessage({ action = "SET_ADMIN", isAdmin = IsAdmin })
end)
RegisterNetEvent('mercyv-bike:claimStatus', function(claimed, model)
AlreadyClaimed = claimed
SendNUIMessage({ action = "SET_CLAIM_STATUS", claimed = claimed, model = model })
end)
-- ── Benachrichtigung ──────────────────────────────────────────
RegisterNetEvent('mercyv-bike:notify', function(msg, type)
Notify(msg, type)
end)
-- ── Panel öffnen/schließen ─────────────────────────────────────
local function OpenPanel()
if PanelOpen then return end
PanelOpen = true
-- Claim-Status vom Server holen
TriggerServerEvent('mercyv-bike:checkClaim')
SetNuiFocus(true, true)
exports['hex_4_hud']:HideHud(true)
SendNUIMessage({
action = "OPEN",
bikes = Config.Bikes,
isAdmin = IsAdmin,
})
end
local function ClosePanel()
PanelOpen = false
SetNuiFocus(false, false)
exports['hex_4_hud']:HideHud(false)
SendNUIMessage({ action = "CLOSE" })
end
-- ── NUI Callbacks ──────────────────────────────────────────────
RegisterNUICallback('close', function(data, cb)
ClosePanel()
cb({})
end)
RegisterNUICallback('claimBike', function(data, cb)
if not data.model then cb({}); return end
TriggerServerEvent('mercyv-bike:claim', data.model)
ClosePanel()
cb({})
end)
-- Admin: NPC-Position speichern
RegisterNUICallback('saveNPC', function(data, cb)
TriggerServerEvent('mercyv-bike:saveNPC', data)
cb({})
end)
-- Admin: Aktuelle Position erfassen
RegisterNUICallback('capturePos', function(data, cb)
ClosePanel()
Citizen.Wait(300)
local ped = PlayerPedId()
local coords = GetEntityCoords(ped)
local heading = GetEntityHeading(ped)
Notify("Geh zur NPC-Position und drücke E.", "info")
exports['hex_4_hud']:ShowHelpNotify("Position erfassen", "E")
Citizen.CreateThread(function()
while true do
Citizen.Wait(0)
if IsControlJustPressed(0, 38) then -- E
exports['hex_4_hud']:HideHelpNotify()
local c = GetEntityCoords(PlayerPedId())
local h = GetEntityHeading(PlayerPedId())
-- Panel wieder öffnen mit erfassten Koordinaten
SetNuiFocus(true, true)
exports['hex_4_hud']:HideHud(true)
PanelOpen = true
SendNUIMessage({
action = "OPEN_ADMIN",
isAdmin = IsAdmin,
captured = { x = c.x, y = c.y, z = c.z, heading = h },
})
break
elseif IsControlJustPressed(0, 322) then -- ESC
exports['hex_4_hud']:HideHelpNotify()
OpenPanel()
break
end
end
end)
cb({})
end)
-- ── Interaktions-Loop ──────────────────────────────────────────
Citizen.CreateThread(function()
while not NetworkIsPlayerActive(PlayerId()) do Citizen.Wait(500) end
Citizen.Wait(2000)
TriggerServerEvent('mercyv-bike:clientReady')
end)
Citizen.CreateThread(function()
local hintShown = false
while true do
local sleep = 500
if NPCData and NPCData.x ~= 0 then
local ped = PlayerPedId()
local pos = GetEntityCoords(ped)
local dist = #(pos - vector3(NPCData.x, NPCData.y, NPCData.z))
if dist < 8.0 then
sleep = 0
if dist < Config.NPC.radius and not PanelOpen then
if not hintShown then
hintShown = true
ShowHint("Fahrrad abholen")
end
if IsControlJustPressed(0, 38) then
OpenPanel()
end
else
if hintShown then
HideHint()
hintShown = false
end
end
else
if hintShown then
HideHint()
hintShown = false
end
end
end
Citizen.Wait(sleep)
end
end)
-- ── Fahrrad spawnen ────────────────────────────────────────────
RegisterNetEvent('mercyv-bike:doSpawn', function(bikeModel)
local model = GetHashKey(bikeModel)
RequestModel(model)
local t = GetGameTimer() + 5000
while not HasModelLoaded(model) do
if GetGameTimer() > t then
Notify("Fahrrad konnte nicht gespawnt werden.", "error")
return
end
Citizen.Wait(100)
end
local ped = PlayerPedId()
local coords = GetEntityCoords(ped)
local heading = GetEntityHeading(ped)
local rad = math.rad(heading)
local spawnX = coords.x + math.sin(-rad) * Config.SpawnOffset
local spawnY = coords.y + math.cos(-rad) * Config.SpawnOffset
local bike = CreateVehicle(model, spawnX, spawnY, coords.z, heading, true, false)
local tw = GetGameTimer() + 3000
while not DoesEntityExist(bike) do
if GetGameTimer() > tw then break end
Citizen.Wait(100)
end
if DoesEntityExist(bike) then
SetEntityAsMissionEntity(bike, true, true)
SetVehicleEngineOn(bike, true, true, false)
SetModelAsNoLongerNeeded(model)
-- Schlüssel geben
local ks = Config.VehicleKeySystem
if ks == 'jaksam' then
TriggerServerEvent('vehicles_keys:selfGiveVehicleKeys',
GetVehicleNumberPlateText(bike))
end
end
end)
-- ── Admin-Befehl ───────────────────────────────────────────────
RegisterCommand('bikeadmin', function()
if not IsAdmin then
Notify(Config.Notify.NO_ACCESS, "error")
return
end
OpenPanel()
Citizen.Wait(200)
SendNUIMessage({ action = "OPEN_ADMIN", isAdmin = true })
end, false)

49
mercyv-bike/config.lua Normal file
View File

@ -0,0 +1,49 @@
-- ═══════════════════════════════════════════════════════════════
-- MercyV Bike Konfiguration
-- ═══════════════════════════════════════════════════════════════
Config = {}
Config.Framework = "esx" -- esx | qb
Config.NewESX = true
Config.TextUIHandler = "custom" -- custom (hex_4_hud) | esx | qb
-- NPC Position (kann im Spiel mit /bikeadmin gesetzt werden)
Config.NPC = {
model = "a_m_m_beach_01",
x = 0.0,
y = 0.0,
z = 0.0,
heading = 0.0,
radius = 3.0, -- Interaktionsradius in Metern
}
-- Fahrrad-Optionen
Config.Bikes = {
{ model = "tribike", label = "Mountainbike", image = "tribike" },
{ model = "tribike2", label = "Mountainbike Pro", image = "tribike2" },
{ model = "tribike3", label = "Mountainbike Max", image = "tribike3" },
}
-- Spawn-Position des Fahrrads (vor dem Spieler)
Config.SpawnOffset = 3.0 -- Meter vor dem Spieler
-- ACE-Permission für Admin-Befehle
Config.AdminAce = "mercyv-bike.admin"
-- Benachrichtigungs-Texte
Config.Notify = {
ALREADY_CLAIMED = "Du hast bereits ein Fahrrad erhalten.",
CLAIMED = "Fahrrad erfolgreich erhalten!",
NO_ACCESS = "Du hast keine Berechtigung.",
}
-- ─── Interne Hilfsfunktionen ────────────────────────────────────
function Config.ClientNotification(msg, type)
exports['hex_4_hud']:SendNotification(msg, type or "info")
end
function Config.ServerNotification(src, msg, type)
TriggerClientEvent('mercyv-bike:notify', src, msg, type or "info")
end

View File

@ -0,0 +1,31 @@
fx_version 'cerulean'
game 'gta5'
lua54 'yes'
name 'mercyv-bike'
description 'MercyV Gratis-Fahrrad NPC'
version '1.0.0'
shared_scripts {
'config.lua',
'GetFramework.lua',
}
client_scripts {
'client/main.lua',
}
server_scripts {
'@oxmysql/lib/MySQL.lua',
'server/main.lua',
}
ui_page 'nui/index.html'
files {
'nui/index.html',
'nui/style.css',
'nui/script.js',
'nui/vue.js',
'nui/images/*.png',
}

130
mercyv-bike/nui/index.html Normal file
View File

@ -0,0 +1,130 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>MercyV Bike</title>
<link rel="stylesheet" href="style.css">
<script src="vue.js"></script>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
</head>
<body>
<div id="app" v-show="show">
<!-- ══════════════════ HAUPT-PANEL ══════════════════ -->
<div class="mb-backdrop" v-show="!showAdmin">
<div class="mb-modal">
<!-- Header -->
<div class="mb-header">
<div class="mb-header-left">
<div class="mb-header-icon">
<i class="fas fa-bicycle" style="color:#E8830A;font-size:16px;"></i>
</div>
<div class="mb-header-title">
<span class="mb-title-main">Gratis Fahrrad</span>
<span class="mb-title-sub">Jeder bekommt eines</span>
</div>
</div>
<div class="mb-header-right">
<button class="mb-close-btn" @click="close()"><i class="fas fa-times"></i></button>
</div>
</div>
<!-- Body -->
<div class="mb-body">
<!-- Bereits abgeholt -->
<div class="mb-claimed-notice" v-if="claimed">
<i class="fas fa-check-circle"></i>
<span>Du hast bereits ein Fahrrad erhalten.</span>
<span class="mb-claimed-model" v-if="claimedModel">{{ claimedModel }}</span>
</div>
<!-- Fahrrad-Auswahl -->
<div class="mb-bikes" v-if="!claimed">
<div class="mb-bike-card"
v-for="b in bikes" :key="b.model"
:class="selected === b.model ? 'mb-bike-active' : ''"
@click="selectBike(b.model)">
<img :src="'images/' + b.image + '.png'"
class="mb-bike-img"
onerror="this.src='images/defaultimage.png'">
<div class="mb-bike-name">{{ b.label }}</div>
<div class="mb-bike-free"><i class="fas fa-gift"></i> Gratis</div>
</div>
</div>
<!-- Button -->
<button class="mb-claim-btn" v-if="!claimed && selected" @click="claimBike()">
<i class="fas fa-bicycle"></i> Fahrrad abholen
</button>
</div><!-- /.mb-body -->
<!-- Admin-Link -->
<div class="mb-admin-link" v-if="isAdmin" @click="openAdmin()">
<i class="fas fa-cog"></i> Admin
</div>
</div><!-- /.mb-modal -->
</div>
<!-- ══════════════════ ADMIN-PANEL ══════════════════ -->
<div class="mb-backdrop-transparent" v-if="showAdmin">
<div class="mb-modal mb-modal-admin">
<div class="mb-header">
<div class="mb-header-left">
<div class="mb-header-icon">
<i class="fas fa-cog" style="color:#E8830A;font-size:14px;"></i>
</div>
<div class="mb-header-title">
<span class="mb-title-main">Bike Admin</span>
<span class="mb-title-sub">NPC Konfiguration</span>
</div>
</div>
<div class="mb-header-right">
<button class="mb-close-btn" @click="closeAdmin()"><i class="fas fa-times"></i></button>
</div>
</div>
<div class="mb-admin-body">
<div class="mb-form-section">
<div class="mb-form-label"><i class="fas fa-user"></i> NPC Modell</div>
<input v-model="npc.model" placeholder="a_m_m_beach_01" class="mb-input">
</div>
<div class="mb-form-section">
<div class="mb-form-label"><i class="fas fa-map-marker-alt"></i> NPC Position</div>
<div class="mb-pos-row">
<input type="number" v-model.number="npc.x" placeholder="X" class="mb-input mb-input-sm">
<input type="number" v-model.number="npc.y" placeholder="Y" class="mb-input mb-input-sm">
<input type="number" v-model.number="npc.z" placeholder="Z" class="mb-input mb-input-sm">
<input type="number" v-model.number="npc.heading" placeholder="Heading" class="mb-input mb-input-sm">
</div>
<button class="mb-capture-btn" @click="capturePos()">
<i class="fas fa-crosshairs"></i> Hier erfassen (E drücken)
</button>
</div>
<button class="mb-save-btn" @click="saveNPC()">
<i class="fas fa-save"></i> NPC speichern & setzen
</button>
<div class="mb-admin-hint">
<i class="fas fa-info-circle"></i>
<span>Mit <kbd>/bikereset [ID]</kbd> kannst du den Claim eines Spielers zurücksetzen.</span>
</div>
</div>
</div>
</div>
</div><!-- /#app -->
<script src="script.js"></script>
</body>
</html>

113
mercyv-bike/nui/script.js Normal file
View File

@ -0,0 +1,113 @@
// ═══════════════════════════════════════════════════
// MercyV Bike NUI Script
// ═══════════════════════════════════════════════════
const { createApp } = Vue;
function postNUI(event, data) {
return $.post(`https://${GetParentResourceName()}/${event}`, JSON.stringify(data || {}));
}
const app = createApp({
data() {
return {
show: false,
showAdmin: false,
isAdmin: false,
bikes: [],
selected: null,
claimed: false,
claimedModel: null,
npc: {
model: 'a_m_m_beach_01',
x: 0,
y: 0,
z: 0,
heading: 0,
},
};
},
methods: {
close() {
this.show = false;
this.showAdmin = false;
this.selected = null;
postNUI('close');
},
claimBike() {
if (!this.selected) return;
postNUI('claimBike', { model: this.selected });
},
selectBike(model) {
this.selected = this.selected === model ? null : model;
},
openAdmin() {
this.showAdmin = true;
},
closeAdmin() {
this.showAdmin = false;
postNUI('close');
},
capturePos() {
postNUI('capturePos', {});
},
saveNPC() {
postNUI('saveNPC', { ...this.npc });
},
},
}).mount('#app');
// ── Message Handler ────────────────────────────────
window.addEventListener('message', function(event) {
const msg = event.data;
switch (msg.action) {
case 'OPEN':
app.show = true;
app.showAdmin = false;
app.bikes = msg.bikes || [];
app.isAdmin = msg.isAdmin || false;
app.selected = null;
break;
case 'OPEN_ADMIN':
app.show = true;
app.showAdmin = true;
app.isAdmin = true;
if (msg.captured) {
app.npc.x = Math.round(msg.captured.x * 100) / 100;
app.npc.y = Math.round(msg.captured.y * 100) / 100;
app.npc.z = Math.round(msg.captured.z * 100) / 100;
app.npc.heading = Math.round(msg.captured.heading * 100) / 100;
}
break;
case 'CLOSE':
app.show = false;
app.showAdmin = false;
break;
case 'SET_ADMIN':
app.isAdmin = msg.isAdmin;
break;
case 'SET_CLAIM_STATUS':
app.claimed = msg.claimed;
app.claimedModel = msg.model || null;
break;
}
});
// ESC schließt Panel
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && app.show) {
app.close();
}
});

194
mercyv-bike/nui/style.css Normal file
View File

@ -0,0 +1,194 @@
/*
MercyV Bike NUI Style (gleiche Sprache wie mercyv-garage)
*/
@font-face {
font-family: "GilroyBold";
src: url("../mercyv-garage/nui/fonts/Gilroy-Bold.ttf") format("truetype");
}
:root {
--accent: #E8830A;
--accent-hover: #F59D2A;
--bg-modal: rgba(18, 18, 22, 0.96);
--bg-card: rgba(30, 30, 36, 0.9);
--bg-input: rgba(255,255,255,0.05);
--border: rgba(255,255,255,0.08);
--text-primary: #ffffff;
--text-secondary: rgba(255,255,255,0.75);
--text-muted: rgba(255,255,255,0.35);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: "GilroyBold", -apple-system, sans-serif;
background: transparent;
color: var(--text-primary);
user-select: none;
}
#app { position: fixed; inset: 0; pointer-events: none; }
/* ── Backdrops ─────────────────────────────────── */
.mb-backdrop {
position: fixed; inset: 0;
display: flex; align-items: center; justify-content: center;
background: rgba(0,0,0,0.55);
backdrop-filter: blur(2px);
pointer-events: all;
}
.mb-backdrop-transparent {
position: fixed; inset: 0;
display: flex; align-items: center; justify-content: center;
pointer-events: all;
}
/* ── Modal ─────────────────────────────────────── */
.mb-modal {
width: 680px;
background: var(--bg-modal);
border-radius: 14px;
border: 1px solid rgba(255,255,255,0.1);
box-shadow: 0 8px 40px rgba(0,0,0,0.7);
overflow: hidden;
display: flex;
flex-direction: column;
}
.mb-modal-admin { width: 520px; }
/* ── Header ────────────────────────────────────── */
.mb-header {
display: flex; align-items: center; justify-content: space-between;
padding: 14px 16px;
border-bottom: 1px solid var(--border);
background: rgba(0,0,0,0.2);
}
.mb-header-left { display: flex; align-items: center; gap: 10px; }
.mb-header-right { display: flex; align-items: center; gap: 8px; }
.mb-header-icon {
width: 36px; height: 36px; border-radius: 8px;
background: rgba(232,131,10,0.15);
display: flex; align-items: center; justify-content: center;
}
.mb-title-main { font-size: 16px; font-family: "GilroyBold", sans-serif; display: block; }
.mb-title-sub { font-size: 11px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.4px; display: block; }
.mb-close-btn {
width: 28px; height: 28px; border-radius: 6px;
background: rgba(255,255,255,0.07); border: none;
color: var(--text-muted); cursor: pointer; font-size: 12px;
display: flex; align-items: center; justify-content: center;
transition: background 0.15s, color 0.15s;
}
.mb-close-btn:hover { background: rgba(255,255,255,0.12); color: #fff; }
/* ── Body ──────────────────────────────────────── */
.mb-body {
padding: 24px 20px;
display: flex; flex-direction: column; gap: 16px; align-items: center;
}
/* ── Bereits abgeholt ──────────────────────────── */
.mb-claimed-notice {
display: flex; flex-direction: column; align-items: center; gap: 8px;
padding: 24px; text-align: center;
color: var(--accent);
}
.mb-claimed-notice i { font-size: 40px; margin-bottom: 4px; }
.mb-claimed-notice span { font-size: 15px; color: var(--text-secondary); }
.mb-claimed-model { font-size: 13px; color: var(--text-muted); }
/* ── Fahrrad-Karten ────────────────────────────── */
.mb-bikes {
display: flex; gap: 14px; justify-content: center; flex-wrap: wrap;
}
.mb-bike-card {
width: 160px; padding: 16px 12px;
background: var(--bg-card);
border: 2px solid var(--border);
border-radius: 12px;
cursor: pointer;
display: flex; flex-direction: column; align-items: center; gap: 8px;
transition: border-color 0.2s, transform 0.1s;
}
.mb-bike-card:hover { border-color: rgba(232,131,10,0.4); transform: translateY(-2px); }
.mb-bike-active { border-color: var(--accent) !important; background: rgba(232,131,10,0.08) !important; }
.mb-bike-img { width: 120px; height: 75px; object-fit: contain; }
.mb-bike-name { font-size: 13px; font-family: "GilroyBold", sans-serif; text-align: center; }
.mb-bike-free { font-size: 11px; color: var(--accent); }
.mb-bike-free i { margin-right: 4px; }
/* ── Claim-Button ──────────────────────────────── */
.mb-claim-btn {
padding: 13px 32px;
background: var(--accent); border: none; border-radius: 8px;
color: #fff; font-size: 15px; font-family: "GilroyBold", sans-serif;
cursor: pointer; letter-spacing: 0.04em;
transition: background 0.15s, transform 0.1s;
}
.mb-claim-btn:hover { background: var(--accent-hover); }
.mb-claim-btn:active { transform: scale(0.98); }
/* ── Admin-Link ────────────────────────────────── */
.mb-admin-link {
padding: 8px 16px;
border-top: 1px solid var(--border);
font-size: 12px; color: var(--text-muted);
cursor: pointer; display: flex; align-items: center; gap: 6px;
transition: color 0.15s;
}
.mb-admin-link:hover { color: var(--accent); }
/* ── Admin Panel ───────────────────────────────── */
.mb-admin-body { padding: 20px; display: flex; flex-direction: column; gap: 16px; }
.mb-form-section { display: flex; flex-direction: column; gap: 8px; }
.mb-form-label {
font-size: 13px; color: var(--text-secondary);
display: flex; align-items: center; gap: 6px;
}
.mb-form-label i { color: var(--accent); }
.mb-input {
padding: 9px 12px;
background: var(--bg-input); border: 1px solid var(--border);
border-radius: 8px; color: var(--text-secondary);
font-size: 13px; font-family: inherit; outline: none;
transition: border-color 0.15s;
}
.mb-input:focus { border-color: var(--accent); }
.mb-input-sm { flex: 1; min-width: 0; }
.mb-pos-row { display: flex; gap: 8px; }
.mb-capture-btn {
padding: 9px 14px; border-radius: 8px;
background: rgba(232,131,10,0.15);
border: 1px solid rgba(232,131,10,0.3);
color: var(--accent); font-size: 13px;
font-family: inherit; cursor: pointer;
transition: background 0.15s;
}
.mb-capture-btn:hover { background: rgba(232,131,10,0.25); }
.mb-save-btn {
padding: 12px; border-radius: 8px;
background: var(--accent); border: none;
color: #fff; font-size: 14px; font-family: "GilroyBold", sans-serif;
cursor: pointer; letter-spacing: 0.04em;
transition: background 0.15s, transform 0.1s;
}
.mb-save-btn:hover { background: var(--accent-hover); }
.mb-save-btn:active { transform: scale(0.98); }
.mb-admin-hint {
display: flex; align-items: flex-start; gap: 8px;
padding: 10px 12px;
background: rgba(255,255,255,0.04);
border-radius: 8px; border: 1px solid var(--border);
font-size: 12px; color: var(--text-muted);
}
.mb-admin-hint i { color: var(--accent); margin-top: 2px; }
.mb-admin-hint kbd {
background: rgba(255,255,255,0.1);
padding: 1px 6px; border-radius: 4px;
font-size: 11px; color: var(--text-secondary);
}

11967
mercyv-bike/nui/vue.js Normal file

File diff suppressed because it is too large Load Diff

187
mercyv-bike/server/main.lua Normal file
View File

@ -0,0 +1,187 @@
-- ═══════════════════════════════════════════════════════════════
-- MercyV Bike Server
-- ═══════════════════════════════════════════════════════════════
local ESX = nil
Citizen.CreateThread(function()
if Config.NewESX then
ESX = exports['es_extended']:getSharedObject()
end
end)
local function GetIdentifier(src)
local xp = ESX and ESX.GetPlayerFromId(src)
return xp and xp.identifier or nil
end
local function IsAdmin(src)
if IsPlayerAceAllowed(src, Config.AdminAce) then return true end
if ESX then
local xp = ESX.GetPlayerFromId(src)
if xp then
local g = xp.getGroup()
if g == "admin" or g == "superadmin" then return true end
end
end
return false
end
-- ── Tabelle erstellen ──────────────────────────────────────────
AddEventHandler('onResourceStart', function(res)
if res ~= GetCurrentResourceName() then return end
Wait(1000)
MySQL.query.await([[
CREATE TABLE IF NOT EXISTS `mercyv_bike_claims` (
`identifier` VARCHAR(120) NOT NULL,
`bike_model` VARCHAR(60) NOT NULL,
`claimed_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`identifier`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
]])
print('^2[mercyv-bike]^0 Tabelle bereit.')
end)
-- ── NPC-Position synchronisieren ──────────────────────────────
local NPCData = nil -- wird aus DB oder Config geladen
AddEventHandler('onResourceStart', function(res)
if res ~= GetCurrentResourceName() then return end
Wait(1500)
-- NPC-Position aus DB laden falls gespeichert
local r = MySQL.query.await("SELECT * FROM mercyv_bike_npc LIMIT 1")
if r and r[1] then
NPCData = r[1]
else
NPCData = {
model = Config.NPC.model,
x = Config.NPC.x,
y = Config.NPC.y,
z = Config.NPC.z,
heading = Config.NPC.heading,
}
end
-- NPC-Tabelle anlegen falls nicht vorhanden
MySQL.query.await([[
CREATE TABLE IF NOT EXISTS `mercyv_bike_npc` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`model` VARCHAR(100) DEFAULT 'a_m_m_beach_01',
`x` FLOAT DEFAULT 0,
`y` FLOAT DEFAULT 0,
`z` FLOAT DEFAULT 0,
`heading` FLOAT DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
]])
TriggerClientEvent('mercyv-bike:syncNPC', -1, NPCData)
print('^2[mercyv-bike]^0 NPC-Daten geladen.')
end)
-- ── Client ready ───────────────────────────────────────────────
RegisterNetEvent('mercyv-bike:clientReady', function()
local src = source
Citizen.CreateThread(function()
Wait(500)
TriggerClientEvent('mercyv-bike:syncNPC', src, NPCData)
Wait(200)
TriggerClientEvent('mercyv-bike:setAdminStatus', src, IsAdmin(src))
end)
end)
RegisterNetEvent('mercyv-bike:checkAdmin', function()
TriggerClientEvent('mercyv-bike:setAdminStatus', source, IsAdmin(source))
end)
-- ── Fahrrad abholen ────────────────────────────────────────────
RegisterNetEvent('mercyv-bike:claim', function(bikeModel)
local src = source
local identifier = GetIdentifier(src)
if not identifier then return end
-- Prüfen ob bereits eines abgeholt
local existing = MySQL.query.await(
'SELECT identifier FROM mercyv_bike_claims WHERE identifier = ?',
{ identifier }
)
if existing and existing[1] then
Config.ServerNotification(src, Config.Notify.ALREADY_CLAIMED, "error")
return
end
-- Eintragen
MySQL.insert(
'INSERT INTO mercyv_bike_claims (identifier, bike_model) VALUES (?, ?)',
{ identifier, bikeModel }
)
-- Spawn-Signal
TriggerClientEvent('mercyv-bike:doSpawn', src, bikeModel)
Config.ServerNotification(src, Config.Notify.CLAIMED, "success")
print(string.format('[mercyv-bike] %s hat %s erhalten.', identifier, bikeModel))
end)
-- ── Prüfen ob Spieler bereits eines hat ───────────────────────
RegisterNetEvent('mercyv-bike:checkClaim', function()
local src = source
local identifier = GetIdentifier(src)
if not identifier then
TriggerClientEvent('mercyv-bike:claimStatus', src, false)
return
end
local r = MySQL.query.await(
'SELECT bike_model FROM mercyv_bike_claims WHERE identifier = ?',
{ identifier }
)
TriggerClientEvent('mercyv-bike:claimStatus', src, r and r[1] ~= nil, r and r[1] and r[1].bike_model)
end)
-- ── Admin: NPC-Position speichern ─────────────────────────────
RegisterNetEvent('mercyv-bike:saveNPC', function(data)
local src = source
if not IsAdmin(src) then
Config.ServerNotification(src, Config.Notify.NO_ACCESS, "error")
return
end
NPCData = data
-- In DB speichern
local existing = MySQL.query.await('SELECT id FROM mercyv_bike_npc LIMIT 1')
if existing and existing[1] then
MySQL.update('UPDATE mercyv_bike_npc SET model=?, x=?, y=?, z=?, heading=? WHERE id=?',
{ data.model, data.x, data.y, data.z, data.heading, existing[1].id })
else
MySQL.insert('INSERT INTO mercyv_bike_npc (model, x, y, z, heading) VALUES (?, ?, ?, ?, ?)',
{ data.model, data.x, data.y, data.z, data.heading })
end
TriggerClientEvent('mercyv-bike:syncNPC', -1, NPCData)
Config.ServerNotification(src, "NPC-Position gespeichert.", "success")
end)
-- ── Benachrichtigung ──────────────────────────────────────────
RegisterNetEvent('mercyv-bike:notify', function(msg, type)
-- Wird clientseitig behandelt
end)
-- ── Admin: Claim zurücksetzen ─────────────────────────────────
RegisterCommand('bikereset', function(src, args)
if not IsAdmin(src) then return end
local targetId = tonumber(args[1])
if not targetId then
print('[mercyv-bike] Verwendung: /bikereset [Spieler-ID]')
return
end
local xp = ESX and ESX.GetPlayerFromId(targetId)
if not xp then return end
MySQL.update('DELETE FROM mercyv_bike_claims WHERE identifier = ?', { xp.identifier })
Config.ServerNotification(src, 'Claim von Spieler ' .. targetId .. ' zurückgesetzt.', 'success')
Config.ServerNotification(targetId, 'Dein Fahrrad-Claim wurde zurückgesetzt.', 'info')
end, false)

View File

@ -0,0 +1,11 @@
## mercyv-bike server.cfg Einträge
add_ace group.admin mercyv-bike.admin allow
add_ace group.superadmin mercyv-bike.admin allow
## Nach oxmysql und es_extended
ensure mercyv-bike
## Admin-Befehle:
## /bikeadmin → NPC-Position ingame setzen
## /bikereset [ID] → Claim eines Spielers zurücksetzen

View File

@ -30,15 +30,16 @@ local DownedPlayers = {}
-- ============================================================ -- ============================================================
local function IsLocalPlayerEMS() local function IsLocalPlayerEMS()
if not ESX or not ESX.PlayerData or not ESX.PlayerData.job then return false end local playerData = ESX.GetPlayerData()
if not playerData or not playerData.job then return false end
for _, jobName in ipairs(Config.EMSJobNames) do for _, jobName in ipairs(Config.EMSJobNames) do
if ESX.PlayerData.job.name == jobName then if playerData.job.name == jobName then
return true return true
end end
end end
return false return false
end end
-- ============================================================ -- ============================================================
-- UTILS -- UTILS
-- ============================================================ -- ============================================================
@ -647,6 +648,7 @@ CreateThread(function()
icon = 'fas fa-kit-medical', icon = 'fas fa-kit-medical',
label = 'Sich selbst heilen (Kostenlos)', label = 'Sich selbst heilen (Kostenlos)',
canInteract = function() canInteract = function()
-- Wird nur angezeigt, wenn man lebt und nicht volle HP (200) hat
return deathState == 'ALIVE' and GetEntityHealth(PlayerPedId()) < 200 return deathState == 'ALIVE' and GetEntityHealth(PlayerPedId()) < 200
end, end,
onSelect = function() onSelect = function()
@ -674,7 +676,6 @@ CreateThread(function()
local coords = GetEntityCoords(PlayerPedId()) local coords = GetEntityCoords(PlayerPedId())
local players = ESX.Game.GetPlayersInArea(coords, 5.0) local players = ESX.Game.GetPlayersInArea(coords, 5.0)
local closestPlayer = nil local closestPlayer = nil
local shortestDist = 5.0
for _, player in ipairs(players) do for _, player in ipairs(players) do
local serverId = GetPlayerServerId(player) local serverId = GetPlayerServerId(player)