From 19c2a7b3c2774a444eeaf9e6e87b6057ddd40d06 Mon Sep 17 00:00:00 2001 From: dewral Date: Fri, 3 Mar 2023 12:00:51 +0100 Subject: [PATCH] Task Module --- modules/game_tasks/images/taskIcon.png | Bin 0 -> 11199 bytes .../game_tasks/images/taskIconColorless.png | Bin 0 -> 6506 bytes .../game_tasks/serverSIDE/extendedopcode.txt | 12 + .../game_tasks/serverSIDE/lib/core/json.lua | 400 ++++++++++++++++++ .../game_tasks/serverSIDE/lib/core/player.lua | 13 + modules/game_tasks/serverSIDE/taskSystem.lua | 317 ++++++++++++++ modules/game_tasks/tasks.lua | 274 ++++++++++++ modules/game_tasks/tasks.otmod | 8 + modules/game_tasks/tasks.otui | 188 ++++++++ 9 files changed, 1212 insertions(+) create mode 100644 modules/game_tasks/images/taskIcon.png create mode 100644 modules/game_tasks/images/taskIconColorless.png create mode 100644 modules/game_tasks/serverSIDE/extendedopcode.txt create mode 100644 modules/game_tasks/serverSIDE/lib/core/json.lua create mode 100644 modules/game_tasks/serverSIDE/lib/core/player.lua create mode 100644 modules/game_tasks/serverSIDE/taskSystem.lua create mode 100644 modules/game_tasks/tasks.lua create mode 100644 modules/game_tasks/tasks.otmod create mode 100644 modules/game_tasks/tasks.otui diff --git a/modules/game_tasks/images/taskIcon.png b/modules/game_tasks/images/taskIcon.png new file mode 100644 index 0000000000000000000000000000000000000000..e81f6f6a507e2c0c84bb1d3efc57f9c33bbe2102 GIT binary patch literal 11199 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 O{aWh!YL8Xy0{;sHBV{T8 literal 0 HcmV?d00001 diff --git a/modules/game_tasks/images/taskIconColorless.png b/modules/game_tasks/images/taskIconColorless.png new file mode 100644 index 0000000000000000000000000000000000000000..8527b39e2439a0ee24794ea476848f8518fdf635 GIT binary patch literal 6506 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 literal 0 HcmV?d00001 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