Files
OpenRA/mods/ra/maps/proving-grounds/proving-grounds.lua
let5sne.win10 9cf6ebb986
Some checks failed
Continuous Integration / Linux (.NET 8.0) (push) Has been cancelled
Continuous Integration / Windows (.NET 8.0) (push) Has been cancelled
Initial commit: OpenRA game engine
Fork from OpenRA/OpenRA with one-click launch script (start-ra.cmd)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 21:46:54 +08:00

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