using System; using System.Collections.Generic; using System.Diagnostics; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using FalseGods.Core.Arena; using FalseGods.Core.Arena.Events; using FalseGods.Core.Bosses; using FalseGods.Core.Bosses.Combat; using FalseGods.Core.Bosses.Events; using FalseGods.Core.Simulation; using Microsoft.CodeAnalysis; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyCompany("FalseGods.Core")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0+6e5740a80f810ab422551028733810b527fb614d")] [assembly: AssemblyProduct("FalseGods.Core")] [assembly: AssemblyTitle("FalseGods.Core")] [assembly: AssemblyVersion("1.0.0.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } internal static class IsExternalInit { } } namespace FalseGods.Core.Simulation { public readonly struct DefinitionId : IEquatable { private readonly int _value; public int Value => _value; public DefinitionId(int value) { _value = value; } public bool Equals(DefinitionId other) { return _value == other._value; } public override bool Equals(object? obj) { if (obj is DefinitionId other) { return Equals(other); } return false; } public override int GetHashCode() { return _value; } public override string ToString() { return $"definition:{_value}"; } public static bool operator ==(DefinitionId left, DefinitionId right) { return left.Equals(right); } public static bool operator !=(DefinitionId left, DefinitionId right) { return !left.Equals(right); } } public readonly struct EncounterId : IEquatable { private readonly int _value; public int Value => _value; public EncounterId(int value) { _value = value; } public bool Equals(EncounterId other) { return _value == other._value; } public override bool Equals(object? obj) { if (obj is EncounterId other) { return Equals(other); } return false; } public override int GetHashCode() { return _value; } public override string ToString() { return $"encounter:{_value}"; } public static bool operator ==(EncounterId left, EncounterId right) { return left.Equals(right); } public static bool operator !=(EncounterId left, EncounterId right) { return !left.Equals(right); } } public interface IAuthoritativeRandom { int NextInt(int minInclusive, int maxExclusive); float NextFloat(); } public interface IEncounterParticipantQuery { IReadOnlyList Participants { get; } bool TryGetPosition(ParticipantId participant, out SimVector2 position); } public interface ISimulationClock { long Tick { get; } float Time { get; } } public readonly struct ParticipantId : IEquatable { private readonly int _value; public int Value => _value; public ParticipantId(int value) { _value = value; } public bool Equals(ParticipantId other) { return _value == other._value; } public override bool Equals(object? obj) { if (obj is ParticipantId other) { return Equals(other); } return false; } public override int GetHashCode() { return _value; } public override string ToString() { return $"participant:{_value}"; } public static bool operator ==(ParticipantId left, ParticipantId right) { return left.Equals(right); } public static bool operator !=(ParticipantId left, ParticipantId right) { return !left.Equals(right); } } public readonly struct SimVector2 : IEquatable { public float X { get; } public float Z { get; } public static SimVector2 Zero => new SimVector2(0f, 0f); public SimVector2(float x, float z) { X = x; Z = z; } public float DistanceTo(SimVector2 other) { float num = other.X - X; float num2 = other.Z - Z; return (float)Math.Sqrt(num * num + num2 * num2); } public SimVector2 MoveToward(SimVector2 target, float maxDistance) { if (maxDistance <= 0f) { return this; } float num = target.X - X; float num2 = target.Z - Z; float num3 = (float)Math.Sqrt(num * num + num2 * num2); if (num3 <= maxDistance || num3 <= 0f) { return target; } float num4 = maxDistance / num3; return new SimVector2(X + num * num4, Z + num2 * num4); } public SimVector2 DirectionTo(SimVector2 target) { float num = target.X - X; float num2 = target.Z - Z; float num3 = (float)Math.Sqrt(num * num + num2 * num2); if (num3 <= 0f) { return Zero; } return new SimVector2(num / num3, num2 / num3); } public bool Equals(SimVector2 other) { if (X.Equals(other.X)) { return Z.Equals(other.Z); } return false; } public override bool Equals(object? obj) { if (obj is SimVector2 other) { return Equals(other); } return false; } public override int GetHashCode() { return (X.GetHashCode() * 397) ^ Z.GetHashCode(); } public override string ToString() { return $"({X}, {Z})"; } public static bool operator ==(SimVector2 left, SimVector2 right) { return left.Equals(right); } public static bool operator !=(SimVector2 left, SimVector2 right) { return !left.Equals(right); } } } namespace FalseGods.Core.Encounters { public sealed class EncounterCoordinator { private readonly ArenaSimulation _arena; private readonly MechanismGroupId _phaseTwoGroup; public EncounterId Id { get; } public EncounterPhase Phase { get; private set; } public EncounterCoordinator(EncounterId id, ArenaSimulation arena, MechanismGroupId phaseTwoGroup) { Id = id; _arena = arena ?? throw new ArgumentNullException("arena"); _phaseTwoGroup = phaseTwoGroup; Phase = EncounterPhase.PreFight; } public void Begin() { if (Phase == EncounterPhase.PreFight) { Phase = EncounterPhase.Fighting; } } public void BeginExit() { Phase = EncounterPhase.Exiting; } public void Process(IReadOnlyList bossEvents) { if (bossEvents == null) { throw new ArgumentNullException("bossEvents"); } for (int i = 0; i < bossEvents.Count; i++) { IBossDomainEvent bossDomainEvent = bossEvents[i]; if (!(bossDomainEvent is BossPhaseChanged bossPhaseChanged)) { if (bossDomainEvent is BossDied) { Phase = EncounterPhase.Defeated; } } else if (bossPhaseChanged.Phase == BossPhase.Two) { _arena.ActivateMechanismGroup(_phaseTwoGroup); } } } } public enum EncounterPhase { PreFight, Fighting, Defeated, Exiting } } namespace FalseGods.Core.Bosses { public readonly struct AttackInstanceId : IEquatable { private readonly long _value; public long Value => _value; public AttackInstanceId(long value) { _value = value; } public AttackInstanceId Next() { return new AttackInstanceId(_value + 1); } public bool Equals(AttackInstanceId other) { return _value == other._value; } public override bool Equals(object? obj) { if (obj is AttackInstanceId other) { return Equals(other); } return false; } public override int GetHashCode() { long value = _value; return value.GetHashCode(); } public override string ToString() { return $"attack:{_value}"; } public static bool operator ==(AttackInstanceId left, AttackInstanceId right) { return left.Equals(right); } public static bool operator !=(AttackInstanceId left, AttackInstanceId right) { return !left.Equals(right); } } public enum BossActivity { Idle, Telegraphing, Committing, Recovering, Dead, Vanishing, Hidden, Appearing } public enum BossAttackKind { AimedProjectile = 1, AreaTelegraph } public sealed record BossDefinition { public int MaxHealth { get; } public float PhaseTwoHealthFraction { get; } public float MoveSpeed { get; } public float IdleSeconds { get; } public float TelegraphSeconds { get; } public float CommitSeconds { get; } public float RecoverSeconds { get; } public int WeakPointDamageMultiplier { get; } public int AttackDamage { get; } public float AimedHitRadius { get; } public float AreaHitRadius { get; } public int PhaseTwoHealthThreshold => (int)Math.Floor((float)MaxHealth * PhaseTwoHealthFraction); public IReadOnlyList Stations { get; } public bool HasItinerary => Stations.Count > 0; public float VanishSeconds { get; } public float HiddenSeconds { get; } public float AppearSeconds { get; } public float RelocationSeconds => VanishSeconds + HiddenSeconds + AppearSeconds; public float OpeningSeconds { get; } public bool AttacksOnItsOwn { get; } public float RageDamageMultiplier { get; } public float RageEndsAfterHealthFraction { get; } private const float DefaultVanishSeconds = 0.45f; private const float DefaultHiddenSeconds = 0.6f; private const float DefaultAppearSeconds = 0.6f; public BossDefinition(int maxHealth, float phaseTwoHealthFraction, float moveSpeed, float idleSeconds, float telegraphSeconds, float commitSeconds, float recoverSeconds, int weakPointDamageMultiplier, int attackDamage, float aimedHitRadius, float areaHitRadius, IReadOnlyList? stations = null, float vanishSeconds = 0.45f, float hiddenSeconds = 0.6f, float appearSeconds = 0.6f, float openingSeconds = 0f, bool attacksOnItsOwn = true, float rageDamageMultiplier = 1f, float rageEndsAfterHealthFraction = 0f) { if (maxHealth <= 0) { throw new ArgumentOutOfRangeException("maxHealth", maxHealth, "Max health must be positive."); } if (phaseTwoHealthFraction <= 0f || phaseTwoHealthFraction >= 1f) { throw new ArgumentOutOfRangeException("phaseTwoHealthFraction", phaseTwoHealthFraction, "Phase-two health fraction must be strictly between 0 and 1."); } if (moveSpeed < 0f) { throw new ArgumentOutOfRangeException("moveSpeed", moveSpeed, "Move speed cannot be negative."); } RequireNonNegative(idleSeconds, "idleSeconds"); RequirePositive(telegraphSeconds, "telegraphSeconds"); RequirePositive(commitSeconds, "commitSeconds"); RequirePositive(recoverSeconds, "recoverSeconds"); if (weakPointDamageMultiplier < 1) { throw new ArgumentOutOfRangeException("weakPointDamageMultiplier", weakPointDamageMultiplier, "Weak-point damage multiplier must be at least 1."); } if (attackDamage <= 0) { throw new ArgumentOutOfRangeException("attackDamage", attackDamage, "Attack damage must be positive."); } RequirePositive(aimedHitRadius, "aimedHitRadius"); RequirePositive(areaHitRadius, "areaHitRadius"); RequirePositive(vanishSeconds, "vanishSeconds"); RequirePositive(hiddenSeconds, "hiddenSeconds"); RequirePositive(appearSeconds, "appearSeconds"); RequireNonNegative(openingSeconds, "openingSeconds"); if (rageDamageMultiplier < 1f || float.IsNaN(rageDamageMultiplier)) { throw new ArgumentOutOfRangeException("rageDamageMultiplier", rageDamageMultiplier, "A rage cannot make the boss take less damage than usual; use 1 for no amplification."); } if (rageEndsAfterHealthFraction < 0f || rageEndsAfterHealthFraction >= 1f || float.IsNaN(rageEndsAfterHealthFraction)) { throw new ArgumentOutOfRangeException("rageEndsAfterHealthFraction", rageEndsAfterHealthFraction, "A rage that ends only after the boss's whole health bar is not an end; use a fraction below 1, or 0 for a rage that is never cut short."); } Stations = ValidateItinerary(stations, moveSpeed); VanishSeconds = vanishSeconds; HiddenSeconds = hiddenSeconds; AppearSeconds = appearSeconds; OpeningSeconds = openingSeconds; AttacksOnItsOwn = attacksOnItsOwn; RageDamageMultiplier = rageDamageMultiplier; RageEndsAfterHealthFraction = rageEndsAfterHealthFraction; MaxHealth = maxHealth; PhaseTwoHealthFraction = phaseTwoHealthFraction; MoveSpeed = moveSpeed; IdleSeconds = idleSeconds; TelegraphSeconds = telegraphSeconds; CommitSeconds = commitSeconds; RecoverSeconds = recoverSeconds; WeakPointDamageMultiplier = weakPointDamageMultiplier; AttackDamage = attackDamage; AimedHitRadius = aimedHitRadius; AreaHitRadius = areaHitRadius; } private static IReadOnlyList ValidateItinerary(IReadOnlyList? stations, float moveSpeed) { if (stations == null || stations.Count == 0) { return Array.Empty(); } if (moveSpeed > 0f) { throw new ArgumentOutOfRangeException("moveSpeed", moveSpeed, "A boss with an itinerary stands where its stations put it, so it must not also move: give it a move speed of zero, or no stations."); } if (stations[0].EnterAtHealthFraction < 1f) { throw new ArgumentException("The first station is where the boss starts, so it is entered at full health (fraction 1).", "stations"); } for (int i = 1; i < stations.Count; i++) { if (stations[i].EnterAtHealthFraction >= stations[i - 1].EnterAtHealthFraction) { throw new ArgumentException($"Station {i} is entered at {stations[i].EnterAtHealthFraction:P0} health, which is not " + $"below station {i - 1}'s {stations[i - 1].EnterAtHealthFraction:P0}; an itinerary must " + "descend strictly.", "stations"); } } BossStation[] array = new BossStation[stations.Count]; for (int j = 0; j < stations.Count; j++) { array[j] = stations[j]; } return array; } private static void RequireNonNegative(float value, string name) { if (value < 0f || float.IsNaN(value)) { throw new ArgumentOutOfRangeException(name, value, "Duration cannot be negative or NaN."); } } private static void RequirePositive(float value, string name) { if (value <= 0f || float.IsNaN(value)) { throw new ArgumentOutOfRangeException(name, value, "Duration must be positive."); } } [CompilerGenerated] private BossDefinition(BossDefinition original) { MaxHealth = original.MaxHealth; PhaseTwoHealthFraction = original.PhaseTwoHealthFraction; MoveSpeed = original.MoveSpeed; IdleSeconds = original.IdleSeconds; TelegraphSeconds = original.TelegraphSeconds; CommitSeconds = original.CommitSeconds; RecoverSeconds = original.RecoverSeconds; WeakPointDamageMultiplier = original.WeakPointDamageMultiplier; AttackDamage = original.AttackDamage; AimedHitRadius = original.AimedHitRadius; AreaHitRadius = original.AreaHitRadius; Stations = original.Stations; VanishSeconds = original.VanishSeconds; HiddenSeconds = original.HiddenSeconds; AppearSeconds = original.AppearSeconds; OpeningSeconds = original.OpeningSeconds; AttacksOnItsOwn = original.AttacksOnItsOwn; RageDamageMultiplier = original.RageDamageMultiplier; RageEndsAfterHealthFraction = original.RageEndsAfterHealthFraction; } } public readonly struct BossInstanceId : IEquatable { private readonly int _value; public int Value => _value; public BossInstanceId(int value) { _value = value; } public bool Equals(BossInstanceId other) { return _value == other._value; } public override bool Equals(object? obj) { if (obj is BossInstanceId other) { return Equals(other); } return false; } public override int GetHashCode() { return _value; } public override string ToString() { return $"boss:{_value}"; } public static bool operator ==(BossInstanceId left, BossInstanceId right) { return left.Equals(right); } public static bool operator !=(BossInstanceId left, BossInstanceId right) { return !left.Equals(right); } } public enum BossPhase { One = 1, Two } public sealed class BossSimulation { private readonly BossDefinition _definition; private readonly ISimulationClock _clock; private readonly IAuthoritativeRandom _random; private readonly IEncounterParticipantQuery _participants; private readonly List _events = new List(); private readonly List _damageRequests = new List(); private readonly List _summonRequests = new List(); private readonly IReadOnlyList _anchors; private bool _spawned; private float _beganTime; private int _healthWhenEnraged; private int _pendingStation = -1; private float _activityEnteredTime; private float _lastAdvanceTime; private AttackInstanceId _lastAttackId; public BossInstanceId Id { get; } public bool IsSpawned => _spawned; public bool HasBegun { get; private set; } private float OpeningEndsAt => _beganTime + _definition.OpeningSeconds; public bool IsOpening { get { if (HasBegun) { return _clock.Time < OpeningEndsAt; } return false; } } public bool IsOutsideTheFight { get { if (HasBegun) { return IsOpening; } return true; } } public int Health { get; private set; } public int MaxHealth => _definition.MaxHealth; public float HealthFraction { get { if (_definition.MaxHealth <= 0) { return 0f; } return (float)Health / (float)_definition.MaxHealth; } } public BossPhase Phase { get; private set; } public BossActivity Activity { get; private set; } public bool IsDead => Activity == BossActivity.Dead; public bool IsWeakPointExposed => Activity == BossActivity.Recovering; public bool IsEnraged { get; private set; } public SimVector2 Position { get; private set; } public float PositionHeight { get; private set; } public int StationIndex { get; private set; } public int PendingStationIndex => _pendingStation; public bool IsRelocating { get { if (Activity != BossActivity.Vanishing && Activity != BossActivity.Hidden) { return Activity == BossActivity.Appearing; } return true; } } public SimVector2 Facing { get; private set; } public AttackInstanceId? CurrentAttack { get; private set; } public BossAttackKind CurrentAttackKind { get; private set; } public SimVector2 CurrentAttackAimPoint { get; private set; } public BossSimulation(BossInstanceId id, BossDefinition definition, ISimulationClock clock, IAuthoritativeRandom random, IEncounterParticipantQuery participants, IReadOnlyList? anchors = null) { Id = id; _definition = definition ?? throw new ArgumentNullException("definition"); _clock = clock ?? throw new ArgumentNullException("clock"); _random = random ?? throw new ArgumentNullException("random"); _participants = participants ?? throw new ArgumentNullException("participants"); _anchors = anchors ?? Array.Empty(); _lastAttackId = new AttackInstanceId(0L); Activity = BossActivity.Idle; Phase = BossPhase.One; StationIndex = -1; } public void Spawn(SimVector2 startPosition, float startHeight = 0f, bool waitToBegin = false) { if (!_spawned) { _spawned = true; Health = _definition.MaxHealth; Phase = BossPhase.One; Activity = BossActivity.Idle; Position = startPosition; PositionHeight = startHeight; Facing = SimVector2.Zero; StationIndex = -1; if (TryEnterStation(0)) { StationIndex = 0; } _activityEnteredTime = _clock.Time; _lastAdvanceTime = _clock.Time; _events.Add(new BossSpawned(Id, Phase, Health)); if (!waitToBegin) { Begin(); } } } public bool Begin() { if (!_spawned || IsDead || HasBegun) { return false; } HasBegun = true; _beganTime = _clock.Time; _activityEnteredTime = _beganTime; _lastAdvanceTime = _beganTime; _events.Add(new BossBegan(Id)); return true; } public void Advance() { if (!_spawned || IsDead) { return; } if (!HasBegun || _clock.Time < OpeningEndsAt) { _lastAdvanceTime = _clock.Time; _activityEnteredTime = _clock.Time; return; } if (_activityEnteredTime < OpeningEndsAt) { _activityEnteredTime = OpeningEndsAt; if (_lastAdvanceTime < OpeningEndsAt) { _lastAdvanceTime = OpeningEndsAt; } } float time = _clock.Time; float num = Math.Max(0f, time - _lastAdvanceTime); _lastAdvanceTime = time; ParticipantId nearest; SimVector2 position; bool flag = TryGetNearestTarget(out nearest, out position); if (flag && !IsRelocating) { Facing = Position.DirectionTo(position); } if (Activity == BossActivity.Idle && flag && _definition.MoveSpeed > 0f) { Position = Position.MoveToward(position, _definition.MoveSpeed * num); } float num2 = Math.Max(0f, time - _activityEnteredTime); switch (Activity) { case BossActivity.Vanishing: if (num2 >= _definition.VanishSeconds) { EnterHidden(time); } break; case BossActivity.Hidden: if (num2 >= _definition.HiddenSeconds) { EnterAppearing(time); } break; case BossActivity.Appearing: if (num2 >= _definition.AppearSeconds) { FinishRelocation(time); } break; case BossActivity.Idle: if (!flag || !_definition.AttacksOnItsOwn) { _activityEnteredTime = time; } else if (num2 >= _definition.IdleSeconds) { SelectAttack(time, position); } break; case BossActivity.Telegraphing: if (num2 >= _definition.TelegraphSeconds) { CommitAttack(time); } break; case BossActivity.Committing: if (num2 >= _definition.CommitSeconds) { BeginRecovery(time); } break; case BossActivity.Recovering: if (num2 >= _definition.RecoverSeconds) { EndRecovery(time); } break; case BossActivity.Dead: break; } } public void ApplyDamage(int rawAmount) { if (!_spawned || IsDead || rawAmount <= 0 || IsRelocating || IsOutsideTheFight) { return; } bool isWeakPointExposed = IsWeakPointExposed; int num = rawAmount * ((!isWeakPointExposed) ? 1 : _definition.WeakPointDamageMultiplier); if (IsEnraged) { num = (int)Math.Round((double)num * (double)_definition.RageDamageMultiplier, MidpointRounding.AwayFromZero); } Health = Math.Max(0, Health - num); _events.Add(new BossDamaged(Id, num, Health, isWeakPointExposed)); if (Health == 0) { Activity = BossActivity.Dead; CurrentAttack = null; Facing = SimVector2.Zero; IsEnraged = false; _events.Add(new BossDied(Id)); return; } SpendTheRage(); AdvanceStations(); if (Phase == BossPhase.One && Health <= _definition.PhaseTwoHealthThreshold) { Phase = BossPhase.Two; _events.Add(new BossPhaseChanged(Id, Phase)); } } public bool SetEnraged(bool enraged) { if (!_spawned || IsDead || enraged == IsEnraged) { return false; } IsEnraged = enraged; if (enraged) { _healthWhenEnraged = Health; } _events.Add(new BossEnraged(Id, enraged)); return true; } private void SpendTheRage() { if (IsEnraged && !(_definition.RageEndsAfterHealthFraction <= 0f) && !((float)(_healthWhenEnraged - Health) < (float)_definition.MaxHealth * _definition.RageEndsAfterHealthFraction)) { SetEnraged(enraged: false); } } public IReadOnlyList DrainEvents() { if (_events.Count == 0) { return Array.Empty(); } IBossDomainEvent[] result = _events.ToArray(); _events.Clear(); return result; } public IReadOnlyList DrainDamageRequests() { if (_damageRequests.Count == 0) { return Array.Empty(); } DamageRequest[] result = _damageRequests.ToArray(); _damageRequests.Clear(); return result; } private void AdvanceStations() { IReadOnlyList stations = _definition.Stations; if (StationIndex >= 0 && !IsRelocating && _pendingStation < 0) { int num = StationIndex + 1; if (num < stations.Count && !(HealthFraction > stations[num].EnterAtHealthFraction)) { _pendingStation = num; BeginRelocation(_clock.Time); } } } private void BeginRelocation(float now) { if (Activity == BossActivity.Recovering) { _events.Add(new WeakPointExposed(Id, Exposed: false)); } CurrentAttack = null; Facing = SimVector2.Zero; Activity = BossActivity.Vanishing; _activityEnteredTime = now; } private void EnterHidden(float now) { Activity = BossActivity.Hidden; _activityEnteredTime = now; int pendingStation = _pendingStation; _pendingStation = -1; if (pendingStation >= 0 && TryEnterStation(pendingStation)) { StationIndex = pendingStation; _events.Add(new BossRelocated(Id, pendingStation, _definition.Stations[pendingStation].AnchorIndex, Position, PositionHeight)); } } private void EnterAppearing(float now) { Activity = BossActivity.Appearing; _activityEnteredTime = now; IReadOnlyList stations = _definition.Stations; if (StationIndex >= 0 && StationIndex < stations.Count && stations[StationIndex].Summons.NamesABand) { _summonRequests.Add(new SummonRequest(StationIndex, stations[StationIndex].Summons)); } } private void FinishRelocation(float now) { Activity = BossActivity.Idle; _activityEnteredTime = now; AdvanceStations(); } private bool TryEnterStation(int stationIndex) { IReadOnlyList stations = _definition.Stations; if (stationIndex < 0 || stationIndex >= stations.Count) { return false; } int anchorIndex = stations[stationIndex].AnchorIndex; if (anchorIndex < 0 || anchorIndex >= _anchors.Count) { return false; } BossAnchor bossAnchor = _anchors[anchorIndex]; Position = bossAnchor.Ground; PositionHeight = bossAnchor.Height; return true; } public IReadOnlyList DrainSummonRequests() { if (_summonRequests.Count == 0) { return Array.Empty(); } SummonRequest[] result = _summonRequests.ToArray(); _summonRequests.Clear(); return result; } private void SelectAttack(float now, SimVector2 targetPosition) { BossAttackKind bossAttackKind = ((_random.NextInt(0, 2) == 0) ? BossAttackKind.AimedProjectile : BossAttackKind.AreaTelegraph); _lastAttackId = _lastAttackId.Next(); CurrentAttack = _lastAttackId; CurrentAttackKind = bossAttackKind; CurrentAttackAimPoint = targetPosition; Activity = BossActivity.Telegraphing; _activityEnteredTime = now; _events.Add(new AttackTelegraphed(Id, _lastAttackId, bossAttackKind, targetPosition, _definition.TelegraphSeconds)); } private void CommitAttack(float now) { Activity = BossActivity.Committing; _activityEnteredTime = now; _events.Add(new AttackCommitted(Id, _lastAttackId, CurrentAttackKind, CurrentAttackAimPoint)); ResolveAttackHits(); } private void ResolveAttackHits() { float num = ((CurrentAttackKind == BossAttackKind.AimedProjectile) ? _definition.AimedHitRadius : _definition.AreaHitRadius); IReadOnlyList participants = _participants.Participants; for (int i = 0; i < participants.Count; i++) { ParticipantId participantId = participants[i]; if (_participants.TryGetPosition(participantId, out var position) && CurrentAttackAimPoint.DistanceTo(position) <= num) { _damageRequests.Add(new DamageRequest(participantId, _definition.AttackDamage, _lastAttackId)); } } } private void BeginRecovery(float now) { CurrentAttack = null; Activity = BossActivity.Recovering; _activityEnteredTime = now; _events.Add(new WeakPointExposed(Id, Exposed: true)); } private void EndRecovery(float now) { Activity = BossActivity.Idle; _activityEnteredTime = now; _events.Add(new WeakPointExposed(Id, Exposed: false)); } private bool TryGetNearestTarget(out ParticipantId nearest, out SimVector2 position) { nearest = default(ParticipantId); position = default(SimVector2); bool flag = false; float num = float.MaxValue; IReadOnlyList participants = _participants.Participants; for (int i = 0; i < participants.Count; i++) { ParticipantId participantId = participants[i]; if (_participants.TryGetPosition(participantId, out var position2)) { float num2 = Position.DistanceTo(position2); if (!flag || num2 < num) { flag = true; num = num2; nearest = participantId; position = position2; } } } return flag; } } public readonly struct BossAnchor : IEquatable { public SimVector2 Ground { get; } public float Height { get; } public BossAnchor(SimVector2 ground, float height) { Ground = ground; Height = height; } public bool Equals(BossAnchor other) { if (Ground.Equals(other.Ground)) { return Height.Equals(other.Height); } return false; } public override bool Equals(object? obj) { if (obj is BossAnchor other) { return Equals(other); } return false; } public override int GetHashCode() { return (Ground.GetHashCode() * 397) ^ Height.GetHashCode(); } public override string ToString() { return $"({Ground.X:0.0}, {Height:0.0}, {Ground.Z:0.0})"; } } public readonly struct BossStation : IEquatable { public int AnchorIndex { get; } public float EnterAtHealthFraction { get; } public MinionBandId Summons { get; } public BossStation(int anchorIndex, float enterAtHealthFraction, MinionBandId summons = default(MinionBandId)) { if (anchorIndex < 0) { throw new ArgumentOutOfRangeException("anchorIndex", anchorIndex, "An anchor index cannot be negative."); } if (enterAtHealthFraction <= 0f || enterAtHealthFraction > 1f) { throw new ArgumentOutOfRangeException("enterAtHealthFraction", enterAtHealthFraction, "A station is entered at a health fraction in (0, 1] — a station entered at zero health would be entered by a dead boss."); } AnchorIndex = anchorIndex; EnterAtHealthFraction = enterAtHealthFraction; Summons = summons; } public bool Equals(BossStation other) { if (AnchorIndex == other.AnchorIndex && EnterAtHealthFraction.Equals(other.EnterAtHealthFraction)) { return Summons.Equals(other.Summons); } return false; } public override bool Equals(object? obj) { if (obj is BossStation other) { return Equals(other); } return false; } public override int GetHashCode() { return (AnchorIndex * 397) ^ EnterAtHealthFraction.GetHashCode(); } public override string ToString() { if (!Summons.NamesABand) { return $"anchor {AnchorIndex} at <= {EnterAtHealthFraction:P0} health"; } return $"anchor {AnchorIndex} at <= {EnterAtHealthFraction:P0} health, summoning the {Summons} band"; } } } namespace FalseGods.Core.Bosses.Events { public sealed record BossSpawned(BossInstanceId Boss, BossPhase Phase, int Health) : IBossDomainEvent; public sealed record BossBegan(BossInstanceId Boss) : IBossDomainEvent; public sealed record AttackTelegraphed(BossInstanceId Boss, AttackInstanceId Attack, BossAttackKind Kind, SimVector2 AimPoint, float TelegraphSeconds) : IBossDomainEvent; public sealed record AttackCommitted(BossInstanceId Boss, AttackInstanceId Attack, BossAttackKind Kind, SimVector2 AimPoint) : IBossDomainEvent; public sealed record WeakPointExposed(BossInstanceId Boss, bool Exposed) : IBossDomainEvent; public sealed record BossPhaseChanged(BossInstanceId Boss, BossPhase Phase) : IBossDomainEvent; public sealed record BossDamaged(BossInstanceId Boss, int Amount, int RemainingHealth, bool WeakPointHit) : IBossDomainEvent; public sealed record BossRelocated(BossInstanceId Boss, int StationIndex, int AnchorIndex, SimVector2 Position, float Height) : IBossDomainEvent; public sealed record BossEnraged(BossInstanceId Boss, bool Enraged) : IBossDomainEvent; public sealed record BossDied(BossInstanceId Boss) : IBossDomainEvent; public interface IBossDomainEvent { BossInstanceId Boss { get; } } } namespace FalseGods.Core.Bosses.Combat { public static class BallisticArc { public static float HorizontalFraction(float normalizedTime) { return Clamp01(normalizedTime); } public static float Height(float normalizedTime, float apexHeight) { float num = Clamp01(normalizedTime); return 4f * apexHeight * num * (1f - num); } private static float Clamp01(float value) { if (value < 0f) { return 0f; } if (!(value > 1f)) { return value; } return 1f; } } public enum CrateDeath { Shot, Struck } public enum CratePileKind { Loose, Source, Delivery } public readonly struct CratePileId : IEquatable { public static CratePileId Loose => new CratePileId(CratePileKind.Loose, 0); public CratePileKind Kind { get; } public int Index { get; } private CratePileId(CratePileKind kind, int index) { Kind = kind; Index = index; } public static CratePileId Source(int index) { return Of(CratePileKind.Source, index); } public static CratePileId Delivery(int index) { return Of(CratePileKind.Delivery, index); } public static bool TryFrom(int kind, int index, out CratePileId pile) { pile = Loose; if (index < 0) { return false; } switch ((CratePileKind)kind) { case CratePileKind.Loose: return true; case CratePileKind.Source: case CratePileKind.Delivery: pile = new CratePileId((CratePileKind)kind, index); return true; default: return false; } } private static CratePileId Of(CratePileKind kind, int index) { if (index < 0) { throw new ArgumentOutOfRangeException("index", index, "A pile index is a position in the room's authored group, never negative."); } return new CratePileId(kind, index); } public bool Equals(CratePileId other) { if (Kind == other.Kind) { return Index == other.Index; } return false; } public override bool Equals(object? obj) { if (obj is CratePileId other) { return Equals(other); } return false; } public override int GetHashCode() { return ((int)Kind * 397) ^ Index; } public static bool operator ==(CratePileId left, CratePileId right) { return left.Equals(right); } public static bool operator !=(CratePileId left, CratePileId right) { return !left.Equals(right); } public override string ToString() { if (Kind != CratePileKind.Loose) { return $"{Kind.ToString().ToLowerInvariant()} #{Index}"; } return "loose"; } } public sealed record DamageRequest(ParticipantId Target, int Amount, AttackInstanceId Attack); public static class LeadAim { public static SimVector2 Predict(SimVector2 position, SimVector2 velocity, float seconds) { return new SimVector2(position.X + velocity.X * seconds, position.Z + velocity.Z * seconds); } } public readonly struct MinionBandId : IEquatable { private readonly string _value; public string Value => _value ?? string.Empty; public bool NamesABand => !string.IsNullOrEmpty(_value); public MinionBandId(string value) { if (string.IsNullOrEmpty(value)) { throw new ArgumentException("A minion band id must be a non-empty string.", "value"); } _value = value; } public bool Equals(MinionBandId other) { return string.Equals(Value, other.Value, StringComparison.Ordinal); } public override bool Equals(object? obj) { if (obj is MinionBandId other) { return Equals(other); } return false; } public override int GetHashCode() { return StringComparer.Ordinal.GetHashCode(Value); } public override string ToString() { if (!NamesABand) { return "(no band)"; } return Value; } public static bool operator ==(MinionBandId left, MinionBandId right) { return left.Equals(right); } public static bool operator !=(MinionBandId left, MinionBandId right) { return !left.Equals(right); } } public static class MinionBands { public static readonly MinionBandId Vanguard = new MinionBandId("vanguard"); public static readonly MinionBandId Warband = new MinionBandId("warband"); public static readonly MinionBandId Coven = new MinionBandId("coven"); public static readonly MinionBandId Emergency = new MinionBandId("emergency"); } public static class SeededRandom { public static double Unit01(int seed, int salt) { int num = seed * -1640531535 + salt * -2048144777 + 374761393; int num2 = (num ^ (num >>> 15)) * -2048144777; int num3 = (num2 ^ (num2 >>> 13)) * -1028477379; return (double)(uint)(num3 ^ (num3 >>> 16)) / 4294967296.0; } public static float Range(int seed, int salt, float min, float max) { return min + (float)Unit01(seed, salt) * (max - min); } } public readonly struct SpreadOffset { public float X { get; } public float Z { get; } public SpreadOffset(float x, float z) { X = x; Z = z; } } public static class ShotgunSpread { public static SpreadOffset Offset(int seed, int index, int count, float minRadius, float maxRadius) { if (count < 1) { count = 1; } if (index < 0) { index = 0; } double num = Math.PI * 2.0 / (double)count; double num2 = SeededRandom.Unit01(seed, 0) * 2.0 * Math.PI; double num3 = (SeededRandom.Unit01(seed, index * 2 + 1) - 0.5) * (num * 0.5); double num4 = num2 + num * (double)index + num3; float num5 = SeededRandom.Range(seed, index * 2 + 2, minRadius, maxRadius); return new SpreadOffset((float)(Math.Cos(num4) * (double)num5), (float)(Math.Sin(num4) * (double)num5)); } } public sealed class StarvationWatch { private float _sinceDelivery; private float _sinceHit; public bool Enraged { get; private set; } public float SinceDelivery => _sinceDelivery; public float SinceHit => _sinceHit; public StarvationReason Reason { get; private set; } public StarvationChange Advance(float deltaSeconds, bool deliveryArrived, float patienceSeconds, int emergencyBandAlive, bool hitSomebody = false, float futilitySeconds = 0f) { float num = ((deltaSeconds > 0f) ? deltaSeconds : 0f); _sinceDelivery = (deliveryArrived ? 0f : (_sinceDelivery + num)); _sinceHit = (hitSomebody ? 0f : (_sinceHit + num)); bool flag = _sinceDelivery < patienceSeconds; bool flag2 = futilitySeconds <= 0f || _sinceHit < futilitySeconds; if (!Enraged) { if (flag && flag2) { return StarvationChange.Nothing; } Enraged = true; Reason = (flag ? StarvationReason.NothingLanding : StarvationReason.NothingArriving); return StarvationChange.Enraged; } if (flag && flag2 && emergencyBandAlive <= 0) { Enraged = false; return StarvationChange.Calmed; } return StarvationChange.Nothing; } public void Reset() { Enraged = false; _sinceDelivery = 0f; _sinceHit = 0f; } } public enum StarvationReason { NothingArriving, NothingLanding } public enum StarvationChange { Nothing, Enraged, Calmed } public sealed record SummonRequest(int StationIndex, MinionBandId Band); public readonly struct SupplyStep { public float AboveHealthFraction { get; } public int Carriers { get; } public int LoadPerCarrier { get; } public float ExplosiveChance { get; } public int Throughput => Carriers * LoadPerCarrier; public SupplyStep(float aboveHealthFraction, int carriers, int loadPerCarrier, float explosiveChance = 0f) { if (explosiveChance < 0f || explosiveChance > 1f || float.IsNaN(explosiveChance)) { throw new ArgumentOutOfRangeException("explosiveChance", explosiveChance, "A chance lies in [0, 1]."); } if (aboveHealthFraction < 0f || aboveHealthFraction > 1f) { throw new ArgumentOutOfRangeException("aboveHealthFraction", aboveHealthFraction, "A health fraction lies in [0, 1]."); } if (carriers < 0) { throw new ArgumentOutOfRangeException("carriers", carriers, "A step cannot put a negative number of carriers on the route."); } if (loadPerCarrier < 0) { throw new ArgumentOutOfRangeException("loadPerCarrier", loadPerCarrier, "A carrier cannot haul a negative load."); } AboveHealthFraction = aboveHealthFraction; Carriers = carriers; LoadPerCarrier = loadPerCarrier; ExplosiveChance = explosiveChance; } } public sealed class SupplyEscalation { private readonly SupplyStep[] _steps; public int Count => _steps.Length; public SupplyEscalation(params SupplyStep[] steps) { _steps = steps ?? Array.Empty(); for (int i = 1; i < _steps.Length; i++) { if (_steps[i].AboveHealthFraction > _steps[i - 1].AboveHealthFraction) { throw new ArgumentException("Supply steps are read in order as the boss loses health, so their thresholds must " + $"descend; step {i} sits above step {i - 1}.", "steps"); } } } public SupplyStep At(float healthFraction) { if (_steps.Length == 0) { return new SupplyStep(0f, 0, 0); } for (int i = 0; i < _steps.Length; i++) { if (healthFraction > _steps[i].AboveHealthFraction) { return _steps[i]; } } return _steps[_steps.Length - 1]; } public static float RatePerSecond(SupplyStep step, float roundTripSeconds) { if (!(roundTripSeconds > 0f)) { return 0f; } return (float)step.Throughput / roundTripSeconds; } } public readonly struct SupplyLineShape { public float SecondsPerCrate { get; } public int SourceCapacity { get; } public int DeliveryCapacity { get; } public SupplyLineShape(float secondsPerCrate, int sourceCapacity, int deliveryCapacity) { if (!(secondsPerCrate > 0f)) { throw new ArgumentOutOfRangeException("secondsPerCrate", secondsPerCrate, "A production interval must be a positive number of seconds; zero would produce a crate every frame."); } if (sourceCapacity < 0) { throw new ArgumentOutOfRangeException("sourceCapacity", sourceCapacity, "A capacity cannot be negative."); } if (deliveryCapacity < 0) { throw new ArgumentOutOfRangeException("deliveryCapacity", deliveryCapacity, "A capacity cannot be negative."); } SecondsPerCrate = secondsPerCrate; SourceCapacity = sourceCapacity; DeliveryCapacity = deliveryCapacity; } } public sealed class SupplyLine { private readonly SupplyLineShape _shape; private readonly float[] _sinceProduced; private readonly List _due = new List(); public int SourceCount => _sinceProduced.Length; public SupplyLine(SupplyLineShape shape, int sourceCount) { if (sourceCount < 0) { throw new ArgumentOutOfRangeException("sourceCount", sourceCount, "A room cannot have a negative number of production points."); } _shape = shape; _sinceProduced = new float[sourceCount]; } public void Advance(float deltaSeconds, IReadOnlyList restingAtSource) { _due.Clear(); if (deltaSeconds <= 0f) { return; } for (int i = 0; i < _sinceProduced.Length; i++) { if (((restingAtSource != null && i < restingAtSource.Count) ? restingAtSource[i] : 0) >= _shape.SourceCapacity) { _sinceProduced[i] = 0f; continue; } _sinceProduced[i] += deltaSeconds; if (!(_sinceProduced[i] < _shape.SecondsPerCrate)) { _sinceProduced[i] = 0f; _due.Add(i); } } } public IReadOnlyList DrainProductionRequests() { return _due; } public bool AcceptsDelivery(int resting) { return resting < _shape.DeliveryCapacity; } } public sealed class TargetMotionTracker { private readonly float _smoothingSeconds; private SimVector2 _smoothed = SimVector2.Zero; private bool _hasSample; public SimVector2 SmoothedVelocity { get { if (!_hasSample) { return SimVector2.Zero; } return _smoothed; } } public TargetMotionTracker(float smoothingSeconds) { _smoothingSeconds = smoothingSeconds; } public void Observe(SimVector2 velocity, float deltaSeconds) { if (!_hasSample || _smoothingSeconds <= 0f || deltaSeconds <= 0f) { _smoothed = velocity; _hasSample = true; return; } float num = deltaSeconds / _smoothingSeconds; if (num > 1f) { num = 1f; } _smoothed = new SimVector2(_smoothed.X + (velocity.X - _smoothed.X) * num, _smoothed.Z + (velocity.Z - _smoothed.Z) * num); } public void Reset() { _smoothed = SimVector2.Zero; _hasSample = false; } } } namespace FalseGods.Core.Arena { public sealed class ArenaSimulation { private readonly HashSet _activeGroups = new HashSet(); private readonly List _events = new List(); public bool IsExitUnlocked { get; private set; } public IReadOnlyCollection ActiveMechanismGroups => _activeGroups; public bool IsMechanismGroupActive(MechanismGroupId group) { return _activeGroups.Contains(group); } public void ActivateMechanismGroup(MechanismGroupId group) { if (_activeGroups.Add(group)) { _events.Add(new MechanismGroupActivated(group)); } } public void UnlockExit() { if (!IsExitUnlocked) { IsExitUnlocked = true; _events.Add(new ArenaExitUnlocked()); } } public IReadOnlyList DrainEvents() { if (_events.Count == 0) { return Array.Empty(); } IArenaDomainEvent[] result = _events.ToArray(); _events.Clear(); return result; } } public readonly struct MechanismGroupId : IEquatable { private readonly string _value; public string Value => _value ?? string.Empty; public MechanismGroupId(string value) { if (string.IsNullOrEmpty(value)) { throw new ArgumentException("A mechanism group id must be a non-empty string.", "value"); } _value = value; } public bool Equals(MechanismGroupId other) { return string.Equals(Value, other.Value, StringComparison.Ordinal); } public override bool Equals(object? obj) { if (obj is MechanismGroupId other) { return Equals(other); } return false; } public override int GetHashCode() { return StringComparer.Ordinal.GetHashCode(Value); } public override string ToString() { return Value; } public static bool operator ==(MechanismGroupId left, MechanismGroupId right) { return left.Equals(right); } public static bool operator !=(MechanismGroupId left, MechanismGroupId right) { return !left.Equals(right); } } } namespace FalseGods.Core.Arena.Events { public sealed record MechanismGroupActivated(MechanismGroupId Group) : IArenaDomainEvent; public sealed record ArenaExitUnlocked : IArenaDomainEvent; public interface IArenaDomainEvent { } }