-- ar_waypoint : client (beacon) -- มาร์คเกอร์ DUI ปักพื้นแบบ sleepless_waypoints ที่ปลายทาง + เคลียร์ waypoint เมื่อถึง -- ส่วน "เส้นนำทางบนถนน" ใช้เอนจิน l2k_gps3d ใน client/route_gps3d.lua (แยกอิสระจากไฟล์นี้) local enabled = Config.Enabled local dest = nil -- vector3 ปลายทางจาก waypoint (z จาก blip เชื่อไม่ได้ ใช้ระยะแบบ 2D) local destGroundZ = nil -- z พื้นจริงที่ปลายทาง (ละเอียด — หาเจอเมื่อ collision โหลดถึง) local destCoarseZ = nil -- z พื้นหยาบจาก heightmap (ใช้ไปก่อนตอนอยู่ไกล) -- DUI (สร้างครั้งเดียว ใช้ซ้ำทุก waypoint) local TXD_NAME, TXN_NAME = 'ar_waypoint_txd', 'ar_waypoint_txn' local runtimeTxd = nil local duiObj = nil local duiBornAt = 0 local nextDuiUpdate = 0 local lastDistV, lastDistU = nil, nil local function Notify(msg) if msg then Config.Functions.Client.Notify(msg) end end local function ResetDest() dest, destGroundZ, destCoarseZ = nil, nil, nil lastDistV, lastDistU = nil, nil end -- คืนค่าเป็น (ตัวเลข, หน่วย) เช่น '1.2','Km.' / '850','M.' local function FormatDist(d) if d >= 1000.0 then return ('%.1f'):format(d / 1000.0), 'Km.' end return ('%d'):format(math.floor(d)), 'M.' end local function EnsureTxd() if not runtimeTxd then runtimeTxd = CreateRuntimeTxd(TXD_NAME) end return runtimeTxd end local function EnsureDui() if duiObj then return end local D = Config.Beacon.dui duiObj = CreateDui(('nui://%s/html/dui.html'):format(GetCurrentResourceName()), D.texture.w, D.texture.h) CreateRuntimeTextureFromDuiHandle(EnsureTxd(), TXN_NAME, GetDuiHandle(duiObj)) duiBornAt = GetGameTimer() end -- วาด DUI เป็น quad 3D ปักพื้น หันเข้าหากล้องเสมอ (วิธีเดียวกับ sleepless_waypoints) -- bottom = จุดปักพื้น, quad กาง width x height ขึ้นด้านบน local function DrawDuiQuad(bottom, width, height, alpha) local cam = GetFinalRenderedCamCoord() local flat = vec3(cam.x - bottom.x, cam.y - bottom.y, 0.0) if #flat < 0.01 then return end -- กล้องอยู่ตรงหัวพอดี คำนวณทิศไม่ได้ local fwd = norm(flat) local right = norm(cross(vec3(0.0, 0.0, 1.0), fwd)) local half = right * (width * 0.5) local up = vec3(0.0, 0.0, height) local bl, br = bottom - half, bottom + half local tl, tr = bl + up, br + up DrawTexturedPoly(tr.x, tr.y, tr.z, tl.x, tl.y, tl.z, bl.x, bl.y, bl.z, 255, 255, 255, alpha, TXD_NAME, TXN_NAME, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0) DrawTexturedPoly(tr.x, tr.y, tr.z, bl.x, bl.y, bl.z, br.x, br.y, br.z, 255, 255, 255, alpha, TXD_NAME, TXN_NAME, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0) end -- ── เฝ้าดู waypoint บนแผนที่ (มาร์คใหม่ / เอาออก) ────────────────────────── CreateThread(function() while true do Wait(500) if enabled and IsWaypointActive() then local blip = GetFirstBlipInfoId(8) -- 8 = waypoint blip if DoesBlipExist(blip) then local c = GetBlipInfoIdCoord(blip) if not dest or #(vector2(c.x - dest.x, c.y - dest.y)) > 5.0 then ResetDest() dest = vector3(c.x, c.y, c.z) nextDuiUpdate = 0 -- ให้ป้ายอัปเดตทันทีรอบแรก -- z หยาบจาก heightmap = ระดับพื้นโลกจริง (ถ้า native ไม่มีก็ข้าม) local okH, hz = pcall(GetHeightmapBottomZForPosition, c.x, c.y) if okH and type(hz) == 'number' then destCoarseZ = hz end Config.Functions.Client.OnWaypointSet(dest) end end elseif dest then ResetDest() Config.Functions.Client.OnWaypointCleared() end end end) -- ── หา z พื้นของปลายทาง (ทำงานช้าๆ แยกจาก loop วาด) ─────────────────────── CreateThread(function() while true do Wait(500) if dest and enabled and not destGroundZ then local pc = GetEntityCoords(PlayerPedId()) if #(vector2(pc.x - dest.x, pc.y - dest.y)) < 350.0 then if Config.Beacon.groundMode == 'terrain' and destCoarseZ then -- ปักทะลุพื้นโลก: เริ่มยิงหาพื้นจากระดับ heightmap (อยู่ใต้หลังคาตึก) -- → ได้ระดับถนน/พื้นดินจริง ไม่ใช่ดาดฟ้าสิ่งก่อสร้าง local ok, gz = GetGroundZFor_3dCoord(dest.x, dest.y, destCoarseZ + 8.0, false) if ok and gz ~= 0.0 then destGroundZ = gz end else -- เกาะพื้นผิวบนสุด: ยิงหาพื้นจากบนฟ้าไล่ลงมา for _, h in ipairs({ 1000.0, 250.0, 80.0 }) do local ok, gz = GetGroundZFor_3dCoord(dest.x, dest.y, h, false) if ok and gz ~= 0.0 then destGroundZ = gz break end end end end end end end) -- ── วาดทุกเฟรม (เฉพาะตอนมี waypoint) ─────────────────────────────────────── CreateThread(function() while true do local sleep = 500 if dest and enabled and Config.Functions.Client.CanShow() == true then sleep = 0 local pc = GetEntityCoords(PlayerPedId()) local dist2d = #(vector2(pc.x - dest.x, pc.y - dest.y)) if Config.ArriveDistance and dist2d <= Config.ArriveDistance then -- ถึงปลายทาง → เคลียร์ waypoint ให้อัตโนมัติ if IsWaypointActive() then SetWaypointOff() end Notify(Config.ArriveMessage) Config.Functions.Client.OnArrived(dest) ResetDest() else local B = Config.Beacon if B.enabled then local col = B.color -- z ฐาน: พื้นจริง > พื้นหยาบจาก heightmap > ความสูงตัวผู้เล่น local baseZ = destGroundZ or destCoarseZ or pc.z if B.beam.enabled then DrawMarker(1, dest.x, dest.y, baseZ - 40.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, B.beam.radius, B.beam.radius, B.beam.height, col.r, col.g, col.b, B.beam.alpha, false, false, 2, false, nil, nil, false) end if B.ring.enabled and destGroundZ and dist2d < 200.0 then DrawMarker(25, dest.x, dest.y, destGroundZ + 0.2, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, B.ring.radius, B.ring.radius, 1.0, col.r, col.g, col.b, B.ring.alpha, false, false, 2, false, nil, nil, false) end local M = B.dui.marker -- drawDistance = false → วาดตลอดทุกระยะ if B.dui.enabled and (not M.drawDistance or dist2d < M.drawDistance) then EnsureDui() -- อัปเดตตัวเลขเฉพาะตอนค่าเปลี่ยน (ช่วง 5 วิแรกหลังสร้าง DUI ส่งซ้ำกันเคสหน้าเพิ่งโหลด) local now = GetGameTimer() if now >= nextDuiUpdate then nextDuiUpdate = now + B.dui.updateMs local v, u = FormatDist(dist2d) if v ~= lastDistV or u ~= lastDistU or (now - duiBornAt) < 5000 then lastDistV, lastDistU = v, u SendDuiMessage(duiObj, json.encode({ action = 'update', dist = v, unit = u, label = B.dui.label, primary = B.dui.primary, secondary = B.dui.secondary, })) end end -- สเกลตามระยะกล้องให้ขนาดบนจอคงที่ (สูตร sleepless) local scale = math.max(M.minScale, dist2d / M.perDistance) local width = M.baseWidth * scale local height = width * M.aspect -- maxHeight = false → โตตามระยะไม่จำกัด if M.maxHeight and height > M.maxHeight then height = M.maxHeight width = height / M.aspect end -- minHeight = false → หดตามระยะ | ใส่ค่า = เข้าใกล้แล้วป้ายไม่เล็กกว่านี้ if M.minHeight and height < M.minHeight then height = M.minHeight width = height / M.aspect end -- fade = false → ทึบตลอด local alpha = 255 if M.fade and dist2d > M.fade.start then local fin = M.fade.finish or (M.fade.start + 300.0) local t = (dist2d - M.fade.start) / math.max(1.0, fin - M.fade.start) alpha = math.floor(255 * math.max(0.0, 1.0 - t)) end if alpha > 0 then DrawDuiQuad(vec3(dest.x, dest.y, baseZ + M.groundOffset), width, height, alpha) end end end end end Wait(sleep) end end) exports('SetNavigateValue', function(value) enabled = value pcall(function() exports[GetCurrentResourceName()]:SetEnabled(enabled) end) end) -- ── เก็บกวาด DUI ตอน resource หยุด ────────────────────────────────────────── AddEventHandler('onResourceStop', function(res) if res ~= GetCurrentResourceName() then return end if duiObj then DestroyDui(duiObj); duiObj = nil end end) -- ── exports ให้สคริปต์อื่นสั่งนำทางได้ ────────────────────────────────────── -- exports['ar_waypoint']:SetDestination(vector3(x, y, z)) → มาร์ค waypoint + เริ่มนำทาง -- exports['ar_waypoint']:ClearDestination() → ยกเลิกนำทาง -- exports['ar_waypoint']:IsNavigating() → กำลังนำทางอยู่ไหม exports('SetDestination', function(coords) if not coords then return end SetNewWaypoint(coords.x + 0.0, coords.y + 0.0) end) exports('ClearDestination', function() if IsWaypointActive() then SetWaypointOff() end end) exports('IsNavigating', function() return dest ~= nil end)