otclient-redemption/modules/game_taskboard/controller/tab_bounty.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

258 lines
8.6 KiB
Lua

-- Bounty tab logic — methods added to TaskBoardController
local function formatPercent(value)
if value * 10 == math.floor(value * 10) then
return string.format('%.1f', value)
else
return string.format('%.2f', value)
end
end
local function normalizeTalismanValues(index, currentRaw, nextRaw)
local currentValue = tonumber(currentRaw) or 0
local nextValue = tonumber(nextRaw) or 0
-- Some payloads report level 0 as current=0 with a legacy next value.
-- Normalize the level-0 baseline so UI always starts at 2.5/5.0%.
if currentValue == 0 and nextValue > 0 then
local baseCurrent = (index == 4) and 500 or 250
local baseStep = (index == 4) and 100 or 50
currentValue = baseCurrent
if nextValue <= currentValue then
nextValue = currentValue + baseStep
end
end
return currentValue / 100, nextValue / 100
end
-- Server data handler
function TaskBoardController:onBountyServerData(header, monsters, talisman)
local rerollPoints = tonumber(header.rerollPoints) or 0
local claimDaily = tonumber(header.claimDaily) or 0
local difficulty = tonumber(header.difficulty) or 1
self.claimDailyServerAvailable = claimDaily == 1
self.rerollPoints = rerollPoints
self.claimDailyAvailable = self.claimDailyServerAvailable and rerollPoints < REROLL_TOKEN_CAP
self.claimDailyWarning = (rerollPoints >= REROLL_TOKEN_CAP)
self.bountyDifficulty = difficulty
-- Build bountyTasks table for *for loop in HTML
local tasks = {}
for _, m in ipairs(monsters) do
local raceId = tonumber(m.raceId) or 0
local currentKills = tonumber(m.currentKills) or 0
local totalKills = tonumber(m.totalKills) or 0
local rarity = tonumber(m.rarity) or 0
local isActive = tonumber(m.isActive) == 1
local isCompleted = tonumber(m.isCompleted) == 1
local raceData = raceId > 0 and g_things.getRaceData(raceId) or nil
local name = raceData and raceData.name or 'Unknown'
name = name:capitalize()
if #name > 20 then
name = name:sub(1, 20) .. '...'
end
table.insert(tasks, {
taskIndex = tonumber(m.taskIndex) or 0,
raceId = raceId,
name = name,
outfit = raceData and raceData.outfit or nil,
current = currentKills,
total = totalKills,
rarity = rarity,
backdrop = RARITY_BACKDROPS[rarity] or RARITY_BACKDROPS[0],
xp = comma_value(tonumber(m.rewardXp) or 0),
points = tostring(tonumber(m.rewardPoints) or 0),
reroll = tostring(tonumber(m.rewardReroll) or 0),
isActive = isActive,
isCompleted = isCompleted,
canClaim = (not isCompleted) and isActive and currentKills >= totalKills
})
end
self.bountyTasks = tasks
self:syncBountyDifficultySelect()
-- Build talismans table for *for loop
local tals = {}
for i = 1, 4 do
local s = type(talisman) == 'table' and talisman[i] or nil
if s then
local currentValue, nextValue = normalizeTalismanValues(i, s.currentValue, s.nextValue)
local upgradeCost = tonumber(s.upgradeCost) or 0
local rawNextValue = tonumber(s.nextValue) or 0
local isMaxed = rawNextValue == 0 and upgradeCost == 0
table.insert(tals, {
icon = TALISMAN_ICONS[i],
title = TALISMAN_TITLES[i],
current = string.format('Current: %s%%', formatPercent(currentValue)),
buttonText = isMaxed and 'MAX' or string.format('Upgrade to %s%%', formatPercent(nextValue)),
cost = isMaxed and '-' or tostring(upgradeCost),
statType = i - 1,
isMaxed = isMaxed,
upgradeable = not isMaxed,
showSeparator = i > 1
})
end
end
self.talismans = tals
-- Update kill tracker
self:updateBountyTracker(monsters)
end
-- Kill update
function TaskBoardController:onBountyKillUpdate(raceId, currentKills, totalKills, isCompleted)
-- Update tracker
self:onBountyTrackerKillUpdate(raceId, currentKills, totalKills, isCompleted)
-- Update the active task entry in bountyTasks if window is open
for i, task in ipairs(self.bountyTasks) do
if task.raceId == raceId then
self.bountyTasks[i].current = currentKills
self.bountyTasks[i].total = totalKills
self.bountyTasks[i].isCompleted = currentKills >= totalKills
self.bountyTasks[i].canClaim = (not self.bountyTasks[i].isCompleted) and task.isActive and currentKills >=
totalKills
-- Trigger reactive update by reassigning the table
self.bountyTasks = self.bountyTasks
break
end
end
end
-- Actions
function TaskBoardController:rerollMonsters()
if self.rerollPoints <= 0 then
return
end
local msgBox
local function yes()
if msgBox then
msgBox:destroy();
msgBox = nil
end
g_game.bountyTaskAction(BOUNTY_ACTION_REROLL, 0)
end
local function cancel()
if msgBox then
msgBox:destroy();
msgBox = nil
end
end
msgBox = displayGeneralBox(tr('Reroll Tasks'),
tr('Do you want to reroll your bounty tasks? This will consume 1 reroll token.'), {{
text = tr('Yes'),
callback = yes
}, {
text = tr('Cancel'),
callback = cancel
}}, yes, cancel)
end
function TaskBoardController:selectTask(taskIndex)
local parsedTaskIndex = tonumber(taskIndex) or 0
g_game.bountyTaskAction(BOUNTY_ACTION_SELECT, parsedTaskIndex)
end
function TaskBoardController:claimReward()
local msgBox
local function yes()
if msgBox then
msgBox:destroy();
msgBox = nil
end
g_game.bountyTaskAction(BOUNTY_ACTION_CLAIM_REWARD, 0)
end
local function cancel()
if msgBox then
msgBox:destroy();
msgBox = nil
end
end
msgBox = displayGeneralBox(tr('Claim Reward'), tr('Claim reward for completing the bounty task?'), {{
text = tr('Yes'),
callback = yes
}, {
text = tr('Cancel'),
callback = cancel
}}, yes, cancel)
end
function TaskBoardController:changeDifficulty(event)
-- event.data = selected option data (difficulty id set when populating the combobox)
local diffId = event and (tonumber(event.data) or tonumber(event.value)) or nil
if not diffId then
return
end
g_game.bountyTaskAction(BOUNTY_ACTION_CHANGE_DIFFICULTY, diffId)
end
function TaskBoardController:claimDaily()
if self.claimDailyWarning then
local msgBox
local function yes()
if msgBox then
msgBox:destroy();
msgBox = nil
end
g_game.bountyTaskAction(BOUNTY_ACTION_CLAIM_DAILY, 0)
end
local function cancel()
if msgBox then
msgBox:destroy();
msgBox = nil
end
end
msgBox = displayGeneralBox(tr('Reroll Token Cap'), tr(
'You already have 10 reroll tokens. Claiming another will discard the oldest. Continue?'), {{
text = tr('Yes'),
callback = yes
}, {
text = tr('Cancel'),
callback = cancel
}}, yes, cancel)
else
g_game.bountyTaskAction(BOUNTY_ACTION_CLAIM_DAILY, 0)
end
end
function TaskBoardController:upgradeTalisman(statType)
g_game.bountyTalismanUpgrade(statType)
end
-- Tracker helpers
function TaskBoardController:updateBountyTracker(monsters)
if not Tracker or not Tracker.Bounty then
return
end
local activeMonster = nil
for _, m in ipairs(monsters) do
if tonumber(m.isActive) == 1 then
activeMonster = m
break
end
end
if not activeMonster then
Tracker.Bounty.setInactive()
return
end
local raceId = tonumber(activeMonster.raceId) or 0
local currentKills = tonumber(activeMonster.currentKills) or 0
local totalKills = tonumber(activeMonster.totalKills) or 0
local isCompleted = currentKills >= totalKills and 1 or 0
self:onBountyTrackerKillUpdate(raceId, currentKills, totalKills, isCompleted)
end
function TaskBoardController:onBountyTrackerKillUpdate(raceId, currentKills, totalKills, isCompleted)
if not Tracker or not Tracker.Bounty then
return
end
Tracker.Bounty.onKillUpdate(raceId, currentKills, totalKills, isCompleted)
end