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>
This commit is contained in:
BIN
mods/cnc/maps/the-tiberium-strain/map.bin
Normal file
BIN
mods/cnc/maps/the-tiberium-strain/map.bin
Normal file
Binary file not shown.
2
mods/cnc/maps/the-tiberium-strain/map.ftl
Normal file
2
mods/cnc/maps/the-tiberium-strain/map.ftl
Normal file
@@ -0,0 +1,2 @@
|
||||
## rules.yaml
|
||||
briefing = A nearby GDI base is conducting further ion research. In doing so, they use a large number of chemicals, located in their bio centers. Destroy all the bio centers, and "contaminate" all units and civilians. Leave all the other structures intact, so that it looks like an accident.
|
||||
BIN
mods/cnc/maps/the-tiberium-strain/map.png
Normal file
BIN
mods/cnc/maps/the-tiberium-strain/map.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.8 KiB |
1330
mods/cnc/maps/the-tiberium-strain/map.yaml
Normal file
1330
mods/cnc/maps/the-tiberium-strain/map.yaml
Normal file
File diff suppressed because it is too large
Load Diff
89
mods/cnc/maps/the-tiberium-strain/rules.yaml
Normal file
89
mods/cnc/maps/the-tiberium-strain/rules.yaml
Normal file
@@ -0,0 +1,89 @@
|
||||
World:
|
||||
LuaScript:
|
||||
Scripts: campaign.lua, utils.lua, the-tiberium-strain.lua
|
||||
MissionData:
|
||||
Briefing: briefing
|
||||
StartVideo: nodsweep.vqa
|
||||
LossVideo: nodflees.vqa
|
||||
WinVideo: desolat.vqa
|
||||
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
|
||||
Locked: false
|
||||
|
||||
Player:
|
||||
EnemyWatcher:
|
||||
PlayerResources:
|
||||
DefaultCash: 0
|
||||
|
||||
^Bridge:
|
||||
DamageMultiplier@INVULNERABLE:
|
||||
Modifier: 0
|
||||
|
||||
^CivBuilding:
|
||||
Targetable:
|
||||
RequiresCondition: !force-fire-required
|
||||
Targetable@FORCENEEDED:
|
||||
TargetTypes: Ground, Structure
|
||||
RequiresForceFire: true
|
||||
RequiresCondition: force-fire-required
|
||||
GrantConditionOnPrerequisite:
|
||||
Condition: force-fire-required
|
||||
Prerequisites: village-targetable-switch
|
||||
|
||||
# Spawned on Easy difficulty.
|
||||
VillageTargetableSwitch:
|
||||
ProvidesPrerequisite:
|
||||
Prerequisite: village-targetable-switch
|
||||
|
||||
^CivInfantry:
|
||||
AnnounceOnSeen:
|
||||
MustBeDestroyed:
|
||||
ScriptTags:
|
||||
|
||||
E1:
|
||||
ScriptTags:
|
||||
|
||||
E2:
|
||||
ScriptTags:
|
||||
|
||||
FTNK:
|
||||
GrantConditionOnTerrain@SKIPBRIDGEHUSK:
|
||||
Condition: skip-bridge-husk
|
||||
TerrainTypes: Bridge
|
||||
SpawnActorOnDeath:
|
||||
RequiresCondition: !skip-bridge-husk
|
||||
|
||||
JEEP:
|
||||
GrantConditionOnTerrain@SKIPBRIDGEHUSK:
|
||||
Condition: skip-bridge-husk
|
||||
TerrainTypes: Bridge
|
||||
SpawnActorOnDeath:
|
||||
RequiresCondition: !skip-bridge-husk
|
||||
ScriptTags:
|
||||
|
||||
MTNK:
|
||||
GrantConditionOnTerrain@SKIPBRIDGEHUSK:
|
||||
Condition: skip-bridge-husk
|
||||
TerrainTypes: Bridge
|
||||
SpawnActorOnDeath:
|
||||
RequiresCondition: !skip-bridge-husk
|
||||
ScriptTags:
|
||||
|
||||
BIO:
|
||||
# The original BIO is fragile and Nod may take heavy losses on the path here.
|
||||
Health:
|
||||
HP: 70000
|
||||
Armor:
|
||||
Type: Light
|
||||
|
||||
BIO.Husk:
|
||||
RevealsShroud:
|
||||
Range: 2c0
|
||||
ValidRelationships: Enemy
|
||||
735
mods/cnc/maps/the-tiberium-strain/the-tiberium-strain.lua
Normal file
735
mods/cnc/maps/the-tiberium-strain/the-tiberium-strain.lua
Normal file
@@ -0,0 +1,735 @@
|
||||
--[[
|
||||
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 Nod = Player.GetPlayer("Nod")
|
||||
|
||||
--- Owner of the defenders.
|
||||
local GDI = Player.GetPlayer("GDI")
|
||||
|
||||
--- Owner of the homes and villagers.
|
||||
local Villagers = Player.GetPlayer("Villagers")
|
||||
|
||||
---@type integer Objective to destroy GDI's bio centers.
|
||||
local BioObjective
|
||||
|
||||
---@type integer Objective to destroy all villagers.
|
||||
local VillagerObjective
|
||||
|
||||
---@type integer Final objective to keep all non-BIO structures intact.
|
||||
local IntactObjective
|
||||
|
||||
--- Tally of most GDI deaths. Extra units on Hard do not apply.
|
||||
local GDIDeathCount = 0
|
||||
|
||||
--- Number of counted GDI deaths that will trigger a map-wide hunt.
|
||||
local HuntThreshold = 65
|
||||
|
||||
--- Has Nod's starting force arrived?
|
||||
local NodArrived = false
|
||||
|
||||
--- Are Nod's village sweepers about to reinforce?
|
||||
local SweepersRequested = false
|
||||
|
||||
--- Have civilians spawned from the south village road?
|
||||
local RunnersArrived = false
|
||||
|
||||
--- Return one of the entry waypoints by the east ore field.
|
||||
---@return actor waypoint
|
||||
local function RandomEastEntry()
|
||||
return Map.NamedActor("EastEntry" .. Utils.RandomInteger(1, 5))
|
||||
end
|
||||
|
||||
--- Is this actor a Nod attacker?
|
||||
---@param actor actor
|
||||
---@return boolean
|
||||
local function IsMobileNod(actor)
|
||||
return not actor.IsDead and actor.HasProperty("Move") and actor.Owner == Nod
|
||||
end
|
||||
|
||||
--- Is this a resident of the main village?
|
||||
local function IsWestVillager(actor)
|
||||
return actor.HasTag("West Villager")
|
||||
end
|
||||
|
||||
--- Is this actor alive and tagged to guard a wide area?
|
||||
---@param actor actor
|
||||
---@return boolean
|
||||
local function IsLiveAreaGuard(actor)
|
||||
return not actor.IsDead and actor.HasTag("Area Guard")
|
||||
end
|
||||
|
||||
--- Is this actor either dead or idle?
|
||||
---@param actor actor
|
||||
---@return boolean
|
||||
local function IsDeadOrIdle(actor)
|
||||
return actor.IsDead or actor.IsIdle
|
||||
end
|
||||
|
||||
--- Is Nod wiped out with no incoming sweepers?
|
||||
--- @return boolean
|
||||
local function IsNodDead()
|
||||
return NodArrived and Nod.HasNoRequiredUnits() and not SweepersRequested
|
||||
end
|
||||
|
||||
--- Is this actor free to come hunt base attackers?
|
||||
---@param actor actor
|
||||
---@return boolean
|
||||
local function CanDefendBase(actor)
|
||||
return not actor.IsDead and actor.IsIdle and not actor.HasTag("Area Guard")
|
||||
end
|
||||
|
||||
--- Is GDI weak enough that Nod sweepers can be reinforced?
|
||||
---@return boolean
|
||||
local function AreSweepersReady()
|
||||
local defenders = Utils.Where(GDI.GetGroundAttackers(), function(a)
|
||||
-- Disregard the slow Mammoth Tank, which can be avoided if necessary.
|
||||
return a.Type ~= "htnk"
|
||||
end)
|
||||
|
||||
return #defenders == 0
|
||||
end
|
||||
|
||||
--- Reveal the defenses by GDI's west gate.
|
||||
local function RevealWestBaseEntrance()
|
||||
local camera = Actor.Create("camera.small", true, { Owner = Nod, Location = WestGateReveal.Location })
|
||||
Trigger.AfterDelay(DateTime.Seconds(6), camera.Destroy)
|
||||
end
|
||||
|
||||
--- Mark defeat for Nod's dead task force.
|
||||
local function MarkNodDead()
|
||||
local objs = { BioObjective, VillagerObjective }
|
||||
|
||||
Utils.Do(objs, function(obj)
|
||||
if Nod.IsObjectiveCompleted(obj) then
|
||||
return
|
||||
end
|
||||
|
||||
Nod.MarkFailedObjective(obj)
|
||||
end)
|
||||
end
|
||||
|
||||
--- Add the villager slaying objective if it's not already present.
|
||||
--- This is done more than once as a safety check, partly because the
|
||||
--- visibility cheat will bypass the usual trigger for this.
|
||||
---@param announced? boolean
|
||||
local function AddVillagerObjective(announced)
|
||||
if not announced or IsNodDead() then
|
||||
VillagerObjective = VillagerObjective or AddPrimaryObjective(Nod, "kill-every-villager-in-area")
|
||||
return
|
||||
end
|
||||
|
||||
local speaker = UserInterface.GetFluentMessage("nod-soldier")
|
||||
Media.DisplayMessage(UserInterface.GetFluentMessage("careless-gdi-experiments-killed"), speaker, Nod.Color)
|
||||
|
||||
Trigger.AfterDelay(DateTime.Seconds(4), function()
|
||||
if IsNodDead() then
|
||||
return
|
||||
end
|
||||
|
||||
Media.DisplayMessage(UserInterface.GetFluentMessage("villagers-must-not-live"), speaker, Nod.Color)
|
||||
end)
|
||||
|
||||
Trigger.AfterDelay(DateTime.Seconds(8), AddVillagerObjective)
|
||||
end
|
||||
|
||||
--- Check the villager objective after each has been slain.
|
||||
local function CheckVillagerObjective()
|
||||
-- Delay so HasNoRequiredUnits accounts for deaths during THIS tick.
|
||||
Trigger.AfterDelay(1, function()
|
||||
if RunnersArrived and Villagers.HasNoRequiredUnits() then
|
||||
AddVillagerObjective()
|
||||
Nod.MarkCompletedObjective(VillagerObjective)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
--- Spawn some extra Nod units for Easy difficulty.
|
||||
local function ReinforceEasyNod()
|
||||
if IsNodDead() then
|
||||
return
|
||||
end
|
||||
|
||||
local rallies = { ChemRally1, ChemRally2, FlameRally2 }
|
||||
Media.PlaySpeechNotification(Nod, "Reinforce")
|
||||
|
||||
Utils.Do(rallies, function(rally)
|
||||
local path = { NorthWestEntry2.Location, rally.Location }
|
||||
Reinforcements.Reinforce(Nod, { "e5" }, path)
|
||||
end)
|
||||
end
|
||||
|
||||
--- Prepare to count an actor's death toward GDI's hunt threshold.
|
||||
---@param actor actor
|
||||
local function TallyDeath(actor)
|
||||
Trigger.OnKilled(actor, function()
|
||||
GDIDeathCount = GDIDeathCount + 1
|
||||
|
||||
if GDIDeathCount == HuntThreshold then
|
||||
-- The expected start -> village -> bridges -> SE corner -> base
|
||||
-- path should yield 56-63 GDI deaths once the south gate guards
|
||||
-- are cleared. 20 are from the village (incl. helicopter cargo).
|
||||
Utils.Do(GDI.GetGroundAttackers(), function(attacker)
|
||||
-- Stop any patrolling or area guarding.
|
||||
attacker.Stop()
|
||||
attacker.RemoveTag("Area Guard")
|
||||
attacker.Stance = "AttackAnything"
|
||||
IdleHunt(attacker)
|
||||
end)
|
||||
end
|
||||
|
||||
if Difficulty == "easy" and GDIDeathCount % 15 == 0 then
|
||||
ReinforceEasyNod()
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
--- Spawn and assemble Nod's starting force.
|
||||
local function ReinforceFirstNod()
|
||||
Media.PlaySpeechNotification(Nod, "Reinforce")
|
||||
local chemTypes = { "e5", "e5", "e5", "e5" }
|
||||
local flameTypes = { "ftnk" }
|
||||
|
||||
for i = 1, 3 do
|
||||
local path = { Map.NamedActor("NorthWestEntry" .. i).Location, Map.NamedActor("FlameRally" .. i).Location }
|
||||
Reinforcements.Reinforce(Nod, flameTypes, path)
|
||||
|
||||
if i ~= 3 then
|
||||
Trigger.AfterDelay(20, function()
|
||||
local chemPath = { NorthWestEntry2.Location, Map.NamedActor("ChemRally" .. i).Location }
|
||||
Reinforcements.Reinforce(Nod, chemTypes, chemPath, 10)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
Trigger.AfterDelay(DateTime.Seconds(3), function()
|
||||
NodArrived = true
|
||||
end)
|
||||
end
|
||||
|
||||
--- Spawn another Flame Tank for Nod after the bridge tank's death.
|
||||
local function ReinforceNodTank()
|
||||
if IsNodDead() then
|
||||
return
|
||||
end
|
||||
|
||||
Media.PlaySpeechNotification(Nod, "Reinforce")
|
||||
Reinforcements.Reinforce(Nod, { "ftnk" }, { NorthWestEntry2.Location, Waypoint1.Location })
|
||||
end
|
||||
|
||||
--- Reinforce a Flame Tank as a reward for the intersection tank's death.
|
||||
--- This originally spawns in the starting area, with no runners to chase.
|
||||
local function ReinforceVillageFlameTank()
|
||||
if IsNodDead() then
|
||||
return
|
||||
end
|
||||
|
||||
local runnerTypes = { "c3", "c9", "c7" }
|
||||
local runners = Reinforcements.Reinforce(Villagers, runnerTypes, { SouthVillageEntry.Location }, 5, function(runner)
|
||||
Trigger.OnIdle(runner, function()
|
||||
if runner.Location == Waypoint3.Location then
|
||||
Trigger.ClearAll(runner)
|
||||
end
|
||||
|
||||
runner.Move(Waypoint3.Location)
|
||||
end)
|
||||
end)
|
||||
|
||||
RunnersArrived = true
|
||||
Trigger.OnAllKilled(runners, CheckVillagerObjective)
|
||||
local camera = Actor.Create("camera.small", true, { Owner = Nod, Location = SouthVillageEntry.Location })
|
||||
|
||||
Trigger.AfterDelay(DateTime.Seconds(1), function()
|
||||
local path = { SouthVillageEntry.Location, SouthVillageEntry.Location + CVec.New(0, -2) }
|
||||
Reinforcements.Reinforce(Nod, { "ftnk" }, path)
|
||||
Media.PlaySpeechNotification(Nod, "Reinforce")
|
||||
camera.Destroy()
|
||||
end)
|
||||
end
|
||||
|
||||
--- Reinforce Nod to resolve loose ends in the main village. This will speed
|
||||
--- things along if Nod is close to victory but on the opposite end of the map.
|
||||
local function ScheduleVillageSweepers()
|
||||
if #Utils.Where(Villagers.GetActors(), IsWestVillager) == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
if not AreSweepersReady() then
|
||||
Trigger.AfterDelay(10, ScheduleVillageSweepers)
|
||||
return
|
||||
end
|
||||
|
||||
SweepersRequested = true
|
||||
|
||||
Trigger.AfterDelay(DateTime.Seconds(3), function()
|
||||
Media.PlaySpeechNotification(Nod, "Reinforce")
|
||||
local path = { WestVillageEntry.Location, Intersection.Location }
|
||||
local types = { "ftnk", "e5", "e5", "e5", "e5", "e5" }
|
||||
Reinforcements.Reinforce(Nod, types, path, 10)
|
||||
end)
|
||||
|
||||
Trigger.AfterDelay(DateTime.Seconds(4), function()
|
||||
SweepersRequested = false
|
||||
end)
|
||||
end
|
||||
|
||||
--- Airlift an infantry team to avenge GDI units near the intersection.
|
||||
local function ReinforceVillageHelicopter()
|
||||
local path = { RandomEastEntry().Location, Waypoint0.Location }
|
||||
local passengerTypes = { "e2", "e2", "e2", "e3", "e3" }
|
||||
local passengers = Reinforcements.ReinforceWithTransport(GDI, "tran", passengerTypes, path, { path[1] })[2]
|
||||
|
||||
Utils.Do(passengers, function(passenger)
|
||||
IdleHunt(passenger)
|
||||
TallyDeath(passenger)
|
||||
end)
|
||||
end
|
||||
|
||||
--- Airlift an infantry team to hunt near the south edge's river crossing.
|
||||
local function ReinforceRiverHelicopter()
|
||||
local path = { SouthRiverEntry.Location, Waypoint2.Location }
|
||||
local exit = RandomEastEntry().Location
|
||||
local passengerTypes = { "e1", "e1", "e1", "e1", "e3" }
|
||||
local passengers = Reinforcements.ReinforceWithTransport(GDI, "tran", passengerTypes, path, { exit })[2]
|
||||
|
||||
Utils.Do(passengers, function(passenger)
|
||||
TallyDeath(passenger)
|
||||
|
||||
Trigger.OnAddedToWorld(passenger, function()
|
||||
-- IdleHunt on its own may cause some of these infantry to take a
|
||||
-- long route around the map because the narrow canyon is occupied.
|
||||
passenger.AttackMove(SouthRiverRally.Location)
|
||||
passenger.AttackMove(SouthBridgeRally.Location)
|
||||
IdleHunt(passenger)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
--- Reinforce some scouts to counterattack at the south gate.
|
||||
local function ReinforceEdgeHumvees()
|
||||
local entry = RandomEastEntry()
|
||||
local path = { entry.Location, entry.Location + CVec.New(-2, 0) }
|
||||
|
||||
Reinforcements.Reinforce(GDI, { "jeep", "jeep" }, path, 25, function(humvee)
|
||||
humvee.AttackMove(EastHumveeRally.Location, 2)
|
||||
humvee.AttackMove(SouthGate.Location, 2)
|
||||
humvee.AttackMove(Waypoint3.Location, 2)
|
||||
IdleHunt(humvee)
|
||||
TallyDeath(humvee)
|
||||
end)
|
||||
end
|
||||
|
||||
--- Order a group to intercept (or chase away) nearby intruders when idle.
|
||||
--- This is intended to feel similar to GUARD_AREA orders in TD '95.
|
||||
---@param guards actor[] List of guards.
|
||||
---@param center wpos Center of the group's search area.
|
||||
---@param range wdist Radius of the area to guard.
|
||||
local function GuardArea(guards, center, range)
|
||||
local activated = false
|
||||
local refreshed = false
|
||||
guards = Utils.Where(guards, IsLiveAreaGuard)
|
||||
|
||||
if #guards == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
Trigger.OnEnteredProximityTrigger(center, range, function(target, id)
|
||||
if activated or not IsMobileNod(target) then
|
||||
return
|
||||
end
|
||||
|
||||
activated = true
|
||||
Trigger.RemoveProximityTrigger(id)
|
||||
|
||||
Utils.Do(guards, function(guard)
|
||||
if not IsLiveAreaGuard(guard) or not guard.IsIdle then
|
||||
return
|
||||
end
|
||||
|
||||
guard.AttackMove(target.Location, 2)
|
||||
guard.AttackMove(guard.Location)
|
||||
end)
|
||||
end)
|
||||
|
||||
Utils.Do(guards, function(guard)
|
||||
guard.Stance = "Defend"
|
||||
|
||||
Trigger.OnIdle(guard, function()
|
||||
-- Does at least one guard remain after an intruder search?
|
||||
-- If so, reset the guards and prepare another search area.
|
||||
if refreshed or not activated or not Utils.All(guards, IsDeadOrIdle) then
|
||||
return
|
||||
end
|
||||
|
||||
refreshed = true
|
||||
local survivors = Utils.Where(guards, IsLiveAreaGuard)
|
||||
|
||||
Utils.Do(survivors, function(survivor)
|
||||
Trigger.Clear(survivor, "OnIdle")
|
||||
end)
|
||||
|
||||
Trigger.AfterDelay(10, function()
|
||||
GuardArea(survivors, center, range)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
--- Order a random armed villager in the west to begin hunting.
|
||||
local function SendVillagerHunter()
|
||||
local villagers = Utils.Where(Villagers.GetGroundAttackers(), IsWestVillager)
|
||||
if #villagers == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
local guard = Utils.Random(villagers)
|
||||
IdleHunt(guard)
|
||||
end
|
||||
|
||||
--- Send an available unit to defend a damaged base structure.
|
||||
---@param location cpos Location of the structure attacker.
|
||||
local function SendBaseGuard(location)
|
||||
local guards = Utils.Where(GDI.GetGroundAttackers(), CanDefendBase)
|
||||
if #guards == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
local guard = Utils.Random(guards)
|
||||
guard.AttackMove(location, 2)
|
||||
IdleHunt(guard)
|
||||
end
|
||||
|
||||
--- Prepare simple events for the pre-placed villagers.
|
||||
local function PrepareVillagers()
|
||||
Trigger.OnPlayerDiscovered(Villagers, function(_, discoverer)
|
||||
if discoverer == Nod then
|
||||
AddVillagerObjective(true)
|
||||
end
|
||||
end)
|
||||
|
||||
local mobiles = Utils.Where(Villagers.GetActors(), function(actor)
|
||||
return actor.HasProperty("Move")
|
||||
end)
|
||||
|
||||
Trigger.OnAllKilled(mobiles, CheckVillagerObjective)
|
||||
end
|
||||
|
||||
--- Place all the footprint triggers needed at startup.
|
||||
local function PrepareFootprints()
|
||||
local footprints =
|
||||
{
|
||||
{
|
||||
action = ReinforceRiverHelicopter,
|
||||
cells =
|
||||
{
|
||||
CPos.New(28, 43),
|
||||
CPos.New(28, 44),
|
||||
CPos.New(28, 45),
|
||||
CPos.New(28, 46)
|
||||
}
|
||||
},
|
||||
{
|
||||
action = ReinforceEdgeHumvees,
|
||||
cells =
|
||||
{
|
||||
CPos.New(49, 20),
|
||||
CPos.New(50, 20),
|
||||
CPos.New(51, 20),
|
||||
CPos.New(52, 20),
|
||||
CPos.New(53, 20)
|
||||
}
|
||||
},
|
||||
{
|
||||
action = RevealWestBaseEntrance,
|
||||
cells =
|
||||
{
|
||||
CPos.New(22, 11),
|
||||
CPos.New(22, 12),
|
||||
CPos.New(22, 13),
|
||||
CPos.New(22, 14),
|
||||
CPos.New(22, 15),
|
||||
CPos.New(22, 16)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Utils.Do(footprints, function(footprint)
|
||||
local activated = false
|
||||
|
||||
Trigger.OnEnteredFootprint(footprint.cells, function(actor, id)
|
||||
if activated or not IsMobileNod(actor) then
|
||||
return
|
||||
end
|
||||
|
||||
activated = true
|
||||
Trigger.RemoveFootprintTrigger(id)
|
||||
footprint.action()
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
--- Set up the flares and objective for GDI's bio centers.
|
||||
local function PrepareBioCenters()
|
||||
local centers = { Bio1, Bio2, Bio3 }
|
||||
local offset = CVec.New(1, 0)
|
||||
|
||||
Utils.Do(centers, function(center)
|
||||
Trigger.OnKilled(center, function()
|
||||
Actor.Create("flare", true, { Owner = GDI, Location = center.Location + offset })
|
||||
end)
|
||||
end)
|
||||
|
||||
Trigger.OnAllKilled(centers, function()
|
||||
AddVillagerObjective()
|
||||
ScheduleVillageSweepers()
|
||||
Nod.MarkCompletedObjective(BioObjective)
|
||||
end)
|
||||
end
|
||||
|
||||
--- Prepare GDI structures' survival objective, repairs, and calls for help.
|
||||
local function PrepareBase()
|
||||
local baseStructures = Utils.Where(GDI.GetActors(), function(actor)
|
||||
return actor.HasProperty("StartBuildingRepairs") and actor.Type ~= "bio"
|
||||
end)
|
||||
|
||||
Utils.Do(baseStructures, function(structure)
|
||||
Trigger.OnKilled(structure, function()
|
||||
Nod.MarkFailedObjective(IntactObjective)
|
||||
end)
|
||||
|
||||
RepairBuilding(GDI, structure, 0.75)
|
||||
|
||||
-- Do not guard defenses or the isolated Communications Center.
|
||||
if structure.HasProperty("Attack") or structure.Type == "hq" then
|
||||
return
|
||||
end
|
||||
|
||||
Trigger.OnDamaged(structure, function(_, attacker)
|
||||
if attacker.Owner == structure.Owner or attacker.IsDead then
|
||||
return
|
||||
end
|
||||
|
||||
SendBaseGuard(attacker.Location)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
--- Prepare certain groups to begin area guarding. Some infantry that did area
|
||||
--- guard have been skipped because their ORA range makes this feel redundant.
|
||||
local function PrepareAreaGuards()
|
||||
local teams =
|
||||
{
|
||||
{
|
||||
actors = Map.ActorsWithTag("West Gate Grenadier"),
|
||||
range = 5
|
||||
},
|
||||
{
|
||||
actors = Map.ActorsWithTag("Village Entrance Guard"),
|
||||
center = VillageEntranceGrenadier.CenterPosition,
|
||||
range = 5
|
||||
},
|
||||
{
|
||||
actors = Map.ActorsWithTag("Second House Guard"),
|
||||
center = VillageHouse2.CenterPosition,
|
||||
},
|
||||
{
|
||||
actors = { HilltopHumvee }
|
||||
},
|
||||
{
|
||||
actors = { IntersectionHumvee }
|
||||
},
|
||||
{
|
||||
actors = { CommunicationsHumvee },
|
||||
},
|
||||
{
|
||||
actors = Map.ActorsWithTag("Communications Guard"),
|
||||
center = CommunicationsGrenadier.CenterPosition,
|
||||
range = 6
|
||||
},
|
||||
{
|
||||
actors = { BioGrenadier },
|
||||
range = 5
|
||||
}
|
||||
}
|
||||
|
||||
Utils.Do(teams, function(team)
|
||||
local radius = WDist.FromCells(team.range or 7)
|
||||
local center = team.center or team.actors[1].CenterPosition
|
||||
GuardArea(team.actors, center, radius)
|
||||
end)
|
||||
end
|
||||
|
||||
--- Prepare houses' survival objective, targetable switch, and calls for help.
|
||||
local function PrepareVillageHouses()
|
||||
if Difficulty == "easy" then
|
||||
-- Require force-fire to attack houses, as butterfingers insurance.
|
||||
Actor.Create("villagetargetableswitch", true, { Owner = Villagers, Location = CPos.Zero })
|
||||
end
|
||||
|
||||
local village = Utils.Where(Villagers.GetActors(), function(actor)
|
||||
return actor.HasProperty("Health") and not actor.HasProperty("Move")
|
||||
end)
|
||||
|
||||
Utils.Do(village, function(house)
|
||||
Trigger.OnKilled(house, function()
|
||||
Nod.MarkFailedObjective(IntactObjective)
|
||||
end)
|
||||
|
||||
-- Only houses inside the main village should call for help.
|
||||
if house.Location.X >= WestGateReveal.Location.X then
|
||||
return
|
||||
end
|
||||
|
||||
local hunterCalled = false
|
||||
|
||||
Trigger.OnDamaged(house, function()
|
||||
if hunterCalled or house.IsDead then
|
||||
return
|
||||
end
|
||||
|
||||
hunterCalled = true
|
||||
SendVillagerHunter()
|
||||
Trigger.Clear(house, "OnDamaged")
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
--- Prepare the reveals and patrol for Hard's Mammoth Tank.
|
||||
---@param tank actor
|
||||
local function PrepareMammoth(tank)
|
||||
local path = { MammothPatrolPoint1.Location, MammothPatrolPoint2.Location, MammothPatrolPoint3.Location, MammothPatrolPoint4.Location }
|
||||
local encountered = false
|
||||
|
||||
Trigger.OnEnteredProximityTrigger(MammothEntry.CenterPosition, WDist.FromCells(9), function(actor, id)
|
||||
if encountered or not IsMobileNod(actor) then
|
||||
return
|
||||
end
|
||||
|
||||
encountered = true
|
||||
Trigger.RemoveProximityTrigger(id)
|
||||
tank.Patrol(path, true, DateTime.Seconds(3))
|
||||
local camera = Actor.Create("camera.small", true, { Owner = Nod, Location = MammothPatrolPoint1.Location })
|
||||
Trigger.AfterDelay(DateTime.Seconds(6), camera.Destroy)
|
||||
end)
|
||||
|
||||
Trigger.OnEnteredProximityTrigger(MammothGateReveal.CenterPosition, WDist.FromCells(4), function(actor, id)
|
||||
if actor.Type ~= "htnk" then
|
||||
return
|
||||
end
|
||||
|
||||
Trigger.RemoveProximityTrigger(id)
|
||||
local camera = Actor.Create("camera", true, { Owner = Nod, Location = MammothGateReveal.Location })
|
||||
Trigger.AfterDelay(DateTime.Seconds(6), camera.Destroy)
|
||||
end)
|
||||
end
|
||||
|
||||
--- Spawn some extra units for the light "remix" of Hard difficulty.
|
||||
local function SpawnHardUnits()
|
||||
if Difficulty ~= "hard" then
|
||||
return
|
||||
end
|
||||
|
||||
local spawns =
|
||||
{
|
||||
{
|
||||
type = "apc",
|
||||
cell = ApcEntry.Location,
|
||||
facing = Angle.SouthWest
|
||||
},
|
||||
{
|
||||
type = "htnk",
|
||||
cell = MammothEntry.Location,
|
||||
facing = Angle.West,
|
||||
action = PrepareMammoth
|
||||
},
|
||||
{
|
||||
type = "e2",
|
||||
cell = SouthGateGrenadierEntry.Location,
|
||||
facing = Angle.SouthEast,
|
||||
stance = "Defend"
|
||||
},
|
||||
{
|
||||
type = "e3",
|
||||
cell = SouthGateRocketEntry.Location,
|
||||
facing = Angle.SouthWest,
|
||||
stance = "Defend"
|
||||
},
|
||||
{
|
||||
type = "mtnk",
|
||||
cell = Waypoint0.Location,
|
||||
facing = Angle.North,
|
||||
stance = "Defend"
|
||||
},
|
||||
{
|
||||
type = "e3",
|
||||
cell = CentralFordEntry1.Location
|
||||
},
|
||||
{
|
||||
type = "e3",
|
||||
cell = CentralFordEntry2.Location
|
||||
},
|
||||
{
|
||||
type = "e3",
|
||||
cell = CentralFordEntry3.Location
|
||||
},
|
||||
{
|
||||
type = "mtnk",
|
||||
cell = SouthFordEntry.Location,
|
||||
stance = "Defend"
|
||||
}
|
||||
}
|
||||
|
||||
Utils.Do(spawns, function(spawn)
|
||||
local facing = spawn.facing or Angle.West
|
||||
local actor = Actor.Create(spawn.type, true, { Owner = GDI, Location = spawn.cell, Facing = facing })
|
||||
actor.Stance = spawn.stance or actor.Stance
|
||||
|
||||
if spawn.action then
|
||||
spawn.action(actor)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
WorldLoaded = function()
|
||||
InitObjectives(Nod)
|
||||
BioObjective = AddPrimaryObjective(Nod, "destroy-gdi-bio-centers")
|
||||
IntactObjective = AddPrimaryObjective(Nod, "keep-all-other-structures")
|
||||
|
||||
PrepareFootprints()
|
||||
PrepareBase()
|
||||
PrepareBioCenters()
|
||||
PrepareVillageHouses()
|
||||
|
||||
Utils.Do(GDI.GetGroundAttackers(), TallyDeath)
|
||||
PrepareVillagers()
|
||||
PrepareAreaGuards()
|
||||
SpawnHardUnits()
|
||||
ReinforceFirstNod()
|
||||
|
||||
GDI.Cash = 5000
|
||||
Camera.Position = FlameRally2.CenterPosition
|
||||
|
||||
Trigger.OnKilled(BridgeTank, ReinforceNodTank)
|
||||
local intersectionTeam = { IntersectionHumvee, IntersectionRifle1, IntersectionRifle2, IntersectionRifle3 }
|
||||
Trigger.OnAllKilled(intersectionTeam, ReinforceVillageHelicopter)
|
||||
|
||||
Trigger.OnKilled(IntersectionTank, function()
|
||||
Trigger.AfterDelay(DateTime.Seconds(1), ReinforceVillageFlameTank)
|
||||
end)
|
||||
end
|
||||
|
||||
Tick = function()
|
||||
if IsNodDead() then
|
||||
MarkNodDead()
|
||||
end
|
||||
|
||||
if Nod.IsObjectiveCompleted(BioObjective) and Nod.IsObjectiveCompleted(VillagerObjective) then
|
||||
Nod.MarkCompletedObjective(IntactObjective)
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user