diff --git a/.gitignore b/.gitignore index 07b5567..32cc0ee 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ *.cfg /cache/ /resources/\[local\]/ +/db/ \ No newline at end of file diff --git a/resources/[gameplay]/[examples]/money-fountain-example-map/fxmanifest.lua b/resources/[gameplay]/[examples]/money-fountain-example-map/fxmanifest.lua new file mode 100644 index 0000000..412880b --- /dev/null +++ b/resources/[gameplay]/[examples]/money-fountain-example-map/fxmanifest.lua @@ -0,0 +1,6 @@ +fx_version 'cerulean' +game 'gta5' + +map 'map.lua' + +dependency 'money-fountain' \ No newline at end of file diff --git a/resources/[gameplay]/[examples]/money-fountain-example-map/map.lua b/resources/[gameplay]/[examples]/money-fountain-example-map/map.lua new file mode 100644 index 0000000..e7e1626 --- /dev/null +++ b/resources/[gameplay]/[examples]/money-fountain-example-map/map.lua @@ -0,0 +1,4 @@ +money_fountain 'test_fountain' { + vector3(97.334, -973.621, 29.36), + amount = 75 +} \ No newline at end of file diff --git a/resources/[gameplay]/[examples]/money-fountain/client.lua b/resources/[gameplay]/[examples]/money-fountain/client.lua new file mode 100644 index 0000000..1cdc1f1 --- /dev/null +++ b/resources/[gameplay]/[examples]/money-fountain/client.lua @@ -0,0 +1,101 @@ +-- add text entries for all the help types we have +AddTextEntry('FOUNTAIN_HELP', 'This fountain currently contains $~1~.~n~Press ~INPUT_PICKUP~ to obtain $~1~.~n~Press ~INPUT_DETONATE~ to place $~1~.') +AddTextEntry('FOUNTAIN_HELP_DRAINED', 'This fountain currently contains $~1~.~n~Press ~INPUT_DETONATE~ to place $~1~.') +AddTextEntry('FOUNTAIN_HELP_BROKE', 'This fountain currently contains $~1~.~n~Press ~INPUT_PICKUP~ to obtain $~1~.') +AddTextEntry('FOUNTAIN_HELP_BROKE_N_DRAINED', 'This fountain currently contains $~1~.') +AddTextEntry('FOUNTAIN_HELP_INUSE', 'This fountain currently contains $~1~.~n~You can use it again in ~a~.') + +-- upvalue aliases so that we will be fast if far away +local Wait = Wait +local GetEntityCoords = GetEntityCoords +local PlayerPedId = PlayerPedId + +-- timer, don't tick as frequently if we're far from any money fountain +local relevanceTimer = 500 + +CreateThread(function() + local pressing = false + + while true do + Wait(relevanceTimer) + + local coords = GetEntityCoords(PlayerPedId()) + + for _, data in pairs(moneyFountains) do + -- if we're near this fountain + local dist = #(coords - data.coords) + + -- near enough to draw + if dist < 40 then + -- ensure per-frame tick + relevanceTimer = 0 + + DrawMarker(29, data.coords.x, data.coords.y, data.coords.z, 0, 0, 0, 0.0, 0, 0, 1.0, 1.0, 1.0, 0, 150, 0, 120, false, true, 2, false, nil, nil, false) + else + -- put the relevance timer back to the way it was + relevanceTimer = 500 + end + + -- near enough to use + if dist < 1 then + -- are we able to use it? if not, display appropriate help + local player = LocalPlayer + local nextUse = player.state['fountain_nextUse'] + + -- GetNetworkTime is synced for everyone + if nextUse and nextUse >= GetNetworkTime() then + BeginTextCommandDisplayHelp('FOUNTAIN_HELP_INUSE') + AddTextComponentInteger(GlobalState['fountain_' .. data.id]) + AddTextComponentSubstringTime(math.tointeger(nextUse - GetNetworkTime()), 2 | 4) -- seconds (2), minutes (4) + EndTextCommandDisplayHelp(0, false, false, 1000) + else + -- handle inputs for pickup/place + if not pressing then + if IsControlPressed(0, 38 --[[ INPUT_PICKUP ]]) then + TriggerServerEvent('money_fountain:tryPickup', data.id) + pressing = true + elseif IsControlPressed(0, 47 --[[ INPUT_DETONATE ]]) then + TriggerServerEvent('money_fountain:tryPlace', data.id) + pressing = true + end + else + if not IsControlPressed(0, 38 --[[ INPUT_PICKUP ]]) and + not IsControlPressed(0, 47 --[[ INPUT_DETONATE ]]) then + pressing = false + end + end + + -- decide the appropriate help message + local youCanSpend = (player.state['money_cash'] or 0) >= data.amount + local fountainCanSpend = GlobalState['fountain_' .. data.id] >= data.amount + + local helpName + + if youCanSpend and fountainCanSpend then + helpName = 'FOUNTAIN_HELP' + elseif youCanSpend and not fountainCanSpend then + helpName = 'FOUNTAIN_HELP_DRAINED' + elseif not youCanSpend and fountainCanSpend then + helpName = 'FOUNTAIN_HELP_BROKE' + else + helpName = 'FOUNTAIN_HELP_BROKE_N_DRAINED' + end + + -- and print it + BeginTextCommandDisplayHelp(helpName) + AddTextComponentInteger(GlobalState['fountain_' .. data.id]) + + if fountainCanSpend then + AddTextComponentInteger(data.amount) + end + + if youCanSpend then + AddTextComponentInteger(data.amount) + end + + EndTextCommandDisplayHelp(0, false, false, 1000) + end + end + end + end +end) \ No newline at end of file diff --git a/resources/[gameplay]/[examples]/money-fountain/fxmanifest.lua b/resources/[gameplay]/[examples]/money-fountain/fxmanifest.lua new file mode 100644 index 0000000..159b453 --- /dev/null +++ b/resources/[gameplay]/[examples]/money-fountain/fxmanifest.lua @@ -0,0 +1,18 @@ +version '1.0.0' +description 'An example money system client containing a money fountain.' +author 'Cfx.re ' + +fx_version 'bodacious' +game 'gta5' + +client_script 'client.lua' +server_script 'server.lua' + +shared_script 'mapdata.lua' + +dependencies { + 'mapmanager', + 'money' +} + +lua54 'yes' \ No newline at end of file diff --git a/resources/[gameplay]/[examples]/money-fountain/mapdata.lua b/resources/[gameplay]/[examples]/money-fountain/mapdata.lua new file mode 100644 index 0000000..4eecad8 --- /dev/null +++ b/resources/[gameplay]/[examples]/money-fountain/mapdata.lua @@ -0,0 +1,28 @@ +-- define the money fountain list (SHARED SCRIPT) +moneyFountains = {} + +-- index to know what to remove +local fountainIdx = 1 + +AddEventHandler('getMapDirectives', function(add) + -- add a 'money_fountain' map directive + add('money_fountain', function(state, name) + return function(data) + local coords = data[1] + local amount = data.amount or 100 + + local idx = fountainIdx + fountainIdx += 1 + + moneyFountains[idx] = { + id = name, + coords = coords, + amount = amount + } + + state.add('idx', idx) + end + end, function(state) + moneyFountains[state.idx] = nil + end) +end) \ No newline at end of file diff --git a/resources/[gameplay]/[examples]/money-fountain/server.lua b/resources/[gameplay]/[examples]/money-fountain/server.lua new file mode 100644 index 0000000..9b1e238 --- /dev/null +++ b/resources/[gameplay]/[examples]/money-fountain/server.lua @@ -0,0 +1,107 @@ +-- track down what we've added to global state +local sentState = {} + +-- money system +local ms = exports['money'] + +-- get the fountain content from storage +local function getMoneyForId(fountainId) + return GetResourceKvpInt(('money:%s'):format(fountainId)) / 100.0 +end + +-- set the fountain content in storage + state +local function setMoneyForId(fountainId, money) + GlobalState['fountain_' .. fountainId] = math.tointeger(money) + + return SetResourceKvpInt(('money:%s'):format(fountainId), math.tointeger(money * 100.0)) +end + +-- get the nearest fountain to the player + ID +local function getMoneyFountain(id, source) + local coords = GetEntityCoords(GetPlayerPed(source)) + + for _, v in pairs(moneyFountains) do + if v.id == id then + if #(v.coords - coords) < 2.5 then + return v + end + end + end + + return nil +end + +-- generic function for events +local function handleFountainStuff(source, id, pickup) + -- if near the fountain we specify + local fountain = getMoneyFountain(id, source) + + if fountain then + -- and we can actually use the fountain already + local player = Player(source) + + local nextUse = player.state['fountain_nextUse'] + if not nextUse then + nextUse = 0 + end + + -- GetGameTimer ~ GetNetworkTime on client + if nextUse <= GetGameTimer() then + -- not rate limited + local success = false + local money = getMoneyForId(fountain.id) + + -- decide the op + if pickup then + -- if the fountain is rich enough to get the per-use amount + if money >= fountain.amount then + -- give the player money + if ms:addMoney(source, 'cash', fountain.amount) then + money -= fountain.amount + success = true + end + end + else + -- if the player is rich enough + if ms:removeMoney(source, 'cash', fountain.amount) then + -- add to the fountain + money += fountain.amount + success = true + end + end + + -- save it and set the player's cooldown + if success then + setMoneyForId(fountain.id, money) + player.state['fountain_nextUse'] = GetGameTimer() + GetConvarInt('moneyFountain_cooldown', 5000) + end + end + end +end + +-- event for picking up fountain->player +RegisterNetEvent('money_fountain:tryPickup') +AddEventHandler('money_fountain:tryPickup', function(id) + handleFountainStuff(source, id, true) +end) + +-- event for donating player->fountain +RegisterNetEvent('money_fountain:tryPlace') +AddEventHandler('money_fountain:tryPlace', function(id) + handleFountainStuff(source, id, false) +end) + +-- listener: if a new fountain is added, set its current money in state +CreateThread(function() + while true do + Wait(500) + + for _, fountain in pairs(moneyFountains) do + if not sentState[fountain.id] then + GlobalState['fountain_' .. fountain.id] = math.tointeger(getMoneyForId(fountain.id)) + + sentState[fountain.id] = true + end + end + end +end) \ No newline at end of file diff --git a/resources/[gameplay]/[examples]/money/client.lua b/resources/[gameplay]/[examples]/money/client.lua new file mode 100644 index 0000000..e094dd1 --- /dev/null +++ b/resources/[gameplay]/[examples]/money/client.lua @@ -0,0 +1,30 @@ +local moneyTypes = { + cash = `MP0_WALLET_BALANCE`, + bank = `BANK_BALANCE`, +} + +RegisterNetEvent('money:displayUpdate') + +AddEventHandler('money:displayUpdate', function(type, money) + local stat = moneyTypes[type] + if not stat then return end + StatSetInt(stat, math.floor(money)) +end) + +TriggerServerEvent('money:requestDisplay') + +CreateThread(function() + while true do + Wait(0) + + if IsControlJustPressed(0, 20) then + SetMultiplayerBankCash() + SetMultiplayerWalletCash() + + Wait(4350) + + RemoveMultiplayerBankCash() + RemoveMultiplayerWalletCash() + end + end +end) \ No newline at end of file diff --git a/resources/[gameplay]/[examples]/money/fxmanifest.lua b/resources/[gameplay]/[examples]/money/fxmanifest.lua new file mode 100644 index 0000000..8dd5ed7 --- /dev/null +++ b/resources/[gameplay]/[examples]/money/fxmanifest.lua @@ -0,0 +1,12 @@ +version '1.0.0' +description 'An example money system using KVS.' +author 'Cfx.re ' + +fx_version 'bodacious' +game 'gta5' + +client_script 'client.lua' +server_script 'server.lua' + +--dependency 'cfx.re/playerData.v1alpha1' +lua54 'yes' \ No newline at end of file diff --git a/resources/[gameplay]/[examples]/money/server.lua b/resources/[gameplay]/[examples]/money/server.lua new file mode 100644 index 0000000..2b2ce96 --- /dev/null +++ b/resources/[gameplay]/[examples]/money/server.lua @@ -0,0 +1,119 @@ +local playerData = exports['cfx.re/playerData.v1alpha1'] + +local validMoneyTypes = { + bank = true, + cash = true, +} + +local function getMoneyForId(playerId, moneyType) + return GetResourceKvpInt(('money:%s:%s'):format(playerId, moneyType)) / 100.0 +end + +local function setMoneyForId(playerId, moneyType, money) + local s = playerData:getPlayerById(playerId) + + TriggerEvent('money:updated', { + dbId = playerId, + source = s, + moneyType = moneyType, + money = money + }) + + return SetResourceKvpInt(('money:%s:%s'):format(playerId, moneyType), math.tointeger(money * 100.0)) +end + +local function addMoneyForId(playerId, moneyType, amount) + local curMoney = getMoneyForId(playerId, moneyType) + curMoney += amount + + if curMoney >= 0 then + setMoneyForId(playerId, moneyType, curMoney) + return true, curMoney + end + + return false, 0 +end + +exports('addMoney', function(playerIdx, moneyType, amount) + amount = tonumber(amount) + + if amount <= 0 or amount > (1 << 30) then + return false + end + + if not validMoneyTypes[moneyType] then + return false + end + + local playerId = playerData:getPlayerId(playerIdx) + local success, money = addMoneyForId(playerId, moneyType, amount) + + if success then + Player(playerIdx).state['money_' .. moneyType] = money + end + + return true +end) + +exports('removeMoney', function(playerIdx, moneyType, amount) + amount = tonumber(amount) + + if amount <= 0 or amount > (1 << 30) then + return false + end + + if not validMoneyTypes[moneyType] then + return false + end + + local playerId = playerData:getPlayerId(playerIdx) + local success, money = addMoneyForId(playerId, moneyType, -amount) + + if success then + Player(playerIdx).state['money_' .. moneyType] = money + end + + return success +end) + +exports('getMoney', function(playerIdx, moneyType) + local playerId = playerData:getPlayerId(playerIdx) + return getMoneyForId(playerId, moneyType) +end) + +-- player display bits +AddEventHandler('money:updated', function(data) + if data.source then + TriggerClientEvent('money:displayUpdate', data.source, data.moneyType, data.money) + end +end) + +RegisterNetEvent('money:requestDisplay') + +AddEventHandler('money:requestDisplay', function() + local source = source + local playerId = playerData:getPlayerId(source) + + for type, _ in pairs(validMoneyTypes) do + local amount = getMoneyForId(playerId, type) + TriggerClientEvent('money:displayUpdate', source, type, amount) + + Player(source).state['money_' .. type] = amount + end +end) + +RegisterCommand('earn', function(source, args) + local type = args[1] + local amount = tonumber(args[2]) + + exports['money']:addMoney(source, type, amount) +end, true) + +RegisterCommand('spend', function(source, args) + local type = args[1] + local amount = tonumber(args[2]) + + if not exports['money']:removeMoney(source, type, amount) then + print('you are broke??') + end +end, true) \ No newline at end of file diff --git a/resources/[gameplay]/[examples]/ped-money-drops/client.lua b/resources/[gameplay]/[examples]/ped-money-drops/client.lua new file mode 100644 index 0000000..d6d000c --- /dev/null +++ b/resources/[gameplay]/[examples]/ped-money-drops/client.lua @@ -0,0 +1,41 @@ +AddEventHandler('gameEventTriggered', function(eventName, args) + if eventName == 'CEventNetworkEntityDamage' then + local victim = args[1] + local culprit = args[2] + local isDead = args[4] == 1 + + if isDead then + local origCoords = GetEntityCoords(victim) + local pickup = CreatePickupRotate(`PICKUP_MONEY_VARIABLE`, origCoords.x, origCoords.y, origCoords.z - 0.7, 0.0, 0.0, 0.0, 512, 0, false, 0) + local netId = PedToNet(victim) + + local undoStuff = { false } + + CreateThread(function() + local self = PlayerPedId() + + while not undoStuff[1] do + Wait(50) + + if #(GetEntityCoords(self) - origCoords) < 2.5 and HasPickupBeenCollected(pickup) then + TriggerServerEvent('money:tryPickup', netId) + + RemovePickup(pickup) + break + end + end + + undoStuff[1] = true + end) + + SetTimeout(15000, function() + if not undoStuff[1] then + RemovePickup(pickup) + undoStuff[1] = true + end + end) + + TriggerServerEvent('money:allowPickupNear', netId) + end + end +end) \ No newline at end of file diff --git a/resources/[gameplay]/[examples]/ped-money-drops/fxmanifest.lua b/resources/[gameplay]/[examples]/ped-money-drops/fxmanifest.lua new file mode 100644 index 0000000..f338973 --- /dev/null +++ b/resources/[gameplay]/[examples]/ped-money-drops/fxmanifest.lua @@ -0,0 +1,11 @@ +version '1.0.0' +description 'An example money system client.' +author 'Cfx.re ' + +fx_version 'bodacious' +game 'gta5' + +client_script 'client.lua' +server_script 'server.lua' + +lua54 'yes' diff --git a/resources/[gameplay]/[examples]/ped-money-drops/server.lua b/resources/[gameplay]/[examples]/ped-money-drops/server.lua new file mode 100644 index 0000000..6876952 --- /dev/null +++ b/resources/[gameplay]/[examples]/ped-money-drops/server.lua @@ -0,0 +1,42 @@ +local safePositions = {} + +RegisterNetEvent('money:allowPickupNear') + +AddEventHandler('money:allowPickupNear', function(pedId) + local entity = NetworkGetEntityFromNetworkId(pedId) + + Wait(250) + + if not DoesEntityExist(entity) then + return + end + + if GetEntityHealth(entity) > 100 then + return + end + + local coords = GetEntityCoords(entity) + safePositions[pedId] = coords +end) + +RegisterNetEvent('money:tryPickup') + +AddEventHandler('money:tryPickup', function(entity) + if not safePositions[entity] then + return + end + + local source = source + local playerPed = GetPlayerPed(source) + local coords = GetEntityCoords(playerPed) + + if #(safePositions[entity] - coords) < 2.5 then + exports['money']:addMoney(source, 'cash', 40) + end + + safePositions[entity] = nil +end) + +AddEventHandler('entityRemoved', function(entity) + safePositions[entity] = nil +end) \ No newline at end of file diff --git a/resources/[gameplay]/chat-theme-gtao/style.css b/resources/[gameplay]/chat-theme-gtao/style.css index c2c0fa3..5e28f97 100644 --- a/resources/[gameplay]/chat-theme-gtao/style.css +++ b/resources/[gameplay]/chat-theme-gtao/style.css @@ -85,7 +85,9 @@ } .chat-input > div { - background-color: rgba(0, 0, 0, .6); + background-color: rgba(0, 0, 0, .6) !important; + border: calc(0.28vh / 2) solid rgba(180, 180, 180, .6); + outline: calc(0.28vh / 2) solid rgba(0, 0, 0, .8); /* to replace margin-background */ padding: calc(0.28vh / 2); } @@ -93,6 +95,19 @@ margin: 0; margin-left: 0.7%; margin-top: -0.1%; + line-height: 2.8vh; +} + +.chat-input .prefix.any { + opacity: 0.8; +} + +.chat-input .prefix.any:before { + content: '['; +} + +.chat-input .prefix.any:after { + content: ']'; } .chat-input > div + div { @@ -110,9 +125,7 @@ textarea { background: transparent; - border: calc(0.28vh / 2) solid rgba(180, 180, 180, .6); - padding: calc(0.28vh / 2); - padding-left: calc(3.5% + (0.28vh / 2)); + padding: 0.5vh; } @media screen and (min-aspect-ratio: 21/9) { diff --git a/resources/[gameplay]/chat/.gitignore b/resources/[gameplay]/chat/.gitignore new file mode 100644 index 0000000..3e0ed55 --- /dev/null +++ b/resources/[gameplay]/chat/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +.yarn.installed +yarn-error.log +dist/ \ No newline at end of file diff --git a/resources/[gameplay]/chat/cl_chat.lua b/resources/[gameplay]/chat/cl_chat.lua index f42d441..b0ad88e 100644 --- a/resources/[gameplay]/chat/cl_chat.lua +++ b/resources/[gameplay]/chat/cl_chat.lua @@ -2,7 +2,6 @@ local isRDR = not TerraingridActivate and true or false local chatInputActive = false local chatInputActivating = false -local chatHidden = true local chatLoaded = false RegisterNetEvent('chatMessage') @@ -10,6 +9,8 @@ RegisterNetEvent('chat:addTemplate') RegisterNetEvent('chat:addMessage') RegisterNetEvent('chat:addSuggestion') RegisterNetEvent('chat:addSuggestions') +RegisterNetEvent('chat:addMode') +RegisterNetEvent('chat:removeMode') RegisterNetEvent('chat:removeSuggestion') RegisterNetEvent('chat:clear') @@ -42,19 +43,31 @@ AddEventHandler('__cfx_internal:serverPrint', function(msg) message = { templateId = 'print', multiline = true, - args = { msg } + args = { msg }, + mode = '_global' } }) end) -AddEventHandler('chat:addMessage', function(message) +-- addMessage +local addMessage = function(message) + if type(message) == 'string' then + message = { + args = { message } + } + end + SendNUIMessage({ type = 'ON_MESSAGE', message = message }) -end) +end -AddEventHandler('chat:addSuggestion', function(name, help, params) +exports('addMessage', addMessage) +AddEventHandler('chat:addMessage', addMessage) + +-- addSuggestion +local addSuggestion = function(name, help, params) SendNUIMessage({ type = 'ON_SUGGESTION_ADD', suggestion = { @@ -63,7 +76,10 @@ AddEventHandler('chat:addSuggestion', function(name, help, params) params = params or nil } }) -end) +end + +exports('addSuggestion', addSuggestion) +AddEventHandler('chat:addSuggestion', addSuggestion) AddEventHandler('chat:addSuggestions', function(suggestions) for _, suggestion in ipairs(suggestions) do @@ -81,6 +97,20 @@ AddEventHandler('chat:removeSuggestion', function(name) }) end) +AddEventHandler('chat:addMode', function(mode) + SendNUIMessage({ + type = 'ON_MODE_ADD', + mode = mode + }) +end) + +AddEventHandler('chat:removeMode', function(name) + SendNUIMessage({ + type = 'ON_MODE_REMOVE', + name = name + }) +end) + AddEventHandler('chat:addTemplate', function(id, html) SendNUIMessage({ type = 'ON_TEMPLATE_ADD', @@ -110,7 +140,7 @@ RegisterNUICallback('chatResult', function(data, cb) if data.message:sub(1, 1) == '/' then ExecuteCommand(data.message:sub(2)) else - TriggerServerEvent('_chat:messageEntered', GetPlayerName(id), { r, g, b }, data.message) + TriggerServerEvent('_chat:messageEntered', GetPlayerName(id), { r, g, b }, data.message, data.mode) end end @@ -124,7 +154,7 @@ local function refreshCommands() local suggestions = {} for _, command in ipairs(registeredCommands) do - if IsAceAllowed(('command.%s'):format(command.name)) then + if IsAceAllowed(('command.%s'):format(command.name)) and command.name ~= 'toggleChat' then table.insert(suggestions, { name = '/' .. command.name, help = '' @@ -178,7 +208,7 @@ AddEventHandler('onClientResourceStop', function(resName) end) RegisterNUICallback('loaded', function(data, cb) - TriggerServerEvent('chat:init'); + TriggerServerEvent('chat:init') refreshCommands() refreshThemes() @@ -188,10 +218,43 @@ RegisterNUICallback('loaded', function(data, cb) cb('ok') end) +local CHAT_HIDE_STATES = { + SHOW_WHEN_ACTIVE = 0, + ALWAYS_SHOW = 1, + ALWAYS_HIDE = 2 +} + +local kvpEntry = GetResourceKvpString('hideState') +local chatHideState = kvpEntry and tonumber(kvpEntry) or CHAT_HIDE_STATES.SHOW_WHEN_ACTIVE +local isFirstHide = true + +if not isRDR then + if RegisterKeyMapping then + RegisterKeyMapping('toggleChat', 'Toggle chat', 'keyboard', 'l') + end + + RegisterCommand('toggleChat', function() + if chatHideState == CHAT_HIDE_STATES.SHOW_WHEN_ACTIVE then + chatHideState = CHAT_HIDE_STATES.ALWAYS_SHOW + elseif chatHideState == CHAT_HIDE_STATES.ALWAYS_SHOW then + chatHideState = CHAT_HIDE_STATES.ALWAYS_HIDE + elseif chatHideState == CHAT_HIDE_STATES.ALWAYS_HIDE then + chatHideState = CHAT_HIDE_STATES.SHOW_WHEN_ACTIVE + end + + isFirstHide = false + + SetResourceKvp('hideState', tostring(chatHideState)) + end, false) +end + Citizen.CreateThread(function() SetTextChatEnabled(false) SetNuiFocus(false) + local lastChatHideState = -1 + local origChatHideState = -1 + while true do Wait(0) @@ -215,19 +278,30 @@ Citizen.CreateThread(function() end if chatLoaded then - local shouldBeHidden = false + local forceHide = IsScreenFadedOut() or IsPauseMenuActive() + local wasForceHide = false - if IsScreenFadedOut() or IsPauseMenuActive() then - shouldBeHidden = true + if chatHideState ~= CHAT_HIDE_STATES.ALWAYS_HIDE then + if forceHide then + origChatHideState = chatHideState + chatHideState = CHAT_HIDE_STATES.ALWAYS_HIDE + end + elseif not forceHide and origChatHideState ~= -1 then + chatHideState = origChatHideState + origChatHideState = -1 + wasForceHide = true end - if (shouldBeHidden and not chatHidden) or (not shouldBeHidden and chatHidden) then - chatHidden = shouldBeHidden + if chatHideState ~= lastChatHideState then + lastChatHideState = chatHideState SendNUIMessage({ type = 'ON_SCREEN_STATE_CHANGE', - shouldHide = shouldBeHidden + hideState = chatHideState, + fromUserInteraction = not forceHide and not isFirstHide and not wasForceHide }) + + isFirstHide = false end end end diff --git a/resources/[gameplay]/chat/fxmanifest.lua b/resources/[gameplay]/chat/fxmanifest.lua index 14c61bd..a2d58dd 100644 --- a/resources/[gameplay]/chat/fxmanifest.lua +++ b/resources/[gameplay]/chat/fxmanifest.lua @@ -1,30 +1,24 @@ description 'chat management stuff' -ui_page 'html/index.html' +ui_page 'dist/ui.html' client_script 'cl_chat.lua' server_script 'sv_chat.lua' files { - 'html/index.html', - 'html/index.css', - 'html/config.default.js', - 'html/config.js', - 'html/App.js', - 'html/Message.js', - 'html/Suggestions.js', - 'html/vendor/vue.2.3.3.min.js', - 'html/vendor/flexboxgrid.6.3.1.min.css', - 'html/vendor/animate.3.5.2.min.css', - 'html/vendor/latofonts.css', - 'html/vendor/fonts/LatoRegular.woff2', - 'html/vendor/fonts/LatoRegular2.woff2', - 'html/vendor/fonts/LatoLight2.woff2', - 'html/vendor/fonts/LatoLight.woff2', - 'html/vendor/fonts/LatoBold.woff2', - 'html/vendor/fonts/LatoBold2.woff2', - } + 'dist/ui.html', + 'dist/index.css', + 'html/vendor/*.css', + 'html/vendor/fonts/*.woff2', +} fx_version 'adamant' games { 'rdr3', 'gta5' } -rdr3_warning 'I acknowledge that this is a prerelease build of RedM, and I am aware my resources *will* become incompatible once RedM ships.' \ No newline at end of file +rdr3_warning 'I acknowledge that this is a prerelease build of RedM, and I am aware my resources *will* become incompatible once RedM ships.' + +dependencies { + 'yarn', + 'webpack' +} + +webpack_config 'webpack.config.js' \ No newline at end of file diff --git a/resources/[gameplay]/chat/html/App.js b/resources/[gameplay]/chat/html/App.js deleted file mode 100644 index 6e54d2f..0000000 --- a/resources/[gameplay]/chat/html/App.js +++ /dev/null @@ -1,255 +0,0 @@ -window.APP = { - template: '#app_template', - name: 'app', - data() { - return { - style: CONFIG.style, - showInput: false, - showWindow: false, - shouldHide: true, - backingSuggestions: [], - removedSuggestions: [], - templates: CONFIG.templates, - message: '', - messages: [], - oldMessages: [], - oldMessagesIndex: -1, - tplBackups: [], - msgTplBackups: [] - }; - }, - destroyed() { - clearInterval(this.focusTimer); - window.removeEventListener('message', this.listener); - }, - mounted() { - post('http://chat/loaded', JSON.stringify({})); - this.listener = window.addEventListener('message', (event) => { - const item = event.data || event.detail; //'detail' is for debuging via browsers - if (this[item.type]) { - this[item.type](item); - } - }); - }, - watch: { - messages() { - if (this.showWindowTimer) { - clearTimeout(this.showWindowTimer); - } - this.showWindow = true; - this.resetShowWindowTimer(); - - const messagesObj = this.$refs.messages; - this.$nextTick(() => { - messagesObj.scrollTop = messagesObj.scrollHeight; - }); - }, - }, - computed: { - suggestions() { - return this.backingSuggestions.filter((el) => this.removedSuggestions.indexOf(el.name) <= -1); - }, - }, - methods: { - ON_SCREEN_STATE_CHANGE({ shouldHide }) { - this.shouldHide = shouldHide; - }, - ON_OPEN() { - this.showInput = true; - this.showWindow = true; - if (this.showWindowTimer) { - clearTimeout(this.showWindowTimer); - } - this.focusTimer = setInterval(() => { - if (this.$refs.input) { - this.$refs.input.focus(); - } else { - clearInterval(this.focusTimer); - } - }, 100); - }, - ON_MESSAGE({ message }) { - this.messages.push(message); - }, - ON_CLEAR() { - this.messages = []; - this.oldMessages = []; - this.oldMessagesIndex = -1; - }, - ON_SUGGESTION_ADD({ suggestion }) { - const duplicateSuggestion = this.backingSuggestions.find(a => a.name == suggestion.name); - if (duplicateSuggestion) { - if(suggestion.help || suggestion.params) { - duplicateSuggestion.help = suggestion.help || ""; - duplicateSuggestion.params = suggestion.params || []; - } - return; - } - if (!suggestion.params) { - suggestion.params = []; //TODO Move somewhere else - } - this.backingSuggestions.push(suggestion); - }, - ON_SUGGESTION_REMOVE({ name }) { - if(this.removedSuggestions.indexOf(name) <= -1) { - this.removedSuggestions.push(name); - } - }, - ON_TEMPLATE_ADD({ template }) { - if (this.templates[template.id]) { - this.warn(`Tried to add duplicate template '${template.id}'`) - } else { - this.templates[template.id] = template.html; - } - }, - ON_UPDATE_THEMES({ themes }) { - this.removeThemes(); - - this.setThemes(themes); - }, - removeThemes() { - for (let i = 0; i < document.styleSheets.length; i++) { - const styleSheet = document.styleSheets[i]; - const node = styleSheet.ownerNode; - - if (node.getAttribute('data-theme')) { - node.parentNode.removeChild(node); - } - } - - this.tplBackups.reverse(); - - for (const [ elem, oldData ] of this.tplBackups) { - elem.innerText = oldData; - } - - this.tplBackups = []; - - this.msgTplBackups.reverse(); - - for (const [ id, oldData ] of this.msgTplBackups) { - this.templates[id] = oldData; - } - - this.msgTplBackups = []; - }, - setThemes(themes) { - for (const [ id, data ] of Object.entries(themes)) { - if (data.style) { - const style = document.createElement('style'); - style.type = 'text/css'; - style.setAttribute('data-theme', id); - style.appendChild(document.createTextNode(data.style)); - - document.head.appendChild(style); - } - - if (data.styleSheet) { - const link = document.createElement('link'); - link.rel = 'stylesheet'; - link.type = 'text/css'; - link.href = data.baseUrl + data.styleSheet; - link.setAttribute('data-theme', id); - - document.head.appendChild(link); - } - - if (data.templates) { - for (const [ tplId, tpl ] of Object.entries(data.templates)) { - const elem = document.getElementById(tplId); - - if (elem) { - this.tplBackups.push([ elem, elem.innerText ]); - elem.innerText = tpl; - } - } - } - - if (data.script) { - const script = document.createElement('script'); - script.type = 'text/javascript'; - script.src = data.baseUrl + data.script; - - document.head.appendChild(script); - } - - if (data.msgTemplates) { - for (const [ tplId, tpl ] of Object.entries(data.msgTemplates)) { - this.msgTplBackups.push([ tplId, this.templates[tplId] ]); - this.templates[tplId] = tpl; - } - } - } - }, - warn(msg) { - this.messages.push({ - args: [msg], - template: '^3CHAT-WARN: ^0{0}', - }); - }, - clearShowWindowTimer() { - clearTimeout(this.showWindowTimer); - }, - resetShowWindowTimer() { - this.clearShowWindowTimer(); - this.showWindowTimer = setTimeout(() => { - if (!this.showInput) { - this.showWindow = false; - } - }, CONFIG.fadeTimeout); - }, - keyUp() { - this.resize(); - }, - keyDown(e) { - if (e.which === 38 || e.which === 40) { - e.preventDefault(); - this.moveOldMessageIndex(e.which === 38); - } else if (e.which == 33) { - var buf = document.getElementsByClassName('chat-messages')[0]; - buf.scrollTop = buf.scrollTop - 100; - } else if (e.which == 34) { - var buf = document.getElementsByClassName('chat-messages')[0]; - buf.scrollTop = buf.scrollTop + 100; - } - }, - moveOldMessageIndex(up) { - if (up && this.oldMessages.length > this.oldMessagesIndex + 1) { - this.oldMessagesIndex += 1; - this.message = this.oldMessages[this.oldMessagesIndex]; - } else if (!up && this.oldMessagesIndex - 1 >= 0) { - this.oldMessagesIndex -= 1; - this.message = this.oldMessages[this.oldMessagesIndex]; - } else if (!up && this.oldMessagesIndex - 1 === -1) { - this.oldMessagesIndex = -1; - this.message = ''; - } - }, - resize() { - const input = this.$refs.input; - input.style.height = '5px'; - input.style.height = `${input.scrollHeight + 2}px`; - }, - send(e) { - if(this.message !== '') { - post('http://chat/chatResult', JSON.stringify({ - message: this.message, - })); - this.oldMessages.unshift(this.message); - this.oldMessagesIndex = -1; - this.hideInput(); - } else { - this.hideInput(true); - } - }, - hideInput(canceled = false) { - if (canceled) { - post('http://chat/chatResult', JSON.stringify({ canceled })); - } - this.message = ''; - this.showInput = false; - clearInterval(this.focusTimer); - this.resetShowWindowTimer(); - }, - }, -}; diff --git a/resources/[gameplay]/chat/html/App.ts b/resources/[gameplay]/chat/html/App.ts new file mode 100644 index 0000000..03b01ac --- /dev/null +++ b/resources/[gameplay]/chat/html/App.ts @@ -0,0 +1,463 @@ +import { post } from './utils'; +import CONFIG from './config'; +import Vue from 'vue'; + +import Suggestions from './Suggestions.vue'; +import MessageV from './Message.vue'; +import { Suggestion } from './Suggestions'; + +export interface Message { + args: string[]; + template: string; + params?: { [key: string]: string }; + multiline?: boolean; + color?: [ number, number, number ]; + templateId?: number; + mode?: string; + modeData?: Mode; + + id?: string; +} + +export interface ThemeData { + style: string; + styleSheet: string; + baseUrl: string; + script: string; + templates: { [id: string]: string }; // not supported rn + msgTemplates: { [id: string]: string }; + +} + +export interface Mode { + name: string; + displayName: string; + color: string; + hidden?: boolean; + isChannel?: boolean; + isGlobal?: boolean; +} + +enum ChatHideStates { + ShowWhenActive = 0, + AlwaysShow = 1, + AlwaysHide = 2, +} + +const defaultMode: Mode = { + name: 'all', + displayName: 'All', + color: '#fff' +}; + +const globalMode: Mode = { + name: '_global', + displayName: 'All', + color: '#fff', + isGlobal: true, + hidden: true +}; + +export default Vue.extend({ + template: "#app_template", + name: "app", + components: { + Suggestions, + MessageV + }, + data() { + return { + style: CONFIG.style, + showInput: false, + showWindow: false, + showHideState: false, + hideState: ChatHideStates.ShowWhenActive, + backingSuggestions: [] as Suggestion[], + removedSuggestions: [] as string[], + templates: { ...CONFIG.templates } as { [ key: string ]: string }, + message: "", + messages: [] as Message[], + oldMessages: [] as string[], + oldMessagesIndex: -1, + tplBackups: [] as unknown as [ HTMLElement, string ][], + msgTplBackups: [] as unknown as [ string, string ][], + focusTimer: 0, + showWindowTimer: 0, + showHideStateTimer: 0, + listener: (event: MessageEvent) => {}, + modes: [defaultMode, globalMode] as Mode[], + modeIdx: 0, + }; + }, + destroyed() { + clearInterval(this.focusTimer); + window.removeEventListener("message", this.listener); + }, + mounted() { + post("http://chat/loaded", JSON.stringify({})); + + this.listener = (event: MessageEvent) => { + const item: any = event.data || (event).detail; //'detail' is for debugging via browsers + + if (!item || !item.type) { + return; + } + + const typeRef = item.type as + 'ON_OPEN' | 'ON_SCREEN_STATE_CHANGE' | 'ON_MESSAGE' | 'ON_CLEAR' | 'ON_SUGGESTION_ADD' | + 'ON_SUGGESTION_REMOVE' | 'ON_TEMPLATE_ADD' | 'ON_UPDATE_THEMES' | 'ON_MODE_ADD' | 'ON_MODE_REMOVE'; + + if (this[typeRef]) { + this[typeRef](item); + } + }; + + window.addEventListener("message", this.listener); + }, + watch: { + messages() { + if (this.hideState !== ChatHideStates.AlwaysHide) { + if (this.showWindowTimer) { + clearTimeout(this.showWindowTimer); + } + this.showWindow = true; + this.resetShowWindowTimer(); + } + + const messagesObj = this.$refs.messages as HTMLDivElement; + this.$nextTick(() => { + messagesObj.scrollTop = messagesObj.scrollHeight; + }); + } + }, + computed: { + filteredMessages(): Message[] { + return this.messages.filter( + // show messages that are + // - (if the current mode is a channel) global, or in the current mode + // - (if the message is a channel) in the current mode + el => (el.modeData?.isChannel || this.modes[this.modeIdx].isChannel) ? + (el.mode === this.modes[this.modeIdx].name || el.modeData?.isGlobal) : + true + ); + }, + + suggestions(): Suggestion[] { + return this.backingSuggestions.filter( + el => this.removedSuggestions.indexOf(el.name) <= -1 + ); + }, + + hideAnimated(): boolean { + return this.hideState !== ChatHideStates.AlwaysHide; + }, + + modeIdxGet(): number { + return (this.modeIdx >= this.modes.length) ? (this.modes.length - 1) : this.modeIdx; + }, + + modePrefix(): string { + if (this.modes.length === 2) { + return `➤`; + } + + return this.modes[this.modeIdxGet].displayName; + }, + + modeColor(): string { + return this.modes[this.modeIdxGet].color; + }, + + hideStateString(): string { + // TODO: localization + switch (this.hideState) { + case ChatHideStates.AlwaysShow: + return 'Visible'; + case ChatHideStates.AlwaysHide: + return 'Hidden'; + case ChatHideStates.ShowWhenActive: + return 'When active'; + } + } + }, + methods: { + ON_SCREEN_STATE_CHANGE({ hideState, fromUserInteraction }: { hideState: ChatHideStates, fromUserInteraction: boolean }) { + this.hideState = hideState; + + if (this.hideState === ChatHideStates.AlwaysHide) { + if (!this.showInput) { + this.showWindow = false; + } + } else if (this.hideState === ChatHideStates.AlwaysShow) { + this.showWindow = true; + if (this.showWindowTimer) { + clearTimeout(this.showWindowTimer); + } + } else { + this.resetShowWindowTimer(); + } + + if (fromUserInteraction) { + this.showHideState = true; + + if (this.showHideStateTimer) { + clearTimeout(this.showHideStateTimer); + } + + this.showHideStateTimer = window.setTimeout(() => { + this.showHideState = false; + }, 1500); + } + }, + ON_OPEN() { + this.showInput = true; + this.showWindow = true; + if (this.showWindowTimer) { + clearTimeout(this.showWindowTimer); + } + this.focusTimer = window.setInterval(() => { + if (this.$refs.input) { + (this.$refs.input as HTMLInputElement).focus(); + } else { + clearInterval(this.focusTimer); + } + }, 100); + }, + ON_MESSAGE({ message }: { message: Message }) { + message.id = `${new Date().getTime()}${Math.random()}`; + message.modeData = this.modes.find(mode => mode.name === message.mode); + this.messages.push(message); + }, + ON_CLEAR() { + this.messages = []; + this.oldMessages = []; + this.oldMessagesIndex = -1; + }, + ON_SUGGESTION_ADD({ suggestion }: { suggestion: Suggestion }) { + const duplicateSuggestion = this.backingSuggestions.find( + a => a.name == suggestion.name + ); + if (duplicateSuggestion) { + if (suggestion.help || suggestion.params) { + duplicateSuggestion.help = suggestion.help || ""; + duplicateSuggestion.params = suggestion.params || []; + } + return; + } + if (!suggestion.params) { + suggestion.params = []; //TODO Move somewhere else + } + this.backingSuggestions.push(suggestion); + }, + ON_SUGGESTION_REMOVE({ name }: { name: string }) { + if (this.removedSuggestions.indexOf(name) <= -1) { + this.removedSuggestions.push(name); + } + }, + ON_MODE_ADD({ mode }: { mode: Mode }) { + this.modes = [ + ...this.modes.filter(a => a.name !== mode.name), + mode + ]; + }, + ON_MODE_REMOVE({ name }: { name: string }) { + this.modes = this.modes.filter(a => a.name !== name); + + if (this.modes.length === 0) { + this.modes = [defaultMode]; + } + }, + ON_TEMPLATE_ADD({ template }: { template: { id: string, html: string }}) { + if (this.templates[template.id]) { + this.warn(`Tried to add duplicate template '${template.id}'`); + } else { + this.templates[template.id] = template.html; + } + }, + ON_UPDATE_THEMES({ themes }: { themes: { [key: string]: ThemeData } }) { + this.removeThemes(); + + this.setThemes(themes); + }, + removeThemes() { + for (let i = 0; i < document.styleSheets.length; i++) { + const styleSheet = document.styleSheets[i]; + const node = styleSheet.ownerNode as Element; + + if (node.getAttribute("data-theme")) { + node.parentNode?.removeChild(node); + } + } + + this.tplBackups.reverse(); + + for (const [elem, oldData] of this.tplBackups) { + elem.innerText = oldData; + } + + this.tplBackups = []; + + this.msgTplBackups.reverse(); + + for (const [id, oldData] of this.msgTplBackups) { + this.templates[id] = oldData; + } + + this.msgTplBackups = []; + }, + setThemes(themes: { [key: string]: ThemeData }) { + for (const [id, data] of Object.entries(themes)) { + if (data.style) { + const style = document.createElement("style"); + style.type = "text/css"; + style.setAttribute("data-theme", id); + style.appendChild(document.createTextNode(data.style)); + + document.head.appendChild(style); + } + + if (data.styleSheet) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.type = "text/css"; + link.href = data.baseUrl + data.styleSheet; + link.setAttribute("data-theme", id); + + document.head.appendChild(link); + } + + if (data.templates) { + for (const [tplId, tpl] of Object.entries(data.templates)) { + const elem = document.getElementById(tplId); + + if (elem) { + this.tplBackups.push([elem, elem.innerText]); + elem.innerText = tpl; + } + } + } + + if (data.script) { + const script = document.createElement("script"); + script.type = "text/javascript"; + script.src = data.baseUrl + data.script; + + document.head.appendChild(script); + } + + if (data.msgTemplates) { + for (const [tplId, tpl] of Object.entries(data.msgTemplates)) { + this.msgTplBackups.push([tplId, this.templates[tplId]]); + this.templates[tplId] = tpl; + } + } + } + }, + warn(msg: string) { + this.messages.push({ + args: [msg], + template: "^3CHAT-WARN: ^0{0}" + }); + }, + clearShowWindowTimer() { + clearTimeout(this.showWindowTimer); + }, + resetShowWindowTimer() { + this.clearShowWindowTimer(); + this.showWindowTimer = window.setTimeout(() => { + if (this.hideState !== ChatHideStates.AlwaysShow && !this.showInput) { + this.showWindow = false; + } + }, CONFIG.fadeTimeout); + }, + keyUp() { + this.resize(); + }, + keyDown(e: KeyboardEvent) { + if (e.which === 38 || e.which === 40) { + e.preventDefault(); + this.moveOldMessageIndex(e.which === 38); + } else if (e.which == 33) { + var buf = document.getElementsByClassName("chat-messages")[0]; + buf.scrollTop = buf.scrollTop - 100; + } else if (e.which == 34) { + var buf = document.getElementsByClassName("chat-messages")[0]; + buf.scrollTop = buf.scrollTop + 100; + } else if (e.which === 9) { // tab + if (e.shiftKey || e.altKey) { + do { + --this.modeIdx; + + if (this.modeIdx < 0) { + this.modeIdx = this.modes.length - 1; + } + } while (this.modes[this.modeIdx].hidden); + } else { + do { + this.modeIdx = (this.modeIdx + 1) % this.modes.length; + } while (this.modes[this.modeIdx].hidden); + } + } + + this.resize(); + }, + moveOldMessageIndex(up: boolean) { + if (up && this.oldMessages.length > this.oldMessagesIndex + 1) { + this.oldMessagesIndex += 1; + this.message = this.oldMessages[this.oldMessagesIndex]; + } else if (!up && this.oldMessagesIndex - 1 >= 0) { + this.oldMessagesIndex -= 1; + this.message = this.oldMessages[this.oldMessagesIndex]; + } else if (!up && this.oldMessagesIndex - 1 === -1) { + this.oldMessagesIndex = -1; + this.message = ""; + } + }, + resize() { + const input = this.$refs.input as HTMLInputElement; + + // scrollHeight includes padding, but content-box excludes padding + // remove padding before setting height on the element + const style = getComputedStyle(input); + const paddingRemove = parseFloat(style.paddingBottom) + parseFloat(style.paddingTop); + + input.style.height = "5px"; + input.style.height = `${input.scrollHeight - paddingRemove}px`; + }, + send() { + if (this.message !== "") { + post( + "http://chat/chatResult", + JSON.stringify({ + message: this.message, + mode: this.modes[this.modeIdxGet].name + }) + ); + this.oldMessages.unshift(this.message); + this.oldMessagesIndex = -1; + this.hideInput(); + } else { + this.hideInput(true); + } + }, + hideInput(canceled = false) { + setTimeout(() => { + const input = this.$refs.input as HTMLInputElement; + delete input.style.height; + }, 50); + + if (canceled) { + post("http://chat/chatResult", JSON.stringify({ canceled })); + } + this.message = ""; + this.showInput = false; + clearInterval(this.focusTimer); + + if (this.hideState !== ChatHideStates.AlwaysHide) { + this.resetShowWindowTimer(); + } else { + this.showWindow = false; + } + } + } +}); \ No newline at end of file diff --git a/resources/[gameplay]/chat/html/App.vue b/resources/[gameplay]/chat/html/App.vue new file mode 100644 index 0000000..9a76ab8 --- /dev/null +++ b/resources/[gameplay]/chat/html/App.vue @@ -0,0 +1,44 @@ + + + \ No newline at end of file diff --git a/resources/[gameplay]/chat/html/Message.js b/resources/[gameplay]/chat/html/Message.ts similarity index 58% rename from resources/[gameplay]/chat/html/Message.js rename to resources/[gameplay]/chat/html/Message.ts index 067d842..9326fd7 100644 --- a/resources/[gameplay]/chat/html/Message.js +++ b/resources/[gameplay]/chat/html/Message.ts @@ -1,42 +1,48 @@ -Vue.component('message', { - template: '#message_template', +import CONFIG from './config'; +import Vue, { PropType } from 'vue'; + +export default Vue.component('message', { data() { return {}; }, computed: { - textEscaped() { + textEscaped(): string { let s = this.template ? this.template : this.templates[this.templateId]; - if (this.template) { - //We disable templateId since we are using a direct raw template - this.templateId = -1; - } - //This hack is required to preserve backwards compatability - if (this.templateId == CONFIG.defaultTemplateId + if (!this.template && this.templateId == CONFIG.defaultTemplateId && this.args.length == 1) { s = this.templates[CONFIG.defaultAltTemplateId] //Swap out default template :/ } + s = s.replace(`@default`, this.templates[this.templateId]); + s = s.replace(/{(\d+)}/g, (match, number) => { - const argEscaped = this.args[number] != undefined ? this.escape(this.args[number]) : match + const argEscaped = this.args[number] != undefined ? this.escape(this.args[number]) : match; if (number == 0 && this.color) { //color is deprecated, use templates or ^1 etc. return this.colorizeOld(argEscaped); } return argEscaped; }); + + // format variant args + s = s.replace(/\{\{([a-zA-Z0-9_\-]+?)\}\}/g, (match, id) => { + const argEscaped = this.params[id] != undefined ? this.escape(this.params[id]) : match; + return argEscaped; + }); + return this.colorize(s); }, }, methods: { - colorizeOld(str) { + colorizeOld(str: string): string { return `${str}` }, - colorize(str) { - let s = "" + (str.replace(/\^([0-9])/g, (str, color) => ``)) + ""; + colorize(str: string): string { + let s = "" + colorTrans(str) + ""; - const styleDict = { + const styleDict: {[ key: string ]: string} = { '*': 'font-weight: bold;', '_': 'text-decoration: underline;', '~': 'text-decoration: line-through;', @@ -49,8 +55,15 @@ Vue.component('message', { s = s.replace(styleRegex, (str, style, inner) => `${inner}`) } return s.replace(/]*><\/span[^>]*>/g, ''); + + function colorTrans(str: string) { + return str + .replace(/\^([0-9])/g, (str, color) => ``) + .replace(/\^#([0-9A-F]{3,6})/gi, (str, color) => ``) + .replace(/~([a-z])~/g, (str, color) => ``); + } }, - escape(unsafe) { + escape(unsafe: string): string { return String(unsafe) .replace(/&/g, '&') .replace(/, }, args: { - type: Array, + type: Array as PropType, + }, + params: { + type: Object as PropType<{ [ key: string]: string }>, }, template: { type: String, @@ -79,8 +95,8 @@ Vue.component('message', { default: false, }, color: { //deprecated - type: Array, - default: false, + type: Array as PropType, + default: null, }, }, }); diff --git a/resources/[gameplay]/chat/html/Message.vue b/resources/[gameplay]/chat/html/Message.vue new file mode 100644 index 0000000..7f87cea --- /dev/null +++ b/resources/[gameplay]/chat/html/Message.vue @@ -0,0 +1,7 @@ + + + \ No newline at end of file diff --git a/resources/[gameplay]/chat/html/Suggestions.js b/resources/[gameplay]/chat/html/Suggestions.ts similarity index 75% rename from resources/[gameplay]/chat/html/Suggestions.js rename to resources/[gameplay]/chat/html/Suggestions.ts index 07c4688..08cd9ae 100644 --- a/resources/[gameplay]/chat/html/Suggestions.js +++ b/resources/[gameplay]/chat/html/Suggestions.ts @@ -1,11 +1,29 @@ -Vue.component('suggestions', { - template: '#suggestions_template', - props: ['message', 'suggestions'], +import CONFIG from './config'; +import Vue, { PropType } from 'vue'; + +export interface Suggestion { + name: string; + help: string; + params: string[]; + + disabled: boolean; +} + +export default Vue.component('suggestions', { + props: { + message: { + type: String + }, + + suggestions: { + type: Array as PropType + } + }, data() { return {}; }, computed: { - currentSuggestions() { + currentSuggestions(): Suggestion[] { if (this.message === '') { return []; } @@ -34,6 +52,7 @@ Vue.component('suggestions', { const regex = new RegExp(`${s.name} (?:\\w+ ){${index}}(?:${wType}*)$`, 'g'); // eslint-disable-next-line no-param-reassign + // @ts-ignore p.disabled = this.message.match(regex) == null; }); }); diff --git a/resources/[gameplay]/chat/html/Suggestions.vue b/resources/[gameplay]/chat/html/Suggestions.vue new file mode 100644 index 0000000..d8d5c38 --- /dev/null +++ b/resources/[gameplay]/chat/html/Suggestions.vue @@ -0,0 +1,29 @@ + + + \ No newline at end of file diff --git a/resources/[gameplay]/chat/html/config.default.js b/resources/[gameplay]/chat/html/config.ts similarity index 81% rename from resources/[gameplay]/chat/html/config.default.js rename to resources/[gameplay]/chat/html/config.ts index 82192ad..bd76f77 100644 --- a/resources/[gameplay]/chat/html/config.default.js +++ b/resources/[gameplay]/chat/html/config.ts @@ -1,6 +1,4 @@ -// DO NOT EDIT THIS FILE -// Copy it to `config.js` and edit it -window.CONFIG = { +export default { defaultTemplateId: 'default', //This is the default template for 2 args1 defaultAltTemplateId: 'defaultAlt', //This one for 1 arg templates: { //You can add static templates here @@ -13,7 +11,7 @@ window.CONFIG = { suggestionLimit: 5, style: { background: 'rgba(52, 73, 94, 0.7)', - width: '38%', + width: '38vw', height: '22%', } }; diff --git a/resources/[gameplay]/chat/html/index.css b/resources/[gameplay]/chat/html/index.css index 30f1699..df5c1c2 100644 --- a/resources/[gameplay]/chat/html/index.css +++ b/resources/[gameplay]/chat/html/index.css @@ -8,6 +8,14 @@ .color-8{color: #cc0000;} .color-9{color: #cc0068;} +.gameColor-w{color: #ffffff;} +.gameColor-r{color: #ff4444;} +.gameColor-g{color: #99cc00;} +.gameColor-y{color: #ffbb33;} +.gameColor-b{color: #33b5e5;} + +/* todo: more game colors */ + * { font-family: 'Lato', sans-serif; margin: 0; @@ -63,26 +71,47 @@ em { box-sizing: border-box; } +.chat-input > div.input { + position: relative; + display: flex; + align-items: stretch; + width: 100%; + background-color: rgba(44, 62, 80, 1.0); +} + +.chat-hide-state { + text-transform: uppercase; + margin-left: 0.05vw; + font-size: 1.65vh; +} + .prefix { font-size: 1.8vh; - position: absolute; - margin-top: 0.5%; - left: 0.208%; + /*position: absolute; + top: 0%;*/ + height: 100%; + vertical-align: middle; + line-height: calc(1vh + 1vh + 1.85vh); + padding-left: 0.5vh; + text-transform: uppercase; + font-weight: bold; + display: inline-block; } textarea { font-size: 1.65vh; + line-height: 1.85vh; display: block; - box-sizing: border-box; - padding: 1%; - padding-left: 3.5%; + box-sizing: content-box; + padding: 1vh; + padding-left: 0.5vh; color: white; - background-color: rgba(44, 62, 80, 1.0); - width: 100%; border-width: 0; height: 3.15%; overflow: hidden; text-overflow: ellipsis; + flex: 1; + background-color: transparent; } textarea:focus, input:focus { @@ -123,5 +152,9 @@ textarea:focus, input:focus { } .hidden { - display: none; + opacity: 0; +} + +.hidden.animated { + transition: opacity 1s; } \ No newline at end of file diff --git a/resources/[gameplay]/chat/html/index.d.ts b/resources/[gameplay]/chat/html/index.d.ts new file mode 100644 index 0000000..314e3aa --- /dev/null +++ b/resources/[gameplay]/chat/html/index.d.ts @@ -0,0 +1,4 @@ +declare module '*.vue' { + import Vue from 'vue' + export default Vue +} \ No newline at end of file diff --git a/resources/[gameplay]/chat/html/index.html b/resources/[gameplay]/chat/html/index.html index 6ce65dd..c9b40ac 100644 --- a/resources/[gameplay]/chat/html/index.html +++ b/resources/[gameplay]/chat/html/index.html @@ -3,115 +3,12 @@ - - - + + + - - - -
- - - - - - - - - - - - - - - - - - diff --git a/resources/[gameplay]/chat/html/main.ts b/resources/[gameplay]/chat/html/main.ts new file mode 100644 index 0000000..9a487fd --- /dev/null +++ b/resources/[gameplay]/chat/html/main.ts @@ -0,0 +1,7 @@ +import Vue from 'vue'; +import App from './App.vue'; + +const instance = new Vue({ + el: '#app', + render: h => h(App), +}); \ No newline at end of file diff --git a/resources/[gameplay]/chat/html/tsconfig.json b/resources/[gameplay]/chat/html/tsconfig.json new file mode 100644 index 0000000..9a44563 --- /dev/null +++ b/resources/[gameplay]/chat/html/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "outDir": "./", + "module": "es6", + "strict": true, + "moduleResolution": "node", + "target": "es6", + "allowJs": true, + "lib": [ + "es2017", + "dom" + ] + }, + "include": [ + "./**/*" + ], + "exclude": [] +} \ No newline at end of file diff --git a/resources/[gameplay]/chat/html/utils.ts b/resources/[gameplay]/chat/html/utils.ts new file mode 100644 index 0000000..76c30e5 --- /dev/null +++ b/resources/[gameplay]/chat/html/utils.ts @@ -0,0 +1,31 @@ +export function post(url: string, data: any) { + var request = new XMLHttpRequest(); + request.open('POST', url, true); + request.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); + request.send(data); +} + +function emulate(type: string, detail = {}) { + const detailRef = { + type, + ...detail + }; + + window.dispatchEvent(new CustomEvent('message', { + detail: detailRef + })); +} + +(window as any)['emulate'] = emulate; + +(window as any)['demo'] = () => { + emulate('ON_MESSAGE', { + message: { + args: [ 'me', 'hello!' ] + } + }) + + emulate('ON_SCREEN_STATE_CHANGE', { + shouldHide: false + }); +}; \ No newline at end of file diff --git a/resources/[gameplay]/chat/html/vendor/vue.2.3.3.min.js b/resources/[gameplay]/chat/html/vendor/vue.2.3.3.min.js deleted file mode 100644 index 757d5aa..0000000 --- a/resources/[gameplay]/chat/html/vendor/vue.2.3.3.min.js +++ /dev/null @@ -1,8 +0,0 @@ -/*! - * Vue.js v2.3.3 - * (c) 2014-2017 Evan You - * Released under the MIT License. - */ -!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.Vue=t()}(this,function(){"use strict";function e(e){return void 0===e||null===e}function t(e){return void 0!==e&&null!==e}function n(e){return!0===e}function r(e){return!1===e}function i(e){return"string"==typeof e||"number"==typeof e}function o(e){return null!==e&&"object"==typeof e}function a(e){return"[object Object]"===Ti.call(e)}function s(e){return"[object RegExp]"===Ti.call(e)}function c(e){return null==e?"":"object"==typeof e?JSON.stringify(e,null,2):String(e)}function u(e){var t=parseFloat(e);return isNaN(t)?e:t}function l(e,t){for(var n=Object.create(null),r=e.split(","),i=0;i-1)return e.splice(n,1)}}function p(e,t){return ji.call(e,t)}function d(e){var t=Object.create(null);return function(n){return t[n]||(t[n]=e(n))}}function v(e,t){function n(n){var r=arguments.length;return r?r>1?e.apply(t,arguments):e.call(t,n):e.call(t)}return n._length=e.length,n}function h(e,t){t=t||0;for(var n=e.length-t,r=new Array(n);n--;)r[n]=e[n+t];return r}function m(e,t){for(var n in t)e[n]=t[n];return e}function g(e){for(var t={},n=0;nTo&&wo[n].id>e.id;)n--;wo.splice(n+1,0,e)}else wo.push(e);Oo||(Oo=!0,ao(xe))}}function Se(e){No.clear(),Te(e,No)}function Te(e,t){var n,r,i=Array.isArray(e);if((i||o(e))&&Object.isExtensible(e)){if(e.__ob__){var a=e.__ob__.dep.id;if(t.has(a))return;t.add(a)}if(i)for(n=e.length;n--;)Te(e[n],t);else for(r=Object.keys(e),n=r.length;n--;)Te(e[r[n]],t)}}function Ee(e,t,n){Lo.get=function(){return this[t][n]},Lo.set=function(e){this[t][n]=e},Object.defineProperty(e,n,Lo)}function je(e){e._watchers=[];var t=e.$options;t.props&&Ne(e,t.props),t.methods&&Re(e,t.methods),t.data?Le(e):j(e._data={},!0),t.computed&&De(e,t.computed),t.watch&&Fe(e,t.watch)}function Ne(e,t){var n=e.$options.propsData||{},r=e._props={},i=e.$options._propKeys=[],o=!e.$parent;vo.shouldConvert=o;for(var a in t)!function(o){i.push(o);var a=V(o,t,n,e);N(r,o,a),o in e||Ee(e,"_props",o)}(a);vo.shouldConvert=!0}function Le(e){var t=e.$options.data;t=e._data="function"==typeof t?Ie(t,e):t||{},a(t)||(t={});for(var n=Object.keys(t),r=e.$options.props,i=n.length;i--;)r&&p(r,n[i])||C(n[i])||Ee(e,"_data",n[i]);j(t,!0)}function Ie(e,t){try{return e.call(t)}catch(e){return k(e,t,"data()"),{}}}function De(e,t){var n=e._computedWatchers=Object.create(null);for(var r in t){var i=t[r],o="function"==typeof i?i:i.get;n[r]=new jo(e,o,y,Io),r in e||Me(e,r,i)}}function Me(e,t,n){"function"==typeof n?(Lo.get=Pe(t),Lo.set=y):(Lo.get=n.get?!1!==n.cache?Pe(t):n.get:y,Lo.set=n.set?n.set:y),Object.defineProperty(e,t,Lo)}function Pe(e){return function(){var t=this._computedWatchers&&this._computedWatchers[e];if(t)return t.dirty&&t.evaluate(),co.target&&t.depend(),t.value}}function Re(e,t){e.$options.props;for(var n in t)e[n]=null==t[n]?y:v(t[n],e)}function Fe(e,t){for(var n in t){var r=t[n];if(Array.isArray(r))for(var i=0;i=0||n.indexOf(e[i])<0)&&r.push(e[i]);return r}return e}function vt(e){this._init(e)}function ht(e){e.use=function(e){if(e.installed)return this;var t=h(arguments,1);return t.unshift(this),"function"==typeof e.install?e.install.apply(e,t):"function"==typeof e&&e.apply(null,t),e.installed=!0,this}}function mt(e){e.mixin=function(e){return this.options=H(this.options,e),this}}function gt(e){e.cid=0;var t=1;e.extend=function(e){e=e||{};var n=this,r=n.cid,i=e._Ctor||(e._Ctor={});if(i[r])return i[r];var o=e.name||n.options.name,a=function(e){this._init(e)};return a.prototype=Object.create(n.prototype),a.prototype.constructor=a,a.cid=t++,a.options=H(n.options,e),a.super=n,a.options.props&&yt(a),a.options.computed&&_t(a),a.extend=n.extend,a.mixin=n.mixin,a.use=n.use,Ri.forEach(function(e){a[e]=n[e]}),o&&(a.options.components[o]=a),a.superOptions=n.options,a.extendOptions=e,a.sealedOptions=m({},a.options),i[r]=a,a}}function yt(e){var t=e.options.props;for(var n in t)Ee(e.prototype,"_props",n)}function _t(e){var t=e.options.computed;for(var n in t)Me(e.prototype,n,t[n])}function bt(e){Ri.forEach(function(t){e[t]=function(e,n){return n?("component"===t&&a(n)&&(n.name=n.name||e,n=this.options._base.extend(n)),"directive"===t&&"function"==typeof n&&(n={bind:n,update:n}),this.options[t+"s"][e]=n,n):this.options[t+"s"][e]}})}function $t(e){return e&&(e.Ctor.options.name||e.tag)}function Ct(e,t){return"string"==typeof e?e.split(",").indexOf(t)>-1:!!s(e)&&e.test(t)}function xt(e,t,n){for(var r in e){var i=e[r];if(i){var o=$t(i.componentOptions);o&&!n(o)&&(i!==t&&wt(i),e[r]=null)}}}function wt(e){e&&e.componentInstance.$destroy()}function kt(e){for(var n=e.data,r=e,i=e;t(i.componentInstance);)i=i.componentInstance._vnode,i.data&&(n=At(i.data,n));for(;t(r=r.parent);)r.data&&(n=At(n,r.data));return Ot(n)}function At(e,n){return{staticClass:St(e.staticClass,n.staticClass),class:t(e.class)?[e.class,n.class]:n.class}}function Ot(e){var n=e.class,r=e.staticClass;return t(r)||t(n)?St(r,Tt(n)):""}function St(e,t){return e?t?e+" "+t:e:t||""}function Tt(n){if(e(n))return"";if("string"==typeof n)return n;var r="";if(Array.isArray(n)){for(var i,a=0,s=n.length;a-1?pa[e]=t.constructor===window.HTMLUnknownElement||t.constructor===window.HTMLElement:pa[e]=/HTMLUnknownElement/.test(t.toString())}function Nt(e){if("string"==typeof e){var t=document.querySelector(e);return t||document.createElement("div")}return e}function Lt(e,t){var n=document.createElement(e);return"select"!==e?n:(t.data&&t.data.attrs&&void 0!==t.data.attrs.multiple&&n.setAttribute("multiple","multiple"),n)}function It(e,t){return document.createElementNS(sa[e],t)}function Dt(e){return document.createTextNode(e)}function Mt(e){return document.createComment(e)}function Pt(e,t,n){e.insertBefore(t,n)}function Rt(e,t){e.removeChild(t)}function Ft(e,t){e.appendChild(t)}function Bt(e){return e.parentNode}function Ht(e){return e.nextSibling}function Ut(e){return e.tagName}function Vt(e,t){e.textContent=t}function zt(e,t,n){e.setAttribute(t,n)}function Jt(e,t){var n=e.data.ref;if(n){var r=e.context,i=e.componentInstance||e.elm,o=r.$refs;t?Array.isArray(o[n])?f(o[n],i):o[n]===i&&(o[n]=void 0):e.data.refInFor?Array.isArray(o[n])&&o[n].indexOf(i)<0?o[n].push(i):o[n]=[i]:o[n]=i}}function Kt(e,n){return e.key===n.key&&e.tag===n.tag&&e.isComment===n.isComment&&t(e.data)===t(n.data)&&qt(e,n)}function qt(e,n){if("input"!==e.tag)return!0;var r;return(t(r=e.data)&&t(r=r.attrs)&&r.type)===(t(r=n.data)&&t(r=r.attrs)&&r.type)}function Wt(e,n,r){var i,o,a={};for(i=n;i<=r;++i)o=e[i].key,t(o)&&(a[o]=i);return a}function Zt(e,t){(e.data.directives||t.data.directives)&&Gt(e,t)}function Gt(e,t){var n,r,i,o=e===ha,a=t===ha,s=Yt(e.data.directives,e.context),c=Yt(t.data.directives,t.context),u=[],l=[];for(n in c)r=s[n],i=c[n],r?(i.oldValue=r.value,Xt(i,"update",t,e),i.def&&i.def.componentUpdated&&l.push(i)):(Xt(i,"bind",t,e),i.def&&i.def.inserted&&u.push(i));if(u.length){var f=function(){for(var n=0;n=0&&" "===(m=e.charAt(h));h--);m&&Ca.test(m)||(l=!0)}}else void 0===o?(v=i+1,o=e.slice(0,i).trim()):t();if(void 0===o?o=e.slice(0,i).trim():0!==v&&t(),a)for(i=0;i=Vo}function _n(e){return 34===e||39===e}function bn(e){var t=1;for(qo=Ko;!yn();)if(e=gn(),_n(e))$n(e);else if(91===e&&t++,93===e&&t--,0===t){Wo=Ko;break}}function $n(e){for(var t=e;!yn()&&(e=gn())!==t;);}function Cn(e,t,n){Zo=n;var r=t.value,i=t.modifiers,o=e.tag,a=e.attrsMap.type;if("select"===o)kn(e,r,i);else if("input"===o&&"checkbox"===a)xn(e,r,i);else if("input"===o&&"radio"===a)wn(e,r,i);else if("input"===o||"textarea"===o)An(e,r,i);else if(!Bi.isReservedTag(o))return vn(e,r,i),!1;return!0}function xn(e,t,n){var r=n&&n.number,i=pn(e,"value")||"null",o=pn(e,"true-value")||"true",a=pn(e,"false-value")||"false";cn(e,"checked","Array.isArray("+t+")?_i("+t+","+i+")>-1"+("true"===o?":("+t+")":":_q("+t+","+o+")")),fn(e,wa,"var $$a="+t+",$$el=$event.target,$$c=$$el.checked?("+o+"):("+a+");if(Array.isArray($$a)){var $$v="+(r?"_n("+i+")":i)+",$$i=_i($$a,$$v);if($$c){$$i<0&&("+t+"=$$a.concat($$v))}else{$$i>-1&&("+t+"=$$a.slice(0,$$i).concat($$a.slice($$i+1)))}}else{"+hn(t,"$$c")+"}",null,!0)}function wn(e,t,n){var r=n&&n.number,i=pn(e,"value")||"null";i=r?"_n("+i+")":i,cn(e,"checked","_q("+t+","+i+")"),fn(e,wa,hn(t,i),null,!0)}function kn(e,t,n){var r=n&&n.number,i='Array.prototype.filter.call($event.target.options,function(o){return o.selected}).map(function(o){var val = "_value" in o ? o._value : o.value;return '+(r?"_n(val)":"val")+"})",o="var $$selectedVal = "+i+";";o=o+" "+hn(t,"$event.target.multiple ? $$selectedVal : $$selectedVal[0]"),fn(e,"change",o,null,!0)}function An(e,t,n){var r=e.attrsMap.type,i=n||{},o=i.lazy,a=i.number,s=i.trim,c=!o&&"range"!==r,u=o?"change":"range"===r?xa:"input",l="$event.target.value";s&&(l="$event.target.value.trim()"),a&&(l="_n("+l+")");var f=hn(t,l);c&&(f="if($event.target.composing)return;"+f),cn(e,"value","("+t+")"),fn(e,u,f,null,!0),(s||a||"number"===r)&&fn(e,"blur","$forceUpdate()")}function On(e){var n;t(e[xa])&&(n=qi?"change":"input",e[n]=[].concat(e[xa],e[n]||[]),delete e[xa]),t(e[wa])&&(n=Qi?"click":"change",e[n]=[].concat(e[wa],e[n]||[]),delete e[wa])}function Sn(e,t,n,r,i){if(n){var o=t,a=Go;t=function(n){null!==(1===arguments.length?o(n):o.apply(null,arguments))&&Tn(e,t,r,a)}}Go.addEventListener(e,t,Xi?{capture:r,passive:i}:r)}function Tn(e,t,n,r){(r||Go).removeEventListener(e,t,n)}function En(t,n){if(!e(t.data.on)||!e(n.data.on)){var r=n.data.on||{},i=t.data.on||{};Go=n.elm,On(r),Y(r,i,Sn,Tn,n.context)}}function jn(n,r){if(!e(n.data.domProps)||!e(r.data.domProps)){var i,o,a=r.elm,s=n.data.domProps||{},c=r.data.domProps||{};t(c.__ob__)&&(c=r.data.domProps=m({},c));for(i in s)e(c[i])&&(a[i]="");for(i in c)if(o=c[i],"textContent"!==i&&"innerHTML"!==i||(r.children&&(r.children.length=0),o!==s[i]))if("value"===i){a._value=o;var u=e(o)?"":String(o);Nn(a,r,u)&&(a.value=u)}else a[i]=o}}function Nn(e,t,n){return!e.composing&&("option"===t.tag||Ln(e,n)||In(e,n))}function Ln(e,t){return document.activeElement!==e&&e.value!==t}function In(e,n){var r=e.value,i=e._vModifiers;return t(i)&&i.number||"number"===e.type?u(r)!==u(n):t(i)&&i.trim?r.trim()!==n.trim():r!==n}function Dn(e){var t=Mn(e.style);return e.staticStyle?m(e.staticStyle,t):t}function Mn(e){return Array.isArray(e)?g(e):"string"==typeof e?Oa(e):e}function Pn(e,t){var n,r={};if(t)for(var i=e;i.componentInstance;)i=i.componentInstance._vnode,i.data&&(n=Dn(i.data))&&m(r,n);(n=Dn(e.data))&&m(r,n);for(var o=e;o=o.parent;)o.data&&(n=Dn(o.data))&&m(r,n);return r}function Rn(n,r){var i=r.data,o=n.data;if(!(e(i.staticStyle)&&e(i.style)&&e(o.staticStyle)&&e(o.style))){var a,s,c=r.elm,u=o.staticStyle,l=o.normalizedStyle||o.style||{},f=u||l,p=Mn(r.data.style)||{};r.data.normalizedStyle=t(p.__ob__)?m({},p):p;var d=Pn(r,!0);for(s in f)e(d[s])&&Ea(c,s,"");for(s in d)(a=d[s])!==f[s]&&Ea(c,s,null==a?"":a)}}function Fn(e,t){if(t&&(t=t.trim()))if(e.classList)t.indexOf(" ")>-1?t.split(/\s+/).forEach(function(t){return e.classList.add(t)}):e.classList.add(t);else{var n=" "+(e.getAttribute("class")||"")+" ";n.indexOf(" "+t+" ")<0&&e.setAttribute("class",(n+t).trim())}}function Bn(e,t){if(t&&(t=t.trim()))if(e.classList)t.indexOf(" ")>-1?t.split(/\s+/).forEach(function(t){return e.classList.remove(t)}):e.classList.remove(t);else{for(var n=" "+(e.getAttribute("class")||"")+" ",r=" "+t+" ";n.indexOf(r)>=0;)n=n.replace(r," ");e.setAttribute("class",n.trim())}}function Hn(e){if(e){if("object"==typeof e){var t={};return!1!==e.css&&m(t,Ia(e.name||"v")),m(t,e),t}return"string"==typeof e?Ia(e):void 0}}function Un(e){Ua(function(){Ua(e)})}function Vn(e,t){(e._transitionClasses||(e._transitionClasses=[])).push(t),Fn(e,t)}function zn(e,t){e._transitionClasses&&f(e._transitionClasses,t),Bn(e,t)}function Jn(e,t,n){var r=Kn(e,t),i=r.type,o=r.timeout,a=r.propCount;if(!i)return n();var s=i===Ma?Fa:Ha,c=0,u=function(){e.removeEventListener(s,l),n()},l=function(t){t.target===e&&++c>=a&&u()};setTimeout(function(){c0&&(n=Ma,l=a,f=o.length):t===Pa?u>0&&(n=Pa,l=u,f=c.length):(l=Math.max(a,u),n=l>0?a>u?Ma:Pa:null,f=n?n===Ma?o.length:c.length:0),{type:n,timeout:l,propCount:f,hasTransform:n===Ma&&Va.test(r[Ra+"Property"])}}function qn(e,t){for(;e.length1}function Xn(e,t){!0!==t.data.show&&Zn(t)}function er(e,t,n){var r=t.value,i=e.multiple;if(!i||Array.isArray(r)){for(var o,a,s=0,c=e.options.length;s-1,a.selected!==o&&(a.selected=o);else if(_(nr(a),r))return void(e.selectedIndex!==s&&(e.selectedIndex=s));i||(e.selectedIndex=-1)}}function tr(e,t){for(var n=0,r=t.length;n=0&&a[i].lowerCasedTag!==s;i--);else i=0;if(i>=0){for(var c=a.length-1;c>=i;c--)t.end&&t.end(a[c].tag,n,r);a.length=i,o=i&&a[i-1].tag}else"br"===s?t.start&&t.start(e,[],!0,n,r):"p"===s&&(t.start&&t.start(e,[],!1,n,r),t.end&&t.end(e,n,r))}for(var i,o,a=[],s=t.expectHTML,c=t.isUnaryTag||Di,u=t.canBeLeftOpenTag||Di,l=0;e;){if(i=e,o&&Ds(o)){var f=o.toLowerCase(),p=Ms[f]||(Ms[f]=new RegExp("([\\s\\S]*?)(]*>)","i")),d=0,v=e.replace(p,function(e,n,r){return d=r.length,Ds(f)||"noscript"===f||(n=n.replace(//g,"$1").replace(//g,"$1")),t.chars&&t.chars(n),""});l+=e.length-v.length,e=v,r(f,l-d,l)}else{var h=e.indexOf("<");if(0===h){if(vs.test(e)){var m=e.indexOf("--\x3e");if(m>=0){n(m+3);continue}}if(hs.test(e)){var g=e.indexOf("]>");if(g>=0){n(g+2);continue}}var y=e.match(ds);if(y){n(y[0].length);continue}var _=e.match(ps);if(_){var b=l;n(_[0].length),r(_[1],b,l);continue}var $=function(){var t=e.match(ls);if(t){var r={tagName:t[1],attrs:[],start:l};n(t[0].length);for(var i,o;!(i=e.match(fs))&&(o=e.match(cs));)n(o[0].length),r.attrs.push(o);if(i)return r.unarySlash=i[1],n(i[0].length),r.end=l,r}}();if($){!function(e){var n=e.tagName,i=e.unarySlash;s&&("p"===o&&as(n)&&r(o),u(n)&&o===n&&r(n));for(var l=c(n)||"html"===n&&"head"===o||!!i,f=e.attrs.length,p=new Array(f),d=0;d=0){for(x=e.slice(h);!(ps.test(x)||ls.test(x)||vs.test(x)||hs.test(x)||(w=x.indexOf("<",1))<0);)h+=w,x=e.slice(h);C=e.substring(0,h),n(h)}h<0&&(C=e,e=""),t.chars&&C&&t.chars(C)}if(e===i){t.chars&&t.chars(e);break}}r()}function yr(e,t){var n=t?Hs(t):Bs;if(n.test(e)){for(var r,i,o=[],a=n.lastIndex=0;r=n.exec(e);){i=r.index,i>a&&o.push(JSON.stringify(e.slice(a,i)));var s=rn(r[1].trim());o.push("_s("+s+")"),a=i+r[0].length}return a0,Zi=Ki&&Ki.indexOf("edge/")>0,Gi=Ki&&Ki.indexOf("android")>0,Yi=Ki&&/iphone|ipad|ipod|ios/.test(Ki),Qi=Ki&&/chrome\/\d+/.test(Ki)&&!Zi,Xi=!1;if(Ji)try{var eo={};Object.defineProperty(eo,"passive",{get:function(){Xi=!0}}),window.addEventListener("test-passive",null,eo)}catch(e){}var to,no,ro=function(){return void 0===to&&(to=!Ji&&"undefined"!=typeof global&&"server"===global.process.env.VUE_ENV),to},io=Ji&&window.__VUE_DEVTOOLS_GLOBAL_HOOK__,oo="undefined"!=typeof Symbol&&A(Symbol)&&"undefined"!=typeof Reflect&&A(Reflect.ownKeys),ao=function(){function e(){r=!1;var e=n.slice(0);n.length=0;for(var t=0;t1?h(n):n;for(var r=h(arguments,1),i=0,o=n.length;i1&&(t[n[0].trim()]=n[1].trim())}}),t}),Sa=/^--/,Ta=/\s*!important$/,Ea=function(e,t,n){if(Sa.test(t))e.style.setProperty(t,n);else if(Ta.test(n))e.style.setProperty(t,n.replace(Ta,""),"important");else{var r=Na(t);if(Array.isArray(n))for(var i=0,o=n.length;iv?(f=e(i[g+1])?null:i[g+1].elm,y(n,f,i,d,g,o)):d>g&&b(n,r,p,v)}function x(r,i,o,a){if(r!==i){if(n(i.isStatic)&&n(r.isStatic)&&i.key===r.key&&(n(i.isCloned)||n(i.isOnce)))return i.elm=r.elm,void(i.componentInstance=r.componentInstance);var s,c=i.data;t(c)&&t(s=c.hook)&&t(s=s.prepatch)&&s(r,i);var u=i.elm=r.elm,l=r.children,f=i.children;if(t(c)&&h(i)){for(s=0;s',n.innerHTML.indexOf(t)>0}("\n"," "),is=l("area,base,br,col,embed,frame,hr,img,input,isindex,keygen,link,meta,param,source,track,wbr"),os=l("colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr,source"),as=l("address,article,aside,base,blockquote,body,caption,col,colgroup,dd,details,dialog,div,dl,dt,fieldset,figcaption,figure,footer,form,h1,h2,h3,h4,h5,h6,head,header,hgroup,hr,html,legend,li,menuitem,meta,optgroup,option,param,rp,rt,source,style,summary,tbody,td,tfoot,th,thead,title,tr,track"),ss=[/"([^"]*)"+/.source,/'([^']*)'+/.source,/([^\s"'=<>`]+)/.source],cs=new RegExp("^\\s*"+/([^\s"'<>\/=]+)/.source+"(?:\\s*("+/(?:=)/.source+")\\s*(?:"+ss.join("|")+"))?"),us="[a-zA-Z_][\\w\\-\\.]*",ls=new RegExp("^<((?:"+us+"\\:)?"+us+")"),fs=/^\s*(\/?)>/,ps=new RegExp("^<\\/((?:"+us+"\\:)?"+us+")[^>]*>"),ds=/^]+>/i,vs=/^