From e2eb0545d0f0bfbadb27172883b1e96309ae030d Mon Sep 17 00:00:00 2001 From: Nighthawk42 <6307495+Nighthawk42@users.noreply.github.com> Date: Mon, 24 Aug 2026 15:42:04 -0400 Subject: [PATCH] feat: restore minimap launcher --- Bejeweled/Bejeweled_Mainline.toc | 1 + Bejeweled/Core/Init.lua | 36 +++++ Bejeweled/UI/MainWindow.lua | 4 + Bejeweled/UI/Minimap.lua | 244 +++++++++++++++++++++++++++++++ Bejeweled/UI/Options.lua | 7 +- README.md | 4 +- docs/api-baseline.md | 2 + docs/architecture.md | 3 +- tools/test-runtime.lua | 194 +++++++++++++++++++++++- tools/verify-runtime.ps1 | 5 +- 10 files changed, 493 insertions(+), 7 deletions(-) create mode 100644 Bejeweled/UI/Minimap.lua diff --git a/Bejeweled/Bejeweled_Mainline.toc b/Bejeweled/Bejeweled_Mainline.toc index 1f9e58b..05665e8 100644 --- a/Bejeweled/Bejeweled_Mainline.toc +++ b/Bejeweled/Bejeweled_Mainline.toc @@ -32,3 +32,4 @@ UI\About.lua UI\Legal.lua UI\MainWindow.lua UI\Compartment.lua +UI\Minimap.lua diff --git a/Bejeweled/Core/Init.lua b/Bejeweled/Core/Init.lua index 9417724..e6511c2 100644 --- a/Bejeweled/Core/Init.lua +++ b/Bejeweled/Core/Init.lua @@ -27,6 +27,7 @@ function addon:Initialize(accountData, profileData) assert(self.Legal, "Legal module is not loaded") assert(self.MainWindow, "MainWindow module is not loaded") assert(self.Compartment, "Compartment module is not loaded") + assert(self.MinimapButton, "MinimapButton module is not loaded") assert(self.Input, "Input module is not loaded") assert(self.Session, "Session module is not loaded") @@ -45,6 +46,7 @@ function addon:Initialize(accountData, profileData) self.legalFactory = self.Legal self.mainWindowFactory = self.MainWindow self.compartment = self.Compartment + self.minimapButtonFactory = self.MinimapButton self.inputFactory = self.Input self.sessionFactory = self.Session self.initialized = true @@ -55,9 +57,34 @@ function addon:Initialize(accountData, profileData) return self end +function addon:EnsureMinimapButton(options) + if self.minimapButton then + return self.minimapButton + end + options = options or {} + local minimap = options.minimap or Minimap + local uiParent = options.uiParent or UIParent + if not minimap or not uiParent then + return nil + end + self.minimapButton = self.MinimapButton:New({ + minimap = minimap, + uiParent = uiParent, + settings = self.profileData.settings, + createFrame = options.createFrame, + tooltip = options.minimapTooltip, + cursorPosition = options.cursorPosition, + runtimeProvider = function() + return self.runtime + end, + }) + return self.minimapButton +end + function addon:StartRuntime(options) assert(self.initialized, "addon must be initialized before starting the runtime") if self.runtime then + self:EnsureMinimapButton(options) return self.runtime end options = options or {} @@ -79,7 +106,16 @@ function addon:StartRuntime(options) hintsEnabled = options.hintsEnabled, flightOptionProvider = options.flightOptionProvider, onFlightTimedRequested = options.onFlightTimedRequested, + onSettingsChanged = function(key, value, window) + if key == "hideMinimap" and self.minimapButton then + self.minimapButton:RefreshVisibility() + end + if options.onSettingsChanged then + return options.onSettingsChanged(key, value, window) + end + end, }) + self:EnsureMinimapButton(options) self.runtime:Show() return self.runtime end diff --git a/Bejeweled/UI/MainWindow.lua b/Bejeweled/UI/MainWindow.lua index d91b356..f2ec9c2 100644 --- a/Bejeweled/UI/MainWindow.lua +++ b/Bejeweled/UI/MainWindow.lua @@ -367,6 +367,7 @@ function MainWindow:New(uiParent, options) onSessionStopped = ValidateCallback(options.onSessionStopped, "onSessionStopped"), flightOptionProvider = ValidateCallback(options.flightOptionProvider, "flightOptionProvider"), onFlightTimedRequested = ValidateCallback(options.onFlightTimedRequested, "onFlightTimedRequested"), + onSettingsChanged = ValidateCallback(options.onSettingsChanged, "onSettingsChanged"), grid = options.grid or Grid:New(options.random), session = nil, activeOverlay = nil, @@ -457,6 +458,9 @@ function MainWindow:ApplySettings(key) if key == nil or key == "gameAlpha" then self.frame:SetAlpha(self.profile.settings.gameAlpha or 1) end + if self.onSettingsChanged then + self.onSettingsChanged(key, key and self.profile.settings[key] or nil, self) + end return key and self.profile.settings[key] or self.profile.settings end diff --git a/Bejeweled/UI/Minimap.lua b/Bejeweled/UI/Minimap.lua new file mode 100644 index 0000000..7d84bfb --- /dev/null +++ b/Bejeweled/UI/Minimap.lua @@ -0,0 +1,244 @@ +local _, addon = ... + +local Constants = assert(addon.Constants, "Constants module is not loaded") + +local MinimapButton = {} +MinimapButton.__index = MinimapButton + +local BUTTON_SIZE = 33 +local ICON_SIZE = 26 +local ATTACHED_RADIUS = 105 +local TRACKING_BORDER = "Interface\\Minimap\\MiniMap-TrackingBorder" +local HIGHLIGHT_TEXTURE = "Interface\\Minimap\\UI-Minimap-ZoomButton-Highlight" + +local function Atan2(y, x) + if x > 0 then + return math.atan(y / x) + elseif x < 0 and y >= 0 then + return math.atan(y / x) + math.pi + elseif x < 0 then + return math.atan(y / x) - math.pi + elseif y > 0 then + return math.pi / 2 + elseif y < 0 then + return -math.pi / 2 + end + return 0 +end + +local function SetTextureSize(texture, width, height) + texture:SetWidth(width) + texture:SetHeight(height) +end + +function MinimapButton:New(options) + options = options or {} + assert(type(options) == "table", "minimap-button options must be a table") + assert(options.minimap ~= nil, "minimap button requires Minimap") + assert(options.uiParent ~= nil, "minimap button requires UIParent") + assert(type(options.settings) == "table", "minimap button requires profile settings") + assert(type(options.runtimeProvider) == "function", "minimap button requires a runtime provider") + + local instance = setmetatable({ + minimap = options.minimap, + uiParent = options.uiParent, + settings = options.settings, + runtimeProvider = options.runtimeProvider, + createFrame = options.createFrame or CreateFrame, + tooltip = options.tooltip, + cursorPosition = options.cursorPosition or GetCursorPosition, + }, self) + assert(type(instance.createFrame) == "function", "CreateFrame is unavailable for minimap button") + assert(type(instance.cursorPosition) == "function", "cursor position provider must be a function") + + local frame = instance.createFrame("Frame", "BejeweledMinimapIcon", instance.minimap) + frame:SetWidth(BUTTON_SIZE) + frame:SetHeight(BUTTON_SIZE) + if type(frame.SetFrameStrata) == "function" then + frame:SetFrameStrata("HIGH") + end + frame:EnableMouse(true) + if type(frame.SetClampedToScreen) == "function" then + frame:SetClampedToScreen(true) + end + + frame.icon = frame:CreateTexture(nil, "BACKGROUND") + SetTextureSize(frame.icon, ICON_SIZE, ICON_SIZE) + frame.icon:SetPoint("CENTER", frame, "CENTER", -1, 1) + frame.icon:SetTexture(Constants.IMAGE_ROOT .. "windowIcon") + + frame.border = frame:CreateTexture(nil, "ARTWORK") + SetTextureSize(frame.border, 52, 52) + frame.border:SetPoint("TOPLEFT", frame, "TOPLEFT", 0, 0) + frame.border:SetTexture(TRACKING_BORDER) + + frame.highlight = frame:CreateTexture(nil, "OVERLAY") + SetTextureSize(frame.highlight, 32, 32) + frame.highlight:SetPoint("CENTER", frame, "CENTER", 0, 0) + frame.highlight:SetTexture(HIGHLIGHT_TEXTURE) + frame.highlight:SetBlendMode("ADD") + frame.highlight:Hide() + + frame:SetScript("OnMouseDown", function(button, mouseButton) + button.icon:ClearAllPoints() + button.icon:SetPoint("CENTER", button, "CENTER", 0, 0) + if mouseButton == "RightButton" then + button.moving = true + end + end) + frame:SetScript("OnMouseUp", function(button, mouseButton) + button.icon:ClearAllPoints() + button.icon:SetPoint("CENTER", button, "CENTER", -1, 1) + button.moving = nil + if mouseButton == "LeftButton" then + instance:ToggleRuntime() + end + end) + frame:SetScript("OnEnter", function(button) + instance:OnEnter(button) + end) + frame:SetScript("OnLeave", function(button) + instance:OnLeave(button) + end) + frame:SetScript("OnUpdate", function(button) + if button.moving then + instance:UpdateDrag() + end + end) + + instance.frame = frame + instance:RefreshPosition() + instance:RefreshVisibility() + return instance +end + +function MinimapButton:GetTooltip() + return self.tooltip or GameTooltip +end + +function MinimapButton:GetUIScale() + if type(self.uiParent.GetEffectiveScale) == "function" then + return self.uiParent:GetEffectiveScale() + end + if type(self.uiParent.GetScale) == "function" then + return self.uiParent:GetScale() + end + return 1 +end + +function MinimapButton:SetAttachedPosition(angle) + angle = tonumber(angle) or 0 + self.frame:ClearAllPoints() + self.frame:SetPoint( + "CENTER", + self.minimap, + "CENTER", + -(ATTACHED_RADIUS * math.cos(math.rad(angle))), + ATTACHED_RADIUS * math.sin(math.rad(angle)) + ) + return angle +end + +function MinimapButton:SetDetachedPosition(x, y) + x = assert(tonumber(x), "detached minimap X position must be numeric") + y = assert(tonumber(y), "detached minimap Y position must be numeric") + self.frame:ClearAllPoints() + self.frame:SetPoint("CENTER", self.uiParent, "BOTTOMLEFT", x, y) + return x, y +end + +function MinimapButton:RefreshPosition() + local settings = self.settings + local x = tonumber(settings.minimapX) + local y = tonumber(settings.minimapY) + if settings.minimapDetached and x and y then + return self:SetDetachedPosition(x, y) + end + settings.minimapDetached = nil + return self:SetAttachedPosition(settings.minimapAngle) +end + +function MinimapButton:RefreshVisibility() + if self.settings.hideMinimap then + self.frame:Hide() + return false + end + self.frame:Show() + return true +end + +function MinimapButton:SetHidden(hidden) + self.settings.hideMinimap = hidden and 1 or nil + return self:RefreshVisibility() +end + +function MinimapButton:ToggleRuntime() + local runtime = self.runtimeProvider() + if not runtime or type(runtime.Toggle) ~= "function" or type(runtime.IsShown) ~= "function" then + return nil + end + runtime:Toggle() + return runtime:IsShown() +end + +function MinimapButton:OnEnter(frame) + frame.highlight:Show() + local tooltip = self:GetTooltip() + if not tooltip or type(tooltip.SetOwner) ~= "function" or type(tooltip.SetText) ~= "function" + or type(tooltip.AddLine) ~= "function" or type(tooltip.Show) ~= "function" then + return false + end + tooltip:SetOwner(frame, "ANCHOR_LEFT") + tooltip:SetText("Bejeweled") + tooltip:AddLine("Left-click to show or hide the game.", 1, 1, 1) + tooltip:AddLine("Right-drag to move the icon.", 0.75, 0.75, 0.75) + tooltip:Show() + return true +end + +function MinimapButton:OnLeave(frame) + frame.highlight:Hide() + local tooltip = self:GetTooltip() + if tooltip and type(tooltip.Hide) == "function" then + tooltip:Hide() + end + return true +end + +function MinimapButton:UpdateDrag() + local cursorX, cursorY = self.cursorPosition() + assert(type(cursorX) == "number" and type(cursorY) == "number", "cursor position must be numeric") + local scale = self:GetUIScale() + assert(type(scale) == "number" and scale > 0, "UI scale must be positive") + cursorX = cursorX / scale + cursorY = cursorY / scale + + local minimapLeft = assert(self.minimap:GetLeft(), "Minimap left position is unavailable") + local minimapBottom = assert(self.minimap:GetBottom(), "Minimap bottom position is unavailable") + local minimapWidth = assert(self.minimap:GetWidth(), "Minimap width is unavailable") + local minimapHeight = assert(self.minimap:GetHeight(), "Minimap height is unavailable") + local centerX = minimapLeft + minimapWidth / 2 + local centerY = minimapBottom + minimapHeight / 2 + local offsetX = cursorX - centerX + local offsetY = cursorY - centerY + + if math.sqrt(offsetX * offsetX + offsetY * offsetY) > minimapWidth then + self.settings.minimapDetached = true + self.settings.minimapX = cursorX + self.settings.minimapY = cursorY + self:SetDetachedPosition(cursorX, cursorY) + return "detached", cursorX, cursorY + end + + local angle = math.deg(Atan2(offsetY, -offsetX)) + self.settings.minimapAngle = angle + self.settings.minimapDetached = nil + self:SetAttachedPosition(angle) + return "attached", angle +end + +MinimapButton.BUTTON_SIZE = BUTTON_SIZE +MinimapButton.ICON_SIZE = ICON_SIZE +MinimapButton.ATTACHED_RADIUS = ATTACHED_RADIUS + +addon.MinimapButton = MinimapButton diff --git a/Bejeweled/UI/Options.lua b/Bejeweled/UI/Options.lua index 1479b7a..d2fec43 100644 --- a/Bejeweled/UI/Options.lua +++ b/Bejeweled/UI/Options.lua @@ -15,6 +15,7 @@ local DEFINITIONS = { { key = "soundMode", label = "Sound", kind = "sound" }, { key = "disableHints", label = "Disable Hints", kind = "toggle" }, { key = "lockWindow", label = "Lock Window", kind = "toggle" }, + { key = "hideMinimap", label = "Hide Minimap Icon", kind = "toggle" }, { key = "publishSkillGains", label = "Chat Skill Gains", kind = "toggle" }, { key = "publishRankGains", label = "Guild Rank Gains", kind = "toggle" }, { key = "publishScores", label = "Publish Scores", kind = "toggle" }, @@ -136,7 +137,11 @@ function Options:Activate(definition) elseif definition.kind == "sound" then self:CycleSound() else - self.settings[definition.key] = self.settings[definition.key] and nil or 1 + if self.settings[definition.key] then + self.settings[definition.key] = nil + else + self.settings[definition.key] = 1 + end end self.onChanged(definition.key, self.settings[definition.key], self) self:Refresh() diff --git a/README.md b/README.md index 946e2e0..78fe6d5 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ This branch contains the analysis-complete, Mainline-first modernization of the The preserved 8,401-line Mainline source must be analyzed sequentially, in evidence-backed batches, before runtime work begins. All behavior-critical shortened symbols must be resolved and all batches must be complete before any public Lua API, runtime module, modern TOC, packaging, or release work is added. -All 17 batches (lines 1–8,401) are documented, the cross-batch identifier audit has no remaining `working` or `unresolved` declarations, and the retained Retail API contracts are pinned in `docs/api-baseline.md`. The analysis phase gate is closed. Runtime implementation now includes wire-compatible SavedVariables initialization, authenticated Classic restoration and personal-best persistence, deterministic grid/match/cascade/scoring transitions, pause/resume and stable-state autosave coordination, session-owned level/game-over handoffs, no-move and timed-expiry detection, click selection, validated swap/rollback sessions, skills, levels, legacy-compatible audio cue scheduling, persistent board rendering and effects, the Classic/Timed HUD, complete local summary/Feats/settings/about/legal presentation, and an automatically assembled movable window with one authoritative runtime update path. +All 17 batches (lines 1–8,401) are documented, the cross-batch identifier audit has no remaining `working` or `unresolved` declarations, and the retained Retail API contracts are pinned in `docs/api-baseline.md`. The analysis phase gate is closed. Runtime implementation now includes wire-compatible SavedVariables initialization, authenticated Classic restoration and personal-best persistence, deterministic grid/match/cascade/scoring transitions, pause/resume and stable-state autosave coordination, session-owned level/game-over handoffs, no-move and timed-expiry detection, click selection, validated swap/rollback sessions, skills, levels, legacy-compatible audio cue scheduling, persistent board rendering and effects, the Classic/Timed HUD, complete local summary/Feats/settings/about/legal presentation, minimap and addon-compartment access, and an automatically assembled movable window with one authoritative runtime update path. ## Goal @@ -19,7 +19,7 @@ The eventual addon will target current Retail/Mainline World of Warcraft while p - `Bejeweled/sounds/` — immutable legacy sounds. - `Bejeweled/Core/` — private addon initialization, constants, supported audio playback, non-destructive SavedVariables defaulting, legacy-authenticated Classic save encoding, terminal save clearing, account game counts, and authenticated personal bests. - `Bejeweled/Engine/` — deterministic gameplay state; currently the 8×8 grid, selection/swap and pause/resume sessions, authenticated restoration, session-locked level/game-over transitions, legal moves, legacy cell encoding, stable cascade resolution, legacy score formulas, move accounting, final summary metrics, statistics, skill gains, achievements, and level thresholds. -- `Bejeweled/UI/` — Retail-safe frame/rendering boundaries; currently bundled-to-standard font fallback, shared backdrop construction, board tiles, persistent gem-frame projection, recorded cascade-transition playback, pooled legacy-cadence board effects, the session-bound HUD, full local summary and Feats/statistics/leaderboard presentation, settings, About/legal screens, the playable main-window/session shell with Timed setup, and addon-compartment access that can restore a closed window. +- `Bejeweled/UI/` — Retail-safe frame/rendering boundaries; currently bundled-to-standard font fallback, shared backdrop construction, board tiles, persistent gem-frame projection, recorded cascade-transition playback, pooled legacy-cadence board effects, the session-bound HUD, full local summary and Feats/statistics/leaderboard presentation, settings, About/legal screens, the playable main-window/session shell with Timed setup, and minimap/addon-compartment access that can restore a closed window. - `docs/analysis/` — sequential batch reports, identifier ledger, and exact coverage schedule. - `docs/architecture.md` — module ownership and authoritative runtime data flow. - `docs/api-baseline.md` — verified Retail API constraints for implementation. diff --git a/docs/api-baseline.md b/docs/api-baseline.md index 0d83450..921193e 100644 --- a/docs/api-baseline.md +++ b/docs/api-baseline.md @@ -16,6 +16,7 @@ The modernization baseline is the authoritative WoW UI/API source snapshot for l | Main window | `SimpleFrame` exposes `RegisterForDrag`, `SetMovable`, protected `StartMoving`, and protected `StopMovingOrSizing` in all environments. `UnitName(unit)` accepts a non-nil unit token and returns non-nil name/server strings, subject to the documented identity restriction. | `UI/MainWindow.lua` begins movement only from its direct left-button drag callback when the profile is unlocked. `Core/Init.lua` supplies `UnitName("player")` lazily as the existing SavedVariables authentication identity. | | Line rendering | `SimpleFrame:CreateLine` returns `SimpleLine`; its current all-environment contract exposes `ClearAllPoints`, `SetStartPoint`, `SetEndPoint`, and `SetThickness`. Line regions inherit texture-region operations used here for addon texture/color, additive blending, alpha, and visibility. | `UI/Animations.lua` replaces the removed legacy `DrawRouteLine` call with pooled, board-relative lightning lines and preserves the 15-tick alternating-highlight lifetime. | | Addon compartment | TOC metadata names global callbacks. In the pinned Mainline implementation, click invokes the global with `(addonName, buttonName)`; hover enter/leave invoke their globals with `(addonName, menuButtonFrame)`. Current Blizzard code uses `GameTooltip:SetOwner`, `SetText`, `AddLine`, `Show`, and `Hide` for frame hover help. | `UI/Compartment.lua` registers the three distinct callbacks, toggles only on a left click, and anchors concise hover help to the supplied menu button. | +| Minimap launcher | Current frames expose the positioning, sizing, scale, mouse-script, and clamping operations used by the preserved launcher. Pinned Mainline code continues to normalize `GetCursorPosition()` by effective UI scale before positioning a frame. | `UI/Minimap.lua` restores the 33-pixel local launcher, left-click window toggle, tooltip, right-drag attached/detached placement, and the existing `hideMinimap`, `minimapAngle`, `minimapDetached`, `minimapX`, and `minimapY` profile fields. | | Addon lifecycle | `ADDON_LOADED` is synchronous and supplies `addOnName` followed by `containsBindings`. Frames expose `RegisterEvent(eventName)` and `UnregisterEvent(eventName)` in all environments. | `Core/Init.lua` filters the event by addon name, unregisters its one-shot listener, and performs idempotent initialization after every TOC module is loaded. | | Friends | `C_FriendList.GetFriendInfoByIndex(index)` may return nothing; otherwise it returns one `FriendInfo` table with `name` and Boolean `connected` fields. | Replace all legacy positional `GetFriendInfo(index)` reads with a nil-checked table read and whisper only when `info.connected`. | | Battlefield queues | Current Blizzard Mainline code still reads `local status = GetBattlefieldStatus(index)` and compares the first return with queue states. | Keep the queue scan local and preserve the legacy `status == "queued"` behavior. | @@ -38,6 +39,7 @@ API existence alone does not prove behavioral equivalence. Each future substitut - Animation-group lifecycle, translation, and alpha contracts: [`SimpleAnimGroupAPIDocumentation.lua`](https://github.com/Gethe/wow-ui-source/blob/81d15e42f16f3473131880500e7a8c8eb88fa5e6/Interface/AddOns/Blizzard_APIDocumentationGenerated/SimpleAnimGroupAPIDocumentation.lua), [`SimpleAnimTranslationAPIDocumentation.lua`](https://github.com/Gethe/wow-ui-source/blob/81d15e42f16f3473131880500e7a8c8eb88fa5e6/Interface/AddOns/Blizzard_APIDocumentationGenerated/SimpleAnimTranslationAPIDocumentation.lua), and [`SimpleAnimAlphaAPIDocumentation.lua`](https://github.com/Gethe/wow-ui-source/blob/81d15e42f16f3473131880500e7a8c8eb88fa5e6/Interface/AddOns/Blizzard_APIDocumentationGenerated/SimpleAnimAlphaAPIDocumentation.lua) - Animation ordering: [`SimpleAnimAPIDocumentation.lua`](https://github.com/Gethe/wow-ui-source/blob/81d15e42f16f3473131880500e7a8c8eb88fa5e6/Interface/AddOns/Blizzard_APIDocumentationGenerated/SimpleAnimAPIDocumentation.lua) - Addon-compartment metadata dispatch and current callback signatures: [`AddonCompartment.lua`](https://github.com/Gethe/wow-ui-source/blob/81d15e42f16f3473131880500e7a8c8eb88fa5e6/Interface/AddOns/Blizzard_Minimap/Mainline/AddonCompartment.lua) +- Minimap-button frame operations and effective-scale cursor positioning: [`SimpleFrameAPIDocumentation.lua`](https://github.com/Gethe/wow-ui-source/blob/81d15e42f16f3473131880500e7a8c8eb88fa5e6/Interface/AddOns/Blizzard_APIDocumentationGenerated/SimpleFrameAPIDocumentation.lua) and [`LootFrame.lua`](https://github.com/Gethe/wow-ui-source/blob/81d15e42f16f3473131880500e7a8c8eb88fa5e6/Interface/AddOns/Blizzard_UIPanels_Game/Mainline/LootFrame.lua) - Friend contract: [`FriendListDocumentation.lua`](https://github.com/Gethe/wow-ui-source/blob/81d15e42f16f3473131880500e7a8c8eb88fa5e6/Interface/AddOns/Blizzard_APIDocumentationGenerated/FriendListDocumentation.lua) - Combat-log event/access environments: [`CombatLogDocumentation.lua`](https://github.com/Gethe/wow-ui-source/blob/81d15e42f16f3473131880500e7a8c8eb88fa5e6/Interface/AddOns/Blizzard_APIDocumentationGenerated/CombatLogDocumentation.lua), [`CombatLogInternalDocumentation.lua`](https://github.com/Gethe/wow-ui-source/blob/81d15e42f16f3473131880500e7a8c8eb88fa5e6/Interface/AddOns/Blizzard_APIDocumentationGenerated/CombatLogInternalDocumentation.lua), and [`CombatLogSecureDocumentation.lua`](https://github.com/Gethe/wow-ui-source/blob/81d15e42f16f3473131880500e7a8c8eb88fa5e6/Interface/AddOns/Blizzard_APIDocumentationGenerated/CombatLogSecureDocumentation.lua) - Ready-check contract: [`PartyInfoDocumentation.lua`](https://github.com/Gethe/wow-ui-source/blob/81d15e42f16f3473131880500e7a8c8eb88fa5e6/Interface/AddOns/Blizzard_APIDocumentationGenerated/PartyInfoDocumentation.lua) diff --git a/docs/architecture.md b/docs/architecture.md index 43bab05..783fea3 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,6 +1,6 @@ # Target architecture (post-analysis) -The analysis phase gate is satisfied. This ownership map now governs runtime implementation; currently implemented modules are `Core/Init.lua`, `Core/Constants.lua`, `Core/Audio.lua`, `Core/SavedVariables.lua`, `Engine/Grid.lua`, `Engine/Matches.lua`, `Engine/Cascade.lua`, `Engine/Scoring.lua`, `Engine/Input.lua`, `Engine/Session.lua`, `UI/Fonts.lua`, `UI/Backdrops.lua`, `UI/GemPool.lua`, `UI/Animations.lua`, `UI/HUD.lua`, `UI/Summary.lua`, `UI/Skills.lua`, `UI/Options.lua`, `UI/About.lua`, `UI/Legal.lua`, `UI/MainWindow.lua`, and `UI/Compartment.lua`. +The analysis phase gate is satisfied. This ownership map now governs runtime implementation; currently implemented modules are `Core/Init.lua`, `Core/Constants.lua`, `Core/Audio.lua`, `Core/SavedVariables.lua`, `Engine/Grid.lua`, `Engine/Matches.lua`, `Engine/Cascade.lua`, `Engine/Scoring.lua`, `Engine/Input.lua`, `Engine/Session.lua`, `UI/Fonts.lua`, `UI/Backdrops.lua`, `UI/GemPool.lua`, `UI/Animations.lua`, `UI/HUD.lua`, `UI/Summary.lua`, `UI/Skills.lua`, `UI/Options.lua`, `UI/About.lua`, `UI/Legal.lua`, `UI/MainWindow.lua`, `UI/Compartment.lua`, and `UI/Minimap.lua`. ## Load order and ownership @@ -26,6 +26,7 @@ The analysis phase gate is satisfied. This ownership map now governs runtime imp 20. `UI/Legal.lua` — first-run and menu-accessible legal/attribution presentation. Explicit acknowledgement writes only the established account-wide `legalDisplayed` field before returning through an injected callback. 21. `UI/MainWindow.lua` — the 448×510 movable runtime shell, 400×400 board construction, complete local Menu/New Game/Feats/Settings/About/Legal navigation, Classic Continue/New Game, and a Timed setup screen with a 2–10 minute unit-step slider defaulting to five. It owns session replacement, full-summary handoff, live setting application, first-run legal routing, and show/hide pause behavior; its frame update delegates authoritative elapsed time to `Engine/Session.lua`, HUD expiry to `UI/HUD.lua`, and queued sound flushing to `Core/Audio.lua`. `Core/Init.lua` assembles and shows this shell after `ADDON_LOADED` when live `UIParent` and player identity are available. 22. `UI/Compartment.lua` — the minimal public-global boundary required by Retail TOC metadata. Its left-click callback acquires or reuses the private runtime and toggles `UI/MainWindow.lua`; its distinct hover callbacks own anchored `GameTooltip` help. Right clicks and callbacks for another addon name are ignored. +23. `UI/Minimap.lua` — the legacy local launcher boundary. It renders the preserved icon inside Blizzard minimap chrome, toggles `UI/MainWindow.lua` on left click, owns hover help, and converts right-drag cursor coordinates into persisted attached angles or detached UIParent coordinates using the established profile fields. `UI/Options.lua` changes visibility through the injected `UI/MainWindow.lua` settings callback; the launcher owns no gameplay state. Flight timing remains an explicit integration boundary rather than a third engine mode. `UI/MainWindow.lua` accepts paired `flightOptionProvider(window)` and `onFlightTimedRequested(state, window)` callbacks. The provider returns `nil` when inactive or a copied `{ seconds = nonnegativeNumber, learning = optionalBoolean, ... }` state. Known routes under 60 seconds are shown as too short; learning and longer known routes may be selected. Only the paired request callback can consume that state, so the later Retail event adapter can own taxi observation and session transition without reintroducing the legacy estimator's unreachable APIs. diff --git a/tools/test-runtime.lua b/tools/test-runtime.lua index 6e77c1d..b679c90 100644 --- a/tools/test-runtime.lua +++ b/tools/test-runtime.lua @@ -61,6 +61,7 @@ LoadAddonFile("Bejeweled/UI/About.lua", addon) LoadAddonFile("Bejeweled/UI/Legal.lua", addon) LoadAddonFile("Bejeweled/UI/MainWindow.lua", addon) LoadAddonFile("Bejeweled/UI/Compartment.lua", addon) +LoadAddonFile("Bejeweled/UI/Minimap.lua", addon) AssertEqual(eventFrame.registeredEvent, "ADDON_LOADED", "initializer event registration") AssertEqual(addon.Constants.GRID_WIDTH, 8, "grid width") @@ -152,6 +153,9 @@ local function CreateMockTexture(layer) self.points = self.points or {} self.points[#self.points + 1] = { ... } end + function texture:ClearAllPoints() + self.points = {} + end function texture:SetTexture(path) self.path = path return true @@ -375,6 +379,18 @@ local function CreateGemPoolFrame(frameType, name, parent, template) function frame:SetFrameLevel(frameLevel) self.frameLevel = frameLevel end + function frame:SetFrameStrata(frameStrata) + self.frameStrata = frameStrata + end + function frame:GetWidth() + return self.width + end + function frame:GetHeight() + return self.height + end + function frame:IsShown() + return self.shown and true or false + end function frame:GetFrameLevel() if self.frameLevel then return self.frameLevel @@ -419,6 +435,9 @@ local gemPoolParent = { GetFrameLevel = function() return 10 end, + GetEffectiveScale = function() + return 1 + end, } local enteredGem local gemPool = addon.GemPool:New(gemPoolParent, { @@ -1782,6 +1801,7 @@ function runtimeAudio:Update(elapsed) end local sessionStarts = {} local sessionStops = {} +local settingsChanges = {} local flightState local flightRequests = {} local runtime = addon.MainWindow:New(gemPoolParent, { @@ -1797,6 +1817,9 @@ local runtime = addon.MainWindow:New(gemPoolParent, { onSessionStopped = function(result) sessionStops[#sessionStops + 1] = result end, + onSettingsChanged = function(key, value) + settingsChanges[#settingsChanges + 1] = { key = key, value = value } + end, flightOptionProvider = function() return flightState end, @@ -1905,6 +1928,11 @@ AssertEqual(runtime.options:GetSoundMode(), "Quiet", "runtime sound-mode setting runtime.options.rows[4].scripts.OnClick() assert(runtimeProfile.settings.disableHints, "runtime hint setting") assert(not runtime.hud:AreHintsEnabled(), "runtime hint setting did not reach the HUD") +runtime.options.rows[6].scripts.OnClick() +assert(runtimeProfile.settings.hideMinimap, "runtime minimap visibility setting") +AssertEqual(settingsChanges[#settingsChanges].key, "hideMinimap", "runtime minimap setting callback") +runtime.options.rows[6].scripts.OnClick() +assert(not runtimeProfile.settings.hideMinimap, "runtime minimap visibility setting did not toggle off") runtime.options.backButton.scripts.OnClick() AssertEqual(runtime.activeOverlay, "menu", "runtime Settings Back action") runtimeProfile.settings.gameAlpha = 1 @@ -2095,12 +2123,125 @@ AssertEqual(#sessionStarts, 6, "runtime session-start callback count") AssertEqual(#sessionStops, 5, "runtime session-stop callback count") end +function addon:TestMinimapButtonForTest() + local minimap = { + left = 100, + bottom = 200, + width = 140, + height = 140, + } + function minimap:GetLeft() + return self.left + end + function minimap:GetBottom() + return self.bottom + end + function minimap:GetWidth() + return self.width + end + function minimap:GetHeight() + return self.height + end + local uiParent = { + GetEffectiveScale = function() + return 2 + end, + } + local tooltip = { lines = {} } + function tooltip:SetOwner(owner, anchor) + self.owner = owner + self.anchor = anchor + end + function tooltip:SetText(value) + self.text = value + end + function tooltip:AddLine(value) + self.lines[#self.lines + 1] = value + end + function tooltip:Show() + self.shown = true + end + function tooltip:Hide() + self.shown = false + end + local runtime = { shown = true } + function runtime:Toggle() + self.shown = not self.shown + end + function runtime:IsShown() + return self.shown + end + local cursorX = 340 + local cursorY = 680 + local settings = {} + local button = self.MinimapButton:New({ + minimap = minimap, + uiParent = uiParent, + settings = settings, + createFrame = CreateGemPoolFrame, + tooltip = tooltip, + cursorPosition = function() + return cursorX, cursorY + end, + runtimeProvider = function() + return runtime + end, + }) + AssertEqual(button.frame.width, self.MinimapButton.BUTTON_SIZE, "minimap button width") + AssertEqual(button.frame.height, self.MinimapButton.BUTTON_SIZE, "minimap button height") + AssertEqual(button.frame.icon.width, self.MinimapButton.ICON_SIZE, "minimap icon width") + AssertEqual(button.frame.icon.path, self.Constants.IMAGE_ROOT .. "windowIcon", "minimap icon texture") + assert(button.frame.shown, "default minimap button remained hidden") + AssertEqual(button.frame.points[1][2], minimap, "default minimap button parent anchor") + AssertEqual(button.frame.points[1][4], -self.MinimapButton.ATTACHED_RADIUS, "default minimap button radius") + + button.frame.scripts.OnMouseDown(button.frame, "LeftButton") + button.frame.scripts.OnMouseUp(button.frame, "LeftButton") + assert(not runtime.shown, "minimap left-click did not hide the runtime") + button.frame.scripts.OnMouseDown(button.frame, "LeftButton") + button.frame.scripts.OnMouseUp(button.frame, "LeftButton") + assert(runtime.shown, "minimap left-click did not restore the runtime") + + button.frame.scripts.OnEnter(button.frame) + assert(button.frame.highlight.shown, "minimap hover highlight remained hidden") + AssertEqual(tooltip.owner, button.frame, "minimap tooltip owner") + AssertEqual(tooltip.anchor, "ANCHOR_LEFT", "minimap tooltip anchor") + AssertEqual(tooltip.lines[1], "Left-click to show or hide the game.", "minimap tooltip click help") + button.frame.scripts.OnLeave(button.frame) + assert(not button.frame.highlight.shown and not tooltip.shown, "minimap tooltip or highlight remained visible") + + button.frame.scripts.OnMouseDown(button.frame, "RightButton") + button.frame.scripts.OnUpdate(button.frame) + button.frame.scripts.OnMouseUp(button.frame, "RightButton") + assert(not settings.minimapDetached, "near-minimap drag detached the button") + assert(math.abs(settings.minimapAngle - 90) < 0.001, "attached minimap angle was not persisted") + AssertEqual(button.frame.points[1][2], minimap, "attached minimap drag anchor") + + cursorX = 1000 + cursorY = 1000 + button.frame.scripts.OnMouseDown(button.frame, "RightButton") + button.frame.scripts.OnUpdate(button.frame) + button.frame.scripts.OnMouseUp(button.frame, "RightButton") + assert(settings.minimapDetached, "distant minimap drag did not detach the button") + AssertEqual(settings.minimapX, 500, "detached minimap X position") + AssertEqual(settings.minimapY, 500, "detached minimap Y position") + AssertEqual(button.frame.points[1][2], uiParent, "detached minimap parent anchor") + AssertEqual(button.frame.points[1][3], "BOTTOMLEFT", "detached minimap relative anchor") + + button:SetHidden(true) + assert(not button.frame.shown and settings.hideMinimap, "minimap visibility setting did not hide the button") + button:SetHidden(false) + assert(button.frame.shown and not settings.hideMinimap, "minimap visibility setting did not restore the button") +end + TestSessionRestore() TestGameOverTransitions() addon:TestHUDPresentationForTest() addon.TestHUDPresentationForTest = nil addon:TestMainWindowForTest() addon.TestMainWindowForTest = nil +addon:TestMinimapButtonForTest() +addon.TestMinimapButtonForTest = nil FillStablePattern(cascadeGrid) for x = 2, 5 do @@ -2466,6 +2607,7 @@ assert(addon.aboutFactory == addon.About, "addon initialization did not install assert(addon.legalFactory == addon.Legal, "addon initialization did not install Legal") assert(addon.mainWindowFactory == addon.MainWindow, "addon initialization did not install MainWindow") assert(addon.compartment == addon.Compartment, "addon initialization did not install Compartment") +assert(addon.minimapButtonFactory == addon.MinimapButton, "addon initialization did not install MinimapButton") assert(addon.inputFactory == addon.Input, "addon initialization did not install Input") assert(addon.sessionFactory == addon.Session, "addon initialization did not install Session") assert(eventFrame.registeredEvent == nil, "initializer event was not unregistered") @@ -2492,18 +2634,68 @@ assert(addon.aboutFactory == addon.About, "About initialization is not idempoten assert(addon.legalFactory == addon.Legal, "Legal initialization is not idempotent") assert(addon.mainWindowFactory == addon.MainWindow, "MainWindow initialization is not idempotent") assert(addon.compartment == addon.Compartment, "Compartment initialization is not idempotent") +assert(addon.minimapButtonFactory == addon.MinimapButton, "MinimapButton initialization is not idempotent") assert(addon.inputFactory == initializedInputFactory, "Input initialization is not idempotent") assert(addon.sessionFactory == initializedSessionFactory, "Session initialization is not idempotent") +addon.runtimeMinimapForTest = { + left = 100, + bottom = 200, + width = 140, + height = 140, +} +function addon.runtimeMinimapForTest:GetLeft() + return self.left +end +function addon.runtimeMinimapForTest:GetBottom() + return self.bottom +end +function addon.runtimeMinimapForTest:GetWidth() + return self.width +end +function addon.runtimeMinimapForTest:GetHeight() + return self.height +end +addon.runtimeMinimapTooltipForTest = { lines = {} } +function addon.runtimeMinimapTooltipForTest:SetOwner(owner, anchor) + self.owner = owner + self.anchor = anchor +end +function addon.runtimeMinimapTooltipForTest:SetText(value) + self.text = value +end +function addon.runtimeMinimapTooltipForTest:AddLine(value) + self.lines[#self.lines + 1] = value +end +function addon.runtimeMinimapTooltipForTest:Show() + self.shown = true +end +function addon.runtimeMinimapTooltipForTest:Hide() + self.shown = false +end addon.runtimeForTest = addon:StartRuntime({ uiParent = gemPoolParent, createFrame = CreateGemPoolFrame, playerName = "Nighthawk", random = MakeRandom(13001), + minimap = addon.runtimeMinimapForTest, + minimapTooltip = addon.runtimeMinimapTooltipForTest, + cursorPosition = function() + return 170, 270 + end, }) +addon.runtimeMinimapForTest = nil +addon.runtimeMinimapTooltipForTest = nil assert(addon.runtimeForTest == addon.runtime, "StartRuntime did not retain the playable shell") +assert(addon.minimapButton and addon.minimapButton.frame.shown, "StartRuntime did not create the minimap launcher") AssertEqual(addon.runtimeForTest.activeOverlay, "legal", "StartRuntime did not open the first-run legal notice") addon.runtimeForTest.legal.okayButton.scripts.OnClick() AssertEqual(addon.runtimeForTest.activeOverlay, "menu", "StartRuntime legal acknowledgement did not open the menu") +addon.runtimeForTest:ShowOptions() +addon.runtimeForTest.options.rows[6].scripts.OnClick() +assert(not addon.minimapButton.frame.shown, "live Hide Minimap setting did not hide the launcher") +addon.runtimeForTest.options.rows[6].scripts.OnClick() +assert(addon.minimapButton.frame.shown, "live Hide Minimap setting did not restore the launcher") +addon.runtimeForTest:ShowMenu() assert(addon:StartRuntime() == addon.runtimeForTest, "StartRuntime is not idempotent") function addon:TestCompartmentForTest() @@ -2577,4 +2769,4 @@ end addon:TestCompartmentForTest() addon.TestCompartmentForTest = nil -print("Runtime verification passed: local skill chat, aligned skill/footer presentation, full local summary, Timed setup/flight boundary, addon-compartment access, playable Classic/Timed window shell, HUD, pause/restore/level/game-over sessions, input, cascade/effect animation, gem projection, UI backdrops, audio, SavedVariables, and deterministic gameplay engine.") +print("Runtime verification passed: minimap/addon-compartment access, local skill chat, aligned skill/footer presentation, full local summary, Timed setup/flight boundary, playable Classic/Timed window shell, HUD, pause/restore/level/game-over sessions, input, cascade/effect animation, gem projection, UI backdrops, audio, SavedVariables, and deterministic gameplay engine.") diff --git a/tools/verify-runtime.ps1 b/tools/verify-runtime.ps1 index 8756d21..c7932a1 100644 --- a/tools/verify-runtime.ps1 +++ b/tools/verify-runtime.ps1 @@ -45,7 +45,8 @@ try { "UI\About.lua", "UI\Legal.lua", "UI\MainWindow.lua", - "UI\Compartment.lua" + "UI\Compartment.lua", + "UI\Minimap.lua" ) $actualFiles = @($toc | Where-Object { $_ -match "\.lua$" }) if (Compare-Object -ReferenceObject $expectedFiles -DifferenceObject $actualFiles -SyncWindow 0) { @@ -59,7 +60,7 @@ try { } } - Write-Output "Verified: Retail TOC order, complete local presentation screens, Timed setup/flight boundary, addon-compartment access, and Lua 5.1-compatible playable window/session runtime." + Write-Output "Verified: Retail TOC order, complete local presentation screens, Timed setup/flight boundary, minimap/addon-compartment access, and Lua 5.1-compatible playable window/session runtime." } finally { Pop-Location