Compare commits

...

10 Commits

Author SHA1 Message Date
8ab6376676 Auto-sync 2026-04-15 23:50 2026-04-15 23:50:01 +02:00
6164274576 Auto-sync 2026-04-15 23:40 2026-04-15 23:40:01 +02:00
f14cfef22f Auto-sync 2026-04-15 23:35 2026-04-15 23:35:01 +02:00
43a039d098 Auto-sync 2026-04-15 23:10 2026-04-15 23:10:01 +02:00
fb58059168 Auto-sync 2026-04-15 23:05 2026-04-15 23:05:02 +02:00
a2ccf2ad11 Auto-sync 2026-04-15 22:50 2026-04-15 22:50:01 +02:00
f148a720da Auto-sync 2026-04-15 22:45 2026-04-15 22:45:02 +02:00
a3d5af24ec Auto-sync 2026-04-15 22:40 2026-04-15 22:40:01 +02:00
c4aca0cedd Auto-sync 2026-04-15 22:35 2026-04-15 22:35:01 +02:00
76924ef4e2 Auto-sync 2026-04-15 22:30 2026-04-15 22:30:01 +02:00
11 changed files with 667 additions and 125 deletions

View File

@ -29,7 +29,7 @@ Config.KeyBind = {
Config.Tebex = { Config.Tebex = {
enabled = true, -- Tebex Redeem aktivieren/deaktivieren enabled = true, -- Tebex Redeem aktivieren/deaktivieren
apiKey = 'uJKATjAeO2X6NNDYvpt6bESwi5qMNCD2', -- Tebex Secret Key (Plugin API) apiKey = 'uJKATjAeO2X6NNDYvpt6bESwi5qMNCD2', -- Tebex Secret Key (Plugin API)
identifierType = 'steam', -- 'steam' oder 'license' identifierType = 'license', -- 'steam' oder 'license'
storeUrl = 'https://mercyv.tebex.io', -- Tebex Store URL (wird geoeffnet wenn man auf Coins klickt) storeUrl = 'https://mercyv.tebex.io', -- Tebex Store URL (wird geoeffnet wenn man auf Coins klickt)
} }

View File

@ -34,7 +34,7 @@ local function HideHint()
end end
local function Notify(msg, type) local function Notify(msg, type)
exports['hex_4_hud']:SendNotification(msg, type or "info") exports['hex_4_hud']:Notify("Fahrrad", msg, type or "info", 3000)
end end
-- ── NPC spawnen ──────────────────────────────────────────────── -- ── NPC spawnen ────────────────────────────────────────────────
@ -70,6 +70,14 @@ end
RegisterNetEvent('mercyv-bike:syncNPC', function(data) RegisterNetEvent('mercyv-bike:syncNPC', function(data)
NPCData = data NPCData = data
if not data then
-- NPC löschen
if NPCEntity and DoesEntityExist(NPCEntity) then
DeleteEntity(NPCEntity)
NPCEntity = nil
end
return
end
SpawnNPC(data) SpawnNPC(data)
end) end)
@ -136,33 +144,30 @@ end)
-- Admin: Aktuelle Position erfassen -- Admin: Aktuelle Position erfassen
RegisterNUICallback('capturePos', function(data, cb) RegisterNUICallback('capturePos', function(data, cb)
local field = data.field or 'npc'
ClosePanel() ClosePanel()
Citizen.Wait(300) Citizen.Wait(300)
local ped = PlayerPedId()
local coords = GetEntityCoords(ped)
local heading = GetEntityHeading(ped)
Notify("Geh zur NPC-Position und drücke E.", "info") Notify("Geh zur Position und drücke E.", "info")
exports['hex_4_hud']:ShowHelpNotify("Position erfassen", "E") exports['hex_4_hud']:ShowHelpNotify("Position erfassen", "E")
Citizen.CreateThread(function() Citizen.CreateThread(function()
while true do while true do
Citizen.Wait(0) Citizen.Wait(0)
if IsControlJustPressed(0, 38) then -- E if IsControlJustPressed(0, 38) then
exports['hex_4_hud']:HideHelpNotify() exports['hex_4_hud']:HideHelpNotify()
local c = GetEntityCoords(PlayerPedId()) local coords = GetEntityCoords(PlayerPedId())
local h = GetEntityHeading(PlayerPedId()) local heading = GetEntityHeading(PlayerPedId())
-- Panel wieder öffnen mit erfassten Koordinaten
SetNuiFocus(true, true) SetNuiFocus(true, true)
exports['hex_4_hud']:HideHud(true) exports['hex_4_hud']:HideHud(true)
PanelOpen = true PanelOpen = true
SendNUIMessage({ SendNUIMessage({
action = "OPEN_ADMIN", action = "OPEN_ADMIN",
isAdmin = IsAdmin, isAdmin = IsAdmin,
captured = { x = c.x, y = c.y, z = c.z, heading = h }, captured = { x = coords.x, y = coords.y, z = coords.z, heading = heading, field = field },
}) })
break break
elseif IsControlJustPressed(0, 322) then -- ESC elseif IsControlJustPressed(0, 322) then
exports['hex_4_hud']:HideHelpNotify() exports['hex_4_hud']:HideHelpNotify()
OpenPanel() OpenPanel()
break break
@ -218,27 +223,47 @@ end)
-- ── Fahrrad spawnen ──────────────────────────────────────────── -- ── Fahrrad spawnen ────────────────────────────────────────────
RegisterNetEvent('mercyv-bike:doSpawn', function(bikeModel) RegisterNetEvent('mercyv-bike:doSpawn', function(bikeModel, spawnData, plate)
print('[mercyv-bike] Spawne Fahrrad: ' .. tostring(bikeModel))
local model = GetHashKey(bikeModel) local model = GetHashKey(bikeModel)
if model == 0 then
Notify("Unbekanntes Fahrrad-Modell: " .. tostring(bikeModel), "error")
return
end
RequestModel(model) RequestModel(model)
local t = GetGameTimer() + 5000 local t = GetGameTimer() + 8000
while not HasModelLoaded(model) do while not HasModelLoaded(model) do
if GetGameTimer() > t then if GetGameTimer() > t then
Notify("Fahrrad konnte nicht gespawnt werden.", "error") Notify("Fahrrad-Modell konnte nicht geladen werden.", "error")
return return
end end
Citizen.Wait(100) Citizen.Wait(100)
end end
local ped = PlayerPedId() -- Spawn-Position: vom Server gesetzte Koordinaten oder vor Spieler
local coords = GetEntityCoords(ped) local sx, sy, sz, sh
local heading = GetEntityHeading(ped) if spawnData and spawnData.x and spawnData.x ~= 0 then
local rad = math.rad(heading) sx = spawnData.x
local spawnX = coords.x + math.sin(-rad) * Config.SpawnOffset sy = spawnData.y
local spawnY = coords.y + math.cos(-rad) * Config.SpawnOffset sz = spawnData.z
sh = spawnData.heading or 0
else
local ped = PlayerPedId()
local coords = GetEntityCoords(ped)
local heading = GetEntityHeading(ped)
local rad = math.rad(heading)
sx = coords.x + math.sin(-rad) * Config.SpawnOffset
sy = coords.y + math.cos(-rad) * Config.SpawnOffset
sz = coords.z
sh = heading
end
local bike = CreateVehicle(model, spawnX, spawnY, coords.z, heading, true, false) print(string.format('[mercyv-bike] Spawn bei %.1f, %.1f, %.1f', sx, sy, sz))
local tw = GetGameTimer() + 3000
local bike = CreateVehicle(model, sx, sy, sz, sh, true, false)
local tw = GetGameTimer() + 4000
while not DoesEntityExist(bike) do while not DoesEntityExist(bike) do
if GetGameTimer() > tw then break end if GetGameTimer() > tw then break end
Citizen.Wait(100) Citizen.Wait(100)
@ -247,13 +272,17 @@ RegisterNetEvent('mercyv-bike:doSpawn', function(bikeModel)
if DoesEntityExist(bike) then if DoesEntityExist(bike) then
SetEntityAsMissionEntity(bike, true, true) SetEntityAsMissionEntity(bike, true, true)
SetVehicleEngineOn(bike, true, true, false) SetVehicleEngineOn(bike, true, true, false)
SetModelAsNoLongerNeeded(model) -- Kennzeichen setzen falls vom Server übergeben
-- Schlüssel geben if plate and plate ~= '' then
local ks = Config.VehicleKeySystem SetVehicleNumberPlateText(bike, plate)
if ks == 'jaksam' then
TriggerServerEvent('vehicles_keys:selfGiveVehicleKeys',
GetVehicleNumberPlateText(bike))
end end
SetModelAsNoLongerNeeded(model)
local finalPlate = plate or GetVehicleNumberPlateText(bike)
TriggerServerEvent('vehicles_keys:selfGiveVehicleKeys', finalPlate)
Notify("Fahrrad erhalten! Viel Spaß!", "success")
print('[mercyv-bike] Fahrrad gespawnt mit Kennzeichen: ' .. tostring(finalPlate))
else
Notify("Fahrrad konnte nicht gespawnt werden.", "error")
end end
end) end)
@ -268,3 +297,8 @@ RegisterCommand('bikeadmin', function()
Citizen.Wait(200) Citizen.Wait(200)
SendNUIMessage({ action = "OPEN_ADMIN", isAdmin = true }) SendNUIMessage({ action = "OPEN_ADMIN", isAdmin = true })
end, false) end, false)
RegisterNUICallback('deleteNPC', function(data, cb)
TriggerServerEvent('mercyv-bike:deleteNPC')
cb({})
end)

View File

@ -20,9 +20,7 @@ Config.NPC = {
-- Fahrrad-Optionen -- Fahrrad-Optionen
Config.Bikes = { Config.Bikes = {
{ model = "tribike", label = "Mountainbike", image = "tribike" }, { model = "scorcher", label = "Scorcher", image = "scorcher" },
{ model = "tribike2", label = "Mountainbike Pro", image = "tribike2" },
{ model = "tribike3", label = "Mountainbike Max", image = "tribike3" },
} }
-- Spawn-Position des Fahrrads (vor dem Spieler) -- Spawn-Position des Fahrrads (vor dem Spieler)
@ -40,10 +38,10 @@ Config.Notify = {
-- ─── Interne Hilfsfunktionen ──────────────────────────────────── -- ─── Interne Hilfsfunktionen ────────────────────────────────────
function Config.ClientNotification(msg, type) function Config.ClientNotification(msg, ntype)
exports['hex_4_hud']:SendNotification(msg, type or "info") exports['hex_4_hud']:Notify("Fahrrad", msg, ntype or "info", 3000)
end end
function Config.ServerNotification(src, msg, type) function Config.ServerNotification(src, msg, ntype)
TriggerClientEvent('mercyv-bike:notify', src, msg, type or "info") TriggerClientEvent("hex_4_hud:Notify", src, "Fahrrad", msg, ntype or "info", 3000)
end end

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

View File

@ -2,15 +2,12 @@
// MercyV Bike NUI Script // MercyV Bike NUI Script
// ═══════════════════════════════════════════════════ // ═══════════════════════════════════════════════════
const { createApp } = Vue;
function postNUI(event, data) { function postNUI(event, data) {
return $.post(`https://${GetParentResourceName()}/${event}`, JSON.stringify(data || {})); return $.post(`https://${GetParentResourceName()}/${event}`, JSON.stringify(data || {}));
} }
const app = createApp({ const app = new Vue({
data() { data: {
return {
show: false, // WICHTIG: immer false beim Start show: false, // WICHTIG: immer false beim Start
showAdmin: false, showAdmin: false,
isAdmin: false, isAdmin: false,
@ -67,7 +64,8 @@ const app = createApp({
postNUI('saveNPC', { ...this.npc }); postNUI('saveNPC', { ...this.npc });
}, },
}, },
}).mount('#app'); el: '#app'
});
// ── Message Handler ──────────────────────────────── // ── Message Handler ────────────────────────────────

View File

@ -0,0 +1,98 @@
function postNUI(event, data) {
$.post('https://' + GetParentResourceName() + '/' + event, JSON.stringify(data || {}));
}
var app = new Vue({
el: '#app',
data: {
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: function() {
this.show = false;
this.showAdmin = false;
this.selected = null;
postNUI('close');
},
claimBike: function() {
if (!this.selected) return;
postNUI('claimBike', { model: this.selected });
},
selectBike: function(model) {
this.selected = this.selected === model ? null : model;
},
openAdmin: function() {
this.showAdmin = true;
},
closeAdmin: function() {
this.showAdmin = false;
postNUI('close');
},
capturePos: function() {
postNUI('capturePos', {});
},
saveNPC: function() {
postNUI('saveNPC', {
model: this.npc.model,
x: this.npc.x,
y: this.npc.y,
z: this.npc.z,
heading: this.npc.heading
});
}
}
});
window.addEventListener('message', function(event) {
var 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;
}
});
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && app.show) {
app.close();
}
});

View File

@ -29,8 +29,6 @@ body {
.mb-backdrop { .mb-backdrop {
position: fixed; inset: 0; position: fixed; inset: 0;
display: flex; align-items: center; justify-content: center; display: flex; align-items: center; justify-content: center;
background: rgba(0,0,0,0.55);
backdrop-filter: blur(2px);
pointer-events: all; pointer-events: all;
} }
.mb-backdrop-transparent { .mb-backdrop-transparent {
@ -41,7 +39,7 @@ body {
/* ── Modal ─────────────────────────────────────── */ /* ── Modal ─────────────────────────────────────── */
.mb-modal { .mb-modal {
width: 680px; width: 820px;
background: var(--bg-modal); background: var(--bg-modal);
border-radius: 14px; border-radius: 14px;
border: 1px solid rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.1);
@ -66,7 +64,7 @@ body {
background: rgba(232,131,10,0.15); background: rgba(232,131,10,0.15);
display: flex; align-items: center; justify-content: center; display: flex; align-items: center; justify-content: center;
} }
.mb-title-main { font-size: 16px; font-family: inherit; font-weight: 700; display: block; } .mb-title-main { font-size: 18px; font-family: inherit; font-weight: 700; display: block; }
.mb-title-sub { font-size: 11px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.4px; display: block; } .mb-title-sub { font-size: 11px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.4px; display: block; }
.mb-close-btn { .mb-close-btn {
@ -99,7 +97,7 @@ body {
display: flex; gap: 14px; justify-content: center; flex-wrap: wrap; display: flex; gap: 14px; justify-content: center; flex-wrap: wrap;
} }
.mb-bike-card { .mb-bike-card {
width: 160px; padding: 16px 12px; width: 210px; padding: 20px 16px;
background: var(--bg-card); background: var(--bg-card);
border: 2px solid var(--border); border: 2px solid var(--border);
border-radius: 12px; border-radius: 12px;
@ -109,14 +107,14 @@ body {
} }
.mb-bike-card:hover { border-color: rgba(232,131,10,0.4); transform: translateY(-2px); } .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-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-img { width: 160px; height: 100px; object-fit: contain; }
.mb-bike-name { font-size: 13px; font-family: inherit; font-weight: 700; text-align: center; } .mb-bike-name { font-size: 13px; font-family: inherit; font-weight: 700; text-align: center; }
.mb-bike-free { font-size: 11px; color: var(--accent); } .mb-bike-free { font-size: 13px; color: var(--accent); }
.mb-bike-free i { margin-right: 4px; } .mb-bike-free i { margin-right: 4px; }
/* ── Claim-Button ──────────────────────────────── */ /* ── Claim-Button ──────────────────────────────── */
.mb-claim-btn { .mb-claim-btn {
padding: 13px 32px; padding: 15px 40px;
background: var(--accent); border: none; border-radius: 8px; background: var(--accent); border: none; border-radius: 8px;
color: #fff; font-size: 15px; font-family: inherit; font-weight: 700; color: #fff; font-size: 15px; font-family: inherit; font-weight: 700;
cursor: pointer; letter-spacing: 0.04em; cursor: pointer; letter-spacing: 0.04em;

View File

@ -15,6 +15,21 @@ local function GetIdentifier(src)
return xp and xp.identifier or nil return xp and xp.identifier or nil
end end
-- Fahrzeug-Tabelle (ESX)
local function vT() return "owned_vehicles" end
local function oC() return "owner" end
local function pC() return "vehicle" end
-- Zufälliges Kennzeichen generieren
local function GeneratePlate()
local chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ'
local plate = ''
for i = 1, 3 do plate = plate .. chars:sub(math.random(1,#chars),math.random(1,#chars)):sub(1,1) end
plate = plate .. ' '
for i = 1, 3 do plate = plate .. tostring(math.random(0,9)) end
return plate
end
local function IsAdmin(src) local function IsAdmin(src)
if IsPlayerAceAllowed(src, Config.AdminAce) then return true end if IsPlayerAceAllowed(src, Config.AdminAce) then return true end
if ESX then if ESX then
@ -45,15 +60,44 @@ end)
-- ── NPC-Position synchronisieren ────────────────────────────── -- ── NPC-Position synchronisieren ──────────────────────────────
local NPCData = nil -- wird aus DB oder Config geladen local NPCData = nil
local SpawnData = nil -- separate Spawn-Position für Fahrräder
AddEventHandler('onResourceStart', function(res) AddEventHandler('onResourceStart', function(res)
if res ~= GetCurrentResourceName() then return end if res ~= GetCurrentResourceName() then return end
Wait(1500) Wait(1500)
-- NPC-Position aus DB laden falls gespeichert -- Tabelle ZUERST anlegen
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,
`spawn_x` FLOAT DEFAULT 0,
`spawn_y` FLOAT DEFAULT 0,
`spawn_z` FLOAT DEFAULT 0,
`spawn_heading` FLOAT DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
]])
-- Migration: Spalten hinzufügen falls noch nicht vorhanden
for _, col in ipairs({'spawn_x','spawn_y','spawn_z','spawn_heading'}) do
local exists = MySQL.query.await(string.format(
"SELECT COUNT(*) AS cnt FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='mercyv_bike_npc' AND COLUMN_NAME='%s'", col))
if exists and exists[1] and exists[1].cnt == 0 then
MySQL.query.await(string.format("ALTER TABLE `mercyv_bike_npc` ADD COLUMN `%s` FLOAT DEFAULT 0", col))
print('[mercyv-bike] Spalte ' .. col .. ' hinzugefügt.')
end
end
-- Dann lesen
local r = MySQL.query.await("SELECT * FROM mercyv_bike_npc LIMIT 1") local r = MySQL.query.await("SELECT * FROM mercyv_bike_npc LIMIT 1")
if r and r[1] then if r and r[1] then
NPCData = r[1] NPCData = r[1]
if r[1].spawn_x and r[1].spawn_x ~= 0 then
SpawnData = { x = r[1].spawn_x, y = r[1].spawn_y, z = r[1].spawn_z, heading = r[1].spawn_heading or 0 }
end
else else
NPCData = { NPCData = {
model = Config.NPC.model, model = Config.NPC.model,
@ -63,19 +107,7 @@ AddEventHandler('onResourceStart', function(res)
heading = Config.NPC.heading, heading = Config.NPC.heading,
} }
end 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
]])
print('^2[mercyv-bike]^0 NPC-Daten geladen.') print('^2[mercyv-bike]^0 NPC-Daten geladen.')
-- Kein Broadcast hier - jeder Client bekommt die Daten beim clientReady
end) end)
-- ── Client ready ─────────────────────────────────────────────── -- ── Client ready ───────────────────────────────────────────────
@ -111,16 +143,30 @@ RegisterNetEvent('mercyv-bike:claim', function(bikeModel)
return return
end end
-- Eintragen -- Zufälliges Kennzeichen
local plate = GeneratePlate()
-- Als eigenes Fahrzeug in owned_vehicles eintragen
local tbl = vT(); local owner = oC(); local props = pC()
local modelHash = joaat and joaat(bikeModel) or 0
local skinData = json.encode({ model = GetHashKey(bikeModel), plate = plate })
-- Fahrzeug eintragen mit stored=0 (ist direkt in der Welt)
MySQL.insert(
string.format("INSERT INTO `%s` (plate, `%s`, `%s`, stored, parking, job, veh_class) VALUES (?, ?, ?, 0, 'Garage A', '', 13)", tbl, owner, props),
{ plate, identifier, skinData }
)
-- Claim eintragen
MySQL.insert( MySQL.insert(
'INSERT INTO mercyv_bike_claims (identifier, bike_model) VALUES (?, ?)', 'INSERT INTO mercyv_bike_claims (identifier, bike_model) VALUES (?, ?)',
{ identifier, bikeModel } { identifier, bikeModel }
) )
-- Spawn-Signal -- Spawn-Signal mit Kennzeichen
TriggerClientEvent('mercyv-bike:doSpawn', src, bikeModel) TriggerClientEvent('mercyv-bike:doSpawn', src, bikeModel, SpawnData, plate)
Config.ServerNotification(src, Config.Notify.CLAIMED, "success") Config.ServerNotification(src, Config.Notify.CLAIMED, "success")
print(string.format('[mercyv-bike] %s hat %s erhalten.', identifier, bikeModel)) print(string.format('[mercyv-bike] %s hat %s erhalten (Kennzeichen: %s).', identifier, bikeModel, plate))
end) end)
-- ── Prüfen ob Spieler bereits eines hat ─────────────────────── -- ── Prüfen ob Spieler bereits eines hat ───────────────────────
@ -149,15 +195,22 @@ RegisterNetEvent('mercyv-bike:saveNPC', function(data)
end end
NPCData = data NPCData = data
if data.spawn_x and data.spawn_x ~= 0 then
SpawnData = { x = data.spawn_x, y = data.spawn_y, z = data.spawn_z, heading = data.spawn_heading or 0 }
end
-- In DB speichern -- In DB speichern
local existing = MySQL.query.await('SELECT id FROM mercyv_bike_npc LIMIT 1') local existing = MySQL.query.await('SELECT id FROM mercyv_bike_npc LIMIT 1')
local sx = data.spawn_x or 0
local sy = data.spawn_y or 0
local sz = data.spawn_z or 0
local sh = data.spawn_heading or 0
if existing and existing[1] then if existing and existing[1] then
MySQL.update('UPDATE mercyv_bike_npc SET model=?, x=?, y=?, z=?, heading=? WHERE id=?', MySQL.update('UPDATE mercyv_bike_npc SET model=?, x=?, y=?, z=?, heading=?, spawn_x=?, spawn_y=?, spawn_z=?, spawn_heading=? WHERE id=?',
{ data.model, data.x, data.y, data.z, data.heading, existing[1].id }) { data.model, data.x, data.y, data.z, data.heading, sx, sy, sz, sh, existing[1].id })
else else
MySQL.insert('INSERT INTO mercyv_bike_npc (model, x, y, z, heading) VALUES (?, ?, ?, ?, ?)', MySQL.insert('INSERT INTO mercyv_bike_npc (model, x, y, z, heading, spawn_x, spawn_y, spawn_z, spawn_heading) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)',
{ data.model, data.x, data.y, data.z, data.heading }) { data.model, data.x, data.y, data.z, data.heading, sx, sy, sz, sh })
end end
TriggerClientEvent('mercyv-bike:syncNPC', -1, NPCData) TriggerClientEvent('mercyv-bike:syncNPC', -1, NPCData)
@ -185,3 +238,13 @@ RegisterCommand('bikereset', function(src, args)
Config.ServerNotification(src, 'Claim von Spieler ' .. targetId .. ' zurückgesetzt.', 'success') Config.ServerNotification(src, 'Claim von Spieler ' .. targetId .. ' zurückgesetzt.', 'success')
Config.ServerNotification(targetId, 'Dein Fahrrad-Claim wurde zurückgesetzt.', 'info') Config.ServerNotification(targetId, 'Dein Fahrrad-Claim wurde zurückgesetzt.', 'info')
end, false) end, false)
RegisterNetEvent('mercyv-bike:deleteNPC', function()
local src = source
if not IsAdmin(src) then return end
MySQL.update('DELETE FROM mercyv_bike_npc')
NPCData = nil
SpawnData = nil
TriggerClientEvent('mercyv-bike:syncNPC', -1, nil)
Config.ServerNotification(src, "NPC gelöscht.", "success")
end)

View File

@ -692,5 +692,38 @@ CreateThread(function()
end end
end end
} }
}) -- Diese Zeile schließt den ox_target Block })
end) -- Diese Zeile schließt den CreateThread end)
-- ============================================================
-- NPC REVIVE TELEPORT & ANIMATION (Für den Wiederbelebten)
-- ============================================================
RegisterNetEvent('mercyv-deathscreen:client:npcReviveTeleport')
AddEventHandler('mercyv-deathscreen:client:npcReviveTeleport', function()
local ped = PlayerPedId()
-- Kurz warten, damit der Spieler nach dem Revive sicher am Leben ist
Wait(200)
-- Bildschirm langsam schwarz ausblenden (Dauer: 1000 Millisekunden = 1 Sekunde)
DoScreenFadeOut(1000)
-- Warten, bis der Bildschirm komplett schwarz ist
Wait(1000)
-- Spieler zum Bett teleportieren (während der Bildschirm schwarz ist)
SetEntityCoords(ped, -345.2544, -602.0786, 38.1887, false, false, false, true)
SetEntityHeading(ped, 297.6853)
-- Dem Server kurz Zeit geben, die Position zu synchronisieren
Wait(500)
-- Emote-Menü Befehl ausführen (Spieler legt sich hin)
ExecuteCommand('e sleep')
-- Noch kurz warten, damit die Animation schon gestartet ist, wenn das Bild wiederkommt
Wait(1000)
-- Bildschirm wieder sanft einblenden (1000 ms)
DoScreenFadeIn(1000)
end)

View File

@ -374,6 +374,7 @@ AddEventHandler('mercyv-deathscreen:server:npcHealSelf', function()
end end
end) end)
-- NPC Wiederbelebung für andere
-- NPC Wiederbelebung für andere -- NPC Wiederbelebung für andere
RegisterNetEvent('mercyv-deathscreen:server:npcReviveOther') RegisterNetEvent('mercyv-deathscreen:server:npcReviveOther')
AddEventHandler('mercyv-deathscreen:server:npcReviveOther', function(targetId) AddEventHandler('mercyv-deathscreen:server:npcReviveOther', function(targetId)
@ -401,6 +402,9 @@ AddEventHandler('mercyv-deathscreen:server:npcReviveOther', function(targetId)
if RevivePlayer(targetId) then if RevivePlayer(targetId) then
TriggerClientEvent('esx:showNotification', src, 'Du hast die Person für $' .. cost .. ' wiederbelebt.') TriggerClientEvent('esx:showNotification', src, 'Du hast die Person für $' .. cost .. ' wiederbelebt.')
TriggerClientEvent('esx:showNotification', targetId, 'Du wurdest von jemandem beim NPC-Arzt gerettet.') TriggerClientEvent('esx:showNotification', targetId, 'Du wurdest von jemandem beim NPC-Arzt gerettet.')
-- HIER IST NEU: Teleportiert den ZIEL-SPIELER (targetId) ins Bett und startet /e sleep
TriggerClientEvent('mercyv-deathscreen:client:npcReviveTeleport', targetId)
else else
-- Falls die Wiederbelebung fehlschlägt, geben wir das Geld zurück -- Falls die Wiederbelebung fehlschlägt, geben wir das Geld zurück
xPlayer.addMoney(cost) xPlayer.addMoney(cost)