diff --git a/.idea/.gitignore b/.idea/.gitignore old mode 100755 new mode 100644 diff --git a/.idea/misc.xml b/.idea/misc.xml old mode 100755 new mode 100644 diff --git a/.idea/modules.xml b/.idea/modules.xml old mode 100755 new mode 100644 diff --git a/.idea/vcs.xml b/.idea/vcs.xml old mode 100755 new mode 100644 diff --git a/AltSystem.iml b/AltSystem.iml old mode 100755 new mode 100644 diff --git a/AltSystem.toc b/AltSystem.toc old mode 100755 new mode 100644 index 3b8554d..57c8602 --- a/AltSystem.toc +++ b/AltSystem.toc @@ -1,13 +1,12 @@ -## Interface: 120005 +## Interface: 120001 ## Title: AltSystem ## Notes: Enhances RP gameplay with a custom rolling system -## Author: Rukira -## Version: 1.2.0 +## Author: Goncalo +## Version: 1.0.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 deleted file mode 100755 index d8e7f8f..0000000 --- a/BuildSkillsUI.lua +++ /dev/null @@ -1,430 +0,0 @@ --- 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 100755 new mode 100644 index 865bf63..ead7cd8 --- a/Core.lua +++ b/Core.lua @@ -4,24 +4,9 @@ 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 = No Armor + selectedDefenseIndex = 1, -- 1 = Base 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 @@ -36,69 +21,12 @@ 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 100755 new mode 100644 index 44914b4..d95773d --- a/Data.lua +++ b/Data.lua @@ -6,92 +6,44 @@ AltSystem.Data = {} -- Skill levels and their modifiers AltSystem.Data.SkillLevels = { - ["Inept"] = -4, ["Novice"] = -2, ["Adept"] = 0, ["Expert"] = 2, ["Master"] = 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 } +-- The "Unskilled" entry is always the first (default) 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 = "Base roll", level = "Base", modifier = 0 }, - { name = "Novice Skill", level = "Novice", modifier = -2 }, - { name = "Adept Skill", level = "Adept", modifier = 0 }, + { name = "Unskilled", level = "Unskilled", modifier = -4 }, + { 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 = { "Inept", "Novice", "Adept", "Expert", "Master" } - --- 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 1 -end +local VALID_SKILL_KEYWORDS = { "Novice", "Adept", "Expert", "Master" } -- 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 +-- 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 keyword + return true end end - return nil + return false 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 +-- 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 + if numericValue >= 20 then return "Master", AltSystem.Data.SkillLevels["Master"] elseif numericValue >= 11 then @@ -104,13 +56,13 @@ local function ParseSkillLevel(rightText, numericValue) end -- Fetch skills from the current TRP3 profile's personality traits. --- Returns an array of skill entries, always starting with "Base roll". --- Traits marked as Inept get a -4 modifier regardless of their numeric value. +-- Returns an array of skill entries, always starting with "Unskilled". -- Falls back to the default list if no profile or no valid skills are found. function AltSystem.Data:RefreshSkills() local skills = {} - -- 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 }) + + -- Always add Unskilled as the first entry + table.insert(skills, { name = UNSKILLED_ENTRY.name, level = UNSKILLED_ENTRY.level, modifier = UNSKILLED_ENTRY.modifier }) local foundAny = false @@ -122,16 +74,14 @@ function AltSystem.Data:RefreshSkills() local skillName = trait.LT local numericValue = trait.V2 - local keyword = FindSkillKeyword(trait.RT) - if skillName and skillName ~= "" and keyword then - local level, modifier = ParseSkillLevel(trait.RT, numericValue) + if skillName and skillName ~= "" and HasSkillKeyword(trait.RT) then + local level, modifier = ParseSkillLevel(numericValue) if level and modifier then foundAny = true table.insert(skills, { name = skillName, level = level, modifier = modifier, - warning = CheckSkillMismatch(keyword, numericValue), }) end end @@ -139,7 +89,7 @@ function AltSystem.Data:RefreshSkills() end end - -- If no valid skills were found, use the default fallback list + -- If no valid skills were found, use the default fallback list (skip first "Unskilled" since we already added it) if not foundAny then skills = {} for _, skill in ipairs(DEFAULT_SKILLS) do @@ -147,119 +97,28 @@ 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, label (used in roll messages), and modifier (first entry = no item) +-- Item options: name and modifier (first entry = no item) AltSystem.Data.Items = { - { name = "No item", label = "No item", modifier = 0 }, - { name = "Rare", label = "Rare item", modifier = 3 }, - { name = "Epic", label = "Epic item", modifier = 5 }, + { name = "No item", modifier = 0 }, + { name = "Rare item", modifier = 3 }, + { name = "Epic item", modifier = 5 }, } --- Defense / Armor options: name, label (used in roll messages), and modifier +-- Defense options: name and modifier AltSystem.Data.Defenses = { - { name = "None", label = "No armor", modifier = 0 }, - { name = "Partial", label = "Extra armor", modifier = 1 }, - { name = "Full", label = "Extra armor", modifier = 2 }, + { name = "Base armor", modifier = 0 }, + { name = "Extra small armor", modifier = 1 }, + { name = "Extra large armor", modifier = 2 }, } -- Shield modifier diff --git a/README.md b/README.md old mode 100755 new mode 100644 index 434c0a0..35b32f7 --- a/README.md +++ b/README.md @@ -6,11 +6,6 @@ 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 100755 new mode 100644 index 036a680..28f3752 --- a/Roll.lua +++ b/Roll.lua @@ -4,119 +4,9 @@ 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 @@ -126,55 +16,21 @@ 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 roller, roll = message:match("(.+) rolls (%d+) %(1%-20%)") + local 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, 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 - +function AltSystem:CalculateAndDisplayResult(rollType, rollValue) local state = AltSystem.State local skill = AltSystem.Data.Skills[state.selectedSkillIndex] local item = AltSystem.Data.Items[state.selectedItemIndex] @@ -183,26 +39,17 @@ function AltSystem:CalculateAndDisplayResult(rollType, rollValue, petRollValue) local itemMod = item and item.modifier or 0 local total = rollValue - - local modifiers = {} - - local isBaseRoll = skill and skill.name == "Base roll" - - local petMod = petRollValue or 0 + local breakdown = "Roll: " .. rollValue if rollType == "attack" then - -- Attack Roll = roll + skill modifier + item modifier + pet/summon modifier - total = rollValue + skillMod + itemMod + petMod + -- Attack Roll = roll + skill modifier + item modifier + total = rollValue + skillMod + itemMod - if not isBaseRoll then - table.insert(modifiers, { name = skill and skill.name or "Skill", value = skillMod }) - end + breakdown = breakdown .. "\nSkill: " .. FormatModifier(skillMod) 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 }) + breakdown = breakdown .. " | Item: " .. FormatModifier(itemMod) end + breakdown = breakdown .. "\n|cffffd100Attack Total: " .. total .. "|r" elseif rollType == "defense" then -- Defense Roll = roll + skill modifier + item modifier + defense modifier + shield modifier @@ -210,30 +57,22 @@ function AltSystem:CalculateAndDisplayResult(rollType, rollValue, petRollValue) local defenseMod = defense and defense.modifier or 0 local shieldMod = state.shieldEnabled and AltSystem.Data.ShieldModifier or 0 - total = rollValue + skillMod + itemMod + defenseMod + shieldMod + petMod + total = rollValue + skillMod + itemMod + defenseMod + shieldMod - if not isBaseRoll then - table.insert(modifiers, { name = skill and skill.name or "Skill", value = skillMod }) - end + breakdown = breakdown .. "\nSkill: " .. FormatModifier(skillMod) if itemMod ~= 0 then - 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 }) + breakdown = breakdown .. " | Item: " .. FormatModifier(itemMod) end + breakdown = breakdown .. "\nDefense: " .. FormatModifier(defenseMod) if shieldMod ~= 0 then - table.insert(modifiers, { name = "Shield", value = shieldMod }) - end - if petMod ~= 0 then - table.insert(modifiers, { name = "Pet", value = petMod }) + breakdown = breakdown .. " | Shield: " .. FormatModifier(shieldMod) end + breakdown = breakdown .. "\n|cff00ccffDefense Total: " .. total .. "|r" end - -- Add to log (always, regardless of announce setting) - AddLogEntry(BuildLogMessage(rollValue, modifiers, total)) - - -- Announce to chat (if enabled) - AnnounceRoll(rollValue, modifiers, total) + if AltSystem.ResultText then + AltSystem.ResultText:SetText(breakdown) + end end -- Format a modifier value with sign diff --git a/UI.lua b/UI.lua old mode 100755 new mode 100644 index 642d328..5d64ea6 --- a/UI.lua +++ b/UI.lua @@ -1,21 +1,14 @@ -- AltSystem UI --- Creates the main dialog window with tabbed layout. --- The "Use Skills" tab contains controls on the left and a Log panel on the right. +-- Creates the main dialog window with all interface elements. -- Uses the modern DropdownButton API (WoW 10.2.5+ / 12.0+). AltSystem = AltSystem or {} -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 +local WINDOW_WIDTH = 300 +local WINDOW_HEIGHT = 380 +local PADDING = 12 +local ROW_HEIGHT = 30 +local LABEL_WIDTH = 80 -- Helper: Build the skill option list from current AltSystem.Data.Skills local function BuildSkillOptions() @@ -23,7 +16,7 @@ local function BuildSkillOptions() for _, skill in ipairs(AltSystem.Data.Skills) do local sign = skill.modifier >= 0 and "+" or "" local text - if skill.level == "Base" or skill.level == "Inept" then + if skill.level == "Unskilled" then text = skill.name .. " (" .. sign .. skill.modifier .. ")" else text = skill.name .. " (" .. skill.level .. " " .. sign .. skill.modifier .. ")" @@ -33,144 +26,39 @@ local function BuildSkillOptions() return options end --- 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 +-- 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") local selectedIndex = defaultIndex or 1 - 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 + local dropdown = CreateFrame("DropdownButton", name, parent, "WowStyle1DropdownTemplate") + dropdown:SetPoint("LEFT", label, "RIGHT", 4, 0) + dropdown:SetWidth(160) - -- Set initial label text - if options[selectedIndex] then - dropdown.label:SetText(options[selectedIndex].text) - end - - dropdown:SetupMenu(function(owner, rootDescription) + dropdown:SetupMenu(function(dropdown, rootDescription) for i, option in ipairs(options) do rootDescription:CreateRadio( - option.text, - function() - return i == selectedIndex - end, - function() - selectedIndex = i - dropdown.label:SetText(option.text) - if onSelect then - onSelect(i, option) - end - end + option.text, + function(data) return data == selectedIndex end, + function(data) + selectedIndex = data + if onSelect then onSelect(data, options[data]) end + end, + i ) end 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 + return dropdown, function() return selectedIndex end, function(idx) selectedIndex = idx end 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") @@ -188,508 +76,166 @@ function AltSystem:CreateMainFrame() f.title:SetPoint("TOPLEFT", f.TitleBg, "TOPLEFT", 5, -3) f.title:SetText("AltSystem") - --------------------- - -- 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 + -- Track current Y offset for layout + local yPos = -40 + ------------------------- -- Skill dropdown + ------------------------- local skillOptions = BuildSkillOptions() - local UpdateSkillWarning -- forward declaration - 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 + local skillDropdown, getSkillIndex, setSkillIndex = CreateDropdown( + f, "AltSystemSkillDropdown", yPos, "Skill:", skillOptions, + AltSystem.State.selectedSkillIndex, + function(index) + AltSystem.State.selectedSkillIndex = index + end) -- Store references for refreshing AltSystem.SkillDropdown = skillDropdown AltSystem.GetSkillIndex = getSkillIndex AltSystem.SetSkillIndex = setSkillIndex - AltSystem.UpdateSkillWarning = UpdateSkillWarning + yPos = yPos - ROW_HEIGHT - 8 - 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 + ------------------------- + -- 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, + }) end - 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 + 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, + }) end - yPos = yPos - ROW_HEIGHT - SECTION_GAP + CreateDropdown(f, "AltSystemDefenseDropdown", yPos, "Defense:", defenseOptions, AltSystem.State.selectedDefenseIndex, + function(index) + AltSystem.State.selectedDefenseIndex = index + end) + yPos = yPos - ROW_HEIGHT - 8 - -- Section: Modifiers (optional) - CreateSectionHeader(content, "Modifiers (optional)", PADDING_HEADER, yPos) - yPos = yPos - 26 - --CreateSubLabel(content, "Label", PADDING, yPos) - --yPos = yPos - 22 + ------------------------- + -- 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") - -- Shield checkbox (flat square toggle) - local shieldCheck = CreateFrame("CheckButton", "AltSystemShieldCheck", content) - shieldCheck:SetSize(20, 20) - shieldCheck:SetPoint("TOPLEFT", content, "TOPLEFT", PADDING, yPos) + local shieldCheck = CreateFrame("CheckButton", "AltSystemShieldCheck", f, "UICheckButtonTemplate") + shieldCheck:SetPoint("LEFT", shieldLabel, "RIGHT", -6, 0) shieldCheck:SetChecked(AltSystem.State.shieldEnabled) - 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)") + local shieldText = shieldCheck:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall") + shieldText:SetPoint("LEFT", shieldCheck, "RIGHT", 2, 0) + shieldText:SetText("+1 modifier") shieldCheck:SetScript("OnClick", function(self) AltSystem.State.shieldEnabled = self:GetChecked() - UpdateShieldVisual() end) - -- 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) + yPos = yPos - ROW_HEIGHT - 12 - local petBg = petCheck:CreateTexture(nil, "BACKGROUND") - petBg:SetAllPoints() - petBg:SetColorTexture(0.3, 0.3, 0.3, 1) + ------------------------- + -- Roll buttons + ------------------------- + local btnWidth = (WINDOW_WIDTH - PADDING * 3) / 2 - 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 attackBtn = CreateFrame("Button", "AltSystemAttackRollBtn", f, "UIPanelButtonTemplate") + attackBtn:SetSize(btnWidth, 28) + attackBtn:SetPoint("TOPLEFT", f, "TOPLEFT", PADDING, yPos) + attackBtn:SetText("Attack/Skill 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() + local defenseBtn = CreateFrame("Button", "AltSystemDefenseRollBtn", f, "UIPanelButtonTemplate") + defenseBtn:SetSize(btnWidth, 28) + defenseBtn:SetPoint("TOPRIGHT", f, "TOPRIGHT", -PADDING, yPos) + defenseBtn:SetText("Defense Roll") - local petLabel = petCheck:CreateFontString(nil, "OVERLAY", "GameFontHighlight") - petLabel:SetPoint("LEFT", petCheck, "RIGHT", 6, 0) - petLabel:SetText("Pet (+d5)") + yPos = yPos - 40 - petCheck:SetScript("OnClick", function(self) - AltSystem.State.petSummonEnabled = self:GetChecked() - UpdatePetVisual() + ------------------------- + -- 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") end) - 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) + defenseBtn:SetScript("OnClick", function() + AltSystem:PerformRoll("defense") 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() - -- 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 + -- Reset selection to 1 (Unskilled) since the skill list may have changed + AltSystem.State.selectedSkillIndex = 1 if AltSystem.SetSkillIndex then - AltSystem.SetSkillIndex(newIndex) - end - if AltSystem.UpdateSkillWarning then - AltSystem.UpdateSkillWarning(newIndex) + AltSystem.SetSkillIndex(1) end -- Rebuild the dropdown menu with the new skill list if AltSystem.SkillDropdown then local skillOptions = BuildSkillOptions() - -- Update the displayed label text - if skillOptions[newIndex] then - AltSystem.SkillDropdown.label:SetText(skillOptions[newIndex].text) - end - AltSystem.SkillDropdown:SetupMenu(function(owner, rootDescription) + AltSystem.SkillDropdown:SetupMenu(function(dropdown, rootDescription) for i, option in ipairs(skillOptions) do rootDescription:CreateRadio( - 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 + option.text, + function(data) return data == AltSystem.State.selectedSkillIndex end, + function(data) + AltSystem.State.selectedSkillIndex = data + if AltSystem.SetSkillIndex then + AltSystem.SetSkillIndex(data) end + end, + i ) end end) end end --- Frame is created by AltSystem:Init() in Core.lua, after saved variables are loaded +-- Create the frame on file load so it's ready when Init runs +AltSystem:CreateMainFrame() diff --git a/docs/1-interface.md b/docs/1-interface.md old mode 100755 new mode 100644 diff --git a/docs/2-skills.md b/docs/2-skills.md old mode 100755 new mode 100644 index cd4429d..c7ec7fb --- 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 (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 right field must contain a valid skill keyword (Novice, Adept, Expert, Master) — traits without one of these keywords are omitted - 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 - - Base roll, Novice Skill, Adept Skill, Expert Skill, Master Skill \ No newline at end of file + - Unskilled, 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 deleted file mode 100755 index 2961e7b..0000000 --- a/docs/3-announce.md +++ /dev/null @@ -1,13 +0,0 @@ -# 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 deleted file mode 100755 index fcd504c..0000000 --- a/docs/4-redesign.md +++ /dev/null @@ -1,78 +0,0 @@ -# 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 deleted file mode 100755 index 9b76326..0000000 --- a/docs/5-build_skills.md +++ /dev/null @@ -1,132 +0,0 @@ -# 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 deleted file mode 100755 index e69de29..0000000 diff --git a/docs/build_skills_tab_design.png b/docs/build_skills_tab_design.png deleted file mode 100755 index ddbfdd3..0000000 Binary files a/docs/build_skills_tab_design.png and /dev/null differ diff --git a/docs/roll_tab_design.png b/docs/roll_tab_design.png deleted file mode 100755 index c80f5dc..0000000 Binary files a/docs/roll_tab_design.png and /dev/null differ diff --git a/package.sh b/package.sh deleted file mode 100755 index 194f78f..0000000 --- a/package.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/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"