mirror of
https://github.com/Nighthawk42/wow_bejeweled.git
synced 2026-08-30 04:30:22 +00:00
feat: add input session coordination
This commit is contained in:
@@ -15,6 +15,7 @@ Engine\Grid.lua
|
||||
Engine\Matches.lua
|
||||
Engine\Cascade.lua
|
||||
Engine\Scoring.lua
|
||||
Engine\Input.lua
|
||||
UI\Backdrops.lua
|
||||
UI\GemPool.lua
|
||||
UI\Animations.lua
|
||||
|
||||
@@ -46,9 +46,11 @@ local Constants = {
|
||||
SKILL_CLEAR25 = 4,
|
||||
SKILL_SCORE10000 = 1,
|
||||
SKILL_LEVEL10 = 2,
|
||||
SKILL_MOVE100 = 3,
|
||||
SKILL_SCORE25000 = 4,
|
||||
SKILL_LEVEL15 = 5,
|
||||
SKILL_SCORE50000 = 6,
|
||||
SKILL_MOVE250 = 7,
|
||||
SKILL_SCORE75000 = 8,
|
||||
ACHIEVEMENT_CLEAR1000 = 2,
|
||||
ACHIEVEMENT_POWER100 = 4,
|
||||
|
||||
@@ -18,6 +18,7 @@ function addon:Initialize(accountData, profileData)
|
||||
assert(self.Backdrops, "Backdrops module is not loaded")
|
||||
assert(self.GemPool, "GemPool module is not loaded")
|
||||
assert(self.Animations, "Animations module is not loaded")
|
||||
assert(self.Input, "Input module is not loaded")
|
||||
|
||||
self.accountData, self.profileData = self.SavedVariables:Initialize(accountData, profileData)
|
||||
self.grid = self.Grid:New()
|
||||
@@ -25,6 +26,7 @@ function addon:Initialize(accountData, profileData)
|
||||
self.backdrops = self.Backdrops
|
||||
self.gemPoolFactory = self.GemPool
|
||||
self.animationFactory = self.Animations
|
||||
self.inputFactory = self.Input
|
||||
self.initialized = true
|
||||
|
||||
return self
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
local _, addon = ...
|
||||
|
||||
local Constants = assert(addon.Constants, "Constants module is not loaded")
|
||||
local Matches = assert(addon.Matches, "Matches module is not loaded")
|
||||
local Cascade = assert(addon.Cascade, "Cascade module is not loaded")
|
||||
local Scoring = assert(addon.Scoring, "Scoring module is not loaded")
|
||||
|
||||
local Input = {}
|
||||
Input.__index = Input
|
||||
|
||||
local CALLBACK_NAMES = {
|
||||
"onSelectionChanged",
|
||||
"onMoveStarted",
|
||||
"onCascadeResolved",
|
||||
"onMoveComplete",
|
||||
}
|
||||
|
||||
local function ValidateCallbacks(options)
|
||||
local callbacks = {}
|
||||
for index = 1, #CALLBACK_NAMES do
|
||||
local callbackName = CALLBACK_NAMES[index]
|
||||
local callback = options[callbackName]
|
||||
assert(callback == nil or type(callback) == "function", callbackName .. " must be a function")
|
||||
callbacks[callbackName] = callback
|
||||
end
|
||||
return callbacks
|
||||
end
|
||||
|
||||
local function CopyCell(cell)
|
||||
return {
|
||||
x = cell.gridX,
|
||||
y = cell.gridY,
|
||||
contents = cell.contents,
|
||||
bigStar = cell.bigStar and true or false,
|
||||
}
|
||||
end
|
||||
|
||||
function Input:New(grid, gemPool, animations, options)
|
||||
assert(type(grid) == "table" and type(grid.Get) == "function" and type(grid.Swap) == "function", "input requires a grid")
|
||||
assert(type(gemPool) == "table" and type(gemPool.SetSelection) == "function", "input requires a gem pool")
|
||||
assert(type(animations) == "table" and type(animations.PlaySwap) == "function" and type(animations.Play) == "function" and type(animations.IsPlaying) == "function", "input requires animations")
|
||||
options = options or {}
|
||||
assert(type(options) == "table", "input options must be a table")
|
||||
assert((options.scoringState == nil) == (options.profile == nil), "scoring state and profile must be supplied together")
|
||||
assert(options.audio == nil or type(options.audio.Play) == "function", "input audio must expose Play")
|
||||
|
||||
local instance = setmetatable({}, self)
|
||||
instance.grid = grid
|
||||
instance.gemPool = gemPool
|
||||
instance.animations = animations
|
||||
instance.matches = options.matches or Matches
|
||||
instance.cascade = options.cascade or Cascade
|
||||
instance.scoring = options.scoring or Scoring
|
||||
instance.scoringState = options.scoringState
|
||||
instance.profile = options.profile
|
||||
instance.audio = options.audio
|
||||
instance.random = options.random or grid.random or math.random
|
||||
instance.requireLegalMove = options.requireLegalMove ~= false
|
||||
instance.maximumRefillAttempts = options.maximumRefillAttempts
|
||||
instance.maximumCascades = options.maximumCascades
|
||||
instance.skillLimit = options.skillLimit
|
||||
instance.callbacks = ValidateCallbacks(options)
|
||||
instance.selectedX = nil
|
||||
instance.selectedY = nil
|
||||
instance.locked = false
|
||||
instance.pendingMove = nil
|
||||
instance.lastMove = nil
|
||||
instance.moves = instance.scoringState and (instance.scoringState.moves or 0) or (options.moves or 0)
|
||||
return instance
|
||||
end
|
||||
|
||||
function Input:IsLocked()
|
||||
return self.locked or self.animations:IsPlaying()
|
||||
end
|
||||
|
||||
function Input:GetSelection()
|
||||
if not self.selectedX then
|
||||
return nil
|
||||
end
|
||||
return {
|
||||
x = self.selectedX,
|
||||
y = self.selectedY,
|
||||
cell = self.grid:Get(self.selectedX, self.selectedY),
|
||||
}
|
||||
end
|
||||
|
||||
function Input:PlaySound(soundName)
|
||||
if self.audio then
|
||||
self.audio:Play(soundName)
|
||||
end
|
||||
end
|
||||
|
||||
function Input:Notify(callbackName, result)
|
||||
local callback = self.callbacks[callbackName]
|
||||
if callback then
|
||||
callback(result, self)
|
||||
end
|
||||
end
|
||||
|
||||
function Input:SetSelection(column, row, reason)
|
||||
self.selectedX = column
|
||||
self.selectedY = row
|
||||
self.gemPool:SetSelection(column, row)
|
||||
local selection = self:GetSelection()
|
||||
self:Notify("onSelectionChanged", {
|
||||
status = selection and "selected" or "cleared",
|
||||
reason = reason,
|
||||
selection = selection,
|
||||
})
|
||||
return selection
|
||||
end
|
||||
|
||||
function Input:ClearSelection(reason)
|
||||
if not self.selectedX then
|
||||
return false
|
||||
end
|
||||
self:SetSelection(nil, nil, reason or "cleared")
|
||||
return true
|
||||
end
|
||||
|
||||
function Input:FinishMove(result, status)
|
||||
if self.pendingMove ~= result then
|
||||
return
|
||||
end
|
||||
result.status = status
|
||||
self.pendingMove = nil
|
||||
self.locked = false
|
||||
self.lastMove = result
|
||||
self:Notify("onMoveComplete", result)
|
||||
end
|
||||
|
||||
function Input:ResolveAcceptedMove(result)
|
||||
if self.pendingMove ~= result or result.cascadeStarted then
|
||||
return
|
||||
end
|
||||
result.cascadeStarted = true
|
||||
self:PlaySound("GemClick")
|
||||
local cascadeResult = self.cascade:Resolve(self.grid, {
|
||||
initialMatches = result.matchResult,
|
||||
random = self.random,
|
||||
requireLegalMove = self.requireLegalMove,
|
||||
maximumRefillAttempts = self.maximumRefillAttempts,
|
||||
maximumCascades = self.maximumCascades,
|
||||
})
|
||||
result.cascadeResult = cascadeResult
|
||||
if self.scoringState then
|
||||
result.scoringResult = self.scoring:ApplyCascade(
|
||||
self.scoringState,
|
||||
cascadeResult,
|
||||
self.profile,
|
||||
{ random = self.random, skillLimit = self.skillLimit }
|
||||
)
|
||||
end
|
||||
self:Notify("onCascadeResolved", result)
|
||||
result.cascadeRun = self.animations:Play(cascadeResult, self.grid, {
|
||||
onComplete = function()
|
||||
self:FinishMove(result, "complete")
|
||||
end,
|
||||
onCancel = function(reason)
|
||||
result.cascadePresentationCancelled = reason
|
||||
self:FinishMove(result, "complete")
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
function Input:BeginSwap(firstX, firstY, secondX, secondY)
|
||||
local firstCell = self.grid:Get(firstX, firstY)
|
||||
local secondCell = self.grid:Get(secondX, secondY)
|
||||
assert(firstCell and secondCell and self.grid:AreAdjacent(firstX, firstY, secondX, secondY), "input swap requires adjacent cells")
|
||||
if firstCell.contents == Constants.HYPER_CONTENTS or secondCell.contents == Constants.HYPER_CONTENTS then
|
||||
return {
|
||||
status = "hyper-pending",
|
||||
first = CopyCell(firstCell),
|
||||
second = CopyCell(secondCell),
|
||||
}
|
||||
end
|
||||
|
||||
local result = {
|
||||
status = "animating",
|
||||
first = CopyCell(firstCell),
|
||||
second = CopyCell(secondCell),
|
||||
valid = false,
|
||||
}
|
||||
self.locked = true
|
||||
self:ClearSelection("swap")
|
||||
self.grid:Swap(firstX, firstY, secondX, secondY)
|
||||
local preferredCells = {
|
||||
self.grid:Get(firstX, firstY),
|
||||
self.grid:Get(secondX, secondY),
|
||||
}
|
||||
local matchResult = self.matches:Find(self.grid, {
|
||||
preferredCells = preferredCells,
|
||||
random = self.random,
|
||||
})
|
||||
result.matchResult = matchResult
|
||||
result.valid = matchResult.hasMatches
|
||||
if not result.valid then
|
||||
self.grid:Swap(firstX, firstY, secondX, secondY)
|
||||
self:PlaySound("Invalid")
|
||||
else
|
||||
if self.scoringState then
|
||||
result.moveResult = self.scoring:RecordMove(
|
||||
self.scoringState,
|
||||
self.profile,
|
||||
{ random = self.random, skillLimit = self.skillLimit }
|
||||
)
|
||||
self.moves = self.scoringState.moves
|
||||
else
|
||||
self.moves = self.moves + 1
|
||||
result.moveResult = { moves = self.moves, skillEvents = {} }
|
||||
end
|
||||
end
|
||||
self.pendingMove = result
|
||||
result.swapRun = self.animations:PlaySwap(
|
||||
firstX,
|
||||
firstY,
|
||||
secondX,
|
||||
secondY,
|
||||
not result.valid,
|
||||
self.grid,
|
||||
{
|
||||
onComplete = function()
|
||||
if result.valid then
|
||||
self:ResolveAcceptedMove(result)
|
||||
else
|
||||
self:FinishMove(result, "rejected")
|
||||
end
|
||||
end,
|
||||
onCancel = function(reason)
|
||||
result.swapPresentationCancelled = reason
|
||||
if result.valid then
|
||||
self:ResolveAcceptedMove(result)
|
||||
else
|
||||
self:FinishMove(result, "rejected")
|
||||
end
|
||||
end,
|
||||
}
|
||||
)
|
||||
self:Notify("onMoveStarted", result)
|
||||
return result
|
||||
end
|
||||
|
||||
function Input:HandleCell(column, row)
|
||||
if self:IsLocked() then
|
||||
return { status = "locked" }
|
||||
end
|
||||
local cell = self.grid:Get(column, row)
|
||||
assert(cell, "input cell is out of bounds")
|
||||
if cell.contents == Constants.EMPTY_CONTENTS then
|
||||
return { status = "empty" }
|
||||
end
|
||||
if not self.selectedX then
|
||||
self:SetSelection(column, row, "input")
|
||||
self:PlaySound("Select")
|
||||
return { status = "selected", selection = self:GetSelection() }
|
||||
end
|
||||
if self.selectedX == column and self.selectedY == row then
|
||||
self:ClearSelection("toggle")
|
||||
return { status = "cleared" }
|
||||
end
|
||||
if not self.grid:AreAdjacent(self.selectedX, self.selectedY, column, row) then
|
||||
self:ClearSelection("nonadjacent")
|
||||
return { status = "cleared", reason = "nonadjacent" }
|
||||
end
|
||||
return self:BeginSwap(self.selectedX, self.selectedY, column, row)
|
||||
end
|
||||
|
||||
function Input:CreateGemHandlers()
|
||||
return {
|
||||
onMouseDown = function(frame, button)
|
||||
if button == nil or button == "LeftButton" then
|
||||
return self:HandleCell(frame.gridX, frame.gridY)
|
||||
end
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
addon.Input = Input
|
||||
@@ -304,6 +304,33 @@ local function UpdateLargestCascade(state, stats, awards)
|
||||
return largest
|
||||
end
|
||||
|
||||
function Scoring:RecordMove(state, profile, options)
|
||||
assert(type(state) == "table", "move recording requires game state")
|
||||
ValidateMode(state.gameMode)
|
||||
ValidateProfile(profile)
|
||||
options = options or {}
|
||||
state.moves = (state.moves or 0) + 1
|
||||
if state.gameMode == Constants.GAME_MODE_TIMED then
|
||||
profile.stats.timed.mostMoves = math.max(profile.stats.timed.mostMoves or 0, state.moves)
|
||||
end
|
||||
local result = {
|
||||
moves = state.moves,
|
||||
skillEvents = {},
|
||||
}
|
||||
local skillOptions = {
|
||||
random = options.random or math.random,
|
||||
skillLimit = options.skillLimit,
|
||||
}
|
||||
if state.gameMode == Constants.GAME_MODE_CLASSIC then
|
||||
if state.moves == 100 then
|
||||
TrySkill(self, result.skillEvents, profile, Constants.SKILL_TYPE_CLASSIC, Constants.SKILL_MOVE100, skillOptions)
|
||||
elseif state.moves == 250 then
|
||||
TrySkill(self, result.skillEvents, profile, Constants.SKILL_TYPE_CLASSIC, Constants.SKILL_MOVE250, skillOptions)
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
function Scoring:ApplyCascade(state, cascadeResult, profile, options)
|
||||
assert(type(state) == "table", "cascade scoring requires game state")
|
||||
ValidateMode(state.gameMode)
|
||||
|
||||
@@ -9,6 +9,7 @@ local DEFAULT_CLEAR_DURATION = 0.1
|
||||
local DEFAULT_FALL_PER_CELL = 0.05
|
||||
local DEFAULT_MINIMUM_FALL_DURATION = 0.1
|
||||
local DEFAULT_EFFECT_INTERVAL = 0.025
|
||||
local DEFAULT_SWAP_DURATION = Constants.GEM_WIDTH / 150
|
||||
local HYPER_FRAME_COUNT = 40
|
||||
local EXPLOSION_FRAME_COUNT = 16
|
||||
local POWER_STAR_SIZE = 90
|
||||
@@ -122,6 +123,24 @@ local function CreateMoveAnimation(frame)
|
||||
return frame.bejeweledMoveAnimation
|
||||
end
|
||||
|
||||
local function CreateSwapAnimation(frame, rollback)
|
||||
local group = frame:CreateAnimationGroup()
|
||||
local outbound = group:CreateAnimation("Translation")
|
||||
outbound:SetOrder(1)
|
||||
local animation = {
|
||||
group = group,
|
||||
outbound = outbound,
|
||||
}
|
||||
if rollback then
|
||||
animation.returnTranslation = group:CreateAnimation("Translation")
|
||||
animation.returnTranslation:SetOrder(2)
|
||||
frame.bejeweledSwapRollbackAnimation = animation
|
||||
else
|
||||
frame.bejeweledSwapForwardAnimation = animation
|
||||
end
|
||||
return animation
|
||||
end
|
||||
|
||||
local function FinishPending(runner, run, onFinished)
|
||||
if runner.active ~= run or run.cancelled then
|
||||
return
|
||||
@@ -154,10 +173,12 @@ function Animations:New(gemPool, options)
|
||||
instance.fallPerCell = options.fallPerCell or DEFAULT_FALL_PER_CELL
|
||||
instance.minimumFallDuration = options.minimumFallDuration or DEFAULT_MINIMUM_FALL_DURATION
|
||||
instance.effectInterval = options.effectInterval or DEFAULT_EFFECT_INTERVAL
|
||||
instance.swapDuration = options.swapDuration or DEFAULT_SWAP_DURATION
|
||||
assert(type(instance.clearDuration) == "number" and instance.clearDuration > 0, "clear duration must be positive")
|
||||
assert(type(instance.fallPerCell) == "number" and instance.fallPerCell > 0, "fall duration per cell must be positive")
|
||||
assert(type(instance.minimumFallDuration) == "number" and instance.minimumFallDuration > 0, "minimum fall duration must be positive")
|
||||
assert(type(instance.effectInterval) == "number" and instance.effectInterval > 0, "effect interval must be positive")
|
||||
assert(type(instance.swapDuration) == "number" and instance.swapDuration > 0, "swap duration must be positive")
|
||||
instance.callbacks = CopyCallbacks({}, options)
|
||||
instance.generation = 0
|
||||
instance.active = nil
|
||||
@@ -617,6 +638,66 @@ function Animations:PlayStep(run, stepIndex)
|
||||
end)
|
||||
end
|
||||
|
||||
function Animations:PlaySwap(firstX, firstY, secondX, secondY, rollback, finalGrid, callbacks)
|
||||
AssertCoordinate(firstX, Constants.GRID_WIDTH, "first swap column")
|
||||
AssertCoordinate(firstY, Constants.GRID_HEIGHT, "first swap row")
|
||||
AssertCoordinate(secondX, Constants.GRID_WIDTH, "second swap column")
|
||||
AssertCoordinate(secondY, Constants.GRID_HEIGHT, "second swap row")
|
||||
assert(math.abs(firstX - secondX) + math.abs(firstY - secondY) == 1, "swap animation requires adjacent cells")
|
||||
assert(type(finalGrid) == "table" and type(finalGrid.Get) == "function", "swap animation requires the final grid")
|
||||
if self.active then
|
||||
self:Cancel("superseded")
|
||||
end
|
||||
|
||||
self.generation = self.generation + 1
|
||||
local run = {
|
||||
kind = "swap",
|
||||
generation = self.generation,
|
||||
finalGrid = finalGrid,
|
||||
callbacks = CopyCallbacks(self.callbacks, callbacks),
|
||||
activeGroups = {},
|
||||
activeExplosions = {},
|
||||
pending = 0,
|
||||
cancelled = false,
|
||||
completed = false,
|
||||
rollback = rollback and true or false,
|
||||
}
|
||||
self.active = run
|
||||
self.gemPool:SetInteractive(false)
|
||||
self.gemPool:SetSelection(nil)
|
||||
if run.callbacks.onPhase then
|
||||
run.callbacks.onPhase(run.rollback and "swap-rollback" or "swap", 1, nil, run)
|
||||
end
|
||||
if self.active ~= run or run.cancelled then
|
||||
return run
|
||||
end
|
||||
|
||||
local groups = {}
|
||||
local movements = {
|
||||
{ frame = self.gemPool:GetFrame(firstX, firstY), offsetX = (secondX - firstX) * Constants.GEM_WIDTH, offsetY = -(secondY - firstY) * Constants.GEM_HEIGHT },
|
||||
{ frame = self.gemPool:GetFrame(secondX, secondY), offsetX = (firstX - secondX) * Constants.GEM_WIDTH, offsetY = -(firstY - secondY) * Constants.GEM_HEIGHT },
|
||||
}
|
||||
for index = 1, #movements do
|
||||
local movement = movements[index]
|
||||
local animation
|
||||
if run.rollback then
|
||||
animation = movement.frame.bejeweledSwapRollbackAnimation or CreateSwapAnimation(movement.frame, true)
|
||||
animation.returnTranslation:SetDuration(self.swapDuration)
|
||||
animation.returnTranslation:SetOffset(-movement.offsetX, -movement.offsetY)
|
||||
else
|
||||
animation = movement.frame.bejeweledSwapForwardAnimation or CreateSwapAnimation(movement.frame, false)
|
||||
end
|
||||
animation.outbound:SetDuration(self.swapDuration)
|
||||
animation.outbound:SetOffset(movement.offsetX, movement.offsetY)
|
||||
groups[#groups + 1] = animation.group
|
||||
end
|
||||
|
||||
self:WaitForPhase(run, groups, {}, function()
|
||||
self:CompleteRun(run)
|
||||
end)
|
||||
return run
|
||||
end
|
||||
|
||||
function Animations:Play(cascadeResult, finalGrid, callbacks)
|
||||
assert(type(finalGrid) == "table" and type(finalGrid.Get) == "function", "animation playback requires the final grid")
|
||||
if self.active then
|
||||
|
||||
@@ -216,6 +216,7 @@ function GemPool:RenderCell(column, row, cell, force)
|
||||
end
|
||||
|
||||
function GemPool:ResetPresentation(grid)
|
||||
self.selectedFrame = nil
|
||||
for row = 1, Constants.GRID_HEIGHT do
|
||||
for column = 1, Constants.GRID_WIDTH do
|
||||
local frame = self.frames[row][column]
|
||||
@@ -247,6 +248,21 @@ function GemPool:ResetPresentation(grid)
|
||||
return { changedCount = 0, changes = {} }
|
||||
end
|
||||
|
||||
function GemPool:SetSelection(column, row)
|
||||
if self.selectedFrame then
|
||||
self.selectedFrame.selector:Hide()
|
||||
self.selectedFrame = nil
|
||||
end
|
||||
if column == nil and row == nil then
|
||||
return nil
|
||||
end
|
||||
assert(column ~= nil and row ~= nil, "gem selection requires both coordinates")
|
||||
local frame = self:GetFrame(column, row)
|
||||
frame.selector:Show()
|
||||
self.selectedFrame = frame
|
||||
return frame
|
||||
end
|
||||
|
||||
function GemPool:Project(grid, force)
|
||||
assert(type(grid) == "table" and type(grid.Get) == "function", "gem projection requires a grid")
|
||||
local changes = {}
|
||||
|
||||
@@ -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 UI, animation, and input slices remain to be restored.
|
||||
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 hyper activation, remaining session states, HUD, and menu integration still need to be restored.
|
||||
|
||||
## 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, deterministic grid/match/cascade/scoring transitions, skills, levels, legacy-compatible audio cue scheduling, BackdropTemplate-safe UI chrome, persistent grid-to-gem texture projection, cancellable cascade sequencing, power/hyper presentation, and pooled explosion playback.
|
||||
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, deterministic grid/match/cascade/scoring transitions, 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, power/hyper presentation, and pooled explosion playback.
|
||||
|
||||
## Goal
|
||||
|
||||
@@ -18,7 +18,7 @@ The eventual addon will target current Retail/Mainline World of Warcraft while p
|
||||
- `Bejeweled/images/` — immutable legacy images and bundled font.
|
||||
- `Bejeweled/sounds/` — immutable legacy sounds.
|
||||
- `Bejeweled/Core/` — private addon initialization, constants, supported audio playback, and non-destructive SavedVariables defaulting.
|
||||
- `Bejeweled/Engine/` — deterministic gameplay state; currently the 8×8 grid, swaps, legal moves, legacy cell encoding, stable cascade resolution, legacy score formulas, statistics, skill gains, achievements, and level thresholds.
|
||||
- `Bejeweled/Engine/` — deterministic gameplay state; currently the 8×8 grid, selection/swap sessions, legal moves, legacy cell encoding, stable cascade resolution, legacy score formulas, move accounting, statistics, skill gains, achievements, and level thresholds.
|
||||
- `Bejeweled/UI/` — Retail-safe frame/rendering boundaries; currently shared backdrop construction, board tiles, persistent gem-frame projection, and recorded cascade-transition playback.
|
||||
- `docs/analysis/` — sequential batch reports, identifier ledger, and exact coverage schedule.
|
||||
- `docs/architecture.md` — module ownership and authoritative runtime data flow.
|
||||
|
||||
@@ -8,7 +8,7 @@ The modernization baseline is the authoritative WoW UI/API source snapshot for l
|
||||
| Timers | `C_Timer.After` and `C_Timer.NewTicker` are the supported timer primitives. | Replace legacy polling/on-update timing only after behavior-equivalence analysis. |
|
||||
| Addon sounds | `PlaySoundFile` continues to support addon-owned paths. Current Blizzard Mainline code calls `PlaySound` with identifiers from the current `SOUNDKIT` table. Obsolete Blizzard-internal paths are not stable. | `Core/Audio.lua` preserves all addon media paths and replaces the legacy `Sound\\Spells\\LevelUp.wav` call with `SOUNDKIT.UI_SCENARIO_STAGE_END`, falling back to `SOUNDKIT.UI_AUTO_QUEST_COMPLETE` when necessary. |
|
||||
| Metadata | Addon metadata access is provided by `C_AddOns`. | Route future metadata queries through `C_AddOns`. |
|
||||
| Animation | Frames expose `CreateAnimationGroup`; groups expose `CreateAnimation`, `Play`, `Stop`, and `SetScript`; translation animations expose `SetOffset`; alpha animations expose `SetFromAlpha` and `SetToAlpha`. | `UI/Animations.lua` reuses per-frame animation groups to replay cascade clear, gravity, and refill records, and removes completion scripts before cancellation. |
|
||||
| Animation | Frames expose `CreateAnimationGroup`; groups expose `CreateAnimation`, `Play`, `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 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. |
|
||||
| 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. |
|
||||
@@ -26,6 +26,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)
|
||||
- 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)
|
||||
- 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)
|
||||
|
||||
@@ -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`, `UI/Backdrops.lua`, `UI/GemPool.lua`, and the core transition runner in `UI/Animations.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`, `UI/Backdrops.lua`, `UI/GemPool.lua`, and `UI/Animations.lua`.
|
||||
|
||||
## Load order and ownership
|
||||
|
||||
@@ -12,15 +12,16 @@ The analysis phase gate is satisfied. This ownership map now governs runtime imp
|
||||
6. `Engine/Matches.lua` — pure legacy-order match detection, axis-overlap reporting, and power/hyper-gem classification; clearing and scoring remain downstream responsibilities.
|
||||
7. `Engine/Cascade.lua` — transactional clears, matched power-gem expansion, spawned-special preservation, fixed-cell gravity, bounded refill, and repeated transitions to a stable board. It emits logical movement/refill records but owns no animation or scoring.
|
||||
8. `Engine/Scoring.lua` — legacy score arithmetic, combo/mode/level multipliers, wire-compatible statistics, probabilistic skill gains, one-time achievements, rank advancement, and pending/explicit level transitions. It emits presentation events and owns no frames, text, sound, or chat publishing.
|
||||
9. `UI/Backdrops.lua` — backdrop-compatible frame construction and fresh-copy presets for tooltip, window, panel, slider, and level-border chrome. Every constructed frame explicitly inherits `BackdropTemplate`.
|
||||
10. `UI/GemPool.lua` — fixed allocation and reuse of the 64 interactive gem frames, the sixteen board-art tiles, input-handler attachment, and change-aware projection from authoritative grid cells into normal/hyper texture layers. Power-gem overlay animation remains downstream.
|
||||
11. `UI/Animations.lua` — deterministic clear/gravity/refill plans, reusable animation groups, 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 explosion phase barrier. Shards, lightwaves/lightning, hint bounce, and floating text remain follow-on presentation work; current fall timings are explicit modernization defaults pending in-game tuning.
|
||||
12. `UI/HUD.lua` — score, timer, level, status, hint, and achievement presentation.
|
||||
13. `UI/Compartment.lua` — addon-compartment click and hover callbacks.
|
||||
9. `Engine/Input.lua` — authoritative selection and move-session coordination, optimistic adjacent swaps, match validation, immediate engine rollback for invalid moves, move accounting, cascade/scoring handoff, and presentation locking. Hyper activation, pause/resume, levels, restore, and game-over remain follow-on session work.
|
||||
10. `UI/Backdrops.lua` — backdrop-compatible frame construction and fresh-copy presets for tooltip, window, panel, slider, and level-border chrome. Every constructed frame explicitly inherits `BackdropTemplate`.
|
||||
11. `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.
|
||||
12. `UI/Animations.lua` — deterministic swap/rollback and clear/gravity/refill plans, reusable animation groups, 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 explosion phase barrier. Shards, lightwaves/lightning, hint bounce, and floating text remain follow-on presentation work; current fall timings are explicit modernization defaults pending in-game tuning.
|
||||
13. `UI/HUD.lua` — score, timer, level, status, hint, and achievement presentation.
|
||||
14. `UI/Compartment.lua` — addon-compartment click and hover callbacks.
|
||||
|
||||
## Data flow
|
||||
|
||||
`SavedVariables initialization → engine-owned deterministic grid state → match/cascade/scoring transitions → UI rendering and animation → HUD/audio feedback`
|
||||
`SavedVariables initialization → engine-owned deterministic grid state → input/match/cascade/scoring transitions → UI rendering and animation → HUD/audio feedback`
|
||||
|
||||
The engine will own authoritative game state. UI frames will render state and report input; they will not become the source of gameplay truth. Audio and HUD reactions consume completed transitions.
|
||||
|
||||
|
||||
+132
-1
@@ -47,6 +47,7 @@ LoadAddonFile("Bejeweled/Engine/Grid.lua", addon)
|
||||
LoadAddonFile("Bejeweled/Engine/Matches.lua", addon)
|
||||
LoadAddonFile("Bejeweled/Engine/Cascade.lua", addon)
|
||||
LoadAddonFile("Bejeweled/Engine/Scoring.lua", addon)
|
||||
LoadAddonFile("Bejeweled/Engine/Input.lua", addon)
|
||||
LoadAddonFile("Bejeweled/UI/Backdrops.lua", addon)
|
||||
LoadAddonFile("Bejeweled/UI/GemPool.lua", addon)
|
||||
LoadAddonFile("Bejeweled/UI/Animations.lua", addon)
|
||||
@@ -191,6 +192,9 @@ local function CreateGemPoolFrame(frameType, name, parent, template)
|
||||
function animation:SetDuration(duration)
|
||||
self.duration = duration
|
||||
end
|
||||
function animation:SetOrder(order)
|
||||
self.order = order
|
||||
end
|
||||
function animation:SetOffset(offsetX, offsetY)
|
||||
self.offsetX = offsetX
|
||||
self.offsetY = offsetY
|
||||
@@ -321,6 +325,12 @@ AssertEqual(redGemFrame.glow.alpha, 0, "presentation reset left gem glow visible
|
||||
AssertEqual(redGemFrame.points[1][1], "TOPLEFT", "presentation reset gem anchor")
|
||||
AssertEqual(redGemFrame.points[1][2], 50, "presentation reset gem x position")
|
||||
AssertEqual(redGemFrame.points[1][3], -100, "presentation reset gem y position")
|
||||
gemPool:SetSelection(2, 3)
|
||||
assert(gemPool.selectedFrame == redGemFrame, "gem selection frame was not retained")
|
||||
assert(redGemFrame.selector.shown, "selected gem selector was not shown")
|
||||
gemPool:SetSelection(nil)
|
||||
assert(gemPool.selectedFrame == nil, "cleared gem selection was retained")
|
||||
assert(not redGemFrame.selector.shown, "cleared gem selector remained visible")
|
||||
|
||||
local playedFiles = {}
|
||||
local playedSoundKits = {}
|
||||
@@ -757,6 +767,114 @@ assert(not cancelledExplosionFrame.shown, "cancelled explosion remained visible"
|
||||
AssertEqual(#animations.activeExplosions, 0, "cancelled explosion remained active")
|
||||
AssertEqual(#animations.explosionPool, 1, "cancelled explosion was not pooled")
|
||||
|
||||
local inputGrid = addon.Grid:New()
|
||||
FillStablePattern(inputGrid)
|
||||
inputGrid:Set(2, 8, 1)
|
||||
inputGrid:Set(3, 8, 1)
|
||||
inputGrid:Set(1, 7, 1)
|
||||
assert(not inputGrid:HasAnyMatch(), "input test board started with a match")
|
||||
assert(inputGrid:IsLegalSwap(1, 7, 1, 8), "input test swap was not legal")
|
||||
local inputPool = addon.GemPool:New(gemPoolParent, {
|
||||
createFrame = CreateGemPoolFrame,
|
||||
createBoardTiles = false,
|
||||
})
|
||||
inputPool:Project(inputGrid, true)
|
||||
local inputAnimations = addon.Animations:New(inputPool, {
|
||||
createFrame = CreateGemPoolFrame,
|
||||
swapDuration = 0.2,
|
||||
})
|
||||
local inputProfile = addon.SavedVariables:CreateDefaultProfile()
|
||||
local inputScoringState = addon.Scoring:NewState(addon.Constants.GAME_MODE_CLASSIC)
|
||||
local inputRandomValues = { 2, 3, 4 }
|
||||
local inputRandomIndex = 0
|
||||
local function InputRandom(minimum, maximum)
|
||||
inputRandomIndex = inputRandomIndex + 1
|
||||
local value = inputRandomValues[((inputRandomIndex - 1) % #inputRandomValues) + 1]
|
||||
return math.max(minimum, math.min(maximum, value))
|
||||
end
|
||||
local inputSounds = {}
|
||||
local inputAudio = {
|
||||
Play = function(_, soundName)
|
||||
inputSounds[#inputSounds + 1] = soundName
|
||||
return true
|
||||
end,
|
||||
}
|
||||
local inputCompletions = {}
|
||||
local inputCascades = {}
|
||||
local input = addon.Input:New(inputGrid, inputPool, inputAnimations, {
|
||||
audio = inputAudio,
|
||||
scoringState = inputScoringState,
|
||||
profile = inputProfile,
|
||||
random = InputRandom,
|
||||
requireLegalMove = false,
|
||||
onMoveComplete = function(result)
|
||||
inputCompletions[#inputCompletions + 1] = result
|
||||
end,
|
||||
onCascadeResolved = function(result)
|
||||
inputCascades[#inputCascades + 1] = result.cascadeResult
|
||||
end,
|
||||
})
|
||||
local inputHandlers = input:CreateGemHandlers()
|
||||
assert(type(inputHandlers.onMouseDown) == "function", "input gem handler was not created")
|
||||
local firstSelection = inputHandlers.onMouseDown(inputPool:GetFrame(8, 1), "LeftButton")
|
||||
AssertEqual(firstSelection.status, "selected", "input first selection status")
|
||||
assert(inputPool:GetFrame(8, 1).selector.shown, "input selection did not show selector")
|
||||
local clearedSelection = input:HandleCell(1, 7)
|
||||
AssertEqual(clearedSelection.reason, "nonadjacent", "nonadjacent selection clear reason")
|
||||
assert(input:GetSelection() == nil, "nonadjacent click retained selection")
|
||||
|
||||
AssertEqual(input:HandleCell(1, 7).status, "selected", "legal swap source selection")
|
||||
local acceptedMove = input:HandleCell(1, 8)
|
||||
assert(acceptedMove.valid, "legal input move was rejected")
|
||||
AssertEqual(acceptedMove.status, "animating", "accepted input move status")
|
||||
assert(input:IsLocked(), "accepted input move did not lock input")
|
||||
AssertEqual(input:HandleCell(4, 4).status, "locked", "locked input accepted another cell")
|
||||
AssertEqual(inputScoringState.moves, 1, "accepted move count")
|
||||
assert(inputGrid:HasAnyMatch(), "accepted swap was not applied before presentation")
|
||||
AssertEqual(acceptedMove.swapRun.kind, "swap", "accepted swap animation kind")
|
||||
AssertEqual(inputPool:GetFrame(1, 7).bejeweledSwapForwardAnimation.outbound.duration, 0.2, "accepted swap duration")
|
||||
FinishAllGemAnimations()
|
||||
assert(not input:IsLocked(), "cascade completion left input locked")
|
||||
AssertEqual(acceptedMove.status, "complete", "accepted move completion status")
|
||||
AssertEqual(#inputCascades, 1, "accepted move cascade callback count")
|
||||
AssertEqual(#inputCompletions, 1, "accepted move completion callback count")
|
||||
assert(inputCascades[1].stable, "accepted move cascade did not stabilize")
|
||||
assert(not inputGrid:HasAnyMatch(), "accepted move cascade left a match")
|
||||
assert(inputPool:GetFrame(1, 1).mouseEnabled, "accepted move completion left gem input disabled")
|
||||
|
||||
local invalidFirstX, invalidFirstY, invalidSecondX, invalidSecondY
|
||||
for y = 1, addon.Constants.GRID_HEIGHT do
|
||||
for x = 1, addon.Constants.GRID_WIDTH - 1 do
|
||||
if not inputGrid:IsLegalSwap(x, y, x + 1, y) then
|
||||
invalidFirstX, invalidFirstY = x, y
|
||||
invalidSecondX, invalidSecondY = x + 1, y
|
||||
break
|
||||
end
|
||||
end
|
||||
if invalidFirstX then
|
||||
break
|
||||
end
|
||||
end
|
||||
assert(invalidFirstX, "input test could not find an invalid adjacent swap")
|
||||
local invalidBefore = inputGrid:ExportLegacyBoard()
|
||||
input:HandleCell(invalidFirstX, invalidFirstY)
|
||||
local rejectedMove = input:HandleCell(invalidSecondX, invalidSecondY)
|
||||
assert(not rejectedMove.valid, "invalid input move was accepted")
|
||||
assert(input:IsLocked(), "invalid rollback did not lock input")
|
||||
for y = 1, addon.Constants.GRID_HEIGHT do
|
||||
for x = 1, addon.Constants.GRID_WIDTH do
|
||||
AssertEqual(inputGrid:EncodeLegacyValue(inputGrid:Get(x, y)), invalidBefore[y][x], "invalid swap changed authoritative grid")
|
||||
end
|
||||
end
|
||||
local rollbackAnimation = inputPool:GetFrame(invalidFirstX, invalidFirstY).bejeweledSwapRollbackAnimation
|
||||
AssertEqual(rollbackAnimation.outbound.order, 1, "rollback outbound animation order")
|
||||
AssertEqual(rollbackAnimation.returnTranslation.order, 2, "rollback return animation order")
|
||||
FinishAllGemAnimations()
|
||||
AssertEqual(rejectedMove.status, "rejected", "invalid move completion status")
|
||||
assert(not input:IsLocked(), "invalid rollback left input locked")
|
||||
AssertEqual(inputScoringState.moves, 1, "invalid move changed move count")
|
||||
AssertEqual(inputSounds[#inputSounds], "Invalid", "invalid move sound")
|
||||
|
||||
FillStablePattern(cascadeGrid)
|
||||
for x = 2, 5 do
|
||||
cascadeGrid:Set(x, 8, 7)
|
||||
@@ -850,6 +968,16 @@ AssertEqual(scoringProfile.stats.totalGemsMatched, 3, "legacy refill-backed gem
|
||||
AssertEqual(scoringProfile.stats.gemMatch[1], 1, "per-color match statistic")
|
||||
AssertEqual(scoringProfile.skill.skillPoints, 1, "match-three skill gain")
|
||||
|
||||
local moveProfile = addon.SavedVariables:CreateDefaultProfile()
|
||||
local moveState = addon.Scoring:NewState(addon.Constants.GAME_MODE_CLASSIC, { moves = 99 })
|
||||
local recordedMove = addon.Scoring:RecordMove(moveState, moveProfile, { random = function() return 1 end })
|
||||
AssertEqual(recordedMove.moves, 100, "classic recorded move count")
|
||||
AssertEqual(recordedMove.skillEvents[1].index, addon.Constants.SKILL_MOVE100, "classic move-100 skill index")
|
||||
local timedMoveProfile = addon.SavedVariables:CreateDefaultProfile()
|
||||
local timedMoveState = addon.Scoring:NewState(addon.Constants.GAME_MODE_TIMED, { moves = 4 })
|
||||
addon.Scoring:RecordMove(timedMoveState, timedMoveProfile)
|
||||
AssertEqual(timedMoveProfile.stats.timed.mostMoves, 5, "timed most-moves statistic")
|
||||
|
||||
local powerScoringProfile = addon.SavedVariables:CreateDefaultProfile()
|
||||
local powerScoringState = addon.Scoring:NewState(addon.Constants.GAME_MODE_CLASSIC)
|
||||
local powerScoring = addon.Scoring:ApplyCascade(powerScoringState, { steps = { powerCascade } }, powerScoringProfile, {
|
||||
@@ -962,17 +1090,20 @@ assert(addon.audio, "addon initialization did not create audio")
|
||||
assert(addon.backdrops == addon.Backdrops, "addon initialization did not install backdrops")
|
||||
assert(addon.gemPoolFactory == addon.GemPool, "addon initialization did not install GemPool")
|
||||
assert(addon.animationFactory == addon.Animations, "addon initialization did not install Animations")
|
||||
assert(addon.inputFactory == addon.Input, "addon initialization did not install Input")
|
||||
assert(eventFrame.registeredEvent == nil, "initializer event was not unregistered")
|
||||
local initializedGrid = addon.grid
|
||||
local initializedAudio = addon.audio
|
||||
local initializedBackdrops = addon.backdrops
|
||||
local initializedGemPoolFactory = addon.gemPoolFactory
|
||||
local initializedAnimationFactory = addon.animationFactory
|
||||
local initializedInputFactory = addon.inputFactory
|
||||
addon:Initialize({}, {})
|
||||
assert(addon.grid == initializedGrid, "addon initialization is not idempotent")
|
||||
assert(addon.audio == initializedAudio, "audio initialization is not idempotent")
|
||||
assert(addon.backdrops == initializedBackdrops, "backdrop initialization is not idempotent")
|
||||
assert(addon.gemPoolFactory == initializedGemPoolFactory, "GemPool initialization is not idempotent")
|
||||
assert(addon.animationFactory == initializedAnimationFactory, "Animations initialization is not idempotent")
|
||||
assert(addon.inputFactory == initializedInputFactory, "Input initialization is not idempotent")
|
||||
|
||||
print("Runtime verification passed: cascade animation, gem projection, UI backdrops, audio, SavedVariables, and deterministic gameplay engine.")
|
||||
print("Runtime verification passed: input sessions, cascade animation, gem projection, UI backdrops, audio, SavedVariables, and deterministic gameplay engine.")
|
||||
|
||||
@@ -27,6 +27,7 @@ try {
|
||||
"Engine\Matches.lua",
|
||||
"Engine\Cascade.lua",
|
||||
"Engine\Scoring.lua",
|
||||
"Engine\Input.lua",
|
||||
"UI\Backdrops.lua",
|
||||
"UI\GemPool.lua",
|
||||
"UI\Animations.lua"
|
||||
|
||||
Reference in New Issue
Block a user