diff --git a/.idea/.gitignore b/.idea/.gitignore old mode 100644 new mode 100755 diff --git a/.idea/misc.xml b/.idea/misc.xml old mode 100644 new mode 100755 diff --git a/.idea/modules.xml b/.idea/modules.xml old mode 100644 new mode 100755 diff --git a/.idea/vcs.xml b/.idea/vcs.xml old mode 100644 new mode 100755 diff --git a/AltSystem.iml b/AltSystem.iml old mode 100644 new mode 100755 diff --git a/AltSystem.toc b/AltSystem.toc old mode 100644 new mode 100755 index 57c8602..3b8554d --- a/AltSystem.toc +++ b/AltSystem.toc @@ -1,12 +1,13 @@ -## Interface: 120001 +## Interface: 120005 ## Title: AltSystem ## Notes: Enhances RP gameplay with a custom rolling system -## Author: Goncalo -## Version: 1.0.0 +## Author: Rukira +## Version: 1.2.0 ## Dependencies: totalRP3, totalRP3_Extended ## SavedVariables: AltSystemDB Data.lua Core.lua +BuildSkillsUI.lua UI.lua Roll.lua diff --git a/BuildSkillsUI.lua b/BuildSkillsUI.lua new file mode 100755 index 0000000..d8e7f8f --- /dev/null +++ b/BuildSkillsUI.lua @@ -0,0 +1,430 @@ +-- AltSystem Build Skills UI +-- Creates the Build Skills tab content with editable skill rows, add/delete functionality, and save to TRP. + +AltSystem = AltSystem or {} + +local PADDING = 12 +local ROW_HEIGHT = 36 +local ROW_SPACING = 8 +local NAME_WIDTH = 340 +local LEVEL_WIDTH = 140 +local VALUE_WIDTH = 80 +local DELETE_WIDTH = 30 + +-- Working copy of skills being edited (not saved until user clicks Save) +local editableSkills = {} + +-- Pool of created row frames for reuse +local skillRowFrames = {} + +-- References set during creation +local scrollFrame, scrollChild, addRowButton + +-- ─── Helper: Create a flat dark input box matching the blocky design ──────── + +local function CreateFlatEditBox(name, parent, width) + local container = CreateFrame("Frame", name .. "Container", parent, "BackdropTemplate") + container:SetSize(width, 28) + container:SetBackdrop({ + bgFile = "Interface\\ChatFrame\\ChatFrameBackground", + edgeFile = "Interface\\ChatFrame\\ChatFrameBackground", + edgeSize = 1, + }) + container:SetBackdropColor(0.12, 0.12, 0.12, 1) + container:SetBackdropBorderColor(0.25, 0.25, 0.25, 1) + + local editBox = CreateFrame("EditBox", name, container) + editBox:SetPoint("LEFT", 8, 0) + editBox:SetPoint("RIGHT", -8, 0) + editBox:SetHeight(28) + editBox:SetAutoFocus(false) + editBox:SetFontObject("GameFontHighlight") + + return container, editBox +end + +-- ─── Helper: Create a flat dark dropdown button matching the blocky design ── +-- Shared globally so other files (UI.lua) can reuse it. + +function AltSystem.CreateFlatDropdown(name, parent, width) + local btn = CreateFrame("Button", name, parent, "BackdropTemplate") + btn:SetSize(width, 28) + btn:SetBackdrop({ + bgFile = "Interface\\ChatFrame\\ChatFrameBackground", + edgeFile = "Interface\\ChatFrame\\ChatFrameBackground", + edgeSize = 1, + }) + btn:SetBackdropColor(0.12, 0.12, 0.12, 1) + btn:SetBackdropBorderColor(0.25, 0.25, 0.25, 1) + + -- Text label (left-aligned) + local text = btn:CreateFontString(nil, "OVERLAY", "GameFontHighlight") + text:SetPoint("LEFT", 8, 0) + text:SetPoint("RIGHT", -24, 0) + text:SetJustifyH("LEFT") + btn.label = text + + -- Gold arrow icon (right side) + local arrow = btn:CreateTexture(nil, "OVERLAY") + arrow:SetSize(12, 12) + arrow:SetPoint("RIGHT", -6, 0) + arrow:SetTexture("Interface\\ChatFrame\\ChatFrameExpandArrow") + arrow:SetVertexColor(0.9, 0.75, 0.2, 1) + btn.arrow = arrow + + -- Hover highlight + btn:SetScript("OnEnter", function(self) + self:SetBackdropColor(0.18, 0.18, 0.18, 1) + end) + btn:SetScript("OnLeave", function(self) + self:SetBackdropColor(0.12, 0.12, 0.12, 1) + end) + + -- Menu storage + btn.menuSetup = nil + btn:SetScript("OnClick", function(self) + if self.menuSetup then + MenuUtil.CreateContextMenu(self, self.menuSetup) + end + end) + + function btn:SetupMenu(setupFunc) + self.menuSetup = setupFunc + end + + return btn +end + +-- ─── Helper: Create a flat dark button matching the blocky design ──────────── +-- Shared globally so other files (UI.lua) can reuse it. + +function AltSystem.CreateFlatButton(name, parent, width, height, text) + local btn = CreateFrame("Button", name, parent, "BackdropTemplate") + btn:SetSize(width, height) + btn:SetBackdrop({ + bgFile = "Interface\\ChatFrame\\ChatFrameBackground", + edgeFile = "Interface\\ChatFrame\\ChatFrameBackground", + edgeSize = 1, + }) + btn:SetBackdropColor(0.4, 0.08, 0.08, 0.9) + btn:SetBackdropBorderColor(0.25, 0.25, 0.25, 1) + + local label = btn:CreateFontString(nil, "OVERLAY", "GameFontNormal") + label:SetPoint("CENTER") + label:SetText(text or "") + btn.label = label + + btn:SetScript("OnEnter", function(self) + self:SetBackdropColor(0.55, 0.12, 0.12, 0.9) + end) + btn:SetScript("OnLeave", function(self) + self:SetBackdropColor(0.4, 0.08, 0.08, 0.9) + end) + + function btn:SetText(newText) + self.label:SetText(newText) + end + + function btn:GetText() + return self.label:GetText() + end + + return btn +end + +-- ─── Create a single skill row frame ──────────────────────────────────────── + +local function CreateSkillRowFrame(index) + local row = CreateFrame("Frame", "AltSystemSkillRow" .. index, scrollChild) + row:SetHeight(ROW_HEIGHT) + + -- Name EditBox (flat dark style) + local nameContainer, nameBox = CreateFlatEditBox("AltSystemSkillName" .. index, row, NAME_WIDTH) + nameContainer:SetPoint("LEFT", row, "LEFT", 8, 0) + row.nameBox = nameBox + row.nameContainer = nameContainer + + -- Level Dropdown (flat dark style) + local levelDropdown = AltSystem.CreateFlatDropdown("AltSystemSkillLevel" .. index, row, LEVEL_WIDTH) + levelDropdown:SetPoint("LEFT", nameContainer, "RIGHT", 12, 0) + row.levelDropdown = levelDropdown + + -- Value Dropdown (flat dark style) + local valueDropdown = AltSystem.CreateFlatDropdown("AltSystemSkillValue" .. index, row, VALUE_WIDTH) + valueDropdown:SetPoint("LEFT", levelDropdown, "RIGHT", 8, 0) + row.valueDropdown = valueDropdown + + -- Delete Button (trash can icon with dark red background, matching design) + local deleteBtn = CreateFrame("Button", "AltSystemSkillDelete" .. index, row) + deleteBtn:SetSize(DELETE_WIDTH, DELETE_WIDTH) + deleteBtn:SetPoint("LEFT", valueDropdown, "RIGHT", 8, 0) + + + local deleteIcon = deleteBtn:CreateTexture(nil, "ARTWORK") + deleteIcon:SetSize(DELETE_WIDTH - 4, DELETE_WIDTH - 4) + deleteIcon:SetPoint("CENTER") + deleteIcon:SetTexture("Interface\\Buttons\\UI-GroupLoot-Pass-Up") + deleteBtn.icon = deleteIcon + + -- Shimmer animation on hover + local shimmer = deleteBtn:CreateTexture(nil, "OVERLAY") + shimmer:SetSize(DELETE_WIDTH - 4, DELETE_WIDTH - 4) + shimmer:SetPoint("CENTER") + shimmer:SetTexture("Interface\\Buttons\\UI-GroupLoot-Pass-Up") + shimmer:SetBlendMode("ADD") + shimmer:SetAlpha(0) + deleteBtn.shimmer = shimmer + + local shimmerAnim = shimmer:CreateAnimationGroup() + shimmerAnim:SetLooping("REPEAT") + local fadeIn = shimmerAnim:CreateAnimation("Alpha") + fadeIn:SetFromAlpha(0) + fadeIn:SetToAlpha(0.5) + fadeIn:SetDuration(0.5) + fadeIn:SetOrder(1) + local fadeOut = shimmerAnim:CreateAnimation("Alpha") + fadeOut:SetFromAlpha(0.5) + fadeOut:SetToAlpha(0) + fadeOut:SetDuration(0.5) + fadeOut:SetOrder(2) + deleteBtn.shimmerAnim = shimmerAnim + + deleteBtn:SetScript("OnEnter", function(self) + self.shimmer:SetAlpha(0) + self.shimmerAnim:Play() + end) + deleteBtn:SetScript("OnLeave", function(self) + self.shimmerAnim:Stop() + self.shimmer:SetAlpha(0) + end) + + row.deleteBtn = deleteBtn + + return row +end + +-- ─── Refresh all skill rows ───────────────────────────────────────────────── + +local function RefreshSkillRows() + -- Hide all existing row frames + for _, row in ipairs(skillRowFrames) do + row:Hide() + end + + local yPos = 0 + + for i, skillData in ipairs(editableSkills) do + local row = skillRowFrames[i] + if not row then + row = CreateSkillRowFrame(i) + skillRowFrames[i] = row + end + + -- Clear previous anchor points before repositioning + row:ClearAllPoints() + row:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", 0, -yPos) + row:SetPoint("TOPRIGHT", scrollChild, "TOPRIGHT", 0, -yPos) + + -- Populate name + row.nameBox:SetText(skillData.name or "") + row.nameBox:SetScript("OnTextChanged", function(self) + local idx = nil + for j, s in ipairs(editableSkills) do + if skillRowFrames[j] == row then + idx = j + break + end + end + if idx then + editableSkills[idx].name = self:GetText() + end + end) + + -- Setup level dropdown + local currentRowIndex = i + row.levelDropdown.label:SetText(skillData.level or "Novice") + row.levelDropdown:SetupMenu(function(owner, rootDescription) + for _, levelName in ipairs(AltSystem.Data.SkillLevelOrder) do + rootDescription:CreateRadio( + levelName, + function() + return editableSkills[currentRowIndex] and editableSkills[currentRowIndex].level == levelName + end, + function() + if editableSkills[currentRowIndex] then + editableSkills[currentRowIndex].level = levelName + editableSkills[currentRowIndex].value = AltSystem.Data:GetDefaultValueForLevel(levelName) + RefreshSkillRows() + end + end + ) + end + end) + + -- Setup value dropdown based on current level + row.valueDropdown.label:SetText(tostring(skillData.value or 1)) + local range = AltSystem.Data.SkillValueRanges[skillData.level] + if range then + row.valueDropdown:SetupMenu(function(owner, rootDescription) + for v = range.min, range.max do + rootDescription:CreateRadio( + tostring(v), + function() + return editableSkills[currentRowIndex] and editableSkills[currentRowIndex].value == v + end, + function() + if editableSkills[currentRowIndex] then + editableSkills[currentRowIndex].value = v + row.valueDropdown.label:SetText(tostring(v)) + end + end + ) + end + end) + end + + -- Bind delete button for current index + row.deleteBtn:SetScript("OnClick", function() + table.remove(editableSkills, currentRowIndex) + RefreshSkillRows() + end) + + row:Show() + yPos = yPos + ROW_HEIGHT + ROW_SPACING + end + + -- Reposition Add A Row button + if addRowButton then + addRowButton:ClearAllPoints() + addRowButton:SetPoint("TOPRIGHT", scrollChild, "TOPRIGHT", -PADDING, -(yPos + 4)) + end + + -- Update scroll child height + local totalHeight = yPos + ROW_HEIGHT + 20 -- extra space for Add A Row button + if scrollChild then + scrollChild:SetHeight(math.max(totalHeight, 1)) + end +end + +-- ─── Create Build Skills Content ──────────────────────────────────────────── + +function AltSystem:CreateBuildSkillsContent(parentFrame) + local contentWidth = parentFrame:GetWidth() or 692 + local contentHeight = parentFrame:GetHeight() or 444 + + local yPos = -PADDING + + -- Info text paragraph 1 + local info1 = parentFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal") + info1:SetPoint("TOPLEFT", parentFrame, "TOPLEFT", PADDING, yPos) + info1:SetPoint("TOPRIGHT", parentFrame, "TOPRIGHT", -PADDING, yPos) + info1:SetJustifyH("LEFT") + info1:SetText("All skills below are directly extracted from your TRP's characteristics sheet. You can view and edit them there at any time.") + info1:SetTextColor(0.9, 0.75, 0.2) + info1:SetWordWrap(true) + + yPos = yPos - (info1:GetStringHeight() or 16) - 10 + + -- Info text paragraph 2 + local info2 = parentFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal") + info2:SetPoint("TOPLEFT", parentFrame, "TOPLEFT", PADDING, yPos) + info2:SetPoint("TOPRIGHT", parentFrame, "TOPRIGHT", -PADDING, yPos) + info2:SetJustifyH("LEFT") + info2:SetText("This menu serves as an easy alternative for if you want to use this system fast without diving deep into understanding it and/or styling your TRP sheet at this point in time. All your changes made will not be saved to TRP up until you hit the specific button to do so.") + info2:SetTextColor(0.9, 0.75, 0.2) + info2:SetWordWrap(true) + + yPos = yPos - (info2:GetStringHeight() or 32) - 16 + + -- "Skill list" section header + local sectionHeader = parentFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge") + sectionHeader:SetPoint("TOPLEFT", parentFrame, "TOPLEFT", PADDING, yPos) + sectionHeader:SetText("Skill list") + --sectionHeader:SetTextColor(1, 1, 1) + + yPos = yPos - 24 + + -- Column headers + local nameHeader = parentFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal") + nameHeader:SetPoint("TOPLEFT", parentFrame, "TOPLEFT", PADDING + 8, yPos) + nameHeader:SetText("Name") + nameHeader:SetTextColor(0.9, 0.75, 0.2) + + local levelHeader = parentFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal") + levelHeader:SetPoint("TOPLEFT", parentFrame, "TOPLEFT", PADDING + 8 + NAME_WIDTH + 12, yPos) + levelHeader:SetText("Level") + levelHeader:SetTextColor(0.9, 0.75, 0.2) + + local valueHeader = parentFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal") + valueHeader:SetPoint("TOPLEFT", parentFrame, "TOPLEFT", PADDING + 8 + NAME_WIDTH + 12 + LEVEL_WIDTH + 8, yPos) + valueHeader:SetText("Value") + valueHeader:SetTextColor(0.9, 0.75, 0.2) + + yPos = yPos - 20 + + -- Save button (pinned to bottom, outside scroll frame) + local saveButton = AltSystem.CreateFlatButton("AltSystemSaveSkillsButton", parentFrame, 180, 30, "Save Skills to TRP") + saveButton:SetPoint("BOTTOM", parentFrame, "BOTTOM", 0, PADDING) + + saveButton:SetScript("OnClick", function() + local success = AltSystem.Data:SaveSkills(editableSkills) + if success and AltSystem.RefreshSkillDropdown then + AltSystem:RefreshSkillDropdown() + end + end) + + -- Scrollable skill list area (between column headers and save button) + scrollFrame = CreateFrame("ScrollFrame", "AltSystemBuildSkillsScrollFrame", parentFrame, "UIPanelScrollFrameTemplate") + scrollFrame:SetPoint("TOPLEFT", parentFrame, "TOPLEFT", PADDING, yPos) + scrollFrame:SetPoint("BOTTOMRIGHT", parentFrame, "BOTTOMRIGHT", -PADDING - 22, saveButton:GetHeight() + PADDING + 8) + + scrollChild = CreateFrame("Frame", "AltSystemBuildSkillsScrollChild", scrollFrame) + scrollChild:SetWidth(scrollFrame:GetWidth() or (contentWidth - PADDING * 2 - 22)) + scrollChild:SetHeight(1) + scrollFrame:SetScrollChild(scrollChild) + + -- Update scroll child width when frame resizes + scrollFrame:SetScript("OnSizeChanged", function(self) + scrollChild:SetWidth(self:GetWidth()) + end) + + -- "Add A Row" button (inside scroll child, below last row) + addRowButton = CreateFrame("Button", "AltSystemAddRowButton", scrollChild, "UIPanelButtonTemplate") + addRowButton:SetSize(130, 26) + addRowButton:SetText("+ Add A Row") + addRowButton:SetPoint("TOPRIGHT", scrollChild, "TOPRIGHT", -PADDING, 0) + + -- Style add row button + local addBg = addRowButton:CreateTexture(nil, "BACKGROUND") + addBg:SetAllPoints() + addBg:SetColorTexture(0.4, 0.08, 0.08, 0.9) + + addRowButton:SetScript("OnClick", function() + table.insert(editableSkills, { + name = "Skillname", + level = "Novice", + value = 1, + icon = "inv_misc_questionmark", + isNew = true, + }) + RefreshSkillRows() + + -- Auto-scroll to bottom to show the new row + C_Timer.After(0.05, function() + if scrollFrame then + scrollFrame:SetVerticalScroll(scrollFrame:GetVerticalScrollRange()) + end + end) + end) + + AltSystem.BuildSkillsScrollFrame = scrollFrame + AltSystem.BuildSkillsScrollChild = scrollChild +end + +-- ─── Refresh Build Skills list (called on tab switch) ─────────────────────── + +function AltSystem:RefreshBuildSkillsList() + -- Reload skills from TRP3 into working copy, discarding any unsaved edits + editableSkills = AltSystem.Data:GetEditableSkills() + RefreshSkillRows() +end diff --git a/Core.lua b/Core.lua old mode 100644 new mode 100755 index ead7cd8..865bf63 --- a/Core.lua +++ b/Core.lua @@ -4,9 +4,24 @@ AltSystem = AltSystem or {} AltSystem.State = { selectedSkillIndex = 1, + selectedSkillName = nil, -- skill name used to restore selection across sessions selectedItemIndex = 1, -- 1 = No item - selectedDefenseIndex = 1, -- 1 = Base armor + selectedDefenseIndex = 1, -- 1 = No Armor shieldEnabled = false, + petSummonEnabled = false, + announceEnabled = false, + announceChannelIndex = 1, -- 1 = Emote, 2 = Party, 3 = Raid, 4 = Guild + rollType = "attack", -- "attack" or "defense" + announceOptionIndex = 1, -- 1 = Self Roll, 2+ = channel from AnnounceChannels + rollLog = {}, -- array of { text = "...", timestamp = time() }, max 100, not persisted +} + +-- Channel definitions for announcing rolls +AltSystem.AnnounceChannels = { + { name = "Emote (/e)", channel = "EMOTE" }, + { name = "Party (/p)", channel = "PARTY" }, + { name = "Raid (/ra)", channel = "RAID" }, + { name = "Guild (/g)", channel = "GUILD" }, } -- Initialization on ADDON_LOADED @@ -21,12 +36,69 @@ frame:SetScript("OnEvent", function(self, event, arg1) end) function AltSystem:Init() + -- Load saved settings + AltSystemDB = AltSystemDB or {} + if AltSystemDB.petSummonEnabled ~= nil then + AltSystem.State.petSummonEnabled = AltSystemDB.petSummonEnabled + end + if AltSystemDB.shieldEnabled ~= nil then + AltSystem.State.shieldEnabled = AltSystemDB.shieldEnabled + end + if AltSystemDB.selectedItemIndex then + AltSystem.State.selectedItemIndex = AltSystemDB.selectedItemIndex + end + if AltSystemDB.selectedDefenseIndex then + AltSystem.State.selectedDefenseIndex = AltSystemDB.selectedDefenseIndex + end + if AltSystemDB.selectedSkillName then + AltSystem.State.selectedSkillName = AltSystemDB.selectedSkillName + end + if AltSystemDB.rollType then + AltSystem.State.rollType = AltSystemDB.rollType + end + -- Migrate from old announce settings or load new announceOptionIndex + if AltSystemDB.announceOptionIndex then + AltSystem.State.announceOptionIndex = AltSystemDB.announceOptionIndex + elseif AltSystemDB.announceEnabled ~= nil then + -- Backwards compat: derive from old announceEnabled + announceChannelIndex + if AltSystemDB.announceEnabled and AltSystemDB.announceChannelIndex then + AltSystem.State.announceOptionIndex = AltSystemDB.announceChannelIndex + 1 + else + AltSystem.State.announceOptionIndex = 1 + end + end + -- Derive announceEnabled and announceChannelIndex from announceOptionIndex + if AltSystem.State.announceOptionIndex > 1 then + AltSystem.State.announceEnabled = true + AltSystem.State.announceChannelIndex = AltSystem.State.announceOptionIndex - 1 + else + AltSystem.State.announceEnabled = false + AltSystem.State.announceChannelIndex = 1 + end + -- Register slash command /altsystem SLASH_ALTSYSTEM1 = "/altsystem" SlashCmdList["ALTSYSTEM"] = function() AltSystem:ToggleWindow() end + -- Save settings on logout + local saveFrame = CreateFrame("Frame") + saveFrame:RegisterEvent("PLAYER_LOGOUT") + saveFrame:SetScript("OnEvent", function() + AltSystemDB = AltSystemDB or {} + AltSystemDB.announceOptionIndex = AltSystem.State.announceOptionIndex + AltSystemDB.petSummonEnabled = AltSystem.State.petSummonEnabled + AltSystemDB.shieldEnabled = AltSystem.State.shieldEnabled + AltSystemDB.selectedItemIndex = AltSystem.State.selectedItemIndex + AltSystemDB.selectedDefenseIndex = AltSystem.State.selectedDefenseIndex + AltSystemDB.selectedSkillName = AltSystem.State.selectedSkillName + AltSystemDB.rollType = AltSystem.State.rollType + end) + + -- Build the main frame now that saved variables are loaded + AltSystem:CreateMainFrame() + print("|cff00ccffAltSystem|r loaded. Type /altsystem to open.") end diff --git a/Data.lua b/Data.lua old mode 100644 new mode 100755 index d95773d..44914b4 --- a/Data.lua +++ b/Data.lua @@ -6,44 +6,92 @@ AltSystem.Data = {} -- Skill levels and their modifiers AltSystem.Data.SkillLevels = { + ["Inept"] = -4, ["Novice"] = -2, ["Adept"] = 0, ["Expert"] = 2, ["Master"] = 4, } --- The "Unskilled" entry is always the first (default) skill -local UNSKILLED_ENTRY = { name = "Unskilled", level = "Unskilled", modifier = -4 } +-- The "Base roll" entry is always the first (default) skill +local BASE_ROLL_ENTRY = { name = "Base roll", level = "Base", modifier = 0 } +-- The "Unskilled" entry is always the last skill +local UNSKILLED_ENTRY = { name = "Unskilled", level = "Unskilled", modifier = -4 } -- Default/fallback skill list used when no TRP3 profile skills are found local DEFAULT_SKILLS = { - { name = "Unskilled", level = "Unskilled", modifier = -4 }, - { name = "Novice Skill", level = "Novice", modifier = -2 }, - { name = "Adept Skill", level = "Adept", modifier = 0 }, + { name = "Base roll", level = "Base", modifier = 0 }, + { name = "Novice Skill", level = "Novice", modifier = -2 }, + { name = "Adept Skill", level = "Adept", modifier = 0 }, { name = "Expert Skill", level = "Expert", modifier = 2 }, { name = "Master Skill", level = "Master", modifier = 4 }, } -- Valid skill level keywords that must appear in the trait's right field (RT) -local VALID_SKILL_KEYWORDS = { "Novice", "Adept", "Expert", "Master" } +local VALID_SKILL_KEYWORDS = { "Inept", "Novice", "Adept", "Expert", "Master" } --- Check if the trait's right text field contains a valid skill keyword. --- Returns true if any keyword is found, false otherwise. -local function HasSkillKeyword(rightText) - if not rightText or rightText == "" then return false end - for _, keyword in ipairs(VALID_SKILL_KEYWORDS) do - if rightText:find(keyword) then - return true - end +-- Expected numeric value ranges for each skill keyword +local SKILL_KEYWORD_RANGES = { + ["Novice"] = { min = 1, max = 5 }, + ["Adept"] = { min = 6, max = 10 }, + ["Expert"] = { min = 11, max = 19 }, + ["Master"] = { min = 20, max = 20 }, +} + +-- Shared lookup tables for the Build Skills tab +AltSystem.Data.SkillValueRanges = { + ["Inept"] = { min = 0, max = 0 }, + ["Novice"] = { min = 1, max = 5 }, + ["Adept"] = { min = 6, max = 10 }, + ["Expert"] = { min = 11, max = 19 }, + ["Master"] = { min = 20, max = 20 }, +} + +-- Ordered list of skill levels for dropdown display +AltSystem.Data.SkillLevelOrder = { "Inept", "Novice", "Adept", "Expert", "Master" } + +-- Returns the minimum value for a given skill level +function AltSystem.Data:GetDefaultValueForLevel(level) + local range = AltSystem.Data.SkillValueRanges[level] + if range then + return range.min end - return false + return 1 end --- Determine the skill level from the trait's numeric value (V2 field, 0-20 range). --- Returns the level string and modifier, or nil if the value is 0 or absent. -local function ParseSkillLevel(numericValue) - if not numericValue or numericValue <= 0 then return nil, nil end +-- Check if the trait's right text field contains a valid skill keyword. +-- Returns the matched keyword if found, nil otherwise. +local function FindSkillKeyword(rightText) + if not rightText or rightText == "" then return nil end + for _, keyword in ipairs(VALID_SKILL_KEYWORDS) do + if rightText:find(keyword) then + return keyword + end + end + return nil +end +-- Check whether the numeric value matches the expected range for the given keyword. +-- Returns a warning string if mismatched, or nil if everything is fine. +local function CheckSkillMismatch(keyword, numericValue) + if not keyword or keyword == "Inept" then return nil end + local range = SKILL_KEYWORD_RANGES[keyword] + if not range then return nil end + if not numericValue or numericValue <= 0 then return nil end + if numericValue < range.min or numericValue > range.max then + return "Skill is marked as " .. keyword .. " (" .. range.min .. "-" .. range.max .. "), but the profile has a value of " .. numericValue .. "." + end + return nil +end + +-- Determine the skill level from the trait's right text and numeric value (V2 field, 0-20 range). +-- If the trait is marked as Inept, always returns "Inept" with -4 modifier regardless of numeric value. +-- Otherwise returns the level string and modifier based on numeric value, or nil if the value is 0 or absent. +local function ParseSkillLevel(rightText, numericValue) + if rightText and rightText:find("Inept") then + return "Inept", AltSystem.Data.SkillLevels["Inept"] + end + if not numericValue or numericValue <= 0 then return nil, nil end if numericValue >= 20 then return "Master", AltSystem.Data.SkillLevels["Master"] elseif numericValue >= 11 then @@ -56,13 +104,13 @@ local function ParseSkillLevel(numericValue) end -- Fetch skills from the current TRP3 profile's personality traits. --- Returns an array of skill entries, always starting with "Unskilled". +-- Returns an array of skill entries, always starting with "Base roll". +-- Traits marked as Inept get a -4 modifier regardless of their numeric value. -- Falls back to the default list if no profile or no valid skills are found. function AltSystem.Data:RefreshSkills() local skills = {} - - -- Always add Unskilled as the first entry - table.insert(skills, { name = UNSKILLED_ENTRY.name, level = UNSKILLED_ENTRY.level, modifier = UNSKILLED_ENTRY.modifier }) + -- Always add Base roll as the first entry + table.insert(skills, { name = BASE_ROLL_ENTRY.name, level = BASE_ROLL_ENTRY.level, modifier = BASE_ROLL_ENTRY.modifier }) local foundAny = false @@ -74,14 +122,16 @@ function AltSystem.Data:RefreshSkills() local skillName = trait.LT local numericValue = trait.V2 - if skillName and skillName ~= "" and HasSkillKeyword(trait.RT) then - local level, modifier = ParseSkillLevel(numericValue) + local keyword = FindSkillKeyword(trait.RT) + if skillName and skillName ~= "" and keyword then + local level, modifier = ParseSkillLevel(trait.RT, numericValue) if level and modifier then foundAny = true table.insert(skills, { name = skillName, level = level, modifier = modifier, + warning = CheckSkillMismatch(keyword, numericValue), }) end end @@ -89,7 +139,7 @@ function AltSystem.Data:RefreshSkills() end end - -- If no valid skills were found, use the default fallback list (skip first "Unskilled" since we already added it) + -- If no valid skills were found, use the default fallback list if not foundAny then skills = {} for _, skill in ipairs(DEFAULT_SKILLS) do @@ -97,28 +147,119 @@ function AltSystem.Data:RefreshSkills() end end + -- Always add Unskilled as the last entry + table.insert(skills, { name = UNSKILLED_ENTRY.name, level = UNSKILLED_ENTRY.level, modifier = UNSKILLED_ENTRY.modifier }) + AltSystem.Data.Skills = skills return skills end +-- Returns an array of {name, level, value, icon} for each valid skill trait in the TRP3 profile. +-- Excludes Base Roll and Unskilled (system-generated). Sorted by level order (Inept first, Master last). +function AltSystem.Data:GetEditableSkills() + local skills = {} + + if TRP3_API and TRP3_API.profile and TRP3_API.profile.getData then + local ok, characteristics = pcall(TRP3_API.profile.getData, "player/characteristics") + if ok and characteristics and characteristics.PS then + for _, trait in ipairs(characteristics.PS) do + local skillName = trait.LT + local numericValue = trait.V2 or 0 + local keyword = FindSkillKeyword(trait.RT) + if skillName and skillName ~= "" and keyword then + table.insert(skills, { + name = skillName, + level = keyword, + value = numericValue, + icon = trait.IC or "inv_misc_questionmark", + }) + end + end + end + end + + -- Sort by level order descending (Master first, Inept last) + local levelOrderMap = {} + for i, lvl in ipairs(AltSystem.Data.SkillLevelOrder) do + levelOrderMap[lvl] = i + end + table.sort(skills, function(a, b) + return (levelOrderMap[a.level] or 99) > (levelOrderMap[b.level] or 99) + end) + + return skills +end + +-- Saves the edited skills back to the TRP3 profile. +-- editedSkills is an array of {name, level, value, icon}. +function AltSystem.Data:SaveSkills(editedSkills) + if not TRP3_API or not TRP3_API.profile or not TRP3_API.profile.getData then + print("|cFFFF0000AltSystem:|r TRP3 API is unavailable. Cannot save skills.") + return false + end + + local ok, characteristics = pcall(TRP3_API.profile.getData, "player/characteristics") + if not ok or not characteristics then + print("|cFFFF0000AltSystem:|r Could not access TRP3 profile characteristics. Cannot save skills.") + return false + end + + -- Ensure PS array exists + if not characteristics.PS then + characteristics.PS = {} + end + + -- Separate non-skill traits from skill traits in the existing PS + local nonSkillTraits = {} + for _, trait in ipairs(characteristics.PS) do + local keyword = FindSkillKeyword(trait.RT) + if not keyword then + -- This is not a skill trait, preserve it + table.insert(nonSkillTraits, trait) + end + end + + -- Rebuild PS: non-skill traits first, then edited skills + local newPS = {} + for _, trait in ipairs(nonSkillTraits) do + table.insert(newPS, trait) + end + for _, skill in ipairs(editedSkills) do + table.insert(newPS, { + LT = skill.name, + RT = skill.level, + V2 = skill.value, + IC = skill.icon or "inv_misc_questionmark", + }) + end + + characteristics.PS = newPS + + -- Refresh the Use Skills tab data + AltSystem.Data:RefreshSkills() + + print("|cFF00FF00AltSystem:|r Skills saved to TRP profile successfully.") + return true +end + -- Initialize with the default skill list AltSystem.Data.Skills = {} for _, skill in ipairs(DEFAULT_SKILLS) do table.insert(AltSystem.Data.Skills, { name = skill.name, level = skill.level, modifier = skill.modifier }) end --- Item options: name and modifier (first entry = no item) +-- Item options: name, label (used in roll messages), and modifier (first entry = no item) AltSystem.Data.Items = { - { name = "No item", modifier = 0 }, - { name = "Rare item", modifier = 3 }, - { name = "Epic item", modifier = 5 }, + { name = "No item", label = "No item", modifier = 0 }, + { name = "Rare", label = "Rare item", modifier = 3 }, + { name = "Epic", label = "Epic item", modifier = 5 }, } --- Defense options: name and modifier +-- Defense / Armor options: name, label (used in roll messages), and modifier AltSystem.Data.Defenses = { - { name = "Base armor", modifier = 0 }, - { name = "Extra small armor", modifier = 1 }, - { name = "Extra large armor", modifier = 2 }, + { name = "None", label = "No armor", modifier = 0 }, + { name = "Partial", label = "Extra armor", modifier = 1 }, + { name = "Full", label = "Extra armor", modifier = 2 }, } -- Shield modifier diff --git a/README.md b/README.md old mode 100644 new mode 100755 index 35b32f7..434c0a0 --- a/README.md +++ b/README.md @@ -6,6 +6,11 @@ The rolling system relies on TRP3 and TRP3:Enhanced addons, so these are explici - Main features defined in the /docs folder ## Installation + +1. Download the [latest release](https://git.asarius.site/rukira/AltSystem/releases/latest) ZIP file. +2. Extract the contents to your World of Warcraft AddOns folder. + +### Note To use AltSystem, ensure you have the following addons installed: - TRP3 - TRP3:Enhanced diff --git a/Roll.lua b/Roll.lua old mode 100644 new mode 100755 index 28f3752..036a680 --- a/Roll.lua +++ b/Roll.lua @@ -4,9 +4,119 @@ AltSystem = AltSystem or {} local pendingRollType = nil -- "attack" or "defense" +local pendingPetRoll = nil -- { rollType, mainRollValue } when awaiting pet/summon roll + +-- Get the TRP3 character first name, falling back to the WoW unit name +local function GetCharacterName() + if TRP3_API and TRP3_API.profile and TRP3_API.profile.getData then + local ok, characteristics = pcall(TRP3_API.profile.getData, "player/characteristics") + if ok and characteristics and characteristics.FN and characteristics.FN ~= "" then + return characteristics.FN + end + end + return UnitName("player") +end + +-- Build the modifier description string for the announce message +local function BuildModifierString(modifiers) + local parts = {} + for _, mod in ipairs(modifiers) do + local sign = mod.value >= 0 and "+" or "" + table.insert(parts, sign .. mod.value .. " (" .. mod.name .. ")") + end + return table.concat(parts, " ") +end + +-- Build the log message string (always uses the non-emote format with character name) +local function BuildLogMessage(rollValue, modifiers, total) + local name = GetCharacterName() + local modStr = BuildModifierString(modifiers) + if modStr ~= "" then + return name .. " rolled " .. rollValue .. " " .. modStr .. " = " .. total + else + return name .. " rolled " .. rollValue .. " = " .. total + end +end + +-- Build the critical roll log message +local function BuildCriticalLogMessage(isCriticalSuccess) + local name = GetCharacterName() + if isCriticalSuccess then + return name .. " rolled a Critical Success!" + else + return name .. " rolled a Critical Failure!" + end +end + +-- Add an entry to the roll log and refresh the UI +local function AddLogEntry(text) + local rollLog = AltSystem.State.rollLog + table.insert(rollLog, { text = text, timestamp = time() }) + -- Cap at 100 entries + if #rollLog > 100 then + table.remove(rollLog, 1) + end + -- Refresh the log panel if it exists + if AltSystem.RefreshLogPanel then + AltSystem:RefreshLogPanel() + end +end + +-- Send a message to the given channel. +local function SendToChannel(msg, channel) + SendChatMessage(msg, channel) +end + +-- Announce the roll result to the selected chat channel +local function AnnounceRoll(rollValue, modifiers, total) + if not AltSystem.State.announceEnabled then return end + + local channelDef = AltSystem.AnnounceChannels[AltSystem.State.announceChannelIndex] + if not channelDef then return end + + local isEmote = channelDef.channel == "EMOTE" + local modStr = BuildModifierString(modifiers) + + local msg + if isEmote then + if modStr ~= "" then + msg = "rolled " .. rollValue .. " " .. modStr .. " = " .. total + else + msg = "rolled " .. rollValue .. " = " .. total + end + else + local name = GetCharacterName() + if modStr ~= "" then + msg = name .. " rolled " .. rollValue .. " " .. modStr .. " = " .. total + else + msg = name .. " rolled " .. rollValue .. " = " .. total + end + end + + SendToChannel(msg, channelDef.channel) +end + +-- Announce a critical roll result +local function AnnounceCritical(isCriticalSuccess) + if not AltSystem.State.announceEnabled then return end + + local channelDef = AltSystem.AnnounceChannels[AltSystem.State.announceChannelIndex] + if not channelDef then return end + + local critText = isCriticalSuccess and "rolled a Critical Success!" or "rolled a Critical Failure!" + local msg + if channelDef.channel == "EMOTE" then + msg = critText + else + msg = GetCharacterName() .. " " .. critText + end + + SendToChannel(msg, channelDef.channel) +end -- Perform a roll: triggers the WoW native /roll 20 command function AltSystem:PerformRoll(rollType) + pendingPetRoll = nil pendingRollType = rollType RandomRoll(1, 20) end @@ -16,21 +126,55 @@ local rollListener = CreateFrame("Frame") rollListener:RegisterEvent("CHAT_MSG_SYSTEM") rollListener:SetScript("OnEvent", function(self, event, message) + -- Phase 2: waiting for pet/summon roll result (1-5) + if pendingPetRoll then + local petRoller, petRoll = message:match("(.+) rolls (%d+) %(1%-5%)") + if not petRoll then return end + if petRoller ~= UnitName("player") then return end + + local petValue = tonumber(petRoll) + local info = pendingPetRoll + pendingPetRoll = nil + + AltSystem:CalculateAndDisplayResult(info.rollType, info.mainRollValue, petValue) + return + end + + -- Phase 1: waiting for main roll result (1-20) if not pendingRollType then return end -- Match the roll result pattern: "PlayerName rolls X (1-20)" - local roll = message:match("rolls (%d+) %(1%-20%)") + local roller, roll = message:match("(.+) rolls (%d+) %(1%-20%)") if not roll then return end + if roller ~= UnitName("player") then return end local rollValue = tonumber(roll) local rollType = pendingRollType pendingRollType = nil + -- If pet/summon is enabled, trigger a second roll (1-5) + if AltSystem.State.petSummonEnabled then + pendingPetRoll = { rollType = rollType, mainRollValue = rollValue } + RandomRoll(1, 5) + return + end + AltSystem:CalculateAndDisplayResult(rollType, rollValue) end) -- Calculate the final result based on the roll type and selected modifiers -function AltSystem:CalculateAndDisplayResult(rollType, rollValue) +function AltSystem:CalculateAndDisplayResult(rollType, rollValue, petRollValue) + -- Critical rolls bypass normal calculation + if rollValue == 1 then + AddLogEntry(BuildCriticalLogMessage(false)) + AnnounceCritical(false) + return + elseif rollValue == 20 then + AddLogEntry(BuildCriticalLogMessage(true)) + AnnounceCritical(true) + return + end + local state = AltSystem.State local skill = AltSystem.Data.Skills[state.selectedSkillIndex] local item = AltSystem.Data.Items[state.selectedItemIndex] @@ -39,17 +183,26 @@ function AltSystem:CalculateAndDisplayResult(rollType, rollValue) local itemMod = item and item.modifier or 0 local total = rollValue - local breakdown = "Roll: " .. rollValue + + local modifiers = {} + + local isBaseRoll = skill and skill.name == "Base roll" + + local petMod = petRollValue or 0 if rollType == "attack" then - -- Attack Roll = roll + skill modifier + item modifier - total = rollValue + skillMod + itemMod + -- Attack Roll = roll + skill modifier + item modifier + pet/summon modifier + total = rollValue + skillMod + itemMod + petMod - breakdown = breakdown .. "\nSkill: " .. FormatModifier(skillMod) - if itemMod ~= 0 then - breakdown = breakdown .. " | Item: " .. FormatModifier(itemMod) + if not isBaseRoll then + table.insert(modifiers, { name = skill and skill.name or "Skill", value = skillMod }) + end + if itemMod ~= 0 then + table.insert(modifiers, { name = item and (item.label or item.name) or "Item", value = itemMod }) + end + if petMod ~= 0 then + table.insert(modifiers, { name = "Pet", value = petMod }) end - breakdown = breakdown .. "\n|cffffd100Attack Total: " .. total .. "|r" elseif rollType == "defense" then -- Defense Roll = roll + skill modifier + item modifier + defense modifier + shield modifier @@ -57,22 +210,30 @@ function AltSystem:CalculateAndDisplayResult(rollType, rollValue) local defenseMod = defense and defense.modifier or 0 local shieldMod = state.shieldEnabled and AltSystem.Data.ShieldModifier or 0 - total = rollValue + skillMod + itemMod + defenseMod + shieldMod + total = rollValue + skillMod + itemMod + defenseMod + shieldMod + petMod - breakdown = breakdown .. "\nSkill: " .. FormatModifier(skillMod) + if not isBaseRoll then + table.insert(modifiers, { name = skill and skill.name or "Skill", value = skillMod }) + end if itemMod ~= 0 then - breakdown = breakdown .. " | Item: " .. FormatModifier(itemMod) + table.insert(modifiers, { name = item and (item.label or item.name) or "Item", value = itemMod }) + end + if defenseMod ~= 0 then + table.insert(modifiers, { name = defense and (defense.label or defense.name) or "Armor", value = defenseMod }) end - breakdown = breakdown .. "\nDefense: " .. FormatModifier(defenseMod) if shieldMod ~= 0 then - breakdown = breakdown .. " | Shield: " .. FormatModifier(shieldMod) + table.insert(modifiers, { name = "Shield", value = shieldMod }) + end + if petMod ~= 0 then + table.insert(modifiers, { name = "Pet", value = petMod }) end - breakdown = breakdown .. "\n|cff00ccffDefense Total: " .. total .. "|r" end - if AltSystem.ResultText then - AltSystem.ResultText:SetText(breakdown) - end + -- Add to log (always, regardless of announce setting) + AddLogEntry(BuildLogMessage(rollValue, modifiers, total)) + + -- Announce to chat (if enabled) + AnnounceRoll(rollValue, modifiers, total) end -- Format a modifier value with sign diff --git a/UI.lua b/UI.lua old mode 100644 new mode 100755 index 5d64ea6..642d328 --- a/UI.lua +++ b/UI.lua @@ -1,14 +1,21 @@ -- AltSystem UI --- Creates the main dialog window with all interface elements. +-- Creates the main dialog window with tabbed layout. +-- The "Use Skills" tab contains controls on the left and a Log panel on the right. -- Uses the modern DropdownButton API (WoW 10.2.5+ / 12.0+). AltSystem = AltSystem or {} -local WINDOW_WIDTH = 300 -local WINDOW_HEIGHT = 380 -local PADDING = 12 -local ROW_HEIGHT = 30 -local LABEL_WIDTH = 80 +local WINDOW_WIDTH = 720 +local WINDOW_HEIGHT = 520 +local CONTROLS_WIDTH = 360 +local LOG_WIDTH = 360 +local PADDING = 12 +local PADDING_HEADER = 6 +local ROW_HEIGHT = 26 +local LABEL_HEIGHT = 14 -- approximate height of GameFontNormal text +local LABEL_GAP = 4 -- gap between a sub-label and its input +local ITEM_GAP = 10 -- gap between inputs within the same section +local SECTION_GAP = 22 -- gap between major sections -- Helper: Build the skill option list from current AltSystem.Data.Skills local function BuildSkillOptions() @@ -16,7 +23,7 @@ local function BuildSkillOptions() for _, skill in ipairs(AltSystem.Data.Skills) do local sign = skill.modifier >= 0 and "+" or "" local text - if skill.level == "Unskilled" then + if skill.level == "Base" or skill.level == "Inept" then text = skill.name .. " (" .. sign .. skill.modifier .. ")" else text = skill.name .. " (" .. skill.level .. " " .. sign .. skill.modifier .. ")" @@ -26,39 +33,144 @@ local function BuildSkillOptions() return options end --- Helper: Create a modern dropdown (WowStyle1DropdownTemplate) -local function CreateDropdown(parent, name, yOffset, labelText, options, defaultIndex, onSelect) - local label = parent:CreateFontString(nil, "OVERLAY", "GameFontNormal") - label:SetPoint("TOPLEFT", parent, "TOPLEFT", PADDING, yOffset) - label:SetText(labelText) - label:SetWidth(LABEL_WIDTH) - label:SetJustifyH("LEFT") +-- Helper: Create a flat dark dropdown with label on top (reuses shared CreateFlatDropdown) +local function CreateDropdown(parent, name, labelText, options, defaultIndex, onSelect, labelFont) + local hasLabel = labelText and labelText ~= "" + local containerHeight = hasLabel and (LABEL_HEIGHT + LABEL_GAP + 28) or ROW_HEIGHT + + local container = CreateFrame("Frame", nil, parent) + container:SetHeight(containerHeight) + + local label + if hasLabel then + label = container:CreateFontString(nil, "OVERLAY", labelFont or "GameFontNormal") + label:SetPoint("TOPLEFT", container, "TOPLEFT", 0, 0) + label:SetText(labelText) + label:SetJustifyH("LEFT") + label:SetTextColor(0.9, 0.75, 0.2) + end local selectedIndex = defaultIndex or 1 - local dropdown = CreateFrame("DropdownButton", name, parent, "WowStyle1DropdownTemplate") - dropdown:SetPoint("LEFT", label, "RIGHT", 4, 0) - dropdown:SetWidth(160) + local dropdown = AltSystem.CreateFlatDropdown(name, container, 190) + if hasLabel then + dropdown:SetPoint("TOPLEFT", container, "TOPLEFT", 0, -(LABEL_HEIGHT + LABEL_GAP)) + dropdown:SetPoint("TOPRIGHT", container, "TOPRIGHT", 0, -(LABEL_HEIGHT + LABEL_GAP)) + else + dropdown:SetPoint("LEFT", container, "LEFT", 0, 0) + dropdown:SetPoint("RIGHT", container, "RIGHT", 0, 0) + end - dropdown:SetupMenu(function(dropdown, rootDescription) + -- Set initial label text + if options[selectedIndex] then + dropdown.label:SetText(options[selectedIndex].text) + end + + dropdown:SetupMenu(function(owner, rootDescription) for i, option in ipairs(options) do rootDescription:CreateRadio( - option.text, - function(data) return data == selectedIndex end, - function(data) - selectedIndex = data - if onSelect then onSelect(data, options[data]) end - end, - i + option.text, + function() + return i == selectedIndex + end, + function() + selectedIndex = i + dropdown.label:SetText(option.text) + if onSelect then + onSelect(i, option) + end + end ) end end) - return dropdown, function() return selectedIndex end, function(idx) selectedIndex = idx end + return container, dropdown, function() + return selectedIndex + end, function(idx) + selectedIndex = idx + if options[idx] then + dropdown.label:SetText(options[idx].text) + end + end +end + +-- Helper: Create a custom flat radio button (gold circle when selected, grey when not) +local function CreateRadioButton(parent, name, text, x, y, isChecked, onClick) + local size = 20 + local btn = CreateFrame("CheckButton", name, parent) + btn:SetSize(size, size) + btn:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y) + + -- Outer grey circle (always visible, acts as border) + local border = btn:CreateTexture(nil, "BACKGROUND") + border:SetAllPoints() + border:SetColorTexture(0.3, 0.3, 0.3, 1) + local borderMask = btn:CreateMaskTexture() + borderMask:SetAllPoints() + borderMask:SetTexture("Interface\\CharacterFrame\\TempPortraitAlphaMask", "CLAMPTOBLACKADDITIVE", "CLAMPTOBLACKADDITIVE") + border:AddMaskTexture(borderMask) + + -- Inner yellow circle (shown when selected, slightly smaller to reveal grey border) + local inner = btn:CreateTexture(nil, "BORDER") + local inset = 3 + inner:SetPoint("TOPLEFT", btn, "TOPLEFT", inset, -inset) + inner:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", -inset, inset) + inner:SetColorTexture(0.9, 0.75, 0.2, 1) + local innerMask = btn:CreateMaskTexture() + innerMask:SetPoint("TOPLEFT", btn, "TOPLEFT", inset, -inset) + innerMask:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", -inset, inset) + innerMask:SetTexture("Interface\\CharacterFrame\\TempPortraitAlphaMask", "CLAMPTOBLACKADDITIVE", "CLAMPTOBLACKADDITIVE") + inner:AddMaskTexture(innerMask) + inner:Hide() + + btn.checkTex = nil -- unused, kept for compatibility + + local function UpdateVisual() + if btn:GetChecked() then + inner:Show() + else + inner:Hide() + end + end + + btn:SetChecked(isChecked) + UpdateVisual() + + local label = btn:CreateFontString(nil, "OVERLAY", "GameFontHighlight") + label:SetPoint("LEFT", btn, "RIGHT", 6, 0) + label:SetText(text) + + btn:SetScript("OnClick", function(self) + onClick(self) + UpdateVisual() + end) + + btn.UpdateVisual = UpdateVisual + return btn +end + +-- Helper: Create a section header (golden text) +local function CreateSectionHeader(parent, text, x, y) + local header = parent:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge") + header:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y) + header:SetText(text) + header:SetTextColor(0.9, 0.75, 0.2) + return header +end + +-- Helper: Create a sub-label (smaller golden text) +local function CreateSubLabel(parent, text, x, y) + local label = parent:CreateFontString(nil, "OVERLAY", "GameFontNormal") + label:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y) + label:SetText(text) + label:SetTextColor(0.9, 0.75, 0.2) + return label end function AltSystem:CreateMainFrame() - if AltSystem.MainFrame then return end + if AltSystem.MainFrame then + return + end -- Main frame local f = CreateFrame("Frame", "AltSystemMainFrame", UIParent, "BasicFrameTemplateWithInset") @@ -76,166 +188,508 @@ function AltSystem:CreateMainFrame() f.title:SetPoint("TOPLEFT", f.TitleBg, "TOPLEFT", 5, -3) f.title:SetText("AltSystem") - -- Track current Y offset for layout - local yPos = -40 + --------------------- + -- TAB BUTTONS (span full window width) + --------------------- + local contentTop = -24 + local tabHeight = 36 + local contentWidth = WINDOW_WIDTH - 8 -- 4px inset on each side + local tabWidth = contentWidth / 2 + + local tabUseSkills = CreateFrame("Button", "AltSystemTabUseSkills", f) + tabUseSkills:SetSize(tabWidth, tabHeight) + tabUseSkills:SetPoint("TOPLEFT", f, "TOPLEFT", 4, contentTop) + tabUseSkills:SetNormalFontObject("GameFontNormalLarge") + tabUseSkills:SetHighlightFontObject("GameFontNormalLarge") + tabUseSkills:SetText("Use Skills") + + local tabUseSkillsBg = tabUseSkills:CreateTexture(nil, "BACKGROUND") + tabUseSkillsBg:SetAllPoints() + tabUseSkillsBg:SetColorTexture(0, 0, 0, 0) + + local tabUseSkillsText = tabUseSkills:GetFontString() + tabUseSkillsText:SetTextColor(0.9, 0.75, 0.2, 1) + + local tabBuildSkills = CreateFrame("Button", "AltSystemTabBuildSkills", f) + tabBuildSkills:SetSize(tabWidth, tabHeight) + tabBuildSkills:SetPoint("TOPLEFT", tabUseSkills, "TOPRIGHT", 0, 0) + tabBuildSkills:SetNormalFontObject("GameFontNormalLarge") + tabBuildSkills:SetHighlightFontObject("GameFontNormalLarge") + tabBuildSkills:SetText("Build Skills") + + local tabBuildSkillsBg = tabBuildSkills:CreateTexture(nil, "BACKGROUND") + tabBuildSkillsBg:SetAllPoints() + tabBuildSkillsBg:SetColorTexture(0.3, 0.3, 0.3, 1) + + local tabBuildSkillsText = tabBuildSkills:GetFontString() + tabBuildSkillsText:SetTextColor(1, 1, 1, 1) + + --------------------- + -- TAB CONTENT FRAMES + --------------------- + local tabContentTop = contentTop - tabHeight + local tabContentHeight = WINDOW_HEIGHT - 28 - tabHeight + + local useSkillsContent = CreateFrame("Frame", "AltSystemUseSkillsContent", f) + useSkillsContent:SetPoint("TOPLEFT", f, "TOPLEFT", 4, tabContentTop) + useSkillsContent:SetSize(contentWidth, tabContentHeight) + + local buildSkillsContent = CreateFrame("Frame", "AltSystemBuildSkillsContent", f) + buildSkillsContent:SetPoint("TOPLEFT", f, "TOPLEFT", 4, tabContentTop) + buildSkillsContent:SetSize(contentWidth, tabContentHeight) + buildSkillsContent:Hide() + + -- Build Skills tab content (created in BuildSkillsUI.lua) + AltSystem:CreateBuildSkillsContent(buildSkillsContent) + + -- Tab switching logic + local function SelectTab(tabIndex) + if tabIndex == 1 then + useSkillsContent:Show() + buildSkillsContent:Hide() + tabUseSkillsBg:SetColorTexture(0, 0, 0, 0) + tabBuildSkillsBg:SetColorTexture(0.3, 0.3, 0.3, 1) + tabUseSkillsText:SetTextColor(0.9, 0.75, 0.2, 1) + tabBuildSkillsText:SetTextColor(1, 1, 1, 1) + else + useSkillsContent:Hide() + buildSkillsContent:Show() + tabUseSkillsBg:SetColorTexture(0.3, 0.3, 0.3, 1) + tabBuildSkillsBg:SetColorTexture(0, 0, 0, 0) + tabUseSkillsText:SetTextColor(1, 1, 1, 1) + tabBuildSkillsText:SetTextColor(0.9, 0.75, 0.2, 1) + AltSystem:RefreshBuildSkillsList() + end + end + + tabUseSkills:SetScript("OnClick", function() + SelectTab(1) + end) + tabBuildSkills:SetScript("OnClick", function() + SelectTab(2) + end) + + --------------------- + -- USE SKILLS TAB CONTENT (left: controls, right: log) + --------------------- + local controlsPanel = CreateFrame("Frame", nil, useSkillsContent) + controlsPanel:SetPoint("TOPLEFT", useSkillsContent, "TOPLEFT", 0, 0) + controlsPanel:SetSize(CONTROLS_WIDTH, tabContentHeight) + + local content = controlsPanel + local yPos = -PADDING + + -- Section: Define Your Base Roll + CreateSectionHeader(content, "Base Roll", PADDING_HEADER, yPos) + yPos = yPos - 26 + + -- Roll Type label + CreateSubLabel(content, "Roll Type", PADDING, yPos) + yPos = yPos - (LABEL_HEIGHT + LABEL_GAP) + + -- Roll Type radio buttons + local attackRadio, defenseRadio + + local function UpdateRollTypeSelection(rollType) + AltSystem.State.rollType = rollType + attackRadio:SetChecked(rollType == "attack") + defenseRadio:SetChecked(rollType == "defense") + attackRadio.UpdateVisual() + defenseRadio.UpdateVisual() + -- Update roll button text + if AltSystem.RollButton then + local label = rollType == "attack" and "Roll Attack" or "Roll Defense" + AltSystem.RollButton:SetText(label) + end + end + + attackRadio = CreateRadioButton(content, "AltSystemAttackRadio", "Attack Roll", PADDING, yPos, + AltSystem.State.rollType == "attack", + function() + UpdateRollTypeSelection("attack") + end) + + defenseRadio = CreateRadioButton(content, "AltSystemDefenseRadio", "Defense Roll", CONTROLS_WIDTH / 2, yPos, + AltSystem.State.rollType == "defense", + function() + UpdateRollTypeSelection("defense") + end) + + yPos = yPos - ROW_HEIGHT - ITEM_GAP - ------------------------- -- Skill dropdown - ------------------------- local skillOptions = BuildSkillOptions() + local UpdateSkillWarning -- forward declaration - local skillDropdown, getSkillIndex, setSkillIndex = CreateDropdown( - f, "AltSystemSkillDropdown", yPos, "Skill:", skillOptions, - AltSystem.State.selectedSkillIndex, - function(index) - AltSystem.State.selectedSkillIndex = index - end) + local skillContainer, skillDropdown, getSkillIndex, setSkillIndex = CreateDropdown( + content, "AltSystemSkillDropdown", "Skill", skillOptions, + AltSystem.State.selectedSkillIndex, + function(index) + AltSystem.State.selectedSkillIndex = index + AltSystem.State.selectedSkillName = AltSystem.Data.Skills[index] and AltSystem.Data.Skills[index].name or nil + UpdateSkillWarning(index) + end, + "GameFontNormal" + ) + skillContainer:SetPoint("TOPLEFT", content, "TOPLEFT", PADDING, yPos) + skillContainer:SetWidth(CONTROLS_WIDTH - PADDING * 2) + + -- Warning icon for skill mismatch + local skillWarning = CreateFrame("Frame", nil, skillContainer) + skillWarning:SetSize(20, 20) + skillWarning:SetPoint("LEFT", skillDropdown, "RIGHT", 4, 0) + + local skillWarningIcon = skillWarning:CreateTexture(nil, "ARTWORK") + skillWarningIcon:SetAllPoints() + skillWarningIcon:SetAtlas("services-icon-warning") + + skillWarning:SetScript("OnEnter", function(self) + if self.tooltipText then + GameTooltip:SetOwner(self, "ANCHOR_RIGHT") + GameTooltip:SetText(self.tooltipText, 1, 0.82, 0) + GameTooltip:Show() + end + end) + skillWarning:SetScript("OnLeave", function() + GameTooltip:Hide() + end) + skillWarning:Hide() + + UpdateSkillWarning = function(index) + local skill = AltSystem.Data.Skills[index] + if skill and skill.warning then + skillWarning.tooltipText = skill.warning + skillWarning:Show() + else + skillWarning:Hide() + end + end -- Store references for refreshing AltSystem.SkillDropdown = skillDropdown AltSystem.GetSkillIndex = getSkillIndex AltSystem.SetSkillIndex = setSkillIndex - yPos = yPos - ROW_HEIGHT - 8 + AltSystem.UpdateSkillWarning = UpdateSkillWarning - ------------------------- - -- Item dropdown - ------------------------- - local itemOptions = {} - for _, item in ipairs(AltSystem.Data.Items) do - local sign = item.modifier >= 0 and "+" or "" - local modText = item.modifier ~= 0 and (" (" .. sign .. item.modifier .. ")") or "" - table.insert(itemOptions, { - text = item.name .. modText, - }) + yPos = yPos - (LABEL_HEIGHT + LABEL_GAP + 28) - ITEM_GAP + + -- Armor label + CreateSubLabel(content, "Extra Armor", PADDING, yPos) + yPos = yPos - (LABEL_HEIGHT + LABEL_GAP) + + -- Armor radio buttons + local armorRadios = {} + + local function UpdateArmorSelection(index) + AltSystem.State.selectedDefenseIndex = index + for i, radio in ipairs(armorRadios) do + radio:SetChecked(i == index) + radio.UpdateVisual() + end end - CreateDropdown(f, "AltSystemItemDropdown", yPos, "Item:", itemOptions, AltSystem.State.selectedItemIndex, - function(index) - AltSystem.State.selectedItemIndex = index - end) - yPos = yPos - ROW_HEIGHT - 8 - - ------------------------- - -- Defense dropdown - ------------------------- - local defenseOptions = {} - for _, def in ipairs(AltSystem.Data.Defenses) do - local sign = def.modifier >= 0 and "+" or "" - local modText = " (" .. sign .. def.modifier .. ")" - table.insert(defenseOptions, { - text = def.name .. modText, - }) + local armorX = PADDING + for i, def in ipairs(AltSystem.Data.Defenses) do + local text = def.name + if def.modifier > 0 then + text = text .. " (+" .. def.modifier .. ")" + end + local radio = CreateRadioButton(content, "AltSystemArmorRadio" .. i, text, armorX, yPos, + AltSystem.State.selectedDefenseIndex == i, + function() + UpdateArmorSelection(i) + end) + table.insert(armorRadios, radio) + armorX = armorX + 110 end - CreateDropdown(f, "AltSystemDefenseDropdown", yPos, "Defense:", defenseOptions, AltSystem.State.selectedDefenseIndex, - function(index) - AltSystem.State.selectedDefenseIndex = index - end) - yPos = yPos - ROW_HEIGHT - 8 + yPos = yPos - ROW_HEIGHT - SECTION_GAP - ------------------------- - -- Shield checkbox - ------------------------- - local shieldLabel = f:CreateFontString(nil, "OVERLAY", "GameFontNormal") - shieldLabel:SetPoint("TOPLEFT", f, "TOPLEFT", PADDING, yPos) - shieldLabel:SetText("Shield:") - shieldLabel:SetWidth(LABEL_WIDTH) - shieldLabel:SetJustifyH("LEFT") + -- Section: Modifiers (optional) + CreateSectionHeader(content, "Modifiers (optional)", PADDING_HEADER, yPos) + yPos = yPos - 26 + --CreateSubLabel(content, "Label", PADDING, yPos) + --yPos = yPos - 22 - local shieldCheck = CreateFrame("CheckButton", "AltSystemShieldCheck", f, "UICheckButtonTemplate") - shieldCheck:SetPoint("LEFT", shieldLabel, "RIGHT", -6, 0) + -- Shield checkbox (flat square toggle) + local shieldCheck = CreateFrame("CheckButton", "AltSystemShieldCheck", content) + shieldCheck:SetSize(20, 20) + shieldCheck:SetPoint("TOPLEFT", content, "TOPLEFT", PADDING, yPos) shieldCheck:SetChecked(AltSystem.State.shieldEnabled) - local shieldText = shieldCheck:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall") - shieldText:SetPoint("LEFT", shieldCheck, "RIGHT", 2, 0) - shieldText:SetText("+1 modifier") + local shieldBg = shieldCheck:CreateTexture(nil, "BACKGROUND") + shieldBg:SetAllPoints() + shieldBg:SetColorTexture(0.3, 0.3, 0.3, 1) + + local shieldCheckMark = shieldCheck:CreateTexture(nil, "ARTWORK") + shieldCheckMark:SetSize(14, 14) + shieldCheckMark:SetPoint("CENTER") + shieldCheckMark:SetTexture("Interface\\RAIDFRAME\\ReadyCheck-Ready") + shieldCheckMark:SetVertexColor(0.9, 0.75, 0.2, 1) + + local function UpdateShieldVisual() + if shieldCheck:GetChecked() then + shieldCheckMark:Show() + shieldBg:SetColorTexture(0.25, 0.25, 0.25, 1) + else + shieldCheckMark:Hide() + shieldBg:SetColorTexture(0.3, 0.3, 0.3, 1) + end + end + UpdateShieldVisual() + + local shieldLabel = shieldCheck:CreateFontString(nil, "OVERLAY", "GameFontHighlight") + shieldLabel:SetPoint("LEFT", shieldCheck, "RIGHT", 6, 0) + shieldLabel:SetText("Shield (+ 1)") shieldCheck:SetScript("OnClick", function(self) AltSystem.State.shieldEnabled = self:GetChecked() + UpdateShieldVisual() end) - yPos = yPos - ROW_HEIGHT - 12 + -- Pet checkbox (flat square toggle) + local petCheck = CreateFrame("CheckButton", "AltSystemPetSummonCheck", content) + petCheck:SetSize(20, 20) + petCheck:SetPoint("LEFT", shieldCheck, "RIGHT", 80, 0) + petCheck:SetChecked(AltSystem.State.petSummonEnabled) - ------------------------- - -- Roll buttons - ------------------------- - local btnWidth = (WINDOW_WIDTH - PADDING * 3) / 2 + local petBg = petCheck:CreateTexture(nil, "BACKGROUND") + petBg:SetAllPoints() + petBg:SetColorTexture(0.3, 0.3, 0.3, 1) - local attackBtn = CreateFrame("Button", "AltSystemAttackRollBtn", f, "UIPanelButtonTemplate") - attackBtn:SetSize(btnWidth, 28) - attackBtn:SetPoint("TOPLEFT", f, "TOPLEFT", PADDING, yPos) - attackBtn:SetText("Attack/Skill Roll") + local petCheckMark = petCheck:CreateTexture(nil, "ARTWORK") + petCheckMark:SetSize(14, 14) + petCheckMark:SetPoint("CENTER") + petCheckMark:SetTexture("Interface\\RAIDFRAME\\ReadyCheck-Ready") + petCheckMark:SetVertexColor(0.9, 0.75, 0.2, 1) - local defenseBtn = CreateFrame("Button", "AltSystemDefenseRollBtn", f, "UIPanelButtonTemplate") - defenseBtn:SetSize(btnWidth, 28) - defenseBtn:SetPoint("TOPRIGHT", f, "TOPRIGHT", -PADDING, yPos) - defenseBtn:SetText("Defense Roll") + local function UpdatePetVisual() + if petCheck:GetChecked() then + petCheckMark:Show() + petBg:SetColorTexture(0.25, 0.25, 0.25, 1) + else + petCheckMark:Hide() + petBg:SetColorTexture(0.3, 0.3, 0.3, 1) + end + end + UpdatePetVisual() - yPos = yPos - 40 + local petLabel = petCheck:CreateFontString(nil, "OVERLAY", "GameFontHighlight") + petLabel:SetPoint("LEFT", petCheck, "RIGHT", 6, 0) + petLabel:SetText("Pet (+d5)") - ------------------------- - -- Roll result area - ------------------------- - local resultBg = CreateFrame("Frame", nil, f, "InsetFrameTemplate") - resultBg:SetPoint("TOPLEFT", f, "TOPLEFT", PADDING, yPos) - resultBg:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -PADDING, PADDING) - - local resultText = resultBg:CreateFontString(nil, "OVERLAY", "GameFontHighlightLarge") - resultText:SetPoint("CENTER") - resultText:SetText("Roll result will appear here") - resultText:SetJustifyH("CENTER") - resultText:SetWidth(resultBg:GetWidth() - 10) - - AltSystem.ResultText = resultText - - -- Button click handlers - attackBtn:SetScript("OnClick", function() - AltSystem:PerformRoll("attack") + petCheck:SetScript("OnClick", function(self) + AltSystem.State.petSummonEnabled = self:GetChecked() + UpdatePetVisual() end) - defenseBtn:SetScript("OnClick", function() - AltSystem:PerformRoll("defense") + yPos = yPos - ROW_HEIGHT - ITEM_GAP + + -- Item label + CreateSubLabel(content, "Item", PADDING, yPos) + yPos = yPos - (LABEL_HEIGHT + LABEL_GAP) + + -- Item radio buttons + local itemRadios = {} + + local function UpdateItemSelection(index) + AltSystem.State.selectedItemIndex = index + for i, radio in ipairs(itemRadios) do + radio:SetChecked(i == index) + radio.UpdateVisual() + end + end + + local itemX = PADDING + for i, item in ipairs(AltSystem.Data.Items) do + local text = item.name + if item.modifier > 0 then + text = text .. " (+" .. item.modifier .. ")" + end + local radio = CreateRadioButton(content, "AltSystemItemRadio" .. i, text, itemX, yPos, + AltSystem.State.selectedItemIndex == i, + function() + UpdateItemSelection(i) + end) + table.insert(itemRadios, radio) + itemX = itemX + 110 + end + + yPos = yPos - ROW_HEIGHT - SECTION_GAP + + -- Section: Roll Dice + CreateSectionHeader(content, "Roll Mode", PADDING_HEADER, yPos) + yPos = yPos - 26 + + -- Announce Roll dropdown (Self Roll + channels) + local announceOptions = { { text = "Self Roll" } } + for _, ch in ipairs(AltSystem.AnnounceChannels) do + table.insert(announceOptions, { text = ch.name }) + end + + local announceContainer, announceDropdown = CreateDropdown( + content, "AltSystemAnnounceDropdown", "", announceOptions, + AltSystem.State.announceOptionIndex, + function(index) + AltSystem.State.announceOptionIndex = index + if index > 1 then + AltSystem.State.announceEnabled = true + AltSystem.State.announceChannelIndex = index - 1 + else + AltSystem.State.announceEnabled = false + AltSystem.State.announceChannelIndex = 1 + end + end, + "GameFontNormal") + announceContainer:SetPoint("TOPLEFT", content, "TOPLEFT", PADDING, yPos) + announceContainer:SetWidth(CONTROLS_WIDTH - PADDING * 2) + + yPos = yPos - ROW_HEIGHT - ITEM_GAP + + -- Roll button + local rollLabel = AltSystem.State.rollType == "attack" and "Roll Attack" or "Roll Defense" + local rollBtn = AltSystem.CreateFlatButton("AltSystemRollBtn", content, CONTROLS_WIDTH - PADDING * 2, 32, rollLabel) + rollBtn:SetPoint("TOPLEFT", content, "TOPLEFT", PADDING, yPos) + + rollBtn:SetScript("OnClick", function() + AltSystem:PerformRoll(AltSystem.State.rollType) end) + AltSystem.RollButton = rollBtn + + --------------------- + -- LOG PANEL (right side of Use Skills tab) + --------------------- + local logPanel = CreateFrame("Frame", nil, useSkillsContent) + logPanel:SetPoint("TOPLEFT", useSkillsContent, "TOPLEFT", CONTROLS_WIDTH, 0) + logPanel:SetSize(LOG_WIDTH, tabContentHeight) + + -- Log header + local logHeader = CreateSectionHeader(logPanel, "Log", 0, -PADDING) + + -- Log scroll area background + local logBg = CreateFrame("Frame", nil, logPanel, "InsetFrameTemplate") + logBg:SetPoint("TOPLEFT", logPanel, "TOPLEFT", 4, -38) + logBg:SetPoint("BOTTOMRIGHT", logPanel, "BOTTOMRIGHT", -PADDING, 4) + + -- Scroll frame for log entries + local scrollFrame = CreateFrame("ScrollFrame", "AltSystemLogScrollFrame", logBg, "UIPanelScrollFrameTemplate") + scrollFrame:SetPoint("TOPLEFT", logBg, "TOPLEFT", 6, -6) + scrollFrame:SetPoint("BOTTOMRIGHT", logBg, "BOTTOMRIGHT", -28, 6) + + local scrollChild = CreateFrame("Frame", "AltSystemLogScrollChild", scrollFrame) + scrollChild:SetWidth(scrollFrame:GetWidth() or (LOG_WIDTH - 50)) + scrollChild:SetHeight(1) -- Will be updated dynamically + scrollFrame:SetScrollChild(scrollChild) + + AltSystem.LogScrollFrame = scrollFrame + AltSystem.LogScrollChild = scrollChild + -- Refresh skills from TRP3 profile each time the window is shown f:SetScript("OnShow", function() AltSystem:RefreshSkillDropdown() + AltSystem:RefreshLogPanel() end) f:Hide() AltSystem.MainFrame = f end +-- Refresh the log panel UI from AltSystem.State.rollLog +function AltSystem:RefreshLogPanel() + local scrollChild = AltSystem.LogScrollChild + if not scrollChild then + return + end + + -- Remove existing log entry fontstrings + if scrollChild.entries then + for _, entry in ipairs(scrollChild.entries) do + entry:Hide() + entry:SetText("") + end + end + scrollChild.entries = scrollChild.entries or {} + + local rollLog = AltSystem.State.rollLog + local entryHeight = 16 + local spacing = 4 + local yPos = 0 + local childWidth = AltSystem.LogScrollFrame:GetWidth() or 250 + + -- Entries are newest-first + for i = #rollLog, 1, -1 do + local idx = #rollLog - i + 1 + local logEntry = rollLog[i] + local fontStr = scrollChild.entries[idx] + + if not fontStr then + fontStr = scrollChild:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall") + scrollChild.entries[idx] = fontStr + end + + fontStr:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", 2, -yPos) + fontStr:SetWidth(childWidth - 4) + fontStr:SetJustifyH("LEFT") + fontStr:SetText(logEntry.text) + fontStr:Show() + + yPos = yPos + entryHeight + spacing + end + + scrollChild:SetHeight(math.max(yPos, 1)) +end + -- Refresh the skill dropdown with current TRP3 profile data function AltSystem:RefreshSkillDropdown() AltSystem.Data:RefreshSkills() - -- Reset selection to 1 (Unskilled) since the skill list may have changed - AltSystem.State.selectedSkillIndex = 1 + -- Try to restore the previously selected skill by name; fall back to 1 (Base roll) + local newIndex = 1 + local savedName = AltSystem.State.selectedSkillName + if savedName then + for i, skill in ipairs(AltSystem.Data.Skills) do + if skill.name == savedName then + newIndex = i + break + end + end + end + AltSystem.State.selectedSkillIndex = newIndex + AltSystem.State.selectedSkillName = AltSystem.Data.Skills[newIndex] and AltSystem.Data.Skills[newIndex].name or nil if AltSystem.SetSkillIndex then - AltSystem.SetSkillIndex(1) + AltSystem.SetSkillIndex(newIndex) + end + if AltSystem.UpdateSkillWarning then + AltSystem.UpdateSkillWarning(newIndex) end -- Rebuild the dropdown menu with the new skill list if AltSystem.SkillDropdown then local skillOptions = BuildSkillOptions() - AltSystem.SkillDropdown:SetupMenu(function(dropdown, rootDescription) + -- Update the displayed label text + if skillOptions[newIndex] then + AltSystem.SkillDropdown.label:SetText(skillOptions[newIndex].text) + end + AltSystem.SkillDropdown:SetupMenu(function(owner, rootDescription) for i, option in ipairs(skillOptions) do rootDescription:CreateRadio( - option.text, - function(data) return data == AltSystem.State.selectedSkillIndex end, - function(data) - AltSystem.State.selectedSkillIndex = data - if AltSystem.SetSkillIndex then - AltSystem.SetSkillIndex(data) + option.text, + function() + return i == AltSystem.State.selectedSkillIndex + end, + function() + AltSystem.State.selectedSkillIndex = i + AltSystem.State.selectedSkillName = AltSystem.Data.Skills[i] and AltSystem.Data.Skills[i].name or nil + AltSystem.SkillDropdown.label:SetText(option.text) + if AltSystem.UpdateSkillWarning then + AltSystem.UpdateSkillWarning(i) + end end - end, - i ) end end) end end --- Create the frame on file load so it's ready when Init runs -AltSystem:CreateMainFrame() +-- Frame is created by AltSystem:Init() in Core.lua, after saved variables are loaded diff --git a/docs/1-interface.md b/docs/1-interface.md old mode 100644 new mode 100755 diff --git a/docs/2-skills.md b/docs/2-skills.md old mode 100644 new mode 100755 index c7ec7fb..cd4429d --- a/docs/2-skills.md +++ b/docs/2-skills.md @@ -3,13 +3,13 @@ - The skills are defined in the TRP profile as 'Personality traits' - For each trait: - the left field represents the skill name - - the right field must contain a valid skill keyword (Novice, Adept, Expert, Master) — traits without one of these keywords are omitted + - the right field must contain a valid skill keyword (Inept, Novice, Adept, Expert, Master) — traits without one of these keywords are omitted + - if the right field contains "Inept", the trait always gets a -4 modifier regardless of its numeric value - the first numeric value (V2) determines the skill level based on these ranges: - Novice: 1-5 - Adept: 6-10 - Expert: 11-19 - Master: 20 - should a skill have a value of 0 or no value, it should be omitted from the list -- The list should have a default selected value of "Unskilled" which corresponds to a -4 modifier - In case no skills are found in the profile, or no profile is selected, a default list should be displayed - - Unskilled, Novice Skill, Adept Skill, Expert Skill, Master Skill \ No newline at end of file + - Base roll, Novice Skill, Adept Skill, Expert Skill, Master Skill \ No newline at end of file diff --git a/docs/3-announce.md b/docs/3-announce.md new file mode 100755 index 0000000..2961e7b --- /dev/null +++ b/docs/3-announce.md @@ -0,0 +1,13 @@ +# Feature: Announcing rolls +- There should be a setting to allow announcing rolls in chat +- This setting can be enabled or disabled through a checkbox +- When enabled, a dropdown selection of channels should be shown: + - Emote (/e) + - Party (/p) + - Raid (/ra) + - Guild (/g) +- Selection and enabled/disabled state should be saved +- When enabled, rolls should be announced in the selected channel in the following format: + - "[Name] rolled [result of d20] + [modifier] = [total]" + - Modifiers should be shown with names in parentheses, for example "-2 (First Aid) +2 (Item)" + - Name should be the TRP character first name \ No newline at end of file diff --git a/docs/4-redesign.md b/docs/4-redesign.md new file mode 100755 index 0000000..fcd504c --- /dev/null +++ b/docs/4-redesign.md @@ -0,0 +1,78 @@ +# Feature: Major Redesign +This redesign of the Addon's window will start by following the following [design](./roll_tab_design.png) + +- The window will be a tabbed window +- The 'Use Skills' tab will correspond to the current roll screen +- The 'Build Skills' tab will be a new screen, to be implemented later. For now, leave it empty. +- The 'Log' should record all rolls made by the user, and be displayed in a scrollable list. + - They should be displayed in the same style as the announced rolls, even when the announce option is off +- The log should store a maximum of 100 rolls + +## Implementation plan + +### 1. Restructure the main window layout (UI.lua) +- Increase `WINDOW_WIDTH` to roughly double (≈660) to accommodate the two-column layout (left panel for tabs, right panel for the log) +- Replace `BasicFrameTemplateWithInset` or layer a new structure inside it: + - **Left column (~50% width):** contains two tabs ("Use Skills", "Build Skills") and their content panels + - **Right column (~50% width):** contains the "Log" header and a scrollable log list +- Keep the title bar ("AltSystem") and close button at the top spanning the full width + +### 2. Implement the tab system (UI.lua) +- Create two tab buttons ("Use Skills" and "Build Skills") anchored at the top of the left column +- Use `PanelTemplates_SetNumTabs` / `PanelTemplates_SetTab` or manual highlight toggling to switch active tab styling +- **"Use Skills" tab content:** migrate all existing UI elements (Roll Type radios → Skill dropdown → Armor radios → Modifiers checkboxes → Announce dropdown → Roll button) into this tab's content frame + - Adapt the current layout from `CreateMainFrame` — re-parent all widgets to the tab content frame instead of `f` directly +- **"Build Skills" tab content:** create an empty placeholder frame (can show a "Coming soon" label) +- Toggling tabs shows/hides the corresponding content frame + +### 3. Redesign the "Use Skills" tab to match the mockup (UI.lua) +- Replace the current separate Attack/Defense buttons with a **Roll Type** radio-button group ("Attack Roll" / "Defense Roll") that sets `AltSystem.State.rollType` +- Keep the **Skill** dropdown as-is (already matches the mockup) +- Replace the current Defense dropdown with an **Armor** radio-button group ("No Armor" / "Basic Armor (+1)" / "Heavy Armor (+2)") + - Map these to the existing `AltSystem.Data.Defenses` entries; show armor options only when Defense Roll is selected, or always visible per mockup +- Group **Shield** and **Pet** checkboxes under a "Modifiers (optional)" section header with a "Label" sub-header matching the mockup +- Replace the Announce checkbox + channel dropdown with a single **"Announce Roll"** dropdown whose options are "Self Roll" (no announce) plus the existing channel list (Emote, Party, Raid, Guild) + - "Self Roll" maps to `announceEnabled = false`; any other selection maps to `announceEnabled = true` with the corresponding channel index +- Add a single **"Roll $rollType"** button at the bottom (text dynamically reflects "Roll Attack" or "Roll Defense") +- Remove the old roll-result text area from this tab (results now go to the Log panel) + +### 4. Build the Log panel (UI.lua) +- Create a right-side panel with a "Log" header label +- Inside, create a `ScrollFrame` (using `UIPanelScrollFrameTemplate` or a manual scroll child) to hold log entries +- Each log entry is a small frame/fontstring displaying the roll result in the same format as the announced message: + - `"[Name] rolled [d20 result] [modifiers] = [total]"` (reuse `BuildModifierString` from Roll.lua) + - Critical rolls show "rolled a Critical Failure!" or "rolled a Critical Success!" +- Entries are listed newest-first (most recent at top) in a vertically stacked layout + +### 5. Implement the roll log data store (Core.lua / Roll.lua) +- Add `AltSystem.State.rollLog = {}` — an array of log entry tables, each containing: `{ text = "...", timestamp = time() }` +- In `Roll.lua`, after every roll result is calculated (in `CalculateAndDisplayResult`), build the log message string (same format as announce) and insert it into `AltSystem.State.rollLog` +- Cap the log at **100 entries**: if `#rollLog > 100`, remove the oldest entry (`table.remove(rollLog, 1)`) +- After inserting, call a UI refresh function to update the scroll frame content +- The log is **always populated**, regardless of the announce setting (per the requirement: "displayed in the same style as the announced rolls, even when the announce option is off") +- Log does **not** need to persist across sessions (not mentioned in requirements); keep it in memory only + +### 6. Wire up the new Roll button (Roll.lua) +- The single "Roll $rollType" button calls `AltSystem:PerformRoll(state.rollType)` where `state.rollType` is set by the radio-button group ("attack" or "defense") +- Existing `PerformRoll` and `CalculateAndDisplayResult` logic remains largely unchanged; only the final display step changes from setting `ResultText` to appending to the log + refreshing the log UI + +### 7. Update state persistence (Core.lua) +- Save/restore `rollType` selection (attack/defense) in `AltSystemDB` +- Update announce state handling to work with the new single-dropdown approach (save selected option index) +- Armor selection (radio group) replaces `selectedDefenseIndex` — reuse same key or migrate + +### 8. Update the .toc file if needed (AltSystem.toc) +- No new Lua files are expected (all changes fit in existing files), but verify the load order is still correct + +### 9. Testing checklist +- [ ] Window opens at new size, tabs switch correctly +- [ ] "Use Skills" tab shows all controls matching the mockup layout +- [ ] "Build Skills" tab is empty / shows placeholder +- [ ] Attack and Defense rolls work correctly via the new single Roll button +- [ ] Log panel populates with each roll, formatted like announce messages +- [ ] Log scrolls when entries exceed visible area +- [ ] Log caps at 100 entries, oldest removed first +- [ ] Log populates even when announce is set to "Self Roll" (off) +- [ ] Announce still works when a channel is selected +- [ ] State (roll type, armor, announce option) persists across sessions +- [ ] Window is draggable and clamps to screen \ No newline at end of file diff --git a/docs/5-build_skills.md b/docs/5-build_skills.md new file mode 100755 index 0000000..9b76326 --- /dev/null +++ b/docs/5-build_skills.md @@ -0,0 +1,132 @@ +# Feature: Build Skills tab +The second tab of the addon will follow these [designs](./build_skills_tab_design.png). + +## Acceptance Criteria +- This screen should show the same skills we use in the main screen, which come from the TRP profile +- The skills should be sorted by level +- The skill list should be scrollable, with the "Save" button pinned/sticky to the bottom +## Editing skills +- The user should be able to edit the name, level, and numerical score of each skill +- Edits should not be saved until the user explicitly clicks the "Save" button +### Skill Level and Value +- When a skill level is selected, the numerical score dropdown should update to only allow values within the skill level + - Inept: 0 + - Novice: 1-5 + - Adept: 6-10 + - Expert: 11-19 + - Master: 20 +### Deleting skills +- Clicking the "Delete" button should remove the skill from the list +### Adding Skills +- Clicking the "Add a Row" button should add a new skill row to the list +- Default values for the new skill should be: + - Name: Skillname + - Level: Novice + - Value: 1 +### Saving Skills +- When clicking the "Save" button: + - Newly added skills should be added to the TRP profile + - Existing skills should be updated in the TRP profile + - Skills that were deleted should also be removed from the TRP profile +- Icons should not be changed +## References +- Refer to [Data.lua](../Data.lua) for details on how we currently fetch the skills from TRP +- Refer to the TRP3 source code in case it's necessary [here](https://github.com/Total-RP/Total-RP-3) + +--- + +## Implementation Plan + +### 1. Add a `SaveSkills` function to Data.lua +- Create `AltSystem.Data:SaveSkills(editedSkills)` that writes skills back to the TRP3 profile +- Access the TRP3 profile via `TRP3_API.profile.getData("player/characteristics")` to get the `characteristics.PS` (personality traits) array +- For each edited skill, update or insert entries in `PS`: + - `LT` = skill name + - `RT` = level keyword (e.g. "Novice", "Adept", etc.) + - `V2` = numeric value (0–20) + - `IC` = preserve existing icon (do not change); for new skills, use a sensible default icon (e.g. `"inv_misc_questionmark"`) +- Remove any PS entries that were deleted by the user +- After writing, call `TRP3_API.dashboard.showCharacteristics()` or fire the appropriate TRP3 event if needed to refresh TRP3's own UI +- **Edge case:** If the TRP3 API is unavailable, show a warning message and abort save + +### 2. Extract skill-level constants into shared lookup tables in Data.lua +- The acceptance criteria defines value ranges per level (Inept: 0, Novice: 1–5, Adept: 6–10, Expert: 11–19, Master: 20) +- `SKILL_KEYWORD_RANGES` already exists but excludes Inept/Master min-max correctly for the Build tab's needs; extend or create a new table `AltSystem.Data.SkillValueRanges` that is accessible from UI.lua: + ``` + { Inept = {min=0, max=0}, Novice = {min=1, max=5}, Adept = {min=6, max=10}, Expert = {min=11, max=19}, Master = {min=20, max=20} } + ``` +- Create `AltSystem.Data.SkillLevelOrder` — an ordered array `{"Inept", "Novice", "Adept", "Expert", "Master"}` for populating the level dropdown in display order +- Create a helper `AltSystem.Data:GetDefaultValueForLevel(level)` that returns the minimum value for that level (used when the user changes level to auto-set the value) + +### 3. Add a function to read raw skills (with numeric values) from TRP3 in Data.lua +- Currently `RefreshSkills()` converts TRP3 traits into `{name, level, modifier}` — the Build tab needs the raw **numeric value** and the **icon** as well +- Create `AltSystem.Data:GetEditableSkills()` that returns an array of `{name, level, value, icon}` for each valid skill trait in the TRP3 profile (excluding Base Roll and Unskilled, which are system-generated entries) +- Sort the returned skills by level using `AltSystem.Data.SkillLevelOrder` ordering (Inept first, Master last) — matching the acceptance criteria "sorted by level" +- Reuse existing helpers `FindSkillKeyword` and `ParseSkillLevel` (promote them from local to module-level if needed, or call internally) + +### 4. Build the Build Skills tab UI (new file: BuildSkillsUI.lua) +- Create a new file to keep UI.lua manageable; register it in `AltSystem.toc` between `UI.lua` and `Roll.lua` +- Create `AltSystem:CreateBuildSkillsContent(parentFrame)` called from `CreateMainFrame` in UI.lua (replacing the placeholder) +- **Layout structure:** + - **Info text** at top — two golden/yellow paragraphs explaining that skills come from TRP (matches mockup) + - **"Skill list" section header** + - **Column headers**: Name, Level, Value (bold golden text) + - **Scrollable skill list** — a `ScrollFrame` containing dynamically created skill rows + - **"Add A Row" button** — anchored below the last skill row, inside the scroll child + - **"Save Skills to TRP" button** — pinned/sticky at the bottom of the tab, outside the scroll frame + +### 5. Implement editable skill rows +- Each skill row is a frame containing: + - **Name**: `EditBox` (text input) — pre-filled with current skill name + - **Level**: `DropdownButton` (WowStyle1DropdownTemplate) — options: Inept, Novice, Adept, Expert, Master + - **Value**: `DropdownButton` — options dynamically generated based on selected level (e.g. Novice → 1,2,3,4,5) + - **Delete button**: A button with a trash-can icon/red texture that removes the row +- Store all row data in a local working copy array (`editableSkills`), not directly in `AltSystem.Data.Skills` +- When the **level dropdown** changes: + - Update the value dropdown options to only show valid values for the new level + - Auto-set the value to the minimum for that level (e.g. switching to Adept → value becomes 6) +- **Row management:** + - `CreateSkillRow(parent, index, skillData)` — creates or recycles a row frame + - `RefreshSkillRows()` — rebuilds/repositions all rows and updates scroll child height + - Deleting a row removes it from `editableSkills` and calls `RefreshSkillRows()` + +### 6. Implement "Add A Row" functionality +- Clicking "Add A Row" inserts a new entry into `editableSkills`: + - `{ name = "Skillname", level = "Novice", value = 1, icon = "inv_misc_questionmark", isNew = true }` +- Calls `RefreshSkillRows()` to render the new row +- The scroll frame should auto-scroll to show the new row + +### 7. Implement "Save Skills to TRP" functionality +- On click, call `AltSystem.Data:SaveSkills(editableSkills)` which: + 1. Reads current `characteristics.PS` from TRP3 + 2. Rebuilds the PS array: keeps non-skill traits untouched, updates/adds/removes skill traits based on `editableSkills` + 3. Writes the updated PS back to the TRP3 profile data + 4. Calls `RefreshSkills()` so the Use Skills tab dropdown reflects the changes immediately +- Show a confirmation message (print to chat or a brief on-screen text) on successful save +- **Edge cases:** + - Empty skill list: allowed — just remove all skill traits from PS + - Duplicate skill names: allowed (TRP3 doesn't enforce uniqueness) + - Unsaved changes + tab switch: no confirmation dialog required (per spec, changes are just lost) + +### 8. Wire up tab switching to populate Build Skills tab +- In `SelectTab(2)` (UI.lua), call `AltSystem:RefreshBuildSkillsList()` to reload skills from TRP3 into the working copy +- This ensures the Build tab always shows the latest TRP3 data when opened, and any unsaved edits are discarded on tab switch + +### 9. Update AltSystem.toc +- Add `BuildSkillsUI.lua` to the file list (after `UI.lua`, before `Roll.lua`) + +### 10. Testing checklist +- [ ] Build Skills tab shows skills from TRP3 profile, sorted by level +- [ ] Skill name is editable via text input +- [ ] Level dropdown shows all 5 levels; changing level updates value dropdown options and auto-selects minimum value +- [ ] Value dropdown only shows values valid for the current level +- [ ] Delete button removes the row immediately +- [ ] "Add A Row" adds a row with defaults (Skillname, Novice, 1) +- [ ] Skill list scrolls when rows exceed visible area +- [ ] "Save" button is always visible (pinned to bottom) +- [ ] Save writes correct data to TRP3 profile (LT, RT, V2, IC preserved) +- [ ] Save does not modify icons of existing skills +- [ ] After save, Use Skills tab dropdown reflects the updated skills +- [ ] Switching tabs discards unsaved changes and reloads from TRP3 +- [ ] Works correctly with 0 skills (empty profile) +- [ ] Works correctly with many skills (20+) — scroll behavior \ No newline at end of file diff --git a/docs/Changelog.md b/docs/Changelog.md new file mode 100755 index 0000000..e69de29 diff --git a/docs/build_skills_tab_design.png b/docs/build_skills_tab_design.png new file mode 100755 index 0000000..ddbfdd3 Binary files /dev/null and b/docs/build_skills_tab_design.png differ diff --git a/docs/roll_tab_design.png b/docs/roll_tab_design.png new file mode 100755 index 0000000..c80f5dc Binary files /dev/null and b/docs/roll_tab_design.png differ diff --git a/package.sh b/package.sh new file mode 100755 index 0000000..194f78f --- /dev/null +++ b/package.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +ADDON_NAME="AltSystem" +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +OUTPUT_DIR="$(mktemp -d)" +OUTPUT_FILE="$OUTPUT_DIR/$ADDON_NAME.zip" +STAGING_DIR="$(mktemp -d)" + +mkdir -p "$STAGING_DIR/$ADDON_NAME" +rsync -a --exclude='.DS_Store' --exclude='__MACOSX' --exclude='.git' --exclude='docs' "$SCRIPT_DIR/" "$STAGING_DIR/$ADDON_NAME/" + +cd "$STAGING_DIR" && \ +zip -r "$OUTPUT_FILE" "$ADDON_NAME" + +rm -rf "$STAGING_DIR" + +echo "$OUTPUT_FILE"