Merge pull request #6284 from obrakmann/gdi01-touchup
More Lua API enhancements
This commit is contained in:
@@ -25,10 +25,10 @@ namespace OpenRA.Scripting
|
||||
{
|
||||
this.actor = actor;
|
||||
|
||||
var args = new [] { actor };
|
||||
var args = new object[] { context, actor };
|
||||
var objects = context.ActorCommands[actor.Info].Select(cg =>
|
||||
{
|
||||
var groupCtor = cg.GetConstructor(new Type[] { typeof(Actor) });
|
||||
var groupCtor = cg.GetConstructor(new Type[] { typeof(ScriptContext), typeof(Actor) });
|
||||
return groupCtor.Invoke(args);
|
||||
});
|
||||
|
||||
|
||||
@@ -44,13 +44,23 @@ namespace OpenRA.Scripting
|
||||
public abstract class ScriptActorProperties
|
||||
{
|
||||
protected readonly Actor self;
|
||||
public ScriptActorProperties(Actor self) { this.self = self; }
|
||||
protected readonly ScriptContext context;
|
||||
public ScriptActorProperties(ScriptContext context, Actor self)
|
||||
{
|
||||
this.self = self;
|
||||
this.context = context;
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class ScriptPlayerProperties
|
||||
{
|
||||
protected readonly Player player;
|
||||
public ScriptPlayerProperties(Player player) { this.player = player; }
|
||||
protected readonly ScriptContext context;
|
||||
public ScriptPlayerProperties(ScriptContext context, Player player)
|
||||
{
|
||||
this.player = player;
|
||||
this.context = context;
|
||||
}
|
||||
}
|
||||
|
||||
// For global-level bindings
|
||||
|
||||
@@ -25,10 +25,10 @@ namespace OpenRA.Scripting
|
||||
{
|
||||
this.player = player;
|
||||
|
||||
var args = new [] { player };
|
||||
var args = new object[] { context, player };
|
||||
var objects = context.PlayerCommands.Select(cg =>
|
||||
{
|
||||
var groupCtor = cg.GetConstructor(new Type[] { typeof(Player) });
|
||||
var groupCtor = cg.GetConstructor(new Type[] { typeof(ScriptContext), typeof(Player) });
|
||||
return groupCtor.Invoke(args);
|
||||
});
|
||||
|
||||
|
||||
@@ -52,12 +52,6 @@ namespace OpenRA.Mods.RA
|
||||
mo.MarkCompleted(self.Owner, objectiveID);
|
||||
}
|
||||
|
||||
public void ResolveOrder(Actor self, Order order)
|
||||
{
|
||||
if (order.OrderString == "Surrender")
|
||||
mo.MarkFailed(self.Owner, objectiveID);
|
||||
}
|
||||
|
||||
public void OnPlayerLost(Player player)
|
||||
{
|
||||
Game.Debug("{0} is defeated.".F(player.PlayerName));
|
||||
|
||||
@@ -566,6 +566,7 @@
|
||||
<Compile Include="Warheads\HealthPercentageDamageWarhead.cs" />
|
||||
<Compile Include="Warheads\PerCellDamageWarhead.cs" />
|
||||
<Compile Include="Warheads\SpreadDamageWarhead.cs" />
|
||||
<Compile Include="Scripting\Global\ReinforcementsGlobal.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\OpenRA.Game\OpenRA.Game.csproj">
|
||||
@@ -611,4 +612,4 @@ copy "FuzzyLogicLibrary.dll" "$(SolutionDir)"
|
||||
cd "$(SolutionDir)"</PostBuildEvent>
|
||||
</PropertyGroup>
|
||||
<ItemGroup />
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
@@ -79,8 +79,8 @@ namespace OpenRA.Mods.RA
|
||||
objectives.Insert(newID, new MissionObjective(type, description));
|
||||
|
||||
ObjectiveAdded(player);
|
||||
foreach (var imo in player.PlayerActor.TraitsImplementing<INotifyObjectivesUpdated>())
|
||||
imo.OnObjectiveAdded(player, newID);
|
||||
foreach (var inou in player.PlayerActor.TraitsImplementing<INotifyObjectivesUpdated>())
|
||||
inou.OnObjectiveAdded(player, newID);
|
||||
|
||||
return newID;
|
||||
}
|
||||
@@ -90,22 +90,23 @@ namespace OpenRA.Mods.RA
|
||||
if (objectiveID >= objectives.Count || objectives[objectiveID].State == ObjectiveState.Completed)
|
||||
return;
|
||||
|
||||
var inous = player.PlayerActor.TraitsImplementing<INotifyObjectivesUpdated>();
|
||||
|
||||
objectives[objectiveID].State = ObjectiveState.Completed;
|
||||
foreach (var inou in inous)
|
||||
inou.OnObjectiveCompleted(player, objectiveID);
|
||||
|
||||
if (objectives[objectiveID].Type == ObjectiveType.Primary)
|
||||
{
|
||||
var playerWon = objectives.Where(o => o.Type == ObjectiveType.Primary).All(o => o.State == ObjectiveState.Completed);
|
||||
|
||||
foreach (var imo in player.PlayerActor.TraitsImplementing<INotifyObjectivesUpdated>())
|
||||
{
|
||||
imo.OnObjectiveCompleted(player, objectiveID);
|
||||
|
||||
if (playerWon)
|
||||
imo.OnPlayerWon(player);
|
||||
}
|
||||
|
||||
if (playerWon)
|
||||
{
|
||||
foreach (var inou in inous)
|
||||
inou.OnPlayerWon(player);
|
||||
|
||||
CheckIfGameIsOver(player);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,89 +115,118 @@ namespace OpenRA.Mods.RA
|
||||
if (objectiveID >= objectives.Count || objectives[objectiveID].State == ObjectiveState.Failed)
|
||||
return;
|
||||
|
||||
var inous = player.PlayerActor.TraitsImplementing<INotifyObjectivesUpdated>();
|
||||
|
||||
objectives[objectiveID].State = ObjectiveState.Failed;
|
||||
foreach (var inou in inous)
|
||||
inou.OnObjectiveFailed(player, objectiveID);
|
||||
|
||||
if (objectives[objectiveID].Type == ObjectiveType.Primary)
|
||||
{
|
||||
var playerLost = objectives.Where(o => o.Type == ObjectiveType.Primary).Any(o => o.State == ObjectiveState.Failed);
|
||||
|
||||
foreach (var imo in player.PlayerActor.TraitsImplementing<INotifyObjectivesUpdated>())
|
||||
{
|
||||
imo.OnObjectiveFailed(player, objectiveID);
|
||||
|
||||
if (playerLost)
|
||||
imo.OnPlayerLost(player);
|
||||
}
|
||||
|
||||
if (playerLost)
|
||||
{
|
||||
foreach (var inou in inous)
|
||||
inou.OnPlayerLost(player);
|
||||
|
||||
CheckIfGameIsOver(player);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CheckIfGameIsOver(Player player)
|
||||
{
|
||||
var players = player.World.Players.Where(p => !p.NonCombatant);
|
||||
var allies = players.Where(p => p.IsAlliedWith(player));
|
||||
|
||||
var gameOver = ((info.EarlyGameOver && !info.Cooperative && player.WinState != WinState.Undefined) ||
|
||||
(info.EarlyGameOver && info.Cooperative && allies.All(p => p.WinState != WinState.Undefined)) ||
|
||||
players.All(p => p.WinState != WinState.Undefined));
|
||||
|
||||
var gameOver = players.All(p => p.WinState != WinState.Undefined);
|
||||
if (gameOver)
|
||||
{
|
||||
Game.RunAfterDelay(info.GameOverDelay, () =>
|
||||
{
|
||||
player.World.EndGame();
|
||||
player.World.SetPauseState(true);
|
||||
player.World.PauseStateLocked = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void OnPlayerWon(Player player)
|
||||
{
|
||||
var players = player.World.Players.Where(p => !p.NonCombatant);
|
||||
var enemies = players.Where(p => !p.IsAlliedWith(player));
|
||||
|
||||
if (info.Cooperative)
|
||||
{
|
||||
WinStateCooperative = WinState.Won;
|
||||
var players = player.World.Players.Where(p => !p.NonCombatant);
|
||||
var allies = players.Where(p => p.IsAlliedWith(player));
|
||||
|
||||
if (allies.All(p => p.PlayerActor.Trait<MissionObjectives>().WinStateCooperative == WinState.Won))
|
||||
{
|
||||
foreach (var p in allies)
|
||||
{
|
||||
p.WinState = WinState.Won;
|
||||
p.World.OnPlayerWinStateChanged(p);
|
||||
}
|
||||
|
||||
if (info.EarlyGameOver)
|
||||
foreach (var p in enemies)
|
||||
p.PlayerActor.Trait<MissionObjectives>().ForceDefeat(p);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
player.WinState = WinState.Won;
|
||||
player.World.OnPlayerWinStateChanged(player);
|
||||
|
||||
if (info.EarlyGameOver)
|
||||
foreach (var p in enemies)
|
||||
p.PlayerActor.Trait<MissionObjectives>().ForceDefeat(p);
|
||||
}
|
||||
}
|
||||
|
||||
public void OnPlayerLost(Player player)
|
||||
{
|
||||
var players = player.World.Players.Where(p => !p.NonCombatant);
|
||||
var enemies = players.Where(p => !p.IsAlliedWith(player));
|
||||
|
||||
if (info.Cooperative)
|
||||
{
|
||||
WinStateCooperative = WinState.Lost;
|
||||
var players = player.World.Players.Where(p => !p.NonCombatant);
|
||||
var allies = players.Where(p => p.IsAlliedWith(player));
|
||||
|
||||
if (allies.Any(p => p.PlayerActor.Trait<MissionObjectives>().WinStateCooperative == WinState.Lost))
|
||||
{
|
||||
foreach (var p in allies)
|
||||
{
|
||||
p.WinState = WinState.Lost;
|
||||
p.World.OnPlayerWinStateChanged(p);
|
||||
}
|
||||
|
||||
if (info.EarlyGameOver)
|
||||
foreach (var p in enemies)
|
||||
p.PlayerActor.Trait<MissionObjectives>().ForceDefeat(p);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
player.WinState = WinState.Lost;
|
||||
player.World.OnPlayerWinStateChanged(player);
|
||||
|
||||
if (info.EarlyGameOver)
|
||||
foreach (var p in enemies)
|
||||
{
|
||||
p.WinState = WinState.Won;
|
||||
p.World.OnPlayerWinStateChanged(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void ForceDefeat(Player player)
|
||||
{
|
||||
for (var id = 0; id < Objectives.Count; id++)
|
||||
if (Objectives[id].State == ObjectiveState.Incomplete)
|
||||
MarkFailed(player, id);
|
||||
}
|
||||
|
||||
public event Action<Player> ObjectiveAdded = player => { };
|
||||
|
||||
public void OnObjectiveAdded(Player player, int id) {}
|
||||
@@ -206,8 +236,7 @@ namespace OpenRA.Mods.RA
|
||||
public void ResolveOrder(Actor self, Order order)
|
||||
{
|
||||
if (order.OrderString == "Surrender")
|
||||
for (var id = 0; id < objectives.Count; id++)
|
||||
MarkFailed(self.Owner, id);
|
||||
ForceDefeat(self.Owner);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -65,5 +65,8 @@ namespace OpenRA.Scripting
|
||||
|
||||
[Desc("Create a new WRange.")]
|
||||
public WRange New(int r) { return new WRange(r); }
|
||||
|
||||
[Desc("Create a new WRange by cell distance")]
|
||||
public WRange FromCells(int numCells) { return WRange.FromCells(numCells); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Eluant;
|
||||
using OpenRA.Scripting;
|
||||
@@ -43,6 +44,34 @@ namespace OpenRA.Mods.RA.Scripting
|
||||
return actors.ToLuaTable(context);
|
||||
}
|
||||
|
||||
[Desc("Returns a table of all actors within the requested rectangle, filtered using the specified function.")]
|
||||
public LuaTable ActorsInBox(WPos topLeft, WPos bottomRight, LuaFunction filter = null)
|
||||
{
|
||||
var actors = context.World.ActorMap.ActorsInBox(topLeft, bottomRight)
|
||||
.Select(a => a.ToLuaValue(context));
|
||||
|
||||
if (filter != null)
|
||||
actors = actors.Where(a =>
|
||||
{
|
||||
using (var f = filter.Call(a))
|
||||
return f.First().ToBoolean();
|
||||
});
|
||||
|
||||
return actors.ToLuaTable(context);
|
||||
}
|
||||
|
||||
[Desc("Returns the location of the top-left corner of the map.")]
|
||||
public WPos TopLeft
|
||||
{
|
||||
get { return new WPos(context.World.Map.Bounds.Left * 1024, context.World.Map.Bounds.Top * 1024, 0); }
|
||||
}
|
||||
|
||||
[Desc("Returns the location of the bottom-right corner of the map.")]
|
||||
public WPos BottomRight
|
||||
{
|
||||
get { return new WPos(context.World.Map.Bounds.Right * 1024, context.World.Map.Bounds.Bottom * 1024, 0); }
|
||||
}
|
||||
|
||||
[Desc("Returns a random cell inside the visible region of the map.")]
|
||||
public CPos RandomCell()
|
||||
{
|
||||
|
||||
220
OpenRA.Mods.RA/Scripting/Global/ReinforcementsGlobal.cs
Normal file
220
OpenRA.Mods.RA/Scripting/Global/ReinforcementsGlobal.cs
Normal file
@@ -0,0 +1,220 @@
|
||||
#region Copyright & License Information
|
||||
/*
|
||||
* Copyright 2007-2014 The OpenRA Developers (see AUTHORS)
|
||||
* 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. For more information,
|
||||
* see COPYING.
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using Eluant;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenRA;
|
||||
using OpenRA.Primitives;
|
||||
using OpenRA.Traits;
|
||||
using OpenRA.Scripting;
|
||||
using OpenRA.Effects;
|
||||
using OpenRA.Mods.RA.Activities;
|
||||
using OpenRA.Mods.RA.Air;
|
||||
|
||||
namespace OpenRA.Mods.RA.Scripting
|
||||
{
|
||||
[ScriptGlobal("Reinforcements")]
|
||||
public class ReinforcementsGlobal : ScriptGlobal
|
||||
{
|
||||
public ReinforcementsGlobal(ScriptContext context) : base(context) { }
|
||||
|
||||
Actor CreateActor(Player owner, string actorType, bool addToWorld, CPos? entryLocation = null, CPos? nextLocation = null)
|
||||
{
|
||||
ActorInfo ai;
|
||||
if (!context.World.Map.Rules.Actors.TryGetValue(actorType, out ai))
|
||||
throw new LuaException("Unknown actor type '{0}'".F(actorType));
|
||||
|
||||
var initDict = new TypeDictionary();
|
||||
|
||||
initDict.Add(new OwnerInit(owner));
|
||||
|
||||
if (entryLocation.HasValue)
|
||||
{
|
||||
var pi = ai.Traits.GetOrDefault<AircraftInfo>();
|
||||
initDict.Add(new CenterPositionInit(owner.World.Map.CenterOfCell(entryLocation.Value) + new WVec(0, 0, pi != null ? pi.CruiseAltitude.Range : 0)));
|
||||
initDict.Add(new LocationInit(entryLocation.Value));
|
||||
}
|
||||
|
||||
if (entryLocation.HasValue && nextLocation.HasValue)
|
||||
initDict.Add(new FacingInit(context.World.Map.FacingBetween(CPos.Zero, CPos.Zero + (nextLocation.Value - entryLocation.Value), 0)));
|
||||
|
||||
var actor = context.World.CreateActor(addToWorld, actorType, initDict);
|
||||
|
||||
return actor;
|
||||
}
|
||||
|
||||
void Move(Actor actor, CPos dest)
|
||||
{
|
||||
if (actor.HasTrait<Aircraft>())
|
||||
{
|
||||
if (actor.HasTrait<Helicopter>())
|
||||
actor.QueueActivity(new HeliFly(actor, Target.FromCell(actor.World, dest)));
|
||||
else
|
||||
actor.QueueActivity(new Fly(actor, Target.FromCell(actor.World, dest)));
|
||||
}
|
||||
else
|
||||
{
|
||||
actor.QueueActivity(new Move.Move(dest, 2));
|
||||
}
|
||||
}
|
||||
|
||||
[Desc("Send reinforcements consisting of multiple units. Supports ground-based, naval and air units. " +
|
||||
"The first member of the 'entryPath' array will be the units' spawnpoint, " +
|
||||
"while the last one will be their destination. If 'actionFunc' is given, " +
|
||||
"it will be executed once a unit has reached its destination. 'actionFunc' " +
|
||||
"will be called as 'actionFunc(Actor actor)'")]
|
||||
public LuaTable Reinforce(Player owner, LuaTable actorTypes, LuaTable entryPath, int interval = 25, LuaFunction actionFunc = null)
|
||||
{
|
||||
var actors = new List<Actor>();
|
||||
for (var i = 1; i <= actorTypes.Count; i++)
|
||||
{
|
||||
string actorType;
|
||||
if (!(actorTypes[i].TryGetClrValue<String>(out actorType)))
|
||||
throw new LuaException("Invalid data in actorTypes array");
|
||||
|
||||
CPos entry, next = new CPos();
|
||||
if (!(entryPath[1].TryGetClrValue<CPos>(out entry)
|
||||
&& (entryPath.Count < 2 || entryPath[2].TryGetClrValue<CPos>(out next))))
|
||||
throw new LuaException("Invalid data in entryPath array");
|
||||
|
||||
var actor = CreateActor(owner, actorType, false, entry, entryPath.Count > 1 ? next : (CPos?)null);
|
||||
actors.Add(actor);
|
||||
|
||||
var ep = entryPath.CopyReference() as LuaTable;
|
||||
var af = actionFunc != null ? actionFunc.CopyReference() as LuaFunction : null;
|
||||
|
||||
var actionDelay = (i - 1) * interval;
|
||||
Action actorAction = () =>
|
||||
{
|
||||
context.World.Add(actor);
|
||||
for (var j = 2; j <= ep.Count; j++)
|
||||
{
|
||||
CPos wpt;
|
||||
if (!(ep[j].TryGetClrValue<CPos>(out wpt)))
|
||||
throw new LuaException("Invalid data in entryPath array");
|
||||
|
||||
Move(actor, wpt);
|
||||
}
|
||||
ep.Dispose();
|
||||
|
||||
if (af != null)
|
||||
actor.QueueActivity(new CallFunc(() =>
|
||||
{
|
||||
af.Call(actor.ToLuaValue(context));
|
||||
af.Dispose();
|
||||
}));
|
||||
};
|
||||
|
||||
context.World.AddFrameEndTask(w => w.Add(new DelayedAction(actionDelay, actorAction)));
|
||||
}
|
||||
return actors.Select(a => a.ToLuaValue(context)).ToLuaTable(context);
|
||||
}
|
||||
|
||||
[Desc("Send reinforcements in a transport. A transport can be a ground unit (APC etc.), ships and aircraft. " +
|
||||
"The first member of the 'entryPath' array will be the spawnpoint for the transport, " +
|
||||
"while the last one will be its destination. The last member of the 'exitPath' array " +
|
||||
"is be the place where the transport will be removed from the game. When the transport " +
|
||||
"has reached the destination, it will unload its cargo unless a custom 'actionFunc' has " +
|
||||
"been supplied. Afterwards, the transport will follow the 'exitPath' and leave the map, " +
|
||||
"unless a custom 'exitFunc' has been supplied. 'actionFunc' will be called as " +
|
||||
"'actionFunc(Actor transport, Actor[] cargo). 'exitFunc' will be called as 'exitFunc(Actor transport)'.")]
|
||||
public LuaTable ReinforceWithTransport(Player owner, string actorType, LuaTable cargoTypes, LuaTable entryPath, LuaTable exitPath = null,
|
||||
LuaFunction actionFunc = null, LuaFunction exitFunc = null)
|
||||
{
|
||||
CPos entry, next = new CPos();
|
||||
if (!(entryPath[1].TryGetClrValue<CPos>(out entry)
|
||||
&& (entryPath.Count < 2 || entryPath[2].TryGetClrValue<CPos>(out next))))
|
||||
throw new LuaException("Invalid data in entryPath array");
|
||||
|
||||
var transport = CreateActor(owner, actorType, true, entry, entryPath.Count > 1 ? next : (CPos?)null);
|
||||
var cargo = transport.TraitOrDefault<Cargo>();
|
||||
|
||||
var passengers = context.CreateTable();
|
||||
|
||||
if (cargo != null && cargoTypes != null)
|
||||
{
|
||||
for (var i = 1; i <= cargoTypes.Count; i++)
|
||||
{
|
||||
string cargoType;
|
||||
if (!(cargoTypes [i].TryGetClrValue<String>(out cargoType)))
|
||||
throw new LuaException("Invalid data in cargoTypes array");
|
||||
|
||||
var passenger = CreateActor(owner, cargoType, false);
|
||||
passengers.Add(passengers.Count + 1, passenger.ToLuaValue(context));
|
||||
cargo.Load(transport, passenger);
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 2; i <= entryPath.Count; i++)
|
||||
{
|
||||
CPos wpt;
|
||||
if (!(entryPath[i].TryGetClrValue<CPos>(out wpt)))
|
||||
throw new LuaException("Invalid data in entryPath array");
|
||||
|
||||
Move(transport, wpt);
|
||||
}
|
||||
|
||||
if (actionFunc != null)
|
||||
{
|
||||
var af = actionFunc.CopyReference() as LuaFunction;
|
||||
transport.QueueActivity(new CallFunc(() =>
|
||||
{
|
||||
af.Call(transport.ToLuaValue(context), passengers);
|
||||
af.Dispose();
|
||||
}));
|
||||
}
|
||||
else
|
||||
{
|
||||
var heli = transport.TraitOrDefault<Helicopter>();
|
||||
if (heli != null)
|
||||
{
|
||||
transport.QueueActivity(new Turn(heli.Info.InitialFacing));
|
||||
transport.QueueActivity(new HeliLand(true));
|
||||
transport.QueueActivity(new Wait(15));
|
||||
}
|
||||
if (cargo != null)
|
||||
{
|
||||
transport.QueueActivity(new UnloadCargo(transport, true));
|
||||
transport.QueueActivity(new WaitFor(() => cargo.IsEmpty(transport)));
|
||||
}
|
||||
transport.QueueActivity(new Wait(heli != null ? 50 : 25));
|
||||
}
|
||||
|
||||
if (exitFunc != null)
|
||||
{
|
||||
var ef = exitFunc.CopyReference() as LuaFunction;
|
||||
transport.QueueActivity(new CallFunc(() =>
|
||||
{
|
||||
ef.Call(transport.ToLuaValue(context));
|
||||
ef.Dispose();
|
||||
}));
|
||||
}
|
||||
else if (exitPath != null)
|
||||
{
|
||||
for (var i = 1; i <= exitPath.Count; i++)
|
||||
{
|
||||
CPos wpt;
|
||||
if (!(exitPath[i].TryGetClrValue<CPos>(out wpt)))
|
||||
throw new LuaException("Invalid data in exitPath array.");
|
||||
|
||||
Move(transport, wpt);
|
||||
}
|
||||
transport.QueueActivity(new RemoveSelf());
|
||||
}
|
||||
|
||||
var ret = context.CreateTable();
|
||||
ret.Add(1, transport.ToLuaValue(context));
|
||||
ret.Add(2, passengers);
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -144,6 +144,27 @@ namespace OpenRA.Mods.RA.Scripting
|
||||
GetScriptTriggers(player.PlayerActor).RegisterCallback(Trigger.OnObjectiveFailed, func, context);
|
||||
}
|
||||
|
||||
[Desc("Call a function when this actor is added to the world. " +
|
||||
"The callback function will be called as func(Actor self).")]
|
||||
public void OnAddedToWorld(Actor a, LuaFunction func)
|
||||
{
|
||||
GetScriptTriggers(a).RegisterCallback(Trigger.OnAddedToWorld, func, context);
|
||||
}
|
||||
|
||||
[Desc("Call a function when this actor is removed from the world. " +
|
||||
"The callback function will be called as func(Actor self).")]
|
||||
public void OnRemovedFromWorld(Actor a, LuaFunction func)
|
||||
{
|
||||
GetScriptTriggers(a).RegisterCallback(Trigger.OnRemovedFromWorld, func, context);
|
||||
}
|
||||
|
||||
[Desc("Call a function when this actor is captured. The callback function " +
|
||||
"will be called as func(Actor self, Actor captor, Player oldOwner, Player newOwner).")]
|
||||
public void OnCapture(Actor a, LuaFunction func)
|
||||
{
|
||||
GetScriptTriggers(a).RegisterCallback(Trigger.OnCapture, func, context);
|
||||
}
|
||||
|
||||
[Desc("Removes all triggers from this actor")]
|
||||
public void ClearAll(Actor a)
|
||||
{
|
||||
|
||||
@@ -58,6 +58,17 @@ namespace OpenRA.Mods.RA.Scripting
|
||||
return true;
|
||||
}
|
||||
|
||||
[Desc("Skips over the first numElements members of the array and returns the rest")]
|
||||
public LuaTable Skip(LuaTable table, int numElements)
|
||||
{
|
||||
var t = context.CreateTable();
|
||||
|
||||
for (var i = numElements; i <= table.Count; i++)
|
||||
t.Add(t.Count + 1, table[i]);
|
||||
|
||||
return t;
|
||||
}
|
||||
|
||||
[Desc("Returns a random value from table.")]
|
||||
public LuaValue Random(LuaTable table)
|
||||
{
|
||||
@@ -94,5 +105,17 @@ namespace OpenRA.Mods.RA.Scripting
|
||||
{
|
||||
return context.World.Map.CenterOfCell(cell);
|
||||
}
|
||||
|
||||
[Desc("Converts the number of seconds into game time (ticks).")]
|
||||
public int Seconds(int seconds)
|
||||
{
|
||||
return seconds * 25;
|
||||
}
|
||||
|
||||
[Desc("Converts the number of minutes into game time (ticks).")]
|
||||
public int Minutes(int minutes)
|
||||
{
|
||||
return Seconds(minutes * 60);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,8 +17,8 @@ namespace OpenRA.Mods.RA.Scripting
|
||||
[ScriptPropertyGroup("Support Powers")]
|
||||
public class ChronsphereProperties : ScriptActorProperties, Requires<ChronoshiftPowerInfo>
|
||||
{
|
||||
public ChronsphereProperties(Actor self)
|
||||
: base(self) { }
|
||||
public ChronsphereProperties(ScriptContext context, Actor self)
|
||||
: base(context, self) { }
|
||||
|
||||
[Desc("Chronoshift a group of actors. A duration of 0 will teleport the actors permanently.")]
|
||||
public void Chronoshift(LuaTable unitLocationPairs, int duration = 0, bool killCargo = false)
|
||||
|
||||
@@ -17,7 +17,8 @@ namespace OpenRA.Mods.RA.Scripting
|
||||
[ScriptPropertyGroup("Combat")]
|
||||
public class CombatProperties : ScriptActorProperties, Requires<AttackBaseInfo>, Requires<IMoveInfo>
|
||||
{
|
||||
public CombatProperties(Actor self) : base(self) { }
|
||||
public CombatProperties(ScriptContext context, Actor self)
|
||||
: base(context, self) { }
|
||||
|
||||
[ScriptActorPropertyActivity]
|
||||
[Desc("Seek out and attack nearby targets.")]
|
||||
@@ -35,4 +36,4 @@ namespace OpenRA.Mods.RA.Scripting
|
||||
self.QueueActivity(new AttackMove.AttackMoveActivity(self, new Move.Move(cell, WRange.FromCells(closeEnough))));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,8 @@ namespace OpenRA.Mods.RA.Scripting
|
||||
readonly IFacing facing;
|
||||
readonly AutoTarget autotarget;
|
||||
|
||||
public GeneralProperties(Actor self) : base(self)
|
||||
public GeneralProperties(ScriptContext context, Actor self)
|
||||
: base(context, self)
|
||||
{
|
||||
facing = self.TraitOrDefault<IFacing>();
|
||||
autotarget = self.TraitOrDefault<AutoTarget>();
|
||||
@@ -135,4 +136,4 @@ namespace OpenRA.Mods.RA.Scripting
|
||||
return self.HasScriptProperty(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,8 +17,8 @@ namespace OpenRA.Mods.RA.Scripting
|
||||
public class GuardProperties : ScriptActorProperties, Requires<GuardInfo>, Requires<IMoveInfo>
|
||||
{
|
||||
Guard guard;
|
||||
public GuardProperties(Actor self)
|
||||
: base(self)
|
||||
public GuardProperties(ScriptContext context, Actor self)
|
||||
: base(context, self)
|
||||
{
|
||||
guard = self.Trait<Guard>();
|
||||
}
|
||||
|
||||
@@ -17,8 +17,8 @@ namespace OpenRA.Mods.RA.Scripting
|
||||
public class HealthProperties : ScriptActorProperties, Requires<HealthInfo>
|
||||
{
|
||||
Health health;
|
||||
public HealthProperties(Actor self)
|
||||
: base(self)
|
||||
public HealthProperties(ScriptContext context, Actor self)
|
||||
: base(context, self)
|
||||
{
|
||||
health = self.Trait<Health>();
|
||||
}
|
||||
@@ -41,8 +41,8 @@ namespace OpenRA.Mods.RA.Scripting
|
||||
public class InvulnerableProperties : ScriptActorProperties, Requires<ScriptInvulnerableInfo>
|
||||
{
|
||||
ScriptInvulnerable invulnerable;
|
||||
public InvulnerableProperties(Actor self)
|
||||
: base(self)
|
||||
public InvulnerableProperties(ScriptContext context, Actor self)
|
||||
: base(context, self)
|
||||
{
|
||||
invulnerable = self.Trait<ScriptInvulnerable>();
|
||||
}
|
||||
|
||||
@@ -20,8 +20,8 @@ namespace OpenRA.Mods.RA.Scripting
|
||||
{
|
||||
readonly MissionObjectives mo;
|
||||
|
||||
public MissionObjectiveProperties(Player player)
|
||||
: base(player)
|
||||
public MissionObjectiveProperties(ScriptContext context, Player player)
|
||||
: base(context, player)
|
||||
{
|
||||
mo = player.PlayerActor.Trait<MissionObjectives>();
|
||||
}
|
||||
|
||||
@@ -17,7 +17,8 @@ namespace OpenRA.Mods.RA.Scripting
|
||||
[ScriptPropertyGroup("Movement")]
|
||||
public class MobileProperties : ScriptActorProperties, Requires<MobileInfo>
|
||||
{
|
||||
public MobileProperties(Actor self) : base(self) { }
|
||||
public MobileProperties(ScriptContext context, Actor self)
|
||||
: base(context, self) { }
|
||||
|
||||
[ScriptActorPropertyActivity]
|
||||
[Desc("Moves within the cell grid. closeEnough defines an optional range " +
|
||||
|
||||
@@ -9,22 +9,28 @@
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Eluant;
|
||||
using OpenRA.Scripting;
|
||||
using OpenRA.Mods.RA.Move;
|
||||
|
||||
namespace OpenRA.Mods.RA.Scripting
|
||||
{
|
||||
[ScriptPropertyGroup("Player")]
|
||||
public class PlayerProperties : ScriptPlayerProperties
|
||||
{
|
||||
readonly Player p;
|
||||
|
||||
public PlayerProperties(Player player)
|
||||
: base(player)
|
||||
{
|
||||
p = player;
|
||||
}
|
||||
public PlayerProperties(ScriptContext context, Player player)
|
||||
: base(context, player) { }
|
||||
|
||||
[Desc("The player's name.")]
|
||||
public string PlayerName { get { return p.PlayerName; } }
|
||||
public string Name { get { return player.PlayerName; } }
|
||||
|
||||
[Desc("Returns an array of actors representing all ground attack units of this player.")]
|
||||
public LuaTable GetGroundAttackers()
|
||||
{
|
||||
return player.World.ActorsWithTrait<AttackBase>().Select(a => a.Actor)
|
||||
.Where(a => a.Owner == player && !a.IsDead() && a.IsInWorld && a.HasTrait<Mobile>())
|
||||
.Select(a => a.ToLuaValue(context)).ToLuaTable(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,8 +20,8 @@ namespace OpenRA.Mods.RA.Scripting
|
||||
{
|
||||
readonly Production p;
|
||||
|
||||
public ProductionProperties(Actor self)
|
||||
: base(self)
|
||||
public ProductionProperties(ScriptContext context, Actor self)
|
||||
: base(context, self)
|
||||
{
|
||||
p = self.Trait<Production>();
|
||||
}
|
||||
|
||||
@@ -19,8 +19,8 @@ namespace OpenRA.Mods.RA.Scripting
|
||||
{
|
||||
readonly PlayerResources pr;
|
||||
|
||||
public ResourceProperties(Player player)
|
||||
: base(player)
|
||||
public ResourceProperties(ScriptContext context, Player player)
|
||||
: base(context, player)
|
||||
{
|
||||
pr = player.PlayerActor.Trait<PlayerResources>();
|
||||
}
|
||||
|
||||
@@ -21,8 +21,8 @@ namespace OpenRA.Mods.RA.Scripting
|
||||
{
|
||||
readonly Cargo cargo;
|
||||
|
||||
public TransportProperties(Actor self)
|
||||
: base(self)
|
||||
public TransportProperties(ScriptContext context, Actor self)
|
||||
: base(context, self)
|
||||
{
|
||||
cargo = self.Trait<Cargo>();
|
||||
}
|
||||
@@ -46,8 +46,8 @@ namespace OpenRA.Mods.RA.Scripting
|
||||
{
|
||||
readonly ParaDrop paradrop;
|
||||
|
||||
public ParadropPowers(Actor self)
|
||||
: base(self)
|
||||
public ParadropPowers(ScriptContext context, Actor self)
|
||||
: base(context, self)
|
||||
{
|
||||
paradrop = self.Trait<ParaDrop>();
|
||||
}
|
||||
|
||||
@@ -18,12 +18,13 @@ using OpenRA.Traits;
|
||||
|
||||
namespace OpenRA.Mods.RA.Scripting
|
||||
{
|
||||
public enum Trigger { OnIdle, OnDamaged, OnKilled, OnProduction, OnPlayerWon, OnPlayerLost, OnObjectiveAdded, OnObjectiveCompleted, OnObjectiveFailed };
|
||||
public enum Trigger { OnIdle, OnDamaged, OnKilled, OnProduction, OnPlayerWon, OnPlayerLost, OnObjectiveAdded,
|
||||
OnObjectiveCompleted, OnObjectiveFailed, OnCapture, OnAddedToWorld, OnRemovedFromWorld };
|
||||
|
||||
[Desc("Allows map scripts to attach triggers to this actor via the Triggers global.")]
|
||||
public class ScriptTriggersInfo : TraitInfo<ScriptTriggers> { }
|
||||
|
||||
public sealed class ScriptTriggers : INotifyIdle, INotifyDamage, INotifyKilled, INotifyProduction, INotifyObjectivesUpdated, IDisposable
|
||||
public sealed class ScriptTriggers : INotifyIdle, INotifyDamage, INotifyKilled, INotifyProduction, INotifyObjectivesUpdated, INotifyCapture, INotifyAddedToWorld, INotifyRemovedFromWorld, IDisposable
|
||||
{
|
||||
public event Action<Actor> OnKilledInternal = _ => {};
|
||||
|
||||
@@ -43,36 +44,25 @@ namespace OpenRA.Mods.RA.Scripting
|
||||
public void TickIdle(Actor self)
|
||||
{
|
||||
foreach (var f in Triggers[Trigger.OnIdle])
|
||||
{
|
||||
var a = self.ToLuaValue(f.Second);
|
||||
f.First.Call(a).Dispose();
|
||||
a.Dispose();
|
||||
}
|
||||
using (var a = self.ToLuaValue(f.Second))
|
||||
f.First.Call(a).Dispose();
|
||||
}
|
||||
|
||||
public void Damaged(Actor self, AttackInfo e)
|
||||
{
|
||||
foreach (var f in Triggers[Trigger.OnDamaged])
|
||||
{
|
||||
var a = self.ToLuaValue(f.Second);
|
||||
var b = e.Attacker.ToLuaValue(f.Second);
|
||||
f.First.Call(a, b).Dispose();
|
||||
a.Dispose();
|
||||
b.Dispose();
|
||||
}
|
||||
using (var a = self.ToLuaValue(f.Second))
|
||||
using (var b = e.Attacker.ToLuaValue(f.Second))
|
||||
f.First.Call(a, b).Dispose();
|
||||
}
|
||||
|
||||
public void Killed(Actor self, AttackInfo e)
|
||||
{
|
||||
// Run lua callbacks
|
||||
foreach (var f in Triggers[Trigger.OnKilled])
|
||||
{
|
||||
var a = self.ToLuaValue(f.Second);
|
||||
var b = e.Attacker.ToLuaValue(f.Second);
|
||||
f.First.Call(a, b).Dispose();
|
||||
a.Dispose();
|
||||
b.Dispose();
|
||||
}
|
||||
using (var a = self.ToLuaValue(f.Second))
|
||||
using (var b = e.Attacker.ToLuaValue(f.Second))
|
||||
f.First.Call(a, b).Dispose();
|
||||
|
||||
// Run any internally bound callbacks
|
||||
OnKilledInternal(self);
|
||||
@@ -81,69 +71,71 @@ namespace OpenRA.Mods.RA.Scripting
|
||||
public void UnitProduced(Actor self, Actor other, CPos exit)
|
||||
{
|
||||
foreach (var f in Triggers[Trigger.OnProduction])
|
||||
{
|
||||
var a = self.ToLuaValue(f.Second);
|
||||
var b = other.ToLuaValue(f.Second);
|
||||
f.First.Call(a, b).Dispose();
|
||||
a.Dispose();
|
||||
b.Dispose();
|
||||
}
|
||||
using (var a = self.ToLuaValue(f.Second))
|
||||
using (var b = other.ToLuaValue(f.Second))
|
||||
f.First.Call(a, b).Dispose();
|
||||
}
|
||||
|
||||
public void OnPlayerWon(Player player)
|
||||
{
|
||||
foreach (var f in Triggers[Trigger.OnPlayerWon])
|
||||
{
|
||||
var a = player.ToLuaValue(f.Second);
|
||||
f.First.Call(a).Dispose();
|
||||
a.Dispose();
|
||||
}
|
||||
using (var a = player.ToLuaValue(f.Second))
|
||||
f.First.Call(a).Dispose();
|
||||
}
|
||||
|
||||
public void OnPlayerLost(Player player)
|
||||
{
|
||||
foreach (var f in Triggers[Trigger.OnPlayerLost])
|
||||
{
|
||||
var a = player.ToLuaValue(f.Second);
|
||||
f.First.Call(a).Dispose();
|
||||
a.Dispose();
|
||||
}
|
||||
using (var a = player.ToLuaValue(f.Second))
|
||||
f.First.Call(a).Dispose();
|
||||
}
|
||||
|
||||
public void OnObjectiveAdded(Player player, int id)
|
||||
{
|
||||
foreach (var f in Triggers[Trigger.OnObjectiveAdded])
|
||||
{
|
||||
var a = player.ToLuaValue(f.Second);
|
||||
var b = id.ToLuaValue(f.Second);
|
||||
f.First.Call(a, b).Dispose();
|
||||
a.Dispose();
|
||||
b.Dispose();
|
||||
}
|
||||
using (var a = player.ToLuaValue(f.Second))
|
||||
using (var b = id.ToLuaValue(f.Second))
|
||||
f.First.Call(a, b).Dispose();
|
||||
}
|
||||
|
||||
public void OnObjectiveCompleted(Player player, int id)
|
||||
{
|
||||
foreach (var f in Triggers[Trigger.OnObjectiveCompleted])
|
||||
{
|
||||
var a = player.ToLuaValue(f.Second);
|
||||
var b = id.ToLuaValue(f.Second);
|
||||
f.First.Call(a, b).Dispose();
|
||||
a.Dispose();
|
||||
b.Dispose();
|
||||
}
|
||||
using (var a = player.ToLuaValue(f.Second))
|
||||
using (var b = id.ToLuaValue(f.Second))
|
||||
f.First.Call(a, b).Dispose();
|
||||
}
|
||||
|
||||
public void OnObjectiveFailed(Player player, int id)
|
||||
{
|
||||
foreach (var f in Triggers[Trigger.OnObjectiveFailed])
|
||||
{
|
||||
var a = player.ToLuaValue(f.Second);
|
||||
var b = id.ToLuaValue(f.Second);
|
||||
f.First.Call(a, b).Dispose();
|
||||
a.Dispose();
|
||||
b.Dispose();
|
||||
}
|
||||
using (var a = player.ToLuaValue(f.Second))
|
||||
using (var b = id.ToLuaValue(f.Second))
|
||||
f.First.Call(a, b).Dispose();
|
||||
}
|
||||
|
||||
public void OnCapture(Actor self, Actor captor, Player oldOwner, Player newOwner)
|
||||
{
|
||||
foreach (var f in Triggers[Trigger.OnCapture])
|
||||
using (var a = self.ToLuaValue(f.Second))
|
||||
using (var b = captor.ToLuaValue(f.Second))
|
||||
using (var c = oldOwner.ToLuaValue(f.Second))
|
||||
using (var d = newOwner.ToLuaValue(f.Second))
|
||||
f.First.Call(a, b, c, d).Dispose();
|
||||
}
|
||||
|
||||
public void AddedToWorld(Actor self)
|
||||
{
|
||||
foreach (var f in Triggers[Trigger.OnAddedToWorld])
|
||||
using (var a = self.ToLuaValue(f.Second))
|
||||
f.First.Call(a).Dispose();
|
||||
}
|
||||
|
||||
public void RemovedFromWorld(Actor self)
|
||||
{
|
||||
foreach (var f in Triggers[Trigger.OnRemovedFromWorld])
|
||||
using (var a = self.ToLuaValue(f.Second))
|
||||
f.First.Call(a).Dispose();
|
||||
}
|
||||
|
||||
public void Clear(Trigger trigger)
|
||||
|
||||
@@ -3,9 +3,7 @@ VehicleReinforcements = { "jeep" }
|
||||
NodPatrol = { "e1", "e1" }
|
||||
|
||||
SendNodPatrol = function()
|
||||
Utils.Do(NodPatrol, function(type)
|
||||
local soldier = Actor.Create(type, true, { Location = nod0.Location, Owner = enemy })
|
||||
soldier.Move(nod1.Location)
|
||||
Reinforcements.Reinforce(enemy, NodPatrol, { nod0.Location, nod1.Location }, 15, function(soldier)
|
||||
soldier.AttackMove(nod2.Location)
|
||||
soldier.Move(nod3.Location)
|
||||
soldier.Hunt()
|
||||
@@ -17,21 +15,27 @@ SetGunboatPath = function(gunboat)
|
||||
gunboat.AttackMove(gunboatRight.Location)
|
||||
end
|
||||
|
||||
ReinforceFromSea = function(passengers)
|
||||
local transport = Actor.Create("oldlst", true, { Location = lstStart.Location, Owner = player })
|
||||
Reinforce = function(units)
|
||||
Media.PlaySpeechNotification(player, "Reinforce")
|
||||
Reinforcements.ReinforceWithTransport(player, "oldlst", units, { lstStart.Location, lstEnd.Location }, { lstStart.Location })
|
||||
end
|
||||
|
||||
Utils.Do(passengers, function(actorType)
|
||||
local passenger = Actor.Create(actorType, false, { Owner = player })
|
||||
transport.LoadPassenger(passenger)
|
||||
triggerAdded = false
|
||||
CheckForBase = function()
|
||||
baseBuildings = Map.ActorsInBox(Map.TopLeft, Map.BottomRight, function(actor)
|
||||
return actor.Type == "fact" or actor.Type == "pyle" or actor.Type == "nuke"
|
||||
end)
|
||||
|
||||
transport.Move(lstEnd.Location)
|
||||
transport.UnloadPassengers()
|
||||
transport.Wait(50)
|
||||
transport.Move(lstStart.Location)
|
||||
transport.Destroy()
|
||||
Utils.Do(baseBuildings, function(building)
|
||||
if not triggerAdded and building.Type == "fact" then
|
||||
Trigger.OnRemovedFromWorld(building, function()
|
||||
player.MarkFailedObjective(gdiObjective2)
|
||||
end)
|
||||
triggerAdded = true
|
||||
end
|
||||
end)
|
||||
|
||||
Media.PlaySpeechNotification(player, "Reinforce")
|
||||
return #baseBuildings >= 3
|
||||
end
|
||||
|
||||
WorldLoaded = function()
|
||||
@@ -40,7 +44,12 @@ WorldLoaded = function()
|
||||
player = Player.GetPlayer("GDI")
|
||||
enemy = Player.GetPlayer("Nod")
|
||||
|
||||
gdiObjective = player.AddPrimaryObjective("Destroy all Nod forces in the area!")
|
||||
nodObjective = enemy.AddPrimaryObjective("Destroy all GDI troops")
|
||||
gdiObjective1 = player.AddPrimaryObjective("Eliminate all Nod forces in the area")
|
||||
gdiObjective2 = player.AddSecondaryObjective("Establish a beachhead")
|
||||
|
||||
Trigger.OnObjectiveCompleted(player, function() Media.DisplayMessage("Objective completed") end)
|
||||
Trigger.OnObjectiveFailed(player, function() Media.DisplayMessage("Objective failed") end)
|
||||
|
||||
Trigger.OnPlayerWon(player, function()
|
||||
Media.PlaySpeechNotification(player, "Win")
|
||||
@@ -60,18 +69,26 @@ WorldLoaded = function()
|
||||
|
||||
SendNodPatrol()
|
||||
|
||||
Trigger.AfterDelay(25 * 5, function() ReinforceFromSea(InfantryReinforcements) end)
|
||||
Trigger.AfterDelay(25 * 15, function() ReinforceFromSea(InfantryReinforcements) end)
|
||||
Trigger.AfterDelay(25 * 30, function() ReinforceFromSea(VehicleReinforcements) end)
|
||||
Trigger.AfterDelay(25 * 60, function() ReinforceFromSea(VehicleReinforcements) end)
|
||||
Trigger.AfterDelay(Utils.Seconds(5), function() Reinforce(InfantryReinforcements) end)
|
||||
Trigger.AfterDelay(Utils.Seconds(15), function() Reinforce(InfantryReinforcements) end)
|
||||
Trigger.AfterDelay(Utils.Seconds(30), function() Reinforce(VehicleReinforcements) end)
|
||||
Trigger.AfterDelay(Utils.Seconds(60), function() Reinforce(VehicleReinforcements) end)
|
||||
end
|
||||
|
||||
tick = 0
|
||||
baseEstablished = false
|
||||
Tick = function()
|
||||
tick = tick + 1
|
||||
if enemy.HasNoRequiredUnits() then
|
||||
player.MarkCompletedObjective(gdiObjective)
|
||||
player.MarkCompletedObjective(gdiObjective1)
|
||||
end
|
||||
|
||||
if player.HasNoRequiredUnits() then
|
||||
player.MarkFailedObjective(gdiObjective)
|
||||
enemy.MarkCompletedObjective(nodObjective)
|
||||
end
|
||||
|
||||
if not baseEstablished and tick % Utils.Seconds(1) == 0 and CheckForBase() then
|
||||
baseEstablished = true
|
||||
player.MarkCompletedObjective(gdiObjective2)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -467,6 +467,8 @@ Rules:
|
||||
EarlyGameOver: true
|
||||
^Infantry:
|
||||
MustBeDestroyed:
|
||||
^Vehicle:
|
||||
MustBeDestroyed:
|
||||
PROC:
|
||||
Buildable:
|
||||
Prerequisites: ~disabled
|
||||
|
||||
Reference in New Issue
Block a user