-- ============================================================================ -- M-Addon :: tablet NUI bridge -- Data is fetched fresh from the server on open, so the "X / Y virov na voljo" -- counters are never stale. -- ============================================================================ local open = false local tabletProp = nil local hudVisible = false local hudWatcher = false -- -------------------------------------------------------------------------- -- PROP + ANIMATION -- -------------------------------------------------------------------------- local function attachTablet() local config = Config.TabletProp local hash = joaat(config.model) if not IsModelValid(hash) then return end lib.requestModel(hash, 5000) lib.requestAnimDict(config.anim.dict, 5000) local coords = GetEntityCoords(cache.ped) tabletProp = CreateObject(hash, coords.x, coords.y, coords.z, true, true, false) AttachEntityToEntity(tabletProp, cache.ped, GetPedBoneIndex(cache.ped, config.bone), config.offset.x, config.offset.y, config.offset.z, config.rotation.x, config.rotation.y, config.rotation.z, true, true, false, true, 1, true) TaskPlayAnim(cache.ped, config.anim.dict, config.anim.clip, 3.0, -3.0, -1, 49, 0, false, false, false) SetModelAsNoLongerNeeded(hash) end local function detachTablet() if tabletProp and DoesEntityExist(tabletProp) then DetachEntity(tabletProp, true, true) DeleteObject(tabletProp) end tabletProp = nil ClearPedTasks(cache.ped) end -- -------------------------------------------------------------------------- -- OPEN / CLOSE -- -------------------------------------------------------------------------- function OpenTablet() if open then return end local data = lib.callback.await('M-Addon:getTabletData', false) if not data then return Notify('notify_dependency_missing', nil, 'error') end open = true attachTablet() -- Strings are shipped to the NUI so the UI never hardcodes Slovenian. data.locale = { title = locale('tablet_title'), subtitle = locale('tablet_subtitle'), requirements = locale('tablet_requirements'), rewards = locale('tablet_rewards'), setWaypoint = locale('tablet_set_waypoint'), details = locale('tablet_details'), startJob = locale('tablet_start_job'), stopJob = locale('tablet_stop_job'), locked = locale('tablet_locked'), close = locale('tablet_close'), noTool = locale('tablet_no_tool_needed'), tabSources = locale('tablet_tab_sources'), tabBoard = locale('tablet_tab_leaderboard'), tabQuests = locale('tablet_tab_quests'), emptyQuests = locale('tablet_empty_quests'), questReward = locale('tablet_quest_reward'), questDone = locale('tablet_quest_done'), completed = locale('tablet_completed'), quests = locale('tablet_quests'), leaderboard = locale('tablet_leaderboard'), accept = locale('tablet_accept'), claim = locale('tablet_claim'), howItWorks = locale('tablet_how_it_works'), toolConsumed = locale('tablet_tool_consumed'), toolKept = locale('tablet_tool_kept'), questProgress = locale('quest_progress', '%s', '%s'), questCooldown = locale('quest_cooldown', '%s'), questLevel = locale('quest_level_required', '%s'), needs = locale('tablet_needs'), player = locale('tablet_col_player'), collected = locale('tablet_col_collected'), legal = locale('tablet_legal'), illegal = locale('tablet_illegal'), propCount = locale('tablet_field_props'), statusField = locale('tablet_field_status'), active = locale('tablet_status_active'), overview = locale('tablet_overview'), howTo = locale('tablet_how_to'), stepTravel = locale('tablet_step_travel'), stepTool = locale('tablet_step_tool', '%s'), stepNoTool = locale('tablet_step_no_tool'), stepStart = locale('tablet_step_start'), stepHarvest = locale('tablet_step_harvest'), stepStop = locale('tablet_step_stop'), fieldDuration = locale('tablet_field_duration'), fieldRespawn = locale('tablet_field_respawn'), fieldRadius = locale('tablet_field_radius'), fieldLocation = locale('tablet_field_location'), seconds = locale('tablet_seconds', '%s'), metres = locale('tablet_metres', '%s'), warnLevel = locale('tablet_warn_level', '%s'), warnJob = locale('tablet_warn_job'), warnDepleted = locale('tablet_warn_depleted'), level = locale('tablet_level'), total = locale('tablet_total'), progress = locale('tablet_progress'), xpRemaining = locale('tablet_xp_remaining', '%s'), maxLevel = locale('tablet_max_level'), depleted = locale('tablet_depleted'), emptyBoard = locale('tablet_empty_board'), categories = locale('tablet_categories'), weight = locale('tablet_weight'), consumable = locale('tablet_tool_consumable'), breakable = locale('tablet_tool_breakable', '%s'), available = locale('tablet_available', '%s', '%s'), levelRequired = locale('tablet_level_required', '%s'), jobRequired = locale('tablet_job_required') } SetNuiFocus(true, true) SendNUIMessage({ action = 'open', data = data }) end local function closeTablet() if not open then return end open = false SetNuiFocus(false, false) SendNUIMessage({ action = 'close' }) detachTablet() end -- -------------------------------------------------------------------------- -- NUI CALLBACKS -- -------------------------------------------------------------------------- RegisterNUICallback('close', function(_, cb) closeTablet() cb('ok') end) RegisterNUICallback('setWaypoint', function(data, cb) -- A waypoint only, deliberately: a permanent route blip would outlive the -- tablet and clutter the map the way the old job blips did. if type(data.x) == 'number' and type(data.y) == 'number' then SetNewWaypoint(data.x + 0.0, data.y + 0.0) Notify('notify_waypoint_set', nil, 'success') end closeTablet() cb('ok') end) RegisterNUICallback('startJob', function(data, cb) closeTablet() if type(data.index) == 'number' then StartJob(data.index) end cb('ok') end) RegisterNUICallback('stopJob', function(_, cb) if not StopJob() then Notify('notify_no_active_job', nil, 'error') end cb('ok') end) RegisterNUICallback('refresh', function(_, cb) cb(lib.callback.await('M-Addon:getTabletData', false) or {}) end) -- -------------------------------------------------------------------------- -- ESC -- While the NUI has focus the browser receives the key, so web/script.js -- handles ESC and calls the 'close' callback above. That avoids a per-frame -- IsControlJustPressed loop and a reserved-key binding. -- -------------------------------------------------------------------------- AddEventHandler('onResourceStop', function(resourceName) if resourceName ~= GetCurrentResourceName() then return end SetNuiFocus(false, false) detachTablet() end) -- -------------------------------------------------------------------------- -- HUD TRACKER -- A passive layer in the same NUI page. SetNuiFocus is never called for it, -- so it cannot swallow mouse or keyboard input. -- -------------------------------------------------------------------------- function IsTabletOpen() return open end function ShowHud(data) if not Config.HudEnabled or not data then return end hudVisible = true data.locale = { collected = locale('hud_collected'), xp = locale('hud_session_xp'), hint = locale('hud_stop_hint') } SendNUIMessage({ action = 'hud', data = data }) StartHudWatcher() end function UpdateHud(data) if not Config.HudEnabled or not hudVisible or not data then return end data.locale = { collected = locale('hud_collected'), xp = locale('hud_session_xp'), hint = locale('hud_stop_hint') } SendNUIMessage({ action = 'hud', data = data }) end function HideHud() hudVisible = false SendNUIMessage({ action = 'hudHide' }) end ---Hides the panel while the pause menu or tablet is up. Runs only while the ---HUD is on screen, so there is no idle cost. function StartHudWatcher() if hudWatcher then return end hudWatcher = true CreateThread(function() local shown = true while hudVisible do local hide = IsPauseMenuActive() or open if Config.HudHideInVehicle and IsPedInAnyVehicle(cache.ped, false) then hide = true end if hide == shown then shown = not hide SendNUIMessage({ action = 'hudVisible', visible = shown }) end Wait(250) end hudWatcher = false end) end ---Animated toast used for quest completion and level ups. function ShowToast(payload) SendNUIMessage({ action = 'toast', data = payload }) if Config.QuestSound then PlaySoundFrontend(-1, Config.QuestSound.name, Config.QuestSound.set, true) end end -- -------------------------------------------------------------------------- -- QUEST CALLBACKS -- -------------------------------------------------------------------------- RegisterNUICallback('acceptQuest', function(data, cb) local ok, reason, args, result = lib.callback.await('M-Addon:acceptQuest', false, data.id) if not ok then Notify(reason or 'notify_gone', args, 'error') return cb({ ok = false }) end Notify('quest_accepted', { result.label }, 'success') cb({ ok = true, data = lib.callback.await('M-Addon:getTabletData', false) }) end) RegisterNUICallback('claimQuest', function(data, cb) local ok, reason, args, result = lib.callback.await('M-Addon:claimQuest', false, data.id) if not ok then Notify(reason or 'notify_gone', args, 'error') return cb({ ok = false }) end ShowToast({ kind = 'quest', title = locale('quest_completed_title'), name = result.label, xp = result.xp, money = result.money, items = result.items }) if result.levelUp then ShowToast({ kind = 'level', title = locale('levelup_title'), name = tostring(result.levelUp) }) end cb({ ok = true, data = lib.callback.await('M-Addon:getTabletData', false) }) end)