From e17370a89f90c66904938fef8e608e242a6ec893 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Correia?= Date: Sat, 11 Apr 2026 20:27:46 +0100 Subject: [PATCH 01/21] Fixes --- Data.lua | 12 ++++++------ Roll.lua | 13 +++++++++++++ UI.lua | 4 ++-- docs/2-skills.md | 4 ++-- 4 files changed, 23 insertions(+), 10 deletions(-) diff --git a/Data.lua b/Data.lua index d95773d..38ccf11 100644 --- a/Data.lua +++ b/Data.lua @@ -12,12 +12,12 @@ AltSystem.Data.SkillLevels = { ["Master"] = 4, } --- The "Unskilled" entry is always the first (default) skill -local UNSKILLED_ENTRY = { name = "Unskilled", level = "Unskilled", modifier = -4 } +-- The "Inept" entry is always the first (default) skill +local UNSKILLED_ENTRY = { name = "Inept", level = "Inept", modifier = -4 } -- Default/fallback skill list used when no TRP3 profile skills are found local DEFAULT_SKILLS = { - { name = "Unskilled", level = "Unskilled", modifier = -4 }, + { name = "Inept", level = "Inept", modifier = -4 }, { name = "Novice Skill", level = "Novice", modifier = -2 }, { name = "Adept Skill", level = "Adept", modifier = 0 }, { name = "Expert Skill", level = "Expert", modifier = 2 }, @@ -56,12 +56,12 @@ local function ParseSkillLevel(numericValue) end -- Fetch skills from the current TRP3 profile's personality traits. --- Returns an array of skill entries, always starting with "Unskilled". +-- Returns an array of skill entries, always starting with "Inept". -- Falls back to the default list if no profile or no valid skills are found. function AltSystem.Data:RefreshSkills() local skills = {} - -- Always add Unskilled as the first entry + -- Always add Inept as the first entry table.insert(skills, { name = UNSKILLED_ENTRY.name, level = UNSKILLED_ENTRY.level, modifier = UNSKILLED_ENTRY.modifier }) local foundAny = false @@ -89,7 +89,7 @@ function AltSystem.Data:RefreshSkills() end end - -- If no valid skills were found, use the default fallback list (skip first "Unskilled" since we already added it) + -- If no valid skills were found, use the default fallback list (skip first "Inept" since we already added it) if not foundAny then skills = {} for _, skill in ipairs(DEFAULT_SKILLS) do diff --git a/Roll.lua b/Roll.lua index 28f3752..aa1f97e 100644 --- a/Roll.lua +++ b/Roll.lua @@ -31,6 +31,19 @@ end) -- Calculate the final result based on the roll type and selected modifiers function AltSystem:CalculateAndDisplayResult(rollType, rollValue) + -- Critical rolls bypass normal calculation + if rollValue == 1 then + if AltSystem.ResultText then + AltSystem.ResultText:SetText("|cffff0000Critical Failure|r") + end + return + elseif rollValue == 20 then + if AltSystem.ResultText then + AltSystem.ResultText:SetText("|cff00ff00Critical Success|r") + end + return + end + local state = AltSystem.State local skill = AltSystem.Data.Skills[state.selectedSkillIndex] local item = AltSystem.Data.Items[state.selectedItemIndex] diff --git a/UI.lua b/UI.lua index 5d64ea6..cea1152 100644 --- a/UI.lua +++ b/UI.lua @@ -16,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 == "Unskilled" then + if skill.level == "Inept" then text = skill.name .. " (" .. sign .. skill.modifier .. ")" else text = skill.name .. " (" .. skill.level .. " " .. sign .. skill.modifier .. ")" @@ -210,7 +210,7 @@ end function AltSystem:RefreshSkillDropdown() AltSystem.Data:RefreshSkills() - -- Reset selection to 1 (Unskilled) since the skill list may have changed + -- Reset selection to 1 (Inept) since the skill list may have changed AltSystem.State.selectedSkillIndex = 1 if AltSystem.SetSkillIndex then AltSystem.SetSkillIndex(1) diff --git a/docs/2-skills.md b/docs/2-skills.md index c7ec7fb..f118ffe 100644 --- a/docs/2-skills.md +++ b/docs/2-skills.md @@ -10,6 +10,6 @@ - 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 +- The list should have a default selected value of "Inept" which corresponds to a -4 modifier - In case no skills are found in the profile, or no profile is selected, a default list should be displayed - - Unskilled, Novice Skill, Adept Skill, Expert Skill, Master Skill \ No newline at end of file + - Inept, Novice Skill, Adept Skill, Expert Skill, Master Skill \ No newline at end of file From 907e0464eddb71d2780be6b8fa23752668b31748 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Correia?= Date: Sun, 12 Apr 2026 20:48:49 +0100 Subject: [PATCH 02/21] Chat announce rolls --- AltSystem.toc | 2 +- Core.lua | 28 ++++++++++++++++++++ Roll.lua | 65 ++++++++++++++++++++++++++++++++++++++++++++++ UI.lua | 48 +++++++++++++++++++++++++++++++++- docs/3-announce.md | 13 ++++++++++ 5 files changed, 154 insertions(+), 2 deletions(-) create mode 100644 docs/3-announce.md diff --git a/AltSystem.toc b/AltSystem.toc index f72ae9f..969c40b 100644 --- a/AltSystem.toc +++ b/AltSystem.toc @@ -2,7 +2,7 @@ ## Title: AltSystem ## Notes: Enhances RP gameplay with a custom rolling system ## Author: Rukira -## Version: 0.2 +## Version: 0.3 ## Dependencies: totalRP3, totalRP3_Extended ## SavedVariables: AltSystemDB diff --git a/Core.lua b/Core.lua index ead7cd8..c5bb3a8 100644 --- a/Core.lua +++ b/Core.lua @@ -7,6 +7,16 @@ AltSystem.State = { selectedItemIndex = 1, -- 1 = No item selectedDefenseIndex = 1, -- 1 = Base armor shieldEnabled = false, + announceEnabled = false, + announceChannelIndex = 1, -- 1 = Emote, 2 = Party, 3 = Raid, 4 = Guild +} + +-- Channel definitions for announcing rolls +AltSystem.AnnounceChannels = { + { name = "Emote (/e)", channel = "EMOTE" }, + { name = "Party (/p)", channel = "PARTY" }, + { name = "Raid (/ra)", channel = "RAID" }, + { name = "Guild (/g)", channel = "GUILD" }, } -- Initialization on ADDON_LOADED @@ -21,12 +31,30 @@ frame:SetScript("OnEvent", function(self, event, arg1) end) function AltSystem:Init() + -- Load saved settings + AltSystemDB = AltSystemDB or {} + if AltSystemDB.announceEnabled ~= nil then + AltSystem.State.announceEnabled = AltSystemDB.announceEnabled + end + if AltSystemDB.announceChannelIndex then + AltSystem.State.announceChannelIndex = AltSystemDB.announceChannelIndex + 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.announceEnabled = AltSystem.State.announceEnabled + AltSystemDB.announceChannelIndex = AltSystem.State.announceChannelIndex + end) + print("|cff00ccffAltSystem|r loaded. Type /altsystem to open.") end diff --git a/Roll.lua b/Roll.lua index aa1f97e..cdfead6 100644 --- a/Roll.lua +++ b/Roll.lua @@ -5,6 +5,49 @@ AltSystem = AltSystem or {} local pendingRollType = nil -- "attack" or "defense" +-- 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 + if mod.value ~= 0 then + local sign = mod.value >= 0 and "+" or "" + table.insert(parts, sign .. mod.value .. " (" .. mod.name .. ")") + end + end + return table.concat(parts, " ") +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 name = GetCharacterName() + local modStr = BuildModifierString(modifiers) + + local msg + if modStr ~= "" then + msg = name .. " rolled " .. rollValue .. " " .. modStr .. " = " .. total + else + msg = name .. " rolled " .. rollValue .. " = " .. total + end + + SendChatMessage(msg, channelDef.channel) +end + -- Perform a roll: triggers the WoW native /roll 20 command function AltSystem:PerformRoll(rollType) pendingRollType = rollType @@ -36,11 +79,23 @@ function AltSystem:CalculateAndDisplayResult(rollType, rollValue) if AltSystem.ResultText then AltSystem.ResultText:SetText("|cffff0000Critical Failure|r") end + if AltSystem.State.announceEnabled then + local channelDef = AltSystem.AnnounceChannels[AltSystem.State.announceChannelIndex] + if channelDef then + SendChatMessage(GetCharacterName() .. " rolled a Critical Failure!", channelDef.channel) + end + end return elseif rollValue == 20 then if AltSystem.ResultText then AltSystem.ResultText:SetText("|cff00ff00Critical Success|r") end + if AltSystem.State.announceEnabled then + local channelDef = AltSystem.AnnounceChannels[AltSystem.State.announceChannelIndex] + if channelDef then + SendChatMessage(GetCharacterName() .. " rolled a Critical Success!", channelDef.channel) + end + end return end @@ -54,13 +109,17 @@ function AltSystem:CalculateAndDisplayResult(rollType, rollValue) local total = rollValue local breakdown = "Roll: " .. rollValue + local modifiers = {} + if rollType == "attack" then -- Attack Roll = roll + skill modifier + item modifier total = rollValue + skillMod + itemMod breakdown = breakdown .. "\nSkill: " .. FormatModifier(skillMod) + table.insert(modifiers, { name = skill and skill.name or "Skill", value = skillMod }) if itemMod ~= 0 then breakdown = breakdown .. " | Item: " .. FormatModifier(itemMod) + table.insert(modifiers, { name = item and item.name or "Item", value = itemMod }) end breakdown = breakdown .. "\n|cffffd100Attack Total: " .. total .. "|r" @@ -73,12 +132,16 @@ function AltSystem:CalculateAndDisplayResult(rollType, rollValue) total = rollValue + skillMod + itemMod + defenseMod + shieldMod breakdown = breakdown .. "\nSkill: " .. FormatModifier(skillMod) + table.insert(modifiers, { name = skill and skill.name or "Skill", value = skillMod }) if itemMod ~= 0 then breakdown = breakdown .. " | Item: " .. FormatModifier(itemMod) + table.insert(modifiers, { name = item and item.name or "Item", value = itemMod }) end breakdown = breakdown .. "\nDefense: " .. FormatModifier(defenseMod) + table.insert(modifiers, { name = defense and defense.name or "Defense", value = defenseMod }) if shieldMod ~= 0 then breakdown = breakdown .. " | Shield: " .. FormatModifier(shieldMod) + table.insert(modifiers, { name = "Shield", value = shieldMod }) end breakdown = breakdown .. "\n|cff00ccffDefense Total: " .. total .. "|r" end @@ -86,6 +149,8 @@ function AltSystem:CalculateAndDisplayResult(rollType, rollValue) if AltSystem.ResultText then AltSystem.ResultText:SetText(breakdown) end + + AnnounceRoll(rollValue, modifiers, total) end -- Format a modifier value with sign diff --git a/UI.lua b/UI.lua index cea1152..74c45d4 100644 --- a/UI.lua +++ b/UI.lua @@ -5,7 +5,7 @@ AltSystem = AltSystem or {} local WINDOW_WIDTH = 300 -local WINDOW_HEIGHT = 380 +local WINDOW_HEIGHT = 440 local PADDING = 12 local ROW_HEIGHT = 30 local LABEL_WIDTH = 80 @@ -154,6 +154,52 @@ function AltSystem:CreateMainFrame() AltSystem.State.shieldEnabled = self:GetChecked() end) + yPos = yPos - ROW_HEIGHT - 8 + + ------------------------- + -- Announce checkbox + channel dropdown + ------------------------- + local announceLabel = f:CreateFontString(nil, "OVERLAY", "GameFontNormal") + announceLabel:SetPoint("TOPLEFT", f, "TOPLEFT", PADDING, yPos) + announceLabel:SetText("Announce:") + announceLabel:SetWidth(LABEL_WIDTH) + announceLabel:SetJustifyH("LEFT") + + local announceCheck = CreateFrame("CheckButton", "AltSystemAnnounceCheck", f, "UICheckButtonTemplate") + announceCheck:SetPoint("LEFT", announceLabel, "RIGHT", -6, 0) + announceCheck:SetChecked(AltSystem.State.announceEnabled) + + -- Channel dropdown (shown only when announce is enabled) + local channelOptions = {} + for _, ch in ipairs(AltSystem.AnnounceChannels) do + table.insert(channelOptions, { text = ch.name }) + end + + local channelDropdown, getChannelIndex, setChannelIndex = CreateDropdown( + f, "AltSystemChannelDropdown", yPos, "", channelOptions, + AltSystem.State.announceChannelIndex, + function(index) + AltSystem.State.announceChannelIndex = index + end) + channelDropdown:SetPoint("LEFT", announceCheck, "RIGHT", 2, 0) + channelDropdown:SetWidth(130) + + -- Show/hide channel dropdown based on checkbox state + local function UpdateChannelDropdownVisibility() + if AltSystem.State.announceEnabled then + channelDropdown:Show() + else + channelDropdown:Hide() + end + end + + announceCheck:SetScript("OnClick", function(self) + AltSystem.State.announceEnabled = self:GetChecked() + UpdateChannelDropdownVisibility() + end) + + UpdateChannelDropdownVisibility() + yPos = yPos - ROW_HEIGHT - 12 ------------------------- diff --git a/docs/3-announce.md b/docs/3-announce.md new file mode 100644 index 0000000..2961e7b --- /dev/null +++ b/docs/3-announce.md @@ -0,0 +1,13 @@ +# Feature: Announcing rolls +- There should be a setting to allow announcing rolls in chat +- This setting can be enabled or disabled through a checkbox +- When enabled, a dropdown selection of channels should be shown: + - Emote (/e) + - Party (/p) + - Raid (/ra) + - Guild (/g) +- Selection and enabled/disabled state should be saved +- When enabled, rolls should be announced in the selected channel in the following format: + - "[Name] rolled [result of d20] + [modifier] = [total]" + - Modifiers should be shown with names in parentheses, for example "-2 (First Aid) +2 (Item)" + - Name should be the TRP character first name \ No newline at end of file From 158c1742fe2bfcff855ebf617ea24d46a71fe212 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Correia?= Date: Mon, 13 Apr 2026 14:10:52 +0100 Subject: [PATCH 03/21] Packaging script --- package.sh | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100755 package.sh diff --git a/package.sh b/package.sh new file mode 100755 index 0000000..d3cdbae --- /dev/null +++ b/package.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +ADDON_NAME="AltSystem" +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +OUTPUT_DIR="$(mktemp -d)" +OUTPUT_FILE="$OUTPUT_DIR/$ADDON_NAME.zip" + +cd "$SCRIPT_DIR/.." && \ +zip -r "$OUTPUT_FILE" "$ADDON_NAME" \ + -x "*/.DS_Store" \ + -x "*/__MACOSX/*" \ + -x "*/.git/*" \ + -x "*/.git" + +echo "$OUTPUT_FILE" From f8182ca30a8c7006642680a72f3f889c6fa4cce8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Correia?= Date: Tue, 14 Apr 2026 00:44:08 +0100 Subject: [PATCH 04/21] Fixing emote announce while in raid --- Roll.lua | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/Roll.lua b/Roll.lua index cdfead6..7b368b0 100644 --- a/Roll.lua +++ b/Roll.lua @@ -28,6 +28,20 @@ local function BuildModifierString(modifiers) return table.concat(parts, " ") end +-- Send a message to the given channel, handling EMOTE specially +-- to avoid the message being redirected to raid chat when in a raid group. +local function SendToChannel(msg, channel) + if channel == "EMOTE" then + local editBox = ChatFrame1 and ChatFrame1.editBox or ChatFrame1EditBox + if editBox then + ChatFrame_OpenChat("/e " .. msg, ChatFrame1) + ChatEdit_SendText(editBox) + end + return + end + 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 @@ -45,7 +59,7 @@ local function AnnounceRoll(rollValue, modifiers, total) msg = name .. " rolled " .. rollValue .. " = " .. total end - SendChatMessage(msg, channelDef.channel) + SendToChannel(msg, channelDef.channel) end -- Perform a roll: triggers the WoW native /roll 20 command @@ -82,7 +96,7 @@ function AltSystem:CalculateAndDisplayResult(rollType, rollValue) if AltSystem.State.announceEnabled then local channelDef = AltSystem.AnnounceChannels[AltSystem.State.announceChannelIndex] if channelDef then - SendChatMessage(GetCharacterName() .. " rolled a Critical Failure!", channelDef.channel) + SendToChannel(GetCharacterName() .. " rolled a Critical Failure!", channelDef.channel) end end return @@ -93,7 +107,7 @@ function AltSystem:CalculateAndDisplayResult(rollType, rollValue) if AltSystem.State.announceEnabled then local channelDef = AltSystem.AnnounceChannels[AltSystem.State.announceChannelIndex] if channelDef then - SendChatMessage(GetCharacterName() .. " rolled a Critical Success!", channelDef.channel) + SendToChannel(GetCharacterName() .. " rolled a Critical Success!", channelDef.channel) end end return From 5a0d0f2ca49948610d8055bfd86cfe37ae1ddf5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Correia?= Date: Tue, 14 Apr 2026 00:45:30 +0100 Subject: [PATCH 05/21] Showing +0 mods in announce message --- Roll.lua | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Roll.lua b/Roll.lua index 7b368b0..9fb29c5 100644 --- a/Roll.lua +++ b/Roll.lua @@ -20,10 +20,8 @@ end local function BuildModifierString(modifiers) local parts = {} for _, mod in ipairs(modifiers) do - if mod.value ~= 0 then - local sign = mod.value >= 0 and "+" or "" - table.insert(parts, sign .. mod.value .. " (" .. mod.name .. ")") - end + local sign = mod.value >= 0 and "+" or "" + table.insert(parts, sign .. mod.value .. " (" .. mod.name .. ")") end return table.concat(parts, " ") end From 865404ffb75d5761fda5c8ac6863fe5cc86c89f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Correia?= Date: Tue, 14 Apr 2026 00:49:04 +0100 Subject: [PATCH 06/21] Add a 'base roll' skill --- Data.lua | 13 +++++++++---- Roll.lua | 14 ++++++++++---- UI.lua | 2 +- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/Data.lua b/Data.lua index 38ccf11..6dc2a35 100644 --- a/Data.lua +++ b/Data.lua @@ -12,11 +12,15 @@ AltSystem.Data.SkillLevels = { ["Master"] = 4, } --- The "Inept" entry is always the first (default) skill +-- The "Base roll" entry is always the first (default) skill +local BASE_ROLL_ENTRY = { name = "Base roll", level = "Base", modifier = 0 } + +-- The "Inept" entry is always the second skill local UNSKILLED_ENTRY = { name = "Inept", level = "Inept", 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 = "Inept", level = "Inept", modifier = -4 }, { name = "Novice Skill", level = "Novice", modifier = -2 }, { name = "Adept Skill", level = "Adept", modifier = 0 }, @@ -56,12 +60,13 @@ local function ParseSkillLevel(numericValue) end -- Fetch skills from the current TRP3 profile's personality traits. --- Returns an array of skill entries, always starting with "Inept". +-- Returns an array of skill entries, always starting with "Base roll" and "Inept". -- Falls back to the default list if no profile or no valid skills are found. function AltSystem.Data:RefreshSkills() local skills = {} - -- Always add Inept as the first entry + -- Always add Base roll as the first entry, then Inept + table.insert(skills, { name = BASE_ROLL_ENTRY.name, level = BASE_ROLL_ENTRY.level, modifier = BASE_ROLL_ENTRY.modifier }) table.insert(skills, { name = UNSKILLED_ENTRY.name, level = UNSKILLED_ENTRY.level, modifier = UNSKILLED_ENTRY.modifier }) local foundAny = false @@ -89,7 +94,7 @@ function AltSystem.Data:RefreshSkills() end end - -- If no valid skills were found, use the default fallback list (skip first "Inept" since we already added it) + -- If no valid skills were found, use the default fallback list if not foundAny then skills = {} for _, skill in ipairs(DEFAULT_SKILLS) do diff --git a/Roll.lua b/Roll.lua index 9fb29c5..49e890c 100644 --- a/Roll.lua +++ b/Roll.lua @@ -123,12 +123,16 @@ function AltSystem:CalculateAndDisplayResult(rollType, rollValue) local modifiers = {} + local isBaseRoll = skill and skill.name == "Base roll" + if rollType == "attack" then -- Attack Roll = roll + skill modifier + item modifier total = rollValue + skillMod + itemMod - breakdown = breakdown .. "\nSkill: " .. FormatModifier(skillMod) - table.insert(modifiers, { name = skill and skill.name or "Skill", value = skillMod }) + if not isBaseRoll then + breakdown = breakdown .. "\nSkill: " .. FormatModifier(skillMod) + table.insert(modifiers, { name = skill and skill.name or "Skill", value = skillMod }) + end if itemMod ~= 0 then breakdown = breakdown .. " | Item: " .. FormatModifier(itemMod) table.insert(modifiers, { name = item and item.name or "Item", value = itemMod }) @@ -143,8 +147,10 @@ function AltSystem:CalculateAndDisplayResult(rollType, rollValue) total = rollValue + skillMod + itemMod + defenseMod + shieldMod - breakdown = breakdown .. "\nSkill: " .. FormatModifier(skillMod) - table.insert(modifiers, { name = skill and skill.name or "Skill", value = skillMod }) + if not isBaseRoll then + breakdown = breakdown .. "\nSkill: " .. FormatModifier(skillMod) + table.insert(modifiers, { name = skill and skill.name or "Skill", value = skillMod }) + end if itemMod ~= 0 then breakdown = breakdown .. " | Item: " .. FormatModifier(itemMod) table.insert(modifiers, { name = item and item.name or "Item", value = itemMod }) diff --git a/UI.lua b/UI.lua index 74c45d4..8b7e8d8 100644 --- a/UI.lua +++ b/UI.lua @@ -16,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 == "Inept" then + if skill.level == "Base" or skill.level == "Inept" then text = skill.name .. " (" .. sign .. skill.modifier .. ")" else text = skill.name .. " (" .. skill.level .. " " .. sign .. skill.modifier .. ")" From c4075d1ee906d863dbab86775ffa35491cf25a69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Correia?= Date: Tue, 14 Apr 2026 01:05:11 +0100 Subject: [PATCH 07/21] More fixes to emote channel --- AltSystem.toc | 2 +- Roll.lua | 14 ++++---------- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/AltSystem.toc b/AltSystem.toc index 969c40b..3be4abb 100644 --- a/AltSystem.toc +++ b/AltSystem.toc @@ -2,7 +2,7 @@ ## Title: AltSystem ## Notes: Enhances RP gameplay with a custom rolling system ## Author: Rukira -## Version: 0.3 +## Version: 0.4 ## Dependencies: totalRP3, totalRP3_Extended ## SavedVariables: AltSystemDB diff --git a/Roll.lua b/Roll.lua index 49e890c..e632497 100644 --- a/Roll.lua +++ b/Roll.lua @@ -26,17 +26,11 @@ local function BuildModifierString(modifiers) return table.concat(parts, " ") end --- Send a message to the given channel, handling EMOTE specially --- to avoid the message being redirected to raid chat when in a raid group. +-- Send a message to the given channel. +-- Note: EMOTE channel uses SendChatMessage like all others. The WoW API +-- SendChatMessage(msg, "EMOTE") sends a proper /e emote from any context, +-- unlike RunMacroText which is a protected function requiring a hardware event. local function SendToChannel(msg, channel) - if channel == "EMOTE" then - local editBox = ChatFrame1 and ChatFrame1.editBox or ChatFrame1EditBox - if editBox then - ChatFrame_OpenChat("/e " .. msg, ChatFrame1) - ChatEdit_SendText(editBox) - end - return - end SendChatMessage(msg, channel) end From 88f09f4fe48c26884a2e10613c0efd10c70203ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Correia?= Date: Tue, 14 Apr 2026 01:08:58 +0100 Subject: [PATCH 08/21] Fix emote announce text --- Roll.lua | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/Roll.lua b/Roll.lua index e632497..71b4438 100644 --- a/Roll.lua +++ b/Roll.lua @@ -41,14 +41,23 @@ local function AnnounceRoll(rollValue, modifiers, total) local channelDef = AltSystem.AnnounceChannels[AltSystem.State.announceChannelIndex] if not channelDef then return end - local name = GetCharacterName() + local isEmote = channelDef.channel == "EMOTE" local modStr = BuildModifierString(modifiers) local msg - if modStr ~= "" then - msg = name .. " rolled " .. rollValue .. " " .. modStr .. " = " .. total + if isEmote then + if modStr ~= "" then + msg = "rolled " .. rollValue .. " " .. modStr .. " = " .. total + else + msg = "rolled " .. rollValue .. " = " .. total + end else - msg = name .. " rolled " .. rollValue .. " = " .. total + local name = GetCharacterName() + if modStr ~= "" then + msg = name .. " rolled " .. rollValue .. " " .. modStr .. " = " .. total + else + msg = name .. " rolled " .. rollValue .. " = " .. total + end end SendToChannel(msg, channelDef.channel) @@ -88,7 +97,10 @@ function AltSystem:CalculateAndDisplayResult(rollType, rollValue) if AltSystem.State.announceEnabled then local channelDef = AltSystem.AnnounceChannels[AltSystem.State.announceChannelIndex] if channelDef then - SendToChannel(GetCharacterName() .. " rolled a Critical Failure!", channelDef.channel) + local critMsg = channelDef.channel == "EMOTE" + and "rolled a Critical Failure!" + or (GetCharacterName() .. " rolled a Critical Failure!") + SendToChannel(critMsg, channelDef.channel) end end return @@ -99,7 +111,10 @@ function AltSystem:CalculateAndDisplayResult(rollType, rollValue) if AltSystem.State.announceEnabled then local channelDef = AltSystem.AnnounceChannels[AltSystem.State.announceChannelIndex] if channelDef then - SendToChannel(GetCharacterName() .. " rolled a Critical Success!", channelDef.channel) + local critMsg = channelDef.channel == "EMOTE" + and "rolled a Critical Success!" + or (GetCharacterName() .. " rolled a Critical Success!") + SendToChannel(critMsg, channelDef.channel) end end return From 2c878669b6697cc0a60596beee3c6b58247b4227 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Correia?= Date: Tue, 14 Apr 2026 01:12:51 +0100 Subject: [PATCH 09/21] Improved packaging script --- package.sh | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/package.sh b/package.sh index d3cdbae..6febfa8 100755 --- a/package.sh +++ b/package.sh @@ -4,12 +4,14 @@ ADDON_NAME="AltSystem" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" OUTPUT_DIR="$(mktemp -d)" OUTPUT_FILE="$OUTPUT_DIR/$ADDON_NAME.zip" +STAGING_DIR="$(mktemp -d)" -cd "$SCRIPT_DIR/.." && \ -zip -r "$OUTPUT_FILE" "$ADDON_NAME" \ - -x "*/.DS_Store" \ - -x "*/__MACOSX/*" \ - -x "*/.git/*" \ - -x "*/.git" +mkdir -p "$STAGING_DIR/$ADDON_NAME" +rsync -a --exclude='.DS_Store' --exclude='__MACOSX' --exclude='.git' "$SCRIPT_DIR/" "$STAGING_DIR/$ADDON_NAME/" + +cd "$STAGING_DIR" && \ +zip -r "$OUTPUT_FILE" "$ADDON_NAME" + +rm -rf "$STAGING_DIR" echo "$OUTPUT_FILE" From 52c27010751d370709730d43ad505100dc2ecb5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Correia?= Date: Wed, 22 Apr 2026 16:16:31 +0100 Subject: [PATCH 10/21] TOC bump --- AltSystem.toc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AltSystem.toc b/AltSystem.toc index 3be4abb..031f3db 100644 --- a/AltSystem.toc +++ b/AltSystem.toc @@ -1,4 +1,4 @@ -## Interface: 120001 +## Interface: 120005 ## Title: AltSystem ## Notes: Enhances RP gameplay with a custom rolling system ## Author: Rukira From af5a3d2d41a5c6844fa1934b52068891550dfb57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Correia?= Date: Wed, 22 Apr 2026 16:17:06 +0100 Subject: [PATCH 11/21] TOC bump --- AltSystem.toc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AltSystem.toc b/AltSystem.toc index 031f3db..3e5feca 100644 --- a/AltSystem.toc +++ b/AltSystem.toc @@ -2,7 +2,7 @@ ## Title: AltSystem ## Notes: Enhances RP gameplay with a custom rolling system ## Author: Rukira -## Version: 0.4 +## Version: 0.5 ## Dependencies: totalRP3, totalRP3_Extended ## SavedVariables: AltSystemDB From 961d642018cf5e9a40293a1756225670d9de46a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Correia?= Date: Fri, 8 May 2026 16:14:45 +0100 Subject: [PATCH 12/21] Inept skill level --- AltSystem.toc | 2 +- Data.lua | 27 +++++++++++++-------------- UI.lua | 2 +- docs/2-skills.md | 6 +++--- 4 files changed, 18 insertions(+), 19 deletions(-) diff --git a/AltSystem.toc b/AltSystem.toc index 3e5feca..b304469 100644 --- a/AltSystem.toc +++ b/AltSystem.toc @@ -2,7 +2,7 @@ ## Title: AltSystem ## Notes: Enhances RP gameplay with a custom rolling system ## Author: Rukira -## Version: 0.5 +## Version: 0.6 ## Dependencies: totalRP3, totalRP3_Extended ## SavedVariables: AltSystemDB diff --git a/Data.lua b/Data.lua index 6dc2a35..164eae0 100644 --- a/Data.lua +++ b/Data.lua @@ -6,6 +6,7 @@ AltSystem.Data = {} -- Skill levels and their modifiers AltSystem.Data.SkillLevels = { + ["Inept"] = -4, ["Novice"] = -2, ["Adept"] = 0, ["Expert"] = 2, @@ -15,13 +16,9 @@ AltSystem.Data.SkillLevels = { -- The "Base roll" entry is always the first (default) skill local BASE_ROLL_ENTRY = { name = "Base roll", level = "Base", modifier = 0 } --- The "Inept" entry is always the second skill -local UNSKILLED_ENTRY = { name = "Inept", level = "Inept", 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 = "Inept", level = "Inept", modifier = -4 }, { name = "Novice Skill", level = "Novice", modifier = -2 }, { name = "Adept Skill", level = "Adept", modifier = 0 }, { name = "Expert Skill", level = "Expert", modifier = 2 }, @@ -29,7 +26,7 @@ local DEFAULT_SKILLS = { } -- Valid skill level keywords that must appear in the trait's right field (RT) -local VALID_SKILL_KEYWORDS = { "Novice", "Adept", "Expert", "Master" } +local VALID_SKILL_KEYWORDS = { "Inept", "Novice", "Adept", "Expert", "Master" } -- Check if the trait's right text field contains a valid skill keyword. -- Returns true if any keyword is found, false otherwise. @@ -43,11 +40,14 @@ local function HasSkillKeyword(rightText) return false 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) +-- Determine the skill level from the trait's right text and numeric value (V2 field, 0-20 range). +-- If the trait is marked as Inept, always returns "Inept" with -4 modifier regardless of numeric value. +-- Otherwise returns the level string and modifier based on numeric value, or nil if the value is 0 or absent. +local function ParseSkillLevel(rightText, numericValue) + if rightText and rightText:find("Inept") then + return "Inept", AltSystem.Data.SkillLevels["Inept"] + end if not numericValue or numericValue <= 0 then return nil, nil end - if numericValue >= 20 then return "Master", AltSystem.Data.SkillLevels["Master"] elseif numericValue >= 11 then @@ -60,14 +60,13 @@ local function ParseSkillLevel(numericValue) end -- Fetch skills from the current TRP3 profile's personality traits. --- Returns an array of skill entries, always starting with "Base roll" and "Inept". +-- Returns an array of skill entries, always starting with "Base roll". +-- Traits marked as Inept get a -4 modifier regardless of their numeric value. -- Falls back to the default list if no profile or no valid skills are found. function AltSystem.Data:RefreshSkills() local skills = {} - - -- Always add Base roll as the first entry, then Inept + -- 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 }) - table.insert(skills, { name = UNSKILLED_ENTRY.name, level = UNSKILLED_ENTRY.level, modifier = UNSKILLED_ENTRY.modifier }) local foundAny = false @@ -80,7 +79,7 @@ function AltSystem.Data:RefreshSkills() local numericValue = trait.V2 if skillName and skillName ~= "" and HasSkillKeyword(trait.RT) then - local level, modifier = ParseSkillLevel(numericValue) + local level, modifier = ParseSkillLevel(trait.RT, numericValue) if level and modifier then foundAny = true table.insert(skills, { diff --git a/UI.lua b/UI.lua index 8b7e8d8..839cd58 100644 --- a/UI.lua +++ b/UI.lua @@ -256,7 +256,7 @@ end function AltSystem:RefreshSkillDropdown() AltSystem.Data:RefreshSkills() - -- Reset selection to 1 (Inept) since the skill list may have changed + -- Reset selection to 1 (Base roll) since the skill list may have changed AltSystem.State.selectedSkillIndex = 1 if AltSystem.SetSkillIndex then AltSystem.SetSkillIndex(1) diff --git a/docs/2-skills.md b/docs/2-skills.md index f118ffe..cd4429d 100644 --- a/docs/2-skills.md +++ b/docs/2-skills.md @@ -3,13 +3,13 @@ - The skills are defined in the TRP profile as 'Personality traits' - For each trait: - the left field represents the skill name - - the right field must contain a valid skill keyword (Novice, Adept, Expert, Master) — traits without one of these keywords are omitted + - the right field must contain a valid skill keyword (Inept, Novice, Adept, Expert, Master) — traits without one of these keywords are omitted + - if the right field contains "Inept", the trait always gets a -4 modifier regardless of its numeric value - the first numeric value (V2) determines the skill level based on these ranges: - Novice: 1-5 - Adept: 6-10 - Expert: 11-19 - Master: 20 - should a skill have a value of 0 or no value, it should be omitted from the list -- The list should have a default selected value of "Inept" 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 - - Inept, Novice Skill, Adept Skill, Expert Skill, Master Skill \ No newline at end of file + - Base roll, Novice Skill, Adept Skill, Expert Skill, Master Skill \ No newline at end of file From 0d9ec1913d436f8cd78a1c5a78b485aa69a4b705 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Correia?= Date: Fri, 8 May 2026 16:33:36 +0100 Subject: [PATCH 13/21] Skill level mismatch warning --- Data.lua | 35 +++++++++++++++++++++++++++++------ UI.lua | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 6 deletions(-) diff --git a/Data.lua b/Data.lua index 164eae0..19d5ca9 100644 --- a/Data.lua +++ b/Data.lua @@ -28,16 +28,37 @@ local DEFAULT_SKILLS = { -- 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 }, +} + -- Check if the trait's right text field contains a valid skill keyword. --- Returns true if any keyword is found, false otherwise. -local function HasSkillKeyword(rightText) - if not rightText or rightText == "" then return false end +-- Returns the matched keyword if found, nil otherwise. +local function FindSkillKeyword(rightText) + if not rightText or rightText == "" then return nil end for _, keyword in ipairs(VALID_SKILL_KEYWORDS) do if rightText:find(keyword) then - return true + return keyword end end - return false + return nil +end + +-- Check whether the numeric value matches the expected range for the given keyword. +-- Returns a warning string if mismatched, or nil if everything is fine. +local function CheckSkillMismatch(keyword, numericValue) + if not keyword or keyword == "Inept" then return nil end + local range = SKILL_KEYWORD_RANGES[keyword] + if not range then return nil end + if not numericValue or numericValue <= 0 then return nil end + if numericValue < range.min or numericValue > range.max then + return "Skill is marked as " .. keyword .. " (" .. range.min .. "-" .. range.max .. "), but the profile has a value of " .. numericValue .. "." + end + return nil end -- Determine the skill level from the trait's right text and numeric value (V2 field, 0-20 range). @@ -78,7 +99,8 @@ function AltSystem.Data:RefreshSkills() local skillName = trait.LT local numericValue = trait.V2 - if skillName and skillName ~= "" and HasSkillKeyword(trait.RT) then + local keyword = FindSkillKeyword(trait.RT) + if skillName and skillName ~= "" and keyword then local level, modifier = ParseSkillLevel(trait.RT, numericValue) if level and modifier then foundAny = true @@ -86,6 +108,7 @@ function AltSystem.Data:RefreshSkills() name = skillName, level = level, modifier = modifier, + warning = CheckSkillMismatch(keyword, numericValue), }) end end diff --git a/UI.lua b/UI.lua index 839cd58..81b3bd3 100644 --- a/UI.lua +++ b/UI.lua @@ -83,18 +83,53 @@ function AltSystem:CreateMainFrame() -- Skill dropdown ------------------------- local skillOptions = BuildSkillOptions() + local UpdateSkillWarning -- forward declaration local skillDropdown, getSkillIndex, setSkillIndex = CreateDropdown( f, "AltSystemSkillDropdown", yPos, "Skill:", skillOptions, AltSystem.State.selectedSkillIndex, function(index) AltSystem.State.selectedSkillIndex = index + UpdateSkillWarning(index) end) + -- Warning icon for skill mismatch (shown next to dropdown when value doesn't match keyword) + local skillWarning = CreateFrame("Frame", nil, f) + 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() + + -- Update the warning icon visibility based on the currently selected skill + UpdateSkillWarning = function(index) + local skill = AltSystem.Data.Skills[index] + if skill and skill.warning then + skillWarning.tooltipText = skill.warning + skillWarning:Show() + else + skillWarning:Hide() + end + end + -- Store references for refreshing AltSystem.SkillDropdown = skillDropdown AltSystem.GetSkillIndex = getSkillIndex AltSystem.SetSkillIndex = setSkillIndex + AltSystem.UpdateSkillWarning = UpdateSkillWarning yPos = yPos - ROW_HEIGHT - 8 ------------------------- @@ -261,6 +296,9 @@ function AltSystem:RefreshSkillDropdown() if AltSystem.SetSkillIndex then AltSystem.SetSkillIndex(1) end + if AltSystem.UpdateSkillWarning then + AltSystem.UpdateSkillWarning(1) + end -- Rebuild the dropdown menu with the new skill list if AltSystem.SkillDropdown then @@ -275,6 +313,9 @@ function AltSystem:RefreshSkillDropdown() if AltSystem.SetSkillIndex then AltSystem.SetSkillIndex(data) end + if AltSystem.UpdateSkillWarning then + AltSystem.UpdateSkillWarning(data) + end end, i ) From ed1e696eb5db61ed6d666d7f29b1b59a8283a8b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Correia?= Date: Sat, 9 May 2026 02:11:24 +0100 Subject: [PATCH 14/21] Pet modifier --- AltSystem.toc | 2 +- Core.lua | 5 +++++ Roll.lua | 41 +++++++++++++++++++++++++++++++++++++---- UI.lua | 25 ++++++++++++++++++++++++- 4 files changed, 67 insertions(+), 6 deletions(-) diff --git a/AltSystem.toc b/AltSystem.toc index b304469..0b03820 100644 --- a/AltSystem.toc +++ b/AltSystem.toc @@ -2,7 +2,7 @@ ## Title: AltSystem ## Notes: Enhances RP gameplay with a custom rolling system ## Author: Rukira -## Version: 0.6 +## Version: 1.0 ## Dependencies: totalRP3, totalRP3_Extended ## SavedVariables: AltSystemDB diff --git a/Core.lua b/Core.lua index c5bb3a8..68c41ac 100644 --- a/Core.lua +++ b/Core.lua @@ -7,6 +7,7 @@ AltSystem.State = { selectedItemIndex = 1, -- 1 = No item selectedDefenseIndex = 1, -- 1 = Base armor shieldEnabled = false, + petSummonEnabled = false, announceEnabled = false, announceChannelIndex = 1, -- 1 = Emote, 2 = Party, 3 = Raid, 4 = Guild } @@ -39,6 +40,9 @@ function AltSystem:Init() if AltSystemDB.announceChannelIndex then AltSystem.State.announceChannelIndex = AltSystemDB.announceChannelIndex end + if AltSystemDB.petSummonEnabled ~= nil then + AltSystem.State.petSummonEnabled = AltSystemDB.petSummonEnabled + end -- Register slash command /altsystem SLASH_ALTSYSTEM1 = "/altsystem" @@ -53,6 +57,7 @@ function AltSystem:Init() AltSystemDB = AltSystemDB or {} AltSystemDB.announceEnabled = AltSystem.State.announceEnabled AltSystemDB.announceChannelIndex = AltSystem.State.announceChannelIndex + AltSystemDB.petSummonEnabled = AltSystem.State.petSummonEnabled end) print("|cff00ccffAltSystem|r loaded. Type /altsystem to open.") diff --git a/Roll.lua b/Roll.lua index 71b4438..1e98b0c 100644 --- a/Roll.lua +++ b/Roll.lua @@ -4,6 +4,7 @@ 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() @@ -65,6 +66,7 @@ end -- Perform a roll: triggers the WoW native /roll 20 command function AltSystem:PerformRoll(rollType) + pendingPetRoll = nil pendingRollType = rollType RandomRoll(1, 20) end @@ -74,6 +76,20 @@ 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 petRoll = message:match("rolls (%d+) %(1%-5%)") + if not petRoll 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)" @@ -84,11 +100,18 @@ rollListener:SetScript("OnEvent", function(self, event, message) local rollType = pendingRollType pendingRollType = nil + -- If pet/summon is enabled, trigger a second roll (1-5) + if AltSystem.State.petSummonEnabled then + pendingPetRoll = { rollType = rollType, mainRollValue = rollValue } + RandomRoll(1, 5) + return + end + AltSystem:CalculateAndDisplayResult(rollType, rollValue) end) -- Calculate the final result based on the roll type and selected modifiers -function AltSystem:CalculateAndDisplayResult(rollType, rollValue) +function AltSystem:CalculateAndDisplayResult(rollType, rollValue, petRollValue) -- Critical rolls bypass normal calculation if rollValue == 1 then if AltSystem.ResultText then @@ -134,9 +157,11 @@ function AltSystem:CalculateAndDisplayResult(rollType, rollValue) local isBaseRoll = skill and skill.name == "Base roll" + local petMod = petRollValue or 0 + if rollType == "attack" then - -- Attack Roll = roll + skill modifier + item modifier - total = rollValue + skillMod + itemMod + -- Attack Roll = roll + skill modifier + item modifier + pet/summon modifier + total = rollValue + skillMod + itemMod + petMod if not isBaseRoll then breakdown = breakdown .. "\nSkill: " .. FormatModifier(skillMod) @@ -146,6 +171,10 @@ function AltSystem:CalculateAndDisplayResult(rollType, rollValue) breakdown = breakdown .. " | Item: " .. FormatModifier(itemMod) table.insert(modifiers, { name = item and item.name or "Item", value = itemMod }) end + if petMod ~= 0 then + breakdown = breakdown .. " | Pet: +" .. petMod + table.insert(modifiers, { name = "Pet", value = petMod }) + end breakdown = breakdown .. "\n|cffffd100Attack Total: " .. total .. "|r" elseif rollType == "defense" then @@ -154,7 +183,7 @@ function AltSystem:CalculateAndDisplayResult(rollType, rollValue) local defenseMod = defense and defense.modifier or 0 local shieldMod = state.shieldEnabled and AltSystem.Data.ShieldModifier or 0 - total = rollValue + skillMod + itemMod + defenseMod + shieldMod + total = rollValue + skillMod + itemMod + defenseMod + shieldMod + petMod if not isBaseRoll then breakdown = breakdown .. "\nSkill: " .. FormatModifier(skillMod) @@ -170,6 +199,10 @@ function AltSystem:CalculateAndDisplayResult(rollType, rollValue) breakdown = breakdown .. " | Shield: " .. FormatModifier(shieldMod) table.insert(modifiers, { name = "Shield", value = shieldMod }) end + if petMod ~= 0 then + breakdown = breakdown .. " | Pet: +" .. petMod + table.insert(modifiers, { name = "Pet", value = petMod }) + end breakdown = breakdown .. "\n|cff00ccffDefense Total: " .. total .. "|r" end diff --git a/UI.lua b/UI.lua index 81b3bd3..c425d9d 100644 --- a/UI.lua +++ b/UI.lua @@ -5,7 +5,7 @@ AltSystem = AltSystem or {} local WINDOW_WIDTH = 300 -local WINDOW_HEIGHT = 440 +local WINDOW_HEIGHT = 478 local PADDING = 12 local ROW_HEIGHT = 30 local LABEL_WIDTH = 80 @@ -191,6 +191,29 @@ function AltSystem:CreateMainFrame() yPos = yPos - ROW_HEIGHT - 8 + ------------------------- + -- Pet/Summon checkbox + ------------------------- + local petLabel = f:CreateFontString(nil, "OVERLAY", "GameFontNormal") + petLabel:SetPoint("TOPLEFT", f, "TOPLEFT", PADDING, yPos) + petLabel:SetText("Pet:") + petLabel:SetWidth(LABEL_WIDTH) + petLabel:SetJustifyH("LEFT") + + local petCheck = CreateFrame("CheckButton", "AltSystemPetSummonCheck", f, "UICheckButtonTemplate") + petCheck:SetPoint("LEFT", petLabel, "RIGHT", -6, 0) + petCheck:SetChecked(AltSystem.State.petSummonEnabled) + + local petText = petCheck:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall") + petText:SetPoint("LEFT", petCheck, "RIGHT", 2, 0) + petText:SetText("+d5 modifier") + + petCheck:SetScript("OnClick", function(self) + AltSystem.State.petSummonEnabled = self:GetChecked() + end) + + yPos = yPos - ROW_HEIGHT - 8 + ------------------------- -- Announce checkbox + channel dropdown ------------------------- From ab8388e27c6bc5038fc5fcb13ec923f97f5c4d3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Correia?= Date: Mon, 11 May 2026 12:20:26 +0100 Subject: [PATCH 15/21] Fixing roll detection by character name --- AltSystem.toc | 2 +- Roll.lua | 6 ++++-- docs/Changelog.md | 1 + 3 files changed, 6 insertions(+), 3 deletions(-) create mode 100644 docs/Changelog.md diff --git a/AltSystem.toc b/AltSystem.toc index 0b03820..e541ac4 100644 --- a/AltSystem.toc +++ b/AltSystem.toc @@ -2,7 +2,7 @@ ## Title: AltSystem ## Notes: Enhances RP gameplay with a custom rolling system ## Author: Rukira -## Version: 1.0 +## Version: 1.1 ## Dependencies: totalRP3, totalRP3_Extended ## SavedVariables: AltSystemDB diff --git a/Roll.lua b/Roll.lua index 1e98b0c..a37ca8b 100644 --- a/Roll.lua +++ b/Roll.lua @@ -78,8 +78,9 @@ 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 petRoll = message:match("rolls (%d+) %(1%-5%)") + 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 @@ -93,8 +94,9 @@ rollListener:SetScript("OnEvent", function(self, event, message) if not pendingRollType then return end -- Match the roll result pattern: "PlayerName rolls X (1-20)" - local roll = message:match("rolls (%d+) %(1%-20%)") + local roller, roll = message:match("(.+) rolls (%d+) %(1%-20%)") if not roll then return end + if roller ~= UnitName("player") then return end local rollValue = tonumber(roll) local rollType = pendingRollType diff --git a/docs/Changelog.md b/docs/Changelog.md new file mode 100644 index 0000000..dad8bdc --- /dev/null +++ b/docs/Changelog.md @@ -0,0 +1 @@ +- Fixed an issue where the wrong rolls were displayed when in a large enough group \ No newline at end of file From 5b36bfdb1b1d5cdce217f6a7be633c5ad637e40b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Correia?= Date: Mon, 11 May 2026 12:24:48 +0100 Subject: [PATCH 16/21] Adding unskilled default option --- Data.lua | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Data.lua b/Data.lua index 19d5ca9..e46d887 100644 --- a/Data.lua +++ b/Data.lua @@ -14,15 +14,18 @@ AltSystem.Data.SkillLevels = { } -- The "Base roll" entry is always the first (default) skill -local BASE_ROLL_ENTRY = { name = "Base roll", level = "Base", modifier = 0 } +local BASE_ROLL_ENTRY = { name = "Base roll", level = "Base", modifier = 0 } +-- The "Unskilled" entry is always the last skill +local UNSKILLED_ENTRY = { name = "Unskilled", level = "Unskilled", modifier = -4 } -- Default/fallback skill list used when no TRP3 profile skills are found local DEFAULT_SKILLS = { { name = "Base roll", level = "Base", modifier = 0 }, - { name = "Novice Skill", level = "Novice", modifier = -2 }, - { name = "Adept Skill", level = "Adept", modifier = 0 }, + { name = "Novice Skill", level = "Novice", modifier = -2 }, + { name = "Adept Skill", level = "Adept", modifier = 0 }, { name = "Expert Skill", level = "Expert", modifier = 2 }, { name = "Master Skill", level = "Master", modifier = 4 }, + { name = "Unskilled", level = "Unskilled", modifier = -4 }, } -- Valid skill level keywords that must appear in the trait's right field (RT) @@ -124,6 +127,9 @@ 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 From c27a6cd033c665dfe5891c67a3487e735c3c4796 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Correia?= Date: Mon, 11 May 2026 13:59:44 +0100 Subject: [PATCH 17/21] Remember user selections across frame instances --- Core.lua | 20 ++++++++++++++++++++ UI.lua | 24 ++++++++++++++++++------ docs/Changelog.md | 1 + 3 files changed, 39 insertions(+), 6 deletions(-) diff --git a/Core.lua b/Core.lua index 68c41ac..a3d1ab9 100644 --- a/Core.lua +++ b/Core.lua @@ -4,6 +4,7 @@ AltSystem = AltSystem or {} AltSystem.State = { selectedSkillIndex = 1, + selectedSkillName = nil, -- skill name used to restore selection across sessions selectedItemIndex = 1, -- 1 = No item selectedDefenseIndex = 1, -- 1 = Base armor shieldEnabled = false, @@ -43,6 +44,18 @@ function AltSystem:Init() 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 -- Register slash command /altsystem SLASH_ALTSYSTEM1 = "/altsystem" @@ -58,8 +71,15 @@ function AltSystem:Init() AltSystemDB.announceEnabled = AltSystem.State.announceEnabled AltSystemDB.announceChannelIndex = AltSystem.State.announceChannelIndex 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 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/UI.lua b/UI.lua index c425d9d..5162da2 100644 --- a/UI.lua +++ b/UI.lua @@ -90,6 +90,7 @@ function AltSystem:CreateMainFrame() 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) @@ -314,13 +315,24 @@ end function AltSystem:RefreshSkillDropdown() AltSystem.Data:RefreshSkills() - -- Reset selection to 1 (Base roll) since the skill list may have changed - AltSystem.State.selectedSkillIndex = 1 + -- Try to restore the previously selected skill by name; fall back to 1 (Base roll) + local newIndex = 1 + local savedName = AltSystem.State.selectedSkillName + if savedName then + for i, skill in ipairs(AltSystem.Data.Skills) do + if skill.name == savedName then + newIndex = i + break + end + end + end + AltSystem.State.selectedSkillIndex = newIndex + AltSystem.State.selectedSkillName = AltSystem.Data.Skills[newIndex] and AltSystem.Data.Skills[newIndex].name or nil if AltSystem.SetSkillIndex then - AltSystem.SetSkillIndex(1) + AltSystem.SetSkillIndex(newIndex) end if AltSystem.UpdateSkillWarning then - AltSystem.UpdateSkillWarning(1) + AltSystem.UpdateSkillWarning(newIndex) end -- Rebuild the dropdown menu with the new skill list @@ -333,6 +345,7 @@ function AltSystem:RefreshSkillDropdown() function(data) return data == 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 if AltSystem.SetSkillIndex then AltSystem.SetSkillIndex(data) end @@ -347,5 +360,4 @@ function AltSystem:RefreshSkillDropdown() end end --- Create the frame on file load so it's ready when Init runs -AltSystem:CreateMainFrame() +-- Frame is created by AltSystem:Init() in Core.lua, after saved variables are loaded diff --git a/docs/Changelog.md b/docs/Changelog.md index dad8bdc..405d8f0 100644 --- a/docs/Changelog.md +++ b/docs/Changelog.md @@ -1 +1,2 @@ +- The roll window now remembers all user selections (skill, item, armor type, shield, pet, announce and channel) across sessions - Fixed an issue where the wrong rolls were displayed when in a large enough group \ No newline at end of file From 6a444584431d81fe64c422682657001ab0cc9e90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Correia?= Date: Mon, 11 May 2026 14:00:10 +0100 Subject: [PATCH 18/21] Unskilled --- docs/Changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/Changelog.md b/docs/Changelog.md index 405d8f0..dd05c6a 100644 --- a/docs/Changelog.md +++ b/docs/Changelog.md @@ -1,2 +1,3 @@ +- Adding default 'Unskilled (-4)' option - The roll window now remembers all user selections (skill, item, armor type, shield, pet, announce and channel) across sessions - Fixed an issue where the wrong rolls were displayed when in a large enough group \ No newline at end of file From 6ebb92c62b12686ef8e673e2e88a2c112571d605 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Correia?= Date: Fri, 15 May 2026 14:53:15 +0100 Subject: [PATCH 19/21] feat/redesign (#1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reviewed-on: https://git.asarius.site/rukira/AltSystem/pulls/1 Co-authored-by: Gonçalo Correia Co-committed-by: Gonçalo Correia --- .idea/.gitignore | 0 .idea/misc.xml | 0 .idea/modules.xml | 0 .idea/vcs.xml | 0 AltSystem.iml | 0 AltSystem.toc | 3 +- BuildSkillsUI.lua | 430 ++++++++++++++++++ Core.lua | 37 +- Data.lua | 125 +++++- README.md | 0 Roll.lua | 109 +++-- UI.lua | 749 ++++++++++++++++++++++--------- docs/1-interface.md | 0 docs/2-skills.md | 0 docs/3-announce.md | 0 docs/4-redesign.md | 78 ++++ docs/5-build_skills.md | 132 ++++++ docs/Changelog.md | 3 - docs/build_skills_tab_design.png | Bin 0 -> 100436 bytes docs/roll_tab_design.png | Bin 0 -> 75385 bytes package.sh | 2 +- 21 files changed, 1395 insertions(+), 273 deletions(-) mode change 100644 => 100755 .idea/.gitignore mode change 100644 => 100755 .idea/misc.xml mode change 100644 => 100755 .idea/modules.xml mode change 100644 => 100755 .idea/vcs.xml mode change 100644 => 100755 AltSystem.iml mode change 100644 => 100755 AltSystem.toc create mode 100755 BuildSkillsUI.lua mode change 100644 => 100755 Core.lua mode change 100644 => 100755 Data.lua mode change 100644 => 100755 README.md mode change 100644 => 100755 Roll.lua mode change 100644 => 100755 UI.lua mode change 100644 => 100755 docs/1-interface.md mode change 100644 => 100755 docs/2-skills.md mode change 100644 => 100755 docs/3-announce.md create mode 100755 docs/4-redesign.md create mode 100755 docs/5-build_skills.md mode change 100644 => 100755 docs/Changelog.md create mode 100755 docs/build_skills_tab_design.png create mode 100644 docs/roll_tab_design.png 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 e541ac4..3b8554d --- a/AltSystem.toc +++ b/AltSystem.toc @@ -2,11 +2,12 @@ ## Title: AltSystem ## Notes: Enhances RP gameplay with a custom rolling system ## Author: Rukira -## Version: 1.1 +## Version: 1.2.0 ## Dependencies: totalRP3, totalRP3_Extended ## SavedVariables: AltSystemDB Data.lua Core.lua +BuildSkillsUI.lua UI.lua Roll.lua diff --git a/BuildSkillsUI.lua b/BuildSkillsUI.lua new file mode 100755 index 0000000..d8e7f8f --- /dev/null +++ b/BuildSkillsUI.lua @@ -0,0 +1,430 @@ +-- AltSystem Build Skills UI +-- Creates the Build Skills tab content with editable skill rows, add/delete functionality, and save to TRP. + +AltSystem = AltSystem or {} + +local PADDING = 12 +local ROW_HEIGHT = 36 +local ROW_SPACING = 8 +local NAME_WIDTH = 340 +local LEVEL_WIDTH = 140 +local VALUE_WIDTH = 80 +local DELETE_WIDTH = 30 + +-- Working copy of skills being edited (not saved until user clicks Save) +local editableSkills = {} + +-- Pool of created row frames for reuse +local skillRowFrames = {} + +-- References set during creation +local scrollFrame, scrollChild, addRowButton + +-- ─── Helper: Create a flat dark input box matching the blocky design ──────── + +local function CreateFlatEditBox(name, parent, width) + local container = CreateFrame("Frame", name .. "Container", parent, "BackdropTemplate") + container:SetSize(width, 28) + container:SetBackdrop({ + bgFile = "Interface\\ChatFrame\\ChatFrameBackground", + edgeFile = "Interface\\ChatFrame\\ChatFrameBackground", + edgeSize = 1, + }) + container:SetBackdropColor(0.12, 0.12, 0.12, 1) + container:SetBackdropBorderColor(0.25, 0.25, 0.25, 1) + + local editBox = CreateFrame("EditBox", name, container) + editBox:SetPoint("LEFT", 8, 0) + editBox:SetPoint("RIGHT", -8, 0) + editBox:SetHeight(28) + editBox:SetAutoFocus(false) + editBox:SetFontObject("GameFontHighlight") + + return container, editBox +end + +-- ─── Helper: Create a flat dark dropdown button matching the blocky design ── +-- Shared globally so other files (UI.lua) can reuse it. + +function AltSystem.CreateFlatDropdown(name, parent, width) + local btn = CreateFrame("Button", name, parent, "BackdropTemplate") + btn:SetSize(width, 28) + btn:SetBackdrop({ + bgFile = "Interface\\ChatFrame\\ChatFrameBackground", + edgeFile = "Interface\\ChatFrame\\ChatFrameBackground", + edgeSize = 1, + }) + btn:SetBackdropColor(0.12, 0.12, 0.12, 1) + btn:SetBackdropBorderColor(0.25, 0.25, 0.25, 1) + + -- Text label (left-aligned) + local text = btn:CreateFontString(nil, "OVERLAY", "GameFontHighlight") + text:SetPoint("LEFT", 8, 0) + text:SetPoint("RIGHT", -24, 0) + text:SetJustifyH("LEFT") + btn.label = text + + -- Gold arrow icon (right side) + local arrow = btn:CreateTexture(nil, "OVERLAY") + arrow:SetSize(12, 12) + arrow:SetPoint("RIGHT", -6, 0) + arrow:SetTexture("Interface\\ChatFrame\\ChatFrameExpandArrow") + arrow:SetVertexColor(0.9, 0.75, 0.2, 1) + btn.arrow = arrow + + -- Hover highlight + btn:SetScript("OnEnter", function(self) + self:SetBackdropColor(0.18, 0.18, 0.18, 1) + end) + btn:SetScript("OnLeave", function(self) + self:SetBackdropColor(0.12, 0.12, 0.12, 1) + end) + + -- Menu storage + btn.menuSetup = nil + btn:SetScript("OnClick", function(self) + if self.menuSetup then + MenuUtil.CreateContextMenu(self, self.menuSetup) + end + end) + + function btn:SetupMenu(setupFunc) + self.menuSetup = setupFunc + end + + return btn +end + +-- ─── Helper: Create a flat dark button matching the blocky design ──────────── +-- Shared globally so other files (UI.lua) can reuse it. + +function AltSystem.CreateFlatButton(name, parent, width, height, text) + local btn = CreateFrame("Button", name, parent, "BackdropTemplate") + btn:SetSize(width, height) + btn:SetBackdrop({ + bgFile = "Interface\\ChatFrame\\ChatFrameBackground", + edgeFile = "Interface\\ChatFrame\\ChatFrameBackground", + edgeSize = 1, + }) + btn:SetBackdropColor(0.4, 0.08, 0.08, 0.9) + btn:SetBackdropBorderColor(0.25, 0.25, 0.25, 1) + + local label = btn:CreateFontString(nil, "OVERLAY", "GameFontNormal") + label:SetPoint("CENTER") + label:SetText(text or "") + btn.label = label + + btn:SetScript("OnEnter", function(self) + self:SetBackdropColor(0.55, 0.12, 0.12, 0.9) + end) + btn:SetScript("OnLeave", function(self) + self:SetBackdropColor(0.4, 0.08, 0.08, 0.9) + end) + + function btn:SetText(newText) + self.label:SetText(newText) + end + + function btn:GetText() + return self.label:GetText() + end + + return btn +end + +-- ─── Create a single skill row frame ──────────────────────────────────────── + +local function CreateSkillRowFrame(index) + local row = CreateFrame("Frame", "AltSystemSkillRow" .. index, scrollChild) + row:SetHeight(ROW_HEIGHT) + + -- Name EditBox (flat dark style) + local nameContainer, nameBox = CreateFlatEditBox("AltSystemSkillName" .. index, row, NAME_WIDTH) + nameContainer:SetPoint("LEFT", row, "LEFT", 8, 0) + row.nameBox = nameBox + row.nameContainer = nameContainer + + -- Level Dropdown (flat dark style) + local levelDropdown = AltSystem.CreateFlatDropdown("AltSystemSkillLevel" .. index, row, LEVEL_WIDTH) + levelDropdown:SetPoint("LEFT", nameContainer, "RIGHT", 12, 0) + row.levelDropdown = levelDropdown + + -- Value Dropdown (flat dark style) + local valueDropdown = AltSystem.CreateFlatDropdown("AltSystemSkillValue" .. index, row, VALUE_WIDTH) + valueDropdown:SetPoint("LEFT", levelDropdown, "RIGHT", 8, 0) + row.valueDropdown = valueDropdown + + -- Delete Button (trash can icon with dark red background, matching design) + local deleteBtn = CreateFrame("Button", "AltSystemSkillDelete" .. index, row) + deleteBtn:SetSize(DELETE_WIDTH, DELETE_WIDTH) + deleteBtn:SetPoint("LEFT", valueDropdown, "RIGHT", 8, 0) + + + local deleteIcon = deleteBtn:CreateTexture(nil, "ARTWORK") + deleteIcon:SetSize(DELETE_WIDTH - 4, DELETE_WIDTH - 4) + deleteIcon:SetPoint("CENTER") + deleteIcon:SetTexture("Interface\\Buttons\\UI-GroupLoot-Pass-Up") + deleteBtn.icon = deleteIcon + + -- Shimmer animation on hover + local shimmer = deleteBtn:CreateTexture(nil, "OVERLAY") + shimmer:SetSize(DELETE_WIDTH - 4, DELETE_WIDTH - 4) + shimmer:SetPoint("CENTER") + shimmer:SetTexture("Interface\\Buttons\\UI-GroupLoot-Pass-Up") + shimmer:SetBlendMode("ADD") + shimmer:SetAlpha(0) + deleteBtn.shimmer = shimmer + + local shimmerAnim = shimmer:CreateAnimationGroup() + shimmerAnim:SetLooping("REPEAT") + local fadeIn = shimmerAnim:CreateAnimation("Alpha") + fadeIn:SetFromAlpha(0) + fadeIn:SetToAlpha(0.5) + fadeIn:SetDuration(0.5) + fadeIn:SetOrder(1) + local fadeOut = shimmerAnim:CreateAnimation("Alpha") + fadeOut:SetFromAlpha(0.5) + fadeOut:SetToAlpha(0) + fadeOut:SetDuration(0.5) + fadeOut:SetOrder(2) + deleteBtn.shimmerAnim = shimmerAnim + + deleteBtn:SetScript("OnEnter", function(self) + self.shimmer:SetAlpha(0) + self.shimmerAnim:Play() + end) + deleteBtn:SetScript("OnLeave", function(self) + self.shimmerAnim:Stop() + self.shimmer:SetAlpha(0) + end) + + row.deleteBtn = deleteBtn + + return row +end + +-- ─── Refresh all skill rows ───────────────────────────────────────────────── + +local function RefreshSkillRows() + -- Hide all existing row frames + for _, row in ipairs(skillRowFrames) do + row:Hide() + end + + local yPos = 0 + + for i, skillData in ipairs(editableSkills) do + local row = skillRowFrames[i] + if not row then + row = CreateSkillRowFrame(i) + skillRowFrames[i] = row + end + + -- Clear previous anchor points before repositioning + row:ClearAllPoints() + row:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", 0, -yPos) + row:SetPoint("TOPRIGHT", scrollChild, "TOPRIGHT", 0, -yPos) + + -- Populate name + row.nameBox:SetText(skillData.name or "") + row.nameBox:SetScript("OnTextChanged", function(self) + local idx = nil + for j, s in ipairs(editableSkills) do + if skillRowFrames[j] == row then + idx = j + break + end + end + if idx then + editableSkills[idx].name = self:GetText() + end + end) + + -- Setup level dropdown + local currentRowIndex = i + row.levelDropdown.label:SetText(skillData.level or "Novice") + row.levelDropdown:SetupMenu(function(owner, rootDescription) + for _, levelName in ipairs(AltSystem.Data.SkillLevelOrder) do + rootDescription:CreateRadio( + levelName, + function() + return editableSkills[currentRowIndex] and editableSkills[currentRowIndex].level == levelName + end, + function() + if editableSkills[currentRowIndex] then + editableSkills[currentRowIndex].level = levelName + editableSkills[currentRowIndex].value = AltSystem.Data:GetDefaultValueForLevel(levelName) + RefreshSkillRows() + end + end + ) + end + end) + + -- Setup value dropdown based on current level + row.valueDropdown.label:SetText(tostring(skillData.value or 1)) + local range = AltSystem.Data.SkillValueRanges[skillData.level] + if range then + row.valueDropdown:SetupMenu(function(owner, rootDescription) + for v = range.min, range.max do + rootDescription:CreateRadio( + tostring(v), + function() + return editableSkills[currentRowIndex] and editableSkills[currentRowIndex].value == v + end, + function() + if editableSkills[currentRowIndex] then + editableSkills[currentRowIndex].value = v + row.valueDropdown.label:SetText(tostring(v)) + end + end + ) + end + end) + end + + -- Bind delete button for current index + row.deleteBtn:SetScript("OnClick", function() + table.remove(editableSkills, currentRowIndex) + RefreshSkillRows() + end) + + row:Show() + yPos = yPos + ROW_HEIGHT + ROW_SPACING + end + + -- Reposition Add A Row button + if addRowButton then + addRowButton:ClearAllPoints() + addRowButton:SetPoint("TOPRIGHT", scrollChild, "TOPRIGHT", -PADDING, -(yPos + 4)) + end + + -- Update scroll child height + local totalHeight = yPos + ROW_HEIGHT + 20 -- extra space for Add A Row button + if scrollChild then + scrollChild:SetHeight(math.max(totalHeight, 1)) + end +end + +-- ─── Create Build Skills Content ──────────────────────────────────────────── + +function AltSystem:CreateBuildSkillsContent(parentFrame) + local contentWidth = parentFrame:GetWidth() or 692 + local contentHeight = parentFrame:GetHeight() or 444 + + local yPos = -PADDING + + -- Info text paragraph 1 + local info1 = parentFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal") + info1:SetPoint("TOPLEFT", parentFrame, "TOPLEFT", PADDING, yPos) + info1:SetPoint("TOPRIGHT", parentFrame, "TOPRIGHT", -PADDING, yPos) + info1:SetJustifyH("LEFT") + info1:SetText("All skills below are directly extracted from your TRP's characteristics sheet. You can view and edit them there at any time.") + info1:SetTextColor(0.9, 0.75, 0.2) + info1:SetWordWrap(true) + + yPos = yPos - (info1:GetStringHeight() or 16) - 10 + + -- Info text paragraph 2 + local info2 = parentFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal") + info2:SetPoint("TOPLEFT", parentFrame, "TOPLEFT", PADDING, yPos) + info2:SetPoint("TOPRIGHT", parentFrame, "TOPRIGHT", -PADDING, yPos) + info2:SetJustifyH("LEFT") + info2:SetText("This menu serves as an easy alternative for if you want to use this system fast without diving deep into understanding it and/or styling your TRP sheet at this point in time. All your changes made will not be saved to TRP up until you hit the specific button to do so.") + info2:SetTextColor(0.9, 0.75, 0.2) + info2:SetWordWrap(true) + + yPos = yPos - (info2:GetStringHeight() or 32) - 16 + + -- "Skill list" section header + local sectionHeader = parentFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge") + sectionHeader:SetPoint("TOPLEFT", parentFrame, "TOPLEFT", PADDING, yPos) + sectionHeader:SetText("Skill list") + --sectionHeader:SetTextColor(1, 1, 1) + + yPos = yPos - 24 + + -- Column headers + local nameHeader = parentFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal") + nameHeader:SetPoint("TOPLEFT", parentFrame, "TOPLEFT", PADDING + 8, yPos) + nameHeader:SetText("Name") + nameHeader:SetTextColor(0.9, 0.75, 0.2) + + local levelHeader = parentFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal") + levelHeader:SetPoint("TOPLEFT", parentFrame, "TOPLEFT", PADDING + 8 + NAME_WIDTH + 12, yPos) + levelHeader:SetText("Level") + levelHeader:SetTextColor(0.9, 0.75, 0.2) + + local valueHeader = parentFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal") + valueHeader:SetPoint("TOPLEFT", parentFrame, "TOPLEFT", PADDING + 8 + NAME_WIDTH + 12 + LEVEL_WIDTH + 8, yPos) + valueHeader:SetText("Value") + valueHeader:SetTextColor(0.9, 0.75, 0.2) + + yPos = yPos - 20 + + -- Save button (pinned to bottom, outside scroll frame) + local saveButton = AltSystem.CreateFlatButton("AltSystemSaveSkillsButton", parentFrame, 180, 30, "Save Skills to TRP") + saveButton:SetPoint("BOTTOM", parentFrame, "BOTTOM", 0, PADDING) + + saveButton:SetScript("OnClick", function() + local success = AltSystem.Data:SaveSkills(editableSkills) + if success and AltSystem.RefreshSkillDropdown then + AltSystem:RefreshSkillDropdown() + end + end) + + -- Scrollable skill list area (between column headers and save button) + scrollFrame = CreateFrame("ScrollFrame", "AltSystemBuildSkillsScrollFrame", parentFrame, "UIPanelScrollFrameTemplate") + scrollFrame:SetPoint("TOPLEFT", parentFrame, "TOPLEFT", PADDING, yPos) + scrollFrame:SetPoint("BOTTOMRIGHT", parentFrame, "BOTTOMRIGHT", -PADDING - 22, saveButton:GetHeight() + PADDING + 8) + + scrollChild = CreateFrame("Frame", "AltSystemBuildSkillsScrollChild", scrollFrame) + scrollChild:SetWidth(scrollFrame:GetWidth() or (contentWidth - PADDING * 2 - 22)) + scrollChild:SetHeight(1) + scrollFrame:SetScrollChild(scrollChild) + + -- Update scroll child width when frame resizes + scrollFrame:SetScript("OnSizeChanged", function(self) + scrollChild:SetWidth(self:GetWidth()) + end) + + -- "Add A Row" button (inside scroll child, below last row) + addRowButton = CreateFrame("Button", "AltSystemAddRowButton", scrollChild, "UIPanelButtonTemplate") + addRowButton:SetSize(130, 26) + addRowButton:SetText("+ Add A Row") + addRowButton:SetPoint("TOPRIGHT", scrollChild, "TOPRIGHT", -PADDING, 0) + + -- Style add row button + local addBg = addRowButton:CreateTexture(nil, "BACKGROUND") + addBg:SetAllPoints() + addBg:SetColorTexture(0.4, 0.08, 0.08, 0.9) + + addRowButton:SetScript("OnClick", function() + table.insert(editableSkills, { + name = "Skillname", + level = "Novice", + value = 1, + icon = "inv_misc_questionmark", + isNew = true, + }) + RefreshSkillRows() + + -- Auto-scroll to bottom to show the new row + C_Timer.After(0.05, function() + if scrollFrame then + scrollFrame:SetVerticalScroll(scrollFrame:GetVerticalScrollRange()) + end + end) + end) + + AltSystem.BuildSkillsScrollFrame = scrollFrame + AltSystem.BuildSkillsScrollChild = scrollChild +end + +-- ─── Refresh Build Skills list (called on tab switch) ─────────────────────── + +function AltSystem:RefreshBuildSkillsList() + -- Reload skills from TRP3 into working copy, discarding any unsaved edits + editableSkills = AltSystem.Data:GetEditableSkills() + RefreshSkillRows() +end diff --git a/Core.lua b/Core.lua old mode 100644 new mode 100755 index a3d1ab9..865bf63 --- a/Core.lua +++ b/Core.lua @@ -6,11 +6,14 @@ AltSystem.State = { selectedSkillIndex = 1, selectedSkillName = nil, -- skill name used to restore selection across sessions selectedItemIndex = 1, -- 1 = No item - selectedDefenseIndex = 1, -- 1 = Base armor + selectedDefenseIndex = 1, -- 1 = No Armor shieldEnabled = false, petSummonEnabled = false, announceEnabled = false, announceChannelIndex = 1, -- 1 = Emote, 2 = Party, 3 = Raid, 4 = Guild + rollType = "attack", -- "attack" or "defense" + announceOptionIndex = 1, -- 1 = Self Roll, 2+ = channel from AnnounceChannels + rollLog = {}, -- array of { text = "...", timestamp = time() }, max 100, not persisted } -- Channel definitions for announcing rolls @@ -35,12 +38,6 @@ end) function AltSystem:Init() -- Load saved settings AltSystemDB = AltSystemDB or {} - if AltSystemDB.announceEnabled ~= nil then - AltSystem.State.announceEnabled = AltSystemDB.announceEnabled - end - if AltSystemDB.announceChannelIndex then - AltSystem.State.announceChannelIndex = AltSystemDB.announceChannelIndex - end if AltSystemDB.petSummonEnabled ~= nil then AltSystem.State.petSummonEnabled = AltSystemDB.petSummonEnabled end @@ -56,6 +53,28 @@ function AltSystem:Init() 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" @@ -68,13 +87,13 @@ function AltSystem:Init() saveFrame:RegisterEvent("PLAYER_LOGOUT") saveFrame:SetScript("OnEvent", function() AltSystemDB = AltSystemDB or {} - AltSystemDB.announceEnabled = AltSystem.State.announceEnabled - AltSystemDB.announceChannelIndex = AltSystem.State.announceChannelIndex + 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 diff --git a/Data.lua b/Data.lua old mode 100644 new mode 100755 index e46d887..d1a1ae2 --- 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,24 +155,112 @@ 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 table.insert(AltSystem.Data.Skills, { name = skill.name, level = skill.level, modifier = skill.modifier }) end --- Item options: name and modifier (first entry = no item) +-- Item options: name, label (used in roll messages), and modifier (first entry = no item) AltSystem.Data.Items = { - { name = "No item", modifier = 0 }, - { name = "Rare item", modifier = 3 }, - { name = "Epic item", modifier = 5 }, + { name = "No item", label = "No item", modifier = 0 }, + { name = "Rare", label = "Rare item", modifier = 3 }, + { name = "Epic", label = "Epic item", modifier = 5 }, } --- Defense options: name and modifier +-- Defense / Armor options: name, label (used in roll messages), and modifier AltSystem.Data.Defenses = { - { name = "Base armor", modifier = 0 }, - { name = "Extra small armor", modifier = 1 }, - { name = "Extra large armor", modifier = 2 }, + { name = "None", label = "No armor", modifier = 0 }, + { name = "Partial", label = "Extra armor", modifier = 1 }, + { name = "Full", label = "Extra armor", modifier = 2 }, } -- Shield modifier diff --git a/README.md b/README.md old mode 100644 new mode 100755 diff --git a/Roll.lua b/Roll.lua old mode 100644 new mode 100755 index a37ca8b..036a680 --- a/Roll.lua +++ b/Roll.lua @@ -27,10 +27,42 @@ local function BuildModifierString(modifiers) 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. --- Note: EMOTE channel uses SendChatMessage like all others. The WoW API --- SendChatMessage(msg, "EMOTE") sends a proper /e emote from any context, --- unlike RunMacroText which is a protected function requiring a hardware event. local function SendToChannel(msg, channel) SendChatMessage(msg, channel) end @@ -64,6 +96,24 @@ local function AnnounceRoll(rollValue, modifiers, total) 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 @@ -116,32 +166,12 @@ end) function AltSystem:CalculateAndDisplayResult(rollType, rollValue, petRollValue) -- Critical rolls bypass normal calculation if rollValue == 1 then - if AltSystem.ResultText then - AltSystem.ResultText:SetText("|cffff0000Critical Failure|r") - end - if AltSystem.State.announceEnabled then - local channelDef = AltSystem.AnnounceChannels[AltSystem.State.announceChannelIndex] - if channelDef then - local critMsg = channelDef.channel == "EMOTE" - and "rolled a Critical Failure!" - or (GetCharacterName() .. " rolled a Critical Failure!") - SendToChannel(critMsg, channelDef.channel) - end - end + AddLogEntry(BuildCriticalLogMessage(false)) + AnnounceCritical(false) return elseif rollValue == 20 then - if AltSystem.ResultText then - AltSystem.ResultText:SetText("|cff00ff00Critical Success|r") - end - if AltSystem.State.announceEnabled then - local channelDef = AltSystem.AnnounceChannels[AltSystem.State.announceChannelIndex] - if channelDef then - local critMsg = channelDef.channel == "EMOTE" - and "rolled a Critical Success!" - or (GetCharacterName() .. " rolled a Critical Success!") - SendToChannel(critMsg, channelDef.channel) - end - end + AddLogEntry(BuildCriticalLogMessage(true)) + AnnounceCritical(true) return end @@ -153,7 +183,6 @@ function AltSystem:CalculateAndDisplayResult(rollType, rollValue, petRollValue) local itemMod = item and item.modifier or 0 local total = rollValue - local breakdown = "Roll: " .. rollValue local modifiers = {} @@ -166,18 +195,14 @@ function AltSystem:CalculateAndDisplayResult(rollType, rollValue, petRollValue) total = rollValue + skillMod + itemMod + petMod if not isBaseRoll then - breakdown = breakdown .. "\nSkill: " .. FormatModifier(skillMod) table.insert(modifiers, { name = skill and skill.name or "Skill", value = skillMod }) end if itemMod ~= 0 then - breakdown = breakdown .. " | Item: " .. FormatModifier(itemMod) - table.insert(modifiers, { name = item and item.name or "Item", value = itemMod }) + table.insert(modifiers, { name = item and (item.label or item.name) or "Item", value = itemMod }) end if petMod ~= 0 then - breakdown = breakdown .. " | Pet: +" .. petMod table.insert(modifiers, { name = "Pet", value = petMod }) end - breakdown = breakdown .. "\n|cffffd100Attack Total: " .. total .. "|r" elseif rollType == "defense" then -- Defense Roll = roll + skill modifier + item modifier + defense modifier + shield modifier @@ -188,30 +213,26 @@ function AltSystem:CalculateAndDisplayResult(rollType, rollValue, petRollValue) total = rollValue + skillMod + itemMod + defenseMod + shieldMod + petMod if not isBaseRoll then - breakdown = breakdown .. "\nSkill: " .. FormatModifier(skillMod) table.insert(modifiers, { name = skill and skill.name or "Skill", value = skillMod }) end if itemMod ~= 0 then - breakdown = breakdown .. " | Item: " .. FormatModifier(itemMod) - table.insert(modifiers, { name = item and item.name or "Item", value = itemMod }) + table.insert(modifiers, { name = item and (item.label or item.name) or "Item", value = itemMod }) + end + if defenseMod ~= 0 then + table.insert(modifiers, { name = defense and (defense.label or defense.name) or "Armor", value = defenseMod }) end - breakdown = breakdown .. "\nDefense: " .. FormatModifier(defenseMod) - table.insert(modifiers, { name = defense and defense.name or "Defense", value = defenseMod }) if shieldMod ~= 0 then - breakdown = breakdown .. " | Shield: " .. FormatModifier(shieldMod) table.insert(modifiers, { name = "Shield", value = shieldMod }) end if petMod ~= 0 then - breakdown = breakdown .. " | Pet: +" .. petMod table.insert(modifiers, { name = "Pet", value = petMod }) end - breakdown = breakdown .. "\n|cff00ccffDefense Total: " .. total .. "|r" end - if AltSystem.ResultText then - AltSystem.ResultText:SetText(breakdown) - end + -- Add to log (always, regardless of announce setting) + AddLogEntry(BuildLogMessage(rollValue, modifiers, total)) + -- Announce to chat (if enabled) AnnounceRoll(rollValue, modifiers, total) end diff --git a/UI.lua b/UI.lua old mode 100644 new mode 100755 index 5162da2..534d061 --- a/UI.lua +++ b/UI.lua @@ -1,14 +1,21 @@ -- AltSystem UI --- Creates the main dialog window with all interface elements. +-- Creates the main dialog window with tabbed layout. +-- The "Use Skills" tab contains controls on the left and a Log panel on the right. -- Uses the modern DropdownButton API (WoW 10.2.5+ / 12.0+). AltSystem = AltSystem or {} -local WINDOW_WIDTH = 300 -local WINDOW_HEIGHT = 478 -local PADDING = 12 -local ROW_HEIGHT = 30 -local LABEL_WIDTH = 80 +local WINDOW_WIDTH = 720 +local WINDOW_HEIGHT = 520 +local CONTROLS_WIDTH = 360 +local LOG_WIDTH = 360 +local PADDING = 12 +local PADDING_HEADER = 6 +local ROW_HEIGHT = 26 +local LABEL_HEIGHT = 14 -- approximate height of GameFontNormal text +local LABEL_GAP = 4 -- gap between a sub-label and its input +local ITEM_GAP = 10 -- gap between inputs within the same section +local SECTION_GAP = 22 -- gap between major sections -- Helper: Build the skill option list from current AltSystem.Data.Skills local function BuildSkillOptions() @@ -26,39 +33,144 @@ local function BuildSkillOptions() return options end --- Helper: Create a modern dropdown (WowStyle1DropdownTemplate) -local function CreateDropdown(parent, name, yOffset, labelText, options, defaultIndex, onSelect) - local label = parent:CreateFontString(nil, "OVERLAY", "GameFontNormal") - label:SetPoint("TOPLEFT", parent, "TOPLEFT", PADDING, yOffset) - label:SetText(labelText) - label:SetWidth(LABEL_WIDTH) - label:SetJustifyH("LEFT") +-- Helper: Create a flat dark dropdown with label on top (reuses shared CreateFlatDropdown) +local function CreateDropdown(parent, name, labelText, options, defaultIndex, onSelect, labelFont) + local hasLabel = labelText and labelText ~= "" + local containerHeight = hasLabel and (LABEL_HEIGHT + LABEL_GAP + 28) or ROW_HEIGHT + + local container = CreateFrame("Frame", nil, parent) + container:SetHeight(containerHeight) + + local label + if hasLabel then + label = container:CreateFontString(nil, "OVERLAY", labelFont or "GameFontNormal") + label:SetPoint("TOPLEFT", container, "TOPLEFT", 0, 0) + label:SetText(labelText) + label:SetJustifyH("LEFT") + label:SetTextColor(0.9, 0.75, 0.2) + end local selectedIndex = defaultIndex or 1 - local dropdown = CreateFrame("DropdownButton", name, parent, "WowStyle1DropdownTemplate") - dropdown:SetPoint("LEFT", label, "RIGHT", 4, 0) - dropdown:SetWidth(160) + local dropdown = AltSystem.CreateFlatDropdown(name, container, 190) + if hasLabel then + dropdown:SetPoint("TOPLEFT", container, "TOPLEFT", 0, -(LABEL_HEIGHT + LABEL_GAP)) + dropdown:SetPoint("TOPRIGHT", container, "TOPRIGHT", 0, -(LABEL_HEIGHT + LABEL_GAP)) + else + dropdown:SetPoint("LEFT", container, "LEFT", 0, 0) + dropdown:SetPoint("RIGHT", container, "RIGHT", 0, 0) + end - dropdown:SetupMenu(function(dropdown, rootDescription) + -- Set initial label text + if options[selectedIndex] then + dropdown.label:SetText(options[selectedIndex].text) + end + + dropdown:SetupMenu(function(owner, rootDescription) for i, option in ipairs(options) do rootDescription:CreateRadio( - option.text, - function(data) return data == selectedIndex end, - function(data) - selectedIndex = data - if onSelect then onSelect(data, options[data]) end - end, - i + option.text, + function() + return i == selectedIndex + end, + function() + selectedIndex = i + dropdown.label:SetText(option.text) + if onSelect then + onSelect(i, option) + end + end ) end end) - return dropdown, function() return selectedIndex end, function(idx) selectedIndex = idx end + return container, dropdown, function() + return selectedIndex + end, function(idx) + selectedIndex = idx + if options[idx] then + dropdown.label:SetText(options[idx].text) + end + end +end + +-- Helper: Create a custom flat radio button (gold circle when selected, grey when not) +local function CreateRadioButton(parent, name, text, x, y, isChecked, onClick) + local size = 20 + local btn = CreateFrame("CheckButton", name, parent) + btn:SetSize(size, size) + btn:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y) + + -- Outer grey circle (always visible, acts as border) + local border = btn:CreateTexture(nil, "BACKGROUND") + border:SetAllPoints() + border:SetColorTexture(0.3, 0.3, 0.3, 1) + local borderMask = btn:CreateMaskTexture() + borderMask:SetAllPoints() + borderMask:SetTexture("Interface\\CharacterFrame\\TempPortraitAlphaMask", "CLAMPTOBLACKADDITIVE", "CLAMPTOBLACKADDITIVE") + border:AddMaskTexture(borderMask) + + -- Inner yellow circle (shown when selected, slightly smaller to reveal grey border) + local inner = btn:CreateTexture(nil, "BORDER") + local inset = 3 + inner:SetPoint("TOPLEFT", btn, "TOPLEFT", inset, -inset) + inner:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", -inset, inset) + inner:SetColorTexture(0.9, 0.75, 0.2, 1) + local innerMask = btn:CreateMaskTexture() + innerMask:SetPoint("TOPLEFT", btn, "TOPLEFT", inset, -inset) + innerMask:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", -inset, inset) + innerMask:SetTexture("Interface\\CharacterFrame\\TempPortraitAlphaMask", "CLAMPTOBLACKADDITIVE", "CLAMPTOBLACKADDITIVE") + inner:AddMaskTexture(innerMask) + inner:Hide() + + btn.checkTex = nil -- unused, kept for compatibility + + local function UpdateVisual() + if btn:GetChecked() then + inner:Show() + else + inner:Hide() + end + end + + btn:SetChecked(isChecked) + UpdateVisual() + + local label = btn:CreateFontString(nil, "OVERLAY", "GameFontHighlight") + label:SetPoint("LEFT", btn, "RIGHT", 6, 0) + label:SetText(text) + + btn:SetScript("OnClick", function(self) + onClick(self) + UpdateVisual() + end) + + btn.UpdateVisual = UpdateVisual + return btn +end + +-- Helper: Create a section header (golden text) +local function CreateSectionHeader(parent, text, x, y) + local header = parent:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge") + header:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y) + header:SetText(text) + header:SetTextColor(0.9, 0.75, 0.2) + return header +end + +-- Helper: Create a sub-label (smaller golden text) +local function CreateSubLabel(parent, text, x, y) + local label = parent:CreateFontString(nil, "OVERLAY", "GameFontNormal") + label:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y) + label:SetText(text) + label:SetTextColor(0.9, 0.75, 0.2) + return label end function AltSystem:CreateMainFrame() - if AltSystem.MainFrame then return end + if AltSystem.MainFrame then + return + end -- Main frame local f = CreateFrame("Frame", "AltSystemMainFrame", UIParent, "BasicFrameTemplateWithInset") @@ -76,26 +188,154 @@ function AltSystem:CreateMainFrame() f.title:SetPoint("TOPLEFT", f.TitleBg, "TOPLEFT", 5, -3) f.title:SetText("AltSystem") - -- Track current Y offset for layout - local yPos = -40 + --------------------- + -- TAB BUTTONS (span full window width) + --------------------- + local contentTop = -24 + local tabHeight = 36 + local contentWidth = WINDOW_WIDTH - 8 -- 4px inset on each side + local tabWidth = contentWidth / 2 + + local tabUseSkills = CreateFrame("Button", "AltSystemTabUseSkills", f) + tabUseSkills:SetSize(tabWidth, tabHeight) + tabUseSkills:SetPoint("TOPLEFT", f, "TOPLEFT", 4, contentTop) + tabUseSkills:SetNormalFontObject("GameFontNormalLarge") + tabUseSkills:SetHighlightFontObject("GameFontNormalLarge") + tabUseSkills:SetText("Use Skills") + + local tabUseSkillsBg = tabUseSkills:CreateTexture(nil, "BACKGROUND") + tabUseSkillsBg:SetAllPoints() + tabUseSkillsBg:SetColorTexture(0, 0, 0, 0) + + local tabUseSkillsText = tabUseSkills:GetFontString() + tabUseSkillsText:SetTextColor(0.9, 0.75, 0.2, 1) + + local tabBuildSkills = CreateFrame("Button", "AltSystemTabBuildSkills", f) + tabBuildSkills:SetSize(tabWidth, tabHeight) + tabBuildSkills:SetPoint("TOPLEFT", tabUseSkills, "TOPRIGHT", 0, 0) + tabBuildSkills:SetNormalFontObject("GameFontNormalLarge") + tabBuildSkills:SetHighlightFontObject("GameFontNormalLarge") + tabBuildSkills:SetText("Build Skills") + + local tabBuildSkillsBg = tabBuildSkills:CreateTexture(nil, "BACKGROUND") + tabBuildSkillsBg:SetAllPoints() + tabBuildSkillsBg:SetColorTexture(0.3, 0.3, 0.3, 1) + + local tabBuildSkillsText = tabBuildSkills:GetFontString() + tabBuildSkillsText:SetTextColor(1, 1, 1, 1) + + --------------------- + -- TAB CONTENT FRAMES + --------------------- + local tabContentTop = contentTop - tabHeight + local tabContentHeight = WINDOW_HEIGHT - 28 - tabHeight + + local useSkillsContent = CreateFrame("Frame", "AltSystemUseSkillsContent", f) + useSkillsContent:SetPoint("TOPLEFT", f, "TOPLEFT", 4, tabContentTop) + useSkillsContent:SetSize(contentWidth, tabContentHeight) + + local buildSkillsContent = CreateFrame("Frame", "AltSystemBuildSkillsContent", f) + buildSkillsContent:SetPoint("TOPLEFT", f, "TOPLEFT", 4, tabContentTop) + buildSkillsContent:SetSize(contentWidth, tabContentHeight) + buildSkillsContent:Hide() + + -- Build Skills tab content (created in BuildSkillsUI.lua) + AltSystem:CreateBuildSkillsContent(buildSkillsContent) + + -- Tab switching logic + local function SelectTab(tabIndex) + if tabIndex == 1 then + useSkillsContent:Show() + buildSkillsContent:Hide() + tabUseSkillsBg:SetColorTexture(0, 0, 0, 0) + tabBuildSkillsBg:SetColorTexture(0.3, 0.3, 0.3, 1) + tabUseSkillsText:SetTextColor(0.9, 0.75, 0.2, 1) + tabBuildSkillsText:SetTextColor(1, 1, 1, 1) + else + useSkillsContent:Hide() + buildSkillsContent:Show() + tabUseSkillsBg:SetColorTexture(0.3, 0.3, 0.3, 1) + tabBuildSkillsBg:SetColorTexture(0, 0, 0, 0) + tabUseSkillsText:SetTextColor(1, 1, 1, 1) + tabBuildSkillsText:SetTextColor(0.9, 0.75, 0.2, 1) + AltSystem:RefreshBuildSkillsList() + end + end + + tabUseSkills:SetScript("OnClick", function() + SelectTab(1) + end) + tabBuildSkills:SetScript("OnClick", function() + SelectTab(2) + end) + + --------------------- + -- USE SKILLS TAB CONTENT (left: controls, right: log) + --------------------- + local controlsPanel = CreateFrame("Frame", nil, useSkillsContent) + controlsPanel:SetPoint("TOPLEFT", useSkillsContent, "TOPLEFT", 0, 0) + controlsPanel:SetSize(CONTROLS_WIDTH, tabContentHeight) + + local content = controlsPanel + local yPos = -PADDING + + -- Section: Define Your Base Roll + CreateSectionHeader(content, "Base Roll", PADDING_HEADER, yPos) + yPos = yPos - 26 + + -- Roll Type label + CreateSubLabel(content, "Roll Type", PADDING, yPos) + yPos = yPos - (LABEL_HEIGHT + LABEL_GAP) + + -- Roll Type radio buttons + local attackRadio, defenseRadio + + local function UpdateRollTypeSelection(rollType) + AltSystem.State.rollType = rollType + attackRadio:SetChecked(rollType == "attack") + defenseRadio:SetChecked(rollType == "defense") + attackRadio.UpdateVisual() + defenseRadio.UpdateVisual() + -- Update roll button text + if AltSystem.RollButton then + local label = rollType == "attack" and "Roll Attack" or "Roll Defense" + AltSystem.RollButton:SetText(label) + end + end + + attackRadio = CreateRadioButton(content, "AltSystemAttackRadio", "Attack Roll", PADDING, yPos, + AltSystem.State.rollType == "attack", + function() + UpdateRollTypeSelection("attack") + end) + + defenseRadio = CreateRadioButton(content, "AltSystemDefenseRadio", "Defense Roll", CONTROLS_WIDTH / 2, yPos, + AltSystem.State.rollType == "defense", + function() + UpdateRollTypeSelection("defense") + end) + + yPos = yPos - ROW_HEIGHT - ITEM_GAP - ------------------------- -- Skill dropdown - ------------------------- local skillOptions = BuildSkillOptions() local UpdateSkillWarning -- forward declaration - local skillDropdown, getSkillIndex, setSkillIndex = CreateDropdown( - f, "AltSystemSkillDropdown", yPos, "Skill:", skillOptions, - AltSystem.State.selectedSkillIndex, - function(index) - AltSystem.State.selectedSkillIndex = index - AltSystem.State.selectedSkillName = AltSystem.Data.Skills[index] and AltSystem.Data.Skills[index].name or nil - UpdateSkillWarning(index) - end) + local skillContainer, skillDropdown, getSkillIndex, setSkillIndex = CreateDropdown( + content, "AltSystemSkillDropdown", "Skill", skillOptions, + AltSystem.State.selectedSkillIndex, + function(index) + AltSystem.State.selectedSkillIndex = index + AltSystem.State.selectedSkillName = AltSystem.Data.Skills[index] and AltSystem.Data.Skills[index].name or nil + UpdateSkillWarning(index) + end, + "GameFontNormal" + ) + skillContainer:SetPoint("TOPLEFT", content, "TOPLEFT", PADDING, yPos) + skillContainer:SetWidth(CONTROLS_WIDTH - PADDING * 2) - -- Warning icon for skill mismatch (shown next to dropdown when value doesn't match keyword) - local skillWarning = CreateFrame("Frame", nil, f) + -- Warning icon for skill mismatch + local skillWarning = CreateFrame("Frame", nil, skillContainer) skillWarning:SetSize(20, 20) skillWarning:SetPoint("LEFT", skillDropdown, "RIGHT", 4, 0) @@ -115,7 +355,6 @@ function AltSystem:CreateMainFrame() end) skillWarning:Hide() - -- Update the warning icon visibility based on the currently selected skill UpdateSkillWarning = function(index) local skill = AltSystem.Data.Skills[index] if skill and skill.warning then @@ -131,186 +370,276 @@ function AltSystem:CreateMainFrame() AltSystem.GetSkillIndex = getSkillIndex AltSystem.SetSkillIndex = setSkillIndex AltSystem.UpdateSkillWarning = UpdateSkillWarning - yPos = yPos - ROW_HEIGHT - 8 - ------------------------- - -- 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 + yPos = yPos - (LABEL_HEIGHT + LABEL_GAP + 28) - ITEM_GAP - CreateDropdown(f, "AltSystemItemDropdown", yPos, "Item:", itemOptions, AltSystem.State.selectedItemIndex, - function(index) - AltSystem.State.selectedItemIndex = index - end) - yPos = yPos - ROW_HEIGHT - 8 + -- Armor label + CreateSubLabel(content, "Extra Armor", PADDING, yPos) + yPos = yPos - (LABEL_HEIGHT + LABEL_GAP) - ------------------------- - -- 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 + -- Armor radio buttons + local armorRadios = {} - CreateDropdown(f, "AltSystemDefenseDropdown", yPos, "Defense:", defenseOptions, AltSystem.State.selectedDefenseIndex, - function(index) - AltSystem.State.selectedDefenseIndex = index - end) - yPos = yPos - ROW_HEIGHT - 8 - - ------------------------- - -- 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") - - local shieldCheck = CreateFrame("CheckButton", "AltSystemShieldCheck", f, "UICheckButtonTemplate") - shieldCheck:SetPoint("LEFT", shieldLabel, "RIGHT", -6, 0) - shieldCheck:SetChecked(AltSystem.State.shieldEnabled) - - 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() - end) - - yPos = yPos - ROW_HEIGHT - 8 - - ------------------------- - -- Pet/Summon checkbox - ------------------------- - local petLabel = f:CreateFontString(nil, "OVERLAY", "GameFontNormal") - petLabel:SetPoint("TOPLEFT", f, "TOPLEFT", PADDING, yPos) - petLabel:SetText("Pet:") - petLabel:SetWidth(LABEL_WIDTH) - petLabel:SetJustifyH("LEFT") - - local petCheck = CreateFrame("CheckButton", "AltSystemPetSummonCheck", f, "UICheckButtonTemplate") - petCheck:SetPoint("LEFT", petLabel, "RIGHT", -6, 0) - petCheck:SetChecked(AltSystem.State.petSummonEnabled) - - local petText = petCheck:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall") - petText:SetPoint("LEFT", petCheck, "RIGHT", 2, 0) - petText:SetText("+d5 modifier") - - petCheck:SetScript("OnClick", function(self) - AltSystem.State.petSummonEnabled = self:GetChecked() - end) - - yPos = yPos - ROW_HEIGHT - 8 - - ------------------------- - -- Announce checkbox + channel dropdown - ------------------------- - local announceLabel = f:CreateFontString(nil, "OVERLAY", "GameFontNormal") - announceLabel:SetPoint("TOPLEFT", f, "TOPLEFT", PADDING, yPos) - announceLabel:SetText("Announce:") - announceLabel:SetWidth(LABEL_WIDTH) - announceLabel:SetJustifyH("LEFT") - - local announceCheck = CreateFrame("CheckButton", "AltSystemAnnounceCheck", f, "UICheckButtonTemplate") - announceCheck:SetPoint("LEFT", announceLabel, "RIGHT", -6, 0) - announceCheck:SetChecked(AltSystem.State.announceEnabled) - - -- Channel dropdown (shown only when announce is enabled) - local channelOptions = {} - for _, ch in ipairs(AltSystem.AnnounceChannels) do - table.insert(channelOptions, { text = ch.name }) - end - - local channelDropdown, getChannelIndex, setChannelIndex = CreateDropdown( - f, "AltSystemChannelDropdown", yPos, "", channelOptions, - AltSystem.State.announceChannelIndex, - function(index) - AltSystem.State.announceChannelIndex = index - end) - channelDropdown:SetPoint("LEFT", announceCheck, "RIGHT", 2, 0) - channelDropdown:SetWidth(130) - - -- Show/hide channel dropdown based on checkbox state - local function UpdateChannelDropdownVisibility() - if AltSystem.State.announceEnabled then - channelDropdown:Show() - else - channelDropdown:Hide() + local function UpdateArmorSelection(index) + AltSystem.State.selectedDefenseIndex = index + for i, radio in ipairs(armorRadios) do + radio:SetChecked(i == index) + radio.UpdateVisual() end end - announceCheck:SetScript("OnClick", function(self) - AltSystem.State.announceEnabled = self:GetChecked() - UpdateChannelDropdownVisibility() + local armorX = PADDING + for i, def in ipairs(AltSystem.Data.Defenses) do + local text = def.name + if def.modifier > 0 then + text = text .. " (+" .. def.modifier .. ")" + end + local radio = CreateRadioButton(content, "AltSystemArmorRadio" .. i, text, armorX, yPos, + AltSystem.State.selectedDefenseIndex == i, + function() + UpdateArmorSelection(i) + end) + table.insert(armorRadios, radio) + armorX = armorX + 110 + end + + yPos = yPos - ROW_HEIGHT - SECTION_GAP + + -- Section: Modifiers (optional) + CreateSectionHeader(content, "Modifiers (optional)", PADDING_HEADER, yPos) + yPos = yPos - 26 + --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) - UpdateChannelDropdownVisibility() + -- 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 - - ------------------------- - -- Roll result area - ------------------------- - local resultBg = CreateFrame("Frame", nil, f, "InsetFrameTemplate") - resultBg:SetPoint("TOPLEFT", f, "TOPLEFT", PADDING, yPos) - resultBg:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -PADDING, PADDING) - - local resultText = resultBg:CreateFontString(nil, "OVERLAY", "GameFontHighlightLarge") - resultText:SetPoint("CENTER") - resultText:SetText("Roll result will appear here") - resultText:SetJustifyH("CENTER") - resultText:SetWidth(resultBg:GetWidth() - 10) - - AltSystem.ResultText = resultText - - -- Button click handlers - attackBtn:SetScript("OnClick", function() - AltSystem:PerformRoll("attack") + petCheck:SetScript("OnClick", function(self) + AltSystem.State.petSummonEnabled = self:GetChecked() + UpdatePetVisual() end) - defenseBtn:SetScript("OnClick", function() - AltSystem:PerformRoll("defense") + yPos = yPos - ROW_HEIGHT - ITEM_GAP + + -- Item label + CreateSubLabel(content, "Item", PADDING, yPos) + yPos = yPos - (LABEL_HEIGHT + LABEL_GAP) + + -- Item radio buttons + local itemRadios = {} + + local function UpdateItemSelection(index) + AltSystem.State.selectedItemIndex = index + for i, radio in ipairs(itemRadios) do + radio:SetChecked(i == index) + radio.UpdateVisual() + end + end + + local itemX = PADDING + for i, item in ipairs(AltSystem.Data.Items) do + local text = item.name + if item.modifier > 0 then + text = text .. " (+" .. item.modifier .. ")" + end + local radio = CreateRadioButton(content, "AltSystemItemRadio" .. i, text, itemX, yPos, + AltSystem.State.selectedItemIndex == i, + function() + UpdateItemSelection(i) + end) + table.insert(itemRadios, radio) + itemX = itemX + 110 + end + + yPos = yPos - ROW_HEIGHT - SECTION_GAP + + -- Section: Roll Dice + CreateSectionHeader(content, "Roll Mode", PADDING_HEADER, yPos) + yPos = yPos - 26 + + -- Announce Roll dropdown (Self Roll + channels) + local announceOptions = { { text = "Self Roll" } } + for _, ch in ipairs(AltSystem.AnnounceChannels) do + table.insert(announceOptions, { text = ch.name }) + end + + local announceContainer, announceDropdown = CreateDropdown( + content, "AltSystemAnnounceDropdown", "", announceOptions, + AltSystem.State.announceOptionIndex, + function(index) + AltSystem.State.announceOptionIndex = index + if index > 1 then + AltSystem.State.announceEnabled = true + AltSystem.State.announceChannelIndex = index - 1 + else + AltSystem.State.announceEnabled = false + AltSystem.State.announceChannelIndex = 1 + end + end, + "GameFontNormal") + announceContainer:SetPoint("TOPLEFT", content, "TOPLEFT", PADDING, yPos) + announceContainer:SetWidth(CONTROLS_WIDTH - PADDING * 2) + + yPos = yPos - ROW_HEIGHT - ITEM_GAP + + -- Roll button + local rollLabel = AltSystem.State.rollType == "attack" and "Roll Attack" or "Roll Defense" + local rollBtn = AltSystem.CreateFlatButton("AltSystemRollBtn", content, CONTROLS_WIDTH - PADDING * 2, 32, rollLabel) + rollBtn:SetPoint("TOPLEFT", content, "TOPLEFT", PADDING, yPos) + + rollBtn:SetScript("OnClick", function() + AltSystem:PerformRoll(AltSystem.State.rollType) end) + AltSystem.RollButton = rollBtn + + --------------------- + -- LOG PANEL (right side of Use Skills tab) + --------------------- + local logPanel = CreateFrame("Frame", nil, useSkillsContent) + logPanel:SetPoint("TOPLEFT", useSkillsContent, "TOPLEFT", CONTROLS_WIDTH, 0) + logPanel:SetSize(LOG_WIDTH, tabContentHeight) + + -- Log header + local logHeader = CreateSectionHeader(logPanel, "Log", 0, -PADDING) + + -- Log scroll area background + local logBg = CreateFrame("Frame", nil, logPanel, "InsetFrameTemplate") + logBg:SetPoint("TOPLEFT", logPanel, "TOPLEFT", 4, -38) + logBg:SetPoint("BOTTOMRIGHT", logPanel, "BOTTOMRIGHT", -PADDING, 4) + + -- Scroll frame for log entries + local scrollFrame = CreateFrame("ScrollFrame", "AltSystemLogScrollFrame", logBg, "UIPanelScrollFrameTemplate") + scrollFrame:SetPoint("TOPLEFT", logBg, "TOPLEFT", 6, -6) + scrollFrame:SetPoint("BOTTOMRIGHT", logBg, "BOTTOMRIGHT", -28, 6) + + local scrollChild = CreateFrame("Frame", "AltSystemLogScrollChild", scrollFrame) + scrollChild:SetWidth(scrollFrame:GetWidth() or (LOG_WIDTH - 50)) + scrollChild:SetHeight(1) -- Will be updated dynamically + scrollFrame:SetScrollChild(scrollChild) + + AltSystem.LogScrollFrame = scrollFrame + AltSystem.LogScrollChild = scrollChild + -- Refresh skills from TRP3 profile each time the window is shown f:SetScript("OnShow", function() AltSystem:RefreshSkillDropdown() + AltSystem:RefreshLogPanel() end) f:Hide() AltSystem.MainFrame = f end +-- Refresh the log panel UI from AltSystem.State.rollLog +function AltSystem:RefreshLogPanel() + local scrollChild = AltSystem.LogScrollChild + if not scrollChild then + return + end + + -- Remove existing log entry fontstrings + if scrollChild.entries then + for _, entry in ipairs(scrollChild.entries) do + entry:Hide() + entry:SetText("") + end + end + scrollChild.entries = scrollChild.entries or {} + + local rollLog = AltSystem.State.rollLog + local entryHeight = 16 + local spacing = 4 + local yPos = 0 + local childWidth = AltSystem.LogScrollFrame:GetWidth() or 250 + + -- Entries are newest-first + for i = #rollLog, 1, -1 do + local idx = #rollLog - i + 1 + local logEntry = rollLog[i] + local fontStr = scrollChild.entries[idx] + + if not fontStr then + fontStr = scrollChild:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall") + scrollChild.entries[idx] = fontStr + end + + fontStr:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", 2, -yPos) + fontStr:SetWidth(childWidth - 4) + fontStr:SetJustifyH("LEFT") + fontStr:SetText(logEntry.text) + fontStr:Show() + + yPos = yPos + entryHeight + spacing + end + + scrollChild:SetHeight(math.max(yPos, 1)) +end + -- Refresh the skill dropdown with current TRP3 profile data function AltSystem:RefreshSkillDropdown() AltSystem.Data:RefreshSkills() @@ -338,22 +667,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 end, - function(data) - AltSystem.State.selectedSkillIndex = data - AltSystem.State.selectedSkillName = AltSystem.Data.Skills[data] and AltSystem.Data.Skills[data].name or nil - if AltSystem.SetSkillIndex then - AltSystem.SetSkillIndex(data) + option.text, + function() + return i == AltSystem.State.selectedSkillIndex + end, + function() + AltSystem.State.selectedSkillIndex = i + AltSystem.State.selectedSkillName = AltSystem.Data.Skills[i] and AltSystem.Data.Skills[i].name or nil + AltSystem.SkillDropdown.label:SetText(option.text) + if AltSystem.SetSkillIndex then + AltSystem.SetSkillIndex(i) + end + if AltSystem.UpdateSkillWarning then + AltSystem.UpdateSkillWarning(i) + end end - if AltSystem.UpdateSkillWarning then - AltSystem.UpdateSkillWarning(data) - end - end, - i ) 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 new file mode 100755 index 0000000..fcd504c --- /dev/null +++ b/docs/4-redesign.md @@ -0,0 +1,78 @@ +# Feature: Major Redesign +This redesign of the Addon's window will start by following the following [design](./roll_tab_design.png) + +- The window will be a tabbed window +- The 'Use Skills' tab will correspond to the current roll screen +- The 'Build Skills' tab will be a new screen, to be implemented later. For now, leave it empty. +- The 'Log' should record all rolls made by the user, and be displayed in a scrollable list. + - They should be displayed in the same style as the announced rolls, even when the announce option is off +- The log should store a maximum of 100 rolls + +## Implementation plan + +### 1. Restructure the main window layout (UI.lua) +- Increase `WINDOW_WIDTH` to roughly double (≈660) to accommodate the two-column layout (left panel for tabs, right panel for the log) +- Replace `BasicFrameTemplateWithInset` or layer a new structure inside it: + - **Left column (~50% width):** contains two tabs ("Use Skills", "Build Skills") and their content panels + - **Right column (~50% width):** contains the "Log" header and a scrollable log list +- Keep the title bar ("AltSystem") and close button at the top spanning the full width + +### 2. Implement the tab system (UI.lua) +- Create two tab buttons ("Use Skills" and "Build Skills") anchored at the top of the left column +- Use `PanelTemplates_SetNumTabs` / `PanelTemplates_SetTab` or manual highlight toggling to switch active tab styling +- **"Use Skills" tab content:** migrate all existing UI elements (Roll Type radios → Skill dropdown → Armor radios → Modifiers checkboxes → Announce dropdown → Roll button) into this tab's content frame + - Adapt the current layout from `CreateMainFrame` — re-parent all widgets to the tab content frame instead of `f` directly +- **"Build Skills" tab content:** create an empty placeholder frame (can show a "Coming soon" label) +- Toggling tabs shows/hides the corresponding content frame + +### 3. Redesign the "Use Skills" tab to match the mockup (UI.lua) +- Replace the current separate Attack/Defense buttons with a **Roll Type** radio-button group ("Attack Roll" / "Defense Roll") that sets `AltSystem.State.rollType` +- Keep the **Skill** dropdown as-is (already matches the mockup) +- Replace the current Defense dropdown with an **Armor** radio-button group ("No Armor" / "Basic Armor (+1)" / "Heavy Armor (+2)") + - Map these to the existing `AltSystem.Data.Defenses` entries; show armor options only when Defense Roll is selected, or always visible per mockup +- Group **Shield** and **Pet** checkboxes under a "Modifiers (optional)" section header with a "Label" sub-header matching the mockup +- Replace the Announce checkbox + channel dropdown with a single **"Announce Roll"** dropdown whose options are "Self Roll" (no announce) plus the existing channel list (Emote, Party, Raid, Guild) + - "Self Roll" maps to `announceEnabled = false`; any other selection maps to `announceEnabled = true` with the corresponding channel index +- Add a single **"Roll $rollType"** button at the bottom (text dynamically reflects "Roll Attack" or "Roll Defense") +- Remove the old roll-result text area from this tab (results now go to the Log panel) + +### 4. Build the Log panel (UI.lua) +- Create a right-side panel with a "Log" header label +- Inside, create a `ScrollFrame` (using `UIPanelScrollFrameTemplate` or a manual scroll child) to hold log entries +- Each log entry is a small frame/fontstring displaying the roll result in the same format as the announced message: + - `"[Name] rolled [d20 result] [modifiers] = [total]"` (reuse `BuildModifierString` from Roll.lua) + - Critical rolls show "rolled a Critical Failure!" or "rolled a Critical Success!" +- Entries are listed newest-first (most recent at top) in a vertically stacked layout + +### 5. Implement the roll log data store (Core.lua / Roll.lua) +- Add `AltSystem.State.rollLog = {}` — an array of log entry tables, each containing: `{ text = "...", timestamp = time() }` +- In `Roll.lua`, after every roll result is calculated (in `CalculateAndDisplayResult`), build the log message string (same format as announce) and insert it into `AltSystem.State.rollLog` +- Cap the log at **100 entries**: if `#rollLog > 100`, remove the oldest entry (`table.remove(rollLog, 1)`) +- After inserting, call a UI refresh function to update the scroll frame content +- The log is **always populated**, regardless of the announce setting (per the requirement: "displayed in the same style as the announced rolls, even when the announce option is off") +- Log does **not** need to persist across sessions (not mentioned in requirements); keep it in memory only + +### 6. Wire up the new Roll button (Roll.lua) +- The single "Roll $rollType" button calls `AltSystem:PerformRoll(state.rollType)` where `state.rollType` is set by the radio-button group ("attack" or "defense") +- Existing `PerformRoll` and `CalculateAndDisplayResult` logic remains largely unchanged; only the final display step changes from setting `ResultText` to appending to the log + refreshing the log UI + +### 7. Update state persistence (Core.lua) +- Save/restore `rollType` selection (attack/defense) in `AltSystemDB` +- Update announce state handling to work with the new single-dropdown approach (save selected option index) +- Armor selection (radio group) replaces `selectedDefenseIndex` — reuse same key or migrate + +### 8. Update the .toc file if needed (AltSystem.toc) +- No new Lua files are expected (all changes fit in existing files), but verify the load order is still correct + +### 9. Testing checklist +- [ ] Window opens at new size, tabs switch correctly +- [ ] "Use Skills" tab shows all controls matching the mockup layout +- [ ] "Build Skills" tab is empty / shows placeholder +- [ ] Attack and Defense rolls work correctly via the new single Roll button +- [ ] Log panel populates with each roll, formatted like announce messages +- [ ] Log scrolls when entries exceed visible area +- [ ] Log caps at 100 entries, oldest removed first +- [ ] Log populates even when announce is set to "Self Roll" (off) +- [ ] Announce still works when a channel is selected +- [ ] State (roll type, armor, announce option) persists across sessions +- [ ] Window is draggable and clamps to screen \ No newline at end of file diff --git a/docs/5-build_skills.md b/docs/5-build_skills.md new file mode 100755 index 0000000..9b76326 --- /dev/null +++ b/docs/5-build_skills.md @@ -0,0 +1,132 @@ +# Feature: Build Skills tab +The second tab of the addon will follow these [designs](./build_skills_tab_design.png). + +## Acceptance Criteria +- This screen should show the same skills we use in the main screen, which come from the TRP profile +- The skills should be sorted by level +- The skill list should be scrollable, with the "Save" button pinned/sticky to the bottom +## Editing skills +- The user should be able to edit the name, level, and numerical score of each skill +- Edits should not be saved until the user explicitly clicks the "Save" button +### Skill Level and Value +- When a skill level is selected, the numerical score dropdown should update to only allow values within the skill level + - Inept: 0 + - Novice: 1-5 + - Adept: 6-10 + - Expert: 11-19 + - Master: 20 +### Deleting skills +- Clicking the "Delete" button should remove the skill from the list +### Adding Skills +- Clicking the "Add a Row" button should add a new skill row to the list +- Default values for the new skill should be: + - Name: Skillname + - Level: Novice + - Value: 1 +### Saving Skills +- When clicking the "Save" button: + - Newly added skills should be added to the TRP profile + - Existing skills should be updated in the TRP profile + - Skills that were deleted should also be removed from the TRP profile +- Icons should not be changed +## References +- Refer to [Data.lua](../Data.lua) for details on how we currently fetch the skills from TRP +- Refer to the TRP3 source code in case it's necessary [here](https://github.com/Total-RP/Total-RP-3) + +--- + +## Implementation Plan + +### 1. Add a `SaveSkills` function to Data.lua +- Create `AltSystem.Data:SaveSkills(editedSkills)` that writes skills back to the TRP3 profile +- Access the TRP3 profile via `TRP3_API.profile.getData("player/characteristics")` to get the `characteristics.PS` (personality traits) array +- For each edited skill, update or insert entries in `PS`: + - `LT` = skill name + - `RT` = level keyword (e.g. "Novice", "Adept", etc.) + - `V2` = numeric value (0–20) + - `IC` = preserve existing icon (do not change); for new skills, use a sensible default icon (e.g. `"inv_misc_questionmark"`) +- Remove any PS entries that were deleted by the user +- After writing, call `TRP3_API.dashboard.showCharacteristics()` or fire the appropriate TRP3 event if needed to refresh TRP3's own UI +- **Edge case:** If the TRP3 API is unavailable, show a warning message and abort save + +### 2. Extract skill-level constants into shared lookup tables in Data.lua +- The acceptance criteria defines value ranges per level (Inept: 0, Novice: 1–5, Adept: 6–10, Expert: 11–19, Master: 20) +- `SKILL_KEYWORD_RANGES` already exists but excludes Inept/Master min-max correctly for the Build tab's needs; extend or create a new table `AltSystem.Data.SkillValueRanges` that is accessible from UI.lua: + ``` + { Inept = {min=0, max=0}, Novice = {min=1, max=5}, Adept = {min=6, max=10}, Expert = {min=11, max=19}, Master = {min=20, max=20} } + ``` +- Create `AltSystem.Data.SkillLevelOrder` — an ordered array `{"Inept", "Novice", "Adept", "Expert", "Master"}` for populating the level dropdown in display order +- Create a helper `AltSystem.Data:GetDefaultValueForLevel(level)` that returns the minimum value for that level (used when the user changes level to auto-set the value) + +### 3. Add a function to read raw skills (with numeric values) from TRP3 in Data.lua +- Currently `RefreshSkills()` converts TRP3 traits into `{name, level, modifier}` — the Build tab needs the raw **numeric value** and the **icon** as well +- Create `AltSystem.Data:GetEditableSkills()` that returns an array of `{name, level, value, icon}` for each valid skill trait in the TRP3 profile (excluding Base Roll and Unskilled, which are system-generated entries) +- Sort the returned skills by level using `AltSystem.Data.SkillLevelOrder` ordering (Inept first, Master last) — matching the acceptance criteria "sorted by level" +- Reuse existing helpers `FindSkillKeyword` and `ParseSkillLevel` (promote them from local to module-level if needed, or call internally) + +### 4. Build the Build Skills tab UI (new file: BuildSkillsUI.lua) +- Create a new file to keep UI.lua manageable; register it in `AltSystem.toc` between `UI.lua` and `Roll.lua` +- Create `AltSystem:CreateBuildSkillsContent(parentFrame)` called from `CreateMainFrame` in UI.lua (replacing the placeholder) +- **Layout structure:** + - **Info text** at top — two golden/yellow paragraphs explaining that skills come from TRP (matches mockup) + - **"Skill list" section header** + - **Column headers**: Name, Level, Value (bold golden text) + - **Scrollable skill list** — a `ScrollFrame` containing dynamically created skill rows + - **"Add A Row" button** — anchored below the last skill row, inside the scroll child + - **"Save Skills to TRP" button** — pinned/sticky at the bottom of the tab, outside the scroll frame + +### 5. Implement editable skill rows +- Each skill row is a frame containing: + - **Name**: `EditBox` (text input) — pre-filled with current skill name + - **Level**: `DropdownButton` (WowStyle1DropdownTemplate) — options: Inept, Novice, Adept, Expert, Master + - **Value**: `DropdownButton` — options dynamically generated based on selected level (e.g. Novice → 1,2,3,4,5) + - **Delete button**: A button with a trash-can icon/red texture that removes the row +- Store all row data in a local working copy array (`editableSkills`), not directly in `AltSystem.Data.Skills` +- When the **level dropdown** changes: + - Update the value dropdown options to only show valid values for the new level + - Auto-set the value to the minimum for that level (e.g. switching to Adept → value becomes 6) +- **Row management:** + - `CreateSkillRow(parent, index, skillData)` — creates or recycles a row frame + - `RefreshSkillRows()` — rebuilds/repositions all rows and updates scroll child height + - Deleting a row removes it from `editableSkills` and calls `RefreshSkillRows()` + +### 6. Implement "Add A Row" functionality +- Clicking "Add A Row" inserts a new entry into `editableSkills`: + - `{ name = "Skillname", level = "Novice", value = 1, icon = "inv_misc_questionmark", isNew = true }` +- Calls `RefreshSkillRows()` to render the new row +- The scroll frame should auto-scroll to show the new row + +### 7. Implement "Save Skills to TRP" functionality +- On click, call `AltSystem.Data:SaveSkills(editableSkills)` which: + 1. Reads current `characteristics.PS` from TRP3 + 2. Rebuilds the PS array: keeps non-skill traits untouched, updates/adds/removes skill traits based on `editableSkills` + 3. Writes the updated PS back to the TRP3 profile data + 4. Calls `RefreshSkills()` so the Use Skills tab dropdown reflects the changes immediately +- Show a confirmation message (print to chat or a brief on-screen text) on successful save +- **Edge cases:** + - Empty skill list: allowed — just remove all skill traits from PS + - Duplicate skill names: allowed (TRP3 doesn't enforce uniqueness) + - Unsaved changes + tab switch: no confirmation dialog required (per spec, changes are just lost) + +### 8. Wire up tab switching to populate Build Skills tab +- In `SelectTab(2)` (UI.lua), call `AltSystem:RefreshBuildSkillsList()` to reload skills from TRP3 into the working copy +- This ensures the Build tab always shows the latest TRP3 data when opened, and any unsaved edits are discarded on tab switch + +### 9. Update AltSystem.toc +- Add `BuildSkillsUI.lua` to the file list (after `UI.lua`, before `Roll.lua`) + +### 10. Testing checklist +- [ ] Build Skills tab shows skills from TRP3 profile, sorted by level +- [ ] Skill name is editable via text input +- [ ] Level dropdown shows all 5 levels; changing level updates value dropdown options and auto-selects minimum value +- [ ] Value dropdown only shows values valid for the current level +- [ ] Delete button removes the row immediately +- [ ] "Add A Row" adds a row with defaults (Skillname, Novice, 1) +- [ ] Skill list scrolls when rows exceed visible area +- [ ] "Save" button is always visible (pinned to bottom) +- [ ] Save writes correct data to TRP3 profile (LT, RT, V2, IC preserved) +- [ ] Save does not modify icons of existing skills +- [ ] After save, Use Skills tab dropdown reflects the updated skills +- [ ] Switching tabs discards unsaved changes and reloads from TRP3 +- [ ] Works correctly with 0 skills (empty profile) +- [ ] Works correctly with many skills (20+) — scroll behavior \ No newline at end of file diff --git a/docs/Changelog.md b/docs/Changelog.md old mode 100644 new mode 100755 index dd05c6a..e69de29 --- a/docs/Changelog.md +++ b/docs/Changelog.md @@ -1,3 +0,0 @@ -- Adding default 'Unskilled (-4)' option -- The roll window now remembers all user selections (skill, item, armor type, shield, pet, announce and channel) across sessions -- Fixed an issue where the wrong rolls were displayed when in a large enough group \ No newline at end of file diff --git a/docs/build_skills_tab_design.png b/docs/build_skills_tab_design.png new file mode 100755 index 0000000000000000000000000000000000000000..ddbfdd3285606e326bd67833d8876c6172c9c878 GIT binary patch literal 100436 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

1oa-EAKTA<|Rsu63VUjTNtBH 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 literal 0 HcmV?d00001 diff --git a/docs/roll_tab_design.png b/docs/roll_tab_design.png new file mode 100644 index 0000000000000000000000000000000000000000..c80f5dc03faa3d615c90c381e79f74c9b3bc178b GIT binary patch literal 75385 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�*fc=V@KO!p1olbL-( zGAu=rBMb-twGU2S>Yw4?3q2{bhpGl%=w* zKD~ce1lU>UU)C*3Vfti4JV@}nE@*jAmg)v*m`Z@vrwm+dS7t)(Y!?!$I~(iG>Je_V+FlQkwLI9@|R!L&A{ z-B5a4gN#g?cWd2JJPlN;btYx$B=nw}8=&{tIi@HEz=l^^Tea94uL7X)QmIsP{yJV! zkPl3X+iVj;|5x#gy1W4PzSw11!|hI5H{g5-+hbmQJ^fTzwEy`%*(z8iWJ27^JFFRj?7o$^m@$od90D>?Lb z*UJsEMx6)LmEM+Bn3hz*qN4#=dBWF6MJo&37#Z!>u#i5R4U83p@deMWQ9dO@T)Q`! zk&wkWTh2^abs3g0Hvu*!nyA zN60-wv+q<}o$r)%eg3FEsfZgbm6Oq=$MUVbe`lZ^ zzXRNc-3(ZqXkfKZq3%AwbL{J*-oF_?{`D48M@%qlVN>{H)M+)y{m1q2A2-NOF?VE> Wey)1NU1Wo4Z{0Au4!>#_{yzX7^4@j; literal 0 HcmV?d00001 diff --git a/package.sh b/package.sh index 6febfa8..194f78f 100755 --- a/package.sh +++ b/package.sh @@ -7,7 +7,7 @@ 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' "$SCRIPT_DIR/" "$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" From 00a361157c85bd5f2e6ee4f4a2cbf49ca3e4f7b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Correia?= Date: Mon, 18 May 2026 12:07:22 +0100 Subject: [PATCH 20/21] Fixing skill name displaying wrong skill --- UI.lua | 3 --- docs/roll_tab_design.png | Bin 2 files changed, 3 deletions(-) mode change 100644 => 100755 docs/roll_tab_design.png diff --git a/UI.lua b/UI.lua index 534d061..642d328 100755 --- a/UI.lua +++ b/UI.lua @@ -682,9 +682,6 @@ function AltSystem:RefreshSkillDropdown() 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(i) - end if AltSystem.UpdateSkillWarning then AltSystem.UpdateSkillWarning(i) end diff --git a/docs/roll_tab_design.png b/docs/roll_tab_design.png old mode 100644 new mode 100755 From 72f3aa5731d959b35fa4364acd7455ee551a4091 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Correia?= Date: Mon, 18 May 2026 12:09:59 +0100 Subject: [PATCH 21/21] Fixing double 'Unskilled' option --- Data.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/Data.lua b/Data.lua index d1a1ae2..44914b4 100755 --- a/Data.lua +++ b/Data.lua @@ -25,7 +25,6 @@ local DEFAULT_SKILLS = { { name = "Adept Skill", level = "Adept", modifier = 0 }, { name = "Expert Skill", level = "Expert", modifier = 2 }, { name = "Master Skill", level = "Master", modifier = 4 }, - { name = "Unskilled", level = "Unskilled", modifier = -4 }, } -- Valid skill level keywords that must appear in the trait's right field (RT)