New crush code, now with less bs
This commit is contained in:
@@ -35,7 +35,6 @@ namespace OpenRA
|
|||||||
public PathFinder( World world )
|
public PathFinder( World world )
|
||||||
{
|
{
|
||||||
this.world = world;
|
this.world = world;
|
||||||
var map = world.Map;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class CachedPath
|
class CachedPath
|
||||||
|
|||||||
@@ -306,6 +306,7 @@ namespace OpenRA.Traits.Activities
|
|||||||
{
|
{
|
||||||
self.CenterLocation = Util.CenterOfCell( mobile.toCell );
|
self.CenterLocation = Util.CenterOfCell( mobile.toCell );
|
||||||
mobile.fromCell = mobile.toCell;
|
mobile.fromCell = mobile.toCell;
|
||||||
|
mobile.FinishedMoving(self);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ namespace OpenRA.Traits
|
|||||||
{
|
{
|
||||||
public readonly TerrainType[] TerrainTypes;
|
public readonly TerrainType[] TerrainTypes;
|
||||||
public readonly float[] TerrainSpeeds;
|
public readonly float[] TerrainSpeeds;
|
||||||
|
public readonly string[] Crushes;
|
||||||
public readonly int WaitAverage = 60;
|
public readonly int WaitAverage = 60;
|
||||||
public readonly int WaitSpread = 20;
|
public readonly int WaitSpread = 20;
|
||||||
|
|
||||||
@@ -39,6 +40,7 @@ namespace OpenRA.Traits
|
|||||||
public class Mobile : IIssueOrder, IResolveOrder, IOccupySpace, IMove
|
public class Mobile : IIssueOrder, IResolveOrder, IOccupySpace, IMove
|
||||||
{
|
{
|
||||||
public readonly Actor self;
|
public readonly Actor self;
|
||||||
|
public readonly MobileInfo Info;
|
||||||
public readonly Dictionary<TerrainType,float> TerrainCost;
|
public readonly Dictionary<TerrainType,float> TerrainCost;
|
||||||
public readonly Dictionary<TerrainType,float> TerrainSpeed;
|
public readonly Dictionary<TerrainType,float> TerrainSpeed;
|
||||||
|
|
||||||
@@ -67,6 +69,7 @@ namespace OpenRA.Traits
|
|||||||
public Mobile(ActorInitializer init, MobileInfo info)
|
public Mobile(ActorInitializer init, MobileInfo info)
|
||||||
{
|
{
|
||||||
this.self = init.self;
|
this.self = init.self;
|
||||||
|
this.Info = info;
|
||||||
this.__fromCell = this.__toCell = init.location;
|
this.__fromCell = this.__toCell = init.location;
|
||||||
AddInfluence();
|
AddInfluence();
|
||||||
|
|
||||||
@@ -137,9 +140,19 @@ namespace OpenRA.Traits
|
|||||||
|
|
||||||
public virtual bool CanEnterCell(int2 cell, Actor ignoreActor, bool checkTransientActors)
|
public virtual bool CanEnterCell(int2 cell, Actor ignoreActor, bool checkTransientActors)
|
||||||
{
|
{
|
||||||
if (!self.World.WorldActor.traits.Get<BuildingInfluence>().CanMoveHere(cell, ignoreActor))
|
// Check for buildings
|
||||||
return false;
|
var building = self.World.WorldActor.traits.Get<BuildingInfluence>().GetBuildingBlocking(cell);
|
||||||
|
if (building != null && building != ignoreActor)
|
||||||
|
{
|
||||||
|
var crushable = building.traits.WithInterface<ICrushable>();
|
||||||
|
if (crushable.Count() == 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!crushable.Any(b => b.CrushClasses.Intersect(Info.Crushes).Any()))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check mobile actors
|
||||||
if (checkTransientActors)
|
if (checkTransientActors)
|
||||||
{
|
{
|
||||||
var canShare = self.traits.Contains<SharesCell>();
|
var canShare = self.traits.Contains<SharesCell>();
|
||||||
@@ -150,16 +163,27 @@ namespace OpenRA.Traits
|
|||||||
// only allow 5 in a cell
|
// only allow 5 in a cell
|
||||||
if (shareable.Count() >= 5)
|
if (shareable.Count() >= 5)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
// We can enter a cell with nonshareable units if we can crush all of them
|
// We can enter a cell with nonshareable units if we can crush all of them
|
||||||
if (nonshareable.Any(
|
if (nonshareable.Any(a => !(a.traits.Contains<ICrushable>() &&
|
||||||
a => !self.World.IsActorCrushableByActor(a, self)))
|
a.traits.WithInterface<ICrushable>().Any(b => b.CrushClasses.Intersect(Info.Crushes).Any()))))
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return MovementCostForCell(self, cell) < float.PositiveInfinity;
|
return MovementCostForCell(self, cell) < float.PositiveInfinity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public virtual void FinishedMoving(Actor self)
|
||||||
|
{
|
||||||
|
var crushable = self.World.WorldActor.traits.Get<UnitInfluence>().GetUnitsAt(self.Location).Where(a => a != self && a.traits.Contains<ICrushable>());
|
||||||
|
foreach (var a in crushable)
|
||||||
|
{
|
||||||
|
var crushActions = a.traits.WithInterface<ICrushable>().Where(b => b.CrushClasses.Intersect(Info.Crushes).Any());
|
||||||
|
foreach (var b in crushActions)
|
||||||
|
b.OnCrush(self);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public virtual float MovementCostForCell(Actor self, int2 cell)
|
public virtual float MovementCostForCell(Actor self, int2 cell)
|
||||||
{
|
{
|
||||||
if (!self.World.Map.IsInMap(cell.X,cell.Y))
|
if (!self.World.Map.IsInMap(cell.X,cell.Y))
|
||||||
|
|||||||
@@ -115,8 +115,7 @@ namespace OpenRA.Traits
|
|||||||
public interface ICrushable
|
public interface ICrushable
|
||||||
{
|
{
|
||||||
void OnCrush(Actor crusher);
|
void OnCrush(Actor crusher);
|
||||||
bool IsCrushableBy(UnitMovementType umt, Player player);
|
IEnumerable<string> CrushClasses { get; }
|
||||||
bool IsPathableCrush(UnitMovementType umt, Player player);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct Renderable
|
public struct Renderable
|
||||||
|
|||||||
@@ -65,6 +65,12 @@ namespace OpenRA.Traits
|
|||||||
if (!map.IsInMap(cell)) return null;
|
if (!map.IsInMap(cell)) return null;
|
||||||
return influence[cell.X, cell.Y];
|
return influence[cell.X, cell.Y];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Actor GetBuildingBlocking(int2 cell)
|
||||||
|
{
|
||||||
|
if (!map.IsInMap(cell) || !blocked[cell.X, cell.Y]) return null;
|
||||||
|
return influence[cell.X, cell.Y];
|
||||||
|
}
|
||||||
|
|
||||||
public bool CanMoveHere(int2 cell)
|
public bool CanMoveHere(int2 cell)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ namespace OpenRA.Traits
|
|||||||
|
|
||||||
public HazardLayer( World world )
|
public HazardLayer( World world )
|
||||||
{
|
{
|
||||||
map = world.Map;
|
|
||||||
hazards = new List<Pair<Actor, Hazard>>[world.Map.MapSize.X, world.Map.MapSize.Y];
|
hazards = new List<Pair<Actor, Hazard>>[world.Map.MapSize.X, world.Map.MapSize.Y];
|
||||||
for (int i = 0; i < world.Map.MapSize.X; i++)
|
for (int i = 0; i < world.Map.MapSize.X; i++)
|
||||||
for (int j = 0; j < world.Map.MapSize.Y; j++)
|
for (int j = 0; j < world.Map.MapSize.Y; j++)
|
||||||
|
|||||||
@@ -49,27 +49,6 @@ namespace OpenRA.Traits
|
|||||||
|
|
||||||
public void Tick( Actor self )
|
public void Tick( Actor self )
|
||||||
{
|
{
|
||||||
// Does this belong here? NO, but it's your mess.
|
|
||||||
|
|
||||||
// Get the crushable actors
|
|
||||||
foreach (var aa in self.World.Queries.WithTrait<ICrushable>())
|
|
||||||
{
|
|
||||||
var a = aa.Actor;
|
|
||||||
// Are there any units in the same cell that can crush this?
|
|
||||||
foreach( var ios in a.traits.WithInterface<IOccupySpace>() )
|
|
||||||
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 && self.World.IsActorCrushableByActor(a, b)).FirstOrDefault();
|
|
||||||
if (crusher != null)
|
|
||||||
{
|
|
||||||
// Apply the crush action
|
|
||||||
foreach (var crush in a.traits.WithInterface<ICrushable>())
|
|
||||||
crush.OnCrush(crusher);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SanityCheck( self );
|
SanityCheck( self );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,29 +49,6 @@ namespace OpenRA
|
|||||||
.Buildable;
|
.Buildable;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool IsActorCrushableByActor(this World world, Actor a, Actor b)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
//var movement = b.traits.GetOrDefault<IMove>();
|
|
||||||
//return movement != null && world.IsActorCrushableByMovementType(a, movement.GetMovementType());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Todo: Reenable crushing based on actor, not umt
|
|
||||||
/*
|
|
||||||
public static bool IsActorPathableToCrush(this World world, Actor a, UnitMovementType umt)
|
|
||||||
{
|
|
||||||
return a != null &&
|
|
||||||
a.traits.WithInterface<ICrushable>()
|
|
||||||
.Any(c => c.IsPathableCrush(umt, a.Owner));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool IsActorCrushableByMovementType(this World world, Actor a, UnitMovementType umt)
|
|
||||||
{
|
|
||||||
return a != null &&
|
|
||||||
a.traits.WithInterface<ICrushable>()
|
|
||||||
.Any(c => c.IsCrushableBy(umt, a.Owner));
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
public static IEnumerable<Actor> FindUnitsAtMouse(this World world, int2 mouseLocation)
|
public static IEnumerable<Actor> FindUnitsAtMouse(this World world, int2 mouseLocation)
|
||||||
{
|
{
|
||||||
var loc = mouseLocation + Game.viewport.Location;
|
var loc = mouseLocation + Game.viewport.Location;
|
||||||
|
|||||||
@@ -37,11 +37,11 @@ namespace OpenRA.Traits
|
|||||||
|
|
||||||
public class MobileAir : Mobile, ITick, IOccupyAir
|
public class MobileAir : Mobile, ITick, IOccupyAir
|
||||||
{
|
{
|
||||||
MobileAirInfo Info;
|
MobileAirInfo AirInfo;
|
||||||
public MobileAir (ActorInitializer init, MobileAirInfo info)
|
public MobileAir (ActorInitializer init, MobileAirInfo info)
|
||||||
: base(init, info)
|
: base(init, info)
|
||||||
{
|
{
|
||||||
Info = info;
|
AirInfo = info;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void AddInfluence()
|
public override void AddInfluence()
|
||||||
@@ -62,7 +62,9 @@ namespace OpenRA.Traits
|
|||||||
return self.World.WorldActor.traits.Get<AircraftInfluence>().GetUnitsAt(p).Count() == 0;
|
return self.World.WorldActor.traits.Get<AircraftInfluence>().GetUnitsAt(p).Count() == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual float MovementCostForCell(Actor self, int2 cell)
|
public override void FinishedMoving(Actor self) {}
|
||||||
|
|
||||||
|
public override float MovementCostForCell(Actor self, int2 cell)
|
||||||
{
|
{
|
||||||
if (!self.World.Map.IsInMap(cell.X,cell.Y))
|
if (!self.World.Map.IsInMap(cell.X,cell.Y))
|
||||||
return float.PositiveInfinity;
|
return float.PositiveInfinity;
|
||||||
@@ -73,7 +75,7 @@ namespace OpenRA.Traits
|
|||||||
return additionalCost;
|
return additionalCost;
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual float MovementSpeedForCell(Actor self, int2 cell)
|
public override float MovementSpeedForCell(Actor self, int2 cell)
|
||||||
{
|
{
|
||||||
var unitInfo = self.Info.Traits.GetOrDefault<UnitInfo>();
|
var unitInfo = self.Info.Traits.GetOrDefault<UnitInfo>();
|
||||||
if( unitInfo == null )
|
if( unitInfo == null )
|
||||||
@@ -108,14 +110,14 @@ namespace OpenRA.Traits
|
|||||||
//if (unit.Altitude <= 0)
|
//if (unit.Altitude <= 0)
|
||||||
// return;
|
// return;
|
||||||
|
|
||||||
if (unit.Altitude < Info.CruiseAltitude)
|
if (unit.Altitude < AirInfo.CruiseAltitude)
|
||||||
unit.Altitude++;
|
unit.Altitude++;
|
||||||
|
|
||||||
if (--offsetTicks <= 0)
|
if (--offsetTicks <= 0)
|
||||||
{
|
{
|
||||||
self.CenterLocation += Info.InstabilityMagnitude * self.World.SharedRandom.Gauss2D(5);
|
self.CenterLocation += AirInfo.InstabilityMagnitude * self.World.SharedRandom.Gauss2D(5);
|
||||||
unit.Altitude += (int)(Info.InstabilityMagnitude * self.World.SharedRandom.Gauss1D(5));
|
unit.Altitude += (int)(AirInfo.InstabilityMagnitude * self.World.SharedRandom.Gauss1D(5));
|
||||||
offsetTicks = Info.InstabilityTicks;
|
offsetTicks = AirInfo.InstabilityTicks;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,8 +44,6 @@ namespace OpenRA.Mods.RA.Activities
|
|||||||
return NextActivity;
|
return NextActivity;
|
||||||
}
|
}
|
||||||
var unit = self.traits.Get<Unit>();
|
var unit = self.traits.Get<Unit>();
|
||||||
var aircraft = self.traits.Get<Aircraft>();
|
|
||||||
|
|
||||||
var info = self.Info.Traits.Get<HelicopterInfo>();
|
var info = self.Info.Traits.Get<HelicopterInfo>();
|
||||||
if (unit.Altitude != info.CruiseAltitude)
|
if (unit.Altitude != info.CruiseAltitude)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -28,22 +28,24 @@ namespace OpenRA.Mods.RA
|
|||||||
{
|
{
|
||||||
class MineInfo : ITraitInfo
|
class MineInfo : ITraitInfo
|
||||||
{
|
{
|
||||||
public readonly UnitMovementType[] TriggeredBy = { };
|
public readonly string[] CrushClasses = { };
|
||||||
public readonly string Weapon = "ATMine";
|
public readonly string Weapon = "ATMine";
|
||||||
public readonly bool AvoidFriendly = true;
|
public readonly bool AvoidFriendly = true;
|
||||||
|
|
||||||
public object Create(ActorInitializer init) { return new Mine(init); }
|
public object Create(ActorInitializer init) { return new Mine(init, this); }
|
||||||
}
|
}
|
||||||
|
|
||||||
class Mine : ICrushable, IOccupySpace
|
class Mine : ICrushable, IOccupySpace
|
||||||
{
|
{
|
||||||
readonly Actor self;
|
readonly Actor self;
|
||||||
|
readonly MineInfo info;
|
||||||
[Sync]
|
[Sync]
|
||||||
readonly int2 location;
|
readonly int2 location;
|
||||||
|
|
||||||
public Mine(ActorInitializer init)
|
public Mine(ActorInitializer init, MineInfo info)
|
||||||
{
|
{
|
||||||
this.self = init.self;
|
this.self = init.self;
|
||||||
|
this.info = info;
|
||||||
this.location = init.location;
|
this.location = init.location;
|
||||||
self.World.WorldActor.traits.Get<UnitInfluence>().Add(self, this);
|
self.World.WorldActor.traits.Get<UnitInfluence>().Add(self, this);
|
||||||
}
|
}
|
||||||
@@ -57,17 +59,10 @@ namespace OpenRA.Mods.RA
|
|||||||
Combat.DoExplosion(self, info.Weapon, crusher.CenterLocation.ToInt2(), 0);
|
Combat.DoExplosion(self, info.Weapon, crusher.CenterLocation.ToInt2(), 0);
|
||||||
self.QueueActivity(new RemoveSelf());
|
self.QueueActivity(new RemoveSelf());
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsPathableCrush(UnitMovementType umt, Player player)
|
// TODO: Re-implement friendly-mine avoidance using a Hazard
|
||||||
{
|
public IEnumerable<string> CrushClasses { get { return info.CrushClasses; } }
|
||||||
return !self.Info.Traits.Get<MineInfo>().AvoidFriendly || (player != self.Owner);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsCrushableBy(UnitMovementType umt, Player player)
|
|
||||||
{
|
|
||||||
return self.Info.Traits.Get<MineInfo>().TriggeredBy.Contains(umt);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int2 TopLeft { get { return location; } }
|
public int2 TopLeft { get { return location; } }
|
||||||
|
|
||||||
public IEnumerable<int2> OccupiedCells() { yield return TopLeft; }
|
public IEnumerable<int2> OccupiedCells() { yield return TopLeft; }
|
||||||
|
|||||||
@@ -27,30 +27,24 @@ namespace OpenRA.Mods.RA
|
|||||||
{
|
{
|
||||||
public class WallInfo : ITraitInfo
|
public class WallInfo : ITraitInfo
|
||||||
{
|
{
|
||||||
public readonly UnitMovementType[] CrushableBy = { };
|
public readonly string[] CrushClasses = { };
|
||||||
|
|
||||||
public object Create(ActorInitializer init) { return new Wall(init.self); }
|
public object Create(ActorInitializer init) { return new Wall(init.self, this); }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Wall : ICrushable, IBlocksBullets
|
public class Wall : ICrushable, IBlocksBullets
|
||||||
{
|
{
|
||||||
readonly Actor self;
|
readonly Actor self;
|
||||||
|
readonly WallInfo info;
|
||||||
public Wall(Actor self)
|
|
||||||
|
public Wall(Actor self, WallInfo info)
|
||||||
{
|
{
|
||||||
this.self = self;
|
this.self = self;
|
||||||
|
this.info = info;
|
||||||
self.World.WorldActor.traits.Get<UnitInfluence>().Add(self, self.traits.Get<Building>());
|
self.World.WorldActor.traits.Get<UnitInfluence>().Add(self, self.traits.Get<Building>());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IEnumerable<string> CrushClasses { get { return info.CrushClasses; } }
|
||||||
public void OnCrush(Actor crusher) { self.InflictDamage(crusher, self.Health, null); }
|
public void OnCrush(Actor crusher) { self.InflictDamage(crusher, self.Health, null); }
|
||||||
public bool IsCrushableBy(UnitMovementType umt, Player player)
|
|
||||||
{
|
|
||||||
return self.Info.Traits.Get<WallInfo>().CrushableBy.Contains(umt);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsPathableCrush(UnitMovementType umt, Player player)
|
|
||||||
{
|
|
||||||
return IsCrushableBy(umt, player);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
|
|
||||||
### Todo: Need a ^Tank default for MovementType: Track
|
|
||||||
^Vehicle:
|
^Vehicle:
|
||||||
Category: Vehicle
|
Category: Vehicle
|
||||||
Unit:
|
Unit:
|
||||||
@@ -23,6 +21,7 @@
|
|||||||
Unit:
|
Unit:
|
||||||
ROT: 5
|
ROT: 5
|
||||||
Mobile:
|
Mobile:
|
||||||
|
Crushes: wall
|
||||||
TerrainTypes: Clear, Rough, Road, Tree, Water, Rock, Wall, Ore, Beach, River, Special
|
TerrainTypes: Clear, Rough, Road, Tree, Water, Rock, Wall, Ore, Beach, River, Special
|
||||||
TerrainSpeeds: 80%, 70%, 100%, 0%, 0%, 0%, 0%, 70%, 70%, 0%, 100%
|
TerrainSpeeds: 80%, 70%, 100%, 0%, 0%, 0%, 0%, 70%, 70%, 0%, 100%
|
||||||
Selectable:
|
Selectable:
|
||||||
@@ -112,6 +111,7 @@
|
|||||||
DamagedSound: xplos.aud
|
DamagedSound: xplos.aud
|
||||||
DestroyedSound: xplobig4.aud
|
DestroyedSound: xplobig4.aud
|
||||||
Wall:
|
Wall:
|
||||||
|
CrushClasses: wall
|
||||||
LineBuild:
|
LineBuild:
|
||||||
Selectable:
|
Selectable:
|
||||||
Priority: 1
|
Priority: 1
|
||||||
|
|||||||
@@ -465,6 +465,8 @@ BRIK:
|
|||||||
Armor: none
|
Armor: none
|
||||||
RenderBuildingWall:
|
RenderBuildingWall:
|
||||||
DamageStates: 4
|
DamageStates: 4
|
||||||
|
Wall:
|
||||||
|
CrushClasses: heavywall
|
||||||
|
|
||||||
GUN:
|
GUN:
|
||||||
Category: Defense
|
Category: Defense
|
||||||
|
|||||||
@@ -209,7 +209,6 @@ World:
|
|||||||
ResourceLayer:
|
ResourceLayer:
|
||||||
ResourceType@green-tib:
|
ResourceType@green-tib:
|
||||||
ResourceType: 1
|
ResourceType: 1
|
||||||
MovementTerrainType: Tiberium
|
|
||||||
Palette: terrain
|
Palette: terrain
|
||||||
SpriteNames: ti1,ti2,ti3,ti4,ti5,ti6,ti7,ti8,ti9,ti10,ti11,ti12
|
SpriteNames: ti1,ti2,ti3,ti4,ti5,ti6,ti7,ti8,ti9,ti10,ti11,ti12
|
||||||
ValuePerUnit: 30
|
ValuePerUnit: 30
|
||||||
|
|||||||
@@ -294,6 +294,8 @@ HTNK:
|
|||||||
Explodes:
|
Explodes:
|
||||||
Weapon: UnitExplodeSmall
|
Weapon: UnitExplodeSmall
|
||||||
EmptyWeapon: UnitExplodeSmall
|
EmptyWeapon: UnitExplodeSmall
|
||||||
|
Mobile:
|
||||||
|
Crushes: wall, heavywall
|
||||||
|
|
||||||
MSAM:
|
MSAM:
|
||||||
Inherits: ^Tank
|
Inherits: ^Tank
|
||||||
|
|||||||
Reference in New Issue
Block a user