Auto-sync 2026-04-15 22:15
This commit is contained in:
parent
89788d790e
commit
9b30857065
16
mercyv-bike/GetFramework.lua
Normal file
16
mercyv-bike/GetFramework.lua
Normal 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
263
mercyv-bike/client/main.lua
Normal 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
49
mercyv-bike/config.lua
Normal 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
|
||||||
31
mercyv-bike/fxmanifest.lua
Normal file
31
mercyv-bike/fxmanifest.lua
Normal 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
130
mercyv-bike/nui/index.html
Normal 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
113
mercyv-bike/nui/script.js
Normal 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
194
mercyv-bike/nui/style.css
Normal 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
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
187
mercyv-bike/server/main.lua
Normal 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)
|
||||||
11
mercyv-bike/server_cfg_snippet.txt
Normal file
11
mercyv-bike/server_cfg_snippet.txt
Normal 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
|
||||||
@ -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)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user