diff --git a/OpenRa.Game/Actor.cs b/OpenRa.Game/Actor.cs index ff765c9edb..e91f977190 100755 --- a/OpenRa.Game/Actor.cs +++ b/OpenRa.Game/Actor.cs @@ -85,8 +85,10 @@ namespace OpenRa.Game if (!Rules.Map.IsInMap(xy.X, xy.Y)) return null; - - var underCursor = Game.UnitInfluence.GetUnitAt( xy ) + + // HACK: Get the first unit in the cell + // This will need to be updated for multiple-infantry-in-a-cell + var underCursor = Game.UnitInfluence.GetUnitsAt( xy ).FirstOrDefault() ?? Game.BuildingInfluence.GetBuildingAt( xy ); if (underCursor != null && !underCursor.Info.Selectable) diff --git a/OpenRa.Game/Game.cs b/OpenRa.Game/Game.cs index 09c24cccf1..a11ccfbc25 100644 --- a/OpenRa.Game/Game.cs +++ b/OpenRa.Game/Game.cs @@ -228,13 +228,35 @@ namespace OpenRa.Game public static bool IsCellBuildable(int2 a, UnitMovementType umt, Actor toIgnore) { if (BuildingInfluence.GetBuildingAt(a) != null) return false; - if (UnitInfluence.GetUnitAt(a) != null && UnitInfluence.GetUnitAt(a) != toIgnore) return false; + if (UnitInfluence.GetUnitsAt(a).Any(b => b != toIgnore)) return false; return Rules.Map.IsInMap(a.X, a.Y) && TerrainCosts.Cost(umt, Rules.TileSet.GetWalkability(Rules.Map.MapTiles[a.X, a.Y])) < double.PositiveInfinity; } + public static bool IsActorCrushableByActor(Actor a, Actor b) + { + return IsActorCrushableByMovementType(a, b.traits.WithInterface().FirstOrDefault().GetMovementType()); + } + public static bool IsActorCrushableByMovementType(Actor a, UnitMovementType umt) + { + if (a != null) + { + foreach (var crush in a.traits.WithInterface()) + { + if (((crush.IsCrushableByEnemy() && a.Owner != Game.LocalPlayer) || (crush.IsCrushableByFriend() && a.Owner == Game.LocalPlayer)) + && crush.CrushableBy().Contains(umt)) + { + Log.Write("{0} is crushable by MovementType {1}", a.Info.Name, umt); + return true; + } + } + Log.Write("{0} is NOT crushable by MovementType {1}", a.Info.Name, umt); + } + return false; + } + public static bool IsWater(int2 a) { return Rules.Map.IsInMap(a.X, a.Y) && diff --git a/OpenRa.Game/OpenRa.Game.csproj b/OpenRa.Game/OpenRa.Game.csproj index 108df335a8..8c23fce9d4 100644 --- a/OpenRa.Game/OpenRa.Game.csproj +++ b/OpenRa.Game/OpenRa.Game.csproj @@ -173,6 +173,7 @@ + diff --git a/OpenRa.Game/PathFinder.cs b/OpenRa.Game/PathFinder.cs index 2a2717c51c..210613a1e6 100644 --- a/OpenRa.Game/PathFinder.cs +++ b/OpenRa.Game/PathFinder.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using OpenRa.FileFormats; using OpenRa.Game.Support; +using OpenRa.Game.Traits; namespace OpenRa.Game { @@ -22,20 +23,6 @@ namespace OpenRa.Game : float.PositiveInfinity; } - bool IsBlocked(int2 from, UnitMovementType umt) - { - for (int v = -1; v < 2; v++) - for (int u = -1; u < 2; u++) - if (u != 0 || v != 0) - { - var p = from + new int2(u, v); - if (passableCost[(int)umt][from.X + u, from.Y + v] < float.PositiveInfinity) - if (Game.BuildingInfluence.CanMoveHere(p) && (Game.UnitInfluence.GetUnitAt(p) == null)) - return false; - } - return true; - } - public List FindUnitPath( int2 from, int2 target, UnitMovementType umt ) { using (new PerfSample("find_unit_path")) @@ -61,13 +48,13 @@ namespace OpenRa.Game return path; } } - + Func AvoidUnitsNear(int2 p, int dist) { return q => p != q && - ((p - q).LengthSquared < dist * dist) && - (Game.UnitInfluence.GetUnitAt(q) != null); + ((p - q).LengthSquared < dist * dist) && + (Game.UnitInfluence.GetUnitsAt(q).Any()); } public List FindPath( PathSearch search ) diff --git a/OpenRa.Game/PathSearch.cs b/OpenRa.Game/PathSearch.cs index cb2bc4dcd2..0290d34056 100755 --- a/OpenRa.Game/PathSearch.cs +++ b/OpenRa.Game/PathSearch.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using IjwFramework.Collections; using OpenRa.Game.Graphics; @@ -35,7 +36,7 @@ namespace OpenRa.Game if (!ignoreTerrain) if (passableCost[(int)umt][p.Location.X, p.Location.Y] == float.PositiveInfinity) return p.Location; - + foreach( int2 d in Util.directions ) { int2 newHere = p.Location + d; @@ -53,13 +54,15 @@ namespace OpenRa.Game if (Rules.Map.IsOverlaySolid(newHere)) continue; } - - if( checkForBlocked && Game.UnitInfluence.GetUnitAt( newHere ) != null ) + + // Replicate real-ra behavior of not being able to enter a cell if there is a mixture of crushable and uncrushable units + if (checkForBlocked && (Game.UnitInfluence.GetUnitsAt(newHere).Any(a => !Game.IsActorCrushableByMovementType(a, umt)))) continue; + if (customBlock != null && customBlock(newHere)) continue; - + var est = heuristic( newHere ); if( est == float.PositiveInfinity ) continue; @@ -75,6 +78,7 @@ namespace OpenRa.Game cellInfo[ newHere.X, newHere.Y ].MinCost = newCost; queue.Add( new PathDistance( newCost + est, newHere ) ); + } return p.Location; } @@ -96,7 +100,6 @@ namespace OpenRa.Game checkForBlocked = checkForBlocked }; search.AddInitialCell( from ); - return search; } diff --git a/OpenRa.Game/Traits/Activities/Move.cs b/OpenRa.Game/Traits/Activities/Move.cs index ef078d05fb..9a655495a1 100755 --- a/OpenRa.Game/Traits/Activities/Move.cs +++ b/OpenRa.Game/Traits/Activities/Move.cs @@ -45,8 +45,10 @@ namespace OpenRa.Game.Traits.Activities static bool CanEnterCell( int2 c, Actor self ) { if (!Game.BuildingInfluence.CanMoveHere(c)) return false; - var u = Game.UnitInfluence.GetUnitAt( c ); - return (u == null || u == self); + + // Cannot enter a cell if any unit inside is uncrushable + // This will need to be updated for multiple-infantry-in-a-cell + return (!Game.UnitInfluence.GetUnitsAt(c).Any(a => a != self && !Game.IsActorCrushableByActor(a, self))); } public IActivity Tick( Actor self ) @@ -68,7 +70,7 @@ namespace OpenRa.Game.Traits.Activities path = getPath( self, mobile ).TakeWhile( a => a != self.Location ).ToList(); SanityCheckPath( mobile ); } - + if( path.Count == 0 ) { destination = mobile.toCell; diff --git a/OpenRa.Game/Traits/Infantry.cs b/OpenRa.Game/Traits/Infantry.cs new file mode 100644 index 0000000000..0c4d5e5530 --- /dev/null +++ b/OpenRa.Game/Traits/Infantry.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace OpenRa.Game.Traits +{ + class Infantry : ICrushable + { + readonly Actor self; + public Infantry(Actor self) + { + this.self = self; + } + + public bool IsCrushableByFriend() + { + // HACK: should be false + return true; + } + public bool IsCrushableByEnemy() + { + // HACK: should be based off crushable tag + return true; + } + + public void OnCrush(Actor crusher) + { + self.InflictDamage(crusher, self.Health, Rules.WarheadInfo["Crush"]); + } + + public IEnumerable CrushableBy() + { + yield return UnitMovementType.Track; + //yield return UnitMovementType.Wheel; // Can infantry be crushed by wheel? + } + } +} diff --git a/OpenRa.Game/Traits/Mobile.cs b/OpenRa.Game/Traits/Mobile.cs index c25bfae764..c009ab0a1c 100644 --- a/OpenRa.Game/Traits/Mobile.cs +++ b/OpenRa.Game/Traits/Mobile.cs @@ -80,9 +80,27 @@ namespace OpenRa.Game.Traits } } - public bool CanEnterCell(int2 location) + public bool CanEnterCell(int2 a) { - return Game.IsCellBuildable( location, GetMovementType(), self ); + if (Game.BuildingInfluence.GetBuildingAt(a) != null) return false; + + var crushable = true; + foreach (Actor actor in Game.UnitInfluence.GetUnitsAt(a)) + { + if (actor == self) continue; + + if (!Game.IsActorCrushableByActor(actor, self)) + { + crushable = false; + break; + } + } + + if (!crushable) return false; + + return Rules.Map.IsInMap(a.X, a.Y) && + TerrainCosts.Cost(GetMovementType(), + Rules.TileSet.GetWalkability(Rules.Map.MapTiles[a.X, a.Y])) < double.PositiveInfinity; } public IEnumerable GetCurrentPath() diff --git a/OpenRa.Game/Traits/Production.cs b/OpenRa.Game/Traits/Production.cs index 4dcfe7fe63..640f9143d1 100755 --- a/OpenRa.Game/Traits/Production.cs +++ b/OpenRa.Game/Traits/Production.cs @@ -21,7 +21,7 @@ namespace OpenRa.Game.Traits public bool Produce( Actor self, UnitInfo producee ) { var location = CreationLocation( self, producee ); - if( location == null || Game.UnitInfluence.GetUnitAt( location.Value ) != null ) + if( location == null || Game.UnitInfluence.GetUnitsAt( location.Value ).Any() ) return false; var newUnit = new Actor( producee, location.Value, self.Owner ); diff --git a/OpenRa.Game/Traits/RenderBuildingWarFactory.cs b/OpenRa.Game/Traits/RenderBuildingWarFactory.cs index fe8a527911..40ccaf25e9 100644 --- a/OpenRa.Game/Traits/RenderBuildingWarFactory.cs +++ b/OpenRa.Game/Traits/RenderBuildingWarFactory.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using OpenRa.Game.Graphics; namespace OpenRa.Game.Traits @@ -35,7 +36,7 @@ namespace OpenRa.Game.Traits if (doneBuilding) roof.Tick(); var b = self.Bounds; - if (isOpen && null == Game.UnitInfluence.GetUnitAt(((1/24f) * self.CenterLocation).ToInt2())) + if (isOpen && !Game.UnitInfluence.GetUnitsAt(((1/24f) * self.CenterLocation).ToInt2()).Any()) { isOpen = false; roof.PlayBackwardsThen(prefix + "build-top", () => roof.Play(prefix + "idle-top")); diff --git a/OpenRa.Game/Traits/TraitsInterfaces.cs b/OpenRa.Game/Traits/TraitsInterfaces.cs index 9e74ade318..aaa9bb8ebd 100644 --- a/OpenRa.Game/Traits/TraitsInterfaces.cs +++ b/OpenRa.Game/Traits/TraitsInterfaces.cs @@ -58,4 +58,12 @@ namespace OpenRa.Game.Traits UnitMovementType GetMovementType(); bool CanEnterCell(int2 location); } + + interface ICrushable + { + bool IsCrushableByFriend(); + bool IsCrushableByEnemy(); + void OnCrush(Actor crusher); + IEnumerableCrushableBy(); + } } diff --git a/OpenRa.Game/UiOverlay.cs b/OpenRa.Game/UiOverlay.cs index 309d44985a..f7abc7dd06 100644 --- a/OpenRa.Game/UiOverlay.cs +++ b/OpenRa.Game/UiOverlay.cs @@ -1,4 +1,6 @@ using System.Drawing; +using System.Collections.Generic; +using System.Linq; using OpenRa.Game.GameRules; using OpenRa.Game.Graphics; @@ -37,7 +39,7 @@ namespace OpenRa.Game if (ShowUnitDebug) for (var j = 0; j < 128; j++) for (var i = 0; i < 128; i++) - if (Game.UnitInfluence.GetUnitAt(new int2(i, j)) != null) + if (Game.UnitInfluence.GetUnitsAt(new int2(i, j)).Any()) spriteRenderer.DrawSprite(unitDebug, Game.CellSize * new float2(i, j), 0); } diff --git a/OpenRa.Game/UnitInfluenceMap.cs b/OpenRa.Game/UnitInfluenceMap.cs index 9fad708018..de3ecf044b 100644 --- a/OpenRa.Game/UnitInfluenceMap.cs +++ b/OpenRa.Game/UnitInfluenceMap.cs @@ -1,22 +1,50 @@ using System; using System.Diagnostics; using System.Linq; +using System.Collections.Generic; using OpenRa.Game.Traits; namespace OpenRa.Game { class UnitInfluenceMap { - Actor[,] influence = new Actor[128, 128]; + List[,] influence = new List[128, 128]; readonly int2 searchDistance = new int2(2,2); public UnitInfluenceMap() { + for (int i = 0; i < 128; i++) + for (int j = 0; j < 128; j++) + influence[ i, j ] = new List(); + Game.world.ActorRemoved += a => Remove(a, a.traits.WithInterface().FirstOrDefault()); } public void Tick() { + // Does this belong here? + + // Get the crushable actors + foreach (var a in Game.world.Actors.Where(b => b.traits.WithInterface().Any())) + { + // Are there any units in the same cell that can crush this? + foreach( var ios in a.traits.WithInterface() ) + foreach( var cell in ios.OccupiedCells() ) + { + // There should only be one (counterexample: An infantry and a tank try to pick up a crate at the same time.) + // If there is more than one, do action on the first crusher + var crusher = GetUnitsAt(cell).Where(b => a != b && Game.IsActorCrushableByActor(a, b)).FirstOrDefault(); + if (crusher != null) + { + Log.Write("{0} crushes {1}", crusher.Info.Name, a.Info.Name); + // Apply the crush action + foreach (var crush in a.traits.WithInterface()) + { + crush.OnCrush(crusher); + } + } + } + } SanityCheck(); } @@ -25,25 +53,30 @@ namespace OpenRa.Game { for( int y = 0 ; y < 128 ; y++ ) for( int x = 0 ; x < 128 ; x++ ) - if( influence[ x, y ] != null && !influence[ x, y ].traits.WithInterface().First().OccupiedCells().Contains( new int2( x, y ) ) ) - throw new InvalidOperationException( "UIM: Sanity check failed A" ); + if( influence[ x, y ] != null ) + foreach (var a in influence[ x, y ]) + if (!a.traits.WithInterface().First().OccupiedCells().Contains( new int2( x, y ) ) ) + throw new InvalidOperationException( "UIM: Sanity check failed A" ); - foreach( var a in Game.world.Actors ) + foreach( Actor a in Game.world.Actors ) foreach( var ios in a.traits.WithInterface() ) foreach( var cell in ios.OccupiedCells() ) - if( influence[ cell.X, cell.Y ] != a ) - throw new InvalidOperationException( "UIM: Sanity check failed B" ); + if (!influence[cell.X, cell.Y].Contains(a)) + //if( influence[ cell.X, cell.Y ] != a ) + throw new InvalidOperationException( "UIM: Sanity check failed B" ); } [Conditional( "SANITY_CHECKS" )] void SanityCheckAdd( IOccupySpace a ) { + /* This check is too strict now that we can have multiple units in a cell foreach( var c in a.OccupiedCells() ) - if( influence[c.X, c.Y] != null ) + if( influence[c.X, c.Y].Any()) throw new InvalidOperationException( "UIM: Sanity check failed (Add)" ); + */ } - public Actor GetUnitAt( int2 a ) + public IEnumerable GetUnitsAt( int2 a ) { return influence[ a.X, a.Y ]; } @@ -52,14 +85,14 @@ namespace OpenRa.Game { SanityCheckAdd( unit ); foreach( var c in unit.OccupiedCells() ) - influence[c.X, c.Y] = self; + influence[c.X, c.Y].Add(self); } public void Remove( Actor self, IOccupySpace unit ) { if (unit != null) foreach (var c in unit.OccupiedCells()) - influence[c.X, c.Y] = null; + influence[c.X, c.Y].Remove(self); } public void Update(Actor self, IOccupySpace unit) diff --git a/sequences.xml b/sequences.xml index 9187292fa4..fe841d91a2 100644 --- a/sequences.xml +++ b/sequences.xml @@ -498,10 +498,12 @@ + + diff --git a/units.ini b/units.ini index e73f5e55c8..a4da075087 100755 --- a/units.ini +++ b/units.ini @@ -522,52 +522,52 @@ MEDI Description=Attack Dog BuiltAt=KENN Voice=DogVoice -Traits=Unit, Mobile, RenderInfantry +Traits=Unit, Mobile, RenderInfantry, Infantry LongDesc=Anti-infantry unit. Not fooled by the \nSpy's disguise.\n Strong vs Infantry\n Weak vs Vehicles [E1] Description=Rifle Infantry -Traits=Unit, Mobile, RenderInfantry, AttackBase, TakeCover +Traits=Unit, Mobile, RenderInfantry, AttackBase, TakeCover, Infantry LongDesc=General-purpose infantry. Strong vs Infantry\n Weak vs Vehicles [E2] Description=Grenadier -Traits=Unit, Mobile, RenderInfantry, AttackBase, TakeCover +Traits=Unit, Mobile, RenderInfantry, AttackBase, TakeCover, Infantry FireDelay=15 PrimaryOffset=0,0,0,-13 LongDesc=Infantry armed with grenades. \n Strong vs Buildings, Infantry\n Weak vs Vehicles [E3] Description=Rocket Soldier -Traits=Unit, Mobile, RenderInfantry, AttackBase, TakeCover +Traits=Unit, Mobile, RenderInfantry, AttackBase, TakeCover, Infantry PrimaryOffset=0,0,0,-13 LongDesc=Anti-tank/Anti-aircraft infantry.\n Strong vs Tanks, Aircraft\n Weak vs Infantry [E4] Description=Flamethrower -Traits=Unit, Mobile, RenderInfantry, AttackBase, TakeCover +Traits=Unit, Mobile, RenderInfantry, AttackBase, TakeCover, Infantry FireDelay=8 LongDesc=Advanced Anti-infantry unit.\n Strong vs Infantry, Buildings\n Weak vs Vehicles [E6] Description=Engineer -Traits=Unit, Mobile, RenderInfantry, TakeCover +Traits=Unit, Mobile, RenderInfantry, TakeCover, Infantry Voice=EngineerVoice LongDesc=Infiltrates and captures enemy structures.\n Strong vs Nothing\n Weak vs Everything [SPY] Description=Spy Voice=SpyVoice -Traits=Unit, Mobile, RenderInfantry, TakeCover +Traits=Unit, Mobile, RenderInfantry, TakeCover, Infantry 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 [THF] Description=Thief Voice=ThiefVoice -Traits=Unit, Mobile, RenderInfantry, TakeCover +Traits=Unit, Mobile, RenderInfantry, TakeCover, Infantry LongDesc=Infiltrates enemy refineries & \nsilos, and steals money stored there.\n Unarmed [E7] Description=Tanya Voice=TanyaVoice -Traits=Unit, Mobile, RenderInfantry, AttackBase, TakeCover +Traits=Unit, Mobile, RenderInfantry, AttackBase, TakeCover, Infantry 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 [MEDI] Description=Medic Voice=MedicVoice -Traits=Unit, Mobile, RenderInfantry, AttackBase, TakeCover +Traits=Unit, Mobile, RenderInfantry, AttackBase, TakeCover, Infantry LongDesc=Heals nearby infantry.\n Strong vs Nothing\n Weak vs Everything @@ -657,6 +657,7 @@ Super Organic Nuke UnitExplodeWarhead +Crush [HE] ImpactSound=kaboom25 @@ -669,6 +670,10 @@ Explosion=8 InfDeath=3 ImpactSound=kaboom15 +[Crush] +Verses=100%,100%,100%,100%,100% +ImpactSound=squishy2 + [General] OreChance=.02 LowPowerSlowdown=3