mirror of
https://github.com/mehah/otclient
synced 2026-08-15 16:29:06 -04:00
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.
358 lines
14 KiB
Lua
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
|