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.
258 lines
8.6 KiB
Lua
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
|