From 670067ae3a1742d417f7bf01cbab4e88c28c8e1d Mon Sep 17 00:00:00 2001 From: JovialFeline Date: Tue, 30 Apr 2024 18:25:28 -0400 Subject: [PATCH] Add Negotiations mission (Red Alert) --- mods/ra/languages/campaign/en.ftl | 2 +- mods/ra/languages/lua/en.ftl | 18 +- mods/ra/maps/negotiations/map.bin | Bin 0 -> 81937 bytes mods/ra/maps/negotiations/map.png | Bin 0 -> 11517 bytes mods/ra/maps/negotiations/map.yaml | 1346 +++++++++++++++++ mods/ra/maps/negotiations/negotiations-ai.lua | 347 +++++ .../negotiations-reinforcements.lua | 397 +++++ mods/ra/maps/negotiations/negotiations.lua | 1265 ++++++++++++++++ mods/ra/maps/negotiations/notifications.yaml | 6 + mods/ra/maps/negotiations/rules.yaml | 196 +++ mods/ra/maps/negotiations/weapons.yaml | 12 + mods/ra/missions.yaml | 1 + 12 files changed, 3587 insertions(+), 3 deletions(-) create mode 100644 mods/ra/maps/negotiations/map.bin create mode 100644 mods/ra/maps/negotiations/map.png create mode 100644 mods/ra/maps/negotiations/map.yaml create mode 100644 mods/ra/maps/negotiations/negotiations-ai.lua create mode 100644 mods/ra/maps/negotiations/negotiations-reinforcements.lua create mode 100644 mods/ra/maps/negotiations/negotiations.lua create mode 100644 mods/ra/maps/negotiations/notifications.yaml create mode 100644 mods/ra/maps/negotiations/rules.yaml create mode 100644 mods/ra/maps/negotiations/weapons.yaml diff --git a/mods/ra/languages/campaign/en.ftl b/mods/ra/languages/campaign/en.ftl index d004f11742..1dee249463 100644 --- a/mods/ra/languages/campaign/en.ftl +++ b/mods/ra/languages/campaign/en.ftl @@ -14,7 +14,7 @@ actor-crate-name = Crate ## campaign-tooltips.yaml actor-technology-center-name = Technology Center -## allies-05a, allies-05b, allies-05c +## allies-05a, allies-05b, allies-05c, negotiations actor-prison-name = Prison ## fall-of-greece-1-personal-war, situation-critical diff --git a/mods/ra/languages/lua/en.ftl b/mods/ra/languages/lua/en.ftl index 0f5a4cc6ac..0597b6c805 100644 --- a/mods/ra/languages/lua/en.ftl +++ b/mods/ra/languages/lua/en.ftl @@ -17,7 +17,7 @@ find-einstein = Find Einstein. einstein-survive = Einstein must survive. protect-civilians = Protect all civilians. -## allies-01, allies-03, allies-05abc, evacuation +## allies-01, allies-03, allies-05abc, evacuation, negotiations tanya-rules-of-engagement = According to the rules of engagement I need your explicit orders to fire, Commander! # allies-01, allies-05abc, evacuation @@ -214,6 +214,20 @@ all-engineers-killed = All engineers were killed. kill-stavros = Kill Stavros. sabotage-facility = Sabotage the facility with our engineers. +## negotiations +all-hostages-killed = All hostages were killed. +church-destroyed = The church was destroyed. +free-hostages = Locate & free the hostages. +get-hostages-to-church = Get hostages to church. +signal-for-reinforcements = Signal for reinforcements at your drop-off point. +guide-thank-you = Thank you! I'll help you get into town! +guide-follow-me = Follow me! +guide-patrol-coming = Uh oh, a patrol is coming this way. +guide-come-this-way = Come this way! Hurry! +guide-safe-to-move = It's safe to move now. Let's go. +hostage-dies-in = Hostage dies in { $time } +keep-all-hostages-alive = Keep all hostages alive. + ## production-disruption, shock-therapy capture-enemy-radar-dome = Capture the enemy radar dome. @@ -274,7 +288,7 @@ destroy-soviet-convoy = Destroy all Soviet convoy trucks. destroy-bridges-slow-convoy = Destroy the nearby bridges to slow the convoys down. -## siberian-conflict-3-wasteland +## siberian-conflict-3-wasteland, negotiations destroy-soviet-units-infrastructure = Destroy all Soviet units and structures. ## situation-critical diff --git a/mods/ra/maps/negotiations/map.bin b/mods/ra/maps/negotiations/map.bin new file mode 100644 index 0000000000000000000000000000000000000000..726290f776f891af5c2983216eaca9b6553e8f63 GIT binary patch literal 81937 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 player.Cash then + return false + end + + local blocked = IsStructureAreaBlocked(player, structure.position, structure.shape) + if blocked then + return false, "blocked" + end + + local actor = Actor.Create(structure.type, true, { Owner = player, Location = structure.location }) + structure.exists = true + player.Cash = player.Cash - structure.cost + AddRebuildTrigger(actor, structure) + + if structure.onKilled then + Trigger.OnKilled(actor, structure.onKilled) + end + + if structure.onBuilt then + -- Build() will not work properly on producers if called immediately. + Trigger.AfterDelay(1, function() + structure.onBuilt(actor) + end) + end + + return actor +end + +StructureFootprints = +{ + ["1x1"] = WVec.New(1 * 1024, 1 * 1024, 0), + ["2x3"] = WVec.New(2 * 1024, 3 * 1024, 0), + ["3x3"] = WVec.New(3 * 1024, 3 * 1024, 0), + ["3x4"] = WVec.New(3 * 1024, 4 * 1024, 0), +} + +IsStructureAreaBlocked = function(player, position, shape) + local foot = StructureFootprints[shape] + local blockers = Map.ActorsInBox(position, position + foot, function(actor) + return actor.CenterPosition.Z == 0 and actor.HasProperty("Health") + end) + + if #blockers == 0 then + return false + end + + ScatterBlockers(player, blockers) + return true +end + +ScatterBlockers = function(player, actors) + Utils.Do(actors, function(actor) + if actor.IsIdle and actor.Owner == player and actor.HasProperty("Scatter") then + actor.Scatter() + end + end) +end diff --git a/mods/ra/maps/negotiations/negotiations-reinforcements.lua b/mods/ra/maps/negotiations/negotiations-reinforcements.lua new file mode 100644 index 0000000000..3d422c00c7 --- /dev/null +++ b/mods/ra/maps/negotiations/negotiations-reinforcements.lua @@ -0,0 +1,397 @@ +--[[ + 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. +]] +ReinforceAllies = function() + Media.PlaySpeechNotification(Greece, "ReinforcementsArrived") + local proxy = SpawnMiscActor("paradrop.allies", GoodGuy, TanyaDrop.Location, DateTime.Seconds(1)) + local planes = proxy.TargetParatroopers(TanyaDrop.CenterPosition, Angle.SouthEast) + + Utils.Do(planes, function(plane) + Trigger.OnPassengerExited(plane, function(_, passenger) + passenger.Owner = Greece + end) + + Trigger.OnAllKilled(plane.Passengers, CheckAlliedDestruction) + end) + + local demoTypes = { "2tnk", "2tnk", "2tnk", "dtrk" } + local artilleryTypes = {"2tnk", "2tnk", "arty", "arty", "mech", "mech", "mech" } + local groundPath = { WestRoadEntry.Location, WestRoadRally.Location } + + local demoTeam = Reinforcements.Reinforce(Greece, demoTypes, groundPath) + Trigger.OnAllKilled(demoTeam, CheckAlliedDestruction) + + Trigger.AfterDelay(DateTime.Seconds(30), function() + if AlliesDefeated then + return + end + + Media.PlaySpeechNotification(Greece, "ReinforcementsArrived") + local artilleryTeam = Reinforcements.Reinforce(Greece, artilleryTypes, groundPath) + Trigger.OnAllKilled(artilleryTeam, CheckAlliedDestruction) + end) +end + +ReinforceChronoTanks = function(locations) + if ChronoTanksReinforced then + return + end + + ChronoTanksReinforced = true + Media.PlaySoundNotification(Greece, "Chronoshift") + + Utils.Do(locations, function(location) + local proxy = SpawnMiscActor("powerproxy.chronoshift", GoodGuy, location, DateTime.Seconds(1)) + local tank = Actor.Create("ctnk", true, { Owner = Greece, Facing = Angle.NorthWest }) + local payload = { [tank] = location } + proxy.Chronoshift(payload) + end) +end + +ReinforceTanya = function() + local path = { WestRoadEntry.Location, TanyaDrop.Location } + local plane = Reinforcements.Reinforce(Greece, { "badr.tanya" }, path)[1] + local passengers = { TanyaType } + + Trigger.OnAddedToWorld(plane, function() + Utils.Do(passengers, function(type) + local passenger = Actor.Create(type, false, { Owner = Greece }) + plane.LoadPassenger(passenger) + Trigger.OnKilled(passenger, OnTanyaKilled) + Tanya = passenger + AnnounceTanyaRules(passenger) + end) + + plane.Paradrop(path[2]) + end) +end + +AnnounceTanyaRules = function(tanya) + if Difficulty == "easy" then + return + end + + Trigger.OnAddedToWorld(tanya, function() + Trigger.AfterDelay(DateTime.Seconds(1), function() + Media.DisplayMessage(UserInterface.Translate("tanya-rules-of-engagement"), tanya.TooltipName) + Media.PlaySoundNotification(Greece, "AlertBleep") + end) + end) +end + +ReinforceLongbows = function() + if SoutheastTurret.IsDead then + return + end + + local goals = { LongbowGoal1, LongbowGoal2, LongbowGoal3 } + local spawnOffset = 0 + + Utils.Do(goals, function(goal) + local entry = { LongbowEntry.Location + CVec.New(spawnOffset, 15) } + + Reinforcements.Reinforce(GoodGuy, { "heli" }, entry, 0, function(helicopter) + helicopter.Wait(DateTime.Seconds(1)) + helicopter.AttackMove(goal.Location) + + -- Original behavior was to fire at the ground until out of ammo. + Trigger.OnKilled(SoutheastTurret, function() + helicopter.Stop() + helicopter.Wait(10) + helicopter.Move(LongbowExit.Location, 1) + helicopter.Destroy() + end) + end) + + spawnOffset = spawnOffset + 5 + end) +end + +ReinforceBadGuy = function() + -- If ForwardCommand dies early, the General will be busy hunting. + if General.IsDead or ForwardCommand.IsDead then + return + end + + BadGuyReinforced = true + Media.PlaySpeechNotification(Greece, "SignalFlareWest") + local flare = SpawnFlare(NorthFlare.Location) + + Trigger.OnKilled(General, function() + RemoveActor(flare, DateTime.Seconds(30)) + end) + + local waterCamera = SpawnPlayerCamera(NorthWaterEntry.Location, -1) + local boatPath = { NorthWaterEntry.Location, BuilderBeach.Location } + local cargo = Reinforcements.ReinforceWithTransport(BadGuy, "lst", { "mcv" }, boatPath, { boatPath[1] })[2] + + Utils.Do(cargo, function(builder) + Trigger.OnKilled(builder, CheckNewBase) + + Trigger.OnAddedToWorld(builder, function() + local builderCamera = SpawnPlayerCamera(builder.Location, -1, "camera.small") + builder.Move(BuilderRally.Location) + + builder.CallFunc(function() + RemoveActor(waterCamera) + RemoveActor(builderCamera) + end) + + builder.Deploy() + end) + end) +end + +ReinforceNorthChinook = function() + local delay = DateTime.Seconds(4) + local camera = SpawnPlayerCamera(NorthChinookUnload.Location, -1, "camera.small", delay + DateTime.Seconds(2)) + + Trigger.AfterDelay(delay, function() + local transport = Actor.Create("tran.north", true, { Owner = USSR, Location = NorthChinookEntry.Location, Facing = Angle.South }) + local passengers = transport.Passengers + + transport.UnloadPassengers(NorthChinookUnload.Location) + transport.Move(SouthChinookExit.Location) + transport.Destroy() + + Trigger.OnPassengerExited(transport, function() + if transport.PassengerCount > 0 then + return + end + + -- Original unload is slow. Mimic the timing with a short pause. + Trigger.AfterDelay(DateTime.Seconds(3), function() + OrderNorthChinookSoldiers(passengers, camera) + end) + end) + + Trigger.OnAnyKilled(passengers, function() + RemoveActor(camera, DateTime.Seconds(1)) + end) + end) +end + +OrderNorthChinookSoldiers = function(soldiers, camera) + local patrolPath = + { + VillageNortheast.Location, + VillageNorthwest.Location, + VillageSouthwest.Location, + VillageSoutheast.Location + } + local liveSoldiers = Utils.Where(soldiers, IsAlive) + + Utils.Do(liveSoldiers, function(soldier) + soldier.AttackMove(NorthChinookRally.Location) + + soldier.CallFunc(function() + Trigger.AfterDelay(1, function() + if not AreAllIdleOrDead(liveSoldiers) then + return + end + RemoveActor(camera, DateTime.Seconds(1)) + GroupAttackMove(liveSoldiers, VillageBridgeCenter.Location, 2) + GroupTightPatrol(liveSoldiers, patrolPath, true) + GroupHuntOnDamaged(liveSoldiers, Greece) + end) + end) + end) +end + +ReinforceVillagePatrol = function() + if VillagePatrolSent then + return + end + + VillagePatrolSent = true + + local path = + { + VillageSoutheast.Location, + VillageSouthwest.Location, + VillageNorthwest.Location, + VillageNortheast.Location + } + local types = { "e1", "e1", "e2", "dog" } + local origin = { VillagePatrolEntry.Location } + local group = Reinforcements.Reinforce(USSR, types, origin, 0) + GroupTightPatrol(group, path, true) +end + +ReinforceGuardHouse = function(intruderType) + if GuardHouse.IsDead then + return + end + + local goal = GuideBarrelGoal.Location + if intruderType == TanyaType then + goal = PrisonReveal.Location + end + + for i = 1, 4 do + Trigger.AfterDelay(i * 5, function() + local guard = Actor.Create("e1", true, { Owner = USSR, Location = GuardHouseSpawn.Location, SubCell = 4, Facing = Angle.SouthWest }) + guard.Move(GuardHouseExit.Location) + guard.AttackMove(goal) + IdleHunt(guard) + end) + end +end + +ReinforceHardTeams = function() + if Difficulty ~= "hard" then + return + end + + ReinforceBaseDefenders() + ReinforceNorthBeachGuards() + ReinforceV2Team() + ReinforceNorthEdgeGuards() + PrepareSouthChinook() + ReinforceHardDogs() +end + +ReinforceBaseDefenders = function() + local origin = BadGuyRally.Location + CVec.New(2, 2) + local types = { "3tnk", "3tnk" } + local defenders = Reinforcements.Reinforce(USSR, types, { origin }, 0, function(actor) + actor.Wait(1) + actor.Scatter() + end) + GroupHuntOnDamaged(defenders, Greece) + + local structures = { RoadTurretWest, RoadTurretEast, ForwardPower, ForwardCommand, ForwardTech } + Trigger.OnAnyKilled(structures, function() + GroupIdleHunt(defenders) + end) +end + +ReinforceNorthBeachGuards = function() + local types = { "e1", "e1", "e2", "e1" } + local patrolPath = { VillageNortheast.Location, VillageSouthwest.Location } + local soldiers = Reinforcements.Reinforce(USSR, types, { NorthFlare.Location }, 0, function(actor) + actor.Scatter() + end) + + Trigger.OnEnteredFootprint({ GeneralRally.Location }, function(actor, id) + if actor.Type ~= "gnrl" then + return + end + + Trigger.RemoveFootprintTrigger(id) + GroupHuntOnDamaged(soldiers, Greece) + + Utils.Do(soldiers, function(soldier) + if soldier.IsDead then + return + end + + soldier.AttackMove(BadGuyRally.Location) + soldier.AttackMove(GuideBarrelGoal.Location) + soldier.AttackMove(VillageNorthwest.Location) + soldier.Patrol(patrolPath, true) + end) + end) +end + +ReinforceV2Team = function() + local rocket = Actor.Create("v2rl", true, { Owner = USSR, Facing = Angle.West, Location = SamRocketEntry.Location }) + Reinforcements.Reinforce(USSR, { "e1" }, { SamFenceReveal.Location }, 0, function(actor) + actor.Guard(rocket) + end) +end + +PrepareSouthChinook = function() + local startPosition = SouthChinookEntry.CenterPosition + WVec.New(0, 0, Actor.CruiseAltitude("tran")) + + Trigger.OnEnteredProximityTrigger(SouthChinookProximity.CenterPosition, WDist.FromCells(8), function(actor, id) + if actor.Type ~= TanyaType or not EscortFinished then + return + end + + Trigger.RemoveProximityTrigger(id) + + local transport = Actor.Create("tran.south", true, { Owner = USSR, CenterPosition = startPosition, Facing = Angle.East }) + transport.UnloadPassengers(SouthChinookEntry.Location) + transport.Move(SouthChinookExit.Location) + transport.Destroy() + + Utils.Do(transport.Passengers, function(passenger) + Trigger.OnAddedToWorld(passenger, function() + passenger.Wait(DateTime.Seconds(1)) + IdleHunt(passenger) + end) + end) + end) +end + +ReinforceNorthEdgeGuards = function() + local types = { "e1", "e1", "e2" } + local guards = Reinforcements.Reinforce(USSR, types, { NorthEdgeGuardEntry.Location }, 0, function(actor) + actor.Scatter() + end) + GroupHuntOnDamaged(guards, Greece) +end + +ReinforceStartSoldiers = function() + local teams = + { + { + entry = { WestRoadEntry.Location }, + types = { "e2", "e2", "e1" } + }, + { + entry = { RoadTurretRifle2.Location }, + types = { "e1", "e2", "e4" } + } + } + + Utils.Do(teams, function(team) + local soldiers = Reinforcements.Reinforce(USSR, team.types, team.entry, 15, function(actor) + actor.AttackMove(TanyaDrop.Location) + actor.Hunt() + end) + + Trigger.OnAnyKilled(soldiers, function() + IdleHunt(StartSoldier) + end) + end) +end + +ReinforceHardDogs = function() + local startDog = Actor.Create("dog", true, { Owner = USSR, Location = WestRoadEntry.Location, Facing = Angle.South }) + startDog.Move(WestRoadRally.Location + CVec.New(-1, 0)) + PrepareStartDogAttack(startDog) + + local turretDog = Actor.Create("dog.areaguard", true, { Owner = USSR, Location = LongbowGoal2.Location, Facing = Angle.SouthWest }) + turretDog.AddTag("TurretDog") + + Trigger.OnKilled(turretDog, function() + GroupIdleHunt(SoutheastTurretGuards) + end) +end + +PrepareStartDogAttack = function(dog) + -- Tanya will take about 100 ticks/4 seconds to touch the ground. + local delay = DateTime.Seconds(5) + 5 + + Trigger.OnEnteredFootprint({ TanyaDrop.Location }, function(actor, id) + if actor.Type ~= TanyaType then + return + end + + Trigger.RemoveFootprintTrigger(id) + Trigger.AfterDelay(delay, function() + if dog.IsDead or actor.IsDead then + return + end + + dog.Attack(actor) + end) + end) +end diff --git a/mods/ra/maps/negotiations/negotiations.lua b/mods/ra/maps/negotiations/negotiations.lua new file mode 100644 index 0000000000..5ddd6993d6 --- /dev/null +++ b/mods/ra/maps/negotiations/negotiations.lua @@ -0,0 +1,1265 @@ +--[[ + 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. +]] +BadGuy = Player.GetPlayer("BadGuy") +England = Player.GetPlayer("England") +Greece = Player.GetPlayer("Greece") +GoodGuy = Player.GetPlayer("GoodGuy") +Neutral = Player.GetPlayer("Neutral") +USSR = Player.GetPlayer("USSR") + +ForestTeam = { ForestPatroller1, ForestPatroller2, ForestPatroller3, ForestPatroller4, ForestPatroller5, ForestPatroller6, ForestPatroller7, ForestPatroller8 } +BlockerTeam = { BlockerGuard1, BlockerGuard2, BlockerTank } +SoutheastTurretGuards = { SoutheastTurretRifle1, SoutheastTurretRifle2, SoutheastTurretRifle3 } +Hostages = { Hostage1, Hostage2, Hostage3, Hostage4, Hostage5 } + +TimerColor = HSLColor.White +TimeRemaining = 0 + +TanyaType = "e7.noautotarget" +if Difficulty == "easy" then + TanyaType = "e7" +end + +WorldLoaded = function() + Camera.Position = TanyaDrop.CenterPosition + SpawnPlayerCamera(TanyaDrop.Location, DateTime.Seconds(20)) + + StartSoldier.Move(WestRoadRally.Location) + if Difficulty == "hard" then + StartSoldier.CallFunc(ReinforceStartSoldiers) + end + + VillageBridge = GetBridge() + ReinforceHardTeams() + PrepareBadGuy() + ReinforceTanya() + CreateInitialTriggers() + CreateInitialObjectives() + LinkCooperativeObjectives() + CheckSovietDestruction() +end + +CreateInitialObjectives = function() + InitObjectives(Greece) + if Difficulty == "hard" then + SaveAllHostages = AddPrimaryObjective(Greece, "keep-all-hostages-alive") + end + FreeTheHostages = AddPrimaryObjective(Greece, "free-hostages") + SaveAllHostages = SaveAllHostages or AddSecondaryObjective(Greece, "keep-all-hostages-alive") + SurviveUntilEnd = AddPrimaryObjective(England, "") + StopAllies = AddPrimaryObjective(USSR, "") +end + +--[[ + Link Greece's win and victory notification to England's own victory. + + To reduce message spam, the creation of some primary objectives is delayed. + That is done by making this a co-op mission between Greece and England + (owner of the hostages). Because both must achieve their objectives, Greece + can finish all primaries without immediately triggering victory. +]] +LinkCooperativeObjectives = function() + local greeceActor = Greece.GetActorsByType("player")[1] + Trigger.Clear(greeceActor, "OnPlayerWon") + + Trigger.OnPlayerWon(England, function() + USSR.MarkFailedObjective(StopAllies) + + Trigger.AfterDelay(DateTime.Seconds(1), function() + Media.PlaySpeechNotification(Greece, "MissionAccomplished") + end) + end) + + Trigger.AfterDelay(1, function() + -- Triggers any time Greece finishes all CURRENT primary objectives. + Trigger.OnPlayerWon(Greece, function() + local sovietsDestroyed = DestroySoviets ~= nil and Greece.IsObjectiveCompleted(DestroySoviets) + if sovietsDestroyed and EscortFinished then + England.MarkCompletedObjective(SurviveUntilEnd) + end + end) + end) +end + +CheckSovietDestruction = function() + local destroyed = USSR.HasNoRequiredUnits() and BadGuy.HasNoRequiredUnits() + if not destroyed then + Trigger.AfterDelay(DateTime.Seconds(1), CheckSovietDestruction) + return + end + + -- Cover unlikely case that all Soviets are destroyed with hostages in tow. + DestroySoviets = DestroySoviets or AddPrimaryObjective(Greece, "destroy-soviet-units-infrastructure") + Greece.MarkCompletedObjective(DestroySoviets) + + if EscortFinished and not Greece.IsObjectiveFailed(SaveAllHostages) then + Greece.MarkCompletedObjective(SaveAllHostages) + end +end + +CheckAlliedDestruction = function() + if #Greece.GetGroundAttackers() == 0 then + MarkAlliedDefeat() + end +end + +MarkAlliedDefeat = function(objective, delay) + if AlliesDefeated then + return + end + + StopCountdown() + AlliesDefeated = true + delay = delay or DateTime.Seconds(1) + + Trigger.AfterDelay(delay, function() + if not Greece.IsObjectiveCompleted(FreeTheHostages) then + Greece.MarkFailedObjective(SaveAllHostages) + end + + if objective then + Greece.MarkFailedObjective(objective) + return + end + + local mainObjectives = { FreeTheHostages, EscortTheHostages, DestroySoviets } + Utils.Do(mainObjectives, function(o) + if not Greece.IsObjectiveCompleted(o) then + Greece.MarkFailedObjective(o) + end + end) + end) +end + +CheckEscortObjective = function() + if EscortFinished then + return + end + + local maximum = Utils.Where(Hostages, IsAlive) + if Church.PassengerCount < #maximum then + return + end + OnHostagesEscorted() +end + +CreateInitialTriggers = function() + PrepareChurch() + PrepareHostages() + PrepareReveals() + PrepareSovietGuards() + PrepareForestEncounter() + PrepareChronoTanks() + PrepareBlockers() + + Trigger.OnKilled(ForwardCommand, OnForwardCommandKilled) + + local sams = { WestSam, EastSam } + Trigger.OnAllKilled(sams, ReinforceLongbows) + + local guideHouseAttackers = { GuideRifleWest, GuideRifleEast } + Trigger.OnAllKilled(guideHouseAttackers, OnGuideRiflesKilled) + + Trigger.OnEnteredProximityTrigger(TanyaDrop.CenterPosition, WDist.FromCells(4), function(actor, id) + if not EscortFinished or actor.Type ~= TanyaType then + return + end + + Trigger.RemoveProximityTrigger(id) + SignalAllies() + end) + + local prisonApproached = false + Trigger.OnEnteredProximityTrigger(Prison.CenterPosition, WDist.FromCells(8), function(actor, id) + if prisonApproached or not IsGuideOrAllies(actor) then + return + end + + prisonApproached = true + Trigger.RemoveProximityTrigger(id) + ReinforceGuardHouse(actor.Type) + end) + + local prisonBarrels = { PrisonBarrel1, PrisonBarrel2, PrisonBarrel3, PrisonBarrel4 } + Trigger.OnAllKilled(prisonBarrels, function() + local targets = { Prison, Executioner } + Utils.Do(targets, function(target) + if not target.IsDead then + target.Kill("FireDeath") + end + end) + end) +end + +PrepareHostages = function() + ScheduleExecutions() + Trigger.OnKilled(Executioner, FreeHostages) + Trigger.OnAllKilled(Hostages, OnAllHostagesKilled) + Utils.Do(Hostages, function(hostage) + Trigger.OnKilled(hostage, OnHostageKilled) + end) + + -- Keep hostages wandering, but only inside the pen. + Trigger.OnEnteredProximityTrigger(Executioner.CenterPosition, WDist.FromCells(2), function(actor, id) + if Executioner.IsDead then + Trigger.RemoveProximityTrigger(id) + return + end + if actor.Owner ~= Neutral then + return + end + + actor.Stop() + actor.Move(HostageCenter.Location) + end) +end + +Tick = function() + if TimeRemaining < 1 then + return + end + TimeRemaining = TimeRemaining - 1 + + if TimeRemaining % DateTime.Seconds(1) ~= 0 then + return + end + + local text = UserInterface.Translate("hostage-dies-in", { ["time"] = Utils.FormatTime(TimeRemaining) }) + UserInterface.SetMissionText(text, TimerColor) +end + +PrepareCountdown = function(interval) + local text = UserInterface.Translate("hostage-dies-in", { ["time"] = Utils.FormatTime(interval) }) + UserInterface.SetMissionText(text, TimerColor) + DateTime.TimeLimit = 1 + + Trigger.OnTimerExpired(function() + local survivors = Utils.Where(Hostages, IsAlive) + if #survivors > 1 then + ResetCountdown(interval) + return + end + UserInterface.SetMissionText("") + end) +end + +ResetCountdown = function(interval) + DateTime.TimeLimit = interval + TimeRemaining = interval + TimerColor = HSLColor.White + + Trigger.AfterDelay(interval - DateTime.Minutes(2), function() + TimerColor = USSR.Color + end) +end + +StopCountdown = function() + DateTime.TimeLimit = 0 + TimeRemaining = 0 + UserInterface.SetMissionText("") +end + +ScheduleExecutions = function() + local intervals = + { + easy = 5, + normal = 4, + hard = 3, + } + -- -1 tick avoids the redundant x-minute warning speech upon timer refresh. + local interval = DateTime.Minutes(intervals[Difficulty]) - 1 + local delay = 0 + + PrepareCountdown(interval) + + Utils.Do(Hostages, function(hostage) + delay = delay + interval + + Trigger.AfterDelay(delay, function() + ExecuteHostage(hostage) + end) + end) +end + +ExecuteHostage = function(hostage) + if hostage.IsDead then + local survivors = Utils.Where(Hostages, IsAlive) + if #survivors > 0 then + hostage = Utils.Random(survivors) + end + end + + if Executioner.IsDead or hostage.IsDead then + return + end + + Media.PlaySoundNotification(Greece, "HostageShot") + hostage.Kill("DefaultDeath") +end + +FreeHostages = function() + StopCountdown() + ReinforceNorthChinook() + Greece.MarkCompletedObjective(FreeTheHostages) + + Trigger.AfterDelay(DateTime.Seconds(3), function() + EscortTheHostages = AddPrimaryObjective(Greece, "get-hostages-to-church") + end) + + local liveHostages = Utils.Where(Hostages, IsAlive) + Utils.Do(liveHostages, function(hostage) + hostage.Owner = England + FollowTanya(hostage) + end) + + local guide = GetGuide() + if guide then + guide.Stop() + FollowTanya(guide) + end +end + +FollowTanya = function(actor) + -- Hostages' panic can make them forget orders; OnIdle will refresh them. + -- For the same reason, the Guide elsewhere uses OnIdle for his actions. + Trigger.OnIdle(actor, function() + if Tanya.IsDead then + return + end + + actor.Guard(Tanya) + end) +end + +PrepareForestEncounter = function() + ForestTeamReachedHouse = false + local patrolPassed = false + + local forestFoot = Trigger.OnEnteredFootprint({ ForestSafetyCheck.Location }, function(actor, id) + if patrolPassed or actor.Owner ~= USSR then + return + end + + patrolPassed = true + Trigger.RemoveFootprintTrigger(id) + GuideToVillage() + end) + + Trigger.OnAllKilled(ForestTeam, function() + if patrolPassed then + return + end + + patrolPassed = true + Trigger.RemoveFootprintTrigger(forestFoot) + GuideToVillage(nil, true) + end) + + Trigger.OnEnteredFootprint({ ForestCenter.Location }, function(actor, id) + if not IsGuide(actor) then + return + end + + Trigger.RemoveFootprintTrigger(id) + + Trigger.AfterDelay(0, function() + GuideToHideout(actor) + end) + end) + + Trigger.OnExitedFootprint({ ForestHideout.Location }, function(actor, id) + if Executioner.IsDead or actor.IsDead or not IsGuide(actor) then + return + end + + Trigger.RemoveFootprintTrigger(id) + Media.DisplayMessage(UserInterface.Translate("guide-follow-me"), actor.TooltipName) + end) + + Trigger.OnEnteredFootprint({ ForestSouthwest.Location }, function(actor, id) + if actor.Type ~= "dog" then + return + end + + Trigger.RemoveFootprintTrigger(id) + + Trigger.AfterDelay(DateTime.Seconds(1), function() + if not actor.IsDead then + Media.PlaySoundNotification(Greece, "DogWhine") + end + end) + end) + + Trigger.OnEnteredFootprint({ GuideHouseReveal.Location }, function(actor, id) + if actor.Type ~= "dog" then + return + end + + ForestTeamReachedHouse = true + Trigger.RemoveFootprintTrigger(id) + end) +end + +PrepareChronoTanks = function() + local sides = + { + westRoad = + { + cells = { CPos.New(41, 65), CPos.New(42, 65), CPos.New(43, 65), CPos.New(44, 65), CPos.New(45, 65), CPos.New(46, 65) }, + spawns = { ChronoEntryWest1.Location, ChronoEntryWest2.Location } + }, + eastRoad = + { + cells = { CPos.New(66, 70), CPos.New(66, 71), CPos.New(67, 71), CPos.New(68, 71), CPos.New(69, 71), CPos.New(70, 71), CPos.New(71, 71), CPos.New(72, 71), CPos.New(72, 71), CPos.New(73, 71) }, + spawns = { ChronoEntryEast1.Location, ChronoEntryEast2.Location } + } + } + + Utils.Do(sides, function(side) + Trigger.OnEnteredFootprint(side.cells, function(actor, id) + local signaled = SignalForReinforcements and Greece.IsObjectiveCompleted(SignalForReinforcements) + if not signaled or actor.Owner ~= Greece then + return + end + + Trigger.RemoveFootprintTrigger(id) + ReinforceChronoTanks(side.spawns) + end) + end) +end + +PrepareSovietGuards = function() + Utils.Do(BadGuy.GetGroundAttackers(), function(soviet) + Trigger.OnDamaged(soviet, function(_, attacker) + if attacker.Type == "dtrk" or attacker.Owner ~= Greece then + return + end + + Trigger.Clear(soviet, "OnDamaged") + IdleHunt(soviet) + end) + end) + + local beachAlerted = false + local beachGroup = { SouthBeachGuard1, SouthBeachGuard2, SouthBeachGuard3, SouthBeachGuard4 } + Trigger.OnAllKilled(beachGroup, SovietSouthBeachCamera.Destroy) + + Trigger.OnAnyKilled(beachGroup, function() + if beachAlerted or Tanya.IsDead then + return + end + + beachAlerted = true + GroupIdleHunt(beachGroup) + + Utils.Do(beachGroup, function(actor) + if actor.IsDead then + return + end + -- Get attack dogs sprinting if they aren't already. + actor.Attack(Tanya) + end) + end) + + local others = + { + { RoadTurretRifle1, RoadTurretRifle2, RoadTurretWest, RoadTurretEast }, + { GuideRifleWest, GuideRifleEast }, + { FarmGuard1, FarmGuard2, FarmGuard3, FarmGuard4 }, + SoutheastTurretGuards, + ForestTeam, + BlockerTeam + } + + Utils.Do(others, function(group) + GroupHuntOnDamaged(group, Greece) + end) +end + +PrepareMainReveals = function() + local mainReveals = + { + roadTurrets = + { + cells = { CPos.New(36, 71), CPos.New(36, 72), CPos.New(36, 73), CPos.New(37, 73), CPos.New(38, 73), CPos.New(39, 73), CPos.New(40, 73), CPos.New(41, 73), CPos.New(42, 73), CPos.New(43, 73), CPos.New(44, 73), CPos.New(45, 73), CPos.New(46, 73), CPos.New(47, 73) }, + location = RoadTurretReveal.Location + }, + southBeachBarrels = + { + cells = { CPos.New(51, 75), CPos.New(51, 76), CPos.New(51, 77), CPos.New(51, 78), CPos.New(51, 79) }, + location = SouthBeachReveal.Location, + onTriggered = SovietStartCamera.Destroy + }, + southeastTurret = + { + cells = { CPos.New(68, 82), CPos.New(68, 83), CPos.New(68, 84), CPos.New(68, 85), CPos.New(68, 86), CPos.New(68, 87), CPos.New(68, 88), CPos.New(68, 89) }, + location = SoutheastTurret.Location, + onTriggered = function() + local dogs = Map.ActorsWithTag("TurretDog") + GroupAttackMove(dogs, SoutheastTurret.Location + CVec.New(0, 1)) + end + }, + samFence = + { + cells = { CPos.New(69, 74), CPos.New(70, 74), CPos.New(71, 74), CPos.New(72, 74), CPos.New(73, 74), CPos.New(74, 74) }, + location = SamFenceReveal.Location, + onTriggered = ReinforceVillagePatrol + }, + riverHouse = + { + cells = { CPos.New(80, 79), CPos.New(80, 80), CPos.New(80, 81), CPos.New(80, 82), CPos.New(80, 83) }, + location = GuideHouse.Location, + type = "camera.paradrop", + onTriggered = function() + ReinforceVillagePatrol() + OrderGuideRifles() + end + }, + forest = + { + cells = { CPos.New(85, 62), CPos.New(86, 62), CPos.New(87, 62), CPos.New(88, 62), CPos.New(89, 62), CPos.New(90, 62), CPos.New(91, 62), CPos.New(92, 62), CPos.New(93, 62), CPos.New(94, 62) }, + location = ForestCenter.Location, + duration = DateTime.Seconds(20), + onTriggered = function() + OrderForestPatrol(DateTime.Seconds(3)) + SpawnPlayerCamera(ForestPatrolStart.Location, DateTime.Seconds(6), "camera") + end + }, + forestTeamBackAtForest = + { + location = ForestCenter.Location, + type = "camera.tiny", + condition = function(actor) + return ForestTeamReachedHouse and actor.Type == "dog" + end + }, + forestTeamBackAtStart = + { + location = ForestPatrolStart.Location, + type = "camera.small", + condition = function(actor) + return ForestTeamReachedHouse and actor.Type == "dog" + end + }, + forestTeamAtBridge = + { + location = VillageBridgeNortheast.Location, + type = "camera.small", + condition = function(actor) + return ForestTeamReachedHouse and actor.Type == "dog" + end + } + } + + Utils.Do(mainReveals, function(reveal) + local triggered = false + local isCorrect = reveal.condition or function(actor) + return IsGuideOrAllies(actor) + end + + Trigger.OnEnteredFootprint(reveal.cells or { reveal.location }, function(actor, id) + if triggered or not isCorrect(actor) then + return + end + + triggered = true + Trigger.RemoveFootprintTrigger(id) + + if reveal.location then + SpawnPlayerCamera(reveal.location, reveal.duration, reveal.type) + end + + if reveal.onTriggered then + reveal.onTriggered() + end + end) + end) +end + +PrepareBlockers= function() + local houseReached = false + + Trigger.OnEnteredProximityTrigger(BlockerReturnProximity.CenterPosition, WDist.FromCells(2), function(actor, id) + if not Executioner.IsDead or actor.Owner ~= Greece then + return + end + + Trigger.RemoveProximityTrigger(id) + BlockersToBridgePatrol() + end) + + Trigger.OnEnteredProximityTrigger(GuideHouseReveal.CenterPosition, WDist.FromCells(2), function(actor, id) + if BlockerTank.IsDead then + Trigger.RemoveProximityTrigger(id) + return + end + + if not Executioner.IsDead or actor ~= BlockerTank then + return + end + + houseReached = true + Trigger.RemoveProximityTrigger(id) + end) + + -- For BlockerTeam returning north from the church/Guide house area. + Trigger.OnEnteredProximityTrigger(SouthRiver.CenterPosition, WDist.FromCells(3), function(actor, id) + if BlockerTank.IsDead then + Trigger.RemoveProximityTrigger(id) + return + end + + if not houseReached or not Executioner.IsDead or actor ~= BlockerTank then + return + end + + Trigger.RemoveProximityTrigger(id) + SpawnPlayerCamera(SouthRiver.Location, DateTime.Seconds(6), "camera.tiny") + SpawnPlayerCamera(ForestCenter.Location, DateTime.Seconds(20)) + end) +end + +PrepareReveals = function() + PrepareMainReveals() + + Trigger.OnEnteredProximityTrigger(Prison.CenterPosition, WDist.FromCells(10), function(actor, id) + if not IsGuideOrAllies(actor) then + return + end + + Trigger.RemoveProximityTrigger(id) + SpawnPlayerCamera(PrisonReveal.Location, DateTime.Seconds(20)) + BlockersToGuideHouse() + + local delay = DateTime.Seconds(1) + if actor.Type == TanyaType then + delay = 5 + end + Trigger.AfterDelay(delay, OrderGeneralRetreat) + end) + + Trigger.OnKilled(SovietDemoTruck, function() + if not SovietDemoTruck.IsIdle then + return + end + SpawnPlayerCamera(SovietDemoTruck.Location, DateTime.Seconds(1), "camera.paradrop") + end) + + local otherExplosions = + { + { + actors = { SouthBeachPump1, SouthBeachPump2 }, + origin = BeachExplosionReveal.Location + }, + { + actors = { PrisonPump, PrisonBarrel1, PrisonBarrel2, PrisonBarrel3, PrisonBarrel4 }, + origin = PrisonPump.Location + }, + { + actors = { DemoPump1, DemoPump2, DemoPump3, DemoHouse }, + origin = DemoBarrel3.Location + }, + { + actors = { NorthBeachPump2 }, + origin = NorthBeachPump2.Location + }, + { + actors = { LakePump1, LakePump2 }, + origin = LakeExplosionReveal.Location + }, + } + + Utils.Do(otherExplosions, function(group) + PrepareExplosionReveal(group.actors, group.origin, DateTime.Seconds(1)) + end) +end + +PrepareExplosionReveal = function(actors, origin, duration) + local revealed = false + + Utils.Do(actors, function(actor) + Trigger.OnDamaged(actor, function(_, attacker) + if revealed or (attacker.Type ~= "barl" and attacker.Type ~= "brl3") then + return + end + + revealed = true + SpawnPlayerCamera(origin, duration, "camera.small") + end) + end) +end + +PrepareChurch = function() + Trigger.OnKilled(Church, OnChurchKilled) + Trigger.OnPassengerEntered(Church, CheckEscortObjective) + + Trigger.OnEnteredProximityTrigger(Church.CenterPosition, WDist.FromCells(4), function(actor) + local civilian = IsHostage(actor) or IsGuide(actor) + if civilian then + EnterChurch(actor) + end + end) +end + +EnterChurch = function(actor) + if Church.IsDead then + return + end + + actor.Stop() + actor.Move(ChurchRally.Location) + if IsGuide(actor) then + actor.Infiltrate(Church) + return + end + actor.EnterTransport(Church) +end + +StartFireSale = function(player) + -- Short delay in case the Soviets' Tech Center must change owner first. + Trigger.AfterDelay(1, function() + local buildings = Utils.Where(player.GetActors(), function(actor) + return actor.HasProperty("StartBuildingRepairs") + end) + + if #buildings == 0 then + return + end + + Utils.Do(buildings, function(building) + building.Sell() + end) + + Trigger.OnAllRemovedFromWorld(buildings, function() + GroupIdleHunt(player.GetGroundAttackers()) + end) + end) +end + +SignalAllies = function() + Greece.MarkCompletedObjective(SignalForReinforcements) + SpawnFlare(TanyaDrop.Location, DateTime.Minutes(4)) + Trigger.AfterDelay(DateTime.Seconds(1), ReinforceAllies) + + if General.IsDead then + OrderBadGuyHunters() + return + end + Trigger.OnKilled(General, OrderBadGuyHunters) +end + +GroupAttackMove = function(actors, location, closeEnough) + Utils.Do(actors, function(actor) + if actor.IsDead then + return + end + actor.AttackMove(location, closeEnough or 0) + end) +end + +GroupIdleHunt = function(actors) + Utils.Do(actors, IdleHunt) +end + +-- Patrol along path. Pause at each location until others arrive. +-- At final location, call onFinished(actors) or loop and restart. +GroupTightPatrol = function(actors, path, looped, pauseTime, onFinished) + pauseTime = pauseTime or 0 + local paused = false + local goal = 1 + local arrived = { } + local patrollers = Utils.Where(actors, IsAlive) + + for id = 1, #patrollers do + Trigger.OnIdle(patrollers[id], function(actor) + if paused then + return + end + + -- Move each unit to the goal until their arrival or death. + if not arrived[id] then + actor.AttackMove(path[goal], 2) + actor.CallFunc(function() + arrived[id] = true + end) + return + end + + if not AreAllIdleOrDead(patrollers) then + return + end + + -- All live actors have arrived and are idle. + paused = true + Trigger.AfterDelay(pauseTime, function() + paused = false + end) + + arrived = { } + local nextLocation, patrolFinished = NextPatrolLocation(goal, #path, looped) + + if patrolFinished then + ClearTightPatrol(patrollers, onFinished) + return + end + + goal = nextLocation + end) + end +end + +-- Fetch the next goal in a patrol path, or signal the end of a one-way patrol. +NextPatrolLocation = function(current, final, looped) + if current < final then + return current + 1, false + end + + if looped then + return 1, false + end + + return current, true +end + +-- Clear a group's TightPatrol and give orders queued for that patrol's finish. +ClearTightPatrol = function(actors, onFinished) + Utils.Do(actors, function(actor) + if actor.IsDead then + return + end + + Trigger.Clear(actor, "OnIdle") + end) + + if not onFinished then + return + end + + Trigger.AfterDelay(1, function() + onFinished(actors) + end) +end + +GroupHuntOnDamaged = function(actors, targetPlayer) + local alerted = false + Utils.Do(actors, function(actor) + if actor.IsDead then + return + end + + Trigger.OnDamaged(actor, function(_, attacker) + if alerted or attacker.Type == "dtrk" or attacker.Owner ~= targetPlayer then + return + end + alerted = true + + Utils.Do(actors, function(hunter) + if hunter.IsDead or not hunter.HasProperty("Hunt") then + return + end + -- If the hunter is patrolling, their patrol will be aborted. + hunter.Stop() + Trigger.Clear(hunter, "OnIdle") + + Trigger.AfterDelay(1, function() + IdleHunt(hunter) + end) + end) + end) + end) +end + +OrderForestPatrol = function(delay) + local path = + { + ForestNorthwest.Location, + ForestSouthwest.Location, + SouthRiver.Location, + GuideHouseReveal.Location, + ForestCenter.Location, + ForestPatrolStart.Location, + VillageBridgeNortheast.Location, + VillageNortheast.Location, + PrisonReveal.Location, + BadGuyRally.Location + } + + Trigger.AfterDelay(delay, function() + GroupTightPatrol(ForestTeam, path, false) + end) +end + +BlockersToGuideHouse = function() + local path = + { + ForestNorthwest.Location, + SouthRiver.Location, + GuideHouseReveal.Location + } + GroupTightPatrol(BlockerTeam, path, false) +end + +BlockersToBridgePatrol = function() + local path = + { + SouthRiver.Location, + ForestSouthwest.Location, + ForestNorthwest.Location, + ForestPatrolStart.Location, + VillageBridgeNortheast.Location, + ForestPatrolStart.Location, + ForestNorthwest.Location, + ForestSouthwest.Location + } + GroupTightPatrol(BlockerTeam, path, true) +end + +SpawnGuide = function() + Trigger.AfterDelay(15, function() + if GuideHouse.IsDead then + return + end + + local guide = Actor.Create("c1", true, { Owner = GoodGuy, Location = GuideHouse.Location, SubCell = 4, Facing = Angle.South } ) + GuideToForest(guide) + end) +end + +GuideToForest = function(guide) + guide.Move(GuideHouse.Location + CVec.New(0, 1)) + + guide.CallFunc(function() + Media.DisplayMessage(UserInterface.Translate("guide-thank-you"), guide.TooltipName) + end) + + guide.Move(SouthRiver.Location) + guide.Move(ForestSouthwest.Location) + guide.Move(ForestCenter.Location) +end + +GuideToHideout = function(guide) + if IsGroupDead(ForestTeam) then + -- If this team dies, the Guide will skip ahead to GuideToVillage. + return + end + + guide.CallFunc(function() + Media.DisplayMessage(UserInterface.Translate("guide-patrol-coming"), guide.TooltipName) + end) + + guide.Wait(DateTime.Seconds(2)) + + guide.CallFunc(function() + if IsGroupDead(ForestTeam) then + return + end + + Media.DisplayMessage(UserInterface.Translate("guide-come-this-way"), guide.TooltipName) + guide.Move(ForestHideout.Location) + end) +end + +GuideToVillage = function(guide, patrolKilled) + guide = guide or GetGuide() + if not guide then + return + end + + guide.Stop() + Media.PlaySoundNotification(Greece, "GuideOkay") + Media.DisplayMessage(UserInterface.Translate("guide-safe-to-move"), guide.TooltipName) + if not patrolKilled then + guide.Wait(DateTime.Seconds(1)) + end + + if VillageBridge.IsDead then + GuideToBackdoor(guide) + return + else + Trigger.OnKilled(VillageBridge, function() + GuideToBackdoor(guide) + end) + end + + guide.Move(ForestNorthwest.Location) + guide.Move(VillageBridgeNortheast.Location) + + Trigger.OnIdle(guide, function() + guide.AttackMove(PrisonIntersection.Location) + guide.CallFunc(function() + GuideToHostages(guide) + end) + end) +end + +GuideToBackdoor = function(guide) + if guide.IsDead or Greece.IsObjectiveCompleted(FreeTheHostages) then + return + end + + Trigger.Clear(guide, "OnIdle") + Trigger.AfterDelay(1, function() + if guide.IsDead then + return + end + + guide.Stop() + Trigger.OnIdle(guide, function() + guide.AttackMove(GuideBackdoorGoal.Location) + guide.CallFunc(function() + GuideToHostages(guide) + end) + end) + end) +end + +GuideToHostages = function(guide) + if Executioner.IsDead or PrisonBarrel2.IsDead or PrisonBarrel3.IsDead then + return + end + + -- To compensate for the guards' better speed/range with minimal changes, + -- the Guide is made untargetable until actors are in position (or dead). + local ghost = guide.GrantCondition("untargetable") + Trigger.Clear(guide, "OnIdle") + + Trigger.OnEnteredProximityTrigger(PrisonBarrel3.CenterPosition, WDist.FromCells(2), function(actor, id) + if Executioner.IsDead or PrisonBarrel2.IsDead or guide.IsDead then + Trigger.RemoveProximityTrigger(id) + return + end + + if actor.Type ~= "e1" or guide.Location ~= GuideBarrelGoal.Location then + return + end + + Trigger.RemoveProximityTrigger(id) + guide.Stop() + guide.Attack(PrisonBarrel2) + end) + + guide.Move(GuideBarrelGoal.Location) + local revokeTargets = Utils.Where({ Executioner, PrisonBarrel1, PrisonBarrel2, PrisonBarrel3, PrisonBarrel4 }, IsAlive) + + Trigger.OnAnyKilled(revokeTargets, function() + if guide.IsDead then + return + end + + guide.RevokeCondition(ghost) + end) +end + +OrderGeneralRetreat = function() + if General.IsDead or not General.IsIdle then + return + end + + local reinforcementTimer = DateTime.Seconds(30) + + General.Move(BadGuyRally.Location) + General.Move(GeneralRally.Location + CVec.New(0, 3)) + + General.CallFunc(function() + local camera = SpawnPlayerCamera(GeneralRally.Location, reinforcementTimer) + + Trigger.OnKilled(General, function() + RemoveActor(camera) + end) + end) + + General.Move(GeneralRally.Location) + General.CallFunc(function() + Trigger.AfterDelay(reinforcementTimer, ReinforceBadGuy) + end) +end + +OrderGuideRifles = function() + local rifles = { GuideRifleWest, GuideRifleEast } + local offset = 0 + + Utils.Do(rifles, function(rifle) + Trigger.AfterDelay(offset, function() + if rifle.IsDead or GuideHouse.IsDead then + return + end + + rifle.Attack(GuideHouse, true, true) + end) + + offset = offset + 8 + end) +end + +OrderBadGuyHunters = function() + local blockers = Utils.Where(BlockerTeam, IsAlive) + ClearTightPatrol(blockers, function(actors) + GroupAttackMove(actors, WestRoadRally.Location, 2) + GroupIdleHunt(actors) + end) + + local others = { SovietDemoTruck, DemoFlamer1, DemoFlamer2, ForwardCliffGuard2 } + Utils.Do(others, function(other) + if other.IsDead then + return + end + + other.AttackMove(WestRoadRally.Location) + IdleHunt(other) + end) +end + +AreAllIdleOrDead = function(actors) + return Utils.All(actors, function(actor) + return actor.IsIdle or actor.IsDead + end) +end + +GetBridge = function() + local bridges = Neutral.GetActorsByType("bridge1") + return bridges[1] +end + +GetGuide = function() + local guides = GoodGuy.GetActorsByType("c1") + return guides[1] +end + +IsAlive = function(actor) + return not actor.IsDead +end + +IsGroupDead = function(actors) + return Utils.All(actors, function(actor) + return actor.IsDead + end) +end + +IsGuide = function(actor) + return actor.Type == "c1" +end + +IsGuideOrAllies = function(actor) + return actor.Owner == Greece or IsGuide(actor) +end + +IsHostage = function(actor) + return actor.Owner == England +end + +SpawnPlayerCamera = function(location, duration, type, delay) + duration = duration or DateTime.Seconds(6) + type = type or "camera" + return SpawnMiscActor(type, GoodGuy, location, duration, delay) +end + +SpawnFlare = function(location, duration, delay) + return SpawnMiscActor("flare", England, location, duration, delay) +end + +-- Spawn a supporting actor with optional delay and self-removal. +SpawnMiscActor = function(type, owner, location, duration, delay) + local actor = Actor.Create(type, false, { Owner = owner, Location = location or CPos.New(0, 0) }) + + if delay then + Trigger.AfterDelay(delay, function() + actor.IsInWorld = true + end) + else + actor.IsInWorld = true + end + + if duration and duration > 0 then + RemoveActor(actor, duration) + end + + return actor +end + +RemoveActor = function(actor, delay) + Trigger.AfterDelay(delay or 0, function() + if not actor or not actor.IsInWorld then + return + end + actor.Destroy() + end) +end + +OnHostageKilled = function() + if Greece.GetObjectiveType(SaveAllHostages) == "Primary" then + MarkAlliedDefeat(SaveAllHostages) + return + end + + Greece.MarkFailedObjective(SaveAllHostages) + CheckEscortObjective() +end + +OnAllHostagesKilled = function() + Media.PlaySoundNotification(Greece, "AlertBleep") + Media.DisplayMessage(UserInterface.Translate("all-hostages-killed")) + MarkAlliedDefeat(EscortTheHostages or FreeTheHostages) +end + +OnHostagesEscorted = function() + EscortFinished = true + Media.PlaySpeechNotification(Greece, "ObjectiveMet") + Greece.MarkCompletedObjective(EscortTheHostages) + + local delay = DateTime.Seconds(3) + if DestroySoviets then + delay = 5 + if not Greece.IsObjectiveFailed(SaveAllHostages) then + Greece.MarkCompletedObjective(SaveAllHostages) + end + end + + Trigger.AfterDelay(delay, function() + DestroySoviets = DestroySoviets or AddPrimaryObjective(Greece, "destroy-soviet-units-infrastructure") + SignalForReinforcements = AddSecondaryObjective(Greece, "signal-for-reinforcements") + end) +end + +OnChurchKilled = function() + Media.PlaySoundNotification(Greece, "AlertBleep") + Media.DisplayMessage(UserInterface.Translate("church-destroyed")) + MarkAlliedDefeat(EscortTheHostages, DateTime.Seconds(2)) +end + +OnForwardCommandKilled = function() + if not BadGuyReinforced then + PrepareTechSale() + end + + StartFireSale(USSR) + + local forwardBaseGuards = { ForwardRifle1, ForwardRifle2, ForwardGrenadier, ForwardCliffGuard1, ForwardCliffGuard2, TechTank } + GroupIdleHunt(forwardBaseGuards) +end + +OnGuideRiflesKilled = function() + if not GuideHouse.IsDead then + SpawnGuide() + end +end + +OnTanyaKilled = function(_, killer) + CheckAlliedDestruction() + + if killer.Type == "v2rl" and IsAlive(killer) then + SpawnPlayerCamera(killer.Location, DateTime.Minutes(1), "camera.tiny") + end + + if not EscortFinished then + local liveHostages = Utils.Where(Hostages, IsAlive) + Utils.Do(liveHostages, function(hostage) + hostage.Panic() + end) + end +end diff --git a/mods/ra/maps/negotiations/notifications.yaml b/mods/ra/maps/negotiations/notifications.yaml new file mode 100644 index 0000000000..cc87730c8c --- /dev/null +++ b/mods/ra/maps/negotiations/notifications.yaml @@ -0,0 +1,6 @@ +Sounds: + Notifications: + HostageShot: gun27 + DogWhine: dogw5 + GuideOkay: guyokay1 + Chronoshift: chrono2 diff --git a/mods/ra/maps/negotiations/rules.yaml b/mods/ra/maps/negotiations/rules.yaml new file mode 100644 index 0000000000..44878b4251 --- /dev/null +++ b/mods/ra/maps/negotiations/rules.yaml @@ -0,0 +1,196 @@ +World: + LuaScript: + Scripts: campaign.lua, utils.lua, negotiations.lua, negotiations-ai.lua, negotiations-reinforcements.lua + MissionData: + Briefing: A Soviet force has holed up in a small town, threatening to kill a hostage every few minutes until their demands are met. We do not negotiate with terrorists -- explain this to them.\n\nLocate the hostages, free as many of them as you can, and get them safely to a nearby abandoned church. Once done, return to your drop-off point, signal for reinforcements, and finish off the Soviet forces. + WinVideo: allymorf.vqa + LossVideo: battle.vqa + TimeLimitManager: + SkipTimerExpiredNotification: true + StartGameNotification: + Notification: TimerStarted + ScriptLobbyDropdown@difficulty: + ID: difficulty + Label: dropdown-difficulty.label + Description: dropdown-difficulty.description + Values: + easy: options-difficulty.easy + normal: options-difficulty.normal + hard: options-difficulty.hard + Default: normal + +Player: + ExternalCondition@BadGuy: + Condition: ai-enabled + HarvesterBotModule: + RequiresCondition: ai-enabled + BuildingRepairBotModule: + RequiresCondition: ai-enabled + MissionObjectives: + Cooperative: true + +ai.hard: + ProvidesPrerequisite: + Prerequisite: ai.hard + Interactable: + AlwaysVisible: + +paradrop.allies: + Inherits: powerproxy.paratroopers + ParatroopersPower: + DropItems: e1,e1,e3,e3,e3,e3,e1,e1,e1,medi + SquadSize: 2 + DisplayBeacon: false + +powerproxy.chronoshift: + AlwaysVisible: + ChronoshiftPower: + Dimensions: 1, 1 + Footprint: x + +CAMERA.tiny: + Inherits: CAMERA + RevealsShroud: + Range: 2c0 + +CAMERA.small: + Inherits: CAMERA + RevealsShroud: + Range: 4c0 + +CTNK: + Chronoshiftable: + ChronoshiftSound: + +V2RL: + AutoTarget: + # A mobile V2 can be annoying to chase, or too much with the dog attack. + InitialStanceAI: Defend + +# This type will mimic the wide guard range sometimes given to RA '96 dogs by +# Area Guard orders. An extra dog on Hard difficulty will also use this. +DOG.areaguard: + Inherits: DOG + RenderSprites: + Image: dog + AutoTarget: + ScanRadius: 9 + RevealsShroud: + Range: 9c0 + ScriptTags: + +# Keep SAMs online if the nearby truck blows; original SAMs require no power. +# This does not affect the MCV-built base. +APWR: + PowerMultiplier: + Modifier: 200 + +MISS: + Tooltip: + Name: actor-prison-name + +STEK: + ProvidesPrerequisite@TeslaTanks: + Prerequisite: vehicles.russia + ProvidesPrerequisite@ShockTroopers: + Prerequisite: infantry.russia + Power: + Amount: 0 + SpawnActorsOnSell: + # Swap out the normal Technician that resembles one of the civilians. + # Swap Engineers as well because they're useless for hunting. + ActorTypes: e1,tecn2,tecn2,c8,c9 + +V08: + # Keep close to original death time of ~10s (at normal 5/7). + DamageMultiplier: + Modifier: 250 + +C1: + ExternalCondition@untargetable: + Condition: untargetable + Targetable: + RequiresCondition: !untargetable + Infiltrates: + Types: SpyInfiltrate + ValidRelationships: Ally, Neutral + -Wanders: + +V01: + InfiltrateForPowerOutage: + Types: SpyInfiltrate + Targetable@Guide: + TargetTypes: SpyInfiltrate + Cargo: + MaxWeight: 5 + Types: c2,c3,c4,c5,c6 + LoadedCondition: loaded + RevealsShroud: + RequiresCondition: !loaded + Range: 4c0 + Type: CenterPosition + ValidRelationships: Ally, Neutral + +^Hostage: + # Not hostile to Soviets but will "guard" Tanya as she returns to the church. + Inherits@Armed: ^ArmedCivilian + Guard: + TargetLineColor: 00000000 + RevealsShroud@Imprisoned: + Range: 1c512 + ValidRelationships: Neutral + +C2: + Inherits@Hostage: ^Hostage + +C3: + Inherits@Hostage: ^Hostage + +C4: + Inherits@Hostage: ^Hostage + +C5: + Inherits@Hostage: ^Hostage + +C6: + Inherits@Hostage: ^Hostage + +C8: + Inherits@Armed: ^ArmedCivilian + +C9: + Inherits@Armed: ^ArmedCivilian + +GNRL: + SpeedMultiplier: + Modifier: 150 + AutoTarget: + InitialStanceAI: Defend + +BADR.tanya: + Inherits: BADR + RenderSprites: + Image: badr + ParaDrop: + # Avoid dropping Tanya short of her target waypoint (and any triggers). + DropRange: 0c512 + +HELI: + # Make it less obvious that the Guide House rifles do nothing until revealed. + RevealsShroudMultiplier: + Modifier: 70 + +TRAN.north: + Inherits: TRAN + RenderSprites: + Image: tran + Cargo: + InitialUnits: e2,e2,e3,shok,shok + +TRAN.south: + Inherits: TRAN + RenderSprites: + Image: tran + Cargo: + InitialUnits: e1,e1,e4,e2,e2 + AfterUnloadDelay: 0 diff --git a/mods/ra/maps/negotiations/weapons.yaml b/mods/ra/maps/negotiations/weapons.yaml new file mode 100644 index 0000000000..b3a686ca32 --- /dev/null +++ b/mods/ra/maps/negotiations/weapons.yaml @@ -0,0 +1,12 @@ +# Like original, avoid hitting targets besides the southeast flame turret. +HellfireAG: + ValidTargets: Structure + +# The default explosion can kill the prison barrels, freeing all hostages. +# The original spread seems closer to half the default. +# Clipped the last two damage warheads to avoid this. +MiniNuke: + Warhead@7Dam_areanuke2: SpreadDamage + Spread: 2c512 + Warhead@10Dam_areanuke3: SpreadDamage + Spread: 2c512 diff --git a/mods/ra/missions.yaml b/mods/ra/missions.yaml index 9ac6020100..22d0ae332f 100644 --- a/mods/ra/missions.yaml +++ b/mods/ra/missions.yaml @@ -50,6 +50,7 @@ Aftermath Allied Missions: in-the-nick-of-time production-disruption monster-tank-madness + negotiations Aftermath Soviet Missions: shock-therapy situation-critical