Initial commit: OpenRA game engine
Some checks failed
Continuous Integration / Linux (.NET 8.0) (push) Has been cancelled
Continuous Integration / Windows (.NET 8.0) (push) Has been cancelled

Fork from OpenRA/OpenRA with one-click launch script (start-ra.cmd)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
let5sne.win10
2026-01-10 21:46:54 +08:00
commit 9cf6ebb986
4065 changed files with 635973 additions and 0 deletions

View File

@@ -0,0 +1,57 @@
#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.GameRules;
using OpenRA.Mods.Common.Traits;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Warheads
{
[Desc("Interacts with the `" + nameof(TemporaryOwnerManager) + "` trait.")]
public class ChangeOwnerWarhead : Warhead
{
[Desc("Duration of the owner change (in ticks). Set to 0 to make it permanent.")]
public readonly int Duration = 0;
public readonly WDist Range = WDist.FromCells(1);
public override void DoImpact(in Target target, WarheadArgs args)
{
var firedBy = args.SourceActor;
var actors = target.Type == TargetType.Actor ? [target.Actor] :
firedBy.World.FindActorsInCircle(target.CenterPosition, Range);
foreach (var a in actors)
{
if (!IsValidAgainst(a, firedBy))
continue;
// Don't do anything on friendly fire
if (a.Owner == firedBy.Owner)
continue;
if (Duration == 0)
a.ChangeOwner(firedBy.Owner); // Permanent
else
{
var tempOwnerManager = a.TraitOrDefault<TemporaryOwnerManager>();
if (tempOwnerManager == null)
continue;
tempOwnerManager.ChangeOwner(a, firedBy.Owner, Duration);
}
// Stop shooting, you have new enemies
a.CancelActivity();
}
}
}
}

View File

@@ -0,0 +1,149 @@
#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.Immutable;
using System.Linq;
using OpenRA.GameRules;
using OpenRA.Mods.Common.Effects;
using OpenRA.Mods.Common.Traits;
using OpenRA.Primitives;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Warheads
{
[Desc("Spawn a sprite with sound.")]
public class CreateEffectWarhead : Warhead
{
[SequenceReference(nameof(Image), allowNullImage: true)]
[Desc("List of explosion sequences that can be used.")]
public readonly ImmutableArray<string> Explosions = [];
[Desc("Image containing explosion effect sequence.")]
public readonly string Image = "explosion";
[PaletteReference(nameof(UsePlayerPalette))]
[Desc("Palette to use for explosion effect.")]
public readonly string ExplosionPalette = "effect";
[Desc("Remap explosion effect to player color, if art supports it.")]
public readonly bool UsePlayerPalette = false;
[Desc("Display explosion effect at ground level, regardless of explosion altitude.")]
public readonly bool ForceDisplayAtGroundLevel = false;
[Desc("List of sounds that can be played on impact.")]
public readonly ImmutableArray<string> ImpactSounds = [];
[Desc("Chance of impact sound to play.")]
public readonly int ImpactSoundChance = 100;
[Desc("Whether to consider actors in determining whether the explosion should happen. If false, only terrain will be considered.")]
public readonly bool ImpactActors = true;
[Desc("The maximum inaccuracy of the effect spawn position relative to actual impact position.")]
public readonly WDist Inaccuracy = WDist.Zero;
static readonly BitSet<TargetableType> TargetTypeAir = new("Air");
/// <summary>Checks if there are any actors at impact position and if the warhead is valid against any of them.</summary>
ImpactActorType ActorTypeAtImpact(World world, WPos pos, Actor firedBy)
{
var anyInvalidActor = false;
// Check whether the impact position overlaps with an actor's hitshape
foreach (var victim in world.FindActorsOnCircle(pos, WDist.Zero))
{
if (!AffectsParent && victim == firedBy)
continue;
var activeShapes = victim.TraitsImplementing<HitShape>().Where(t => !t.IsTraitDisabled);
if (!activeShapes.Any(s => s.DistanceFromEdge(victim, pos).Length <= 0))
continue;
if (IsValidAgainst(victim, firedBy))
return ImpactActorType.Valid;
anyInvalidActor = true;
}
return anyInvalidActor ? ImpactActorType.Invalid : ImpactActorType.None;
}
// ActorTypeAtImpact already checks AffectsParent beforehand, to avoid parent HitShape look-ups
// (and to prevent returning ImpactActorType.Invalid on AffectsParent=false)
public override bool IsValidAgainst(Actor victim, Actor firedBy)
{
var relationship = firedBy.Owner.RelationshipWith(victim.Owner);
if (!ValidRelationships.HasRelationship(relationship))
return false;
// A target type is valid if it is in the valid targets list, and not in the invalid targets list.
if (!IsValidTarget(victim.GetEnabledTargetTypes()))
return false;
return true;
}
public override void DoImpact(in Target target, WarheadArgs args)
{
if (target.Type == TargetType.Invalid)
return;
var firedBy = args.SourceActor;
var pos = target.CenterPosition;
var world = firedBy.World;
var actorAtImpact = ImpactActors ? ActorTypeAtImpact(world, pos, firedBy) : ImpactActorType.None;
// Ignore the impact if there are only invalid actors within range
if (actorAtImpact == ImpactActorType.Invalid)
return;
// Ignore the impact if there are no valid actors and no valid terrain
// (impacts are allowed on valid actors sitting on invalid terrain!)
if (actorAtImpact == ImpactActorType.None && !IsValidAgainstTerrain(world, pos))
return;
var explosion = Explosions.RandomOrDefault(world.LocalRandom);
if (Image != null && explosion != null)
{
if (Inaccuracy.Length > 0)
pos += WVec.FromPDF(world.SharedRandom, 2) * Inaccuracy.Length / 1024;
if (ForceDisplayAtGroundLevel)
{
var dat = world.Map.DistanceAboveTerrain(pos);
pos -= new WVec(0, 0, dat.Length);
}
var palette = ExplosionPalette;
if (UsePlayerPalette)
palette += firedBy.Owner.InternalName;
world.AddFrameEndTask(w => w.Add(new SpriteEffect(pos, w, Image, explosion, palette)));
}
var impactSound = ImpactSounds.RandomOrDefault(world.LocalRandom);
if (impactSound != null && world.LocalRandom.Next(0, 100) < ImpactSoundChance)
Game.Sound.Play(SoundType.World, impactSound, pos);
}
/// <summary>Checks if the warhead is valid against the terrain at impact position.</summary>
bool IsValidAgainstTerrain(World world, WPos pos)
{
var cell = world.Map.CellContaining(pos);
if (!world.Map.Contains(cell))
return false;
var dat = world.Map.DistanceAboveTerrain(pos);
return IsValidTarget(dat > AirThreshold ? TargetTypeAir : world.Map.GetTerrainInfo(cell).TargetTypes);
}
}
}

View File

@@ -0,0 +1,59 @@
#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.Immutable;
using OpenRA.GameRules;
using OpenRA.Mods.Common.Traits;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Warheads
{
[Desc("Creates resources in a circle.")]
public class CreateResourceWarhead : Warhead
{
[Desc("Size of the area. The resources are seeded within this area.", "Provide 2 values for a ring effect (outer/inner).")]
public readonly ImmutableArray<int> Size = [0, 0];
[Desc("Will this splatter resources and which?")]
[FieldLoader.Require]
public readonly string AddsResourceType = null;
// TODO: Allow maximum resource splatter to be defined. (Per tile, and in total).
public override void DoImpact(in Target target, WarheadArgs args)
{
if (target.Type == TargetType.Invalid)
return;
var firedBy = args.SourceActor;
var pos = target.CenterPosition;
var world = firedBy.World;
var dat = world.Map.DistanceAboveTerrain(pos);
if (dat > AirThreshold)
return;
var targetTile = world.Map.CellContaining(pos);
var minRange = (Size.Length > 1 && Size[1] > 0) ? Size[1] : 0;
var allCells = world.Map.FindTilesInAnnulus(targetTile, minRange, Size[0]);
var resourceLayer = world.WorldActor.Trait<IResourceLayer>();
var maxDensity = resourceLayer.GetMaxDensity(AddsResourceType);
foreach (var cell in allCells)
{
if (!resourceLayer.CanAddResource(AddsResourceType, cell))
continue;
var splash = (byte)world.SharedRandom.Next(1, maxDensity - resourceLayer.GetResource(cell).Density);
resourceLayer.AddResource(AddsResourceType, cell, splash);
}
}
}
}

View File

@@ -0,0 +1,94 @@
#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.Frozen;
using System.Linq;
using OpenRA.GameRules;
using OpenRA.Mods.Common.Traits;
using OpenRA.Primitives;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Warheads
{
public abstract class DamageWarhead : Warhead
{
[Desc("How much (raw) damage to deal.")]
public readonly int Damage = 0;
[Desc("Types of damage that this warhead causes. Leave empty for no damage types.")]
public readonly BitSet<DamageType> DamageTypes = default;
[Desc("Damage percentage versus each armor type.")]
public readonly FrozenDictionary<string, int> Versus = FrozenDictionary<string, int>.Empty;
public override bool IsValidAgainst(Actor victim, Actor firedBy)
{
// Cannot be damaged without a Health trait
if (!victim.Info.HasTraitInfo<IHealthInfo>())
return false;
return base.IsValidAgainst(victim, firedBy);
}
public override void DoImpact(in Target target, WarheadArgs args)
{
var firedBy = args.SourceActor;
// Used by traits or warheads that damage a single actor, rather than a position
if (target.Type == TargetType.Actor)
{
var victim = target.Actor;
if (!IsValidAgainst(victim, firedBy))
return;
// PERF: Avoid using TraitsImplementing<HitShape> that needs to find the actor in the trait dictionary.
var closestActiveShape = (HitShape)victim.EnabledTargetablePositions.MinByOrDefault(t =>
{
if (t is HitShape h)
return h.DistanceFromEdge(victim, victim.CenterPosition);
else
return WDist.MaxValue;
});
// Cannot be damaged without an active HitShape
if (closestActiveShape == null)
return;
InflictDamage(victim, firedBy, closestActiveShape, args);
}
else if (target.Type != TargetType.Invalid)
DoImpact(target.CenterPosition, firedBy, args);
}
protected virtual int DamageVersus(Actor victim, HitShape shape, WarheadArgs args)
{
// If no Versus values are defined, DamageVersus would return 100 anyway, so we might as well do that early.
if (Versus.Count == 0)
return 100;
var armor = victim.TraitsImplementing<Armor>()
.Where(a => !a.IsTraitDisabled && a.Info.Type != null && Versus.ContainsKey(a.Info.Type) &&
(shape.Info.ArmorTypes.IsEmpty || shape.Info.ArmorTypes.Contains(a.Info.Type)))
.Select(a => Versus[a.Info.Type]);
return Util.ApplyPercentageModifiers(100, armor);
}
protected virtual void InflictDamage(Actor victim, Actor firedBy, HitShape shape, WarheadArgs args)
{
var damage = Util.ApplyPercentageModifiers(Damage, args.DamageModifiers.Append(DamageVersus(victim, shape, args)));
victim.InflictDamage(firedBy, new Damage(damage, DamageTypes));
}
protected abstract void DoImpact(WPos pos, Actor firedBy, WarheadArgs args);
}
}

View File

@@ -0,0 +1,66 @@
#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.Frozen;
using System.Collections.Immutable;
using OpenRA.GameRules;
using OpenRA.Mods.Common.Traits;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Warheads
{
[Desc("Destroys resources in a circle.")]
public class DestroyResourceWarhead : Warhead
{
[Desc("Size of the area. The resources are removed within this area.", "Provide 2 values for a ring effect (outer/inner).")]
public readonly ImmutableArray<int> Size = [0, 0];
[Desc("Amount of resources to be removed. If zero, all resources within the area will be removed.")]
public readonly byte ResourceAmount = 0;
[Desc("Resource types to remove with this warhead.", "If empty, all resource types will be removed.")]
public readonly FrozenSet<string> ResourceTypes = FrozenSet<string>.Empty;
public override void DoImpact(in Target target, WarheadArgs args)
{
if (target.Type == TargetType.Invalid)
return;
var firedBy = args.SourceActor;
var pos = target.CenterPosition;
var world = firedBy.World;
var dat = world.Map.DistanceAboveTerrain(pos);
if (dat > AirThreshold)
return;
var targetTile = world.Map.CellContaining(pos);
var resourceLayer = world.WorldActor.Trait<IResourceLayer>();
var minRange = (Size.Length > 1 && Size[1] > 0) ? Size[1] : 0;
var allCells = world.Map.FindTilesInAnnulus(targetTile, minRange, Size[0]);
var removeAllTypes = ResourceTypes.Count == 0;
foreach (var cell in allCells)
{
var cellContents = resourceLayer.GetResource(cell);
if (removeAllTypes || ResourceTypes.Contains(cellContents.Type))
{
if (ResourceAmount <= 0)
resourceLayer.ClearResources(cell);
else
resourceLayer.RemoveResource(cellContents.Type, cell, ResourceAmount);
}
}
}
}
}

View File

@@ -0,0 +1,118 @@
#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.GameRules;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Warheads
{
[Desc("Fires weapons from the point of impact.")]
public class FireClusterWarhead : Warhead, IRulesetLoaded<WeaponInfo>
{
[WeaponReference]
[FieldLoader.Require]
[Desc("Has to be defined in weapons.yaml as well.")]
public readonly string Weapon = null;
[Desc("Number of weapons fired at random 'x' cells. Negative values will result in a number equal to 'x' footprint cells fired.")]
public readonly int RandomClusterCount = -1;
[FieldLoader.Require]
[Desc("Size of the cluster footprint")]
public readonly CVec Dimensions = CVec.Zero;
[FieldLoader.Require]
[Desc("Cluster footprint. Cells marked as X will be attacked.",
"Cells marked as x will be attacked randomly until RandomClusterCount is reached.")]
public readonly string Footprint = string.Empty;
WeaponInfo weapon;
public void RulesetLoaded(Ruleset rules, WeaponInfo info)
{
if (!rules.Weapons.TryGetValue(Weapon.ToLowerInvariant(), out weapon))
throw new YamlException($"Weapons Ruleset does not contain an entry '{Weapon.ToLowerInvariant()}'");
}
public override void DoImpact(in Target target, WarheadArgs args)
{
if (target.Type == TargetType.Invalid)
return;
var firedBy = args.SourceActor;
var map = firedBy.World.Map;
var targetCell = map.CellContaining(target.CenterPosition);
var targetCells = CellsMatching(targetCell, false);
foreach (var c in targetCells)
FireProjectileAtCell(map, firedBy, target, c, args);
if (RandomClusterCount != 0)
{
var randomTargetCells = CellsMatching(targetCell, true).ToList();
var clusterCount = RandomClusterCount < 0 ? randomTargetCells.Count : RandomClusterCount;
if (randomTargetCells.Count != 0)
for (var i = 0; i < clusterCount; i++)
FireProjectileAtCell(map, firedBy, target, randomTargetCells.Random(firedBy.World.SharedRandom), args);
}
}
void FireProjectileAtCell(Map map, Actor firedBy, Target target, CPos targetCell, WarheadArgs args)
{
var tc = Target.FromCell(firedBy.World, targetCell);
if (!weapon.IsValidAgainst(tc, firedBy.World, firedBy))
return;
var projectileArgs = new ProjectileArgs
{
Weapon = weapon,
Facing = (map.CenterOfCell(targetCell) - target.CenterPosition).Yaw,
CurrentMuzzleFacing = () => (map.CenterOfCell(targetCell) - target.CenterPosition).Yaw,
DamageModifiers = args.DamageModifiers,
InaccuracyModifiers = [],
RangeModifiers = [],
Source = target.CenterPosition,
CurrentSource = () => target.CenterPosition,
SourceActor = firedBy,
PassiveTarget = map.CenterOfCell(targetCell),
GuidedTarget = tc
};
if (projectileArgs.Weapon.Projectile != null)
{
var projectile = projectileArgs.Weapon.Projectile.Create(projectileArgs);
if (projectile != null)
firedBy.World.AddFrameEndTask(w => w.Add(projectile));
if (projectileArgs.Weapon.Report != null && projectileArgs.Weapon.Report.Length > 0)
Game.Sound.Play(SoundType.World, projectileArgs.Weapon.Report, firedBy.World, target.CenterPosition);
}
}
IEnumerable<CPos> CellsMatching(CPos location, bool random)
{
var cellType = !random ? 'X' : 'x';
var index = 0;
var footprint = Footprint.Where(c => !char.IsWhiteSpace(c)).ToArray();
var x = location.X - (Dimensions.X - 1) / 2;
var y = location.Y - (Dimensions.Y - 1) / 2;
for (var j = 0; j < Dimensions.Y; j++)
for (var i = 0; i < Dimensions.X; i++)
if (footprint[index++] == cellType)
yield return new CPos(x + i, y + j);
}
}
}

View File

@@ -0,0 +1,35 @@
#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.GameRules;
using OpenRA.Mods.Common.Traits;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Warheads
{
[Desc("Used to trigger a FlashPostProcessEffect trait on the world actor.")]
public class FlashEffectWarhead : Warhead
{
[Desc("Corresponds to `Type` from `FlashPostProcessEffect` on the world actor.")]
public readonly string FlashType = null;
[FieldLoader.Require]
[Desc("Duration of the flashing, measured in ticks. Set to -1 to default to the `Length` of the `FlashPostProcessEffect`.")]
public readonly int Duration = 0;
public override void DoImpact(in Target target, WarheadArgs args)
{
foreach (var flash in args.SourceActor.World.WorldActor.TraitsImplementing<FlashPostProcessEffect>())
if (flash.Info.Type == FlashType)
flash.Enable(Duration);
}
}
}

View File

@@ -0,0 +1,60 @@
#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.GameRules;
using OpenRA.Mods.Common.Effects;
using OpenRA.Primitives;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Warheads
{
[Desc("Trigger a flash effect on the targeted actor, or actors within a circle.")]
public class FlashTargetsInRadiusWarhead : Warhead
{
[Desc("The overlay color to display when ActorFlashType is Overlay.")]
public readonly Color ActorFlashOverlayColor = Color.White;
[Desc("The overlay transparency to display when ActorFlashType is Overlay.")]
public readonly float ActorFlashOverlayAlpha = 0.5f;
[Desc("The tint to apply when ActorFlashType is Tint.")]
public readonly float3 ActorFlashTint = new(1.4f, 1.4f, 1.4f);
[Desc("Number of times to flash actors.")]
public readonly int ActorFlashCount = 2;
[Desc("Number of ticks between actor flashes.")]
public readonly int ActorFlashInterval = 2;
[Desc("Radius of an area at which effect will be applied. If left default effect applies only to target actor.")]
public readonly WDist Radius = new(0);
[Desc("Controls the way damage is calculated. Possible values are 'HitShape', 'ClosestTargetablePosition' and 'CenterPosition'.")]
public readonly DamageCalculationType DamageCalculationType = DamageCalculationType.HitShape;
public override void DoImpact(in Target target, WarheadArgs args)
{
var targetActor = target.Actor;
var firedBy = args.SourceActor;
var victims = Radius == WDist.Zero && targetActor != null ? [targetActor] : firedBy.World.FindActorsInCircle(target.CenterPosition, Radius);
foreach (var victim in victims)
{
if (!IsValidAgainst(victim, firedBy))
continue;
victim.World.AddFrameEndTask(w => w.Add(new FlashTarget(
victim, ActorFlashOverlayColor, ActorFlashOverlayAlpha,
ActorFlashCount, ActorFlashInterval)));
}
}
}
}

View File

@@ -0,0 +1,52 @@
#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.Linq;
using OpenRA.GameRules;
using OpenRA.Mods.Common.Traits;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Warheads
{
[Desc("Grant an external condition to hit actors.")]
public class GrantExternalConditionWarhead : Warhead
{
[FieldLoader.Require]
[Desc("The condition to apply. Must be included in the target actor's ExternalConditions list.")]
public readonly string Condition = null;
[Desc("Duration of the condition (in ticks). Set to 0 for a permanent condition.")]
public readonly int Duration = 0;
public readonly WDist Range = WDist.FromCells(1);
public override void DoImpact(in Target target, WarheadArgs args)
{
var firedBy = args.SourceActor;
if (target.Type == TargetType.Invalid)
return;
var actors = target.Type == TargetType.Actor ? [target.Actor] :
firedBy.World.FindActorsInCircle(target.CenterPosition, Range);
foreach (var a in actors)
{
if (!IsValidAgainst(a, firedBy))
continue;
a.TraitsImplementing<ExternalCondition>()
.FirstOrDefault(t => t.Info.Condition == Condition && t.CanGrantCondition(firedBy))
?.GrantCondition(a, firedBy, Duration);
}
}
}
}

View File

@@ -0,0 +1,28 @@
#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.GameRules;
using OpenRA.Mods.Common.Traits;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Warheads
{
[Desc("Apply damage based on the target's health.")]
public class HealthPercentageDamageWarhead : TargetDamageWarhead
{
protected override void InflictDamage(Actor victim, Actor firedBy, HitShape shape, WarheadArgs args)
{
var healthInfo = victim.Info.TraitInfo<HealthInfo>();
var damage = Util.ApplyPercentageModifiers(healthInfo.HP, args.DamageModifiers.Append(Damage, DamageVersus(victim, shape, args)));
victim.InflictDamage(firedBy, new Damage(damage, DamageTypes));
}
}
}

View File

@@ -0,0 +1,75 @@
#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.Frozen;
using System.Collections.Immutable;
using System.Linq;
using OpenRA.GameRules;
using OpenRA.Mods.Common.Traits;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Warheads
{
[Desc("Creates a smudge in `SmudgeLayer`.")]
public class LeaveSmudgeWarhead : Warhead
{
[Desc("Size of the area. A smudge will be created in each tile.", "Provide 2 values for a ring effect (outer/inner).")]
public readonly ImmutableArray<int> Size = [0, 0];
[Desc("Type of smudge to apply to terrain.")]
public readonly FrozenSet<string> SmudgeType = FrozenSet<string>.Empty;
[Desc("Percentage chance the smudge is created.")]
public readonly int Chance = 100;
public override void DoImpact(in Target target, WarheadArgs args)
{
if (target.Type == TargetType.Invalid)
return;
var firedBy = args.SourceActor;
var world = firedBy.World;
if (Chance < world.LocalRandom.Next(100))
return;
var pos = target.CenterPosition;
var dat = world.Map.DistanceAboveTerrain(pos);
if (dat > AirThreshold)
return;
var targetTile = world.Map.CellContaining(pos);
var smudgeLayers = world.WorldActor.TraitsImplementing<SmudgeLayer>().ToDictionary(x => x.Info.Type);
var minRange = (Size.Length > 1 && Size[1] > 0) ? Size[1] : 0;
var allCells = world.Map.FindTilesInAnnulus(targetTile, minRange, Size[0]);
// Draw the smudges:
foreach (var sc in allCells)
{
var smudgeType = world.Map.GetTerrainInfo(sc).AcceptsSmudgeType.FirstOrDefault(SmudgeType.Contains);
if (smudgeType == null)
continue;
var cellActors = world.ActorMap.GetActorsAt(sc);
if (cellActors.Any(a => !IsValidAgainst(a, firedBy)))
continue;
if (!smudgeLayers.TryGetValue(smudgeType, out var smudgeLayer))
throw new NotImplementedException($"Unknown smudge type `{smudgeType}`");
smudgeLayer.AddSmudge(sc);
}
}
}
}

View File

@@ -0,0 +1,34 @@
#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.GameRules;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Warheads
{
[Desc("Makes the screen shake.")]
public class ShakeScreenWarhead : Warhead
{
[Desc("Duration of the shaking.")]
public readonly int Duration = 0;
[Desc("Shake intensity.")]
public readonly int Intensity = 0;
[Desc("Shake multipliers by the X and Y axis, comma-separated.")]
public readonly float2 Multiplier = new(0, 0);
public override void DoImpact(in Target target, WarheadArgs args)
{
args.SourceActor.World.WorldActor.Trait<ScreenShaker>().AddEffect(Duration, target.CenterPosition, Intensity, Multiplier);
}
}
}

View File

@@ -0,0 +1,143 @@
#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.Immutable;
using System.Linq;
using OpenRA.GameRules;
using OpenRA.Mods.Common.Traits;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Warheads
{
public enum DamageCalculationType { HitShape, ClosestTargetablePosition, CenterPosition }
[Desc("Apply damage in a specified range.")]
public class SpreadDamageWarhead : DamageWarhead, IRulesetLoaded<WeaponInfo>
{
[Desc("Range between falloff steps.")]
public readonly WDist Spread = new(43);
[Desc("Damage percentage at each range step")]
public readonly ImmutableArray<int> Falloff = [100, 37, 14, 5, 0];
[Desc("Ranges at which each Falloff step is defined. Overrides Spread.")]
public readonly ImmutableArray<WDist> Range = default;
[Desc("Controls the way damage is calculated. Possible values are 'HitShape', 'ClosestTargetablePosition' and 'CenterPosition'.")]
public readonly DamageCalculationType DamageCalculationType = DamageCalculationType.HitShape;
ImmutableArray<WDist> effectiveRange;
void IRulesetLoaded<WeaponInfo>.RulesetLoaded(Ruleset rules, WeaponInfo info)
{
if (Range != null)
{
if (Range.Length != 1 && Range.Length != Falloff.Length)
throw new YamlException("Number of range values must be 1 or equal to the number of Falloff values.");
for (var i = 0; i < Range.Length - 1; i++)
if (Range[i] > Range[i + 1])
throw new YamlException("Range values must be specified in an increasing order.");
effectiveRange = Range;
}
else
effectiveRange = Exts.MakeArray(Falloff.Length, i => i * Spread).ToImmutableArray();
}
protected override void DoImpact(WPos pos, Actor firedBy, WarheadArgs args)
{
var debugVis = firedBy.World.WorldActor.TraitOrDefault<DebugVisualizations>();
if (debugVis != null && debugVis.CombatGeometry)
firedBy.World.WorldActor.Trait<WarheadDebugOverlay>().AddImpact(pos, effectiveRange, DebugOverlayColor);
foreach (var victim in firedBy.World.FindActorsOnCircle(pos, effectiveRange[^1]))
{
if (!IsValidAgainst(victim, firedBy))
continue;
HitShape closestActiveShape = null;
var closestDistance = int.MaxValue;
// PERF: Avoid using TraitsImplementing<HitShape> that needs to find the actor in the trait dictionary.
foreach (var targetPos in victim.EnabledTargetablePositions)
{
if (targetPos is HitShape h)
{
var distance = h.DistanceFromEdge(victim, pos).Length;
if (distance < closestDistance)
{
closestDistance = distance;
closestActiveShape = h;
}
}
}
// Cannot be damaged without an active HitShape.
if (closestActiveShape == null)
continue;
var falloffDistance = 0;
switch (DamageCalculationType)
{
case DamageCalculationType.HitShape:
falloffDistance = closestDistance;
break;
case DamageCalculationType.ClosestTargetablePosition:
falloffDistance = victim.GetTargetablePositions().Min(x => (x - pos).Length);
break;
case DamageCalculationType.CenterPosition:
falloffDistance = (victim.CenterPosition - pos).Length;
break;
}
// The range to target is more than the range the warhead covers, so GetDamageFalloff() is going to give us 0 and we're going to do 0 damage anyway, so bail early.
if (falloffDistance > effectiveRange[^1].Length)
continue;
var localModifiers = args.DamageModifiers.Append(GetDamageFalloff(falloffDistance));
var impactOrientation = args.ImpactOrientation;
// If a warhead lands outside the victim's HitShape, we need to calculate the vertical and horizontal impact angles
// from impact position, rather than last projectile facing/angle.
if (falloffDistance > 0)
{
var towardsTargetYaw = (victim.CenterPosition - args.ImpactPosition).Yaw;
var impactAngle = Util.GetVerticalAngle(args.ImpactPosition, victim.CenterPosition);
impactOrientation = new WRot(WAngle.Zero, impactAngle, towardsTargetYaw);
}
var updatedWarheadArgs = new WarheadArgs(args)
{
DamageModifiers = localModifiers.ToArray(),
ImpactOrientation = impactOrientation,
};
InflictDamage(victim, firedBy, closestActiveShape, updatedWarheadArgs);
}
}
int GetDamageFalloff(int distance)
{
var inner = effectiveRange[0].Length;
for (var i = 1; i < effectiveRange.Length; i++)
{
var outer = effectiveRange[i].Length;
if (outer > distance)
return int2.Lerp(Falloff[i - 1], Falloff[i], distance - inner, outer - inner);
inner = outer;
}
return 0;
}
}
}

View File

@@ -0,0 +1,67 @@
#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.GameRules;
using OpenRA.Mods.Common.Traits;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Warheads
{
[Desc("Apply damage to the targeted actor.")]
public class TargetDamageWarhead : DamageWarhead
{
[Desc("Damage will be applied to actors in this area. A value of zero means only targeted actor will be damaged.")]
public readonly WDist Spread = WDist.Zero;
protected override void DoImpact(WPos pos, Actor firedBy, WarheadArgs args)
{
if (Spread == WDist.Zero)
return;
var debugVis = firedBy.World.WorldActor.TraitOrDefault<DebugVisualizations>();
if (debugVis != null && debugVis.CombatGeometry)
firedBy.World.WorldActor.Trait<WarheadDebugOverlay>().AddImpact(pos, [WDist.Zero, Spread], DebugOverlayColor);
foreach (var victim in firedBy.World.FindActorsOnCircle(pos, Spread))
{
if (!IsValidAgainst(victim, firedBy))
continue;
HitShape closestActiveShape = null;
var closestDistance = int.MaxValue;
// PERF: Avoid using TraitsImplementing<HitShape> that needs to find the actor in the trait dictionary.
foreach (var targetPos in victim.EnabledTargetablePositions)
{
if (targetPos is HitShape hitshape)
{
var distance = hitshape.DistanceFromEdge(victim, pos).Length;
if (distance < closestDistance)
{
closestDistance = distance;
closestActiveShape = hitshape;
}
}
}
// Cannot be damaged without an active HitShape.
if (closestActiveShape == null)
continue;
// Cannot be damaged if HitShape is outside Spread.
if (closestDistance > Spread.Length)
continue;
InflictDamage(victim, firedBy, closestActiveShape, args);
}
}
}
}

View File

@@ -0,0 +1,98 @@
#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.GameRules;
using OpenRA.Mods.Common.Traits;
using OpenRA.Primitives;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Warheads
{
public enum ImpactActorType
{
None,
Invalid,
Valid,
}
[Desc("Base warhead class. This can be used to derive other warheads from.")]
public abstract class Warhead : IWarhead
{
[Desc("What types of targets are affected.")]
public readonly BitSet<TargetableType> ValidTargets = new("Ground", "Water");
[Desc("What types of targets are unaffected.", "Overrules ValidTargets.")]
public readonly BitSet<TargetableType> InvalidTargets;
[Desc("What player relationships are affected.")]
public readonly PlayerRelationship ValidRelationships = PlayerRelationship.Ally | PlayerRelationship.Neutral | PlayerRelationship.Enemy;
[Desc("Can this warhead affect the actor that fired it.")]
public readonly bool AffectsParent = false;
[Desc(
"If impact is above this altitude, " +
"warheads that would affect terrain ignore terrain target types " +
"(and either do nothing or perform their own checks).")]
public readonly WDist AirThreshold = new(128);
[Desc("Delay in ticks before applying the warhead effect.", "0 = instant (old model).")]
public readonly int Delay = 0;
int IWarhead.Delay => Delay;
[Desc("The color used for this warhead's visualization in the world's `" + nameof(WarheadDebugOverlay) + "` trait.")]
public readonly Color DebugOverlayColor = Color.Red;
protected bool IsValidTarget(BitSet<TargetableType> targetTypes)
{
return ValidTargets.Overlaps(targetTypes) && !InvalidTargets.Overlaps(targetTypes);
}
/// <summary>Applies the warhead's effect against the target.</summary>
public abstract void DoImpact(in Target target, WarheadArgs args);
/// <summary>Checks if the warhead is valid against (can do something to) the actor.</summary>
public virtual bool IsValidAgainst(Actor victim, Actor firedBy)
{
if (!AffectsParent && victim == firedBy)
return false;
var relationship = firedBy.Owner.RelationshipWith(victim.Owner);
if (!ValidRelationships.HasRelationship(relationship))
return false;
// A target type is valid if it is in the valid targets list, and not in the invalid targets list.
if (!IsValidTarget(victim.GetEnabledTargetTypes()))
return false;
return true;
}
/// <summary>Checks if the warhead is valid against (can do something to) the frozen actor.</summary>
public bool IsValidAgainst(FrozenActor victim, Actor firedBy)
{
if (!victim.IsValid)
return false;
// AffectsParent checks do not make sense for FrozenActors, so skip to relationship checks
var relationship = firedBy.Owner.RelationshipWith(victim.Owner);
if (!ValidRelationships.HasRelationship(relationship))
return false;
// A target type is valid if it is in the valid targets list, and not in the invalid targets list.
if (!IsValidTarget(victim.TargetTypes))
return false;
return true;
}
}
}