Licht ist in GTA nie synchronisiert - DrawSpotLight zeichnet nur lokal fuer den aktuellen Frame. "Gesynct" heisst deshalb: der Zustand wird als Statebag repliziert und jeder Client zeichnet die Lampen aller Taucher in seiner Naehe selbst. Muster uebernommen aus d4rk_phone (Statebag, adaptives Polling, Distanzfilter). client/light.lua ist ein reiner Renderer ohne gemeinsamen Zustand mit main.lua - er kommt mit dem Statebag allein aus und kennt weder Ausruestung noch Flasche. - Richtung ueber zwei bone-relative Punkte am Kopf-Bone statt ueber GetEntityForwardVector: beim Tauchen liegt der Ped waagerecht im Wasser, der Entity-Forward wuerde stur horizontal leuchten. Die Offsets von GetPedBoneCoords sind laut Native-Doku "relative to the bone's rotation", die Differenz zweier solcher Punkte enthaelt also den Pitch - Toggle per lib.addKeybind, Standard L, vom Spieler umlegbar - Distanzfilter und Limit auf die naechsten N Lampen, Schatten abschaltbar - Prop-Model wird vorher mit IsModelValid geprueft: lib.requestModel wirft bei ungueltigen Models einen Error, der sonst die ganze Ausruestung lahmlegt - Lampen-Status im HUD Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
local config = require 'config.client'
|
||||
|
||||
-- Reiner Renderer: zeichnet die Lampen aller Taucher in der Nähe, gesteuert
|
||||
-- ausschließlich über den replizierten Statebag. Kein gemeinsamer Zustand mit
|
||||
-- client/main.lua - das hier läuft auch für Spieler, deren Ausrüstung wir gar
|
||||
-- nicht kennen.
|
||||
--
|
||||
-- Licht ist in GTA nie synchronisiert: DrawSpotLight zeichnet nur lokal, für den
|
||||
-- aktuellen Frame. "Gesynct" heißt deshalb, dass jeder Client das Licht jedes
|
||||
-- anderen Tauchers selbst zeichnet.
|
||||
|
||||
local STATE_KEY = 'divelight'
|
||||
local HEAD_BONE = 31086 -- SKEL_Head
|
||||
|
||||
--- Kandidaten des aktuellen Frames. Wird wiederverwendet statt jedes Frame neu
|
||||
--- angelegt, damit der GC bei Dauerbetrieb nichts zu tun bekommt.
|
||||
local candidates = {}
|
||||
|
||||
--- Position und Richtung der Lampe eines Peds.
|
||||
---
|
||||
--- Die Offsets von GetPedBoneCoords sind laut Native-Doku "relative to the bone's
|
||||
--- rotation" - zwei bone-relative Punkte ergeben also die echte Blickrichtung
|
||||
--- inklusive Pitch. Genau das wird beim Tauchen gebraucht: der Ped liegt waagerecht
|
||||
--- im Wasser, GetEntityForwardVector würde die Lampe stur horizontal leuchten lassen.
|
||||
local function getLampVectors(ped)
|
||||
local offset = config.lampOffset
|
||||
local direction = config.lampDirection
|
||||
|
||||
local from = GetPedBoneCoords(ped, HEAD_BONE, offset.x, offset.y, offset.z)
|
||||
local ahead = GetPedBoneCoords(ped, HEAD_BONE,
|
||||
offset.x + direction.x,
|
||||
offset.y + direction.y,
|
||||
offset.z + direction.z)
|
||||
|
||||
local delta = ahead - from
|
||||
local length = #delta
|
||||
|
||||
if length <= 0 then return end
|
||||
|
||||
return from, delta / length
|
||||
end
|
||||
|
||||
local function drawLamp(ped)
|
||||
local from, direction = getLampVectors(ped)
|
||||
|
||||
if not from then return end
|
||||
|
||||
local colour = config.lampColour
|
||||
|
||||
if config.lampShadows then
|
||||
DrawSpotLightWithShadow(from.x, from.y, from.z, direction.x, direction.y, direction.z,
|
||||
colour[1], colour[2], colour[3],
|
||||
config.lampDistance, config.lampBrightness, config.lampRoundness,
|
||||
config.lampRadius, config.lampFalloff, 0)
|
||||
else
|
||||
DrawSpotLight(from.x, from.y, from.z, direction.x, direction.y, direction.z,
|
||||
colour[1], colour[2], colour[3],
|
||||
config.lampDistance, config.lampBrightness, config.lampRoundness,
|
||||
config.lampRadius, config.lampFalloff)
|
||||
end
|
||||
end
|
||||
|
||||
CreateThread(function()
|
||||
while true do
|
||||
local players = GetActivePlayers()
|
||||
local myCoords = GetEntityCoords(cache.ped)
|
||||
local count = 0
|
||||
|
||||
for i = 1, #players do
|
||||
local player = players[i]
|
||||
local state = Player(GetPlayerServerId(player)).state
|
||||
|
||||
if state and state[STATE_KEY] then
|
||||
local ped = GetPlayerPed(player)
|
||||
|
||||
if ped ~= 0 and DoesEntityExist(ped) then
|
||||
local distance = #(myCoords - GetEntityCoords(ped))
|
||||
|
||||
if distance < config.lampDrawDistance then
|
||||
count += 1
|
||||
local entry = candidates[count]
|
||||
|
||||
if entry then
|
||||
entry.ped, entry.distance = ped, distance
|
||||
else
|
||||
candidates[count] = { ped = ped, distance = distance }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Reste des letzten Frames abschneiden, sonst sortiert table.sort veraltete
|
||||
-- Einträge mit und ein alter, näherer Eintrag verdrängt einen echten.
|
||||
for i = #candidates, count + 1, -1 do
|
||||
candidates[i] = nil
|
||||
end
|
||||
|
||||
-- Nur sortieren wenn tatsächlich mehr Lampen da sind als gezeichnet werden
|
||||
-- dürfen. DrawSpotLightWithShadow ist teuer, in einer vollen Höhle sonst
|
||||
-- der erstbeste Frame-Killer.
|
||||
if count > config.lampMaxLights then
|
||||
table.sort(candidates, function(a, b) return a.distance < b.distance end)
|
||||
end
|
||||
|
||||
for i = 1, math.min(count, config.lampMaxLights) do
|
||||
drawLamp(candidates[i].ped)
|
||||
end
|
||||
|
||||
Wait(count > 0 and 0 or 300)
|
||||
end
|
||||
end)
|
||||
+51
-2
@@ -4,9 +4,14 @@ local shared = require 'config.shared'
|
||||
local currentGear = {
|
||||
mask = 0,
|
||||
tank = 0,
|
||||
lamp = 0,
|
||||
enabled = false
|
||||
}
|
||||
|
||||
--- Lampe an? Der eigentliche Lichtkegel wird in client/light.lua gezeichnet,
|
||||
--- gesteuert über den replizierten Statebag.
|
||||
local lampOn = false
|
||||
|
||||
--- Aktuell getragene Flasche. { slot, name, label, oxygen, capacity }
|
||||
--- oxygen sind Restsekunden, kein Prozentwert.
|
||||
local activeTank = nil
|
||||
@@ -86,6 +91,12 @@ local function deleteGear()
|
||||
DeleteEntity(currentGear.tank)
|
||||
currentGear.tank = 0
|
||||
end
|
||||
|
||||
if currentGear.lamp ~= 0 then
|
||||
DetachEntity(currentGear.lamp, false, true)
|
||||
DeleteEntity(currentGear.lamp)
|
||||
currentGear.lamp = 0
|
||||
end
|
||||
end
|
||||
|
||||
--- Hängt ein Prop hart an einen Ped-Bone.
|
||||
@@ -111,11 +122,34 @@ local function attachProp(ped, model, bone, offset, rotation)
|
||||
return prop
|
||||
end
|
||||
|
||||
--- Lampen-Model einmalig prüfen. lib.requestModel wirft bei ungültigen Models einen
|
||||
--- Error - ohne diese Prüfung würde ein falsch geschriebener Prop-Name die komplette
|
||||
--- Ausrüstung unbenutzbar machen, statt nur die Optik der Lampe zu kosten.
|
||||
local lampModel = joaat(config.lampProp)
|
||||
local hasLampProp = IsModelValid(lampModel) or IsModelInCdimage(lampModel)
|
||||
|
||||
if not hasLampProp then
|
||||
lib.print.warn(('lampProp "%s" ist kein gültiges Model - die Lampe leuchtet, das Prop fehlt.'):format(config.lampProp))
|
||||
end
|
||||
|
||||
--- @param ped number? default cache.ped
|
||||
local function attachGear(ped)
|
||||
ped = ped or cache.ped
|
||||
currentGear.tank = attachProp(ped, TANK_MODEL, TANK_BONE, vec3(-0.25, -0.25, 0.0), vec3(180.0, 90.0, 0.0))
|
||||
currentGear.mask = attachProp(ped, MASK_MODEL, MASK_BONE, vec3(0.0, 0.0, 0.0), vec3(180.0, 90.0, 0.0))
|
||||
|
||||
if hasLampProp then
|
||||
currentGear.lamp = attachProp(ped, lampModel, MASK_BONE, vec3(0.1, 0.05, 0.0), vec3(180.0, 90.0, 0.0))
|
||||
end
|
||||
end
|
||||
|
||||
--- @param state boolean
|
||||
local function setLamp(state)
|
||||
if not currentGear.enabled then state = false end
|
||||
|
||||
lampOn = state
|
||||
-- Repliziert: client/light.lua zeichnet daraus die Lampen aller Taucher.
|
||||
LocalPlayer.state:set('divelight', state, true)
|
||||
end
|
||||
|
||||
local function isPropIntact(prop)
|
||||
@@ -123,6 +157,8 @@ local function isPropIntact(prop)
|
||||
end
|
||||
|
||||
local function isGearIntact()
|
||||
if hasLampProp and not isPropIntact(currentGear.lamp) then return false end
|
||||
|
||||
return isPropIntact(currentGear.mask) and isPropIntact(currentGear.tank)
|
||||
end
|
||||
|
||||
@@ -151,6 +187,7 @@ local function removeGear(outOfAir, immediateSync)
|
||||
syncTank(immediateSync)
|
||||
|
||||
currentGear.enabled = false
|
||||
setLamp(false)
|
||||
activeTank = nil
|
||||
warned = {}
|
||||
|
||||
@@ -272,8 +309,8 @@ local function sendHudState(visible)
|
||||
depth = currentDepth,
|
||||
level = level,
|
||||
deep = currentDepth >= config.deepWarningDepth,
|
||||
hasLamp = false,
|
||||
lamp = false
|
||||
hasLamp = true,
|
||||
lamp = lampOn
|
||||
})
|
||||
end
|
||||
|
||||
@@ -441,6 +478,17 @@ RegisterNetEvent('d4rk_divegear:client:useGear', function()
|
||||
end
|
||||
end)
|
||||
|
||||
lib.addKeybind({
|
||||
name = 'divegear_lamp',
|
||||
description = locale('keybind.toggle_lamp'),
|
||||
defaultKey = config.lampKey,
|
||||
onPressed = function()
|
||||
if not currentGear.enabled then return end
|
||||
|
||||
setLamp(not lampOn)
|
||||
end
|
||||
})
|
||||
|
||||
--- Beim Model-Wechsel (Kleiderladen, Skin-Change) ist der Ped ein anderer -
|
||||
--- alle attached Props sind weg und die Scuba-Flags gelten für den alten Ped.
|
||||
---
|
||||
@@ -463,6 +511,7 @@ AddEventHandler('onResourceStop', function(resource)
|
||||
if resource ~= GetCurrentResourceName() then return end
|
||||
|
||||
syncTank(true)
|
||||
setLamp(false)
|
||||
deleteGear()
|
||||
SendNUIMessage({ action = 'state', visible = false })
|
||||
SetEnableScuba(cache.ped, false)
|
||||
|
||||
Reference in New Issue
Block a user