Compare commits

...

21 commits
0.2 ... main

Author SHA1 Message Date
72f3aa5731 Fixing double 'Unskilled' option 2026-05-18 12:09:59 +01:00
00a361157c Fixing skill name displaying wrong skill 2026-05-18 12:07:22 +01:00
6ebb92c62b feat/redesign (#1)
Reviewed-on: #1
Co-authored-by: Gonçalo Correia <goncalojoaocorreia@gmail.com>
Co-committed-by: Gonçalo Correia <goncalojoaocorreia@gmail.com>
2026-05-15 14:53:15 +01:00
6a44458443 Unskilled 2026-05-11 14:00:10 +01:00
c27a6cd033 Remember user selections across frame instances 2026-05-11 13:59:44 +01:00
5b36bfdb1b Adding unskilled default option 2026-05-11 12:24:48 +01:00
ab8388e27c Fixing roll detection by character name 2026-05-11 12:20:26 +01:00
ed1e696eb5 Pet modifier 2026-05-09 02:11:24 +01:00
0d9ec1913d Skill level mismatch warning 2026-05-08 16:33:36 +01:00
961d642018 Inept skill level 2026-05-08 16:14:45 +01:00
af5a3d2d41 TOC bump 2026-04-22 16:17:06 +01:00
52c2701075 TOC bump 2026-04-22 16:16:31 +01:00
2c878669b6 Improved packaging script 2026-04-14 01:12:51 +01:00
88f09f4fe4 Fix emote announce text 2026-04-14 01:08:58 +01:00
c4075d1ee9 More fixes to emote channel 2026-04-14 01:05:11 +01:00
865404ffb7 Add a 'base roll' skill 2026-04-14 00:49:04 +01:00
5a0d0f2ca4 Showing +0 mods in announce message 2026-04-14 00:45:30 +01:00
f8182ca30a Fixing emote announce while in raid 2026-04-14 00:44:08 +01:00
158c1742fe Packaging script 2026-04-13 14:10:52 +01:00
907e0464ed Chat announce rolls 2026-04-12 20:48:49 +01:00
e17370a89f Fixes 2026-04-11 20:27:46 +01:00
21 changed files with 1687 additions and 188 deletions

0
.idea/.gitignore generated vendored Normal file → Executable file
View file

0
.idea/misc.xml generated Normal file → Executable file
View file

0
.idea/modules.xml generated Normal file → Executable file
View file

0
.idea/vcs.xml generated Normal file → Executable file
View file

0
AltSystem.iml Normal file → Executable file
View file

5
AltSystem.toc Normal file → Executable file
View file

@ -1,12 +1,13 @@
## Interface: 120001 ## Interface: 120005
## Title: AltSystem ## Title: AltSystem
## Notes: Enhances RP gameplay with a custom rolling system ## Notes: Enhances RP gameplay with a custom rolling system
## Author: Rukira ## Author: Rukira
## Version: 0.2 ## Version: 1.2.0
## Dependencies: totalRP3, totalRP3_Extended ## Dependencies: totalRP3, totalRP3_Extended
## SavedVariables: AltSystemDB ## SavedVariables: AltSystemDB
Data.lua Data.lua
Core.lua Core.lua
BuildSkillsUI.lua
UI.lua UI.lua
Roll.lua Roll.lua

430
BuildSkillsUI.lua Executable file
View file

@ -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

74
Core.lua Normal file → Executable file
View file

@ -4,9 +4,24 @@
AltSystem = AltSystem or {} AltSystem = AltSystem or {}
AltSystem.State = { AltSystem.State = {
selectedSkillIndex = 1, selectedSkillIndex = 1,
selectedSkillName = nil, -- skill name used to restore selection across sessions
selectedItemIndex = 1, -- 1 = No item selectedItemIndex = 1, -- 1 = No item
selectedDefenseIndex = 1, -- 1 = Base armor selectedDefenseIndex = 1, -- 1 = No Armor
shieldEnabled = false, shieldEnabled = false,
petSummonEnabled = false,
announceEnabled = false,
announceChannelIndex = 1, -- 1 = Emote, 2 = Party, 3 = Raid, 4 = Guild
rollType = "attack", -- "attack" or "defense"
announceOptionIndex = 1, -- 1 = Self Roll, 2+ = channel from AnnounceChannels
rollLog = {}, -- array of { text = "...", timestamp = time() }, max 100, not persisted
}
-- Channel definitions for announcing rolls
AltSystem.AnnounceChannels = {
{ name = "Emote (/e)", channel = "EMOTE" },
{ name = "Party (/p)", channel = "PARTY" },
{ name = "Raid (/ra)", channel = "RAID" },
{ name = "Guild (/g)", channel = "GUILD" },
} }
-- Initialization on ADDON_LOADED -- Initialization on ADDON_LOADED
@ -21,12 +36,69 @@ frame:SetScript("OnEvent", function(self, event, arg1)
end) end)
function AltSystem:Init() function AltSystem:Init()
-- Load saved settings
AltSystemDB = AltSystemDB or {}
if AltSystemDB.petSummonEnabled ~= nil then
AltSystem.State.petSummonEnabled = AltSystemDB.petSummonEnabled
end
if AltSystemDB.shieldEnabled ~= nil then
AltSystem.State.shieldEnabled = AltSystemDB.shieldEnabled
end
if AltSystemDB.selectedItemIndex then
AltSystem.State.selectedItemIndex = AltSystemDB.selectedItemIndex
end
if AltSystemDB.selectedDefenseIndex then
AltSystem.State.selectedDefenseIndex = AltSystemDB.selectedDefenseIndex
end
if AltSystemDB.selectedSkillName then
AltSystem.State.selectedSkillName = AltSystemDB.selectedSkillName
end
if AltSystemDB.rollType then
AltSystem.State.rollType = AltSystemDB.rollType
end
-- Migrate from old announce settings or load new announceOptionIndex
if AltSystemDB.announceOptionIndex then
AltSystem.State.announceOptionIndex = AltSystemDB.announceOptionIndex
elseif AltSystemDB.announceEnabled ~= nil then
-- Backwards compat: derive from old announceEnabled + announceChannelIndex
if AltSystemDB.announceEnabled and AltSystemDB.announceChannelIndex then
AltSystem.State.announceOptionIndex = AltSystemDB.announceChannelIndex + 1
else
AltSystem.State.announceOptionIndex = 1
end
end
-- Derive announceEnabled and announceChannelIndex from announceOptionIndex
if AltSystem.State.announceOptionIndex > 1 then
AltSystem.State.announceEnabled = true
AltSystem.State.announceChannelIndex = AltSystem.State.announceOptionIndex - 1
else
AltSystem.State.announceEnabled = false
AltSystem.State.announceChannelIndex = 1
end
-- Register slash command /altsystem -- Register slash command /altsystem
SLASH_ALTSYSTEM1 = "/altsystem" SLASH_ALTSYSTEM1 = "/altsystem"
SlashCmdList["ALTSYSTEM"] = function() SlashCmdList["ALTSYSTEM"] = function()
AltSystem:ToggleWindow() AltSystem:ToggleWindow()
end end
-- Save settings on logout
local saveFrame = CreateFrame("Frame")
saveFrame:RegisterEvent("PLAYER_LOGOUT")
saveFrame:SetScript("OnEvent", function()
AltSystemDB = AltSystemDB or {}
AltSystemDB.announceOptionIndex = AltSystem.State.announceOptionIndex
AltSystemDB.petSummonEnabled = AltSystem.State.petSummonEnabled
AltSystemDB.shieldEnabled = AltSystem.State.shieldEnabled
AltSystemDB.selectedItemIndex = AltSystem.State.selectedItemIndex
AltSystemDB.selectedDefenseIndex = AltSystem.State.selectedDefenseIndex
AltSystemDB.selectedSkillName = AltSystem.State.selectedSkillName
AltSystemDB.rollType = AltSystem.State.rollType
end)
-- Build the main frame now that saved variables are loaded
AltSystem:CreateMainFrame()
print("|cff00ccffAltSystem|r loaded. Type /altsystem to open.") print("|cff00ccffAltSystem|r loaded. Type /altsystem to open.")
end end

203
Data.lua Normal file → Executable file
View file

@ -6,18 +6,21 @@ AltSystem.Data = {}
-- Skill levels and their modifiers -- Skill levels and their modifiers
AltSystem.Data.SkillLevels = { AltSystem.Data.SkillLevels = {
["Inept"] = -4,
["Novice"] = -2, ["Novice"] = -2,
["Adept"] = 0, ["Adept"] = 0,
["Expert"] = 2, ["Expert"] = 2,
["Master"] = 4, ["Master"] = 4,
} }
-- The "Unskilled" 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 "Unskilled" entry is always the last skill
local UNSKILLED_ENTRY = { name = "Unskilled", level = "Unskilled", modifier = -4 } local UNSKILLED_ENTRY = { name = "Unskilled", level = "Unskilled", modifier = -4 }
-- Default/fallback skill list used when no TRP3 profile skills are found -- Default/fallback skill list used when no TRP3 profile skills are found
local DEFAULT_SKILLS = { local DEFAULT_SKILLS = {
{ name = "Unskilled", level = "Unskilled", modifier = -4 }, { name = "Base roll", level = "Base", modifier = 0 },
{ name = "Novice Skill", level = "Novice", modifier = -2 }, { name = "Novice Skill", level = "Novice", modifier = -2 },
{ name = "Adept Skill", level = "Adept", modifier = 0 }, { name = "Adept Skill", level = "Adept", modifier = 0 },
{ name = "Expert Skill", level = "Expert", modifier = 2 }, { name = "Expert Skill", level = "Expert", modifier = 2 },
@ -25,25 +28,70 @@ local DEFAULT_SKILLS = {
} }
-- Valid skill level keywords that must appear in the trait's right field (RT) -- 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. -- Expected numeric value ranges for each skill keyword
-- Returns true if any keyword is found, false otherwise. local SKILL_KEYWORD_RANGES = {
local function HasSkillKeyword(rightText) ["Novice"] = { min = 1, max = 5 },
if not rightText or rightText == "" then return false end ["Adept"] = { min = 6, max = 10 },
for _, keyword in ipairs(VALID_SKILL_KEYWORDS) do ["Expert"] = { min = 11, max = 19 },
if rightText:find(keyword) then ["Master"] = { min = 20, max = 20 },
return true }
-- 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 end
end return 1
return false
end end
-- Determine the skill level from the trait's numeric value (V2 field, 0-20 range). -- Check if the trait's right text field contains a valid skill keyword.
-- Returns the level string and modifier, or nil if the value is 0 or absent. -- Returns the matched keyword if found, nil otherwise.
local function ParseSkillLevel(numericValue) local function FindSkillKeyword(rightText)
if not numericValue or numericValue <= 0 then return nil, nil end if not rightText or rightText == "" then return nil end
for _, keyword in ipairs(VALID_SKILL_KEYWORDS) do
if rightText:find(keyword) then
return keyword
end
end
return nil
end
-- Check whether the numeric value matches the expected range for the given keyword.
-- Returns a warning string if mismatched, or nil if everything is fine.
local function CheckSkillMismatch(keyword, numericValue)
if not keyword or keyword == "Inept" then return nil end
local range = SKILL_KEYWORD_RANGES[keyword]
if not range then return nil end
if not numericValue or numericValue <= 0 then return nil end
if numericValue < range.min or numericValue > range.max then
return "Skill is marked as " .. keyword .. " (" .. range.min .. "-" .. range.max .. "), but the profile has a value of " .. numericValue .. "."
end
return nil
end
-- Determine the skill level from the trait's right text and numeric value (V2 field, 0-20 range).
-- If the trait is marked as Inept, always returns "Inept" with -4 modifier regardless of numeric value.
-- Otherwise returns the level string and modifier based on numeric value, or nil if the value is 0 or absent.
local function ParseSkillLevel(rightText, numericValue)
if rightText and rightText:find("Inept") then
return "Inept", AltSystem.Data.SkillLevels["Inept"]
end
if not numericValue or numericValue <= 0 then return nil, nil end
if numericValue >= 20 then if numericValue >= 20 then
return "Master", AltSystem.Data.SkillLevels["Master"] return "Master", AltSystem.Data.SkillLevels["Master"]
elseif numericValue >= 11 then elseif numericValue >= 11 then
@ -56,13 +104,13 @@ local function ParseSkillLevel(numericValue)
end end
-- Fetch skills from the current TRP3 profile's personality traits. -- Fetch skills from the current TRP3 profile's personality traits.
-- Returns an array of skill entries, always starting with "Unskilled". -- Returns an array of skill entries, always starting with "Base roll".
-- Traits marked as Inept get a -4 modifier regardless of their numeric value.
-- Falls back to the default list if no profile or no valid skills are found. -- Falls back to the default list if no profile or no valid skills are found.
function AltSystem.Data:RefreshSkills() function AltSystem.Data:RefreshSkills()
local skills = {} local skills = {}
-- Always add Base roll as the first entry
-- Always add Unskilled 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 local foundAny = false
@ -74,14 +122,16 @@ function AltSystem.Data:RefreshSkills()
local skillName = trait.LT local skillName = trait.LT
local numericValue = trait.V2 local numericValue = trait.V2
if skillName and skillName ~= "" and HasSkillKeyword(trait.RT) then local keyword = FindSkillKeyword(trait.RT)
local level, modifier = ParseSkillLevel(numericValue) if skillName and skillName ~= "" and keyword then
local level, modifier = ParseSkillLevel(trait.RT, numericValue)
if level and modifier then if level and modifier then
foundAny = true foundAny = true
table.insert(skills, { table.insert(skills, {
name = skillName, name = skillName,
level = level, level = level,
modifier = modifier, modifier = modifier,
warning = CheckSkillMismatch(keyword, numericValue),
}) })
end end
end end
@ -89,7 +139,7 @@ function AltSystem.Data:RefreshSkills()
end end
end end
-- If no valid skills were found, use the default fallback list (skip first "Unskilled" since we already added it) -- If no valid skills were found, use the default fallback list
if not foundAny then if not foundAny then
skills = {} skills = {}
for _, skill in ipairs(DEFAULT_SKILLS) do for _, skill in ipairs(DEFAULT_SKILLS) do
@ -97,28 +147,119 @@ function AltSystem.Data:RefreshSkills()
end end
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 AltSystem.Data.Skills = skills
return skills return skills
end 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 -- Initialize with the default skill list
AltSystem.Data.Skills = {} AltSystem.Data.Skills = {}
for _, skill in ipairs(DEFAULT_SKILLS) do for _, skill in ipairs(DEFAULT_SKILLS) do
table.insert(AltSystem.Data.Skills, { name = skill.name, level = skill.level, modifier = skill.modifier }) table.insert(AltSystem.Data.Skills, { name = skill.name, level = skill.level, modifier = skill.modifier })
end 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 = { AltSystem.Data.Items = {
{ name = "No item", modifier = 0 }, { name = "No item", label = "No item", modifier = 0 },
{ name = "Rare item", modifier = 3 }, { name = "Rare", label = "Rare item", modifier = 3 },
{ name = "Epic item", modifier = 5 }, { 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 = { AltSystem.Data.Defenses = {
{ name = "Base armor", modifier = 0 }, { name = "None", label = "No armor", modifier = 0 },
{ name = "Extra small armor", modifier = 1 }, { name = "Partial", label = "Extra armor", modifier = 1 },
{ name = "Extra large armor", modifier = 2 }, { name = "Full", label = "Extra armor", modifier = 2 },
} }
-- Shield modifier -- Shield modifier

0
README.md Normal file → Executable file
View file

197
Roll.lua Normal file → Executable file
View file

@ -4,9 +4,119 @@
AltSystem = AltSystem or {} AltSystem = AltSystem or {}
local pendingRollType = nil -- "attack" or "defense" local pendingRollType = nil -- "attack" or "defense"
local pendingPetRoll = nil -- { rollType, mainRollValue } when awaiting pet/summon roll
-- Get the TRP3 character first name, falling back to the WoW unit name
local function GetCharacterName()
if TRP3_API and TRP3_API.profile and TRP3_API.profile.getData then
local ok, characteristics = pcall(TRP3_API.profile.getData, "player/characteristics")
if ok and characteristics and characteristics.FN and characteristics.FN ~= "" then
return characteristics.FN
end
end
return UnitName("player")
end
-- Build the modifier description string for the announce message
local function BuildModifierString(modifiers)
local parts = {}
for _, mod in ipairs(modifiers) do
local sign = mod.value >= 0 and "+" or ""
table.insert(parts, sign .. mod.value .. " (" .. mod.name .. ")")
end
return table.concat(parts, " ")
end
-- Build the log message string (always uses the non-emote format with character name)
local function BuildLogMessage(rollValue, modifiers, total)
local name = GetCharacterName()
local modStr = BuildModifierString(modifiers)
if modStr ~= "" then
return name .. " rolled " .. rollValue .. " " .. modStr .. " = " .. total
else
return name .. " rolled " .. rollValue .. " = " .. total
end
end
-- Build the critical roll log message
local function BuildCriticalLogMessage(isCriticalSuccess)
local name = GetCharacterName()
if isCriticalSuccess then
return name .. " rolled a Critical Success!"
else
return name .. " rolled a Critical Failure!"
end
end
-- Add an entry to the roll log and refresh the UI
local function AddLogEntry(text)
local rollLog = AltSystem.State.rollLog
table.insert(rollLog, { text = text, timestamp = time() })
-- Cap at 100 entries
if #rollLog > 100 then
table.remove(rollLog, 1)
end
-- Refresh the log panel if it exists
if AltSystem.RefreshLogPanel then
AltSystem:RefreshLogPanel()
end
end
-- Send a message to the given channel.
local function SendToChannel(msg, channel)
SendChatMessage(msg, channel)
end
-- Announce the roll result to the selected chat channel
local function AnnounceRoll(rollValue, modifiers, total)
if not AltSystem.State.announceEnabled then return end
local channelDef = AltSystem.AnnounceChannels[AltSystem.State.announceChannelIndex]
if not channelDef then return end
local isEmote = channelDef.channel == "EMOTE"
local modStr = BuildModifierString(modifiers)
local msg
if isEmote then
if modStr ~= "" then
msg = "rolled " .. rollValue .. " " .. modStr .. " = " .. total
else
msg = "rolled " .. rollValue .. " = " .. total
end
else
local name = GetCharacterName()
if modStr ~= "" then
msg = name .. " rolled " .. rollValue .. " " .. modStr .. " = " .. total
else
msg = name .. " rolled " .. rollValue .. " = " .. total
end
end
SendToChannel(msg, channelDef.channel)
end
-- Announce a critical roll result
local function AnnounceCritical(isCriticalSuccess)
if not AltSystem.State.announceEnabled then return end
local channelDef = AltSystem.AnnounceChannels[AltSystem.State.announceChannelIndex]
if not channelDef then return end
local critText = isCriticalSuccess and "rolled a Critical Success!" or "rolled a Critical Failure!"
local msg
if channelDef.channel == "EMOTE" then
msg = critText
else
msg = GetCharacterName() .. " " .. critText
end
SendToChannel(msg, channelDef.channel)
end
-- Perform a roll: triggers the WoW native /roll 20 command -- Perform a roll: triggers the WoW native /roll 20 command
function AltSystem:PerformRoll(rollType) function AltSystem:PerformRoll(rollType)
pendingPetRoll = nil
pendingRollType = rollType pendingRollType = rollType
RandomRoll(1, 20) RandomRoll(1, 20)
end end
@ -16,21 +126,55 @@ local rollListener = CreateFrame("Frame")
rollListener:RegisterEvent("CHAT_MSG_SYSTEM") rollListener:RegisterEvent("CHAT_MSG_SYSTEM")
rollListener:SetScript("OnEvent", function(self, event, message) rollListener:SetScript("OnEvent", function(self, event, message)
-- Phase 2: waiting for pet/summon roll result (1-5)
if pendingPetRoll then
local petRoller, petRoll = message:match("(.+) rolls (%d+) %(1%-5%)")
if not petRoll then return end
if petRoller ~= UnitName("player") then return end
local petValue = tonumber(petRoll)
local info = pendingPetRoll
pendingPetRoll = nil
AltSystem:CalculateAndDisplayResult(info.rollType, info.mainRollValue, petValue)
return
end
-- Phase 1: waiting for main roll result (1-20)
if not pendingRollType then return end if not pendingRollType then return end
-- Match the roll result pattern: "PlayerName rolls X (1-20)" -- 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 not roll then return end
if roller ~= UnitName("player") then return end
local rollValue = tonumber(roll) local rollValue = tonumber(roll)
local rollType = pendingRollType local rollType = pendingRollType
pendingRollType = nil 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) AltSystem:CalculateAndDisplayResult(rollType, rollValue)
end) end)
-- Calculate the final result based on the roll type and selected modifiers -- Calculate the final result based on the roll type and selected modifiers
function AltSystem:CalculateAndDisplayResult(rollType, rollValue) function AltSystem:CalculateAndDisplayResult(rollType, rollValue, petRollValue)
-- Critical rolls bypass normal calculation
if rollValue == 1 then
AddLogEntry(BuildCriticalLogMessage(false))
AnnounceCritical(false)
return
elseif rollValue == 20 then
AddLogEntry(BuildCriticalLogMessage(true))
AnnounceCritical(true)
return
end
local state = AltSystem.State local state = AltSystem.State
local skill = AltSystem.Data.Skills[state.selectedSkillIndex] local skill = AltSystem.Data.Skills[state.selectedSkillIndex]
local item = AltSystem.Data.Items[state.selectedItemIndex] local item = AltSystem.Data.Items[state.selectedItemIndex]
@ -39,17 +183,26 @@ function AltSystem:CalculateAndDisplayResult(rollType, rollValue)
local itemMod = item and item.modifier or 0 local itemMod = item and item.modifier or 0
local total = rollValue local total = rollValue
local breakdown = "Roll: " .. rollValue
local modifiers = {}
local isBaseRoll = skill and skill.name == "Base roll"
local petMod = petRollValue or 0
if rollType == "attack" then if rollType == "attack" then
-- Attack Roll = roll + skill modifier + item modifier -- Attack Roll = roll + skill modifier + item modifier + pet/summon modifier
total = rollValue + skillMod + itemMod total = rollValue + skillMod + itemMod + petMod
breakdown = breakdown .. "\nSkill: " .. FormatModifier(skillMod) if not isBaseRoll then
if itemMod ~= 0 then table.insert(modifiers, { name = skill and skill.name or "Skill", value = skillMod })
breakdown = breakdown .. " | Item: " .. FormatModifier(itemMod) end
if itemMod ~= 0 then
table.insert(modifiers, { name = item and (item.label or item.name) or "Item", value = itemMod })
end
if petMod ~= 0 then
table.insert(modifiers, { name = "Pet", value = petMod })
end end
breakdown = breakdown .. "\n|cffffd100Attack Total: " .. total .. "|r"
elseif rollType == "defense" then elseif rollType == "defense" then
-- Defense Roll = roll + skill modifier + item modifier + defense modifier + shield modifier -- Defense Roll = roll + skill modifier + item modifier + defense modifier + shield modifier
@ -57,22 +210,30 @@ function AltSystem:CalculateAndDisplayResult(rollType, rollValue)
local defenseMod = defense and defense.modifier or 0 local defenseMod = defense and defense.modifier or 0
local shieldMod = state.shieldEnabled and AltSystem.Data.ShieldModifier or 0 local shieldMod = state.shieldEnabled and AltSystem.Data.ShieldModifier or 0
total = rollValue + skillMod + itemMod + defenseMod + shieldMod total = rollValue + skillMod + itemMod + defenseMod + shieldMod + petMod
breakdown = breakdown .. "\nSkill: " .. FormatModifier(skillMod) if not isBaseRoll then
table.insert(modifiers, { name = skill and skill.name or "Skill", value = skillMod })
end
if itemMod ~= 0 then if itemMod ~= 0 then
breakdown = breakdown .. " | Item: " .. FormatModifier(itemMod) table.insert(modifiers, { name = item and (item.label or item.name) or "Item", value = itemMod })
end
if defenseMod ~= 0 then
table.insert(modifiers, { name = defense and (defense.label or defense.name) or "Armor", value = defenseMod })
end end
breakdown = breakdown .. "\nDefense: " .. FormatModifier(defenseMod)
if shieldMod ~= 0 then if shieldMod ~= 0 then
breakdown = breakdown .. " | Shield: " .. FormatModifier(shieldMod) table.insert(modifiers, { name = "Shield", value = shieldMod })
end
if petMod ~= 0 then
table.insert(modifiers, { name = "Pet", value = petMod })
end end
breakdown = breakdown .. "\n|cff00ccffDefense Total: " .. total .. "|r"
end end
if AltSystem.ResultText then -- Add to log (always, regardless of announce setting)
AltSystem.ResultText:SetText(breakdown) AddLogEntry(BuildLogMessage(rollValue, modifiers, total))
end
-- Announce to chat (if enabled)
AnnounceRoll(rollValue, modifiers, total)
end end
-- Format a modifier value with sign -- Format a modifier value with sign

692
UI.lua Normal file → Executable file
View file

@ -1,14 +1,21 @@
-- AltSystem UI -- 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+). -- Uses the modern DropdownButton API (WoW 10.2.5+ / 12.0+).
AltSystem = AltSystem or {} AltSystem = AltSystem or {}
local WINDOW_WIDTH = 300 local WINDOW_WIDTH = 720
local WINDOW_HEIGHT = 380 local WINDOW_HEIGHT = 520
local CONTROLS_WIDTH = 360
local LOG_WIDTH = 360
local PADDING = 12 local PADDING = 12
local ROW_HEIGHT = 30 local PADDING_HEADER = 6
local LABEL_WIDTH = 80 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 -- Helper: Build the skill option list from current AltSystem.Data.Skills
local function BuildSkillOptions() local function BuildSkillOptions()
@ -16,7 +23,7 @@ local function BuildSkillOptions()
for _, skill in ipairs(AltSystem.Data.Skills) do for _, skill in ipairs(AltSystem.Data.Skills) do
local sign = skill.modifier >= 0 and "+" or "" local sign = skill.modifier >= 0 and "+" or ""
local text local text
if skill.level == "Unskilled" then if skill.level == "Base" or skill.level == "Inept" then
text = skill.name .. " (" .. sign .. skill.modifier .. ")" text = skill.name .. " (" .. sign .. skill.modifier .. ")"
else else
text = skill.name .. " (" .. skill.level .. " " .. sign .. skill.modifier .. ")" text = skill.name .. " (" .. skill.level .. " " .. sign .. skill.modifier .. ")"
@ -26,39 +33,144 @@ local function BuildSkillOptions()
return options return options
end end
-- Helper: Create a modern dropdown (WowStyle1DropdownTemplate) -- Helper: Create a flat dark dropdown with label on top (reuses shared CreateFlatDropdown)
local function CreateDropdown(parent, name, yOffset, labelText, options, defaultIndex, onSelect) local function CreateDropdown(parent, name, labelText, options, defaultIndex, onSelect, labelFont)
local label = parent:CreateFontString(nil, "OVERLAY", "GameFontNormal") local hasLabel = labelText and labelText ~= ""
label:SetPoint("TOPLEFT", parent, "TOPLEFT", PADDING, yOffset) 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:SetText(labelText)
label:SetWidth(LABEL_WIDTH)
label:SetJustifyH("LEFT") label:SetJustifyH("LEFT")
label:SetTextColor(0.9, 0.75, 0.2)
end
local selectedIndex = defaultIndex or 1 local selectedIndex = defaultIndex or 1
local dropdown = CreateFrame("DropdownButton", name, parent, "WowStyle1DropdownTemplate") local dropdown = AltSystem.CreateFlatDropdown(name, container, 190)
dropdown:SetPoint("LEFT", label, "RIGHT", 4, 0) if hasLabel then
dropdown:SetWidth(160) 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 for i, option in ipairs(options) do
rootDescription:CreateRadio( rootDescription:CreateRadio(
option.text, option.text,
function(data) return data == selectedIndex end, function()
function(data) return i == selectedIndex
selectedIndex = data
if onSelect then onSelect(data, options[data]) end
end, end,
i function()
selectedIndex = i
dropdown.label:SetText(option.text)
if onSelect then
onSelect(i, option)
end
end
) )
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 end
function AltSystem:CreateMainFrame() function AltSystem:CreateMainFrame()
if AltSystem.MainFrame then return end if AltSystem.MainFrame then
return
end
-- Main frame -- Main frame
local f = CreateFrame("Frame", "AltSystemMainFrame", UIParent, "BasicFrameTemplateWithInset") local f = CreateFrame("Frame", "AltSystemMainFrame", UIParent, "BasicFrameTemplateWithInset")
@ -76,166 +188,508 @@ function AltSystem:CreateMainFrame()
f.title:SetPoint("TOPLEFT", f.TitleBg, "TOPLEFT", 5, -3) f.title:SetPoint("TOPLEFT", f.TitleBg, "TOPLEFT", 5, -3)
f.title:SetText("AltSystem") 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 -- Skill dropdown
-------------------------
local skillOptions = BuildSkillOptions() local skillOptions = BuildSkillOptions()
local UpdateSkillWarning -- forward declaration
local skillDropdown, getSkillIndex, setSkillIndex = CreateDropdown( local skillContainer, skillDropdown, getSkillIndex, setSkillIndex = CreateDropdown(
f, "AltSystemSkillDropdown", yPos, "Skill:", skillOptions, content, "AltSystemSkillDropdown", "Skill", skillOptions,
AltSystem.State.selectedSkillIndex, AltSystem.State.selectedSkillIndex,
function(index) function(index)
AltSystem.State.selectedSkillIndex = index AltSystem.State.selectedSkillIndex = index
AltSystem.State.selectedSkillName = AltSystem.Data.Skills[index] and AltSystem.Data.Skills[index].name or nil
UpdateSkillWarning(index)
end,
"GameFontNormal"
)
skillContainer:SetPoint("TOPLEFT", content, "TOPLEFT", PADDING, yPos)
skillContainer:SetWidth(CONTROLS_WIDTH - PADDING * 2)
-- Warning icon for skill mismatch
local skillWarning = CreateFrame("Frame", nil, skillContainer)
skillWarning:SetSize(20, 20)
skillWarning:SetPoint("LEFT", skillDropdown, "RIGHT", 4, 0)
local skillWarningIcon = skillWarning:CreateTexture(nil, "ARTWORK")
skillWarningIcon:SetAllPoints()
skillWarningIcon:SetAtlas("services-icon-warning")
skillWarning:SetScript("OnEnter", function(self)
if self.tooltipText then
GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
GameTooltip:SetText(self.tooltipText, 1, 0.82, 0)
GameTooltip:Show()
end
end) end)
skillWarning:SetScript("OnLeave", function()
GameTooltip:Hide()
end)
skillWarning:Hide()
UpdateSkillWarning = function(index)
local skill = AltSystem.Data.Skills[index]
if skill and skill.warning then
skillWarning.tooltipText = skill.warning
skillWarning:Show()
else
skillWarning:Hide()
end
end
-- Store references for refreshing -- Store references for refreshing
AltSystem.SkillDropdown = skillDropdown AltSystem.SkillDropdown = skillDropdown
AltSystem.GetSkillIndex = getSkillIndex AltSystem.GetSkillIndex = getSkillIndex
AltSystem.SetSkillIndex = setSkillIndex AltSystem.SetSkillIndex = setSkillIndex
yPos = yPos - ROW_HEIGHT - 8 AltSystem.UpdateSkillWarning = UpdateSkillWarning
------------------------- yPos = yPos - (LABEL_HEIGHT + LABEL_GAP + 28) - ITEM_GAP
-- 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
CreateDropdown(f, "AltSystemItemDropdown", yPos, "Item:", itemOptions, AltSystem.State.selectedItemIndex, -- Armor label
function(index) CreateSubLabel(content, "Extra Armor", PADDING, yPos)
AltSystem.State.selectedItemIndex = index yPos = yPos - (LABEL_HEIGHT + LABEL_GAP)
end)
yPos = yPos - ROW_HEIGHT - 8
------------------------- -- Armor radio buttons
-- Defense dropdown local armorRadios = {}
-------------------------
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
CreateDropdown(f, "AltSystemDefenseDropdown", yPos, "Defense:", defenseOptions, AltSystem.State.selectedDefenseIndex, local function UpdateArmorSelection(index)
function(index)
AltSystem.State.selectedDefenseIndex = index AltSystem.State.selectedDefenseIndex = index
for i, radio in ipairs(armorRadios) do
radio:SetChecked(i == index)
radio.UpdateVisual()
end
end
local armorX = PADDING
for i, def in ipairs(AltSystem.Data.Defenses) do
local text = def.name
if def.modifier > 0 then
text = text .. " (+" .. def.modifier .. ")"
end
local radio = CreateRadioButton(content, "AltSystemArmorRadio" .. i, text, armorX, yPos,
AltSystem.State.selectedDefenseIndex == i,
function()
UpdateArmorSelection(i)
end) end)
yPos = yPos - ROW_HEIGHT - 8 table.insert(armorRadios, radio)
armorX = armorX + 110
end
------------------------- yPos = yPos - ROW_HEIGHT - SECTION_GAP
-- Shield checkbox
-------------------------
local shieldLabel = f:CreateFontString(nil, "OVERLAY", "GameFontNormal")
shieldLabel:SetPoint("TOPLEFT", f, "TOPLEFT", PADDING, yPos)
shieldLabel:SetText("Shield:")
shieldLabel:SetWidth(LABEL_WIDTH)
shieldLabel:SetJustifyH("LEFT")
local shieldCheck = CreateFrame("CheckButton", "AltSystemShieldCheck", f, "UICheckButtonTemplate") -- Section: Modifiers (optional)
shieldCheck:SetPoint("LEFT", shieldLabel, "RIGHT", -6, 0) 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) shieldCheck:SetChecked(AltSystem.State.shieldEnabled)
local shieldText = shieldCheck:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall") local shieldBg = shieldCheck:CreateTexture(nil, "BACKGROUND")
shieldText:SetPoint("LEFT", shieldCheck, "RIGHT", 2, 0) shieldBg:SetAllPoints()
shieldText:SetText("+1 modifier") 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) shieldCheck:SetScript("OnClick", function(self)
AltSystem.State.shieldEnabled = self:GetChecked() AltSystem.State.shieldEnabled = self:GetChecked()
UpdateShieldVisual()
end) end)
yPos = yPos - ROW_HEIGHT - 12 -- Pet checkbox (flat square toggle)
local petCheck = CreateFrame("CheckButton", "AltSystemPetSummonCheck", content)
petCheck:SetSize(20, 20)
petCheck:SetPoint("LEFT", shieldCheck, "RIGHT", 80, 0)
petCheck:SetChecked(AltSystem.State.petSummonEnabled)
------------------------- local petBg = petCheck:CreateTexture(nil, "BACKGROUND")
-- Roll buttons petBg:SetAllPoints()
------------------------- petBg:SetColorTexture(0.3, 0.3, 0.3, 1)
local btnWidth = (WINDOW_WIDTH - PADDING * 3) / 2
local attackBtn = CreateFrame("Button", "AltSystemAttackRollBtn", f, "UIPanelButtonTemplate") local petCheckMark = petCheck:CreateTexture(nil, "ARTWORK")
attackBtn:SetSize(btnWidth, 28) petCheckMark:SetSize(14, 14)
attackBtn:SetPoint("TOPLEFT", f, "TOPLEFT", PADDING, yPos) petCheckMark:SetPoint("CENTER")
attackBtn:SetText("Attack/Skill Roll") petCheckMark:SetTexture("Interface\\RAIDFRAME\\ReadyCheck-Ready")
petCheckMark:SetVertexColor(0.9, 0.75, 0.2, 1)
local defenseBtn = CreateFrame("Button", "AltSystemDefenseRollBtn", f, "UIPanelButtonTemplate") local function UpdatePetVisual()
defenseBtn:SetSize(btnWidth, 28) if petCheck:GetChecked() then
defenseBtn:SetPoint("TOPRIGHT", f, "TOPRIGHT", -PADDING, yPos) petCheckMark:Show()
defenseBtn:SetText("Defense Roll") petBg:SetColorTexture(0.25, 0.25, 0.25, 1)
else
petCheckMark:Hide()
petBg:SetColorTexture(0.3, 0.3, 0.3, 1)
end
end
UpdatePetVisual()
yPos = yPos - 40 local petLabel = petCheck:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
petLabel:SetPoint("LEFT", petCheck, "RIGHT", 6, 0)
petLabel:SetText("Pet (+d5)")
------------------------- petCheck:SetScript("OnClick", function(self)
-- Roll result area AltSystem.State.petSummonEnabled = self:GetChecked()
------------------------- UpdatePetVisual()
local resultBg = CreateFrame("Frame", nil, f, "InsetFrameTemplate")
resultBg:SetPoint("TOPLEFT", f, "TOPLEFT", PADDING, yPos)
resultBg:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -PADDING, PADDING)
local resultText = resultBg:CreateFontString(nil, "OVERLAY", "GameFontHighlightLarge")
resultText:SetPoint("CENTER")
resultText:SetText("Roll result will appear here")
resultText:SetJustifyH("CENTER")
resultText:SetWidth(resultBg:GetWidth() - 10)
AltSystem.ResultText = resultText
-- Button click handlers
attackBtn:SetScript("OnClick", function()
AltSystem:PerformRoll("attack")
end) end)
defenseBtn:SetScript("OnClick", function() yPos = yPos - ROW_HEIGHT - ITEM_GAP
AltSystem:PerformRoll("defense")
-- 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) 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 -- Refresh skills from TRP3 profile each time the window is shown
f:SetScript("OnShow", function() f:SetScript("OnShow", function()
AltSystem:RefreshSkillDropdown() AltSystem:RefreshSkillDropdown()
AltSystem:RefreshLogPanel()
end) end)
f:Hide() f:Hide()
AltSystem.MainFrame = f AltSystem.MainFrame = f
end 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 -- Refresh the skill dropdown with current TRP3 profile data
function AltSystem:RefreshSkillDropdown() function AltSystem:RefreshSkillDropdown()
AltSystem.Data:RefreshSkills() AltSystem.Data:RefreshSkills()
-- Reset selection to 1 (Unskilled) since the skill list may have changed -- Try to restore the previously selected skill by name; fall back to 1 (Base roll)
AltSystem.State.selectedSkillIndex = 1 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 if AltSystem.SetSkillIndex then
AltSystem.SetSkillIndex(1) AltSystem.SetSkillIndex(newIndex)
end
if AltSystem.UpdateSkillWarning then
AltSystem.UpdateSkillWarning(newIndex)
end end
-- Rebuild the dropdown menu with the new skill list -- Rebuild the dropdown menu with the new skill list
if AltSystem.SkillDropdown then if AltSystem.SkillDropdown then
local skillOptions = BuildSkillOptions() 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 for i, option in ipairs(skillOptions) do
rootDescription:CreateRadio( rootDescription:CreateRadio(
option.text, option.text,
function(data) return data == AltSystem.State.selectedSkillIndex end, function()
function(data) return i == AltSystem.State.selectedSkillIndex
AltSystem.State.selectedSkillIndex = data
if AltSystem.SetSkillIndex then
AltSystem.SetSkillIndex(data)
end
end, end,
i function()
AltSystem.State.selectedSkillIndex = i
AltSystem.State.selectedSkillName = AltSystem.Data.Skills[i] and AltSystem.Data.Skills[i].name or nil
AltSystem.SkillDropdown.label:SetText(option.text)
if AltSystem.UpdateSkillWarning then
AltSystem.UpdateSkillWarning(i)
end
end
) )
end end
end) end)
end end
end end
-- Create the frame on file load so it's ready when Init runs -- Frame is created by AltSystem:Init() in Core.lua, after saved variables are loaded
AltSystem:CreateMainFrame()

0
docs/1-interface.md Normal file → Executable file
View file

6
docs/2-skills.md Normal file → Executable file
View file

@ -3,13 +3,13 @@
- The skills are defined in the TRP profile as 'Personality traits' - The skills are defined in the TRP profile as 'Personality traits'
- For each trait: - For each trait:
- the left field represents the skill name - 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: - the first numeric value (V2) determines the skill level based on these ranges:
- Novice: 1-5 - Novice: 1-5
- Adept: 6-10 - Adept: 6-10
- Expert: 11-19 - Expert: 11-19
- Master: 20 - Master: 20
- should a skill have a value of 0 or no value, it should be omitted from the list - should a skill have a value of 0 or no value, it should be omitted from the list
- The list should have a default selected value of "Unskilled" which corresponds to a -4 modifier
- In case no skills are found in the profile, or no profile is selected, a default list should be displayed - 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 - Base roll, Novice Skill, Adept Skill, Expert Skill, Master Skill

13
docs/3-announce.md Executable file
View file

@ -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

78
docs/4-redesign.md Executable file
View file

@ -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

132
docs/5-build_skills.md Executable file
View file

@ -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 (020)
- `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: 15, Adept: 610, Expert: 1119, 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

0
docs/Changelog.md Executable file
View file

BIN
docs/build_skills_tab_design.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

BIN
docs/roll_tab_design.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

17
package.sh Executable file
View file

@ -0,0 +1,17 @@
#!/bin/bash
ADDON_NAME="AltSystem"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
OUTPUT_DIR="$(mktemp -d)"
OUTPUT_FILE="$OUTPUT_DIR/$ADDON_NAME.zip"
STAGING_DIR="$(mktemp -d)"
mkdir -p "$STAGING_DIR/$ADDON_NAME"
rsync -a --exclude='.DS_Store' --exclude='__MACOSX' --exclude='.git' --exclude='docs' "$SCRIPT_DIR/" "$STAGING_DIR/$ADDON_NAME/"
cd "$STAGING_DIR" && \
zip -r "$OUTPUT_FILE" "$ADDON_NAME"
rm -rf "$STAGING_DIR"
echo "$OUTPUT_FILE"