merged pchote/master attack-omni bridge-setup-as-trait

This commit is contained in:
Chris Forbes
2010-02-09 13:31:26 +13:00
35 changed files with 361 additions and 588 deletions

1
.gitignore vendored
View File

@@ -6,6 +6,7 @@ obj
*.vcproj*
*.suo
*.user
*.sln.cache
mods/*/*.dll
# Red Alert binary files

View File

@@ -24,7 +24,7 @@ namespace OpenRA.Server
const int DownloadChunkInterval = 20000;
const int DownloadChunkSize = 16384;
public static void Main(string[] args)
public static int Main(string[] args)
{
if (args.Length > 0) defaultMods = args;
lobbyInfo = new Session();
@@ -39,10 +39,10 @@ namespace OpenRA.Server
listener.Start();
Console.WriteLine("Server started.");
}
catch (Exception e)
catch (Exception)
{
Console.WriteLine("Server failed to start.");
Environment.Exit(1);
return 1;
}
for (; ; )

View File

@@ -2,44 +2,17 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using IjwFramework.Collections;
using System;
namespace OpenRa.FileFormats
{
public static class FileSystem
{
static List<IFolder> mountedFolders = new List<IFolder>();
static List<IFolder> temporaryMounts = new List<IFolder>();
static Cache<uint, List<IFolder>> allFiles = new Cache<uint, List<IFolder>>( _ => new List<IFolder>() );
static Cache<uint, List<IFolder>> allTemporaryFiles = new Cache<uint, List<IFolder>>( _ => new List<IFolder>() );
public static void MountDefaultPackages()
{
FileSystem.Mount(new Folder("./"));
if( FileSystem.Exists( "main.mix" ) )
FileSystem.Mount( new Package( "main.mix" ) );
FileSystem.Mount( new Package( "redalert.mix" ) );
FileSystem.Mount( new Package( "conquer.mix" ) );
FileSystem.Mount( new Package( "hires.mix" ) );
FileSystem.Mount( new Package( "general.mix" ) );
FileSystem.Mount( new Package( "local.mix" ) );
FileSystem.Mount( new Package( "sounds.mix" ) );
FileSystem.Mount( new Package( "speech.mix" ) );
FileSystem.Mount( new Package( "allies.mix" ) );
FileSystem.Mount( new Package( "russian.mix" ) );
FileSystem.Mount( new Package( "temperat.mix" ) );
FileSystem.Mount( new Package( "snow.mix" ) );
FileSystem.Mount( new Package( "interior.mix" ) );
}
public static void MountAftermathPackages()
{
FileSystem.Mount( new Package( "expand2.mix" ) );
FileSystem.Mount( new Package( "hires1.mix" ) );
}
public static void Mount(IFolder folder)
static void MountInner(IFolder folder)
{
mountedFolders.Add(folder);
@@ -51,25 +24,27 @@ namespace OpenRa.FileFormats
}
}
public static void MountTemporary(IFolder folder)
public static void Mount(string name)
{
mountedFolders.Add(folder);
temporaryMounts.Add(folder);
name = name.ToLowerInvariant();
var optional = name.StartsWith("~");
if (optional) name = name.Substring(1);
foreach( var hash in folder.AllFileHashes() )
{
var l = allTemporaryFiles[hash];
if( !l.Contains( folder ) )
l.Add( folder );
}
var a = name.EndsWith(".mix")
? (Action)(() => FileSystem.MountInner(new Package(name)))
: () => FileSystem.MountInner(new Folder(name));
if (optional)
try { a(); }
catch { }
else
a();
}
public static void UnmountTemporaryPackages()
public static void UnmountAll()
{
mountedFolders.RemoveAll(f => temporaryMounts.Contains(f));
temporaryMounts.Clear();
allTemporaryFiles = new Cache<uint, List<IFolder>>( _ => new List<IFolder>() );
mountedFolders.Clear();
allFiles = new Cache<uint, List<IFolder>>( _ => new List<IFolder>() );
}
static Stream GetFromCache( Cache<uint, List<IFolder>> index, string filename )
@@ -87,8 +62,7 @@ namespace OpenRa.FileFormats
{
if( filename.IndexOfAny( new char[] { '/', '\\' } ) == -1 )
{
var ret = GetFromCache( allFiles, filename )
?? GetFromCache( allTemporaryFiles, filename );
var ret = GetFromCache( allFiles, filename );
if( ret != null )
return ret;
}
@@ -109,8 +83,7 @@ namespace OpenRa.FileFormats
{
foreach( var ext in exts )
{
var s = GetFromCache( allFiles, filename + ext )
?? GetFromCache( allTemporaryFiles, filename + ext );
var s = GetFromCache( allFiles, filename + ext );
if( s != null )
return s;
}

View File

@@ -356,10 +356,8 @@ namespace OpenRa
while (!PaletteAvailable(newIndex) && newIndex != (int)Game.world.LocalPlayer.PaletteIndex)
newIndex = (newIndex + d) % Player.PlayerColors.Count();
Game.world.Minimap.InvalidateSpawnPoints();
Game.IssueOrder(
Order.Chat("/pal " + newIndex));
}
void CycleRace(bool left)
@@ -1193,5 +1191,13 @@ namespace OpenRa
renderer.DrawText(sp.Info.LongDesc.Replace("\\n", "\n"), pos, Color.White);
}
}
public void SetCurrentTab(string produces)
{
if (!paletteOpen)
paletteAnimating = true;
paletteOpen = true;
currentTab = produces;
}
}
}

View File

@@ -13,6 +13,7 @@ namespace OpenRa
public class Controller : IHandleInput
{
public IOrderGenerator orderGenerator;
public Selection selection = new Selection();
readonly Func<Modifiers> GetModifierKeys;
@@ -22,10 +23,7 @@ namespace OpenRa
CancelInputMode();
}
public void CancelInputMode()
{
orderGenerator = new UnitOrderGenerator(new Actor[] { });
}
public void CancelInputMode() { orderGenerator = new UnitOrderGenerator(); }
public bool ToggleInputMode<T>() where T : IOrderGenerator, new()
{
@@ -83,7 +81,7 @@ namespace OpenRa
if (orderGenerator is UnitOrderGenerator)
{
var newSelection = world.SelectActorsInBox(Game.CellSize * dragStart, Game.CellSize * xy);
CombineSelection(world, newSelection, mi.Modifiers.HasModifier(Modifiers.Shift), dragStart == xy);
selection.Combine(world, newSelection, mi.Modifiers.HasModifier(Modifiers.Shift), dragStart == xy);
}
dragStart = dragEnd = xy;
@@ -98,29 +96,6 @@ namespace OpenRa
return true;
}
void CombineSelection(World world, IEnumerable<Actor> newSelection, bool isCombine, bool isClick)
{
var oldSelection = (orderGenerator is UnitOrderGenerator)
? (orderGenerator as UnitOrderGenerator).selection : new Actor[] { }.AsEnumerable();
if (isClick)
{
var adjNewSelection = newSelection.Take(1); /* todo: select BEST, not FIRST */
orderGenerator = new UnitOrderGenerator(isCombine
? oldSelection.SymmetricDifference(adjNewSelection) : adjNewSelection);
}
else
orderGenerator = new UnitOrderGenerator(isCombine
? oldSelection.Union(newSelection) : newSelection);
var voicedUnit = ((UnitOrderGenerator)orderGenerator).selection
.Where(a => a.traits.Contains<Unit>()
&& a.Owner == world.LocalPlayer)
.FirstOrDefault();
Sound.PlayVoice("Select", voicedUnit);
}
public Pair<float2, float2>? SelectionBox
{
get
@@ -153,42 +128,5 @@ namespace OpenRa
throw new InvalidOperationException( "Desync in Controller.ChooseCursor" );
}
}
Cache<int, List<Actor>> controlGroups = new Cache<int, List<Actor>>(_ => new List<Actor>());
public void DoControlGroup(World world, int group, Modifiers mods)
{
var uog = orderGenerator as UnitOrderGenerator;
if (uog == null) return;
if (mods.HasModifier(Modifiers.Ctrl))
{
if (!uog.selection.Any())
return;
controlGroups[group].Clear();
for (var i = 0; i < 10; i++) /* all control groups */
controlGroups[i].RemoveAll(a => uog.selection.Contains(a));
controlGroups[group].AddRange(uog.selection);
return;
}
if (mods.HasModifier(Modifiers.Alt))
{
Game.viewport.Center(controlGroups[group]);
return;
}
CombineSelection(world, controlGroups[group], mods.HasModifier(Modifiers.Shift), false);
}
public int? GetControlGroupForActor(Actor a)
{
return controlGroups.Where(g => g.Value.Contains(a))
.Select(g => (int?)g.Key)
.FirstOrDefault();
}
}
}

View File

@@ -37,19 +37,12 @@ namespace OpenRa
public static void LoadModPackages(Manifest manifest)
{
FileSystem.UnmountTemporaryPackages();
FileSystem.UnmountAll();
Timer.Time("reset: {0}");
foreach (var dir in manifest.Folders)
FileSystem.MountTemporary(new Folder(dir));
foreach (var pkg in manifest.Packages)
if (pkg.StartsWith( "~")) // this package is optional.
try { FileSystem.MountTemporary(new Package(pkg.Substring(1))); }
catch { }
else
FileSystem.MountTemporary(new Package(pkg));
foreach (var dir in manifest.Folders) FileSystem.Mount(dir);
foreach (var pkg in manifest.Packages) FileSystem.Mount(pkg);
Timer.Time("mount temporary packages: {0}");
}
@@ -87,7 +80,6 @@ namespace OpenRa
viewport = new Viewport(clientSize, Game.world.Map.Offset, Game.world.Map.Offset + Game.world.Map.Size, renderer);
Timer.Time( "ChromeProv, SeqProv, viewport: {0}" );
skipMakeAnims = true;
foreach (var treeReference in Game.world.Map.Trees)
world.CreateActor(treeReference.Image, new int2(treeReference.Location), null);
@@ -161,9 +153,9 @@ namespace OpenRa
if (orderManager.IsReadyForNextFrame)
{
orderManager.Tick( world );
if (controller.orderGenerator != null)
controller.orderGenerator.Tick( world );
orderManager.Tick(world);
controller.orderGenerator.Tick(world);
controller.selection.Tick(world);
world.Tick();
}
@@ -300,7 +292,8 @@ namespace OpenRa
if( !Game.chat.isChatting )
if( e.KeyCode >= Keys.D0 && e.KeyCode <= Keys.D9 )
Game.controller.DoControlGroup( world, (int)e.KeyCode - (int)Keys.D0, (Modifiers)(int)e.Modifiers );
Game.controller.selection.DoControlGroup( world,
(int)e.KeyCode - (int)Keys.D0, (Modifiers)(int)e.Modifiers );
if( sync != Game.world.SyncHash() )
throw new InvalidOperationException( "Desync in OnKeyDown" );

View File

@@ -46,9 +46,6 @@ namespace OpenRa.GameRules
public readonly float BuildupTime = 0;
public readonly int GemValue = 0;
public readonly int GoldValue = 0;
public readonly float GrowthRate = 0;
public readonly bool OreGrows = true;
public readonly bool OreSpreads = true;
public readonly float OreTruckRate = 0;
public readonly bool SeparateAircraft = true;
public readonly float SurvivorRate = 0;
@@ -90,7 +87,6 @@ namespace OpenRa.GameRules
public readonly bool FineDiffControl = false;
/* OpenRA-specific */
public readonly float OreChance = 0; /* chance of spreading to a particular eligible cell */
public readonly int LowPowerSlowdown = 3; /* build time multiplier */
}
}

View File

@@ -17,7 +17,7 @@ namespace OpenRa.Graphics
SpriteRenderer rgbaRenderer;
LineRenderer lineRenderer;
Sprite sprite, mapOnlySprite, mapSpawnPointSprite;
Bitmap terrain, oreLayer, spawnPointsLayer;
Bitmap terrain, oreLayer;
Rectangle bounds;
Sprite ownedSpawnPoint;
@@ -74,7 +74,6 @@ namespace OpenRa.Graphics
static Color shroudColor;
public void InvalidateOre() { oreLayer = null; }
public void InvalidateSpawnPoints() { spawnPointsLayer = null; }
public static Bitmap RenderTerrainBitmap(Map map, TileSet tileset)
{

View File

@@ -234,7 +234,7 @@ namespace OpenRa.Graphics
void DrawControlGroup(Actor selectedUnit, float2 basePosition)
{
var group = Game.controller.GetControlGroupForActor(selectedUnit);
var group = Game.controller.selection.GetControlGroupForActor(selectedUnit);
if (group == null) return;
var pipImages = new Animation("pips");

View File

@@ -74,11 +74,11 @@ namespace OpenRa
{
Game.Settings = new UserSettings();
var settingsFile = settings.GetValue("settings", "settings.ini");
FileSystem.MountTemporary(new Folder("./"));
FileSystem.Mount("./");
if (FileSystem.Exists(settingsFile))
FieldLoader.Load(Game.Settings,
new IniFile(FileSystem.Open(settingsFile)).GetSection("Settings"));
FileSystem.UnmountTemporaryPackages();
FileSystem.UnmountAll();
}
internal void Run()

View File

@@ -73,7 +73,6 @@
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="Bridges.cs" />
<Compile Include="Chat.cs" />
<Compile Include="Chrome.cs" />
<Compile Include="Combat.cs" />
@@ -111,13 +110,18 @@
<Compile Include="Ore.cs" />
<Compile Include="PackageDownloader.cs" />
<Compile Include="PathSearch.cs" />
<Compile Include="Selection.cs" />
<Compile Include="Shroud.cs" />
<Compile Include="Smudge.cs" />
<Compile Include="Sound.cs" />
<Compile Include="Support\OpenAlInterop.cs" />
<Compile Include="Support\PerfHistory.cs" />
<Compile Include="Sync.cs" />
<Compile Include="Traits\AttackOmni.cs" />
<Compile Include="Traits\BridgeLoadHook.cs" />
<Compile Include="Traits\ChoosePaletteOnSelect.cs" />
<Compile Include="Traits\CrateSpawner.cs" />
<Compile Include="Traits\OreGrowth.cs" />
<Compile Include="Traits\OreRefinery.cs" />
<Compile Include="Traits\Activities\Attack.cs" />
<Compile Include="Traits\Activities\CallFunc.cs" />
@@ -313,4 +317,4 @@
<Target Name="AfterBuild">
</Target>
-->
</Project>
</Project>

View File

@@ -8,16 +8,9 @@ namespace OpenRa.Orders
{
class UnitOrderGenerator : IOrderGenerator
{
public readonly List<Actor> selection;
public UnitOrderGenerator( IEnumerable<Actor> selected )
{
selection = selected.ToList();
}
public IEnumerable<Order> Order( World world, int2 xy, MouseInput mi )
{
foreach( var unit in selection )
foreach( var unit in Game.controller.selection.Actors )
{
var ret = unit.Order( xy, mi );
if( ret != null )
@@ -25,14 +18,11 @@ namespace OpenRa.Orders
}
}
public void Tick( World world )
{
selection.RemoveAll(a => !a.IsInWorld);
}
public void Tick( World world ) {}
public void Render( World world )
{
foreach( var a in selection )
foreach( var a in Game.controller.selection.Actors )
world.WorldRenderer.DrawSelectionBox( a, Color.White, true );
}

View File

@@ -8,14 +8,13 @@ namespace OpenRa
{
public static void AddOre(this Map map, int i, int j)
{
if (Rules.General.OreSpreads)
if (map.ContainsOre(i, j) && map.MapTiles[i, j].density < 12)
map.MapTiles[i, j].density++;
else if (map.MapTiles[i, j].overlay == 0xff)
{
map.MapTiles[i, j].overlay = ChooseOre();
map.MapTiles[i, j].density = 1;
}
if (map.ContainsOre(i, j) && map.MapTiles[i, j].density < 12)
map.MapTiles[i, j].density++;
else if (map.MapTiles[i, j].overlay == 0xff)
{
map.MapTiles[i, j].overlay = ChooseOre();
map.MapTiles[i, j].density = 1;
}
}
public static void DestroyOre(this Map map, int i, int j)
@@ -37,48 +36,49 @@ namespace OpenRa
< double.PositiveInfinity;
}
public static void SpreadOre(this World world, Random r, float chance)
{
var map = world.Map;
var mini = map.XOffset; var maxi = map.XOffset + map.Width;
var minj = map.YOffset; var maxj = map.YOffset + map.Height;
/* phase 1: grow into neighboring regions */
var newOverlay = new byte[128, 128];
for (int j = minj; j < maxj; j++)
for (int i = mini; i < maxi; i++)
{
newOverlay[i, j] = 0xff;
if (!map.HasOverlay(i, j)
&& r.NextDouble() < chance
&& map.GetOreDensity(i, j) > 0
&& world.OreCanSpreadInto(i, j))
newOverlay[i, j] = ChooseOre();
}
for (int j = minj; j < maxj; j++)
for (int i = mini; i < maxi; i++)
if (newOverlay[i, j] != 0xff)
map.MapTiles[i, j].overlay = newOverlay[i, j];
}
public static void GrowOre(this World world, Random r)
{
var map = world.Map;
var mini = map.XOffset; var maxi = map.XOffset + map.Width;
var minj = map.YOffset; var maxj = map.YOffset + map.Height;
var chance = Rules.General.OreChance;
/* phase 1: grow into neighboring regions */
if (Rules.General.OreSpreads)
{
var newOverlay = new byte[128, 128];
for (int j = minj; j < maxj; j++)
for (int i = mini; i < maxi; i++)
{
newOverlay[i, j] = 0xff;
if (!map.HasOverlay(i, j)
&& r.NextDouble() < chance
&& map.GetOreDensity(i, j) > 0
&& world.OreCanSpreadInto(i,j))
newOverlay[i, j] = ChooseOre();
}
for (int j = minj; j < maxj; j++)
for (int i = mini; i < maxi; i++)
if (newOverlay[i, j] != 0xff)
map.MapTiles[i, j].overlay = newOverlay[i, j];
}
/* phase 2: increase density of existing areas */
if (Rules.General.OreGrows)
{
var newDensity = new byte[128, 128];
for (int j = minj; j < maxj; j++)
for (int i = mini; i < maxi; i++)
if (map.ContainsOre(i, j)) newDensity[i, j] = map.GetOreDensity(i, j);
var newDensity = new byte[128, 128];
for (int j = minj; j < maxj; j++)
for (int i = mini; i < maxi; i++)
if (map.ContainsOre(i, j)) newDensity[i, j] = map.GetOreDensity(i, j);
for (int j = minj; j < maxj; j++)
for (int i = mini; i < maxi; i++)
if (map.MapTiles[i, j].density < newDensity[i, j])
++map.MapTiles[i, j].density;
}
for (int j = minj; j < maxj; j++)
for (int i = mini; i < maxi; i++)
if (map.MapTiles[i, j].density < newDensity[i, j])
++map.MapTiles[i, j].density;
}
public static void InitOreDensity( this Map map )

76
OpenRa.Game/Selection.cs Normal file
View File

@@ -0,0 +1,76 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using OpenRa.Traits;
using IjwFramework.Collections;
namespace OpenRa
{
public class Selection
{
List<Actor> actors = new List<Actor>();
public void Combine(World world, IEnumerable<Actor> newSelection, bool isCombine, bool isClick)
{
var oldSelection = actors.AsEnumerable();
if (isClick)
{
var adjNewSelection = newSelection.Take(1); /* todo: select BEST, not FIRST */
actors = (isCombine ? oldSelection.SymmetricDifference(adjNewSelection) : adjNewSelection).ToList();
}
else
actors = (isCombine ? oldSelection.Union(newSelection) : newSelection).ToList();
var voicedUnit = actors.FirstOrDefault(a => a.traits.Contains<Unit>() && a.Owner == world.LocalPlayer);
Sound.PlayVoice("Select", voicedUnit);
foreach (var ns in world.WorldActor.traits.WithInterface<INotifySelection>())
ns.SelectionChanged();
}
public IEnumerable<Actor> Actors { get { return actors; } }
public void Clear() { actors = new List<Actor>(); }
public void Tick(World world)
{
actors.RemoveAll(a => !a.IsInWorld);
}
Cache<int, List<Actor>> controlGroups = new Cache<int, List<Actor>>(_ => new List<Actor>());
public void DoControlGroup(World world, int group, Modifiers mods)
{
if (mods.HasModifier(Modifiers.Ctrl))
{
if (actors.Count == 0)
return;
controlGroups[group].Clear();
for (var i = 0; i < 10; i++) /* all control groups */
controlGroups[i].RemoveAll(a => actors.Contains(a));
controlGroups[group].AddRange(actors);
return;
}
if (mods.HasModifier(Modifiers.Alt))
{
Game.viewport.Center(controlGroups[group]);
return;
}
Combine(world, controlGroups[group],
mods.HasModifier(Modifiers.Shift), false);
}
public int? GetControlGroupForActor(Actor a)
{
return controlGroups.Where(g => g.Value.Contains(a))
.Select(g => (int?)g.Key)
.FirstOrDefault();
}
}
}

View File

@@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace OpenRa.Traits
{
class AttackOmniInfo : AttackBaseInfo
{
public override object Create(Actor self) { return new AttackOmni(self); }
}
class AttackOmni : AttackBase, INotifyBuildComplete
{
bool buildComplete = false;
public void BuildingComplete(Actor self) { buildComplete = true; }
public AttackOmni(Actor self) : base(self) { }
public override void Tick(Actor self)
{
base.Tick(self);
if (!CanAttack(self)) return;
if (self.traits.Contains<Building>() && !buildComplete) return;
DoAttack(self);
}
protected override void QueueAttack(Actor self, Order order)
{
target = order.TargetActor;
}
}
}

View File

@@ -2,13 +2,14 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using OpenRa.Traits;
namespace OpenRa
namespace OpenRa.Traits
{
static class Bridges
class BridgeLoadHookInfo : StatelessTraitInfo<BridgeLoadHook> { }
class BridgeLoadHook : ILoadWorldHook
{
public static void MakeBridges(World w)
static void MakeBridges(World w)
{
var mini = w.Map.XOffset; var maxi = w.Map.XOffset + w.Map.Width;
var minj = w.Map.YOffset; var maxj = w.Map.YOffset + w.Map.Height;
@@ -40,8 +41,8 @@ namespace OpenRa
if (!template.TerrainType.ContainsKey(n)) continue;
if (w.Map.IsInMap(x, y))
if (w.Map.MapTiles[x, y].tile == tile
&& w.Map.MapTiles[x,y].image == n)
if (w.Map.MapTiles[x, y].tile == tile
&& w.Map.MapTiles[x, y].image == n)
{
// stash it
replacedTiles[new int2(x, y)] = w.Map.MapTiles[x, y].image;
@@ -63,5 +64,7 @@ namespace OpenRa
{
return w.TileSet.walk[t].Bridge != null;
}
public void WorldLoaded(World w) { MakeBridges(w); }
}
}

View File

@@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace OpenRa.Traits
{
class ChoosePaletteOnSelectInfo : StatelessTraitInfo<ChoosePaletteOnSelect> { }
class ChoosePaletteOnSelect : INotifySelection
{
public void SelectionChanged()
{
var firstItem = Game.controller.selection.Actors.FirstOrDefault(
a => a.World.LocalPlayer == a.Owner && a.traits.Contains<Production>());
if (firstItem == null)
return;
var produces = firstItem.Info.Traits.Get<ProductionInfo>().Produces.FirstOrDefault();
if (produces == null)
return;
Game.chrome.SetCurrentTab(produces);
}
}
}

View File

@@ -0,0 +1,47 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using OpenRa.Graphics;
namespace OpenRa.Traits
{
class OreGrowthInfo : ITraitInfo
{
public readonly float Interval = 1f;
public readonly float Chance = .02f;
public readonly bool Spreads = true;
public readonly bool Grows = true;
public object Create(Actor self) { return new OreGrowth(); }
}
class OreGrowth : ITick, ILoadWorldHook
{
int remainingTicks;
public void Tick(Actor self)
{
if (--remainingTicks <= 0)
{
var info = self.Info.Traits.Get<OreGrowthInfo>();
if (info.Spreads)
Ore.SpreadOre(self.World,
Game.SharedRandom,
info.Chance);
if (info.Grows)
Ore.GrowOre(self.World, Game.SharedRandom);
self.World.Minimap.InvalidateOre();
remainingTicks = (int)(info.Interval * 60 * 25);
}
}
public void WorldLoaded(World w)
{
Ore.InitOreDensity(w.Map);
}
}
}

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Linq;
using OpenRa.Graphics;
using OpenRa.Orders;
@@ -27,8 +28,7 @@ namespace OpenRa.Traits
public IEnumerable<Renderable> Render(Actor self)
{
var uog = Game.controller.orderGenerator as UnitOrderGenerator;
if (uog != null && self.Owner == self.World.LocalPlayer && uog.selection.Contains(self))
if (self.Owner == self.World.LocalPlayer && Game.controller.selection.Actors.Contains(self))
yield return Util.Centered(self,
anim.Image, Util.CenterOfCell(rallyPoint));
}

View File

@@ -99,5 +99,8 @@ namespace OpenRa.Traits
public object Create(Actor self) { return Instance.Value; }
}
interface ITraitPrerequisite<T> { }
public interface ITraitPrerequisite<T> { }
public interface INotifySelection { void SelectionChanged(); }
public interface ILoadWorldHook { void WorldLoaded(World w); }
}

View File

@@ -34,7 +34,6 @@ namespace OpenRa
}
if (!string.IsNullOrEmpty(Game.Settings.PlayerName) && LocalPlayer.PlayerName != Game.Settings.PlayerName)
Game.IssueOrder(Order.Chat("/name " + Game.Settings.PlayerName));
}
public readonly Actor WorldActor;
@@ -50,9 +49,6 @@ namespace OpenRa
public readonly WorldRenderer WorldRenderer;
internal readonly Minimap Minimap;
readonly int oreFrequency;
int oreTicks;
public World()
{
Timer.Time( "----World.ctor" );
@@ -66,26 +62,21 @@ namespace OpenRa
WorldRenderer = new WorldRenderer(this, Game.renderer);
Timer.Time("renderer: {0}");
oreFrequency = (int)(Rules.General.GrowthRate * 60 * 25);
oreTicks = oreFrequency;
Map.InitOreDensity();
Timer.Time( "Ore: {0}" );
WorldActor = CreateActor("World", new int2(int.MaxValue, int.MaxValue), null);
for (int i = 0; i < 8; i++)
{
players[i] = new Player(this, i, Game.LobbyInfo.Clients.FirstOrDefault(a => a.Index == i));
}
Timer.Time( "worldActor, players: {0}" );
Queries = new AllQueries( this );
Timer.Time( "queries: {0}" );
Bridges.MakeBridges(this);
foreach (var wlh in WorldActor.traits.WithInterface<ILoadWorldHook>())
wlh.WorldLoaded(this);
PathFinder = new PathFinder(this);
Timer.Time( "bridge, pathing: {0}" );
Timer.Time( "hooks, pathing: {0}" );
Minimap = new Minimap(this, Game.renderer);
Timer.Time( "minimap: {0}" );
@@ -124,14 +115,6 @@ namespace OpenRa
public void Tick()
{
if (--oreTicks == 0)
using( new PerfSample( "ore" ) )
{
this.GrowOre( Game.SharedRandom );
Minimap.InvalidateOre();
oreTicks = oreFrequency;
}
foreach (var a in actors) a.Tick();
Queries.WithTraitMultiple<ITick>().Do( x => x.Trait.Tick( x.Actor ) );

View File

@@ -5,10 +5,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenRa.FileFormats", "OpenR
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenRa.Game", "OpenRa.Game\OpenRa.Game.csproj", "{0DFB103F-2962-400F-8C6D-E2C28CCBA633}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PaletteUsage", "PaletteUsage\PaletteUsage.csproj", "{54577061-E2D2-4336-90A2-A9A7106A30CD}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PaletteMatch", "PaletteMatch\PaletteMatch.csproj", "{31411761-224C-4C54-A5FE-280890A70259}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SequenceEditor", "SequenceEditor\SequenceEditor.csproj", "{230F65CE-A6DE-4235-8B38-13A3D606C7F7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenRA.Server", "OpenRA.Server\OpenRA.Server.csproj", "{76F621A1-3D8E-4A99-9F7E-B071EB657817}"
@@ -33,14 +29,6 @@ Global
{0DFB103F-2962-400F-8C6D-E2C28CCBA633}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0DFB103F-2962-400F-8C6D-E2C28CCBA633}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0DFB103F-2962-400F-8C6D-E2C28CCBA633}.Release|Any CPU.Build.0 = Release|Any CPU
{54577061-E2D2-4336-90A2-A9A7106A30CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{54577061-E2D2-4336-90A2-A9A7106A30CD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{54577061-E2D2-4336-90A2-A9A7106A30CD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{54577061-E2D2-4336-90A2-A9A7106A30CD}.Release|Any CPU.Build.0 = Release|Any CPU
{31411761-224C-4C54-A5FE-280890A70259}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{31411761-224C-4C54-A5FE-280890A70259}.Debug|Any CPU.Build.0 = Debug|Any CPU
{31411761-224C-4C54-A5FE-280890A70259}.Release|Any CPU.ActiveCfg = Release|Any CPU
{31411761-224C-4C54-A5FE-280890A70259}.Release|Any CPU.Build.0 = Release|Any CPU
{230F65CE-A6DE-4235-8B38-13A3D606C7F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{230F65CE-A6DE-4235-8B38-13A3D606C7F7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{230F65CE-A6DE-4235-8B38-13A3D606C7F7}.Release|Any CPU.ActiveCfg = Release|Any CPU

View File

@@ -1,65 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="3.5" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProductVersion>9.0.30729</ProductVersion>
<SchemaVersion>2.0</SchemaVersion>
<ProjectGuid>{31411761-224C-4C54-A5FE-280890A70259}</ProjectGuid>
<OutputType>Exe</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>PaletteMatch</RootNamespace>
<AssemblyName>PaletteMatch</AssemblyName>
<TargetFrameworkVersion>v3.5</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<DebugType>full</DebugType>
<PlatformTarget>AnyCPU</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<Optimize>true</Optimize>
<DebugType>pdbonly</DebugType>
<PlatformTarget>AnyCPU</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core">
<RequiredTargetFramework>3.5</RequiredTargetFramework>
</Reference>
<Reference Include="System.Drawing" />
<Reference Include="System.Xml.Linq">
<RequiredTargetFramework>3.5</RequiredTargetFramework>
</Reference>
<Reference Include="System.Data.DataSetExtensions">
<RequiredTargetFramework>3.5</RequiredTargetFramework>
</Reference>
<Reference Include="System.Data" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\OpenRa.FileFormats\OpenRa.FileFormats.csproj">
<Project>{BDAEAB25-991E-46A7-AF1E-4F0E03358DAA}</Project>
<Name>OpenRa.FileFormats</Name>
</ProjectReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>

View File

@@ -1,71 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using OpenRa.FileFormats;
using System.Drawing;
namespace PaletteMatch
{
/* a simple little hack to work out a sane matching between TD and RA palettes (or, indeed, from RA -> RA) */
/* usage: PaletteMatch srcpal destpal */
static class Program
{
static void Main(string[] args)
{
var tdPalette = WithStream(args[0], s => new Palette(s));
var raPalette = WithStream(args[1], s => new Palette(s));
var ms = tdPalette.Entries().Select(
(a, i) => new
{
Src = i,
Dest = raPalette.Entries().Select(
(b, j) => new { Color = b, Index = j })
.OrderBy(x => x.Color, new ColorDistanceComparer(a)).First().Index
});
foreach( var m in ms )
Console.WriteLine("{0:x2} -> {1:x2}", m.Src, m.Dest);
}
static IEnumerable<Color> Entries(this Palette p)
{
for (var i = 0; i < 256; i++)
yield return p.GetColor(i);
}
static T WithStream<T>(string filename, Func<Stream, T> f)
{
using (var s = File.OpenRead(filename))
return f(s);
}
}
class ColorDistanceComparer : IComparer<Color>
{
readonly Color r;
public ColorDistanceComparer(Color r)
{
this.r = r;
}
float Distance(Color a)
{
var b = a.GetBrightness() - r.GetBrightness();
var h = a.GetHue() - r.GetHue();
var s = a.GetSaturation() - r.GetSaturation();
return b * b + h * h + s * s;
}
public int Compare(Color x, Color y)
{
return Math.Sign(Distance(x) - Distance(y));
}
}
}

View File

@@ -1,36 +0,0 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("PaletteMatch")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("PaletteMatch")]
[assembly: AssemblyCopyright("Copyright © 2009")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("eb944988-f387-46a5-bb5b-392d482df964")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

View File

@@ -1,53 +0,0 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="3.5">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProductVersion>9.0.30729</ProductVersion>
<SchemaVersion>2.0</SchemaVersion>
<ProjectGuid>{54577061-E2D2-4336-90A2-A9A7106A30CD}</ProjectGuid>
<OutputType>Exe</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>PaletteUsage</RootNamespace>
<AssemblyName>PaletteUsage</AssemblyName>
<FileUpgradeFlags>
</FileUpgradeFlags>
<OldToolsVersion>2.0</OldToolsVersion>
<UpgradeBackupLocation>
</UpgradeBackupLocation>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<DebugType>full</DebugType>
<PlatformTarget>AnyCPU</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<Optimize>true</Optimize>
<DebugType>pdbonly</DebugType>
<PlatformTarget>AnyCPU</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Data" />
<Reference Include="System.Drawing" />
<Reference Include="System.Windows.Forms" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>

View File

@@ -1,49 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Forms;
using System.Drawing;
namespace PaletteUsage
{
class Program
{
static void Main(string[] args)
{
OpenFileDialog ofd = new OpenFileDialog();
ofd.RestoreDirectory = true;
ofd.Filter = "PNG Image Cache (*.png)|*.png";
if (DialogResult.OK != ofd.ShowDialog())
return;
Bitmap bitmap = new Bitmap(ofd.FileName);
int[] f = new int[256];
foreach (byte b in ImageBytes(bitmap))
++f[b];
Console.WriteLine("Unused palette entries:");
for (int i = 0; i < 256; i++)
if (f[i] == 0)
Console.WriteLine("0x{0:x}\t\t{0}", i);
}
static IEnumerable<byte> ImageBytes(Bitmap bitmap)
{
int width = bitmap.Width;
int height = bitmap.Height;
for( int i = 0; i < width; i++ )
for (int j = 0; j < height; j++)
{
Color c = bitmap.GetPixel(i, j);
yield return (byte)c.R;
yield return (byte)c.G;
yield return (byte)c.B;
yield return (byte)c.A;
}
}
}
}

View File

@@ -1,33 +0,0 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("PaletteUsage")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("PaletteUsage")]
[assembly: AssemblyCopyright("Copyright © 2007")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("93fb03d7-e484-45a8-bfe0-c0560814553a")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

View File

@@ -69,24 +69,28 @@ namespace SequenceEditor
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
FileSystem.MountDefaultPackages();
try
{
FileSystem.MountAftermathPackages();
}
catch( FileNotFoundException ){}
XmlFilename = args.FirstOrDefault( x => x.EndsWith(".xml") ) ?? "sequences.xml";
if (args.Length != 3)
{
MessageBox.Show( "usage: SequenceEditor mod[,mod]* sequences-file.xml palette.pal");
return;
}
var mods = args[0].Split(',');
var manifest = new Manifest(mods);
foreach (var folder in manifest.Folders) FileSystem.Mount(folder);
foreach (var pkg in manifest.Packages) FileSystem.Mount(pkg);
XmlFilename = args[1];
Doc = new XmlDocument();
Doc.Load(XmlFilename);
var tempPal = new Palette(FileSystem.Open("temperat.pal"));
Pal = tempPal; //new Palette(tempPal, new ShroudPaletteRemap());
var tempPal = new Palette(FileSystem.Open(args[2]));
Pal = tempPal;
UnitName = args.FirstOrDefault( x => !x.EndsWith(".xml") );
if (UnitName == null)
UnitName = GetTextForm.GetString("Unit to edit?", "e1");
if (UnitName == null)
UnitName = GetTextForm.GetString("Unit to edit?", "e1");
if (string.IsNullOrEmpty(UnitName))
return;
LoadAndResolve(UnitName);
@@ -95,10 +99,7 @@ namespace SequenceEditor
foreach (XmlElement e in Doc.SelectNodes(xpath))
{
if (e.HasAttribute("src"))
{
var src = e.GetAttribute("src");
LoadAndResolve(src);
}
LoadAndResolve(e.GetAttribute("src"));
Sequences[e.GetAttribute("name")] = new Sequence(e);
}

19
build-deps.cmd Normal file
View File

@@ -0,0 +1,19 @@
set MSBUILD="c:\windows\Microsoft.NET\Framework\v3.5\msbuild.exe"
call git submodule init
call git submodule update
pushd Ijw.DirectX
call git submodule init
call git submodule update
pushd Ijw.Framework
%MSBUILD% /t:Rebuild /p:Configuration=Debug IjwFramework.sln
%MSBUILD% /t:Rebuild /p:Configuration=Release IjwFramework.sln
popd
%MSBUILD% Ijw.DirectX.sln /p:Configuration=Debug
%MSBUILD% Ijw.DirectX.sln /p:Configuration=Release
popd
%MSBUILD% OpenRA.sln /t:Rebuild /p:Configuration=Debug

View File

@@ -10,14 +10,11 @@ URepairPercent=20% ; [units only] percent cost to fully repair as ratio of
URepairStep=10 ; [units only] hit points to heal per repair 'tick' for units
BuildSpeed=.1 ; general build speed [time (in minutes) to produce a 1000 credit cost item]
OreGrows=yes ; Does ore grow denser over time?
OreSpreads=yes ; Does ore spread into adjacent areas
GemValue=50 ; gem credits per 'bail' carried by a harvester
GoldValue=25 ; gold credits per 'bail' carried by a harvester
GrowthRate=.3 ; minutes between ore (Tiberium) growth
BailCount=28 ; number of 'bails' carried by a harvester
;; OreChance is missing from rules.ini but referenced
;; LowPowerSlowdown is missing from rules.ini but referenced
LowPowerSlowdown=3 ; slowdown factor for low power
GapRegenInterval=.1 ; gap generators will regenerate their shroud at this time interval
SubmergeDelay=.02 ; forced delay that subs will remain on surface before allowing to submerge

View File

@@ -343,10 +343,9 @@ OBLI:
Armor: light
Crewed: no
Sight: 5
Turreted:
RenderBuildingCharge:
ChargeAudio: obelpowr.aud
AttackTurreted:
AttackOmni:
PrimaryWeapon: Laser
FireDelay: 8
AutoTarget:

View File

@@ -14,6 +14,7 @@ World:
WaterPaletteRotation:
BuildingInfluence:
UnitInfluence:
BridgeLoadHook:
PaletteFromFile@terrain_temperat:
Name: terrain
Theater: temperat
@@ -104,4 +105,9 @@ World:
G: 0
B: 0
A: 180
ShroudPalette:
ShroudPalette:
OreGrowth:
Interval: .3
Chance: .02
Spreads: yes
Grows: yes

View File

@@ -69,9 +69,6 @@ BuildSpeed=.8 ; general build speed [time (in minutes) to produce a 10
BuildupTime=.06 ; average minutes that building build-up animation runs
GemValue=50 ; gem credits per 'bail' carried by a harvester
GoldValue=25 ; gold credits per 'bail' carried by a harvester
GrowthRate=.3 ; minutes between ore (Tiberium) growth
OreGrows=yes ; Does ore grow denser over time?
OreSpreads=yes ; Does ore spread into adjacent areas?
OreTruckRate=1 ; speed that harvester truck manages ore [larger means slower]
SeparateAircraft=no ; Is first helicopter to be purchased separately from helipad?
SurvivorRate=.4 ; fraction of building cost to be converted to survivors when sold
@@ -110,6 +107,7 @@ TeamDelay=.6 ; interval between checking for and creating teams
; misc
FineDiffControl=no ; Allow 5 difficulty settings instead of only 3 settings?
LowPowerSlowdown=3
; ******* Weapon Statistics *******
@@ -1011,10 +1009,6 @@ Verses=100%,100%,100%,100%,100%
ImpactSound=kaboom25
Explosion=7
[General]
OreChance=.02
LowPowerSlowdown=3
[VoiceTypes]

View File

@@ -71,6 +71,8 @@ World:
LightPaletteRotator:
BuildingInfluence:
UnitInfluence:
ChoosePaletteOnSelect:
BridgeLoadHook:
PaletteFromFile@terrain_temperat:
Name: terrain
Filename: temperat.pal
@@ -158,6 +160,11 @@ World:
B: 0
A: 180
ShroudPalette:
OreGrowth:
Interval: .3
Chance: .02
Spreads: yes
Grows: yes
MGG:
GeneratesGap:
@@ -453,9 +460,8 @@ TSLA:
Armor: heavy
Crewed: yes
Sight: 8
Turreted:
RenderBuildingCharge:
AttackTurreted:
AttackOmni:
PrimaryWeapon: TeslaZap
FireDelay: 8
AutoTarget:
@@ -1162,8 +1168,7 @@ PBOX:
Armor: wood
Crewed: yes
Sight: 5
Turreted:
AttackTurreted:
AttackOmni:
PrimaryWeapon: Vulcan
AutoTarget:
IronCurtainable:
@@ -1183,8 +1188,7 @@ HBOX:
Armor: wood
Crewed: yes
Sight: 5
Turreted:
AttackTurreted:
AttackOmni:
PrimaryWeapon: Vulcan
AutoTarget:
IronCurtainable:
@@ -1223,15 +1227,13 @@ FTUR:
Cost: 600
Description: Flame Turret
LongDesc: Anti-Infantry base defense.\n Strong vs Infantry\n Weak vs Aircraft
Turreted:
ROT: 10
Building:
Power: -20
HP: 400
Armor: heavy
Crewed: yes
Sight: 6
AttackTurreted:
AttackOmni:
PrimaryWeapon: FireballLauncher
PrimaryOffset: 0,0,12,8
AutoTarget: