From 85b68f5f830590d6c269d9d8fe24d6192458cb3f Mon Sep 17 00:00:00 2001 From: "Paul C. Scharf" Date: Fri, 9 May 2025 23:31:36 +0100 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=A4=AD=20Further=20refactor=20of=20el?= =?UTF-8?q?ement=20system?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Minimizes allocations - Minimizes boilerplate - Simplifies lifecycle slightly --- .../Elements/ElementalPhenomenonScopeBase.cs | 97 ++++++++++--------- .../Simulation/Elements/ElementalStatus.cs | 2 +- .../Elements/ElementalSystemExtensions.cs | 2 +- .../Simulation/Elements/IElementalEffect.cs | 18 +++- .../Elements/IElementalPhenomenon.cs | 14 --- .../Elements/Phenomena/LowerWeaponRange.cs | 49 +++------- .../Elements/Phenomena/OnFire.Effect.cs | 13 --- .../Elements/Phenomena/OnFire.Phenomenon.cs | 16 --- .../Elements/Phenomena/OnFire.Scope.cs | 58 ----------- .../Simulation/Elements/Phenomena/OnFire.cs | 57 +++++++++++ .../Elements/Phenomena/Shocked.Effect.cs | 11 --- .../Elements/Phenomena/Shocked.Phenomenon.cs | 16 --- .../Elements/Phenomena/Shocked.Scope.cs | 75 -------------- .../Simulation/Elements/Phenomena/Shocked.cs | 75 ++++++++++++++ .../Elements/Phenomena/Stunned.Effect.cs | 11 --- .../Elements/Phenomena/Stunned.Phenomenon.cs | 16 --- .../Elements/Phenomena/Stunned.Scope.cs | 70 ------------- .../Simulation/Elements/Phenomena/Stunned.cs | 57 +++++++++++ .../components/ApplyAreaEffectOnImpact.cs | 2 +- .../components/ApplyEffectOnImpact.cs | 2 +- .../components/ElementSystemEntity.cs | 19 ++-- .../Simulation/Projectiles/AreaOfEffect.cs | 2 +- .../Utilities/Collections/LinqExtensions.cs | 12 ++- .../Utilities/Collections/ListExtensions.cs | 34 ++++++- .../Utilities/Collections/SpanExtensions.cs | 85 ++++++++++++++++ 25 files changed, 411 insertions(+), 402 deletions(-) delete mode 100644 src/Bearded.TD/Game/Simulation/Elements/Phenomena/OnFire.Effect.cs delete mode 100644 src/Bearded.TD/Game/Simulation/Elements/Phenomena/OnFire.Phenomenon.cs delete mode 100644 src/Bearded.TD/Game/Simulation/Elements/Phenomena/OnFire.Scope.cs create mode 100644 src/Bearded.TD/Game/Simulation/Elements/Phenomena/OnFire.cs delete mode 100644 src/Bearded.TD/Game/Simulation/Elements/Phenomena/Shocked.Effect.cs delete mode 100644 src/Bearded.TD/Game/Simulation/Elements/Phenomena/Shocked.Phenomenon.cs delete mode 100644 src/Bearded.TD/Game/Simulation/Elements/Phenomena/Shocked.Scope.cs create mode 100644 src/Bearded.TD/Game/Simulation/Elements/Phenomena/Shocked.cs delete mode 100644 src/Bearded.TD/Game/Simulation/Elements/Phenomena/Stunned.Effect.cs delete mode 100644 src/Bearded.TD/Game/Simulation/Elements/Phenomena/Stunned.Phenomenon.cs delete mode 100644 src/Bearded.TD/Game/Simulation/Elements/Phenomena/Stunned.Scope.cs create mode 100644 src/Bearded.TD/Game/Simulation/Elements/Phenomena/Stunned.cs create mode 100644 src/Bearded.TD/Utilities/Collections/SpanExtensions.cs diff --git a/src/Bearded.TD/Game/Simulation/Elements/ElementalPhenomenonScopeBase.cs b/src/Bearded.TD/Game/Simulation/Elements/ElementalPhenomenonScopeBase.cs index adc61d31d..2406b2046 100644 --- a/src/Bearded.TD/Game/Simulation/Elements/ElementalPhenomenonScopeBase.cs +++ b/src/Bearded.TD/Game/Simulation/Elements/ElementalPhenomenonScopeBase.cs @@ -1,23 +1,27 @@ +using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.InteropServices; using Bearded.TD.Game.Simulation.GameObjects; using Bearded.TD.Game.Simulation.StatusDisplays; +using Bearded.TD.Utilities.Collections; using Bearded.Utilities.SpaceTime; namespace Bearded.TD.Game.Simulation.Elements; -abstract class ElementalPhenomenonScopeBase : IElementalPhenomenon.IScope where TEffect : IElementalEffect +abstract class ElementalPhenomenonScopeBase + : ElementalPhenomenonScopeBase, IElementalEffect.IScope + where TEffect : struct, IElementalEffect, IEquatable { - private readonly GameObject target; private readonly IStatusTracker? statusDisplay; private readonly List activeEffects = []; private ActiveEffect? activeEffect; - protected IEnumerable ActiveEffects => activeEffects.Select(e => e.Effect); + protected GameObject Target { get; private set; } protected ElementalPhenomenonScopeBase(GameObject target) { - this.target = target; + Target = target; target.TryGetSingleComponent(out statusDisplay); } @@ -28,57 +32,48 @@ public void Adopt(TEffect effect, Instant now) public void ApplyTick(Instant now) { - activeEffects.RemoveAll(e => e.Expiry <= now); + activeEffects.RemoveAll(now, static (t, e) => e.Expiry <= t); - if (TryChooseEffect(out var effect)) + if (TryChooseEffect(CollectionsMarshal.AsSpan(activeEffects)) is { } effect) { transitionToEffect(effect); - ApplyEffectTick(target, effect); + ApplyEffectTick(effect); } else { endEffectIfPreviouslyActive(); } - if (activeEffect?.StatusIcon is not null) + if (activeEffect?.StatusIcon is { } icon) { - updateStatusIconExpiry(activeEffect.StatusIcon); + updateStatusIconExpiry(icon); } } private void transitionToEffect(TEffect effect) { // null -> effect A - if (activeEffect is null) + if (activeEffect is not { } current) { - StartScope(target, out var createStatus); - startEffect(effect, out var createOverrideStatus); - createStatus = createOverrideStatus ?? createStatus; - var receipt = createStatus is null ? null : reportStatus(createStatus); + EffectChangeResult? statusChange = null; + StartScope(effect, ref statusChange); + var receipt = statusChange?.NewStatus is { } status ? reportStatus(status) : null; activeEffect = new ActiveEffect(effect, receipt); return; } // effect A -> effect A - if (activeEffect.Effect.Equals(effect)) return; + if (current.Effect.Equals(effect)) + return; // effect A -> effect B - var statusIcon = activeEffect.StatusIcon; - EndEffect(target, effect); - startEffect(effect, out var createNewStatus); - if (createNewStatus is not null) { - statusIcon?.DeleteImmediately(); - statusIcon = reportStatus(createNewStatus); + EffectChangeResult? statusChange = null; + ChangeActiveEffect(current.Effect, effect, ref statusChange); + current.StatusIcon?.DeleteImmediately(); + var receipt = statusChange?.NewStatus is { } status ? reportStatus(status) : null; + activeEffect = new ActiveEffect(effect, receipt); } - activeEffect = new ActiveEffect(effect, statusIcon); - } - - private void startEffect(TEffect effect, out ElementalStatus? newStatus) - { - var ctx = new EffectStartContext(); - StartEffect(target, effect, ctx); - newStatus = ctx.NewStatus; } private IStatusReceipt? reportStatus(ElementalStatus status) @@ -92,36 +87,44 @@ private void startEffect(TEffect effect, out ElementalStatus? newStatus) private void endEffectIfPreviouslyActive() { - if (activeEffect is null) return; + if (activeEffect is not { } current) return; - EndScope(target); - activeEffect.StatusIcon?.DeleteImmediately(); + EndScope(current.Effect); + current.StatusIcon?.DeleteImmediately(); activeEffect = null; } private void updateStatusIconExpiry(IStatusReceipt statusIcon) { - var latestExpiry = activeEffects.Select(e => e.Expiry).Max(); + var latestExpiry = activeEffects.Max(static e => e.Expiry); statusIcon.SetExpiryTime(latestExpiry); } - protected abstract bool TryChooseEffect(out TEffect effect); - protected abstract void StartScope(GameObject target, out ElementalStatus? status); - protected abstract void StartEffect(GameObject target, TEffect effect, EffectStartContext context); - protected abstract void ApplyEffectTick(GameObject target, TEffect effect); - protected abstract void EndEffect(GameObject target, TEffect effect); - protected abstract void EndScope(GameObject target); + protected abstract TEffect? TryChooseEffect(ReadOnlySpan activeEffects); + protected abstract void StartScope(TEffect effect, ref EffectChangeResult? statusChange); + protected abstract void ChangeActiveEffect(TEffect previousEffect, TEffect newEffect, ref EffectChangeResult? statusChange); + protected abstract void ApplyEffectTick(TEffect effect); + protected abstract void EndScope(TEffect effect); + + protected readonly record struct EffectWithExpiry(TEffect Effect, Instant Expiry); + private readonly record struct ActiveEffect(TEffect Effect, IStatusReceipt? StatusIcon); - private readonly record struct EffectWithExpiry(TEffect Effect, Instant Expiry); - private sealed record ActiveEffect(TEffect Effect, IStatusReceipt? StatusIcon); +} - protected class EffectStartContext +// Base class for non-generic members +abstract class ElementalPhenomenonScopeBase +{ + public readonly struct EffectChangeResult { - public ElementalStatus? NewStatus { get; private set; } + public ElementalStatus? NewStatus { get; private init; } - public void ChangeStatus(ElementalStatus status) - { - NewStatus = status; - } + public static EffectChangeResult HideStatus => default; + + public static EffectChangeResult ShowStatus(ElementalStatus status) => new() { NewStatus = status }; + + public static implicit operator EffectChangeResult(ElementalStatus newStatus) => ShowStatus(newStatus); } + + protected static EffectChangeResult HideStatus => EffectChangeResult.HideStatus; + protected static EffectChangeResult ShowStatus(ElementalStatus status) => EffectChangeResult.ShowStatus(status); } diff --git a/src/Bearded.TD/Game/Simulation/Elements/ElementalStatus.cs b/src/Bearded.TD/Game/Simulation/Elements/ElementalStatus.cs index 6a41eaeb2..ce4c1a382 100644 --- a/src/Bearded.TD/Game/Simulation/Elements/ElementalStatus.cs +++ b/src/Bearded.TD/Game/Simulation/Elements/ElementalStatus.cs @@ -2,4 +2,4 @@ namespace Bearded.TD.Game.Simulation.Elements; -sealed record ElementalStatus(ModAwareSpriteId Sprite); +readonly record struct ElementalStatus(ModAwareSpriteId Sprite); diff --git a/src/Bearded.TD/Game/Simulation/Elements/ElementalSystemExtensions.cs b/src/Bearded.TD/Game/Simulation/Elements/ElementalSystemExtensions.cs index 959cc84a3..37ae268b7 100644 --- a/src/Bearded.TD/Game/Simulation/Elements/ElementalSystemExtensions.cs +++ b/src/Bearded.TD/Game/Simulation/Elements/ElementalSystemExtensions.cs @@ -4,7 +4,7 @@ namespace Bearded.TD.Game.Simulation.Elements; static class ElementalSystemExtensions { - public static bool TryApplyEffect(this GameObject obj, T effect) where T : IElementalEffect + public static bool TryApplyEffect(this GameObject obj, T effect) where T : IElementalEffect { if (!obj.TryGetSingleComponent(out var entity)) { diff --git a/src/Bearded.TD/Game/Simulation/Elements/IElementalEffect.cs b/src/Bearded.TD/Game/Simulation/Elements/IElementalEffect.cs index 4a3de83cb..267777bf8 100644 --- a/src/Bearded.TD/Game/Simulation/Elements/IElementalEffect.cs +++ b/src/Bearded.TD/Game/Simulation/Elements/IElementalEffect.cs @@ -1,9 +1,25 @@ +using Bearded.TD.Game.Simulation.GameObjects; using Bearded.Utilities.SpaceTime; namespace Bearded.TD.Game.Simulation.Elements; interface IElementalEffect { - IElementalPhenomenon Phenomenon { get; } TimeSpan Duration { get; } + + interface IScope + { + void ApplyTick(Instant now); + } +} + +interface IElementalEffect : IElementalEffect + where TEffect : IElementalEffect +{ + new interface IScope : IElementalEffect.IScope + { + void Adopt(TEffect effect, Instant now); + } + + IScope NewScope(GameObject target); } diff --git a/src/Bearded.TD/Game/Simulation/Elements/IElementalPhenomenon.cs b/src/Bearded.TD/Game/Simulation/Elements/IElementalPhenomenon.cs index ea8f0b311..706fe750f 100644 --- a/src/Bearded.TD/Game/Simulation/Elements/IElementalPhenomenon.cs +++ b/src/Bearded.TD/Game/Simulation/Elements/IElementalPhenomenon.cs @@ -1,21 +1,7 @@ -using System; -using Bearded.TD.Game.Simulation.GameObjects; using Bearded.Utilities.SpaceTime; namespace Bearded.TD.Game.Simulation.Elements; interface IElementalPhenomenon { - public Type EffectType { get; } - public IScope NewScope(GameObject target); - - interface IScope - { - void ApplyTick(Instant now); - } - - interface IScope : IScope where T : IElementalEffect - { - void Adopt(T effect, Instant now); - } } diff --git a/src/Bearded.TD/Game/Simulation/Elements/Phenomena/LowerWeaponRange.cs b/src/Bearded.TD/Game/Simulation/Elements/Phenomena/LowerWeaponRange.cs index 9cfa6017b..ab2661f91 100644 --- a/src/Bearded.TD/Game/Simulation/Elements/Phenomena/LowerWeaponRange.cs +++ b/src/Bearded.TD/Game/Simulation/Elements/Phenomena/LowerWeaponRange.cs @@ -1,70 +1,51 @@ using System; -using System.Collections.Immutable; -using System.Linq; using Bearded.TD.Game.Simulation.GameObjects; using Bearded.TD.Game.Simulation.StatusDisplays; using Bearded.TD.Game.Simulation.Upgrades; using Bearded.TD.Shared.TechEffects; +using Bearded.TD.Utilities.Collections; using TimeSpan = Bearded.Utilities.SpaceTime.TimeSpan; namespace Bearded.TD.Game.Simulation.Elements.Phenomena; static class LowerWeaponRange { - public readonly record struct Effect(double Factor, TimeSpan Duration) : IElementalEffect + public readonly record struct Effect(double Factor, TimeSpan Duration) : IElementalEffect { - IElementalPhenomenon IElementalEffect.Phenomenon => phenomenon; - } - - private static readonly IElementalPhenomenon phenomenon = new Phenomenon(); - - private sealed class Phenomenon : IElementalPhenomenon - { - public Type EffectType => typeof(Effect); - - public IElementalPhenomenon.IScope NewScope(GameObject target) => new Scope(target); + public IElementalEffect.IScope NewScope(GameObject target) => new Scope(target); } private sealed class Scope(GameObject target) : ElementalPhenomenonScopeBase(target) { private IUpgradeReceipt? receipt; - protected override bool TryChooseEffect(out Effect effect) + protected override Effect? TryChooseEffect(ReadOnlySpan activeEffects) { - var effects = ActiveEffects.ToImmutableArray(); - if (effects.IsEmpty) - { - effect = default; - return false; - } - - effect = effects.MinBy(e => e.Factor); - return true; + return activeEffects.MinByOrDefault(e => e.Effect.Factor)?.Effect; } - protected override void StartScope(GameObject target, out ElementalStatus? status) + protected override void StartScope(Effect effect, ref EffectChangeResult? statusChange) { - status = new ElementalStatus("eye-disabled".ToStatusIconSpriteId()); + statusChange = new ElementalStatus("eye-disabled".ToStatusIconSpriteId()); + + var upgrade = Upgrade.FromEffects(createUpgradeEffect(effect)); + if (!Target.CanApplyUpgrade(upgrade)) return; + receipt = Target.ApplyUpgrade(upgrade); } - protected override void StartEffect(GameObject target, Effect effect, EffectStartContext context) + protected override void ChangeActiveEffect(Effect previousEffect, Effect newEffect, ref EffectChangeResult? statusChange) { - var upgrade = Upgrade.FromEffects(createUpgradeEffect(effect)); - if (!target.CanApplyUpgrade(upgrade)) return; - receipt = target.ApplyUpgrade(upgrade); } - protected override void ApplyEffectTick(GameObject target, Effect effect) { } + protected override void ApplyEffectTick(Effect effect) { } - protected override void EndEffect(GameObject target, Effect effect) + protected override void EndScope(Effect effect) { receipt?.Rollback(); receipt = null; } - protected override void EndScope(GameObject target) { } - - private static IUpgradeEffect createUpgradeEffect(Effect effect) + private static ModifyParameter createUpgradeEffect(Effect effect) { return new ModifyParameter( AttributeType.Range, diff --git a/src/Bearded.TD/Game/Simulation/Elements/Phenomena/OnFire.Effect.cs b/src/Bearded.TD/Game/Simulation/Elements/Phenomena/OnFire.Effect.cs deleted file mode 100644 index cc2e8f45e..000000000 --- a/src/Bearded.TD/Game/Simulation/Elements/Phenomena/OnFire.Effect.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Bearded.TD.Game.Simulation.Damage; -using Bearded.Utilities.SpaceTime; - -namespace Bearded.TD.Game.Simulation.Elements.Phenomena; - -static partial class OnFire -{ - public readonly record struct Effect( - UntypedDamagePerSecond DamagePerSecond, IDamageSource? DamageSource, TimeSpan Duration) : IElementalEffect - { - IElementalPhenomenon IElementalEffect.Phenomenon => phenomenon; - } -} diff --git a/src/Bearded.TD/Game/Simulation/Elements/Phenomena/OnFire.Phenomenon.cs b/src/Bearded.TD/Game/Simulation/Elements/Phenomena/OnFire.Phenomenon.cs deleted file mode 100644 index 68bbdc7c7..000000000 --- a/src/Bearded.TD/Game/Simulation/Elements/Phenomena/OnFire.Phenomenon.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using Bearded.TD.Game.Simulation.GameObjects; - -namespace Bearded.TD.Game.Simulation.Elements.Phenomena; - -static partial class OnFire -{ - private static readonly IElementalPhenomenon phenomenon = new Phenomenon(); - - private sealed class Phenomenon : IElementalPhenomenon - { - public Type EffectType => typeof(Effect); - - public IElementalPhenomenon.IScope NewScope(GameObject target) => new Scope(target); - } -} diff --git a/src/Bearded.TD/Game/Simulation/Elements/Phenomena/OnFire.Scope.cs b/src/Bearded.TD/Game/Simulation/Elements/Phenomena/OnFire.Scope.cs deleted file mode 100644 index 4437fa0bc..000000000 --- a/src/Bearded.TD/Game/Simulation/Elements/Phenomena/OnFire.Scope.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System; -using System.Collections.Immutable; -using System.Linq; -using Bearded.TD.Game.Simulation.Damage; -using Bearded.TD.Game.Simulation.GameObjects; -using Bearded.TD.Game.Simulation.StatusDisplays; -using static Bearded.TD.Constants.Game.Elements; - -namespace Bearded.TD.Game.Simulation.Elements.Phenomena; - -static partial class OnFire -{ - private sealed class Scope(GameObject target) : ElementalPhenomenonScopeBase(target) - { - private FireFlicker? fireFlicker; - - protected override bool TryChooseEffect(out Effect effect) - { - var effects = ActiveEffects.ToImmutableArray(); - if (effects.IsEmpty) - { - effect = default; - return false; - } - - effect = effects.MaxBy(e => e.DamagePerSecond.Amount.NumericValue); - return true; - } - - protected override void StartScope(GameObject target, out ElementalStatus? status) - { - fireFlicker = new FireFlicker(); - target.AddComponent(fireFlicker); - status = new ElementalStatus("fire".ToStatusIconSpriteId()); - } - - protected override void StartEffect(GameObject target, Effect effect, EffectStartContext context) { } - - protected override void ApplyEffectTick(GameObject target, Effect effect) - { - var damage = effect.DamagePerSecond * TickDuration; - DamageExecutor.FromDamageSource(effect.DamageSource) - .TryDoDamage(target, damage.Typed(DamageType.Fire), Hit.FromSelf()); - } - - protected override void EndEffect(GameObject target, Effect effect) { } - - protected override void EndScope(GameObject target) - { - if (fireFlicker == null) - { - throw new InvalidOperationException("Cannot end effect that was not started."); - } - target.RemoveComponent(fireFlicker); - fireFlicker = null; - } - } -} diff --git a/src/Bearded.TD/Game/Simulation/Elements/Phenomena/OnFire.cs b/src/Bearded.TD/Game/Simulation/Elements/Phenomena/OnFire.cs new file mode 100644 index 000000000..af3661003 --- /dev/null +++ b/src/Bearded.TD/Game/Simulation/Elements/Phenomena/OnFire.cs @@ -0,0 +1,57 @@ +using System; +using Bearded.TD.Game.Simulation.Damage; +using Bearded.TD.Game.Simulation.GameObjects; +using Bearded.TD.Game.Simulation.StatusDisplays; +using Bearded.TD.Utilities.Collections; +using static Bearded.TD.Constants.Game.Elements; +using TimeSpan = Bearded.Utilities.SpaceTime.TimeSpan; + +namespace Bearded.TD.Game.Simulation.Elements.Phenomena; + +static class OnFire +{ + public readonly record struct Effect( + UntypedDamagePerSecond DamagePerSecond, IDamageSource? DamageSource, TimeSpan Duration) : IElementalEffect + { + public IElementalEffect.IScope NewScope(GameObject target) => new Scope(target); + } + + private sealed class Scope(GameObject target) : ElementalPhenomenonScopeBase(target) + { + private FireFlicker? fireFlicker; + + protected override Effect? TryChooseEffect(ReadOnlySpan activeEffects) + { + return activeEffects.MaxByOrDefault(static e => e.Effect.DamagePerSecond.Amount.NumericValue)?.Effect; + } + + protected override void StartScope(Effect effect, ref EffectChangeResult? statusChange) + { + fireFlicker = new FireFlicker(); + Target.AddComponent(fireFlicker); + statusChange = new ElementalStatus("fire".ToStatusIconSpriteId()); + } + + protected override void ChangeActiveEffect(Effect previousEffect, Effect newEffect, + ref EffectChangeResult? statusChange) + { + } + + protected override void ApplyEffectTick(Effect effect) + { + var damage = effect.DamagePerSecond * TickDuration; + DamageExecutor.FromDamageSource(effect.DamageSource) + .TryDoDamage(Target, damage.Typed(DamageType.Fire), Hit.FromSelf()); + } + + protected override void EndScope(Effect effect) + { + if (fireFlicker == null) + { + throw new InvalidOperationException("Cannot end effect that was not started."); + } + Target.RemoveComponent(fireFlicker); + fireFlicker = null; + } + } +} diff --git a/src/Bearded.TD/Game/Simulation/Elements/Phenomena/Shocked.Effect.cs b/src/Bearded.TD/Game/Simulation/Elements/Phenomena/Shocked.Effect.cs deleted file mode 100644 index 62d6ca603..000000000 --- a/src/Bearded.TD/Game/Simulation/Elements/Phenomena/Shocked.Effect.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Bearded.Utilities.SpaceTime; - -namespace Bearded.TD.Game.Simulation.Elements.Phenomena; - -static partial class Shocked -{ - public readonly record struct Effect(double MovementPenalty, TimeSpan Duration) : IElementalEffect - { - IElementalPhenomenon IElementalEffect.Phenomenon => phenomenon; - } -} diff --git a/src/Bearded.TD/Game/Simulation/Elements/Phenomena/Shocked.Phenomenon.cs b/src/Bearded.TD/Game/Simulation/Elements/Phenomena/Shocked.Phenomenon.cs deleted file mode 100644 index 9611bdc20..000000000 --- a/src/Bearded.TD/Game/Simulation/Elements/Phenomena/Shocked.Phenomenon.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using Bearded.TD.Game.Simulation.GameObjects; - -namespace Bearded.TD.Game.Simulation.Elements.Phenomena; - -static partial class Shocked -{ - private static readonly IElementalPhenomenon phenomenon = new Phenomenon(); - - private sealed class Phenomenon : IElementalPhenomenon - { - public Type EffectType => typeof(Effect); - - public IElementalPhenomenon.IScope NewScope(GameObject target) => new Scope(target); - } -} diff --git a/src/Bearded.TD/Game/Simulation/Elements/Phenomena/Shocked.Scope.cs b/src/Bearded.TD/Game/Simulation/Elements/Phenomena/Shocked.Scope.cs deleted file mode 100644 index 1c2a038fe..000000000 --- a/src/Bearded.TD/Game/Simulation/Elements/Phenomena/Shocked.Scope.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System; -using System.Collections.Immutable; -using System.Linq; -using Bearded.TD.Game.Simulation.GameObjects; -using Bearded.TD.Game.Simulation.StatusDisplays; -using Bearded.TD.Game.Simulation.Upgrades; -using Bearded.TD.Shared.TechEffects; - -namespace Bearded.TD.Game.Simulation.Elements.Phenomena; - -static partial class Shocked -{ - private sealed class Scope : ElementalPhenomenonScopeBase - { - private IUpgradeReceipt? receipt; - private LightningShocks? lightningShocks; - - public Scope(GameObject target) : base(target) { } - - protected override bool TryChooseEffect(out Effect effect) - { - var effects = ActiveEffects.ToImmutableArray(); - if (effects.IsEmpty) - { - effect = default; - return false; - } - - effect = effects.MaxBy(e => e.MovementPenalty); - return true; - } - - private static IUpgradeEffect createUpgradeEffect(Effect effect) - { - return new ModifyParameter( - AttributeType.MovementSpeed, - Modification.MultiplyWith(1 - effect.MovementPenalty), - UpgradePrerequisites.Empty, - false); - } - - protected override void StartScope(GameObject target, out ElementalStatus? status) - { - lightningShocks = new LightningShocks(); - target.AddComponent(lightningShocks); - status = new ElementalStatus("snail".ToStatusIconSpriteId()); - } - - protected override void StartEffect(GameObject target, Effect effect, EffectStartContext context) - { - var upgrade = Upgrade.FromEffects(createUpgradeEffect(effect)); - if (!target.CanApplyUpgrade(upgrade)) return; - receipt = target.ApplyUpgrade(upgrade); - } - - protected override void ApplyEffectTick(GameObject target, Effect effect) { } - - protected override void EndEffect(GameObject target, Effect effect) - { - receipt?.Rollback(); - receipt = null; - } - - protected override void EndScope(GameObject target) - { - if (lightningShocks == null) - { - throw new InvalidOperationException("Cannot end effect that was not started."); - } - - target.RemoveComponent(lightningShocks); - lightningShocks = null; - } - } -} diff --git a/src/Bearded.TD/Game/Simulation/Elements/Phenomena/Shocked.cs b/src/Bearded.TD/Game/Simulation/Elements/Phenomena/Shocked.cs new file mode 100644 index 000000000..86ea0228b --- /dev/null +++ b/src/Bearded.TD/Game/Simulation/Elements/Phenomena/Shocked.cs @@ -0,0 +1,75 @@ +using System; +using Bearded.TD.Game.Simulation.GameObjects; +using Bearded.TD.Game.Simulation.StatusDisplays; +using Bearded.TD.Game.Simulation.Upgrades; +using Bearded.TD.Shared.TechEffects; +using Bearded.TD.Utilities.Collections; +using TimeSpan = Bearded.Utilities.SpaceTime.TimeSpan; + +namespace Bearded.TD.Game.Simulation.Elements.Phenomena; + +static class Shocked +{ + public readonly record struct Effect(double MovementPenalty, TimeSpan Duration) : IElementalEffect + { + public IElementalEffect.IScope NewScope(GameObject target) => new Scope(target); + } + + private sealed class Scope(GameObject target) : ElementalPhenomenonScopeBase(target) + { + private IUpgradeReceipt? receipt; + private LightningShocks? lightningShocks; + + protected override Effect? TryChooseEffect(ReadOnlySpan activeEffects) + { + return activeEffects.MaxByOrDefault(static e => e.Effect.MovementPenalty)?.Effect; + } + + protected override void StartScope(Effect effect, ref EffectChangeResult? statusChange) + { + lightningShocks = new LightningShocks(); + Target.AddComponent(lightningShocks); + statusChange = new ElementalStatus("snail".ToStatusIconSpriteId()); + + tryApplyEffect(effect); + } + + protected override void ChangeActiveEffect(Effect previousEffect, Effect newEffect, ref EffectChangeResult? statusChange) + { + tryApplyEffect(newEffect); + } + + protected override void ApplyEffectTick(Effect effect) { } + + protected override void EndScope(Effect effect) + { + receipt?.Rollback(); + receipt = null; + + if (lightningShocks == null) + { + throw new InvalidOperationException("Cannot end effect that was not started."); + } + + Target.RemoveComponent(lightningShocks); + lightningShocks = null; + } + + private void tryApplyEffect(Effect effect) + { + receipt?.Rollback(); + var upgrade = Upgrade.FromEffects(createUpgradeEffect(effect)); + if (!Target.CanApplyUpgrade(upgrade)) return; + receipt = Target.ApplyUpgrade(upgrade); + } + + private static ModifyParameter createUpgradeEffect(Effect effect) + { + return new ModifyParameter( + AttributeType.MovementSpeed, + Modification.MultiplyWith(1 - effect.MovementPenalty), + UpgradePrerequisites.Empty, + false); + } + } +} diff --git a/src/Bearded.TD/Game/Simulation/Elements/Phenomena/Stunned.Effect.cs b/src/Bearded.TD/Game/Simulation/Elements/Phenomena/Stunned.Effect.cs deleted file mode 100644 index e69cb5c7b..000000000 --- a/src/Bearded.TD/Game/Simulation/Elements/Phenomena/Stunned.Effect.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Bearded.Utilities.SpaceTime; - -namespace Bearded.TD.Game.Simulation.Elements.Phenomena; - -static partial class Stunned -{ - public readonly record struct Effect(TimeSpan Duration) : IElementalEffect - { - IElementalPhenomenon IElementalEffect.Phenomenon => phenomenon; - } -} diff --git a/src/Bearded.TD/Game/Simulation/Elements/Phenomena/Stunned.Phenomenon.cs b/src/Bearded.TD/Game/Simulation/Elements/Phenomena/Stunned.Phenomenon.cs deleted file mode 100644 index 65c872968..000000000 --- a/src/Bearded.TD/Game/Simulation/Elements/Phenomena/Stunned.Phenomenon.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using Bearded.TD.Game.Simulation.GameObjects; - -namespace Bearded.TD.Game.Simulation.Elements.Phenomena; - -static partial class Stunned -{ - private static readonly IElementalPhenomenon phenomenon = new Phenomenon(); - - private sealed class Phenomenon : IElementalPhenomenon - { - public Type EffectType => typeof(Effect); - - public IElementalPhenomenon.IScope NewScope(GameObject target) => new Scope(target); - } -} diff --git a/src/Bearded.TD/Game/Simulation/Elements/Phenomena/Stunned.Scope.cs b/src/Bearded.TD/Game/Simulation/Elements/Phenomena/Stunned.Scope.cs deleted file mode 100644 index 56582a06d..000000000 --- a/src/Bearded.TD/Game/Simulation/Elements/Phenomena/Stunned.Scope.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System; -using System.Collections.Immutable; -using System.Linq; -using Bearded.TD.Game.Simulation.Buildings.Ruins; -using Bearded.TD.Game.Simulation.GameObjects; -using Bearded.TD.Game.Simulation.StatusDisplays; - -namespace Bearded.TD.Game.Simulation.Elements.Phenomena; - -static partial class Stunned -{ - private sealed class Scope : ElementalPhenomenonScopeBase - { - private IBreakageReceipt? receipt; - private LightningShocks? sparks; - - public Scope(GameObject target) : base(target) { } - - protected override bool TryChooseEffect(out Effect effect) - { - var effects = ActiveEffects.ToImmutableArray(); - if (effects.IsEmpty) - { - effect = default; - return false; - } - - effect = effects.MaxBy(e => e.Duration); - return true; - } - - protected override void StartScope(GameObject target, out ElementalStatus? status) - { - if (receipt != null || sparks != null) - { - throw new InvalidOperationException("Cannot start effect that was already started."); - } - - if (!target.TryGetSingleComponent(out var breakageHandler)) - { - status = null; - return; - } - - receipt = breakageHandler.BreakObject(); - sparks = new LightningShocks(); - target.AddComponent(sparks); - - status = new ElementalStatus("unstable-orb".ToStatusIconSpriteId()); - } - - protected override void StartEffect(GameObject target, Effect effect, EffectStartContext context) { } - - protected override void ApplyEffectTick(GameObject target, Effect effect) {} - - protected override void EndEffect(GameObject target, Effect effect) { } - - protected override void EndScope(GameObject target) - { - receipt?.Repair(); - receipt = null; - - if (sparks != null) - { - target.RemoveComponent(sparks); - sparks = null; - } - } - } -} diff --git a/src/Bearded.TD/Game/Simulation/Elements/Phenomena/Stunned.cs b/src/Bearded.TD/Game/Simulation/Elements/Phenomena/Stunned.cs new file mode 100644 index 000000000..0ce7ebefa --- /dev/null +++ b/src/Bearded.TD/Game/Simulation/Elements/Phenomena/Stunned.cs @@ -0,0 +1,57 @@ +using System; +using Bearded.TD.Game.Simulation.Buildings.Ruins; +using Bearded.TD.Game.Simulation.GameObjects; +using Bearded.TD.Game.Simulation.StatusDisplays; +using Bearded.TD.Utilities.Collections; +using TimeSpan = Bearded.Utilities.SpaceTime.TimeSpan; + +namespace Bearded.TD.Game.Simulation.Elements.Phenomena; + +static class Stunned +{ + public readonly record struct Effect(TimeSpan Duration) : IElementalEffect + { + public IElementalEffect.IScope NewScope(GameObject target) => new Scope(target); + } + + private sealed class Scope(GameObject target) : ElementalPhenomenonScopeBase(target) + { + private IBreakageReceipt? receipt; + private LightningShocks? sparks; + + protected override Effect? TryChooseEffect(ReadOnlySpan activeEffects) + { + return activeEffects.MaxByOrDefault(static e => e.Effect.Duration)?.Effect; + } + + protected override void StartScope(Effect effect, ref EffectChangeResult? statusChange) + { + if (!Target.TryGetSingleComponent(out var breakageHandler)) + { + return; + } + + receipt = breakageHandler.BreakObject(); + sparks = new LightningShocks(); + Target.AddComponent(sparks); + + statusChange = new ElementalStatus("unstable-orb".ToStatusIconSpriteId()); + } + + protected override void ChangeActiveEffect(Effect previousEffect, Effect newEffect, ref EffectChangeResult? statusChange) { } + + protected override void ApplyEffectTick(Effect effect) { } + + protected override void EndScope(Effect effect) + { + receipt?.Repair(); + receipt = null; + + if (sparks != null) + { + Target.RemoveComponent(sparks); + sparks = null; + } + } + } +} diff --git a/src/Bearded.TD/Game/Simulation/Elements/Phenomena/components/ApplyAreaEffectOnImpact.cs b/src/Bearded.TD/Game/Simulation/Elements/Phenomena/components/ApplyAreaEffectOnImpact.cs index 66b9431d1..4616554af 100644 --- a/src/Bearded.TD/Game/Simulation/Elements/Phenomena/components/ApplyAreaEffectOnImpact.cs +++ b/src/Bearded.TD/Game/Simulation/Elements/Phenomena/components/ApplyAreaEffectOnImpact.cs @@ -21,7 +21,7 @@ interface IAreaEffectParameters abstract class ApplyAreaEffectOnImpact : Component, IListener, IListener where TParameters : IParametersTemplate, IAreaEffectParameters - where TEffect : IElementalEffect + where TEffect : IElementalEffect { protected ApplyAreaEffectOnImpact(TParameters parameters) : base(parameters) { } diff --git a/src/Bearded.TD/Game/Simulation/Elements/Phenomena/components/ApplyEffectOnImpact.cs b/src/Bearded.TD/Game/Simulation/Elements/Phenomena/components/ApplyEffectOnImpact.cs index 68ae8ea32..896ca92a8 100644 --- a/src/Bearded.TD/Game/Simulation/Elements/Phenomena/components/ApplyEffectOnImpact.cs +++ b/src/Bearded.TD/Game/Simulation/Elements/Phenomena/components/ApplyEffectOnImpact.cs @@ -10,7 +10,7 @@ namespace Bearded.TD.Game.Simulation.Elements.Phenomena; abstract class ApplyEffectOnImpact : Component, IListener where TParameters : IParametersTemplate - where TEffect : IElementalEffect + where TEffect : IElementalEffect { protected abstract double Probability { get; } diff --git a/src/Bearded.TD/Game/Simulation/Elements/components/ElementSystemEntity.cs b/src/Bearded.TD/Game/Simulation/Elements/components/ElementSystemEntity.cs index 6163ca84b..4746964ab 100644 --- a/src/Bearded.TD/Game/Simulation/Elements/components/ElementSystemEntity.cs +++ b/src/Bearded.TD/Game/Simulation/Elements/components/ElementSystemEntity.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using Bearded.TD.Game.Simulation.GameObjects; +using Bearded.TD.Utilities.Collections; using Bearded.Utilities.SpaceTime; using TimeSpan = Bearded.Utilities.SpaceTime.TimeSpan; @@ -8,12 +9,12 @@ namespace Bearded.TD.Game.Simulation.Elements; interface IElementSystemEntity { - void ApplyEffect(T effect) where T : IElementalEffect; + void ApplyEffect(T effect) where T : IElementalEffect; } sealed class ElementSystemEntity : Component, IElementSystemEntity { - private readonly Dictionary effectScopes = new(); + private readonly Dictionary effectScopes = new(); private TickCycle? tickCycle; protected override void OnAdded() { } @@ -38,18 +39,10 @@ private void applyTicks(Instant now) } } - public void ApplyEffect(T effect) where T : IElementalEffect + public void ApplyEffect(T effect) where T : IElementalEffect { - if (!effectScopes.TryGetValue(typeof(T), out var scope)) - { - if (effect.Phenomenon.EffectType != typeof(T)) - { - throw new InvalidOperationException("Type of effect must be same as phenomenon effect type."); - } - scope = effect.Phenomenon.NewScope(Owner); - effectScopes.Add(typeof(T), scope); - } + var scope = effectScopes.GetOrInsert(typeof(T), (effect, Owner), static ctx => ctx.effect.NewScope(ctx.Owner)); - ((IElementalPhenomenon.IScope) scope).Adopt(effect, Owner.Game.Time); + ((IElementalEffect.IScope) scope).Adopt(effect, Owner.Game.Time); } } diff --git a/src/Bearded.TD/Game/Simulation/Projectiles/AreaOfEffect.cs b/src/Bearded.TD/Game/Simulation/Projectiles/AreaOfEffect.cs index d47c2035b..11e1eb645 100644 --- a/src/Bearded.TD/Game/Simulation/Projectiles/AreaOfEffect.cs +++ b/src/Bearded.TD/Game/Simulation/Projectiles/AreaOfEffect.cs @@ -24,7 +24,7 @@ public static void Damage( public static void ApplyStatusEffect( GameState game, T effect, Position3 center, Unit range) - where T : IElementalEffect + where T : IElementalEffect { foreach (var (obj, _) in FindObjects(game, center, range)) { diff --git a/src/Bearded.TD/Utilities/Collections/LinqExtensions.cs b/src/Bearded.TD/Utilities/Collections/LinqExtensions.cs index 8f68f3df2..c12ef15cc 100644 --- a/src/Bearded.TD/Utilities/Collections/LinqExtensions.cs +++ b/src/Bearded.TD/Utilities/Collections/LinqExtensions.cs @@ -17,11 +17,21 @@ public static Dictionary ToDictionary( public static TValue GetOrInsert(this Dictionary dictionary, TKey key, Func getValueToInsert) where TKey : notnull + { + return dictionary.GetOrInsert(key, getValueToInsert, static factory => factory()); + } + + public static TValue GetOrInsert( + this Dictionary dictionary, + TKey key, + TContext context, + Func getValueToInsert) + where TKey : notnull { if (dictionary.TryGetValue(key, out var value)) return value; - value = getValueToInsert(); + value = getValueToInsert(context); dictionary.Add(key, value); return value; } diff --git a/src/Bearded.TD/Utilities/Collections/ListExtensions.cs b/src/Bearded.TD/Utilities/Collections/ListExtensions.cs index 6dc00b83c..bbe32af47 100644 --- a/src/Bearded.TD/Utilities/Collections/ListExtensions.cs +++ b/src/Bearded.TD/Utilities/Collections/ListExtensions.cs @@ -14,4 +14,36 @@ public static T Shift(this IList list) list.RemoveAt(0); return elmt; } -} \ No newline at end of file + + /// + /// Allocation-free version of List.RemoveAll, assuming the predicate is a static function. + /// + public static int RemoveAll(this List list, TContext context, Func match) + { + // Implementation adapted from system method. + + var freeIndex = 0; + var count = list.Count; + + while (freeIndex < count && !match(context, list[freeIndex])) + freeIndex++; + + if (freeIndex >= count) + return 0; + + var current = freeIndex + 1; + while (current < count) + { + while (current < count && match(context, list[current])) + current++; + + if (current < count) + list[freeIndex++] = list[current++]; + } + + var removedCount = count - freeIndex; + list.RemoveRange(freeIndex, removedCount); + + return removedCount; + } +} diff --git a/src/Bearded.TD/Utilities/Collections/SpanExtensions.cs b/src/Bearded.TD/Utilities/Collections/SpanExtensions.cs new file mode 100644 index 000000000..9c8db41ad --- /dev/null +++ b/src/Bearded.TD/Utilities/Collections/SpanExtensions.cs @@ -0,0 +1,85 @@ +using System; + +namespace Bearded.TD.Utilities.Collections; + +static class StructSpanExtensions +{ + public static T? MinByOrDefault(this ReadOnlySpan span, Func selector) + where TComparable : IComparable + where T : struct + { + return span.IsEmpty ? null : span.MinBy(selector); + } + + public static T? MaxByOrDefault(this ReadOnlySpan span, Func selector) + where TComparable : IComparable + where T : struct + { + return span.IsEmpty ? null : span.MaxBy(selector); + } +} + +static class ClassSpanExtensions +{ + public static T? MinByOrDefault(this ReadOnlySpan span, Func selector) + where TComparable : IComparable + where T : class + { + return span.IsEmpty ? null : span.MinBy(selector); + } + + public static T? MaxByOrDefault(this ReadOnlySpan span, Func selector) + where TComparable : IComparable + where T : class + { + return span.IsEmpty ? null : span.MaxBy(selector); + } +} + +static class SpanExtensions +{ + public static T MinBy(this ReadOnlySpan span, Func selector) + where TComparable : IComparable + { + if (span.IsEmpty) + throw new ArgumentException("Span is empty", nameof(span)); + + var minItem = span[0]; + var minValue = selector(minItem); + + for (var i = 1; i < span.Length; i++) + { + var item = span[i]; + var value = selector(item); + if (value.CompareTo(minValue) < 0) + { + minItem = item; + minValue = value; + } + } + + return minItem; + } + public static T MaxBy(this ReadOnlySpan span, Func selector) + where TComparable : IComparable + { + if (span.IsEmpty) + throw new ArgumentException("Span is empty", nameof(span)); + + var maxItem = span[0]; + var maxValue = selector(maxItem); + + for (var i = 1; i < span.Length; i++) + { + var item = span[i]; + var value = selector(item); + if (value.CompareTo(maxValue) > 0) + { + maxItem = item; + maxValue = value; + } + } + + return maxItem; + } +} From 4543e586758811f079214053aac42dc31b822d30 Mon Sep 17 00:00:00 2001 From: "Paul C. Scharf" Date: Mon, 12 May 2025 20:20:15 +0100 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=9A=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Elements/ElementalPhenomenonScopeBase.cs | 68 +++++++++++++------ .../Elements/IElementalPhenomenon.cs | 7 -- .../Elements/Phenomena/LowerWeaponRange.cs | 18 +++-- .../Simulation/Elements/Phenomena/OnFire.cs | 14 ++-- .../Simulation/Elements/Phenomena/Shocked.cs | 25 +++---- .../Simulation/Elements/Phenomena/Stunned.cs | 10 +-- 6 files changed, 82 insertions(+), 60 deletions(-) delete mode 100644 src/Bearded.TD/Game/Simulation/Elements/IElementalPhenomenon.cs diff --git a/src/Bearded.TD/Game/Simulation/Elements/ElementalPhenomenonScopeBase.cs b/src/Bearded.TD/Game/Simulation/Elements/ElementalPhenomenonScopeBase.cs index 2406b2046..14001dd73 100644 --- a/src/Bearded.TD/Game/Simulation/Elements/ElementalPhenomenonScopeBase.cs +++ b/src/Bearded.TD/Game/Simulation/Elements/ElementalPhenomenonScopeBase.cs @@ -34,7 +34,7 @@ public void ApplyTick(Instant now) { activeEffects.RemoveAll(now, static (t, e) => e.Expiry <= t); - if (TryChooseEffect(CollectionsMarshal.AsSpan(activeEffects)) is { } effect) + if (ChooseEffect(CollectionsMarshal.AsSpan(activeEffects)) is { } effect) { transitionToEffect(effect); ApplyEffectTick(effect); @@ -44,7 +44,7 @@ public void ApplyTick(Instant now) endEffectIfPreviouslyActive(); } - if (activeEffect?.StatusIcon is { } icon) + if (activeEffect?.StatusReceipt is { } icon) { updateStatusIconExpiry(icon); } @@ -55,9 +55,14 @@ private void transitionToEffect(TEffect effect) // null -> effect A if (activeEffect is not { } current) { - EffectChangeResult? statusChange = null; - StartScope(effect, ref statusChange); - var receipt = statusChange?.NewStatus is { } status ? reportStatus(status) : null; + var statusChange = EffectChangeResult.NoChange; + StartScope(ref statusChange); + StartEffect(effect, ref statusChange); + + var receipt = statusChange.Type == EffectChangeType.ShowStatusIcon + ? reportStatus(statusChange.NewStatus!.Value) + : null; + activeEffect = new ActiveEffect(effect, receipt); return; } @@ -68,10 +73,18 @@ private void transitionToEffect(TEffect effect) // effect A -> effect B { - EffectChangeResult? statusChange = null; - ChangeActiveEffect(current.Effect, effect, ref statusChange); - current.StatusIcon?.DeleteImmediately(); - var receipt = statusChange?.NewStatus is { } status ? reportStatus(status) : null; + var statusChange = EffectChangeResult.NoChange; + EndEffect(); + StartEffect(effect, ref statusChange); + + var receipt = current.StatusReceipt; + + if (statusChange.Type == EffectChangeType.ShowStatusIcon) + { + receipt?.DeleteImmediately(); + receipt = reportStatus(statusChange.NewStatus!.Value); + } + activeEffect = new ActiveEffect(effect, receipt); } } @@ -89,8 +102,9 @@ private void endEffectIfPreviouslyActive() { if (activeEffect is not { } current) return; - EndScope(current.Effect); - current.StatusIcon?.DeleteImmediately(); + EndEffect(); + EndScope(); + current.StatusReceipt?.DeleteImmediately(); activeEffect = null; } @@ -100,31 +114,41 @@ private void updateStatusIconExpiry(IStatusReceipt statusIcon) statusIcon.SetExpiryTime(latestExpiry); } - protected abstract TEffect? TryChooseEffect(ReadOnlySpan activeEffects); - protected abstract void StartScope(TEffect effect, ref EffectChangeResult? statusChange); - protected abstract void ChangeActiveEffect(TEffect previousEffect, TEffect newEffect, ref EffectChangeResult? statusChange); + protected abstract TEffect? ChooseEffect(ReadOnlySpan activeEffects); + protected abstract void StartScope(ref EffectChangeResult statusChange); + protected abstract void StartEffect(TEffect effect, ref EffectChangeResult statusChange); protected abstract void ApplyEffectTick(TEffect effect); - protected abstract void EndScope(TEffect effect); + protected abstract void EndEffect(); + protected abstract void EndScope(); protected readonly record struct EffectWithExpiry(TEffect Effect, Instant Expiry); - private readonly record struct ActiveEffect(TEffect Effect, IStatusReceipt? StatusIcon); + private readonly record struct ActiveEffect(TEffect Effect, IStatusReceipt? StatusReceipt); } // Base class for non-generic members abstract class ElementalPhenomenonScopeBase { - public readonly struct EffectChangeResult + protected enum EffectChangeType { + Retain = 0, + ShowStatusIcon = 1, + } + + protected readonly struct EffectChangeResult + { + public EffectChangeType Type { get; private init; } + public ElementalStatus? NewStatus { get; private init; } - public static EffectChangeResult HideStatus => default; + public static EffectChangeResult NoChange => default; - public static EffectChangeResult ShowStatus(ElementalStatus status) => new() { NewStatus = status }; + public static EffectChangeResult ShowStatus(ElementalStatus status) => new() + { + Type = EffectChangeType.ShowStatusIcon, + NewStatus = status, + }; public static implicit operator EffectChangeResult(ElementalStatus newStatus) => ShowStatus(newStatus); } - - protected static EffectChangeResult HideStatus => EffectChangeResult.HideStatus; - protected static EffectChangeResult ShowStatus(ElementalStatus status) => EffectChangeResult.ShowStatus(status); } diff --git a/src/Bearded.TD/Game/Simulation/Elements/IElementalPhenomenon.cs b/src/Bearded.TD/Game/Simulation/Elements/IElementalPhenomenon.cs deleted file mode 100644 index 706fe750f..000000000 --- a/src/Bearded.TD/Game/Simulation/Elements/IElementalPhenomenon.cs +++ /dev/null @@ -1,7 +0,0 @@ -using Bearded.Utilities.SpaceTime; - -namespace Bearded.TD.Game.Simulation.Elements; - -interface IElementalPhenomenon -{ -} diff --git a/src/Bearded.TD/Game/Simulation/Elements/Phenomena/LowerWeaponRange.cs b/src/Bearded.TD/Game/Simulation/Elements/Phenomena/LowerWeaponRange.cs index ab2661f91..6249ec2ac 100644 --- a/src/Bearded.TD/Game/Simulation/Elements/Phenomena/LowerWeaponRange.cs +++ b/src/Bearded.TD/Game/Simulation/Elements/Phenomena/LowerWeaponRange.cs @@ -19,32 +19,36 @@ private sealed class Scope(GameObject target) : ElementalPhenomenonScopeBase activeEffects) + protected override Effect? ChooseEffect(ReadOnlySpan activeEffects) { return activeEffects.MinByOrDefault(e => e.Effect.Factor)?.Effect; } - protected override void StartScope(Effect effect, ref EffectChangeResult? statusChange) + protected override void StartScope(ref EffectChangeResult statusChange) { statusChange = new ElementalStatus("eye-disabled".ToStatusIconSpriteId()); - var upgrade = Upgrade.FromEffects(createUpgradeEffect(effect)); - if (!Target.CanApplyUpgrade(upgrade)) return; - receipt = Target.ApplyUpgrade(upgrade); } - protected override void ChangeActiveEffect(Effect previousEffect, Effect newEffect, ref EffectChangeResult? statusChange) + protected override void StartEffect(Effect effect, ref EffectChangeResult statusChange) { + var upgrade = Upgrade.FromEffects(createUpgradeEffect(effect)); + if (!Target.CanApplyUpgrade(upgrade)) return; + receipt = Target.ApplyUpgrade(upgrade); } protected override void ApplyEffectTick(Effect effect) { } - protected override void EndScope(Effect effect) + protected override void EndEffect() { receipt?.Rollback(); receipt = null; } + protected override void EndScope() + { + } + private static ModifyParameter createUpgradeEffect(Effect effect) { return new ModifyParameter( diff --git a/src/Bearded.TD/Game/Simulation/Elements/Phenomena/OnFire.cs b/src/Bearded.TD/Game/Simulation/Elements/Phenomena/OnFire.cs index af3661003..1c5b68356 100644 --- a/src/Bearded.TD/Game/Simulation/Elements/Phenomena/OnFire.cs +++ b/src/Bearded.TD/Game/Simulation/Elements/Phenomena/OnFire.cs @@ -20,23 +20,23 @@ private sealed class Scope(GameObject target) : ElementalPhenomenonScopeBase activeEffects) + protected override Effect? ChooseEffect(ReadOnlySpan activeEffects) { return activeEffects.MaxByOrDefault(static e => e.Effect.DamagePerSecond.Amount.NumericValue)?.Effect; } - protected override void StartScope(Effect effect, ref EffectChangeResult? statusChange) + protected override void StartScope(ref EffectChangeResult statusChange) { fireFlicker = new FireFlicker(); Target.AddComponent(fireFlicker); statusChange = new ElementalStatus("fire".ToStatusIconSpriteId()); } - protected override void ChangeActiveEffect(Effect previousEffect, Effect newEffect, - ref EffectChangeResult? statusChange) + protected override void StartEffect(Effect effect, ref EffectChangeResult statusChange) { } + protected override void ApplyEffectTick(Effect effect) { var damage = effect.DamagePerSecond * TickDuration; @@ -44,7 +44,11 @@ protected override void ApplyEffectTick(Effect effect) .TryDoDamage(Target, damage.Typed(DamageType.Fire), Hit.FromSelf()); } - protected override void EndScope(Effect effect) + protected override void EndEffect() + { + } + + protected override void EndScope() { if (fireFlicker == null) { diff --git a/src/Bearded.TD/Game/Simulation/Elements/Phenomena/Shocked.cs b/src/Bearded.TD/Game/Simulation/Elements/Phenomena/Shocked.cs index 86ea0228b..af410e155 100644 --- a/src/Bearded.TD/Game/Simulation/Elements/Phenomena/Shocked.cs +++ b/src/Bearded.TD/Game/Simulation/Elements/Phenomena/Shocked.cs @@ -20,32 +20,35 @@ private sealed class Scope(GameObject target) : ElementalPhenomenonScopeBase activeEffects) + protected override Effect? ChooseEffect(ReadOnlySpan activeEffects) { return activeEffects.MaxByOrDefault(static e => e.Effect.MovementPenalty)?.Effect; } - protected override void StartScope(Effect effect, ref EffectChangeResult? statusChange) + protected override void StartScope(ref EffectChangeResult statusChange) { lightningShocks = new LightningShocks(); Target.AddComponent(lightningShocks); statusChange = new ElementalStatus("snail".ToStatusIconSpriteId()); - - tryApplyEffect(effect); } - protected override void ChangeActiveEffect(Effect previousEffect, Effect newEffect, ref EffectChangeResult? statusChange) + protected override void StartEffect(Effect effect, ref EffectChangeResult statusChange) { - tryApplyEffect(newEffect); + var upgrade = Upgrade.FromEffects(createUpgradeEffect(effect)); + if (!Target.CanApplyUpgrade(upgrade)) return; + receipt = Target.ApplyUpgrade(upgrade); } protected override void ApplyEffectTick(Effect effect) { } - protected override void EndScope(Effect effect) + protected override void EndEffect() { receipt?.Rollback(); receipt = null; + } + protected override void EndScope() + { if (lightningShocks == null) { throw new InvalidOperationException("Cannot end effect that was not started."); @@ -55,14 +58,6 @@ protected override void EndScope(Effect effect) lightningShocks = null; } - private void tryApplyEffect(Effect effect) - { - receipt?.Rollback(); - var upgrade = Upgrade.FromEffects(createUpgradeEffect(effect)); - if (!Target.CanApplyUpgrade(upgrade)) return; - receipt = Target.ApplyUpgrade(upgrade); - } - private static ModifyParameter createUpgradeEffect(Effect effect) { return new ModifyParameter( diff --git a/src/Bearded.TD/Game/Simulation/Elements/Phenomena/Stunned.cs b/src/Bearded.TD/Game/Simulation/Elements/Phenomena/Stunned.cs index 0ce7ebefa..3b77d442a 100644 --- a/src/Bearded.TD/Game/Simulation/Elements/Phenomena/Stunned.cs +++ b/src/Bearded.TD/Game/Simulation/Elements/Phenomena/Stunned.cs @@ -19,12 +19,12 @@ private sealed class Scope(GameObject target) : ElementalPhenomenonScopeBase activeEffects) + protected override Effect? ChooseEffect(ReadOnlySpan activeEffects) { return activeEffects.MaxByOrDefault(static e => e.Effect.Duration)?.Effect; } - protected override void StartScope(Effect effect, ref EffectChangeResult? statusChange) + protected override void StartScope(ref EffectChangeResult statusChange) { if (!Target.TryGetSingleComponent(out var breakageHandler)) { @@ -38,11 +38,13 @@ protected override void StartScope(Effect effect, ref EffectChangeResult? status statusChange = new ElementalStatus("unstable-orb".ToStatusIconSpriteId()); } - protected override void ChangeActiveEffect(Effect previousEffect, Effect newEffect, ref EffectChangeResult? statusChange) { } + protected override void StartEffect(Effect effect, ref EffectChangeResult statusChange) { } protected override void ApplyEffectTick(Effect effect) { } - protected override void EndScope(Effect effect) + protected override void EndEffect() { } + + protected override void EndScope() { receipt?.Repair(); receipt = null;