diff --git a/modules/game_tasks/images/taskIcon.png b/modules/game_tasks/images/taskIcon.png new file mode 100644 index 0000000000..e81f6f6a50 Binary files /dev/null and b/modules/game_tasks/images/taskIcon.png differ diff --git a/modules/game_tasks/images/taskIconColorless.png b/modules/game_tasks/images/taskIconColorless.png new file mode 100644 index 0000000000..8527b39e24 Binary files /dev/null and b/modules/game_tasks/images/taskIconColorless.png differ diff --git a/modules/game_tasks/serverSIDE/extendedopcode.txt b/modules/game_tasks/serverSIDE/extendedopcode.txt new file mode 100644 index 0000000000..a7ab57a3de --- /dev/null +++ b/modules/game_tasks/serverSIDE/extendedopcode.txt @@ -0,0 +1,12 @@ + if opcode == OPCODE_LANGUAGE then + -- otclient language + if buffer == 'en' or buffer == 'pt' then + -- example, setting player language, because otclient is multi-language... + -- player:setStorageValue(SOME_STORAGE_ID, SOME_VALUE) + end + elseif opcode == 215 then + TaskSystem.onAction(player, json.decode(buffer)) + else + -- other opcodes can be ignored, and the server will just work fine... + end +end \ No newline at end of file diff --git a/modules/game_tasks/serverSIDE/lib/core/json.lua b/modules/game_tasks/serverSIDE/lib/core/json.lua new file mode 100644 index 0000000000..098e7b29a0 --- /dev/null +++ b/modules/game_tasks/serverSIDE/lib/core/json.lua @@ -0,0 +1,400 @@ +-- +-- json.lua +-- +-- Copyright (c) 2019 rxi +-- +-- Permission is hereby granted, free of charge, to any person obtaining a copy of +-- this software and associated documentation files (the "Software"), to deal in +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +-- of the Software, and to permit persons to whom the Software is furnished to do +-- so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all +-- copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +-- SOFTWARE. +-- + +local json = { _version = "0.1.2" } + +------------------------------------------------------------------------------- +-- Encode +------------------------------------------------------------------------------- + +local encode + +local escape_char_map = { + [ "\\" ] = "\\\\", + [ "\"" ] = "\\\"", + [ "\b" ] = "\\b", + [ "\f" ] = "\\f", + [ "\n" ] = "\\n", + [ "\r" ] = "\\r", + [ "\t" ] = "\\t", +} + +local escape_char_map_inv = { [ "\\/" ] = "/" } +for k, v in pairs(escape_char_map) do + escape_char_map_inv[v] = k +end + + +local function escape_char(c) + return escape_char_map[c] or string.format("\\u%04x", c:byte()) +end + + +local function encode_nil(val) + return "null" +end + + +local function encode_table(val, stack) + local res = {} + stack = stack or {} + + -- Circular reference? + if stack[val] then error("circular reference") end + + stack[val] = true + + if rawget(val, 1) ~= nil or next(val) == nil then + -- Treat as array -- check keys are valid and it is not sparse + local n = 0 + for k in pairs(val) do + if type(k) ~= "number" then + error("invalid table: mixed or invalid key types") + end + n = n + 1 + end + if n ~= #val then + error("invalid table: sparse array") + end + -- Encode + for i, v in ipairs(val) do + table.insert(res, encode(v, stack)) + end + stack[val] = nil + return "[" .. table.concat(res, ",") .. "]" + + else + -- Treat as an object + for k, v in pairs(val) do + if type(k) ~= "string" then + error("invalid table: mixed or invalid key types") + end + table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) + end + stack[val] = nil + return "{" .. table.concat(res, ",") .. "}" + end +end + + +local function encode_string(val) + return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' +end + + +local function encode_number(val) + -- Check for NaN, -inf and inf + if val ~= val or val <= -math.huge or val >= math.huge then + error("unexpected number value '" .. tostring(val) .. "'") + end + return string.format("%.14g", val) +end + + +local type_func_map = { + [ "nil" ] = encode_nil, + [ "table" ] = encode_table, + [ "string" ] = encode_string, + [ "number" ] = encode_number, + [ "boolean" ] = tostring, +} + + +encode = function(val, stack) + local t = type(val) + local f = type_func_map[t] + if f then + return f(val, stack) + end + error("unexpected type '" .. t .. "'") +end + + +function json.encode(val) + return ( encode(val) ) +end + + +------------------------------------------------------------------------------- +-- Decode +------------------------------------------------------------------------------- + +local parse + +local function create_set(...) + local res = {} + for i = 1, select("#", ...) do + res[ select(i, ...) ] = true + end + return res +end + +local space_chars = create_set(" ", "\t", "\r", "\n") +local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") +local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") +local literals = create_set("true", "false", "null") + +local literal_map = { + [ "true" ] = true, + [ "false" ] = false, + [ "null" ] = nil, +} + + +local function next_char(str, idx, set, negate) + for i = idx, #str do + if set[str:sub(i, i)] ~= negate then + return i + end + end + return #str + 1 +end + + +local function decode_error(str, idx, msg) + local line_count = 1 + local col_count = 1 + for i = 1, idx - 1 do + col_count = col_count + 1 + if str:sub(i, i) == "\n" then + line_count = line_count + 1 + col_count = 1 + end + end + error( string.format("%s at line %d col %d", msg, line_count, col_count) ) +end + + +local function codepoint_to_utf8(n) + -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa + local f = math.floor + if n <= 0x7f then + return string.char(n) + elseif n <= 0x7ff then + return string.char(f(n / 64) + 192, n % 64 + 128) + elseif n <= 0xffff then + return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) + elseif n <= 0x10ffff then + return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, + f(n % 4096 / 64) + 128, n % 64 + 128) + end + error( string.format("invalid unicode codepoint '%x'", n) ) +end + + +local function parse_unicode_escape(s) + local n1 = tonumber( s:sub(3, 6), 16 ) + local n2 = tonumber( s:sub(9, 12), 16 ) + -- Surrogate pair? + if n2 then + return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) + else + return codepoint_to_utf8(n1) + end +end + + +local function parse_string(str, i) + local has_unicode_escape = false + local has_surrogate_escape = false + local has_escape = false + local last + for j = i + 1, #str do + local x = str:byte(j) + + if x < 32 then + decode_error(str, j, "control character in string") + end + + if last == 92 then -- "\\" (escape char) + if x == 117 then -- "u" (unicode escape sequence) + local hex = str:sub(j + 1, j + 5) + if not hex:find("%x%x%x%x") then + decode_error(str, j, "invalid unicode escape in string") + end + if hex:find("^[dD][89aAbB]") then + has_surrogate_escape = true + else + has_unicode_escape = true + end + else + local c = string.char(x) + if not escape_chars[c] then + decode_error(str, j, "invalid escape char '" .. c .. "' in string") + end + has_escape = true + end + last = nil + + elseif x == 34 then -- '"' (end of string) + local s = str:sub(i + 1, j - 1) + if has_surrogate_escape then + s = s:gsub("\\u[dD][89aAbB]..\\u....", parse_unicode_escape) + end + if has_unicode_escape then + s = s:gsub("\\u....", parse_unicode_escape) + end + if has_escape then + s = s:gsub("\\.", escape_char_map_inv) + end + return s, j + 1 + + else + last = x + end + end + decode_error(str, i, "expected closing quote for string") +end + + +local function parse_number(str, i) + local x = next_char(str, i, delim_chars) + local s = str:sub(i, x - 1) + local n = tonumber(s) + if not n then + decode_error(str, i, "invalid number '" .. s .. "'") + end + return n, x +end + + +local function parse_literal(str, i) + local x = next_char(str, i, delim_chars) + local word = str:sub(i, x - 1) + if not literals[word] then + decode_error(str, i, "invalid literal '" .. word .. "'") + end + return literal_map[word], x +end + + +local function parse_array(str, i) + local res = {} + local n = 1 + i = i + 1 + while 1 do + local x + i = next_char(str, i, space_chars, true) + -- Empty / end of array? + if str:sub(i, i) == "]" then + i = i + 1 + break + end + -- Read token + x, i = parse(str, i) + res[n] = x + n = n + 1 + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "]" then break end + if chr ~= "," then decode_error(str, i, "expected ']' or ','") end + end + return res, i +end + + +local function parse_object(str, i) + local res = {} + i = i + 1 + while 1 do + local key, val + i = next_char(str, i, space_chars, true) + -- Empty / end of object? + if str:sub(i, i) == "}" then + i = i + 1 + break + end + -- Read key + if str:sub(i, i) ~= '"' then + decode_error(str, i, "expected string for key") + end + key, i = parse(str, i) + -- Read ':' delimiter + i = next_char(str, i, space_chars, true) + if str:sub(i, i) ~= ":" then + decode_error(str, i, "expected ':' after key") + end + i = next_char(str, i + 1, space_chars, true) + -- Read value + val, i = parse(str, i) + -- Set + res[key] = val + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "}" then break end + if chr ~= "," then decode_error(str, i, "expected '}' or ','") end + end + return res, i +end + + +local char_func_map = { + [ '"' ] = parse_string, + [ "0" ] = parse_number, + [ "1" ] = parse_number, + [ "2" ] = parse_number, + [ "3" ] = parse_number, + [ "4" ] = parse_number, + [ "5" ] = parse_number, + [ "6" ] = parse_number, + [ "7" ] = parse_number, + [ "8" ] = parse_number, + [ "9" ] = parse_number, + [ "-" ] = parse_number, + [ "t" ] = parse_literal, + [ "f" ] = parse_literal, + [ "n" ] = parse_literal, + [ "[" ] = parse_array, + [ "{" ] = parse_object, +} + + +parse = function(str, idx) + local chr = str:sub(idx, idx) + local f = char_func_map[chr] + if f then + return f(str, idx) + end + decode_error(str, idx, "unexpected character '" .. chr .. "'") +end + + +function json.decode(str) + if type(str) ~= "string" then + error("expected argument of type string, got " .. type(str)) + end + local res, idx = parse(str, next_char(str, 1, space_chars, true)) + idx = next_char(str, idx, space_chars, true) + if idx <= #str then + decode_error(str, idx, "trailing garbage") + end + return res +end + + +return json diff --git a/modules/game_tasks/serverSIDE/lib/core/player.lua b/modules/game_tasks/serverSIDE/lib/core/player.lua new file mode 100644 index 0000000000..2e72f2ff09 --- /dev/null +++ b/modules/game_tasks/serverSIDE/lib/core/player.lua @@ -0,0 +1,13 @@ +function Player.sendExtendedJSONOpcode(self, opcode, buffer) + if not self:isUsingOtClient() then + return false + end + + local networkMessage = NetworkMessage() + networkMessage:addByte(0x32) + networkMessage:addByte(opcode) + networkMessage:addString(json.encode(buffer)) + networkMessage:sendToPlayer(self) + networkMessage:delete() + return true +end \ No newline at end of file diff --git a/modules/game_tasks/serverSIDE/taskSystem.lua b/modules/game_tasks/serverSIDE/taskSystem.lua new file mode 100644 index 0000000000..b396b3d2cb --- /dev/null +++ b/modules/game_tasks/serverSIDE/taskSystem.lua @@ -0,0 +1,317 @@ +local taskPointStorage = 5151 -- which player storage holds task points. + +local configTasks = { + [1] = { + nameOfTheTask = "Rat", -- same as target name e.g. rat / Rat (doesnt matter, the name will be lowered later anyway) + looktype = { type = 21 }, + killsRequired = 25, + rewards = { + expReward = 1500, + pointsReward = 0, -- NOT the id, its the amount of points, if no point reward then delete this line/ dont write at all. + } + }, + [2] = { + nameOfTheTask = "Cave Rat", -- same as target name e.g. rat / Rat (doesnt matter, the name will be lowered later anyway) + looktype = { type = 56 }, + killsRequired = 25, + rewards = { + expReward = 2000, + pointsReward = 50, -- NOT the id, its the amount of points, if no point reward then delete this line/ dont write at all. + } + }, + [3] = { + nameOfTheTask = "Snake", + looktype = { type = 28 }, + killsRequired = 25, + rewards = { + expReward = 2000, + -- no functionality: itemRewards = {26447, 26408, 26430} + } + }, + [4] = { + nameOfTheTask = "Scorpion", + looktype = { type = 43 }, + killsRequired = 25, + rewards = { + expReward = 2000, + -- no functionality: itemRewards = {26447, 26408, 26430} + } + }, + [5] = { + nameOfTheTask = "Amazon", + looktype = { type = 137, feet = 115, addons = 0, legs = 95, auxType = 7399, head = 113, body = 120 }, + killsRequired = 150, + rewards = { + expReward = 5000, + -- no functionality: itemRewards = {26447, 26408, 26430} + } + }, + [6] = { + nameOfTheTask = "Valkyrie", + looktype = { type = 139, feet = 96, addons = 0, legs = 76, auxType = 7399, head = 113, body = 38 }, + killsRequired = 150, + rewards = { + expReward = 8000, + -- no functionality: itemRewards = {26447, 26408, 26430} + } + }, +} + +TaskSystem = { + list = {}, + baseStorage = 1500, + maximumTasks = 100, + countForParty = true, + maxDist = 7, + players = {}, + loadDatabase = function() + if (#TaskSystem.list > 0) then + return true + end + + for i = 1, #configTasks do + table.insert(TaskSystem.list, { + id = i, + name = '' ..configTasks[i].nameOfTheTask..'', + looktype = configTasks[i].looktype, + kills = configTasks[i].killsRequired, + exp = configTasks[i].rewards.expReward, + taskPoints = configTasks[i].rewards.pointsReward, + }) + end + return true + end, + getCurrentTasks = function(player) + local tasks = {} + + for _, task in ipairs(TaskSystem.list) do + if (player:getStorageValue(TaskSystem.baseStorage + task.id) > 0) then + local playerTask = task -- deepcopy(task) + playerTask.left = player:getStorageValue(TaskSystem.baseStorage + task.id) + playerTask.done = playerTask.kills - (playerTask.left - 1) + table.insert(tasks, playerTask) + end + end + + return tasks + end, + getPlayerTaskIds = function(player) + local tasks = {} + + for _, task in ipairs(TaskSystem.list) do + if (player:getStorageValue(TaskSystem.baseStorage + task.id) > 0) then + table.insert(tasks, task.id) + end + end + + return tasks + end, + getTaskNames = function(player) + local tasks = {} + + for _, task in ipairs(TaskSystem.list) do + table.insert(tasks, '{' .. task.name:lower() .. '}') + end + + return table.concat(tasks, ', ') + end, + onAction = function(player, data) + if (data['action'] == 'info') then + TaskSystem.sendData(player) + TaskSystem.players[player.uid] = 1 + elseif (data['action'] == 'hide') then + TaskSystem.players[player.uid] = nil + elseif (data['action'] == 'start') then + local playerTaskIds = TaskSystem.getPlayerTaskIds(player) + + if (#playerTaskIds >= TaskSystem.maximumTasks) then + return player:sendExtendedJSONOpcode(215, { + message = "You can't take more tasks.", + color = 'red' + }) + end + + for _, task in ipairs(TaskSystem.list) do + if (task.id == data['entry']) then + if (table.contains(playerTaskIds, task.id)) then + return player:sendExtendedJSONOpcode(215, { + message = 'You already have this task active.', + color = 'red' + }) + end + + player:setStorageValue(TaskSystem.baseStorage + task.id, task.kills + 1) + player:sendExtendedJSONOpcode(215, { + message = 'Task started.', + color = 'green' + }) + + return TaskSystem.sendData(player) + end + end + + return player:sendExtendedJSONOpcode(215, { + message = 'Unknown task.', + color = 'red' + }) + elseif (data['action'] == 'cancel') then + for _, task in ipairs(TaskSystem.list) do + if (task.id == data['entry']) then + local playerTaskIds = TaskSystem.getPlayerTaskIds(player) + + if (not table.contains(playerTaskIds, task.id)) then + return player:sendExtendedJSONOpcode(215, { + message = "You don't have this task active.", + color = 'red' + }) + end + + player:setStorageValue(TaskSystem.baseStorage + task.id, -1) + player:sendExtendedJSONOpcode(215, { + message = 'Task aborted.', + color = 'green' + }) + + return TaskSystem.sendData(player) + end + end + + return player:sendExtendedJSONOpcode(215, { + message = 'Unknown task.', + color = 'red' + }) + elseif (data['action'] == 'finish') then + for _, task in ipairs(TaskSystem.list) do + if (task.id == data['entry']) then + local playerTaskIds = TaskSystem.getPlayerTaskIds(player) + + if (not table.contains(playerTaskIds, task.id)) then + return player:sendExtendedJSONOpcode(215, { + message = "You don't have this task active.", + color = 'red' + }) + end + + local left = player:getStorageValue(TaskSystem.baseStorage + task.id) + + if (left > 1) then + return player:sendExtendedJSONOpcode(215, { + message = "Task isn't completed yet.", + color = 'red' + }) + end + + player:setStorageValue(TaskSystem.baseStorage + task.id, -1) + player:addExperience(task.exp) + player:setStorageValue(taskPointStorage, (player:getStorageValue(taskPointStorage) + task.taskPoints)) + player:sendExtendedJSONOpcode(215, { + message = 'Task finished.', + color = 'green' + }) + + return TaskSystem.sendData(player) + end + end + + return player:sendExtendedJSONOpcode(215, { + message = 'Unknown task.', + color = 'red' + }) + end + end, + killForPlayer = function(player, task) + local left = player:getStorageValue(TaskSystem.baseStorage + task.id) + + if (left == 1) then + if (TaskSystem.players[player.uid]) then + player:sendExtendedJSONOpcode(215, { + message = 'Task finished.', + color = 'green' + }) + end + + return true + end + + player:setStorageValue(TaskSystem.baseStorage + task.id, left - 1) + + if (TaskSystem.players[player.uid]) then + return TaskSystem.sendData(player) + end + end, + onKill = function(player, target) + local targetName = target:getName():lower() + + for _, task in ipairs(TaskSystem.list) do + if (task.name:lower() == targetName) then + local playerTaskIds = TaskSystem.getPlayerTaskIds(player) + + if (not table.contains(playerTaskIds, task.id)) then + return true + end + + local party = player:getParty() + local tpos = target:getPosition() + + if (TaskSystem.countForParty and party and party:getMembers()) then + for i, creature in pairs(party:getMembers()) do + local pos = creature:getPosition() + + if (pos.z == tpos.z and pos:getDistance(tpos) <= TaskSystem.maxDist) then + TaskSystem.killForPlayer(creature, task) + end + end + + local pos = party:getLeader():getPosition() + + if (pos.z == tpos.z and pos:getDistance(tpos) <= TaskSystem.maxDist) then + TaskSystem.killForPlayer(party:getLeader(), task) + end + else + TaskSystem.killForPlayer(player, task) + end + + return true + end + end + end, + sendData = function(player) + local playerTasks = TaskSystem.getCurrentTasks(player) + + local response = { + allTasks = TaskSystem.list, + playerTasks = playerTasks + } + + return player:sendExtendedJSONOpcode(215, response) + end +} + +local events = {} + +local globalevent = GlobalEvent('Tasks') +TaskSystem.loadDatabase() + +function globalevent.onStartup() + return TaskSystem.loadDatabase() +end + +table.insert(events, globalevent) + +local creatureevent = CreatureEvent('TaskKill') + +function creatureevent.onKill(creature, target) + if (not creature:isPlayer() or not Monster(target)) then + return true + end + + TaskSystem.onKill(creature, target) + + return true +end + +table.insert(events, creatureevent) + +for _, event in ipairs(events) do + event:register() +end diff --git a/modules/game_tasks/tasks.lua b/modules/game_tasks/tasks.lua new file mode 100644 index 0000000000..91679d3a18 --- /dev/null +++ b/modules/game_tasks/tasks.lua @@ -0,0 +1,274 @@ +local window = nil +local selectedEntry = nil +local consoleEvent = nil +local taskButton + +function init() + connect(g_game, { + onGameStart = onGameStart, + onGameEnd = destroy + }) + + window = g_ui.displayUI('tasks') + window:setVisible(false) + + g_keyboard.bindKeyDown('Ctrl+A', toggleWindow) + g_keyboard.bindKeyDown('Escape', hideWindowzz) + taskButton = modules.client_topmenu.addLeftGameButton('taskButton', tr('Tasks'), '/modules/game_tasks/images/taskIcon', toggleWindow) + ProtocolGame.registerExtendedJSONOpcode(215, parseOpcode) +end + +function terminate() + disconnect(g_game, { + onGameEnd = destroy + }) + ProtocolGame.unregisterExtendedJSONOpcode(215, parseOpcode) + taskButton:destroy() + destroy() +end + +function onGameStart() + if (window) then + window:destroy() + window = nil + end + + window = g_ui.displayUI('tasks') + window:setVisible(false) + window.listSearch.search.onKeyPress = onFilterSearch +end + +function destroy() + if (window) then + window:destroy() + window = nil + end +end + +function parseOpcode(protocol, opcode, data) + updateTasks(data) +end + +function sendOpcode(data) + local protocolGame = g_game.getProtocolGame() + + if protocolGame then + protocolGame:sendExtendedJSONOpcode(215, data) + end +end + +function onItemSelect(list, focusedChild, unfocusedChild, reason) + if focusedChild then + selectedEntry = tonumber(focusedChild:getId()) + + if (not selectedEntry) then + return true + end + + window.finishButton:hide() + window.startButton:hide() + window.abortButton:hide() + local children = window.selectionList:getChildren() + + for _, child in ipairs(children) do + local id = tonumber(child:getId()) + + if (selectedEntry == id) then + local kills = child.kills:getText() + + if (child.progress:getWidth() == 159) then + window.finishButton:show() + elseif (kills:find('/')) then + window.abortButton:show() + else + window.startButton:show() + end + end + end + end +end + +function onFilterSearch() + addEvent(function() + local searchText = window.listSearch.search:getText():lower():trim() + local children = window.selectionList:getChildren() + + if (searchText:len() >= 1) then + for _, child in ipairs(children) do + local text = child.name:getText():lower() + + if (text:find(searchText)) then + child:show() + else + child:hide() + end + end + else + for _, child in ipairs(children) do + child:show() + end + end + end) +end + +function start() + if (not selectedEntry) then + return not setTaskConsoleText("Please select monster from monster list.", "red") + end + + sendOpcode({ + action = 'start', + entry = selectedEntry + }) +end + +function finish() + if (not selectedEntry) then + return not setTaskConsoleText("Please select monster from monster list.", "red") + end + + sendOpcode({ + action = 'finish', + entry = selectedEntry + }) +end + +function abort() + local cancelConfirm = nil + + if (cancelConfirm) then + cancelConfirm:destroy() + cancelConfirm = nil + end + + if (not selectedEntry) then + return not setTaskConsoleText("Please select monster from monster list.", "red") + end + + local yesFunc = function() + cancelConfirm:destroy() + cancelConfirm = nil + sendOpcode({ + action = 'cancel', + entry = selectedEntry + }) + end + + local noFunc = function() + cancelConfirm:destroy() + cancelConfirm = nil + end + + cancelConfirm = displayGeneralBox(tr('Tasks'), tr("Do you really want to abort this task?"), { + { + text = tr('Yes'), + callback = yesFunc + }, + { + text = tr('No'), + callback = noFunc + }, + anchor = AnchorHorizontalCenter + }, yesFunc, noFunc) +end + +function updateTasks(data) + if (data['message']) then + return setTaskConsoleText(data['message'], data['color']) + end + + local selectionList = window.selectionList + selectionList.onChildFocusChange = onItemSelect + selectionList:destroyChildren() + local playerTaskIds = {} + + for _, task in ipairs(data['playerTasks']) do + local button = g_ui.createWidget("SelectionButton", window.selectionList) + button:setId(task.id) + table.insert(playerTaskIds, task.id) + button.creature:setOutfit(task.looktype) + button.name:setText(task.name) + button.kills:setText('Kills: ' .. task.done .. '/' .. task.kills) + button.reward:setText('Reward: ' .. task.exp .. ' exp') + if not (task.taskPoints == nil) then + button.rewardTaskPoints:setText('Task Points: ' .. task.taskPoints .. '') + else + button.rewardTaskPoints:setText('Task Points: 0') + end + local progress = 159 * task.done / task.kills + button.progress:setWidth(progress) + selectionList:focusChild(button) + end + + for _, task in ipairs(data['allTasks']) do + if (not table.contains(playerTaskIds, task.id)) then + local button = g_ui.createWidget("SelectionButton", window.selectionList) + button:setId(task.id) + button.creature:setOutfit(task.looktype) + button.name:setText(task.name) + button.kills:setText('Kills: ' .. task.kills) + button.reward:setText('Reward: ' .. task.exp .. ' exp') + if not (task.taskPoints == nil) then + button.rewardTaskPoints:setText('Task Points: ' .. task.taskPoints .. '') + else + button.rewardTaskPoints:setText('Task Points: 0') + end + button.progress:setWidth(0) + selectionList:focusChild(button) + end + end + + selectionList:focusChild(selectionList:getFirstChild()) + onFilterSearch() +end + +function toggleWindow() + if (not g_game.isOnline()) then + return + end + + if (window:isVisible()) then + sendOpcode({ + action = 'hide' + }) + window:setVisible(false) + else + sendOpcode({ + action = 'info' + }) + window:setVisible(true) + end +end + +function hideWindowzz() + if (not g_game.isOnline()) then + return + end + + if (window:isVisible()) then + sendOpcode({ + action = 'hide' + }) + window:setVisible(false) + end +end + +function setTaskConsoleText(text, color) + if (not color) then + color = 'white' + end + + window.info:setText(text) + window.info:setColor(color) + + if consoleEvent then + removeEvent(consoleEvent) + consoleEvent = nil + end + + consoleEvent = scheduleEvent(function() + window.info:setText('') + end, 5000) + + return true +end diff --git a/modules/game_tasks/tasks.otmod b/modules/game_tasks/tasks.otmod new file mode 100644 index 0000000000..8477762a0e --- /dev/null +++ b/modules/game_tasks/tasks.otmod @@ -0,0 +1,8 @@ +Module + name: game_tasks + description: Displays tasks system + author: Rookgaard + sandboxed: true + scripts: [ tasks ] + @onLoad: init() + @onUnload: terminate() diff --git a/modules/game_tasks/tasks.otui b/modules/game_tasks/tasks.otui new file mode 100644 index 0000000000..758fc36c4e --- /dev/null +++ b/modules/game_tasks/tasks.otui @@ -0,0 +1,188 @@ +MiniPanel < Panel + text-offset: 0 3 + text-align: top + image-source: /images/ui/minipanel + image-border: 2 + image-border-top: 19 + padding-left: 7 + padding-bottom: 7 + padding-top: 24 + padding-right: 7 + +SelectionButton < Panel + image-source: /images/ui/button + image-color: #dfdfdf + image-clip: 0 0 22 23 + image-border: 3 + border: 1 alpha + focusable: true + phantom: false + + $hover: + border: 1 white + + $focus: + border: 1 white + image-color: #ffffff + + Panel + id: progress + image-source: /images/ui/panel_flat + image-color: #00ff00 + margin-left: 1 + margin-top: 1 + anchors.top: parent.top + anchors.left: parent.left + size: 159 158 + + UICreature + id: creature + anchors.centerIn: parent + size: 90 90 + margin-bottom: 17 + phantom: true + anchors.verticalCenter: prev.verticalCenter + + Label + id: name + anchors.bottom: creature.top + anchors.left: parent.left + anchors.right: parent.right + margin-top: -7 + text-align: top + text-wrap: true + text-border: 5 black + + Label + id: kills + anchors.top: creature.bottom + anchors.left: parent.left + anchors.right: parent.right + text-align: center + margin-top: 5 + text-wrap: true + + Label + id: reward + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + text-align: center + margin-top: 2 + text-wrap: true + + Label + id: rewardTaskPoints + anchors.top: prev.bottom + anchors.left: parent.left + anchors.right: parent.right + text-align: center + margin-top: 3 + text-wrap: true + +MainWindow + size: 549 509 + id: tasksWindow + !text: tr('Tasks') + + @onEnter: modules.game_tasks.toggleWindow() + @onEscape: modules.game_tasks.toggleWindow() + + MiniPanel + id: listSearch + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + margin-left: 5 + height: 50 + text: Filter + + TextEdit + id: search + anchors.fill: parent + placeholder: Search by name + + ScrollablePanel + id: selectionList + anchors.top: listSearch.bottom + anchors.left: listSearch.left + anchors.right: parent.right + anchors.bottom: separator.top + margin-top: 5 + margin-bottom: 5 + image-source: /images/ui/panel_flat + image-border: 1 + padding: 4 + padding-right: 16 + vertical-scrollbar: selectionScroll + layout: + type: grid + cell-size: 160 160 + cell-spacing: 2 + flow: true + + VerticalScrollBar + id: selectionScroll + anchors.top: prev.top + anchors.right: prev.right + anchors.bottom: prev.bottom + step: 80 + pixel-scroll: true + + HorizontalSeparator + id: separator + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + margin-bottom: 30 + + Label + id: info + anchors.top: separator.top + anchors.left: parent.left + width: 140 + height: 45 + text-align: center + text-wrap: true + + Button + id: toggleButton + anchors.bottom: parent.bottom + anchors.right: parent.right + text: Close + width: 65 + @onClick: modules.game_tasks.toggleWindow() + + Button + id: finishButton + anchors.bottom: parent.bottom + anchors.right: toggleButton.left + text: Finish + width: 55 + margin-right: 5 + @onClick: modules.game_tasks.finish() + + Button + id: startButton + anchors.bottom: parent.bottom + anchors.right: toggleButton.left + text: Start + width: 55 + margin-right: 5 + @onClick: modules.game_tasks.start() + + Button + id: abortButton + anchors.bottom: parent.bottom + anchors.right: toggleButton.left + text: Abort + width: 55 + margin-right: 5 + @onClick: modules.game_tasks.abort() + + ResizeBorder + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + enabled: true + minimum: 190