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 a085d34..3044c78 --- a/AltSystem.toc +++ b/AltSystem.toc @@ -8,5 +8,6 @@ Data.lua Core.lua +BuildSkillsUI.lua UI.lua Roll.lua diff --git a/BuildSkillsUI.lua b/BuildSkillsUI.lua new file mode 100755 index 0000000..c102705 --- /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 diff --git a/Data.lua b/Data.lua old mode 100644 new mode 100755 index d1e5f56..6706c49 --- a/Data.lua +++ b/Data.lua @@ -39,6 +39,27 @@ local SKILL_KEYWORD_RANGES = { ["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 + -- 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) @@ -134,6 +155,94 @@ function AltSystem.Data:RefreshSkills() 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 @@ -143,8 +252,8 @@ end -- Item options: name 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 = "Rare", modifier = 3 }, + { name = "Epic", modifier = 5 }, } -- Defense / Armor options: name and modifier diff --git a/README.md b/README.md old mode 100644 new mode 100755 diff --git a/Roll.lua b/Roll.lua old mode 100644 new mode 100755 diff --git a/UI.lua b/UI.lua old mode 100644 new mode 100755 index 0b989bb..bfc7013 --- a/UI.lua +++ b/UI.lua @@ -29,7 +29,7 @@ local function BuildSkillOptions() return options end --- Helper: Create a modern dropdown (WowStyle1DropdownTemplate) +-- Helper: Create a flat dark dropdown with optional label (reuses shared CreateFlatDropdown) local function CreateDropdown(parent, name, labelText, options, defaultIndex, onSelect, labelFont) local container = CreateFrame("Frame", nil, parent) container:SetHeight(ROW_HEIGHT) @@ -44,28 +44,32 @@ local function CreateDropdown(parent, name, labelText, options, defaultIndex, on local selectedIndex = defaultIndex or 1 - local dropdown = CreateFrame("DropdownButton", name, container, "WowStyle1DropdownTemplate") + local dropdown = AltSystem.CreateFlatDropdown(name, container, 190) if label then dropdown:SetPoint("RIGHT", container, "RIGHT", 0, 0) else dropdown:SetPoint("LEFT", container, "LEFT", 0, 0) end - dropdown:SetWidth(190) - 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 + function() + return i == selectedIndex end, - function(data) - selectedIndex = data + function() + selectedIndex = i + dropdown.label:SetText(option.text) if onSelect then - onSelect(data, options[data]) + onSelect(i, option) end - end, - i + end ) end end) @@ -74,30 +78,52 @@ local function CreateDropdown(parent, name, labelText, options, defaultIndex, on return selectedIndex end, function(idx) selectedIndex = idx + if options[idx] then + dropdown.label:SetText(options[idx].text) + end end end --- Helper: Create a radio button (CheckButton with radio texture) +-- 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 radio = CreateFrame("CheckButton", name, parent, "UIRadioButtonTemplate") - radio:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y) - radio:SetChecked(isChecked) + local size = 20 + local btn = CreateFrame("CheckButton", name, parent) + btn:SetSize(size, size) + btn:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y) - local radioText = radio:GetFontString() - if radioText then - radioText:SetText(text) - radioText:SetFontObject("GameFontHighlight") - else - local t = radio:CreateFontString(nil, "OVERLAY", "GameFontHighlight") - t:SetPoint("LEFT", radio, "RIGHT", 4, 0) - t:SetText(text) + -- Background (grey when unselected, yellow when selected) — circular via mask + local bg = btn:CreateTexture(nil, "BACKGROUND") + bg:SetAllPoints() + bg:SetColorTexture(0.3, 0.3, 0.3, 1) + local mask = btn:CreateMaskTexture() + mask:SetAllPoints() + mask:SetTexture("Interface\\CharacterFrame\\TempPortraitAlphaMask", "CLAMPTOBLACKADDITIVE", "CLAMPTOBLACKADDITIVE") + bg:AddMaskTexture(mask) + + btn.checkTex = nil -- unused, kept for compatibility + + local function UpdateVisual() + if btn:GetChecked() then + bg:SetColorTexture(0.9, 0.75, 0.2, 1) + else + bg:SetColorTexture(0.3, 0.3, 0.3, 1) + end end - radio:SetScript("OnClick", function(self) + 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) - return radio + btn.UpdateVisual = UpdateVisual + return btn end -- Helper: Create a section header (golden text) @@ -184,9 +210,8 @@ function AltSystem:CreateMainFrame() buildSkillsContent:SetSize(contentWidth, tabContentHeight) buildSkillsContent:Hide() - local buildPlaceholder = buildSkillsContent:CreateFontString(nil, "OVERLAY", "GameFontHighlight") - buildPlaceholder:SetPoint("CENTER") - buildPlaceholder:SetText("Coming soon") + -- Build Skills tab content (created in BuildSkillsUI.lua) + AltSystem:CreateBuildSkillsContent(buildSkillsContent) -- Tab switching logic local function SelectTab(tabIndex) @@ -200,6 +225,7 @@ function AltSystem:CreateMainFrame() buildSkillsContent:Show() tabUseSkillsBg:SetColorTexture(0.3, 0.3, 0.3, 1) tabBuildSkillsBg:SetColorTexture(0.15, 0.15, 0.15, 1) + AltSystem:RefreshBuildSkillsList() end end @@ -235,6 +261,8 @@ function AltSystem:CreateMainFrame() 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" @@ -323,6 +351,7 @@ function AltSystem:CreateMainFrame() AltSystem.State.selectedDefenseIndex = index for i, radio in ipairs(armorRadios) do radio:SetChecked(i == index) + radio.UpdateVisual() end end @@ -343,6 +372,86 @@ function AltSystem:CreateMainFrame() yPos = yPos - ROW_HEIGHT - SECTION_GAP + -- Section: Modifiers (optional) + CreateSectionHeader(content, "Modifiers (optional)", PADDING, yPos) + yPos = yPos - 28 + --CreateSubLabel(content, "Label", PADDING, yPos) + --yPos = yPos - 22 + + -- 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 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) + + -- 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) + + local petBg = petCheck:CreateTexture(nil, "BACKGROUND") + petBg:SetAllPoints() + petBg:SetColorTexture(0.3, 0.3, 0.3, 1) + + 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 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 petLabel = petCheck:CreateFontString(nil, "OVERLAY", "GameFontHighlight") + petLabel:SetPoint("LEFT", petCheck, "RIGHT", 6, 0) + petLabel:SetText("Pet (+d5)") + + petCheck:SetScript("OnClick", function(self) + AltSystem.State.petSummonEnabled = self:GetChecked() + UpdatePetVisual() + end) + + yPos = yPos - ROW_HEIGHT - SECTION_GAP + -- Item label CreateSubLabel(content, "Item", PADDING, yPos) yPos = yPos - 20 @@ -354,6 +463,7 @@ function AltSystem:CreateMainFrame() AltSystem.State.selectedItemIndex = index for i, radio in ipairs(itemRadios) do radio:SetChecked(i == index) + radio.UpdateVisual() end end @@ -374,50 +484,6 @@ function AltSystem:CreateMainFrame() yPos = yPos - ROW_HEIGHT - SECTION_GAP - -- Section: Modifiers (optional) - CreateSectionHeader(content, "Modifiers (optional)", PADDING, yPos) - yPos = yPos - 18 - --CreateSubLabel(content, "Label", PADDING, yPos) - --yPos = yPos - 22 - - -- Shield checkbox - local shieldCheck = CreateFrame("CheckButton", "AltSystemShieldCheck", content, "UICheckButtonTemplate") - shieldCheck:SetPoint("TOPLEFT", content, "TOPLEFT", PADDING, yPos) - shieldCheck:SetChecked(AltSystem.State.shieldEnabled) - - local shieldText = shieldCheck:GetFontString() - if shieldText then - shieldText:SetText("Shield (+ 1)") - else - shieldText = shieldCheck:CreateFontString(nil, "OVERLAY", "GameFontHighlight") - shieldText:SetPoint("LEFT", shieldCheck, "RIGHT", 2, 0) - shieldText:SetText("Shield (+ 1)") - end - - shieldCheck:SetScript("OnClick", function(self) - AltSystem.State.shieldEnabled = self:GetChecked() - end) - - -- Pet checkbox - local petCheck = CreateFrame("CheckButton", "AltSystemPetSummonCheck", content, "UICheckButtonTemplate") - petCheck:SetPoint("LEFT", shieldCheck, "RIGHT", 80, 0) - petCheck:SetChecked(AltSystem.State.petSummonEnabled) - - local petText = petCheck:GetFontString() - if petText then - petText:SetText("Pet (+d5)") - else - petText = petCheck:CreateFontString(nil, "OVERLAY", "GameFontHighlight") - petText:SetPoint("LEFT", petCheck, "RIGHT", 2, 0) - petText:SetText("Pet (+d5)") - end - - petCheck:SetScript("OnClick", function(self) - AltSystem.State.petSummonEnabled = self:GetChecked() - end) - - yPos = yPos - ROW_HEIGHT - SECTION_GAP - -- Section: Roll Dice CreateSectionHeader(content, "Roll Dice", PADDING, yPos) yPos = yPos - 22 @@ -448,11 +514,9 @@ function AltSystem:CreateMainFrame() yPos = yPos - ROW_HEIGHT - SECTION_GAP -- Roll button - local rollBtn = CreateFrame("Button", "AltSystemRollBtn", content, "UIPanelButtonTemplate") - rollBtn:SetSize(CONTROLS_WIDTH - PADDING * 2, 32) - rollBtn:SetPoint("TOPLEFT", content, "TOPLEFT", PADDING, yPos) local rollLabel = AltSystem.State.rollType == "attack" and "Roll Attack" or "Roll Defense" - rollBtn:SetText(rollLabel) + 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) @@ -573,24 +637,28 @@ function AltSystem:RefreshSkillDropdown() -- 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 + function() + return i == AltSystem.State.selectedSkillIndex end, - function(data) - AltSystem.State.selectedSkillIndex = data - AltSystem.State.selectedSkillName = AltSystem.Data.Skills[data] and AltSystem.Data.Skills[data].name or nil + 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.SetSkillIndex then - AltSystem.SetSkillIndex(data) + AltSystem.SetSkillIndex(i) end if AltSystem.UpdateSkillWarning then - AltSystem.UpdateSkillWarning(data) + AltSystem.UpdateSkillWarning(i) end - end, - i + end ) end end) 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 diff --git a/docs/3-announce.md b/docs/3-announce.md old mode 100644 new mode 100755 diff --git a/docs/4-redesign.md b/docs/4-redesign.md old mode 100644 new mode 100755 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 old mode 100644 new mode 100755 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 old mode 100644 new mode 100755 index 5754915..3ebb626 Binary files a/docs/roll_tab_design.png and b/docs/roll_tab_design.png differ