diff --git a/OpenRa.FileFormats/IPaletteRemap.cs b/OpenRa.FileFormats/IPaletteRemap.cs
new file mode 100644
index 0000000000..321596490a
--- /dev/null
+++ b/OpenRa.FileFormats/IPaletteRemap.cs
@@ -0,0 +1,9 @@
+using System;
+using System.Drawing;
+namespace OpenRa.FileFormats
+{
+ public interface IPaletteRemap
+ {
+ Color GetRemappedColor(Color original, int index);
+ }
+}
diff --git a/OpenRa.FileFormats/OpenRa.FileFormats.csproj b/OpenRa.FileFormats/OpenRa.FileFormats.csproj
index ce057e867c..26f9983855 100644
--- a/OpenRa.FileFormats/OpenRa.FileFormats.csproj
+++ b/OpenRa.FileFormats/OpenRa.FileFormats.csproj
@@ -55,6 +55,7 @@
+
@@ -62,6 +63,7 @@
+
diff --git a/OpenRa.FileFormats/Palette.cs b/OpenRa.FileFormats/Palette.cs
index 6244913786..5016372a8a 100644
--- a/OpenRa.FileFormats/Palette.cs
+++ b/OpenRa.FileFormats/Palette.cs
@@ -31,7 +31,7 @@ namespace OpenRa.FileFormats
colors[4] = Color.FromArgb(140, 0, 0, 0);
}
- public Palette(Palette p, PaletteRemap r)
+ public Palette(Palette p, IPaletteRemap r)
{
for (int i = 0; i < 256; i++)
colors.Add(r.GetRemappedColor(p.GetColor(i), i));
diff --git a/OpenRa.FileFormats/PaletteRemap.cs b/OpenRa.FileFormats/PaletteRemap.cs
index ecd6de41cb..635c0d5653 100644
--- a/OpenRa.FileFormats/PaletteRemap.cs
+++ b/OpenRa.FileFormats/PaletteRemap.cs
@@ -4,7 +4,7 @@ using System.IO;
namespace OpenRa.FileFormats
{
- public class PaletteRemap
+ public class PaletteRemap : IPaletteRemap
{
int offset;
List remapColors = new List();
diff --git a/OpenRa.FileFormats/ShroudPaletteRemap.cs b/OpenRa.FileFormats/ShroudPaletteRemap.cs
new file mode 100644
index 0000000000..be08298412
--- /dev/null
+++ b/OpenRa.FileFormats/ShroudPaletteRemap.cs
@@ -0,0 +1,32 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Drawing;
+
+namespace OpenRa.FileFormats
+{
+ public class ShroudPaletteRemap : IPaletteRemap
+ {
+ public Color GetRemappedColor(Color original, int index)
+ {
+ // false-color version for debug
+
+ //return new[] {
+ // Color.Orange, Color.Green,
+ // Color.Blue, Color.Yellow,
+ // Color.Black,
+ // Color.Red,
+ // Color.Purple,
+ // Color.Cyan}[index % 8];
+
+ return new[] {
+ Color.Transparent, Color.Green,
+ Color.Blue, Color.Yellow,
+ Color.Black,
+ Color.FromArgb(192,0,0,0),
+ Color.FromArgb(128,0,0,0),
+ Color.FromArgb(64,0,0,0)}[index % 8];
+ }
+ }
+}
diff --git a/OpenRa.Game/Actor.cs b/OpenRa.Game/Actor.cs
index 83b1ece4c6..3d99808bbc 100755
--- a/OpenRa.Game/Actor.cs
+++ b/OpenRa.Game/Actor.cs
@@ -123,6 +123,7 @@ namespace OpenRa.Game
}
public bool IsDead { get { return Health <= 0; } }
+ public bool IsInWorld { get; set; }
public DamageState GetDamageState()
{
diff --git a/OpenRa.Game/Chat.cs b/OpenRa.Game/Chat.cs
index adbac6dcde..f95b214172 100644
--- a/OpenRa.Game/Chat.cs
+++ b/OpenRa.Game/Chat.cs
@@ -34,7 +34,7 @@ namespace OpenRa.Game
typing += c;
}
- static readonly Color[] paletteColors =
+ public static readonly Color[] paletteColors =
{
Color.FromArgb(228, 200, 112),
Color.FromArgb(56, 72, 125),
diff --git a/OpenRa.Game/Chrome.cs b/OpenRa.Game/Chrome.cs
index 12844e1aed..dfa218f8c8 100644
--- a/OpenRa.Game/Chrome.cs
+++ b/OpenRa.Game/Chrome.cs
@@ -26,7 +26,8 @@ namespace OpenRa.Game
readonly Animation repairButton;
readonly Animation sellButton;
-
+ readonly Animation pwrdownButton;
+
readonly SpriteRenderer buildPaletteRenderer;
readonly Animation cantBuild;
readonly Animation ready;
@@ -40,6 +41,8 @@ namespace OpenRa.Game
readonly int paletteColumns;
readonly int2 paletteOrigin;
+
+ const int MinRows = 4;
public Chrome(Renderer r)
{
@@ -75,6 +78,9 @@ namespace OpenRa.Game
sellButton = new Animation("sell");
sellButton.PlayRepeating("normal");
+
+ pwrdownButton = new Animation("repair");
+ pwrdownButton.PlayRepeating("normal");
blank = SheetBuilder.Add(new Size(64, 48), 16);
@@ -96,7 +102,7 @@ namespace OpenRa.Game
cantBuild = new Animation("clock");
cantBuild.PlayFetchIndex("idle", () => 0);
- digitSprites = OpenRa.Game.Graphics.Util.MakeArray(10, a => a)
+ digitSprites = Graphics.Util.MakeArray(10, a => a)
.Select(n => new Sprite(specialBin, new Rectangle(32 + 13 * n, 0, 13, 17), TextureChannel.Alpha)).ToList();
shimSprites = new[]
@@ -130,6 +136,8 @@ namespace OpenRa.Game
PerfHistory.Render(renderer, Game.worldRenderer.lineRenderer);
+ DrawMinimap();
+
chromeRenderer.DrawSprite(specialBinSprite, float2.Zero, PaletteType.Chrome);
chromeRenderer.DrawSprite(moneyBinSprite, new float2(Game.viewport.Width - 320, 0), PaletteType.Chrome);
@@ -143,8 +151,18 @@ namespace OpenRa.Game
DrawChat();
}
- void AddButton(Rectangle r, Action b) { buttons.Add(Pair.New(r, b)); }
+ void DrawMinimap()
+ {
+ var hasRadar = Game.world.Actors.Any(a => a.Owner == Game.LocalPlayer
+ && a.traits.Contains()
+ && a.traits.Get().IsActive());
+ if (hasRadar)
+ Game.minimap.Draw(new float2(Game.viewport.Width - 256, 8));
+ }
+
+ void AddButton(Rectangle r, Action b) { buttons.Add(Pair.New(r, b)); }
+
void DrawBuildTabs(int paletteHeight)
{
const int tabWidth = 24;
@@ -215,6 +233,9 @@ namespace OpenRa.Game
x -= 14;
}
}
+
+ float? lastPowerProvidedPos;
+ float? lastPowerDrainedPos;
void DrawPower()
{
@@ -228,16 +249,32 @@ namespace OpenRa.Game
float2 bottom = powerOrigin + new float2(0, powerLevelTopSprite.size.Y + powerLevelBottomSprite.size.Y) - new float2(0, 50);
var scale = 100;
- while(Game.LocalPlayer.PowerProvided >= scale) scale += 100;
+ while(Math.Max(Game.LocalPlayer.PowerProvided, Game.LocalPlayer.PowerDrained) >= scale) scale *= 2;
//draw bar
- float2 powerTop = new float2(bottom.X, bottom.Y + (top.Y - bottom.Y) * (Game.LocalPlayer.PowerProvided / (float)scale));
+
+ var powerTopY = bottom.Y + (top.Y - bottom.Y) * (Game.LocalPlayer.PowerProvided / (float)scale) - Game.viewport.Location.Y;
+ lastPowerProvidedPos = float2.Lerp(lastPowerProvidedPos.GetValueOrDefault(powerTopY), powerTopY, .3f);
+ float2 powerTop = new float2(bottom.X, lastPowerProvidedPos.Value + Game.viewport.Location.Y);
+
+ var color = Color.LimeGreen;
+ if (Game.LocalPlayer.GetPowerState() == PowerState.Low)
+ color = Color.Orange;
+ if (Game.LocalPlayer.GetPowerState() == PowerState.Critical)
+ color = Color.Red;
+
+ var color2 = Graphics.Util.Lerp(0.25f, color, Color.Black);
+
+ for(int i = 11; i < 13; i++)
+ lineRenderer.DrawLine(bottom + new float2(i, 0), powerTop + new float2(i, 0), color, color);
+ for (int i = 13; i < 15; i++)
+ lineRenderer.DrawLine(bottom + new float2(i, 0), powerTop + new float2(i, 0), color2, color2);
- for(int i = 7; i < 11; i++)
- lineRenderer.DrawLine(bottom + new float2(i, 0), powerTop + new float2(i, 0), Color.LimeGreen, Color.LimeGreen);
lineRenderer.Flush();
-
+
+ var drainedPositionY = bottom.Y + (top.Y - bottom.Y)*(Game.LocalPlayer.PowerDrained/(float) scale) - powerIndicatorSprite.size.Y /2 - Game.viewport.Location.Y;
+ lastPowerDrainedPos = float2.Lerp(lastPowerDrainedPos.GetValueOrDefault(drainedPositionY), drainedPositionY, .3f);
//draw indicator
- float2 drainedPosition = new float2(bottom.X , bottom.Y + (top.Y - bottom.Y)*(Game.LocalPlayer.PowerDrained/(float) scale));
+ float2 drainedPosition = new float2(bottom.X + 2, lastPowerDrainedPos.Value + Game.viewport.Location.Y);
buildPaletteRenderer.DrawSprite(powerIndicatorSprite, drainedPosition, PaletteType.Chrome);
buildPaletteRenderer.Flush();
@@ -245,8 +282,24 @@ namespace OpenRa.Game
void DrawButtons()
{
+ // Chronoshift
+ Rectangle chronoshiftRect = new Rectangle(6, 14, repairButton.Image.bounds.Width, repairButton.Image.bounds.Height);
+ var chronoshiftDrawPos = Game.viewport.Location + new float2(chronoshiftRect.Location);
+
+ var hasChronosphere = Game.world.Actors.Any(a => a.Owner == Game.LocalPlayer && a.traits.Contains());
+
+ if (!hasChronosphere)
+ repairButton.ReplaceAnim("disabled");
+ else
+ {
+ //repairButton.ReplaceAnim(Game.controller.orderGenerator is RepairOrderGenerator ? "pressed" : "normal");
+ AddButton(chronoshiftRect, isLmb => HandleChronosphereButton());
+ }
+ buildPaletteRenderer.DrawSprite(repairButton.Image, chronoshiftDrawPos, PaletteType.Chrome);
+
+
// Repair
- Rectangle repairRect = new Rectangle(Game.viewport.Width - 100, 5, repairButton.Image.bounds.Width, repairButton.Image.bounds.Height);
+ Rectangle repairRect = new Rectangle(Game.viewport.Width - 120, 5, repairButton.Image.bounds.Width, repairButton.Image.bounds.Height);
var repairDrawPos = Game.viewport.Location + new float2(repairRect.Location);
var hasFact = Game.world.Actors.Any(a => a.Owner == Game.LocalPlayer && a.traits.Contains());
@@ -261,7 +314,7 @@ namespace OpenRa.Game
buildPaletteRenderer.DrawSprite(repairButton.Image, repairDrawPos, PaletteType.Chrome);
// Sell
- Rectangle sellRect = new Rectangle(Game.viewport.Width - 60, 5,
+ Rectangle sellRect = new Rectangle(Game.viewport.Width - 80, 5,
sellButton.Image.bounds.Width, sellButton.Image.bounds.Height);
var sellDrawPos = Game.viewport.Location + new float2(sellRect.Location);
@@ -271,6 +324,27 @@ namespace OpenRa.Game
AddButton(sellRect, isLmb => Game.controller.ToggleInputMode());
buildPaletteRenderer.DrawSprite(sellButton.Image, sellDrawPos, PaletteType.Chrome);
buildPaletteRenderer.Flush();
+
+ if (Game.Settings.PowerDownBuildings)
+ {
+ // Power Down
+ Rectangle pwrdownRect = new Rectangle(Game.viewport.Width - 40, 5,
+ pwrdownButton.Image.bounds.Width, pwrdownButton.Image.bounds.Height);
+
+ var pwrdownDrawPos = Game.viewport.Location + new float2(pwrdownRect.Location);
+
+ pwrdownButton.ReplaceAnim(Game.controller.orderGenerator is PowerDownOrderGenerator ? "pressed" : "normal");
+
+ AddButton(pwrdownRect, isLmb => Game.controller.ToggleInputMode());
+ buildPaletteRenderer.DrawSprite(pwrdownButton.Image, pwrdownDrawPos, PaletteType.Chrome);
+ }
+ buildPaletteRenderer.Flush();
+ }
+
+ void HandleChronosphereButton()
+ {
+ if (Game.controller.ToggleInputMode())
+ Sound.Play("slcttgt1.aud");
}
void DrawChat()
@@ -392,7 +466,7 @@ namespace OpenRa.Game
if (++x == columns) { x = 0; y++; }
}
- while (x != 0)
+ while (x != 0 || y < MinRows)
{
var rect = new Rectangle(origin.X + x * 64, origin.Y + 48 * y, 64, 48);
var drawPos = Game.viewport.Location + new float2(rect.Location);
diff --git a/OpenRa.Game/Combat.cs b/OpenRa.Game/Combat.cs
index ea855f1f38..b6cb373754 100644
--- a/OpenRa.Game/Combat.cs
+++ b/OpenRa.Game/Combat.cs
@@ -32,7 +32,7 @@ namespace OpenRa.Game
var maxSpread = GetMaximumSpread(weapon, warhead);
var hitActors = Game.FindUnitsInCircle(loc, maxSpread);
-
+
foreach (var victim in hitActors)
victim.InflictDamage(firedBy, (int)GetDamageToInflict(victim, loc, weapon, warhead), warhead);
}
@@ -46,11 +46,10 @@ namespace OpenRa.Game
{
if (!WeaponValidForTarget(weapon, target))
return 0f;
-
- var distance = (target.CenterLocation - loc).Length;
+
+ var distance = (target.CenterLocation - loc).Length*1/24f;
var rawDamage = weapon.Damage * (float)Math.Exp(-distance / warhead.Spread);
var multiplier = warhead.EffectivenessAgainst(target.Info.Armor);
-
return rawDamage * multiplier;
}
diff --git a/OpenRa.Game/Controller.cs b/OpenRa.Game/Controller.cs
index cbe2d35fa3..2653cc28d3 100644
--- a/OpenRa.Game/Controller.cs
+++ b/OpenRa.Game/Controller.cs
@@ -27,12 +27,18 @@ namespace OpenRa.Game
orderGenerator = new UnitOrderGenerator(new Actor[] { });
}
- public void ToggleInputMode() where T : IOrderGenerator, new()
+ public bool ToggleInputMode() where T : IOrderGenerator, new()
{
if (orderGenerator is T)
+ {
CancelInputMode();
+ return false;
+ }
else
+ {
orderGenerator = new T();
+ return true;
+ }
}
List recentOrders = new List();
diff --git a/OpenRa.Game/Cursor.cs b/OpenRa.Game/Cursor.cs
index d5ff149cb9..54ebacc76d 100644
--- a/OpenRa.Game/Cursor.cs
+++ b/OpenRa.Game/Cursor.cs
@@ -19,10 +19,12 @@ namespace OpenRa.Game
public static Cursor Select { get { return new Cursor("select"); } }
public static Cursor MoveBlocked { get { return new Cursor("move-blocked"); } }
public static Cursor Attack { get { return new Cursor("attack"); } }
+ public static Cursor AttackMove { get { return new Cursor("attackmove"); } }
public static Cursor Deploy { get { return new Cursor("deploy"); } }
public static Cursor Enter { get { return new Cursor("enter"); } }
public static Cursor DeployBlocked { get { return new Cursor("deploy-blocked"); } }
- public static Cursor Chronoshift { get { return new Cursor("chrono"); } }
+ public static Cursor Chronoshift { get { return new Cursor("chrono-target"); } }
+ public static Cursor ChronoshiftSelect { get { return new Cursor("chrono-select"); } }
public static Cursor C4 { get { return new Cursor("c4"); } }
public static Cursor Capture { get { return new Cursor("capture"); } }
public static Cursor Heal { get { return new Cursor("heal"); } }
@@ -30,5 +32,6 @@ namespace OpenRa.Game
public static Cursor SellBlocked { get { return new Cursor("sell-blocked"); } }
public static Cursor Repair { get { return new Cursor("repair"); } }
public static Cursor RepairBlocked { get { return new Cursor("repair-blocked"); } }
+ public static Cursor PowerDown { get { return new Cursor("powerdown"); } }
}
}
diff --git a/OpenRa.Game/Game.cs b/OpenRa.Game/Game.cs
index 964f9012e7..c4b1e52605 100644
--- a/OpenRa.Game/Game.cs
+++ b/OpenRa.Game/Game.cs
@@ -48,6 +48,7 @@ namespace OpenRa.Game
static bool usingAftermath;
static int2 clientSize;
static HardwarePalette palette;
+ public static Minimap minimap;
public static void ChangeMap(string mapName)
{
@@ -58,6 +59,11 @@ namespace OpenRa.Game
palette = new HardwarePalette(renderer, Rules.Map);
world = new World();
+ Game.world.ActorAdded += a =>
+ {
+ if (a.Owner != null && a.Info != null)
+ a.Owner.Shroud.Explore(a);
+ };
for (int i = 0; i < 8; i++)
{
@@ -73,10 +79,12 @@ namespace OpenRa.Game
var worldActor = new Actor(null, new int2(int.MaxValue, int.MaxValue), null);
worldActor.traits.Add(new Traits.WaterPaletteRotation(worldActor));
+ worldActor.traits.Add(new Traits.ChronoshiftPaletteEffect(worldActor));
Game.world.Add(worldActor);
Rules.Map.InitOreDensity();
worldRenderer = new WorldRenderer(renderer);
+ minimap = new Minimap(renderer);
SequenceProvider.Initialize(usingAftermath);
viewport = new Viewport(clientSize, Rules.Map.Offset, Rules.Map.Offset + Rules.Map.Size, renderer);
@@ -97,7 +105,7 @@ namespace OpenRa.Game
chrome = new Chrome(renderer);
- oreFrequency = (int)(Rules.General.GrowthRate * 60 * 1000);
+ oreFrequency = (int)(Rules.General.GrowthRate * 60 * 25);
oreTicks = oreFrequency;
}
@@ -168,6 +176,8 @@ namespace OpenRa.Game
lastTime += Settings.Timestep;
UpdatePalette(world.Actors.SelectMany(
a => a.traits.WithInterface()));
+ minimap.Update();
+
orderManager.TickImmediate();
if (orderManager.IsReadyForNextFrame)
@@ -177,11 +187,12 @@ namespace OpenRa.Game
controller.orderGenerator.Tick();
if (--oreTicks == 0)
- {
using (new PerfSample("ore"))
+ {
Rules.Map.GrowOre(SharedRandom);
- oreTicks = oreFrequency;
- }
+ minimap.InvalidateOre();
+ oreTicks = oreFrequency;
+ }
world.Tick();
UnitInfluence.Tick();
diff --git a/OpenRa.Game/GameRules/UnitInfo.cs b/OpenRa.Game/GameRules/UnitInfo.cs
index 8401ae04ef..63acbe1870 100755
--- a/OpenRa.Game/GameRules/UnitInfo.cs
+++ b/OpenRa.Game/GameRules/UnitInfo.cs
@@ -55,13 +55,15 @@ namespace OpenRa.Game.GameRules
public readonly int OrePips = 0;
public readonly string Icon = null;
public readonly int[] SelectionSize = null;
+ public readonly int Passengers = 0;
+ public readonly int UnloadFacing = 0;
+ public readonly UnitMovementType[] PassengerTypes = null;
public UnitInfo(string name) { Name = name; }
}
public class MobileInfo : UnitInfo
{
- public readonly int Passengers = 0;
public readonly int Speed = 0;
public readonly bool NoMovingFire = false;
public readonly string Voice = "GenericVoice";
diff --git a/OpenRa.Game/GameRules/UserSettings.cs b/OpenRa.Game/GameRules/UserSettings.cs
index bc502d4c36..8c441320f3 100644
--- a/OpenRa.Game/GameRules/UserSettings.cs
+++ b/OpenRa.Game/GameRules/UserSettings.cs
@@ -26,7 +26,8 @@ namespace OpenRa.Game.GameRules
public readonly string Replay = "";
// Gameplay options
- public readonly bool RepairRequiresConyard = false;
+ public readonly bool RepairRequiresConyard = true;
+ public readonly bool PowerDownBuildings = true;
}
}
diff --git a/OpenRa.Game/Graphics/CursorSheetBuilder.cs b/OpenRa.Game/Graphics/CursorSheetBuilder.cs
index 333a225da9..1a6b7291e3 100644
--- a/OpenRa.Game/Graphics/CursorSheetBuilder.cs
+++ b/OpenRa.Game/Graphics/CursorSheetBuilder.cs
@@ -11,8 +11,16 @@ namespace OpenRa.Game.Graphics
static Sprite[] LoadCursors(string filename)
{
- var shp = new Dune2ShpReader(FileSystem.OpenWithExts(filename, exts));
- return shp.Select(a => SheetBuilder.Add(a.Image, a.Size)).ToArray();
+ try
+ {
+ var shp = new Dune2ShpReader(FileSystem.OpenWithExts(filename, exts));
+ return shp.Select(a => SheetBuilder.Add(a.Image, a.Size)).ToArray();
+ }
+ catch (System.IndexOutOfRangeException) // This will occur when loading a custom (RA-format) .shp
+ {
+ var shp = new ShpReader(FileSystem.OpenWithExts(filename, exts));
+ return shp.Select(a => SheetBuilder.Add(a.Image, shp.Size)).ToArray();
+ }
}
public static Sprite[] LoadAllSprites(string filename) { return cursors[filename]; }
diff --git a/OpenRa.Game/Graphics/HardwarePalette.cs b/OpenRa.Game/Graphics/HardwarePalette.cs
index 9c0538e61f..2092291dd0 100644
--- a/OpenRa.Game/Graphics/HardwarePalette.cs
+++ b/OpenRa.Game/Graphics/HardwarePalette.cs
@@ -3,7 +3,12 @@ using OpenRa.FileFormats;
namespace OpenRa.Game.Graphics
{
- public enum PaletteType { Gold, Blue, Red, Orange, Teal, Salmon, Green, Gray, Shadow, Invuln, Chrome };
+ public enum PaletteType
+ {
+ Gold, Blue, Red, Orange, Teal, Salmon, Green, Gray,
+ Shadow, Invuln, Chrome, Shroud,
+ };
+
class HardwarePalette : Sheet
{
const int maxEntries = 16;
@@ -21,6 +26,7 @@ namespace OpenRa.Game.Graphics
AddPalette(new Palette(pal, new PaletteRemap(Color.FromArgb(140, 0, 0, 0))));
AddPalette(pal); // iron curtain. todo: remap!
AddPalette(pal); // chrome (it's like gold, but we're not going to hax it in palettemods)
+ AddPalette(new Palette(pal, new ShroudPaletteRemap()));
}
int AddPalette(Palette p)
diff --git a/OpenRa.Game/Graphics/Minimap.cs b/OpenRa.Game/Graphics/Minimap.cs
new file mode 100644
index 0000000000..fbb13f3ef9
--- /dev/null
+++ b/OpenRa.Game/Graphics/Minimap.cs
@@ -0,0 +1,89 @@
+using System.Drawing;
+using System.Linq;
+using OpenRa.Game.Traits;
+using OpenRa.FileFormats;
+
+namespace OpenRa.Game.Graphics
+{
+ class Minimap
+ {
+ Sheet sheet;
+ SpriteRenderer spriteRenderer;
+ Sprite sprite;
+ Bitmap terrain, oreLayer;
+
+ public void Tick() { }
+
+ public Minimap(Renderer r)
+ {
+ sheet = new Sheet(r, new Size(128, 128));
+ spriteRenderer = new SpriteRenderer(r, true, r.RgbaSpriteShader);
+ sprite = new Sprite(sheet, new Rectangle(0, 0, 128, 128), TextureChannel.Alpha);
+ }
+
+ // todo: extract these from the palette
+ Color[] terrainTypeColors;
+
+ public void InvalidateOre() { oreLayer = null; }
+
+ public void Update()
+ {
+ if (terrainTypeColors == null)
+ {
+ var pal = new Palette(FileSystem.Open(Rules.Map.Theater + ".pal"));
+ terrainTypeColors = new[] {
+ pal.GetColor(0x1a),
+ pal.GetColor(0x63),
+ pal.GetColor(0x2f),
+ pal.GetColor(0x1f),
+ pal.GetColor(0x14),
+ pal.GetColor(0x64),
+ pal.GetColor(0x1f),
+ pal.GetColor(0x68),
+ pal.GetColor(0x6b),
+ pal.GetColor(0x6d),
+ };
+ }
+
+ if (terrain == null)
+ {
+ terrain = new Bitmap(128, 128);
+ for (var y = 0; y < 128; y++)
+ for (var x = 0; x < 128; x++)
+ terrain.SetPixel(x, y, Rules.Map.IsInMap(x, y)
+ ? terrainTypeColors[Rules.TileSet.GetWalkability(Rules.Map.MapTiles[x, y])]
+ : Color.Black);
+ }
+
+ if (oreLayer == null)
+ {
+ oreLayer = new Bitmap(terrain);
+ for (var y = 0; y < 128; y++)
+ for (var x = 0; x < 128; x++)
+ if (Rules.Map.ContainsResource(new int2(x, y)))
+ oreLayer.SetPixel(x, y, terrainTypeColors[(int)TerrainMovementType.Ore]);
+ }
+
+ var bitmap = new Bitmap(oreLayer);
+
+ for( var y = 0; y < 128; y++ )
+ for (var x = 0; x < 128; x++)
+ {
+ var b = Game.BuildingInfluence.GetBuildingAt(new int2(x, y));
+ if (b != null)
+ bitmap.SetPixel(x, y, b.Owner != null ? Chat.paletteColors[(int)b.Owner.Palette] : terrainTypeColors[4]);
+ }
+
+ foreach (var a in Game.world.Actors.Where(a => a.traits.Contains()))
+ bitmap.SetPixel(a.Location.X, a.Location.Y, Chat.paletteColors[(int)a.Owner.Palette]);
+
+ sheet.Texture.SetData(bitmap);
+ }
+
+ public void Draw(float2 pos)
+ {
+ spriteRenderer.DrawSprite(sprite, pos, PaletteType.Chrome, new float2(256,256));
+ spriteRenderer.Flush();
+ }
+ }
+}
diff --git a/OpenRa.Game/Graphics/SequenceProvider.cs b/OpenRa.Game/Graphics/SequenceProvider.cs
index 4915033531..756f523a8b 100644
--- a/OpenRa.Game/Graphics/SequenceProvider.cs
+++ b/OpenRa.Game/Graphics/SequenceProvider.cs
@@ -57,7 +57,7 @@ namespace OpenRa.Game.Graphics
public static Sequence GetSequence(string unitName, string sequenceName)
{
try { return units[unitName][sequenceName]; }
- catch (KeyNotFoundException e)
+ catch (KeyNotFoundException)
{
throw new InvalidOperationException(
"Unit `{0}` does not have a sequence `{1}`".F(unitName, sequenceName));
diff --git a/OpenRa.Game/Graphics/Sprite.cs b/OpenRa.Game/Graphics/Sprite.cs
index a9f7247f37..3fb1a10971 100644
--- a/OpenRa.Game/Graphics/Sprite.cs
+++ b/OpenRa.Game/Graphics/Sprite.cs
@@ -19,10 +19,10 @@ namespace OpenRa.Game.Graphics
this.channel = channel;
uv = new RectangleF(
- (float)(bounds.Left + 0.5f) / sheet.Size.Width,
- (float)(bounds.Top + 0.5f) / sheet.Size.Height,
- (float)(bounds.Width) / sheet.Size.Width,
- (float)(bounds.Height) / sheet.Size.Height);
+ (float)(bounds.Left + .5f) / sheet.Size.Width,
+ (float)(bounds.Top + .5f) / sheet.Size.Height,
+ (float)(bounds.Width - .5f) / sheet.Size.Width,
+ (float)(bounds.Height - .5f) / sheet.Size.Height);
uvhax = new float2[]
{
diff --git a/OpenRa.Game/Graphics/SpriteRenderer.cs b/OpenRa.Game/Graphics/SpriteRenderer.cs
index 87e28c693f..3074c43a0f 100644
--- a/OpenRa.Game/Graphics/SpriteRenderer.cs
+++ b/OpenRa.Game/Graphics/SpriteRenderer.cs
@@ -55,12 +55,17 @@ namespace OpenRa.Game.Graphics
}
public void DrawSprite(Sprite s, float2 location, PaletteType palette)
+ {
+ DrawSprite(s, location, palette, s.size);
+ }
+
+ public void DrawSprite(Sprite s, float2 location, PaletteType palette, float2 size)
{
if (s.sheet != currentSheet)
Flush();
currentSheet = s.sheet;
- Util.FastCreateQuad(vertices, indices, location.ToInt2(), s, (int) palette, nv, ni);
+ Util.FastCreateQuad(vertices, indices, location.ToInt2(), s, (int)palette, nv, ni, size);
nv += 4; ni += 6;
if (++sprites >= spritesPerBatch)
Flush();
diff --git a/OpenRa.Game/Graphics/TerrainRenderer.cs b/OpenRa.Game/Graphics/TerrainRenderer.cs
index 07d4263984..1032c3b0a0 100644
--- a/OpenRa.Game/Graphics/TerrainRenderer.cs
+++ b/OpenRa.Game/Graphics/TerrainRenderer.cs
@@ -34,7 +34,7 @@ namespace OpenRa.Game.Graphics
for( int i = map.XOffset ; i < map.XOffset + map.Width; i++ )
{
Sprite tile = tileMapping[map.MapTiles[i, j]];
- Util.FastCreateQuad(vertices, indices, Game.CellSize * new float2(i, j), tile, 0, nv, ni);
+ Util.FastCreateQuad(vertices, indices, Game.CellSize * new float2(i, j), tile, 0, nv, ni, tile.size);
nv += 4;
ni += 6;
}
diff --git a/OpenRa.Game/Graphics/Util.cs b/OpenRa.Game/Graphics/Util.cs
index d4d7ab60c7..847ad9d505 100644
--- a/OpenRa.Game/Graphics/Util.cs
+++ b/OpenRa.Game/Graphics/Util.cs
@@ -2,23 +2,12 @@ using System;
using System.Collections.Generic;
using System.Drawing.Imaging;
using System.IO;
+using System.Drawing;
namespace OpenRa.Game.Graphics
{
static class Util
{
- static float2 KLerp(float2 o, float2 d, int k)
- {
- switch (k)
- {
- case 0: return o;
- case 1: return new float2(o.X + d.X, o.Y);
- case 2: return new float2(o.X, o.Y + d.Y);
- case 3: return new float2(o.X + d.X, o.Y + d.Y);
- default: throw new InvalidOperationException();
- }
- }
-
public static string[] ReadAllLines(Stream s)
{
List result = new List();
@@ -56,14 +45,18 @@ namespace OpenRa.Game.Graphics
static float[] channelSelect = { 0.75f, 0.25f, -0.25f, -0.75f };
- public static void FastCreateQuad(Vertex[] vertices, ushort[] indices, float2 o, Sprite r, int palette, int nv, int ni)
+ public static void FastCreateQuad(Vertex[] vertices, ushort[] indices, float2 o, Sprite r, int palette, int nv, int ni, float2 size)
{
float2 attrib = new float2(palette / 16.0f, channelSelect[(int)r.channel]);
- vertices[nv] = new Vertex(KLerp(o, r.size, 0), r.FastMapTextureCoords(0), attrib);
- vertices[nv + 1] = new Vertex(KLerp(o, r.size, 1), r.FastMapTextureCoords(1), attrib);
- vertices[nv + 2] = new Vertex(KLerp(o, r.size, 2), r.FastMapTextureCoords(2), attrib);
- vertices[nv + 3] = new Vertex(KLerp(o, r.size, 3), r.FastMapTextureCoords(3), attrib);
+ vertices[nv] = new Vertex(o,
+ r.FastMapTextureCoords(0), attrib);
+ vertices[nv + 1] = new Vertex(new float2(o.X + size.X, o.Y),
+ r.FastMapTextureCoords(1), attrib);
+ vertices[nv + 2] = new Vertex(new float2(o.X, o.Y + size.Y),
+ r.FastMapTextureCoords(2), attrib);
+ vertices[nv + 3] = new Vertex(new float2(o.X + size.X, o.Y + size.Y),
+ r.FastMapTextureCoords(3), attrib);
indices[ni] = (ushort)(nv);
indices[ni + 1] = indices[ni + 3] = (ushort)(nv + 1);
@@ -111,5 +104,19 @@ namespace OpenRa.Game.Graphics
bitmap.UnlockBits(bits);
}
}
+
+ public static Color Lerp(float t, Color a, Color b)
+ {
+ return Color.FromArgb(
+ LerpChannel(t, a.A, b.A),
+ LerpChannel(t, a.R, b.R),
+ LerpChannel(t, a.G, b.G),
+ LerpChannel(t, a.B, b.B));
+ }
+
+ public static int LerpChannel(float t, int a, int b)
+ {
+ return (int)((1 - t) * a + t * b);
+ }
}
}
diff --git a/OpenRa.Game/Graphics/WorldRenderer.cs b/OpenRa.Game/Graphics/WorldRenderer.cs
index 14bbff4afe..157b9ee2ab 100644
--- a/OpenRa.Game/Graphics/WorldRenderer.cs
+++ b/OpenRa.Game/Graphics/WorldRenderer.cs
@@ -87,6 +87,8 @@ namespace OpenRa.Game.Graphics
if (Game.controller.orderGenerator != null)
Game.controller.orderGenerator.Render();
+ Game.LocalPlayer.Shroud.Draw(spriteRenderer);
+
lineRenderer.Flush();
spriteRenderer.Flush();
}
@@ -211,7 +213,7 @@ namespace OpenRa.Game.Graphics
foreach (var pips in selectedUnit.traits.WithInterface())
{
- foreach (var pip in pips.GetPips())
+ foreach (var pip in pips.GetPips(selectedUnit))
{
var pipImages = new Animation("pips");
pipImages.PlayRepeating(pipStrings[(int)pip]);
diff --git a/OpenRa.Game/MainWindow.cs b/OpenRa.Game/MainWindow.cs
index 8d1d61ce81..ea0af1b54f 100755
--- a/OpenRa.Game/MainWindow.cs
+++ b/OpenRa.Game/MainWindow.cs
@@ -4,9 +4,9 @@ using System.IO;
using System.Runtime.InteropServices;
using System.Windows.Forms;
using OpenRa.FileFormats;
+using OpenRa.Game.GameRules;
using OpenRa.Game.Graphics;
using OpenRa.Game.Orders;
-using OpenRa.Game.GameRules;
namespace OpenRa.Game
@@ -31,51 +31,33 @@ namespace OpenRa.Game
[DllImport("user32")]
static extern int ShowCursor([MarshalAs(UnmanagedType.Bool)] bool visible);
-
-
public MainWindow(Settings settings)
{
-
-
FormBorderStyle = FormBorderStyle.None;
BackColor = Color.Black;
StartPosition = FormStartPosition.Manual;
Location = Point.Empty;
Visible = true;
- // Load user settings
- Game.Settings = new UserSettings();
-
while (!File.Exists("redalert.mix"))
{
var current = Directory.GetCurrentDirectory();
if (Directory.GetDirectoryRoot(current) == current)
throw new InvalidOperationException("Unable to load MIX files.");
Directory.SetCurrentDirectory("..");
-
- try
- {
- // settings.ini should be located with the mix files
- FileSystem.MountTemporary(new Folder("./"));
- IniFile SettingsRules = new IniFile(FileSystem.Open("settings.ini"));
- FieldLoader.Load(Game.Settings, SettingsRules.GetSection("Settings"));
- FileSystem.UnmountTemporaryPackages();
- }
- catch (FileNotFoundException) { }
}
+ LoadUserSettings(settings);
+
UiOverlay.ShowUnitDebug = Game.Settings.UnitDebug;
UiOverlay.ShowBuildDebug = Game.Settings.BuildingDebug;
WorldRenderer.ShowUnitPaths = Game.Settings.PathDebug;
Renderer.SheetSize = Game.Settings.SheetSize;
-
FileSystem.MountDefaultPackages();
if (Game.Settings.UseAftermath)
- {
FileSystem.MountAftermathPackages();
- }
bool windowed = !Game.Settings.Fullscreen;
renderer = new Renderer(this, GetResolution(settings), windowed);
@@ -89,6 +71,17 @@ namespace OpenRa.Game
Game.ResetTimer();
}
+ static void LoadUserSettings(Settings settings)
+ {
+ Game.Settings = new UserSettings();
+ var settingsFile = settings.GetValue("settings", "settings.ini");
+ FileSystem.MountTemporary(new Folder("./"));
+ if (FileSystem.Exists(settingsFile))
+ FieldLoader.Load(Game.Settings,
+ new IniFile(FileSystem.Open(settingsFile)).GetSection("Settings"));
+ FileSystem.UnmountTemporaryPackages();
+ }
+
internal void Run()
{
while (Created && Visible)
diff --git a/OpenRa.Game/OpenRa.Game.csproj b/OpenRa.Game/OpenRa.Game.csproj
index d5903beeee..4aa7ac18d0 100644
--- a/OpenRa.Game/OpenRa.Game.csproj
+++ b/OpenRa.Game/OpenRa.Game.csproj
@@ -95,19 +95,23 @@
+
+
+
-
+
+
@@ -116,6 +120,7 @@
+
@@ -183,6 +188,7 @@
+
@@ -196,6 +202,11 @@
+
+
+
+
+
@@ -207,6 +218,8 @@
+
+
@@ -291,4 +304,4 @@
-->
-
\ No newline at end of file
+
diff --git a/OpenRa.Game/Orders/TeleportOrderGenerator.cs b/OpenRa.Game/Orders/ChronoshiftDestinationOrderGenerator.cs
similarity index 64%
rename from OpenRa.Game/Orders/TeleportOrderGenerator.cs
rename to OpenRa.Game/Orders/ChronoshiftDestinationOrderGenerator.cs
index 3fca07bc4b..e85e248752 100644
--- a/OpenRa.Game/Orders/TeleportOrderGenerator.cs
+++ b/OpenRa.Game/Orders/ChronoshiftDestinationOrderGenerator.cs
@@ -3,14 +3,15 @@ using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Drawing;
+using OpenRa.Game.Traits;
namespace OpenRa.Game.Orders
{
- class TeleportOrderGenerator : IOrderGenerator
+ class ChronoshiftDestinationOrderGenerator : IOrderGenerator
{
public readonly Actor self;
- public TeleportOrderGenerator(Actor self)
+ public ChronoshiftDestinationOrderGenerator(Actor self)
{
this.self = self;
}
@@ -34,7 +35,8 @@ namespace OpenRa.Game.Orders
public Cursor GetCursor(int2 xy, MouseInput mi)
{
- return Cursor.Chronoshift;
+ var movement = self.traits.WithInterface().FirstOrDefault();
+ return (movement.CanEnterCell(xy)) ? Cursor.Chronoshift : Cursor.MoveBlocked;
}
}
}
diff --git a/OpenRa.Game/Orders/ChronosphereSelectOrderGenerator.cs b/OpenRa.Game/Orders/ChronosphereSelectOrderGenerator.cs
new file mode 100644
index 0000000000..ae7de0551f
--- /dev/null
+++ b/OpenRa.Game/Orders/ChronosphereSelectOrderGenerator.cs
@@ -0,0 +1,57 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using OpenRa.Game.GameRules;
+using OpenRa.Game.Traits;
+
+namespace OpenRa.Game.Orders
+{
+ class ChronosphereSelectOrderGenerator : IOrderGenerator
+ {
+ public IEnumerable Order(int2 xy, MouseInput mi)
+ {
+ if (mi.Button == MouseButton.Right)
+ Game.controller.CancelInputMode();
+
+ return OrderInner(xy, mi);
+ }
+
+ IEnumerable OrderInner(int2 xy, MouseInput mi)
+ {
+ if (mi.Button == MouseButton.Left)
+ {
+ var loc = mi.Location + Game.viewport.Location;
+ var underCursor = Game.FindUnits(loc, loc)
+ .Where(a => a.Owner == Game.LocalPlayer
+ && a.traits.WithInterface().Any()
+ && a.Info.Selectable).FirstOrDefault();
+
+ var unit = underCursor != null ? underCursor.Info as UnitInfo : null;
+
+ if (unit != null)
+ {
+ yield return new Order("ChronosphereSelect", underCursor, null, int2.Zero, null);
+ }
+ }
+ }
+
+ public void Tick()
+ {
+ var hasChronosphere = Game.world.Actors
+ .Any(a => a.Owner == Game.LocalPlayer && a.traits.Contains());
+
+ if (!hasChronosphere)
+ Game.controller.CancelInputMode();
+ }
+
+ public void Render() { }
+
+ public Cursor GetCursor(int2 xy, MouseInput mi)
+ {
+ mi.Button = MouseButton.Left;
+ return OrderInner(xy, mi).Any()
+ ? Cursor.ChronoshiftSelect : Cursor.MoveBlocked;
+ }
+ }
+}
diff --git a/OpenRa.Game/Orders/PowerDownOrderGenerator.cs b/OpenRa.Game/Orders/PowerDownOrderGenerator.cs
new file mode 100644
index 0000000000..5779e81a20
--- /dev/null
+++ b/OpenRa.Game/Orders/PowerDownOrderGenerator.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using OpenRa.Game.GameRules;
+using OpenRa.Game.Traits;
+
+namespace OpenRa.Game.Orders
+{
+ class PowerDownOrderGenerator : IOrderGenerator
+ {
+ public IEnumerable Order(int2 xy, MouseInput mi)
+ {
+ if (mi.Button == MouseButton.Right)
+ Game.controller.CancelInputMode();
+
+ return OrderInner(xy, mi);
+ }
+
+ IEnumerable OrderInner(int2 xy, MouseInput mi)
+ {
+ if (mi.Button == MouseButton.Left)
+ {
+ var loc = mi.Location + Game.viewport.Location;
+ var underCursor = Game.FindUnits(loc, loc)
+ .Where(a => a.Owner == Game.LocalPlayer
+ && a.traits.Contains()
+ && a.Info.Selectable).FirstOrDefault();
+
+ var building = underCursor != null ? underCursor.Info as BuildingInfo : null;
+
+ if (building != null)
+ yield return new Order("PowerDown", underCursor, null, int2.Zero, null);
+ }
+ }
+
+ public void Tick() { }
+ public void Render() { }
+
+ public Cursor GetCursor(int2 xy, MouseInput mi)
+ {
+ mi.Button = MouseButton.Left;
+ return OrderInner(xy, mi).Any()
+ ? Cursor.PowerDown : Cursor.PowerDown;
+ }
+ }
+}
diff --git a/OpenRa.Game/Orders/UnitOrderGenerator.cs b/OpenRa.Game/Orders/UnitOrderGenerator.cs
index c50379f4e4..dc4019ceea 100644
--- a/OpenRa.Game/Orders/UnitOrderGenerator.cs
+++ b/OpenRa.Game/Orders/UnitOrderGenerator.cs
@@ -27,7 +27,7 @@ namespace OpenRa.Game.Orders
public void Tick()
{
- selection.RemoveAll(a => a.IsDead);
+ selection.RemoveAll(a => !a.IsInWorld);
}
public void Render()
@@ -81,10 +81,11 @@ namespace OpenRa.Game.Orders
else
return Cursor.MoveBlocked;
case "Enter": return Cursor.Enter;
+ case "EnterTransport": return Cursor.Enter;
case "Deliver": return Cursor.Enter;
case "Infiltrate": return Cursor.Enter;
case "Capture": return Cursor.Capture;
- case "Harvest": return Cursor.Attack; // TODO: special harvest cursor?
+ case "Harvest": return Cursor.AttackMove;
default:
return null;
}
diff --git a/OpenRa.Game/Player.cs b/OpenRa.Game/Player.cs
index 5e40e92b32..bae1f38aa5 100644
--- a/OpenRa.Game/Player.cs
+++ b/OpenRa.Game/Player.cs
@@ -1,5 +1,6 @@
using System;
using System.Linq;
+using System.Collections.Generic;
using OpenRa.Game.GameRules;
using OpenRa.Game.Graphics;
using OpenRa.Game.Traits;
@@ -17,15 +18,17 @@ namespace OpenRa.Game
public string InternalName;
public Race Race;
public readonly int Index;
- public int Cash;
- public int Ore;
+ public int Cash = 10000;
+ public int Ore = 0;
public int OreCapacity;
- public int DisplayCash;
- public int PowerProvided;
- public int PowerDrained;
+ public int DisplayCash = 0;
+ public int PowerProvided = 0;
+ public int PowerDrained = 0;
public bool IsReady;
+ public Shroud Shroud = new Shroud();
+
public Player( Actor playerActor, int index, PaletteType palette, string playerName, Race race, string internalName )
{
this.PlayerActor = playerActor;
@@ -34,10 +37,6 @@ namespace OpenRa.Game
this.InternalName = internalName;
this.PlayerName = playerName;
this.Race = race;
- this.Cash = 10000;
- this.Ore = 0;
- this.DisplayCash = 0;
- this.PowerProvided = this.PowerDrained = 0;
}
void UpdatePower()
@@ -52,11 +51,11 @@ namespace OpenRa.Game
foreach (var a in myBuildings)
{
- var bi = a.Info as BuildingInfo;
- if (bi.Power > 0) /* todo: is this how real-ra scales it? */
- PowerProvided += (a.Health * bi.Power) / bi.Strength;
+ var p = a.traits.Get().GetPowerUsage();
+ if (p > 0)
+ PowerProvided += p;
else
- PowerDrained -= bi.Power;
+ PowerDrained -= p;
}
if (PowerProvided - PowerDrained < 0)
diff --git a/OpenRa.Game/Shroud.cs b/OpenRa.Game/Shroud.cs
new file mode 100644
index 0000000000..8f64f7365c
--- /dev/null
+++ b/OpenRa.Game/Shroud.cs
@@ -0,0 +1,110 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using IjwFramework.Types;
+using OpenRa.Game.Graphics;
+
+namespace OpenRa.Game
+{
+ class Shroud
+ {
+ bool[,] explored = new bool[128, 128];
+ Sprite[] shadowBits = SpriteSheetBuilder.LoadAllSprites("shadow");
+ Sprite[,] sprites = new Sprite[128, 128];
+ bool dirty;
+
+ public void Explore(Actor a)
+ {
+ foreach (var t in Game.FindTilesInCircle((1f / Game.CellSize * a.CenterLocation).ToInt2(), a.Info.Sight))
+ explored[t.X, t.Y] = true;
+
+ dirty = true;
+ }
+
+ Sprite ChooseShroud(int i, int j)
+ {
+ // bits are for exploredness: left, right, up, down, self
+ var n = new[] {
+ 0xf,0xf,0xf,0xf,
+ 0xf,0x0f,0x0f,0xf,
+ 0xf,0x0f,0x0f,0xf,
+ 0xf,0xf,0xf,0xf,
+ 0,7,13,0,
+ 14,6,12,4,
+ 11,3,9,1,
+ 0,2,8,0,
+ };
+
+ var v = 0;
+ if (explored[i - 1, j]) v |= 1;
+ if (explored[i + 1, j]) v |= 2;
+ if (explored[i, j - 1]) v |= 4;
+ if (explored[i, j + 1]) v |= 8;
+ if (explored[i, j]) v |= 16;
+
+ var x = n[v];
+
+ if (x == 0)
+ {
+ // bits are for exploredness: TL, TR, BR, BL
+ var m = new[] {
+ 46, 41, 42, 38,
+ 43, 45, 39, 35,
+ 40, 37, 44, 34,
+ 36, 33, 32, 47,
+ };
+
+ var u = 0;
+ if (explored[i - 1, j - 1]) u |= 1;
+ if (explored[i + 1, j - 1]) u |= 2;
+ if (explored[i + 1, j + 1]) u |= 4;
+ if (explored[i - 1, j + 1]) u |= 8;
+ return shadowBits[m[u]];
+ }
+
+ return shadowBits[x];
+ }
+
+ public void Draw(SpriteRenderer r)
+ {
+ if (dirty)
+ {
+ dirty = false;
+ for (int j = 1; j < 127; j++)
+ for (int i = 1; i < 127; i++)
+ sprites[i, j] = ChooseShroud(i, j);
+ }
+
+ for (var j = 1; j < 127; j++)
+ {
+ var starti = 1;
+ for (var i = 1; i < 127; i++)
+ {
+ if (sprites[i, j] == shadowBits[0x0f])
+ continue;
+
+ if (starti != i)
+ {
+ r.DrawSprite(sprites[starti,j],
+ Game.CellSize * new float2(starti, j),
+ PaletteType.Shroud,
+ new float2(Game.CellSize * (i - starti), Game.CellSize));
+ starti = i+1;
+ }
+
+ r.DrawSprite(sprites[i, j],
+ Game.CellSize * new float2(i, j),
+ PaletteType.Shroud);
+ starti = i+1;
+ }
+
+ if (starti < 127)
+ r.DrawSprite(sprites[starti, j],
+ Game.CellSize * new float2(starti, j),
+ PaletteType.Shroud,
+ new float2(Game.CellSize * (127 - starti), Game.CellSize));
+ }
+ }
+ }
+}
diff --git a/OpenRa.Game/TerrainCosts.cs b/OpenRa.Game/TerrainCosts.cs
index 78f17a96ac..7c4ffc9dfb 100644
--- a/OpenRa.Game/TerrainCosts.cs
+++ b/OpenRa.Game/TerrainCosts.cs
@@ -2,7 +2,7 @@ using OpenRa.Game.Graphics;
namespace OpenRa.Game
{
- enum UnitMovementType : byte
+ public enum UnitMovementType : byte
{
Foot = 0,
Track = 1,
diff --git a/OpenRa.Game/Traits/Activities/EnterTransport.cs b/OpenRa.Game/Traits/Activities/EnterTransport.cs
new file mode 100644
index 0000000000..daa875173d
--- /dev/null
+++ b/OpenRa.Game/Traits/Activities/EnterTransport.cs
@@ -0,0 +1,36 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace OpenRa.Game.Traits.Activities
+{
+ class EnterTransport : IActivity
+ {
+ public IActivity NextActivity { get; set; }
+ bool isCanceled;
+ public Actor transport;
+
+ public EnterTransport(Actor self, Actor transport)
+ {
+ this.transport = transport;
+ }
+
+ public IActivity Tick(Actor self)
+ {
+ if (isCanceled) return NextActivity;
+ if (transport == null || !transport.IsInWorld) return NextActivity;
+
+ var cargo = transport.traits.Get();
+ if (cargo.IsFull(transport))
+ return NextActivity;
+
+ cargo.Load(transport, self);
+ Game.world.AddFrameEndTask(w => w.Remove(self));
+
+ return this;
+ }
+
+ public void Cancel(Actor self) { isCanceled = true; NextActivity = null; }
+ }
+}
diff --git a/OpenRa.Game/Traits/Activities/Turn.cs b/OpenRa.Game/Traits/Activities/Turn.cs
index f07033d93e..1b81859e31 100755
--- a/OpenRa.Game/Traits/Activities/Turn.cs
+++ b/OpenRa.Game/Traits/Activities/Turn.cs
@@ -5,7 +5,7 @@ namespace OpenRa.Game.Traits.Activities
{
public IActivity NextActivity { get; set; }
- public int desiredFacing;
+ int desiredFacing;
public Turn( int desiredFacing )
{
diff --git a/OpenRa.Game/Traits/Activities/UnloadCargo.cs b/OpenRa.Game/Traits/Activities/UnloadCargo.cs
new file mode 100644
index 0000000000..b0af980ab5
--- /dev/null
+++ b/OpenRa.Game/Traits/Activities/UnloadCargo.cs
@@ -0,0 +1,69 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace OpenRa.Game.Traits.Activities
+{
+ class UnloadCargo : IActivity
+ {
+ public IActivity NextActivity { get; set; }
+ bool isCanceled;
+
+ int2? ChooseExitTile(Actor self)
+ {
+ // is anyone still hogging this tile?
+ if (Game.UnitInfluence.GetUnitsAt(self.Location).Count() > 1)
+ return null;
+
+ for (var i = -1; i < 2; i++)
+ for (var j = -1; j < 2; j++)
+ if ((i != 0 || j != 0) &&
+ Game.IsCellBuildable(self.Location + new int2(i, j),
+ UnitMovementType.Foot))
+ return self.Location + new int2(i, j);
+
+ return null;
+ }
+
+ public IActivity Tick(Actor self)
+ {
+ if (isCanceled) return NextActivity;
+
+ // if we're a thing that can turn, turn to the
+ // right facing for the unload animation
+ var unit = self.traits.GetOrDefault();
+ if (unit != null && unit.Facing != self.Info.UnloadFacing)
+ return new Turn(self.Info.UnloadFacing) { NextActivity = this };
+
+ // todo: handle the BS of open/close sequences, which are inconsistent,
+ // for reasons that probably make good sense to the westwood guys.
+
+ var cargo = self.traits.Get();
+ if (cargo.IsEmpty(self))
+ return NextActivity;
+
+ var ru = self.traits.WithInterface().FirstOrDefault();
+ if (ru != null)
+ ru.PlayCustomAnimation(self, "unload", null);
+
+ var exitTile = ChooseExitTile(self);
+ if (exitTile == null)
+ return this;
+
+ var actor = cargo.Unload(self);
+
+ Game.world.AddFrameEndTask(w =>
+ {
+ w.Add(actor);
+ actor.traits.Get().TeleportTo(actor, self.Location);
+ actor.CancelActivity();
+ actor.QueueActivity(new Move(exitTile.Value, 0));
+ });
+
+ return this;
+ }
+
+ public void Cancel(Actor self) { NextActivity = null; isCanceled = true; }
+ }
+}
diff --git a/OpenRa.Game/Traits/AttackTurreted.cs b/OpenRa.Game/Traits/AttackTurreted.cs
index 256bd25379..1d0007e223 100755
--- a/OpenRa.Game/Traits/AttackTurreted.cs
+++ b/OpenRa.Game/Traits/AttackTurreted.cs
@@ -26,12 +26,9 @@ namespace OpenRa.Game.Traits
protected override void QueueAttack( Actor self, Order order )
{
- var bi = self.Info as BuildingInfo;
- if (bi != null && bi.Powered && self.Owner.GetPowerState() != PowerState.Normal)
- {
- if (self.Owner == Game.LocalPlayer) Sound.Play("nopowr1.aud");
+ var b = self.traits.Get();
+ if (b != null && b.InsuffientPower())
return;
- }
const int RangeTolerance = 1; /* how far inside our maximum range we should try to sit */
/* todo: choose the appropriate weapon, when only one works against this target */
diff --git a/OpenRa.Game/Traits/Building.cs b/OpenRa.Game/Traits/Building.cs
index 5beea8d05f..dfb870285c 100644
--- a/OpenRa.Game/Traits/Building.cs
+++ b/OpenRa.Game/Traits/Building.cs
@@ -5,20 +5,66 @@ using System.Collections.Generic;
using System.Linq;
using System.Text;
using OpenRa.Game.Effects;
+using OpenRa.Game.Graphics;
namespace OpenRa.Game.Traits
{
- class Building : INotifyDamage, IOrder, ITick
+ class Building : INotifyDamage, IOrder, ITick, IRenderModifier
{
+ readonly Actor self;
public readonly BuildingInfo unitInfo;
bool isRepairing = false;
+ bool isPoweredDown = false;
public Building(Actor self)
{
+ this.self = self;
unitInfo = (BuildingInfo)self.Info;
self.CenterLocation = Game.CellSize
* ((float2)self.Location + .5f * (float2)unitInfo.Dimensions);
}
+
+ public bool InsuffientPower()
+ {
+ return (isPoweredDown || (unitInfo.Powered && self.Owner.GetPowerState() != PowerState.Normal));
+ }
+
+ public int GetPowerUsage()
+ {
+ if (isPoweredDown)
+ return 0;
+
+ if (unitInfo.Power > 0) /* todo: is this how real-ra scales it? */
+ return (self.Health * unitInfo.Power) / unitInfo.Strength;
+ else
+ return unitInfo.Power;
+ }
+
+ public Animation iconAnim;
+ public IEnumerable
+ ModifyRender(Actor self, IEnumerable rs)
+ {
+ if (!InsuffientPower())
+ return rs;
+
+ List nrs = new List(rs);
+ foreach(var r in rs)
+ {
+ // Need 2 shadows to make it dark enough
+ nrs.Add(r.WithPalette(PaletteType.Shadow));
+ nrs.Add(r.WithPalette(PaletteType.Shadow));
+ }
+
+ if (isPoweredDown)
+ {
+ iconAnim = new Animation("powerdown");
+ iconAnim.PlayRepeating("disabled");
+ nrs.Add(new Renderable(iconAnim.Image, self.CenterLocation - 0.5f*iconAnim.Image.size, PaletteType.Chrome));
+ }
+
+
+ return nrs;
+ }
public void Damaged(Actor self, AttackInfo e)
{
@@ -43,6 +89,12 @@ namespace OpenRa.Game.Traits
{
isRepairing = !isRepairing;
}
+
+ if (order.OrderString == "PowerDown")
+ {
+ isPoweredDown = !isPoweredDown;
+ Sound.Play((isPoweredDown) ? "bleep12.aud" : "bleep11.aud");
+ }
}
int remainingTicks;
diff --git a/OpenRa.Game/Traits/Cargo.cs b/OpenRa.Game/Traits/Cargo.cs
new file mode 100644
index 0000000000..3d1f27cea0
--- /dev/null
+++ b/OpenRa.Game/Traits/Cargo.cs
@@ -0,0 +1,85 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using OpenRa.Game.GameRules;
+using OpenRa.Game.Traits.Activities;
+
+namespace OpenRa.Game.Traits
+{
+ class Cargo : IPips, IOrder
+ {
+ List cargo = new List();
+
+ public Cargo(Actor self) {}
+
+ public Order IssueOrder(Actor self, int2 xy, MouseInput mi, Actor underCursor)
+ {
+ // todo: check if there is an unoccupied `land` tile adjacent
+ if (mi.Button == MouseButton.Right && underCursor == self && cargo.Count > 0)
+ {
+ var unit = underCursor.traits.GetOrDefault();
+ if (unit != null && unit.Altitude > 0) return null;
+
+ return new Order("Deploy", self, null, int2.Zero, null);
+ }
+
+ return null;
+ }
+
+ public void ResolveOrder(Actor self, Order order)
+ {
+ if (order.OrderString == "Deploy")
+ {
+ // todo: eject the units
+ self.CancelActivity();
+ self.QueueActivity(new UnloadCargo());
+ }
+ }
+
+ public bool IsFull(Actor self)
+ {
+ return cargo.Count == self.Info.Passengers;
+ }
+
+ public bool IsEmpty(Actor self)
+ {
+ return cargo.Count == 0;
+ }
+
+ public Actor Unload(Actor self)
+ {
+ var a = cargo[0];
+ cargo.RemoveAt(0);
+ return a;
+ }
+
+ public IEnumerable GetPips( Actor self )
+ {
+ for (var i = 0; i < self.Info.Passengers; i++)
+ if (i >= cargo.Count)
+ yield return PipType.Transparent;
+ else
+ yield return GetPipForPassenger(cargo[i]);
+ }
+
+ static PipType GetPipForPassenger(Actor a)
+ {
+ // probably not actually right yet; fix to match real-ra
+
+ if (a.traits.Contains())
+ return PipType.Yellow;
+ if (!a.traits.WithInterface().Any())
+ return PipType.Yellow; // noncombat [E6,SPY,THF]
+ if (a.traits.Contains())
+ return PipType.Red; // E7
+
+ return PipType.Green;
+ }
+
+ public void Load(Actor self, Actor a)
+ {
+ cargo.Add(a);
+ }
+ }
+}
diff --git a/OpenRa.Game/Traits/ChronoshiftDeploy.cs b/OpenRa.Game/Traits/ChronoshiftDeploy.cs
index 46220b51dd..7c4252fc97 100644
--- a/OpenRa.Game/Traits/ChronoshiftDeploy.cs
+++ b/OpenRa.Game/Traits/ChronoshiftDeploy.cs
@@ -6,20 +6,22 @@ namespace OpenRa.Game.Traits
{
class ChronoshiftDeploy : IOrder, ISpeedModifier, ITick, IPips
{
- public ChronoshiftDeploy(Actor self) { }
- int remainingChargeTime = 0; // How long until we can chronoshift again?
- int chargeTime = (int)(Rules.Aftermath.ChronoTankDuration * 60 * 25); // How long between shifts?
+ // Recharge logic
+ int chargeTick = 0; // How long until we can chronoshift again?
+ int chargeLength = (int)(Rules.Aftermath.ChronoTankDuration * 60 * 25); // How long between shifts?
+ public ChronoshiftDeploy(Actor self) { }
+
public void Tick(Actor self)
{
- if (remainingChargeTime > 0)
- remainingChargeTime--;
+ if (chargeTick > 0)
+ chargeTick--;
}
public Order IssueOrder(Actor self, int2 xy, MouseInput mi, Actor underCursor)
{
- if( mi.Button == MouseButton.Right && xy == self.Location && remainingChargeTime <= 0 )
- return new Order( "Deploy", self, null, int2.Zero, null );
+ if (mi.Button == MouseButton.Right && xy == self.Location && chargeTick <= 0)
+ return new Order("Deploy", self, null, int2.Zero, null);
return null;
}
@@ -28,7 +30,7 @@ namespace OpenRa.Game.Traits
{
if (order.OrderString == "Deploy")
{
- Game.controller.orderGenerator = new TeleportOrderGenerator(self);
+ Game.controller.orderGenerator = new ChronoshiftDestinationOrderGenerator(self);
return;
}
@@ -39,23 +41,26 @@ namespace OpenRa.Game.Traits
self.CancelActivity();
self.QueueActivity(new Activities.Teleport(order.TargetLocation));
Sound.Play("chrotnk1.aud");
- remainingChargeTime = chargeTime;
+ chargeTick = chargeLength;
+
+ foreach (var a in Game.world.Actors.Where(a => a.traits.Contains()))
+ a.traits.Get().DoChronoshift();
}
}
-
+
public float GetSpeedModifier()
{
// ARGH! You must not do this, it will desync!
- return (Game.controller.orderGenerator is TeleportOrderGenerator) ? 0f : 1f;
+ return (Game.controller.orderGenerator is ChronoshiftDestinationOrderGenerator) ? 0f : 1f;
}
-
+
// Display 5 pips indicating the current charge status
- public IEnumerable GetPips()
+ public IEnumerable GetPips(Actor self)
{
const int numPips = 5;
for (int i = 0; i < numPips; i++)
{
- if ((1 - remainingChargeTime * 1.0f / chargeTime) * numPips < i + 1)
+ if ((1 - chargeTick * 1.0f / chargeLength) * numPips < i + 1)
{
yield return PipType.Transparent;
continue;
diff --git a/OpenRa.Game/Traits/ChronoshiftPaletteEffect.cs b/OpenRa.Game/Traits/ChronoshiftPaletteEffect.cs
new file mode 100644
index 0000000000..36a4f0e40c
--- /dev/null
+++ b/OpenRa.Game/Traits/ChronoshiftPaletteEffect.cs
@@ -0,0 +1,40 @@
+using System.Drawing;
+using OpenRa.Game.Graphics;
+
+namespace OpenRa.Game.Traits
+{
+ class ChronoshiftPaletteEffect : IPaletteModifier, ITick
+ {
+ const int chronoEffectLength = 20;
+ int remainingFrames;
+
+ public ChronoshiftPaletteEffect(Actor self) { }
+
+ public void DoChronoshift()
+ {
+ remainingFrames = chronoEffectLength;
+ }
+
+ public void Tick(Actor self)
+ {
+ if (remainingFrames > 0)
+ remainingFrames--;
+ }
+
+ public void AdjustPalette(Bitmap b)
+ {
+ if (remainingFrames == 0)
+ return;
+
+ var frac = (float)remainingFrames / chronoEffectLength;
+ for( var y = 0; y < (int)PaletteType.Chrome; y++ )
+ for (var x = 0; x < 256; x++)
+ {
+ var orig = b.GetPixel(x, y);
+ var lum = (int)(255 * orig.GetBrightness());
+ var desat = Color.FromArgb(orig.A, lum, lum, lum);
+ b.SetPixel(x, y, Graphics.Util.Lerp(frac, orig, desat));
+ }
+ }
+ }
+}
diff --git a/OpenRa.Game/Traits/Chronoshiftable.cs b/OpenRa.Game/Traits/Chronoshiftable.cs
new file mode 100644
index 0000000000..bf97b15542
--- /dev/null
+++ b/OpenRa.Game/Traits/Chronoshiftable.cs
@@ -0,0 +1,78 @@
+using OpenRa.Game.Traits;
+using OpenRa.Game.Orders;
+using System.Collections.Generic;
+using System.Linq;
+using System.Drawing;
+
+namespace OpenRa.Game.Traits
+{
+ class Chronoshiftable : IOrder, ISpeedModifier, ITick, IChronoshiftable
+ {
+ // Return-to-sender logic
+ int2 chronoshiftOrigin;
+ int chronoshiftReturnTicks = 0;
+
+ public Chronoshiftable(Actor self) { }
+
+ public void Tick(Actor self)
+ {
+ if (chronoshiftReturnTicks <= 0)
+ return;
+
+ if (chronoshiftReturnTicks > 0)
+ chronoshiftReturnTicks--;
+
+ // Return to original location
+ if (chronoshiftReturnTicks == 0)
+ {
+ self.CancelActivity();
+ // Todo: need a new Teleport method that will move to the closest available cell
+ self.QueueActivity(new Activities.Teleport(chronoshiftOrigin));
+ }
+ }
+
+ public Order IssueOrder(Actor self, int2 xy, MouseInput mi, Actor underCursor)
+ {
+ return null; // Chronoshift order is issued through Chrome.
+ }
+
+ public void ResolveOrder(Actor self, Order order)
+ {
+ if (order.OrderString == "ChronosphereSelect")
+ {
+ Game.controller.orderGenerator = new ChronoshiftDestinationOrderGenerator(self);
+ }
+
+ var movement = self.traits.WithInterface().FirstOrDefault();
+ if (order.OrderString == "Chronoshift" && movement.CanEnterCell(order.TargetLocation))
+ {
+
+ // Set up return-to-sender info
+ chronoshiftOrigin = self.Location;
+ chronoshiftReturnTicks = (int)(Rules.General.ChronoDuration * 60 * 25);
+
+ // TODO: Kill cargo if Rules.General.ChronoKillCargo says so
+
+ // Set up the teleport
+ Game.controller.CancelInputMode();
+ self.CancelActivity();
+ self.QueueActivity(new Activities.Teleport(order.TargetLocation));
+ Sound.Play("chrono2.aud");
+
+ foreach (var a in Game.world.Actors.Where(a => a.traits.Contains()))
+ a.traits.Get().DoChronoshift();
+
+ // Play chronosphere active anim
+ var chronosphere = Game.world.Actors.Where(a => a.Owner == order.Subject.Owner && a.traits.Contains()).FirstOrDefault();
+ if (chronosphere != null)
+ chronosphere.traits.Get().PlayCustomAnim(chronosphere, "active");
+ }
+ }
+
+ public float GetSpeedModifier()
+ {
+ // ARGH! You must not do this, it will desync!
+ return (Game.controller.orderGenerator is ChronoshiftDestinationOrderGenerator) ? 0f : 1f;
+ }
+ }
+}
diff --git a/OpenRa.Game/Traits/Chronosphere.cs b/OpenRa.Game/Traits/Chronosphere.cs
new file mode 100644
index 0000000000..cad8a1cf13
--- /dev/null
+++ b/OpenRa.Game/Traits/Chronosphere.cs
@@ -0,0 +1,12 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace OpenRa.Game.Traits
+{
+ class Chronosphere
+ {
+ public Chronosphere(Actor self) { }
+ }
+}
diff --git a/OpenRa.Game/Traits/DemoTruck.cs b/OpenRa.Game/Traits/DemoTruck.cs
new file mode 100644
index 0000000000..add4849cff
--- /dev/null
+++ b/OpenRa.Game/Traits/DemoTruck.cs
@@ -0,0 +1,59 @@
+using OpenRa.Game.Effects;
+using OpenRa.Game.Traits;
+using System.Collections.Generic;
+using System.Linq;
+using OpenRa.Game.Orders;
+
+namespace OpenRa.Game.Traits
+{
+ class DemoTruck : IOrder, ISpeedModifier, INotifyDamage, IChronoshiftable
+ {
+ readonly Actor self;
+ public DemoTruck(Actor self)
+ {
+ this.self = self;
+ }
+
+ // Fire primary on Chronoshift
+ public Order IssueOrder(Actor self, int2 xy, MouseInput mi, Actor underCursor)
+ {
+ return null; // Chronoshift order is issued through Chrome.
+ }
+
+ public void ResolveOrder(Actor self, Order order)
+ {
+ if (order.OrderString == "ChronosphereSelect")
+ Game.controller.orderGenerator = new ChronoshiftDestinationOrderGenerator(self);
+
+ var movement = self.traits.WithInterface().FirstOrDefault();
+ var chronosphere = Game.world.Actors.Where(a => a.Owner == order.Subject.Owner && a.traits.Contains()).FirstOrDefault();
+ if (order.OrderString == "Chronoshift" && movement.CanEnterCell(order.TargetLocation))
+ self.InflictDamage(chronosphere, self.Health, Rules.WarheadInfo["Super"]);
+ }
+
+ // Fire primary on death
+ public void Damaged(Actor self, AttackInfo e)
+ {
+ if (e.DamageState == DamageState.Dead)
+ Detonate(self, e.Attacker);
+ }
+
+ public void Detonate(Actor self, Actor detonatedBy)
+ {
+ self.InflictDamage(detonatedBy, self.Health, Rules.WarheadInfo["Super"]);
+ var unit = self.traits.GetOrDefault();
+ var altitude = unit != null ? unit.Altitude : 0;
+ int2 detonateLocation = self.CenterLocation.ToInt2();
+
+ Game.world.AddFrameEndTask(
+ w => w.Add(new Bullet(self.Info.Primary, detonatedBy.Owner, detonatedBy,
+ detonateLocation, detonateLocation, altitude, altitude)));
+ }
+
+ public float GetSpeedModifier()
+ {
+ // ARGH! You must not do this, it will desync!
+ return (Game.controller.orderGenerator is ChronoshiftDestinationOrderGenerator) ? 0f : 1f;
+ }
+ }
+}
diff --git a/OpenRa.Game/Traits/Harvester.cs b/OpenRa.Game/Traits/Harvester.cs
index ef535cd876..f68f52f008 100644
--- a/OpenRa.Game/Traits/Harvester.cs
+++ b/OpenRa.Game/Traits/Harvester.cs
@@ -57,7 +57,7 @@ namespace OpenRa.Game.Traits
}
}
- public IEnumerable GetPips()
+ public IEnumerable GetPips(Actor self)
{
const int numPips = 7;
for (int i = 0; i < numPips; i++)
diff --git a/OpenRa.Game/Traits/LimitedAmmo.cs b/OpenRa.Game/Traits/LimitedAmmo.cs
index 2a1a938450..b5c18c55a2 100644
--- a/OpenRa.Game/Traits/LimitedAmmo.cs
+++ b/OpenRa.Game/Traits/LimitedAmmo.cs
@@ -23,7 +23,7 @@ namespace OpenRa.Game.Traits
public void Attacking(Actor self) { --ammo; }
- public IEnumerable GetPips()
+ public IEnumerable GetPips(Actor self)
{
return Graphics.Util.MakeArray(self.Info.Ammo,
i => ammo > i ? PipType.Green : PipType.Transparent);
diff --git a/OpenRa.Game/Traits/Mobile.cs b/OpenRa.Game/Traits/Mobile.cs
index 4bf3a76e1f..e917df2610 100644
--- a/OpenRa.Game/Traits/Mobile.cs
+++ b/OpenRa.Game/Traits/Mobile.cs
@@ -18,7 +18,16 @@ namespace OpenRa.Game.Traits
public int2 toCell
{
get { return self.Location; }
- set { Game.UnitInfluence.Remove(self, this); self.Location = value; Game.UnitInfluence.Add(self, this); }
+ set
+ {
+ if (self.Location != value)
+ {
+ Game.UnitInfluence.Remove(self, this);
+ self.Location = value;
+ self.Owner.Shroud.Explore(self);
+ }
+ Game.UnitInfluence.Add(self, this);
+ }
}
public Mobile(Actor self)
diff --git a/OpenRa.Game/Traits/Passenger.cs b/OpenRa.Game/Traits/Passenger.cs
new file mode 100644
index 0000000000..29521ecb09
--- /dev/null
+++ b/OpenRa.Game/Traits/Passenger.cs
@@ -0,0 +1,42 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using OpenRa.Game.Traits.Activities;
+
+namespace OpenRa.Game.Traits
+{
+ class Passenger : IOrder
+ {
+ public Passenger(Actor self) { }
+
+ public Order IssueOrder(Actor self, int2 xy, MouseInput mi, Actor underCursor)
+ {
+ if (mi.Button != MouseButton.Right)
+ return null;
+
+ if (underCursor == null || underCursor.Owner != self.Owner)
+ return null;
+
+ var cargo = underCursor.traits.GetOrDefault();
+ if (cargo == null || cargo.IsFull(underCursor))
+ return null;
+
+ var umt = self.traits.WithInterface().First().GetMovementType();
+ if (!underCursor.Info.PassengerTypes.Contains(umt))
+ return null;
+
+ return new Order("EnterTransport", self, underCursor, int2.Zero, null);
+ }
+
+ public void ResolveOrder(Actor self, Order order)
+ {
+ if (order.OrderString == "EnterTransport")
+ {
+ self.CancelActivity();
+ self.QueueActivity(new Move(order.TargetActor.Location, 1));
+ self.QueueActivity(new EnterTransport(self, order.TargetActor));
+ }
+ }
+ }
+}
diff --git a/OpenRa.Game/Traits/ProvidesRadar.cs b/OpenRa.Game/Traits/ProvidesRadar.cs
new file mode 100644
index 0000000000..41c3ced78c
--- /dev/null
+++ b/OpenRa.Game/Traits/ProvidesRadar.cs
@@ -0,0 +1,28 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace OpenRa.Game.Traits
+{
+ class ProvidesRadar
+ {
+ Actor self;
+ public ProvidesRadar(Actor self)
+ {
+ this.self = self;
+ }
+
+ public bool IsActive()
+ {
+ // TODO: Check for nearby MRJ
+
+ // Check if powered
+ var b = self.traits.Get();
+ if (b != null && b.InsuffientPower())
+ return false;
+
+ return true;
+ }
+ }
+}
diff --git a/OpenRa.Game/Traits/StoresOre.cs b/OpenRa.Game/Traits/StoresOre.cs
index cd2d5210a8..25d16af2d0 100644
--- a/OpenRa.Game/Traits/StoresOre.cs
+++ b/OpenRa.Game/Traits/StoresOre.cs
@@ -11,7 +11,7 @@ namespace OpenRa.Game.Traits
this.self = self;
}
- public IEnumerable GetPips()
+ public IEnumerable GetPips(Actor self)
{
for (int i = 0; i < self.Info.OrePips; i++)
{
diff --git a/OpenRa.Game/Traits/TraitsInterfaces.cs b/OpenRa.Game/Traits/TraitsInterfaces.cs
index e66744cb4e..de5bc49c19 100644
--- a/OpenRa.Game/Traits/TraitsInterfaces.cs
+++ b/OpenRa.Game/Traits/TraitsInterfaces.cs
@@ -29,7 +29,7 @@ namespace OpenRa.Game.Traits
interface IDamageModifier { float GetDamageModifier(); }
interface ISpeedModifier { float GetSpeedModifier(); }
interface IPaletteModifier { void AdjustPalette(Bitmap b); }
- interface IPips { IEnumerable GetPips(); }
+ interface IPips { IEnumerable GetPips(Actor self); }
interface ITags { IEnumerable GetTags(); }
interface IMovement
{
@@ -43,7 +43,7 @@ namespace OpenRa.Game.Traits
bool IsCrushableBy(UnitMovementType umt, Player player);
bool IsPathableCrush(UnitMovementType umt, Player player);
}
-
+ interface IChronoshiftable{}
struct Renderable
{
public readonly Sprite Sprite;
diff --git a/OpenRa.Game/World.cs b/OpenRa.Game/World.cs
index dc55e86213..f8db600406 100644
--- a/OpenRa.Game/World.cs
+++ b/OpenRa.Game/World.cs
@@ -10,8 +10,19 @@ namespace OpenRa.Game
List effects = new List();
List> frameEndActions = new List>();
- public void Add(Actor a) { actors.Add(a); ActorAdded(a); }
- public void Remove(Actor a) { actors.Remove(a); ActorRemoved(a); }
+ public void Add(Actor a)
+ {
+ a.IsInWorld = true;
+ actors.Add(a);
+ ActorAdded(a);
+ }
+
+ public void Remove(Actor a)
+ {
+ a.IsInWorld = false;
+ actors.Remove(a);
+ ActorRemoved(a);
+ }
public void Add(IEffect b) { effects.Add(b); }
public void Remove(IEffect b) { effects.Remove(b); }
diff --git a/SequenceEditor/Program.cs b/SequenceEditor/Program.cs
index cdef4b4285..1173636ce0 100644
--- a/SequenceEditor/Program.cs
+++ b/SequenceEditor/Program.cs
@@ -83,7 +83,8 @@ namespace SequenceEditor
Doc = new XmlDocument();
Doc.Load(XmlFilename);
- Pal = new Palette(FileSystem.Open("temperat.pal"));
+ var tempPal = new Palette(FileSystem.Open("temperat.pal"));
+ Pal = tempPal; //new Palette(tempPal, new ShroudPaletteRemap());
UnitName = args.FirstOrDefault( x => !x.EndsWith(".xml") );
if (UnitName == null)
diff --git a/aftermathUnits.ini b/aftermathUnits.ini
index b0599a1ccd..d352dc379c 100755
--- a/aftermathUnits.ini
+++ b/aftermathUnits.ini
@@ -7,28 +7,28 @@ QTNK
[STNK]
Description=Stealth Tank
-Traits=Unit, Mobile, Turreted, AttackTurreted, RenderUnitTurreted, Cloak
+Traits=Unit, Mobile, Turreted, AttackTurreted, RenderUnitTurreted, Cloak, Chronoshiftable
Recoil=2
Voice=VehicleVoice
[TTNK]
Description=Tesla Tank
-Traits=Unit, Mobile, AttackBase, RenderUnitSpinner
+Traits=Unit, Mobile, AttackBase, RenderUnitSpinner, Chronoshiftable
Voice=VehicleVoice
[CTNK]
Description=Chrono Tank
-Traits=Unit, Mobile, AttackBase, RenderUnit, ChronoshiftDeploy
+Traits=Unit, Mobile, AttackBase, RenderUnit, ChronoshiftDeploy, Chronoshiftable
Voice=VehicleVoice
[DTRK]
Description=Demo Truck
-Traits=Unit, Mobile, AttackBase, RenderUnit
+Traits=Unit, Mobile, AttackBase, RenderUnit, DemoTruck
Voice=VehicleVoice
[QTNK]
Description=M.A.D. Tank
-Traits=Unit, Mobile, RenderUnit
+Traits=Unit, Mobile, RenderUnit, Chronoshiftable
Voice=VehicleVoice
@@ -42,7 +42,7 @@ MSUB
Description=Missile Submarine
WaterBound=yes
BuiltAt=spen
-Traits=Unit, Mobile, AttackBase, RenderUnit, Submarine
+Traits=Unit, Mobile, AttackBase, RenderUnit, Submarine, Chronoshiftable
FireDelay=2
diff --git a/doc/progress.txt b/doc/progress.txt
index bcc268c1fb..cd612fc2cd 100644
--- a/doc/progress.txt
+++ b/doc/progress.txt
@@ -21,7 +21,7 @@ All tracked vehicles
Light vehicles
V2RL Works
-APC Cargo doesn't work
+APC Works
MNLY Works
MGG No gap
MRJ No radar
@@ -31,8 +31,7 @@ HARV Works
ARTY Works
Helicopters
- - Return to base after attack doesnt work
-TRAN Cargo doesn't work
+TRAN Works
HELI Works
HIND Works
@@ -47,5 +46,5 @@ CA Works
SS Works
DD depth charges don't work
PT depth charges don't work
-LST Cargo doesn't work
+LST Works
diff --git a/sequences.xml b/sequences.xml
index 18ad5e5e17..9e1f0e0ef5 100644
--- a/sequences.xml
+++ b/sequences.xml
@@ -283,6 +283,8 @@
+
+
@@ -368,7 +370,7 @@
-
+
@@ -376,14 +378,15 @@
-
+
+
-
+
@@ -393,13 +396,19 @@
-
-
+
+
+
+
+
+
+
+
@@ -535,6 +544,8 @@
+
+
@@ -571,6 +582,8 @@
+
+
@@ -1007,8 +1020,8 @@
-
-
-
+
+
+
\ No newline at end of file
diff --git a/session.ini b/session.ini
index e78029b52a..d63ac62a10 100644
--- a/session.ini
+++ b/session.ini
@@ -8,4 +8,6 @@ s0=Multi0,mcv,600,2841,0,Guard,None
s1=Multi2,mcv,600,12445,0,Guard,None
s2=Multi1,mcv,600,12505,0,Guard,None
;s2=Multi1,e3,600,12505,0,Guard,None
-s3=Multi3,mcv,600,2910,0,Guard,None
\ No newline at end of file
+s3=Multi3,mcv,600,2910,0,Guard,None
+;s4=Multi1,ctnk,600,12506,Gaurd,None
+s5=Multi1,apc,600,12510,Gaurd,None
\ No newline at end of file
diff --git a/settings-netplay-local-aftermath.ini b/settings-netplay-local-aftermath.ini
new file mode 100644
index 0000000000..fef7a12f8c
--- /dev/null
+++ b/settings-netplay-local-aftermath.ini
@@ -0,0 +1,4 @@
+[Settings]
+NetworkHost=localhost
+NetworkPort=1234
+UseAftermath=yes
\ No newline at end of file
diff --git a/settings-netplay-local.ini b/settings-netplay-local.ini
new file mode 100644
index 0000000000..7d13c14222
--- /dev/null
+++ b/settings-netplay-local.ini
@@ -0,0 +1,3 @@
+[Settings]
+NetworkHost=localhost
+NetworkPort=1234
\ No newline at end of file
diff --git a/units.ini b/units.ini
index 335bc7017f..38418d8b72 100644
--- a/units.ini
+++ b/units.ini
@@ -16,89 +16,91 @@ MNLY.AT
[V2RL]
Description=V2 Rocket
-Traits=Unit, Mobile, AttackBase, RenderUnitReload, AutoTarget, Repairable
+Traits=Unit, Mobile, AttackBase, RenderUnitReload, AutoTarget, Repairable, Chronoshiftable, Passenger
Voice=VehicleVoice
LongDesc=Long-range rocket artillery.\n Strong vs Infantry, Buildings\n Weak vs Tanks, Aircraft
[1TNK]
Description=Light Tank
-Traits=Unit, Mobile, Turreted, AttackTurreted, RenderUnitTurreted, AutoTarget, Repairable
+Traits=Unit, Mobile, Turreted, AttackTurreted, RenderUnitTurreted, AutoTarget, Repairable, Chronoshiftable, Passenger
Recoil=2
Voice=VehicleVoice
LongDesc=Light Tank, good for scouting.\n Strong vs Light Vehicles\n Weak vs Tanks, Aircraft
[2TNK]
Description=Medium Tank
-Traits=Unit, Mobile, Turreted, AttackTurreted, RenderUnitTurreted, AutoTarget, Repairable
+Traits=Unit, Mobile, Turreted, AttackTurreted, RenderUnitTurreted, AutoTarget, Repairable, Chronoshiftable, Passenger
Recoil=3
Voice=VehicleVoice
LongDesc=Allied Main Battle Tank.\n Strong vs Tanks, Light Vehicles\n Weak vs Infantry, Aircraft
[3TNK]
Description=Heavy Tank
-Traits=Unit, Mobile, Turreted, AttackTurreted, RenderUnitTurreted, AutoTarget, Repairable
+Traits=Unit, Mobile, Turreted, AttackTurreted, RenderUnitTurreted, AutoTarget, Repairable, Chronoshiftable, Passenger
Recoil=3
Voice=VehicleVoice
LongDesc=Soviet Main Battle Tank, with dual cannons\n Strong vs Tanks, Light Vehicles\n Weak vs Infantry, Aircraft
[4TNK]
Description=Mammoth Tank
-Traits=Unit, Mobile, Turreted, AttackTurreted, RenderUnitTurreted, AutoTarget, Repairable
+Traits=Unit, Mobile, Turreted, AttackTurreted, RenderUnitTurreted, AutoTarget, Repairable, Chronoshiftable, Passenger
Voice=VehicleVoice
LongDesc=Big and slow tank, with anti-air capability.\n Strong vs Tanks, Aircraft\n Weak vs Infantry
[ARTY]
Description=Artillery
-Traits=Unit, Mobile, AttackBase, RenderUnit, Explodes, AutoTarget, Repairable
+Traits=Unit, Mobile, AttackBase, RenderUnit, Explodes, AutoTarget, Repairable, Chronoshiftable, Passenger
Voice=VehicleVoice
LongDesc=Long-range artillery.\n Strong vs Infantry, Buildings\n Weak vs Tanks, Aircraft
[JEEP]
Description=Ranger
-Traits=Unit, Mobile, Turreted, AttackTurreted, RenderUnitTurreted, AutoTarget, Repairable
+Traits=Unit, Mobile, Turreted, AttackTurreted, RenderUnitTurreted, AutoTarget, Repairable, Chronoshiftable, Passenger
PrimaryOffset=0,0,0,-2
MuzzleFlash=yes
Voice=VehicleVoice
LongDesc=Fast scout & anti-infantry vehicle.\n Strong vs Infantry\n Weak vs Tanks, Aircraft
[APC]
Description=Armored Personnel Carrier
-Traits=Unit, Mobile, AttackBase, RenderUnitMuzzleFlash, AutoTarget, Repairable
+Traits=Unit, Mobile, AttackBase, RenderUnitMuzzleFlash, AutoTarget, Repairable, Chronoshiftable, Cargo, Passenger
PrimaryOffset=0,0,0,-4
MuzzleFlash=yes
Voice=VehicleVoice
LongDesc=Tough infantry transport.\n Strong vs Infantry, Light Vehicles\n Weak vs Tanks, Aircraft
+UnloadFacing=220
+PassengerTypes=Foot
;; non-combat vehicles
[MRJ]
Description=Radar Jammer
-Traits=Unit, Mobile, RenderUnitSpinner, Repairable
+Traits=Unit, Mobile, RenderUnitSpinner, Repairable, Chronoshiftable, Passenger
PrimaryOffset=0,4,0,-6
SelectionPriority=3
Voice=VehicleVoice
LongDesc=Hides nearby units on the enemy's minimap.\n Unarmed
[MGG]
Description=Mobile Gap Generator
-Traits=Unit, Mobile, RenderUnitSpinner, Repairable
+Traits=Unit, Mobile, RenderUnitSpinner, Repairable, Chronoshiftable, Passenger
PrimaryOffset=0,6,0,-3
SelectionPriority=3
Voice=VehicleVoice
LongDesc=Regenerates Fog of War in a small area \naround the unit.\n Unarmed
[HARV]
Description=Ore Truck
-Traits=Harvester, Unit, Mobile, RenderUnit, Repairable
+Traits=Harvester, Unit, Mobile, RenderUnit, Repairable, Chronoshiftable, Passenger
SelectionPriority=7
Voice=VehicleVoice
LongDesc=Collects Ore and Gems for processing.\n Unarmed
[MCV]
Description=Mobile Construction Vehicle
-Traits=Unit, Mobile, McvDeploy, RenderUnit, Repairable
+Traits=Unit, Mobile, McvDeploy, RenderUnit, Repairable, Chronoshiftable, Passenger
SelectionPriority=3
Voice=VehicleVoice
LongDesc=Deploys into another Construction Yard.\n Unarmed
[MNLY.AP]
Description=Minelayer (Anti-Personnel)
-Traits=Unit, Mobile, RenderUnit, Minelayer, MineImmune, Repairable, LimitedAmmo
+Traits=Unit, Mobile, RenderUnit, Minelayer, MineImmune, Repairable, LimitedAmmo, Chronoshiftable, Passenger
Voice=VehicleVoice
LongDesc=Lays mines to destroy unwary enemy units.\n Unarmed
Primary=MINP ;; temporary hack
[MNLY.AT]
Description=Minelayer (Anti-Tank)
-Traits=Unit, Mobile, RenderUnit, Minelayer, MineImmune, Repairable, LimitedAmmo
+Traits=Unit, Mobile, RenderUnit, Minelayer, MineImmune, Repairable, LimitedAmmo, Chronoshiftable, Passenger
Voice=VehicleVoice
LongDesc=Lays mines to destroy unwary enemy units.\n Unarmed
Primary=MINV ;; temporary hack
@@ -116,21 +118,21 @@ PT
Description=Submarine
WaterBound=yes
BuiltAt=spen
-Traits=Unit, Mobile, RenderUnit, Submarine, AttackBase
+Traits=Unit, Mobile, RenderUnit, Submarine, AttackBase, Chronoshiftable
FireDelay=2
LongDesc=Submerged anti-ship unit armed with \ntorpedoes.\n Strong vs Ships\n Weak vs Everything\n Special Ability: Submerge
[DD]
Description=Destroyer
WaterBound=yes
BuiltAt=syrd
-Traits=Unit, Mobile, Turreted, AttackTurreted, RenderUnitTurreted, AutoTarget
+Traits=Unit, Mobile, Turreted, AttackTurreted, RenderUnitTurreted, AutoTarget, Chronoshiftable
PrimaryOffset=0,-8,0,-3
LongDesc=Fast multi-role ship. \n Strong vs Submarines, Aircraft\n Weak vs Infantry, Tanks
[CA]
Description=Cruiser
WaterBound=yes
BuiltAt=syrd
-Traits=Unit, Mobile, Turreted, AttackTurreted, RenderUnitTurreted, AutoTarget
+Traits=Unit, Mobile, Turreted, AttackTurreted, RenderUnitTurreted, AutoTarget, Chronoshiftable
PrimaryOffset=0,17,0,-2
SecondaryOffset=0,-17,0,-2
LongDesc=Very slow long-range ship. \n Strong vs Buildings\n Weak vs Ships, Submarines
@@ -138,13 +140,14 @@ Recoil=3
[LST]
Description=Transport
WaterBound=yes
-Traits=Unit, Mobile, RenderUnit
+Traits=Unit, Mobile, RenderUnit, Cargo
LongDesc=General-purpose naval transport.\nCan carry infantry and tanks.\n Unarmed
+PassengerTypes=Foot,Wheel,Track
[PT]
Description=Gunboat
WaterBound=yes
BuiltAt=syrd
-Traits=Unit, Mobile, Turreted, AttackTurreted, RenderUnitTurreted, AutoTarget
+Traits=Unit, Mobile, Turreted, AttackTurreted, RenderUnitTurreted, AutoTarget, Chronoshiftable
PrimaryOffset=0,-6,0,-1
LongDesc=Light scout & support ship. \n Strong vs Ships, Submarines\n Weak vs Aircraft
@@ -179,9 +182,10 @@ Description=Transport Helicopter
RotorOffset=0,14,0,-4
RotorOffset2=0,-14,0,-2
BuiltAt=hpad
-Traits=Unit, Helicopter, RenderUnitRotor, WithShadow
+Traits=Unit, Helicopter, RenderUnitRotor, WithShadow, Cargo
InitialFacing=20
LongDesc=Fast Infantry Transport Helicopter.\n Unarmed
+PassengerTypes=Foot
[HELI]
Description=Longbow
BuiltAt=hpad
@@ -289,7 +293,7 @@ SelectionPriority=3
LongDesc=Makes a group of units invulnerable for a \nshort time.\n Special Ability: Invulnerability
[PDOX]
Description=Chronosphere
-Traits=Building, RenderBuilding
+Traits=Building, RenderBuilding, Chronosphere
Dimensions=2,2
Footprint=xx xx
SelectionPriority=3
@@ -407,7 +411,7 @@ SpawnOffset=0,-4
LongDesc=Produces and reloads helicopters
[DOME]
Description=Radar Dome
-Traits=Building, RenderBuilding
+Traits=Building, RenderBuilding, ProvidesRadar
Dimensions=2,2
Footprint=xx xx
SelectionPriority=3
@@ -544,62 +548,62 @@ MEDI
Description=Attack Dog
BuiltAt=KENN
Voice=DogVoice
-Traits=Unit, Mobile, RenderInfantry ;; AttackBase, SquishByTank, AutoTarget, dog??
+Traits=Unit, Mobile, RenderInfantry, Passenger ;; AttackBase, SquishByTank, AutoTarget, dog??
LongDesc=Anti-infantry unit. Not fooled by the \nSpy's disguise.\n Strong vs Infantry\n Weak vs Vehicles
SelectionSize=12,17,-1,-4
[E1]
Description=Rifle Infantry
-Traits=Unit, Mobile, RenderInfantry, AttackBase, TakeCover, SquishByTank, AutoTarget
+Traits=Unit, Mobile, RenderInfantry, AttackBase, TakeCover, SquishByTank, AutoTarget, Passenger
LongDesc=General-purpose infantry. Strong vs Infantry\n Weak vs Vehicles
SelectionSize=12,17,0,-9
[E2]
Description=Grenadier
-Traits=Unit, Mobile, RenderInfantry, AttackBase, TakeCover, SquishByTank, AutoTarget
+Traits=Unit, Mobile, RenderInfantry, AttackBase, TakeCover, SquishByTank, AutoTarget, Passenger
FireDelay=15
PrimaryOffset=0,0,0,-13
LongDesc=Infantry armed with grenades. \n Strong vs Buildings, Infantry\n Weak vs Vehicles
SelectionSize=12,17,0,-9
[E3]
Description=Rocket Soldier
-Traits=Unit, Mobile, RenderInfantry, AttackBase, TakeCover, SquishByTank, AutoTarget
+Traits=Unit, Mobile, RenderInfantry, AttackBase, TakeCover, SquishByTank, AutoTarget, Passenger
PrimaryOffset=0,0,0,-13
LongDesc=Anti-tank/Anti-aircraft infantry.\n Strong vs Tanks, Aircraft\n Weak vs Infantry
SelectionSize=12,17,0,-9
[E4]
Description=Flamethrower
-Traits=Unit, Mobile, RenderInfantry, AttackBase, TakeCover, SquishByTank, AutoTarget
+Traits=Unit, Mobile, RenderInfantry, AttackBase, TakeCover, SquishByTank, AutoTarget, Passenger
FireDelay=8
PrimaryOffset=0,0,0,-7
LongDesc=Advanced Anti-infantry unit.\n Strong vs Infantry, Buildings\n Weak vs Vehicles
SelectionSize=12,17,0,-9
[E6]
Description=Engineer
-Traits=Unit, Mobile, EngineerCapture, RenderInfantry, TakeCover, SquishByTank
+Traits=Unit, Mobile, EngineerCapture, RenderInfantry, TakeCover, SquishByTank, Passenger
Voice=EngineerVoice
LongDesc=Infiltrates and captures enemy structures.\n Strong vs Nothing\n Weak vs Everything
SelectionSize=12,17,0,-9
[SPY]
Description=Spy
Voice=SpyVoice
-Traits=Unit, Mobile, RenderInfantry, TakeCover, SquishByTank
+Traits=Unit, Mobile, RenderInfantry, TakeCover, SquishByTank, Passenger
LongDesc=Infiltrates enemy structures to gather \nintelligence. Exact effect depends on the \nbuilding infiltrated.\n Strong vs Nothing\n Weak vs Everything\n Special Ability: Disguised
SelectionSize=12,17,0,-9
[THF]
Description=Thief
Voice=ThiefVoice
-Traits=Unit, Mobile, RenderInfantry, TakeCover, SquishByTank
+Traits=Unit, Mobile, RenderInfantry, TakeCover, SquishByTank, Passenger
LongDesc=Infiltrates enemy refineries & \nsilos, and steals money stored there.\n Unarmed
SelectionSize=12,17,0,-9
[E7]
Description=Tanya
Voice=TanyaVoice
-Traits=Unit, Mobile, RenderInfantry, C4Demolition, AttackBase, TakeCover, SquishByTank, AutoTarget
+Traits=Unit, Mobile, RenderInfantry, C4Demolition, AttackBase, TakeCover, SquishByTank, AutoTarget, Passenger
LongDesc=Elite commando infantry, armed with \ndual pistols and C4.\n Strong vs Infantry, Buildings\n Weak vs Vehicles\n Special Ability: Destroy Building with C4
SelectionSize=12,17,0,-9
[MEDI]
Description=Medic
Voice=MedicVoice
-Traits=Unit, Mobile, RenderInfantry, AutoHeal, AttackBase, TakeCover, SquishByTank
+Traits=Unit, Mobile, RenderInfantry, AutoHeal, AttackBase, TakeCover, SquishByTank, Passenger
LongDesc=Heals nearby infantry.\n Strong vs Nothing\n Weak vs Everything
SelectionSize=12,17,0,-9