otclient-redemption/modules/game_taskboard/controller/tab_weekly.lua
kokekanon 4a87cebb11
feat: protocol 15.24 packets and features (#1604)
New Features:
- Task Board (Bounty, Weekly, Shop) with Preferred & Soulseal modals, Kill Tracker, Offline Training dialog, Vocation tutorial, infobanner notifications, redesigned NPC chat/trade, and multi-slot action bar.

Gameplay Enhancements:
- Expanded Cyclopedia stats, talisman upgrades, weekly rewards/progress, soulseal fights, taskboard shop/offers and offline training actions.

UI / Graphics:
- Per-source spell-effect opacity sliders, status icon bar, HUD/scroll improvements, many new templates/styles, screenshot UI updates, and small label/icon fixes.

Bug Fixes:
- Miscellaneous UI behavior, layout and robustness improvements.
2026-05-20 11:26:11 -03:00

358 lines
14 KiB
Lua

-- Weekly tasks tab logic — methods added to TaskBoardController
-- Server data handler
local function getItemServerName(itemId)
local thingType = g_things.getThingType(itemId, ThingCategoryItem)
if thingType then
return thingType:getName()
end
return "Unknown Item"
end
local function countFinishedEntries(entries)
local total = 0
for _, entry in ipairs(entries or {}) do
if entry.finished then
total = total + 1
end
end
return total
end
function TaskBoardController:refreshWeeklySummary()
local header = self.weeklyHdr or {}
local completedKillTasks = tonumber(header.completedKillTasks)
if completedKillTasks == nil then
completedKillTasks = countFinishedEntries(self.weeklyMonsters)
end
local completedDeliveryTasks = tonumber(header.completedDeliveryTasks) or 0
local totalTaskSlots = tonumber(header.totalTaskSlots) or WEEKLY_DEFAULT_TASK_SLOTS
local maxExperience = tonumber(header.maxExperience) or 0
local maxDeliveryExperience = tonumber(header.maxDeliveryExperience) or 0
local difficultyId = tonumber(header.difficulty) or 0
local killTaskPoints = WEEKLY_KILL_TASK_POINTS[difficultyId] or WEEKLY_KILL_TASK_POINTS[1]
local deliveryTaskPoints = WEEKLY_DELIVERY_TASK_POINTS
local soulsealPointsPerTask = WEEKLY_SOULSEAL_POINTS_PER_TASK
local totalCompleted = completedKillTasks + completedDeliveryTasks
local pointsEarned = tonumber(header.pointsEarned) or 0
local soulsealsEarned = tonumber(header.soulsealsEarned) or 0
local killBase = completedKillTasks * killTaskPoints
local deliveryBase = completedDeliveryTasks * deliveryTaskPoints
local basePoints = killBase + deliveryBase
local rewardMultiplier = (basePoints > 0) and math.max(1, math.floor(pointsEarned / basePoints + 0.5)) or 1
self.weeklyHdr.completedKillTasks = completedKillTasks
self.weeklyRewardTokens = comma_value(pointsEarned)
self.weeklyRewardSeals = comma_value(soulsealsEarned)
self.weeklyRemainingDays = tonumber(header.remainingDays) or 0
self.weeklyShowExtraSlotUnlock = not ((tonumber(header.extraSlot) or 0) == 1 or header.extraSlot == true)
self.weeklyProgressPct = self:calcProgressWidth(totalCompleted)
self.weeklyXpText = (maxExperience > 0) and
tr(
'Each kill task rewards you with %s XP and each delivery task will reward you with %s XP.',
comma_value(maxExperience), comma_value(maxDeliveryExperience)) or ""
self.weeklyProgressTooltip = string.format('Kill Tasks: %d\nDelivery Tasks: %d\nTotal: %d', completedKillTasks,
completedDeliveryTasks, totalCompleted)
self.weeklyRewardTokensTooltip = string.format(
'Hunting Task Points:\n\n %d * %d (Kill Tasks)\n+ %d * %d (Delivery Tasks)\n--------------------------------------\n= %s base points\nx %d reward multiplier\n= %s Hunting Task Points',
completedKillTasks, killTaskPoints, completedDeliveryTasks, deliveryTaskPoints,
comma_value(killBase + deliveryBase), rewardMultiplier, comma_value(pointsEarned))
self.weeklyRewardSealsTooltip = string.format(
'You receive %d Soulseal for each completed task. Soulseals can be\nused in the Soulpit. Click the obelisk there, then your character to\nopen a menu where you can select a creature you want to\nchallenge on your own.',
soulsealPointsPerTask)
self.weeklyDifficultySummaryVisible = completedKillTasks > 0 or completedDeliveryTasks > 0
self.weeklyDifficultyKillSummary = string.format('You have completed %d / %d kill tasks.', completedKillTasks,
totalTaskSlots)
self.weeklyDifficultyDeliverySummary = string.format('You have completed %d / %d delivery tasks.',
completedDeliveryTasks, totalTaskSlots)
self.weeklyDifficultyPointsEarned = comma_value(pointsEarned)
self.weeklyDifficultySealsEarned = comma_value(soulsealsEarned)
end
function TaskBoardController:onWeeklyServerData(header, monsters, items)
if type(header) ~= 'table' then
return
end
monsters = type(monsters) == 'table' and monsters or {}
items = type(items) == 'table' and items or {}
-- Update tracker regardless of UI state
if Tracker and Tracker.Weekly then
Tracker.Weekly.loadFromServerData(monsters)
end
local playerLevel = tonumber(header.currentPlayerLevel) or
(g_game.getLocalPlayer() and g_game.getLocalPlayer():getLevel() or 0)
local data = {
difficulty = tonumber(header.difficulty) or 0,
remainingDays = tonumber(header.remainingDays) or 7,
totalTaskSlots = tonumber(header.totalTaskSlots) or WEEKLY_DEFAULT_TASK_SLOTS,
maxExperience = tonumber(header.maxExperience) or 0,
maxDeliveryExperience = tonumber(header.maxDeliveryExperience) or 0,
completedKillTasks = tonumber(header.completedKillTasks) or 0,
completedDeliveryTasks = tonumber(header.completedDeliveryTasks) or 0,
pointsEarned = tonumber(header.pointsEarned) or 0,
soulsealsEarned = tonumber(header.soulsealsEarned) or 0,
extraSlot = (tonumber(header.extraSlot) or 0) == 1,
weeklyProgressFinished = (tonumber(header.weeklyProgressFinished) or 0) == 1
}
-- Build weeklyMonsters list
local monsterList = {}
for _, m in ipairs(monsters) do
m = type(m) == 'table' and m or {}
local raceId = tonumber(m.raceId) or 0
local raceData = raceId > 0 and g_things.getRaceData(raceId) or nil
local fullName = raceId == 0 and 'Any Creature' or ((raceData and raceData.name) or 'Unknown')
if raceId > 0 then
fullName = fullName:capitalize()
end
local shortName = fullName
if #shortName > 20 then
shortName = shortName:sub(1, 17) .. '...'
end
local finished = (tonumber(m.state) or 0) == 1
table.insert(monsterList, {
raceId = raceId,
name = shortName,
fullName = fullName,
outfit = raceData and raceData.outfit or nil,
current = tonumber(m.current) or 0,
total = tonumber(m.total) or 0,
finished = finished,
showProgress = not finished,
anyCreature = raceId == 0
})
end
self.weeklyMonsters = monsterList
-- Build weeklyItems list
local itemList = {}
for i, it in ipairs(items) do
it = type(it) == 'table' and it or {}
local itemId = tonumber(it.itemId) or 0
local itemName = itemId > 0 and (getItemServerName(itemId) or tostring(itemId)) or "Unknown Item"
local current = tonumber(it.current) or 0
local total = tonumber(it.total) or 0
local delivered = (tonumber(it.claimed) or 0) == 1
local finished = (tonumber(it.state) or 0) == 1
local canDeliver = finished and not delivered
table.insert(itemList, {
itemId = itemId,
itemName = itemName,
current = current,
total = total,
delivered = delivered,
finished = finished,
slotIndex = tonumber(it.slotIndex) or (i - 1),
canDeliver = canDeliver,
showProgress = not delivered
})
end
self.weeklyItems = itemList
-- Build difficulties list for modal (from client-side constant — server does not send it)
local diffList = {}
for _, d in ipairs(WEEKLY_DIFFICULTIES) do
local canSelect = playerLevel >= d.minLevel
table.insert(diffList, {
id = d.id,
name = d.name,
minLevel = d.minLevel,
canSelect = canSelect,
tooltip = (not canSelect) and string.format('The minimum level to start this difficulty is %d', d.minLevel) or
nil
})
end
self.weeklyDifficulties = diffList
self.weeklyHdr = data
self:refreshWeeklySummary()
-- Difficulty modal
self.weeklyDifficultyPending = data.difficulty == 0 or data.weeklyProgressFinished
self.diffModalVisible = false
self:onWeeklyTabSelected()
end
-- Kill update
function TaskBoardController:onWeeklyKillUpdate(raceId, currentKills, totalKills, isCompleted)
if Tracker and Tracker.Weekly then
Tracker.Weekly.onKillUpdate(raceId, currentKills, totalKills, isCompleted)
end
if type(self.weeklyMonsters) ~= "table" then
self.weeklyMonsters = {}
return
end
-- Update matching entry in weeklyMonsters
local completionChanged = false
for i, m in ipairs(self.weeklyMonsters) do
if m.raceId == raceId then
completionChanged = m.finished ~= (isCompleted == 1)
self.weeklyMonsters[i].current = currentKills
self.weeklyMonsters[i].total = totalKills
self.weeklyMonsters[i].finished = isCompleted == 1
self.weeklyMonsters[i].showProgress = isCompleted ~= 1
self.weeklyMonsters = self.weeklyMonsters
break
elseif raceId == 0 and m.anyCreature then
completionChanged = m.finished ~= (isCompleted == 1)
self.weeklyMonsters[i].current = currentKills
self.weeklyMonsters[i].total = totalKills
self.weeklyMonsters[i].finished = isCompleted == 1
self.weeklyMonsters[i].showProgress = isCompleted ~= 1
self.weeklyMonsters = self.weeklyMonsters
break
end
end
local completedKillTasks = countFinishedEntries(self.weeklyMonsters)
self.weeklyHdr.completedKillTasks = completedKillTasks
self:refreshWeeklySummary()
if completionChanged and g_game.weeklyTaskAction then
g_game.weeklyTaskAction(WEEKLY_ACTION_REFRESH_DATA, 0)
end
end
-- Tab selection
function TaskBoardController:onWeeklyTabSelected()
if not self.ui or not self.ui:isVisible() then
return
end
self.diffModalVisible = self.activeTab == self.TAB_WEEKLY and self.weeklyDifficultyPending
end
-- Actions
function TaskBoardController:selectDifficulty(diffId)
local parsedId = tonumber(diffId) or 0
if parsedId <= 0 then
return
end
g_game.weeklyTaskAction(WEEKLY_ACTION_SELECT_DIFFICULTY, parsedId)
self.weeklyDifficultyPending = false
self.diffModalVisible = false
return
end
function TaskBoardController:deliverItem(itemId, slotIndex)
local parsedItemId = tonumber(itemId) or 0
local itemName = parsedItemId > 0 and (getItemServerName(parsedItemId) or tostring(parsedItemId)) or "Unknown Item"
local msgBox
local function yes()
if msgBox then
msgBox:destroy();
msgBox = nil
end
g_game.weeklyTaskAction(WEEKLY_ACTION_DELIVER_ITEM, tonumber(slotIndex) or 0)
end
local function no()
if msgBox then
msgBox:destroy();
msgBox = nil
end
end
msgBox = displayGeneralBox(tr('Deliver Item'), tr('Do you want to deliver %s?', itemName), {{
text = tr('Yes'),
callback = yes
}, {
text = tr('No'),
callback = no
}}, yes, no)
end
-- Delivery item right-click context menu
function TaskBoardController:onDeliveryItemsRendered()
local container = self:findWidget("#deliveryTasksBox")
if not container then
return
end
local ctrl = self
for i = 1, container:getChildCount() do
local card = container:getChildByIndex(i)
if card and not card:isDestroyed() and card.__for_values then
local item = card.__for_values[1]
if item then
local itemId = tonumber(item.itemId) or 0
local itemName = tostring(item.itemName or "")
if itemId > 0 then
card.onMouseRelease = function(widget, mousePos, mouseButton)
if mouseButton == MouseRightButton then
ctrl:showDeliveryItemMenu(itemId, itemName, mousePos)
return true
end
end
end
end
end
end
end
function TaskBoardController:showDeliveryItemMenu(itemId, itemName, mousePos)
local menu = g_ui.createWidget('PopupMenu')
menu:addOption(tr('Cyclopedia'), function()
local cyc = modules.game_cyclopedia
if not cyc then
return
end
if controllerCyclopedia and controllerCyclopedia.ui and controllerCyclopedia.ui:isVisible() then
cyc.SelectWindow('items', false)
else
cyc.show('items')
end
TaskBoardController:scheduleEvent(function()
if cyc.Cyclopedia and cyc.Cyclopedia.ItemSearch then
cyc.Cyclopedia.ItemSearch(itemName, false)
end
end, 100, "showDeliveryItemCyclopedia")
end)
menu:addOption(tr('Show in Market'), function()
local market = modules.game_market
if not market or not market.onShowRedirect then
return
end
local thingType = g_things.getThingType(itemId, ThingCategoryItem)
if not thingType then
return
end
market.onMarketEnter({}, 0, 0, 0)
TaskBoardController:scheduleEvent(function()
market.onShowRedirect(thingType)
end, 100, "showDeliveryItemMarket")
end)
menu:display(mousePos)
end
-- Progress bar helper
function TaskBoardController:calcProgressWidth(completedTasks)
local sectionIndex = 0
for i = 1, WEEKLY_SECTIONS do
if completedTasks >= WEEKLY_THRESHOLDS[i + 1] then
sectionIndex = i
else
break
end
end
local sectionWidth = 100 / WEEKLY_SECTIONS
if sectionIndex >= WEEKLY_SECTIONS then
return 100
end
local sectionStart = WEEKLY_THRESHOLDS[sectionIndex + 1]
local sectionEnd = WEEKLY_THRESHOLDS[sectionIndex + 2]
local fraction = (completedTasks - sectionStart) / (sectionEnd - sectionStart)
return math.min(math.floor((sectionIndex + fraction) * sectionWidth), 100)
end