Moved Minelaying related traits and activity to Common

This commit is contained in:
michaeldgg2
2023-06-24 11:47:15 +02:00
committed by abcdefg30
parent 36420114e0
commit 5ab3276a2d
3 changed files with 4 additions and 8 deletions

View File

@@ -0,0 +1,157 @@
#region Copyright & License Information
/*
* Copyright (c) The OpenRA Developers and Contributors
* This file is part of OpenRA, which is free software. It is made
* available to you under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version. For more
* information, see COPYING.
*/
#endregion
using System.Collections.Generic;
using System.Linq;
using OpenRA.Activities;
using OpenRA.Mods.Common.Traits;
using OpenRA.Primitives;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Activities
{
// Assumes you have Minelayer on that unit
public class LayMines : Activity
{
readonly Minelayer minelayer;
readonly AmmoPool[] ammoPools;
readonly IMove movement;
readonly IMoveInfo moveInfo;
readonly RearmableInfo rearmableInfo;
List<CPos> minefield;
bool returnToBase;
Actor rearmTarget;
public LayMines(Actor self, List<CPos> minefield = null)
{
minelayer = self.Trait<Minelayer>();
ammoPools = self.TraitsImplementing<AmmoPool>().ToArray();
movement = self.Trait<IMove>();
moveInfo = self.Info.TraitInfo<IMoveInfo>();
rearmableInfo = self.Info.TraitInfoOrDefault<RearmableInfo>();
this.minefield = minefield;
}
protected override void OnFirstRun(Actor self)
{
minefield ??= new List<CPos> { self.Location };
}
CPos? NextValidCell(Actor self)
{
if (minefield != null)
foreach (var c in minefield)
if (CanLayMine(self, c))
return c;
return null;
}
public override bool Tick(Actor self)
{
returnToBase = false;
if (IsCanceling)
return true;
if ((minefield == null || minefield.Contains(self.Location)) && CanLayMine(self, self.Location))
{
if (rearmableInfo != null && ammoPools.Any(p => p.Info.Name == minelayer.Info.AmmoPoolName && !p.HasAmmo))
{
// Rearm (and possibly repair) at rearm building, then back out here to refill the minefield some more
rearmTarget = self.World.Actors.Where(a => self.Owner.RelationshipWith(a.Owner) == PlayerRelationship.Ally && rearmableInfo.RearmActors.Contains(a.Info.Name))
.ClosestTo(self);
if (rearmTarget == null)
return true;
// Add a CloseEnough range of 512 to the Rearm/Repair activities in order to ensure that we're at the host actor
QueueChild(new MoveAdjacentTo(self, Target.FromActor(rearmTarget)));
QueueChild(movement.MoveTo(self.World.Map.CellContaining(rearmTarget.CenterPosition), ignoreActor: rearmTarget));
QueueChild(new Resupply(self, rearmTarget, new WDist(512)));
returnToBase = true;
return false;
}
LayMine(self);
QueueChild(new Wait(20)); // A little wait after placing each mine, for show
minefield.Remove(self.Location);
return false;
}
var nextCell = NextValidCell(self);
if (nextCell != null)
{
QueueChild(movement.MoveTo(nextCell.Value, 0));
return false;
}
// TODO: Return somewhere likely to be safe (near rearm building) so we're not sitting out in the minefield.
return true;
}
public void CleanMineField(Actor self)
{
// Remove cells that have already been mined
// or that are revealed to be unmineable.
if (minefield != null)
{
var positionable = (IPositionable)movement;
var mobile = positionable as Mobile;
minefield.RemoveAll(c => self.World.ActorMap.GetActorsAt(c)
.Any(a => a.Info.Name == minelayer.Info.Mine.ToLowerInvariant() && a.CanBeViewedByPlayer(self.Owner)) ||
((!positionable.CanEnterCell(c, null, BlockedByActor.Immovable) || (mobile != null && !mobile.CanStayInCell(c)))
&& self.Owner.Shroud.IsVisible(c)));
}
}
public override IEnumerable<TargetLineNode> TargetLineNodes(Actor self)
{
if (returnToBase)
yield return new TargetLineNode(Target.FromActor(rearmTarget), moveInfo.GetTargetLineColor());
if (minefield == null || minefield.Count == 0)
yield break;
var nextCell = NextValidCell(self);
if (nextCell != null)
yield return new TargetLineNode(Target.FromCell(self.World, nextCell.Value), minelayer.Info.TargetLineColor);
foreach (var c in minefield)
yield return new TargetLineNode(Target.FromCell(self.World, c), minelayer.Info.TargetLineColor, tile: minelayer.Tile);
}
static bool CanLayMine(Actor self, CPos p)
{
// If there is no unit (other than me) here, we can place a mine here
return self.World.ActorMap.GetActorsAt(p).All(a => a == self);
}
void LayMine(Actor self)
{
if (ammoPools != null)
{
var pool = ammoPools.FirstOrDefault(x => x.Info.Name == minelayer.Info.AmmoPoolName);
if (pool == null)
return;
pool.TakeAmmo(self, minelayer.Info.AmmoUsage);
}
self.World.AddFrameEndTask(w => w.CreateActor(minelayer.Info.Mine, new TypeDictionary
{
new LocationInit(self.Location),
new OwnerInit(self.Owner),
}));
}
}
}

View File

@@ -0,0 +1,74 @@
#region Copyright & License Information
/*
* Copyright (c) The OpenRA Developers and Contributors
* This file is part of OpenRA, which is free software. It is made
* available to you under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version. For more
* information, see COPYING.
*/
#endregion
using OpenRA.Primitives;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Traits
{
sealed class MineInfo : TraitInfo
{
public readonly BitSet<CrushClass> CrushClasses = default;
public readonly bool AvoidFriendly = true;
public readonly bool BlockFriendly = true;
public readonly BitSet<CrushClass> DetonateClasses = default;
public override object Create(ActorInitializer init) { return new Mine(this); }
}
sealed class Mine : ICrushable, INotifyCrushed
{
readonly MineInfo info;
public Mine(MineInfo info)
{
this.info = info;
}
void INotifyCrushed.WarnCrush(Actor self, Actor crusher, BitSet<CrushClass> crushClasses) { }
void INotifyCrushed.OnCrush(Actor self, Actor crusher, BitSet<CrushClass> crushClasses)
{
if (!info.CrushClasses.Overlaps(crushClasses))
return;
if (crusher.Info.HasTraitInfo<MineImmuneInfo>() || (self.Owner.RelationshipWith(crusher.Owner) == PlayerRelationship.Ally && info.AvoidFriendly))
return;
var mobile = crusher.TraitOrDefault<Mobile>();
if (mobile != null && !info.DetonateClasses.Overlaps(mobile.Info.LocomotorInfo.Crushes))
return;
self.Kill(crusher, mobile != null ? mobile.Info.LocomotorInfo.CrushDamageTypes : default);
}
bool ICrushable.CrushableBy(Actor self, Actor crusher, BitSet<CrushClass> crushClasses)
{
if (info.BlockFriendly && !crusher.Info.HasTraitInfo<MineImmuneInfo>() && self.Owner.RelationshipWith(crusher.Owner) == PlayerRelationship.Ally)
return false;
return info.CrushClasses.Overlaps(crushClasses);
}
LongBitSet<PlayerBitMask> ICrushable.CrushableBy(Actor self, BitSet<CrushClass> crushClasses)
{
if (!info.CrushClasses.Overlaps(crushClasses))
return self.World.NoPlayersMask;
// Friendly units should move around!
return info.BlockFriendly ? self.World.AllPlayersMask.Except(self.Owner.AlliedPlayersMask) : self.World.AllPlayersMask;
}
}
[Desc("Tag trait for stuff that should not trigger mines.")]
sealed class MineImmuneInfo : TraitInfo<MineImmune> { }
sealed class MineImmune { }
}

View File

@@ -0,0 +1,382 @@
#region Copyright & License Information
/*
* Copyright (c) The OpenRA Developers and Contributors
* This file is part of OpenRA, which is free software. It is made
* available to you under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version. For more
* information, see COPYING.
*/
#endregion
using System;
using System.Collections.Generic;
using System.Linq;
using OpenRA.Graphics;
using OpenRA.Mods.Common.Activities;
using OpenRA.Mods.Common.Orders;
using OpenRA.Primitives;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Traits
{
public class MinelayerInfo : TraitInfo, Requires<RearmableInfo>
{
[ActorReference]
public readonly string Mine = "minv";
public readonly string AmmoPoolName = "primary";
public readonly WDist MinefieldDepth = new(1536);
[VoiceReference]
[Desc("Voice to use when ordered to lay a minefield.")]
public readonly string Voice = "Action";
[Desc("Color to use for the target line when laying mines.")]
public readonly Color TargetLineColor = Color.Crimson;
[Desc("Sprite overlay to use for valid minefield cells.")]
public readonly string TileValidName = "build-valid";
[Desc("Sprite overlay to use for invalid minefield cells.")]
public readonly string TileInvalidName = "build-invalid";
[Desc("Sprite overlay to use for minefield cells hidden behind fog or shroud.")]
public readonly string TileUnknownName = "build-unknown";
[Desc("Only allow laying mines on listed terrain types. Leave empty to allow all terrain types.")]
public readonly HashSet<string> TerrainTypes = new();
[CursorReference]
[Desc("Cursor to display when able to lay a mine.")]
public readonly string DeployCursor = "deploy";
[CursorReference]
[Desc("Cursor to display when unable to lay a mine.")]
public readonly string DeployBlockedCursor = "deploy-blocked";
[CursorReference]
[Desc("Cursor to display when able to lay a mine.")]
public readonly string AbilityCursor = "ability";
[Desc("Ammo the minelayer consumes per mine.")]
public readonly int AmmoUsage = 1;
public override object Create(ActorInitializer init) { return new Minelayer(init.Self, this); }
}
public class Minelayer : IIssueOrder, IResolveOrder, ISync, IIssueDeployOrder, IOrderVoice, ITick
{
public readonly MinelayerInfo Info;
public readonly Sprite Tile;
readonly Actor self;
[Sync]
CPos minefieldStart;
public Minelayer(Actor self, MinelayerInfo info)
{
Info = info;
this.self = self;
var tileset = self.World.Map.Tileset.ToLowerInvariant();
var sequences = self.World.Map.Sequences;
if (sequences.HasSequence("overlay", $"{Info.TileValidName}-{tileset}"))
Tile = sequences.GetSequence("overlay", $"{Info.TileValidName}-{tileset}").GetSprite(0);
else
Tile = sequences.GetSequence("overlay", Info.TileValidName).GetSprite(0);
}
IEnumerable<IOrderTargeter> IIssueOrder.Orders
{
get
{
yield return new BeginMinefieldOrderTargeter(Info.AbilityCursor);
yield return new DeployOrderTargeter("PlaceMine", 5, () => IsCellAcceptable(self, self.Location) ? Info.DeployCursor : Info.DeployBlockedCursor);
}
}
Order IIssueOrder.IssueOrder(Actor self, IOrderTargeter order, in Target target, bool queued)
{
switch (order.OrderID)
{
case "BeginMinefield":
var start = self.World.Map.CellContaining(target.CenterPosition);
if (self.World.OrderGenerator is MinefieldOrderGenerator generator)
generator.AddMinelayer(self);
else
self.World.OrderGenerator = new MinefieldOrderGenerator(self, start, queued);
return new Order("BeginMinefield", self, Target.FromCell(self.World, start), queued);
case "PlaceMine":
return new Order("PlaceMine", self, Target.FromCell(self.World, self.Location), queued);
default:
return null;
}
}
Order IIssueDeployOrder.IssueDeployOrder(Actor self, bool queued)
{
return new Order("PlaceMine", self, Target.FromCell(self.World, self.Location), queued);
}
bool IIssueDeployOrder.CanIssueDeployOrder(Actor self, bool queued)
{
return IsCellAcceptable(self, self.Location);
}
void IResolveOrder.ResolveOrder(Actor self, Order order)
{
if (order.OrderString != "BeginMinefield" && order.OrderString != "PlaceMinefield" && order.OrderString != "PlaceMine")
return;
var cell = self.World.Map.CellContaining(order.Target.CenterPosition);
if (order.OrderString == "BeginMinefield")
minefieldStart = cell;
else if (order.OrderString == "PlaceMine")
{
if (IsCellAcceptable(self, cell))
self.QueueActivity(order.Queued, new LayMines(self));
}
else if (order.OrderString == "PlaceMinefield")
{
// A different minelayer might have started laying the field without this minelayer knowing the start
minefieldStart = order.ExtraLocation;
var movement = self.Trait<IPositionable>();
var minefield = GetMinefieldCells(minefieldStart, cell, Info.MinefieldDepth)
.Where(c => IsCellAcceptable(self, c) && self.Owner.Shroud.IsExplored(c)
&& movement.CanEnterCell(c, null, BlockedByActor.Immovable) && movement is Mobile mobile && mobile.CanStayInCell(c))
.OrderBy(c => (c - minefieldStart).LengthSquared).ToList();
self.QueueActivity(order.Queued, new LayMines(self, minefield));
self.ShowTargetLines();
}
}
void ITick.Tick(Actor self)
{
if (self.CurrentActivity != null)
foreach (var field in self.CurrentActivity.ActivitiesImplementing<LayMines>())
field.CleanMineField(self);
}
string IOrderVoice.VoicePhraseForOrder(Actor self, Order order)
{
if (order.OrderString == "PlaceMine" || order.OrderString == "PlaceMinefield")
return Info.Voice;
return null;
}
static IEnumerable<CPos> GetMinefieldCells(CPos start, CPos end, WDist depth)
{
var mins = new CPos(Math.Min(start.X, end.X), Math.Min(start.Y, end.Y));
var maxs = new CPos(Math.Max(start.X, end.X), Math.Max(start.Y, end.Y));
// TODO: proper endcaps, if anyone cares (which won't happen unless depth is large)
var p = end - start;
var q = new float2(p.Y, -p.X);
q = (start != end) ? 1 / q.Length * q : new float2(1, 0);
var c = -float2.Dot(q, new float2(start.X, start.Y));
// return all points such that |ax + by + c| < depth
// HACK: This will return the wrong results for isometric cells
for (var i = mins.X; i <= maxs.X; i++)
for (var j = mins.Y; j <= maxs.Y; j++)
if (Math.Abs(q.X * i + q.Y * j + c) * 1024 < depth.Length)
yield return new CPos(i, j);
}
public bool IsCellAcceptable(Actor self, CPos cell)
{
if (!self.World.Map.Contains(cell))
return false;
if (Info.TerrainTypes.Count == 0)
return true;
var terrainType = self.World.Map.GetTerrainInfo(cell).Type;
return Info.TerrainTypes.Contains(terrainType);
}
sealed class MinefieldOrderGenerator : OrderGenerator
{
readonly List<Actor> minelayers;
readonly Minelayer minelayer;
readonly Sprite validTile, unknownTile, blockedTile;
readonly float validAlpha, unknownAlpha, blockedAlpha;
readonly CPos minefieldStart;
readonly bool queued;
readonly string cursor;
public MinefieldOrderGenerator(Actor a, CPos xy, bool queued)
{
minelayers = new List<Actor>() { a };
minefieldStart = xy;
this.queued = queued;
minelayer = a.Trait<Minelayer>();
var tileset = a.World.Map.Tileset.ToLowerInvariant();
var sequences = a.World.Map.Sequences;
if (sequences.HasSequence("overlay", $"{minelayer.Info.TileValidName}-{tileset}"))
{
var validSequence = sequences.GetSequence("overlay", $"{minelayer.Info.TileValidName}-{tileset}");
validTile = validSequence.GetSprite(0);
validAlpha = validSequence.GetAlpha(0);
}
else
{
var validSequence = sequences.GetSequence("overlay", minelayer.Info.TileValidName);
validTile = validSequence.GetSprite(0);
validAlpha = validSequence.GetAlpha(0);
}
if (sequences.HasSequence("overlay", $"{minelayer.Info.TileUnknownName}-{tileset}"))
{
var unknownSequence = sequences.GetSequence("overlay", $"{minelayer.Info.TileUnknownName}-{tileset}");
unknownTile = unknownSequence.GetSprite(0);
unknownAlpha = unknownSequence.GetAlpha(0);
}
else
{
var unknownSequence = sequences.GetSequence("overlay", minelayer.Info.TileUnknownName);
unknownTile = unknownSequence.GetSprite(0);
unknownAlpha = unknownSequence.GetAlpha(0);
}
if (sequences.HasSequence("overlay", $"{minelayer.Info.TileInvalidName}-{tileset}"))
{
var blockedSequence = sequences.GetSequence("overlay", $"{minelayer.Info.TileInvalidName}-{tileset}");
blockedTile = blockedSequence.GetSprite(0);
blockedAlpha = blockedSequence.GetAlpha(0);
}
else
{
var blockedSequence = sequences.GetSequence("overlay", minelayer.Info.TileInvalidName);
blockedTile = blockedSequence.GetSprite(0);
blockedAlpha = blockedSequence.GetAlpha(0);
}
cursor = minelayer.Info.AbilityCursor;
}
public void AddMinelayer(Actor a)
{
minelayers.Add(a);
}
protected override IEnumerable<Order> OrderInner(World world, CPos cell, int2 worldPixel, MouseInput mi)
{
if (mi.Button == Game.Settings.Game.MouseButtonPreference.Cancel)
{
world.CancelInputMode();
yield break;
}
if (mi.Button == Game.Settings.Game.MouseButtonPreference.Action)
{
minelayers.First().World.CancelInputMode();
foreach (var minelayer in minelayers)
yield return new Order("PlaceMinefield", minelayer, Target.FromCell(world, cell), queued) { ExtraLocation = minefieldStart };
}
}
protected override void SelectionChanged(World world, IEnumerable<Actor> selected)
{
minelayers.Clear();
minelayers.AddRange(selected.Where(s => !s.IsDead && s.Info.HasTraitInfo<MinelayerInfo>()));
if (minelayers.Count == 0)
world.CancelInputMode();
}
protected override IEnumerable<IRenderable> Render(WorldRenderer wr, World world) { yield break; }
protected override IEnumerable<IRenderable> RenderAboveShroud(WorldRenderer wr, World world)
{
var minelayer = minelayers.FirstOrDefault(m => m.IsInWorld && !m.IsDead);
if (minelayer == null)
yield break;
// We get the biggest depth so we cover all cells that mines could be placed on.
var lastMousePos = wr.Viewport.ViewToWorld(Viewport.LastMousePos);
var minefield = GetMinefieldCells(minefieldStart, lastMousePos,
minelayers.Max(m => m.Info.TraitInfo<MinelayerInfo>().MinefieldDepth));
var movement = minelayer.Trait<IPositionable>();
var mobile = movement as Mobile;
var pal = wr.Palette(TileSet.TerrainPaletteInternalName);
foreach (var c in minefield)
{
var tile = validTile;
var alpha = validAlpha;
if (!world.Map.Contains(c))
{
tile = blockedTile;
alpha = blockedAlpha;
}
else if (world.ShroudObscures(c))
{
tile = blockedTile;
alpha = blockedAlpha;
}
else if (world.FogObscures(c))
{
tile = unknownTile;
alpha = unknownAlpha;
}
else if (!this.minelayer.IsCellAcceptable(minelayer, c)
|| !movement.CanEnterCell(c, null, BlockedByActor.Immovable) || (mobile != null && !mobile.CanStayInCell(c)))
{
tile = blockedTile;
alpha = blockedAlpha;
}
yield return new SpriteRenderable(tile, world.Map.CenterOfCell(c), WVec.Zero, -511, pal, 1f, alpha, float3.Ones, TintModifiers.IgnoreWorldTint, true);
}
}
protected override IEnumerable<IRenderable> RenderAnnotations(WorldRenderer wr, World world) { yield break; }
protected override string GetCursor(World world, CPos cell, int2 worldPixel, MouseInput mi)
{
return cursor;
}
}
sealed class BeginMinefieldOrderTargeter : IOrderTargeter
{
public string OrderID => "BeginMinefield";
public int OrderPriority => 5;
readonly string cursor;
public BeginMinefieldOrderTargeter(string cursor)
{
this.cursor = cursor;
}
public bool TargetOverridesSelection(Actor self, in Target target, List<Actor> actorsAt, CPos xy, TargetModifiers modifiers) { return true; }
public bool CanTarget(Actor self, in Target target, ref TargetModifiers modifiers, ref string cursor)
{
if (target.Type != TargetType.Terrain)
return false;
var location = self.World.Map.CellContaining(target.CenterPosition);
if (!self.World.Map.Contains(location))
return false;
cursor = this.cursor;
IsQueued = modifiers.HasModifier(TargetModifiers.ForceQueue);
return modifiers.HasModifier(TargetModifiers.ForceAttack);
}
public bool IsQueued { get; private set; }
}
}
}