require("PGBase") require("GameManager/Constants") require("GameManager/PlayerClass") require("GameManager/TutorialTextFeed") ---@class TacticalGameClass ---@field GameMode string The tactical game mode. ---@field StartTime integer The start time. ---@field Players PlayerClass[] The combatant players in this game. ---@field CombatFeed TutorialTextFeed Text feed for unit builds and kills. ---@field BuildStatsTable table The engine-owned TacticalBuildStatsTable reference, set from GameScoring.lua. ---@field KillStatsTable table The engine-owned TacticalKillStatsTable reference, set from GameScoring.lua. TacticalGameClass = {} TacticalGameClass.__index = TacticalGameClass ---Creates a new Tactical Game context. ---@param gameMode string The game mode. ---@return TacticalGameClass function TacticalGameClass:New(gameMode) ScriptMessage("Initializing Tactical game...") local self = setmetatable({}, TacticalGameClass) self.GameMode = gameMode self.StartTime = GetCurrentTime() self.Players = self:_FindPlayers(gameMode) self.CombatFeed = TutorialTextFeed:New("combat_feed", 6) ScriptMessage("Tactical Game initialized!") return self end ---Services the Tactical Game. function TacticalGameClass:Service() ScriptMessage("Servicing Tactical Game...") -- Service Players for _, player in pairs(self.Players) do player:Service() end local gameTime = GetCurrentTime() - self.StartTime GUIManager:DisplayGameTime(gameTime) end -- ================================================== -- State functions -- ================================================== ---Whether this Tactical game is in space mode. ---@return boolean function TacticalGameClass:IsSpaceMode() return StringCompare(self.GameMode, "Space") end ---Whether this Tactical game is in land mode. function TacticalGameClass:IsLandMode() return StringCompare(self.GameMode, "Land") end -- ================================================== -- Player functions -- ================================================== ---Gets the Player by ID. ---@param id integer Player ID ---@return PlayerClass|nil function TacticalGameClass:GetPlayer(id) return self.Players[id] end ---Records the Player ID as having quit at the current time. ---@param id integer Player ID. function TacticalGameClass:PlayerQuit(id) local player = self.Players[id] if player then player:Quit() end end -- ================================================== -- Unit event handlers -- ================================================== ---Adds a built unit to the Unit table. ---@param objectType GameObjectType The game object type that was built. ---@param player PlayerObject The player that owns the new game object. ---@param planet PlanetObject|nil The planet the game object type was built at. function TacticalGameClass:UnitBuilt(objectType, player, planet) local playerId = player.Get_ID() local playerEntry = self.Players[playerId] if playerEntry then local unit = playerEntry:AddUnit(objectType) self:UpdateTacticalBuildStatsTable(unit, planet) self:CombatTextUnitBuilt(unit) end end ---Displays unit built combat text. ---@param unit UnitClass The built unit. function TacticalGameClass:CombatTextUnitBuilt(unit) local owner = self.Players[unit.OwnerId] if not owner then return end if not StringCompare(LocalPlayer.Get_Faction_Name(), owner.Faction) then -- Do not display combat feed for players on opposite factions return end local timeText = FormatTime(unit.BuildTime) local text = string.format("%s\t[%s]: BUILT %s ($%d)", timeText, owner.Name, unit.Name, unit.BuildCost) local faction = Factions[owner.Faction] ---@type ColorStruct local color if faction then color = faction.Color else color = ColorStruct:New(1, 1, 1, 1) end self.CombatFeed:Append(text, -1, color) end ---Sets a unit as killed. ---@param gameObject GameObject The game object that was killed. ---@param killer PlayerObject The player that killed the game object. function TacticalGameClass:UnitKilled(gameObject, killer) local ownerId = gameObject.Get_Owner().Get_ID() local owner = self.Players[ownerId] if not owner then return end ---@type UnitClass|nil local killedUnit = nil for _, unit in pairs(owner.Units) do if unit.GameObject == gameObject then killedUnit = unit break end end if killedUnit then killedUnit:SetDead(killer.Get_ID()) self:UpdateTacticalKillStatsTable(killedUnit, killer) self:CombatTextUnitKilled(killedUnit) end end ---Displays killed unit combat text. ---@param unit UnitClass The killed unit. function TacticalGameClass:CombatTextUnitKilled(unit) local owner = self.Players[unit.OwnerId] if not owner then return end if not StringCompare(LocalPlayer.Get_Faction_Name(), owner.Faction) then -- Do not display combat feed for players on opposite factions return end local killer = self.Players[unit.KillerId] local timeText = FormatTime(unit.DeathTime) if not killer then local text = string.format("%s\t[%s]: MISTAKES WERE MADE %s", timeText, owner.Name, unit.Name) local color = ColorStruct:New(0.8, 0.8, 0.8, 1) self.CombatFeed:Append(text, -1, color) else local text = string.format("%s\t[%s]: KILLED %s (%s)", timeText, killer.Name, unit.Name, owner.Name) local faction = Factions[killer.Faction] ---@type ColorStruct local color = nil if faction then color = faction.Color else color = ColorStruct:New(1, 1, 1, 1) end self.CombatFeed:Append(text, -1, color) end end --- ================================================== --- Stat tables --- ================================================== ---Updates the Tactical Build Stats Table with the build object type. ---@param unit UnitClass The game object type that was built. ---@param planet PlanetObject|nil The planet the game object type was built at. function TacticalGameClass:UpdateTacticalBuildStatsTable(unit, planet) ---@type GameObjectType|integer local planetType = 1 if planet then planetType = planet.Get_Type() end local playerEntry = self.BuildStatsTable[unit.OwnerId] if not playerEntry then playerEntry = {} self.BuildStatsTable[unit.OwnerId] = playerEntry end local planetEntry = playerEntry[planetType] if not planetEntry then planetEntry = {} playerEntry[planetType] = planetEntry end local typeEntry = planetEntry[unit.Name] if not typeEntry then typeEntry = { build_count = 1, combat_power = unit.CombatRating, build_cost = unit.BuildCost, score_value = unit.ScoreValue } planetEntry[unit.Name] = typeEntry else typeEntry.build_count = typeEntry.build_count + 1 typeEntry.combat_power = typeEntry.combat_power + unit.CombatRating typeEntry.build_cost = typeEntry.build_cost + unit.BuildCost typeEntry.score_value = typeEntry.score_value + unit.ScoreValue end end ---Updates the Tactical Kill Stats Table with the killed object. ---@param unit UnitClass The object that was killed. ---@param killer PlayerObject The player that killed the object. function TacticalGameClass:UpdateTacticalKillStatsTable(unit, killer) if not TestValid(killer) then return end -- Update Frags local fragEntry = self.KillStatsTable[FragIndex] if not fragEntry then fragEntry = {} self.KillStatsTable[FragIndex] = fragEntry end local killerId = killer.Get_ID() local killerEntry = fragEntry[killerId] if not killerEntry then killerEntry = {} fragEntry[killerId] = killerEntry end local killerObjEntry = killerEntry[unit.GameObjectType] if not killerObjEntry then killerObjEntry = { kills = 1, combat_power = unit.CombatRating, build_cost = unit.BuildCost, score_value = unit.ScoreValue } killerEntry[unit.GameObjectType] = killerObjEntry else killerObjEntry.kills = killerObjEntry.kills + 1 killerObjEntry.combat_power = killerObjEntry.combat_power + unit.CombatRating killerObjEntry.build_cost = killerObjEntry.build_cost + unit.BuildCost killerObjEntry.score_value = killerObjEntry.score_value + unit.ScoreValue end -- Update Deaths local deathEntry = self.KillStatsTable[DeathIndex] if not deathEntry then deathEntry = {} self.KillStatsTable[DeathIndex] = deathEntry end local ownerEntry = deathEntry[unit.OwnerId] if not ownerEntry then ownerEntry = {} deathEntry[unit.OwnerId] = ownerEntry end local ownerObjEntry = ownerEntry[unit.GameObjectType] if not ownerObjEntry then ownerObjEntry = { kills = 1, combat_power = unit.CombatRating, build_cost = unit.BuildCost, score_value = unit.ScoreValue } ownerEntry[unit.GameObjectType] = ownerObjEntry else ownerObjEntry.kills = ownerObjEntry.kills + 1 ownerObjEntry.combat_power = ownerObjEntry.combat_power + unit.CombatRating ownerObjEntry.build_cost = ownerObjEntry.build_cost + unit.BuildCost ownerObjEntry.score_value = ownerObjEntry.score_value + unit.ScoreValue end end -- ================================================== -- Private functions -- ================================================== ---@private ---Finds all players of defined Factions via FoCAPI. ---@param gameMode string The current game mode. ---@return PlayerClass[] function TacticalGameClass:_FindPlayers(gameMode) local isSuccess, result = pcall(Get_All_Players) if isSuccess then local players = {} for _, playerWrapper in pairs(result) do local factionName = playerWrapper.Get_Faction_Name() for _, faction in pairs(Factions) do if faction.Name == factionName then local player = PlayerClass:New(playerWrapper) players[player.Id] = player end end end return players end return self:_FindPlayersFallback(gameMode) end ---@private ---Finds all players of defined Factions via starting unit. ---Note: this will not work when starting units are disabled. ---For the spectator team, it will only ever find the player in seat 1. ---@param gameMode string The current game mode. ---@return PlayerClass[] function TacticalGameClass:_FindPlayersFallback(gameMode) ---@type PlayerClass[] local players = {} for k, faction in pairs(Factions) do local startingUnit = nil if StringCompare(gameMode, "Land") then startingUnit = faction.LandStartUnitName elseif StringCompare(gameMode, "Space") then startingUnit = faction.SpaceStartUnitName end if startingUnit then for i, unit in pairs(Find_All_Objects_Of_Type(startingUnit)) do local playerWrapper = unit.Get_Owner() local playerId = playerWrapper.Get_ID() if players[playerId] == nil then local player = PlayerClass:New(playerWrapper) players[playerId] = player ScriptMessage("Found Player: %s", player:Debug()) end end end end return players end