mirror of
https://github.com/Nighthawk42/wow_bejeweled.git
synced 2026-08-30 04:30:22 +00:00
feat: assemble playable window session shell
This commit is contained in:
@@ -21,3 +21,4 @@ UI\Backdrops.lua
|
||||
UI\GemPool.lua
|
||||
UI\Animations.lua
|
||||
UI\HUD.lua
|
||||
UI\MainWindow.lua
|
||||
|
||||
@@ -19,6 +19,7 @@ function addon:Initialize(accountData, profileData)
|
||||
assert(self.GemPool, "GemPool module is not loaded")
|
||||
assert(self.Animations, "Animations module is not loaded")
|
||||
assert(self.HUD, "HUD module is not loaded")
|
||||
assert(self.MainWindow, "MainWindow module is not loaded")
|
||||
assert(self.Input, "Input module is not loaded")
|
||||
assert(self.Session, "Session module is not loaded")
|
||||
|
||||
@@ -29,13 +30,44 @@ function addon:Initialize(accountData, profileData)
|
||||
self.gemPoolFactory = self.GemPool
|
||||
self.animationFactory = self.Animations
|
||||
self.hudFactory = self.HUD
|
||||
self.mainWindowFactory = self.MainWindow
|
||||
self.inputFactory = self.Input
|
||||
self.sessionFactory = self.Session
|
||||
self.initialized = true
|
||||
if UIParent ~= nil and type(UnitName) == "function" and not self.runtime then
|
||||
self:StartRuntime()
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function addon:StartRuntime(options)
|
||||
assert(self.initialized, "addon must be initialized before starting the runtime")
|
||||
if self.runtime then
|
||||
return self.runtime
|
||||
end
|
||||
options = options or {}
|
||||
assert(type(options) == "table", "runtime options must be a table")
|
||||
local playerName = options.playerName or function()
|
||||
local name = UnitName("player")
|
||||
assert(type(name) == "string" and name ~= "", "player name is unavailable")
|
||||
return name
|
||||
end
|
||||
self.runtime = self.MainWindow:New(options.uiParent or UIParent, {
|
||||
createFrame = options.createFrame,
|
||||
profile = self.profileData,
|
||||
accountData = self.accountData,
|
||||
audio = self.audio,
|
||||
playerName = playerName,
|
||||
grid = self.grid,
|
||||
random = options.random,
|
||||
timedDuration = options.timedDuration,
|
||||
hintsEnabled = options.hintsEnabled,
|
||||
})
|
||||
self.runtime:Show()
|
||||
return self.runtime
|
||||
end
|
||||
|
||||
if type(CreateFrame) == "function" then
|
||||
local eventFrame = CreateFrame("Frame")
|
||||
eventFrame:RegisterEvent("ADDON_LOADED")
|
||||
|
||||
@@ -596,4 +596,27 @@ function Session:CreateGemHandlers()
|
||||
return self.input:CreateGemHandlers()
|
||||
end
|
||||
|
||||
function Session:Deactivate(reason)
|
||||
reason = reason or "session-replaced"
|
||||
assert(type(reason) == "string" and reason ~= "", "session deactivation reason is required")
|
||||
local changed = self.active or self.input.pendingMove ~= nil or self.levelTransition ~= nil
|
||||
self.active = false
|
||||
self.pendingGameOverCause = nil
|
||||
self.levelTransition = nil
|
||||
self.gameOverTransition = nil
|
||||
self.input:SetSessionLocked(true, reason)
|
||||
self.animations:Cancel(reason)
|
||||
if type(self.animations.ClearTransientEffects) == "function" then
|
||||
self.animations:ClearTransientEffects()
|
||||
end
|
||||
if type(self.animations.SetAmbientLightwaves) == "function" then
|
||||
self.animations:SetAmbientLightwaves(false)
|
||||
end
|
||||
return {
|
||||
status = "inactive",
|
||||
changed = changed and true or false,
|
||||
reason = reason,
|
||||
}
|
||||
end
|
||||
|
||||
addon.Session = Session
|
||||
|
||||
@@ -0,0 +1,462 @@
|
||||
local _, addon = ...
|
||||
|
||||
local Constants = assert(addon.Constants, "Constants module is not loaded")
|
||||
local SavedVariables = assert(addon.SavedVariables, "SavedVariables module is not loaded")
|
||||
local Grid = assert(addon.Grid, "Grid module is not loaded")
|
||||
local Backdrops = assert(addon.Backdrops, "Backdrops module is not loaded")
|
||||
local GemPool = assert(addon.GemPool, "GemPool module is not loaded")
|
||||
local Animations = assert(addon.Animations, "Animations module is not loaded")
|
||||
local HUD = assert(addon.HUD, "HUD module is not loaded")
|
||||
|
||||
local MainWindow = {}
|
||||
MainWindow.__index = MainWindow
|
||||
|
||||
local WINDOW_WIDTH = 448
|
||||
local WINDOW_HEIGHT = 510
|
||||
local BOARD_BORDER_WIDTH = 414
|
||||
local BOARD_BORDER_HEIGHT = 412
|
||||
local BOARD_WIDTH = Constants.GRID_WIDTH * Constants.GEM_WIDTH
|
||||
local BOARD_HEIGHT = Constants.GRID_HEIGHT * Constants.GEM_HEIGHT
|
||||
local DEFAULT_TIMED_DURATION = 5 * 60
|
||||
local FONT_PATH = Constants.IMAGE_ROOT .. "Contb___.ttf"
|
||||
|
||||
local function SetFrameSize(frame, width, height)
|
||||
frame:SetWidth(width)
|
||||
frame:SetHeight(height)
|
||||
end
|
||||
|
||||
local function SetTextureSize(texture, width, height)
|
||||
texture:SetWidth(width)
|
||||
texture:SetHeight(height)
|
||||
end
|
||||
|
||||
local function CreateFontString(frame, size, text, color)
|
||||
local fontString = frame:CreateFontString(nil, "OVERLAY")
|
||||
assert(fontString:SetFont(FONT_PATH, size, "OUTLINE"), "bundled window font could not be loaded")
|
||||
fontString:SetText(text or "")
|
||||
fontString:SetTextColor(color[1], color[2], color[3], color[4] or 1)
|
||||
return fontString
|
||||
end
|
||||
|
||||
local function ValidateCallback(callback, name)
|
||||
assert(callback == nil or type(callback) == "function", name .. " must be a function")
|
||||
return callback
|
||||
end
|
||||
|
||||
function MainWindow:CreateBackdropFrame(parent, preset, width, height, levelOffset, frameType)
|
||||
local frame = Backdrops:CreateFrame({
|
||||
frameType = frameType,
|
||||
parent = parent,
|
||||
preset = preset,
|
||||
createFrame = self.createFrame,
|
||||
backgroundColor = { 0.08, 0.08, 0.08, 0.96 },
|
||||
borderColor = { 1, 0.8, 0.45, 1 },
|
||||
})
|
||||
SetFrameSize(frame, width, height)
|
||||
if parent and type(parent.GetFrameLevel) == "function" then
|
||||
frame:SetFrameLevel(parent:GetFrameLevel() + (levelOffset or 1))
|
||||
end
|
||||
return frame
|
||||
end
|
||||
|
||||
function MainWindow:CreateButton(parent, text, width, height, onClick)
|
||||
assert(type(onClick) == "function", "window button requires a click callback")
|
||||
local button = self:CreateBackdropFrame(parent, "tooltip", width, height, 2, "Button")
|
||||
button:EnableMouse(true)
|
||||
button.label = CreateFontString(button, 13, text, { 1, 0.85, 0, 1 })
|
||||
button.label:SetPoint("CENTER", button, "CENTER", 0, 1)
|
||||
button:SetScript("OnClick", function()
|
||||
onClick()
|
||||
end)
|
||||
button:SetScript("OnEnter", function(frame)
|
||||
frame:SetBackdropColor(0.25, 0.18, 0.04, 1)
|
||||
end)
|
||||
button:SetScript("OnLeave", function(frame)
|
||||
frame:SetBackdropColor(0.08, 0.08, 0.08, 0.96)
|
||||
end)
|
||||
return button
|
||||
end
|
||||
|
||||
function MainWindow:CreateWindowFrame()
|
||||
local frame = self:CreateBackdropFrame(self.uiParent, "window", WINDOW_WIDTH, WINDOW_HEIGHT, 5)
|
||||
frame:SetPoint("CENTER", self.uiParent, "CENTER", 0, 0)
|
||||
frame:EnableMouse(true)
|
||||
frame:SetMovable(true)
|
||||
frame:RegisterForDrag("LeftButton")
|
||||
if type(frame.SetClampedToScreen) == "function" then
|
||||
frame:SetClampedToScreen(true)
|
||||
end
|
||||
frame:SetAlpha(self.profile.settings.gameAlpha or 1)
|
||||
frame:SetScript("OnDragStart", function(window)
|
||||
if not self.profile.settings.lockWindow then
|
||||
window:StartMoving()
|
||||
end
|
||||
end)
|
||||
frame:SetScript("OnDragStop", function(window)
|
||||
window:StopMovingOrSizing()
|
||||
end)
|
||||
frame:SetScript("OnShow", function()
|
||||
if not self.suppressWindowScript then
|
||||
self:HandleWindowShown()
|
||||
end
|
||||
end)
|
||||
frame:SetScript("OnHide", function()
|
||||
if not self.suppressWindowScript then
|
||||
self:HandleWindowHidden()
|
||||
end
|
||||
end)
|
||||
frame:SetScript("OnUpdate", function(_, elapsed)
|
||||
self:Update(elapsed)
|
||||
end)
|
||||
frame:Hide()
|
||||
|
||||
frame.icon = frame:CreateTexture(nil, "ARTWORK")
|
||||
frame.icon:SetTexture(Constants.IMAGE_ROOT .. "windowIcon")
|
||||
frame.icon:SetPoint("TOPLEFT", frame, "TOPLEFT", 6, 4)
|
||||
SetTextureSize(frame.icon, 58, 58)
|
||||
frame.title = CreateFontString(frame, 24, "Bejeweled", { 1, 0.85, 0, 1 })
|
||||
frame.title:SetPoint("TOP", frame, "TOP", 0, -18)
|
||||
|
||||
frame.closeButton = self:CreateButton(frame, "X", 28, 26, function()
|
||||
self:Hide()
|
||||
end)
|
||||
frame.closeButton:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -8, -8)
|
||||
frame.menuButton = self:CreateButton(frame, "Menu", 56, 26, function()
|
||||
if self.activeOverlay then
|
||||
self:ResumeGame()
|
||||
else
|
||||
self:ShowMenu()
|
||||
end
|
||||
end)
|
||||
frame.menuButton:SetPoint("TOPRIGHT", frame.closeButton, "TOPLEFT", -4, 0)
|
||||
self.frame = frame
|
||||
end
|
||||
|
||||
function MainWindow:CreateBoard()
|
||||
local boardFrame = self:CreateBackdropFrame(self.frame, "panel", BOARD_BORDER_WIDTH, BOARD_BORDER_HEIGHT, 2)
|
||||
boardFrame:SetPoint("TOPLEFT", self.frame, "TOPLEFT", 17, -60)
|
||||
boardFrame:SetBackdropColor(0, 0, 0, 0)
|
||||
local surface = self.createFrame("Frame", nil, boardFrame)
|
||||
SetFrameSize(surface, BOARD_WIDTH, BOARD_HEIGHT)
|
||||
surface:SetPoint("TOPLEFT", boardFrame, "TOPLEFT", 7, -4)
|
||||
if boardFrame and type(boardFrame.GetFrameLevel) == "function" then
|
||||
surface:SetFrameLevel(boardFrame:GetFrameLevel() + 1)
|
||||
end
|
||||
surface:EnableMouse(true)
|
||||
surface:Show()
|
||||
|
||||
self.boardFrame = boardFrame
|
||||
self.boardSurface = surface
|
||||
self.gemPool = GemPool:New(surface, {
|
||||
createFrame = self.createFrame,
|
||||
})
|
||||
self.gemPool:Project(self.grid, true)
|
||||
self.animations = Animations:New(self.gemPool, {
|
||||
createFrame = self.createFrame,
|
||||
random = self.random,
|
||||
})
|
||||
self.hud = HUD:New(surface, self.animations, {
|
||||
createFrame = self.createFrame,
|
||||
width = BOARD_WIDTH,
|
||||
hintsEnabled = self.hintsEnabled,
|
||||
})
|
||||
end
|
||||
|
||||
function MainWindow:CreateOverlay(title, height)
|
||||
local overlay = self:CreateBackdropFrame(self.frame, "panel", 190, height, 35)
|
||||
overlay:SetPoint("CENTER", self.frame, "CENTER", 0, 8)
|
||||
overlay.title = CreateFontString(overlay, 17, title, { 1, 1, 1, 1 })
|
||||
overlay.title:SetPoint("TOP", overlay, "TOP", 0, -10)
|
||||
overlay:Hide()
|
||||
return overlay
|
||||
end
|
||||
|
||||
function MainWindow:CreateMenus()
|
||||
local menu = self:CreateOverlay("Menu", 128)
|
||||
menu.resume = self:CreateButton(menu, "Resume", 160, 28, function()
|
||||
self:ResumeGame()
|
||||
end)
|
||||
menu.resume:SetPoint("TOP", menu, "TOP", 0, -36)
|
||||
menu.newGame = self:CreateButton(menu, "New Game", 160, 28, function()
|
||||
self:ShowModeMenu()
|
||||
end)
|
||||
menu.newGame:SetPoint("TOP", menu.resume, "BOTTOM", 0, -8)
|
||||
|
||||
local mode = self:CreateOverlay("Game Type", 164)
|
||||
mode.classic = self:CreateButton(mode, "Classic", 160, 28, function()
|
||||
self:ChooseClassic()
|
||||
end)
|
||||
mode.classic:SetPoint("TOP", mode, "TOP", 0, -36)
|
||||
mode.timed = self:CreateButton(mode, "Timed (5 minutes)", 160, 28, function()
|
||||
self:StartTimed()
|
||||
end)
|
||||
mode.timed:SetPoint("TOP", mode.classic, "BOTTOM", 0, -8)
|
||||
mode.back = self:CreateButton(mode, "Back", 160, 28, function()
|
||||
self:ShowMenu()
|
||||
end)
|
||||
mode.back:SetPoint("TOP", mode.timed, "BOTTOM", 0, -8)
|
||||
|
||||
local classic = self:CreateOverlay("Classic Mode", 164)
|
||||
classic.continue = self:CreateButton(classic, "Continue", 160, 28, function()
|
||||
self:StartClassic(true)
|
||||
end)
|
||||
classic.continue:SetPoint("TOP", classic, "TOP", 0, -36)
|
||||
classic.newGame = self:CreateButton(classic, "New Game", 160, 28, function()
|
||||
self:StartClassic(false)
|
||||
end)
|
||||
classic.newGame:SetPoint("TOP", classic.continue, "BOTTOM", 0, -8)
|
||||
classic.back = self:CreateButton(classic, "Back", 160, 28, function()
|
||||
self:ShowModeMenu()
|
||||
end)
|
||||
classic.back:SetPoint("TOP", classic.newGame, "BOTTOM", 0, -8)
|
||||
|
||||
self.overlays = {
|
||||
menu = menu,
|
||||
mode = mode,
|
||||
classic = classic,
|
||||
}
|
||||
end
|
||||
|
||||
function MainWindow:New(uiParent, options)
|
||||
assert(uiParent ~= nil, "main window requires UIParent")
|
||||
options = options or {}
|
||||
assert(type(options) == "table", "main-window options must be a table")
|
||||
assert(type(options.profile) == "table", "main window requires a profile")
|
||||
assert(type(options.accountData) == "table", "main window requires account data")
|
||||
assert(
|
||||
type(options.audio) == "table"
|
||||
and type(options.audio.Play) == "function"
|
||||
and type(options.audio.Update) == "function",
|
||||
"main window requires audio"
|
||||
)
|
||||
assert(type(options.playerName) == "string" or type(options.playerName) == "function", "main window requires a player name")
|
||||
local instance = setmetatable({
|
||||
uiParent = uiParent,
|
||||
createFrame = options.createFrame or CreateFrame,
|
||||
profile = options.profile,
|
||||
accountData = options.accountData,
|
||||
audio = options.audio,
|
||||
playerName = options.playerName,
|
||||
random = options.random or math.random,
|
||||
timedDuration = options.timedDuration or DEFAULT_TIMED_DURATION,
|
||||
hintsEnabled = options.hintsEnabled,
|
||||
onSessionStarted = ValidateCallback(options.onSessionStarted, "onSessionStarted"),
|
||||
onSessionStopped = ValidateCallback(options.onSessionStopped, "onSessionStopped"),
|
||||
grid = options.grid or Grid:New(options.random),
|
||||
session = nil,
|
||||
activeOverlay = nil,
|
||||
menuOwnsPause = false,
|
||||
windowOwnsPause = false,
|
||||
suppressWindowScript = false,
|
||||
visible = false,
|
||||
}, self)
|
||||
assert(type(instance.createFrame) == "function", "CreateFrame is unavailable for main window")
|
||||
assert(type(instance.random) == "function", "main-window random provider must be a function")
|
||||
assert(type(instance.timedDuration) == "number" and instance.timedDuration > 0, "timed duration must be positive")
|
||||
instance:CreateWindowFrame()
|
||||
instance:CreateBoard()
|
||||
instance:CreateMenus()
|
||||
return instance
|
||||
end
|
||||
|
||||
function MainWindow:HideOverlays()
|
||||
for _, overlay in pairs(self.overlays) do
|
||||
overlay:Hide()
|
||||
end
|
||||
self.activeOverlay = nil
|
||||
end
|
||||
|
||||
function MainWindow:ShowOverlay(name)
|
||||
local overlay = self.overlays[name]
|
||||
assert(overlay, "unknown main-window overlay")
|
||||
self:HideOverlays()
|
||||
overlay:Show()
|
||||
self.activeOverlay = name
|
||||
return overlay
|
||||
end
|
||||
|
||||
function MainWindow:PauseForMenu()
|
||||
local session = self.session
|
||||
if session and session.active and not session:IsPaused() then
|
||||
session:Pause("menu")
|
||||
self.menuOwnsPause = true
|
||||
end
|
||||
end
|
||||
|
||||
function MainWindow:ShowMenu()
|
||||
self:PauseForMenu()
|
||||
local menu = self:ShowOverlay("menu")
|
||||
if self.session and self.session.active then
|
||||
menu.resume:Show()
|
||||
else
|
||||
menu.resume:Hide()
|
||||
end
|
||||
return menu
|
||||
end
|
||||
|
||||
function MainWindow:ShowModeMenu()
|
||||
self:PauseForMenu()
|
||||
return self:ShowOverlay("mode")
|
||||
end
|
||||
|
||||
function MainWindow:ChooseClassic()
|
||||
if SavedVariables:HasClassicGame(self.profile) then
|
||||
return self:ShowOverlay("classic")
|
||||
end
|
||||
return self:StartClassic(false)
|
||||
end
|
||||
|
||||
function MainWindow:ResumeGame()
|
||||
self:HideOverlays()
|
||||
if self.menuOwnsPause and self.session and self.session.active and not self.session:IsGameOver() then
|
||||
self.session:Resume("menu")
|
||||
end
|
||||
self.menuOwnsPause = false
|
||||
return self.session
|
||||
end
|
||||
|
||||
function MainWindow:StopSession(reason)
|
||||
local session = self.session
|
||||
if not session then
|
||||
return nil
|
||||
end
|
||||
local result = session:Deactivate(reason or "new-game")
|
||||
if self.onSessionStopped then
|
||||
self.onSessionStopped(result, session, self)
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
function MainWindow:StartGame(gameMode, restore, duration)
|
||||
assert(
|
||||
gameMode == Constants.GAME_MODE_CLASSIC or gameMode == Constants.GAME_MODE_TIMED,
|
||||
"main window supports Classic or Timed sessions"
|
||||
)
|
||||
assert(type(restore) == "boolean", "restore state must be Boolean")
|
||||
if restore then
|
||||
assert(gameMode == Constants.GAME_MODE_CLASSIC and SavedVariables:HasClassicGame(self.profile), "no Classic game is available to continue")
|
||||
end
|
||||
self:StopSession("new-game")
|
||||
self:HideOverlays()
|
||||
self.menuOwnsPause = false
|
||||
self.windowOwnsPause = false
|
||||
self.animations:Resume()
|
||||
self.gemPool:SetInteractive(true)
|
||||
local fillAttempts
|
||||
if not restore then
|
||||
local filled, attempts = self.grid:Fill(self.random)
|
||||
assert(filled, attempts)
|
||||
fillAttempts = attempts
|
||||
if gameMode == Constants.GAME_MODE_CLASSIC then
|
||||
SavedVariables:ClearClassicGame(self.grid, self.profile)
|
||||
end
|
||||
end
|
||||
self.gemPool:Project(self.grid, true)
|
||||
local session = self.hud:CreateSession(self.grid, self.gemPool, {
|
||||
profile = self.profile,
|
||||
accountData = self.accountData,
|
||||
playerName = self.playerName,
|
||||
gameMode = gameMode,
|
||||
timeLimit = gameMode == Constants.GAME_MODE_TIMED and duration or nil,
|
||||
autoSave = true,
|
||||
inputOptions = {
|
||||
random = self.random,
|
||||
audio = self.audio,
|
||||
},
|
||||
})
|
||||
self.session = session
|
||||
self.gemPool:SetHandlers(session:CreateGemHandlers())
|
||||
if restore then
|
||||
session:RestoreClassicGame({ paused = false })
|
||||
else
|
||||
self.gemPool:Project(self.grid, true)
|
||||
self.animations:SyncPersistentEffects(false)
|
||||
if gameMode == Constants.GAME_MODE_CLASSIC then
|
||||
session:SaveClassicGame("new-game")
|
||||
end
|
||||
end
|
||||
local result = {
|
||||
status = restore and "restored" or "started",
|
||||
gameMode = gameMode,
|
||||
timeLimit = session.timeLimit,
|
||||
fillAttempts = fillAttempts,
|
||||
session = session,
|
||||
}
|
||||
self.lastStartResult = result
|
||||
if self.onSessionStarted then
|
||||
self.onSessionStarted(result, session, self)
|
||||
end
|
||||
return session, result
|
||||
end
|
||||
|
||||
function MainWindow:StartClassic(restore)
|
||||
return self:StartGame(Constants.GAME_MODE_CLASSIC, restore and true or false)
|
||||
end
|
||||
|
||||
function MainWindow:StartTimed(duration)
|
||||
duration = duration or self.timedDuration
|
||||
assert(type(duration) == "number" and duration > 0, "timed game duration must be positive")
|
||||
return self:StartGame(Constants.GAME_MODE_TIMED, false, duration)
|
||||
end
|
||||
|
||||
function MainWindow:HandleWindowShown()
|
||||
self.visible = true
|
||||
if self.windowOwnsPause and self.session and self.session.active and not self.activeOverlay then
|
||||
self.session:Resume("window-shown")
|
||||
end
|
||||
self.windowOwnsPause = false
|
||||
if not self.session and not self.activeOverlay then
|
||||
self:ShowMenu()
|
||||
end
|
||||
end
|
||||
|
||||
function MainWindow:HandleWindowHidden()
|
||||
self.visible = false
|
||||
if self.session and self.session.active and not self.session:IsPaused() then
|
||||
self.session:Pause("window-hidden")
|
||||
self.windowOwnsPause = true
|
||||
end
|
||||
end
|
||||
|
||||
function MainWindow:Show()
|
||||
self.suppressWindowScript = true
|
||||
self.frame:Show()
|
||||
self.suppressWindowScript = false
|
||||
self:HandleWindowShown()
|
||||
return self
|
||||
end
|
||||
|
||||
function MainWindow:Hide()
|
||||
self.suppressWindowScript = true
|
||||
self.frame:Hide()
|
||||
self.suppressWindowScript = false
|
||||
self:HandleWindowHidden()
|
||||
return self
|
||||
end
|
||||
|
||||
function MainWindow:IsShown()
|
||||
return self.visible
|
||||
end
|
||||
|
||||
function MainWindow:Toggle()
|
||||
if self.visible then
|
||||
return self:Hide()
|
||||
end
|
||||
return self:Show()
|
||||
end
|
||||
|
||||
function MainWindow:Update(elapsed)
|
||||
assert(type(elapsed) == "number" and elapsed >= 0, "main-window elapsed time must be nonnegative")
|
||||
if self.session then
|
||||
self.session:AdvanceElapsed(elapsed)
|
||||
self.hud:Update(elapsed)
|
||||
end
|
||||
self.audio:Update(elapsed)
|
||||
return self.session and self.session.timerElapsed or 0
|
||||
end
|
||||
|
||||
MainWindow.WINDOW_WIDTH = WINDOW_WIDTH
|
||||
MainWindow.WINDOW_HEIGHT = WINDOW_HEIGHT
|
||||
MainWindow.DEFAULT_TIMED_DURATION = DEFAULT_TIMED_DURATION
|
||||
|
||||
addon.MainWindow = MainWindow
|
||||
@@ -1,12 +1,12 @@
|
||||
# Bejeweled modernization
|
||||
|
||||
This branch contains the analysis-complete, Mainline-first modernization of the legacy World of Warcraft Bejeweled addon. Its Retail TOC and headless runtime foundation are installable for development, but the addon is not yet playable because the main window and menu/session integration still need to be restored.
|
||||
This branch contains the analysis-complete, Mainline-first modernization of the legacy World of Warcraft Bejeweled addon. Its Retail TOC now assembles a headless-verified interactive Classic/Timed gameplay shell, but the modernization is not yet alpha-ready because the remaining screens, Retail event integration, and in-game equivalence testing still need to be completed.
|
||||
|
||||
## Status and phase gate
|
||||
|
||||
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, BackdropTemplate-safe UI chrome, persistent grid-to-gem texture projection, cancellable cascade sequencing, pooled board effects, and a session-bound Classic/Timed HUD with status, hints, achievements, and final summaries.
|
||||
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, and an automatically assembled movable window with New Game, Continue, mode selection, input wiring, and 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 shared backdrop construction, board tiles, persistent gem-frame projection, recorded cascade-transition playback, pooled legacy-cadence board effects, and the session-bound score/timer/status HUD.
|
||||
- `Bejeweled/UI/` — Retail-safe frame/rendering boundaries; currently shared backdrop construction, board tiles, persistent gem-frame projection, recorded cascade-transition playback, pooled legacy-cadence board effects, the session-bound HUD, and the playable main-window/session shell.
|
||||
- `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.
|
||||
|
||||
@@ -11,6 +11,7 @@ The modernization baseline is the authoritative WoW UI/API source snapshot for l
|
||||
| Animation | Frames expose `CreateAnimationGroup`; groups expose `CreateAnimation`, `Play`, `Pause`, `IsPaused`, `Stop`, and `SetScript`; animations expose `SetOrder`; translation animations expose `SetOffset`; alpha animations expose `SetFromAlpha` and `SetToAlpha`. | `UI/Animations.lua` reuses per-frame groups for cascades, pauses/resumes active groups with the session, and uses ordered translation pairs for legacy invalid-swap rollback. Completion scripts are removed before cancellation. |
|
||||
| Texture rendering | `SimpleFrame:CreateTexture` returns a texture region. `SimpleTextureBase` exposes `SetTexture`, four-coordinate `SetTexCoord`, `SetBlendMode`, and `SetRotation(radians, optionalPoint)` in all environments. | `UI/GemPool.lua` projects addon-owned textures and proven legacy UV coordinates. `UI/Animations.lua` uses supported texture rotation instead of the legacy eight-coordinate rotation helper. |
|
||||
| HUD text | `SimpleFrame:CreateFontString` returns a non-nil `SimpleFontString`. Its current all-environment contract exposes `SetFont`, `SetText`, `SetFormattedText`, `SetTextColor`, and horizontal justification; `SetFont` reports whether the asset and height were accepted. | `UI/HUD.lua` uses the immutable bundled font for score, timer, status, achievement, pause, and summary text, and treats a failed font load as a construction error. |
|
||||
| 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 | Click uses `func(addonName, buttonName, menuButtonFrame)`; hover uses `funcOnEnter(addonName, menuButtonFrame)` and `funcOnLeave(addonName, menuButtonFrame)`. | `UI/Compartment.lua` will implement those distinct current contracts without assuming one shared parameter list. |
|
||||
| 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. |
|
||||
@@ -28,6 +29,7 @@ API existence alone does not prove behavioral equivalence. Each future substitut
|
||||
- Supported SoundKit identifiers and current `PlaySound(SOUNDKIT.*)` usage: [`SoundKitConstants.lua`](https://github.com/Gethe/wow-ui-source/blob/81d15e42f16f3473131880500e7a8c8eb88fa5e6/Interface/AddOns/Blizzard_SharedXML/Mainline/SoundKitConstants.lua) and [`LootFrame.lua`](https://github.com/Gethe/wow-ui-source/blob/81d15e42f16f3473131880500e7a8c8eb88fa5e6/Interface/AddOns/Blizzard_UIPanels_Game/Mainline/LootFrame.lua)
|
||||
- Frame texture construction and rendering methods: [`SimpleFrameAPIDocumentation.lua`](https://github.com/Gethe/wow-ui-source/blob/81d15e42f16f3473131880500e7a8c8eb88fa5e6/Interface/AddOns/Blizzard_APIDocumentationGenerated/SimpleFrameAPIDocumentation.lua) and [`SimpleTextureBaseAPIDocumentation.lua`](https://github.com/Gethe/wow-ui-source/blob/81d15e42f16f3473131880500e7a8c8eb88fa5e6/Interface/AddOns/Blizzard_APIDocumentationGenerated/SimpleTextureBaseAPIDocumentation.lua)
|
||||
- HUD font-string creation and text/font operations: [`SimpleFrameAPIDocumentation.lua`](https://github.com/Gethe/wow-ui-source/blob/81d15e42f16f3473131880500e7a8c8eb88fa5e6/Interface/AddOns/Blizzard_APIDocumentationGenerated/SimpleFrameAPIDocumentation.lua) and [`SimpleFontStringAPIDocumentation.lua`](https://github.com/Gethe/wow-ui-source/blob/81d15e42f16f3473131880500e7a8c8eb88fa5e6/Interface/AddOns/Blizzard_APIDocumentationGenerated/SimpleFontStringAPIDocumentation.lua)
|
||||
- Main-window movement and player-name identity: [`SimpleFrameAPIDocumentation.lua`](https://github.com/Gethe/wow-ui-source/blob/81d15e42f16f3473131880500e7a8c8eb88fa5e6/Interface/AddOns/Blizzard_APIDocumentationGenerated/SimpleFrameAPIDocumentation.lua) and [`UnitDocumentation.lua`](https://github.com/Gethe/wow-ui-source/blob/81d15e42f16f3473131880500e7a8c8eb88fa5e6/Interface/AddOns/Blizzard_APIDocumentationGenerated/UnitDocumentation.lua)
|
||||
- Line construction and endpoint/thickness contracts: [`SimpleFrameAPIDocumentation.lua`](https://github.com/Gethe/wow-ui-source/blob/81d15e42f16f3473131880500e7a8c8eb88fa5e6/Interface/AddOns/Blizzard_APIDocumentationGenerated/SimpleFrameAPIDocumentation.lua), [`SimpleLineAPIDocumentation.lua`](https://github.com/Gethe/wow-ui-source/blob/81d15e42f16f3473131880500e7a8c8eb88fa5e6/Interface/AddOns/Blizzard_APIDocumentationGenerated/SimpleLineAPIDocumentation.lua), and [`SimpleTextureBaseAPIDocumentation.lua`](https://github.com/Gethe/wow-ui-source/blob/81d15e42f16f3473131880500e7a8c8eb88fa5e6/Interface/AddOns/Blizzard_APIDocumentationGenerated/SimpleTextureBaseAPIDocumentation.lua)
|
||||
- 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)
|
||||
|
||||
@@ -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/Backdrops.lua`, `UI/GemPool.lua`, `UI/Animations.lua`, and `UI/HUD.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/Backdrops.lua`, `UI/GemPool.lua`, `UI/Animations.lua`, `UI/HUD.lua`, and `UI/MainWindow.lua`.
|
||||
|
||||
## Load order and ownership
|
||||
|
||||
@@ -18,7 +18,8 @@ The analysis phase gate is satisfied. This ownership map now governs runtime imp
|
||||
12. `UI/GemPool.lua` — fixed allocation and reuse of the 64 interactive gem frames, the sixteen board-art tiles, input-handler attachment, selection projection, and change-aware projection from authoritative grid cells into normal/hyper texture layers. Power-gem overlays remain `UI/Animations.lua` ownership.
|
||||
13. `UI/Animations.lua` — deterministic swap/rollback and clear/gravity/refill plans, reusable animation groups, session-controlled pause/resume, interaction locking, cancellation, and final-grid normalization. It also owns the legacy-cadence 40-frame hyper atlas, counter-rotating/cross-faded power layers, pooled 16-frame explosion atlas, and pooled 15-tick lightning lines that gate settling. Nonblocking effects share the same pausable 25 ms clock: every cleared gem emits a ten-shard burst, ambient lightwaves propagate across active boards, hints delay and bounce above a selected cell, and reusable floating text supports score and status presentation. Current fall timings are explicit modernization defaults pending in-game tuning.
|
||||
14. `UI/HUD.lua` — BackdropTemplate-safe Classic score/level/progress and Timed points-per-second/multiplier/countdown presentation. It consumes copied input/session callbacks, schedules idle hints, emits score and skill floating text through `UI/Animations.lua`, owns temporary status/achievement and pause overlays, and renders terminal summaries. Its `Update` method only refreshes presentation and expiry clocks; `Engine/Session.lua` remains the sole owner of elapsed time and gameplay transitions.
|
||||
15. `UI/Compartment.lua` — addon-compartment click and hover callbacks.
|
||||
15. `UI/MainWindow.lua` — the 448×510 movable runtime shell, 400×400 board construction, Menu/New Game overlays, Classic Continue/New Game and direct default five-minute Timed selection, session replacement, and show/hide pause ownership. 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.
|
||||
16. `UI/Compartment.lua` — addon-compartment click and hover callbacks.
|
||||
|
||||
## Data flow
|
||||
|
||||
|
||||
+158
-1
@@ -53,6 +53,7 @@ LoadAddonFile("Bejeweled/UI/Backdrops.lua", addon)
|
||||
LoadAddonFile("Bejeweled/UI/GemPool.lua", addon)
|
||||
LoadAddonFile("Bejeweled/UI/Animations.lua", addon)
|
||||
LoadAddonFile("Bejeweled/UI/HUD.lua", addon)
|
||||
LoadAddonFile("Bejeweled/UI/MainWindow.lua", addon)
|
||||
|
||||
AssertEqual(eventFrame.registeredEvent, "ADDON_LOADED", "initializer event registration")
|
||||
AssertEqual(addon.Constants.GRID_WIDTH, 8, "grid width")
|
||||
@@ -318,9 +319,30 @@ local function CreateGemPoolFrame(frameType, name, parent, template)
|
||||
function frame:SetFrameLevel(frameLevel)
|
||||
self.frameLevel = frameLevel
|
||||
end
|
||||
function frame:GetFrameLevel()
|
||||
if self.frameLevel then
|
||||
return self.frameLevel
|
||||
end
|
||||
if self.parent and type(self.parent.GetFrameLevel) == "function" then
|
||||
return self.parent:GetFrameLevel()
|
||||
end
|
||||
return 0
|
||||
end
|
||||
function frame:EnableMouse(enabled)
|
||||
self.mouseEnabled = enabled
|
||||
end
|
||||
function frame:SetMovable(movable)
|
||||
self.movable = movable
|
||||
end
|
||||
function frame:SetClampedToScreen(clamped)
|
||||
self.clamped = clamped
|
||||
end
|
||||
function frame:StartMoving()
|
||||
self.moving = true
|
||||
end
|
||||
function frame:StopMovingOrSizing()
|
||||
self.moving = false
|
||||
end
|
||||
function frame:RegisterForDrag(button)
|
||||
self.dragButton = button
|
||||
end
|
||||
@@ -1654,10 +1676,134 @@ AssertEqual(timedHUD.progress.text.text, "0:00", "timed HUD zero countdown")
|
||||
AssertEqual(timedHUD.progress.ratio, 0, "timed HUD empty progress")
|
||||
end
|
||||
|
||||
function addon:TestMainWindowForTest()
|
||||
local runtimeProfile = addon.SavedVariables:CreateDefaultProfile()
|
||||
local runtimeAccount = addon.SavedVariables:CreateDefaultAccount()
|
||||
local runtimeAudio = {
|
||||
played = {},
|
||||
elapsed = 0,
|
||||
}
|
||||
function runtimeAudio:Play(soundName)
|
||||
self.played[#self.played + 1] = soundName
|
||||
return true
|
||||
end
|
||||
function runtimeAudio:Update(elapsed)
|
||||
self.elapsed = self.elapsed + elapsed
|
||||
return {}
|
||||
end
|
||||
local sessionStarts = {}
|
||||
local sessionStops = {}
|
||||
local runtime = addon.MainWindow:New(gemPoolParent, {
|
||||
createFrame = CreateGemPoolFrame,
|
||||
profile = runtimeProfile,
|
||||
accountData = runtimeAccount,
|
||||
audio = runtimeAudio,
|
||||
playerName = "Nighthawk",
|
||||
random = MakeRandom(12001),
|
||||
onSessionStarted = function(result)
|
||||
sessionStarts[#sessionStarts + 1] = result
|
||||
end,
|
||||
onSessionStopped = function(result)
|
||||
sessionStops[#sessionStops + 1] = result
|
||||
end,
|
||||
})
|
||||
AssertEqual(runtime.frame.width, addon.MainWindow.WINDOW_WIDTH, "runtime window width")
|
||||
AssertEqual(runtime.frame.height, addon.MainWindow.WINDOW_HEIGHT, "runtime window height")
|
||||
AssertEqual(runtime.boardSurface.width, 400, "runtime board width")
|
||||
AssertEqual(runtime.boardSurface.height, 400, "runtime board height")
|
||||
AssertEqual(#runtime.gemPool.tiles, 16, "runtime board tile count")
|
||||
runtime:Show()
|
||||
assert(runtime.frame.shown, "runtime window did not show")
|
||||
assert(runtime:IsShown(), "runtime visibility state did not follow Show")
|
||||
AssertEqual(runtime.activeOverlay, "menu", "runtime initial menu")
|
||||
assert(not runtime.overlays.menu.resume.shown, "runtime initial menu exposed Resume")
|
||||
|
||||
runtime.overlays.menu.newGame.scripts.OnClick()
|
||||
AssertEqual(runtime.activeOverlay, "mode", "runtime New Game did not show mode selection")
|
||||
runtime.overlays.mode.classic.scripts.OnClick()
|
||||
local firstClassic = runtime.session
|
||||
assert(firstClassic and firstClassic.active, "runtime Classic session did not start")
|
||||
assert(not runtime.animations:IsPaused(), "runtime Classic session inherited the menu pause")
|
||||
AssertEqual(firstClassic.gameMode, addon.Constants.GAME_MODE_CLASSIC, "runtime Classic mode")
|
||||
assert(runtime.grid:FindLegalMove(), "runtime Classic board has no legal move")
|
||||
assert(runtimeProfile.settings.classicInProgress, "new Classic runtime was not resumable")
|
||||
assert(type(runtime.gemPool:GetFrame(1, 1).scripts.OnMouseDown) == "function", "runtime did not attach gem input")
|
||||
AssertEqual(sessionStarts[1].status, "started", "runtime Classic start callback")
|
||||
runtime:Update(1.5)
|
||||
AssertEqual(firstClassic.timerElapsed, 1.5, "runtime update did not advance session time")
|
||||
AssertEqual(runtimeAudio.elapsed, 1.5, "runtime update did not flush audio")
|
||||
|
||||
runtime:ShowMenu()
|
||||
assert(firstClassic:IsPaused(), "runtime menu did not pause Classic play")
|
||||
assert(runtime.overlays.menu.resume.shown, "active runtime menu hid Resume")
|
||||
runtime.overlays.menu.resume.scripts.OnClick()
|
||||
assert(not firstClassic:IsPaused(), "runtime Resume left Classic paused")
|
||||
assert(runtime.activeOverlay == nil, "runtime Resume retained a menu overlay")
|
||||
|
||||
firstClassic.scoringState.score = 2468
|
||||
firstClassic:SaveClassicGame("continue-fixture")
|
||||
runtime:ShowMenu()
|
||||
runtime.overlays.menu.newGame.scripts.OnClick()
|
||||
runtime.overlays.mode.classic.scripts.OnClick()
|
||||
AssertEqual(runtime.activeOverlay, "classic", "saved Classic did not show Continue/New Game")
|
||||
runtime.overlays.classic.continue.scripts.OnClick()
|
||||
local restoredClassic = runtime.session
|
||||
assert(restoredClassic ~= firstClassic, "runtime Continue reused the abandoned session")
|
||||
assert(not runtime.animations:IsPaused(), "runtime Continue inherited the menu pause")
|
||||
assert(not firstClassic.active and firstClassic:IsLocked(), "runtime replacement did not deactivate the old session")
|
||||
AssertEqual(restoredClassic.scoringState.score, 2468, "runtime Continue score")
|
||||
AssertEqual(sessionStarts[2].status, "restored", "runtime Continue callback")
|
||||
AssertEqual(#sessionStops, 1, "runtime Continue omitted session-stop callback")
|
||||
|
||||
runtime:ShowMenu()
|
||||
runtime.overlays.menu.newGame.scripts.OnClick()
|
||||
runtime.overlays.mode.timed.scripts.OnClick()
|
||||
local timedRuntime = runtime.session
|
||||
AssertEqual(timedRuntime.gameMode, addon.Constants.GAME_MODE_TIMED, "runtime Timed mode")
|
||||
assert(not runtime.animations:IsPaused(), "runtime Timed session inherited the menu pause")
|
||||
assert(runtime.gemPool:GetFrame(1, 1).mouseEnabled, "runtime Timed replacement left gem input disabled")
|
||||
AssertEqual(timedRuntime.timeLimit, addon.MainWindow.DEFAULT_TIMED_DURATION, "runtime default Timed duration")
|
||||
AssertEqual(runtime.hud.levelPanel.caption.text, "PPS", "runtime Timed HUD mode")
|
||||
AssertEqual(sessionStarts[3].timeLimit, addon.MainWindow.DEFAULT_TIMED_DURATION, "runtime Timed callback duration")
|
||||
assert(not restoredClassic.active, "runtime Timed start left Classic active")
|
||||
timedRuntime:SetElapsed(1)
|
||||
timedRuntime:BeginGameOver("runtime-restart-fixture")
|
||||
assert(timedRuntime:IsGameOver() and timedRuntime:IsLocked(), "runtime Timed fixture did not reach terminal state")
|
||||
timedRuntime = runtime:StartTimed(60)
|
||||
assert(runtime.gemPool:GetFrame(1, 1).mouseEnabled, "runtime terminal replacement left gem input disabled")
|
||||
AssertEqual(timedRuntime.timeLimit, 60, "runtime custom Timed duration")
|
||||
|
||||
runtime:Hide()
|
||||
assert(timedRuntime:IsPaused(), "hidden runtime window did not pause play")
|
||||
assert(not runtime:IsShown(), "runtime visibility state did not follow Hide")
|
||||
runtime:Show()
|
||||
assert(not timedRuntime:IsPaused(), "shown runtime window did not resume its owned pause")
|
||||
runtime.frame.scripts.OnDragStart(runtime.frame)
|
||||
assert(runtime.frame.moving, "unlocked runtime window did not start moving")
|
||||
runtime.frame.scripts.OnDragStop(runtime.frame)
|
||||
assert(not runtime.frame.moving, "runtime drag stop left the window moving")
|
||||
runtimeProfile.settings.lockWindow = true
|
||||
runtime.frame.scripts.OnDragStart(runtime.frame)
|
||||
assert(not runtime.frame.moving, "locked runtime window started moving")
|
||||
|
||||
runtime:ShowMenu()
|
||||
runtime.overlays.menu.newGame.scripts.OnClick()
|
||||
runtime.overlays.mode.classic.scripts.OnClick()
|
||||
AssertEqual(runtime.activeOverlay, "classic", "runtime Classic save chooser was skipped")
|
||||
runtime.overlays.classic.newGame.scripts.OnClick()
|
||||
local freshClassic = runtime.session
|
||||
AssertEqual(freshClassic.scoringState.score, 0, "runtime fresh Classic retained prior score")
|
||||
assert(freshClassic ~= restoredClassic, "runtime fresh Classic reused a prior session")
|
||||
AssertEqual(#sessionStarts, 5, "runtime session-start callback count")
|
||||
AssertEqual(#sessionStops, 4, "runtime session-stop callback count")
|
||||
end
|
||||
|
||||
TestSessionRestore()
|
||||
TestGameOverTransitions()
|
||||
addon:TestHUDPresentationForTest()
|
||||
addon.TestHUDPresentationForTest = nil
|
||||
addon:TestMainWindowForTest()
|
||||
addon.TestMainWindowForTest = nil
|
||||
|
||||
FillStablePattern(cascadeGrid)
|
||||
for x = 2, 5 do
|
||||
@@ -2015,6 +2161,7 @@ assert(addon.backdrops == addon.Backdrops, "addon initialization did not install
|
||||
assert(addon.gemPoolFactory == addon.GemPool, "addon initialization did not install GemPool")
|
||||
assert(addon.animationFactory == addon.Animations, "addon initialization did not install Animations")
|
||||
assert(addon.hudFactory == addon.HUD, "addon initialization did not install HUD")
|
||||
assert(addon.mainWindowFactory == addon.MainWindow, "addon initialization did not install MainWindow")
|
||||
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")
|
||||
@@ -2033,7 +2180,17 @@ assert(addon.backdrops == initializedBackdrops, "backdrop initialization is not
|
||||
assert(addon.gemPoolFactory == initializedGemPoolFactory, "GemPool initialization is not idempotent")
|
||||
assert(addon.animationFactory == initializedAnimationFactory, "Animations initialization is not idempotent")
|
||||
assert(addon.hudFactory == initializedHUDFactory, "HUD initialization is not idempotent")
|
||||
assert(addon.mainWindowFactory == addon.MainWindow, "MainWindow initialization is not idempotent")
|
||||
assert(addon.inputFactory == initializedInputFactory, "Input initialization is not idempotent")
|
||||
assert(addon.sessionFactory == initializedSessionFactory, "Session initialization is not idempotent")
|
||||
addon.runtimeForTest = addon:StartRuntime({
|
||||
uiParent = gemPoolParent,
|
||||
createFrame = CreateGemPoolFrame,
|
||||
playerName = "Nighthawk",
|
||||
random = MakeRandom(13001),
|
||||
})
|
||||
assert(addon.runtimeForTest == addon.runtime, "StartRuntime did not retain the playable shell")
|
||||
AssertEqual(addon.runtimeForTest.activeOverlay, "menu", "StartRuntime did not open the initial menu")
|
||||
assert(addon:StartRuntime() == addon.runtimeForTest, "StartRuntime is not idempotent")
|
||||
|
||||
print("Runtime verification passed: Classic/Timed 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: 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.")
|
||||
|
||||
@@ -32,7 +32,8 @@ try {
|
||||
"UI\Backdrops.lua",
|
||||
"UI\GemPool.lua",
|
||||
"UI\Animations.lua",
|
||||
"UI\HUD.lua"
|
||||
"UI\HUD.lua",
|
||||
"UI\MainWindow.lua"
|
||||
)
|
||||
$actualFiles = @($toc | Where-Object { $_ -match "\.lua$" })
|
||||
if (Compare-Object -ReferenceObject $expectedFiles -DifferenceObject $actualFiles -SyncWindow 0) {
|
||||
@@ -46,7 +47,7 @@ try {
|
||||
}
|
||||
}
|
||||
|
||||
Write-Output "Verified: Retail TOC order and Lua 5.1-compatible session/gameplay/HUD runtime."
|
||||
Write-Output "Verified: Retail TOC order and Lua 5.1-compatible playable window/session runtime."
|
||||
}
|
||||
finally {
|
||||
Pop-Location
|
||||
|
||||
Reference in New Issue
Block a user