Download Lua/Tactical/Combat.lua from sirnii/myspace: direct link, hf CLI and curl.
- Browser
- Download file 79.3 kB
-
https://huggingface.co/spaces/sirnii/myspace/resolve/main/Lua/Tactical/Combat.lua
- Command line
-
hf download hf://spaces/sirnii/myspace/Lua/Tactical/Combat.lua
-
curl -L -o Combat.lua https://huggingface.co/spaces/sirnii/myspace/resolve/main/Lua/Tactical/Combat.lua
79.3 kB
| function IsHotSeatGame() | |
| return Game and Game.game_type == "HotSeat" | |
| end | |
| function IsCompetitiveGame() | |
| return Game and Game.game_type == "Competitive" | |
| end | |
| function IsCoOpGame() | |
| return netInGame and not IsHotSeatGame() and not IsCompetitiveGame() and table.count(netGamePlayers) == 2 | |
| end | |
| function NetPlayerSide(id) | |
| return netInGame and not NetIsHost(id) and IsCompetitiveGame() and "player2" or "player1" | |
| end | |
| function NetPlayerControlMask(id) | |
| if IsCoOpGame() or IsCompetitiveGame() then | |
| return NetIsHost(id) and ~2 or 2 | |
| end | |
| return ~0 | |
| end | |
| function IsNetPlayerTurn(id) | |
| local active_side = (g_Teams[g_CurrentTeam] or empty_table).side | |
| if active_side ~= "player1" and active_side ~= "player2" then | |
| return false | |
| end | |
| if netInGame and #netGamePlayers == 2 and IsCompetitiveGame() then | |
| local player_side = NetIsHost(id) and "player1" or "player2" | |
| if player_side ~= active_side then | |
| return false | |
| end | |
| end | |
| return true | |
| end | |
| function IsControlledByLocalPlayer(side, player_control) | |
| if side ~= "player1" and side ~= "player2" then | |
| return false | |
| end | |
| local mask = NetPlayerControlMask() | |
| if (mask & player_control) == 0 then | |
| return false | |
| end | |
| if IsHotSeatGame() then | |
| local active_side = g_Teams and (g_Teams[g_CurrentTeam] or empty_table).side | |
| return side == active_side | |
| end | |
| return true | |
| end | |
| GameVar("g_FastForwardGameSpeed", "Normal") | |
| if FirstLoad then | |
| g_Combat = false | |
| g_DefaultShotBodyPart = false | |
| g_CombatPath = false | |
| g_ShouldRebuildVisField = false | |
| g_LastAttackStealth = false | |
| g_LastAttackKill = false | |
| g_FastForwardGameSpeedLocal = false | |
| end | |
| function OnMsg.DoneMap() | |
| DoneObject(g_Combat) | |
| g_Combat = false | |
| g_CombatPath = false | |
| g_LastAttackStealth = false | |
| g_LastAttackKill = false | |
| g_FastForwardGameSpeedLocal = false | |
| end | |
| function OnMsg.DataLoaded() | |
| ForEachPresetInGroup("TargetBodyPart", "Default", function(def) | |
| if def.default then | |
| g_DefaultShotBodyPart = def.id | |
| end | |
| end) | |
| end | |
| function IsInCombat(pending) | |
| return g_Combat and not g_Combat.end_combat and not (pending and g_Combat.end_combat_pending and g_Combat:ShouldEndCombat()) | |
| end | |
| function UpdateFastForwardGameSpeed() | |
| -- synced code | |
| local time_factor | |
| if g_FastForwardGameSpeed == "Fast" and g_Combat and not g_Combat.is_player_control then | |
| time_factor = Clamp(const.DefaultTimeFactor * const.Combat.FastForwardGameSpeed / 100, 0, const.MaxTimeFactor) | |
| else | |
| time_factor = const.DefaultTimeFactor | |
| end | |
| NetUpdateHash("UpdateFastForwardGameSpeed", time_factor, g_Combat and g_Combat.is_player_control, g_FastForwardGameSpeed) | |
| NetTimeFactor = time_factor | |
| __SetTimeFactor(NetPause and 0 or NetTimeFactor) | |
| if netInGame and NetIsHost() then | |
| -- the server should know the time factor | |
| NetChangeGameInfo({ time_factor = time_factor }) | |
| end | |
| end | |
| function NetSyncEvents.SetFastForwardGameSpeed(value) | |
| if g_FastForwardGameSpeedLocal == value then | |
| g_FastForwardGameSpeedLocal = false | |
| end | |
| if value ~= g_FastForwardGameSpeed then | |
| g_FastForwardGameSpeed = value | |
| UpdateFastForwardGameSpeed() | |
| ObjModified(Selection) -- update interface | |
| end | |
| end | |
| NetSyncLocalEffects.SetFastForwardGameSpeed = function(value) | |
| g_FastForwardGameSpeedLocal = value or false | |
| ObjModified(Selection) -- update interface | |
| end | |
| function GetRandomTerrainVoxelPosAroundCenter(unit, center, radius) | |
| local cx, cy = center:xyz() | |
| local x = unit:Random(radius*2) - radius | |
| local y = unit:Random(radius*2) - radius | |
| local z = 0 | |
| x, y, z = SnapToVoxel(cx+x, cy+y, z) | |
| return point(x, y) | |
| end | |
| function IsValidTarget(target) | |
| return IsValid(target) and (not IsKindOf(target, "Unit") or (not target:IsDefeatedVillain() and not target:IsDead())) and target:IsValidPos() | |
| end | |
| function OnMsg.CombatStart() | |
| ShowTacticalNotification("combatStart") | |
| end | |
| local function CheckWeaponsAmmo(team_idx) | |
| local team = g_Teams[team_idx] | |
| team = GetFilteredCurrentTeam(team) | |
| local units = team.units | |
| if not units or not next(units) then return end | |
| for _, unit in ipairs(units) do | |
| if IsMerc(unit) and not unit.infinite_ammo then | |
| unit:ForEachItem("Firearm",function(item, slot, l,t) | |
| if item.ammo and item.MagazineSize >= 5 then | |
| local amount = item.ammo.Amount | |
| if amount < (item.MagazineSize/4) then | |
| item.low_ammo_checked = true | |
| end | |
| end | |
| end) | |
| end | |
| end | |
| end | |
| function OnMsg.TurnStart(g_CurrentTeam) | |
| if IsNetPlayerTurn() then | |
| ShowTacticalNotification("playerTurnStart") | |
| CheckWeaponsAmmo(g_CurrentTeam) | |
| UnlockCameraMovement(nil, "unlock_all") | |
| for _, unit in ipairs(g_ShowTargetBadge ) do | |
| if unit.ui_badge then | |
| unit.ui_badge:SetActive(false) | |
| end | |
| end | |
| g_ShowTargetBadge = {} | |
| for i, t in ipairs(g_Teams) do | |
| if t.control == "UI" and NetPlayerSide(t.side) and t:HasAliveUnits() and g_Combat:AreEnemiesAware(g_CurrentTeam) then | |
| g_Combat:SelectBestUnit(t) | |
| end | |
| end | |
| end | |
| end | |
| GameVar("gv_CombatStartFromConversation", false) | |
| function OnMsg.NewMap() | |
| gv_CombatStartFromConversation = false | |
| end | |
| MapVar("g_StartingCombat", false) | |
| DefineClass.Combat = { | |
| __parents = { "InitDone" }, | |
| combat_id = false, | |
| combat_started = false, | |
| player_end_turn = false, | |
| ai_end_turn = false, | |
| player_end_turn_local_effects = false, | |
| -- Do not forget to add new class members to Get/SetDynamicData function to save in Game state (see GetCurrentGameVarValues) | |
| active_unit = false, | |
| active_unit_shown = false, | |
| starting_unit = false, | |
| units_repositioned = false, | |
| unit_reposition_shown = false, | |
| start_reposition_ended = false, | |
| cinematic_kills_this_turn = false, | |
| thread = false, | |
| end_combat = false, | |
| end_combat_pending = false, | |
| enemies_engaged = false, | |
| retreat_enemies = false, | |
| time_of_day = "day", | |
| turns_no_visibility = 0, | |
| current_turn = 1, | |
| start_of_turn = false, | |
| team_playing = false, | |
| test_combat = false, | |
| is_player_control = false, | |
| queued_bombards = false, | |
| emplacement_assignment = false, -- ai units assigned to take an emplacement | |
| visibility_update_hash = false, | |
| enemy_badges = false, | |
| enter_combat_anim_variants = false, | |
| enter_combat_anim_variants_time = false, | |
| skip_first_autosave = false, | |
| waiting_relations_update = false, | |
| autosave_enabled = false, | |
| log_ai_side = false, -- set manually to the side (e.g. "enemy1") for which to log ai execution | |
| lastStandingVR = false, --flag to prevent VR spam | |
| berserkVRsPerRole = false, --table to store all roles that have said the berserk vr | |
| -- stats only | |
| stealth_attack_start = false, | |
| last_attack_kill = false, | |
| combat_time = false, | |
| hp_loss = false, | |
| hp_healed = false, | |
| turn_dist_travelled = false, | |
| out_of_ammo = false, -- per weapon | |
| } | |
| function Combat:Init() | |
| self.combat_id = random_encode64(64) | |
| self.player_end_turn = {} | |
| self.units_repositioned = {} | |
| self.queued_bombards = {} | |
| self.emplacement_assignment = {} | |
| self.berserkVRsPerRole = {} | |
| -- stats | |
| self.combat_time = GetCurrentPlaytime() | |
| self.hp_loss = {} | |
| self.hp_healed = {} | |
| self.turn_dist_travelled = {} | |
| self.out_of_ammo = {} | |
| end | |
| function Combat:Done() | |
| if IsValidThread(self.thread) then | |
| DeleteThread(self.thread) | |
| end | |
| self.thread = nil | |
| self.emplacement_assignment = nil | |
| self.lastStandingVR=false --reset vr flag | |
| self.berserkVRsPerRole = nil --reset vr table for berserk | |
| end | |
| function WaitUnitsInIdle(check_pending, callback) | |
| -- wait all units to reach Idle | |
| while true do | |
| local done = true | |
| for _, unit in ipairs(g_Units) do | |
| if not unit:IsDead() and unit:IsValidPos() and not unit:IsIdleCommand(check_pending) and (g_Combat or unit:IsAware(check_pending)) then | |
| done = false | |
| break | |
| end | |
| end | |
| if done then break end | |
| if callback then | |
| callback() | |
| end | |
| WaitMsg("Idle", 10) | |
| end | |
| end | |
| function WaitUnitsInIdleOrBehavior() | |
| while true do | |
| local done = true | |
| for _, unit in ipairs(g_Units) do | |
| if not unit:IsDead() and not unit:IsAmbientUnit() then | |
| if not (unit:IsIdleCommand() or (unit.command and unit.command == unit.behavior)) then | |
| done = false | |
| break | |
| end | |
| end | |
| end | |
| if done then break end | |
| WaitMsg("Idle", 10) | |
| end | |
| end | |
| function Combat:Start(dynamic_data) | |
| NetUpdateHash("Combat:Start") | |
| g_CurrentTeam = g_CurrentTeam or 1 | |
| self.current_turn = self.current_turn or 1 | |
| assert(not self.thread) | |
| self:SetPlayerControl(false) | |
| SuspendVisibiltyUpdates("EnterCombat") | |
| Msg("CombatStarting", dynamic_data) | |
| ListCallReactions(g_Units, "OnCombatStarting") | |
| CombatLog("debug", "Combat starting") | |
| if dynamic_data then -- loading a combat | |
| for _, unit in ipairs(g_Units) do | |
| unit:UpdateMeleeTrainingVisual() | |
| end | |
| self.waiting_relations_update = true | |
| InvalidateDiplomacy() | |
| else -- starting a new combat | |
| --snap units to closest free pass slab | |
| for i, unit in ipairs(g_Units) do | |
| unit:ClearCommandQueue() | |
| if not unit:IsDead() and not (unit:IsDefeatedVillain() or UnitIgnoreEnterCombatCommands[unit.command]) then | |
| local overwatch = g_Overwatch[unit] | |
| local interruptable_cmd = unit.command ~= "Cower" and unit.command ~= "EnterMap" | |
| if interruptable_cmd and (not overwatch or not overwatch.permanent or unit.command ~= "PreparedAttackIdle") then | |
| unit:InterruptCommand("EnterCombat") | |
| end | |
| end | |
| end | |
| -- wait all units to reach Idle | |
| WaitUnitsInIdle() | |
| end | |
| InvalidateVisibility() | |
| ResumeVisibiltyUpdates("EnterCombat") | |
| if not self.waiting_relations_update then | |
| self.autosave_enabled = self:ShouldEndCombat() | |
| end | |
| self.thread = CreateGameTimeThread( self.MainLoop, self, dynamic_data ) | |
| if not gv_Sectors then | |
| return | |
| end | |
| gv_ActiveCombat = gv_CurrentSectorId | |
| g_StartingCombat = false | |
| Msg("CombatStart", dynamic_data, self) | |
| ListCallReactions(g_Units, "OnCombatStarted") | |
| PlayFX("ConflictInitiated") | |
| LockCamera("CombatStarting") | |
| self:UpdateVisibility() | |
| UnlockCamera("CombatStarting") | |
| end | |
| function Combat:ShouldEndCombat(killed_units) | |
| killed_units = killed_units or empty_table | |
| -- check for non-empty player team | |
| local player_team | |
| for _, team in ipairs(g_Teams) do | |
| if (team.side == "player1" or team.side == "player2") then | |
| local alive | |
| for _, unit in ipairs(team.units) do | |
| if not unit:IsDead() and not table.find(killed_units, unit) then | |
| alive = true | |
| break | |
| end | |
| end | |
| if alive then | |
| player_team = team | |
| break | |
| end | |
| end | |
| end | |
| if not player_team then | |
| return true | |
| end | |
| for _, team in ipairs(g_Teams) do | |
| if team:IsEnemySide(player_team) then | |
| for _, unit in ipairs(team.units) do | |
| if not unit:IsDead() and unit:IsAware("pending") and not table.find(killed_units, unit) then | |
| return false | |
| end | |
| end | |
| end | |
| end | |
| return true | |
| end | |
| function Combat:EndCombatCheck(force) | |
| if not force and HasAnyCombatActionInProgress() then | |
| return | |
| end | |
| if not self.end_combat and self:ShouldEndCombat() then | |
| self.end_combat = true | |
| self:CheckEndTurn() | |
| end | |
| end | |
| function Combat:CheckPendingEnd(killed_units) | |
| if self:ShouldEndCombat(killed_units) then | |
| self.end_combat_pending = true | |
| end | |
| end | |
| function Combat:SelectBestUnit(team, noFitCheck) | |
| local applyCameraSnap = true | |
| local allyClosest, bestRatio, highestEnemyCount, firstLocalControlled | |
| if self.starting_unit and self.starting_unit.team == team and self.starting_unit:IsLocalPlayerControlled() then | |
| allyClosest = self.starting_unit | |
| applyCameraSnap = false | |
| else | |
| for i, u in ipairs(team.units) do | |
| firstLocalControlled = firstLocalControlled or u:IsLocalPlayerControlled() and u or false | |
| if u:CanBeControlled() and not u:IsDowned() and u:IsLocalPlayerControlled() then | |
| local enemies = u:GetVisibleEnemies() | |
| local enemyCount = #enemies | |
| local closesnessRatio = 0 | |
| for ii, e in ipairs(enemies) do | |
| closesnessRatio = closesnessRatio + u:GetDist(e) | |
| end | |
| if not allyClosest or enemyCount > highestEnemyCount then | |
| highestEnemyCount = enemyCount | |
| bestRatio = closesnessRatio | |
| allyClosest = u | |
| elseif enemyCount == highestEnemyCount and closesnessRatio < bestRatio or bestRatio == 0 then | |
| bestRatio = closesnessRatio | |
| allyClosest = u | |
| end | |
| end | |
| end | |
| end | |
| if not allyClosest then | |
| allyClosest = firstLocalControlled or team.units[1] | |
| end | |
| SelectObj(allyClosest) | |
| AdjustCombatCamera("reset", nil, allyClosest, nil, nil, noFitCheck) | |
| end | |
| function OnMsg.SetpieceDialogClosed() | |
| NetSyncEvent("SetpieceDialogClosed") | |
| end | |
| function NetSyncEvents.SetpieceDialogClosed() | |
| Msg("SyncSetpieceDialogClosed") | |
| end | |
| local function CombatAreEnemiesOutnumbered(unit) | |
| local targetPos = unit:GetVisualPos() | |
| local team = unit.team | |
| local counts = {friends = 0, enemies = 0} | |
| MapForEach(targetPos, const.SlabSizeX * 20, "Unit", | |
| function(u) | |
| if not IsValid(u) or u==unit or u:IsDead() then return false end | |
| local tm = u.team | |
| if team:IsEnemySide(tm) then | |
| counts.enemies = counts.enemies + 1 | |
| else | |
| counts.friends = counts.friends + 1 | |
| end | |
| end,counts ) | |
| return counts.enemies - counts.friends>=1 | |
| end | |
| function CombatCollectOutnumbered(team_id, except) | |
| local units = {} | |
| local team = g_Teams[team_id] | |
| for _, unit in ipairs(team.units) do | |
| if (not except or not table.find(except, unit.session_id)) and IsMerc(unit) and not unit:IsDead() and CombatAreEnemiesOutnumbered(unit) then | |
| units[#units + 1] = unit.session_id | |
| end | |
| end | |
| return units | |
| end | |
| function Combat:MainLoop(dynamic_data) | |
| WaitSyncLoadingDone() --perhaps we can wait for loadings screens to close before starting combat? | |
| local resume_saved_combat = not not dynamic_data | |
| local eot_marker_team = 1 | |
| for i, team in ipairs(g_Teams) do | |
| if team.side == "player1" or team.side == "player2" then | |
| eot_marker_team = i | |
| break | |
| end | |
| end | |
| UpdateSurrounded() | |
| self.combat_started = true | |
| Msg("CombatStartedForReal", dynamic_data, self) | |
| local first_turn = true | |
| while true do | |
| if IsSetpiecePlaying() then | |
| WaitMsg("SyncSetpieceDialogClosed") | |
| end | |
| local team = g_Teams[g_CurrentTeam] | |
| -- check that there are remaining ui controlled units and alive enemies | |
| local enemies_alive, nonempty_player_teams | |
| for _, t in ipairs(g_Teams) do | |
| if (t.side == "player1" or t.side == "player2") and t:HasAliveUnits() then | |
| nonempty_player_teams = true | |
| end | |
| if t ~= team then | |
| local alive | |
| for _, unit in ipairs(t.units) do | |
| alive = alive or not unit:IsDead() | |
| end | |
| if alive and team:IsEnemySide(t) then | |
| enemies_alive = true | |
| end | |
| end | |
| end | |
| if not nonempty_player_teams then | |
| self:End() | |
| break | |
| end | |
| -- update combat visibility | |
| self:UpdateVisibility() | |
| self:ApplyVisibility() | |
| if team.player_team and self:ShouldEndDueToNoVisibility() then | |
| -- clean up awareness from enemies so we don't end up in combat again | |
| local team = GetPoVTeam() | |
| for _, unit in ipairs(g_Units) do | |
| if not unit:IsDead() and unit:IsAware() and unit.team and unit.team:IsEnemySide(team) then | |
| unit:AddStatusEffect("Unaware") | |
| end | |
| end | |
| self:End() | |
| break | |
| end | |
| DeadUnitsPulse() -- call manually here to flag any waiting units so they're finished before the turn actually starts | |
| WaitUnitsInIdle("pending", AlertPendingUnits) | |
| self.start_of_turn = true | |
| self.enemies_engaged = self.enemies_engaged or not self:ShouldEndCombat() | |
| for _, unit in ipairs(team.units) do | |
| if not unit:IsDead() then | |
| unit:BeginTurn(not resume_saved_combat) | |
| ObjModified(unit) | |
| end | |
| end | |
| NetUpdateHash("Combat_MainLoop_BeginTurn_End") | |
| self.start_of_turn = false -- remove the flag temporarily to allow AlertPendingUnits to work | |
| WaitUnitsInIdle("pending", AlertPendingUnits) -- handle units for which the Surprised effect kicked in | |
| NetUpdateHash("Combat_MainLoop_WaitUnitsIdle_End") | |
| self.start_of_turn = true | |
| if not first_turn and not enemies_alive and (team.side == "player1" or team.side == "player2") then | |
| self:End() | |
| break | |
| end | |
| NetUpdateHash("Combat_MainLoop_ExecMoraleActions_Start") | |
| ExecMoraleActions() -- process berserk/panicked units | |
| NetUpdateHash("Combat_MainLoop_ExecMoraleActions_End") | |
| -- wait until action camera is done | |
| while ActionCameraPlaying do | |
| WaitMsg("ActionCameraRemoved", 100) | |
| end | |
| NetUpdateHash("Combat_MainLoop_ActionCameraWait_End") | |
| -- bombard zones | |
| if not resume_saved_combat then | |
| ActivateBombardZones(g_Teams[g_CurrentTeam].side) | |
| if self.current_turn ~= 1 then UpdateTimedExplosives(Traps_CombatTurnToTime, g_Teams[g_CurrentTeam].side) end | |
| TriggerTimedExplosives() | |
| end | |
| NetUpdateHash("Combat_MainLoop_BombardZones_End") | |
| self.start_of_turn = false | |
| self.cinematic_kills_this_turn = {} | |
| self.team_playing = g_CurrentTeam | |
| if IsNetPlayerTurn() then | |
| self:SetPlayerControl(true) | |
| if IsCompetitiveGame() then | |
| ShowTurnNotification() | |
| end | |
| if not resume_saved_combat then | |
| PlayFX("PlayerCombatTurnStart", "start") | |
| end | |
| if (not resume_saved_combat or #Selection == 0) and self:AreEnemiesAware(g_CurrentTeam) then | |
| self:SelectBestUnit(team, "noFitCheck") | |
| end | |
| NetUpdateHash("Combat_MainLoop_SelectUnit_End") | |
| resume_saved_combat = false | |
| ObjModified(self) | |
| -- save at the beginning of the player's turn | |
| if self.skip_first_autosave then | |
| self.skip_first_autosave = false | |
| else | |
| if not self:ShouldEndCombat() then -- there are alerted/aware enemies | |
| self.start_reposition_ended = self.start_reposition_ended or self:AreEnemiesAware(g_CurrentTeam) | |
| while not self.start_reposition_ended or IsRepositionPhase() do | |
| WaitMsg("RepositionEnd", 100) | |
| end | |
| NetUpdateHash("Combat_MainLoop_Reposition_End") | |
| end | |
| if self.autosave_enabled and self.current_turn and self.current_turn > 1 then | |
| RequestAutosave{ autosave_id = "combat", save_state = "Turn", display_name = T{600965840557, "<u(Id)>_Turn<turn>", gv_Sectors[gv_CurrentSectorId], turn = self.current_turn}, turn_phase = self.current_turn, mode = "delayed" } | |
| end | |
| NetUpdateHash("Combat_MainLoop_Autosave_End") | |
| end | |
| Msg("TurnStart", g_CurrentTeam) | |
| ObjModified(SelectedObj) | |
| ObjModified("combat_bar") | |
| else | |
| Msg("TurnStart", g_CurrentTeam) | |
| if not self.lastStandingVR and (team.side == "enemy1" or team.side == "enemy2") then | |
| local lastStanding = IsLastUnitInTeam(team.units) | |
| if lastStanding then | |
| PlayVoiceResponseGroup(lastStanding, "AIStartingTurnLastEnemy") | |
| self.lastStandingVR = true | |
| end | |
| end | |
| if team.control == "AI" then | |
| SelectObj() | |
| NetUpdateHash("Combat_MainLoop_AITurn_Start") | |
| if team.side == "neutral" then | |
| if GameState.skip_civilian_run then | |
| RunProjectorTurnAndWait() | |
| self:AITurn(team) | |
| else | |
| LockCameraMovement("CivilianTurn") | |
| self:NeutralTurn(team) | |
| end | |
| else | |
| self:AITurn(team) | |
| end | |
| NetUpdateHash("Combat_MainLoop_AITurn_End") | |
| UnlockCameraMovement("CivilianTurn") | |
| self.ai_end_turn = true | |
| ObjModified(self) | |
| else | |
| -- competitive game player | |
| SelectObj() | |
| ObjModified(self) | |
| if #netGamePlayers > 1 then | |
| ShowTurnNotification() | |
| end | |
| end | |
| end | |
| -- sanity check awareness in case it was changed in the meantime | |
| if self.end_combat and not self:ShouldEndCombat() then | |
| self.end_combat = false | |
| end | |
| self:WaitEndTurn() | |
| self:SetPlayerControl(false) | |
| WaitAllCombatActionsEnd() | |
| NetUpdateHash("Combat_MainLoop_CombatActionsEnd") | |
| if IsNetPlayerTurn() then WaitUIEndTurn() end | |
| NetUpdateHash("Combat_MainLoop_WaitUIEndTurn") | |
| -- save at the end of the player's turn | |
| local shouldSave = self.autosave_enabled and not self:ShouldEndCombat() and Platform.developer and not IsGameReplayRecording() and not IsGameReplayRunning() | |
| if shouldSave and IsNetPlayerTurn() and self:IsLocalPlayerEndTurn() then | |
| RequestAutosave{ autosave_id = "combat", save_state = "TurnEnd", display_name = T{111401651980, "<u(Id)>_Turn<turn>End", gv_Sectors[gv_CurrentSectorId], turn = self.current_turn}, turn_phase = self.current_turn, mode = "immediate" } | |
| end | |
| if shouldSave then | |
| NetSyncEventFence() --we need to resync this thread after autosave :(. | |
| end | |
| NetUpdateHash("Combat_MainLoop_Autosave_End", shouldSave) | |
| self.player_end_turn = {} | |
| self.player_end_turn_local_effects = false | |
| self.ai_end_turn = false | |
| first_turn = false | |
| for _, unit in ipairs(team.units) do | |
| if not unit:IsDead() then | |
| unit:OnEndTurn() | |
| end | |
| end | |
| NetUpdateHash("Combat_MainLoop_TurnEnded") | |
| Msg("TurnEnded", g_CurrentTeam, self.current_turn, self.end_combat) | |
| if IsNetPlayerTurn() then | |
| if not self:AreEnemiesAware(g_CurrentTeam) then | |
| self:End() | |
| break | |
| end | |
| PlayFX("PlayerCombatTurnEnd", "start") | |
| else | |
| -- sanity check awareness in case it was changed by OnEndTurn; don't end combat immediately so all teams can do their thing | |
| if self.end_combat and not self:ShouldEndCombat() then | |
| self.end_combat = false | |
| end | |
| end | |
| if self.starting_unit and self.starting_unit.team == g_Teams[g_CurrentTeam] then | |
| self.starting_unit = nil | |
| end | |
| local player_turn = IsNetPlayerTurn() | |
| local last_team = g_CurrentTeam | |
| for i = 1, #g_Teams do | |
| g_CurrentTeam = (g_CurrentTeam % #g_Teams) + 1 | |
| if not self:IsEndTurnReady() or self:IsAutomatedTurn() then | |
| break | |
| end | |
| -- Play the neutral objects turn even if no neutral units are present and it is going to be skipped. | |
| -- Turns that arent skipped exit the loop via the break above. | |
| if g_Teams[g_CurrentTeam].side == "neutral" then | |
| RunProjectorTurnAndWait() | |
| UpdateTimedExplosives(Traps_CombatTurnToTime, "neutral") | |
| end | |
| end | |
| if g_CurrentTeam == last_team and self:ShouldEndCombat() then -- in case all enemies are Unconscious we can get two turns in a row | |
| self:End() | |
| break | |
| end | |
| if g_CurrentTeam == eot_marker_team then | |
| self.current_turn = self.current_turn + 1 | |
| Msg("NewCombatTurn", self.current_turn) | |
| CombatLog("debug", T{Untranslated("Combat turn <num> begins"), num = self.current_turn}) | |
| end | |
| for _, unit in ipairs(team.units) do | |
| ObjModified(unit) | |
| end | |
| ObjModified(self) | |
| end | |
| end | |
| function Combat:CheckEnableAutosave(trigger_save) | |
| if self.autosave_enabled or self:ShouldEndCombat() then return end | |
| self.autosave_enabled = true | |
| if trigger_save and IsNetPlayerTurn() then | |
| CreateMapRealTimeThread(function() | |
| WaitUnitsInIdle() | |
| while g_Combat and not g_Combat.start_reposition_ended do | |
| WaitMsg("CombatStartRepositionDone", 1) | |
| end | |
| RequestAutosave{ autosave_id = "combatStart", save_state = "CombatStart", display_name = T{640196537172, "<u(Id)>_CombatStart", gv_Sectors[gv_CurrentSectorId]}, mode = "delayed" } | |
| end) | |
| end | |
| end | |
| function OnMsg.CombatStartRepositionDone() | |
| if g_Combat then | |
| g_Combat:CheckEnableAutosave(true) | |
| end | |
| end | |
| function OnMsg.UnitRelationsUpdated() | |
| -- needed when loading a save and Combat:Start() gets executed too early | |
| if g_Combat then | |
| g_Combat:CheckEnableAutosave(not g_Combat.waiting_relations_update) | |
| g_Combat.waiting_relations_update = false | |
| end | |
| end | |
| function Combat:HasTeamTurnStarted(team) | |
| team = team or g_Teams[g_CurrentTeam] | |
| return (team == g_Teams[g_CurrentTeam]) and (g_CurrentTeam == self.team_playing) | |
| end | |
| function Combat:AreEnemiesAware(team_idx) | |
| local team = g_Teams[team_idx] | |
| if IsCompetitiveGame() then | |
| -- only check if player teams have alive units | |
| for _, t in ipairs(g_Teams) do | |
| if t ~= team and (team.side == "player1" or team.side == "player2") and t:HasAliveUnits() then | |
| return true | |
| end | |
| end | |
| else | |
| -- check for enemy teams having aware units | |
| for _, t in ipairs(g_Teams) do | |
| if t ~= team and t:IsEnemySide(team) and t:HasAliveUnits("aware") then | |
| return true | |
| end | |
| end | |
| end | |
| end | |
| function Combat:SetPlayerControl(value) | |
| self.is_player_control = value | |
| if g_FastForwardGameSpeed ~= "Normal" then | |
| UpdateFastForwardGameSpeed() | |
| end | |
| end | |
| function Combat:IsAutomatedTurn(team) | |
| team = team or g_Teams[g_CurrentTeam] | |
| if team.control ~= "UI" then | |
| return false | |
| end | |
| local automated = false | |
| for _, unit in ipairs(team.units) do | |
| if not unit:IsIncapacitated() and not unit:HasStatusEffect("Panicked") and not unit:HasStatusEffect("Berserk") then | |
| return false | |
| end | |
| automated = true | |
| end | |
| return automated | |
| end | |
| function Combat:IsEndTurnReady(fnCheckUnit) | |
| local mask = 0 | |
| local team = g_Teams[g_CurrentTeam] | |
| if team and (team.side == "player1" or team.side == "player2") then | |
| fnCheckUnit = fnCheckUnit or Unit.IsDisabled | |
| for i = 1, Max(1, #netGamePlayers) do | |
| if self.player_end_turn[i] then | |
| mask = mask | NetPlayerControlMask(i) | |
| end | |
| end | |
| elseif self.ai_end_turn then | |
| mask = ~0 | |
| end | |
| for _, unit in ipairs(team and team.units) do | |
| if (not fnCheckUnit or not fnCheckUnit(unit)) and (team.control ~= "AI" or unit:IsAware("pending")) then | |
| if HasCombatActionInProgress(unit) then | |
| return false | |
| end | |
| if not unit:IsControlledBy(mask) and not self.end_combat then | |
| return false | |
| end | |
| end | |
| end | |
| for _, bombard in ipairs(self.queued_bombards) do | |
| if g_Teams[bombard.team] == team and ((bombard.activation_turn or self.current_turn) <= self.current_turn) then | |
| return false | |
| end | |
| end | |
| if team.side == "neutral" and MapHasProjectors then | |
| return ProjectorsCombatTurnExecuted == self.current_turn | |
| end | |
| return true | |
| end | |
| function Combat:CheckEndTurn() | |
| if self:IsEndTurnReady() then | |
| Msg("EndTurnReady") | |
| end | |
| end | |
| function Combat:UncontrollableUnitsCheck() | |
| -- check if we have an UI-controlled current team which has no controllable units (all panicked/berserk/dead) | |
| local team = g_Teams[g_CurrentTeam] | |
| if self.end_combat or team.control ~= "UI" then return end | |
| local uncontrollable = true | |
| for _, unit in ipairs(team.units) do | |
| if not IsCompetitiveGame() or unit:IsLocalPlayerControlled() then | |
| uncontrollable = uncontrollable and unit:IsDisabled() | |
| end | |
| end | |
| if uncontrollable then | |
| Msg("EndTurnReady") | |
| end | |
| return uncontrollable | |
| end | |
| function Combat:WaitEndTurn() | |
| if self:IsEndTurnReady(Unit.IsDisabled) or self:UncontrollableUnitsCheck() or g_Teams[g_CurrentTeam].control == "AI" then | |
| return | |
| end | |
| WaitMsg("EndTurnReady") | |
| WaitAllOtherThreads() --EndTurnReady msg is fired async, therefore we may or may not be in the same thread order as the other player. | |
| end | |
| function Combat:IsLocalPlayerEndTurn() | |
| return self.player_end_turn_local_effects or self.player_end_turn[netUniqueId] | |
| end | |
| function GetUnitEquippedMedicine(unit) | |
| local item | |
| unit:ForEachItem("Medicine", function(itm) | |
| if itm.Condition > 0 then | |
| if not item or (item.UsePriority < itm.UsePriority) then | |
| item = itm | |
| end | |
| end | |
| end) | |
| return item | |
| end | |
| function CountAnyEnemies(skipAnimals) | |
| local anyEnemies = 0 | |
| local ui_team_idx = table.find(g_Teams, "side", "player1") | |
| local ui_team = g_Teams[ui_team_idx] | |
| for idx, team in ipairs(g_Teams) do | |
| if idx ~= ui_team_idx and #team.units > 0 and SideIsEnemy(ui_team.side, team.side) then | |
| for i, u in ipairs(team.units) do | |
| -- Don't count dead enemies as "existing" | |
| if not u:IsDead() and not (skipAnimals and u.species ~= "Human") then | |
| anyEnemies = anyEnemies + 1 | |
| end | |
| end | |
| end | |
| end | |
| return anyEnemies | |
| end | |
| function Combat:End() | |
| if g_Combat ~= self then | |
| return | |
| end | |
| if IsValidThread(self.thread) and self.thread ~= CurrentThread() then | |
| DeleteThread(self.thread) | |
| self.thread = nil | |
| end | |
| self.combat_time = self.combat_time and (GetCurrentPlaytime() - (self.combat_time or GetCurrentPlaytime())) | |
| NetUpdateHash("Combat_End") | |
| g_Combat = false | |
| gv_ActiveCombat = false | |
| gv_CombatStartFromConversation = false | |
| g_LastAttackStealth = false | |
| g_LastAttackKill = false | |
| local context = {suppress_camera_init = true } | |
| context.time_of_day = self.time_of_day | |
| local anyEnemies = CountAnyEnemies() > 0 | |
| local ui_team_idx = table.find(g_Teams, "side", "player1") | |
| g_CurrentTeam = ui_team_idx | |
| self:SetPlayerControl(false) | |
| SetInGameInterfaceMode("IModeExploration", context) | |
| AdjustCombatCamera("reset") | |
| Msg("CombatEnd", self, anyEnemies) | |
| ListCallReactions(g_Units, "OnCombatEnd") | |
| CombatLog("debug", "Combat ended") | |
| NetUpdateHash("CombatEnd") | |
| -- cleanup in case combat ended outside of player's turn | |
| HideTacticalNotification("allyTurnPhase") | |
| HideTacticalNotification("enemyTurnPhase") | |
| HideTacticalNotification("turn") | |
| if anyEnemies then | |
| ShowTacticalNotification("hostilesPresent") | |
| elseif gv_CurrentSectorId and gv_Sectors and not gv_Sectors[gv_CurrentSectorId].conflict then | |
| ShowTacticalNotification("conflictResolved") | |
| PlayFX("NoEnemiesLeft", "start") | |
| else | |
| ShowTacticalNotification("noEnemyContact") | |
| PlayFX("NoEnemiesLeft", "start") | |
| end | |
| CreateGameTimeThread(DoneObject, self) | |
| local playerSquadsHere = GetSquadsInSector(gv_CurrentSectorId, false, false, true) | |
| local alive_player_units | |
| local all_alive_retreated = #playerSquadsHere > 0 | |
| for i, s in ipairs(playerSquadsHere) do | |
| for _, uId in ipairs(s.units) do | |
| local unit = g_Units[uId] | |
| local ud = gv_UnitData[uId] | |
| if unit then | |
| alive_player_units = alive_player_units or (not unit:IsDead() and not unit:IsDowned()) | |
| end | |
| if (not unit or not unit:IsDead()) and (ud and not ud.retreat_to_sector) then | |
| all_alive_retreated = false | |
| end | |
| end | |
| end | |
| if alive_player_units and self.autosave_enabled then | |
| CreateMapRealTimeThread(function(self) | |
| WaitUnitsInIdleOrBehavior() | |
| RequestAutosave{ autosave_id = "combatEnd", save_state = "CombatEnd", display_name = T{299876017054, "<u(Id)>_CombatEnd", gv_Sectors[gv_CurrentSectorId]}, mode = "delayed" } | |
| end, self) | |
| end | |
| if not alive_player_units and all_alive_retreated and gv_LastRetreatedUnit and gv_LastRetreatedEntrance then | |
| local ud = gv_UnitData[gv_LastRetreatedUnit] | |
| local squad = ud and gv_Squads[ud.Squad] | |
| assert(squad) | |
| if squad.CurrentSector ~= gv_CurrentSectorId then | |
| return -- This means the travel time was 0 and they already changed sectors (underground/cities) | |
| end | |
| local sectorId = gv_LastRetreatedEntrance[1] | |
| assert(sectorId) | |
| local entrance = gv_LastRetreatedEntrance[2] | |
| LeaveSectorExploration(sectorId, squad.units, entrance, true) | |
| end | |
| end | |
| function Combat:IsVisibleByPoVTeam(unit) | |
| return g_Visibility and HasVisibilityTo(GetPoVTeam(), unit) | |
| end | |
| local function IsSafeLocation(loc, combatant_locs, fear) | |
| local x, y, z | |
| if IsValid(loc) then | |
| x, y, z = loc:GetGridCoords() | |
| else | |
| x, y, z = point_unpack(loc) | |
| end | |
| for _, cloc in ipairs(combatant_locs) do | |
| local cx, cy, cz = point_unpack(cloc) | |
| if IsCloser(x, y, z, cx, cy, cz, fear) then | |
| return false | |
| end | |
| end | |
| return true | |
| end | |
| function Combat:NeutralTurn(team) | |
| local destlocks = {} | |
| local fear = const.Combat.NeutralUnitFearRadius | |
| local combatant_locs = {} | |
| for _, unit in ipairs(g_Units) do | |
| if unit.team.side ~= "neutral" then | |
| local x, y, z = unit:GetGridCoords() | |
| combatant_locs[#combatant_locs + 1] = point_pack(x, y, z) | |
| end | |
| end | |
| local moving = {} | |
| for _, unit in ipairs(team.units) do | |
| if not unit:IsDead() and unit.species == "Human" and not unit:IsDefeatedVillain() and not IsSafeLocation(unit, combatant_locs, fear) and not unit.neutral_ai_dont_move then | |
| ShowTacticalNotification("civilianTurn", true) | |
| unit.ActionPoints = const.Combat.NeutralUnitRelocateAP | |
| local cpath = CombatPath:new() | |
| cpath:RebuildPaths(unit) | |
| local safe_voxels = {} | |
| for voxel in sorted_pairs(cpath.paths_ap) do | |
| local vx, vy, vz = point_unpack(voxel) | |
| local gx, gy, gz = WorldToVoxel(vx, vy, vz) | |
| if not destlocks[voxel] and IsSafeLocation(point_pack(gx, gy, gz), combatant_locs, fear) then | |
| safe_voxels[#safe_voxels + 1] = voxel | |
| end | |
| end | |
| if #safe_voxels > 0 then | |
| local idx = 1 + InteractionRand(#safe_voxels, "NeutralRelocation", unit) | |
| local voxel = safe_voxels[idx] | |
| destlocks[voxel] = true | |
| local tx, ty, tz = point_unpack(voxel) | |
| local path = cpath:GetCombatPathFromPos(point(tx, ty, tz)) | |
| local goto_ap = cpath.paths_ap[voxel] or 0 | |
| if path and AIStartCombatAction("Move", unit, goto_ap, { goto_pos = point(point_unpack(path[1])) }) then | |
| moving[#moving + 1] = unit | |
| end | |
| end | |
| end | |
| end | |
| RunProjectorTurnAndWait() | |
| if #moving > 0 then | |
| for _, unit in ipairs(moving) do | |
| WaitCombatActionsEnd(unit) | |
| end | |
| else | |
| Sleep(100) | |
| end | |
| end | |
| function SetFixedCamera(unit) | |
| if ActionCameraPlaying then | |
| WaitMsg("ActionCameraRemoved", 3000) | |
| end | |
| if not cameraTac.GetForceMaxZoom() then | |
| cameraTac.SetForceMaxZoom(true) | |
| Sleep(hr.CameraTacOverviewTime * 10) | |
| end | |
| SnapCameraToObj(unit) | |
| cameraTac.SetFixedLookat(true) | |
| end | |
| function RemoveFixedCamera() | |
| if ActionCameraPlaying then | |
| WaitMsg("ActionCameraRemoved", 3000) | |
| end | |
| if cameraTac.GetForceMaxZoom() then | |
| cameraTac.SetForceMaxZoom(false) | |
| Sleep(hr.CameraTacOverviewTime * 10) | |
| end | |
| cameraTac.SetFixedLookat(false) | |
| end | |
| function Combat:AITurn(team) | |
| if g_ShouldRebuildVisField then | |
| RebuildMapVisField() | |
| end | |
| CreateAIExecutionController() -- waits internally if there's another one active | |
| -- prepare bombards based on unit squads | |
| local bombard_squads = {} | |
| for _, unit in ipairs(team.units) do | |
| local squad = gv_Squads[unit.Squad] | |
| local id = squad and squad.enemy_squad_def or false | |
| local squad_def = EnemySquadDefs[id] | |
| if squad_def and squad_def.Bombard and not table.find(bombard_squads, id) then | |
| bombard_squads[#bombard_squads + 1] = id | |
| bombard_squads[id] = unit.team | |
| end | |
| if squad_def and squad_def.Bombard then | |
| self:QueueBombard(string.format("squad: %s", id), unit.team, squad_def.BombardAreaRadius, squad_def.BombardOrdnance, squad_def.BombardShots, squad_def.BombardLaunchOffset, squad_def.BombardLaunchAngle) | |
| end | |
| end | |
| local start_ai_turn_gametime = GameTime() | |
| AIAssignToEmplacements(team) | |
| if self.log_ai_side == team.side then | |
| g_AIExecutionController.enable_logging = true | |
| end | |
| local units = table.ifilter(team.units, function(idx, unit) return not unit:HasStatusEffect("Panicked") and not unit:HasStatusEffect("Berserk") end) | |
| for _, unit in ipairs(units) do | |
| unit.ai_context = nil | |
| end | |
| g_AIExecutionController:Execute(units) | |
| -- bombard | |
| local bombed_units = {} | |
| local queued = self.queued_bombards | |
| self.queued_bombards = {} | |
| for _, bombard in ipairs(queued) do | |
| if g_Teams[bombard.team] == team then | |
| local ordnance = g_Classes[bombard.ordnance] | |
| local radius = (bombard.radius + (ordnance and ordnance.AreaOfEffect or 0)) * const.SlabSizeX | |
| local target_unit = self:PickBombardTarget(team, bombed_units, radius) | |
| if target_unit then | |
| local zone = PlaceObject("BombardZone") | |
| zone:Setup(target_unit:GetPos(), bombard.radius, team.side, bombard.ordnance, bombard.shots) | |
| zone.bombard_offset = bombard.launchOffset | |
| zone.bombard_dir = bombard.launchAngle | |
| end | |
| else | |
| local idx = #self.queued_bombards + 1 | |
| self.queued_bombards[bombard.id] = idx | |
| self.queued_bombards[idx] = bombard | |
| end | |
| end | |
| Sleep( Max(0, start_ai_turn_gametime + 1200 - GameTime()) ) | |
| RemoveFixedCamera() | |
| DoneObject(g_AIExecutionController) | |
| end | |
| function Combat:QueueBombard(id, team, radius, ordnance, shots, launchOffset, launchAngle) | |
| if self.queued_bombards[id] then | |
| return | |
| end | |
| if type(team) ~= "number" then | |
| team = table.find(g_Teams, team) | |
| end | |
| if not team then | |
| return | |
| end | |
| local idx = #self.queued_bombards + 1 | |
| local activation_turn = self.current_turn + ((team == g_CurrentTeam) and 1 or 0) | |
| self.queued_bombards[id] = idx | |
| self.queued_bombards[idx] = { | |
| team = team, | |
| radius = radius, | |
| ordnance = ordnance, | |
| shots = shots, | |
| launchOffset = launchOffset, | |
| launchAngle = launchAngle, | |
| id = id, | |
| activation_turn = activation_turn, | |
| } | |
| end | |
| function Combat:PickBombardTarget(team, targeted_units, bombard_radius) | |
| local vis_enemies = table.ifilter(g_Visibility[team] or empty_table, function(idx, unit) return team:IsEnemySide(unit.team) end) | |
| if #vis_enemies == 0 then return end | |
| local scores = {} | |
| local best_score = 0 | |
| for _, unit in ipairs(vis_enemies) do | |
| local score = targeted_units[unit] and 0 or 100 | |
| for _, ally in ipairs(team.units) do | |
| if ally:GetDist(unit) > bombard_radius then | |
| score = score + 1 | |
| end | |
| end | |
| scores[unit] = score | |
| best_score = Max(best_score, score) | |
| end | |
| local best = table.ifilter(vis_enemies, function(idx, unit) return scores[unit] == best_score end) | |
| local tbl = (#best > 0) and best or vis_enemies | |
| local target = table.interaction_rand(tbl, "Bombard") | |
| targeted_units[target] = true | |
| return target | |
| end | |
| function Combat:OnUnitDamaged(unit, attacker) | |
| if not self.active_unit_shown and attacker and attacker == self:GetActiveUnit() and self:IsVisibleByPoVTeam(unit) then | |
| --[[SnapCameraToObj(unit) | |
| if IModeCombatMovement.UnitAvailableForNextUnitSelection(nil, attacker) then | |
| --Sleep(1000) | |
| SnapCameraToObj(attacker) | |
| end--]] | |
| self.active_unit_shown = true | |
| end | |
| -- reset appeal of the emplacement used by the damaged unit | |
| local emp = unit and self:GetEmplacementAssignment(unit) | |
| if emp and emp.appeal then | |
| emp.appeal[unit.team.side] = nil | |
| end | |
| end | |
| function Combat:GetNextUnitIdx(team, currentUnit) | |
| local units = team.units | |
| local idx = table.find(units, currentUnit) or 0 | |
| local n = #units | |
| for i = 1, n do | |
| local j = i+idx | |
| if j > n then j = j - n end | |
| local u = units[j] | |
| if u:CanBeControlled() then | |
| return j | |
| end | |
| end | |
| end | |
| function Combat:GetPrevUnitIdx(team, currentUnit) | |
| local units = team.units | |
| local idx = table.find(units, currentUnit) or 0 | |
| local n = #units | |
| for i = 1, n do | |
| local j = idx-i | |
| if j < 1 then j = n end | |
| local u = units[j] | |
| if u:CanBeControlled() then | |
| return j | |
| end | |
| end | |
| end | |
| function Combat:ChangeSelectedUnit(direction, team, force) | |
| local unit = SelectedObj | |
| if not force and not (IsKindOf(unit, "Unit") and unit:CanBeControlled()) then | |
| return | |
| end | |
| team = team or unit and unit.team or g_Teams and g_Teams[g_CurrentTeam] | |
| if team ~= g_Teams[g_CurrentTeam] then | |
| return | |
| end | |
| if not IsNetPlayerTurn() then | |
| return | |
| end | |
| local igi = GetInGameInterfaceModeDlg() | |
| if not IsKindOf(igi, "IModeCombatMovement") and igi.window_state ~= "destroying" then | |
| SetInGameInterfaceMode("IModeCombatMovement") | |
| end | |
| local fTeam = GetFilteredCurrentTeam(team) | |
| local nextUnitIdx = direction=="next" and self:GetNextUnitIdx(fTeam, unit) or self:GetPrevUnitIdx(fTeam, unit) | |
| local newUnit = nextUnitIdx and fTeam.units[nextUnitIdx] | |
| -- If havent found a new unit in the filtered team, try to find one in the non filtered team. | |
| if not newUnit then | |
| nextUnitIdx = direction=="next" and self:GetNextUnitIdx(team, unit) or self:GetPrevUnitIdx(team, unit) | |
| newUnit = nextUnitIdx and team.units[nextUnitIdx] | |
| end | |
| if newUnit then | |
| SelectObj(newUnit) | |
| SnapCameraToObj(newUnit) | |
| return | |
| end | |
| end | |
| -- next controllable unit | |
| function Combat:NextUnit(team, force) | |
| return self:ChangeSelectedUnit("next", team, force) | |
| end | |
| -- next controllable unit | |
| function Combat:PrevUnit(team, force) | |
| return self:ChangeSelectedUnit("prev", team, force) | |
| end | |
| function OnMsg.CombatActionEnd(unit) | |
| if not g_Combat then return end | |
| if g_Combat.active_unit == unit then | |
| g_Combat:SetActiveUnit(false) | |
| end | |
| end | |
| function Combat:SetActiveUnit(unit) | |
| if self.active_unit == unit then | |
| return | |
| end | |
| self.active_unit = unit | |
| self.active_unit_shown = false | |
| end | |
| function Combat:GetActiveUnit() | |
| return self.active_unit | |
| end | |
| function GetRandomMapMerc(squad_id, seed) | |
| local units = {} | |
| for _, unit in ipairs(g_Units) do | |
| if unit:IsMerc() and not unit:IsDead() and unit.team and (unit.team.side == "player1" or unit.team.side == "player2") then | |
| if not squad_id or unit.Squad==squad_id then | |
| units[#units +1] = unit | |
| end | |
| end | |
| end | |
| if not next(units) then | |
| return | |
| end | |
| local rand = 1 + BraidRandom(seed,#units) | |
| return units[rand] | |
| end | |
| function Combat:AssignEmplacement(emplacement, unit) | |
| assert(emplacement) | |
| local assignment = self.emplacement_assignment[emplacement] | |
| assert(not assignment or not unit) | |
| if assignment then | |
| self.emplacement_assignment[assignment] = nil | |
| end | |
| self.emplacement_assignment[emplacement] = unit | |
| if unit then | |
| self.emplacement_assignment[unit] = emplacement | |
| end | |
| end | |
| function Combat:GetEmplacementAssignment(obj) | |
| return self.emplacement_assignment[obj] | |
| end | |
| DefineClass.CombatTeam = { | |
| __parents = { "InitDone" }, | |
| -- Do not forget to add new members to Get/SetDynamicData function to save in Game state (see GetCurrentGameVarValues) | |
| units = false, | |
| control = false, | |
| team_color = RGB(100, 100, 100), | |
| spawn_marker_group = "", | |
| side = "neutral", | |
| team_mask = 0, | |
| ally_mask = 0, | |
| enemy_mask = 0, | |
| player_team = false, | |
| player_ally = false, | |
| player_enemy = false, | |
| neutral = false, | |
| tactical_situations_vr = false, | |
| morale = 0, -- for player teams only | |
| seen_units = false, -- for player teams only, used for camera panning to newly seen enemies | |
| } | |
| function CombatTeam:Init() | |
| self.units = self.units or {} | |
| self.seen_units = self.seen_units or {} | |
| self.tactical_situations_vr = self.tactical_situations_vr or {} | |
| end | |
| function CombatTeam:OnEnemySighted(unit) | |
| Msg("EnemySighted", self, unit) | |
| end | |
| function CombatTeam:OnEnemyLost(unit) | |
| end | |
| function CombatTeam:IsEnemySide(other) | |
| return band(self.enemy_mask, other.team_mask) ~= 0 | |
| end | |
| function CombatTeam:IsAllySide(other) | |
| return band(self.ally_mask, other.team_mask) ~= 0 | |
| end | |
| function CombatTeam:HasAliveUnits(aware) | |
| for _, unit in ipairs(self.units) do | |
| if not unit:IsDead() then | |
| local enemies = GetAllEnemyUnits(unit) | |
| local alive_enemies = 0 | |
| for _, enemy in ipairs(enemies) do | |
| if not enemy:IsDead() then | |
| alive_enemies = alive_enemies + 1 | |
| end | |
| end | |
| if not aware or (unit:IsAware("pending") and alive_enemies > 0) then | |
| return true | |
| end | |
| end | |
| end | |
| end | |
| function CombatTeam:IsDefeated() | |
| local defeated = true | |
| for _, unit in ipairs(self.units) do | |
| defeated = defeated and not unit:CanContinueCombat() | |
| end | |
| return defeated | |
| end | |
| function CombatTeam:GetDynamicData(team_data) | |
| if #(self.units or "") > 0 then | |
| local unit_handles = {} | |
| for i, unit in ipairs(self.units) do | |
| unit_handles[i] = unit:GetHandle() | |
| end | |
| team_data.units = unit_handles | |
| end | |
| if #self.seen_units > 0 then | |
| team_data.seen_units = table.copy(self.seen_units) | |
| end | |
| team_data.control = self.control | |
| team_data.side = self.side | |
| team_data.team_color = self.team_color | |
| team_data.spawn_marker_group = self.spawn_marker_group ~= "" and self.spawn_marker_group or nil | |
| team_data.morale = self.morale | |
| team_data.tactical_situations_vr = table.copy(self.tactical_situations_vr, "deep") | |
| local revealed = g_RevealedUnits[self] | |
| if revealed and #revealed > 0 then | |
| team_data.revealed_units = {} | |
| for i, unit in ipairs(revealed) do | |
| team_data.revealed_units[i] = unit:GetHandle() | |
| end | |
| end | |
| end | |
| function CombatTeam:SetDynamicData(team_data) | |
| self.control = team_data.control | |
| self.side = team_data.side or ((self.control and "UI" and "player1") or "enemy1") | |
| self.team_color = team_data.team_color | |
| self.spawn_marker_group = team_data.spawn_marker_group | |
| self.morale = team_data.morale | |
| self.tactical_situations_vr = table.copy(self.tactical_situations_vr, "deep") | |
| self.units = {} | |
| for _, handle in ipairs(team_data.units) do | |
| local obj = HandleToObject[handle] | |
| if obj then | |
| table.insert(self.units, obj) | |
| obj:SetTeam(self) | |
| end | |
| end | |
| self.seen_units = team_data.seen_units or {} | |
| if team_data.revealed_units then | |
| g_RevealedUnits[self] = g_RevealedUnits[self] or {} | |
| end | |
| for _, handle in ipairs(team_data.revealed_units) do | |
| -- Prior to fixing 186751 traps leaked in this table. | |
| local obj = HandleToObject[handle] | |
| if IsKindOf(obj, "Unit") then | |
| table.insert_unique(g_RevealedUnits[self], obj) | |
| end | |
| end | |
| end | |
| function CombatTeam:IsPlayerControlled() | |
| return self.side == "player1" or self.side == "player2" | |
| end | |
| function GetCombatPath(unit, stance, ap, end_stance) | |
| -- It's possible for the unit to be on a non-passble slab during transitions. | |
| local x,y,z = GetPassSlabXYZ(unit) | |
| stance = stance or unit.stance | |
| if g_CombatPath and unit == g_CombatPath.unit | |
| and stance == g_CombatPath.stance | |
| and ((end_stance or unit.stance) == (g_CombatPath.stance_at_end or unit.stance)) | |
| and g_CombatPath.start_pos == unit:GetPos() | |
| and g_CombatPath.ap == unit.ActionPoints | |
| and g_CombatPath.move_modifier == unit:GetMoveModifier(stance) | |
| and (not ap or ap == g_CombatPath.ap) | |
| then | |
| return g_CombatPath | |
| end | |
| --local combatPath = CombatPath:new() | |
| --combatPath:RebuildPaths(unit, ap or nil, nil, stance) | |
| local combatPath = GetCombatPathKeepStanceAware(unit, end_stance or stance, ap or nil) | |
| if not ap and unit == SelectedObj then | |
| g_CombatPath = combatPath | |
| end | |
| return combatPath | |
| end | |
| function CombatPathReset(unit, reason) | |
| if g_CombatPath and (not unit or g_CombatPath.unit == unit) then | |
| g_CombatPath = false | |
| end | |
| Msg("CombatPathReset") | |
| end | |
| function OnMsg.UnitStanceChanged(unit) | |
| CombatPathReset(unit) | |
| end | |
| function OnMsg.UnitDied() | |
| CombatPathReset(SelectedObj) | |
| end | |
| function OnMsg.UnitMovementDone() | |
| CombatPathReset(SelectedObj) | |
| end | |
| function OnMsg.DoorStateChanged() | |
| CombatPathReset() | |
| end | |
| function OnMsg.OnPassabilityChanged() | |
| CombatPathReset() | |
| end | |
| function OnMsg.SelectedObjChange() | |
| if g_Combat then | |
| ObjModified(g_Combat) | |
| end | |
| end | |
| function OnMsg.OccupiedChange() | |
| CombatPathReset() | |
| if g_Combat then | |
| ObjModified(g_Combat) | |
| end | |
| end | |
| function OnMsg.UnitAPChanged(unit, reason) | |
| CombatPathReset(unit, reason) | |
| if g_Combat then | |
| ObjModified(g_Combat) | |
| end | |
| end | |
| function OnMsg.NetPlayerLeft(player, reason) | |
| if not g_Combat then return end | |
| g_Combat.player_end_turn = {} | |
| g_Combat.player_end_turn_local_effects = false | |
| ObjModified(g_Combat) | |
| ObjModified(SelectedObj) | |
| end | |
| function OnMsg.NetGameLeft() | |
| if not g_Combat then return end | |
| g_Combat.player_end_turn = {} | |
| g_Combat.player_end_turn_local_effects = false | |
| end | |
| function NetSyncLocalEffects.EndTurn() | |
| if not g_Combat then return end | |
| g_Combat.player_end_turn_local_effects = true | |
| g_Combat:EndCombatCheck() | |
| end | |
| function NetSyncRevertLocalEffects.EndTurn(player_id) | |
| if not g_Combat then return end | |
| g_Combat.player_end_turn_local_effects = false | |
| end | |
| function NetSyncEvents.EndTurn(player_id) | |
| local localPlayer = player_id == netUniqueId | |
| local shownUI = not localPlayer or (g_Teams and g_CurrentTeam and g_Teams[g_CurrentTeam].control == "UI" and not g_AIExecutionController) | |
| if not g_Combat or not shownUI or IsSetpiecePlaying() then return end | |
| -- Unready | |
| if g_Combat.player_end_turn[player_id] then | |
| g_Combat.player_end_turn[player_id] = false | |
| if localPlayer then | |
| ObjModified(SelectedObj) | |
| end | |
| return | |
| end | |
| if localPlayer and not g_Combat.player_end_turn[player_id] then | |
| PlayFX("EndTurn", "start") | |
| end | |
| g_Combat.player_end_turn[player_id] = true | |
| g_Combat:CheckEndTurn() | |
| if localPlayer then | |
| ObjModified(SelectedObj) | |
| end | |
| end | |
| function Combat:GetDynamicData(dynamic_data) | |
| local combat_data = dynamic_data -- dynamic_data here is a separate table created for us by OnMsg.SaveDynamicData | |
| combat_data.combat_id = self.combat_id | |
| combat_data.combat_time = self.combat_time | |
| combat_data.stealth_attack_start = self.stealth_attack_start | |
| combat_data.last_attack_kill = self.last_attack_kill | |
| local unit_handles = {} | |
| for i, unit in ipairs(g_Units) do | |
| local handle = unit:GetHandle() | |
| unit_handles[i] = handle | |
| end | |
| combat_data.units = unit_handles | |
| combat_data.teams = {} | |
| for i, team in ipairs(g_Teams) do | |
| local team_data = {} | |
| team:GetDynamicData(team_data) | |
| combat_data.teams[i] = team_data | |
| end | |
| combat_data.time_of_day = self.time_of_day | |
| combat_data.turns_no_visibility = self.turns_no_visibility ~= 0 and self.turns_no_visibility or nil | |
| combat_data.current_team = g_CurrentTeam | |
| combat_data.current_turn = self.current_turn | |
| combat_data.enemies_engaged = self.enemies_engaged or nil | |
| combat_data.units_repositioned = table.copy(self.units_repositioned) | |
| combat_data.unit_reposition_shown = self.unit_reposition_shown | |
| combat_data.start_reposition_ended = self.start_reposition_ended | |
| combat_data.retreat_enemies = self.retreat_enemies or nil | |
| combat_data.queued_bombards = table.copy(self.queued_bombards, "deep") | |
| combat_data.autosave_enabled = self.autosave_enabled or nil | |
| combat_data.lastStandingVR = self.lastStandingVR | |
| combat_data.starting_unit = IsValid(self.starting_unit) and self.starting_unit:GetHandle() or nil | |
| combat_data.emplacement_assignment = {} | |
| for key, value in pairs(self.emplacement_assignment) do | |
| combat_data.emplacement_assignment[key.handle] = value.handle | |
| end | |
| combat_data.hp_loss = self.hp_loss and table.copy(self.hp_loss) | |
| combat_data.hp_healed = self.hp_healed and table.copy(self.hp_healed) | |
| combat_data.turn_dist_travelled = self.turn_dist_travelled | |
| combat_data.out_of_ammo = self.out_of_ammo and table.copy(self.out_of_ammo) | |
| end | |
| function Combat:SetDynamicData(combat_data) | |
| self.combat_id = combat_data.combat_id | |
| self.combat_time = combat_data.combat_time | |
| self.stealth_attack_start = combat_data.stealth_attack_start | |
| self.last_attack_kill = combat_data.last_attack_kill or combat_data.stealth_attack_start_kill -- backward compatibility for renamed property | |
| g_CurrentTeam = g_CurrentTeam or combat_data.current_team or false | |
| self.time_of_day = combat_data.time_of_day | |
| self.turns_no_visibility = combat_data.turns_no_visibility | |
| self.current_turn = combat_data.current_turn | |
| self.enemies_engaged = combat_data.enemies_engaged | |
| self.units_repositioned = table.copy(combat_data.units_repositioned or empty_table) | |
| self.unit_reposition_shown = combat_data.unit_reposition_shown | |
| self.start_reposition_ended = combat_data.start_reposition_ended | |
| self.retreat_enemies = combat_data.retreat_enemies or false | |
| self.queued_bombards = table.copy(combat_data.queued_bombards, "deep") | |
| self.autosave_enabled = combat_data.autosave_enabled | |
| self.lastStandingVR = combat_data.lastStandingVR | |
| self.starting_unit = combat_data.starting_unit and HandleToObject[combat_data.starting_unit] | |
| self.emplacement_assignment = {} | |
| for key, value in pairs(combat_data.emplacement_assignment) do | |
| local ok = HandleToObject[key] | |
| local ov = HandleToObject[value] | |
| self.emplacement_assignment[ok] = ov | |
| end | |
| if combat_data.units and next(combat_data.units) then | |
| local unit_handles = {} | |
| g_Units = {} | |
| for _,handle in ipairs(combat_data.units) do | |
| local obj = HandleToObject[handle] | |
| if obj then | |
| AddToGlobalUnits(obj) | |
| end | |
| end | |
| end | |
| g_Teams = {} | |
| for i, team_data in ipairs(combat_data.teams) do | |
| local team = CombatTeam:new() | |
| team:SetDynamicData(team_data) | |
| g_Teams[i] = team | |
| end | |
| -- Assign ephemeral units | |
| local neutralTeam = table.find_value(g_Teams, "side", "neutral") | |
| for _, unit in ipairs(g_Units) do | |
| if not unit.team then | |
| assert(unit.ephemeral) | |
| unit:SetTeam(neutralTeam) | |
| end | |
| end | |
| self.hp_loss = combat_data.hp_loss and table.copy(combat_data.hp_loss) or false | |
| self.hp_healed = combat_data.hp_healed and table.copy(combat_data.hp_healed) or false | |
| self.turn_dist_travelled = combat_data.turn_dist_travelled | |
| self.out_of_ammo = combat_data.out_of_ammo and table.copy(combat_data.out_of_ammo) or false | |
| end | |
| function Combat:SetRepositioned(unit, state) | |
| self.units_repositioned[unit.handle] = state | |
| end | |
| function Combat:IsRepositioned(unit) | |
| return self.units_repositioned[unit.handle] or false | |
| end | |
| function OnMsg.LoadDynamicData(dynamic_data) | |
| if gv_ActiveCombat and gv_CurrentSectorId == gv_ActiveCombat then | |
| g_Combat = Combat:new() | |
| g_Combat.skip_first_autosave = true | |
| if dynamic_data.g_Combat then | |
| g_Combat:SetDynamicData(dynamic_data.g_Combat) | |
| end | |
| CreateGameTimeThread(function(dynamic_data) | |
| g_Combat:Start(dynamic_data.g_Combat) | |
| ShowInGameInterface(true, false, { Mode = "IModeCombatMovement" }) | |
| end, dynamic_data) | |
| end | |
| end | |
| function OnMsg.SaveDynamicData(dynamic_data) | |
| if not g_Combat then | |
| return | |
| end | |
| dynamic_data.g_Combat = {} | |
| g_Combat:GetDynamicData(dynamic_data.g_Combat) | |
| end | |
| function OnMsg.NewGameSessionStart() | |
| if g_Combat then | |
| g_Combat:delete() | |
| g_Combat = false | |
| end | |
| end | |
| -- Enemy badge logic | |
| function OnMsg.CombatStart() | |
| if not g_UnitCombatBadgesEnabled then return end | |
| local enemy_badges = {} | |
| for _, t in ipairs(g_Teams) do | |
| if t.side == "enemy1" or t.side == "enemy2" then | |
| for _, unit in ipairs(t.units) do | |
| if not unit:IsDead() then | |
| enemy_badges[unit] = CreateBadgeFromPreset("EnemyBadge", unit, unit) | |
| end | |
| end | |
| end | |
| end | |
| g_Combat.enemy_badges = enemy_badges | |
| end | |
| function OnMsg.CombatEnd(combat) | |
| if not combat.enemy_badges then return end | |
| for _, b in pairs(combat.enemy_badges) do | |
| b:Done() | |
| end | |
| combat.enemy_badges = false | |
| SyncStartExploration() | |
| end | |
| -- Other badges which track combat | |
| function OnMsg.CombatStart() | |
| ObjModified("CombatChanged") | |
| end | |
| function OnMsg.CombatEnd() | |
| ObjModified("CombatChanged") | |
| end | |
| function NetSyncEvents.KillAllEnemies() | |
| if not g_Combat then return end | |
| g_AccumulatedTeamXP = {} | |
| for _, team in ipairs(g_Teams) do | |
| if team.side == "enemy1" or team.side == "enemy2" or team.side == "enemyNeutral" then | |
| for _, unit in ipairs(team.units) do | |
| if not unit:IsDead() then | |
| unit:TakeDirectDamage(10000) | |
| QuestAddAttackedGroups(unit.Groups, unit:IsDead()) | |
| end | |
| RewardTeamExperience(unit, GetCampaignPlayerTeam()) | |
| end | |
| end | |
| end | |
| LogAccumulatedTeamXP("debug") | |
| CombatActions_RunningState = {} | |
| end | |
| function NetSyncEvents.DefeatAllVillains() | |
| if not g_Combat then return end | |
| for _, team in ipairs(g_Teams) do | |
| if team.side == "enemy1" or team.side == "enemy2" then | |
| for _, unit in ipairs(team.units) do | |
| if unit.villain and unit.behavior ~= "Dead" and unit.command ~= "Die" then -- check retreat trigger first | |
| unit:SetCommand("Die") | |
| end | |
| end | |
| end | |
| end | |
| end | |
| if FirstLoad then | |
| PredictedDamageUnits = { } | |
| end | |
| MapVar("s_PredictedExposedUnits", {}) | |
| MapVar("s_PredictedTrapExplosions", {}) | |
| MapVar("s_PredictedHighlightedObjs", {}) | |
| MapVar("s_PredictedAOEObjs", {}) | |
| MapVar("s_PredictionNoLofTargets", {}) | |
| local function lClearPredictedExplosions(list) | |
| for i, m in ipairs(list) do | |
| DoneObject(m) | |
| end | |
| end | |
| local function lClearPredictedAOE(list) | |
| for _, obj in ipairs(list) do | |
| if IsValid(obj) then | |
| if IsKindOf(obj, "Unit") then | |
| obj:SetHighlightReason("area target", false) | |
| elseif not IsKindOf(obj, "DamagePredictable") then | |
| SetInteractionHighlightRecursive(obj, false, true) | |
| else | |
| obj:SetObjectMarking(-1) | |
| end | |
| end | |
| end | |
| end | |
| function ApplyDamagePrediction(attacker, action, args, actionResult) | |
| local target = args and args.target | |
| local target_spot_group = args and args.target_spot_group | |
| local targetIsUnit = IsKindOf(target, "Unit") | |
| local targetIsTrap = IsKindOf(target, "Trap") | |
| local targetIsPoint = IsPoint(target) | |
| if not target or (not targetIsUnit and not targetIsPoint and not targetIsTrap) then return end | |
| -- Save trap explosion meshes from clear to prevent flicker. | |
| local trapExplosions = s_PredictedTrapExplosions | |
| s_PredictedTrapExplosions = false | |
| -- Save aoe explosions from clear to prevent flicker. | |
| local aoeObjs = s_PredictedAOEObjs | |
| s_PredictedAOEObjs = false | |
| -- Clear old prediction | |
| ClearDamagePrediction() | |
| s_PredictedHighlightedObjs = s_PredictedHighlightedObjs or {} | |
| s_PredictedTrapExplosions = s_PredictedTrapExplosions or {} | |
| s_PredictedAOEObjs = s_PredictedAOEObjs or {} | |
| if not targetIsPoint and target:IsDead() then | |
| lClearPredictedExplosions(trapExplosions) | |
| return | |
| end | |
| -- Additional info needed for prediction icon | |
| local weapon1, weapon2 = attacker and action:GetAttackWeapons(attacker) | |
| local ignore_cover = IsKindOf(weapon1, "Firearm") and weapon1.IgnoreCoverReduction > 0 or IsKindOf(weapon2, "Firearm") and weapon2.IgnoreCoverReduction > 0 | |
| local aimType = action.AimType | |
| local attackerPosition | |
| if IsPoint(target) and aimType ~= "cone" then | |
| attackerPosition = target | |
| elseif args and args.step_pos then | |
| attackerPosition = args.step_pos | |
| else | |
| attackerPosition = attacker:GetPos() | |
| end | |
| if not ignore_cover and targetIsUnit then | |
| ignore_cover = not target:IsAware() | |
| end | |
| actionResult = actionResult or action:GetActionResults(attacker, args) or empty_table | |
| local attackResultHits = table.icopy(actionResult) | |
| table.iappend(attackResultHits, actionResult.area_hits) | |
| table.iappend(s_PredictionNoLofTargets, actionResult.no_lof_targets or empty_table) | |
| for _, obj in ipairs(s_PredictionNoLofTargets) do | |
| if IsKindOf(obj, "Unit") then | |
| ObjModified(obj) | |
| if obj.ui_badge then | |
| obj.ui_badge:SetActive(true, "dmg") | |
| end | |
| end | |
| end | |
| if #attackResultHits == 0 then | |
| lClearPredictedAOE(aoeObjs) | |
| lClearPredictedExplosions(trapExplosions) | |
| return | |
| end | |
| local stealthKill = (actionResult.stealth_kill_chance or 0) > 0 and actionResult.stealth_kill_chance | |
| local hideConditional = false | |
| if actionResult and actionResult.chance_to_hit then | |
| hideConditional = CthVisible() and actionResult.chance_to_hit == 0 | |
| end | |
| local results = {} | |
| if not targetIsPoint then | |
| results[target] = { total_damage = 0, direct_hit_damage = 0, stray_hit_damage = 0, obstructed = false, death = false } | |
| end | |
| local function lApplyResultsToPrediction(hits, action) | |
| for i, hit in ipairs(hits) do | |
| local obj = hit.obj | |
| if IsKindOf(obj, "CombatObject") and obj:IsDead() then goto continue end | |
| if not IsKindOfClasses(obj, "CombatObject", "Destroyable") then goto continue end | |
| if not IsKindOf(obj, "DamagePredictable") and false then -- Disabled in 184705 | |
| -- highlight only | |
| if ShouldDestroyObject(obj) then | |
| SetInteractionHighlightRecursive(obj, true, true) | |
| s_PredictedHighlightedObjs[#s_PredictedHighlightedObjs + 1] = obj | |
| end | |
| goto continue | |
| end | |
| -- Special highlight for those hit by aoe | |
| if hit.aoe or IsOverwatchAction(action.id) then | |
| if not table.find(aoeObjs, obj) then | |
| if IsKindOf(obj, "Unit") then | |
| if (hit.aoe_type or "none") == "none" then | |
| obj:SetHighlightReason("area target", true) | |
| end | |
| elseif not IsKindOf(obj, "DamagePredictable") then | |
| if ShouldDestroyObject(obj) then | |
| SetInteractionHighlightRecursive(obj, true, true) | |
| end | |
| else | |
| obj:SetObjectMarking(3) | |
| end | |
| end | |
| s_PredictedAOEObjs[#s_PredictedAOEObjs + 1] = obj | |
| end | |
| if not IsKindOf(obj, "DamagePredictable") then | |
| goto continue | |
| end | |
| if IsKindOf(obj, "Trap") then | |
| if not obj:HitWillDamage(hit) then goto continue end | |
| SetInteractionHighlightRecursive(obj, true, true) | |
| s_PredictedHighlightedObjs[#s_PredictedHighlightedObjs + 1] = obj | |
| end | |
| local unit_dmg = results[obj] or { total_damage = 0, direct_hit_damage = 0, stray_hit_damage = 0, obstructed = false, death = false } | |
| results[obj] = unit_dmg | |
| unit_dmg.total_damage = unit_dmg.total_damage + hit.damage | |
| if hit.stray then | |
| unit_dmg.stray_hit_damage = unit_dmg.stray_hit_damage + hit.damage | |
| elseif not hit.aoe then | |
| unit_dmg.direct_hit_damage = unit_dmg.direct_hit_damage + hit.damage | |
| end | |
| if hit.stuck then unit_dmg.obstructed = true end | |
| if IsKindOf(obj, "Unit") then | |
| if not obj:HasStatusEffect("Exposed") and hit.effects.Exposed and not s_PredictedExposedUnits[obj] then | |
| s_PredictedExposedUnits[obj] = true | |
| s_PredictedExposedUnits[#s_PredictedExposedUnits + 1] = obj | |
| end | |
| if not ignore_cover then | |
| local cover, any, coverage = obj:GetCoverPercentage(attackerPosition) | |
| if cover then | |
| -- Mock numbers for testing whether the cover counts as exposed. | |
| local value = 50 | |
| local exposedValue = 100 | |
| local coverEffectResult = InterpolateCoverEffect(coverage, value, exposedValue) | |
| -- Place the icon only if not considered exposed. | |
| if coverEffectResult ~= exposedValue then | |
| results[obj].cover = true | |
| end | |
| end | |
| end | |
| if not hit.ignore_armor and hit.armor then | |
| local armor, armorIcon, iconPath = obj:IsArmored(target_spot_group) | |
| -- It's possible for an aimed aoe shot to hit the armor of another body part. | |
| -- We don't show the armor icon in this case. | |
| if armor and armorIcon then | |
| results[obj].armor = iconPath .. (hit.armor_pen and hit.armor_pen[armor] and "ignored_" or "") .. armorIcon | |
| end | |
| end | |
| end | |
| ::continue:: | |
| end | |
| for obj, data in pairs(results) do | |
| local predictionIcon = false | |
| if data.obstructed then | |
| predictionIcon = "ObstructedIcon" | |
| elseif data.cover then | |
| predictionIcon = (s_PredictedExposedUnits[obj] or obj:HasStatusEffect("Exposed")) and "CoverExposeIcon" or "CoverIcon" | |
| elseif data.armor then | |
| predictionIcon = data.armor | |
| else | |
| -- Set a fake icon so the badge can at least activate and show | |
| -- that the unit is in range of the attack. | |
| predictionIcon = "InRange" | |
| end | |
| obj.SmallPotentialDamageIcon = predictionIcon | |
| local death = false | |
| obj.PotentialDamage = data.total_damage - data.direct_hit_damage - data.stray_hit_damage | |
| if not hideConditional or obj ~= target then | |
| if args.multishot then | |
| local shot_damage = data.direct_hit_damage / Max(1, args.num_shots or 1) | |
| obj.PotentialDamageConditional = shot_damage | |
| obj.PotentialSecondaryConditional = data.direct_hit_damage - shot_damage | |
| if targetIsTrap and obj == target then | |
| death = data.total_damage >= obj:GetTotalHitPoints() | |
| else | |
| death = data.total_damage - obj.PotentialSecondaryConditional >= obj:GetTotalHitPoints() | |
| end | |
| else | |
| obj.PotentialDamageConditional = data.direct_hit_damage | |
| obj.PotentialSecondaryConditional = data.stray_hit_damage | |
| death = data.total_damage - data.stray_hit_damage >= obj:GetTotalHitPoints() | |
| end | |
| end | |
| data.death = death | |
| obj.StealthKillChance = stealthKill or -1 | |
| obj.LargePotentialDamageIcon = stealthKill and "PotentialDeathIcon" | |
| table.insert_unique(PredictedDamageUnits, obj) | |
| ObjModified(obj) | |
| if obj.ui_badge then | |
| obj.ui_badge:SetActive(true, "dmg") | |
| end | |
| end | |
| end | |
| lApplyResultsToPrediction(attackResultHits, action) | |
| -- Create trap explosion predictions for traps that will explode due to the attack. | |
| local traps = {} | |
| for target, data in pairs(results) do | |
| if data.death and IsKindOf(target, "Trap") and target.discovered_trap and target.visible and (not IsKindOf(target, "BoobyTrappable") or target.boobyTrapType == const.BoobyTrapExplosive) then | |
| traps[#traps + 1] = target | |
| end | |
| end | |
| local newExplosionMeshes = traps and {} | |
| for i, t in ipairs(traps) do | |
| local aoeParams = t:GetAreaAttackParams(nil, attacker) | |
| local explosion_pos = t:GetPos() | |
| if not explosion_pos:IsValidZ() then explosion_pos = explosion_pos:SetTerrainZ() end | |
| local range = aoeParams.max_range * const.SlabSizeX | |
| local step_positions, step_objs, los_values = GetAOETiles(explosion_pos, aoeParams.stance, range) | |
| local existingMeshIdx = table.find(trapExplosions, "source", t) | |
| local explosionMesh = CreateAOETilesCircle(step_positions, step_objs, existingMeshIdx and trapExplosions[existingMeshIdx], explosion_pos, range, los_values, "ExplodingBarrelRange_Tiles") | |
| explosionMesh:SetColorFromTextStyle("GrenadeRange") | |
| explosionMesh.source = t | |
| newExplosionMeshes[#newExplosionMeshes + 1] = explosionMesh | |
| if existingMeshIdx then table.remove(trapExplosions, existingMeshIdx) end | |
| end | |
| local aoeObjectsLeftover = table.subtraction(aoeObjs or empty_table, s_PredictedAOEObjs) | |
| lClearPredictedAOE(aoeObjectsLeftover) | |
| s_PredictedTrapExplosions = newExplosionMeshes | |
| lClearPredictedExplosions(trapExplosions) | |
| end | |
| function ClearDamagePrediction() | |
| for i, u in ipairs(PredictedDamageUnits) do | |
| if IsValid(u) then | |
| u.SmallPotentialDamageIcon = false | |
| u.LargePotentialDamageIcon = false | |
| u.PotentialDamage = 0 | |
| u.PotentialDamageConditional = 0 | |
| u.PotentialSecondaryConditional = 0 | |
| u.StealthKillChance = -1 | |
| ObjModified(u) | |
| if u.ui_badge then | |
| u.ui_badge:SetActive(false, "dmg") | |
| end | |
| end | |
| end | |
| for _, obj in ipairs(s_PredictedHighlightedObjs) do | |
| SetInteractionHighlightRecursive(obj, false, true) | |
| end | |
| if s_PredictedHighlightedObjs then | |
| table.iclear(s_PredictedHighlightedObjs) | |
| end | |
| lClearPredictedAOE(s_PredictedAOEObjs) | |
| lClearPredictedExplosions(s_PredictedTrapExplosions) | |
| s_PredictedTrapExplosions = false | |
| table.clear(PredictedDamageUnits) | |
| table.clear(s_PredictedExposedUnits) | |
| table.clear(s_PredictedAOEObjs) | |
| table.clear(s_PredictionNoLofTargets) | |
| end | |
| function RebuildMapVisField() | |
| local sizex, sizey = terrain.GetMapSize() | |
| local bbox = box(0, 0, 0, sizex, sizey, MapSlabsBBox_MaxZ) | |
| RebuildVisField(bbox) | |
| g_ShouldRebuildVisField = false | |
| end | |
| function OnMsg.PostNewMapLoaded() | |
| RebuildMapVisField() | |
| end | |
| function OnMsg.CombatObjectDied() | |
| g_ShouldRebuildVisField = true | |
| end | |
| function OnMsg.CanSaveGameQuery(query, request) | |
| if g_Combat then | |
| local currentTeam = g_Teams[g_CurrentTeam] | |
| if currentTeam and currentTeam.side ~= "player1" then | |
| query.current_team_side = currentTeam.side or "not player1" | |
| end | |
| if g_AIExecutionController then | |
| query.ai_turn = true | |
| end | |
| if IsGameRuleActive("Ironman") and (not request or request.autosave_id ~= "combatStart") then | |
| query.ironman = true | |
| end | |
| for _, unit in ipairs(g_Units) do | |
| if not unit:IsIdleCommand() then | |
| query.unit_actions = true | |
| end | |
| end | |
| end | |
| end | |
| function OnMsg.OnHeal(patient, hp, medkit, healer) | |
| if g_Combat and g_Combat.hp_healed and IsKindOf(patient, "Unit") then | |
| g_Combat.hp_healed[patient.session_id] = (g_Combat.hp_healed[patient.session_id] or 0) + hp | |
| end | |
| end | |
| function OnMsg.TurnStart() | |
| if g_Combat then | |
| g_Combat.turn_dist_travelled = {} | |
| end | |
| end | |
| local function update_unit_pos_dist(unit) | |
| if g_Combat and g_Combat.turn_dist_travelled then | |
| local pos = unit:GetPos() | |
| local entry = g_Combat.turn_dist_travelled[unit.session_id] | |
| if not entry then | |
| g_Combat.turn_dist_travelled[unit.session_id] = {last_pos = pos, total_dist = 0} | |
| else | |
| entry.total_dist = (entry.total_dist or 0) + pos:Dist(entry.last_pos) | |
| entry.last_pos = pos | |
| end | |
| end | |
| end | |
| OnMsg.CombatActionStart = update_unit_pos_dist | |
| OnMsg.CombatActionEnd = update_unit_pos_dist | |
| if Platform.developer then | |
| function wait_interface_mode(mode, step) | |
| while GetInGameInterfaceMode() ~= mode do | |
| Sleep(step or 10) | |
| end | |
| end | |
| function wait_interface_modes(modes, step) | |
| while not table.find(modes, GetInGameInterfaceMode()) do | |
| Sleep(step or 10) | |
| end | |
| end | |
| local function wait_not_interface_mode(mode, step) | |
| while GetInGameInterfaceMode() == mode do | |
| Sleep(step or 10) | |
| end | |
| end | |
| local function wait_game_time(ms, step) | |
| local t = GameTime() | |
| while GameTime() < t + ms do | |
| Sleep(step or 10) | |
| end | |
| end | |
| local timeout = 5000 | |
| local function select_and_wait_control(unit) | |
| while not g_Combat:HasTeamTurnStarted(unit.team) do | |
| wait_game_time(50, 10) | |
| end | |
| local ts = GetPreciseTicks() | |
| while not unit:CanBeControlled() and (GetPreciseTicks() - ts) < timeout do | |
| Sleep(100) | |
| end | |
| if not unit:CanBeControlled() then | |
| assert(false, "Could not select unit " .. unit.session_id) | |
| return | |
| end | |
| SelectObj(unit) | |
| wait_game_time(50, 10) | |
| wait_interface_mode("IModeCombatMovement") | |
| repeat | |
| if SelectedObj ~= unit then | |
| SelectObj(unit) | |
| end | |
| wait_game_time(50, 10) | |
| until unit:CanBeControlled() and SelectedObj == unit | |
| end | |
| local function click_world_pos(pos, dlg, button) | |
| local screen_center = terminal.desktop.box:Center() | |
| ViewPos(pos) | |
| Sleep(100) | |
| SetMouseDeltaMode(true) | |
| terminal.SetMousePos(screen_center) | |
| Sleep(100) | |
| if button then | |
| dlg:OnMouseButtonDown(screen_center, button) | |
| else | |
| dlg:OnMousePos(screen_center) | |
| end | |
| SetMouseDeltaMode(false) | |
| end | |
| if FirstLoad then | |
| GameTestMapLoadRandom = false | |
| end | |
| local coreInitMapLoadRandom = InitMapLoadRandom | |
| function InitMapLoadRandom() | |
| return GameTestMapLoadRandom or coreInitMapLoadRandom() | |
| end | |
| --[[ | |
| GameTests.Combat code could be reused if the need arises for more automated tests. | |
| The proper way to do that would be to implement it in the TestCombat preset: | |
| - An ExecGameTest function should do all the setup until entering combat | |
| - Squad creation shoud be governed by additional preset_id property, allowing the TestCombat to specify | |
| which mercs are in the "player" side squad. | |
| - The same function should then process a nested list of "game turns" | |
| - Every game turn should be a nested list of (unit, actions) elements, where | |
| 'unit' is the id of the unit to play and | |
| 'actions' is a func property that is responsible to execute the chosen skill(s) (like GameTests.Combat does) | |
| - Every game turn should end automatically with "End Turn" to have the other teams (mainly AI) run their turn | |
| --]] | |
| function GameTests.Combat() | |
| ClearItemIdData() | |
| local test_combat_id = "Default" | |
| -- reset & seed interaction rand | |
| GameTestMapLoadRandom = xxhash("GameTestMapLoadRandomSeed") | |
| MapLoadRandom = InitMapLoadRandom() | |
| ResetInteractionRand(0) -- same reset at map game time 0 to get control values for interaction rand results | |
| local expected_sequence = {} | |
| for i = 1, 10 do | |
| expected_sequence[i] = InteractionRand(100, "GameTest") | |
| end | |
| -- reset game session and setup a player squad | |
| NewGameSession() | |
| CreateNewSatelliteSquad({Side = "player1", CurrentSector = "H2", Name = "GAMETEST"}, {"Buns", "Wolf", "Ivan", "Tex"}, 14, 1234567) | |
| -- start a thread to close all popups during the test | |
| local combat_test_in_progress = true | |
| CreateRealTimeThread(function() | |
| while combat_test_in_progress do | |
| if GetDialog("PopupNotification") then | |
| Dialogs.PopupNotification:Close() | |
| end | |
| Sleep(10) | |
| end | |
| end) | |
| TestCombatEnterSector(Presets.TestCombat.GameTest[test_combat_id], "__TestCombatOutlook") | |
| SetTimeFactor(10000) | |
| if IsEditorActive() then | |
| EditorDeactivate() | |
| Sleep(10) | |
| end | |
| if true then -- check for InteractionRand inconsistencies | |
| assert(MapLoadRandom == GameTestMapLoadRandom) | |
| for i = 1, 10 do | |
| local value = InteractionRand(100, "GameTest") | |
| assert(value == expected_sequence[i]) | |
| end | |
| end | |
| -- wait the ingame interface and navigate it to combat | |
| while GetInGameInterfaceMode() ~= "IModeDeployment" and GetInGameInterfaceMode() ~= "IModeExploration" do | |
| Sleep(20) | |
| end | |
| GameTestMapLoadRandom = false | |
| if GetInGameInterfaceMode() == "IModeDeployment" then | |
| Dialogs.IModeDeployment:StartExploration() | |
| while GetInGameInterfaceMode() == "IModeDeployment" do | |
| Sleep(10) | |
| end | |
| end | |
| if GetInGameInterfaceMode() == "IModeExploration" then | |
| --NetSyncEvent("ExplorationStartCombat") | |
| for _, team in ipairs(g_Teams) do | |
| if team.player_enemy and #team.units > 0 then | |
| PushUnitAlert("script", team.units, "aware") | |
| end | |
| end | |
| AlertPendingUnits() | |
| wait_interface_mode("IModeCombatMovement") | |
| end | |
| WaitUnitsInIdle() | |
| -- mark the total hp of the units to make sure the attacks deal some damage | |
| local total_hp = 0 | |
| for _, unit in ipairs(g_Units) do | |
| total_hp = total_hp + unit.HitPoints | |
| end | |
| -- alert and wait reposition to end | |
| assert(#g_Units > 4) | |
| TriggerUnitAlert("discovered", g_Units.Buns) | |
| while not g_Combat.start_reposition_ended do | |
| WaitMsg("CombatStartRepositionDone", 100) | |
| end | |
| if true then -- player turn code block | |
| if g_Units.Buns.ActionPoints >= g_Units.Buns:GetMaxActionPoints() then -- Buns code block | |
| select_and_wait_control(g_Units.Buns) | |
| while not Dialogs.IModeCombatAttack do | |
| local btn = Dialogs.IModeCombatMovement:ResolveId("SingleShot") | |
| if not btn or not btn.enabled then | |
| assert(false, "Buns SingleShot button disabled") | |
| break | |
| end | |
| btn:Press() | |
| wait_interface_mode("IModeCombatAttack") | |
| Sleep(500) | |
| end | |
| local weapon = g_Units.Buns:GetActiveWeapons() | |
| for i = 1, weapon.MaxAimActions do | |
| Dialogs.IModeCombatAttack.crosshair:ToggleAim() | |
| end | |
| Dialogs.IModeCombatAttack.crosshair.idButtonTorso:Press() | |
| wait_game_time(50, 20) | |
| WaitUnitsInIdle() | |
| SetInGameInterfaceMode("IModeCombatMovement") | |
| wait_interface_mode("IModeCombatMovement") | |
| end | |
| if g_Units.Tex.ActionPoints >= g_Units.Tex:GetMaxActionPoints() then -- Tex code block | |
| select_and_wait_control(g_Units.Tex) | |
| while not Dialogs.IModeCombatMovingAttack do | |
| local btn = Dialogs.IModeCombatMovement:ResolveId("MobileShot") | |
| if not btn.enabled then | |
| assert(false, "Tex MobileShot button disabled") | |
| break | |
| end | |
| btn:Press() | |
| wait_interface_mode("IModeCombatMovingAttack") | |
| Sleep(500) | |
| end | |
| click_world_pos(point(186600, 124200), Dialogs.IModeCombatMovingAttack, "L") | |
| wait_game_time(50, 20) | |
| WaitUnitsInIdle() | |
| SetInGameInterfaceMode("IModeCombatMovement") | |
| wait_interface_mode("IModeCombatMovement") | |
| end | |
| if g_Units.Wolf.ActionPoints >= g_Units.Wolf:GetMaxActionPoints() then -- Wolf code block | |
| select_and_wait_control(g_Units.Wolf) | |
| while not Dialogs.IModeCombatAreaAim do | |
| local btn = Dialogs.IModeCombatMovement:ResolveId("AttackShotgun") | |
| if not btn.enabled then | |
| assert(false, "Wolf AttackShotgun button disabled") | |
| break | |
| end | |
| btn:Press() | |
| wait_interface_mode("IModeCombatAreaAim") | |
| Sleep(100) | |
| end | |
| click_world_pos(point(178873, 119115, 12543), Dialogs.IModeCombatAreaAim, "L") | |
| wait_game_time(50, 20) | |
| WaitUnitsInIdle() | |
| SetInGameInterfaceMode("IModeCombatMovement") | |
| wait_interface_mode("IModeCombatMovement") | |
| end | |
| if g_Units.Ivan.ActionPoints >= g_Units.Ivan:GetMaxActionPoints() then -- Ivan code block | |
| select_and_wait_control(g_Units.Ivan) | |
| while not Dialogs.IModeCombatAreaAim do | |
| local btn = Dialogs.IModeCombatMovement:ResolveId("Overwatch") | |
| if not btn.enabled then | |
| assert(false, "Ivan Overwatch button disabled") | |
| break | |
| end | |
| btn:Press() | |
| wait_interface_mode("IModeCombatAreaAim") | |
| Sleep(100) | |
| end | |
| click_world_pos(point(198060, 126387, 11685), Dialogs.IModeCombatAreaAim, "L") | |
| wait_game_time(50, 20) | |
| WaitUnitsInIdle() | |
| SetInGameInterfaceMode("IModeCombatMovement") | |
| wait_interface_mode("IModeCombatMovement") | |
| end | |
| end | |
| if true then -- end turn/other teams' turns code block | |
| local team = g_CurrentTeam | |
| while not Dialogs.IModeCombatMovement do | |
| SetInGameInterfaceMode("IModeCombatMovement") | |
| Sleep(100) | |
| end | |
| -- start other teams' turn | |
| while g_CurrentTeam == team do | |
| Dialogs.IModeCombatMovement.idEndTurnFrame.idTurn:Press() | |
| WaitMsg("TurnEnded", 200) | |
| end | |
| -- wait other teams to finish their turns | |
| local gt = GameTime() | |
| while g_CurrentTeam ~= team do | |
| WaitMsg("TurnEnded", 20) | |
| end | |
| WaitUnitsInIdle() | |
| wait_interface_mode("IModeCombatMovement") | |
| -- make sure there were combat actions played in the AI turn | |
| assert(CombatActions_LastStartedAction and CombatActions_LastStartedAction.start_time > gt, "no actions played in AI turn") | |
| end | |
| local end_total_hp = 0 | |
| for _, unit in ipairs(g_Units) do | |
| total_hp = total_hp + unit.HitPoints | |
| end | |
| assert(end_total_hp < total_hp, "no damage dealt during the combat test") | |
| assert(#g_CurrentAttackActions == 0) | |
| combat_test_in_progress = false | |
| end | |
| --GameTests.Combat = nil | |
| function TestOtherInteractions(unit) | |
| -- Open the weapons modification menu | |
| OpenInventory(unit) | |
| local weapon = unit:GetItemInSlot("Handheld A") | |
| OpenDialog("ModifyWeaponDlg", nil, { weapon = weapon, slot = unit:GetItemPackedPos(weapon), owner = unit }) | |
| Sleep(2000) | |
| CloseDialog("ModifyWeaponDlg") | |
| -- Wait for the dialog to close | |
| while GetDialog("ModifyWeaponDlg") do Sleep(16) end | |
| CloseDialog("FullscreenGameDialogs") | |
| end | |
| ----------------------------------------------------------------------------------- | |
| -- randomstuff pseudo-test | |
| function randomstuff_selection_cycle_units() | |
| local team = GetPoVTeam() | |
| local n = 5 + AsyncRand(5) | |
| for i = 1, n do | |
| local unit = table.rand(team.units) | |
| SelectObj(unit) | |
| Sleep(500 + 1500) | |
| end | |
| end | |
| function randomstuff_movemousearound(time) | |
| local w, h = terminal.desktop.box:size():xy() | |
| time = now() + (time or 5000) | |
| while now() < time do | |
| local x, y = 200 + AsyncRand(600), 200 + AsyncRand(600) | |
| terminal.SetMousePos(point(MulDivRound(w, x, 1000), MulDivRound(h, y, 1000))) | |
| Sleep(100 + AsyncRand(900)) | |
| end | |
| end | |
| function randomstuff_clickmousearound(time) | |
| local w, h = terminal.desktop.box:size():xy() | |
| time = now() + (time or 5000) | |
| while now() < time do | |
| local x, y = 200 + AsyncRand(600), 200 + AsyncRand(600) | |
| terminal.SetMousePos(point(MulDivRound(w, x, 1000), MulDivRound(h, y, 1000))) | |
| local button = AsyncRand(100) < 50 and "L" or "R" | |
| terminal.MouseEvent("OnMouseButtonDown", terminal.GetMousePos(), button) | |
| Sleep(50) | |
| terminal.MouseEvent("OnMouseButtonUp", terminal.GetMousePos(), button) | |
| Sleep(100 + AsyncRand(900)) | |
| end | |
| end | |
| function randomstuff_inventory() | |
| OpenInventory(SelectedObj) | |
| randomstuff_movemousearound() | |
| CloseDialog("FullscreenGameDialogs") | |
| end | |
| function randomstuff_modifyweapondlg() | |
| local team = GetPoVTeam() | |
| local unit, weapon | |
| for _, u in ipairs(team.units) do | |
| local item = u:GetItemInSlot("Handheld A") | |
| if IsKindOf(item, "Firearm") then | |
| unit, weapon = u, item | |
| break | |
| end | |
| end | |
| if unit and weapon then | |
| OpenInventory(unit) | |
| OpenDialog("ModifyWeaponDlg", nil, { weapon = weapon, slot = unit:GetItemPackedPos(weapon), owner = unit }) | |
| randomstuff_movemousearound() | |
| CloseDialog("ModifyWeaponDlg") | |
| end | |
| -- Wait for the dialog to close | |
| while GetDialog("ModifyWeaponDlg") do Sleep(16) end | |
| CloseDialog("FullscreenGameDialogs") | |
| end | |
| function randomstuff_satview() | |
| OpenSatelliteView(Game.Campaign) | |
| while not g_SatelliteUI do | |
| Sleep(100) | |
| end | |
| randomstuff_movemousearound() | |
| CloseSatelliteView() | |
| end | |
| function randomstuff_changesector() | |
| OpenSatelliteView(Game.Campaign) | |
| while not g_SatelliteUI do | |
| Sleep(100) | |
| end | |
| local available_sectors = table.filter(gv_Sectors, function(id, sector) return sector.Map and id ~= gv_CurrentSectorId end) | |
| local sector_ids = table.keys(available_sectors) | |
| local id = table.rand(sector_ids) | |
| local squad = gv_Squads[SelectedObj.Squad] | |
| NetSyncEvent("CheatSatelliteTeleportSquad", squad.UniqueId, id) | |
| while squad.CurrentSector ~= id do | |
| Sleep(100) | |
| end | |
| Sleep(1000) | |
| SatelliteToggleActionRun() | |
| while true do | |
| local ok = WaitMsg("EnterSector", 1000) | |
| if ok then break end | |
| if not GameState.entering_sector then | |
| -- somehow not triggered? | |
| SatelliteToggleActionRun() | |
| end | |
| end | |
| wait_interface_modes({"IModeExploration", "IModeCombatMovement"}) | |
| end | |
| if FirstLoad then | |
| RandomStuffForeverThread = false | |
| RandomStuffForeverPopupThread = false | |
| end | |
| function DoRandomStuffForever() | |
| if not CurrentThread() or IsGameTimeThread(CurrentThread()) then | |
| CreateRealTimeThread(DoRandomStuffForever) | |
| return | |
| end | |
| if IsValidThread(RandomStuffForeverThread) then | |
| DeleteThread(RandomStuffForeverThread) | |
| end | |
| RandomStuffForeverThread = CurrentThread() | |
| RandomStuffForeverPopupThread = RandomStuffForeverPopupThread or CreateRealTimeThread(function() | |
| while true do | |
| if GetDialog("PopupNotification") then | |
| Dialogs.PopupNotification:Close() | |
| end | |
| Sleep(1000) | |
| --print(table.map(Dialogs, "class")) | |
| end | |
| end) | |
| g_AutoClickLoadingScreenStart = 1 | |
| -- quick start new game | |
| EditorDeactivate() | |
| local campaign = "HotDiamonds" | |
| local new_game_params = {difficulty = "Normal"} | |
| NetGossip("QuickStart", campaign, new_game_params, GetCurrentPlaytime(), Game and Game.CampaignTime) | |
| QuickStartCampaign(campaign, new_game_params) | |
| RevealAllSectors() | |
| wait_interface_modes({"IModeExploration", "IModeCombatMovement"}) | |
| hr.FpsCounter = 1 | |
| local randomstuff_routines = { | |
| {func = randomstuff_selection_cycle_units, weight = 100}, | |
| {func = randomstuff_clickmousearound, weight = 100}, | |
| {func = randomstuff_inventory, weight = 100}, | |
| {func = randomstuff_modifyweapondlg, weight = 100}, | |
| {func = randomstuff_satview, weight = 100}, | |
| {func = randomstuff_changesector, weight = 50}, | |
| } | |
| local last_routine | |
| while true do | |
| local routine | |
| repeat | |
| routine = table.weighted_rand(randomstuff_routines, "weight") | |
| until routine ~= last_routine | |
| last_routine = routine | |
| print(routine) | |
| routine.func() | |
| Sleep(1000) | |
| end | |
| end | |
| end |