diff --git a/client/src/BattleRoom.lua b/client/src/BattleRoom.lua index 9eb433cb5..03190c149 100644 --- a/client/src/BattleRoom.lua +++ b/client/src/BattleRoom.lua @@ -15,7 +15,7 @@ local GeneratorSource = require("common.engine.GeneratorSource") -- A Battle Room is a session of matches, keeping track of the room number, player settings, wins / losses etc ---@class BattleRoom : Signal ----@field mode GameMode +---@field mode GameMode The game mode configuration defining rules, player count, and match settings for this battle room ---@field players Player[] ---@field spectators string[] ---@field spectating boolean @@ -59,7 +59,8 @@ end) BattleRoom.states = { Setup = 1, MatchInProgress = 2 } function BattleRoom.createFromServerMessage(message) - local battleRoom = BattleRoom(message.gameMode) + local gameMode = GameModes.createFromServerData(message.gameMode) + local battleRoom = BattleRoom(gameMode) if message.spectate_request_granted then logger.debug("Joining a match as spectator") @@ -108,19 +109,8 @@ function BattleRoom.createFromServerMessage(message) p = Player(player.name, player.publicId or -i, false) end - -- order is important here as setting style will indirectly also override levelData so it needs to be before updateSettings - if gameMode.style ~= GameModes.Styles.CHOOSE then - p:setStyle(gameMode.style) - else - if player.settings.levelData then - if player.settings.levelData.frameConstants.GARBAGE_HOVER then - p:setStyle(GameModes.Styles.MODERN) - else - p:setStyle(GameModes.Styles.CLASSIC) - end - end - end - + -- updateSettings will set levelData which triggers levelDataChanged signal + -- which will automatically update style based on the levelData p:updateSettings(player.settings) if player.ratingInfo then @@ -141,30 +131,33 @@ function BattleRoom.createFromServerMessage(message) return battleRoom end -function BattleRoom.createLocalFromGameMode(gameMode, gameScene) +-- Creates a local (offline) BattleRoom from a GameMode configuration. +-- For single-player modes, uses the game's main local player. For multi-player modes, +-- creates temporary local players that don't persist settings changes. +---@param gameMode GameMode The game mode configuration defining rules and player count +---@param gameScene table? Optional scene class to use for matches (defaults to mode's gameScene) +---@param settingChangesUpdateConfig boolean? If true, setting changes update config (default: true). Only applies to single-player modes. +---@return BattleRoom? battleRoom The created battle room, or nil if input configuration assignment fails +function BattleRoom.createLocalFromGameMode(gameMode, gameScene, settingChangesUpdateConfig) + if settingChangesUpdateConfig == nil then + settingChangesUpdateConfig = true + end + local battleRoom = BattleRoom(gameMode, gameScene) - if gameMode.playerCount == 1 then + if settingChangesUpdateConfig and gameMode.playerCount == 1 then -- always use the game client's local player battleRoom:addPlayer(GAME.localPlayer) else -- with more than 1 local player we can't be sure which player is the "real" regular user -- so make them both local players that don't update config settings for i = 1, gameMode.playerCount do - local player = Player.getLocalPlayer() + local player = Player.createLocalPlayerFromConfig() player.name = loc("player_n", i) battleRoom:addPlayer(player) end end - if gameMode.style ~= GameModes.Styles.CHOOSE then - for i, player in ipairs(battleRoom.players) do - if player.human then - battleRoom.players[i]:setStyle(gameMode.style) - end - end - end - if battleRoom:assignInputConfigurations() then return battleRoom else @@ -404,28 +397,6 @@ function BattleRoom:createScene(match) end end --- sets the style of "level" presets the players select from --- 1 = classic --- 2 = modern --- longterm we want to abandon the concept of "style" on the player / battleRoom level --- just setting difficulty or level should set the levelData and done with it, style is a menu-only concept --- there is no technical reason why someone on level 10 shouldn't be able to play against someone on Hard --- for now it's a battleRoom wide setting and players have to match -function BattleRoom:setStyle(styleChoice) - -- style could be configurable per play instead but let's not for now - if self.mode.style == GameModes.Styles.CHOOSE then - self.style = styleChoice - self.onStyleChanged(styleChoice) - else - error("Trying to set difficulty style in a game mode that doesn't support style selection") - end -end - --- not player specific, so this gets a separate callback that can only be overwritten once --- so the UI can update and load up the different controls for it -function BattleRoom.onStyleChanged(style, player) -end - function BattleRoom:startLoadingNewAssets() if ModLoader.loading_mod == nil then for _, player in ipairs(self.players) do diff --git a/client/src/ClientMatch.lua b/client/src/ClientMatch.lua index 8fb72fa95..dc55b77bd 100644 --- a/client/src/ClientMatch.lua +++ b/client/src/ClientMatch.lua @@ -396,10 +396,10 @@ function ClientMatch:finalizeReplay() ---@cast player Player metadata.name = player.name metadata.publicId = player.publicId - if player.settings.style == GameModes.Styles.MODERN then - metadata.level = player.settings.level - else - metadata.difficulty = player.settings.difficulty + if stack.level then + metadata.level = stack.level + elseif stack.difficulty then + metadata.difficulty = stack.difficulty end metadata.analytics = player.stack.analytic.data ---@diagnostic disable-next-line: inject-field diff --git a/client/src/Game.lua b/client/src/Game.lua index 5ea3a22d6..36eeffb4f 100644 --- a/client/src/Game.lua +++ b/client/src/Game.lua @@ -11,6 +11,7 @@ require("client.src.mods.Theme") -- Not to be confused with "Match" which is the current battle / instance of the game. local consts = require("common.engine.consts") local GraphicsUtil = require("client.src.graphics.graphics_util") +local LevelPresets = require("common.data.LevelPresets") local class = require("common.lib.class") local logger = require("common.lib.logger") local analytics = require("client.src.analytics") @@ -257,7 +258,7 @@ end -- GAME.localPlayer is the standard player for battleRooms that don't get started from replays/spectate -- it basically represents the player that is operating the client (and thus binds to its configuration) function Game:initializeLocalPlayer() - self.localPlayer = Player.getLocalPlayer() + self.localPlayer = Player.createLocalPlayerFromConfig() self.localPlayer:connectSignal("selectedCharacterIdChanged", config, function(config, newId) config.character = newId end) self.localPlayer:connectSignal("selectedStageIdChanged", config, function(config, newId) config.stage = newId end) self.localPlayer:connectSignal("panelIdChanged", config, function(config, newId) config.panels = newId end) @@ -266,11 +267,17 @@ function Game:initializeLocalPlayer() self.localPlayer:connectSignal("difficultyChanged", config, function(config, difficulty) config.endless_difficulty = difficulty end) self.localPlayer:connectSignal("levelChanged", config, function(config, level) config.level = level end) self.localPlayer:connectSignal("wantsRankedChanged", config, function(config, wantsRanked) config.ranked = wantsRanked end) - self.localPlayer:connectSignal("styleChanged", config, function(config, style) - if style == GameModes.Styles.CLASSIC then - config.endless_level = nil - else - config.endless_level = config.level + + self.localPlayer:connectSignal("levelDataChanged", config, function(config, levelData, player) + local presetInfo = LevelPresets.getStyleAndPreset(levelData) + if presetInfo then + if presetInfo.style == GameModes.Styles.MODERN then + config.level = presetInfo.level + config.endless_level = presetInfo.level + else + config.endless_difficulty = presetInfo.difficulty + config.endless_level = nil + end end end) end @@ -305,21 +312,6 @@ function Game:createDirectoriesIfNeeded() end end -function Game:runUnitTests() - coroutine.yield("Running Unit Tests") - - -- GAME.localPlayer is the standard player for battleRooms that don't get started from replays/spectate - -- basically the player that is operating the client - GAME.localPlayer = Player.getLocalPlayer() - -- we need to overwrite the local player as all replay related tests need a non-local player - GAME.localPlayer.isLocal = false - - logger.info("Running Unit Tests...") - GAME.muteSound = true - --require("client.tests.Tests") - SoundController:applyConfigVolumes() -end - function Game:runPerformanceTests() coroutine.yield("Running Performance Tests") require("tests.StackReplayPerformanceTests") diff --git a/client/src/Player.lua b/client/src/Player.lua index a7711081d..375b5866e 100644 --- a/client/src/Player.lua +++ b/client/src/Player.lua @@ -107,12 +107,19 @@ function Player:createClientStack(engineStack) player = self, } - if self.settings.style == GameModes.Styles.MODERN then - args.level = self.settings.level - else - args.difficulty = self.settings.difficulty + local presetInfo = LevelPresets.getStyleAndPreset(self.settings.levelData) + if presetInfo then + if presetInfo.style == GameModes.Styles.MODERN then + if presetInfo.level then + args.level = self.settings.level + end + else + if presetInfo.difficulty then + args.difficulty = self.settings.difficulty + end + end end - + self.stack = PlayerStack(args) return self.stack @@ -144,7 +151,7 @@ end function Player:setLevelData(levelData) self.settings.levelData = levelData self:setSpeed(levelData.startingSpeed) - self:emitSignal("levelDataChanged", levelData) + self:emitSignal("levelDataChanged", levelData, self) end function Player:setSpeed(speed) @@ -172,18 +179,11 @@ end -- sets the style of "level" presets the player selects from -- 1 = classic -- 2 = modern --- longterm we want to abandon the concept of "style" on the player / battleRoom level --- just setting difficulty or level should set the levelData and done with it, style is a menu-only concept --- there is no technical reason why someone on level 10 shouldn't be able to play against someone on Hard +-- style is a menu-only concept for UI display +-- derived settings (levelData) should be updated by calling gameMode.updateLocalPlayersDerivedSettings function Player:setStyle(style) if style ~= self.settings.style then self.settings.style = style - if style == GameModes.Styles.MODERN then - self:setLevelData(LevelPresets.getModern(self.settings.level or config.level)) - else - self:setLevelData(LevelPresets.getClassic(self.settings.difficulty or config.endless_difficulty)) - self:setSpeed(self.settings.speed) - end self:emitSignal("styleChanged", style) end end @@ -233,7 +233,7 @@ function Player:unrestrictInputs() end ---@return Player -function Player.getLocalPlayer() +function Player.createLocalPlayerFromConfig() local player = Player(config.name, -1, true) player:setDifficulty(config.endless_difficulty) @@ -246,10 +246,8 @@ function Player.getLocalPlayer() player:setInputMethod(config.inputMethod) if config.endless_level then player:setStyle(GameModes.Styles.MODERN) - player:setLevelData(LevelPresets.getModern(player.settings.level)) else player:setStyle(GameModes.Styles.CLASSIC) - player:setLevelData(LevelPresets.getClassic(player.settings.difficulty)) player:setSpeed(config.endless_speed) end @@ -325,16 +323,6 @@ function Player:updateSettings(settings) end if settings.levelData ~= nil then - if settings.level ~= nil then - if settings.levelData.frameConstants.GARBAGE_HOVER then - self:setStyle(GameModes.Styles.MODERN) - self:setLevel(settings.level) - elseif settings.level <= LevelPresets.classicPresetCount then - self:setStyle(GameModes.Styles.CLASSIC) - self:setDifficulty(settings.level) - end - end - self:setLevelData(settings.levelData) end diff --git a/client/src/network/NetClient.lua b/client/src/network/NetClient.lua index 4bb83eab1..d785a431c 100644 --- a/client/src/network/NetClient.lua +++ b/client/src/network/NetClient.lua @@ -458,6 +458,7 @@ function NetClient:requestSpectate(roomNumber) end end +---@param gameMode GameMode function NetClient:requestRoom(gameMode) if self:isConnected() then self.tcpClient:sendRequest(ClientMessages.sendRoomRequest(gameMode)) diff --git a/client/src/scenes/CharacterSelect.lua b/client/src/scenes/CharacterSelect.lua index f41efaf34..8ceac0802 100644 --- a/client/src/scenes/CharacterSelect.lua +++ b/client/src/scenes/CharacterSelect.lua @@ -4,11 +4,11 @@ local class = require("common.lib.class") local logger = require("common.lib.logger") local tableUtils = require("common.lib.tableUtils") local GameModes = require("common.data.GameModes") +local LevelPresets = require("common.data.LevelPresets") local Scene = require("client.src.scenes.Scene") local ui = require("client.src.ui") local GraphicsUtil = require("client.src.graphics.graphics_util") local Character = require("client.src.mods.Character") -local LevelPresets = require("common.data.LevelPresets") -- The character select screen scene ---@class CharacterSelect : Scene @@ -57,7 +57,47 @@ function CharacterSelect:load() self.ui.cursors = {} self.ui.characterIcons = {} self.ui.playerInfos = {} + self:customLoad() + + for _, player in ipairs(self.players) do + if player:isHuman() then + if player.isLocal then + self:initializeFromLocalPlayerSettings(player) + end + player:connectSignal("levelDataChanged", self, self.onLevelDataChanged) + self:onLevelDataChanged(player.settings.levelData, player) + end + end +end + +function CharacterSelect:onLevelDataChanged(levelData, player) + local presetInfo = LevelPresets.getStyleAndPreset(levelData) + + if not presetInfo then + -- Custom levelData, default to current settings + return + end + + if presetInfo.style == GameModes.Styles.MODERN then + player:setStyle(GameModes.Styles.MODERN) + if presetInfo.level then + player:setLevel(presetInfo.level) + end + else + player:setStyle(GameModes.Styles.CLASSIC) + if presetInfo.difficulty then + player:setDifficulty(presetInfo.difficulty) + end + end + + self:refresh() +end + +function CharacterSelect:initializeFromLocalPlayerSettings(player) + player:setStyle(GameModes.Styles.MODERN) + player:setLevel(player.settings.level) + player:setLevelData(LevelPresets.getModern(player.settings.level)) end ---@param player Player @@ -893,7 +933,7 @@ function CharacterSelect:createSpeedSlider(player, height, min) return uiElement end -function CharacterSelect:createDifficultyCarousel(player, height) +function CharacterSelect:createDifficultyCarousel(player, height, getPresetFunc) local passengers = { { id = 1, uiElement = ui.Label({text = "easy", vAlign = "center", hAlign = "center"})}, { id = 2, uiElement = ui.Label({text = "normal", vAlign = "center", hAlign = "center"})}, @@ -918,25 +958,16 @@ function CharacterSelect:createDifficultyCarousel(player, height) -- Just update on every passenger change end - local updateDifficultyData = function(difficultyID) - local levelData = LevelPresets.getClassic(difficultyID) - player:setDifficulty(difficultyID) - if self.battleRoom.mode.name == "endless" and difficultyID == 1 then - -- Endless easy uses 5 colors instead of 6 - levelData:setColorCount(5) - -- and by extension also allows adjacent panels of the same colors - levelData:setAdjacentDenialFrequency(0) - end - player:setLevelData(levelData) - end difficultyCarousel.onPassengerUpdateCallback = function(carousel, selectedPassenger) - updateDifficultyData(selectedPassenger.id) + player:setDifficulty(selectedPassenger.id) + if getPresetFunc then + player:setLevelData(getPresetFunc(selectedPassenger.id)) + end GAME.theme:playMoveSfx() - self:refresh() end - -- Note that this updates the player level data which could be wrong before because of the weird endless case - -- its probably fine for now, but ideally the model should be right when the battle room is created - updateDifficultyData(difficultyCarousel.selectedId) + + -- to update the UI if code gets changed from the backend (e.g. network messages) + player:connectSignal("difficultyChanged", difficultyCarousel, difficultyCarousel.setPassengerById) return difficultyCarousel end diff --git a/client/src/scenes/CharacterSelectVsSelf.lua b/client/src/scenes/CharacterSelectVsSelf.lua index dda09e6e3..59a22c562 100644 --- a/client/src/scenes/CharacterSelectVsSelf.lua +++ b/client/src/scenes/CharacterSelectVsSelf.lua @@ -85,14 +85,13 @@ function CharacterSelectVsSelf:refresh() local level if self.battleRoom then level = self.battleRoom.players[1].settings.level - else - level = GAME.localPlayer.settings.level + self.lastScore = GAME.scores:lastVsScoreForLevel(level) + self.record = GAME.scores:recordVsScoreForLevel(level) + if self.ui.recordBox then + self.ui.recordBox:setLastResult(self.lastScore) + self.ui.recordBox:setRecord(self.record) + end end - - self.lastScore = GAME.scores:lastVsScoreForLevel(level) - self.record = GAME.scores:recordVsScoreForLevel(level) - self.ui.recordBox:setLastResult(self.lastScore) - self.ui.recordBox:setRecord(self.record) end return CharacterSelectVsSelf \ No newline at end of file diff --git a/client/src/scenes/EndlessGame.lua b/client/src/scenes/EndlessGame.lua index 655bf50d9..cd12d3dcf 100644 --- a/client/src/scenes/EndlessGame.lua +++ b/client/src/scenes/EndlessGame.lua @@ -16,7 +16,7 @@ function EndlessGame:customLoad() end function EndlessGame:onMatchEnded(match) - if match.players[1].settings.style == GameModes.Styles.CLASSIC then + if match.players[1].stack.difficulty then GAME.scores:saveEndlessScoreForLevel(match.players[1].stack.engine.score, match.players[1].stack.difficulty) end end diff --git a/client/src/scenes/EndlessMenu.lua b/client/src/scenes/EndlessMenu.lua index 626c9c600..92bd9d967 100644 --- a/client/src/scenes/EndlessMenu.lua +++ b/client/src/scenes/EndlessMenu.lua @@ -1,6 +1,7 @@ local CharacterSelect = require("client.src.scenes.CharacterSelect") local class = require("common.lib.class") local GameModes = require("common.data.GameModes") +local LevelPresets = require("common.data.LevelPresets") local ui = require("client.src.ui") -- Scene for the endless game setup menu @@ -71,7 +72,7 @@ function EndlessMenu:loadUserInterface() }) self.ui.difficultySelection:setTitle("difficulty") - local difficultyCarousel = self:createDifficultyCarousel(player, self.ui.grid.unitSize - self.ui.grid.unitMargin * 2 - self.ui.difficultySelection.height) + local difficultyCarousel = self:createDifficultyCarousel(player, self.ui.grid.unitSize - self.ui.grid.unitMargin * 2 - self.ui.difficultySelection.height, LevelPresets.getClassicEndless) self.ui.difficultySelection:addElement(difficultyCarousel, player) self.ui.levelSelection = ui.MultiPlayerSelectionWrapper({hFill = true, alignment = "top", hAlign = "center", vAlign = "top"}) @@ -79,25 +80,14 @@ function EndlessMenu:loadUserInterface() local levelSlider = self:createLevelSlider(player, 20, self.ui.grid.unitSize - self.ui.grid.unitMargin * 2 - self.ui.levelSelection.height) self.ui.levelSelection:addElement(levelSlider, player) - if player.settings.style == GameModes.Styles.MODERN then - self.ui.grid:createElementAt(6, 2, 3, 1, "levelSelection", self.ui.levelSelection, nil, true) - else - self.ui.grid:createElementAt(6, 2, 2, 1, "speedSelection", self.ui.speedSelection, nil, true) - self.ui.grid:createElementAt(8, 2, 1, 1, "difficultySelection", self.ui.difficultySelection, nil, true) - end - styleSelector.onValueChange = function(boolSelector, value) GAME.theme:playValidationSfx() - self.ui.grid:removeElementsIn(6, 2, 3, 1) if value and player.settings.style ~= GameModes.Styles.MODERN then - player:setStyle(GameModes.Styles.MODERN) - self.ui.grid:createElementAt(6, 2, 3, 1, "levelSelection", self.ui.levelSelection, nil, true) - self.ui.recordBox:setVisibility(false) + -- Set levelData for modern style - this will trigger levelDataChanged signal which updates UI + player:setLevelData(LevelPresets.getModern(player.settings.level)) elseif value == false and player.settings.style ~= GameModes.Styles.CLASSIC then - player:setStyle(GameModes.Styles.CLASSIC) - self.ui.grid:createElementAt(6, 2, 2, 1, "speedSelection", self.ui.speedSelection, nil, true) - self.ui.grid:createElementAt(8, 2, 1, 1, "difficultySelection", self.ui.difficultySelection, nil, true) - self.ui.recordBox:setVisibility(true) + -- Set levelData for classic endless style - this will trigger levelDataChanged signal which updates UI + player:setLevelData(LevelPresets.getClassicEndless(player.settings.difficulty)) end end @@ -124,19 +114,42 @@ function EndlessMenu:loadUserInterface() self.ui.cursors[1].raise2Callback = function() self.ui.characterGrid:turnPage(1) end + + player:connectSignal("styleChanged", self, self.onStyleChanged) + self:onStyleChanged(player.settings.style, player) +end + +function EndlessMenu:onStyleChanged(style, player) + if style == GameModes.Styles.MODERN then + self.ui.grid:removeElementsIn(6, 2, 3, 1) + self.ui.grid:createElementAt(6, 2, 3, 1, "levelSelection", self.ui.levelSelection, nil, true) + self.ui.recordBox:setVisibility(false) + else + self.ui.grid:removeElementsIn(6, 2, 3, 1) + self.ui.grid:createElementAt(6, 2, 2, 1, "speedSelection", self.ui.speedSelection, nil, true) + self.ui.grid:createElementAt(8, 2, 1, 1, "difficultySelection", self.ui.difficultySelection, nil, true) + self.ui.recordBox:setVisibility(true) + end +end + +function EndlessMenu:initializeFromLocalPlayerSettings(player) + if player.settings.style == GameModes.Styles.MODERN then + player:setLevelData(LevelPresets.getModern(player.settings.level)) + else + player:setLevelData(LevelPresets.getClassicEndless(player.settings.difficulty)) + end end function EndlessMenu:refresh() - local difficulty if self.battleRoom then - difficulty = self.battleRoom.players[1].settings.difficulty - else - difficulty = GAME.localPlayer.settings.difficulty + local difficulty = self.battleRoom.players[1].settings.difficulty + self.lastScore = GAME.scores:lastEndlessForLevel(difficulty) + self.record = GAME.scores:recordEndlessForLevel(difficulty) + if self.ui.recordBox then + self.ui.recordBox:setLastResult(self.lastScore) + self.ui.recordBox:setRecord(self.record) + end end - self.lastScore = GAME.scores:lastEndlessForLevel(difficulty) - self.record = GAME.scores:recordEndlessForLevel(difficulty) - self.ui.recordBox:setLastResult(self.lastScore) - self.ui.recordBox:setRecord(self.record) end return EndlessMenu \ No newline at end of file diff --git a/client/src/scenes/ReplayBrowser.lua b/client/src/scenes/ReplayBrowser.lua index 30d42ad3f..14f0c8d11 100644 --- a/client/src/scenes/ReplayBrowser.lua +++ b/client/src/scenes/ReplayBrowser.lua @@ -223,8 +223,10 @@ function ReplayBrowser:draw() if player.level then GraphicsUtil.print(loc("rp_browser_info_level", player.level), menu_x + offsetX, menu_y + 95) else - GraphicsUtil.print(loc("rp_browser_info_speed", stack.levelData.startingSpeed), menu_x + offsetX, menu_y + 95) - GraphicsUtil.print(loc("rp_browser_info_difficulty", player.difficulty), menu_x + offsetX, menu_y + 110) + if player.difficulty then + GraphicsUtil.print(loc("rp_browser_info_speed", stack.levelData.startingSpeed), menu_x + offsetX, menu_y + 95) + GraphicsUtil.print(loc("rp_browser_info_difficulty", player.difficulty), menu_x + offsetX, menu_y + 110) + end end else ---@cast player SimulatedStackMetadata diff --git a/client/src/scenes/TimeAttackGame.lua b/client/src/scenes/TimeAttackGame.lua index 504f77767..1fd7af4a8 100644 --- a/client/src/scenes/TimeAttackGame.lua +++ b/client/src/scenes/TimeAttackGame.lua @@ -16,7 +16,7 @@ function TimeAttackGame:customLoad() end function TimeAttackGame:onMatchEnded(match) - if match.players[1].settings.style == GameModes.Styles.CLASSIC then + if match.players[1].stack.difficulty then GAME.scores:saveTimeAttack1PScoreForLevel(match.players[1].stack.engine.score, match.players[1].stack.difficulty) end end diff --git a/client/src/scenes/TimeAttackMenu.lua b/client/src/scenes/TimeAttackMenu.lua index 75904d97a..1ab39f816 100644 --- a/client/src/scenes/TimeAttackMenu.lua +++ b/client/src/scenes/TimeAttackMenu.lua @@ -1,6 +1,7 @@ local class = require("common.lib.class") local CharacterSelect = require("client.src.scenes.CharacterSelect") local GameModes = require("common.data.GameModes") +local LevelPresets = require("common.data.LevelPresets") local ui = require("client.src.ui") -- Scene for the time attack game setup menu @@ -70,7 +71,7 @@ function TimeAttackMenu:loadUserInterface() }) self.ui.difficultySelection:setTitle("difficulty") - local difficultyCarousel = self:createDifficultyCarousel(player, self.ui.grid.unitSize - self.ui.grid.unitMargin * 2 - self.ui.difficultySelection.height) + local difficultyCarousel = self:createDifficultyCarousel(player, self.ui.grid.unitSize - self.ui.grid.unitMargin * 2 - self.ui.difficultySelection.height, LevelPresets.getClassic) self.ui.difficultySelection:addElement(difficultyCarousel, player) self.ui.levelSelection = ui.MultiPlayerSelectionWrapper({hFill = true, alignment = "top", hAlign = "center", vAlign = "top"}) @@ -78,25 +79,14 @@ function TimeAttackMenu:loadUserInterface() local levelSlider = self:createLevelSlider(player, 20, self.ui.grid.unitSize - self.ui.grid.unitMargin * 2 - self.ui.levelSelection.height) self.ui.levelSelection:addElement(levelSlider, player) - if player.settings.style == GameModes.Styles.MODERN then - self.ui.grid:createElementAt(6, 2, 3, 1, "levelSelection", self.ui.levelSelection, nil, true) - else - self.ui.grid:createElementAt(6, 2, 2, 1, "speedSelection", self.ui.speedSelection, nil, true) - self.ui.grid:createElementAt(8, 2, 1, 1, "difficultySelection", self.ui.difficultySelection, nil, true) - end - styleSelector.onValueChange = function(boolSelector, value) GAME.theme:playValidationSfx() - self.ui.grid:removeElementsIn(6, 2, 3, 1) if value and player.settings.style ~= GameModes.Styles.MODERN then - player:setStyle(GameModes.Styles.MODERN) - self.ui.grid:createElementAt(6, 2, 3, 1, "levelSelection", self.ui.levelSelection, nil, true) - self.ui.recordBox:setVisibility(false) + -- Set levelData for modern style - this will trigger levelDataChanged signal which updates UI + player:setLevelData(LevelPresets.getModern(player.settings.level)) elseif value == false and player.settings.style ~= GameModes.Styles.CLASSIC then - player:setStyle(GameModes.Styles.CLASSIC) - self.ui.grid:createElementAt(6, 2, 2, 1, "speedSelection", self.ui.speedSelection, nil, true) - self.ui.grid:createElementAt(8, 2, 1, 1, "difficultySelection", self.ui.difficultySelection, nil, true) - self.ui.recordBox:setVisibility(true) + -- Set levelData for classic style - this will trigger levelDataChanged signal which updates UI + player:setLevelData(LevelPresets.getClassic(player.settings.difficulty)) end end @@ -123,20 +113,42 @@ function TimeAttackMenu:loadUserInterface() self.ui.cursors[1].raise2Callback = function() self.ui.characterGrid:turnPage(1) end + + player:connectSignal("styleChanged", self, self.onStyleChanged) + self:onStyleChanged(player.settings.style, player) end -function TimeAttackMenu:refresh() - local difficulty - if self.battleRoom then - difficulty = self.battleRoom.players[1].settings.difficulty +function TimeAttackMenu:onStyleChanged(style, player) + if style == GameModes.Styles.MODERN then + self.ui.grid:removeElementsIn(6, 2, 3, 1) + self.ui.grid:createElementAt(6, 2, 3, 1, "levelSelection", self.ui.levelSelection, nil, true) + self.ui.recordBox:setVisibility(false) else - difficulty = GAME.localPlayer.settings.difficulty + self.ui.grid:removeElementsIn(6, 2, 3, 1) + self.ui.grid:createElementAt(6, 2, 2, 1, "speedSelection", self.ui.speedSelection, nil, true) + self.ui.grid:createElementAt(8, 2, 1, 1, "difficultySelection", self.ui.difficultySelection, nil, true) + self.ui.recordBox:setVisibility(true) end +end - self.lastScore = GAME.scores:lastTimeAttack1PForLevel(difficulty) - self.record = GAME.scores:recordTimeAttack1PForLevel(difficulty) - self.ui.recordBox:setLastResult(self.lastScore) - self.ui.recordBox:setRecord(self.record) +function TimeAttackMenu:initializeFromLocalPlayerSettings(player) + if player.settings.style == GameModes.Styles.MODERN then + player:setLevelData(LevelPresets.getModern(player.settings.level)) + else + player:setLevelData(LevelPresets.getClassic(player.settings.difficulty)) + end +end + +function TimeAttackMenu:refresh() + if self.battleRoom then + local difficulty = self.battleRoom.players[1].settings.difficulty + self.lastScore = GAME.scores:lastTimeAttack1PForLevel(difficulty) + self.record = GAME.scores:recordTimeAttack1PForLevel(difficulty) + if self.ui.recordBox then + self.ui.recordBox:setLastResult(self.lastScore) + self.ui.recordBox:setRecord(self.record) + end + end end return TimeAttackMenu \ No newline at end of file diff --git a/client/src/scenes/VsSelfGame.lua b/client/src/scenes/VsSelfGame.lua index b638a7345..c61dba5f1 100644 --- a/client/src/scenes/VsSelfGame.lua +++ b/client/src/scenes/VsSelfGame.lua @@ -16,7 +16,9 @@ end function VsSelfGame:onMatchEnded(match) local P1 = match.players[1].stack - GAME.scores:saveVsSelfScoreForLevel(P1.analytic.data.sent_garbage_lines, P1.level) + if P1.level then + GAME.scores:saveVsSelfScoreForLevel(P1.analytic.data.sent_garbage_lines, P1.level) + end end return VsSelfGame \ No newline at end of file diff --git a/client/src/ui/Grid.lua b/client/src/ui/Grid.lua index 8d1d1cbbd..2dfc7a2bf 100644 --- a/client/src/ui/Grid.lua +++ b/client/src/ui/Grid.lua @@ -91,8 +91,12 @@ function Grid:drawSelf() end end --- removes all gridElements overlapping with the specified box --- the box is top left anchored +--- removes all gridElements overlapping with the specified box +--- the box is top left anchored +---@param x integer +---@param y integer +---@param width integer +---@param height integer function Grid:removeElementsIn(x, y, width, height) height = height or 1 width = width or 1 diff --git a/client/tests/PlayerSettingsTests.lua b/client/tests/PlayerSettingsTests.lua new file mode 100644 index 000000000..680b0dfa7 --- /dev/null +++ b/client/tests/PlayerSettingsTests.lua @@ -0,0 +1,108 @@ +local Player = require("client.src.Player") +local GameModes = require("common.data.GameModes") +local LevelPresets = require("common.data.LevelPresets") +local CharacterSelect = require("client.src.scenes.CharacterSelect") +local BattleRoom = require("client.src.BattleRoom") + +local function testDifficultyCarouselShouldNotMutatePlayerOnCreation() + + local player = Player("TestPlayer", 1, true) + + local gameMode = GameModes.getPreset("ONE_PLAYER_ENDLESS") + local battleRoom = BattleRoom(gameMode) + battleRoom:addPlayer(player) + + local characterSelect = CharacterSelect({battleRoom = battleRoom}) + + local originalLevel = player.settings.level + local originalDifficulty = player.settings.difficulty + local originalStyle = player.settings.style + local originalGarbageHover = player.settings.levelData.frameConstants.GARBAGE_HOVER + local originalColorCount = player.settings.levelData.colors + + local _ = characterSelect:createDifficultyCarousel(player, 100) + + assert(player.settings.level == originalLevel, "createDifficultyCarousel should not change level, but changed from " .. originalLevel .. " to " .. player.settings.level) + assert(player.settings.difficulty == originalDifficulty, "createDifficultyCarousel should not change difficulty, but changed from " .. tostring(originalDifficulty) .. " to " .. tostring(player.settings.difficulty)) + assert(player.settings.style == originalStyle, "createDifficultyCarousel should not change style, but changed from " .. originalStyle .. " to " .. player.settings.style) + assert(player.settings.levelData.frameConstants.GARBAGE_HOVER == originalGarbageHover, "createDifficultyCarousel should not change GARBAGE_HOVER, but changed from " .. tostring(originalGarbageHover) .. " to " .. tostring(player.settings.levelData.frameConstants.GARBAGE_HOVER)) + assert(player.settings.levelData.colors == originalColorCount, "createDifficultyCarousel should not change color count, but changed from " .. originalColorCount .. " to " .. player.settings.levelData.colors) + + battleRoom:shutdown() +end + +testDifficultyCarouselShouldNotMutatePlayerOnCreation() + +local function testEndlessModeClassicDifficulty1SetsCorrectSettings() + + local gameMode = GameModes.getPreset("ONE_PLAYER_ENDLESS") + local battleRoom = BattleRoom.createLocalFromGameMode(gameMode, nil, false) + + assert(battleRoom ~= nil, "BattleRoom should be created successfully") + assert(#battleRoom.players == 1, "BattleRoom should have exactly 1 player") + + local battleRoomPlayer = battleRoom.players[1] + + battleRoomPlayer:setLevelData(LevelPresets.getClassicEndless(1)) + + assert(battleRoomPlayer.settings.levelData.colors == 5, + "Endless mode with classic difficulty 1 should have 5 colors, but got " .. + tostring(battleRoomPlayer.settings.levelData.colors)) + + assert(battleRoomPlayer.settings.levelData.adjacentDenialFrequency == 0, + "Endless mode with classic difficulty 1 should have adjacent denial frequency of 0, but got " .. + tostring(battleRoomPlayer.settings.levelData.adjacentDenialFrequency)) + + battleRoom:shutdown() +end + +testEndlessModeClassicDifficulty1SetsCorrectSettings() + +local function testVsSelfChangesEndlessClassicSettingsToModern() + -- First create an endless battle room with classic difficulty 1 + local gameMode = GameModes.getPreset("ONE_PLAYER_ENDLESS") + local endlessBattleRoom = BattleRoom.createLocalFromGameMode(gameMode, nil, false) + + assert(endlessBattleRoom ~= nil, "Endless BattleRoom should be created successfully") + + local endlessPlayer = endlessBattleRoom.players[1] + + -- Set to classic difficulty 1 (simulating what happens in endless mode) + endlessPlayer:setLevelData(LevelPresets.getClassicEndless(1)) + + -- Verify endless settings + assert(endlessPlayer.settings.style == GameModes.Styles.CLASSIC, + "Player should have classic style before vs self") + assert(endlessPlayer.settings.levelData.colors == 5, + "Player should have 5 colors before vs self") + assert(endlessPlayer.settings.levelData.adjacentDenialFrequency == 0, + "Player should have adjacent denial frequency of 0 before vs self") + + endlessBattleRoom:shutdown() + + -- Now create a vs self battle room which should change settings to modern + local vsSelfGameMode = GameModes.getPreset("ONE_PLAYER_VS_SELF") + local vsSelfBattleRoom = BattleRoom.createLocalFromGameMode(vsSelfGameMode, nil, false) + + assert(vsSelfBattleRoom ~= nil, "Vs self BattleRoom should be created successfully") + + local vsSelfPlayer = vsSelfBattleRoom.players[1] + vsSelfPlayer:setLevelData(LevelPresets.getModern(10)) + + assert(vsSelfPlayer.settings.levelData.frameConstants.GARBAGE_HOVER ~= nil, + "Modern style should have GARBAGE_HOVER set") + + -- Modern levels have 6 colors (not 5 like endless classic difficulty 1) + assert(vsSelfPlayer.settings.levelData.colors == 6, + "Vs self should change color count to 6 (modern default), but got " .. + tostring(vsSelfPlayer.settings.levelData.colors)) + + -- Modern levels have non-zero adjacent denial frequency + assert(vsSelfPlayer.settings.levelData.adjacentDenialFrequency > 0, + "Vs self should have adjacent denial frequency > 0 (modern default), but got " .. + tostring(vsSelfPlayer.settings.levelData.adjacentDenialFrequency)) + + vsSelfBattleRoom:shutdown() +end + +testVsSelfChangesEndlessClassicSettingsToModern() diff --git a/client/tests/StackGraphicsTests.lua b/client/tests/StackGraphicsTests.lua index ddaa19a0d..09db7e0bc 100644 --- a/client/tests/StackGraphicsTests.lua +++ b/client/tests/StackGraphicsTests.lua @@ -26,7 +26,7 @@ local function createEndlessClientMatch(playerCount, theme) playerCount = 1 end for i = 1, playerCount do - local player = Player.getLocalPlayer() + local player = Player.createLocalPlayerFromConfig() player.isLocal = false player:setLevel(10) player:setLevelData(LevelPresets.getModern(10)) diff --git a/common/data/GameModes.lua b/common/data/GameModes.lua index 398ffbb77..2e4efbc70 100644 --- a/common/data/GameModes.lua +++ b/common/data/GameModes.lua @@ -1,3 +1,4 @@ +local class = require("common.lib.class") local MatchRules = require("common.data.MatchRules") local TIME_ATTACK_TIME = 120 @@ -13,6 +14,25 @@ local GameModes = {} ---@field gameScene string ---@field style Styles ---@field richPresenceLabel string? +---@field updateLocalPlayersDerivedSettings function +local GameMode = class(function(self, properties) + for key, value in pairs(properties) do + self[key] = value + end +end) + +-- Returns a copy of the game mode data suitable for JSON serialization +-- Removes all methods/functions from the copied data +---@return table +function GameMode:getGameModeJSONData() + local gameModeData = deepcpy(self) + for key, value in pairs(gameModeData) do + if type(value) == "function" then + gameModeData[key] = nil + end + end + return gameModeData +end -- longterm we want to abandon the concept of "style" on the engine and room setup level -- the engine only cares about levelData, style is a menu-only concept @@ -24,8 +44,7 @@ local Styles = { CHOOSE = 0, CLASSIC = 1, MODERN = 2} local StackInteractions = { NONE = 0, VERSUS = 1, SELF = 2, ATTACK_ENGINE = 3 } ---@type GameMode -local OnePlayerVsSelf = { - style = Styles.MODERN, +local OnePlayerVsSelf = GameMode({ gameScene = "VsSelfGame", richPresenceLabel = "1p vs self", -- loc("mm_1_vs"), name = "vsSelf", @@ -40,12 +59,12 @@ local OnePlayerVsSelf = { stackWinConditions = {}, stackSetupModifications = {}, doCountdown = true, - } -} + }, + +}) ---@type GameMode -local OnePlayerTimeAttack = { - style = Styles.CHOOSE, +local OnePlayerTimeAttack = GameMode({ gameScene = "TimeAttackGame", richPresenceLabel = "Time Attack", -- loc("mm_1_time"), name = "timeattack", @@ -60,12 +79,12 @@ local OnePlayerTimeAttack = { stackWinConditions = {}, stackSetupModifications = {}, doCountdown = true, - } -} + }, + +}) ---@type GameMode -local OnePlayerEndless = { - style = Styles.CHOOSE, +local OnePlayerEndless = GameMode({ gameScene = "EndlessGame", richPresenceLabel = "Endless", -- loc("mm_1_endless"), name = "endless", @@ -80,12 +99,12 @@ local OnePlayerEndless = { stackWinConditions = {}, stackSetupModifications = {}, doCountdown = true, - } -} + }, + +}) ---@type GameMode -local OnePlayerTraining = { - style = Styles.MODERN, +local OnePlayerTraining = GameMode({ gameScene = "GameBase", richPresenceLabel = "Training", -- loc("mm_1_training"), name = "training", @@ -100,13 +119,13 @@ local OnePlayerTraining = { stackWinConditions = {}, stackSetupModifications = {}, doCountdown = true, - } -} + }, + +}) ---@type GameMode -local OnePlayerPuzzle = { +local OnePlayerPuzzle = GameMode({ -- flags for battleRoom to evaluate and in some cases offer UI for - style = Styles.MODERN, richPresenceLabel = "Puzzle", -- loc("mm_1_puzzle"), gameScene = "PuzzleGame", name = "puzzle", @@ -124,12 +143,12 @@ local OnePlayerPuzzle = { -- these are extended based on the loaded puzzle stackSetupModifications = {}, doCountdown = false, - } -} + }, + +}) ---@type GameMode -local OnePlayerChallenge = { - style = Styles.MODERN, +local OnePlayerChallenge = GameMode({ gameScene = "Game1pChallenge", richPresenceLabel = "Challenge Mode", -- loc("mm_1_challenge_mode"), name = "challenge", @@ -144,12 +163,12 @@ local OnePlayerChallenge = { stackWinConditions = {}, stackSetupModifications = {}, doCountdown = true, - } -} + }, + +}) ---@type GameMode -local TwoPlayerVersus = { - style = Styles.MODERN, +local TwoPlayerVersus = GameMode({ gameScene = "GameBase", richPresenceLabel = "2p versus", -- loc("mm_2_vs"), name = "VS", @@ -164,11 +183,11 @@ local TwoPlayerVersus = { stackWinConditions = {}, stackSetupModifications = {}, doCountdown = true - } -} + }, + +}) ---@type GameMode -local TwoPlayerTimeAttack = { - style = Styles.MODERN, +local TwoPlayerTimeAttack = GameMode({ gameScene = "TimeAttackGame", richPresenceLabel = "2p Time Attack", -- loc("mm_2_time"), name = "2p_timeattack", @@ -182,8 +201,9 @@ local TwoPlayerTimeAttack = { stackWinConditions = {}, stackSetupModifications = {}, doCountdown = true, - } -} + }, + +}) GameModes.Styles = Styles GameModes.StackInteractions = StackInteractions @@ -213,4 +233,27 @@ function GameModes.getPreset(mode) return deepcpy(privateGameModes[mode]) end +-- Creates a GameMode from server message data by loading the preset and applying overrides +---@param gameModeData table The game mode data from the server message +---@return GameMode +function GameModes.createFromServerData(gameModeData) + local preset = nil + for _, gameMode in pairs(privateGameModes) do + if gameMode.name == gameModeData.name then + preset = gameMode + break + end + end + + assert(preset, "Unknown game mode name: " .. tostring(gameModeData.name)) + + local result = deepcpy(preset) + + for key, value in pairs(gameModeData) do + result[key] = value + end + + return result +end + return GameModes diff --git a/common/data/LevelPresets.lua b/common/data/LevelPresets.lua index fa49c68d8..380ac1aea 100644 --- a/common/data/LevelPresets.lua +++ b/common/data/LevelPresets.lua @@ -1,6 +1,7 @@ -- this file documents presets for level data local LevelData = require("common.data.LevelData") local JsonSafePrecision = require("common.data.JsonSafePrecision") +local GameModes = require("common.data.GameModes") ---@type LevelData[] local modern = {} @@ -326,4 +327,71 @@ end LevelPresets.classicPresetCount = #classic +---@type (table) +local classicEndless = {} +-- Deep copy from classic presets and modify only what's different for endless mode +classicEndless[1] = deepcpy(classic[1]) +-- Endless easy uses 5 colors instead of 6 +classicEndless[1]:setColorCount(5) +-- and allows adjacent panels of the same colors +classicEndless[1]:setAdjacentDenialFrequency(0) +classicEndless.easy = classicEndless[1] + +-- Normal, hard, and ex are identical to classic mode for endless +classicEndless[2] = deepcpy(classic[2]) +classicEndless.normal = classicEndless[2] + +classicEndless[3] = deepcpy(classic[3]) +classicEndless.hard = classicEndless[3] + +classicEndless[4] = deepcpy(classic[4]) +classicEndless.ex = classicEndless[4] + +---@param difficulty number | string the difficulty expressed as index 1 2 3 4 or easy normal hard ex +---@return LevelData # a deepcopy of the classic endless preset +function LevelPresets.getClassicEndless(difficulty) + assert(classicEndless[difficulty], "trying to load inexistent difficulty preset" .. difficulty) + return deepcpy(classicEndless[difficulty]) +end + +LevelPresets.classicEndlessPresetCount = #classicEndless + +---@class PresetInfo +---@field style Styles +---@field level integer? +---@field difficulty integer? +---@field isEndless boolean? + +---@param levelData LevelData +---@return PresetInfo? # style and preset information, or nil if levelData doesn't match any preset +function LevelPresets.getStyleAndPreset(levelData) + if not levelData then + return nil + end + + -- Check modern presets + for level = 1, #modern do + if LevelData.__eq(levelData, modern[level]) then + return {style = GameModes.Styles.MODERN, level = level, difficulty = nil, isEndless = false} + end + end + + -- Check classicEndless presets + for difficulty = 1, #classicEndless do + if LevelData.__eq(levelData, classicEndless[difficulty]) then + return {style = GameModes.Styles.CLASSIC, level = nil, difficulty = difficulty, isEndless = true} + end + end + + -- Check classic presets + for difficulty = 1, #classic do + if LevelData.__eq(levelData, classic[difficulty]) then + return {style = GameModes.Styles.CLASSIC, level = nil, difficulty = difficulty, isEndless = false} + end + end + + -- Doesn't match any preset - return nil + return nil +end + return LevelPresets \ No newline at end of file diff --git a/common/engine/Health.lua b/common/engine/Health.lua index 0d0ce4465..7728929e2 100644 --- a/common/engine/Health.lua +++ b/common/engine/Health.lua @@ -1,6 +1,7 @@ local logger = require("common.lib.logger") local consts = require("common.engine.consts") local class = require("common.lib.class") +local JsonSafePrecision = require("common.data.JsonSafePrecision") ---@class HealthSettings ---@field framesToppedOutToLose number Starting value of framesToppedOutToLose @@ -24,7 +25,7 @@ local Health = class( function(self, framesToppedOutToLose, lineClearGPM, height, riseSpeed) self.framesToppedOutToLose = framesToppedOutToLose self.maxSecondsToppedOutToLose = framesToppedOutToLose - self.lineClearRate = lineClearGPM / 60 + self.lineClearRate = JsonSafePrecision.toSafePrecision(lineClearGPM / 60) self.currentLines = 0 self.height = height self.lastWasFourCombo = false @@ -139,7 +140,7 @@ end function Health:getSettings() return { framesToppedOutToLose = self.maxSecondsToppedOutToLose, - lineClearGPM = self.lineClearRate * 60, + lineClearGPM = JsonSafePrecision.toSafePrecision(self.lineClearRate * 60), lineHeightToKill = self.height, riseSpeed = self.initialRiseSpeed } diff --git a/common/lib/util.lua b/common/lib/util.lua index 3801ccd03..78439b103 100644 --- a/common/lib/util.lua +++ b/common/lib/util.lua @@ -114,7 +114,9 @@ function real_deepcpy(tab) return setmetatable(ret, getmetatable(tab)) end --- copys the full variable deeply +-- Creates a deep copy of a table, recursively copying all nested tables +-- Preserves metatables and handles circular references +-- If the input is not a table, returns it unchanged ---@generic T ---@param tab T ---@return T deepCopy diff --git a/common/network/ClientProtocol.lua b/common/network/ClientProtocol.lua index 3ed87bf98..5f8d72a98 100644 --- a/common/network/ClientProtocol.lua +++ b/common/network/ClientProtocol.lua @@ -134,11 +134,13 @@ function ClientMessages.sendTaunt(direction, index) } end +---@param gameMode GameMode function ClientMessages.sendRoomRequest(gameMode) + local gameModeData = gameMode:getGameModeJSONData() local roomRequestMessage = { recipient = "server", type = "roomRequest", - content = { gameMode = gameMode } + content = { gameMode = gameModeData } } return { messageType = msgTypes.jsonMessage, diff --git a/server/Game.lua b/server/Game.lua index b585da5c6..28f67a583 100644 --- a/server/Game.lua +++ b/server/Game.lua @@ -77,6 +77,8 @@ function Game.createFromRoomState(room) metadata.level = player.level else -- TODO: https://github.com/panel-attack/panel-game/issues/602 + -- Use this pattern when we are in this area again and testing server + -- local presetInfo = LevelPresets.getStyleAndPreset(levelData) metadata.difficulty = player.level end diff --git a/server/Room.lua b/server/Room.lua index 87cc1d177..e2455db70 100644 --- a/server/Room.lua +++ b/server/Room.lua @@ -21,7 +21,7 @@ local ServerGame = require("server.Game") ---@field ratings table[] ratings by player number ---@field matchCount integer ---@field game ServerGame? ----@field gameMode GameMode +---@field gameMode table -- only the data portion of the game mode ---@field ranked boolean if the next match is anticipated to be ranked ---@field rankedReasons string[] ---@overload fun(roomNumber: integer, players: ServerPlayer[], gameMode: GameMode, leaderboard: Leaderboard?): Room @@ -29,7 +29,7 @@ local Room = class( ---@param self Room ---@param roomNumber integer ---@param players ServerPlayer[] ----@param gameMode GameMode +---@param gameMode table -- only the data portion of the game mode ---@param leaderboard Leaderboard? function(self, roomNumber, players, gameMode, leaderboard) self.players = players diff --git a/server/tests/ServerTests.lua b/server/tests/ServerTests.lua index e830581ab..17ea93d2c 100644 --- a/server/tests/ServerTests.lua +++ b/server/tests/ServerTests.lua @@ -269,7 +269,7 @@ local function testSinglePlayer() assert(message.type == "spectatorUpdate") message = alice.connection.outgoingMessageQueue:pop().messageText assert(message.type == "spectateRequestGranted" and message.content.replay == nil) - assert(tableUtils.deep_content_equal(message.content.gameMode, GameModes.getPreset("ONE_PLAYER_VS_SELF"))) + assert(tableUtils.deep_content_equal(message.content.gameMode, GameModes.getPreset("ONE_PLAYER_VS_SELF"):getGameModeJSONData())) message = alice.connection.outgoingMessageQueue:pop().messageText assert(message.type == "spectatorUpdate") diff --git a/testLauncher.lua b/testLauncher.lua index 119c52ef9..19393cec8 100644 --- a/testLauncher.lua +++ b/testLauncher.lua @@ -92,6 +92,7 @@ local allTests = { "client.tests.TcpClientTests", "client.tests.ThemeTests", "client.tests.StackGraphicsTests", + "client.tests.PlayerSettingsTests", } -- Check for specific test name argument