first commit
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
local peds = {}
|
||||
local companion = nil
|
||||
local busy = false
|
||||
|
||||
local function notify(message) exports.devx_core:Notify(message) end
|
||||
local function help(text)
|
||||
BeginTextCommandDisplayHelp('STRING'); AddTextComponentSubstringPlayerName(text); EndTextCommandDisplayHelp(0, false, true, -1)
|
||||
end
|
||||
local function subtitle(text, duration)
|
||||
BeginTextCommandPrint('STRING'); AddTextComponentSubstringPlayerName(text); EndTextCommandPrint(duration or 3000, true)
|
||||
end
|
||||
local function loadModel(name)
|
||||
local hash = GetHashKey(name); RequestModel(hash)
|
||||
local deadline = GetGameTimer() + 6000
|
||||
while not HasModelLoaded(hash) and GetGameTimer() < deadline do Wait(0) end
|
||||
return HasModelLoaded(hash) and hash or nil
|
||||
end
|
||||
local function loadAnim(dict)
|
||||
RequestAnimDict(dict); local deadline = GetGameTimer() + 5000
|
||||
while not HasAnimDictLoaded(dict) and GetGameTimer() < deadline do Wait(0) end
|
||||
return HasAnimDictLoaded(dict)
|
||||
end
|
||||
|
||||
local function spawnPed(modelName, pos, scenario)
|
||||
local model = loadModel(modelName); if not model then return nil end
|
||||
local ped = CreatePed(4, model, pos.x, pos.y, pos.z, pos.w or 0.0, false, false)
|
||||
SetEntityInvincible(ped, true); SetBlockingOfNonTemporaryEvents(ped, true); FreezeEntityPosition(ped, true)
|
||||
if scenario then TaskStartScenarioInPlace(ped, scenario, 0, true) end
|
||||
SetModelAsNoLongerNeeded(model); return ped
|
||||
end
|
||||
|
||||
local function updateBar(result, message)
|
||||
SendNUIMessage({ action = 'relationship', name = DevXStripclub.Nikki.name,
|
||||
value = result.satisfaction or 0, maximum = result.threshold or 100, message = message })
|
||||
end
|
||||
|
||||
local function runTalk(action)
|
||||
if busy then return end
|
||||
busy = true
|
||||
DevXTriggerCallback('stripclub:interact', { action = action }, function(result)
|
||||
if not result.ok then notify(result.error or 'Interaktion fehlgeschlagen.'); busy = false; return end
|
||||
local player = PlayerPedId(); local nikki = peds.nikki
|
||||
if nikki and DoesEntityExist(nikki) then
|
||||
ClearPedTasksImmediately(nikki); FreezeEntityPosition(player, true)
|
||||
TaskTurnPedToFaceEntity(player, nikki, 900); TaskTurnPedToFaceEntity(nikki, player, 900); Wait(900)
|
||||
if action == 'lounge' then
|
||||
local lounge = DevXStripclub.Nikki.lounge
|
||||
SetEntityCoordsNoOffset(player, lounge.x + 1.25, lounge.y, lounge.z, false, false, false)
|
||||
SetEntityHeading(player, lounge.w)
|
||||
SetEntityCoordsNoOffset(nikki, lounge.x, lounge.y, lounge.z, false, false, false)
|
||||
SetEntityHeading(nikki, lounge.w + 180.0)
|
||||
if loadAnim('mini@strip_club@idles@stripper') then
|
||||
TaskPlayAnim(nikki, 'mini@strip_club@idles@stripper', 'stripper_idle_02', 2.0, -2.0, 9000, 1, 0.0, false, false, false)
|
||||
end
|
||||
TaskStartScenarioInPlace(player, 'WORLD_HUMAN_SEAT_WALL', 0, true)
|
||||
subtitle('~p~Nikki:~s~ Schön, dass du wieder da bist.', 4000); Wait(9000)
|
||||
else
|
||||
if loadAnim('gestures@f@standing@casual') then
|
||||
TaskPlayAnim(nikki, 'gestures@f@standing@casual', 'gesture_hello', 2.0, -2.0, 3200, 49, 0.0, false, false, false)
|
||||
end
|
||||
subtitle('~p~Nikki:~s~ Hey, wie läuft dein neues Leben in DevX-City?', 3500); Wait(3600)
|
||||
end
|
||||
ClearPedTasksImmediately(player); FreezeEntityPosition(player, false)
|
||||
FreezeEntityPosition(nikki, true); TaskStartScenarioInPlace(nikki, 'WORLD_HUMAN_STAND_MOBILE', 0, true)
|
||||
end
|
||||
updateBar(result, result.newlyUnlocked and 'Kontakt freigeschaltet – Nikki wurde im Handy gespeichert.' or 'Zufriedenheit gestiegen.')
|
||||
busy = false
|
||||
end)
|
||||
end
|
||||
|
||||
local function spawnClub()
|
||||
for _, ped in pairs(peds) do if DoesEntityExist(ped) then DeleteEntity(ped) end end
|
||||
peds = {}
|
||||
peds.nikki = spawnPed(DevXStripclub.Nikki.model, DevXStripclub.Nikki.position, 'WORLD_HUMAN_STAND_MOBILE')
|
||||
for index, definition in ipairs(DevXStripclub.Dancers) do
|
||||
peds['ambient' .. index] = spawnPed(definition.model, definition.position, definition.scenario)
|
||||
end
|
||||
end
|
||||
|
||||
RegisterNetEvent('devx_stripclub:client:spawnCompanion', function(data)
|
||||
if companion and DoesEntityExist(companion) then DeleteEntity(companion) end
|
||||
local pos = data.spawn
|
||||
companion = spawnPed(data.model, vector4(pos.x, pos.y, pos.z, pos.heading or pos.w or 0.0), 'WORLD_HUMAN_STAND_MOBILE')
|
||||
if companion then notify(('%s ist in deinem Apartment angekommen.'):format(data.name or 'Dein Gast')) end
|
||||
end)
|
||||
|
||||
AddEventHandler('devx_housing:client:leftApartment', function()
|
||||
if companion and DoesEntityExist(companion) then DeleteEntity(companion) end
|
||||
companion = nil
|
||||
end)
|
||||
|
||||
CreateThread(function()
|
||||
while not exports.devx_core:IsPlayerLoaded() do Wait(500) end
|
||||
spawnClub()
|
||||
local blip = AddBlipForCoord(DevXStripclub.Entry.x, DevXStripclub.Entry.y, DevXStripclub.Entry.z)
|
||||
SetBlipSprite(blip, 121); SetBlipScale(blip, .7); SetBlipColour(blip, 27); SetBlipAsShortRange(blip, true)
|
||||
BeginTextCommandSetBlipName('STRING'); AddTextComponentString('Vanilla Unicorn'); EndTextCommandSetBlipName(blip)
|
||||
end)
|
||||
|
||||
CreateThread(function()
|
||||
while true do
|
||||
local sleep = 700
|
||||
if exports.devx_core:IsPlayerLoaded() and peds.nikki and DoesEntityExist(peds.nikki) then
|
||||
local coords = GetEntityCoords(PlayerPedId()); local npcCoords = GetEntityCoords(peds.nikki)
|
||||
local distance = #(coords - npcCoords)
|
||||
if distance < 8.0 then
|
||||
sleep = 0
|
||||
DrawMarker(2, npcCoords.x, npcCoords.y, npcCoords.z + 1.0, 0,0,0,0,180.0,0, .18,.18,.18, 220,90,180,150,false,false,2,false,nil,nil,false)
|
||||
if distance < 1.8 then
|
||||
help(('~INPUT_CONTEXT~ mit Nikki reden | ~INPUT_DETONATE~ VIP-Lounge (%s $)'):format(DevXStripclub.LoungePrice))
|
||||
if IsControlJustReleased(0, 38) then runTalk('talk')
|
||||
elseif IsControlJustReleased(0, 47) then runTalk('lounge') end
|
||||
end
|
||||
end
|
||||
end
|
||||
Wait(sleep)
|
||||
end
|
||||
end)
|
||||
|
||||
AddEventHandler('onResourceStop', function(resource)
|
||||
if resource ~= GetCurrentResourceName() then return end
|
||||
for _, ped in pairs(peds) do if DoesEntityExist(ped) then DeleteEntity(ped) end end
|
||||
if companion and DoesEntityExist(companion) then DeleteEntity(companion) end
|
||||
end)
|
||||
@@ -0,0 +1,13 @@
|
||||
fx_version 'cerulean'
|
||||
game 'gta5'
|
||||
|
||||
author 'Colin-Joel / DevX Studios'
|
||||
description 'DevX RP Vanilla Unicorn social relationship and apartment companion system'
|
||||
version '0.4.3'
|
||||
lua54 'yes'
|
||||
shared_script 'shared/config.lua'
|
||||
ui_page 'web/index.html'
|
||||
files { 'web/index.html', 'web/style.css', 'web/app.js' }
|
||||
client_scripts { '@devx_core/client/callbacks.lua', 'client/main.lua' }
|
||||
server_scripts { '@oxmysql/lib/MySQL.lua', '@devx_core/server/callbacks.lua', 'server/main.lua' }
|
||||
dependencies { 'devx_core', 'devx_banking', 'devx_phone', 'devx_housing' }
|
||||
@@ -0,0 +1,139 @@
|
||||
local actionCooldowns = {}
|
||||
|
||||
MySQL.ready(function()
|
||||
MySQL.query.await([[
|
||||
CREATE TABLE IF NOT EXISTS devx_club_relations (
|
||||
character_id BIGINT UNSIGNED NOT NULL,
|
||||
relation_key VARCHAR(32) NOT NULL,
|
||||
satisfaction TINYINT UNSIGNED NOT NULL DEFAULT 0,
|
||||
contact_unlocked TINYINT(1) NOT NULL DEFAULT 0,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (character_id, relation_key),
|
||||
CONSTRAINT fk_devx_club_character FOREIGN KEY (character_id)
|
||||
REFERENCES devx_characters (id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
]])
|
||||
end)
|
||||
|
||||
local function relation(characterId)
|
||||
local row = MySQL.single.await([[
|
||||
SELECT satisfaction, contact_unlocked AS contactUnlocked
|
||||
FROM devx_club_relations WHERE character_id = ? AND relation_key = ? LIMIT 1
|
||||
]], { characterId, DevXStripclub.Nikki.key })
|
||||
return {
|
||||
satisfaction = row and tonumber(row.satisfaction) or 0,
|
||||
contactUnlocked = row and tonumber(row.contactUnlocked) == 1 or false
|
||||
}
|
||||
end
|
||||
|
||||
local function saveRelation(characterId, satisfaction, unlocked)
|
||||
MySQL.query.await([[
|
||||
INSERT INTO devx_club_relations (character_id, relation_key, satisfaction, contact_unlocked)
|
||||
VALUES (?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE satisfaction = VALUES(satisfaction), contact_unlocked = VALUES(contact_unlocked)
|
||||
]], { characterId, DevXStripclub.Nikki.key, satisfaction, unlocked and 1 or 0 })
|
||||
end
|
||||
|
||||
DevXRegisterCallback('stripclub:state', function(source)
|
||||
local player = exports.devx_core:GetPlayer(source)
|
||||
if not player then return { ok = false, error = 'Kein Charakter aktiv.' } end
|
||||
local current = relation(player.characterId)
|
||||
current.ok = true
|
||||
current.name = DevXStripclub.Nikki.name
|
||||
current.threshold = DevXStripclub.Nikki.contactThreshold
|
||||
return current
|
||||
end)
|
||||
|
||||
DevXRegisterCallback('stripclub:interact', function(source, payload)
|
||||
local player = exports.devx_core:GetPlayer(source)
|
||||
local action = payload and payload.action
|
||||
if not player or (action ~= 'talk' and action ~= 'lounge') then return { ok = false, error = 'Ungültige Anfrage.' } end
|
||||
|
||||
local now = os.time()
|
||||
local cooldown = actionCooldowns[source] or 0
|
||||
if cooldown > now then return { ok = false, error = ('Warte noch %s Sekunden.'):format(cooldown - now) } end
|
||||
|
||||
local gain = DevXStripclub.TalkGain
|
||||
if action == 'lounge' then
|
||||
local paid, result = exports.devx_banking:ChargeCharacter(
|
||||
player.characterId, DevXStripclub.LoungePrice, 'VIP-Lounge', 'Vanilla Unicorn'
|
||||
)
|
||||
if not paid then return { ok = false, error = result } end
|
||||
exports.devx_core:SetBankCache(source, result)
|
||||
gain = DevXStripclub.LoungeGain
|
||||
actionCooldowns[source] = now + 45
|
||||
else
|
||||
actionCooldowns[source] = now + 15
|
||||
end
|
||||
|
||||
local current = relation(player.characterId)
|
||||
current.satisfaction = math.min(DevXStripclub.Nikki.contactThreshold, current.satisfaction + gain)
|
||||
local newlyUnlocked = not current.contactUnlocked and current.satisfaction >= DevXStripclub.Nikki.contactThreshold
|
||||
if newlyUnlocked then current.contactUnlocked = true end
|
||||
saveRelation(player.characterId, current.satisfaction, current.contactUnlocked)
|
||||
|
||||
if current.contactUnlocked then
|
||||
exports.devx_phone:AddSystemContact(
|
||||
player.characterId, DevXStripclub.Nikki.name, DevXStripclub.Nikki.phone,
|
||||
'npc_companion', { relationKey = DevXStripclub.Nikki.key }
|
||||
)
|
||||
TriggerClientEvent('devx_phone:client:refresh', source)
|
||||
end
|
||||
|
||||
return {
|
||||
ok = true,
|
||||
satisfaction = current.satisfaction,
|
||||
threshold = DevXStripclub.Nikki.contactThreshold,
|
||||
contactUnlocked = current.contactUnlocked,
|
||||
newlyUnlocked = newlyUnlocked,
|
||||
action = action
|
||||
}
|
||||
end)
|
||||
|
||||
local function inviteCompanion(source)
|
||||
local player = exports.devx_core:GetPlayer(source)
|
||||
if not player then return { ok = false, error = 'Kein Charakter aktiv.' } end
|
||||
local current = relation(player.characterId)
|
||||
if not current.contactUnlocked then return { ok = false, error = 'Dieser Kontakt ist noch nicht freigeschaltet.' } end
|
||||
|
||||
local location = exports.devx_housing:GetPlayerHousingLocation(source)
|
||||
if not location or location.type ~= 'unit' then
|
||||
return { ok = false, error = 'Du musst dich in deinem Apartment befinden.' }
|
||||
end
|
||||
if tonumber(location.ownerCharacterId) ~= tonumber(player.characterId) then
|
||||
return { ok = false, error = 'Du kannst Nikki nur in dein eigenes Apartment einladen.' }
|
||||
end
|
||||
local spawn = location.interior and location.interior.companionSpawn
|
||||
if not spawn then return { ok = false, error = 'Für dieses Apartment ist kein Treffpunkt eingerichtet.' } end
|
||||
|
||||
TriggerClientEvent('devx_stripclub:client:spawnCompanion', source, {
|
||||
name = DevXStripclub.Nikki.name,
|
||||
model = DevXStripclub.Nikki.model,
|
||||
spawn = spawn
|
||||
})
|
||||
return { ok = true, message = 'Nikki ist auf dem Weg zu deinem Apartment.' }
|
||||
end
|
||||
|
||||
exports('InviteCompanion', inviteCompanion)
|
||||
|
||||
DevXRegisterCallback('stripclub:inviteCompanion', function(source)
|
||||
return inviteCompanion(source)
|
||||
end)
|
||||
|
||||
|
||||
RegisterCommand('resetnikki', function(source, args)
|
||||
if source ~= 0 and not IsPlayerAceAllowed(source, 'devx.admin') then return end
|
||||
local target = tonumber(args[1]) or source
|
||||
local player = exports.devx_core:GetPlayer(target)
|
||||
if not player then return end
|
||||
MySQL.query.await('DELETE FROM devx_club_relations WHERE character_id = ? AND relation_key = ?', {
|
||||
player.characterId, DevXStripclub.Nikki.key
|
||||
})
|
||||
MySQL.query.await("DELETE FROM devx_phone_contacts WHERE character_id = ? AND contact_type = 'npc_companion'", {
|
||||
player.characterId
|
||||
})
|
||||
TriggerClientEvent('devx_phone:client:refresh', target)
|
||||
TriggerClientEvent('devx_housing:client:notify', target, 'Nikki-Beziehung wurde zurückgesetzt.')
|
||||
end, false)
|
||||
|
||||
AddEventHandler('playerDropped', function() actionCooldowns[source] = nil end)
|
||||
@@ -0,0 +1,20 @@
|
||||
DevXStripclub = {
|
||||
Entry = vector3(127.40, -1283.12, 29.27),
|
||||
Nikki = {
|
||||
key = 'nikki',
|
||||
name = 'Nikki',
|
||||
phone = '555-0199',
|
||||
model = 'a_f_y_bevhills_02',
|
||||
position = vector4(117.18, -1302.15, 28.77, 303.0),
|
||||
lounge = vector4(120.70, -1300.05, 28.77, 210.0),
|
||||
contactThreshold = 100
|
||||
},
|
||||
Dancers = {
|
||||
{ model = 's_f_y_stripper_01', position = vector4(112.75, -1287.35, 28.46, 195.0), scenario = 'WORLD_HUMAN_MUSICIAN' },
|
||||
{ model = 's_f_y_stripper_02', position = vector4(104.85, -1294.10, 28.26, 25.0), scenario = 'WORLD_HUMAN_PARTYING' },
|
||||
{ model = 'a_f_y_bevhills_01', position = vector4(128.25, -1284.25, 29.28, 125.0), scenario = 'WORLD_HUMAN_STAND_MOBILE' }
|
||||
},
|
||||
TalkGain = 10,
|
||||
LoungeGain = 25,
|
||||
LoungePrice = 500
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
const box=document.querySelector('#relation'),fill=document.querySelector('#fill'),value=document.querySelector('#value'),nameEl=document.querySelector('#name'),message=document.querySelector('#message');let timer;
|
||||
window.addEventListener('message',event=>{const d=event.data||{};if(d.action!=='relationship')return;const max=Number(d.maximum)||100,current=Math.max(0,Math.min(max,Number(d.value)||0));nameEl.textContent=d.name||'Kontakt';message.textContent=d.message||'Zufriedenheit';value.textContent=`${current} / ${max}`;fill.style.width=`${current/max*100}%`;box.classList.remove('hidden');clearTimeout(timer);timer=setTimeout(()=>box.classList.add('hidden'),6500)});
|
||||
@@ -0,0 +1 @@
|
||||
<!doctype html><html lang="de"><head><meta charset="utf-8"><link rel="stylesheet" href="style.css"></head><body><section id="relation" class="hidden"><div><span id="name">Nikki</span><b id="message"></b><div class="track"><i id="fill"></i></div><small id="value">0 / 100</small></div></section><script src="app.js"></script></body></html>
|
||||
@@ -0,0 +1 @@
|
||||
:root{font-family:Inter,system-ui,sans-serif;color:white}*{box-sizing:border-box}body{margin:0;background:transparent;overflow:hidden}.hidden{display:none!important}#relation{position:fixed;left:50%;bottom:84px;transform:translateX(-50%);width:430px;padding:14px 18px;border:1px solid rgba(255,255,255,.14);border-radius:14px;background:rgba(8,10,16,.9);box-shadow:0 18px 50px rgba(0,0,0,.45)}#name{display:block;color:#f097ca;font-weight:900;font-size:12px;letter-spacing:.12em;text-transform:uppercase}#message{display:block;margin:4px 0 9px;font-size:13px}.track{height:8px;border-radius:20px;background:rgba(255,255,255,.1);overflow:hidden}.track i{display:block;height:100%;width:0;background:linear-gradient(90deg,#d958a8,#ff9ed5);transition:width .45s}small{display:block;margin-top:5px;text-align:right;color:rgba(255,255,255,.6)}
|
||||
Reference in New Issue
Block a user