Revamped Meteor Swarm (#28974)
* meteor code and balanced values * Meteor Swarms * Update meteors.yml * Update meteors.yml * HOO! (fix overkill bug and buff space dust) * undo BloodstreamComponent.cs changes * DamageDistribution -> DamageTypes * part 2.
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Content.Server.Administration.Logs;
|
||||
using Content.Server.Atmos.EntitySystems;
|
||||
using Content.Server.Body.Systems;
|
||||
@@ -90,6 +91,16 @@ namespace Content.Server.Destructible
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryGetDestroyedAt(Entity<DestructibleComponent?> ent, [NotNullWhen(true)] out FixedPoint2? destroyedAt)
|
||||
{
|
||||
destroyedAt = null;
|
||||
if (!Resolve(ent, ref ent.Comp, false))
|
||||
return false;
|
||||
|
||||
destroyedAt = DestroyedAt(ent, ent.Comp);
|
||||
return true;
|
||||
}
|
||||
|
||||
// FFS this shouldn't be this hard. Maybe this should just be a field of the destructible component. Its not
|
||||
// like there is currently any entity that is NOT just destroyed upon reaching a total-damage value.
|
||||
/// <summary>
|
||||
|
||||
25
Content.Server/Mining/MeteorComponent.cs
Normal file
25
Content.Server/Mining/MeteorComponent.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using Content.Shared.Damage;
|
||||
|
||||
namespace Content.Server.Mining;
|
||||
|
||||
/// <summary>
|
||||
/// This is used for meteors which hit objects, dealing damage to destroy/kill the object and dealing equal damage back to itself.
|
||||
/// </summary>
|
||||
[RegisterComponent, Access(typeof(MeteorSystem))]
|
||||
public sealed partial class MeteorComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Damage specifier that is multiplied against the calculated damage amount to determine what damage is applied to the colliding entity.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The values of this should add up to 1 or else the damage will be scaled.
|
||||
/// </remarks>
|
||||
[DataField]
|
||||
public DamageSpecifier DamageTypes = new();
|
||||
|
||||
/// <summary>
|
||||
/// A list of entities that this meteor has collided with. used to ensure no double collisions occur.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public HashSet<EntityUid> HitList = new();
|
||||
}
|
||||
65
Content.Server/Mining/MeteorSystem.cs
Normal file
65
Content.Server/Mining/MeteorSystem.cs
Normal file
@@ -0,0 +1,65 @@
|
||||
using Content.Server.Administration.Logs;
|
||||
using Content.Server.Destructible;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
using Robust.Shared.Physics.Events;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Content.Server.Mining;
|
||||
|
||||
public sealed class MeteorSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IAdminLogManager _adminLog = default!;
|
||||
[Dependency] private readonly DamageableSystem _damageable = default!;
|
||||
[Dependency] private readonly DestructibleSystem _destructible = default!;
|
||||
[Dependency] private readonly MobThresholdSystem _mobThreshold = default!;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Initialize()
|
||||
{
|
||||
SubscribeLocalEvent<MeteorComponent, StartCollideEvent>(OnCollide);
|
||||
}
|
||||
|
||||
private void OnCollide(EntityUid uid, MeteorComponent component, ref StartCollideEvent args)
|
||||
{
|
||||
if (TerminatingOrDeleted(args.OtherEntity) || TerminatingOrDeleted(uid))
|
||||
return;
|
||||
|
||||
if (component.HitList.Contains(args.OtherEntity))
|
||||
return;
|
||||
|
||||
FixedPoint2 threshold;
|
||||
if (_mobThreshold.TryGetDeadThreshold(args.OtherEntity, out var mobThreshold))
|
||||
{
|
||||
threshold = mobThreshold.Value;
|
||||
if (HasComp<ActorComponent>(args.OtherEntity))
|
||||
_adminLog.Add(LogType.Action, LogImpact.Extreme, $"{ToPrettyString(args.OtherEntity):player} was struck by meteor {ToPrettyString(uid):ent} and killed instantly.");
|
||||
}
|
||||
else if (_destructible.TryGetDestroyedAt(args.OtherEntity, out var destroyThreshold))
|
||||
{
|
||||
threshold = destroyThreshold.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
threshold = FixedPoint2.MaxValue;
|
||||
}
|
||||
var otherEntDamage = CompOrNull<DamageableComponent>(args.OtherEntity)?.TotalDamage ?? FixedPoint2.Zero;
|
||||
// account for the damage that the other entity has already taken: don't overkill
|
||||
threshold -= otherEntDamage;
|
||||
|
||||
// The max amount of damage our meteor can take before breaking.
|
||||
var maxMeteorDamage = _destructible.DestroyedAt(uid) - CompOrNull<DamageableComponent>(uid)?.TotalDamage ?? FixedPoint2.Zero;
|
||||
|
||||
// Cap damage so we don't overkill the meteor
|
||||
var trueDamage = FixedPoint2.Min(maxMeteorDamage, threshold);
|
||||
|
||||
var damage = component.DamageTypes * trueDamage;
|
||||
_damageable.TryChangeDamage(args.OtherEntity, damage, true, origin: uid);
|
||||
_damageable.TryChangeDamage(uid, damage);
|
||||
|
||||
if (!TerminatingOrDeleted(args.OtherEntity))
|
||||
component.HitList.Add(args.OtherEntity);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using Content.Shared.Random;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.StationEvents.Components;
|
||||
|
||||
/// <summary>
|
||||
/// This is used for running meteor swarm events at regular intervals.
|
||||
/// </summary>
|
||||
[RegisterComponent, Access(typeof(MeteorSchedulerSystem)), AutoGenerateComponentPause]
|
||||
public sealed partial class MeteorSchedulerComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The weights for which swarms will be selected.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public ProtoId<WeightedRandomEntityPrototype> Config = "DefaultConfig";
|
||||
|
||||
/// <summary>
|
||||
/// The time at which the next swarm occurs.
|
||||
/// </summary>
|
||||
[DataField, AutoPausedField]
|
||||
public TimeSpan NextSwarmTime = TimeSpan.Zero;
|
||||
|
||||
/// <summary>
|
||||
/// The minimum time between swarms
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public TimeSpan MinSwarmDelay = TimeSpan.FromMinutes(7.5f);
|
||||
|
||||
/// <summary>
|
||||
/// The maximum time between swarms
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public TimeSpan MaxSwarmDelay = TimeSpan.FromMinutes(12.5f);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using Content.Server.StationEvents.Events;
|
||||
using Content.Shared.Destructible.Thresholds;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.StationEvents.Components;
|
||||
|
||||
[RegisterComponent, Access(typeof(MeteorSwarmSystem)), AutoGenerateComponentPause]
|
||||
public sealed partial class MeteorSwarmComponent : Component
|
||||
{
|
||||
[DataField, AutoPausedField]
|
||||
public TimeSpan NextWaveTime;
|
||||
|
||||
/// <summary>
|
||||
/// We'll send a specific amount of waves of meteors towards the station per ending rather than using a timer.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public int WaveCounter;
|
||||
|
||||
[DataField]
|
||||
public float MeteorVelocity = 10f;
|
||||
|
||||
/// <summary>
|
||||
/// If true, meteors will be thrown from all angles instead of from a singular source
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool NonDirectional;
|
||||
|
||||
/// <summary>
|
||||
/// The announcement played when a meteor swarm begins.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public LocId? Announcement = "station-event-meteor-swarm-start-announcement";
|
||||
|
||||
[DataField]
|
||||
public SoundSpecifier? AnnouncementSound = new SoundPathSpecifier("/Audio/Announcements/meteors.ogg")
|
||||
{
|
||||
Params = new()
|
||||
{
|
||||
Volume = -4
|
||||
}
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Each meteor entity prototype and their corresponding weight in being picked.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public Dictionary<EntProtoId, float> Meteors = new();
|
||||
|
||||
[DataField]
|
||||
public MinMax Waves = new(3, 3);
|
||||
|
||||
[DataField]
|
||||
public MinMax MeteorsPerWave = new(3, 4);
|
||||
|
||||
[DataField]
|
||||
public MinMax WaveCooldown = new (10, 60);
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
using Content.Server.StationEvents.Events;
|
||||
|
||||
namespace Content.Server.StationEvents.Components;
|
||||
|
||||
[RegisterComponent, Access(typeof(MeteorSwarmRule))]
|
||||
public sealed partial class MeteorSwarmRuleComponent : Component
|
||||
{
|
||||
[DataField("cooldown")]
|
||||
public float Cooldown;
|
||||
|
||||
/// <summary>
|
||||
/// We'll send a specific amount of waves of meteors towards the station per ending rather than using a timer.
|
||||
/// </summary>
|
||||
[DataField("waveCounter")]
|
||||
public int WaveCounter;
|
||||
|
||||
[DataField("minimumWaves")]
|
||||
public int MinimumWaves = 3;
|
||||
|
||||
[DataField("maximumWaves")]
|
||||
public int MaximumWaves = 8;
|
||||
|
||||
[DataField("minimumCooldown")]
|
||||
public float MinimumCooldown = 10f;
|
||||
|
||||
[DataField("maximumCooldown")]
|
||||
public float MaximumCooldown = 60f;
|
||||
|
||||
[DataField("meteorsPerWave")]
|
||||
public int MeteorsPerWave = 5;
|
||||
|
||||
[DataField("meteorVelocity")]
|
||||
public float MeteorVelocity = 10f;
|
||||
|
||||
[DataField("maxAngularVelocity")]
|
||||
public float MaxAngularVelocity = 0.25f;
|
||||
|
||||
[DataField("minAngularVelocity")]
|
||||
public float MinAngularVelocity = -0.25f;
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
using System.Numerics;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Server.StationEvents.Components;
|
||||
using Content.Shared.GameTicking.Components;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Physics.Systems;
|
||||
using Robust.Shared.Spawners;
|
||||
|
||||
namespace Content.Server.StationEvents.Events
|
||||
{
|
||||
public sealed class MeteorSwarmRule : StationEventSystem<MeteorSwarmRuleComponent>
|
||||
{
|
||||
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
|
||||
|
||||
protected override void Started(EntityUid uid, MeteorSwarmRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
|
||||
{
|
||||
base.Started(uid, component, gameRule, args);
|
||||
|
||||
component.WaveCounter = RobustRandom.Next(component.MinimumWaves, component.MaximumWaves);
|
||||
}
|
||||
|
||||
protected override void ActiveTick(EntityUid uid, MeteorSwarmRuleComponent component, GameRuleComponent gameRule, float frameTime)
|
||||
{
|
||||
if (component.WaveCounter <= 0)
|
||||
{
|
||||
ForceEndSelf(uid, gameRule);
|
||||
return;
|
||||
}
|
||||
|
||||
component.Cooldown -= frameTime;
|
||||
|
||||
if (component.Cooldown > 0f)
|
||||
return;
|
||||
|
||||
component.WaveCounter--;
|
||||
|
||||
component.Cooldown += (component.MaximumCooldown - component.MinimumCooldown) * RobustRandom.NextFloat() + component.MinimumCooldown;
|
||||
|
||||
Box2? playableArea = null;
|
||||
var mapId = GameTicker.DefaultMap;
|
||||
|
||||
var query = AllEntityQuery<MapGridComponent, TransformComponent>();
|
||||
while (query.MoveNext(out var gridId, out _, out var xform))
|
||||
{
|
||||
if (xform.MapID != mapId)
|
||||
continue;
|
||||
|
||||
var aabb = _physics.GetWorldAABB(gridId);
|
||||
playableArea = playableArea?.Union(aabb) ?? aabb;
|
||||
}
|
||||
|
||||
if (playableArea == null)
|
||||
{
|
||||
ForceEndSelf(uid, gameRule);
|
||||
return;
|
||||
}
|
||||
|
||||
var minimumDistance = (playableArea.Value.TopRight - playableArea.Value.Center).Length() + 50f;
|
||||
var maximumDistance = minimumDistance + 100f;
|
||||
|
||||
var center = playableArea.Value.Center;
|
||||
|
||||
for (var i = 0; i < component.MeteorsPerWave; i++)
|
||||
{
|
||||
var angle = new Angle(RobustRandom.NextFloat() * MathF.Tau);
|
||||
var offset = angle.RotateVec(new Vector2((maximumDistance - minimumDistance) * RobustRandom.NextFloat() + minimumDistance, 0));
|
||||
var spawnPosition = new MapCoordinates(center + offset, mapId);
|
||||
var meteor = Spawn("MeteorLarge", spawnPosition);
|
||||
var physics = EntityManager.GetComponent<PhysicsComponent>(meteor);
|
||||
_physics.SetBodyStatus(meteor, physics, BodyStatus.InAir);
|
||||
_physics.SetLinearDamping(meteor, physics, 0f);
|
||||
_physics.SetAngularDamping(meteor, physics, 0f);
|
||||
_physics.ApplyLinearImpulse(meteor, -offset.Normalized() * component.MeteorVelocity * physics.Mass, body: physics);
|
||||
_physics.ApplyAngularImpulse(
|
||||
meteor,
|
||||
physics.Mass * ((component.MaxAngularVelocity - component.MinAngularVelocity) * RobustRandom.NextFloat() + component.MinAngularVelocity),
|
||||
body: physics);
|
||||
|
||||
EnsureComp<TimedDespawnComponent>(meteor).Lifetime = 120f;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
82
Content.Server/StationEvents/Events/MeteorSwarmSystem.cs
Normal file
82
Content.Server/StationEvents/Events/MeteorSwarmSystem.cs
Normal file
@@ -0,0 +1,82 @@
|
||||
using System.Numerics;
|
||||
using Content.Server.Chat.Systems;
|
||||
using Content.Server.GameTicking.Rules;
|
||||
using Content.Server.Station.Components;
|
||||
using Content.Server.Station.Systems;
|
||||
using Content.Server.StationEvents.Components;
|
||||
using Content.Shared.GameTicking.Components;
|
||||
using Content.Shared.Random.Helpers;
|
||||
using Robust.Server.Audio;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Physics.Systems;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Server.StationEvents.Events;
|
||||
|
||||
public sealed class MeteorSwarmSystem : GameRuleSystem<MeteorSwarmComponent>
|
||||
{
|
||||
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
|
||||
[Dependency] private readonly AudioSystem _audio = default!;
|
||||
[Dependency] private readonly ChatSystem _chat = default!;
|
||||
[Dependency] private readonly StationSystem _station = default!;
|
||||
|
||||
protected override void Added(EntityUid uid, MeteorSwarmComponent component, GameRuleComponent gameRule, GameRuleAddedEvent args)
|
||||
{
|
||||
base.Added(uid, component, gameRule, args);
|
||||
|
||||
component.WaveCounter = component.Waves.Next(RobustRandom);
|
||||
|
||||
if (component.Announcement is { } locId)
|
||||
_chat.DispatchGlobalAnnouncement(Loc.GetString(locId), playSound: false, colorOverride: Color.Yellow);
|
||||
_audio.PlayGlobal(component.AnnouncementSound, Filter.Broadcast(), true);
|
||||
}
|
||||
|
||||
protected override void ActiveTick(EntityUid uid, MeteorSwarmComponent component, GameRuleComponent gameRule, float frameTime)
|
||||
{
|
||||
if (Timing.CurTime < component.NextWaveTime)
|
||||
return;
|
||||
|
||||
component.NextWaveTime += TimeSpan.FromSeconds(component.WaveCooldown.Next(RobustRandom));
|
||||
|
||||
|
||||
if (_station.GetStations().Count == 0)
|
||||
return;
|
||||
|
||||
var station = RobustRandom.Pick(_station.GetStations());
|
||||
if (_station.GetLargestGrid(Comp<StationDataComponent>(station)) is not { } grid)
|
||||
return;
|
||||
|
||||
var mapId = Transform(grid).MapID;
|
||||
var playableArea = _physics.GetWorldAABB(grid);
|
||||
|
||||
var minimumDistance = (playableArea.TopRight - playableArea.Center).Length() + 50f;
|
||||
var maximumDistance = minimumDistance + 100f;
|
||||
|
||||
var center = playableArea.Center;
|
||||
|
||||
var meteorsToSpawn = component.MeteorsPerWave.Next(RobustRandom);
|
||||
for (var i = 0; i < meteorsToSpawn; i++)
|
||||
{
|
||||
var spawnProto = RobustRandom.Pick(component.Meteors);
|
||||
|
||||
var angle = component.NonDirectional
|
||||
? RobustRandom.NextAngle()
|
||||
: new Random(uid.Id).NextAngle();
|
||||
|
||||
var offset = angle.RotateVec(new Vector2((maximumDistance - minimumDistance) * RobustRandom.NextFloat() + minimumDistance, 0));
|
||||
var subOffset = RobustRandom.NextAngle().RotateVec(new Vector2( (playableArea.TopRight - playableArea.Center).Length() / 2 * RobustRandom.NextFloat(), 0));
|
||||
var spawnPosition = new MapCoordinates(center + offset + subOffset, mapId);
|
||||
var meteor = Spawn(spawnProto, spawnPosition);
|
||||
var physics = Comp<PhysicsComponent>(meteor);
|
||||
_physics.ApplyLinearImpulse(meteor, -offset.Normalized() * component.MeteorVelocity * physics.Mass, body: physics);
|
||||
}
|
||||
|
||||
component.WaveCounter--;
|
||||
if (component.WaveCounter <= 0)
|
||||
{
|
||||
ForceEndSelf(uid, gameRule);
|
||||
}
|
||||
}
|
||||
}
|
||||
39
Content.Server/StationEvents/MeteorSchedulerSystem.cs
Normal file
39
Content.Server/StationEvents/MeteorSchedulerSystem.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using Content.Server.GameTicking.Rules;
|
||||
using Content.Server.StationEvents.Components;
|
||||
using Content.Shared.GameTicking.Components;
|
||||
using Content.Shared.Random.Helpers;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.StationEvents;
|
||||
|
||||
/// <summary>
|
||||
/// This handles scheduling and launching meteors at a station at regular intervals.
|
||||
/// TODO: there is 100% a world in which this is genericized and can be used for lots of basic event scheduling
|
||||
/// </summary>
|
||||
public sealed class MeteorSchedulerSystem : GameRuleSystem<MeteorSchedulerComponent>
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
|
||||
protected override void Started(EntityUid uid, MeteorSchedulerComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
|
||||
{
|
||||
base.Started(uid, component, gameRule, args);
|
||||
|
||||
component.NextSwarmTime = Timing.CurTime + RobustRandom.Next(component.MinSwarmDelay, component.MaxSwarmDelay);
|
||||
}
|
||||
|
||||
protected override void ActiveTick(EntityUid uid, MeteorSchedulerComponent component, GameRuleComponent gameRule, float frameTime)
|
||||
{
|
||||
base.ActiveTick(uid, component, gameRule, frameTime);
|
||||
|
||||
if (Timing.CurTime < component.NextSwarmTime)
|
||||
return;
|
||||
RunSwarm((uid, component));
|
||||
component.NextSwarmTime += RobustRandom.Next(component.MinSwarmDelay, component.MaxSwarmDelay);
|
||||
}
|
||||
|
||||
private void RunSwarm(Entity<MeteorSchedulerComponent> ent)
|
||||
{
|
||||
var swarmWeights = _prototypeManager.Index(ent.Comp.Config);
|
||||
GameTicker.StartGameRule(swarmWeights.Pick(RobustRandom));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user