Fork from OpenRA/OpenRA with one-click launch script (start-ra.cmd) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1814 lines
48 KiB
Lua
1814 lines
48 KiB
Lua
--[[
|
|
Copyright (c) The OpenRA Developers and Contributors
|
|
This file is part of OpenRA, which is free software. It is made
|
|
available to you under the terms of the GNU General Public License
|
|
as published by the Free Software Foundation, either version 3 of
|
|
the License, or (at your option) any later version. For more
|
|
information, see COPYING.
|
|
]]
|
|
|
|
--- This mission's human player.
|
|
local USSR = Player.GetPlayer("USSR")
|
|
|
|
--- Owner of the inactive Soviet bases.
|
|
local BadGuy = Player.GetPlayer("BadGuy")
|
|
|
|
--- Owner of most hostile Allied forces.
|
|
--- (France guards crates near the west edge.)
|
|
local Greece = Player.GetPlayer("Greece")
|
|
|
|
--- Owner of the green smoke signals.
|
|
local England = Player.GetPlayer("England")
|
|
|
|
--- Both Soviet players, grouped here for later targeting.
|
|
local SovietPlayers = { BadGuy, USSR }
|
|
|
|
---@type integer USSR must defend the Tech Center.
|
|
local TechObjective
|
|
|
|
---@type integer USSR must reach the Sub Pen.
|
|
local PenObjective
|
|
|
|
---@type integer USSR must wipe out all intruders owned by Greece.
|
|
local IntruderObjective
|
|
|
|
---@type integer USSR may quickly capture an Airfield for a bonus.
|
|
local AirfieldObjective
|
|
|
|
local TimerColor = HSLColor.White
|
|
local DefeatStarted = false
|
|
local VictoryStarted = false
|
|
local NavalBaseFound = false
|
|
local CruisersArrived = false
|
|
local CruisersKilled = false
|
|
local TanyaReinforced = false
|
|
local FirstAirfieldCaptured = false
|
|
|
|
--- Prefix for the player messages.
|
|
local BattlefieldControl = UserInterface.GetFluentMessage("battlefield-control")
|
|
|
|
--[[
|
|
Ticks until certain teams reinforce after the service base is regained.
|
|
The Normal event timings should overall be a decent match for the RA1
|
|
version, but they will differ.
|
|
|
|
The original Cruiser-activated footprints can not be relied upon because
|
|
ORA movement speed is different. The RA1 Cruisers and Longbows had delays
|
|
of ~300/~600 seconds at 5/7 speed.
|
|
]]
|
|
local EndTeamDelays =
|
|
{
|
|
easy =
|
|
{
|
|
cruisers = DateTime.Seconds(450),
|
|
longbows = DateTime.Seconds(600),
|
|
sea7_sea8 = DateTime.Seconds(454),
|
|
end1 = DateTime.Seconds(493),
|
|
end2_end5 = DateTime.Seconds(524)
|
|
},
|
|
normal =
|
|
{
|
|
cruisers = DateTime.Seconds(390),
|
|
longbows = DateTime.Seconds(600),
|
|
sea7_sea8 = DateTime.Seconds(394),
|
|
end1 = DateTime.Seconds(433),
|
|
end2_end5 = DateTime.Seconds(464)
|
|
},
|
|
hard =
|
|
{
|
|
cruisers = DateTime.Seconds(330),
|
|
longbows = DateTime.Seconds(300),
|
|
sea7_sea8 = DateTime.Seconds(334),
|
|
end1 = DateTime.Seconds(373),
|
|
end2_end5 = DateTime.Seconds(404)
|
|
}
|
|
}
|
|
|
|
--- A collection of LST attackers scheduled once the Service Depot is regained.
|
|
local EndTransportTeams =
|
|
{
|
|
end1 =
|
|
{
|
|
types = { "e3", "e3", "e3", "e3", "e3" },
|
|
path = { Waypoint90.Location, NavalRocketUnload.Location },
|
|
},
|
|
end2 =
|
|
{
|
|
types = { "2tnk", "2tnk", "arty" },
|
|
path = { Waypoint93.Location, Waypoint95.Location }
|
|
},
|
|
end5 =
|
|
{
|
|
types = { "2tnk", "2tnk", "2tnk" },
|
|
path = { Waypoint65.Location, Waypoint66.Location }
|
|
},
|
|
sea7 =
|
|
{
|
|
types = { "1tnk", "1tnk", "arty", "arty" },
|
|
path = { Waypoint94.Location, Waypoint67.Location }
|
|
},
|
|
sea8 =
|
|
{
|
|
types = { "1tnk", "1tnk", "1tnk", "arty" },
|
|
path = { Waypoint65.Location, Waypoint66.Location }
|
|
}
|
|
}
|
|
|
|
local Footprints =
|
|
{
|
|
--- Beach rescue trigger by Waypoint 74.
|
|
atk2 =
|
|
{
|
|
CPos.New(91, 92),
|
|
CPos.New(91, 93),
|
|
CPos.New(91, 94),
|
|
CPos.New(91, 95),
|
|
CPos.New(91, 96)
|
|
},
|
|
--- Outside the starting base by Waypoint 75.
|
|
atk6 =
|
|
{
|
|
CPos.New(71, 72),
|
|
CPos.New(71, 73),
|
|
CPos.New(71, 74),
|
|
CPos.New(71, 75),
|
|
CPos.New(71, 76),
|
|
CPos.New(71, 77),
|
|
CPos.New(71, 78),
|
|
CPos.New(71, 79)
|
|
},
|
|
--- Cells north/northeast of the mining outpost.
|
|
atk7 =
|
|
{
|
|
--- Ore patch cells.
|
|
CPos.New(83, 51),
|
|
CPos.New(84, 51),
|
|
CPos.New(85, 51),
|
|
CPos.New(86, 51),
|
|
CPos.New(87, 51),
|
|
CPos.New(88, 51),
|
|
CPos.New(89, 51),
|
|
CPos.New(90, 51),
|
|
CPos.New(91, 51),
|
|
CPos.New(92, 51),
|
|
CPos.New(93, 51),
|
|
--- Path to NE corner base.
|
|
CPos.New(98, 56),
|
|
CPos.New(99, 56),
|
|
CPos.New(99, 57),
|
|
CPos.New(100, 57),
|
|
CPos.New(101, 57),
|
|
CPos.New(101, 58),
|
|
CPos.New(102, 58),
|
|
CPos.New(103, 58),
|
|
CPos.New(103, 59),
|
|
CPos.New(104, 59),
|
|
CPos.New(104, 60)
|
|
},
|
|
--- Land approach to the airbase.
|
|
des3 =
|
|
{
|
|
--- West end.
|
|
CPos.New(20, 18),
|
|
CPos.New(20, 19),
|
|
CPos.New(20, 20),
|
|
CPos.New(20, 21),
|
|
CPos.New(20, 22),
|
|
CPos.New(20, 23),
|
|
CPos.New(20, 24),
|
|
CPos.New(20, 25),
|
|
CPos.New(20, 26),
|
|
CPos.New(20, 27),
|
|
CPos.New(20, 28),
|
|
CPos.New(20, 29),
|
|
--- South end.
|
|
CPos.New(21, 29),
|
|
CPos.New(22, 29),
|
|
CPos.New(23, 29),
|
|
CPos.New(24, 29),
|
|
CPos.New(25, 29),
|
|
CPos.New(26, 29),
|
|
CPos.New(27, 29),
|
|
CPos.New(28, 29),
|
|
CPos.New(29, 29),
|
|
CPos.New(30, 29),
|
|
CPos.New(31, 29),
|
|
CPos.New(32, 29),
|
|
CPos.New(33, 29),
|
|
CPos.New(34, 29),
|
|
--- East end.
|
|
CPos.New(34, 28),
|
|
CPos.New(34, 27),
|
|
CPos.New(34, 26),
|
|
CPos.New(34, 25),
|
|
CPos.New(34, 24),
|
|
CPos.New(34, 23),
|
|
CPos.New(34, 22),
|
|
CPos.New(33, 22),
|
|
CPos.New(33, 21),
|
|
CPos.New(33, 20),
|
|
CPos.New(33, 19),
|
|
CPos.New(33, 18)
|
|
},
|
|
--- River between waypoints 73 and 84.
|
|
ent2 =
|
|
{
|
|
CPos.New(35, 44),
|
|
CPos.New(35, 45),
|
|
CPos.New(36, 45),
|
|
CPos.New(36, 46),
|
|
CPos.New(37, 46),
|
|
CPos.New(37, 47),
|
|
CPos.New(38, 47),
|
|
CPos.New(38, 48),
|
|
CPos.New(38, 48),
|
|
CPos.New(39, 48),
|
|
CPos.New(39, 49),
|
|
CPos.New(39, 50)
|
|
},
|
|
--- Small strip of coast west of Waypoint 57.
|
|
ent3 =
|
|
{
|
|
CPos.New(69, 18),
|
|
CPos.New(69, 19),
|
|
CPos.New(69, 20),
|
|
CPos.New(69, 21),
|
|
CPos.New(69, 22),
|
|
CPos.New(69, 23)
|
|
},
|
|
--- River between waypoints 73 and 76.
|
|
ent4 =
|
|
{
|
|
CPos.New(21, 62),
|
|
CPos.New(22, 62),
|
|
CPos.New(23, 62),
|
|
CPos.New(24, 62),
|
|
CPos.New(25, 62),
|
|
CPos.New(26, 62),
|
|
CPos.New(27, 62),
|
|
CPos.New(28, 62),
|
|
CPos.New(29, 62),
|
|
CPos.New(30, 62)
|
|
},
|
|
--- River between waypoints 76 and 53.
|
|
ent5 =
|
|
{
|
|
CPos.New(19, 82),
|
|
CPos.New(20, 82),
|
|
CPos.New(21, 82),
|
|
CPos.New(22, 82),
|
|
CPos.New(23, 82),
|
|
CPos.New(24, 82),
|
|
CPos.New(25, 82),
|
|
CPos.New(26, 82),
|
|
CPos.New(27, 82),
|
|
CPos.New(28, 82),
|
|
CPos.New(29, 82)
|
|
},
|
|
--- River between waypoints 53 and 72.
|
|
ent6 =
|
|
{
|
|
CPos.New(19, 101),
|
|
CPos.New(20, 101),
|
|
CPos.New(21, 101),
|
|
CPos.New(22, 101),
|
|
CPos.New(23, 101),
|
|
CPos.New(24, 101),
|
|
CPos.New(25, 101),
|
|
CPos.New(26, 101),
|
|
CPos.New(27, 101),
|
|
CPos.New(28, 101),
|
|
CPos.New(29, 101),
|
|
CPos.New(30, 101)
|
|
},
|
|
--- River between waypoints 73 and 84.
|
|
rev3 =
|
|
{
|
|
CPos.New(33, 45),
|
|
CPos.New(33, 46),
|
|
CPos.New(33, 47),
|
|
CPos.New(34, 47),
|
|
CPos.New(34, 48),
|
|
CPos.New(35, 48),
|
|
CPos.New(35, 49),
|
|
CPos.New(36, 48),
|
|
CPos.New(36, 49),
|
|
CPos.New(36, 50),
|
|
CPos.New(37, 49),
|
|
CPos.New(37, 50),
|
|
CPos.New(37, 51)
|
|
},
|
|
--- River near Waypoint 84.
|
|
rev4 =
|
|
{
|
|
CPos.New(49, 34),
|
|
CPos.New(48, 35),
|
|
CPos.New(49, 35),
|
|
CPos.New(47, 36),
|
|
CPos.New(48, 36),
|
|
CPos.New(46, 37),
|
|
CPos.New(47, 37),
|
|
CPos.New(45, 38),
|
|
CPos.New(46, 38)
|
|
},
|
|
--- Between the first flare and a beach to its south. Non-original.
|
|
tanya =
|
|
{
|
|
CPos.New(92, 90),
|
|
CPos.New(93, 90),
|
|
CPos.New(94, 90),
|
|
CPos.New(95, 90),
|
|
CPos.New(96, 90)
|
|
}
|
|
}
|
|
|
|
local function FinishCountdown()
|
|
DateTime.TimeLimit = 0
|
|
TimerColor = USSR.Color
|
|
local text = UserInterface.GetFluentMessage("cruisers-arrived")
|
|
|
|
for i = 0, 5 do
|
|
local flash
|
|
|
|
if i % 2 == 0 then
|
|
flash = HSLColor.White
|
|
end
|
|
|
|
Trigger.AfterDelay(DateTime.Seconds(i), function()
|
|
UserInterface.SetMissionText(text, flash or TimerColor)
|
|
end)
|
|
end
|
|
|
|
Trigger.AfterDelay(DateTime.Seconds(6), function()
|
|
UserInterface.SetMissionText("")
|
|
end)
|
|
end
|
|
|
|
---@param ticks integer
|
|
---@param interval integer
|
|
local function UpdateCountdown(ticks, interval)
|
|
if ticks < 1 then
|
|
return
|
|
end
|
|
|
|
local text = UserInterface.GetFluentMessage("cruisers-arrive-in", { ["time"] = Utils.FormatTime(ticks)})
|
|
UserInterface.SetMissionText(text, TimerColor)
|
|
|
|
Trigger.AfterDelay(interval, function()
|
|
UpdateCountdown(ticks - interval, interval)
|
|
end)
|
|
end
|
|
|
|
---@param duration integer
|
|
local function StartCountdown(duration)
|
|
DateTime.TimeLimit = duration
|
|
Media.PlaySpeechNotification(USSR, "TimerStarted")
|
|
UpdateCountdown(duration, DateTime.Seconds(1))
|
|
Trigger.OnTimerExpired(FinishCountdown)
|
|
|
|
Trigger.AfterDelay(duration - DateTime.Seconds(120), function()
|
|
TimerColor = USSR.Color
|
|
end)
|
|
end
|
|
|
|
---@param speech string Speech notification to play.
|
|
---@param delay integer Ticks until the speech plays.
|
|
local function PlayDelayedSpeech(speech, delay)
|
|
Trigger.AfterDelay(delay, function()
|
|
Media.PlaySpeechNotification(USSR, speech)
|
|
end)
|
|
end
|
|
|
|
local function AnnounceSovietVictory()
|
|
if VictoryStarted then
|
|
return
|
|
end
|
|
|
|
VictoryStarted = true
|
|
PlayDelayedSpeech("ObjectiveMet", DateTime.Seconds(1))
|
|
|
|
-- Like the original, it is possible to destroy Greece without naval units.
|
|
-- For that, PenObjective is marked here (for a second time in most cases).
|
|
local objectives = { PenObjective, IntruderObjective, TechObjective }
|
|
|
|
Trigger.AfterDelay(DateTime.Seconds(2), function()
|
|
Utils.Do(objectives, USSR.MarkCompletedObjective)
|
|
end)
|
|
end
|
|
|
|
---@param objective integer ID of the failed objective.
|
|
---@param delay? integer Delay until the failed objective is marked.
|
|
---@param speech? string Notification speech to be played.
|
|
---@param speechDelay? integer Delay until the speech is started.
|
|
local function AnnounceSovietDefeat(objective, delay, speech, speechDelay)
|
|
if DefeatStarted or VictoryStarted then
|
|
return
|
|
end
|
|
DefeatStarted = true
|
|
|
|
if speech then
|
|
PlayDelayedSpeech(speech, speechDelay or DateTime.Seconds(1))
|
|
end
|
|
|
|
Trigger.AfterDelay(delay or DateTime.Seconds(2), function()
|
|
USSR.MarkFailedObjective(objective)
|
|
end)
|
|
end
|
|
|
|
---@param player player
|
|
local function StartFireSale(player)
|
|
local buildings = Utils.Where(player.GetActors(), function(actor)
|
|
return actor.HasProperty("StartBuildingRepairs") end)
|
|
|
|
if #buildings == 0 then
|
|
Utils.Do(player.GetGroundAttackers(), IdleHunt)
|
|
return
|
|
end
|
|
|
|
Utils.Do(buildings, function(b)
|
|
b.Sell()
|
|
end)
|
|
|
|
-- Delay until all sales (and potential actor spawns from them) are done.
|
|
Trigger.OnAllRemovedFromWorld(buildings, function()
|
|
Utils.Do(player.GetGroundAttackers(), IdleHunt)
|
|
end)
|
|
end
|
|
|
|
---@param actor actor Actor to be removed.
|
|
---@param delay? integer Ticks to delay removal.
|
|
local function RemoveActor(actor, delay)
|
|
if delay then
|
|
Trigger.AfterDelay(delay, function()
|
|
RemoveActor(actor)
|
|
end)
|
|
|
|
return
|
|
end
|
|
|
|
if actor and actor.IsInWorld then
|
|
actor.Destroy()
|
|
end
|
|
end
|
|
|
|
---@param type string Type of the new actor.
|
|
---@param owner player Owner of the new actor.
|
|
---@param location? cpos Location of the new actor.
|
|
---@param duration? integer Ticks to delay the actor's removal, after creation.
|
|
---@param delay? integer Ticks to delay adding the new actor to the world.
|
|
---@return actor
|
|
local function SpawnMiscActor(type, owner, location, duration, delay)
|
|
delay = delay or 0
|
|
local actor = Actor.Create(type, delay <= 0, { Owner = owner, Location = location or CPos.Zero })
|
|
|
|
if delay > 0 then
|
|
Trigger.AfterDelay(delay, function()
|
|
actor.IsInWorld = true
|
|
end)
|
|
end
|
|
|
|
if duration and duration > 0 then
|
|
RemoveActor(actor, delay + duration)
|
|
end
|
|
|
|
return actor
|
|
end
|
|
|
|
---@param location cpos
|
|
---@return actor
|
|
local function SpawnSignalFlare(location)
|
|
return SpawnMiscActor("flare", England, location, -1)
|
|
end
|
|
|
|
---@param location cpos Cell to use as the camera's center.
|
|
---@param duration? integer Ticks for the camera to linger (after delay, if any).
|
|
---@param type? string Camera's actor type.
|
|
---@param delay? integer Ticks to delay the camera's creation.
|
|
---@return actor
|
|
local function SpawnRevealCamera(location, duration, type, delay)
|
|
duration = duration or DateTime.Seconds(6)
|
|
type = type or "camera"
|
|
return SpawnMiscActor(type, USSR, location, duration, delay)
|
|
end
|
|
|
|
--- Drop some Soviets near France's storage area. Non-original.
|
|
local function ReinforceCrateParatroopers()
|
|
local crates = { MoneyCrate1, MoneyCrate2, HealCrate }
|
|
local cratesAcquired = Utils.All(crates, function(c)
|
|
return not c.IsInWorld end)
|
|
|
|
if cratesAcquired then
|
|
return
|
|
end
|
|
|
|
local revealed = false
|
|
local proxy = Actor.Create("powerproxy.flametroopers", false, { Owner = USSR })
|
|
local targetPos = Map.CenterOfCell(CPos.New(30, 43))
|
|
local planes = proxy.TargetParatroopers(targetPos, Angle.New(384))
|
|
|
|
Utils.Do(planes, function(plane)
|
|
Trigger.OnPassengerExited(plane, function()
|
|
if revealed then
|
|
return
|
|
end
|
|
|
|
revealed = true
|
|
SpawnRevealCamera(HealCrateRevealMark.Location, 85, "camera.paradrop")
|
|
end)
|
|
end)
|
|
end
|
|
|
|
--- Send some non-original units to support the starting base. This
|
|
--- is intended to compensate for the inability to build defenses there.
|
|
local function SendTechGuards()
|
|
local types =
|
|
{
|
|
default = { "apc.techguard", "apc.techguard" },
|
|
easy = { "3tnk", "e4", "3tnk", "e4", "apc.techguard" }
|
|
}
|
|
local path = { TechGuardEntry.Location, Waypoint67.Location }
|
|
local guards = Reinforcements.ReinforceWithTransport(USSR, "lst.reinforcement", types[Difficulty] or types.default, path, { path[1] })[2]
|
|
local speechPlayed = false
|
|
local flipped = false
|
|
local goals = { [true] = TechGuardGoalWest.Location, [false] = TechGuardGoalEast.Location }
|
|
|
|
Utils.Do(guards, function(guard)
|
|
Trigger.OnAddedToWorld(guard, function()
|
|
guard.Move(goals[flipped], 2)
|
|
flipped = not flipped
|
|
|
|
if guard.HasProperty("UnloadPassengers") then
|
|
guard.UnloadPassengers()
|
|
end
|
|
|
|
if speechPlayed then
|
|
return
|
|
end
|
|
|
|
speechPlayed = true
|
|
Media.PlaySpeechNotification(USSR, "ReinforcementsArrived")
|
|
Beacon.New(England, TechGuardGoalEast.CenterPosition)
|
|
end)
|
|
end)
|
|
end
|
|
|
|
---@param types string[]
|
|
---@param path cpos[]
|
|
local function ReinforceBasicBoatTeam(types, path)
|
|
local boatTeam = Reinforcements.ReinforceWithTransport(Greece, "lst", types, path, { path[1] })
|
|
local boat, passengers = boatTeam[1], boatTeam[2]
|
|
Utils.Do(passengers, IdleHunt)
|
|
|
|
return boat, passengers
|
|
end
|
|
|
|
---@param transport actor Transport to reveal.
|
|
---@param location cpos Spawn location for the camera and trigger.
|
|
---@param hold? integer Ticks to continue revealing once the unload is done.
|
|
---@param type? string Type of camera to spawn.
|
|
---@param range? wdist Distance at which the transport triggers the reveal.
|
|
local function PrepareTransportReveal(transport, location, hold, type, range)
|
|
range = range or WDist.FromCells(6)
|
|
local pos = Map.CenterOfCell(location)
|
|
local activated = false
|
|
|
|
Trigger.OnEnteredProximityTrigger(pos, range, function(actor, id)
|
|
if activated or actor ~= transport then
|
|
return
|
|
end
|
|
|
|
activated = true
|
|
Trigger.RemoveProximityTrigger(id)
|
|
local camera = SpawnRevealCamera(location, -1, type)
|
|
|
|
Trigger.OnPassengerExited(transport, function()
|
|
if transport.HasPassengers then
|
|
return
|
|
end
|
|
|
|
RemoveActor(camera, hold)
|
|
end)
|
|
|
|
Trigger.OnKilled(transport, function()
|
|
RemoveActor(camera, hold)
|
|
end)
|
|
end)
|
|
end
|
|
|
|
---@param actors actor[]
|
|
---@param action fun()
|
|
local function OnAllAddedToWorld(actors, action)
|
|
local i = 0
|
|
|
|
Utils.Do(actors, function(actor)
|
|
Trigger.OnAddedToWorld(actor, function()
|
|
i = i + 1
|
|
|
|
if i == #actors then
|
|
action()
|
|
end
|
|
end)
|
|
end)
|
|
end
|
|
|
|
---@param actor actor
|
|
---@return boolean
|
|
local function IsEastOfCenterRidge(actor)
|
|
return actor.Location.X > Waypoint99.Location.X
|
|
end
|
|
|
|
---@param actor actor
|
|
---@return boolean
|
|
local function IsWall(actor)
|
|
return actor.Type == "brik" or actor.Type == "cycl" or actor.Type == "fenc"
|
|
end
|
|
|
|
---@param actor actor
|
|
local function IsMobileSoviet(actor)
|
|
return actor.Owner == USSR and actor.HasProperty("Move")
|
|
end
|
|
|
|
---@param actor actor
|
|
local function IsCruiser(actor)
|
|
return actor.Owner == Greece and actor.Type == "ca"
|
|
end
|
|
|
|
---@param posA wpos
|
|
---@param posB wpos
|
|
---@return integer
|
|
local function SquaredDistance(posA, posB)
|
|
local diffX = posA.X - posB.X
|
|
local diffY = posA.Y - posB.Y
|
|
return diffX * diffX + diffY * diffY
|
|
end
|
|
|
|
---@param origin wpos Point from which distances are measured.
|
|
---@param types string[] Target types to find.
|
|
---@param enemyPlayers player[] Players who will have their actors targeted.
|
|
---@param filter? fun(actor: actor):boolean Filter applied to actors after type and owner.
|
|
---@return actor[]
|
|
local function TargetsByDistance(origin, types, enemyPlayers, filter)
|
|
local targets = { }
|
|
local sort = table.sort
|
|
|
|
Utils.Do(enemyPlayers, function(player)
|
|
targets = Utils.Concat(targets, player.GetActorsByTypes(types))
|
|
end)
|
|
|
|
if filter then
|
|
targets = Utils.Where(targets, filter)
|
|
end
|
|
|
|
sort(targets, function(a, b)
|
|
return SquaredDistance(origin, a.CenterPosition) < SquaredDistance(origin, b.CenterPosition)
|
|
end)
|
|
|
|
return targets
|
|
end
|
|
|
|
---@param target actor
|
|
---@param harasser actor
|
|
---@return boolean
|
|
local function IsValidHarassTarget(target, harasser)
|
|
return target and not target.IsDead and not target.Owner.IsAlliedWith(harasser.Owner)
|
|
end
|
|
|
|
---@param harasser actor Unit finished with harassment.
|
|
---@param onFinished? fun(harasser: actor) Called after OnIdle is clear.
|
|
local function ClearIdleHarass(harasser, onFinished)
|
|
Trigger.Clear(harasser, "OnIdle")
|
|
|
|
if onFinished then
|
|
-- Delay in case the next function adds another OnIdle.
|
|
Trigger.AfterDelay(1, function()
|
|
if harasser.IsDead then
|
|
return
|
|
end
|
|
|
|
onFinished(harasser)
|
|
end)
|
|
end
|
|
end
|
|
|
|
--- Give a list of targets to a unit, and orders for interacting with each.
|
|
--- When idle, the unit performs these orders until no valid targets remain.
|
|
---@param harasser actor Unit taking orders.
|
|
---@param types string[] Target types to find.
|
|
---@param enemyPlayers player[] Players who will have their actors targeted.
|
|
---@param filter? fun(a: actor):boolean Filter applied to actors after type and owner.
|
|
---@param onNewTarget fun(harasser: actor, target: actor) Called when a new target is selected.
|
|
---@param onFinished? fun(harasser: actor) Called once no more targets can be found.
|
|
local function IdleHarass(harasser, types, enemyPlayers, filter, onNewTarget, onFinished)
|
|
if harasser.IsDead then
|
|
return
|
|
end
|
|
|
|
Trigger.OnIdle(harasser, function()
|
|
local tbd = TargetsByDistance(harasser.CenterPosition, types, enemyPlayers, filter)
|
|
-- Cap the list size to limit how outdated it may become.
|
|
local targets = Utils.Take(5, tbd)
|
|
|
|
if #targets == 0 then
|
|
ClearIdleHarass(harasser, onFinished)
|
|
return
|
|
end
|
|
|
|
Utils.Do(targets, function(target)
|
|
harasser.CallFunc(function()
|
|
-- Target outdated? Assume a list refresh is needed.
|
|
if not IsValidHarassTarget(target, harasser) then
|
|
targets = { }
|
|
return
|
|
end
|
|
|
|
onNewTarget(harasser, target)
|
|
end)
|
|
end)
|
|
end)
|
|
end
|
|
|
|
--- Order units to harass when idle, each with their own initial target.
|
|
--- The intent is to reduce or stagger TargetsByDistance calls from groups.
|
|
---@param harassers actor[] Units taking orders.
|
|
---@param types string[] Target types to find.
|
|
---@param enemyPlayers player[] Players who will have their actors targeted.
|
|
---@param filter? fun(a: actor):boolean Filter applied to actors after type and owner.
|
|
---@param onNewTarget fun(harasser: actor, target: actor) Called when a new target is selected.
|
|
---@param onFinished? fun(harasser: actor) Called once no more targets can be found.
|
|
local function GroupHarass(harassers, types, enemyPlayers, filter, onNewTarget, onFinished)
|
|
local targets
|
|
local targetId = 1
|
|
|
|
Utils.Do(harassers, function(harasser)
|
|
IdleHarass(harasser, types, enemyPlayers, filter, onNewTarget, onFinished)
|
|
|
|
if not harasser.IsInWorld then
|
|
return
|
|
end
|
|
|
|
targets = targets or TargetsByDistance(harasser.CenterPosition, types, enemyPlayers, filter)
|
|
|
|
if targets[targetId] then
|
|
onNewTarget(harasser, targets[targetId])
|
|
targetId = targetId + 1
|
|
end
|
|
end)
|
|
end
|
|
|
|
---@param plane actor Plane that will attack.
|
|
---@param types string[] Enemy types to attack, ordered by high to low priority.
|
|
---@param enemyPlayer player Owner of the enemy types.
|
|
---@return actor
|
|
local function NewPlaneTarget(plane, types, enemyPlayer)
|
|
local enemies = enemyPlayer.GetActorsByTypes(types)
|
|
local target
|
|
|
|
Utils.Do(types, function(type)
|
|
if target then
|
|
return
|
|
end
|
|
|
|
local matches = Utils.Where(enemies, function(enemy)
|
|
return enemy.Type == type and plane.CanTarget(enemy) end)
|
|
|
|
if #matches > 0 then
|
|
target = Utils.Random(matches)
|
|
end
|
|
end)
|
|
|
|
return target
|
|
end
|
|
|
|
---@param plane actor Plane that will attack.
|
|
---@param types string[] Enemy types to attack, ordered by high to low priority.
|
|
---@param enemyPlayer player Owner of the enemy types.
|
|
local function IdlePlaneHarass(plane, types, enemyPlayer)
|
|
local target
|
|
|
|
Trigger.OnIdle(plane, function()
|
|
local ammoStarved = plane.AmmoCount() == 0 and not plane.Owner.HasPrerequisites({ "afld" })
|
|
|
|
if ammoStarved then
|
|
plane.Move(Waypoint64.Location)
|
|
plane.Destroy()
|
|
return
|
|
end
|
|
|
|
target = target or NewPlaneTarget(plane, types, enemyPlayer)
|
|
|
|
if not target or target.IsDead then
|
|
plane.ReturnToBase()
|
|
target = nil
|
|
return
|
|
end
|
|
|
|
-- Ensure vision for targeting.
|
|
SpawnMiscActor("camera.spotter", plane.Owner, target.Location, 3)
|
|
|
|
Trigger.AfterDelay(1, function()
|
|
if not target or target.IsDead or plane.IsDead or not plane.CanTarget(target) then
|
|
target = nil
|
|
return
|
|
end
|
|
|
|
plane.Attack(target)
|
|
end)
|
|
end)
|
|
end
|
|
|
|
---@param types string[] Plane types to spawn.
|
|
---@param targetTypes string[] Enemy types to attack, ordered by high to low priority.
|
|
local function SendAlliedPlanes(types, targetTypes)
|
|
if not Greece.HasPrerequisites({ "afld"}) then
|
|
return
|
|
end
|
|
|
|
-- The RA1 planes were set to reinforce from Waypoint 95 but because that
|
|
-- sat on a land tile, the edge directly south was (and is) used instead.
|
|
local path = { AlliedPlaneEntry.Location }
|
|
local planes = Reinforcements.Reinforce(Greece, types, path)
|
|
|
|
Utils.Do(planes, function(plane)
|
|
IdlePlaneHarass(plane, targetTypes, USSR)
|
|
Trigger.OnCapture(plane, Trigger.ClearAll)
|
|
end)
|
|
|
|
if Difficulty ~= "hard" then
|
|
return
|
|
end
|
|
|
|
local delay = Actor.BuildTime(planes[1].Type) * 2 * #types
|
|
|
|
Trigger.OnAllKilled(planes, function()
|
|
Trigger.AfterDelay(delay, function()
|
|
SendAlliedPlanes(types, targetTypes)
|
|
end)
|
|
end)
|
|
end
|
|
|
|
--- Reinforce a pair of Yak planes, based on RA1 team "air1".
|
|
local function SendYaks()
|
|
local priorityList = { "4tnk", "3tnk", "silo", "harv" }
|
|
SendAlliedPlanes({ "yak", "yak" }, priorityList)
|
|
end
|
|
|
|
--- Reinforce a MiG bomber, based on RA1 team "air2".
|
|
local function SendJet()
|
|
local priorityList = { "4tnk", "3tnk", "powr", "silo", "v2rl", "mnly", "harv" }
|
|
SendAlliedPlanes({ "mig" }, priorityList)
|
|
end
|
|
|
|
local function ReinforceLongbows()
|
|
local types = { "heli", "heli", "heli", "heli" }
|
|
-- This offset arranges them in a spread formation before the attack order.
|
|
local offset = CVec.New(-5, 10)
|
|
local path = { Waypoint94.Location, Waypoint94.Location + offset }
|
|
|
|
return Reinforcements.Reinforce(Greece, types, path, 20)
|
|
end
|
|
|
|
--- Prepare an area trigger that will dispose of retreating Longbows.
|
|
--- Their repulsion trait makes the usual Move + Destroy orders unreliable.
|
|
---@param units actor[]
|
|
local function PrepareLongbowDisposal(units)
|
|
local disposal = Trigger.OnEnteredProximityTrigger(AlliedHeliDisposal.CenterPosition, WDist.FromCells(2), function(actor)
|
|
if actor.Type ~= "heli" then
|
|
return
|
|
end
|
|
|
|
actor.Stop()
|
|
actor.Destroy()
|
|
end)
|
|
|
|
Trigger.OnAllRemovedFromWorld(units, function()
|
|
Trigger.RemoveProximityTrigger(disposal)
|
|
end)
|
|
end
|
|
|
|
--- Send a Longbow team, based on the RA1 trigger "ent1".
|
|
--- The starting base is the normal target, but the depot is possible.
|
|
local function SendLongbows()
|
|
if CruisersKilled then
|
|
return
|
|
end
|
|
|
|
local targetTypes = { "ftur", "tsla", "apwr", "sam" }
|
|
local longbows = ReinforceLongbows()
|
|
PrepareLongbowDisposal(longbows)
|
|
|
|
OnAllAddedToWorld(longbows, function()
|
|
local targets = TargetsByDistance(Waypoint94.CenterPosition, targetTypes, SovietPlayers, IsEastOfCenterRidge)
|
|
|
|
-- Ensure vision for targeting.
|
|
SpawnMiscActor("camera", Greece, Waypoint67.Location, DateTime.Seconds(1))
|
|
|
|
Utils.Do(longbows, function(longbow)
|
|
if longbow.IsDead then
|
|
return
|
|
end
|
|
|
|
longbow.Stop()
|
|
local samFound = false
|
|
|
|
Utils.Do(targets, function(target)
|
|
if samFound or target.Type == "sam" then
|
|
samFound = true
|
|
return
|
|
end
|
|
|
|
longbow.Attack(target)
|
|
end)
|
|
|
|
Trigger.OnIdle(longbow, function()
|
|
longbow.Move(AlliedHeliDisposal.Location)
|
|
end)
|
|
end)
|
|
end)
|
|
end
|
|
|
|
--- Reveal the tech center to nearby Cruisers to ensure targeting.
|
|
---@param cruiser actor
|
|
local function OnCruiserReachedTech(cruiser)
|
|
if TechCenter.IsDead then
|
|
return
|
|
end
|
|
|
|
local camera = SpawnMiscActor("camera", Greece, Waypoint89.Location)
|
|
Trigger.OnKilled(cruiser, camera.Destroy)
|
|
end
|
|
|
|
local function OnAllCruisersKilled()
|
|
CruisersKilled = true
|
|
local attackers = Greece.GetGroundAttackers()
|
|
local navyPath = { EndBoatPatrol1.Location, EndBoatPatrol2.Location, EndBoatPatrol3.Location, EndBoatPatrol4.Location }
|
|
|
|
if #attackers == 0 then
|
|
-- Done in RA1 when Greece lost all current infantry & ground vehicles.
|
|
StartFireSale(Greece)
|
|
return
|
|
end
|
|
|
|
Utils.Do(attackers, function(a)
|
|
if a.Type == "dd" or a.Type == "dd.escort" then
|
|
a.Patrol(navyPath, true)
|
|
return
|
|
end
|
|
|
|
-- Mostly for any remaining east edge tanks.
|
|
IdleHunt(a)
|
|
end)
|
|
|
|
Trigger.OnAllKilled(attackers, function()
|
|
StartFireSale(Greece)
|
|
end)
|
|
end
|
|
|
|
--- Send ships ahead of the Cruisers, based on RA1 trigger "des2".
|
|
local function SendCruiserEscorts()
|
|
local types = { "dd.escort", "dd.escort" }
|
|
local path = { Waypoint90.Location, Waypoint83.Location }
|
|
|
|
Reinforcements.Reinforce(Greece, types, path, DateTime.Seconds(1), function(escort)
|
|
Trigger.OnIdle(escort, function()
|
|
escort.Stance = "AttackAnything"
|
|
local cruisers = Greece.GetActorsByType("ca")
|
|
|
|
if #cruisers == 0 then
|
|
escort.Hunt()
|
|
return
|
|
end
|
|
|
|
Utils.Do(cruisers, escort.Guard)
|
|
end)
|
|
end)
|
|
end
|
|
|
|
--- Send in the Allies' Cruisers, based on trigger "crus".
|
|
local function SendCruisers()
|
|
if not FirstAirfieldCaptured then
|
|
USSR.MarkFailedObjective(AirfieldObjective)
|
|
end
|
|
|
|
Trigger.AfterDelay(DateTime.Seconds(9), SendCruiserEscorts)
|
|
PlayDelayedSpeech("AlliedForcesApproaching", 20)
|
|
Media.DisplayMessage(UserInterface.GetFluentMessage("unauthorized-naval-units"), BattlefieldControl, USSR.Color)
|
|
|
|
Trigger.AfterDelay(DateTime.Seconds(4), function()
|
|
Media.DisplayMessage(UserInterface.GetFluentMessage("protect-our-tech-center-at-all-costs"), BattlefieldControl, USSR.Color)
|
|
end)
|
|
|
|
local path =
|
|
{
|
|
Waypoint90.Location,
|
|
Waypoint85.Location,
|
|
Waypoint86.Location,
|
|
Waypoint84.Location,
|
|
Waypoint73.Location,
|
|
Waypoint72.Location,
|
|
Waypoint71.Location
|
|
}
|
|
|
|
local cruisers = Reinforcements.Reinforce(Greece, { "ca", "ca" }, path, DateTime.Seconds(2))
|
|
Trigger.OnAllKilled(cruisers, OnAllCruisersKilled)
|
|
SpawnRevealCamera(cruisers[1].Location)
|
|
|
|
Utils.Do(cruisers, function(cruiser)
|
|
Trigger.OnIdle(cruiser, function()
|
|
if cruiser.Location == path[#path] then
|
|
Trigger.Clear(cruiser, "OnIdle")
|
|
return
|
|
end
|
|
|
|
cruiser.Move(path[#path])
|
|
end)
|
|
|
|
Trigger.OnEnteredProximityTrigger(Waypoint72.CenterPosition, WDist.FromCells(3), function(actor, id)
|
|
if actor ~= cruiser then
|
|
return
|
|
end
|
|
|
|
Trigger.RemoveProximityTrigger(id)
|
|
OnCruiserReachedTech(cruiser)
|
|
end)
|
|
end)
|
|
|
|
Trigger.AfterDelay(DateTime.Seconds(6), function()
|
|
CruisersArrived = true
|
|
end)
|
|
end
|
|
|
|
--- Send tanks to guard a path between the refineries and vehicle depot.
|
|
--- The original team was "end4" from trigger "des3".
|
|
local function SendEastEdgeTanks()
|
|
local path = { Waypoint68.Location, Waypoint69.Location }
|
|
local types = { "2tnk.widescan", "2tnk.widescan", "2tnk.widescan" }
|
|
local passengers = Reinforcements.ReinforceWithTransport(Greece, "lst", types, path, { path[1] })[2]
|
|
|
|
Utils.Do(passengers, function(p)
|
|
Trigger.OnAddedToWorld(p, function()
|
|
p.AttackMove(Waypoint63.Location, 2)
|
|
end)
|
|
end)
|
|
end
|
|
|
|
---@return actor[]
|
|
local function NavalWallTargets()
|
|
local sort = table.sort
|
|
local nw, se = NavalRocketScanWest.CenterPosition, NavalRocketScanEast.CenterPosition
|
|
local walls = Map.ActorsInBox(nw, se, IsWall)
|
|
|
|
-- Ensure our target list is ordered from west to east.
|
|
sort(walls, function(a, b)
|
|
return a.Location.X < b.Location.X
|
|
end)
|
|
|
|
return walls
|
|
end
|
|
|
|
--- Unload rockets to attack the Soviet pen, based on RA1's "ent4" trigger.
|
|
--- They can't outrange towers like RA1; a wall breach is attempted on Hard.
|
|
local function SendNavalRockets(types, path)
|
|
if not NavalBaseFound then
|
|
-- Cruisers are halfway down the river and the player seems
|
|
-- to be running late. Delay this attack as a small courtesy.
|
|
Trigger.OnObjectiveCompleted(USSR, function(_, id)
|
|
if id ~= PenObjective then
|
|
return
|
|
end
|
|
|
|
SendNavalRockets(types, path)
|
|
end)
|
|
|
|
return
|
|
end
|
|
|
|
local rockets = Reinforcements.ReinforceWithTransport(Greece, "lst", types, path, { path[1] })[2]
|
|
|
|
if Difficulty ~= "hard" then
|
|
Utils.Do(rockets, IdleHunt)
|
|
return
|
|
end
|
|
|
|
local walls = NavalWallTargets()
|
|
|
|
Utils.Do(rockets, function(rocket)
|
|
Trigger.OnAddedToWorld(rocket, function()
|
|
rocket.AttackMove(NavalRocketScanWest.Location, 4)
|
|
|
|
Utils.Do(walls, function(wall)
|
|
rocket.Attack(wall)
|
|
rocket.AttackMove(wall.Location, 1)
|
|
end)
|
|
|
|
rocket.AttackMove(Waypoint52.Location, 2)
|
|
IdleHunt(rocket)
|
|
end)
|
|
end)
|
|
end
|
|
|
|
local function ScheduleEndTransports()
|
|
local events =
|
|
{
|
|
{
|
|
teams = { EndTransportTeams.sea7, EndTransportTeams.sea8 },
|
|
delay = EndTeamDelays[Difficulty].sea7_sea8
|
|
},
|
|
{
|
|
teams = { EndTransportTeams.end1 },
|
|
delay = EndTeamDelays[Difficulty].end1,
|
|
teamFunc = SendNavalRockets,
|
|
},
|
|
{
|
|
teams = { EndTransportTeams.end2, EndTransportTeams.end5 },
|
|
delay = EndTeamDelays[Difficulty].end2_end5
|
|
}
|
|
}
|
|
|
|
Utils.Do(events, function(event)
|
|
local teamFunc = event.teamFunc or ReinforceBasicBoatTeam
|
|
|
|
Trigger.AfterDelay(event.delay, function()
|
|
if CruisersKilled then
|
|
return
|
|
end
|
|
|
|
Utils.Do(event.teams, function(team)
|
|
teamFunc(team.types, team.path)
|
|
end)
|
|
end)
|
|
end)
|
|
end
|
|
|
|
--- The Soviet naval base has died, possibly while still in shroud.
|
|
--- Under normal circumstances, it should remain intact until regained.
|
|
local function OnNavalBaseKilled()
|
|
if VictoryStarted or NavalBaseFound then
|
|
return
|
|
end
|
|
|
|
SpawnRevealCamera(Waypoint52.Location, -1)
|
|
AnnounceSovietDefeat(PenObjective, DateTime.Seconds(2), "ObjectiveNotMet", DateTime.Seconds(1))
|
|
Media.DisplayMessage(UserInterface.GetFluentMessage("naval-base-destroyed"), BattlefieldControl, USSR.Color)
|
|
Media.PlaySoundNotification(USSR, "AlertBleep")
|
|
end
|
|
|
|
local function OnStartAttackersRemoved()
|
|
if DefeatStarted then
|
|
return
|
|
end
|
|
|
|
Trigger.AfterDelay(DateTime.Seconds(4), function()
|
|
Media.DisplayMessage(UserInterface.GetFluentMessage("scouts-mark-the-way-to-bases"), BattlefieldControl, USSR.Color)
|
|
end)
|
|
|
|
Trigger.AfterDelay(DateTime.Seconds(8), function()
|
|
Media.PlaySpeechNotification(USSR, "SignalFlare")
|
|
Beacon.New(USSR, Waypoint0.CenterPosition)
|
|
end)
|
|
end
|
|
|
|
local function OnTechCenterLost()
|
|
SpawnRevealCamera(Waypoint89.Location, -1)
|
|
AnnounceSovietDefeat(TechObjective, DateTime.Seconds(2), "ObjectiveNotMet", DateTime.Seconds(1))
|
|
Media.PlaySoundNotification(USSR, "AlertBleep")
|
|
|
|
if TechCenter.IsDead then
|
|
Media.DisplayMessage(UserInterface.GetFluentMessage("our-tech-center-destroyed"), BattlefieldControl, USSR.Color)
|
|
return
|
|
end
|
|
|
|
Media.DisplayMessage(UserInterface.GetFluentMessage("our-tech-center-captured"), BattlefieldControl, USSR.Color)
|
|
|
|
Trigger.AfterDelay(DateTime.Seconds(2), function()
|
|
if TechCenter.IsInWorld then
|
|
TechCenter.Sell()
|
|
end
|
|
end)
|
|
end
|
|
|
|
---@param actors actor[]
|
|
local function RegainBase(actors)
|
|
Utils.Do(actors, function(actor)
|
|
if actor.IsDead then
|
|
return
|
|
end
|
|
|
|
actor.Owner = USSR
|
|
|
|
if actor.Type == "harv" then
|
|
actor.FindResources()
|
|
end
|
|
end)
|
|
end
|
|
|
|
local function OnMineBaseReached()
|
|
local trucks = { MineTruck1, MineTruck2, MineTruck3 }
|
|
local structures = { MineRefinery1, MineRefinery2, MineRefinery3, MinePower1, MinePower2, MinePower3, MinePower4, MineSilo1, MineSilo2, MineSilo3, MineSilo4, MineSilo5, MineSilo6, MineSilo7, MineSilo8 }
|
|
RegainBase(structures)
|
|
RegainBase(trucks)
|
|
|
|
Media.DisplayMessage(UserInterface.GetFluentMessage("mining-outpost-reached"), BattlefieldControl, USSR.Color)
|
|
|
|
Trigger.AfterDelay(DateTime.Seconds(4), function()
|
|
Media.PlaySpeechNotification(USSR, "SignalFlareNorth")
|
|
Beacon.New(USSR, Waypoint1.CenterPosition)
|
|
end)
|
|
|
|
if Difficulty == "hard" then
|
|
Trigger.AfterDelay(DateTime.Seconds(60), SendYaks)
|
|
Trigger.AfterDelay(DateTime.Seconds(120), SendJet)
|
|
return
|
|
end
|
|
|
|
if Difficulty == "easy" then
|
|
local proxy = Actor.Create("powerproxy.paratroopers", false, { Owner = USSR })
|
|
proxy.TargetParatroopers(Waypoint55.CenterPosition, Angle.New(896))
|
|
end
|
|
end
|
|
|
|
local function OnServiceBaseReached()
|
|
local base = { ServiceDepot, ServiceFactory, ServiceFlame1, ServiceFlame2, ServiceFlame3, ServiceFlame4, ServicePower1, ServicePower2, ServiceCommand, ServiceEngineer1, ServiceEngineer2 }
|
|
RegainBase(base)
|
|
Media.DisplayMessage(UserInterface.GetFluentMessage("vehicle-depot-reached"), BattlefieldControl, USSR.Color)
|
|
SpawnRevealCamera(Waypoint1.Location)
|
|
|
|
Trigger.AfterDelay(DateTime.Seconds(4), function()
|
|
Media.PlaySpeechNotification(USSR, "SignalFlareWest")
|
|
Beacon.New(USSR, Waypoint2.CenterPosition)
|
|
AirfieldObjective = AirfieldObjective or AddSecondaryObjective(USSR, "capture-airfield-before-cruisers")
|
|
end)
|
|
|
|
ScheduleEndTransports()
|
|
Trigger.AfterDelay(EndTeamDelays[Difficulty].cruisers, SendCruisers)
|
|
Trigger.AfterDelay(EndTeamDelays[Difficulty].longbows, SendLongbows)
|
|
Trigger.AfterDelay(DateTime.Seconds(60), SendTechGuards)
|
|
|
|
Trigger.AfterDelay(DateTime.Seconds(20), function()
|
|
StartCountdown(EndTeamDelays[Difficulty].cruisers - DateTime.Seconds(20))
|
|
end)
|
|
|
|
-- On Hard, planes will already be triggered from the mining base.
|
|
if Difficulty == "hard" then
|
|
return
|
|
end
|
|
|
|
Trigger.AfterDelay(DateTime.Seconds(60), SendYaks)
|
|
Trigger.AfterDelay(DateTime.Seconds(120), SendJet)
|
|
|
|
if Difficulty == "easy" and not ServiceFactory.IsDead then
|
|
local tankType = "4tnk"
|
|
ServiceFactory.Produce(tankType)
|
|
|
|
Trigger.AfterDelay(1, function()
|
|
RegainBase(BadGuy.GetActorsByType(tankType))
|
|
end)
|
|
end
|
|
end
|
|
|
|
local function OnAirbaseReached()
|
|
SpawnRevealCamera(Waypoint2.Location)
|
|
|
|
if NavalBaseFound then
|
|
return
|
|
end
|
|
|
|
Media.DisplayMessage(UserInterface.GetFluentMessage("airfields-reached"), BattlefieldControl, USSR.Color)
|
|
|
|
Trigger.AfterDelay(DateTime.Seconds(4), function()
|
|
Media.PlaySpeechNotification(USSR, "SignalFlareSouth")
|
|
Beacon.New(USSR, Waypoint3.CenterPosition)
|
|
end)
|
|
end
|
|
|
|
local function OnAirfieldCaptured()
|
|
if FirstAirfieldCaptured or DateTime.TimeLimit == 0 then
|
|
return
|
|
end
|
|
|
|
FirstAirfieldCaptured = true
|
|
AirfieldObjective = AirfieldObjective or AddSecondaryObjective(USSR, "capture-airfield-before-cruisers")
|
|
USSR.MarkCompletedObjective(AirfieldObjective)
|
|
Trigger.AfterDelay(DateTime.Seconds(1), ReinforceCrateParatroopers)
|
|
end
|
|
|
|
--- The player has regained the Sub Pen, by approaching the
|
|
--- gate or otherwise discovering the production structures.
|
|
local function OnNavalBaseFound()
|
|
if VictoryStarted or NavalBaseFound then
|
|
return
|
|
end
|
|
|
|
NavalBaseFound = true
|
|
USSR.MarkCompletedObjective(PenObjective)
|
|
Media.PlaySpeechNotification(USSR, "NewOptions")
|
|
local structures = { NavalPower1, NavalPower2, NavalFlame1, NavalFlame2, NavalConstructionYard, NavalPen }
|
|
RegainBase(structures)
|
|
SpawnRevealCamera(Waypoint52.Location)
|
|
|
|
Trigger.AfterDelay(DateTime.Seconds(4), function()
|
|
if not VictoryStarted then
|
|
IntruderObjective = AddPrimaryObjective(USSR, "wipe-out-intruders-with-new-submarines")
|
|
end
|
|
end)
|
|
end
|
|
|
|
--- Prepare the chain of Soviet signal flares and their triggers.
|
|
--- Each trigger spawns the next flare and schedules the current one's removal.
|
|
local function PrepareSovietSignals()
|
|
local signals =
|
|
{
|
|
{
|
|
action = OnMineBaseReached,
|
|
location = Waypoint0.Location,
|
|
radius = WDist.FromCells(3)
|
|
},
|
|
{
|
|
action = OnServiceBaseReached,
|
|
location = Waypoint1.Location,
|
|
radius = WDist.FromCells(11)
|
|
},
|
|
{
|
|
action = OnAirbaseReached,
|
|
location = Waypoint2.Location,
|
|
radius = WDist.FromCells(6)
|
|
},
|
|
{
|
|
location = Waypoint3.Location,
|
|
radius = WDist.FromCells(3)
|
|
}
|
|
}
|
|
|
|
for i = 1, #signals do
|
|
local activated = false
|
|
|
|
Trigger.OnEnteredProximityTrigger(Map.CenterOfCell(signals[i].location), signals[i].radius, function(actor, id)
|
|
if activated or not IsMobileSoviet(actor) then
|
|
return
|
|
end
|
|
|
|
activated = true
|
|
Trigger.RemoveProximityTrigger(id)
|
|
RemoveActor(signals[i].smoke, DateTime.Minutes(2))
|
|
|
|
if signals[i].action then
|
|
signals[i].action()
|
|
end
|
|
|
|
if signals[i + 1] and not NavalBaseFound then
|
|
signals[i + 1].smoke = SpawnSignalFlare(signals[i + 1].location)
|
|
end
|
|
end)
|
|
end
|
|
|
|
signals[1].smoke = SpawnSignalFlare(Waypoint0.Location)
|
|
end
|
|
|
|
--- Send some engineers to capture the Tech Center if playing Hard.
|
|
--- This is a nod to the "end3" team unused in the RA1 mission.
|
|
---@return actor[]
|
|
local function SendSaboteurs()
|
|
if Difficulty ~= "hard" then
|
|
return { }
|
|
end
|
|
|
|
local cargoTypes = { "e6", "e6", "e1", "e3", "e3" }
|
|
local entryPath = { Waypoint66.Location, CPos.New(56, 92), Waypoint87.Location }
|
|
local apc = Reinforcements.ReinforceWithTransport(Greece, "apc", cargoTypes, entryPath)[1]
|
|
local captureTypes = { "stek", "iron" }
|
|
|
|
Utils.Do(apc.Passengers, function(passenger)
|
|
Trigger.OnAddedToWorld(passenger, function()
|
|
if not passenger.HasProperty("Capture") then
|
|
IdleHunt(passenger)
|
|
return
|
|
end
|
|
|
|
passenger.Move(Waypoint89.Location, 2)
|
|
|
|
IdleHarass(passenger, captureTypes, SovietPlayers, nil, function(harasser, target)
|
|
harasser.Capture(target)
|
|
end)
|
|
end)
|
|
end)
|
|
|
|
IdleHunt(apc)
|
|
return apc.Passengers
|
|
end
|
|
|
|
local function OrderStartAttackers()
|
|
local saboteurs = SendSaboteurs()
|
|
local rockets = { StartAttacker1, StartAttacker2, StartAttacker3 }
|
|
|
|
Utils.Do(rockets, function(a)
|
|
-- Maintain their spread formation as they approach.
|
|
local goal = a.Location + CVec.New(0, 6)
|
|
a.AttackMove(goal, 2)
|
|
IdleHunt(a)
|
|
end)
|
|
|
|
local attackers = Utils.Concat(saboteurs, rockets)
|
|
Trigger.OnAllRemovedFromWorld(attackers, OnStartAttackersRemoved)
|
|
end
|
|
|
|
---@param actor actor Actor that must survive for the speech to play.
|
|
---@param sound string Sound notification to play.
|
|
---@param delay integer Ticks until the speech plays.
|
|
---@param camera? string Camera type to reveal Tanya as she speaks.
|
|
local function TauntWithTanya(actor, sound, delay, camera)
|
|
Trigger.AfterDelay(delay, function()
|
|
if actor.IsDead then
|
|
return
|
|
end
|
|
|
|
if camera then
|
|
SpawnRevealCamera(actor.Location, DateTime.Seconds(3), camera)
|
|
end
|
|
|
|
Media.PlaySpeechNotification(USSR, sound)
|
|
end)
|
|
end
|
|
|
|
--- Arrange extraction for Tanya once her sabotage is complete.
|
|
---@param tanya actor
|
|
local function ExtractTanya(tanya)
|
|
local path = { Waypoint93.Location, TanyaBackupUnload.Location }
|
|
local boat = Reinforcements.Reinforce(Greece, { "lst" }, path)[1]
|
|
tanya.Move(TanyaBackupUnload.Location)
|
|
|
|
Trigger.OnIdle(boat, function()
|
|
if tanya.IsDead or boat.HasPassengers then
|
|
boat.Move(path[1])
|
|
boat.Destroy()
|
|
return
|
|
end
|
|
|
|
if tanya.IsIdle then
|
|
tanya.EnterTransport(boat)
|
|
end
|
|
end)
|
|
|
|
Trigger.OnPassengerEntered(boat, function()
|
|
TauntWithTanya(boat, "TanyaLetsRock", 15, "camera.small")
|
|
end)
|
|
|
|
Trigger.OnIdle(tanya, function()
|
|
if boat.IsDead then
|
|
tanya.Hunt()
|
|
end
|
|
end)
|
|
end
|
|
|
|
local function ReinforceTanyaBackup()
|
|
local path = { Waypoint68.Location, TanyaBackupUnload.Location }
|
|
local exit = { Waypoint94.Location }
|
|
local types = { "e3.vehiclehunter", "e3.vehiclehunter", "e3.vehiclehunter", "e1", "2tnk.widescan" }
|
|
local boatTeam = Reinforcements.ReinforceWithTransport(Greece, "lst", types, path, exit)
|
|
local boat, cargo = boatTeam[1], boatTeam[2]
|
|
PrepareTransportReveal(boat, path[#path], DateTime.Seconds(1), "camera.small")
|
|
|
|
return cargo
|
|
end
|
|
|
|
local function SendTanyaBackup()
|
|
if Difficulty ~= "hard" then
|
|
return
|
|
end
|
|
|
|
local units = ReinforceTanyaBackup()
|
|
local attacking = false
|
|
local guardGoal = Waypoint55.Location + CVec.New(0, 2)
|
|
|
|
OnAnyDamaged(units, function()
|
|
attacking = true
|
|
end)
|
|
|
|
Utils.Do(units, function(unit)
|
|
Trigger.OnAddedToWorld(unit, function()
|
|
-- Ignore structures for now and leave them to Tanya.
|
|
unit.Stance = "Defend"
|
|
unit.AttackMove(guardGoal, 2)
|
|
end)
|
|
|
|
Trigger.OnIdle(unit, function()
|
|
if not attacking then
|
|
return
|
|
end
|
|
|
|
if unit.Location ~= Waypoint0.Location then
|
|
unit.AttackMove(Waypoint0.Location)
|
|
return
|
|
end
|
|
|
|
unit.Stance = "AttackAnything"
|
|
unit.Hunt()
|
|
end)
|
|
end)
|
|
end
|
|
|
|
--- Reinforce Tanya by boat, along with any backup she may have.
|
|
---@return actor
|
|
local function ReinforceTanya()
|
|
local path = { Waypoint93.Location, Waypoint92.Location }
|
|
local cargo = { "e7" }
|
|
local boatTeam = Reinforcements.ReinforceWithTransport(Greece, "lst", cargo, path, { path[1] })
|
|
local boat, tanya = boatTeam[1], boatTeam[2][1]
|
|
PrepareTransportReveal(boat, path[#path], DateTime.Seconds(3), "camera", WDist.FromCells(4))
|
|
Trigger.OnAddedToWorld(tanya, SendTanyaBackup)
|
|
|
|
return tanya
|
|
end
|
|
|
|
--- Begin Tanya's demolition spree. She will attempt extraction when done.
|
|
local function SendTanya()
|
|
if TanyaReinforced then
|
|
return
|
|
end
|
|
|
|
TanyaReinforced = true
|
|
local tanya = ReinforceTanya()
|
|
local demolitionTypes = { "proc", "powr", "silo" }
|
|
local lastTarget
|
|
|
|
local onNewTarget = function(demolisher, target)
|
|
-- Guard against a re-demolish, which may happen if the target
|
|
-- list refreshes as a bomb timer is still ticking down.
|
|
if target == lastTarget then
|
|
return
|
|
end
|
|
|
|
demolisher.Demolish(target)
|
|
lastTarget = target
|
|
end
|
|
|
|
Trigger.OnAddedToWorld(tanya, function()
|
|
TauntWithTanya(tanya, "TanyaLaugh", 1)
|
|
TauntWithTanya(tanya, "TanyaGiveItToMe", DateTime.Seconds(6))
|
|
tanya.Move(Waypoint77.Location)
|
|
tanya.Move(TanyaDemolishStart.Location)
|
|
IdleHarass(tanya, demolitionTypes, SovietPlayers, IsEastOfCenterRidge, onNewTarget, ExtractTanya)
|
|
end)
|
|
|
|
-- Tanya's unload cell from the boat is randomized. Since that affects
|
|
-- her travel time, this will be more consistent than a delay.
|
|
Trigger.OnEnteredProximityTrigger(TanyaDemolishStart.CenterPosition, WDist.New(1536), function(actor, id)
|
|
if actor ~= tanya then
|
|
return
|
|
end
|
|
|
|
Trigger.RemoveProximityTrigger(id)
|
|
TauntWithTanya(actor, "TanyaKissItByeBye", 1, "camera.small")
|
|
end)
|
|
end
|
|
|
|
--- Airlift thieves to the mining base, based on RA1 trigger "ent3".
|
|
local function SendThieves()
|
|
local thiefTypes = { "thf", "thf" }
|
|
if Difficulty == "hard" then
|
|
thiefTypes = { "thf", "thf", "thf", "thf" }
|
|
end
|
|
|
|
local path = { Waypoint68.Location, Waypoint55.Location }
|
|
local thieves = Reinforcements.ReinforceWithTransport(Greece, "tran", thiefTypes, path, { path[1] })[2]
|
|
local stealTypes = { "proc", "silo" }
|
|
|
|
local onNewTarget = function(thief, target)
|
|
thief.Infiltrate(target)
|
|
end
|
|
|
|
OnAllAddedToWorld(thieves, function()
|
|
GroupHarass(thieves, stealTypes, SovietPlayers, IsEastOfCenterRidge, onNewTarget)
|
|
end)
|
|
end
|
|
|
|
--- Send vehicles to help the beach runner, and prepare Tanya's arrival.
|
|
--- The original team was "sea4" from trigger "atk2".
|
|
local function SendBeachRescue()
|
|
local path = { Waypoint94.Location, Waypoint95.Location }
|
|
local cargoTypes = { "1tnk", "arty", "arty" }
|
|
if Difficulty == "hard" then
|
|
cargoTypes = { "2tnk", "2tnk", "e3", "e3", "arty" }
|
|
end
|
|
|
|
local boat, cargo = ReinforceBasicBoatTeam(cargoTypes, path)
|
|
PrepareTransportReveal(boat, Waypoint74.Location, DateTime.Seconds(1), "camera.small", WDist.FromCells(15))
|
|
|
|
Trigger.OnAllKilled(cargo, function()
|
|
SendTanya()
|
|
IdleHunt(BeachRunner)
|
|
end)
|
|
end
|
|
|
|
--- Order a soldier to run for help, based on RA1 trigger "atk6".
|
|
local function OrderBeachRunner()
|
|
if BeachRunner.IsDead then
|
|
return
|
|
end
|
|
|
|
SpawnRevealCamera(BeachRunner.Location)
|
|
BeachRunner.Stop()
|
|
BeachRunner.Move(RidgeRunPoint1.Location)
|
|
BeachRunner.Move(RidgeRunPoint2.Location)
|
|
BeachRunner.Move(Waypoint74.Location)
|
|
|
|
Trigger.OnIdle(BeachRunner, function()
|
|
if BeachRunner.Location == Waypoint74.Location then
|
|
Trigger.Clear(BeachRunner, "OnIdle")
|
|
BeachRunner.Stance = "AttackAnything"
|
|
return
|
|
end
|
|
|
|
BeachRunner.Move(Waypoint74.Location)
|
|
end)
|
|
end
|
|
|
|
local function PrepareBeachRunner()
|
|
local team = { BeachRunnerRocket1, BeachRunnerRocket2, BeachRunner }
|
|
local alerted = false
|
|
|
|
Trigger.OnEnteredFootprint(Footprints.atk6, function(actor, id)
|
|
if alerted or not IsMobileSoviet(actor) then
|
|
return
|
|
end
|
|
|
|
alerted = true
|
|
OrderBeachRunner()
|
|
Trigger.RemoveFootprintTrigger(id)
|
|
end)
|
|
|
|
OnAnyDamaged(team, function()
|
|
if alerted then
|
|
return
|
|
end
|
|
|
|
alerted = true
|
|
OrderBeachRunner()
|
|
end)
|
|
|
|
Trigger.OnEnteredFootprint(Footprints.atk2, function(actor, id)
|
|
if actor ~= BeachRunner then
|
|
return
|
|
end
|
|
|
|
Trigger.RemoveFootprintTrigger(id)
|
|
SendBeachRescue()
|
|
end)
|
|
end
|
|
|
|
--- Send some APC troops for a flank attack at the north edge, based on "ent3".
|
|
local function SendLakeAmbushers()
|
|
local path = { Waypoint57.Location, Waypoint56.Location }
|
|
local cargoTypes = { "e3", "e3", "e3", "e3", "e3" }
|
|
local apc = Reinforcements.ReinforceWithTransport(Greece, "apc", cargoTypes, path)[1]
|
|
|
|
Trigger.OnPassengerExited(apc, function(_, passenger)
|
|
IdleHunt(passenger)
|
|
end)
|
|
|
|
IdleHunt(apc)
|
|
end
|
|
|
|
--- Unload some mining base harassers, based on RA1 trigger "atk8".
|
|
--- Hard adds the intended but bugged "sea6" team, plus an Artillery unit.
|
|
local function SendEastEdgeRockets()
|
|
local path = { Waypoint68.Location, Waypoint70.Location }
|
|
local cargoTypes = { "e3.vehiclehunter", "e3.vehiclehunter", "e3.vehiclehunter", "e3.vehiclehunter", "e3.vehiclehunter" }
|
|
local boat = ReinforceBasicBoatTeam(cargoTypes, path)
|
|
PrepareTransportReveal(boat, Waypoint70.Location, DateTime.Seconds(1), "camera.small")
|
|
|
|
if Difficulty ~= "hard" then
|
|
return
|
|
end
|
|
|
|
local extras = { "e3.vehiclehunter", "e3.vehiclehunter", "e3.vehiclehunter", "arty" }
|
|
local extraPath = { Waypoint68.Location, Waypoint69.Location }
|
|
|
|
Trigger.AfterDelay(10, function()
|
|
ReinforceBasicBoatTeam(extras, extraPath)
|
|
end)
|
|
end
|
|
|
|
local function PrepareBasicFootprints()
|
|
local triggers =
|
|
{
|
|
{
|
|
cells = Footprints.atk7,
|
|
actions = { SendEastEdgeRockets }
|
|
},
|
|
{
|
|
cells = Footprints.des3,
|
|
actions = { SendEastEdgeTanks }
|
|
},
|
|
{
|
|
cells = Footprints.ent3,
|
|
actions = { SendLakeAmbushers, SendThieves }
|
|
},
|
|
{
|
|
cells = Footprints.tanya,
|
|
actions = { SendTanya }
|
|
}
|
|
}
|
|
|
|
Utils.Do(triggers, function(trigger)
|
|
local activated = false
|
|
|
|
Trigger.OnEnteredFootprint(trigger.cells, function(actor, id)
|
|
if activated or not IsMobileSoviet(actor) then
|
|
return
|
|
end
|
|
|
|
activated = true
|
|
Trigger.RemoveFootprintTrigger(id)
|
|
|
|
Utils.Do(trigger.actions, function(action)
|
|
action()
|
|
end)
|
|
end)
|
|
end)
|
|
end
|
|
|
|
local function PrepareCruiserReveals()
|
|
local triggers =
|
|
{
|
|
{ cells = Footprints.rev4, reveal = Waypoint84.Location },
|
|
{ cells = Footprints.rev3, reveal = Waypoint73.Location },
|
|
{ cells = Footprints.ent4, reveal = Waypoint76.Location },
|
|
{ cells = Footprints.ent5, reveal = Waypoint53.Location },
|
|
{ cells = Footprints.ent6, reveal = Waypoint72.Location }
|
|
}
|
|
|
|
Utils.Do(triggers, function(trigger)
|
|
Trigger.OnEnteredFootprint(trigger.cells, function(actor, id)
|
|
if not IsCruiser(actor) then
|
|
return
|
|
end
|
|
|
|
Trigger.RemoveFootprintTrigger(id)
|
|
SpawnRevealCamera(trigger.reveal)
|
|
end)
|
|
end)
|
|
end
|
|
|
|
--- Easy will swap out the end teams' artillery. In RA1, Tesla Coils
|
|
--- outranged Allied artillery with ease, but the same does not apply here.
|
|
local function SwapEndArtillery()
|
|
if Difficulty ~= "easy" then
|
|
return
|
|
end
|
|
|
|
Utils.Do(EndTransportTeams, function(team)
|
|
for i = 1, #team.types do
|
|
if team.types[i] == "arty" then
|
|
team.types[i] = "1tnk"
|
|
end
|
|
end
|
|
end)
|
|
end
|
|
|
|
WorldLoaded = function()
|
|
PrepareSovietSignals()
|
|
PrepareCruiserReveals()
|
|
PrepareBasicFootprints()
|
|
PrepareBeachRunner()
|
|
OrderStartAttackers()
|
|
SwapEndArtillery()
|
|
|
|
Camera.Position = TechCoil1.CenterPosition
|
|
USSR.Cash = 500
|
|
BadGuy.Cash = 5000
|
|
|
|
InitObjectives(USSR)
|
|
TechObjective = AddPrimaryObjective(USSR, "tech-center-survive")
|
|
PenObjective = AddPrimaryObjective(USSR, "regain-control-of-our-naval-base")
|
|
|
|
Trigger.OnKilledOrCaptured(TechCenter, OnTechCenterLost)
|
|
Trigger.OnAllKilled({ NavalConstructionYard, NavalPen }, OnNavalBaseKilled)
|
|
Trigger.OnDiscovered(NavalConstructionYard, OnNavalBaseFound)
|
|
Trigger.OnDiscovered(NavalPen, OnNavalBaseFound)
|
|
|
|
-- Fallback trigger that lets the player continue if using the
|
|
-- visibility cheat; OnDiscovered events will be blocked.
|
|
Trigger.OnEnteredProximityTrigger(NavalConstructionYard.CenterPosition, WDist.FromCells(4), function(actor, id)
|
|
if not IsMobileSoviet(actor) then
|
|
return
|
|
end
|
|
|
|
Trigger.RemoveProximityTrigger(id)
|
|
OnNavalBaseFound()
|
|
end)
|
|
|
|
Trigger.AfterDelay(DateTime.Seconds(6), function()
|
|
-- Ownership is delayed to help avoid the speech muddling others.
|
|
RegainBase({ IronCurtain })
|
|
end)
|
|
|
|
Utils.Do(Greece.GetActorsByType("afld"), function(field)
|
|
Trigger.OnCapture(field, OnAirfieldCaptured)
|
|
end)
|
|
end
|
|
|
|
Tick = function()
|
|
if CruisersArrived and TechCenter.IsInWorld and Greece.HasNoRequiredUnits() then
|
|
AnnounceSovietVictory()
|
|
end
|
|
end
|