Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add SpawnActorWarhead #21364

Open
wants to merge 2 commits into
base: bleed
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
222 changes: 222 additions & 0 deletions OpenRA.Mods.Common/Warheads/SpawnActorWarhead.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
#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.GameRules;
using OpenRA.Mods.Common.Activities;
using OpenRA.Mods.Common.Effects;
using OpenRA.Mods.Common.Traits;
using OpenRA.Primitives;
using OpenRA.Traits;

namespace OpenRA.Mods.Common.Warheads
{
[Desc("Spawns actors upon detonation.")]
public class SpawnActorWarhead : Warhead, IRulesetLoaded<WeaponInfo>
{
[FieldLoader.Require]
[ActorReference]
[Desc("Actors to spawn.")]
public readonly string[] Actors;
PunkPun marked this conversation as resolved.
Show resolved Hide resolved

[Desc("The cell range to try placing the actors within.")]
public readonly int Range = 10;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How is the location determined? First available cell in circle radius or also semi random?

Won't it look bad if it's always the same pattern and order of units?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea is to allow the actor to spawn even if centre tiles are occupied. I don't believe we have a function that would randomise with ranked range


[Desc("Map player to give the actors to. Defaults to the attacker's owner.")]
public readonly string Owner;

[Desc("Should this actor link to the actor which created it?")]
public readonly bool LinkToParent = false;

[Desc("Should actors always be spawned on the ground?")]
public readonly bool ForceGround = false;

[Desc("Should actors spawn facing the same direction as the projectile?")]
public readonly bool SetFacing = false;

[Desc("Don't spawn actors on this terrain.")]
public readonly HashSet<string> InvalidTerrain = new();

[Desc("Defines the image of an optional animation played at the spawning location.")]
public readonly string Image;

[SequenceReference(nameof(Image), allowNullImage: true)]
[Desc("Defines the sequence of an optional animation played at the spawning location.")]
public readonly string Sequence = "idle";

[PaletteReference]
[Desc("Defines the palette of an optional animation played at the spawning location.")]
public readonly string Palette = "effect";

public readonly bool UsePlayerPalette = false;

[Desc("List of sounds that can be played at the spawning location.")]
public readonly string[] Sounds = Array.Empty<string>();

void IRulesetLoaded<WeaponInfo>.RulesetLoaded(Ruleset rules, WeaponInfo info)
{
foreach (var a in Actors)
{
var actorInfo = rules.Actors[a.ToLowerInvariant()];
var buildingInfo = actorInfo.TraitInfoOrDefault<BuildingInfo>();

if (buildingInfo != null)
throw new YamlException($"SpawnActorWarhead cannot be used to spawn building actor '{a}'!");
}
}

public override void DoImpact(in Target target, WarheadArgs args)
{
var firedBy = args.SourceActor;
if (!target.IsValidFor(firedBy))
return;

var world = firedBy.World;
var map = world.Map;
var pos = target.CenterPosition;
var targetCell = map.CellContaining(pos);
if (!map.Contains(targetCell))
return;

var directHit = false;
foreach (var victim in world.FindActorsOnCircle(pos, WDist.Zero))
{
if (!IsValidAgainst(victim, firedBy))
continue;

if (!victim.Info.HasTraitInfo<IHealthInfo>())
continue;

if (victim.TraitsImplementing<HitShape>()
.Any(i => !i.IsTraitDisabled && i.DistanceFromEdge(victim, pos).Length <= 0))
{
directHit = true;
break;
}
}

if (!directHit && !IsValidTarget(map.GetTerrainInfo(targetCell).TargetTypes))
return;

foreach (var a in Actors)
{
var td = new TypeDictionary();
var actorName = a.ToLowerInvariant();
var ai = map.Rules.Actors[actorName];

var owner = Owner == null ? firedBy.Owner : world.Players.First(p => p.InternalName == Owner);
td.Add(new OwnerInit(owner));

if (LinkToParent)
td.Add(new ParentActorInit(firedBy));

if (SetFacing)
td.Add(new FacingInit(args.ImpactOrientation.Yaw));

var cachedTarget = target;
world.AddFrameEndTask(w =>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trivial. But could potentially become a long running frame end task.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is that a problem? Everything that handles adding or removing actors to world needs to be in frame end task to avoid a System.InvalidOperationException

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking of #20903.

But yeah, it may not matter if it's executed as one or multiple tasks, needs to get executed.

{
var immobileInfo = ai.TraitInfoOrDefault<ImmobileInfo>();
if (immobileInfo != null)
{
foreach (var cell in map.FindTilesInCircle(targetCell, Range))
{
if (!InvalidTerrain.Contains(map.GetTerrainInfo(cell).Type)
&& (!immobileInfo.OccupiesSpace || !world.ActorMap.GetActorsAt(cell).Any()))
{
td.Add(new LocationInit(cell));
world.CreateActor(actorName, td);
PlaySoundAndEffect(w, owner, map.CenterOfCell(cell));
return;
}
}

return;
}

var unit = world.CreateActor(false, actorName, td);
var positionable = unit.Trait<IPositionable>();
foreach (var cell in map.FindTilesInCircle(targetCell, Range))
{
if (InvalidTerrain.Contains(map.GetTerrainInfo(cell).Type))
continue;

if (positionable is Aircraft aircraft)
{
var position = map.CenterOfCell(cell);
var dat = map.DistanceAboveTerrain(cachedTarget.CenterPosition);
var isAtGroundLevel = ForceGround || dat.Length <= 0;
if (!aircraft.Info.TakeOffOnCreation && isAtGroundLevel)
{
if (aircraft.CanLand(cell))
{
positionable.SetPosition(unit, position);
aircraft.AddInfluence(cell);
aircraft.FinishedMoving(unit);
}
else
continue;
}
else
{
position = new WPos(position.X, position.Y, Math.Max(position.Z, dat.Length));
positionable.SetPosition(unit, position);
unit.QueueActivity(new FlyIdle(unit));
}

PlaySoundAndEffect(w, owner, position);
w.Add(unit);
return;
}
else
{
var subCell = positionable.GetAvailableSubCell(cell);
if (subCell != SubCell.Invalid)
{
positionable.SetPosition(unit, cell, subCell);
var position = positionable.CenterPosition;
if (positionable is Mobile mobile)
{
var dat = map.DistanceAboveTerrain(cachedTarget.CenterPosition);
if (!ForceGround && dat.Length > 0)
{
positionable.SetCenterPosition(unit,
new WPos(position.X, position.Y, Math.Max(position.Z, dat.Length)));

unit.QueueActivity(mobile.ReturnToCell(unit));
}
}

PlaySoundAndEffect(w, owner, position);
w.Add(unit);
return;
}
}
}

unit.Dispose();
});
}
}

void PlaySoundAndEffect(World w, Player owner, WPos pos)
{
if (Image != null)
w.Add(new SpriteEffect(pos, w, Image, Sequence, UsePlayerPalette ? Palette + owner.InternalName : Palette));

var sound = Sounds.RandomOrDefault(Game.CosmeticRandom);
if (sound != null)
Game.Sound.Play(SoundType.World, sound, pos);
}
}
}
4 changes: 3 additions & 1 deletion mods/ra/weapons/ballistics.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -176,13 +176,15 @@ Grenade:
Image: BOMB
Warhead@1Dam: SpreadDamage
Spread: 256
Damage: 6000
Damage: 0
Versus:
None: 60
Wood: 100
Light: 25
Heavy: 25
Concrete: 100
Warhead@spawn: SpawnActor
Actors: heli
Warhead@3Eff: CreateEffect
Explosions: med_explosion
ImpactSounds: kaboom25.aud
Expand Down
10 changes: 8 additions & 2 deletions mods/ra/weapons/missiles.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ Dragon:
Inherits: ^AntiGroundMissile
Projectile: Missile
TrailImage: smokey
Warhead@spawn: SpawnActor
Actors: e1

HellfireAG:
Inherits: ^AntiGroundMissile
Expand Down Expand Up @@ -152,7 +154,9 @@ Nike:
RangeLimit: 11c0
Speed: 341
Warhead@1Dam: SpreadDamage
Damage: 4500
Damage: 0
Warhead@spawn: SpawnActor
Actors: heli
Warhead@3Eff: CreateEffect
Explosions: med_explosion_air
ImpactSounds: kaboom25.aud
Expand All @@ -165,7 +169,9 @@ RedEye:
HorizontalRateOfTurn: 80
Speed: 298
Warhead@1Dam: SpreadDamage
Damage: 2400
Damage: 0
Warhead@spawn: SpawnActor
Actors: e1

Stinger:
Inherits: ^AntiGroundMissile
Expand Down