InspectController = Controller:new()
-- /*=============================================
-- = Helpers =
-- =============================================*/
local function asText(v)
return v ~= nil and tostring(v) or ""
end
local function normalizeDescription(d)
if type(d) ~= "table" then
return {
detail = "",
description = asText(d)
}
end
return {
detail = asText(d.detail or d[1] or d.key),
description = asText(d.description or d[2] or d.value)
}
end
local function normalizeDescriptions(list)
if type(list) ~= "table" then
return {}
end
local out = {}
for _, d in ipairs(list) do
out[#out + 1] = normalizeDescription(d)
end
return out
end
local function normalizeItem(item)
return type(item) == "number" and Item and Item.create and Item.create(item) or item
end
-- LuaFormatter off
-- slot > 0 → slotIndex = slot - 1 (1-based server → 0-based widget)
-- slot == 0 → slotIndex = 0
local function normalizeInventoryItems(items)
if type(items) ~= "table" then
return {}, {}
end
local list, bySlot = {}, {}
for _, data in ipairs(items) do
if type(data) == "table" then
local slot = tonumber(data.slot)
local idx = slot and (slot > 0 and slot - 1 or slot) or nil
local entry = {
item = normalizeItem(data.item or data.itemId),
name = asText(data.name or data.itemName),
slot = slot,
slotIndex = idx,
descriptions = normalizeDescriptions(data.descriptions),
imbuements = type(data.imbuements) == "table" and data.imbuements or {}
}
list[#list + 1] = entry
if idx ~= nil then
bySlot[idx] = entry
end
end
end
return list, bySlot
end
-- /*=============================================
-- = State Builder =
-- =============================================*/
-- INSPECT_CYCLOPEDIA → character view (player + equipment slots + outfit)
-- INSPECT_NORMALOBJECT / NPCTRADE → item view (single item, no slots)
local function buildState(data)
if data.inspectionType == InspectObjectTypes.INSPECT_CYCLOPEDIA then
local playerName = asText(data.playerName or data.name)
local playerDesc = normalizeDescriptions(data.playerDescriptions)
local items, bySlot = normalizeInventoryItems(data.inventoryItems)
local title = playerName ~= "" and tr("Inspect Character") .. " " .. playerName or tr("Inspect Character")
return {
isCyclopedia = true,
windowTitle = title,
playerName = playerName,
playerDesc = playerDesc,
inventoryItems = items,
bySlot = bySlot,
selectedSlot = nil,
viewState = InspectConst.CYCLOPEDIA_VIEW_INVENTORY,
outfit = type(data.outfit) == "table" and data.outfit or nil,
activeItem = nil,
activeName = playerName,
activeDesc = playerDesc,
activeImbu = {},
creatureId = data.creatureId
}
end
return {
isCyclopedia = false,
windowTitle = tr("Inspect Object"),
playerName = "",
playerDesc = {},
inventoryItems = {},
bySlot = {},
selectedSlot = nil,
viewState = InspectConst.CYCLOPEDIA_VIEW_INVENTORY,
outfit = nil,
activeItem = normalizeItem(data.item or data.itemId),
activeName = asText(data.itemName or data.name),
activeDesc = normalizeDescriptions(data.descriptions),
activeImbu = type(data.imbuements) == "table" and data.imbuements or {}
}
end
-- LuaFormatter on
-- /*=============================================
-- = Lifecycle =
-- =============================================*/
function InspectController:onGameStart()
if g_game.getClientVersion() < 1281 then
self:scheduleEvent(function()
g_modules.getModule("game_inspect"):unload()
end, 100, "unloadInspect")
return
end
self:registerEvents(g_game, {
onParseItemDetail = function(...)
if not (modules.game_cyclopedia and modules.game_cyclopedia.isVisible()) then
self:onInspection(...)
end
end,
onParseCharacterInspection = function(...)
self:onInspection(...)
end,
onInspectionState = function(creatureId, state)
g_logger.info(string.format("[InspectController] onInspectionState: creatureId=%s, state=%s TODO: forward state to UI when behavior is defined" ,
tostring(creatureId), tostring(state)))
end
})
end
function InspectController:onGameEnd()
self.state = nil
self.layout = nil
self.pendingCreatureId = nil
self:hide()
end
function InspectController:onTerminate()
self.state = nil
self.layout = nil
self.pendingCreatureId = nil
self:hide()
end
-- /*=============================================
-- = Incoming Packet =
-- =============================================*/
function InspectController:onInspection(data)
if type(data) ~= "table" or not InspectObjectTypes then
return
end
local t = data.inspectionType
if t ~= InspectObjectTypes.INSPECT_CYCLOPEDIA and t ~= InspectObjectTypes.INSPECT_NORMALOBJECT and t ~=
InspectObjectTypes.INSPECT_NPCTRADE then
return
end
if not data.creatureId and self.pendingCreatureId then
data.creatureId = self.pendingCreatureId
end
self.pendingCreatureId = nil
self.state = buildState(data)
self.layout = self.state.isCyclopedia and InspectConst.LAYOUTS.CYCLOPEDIA or InspectConst.LAYOUTS.NPCTRADE
self:show()
end
-- /*=============================================
-- = Capability Checks =
-- =============================================*/
function InspectController:isCyclopediaInspection()
return self.state ~= nil and self.state.isCyclopedia == true
end
function InspectController:isItemCyclopediaable()
local item = self.state and self.state.activeItem
return item ~= nil and modules.game_cyclopedia ~= nil and item:getCyclopediaType() > 0
end
function InspectController:isItemProficiencyable()
local item = self.state and self.state.activeItem
return item ~= nil and modules.game_proficiency ~= nil and item:getProficiencyId() > 0
end
-- /*=============================================
-- = UI Management =
-- =============================================*/
function InspectController:show()
if not self.state then
return
end
if not self.ui then
self:loadHtml(InspectConst.HTML_PATH)
end
self:render()
self.ui:show()
self.ui:raise()
self.ui:focus()
end
function InspectController:hide()
if self.ui then
self:unloadHtml()
end
end
function InspectController:toggle()
if self.ui and self.ui:isVisible() then
self:hide()
else
self:show()
end
end
-- /*=============================================
-- = Render Engine =
-- =============================================*/
function InspectController:_scheduleResize()
self:scheduleEvent(function()
self:resizeDetailRows()
end, 50, "resizeDetailRows")
end
function InspectController:_renderActive()
self:renderHeader()
self:renderDescriptions()
self:_scheduleResize()
end
function InspectController:render()
if not self.ui or not self.state then
return
end
self.ui:setTitle(self.state.windowTitle)
if self.state.isCyclopedia then
self:renderInventorySlots()
self:renderOutfit()
self:renderPanelMode()
else
local panel = self:findWidget("#cyclopediaPanel")
if panel then
panel:setVisible(false)
end
end
self:_renderActive()
end
function InspectController:renderHeader()
local s = self.state
if not s then
return
end
local label = self:findWidget("#inspectName")
if label then
label:setText(s.activeName ~= "" and tr("You are inspecting: ") .. s.activeName or tr("You are inspecting:"))
end
local itemWidget = self:findWidget("#inspectItem")
if itemWidget then
if s.activeItem then
itemWidget:setItem(s.activeItem)
ItemsDatabase.setTier(itemWidget, itemWidget:getItem(), false)
else
itemWidget:clearItem()
end
end
local slotRow = self:findWidget("#slotRow")
local hasImbuements = false
if slotRow then
slotRow:destroyChildren()
for i = 1, 3 do
local val = s.activeImbu[4 - i]
if val ~= nil then
hasImbuements = true
local slot = self:createWidgetFromHTML([[