diff --git a/Content.Server/Imperial/SCP/SCP008/Components/SCP008InfectionAuraComponent.cs b/Content.Server/Imperial/SCP/SCP008/Components/SCP008InfectionAuraComponent.cs new file mode 100644 index 00000000000..78709b3da5f --- /dev/null +++ b/Content.Server/Imperial/SCP/SCP008/Components/SCP008InfectionAuraComponent.cs @@ -0,0 +1,37 @@ +// usings moved to file-level globals; removed unused using + +namespace Content.Server.Imperial.SCP.SCP008.Components; + +[RegisterComponent] +public sealed partial class SCP008InfectionAuraComponent : Component +{ + [DataField("radius")] + public float Radius = 8f; + + [DataField("zombifyDelay")] + public TimeSpan ZombifyDelay = TimeSpan.FromSeconds(12); + + [DataField("updateInterval")] + public TimeSpan UpdateInterval = TimeSpan.FromSeconds(1); + + [DataField("lookupFlags")] + public LookupFlags LookupFlags = LookupFlags.Dynamic; + + [DataField("allowCritical")] + public bool AllowCritical = false; + + [DataField("warningDelay")] + public TimeSpan WarningDelay = TimeSpan.FromSeconds(3); + + [DataField("warningPopup")] + public LocId WarningPopup = "scp008-warning-popup"; + + [ViewVariables] + public TimeSpan NextUpdate; + + [ViewVariables] + public TimeSpan LastUpdate; + + [ViewVariables] + public Dictionary ExposureTime = new(); +} diff --git a/Content.Server/Imperial/SCP/SCP008/Systems/SCP008InfectionAuraSystem.cs b/Content.Server/Imperial/SCP/SCP008/Systems/SCP008InfectionAuraSystem.cs new file mode 100644 index 00000000000..bf58f88931a --- /dev/null +++ b/Content.Server/Imperial/SCP/SCP008/Systems/SCP008InfectionAuraSystem.cs @@ -0,0 +1,119 @@ +using Content.Server.Imperial.SCP.SCP008.Components; +using Content.Server.Zombies; +using Content.Shared.Mobs; +using Content.Shared.Mobs.Components; +using Content.Shared.Popups; +using Content.Shared.Zombies; +using Robust.Shared.Timing; +using Robust.Shared.Localization; + +namespace Content.Server.Imperial.SCP.SCP008.Systems; + +public sealed class SCP008InfectionAuraSystem : EntitySystem +{ + private readonly HashSet _reusableInRange = new(); + [Dependency] private readonly EntityLookupSystem _lookup = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly ZombieSystem _zombie = default!; + [Dependency] private readonly IGameTiming _timing = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnMapInit); + SubscribeLocalEvent(OnShutdown); + } + + private void OnMapInit(Entity ent, ref MapInitEvent args) + { + ent.Comp.LastUpdate = _timing.CurTime; + ent.Comp.NextUpdate = _timing.CurTime + ent.Comp.UpdateInterval; + ent.Comp.ExposureTime.Clear(); + } + + private void OnShutdown(Entity ent, ref ComponentShutdown args) + { + ent.Comp.ExposureTime.Clear(); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var curTime = _timing.CurTime; + var query = EntityQueryEnumerator(); + + while (query.MoveNext(out var uid, out var comp)) + { + if (comp.NextUpdate > curTime) + continue; + + var elapsed = curTime - comp.LastUpdate; + comp.LastUpdate = curTime; + comp.NextUpdate = curTime + comp.UpdateInterval; + + if (elapsed <= TimeSpan.Zero) + continue; + + _reusableInRange.Clear(); + var nearbyEntities = _lookup.GetEntitiesInRange(uid, comp.Radius, comp.LookupFlags); + + foreach (var target in nearbyEntities) + { + if (target == uid) + continue; + + if (!TryComp(target, out var mobState)) + continue; + + var validState = mobState.CurrentState == MobState.Alive || + (comp.AllowCritical && mobState.CurrentState == MobState.Critical); + + if (!validState) + continue; + + if (HasComp(target) || HasComp(target)) + continue; + + _reusableInRange.Add(target); + + var totalExposure = elapsed; + var previousExposure = TimeSpan.Zero; + if (comp.ExposureTime.TryGetValue(target, out var existingExposure)) + { + previousExposure = existingExposure; + totalExposure += previousExposure; + } + + var warningStart = comp.ZombifyDelay - comp.WarningDelay; + if (warningStart < TimeSpan.Zero) + warningStart = TimeSpan.Zero; + + if (previousExposure < warningStart && totalExposure >= warningStart && totalExposure < comp.ZombifyDelay) + _popup.PopupEntity(Loc.GetString(comp.WarningPopup), target, target, PopupType.MediumCaution); + + if (totalExposure >= comp.ZombifyDelay) + { + _zombie.ZombifyEntity(target, mobState); + comp.ExposureTime.Remove(target); + continue; + } + + comp.ExposureTime[target] = totalExposure; + } + + var toRemove = new List(); + foreach (var (tracked, _) in comp.ExposureTime) + { + if (!_reusableInRange.Contains(tracked)) + toRemove.Add(tracked); + } + + foreach (var tracked in toRemove) + { + comp.ExposureTime.Remove(tracked); + } + } + } +} diff --git a/Content.Server/Imperial/SCP/SCP096/Components/SCP096RageOnLookComponent.cs b/Content.Server/Imperial/SCP/SCP096/Components/SCP096RageOnLookComponent.cs new file mode 100644 index 00000000000..c69215ac669 --- /dev/null +++ b/Content.Server/Imperial/SCP/SCP096/Components/SCP096RageOnLookComponent.cs @@ -0,0 +1,73 @@ +using Robust.Shared.Audio; + +namespace Content.Server.Imperial.SCP.SCP096.Components; + +[RegisterComponent] +public sealed partial class SCP096RageOnLookComponent : Component +{ + [DataField("observeRadius")] + public float ObserveRadius = 9f; + + [DataField("minLookDot")] + public float MinLookDot = 0.42f; + + [DataField("requireUnobstructed")] + public bool RequireUnobstructed = true; + + [DataField("enragedWalkModifier")] + public float EnragedWalkModifier = 1.7f; + + [DataField("enragedSprintModifier")] + public float EnragedSprintModifier = 1.7f; + + [DataField("enragedAttackRate")] + public float EnragedAttackRate = 1.7f; + + [DataField("calmAttackRate")] + public float CalmAttackRate = 1.0f; + + [DataField("rageWindup")] + public TimeSpan RageWindup = TimeSpan.FromSeconds(10); + + [DataField("rageDuration")] + public TimeSpan RageDuration = TimeSpan.FromSeconds(120); + + [DataField("rageWindupPopup")] + public LocId RageWindupPopup = "scp096-rage-windup-popup"; + + [DataField("ragePopup")] + public LocId RagePopup = "scp096-rage-popup"; + + [DataField("rageCalmPopup")] + public LocId RageCalmPopup = "scp096-rage-calm-popup"; + + [DataField("rageSound")] + public SoundSpecifier RageSound = new SoundPathSpecifier("/Audio/Voice/Human/malescream_1.ogg"); + + [DataField("cryingSound")] + public SoundSpecifier CryingSound = new SoundPathSpecifier("/Audio/Voice/Human/cry_male_1.ogg"); + + [DataField("rageLoopSound")] + public SoundSpecifier RageLoopSound = new SoundPathSpecifier("/Audio/Ambience/Objects/anomaly_generator_ambi.ogg"); + + [ViewVariables] + public bool IsEnraged; + + [ViewVariables] + public bool IsRageWindup; + + [ViewVariables] + public TimeSpan RageWindupEndTime; + + [ViewVariables] + public TimeSpan RageEndTime; + + [ViewVariables] + public bool UsingRageLoopSound; + + [ViewVariables] + public float? OriginalAttackRate; + + [ViewVariables] + public HashSet RageTargets = new(); +} diff --git a/Content.Server/Imperial/SCP/SCP096/Components/SCP096RageSuppressorComponent.cs b/Content.Server/Imperial/SCP/SCP096/Components/SCP096RageSuppressorComponent.cs new file mode 100644 index 00000000000..e0efdea6799 --- /dev/null +++ b/Content.Server/Imperial/SCP/SCP096/Components/SCP096RageSuppressorComponent.cs @@ -0,0 +1,6 @@ +namespace Content.Server.Imperial.SCP.SCP096.Components; + +[RegisterComponent] +public sealed partial class SCP096RageSuppressorComponent : Component +{ +} \ No newline at end of file diff --git a/Content.Server/Imperial/SCP/SCP096/Systems/SCP096RageOnLookSystem.cs b/Content.Server/Imperial/SCP/SCP096/Systems/SCP096RageOnLookSystem.cs new file mode 100644 index 00000000000..ec698a1c651 --- /dev/null +++ b/Content.Server/Imperial/SCP/SCP096/Systems/SCP096RageOnLookSystem.cs @@ -0,0 +1,292 @@ +using System.Numerics; +using Content.Server.Imperial.SCP.SCP096.Components; +using Content.Server.Imperial.SCP.SCPBlink.Components; +using Content.Server.Popups; +using Content.Shared.Imperial.SCP.SCP096; +using Robust.Shared.Audio.Systems; +using Content.Shared.Audio; +using Content.Shared.Doors.Components; +using Content.Shared.Interaction; +using Content.Shared.Interaction.Events; +using Content.Shared.Inventory; +using Content.Shared.Inventory.Events; +using Content.Shared.Mobs; +using Content.Shared.Mobs.Components; +using Content.Shared.Movement.Systems; +using Content.Shared.Popups; +using Content.Shared.Weapons.Melee; +using Robust.Server.GameObjects; +using Robust.Shared.Timing; + +namespace Content.Server.Imperial.SCP.SCP096.Systems; + +public sealed class SCP096RageOnLookSystem : EntitySystem +{ + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedAmbientSoundSystem _ambientSound = default!; + [Dependency] private readonly EntityLookupSystem _lookup = default!; + [Dependency] private readonly SharedInteractionSystem _interaction = default!; + [Dependency] private readonly InventorySystem _inventory = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + [Dependency] private readonly MovementSpeedModifierSystem _movement = default!; + [Dependency] private readonly AppearanceSystem _appearance = default!; + [Dependency] private readonly PopupSystem _popup = default!; + [Dependency] private readonly IGameTiming _timing = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnRefreshMove); + SubscribeLocalEvent(OnAttackAttempt); + SubscribeLocalEvent(OnIsEquippingTargetAttempt); + } + + private void OnAttackAttempt(Entity ent, ref AttackAttemptEvent args) + { + if (ent.Comp.IsEnraged) + { + if (args.Target is not { } target) + return; + + // Allow attacking doors regardless of rage target + if (HasComp(target)) + return; + + // Allow attacking rage targets + if (ent.Comp.RageTargets.Contains(target)) + return; + } + + // Block all other attacks (non-enraged or non-target entities) + args.Cancel(); + } + + private void OnIsEquippingTargetAttempt(Entity ent, ref IsEquippingTargetAttemptEvent args) + { + if (!ent.Comp.IsRageWindup) + return; + + if (!HasComp(args.Equipment)) + return; + + args.Cancel(); + args.Reason = "inventory-component-can-equip-cannot"; + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + var curTime = _timing.CurTime; + + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var comp, out var mobState)) + { + if (mobState.CurrentState == MobState.Dead) + { + if (comp.IsEnraged || comp.IsRageWindup) + ResetToCalm(uid, comp); + + UpdateStageAmbient(uid, comp, false); + SetVisualState(uid, SCP096VisualState.Dead); + continue; + } + + if (IsRageSuppressed(uid)) + { + if (comp.IsEnraged || comp.IsRageWindup) + ResetToCalm(uid, comp); + + UpdateStageAmbient(uid, comp, true); + SetVisualState(uid, SCP096VisualState.Calm); + continue; + } + + PruneRageTargets(comp); + + var isBeingWatched = UpdateRageTargets(uid, comp); + + if (comp.IsRageWindup) + { + SetVisualState(uid, SCP096VisualState.Screaming); + + if (curTime < comp.RageWindupEndTime) + continue; + + comp.IsRageWindup = false; + comp.IsEnraged = true; + comp.RageEndTime = curTime + comp.RageDuration; + _movement.RefreshMovementSpeedModifiers(uid); + + if (TryComp(uid, out var enragedMelee)) + { + comp.OriginalAttackRate ??= enragedMelee.AttackRate; + enragedMelee.AttackRate = comp.EnragedAttackRate; + Dirty(uid, enragedMelee); + } + + _popup.PopupEntity(Loc.GetString(comp.RagePopup), uid, PopupType.LargeCaution); + UpdateStageAmbient(uid, comp, true); + SetVisualState(uid, SCP096VisualState.Chasing); + continue; + } + + if (comp.IsEnraged) + { + if (curTime >= comp.RageEndTime) + ResetToCalm(uid, comp); + + UpdateStageAmbient(uid, comp, true); + SetVisualState(uid, SCP096VisualState.Chasing); + continue; + } + + if (!isBeingWatched) + continue; + + comp.IsRageWindup = true; + comp.RageWindupEndTime = curTime + comp.RageWindup; + _movement.RefreshMovementSpeedModifiers(uid); + _popup.PopupEntity(Loc.GetString(comp.RageWindupPopup), uid, PopupType.MediumCaution); + _audio.PlayPvs(comp.RageSound, uid); + UpdateStageAmbient(uid, comp, true); + SetVisualState(uid, SCP096VisualState.Screaming); + } + } + + private void ResetToCalm(EntityUid uid, SCP096RageOnLookComponent comp) + { + comp.IsEnraged = false; + comp.IsRageWindup = false; + comp.RageEndTime = TimeSpan.Zero; + comp.RageWindupEndTime = TimeSpan.Zero; + comp.RageTargets.Clear(); + + _movement.RefreshMovementSpeedModifiers(uid); + + if (TryComp(uid, out var calmMelee)) + { + calmMelee.AttackRate = comp.OriginalAttackRate ?? comp.CalmAttackRate; + comp.OriginalAttackRate = null; + Dirty(uid, calmMelee); + } + + _popup.PopupEntity(Loc.GetString(comp.RageCalmPopup), uid, PopupType.Medium); + UpdateStageAmbient(uid, comp, true); + SetVisualState(uid, SCP096VisualState.Calm); + } + + private void SetVisualState(EntityUid uid, SCP096VisualState state) + { + _appearance.SetData(uid, SCP096Visuals.State, state); + } + + private void OnRefreshMove(Entity ent, ref RefreshMovementSpeedModifiersEvent args) + { + if (ent.Comp.IsRageWindup) + { + args.ModifySpeed(0f, 0f); + return; + } + + if (!ent.Comp.IsEnraged) + return; + + args.ModifySpeed(ent.Comp.EnragedWalkModifier, ent.Comp.EnragedSprintModifier); + } + + private bool UpdateRageTargets(EntityUid target, SCP096RageOnLookComponent comp) + { + var targetXform = Transform(target); + var targetPosition = _transform.GetWorldPosition(targetXform); + var targetMap = targetXform.MapID; + var watched = false; + + foreach (var observer in _lookup.GetEntitiesInRange(target, comp.ObserveRadius, LookupFlags.Dynamic)) + { + if (observer == target) + continue; + + if (TryComp(observer, out var blink) && blink.IsBlinking) + continue; + + if (!TryComp(observer, out var mobState) || mobState.CurrentState != MobState.Alive) + continue; + + if (!TryComp(observer, out var observerTransform)) + continue; + + if (observerTransform.MapID != targetMap) + continue; + + if (comp.RequireUnobstructed && !_interaction.InRangeUnobstructed(observer, target, comp.ObserveRadius + 0.1f)) + continue; + + var observerPosition = _transform.GetWorldPosition(observer); + var toTarget = targetPosition - observerPosition; + if (toTarget.LengthSquared() <= 0.001f) + continue; + + var lookDirection = _transform.GetWorldRotation(observerTransform).ToWorldVec(); + var dot = Vector2.Dot(Vector2.Normalize(lookDirection), Vector2.Normalize(toTarget)); + if (dot >= comp.MinLookDot) + { + watched = true; + comp.RageTargets.Add(observer); + } + } + + return watched; + } + + private bool IsRageSuppressed(EntityUid uid) + { + if (_inventory.TryGetSlotEntity(uid, "head", out var headEnt) && HasComp(headEnt)) + return true; + + if (_inventory.TryGetSlotEntity(uid, "mask", out var maskEnt) && HasComp(maskEnt)) + return true; + + return false; + } + + private void UpdateStageAmbient(EntityUid uid, SCP096RageOnLookComponent comp, bool alive) + { + if (!TryComp(uid, out var ambient)) + return; + + if (!alive) + { + _ambientSound.SetAmbience(uid, false, ambient); + comp.UsingRageLoopSound = false; + return; + } + + if (comp.IsEnraged) + { + if (!comp.UsingRageLoopSound) + { + _ambientSound.SetSound(uid, comp.RageLoopSound, ambient); + comp.UsingRageLoopSound = true; + } + + _ambientSound.SetAmbience(uid, true, ambient); + return; + } + + if (comp.UsingRageLoopSound) + { + _ambientSound.SetSound(uid, comp.CryingSound, ambient); + comp.UsingRageLoopSound = false; + } + + _ambientSound.SetAmbience(uid, true, ambient); + } + + private void PruneRageTargets(SCP096RageOnLookComponent comp) + { + comp.RageTargets.RemoveWhere(target => Deleted(target) + || !TryComp(target, out var mobState) + || mobState.CurrentState != MobState.Alive); + } +} diff --git a/Content.Server/Imperial/SCP/SCP173/Components/SCP173ContainmentCellComponent.cs b/Content.Server/Imperial/SCP/SCP173/Components/SCP173ContainmentCellComponent.cs new file mode 100644 index 00000000000..576b7746b65 --- /dev/null +++ b/Content.Server/Imperial/SCP/SCP173/Components/SCP173ContainmentCellComponent.cs @@ -0,0 +1,6 @@ +namespace Content.Server.Imperial.SCP.SCP173.Components; + +[RegisterComponent] +public sealed partial class SCP173ContainmentCellComponent : Component +{ +} \ No newline at end of file diff --git a/Content.Server/Imperial/SCP/SCP173/Components/SCP173LightFlickerComponent.cs b/Content.Server/Imperial/SCP/SCP173/Components/SCP173LightFlickerComponent.cs new file mode 100644 index 00000000000..7f4d74f1ba0 --- /dev/null +++ b/Content.Server/Imperial/SCP/SCP173/Components/SCP173LightFlickerComponent.cs @@ -0,0 +1,41 @@ +using Robust.Shared.Audio; +using Robust.Shared.Prototypes; + +namespace Content.Server.Imperial.SCP.SCP173.Components; + +[RegisterComponent] +public sealed partial class SCP173LightFlickerComponent : Component +{ + [DataField("action")] + public EntProtoId ActionProto = "ActionSCP173LightFlicker"; + + [DataField("radius")] + public float Radius = 9f; + + [DataField("duration")] + public TimeSpan Duration = TimeSpan.FromSeconds(8); + + [DataField("toggleInterval")] + public TimeSpan ToggleInterval = TimeSpan.FromSeconds(0.25f); + + [DataField("activationSound")] + public SoundSpecifier? ActivationSound = new SoundPathSpecifier("/Audio/Machines/lightswitch.ogg"); + + [ViewVariables] + public EntityUid? ActionEntity; + + [ViewVariables] + public bool IsActive; + + [ViewVariables] + public bool LightsOff; + + [ViewVariables] + public TimeSpan EndTime; + + [ViewVariables] + public TimeSpan NextToggle; + + [ViewVariables] + public Dictionary CapturedLightStates = new(); +} \ No newline at end of file diff --git a/Content.Server/Imperial/SCP/SCP173/Components/SCP173WatchLockComponent.cs b/Content.Server/Imperial/SCP/SCP173/Components/SCP173WatchLockComponent.cs new file mode 100644 index 00000000000..3d09e8b158a --- /dev/null +++ b/Content.Server/Imperial/SCP/SCP173/Components/SCP173WatchLockComponent.cs @@ -0,0 +1,35 @@ +namespace Content.Server.Imperial.SCP.SCP173.Components; + +[RegisterComponent] +public sealed partial class SCP173WatchLockComponent : Component +{ + [DataField("observeRadius")] + public float ObserveRadius = 8f; + + [DataField("minLookDot")] + public float MinLookDot = 0.35f; + + [DataField("requireUnobstructed")] + public bool RequireUnobstructed = true; + + [DataField("frozenWalkModifier")] + public float FrozenWalkModifier = 0f; + + [DataField("frozenSprintModifier")] + public float FrozenSprintModifier = 0f; + + [DataField("requireLightToObserve")] + public bool RequireLightToObserve = true; + + [DataField("lightLookupRadius")] + public float LightLookupRadius = 12f; + + [DataField("lightRadiusPadding")] + public float LightRadiusPadding = 0.15f; + + [ViewVariables] + public bool IsLocked; + + [ViewVariables] + public bool IsContainedByCell; +} diff --git a/Content.Server/Imperial/SCP/SCP173/Systems/SCP173ContainmentCellSystem.cs b/Content.Server/Imperial/SCP/SCP173/Systems/SCP173ContainmentCellSystem.cs new file mode 100644 index 00000000000..840d23915c7 --- /dev/null +++ b/Content.Server/Imperial/SCP/SCP173/Systems/SCP173ContainmentCellSystem.cs @@ -0,0 +1,47 @@ +using Content.Server.Imperial.SCP.SCP173.Components; +using Content.Shared.Mobs; +using Content.Shared.Mobs.Components; +using Content.Shared.Movement.Systems; +using Robust.Server.Containers; + +namespace Content.Server.Imperial.SCP.SCP173.Systems; + +public sealed class SCP173ContainmentCellSystem : EntitySystem +{ + [Dependency] private readonly ContainerSystem _container = default!; + [Dependency] private readonly MovementSpeedModifierSystem _movement = default!; + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var lockComp, out var mobState)) + { + var contained = false; + + if (mobState.CurrentState != MobState.Dead) + contained = IsInsideContainmentCell(uid); + + if (lockComp.IsContainedByCell == contained) + continue; + + lockComp.IsContainedByCell = contained; + _movement.RefreshMovementSpeedModifiers(uid); + } + } + + private bool IsInsideContainmentCell(EntityUid target) + { + var current = target; + while (_container.TryGetContainingContainer(current, out var container)) + { + if (HasComp(container.Owner)) + return true; + + current = container.Owner; + } + + return false; + } +} \ No newline at end of file diff --git a/Content.Server/Imperial/SCP/SCP173/Systems/SCP173LightFlickerSystem.cs b/Content.Server/Imperial/SCP/SCP173/Systems/SCP173LightFlickerSystem.cs new file mode 100644 index 00000000000..9a1ce5c52f7 --- /dev/null +++ b/Content.Server/Imperial/SCP/SCP173/Systems/SCP173LightFlickerSystem.cs @@ -0,0 +1,123 @@ +using Content.Server.Actions; +using Content.Server.Imperial.SCP.SCP173.Components; +using Content.Shared.Imperial.SCP.SCP173; +using Robust.Server.GameObjects; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Timing; + +namespace Content.Server.Imperial.SCP.SCP173.Systems; + +public sealed class SCP173LightFlickerSystem : EntitySystem +{ + [Dependency] private readonly ActionsSystem _actions = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly EntityLookupSystem _lookup = default!; + [Dependency] private readonly SharedPointLightSystem _pointLight = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + [Dependency] private readonly IGameTiming _timing = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnMapInit); + SubscribeLocalEvent(OnShutdown); + SubscribeLocalEvent(OnAction); + } + + private void OnMapInit(Entity ent, ref MapInitEvent args) + { + _actions.AddAction(ent, ref ent.Comp.ActionEntity, ent.Comp.ActionProto); + } + + private void OnShutdown(Entity ent, ref ComponentShutdown args) + { + RestoreLights(ent); + } + + private void OnAction(Entity ent, ref SCP173LightFlickerActionEvent args) + { + if (args.Handled) + return; + + if (ent.Comp.IsActive) + return; + + ent.Comp.IsActive = true; + ent.Comp.LightsOff = false; + ent.Comp.EndTime = _timing.CurTime + ent.Comp.Duration; + ent.Comp.NextToggle = _timing.CurTime; + ent.Comp.CapturedLightStates.Clear(); + + if (ent.Comp.ActivationSound != null) + _audio.PlayPvs(ent.Comp.ActivationSound, ent); + + args.Handled = true; + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var curTime = _timing.CurTime; + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var comp)) + { + if (!comp.IsActive) + continue; + + if (curTime >= comp.EndTime) + { + RestoreLights((uid, comp)); + comp.IsActive = false; + comp.LightsOff = false; + continue; + } + + if (curTime < comp.NextToggle) + continue; + + comp.LightsOff = !comp.LightsOff; + comp.NextToggle = curTime + comp.ToggleInterval; + ApplyLightState((uid, comp), !comp.LightsOff); + } + } + + private void ApplyLightState(Entity ent, bool turnOn) + { + var origin = _transform.GetWorldPosition(ent); + var map = Transform(ent).MapID; + + foreach (var lightUid in _lookup.GetEntitiesInRange(ent, ent.Comp.Radius, LookupFlags.Dynamic | LookupFlags.Static)) + { + if (!TryComp(lightUid, out var pointLight)) + continue; + + if (!TryComp(lightUid, out var lightXform) || lightXform.MapID != map) + continue; + + var distance = (_transform.GetWorldPosition(lightUid) - origin).Length(); + if (distance > ent.Comp.Radius) + continue; + + if (!ent.Comp.CapturedLightStates.ContainsKey(lightUid)) + ent.Comp.CapturedLightStates[lightUid] = pointLight.Enabled; + + var targetEnabled = turnOn ? ent.Comp.CapturedLightStates[lightUid] : false; + _pointLight.SetEnabled(lightUid, targetEnabled, pointLight); + } + } + + private void RestoreLights(Entity ent) + { + foreach (var (lightUid, originalEnabled) in ent.Comp.CapturedLightStates) + { + if (!TryComp(lightUid, out var pointLight)) + continue; + + _pointLight.SetEnabled(lightUid, originalEnabled, pointLight); + } + + ent.Comp.CapturedLightStates.Clear(); + } +} \ No newline at end of file diff --git a/Content.Server/Imperial/SCP/SCP173/Systems/SCP173WatchLockSystem.cs b/Content.Server/Imperial/SCP/SCP173/Systems/SCP173WatchLockSystem.cs new file mode 100644 index 00000000000..1838af22c23 --- /dev/null +++ b/Content.Server/Imperial/SCP/SCP173/Systems/SCP173WatchLockSystem.cs @@ -0,0 +1,160 @@ +using System.Numerics; +using Content.Server.Imperial.SCP.SCP173.Components; +using Content.Server.Imperial.SCP.SCPBlink.Components; +using Content.Shared.Interaction; +using Content.Shared.Interaction.Events; +using Content.Shared.Mobs; +using Content.Shared.Mobs.Components; +using Content.Shared.Movement.Systems; +using Robust.Server.GameObjects; + +namespace Content.Server.Imperial.SCP.SCP173.Systems; + +public sealed class SCP173WatchLockSystem : EntitySystem +{ + [Dependency] private readonly EntityLookupSystem _lookup = default!; + [Dependency] private readonly SharedInteractionSystem _interaction = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + [Dependency] private readonly MovementSpeedModifierSystem _movement = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnAttackAttempt); + SubscribeLocalEvent(OnRefreshMove); + } + + private void OnAttackAttempt(Entity ent, ref AttackAttemptEvent args) + { + if (ent.Comp.IsContainedByCell) + { + args.Cancel(); + return; + } + + if (!ent.Comp.IsLocked) + return; + + if (IsDarkForWatchLock(ent, ent.Comp)) + return; + + args.Cancel(); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var comp, out var mobState)) + { + if (mobState.CurrentState == MobState.Dead) + { + if (comp.IsLocked) + SetLocked((uid, comp), false); + continue; + } + + var watched = IsBeingWatched(uid, comp); + if (watched == comp.IsLocked) + continue; + + SetLocked((uid, comp), watched); + } + } + + private void SetLocked(Entity ent, bool locked) + { + ent.Comp.IsLocked = locked; + _movement.RefreshMovementSpeedModifiers(ent); + } + + private void OnRefreshMove(Entity ent, ref RefreshMovementSpeedModifiersEvent args) + { + if (ent.Comp.IsContainedByCell) + { + args.ModifySpeed(ent.Comp.FrozenWalkModifier, ent.Comp.FrozenSprintModifier); + return; + } + + if (!ent.Comp.IsLocked) + return; + + if (IsDarkForWatchLock(ent, ent.Comp)) + return; + + args.ModifySpeed(ent.Comp.FrozenWalkModifier, ent.Comp.FrozenSprintModifier); + } + + private bool IsDarkForWatchLock(EntityUid uid, SCP173WatchLockComponent comp) + { + return comp.RequireLightToObserve && !IsTargetLit(uid, comp); + } + + private bool IsBeingWatched(EntityUid target, SCP173WatchLockComponent comp) + { + if (comp.RequireLightToObserve && !IsTargetLit(target, comp)) + return false; + + var targetPosition = _transform.GetWorldPosition(target); + var targetMap = Transform(target).MapID; + + foreach (var observer in _lookup.GetEntitiesInRange(target, comp.ObserveRadius, LookupFlags.Dynamic)) + { + if (observer == target) + continue; + + if (TryComp(observer, out var blink) && blink.IsBlinking) + continue; + + if (!TryComp(observer, out var mobState) || mobState.CurrentState != MobState.Alive) + continue; + + if (!TryComp(observer, out var observerTransform)) + continue; + + if (observerTransform.MapID != targetMap) + continue; + + if (comp.RequireUnobstructed && !_interaction.InRangeUnobstructed(observer, target, comp.ObserveRadius + 0.1f)) + continue; + + var observerPosition = _transform.GetWorldPosition(observer); + var toTarget = targetPosition - observerPosition; + if (toTarget.LengthSquared() <= 0.001f) + continue; + + var lookDirection = _transform.GetWorldRotation(observerTransform).ToWorldVec(); + var dot = Vector2.Dot(Vector2.Normalize(lookDirection), Vector2.Normalize(toTarget)); + if (dot >= comp.MinLookDot) + return true; + } + + return false; + } + + private bool IsTargetLit(EntityUid target, SCP173WatchLockComponent comp) + { + if (TryComp(target, out var selfLight) && selfLight.Enabled && selfLight.Radius > 0f) + return true; + + var targetPosition = _transform.GetWorldPosition(target); + var targetMap = Transform(target).MapID; + + foreach (var ent in _lookup.GetEntitiesInRange(target, comp.LightLookupRadius, LookupFlags.Dynamic | LookupFlags.Static)) + { + if (!TryComp(ent, out var light) || !light.Enabled) + continue; + + if (!TryComp(ent, out var lightTransform) || lightTransform.MapID != targetMap) + continue; + + var distance = (_transform.GetWorldPosition(ent) - targetPosition).Length(); + if (distance <= light.Radius + comp.LightRadiusPadding) + return true; + } + + return false; + } +} diff --git a/Content.Server/Imperial/SCP/SCPBlink/Components/SCPBlinkBlindnessComponent.cs b/Content.Server/Imperial/SCP/SCPBlink/Components/SCPBlinkBlindnessComponent.cs new file mode 100644 index 00000000000..a817ddfb268 --- /dev/null +++ b/Content.Server/Imperial/SCP/SCPBlink/Components/SCPBlinkBlindnessComponent.cs @@ -0,0 +1,6 @@ +namespace Content.Server.Imperial.SCP.SCPBlink.Components; + +[RegisterComponent] +public sealed partial class SCPBlinkBlindnessComponent : Component +{ +} diff --git a/Content.Server/Imperial/SCP/SCPBlink/Components/SCPBlinkManualTriggerComponent.cs b/Content.Server/Imperial/SCP/SCPBlink/Components/SCPBlinkManualTriggerComponent.cs new file mode 100644 index 00000000000..f6d3b624908 --- /dev/null +++ b/Content.Server/Imperial/SCP/SCPBlink/Components/SCPBlinkManualTriggerComponent.cs @@ -0,0 +1,6 @@ +namespace Content.Server.Imperial.SCP.SCPBlink.Components; + +[RegisterComponent] +public sealed partial class SCPBlinkManualTriggerComponent : Component +{ +} diff --git a/Content.Server/Imperial/SCP/SCPBlink/Components/SCPBlinkableComponent.cs b/Content.Server/Imperial/SCP/SCPBlink/Components/SCPBlinkableComponent.cs new file mode 100644 index 00000000000..d0ff4533071 --- /dev/null +++ b/Content.Server/Imperial/SCP/SCPBlink/Components/SCPBlinkableComponent.cs @@ -0,0 +1,62 @@ +using Content.Shared.Mobs; +using Robust.Shared.Audio; + +namespace Content.Server.Imperial.SCP.SCPBlink.Components; + +[RegisterComponent] +public sealed partial class SCPBlinkableComponent : Component +{ + [DataField("blinkInterval")] + public TimeSpan BlinkInterval = TimeSpan.FromSeconds(8); + + [DataField("blinkDuration")] + public TimeSpan BlinkDuration = TimeSpan.FromSeconds(1); + + [DataField("visualBlindDuration")] + public TimeSpan VisualBlindDuration = TimeSpan.FromSeconds(0.15f); + + [DataField("allowCritical")] + public bool AllowCritical = false; + + [DataField("canManualBlink")] + public bool CanManualBlink = true; + + [DataField("manualBlinkPopup")] + public string ManualBlinkPopup = "Вы моргнули."; + + [DataField("blinkStartPopup")] + public string BlinkStartPopup = "Вы моргаете..."; + + [DataField("blinkEndPopup")] + public string BlinkEndPopup = "Вы снова видите четко."; + + [DataField("blinkStartSound")] + public SoundSpecifier? BlinkStartSound = new SoundPathSpecifier("/Audio/Effects/glass_knock.ogg"); + + [DataField("blinkEndSound")] + public SoundSpecifier? BlinkEndSound = new SoundPathSpecifier("/Audio/Effects/flip.ogg"); + + [DataField("manualBlinkCooldown")] + public TimeSpan ManualBlinkCooldown = TimeSpan.FromSeconds(3); + + [ViewVariables] + public TimeSpan NextBlinkTime; + + [ViewVariables] + public TimeSpan BlinkEndTime; + + [ViewVariables] + public TimeSpan NextManualBlink; + + [ViewVariables] + public TimeSpan NextAlertUpdate; + + [ViewVariables] + public TimeSpan VisualBlindEndTime; + + [ViewVariables] + public bool LegacyBlindnessCleanupDone; + + [ViewVariables] + public bool IsBlinking; +} diff --git a/Content.Server/Imperial/SCP/SCPBlink/Systems/SCPBlinkSystem.cs b/Content.Server/Imperial/SCP/SCPBlink/Systems/SCPBlinkSystem.cs new file mode 100644 index 00000000000..ee3f683207d --- /dev/null +++ b/Content.Server/Imperial/SCP/SCPBlink/Systems/SCPBlinkSystem.cs @@ -0,0 +1,210 @@ +using Content.Server.Imperial.SCP.SCPBlink.Components; +using Content.Server.Popups; +using Content.Shared.Alert; +using Content.Shared.Eye.Blinding.Components; +using Content.Shared.Eye.Blinding.Systems; +using Content.Shared.Imperial.SCP.SCPBlink; +using Robust.Shared.Audio.Systems; +using Content.Shared.Interaction; +using Content.Shared.Mobs; +using Content.Shared.Mobs.Components; +using Content.Shared.Popups; +using Robust.Shared.Timing; + +namespace Content.Server.Imperial.SCP.SCPBlink.Systems; + +public sealed class SCPBlinkSystem : EntitySystem +{ + private const string BlinkAlert = "SCPBlink"; + + [Dependency] private readonly AlertsSystem _alerts = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly BlindableSystem _blindable = default!; + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly PopupSystem _popup = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnMapInit); + SubscribeLocalEvent(OnShutdown); + SubscribeLocalEvent(OnBlinkAlert); + SubscribeLocalEvent(OnBlinkBlindnessSeeAttempt); + SubscribeLocalEvent(OnInteractHand); + } + + private void OnBlinkAlert(Entity ent, ref SCPBlinkAlertEvent args) + { + if (args.Handled) + return; + + TryManualBlink(ent.Owner); + args.Handled = true; + } + + private void OnMapInit(Entity ent, ref MapInitEvent args) + { + ent.Comp.IsBlinking = false; + ent.Comp.NextBlinkTime = _timing.CurTime + ent.Comp.BlinkInterval; + ent.Comp.BlinkEndTime = TimeSpan.Zero; + ent.Comp.NextManualBlink = TimeSpan.Zero; + ent.Comp.NextAlertUpdate = TimeSpan.Zero; + ent.Comp.VisualBlindEndTime = TimeSpan.Zero; + ent.Comp.LegacyBlindnessCleanupDone = false; + } + + private void OnShutdown(Entity ent, ref ComponentShutdown args) + { + _alerts.ClearAlert(ent.Owner, BlinkAlert); + + if (HasComp(ent.Owner)) + { + RemComp(ent.Owner); + _blindable.UpdateIsBlind(ent.Owner); + } + + if (HasComp(ent.Owner)) + { + RemComp(ent.Owner); + _blindable.UpdateIsBlind(ent.Owner); + } + } + + private void OnBlinkBlindnessSeeAttempt(Entity ent, ref CanSeeAttemptEvent args) + { + args.Cancel(); + } + + private void OnInteractHand(Entity ent, ref InteractHandEvent args) + { + if (TryManualBlink(args.User)) + args.Handled = true; + } + + private bool TryManualBlink(EntityUid uid) + { + if (!TryComp(uid, out var blink)) + return false; + + if (!blink.CanManualBlink) + return false; + + if (blink.IsBlinking) + return false; + + var curTime = _timing.CurTime; + if (curTime < blink.NextManualBlink) + return false; + + if (!TryComp(uid, out var mobState)) + return false; + + var validState = mobState.CurrentState == MobState.Alive || + (blink.AllowCritical && mobState.CurrentState == MobState.Critical); + + if (!validState) + return false; + + BeginBlink((uid, blink)); + blink.NextManualBlink = curTime + blink.ManualBlinkCooldown; + _popup.PopupEntity(blink.ManualBlinkPopup, uid, uid, PopupType.Medium); + + return true; + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var curTime = _timing.CurTime; + var query = EntityQueryEnumerator(); + + while (query.MoveNext(out var uid, out var comp, out var mobState)) + { + if (!comp.LegacyBlindnessCleanupDone) + { + comp.LegacyBlindnessCleanupDone = true; + if (HasComp(uid)) + { + RemComp(uid); + _blindable.UpdateIsBlind(uid); + } + } + + if (comp.VisualBlindEndTime != TimeSpan.Zero && curTime >= comp.VisualBlindEndTime) + { + comp.VisualBlindEndTime = TimeSpan.Zero; + if (HasComp(uid)) + { + RemComp(uid); + _blindable.UpdateIsBlind(uid); + } + } + + var validState = mobState.CurrentState == MobState.Alive || + (comp.AllowCritical && mobState.CurrentState == MobState.Critical); + + if (!validState) + continue; + + if (comp.IsBlinking) + { + if (curTime < comp.BlinkEndTime) + { + UpdateBlinkAlert(uid, comp, curTime, comp.BlinkEndTime - curTime); + continue; + } + + comp.IsBlinking = false; + if (!string.IsNullOrWhiteSpace(comp.BlinkEndPopup)) + _popup.PopupEntity(comp.BlinkEndPopup, uid, uid, PopupType.SmallCaution); + + if (comp.BlinkEndSound != null) + _audio.PlayPvs(comp.BlinkEndSound, uid); + + UpdateBlinkAlert(uid, comp, curTime, comp.NextBlinkTime - curTime); + continue; + } + + if (curTime >= comp.NextBlinkTime) + BeginBlink((uid, comp)); + + UpdateBlinkAlert(uid, comp, curTime, comp.NextBlinkTime - curTime); + } + } + + private void BeginBlink(Entity ent) + { + var curTime = _timing.CurTime; + ent.Comp.IsBlinking = true; + ent.Comp.BlinkEndTime = curTime + ent.Comp.BlinkDuration; + ent.Comp.NextBlinkTime = curTime + ent.Comp.BlinkInterval; + if (ent.Comp.VisualBlindDuration > TimeSpan.Zero) + { + ent.Comp.VisualBlindEndTime = curTime + ent.Comp.VisualBlindDuration; + EnsureComp(ent.Owner); + _blindable.UpdateIsBlind(ent.Owner); + } + + if (!string.IsNullOrWhiteSpace(ent.Comp.BlinkStartPopup)) + _popup.PopupEntity(ent.Comp.BlinkStartPopup, ent, ent, PopupType.SmallCaution); + + if (ent.Comp.BlinkStartSound != null) + _audio.PlayPvs(ent.Comp.BlinkStartSound, ent); + + UpdateBlinkAlert(ent.Owner, ent.Comp, curTime, ent.Comp.BlinkEndTime - curTime); + } + + private void UpdateBlinkAlert(EntityUid uid, SCPBlinkableComponent comp, TimeSpan curTime, TimeSpan remaining) + { + if (curTime < comp.NextAlertUpdate) + return; + + comp.NextAlertUpdate = curTime + TimeSpan.FromSeconds(0.2f); + if (remaining < TimeSpan.Zero) + remaining = TimeSpan.Zero; + + _alerts.UpdateAlert(uid, BlinkAlert, cooldown: remaining, showCooldown: true); + } +} diff --git a/Content.Server/Imperial/SCP/SCPFireman/Components/SCPFireSpreadComponent.cs b/Content.Server/Imperial/SCP/SCPFireman/Components/SCPFireSpreadComponent.cs new file mode 100644 index 00000000000..7f62d1549de --- /dev/null +++ b/Content.Server/Imperial/SCP/SCPFireman/Components/SCPFireSpreadComponent.cs @@ -0,0 +1,44 @@ +namespace Content.Server.Imperial.SCP.SCPFireman.Components; + +[RegisterComponent] +public sealed partial class SCPFireSpreadComponent : Component +{ + [DataField("owner")] + public EntityUid FireOwner; + + [DataField("spreadDelay")] + public TimeSpan SpreadDelay = TimeSpan.FromSeconds(15); + + [DataField("spreadInterval")] + public TimeSpan SpreadInterval = TimeSpan.FromSeconds(15); + + [DataField("pointInterval")] + public TimeSpan PointInterval = TimeSpan.FromSeconds(1); + + [DataField("healInterval")] + public TimeSpan HealInterval = TimeSpan.FromSeconds(5); + + [DataField("healPerType")] + public float HealPerType = 0.1f; + + [DataField("igniteInterval")] + public TimeSpan IgniteInterval = TimeSpan.FromSeconds(1); + + [DataField("igniteRadius")] + public float IgniteRadius = 0.7f; + + [DataField("maxSpreadPerPulse")] + public int MaxSpreadPerPulse = 4; + + [ViewVariables] + public TimeSpan NextSpreadTime; + + [ViewVariables] + public TimeSpan NextPointTime; + + [ViewVariables] + public TimeSpan NextHealTime; + + [ViewVariables] + public TimeSpan NextIgniteTime; +} diff --git a/Content.Server/Imperial/SCP/SCPFireman/Components/SCPFireWhirlComponent.cs b/Content.Server/Imperial/SCP/SCPFireman/Components/SCPFireWhirlComponent.cs new file mode 100644 index 00000000000..d412ee10607 --- /dev/null +++ b/Content.Server/Imperial/SCP/SCPFireman/Components/SCPFireWhirlComponent.cs @@ -0,0 +1,31 @@ +using System.Numerics; + +namespace Content.Server.Imperial.SCP.SCPFireman.Components; + +[RegisterComponent] +public sealed partial class SCPFireWhirlComponent : Component +{ + [DataField("owner")] + public EntityUid FireOwner; + + [DataField("speed")] + public float Speed = 4f; + + [DataField("lifetime")] + public TimeSpan Lifetime = TimeSpan.FromSeconds(30); + + [DataField("igniteRadius")] + public float IgniteRadius = 0.6f; + + [DataField("effectInterval")] + public TimeSpan EffectInterval = TimeSpan.FromSeconds(0.25f); + + [ViewVariables] + public Vector2 Direction = Vector2.UnitX; + + [ViewVariables] + public TimeSpan EndTime; + + [ViewVariables] + public TimeSpan NextEffectTime; +} diff --git a/Content.Server/Imperial/SCP/SCPFireman/Components/SCPFiremanComponent.cs b/Content.Server/Imperial/SCP/SCPFireman/Components/SCPFiremanComponent.cs new file mode 100644 index 00000000000..7d758c61aaf --- /dev/null +++ b/Content.Server/Imperial/SCP/SCPFireman/Components/SCPFiremanComponent.cs @@ -0,0 +1,82 @@ +using Robust.Shared.Prototypes; + +namespace Content.Server.Imperial.SCP.SCPFireman.Components; + +[RegisterComponent] +public sealed partial class SCPFiremanComponent : Component +{ + [DataField("maxFirePoints")] + public float MaxFirePoints = 100f; + + [DataField("firePoints")] + public float FirePoints = 100f; + + [DataField("passiveRegenPerSecond")] + public float PassiveRegenPerSecond = 1f; + + [DataField("secondModeDrainPerSecond")] + public float SecondModeDrainPerSecond = 1.5f; + + [DataField("maxOwnedFires")] + public int MaxOwnedFires = 80; + + [DataField("trueFlameDuration")] + public TimeSpan TrueFlameDuration = TimeSpan.FromSeconds(25); + + [DataField("igniteAction")] + public EntProtoId IgniteAction = "ActionSCPFiremanIgnite"; + + [DataField("fireballAction")] + public EntProtoId FireballAction = "ActionSCPFiremanFireball"; + + [DataField("whirlAction")] + public EntProtoId WhirlAction = "ActionSCPFiremanWhirl"; + + [DataField("meltAction")] + public EntProtoId MeltAction = "ActionSCPFiremanMelt"; + + [DataField("trueFlameAction")] + public EntProtoId TrueFlameAction = "ActionSCPFiremanTrueFlame"; + + [DataField("strikeAction")] + public EntProtoId StrikeAction = "ActionSCPFiremanStrike"; + + [DataField("secondModeAction")] + public EntProtoId SecondModeAction = "ActionSCPFiremanSecondMode"; + + [DataField] + public EntityUid? IgniteActionEntity; + + [DataField] + public EntityUid? FireballActionEntity; + + [DataField] + public EntityUid? WhirlActionEntity; + + [DataField] + public EntityUid? MeltActionEntity; + + [DataField] + public EntityUid? TrueFlameActionEntity; + + [DataField] + public EntityUid? StrikeActionEntity; + + [DataField] + public EntityUid? SecondModeActionEntity; + + [ViewVariables] + public TimeSpan NextPassiveTick; + + [ViewVariables] + public TimeSpan NextSecondModeDrainTick; + + [ViewVariables] + public bool SecondModeEnabled; + + [ViewVariables] + public bool TrueFlameActive; + + [ViewVariables] + public TimeSpan TrueFlameEnd; +} diff --git a/Content.Server/Imperial/SCP/SCPFireman/Systems/SCPFiremanSystem.cs b/Content.Server/Imperial/SCP/SCPFireman/Systems/SCPFiremanSystem.cs new file mode 100644 index 00000000000..e6d1f2667c4 --- /dev/null +++ b/Content.Server/Imperial/SCP/SCPFireman/Systems/SCPFiremanSystem.cs @@ -0,0 +1,681 @@ +using System.Numerics; +using Content.Server.Actions; +using Content.Server.Atmos.EntitySystems; +using Content.Server.Damage.Systems; +using Content.Server.Imperial.SCP.SCPFireman.Components; +using Content.Shared.Alert; +using Content.Shared.Atmos; +using Content.Shared.Atmos.Components; +using Content.Shared.Chemistry; +using Content.Shared.Chemistry.EntitySystems; +using Content.Shared.Chemistry.Reagent; +using Content.Shared.Damage; +using Content.Shared.Damage.Components; +using Content.Shared.Damage.Prototypes; +using Content.Shared.Damage.Systems; +using Content.Shared.Fluids.Components; +using Content.Shared.Imperial.SCP.SCPFireman.Events; +using Content.Shared.Interaction; +using Content.Shared.Mobs.Components; +using Content.Shared.Popups; +using Content.Shared.Tag; +using Content.Shared.FixedPoint; +using Robust.Server.GameObjects; +using Robust.Shared.GameObjects; +using Robust.Shared.Map; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; +using TimedDespawnComponent = Robust.Shared.Spawners.TimedDespawnComponent; +using Robust.Shared.Timing; + +namespace Content.Server.Imperial.SCP.SCPFireman.Systems; + +public sealed class SCPFiremanSystem : EntitySystem +{ + private static readonly ReagentId WaterReagent = new("Water", null); + private static readonly ReagentId SpaceCleanerReagent = new("SpaceCleaner", null); + private static readonly ProtoId StructuralDamageId = "Structural"; + private static readonly ProtoId FirePointsAlert = "SCPFiremanPoints"; + + private const float FireballCost = 50f; + private const float WhirlCost = 30f; + private const float MeltCost = 20f; + private const float TrueFlameCost = 90f; + private const float StrikeCost = 40f; + private const float SecondModeRange = 6f; + + private const string WallTag = "Wall"; + private const string WindowTag = "Window"; + private const string GateTag = "Gate"; + + [Dependency] private readonly ActionsSystem _actions = default!; + [Dependency] private readonly AlertsSystem _alerts = default!; + [Dependency] private readonly DamageableSystem _damageable = default!; + [Dependency] private readonly SharedGodmodeSystem _godmode = default!; + [Dependency] private readonly FlammableSystem _flammable = default!; + [Dependency] private readonly SharedSolutionContainerSystem _solutions = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly TagSystem _tag = default!; + [Dependency] private readonly TransformSystem _transform = default!; + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly IPrototypeManager _prototypes = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly EntityLookupSystem _lookup = default!; + + private readonly List<(EntityUid Owner, TimeSpan TriggerTime)> _pendingStrikes = new(); + private readonly List<(EntityUid Source, EntityUid Owner, int MaxSpread)> _pendingSpreads = new(); + private readonly HashSet> _nearFlammables = new(); + private readonly HashSet> _nearPuddles = new(); + private readonly HashSet> _nearFires = new(); + private readonly HashSet> _nearStructures = new(); + private readonly Dictionary _ownedFireCounts = new(); + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnMapInit); + SubscribeLocalEvent(OnShutdown); + SubscribeLocalEvent(OnIgniteAction); + SubscribeLocalEvent(OnFireballAction); + SubscribeLocalEvent(OnWhirlAction); + SubscribeLocalEvent(OnMeltAction); + SubscribeLocalEvent(OnTrueFlameAction); + SubscribeLocalEvent(OnStrikeAction); + SubscribeLocalEvent(OnSecondModeAction); + SubscribeLocalEvent(OnActivateInWorld); + SubscribeLocalEvent(OnSpreadMapInit); + SubscribeLocalEvent(OnSpreadExtinguish); + SubscribeLocalEvent(OnSpreadExtinguished); + SubscribeLocalEvent(OnSpreadReactionEntity); + SubscribeLocalEvent(OnSpreadShutdown); + + SubscribeLocalEvent(OnWhirlMapInit); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + UpdateFiremen(); + UpdateSpreadFires(); + UpdateWhirls(frameTime); + UpdatePendingStrikes(); + } + + private void OnMapInit(Entity ent, ref MapInitEvent args) + { + _actions.AddAction(ent, ref ent.Comp.IgniteActionEntity, ent.Comp.IgniteAction); + _actions.AddAction(ent, ref ent.Comp.FireballActionEntity, ent.Comp.FireballAction); + _actions.AddAction(ent, ref ent.Comp.WhirlActionEntity, ent.Comp.WhirlAction); + _actions.AddAction(ent, ref ent.Comp.MeltActionEntity, ent.Comp.MeltAction); + _actions.AddAction(ent, ref ent.Comp.TrueFlameActionEntity, ent.Comp.TrueFlameAction); + _actions.AddAction(ent, ref ent.Comp.StrikeActionEntity, ent.Comp.StrikeAction); + _actions.AddAction(ent, ref ent.Comp.SecondModeActionEntity, ent.Comp.SecondModeAction); + + var now = _timing.CurTime; + ent.Comp.FirePoints = ent.Comp.MaxFirePoints; + ent.Comp.NextPassiveTick = now + TimeSpan.FromSeconds(1); + ent.Comp.NextSecondModeDrainTick = now + TimeSpan.FromSeconds(1); + Dirty(ent.Owner, ent.Comp); + UpdateFirePointsAlert(ent.Owner, ent.Comp); + } + + private void OnShutdown(Entity ent, ref ComponentShutdown args) + { + _alerts.ClearAlert(ent.Owner, FirePointsAlert); + _godmode.DisableGodmode(ent.Owner); + } + + private void OnIgniteAction(Entity ent, ref SCPFiremanIgniteActionEvent args) + { + if (args.Handled) + return; + + SpawnFireAtEntity(ent.Owner, ent.Owner); + args.Handled = true; + } + + private void OnFireballAction(Entity ent, ref SCPFiremanFireballActionEvent args) + { + if (args.Handled || !TryConsumePoints(ent, FireballCost)) + return; + + var target = args.Target.ToMap(EntityManager, _transform); + SpawnFireAtMap(target, ent.Owner); + args.Handled = true; + } + + private void OnWhirlAction(Entity ent, ref SCPFiremanWhirlActionEvent args) + { + if (args.Handled || !TryConsumePoints(ent, WhirlCost) || !TryGetMapCoordinates(ent.Owner, out var start)) + return; + + var target = args.Target.ToMap(EntityManager, _transform); + var direction = target.Position - start.Position; + if (direction.LengthSquared() <= 0.001f) + direction = Vector2.UnitX; + + direction = Vector2.Normalize(direction); + + var whirl = Spawn("SCPFiremanWhirl", Transform(ent.Owner).Coordinates); + if (TryComp(whirl, out var whirlComp)) + { + whirlComp.FireOwner = ent.Owner; + whirlComp.Direction = direction; + whirlComp.EndTime = _timing.CurTime + whirlComp.Lifetime; + + var timedDespawn = EnsureComp(whirl); + timedDespawn.Lifetime = (float) whirlComp.Lifetime.TotalSeconds; + } + + args.Handled = true; + } + + private void OnMeltAction(Entity ent, ref SCPFiremanMeltActionEvent args) + { + if (args.Handled || !TryConsumePoints(ent, MeltCost)) + return; + + if (!EntityManager.EntityExists(args.Target)) + return; + + if (_prototypes.TryIndex(StructuralDamageId, out DamageTypePrototype? structuralDamage)) + { + var damage = new DamageSpecifier(structuralDamage, FixedPoint2.New(100)); + _damageable.TryChangeDamage(args.Target, damage, origin: ent.Owner); + } + + SpawnFireAtEntity(args.Target, ent.Owner); + args.Handled = true; + } + + private void OnTrueFlameAction(Entity ent, ref SCPFiremanTrueFlameActionEvent args) + { + if (args.Handled || !TryConsumePoints(ent, TrueFlameCost)) + return; + + ent.Comp.TrueFlameActive = true; + ent.Comp.TrueFlameEnd = _timing.CurTime + ent.Comp.TrueFlameDuration; + _godmode.EnableGodmode(ent.Owner); + if (ent.Comp.TrueFlameActionEntity != null) + _actions.SetCooldown(ent.Comp.TrueFlameActionEntity.Value, _timing.CurTime, _timing.CurTime + ent.Comp.TrueFlameDuration); + args.Handled = true; + } + + private void OnStrikeAction(Entity ent, ref SCPFiremanStrikeActionEvent args) + { + if (args.Handled || !TryConsumePoints(ent, StrikeCost)) + return; + + _pendingStrikes.Add((ent.Owner, _timing.CurTime + TimeSpan.FromSeconds(1.5f))); + args.Handled = true; + } + + private void OnSecondModeAction(Entity ent, ref SCPFiremanSecondModeActionEvent args) + { + if (args.Handled) + return; + + ent.Comp.SecondModeEnabled = !ent.Comp.SecondModeEnabled; + args.Handled = true; + } + + private void OnActivateInWorld(Entity ent, ref UserActivateInWorldEvent args) + { + if (args.Handled || args.User != ent.Owner || !ent.Comp.SecondModeEnabled) + return; + + if (!EntityManager.EntityExists(args.Target) || !TryGetMapCoordinates(ent.Owner, out var start) || !TryGetMapCoordinates(args.Target, out var end)) + return; + + if (start.MapId != end.MapId) + return; + + EmitSecondModeFire(ent.Owner, start, end); + args.Handled = true; + } + + private void OnBeforeDamageChanged(Entity ent, ref BeforeDamageChangedEvent args) + { + if (!ent.Comp.TrueFlameActive) + return; + + if (_timing.CurTime >= ent.Comp.TrueFlameEnd) + return; + + args.Cancelled = true; + } + + private void OnSpreadMapInit(Entity ent, ref MapInitEvent args) + { + var now = _timing.CurTime; + ent.Comp.NextSpreadTime = now + ent.Comp.SpreadDelay; + ent.Comp.NextPointTime = now + ent.Comp.PointInterval; + ent.Comp.NextHealTime = now + ent.Comp.HealInterval; + ent.Comp.NextIgniteTime = now + ent.Comp.IgniteInterval; + + if (ent.Comp.FireOwner != EntityUid.Invalid) + IncrementOwnedFire(ent.Comp.FireOwner); + } + + private void OnSpreadShutdown(Entity ent, ref ComponentShutdown args) + { + if (ent.Comp.FireOwner != EntityUid.Invalid) + DecrementOwnedFire(ent.Comp.FireOwner); + } + + private void OnSpreadExtinguished(Entity ent, ref ExtinguishedEvent args) + { + QueueDel(ent.Owner); + } + + private void OnSpreadExtinguish(Entity ent, ref ExtinguishEvent args) + { + QueueDel(ent.Owner); + } + + private void OnSpreadReactionEntity(Entity ent, ref ReactionEntityEvent args) + { + var reagentId = args.Reagent.ID; + if (reagentId != WaterReagent.Prototype && reagentId != SpaceCleanerReagent.Prototype) + return; + + QueueDel(ent.Owner); + } + + private void OnWhirlMapInit(Entity ent, ref MapInitEvent args) + { + ent.Comp.EndTime = _timing.CurTime + ent.Comp.Lifetime; + ent.Comp.NextEffectTime = _timing.CurTime; + + var timedDespawn = EnsureComp(ent.Owner); + timedDespawn.Lifetime = (float) ent.Comp.Lifetime.TotalSeconds; + } + + private void UpdateFiremen() + { + var now = _timing.CurTime; + var query = EntityQueryEnumerator(); + + while (query.MoveNext(out var uid, out var comp)) + { + if (comp.TrueFlameActive && now >= comp.TrueFlameEnd) + { + comp.TrueFlameActive = false; + _godmode.DisableGodmode(uid); + } + + while (now >= comp.NextPassiveTick) + { + AddPoints(uid, comp.PassiveRegenPerSecond, comp); + comp.NextPassiveTick += TimeSpan.FromSeconds(1); + } + + if (!comp.SecondModeEnabled) + continue; + + while (now >= comp.NextSecondModeDrainTick) + { + if (comp.FirePoints <= 0f) + { + comp.SecondModeEnabled = false; + comp.NextSecondModeDrainTick = now + TimeSpan.FromSeconds(1); + break; + } + + AddPoints(uid, -comp.SecondModeDrainPerSecond, comp); + comp.NextSecondModeDrainTick += TimeSpan.FromSeconds(1); + + if (comp.FirePoints <= 0f) + { + comp.SecondModeEnabled = false; + comp.NextSecondModeDrainTick = now + TimeSpan.FromSeconds(1); + break; + } + } + } + } + + private void UpdateSpreadFires() + { + var now = _timing.CurTime; + var query = EntityQueryEnumerator(); + _pendingSpreads.Clear(); + + while (query.MoveNext(out var uid, out var comp)) + { + if (!TryGetMapCoordinates(uid, out var mapCoords)) + continue; + + if (IsWaterAt(mapCoords)) + { + QueueDel(uid); + continue; + } + + var hasOwner = comp.FireOwner != EntityUid.Invalid; + + while (now >= comp.NextPointTime) + { + if (hasOwner && TryComp(comp.FireOwner, out var fireman)) + AddPoints(comp.FireOwner, 1f, fireman); + + comp.NextPointTime += comp.PointInterval; + } + + while (now >= comp.NextHealTime) + { + if (hasOwner) + HealOwner(comp.FireOwner, comp.HealPerType); + + comp.NextHealTime += comp.HealInterval; + } + + while (now >= comp.NextIgniteTime) + { + IgniteEntitiesInRadius(mapCoords, comp.IgniteRadius, comp.FireOwner); + comp.NextIgniteTime += comp.IgniteInterval; + } + + while (now >= comp.NextSpreadTime) + { + _pendingSpreads.Add((uid, comp.FireOwner, comp.MaxSpreadPerPulse)); + comp.NextSpreadTime += comp.SpreadInterval; + } + } + + foreach (var spread in _pendingSpreads) + { + SpreadFire(spread.Source, spread.Owner, spread.MaxSpread); + } + } + + private void UpdateWhirls(float frameTime) + { + var now = _timing.CurTime; + var query = EntityQueryEnumerator(); + + while (query.MoveNext(out var uid, out var comp)) + { + if (now >= comp.EndTime || !TryGetMapCoordinates(uid, out var mapCoords)) + { + QueueDel(uid); + continue; + } + + var nextPosition = mapCoords.Position + comp.Direction * comp.Speed * frameTime; + var nextCoords = new MapCoordinates(nextPosition, mapCoords.MapId); + + if (IsBlockedBySolidTag(nextCoords)) + { + comp.Direction = -comp.Direction; + nextPosition = mapCoords.Position + comp.Direction * comp.Speed * frameTime; + nextCoords = new MapCoordinates(nextPosition, mapCoords.MapId); + } + + _transform.SetWorldPosition(uid, nextPosition); + + if (now >= comp.NextEffectTime) + { + comp.NextEffectTime = now + comp.EffectInterval; + IgniteEntitiesInRadius(nextCoords, comp.IgniteRadius, comp.FireOwner); + SpawnFireAtMap(nextCoords, comp.FireOwner); + } + } + } + + private void UpdatePendingStrikes() + { + if (_pendingStrikes.Count == 0) + return; + + var now = _timing.CurTime; + + for (var i = _pendingStrikes.Count - 1; i >= 0; i--) + { + var strike = _pendingStrikes[i]; + if (now < strike.TriggerTime) + continue; + + _pendingStrikes.RemoveAt(i); + + if (!TryGetMapCoordinates(strike.Owner, out var center)) + continue; + + IgniteEntitiesInRadius(center, 20f, strike.Owner); + SpawnFireAtMap(center, strike.Owner); + } + } + + private bool TryConsumePoints(Entity ent, float cost) + { + if (ent.Comp.FirePoints < cost) + { + _popup.PopupEntity("Недостаточно огненных очков.", ent, ent, PopupType.MediumCaution); + return false; + } + + AddPoints(ent.Owner, -cost, ent.Comp); + return true; + } + + private void AddPoints(EntityUid uid, float delta, SCPFiremanComponent comp) + { + comp.FirePoints = Math.Clamp(comp.FirePoints + delta, 0f, comp.MaxFirePoints); + Dirty(uid, comp); + UpdateFirePointsAlert(uid, comp); + } + + private void UpdateFirePointsAlert(EntityUid uid, SCPFiremanComponent comp) + { + var severity = (short) Math.Clamp((int) MathF.Floor(comp.FirePoints / 10f), 0, 10); + _alerts.ShowAlert(uid, FirePointsAlert, severity); + } + + private void HealOwner(EntityUid owner, float perType) + { + if (!TryComp(owner, out var damageable)) + return; + + var healing = new DamageSpecifier(); + foreach (var damageType in damageable.Damage.DamageDict.Keys) + { + healing.DamageDict[damageType] = FixedPoint2.New(-perType); + } + + if (!healing.Empty) + _damageable.TryChangeDamage(owner, healing, ignoreResistances: true, interruptsDoAfters: false); + } + + private void SpreadFire(EntityUid source, EntityUid owner, int maxSpreadPerPulse) + { + if (!TryGetMapCoordinates(source, out var sourceCoords)) + return; + + var spawned = 0; + + var offset = _random.Next(4); + for (var i = 0; i < 4; i++) + { + if (spawned >= maxSpreadPerPulse) + break; + + var dirIndex = (offset + i) % 4; + var direction = dirIndex switch + { + 0 => Vector2.UnitX, + 1 => -Vector2.UnitX, + 2 => Vector2.UnitY, + _ => -Vector2.UnitY, + }; + + var candidate = new MapCoordinates(sourceCoords.Position + direction, sourceCoords.MapId); + + if (!IsValidFireTile(candidate)) + continue; + + SpawnFireAtMap(candidate, owner); + spawned++; + } + } + + private void EmitSecondModeFire(EntityUid owner, MapCoordinates start, MapCoordinates target) + { + var delta = target.Position - start.Position; + var length = delta.Length(); + + if (length <= 0.01f) + return; + + var direction = Vector2.Normalize(delta); + var distance = Math.Min(length, SecondModeRange); + var steps = Math.Max(1, (int) MathF.Ceiling(distance)); + + for (var i = 1; i <= steps; i++) + { + var position = start.Position + direction * i; + SpawnFireAtMap(new MapCoordinates(position, start.MapId), owner); + } + } + + private void SpawnFireAtEntity(EntityUid target, EntityUid owner) + { + if (!TryGetMapCoordinates(target, out var coords)) + return; + + SpawnFireAtMap(coords, owner); + } + + private void SpawnFireAtMap(MapCoordinates coords, EntityUid owner) + { + if (TryComp(owner, out var fireman) && GetOwnedFireCount(owner) >= fireman.MaxOwnedFires) + return; + + if (!IsValidFireTile(coords)) + return; + + var fire = Spawn("SCPFiremanFlame", coords); + if (TryComp(fire, out var spread)) + { + spread.FireOwner = owner; + IncrementOwnedFire(owner); + } + } + + private int GetOwnedFireCount(EntityUid owner) + { + if (_ownedFireCounts.TryGetValue(owner, out var cached)) + return cached; + + var count = 0; + var query = EntityQueryEnumerator(); + while (query.MoveNext(out _, out var spread)) + { + if (spread.FireOwner == owner) + count++; + } + + _ownedFireCounts[owner] = count; + return count; + } + + private void IncrementOwnedFire(EntityUid owner) + { + if (owner == EntityUid.Invalid) + return; + + _ownedFireCounts.TryGetValue(owner, out var count); + _ownedFireCounts[owner] = count + 1; + } + + private void DecrementOwnedFire(EntityUid owner) + { + if (owner == EntityUid.Invalid) + return; + + if (!_ownedFireCounts.TryGetValue(owner, out var count)) + return; + + count = Math.Max(0, count - 1); + if (count == 0) + _ownedFireCounts.Remove(owner); + else + _ownedFireCounts[owner] = count; + } + + private void IgniteEntitiesInRadius(MapCoordinates center, float radius, EntityUid source) + { + _nearFlammables.Clear(); + _lookup.GetEntitiesInRange(center, radius, _nearFlammables, LookupFlags.Dynamic | LookupFlags.Static | LookupFlags.Sundries); + + foreach (var (uid, flammable) in _nearFlammables) + { + _flammable.AdjustFireStacks(uid, 1.5f, flammable); + _flammable.Ignite(uid, source, flammable); + } + } + + private bool IsValidFireTile(MapCoordinates coords) + { + if (IsBlockedBySolidTag(coords)) + return false; + + if (IsWaterAt(coords)) + return false; + + _nearFires.Clear(); + _lookup.GetEntitiesInRange(coords, 0.3f, _nearFires, LookupFlags.Static | LookupFlags.Sundries); + if (_nearFires.Count > 0) + return false; + + return true; + } + + private bool IsBlockedBySolidTag(MapCoordinates coords) + { + _nearStructures.Clear(); + _lookup.GetEntitiesInRange(coords, 0.45f, _nearStructures, LookupFlags.Static | LookupFlags.Sundries); + + foreach (var (uid, xform) in _nearStructures) + { + if (!xform.Anchored) + continue; + + if (_tag.HasTag(uid, WallTag) || _tag.HasTag(uid, WindowTag) || _tag.HasTag(uid, GateTag)) + return true; + } + + return false; + } + + private bool IsWaterAt(MapCoordinates coords) + { + _nearPuddles.Clear(); + _lookup.GetEntitiesInRange(coords, 2f, _nearPuddles, LookupFlags.Static | LookupFlags.Sundries); + + foreach (var (uid, puddle) in _nearPuddles) + { + if (!_solutions.TryGetSolution(uid, puddle.SolutionName, out _, out var solution)) + continue; + + if (solution.ContainsReagent(WaterReagent) || solution.ContainsReagent(SpaceCleanerReagent)) + return true; + } + + return false; + } + + private bool TryGetMapCoordinates(EntityUid uid, out MapCoordinates mapCoordinates) + { + var xform = Transform(uid); + if (xform.MapUid is null) + { + mapCoordinates = default; + return false; + } + + mapCoordinates = xform.Coordinates.ToMap(EntityManager, _transform); + return true; + } +} diff --git a/Content.Server/Imperial/Sanity/Components/SanityComponent.cs b/Content.Server/Imperial/Sanity/Components/SanityComponent.cs new file mode 100644 index 00000000000..45bf9db4e26 --- /dev/null +++ b/Content.Server/Imperial/Sanity/Components/SanityComponent.cs @@ -0,0 +1,199 @@ +using Content.Shared.Prototypes; +using Robust.Shared.Audio; + +namespace Content.Server.Imperial.Sanity.Components; + +[RegisterComponent] +public sealed partial class SanityComponent : Component +{ + [DataField("startSanity")] + public float StartSanity = 50f; + + [DataField("minSanity")] + public float MinSanity = 1f; + + [DataField("maxSanity")] + public float MaxSanity = 100f; + + [DataField("highThreshold")] + public float HighThreshold = 80f; + + [DataField("lowThreshold")] + public float LowThreshold = 10f; + + [ViewVariables(VVAccess.ReadOnly)] + public float Value = 50f; + + [DataField("updateInterval")] + public TimeSpan UpdateInterval = TimeSpan.FromSeconds(1); + + [DataField("proximityInterval")] + public TimeSpan ProximityInterval = TimeSpan.FromSeconds(2); + + [DataField("silenceTimeout")] + public TimeSpan SilenceTimeout = TimeSpan.FromSeconds(90); + + [DataField("silencePenaltyInterval")] + public TimeSpan SilencePenaltyInterval = TimeSpan.FromSeconds(10); + + [DataField("coffeeGainCooldown")] + public TimeSpan CoffeeGainCooldown = TimeSpan.FromSeconds(20); + + [DataField("initialLowSoundDelay")] + public TimeSpan InitialLowSoundDelay = TimeSpan.FromSeconds(8); + + [DataField("initialHighRegenDelay")] + public TimeSpan InitialHighRegenDelay = TimeSpan.FromSeconds(1); + + [DataField("highWalkMultiplier")] + public float HighWalkMultiplier = 1.05f; + + [DataField("highSprintMultiplier")] + public float HighSprintMultiplier = 1.05f; + + [DataField("lowWalkMultiplier")] + public float LowWalkMultiplier = 0.88f; + + [DataField("lowSprintMultiplier")] + public float LowSprintMultiplier = 0.88f; + + [DataField("highHungerDecayMultiplier")] + public float HighHungerDecayMultiplier = 0.92f; + + [DataField("highThirstDecayMultiplier")] + public float HighThirstDecayMultiplier = 0.92f; + + [DataField("highBloodUpdateMultiplier")] + public float HighBloodUpdateMultiplier = 0.90f; + + [DataField("lowHungerDecayMultiplier")] + public float LowHungerDecayMultiplier = 1.25f; + + [DataField("lowThirstDecayMultiplier")] + public float LowThirstDecayMultiplier = 1.25f; + + [DataField("lowBloodUpdateMultiplier")] + public float LowBloodUpdateMultiplier = 1.20f; + + [DataField("damageLossPerPoint")] + public float DamageLossPerPoint = 0.08f; + + [DataField("damageLossCap")] + public float DamageLossCap = 5f; + + [DataField("dialogueGain")] + public float DialogueGain = 0.6f; + + [DataField("eatGain")] + public float EatGain = 1.2f; + + [DataField("drinkGain")] + public float DrinkGain = 0.8f; + + [DataField("coffeeGain")] + public float CoffeeGain = 1.5f; + + [DataField("nearHumanGain")] + public float NearHumanGain = 0.35f; + + [DataField("corpseLoss")] + public float CorpseLoss = 0.9f; + + [DataField("nearNdaGain")] + public float NearNdaGain = 1.0f; + + [DataField("highHungerPerTick")] + public float HighHungerPerTick = 0.01f; + + [DataField("highThirstPerTick")] + public float HighThirstPerTick = 0.06f; + + [DataField("highBloodPerTick")] + public float HighBloodPerTick = 0.03f; + + [DataField("lowHungerPerTick")] + public float LowHungerPerTick = -0.12f; + + [DataField("lowThirstPerTick")] + public float LowThirstPerTick = -0.26f; + + [DataField("lowBloodPerTick")] + public float LowBloodPerTick = -0.02f; + + [DataField("silenceLoss")] + public float SilenceLoss = 1.0f; + + [DataField("highRegenPerType")] + public float HighRegenPerType = 0.03f; + + [DataField("nearHumanRadius")] + public float NearHumanRadius = 4f; + + [DataField("corpseRadius")] + public float CorpseRadius = 8f; + + [DataField("ndaRadius")] + public float NdaRadius = 8f; + + [DataField("scpProximityRadius")] + public float ScpProximityRadius = 8f; + + [DataField("scpFriendlyGain")] + public float ScpFriendlyGain = 0.5f; + + [DataField("scpHostileLoss")] + public float ScpHostileLoss = 1f; + + [DataField("seenScpLoss")] + public float SeenScpLoss = 1f; + + [DataField("lowSoundMinInterval")] + public TimeSpan LowSoundMinInterval = TimeSpan.FromSeconds(12); + + [DataField("lowSoundMaxInterval")] + public TimeSpan LowSoundMaxInterval = TimeSpan.FromSeconds(25); + + [DataField("lowSanitySounds")] + public List LowSanitySounds = + [ + new SoundPathSpecifier("/Audio/Imperial/Seriozha/Fredik21/reason/breath.ogg"), + new SoundPathSpecifier("/Audio/Imperial/Seriozha/Fredik21/reason/heart.ogg") + ]; + + [ViewVariables] + public TimeSpan NextUpdate; + + [ViewVariables] + public TimeSpan NextProximityCheck; + + [ViewVariables] + public TimeSpan LastDialogueTime; + + [ViewVariables] + public TimeSpan NextSilencePenalty; + + [ViewVariables] + public TimeSpan NextCoffeeGain; + + [ViewVariables] + public TimeSpan NextLowSound; + + [ViewVariables] + public TimeSpan NextHighRegenTick; + + [ViewVariables] + public float LastHunger; + + [ViewVariables] + public float LastThirst; + + [ViewVariables] + public SanityState State = SanityState.Normal; +} + +public enum SanityState : byte +{ + Normal, + High, + Low, +} diff --git a/Content.Server/Imperial/Sanity/Systems/SanitySystem.cs b/Content.Server/Imperial/Sanity/Systems/SanitySystem.cs new file mode 100644 index 00000000000..424f821ce7e --- /dev/null +++ b/Content.Server/Imperial/Sanity/Systems/SanitySystem.cs @@ -0,0 +1,535 @@ +using Content.Server.Imperial.Sanity.Components; +using Content.Server.Body.Systems; +using Content.Server.Damage.Systems; +using Content.Server.Popups; +using Content.Shared.Alert; +using Content.Shared.Body.Components; +using Content.Shared.Chat; +using Content.Shared.Chemistry.EntitySystems; +using Content.Shared.Damage; +using Content.Shared.Damage.Components; +using Content.Shared.Damage.Systems; +using Content.Shared.FixedPoint; +using Content.Shared.Humanoid; +using Content.Shared.Interaction; +using Content.Shared.Mobs; +using Content.Shared.Mobs.Components; +using Content.Shared.Movement.Systems; +using Content.Shared.NPC.Components; +using Content.Shared.Nutrition.Components; +using Content.Shared.Nutrition.EntitySystems; +using Robust.Server.GameObjects; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Player; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; +using Robust.Shared.Timing; + +namespace Content.Server.Imperial.Sanity.Systems; + +public sealed class SanitySystem : EntitySystem +{ + private static readonly ProtoId _sanityAlert = "Sanity"; + private const string ScpBaseParent = "ImperialSCPBase"; + private const string ScpPresetParent = "ImperialSCPBasePreset"; + + private static readonly HashSet _friendlyScpIds = + [ + "ImperialSCPNDA131", + "ImperialSCPNDA131A", + "ImperialSCPJelly", + ]; + + [Dependency] private readonly AlertsSystem _alerts = default!; + [Dependency] private readonly DamageableSystem _damageable = default!; + [Dependency] private readonly EntityLookupSystem _lookup = default!; + [Dependency] private readonly HungerSystem _hunger = default!; + [Dependency] private readonly ThirstSystem _thirst = default!; + [Dependency] private readonly BloodstreamSystem _bloodstream = default!; + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly MovementSpeedModifierSystem _movement = default!; + [Dependency] private readonly PopupSystem _popup = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedInteractionSystem _interaction = default!; + [Dependency] private readonly IPrototypeManager _prototypes = default!; + [Dependency] private readonly SharedSolutionContainerSystem _solutions = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnMapInit); + SubscribeLocalEvent(OnShutdown); + SubscribeLocalEvent(OnDamageChanged); + SubscribeLocalEvent(OnEntitySpoke); + SubscribeLocalEvent(OnRefreshMovement); + } + + private void OnMapInit(Entity ent, ref MapInitEvent args) + { + var now = _timing.CurTime; + ent.Comp.Value = Math.Clamp(ent.Comp.StartSanity, ent.Comp.MinSanity, ent.Comp.MaxSanity); + ent.Comp.NextUpdate = now + ent.Comp.UpdateInterval; + ent.Comp.NextProximityCheck = now + ent.Comp.ProximityInterval; + ent.Comp.LastDialogueTime = now; + ent.Comp.NextSilencePenalty = now + ent.Comp.SilencePenaltyInterval; + ent.Comp.NextCoffeeGain = now; + ent.Comp.NextLowSound = now + ent.Comp.InitialLowSoundDelay; + ent.Comp.NextHighRegenTick = now + ent.Comp.InitialHighRegenDelay; + + if (TryComp(ent, out var hungerComp)) + ent.Comp.LastHunger = _hunger.GetHunger(hungerComp); + + if (TryComp(ent, out var thirstComp)) + ent.Comp.LastThirst = thirstComp.CurrentThirst; + + UpdateState(ent); + UpdateAlert(ent.Owner, ent.Comp); + } + + private void OnShutdown(Entity ent, ref ComponentShutdown args) + { + _alerts.ClearAlert(ent.Owner, _sanityAlert); + } + + private void OnDamageChanged(Entity ent, ref DamageChangedEvent args) + { + if (!args.DamageIncreased || args.DamageDelta is null) + return; + + var total = 0f; + foreach (var delta in args.DamageDelta.DamageDict.Values) + { + if (delta <= 0) + continue; + + total += (float) delta; + } + + if (total <= 0f) + return; + + var loss = Math.Clamp(total * ent.Comp.DamageLossPerPoint, 0f, ent.Comp.DamageLossCap); + ModifySanity(ent, -loss); + } + + private void OnEntitySpoke(Entity ent, ref EntitySpokeEvent args) + { + if (args.Source != ent.Owner) + return; + + ent.Comp.LastDialogueTime = _timing.CurTime; + ModifySanity(ent, ent.Comp.DialogueGain); + } + + private void OnRefreshMovement(Entity ent, ref RefreshMovementSpeedModifiersEvent args) + { + if (ent.Comp.State == SanityState.High) + { + args.ModifySpeed(ent.Comp.HighWalkMultiplier, ent.Comp.HighSprintMultiplier); + return; + } + + if (ent.Comp.State == SanityState.Low) + args.ModifySpeed(ent.Comp.LowWalkMultiplier, ent.Comp.LowSprintMultiplier); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var now = _timing.CurTime; + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var sanity, out var mobState)) + { + if (mobState.CurrentState == MobState.Dead) + continue; + + if (now < sanity.NextUpdate) + continue; + + sanity.NextUpdate = now + sanity.UpdateInterval; + + ProcessNutritionChanges((uid, sanity)); + ProcessCoffeeGain((uid, sanity), now); + ProcessSilencePenalty((uid, sanity), now); + + if (now >= sanity.NextProximityCheck) + { + sanity.NextProximityCheck = now + sanity.ProximityInterval; + ProcessProximity((uid, sanity)); + } + + UpdateState((uid, sanity)); + ApplyNeedsAndBloodEffects((uid, sanity)); + ProcessHighSanityRegen((uid, sanity), now); + ProcessLowSanitySound((uid, sanity), now); + UpdateAlert(uid, sanity); + } + } + + private void ProcessNutritionChanges(Entity ent) + { + if (TryComp(ent, out var hungerComp)) + { + var current = _hunger.GetHunger(hungerComp); + var delta = current - ent.Comp.LastHunger; + if (delta >= 2f) + ModifySanity(ent, ent.Comp.EatGain); + + ent.Comp.LastHunger = current; + } + + if (TryComp(ent, out var thirstComp)) + { + var delta = thirstComp.CurrentThirst - ent.Comp.LastThirst; + if (delta >= 2f) + ModifySanity(ent, ent.Comp.DrinkGain); + + ent.Comp.LastThirst = thirstComp.CurrentThirst; + } + } + + private void ProcessCoffeeGain(Entity ent, TimeSpan now) + { + if (now < ent.Comp.NextCoffeeGain) + return; + + if (!TryComp(ent, out var bloodstream)) + return; + + if (!_solutions.TryGetSolution(ent.Owner, bloodstream.ChemicalSolutionName, out _, out var solution)) + return; + + foreach (var reagent in solution.Contents) + { + if (!IsCoffeeLikeReagent(reagent.Reagent.Prototype)) + continue; + + ModifySanity(ent, ent.Comp.CoffeeGain); + ent.Comp.NextCoffeeGain = now + ent.Comp.CoffeeGainCooldown; + return; + } + } + + private void ProcessSilencePenalty(Entity ent, TimeSpan now) + { + if (now < ent.Comp.NextSilencePenalty) + return; + + ent.Comp.NextSilencePenalty = now + ent.Comp.SilencePenaltyInterval; + if (now - ent.Comp.LastDialogueTime < ent.Comp.SilenceTimeout) + return; + + ModifySanity(ent, -ent.Comp.SilenceLoss); + } + + private void ProcessProximity(Entity ent) + { + if (IsNearLivingHumanoid(ent.Owner, ent.Comp.NearHumanRadius)) + ModifySanity(ent, ent.Comp.NearHumanGain); + + ProcessScpProximity(ent); + + var sawCorpse = IsCorpseNearVisible(ent.Owner, ent.Comp.CorpseRadius); + var sawScp = IsScpNearVisible(ent.Owner, ent.Comp.ScpProximityRadius); + + if (sawCorpse) + ModifySanity(ent, -ent.Comp.CorpseLoss); + + if (sawScp) + ModifySanity(ent, -ent.Comp.SeenScpLoss); + + if (IsNdaObjectNear(ent.Owner, ent.Comp.NdaRadius)) + ModifySanity(ent, ent.Comp.NearNdaGain); + } + + private void ProcessScpProximity(Entity ent) + { + var hasFriendly = false; + var hasHostile = false; + + foreach (var nearby in _lookup.GetEntitiesInRange(ent.Owner, ent.Comp.ScpProximityRadius, LookupFlags.Dynamic)) + { + if (nearby == ent.Owner) + continue; + + if (!TryComp(nearby, out var state) || state.CurrentState == MobState.Dead) + continue; + + if (!TryComp(nearby, out var meta)) + continue; + + var protoId = meta.EntityPrototype?.ID; + if (string.IsNullOrWhiteSpace(protoId)) + continue; + + if (_friendlyScpIds.Contains(protoId)) + { + hasFriendly = true; + continue; + } + + if (PrototypeInherits(protoId, ScpPresetParent)) + { + hasFriendly = true; + continue; + } + + if (PrototypeInherits(protoId, ScpBaseParent)) + hasHostile = true; + } + + if (hasFriendly) + ModifySanity(ent, ent.Comp.ScpFriendlyGain); + + if (hasHostile) + ModifySanity(ent, -ent.Comp.ScpHostileLoss); + } + + private void UpdateState(Entity ent) + { + var old = ent.Comp.State; + ent.Comp.State = ent.Comp.Value > ent.Comp.HighThreshold + ? SanityState.High + : ent.Comp.Value < ent.Comp.LowThreshold + ? SanityState.Low + : SanityState.Normal; + + if (old != ent.Comp.State) + _movement.RefreshMovementSpeedModifiers(ent); + } + + private void ApplyNeedsAndBloodEffects(Entity ent) + { + if (ent.Comp.State == SanityState.High) + { + if (TryComp(ent, out var hungerComp)) + _hunger.ModifyHunger(ent.Owner, ent.Comp.HighHungerPerTick, hungerComp); + + if (TryComp(ent, out var thirstComp)) + _thirst.ModifyThirst(ent.Owner, thirstComp, ent.Comp.HighThirstPerTick); + + if (TryComp(ent, out var bloodComp)) + _bloodstream.TryModifyBloodLevel((ent.Owner, bloodComp), FixedPoint2.New(ent.Comp.HighBloodPerTick)); + + return; + } + + if (ent.Comp.State == SanityState.Low) + { + if (TryComp(ent, out var hungerComp)) + _hunger.ModifyHunger(ent.Owner, ent.Comp.LowHungerPerTick, hungerComp); + + if (TryComp(ent, out var thirstComp)) + _thirst.ModifyThirst(ent.Owner, thirstComp, ent.Comp.LowThirstPerTick); + + if (TryComp(ent, out var bloodComp)) + _bloodstream.TryModifyBloodLevel((ent.Owner, bloodComp), FixedPoint2.New(ent.Comp.LowBloodPerTick)); + } + } + + private static bool IsCoffeeLikeReagent(string reagentId) + { + return reagentId.Contains("Coffee", StringComparison.OrdinalIgnoreCase) + || reagentId.Contains("Caffeine", StringComparison.OrdinalIgnoreCase) + || reagentId.Contains("Espresso", StringComparison.OrdinalIgnoreCase) + || reagentId.Contains("Latte", StringComparison.OrdinalIgnoreCase) + || reagentId.Contains("Cappuccino", StringComparison.OrdinalIgnoreCase) + || reagentId.Contains("Americano", StringComparison.OrdinalIgnoreCase) + || reagentId.Contains("Mocha", StringComparison.OrdinalIgnoreCase) + || reagentId.Contains("Macchiato", StringComparison.OrdinalIgnoreCase); + } + + private void ProcessHighSanityRegen(Entity ent, TimeSpan now) + { + if (ent.Comp.State != SanityState.High) + return; + + if (now < ent.Comp.NextHighRegenTick) + return; + + ent.Comp.NextHighRegenTick = now + TimeSpan.FromSeconds(1); + + if (!TryComp(ent, out var damageable)) + return; + + var healing = new DamageSpecifier(); + foreach (var kv in damageable.Damage.DamageDict) + { + if (kv.Value == FixedPoint2.Zero) + continue; + + healing.DamageDict[kv.Key] = FixedPoint2.New(-ent.Comp.HighRegenPerType); + } + + if (!healing.Empty) + _damageable.TryChangeDamage(ent.Owner, healing, ignoreResistances: true, interruptsDoAfters: false); + } + + private void ProcessLowSanitySound(Entity ent, TimeSpan now) + { + if (ent.Comp.State != SanityState.Low) + return; + + if (now < ent.Comp.NextLowSound || ent.Comp.LowSanitySounds.Count == 0) + return; + + var sound = _random.Pick(ent.Comp.LowSanitySounds); + _audio.PlayEntity(sound, Filter.Entities(ent.Owner), ent.Owner, true); + + var min = ent.Comp.LowSoundMinInterval.TotalSeconds; + var max = ent.Comp.LowSoundMaxInterval.TotalSeconds; + var next = _random.NextFloat((float) min, (float) max); + ent.Comp.NextLowSound = now + TimeSpan.FromSeconds(next); + } + + private bool IsNearLivingHumanoid(EntityUid uid, float radius) + { + foreach (var ent in _lookup.GetEntitiesInRange(uid, radius, LookupFlags.Dynamic)) + { + if (ent == uid) + continue; + + if (!HasComp(ent)) + continue; + + if (!TryComp(ent, out var state) || state.CurrentState != MobState.Alive) + continue; + + if (!_interaction.InRangeUnobstructed(uid, ent, radius + 0.1f)) + continue; + + return true; + } + + return false; + } + + private bool IsCorpseNearVisible(EntityUid uid, float radius) + { + foreach (var ent in _lookup.GetEntitiesInRange(uid, radius, LookupFlags.Dynamic)) + { + if (!TryComp(ent, out var state) || state.CurrentState != MobState.Dead) + continue; + + if (!_interaction.InRangeUnobstructed(uid, ent, radius + 0.1f)) + continue; + + return true; + } + + return false; + } + + private bool IsNdaObjectNear(EntityUid uid, float radius) + { + foreach (var ent in _lookup.GetEntitiesInRange(uid, radius, LookupFlags.Dynamic)) + { + if (ent == uid) + continue; + + if (!_interaction.InRangeUnobstructed(uid, ent, radius + 0.1f)) + continue; + + if (TryComp(ent, out var meta) && + meta.EntityPrototype?.ID.Contains("NDA", StringComparison.OrdinalIgnoreCase) == true) + { + return true; + } + + if (TryComp(ent, out var faction)) + { + foreach (var id in faction.Factions) + { + if (!id.Id.Contains("NDA", StringComparison.OrdinalIgnoreCase)) + continue; + + return true; + } + } + } + + return false; + } + + private bool IsScpNearVisible(EntityUid uid, float radius) + { + foreach (var ent in _lookup.GetEntitiesInRange(uid, radius, LookupFlags.Dynamic)) + { + if (ent == uid) + continue; + + if (!TryComp(ent, out var state) || state.CurrentState == MobState.Dead) + continue; + + if (!TryComp(ent, out var meta)) + continue; + + var protoId = meta.EntityPrototype?.ID; + if (string.IsNullOrWhiteSpace(protoId)) + continue; + + // Skip friendly SCPs — they are handled by ProcessScpProximity + if (_friendlyScpIds.Contains(protoId) || PrototypeInherits(protoId, ScpPresetParent)) + continue; + + if (!PrototypeInherits(protoId, ScpBaseParent)) + continue; + + if (!_interaction.InRangeUnobstructed(uid, ent, radius + 0.1f)) + continue; + + return true; + } + + return false; + } + + private bool PrototypeInherits(string prototypeId, string parentId, int depth = 0) + { + if (depth > 16) + return false; + + if (prototypeId.Equals(parentId, StringComparison.Ordinal)) + return true; + + if (!_prototypes.TryIndex(prototypeId, out var prototype) || prototype.Parents is null) + return false; + + foreach (var parent in prototype.Parents) + { + if (parent.Equals(parentId, StringComparison.Ordinal)) + return true; + + if (PrototypeInherits(parent, parentId, depth + 1)) + return true; + } + + return false; + } + + private void ModifySanity(Entity ent, float delta) + { + if (MathF.Abs(delta) <= 0.0001f) + return; + + var old = ent.Comp.Value; + ent.Comp.Value = Math.Clamp(ent.Comp.Value + delta, ent.Comp.MinSanity, ent.Comp.MaxSanity); + if (MathF.Abs(old - ent.Comp.Value) <= 0.0001f) + return; + + if (ent.Comp.Value <= ent.Comp.LowThreshold && old > ent.Comp.LowThreshold) + _popup.PopupEntity(Loc.GetString("sanity-low-warning"), ent, ent); + + if (ent.Comp.Value >= ent.Comp.HighThreshold && old < ent.Comp.HighThreshold) + _popup.PopupEntity(Loc.GetString("sanity-high-feeling"), ent, ent); + } + + private void UpdateAlert(EntityUid uid, SanityComponent comp) + { + var severity = (short) Math.Clamp((int) MathF.Floor(comp.Value / comp.MaxSanity * 10f), 0, 10); + _alerts.ShowAlert(uid, _sanityAlert, severity); + } +} diff --git a/Content.Server/Imperial/Seriozha/Administration/Commands/NedraNukeProtocolCommand.cs b/Content.Server/Imperial/Seriozha/Administration/Commands/NedraNukeProtocolCommand.cs new file mode 100644 index 00000000000..2752d60e835 --- /dev/null +++ b/Content.Server/Imperial/Seriozha/Administration/Commands/NedraNukeProtocolCommand.cs @@ -0,0 +1,288 @@ +using System.Globalization; +using Content.Server.Administration; +using Content.Server.Audio; +using Content.Server.Doors.Systems; +using Content.Server.Explosion.EntitySystems; +using Content.Server.Chat.Systems; +using Content.Server.Light.EntitySystems; +using Content.Shared.Administration; +using Content.Shared.Damage.Systems; +using Content.Shared.Doors.Components; +using Robust.Server.GameObjects; +using Robust.Shared.Audio; +using Robust.Shared.Console; +using Robust.Shared.Map; +using Robust.Shared.Map.Components; +using Robust.Shared.Maths; +using Robust.Shared.Player; +using Robust.Shared.Timing; + +namespace Content.Server.Imperial.Seriozha.Administration.Commands; + +[AdminCommand(AdminFlags.Fun)] +public sealed class NedraNukeProtocolCommand : IConsoleCommand +{ + [Dependency] private readonly IEntityManager _entManager = default!; + [Dependency] private readonly IMapManager _mapManager = default!; + + private const string NukeMusicPath = "/Audio/Imperial/Seriozha/SCP/event/nuke.ogg"; + private const int DefaultPhaseSeconds = 125; + private const string CassieSender = "C.A.S.S.I.E"; + private const float ExplosionIntensity = 900f; + private const float ExplosionSlope = 1f; + private const float ExplosionMaxTileIntensity = 32f; + + public string Command => "nedranukeprotocol"; + public string Description => "Starts Nedra nuke protocol sequence on a map."; + public string Help => "nedranukeprotocol [mapId]"; + + public void Execute(IConsoleShell shell, string argStr, string[] args) + { + var phaseSeconds = DefaultPhaseSeconds; + + MapId? targetMap = null; + if (args.Length >= 1) + { + if (!int.TryParse(args[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out var mapInt)) + { + shell.WriteError($"Invalid map id: {args[0]}"); + return; + } + + var mapId = new MapId(mapInt); + if (!_mapManager.MapExists(mapId)) + { + shell.WriteError($"Map with id {mapInt} does not exist."); + return; + } + + targetMap = mapId; + } + + if (targetMap == null) + { + if (shell.Player is not { AttachedEntity: { } attached }) + { + shell.WriteError("No map specified and unable to infer executor map."); + return; + } + + if (!_entManager.TryGetComponent(attached, out TransformComponent? xform)) + { + shell.WriteError("No map specified and unable to infer executor map."); + return; + } + + targetMap = xform.MapID; + } + + if (!_mapManager.MapExists(targetMap.Value)) + { + shell.WriteError($"Map with id {(int) targetMap.Value} does not exist."); + return; + } + + var mapUid = _mapManager.GetMapEntityId(targetMap.Value); + if (!mapUid.IsValid()) + { + shell.WriteError("Unable to resolve map entity."); + return; + } + + var protocolAirlocks = new HashSet(); + + SetMapAmbientColor(mapUid, Color.FromHex("#ff0000")); + SetAllPointLightsColor(targetMap.Value, Color.FromHex("#ff0000")); + PlayMapSound(targetMap.Value, NukeMusicPath); + SendCassieAnnouncement(phaseSeconds); + OpenHermeticsAndAirlocks(targetMap.Value, protocolAirlocks); + + shell.WriteLine($"Nedra protocol started on map {(int) targetMap.Value}. Detonation phase in {phaseSeconds} seconds."); + + Timer.Spawn(phaseSeconds * 1000, () => + { + if (_entManager.Deleted(mapUid)) + return; + + var centcommHermetics = CloseCentcommHermeticsAndEnableGodmode(targetMap.Value); + + TriggerExplosionSpawners(targetMap.Value); + + SetMapAmbientColor(mapUid, Color.FromHex("#000000")); + SetAllPointLightsColor(targetMap.Value, Color.White); + UnboltAirlocks(protocolAirlocks); + DisableGodmode(centcommHermetics); + }); + } + + private void SendCassieAnnouncement(int detonationSeconds) + { + var chat = _entManager.System(); + var message = $"По решению совета O5, запущен протокол \"Мёртвая рука\". Детонация боеголовок произойдет через... {detonationSeconds} секунд. Объявлена эвакуация."; + chat.DispatchGlobalAnnouncement(message, CassieSender, playSound: true, colorOverride: Color.Gold); + } + + private void PlayMapSound(MapId mapId, string soundPath) + { + var audio = AudioParams.Default.AddVolume(-8); + var filter = Filter.BroadcastMap(mapId); + _entManager.System().PlayAdminGlobal(filter, soundPath, audio, true); + } + + private void SetMapAmbientColor(EntityUid mapUid, Color color) + { + var light = _entManager.EnsureComponent(mapUid); + light.AmbientLightColor = color; + _entManager.Dirty(mapUid, light); + } + + private void SetAllPointLightsColor(MapId mapId, Color color) + { + var pointLightSystem = _entManager.System(); + var query = _entManager.EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var pointLight, out var xform)) + { + if (xform.MapID != mapId) + continue; + + pointLightSystem.SetColor(uid, color, pointLight); + } + } + + private void OpenHermeticsAndAirlocks(MapId mapId, HashSet protocolAirlocks) + { + var doorSystem = _entManager.System(); + var query = _entManager.EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var door, out var xform)) + { + if (xform.MapID != mapId) + continue; + + if (!IsHermeticOrAirlock(uid)) + continue; + + if (_entManager.TryGetComponent(uid, out var preBolts)) + doorSystem.SetBoltsDown((uid, preBolts), false); + + doorSystem.TryOpen(uid, door); + + if (!_entManager.HasComponent(uid)) + continue; + + if (_entManager.TryGetComponent(uid, out var bolts)) + doorSystem.SetBoltsDown((uid, bolts), true); + + protocolAirlocks.Add(uid); + } + } + + private HashSet CloseCentcommHermeticsAndEnableGodmode(MapId mapId) + { + var doorSystem = _entManager.System(); + var godmodeSystem = _entManager.System(); + var result = new HashSet(); + + var query = _entManager.EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var door, out var xform)) + { + if (xform.MapID != mapId) + continue; + + if (!IsCentcommHermetic(uid)) + continue; + + if (_entManager.TryGetComponent(uid, out var bolts)) + doorSystem.SetBoltsDown((uid, bolts), false); + + doorSystem.TryClose(uid, door); + godmodeSystem.EnableGodmode(uid); + result.Add(uid); + } + + return result; + } + + private void TriggerExplosionSpawners(MapId mapId) + { + var xformSystem = _entManager.System(); + var explosionSystem = _entManager.System(); + var query = _entManager.EntityQueryEnumerator(); + + while (query.MoveNext(out var uid, out var xform, out var meta)) + { + if (xform.MapID != mapId) + continue; + + var protoId = meta.EntityPrototype?.ID; + if (!string.Equals(protoId, "NedraNukeExplosionSpawner", StringComparison.Ordinal)) + continue; + + var coords = new MapCoordinates(xformSystem.GetWorldPosition(xform), xform.MapID); + explosionSystem.QueueExplosion( + coords, + ExplosionSystem.DefaultExplosionPrototypeId, + ExplosionIntensity, + ExplosionSlope, + ExplosionMaxTileIntensity, + null, + canCreateVacuum: true, + maxTileBreak: 0); + } + } + + private void UnboltAirlocks(HashSet airlocks) + { + var doorSystem = _entManager.System(); + foreach (var uid in airlocks) + { + if (!_entManager.EntityExists(uid)) + continue; + + if (!_entManager.TryGetComponent(uid, out var bolts)) + continue; + + doorSystem.SetBoltsDown((uid, bolts), false); + } + } + + private void DisableGodmode(HashSet entities) + { + var godmodeSystem = _entManager.System(); + foreach (var uid in entities) + { + if (!_entManager.EntityExists(uid)) + continue; + + godmodeSystem.DisableGodmode(uid); + } + } + + private bool IsHermeticOrAirlock(EntityUid uid) + { + if (_entManager.HasComponent(uid)) + return true; + + if (_entManager.HasComponent(uid)) + return true; + + var protoId = _entManager.GetComponent(uid).EntityPrototype?.ID; + if (protoId == null) + return false; + + return protoId.Contains("BlastDoor", StringComparison.OrdinalIgnoreCase) + || protoId.Contains("Shutter", StringComparison.OrdinalIgnoreCase); + } + + private bool IsCentcommHermetic(EntityUid uid) + { + var protoId = _entManager.GetComponent(uid).EntityPrototype?.ID; + if (protoId == null) + return false; + + if (!protoId.Contains("CentralCommand", StringComparison.OrdinalIgnoreCase)) + return false; + + return protoId.Contains("BlastDoor", StringComparison.OrdinalIgnoreCase) + || protoId.Contains("Shutter", StringComparison.OrdinalIgnoreCase); + } +} diff --git a/Content.Server/Imperial/TerrorSpider/Components/TerrorDronMeleeDebuffComponent.cs b/Content.Server/Imperial/TerrorSpider/Components/TerrorDronMeleeDebuffComponent.cs new file mode 100644 index 00000000000..05c5319c4a8 --- /dev/null +++ b/Content.Server/Imperial/TerrorSpider/Components/TerrorDronMeleeDebuffComponent.cs @@ -0,0 +1,11 @@ +namespace Content.Server.Imperial.TerrorSpider.Components; + +[RegisterComponent] +public sealed partial class TerrorDronMeleeDebuffComponent : Component +{ + [DataField] + public float SlowDuration = 4f; + + [DataField] + public float SlowMultiplier = 0.5f; +} diff --git a/Content.Server/Imperial/TerrorSpider/Components/TerrorDronProjectileDebuffComponent.cs b/Content.Server/Imperial/TerrorSpider/Components/TerrorDronProjectileDebuffComponent.cs new file mode 100644 index 00000000000..bdbc1920a24 --- /dev/null +++ b/Content.Server/Imperial/TerrorSpider/Components/TerrorDronProjectileDebuffComponent.cs @@ -0,0 +1,14 @@ +namespace Content.Server.Imperial.TerrorSpider.Components; + +[RegisterComponent] +public sealed partial class TerrorDronProjectileDebuffComponent : Component +{ + [DataField] + public float SlowDuration = 2f; + + [DataField] + public float SlowMultiplier = 0.5f; + + [DataField] + public float StaminaDamage = 15f; +} diff --git a/Content.Server/Imperial/TerrorSpider/Components/TerrorSpiderDestroyerComponent.cs b/Content.Server/Imperial/TerrorSpider/Components/TerrorSpiderDestroyerComponent.cs new file mode 100644 index 00000000000..9d362c778ff --- /dev/null +++ b/Content.Server/Imperial/TerrorSpider/Components/TerrorSpiderDestroyerComponent.cs @@ -0,0 +1,51 @@ +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; +using Robust.Shared.Audio; + +namespace Content.Server.Imperial.TerrorSpider.Components; + +[RegisterComponent] +public sealed partial class TerrorSpiderDestroyerComponent : Component +{ + [DataField] + public EntProtoId EmpScreamAction = "ActionTerrorSpiderDestroyerEmpScream"; + + [DataField] + public EntityUid? EmpScreamActionEntity; + + [DataField] + public EntProtoId FireBurstAction = "ActionTerrorSpiderDestroyerFireBurst"; + + [DataField] + public EntityUid? FireBurstActionEntity; + + [DataField] + public float EmpScreamRange = 3.5f; + + [DataField] + public float DeathEmpRange = 6.5f; + + [DataField] + public float EmpScreamEnergyConsumption = 120000f; + + [DataField] + public float EmpScreamDisableSeconds = 20f; + + [DataField] + public SoundSpecifier? EmpUseSound = new SoundPathSpecifier("/Audio/Effects/sparks4.ogg"); + + [DataField] + public SoundSpecifier? EmpDeathSound = new SoundPathSpecifier("/Audio/Effects/sparks4.ogg"); + + [ViewVariables] + public bool DeathEmpTriggered; + + [DataField] + public float FireBurstRadius = 3.5f; + + [DataField] + public float FireStacksPerHit = 4f; + + [DataField] + public float FireHeatDamagePerHit = 12f; +} diff --git a/Content.Server/Imperial/TerrorSpider/Components/TerrorSpiderGuardianLeashComponent.cs b/Content.Server/Imperial/TerrorSpider/Components/TerrorSpiderGuardianLeashComponent.cs new file mode 100644 index 00000000000..7bf4dbabece --- /dev/null +++ b/Content.Server/Imperial/TerrorSpider/Components/TerrorSpiderGuardianLeashComponent.cs @@ -0,0 +1,23 @@ +namespace Content.Server.Imperial.TerrorSpider.Components; + +[RegisterComponent] +public sealed partial class TerrorSpiderGuardianLeashComponent : Component +{ + [DataField] + public float RequiredRoyalRange = 8f; + + [DataField] + public float DamageIfFar = 6f; + + [DataField] + public float TickInterval = 1f; + + [DataField] + public float WarningCooldown = 4f; + + [ViewVariables] + public TimeSpan NextTickTime = TimeSpan.Zero; + + [ViewVariables] + public TimeSpan NextWarningTime = TimeSpan.Zero; +} diff --git a/Content.Server/Imperial/TerrorSpider/Components/TerrorSpiderHealerBlindWebComponent.cs b/Content.Server/Imperial/TerrorSpider/Components/TerrorSpiderHealerBlindWebComponent.cs new file mode 100644 index 00000000000..c838fb85287 --- /dev/null +++ b/Content.Server/Imperial/TerrorSpider/Components/TerrorSpiderHealerBlindWebComponent.cs @@ -0,0 +1,8 @@ +namespace Content.Server.Imperial.TerrorSpider.Components; + +[RegisterComponent] +public sealed partial class TerrorSpiderHealerBlindWebComponent : Component +{ + [DataField] + public float BlindDuration = 30f; +} diff --git a/Content.Server/Imperial/TerrorSpider/Components/TerrorSpiderHealerComponent.cs b/Content.Server/Imperial/TerrorSpider/Components/TerrorSpiderHealerComponent.cs new file mode 100644 index 00000000000..e7e3ebb9986 --- /dev/null +++ b/Content.Server/Imperial/TerrorSpider/Components/TerrorSpiderHealerComponent.cs @@ -0,0 +1,71 @@ +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; + +namespace Content.Server.Imperial.TerrorSpider.Components; + +[RegisterComponent] +public sealed partial class TerrorSpiderHealerComponent : Component +{ + [DataField] + public EntProtoId PulseAction = "ActionTerrorSpiderHealerPulse"; + + [DataField] + public EntityUid? PulseActionEntity; + + [DataField] + public EntProtoId EggRusarAction = "ActionTerrorSpiderHealerLayEggRusar"; + + [DataField] + public EntityUid? EggRusarActionEntity; + + [DataField] + public EntProtoId EggDronAction = "ActionTerrorSpiderHealerLayEggDron"; + + [DataField] + public EntityUid? EggDronActionEntity; + + [DataField] + public EntProtoId EggLurkerAction = "ActionTerrorSpiderHealerLayEggLurker"; + + [DataField] + public EntityUid? EggLurkerActionEntity; + + [DataField] + public EntProtoId EggHealerAction = "ActionTerrorSpiderHealerLayEggHealer"; + + [DataField] + public EntityUid? EggHealerActionEntity; + + [DataField] + public int SatietyLevel = 1; + + [DataField] + public int MaxSatietyLevel = 10; + + [DataField] + public int EggRequiredSatiety = 3; + + [DataField] + public int CocoonSatietyGain = 1; + + [DataField] + public float TouchHealLevel1 = 2f; + + [DataField] + public float TouchHealLevel2 = 4f; + + [DataField] + public float TouchHealLevel3 = 8f; + + [DataField] + public float PulseHealAmount = 20f; + + [DataField] + public float PulseRange = 13f; + + [DataField] + public float TouchHealCooldown = 1f; + + [ViewVariables] + public TimeSpan NextTouchHealTime = TimeSpan.Zero; +} diff --git a/Content.Server/Imperial/TerrorSpider/Components/TerrorSpiderLurkerComponent.cs b/Content.Server/Imperial/TerrorSpider/Components/TerrorSpiderLurkerComponent.cs new file mode 100644 index 00000000000..40fd67f2b1c --- /dev/null +++ b/Content.Server/Imperial/TerrorSpider/Components/TerrorSpiderLurkerComponent.cs @@ -0,0 +1,38 @@ +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; + +namespace Content.Server.Imperial.TerrorSpider.Components; + +[RegisterComponent] +public sealed partial class TerrorSpiderLurkerComponent : Component +{ + [DataField] + public EntProtoId StealthAction = "ActionTerrorSpiderLurkerStealth"; + + [DataField] + public EntityUid? StealthActionEntity; + + [DataField] + public float StealthDuration = 8f; + + [DataField] + public float BaseMeleeDamage = 15f; + + [DataField] + public float WebBuffMeleeDamage = 45f; + + [DataField] + public float WebBuffStaminaDamage = 45f; + + [DataField] + public float MinStealthVisibility = -10f; + + [ViewVariables] + public bool ActionStealthActive; + + [ViewVariables] + public TimeSpan ActionStealthEndTime; + + [ViewVariables] + public int WebContacts; +} diff --git a/Content.Server/Imperial/TerrorSpider/Components/TerrorSpiderLurkerWebAreaComponent.cs b/Content.Server/Imperial/TerrorSpider/Components/TerrorSpiderLurkerWebAreaComponent.cs new file mode 100644 index 00000000000..7c62acbb8c1 --- /dev/null +++ b/Content.Server/Imperial/TerrorSpider/Components/TerrorSpiderLurkerWebAreaComponent.cs @@ -0,0 +1,11 @@ +namespace Content.Server.Imperial.TerrorSpider.Components; + +[RegisterComponent] +public sealed partial class TerrorSpiderLurkerWebAreaComponent : Component +{ + [DataField] + public float StaminaDamage = 100f; + + [DataField] + public float MuteDuration = 14f; +} diff --git a/Content.Server/Imperial/TerrorSpider/Components/TerrorSpiderMotherComponent.cs b/Content.Server/Imperial/TerrorSpider/Components/TerrorSpiderMotherComponent.cs new file mode 100644 index 00000000000..c1068e4bbcd --- /dev/null +++ b/Content.Server/Imperial/TerrorSpider/Components/TerrorSpiderMotherComponent.cs @@ -0,0 +1,73 @@ +using Robust.Shared.Prototypes; + +namespace Content.Server.Imperial.TerrorSpider.Components; + +[RegisterComponent] +public sealed partial class TerrorSpiderMotherComponent : Component +{ + [DataField] + public EntProtoId PulseAction = "ActionTerrorSpiderMotherPulse"; + + [ViewVariables] + public EntityUid? PulseActionEntity; + + [DataField] + public EntProtoId RemoteViewNextAction = "ActionTerrorSpiderMotherRemoteViewNext"; + + [ViewVariables] + public EntityUid? RemoteViewNextActionEntity; + + [DataField] + public EntProtoId RemoteViewPreviousAction = "ActionTerrorSpiderMotherRemoteViewPrevious"; + + [ViewVariables] + public EntityUid? RemoteViewPreviousActionEntity; + + [DataField] + public EntProtoId RemoteViewExitAction = "ActionTerrorSpiderMotherRemoteViewExit"; + + [ViewVariables] + public EntityUid? RemoteViewExitActionEntity; + + [DataField] + public EntProtoId LayJellyAction = "ActionTerrorSpiderMotherLayJelly"; + + [ViewVariables] + public EntityUid? LayJellyActionEntity; + + [DataField] + public float AuraHalfRange = 7.5f; + + [DataField] + public float AuraHealAmount = 3f; + + [DataField] + public float AuraDamageAmount = 3f; + + [DataField] + public float AuraInterval = 2f; + + [DataField] + public float TouchHealAmount = 2f; + + [DataField] + public float PulseHealAmount = 30f; + + [DataField] + public float PulseRange = 13f; + + [DataField] + public EntProtoId JellyPrototype = "TerrorSpiderMotherJelly"; + + [DataField] + public EntProtoId RemoteViewImmobileStatusEffect = "TerrorSpiderMotherRemoteViewImmobileStatusEffect"; + + [DataField] + public float RemoteViewImmobileRefresh = 0.5f; + + [ViewVariables] + public int RemoteViewIndex = -1; + + [ViewVariables] + public TimeSpan NextAuraTick = TimeSpan.Zero; +} diff --git a/Content.Server/Imperial/TerrorSpider/Components/TerrorSpiderMotherJellyComponent.cs b/Content.Server/Imperial/TerrorSpider/Components/TerrorSpiderMotherJellyComponent.cs new file mode 100644 index 00000000000..5908ae7113f --- /dev/null +++ b/Content.Server/Imperial/TerrorSpider/Components/TerrorSpiderMotherJellyComponent.cs @@ -0,0 +1,17 @@ +namespace Content.Server.Imperial.TerrorSpider.Components; + +[RegisterComponent] +public sealed partial class TerrorSpiderMotherJellyComponent : Component +{ + [DataField] + public float BuffRange = 2f; + + [DataField] + public float BuffHealPerTick = 6f; + + [DataField] + public float BuffInterval = 1f; + + [DataField] + public float BuffDuration = 25f; +} diff --git a/Content.Server/Imperial/TerrorSpider/Components/TerrorSpiderMotherRegenBuffComponent.cs b/Content.Server/Imperial/TerrorSpider/Components/TerrorSpiderMotherRegenBuffComponent.cs new file mode 100644 index 00000000000..915a6993b4b --- /dev/null +++ b/Content.Server/Imperial/TerrorSpider/Components/TerrorSpiderMotherRegenBuffComponent.cs @@ -0,0 +1,17 @@ +namespace Content.Server.Imperial.TerrorSpider.Components; + +[RegisterComponent] +public sealed partial class TerrorSpiderMotherRegenBuffComponent : Component +{ + [DataField] + public float HealPerTick = 6f; + + [DataField] + public float TickInterval = 1f; + + [ViewVariables] + public TimeSpan NextTick = TimeSpan.Zero; + + [ViewVariables] + public TimeSpan ExpiresAt = TimeSpan.Zero; +} diff --git a/Content.Server/Imperial/TerrorSpider/Components/TerrorSpiderPrincessBroodComponent.cs b/Content.Server/Imperial/TerrorSpider/Components/TerrorSpiderPrincessBroodComponent.cs new file mode 100644 index 00000000000..3c475c78e91 --- /dev/null +++ b/Content.Server/Imperial/TerrorSpider/Components/TerrorSpiderPrincessBroodComponent.cs @@ -0,0 +1,11 @@ +namespace Content.Server.Imperial.TerrorSpider.Components; + +[RegisterComponent] +public sealed partial class TerrorSpiderPrincessBroodComponent : Component +{ + [DataField] + public EntityUid? Princess; + + [DataField] + public bool Elite; +} diff --git a/Content.Server/Imperial/TerrorSpider/Components/TerrorSpiderPrincessComponent.cs b/Content.Server/Imperial/TerrorSpider/Components/TerrorSpiderPrincessComponent.cs new file mode 100644 index 00000000000..67675fb4576 --- /dev/null +++ b/Content.Server/Imperial/TerrorSpider/Components/TerrorSpiderPrincessComponent.cs @@ -0,0 +1,129 @@ +using Robust.Shared.Audio; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; + +namespace Content.Server.Imperial.TerrorSpider.Components; + +[RegisterComponent] +public sealed partial class TerrorSpiderPrincessComponent : Component +{ + [DataField] + public EntProtoId RemoteViewNextAction = "ActionTerrorSpiderPrincessRemoteViewNext"; + + [DataField] + public EntityUid? RemoteViewNextActionEntity; + + [DataField] + public EntProtoId RemoteViewPreviousAction = "ActionTerrorSpiderPrincessRemoteViewPrevious"; + + [DataField] + public EntityUid? RemoteViewPreviousActionEntity; + + [DataField] + public EntProtoId RemoteViewExitAction = "ActionTerrorSpiderPrincessRemoteViewExit"; + + [DataField] + public EntityUid? RemoteViewExitActionEntity; + + [DataField] + public EntProtoId HiveSenseAction = "ActionTerrorSpiderPrincessHiveSense"; + + [DataField] + public EntityUid? HiveSenseActionEntity; + + [DataField] + public EntProtoId ScreamAction = "ActionTerrorSpiderPrincessScream"; + + [DataField] + public EntityUid? ScreamActionEntity; + + [DataField] + public EntProtoId LayEggRusarAction = "ActionTerrorSpiderPrincessLayEggRusar"; + + [DataField] + public EntityUid? LayEggRusarActionEntity; + + [DataField] + public EntProtoId LayEggDronAction = "ActionTerrorSpiderPrincessLayEggDron"; + + [DataField] + public EntityUid? LayEggDronActionEntity; + + [DataField] + public EntProtoId LayEggLurkerAction = "ActionTerrorSpiderPrincessLayEggLurker"; + + [DataField] + public EntityUid? LayEggLurkerActionEntity; + + [DataField] + public EntProtoId LayEggHealerAction = "ActionTerrorSpiderPrincessLayEggHealer"; + + [DataField] + public EntityUid? LayEggHealerActionEntity; + + [DataField] + public EntProtoId LayEggReaperAction = "ActionTerrorSpiderPrincessLayEggReaper"; + + [DataField] + public EntityUid? LayEggReaperActionEntity; + + [DataField] + public EntProtoId LayEggWidowAction = "ActionTerrorSpiderPrincessLayEggWidow"; + + [DataField] + public EntityUid? LayEggWidowActionEntity; + + [DataField] + public EntProtoId LayEggGuardianAction = "ActionTerrorSpiderPrincessLayEggGuardian"; + + [DataField] + public EntityUid? LayEggGuardianActionEntity; + + [DataField] + public EntProtoId LayEggDestroyerAction = "ActionTerrorSpiderPrincessLayEggDestroyer"; + + [DataField] + public EntityUid? LayEggDestroyerActionEntity; + + [DataField] + public float ScreamRange = 13f; + + [DataField] + public float ScreamSlowDuration = 10f; + + [DataField] + public float ScreamSlowMultiplier = 0.5f; + + [DataField] + public float ScreamStaminaDamage = 30f; + + [DataField] + public float ScreamEmpEnergyConsumption = 120000f; + + [DataField] + public float ScreamRobotDisableSeconds = 12f; + + [DataField] + public SoundSpecifier? ScreamSound = new SoundPathSpecifier("/Audio/Imperial/TerrorSpider/scream.ogg"); + + [DataField] + public EntProtoId RemoteViewImmobileStatusEffect = "TerrorSpiderPrincessRemoteViewImmobileStatusEffect"; + + [DataField] + public float RemoteViewImmobileRefresh = 0.5f; + + [DataField] + public int MaxBroodOnMap = 20; + + [DataField] + public int MaxEliteBroodOnMap = 3; + + [DataField] + public float OrphanDamagePerTick = 6f; + + [DataField] + public float OrphanDamageInterval = 2f; + + [ViewVariables] + public int RemoteViewIndex = -1; +} diff --git a/Content.Server/Imperial/TerrorSpider/Components/TerrorSpiderPrincessEggComponent.cs b/Content.Server/Imperial/TerrorSpider/Components/TerrorSpiderPrincessEggComponent.cs new file mode 100644 index 00000000000..014c0c6e33c --- /dev/null +++ b/Content.Server/Imperial/TerrorSpider/Components/TerrorSpiderPrincessEggComponent.cs @@ -0,0 +1,52 @@ +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List; + +namespace Content.Server.Imperial.TerrorSpider.Components; + +[RegisterComponent] +public sealed partial class TerrorSpiderPrincessEggComponent : Component +{ + [DataField] + public EntityUid? Princess; + + [DataField] + public bool TierTwo; + + [DataField] + public float HatchDelay = 240f; + + [DataField] + public TimeSpan HatchAt = TimeSpan.Zero; + + [DataField(customTypeSerializer: typeof(PrototypeIdListSerializer))] + public List TierOnePrototypes = new() + { + "MobRusarSpiderAngrys", + "MobDronSpiderAngrys", + "MobsogladatelSpiderAngrys", + "MobGiantHealerSpiderAngry" + }; + + [DataField(customTypeSerializer: typeof(PrototypeIdListSerializer))] + public List TierTwoPrototypes = new() + { + "MobGiantreaperSpiderAngry", + "MobGiantWidowSpiderAngry", + "MobGiantGuardianSpiderAngry", + "MobGiantDestroyerSpiderAngry" + }; + + [DataField(customTypeSerializer: typeof(PrototypeIdListSerializer))] + public List TierTwoElitePrototypes = new() + { + "MobGiantWidowSpiderAngry", + "MobGiantGuardianSpiderAngry", + "MobGiantDestroyerSpiderAngry" + }; + + [DataField(customTypeSerializer: typeof(PrototypeIdListSerializer))] + public List TierTwoUnlimitedPrototypes = new() + { + "MobGiantreaperSpiderAngry" + }; +} diff --git a/Content.Server/Imperial/TerrorSpider/Components/TerrorSpiderPrincessOrphanDamageComponent.cs b/Content.Server/Imperial/TerrorSpider/Components/TerrorSpiderPrincessOrphanDamageComponent.cs new file mode 100644 index 00000000000..b740d5d0e58 --- /dev/null +++ b/Content.Server/Imperial/TerrorSpider/Components/TerrorSpiderPrincessOrphanDamageComponent.cs @@ -0,0 +1,14 @@ +namespace Content.Server.Imperial.TerrorSpider.Components; + +[RegisterComponent] +public sealed partial class TerrorSpiderPrincessOrphanDamageComponent : Component +{ + [DataField] + public float DamagePerTick = 6f; + + [DataField] + public float TickInterval = 2f; + + [DataField] + public TimeSpan NextTick = TimeSpan.Zero; +} diff --git a/Content.Server/Imperial/TerrorSpider/Components/TerrorSpiderQueenBroodComponent.cs b/Content.Server/Imperial/TerrorSpider/Components/TerrorSpiderQueenBroodComponent.cs new file mode 100644 index 00000000000..dd55a475869 --- /dev/null +++ b/Content.Server/Imperial/TerrorSpider/Components/TerrorSpiderQueenBroodComponent.cs @@ -0,0 +1,11 @@ +namespace Content.Server.Imperial.TerrorSpider.Components; + +[RegisterComponent] +public sealed partial class TerrorSpiderQueenBroodComponent : Component +{ + [DataField] + public EntityUid? Queen; + + [DataField] + public string? RoyalCooldownKey; +} diff --git a/Content.Server/Imperial/TerrorSpider/Components/TerrorSpiderQueenComponent.cs b/Content.Server/Imperial/TerrorSpider/Components/TerrorSpiderQueenComponent.cs new file mode 100644 index 00000000000..10cf231b251 --- /dev/null +++ b/Content.Server/Imperial/TerrorSpider/Components/TerrorSpiderQueenComponent.cs @@ -0,0 +1,174 @@ +using Robust.Shared.Audio; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; + +namespace Content.Server.Imperial.TerrorSpider.Components; + +[RegisterComponent] +public sealed partial class TerrorSpiderQueenComponent : Component +{ + [DataField] + public EntProtoId CreateHiveAction = "ActionTerrorSpiderQueenCreateHive"; + + [DataField] + public EntityUid? CreateHiveActionEntity; + + [DataField] + public EntProtoId ScreamAction = "ActionTerrorSpiderQueenScream"; + + [DataField] + public EntityUid? ScreamActionEntity; + + [DataField] + public EntProtoId RemoteViewNextAction = "ActionTerrorSpiderMotherRemoteViewNext"; + + [DataField] + public EntityUid? RemoteViewNextActionEntity; + + [DataField] + public EntProtoId RemoteViewPreviousAction = "ActionTerrorSpiderMotherRemoteViewPrevious"; + + [DataField] + public EntityUid? RemoteViewPreviousActionEntity; + + [DataField] + public EntProtoId RemoteViewExitAction = "ActionTerrorSpiderMotherRemoteViewExit"; + + [DataField] + public EntityUid? RemoteViewExitActionEntity; + + [DataField] + public EntProtoId HiveCountAction = "ActionTerrorSpiderQueenHiveCount"; + + [DataField] + public EntityUid? HiveCountActionEntity; + + [DataField] + public EntProtoId LayEggRusarAction = "ActionTerrorSpiderQueenLayEggRusar"; + + [DataField] + public EntityUid? LayEggRusarActionEntity; + + [DataField] + public EntProtoId LayEggDronAction = "ActionTerrorSpiderQueenLayEggDron"; + + [DataField] + public EntityUid? LayEggDronActionEntity; + + [DataField] + public EntProtoId LayEggLurkerAction = "ActionTerrorSpiderQueenLayEggLurker"; + + [DataField] + public EntityUid? LayEggLurkerActionEntity; + + [DataField] + public EntProtoId LayEggHealerAction = "ActionTerrorSpiderQueenLayEggHealer"; + + [DataField] + public EntityUid? LayEggHealerActionEntity; + + [DataField] + public EntProtoId LayEggReaperAction = "ActionTerrorSpiderQueenLayEggReaper"; + + [DataField] + public EntityUid? LayEggReaperActionEntity; + + [DataField] + public EntProtoId LayEggWidowAction = "ActionTerrorSpiderQueenLayEggWidow"; + + [DataField] + public EntityUid? LayEggWidowActionEntity; + + [DataField] + public EntProtoId LayEggGuardianAction = "ActionTerrorSpiderQueenLayEggGuardian"; + + [DataField] + public EntityUid? LayEggGuardianActionEntity; + + [DataField] + public EntProtoId LayEggDestroyerAction = "ActionTerrorSpiderQueenLayEggDestroyer"; + + [DataField] + public EntityUid? LayEggDestroyerActionEntity; + + [DataField] + public EntProtoId LayEggPrinceAction = "ActionTerrorSpiderQueenLayEggPrince"; + + [DataField] + public EntityUid? LayEggPrinceActionEntity; + + [DataField] + public EntProtoId LayEggPrincessAction = "ActionTerrorSpiderQueenLayEggPrincess"; + + [DataField] + public EntityUid? LayEggPrincessActionEntity; + + [DataField] + public EntProtoId LayEggMotherAction = "ActionTerrorSpiderQueenLayEggMother"; + + [DataField] + public EntityUid? LayEggMotherActionEntity; + + [DataField] + public bool HiveCreated; + + [DataField] + public float HiveSpeedMultiplier = 0.5f; + + [DataField] + public float HiveStructuralDamage = 400f; + + [DataField] + public EntProtoId HiveSlowStatusEffect = "TerrorSpiderQueenHiveSlowStatusEffect"; + + [DataField] + public float HiveSlowRefresh = 0.5f; + + [DataField] + public EntProtoId RemoteViewImmobileStatusEffect = "TerrorSpiderMotherRemoteViewImmobileStatusEffect"; + + [DataField] + public float RemoteViewImmobileRefresh = 0.5f; + + [DataField] + public float ScreamRange = 8.5f; + + [DataField] + public float ScreamSlowDuration = 14f; + + [DataField] + public float ScreamSlowMultiplier = 0.5f; + + [DataField] + public float ScreamStaminaDamage = 50f; + + [DataField] + public float ScreamEmpEnergyConsumption = 120000f; + + [DataField] + public float ScreamRobotDisableSeconds = 16f; + + [DataField] + public float LightBreakHalfRange = 8.5f; + + [DataField] + public float RoyalEggCooldownSeconds = 1500f; + + [ViewVariables] + public Dictionary SharedEggCooldownEnds = new(); + + [DataField] + public float OrphanDamagePerTick = 6f; + + [DataField] + public float OrphanDamageInterval = 2f; + + [DataField] + public SoundSpecifier? ScreamSound = new SoundPathSpecifier("/Audio/Imperial/TerrorSpider/scream.ogg"); + + [ViewVariables] + public int RemoteViewIndex = -1; + + [ViewVariables] + public Dictionary RoyalCooldownEnds = new(); +} diff --git a/Content.Server/Imperial/TerrorSpider/Components/TerrorSpiderQueenEggComponent.cs b/Content.Server/Imperial/TerrorSpider/Components/TerrorSpiderQueenEggComponent.cs new file mode 100644 index 00000000000..fcae451d106 --- /dev/null +++ b/Content.Server/Imperial/TerrorSpider/Components/TerrorSpiderQueenEggComponent.cs @@ -0,0 +1,23 @@ +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; + +namespace Content.Server.Imperial.TerrorSpider.Components; + +[RegisterComponent] +public sealed partial class TerrorSpiderQueenEggComponent : Component +{ + [DataField] + public EntityUid? Queen; + + [DataField] + public float HatchDelay = 240f; + + [DataField] + public TimeSpan HatchAt = TimeSpan.Zero; + + [DataField(required: true, customTypeSerializer: typeof(PrototypeIdSerializer))] + public string SpawnPrototype = string.Empty; + + [DataField] + public string? RoyalCooldownKey; +} diff --git a/Content.Server/Imperial/TerrorSpider/Components/TerrorSpiderQueenOrphanDamageComponent.cs b/Content.Server/Imperial/TerrorSpider/Components/TerrorSpiderQueenOrphanDamageComponent.cs new file mode 100644 index 00000000000..b3102fa203c --- /dev/null +++ b/Content.Server/Imperial/TerrorSpider/Components/TerrorSpiderQueenOrphanDamageComponent.cs @@ -0,0 +1,14 @@ +namespace Content.Server.Imperial.TerrorSpider.Components; + +[RegisterComponent] +public sealed partial class TerrorSpiderQueenOrphanDamageComponent : Component +{ + [DataField] + public float DamagePerTick = 6f; + + [DataField] + public float TickInterval = 2f; + + [DataField] + public TimeSpan NextTick = TimeSpan.Zero; +} diff --git a/Content.Server/Imperial/TerrorSpider/Components/TerrorSpiderReaperLifestealComponent.cs b/Content.Server/Imperial/TerrorSpider/Components/TerrorSpiderReaperLifestealComponent.cs new file mode 100644 index 00000000000..21ea4e7431e --- /dev/null +++ b/Content.Server/Imperial/TerrorSpider/Components/TerrorSpiderReaperLifestealComponent.cs @@ -0,0 +1,8 @@ +namespace Content.Server.Imperial.TerrorSpider.Components; + +[RegisterComponent] +public sealed partial class TerrorSpiderReaperLifestealComponent : Component +{ + [DataField] + public float HealAmount = 30f; +} diff --git a/Content.Server/Imperial/TerrorSpider/Components/TerrorSpiderRoyalComponent.cs b/Content.Server/Imperial/TerrorSpider/Components/TerrorSpiderRoyalComponent.cs new file mode 100644 index 00000000000..6549ec31326 --- /dev/null +++ b/Content.Server/Imperial/TerrorSpider/Components/TerrorSpiderRoyalComponent.cs @@ -0,0 +1,39 @@ +using Robust.Shared.Audio; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; + +namespace Content.Server.Imperial.TerrorSpider.Components; + +[RegisterComponent] +public sealed partial class TerrorSpiderRoyalComponent : Component +{ + [DataField] + public bool StompEnabled = true; + + [DataField] + public EntProtoId StompAction = "ActionTerrorSpiderRoyalStomp"; + + [DataField] + public EntityUid? StompActionEntity; + + [DataField] + public float StompRadius = 5f; + + [DataField] + public float StompDamage = 20f; + + [DataField] + public string StompDamageType = "Blunt"; + + [DataField] + public float StompSlowDuration = 10f; + + [DataField] + public float StompSlowMultiplier = 0.5f; + + [DataField] + public EntProtoId StompSlowStatusEffect = "TerrorSpiderRoyalStompSlowStatusEffect"; + + [DataField] + public SoundSpecifier? StompSound = new SoundPathSpecifier("/Audio/Imperial/TerrorSpider/slam.ogg"); +} diff --git a/Content.Server/Imperial/TerrorSpider/Components/TerrorSpiderWidowMeleeDebuffComponent.cs b/Content.Server/Imperial/TerrorSpider/Components/TerrorSpiderWidowMeleeDebuffComponent.cs new file mode 100644 index 00000000000..af91a9673c2 --- /dev/null +++ b/Content.Server/Imperial/TerrorSpider/Components/TerrorSpiderWidowMeleeDebuffComponent.cs @@ -0,0 +1,14 @@ +namespace Content.Server.Imperial.TerrorSpider.Components; + +[RegisterComponent] +public sealed partial class TerrorSpiderWidowMeleeDebuffComponent : Component +{ + [DataField] + public float MuteDuration = 10f; + + [DataField] + public float VenomPerHit = 10f; + + [DataField] + public string VenomReagent = "BlackTerrorVenom"; +} diff --git a/Content.Server/Imperial/TerrorSpider/Components/TerrorSpiderWidowWebAreaComponent.cs b/Content.Server/Imperial/TerrorSpider/Components/TerrorSpiderWidowWebAreaComponent.cs new file mode 100644 index 00000000000..ab107171258 --- /dev/null +++ b/Content.Server/Imperial/TerrorSpider/Components/TerrorSpiderWidowWebAreaComponent.cs @@ -0,0 +1,11 @@ +namespace Content.Server.Imperial.TerrorSpider.Components; + +[RegisterComponent] +public sealed partial class TerrorSpiderWidowWebAreaComponent : Component +{ + [DataField] + public float VenomOnTouch = 20f; + + [DataField] + public string VenomReagent = "BlackTerrorVenom"; +} diff --git a/Content.Server/Imperial/TerrorSpider/Systems/TerrorDronDebuffSystem.cs b/Content.Server/Imperial/TerrorSpider/Systems/TerrorDronDebuffSystem.cs new file mode 100644 index 00000000000..eda7fdb6542 --- /dev/null +++ b/Content.Server/Imperial/TerrorSpider/Systems/TerrorDronDebuffSystem.cs @@ -0,0 +1,47 @@ +using Content.Server.Imperial.TerrorSpider.Components; +using Content.Shared.Damage.Systems; +using Content.Shared.Movement.Systems; +using Content.Shared.Projectiles; +using Content.Shared.Weapons.Melee.Events; + +namespace Content.Server.Imperial.TerrorSpider.Systems; + +public sealed class TerrorDronDebuffSystem : EntitySystem +{ + [Dependency] private readonly MovementModStatusSystem _movementModStatus = default!; + [Dependency] private readonly SharedStaminaSystem _stamina = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnMeleeHit); + SubscribeLocalEvent(OnProjectileHit); + } + + private void OnMeleeHit(Entity ent, ref MeleeHitEvent args) + { + if (!args.IsHit) + return; + + foreach (var target in args.HitEntities) + { + _movementModStatus.TryUpdateMovementSpeedModDuration( + target, + MovementModStatusSystem.TaserSlowdown, + TimeSpan.FromSeconds(ent.Comp.SlowDuration), + ent.Comp.SlowMultiplier); + } + } + + private void OnProjectileHit(Entity ent, ref ProjectileHitEvent args) + { + _movementModStatus.TryUpdateMovementSpeedModDuration( + args.Target, + MovementModStatusSystem.TaserSlowdown, + TimeSpan.FromSeconds(ent.Comp.SlowDuration), + ent.Comp.SlowMultiplier); + + _stamina.TakeStaminaDamage(args.Target, ent.Comp.StaminaDamage, source: args.Shooter ?? ent.Owner); + } +} diff --git a/Content.Server/Imperial/TerrorSpider/Systems/TerrorSpiderArmorSystem.cs b/Content.Server/Imperial/TerrorSpider/Systems/TerrorSpiderArmorSystem.cs new file mode 100644 index 00000000000..801072d0bdb --- /dev/null +++ b/Content.Server/Imperial/TerrorSpider/Systems/TerrorSpiderArmorSystem.cs @@ -0,0 +1,7 @@ +using Content.Shared.Imperial.TerrorSpider.Systems; + +namespace Content.Server.Imperial.TerrorSpider.Systems; + +public sealed class TerrorSpiderArmorSystem : SharedTerrorSpiderArmorSystem +{ +} diff --git a/Content.Server/Imperial/TerrorSpider/Systems/TerrorSpiderCocoonSystem.cs b/Content.Server/Imperial/TerrorSpider/Systems/TerrorSpiderCocoonSystem.cs new file mode 100644 index 00000000000..3373cd2c287 --- /dev/null +++ b/Content.Server/Imperial/TerrorSpider/Systems/TerrorSpiderCocoonSystem.cs @@ -0,0 +1,7 @@ +using Content.Shared.Imperial.TerrorSpider.Systems; + +namespace Content.Server.Imperial.TerrorSpider.Systems; + +public sealed class TerrorSpiderCocoonSystem : SharedTerrorSpiderCocoonSystem +{ +} diff --git a/Content.Server/Imperial/TerrorSpider/Systems/TerrorSpiderDestroyerSystem.cs b/Content.Server/Imperial/TerrorSpider/Systems/TerrorSpiderDestroyerSystem.cs new file mode 100644 index 00000000000..ca4145554db --- /dev/null +++ b/Content.Server/Imperial/TerrorSpider/Systems/TerrorSpiderDestroyerSystem.cs @@ -0,0 +1,127 @@ +using Content.Server.Actions; +using Content.Server.Atmos.EntitySystems; +using Content.Server.Imperial.TerrorSpider.Components; +using Content.Shared.Atmos.Components; +using Content.Shared.Damage; +using Content.Shared.Damage.Systems; +using Content.Shared.Emp; +using Content.Shared.FixedPoint; +using Content.Shared.Imperial.TerrorSpider.Events; +using Content.Shared.Mobs; +using Robust.Shared.Map; +using Robust.Shared.Audio.Systems; + +namespace Content.Server.Imperial.TerrorSpider.Systems; + +public sealed class TerrorSpiderDestroyerSystem : EntitySystem +{ + [Dependency] private readonly ActionsSystem _actions = default!; + [Dependency] private readonly SharedEmpSystem _emp = default!; + [Dependency] private readonly EntityLookupSystem _lookup = default!; + [Dependency] private readonly FlammableSystem _flammable = default!; + [Dependency] private readonly DamageableSystem _damageable = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + private readonly HashSet> _nearFlammables = new(); + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnMapInit); + SubscribeLocalEvent(OnShutdown); + SubscribeLocalEvent(OnMobStateChanged); + SubscribeLocalEvent(OnEmpScreamAction); + SubscribeLocalEvent(OnFireBurstAction); + } + + private void OnMapInit(EntityUid uid, TerrorSpiderDestroyerComponent comp, MapInitEvent args) + { + _actions.AddAction(uid, ref comp.EmpScreamActionEntity, comp.EmpScreamAction); + _actions.AddAction(uid, ref comp.FireBurstActionEntity, comp.FireBurstAction); + } + + private void OnShutdown(EntityUid uid, TerrorSpiderDestroyerComponent comp, ComponentShutdown args) + { + _actions.RemoveAction(uid, comp.EmpScreamActionEntity); + _actions.RemoveAction(uid, comp.FireBurstActionEntity); + } + + private void OnEmpScreamAction(Entity ent, ref TerrorSpiderDestroyerEmpScreamActionEvent args) + { + if (args.Handled) + return; + + _emp.EmpPulse( + Transform(ent.Owner).Coordinates, + ent.Comp.DeathEmpRange, + ent.Comp.EmpScreamEnergyConsumption, + TimeSpan.FromSeconds(ent.Comp.EmpScreamDisableSeconds), + ent.Owner); + + if (ent.Comp.EmpUseSound != null) + _audio.PlayPvs(ent.Comp.EmpUseSound, ent.Owner); + + args.Handled = true; + } + + private void OnMobStateChanged(Entity ent, ref MobStateChangedEvent args) + { + if (args.NewMobState != MobState.Dead) + return; + + if (ent.Comp.DeathEmpTriggered) + return; + + ent.Comp.DeathEmpTriggered = true; + + _emp.EmpPulse( + Transform(ent.Owner).Coordinates, + ent.Comp.EmpScreamRange, + ent.Comp.EmpScreamEnergyConsumption, + TimeSpan.FromSeconds(ent.Comp.EmpScreamDisableSeconds), + ent.Owner); + + if (ent.Comp.EmpDeathSound != null) + _audio.PlayPvs(ent.Comp.EmpDeathSound, ent.Owner); + } + + private void OnFireBurstAction(Entity ent, ref TerrorSpiderDestroyerFireBurstActionEvent args) + { + if (args.Handled) + return; + + var origin = Transform(ent.Owner).MapPosition; + + var ignited = new HashSet(); + + IgniteArea(origin, ent.Comp.FireBurstRadius, ent, ignited); + + args.Handled = true; + } + + private void IgniteArea(MapCoordinates center, float radius, Entity source, HashSet ignited) + { + _nearFlammables.Clear(); + _lookup.GetEntitiesInRange( + center, + radius, + _nearFlammables, + LookupFlags.Dynamic | LookupFlags.Sundries | LookupFlags.Static); + + foreach (var (uid, flammable) in _nearFlammables) + { + if (uid == source.Owner) + continue; + + _flammable.AdjustFireStacks(uid, source.Comp.FireStacksPerHit, flammable); + _flammable.Ignite(uid, source.Owner, flammable); + + if (!ignited.Add(uid)) + continue; + + var heatDamage = new DamageSpecifier(); + heatDamage.DamageDict["Heat"] = FixedPoint2.New(source.Comp.FireHeatDamagePerHit); + _damageable.TryChangeDamage(uid, heatDamage, ignoreResistances: false, interruptsDoAfters: false); + } + } +} diff --git a/Content.Server/Imperial/TerrorSpider/Systems/TerrorSpiderGuardianLeashSystem.cs b/Content.Server/Imperial/TerrorSpider/Systems/TerrorSpiderGuardianLeashSystem.cs new file mode 100644 index 00000000000..edd5757d3d2 --- /dev/null +++ b/Content.Server/Imperial/TerrorSpider/Systems/TerrorSpiderGuardianLeashSystem.cs @@ -0,0 +1,104 @@ +using Content.Server.Imperial.TerrorSpider.Components; +using Content.Shared.Imperial.TerrorSpider.Components; +using Content.Shared.Damage; +using Content.Shared.Damage.Systems; +using Content.Shared.FixedPoint; +using Content.Shared.Mobs; +using Content.Shared.Mobs.Components; +using Content.Shared.Popups; +using Content.Shared.Tag; +using Content.Server.Popups; +using Robust.Server.GameObjects; +using Robust.Shared.Map; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; + +namespace Content.Server.Imperial.TerrorSpider.Systems; + +public sealed class TerrorSpiderGuardianLeashSystem : EntitySystem +{ + private static readonly ProtoId TerrorSpiderTag = "TerrorSpider"; + + [Dependency] private readonly DamageableSystem _damageable = default!; + [Dependency] private readonly EntityLookupSystem _lookup = default!; + [Dependency] private readonly PopupSystem _popup = default!; + [Dependency] private readonly TagSystem _tag = default!; + [Dependency] private readonly IGameTiming _timing = default!; + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var now = _timing.CurTime; + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var leash, out var mobState)) + { + if (mobState.CurrentState == MobState.Dead) + continue; + + if (now < leash.NextTickTime) + continue; + + leash.NextTickTime = now + TimeSpan.FromSeconds(leash.TickInterval); + + var guardPos = Transform(uid).MapPosition; + var nearRoyal = IsNearAliveRoyal(guardPos, leash.RequiredRoyalRange, uid); + + if (nearRoyal) + continue; + + var damage = new DamageSpecifier(); + damage.DamageDict["Blunt"] = FixedPoint2.New(leash.DamageIfFar); + _damageable.TryChangeDamage(uid, damage, ignoreResistances: false, interruptsDoAfters: false); + + if (now < leash.NextWarningTime) + continue; + + leash.NextWarningTime = now + TimeSpan.FromSeconds(leash.WarningCooldown); + + _popup.PopupEntity( + "Вы слишком далеко! Вернитесь к принцессе или королеве.", + uid, + uid, + PopupType.SmallCaution); + } + } + + private bool IsNearAliveRoyal(MapCoordinates guardPos, float maxRange, EntityUid guard) + { + var maxRangeSquared = maxRange * maxRange; + + var princessQuery = EntityQueryEnumerator(); + while (princessQuery.MoveNext(out var uid, out _, out var state, out var xform)) + { + if (uid == guard || state.CurrentState != MobState.Alive) + continue; + + if (xform.MapPosition.MapId != guardPos.MapId) + continue; + + if ((xform.MapPosition.Position - guardPos.Position).LengthSquared() <= maxRangeSquared) + return true; + } + + var queenQuery = EntityQueryEnumerator(); + while (queenQuery.MoveNext(out var uid, out _, out var state, out var xform)) + { + if (uid == guard || state.CurrentState != MobState.Alive) + continue; + + if (xform.MapPosition.MapId != guardPos.MapId) + continue; + + if ((xform.MapPosition.Position - guardPos.Position).LengthSquared() <= maxRangeSquared) + return true; + } + + return false; + } + + private bool IsTerrorSpider(EntityUid uid) + { + return HasComp(uid) || _tag.HasTag(uid, TerrorSpiderTag); + } +} diff --git a/Content.Server/Imperial/TerrorSpider/Systems/TerrorSpiderHealerSystem.cs b/Content.Server/Imperial/TerrorSpider/Systems/TerrorSpiderHealerSystem.cs new file mode 100644 index 00000000000..1ee7c72b3bf --- /dev/null +++ b/Content.Server/Imperial/TerrorSpider/Systems/TerrorSpiderHealerSystem.cs @@ -0,0 +1,232 @@ +using Content.Server.Actions; +using Content.Server.Imperial.TerrorSpider.Components; +using Content.Shared.Damage.Components; +using Content.Shared.Damage; +using Content.Shared.Damage.Systems; +using Content.Shared.Eye.Blinding.Components; +using Content.Shared.Eye.Blinding.Systems; +using Content.Shared.Imperial.TerrorSpider.Components; +using Content.Shared.Imperial.TerrorSpider.Events; +using Content.Shared.Interaction; +using Content.Shared.Spider; +using Content.Shared.StatusEffect; +using Content.Shared.Weapons.Melee.Events; +using Robust.Server.GameObjects; +using Robust.Shared.Map; +using Robust.Shared.Physics.Events; +using Robust.Shared.Timing; + +namespace Content.Server.Imperial.TerrorSpider.Systems; + +public sealed class TerrorSpiderHealerSystem : EntitySystem +{ + [Dependency] private readonly ActionsSystem _actions = default!; + [Dependency] private readonly DamageableSystem _damageable = default!; + [Dependency] private readonly StatusEffectsSystem _statusEffects = default!; + [Dependency] private readonly IGameTiming _timing = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnMapInit); + SubscribeLocalEvent(OnShutdown); + + SubscribeLocalEvent(OnPulseAction); + SubscribeLocalEvent(OnLayEggAction); + SubscribeLocalEvent(OnCocoonWrapped); + SubscribeLocalEvent(OnBeforeInteractHand); + SubscribeLocalEvent(OnBeforeSpiderDamageChanged); + SubscribeLocalEvent(OnMeleeHit); + + SubscribeLocalEvent(OnInteractHand); + SubscribeLocalEvent(OnBlindWebStartCollide); + } + + private void OnMapInit(EntityUid uid, TerrorSpiderHealerComponent comp, MapInitEvent args) + { + _actions.AddAction(uid, ref comp.PulseActionEntity, comp.PulseAction); + _actions.AddAction(uid, ref comp.EggRusarActionEntity, comp.EggRusarAction); + _actions.AddAction(uid, ref comp.EggDronActionEntity, comp.EggDronAction); + _actions.AddAction(uid, ref comp.EggLurkerActionEntity, comp.EggLurkerAction); + _actions.AddAction(uid, ref comp.EggHealerActionEntity, comp.EggHealerAction); + } + + private void OnShutdown(EntityUid uid, TerrorSpiderHealerComponent comp, ComponentShutdown args) + { + _actions.RemoveAction(uid, comp.PulseActionEntity); + _actions.RemoveAction(uid, comp.EggRusarActionEntity); + _actions.RemoveAction(uid, comp.EggDronActionEntity); + _actions.RemoveAction(uid, comp.EggLurkerActionEntity); + _actions.RemoveAction(uid, comp.EggHealerActionEntity); + } + + private void OnPulseAction(Entity ent, ref TerrorSpiderHealerPulseActionEvent args) + { + if (args.Handled) + return; + + var origin = Transform(ent.Owner).MapPosition; + + var rangeSquared = ent.Comp.PulseRange * ent.Comp.PulseRange; + var query = EntityQueryEnumerator(); + + while (query.MoveNext(out var target, out _, out var xform)) + { + var targetCoords = xform.MapPosition; + + if (targetCoords.MapId != origin.MapId) + continue; + + if ((targetCoords.Position - origin.Position).LengthSquared() > rangeSquared) + continue; + + ApplyHealAllDamageTypes(target, ent.Comp.PulseHealAmount); + } + + args.Handled = true; + } + + private void OnLayEggAction(Entity ent, ref TerrorSpiderHealerLayEggActionEvent args) + { + if (args.Handled) + return; + + if (ent.Comp.SatietyLevel < ent.Comp.EggRequiredSatiety) + return; + + Spawn(args.EggPrototype, Transform(ent.Owner).Coordinates); + ent.Comp.SatietyLevel -= ent.Comp.EggRequiredSatiety; + Dirty(ent); + args.Handled = true; + } + + private void OnCocoonWrapped(Entity ent, ref TerrorSpiderCocoonWrappedEvent args) + { + ent.Comp.SatietyLevel = Math.Min(ent.Comp.MaxSatietyLevel, ent.Comp.SatietyLevel + ent.Comp.CocoonSatietyGain); + Dirty(ent); + } + + private void OnInteractHand(Entity target, ref InteractHandEvent args) + { + if (args.Handled) + return; + + if (args.Target == args.User) + return; + + if (!TryComp(args.User, out var healer)) + return; + + TryApplyTouchHeal(args.Target, healer); + args.Handled = true; + } + + private void OnBeforeInteractHand(Entity ent, ref BeforeInteractHandEvent args) + { + if (args.Handled) + return; + + if (args.Target == ent.Owner) + return; + + if (!TryComp(args.Target, out _)) + return; + + TryApplyTouchHeal(args.Target, ent.Comp); + args.Handled = true; + } + + private void OnBeforeSpiderDamageChanged(Entity ent, ref BeforeDamageChangedEvent args) + { + if (args.Origin is not { } origin) + return; + + if (!TryComp(origin, out var healer)) + return; + + var amount = GetTouchHealAmount(healer); + args.Damage = new DamageSpecifier(); + ApplyHealAllDamageTypes(ent.Owner, amount); + } + + private void OnMeleeHit(MeleeHitEvent args) + { + if (!args.IsHit || args.HitEntities.Count == 0) + return; + + if (!TryComp(args.User, out var healer)) + return; + + var amount = GetTouchHealAmount(healer); + + foreach (var target in args.HitEntities) + { + if (!HasComp(target) || target == args.User) + continue; + + TryApplyTouchHeal(target, healer, amount); + } + } + + private bool TryApplyTouchHeal(EntityUid target, TerrorSpiderHealerComponent healer, float? amount = null) + { + var now = _timing.CurTime; + if (now < healer.NextTouchHealTime) + return false; + + healer.NextTouchHealTime = now + TimeSpan.FromSeconds(healer.TouchHealCooldown); + + var healAmount = amount ?? GetTouchHealAmount(healer); + ApplyHealAllDamageTypes(target, healAmount); + return true; + } + + private void ApplyHealAllDamageTypes(EntityUid target, float amount) + { + if (!TryComp(target, out var damageable)) + return; + + var heal = new DamageSpecifier(); + foreach (var group in damageable.DamagePerGroup.Keys) + { + heal.DamageDict[group] = -amount; + } + + foreach (var damageType in damageable.Damage.DamageDict.Keys) + { + heal.DamageDict[damageType] = -amount; + } + + if (heal.Empty) + return; + + _damageable.TryChangeDamage(target, heal, ignoreResistances: true, interruptsDoAfters: false); + } + + private void OnBlindWebStartCollide(Entity ent, ref StartCollideEvent args) + { + if (HasComp(args.OtherEntity)) + return; + + if (!TryComp(args.OtherEntity, out var statusEffects)) + return; + + _statusEffects.TryAddStatusEffect( + args.OtherEntity, + TemporaryBlindnessSystem.BlindingStatusEffect, + TimeSpan.FromSeconds(ent.Comp.BlindDuration), + true, + statusEffects); + } + + private static float GetTouchHealAmount(TerrorSpiderHealerComponent comp) + { + return comp.SatietyLevel switch + { + <= 1 => comp.TouchHealLevel1, + 2 => comp.TouchHealLevel2, + _ => comp.TouchHealLevel3, + }; + } +} diff --git a/Content.Server/Imperial/TerrorSpider/Systems/TerrorSpiderKnightGuardSystem.cs b/Content.Server/Imperial/TerrorSpider/Systems/TerrorSpiderKnightGuardSystem.cs new file mode 100644 index 00000000000..03e9797ddb2 --- /dev/null +++ b/Content.Server/Imperial/TerrorSpider/Systems/TerrorSpiderKnightGuardSystem.cs @@ -0,0 +1,7 @@ +using Content.Shared.Imperial.TerrorSpider.Systems; + +namespace Content.Server.Imperial.TerrorSpider.Systems; + +public sealed class TerrorSpiderKnightGuardSystem : SharedTerrorSpiderKnightGuardSystem +{ +} diff --git a/Content.Server/Imperial/TerrorSpider/Systems/TerrorSpiderKnightRageSystem.cs b/Content.Server/Imperial/TerrorSpider/Systems/TerrorSpiderKnightRageSystem.cs new file mode 100644 index 00000000000..78873a6c4c7 --- /dev/null +++ b/Content.Server/Imperial/TerrorSpider/Systems/TerrorSpiderKnightRageSystem.cs @@ -0,0 +1,7 @@ +using Content.Shared.Imperial.TerrorSpider.Systems; + +namespace Content.Server.Imperial.TerrorSpider.Systems; + +public sealed class TerrorSpiderKnightRageSystem : SharedTerrorSpiderKnightRageSystem +{ +} diff --git a/Content.Server/Imperial/TerrorSpider/Systems/TerrorSpiderLurkerSystem.cs b/Content.Server/Imperial/TerrorSpider/Systems/TerrorSpiderLurkerSystem.cs new file mode 100644 index 00000000000..84a00797343 --- /dev/null +++ b/Content.Server/Imperial/TerrorSpider/Systems/TerrorSpiderLurkerSystem.cs @@ -0,0 +1,152 @@ +using Content.Server.Actions; +using Content.Server.Imperial.TerrorSpider.Components; +using Content.Shared.Damage.Components; +using Content.Shared.Damage.Systems; +using Content.Shared.FixedPoint; +using Content.Shared.Imperial.TerrorSpider.Events; +using Content.Shared.Speech.Muting; +using Content.Shared.Spider; +using Content.Shared.StatusEffect; +using Content.Shared.Stealth; +using Content.Shared.Stealth.Components; +using Content.Shared.Weapons.Melee; +using Robust.Shared.Physics.Events; +using Robust.Shared.Timing; + +namespace Content.Server.Imperial.TerrorSpider.Systems; + +public sealed class TerrorSpiderLurkerSystem : EntitySystem +{ + [Dependency] private readonly ActionsSystem _actions = default!; + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly SharedStaminaSystem _stamina = default!; + [Dependency] private readonly StatusEffectsSystem _statusEffects = default!; + [Dependency] private readonly SharedStealthSystem _stealth = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnMapInit); + SubscribeLocalEvent(OnShutdown); + SubscribeLocalEvent(OnStealthAction); + + SubscribeLocalEvent(OnWebStartCollide); + SubscribeLocalEvent(OnWebEndCollide); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var now = _timing.CurTime; + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var comp)) + { + if (!comp.ActionStealthActive || now < comp.ActionStealthEndTime) + continue; + + comp.ActionStealthActive = false; + comp.ActionStealthEndTime = TimeSpan.Zero; + ApplyCurrentState(uid, comp); + Dirty(uid, comp); + } + } + + private void OnMapInit(EntityUid uid, TerrorSpiderLurkerComponent comp, MapInitEvent args) + { + _actions.AddAction(uid, ref comp.StealthActionEntity, comp.StealthAction); + comp.WebContacts = 0; + comp.ActionStealthActive = false; + comp.ActionStealthEndTime = TimeSpan.Zero; + ApplyCurrentState(uid, comp); + } + + private void OnShutdown(EntityUid uid, TerrorSpiderLurkerComponent comp, ComponentShutdown args) + { + _actions.RemoveAction(uid, comp.StealthActionEntity); + } + + private void OnStealthAction(Entity ent, ref TerrorSpiderLurkerStealthActionEvent args) + { + if (args.Handled) + return; + + ent.Comp.ActionStealthActive = true; + ent.Comp.ActionStealthEndTime = _timing.CurTime + TimeSpan.FromSeconds(ent.Comp.StealthDuration); + ApplyCurrentState(ent.Owner, ent.Comp); + Dirty(ent); + args.Handled = true; + } + + private void OnWebStartCollide(Entity ent, ref StartCollideEvent args) + { + if (TryComp(args.OtherEntity, out var lurker)) + { + lurker.WebContacts++; + ApplyCurrentState(args.OtherEntity, lurker); + Dirty(args.OtherEntity, lurker); + return; + } + + if (HasComp(args.OtherEntity)) + return; + + _stamina.TakeStaminaDamage(args.OtherEntity, ent.Comp.StaminaDamage, source: ent.Owner); + + if (!TryComp(args.OtherEntity, out var statusEffects)) + return; + + _statusEffects.TryAddStatusEffect( + args.OtherEntity, + "Muted", + TimeSpan.FromSeconds(ent.Comp.MuteDuration), + true, + statusEffects); + } + + private void OnWebEndCollide(Entity ent, ref EndCollideEvent args) + { + if (!TryComp(args.OtherEntity, out var lurker)) + return; + + lurker.WebContacts = Math.Max(0, lurker.WebContacts - 1); + ApplyCurrentState(args.OtherEntity, lurker); + Dirty(args.OtherEntity, lurker); + } + + private void ApplyCurrentState(EntityUid uid, TerrorSpiderLurkerComponent comp) + { + var onLurkerWeb = comp.WebContacts > 0; + var shouldBeStealthed = comp.ActionStealthActive || onLurkerWeb; + + if (TryComp(uid, out var melee)) + { + melee.Damage.DamageDict["Piercing"] = FixedPoint2.New(onLurkerWeb ? comp.WebBuffMeleeDamage : comp.BaseMeleeDamage); + Dirty(uid, melee); + } + + if (onLurkerWeb) + { + var staminaOnHit = EnsureComp(uid); + staminaOnHit.Damage = comp.WebBuffStaminaDamage; + Dirty(uid, staminaOnHit); + } + else if (HasComp(uid)) + { + RemComp(uid); + } + + if (shouldBeStealthed) + { + var stealth = EnsureComp(uid); + _stealth.SetEnabled(uid, true, stealth); + _stealth.SetVisibility(uid, comp.MinStealthVisibility, stealth); + Dirty(uid, stealth); + } + else if (HasComp(uid)) + { + RemComp(uid); + } + } +} diff --git a/Content.Server/Imperial/TerrorSpider/Systems/TerrorSpiderMotherSystem.cs b/Content.Server/Imperial/TerrorSpider/Systems/TerrorSpiderMotherSystem.cs new file mode 100644 index 00000000000..76834d8024a --- /dev/null +++ b/Content.Server/Imperial/TerrorSpider/Systems/TerrorSpiderMotherSystem.cs @@ -0,0 +1,349 @@ +using Content.Server.Actions; +using Content.Server.Imperial.TerrorSpider.Components; +using Content.Shared.Damage; +using Content.Shared.Damage.Components; +using Content.Shared.Damage.Systems; +using Content.Shared.Imperial.TerrorSpider.Components; +using Content.Shared.Imperial.TerrorSpider.Events; +using Content.Shared.Interaction; +using Content.Shared.Mobs; +using Content.Shared.Mobs.Components; +using Content.Shared.Movement.Systems; +using Content.Shared.Tag; +using Content.Shared.Weapons.Melee.Events; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; + +namespace Content.Server.Imperial.TerrorSpider.Systems; + +public sealed class TerrorSpiderMotherSystem : EntitySystem +{ + private static readonly ProtoId TerrorSpiderTag = "TerrorSpider"; + + [Dependency] private readonly ActionsSystem _actions = default!; + [Dependency] private readonly DamageableSystem _damageable = default!; + [Dependency] private readonly SharedEyeSystem _eye = default!; + [Dependency] private readonly EntityLookupSystem _lookup = default!; + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly MovementModStatusSystem _movementModStatus = default!; + [Dependency] private readonly TagSystem _tagSystem = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnMapInit); + SubscribeLocalEvent(OnShutdown); + SubscribeLocalEvent(OnPulseAction); + SubscribeLocalEvent(OnRemoteViewNextAction); + SubscribeLocalEvent(OnRemoteViewPreviousAction); + SubscribeLocalEvent(OnRemoteViewExitAction); + SubscribeLocalEvent(OnLayJellyAction); + SubscribeLocalEvent(OnBeforeInteractHand); + SubscribeLocalEvent(OnMeleeHit); + SubscribeLocalEvent(OnAnyMeleeHit); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var now = _timing.CurTime; + UpdateRegenBuffs(now); + var mothers = EntityQueryEnumerator(); + + while (mothers.MoveNext(out var mother, out var comp)) + { + if (TryComp(mother, out var eye)) + UpdateRemoteViewMovementLock((mother, comp), eye.Target); + + if (comp.NextAuraTick == TimeSpan.Zero) + comp.NextAuraTick = now + TimeSpan.FromSeconds(comp.AuraInterval); + + if (now < comp.NextAuraTick) + continue; + + comp.NextAuraTick = now + TimeSpan.FromSeconds(comp.AuraInterval); + + foreach (var target in _lookup.GetEntitiesInRange(mother, comp.AuraHalfRange, LookupFlags.Dynamic)) + { + if (target == mother) + continue; + + if (!TryComp(target, out var damageable)) + continue; + + if (IsTerrorSpider(target)) + { + ApplyHealAllDamageTypes(target, damageable, comp.AuraHealAmount); + continue; + } + + if (!TryComp(target, out var mobState) || mobState.CurrentState != MobState.Alive) + continue; + + var damage = new DamageSpecifier(); + damage.DamageDict["Poison"] = comp.AuraDamageAmount; + _damageable.TryChangeDamage(target, damage, ignoreResistances: false, interruptsDoAfters: false); + } + } + } + + private void OnMapInit(EntityUid uid, TerrorSpiderMotherComponent comp, MapInitEvent args) + { + _actions.AddAction(uid, ref comp.PulseActionEntity, comp.PulseAction); + _actions.AddAction(uid, ref comp.RemoteViewNextActionEntity, comp.RemoteViewNextAction); + _actions.AddAction(uid, ref comp.RemoteViewPreviousActionEntity, comp.RemoteViewPreviousAction); + _actions.AddAction(uid, ref comp.RemoteViewExitActionEntity, comp.RemoteViewExitAction); + _actions.AddAction(uid, ref comp.LayJellyActionEntity, comp.LayJellyAction); + } + + private void OnShutdown(EntityUid uid, TerrorSpiderMotherComponent comp, ComponentShutdown args) + { + _actions.RemoveAction(uid, comp.PulseActionEntity); + _actions.RemoveAction(uid, comp.RemoteViewNextActionEntity); + _actions.RemoveAction(uid, comp.RemoteViewPreviousActionEntity); + _actions.RemoveAction(uid, comp.RemoteViewExitActionEntity); + _actions.RemoveAction(uid, comp.LayJellyActionEntity); + } + + private void OnPulseAction(Entity ent, ref TerrorSpiderMotherPulseActionEvent args) + { + if (args.Handled) + return; + + foreach (var target in _lookup.GetEntitiesInRange(ent.Owner, ent.Comp.PulseRange, LookupFlags.Dynamic)) + { + if (!IsTerrorSpider(target)) + continue; + + if (!TryComp(target, out var damageable)) + continue; + + ApplyHealAllDamageTypes(target, damageable, ent.Comp.PulseHealAmount); + } + + args.Handled = true; + } + + private void OnRemoteViewNextAction(Entity ent, ref TerrorSpiderMotherRemoteViewNextActionEvent args) + { + if (args.Handled) + return; + + if (!TryComp(ent.Owner, out _)) + return; + + var candidates = GetRemoteViewCandidates(ent.Owner); + + if (candidates.Count == 0) + { + _eye.SetTarget(ent.Owner, null); + ent.Comp.RemoteViewIndex = -1; + UpdateRemoteViewMovementLock(ent, null); + args.Handled = true; + return; + } + + if (ent.Comp.RemoteViewIndex < -1 || ent.Comp.RemoteViewIndex >= candidates.Count) + ent.Comp.RemoteViewIndex = -1; + + ent.Comp.RemoteViewIndex = (ent.Comp.RemoteViewIndex + 1) % candidates.Count; + _eye.SetTarget(ent.Owner, candidates[ent.Comp.RemoteViewIndex]); + UpdateRemoteViewMovementLock(ent, candidates[ent.Comp.RemoteViewIndex]); + args.Handled = true; + } + + private void OnRemoteViewPreviousAction(Entity ent, ref TerrorSpiderMotherRemoteViewPreviousActionEvent args) + { + if (args.Handled) + return; + + if (!TryComp(ent.Owner, out _)) + return; + + var candidates = GetRemoteViewCandidates(ent.Owner); + + if (candidates.Count == 0) + { + _eye.SetTarget(ent.Owner, null); + ent.Comp.RemoteViewIndex = -1; + UpdateRemoteViewMovementLock(ent, null); + args.Handled = true; + return; + } + + if (ent.Comp.RemoteViewIndex < 0 || ent.Comp.RemoteViewIndex >= candidates.Count) + ent.Comp.RemoteViewIndex = 0; + + ent.Comp.RemoteViewIndex = (ent.Comp.RemoteViewIndex - 1 + candidates.Count) % candidates.Count; + _eye.SetTarget(ent.Owner, candidates[ent.Comp.RemoteViewIndex]); + UpdateRemoteViewMovementLock(ent, candidates[ent.Comp.RemoteViewIndex]); + args.Handled = true; + } + + private void OnRemoteViewExitAction(Entity ent, ref TerrorSpiderMotherRemoteViewExitActionEvent args) + { + if (args.Handled) + return; + + if (!TryComp(ent.Owner, out _)) + return; + + ent.Comp.RemoteViewIndex = -1; + _eye.SetTarget(ent.Owner, null); + UpdateRemoteViewMovementLock(ent, null); + args.Handled = true; + } + + private void UpdateRemoteViewMovementLock(Entity ent, EntityUid? currentTarget) + { + var speedMultiplier = currentTarget == null ? 1f : 0f; + _movementModStatus.TryUpdateMovementSpeedModDuration( + ent.Owner, + ent.Comp.RemoteViewImmobileStatusEffect, + TimeSpan.FromSeconds(ent.Comp.RemoteViewImmobileRefresh), + speedMultiplier); + } + + + + private void OnLayJellyAction(Entity ent, ref TerrorSpiderMotherLayJellyActionEvent args) + { + if (args.Handled) + return; + + Spawn(ent.Comp.JellyPrototype, Transform(ent.Owner).Coordinates); + args.Handled = true; + } + + private void OnBeforeInteractHand(Entity ent, ref BeforeInteractHandEvent args) + { + if (args.Target == ent.Owner) + return; + + if (!IsTerrorSpider(args.Target)) + return; + + ApplyHealAllDamageTypes(args.Target, ent.Comp.TouchHealAmount); + args.Handled = true; + } + + private void OnMeleeHit(Entity ent, ref MeleeHitEvent args) + { + if (!args.IsHit || args.HitEntities.Count == 0) + return; + + foreach (var target in args.HitEntities) + { + if (target == ent.Owner) + continue; + + if (!IsTerrorSpider(target)) + continue; + + ApplyHealAllDamageTypes(target, ent.Comp.TouchHealAmount); + } + } + + private void OnAnyMeleeHit(Entity ent, ref MeleeHitEvent args) + { + if (!args.IsHit) + return; + + var now = _timing.CurTime; + + foreach (var target in args.HitEntities) + { + if (!TryComp(target, out var jelly)) + continue; + + var buff = EnsureComp(args.User); + buff.HealPerTick = jelly.BuffHealPerTick; + buff.TickInterval = jelly.BuffInterval; + buff.NextTick = TimeSpan.Zero; + buff.ExpiresAt = now + TimeSpan.FromSeconds(jelly.BuffDuration); + + if (!Deleted(target)) + Del(target); + } + } + + private void UpdateRegenBuffs(TimeSpan now) + { + var buffs = EntityQueryEnumerator(); + + while (buffs.MoveNext(out var uid, out var buff)) + { + if (buff.ExpiresAt != TimeSpan.Zero && now >= buff.ExpiresAt) + { + RemCompDeferred(uid); + continue; + } + + if (buff.NextTick == TimeSpan.Zero) + buff.NextTick = now + TimeSpan.FromSeconds(buff.TickInterval); + + if (now < buff.NextTick) + continue; + + buff.NextTick = now + TimeSpan.FromSeconds(buff.TickInterval); + ApplyHealAllDamageTypes(uid, buff.HealPerTick); + } + } + + private void ApplyHealAllDamageTypes(EntityUid target, float amount) + { + if (!TryComp(target, out var damageable)) + return; + + ApplyHealAllDamageTypes(target, damageable, amount); + } + + private void ApplyHealAllDamageTypes(EntityUid target, DamageableComponent damageable, float amount) + { + var heal = new DamageSpecifier(); + + foreach (var damageType in damageable.Damage.DamageDict.Keys) + { + heal.DamageDict[damageType] = -amount; + } + + if (heal.Empty) + return; + + _damageable.TryChangeDamage(target, heal, ignoreResistances: true, interruptsDoAfters: false); + } + + private List GetRemoteViewCandidates(EntityUid mother) + { + var candidates = new List(); + var motherPos = Transform(mother).MapPosition; + var query = EntityQueryEnumerator(); + + while (query.MoveNext(out var uid, out var mob, out var xform)) + { + if (uid == mother) + continue; + + if (mob.CurrentState != MobState.Alive) + continue; + + if (!IsTerrorSpider(uid)) + continue; + + if (xform.MapPosition.MapId != motherPos.MapId) + continue; + + candidates.Add(uid); + } + + candidates.Sort((a, b) => a.Id.CompareTo(b.Id)); + return candidates; + } + + private bool IsTerrorSpider(EntityUid uid) + { + return HasComp(uid) || _tagSystem.HasTag(uid, TerrorSpiderTag); + } +} diff --git a/Content.Server/Imperial/TerrorSpider/Systems/TerrorSpiderPrincessSystem.cs b/Content.Server/Imperial/TerrorSpider/Systems/TerrorSpiderPrincessSystem.cs new file mode 100644 index 00000000000..4ce5db8e909 --- /dev/null +++ b/Content.Server/Imperial/TerrorSpider/Systems/TerrorSpiderPrincessSystem.cs @@ -0,0 +1,544 @@ +using Content.Server.Actions; +using Content.Server.Chat.Managers; +using Content.Server.Imperial.TerrorSpider.Components; +using Content.Shared.Damage; +using Content.Shared.Damage.Components; +using Content.Shared.Damage.Systems; +using Content.Shared.Emp; +using Content.Shared.FixedPoint; +using Content.Shared.Imperial.TerrorSpider.Events; +using Content.Shared.Mind.Components; +using Content.Shared.Mobs; +using Content.Shared.Mobs.Components; +using Content.Shared.Movement.Systems; +using Content.Shared.Pinpointer; +using Content.Shared.Popups; +using Content.Shared.Silicons.Laws.Components; +using Content.Shared.Tag; +using Content.Shared.Damage.Prototypes; +using Content.Shared.Imperial.TerrorSpider.Components; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Map; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; +using Robust.Shared.Timing; +using Robust.Shared.Player; + +namespace Content.Server.Imperial.TerrorSpider.Systems; + +public sealed class TerrorSpiderPrincessSystem : EntitySystem +{ + private static readonly ProtoId _terrorSpiderTag = "TerrorSpider"; + + [Dependency] private readonly ActionsSystem _actions = default!; + [Dependency] private readonly IChatManager _chat = default!; + [Dependency] private readonly DamageableSystem _damageable = default!; + [Dependency] private readonly SharedEmpSystem _emp = default!; + [Dependency] private readonly SharedEyeSystem _eye = default!; + [Dependency] private readonly MovementModStatusSystem _movementModStatus = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedStaminaSystem _stamina = default!; + [Dependency] private readonly TagSystem _tagSystem = default!; + [Dependency] private readonly IGameTiming _timing = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnMapInit); + SubscribeLocalEvent(OnShutdown); + SubscribeLocalEvent(OnMobStateChanged); + SubscribeLocalEvent(OnRemoteViewNextAction); + SubscribeLocalEvent(OnRemoteViewPreviousAction); + SubscribeLocalEvent(OnRemoteViewExitAction); + SubscribeLocalEvent(OnHiveSenseAction); + SubscribeLocalEvent(OnScreamAction); + SubscribeLocalEvent(OnLayEggAction); + SubscribeLocalEvent(OnEggMapInit); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var now = _timing.CurTime; + UpdateRemoteViewMovementLock(); + UpdateEggs(now); + UpdateOrphanDamage(now); + } + + private void OnMapInit(EntityUid uid, TerrorSpiderPrincessComponent comp, MapInitEvent args) + { + _actions.AddAction(uid, ref comp.RemoteViewNextActionEntity, comp.RemoteViewNextAction); + _actions.AddAction(uid, ref comp.RemoteViewPreviousActionEntity, comp.RemoteViewPreviousAction); + _actions.AddAction(uid, ref comp.RemoteViewExitActionEntity, comp.RemoteViewExitAction); + _actions.AddAction(uid, ref comp.HiveSenseActionEntity, comp.HiveSenseAction); + _actions.AddAction(uid, ref comp.ScreamActionEntity, comp.ScreamAction); + _actions.AddAction(uid, ref comp.LayEggRusarActionEntity, comp.LayEggRusarAction); + _actions.AddAction(uid, ref comp.LayEggDronActionEntity, comp.LayEggDronAction); + _actions.AddAction(uid, ref comp.LayEggLurkerActionEntity, comp.LayEggLurkerAction); + _actions.AddAction(uid, ref comp.LayEggHealerActionEntity, comp.LayEggHealerAction); + _actions.AddAction(uid, ref comp.LayEggReaperActionEntity, comp.LayEggReaperAction); + _actions.AddAction(uid, ref comp.LayEggWidowActionEntity, comp.LayEggWidowAction); + _actions.AddAction(uid, ref comp.LayEggGuardianActionEntity, comp.LayEggGuardianAction); + _actions.AddAction(uid, ref comp.LayEggDestroyerActionEntity, comp.LayEggDestroyerAction); + } + + private void OnShutdown(EntityUid uid, TerrorSpiderPrincessComponent comp, ComponentShutdown args) + { + _actions.RemoveAction(uid, comp.RemoteViewNextActionEntity); + _actions.RemoveAction(uid, comp.RemoteViewPreviousActionEntity); + _actions.RemoveAction(uid, comp.RemoteViewExitActionEntity); + _actions.RemoveAction(uid, comp.HiveSenseActionEntity); + _actions.RemoveAction(uid, comp.ScreamActionEntity); + _actions.RemoveAction(uid, comp.LayEggRusarActionEntity); + _actions.RemoveAction(uid, comp.LayEggDronActionEntity); + _actions.RemoveAction(uid, comp.LayEggLurkerActionEntity); + _actions.RemoveAction(uid, comp.LayEggHealerActionEntity); + _actions.RemoveAction(uid, comp.LayEggReaperActionEntity); + _actions.RemoveAction(uid, comp.LayEggWidowActionEntity); + _actions.RemoveAction(uid, comp.LayEggGuardianActionEntity); + _actions.RemoveAction(uid, comp.LayEggDestroyerActionEntity); + } + + private void OnMobStateChanged(Entity ent, ref MobStateChangedEvent args) + { + if (args.NewMobState != MobState.Dead) + return; + + var broodQuery = EntityQueryEnumerator(); + while (broodQuery.MoveNext(out var uid, out var brood)) + { + if (brood.Princess != ent.Owner) + continue; + + var orphan = EnsureComp(uid); + orphan.DamagePerTick = ent.Comp.OrphanDamagePerTick; + orphan.TickInterval = ent.Comp.OrphanDamageInterval; + orphan.NextTick = TimeSpan.Zero; + } + } + + private void OnEggMapInit(Entity ent, ref MapInitEvent args) + { + if (ent.Comp.HatchAt != TimeSpan.Zero) + return; + + ent.Comp.HatchAt = _timing.CurTime + TimeSpan.FromSeconds(ent.Comp.HatchDelay); + } + + private void OnRemoteViewNextAction(Entity ent, ref TerrorSpiderMotherRemoteViewNextActionEvent args) + { + if (args.Handled) + return; + + if (!TryComp(ent.Owner, out _)) + return; + + var candidates = GetRemoteViewCandidates(ent.Owner); + + if (candidates.Count == 0) + { + _eye.SetTarget(ent.Owner, null); + ent.Comp.RemoteViewIndex = -1; + UpdateRemoteViewMovementLock(ent, null); + args.Handled = true; + return; + } + + if (ent.Comp.RemoteViewIndex < -1 || ent.Comp.RemoteViewIndex >= candidates.Count) + ent.Comp.RemoteViewIndex = -1; + + ent.Comp.RemoteViewIndex = (ent.Comp.RemoteViewIndex + 1) % candidates.Count; + _eye.SetTarget(ent.Owner, candidates[ent.Comp.RemoteViewIndex]); + UpdateRemoteViewMovementLock(ent, candidates[ent.Comp.RemoteViewIndex]); + args.Handled = true; + } + + private void OnRemoteViewPreviousAction(Entity ent, ref TerrorSpiderMotherRemoteViewPreviousActionEvent args) + { + if (args.Handled) + return; + + if (!TryComp(ent.Owner, out _)) + return; + + var candidates = GetRemoteViewCandidates(ent.Owner); + + if (candidates.Count == 0) + { + _eye.SetTarget(ent.Owner, null); + ent.Comp.RemoteViewIndex = -1; + UpdateRemoteViewMovementLock(ent, null); + args.Handled = true; + return; + } + + if (ent.Comp.RemoteViewIndex < 0 || ent.Comp.RemoteViewIndex >= candidates.Count) + ent.Comp.RemoteViewIndex = 0; + + ent.Comp.RemoteViewIndex = (ent.Comp.RemoteViewIndex - 1 + candidates.Count) % candidates.Count; + _eye.SetTarget(ent.Owner, candidates[ent.Comp.RemoteViewIndex]); + UpdateRemoteViewMovementLock(ent, candidates[ent.Comp.RemoteViewIndex]); + args.Handled = true; + } + + private void OnRemoteViewExitAction(Entity ent, ref TerrorSpiderMotherRemoteViewExitActionEvent args) + { + if (args.Handled) + return; + + if (!TryComp(ent.Owner, out _)) + return; + + ent.Comp.RemoteViewIndex = -1; + _eye.SetTarget(ent.Owner, null); + UpdateRemoteViewMovementLock(ent, null); + args.Handled = true; + } + + private void OnHiveSenseAction(Entity ent, ref TerrorSpiderPrincessHiveSenseActionEvent args) + { + if (args.Handled) + return; + + if (!TryComp(ent.Owner, out var actor)) + return; + + var lines = BuildHiveSenseLines(ent.Owner); + if (lines.Count == 0) + lines.Add(Loc.GetString("terror-spider-hive-sense-empty")); + + foreach (var line in lines) + { + _chat.DispatchServerMessage(actor.PlayerSession, line); + } + + args.Handled = true; + } + + private void OnScreamAction(Entity ent, ref TerrorSpiderPrincessScreamActionEvent args) + { + if (args.Handled) + return; + + var origin = Transform(ent.Owner).MapPosition; + var rangeSquared = ent.Comp.ScreamRange * ent.Comp.ScreamRange; + var query = EntityQueryEnumerator(); + + while (query.MoveNext(out var uid, out var mobState, out var xform)) + { + if (uid == ent.Owner) + continue; + + if (mobState.CurrentState != MobState.Alive) + continue; + + if (xform.MapPosition.MapId != origin.MapId) + continue; + + if ((xform.MapPosition.Position - origin.Position).LengthSquared() > rangeSquared) + continue; + + if (!IsTerrorSpider(uid)) + { + _movementModStatus.TryUpdateMovementSpeedModDuration( + uid, + MovementModStatusSystem.TaserSlowdown, + TimeSpan.FromSeconds(ent.Comp.ScreamSlowDuration), + ent.Comp.ScreamSlowMultiplier); + + _stamina.TakeStaminaDamage(uid, ent.Comp.ScreamStaminaDamage, source: ent.Owner); + } + + if (HasComp(uid)) + { + _emp.EmpPulse( + xform.Coordinates, + 0.2f, + ent.Comp.ScreamEmpEnergyConsumption, + TimeSpan.FromSeconds(ent.Comp.ScreamRobotDisableSeconds), + ent.Owner); + } + } + + if (ent.Comp.ScreamSound != null) + _audio.PlayPvs(ent.Comp.ScreamSound, ent.Owner); + + args.Handled = true; + } + + private void OnLayEggAction(Entity ent, ref TerrorSpiderPrincessLayEggActionEvent args) + { + if (args.Handled) + return; + + if (!CanLayEgg(ent, out _)) + return; + + var egg = Spawn(args.EggPrototype, Transform(ent.Owner).Coordinates); + var eggComp = EnsureComp(egg); + eggComp.Princess = ent.Owner; + eggComp.HatchAt = _timing.CurTime + TimeSpan.FromSeconds(eggComp.HatchDelay); + args.Handled = true; + } + + private bool CanLayEgg(Entity ent, out int broodCount) + { + broodCount = 0; + + var eggs = EntityQueryEnumerator(); + while (eggs.MoveNext(out _, out var eggComp)) + { + if (eggComp.Princess == ent.Owner) + return false; + } + + var mapId = Transform(ent.Owner).MapID; + var brood = EntityQueryEnumerator(); + while (brood.MoveNext(out _, out var broodComp, out var state, out var xform)) + { + if (broodComp.Princess != ent.Owner) + continue; + + if (state.CurrentState != MobState.Alive) + continue; + + if (xform.MapID != mapId) + continue; + + broodCount++; + } + + return broodCount < ent.Comp.MaxBroodOnMap; + } + + private void UpdateRemoteViewMovementLock() + { + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var comp)) + { + if (!TryComp(uid, out var eye)) + continue; + + var speedMultiplier = eye.Target == null ? 1f : 0f; + _movementModStatus.TryUpdateMovementSpeedModDuration( + uid, + comp.RemoteViewImmobileStatusEffect, + TimeSpan.FromSeconds(comp.RemoteViewImmobileRefresh), + speedMultiplier); + } + } + + private void UpdateRemoteViewMovementLock(Entity ent, EntityUid? currentTarget) + { + var speedMultiplier = currentTarget == null ? 1f : 0f; + _movementModStatus.TryUpdateMovementSpeedModDuration( + ent.Owner, + ent.Comp.RemoteViewImmobileStatusEffect, + TimeSpan.FromSeconds(ent.Comp.RemoteViewImmobileRefresh), + speedMultiplier); + } + + private void UpdateEggs(TimeSpan now) + { + var eggs = EntityQueryEnumerator(); + while (eggs.MoveNext(out var uid, out var egg, out var xform)) + { + if (egg.HatchAt == TimeSpan.Zero) + egg.HatchAt = now + TimeSpan.FromSeconds(egg.HatchDelay); + + if (now < egg.HatchAt) + continue; + + var hatchProto = SelectHatchPrototype(egg, xform.MapID); + if (hatchProto == null) + { + QueueDel(uid); + continue; + } + + var spawned = Spawn(hatchProto, xform.Coordinates); + if (egg.Princess != null && !Deleted(egg.Princess.Value)) + { + var brood = EnsureComp(spawned); + brood.Princess = egg.Princess; + brood.Elite = egg.TierTwo && egg.TierTwoElitePrototypes.Contains(hatchProto); + } + + QueueDel(uid); + } + } + + private void UpdateOrphanDamage(TimeSpan now) + { + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var orphan, out var mobState)) + { + if (mobState.CurrentState != MobState.Alive) + continue; + + if (orphan.NextTick == TimeSpan.Zero) + orphan.NextTick = now + TimeSpan.FromSeconds(orphan.TickInterval); + + if (now < orphan.NextTick) + continue; + + orphan.NextTick = now + TimeSpan.FromSeconds(orphan.TickInterval); + var damage = new DamageSpecifier(); + damage.DamageDict["Blunt"] = FixedPoint2.New(orphan.DamagePerTick); + _damageable.TryChangeDamage(uid, damage, ignoreResistances: false, interruptsDoAfters: false); + } + } + + private string? SelectHatchPrototype(TerrorSpiderPrincessEggComponent egg, MapId mapId) + { + if (!egg.TierTwo) + { + if (egg.TierOnePrototypes.Count == 0) + return null; + + return _random.Pick(egg.TierOnePrototypes); + } + + var allowed = egg.TierTwoPrototypes; + + if (egg.Princess != null + && TryComp(egg.Princess.Value, out var princessComp) + && CountEliteBroodOnMap(egg.Princess.Value, mapId) >= princessComp.MaxEliteBroodOnMap) + { + allowed = egg.TierTwoUnlimitedPrototypes; + } + + if (allowed.Count == 0) + return null; + + return _random.Pick(allowed); + } + + private int CountEliteBroodOnMap(EntityUid princess, MapId mapId) + { + var count = 0; + var query = EntityQueryEnumerator(); + while (query.MoveNext(out _, out var brood, out var mob, out var xform)) + { + if (brood.Princess != princess || !brood.Elite) + continue; + + if (mob.CurrentState != MobState.Alive) + continue; + + if (xform.MapID != mapId) + continue; + + count++; + } + + return count; + } + + + + private bool IsTerrorSpider(EntityUid uid) + { + return HasComp(uid) || _tagSystem.HasTag(uid, _terrorSpiderTag); + } + + private List BuildHiveSenseLines(EntityUid princess) + { + var lines = new List(); + var query = EntityQueryEnumerator(); + + while (query.MoveNext(out var uid, out var brood, out var mobState, out var xform)) + { + if (brood.Princess != princess) + continue; + + if (mobState.CurrentState != MobState.Alive) + continue; + + var hp = 0f; + var hpMax = 0f; + + if (TryComp(uid, out var damageable) + && TryComp(uid, out var thresholds)) + { + foreach (var (value, state) in thresholds.Thresholds) + { + if (state == MobState.Dead) + { + hpMax = value.Float(); + break; + } + } + + hp = MathF.Max(0, hpMax - damageable.TotalDamage.Float()); + } + + var name = MetaData(uid).EntityName; + var beacon = FindNearestBeaconName(xform.MapPosition); + lines.Add(Loc.GetString("terror-spider-hive-sense-entry", ("name", name), ("hp", $"{hp:0}"), ("hpMax", $"{hpMax:0}"), ("beacon", beacon))); + } + + return lines; + } + + private string FindNearestBeaconName(MapCoordinates from) + { + EntityUid? nearest = null; + var bestDistance = float.MaxValue; + var query = EntityQueryEnumerator(); + + while (query.MoveNext(out var uid, out var beacon, out var xform)) + { + if (!beacon.Enabled) + continue; + + if (xform.MapPosition.MapId != from.MapId) + continue; + + var distance = (xform.MapPosition.Position - from.Position).LengthSquared(); + if (distance >= bestDistance) + continue; + + bestDistance = distance; + nearest = uid; + } + + if (nearest == null) + return Loc.GetString("terror-spider-hive-sense-unknown"); + + if (TryComp(nearest, out var beaconComp) && !string.IsNullOrWhiteSpace(beaconComp.Text)) + return beaconComp.Text; + + return MetaData(nearest.Value).EntityName; + } + + private List GetRemoteViewCandidates(EntityUid princess) + { + var candidates = new List(); + var princessPos = Transform(princess).MapPosition; + var query = EntityQueryEnumerator(); + + while (query.MoveNext(out var uid, out var mob, out var xform)) + { + if (uid == princess) + continue; + + if (mob.CurrentState != MobState.Alive) + continue; + + if (!IsTerrorSpider(uid)) + continue; + + if (xform.MapPosition.MapId != princessPos.MapId) + continue; + + candidates.Add(uid); + } + + candidates.Sort((a, b) => a.Id.CompareTo(b.Id)); + return candidates; + } +} diff --git a/Content.Server/Imperial/TerrorSpider/Systems/TerrorSpiderQueenSystem.cs b/Content.Server/Imperial/TerrorSpider/Systems/TerrorSpiderQueenSystem.cs new file mode 100644 index 00000000000..0e5f865a8a3 --- /dev/null +++ b/Content.Server/Imperial/TerrorSpider/Systems/TerrorSpiderQueenSystem.cs @@ -0,0 +1,594 @@ +using Content.Server.Actions; +using Content.Server.Chat.Managers; +using Content.Server.Imperial.TerrorSpider.Components; +using Content.Server.Light.EntitySystems; +using Content.Shared.Damage; +using Content.Shared.Damage.Components; +using Content.Shared.Damage.Systems; +using Content.Shared.Emp; +using Content.Shared.FixedPoint; +using Content.Shared.Imperial.TerrorSpider.Events; +using Content.Shared.Light.Components; +using Content.Shared.Mobs; +using Content.Shared.Mobs.Components; +using Content.Shared.Movement.Systems; +using Content.Shared.Pinpointer; +using Content.Shared.Silicons.Laws.Components; +using Content.Shared.Tag; +using Content.Shared.Imperial.TerrorSpider.Components; +using Content.Shared.Weapons.Melee; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Map; +using Robust.Shared.Player; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; + +namespace Content.Server.Imperial.TerrorSpider.Systems; + +public sealed class TerrorSpiderQueenSystem : EntitySystem +{ + private static readonly ProtoId _terrorSpiderTag = "TerrorSpider"; + + [Dependency] private readonly ActionsSystem _actions = default!; + [Dependency] private readonly IChatManager _chat = default!; + [Dependency] private readonly DamageableSystem _damageable = default!; + [Dependency] private readonly SharedEmpSystem _emp = default!; + [Dependency] private readonly SharedEyeSystem _eye = default!; + [Dependency] private readonly MovementModStatusSystem _movementModStatus = default!; + [Dependency] private readonly PoweredLightSystem _poweredLight = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedStaminaSystem _stamina = default!; + [Dependency] private readonly TagSystem _tagSystem = default!; + [Dependency] private readonly IGameTiming _timing = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnMapInit); + SubscribeLocalEvent(OnShutdown); + SubscribeLocalEvent(OnMobStateChanged); + SubscribeLocalEvent(OnRemoteViewNextAction); + SubscribeLocalEvent(OnRemoteViewPreviousAction); + SubscribeLocalEvent(OnRemoteViewExitAction); + SubscribeLocalEvent(OnCreateHiveAction); + SubscribeLocalEvent(OnScreamAction); + SubscribeLocalEvent(OnHiveCountAction); + SubscribeLocalEvent(OnLayEggAction); + SubscribeLocalEvent(OnEggMapInit); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var now = _timing.CurTime; + UpdateRemoteViewMovementLock(); + UpdateHiveSpeedDebuff(); + UpdateEggs(now); + UpdateOrphanDamage(now); + } + + private void OnMapInit(EntityUid uid, TerrorSpiderQueenComponent comp, MapInitEvent args) + { + _actions.AddAction(uid, ref comp.CreateHiveActionEntity, comp.CreateHiveAction); + _actions.AddAction(uid, ref comp.ScreamActionEntity, comp.ScreamAction); + _actions.AddAction(uid, ref comp.RemoteViewNextActionEntity, comp.RemoteViewNextAction); + _actions.AddAction(uid, ref comp.RemoteViewPreviousActionEntity, comp.RemoteViewPreviousAction); + _actions.AddAction(uid, ref comp.RemoteViewExitActionEntity, comp.RemoteViewExitAction); + + if (comp.HiveCreated) + AddHiveActions(uid, comp); + } + + private void OnShutdown(EntityUid uid, TerrorSpiderQueenComponent comp, ComponentShutdown args) + { + _actions.RemoveAction(uid, comp.CreateHiveActionEntity); + _actions.RemoveAction(uid, comp.ScreamActionEntity); + _actions.RemoveAction(uid, comp.RemoteViewNextActionEntity); + _actions.RemoveAction(uid, comp.RemoteViewPreviousActionEntity); + _actions.RemoveAction(uid, comp.RemoteViewExitActionEntity); + RemoveHiveActions(uid, comp); + } + + private void OnMobStateChanged(Entity ent, ref MobStateChangedEvent args) + { + if (args.NewMobState != MobState.Dead) + return; + + var broodQuery = EntityQueryEnumerator(); + while (broodQuery.MoveNext(out var uid, out var brood)) + { + if (brood.Queen != ent.Owner) + continue; + + var orphan = EnsureComp(uid); + orphan.DamagePerTick = ent.Comp.OrphanDamagePerTick; + orphan.TickInterval = ent.Comp.OrphanDamageInterval; + orphan.NextTick = TimeSpan.Zero; + } + } + + private void OnEggMapInit(Entity ent, ref MapInitEvent args) + { + if (ent.Comp.HatchAt != TimeSpan.Zero) + return; + + ent.Comp.HatchAt = _timing.CurTime + TimeSpan.FromSeconds(ent.Comp.HatchDelay); + } + + private void OnRemoteViewNextAction(Entity ent, ref TerrorSpiderMotherRemoteViewNextActionEvent args) + { + if (args.Handled) + return; + + if (!TryComp(ent.Owner, out _)) + return; + + var candidates = GetRemoteViewCandidates(ent.Owner); + + if (candidates.Count == 0) + { + _eye.SetTarget(ent.Owner, null); + ent.Comp.RemoteViewIndex = -1; + UpdateRemoteViewMovementLock(ent, null); + args.Handled = true; + return; + } + + if (ent.Comp.RemoteViewIndex < -1 || ent.Comp.RemoteViewIndex >= candidates.Count) + ent.Comp.RemoteViewIndex = -1; + + ent.Comp.RemoteViewIndex = (ent.Comp.RemoteViewIndex + 1) % candidates.Count; + _eye.SetTarget(ent.Owner, candidates[ent.Comp.RemoteViewIndex]); + UpdateRemoteViewMovementLock(ent, candidates[ent.Comp.RemoteViewIndex]); + args.Handled = true; + } + + private void OnRemoteViewPreviousAction(Entity ent, ref TerrorSpiderMotherRemoteViewPreviousActionEvent args) + { + if (args.Handled) + return; + + if (!TryComp(ent.Owner, out _)) + return; + + var candidates = GetRemoteViewCandidates(ent.Owner); + + if (candidates.Count == 0) + { + _eye.SetTarget(ent.Owner, null); + ent.Comp.RemoteViewIndex = -1; + UpdateRemoteViewMovementLock(ent, null); + args.Handled = true; + return; + } + + if (ent.Comp.RemoteViewIndex < 0 || ent.Comp.RemoteViewIndex >= candidates.Count) + ent.Comp.RemoteViewIndex = 0; + + ent.Comp.RemoteViewIndex = (ent.Comp.RemoteViewIndex - 1 + candidates.Count) % candidates.Count; + _eye.SetTarget(ent.Owner, candidates[ent.Comp.RemoteViewIndex]); + UpdateRemoteViewMovementLock(ent, candidates[ent.Comp.RemoteViewIndex]); + args.Handled = true; + } + + private void OnRemoteViewExitAction(Entity ent, ref TerrorSpiderMotherRemoteViewExitActionEvent args) + { + if (args.Handled) + return; + + if (!TryComp(ent.Owner, out _)) + return; + + ent.Comp.RemoteViewIndex = -1; + _eye.SetTarget(ent.Owner, null); + UpdateRemoteViewMovementLock(ent, null); + args.Handled = true; + } + + private void OnCreateHiveAction(Entity ent, ref TerrorSpiderQueenCreateHiveActionEvent args) + { + if (args.Handled) + return; + + if (ent.Comp.HiveCreated) + return; + + ent.Comp.HiveCreated = true; + AddHiveActions(ent.Owner, ent.Comp); + _actions.RemoveAction(ent.Owner, ent.Comp.CreateHiveActionEntity); + ent.Comp.CreateHiveActionEntity = null; + + if (TryComp(ent.Owner, out var melee)) + { + melee.Damage.DamageDict["Structural"] = FixedPoint2.New(ent.Comp.HiveStructuralDamage); + Dirty(ent.Owner, melee); + } + + ApplyScream(ent.Owner, ent.Comp); + args.Handled = true; + } + + private void OnScreamAction(Entity ent, ref TerrorSpiderQueenScreamActionEvent args) + { + if (args.Handled) + return; + + ApplyScream(ent.Owner, ent.Comp); + args.Handled = true; + } + + private void OnHiveCountAction(Entity ent, ref TerrorSpiderQueenHiveCountActionEvent args) + { + if (args.Handled) + return; + + if (!TryComp(ent.Owner, out var actor)) + return; + + var lines = BuildHiveSenseLines(ent.Owner); + if (lines.Count == 0) + lines.Add(Loc.GetString("terror-spider-hive-sense-empty")); + + foreach (var line in lines) + { + _chat.DispatchServerMessage(actor.PlayerSession, line); + } + + args.Handled = true; + } + + private List BuildHiveSenseLines(EntityUid queen) + { + var lines = new List(); + var query = EntityQueryEnumerator(); + + while (query.MoveNext(out var uid, out var brood, out var mobState, out var xform)) + { + if (brood.Queen != queen) + continue; + + if (mobState.CurrentState != MobState.Alive) + continue; + + var hp = 0f; + var hpMax = 0f; + + if (TryComp(uid, out var damageable) + && TryComp(uid, out var thresholds)) + { + foreach (var (value, state) in thresholds.Thresholds) + { + if (state == MobState.Dead) + { + hpMax = value.Float(); + break; + } + } + + hp = MathF.Max(0, hpMax - damageable.TotalDamage.Float()); + } + + var name = MetaData(uid).EntityName; + var beacon = FindNearestBeaconName(xform.MapPosition); + lines.Add(Loc.GetString("terror-spider-hive-sense-entry", ("name", name), ("hp", $"{hp:0}"), ("hpMax", $"{hpMax:0}"), ("beacon", beacon))); + } + + return lines; + } + + private string FindNearestBeaconName(MapCoordinates from) + { + EntityUid? nearest = null; + var bestDistance = float.MaxValue; + var query = EntityQueryEnumerator(); + + while (query.MoveNext(out var uid, out var beacon, out var xform)) + { + if (!beacon.Enabled) + continue; + + if (xform.MapPosition.MapId != from.MapId) + continue; + + var distance = (xform.MapPosition.Position - from.Position).LengthSquared(); + if (distance >= bestDistance) + continue; + + bestDistance = distance; + nearest = uid; + } + + if (nearest == null) + return Loc.GetString("terror-spider-hive-sense-unknown"); + + if (TryComp(nearest, out var beaconComp) && !string.IsNullOrWhiteSpace(beaconComp.Text)) + return beaconComp.Text; + + return MetaData(nearest.Value).EntityName; + } + + + + private void OnLayEggAction(Entity ent, ref TerrorSpiderQueenLayEggActionEvent args) + { + if (args.Handled) + return; + + if (!ent.Comp.HiveCreated) + return; + + var now = _timing.CurTime; + + if (!string.IsNullOrWhiteSpace(args.SharedCooldownKey) + && ent.Comp.SharedEggCooldownEnds.TryGetValue(args.SharedCooldownKey, out var sharedCooldownEnd) + && now < sharedCooldownEnd) + { + return; + } + + var royalGroup = !string.IsNullOrWhiteSpace(args.RoyalCooldownKey); + if (HasActiveEggInGroup(ent.Owner, royalGroup)) + return; + + if (!string.IsNullOrWhiteSpace(args.RoyalCooldownKey) + && ent.Comp.RoyalCooldownEnds.TryGetValue(args.RoyalCooldownKey, out var cooldownEnd) + && now < cooldownEnd) + { + return; + } + + var egg = Spawn(args.EggPrototype, Transform(ent.Owner).Coordinates); + var eggComp = EnsureComp(egg); + eggComp.Queen = ent.Owner; + eggComp.HatchAt = now + TimeSpan.FromSeconds(eggComp.HatchDelay); + eggComp.RoyalCooldownKey = args.RoyalCooldownKey; + + if (!string.IsNullOrWhiteSpace(args.SharedCooldownKey) && args.SharedCooldownSeconds > 0) + ent.Comp.SharedEggCooldownEnds[args.SharedCooldownKey] = now + TimeSpan.FromSeconds(args.SharedCooldownSeconds); + + if (!string.IsNullOrWhiteSpace(args.RoyalCooldownKey)) + ent.Comp.RoyalCooldownEnds[args.RoyalCooldownKey] = now + TimeSpan.FromSeconds(ent.Comp.RoyalEggCooldownSeconds); + + args.Handled = true; + } + + private bool HasActiveEggInGroup(EntityUid queen, bool royalGroup) + { + var eggs = EntityQueryEnumerator(); + while (eggs.MoveNext(out _, out var egg)) + { + if (egg.Queen != queen) + continue; + + var eggIsRoyal = !string.IsNullOrWhiteSpace(egg.RoyalCooldownKey); + if (eggIsRoyal == royalGroup) + return true; + } + + return false; + } + + private void UpdateRemoteViewMovementLock() + { + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var comp)) + { + if (!TryComp(uid, out var eye)) + continue; + + var speedMultiplier = eye.Target == null ? 1f : 0f; + _movementModStatus.TryUpdateMovementSpeedModDuration( + uid, + comp.RemoteViewImmobileStatusEffect, + TimeSpan.FromSeconds(comp.RemoteViewImmobileRefresh), + speedMultiplier); + } + } + + private void UpdateRemoteViewMovementLock(Entity ent, EntityUid? currentTarget) + { + var speedMultiplier = currentTarget == null ? 1f : 0f; + _movementModStatus.TryUpdateMovementSpeedModDuration( + ent.Owner, + ent.Comp.RemoteViewImmobileStatusEffect, + TimeSpan.FromSeconds(ent.Comp.RemoteViewImmobileRefresh), + speedMultiplier); + } + + private void UpdateHiveSpeedDebuff() + { + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var comp)) + { + if (!comp.HiveCreated) + continue; + + _movementModStatus.TryUpdateMovementSpeedModDuration( + uid, + comp.HiveSlowStatusEffect, + TimeSpan.FromSeconds(comp.HiveSlowRefresh), + comp.HiveSpeedMultiplier); + } + } + + private void UpdateEggs(TimeSpan now) + { + var eggs = EntityQueryEnumerator(); + while (eggs.MoveNext(out var uid, out var egg, out var xform)) + { + if (egg.HatchAt == TimeSpan.Zero) + egg.HatchAt = now + TimeSpan.FromSeconds(egg.HatchDelay); + + if (now < egg.HatchAt) + continue; + + if (string.IsNullOrWhiteSpace(egg.SpawnPrototype)) + { + QueueDel(uid); + continue; + } + + var spawned = Spawn(egg.SpawnPrototype, xform.Coordinates); + if (egg.Queen != null && !Deleted(egg.Queen.Value)) + { + var brood = EnsureComp(spawned); + brood.Queen = egg.Queen; + brood.RoyalCooldownKey = egg.RoyalCooldownKey; + } + + QueueDel(uid); + } + } + + private void UpdateOrphanDamage(TimeSpan now) + { + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var orphan, out var mobState)) + { + if (mobState.CurrentState != MobState.Alive) + continue; + + if (orphan.NextTick == TimeSpan.Zero) + orphan.NextTick = now + TimeSpan.FromSeconds(orphan.TickInterval); + + if (now < orphan.NextTick) + continue; + + orphan.NextTick = now + TimeSpan.FromSeconds(orphan.TickInterval); + var damage = new DamageSpecifier(); + damage.DamageDict["Blunt"] = FixedPoint2.New(orphan.DamagePerTick); + _damageable.TryChangeDamage(uid, damage, ignoreResistances: false, interruptsDoAfters: false); + } + } + + private void ApplyScream(EntityUid queen, TerrorSpiderQueenComponent comp) + { + var origin = Transform(queen).MapPosition; + var rangeSquared = comp.ScreamRange * comp.ScreamRange; + var query = EntityQueryEnumerator(); + + while (query.MoveNext(out var uid, out var mobState, out var xform)) + { + if (uid == queen) + continue; + + if (mobState.CurrentState != MobState.Alive) + continue; + + if (xform.MapPosition.MapId != origin.MapId) + continue; + + if ((xform.MapPosition.Position - origin.Position).LengthSquared() > rangeSquared) + continue; + + if (!IsTerrorSpider(uid)) + { + _movementModStatus.TryUpdateMovementSpeedModDuration( + uid, + MovementModStatusSystem.TaserSlowdown, + TimeSpan.FromSeconds(comp.ScreamSlowDuration), + comp.ScreamSlowMultiplier); + + _stamina.TakeStaminaDamage(uid, comp.ScreamStaminaDamage, source: queen); + } + + if (HasComp(uid)) + { + _emp.EmpPulse( + xform.Coordinates, + 0.2f, + comp.ScreamEmpEnergyConsumption, + TimeSpan.FromSeconds(comp.ScreamRobotDisableSeconds), + queen); + } + } + + BreakLightsInRange(queen, comp.LightBreakHalfRange); + + if (comp.ScreamSound != null) + _audio.PlayPvs(comp.ScreamSound, queen); + } + + private void BreakLightsInRange(EntityUid queen, float halfRange) + { + var queenPos = Transform(queen).MapPosition; + var lightQuery = EntityQueryEnumerator(); + while (lightQuery.MoveNext(out var uid, out var light, out var xform)) + { + if (xform.MapPosition.MapId != queenPos.MapId) + continue; + + var delta = xform.MapPosition.Position - queenPos.Position; + if (MathF.Abs(delta.X) > halfRange || MathF.Abs(delta.Y) > halfRange) + continue; + + _poweredLight.TryDestroyBulb(uid, light); + } + } + + private List GetRemoteViewCandidates(EntityUid queen) + { + var candidates = new List(); + var queenPos = Transform(queen).MapPosition; + var query = EntityQueryEnumerator(); + + while (query.MoveNext(out var uid, out var mob, out var xform)) + { + if (uid == queen) + continue; + + if (mob.CurrentState != MobState.Alive) + continue; + + if (!IsTerrorSpider(uid)) + continue; + + if (xform.MapPosition.MapId != queenPos.MapId) + continue; + + candidates.Add(uid); + } + + candidates.Sort((a, b) => a.Id.CompareTo(b.Id)); + return candidates; + } + + private bool IsTerrorSpider(EntityUid uid) + { + return HasComp(uid) || _tagSystem.HasTag(uid, _terrorSpiderTag); + } + + private void AddHiveActions(EntityUid uid, TerrorSpiderQueenComponent comp) + { + _actions.AddAction(uid, ref comp.HiveCountActionEntity, comp.HiveCountAction); + _actions.AddAction(uid, ref comp.LayEggRusarActionEntity, comp.LayEggRusarAction); + _actions.AddAction(uid, ref comp.LayEggDronActionEntity, comp.LayEggDronAction); + _actions.AddAction(uid, ref comp.LayEggLurkerActionEntity, comp.LayEggLurkerAction); + _actions.AddAction(uid, ref comp.LayEggHealerActionEntity, comp.LayEggHealerAction); + _actions.AddAction(uid, ref comp.LayEggReaperActionEntity, comp.LayEggReaperAction); + _actions.AddAction(uid, ref comp.LayEggWidowActionEntity, comp.LayEggWidowAction); + _actions.AddAction(uid, ref comp.LayEggGuardianActionEntity, comp.LayEggGuardianAction); + _actions.AddAction(uid, ref comp.LayEggDestroyerActionEntity, comp.LayEggDestroyerAction); + _actions.AddAction(uid, ref comp.LayEggPrinceActionEntity, comp.LayEggPrinceAction); + _actions.AddAction(uid, ref comp.LayEggPrincessActionEntity, comp.LayEggPrincessAction); + _actions.AddAction(uid, ref comp.LayEggMotherActionEntity, comp.LayEggMotherAction); + } + + private void RemoveHiveActions(EntityUid uid, TerrorSpiderQueenComponent comp) + { + _actions.RemoveAction(uid, comp.HiveCountActionEntity); + _actions.RemoveAction(uid, comp.LayEggRusarActionEntity); + _actions.RemoveAction(uid, comp.LayEggDronActionEntity); + _actions.RemoveAction(uid, comp.LayEggLurkerActionEntity); + _actions.RemoveAction(uid, comp.LayEggHealerActionEntity); + _actions.RemoveAction(uid, comp.LayEggReaperActionEntity); + _actions.RemoveAction(uid, comp.LayEggWidowActionEntity); + _actions.RemoveAction(uid, comp.LayEggGuardianActionEntity); + _actions.RemoveAction(uid, comp.LayEggDestroyerActionEntity); + _actions.RemoveAction(uid, comp.LayEggPrinceActionEntity); + _actions.RemoveAction(uid, comp.LayEggPrincessActionEntity); + _actions.RemoveAction(uid, comp.LayEggMotherActionEntity); + } +} diff --git a/Content.Server/Imperial/TerrorSpider/Systems/TerrorSpiderReaperLifestealSystem.cs b/Content.Server/Imperial/TerrorSpider/Systems/TerrorSpiderReaperLifestealSystem.cs new file mode 100644 index 00000000000..95b3cc42e22 --- /dev/null +++ b/Content.Server/Imperial/TerrorSpider/Systems/TerrorSpiderReaperLifestealSystem.cs @@ -0,0 +1,71 @@ +using Content.Server.Imperial.TerrorSpider.Components; +using Content.Shared.Damage.Components; +using Content.Shared.Damage; +using Content.Shared.Damage.Systems; +using Content.Shared.Mobs; +using Content.Shared.Mobs.Components; +using Content.Shared.Weapons.Melee.Events; + +namespace Content.Server.Imperial.TerrorSpider.Systems; + +public sealed class TerrorSpiderReaperLifestealSystem : EntitySystem +{ + [Dependency] private readonly DamageableSystem _damageable = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnMeleeHit); + } + + private void OnMeleeHit(Entity ent, ref MeleeHitEvent args) + { + if (!args.IsHit || args.HitEntities.Count == 0) + return; + + var attacker = args.User; + + if (attacker != ent.Owner) + attacker = ent.Owner; + + if (!TryComp(attacker, out var damageable)) + return; + + var validHits = 0; + foreach (var target in args.HitEntities) + { + if (target == attacker) + continue; + + if (!TryComp(target, out var mobState)) + continue; + + if (mobState.CurrentState != MobState.Alive) + continue; + + validHits++; + } + + if (validHits <= 0) + return; + + var totalHeal = ent.Comp.HealAmount * validHits; + + var heal = new DamageSpecifier(); + foreach (var group in damageable.DamagePerGroup.Keys) + { + heal.DamageDict[group] = -totalHeal; + } + + foreach (var damageType in damageable.Damage.DamageDict.Keys) + { + heal.DamageDict[damageType] = -totalHeal; + } + + if (heal.Empty) + return; + + _damageable.TryChangeDamage(attacker, heal, ignoreResistances: true, interruptsDoAfters: false); + } +} diff --git a/Content.Server/Imperial/TerrorSpider/Systems/TerrorSpiderRoyalSystem.cs b/Content.Server/Imperial/TerrorSpider/Systems/TerrorSpiderRoyalSystem.cs new file mode 100644 index 00000000000..8c6b2cbaf42 --- /dev/null +++ b/Content.Server/Imperial/TerrorSpider/Systems/TerrorSpiderRoyalSystem.cs @@ -0,0 +1,106 @@ +using Content.Server.Actions; +using Content.Server.Imperial.TerrorSpider.Components; +using Content.Shared.Damage; +using Content.Shared.Damage.Components; +using Content.Shared.Damage.Systems; +using Content.Shared.FixedPoint; +using Content.Shared.Imperial.TerrorSpider.Events; +using Content.Shared.Mobs; +using Content.Shared.Mobs.Components; +using Content.Shared.Movement.Systems; +using Content.Shared.Imperial.TerrorSpider.Components; +using Content.Shared.Spider; +using Robust.Shared.Audio.Systems; + +namespace Content.Server.Imperial.TerrorSpider.Systems; + +public sealed class TerrorSpiderRoyalSystem : EntitySystem +{ + [Dependency] private readonly ActionsSystem _actions = default!; + [Dependency] private readonly DamageableSystem _damageable = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly MovementModStatusSystem _movementModStatus = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnMapInit); + SubscribeLocalEvent(OnShutdown); + SubscribeLocalEvent(OnStompAction); + } + + private void OnMapInit(EntityUid uid, TerrorSpiderRoyalComponent comp, MapInitEvent args) + { + if (!comp.StompEnabled) + return; + + _actions.AddAction(uid, ref comp.StompActionEntity, comp.StompAction); + } + + private void OnShutdown(EntityUid uid, TerrorSpiderRoyalComponent comp, ComponentShutdown args) + { + if (!comp.StompEnabled) + return; + + _actions.RemoveAction(uid, comp.StompActionEntity); + } + + private void OnStompAction(Entity ent, ref TerrorSpiderRoyalStompActionEvent args) + { + if (args.Handled) + return; + + if (!ent.Comp.StompEnabled) + return; + + var origin = Transform(ent.Owner).MapPosition; + var radiusSquared = ent.Comp.StompRadius * ent.Comp.StompRadius; + var query = EntityQueryEnumerator(); + var targets = new List(); + + while (query.MoveNext(out var uid, out var xform)) + { + if (uid == ent.Owner) + continue; + + if (xform.MapPosition.MapId != origin.MapId) + continue; + + if ((xform.MapPosition.Position - origin.Position).LengthSquared() > radiusSquared) + continue; + + if (HasComp(uid) || HasComp(uid)) + continue; + + if (!TryComp(uid, out var mobState) || mobState.CurrentState != MobState.Alive) + continue; + + targets.Add(uid); + } + + foreach (var uid in targets) + { + if (Deleted(uid)) + continue; + + _movementModStatus.TryUpdateMovementSpeedModDuration( + uid, + ent.Comp.StompSlowStatusEffect, + TimeSpan.FromSeconds(ent.Comp.StompSlowDuration), + ent.Comp.StompSlowMultiplier); + + if (!TryComp(uid, out _)) + continue; + + var damage = new DamageSpecifier(); + damage.DamageDict[ent.Comp.StompDamageType] = FixedPoint2.New(ent.Comp.StompDamage); + _damageable.TryChangeDamage(uid, damage, ignoreResistances: false, interruptsDoAfters: false); + } + + if (ent.Comp.StompSound != null) + _audio.PlayPvs(ent.Comp.StompSound, ent.Owner); + + args.Handled = true; + } +} diff --git a/Content.Server/Imperial/TerrorSpider/Systems/TerrorSpiderWebActionSystem.cs b/Content.Server/Imperial/TerrorSpider/Systems/TerrorSpiderWebActionSystem.cs new file mode 100644 index 00000000000..a07e98afb6e --- /dev/null +++ b/Content.Server/Imperial/TerrorSpider/Systems/TerrorSpiderWebActionSystem.cs @@ -0,0 +1,83 @@ +using Content.Server.Popups; +using Content.Shared.Imperial.TerrorSpider.Events; +using Content.Shared.Maps; +using Content.Shared.Spider; +using Robust.Shared.Map; +using Robust.Shared.Prototypes; + +namespace Content.Server.Imperial.TerrorSpider.Systems; + +public sealed class TerrorSpiderWebActionSystem : EntitySystem +{ + private const string GuardianBarrierPrototype = "SpiderWebTerrorGuardianBarrier"; + private static readonly EntProtoId DefaultWebPrototype = "SpiderWeb"; + + [Dependency] private readonly PopupSystem _popup = default!; + [Dependency] private readonly TurfSystem _turf = default!; + + private readonly HashSet _webs = []; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnSpawnWebAction); + } + + private void OnSpawnWebAction(TerrorSpiderSpawnWebActionEvent args) + { + if (args.Handled) + return; + + var performer = args.Performer; + var webPrototype = args.WebPrototype; + + if (string.IsNullOrWhiteSpace(webPrototype) && TryComp(performer, out var spider)) + webPrototype = spider.WebPrototype; + + if (string.IsNullOrWhiteSpace(webPrototype)) + webPrototype = DefaultWebPrototype; + + var transform = Transform(performer); + + if (transform.GridUid == null) + { + _popup.PopupEntity(Loc.GetString("spider-web-action-nogrid"), performer, performer); + return; + } + + if (IsTileBlockedByWeb(transform.Coordinates, webPrototype)) + { + _popup.PopupEntity(Loc.GetString("spider-web-action-fail"), performer, performer); + return; + } + + Spawn(webPrototype, transform.Coordinates); + _popup.PopupEntity(Loc.GetString("spider-web-action-success"), performer, performer); + args.Handled = true; + } + + private bool IsTileBlockedByWeb(EntityCoordinates coords, string targetPrototype) + { + _webs.Clear(); + _turf.GetEntitiesInTile(coords, _webs); + + var hasAnyWeb = false; + + foreach (var entity in _webs) + { + if (HasComp(entity)) + { + hasAnyWeb = true; + + if (MetaData(entity).EntityPrototype?.ID == targetPrototype) + return true; + } + } + + if (targetPrototype == GuardianBarrierPrototype) + return false; + + return hasAnyWeb; + } +} diff --git a/Content.Server/Imperial/TerrorSpider/Systems/TerrorSpiderWebBuffSystem.cs b/Content.Server/Imperial/TerrorSpider/Systems/TerrorSpiderWebBuffSystem.cs new file mode 100644 index 00000000000..571a100ad54 --- /dev/null +++ b/Content.Server/Imperial/TerrorSpider/Systems/TerrorSpiderWebBuffSystem.cs @@ -0,0 +1,7 @@ +using Content.Shared.Imperial.TerrorSpider.Systems; + +namespace Content.Server.Imperial.TerrorSpider.Systems; + +public sealed class TerrorSpiderWebBuffSystem : SharedTerrorSpiderWebBuffSystem +{ +} diff --git a/Content.Server/Imperial/TerrorSpider/Systems/TerrorSpiderWidowSystem.cs b/Content.Server/Imperial/TerrorSpider/Systems/TerrorSpiderWidowSystem.cs new file mode 100644 index 00000000000..4a0b6b00539 --- /dev/null +++ b/Content.Server/Imperial/TerrorSpider/Systems/TerrorSpiderWidowSystem.cs @@ -0,0 +1,115 @@ +using Content.Server.Body.Systems; +using Content.Server.Imperial.TerrorSpider.Components; +using Content.Shared.Body.Components; +using Content.Shared.Chemistry.Components; +using Content.Shared.Chemistry.EntitySystems; +using Content.Shared.Damage; +using Content.Shared.Damage.Systems; +using Content.Shared.FixedPoint; +using Content.Shared.Speech.Muting; +using Content.Shared.Spider; +using Content.Shared.StatusEffect; +using Content.Shared.Weapons.Melee.Events; +using Robust.Shared.Physics.Events; +using Robust.Shared.Timing; + +namespace Content.Server.Imperial.TerrorSpider.Systems; + +public sealed class TerrorSpiderWidowSystem : EntitySystem +{ + [Dependency] private readonly BloodstreamSystem _bloodstream = default!; + [Dependency] private readonly DamageableSystem _damageable = default!; + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly StatusEffectsSystem _statusEffects = default!; + [Dependency] private readonly SharedSolutionContainerSystem _solution = default!; + + private TimeSpan _nextVenomTick = TimeSpan.Zero; + private static readonly TimeSpan VenomTickInterval = TimeSpan.FromSeconds(1); + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnMeleeHit); + SubscribeLocalEvent(OnWebStartCollide); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + if (_timing.CurTime < _nextVenomTick) + return; + + _nextVenomTick = _timing.CurTime + VenomTickInterval; + + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out _)) + { + var venomAmount = _solution.GetTotalPrototypeQuantity(uid, "BlackTerrorVenom").Float(); + if (venomAmount <= 0f) + continue; + + var toxinDamage = GetVenomDamageByVolume(venomAmount); + if (toxinDamage <= 0f) + continue; + + var damage = new DamageSpecifier(); + damage.DamageDict["Toxin"] = FixedPoint2.New(toxinDamage); + _damageable.TryChangeDamage(uid, damage, ignoreResistances: false, interruptsDoAfters: false); + } + } + + private void OnMeleeHit(Entity ent, ref MeleeHitEvent args) + { + if (!args.IsHit || args.HitEntities.Count == 0) + return; + + foreach (var target in args.HitEntities) + { + if (TryComp(target, out var statusEffects)) + { + _statusEffects.TryAddStatusEffect( + target, + "Muted", + TimeSpan.FromSeconds(ent.Comp.MuteDuration), + true, + statusEffects); + } + + AddVenomToTarget(target, ent.Comp.VenomReagent, ent.Comp.VenomPerHit); + } + } + + private void OnWebStartCollide(Entity ent, ref StartCollideEvent args) + { + if (HasComp(args.OtherEntity)) + return; + + AddVenomToTarget(args.OtherEntity, ent.Comp.VenomReagent, ent.Comp.VenomOnTouch); + } + + private void AddVenomToTarget(EntityUid target, string reagentId, float amount) + { + if (!TryComp(target, out var bloodstream)) + return; + + var solution = new Solution(); + solution.AddReagent(reagentId, FixedPoint2.New(amount)); + _bloodstream.TryAddToChemicals((target, bloodstream), solution); + } + + private static float GetVenomDamageByVolume(float amount) + { + if (amount < 30f) + return 1f; + + if (amount < 60f) + return 2f; + + if (amount < 90f) + return 4f; + + return 8f; + } +} diff --git a/Content.Shared/Imperial/SCP/SCP096/SCP096Visuals.cs b/Content.Shared/Imperial/SCP/SCP096/SCP096Visuals.cs new file mode 100644 index 00000000000..c2eae81ea5d --- /dev/null +++ b/Content.Shared/Imperial/SCP/SCP096/SCP096Visuals.cs @@ -0,0 +1,18 @@ +using Robust.Shared.Serialization; + +namespace Content.Shared.Imperial.SCP.SCP096; + +[Serializable, NetSerializable] +public enum SCP096Visuals : byte +{ + State, +} + +[Serializable, NetSerializable] +public enum SCP096VisualState : byte +{ + Calm, + Screaming, + Chasing, + Dead, +} diff --git a/Content.Shared/Imperial/SCP/SCP173/SCP173LightFlickerActionEvent.cs b/Content.Shared/Imperial/SCP/SCP173/SCP173LightFlickerActionEvent.cs new file mode 100644 index 00000000000..876284b1353 --- /dev/null +++ b/Content.Shared/Imperial/SCP/SCP173/SCP173LightFlickerActionEvent.cs @@ -0,0 +1,5 @@ +using Content.Shared.Actions; + +namespace Content.Shared.Imperial.SCP.SCP173; + +public sealed partial class SCP173LightFlickerActionEvent : InstantActionEvent; \ No newline at end of file diff --git a/Content.Shared/Imperial/SCP/SCPBlink/SCPBlinkAlertEvent.cs b/Content.Shared/Imperial/SCP/SCPBlink/SCPBlinkAlertEvent.cs new file mode 100644 index 00000000000..8b7d0423b5a --- /dev/null +++ b/Content.Shared/Imperial/SCP/SCPBlink/SCPBlinkAlertEvent.cs @@ -0,0 +1,5 @@ +using Content.Shared.Alert; + +namespace Content.Shared.Imperial.SCP.SCPBlink; + +public sealed partial class SCPBlinkAlertEvent : BaseAlertEvent; diff --git a/Content.Shared/Imperial/SCP/SCPFireman/Events/SCPFiremanActionEvents.cs b/Content.Shared/Imperial/SCP/SCPFireman/Events/SCPFiremanActionEvents.cs new file mode 100644 index 00000000000..4b7aa8a7d26 --- /dev/null +++ b/Content.Shared/Imperial/SCP/SCPFireman/Events/SCPFiremanActionEvents.cs @@ -0,0 +1,17 @@ +using Content.Shared.Actions; + +namespace Content.Shared.Imperial.SCP.SCPFireman.Events; + +public sealed partial class SCPFiremanIgniteActionEvent : InstantActionEvent; + +public sealed partial class SCPFiremanFireballActionEvent : WorldTargetActionEvent; + +public sealed partial class SCPFiremanWhirlActionEvent : WorldTargetActionEvent; + +public sealed partial class SCPFiremanMeltActionEvent : EntityTargetActionEvent; + +public sealed partial class SCPFiremanTrueFlameActionEvent : InstantActionEvent; + +public sealed partial class SCPFiremanStrikeActionEvent : InstantActionEvent; + +public sealed partial class SCPFiremanSecondModeActionEvent : InstantActionEvent; diff --git a/Content.Shared/Imperial/TerrorSpider/Components/TerrorSpiderArmorComponent.cs b/Content.Shared/Imperial/TerrorSpider/Components/TerrorSpiderArmorComponent.cs new file mode 100644 index 00000000000..224d38cab70 --- /dev/null +++ b/Content.Shared/Imperial/TerrorSpider/Components/TerrorSpiderArmorComponent.cs @@ -0,0 +1,31 @@ +using Content.Shared.Imperial.TerrorSpider.Systems; +using Robust.Shared.GameStates; + +namespace Content.Shared.Imperial.TerrorSpider.Components; + +/// +/// Modifies incoming damage for terror spiders by damage group. +/// BruteModifier affects Blunt, Slash, Piercing (and the Brute group itself). +/// BurnModifier affects Heat, Shock, Cold, Caustic (and the Burn group itself). +/// Values below 1 reduce damage, above 1 increase it. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +[Access(typeof(SharedTerrorSpiderArmorSystem), typeof(SharedTerrorSpiderKnightGuardSystem), typeof(SharedTerrorSpiderKnightRageSystem))] +public sealed partial class TerrorSpiderArmorComponent : Component +{ + /// + /// Multiplier for Brute damage group (Blunt, Slash, Piercing). + /// Default 1.0 = no modification. + /// + [DataField] + [AutoNetworkedField] + public float BruteModifier = 1f; + + /// + /// Multiplier for Burn damage group (Heat, Shock, Cold, Caustic). + /// Default 1.0 = no modification. + /// + [DataField] + [AutoNetworkedField] + public float BurnModifier = 1f; +} diff --git a/Content.Shared/Imperial/TerrorSpider/Components/TerrorSpiderCocoonComponent.cs b/Content.Shared/Imperial/TerrorSpider/Components/TerrorSpiderCocoonComponent.cs new file mode 100644 index 00000000000..77e588a5ad4 --- /dev/null +++ b/Content.Shared/Imperial/TerrorSpider/Components/TerrorSpiderCocoonComponent.cs @@ -0,0 +1,28 @@ +using Content.Shared.Imperial.TerrorSpider.Systems; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; + +namespace Content.Shared.Imperial.TerrorSpider.Components; + +[RegisterComponent, NetworkedComponent, Access(typeof(SharedTerrorSpiderCocoonSystem)), AutoGenerateComponentState] +public sealed partial class TerrorSpiderCocoonComponent : Component +{ + [DataField] + [AutoNetworkedField] + public bool Enabled = true; + + [DataField] + [AutoNetworkedField] + public EntProtoId Action = "ActionTerrorSpiderCocoon"; + + public EntityUid? ActionEntity; + + [DataField] + [AutoNetworkedField] + public EntProtoId CocoonPrototype = "TerrorSpiderCocoon"; + + [DataField] + [AutoNetworkedField] + public float CocoonDelay = 4f; +} diff --git a/Content.Shared/Imperial/TerrorSpider/Components/TerrorSpiderCocoonPrisonComponent.cs b/Content.Shared/Imperial/TerrorSpider/Components/TerrorSpiderCocoonPrisonComponent.cs new file mode 100644 index 00000000000..0d3c45ae294 --- /dev/null +++ b/Content.Shared/Imperial/TerrorSpider/Components/TerrorSpiderCocoonPrisonComponent.cs @@ -0,0 +1,9 @@ +using Content.Shared.Imperial.TerrorSpider.Systems; +using Robust.Shared.GameStates; + +namespace Content.Shared.Imperial.TerrorSpider.Components; + +[RegisterComponent, NetworkedComponent, Access(typeof(SharedTerrorSpiderCocoonSystem))] +public sealed partial class TerrorSpiderCocoonPrisonComponent : Component +{ +} diff --git a/Content.Shared/Imperial/TerrorSpider/Components/TerrorSpiderGuardianShieldBarrierComponent.cs b/Content.Shared/Imperial/TerrorSpider/Components/TerrorSpiderGuardianShieldBarrierComponent.cs new file mode 100644 index 00000000000..262f075a1cc --- /dev/null +++ b/Content.Shared/Imperial/TerrorSpider/Components/TerrorSpiderGuardianShieldBarrierComponent.cs @@ -0,0 +1,10 @@ +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; + +namespace Content.Shared.Imperial.TerrorSpider.Components; + +[RegisterComponent] +public sealed partial class TerrorSpiderGuardianShieldBarrierComponent : Component +{ + [DataField] + public string PassTag = "TerrorSpider"; +} diff --git a/Content.Shared/Imperial/TerrorSpider/Components/TerrorSpiderKnightGuardComponent.cs b/Content.Shared/Imperial/TerrorSpider/Components/TerrorSpiderKnightGuardComponent.cs new file mode 100644 index 00000000000..0a99bec6818 --- /dev/null +++ b/Content.Shared/Imperial/TerrorSpider/Components/TerrorSpiderKnightGuardComponent.cs @@ -0,0 +1,57 @@ +using Content.Shared.Damage; +using Content.Shared.Imperial.TerrorSpider.Systems; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Imperial.TerrorSpider.Components; + +[RegisterComponent, NetworkedComponent, Access(typeof(SharedTerrorSpiderKnightGuardSystem)), AutoGenerateComponentState, AutoGenerateComponentPause] +public sealed partial class TerrorSpiderKnightGuardComponent : Component +{ + [DataField] + [AutoNetworkedField] + public EntProtoId Action = "ActionTerrorSpiderKnightGuard"; + + public EntityUid? ActionEntity; + + [DataField] + [AutoNetworkedField] + public float GuardDuration = 10f; + + [DataField] + [AutoNetworkedField] + public float GuardSpeedMultiplier = 0.5f; + + [DataField] + [AutoNetworkedField] + public float GuardMeleeDamage = 10f; + + [DataField] + [AutoNetworkedField] + public float BruteIncomingMultiplier = 0.4f; + + [DataField] + [AutoNetworkedField] + public float BurnIncomingMultiplier = 0.7f; + + [ViewVariables] + [AutoNetworkedField] + public bool IsGuarding; + + [ViewVariables] + [AutoPausedField] + [AutoNetworkedField] + public TimeSpan GuardEndTime; + + [ViewVariables] + public DamageSpecifier? CachedMeleeDamage; + + [ViewVariables] + public DamageSpecifier? CachedPassiveDamage; + + [ViewVariables] + public float CachedBruteModifier; + + [ViewVariables] + public float CachedBurnModifier; +} diff --git a/Content.Shared/Imperial/TerrorSpider/Components/TerrorSpiderKnightRageComponent.cs b/Content.Shared/Imperial/TerrorSpider/Components/TerrorSpiderKnightRageComponent.cs new file mode 100644 index 00000000000..5511626716f --- /dev/null +++ b/Content.Shared/Imperial/TerrorSpider/Components/TerrorSpiderKnightRageComponent.cs @@ -0,0 +1,59 @@ +using Content.Shared.Imperial.TerrorSpider.Systems; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; +using Content.Shared.Damage; + +namespace Content.Shared.Imperial.TerrorSpider.Components; + +[RegisterComponent, NetworkedComponent, Access(typeof(SharedTerrorSpiderKnightRageSystem)), AutoGenerateComponentState, AutoGenerateComponentPause] +public sealed partial class TerrorSpiderKnightRageComponent : Component +{ + [DataField] + [AutoNetworkedField] + public EntProtoId Action = "ActionTerrorSpiderKnightRage"; + + public EntityUid? ActionEntity; + + [DataField] + [AutoNetworkedField] + public float RageDuration = 10f; + + [DataField] + [AutoNetworkedField] + public float EnragedSpeedMultiplier = 1.33f; + + [DataField] + [AutoNetworkedField] + public float EnragedMeleeDamage = 30f; + + [DataField] + [AutoNetworkedField] + public float BruteIncomingMultiplier = 0.8f; + + [DataField] + [AutoNetworkedField] + public float BurnIncomingMultiplier = 1.2f; + + [ViewVariables] + [AutoNetworkedField] + public bool IsEnraged; + + [ViewVariables] + [AutoPausedField] + public TimeSpan RageEndTime; + + [ViewVariables] + public DamageSpecifier? CachedMeleeDamage; + + [ViewVariables] + public DamageSpecifier? CachedPassiveDamage; + + [ViewVariables] + public float CachedBruteModifier; + + [ViewVariables] + public float CachedBurnModifier; + + [ViewVariables] + public bool HasCachedArmor; +} diff --git a/Content.Shared/Imperial/TerrorSpider/Components/TerrorSpiderWebBuffAreaComponent.cs b/Content.Shared/Imperial/TerrorSpider/Components/TerrorSpiderWebBuffAreaComponent.cs new file mode 100644 index 00000000000..8b25485548a --- /dev/null +++ b/Content.Shared/Imperial/TerrorSpider/Components/TerrorSpiderWebBuffAreaComponent.cs @@ -0,0 +1,9 @@ +using Content.Shared.Imperial.TerrorSpider.Systems; +using Robust.Shared.GameStates; + +namespace Content.Shared.Imperial.TerrorSpider.Components; + +[RegisterComponent, NetworkedComponent, Access(typeof(SharedTerrorSpiderWebBuffSystem))] +public sealed partial class TerrorSpiderWebBuffAreaComponent : Component +{ +} diff --git a/Content.Shared/Imperial/TerrorSpider/Components/TerrorSpiderWebBuffReceiverComponent.cs b/Content.Shared/Imperial/TerrorSpider/Components/TerrorSpiderWebBuffReceiverComponent.cs new file mode 100644 index 00000000000..0971b6e8b08 --- /dev/null +++ b/Content.Shared/Imperial/TerrorSpider/Components/TerrorSpiderWebBuffReceiverComponent.cs @@ -0,0 +1,27 @@ +using Content.Shared.Imperial.TerrorSpider.Systems; +using Robust.Shared.GameStates; + +namespace Content.Shared.Imperial.TerrorSpider.Components; + +[RegisterComponent, NetworkedComponent, Access(typeof(SharedTerrorSpiderWebBuffSystem)), AutoGenerateComponentState] +public sealed partial class TerrorSpiderWebBuffReceiverComponent : Component +{ + [DataField] + [AutoNetworkedField] + public int WebContacts; + + [ViewVariables] + public TimeSpan NextRegenTick; + + [DataField] + [AutoNetworkedField] + public float RegenPerTick = 3f; + + [DataField] + [AutoNetworkedField] + public float RegenInterval = 2f; + + [DataField] + [AutoNetworkedField] + public float SpeedMultiplier = 1f; +} diff --git a/Content.Shared/Imperial/TerrorSpider/Components/TerrorSpiderWebSpeedBoostComponent.cs b/Content.Shared/Imperial/TerrorSpider/Components/TerrorSpiderWebSpeedBoostComponent.cs new file mode 100644 index 00000000000..1e2bb02796a --- /dev/null +++ b/Content.Shared/Imperial/TerrorSpider/Components/TerrorSpiderWebSpeedBoostComponent.cs @@ -0,0 +1,6 @@ +namespace Content.Shared.Imperial.TerrorSpider.Components; + +[RegisterComponent] +public sealed partial class TerrorSpiderWebSpeedBoostComponent : Component +{ +} diff --git a/Content.Shared/Imperial/TerrorSpider/Events/TerrorSpiderCocoonEvents.cs b/Content.Shared/Imperial/TerrorSpider/Events/TerrorSpiderCocoonEvents.cs new file mode 100644 index 00000000000..d9b205d22ca --- /dev/null +++ b/Content.Shared/Imperial/TerrorSpider/Events/TerrorSpiderCocoonEvents.cs @@ -0,0 +1,10 @@ +using Content.Shared.Actions; +using Content.Shared.DoAfter; +using Robust.Shared.Serialization; + +namespace Content.Shared.Imperial.TerrorSpider.Events; + +public sealed partial class TerrorSpiderCocoonActionEvent : EntityTargetActionEvent; + +[Serializable, NetSerializable] +public sealed partial class TerrorSpiderCocoonDoAfterEvent : SimpleDoAfterEvent; diff --git a/Content.Shared/Imperial/TerrorSpider/Events/TerrorSpiderCocoonWrappedEvent.cs b/Content.Shared/Imperial/TerrorSpider/Events/TerrorSpiderCocoonWrappedEvent.cs new file mode 100644 index 00000000000..ddbd20e70ad --- /dev/null +++ b/Content.Shared/Imperial/TerrorSpider/Events/TerrorSpiderCocoonWrappedEvent.cs @@ -0,0 +1,15 @@ +namespace Content.Shared.Imperial.TerrorSpider.Events; + +public sealed partial class TerrorSpiderCocoonWrappedEvent : EntityEventArgs +{ + public EntityUid User; + public EntityUid Target; + public EntityUid Cocoon; + + public TerrorSpiderCocoonWrappedEvent(EntityUid user, EntityUid target, EntityUid cocoon) + { + User = user; + Target = target; + Cocoon = cocoon; + } +} diff --git a/Content.Shared/Imperial/TerrorSpider/Events/TerrorSpiderDestroyerEvents.cs b/Content.Shared/Imperial/TerrorSpider/Events/TerrorSpiderDestroyerEvents.cs new file mode 100644 index 00000000000..b88c3080cd4 --- /dev/null +++ b/Content.Shared/Imperial/TerrorSpider/Events/TerrorSpiderDestroyerEvents.cs @@ -0,0 +1,7 @@ +using Content.Shared.Actions; + +namespace Content.Shared.Imperial.TerrorSpider.Events; + +public sealed partial class TerrorSpiderDestroyerEmpScreamActionEvent : InstantActionEvent; + +public sealed partial class TerrorSpiderDestroyerFireBurstActionEvent : InstantActionEvent; diff --git a/Content.Shared/Imperial/TerrorSpider/Events/TerrorSpiderHealerEvents.cs b/Content.Shared/Imperial/TerrorSpider/Events/TerrorSpiderHealerEvents.cs new file mode 100644 index 00000000000..9c19de09551 --- /dev/null +++ b/Content.Shared/Imperial/TerrorSpider/Events/TerrorSpiderHealerEvents.cs @@ -0,0 +1,13 @@ +using Content.Shared.Actions; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; + +namespace Content.Shared.Imperial.TerrorSpider.Events; + +public sealed partial class TerrorSpiderHealerPulseActionEvent : InstantActionEvent; + +public sealed partial class TerrorSpiderHealerLayEggActionEvent : InstantActionEvent +{ + [DataField(required: true)] + public EntProtoId EggPrototype; +} diff --git a/Content.Shared/Imperial/TerrorSpider/Events/TerrorSpiderKnightGuardEvents.cs b/Content.Shared/Imperial/TerrorSpider/Events/TerrorSpiderKnightGuardEvents.cs new file mode 100644 index 00000000000..11826278d72 --- /dev/null +++ b/Content.Shared/Imperial/TerrorSpider/Events/TerrorSpiderKnightGuardEvents.cs @@ -0,0 +1,5 @@ +using Content.Shared.Actions; + +namespace Content.Shared.Imperial.TerrorSpider.Events; + +public sealed partial class TerrorSpiderKnightGuardActionEvent : InstantActionEvent; diff --git a/Content.Shared/Imperial/TerrorSpider/Events/TerrorSpiderKnightRageEvents.cs b/Content.Shared/Imperial/TerrorSpider/Events/TerrorSpiderKnightRageEvents.cs new file mode 100644 index 00000000000..8ad54c75657 --- /dev/null +++ b/Content.Shared/Imperial/TerrorSpider/Events/TerrorSpiderKnightRageEvents.cs @@ -0,0 +1,5 @@ +using Content.Shared.Actions; + +namespace Content.Shared.Imperial.TerrorSpider.Events; + +public sealed partial class TerrorSpiderKnightRageActionEvent : InstantActionEvent; diff --git a/Content.Shared/Imperial/TerrorSpider/Events/TerrorSpiderLurkerEvents.cs b/Content.Shared/Imperial/TerrorSpider/Events/TerrorSpiderLurkerEvents.cs new file mode 100644 index 00000000000..570f8c84565 --- /dev/null +++ b/Content.Shared/Imperial/TerrorSpider/Events/TerrorSpiderLurkerEvents.cs @@ -0,0 +1,5 @@ +using Content.Shared.Actions; + +namespace Content.Shared.Imperial.TerrorSpider.Events; + +public sealed partial class TerrorSpiderLurkerStealthActionEvent : InstantActionEvent; diff --git a/Content.Shared/Imperial/TerrorSpider/Events/TerrorSpiderMotherEvents.cs b/Content.Shared/Imperial/TerrorSpider/Events/TerrorSpiderMotherEvents.cs new file mode 100644 index 00000000000..3124677c7e2 --- /dev/null +++ b/Content.Shared/Imperial/TerrorSpider/Events/TerrorSpiderMotherEvents.cs @@ -0,0 +1,13 @@ +using Content.Shared.Actions; + +namespace Content.Shared.Imperial.TerrorSpider.Events; + +public sealed partial class TerrorSpiderMotherPulseActionEvent : InstantActionEvent; + +public sealed partial class TerrorSpiderMotherRemoteViewNextActionEvent : InstantActionEvent; + +public sealed partial class TerrorSpiderMotherRemoteViewPreviousActionEvent : InstantActionEvent; + +public sealed partial class TerrorSpiderMotherRemoteViewExitActionEvent : InstantActionEvent; + +public sealed partial class TerrorSpiderMotherLayJellyActionEvent : InstantActionEvent; diff --git a/Content.Shared/Imperial/TerrorSpider/Events/TerrorSpiderPrincessEvents.cs b/Content.Shared/Imperial/TerrorSpider/Events/TerrorSpiderPrincessEvents.cs new file mode 100644 index 00000000000..f55f7bb1ada --- /dev/null +++ b/Content.Shared/Imperial/TerrorSpider/Events/TerrorSpiderPrincessEvents.cs @@ -0,0 +1,15 @@ +using Content.Shared.Actions; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; + +namespace Content.Shared.Imperial.TerrorSpider.Events; + +public sealed partial class TerrorSpiderPrincessHiveSenseActionEvent : InstantActionEvent; + +public sealed partial class TerrorSpiderPrincessScreamActionEvent : InstantActionEvent; + +public sealed partial class TerrorSpiderPrincessLayEggActionEvent : InstantActionEvent +{ + [DataField(required: true)] + public EntProtoId EggPrototype; +} diff --git a/Content.Shared/Imperial/TerrorSpider/Events/TerrorSpiderQueenEvents.cs b/Content.Shared/Imperial/TerrorSpider/Events/TerrorSpiderQueenEvents.cs new file mode 100644 index 00000000000..716149e0164 --- /dev/null +++ b/Content.Shared/Imperial/TerrorSpider/Events/TerrorSpiderQueenEvents.cs @@ -0,0 +1,26 @@ +using Content.Shared.Actions; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; + +namespace Content.Shared.Imperial.TerrorSpider.Events; + +public sealed partial class TerrorSpiderQueenCreateHiveActionEvent : InstantActionEvent; + +public sealed partial class TerrorSpiderQueenScreamActionEvent : InstantActionEvent; + +public sealed partial class TerrorSpiderQueenHiveCountActionEvent : InstantActionEvent; + +public sealed partial class TerrorSpiderQueenLayEggActionEvent : InstantActionEvent +{ + [DataField(required: true)] + public EntProtoId EggPrototype; + + [DataField] + public string? SharedCooldownKey; + + [DataField] + public float SharedCooldownSeconds; + + [DataField] + public string? RoyalCooldownKey; +} diff --git a/Content.Shared/Imperial/TerrorSpider/Events/TerrorSpiderReaperEvents.cs b/Content.Shared/Imperial/TerrorSpider/Events/TerrorSpiderReaperEvents.cs new file mode 100644 index 00000000000..b7fe19bc5d6 --- /dev/null +++ b/Content.Shared/Imperial/TerrorSpider/Events/TerrorSpiderReaperEvents.cs @@ -0,0 +1,5 @@ +using Content.Shared.Actions; + +namespace Content.Shared.Imperial.TerrorSpider.Events; + +public sealed partial class TerrorSpiderNoWebActionEvent : InstantActionEvent; diff --git a/Content.Shared/Imperial/TerrorSpider/Events/TerrorSpiderRoyalEvents.cs b/Content.Shared/Imperial/TerrorSpider/Events/TerrorSpiderRoyalEvents.cs new file mode 100644 index 00000000000..d2e0f045a4d --- /dev/null +++ b/Content.Shared/Imperial/TerrorSpider/Events/TerrorSpiderRoyalEvents.cs @@ -0,0 +1,5 @@ +using Content.Shared.Actions; + +namespace Content.Shared.Imperial.TerrorSpider.Events; + +public sealed partial class TerrorSpiderRoyalStompActionEvent : InstantActionEvent; diff --git a/Content.Shared/Imperial/TerrorSpider/Events/TerrorSpiderWebEvents.cs b/Content.Shared/Imperial/TerrorSpider/Events/TerrorSpiderWebEvents.cs new file mode 100644 index 00000000000..2880e013ec5 --- /dev/null +++ b/Content.Shared/Imperial/TerrorSpider/Events/TerrorSpiderWebEvents.cs @@ -0,0 +1,10 @@ +using Content.Shared.Actions; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Imperial.TerrorSpider.Events; + +public sealed partial class TerrorSpiderSpawnWebActionEvent : InstantActionEvent +{ + [DataField(required: true)] + public EntProtoId WebPrototype; +} diff --git a/Content.Shared/Imperial/TerrorSpider/Systems/SharedTerrorSpiderArmorSystem.cs b/Content.Shared/Imperial/TerrorSpider/Systems/SharedTerrorSpiderArmorSystem.cs new file mode 100644 index 00000000000..fa7e063c120 --- /dev/null +++ b/Content.Shared/Imperial/TerrorSpider/Systems/SharedTerrorSpiderArmorSystem.cs @@ -0,0 +1,60 @@ +using Content.Shared.Damage; +using Content.Shared.Damage.Systems; +using Content.Shared.FixedPoint; +using Content.Shared.Imperial.TerrorSpider.Components; + +namespace Content.Shared.Imperial.TerrorSpider.Systems; + +public abstract class SharedTerrorSpiderArmorSystem : EntitySystem +{ + // Brute damage types + private static readonly string[] BruteTypes = { "Blunt", "Slash", "Piercing" }; + + // Burn damage types + private static readonly string[] BurnTypes = { "Heat", "Shock", "Cold", "Caustic" }; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnDamageModify); + } + + private void OnDamageModify(Entity ent, ref DamageModifyEvent args) + { + var comp = ent.Comp; + + if (comp.BruteModifier == 1f && comp.BurnModifier == 1f) + return; + + // Apply Brute modifier to individual types and the group key + if (comp.BruteModifier != 1f) + { + foreach (var type in BruteTypes) + { + MultiplyDamageType(args.Damage, type, comp.BruteModifier); + } + + MultiplyDamageType(args.Damage, "Brute", comp.BruteModifier); + } + + // Apply Burn modifier to individual types and the group key + if (comp.BurnModifier != 1f) + { + foreach (var type in BurnTypes) + { + MultiplyDamageType(args.Damage, type, comp.BurnModifier); + } + + MultiplyDamageType(args.Damage, "Burn", comp.BurnModifier); + } + } + + private static void MultiplyDamageType(DamageSpecifier specifier, string key, float multiplier) + { + if (!specifier.DamageDict.TryGetValue(key, out var damage) || damage <= FixedPoint2.Zero) + return; + + specifier.DamageDict[key] = FixedPoint2.New(damage.Float() * multiplier); + } +} diff --git a/Content.Shared/Imperial/TerrorSpider/Systems/SharedTerrorSpiderCocoonSystem.cs b/Content.Shared/Imperial/TerrorSpider/Systems/SharedTerrorSpiderCocoonSystem.cs new file mode 100644 index 00000000000..85197149d9f --- /dev/null +++ b/Content.Shared/Imperial/TerrorSpider/Systems/SharedTerrorSpiderCocoonSystem.cs @@ -0,0 +1,148 @@ +using System.Linq; +using Content.Shared.Actions; +using Content.Shared.DoAfter; +using Content.Shared.Imperial.TerrorSpider.Components; +using Content.Shared.Imperial.TerrorSpider.Events; +using Content.Shared.Interaction; +using Content.Shared.Kitchen.Components; +using Content.Shared.Mobs; +using Content.Shared.Mobs.Components; +using Content.Shared.Popups; +using Robust.Shared.Containers; +using Robust.Shared.Network; + +namespace Content.Shared.Imperial.TerrorSpider.Systems; + +public abstract class SharedTerrorSpiderCocoonSystem : EntitySystem +{ + private const string CocoonContainerId = "cocoon-body"; + + [Dependency] private readonly INetManager _net = default!; + [Dependency] private readonly SharedActionsSystem _actions = default!; + [Dependency] private readonly SharedContainerSystem _container = default!; + [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnMapInit); + SubscribeLocalEvent(OnShutdown); + SubscribeLocalEvent(OnCocoonAction); + SubscribeLocalEvent(OnCocoonDoAfter); + SubscribeLocalEvent(OnCocoonInteractUsing); + } + + private void OnCocoonInteractUsing(Entity ent, ref InteractUsingEvent args) + { + if (args.Handled) + return; + + if (_net.IsClient) + return; + + var container = _container.EnsureContainer(ent.Owner, CocoonContainerId); + + if (container.ContainedEntities.Count == 0) + return; + + if (!HasComp(args.Used)) + { + var message = Loc.GetString("butcherable-need-knife", ("target", ent.Owner)); + _popup.PopupEntity(message, ent.Owner, args.User); + return; + } + + foreach (var contained in container.ContainedEntities.ToArray()) + { + _container.Remove(contained, container); + } + + Del(ent.Owner); + args.Handled = true; + } + + private void OnMapInit(EntityUid uid, TerrorSpiderCocoonComponent comp, MapInitEvent args) + { + if (!comp.Enabled) + return; + + _actions.AddAction(uid, ref comp.ActionEntity, comp.Action); + } + + private void OnShutdown(EntityUid uid, TerrorSpiderCocoonComponent comp, ComponentShutdown args) + { + if (!comp.Enabled) + return; + + _actions.RemoveAction(uid, comp.ActionEntity); + } + + private void OnCocoonAction(Entity ent, ref TerrorSpiderCocoonActionEvent args) + { + if (!ent.Comp.Enabled) + return; + + if (args.Handled) + return; + + var target = args.Target; + if (target == EntityUid.Invalid) + return; + + if (target == ent.Owner) + return; + + if (!TryComp(target, out var mobState) + || mobState.CurrentState != MobState.Alive) + { + return; + } + + var doAfter = new DoAfterArgs(EntityManager, + ent.Owner, + ent.Comp.CocoonDelay, + new TerrorSpiderCocoonDoAfterEvent(), + ent.Owner, + target: target) + { + BreakOnMove = true, + BreakOnDamage = true, + NeedHand = false, + }; + + if (!_doAfter.TryStartDoAfter(doAfter)) + return; + + args.Handled = true; + } + + private void OnCocoonDoAfter(Entity ent, ref TerrorSpiderCocoonDoAfterEvent args) + { + if (args.Cancelled || args.Handled) + return; + + if (args.Target is not EntityUid target || target == EntityUid.Invalid || Deleted(target) || target == ent.Owner) + return; + + if (!HasComp(target)) + return; + + if (_net.IsClient) + return; + + var cocoon = Spawn(ent.Comp.CocoonPrototype, Transform(target).Coordinates); + var container = _container.EnsureContainer(cocoon, CocoonContainerId); + + if (!_container.Insert(target, container)) + { + Del(cocoon); + return; + } + + RaiseLocalEvent(ent.Owner, new TerrorSpiderCocoonWrappedEvent(ent.Owner, target, cocoon)); + + args.Handled = true; + } +} diff --git a/Content.Shared/Imperial/TerrorSpider/Systems/SharedTerrorSpiderGuardianShieldBarrierSystem.cs b/Content.Shared/Imperial/TerrorSpider/Systems/SharedTerrorSpiderGuardianShieldBarrierSystem.cs new file mode 100644 index 00000000000..49dc34b6559 --- /dev/null +++ b/Content.Shared/Imperial/TerrorSpider/Systems/SharedTerrorSpiderGuardianShieldBarrierSystem.cs @@ -0,0 +1,23 @@ +using Content.Shared.Imperial.TerrorSpider.Components; +using Robust.Shared.Physics.Events; + +namespace Content.Shared.Imperial.TerrorSpider.Systems; + +public sealed class SharedTerrorSpiderGuardianShieldBarrierSystem : EntitySystem +{ + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnPreventCollide); + } + + private void OnPreventCollide(Entity ent, ref PreventCollideEvent args) + { + if (args.Cancelled || !args.OurFixture.Hard || !args.OtherFixture.Hard) + return; + + if (HasComp(args.OtherEntity)) + args.Cancelled = true; + } +} diff --git a/Content.Shared/Imperial/TerrorSpider/Systems/SharedTerrorSpiderKnightGuardSystem.cs b/Content.Shared/Imperial/TerrorSpider/Systems/SharedTerrorSpiderKnightGuardSystem.cs new file mode 100644 index 00000000000..d31eaa577b3 --- /dev/null +++ b/Content.Shared/Imperial/TerrorSpider/Systems/SharedTerrorSpiderKnightGuardSystem.cs @@ -0,0 +1,148 @@ +using Content.Shared.Actions; +using Content.Shared.Damage; +using Content.Shared.Damage.Components; +using Content.Shared.FixedPoint; +using Content.Shared.Imperial.TerrorSpider.Components; +using Content.Shared.Imperial.TerrorSpider.Events; +using Content.Shared.Movement.Events; +using Content.Shared.Movement.Systems; +using Content.Shared.Weapons.Melee; +using Robust.Shared.Timing; + +namespace Content.Shared.Imperial.TerrorSpider.Systems; + +public abstract class SharedTerrorSpiderKnightGuardSystem : EntitySystem +{ + [Dependency] private readonly SharedActionsSystem _actions = default!; + [Dependency] private readonly MovementSpeedModifierSystem _movement = default!; + [Dependency] private readonly IGameTiming _timing = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnMapInit); + SubscribeLocalEvent(OnShutdown); + SubscribeLocalEvent(OnGuardAction); + SubscribeLocalEvent(OnRefreshMove); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var now = _timing.CurTime; + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var comp)) + { + if (!comp.IsGuarding || now < comp.GuardEndTime) + continue; + + EndGuard(uid, comp); + } + } + + private void OnMapInit(EntityUid uid, TerrorSpiderKnightGuardComponent comp, MapInitEvent args) + { + _actions.AddAction(uid, ref comp.ActionEntity, comp.Action); + } + + private void OnShutdown(EntityUid uid, TerrorSpiderKnightGuardComponent comp, ComponentShutdown args) + { + _actions.RemoveAction(uid, comp.ActionEntity); + } + + private void OnGuardAction(Entity ent, ref TerrorSpiderKnightGuardActionEvent args) + { + if (args.Handled || ent.Comp.IsGuarding) + return; + + if (TryComp(ent.Owner, out var rage) && rage.IsEnraged) + return; + + ent.Comp.IsGuarding = true; + ent.Comp.GuardEndTime = _timing.CurTime + TimeSpan.FromSeconds(ent.Comp.GuardDuration); + + if (TryComp(ent.Owner, out var melee)) + { + ent.Comp.CachedMeleeDamage = new DamageSpecifier(melee.Damage); + SetDamageValue(melee.Damage, "Piercing", ent.Comp.GuardMeleeDamage); + Dirty(ent.Owner, melee); + } + + if (TryComp(ent.Owner, out var passive)) + { + ent.Comp.CachedPassiveDamage = new DamageSpecifier(passive.Damage); + passive.Damage = new DamageSpecifier(); + // 6 HP/s total regen (double the base 3 HP/s) + SetDamageValue(passive.Damage, "Blunt", -1f); + SetDamageValue(passive.Damage, "Slash", -1f); + SetDamageValue(passive.Damage, "Piercing", -1f); + SetDamageValue(passive.Damage, "Heat", -0.5f); + SetDamageValue(passive.Damage, "Shock", -0.5f); + SetDamageValue(passive.Damage, "Cold", -0.5f); + SetDamageValue(passive.Damage, "Caustic", -0.5f); + SetDamageValue(passive.Damage, "Poison", -1f); + Dirty(ent.Owner, passive); + } + + // Apply guard armor modifiers via TerrorSpiderArmorComponent + if (TryComp(ent.Owner, out var armor)) + { + ent.Comp.CachedBruteModifier = armor.BruteModifier; + ent.Comp.CachedBurnModifier = armor.BurnModifier; + armor.BruteModifier = ent.Comp.BruteIncomingMultiplier; + armor.BurnModifier = ent.Comp.BurnIncomingMultiplier; + Dirty(ent.Owner, armor); + } + + _movement.RefreshMovementSpeedModifiers(ent.Owner); + Dirty(ent); + args.Handled = true; + } + + private void EndGuard(EntityUid uid, TerrorSpiderKnightGuardComponent comp) + { + comp.IsGuarding = false; + comp.GuardEndTime = TimeSpan.Zero; + + if (TryComp(uid, out var melee) && comp.CachedMeleeDamage != null) + { + melee.Damage = new DamageSpecifier(comp.CachedMeleeDamage); + Dirty(uid, melee); + } + + if (TryComp(uid, out var passive) && comp.CachedPassiveDamage != null) + { + passive.Damage = new DamageSpecifier(comp.CachedPassiveDamage); + Dirty(uid, passive); + } + + comp.CachedMeleeDamage = null; + comp.CachedPassiveDamage = null; + + // Restore armor modifiers + if (TryComp(uid, out var armor)) + { + armor.BruteModifier = comp.CachedBruteModifier; + armor.BurnModifier = comp.CachedBurnModifier; + Dirty(uid, armor); + } + + _movement.RefreshMovementSpeedModifiers(uid); + Dirty(uid, comp); + } + + private void OnRefreshMove(Entity ent, ref RefreshMovementSpeedModifiersEvent args) + { + if (!ent.Comp.IsGuarding) + return; + + args.ModifySpeed(ent.Comp.GuardSpeedMultiplier, ent.Comp.GuardSpeedMultiplier); + } + + private static void SetDamageValue(DamageSpecifier specifier, string key, float value) + { + specifier.DamageDict[key] = FixedPoint2.New(value); + } +} diff --git a/Content.Shared/Imperial/TerrorSpider/Systems/SharedTerrorSpiderKnightRageSystem.cs b/Content.Shared/Imperial/TerrorSpider/Systems/SharedTerrorSpiderKnightRageSystem.cs new file mode 100644 index 00000000000..ace36ab6ba5 --- /dev/null +++ b/Content.Shared/Imperial/TerrorSpider/Systems/SharedTerrorSpiderKnightRageSystem.cs @@ -0,0 +1,145 @@ +using Content.Shared.Actions; +using Content.Shared.Damage; +using Content.Shared.Damage.Components; +using Content.Shared.FixedPoint; +using Content.Shared.Imperial.TerrorSpider.Components; +using Content.Shared.Imperial.TerrorSpider.Events; +using Content.Shared.Movement.Events; +using Content.Shared.Movement.Systems; +using Content.Shared.Weapons.Melee; +using Robust.Shared.Timing; + +namespace Content.Shared.Imperial.TerrorSpider.Systems; + +public abstract class SharedTerrorSpiderKnightRageSystem : EntitySystem +{ + [Dependency] private readonly SharedActionsSystem _actions = default!; + [Dependency] private readonly MovementSpeedModifierSystem _movement = default!; + [Dependency] private readonly IGameTiming _timing = default!; + + private const string PiercingDamageType = "Piercing"; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnMapInit); + SubscribeLocalEvent(OnShutdown); + SubscribeLocalEvent(OnRageAction); + SubscribeLocalEvent(OnRefreshMove); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var now = _timing.CurTime; + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var comp)) + { + if (!comp.IsEnraged || now < comp.RageEndTime) + continue; + + EndRage(uid, comp); + } + } + + private void OnMapInit(EntityUid uid, TerrorSpiderKnightRageComponent comp, MapInitEvent args) + { + _actions.AddAction(uid, ref comp.ActionEntity, comp.Action); + } + + private void OnShutdown(EntityUid uid, TerrorSpiderKnightRageComponent comp, ComponentShutdown args) + { + if (comp.IsEnraged) + EndRage(uid, comp); + + _actions.RemoveAction(uid, comp.ActionEntity); + } + + private void OnRageAction(Entity ent, ref TerrorSpiderKnightRageActionEvent args) + { + if (args.Handled || ent.Comp.IsEnraged) + return; + + if (TryComp(ent.Owner, out var guard) && guard.IsGuarding) + return; + + ent.Comp.IsEnraged = true; + ent.Comp.RageEndTime = _timing.CurTime + TimeSpan.FromSeconds(ent.Comp.RageDuration); + + if (TryComp(ent.Owner, out var melee)) + { + ent.Comp.CachedMeleeDamage = new DamageSpecifier(melee.Damage); + SetDamageValue(melee.Damage, PiercingDamageType, ent.Comp.EnragedMeleeDamage); + Dirty(ent.Owner, melee); + } + + if (TryComp(ent.Owner, out var passive)) + { + ent.Comp.CachedPassiveDamage = new DamageSpecifier(passive.Damage); + passive.Damage = new DamageSpecifier(); + Dirty(ent.Owner, passive); + } + + if (TryComp(ent.Owner, out var armor)) + { + ent.Comp.CachedBruteModifier = armor.BruteModifier; + ent.Comp.CachedBurnModifier = armor.BurnModifier; + ent.Comp.HasCachedArmor = true; + armor.BruteModifier = ent.Comp.BruteIncomingMultiplier; + armor.BurnModifier = ent.Comp.BurnIncomingMultiplier; + Dirty(ent.Owner, armor); + } + + _movement.RefreshMovementSpeedModifiers(ent.Owner); + Dirty(ent.Owner, ent.Comp); + args.Handled = true; + } + + private void EndRage(EntityUid uid, TerrorSpiderKnightRageComponent comp) + { + comp.IsEnraged = false; + comp.RageEndTime = TimeSpan.Zero; + + if (TryComp(uid, out var melee) && comp.CachedMeleeDamage != null) + { + melee.Damage = new DamageSpecifier(comp.CachedMeleeDamage); + Dirty(uid, melee); + } + + if (TryComp(uid, out var passive) && comp.CachedPassiveDamage != null) + { + passive.Damage = new DamageSpecifier(comp.CachedPassiveDamage); + Dirty(uid, passive); + } + + comp.CachedMeleeDamage = null; + comp.CachedPassiveDamage = null; + + if (comp.HasCachedArmor && TryComp(uid, out var armor)) + { + armor.BruteModifier = comp.CachedBruteModifier; + armor.BurnModifier = comp.CachedBurnModifier; + Dirty(uid, armor); + } + + comp.HasCachedArmor = false; + + _movement.RefreshMovementSpeedModifiers(uid); + Dirty(uid, comp); + } + + private void OnRefreshMove(Entity ent, ref RefreshMovementSpeedModifiersEvent args) + { + if (!ent.Comp.IsEnraged) + return; + + args.ModifySpeed(ent.Comp.EnragedSpeedMultiplier, ent.Comp.EnragedSpeedMultiplier); + } + + private static void SetDamageValue(DamageSpecifier specifier, string key, float value) + { + specifier.DamageDict[key] = FixedPoint2.New(value); + } +} diff --git a/Content.Shared/Imperial/TerrorSpider/Systems/SharedTerrorSpiderWebBuffSystem.cs b/Content.Shared/Imperial/TerrorSpider/Systems/SharedTerrorSpiderWebBuffSystem.cs new file mode 100644 index 00000000000..f534d2905c0 --- /dev/null +++ b/Content.Shared/Imperial/TerrorSpider/Systems/SharedTerrorSpiderWebBuffSystem.cs @@ -0,0 +1,100 @@ +using Content.Shared.Damage; +using Content.Shared.Damage.Systems; +using Content.Shared.Imperial.TerrorSpider.Components; +using Content.Shared.Movement.Events; +using Content.Shared.Movement.Systems; +using Robust.Shared.GameObjects; +using Robust.Shared.Network; +using Robust.Shared.Physics.Events; +using Robust.Shared.Timing; + +namespace Content.Shared.Imperial.TerrorSpider.Systems; + +public abstract class SharedTerrorSpiderWebBuffSystem : EntitySystem +{ + [Dependency] private readonly DamageableSystem _damageable = default!; + [Dependency] private readonly SharedEyeSystem _eye = default!; + [Dependency] private readonly MovementSpeedModifierSystem _movement = default!; + [Dependency] private readonly INetManager _net = default!; + [Dependency] private readonly IGameTiming _timing = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnReceiverMapInit); + SubscribeLocalEvent(OnRefreshMove); + SubscribeLocalEvent(OnStartCollide); + SubscribeLocalEvent(OnEndCollide); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + if (_net.IsClient) + return; + + var now = _timing.CurTime; + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var comp)) + { + if (comp.WebContacts <= 0) + continue; + + if (comp.NextRegenTick == TimeSpan.Zero) + comp.NextRegenTick = now + TimeSpan.FromSeconds(comp.RegenInterval); + + while (now >= comp.NextRegenTick) + { + var regenDamage = new DamageSpecifier(); + regenDamage.DamageDict["Brute"] = -comp.RegenPerTick; + _damageable.TryChangeDamage(uid, regenDamage, ignoreResistances: true, interruptsDoAfters: false); + comp.NextRegenTick += TimeSpan.FromSeconds(comp.RegenInterval); + } + } + } + + private void OnReceiverMapInit(EntityUid uid, TerrorSpiderWebBuffReceiverComponent comp, MapInitEvent args) + { + comp.WebContacts = 0; + comp.NextRegenTick = TimeSpan.Zero; + + // Dark vision for all terror spiders + if (TryComp(uid, out var eye)) + { + _eye.SetDrawLight((uid, eye), false); + } + } + + private void OnRefreshMove(Entity ent, ref RefreshMovementSpeedModifiersEvent args) + { + if (ent.Comp.WebContacts <= 0) + return; + + args.ModifySpeed(ent.Comp.SpeedMultiplier); + } + + private void OnStartCollide(Entity ent, ref StartCollideEvent args) + { + if (!TryComp(args.OtherEntity, out var receiver)) + return; + + receiver.WebContacts++; + _movement.RefreshMovementSpeedModifiers(args.OtherEntity); + Dirty(args.OtherEntity, receiver); + } + + private void OnEndCollide(Entity ent, ref EndCollideEvent args) + { + if (!TryComp(args.OtherEntity, out var receiver)) + return; + + receiver.WebContacts = Math.Max(0, receiver.WebContacts - 1); + if (receiver.WebContacts <= 0) + receiver.NextRegenTick = TimeSpan.Zero; + + _movement.RefreshMovementSpeedModifiers(args.OtherEntity); + Dirty(args.OtherEntity, receiver); + } +} diff --git a/Resources/Audio/Imperial/Seriozha/Fredik21/reason/breath.ogg b/Resources/Audio/Imperial/Seriozha/Fredik21/reason/breath.ogg new file mode 100644 index 00000000000..7b4acc18358 Binary files /dev/null and b/Resources/Audio/Imperial/Seriozha/Fredik21/reason/breath.ogg differ diff --git a/Resources/Audio/Imperial/Seriozha/Fredik21/reason/heart.ogg b/Resources/Audio/Imperial/Seriozha/Fredik21/reason/heart.ogg new file mode 100644 index 00000000000..07fa500407c Binary files /dev/null and b/Resources/Audio/Imperial/Seriozha/Fredik21/reason/heart.ogg differ diff --git a/Resources/Audio/Imperial/Seriozha/SCP/096/attributions.yml b/Resources/Audio/Imperial/Seriozha/SCP/096/attributions.yml new file mode 100644 index 00000000000..a0c2b83dcca --- /dev/null +++ b/Resources/Audio/Imperial/Seriozha/SCP/096/attributions.yml @@ -0,0 +1,12 @@ +- files: ["nda096rage.ogg"] + license: "CC-BY-SA-3.0" + copyright: "Joonas Rikkonen / Undertow Games, Converted from mp3 to .ogg" + source: "https://scpcbgame.com/" +- files: ["nda096cry.ogg"] + license: "CC-BY-SA-3.0" + copyright: "Joonas Rikkonen / Undertow Games, Converted from mp3 to .ogg" + source: "https://scpcbgame.com/" +- files: ["nda096agr.ogg"] + license: "CC-BY-SA-3.0" + copyright: "Joonas Rikkonen / Undertow Games, Converted from mp3 to .ogg" + source: "https://scpcbgame.com/" diff --git a/Resources/Audio/Imperial/Seriozha/SCP/096/nda096agr.ogg b/Resources/Audio/Imperial/Seriozha/SCP/096/nda096agr.ogg new file mode 100644 index 00000000000..f6a065a3830 Binary files /dev/null and b/Resources/Audio/Imperial/Seriozha/SCP/096/nda096agr.ogg differ diff --git a/Resources/Audio/Imperial/Seriozha/SCP/096/nda096cry.ogg b/Resources/Audio/Imperial/Seriozha/SCP/096/nda096cry.ogg new file mode 100644 index 00000000000..a9e51512d17 Binary files /dev/null and b/Resources/Audio/Imperial/Seriozha/SCP/096/nda096cry.ogg differ diff --git a/Resources/Audio/Imperial/Seriozha/SCP/096/nda096rage.ogg b/Resources/Audio/Imperial/Seriozha/SCP/096/nda096rage.ogg new file mode 100644 index 00000000000..5bad966bdb7 Binary files /dev/null and b/Resources/Audio/Imperial/Seriozha/SCP/096/nda096rage.ogg differ diff --git a/Resources/Audio/Imperial/Seriozha/SCP/173/NeckSnap1.ogg b/Resources/Audio/Imperial/Seriozha/SCP/173/NeckSnap1.ogg new file mode 100644 index 00000000000..a75be218c10 Binary files /dev/null and b/Resources/Audio/Imperial/Seriozha/SCP/173/NeckSnap1.ogg differ diff --git a/Resources/Audio/Imperial/Seriozha/SCP/173/NeckSnap2.ogg b/Resources/Audio/Imperial/Seriozha/SCP/173/NeckSnap2.ogg new file mode 100644 index 00000000000..e166d28d00d Binary files /dev/null and b/Resources/Audio/Imperial/Seriozha/SCP/173/NeckSnap2.ogg differ diff --git a/Resources/Audio/Imperial/Seriozha/SCP/173/NeckSnap3.ogg b/Resources/Audio/Imperial/Seriozha/SCP/173/NeckSnap3.ogg new file mode 100644 index 00000000000..629cd0a8985 Binary files /dev/null and b/Resources/Audio/Imperial/Seriozha/SCP/173/NeckSnap3.ogg differ diff --git a/Resources/Audio/Imperial/Seriozha/SCP/173/Rattle1.ogg b/Resources/Audio/Imperial/Seriozha/SCP/173/Rattle1.ogg new file mode 100644 index 00000000000..82948334ce7 Binary files /dev/null and b/Resources/Audio/Imperial/Seriozha/SCP/173/Rattle1.ogg differ diff --git a/Resources/Audio/Imperial/Seriozha/SCP/173/Rattle2.ogg b/Resources/Audio/Imperial/Seriozha/SCP/173/Rattle2.ogg new file mode 100644 index 00000000000..a31e5255351 Binary files /dev/null and b/Resources/Audio/Imperial/Seriozha/SCP/173/Rattle2.ogg differ diff --git a/Resources/Audio/Imperial/Seriozha/SCP/173/Rattle3.ogg b/Resources/Audio/Imperial/Seriozha/SCP/173/Rattle3.ogg new file mode 100644 index 00000000000..ecb8774b342 Binary files /dev/null and b/Resources/Audio/Imperial/Seriozha/SCP/173/Rattle3.ogg differ diff --git a/Resources/Audio/Imperial/Seriozha/SCP/173/attributions.yml b/Resources/Audio/Imperial/Seriozha/SCP/173/attributions.yml new file mode 100644 index 00000000000..899b6c8161b --- /dev/null +++ b/Resources/Audio/Imperial/Seriozha/SCP/173/attributions.yml @@ -0,0 +1,24 @@ +- files: ["NeckSnap1.ogg"] + license: "CC-BY-SA-3.0" + copyright: "Joonas Rikkonen / Undertow Games, Converted from mp3 to .ogg" + source: "https://scpcbgame.com/" +- files: ["NeckSnap2.ogg"] + license: "CC-BY-SA-3.0" + copyright: "Joonas Rikkonen / Undertow Games, Converted from mp3 to .ogg" + source: "https://scpcbgame.com/" +- files: ["NeckSnap3.ogg"] + license: "CC-BY-SA-3.0" + copyright: "Joonas Rikkonen / Undertow Games, Converted from mp3 to .ogg" + source: "https://scpcbgame.com/" +- files: ["Rattle1.ogg"] + license: "CC-BY-SA-3.0" + copyright: "Joonas Rikkonen / Undertow Games, Converted from mp3 to .ogg" + source: "https://scpcbgame.com/" +- files: ["Rattle2.ogg"] + license: "CC-BY-SA-3.0" + copyright: "Joonas Rikkonen / Undertow Games, Converted from mp3 to .ogg" + source: "https://scpcbgame.com/" +- files: ["Rattle3.ogg"] + license: "CC-BY-SA-3.0" + copyright: "Joonas Rikkonen / Undertow Games, Converted from mp3 to .ogg" + source: "https://scpcbgame.com/" diff --git a/Resources/Audio/Imperial/TerrorSpider/scream.ogg b/Resources/Audio/Imperial/TerrorSpider/scream.ogg new file mode 100644 index 00000000000..ccd83386acf Binary files /dev/null and b/Resources/Audio/Imperial/TerrorSpider/scream.ogg differ diff --git a/Resources/Audio/Imperial/TerrorSpider/slam.ogg b/Resources/Audio/Imperial/TerrorSpider/slam.ogg new file mode 100644 index 00000000000..8d901dcfe50 Binary files /dev/null and b/Resources/Audio/Imperial/TerrorSpider/slam.ogg differ diff --git a/Resources/Locale/en-US/Imperial/TerrorSpider/terrorspider.ftl b/Resources/Locale/en-US/Imperial/TerrorSpider/terrorspider.ftl new file mode 100644 index 00000000000..498be72f7f6 --- /dev/null +++ b/Resources/Locale/en-US/Imperial/TerrorSpider/terrorspider.ftl @@ -0,0 +1,167 @@ +spider-terror-name = terror spider +spider-terror-desc = Grow the hive, support the royal bloodline, and eliminate the crew. +spider-terroregg-name = terror spider egg +spider-terroregg-desc = Hatch and continue expanding the nest. +spider-terroregg-rul = Do not harm the hive. Obey the queen and princess. +spider-terror-lay-egg-desc = eggs +spider-terror-hive-channel-name = Hive + +# auto-generated terror spider localization keys +spider-terror-auto-001 = Замедляет всех не-пауков в радиусе 5 и наносит им 20 brute урона. +spider-terror-auto-002 = Замедляет не-пауков, наносит 50 стамины, отключает роботов и бьёт лампы в радиусе 17x17. +spider-terror-auto-003 = Откладывает яйцо Вдовы (II тир). +spider-terror-auto-004 = Откладывает яйцо Дрона (I тир). +spider-terror-auto-005 = Откладывает яйцо Жнеца (II тир). +spider-terror-auto-006 = Откладывает яйцо Лекаря (I тир). +spider-terror-auto-007 = Откладывает яйцо Матери (III тир, лимит 1 в 25 минут). +spider-terror-auto-008 = Откладывает яйцо Принца (III тир, лимит 1 в 25 минут). +spider-terror-auto-009 = Откладывает яйцо Принцессы (III тир, лимит 1 в 25 минут). +spider-terror-auto-010 = Откладывает яйцо Разрушителя (II тир). +spider-terror-auto-011 = Откладывает яйцо Русара (I тир). +spider-terror-auto-012 = Откладывает яйцо Соглядателя (I тир). +spider-terror-auto-013 = Откладывает яйцо Стража (II тир). +spider-terror-auto-014 = Показывает число пауков ужаса и вашего потомства на карте. +spider-terror-auto-015 = Формирует улей, замедляет королеву, усиливает урон по структурам и открывает кладку яиц. +spider-terror-auto-016 = Берсерк должен вести себя агрессивно и регулярно кусать кого-то, чтобы выжить. Абсолютно смертоносен в ближнем бою. +spider-terror-auto-017 = Вернуться к собственному обзору. +spider-terror-auto-018 = Возвращает обычный обзор. +spider-terror-auto-019 = Выводит сведения о вашем потомстве в чат. +spider-terror-auto-020 = Выглядит отвратительно. +spider-terror-auto-021 = Выжидает в засаде и быстро расправляется с целью, действуя в одиночку или в составе гнезда. +spider-terror-auto-022 = Выпускает огненные линии во все стороны, поджигая цели. +spider-terror-auto-023 = Выпускает плевок, создающий дымовую завесу. +spider-terror-auto-024 = Выпускает плевок, создающий ядовитое химическое облако. +spider-terror-auto-025 = Выпускает ЭМИ-импульс в области 7x7 вокруг паука. +spider-terror-auto-026 = Делает соглядателя невидимым на 8 секунд. +spider-terror-auto-027 = Жертва, плотно опутанная паучьим шёлком. +spider-terror-auto-028 = Замедляет не-пауков, наносит 30 стамины и отключает роботов. +spider-terror-auto-029 = Защищает королев и принцесс, жертвуя жизнью при необходимости. Самый преданный слуга и защитник. +spider-terror-auto-030 = Концентрированная токсичность. +spider-terror-auto-031 = Лечит всех пауков ужаса в радиусе на 20 здоровья. +spider-terror-auto-032 = Лечит всех пауков ужаса в радиусе на 30 здоровья. +spider-terror-auto-033 = Ломает стены и двери, саботирует станцию и разносит отделы. Главный враг синтетиков. +spider-terror-auto-034 = Один из самых смертоносных пауков: убивает органику за несколько укусов, имеет опасные плевки, но мало здоровья. Охотится на одиночек или атакует вместе с другими пауками. +spider-terror-auto-035 = Оплетает всё паутиной, отгоняет экипаж плевками и защищает гнездо. +spider-terror-auto-036 = Откладывает желе, которое после разрушения даёт усиление регенерации. +spider-terror-auto-037 = Откладывает королевское яйцо Вдовы (II тир). +spider-terror-auto-038 = Откладывает королевское яйцо Дрона (I тир). +spider-terror-auto-039 = Откладывает королевское яйцо Жнеца (II тир). +spider-terror-auto-040 = Откладывает королевское яйцо Лекаря (I тир). +spider-terror-auto-041 = Откладывает королевское яйцо Разрушителя (II тир). +spider-terror-auto-042 = Откладывает королевское яйцо Русара (I тир). +spider-terror-auto-043 = Откладывает королевское яйцо Соглядателя (I тир). +spider-terror-auto-044 = Откладывает королевское яйцо Стража (II тир). +spider-terror-auto-045 = Откладывает яйца и обеспечивает лечебную поддержку другим паукам, стараясь держаться в стороне. +spider-terror-auto-046 = Откладывает яйцо паука ужаса I тира. +spider-terror-auto-047 = Откладывает яйцо паука ужаса II тира. +spider-terror-auto-048 = Отложить яйцо I тира (Дрон). Требуется сытость 3. +spider-terror-auto-049 = Отложить яйцо I тира (Луркер). Требуется сытость 3. +spider-terror-auto-050 = Отложить яйцо I тира (Русар). Требуется сытость 3. +spider-terror-auto-051 = Отложить яйцо I тира (Целитель). Требуется сытость 3. +spider-terror-auto-052 = паутина ужаса +spider-terror-auto-053 = Переключает наблюдение на предыдущего паука ужаса. +spider-terror-auto-054 = Переключает наблюдение на следующего паука ужаса. +spider-terror-auto-055 = Переключить наблюдение на предыдущего паука. +spider-terror-auto-056 = Переключить наблюдение на следующего паука. +spider-terror-auto-057 = Плевок. +spider-terror-auto-058 = Создаёт временный неразрушимый барьер, проходимый только для пауков ужаса. +spider-terror-auto-059 = Создаёт логово, массово производит пауков и сражается на передовой, используя опасные плевки и крик. +spider-terror-auto-060 = Создаёт логово, откладывает яйца и старается не попадаться на глаза экипажу. +spider-terror-auto-061 = Создаёт паутину. +spider-terror-auto-062 = Создаёт паутину. Немного прочнее обычной. +spider-terror-auto-063 = Сражается с экипажем, защищает других пауков. Благодаря стойкости освобождает пространство для расширения гнезда. +spider-terror-auto-064 = Странное желе от матери улья. +spider-terror-auto-065 = Ужас! +spider-terror-auto-066 = Уничтожает всё живое и неживое, что встаёт на его пути. +spider-terror-auto-067 = Этот гигант лечит пауков своей аурой и ей же опустошает ряды экипажа. Сражается на передовой вместе с другими пауками и поддерживает их. +spider-terror-auto-068 = Этот паук не может плести паутину. +spider-terror-auto-069 = Я ПРИКАЗЫВАЮ ТЕБЕ ЖИТЬ! +spider-terror-auto-070 = Королева: Крик +spider-terror-auto-071 = Королева: Создать улей +spider-terror-auto-072 = Королева: Чувство улья +spider-terror-auto-073 = Королева: Яйцо Вдовы +spider-terror-auto-074 = Королева: Яйцо Дрона +spider-terror-auto-075 = Королева: Яйцо Жнеца +spider-terror-auto-076 = Королева: Яйцо Лекаря +spider-terror-auto-077 = Королева: Яйцо Матери +spider-terror-auto-078 = Королева: Яйцо Принца +spider-terror-auto-079 = Королева: Яйцо Принцессы +spider-terror-auto-080 = Королева: Яйцо Разрушителя +spider-terror-auto-081 = Королева: Яйцо Русара +spider-terror-auto-082 = Королева: Яйцо Соглядателя +spider-terror-auto-083 = Королева: Яйцо Стража +spider-terror-auto-084 = Принц: Топот +spider-terror-auto-085 = Принцесса: Выйти из наблюдения +spider-terror-auto-086 = Принцесса: Крик +spider-terror-auto-087 = Принцесса: Предыдущий паук +spider-terror-auto-088 = Принцесса: Следующий паук +spider-terror-auto-089 = Принцесса: Чувство улья +spider-terror-auto-090 = Принцесса: Яйцо Вдовы +spider-terror-auto-091 = Принцесса: Яйцо Дрона +spider-terror-auto-092 = Принцесса: Яйцо Жнеца +spider-terror-auto-093 = Принцесса: Яйцо Лекаря +spider-terror-auto-094 = Принцесса: Яйцо Разрушителя +spider-terror-auto-095 = Принцесса: Яйцо Русара +spider-terror-auto-096 = Принцесса: Яйцо Соглядателя +spider-terror-auto-097 = Принцесса: Яйцо Стража +spider-terror-auto-098 = без паутины +spider-terror-auto-099 = Вдова ужаса +spider-terror-auto-100 = Выйти +spider-terror-auto-101 = газовый плевок ксено +spider-terror-auto-102 = Дрон ужаса +spider-terror-auto-103 = Дымный плевок вдовы +spider-terror-auto-104 = желе улья +spider-terror-auto-105 = Жнец ужаса +spider-terror-auto-106 = замедление королевы паука ужаса в улье +spider-terror-auto-107 = замедление от топота паука ужаса +spider-terror-auto-108 = защитный барьер стража +spider-terror-auto-109 = Кислотный паук ужаса +spider-terror-auto-110 = Кислотный ЭМИ-паук ужаса +spider-terror-auto-111 = кокон +spider-terror-auto-112 = Королева ужаса +spider-terror-auto-113 = королевское яйцо паука ужаса +spider-terror-auto-114 = Ксено +spider-terror-auto-115 = Лекарь ужаса +spider-terror-auto-116 = лечащий сгусток +spider-terror-auto-117 = Лечебный пульс матери +spider-terror-auto-118 = Мать ужаса +spider-terror-auto-119 = Мать: Отложить желе +spider-terror-auto-120 = Морозное масло +spider-terror-auto-121 = обездвиживание матери паука ужаса при наблюдении +spider-terror-auto-122 = обездвиживание принцессы паука ужаса при наблюдении +spider-terror-auto-123 = Огненный выброс разрушителя +spider-terror-auto-124 = Отложить яйцо I тира +spider-terror-auto-125 = Отложить яйцо II тира +spider-terror-auto-126 = Паутина +spider-terror-auto-127 = паутина паука ужаса +spider-terror-auto-128 = Паутина принцессы +spider-terror-auto-129 = Паутина целителя +spider-terror-auto-130 = плевок вдовы +spider-terror-auto-131 = плевок ксено +spider-terror-auto-132 = плевок ужаса +spider-terror-auto-133 = Предыдущий +spider-terror-auto-134 = Принц ужаса +spider-terror-auto-135 = Принцесса ужаса +spider-terror-auto-136 = Пульс лекаря +spider-terror-auto-137 = Разрушитель ужаса +spider-terror-auto-138 = Русар ужаса +spider-terror-auto-139 = Скрытность соглядателя +spider-terror-auto-140 = Следующий +spider-terror-auto-141 = Соглядатай ужаса +spider-terror-auto-142 = Соткать паутину +spider-terror-auto-143 = Способности вдовы +spider-terror-auto-144 = Страж ужаса +spider-terror-auto-145 = Улей +spider-terror-auto-146 = Чёрный яд ужаса +spider-terror-auto-147 = Щит стража +spider-terror-auto-148 = ЭМИ-крик разрушителя +spider-terror-auto-149 = ЭМИ-паук ужаса +spider-terror-auto-150 = Ядовитый плевок вдовы +spider-terror-auto-151 = Яйца II тира +spider-terror-auto-152 = Яйцо I тира +spider-terror-auto-153 = Яйцо II тира +spider-terror-auto-154 = яйцо паука ужаса +spider-terror-auto-155 = Яйцо: Дрон +spider-terror-auto-156 = Яйцо: Луркер +spider-terror-auto-157 = Яйцо: Русар +spider-terror-auto-158 = Яйцо: Целитель diff --git a/Resources/Locale/ru-RU/Imperial/Seriozha/SCP/locale.ftl b/Resources/Locale/ru-RU/Imperial/Seriozha/SCP/locale.ftl index ee61879cf44..ec765b6ada5 100644 --- a/Resources/Locale/ru-RU/Imperial/Seriozha/SCP/locale.ftl +++ b/Resources/Locale/ru-RU/Imperial/Seriozha/SCP/locale.ftl @@ -192,3 +192,28 @@ ent-RubberStampComitetNedra = Печать комитета управления .desc = Печать, повышает уровень важности документа ent-ChemistryBottleSaline = бутылочка физраствора .desc = { ent-BaseChemistryEmptyBottle.desc } + +# SCP008 +scp008-warning-popup = SCP-008: покиньте зону немедленно, иначе вы превратитесь в зомби! + +# SCP096 +scp096-rage-windup-popup = SCP-096 начинает входить в ярость! +scp096-rage-popup = SCP-096 входит в ярость! +scp096-rage-calm-popup = SCP-096 успокаивается. + +# Sanity +sanity-low-warning = Ваш рассудок на исходе. +sanity-high-feeling = Вы чувствуете душевный подъём. + +# Alerts +alerts-sanity-name = Рассудок +alerts-sanity-desc = Текущее состояние психики. +alerts-scp-blink-name = Моргание +alerts-scp-blink-desc = До следующего моргания. +alerts-scp-fireman-points-name = Огненные очки +alerts-scp-fireman-points-desc = Текущий запас огненной энергии. + +# Terror Spider +terror-spider-hive-sense-empty = Чувство улья: потомства не обнаружено. +terror-spider-hive-sense-entry = {$name}: {$hp}/{$hpMax} HP, маяк: {$beacon} +terror-spider-hive-sense-unknown = неизвестно \ No newline at end of file diff --git a/Resources/Locale/ru-RU/Imperial/Terrorspider/spider.ftl b/Resources/Locale/ru-RU/Imperial/Terrorspider/spider.ftl index 8c30386776b..5044b2a295f 100644 --- a/Resources/Locale/ru-RU/Imperial/Terrorspider/spider.ftl +++ b/Resources/Locale/ru-RU/Imperial/Terrorspider/spider.ftl @@ -1,141 +1,584 @@ -spider-terror-name = Паук ужаса +# ========================================== +# Terror Spider Localization Russian +# ========================================== + +# Ghost role +spider-terror-name = Паук ужаса spider-terror-desc = Паук ужаса. Защищайте королеву или принцессу, создайте логово и уничтожайте станцию! +# Game rule spider-terror-gamerule = Пауки ужаса -spider-terror-gamerule-desc = На станции появились пауки ужаса. Эти мерзкие и кровожадные пауки хотят захватить станцию превратив ее в свой улей. +spider-terror-gamerule-desc = На станции появились пауки ужаса. Эти мерзкие и кровожадные пауки хотят захватить станцию, превратив её в свой улей. +# Egg ghost role spider-terroregg-name = Яйцо паука ужаса -spider-terroregg-desc = Готово к вылупрелнию нового защитника улья! -spider-terroregg-rul = Превратитеть в полноценного защитника улья. +spider-terroregg-desc = Готово к вылуплению нового защитника улья! +spider-terroregg-rul = Превратитесь в полноценного защитника улья. + +# Knight actions +spider-terror-knight-rage-action-name = ярость +spider-terror-knight-rage-action-desc = На 10 секунд увеличивает скорость и урон рыцаря ужаса, но делает его уязвимее и отключает регенерацию. + +spider-terror-knight-guard-action-name = защита +spider-terror-knight-guard-action-desc = На 10 секунд замедляет рыцаря ужаса, снижает урон, усиливает регенерацию и повышает устойчивость к урону. + +# Cocoon action +spider-terror-cocoon-action-name = кокон +spider-terror-cocoon-action-desc = Оплетает цель коконом. + +# ========================================== +# Mobs Base +# ========================================== ent-MobRusarSpider = рыцарь ужаса - .desc = Сражается с экипажем, защищает других пауков. За счёт своей устойчивости Создаёт свободное пространство, чтобы другие пауки могли расширять гнездо. + .desc = Сражается с экипажем, защищает других пауков. Благодаря своей стойкости создаёт пространство для расширения гнезда. -ent-MobRusarSpiderAngrys = рыцарь ужаса - .desc = Сражается с экипажем, защищает других пауков. За счёт своей устойчивости Создаёт свободное пространство, чтобы другие пауки могли расширять гнездо. - .suffix = пауки ужаса +ent-MobDronSpider = дрон ужаса + .desc = Оплетает всё вокруг паутиной, отгоняет плевками экипаж, защищает гнездо. -ent-MobDronSpider = дрон Ужаса - .desc = Оплетает всё вокруг паутиной, отгоняет плевками экипаж, защищает гнездо. +ent-MobsogladatelSpider = соглядатай ужаса + .desc = Выжидает в засаде и быстро расправляется с целью, действуя в одиночку или вместе с гнездом. -ent-MobDronSpiderAngrys = дрон Ужаса - .desc = Оплетает всё вокруг паутиной, отгоняет плевками экипаж, защищает гнездо. - .suffix = пауки ужаса +ent-MobreaperSpider = жнец ужаса + .desc = Берсерк, обязан вести себя агрессивно и регулярно кусать кого-нибудь, чтобы выживать. Абсолютно смертоносен в ближнем бою. -ent-MobreaperSpider = жнец Ужаса - .desc = Берсерк, обязан вести себя агрессивно и регулярно кусать кого-нибудь чтобы выживать. Абсолютно смертоносен в ближнем бою. +ent-MobDestroyerSpider = разрушитель ужаса + .desc = Взламывает стены и двери, саботирует работу станции, уничтожает отделы. Главный враг синтетиков. -ent-MobGiantreaperSpiderAngry = жнец Ужаса - .desc = Берсерк, обязан вести себя агрессивно и регулярно кусать кого-нибудь чтобы выживать. Абсолютно смертоносен в ближнем бою. - .suffix = пауки ужаса +ent-MobWidowSpider = вдова ужаса + .desc = Один из самых смертоносных пауков, убивает любую органическую цель за несколько укусов, обладает опасными плевками, но имеет мало здоровья. -ent-MobDestroyerSpider = разрушитель Ужаса - .desc = Взламывает стены и двери, саботирует работу станции, уничтожает отделы. Главный враг синтетиков. +ent-MobGuardianSpider = страж ужаса + .desc = Охраняет королев и принцесс, жертвуя своей жизнью в случае необходимости. Самый верный слуга и защитник. -ent-MobGiantDestroyerSpiderAngry = разрушитель Ужаса - .desc = Взламывает стены и двери, саботирует работу станции, уничтожает отделы. Главный враг синтетиков. - .suffix = пауки ужаса +ent-MobMotherSpider = мать ужаса + .desc = Массово исцеляет пауков своей аурой и истребляет ею экипаж. Сражается вместе с другими пауками на передовой. -ent-MobWidowSpider = вдова Ужаса - .desc = Один из самых смертоносных пауков, убивает любую органическую цель за несколько укусов, обладает опасными плевками, но имеет мало здоровья. Охотится на одиночек, или атакует вместе с другими пауками. +ent-MobPrinceSpider = принц ужаса + .desc = Уничтожает всё живое и неживое, что окажется у него на пути. -ent-MobGiantWidowSpiderAngry = вдова Ужаса - .desc = Один из самых смертоносных пауков, убивает любую органическую цель за несколько укусов, обладает опасными плевками, но имеет мало здоровья. Охотится на одиночек, или атакует вместе с другими пауками. - .suffix = пауки ужаса +ent-MobPrincessSpider = принцесса ужаса + .desc = Создаёт логово, откладывает яйца, старается не попадаться экипажу на глаза. -ent-MobGuardianSpider = страж Ужаса - .desc = Охраняет королев/принцесс, безопасность, жертвуя своей жизнью в случае необходимости. Самый верный слуга и защитник. +ent-MobQueenSpider = королева ужаса + .desc = Создаёт логово, массово производит пауков, сражается на передовой, используя свои опасные плевки и крик. -ent-MobGiantGuardianSpiderAngry = страж Ужаса - .desc = Охраняет королев/принцесс, безопасность, жертвуя своей жизнью в случае необходимости. Самый верный слуга и защитник. - .suffix = пауки ужаса +ent-MobHealerSpider = целитель ужаса + .desc = Откладывает яйца и оказывает лечебную поддержку другим паукам, стараясь держаться в стороне. -ent-MobMotherSpider = мать Ужаса - .desc = Массовый исцеляет пауков своей аурой, и также массово истребляет ею экипаж. Сражается вместе с другими паукам на передовой, оказывая им поддержку. +# ========================================== +# Mobs Angry (AI / ghost-role variants) +# ========================================== -ent-MobGiantMotherSpiderAngry = мать Ужаса - .desc = Массовый исцеляет пауков своей аурой, и также массово истребляет ею экипаж. Сражается вместе с другими паукам на передовой, оказывая им поддержку. - .suffix = пауки ужаса +ent-MobRusarSpiderAngrys = рыцарь ужаса + .desc = Сражается с экипажем, защищает других пауков. Благодаря своей стойкости создаёт пространство для расширения гнезда. -ent-MobPrinceSpider = принц Ужаса - .desc = Уничтожает всё живое и неживое, что окажется у него на пути. +ent-MobDronSpiderAngrys = дрон ужаса + .desc = Оплетает всё вокруг паутиной, отгоняет плевками экипаж, защищает гнездо. -ent-MobGiantPrinceSpiderAngry = принц Ужаса - .desc = Уничтожает всё живое и неживое, что окажется у него на пути. - .suffix = пауки ужаса +ent-MobsogladatelSpiderAngrys = соглядатай ужаса + .desc = Выжидает в засаде и быстро расправляется с целью, действуя в одиночку или вместе с гнездом. -ent-MobPrincessSpider = принцесса Ужаса - .desc = Создает логово, откладывает яйца, старается не попадаться экипажу на глаза. +ent-MobGiantreaperSpiderAngry = жнец ужаса + .desc = Берсерк, обязан вести себя агрессивно и регулярно кусать кого-нибудь, чтобы выживать. Абсолютно смертоносен в ближнем бою. -ent-MobGiantPrincessSpiderAngry = принцесса Ужаса - .desc = Создает логово, откладывает яйца, старается не попадаться экипажу на глаза. - .suffix = пауки ужаса +ent-MobGiantDestroyerSpiderAngry = разрушитель ужаса + .desc = Взламывает стены и двери, саботирует работу станции, уничтожает отделы. Главный враг синтетиков. -ent-MobQueenSpider = королева Ужаса - .desc = Создает логово, массово производит пауков, сражается с пауками на передовой, используя свои опасные плевки и крик. +ent-MobGiantWidowSpiderAngry = вдова ужаса + .desc = Один из самых смертоносных пауков, убивает любую органическую цель за несколько укусов, обладает опасными плевками, но имеет мало здоровья. -ent-MobGiantQueenSpiderAngry = королева Ужаса - .desc = Создает логово, массово производит пауков, сражается с пауками на передовой, используя свои опасные плевки и крик. - .suffix = пауки ужаса +ent-MobGiantGuardianSpiderAngry = страж ужаса + .desc = Охраняет королев и принцесс, жертвуя своей жизнью в случае необходимости. Самый верный слуга и защитник. -ent-MobHealerSpider = целитель Ужаса - .desc = оказывает поддержку лечением другим паукам, старается держаться в стороне. +ent-MobGiantMotherSpiderAngry = мать ужаса + .desc = Массово исцеляет пауков своей аурой и истребляет ею экипаж. Сражается вместе с другими пауками на передовой. -ent-MobGiantHealerSpiderAngry = целитель Ужаса - .desc = оказывает поддержку лечением другим паукам, старается держаться в стороне. - .suffix = пауки ужаса +ent-MobGiantPrinceSpiderAngry = принц ужаса + .desc = Уничтожает всё живое и неживое, что окажется у него на пути. -ent-SpiderWebTerrorRusar = паутина паука ужаса - .desc = УЖАСНО! - .suffix = пауки ужаса +ent-MobGiantPrincessSpiderAngry = принцесса ужаса + .desc = Создаёт логово, откладывает яйца, старается не попадаться экипажу на глаза. -ent-SpiderWebTerrorDron = паутина паука ужаса - .desc = УЖАСНО! - .suffix = пауки ужаса +ent-MobGiantQueenSpiderAngry = королева ужаса + .desc = Создаёт логово, массово производит пауков, сражается на передовой, используя свои опасные плевки и крик. -ent-WeaponTerrorSpit = плевок ужаса - .desc = ПлЭвок - .suffix = пауки ужаса +ent-MobGiantHealerSpiderAngry = целитель ужаса + .desc = Откладывает яйца и оказывает лечебную поддержку другим паукам, стараясь держаться в стороне. -ent-TerrorSpitBullet = плевок ужаса - .desc = ПлЭвок - .suffix = пауки ужаса +# ========================================== +# Webs +# ========================================== + +ent-SpiderWebTerrorRusar = паутина паука-рыцаря + .desc = Липкая паутина, сотканная рыцарем ужаса. + +ent-SpiderWebTerrorDron = паутина паука-дрона + .desc = Липкая паутина, сотканная дроном ужаса. + +ent-SpiderWebTerrorLurker = паутина соглядатая + .desc = Липкая паутина, сотканная соглядатаем ужаса. + +ent-SpiderWebTerrorWidow = паутина вдовы + .desc = Ядовитая паутина паука-вдовы. + +ent-SpiderWebTerrorGuardian = паутина стража + .desc = Прочная паутина, сотканная стражем ужаса. + +ent-SpiderWebTerrorGuardianBarrier = защитный барьер стража + .desc = Ужасно! + +ent-SpiderWebTerrorHealer = паутина целителя + .desc = Лечебная паутина, сотканная целителем ужаса. + +ent-SpiderWebPrincess = паутина принцессы + .desc = Паутина, сотканная принцессой ужаса. + +ent-SpiderWebTerrorUwU = паутина паука UwU + .desc = Мягкая, но крайне опасная паутина. + +# ========================================== +# Eggs +# ========================================== + +ent-TerrorEgg1Tir = яйцо паука ужаса + .desc = На вид мерзкое. ent-TerrorEgg2Tir = яйцо паука ужаса .desc = На вид мерзкое. - .suffix = пауки ужаса ent-TerrorEgg3Tir = яйцо паука ужаса .desc = На вид мерзкое. - .suffix = пауки ужаса -ent-SpiderWebTerrorGuardian = паутина паука ужаса - .desc = УЖАСНО! - .suffix = пауки ужаса +ent-TerrorEggPrincessTier1 = яйцо паука ужаса + .desc = На вид мерзкое. + +ent-TerrorEggPrincessTier2 = яйцо паука ужаса + .desc = На вид мерзкое. + +ent-TerrorEggHealerRusar = яйцо паука ужаса + .desc = На вид мерзкое. + +ent-TerrorEggHealerDron = яйцо паука ужаса + .desc = На вид мерзкое. + +ent-TerrorEggHealerLurker = яйцо паука ужаса + .desc = На вид мерзкое. + +ent-TerrorEggHealerHealer = яйцо паука ужаса + .desc = На вид мерзкое. + +ent-TerrorEggQueenRusar = яйцо паука ужаса + .desc = На вид мерзкое. -ent-SpiderWebPrincess = паутина паука ужаса - .desc = УЖАСНО! - .suffix = пауки ужаса +ent-TerrorEggQueenDron = яйцо паука ужаса + .desc = На вид мерзкое. + +ent-TerrorEggQueenLurker = яйцо паука ужаса + .desc = На вид мерзкое. + +ent-TerrorEggQueenHealer = яйцо паука ужаса + .desc = На вид мерзкое. + +ent-TerrorEggQueenReaper = яйцо паука ужаса + .desc = На вид мерзкое. + +ent-TerrorEggQueenWidow = яйцо паука ужаса + .desc = На вид мерзкое. + +ent-TerrorEggQueenGuardian = яйцо паука ужаса + .desc = На вид мерзкое. + +ent-TerrorEggQueenDestroyer = яйцо паука ужаса + .desc = На вид мерзкое. + +ent-TerrorEggQueenPrince = яйцо паука ужаса + .desc = На вид мерзкое. + +ent-TerrorEggQueenPrincess = яйцо паука ужаса + .desc = На вид мерзкое. + +ent-TerrorEggQueenMother = яйцо паука ужаса + .desc = На вид мерзкое. + +ent-TerrorEggPrincessRusar = яйцо паука ужаса + .desc = На вид мерзкое. + +ent-TerrorEggPrincessDron = яйцо паука ужаса + .desc = На вид мерзкое. + +ent-TerrorEggPrincessLurker = яйцо паука ужаса + .desc = На вид мерзкое. + +ent-TerrorEggPrincessHealer = яйцо паука ужаса + .desc = На вид мерзкое. + +ent-TerrorEggPrincessReaper = яйцо паука ужаса + .desc = На вид мерзкое. + +ent-TerrorEggPrincessWidow = яйцо паука ужаса + .desc = На вид мерзкое. + +ent-TerrorEggPrincessGuardian = яйцо паука ужаса + .desc = На вид мерзкое. + +ent-TerrorEggPrincessDestroyer = яйцо паука ужаса + .desc = На вид мерзкое. + +# ========================================== +# Cocoon & Jelly +# ========================================== + +ent-TerrorSpiderCocoon = кокон + .desc = Жертва, плотно опутанная паучьим шёлком. + +ent-TerrorSpiderMotherJelly = желе улья + .desc = Странное желе от матери улья. + +# ========================================== +# Projectiles & Weapons +# ========================================== + +ent-WeaponTerrorSpit = плевок ужаса + .desc = Ядовитый плевок паука ужаса. + +ent-TerrorSpitBullet = плевок ужаса + .desc = Ядовитый плевок паука ужаса. + +ent-TerrorDrontAcid = кислотный плевок ужаса + .desc = Разъедающая кислота паука-дрона. + +ent-TerrorRavAcid = ЭМИ-кислотный плевок ужаса + .desc = Кислотный плевок с ЭМИ-зарядом. + +ent-BulletGrenadeEMPToxin = ЭМИ-плевок ужаса + .desc = Плевок с мощным электромагнитным импульсом. + +ent-TerrorWidowSpit = плевок вдовы + .desc = Ядовитый плевок паука-вдовы. + +ent-TerrorWidowSmokeSpitBullet = дымный плевок вдовы + .desc = Плевок паука-вдовы, создающий облако дыма. + +ent-TerrorWidowPoisonSpitBullet = ядовитый плевок вдовы + .desc = Концентрированный яд паука-вдовы. + +ent-TerrorPrincessSpit = плевок принцессы + .desc = Плевок паука-принцессы. + +ent-TerrorQueentAcid = кислотный плевок королевы + .desc = Мощная кислота королевы пауков ужаса. + +ent-ProjectileHealingBoltTerror = лечащий сгусток + .desc = Я ПРИКАЗЫВАЮ ТЕБЕ ЖИТЬ! ent-WeaponXenoSpit = плевок ужаса .desc = Концентрированная токсичность - .suffix = пауки ужаса ent-GasXenoSpitBullet = газовый плевок ксено .desc = Концентрированная токсичность - .suffix = пауки ужаса -ent-TerrorEgg1Tir = яйцо паука ужаса - .desc = На вид мерзкое. - .suffix = пауки ужаса +# ========================================== +# Implants +# ========================================== + +ent-LightImplantSpiderRusar = создание паутины + .desc = Создаёт паутину. + +ent-LightImplantSpiderDron = создание паутины + .desc = Создаёт паутину. + +ent-LightImplantSpiderGuardian = создание паутины + .desc = Создаёт паутину. + +ent-LightImplantSpiderPrincess = создание паутины + .desc = Создаёт паутину. + +ent-LightImplantSpiderWidowSmoke = дымный плевок + .desc = Дымный плевок. + +ent-LightImplantSpiderWidowPoison = ядовитый плевок + .desc = Ядовитый плевок. + +ent-LightImplantSpiderTir1 = кладка яиц I тир + .desc = Откладывает яйцо I тира. + +ent-LightImplantSpiderTir2 = кладка яиц II тир + .desc = Откладывает яйцо II тира. + +ent-LightImplantSpiderTir1Queen = кладка яиц I тир + .desc = Откладывает королевское яйцо I тира. + +ent-LightImplantSpiderTir2Queen = кладка яиц II тир + .desc = Откладывает королевское яйцо II тира. + +ent-LightImplantSpiderTir3 = кладка яиц III тир + .desc = Откладывает яйцо III тира. + +# ========================================== +# Spawn Web / Egg Actions +# ========================================== + +ent-ActionSpawnSpiderRusar = создание паутины + .desc = Создаёт паутину. Немного прочнее обычной. + +ent-ActionSpawnSpiderDron = создание паутины + .desc = Создаёт паутину. Немного прочнее обычной. + +ent-ActionSpawnSpiderGuardian = создание паутины + .desc = Создаёт паутину. Немного прочнее обычной. + +ent-ActionSpawnSpiderPrincess = создание паутины + .desc = Создаёт паутину. Немного прочнее обычной. + +ent-ActionTerrorSpiderCocoon = кокон + .desc = Оплетает цель коконом. + +ent-ActionSpawnTerrorEgg1Tir = кладка яйца I тир + .desc = Откладывает яйцо I тира. + +ent-ActionSpawnTerrorEgg2Tir = кладка яйца II тир + .desc = Откладывает яйцо II тира. + +ent-ActionSpawnTerrorEgg1TirQueen = кладка яйца I тир + .desc = Откладывает королевское яйцо I тира. + +ent-ActionSpawnTerrorEgg2TirQueen = кладка яйца II тир + .desc = Откладывает королевское яйцо II тира. + +ent-ActionSpawnTerrorEgg3Tir = кладка яйца III тир + .desc = Откладывает яйцо III тира. + +ent-TerrorLayEgg = отложить яйцо + .desc = Откладывает яйцо паука ужаса. + +# ========================================== +# Web Actions (Spider component) +# ========================================== + +ent-TerrorWeedsAction = паутина + .desc = Создаёт паутину. + +ent-TerrorWeedsQueenAction = паутина королевы + .desc = Создаёт паутину. + +ent-TerrorWeedsGuardianAction = паутина стража + .desc = Создаёт паутину. + +ent-TerrorWeedsHealerAction = паутина целителя + .desc = Создаёт паутину. + +ent-TerrorWeedsWidowAction = паутина вдовы + .desc = Создаёт паутину. + +ent-TerrorWeedsLurkerAction = паутина соглядатая + .desc = Создаёт паутину. + +ent-TerrorWeedsPrincessAction = паутина принцессы + .desc = Создаёт паутину. + +# ========================================== +# Widow combat actions +# ========================================== + +ent-ActionTerrorSpiderWidowSmokeSpit = дымный плевок + .desc = Выпускает плевок, создающий дымовую завесу. + +ent-ActionTerrorSpiderWidowPoisonSpit = ядовитый плевок + .desc = Выпускает плевок, создающий ядовитое облако. + +# ========================================== +# Mother actions +# ========================================== + +ent-ActionTerrorSpiderMotherRemoteViewNext = следующий паук + .desc = Переключает наблюдение на следующего паука ужаса. + +ent-ActionTerrorSpiderMotherRemoteViewPrevious = предыдущий паук + .desc = Переключает наблюдение на предыдущего паука ужаса. + +ent-ActionTerrorSpiderMotherRemoteViewExit = выйти из наблюдения + .desc = Возвращает ваш обзор. + +ent-ActionTerrorSpiderMotherPulse = лечебный пульс матери + .desc = Лечит всех пауков ужаса в радиусе. + +ent-ActionTerrorSpiderMotherLayJelly = отложить желе + .desc = Откладывает желе, которое после разрушения даёт усиление регенерации. + +# ========================================== +# Queen egg actions +# ========================================== + +ent-ActionTerrorSpiderQueenCreateHive = создать улей + .desc = Формирует улей, замедляет королеву, усиливает урон по структурам и открывает кладку яиц. + +ent-ActionTerrorSpiderQueenScream = крик королевы + .desc = Замедляет врагов и отключает роботов. + +ent-ActionTerrorSpiderQueenHiveCount = чувство улья + .desc = Показывает число пауков ужаса и вашего потомства. + +ent-ActionTerrorSpiderQueenLayEggRusar = яйцо: рыцарь + .desc = Откладывает королевское яйцо рыцаря (I тир). + +ent-ActionTerrorSpiderQueenLayEggDron = яйцо: дрон + .desc = Откладывает королевское яйцо дрона (I тир). + +ent-ActionTerrorSpiderQueenLayEggLurker = яйцо: соглядатай + .desc = Откладывает королевское яйцо соглядатая (I тир). + +ent-ActionTerrorSpiderQueenLayEggHealer = яйцо: целитель + .desc = Откладывает королевское яйцо целителя (I тир). + +ent-ActionTerrorSpiderQueenLayEggReaper = яйцо: жнец + .desc = Откладывает королевское яйцо жнеца (II тир). + +ent-ActionTerrorSpiderQueenLayEggWidow = яйцо: вдова + .desc = Откладывает королевское яйцо вдовы (II тир). + +ent-ActionTerrorSpiderQueenLayEggGuardian = яйцо: страж + .desc = Откладывает королевское яйцо стража (II тир). + +ent-ActionTerrorSpiderQueenLayEggDestroyer = яйцо: разрушитель + .desc = Откладывает королевское яйцо разрушителя (II тир). + +ent-ActionTerrorSpiderQueenLayEggPrince = яйцо: принц + .desc = Откладывает королевское яйцо принца (III тир). + +ent-ActionTerrorSpiderQueenLayEggPrincess = яйцо: принцесса + .desc = Откладывает королевское яйцо принцессы (III тир). + +ent-ActionTerrorSpiderQueenLayEggMother = яйцо: мать + .desc = Откладывает королевское яйцо матери (III тир). + +# ========================================== +# Knight actions +# ========================================== + +ent-ActionTerrorSpiderKnightGuard = защита + .desc = Оборонительная стойка: замедление, снижение урона, усиление регенерации. + +ent-ActionTerrorSpiderKnightRage = ярость + .desc = Яростная стойка: ускорение, увеличение урона, отключение регенерации. + +# ========================================== +# Destroyer actions +# ========================================== + +ent-ActionTerrorSpiderDestroyerEmpScream = ЭМИ-крик + .desc = Выпускает ЭМИ-импульс вокруг себя. + +ent-ActionTerrorSpiderDestroyerFireBurst = огненный выброс + .desc = Поджигает всех вокруг. + +# ========================================== +# Princess actions +# ========================================== + +ent-ActionTerrorSpiderPrincessRemoteViewNext = следующий паук + .desc = Переключает наблюдение на следующего паука потомства. + +ent-ActionTerrorSpiderPrincessRemoteViewPrevious = предыдущий паук + .desc = Переключает наблюдение на предыдущего паука потомства. + +ent-ActionTerrorSpiderPrincessRemoteViewExit = выйти из наблюдения + .desc = Возвращает ваш обзор. + +ent-ActionTerrorSpiderPrincessHiveSense = чувство улья + .desc = Выводит сведения о вашем потомстве в чат. + +ent-ActionTerrorSpiderPrincessScream = крик принцессы + .desc = Замедляет врагов и отключает роботов. + +ent-ActionTerrorSpiderPrincessLayEggRusar = яйцо: рыцарь + .desc = Откладывает яйцо рыцаря (I тир). + +ent-ActionTerrorSpiderPrincessLayEggDron = яйцо: дрон + .desc = Откладывает яйцо дрона (I тир). + +ent-ActionTerrorSpiderPrincessLayEggLurker = яйцо: соглядатай + .desc = Откладывает яйцо соглядатая (I тир). + +ent-ActionTerrorSpiderPrincessLayEggHealer = яйцо: целитель + .desc = Откладывает яйцо целителя (I тир). + +ent-ActionTerrorSpiderPrincessLayEggReaper = яйцо: жнец + .desc = Откладывает яйцо жнеца (II тир). + +ent-ActionTerrorSpiderPrincessLayEggWidow = яйцо: вдова + .desc = Откладывает яйцо вдовы (II тир). + +ent-ActionTerrorSpiderPrincessLayEggGuardian = яйцо: страж + .desc = Откладывает яйцо стража (II тир). + +ent-ActionTerrorSpiderPrincessLayEggDestroyer = яйцо: разрушитель + .desc = Откладывает яйцо разрушителя (II тир). + +# ========================================== +# Royal stomp +# ========================================== + +ent-ActionTerrorSpiderRoyalStomp = топот + .desc = Замедляет и давит ближайших врагов. + +# ========================================== +# Healer actions +# ========================================== + +ent-ActionTerrorSpiderHealerPulse = пульс целителя + .desc = Лечит всех пауков ужаса в радиусе. + +ent-ActionTerrorSpiderHealerLayEggRusar = яйцо: рыцарь + .desc = Откладывает яйцо рыцаря (I тир). + +ent-ActionTerrorSpiderHealerLayEggDron = яйцо: дрон + .desc = Откладывает яйцо дрона (I тир). + +ent-ActionTerrorSpiderHealerLayEggLurker = яйцо: соглядатай + .desc = Откладывает яйцо соглядатая (I тир). + +ent-ActionTerrorSpiderHealerLayEggHealer = яйцо: целитель + .desc = Откладывает яйцо целителя (I тир). + +# ========================================== +# Lurker stealth +# ========================================== + +ent-ActionTerrorSpiderLurkerStealth = невидимость + .desc = Делает соглядатая невидимым на короткое время. + +# ========================================== +# Reagents +# ========================================== + +ent-FrostOilTerrorSpider = морозное масло + .desc = Охлаждает тело жертвы. + +ent-BlackTerrorVenom = чёрный яд ужаса + .desc = Смертоносный яд вдовы ужаса. + +# ========================================== +# Status effects +# ========================================== + +ent-TerrorSpiderRoyalStompSlowStatusEffect = замедление от топота паука ужаса + +ent-TerrorSpiderPrincessRemoteViewImmobileStatusEffect = обездвиживание принцессы при наблюдении + +ent-TerrorSpiderMotherRemoteViewImmobileStatusEffect = обездвиживание матери при наблюдении + +ent-TerrorSpiderQueenHiveSlowStatusEffect = замедление королевы в улье -ent-SpiderWebTerrorUwU = паутина паука ужаса - .desc = УЖАСНО! - .suffix = пауки ужаса +# ========================================== +# Radio channel +# ========================================== -ent-LightImplantSpiderRusar = спавн паутины +ent-hiveconnection = Улей -ent-ActionSpawnSpiderRusar = спавн паутины - .desc = спанит паутину. Немного крепче обычной. +# ========================================== +# Game preset +# ========================================== -ent-TerrorRavAcid = плевок ужаса ЭМИ +ent-Terrorspider = Пауки ужаса + .desc = На станции появились пауки ужаса. Эти мерзкие и кровожадные пауки хотят захватить станцию, превратив её в свой улей. diff --git a/Resources/Locale/ru-RU/Imperial/Terrorspider/terrorspider.ftl b/Resources/Locale/ru-RU/Imperial/Terrorspider/terrorspider.ftl new file mode 100644 index 00000000000..01f55d0d391 --- /dev/null +++ b/Resources/Locale/ru-RU/Imperial/Terrorspider/terrorspider.ftl @@ -0,0 +1 @@ +# Terror spider localization has been moved to spider.ftl diff --git a/Resources/Prototypes/Alerts/alerts.yml b/Resources/Prototypes/Alerts/alerts.yml index 5c70b7e5f14..860b154a89b 100644 --- a/Resources/Prototypes/Alerts/alerts.yml +++ b/Resources/Prototypes/Alerts/alerts.yml @@ -5,6 +5,9 @@ id: BaseAlertOrder order: - category: Health + - alertType: Sanity + - alertType: SCPBlink + - alertType: SCPFiremanPoints - category: Stamina - alertType: SuitPower - category: Internals @@ -36,6 +39,75 @@ layers: - map: [ "enum.AlertVisualLayers.Base" ] +- type: alert + id: Sanity + icons: + - sprite: /Textures/Interface/Alerts/battery.rsi + state: battery0 + - sprite: /Textures/Interface/Alerts/battery.rsi + state: battery1 + - sprite: /Textures/Interface/Alerts/battery.rsi + state: battery2 + - sprite: /Textures/Interface/Alerts/battery.rsi + state: battery3 + - sprite: /Textures/Interface/Alerts/battery.rsi + state: battery4 + - sprite: /Textures/Interface/Alerts/battery.rsi + state: battery5 + - sprite: /Textures/Interface/Alerts/battery.rsi + state: battery6 + - sprite: /Textures/Interface/Alerts/battery.rsi + state: battery7 + - sprite: /Textures/Interface/Alerts/battery.rsi + state: battery8 + - sprite: /Textures/Interface/Alerts/battery.rsi + state: battery9 + - sprite: /Textures/Interface/Alerts/battery.rsi + state: battery10 + minSeverity: 0 + maxSeverity: 10 + name: alerts-sanity-name + description: alerts-sanity-desc + +- type: alert + id: SCPBlink + icons: + - sprite: /Textures/Clothing/Eyes/Glasses/glasses.rsi + state: icon + clickEvent: !type:SCPBlinkAlertEvent + name: alerts-scp-blink-name + description: alerts-scp-blink-desc + +- type: alert + id: SCPFiremanPoints + icons: + - sprite: /Textures/Interface/Alerts/battery.rsi + state: battery0 + - sprite: /Textures/Interface/Alerts/battery.rsi + state: battery1 + - sprite: /Textures/Interface/Alerts/battery.rsi + state: battery2 + - sprite: /Textures/Interface/Alerts/battery.rsi + state: battery3 + - sprite: /Textures/Interface/Alerts/battery.rsi + state: battery4 + - sprite: /Textures/Interface/Alerts/battery.rsi + state: battery5 + - sprite: /Textures/Interface/Alerts/battery.rsi + state: battery6 + - sprite: /Textures/Interface/Alerts/battery.rsi + state: battery7 + - sprite: /Textures/Interface/Alerts/battery.rsi + state: battery8 + - sprite: /Textures/Interface/Alerts/battery.rsi + state: battery9 + - sprite: /Textures/Interface/Alerts/battery.rsi + state: battery10 + minSeverity: 0 + maxSeverity: 10 + name: alerts-scp-fireman-points-name + description: alerts-scp-fireman-points-desc + - type: alert id: LowOxygen category: Breathing diff --git a/Resources/Prototypes/Imperial/DeadSector/water.yml b/Resources/Prototypes/Imperial/DeadSector/water.yml new file mode 100644 index 00000000000..f3846bc82b2 --- /dev/null +++ b/Resources/Prototypes/Imperial/DeadSector/water.yml @@ -0,0 +1,72 @@ +# Реагент отравленной воды: 2 Poison за метаболический тик (~2 сек) ≈ 1 ед/сек +- type: reagent + id: DeadSectorWaterToxin + name: reagent-name-deadsector-water-toxin + group: Toxins + desc: reagent-desc-deadsector-water-toxin + flavor: bitter + color: "#4a6b3a" + physicalDesc: reagent-physical-desc-opaque + metabolisms: + Poison: + effects: + - !type:HealthChange + damage: + types: + Poison: 2 + +- type: entity + id: DeadSectorPoisonWater + name: отравленная вода + suffix: DeadSector, Structure + description: Мутная радиоактивная вода. Смертельно опасна. + placement: + mode: SnapgridCenter + components: + - type: Transform + anchored: true + noRot: true + - type: Physics + bodyType: Static + - type: Fixtures + fixtures: + slipFixture: + shape: + !type:PhysShapeAabb + bounds: "-0.45,-0.45,0.45,0.45" + mask: + - ItemMask + layer: + - SlipLayer + hard: false + - type: IconSmooth + key: deadsector_water + base: water + - type: Sprite + drawdepth: Puddles + sprite: Imperial/DeadSector/water/water.rsi + layers: + - sprite: Imperial/DeadSector/water/waterbuble.rsi + state: Water-buble-Sheet + - type: SolutionContainerManager + solutions: + puddle: + maxVol: 1000 + reagents: + - ReagentId: DeadSectorWaterToxin + Quantity: 500 + - type: SolutionRegeneration + solution: puddle + generated: + reagents: + - ReagentId: DeadSectorWaterToxin + Quantity: 5 + - type: DrawableSolution + solution: puddle + - type: DamageContacts + damage: + types: + Poison: 1 + - type: Appearance + - type: RadiationSource + intensity: 0.7 diff --git a/Resources/Prototypes/Imperial/Seriozha/SCP/Fredik21/scp.yml b/Resources/Prototypes/Imperial/Seriozha/SCP/Fredik21/scp.yml index 821dbf6eb1d..bab404a3dd8 100644 --- a/Resources/Prototypes/Imperial/Seriozha/SCP/Fredik21/scp.yml +++ b/Resources/Prototypes/Imperial/Seriozha/SCP/Fredik21/scp.yml @@ -20,6 +20,20 @@ - /Audio/Imperial/Seriozha/SCP/StepMTF2.ogg - /Audio/Imperial/Seriozha/SCP/StepMTF3.ogg +- type: soundCollection + id: SCP173NeckSnap + files: + - /Audio/Imperial/Seriozha/SCP/173/NeckSnap1.ogg + - /Audio/Imperial/Seriozha/SCP/173/NeckSnap2.ogg + - /Audio/Imperial/Seriozha/SCP/173/NeckSnap3.ogg + +- type: soundCollection + id: SCP173Rattle + files: + - /Audio/Imperial/Seriozha/SCP/173/Rattle1.ogg + - /Audio/Imperial/Seriozha/SCP/173/Rattle2.ogg + - /Audio/Imperial/Seriozha/SCP/173/Rattle3.ogg + - type: entity parent: PosterBase id: Posterbrokengod @@ -4085,6 +4099,9 @@ state: alive sprite: Imperial/Seriozha/Fredik21/nda/nda4975.rsi scale: 1.5, 1.5 + - map: ["enum.TriggerVisualLayers.Base"] + state: alive + sprite: Imperial/Seriozha/Fredik21/nda/nda4975.rsi - type: DamageStateVisuals states: Alive: @@ -5077,6 +5094,9 @@ state: alive sprite: Imperial/Seriozha/Fredik21/nda/nda682.rsi scale: 2, 2 + - map: ["enum.TriggerVisualLayers.Base"] + state: alive + sprite: Imperial/Seriozha/Fredik21/nda/nda682.rsi - type: DamageStateVisuals states: Alive: @@ -7250,6 +7270,9 @@ - map: ["enum.DamageStateVisualLayers.Base"] state: alive sprite: Imperial/Seriozha/Fredik21/nda/nda020.rsi + - map: ["enum.TriggerVisualLayers.Base"] + state: alive + sprite: Imperial/Seriozha/Fredik21/nda/nda020.rsi - type: DamageStateVisuals states: Alive: @@ -7327,6 +7350,293 @@ raffle: settings: short +- type: entity + name: SCP-008 + suffix: Fredik21, Недра, Объект + parent: BaseStructure + id: ImperialSCP008 + description: Вирусный источник. Длительное нахождение рядом приводит к заражению и превращению в зомби. + components: + - type: Sprite + sprite: Imperial/Seriozha/SCP/objects/Zombie.rsi + state: alive + - type: Physics + bodyType: Static + - type: Fixtures + fixtures: + fix1: + shape: + !type:PhysShapeAabb + bounds: "-0.45,-0.45,0.45,0.45" + hard: false + mask: + - None + layer: + - None + - type: Anchorable + fixed: true + - type: Damageable + - type: Destructible + thresholds: [] + - type: SCP008InfectionAura + radius: 8 + zombifyDelay: 12 + updateInterval: 1 + allowCritical: false + warningDelay: 3 + warningPopup: "SCP-008: покиньте зону немедленно, иначе вы превратитесь в зомби!" + +- type: entity + name: SCP-173 + suffix: Fredik21, Недра, Объект + parent: ImperialSCPBase + id: ImperialSCP173 + description: Не разрывайте зрительный контакт. + components: + - type: Sprite + drawdepth: Mobs + layers: + - map: ["enum.DamageStateVisualLayers.Base"] + state: 173 + sprite: Imperial/Seriozha/Fredik21/nda/nda173.rsi + - map: ["enum.TriggerVisualLayers.Base"] + state: 173 + sprite: Imperial/Seriozha/Fredik21/nda/nda173.rsi + visible: false + - type: TimerTriggerVisuals + unprimedSprite: 173 + primingSprite: 173 + - type: DamageStateVisuals + states: + Alive: + Base: 173 + Dead: + Base: 173 + - type: MobThresholds + thresholds: + 0: Alive + 8000: Dead + - type: MovementSpeedModifier + baseWalkSpeed: 4.2 + baseSprintSpeed: 6.0 + - type: MeleeWeapon + hidden: true + altDisarm: false + soundHit: + collection: SCP173NeckSnap + angle: 0 + animation: WeaponArcBite + attackRate: 1.2 + damage: + types: + Blunt: 200 + - type: FootstepModifier + footstepSoundCollection: + collection: SCP173Rattle + - type: SCP173WatchLock + observeRadius: 8 + minLookDot: 0.35 + requireLightToObserve: true + lightLookupRadius: 12 + - type: SCPBlinkManualTrigger + - type: SCP173LightFlicker + action: ActionSCP173LightFlicker + radius: 9 + duration: 8 + toggleInterval: 0.25 + - type: HTN + rootTask: + task: SimpleHostileCompound + - type: Puller + - type: Pullable + - type: Fixtures + fixtures: + fix1: + shape: + !type:PhysShapeCircle + radius: 0.4 + density: 350 + mask: + - MobMask + layer: + - MobLayer + +- type: entity + name: SCP-096 + suffix: Fredik21, Недра, Объект + parent: ImperialSCPBase + id: ImperialSCP096 + description: Не смотри ему в лицо. + components: + - type: Sprite + drawdepth: Mobs + layers: + - map: ["enum.DamageStateVisualLayers.Base"] + state: scp + sprite: Imperial/Seriozha/Fredik21/nda/nda096.rsi + - map: ["enum.TriggerVisualLayers.Base"] + state: scp + sprite: Imperial/Seriozha/Fredik21/nda/nda096.rsi + visible: false + - type: TimerTriggerVisuals + unprimedSprite: scp + primingSprite: scp-screaming + - type: DamageStateVisuals + states: + Alive: + Base: scp + Dead: + Base: scp-dead + - type: Appearance + - type: GenericVisualizer + visuals: + enum.SCP096Visuals.State: + enum.DamageStateVisualLayers.Base: + Calm: + state: scp + Screaming: + state: scp-screaming + Chasing: + state: scp-chasing + Dead: + state: scp-dead + - type: MobThresholds + thresholds: + 0: Alive + 7000: Dead + - type: MovementSpeedModifier + baseWalkSpeed: 1.25 + baseSprintSpeed: 1.9 + - type: MeleeWeapon + hidden: true + altDisarm: false + soundHit: + path: /Audio/Weapons/Xeno/alien_claw_flesh3.ogg + angle: 0 + animation: WeaponArcClaw + attackRate: 1.0 + damage: + types: + Blunt: 30 + Structural: 500 + - type: SCP096RageOnLook + observeRadius: 9 + minLookDot: 0.42 + enragedWalkModifier: 3.2 + enragedSprintModifier: 3.2 + rageWindup: 28 + rageDuration: 120 + rageWindupPopup: "SCP-096 в бешенстве..." + ragePopup: "SCP-096 впадает в ярость!" + rageCalmPopup: "SCP-096 успокаивается." + rageSound: /Audio/Imperial/Seriozha/SCP/096/nda096agr.ogg + cryingSound: /Audio/Imperial/Seriozha/SCP/096/nda096cry.ogg + rageLoopSound: /Audio/Imperial/Seriozha/SCP/096/nda096rage.ogg + - type: AmbientSound + enabled: true + sound: /Audio/Imperial/Seriozha/SCP/096/nda096cry.ogg + range: 12 + volume: -4 + - type: Strippable + - type: UserInterface + interfaces: + enum.StrippingUiKey.Key: + type: StrippableBoundUserInterface + - type: InventorySlots + - type: Inventory + templateId: scp096inv + - type: SCPBlinkManualTrigger + - type: HTN + rootTask: + task: SimpleHostileCompound + - type: Puller + - type: Pullable + - type: Fixtures + fixtures: + fix1: + shape: + !type:PhysShapeCircle + radius: 0.4 + density: 320 + mask: + - MobMask + layer: + - MobLayer + +- type: entity + id: ImperialSCP173ContainmentCell + name: клетка сдерживания SCP-173 + suffix: Fredik21, Недра, Объект + parent: ClosetSteelBase + description: Удерживающий ящик для SCP-173. Пока объект внутри, он не может двигаться и атаковать. Ящик можно таскать. + components: + - type: SCP173ContainmentCell + - type: EntityStorage + capacity: 1 + isCollidableWhenOpen: false + openOnMove: false + enteringRange: 0.45 + whitelist: + components: + - SCP173WatchLock + - type: Appearance + - type: EntityStorageVisuals + stateBaseClosed: generic + stateDoorOpen: generic_open + stateDoorClosed: generic_door + +- type: entity + id: ImperialSCP096HeadBag + name: мешок на голову SCP-096 + suffix: Fredik21, Недра, Объект + parent: ClothingHeadPaperSack + description: Плотный мешок, закрывающий лицо SCP-096 и подавляющий вход в ярость. + components: + - type: SCP096RageSuppressor + +- type: entity + parent: BaseAction + id: ActionSCP173LightFlicker + name: Светопомеха + description: Заставляет свет вокруг вас мигать. В темноте на вас нельзя смотреть. + components: + - type: Action + useDelay: 25 + - type: InstantAction + event: !type:SCP173LightFlickerActionEvent + +- type: entity + parent: MarkerBase + id: NedraNukeExplosionSpawner + name: Nedra nuke explosion spawner + suffix: Fredik21, Недра, Объект + description: Маркер точки детонации для команды nedranukeprotocol. + components: + - type: Sprite + sprite: Markers/cross.rsi + +- type: inventoryTemplate + id: scp096inv + slots: + - name: head + slotTexture: head + fullTextureName: template_small + slotFlags: HEAD + slotGroup: MainHotbar + stripTime: 3 + uiWindowPos: 1,2 + strippingWindowPos: 1,2 + displayName: Head + - name: mask + slotTexture: mask + fullTextureName: template_small + slotFlags: MASK + slotGroup: MainHotbar + stripTime: 3 + uiWindowPos: 0,2 + strippingWindowPos: 0,2 + displayName: Mask + - type: entity parent: AtmosDeviceFanTiny id: titleNDA020 @@ -8595,6 +8905,9 @@ - map: ["enum.DamageStateVisualLayers.Base"] state: alive sprite: Imperial/Seriozha/Fredik21/nda/nda131.rsi + - map: ["enum.TriggerVisualLayers.Base"] + state: alive + sprite: Imperial/Seriozha/Fredik21/nda/nda131.rsi - type: DamageStateVisuals states: Alive: @@ -8668,6 +8981,9 @@ - map: ["enum.DamageStateVisualLayers.Base"] state: alive sprite: Imperial/Seriozha/Fredik21/nda/nda131A.rsi + - map: ["enum.TriggerVisualLayers.Base"] + state: alive + sprite: Imperial/Seriozha/Fredik21/nda/nda131A.rsi - type: DamageStateVisuals states: Alive: @@ -16359,7 +16675,7 @@ components: - type: Sprite sprite: Imperial/Seriozha/Fredik21/structure/decor/Sign/nda.rsi - state: icon2 + state: icon3 - type: Physics bodyType: Static - type: Fixtures diff --git a/Resources/Prototypes/Imperial/Seriozha/SCP/SCP.yml b/Resources/Prototypes/Imperial/Seriozha/SCP/SCP.yml index 188090ef862..cf03abbe854 100644 --- a/Resources/Prototypes/Imperial/Seriozha/SCP/SCP.yml +++ b/Resources/Prototypes/Imperial/Seriozha/SCP/SCP.yml @@ -22,6 +22,7 @@ primingSound: path: /Audio/Effects/Smoke-grenade.ogg - type: NoSlip + - type: Appearance - type: MovementIgnoreGravity - type: ZombieImmune - type: NonSpreaderZombie @@ -142,6 +143,9 @@ - map: ["enum.DamageStateVisualLayers.Base"] state: alive sprite: Imperial/Seriozha/SCP/objects/Fireman.rsi + - map: ["enum.TriggerVisualLayers.Base"] + state: alive + sprite: Imperial/Seriozha/SCP/objects/Fireman.rsi - type: DamageStateVisuals states: Alive: @@ -203,6 +207,9 @@ - type: ToggleClothing action: ActionToggleSCPMusic mustEquip: false + - type: TimerTriggerVisuals + unprimedSprite: alive + primingSprite: alive - type: Reactive groups: Flammable: [ Touch ] @@ -237,6 +244,8 @@ params: loop: true stream: true + - type: SCPFireman + maxOwnedFires: 50 - type: entity name: Avel @@ -686,3 +695,167 @@ id: ImperialAvelGear inhand: - ImperialSCPKatana + +- type: entity + id: ActionSCPFiremanIgnite + parent: BaseAction + name: Поджог + description: Создаёт огонь под собой. + components: + - type: Action + checkCanAccess: false + useDelay: 1 + - type: InstantAction + event: !type:SCPFiremanIgniteActionEvent + +- type: entity + id: ActionSCPFiremanFireball + parent: BaseAction + name: Огненный шар + description: Создаёт взрыв огня в выбранной точке. + components: + - type: Action + checkCanAccess: false + useDelay: 2 + - type: TargetAction + checkCanAccess: false + range: 60 + - type: WorldTargetAction + event: !type:SCPFiremanFireballActionEvent + +- type: entity + id: ActionSCPFiremanWhirl + parent: BaseAction + name: Огненный вихрь + description: Выпускает огненный вихрь в направлении курсора. + components: + - type: Action + checkCanAccess: false + useDelay: 2 + - type: TargetAction + checkCanAccess: false + range: 60 + - type: WorldTargetAction + event: !type:SCPFiremanWhirlActionEvent + +- type: entity + id: ActionSCPFiremanMelt + parent: BaseAction + name: Плавление + description: Наносит 100 урона выбранной структуре и поджигает её. + components: + - type: Action + checkCanAccess: false + useDelay: 2 + - type: TargetAction + checkCanAccess: false + range: 20 + - type: EntityTargetAction + event: !type:SCPFiremanMeltActionEvent + +- type: entity + id: ActionSCPFiremanTrueFlame + parent: BaseAction + name: Истинное пламя + description: Делает вас неуязвимым на 25 секунд. + components: + - type: Action + checkCanAccess: false + useDelay: 2 + - type: InstantAction + event: !type:SCPFiremanTrueFlameActionEvent + +- type: entity + id: ActionSCPFiremanStrike + parent: BaseAction + name: Огненный удар + description: Через 1.5 секунды поджигает существ в радиусе 20 клеток. + components: + - type: Action + checkCanAccess: false + useDelay: 2 + - type: InstantAction + event: !type:SCPFiremanStrikeActionEvent + +- type: entity + id: ActionSCPFiremanSecondMode + parent: BaseAction + name: Второй режим + description: Переключает режим ЛКМ-излучения огня. + components: + - type: Action + checkCanAccess: false + useDelay: 0.5 + - type: InstantAction + event: !type:SCPFiremanSecondModeActionEvent + +- type: entity + id: SCPFiremanFlame + name: Пламя + description: Горячее пламя. + components: + - type: Transform + anchored: true + - type: Physics + bodyType: Static + - type: TimedDespawn + lifetime: 120 + - type: Sprite + noRot: true + sprite: Effects/fire.rsi + layers: + - state: 1 + - type: Appearance + - type: FireVisuals + sprite: Effects/fire.rsi + normalState: 1 + - type: PointLight + color: "#ff8c32" + radius: 2 + energy: 2 + - type: IgnitionSource + temperature: 1000 + - type: Flammable + onFire: true + fireStacks: 5 + alwaysCombustible: true + canExtinguish: true + damage: + types: + Heat: 0 + - type: Reactive + groups: + Extinguish: [ Touch ] + reactions: + - reagents: [ Water, SpaceCleaner ] + methods: [ Touch ] + effects: + - !type:Extinguish + - type: SCPFireSpread + igniteRadius: 0.55 + igniteInterval: 1.5 + spreadInterval: 18 + maxSpreadPerPulse: 2 + +- type: entity + id: SCPFiremanWhirl + name: Огненный вихрь + description: Движущийся огонь. + components: + - type: Transform + anchored: false + - type: TimedDespawn + - type: Sprite + noRot: true + sprite: Effects/fire.rsi + layers: + - state: 2 + - type: PointLight + color: "#ff8c32" + radius: 2 + energy: 2 + - type: IgnitionSource + temperature: 1000 + - type: SCPFireWhirl + lifetime: 8 + effectInterval: 0.33 diff --git a/Resources/Prototypes/Imperial/Seriozha/SCP/presets.yml b/Resources/Prototypes/Imperial/Seriozha/SCP/presets.yml index 7229a05d178..330008aa228 100644 --- a/Resources/Prototypes/Imperial/Seriozha/SCP/presets.yml +++ b/Resources/Prototypes/Imperial/Seriozha/SCP/presets.yml @@ -4,6 +4,7 @@ id: ImperialSCPBasePreset components: - type: AntagImmune + - type: Sanity - type: RandomHumanoidAppearance randomizeName: false - type: CrewSkills @@ -33,6 +34,15 @@ - type: Damageable damageContainer: BiologicalMemetical damageModifierSet: Infernal + - type: SCPBlinkable + blinkInterval: 14 + blinkDuration: 0.8 + manualBlinkCooldown: 4 + manualBlinkPopup: "Вы моргнули." + blinkStartPopup: "Вы моргаете..." + blinkEndPopup: "" + blinkStartSound: null + blinkEndSound: null - type: ShowJobIcons - type: ShowMindShieldIcons diff --git a/Resources/Prototypes/Imperial/Terrorspider/TerrorSpider.yml b/Resources/Prototypes/Imperial/Terrorspider/TerrorSpider.yml index e7c7c503c96..daa59d394c4 100644 --- a/Resources/Prototypes/Imperial/Terrorspider/TerrorSpider.yml +++ b/Resources/Prototypes/Imperial/Terrorspider/TerrorSpider.yml @@ -1,9 +1,14 @@ - type: entity - name: Rusar terror + name: Knight terror parent: MobRusarSpider id: MobRusarSpiderAngrys suffix: terror spider components: + - type: TerrorSpiderWebBuffReceiver + - type: TerrorSpiderKnightGuard + guardSpeedMultiplier: 0.8 + - type: TerrorSpiderKnightRage + enragedSpeedMultiplier: 1 - type: NpcFactionMember factions: - Xeno @@ -27,6 +32,7 @@ parent: LightImplantSpider id: LightImplantSpiderRusar name: Spawn web + noSpawn: true components: - type: SubdermalImplant implantAction: ActionSpawnSpiderRusar @@ -35,6 +41,7 @@ id: ActionSpawnSpiderRusar name: Spawn web description: it spans the web. A little stronger than usual. + noSpawn: true components: - type: Action useDelay: 2.5 @@ -42,21 +49,40 @@ icon: sprite: Structures/floor_web.rsi state: full + - type: InstantAction + event: !type:TerrorSpiderSpawnWebActionEvent + webPrototype: SpiderWebTerrorRusar + +- type: entity + id: ActionTerrorSpiderCocoon + name: Cocoon + description: Wrap a living target in cocoon. + noSpawn: true + components: + - type: Action + useDelay: 4 + itemIconStyle: BigAction + icon: + sprite: Imperial/TerrorSpider/effects.rsi + state: cocoon_large3 - type: TargetAction + canTargetSelf: false range: 1 - - type: WorldTargetAction - event: !type:WorldSpawnSpellEvent - prototypes: - - id: SpiderWebTerrorRusar - amount: 1 - offset: 0, 1 + - type: EntityTargetAction + event: !type:TerrorSpiderCocoonActionEvent - type: entity - name: Rusar terror + name: Knight terror parent: [ SimpleMobBases, MobCombat ] id: MobRusarSpider - description: Fights with the crew, protects other spiders. Due to its stability, it creates free space so that other spiders can expand the nest. + description: Fights the crew and protects other spiders. Thanks to its resilience, it creates space for the nest to expand. components: + - type: TerrorSpiderCocoon + - type: TerrorSpiderWebBuffReceiver + - type: TerrorSpiderKnightGuard + guardSpeedMultiplier: 0.8 + - type: TerrorSpiderKnightRage + enragedSpeedMultiplier: 1 - type: Sprite drawdepth: Mobs layers: @@ -119,8 +145,9 @@ interactSuccessSpawn: EffectHearts - type: NoSlip - type: Spider - webPrototype: SpiderWebTerrorUwU - webAction: TerrorWeedsAction + webPrototype: SpiderWebTerrorRusar + webAction: TerrorWeedsRusarAction + spawnsWebsAsNonPlayer: false - type: IgnoreSpiderWeb - type: Bloodstream bloodMaxVolume: 0 @@ -137,19 +164,16 @@ - type: SlowOnDamage speedModifierThresholds: 200: 0.7 - - type: Armor - modifiers: - coefficients: - Blunt: 0.6 - Slash: 0.6 - Piercing: 0.6 + - type: TerrorSpiderArmor + bruteModifier: 0.75 + burnModifier: 1.25 - type: ContainerContainer containers: cell_slot: !type:ContainerSlot - type: PassiveDamage allowedStates: - Alive - damageCap: 174 + damageCap: 220 damage: types: Poison: -1 @@ -159,11 +183,14 @@ - type: entity - name: Dron terror + name: Drone terror parent: MobDronSpider id: MobDronSpiderAngrys suffix: terror spider components: + - type: TerrorSpiderWebBuffReceiver + speedMultiplier: 1.4 + - type: TerrorDronMeleeDebuff - type: NpcFactionMember factions: - Xeno @@ -187,6 +214,7 @@ parent: LightImplantSpider id: LightImplantSpiderDron name: Spawn eggs 2 tir + noSpawn: true components: - type: SubdermalImplant implantAction: ActionSpawnSpiderDron @@ -195,28 +223,29 @@ id: ActionSpawnSpiderDron name: Spawn web description: it spans the web. A little stronger than usual. + noSpawn: true components: - type: Action - useDelay: 2 + useDelay: 1 itemIconStyle: BigAction icon: sprite: Structures/floor_web.rsi state: full - - type: TargetAction - range: 1 - - type: WorldTargetAction - event: !type:WorldSpawnSpellEvent - prototypes: - - id: SpiderWebTerrorDron - amount: 1 - offset: 0, 1 + - type: InstantAction + event: !type:TerrorSpiderSpawnWebActionEvent + webPrototype: SpiderWebTerrorDron - type: entity - name: Dron terror + name: Drone terror parent: [ SimpleMobBases, MobCombat ] id: MobDronSpider - description: It weaves a web around everything, drives away the crew with spit, protects the nest. + description: Covers everything in webs, repels the crew with spits, and protects the nest. components: + - type: TerrorSpiderWebBuffReceiver + speedMultiplier: 1.4 + - type: TerrorDronMeleeDebuff + slowDuration: 4 + slowMultiplier: 0.5 - type: Sprite drawdepth: Mobs layers: @@ -258,10 +287,17 @@ damage: types: Piercing: 10 + Structural: 50 - type: SolutionContainerManager solutions: melee: maxVol: 30 + - type: SolutionRegeneration + solution: melee + generated: + reagents: + - ReagentId: FrostOilTerrorSpider + Quantity: 600 - type: MeleeChemicalInjector transferAmount: 10 solution: melee @@ -272,8 +308,8 @@ interactSuccessSpawn: EffectHearts - type: NoSlip - type: Spider - webPrototype: SpiderWebTerrorUwU - webAction: TerrorWeedsAction + webPrototype: SpiderWebTerrorDron + spawnsWebsAsNonPlayer: false - type: IgnoreSpiderWeb - type: Bloodstream bloodMaxVolume: 0 @@ -286,29 +322,29 @@ - FootstepSound - type: MovementSpeedModifier baseWalkSpeed : 4 - baseSprintSpeed : 6 + baseSprintSpeed : 4 - type: SlowOnDamage speedModifierThresholds: 80: 0.7 - type: RechargeBasicEntityAmmo - rechargeCooldown: 0.75 + rechargeCooldown: 3 - type: BasicEntityAmmoProvider proto: TerrorDrontAcid - capacity: 1 - count: 1 + capacity: 2 + count: 2 - type: Gun - fireRate: 0.75 + fireRate: 1 + burstFireRate: 2 useKey: false - selectedMode: FullAuto + selectedMode: Burst availableModes: - - FullAuto + - Burst + shotsPerBurst: 2 + burstCooldown: 3 soundGunshot: /Audio/Weapons/Xeno/alien_spitacid.ogg - - type: Armor - modifiers: - coefficients: - Blunt: 0.75 - Slash: 0.75 - Piercing: 0.75 + - type: TerrorSpiderArmor + bruteModifier: 0.75 + burnModifier: 1.25 - type: entity @@ -317,10 +353,14 @@ parent: BaseBullet categories: [ HideSpawnMenu ] components: + - type: TerrorDronProjectileDebuff + slowDuration: 2 + slowMultiplier: 0.5 + staminaDamage: 15 - type: Projectile damage: types: - Caustic: 20 + Poison: 15 - type: Sprite sprite: Objects/Weapons/Guns/Projectiles/xeno_toxic.rsi layers: @@ -334,6 +374,12 @@ id: MobsogladatelSpiderAngrys suffix: terror spider components: + - type: TerrorSpiderWebBuffReceiver + - type: TerrorSpiderLurker + stealthDuration: 8 + baseMeleeDamage: 15 + webBuffMeleeDamage: 45 + webBuffStaminaDamage: 45 - type: NpcFactionMember factions: - Xeno @@ -354,8 +400,17 @@ name: Lurker terror parent: [ SimpleMobBases, MobCombat ] id: MobsogladatelSpider - description: He waits in ambush, and then quickly deals with his target, acting either as a loner or in a nest. + description: Waits in ambush, then quickly dispatches the target. Acts alone or as part of the nest. components: + - type: TerrorSpiderCocoon + - type: TerrorSpiderWebBuffReceiver + regenPerTick: 2 + regenInterval: 2 + - type: TerrorSpiderLurker + stealthDuration: 8 + baseMeleeDamage: 15 + webBuffMeleeDamage: 45 + webBuffStaminaDamage: 45 - type: Sprite drawdepth: Mobs layers: @@ -397,6 +452,7 @@ damage: types: Piercing: 15 + Structural: 40 - type: SolutionContainerManager solutions: melee: @@ -417,8 +473,9 @@ interactSuccessSpawn: EffectHearts - type: NoSlip - type: Spider - webPrototype: SpiderWebTerrorUwU - webAction: TerrorWeedsAction + webPrototype: SpiderWebTerrorLurker + webAction: TerrorWeedsLurkerAction + spawnsWebsAsNonPlayer: false - type: IgnoreSpiderWeb - type: Bloodstream bloodMaxVolume: 0 @@ -430,17 +487,23 @@ - DoorBumpOpener - FootstepSound - type: MovementSpeedModifier - baseWalkSpeed : 4 - baseSprintSpeed : 4 + baseWalkSpeed : 5.2 + baseSprintSpeed : 5.2 + actionPsionicInvisibility: ActionToggleSuitInvisiblePower + stunSecond: 1 + - type: PassiveDamage + allowedStates: + - Alive + damageCap: 100 + damage: + groups: + Brute: -2 - type: SlowOnDamage speedModifierThresholds: 80: 0.7 - - type: Armor - modifiers: - coefficients: - Blunt: 0.75 - Slash: 0.75 - Piercing: 0.75 + - type: TerrorSpiderArmor + bruteModifier: 0.75 + burnModifier: 1.25 - type: entity @@ -449,6 +512,9 @@ id: MobGiantreaperSpiderAngry suffix: terror spider components: + - type: TerrorSpiderWebBuffReceiver + - type: TerrorSpiderReaperLifesteal + healAmount: 5 - type: NpcFactionMember factions: - Xeno @@ -469,8 +535,12 @@ name: Reaper terror parent: [ SimpleMobBases, MobCombat ] id: MobreaperSpider - description: A berserker must behave aggressively and bite someone regularly in order to survive. Absolutely deadly in close combat. + description: A berserker that must constantly bite someone to survive. Absolutely deadly in melee combat. components: + - type: TerrorSpiderCocoon + - type: TerrorSpiderWebBuffReceiver + - type: TerrorSpiderReaperLifesteal + healAmount: 5 - type: Sprite drawdepth: Mobs layers: @@ -511,7 +581,7 @@ path: /Audio/Effects/bite.ogg damage: types: - Piercing: 30 + Piercing: 25 - type: SolutionContainerManager solutions: melee: @@ -531,9 +601,6 @@ interactFailureString: petting-failure-generic interactSuccessSpawn: EffectHearts - type: NoSlip - - type: Spider - webPrototype: SpiderWebTerrorUwU - webAction: TerrorWeedsAction - type: IgnoreSpiderWeb - type: Bloodstream bloodMaxVolume: 0 @@ -545,18 +612,18 @@ - DoorBumpOpener - FootstepSound - type: MovementSpeedModifier - baseWalkSpeed : 4 - baseSprintSpeed : 4 + baseWalkSpeed : 5.2 + baseSprintSpeed : 5.2 + - type: PassiveDamage + allowedStates: + - Alive + damageCap: 130 + damage: + groups: + Brute: 1 - type: SlowOnDamage speedModifierThresholds: 120: 0.7 - - type: Armor - modifiers: - coefficients: - Blunt: 0.75 - Slash: 0.75 - Piercing: 0.75 - - type: entity name: Destroye terror @@ -564,6 +631,8 @@ id: MobGiantDestroyerSpiderAngry suffix: terror spider components: + - type: TerrorSpiderWebBuffReceiver + - type: TerrorSpiderDestroyer - type: NpcFactionMember factions: - Xeno @@ -579,16 +648,18 @@ raffle: settings: short - type: GhostTakeoverAvailable - - type: AutoImplant - implants: - - EmpImplant - type: entity name: Destroyer terror parent: [ SimpleMobBases, MobCombat ] id: MobDestroyerSpider - description: Breaks down walls and doors, sabotages the station, destroys departments. The main enemy of synthetics. + description: Breaks walls and doors, sabotages the station, and destroys departments. The main enemy of synthetics. components: + - type: TerrorSpiderWebBuffReceiver + - type: TerrorSpiderDestroyer + empScreamRange: 3.5 + deathEmpRange: 6.5 + fireBurstRadius: 3.5 - type: Sprite drawdepth: Mobs layers: @@ -621,7 +692,7 @@ - type: MobThresholds thresholds: 0: Alive - 100: Dead + 120: Dead - type: MeleeWeapon angle: 0 animation: WeaponArcBite @@ -650,9 +721,6 @@ interactFailureString: petting-failure-generic interactSuccessSpawn: EffectHearts - type: NoSlip - - type: Spider - webPrototype: SpiderWebTerrorUwU - webAction: TerrorWeedsAction - type: IgnoreSpiderWeb - type: Bloodstream bloodMaxVolume: 0 @@ -664,8 +732,8 @@ - DoorBumpOpener - FootstepSound - type: MovementSpeedModifier - baseWalkSpeed : 4 - baseSprintSpeed : 4 + baseWalkSpeed : 4.4 + baseSprintSpeed : 4.4 - type: SlowOnDamage speedModifierThresholds: 80: 0.7 @@ -685,19 +753,13 @@ - type: PassiveDamage allowedStates: - Alive - damageCap: 99 + damageCap: 120 damage: - types: - Poison: -1 groups: - Brute: -1 - Burn: -1 - - type: Armor - modifiers: - coefficients: - Blunt: 0.75 - Slash: 0.75 - Piercing: 0.75 + Brute: -2 + - type: TerrorSpiderArmor + bruteModifier: 0.75 + burnModifier: 1.25 - type: entity id: TerrorRavAcid @@ -725,9 +787,9 @@ categories: [ HideSpawnMenu ] components: - type: Sprite - sprite: Imperial/TerrorSpider/Spider/bluexeno_toxin.rsi + sprite: Objects/Weapons/Guns/Projectiles/xeno_toxic.rsi layers: - - state: bluetox + - state: xeno_toxic - type: EmpOnTrigger range: 5 energyConsumption: 50000 @@ -745,6 +807,11 @@ id: MobGiantWidowSpiderAngry suffix: terror spider components: + - type: TerrorSpiderWebBuffReceiver + - type: TerrorSpiderWidowMeleeDebuff + muteDuration: 10 + venomPerHit: 10 + venomReagent: BlackTerrorVenom - type: NpcFactionMember factions: - Xeno @@ -762,7 +829,8 @@ - type: GhostTakeoverAvailable - type: AutoImplant implants: - - LightImplantSpiderRusar + - LightImplantSpiderWidowSmoke + - LightImplantSpiderWidowPoison - type: entity name: Widow terror @@ -770,6 +838,12 @@ id: MobWidowSpider description: One of the deadliest spiders, it kills any organic target in a few bites, has dangerous spits, but has little health. It hunts singletons, or attacks together with other spiders. components: + - type: TerrorSpiderCocoon + - type: TerrorSpiderWebBuffReceiver + - type: TerrorSpiderWidowMeleeDebuff + muteDuration: 10 + venomPerHit: 10 + venomReagent: BlackTerrorVenom - type: Sprite drawdepth: Mobs layers: @@ -802,7 +876,7 @@ - type: MobThresholds thresholds: 0: Alive - 100: Dead + 120: Dead - type: MeleeWeapon angle: 0 animation: WeaponArcBite @@ -810,7 +884,8 @@ path: /Audio/Effects/bite.ogg damage: types: - Piercing: 15 + Piercing: 10 + Structural: 40 - type: SolutionContainerManager solutions: melee: @@ -819,7 +894,7 @@ solution: melee generated: reagents: - - ReagentId: Toxin + - ReagentId: BlackTerrorVenom Quantity: 1 - type: MeleeChemicalInjector transferAmount: 0.75 @@ -831,8 +906,9 @@ interactSuccessSpawn: EffectHearts - type: NoSlip - type: Spider - webPrototype: SpiderWebTerrorUwU - webAction: TerrorWeedsAction + webPrototype: SpiderWebTerrorWidow + webAction: TerrorWeedsWidowAction + spawnsWebsAsNonPlayer: false - type: IgnoreSpiderWeb - type: Bloodstream bloodMaxVolume: 0 @@ -844,41 +920,175 @@ - DoorBumpOpener - FootstepSound - type: MovementSpeedModifier - baseWalkSpeed : 4 - baseSprintSpeed : 4 + baseWalkSpeed : 4.4 + baseSprintSpeed : 4.4 - type: SlowOnDamage speedModifierThresholds: 80: 0.7 - type: RechargeBasicEntityAmmo - rechargeCooldown: 20 + rechargeCooldown: 3 - type: BasicEntityAmmoProvider - proto: GasXenoSpitBullet - capacity: 1 - count: 1 + proto: TerrorWidowSpit + capacity: 2 + count: 2 - type: Gun - fireRate: 20 + fireRate: 1 + burstFireRate: 2 useKey: false - selectedMode: FullAuto + selectedMode: Burst availableModes: - - FullAuto + - Burst + shotsPerBurst: 2 + burstCooldown: 3 soundGunshot: /Audio/Weapons/Xeno/alien_spitacid.ogg - type: PassiveDamage allowedStates: - Alive - damageCap: 99 + damageCap: 120 damage: - types: - Poison: -1 groups: - Brute: -1 - Burn: -1 - - type: Armor - modifiers: - coefficients: - Blunt: 0.75 - Slash: 0.75 - Piercing: 0.75 + Brute: -2 + Burn: -2 + - type: TerrorSpiderArmor + bruteModifier: 0.75 + burnModifier: 1.25 + + - type: AutoImplant + implants: + - LightImplantSpiderWidowSmoke + - LightImplantSpiderWidowPoison + +- type: entity + parent: LightImplantSpider + id: LightImplantSpiderWidowSmoke + name: Smoke spit + noSpawn: true + components: + - type: SubdermalImplant + implantAction: ActionTerrorSpiderWidowSmokeSpit + +- type: entity + parent: LightImplantSpider + id: LightImplantSpiderWidowPoison + name: Poison spit + noSpawn: true + components: + - type: SubdermalImplant + implantAction: ActionTerrorSpiderWidowPoisonSpit + +- type: entity + id: ActionTerrorSpiderWidowSmokeSpit + name: Smoke spit + description: Launch a harmless smoke spit. + noSpawn: true + components: + - type: Action + useDelay: 10 + itemIconStyle: BigAction + icon: + sprite: Imperial/TerrorSpider/act.rsi + state: parasmoke + - type: TargetAction + checkCanAccess: false + range: 25 + - type: WorldTargetAction + event: !type:ProjectileSpellEvent + prototype: TerrorWidowSmokeSpitBullet + +- type: entity + id: ActionTerrorSpiderWidowPoisonSpit + name: Poison spit + description: Launch a spit that creates a venom cloud. + noSpawn: true + components: + - type: Action + useDelay: 25 + itemIconStyle: BigAction + icon: + sprite: Imperial/TerrorSpider/act.rsi + state: smoke_old + - type: TargetAction + checkCanAccess: false + range: 25 + - type: WorldTargetAction + event: !type:ProjectileSpellEvent + prototype: TerrorWidowPoisonSpitBullet + +- type: entity + id: TerrorWidowSpit + name: Widow spit + parent: BaseBullet + categories: [ HideSpawnMenu ] + components: + - type: TerrorDronProjectileDebuff + slowDuration: 0 + slowMultiplier: 1 + staminaDamage: 10 + - type: Projectile + damage: + types: + Poison: 15 + - type: Sprite + sprite: Objects/Weapons/Guns/Projectiles/xeno_toxic.rsi + layers: + - state: xeno_toxic + - type: Ammo + muzzleFlash: null + +- type: entity + id: TerrorWidowSmokeSpitBullet + name: Widow smoke spit + parent: BaseBulletTrigger + categories: [ HideSpawnMenu ] + components: + - type: Sprite + sprite: Objects/Weapons/Guns/Projectiles/magic.rsi + scale: 1.3, 2 + layers: + - state: declone + shader: unshaded + - type: Projectile + deleteOnCollide: true + damage: + types: + Blunt: 1 + soundHit: + path: /Audio/Effects/gen_hit.ogg + - type: SmokeOnTrigger + duration: 10 + spreadAmount: 30 + solution: + reagents: + - ReagentId: Water + Quantity: 1 +- type: entity + id: TerrorWidowPoisonSpitBullet + name: Widow poison spit + parent: BaseBulletTrigger + categories: [ HideSpawnMenu ] + components: + - type: Sprite + sprite: Objects/Weapons/Guns/Projectiles/magic.rsi + scale: 1.3, 2 + layers: + - state: declone + shader: unshaded + - type: Projectile + deleteOnCollide: true + damage: + types: + Blunt: 1 + soundHit: + path: /Audio/Effects/gen_hit.ogg + - type: SmokeOnTrigger + duration: 10 + spreadAmount: 30 + solution: + reagents: + - ReagentId: BlackTerrorVenom + Quantity: 20 + - type: entity name: Guardian terror @@ -886,6 +1096,8 @@ id: MobGiantGuardianSpiderAngry suffix: terror spider components: + - type: TerrorSpiderWebBuffReceiver + - type: TerrorSpiderGuardianLeash - type: NpcFactionMember factions: - Xeno @@ -909,6 +1121,7 @@ parent: LightImplantSpider id: LightImplantSpiderGuardian name: Spawn web + noSpawn: true components: - type: SubdermalImplant implantAction: ActionSpawnSpiderGuardian @@ -917,21 +1130,34 @@ id: ActionSpawnSpiderGuardian name: Spawn web description: it spans the web. A little stronger than usual. + noSpawn: true components: - type: Action - useDelay: 3 + useDelay: 8 itemIconStyle: BigAction + icon: + sprite: Imperial/TerrorSpider/act.rsi + state: terror_shield + - type: InstantAction + event: !type:TerrorSpiderSpawnWebActionEvent + webPrototype: SpiderWebTerrorGuardianBarrier + +- type: entity + id: TerrorWeedsGuardianAction + name: guardian web + description: terror web + categories: [ HideSpawnMenu ] + parent: BaseAction + components: + - type: Action icon: sprite: Structures/floor_web.rsi state: full - - type: TargetAction - range: 1 - - type: WorldTargetAction - event: !type:WorldSpawnSpellEvent - prototypes: - - id: SpiderWebTerrorGuardian - amount: 1 - offset: 0, 1 + itemIconStyle: BigAction + useDelay: 2 + - type: InstantAction + event: !type:TerrorSpiderSpawnWebActionEvent + webPrototype: SpiderWebTerrorGuardian - type: entity name: Guardian terror @@ -939,6 +1165,12 @@ id: MobGuardianSpider description: Protects queens/princesses, safety, sacrificing his life if necessary. The most loyal servant and protector. components: + - type: TerrorSpiderGuardianShieldBarrier + - type: TerrorSpiderWebBuffReceiver + - type: TerrorSpiderGuardianLeash + - type: AutoImplant + implants: + - LightImplantSpiderGuardian - type: Sprite drawdepth: Mobs layers: @@ -979,7 +1211,8 @@ path: /Audio/Effects/bite.ogg damage: types: - Piercing: 25 + Piercing: 20 + Structural: 70 - type: SolutionContainerManager solutions: melee: @@ -1000,8 +1233,9 @@ interactSuccessSpawn: EffectHearts - type: NoSlip - type: Spider - webPrototype: SpiderWebTerrorUwU - webAction: TerrorWeedsAction + webPrototype: SpiderWebTerrorGuardian + webAction: TerrorWeedsGuardianAction + spawnsWebsAsNonPlayer: false - type: IgnoreSpiderWeb - type: Bloodstream bloodMaxVolume: 0 @@ -1021,19 +1255,14 @@ - type: PassiveDamage allowedStates: - Alive - damageCap: 379 + damageCap: 250 damage: - types: - Poison: -1 groups: - Brute: -1 - Burn: -1 - - type: Armor - modifiers: - coefficients: - Blunt: 0.6 - Slash: 0.6 - Piercing: 0.6 + Brute: -2 + Burn: -2 + - type: TerrorSpiderArmor + bruteModifier: 0.75 + burnModifier: 1.25 - type: entity name: Mother terror @@ -1041,6 +1270,8 @@ id: MobGiantMotherSpiderAngry suffix: terror spider components: + - type: TerrorSpiderWebBuffReceiver + - type: TerrorSpiderMother - type: NpcFactionMember factions: - Xeno @@ -1056,9 +1287,6 @@ raffle: settings: short - type: GhostTakeoverAvailable - - type: AutoImplant - implants: - - LightImplantSpiderRusar - type: entity name: Mother terror @@ -1066,6 +1294,13 @@ id: MobMotherSpider description: The massive one heals the spiders with his aura, and also massively destroys the crew with it. He fights with other spiders on the front line, supporting them. components: + - type: TerrorSpiderWebBuffReceiver + - type: TerrorSpiderMother + auraHalfRange: 7.5 + auraHealAmount: 3 + auraDamageAmount: 3 + auraInterval: 2 + touchHealAmount: 2 - type: Sprite drawdepth: Mobs layers: @@ -1106,7 +1341,8 @@ path: /Audio/Effects/bite.ogg damage: types: - Piercing: 10 + Piercing: 15 + Structural: 40 - type: SolutionContainerManager solutions: melee: @@ -1126,9 +1362,6 @@ interactFailureString: petting-failure-generic interactSuccessSpawn: EffectHearts - type: NoSlip - - type: Spider - webPrototype: SpiderWebTerrorUwU - webAction: TerrorWeedsAction - type: IgnoreSpiderWeb - type: Bloodstream bloodMaxVolume: 0 @@ -1140,21 +1373,19 @@ - DoorBumpOpener - FootstepSound - type: MovementSpeedModifier - baseWalkSpeed : 4 - baseSprintSpeed : 4 + baseWalkSpeed : 4.4 + baseSprintSpeed : 4.4 - type: SlowOnDamage speedModifierThresholds: 200: 0.7 - type: PassiveDamage allowedStates: - Alive - damageCap: 219 + damageCap: 220 damage: - types: - Poison: -3 groups: - Brute: -3 - Burn: -3 + Brute: -2 + Burn: -2 - type: RechargeBasicEntityAmmo rechargeCooldown: 4 - type: BasicEntityAmmoProvider @@ -1168,12 +1399,9 @@ availableModes: - FullAuto soundGunshot: /Audio/Weapons/Xeno/alien_spitacid.ogg - - type: Armor - modifiers: - coefficients: - Blunt: 0.75 - Slash: 0.75 - Piercing: 0.75 + - type: TerrorSpiderArmor + bruteModifier: 0.75 + burnModifier: 1.25 - type: entity @@ -1182,6 +1410,8 @@ id: MobGiantPrinceSpiderAngry suffix: terror spider components: + - type: TerrorSpiderWebBuffReceiver + - type: TerrorSpiderRoyal - type: NpcFactionMember factions: - Xeno @@ -1204,6 +1434,14 @@ id: MobPrinceSpider description: Destroys all living and inanimate things that get in his way. components: + - type: TerrorSpiderWebBuffReceiver + - type: TerrorSpiderRoyal + stompRadius: 2.5 + stompDamage: 20 + stompDamageType: Blunt + stompSlowDuration: 10 + stompSlowMultiplier: 0.5 + stompSlowStatusEffect: TerrorSpiderRoyalStompSlowStatusEffect - type: Sprite drawdepth: Mobs layers: @@ -1236,7 +1474,7 @@ - type: MobThresholds thresholds: 0: Alive - 400: Dead + 600: Dead - type: MeleeWeapon angle: 0 animation: WeaponArcBite @@ -1244,7 +1482,8 @@ path: /Audio/Effects/bite.ogg damage: types: - Piercing: 25 + Piercing: 40 + Structural: 100 - type: SolutionContainerManager solutions: melee: @@ -1264,9 +1503,6 @@ interactFailureString: petting-failure-generic interactSuccessSpawn: EffectHearts - type: NoSlip - - type: Spider - webPrototype: SpiderWebTerrorUwU - webAction: TerrorWeedsAction - type: IgnoreSpiderWeb - type: Bloodstream bloodMaxVolume: 0 @@ -1278,17 +1514,14 @@ - DoorBumpOpener - FootstepSound - type: MovementSpeedModifier - baseWalkSpeed : 4 - baseSprintSpeed : 4 + baseWalkSpeed : 4.4 + baseSprintSpeed : 4.4 - type: SlowOnDamage speedModifierThresholds: - 500: 0.7 - - type: Armor - modifiers: - coefficients: - Blunt: 0.5 - Slash: 0.5 - Piercing: 0.5 + 450: 0.7 + - type: TerrorSpiderArmor + bruteModifier: 0.3 + burnModifier: 0.6 - type: entity name: Princess terror @@ -1296,6 +1529,9 @@ id: MobGiantPrincessSpiderAngry suffix: terror spider components: + - type: TerrorSpiderWebBuffReceiver + - type: TerrorSpiderPrincess + - type: TerrorSpiderRoyal - type: NpcFactionMember factions: - Xeno @@ -1311,16 +1547,12 @@ raffle: settings: short - type: GhostTakeoverAvailable - - type: AutoImplant - implants: - - LightImplantSpiderTir1 - - LightImplantSpiderTir2 - - LightImplantSpiderPrincess - type: entity parent: LightImplantSpider id: LightImplantSpiderPrincess name: Spawnd web + noSpawn: true components: - type: SubdermalImplant implantAction: ActionSpawnSpiderPrincess @@ -1329,26 +1561,23 @@ id: ActionSpawnSpiderPrincess name: Spawn web description: it spans the web. A little stronger than usual. + noSpawn: true components: - type: Action useDelay: 2 itemIconStyle: BigAction icon: - sprite: Structures/floor_web.rsi - state: full - - type: TargetAction - range: 1 - - type: WorldTargetAction - event: !type:WorldSpawnSpellEvent - prototypes: - - id: SpiderWebPrincess - amount: 1 - offset: 0, 1 + sprite: Imperial/TerrorSpider/act.rsi + state: terror_shield + - type: InstantAction + event: !type:TerrorSpiderSpawnWebActionEvent + webPrototype: SpiderWebPrincess - type: entity - parent: LightImplantSpider + parent: LightImplantSpider id: LightImplantSpiderTir1 name: Spawnd egg 1 tir + noSpawn: true components: - type: SubdermalImplant implantAction: ActionSpawnTerrorEgg1Tir @@ -1357,26 +1586,27 @@ id: ActionSpawnTerrorEgg1Tir name: Spawnd egg 1 tir description: Lays an egg + noSpawn: true components: - - type: Action + - type: WorldTargetAction useDelay: 160 + range: 1 itemIconStyle: BigAction icon: sprite: Imperial/TerrorSpider/Spider/actions_animal.rsi state: lay_eggs - - type: TargetAction - range: 1 - - type: WorldTargetAction event: !type:WorldSpawnSpellEvent prototypes: - id: TerrorEgg1Tir amount: 1 offset: 0, 1 + speech: action-speech-spell-spider - type: entity - parent: LightImplantSpider + parent: LightImplantSpider id: LightImplantSpiderTir2 name: Spawnd egg 2 tir + noSpawn: true components: - type: SubdermalImplant implantAction: ActionSpawnTerrorEgg2Tir @@ -1385,21 +1615,21 @@ id: ActionSpawnTerrorEgg2Tir name: Spawnd egg 2 tir description: Lays an egg + noSpawn: true components: - - type: Action + - type: WorldTargetAction useDelay: 300 + range: 1 itemIconStyle: BigAction icon: sprite: Imperial/TerrorSpider/Spider/actions_animal.rsi state: lay_eggs - - type: TargetAction - range: 1 - - type: WorldTargetAction event: !type:WorldSpawnSpellEvent prototypes: - id: TerrorEgg2Tir amount: 1 offset: 0, 1 + speech: action-speech-spell-spider - type: entity name: Princess terror @@ -1407,6 +1637,20 @@ id: MobPrincessSpider description: Creates a lair, lays eggs, and tries to keep out of sight of the crew. components: + - type: TerrorSpiderWebBuffReceiver + - type: TerrorSpiderPrincess + screamRange: 6.5 + screamSlowDuration: 10 + screamSlowMultiplier: 0.5 + screamStaminaDamage: 30 + screamRobotDisableSeconds: 12 + maxBroodOnMap: 20 + maxEliteBroodOnMap: 3 + orphanDamagePerTick: 6 + orphanDamageInterval: 2 + remoteViewImmobileStatusEffect: TerrorSpiderPrincessRemoteViewImmobileStatusEffect + - type: TerrorSpiderRoyal + stompEnabled: false - type: Sprite drawdepth: Mobs layers: @@ -1469,8 +1713,9 @@ interactSuccessSpawn: EffectHearts - type: NoSlip - type: Spider - webPrototype: SpiderWebTerrorUwU - webAction: TerrorWeedsAction + webPrototype: SpiderWebPrincess + webAction: TerrorWeedsPrincessAction + spawnsWebsAsNonPlayer: false - type: IgnoreSpiderWeb - type: Bloodstream bloodMaxVolume: 0 @@ -1488,13 +1733,13 @@ speedModifierThresholds: 180: 0.7 - type: RechargeBasicEntityAmmo - rechargeCooldown: 0.75 + rechargeCooldown: 3 - type: BasicEntityAmmoProvider - proto: TerrorDrontAcid + proto: TerrorPrincessSpit capacity: 1 count: 1 - type: Gun - fireRate: 0.75 + fireRate: 1 useKey: false selectedMode: FullAuto availableModes: @@ -1503,31 +1748,42 @@ - type: PassiveDamage allowedStates: - Alive - damageCap: 179 + damageCap: 200 damage: - types: - Poison: -1.5 groups: - Brute: -1.5 - Burn: -1.5 - - type: Armor - modifiers: - coefficients: - Blunt: 0.7 - Slash: 0.7 - Piercing: 0.7 + Brute: -3 + Burn: -3 + - type: TerrorSpiderArmor + bruteModifier: 0.7 + burnModifier: 1.1 +- type: entity + id: TerrorPrincessSpit + name: Princess spit + parent: BaseBullet + categories: [ HideSpawnMenu ] + components: + - type: Projectile + damage: + types: + Heat: 25 + - type: Sprite + sprite: Objects/Weapons/Guns/Projectiles/xeno_toxic.rsi + layers: + - state: xeno_toxic + - type: Ammo + muzzleFlash: null + - type: entity id: TerrorLayEgg - name: Spawnd egg terror - description: яички + name: Lay egg + description: Lay a terror spider egg. categories: [ HideSpawnMenu ] components: - - type: Action + - type: InstantAction icon: { sprite: Imperial/TerrorSpider/Spider/actions_animal.rsi, state: lay_eggs } useDelay: 160 - - type: InstantAction event: !type:EggLayInstantActionEvent - type: entity @@ -1536,6 +1792,9 @@ id: MobGiantQueenSpiderAngry suffix: terror spider components: + - type: TerrorSpiderWebBuffReceiver + - type: TerrorSpiderQueen + - type: TerrorSpiderRoyal - type: NpcFactionMember factions: - Xeno @@ -1551,17 +1810,12 @@ raffle: settings: short - type: GhostTakeoverAvailable - - type: AutoImplant - implants: - - LightImplantSpiderTir3 - - LightImplantSpiderTir2Queen - - LightImplantSpiderTir1Queen - - LightImplantSpiderPrincess - type: entity - parent: LightImplantSpider + parent: LightImplantSpider id: LightImplantSpiderTir2Queen name: Spawnd egg 2 tir + noSpawn: true components: - type: SubdermalImplant implantAction: ActionSpawnTerrorEgg2TirQueen @@ -1570,26 +1824,27 @@ id: ActionSpawnTerrorEgg2TirQueen name: Spawnd egg 2 tir description: Lays an egg + noSpawn: true components: - - type: Action + - type: WorldTargetAction useDelay: 400 + range: 1 itemIconStyle: BigAction icon: sprite: Imperial/TerrorSpider/Spider/actions_animal.rsi state: lay_eggs - - type: TargetAction - range: 1 - - type: WorldTargetAction event: !type:WorldSpawnSpellEvent prototypes: - id: TerrorEgg2Tir amount: 1 offset: 0, 1 + speech: action-speech-spell-spider - type: entity - parent: LightImplantSpider + parent: LightImplantSpider id: LightImplantSpiderTir1Queen name: Spawnd egg 1 tir + noSpawn: true components: - type: SubdermalImplant implantAction: ActionSpawnTerrorEgg1TirQueen @@ -1598,27 +1853,27 @@ id: ActionSpawnTerrorEgg1TirQueen name: Spawnd egg 1 tir description: Lays an egg + noSpawn: true components: - - type: Action + - type: WorldTargetAction useDelay: 220 + range: 1 itemIconStyle: BigAction icon: sprite: Imperial/TerrorSpider/Spider/actions_animal.rsi state: lay_eggs - - type: TargetAction - range: 1 - - type: WorldTargetAction event: !type:WorldSpawnSpellEvent prototypes: - id: TerrorEgg1Tir amount: 1 offset: 0, 1 - + speech: action-speech-spell-spider - type: entity - parent: LightImplantSpider + parent: LightImplantSpider id: LightImplantSpiderTir3 name: Spawnd egg 3 tir + noSpawn: true components: - type: SubdermalImplant implantAction: ActionSpawnTerrorEgg3Tir @@ -1627,21 +1882,21 @@ id: ActionSpawnTerrorEgg3Tir name: Spawnd egg 3 tir description: Lays an egg + noSpawn: true components: - - type: Action + - type: WorldTargetAction useDelay: 1600 + range: 1 itemIconStyle: BigAction icon: sprite: Imperial/TerrorSpider/Spider/actions_animal.rsi state: lay_eggs - - type: TargetAction - range: 1 - - type: WorldTargetAction event: !type:WorldSpawnSpellEvent prototypes: - id: TerrorEgg3Tir amount: 1 offset: 0, 1 + speech: action-speech-spell-spider - type: entity name: Queen terror @@ -1649,6 +1904,23 @@ id: MobQueenSpider description: Creates a lair, mass-produces spiders, and fights spiders on the front lines using his dangerous spitting and screaming. components: + - type: TerrorSpiderWebBuffReceiver + - type: TerrorSpiderQueen + hiveSpeedMultiplier: 0.5 + hiveStructuralDamage: 400 + hiveSlowStatusEffect: TerrorSpiderQueenHiveSlowStatusEffect + remoteViewImmobileStatusEffect: TerrorSpiderMotherRemoteViewImmobileStatusEffect + screamRange: 6.5 + screamSlowDuration: 14 + screamSlowMultiplier: 0.5 + screamStaminaDamage: 50 + screamRobotDisableSeconds: 16 + lightBreakHalfRange: 8.5 + royalEggCooldownSeconds: 1500 + orphanDamagePerTick: 6 + orphanDamageInterval: 2 + - type: TerrorSpiderRoyal + stompEnabled: false - type: Sprite drawdepth: Mobs layers: @@ -1690,7 +1962,7 @@ damage: types: Piercing: 25 - Structural: 50 + Structural: 100 - type: SolutionContainerManager solutions: melee: @@ -1711,8 +1983,9 @@ interactSuccessSpawn: EffectHearts - type: NoSlip - type: Spider - webPrototype: SpiderWebTerrorUwU - webAction: TerrorWeedsAction + webPrototype: SpiderWebPrincess + webAction: TerrorWeedsQueenAction + spawnsWebsAsNonPlayer: false - type: IgnoreSpiderWeb - type: Bloodstream bloodMaxVolume: 0 @@ -1730,13 +2003,13 @@ speedModifierThresholds: 320: 0.7 - type: RechargeBasicEntityAmmo - rechargeCooldown: 0.75 + rechargeCooldown: 2 - type: BasicEntityAmmoProvider proto: TerrorQueentAcid capacity: 1 count: 1 - type: Gun - fireRate: 0.75 + fireRate: 1 useKey: false selectedMode: FullAuto availableModes: @@ -1745,19 +2018,14 @@ - type: PassiveDamage allowedStates: - Alive - damageCap: 174 + damageCap: 340 damage: - types: - Poison: -1.5 groups: - Brute: -1.5 - Burn: -1.5 - - type: Armor - modifiers: - coefficients: - Blunt: 0.7 - Slash: 0.7 - Piercing: 0.7 + Brute: -3 + Burn: -3 + - type: TerrorSpiderArmor + bruteModifier: 0.7 + burnModifier: 1.1 - type: entity @@ -1766,10 +2034,14 @@ parent: BaseBullet categories: [ HideSpawnMenu ] components: + - type: TerrorDronProjectileDebuff + slowDuration: 0 + slowMultiplier: 1 + staminaDamage: 20 - type: Projectile damage: types: - Caustic: 40 + Heat: 40 - type: Sprite sprite: Objects/Weapons/Guns/Projectiles/xeno_toxic.rsi layers: @@ -1793,8 +2065,8 @@ Acidic: [Touch, Ingestion] - type: Internals - type: MovementSpeedModifier - baseWalkSpeed : 4 - baseSprintSpeed : 4 + baseWalkSpeed : 16 + baseSprintSpeed : 16 - type: StatusEffects allowed: - SlowedDown @@ -1880,16 +2152,12 @@ 301: 0.8 295: 0.6 285: 0.4 - - type: Armor - modifiers: - coefficients: - Blunt: 0.8 - Slash: 0.8 - Piercing: 0.8 + - type: TerrorSpiderArmor + bruteModifier: 0.8 - type: UseDelay delay: 4 - type: ShowHealthBars - + - type: entity save: false @@ -1904,6 +2172,9 @@ id: MobGiantHealerSpiderAngry suffix: terror spider components: + - type: TerrorSpiderCocoon + - type: TerrorSpiderWebBuffReceiver + - type: TerrorSpiderHealer - type: NpcFactionMember factions: - Xeno @@ -1919,9 +2190,6 @@ raffle: settings: short - type: GhostTakeoverAvailable - - type: AutoImplant - implants: - - LightImplantSpiderDron - type: entity name: Healer terror @@ -1929,6 +2197,16 @@ id: MobHealerSpider description: It lays eggs and provides treatment support to other spiders, trying to stay away. components: + - type: TerrorSpiderCocoon + - type: TerrorSpiderWebBuffReceiver + - type: TerrorSpiderHealer + pulseHealAmount: 20 + pulseRange: 13 + eggRequiredSatiety: 3 + cocoonSatietyGain: 1 + touchHealLevel1: 2 + touchHealLevel2: 4 + touchHealLevel3: 8 - type: Sprite drawdepth: Mobs layers: @@ -1969,7 +2247,8 @@ path: /Audio/Effects/bite.ogg damage: types: - Piercing: 15 + Piercing: 10 + Structural: 40 - type: SolutionContainerManager solutions: melee: @@ -1984,8 +2263,9 @@ interactSuccessSpawn: EffectHearts - type: NoSlip - type: Spider - webPrototype: SpiderWebTerrorUwU - webAction: TerrorWeedsAction + webPrototype: SpiderWebTerrorHealer + webAction: TerrorWeedsHealerAction + spawnsWebsAsNonPlayer: false - type: IgnoreSpiderWeb - type: Bloodstream bloodMaxVolume: 0 @@ -2005,7 +2285,7 @@ - type: PassiveDamage allowedStates: - Alive - damageCap: 79 + damageCap: 100 damage: types: Poison: -1 @@ -2025,12 +2305,9 @@ availableModes: - FullAuto soundGunshot: /Audio/Weapons/Xeno/alien_spitacid.ogg - - type: Armor - modifiers: - coefficients: - Blunt: 0.75 - Slash: 0.75 - Piercing: 0.75 + - type: TerrorSpiderArmor + bruteModifier: 0.75 + burnModifier: 1.25 - type: entity id: ProjectileHealingBoltTerror @@ -2053,11 +2330,50 @@ Toxin: -45 ignoreResistances: true +- type: entity + id: TerrorSpiderCocoon + name: cocoon + description: Wrapped victim of terror spiders. + categories: [ HideSpawnMenu ] + components: + - type: Sprite + sprite: Imperial/TerrorSpider/effects.rsi + layers: + - state: cocoon_large3 + - type: Physics + - type: Fixtures + fixtures: + fix1: + hard: false + density: 7 + shape: + !type:PhysShapeAabb + bounds: "-0.45,-0.45,0.45,0.45" + layer: + - MidImpassable + - type: Damageable + damageModifierSet: Wood + - type: Destructible + thresholds: + - trigger: + !type:DamageTrigger + damage: 45 + behaviors: + - !type:DoActsBehavior + acts: [ "Destruction" ] + - type: ContainerContainer + containers: + cocoon-body: !type:Container + - type: TerrorSpiderCocoonPrison + - type: entity id: SpiderWebTerrorRusar - parent: SpiderWebBase name: Web terror spider description: TERROR! + placement: + mode: SnapgridCenter + snap: + - Wall components: - type: MeleeSound soundGroups: @@ -2065,7 +2381,16 @@ path: "/Audio/Weapons/slash.ogg" - type: Sprite - color: "#ffffffdd" + sprite: Structures/floor_web.rsi + layers: + - state: full + map: ["spiderWebLayer"] + drawdepth: WallMountedItems + - type: Appearance + - type: Clickable + - type: Transform + anchored: true + - type: Physics - type: Fixtures fixtures: fix1: @@ -2076,11 +2401,13 @@ bounds: "-0.5,-0.5,0.5,0.5" layer: - MidImpassable + - type: Damageable + damageModifierSet: Wood - type: Destructible thresholds: - trigger: !type:DamageTrigger - damage: 60 + damage: 30 behaviors: - !type:DoActsBehavior acts: [ "Destruction" ] @@ -2100,6 +2427,8 @@ Flammable: [Touch] Extinguish: [Touch] - type: SpiderWebObject + - type: TerrorSpiderWebBuffArea + - type: TerrorSpiderWidowWebArea - type: SpeedModifierContacts walkSpeedModifier: 0.5 sprintSpeedModifier: 0.5 @@ -2113,7 +2442,6 @@ - type: entity id: SpiderWebTerrorDron name: Web terror spider - parent: SpiderWebBase description: TERROR! placement: mode: SnapgridCenter @@ -2126,7 +2454,16 @@ path: "/Audio/Weapons/slash.ogg" - type: Sprite - color: "#ffffffdd" + sprite: Structures/floor_web.rsi + layers: + - state: full + map: ["spiderWebLayer"] + drawdepth: WallMountedItems + - type: Appearance + - type: Clickable + - type: Transform + anchored: true + - type: Physics - type: Fixtures fixtures: fix1: @@ -2137,11 +2474,14 @@ bounds: "-0.5,-0.5,0.5,0.5" layer: - MidImpassable + - type: Occluder + - type: Damageable + damageModifierSet: Wood - type: Destructible thresholds: - trigger: !type:DamageTrigger - damage: 70 + damage: 35 behaviors: - !type:DoActsBehavior acts: [ "Destruction" ] @@ -2161,6 +2501,9 @@ Flammable: [Touch] Extinguish: [Touch] - type: SpiderWebObject + - type: TerrorSpiderWebBuffArea + - type: TerrorSpiderHealerBlindWeb + - type: Airtight - type: SpeedModifierContacts walkSpeedModifier: 0.5 sprintSpeedModifier: 0.5 @@ -2170,8 +2513,23 @@ - type: Slippery - type: StepTrigger intersectRatio: 0.2 - - type: Airtight - noAirWhenFullyAirBlocked: false + +- type: reagent + id: FrostOilTerrorSpider + name: Frost oil (terror spider) + group: Toxins + desc: Cools the victim's body. + physicalDesc: reagent-physical-desc-oily + flavor: cold + color: "#8ad8ff" + metabolisms: + Poison: + effects: + - !type:AdjustTemperature + conditions: + - !type:TemperatureCondition + min: 160.15 + amount: -10000 - type: entity name: Spit terror @@ -2184,8 +2542,8 @@ capacity: 1 count: 1 - type: RechargeBasicEntityAmmo - rechargeCooldown: 2 - rechargeSound: /Audio/Animals/cat_hiss.ogg + rechargeCooldown: 2 + rechargeSound: /Audio/Animals/cat_hiss.ogg - type: entity id: TerrorSpitBullet @@ -2275,7 +2633,7 @@ revertOnCrit: false revertOnDeath: false allowRepeatedMorphs: true - + - type: polymorph id: EggToMobDronSpiderAngrys configuration: @@ -2443,192 +2801,1657 @@ name: web description: terror web categories: [ HideSpawnMenu ] + parent: BaseAction components: - type: Action + icon: + sprite: Structures/floor_web.rsi + state: full + itemIconStyle: BigAction useDelay: 8 - icon: Imperial/TerrorSpider/Spider/actions_animal.rsi/lay_web.png - type: InstantAction - event: !type:SpiderWebActionEvent + event: !type:TerrorSpiderSpawnWebActionEvent + webPrototype: SpiderWebTerrorUwU - type: entity - id: SpiderWebTerrorGuardian - name: web terror spider - parent: SpiderWebBase - description: TERROR! - placement: - mode: SnapgridCenter - snap: - - Wall + id: TerrorWeedsQueenAction + name: queen web + description: terror web + categories: [ HideSpawnMenu ] + parent: BaseAction components: - - type: MeleeSound - soundGroups: - Brute: - path: - "/Audio/Weapons/slash.ogg" - - type: Sprite - color: "#ffffffdd" - - type: Fixtures - fixtures: - fix1: - hard: false - density: 7 - shape: - !type:PhysShapeAabb - bounds: "-0.5,-0.5,0.5,0.5" - layer: - - MidImpassable - - type: Destructible - thresholds: - - trigger: - !type:DamageTrigger - damage: 60 - behaviors: - - !type:DoActsBehavior - acts: [ "Destruction" ] - - !type:SpawnEntitiesBehavior - spawn: - MaterialWebSilk: - min: 0 - max: 1 - - type: Temperature - heatDamage: - types: - Heat: 80 - coldDamage: {} - coldDamageThreshold: 0 - - type: Flammable - fireSpread: true - damage: - types: - Heat: 80 - - type: Reactive - groups: - Flammable: [Touch] - Extinguish: [Touch] - - type: SpiderWebObject - - type: SpeedModifierContacts - walkSpeedModifier: 0.5 - sprintSpeedModifier: 0.5 - ignoreWhitelist: - components: - - IgnoreSpiderWeb - - type: DamageContacts - damage: - types: - Poison: 4 - Piercing: 4 - ignoreWhitelist: - components: - - IgnoreSpiderWeb - - type: TimedDespawn - lifetime: 15 - - type: Slippery - - type: StepTrigger - intersectRatio: 0.2 + - type: Action + icon: + sprite: Structures/floor_web.rsi + state: full + itemIconStyle: BigAction + useDelay: 1.5 + - type: InstantAction + event: !type:TerrorSpiderSpawnWebActionEvent + webPrototype: SpiderWebPrincess - type: entity - id: SpiderWebPrincess - name: web terror spider - parent: SpiderWebBase - description: TERROR! - placement: - mode: SnapgridCenter - snap: - - Wall + id: ActionTerrorSpiderMotherRemoteViewNext + name: Remote next + description: Switch view to next spider. + categories: [ HideSpawnMenu ] + parent: BaseAction components: - - type: MeleeSound - soundGroups: - Brute: - path: - "/Audio/Weapons/slash.ogg" - - type: Sprite - color: "#ffffffdd" - - type: Fixtures - fixtures: - fix1: - hard: false - density: 7 - shape: - !type:PhysShapeAabb - bounds: "-0.5,-0.5,0.5,0.5" - layer: - - MidImpassable - - type: Damageable - damageModifierSet: Wood - - type: Destructible - thresholds: - - trigger: - !type:DamageTrigger - damage: 60 - behaviors: - - !type:DoActsBehavior - acts: [ "Destruction" ] - - type: Temperature - heatDamage: - types: - Heat: 60 - coldDamage: {} - coldDamageThreshold: 0 - - type: Flammable - fireSpread: true - damage: - types: - Heat: 60 - - type: Reactive - groups: - Flammable: [Touch] - Extinguish: [Touch] - - type: SpiderWebObject - - type: SpeedModifierContacts - walkSpeedModifier: 0.5 - sprintSpeedModifier: 0.5 - ignoreWhitelist: - components: - - IgnoreSpiderWeb - - type: Slippery - - type: StepTrigger - intersectRatio: 0.2 - - type: Airtight - noAirWhenFullyAirBlocked: false + - type: Action + itemIconStyle: BigAction + icon: + sprite: Imperial/TerrorSpider/act.rsi + state: show + useDelay: 1 + - type: InstantAction + event: !type:TerrorSpiderMotherRemoteViewNextActionEvent - type: entity - abstract: true - name: Xeno - id: KsenosXenoBase - parent: MobXeno - suffix: KsenosXeno + id: ActionTerrorSpiderMotherRemoteViewPrevious + name: Remote previous + description: Switch view to previous spider. + categories: [ HideSpawnMenu ] + parent: BaseAction components: - - type: LagCompensation - - type: Input - context: "human" - - type: MovedByPressure - - type: DamageOnHighSpeedImpact - damage: - types: - Blunt: 5 - soundHit: - path: /Audio/Effects/hit_kick.ogg - - type: Sprite - drawdepth: Mobs - sprite: Mobs/Aliens/Xenos/burrower.rsi - layers: - - map: ["enum.DamageStateVisualLayers.Base"] - state: running - noRot: true - netsync: false - - type: Clickable - - type: InteractionOutline - - type: SolutionContainerManager - - type: AtmosExposed - - type: MobThresholds - thresholds: - 0: Alive - 50: Critical - 300: Dead - - type: Internals - - type: Damageable - damageContainer: Biological - damageModifierSet: Slime + - type: Action + itemIconStyle: BigAction + icon: + sprite: Imperial/TerrorSpider/act.rsi + state: show2 + useDelay: 1 + - type: InstantAction + event: !type:TerrorSpiderMotherRemoteViewPreviousActionEvent + +- type: entity + id: ActionTerrorSpiderMotherRemoteViewExit + name: Remote exit + description: Return your view. + categories: [ HideSpawnMenu ] + parent: BaseAction + components: + - type: Action + itemIconStyle: BigAction + icon: + sprite: Imperial/TerrorSpider/act.rsi + state: pause + useDelay: 1 + - type: InstantAction + event: !type:TerrorSpiderMotherRemoteViewExitActionEvent + +- type: entity + id: ActionTerrorSpiderMotherPulse + name: Mother heal pulse + description: Heal nearby terror spiders. + categories: [ HideSpawnMenu ] + parent: BaseAction + components: + - type: Action + itemIconStyle: BigAction + icon: + sprite: Imperial/TerrorSpider/act.rsi + state: heal + useDelay: 30 + - type: InstantAction + event: !type:TerrorSpiderMotherPulseActionEvent + +- type: entity + id: ActionTerrorSpiderMotherLayJelly + name: Lay jelly + description: Lay regeneration jelly. + categories: [ HideSpawnMenu ] + parent: BaseAction + components: + - type: Action + itemIconStyle: BigAction + icon: + sprite: Imperial/TerrorSpider/act.rsi + state: spiderjelly + useDelay: 15 + - type: InstantAction + event: !type:TerrorSpiderMotherLayJellyActionEvent + +- type: entity + id: TerrorSpiderMotherJelly + name: spider jelly + categories: [ HideSpawnMenu ] + components: + - type: Sprite + sprite: Imperial/TerrorSpider/act.rsi + layers: + - state: spiderjelly + - type: Transform + anchored: true + - type: Clickable + - type: Physics + - type: Fixtures + fixtures: + fix1: + hard: false + density: 1 + shape: + !type:PhysShapeAabb + bounds: "-0.3,-0.3,0.3,0.3" + layer: + - MidImpassable + - type: TimedDespawn + lifetime: 6 + - type: Damageable + - type: Destructible + thresholds: + - trigger: + !type:DamageTrigger + damage: 1 + behaviors: + - !type:DoActsBehavior + acts: [ "Destruction" ] + - type: TerrorSpiderMotherJelly + +- type: entity + id: ActionTerrorSpiderQueenCreateHive + name: Create hive + description: Create hive mode and unlock egg laying. + categories: [ HideSpawnMenu ] + parent: BaseAction + components: + - type: Action + itemIconStyle: BigAction + icon: + sprite: Imperial/TerrorSpider/Spider/queen.rsi + state: terror_queen + useDelay: 1 + - type: InstantAction + event: !type:TerrorSpiderQueenCreateHiveActionEvent + +- type: entity + id: ActionTerrorSpiderQueenScream + name: Queen scream + description: Slow enemies and disable robots. + categories: [ HideSpawnMenu ] + parent: BaseAction + components: + - type: Action + itemIconStyle: BigAction + icon: + sprite: Imperial/TerrorSpider/act.rsi + state: terror_shriek + useDelay: 45 + - type: InstantAction + event: !type:TerrorSpiderQueenScreamActionEvent + +- type: entity + id: ActionTerrorSpiderQueenHiveCount + name: Hive count + description: Show your brood status. + categories: [ HideSpawnMenu ] + parent: BaseAction + components: + - type: Action + itemIconStyle: BigAction + icon: + sprite: Imperial/TerrorSpider/act.rsi + state: mindswap + useDelay: 5 + - type: InstantAction + event: !type:TerrorSpiderQueenHiveCountActionEvent + +- type: entity + id: ActionTerrorSpiderQueenLayEggRusar + name: Lay Rusar egg + description: Lay a tier-1 Rusar egg. + categories: [ HideSpawnMenu ] + parent: BaseAction + components: + - type: Action + itemIconStyle: BigAction + icon: + sprite: Imperial/TerrorSpider/Spider/rusar.rsi + state: terror_red + useDelay: 160 + - type: InstantAction + event: !type:TerrorSpiderQueenLayEggActionEvent + eggPrototype: TerrorEggQueenRusar + +- type: entity + id: ActionTerrorSpiderQueenLayEggDron + name: Lay Dron egg + description: Lay a tier-1 Dron egg. + categories: [ HideSpawnMenu ] + parent: BaseAction + components: + - type: Action + itemIconStyle: BigAction + icon: + sprite: Imperial/TerrorSpider/Spider/dron.rsi + state: terror_widow + useDelay: 160 + - type: InstantAction + event: !type:TerrorSpiderQueenLayEggActionEvent + eggPrototype: TerrorEggQueenDron + +- type: entity + id: ActionTerrorSpiderQueenLayEggLurker + name: Lay Lurker egg + description: Lay a tier-1 Lurker egg. + categories: [ HideSpawnMenu ] + parent: BaseAction + components: + - type: Action + itemIconStyle: BigAction + icon: + sprite: Imperial/TerrorSpider/Spider/sogladatel.rsi + state: terror_gray_cloaked + useDelay: 160 + - type: InstantAction + event: !type:TerrorSpiderQueenLayEggActionEvent + eggPrototype: TerrorEggQueenLurker + +- type: entity + id: ActionTerrorSpiderQueenLayEggHealer + name: Lay Healer egg + description: Lay a tier-1 Healer egg. + categories: [ HideSpawnMenu ] + parent: BaseAction + components: + - type: Action + itemIconStyle: BigAction + icon: + sprite: Imperial/TerrorSpider/Spider/healer.rsi + state: terror_green + useDelay: 160 + - type: InstantAction + event: !type:TerrorSpiderQueenLayEggActionEvent + eggPrototype: TerrorEggQueenHealer + +- type: entity + id: ActionTerrorSpiderQueenLayEggReaper + name: Lay Reaper egg + description: Lay a tier-2 Reaper egg. + categories: [ HideSpawnMenu ] + parent: BaseAction + components: + - type: Action + itemIconStyle: BigAction + icon: + sprite: Imperial/TerrorSpider/Spider/reaper.rsi + state: terror_white + useDelay: 160 + - type: InstantAction + event: !type:TerrorSpiderQueenLayEggActionEvent + eggPrototype: TerrorEggQueenReaper + +- type: entity + id: ActionTerrorSpiderQueenLayEggWidow + name: Lay Widow egg + description: Lay a tier-2 Widow egg. + categories: [ HideSpawnMenu ] + parent: BaseAction + components: + - type: Action + itemIconStyle: BigAction + icon: + sprite: Imperial/TerrorSpider/Spider/widow.rsi + state: terror_widow + useDelay: 160 + - type: InstantAction + event: !type:TerrorSpiderQueenLayEggActionEvent + eggPrototype: TerrorEggQueenWidow + +- type: entity + id: ActionTerrorSpiderQueenLayEggGuardian + name: Lay Guardian egg + description: Lay a tier-2 Guardian egg. + categories: [ HideSpawnMenu ] + parent: BaseAction + components: + - type: Action + itemIconStyle: BigAction + icon: + sprite: Imperial/TerrorSpider/Spider/guardian.rsi + state: terror_purple + useDelay: 160 + - type: InstantAction + event: !type:TerrorSpiderQueenLayEggActionEvent + eggPrototype: TerrorEggQueenGuardian + +- type: entity + id: ActionTerrorSpiderQueenLayEggDestroyer + name: Lay Destroyer egg + description: Lay a tier-2 Destroyer egg. + categories: [ HideSpawnMenu ] + parent: BaseAction + components: + - type: Action + itemIconStyle: BigAction + icon: + sprite: Imperial/TerrorSpider/Spider/destroyer.rsi + state: terror_brown + useDelay: 160 + - type: InstantAction + event: !type:TerrorSpiderQueenLayEggActionEvent + eggPrototype: TerrorEggQueenDestroyer + +- type: entity + id: ActionTerrorSpiderQueenLayEggPrince + name: Lay Prince egg + description: Lay a tier-3 Prince egg. + categories: [ HideSpawnMenu ] + parent: BaseAction + components: + - type: Action + itemIconStyle: BigAction + icon: + sprite: Imperial/TerrorSpider/Spider/prince.rsi + state: terror_allblack + useDelay: 160 + - type: InstantAction + event: !type:TerrorSpiderQueenLayEggActionEvent + eggPrototype: TerrorEggQueenPrince + royalCooldownKey: Prince + +- type: entity + id: ActionTerrorSpiderQueenLayEggPrincess + name: Lay Princess egg + description: Lay a tier-3 Princess egg. + categories: [ HideSpawnMenu ] + parent: BaseAction + components: + - type: Action + itemIconStyle: BigAction + icon: + sprite: Imperial/TerrorSpider/Spider/princess.rsi + state: terror_princess1 + useDelay: 160 + - type: InstantAction + event: !type:TerrorSpiderQueenLayEggActionEvent + eggPrototype: TerrorEggQueenPrincess + royalCooldownKey: Princess + +- type: entity + id: ActionTerrorSpiderQueenLayEggMother + name: Lay Mother egg + description: Lay a tier-3 Mother egg. + categories: [ HideSpawnMenu ] + parent: BaseAction + components: + - type: Action + itemIconStyle: BigAction + icon: + sprite: Imperial/TerrorSpider/Spider/mother.rsi + state: terror_mother + useDelay: 160 + - type: InstantAction + event: !type:TerrorSpiderQueenLayEggActionEvent + eggPrototype: TerrorEggQueenMother + royalCooldownKey: Mother + +- type: entity + id: ActionTerrorSpiderKnightGuard + name: Guard + description: Defensive stance. + categories: [ HideSpawnMenu ] + parent: BaseAction + components: + - type: Action + icon: + sprite: Imperial/TerrorSpider/act.rsi + state: defence + itemIconStyle: BigAction + useDelay: 30 + - type: InstantAction + event: !type:TerrorSpiderKnightGuardActionEvent + +- type: entity + id: ActionTerrorSpiderDestroyerEmpScream + name: EMP scream + description: Emit EMP wave around you. + categories: [ HideSpawnMenu ] + parent: BaseAction + components: + - type: Action + icon: + sprite: Imperial/TerrorSpider/act.rsi + state: emp_new + itemIconStyle: BigAction + useDelay: 40 + - type: InstantAction + event: !type:TerrorSpiderDestroyerEmpScreamActionEvent + +- type: entity + id: ActionTerrorSpiderDestroyerFireBurst + name: Fire burst + description: Ignite everyone around you. + categories: [ HideSpawnMenu ] + parent: BaseAction + components: + - type: Action + icon: + sprite: Imperial/TerrorSpider/act.rsi + state: explosion_old + itemIconStyle: BigAction + useDelay: 25 + - type: InstantAction + event: !type:TerrorSpiderDestroyerFireBurstActionEvent + +- type: entity + id: ActionTerrorSpiderPrincessRemoteViewNext + name: Princess remote next + description: Switch view to next brood spider. + categories: [ HideSpawnMenu ] + parent: BaseAction + components: + - type: Action + itemIconStyle: BigAction + icon: + sprite: Imperial/TerrorSpider/act.rsi + state: show + useDelay: 1 + - type: InstantAction + event: !type:TerrorSpiderMotherRemoteViewNextActionEvent + +- type: entity + id: ActionTerrorSpiderPrincessRemoteViewPrevious + name: Princess remote previous + description: Switch view to previous brood spider. + categories: [ HideSpawnMenu ] + parent: BaseAction + components: + - type: Action + itemIconStyle: BigAction + icon: + sprite: Imperial/TerrorSpider/act.rsi + state: show2 + useDelay: 1 + - type: InstantAction + event: !type:TerrorSpiderMotherRemoteViewPreviousActionEvent + +- type: entity + id: ActionTerrorSpiderPrincessRemoteViewExit + name: Princess remote exit + description: Return your view. + categories: [ HideSpawnMenu ] + parent: BaseAction + components: + - type: Action + itemIconStyle: BigAction + icon: + sprite: Imperial/TerrorSpider/act.rsi + state: pause + useDelay: 1 + - type: InstantAction + event: !type:TerrorSpiderMotherRemoteViewExitActionEvent + +- type: entity + id: ActionTerrorSpiderPrincessHiveSense + name: Hive sense + description: Show brood status in chat. + categories: [ HideSpawnMenu ] + parent: BaseAction + components: + - type: Action + itemIconStyle: BigAction + icon: + sprite: Imperial/TerrorSpider/act.rsi + state: mindswap + useDelay: 5 + - type: InstantAction + event: !type:TerrorSpiderPrincessHiveSenseActionEvent + +- type: entity + id: ActionTerrorSpiderPrincessScream + name: Princess scream + description: Slow enemies and disable robots. + categories: [ HideSpawnMenu ] + parent: BaseAction + components: + - type: Action + itemIconStyle: BigAction + icon: + sprite: Imperial/TerrorSpider/act.rsi + state: terror_shriek + useDelay: 60 + - type: InstantAction + event: !type:TerrorSpiderPrincessScreamActionEvent + +- type: entity + id: ActionTerrorSpiderPrincessLayEggRusar + name: Lay Rusar egg + description: Lay a tier-1 Rusar egg. + categories: [ HideSpawnMenu ] + parent: BaseAction + components: + - type: Action + itemIconStyle: BigAction + icon: + sprite: Imperial/TerrorSpider/Spider/rusar.rsi + state: terror_red + useDelay: 160 + - type: InstantAction + event: !type:TerrorSpiderPrincessLayEggActionEvent + eggPrototype: TerrorEggPrincessRusar + +- type: entity + id: ActionTerrorSpiderPrincessLayEggDron + name: Lay Dron egg + description: Lay a tier-1 Dron egg. + categories: [ HideSpawnMenu ] + parent: BaseAction + components: + - type: Action + itemIconStyle: BigAction + icon: + sprite: Imperial/TerrorSpider/Spider/dron.rsi + state: terror_widow + useDelay: 160 + - type: InstantAction + event: !type:TerrorSpiderPrincessLayEggActionEvent + eggPrototype: TerrorEggPrincessDron + +- type: entity + id: ActionTerrorSpiderPrincessLayEggLurker + name: Lay Lurker egg + description: Lay a tier-1 Lurker egg. + categories: [ HideSpawnMenu ] + parent: BaseAction + components: + - type: Action + itemIconStyle: BigAction + icon: + sprite: Imperial/TerrorSpider/Spider/sogladatel.rsi + state: terror_gray_cloaked + useDelay: 160 + - type: InstantAction + event: !type:TerrorSpiderPrincessLayEggActionEvent + eggPrototype: TerrorEggPrincessLurker + +- type: entity + id: ActionTerrorSpiderPrincessLayEggHealer + name: Lay Healer egg + description: Lay a tier-1 Healer egg. + categories: [ HideSpawnMenu ] + parent: BaseAction + components: + - type: Action + itemIconStyle: BigAction + icon: + sprite: Imperial/TerrorSpider/Spider/healer.rsi + state: terror_green + useDelay: 160 + - type: InstantAction + event: !type:TerrorSpiderPrincessLayEggActionEvent + eggPrototype: TerrorEggPrincessHealer + +- type: entity + id: ActionTerrorSpiderPrincessLayEggReaper + name: Lay Reaper egg + description: Lay a tier-2 Reaper egg. + categories: [ HideSpawnMenu ] + parent: BaseAction + components: + - type: Action + itemIconStyle: BigAction + icon: + sprite: Imperial/TerrorSpider/Spider/reaper.rsi + state: terror_white + useDelay: 160 + - type: InstantAction + event: !type:TerrorSpiderPrincessLayEggActionEvent + eggPrototype: TerrorEggPrincessReaper + +- type: entity + id: ActionTerrorSpiderPrincessLayEggWidow + name: Lay Widow egg + description: Lay a tier-2 Widow egg. + categories: [ HideSpawnMenu ] + parent: BaseAction + components: + - type: Action + itemIconStyle: BigAction + icon: + sprite: Imperial/TerrorSpider/Spider/widow.rsi + state: terror_widow + useDelay: 160 + - type: InstantAction + event: !type:TerrorSpiderPrincessLayEggActionEvent + eggPrototype: TerrorEggPrincessWidow + +- type: entity + id: ActionTerrorSpiderPrincessLayEggGuardian + name: Lay Guardian egg + description: Lay a tier-2 Guardian egg. + categories: [ HideSpawnMenu ] + parent: BaseAction + components: + - type: Action + itemIconStyle: BigAction + icon: + sprite: Imperial/TerrorSpider/Spider/guardian.rsi + state: terror_purple + useDelay: 160 + - type: InstantAction + event: !type:TerrorSpiderPrincessLayEggActionEvent + eggPrototype: TerrorEggPrincessGuardian + +- type: entity + id: ActionTerrorSpiderPrincessLayEggDestroyer + name: Lay Destroyer egg + description: Lay a tier-2 Destroyer egg. + categories: [ HideSpawnMenu ] + parent: BaseAction + components: + - type: Action + itemIconStyle: BigAction + icon: + sprite: Imperial/TerrorSpider/Spider/destroyer.rsi + state: terror_brown + useDelay: 160 + - type: InstantAction + event: !type:TerrorSpiderPrincessLayEggActionEvent + eggPrototype: TerrorEggPrincessDestroyer + +- type: entity + id: TerrorEggPrincessTier1 + parent: TerrorEgg1Tir + name: egg terror spider + suffix: terror spider + components: + - type: Destructible + thresholds: + - trigger: + !type:DamageTrigger + damage: 40 + behaviors: + - !type:DoActsBehavior + acts: ["Destruction"] + - type: TerrorSpiderPrincessEgg + hatchDelay: 240 + tierTwo: false + +- type: entity + id: TerrorEggPrincessTier2 + parent: TerrorEgg2Tir + name: egg terror spider + suffix: terror spider + components: + - type: Destructible + thresholds: + - trigger: + !type:DamageTrigger + damage: 40 + behaviors: + - !type:DoActsBehavior + acts: ["Destruction"] + - type: TerrorSpiderPrincessEgg + hatchDelay: 240 + tierTwo: true + +- type: entity + id: TerrorEggPrincessRusar + parent: TerrorEggPrincessTier1 + components: + - type: TerrorSpiderPrincessEgg + tierTwo: false + tierOnePrototypes: [ MobRusarSpiderAngrys ] + +- type: entity + id: TerrorEggPrincessDron + parent: TerrorEggPrincessTier1 + components: + - type: TerrorSpiderPrincessEgg + tierTwo: false + tierOnePrototypes: [ MobDronSpiderAngrys ] + +- type: entity + id: TerrorEggPrincessLurker + parent: TerrorEggPrincessTier1 + components: + - type: TerrorSpiderPrincessEgg + tierTwo: false + tierOnePrototypes: [ MobsogladatelSpiderAngrys ] + +- type: entity + id: TerrorEggPrincessHealer + parent: TerrorEggPrincessTier1 + components: + - type: TerrorSpiderPrincessEgg + tierTwo: false + tierOnePrototypes: [ MobGiantHealerSpiderAngry ] + +- type: entity + id: TerrorEggPrincessReaper + parent: TerrorEggPrincessTier2 + components: + - type: TerrorSpiderPrincessEgg + tierTwo: true + tierTwoPrototypes: [ MobGiantreaperSpiderAngry ] + tierTwoElitePrototypes: [ ] + tierTwoUnlimitedPrototypes: [ MobGiantreaperSpiderAngry ] + +- type: entity + id: TerrorEggPrincessWidow + parent: TerrorEggPrincessTier2 + components: + - type: TerrorSpiderPrincessEgg + tierTwo: true + tierTwoPrototypes: [ MobGiantWidowSpiderAngry ] + tierTwoElitePrototypes: [ MobGiantWidowSpiderAngry ] + tierTwoUnlimitedPrototypes: [ MobGiantWidowSpiderAngry ] + +- type: entity + id: TerrorEggPrincessGuardian + parent: TerrorEggPrincessTier2 + components: + - type: TerrorSpiderPrincessEgg + tierTwo: true + tierTwoPrototypes: [ MobGiantGuardianSpiderAngry ] + tierTwoElitePrototypes: [ MobGiantGuardianSpiderAngry ] + tierTwoUnlimitedPrototypes: [ MobGiantGuardianSpiderAngry ] + +- type: entity + id: TerrorEggPrincessDestroyer + parent: TerrorEggPrincessTier2 + components: + - type: TerrorSpiderPrincessEgg + tierTwo: true + tierTwoPrototypes: [ MobGiantDestroyerSpiderAngry ] + tierTwoElitePrototypes: [ MobGiantDestroyerSpiderAngry ] + tierTwoUnlimitedPrototypes: [ MobGiantDestroyerSpiderAngry ] + +- type: entity + id: TerrorEggQueenRusar + parent: TerrorEgg1Tir + components: + - type: Destructible + thresholds: + - trigger: + !type:DamageTrigger + damage: 40 + behaviors: + - !type:DoActsBehavior + acts: ["Destruction"] + - type: TerrorSpiderQueenEgg + hatchDelay: 240 + spawnPrototype: MobRusarSpiderAngrys + +- type: entity + id: TerrorEggQueenDron + parent: TerrorEgg1Tir + components: + - type: Destructible + thresholds: + - trigger: + !type:DamageTrigger + damage: 40 + behaviors: + - !type:DoActsBehavior + acts: ["Destruction"] + - type: TerrorSpiderQueenEgg + hatchDelay: 240 + spawnPrototype: MobDronSpiderAngrys + +- type: entity + id: TerrorEggQueenLurker + parent: TerrorEgg1Tir + components: + - type: Destructible + thresholds: + - trigger: + !type:DamageTrigger + damage: 40 + behaviors: + - !type:DoActsBehavior + acts: ["Destruction"] + - type: TerrorSpiderQueenEgg + hatchDelay: 240 + spawnPrototype: MobsogladatelSpiderAngrys + +- type: entity + id: TerrorEggQueenHealer + parent: TerrorEgg1Tir + components: + - type: Destructible + thresholds: + - trigger: + !type:DamageTrigger + damage: 40 + behaviors: + - !type:DoActsBehavior + acts: ["Destruction"] + - type: TerrorSpiderQueenEgg + hatchDelay: 240 + spawnPrototype: MobGiantHealerSpiderAngry + +- type: entity + id: TerrorEggQueenReaper + parent: TerrorEgg2Tir + components: + - type: Destructible + thresholds: + - trigger: + !type:DamageTrigger + damage: 40 + behaviors: + - !type:DoActsBehavior + acts: ["Destruction"] + - type: TerrorSpiderQueenEgg + hatchDelay: 240 + spawnPrototype: MobGiantreaperSpiderAngry + +- type: entity + id: TerrorEggQueenWidow + parent: TerrorEgg2Tir + components: + - type: Destructible + thresholds: + - trigger: + !type:DamageTrigger + damage: 40 + behaviors: + - !type:DoActsBehavior + acts: ["Destruction"] + - type: TerrorSpiderQueenEgg + hatchDelay: 240 + spawnPrototype: MobGiantWidowSpiderAngry + +- type: entity + id: TerrorEggQueenGuardian + parent: TerrorEgg2Tir + components: + - type: Destructible + thresholds: + - trigger: + !type:DamageTrigger + damage: 40 + behaviors: + - !type:DoActsBehavior + acts: ["Destruction"] + - type: TerrorSpiderQueenEgg + hatchDelay: 240 + spawnPrototype: MobGiantGuardianSpiderAngry + +- type: entity + id: TerrorEggQueenDestroyer + parent: TerrorEgg2Tir + components: + - type: Destructible + thresholds: + - trigger: + !type:DamageTrigger + damage: 40 + behaviors: + - !type:DoActsBehavior + acts: ["Destruction"] + - type: TerrorSpiderQueenEgg + hatchDelay: 240 + spawnPrototype: MobGiantDestroyerSpiderAngry + +- type: entity + id: TerrorEggQueenPrince + parent: TerrorEgg3Tir + components: + - type: Destructible + thresholds: + - trigger: + !type:DamageTrigger + damage: 40 + behaviors: + - !type:DoActsBehavior + acts: ["Destruction"] + - type: TerrorSpiderQueenEgg + hatchDelay: 240 + spawnPrototype: MobGiantPrinceSpiderAngry + royalCooldownKey: Prince + +- type: entity + id: TerrorEggQueenPrincess + parent: TerrorEgg3Tir + components: + - type: Destructible + thresholds: + - trigger: + !type:DamageTrigger + damage: 40 + behaviors: + - !type:DoActsBehavior + acts: ["Destruction"] + - type: TerrorSpiderQueenEgg + hatchDelay: 240 + spawnPrototype: MobGiantPrincessSpiderAngry + royalCooldownKey: Princess + +- type: entity + id: TerrorEggQueenMother + parent: TerrorEgg3Tir + components: + - type: Destructible + thresholds: + - trigger: + !type:DamageTrigger + damage: 40 + behaviors: + - !type:DoActsBehavior + acts: ["Destruction"] + - type: TerrorSpiderQueenEgg + hatchDelay: 240 + spawnPrototype: MobGiantMotherSpiderAngry + royalCooldownKey: Mother + +- type: entity + id: ActionTerrorSpiderRoyalStomp + name: Stomp + description: Slow and crush nearby enemies. + categories: [ HideSpawnMenu ] + parent: BaseAction + components: + - type: Action + icon: + sprite: Imperial/TerrorSpider/act.rsi + state: slam + itemIconStyle: BigAction + useDelay: 35 + - type: InstantAction + event: !type:TerrorSpiderRoyalStompActionEvent + +- type: entity + id: ActionTerrorSpiderKnightRage + name: Rage + description: Enraged stance. + categories: [ HideSpawnMenu ] + parent: BaseAction + components: + - type: Action + icon: + sprite: Imperial/TerrorSpider/act.rsi + state: attack + itemIconStyle: BigAction + useDelay: 30 + - type: InstantAction + event: !type:TerrorSpiderKnightRageActionEvent + +- type: entity + id: ActionTerrorSpiderHealerPulse + name: Healer pulse + description: Heal all terror spiders in range. + categories: [ HideSpawnMenu ] + parent: BaseAction + components: + - type: Action + icon: + sprite: Imperial/TerrorSpider/act.rsi + state: heal + itemIconStyle: BigAction + useDelay: 30 + - type: InstantAction + event: !type:TerrorSpiderHealerPulseActionEvent + +- type: entity + id: ActionTerrorSpiderHealerLayEggRusar + name: Lay Rusar egg + description: Lay a tier-1 Rusar egg. + categories: [ HideSpawnMenu ] + parent: BaseAction + components: + - type: Action + icon: + sprite: Imperial/TerrorSpider/Spider/rusar.rsi + state: terror_red + itemIconStyle: BigAction + - type: InstantAction + event: !type:TerrorSpiderHealerLayEggActionEvent + eggPrototype: TerrorEggHealerRusar + +- type: entity + id: ActionTerrorSpiderHealerLayEggDron + name: Lay Dron egg + description: Lay a tier-1 Dron egg. + categories: [ HideSpawnMenu ] + parent: BaseAction + components: + - type: Action + icon: + sprite: Imperial/TerrorSpider/Spider/dron.rsi + state: terror_widow + itemIconStyle: BigAction + - type: InstantAction + event: !type:TerrorSpiderHealerLayEggActionEvent + eggPrototype: TerrorEggHealerDron + +- type: entity + id: ActionTerrorSpiderHealerLayEggLurker + name: Lay Lurker egg + description: Lay a tier-1 Lurker egg. + categories: [ HideSpawnMenu ] + parent: BaseAction + components: + - type: Action + icon: + sprite: Imperial/TerrorSpider/Spider/sogladatel.rsi + state: terror_gray_cloaked + itemIconStyle: BigAction + - type: InstantAction + event: !type:TerrorSpiderHealerLayEggActionEvent + eggPrototype: TerrorEggHealerLurker + +- type: entity + id: ActionTerrorSpiderHealerLayEggHealer + name: Lay Healer egg + description: Lay a tier-1 Healer egg. + categories: [ HideSpawnMenu ] + parent: BaseAction + components: + - type: Action + icon: + sprite: Imperial/TerrorSpider/Spider/healer.rsi + state: terror_green + itemIconStyle: BigAction + - type: InstantAction + event: !type:TerrorSpiderHealerLayEggActionEvent + eggPrototype: TerrorEggHealerHealer + +- type: entity + id: TerrorWeedsHealerAction + name: healer web + description: terror web + categories: [ HideSpawnMenu ] + parent: BaseAction + components: + - type: Action + icon: + sprite: Structures/floor_web.rsi + state: full + itemIconStyle: BigAction + useDelay: 2 + - type: InstantAction + event: !type:TerrorSpiderSpawnWebActionEvent + webPrototype: SpiderWebTerrorHealer + +- type: entity + id: TerrorWeedsWidowAction + name: widow web + description: terror web + categories: [ HideSpawnMenu ] + parent: BaseAction + components: + - type: Action + icon: + sprite: Structures/floor_web.rsi + state: full + itemIconStyle: BigAction + useDelay: 2.5 + - type: InstantAction + event: !type:TerrorSpiderSpawnWebActionEvent + webPrototype: SpiderWebTerrorWidow + +- type: entity + id: ActionTerrorSpiderLurkerStealth + name: Stealth + description: Become invisible for a short duration. + categories: [ HideSpawnMenu ] + parent: BaseAction + components: + - type: Action + icon: + sprite: Imperial/TerrorSpider/act.rsi + state: stealth + itemIconStyle: BigAction + useDelay: 25 + - type: InstantAction + event: !type:TerrorSpiderLurkerStealthActionEvent + +- type: entity + id: TerrorWeedsRusarAction + name: knight web + description: terror web + categories: [ HideSpawnMenu ] + parent: BaseAction + components: + - type: Action + icon: + sprite: Structures/floor_web.rsi + state: full + itemIconStyle: BigAction + useDelay: 2.5 + - type: InstantAction + event: !type:TerrorSpiderSpawnWebActionEvent + webPrototype: SpiderWebTerrorRusar + +- type: entity + id: TerrorWeedsLurkerAction + name: lurker web + description: terror web + categories: [ HideSpawnMenu ] + parent: BaseAction + components: + - type: Action + icon: + sprite: Structures/floor_web.rsi + state: full + itemIconStyle: BigAction + useDelay: 3 + - type: InstantAction + event: !type:TerrorSpiderSpawnWebActionEvent + webPrototype: SpiderWebTerrorLurker + +- type: entity + id: TerrorWeedsPrincessAction + name: princess web + description: terror web + categories: [ HideSpawnMenu ] + parent: BaseAction + components: + - type: Action + icon: + sprite: Structures/floor_web.rsi + state: full + itemIconStyle: BigAction + useDelay: 2 + - type: InstantAction + event: !type:TerrorSpiderSpawnWebActionEvent + webPrototype: SpiderWebPrincess + +- type: entity + id: SpiderWebTerrorLurker + name: web terror spider + description: TERROR! + placement: + mode: SnapgridCenter + snap: + - Wall + components: + - type: MeleeSound + soundGroups: + Brute: + path: + "/Audio/Weapons/slash.ogg" + - type: Sprite + sprite: Structures/floor_web.rsi + color: "#FFFFFF88" + layers: + - state: full + map: ["spiderWebLayer"] + drawdepth: WallMountedItems + - type: Appearance + - type: Clickable + - type: Transform + anchored: true + - type: Physics + - type: Fixtures + fixtures: + fix1: + hard: false + density: 7 + shape: + !type:PhysShapeAabb + bounds: "-0.5,-0.5,0.5,0.5" + layer: + - MidImpassable + - type: Damageable + damageModifierSet: Wood + - type: Destructible + thresholds: + - trigger: + !type:DamageTrigger + damage: 20 + behaviors: + - !type:DoActsBehavior + acts: [ "Destruction" ] + - type: Temperature + heatDamage: + types: + Heat: 60 + coldDamage: {} + coldDamageThreshold: 0 + - type: Flammable + fireSpread: true + damage: + types: + Heat: 60 + - type: Reactive + groups: + Flammable: [Touch] + Extinguish: [Touch] + - type: SpiderWebObject + - type: TerrorSpiderWebBuffArea + - type: TerrorSpiderLurkerWebArea + staminaDamage: 100 + muteDuration: 14 + - type: SpeedModifierContacts + walkSpeedModifier: 0.5 + sprintSpeedModifier: 0.5 + ignoreWhitelist: + components: + - IgnoreSpiderWeb + - type: Slippery + - type: StepTrigger + intersectRatio: 0.2 + +- type: entity + id: SpiderWebTerrorWidow + name: web terror spider + description: TERROR! + placement: + mode: SnapgridCenter + snap: + - Wall + components: + - type: MeleeSound + soundGroups: + Brute: + path: + "/Audio/Weapons/slash.ogg" + - type: Sprite + sprite: Structures/floor_web.rsi + layers: + - state: full + map: ["spiderWebLayer"] + drawdepth: WallMountedItems + - type: Appearance + - type: Clickable + - type: Transform + anchored: true + - type: Physics + - type: Fixtures + fixtures: + fix1: + hard: false + density: 7 + shape: + !type:PhysShapeAabb + bounds: "-0.5,-0.5,0.5,0.5" + layer: + - MidImpassable + - type: Damageable + damageModifierSet: Wood + - type: Destructible + thresholds: + - trigger: + !type:DamageTrigger + damage: 20 + behaviors: + - !type:DoActsBehavior + acts: [ "Destruction" ] + - type: Temperature + heatDamage: + types: + Heat: 60 + coldDamage: {} + coldDamageThreshold: 0 + - type: Flammable + fireSpread: true + damage: + types: + Heat: 60 + - type: Reactive + groups: + Flammable: [Touch] + Extinguish: [Touch] + - type: SpiderWebObject + - type: TerrorSpiderWebBuffArea + - type: TerrorSpiderWidowWebArea + venomOnTouch: 20 + venomReagent: BlackTerrorVenom + - type: SpeedModifierContacts + walkSpeedModifier: 0.5 + sprintSpeedModifier: 0.5 + ignoreWhitelist: + components: + - IgnoreSpiderWeb + - type: Slippery + - type: StepTrigger + intersectRatio: 0.2 + +- type: entity + id: SpiderWebTerrorGuardian + name: web terror spider + description: TERROR! + placement: + mode: SnapgridCenter + snap: + - Wall + components: + - type: MeleeSound + soundGroups: + Brute: + path: + "/Audio/Weapons/slash.ogg" + - type: Sprite + sprite: Structures/floor_web.rsi + layers: + - state: full + map: ["spiderWebLayer"] + drawdepth: WallMountedItems + - type: Appearance + - type: Clickable + - type: Transform + anchored: true + - type: Physics + - type: Fixtures + fixtures: + fix1: + hard: false + density: 7 + shape: + !type:PhysShapeAabb + bounds: "-0.5,-0.5,0.5,0.5" + layer: + - MidImpassable + - type: Damageable + damageModifierSet: Wood + - type: Destructible + thresholds: + - trigger: + !type:DamageTrigger + damage: 40 + behaviors: + - !type:DoActsBehavior + acts: [ "Destruction" ] + - type: Occluder + - type: SpiderWebObject + - type: TerrorSpiderWebBuffArea + - type: SpeedModifierContacts + walkSpeedModifier: 0.5 + sprintSpeedModifier: 0.5 + ignoreWhitelist: + components: + - IgnoreSpiderWeb + - type: Slippery + - type: StepTrigger + intersectRatio: 0.2 + +- type: entity + id: SpiderWebTerrorGuardianBarrier + name: guardian barrier + description: TERROR! + placement: + mode: SnapgridCenter + snap: + - Wall + components: + - type: Sprite + sprite: Imperial/TerrorSpider/effects.rsi + layers: + - state: terror_shield + map: ["spiderWebLayer"] + drawdepth: WallMountedItems + - type: Appearance + - type: Clickable + - type: Transform + anchored: true + - type: Physics + - type: Fixtures + fixtures: + fix1: + hard: true + density: 7 + shape: + !type:PhysShapeAabb + bounds: "-0.5,-0.5,0.5,0.5" + layer: + - MidImpassable + - type: Occluder + - type: SpiderWebObject + - type: TerrorSpiderGuardianShieldBarrier + - type: TimedDespawn + lifetime: 16.5 + +- type: entity + id: SpiderWebTerrorHealer + name: web terror spider + description: TERROR! + placement: + mode: SnapgridCenter + snap: + - Wall + components: + - type: MeleeSound + soundGroups: + Brute: + path: + "/Audio/Weapons/slash.ogg" + - type: Sprite + sprite: Structures/floor_web.rsi + layers: + - state: full + map: ["spiderWebLayer"] + drawdepth: WallMountedItems + - type: Appearance + - type: Clickable + - type: Transform + anchored: true + - type: Physics + - type: Fixtures + fixtures: + fix1: + hard: false + density: 7 + shape: + !type:PhysShapeAabb + bounds: "-0.5,-0.5,0.5,0.5" + layer: + - MidImpassable + - type: Damageable + damageModifierSet: Wood + - type: Destructible + thresholds: + - trigger: + !type:DamageTrigger + damage: 20 + behaviors: + - !type:DoActsBehavior + acts: [ "Destruction" ] + - type: Temperature + heatDamage: + types: + Heat: 60 + coldDamage: {} + coldDamageThreshold: 0 + - type: Flammable + fireSpread: true + damage: + types: + Heat: 60 + - type: Reactive + groups: + Flammable: [Touch] + Extinguish: [Touch] + - type: SpiderWebObject + - type: TerrorSpiderWebBuffArea + - type: TerrorSpiderHealerBlindWeb + blindDuration: 30 + - type: SpeedModifierContacts + walkSpeedModifier: 0.5 + sprintSpeedModifier: 0.5 + ignoreWhitelist: + components: + - IgnoreSpiderWeb + - type: Slippery + - type: StepTrigger + intersectRatio: 0.2 + +- type: entity + id: TerrorEggHealerRusar + parent: TerrorEgg1Tir + name: egg terror spider + suffix: terror spider + components: + - type: Destructible + thresholds: + - trigger: + !type:DamageTrigger + damage: 40 + behaviors: + - !type:DoActsBehavior + acts: ["Destruction"] + - type: TerrorSpiderQueenEgg + hatchDelay: 240 + spawnPrototype: MobRusarSpiderAngrys + +- type: entity + id: TerrorEggHealerDron + parent: TerrorEgg1Tir + name: egg terror spider + suffix: terror spider + components: + - type: Destructible + thresholds: + - trigger: + !type:DamageTrigger + damage: 40 + behaviors: + - !type:DoActsBehavior + acts: ["Destruction"] + - type: TerrorSpiderQueenEgg + hatchDelay: 240 + spawnPrototype: MobDronSpiderAngrys + +- type: entity + id: TerrorEggHealerLurker + parent: TerrorEgg1Tir + name: egg terror spider + suffix: terror spider + components: + - type: Destructible + thresholds: + - trigger: + !type:DamageTrigger + damage: 40 + behaviors: + - !type:DoActsBehavior + acts: ["Destruction"] + - type: TerrorSpiderQueenEgg + hatchDelay: 240 + spawnPrototype: MobsogladatelSpiderAngrys + +- type: entity + id: TerrorEggHealerHealer + parent: TerrorEgg1Tir + name: egg terror spider + suffix: terror spider + components: + - type: Destructible + thresholds: + - trigger: + !type:DamageTrigger + damage: 40 + behaviors: + - !type:DoActsBehavior + acts: ["Destruction"] + - type: TerrorSpiderQueenEgg + hatchDelay: 240 + spawnPrototype: MobGiantHealerSpiderAngry + +- type: entity + id: SpiderWebPrincess + name: web terror spider + description: TERROR! + placement: + mode: SnapgridCenter + snap: + - Wall + components: + - type: MeleeSound + soundGroups: + Brute: + path: + "/Audio/Weapons/slash.ogg" + - type: Sprite + sprite: Structures/floor_web.rsi + layers: + - state: full + map: ["spiderWebLayer"] + drawdepth: WallMountedItems + - type: Appearance + - type: Clickable + - type: Transform + anchored: true + - type: Physics + - type: Fixtures + fixtures: + fix1: + hard: false + density: 7 + shape: + !type:PhysShapeAabb + bounds: "-0.5,-0.5,0.5,0.5" + layer: + - MidImpassable + - type: Damageable + damageModifierSet: Wood + - type: Destructible + thresholds: + - trigger: + !type:DamageTrigger + damage: 30 + behaviors: + - !type:DoActsBehavior + acts: [ "Destruction" ] + - type: Temperature + heatDamage: + types: + Heat: 60 + coldDamage: {} + coldDamageThreshold: 0 + - type: Flammable + fireSpread: true + damage: + types: + Heat: 60 + - type: Reactive + groups: + Flammable: [Touch] + Extinguish: [Touch] + - type: SpiderWebObject + - type: TerrorSpiderWebBuffArea + - type: SpeedModifierContacts + walkSpeedModifier: 0.5 + sprintSpeedModifier: 0.5 + ignoreWhitelist: + components: + - IgnoreSpiderWeb + - type: Slippery + - type: StepTrigger + intersectRatio: 0.2 + - type: Airtight + noAirWhenFullyAirBlocked: false + +- type: entity + abstract: true + name: Xeno + id: KsenosXenoBase + parent: MobXeno + suffix: KsenosXeno + nospawn: true + components: + - type: LagCompensation + - type: Input + context: "human" + - type: MovedByPressure + - type: DamageOnHighSpeedImpact + damage: + types: + Blunt: 5 + soundHit: + path: /Audio/Effects/hit_kick.ogg + - type: Sprite + drawdepth: Mobs + sprite: Mobs/Aliens/Xenos/burrower.rsi + layers: + - map: ["enum.DamageStateVisualLayers.Base"] + state: running + noRot: true + netsync: false + - type: Clickable + - type: InteractionOutline + - type: SolutionContainerManager + - type: AtmosExposed + - type: MobThresholds + thresholds: + 0: Alive + 50: Critical + 300: Dead + - type: Internals + - type: Damageable + damageContainer: Biological + damageModifierSet: Slime - type: Body prototype: Animal - type: Actions @@ -2650,11 +4473,6 @@ - type: Appearance - type: Bloodstream bloodMaxVolume: 0 - - type: UnpoweredFlashlight - - type: PointLight - enabled: false - radius: 4 - color: "purple" - type: Puller needsHands: false - type: NoSlip @@ -2677,10 +4495,13 @@ reagents: - ReagentId: Water Quantity: 10 + - type: Access + tags: + - Xeno - type: radioChannel id: hiveconnection - name: Улей + name: Hive keycode: 'й' frequency: 8888 color: "#800080" @@ -2700,6 +4521,7 @@ parent: BaseItem id: WeaponXenoSpit description: Concentrated toxicity + nospawn: true components: - type: Sprite sprite: Objects/Weapons/Guns/Projectiles/magic.rsi @@ -2729,18 +4551,71 @@ id: Terrorspider alias: - Terror - name: spider-terror-gamerule - description: spider-terror-gamerule-desc + name: Terror Spiders + description: Terror spiders have appeared on the station. These vile and bloodthirsty spiders want to capture the station and turn it into their hive. showInVote: false rules: - BasicStationEventScheduler - BasicRoundstartVariation +- type: reagent + id: BlackTerrorVenom + name: Black Terror Venom + group: Toxins + desc: A venom used by terror widow spiders. + flavor: bitter + color: "#800080" + physicalDesc: reagent-physical-desc-opaque + metabolisms: + Poison: + effects: + - !type:HealthChange + conditions: + - !type:ReagentCondition + reagent: BlackTerrorVenom + max: 30 + damage: + types: + Poison: 1 + - !type:HealthChange + conditions: + - !type:ReagentCondition + reagent: BlackTerrorVenom + min: 30 + max: 60 + damage: + types: + Poison: 2 + - !type:HealthChange + conditions: + - !type:ReagentCondition + reagent: BlackTerrorVenom + min: 60 + max: 90 + damage: + types: + Poison: 4 + - !type:HealthChange + conditions: + - !type:ReagentCondition + reagent: BlackTerrorVenom + min: 90 + damage: + types: + Poison: 8 + - !type:GenericStatusEffect + key: TemporaryBlindness + component: TemporaryBlindness + conditions: + - !type:ReagentCondition + reagent: BlackTerrorVenom + min: 90 + - type: entity id: GasXenoSpitBullet name: Xeno's gas spit - description: Concentrated toxicity parent: BaseBulletTrigger + categories: [ HideSpawnMenu ] components: - type: Sprite sprite: Objects/Weapons/Guns/Projectiles/magic.rsi @@ -2818,7 +4693,6 @@ - type: entity id: SpiderWebTerrorUwU name: web terror spider - parent: SpiderWebBase description: TERROR! placement: mode: SnapgridCenter @@ -2831,7 +4705,16 @@ path: "/Audio/Weapons/slash.ogg" - type: Sprite - color: "#ffffffdd" + sprite: Structures/floor_web.rsi + layers: + - state: full + map: ["spiderWebLayer"] + drawdepth: WallMountedItems + - type: Appearance + - type: Clickable + - type: Transform + anchored: true + - type: Physics - type: Fixtures fixtures: fix1: @@ -2873,6 +4756,7 @@ Flammable: [Touch] Extinguish: [Touch] - type: SpiderWebObject + - type: TerrorSpiderWebBuffArea - type: SpeedModifierContacts walkSpeedModifier: 0.5 sprintSpeedModifier: 0.5 @@ -2883,28 +4767,21 @@ lifetime: 1 - type: entity - id: XenoSpitBullet - name: Spit xeno - parent: BulletDisablerPractice - components: - - type: Sprite - sprite: Objects/Weapons/Guns/Projectiles/magic.rsi - scale: 1.3, 2 - layers: - - state: declone - shader: unshaded - - type: PointLight - radius: 3 - energy: 3 - enabled: true - color: Green - - type: Projectile - impactEffect: BulletImpactEffect - damage: - types: - Poison: 20 - Structural: 25 - soundHit: - path: "/Audio/Weapons/Guns/Hits/energy_meat1.ogg" - - type: TimedDespawn - lifetime: 2 + parent: StatusEffectSlowdown + id: TerrorSpiderRoyalStompSlowStatusEffect + name: terror spider royal stomp slowdown + +- type: entity + parent: StatusEffectSlowdown + id: TerrorSpiderPrincessRemoteViewImmobileStatusEffect + name: terror spider princess remote view immobile + +- type: entity + parent: StatusEffectSlowdown + id: TerrorSpiderMotherRemoteViewImmobileStatusEffect + name: terror spider mother remote view immobile + +- type: entity + parent: StatusEffectSlowdown + id: TerrorSpiderQueenHiveSlowStatusEffect + name: terror spider queen hive slowdown \ No newline at end of file diff --git a/Resources/Textures/Imperial/Seriozha/Fredik21/nda/nda096.rsi/meta.json b/Resources/Textures/Imperial/Seriozha/Fredik21/nda/nda096.rsi/meta.json new file mode 100644 index 00000000000..c3030c6ac2a --- /dev/null +++ b/Resources/Textures/Imperial/Seriozha/Fredik21/nda/nda096.rsi/meta.json @@ -0,0 +1,36 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "https://github.com/Foundation-19", + "size": { + "x": 48, + "y": 64 + }, + "states": [ + { + "name": "scp-dead" + }, + { + "name": "scp-chasing", + "directions": 4 + }, + { + "name": "scp-screaming", + "delays": [ + [ + 6, + 2, + 10, + 2, + 2, + 2, + 2, + 3 + ] + ] + }, + { + "name": "scp" + } + ] +} diff --git a/Resources/Textures/Imperial/Seriozha/Fredik21/nda/nda096.rsi/scp-chasing.png b/Resources/Textures/Imperial/Seriozha/Fredik21/nda/nda096.rsi/scp-chasing.png new file mode 100644 index 00000000000..dfa29f250eb Binary files /dev/null and b/Resources/Textures/Imperial/Seriozha/Fredik21/nda/nda096.rsi/scp-chasing.png differ diff --git a/Resources/Textures/Imperial/Seriozha/Fredik21/nda/nda096.rsi/scp-dead.png b/Resources/Textures/Imperial/Seriozha/Fredik21/nda/nda096.rsi/scp-dead.png new file mode 100644 index 00000000000..ff2003d3562 Binary files /dev/null and b/Resources/Textures/Imperial/Seriozha/Fredik21/nda/nda096.rsi/scp-dead.png differ diff --git a/Resources/Textures/Imperial/Seriozha/Fredik21/nda/nda096.rsi/scp-screaming.png b/Resources/Textures/Imperial/Seriozha/Fredik21/nda/nda096.rsi/scp-screaming.png new file mode 100644 index 00000000000..c1f857eb1d5 Binary files /dev/null and b/Resources/Textures/Imperial/Seriozha/Fredik21/nda/nda096.rsi/scp-screaming.png differ diff --git a/Resources/Textures/Imperial/Seriozha/Fredik21/nda/nda096.rsi/scp.png b/Resources/Textures/Imperial/Seriozha/Fredik21/nda/nda096.rsi/scp.png new file mode 100644 index 00000000000..53410531062 Binary files /dev/null and b/Resources/Textures/Imperial/Seriozha/Fredik21/nda/nda096.rsi/scp.png differ diff --git a/Resources/Textures/Imperial/Seriozha/Fredik21/nda/nda173.rsi/173.png b/Resources/Textures/Imperial/Seriozha/Fredik21/nda/nda173.rsi/173.png new file mode 100644 index 00000000000..65e4b98410f Binary files /dev/null and b/Resources/Textures/Imperial/Seriozha/Fredik21/nda/nda173.rsi/173.png differ diff --git a/Resources/Textures/Imperial/TerrorSpider/Spider/bluexeno_toxin.rsi/meta.json b/Resources/Textures/Imperial/Seriozha/Fredik21/nda/nda173.rsi/meta.json similarity index 50% rename from Resources/Textures/Imperial/TerrorSpider/Spider/bluexeno_toxin.rsi/meta.json rename to Resources/Textures/Imperial/Seriozha/Fredik21/nda/nda173.rsi/meta.json index c3fa86c7745..09f3de0338f 100644 --- a/Resources/Textures/Imperial/TerrorSpider/Spider/bluexeno_toxin.rsi/meta.json +++ b/Resources/Textures/Imperial/Seriozha/Fredik21/nda/nda173.rsi/meta.json @@ -1,14 +1,15 @@ { "version": 1, "license": "CC-BY-SA-3.0", - "copyright": "By Fredik21 https://github.com/imperial-space", + "copyright": "https://github.com/Foundation-19", "size": { "x": 32, - "y": 32 + "y": 64 }, "states": [ { - "name": "bluetox" + "name": "173", + "directions": 4 } ] -} +} \ No newline at end of file diff --git a/Resources/Textures/Imperial/TerrorSpider/Spider/bluexeno_toxin.rsi/bluetox.png b/Resources/Textures/Imperial/TerrorSpider/Spider/bluexeno_toxin.rsi/bluetox.png deleted file mode 100644 index c46162f1028..00000000000 Binary files a/Resources/Textures/Imperial/TerrorSpider/Spider/bluexeno_toxin.rsi/bluetox.png and /dev/null differ diff --git a/Resources/Textures/Imperial/TerrorSpider/act.rsi/attack.png b/Resources/Textures/Imperial/TerrorSpider/act.rsi/attack.png new file mode 100644 index 00000000000..0002e9d5c7e Binary files /dev/null and b/Resources/Textures/Imperial/TerrorSpider/act.rsi/attack.png differ diff --git a/Resources/Textures/Imperial/TerrorSpider/act.rsi/defence.png b/Resources/Textures/Imperial/TerrorSpider/act.rsi/defence.png new file mode 100644 index 00000000000..a320e1aa3db Binary files /dev/null and b/Resources/Textures/Imperial/TerrorSpider/act.rsi/defence.png differ diff --git a/Resources/Textures/Imperial/TerrorSpider/act.rsi/emp_new.png b/Resources/Textures/Imperial/TerrorSpider/act.rsi/emp_new.png new file mode 100644 index 00000000000..6e3bcf3d8b7 Binary files /dev/null and b/Resources/Textures/Imperial/TerrorSpider/act.rsi/emp_new.png differ diff --git a/Resources/Textures/Imperial/TerrorSpider/act.rsi/explosion_old.png b/Resources/Textures/Imperial/TerrorSpider/act.rsi/explosion_old.png new file mode 100644 index 00000000000..799183e240f Binary files /dev/null and b/Resources/Textures/Imperial/TerrorSpider/act.rsi/explosion_old.png differ diff --git a/Resources/Textures/Imperial/TerrorSpider/act.rsi/heal.png b/Resources/Textures/Imperial/TerrorSpider/act.rsi/heal.png new file mode 100644 index 00000000000..55f917a815a Binary files /dev/null and b/Resources/Textures/Imperial/TerrorSpider/act.rsi/heal.png differ diff --git a/Resources/Textures/Imperial/TerrorSpider/act.rsi/meta.json b/Resources/Textures/Imperial/TerrorSpider/act.rsi/meta.json new file mode 100644 index 00000000000..e6f9890bcae --- /dev/null +++ b/Resources/Textures/Imperial/TerrorSpider/act.rsi/meta.json @@ -0,0 +1,59 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Paradise Station (https://github.com/ParadiseSS13/Paradise), modified by Imperial Space SS14", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "attack" + }, + { + "name": "defence" + }, + { + "name": "emp_new" + }, + { + "name": "explosion_old" + }, + { + "name": "heal" + }, + { + "name": "mindswap" + }, + { + "name": "parasmoke" + }, + { + "name": "pause" + }, + { + "name": "show" + }, + { + "name": "show2" + }, + { + "name": "slam" + }, + { + "name": "smoke_old" + }, + { + "name": "spiderjelly" + }, + { + "name": "stealth" + }, + { + "name": "terror_shield" + }, + { + "name": "terror_shriek" + } + ] +} \ No newline at end of file diff --git a/Resources/Textures/Imperial/TerrorSpider/act.rsi/mindswap.png b/Resources/Textures/Imperial/TerrorSpider/act.rsi/mindswap.png new file mode 100644 index 00000000000..7386edc85eb Binary files /dev/null and b/Resources/Textures/Imperial/TerrorSpider/act.rsi/mindswap.png differ diff --git a/Resources/Textures/Imperial/TerrorSpider/act.rsi/parasmoke.png b/Resources/Textures/Imperial/TerrorSpider/act.rsi/parasmoke.png new file mode 100644 index 00000000000..24165310a6d Binary files /dev/null and b/Resources/Textures/Imperial/TerrorSpider/act.rsi/parasmoke.png differ diff --git a/Resources/Textures/Imperial/TerrorSpider/act.rsi/pause.png b/Resources/Textures/Imperial/TerrorSpider/act.rsi/pause.png new file mode 100644 index 00000000000..7a19eb5a69a Binary files /dev/null and b/Resources/Textures/Imperial/TerrorSpider/act.rsi/pause.png differ diff --git a/Resources/Textures/Imperial/TerrorSpider/act.rsi/show.png b/Resources/Textures/Imperial/TerrorSpider/act.rsi/show.png new file mode 100644 index 00000000000..b027c9a5e9e Binary files /dev/null and b/Resources/Textures/Imperial/TerrorSpider/act.rsi/show.png differ diff --git a/Resources/Textures/Imperial/TerrorSpider/act.rsi/show2.png b/Resources/Textures/Imperial/TerrorSpider/act.rsi/show2.png new file mode 100644 index 00000000000..efd19b6a9e1 Binary files /dev/null and b/Resources/Textures/Imperial/TerrorSpider/act.rsi/show2.png differ diff --git a/Resources/Textures/Imperial/TerrorSpider/act.rsi/slam.png b/Resources/Textures/Imperial/TerrorSpider/act.rsi/slam.png new file mode 100644 index 00000000000..9dd39de0672 Binary files /dev/null and b/Resources/Textures/Imperial/TerrorSpider/act.rsi/slam.png differ diff --git a/Resources/Textures/Imperial/TerrorSpider/act.rsi/smoke_old.png b/Resources/Textures/Imperial/TerrorSpider/act.rsi/smoke_old.png new file mode 100644 index 00000000000..46fbcdd033b Binary files /dev/null and b/Resources/Textures/Imperial/TerrorSpider/act.rsi/smoke_old.png differ diff --git a/Resources/Textures/Imperial/TerrorSpider/act.rsi/spiderjelly.png b/Resources/Textures/Imperial/TerrorSpider/act.rsi/spiderjelly.png new file mode 100644 index 00000000000..2638a7263d6 Binary files /dev/null and b/Resources/Textures/Imperial/TerrorSpider/act.rsi/spiderjelly.png differ diff --git a/Resources/Textures/Imperial/TerrorSpider/act.rsi/stealth.png b/Resources/Textures/Imperial/TerrorSpider/act.rsi/stealth.png new file mode 100644 index 00000000000..e0f06ead966 Binary files /dev/null and b/Resources/Textures/Imperial/TerrorSpider/act.rsi/stealth.png differ diff --git a/Resources/Textures/Imperial/TerrorSpider/act.rsi/terror_shield.png b/Resources/Textures/Imperial/TerrorSpider/act.rsi/terror_shield.png new file mode 100644 index 00000000000..a5ea15a4ac2 Binary files /dev/null and b/Resources/Textures/Imperial/TerrorSpider/act.rsi/terror_shield.png differ diff --git a/Resources/Textures/Imperial/TerrorSpider/act.rsi/terror_shriek.png b/Resources/Textures/Imperial/TerrorSpider/act.rsi/terror_shriek.png new file mode 100644 index 00000000000..b9904e2e0ff Binary files /dev/null and b/Resources/Textures/Imperial/TerrorSpider/act.rsi/terror_shriek.png differ diff --git a/Resources/Textures/Imperial/TerrorSpider/effects.rsi/cocoon_large3.png b/Resources/Textures/Imperial/TerrorSpider/effects.rsi/cocoon_large3.png new file mode 100644 index 00000000000..97cea079615 Binary files /dev/null and b/Resources/Textures/Imperial/TerrorSpider/effects.rsi/cocoon_large3.png differ diff --git a/Resources/Textures/Imperial/TerrorSpider/effects.rsi/meta.json b/Resources/Textures/Imperial/TerrorSpider/effects.rsi/meta.json new file mode 100644 index 00000000000..97c1e24f37d --- /dev/null +++ b/Resources/Textures/Imperial/TerrorSpider/effects.rsi/meta.json @@ -0,0 +1,17 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "https://github.com/ParadiseSS13/Paradise", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "cocoon_large3" + }, + { + "name": "terror_shield" + } + ] +} \ No newline at end of file diff --git a/Resources/Textures/Imperial/TerrorSpider/effects.rsi/terror_shield.png b/Resources/Textures/Imperial/TerrorSpider/effects.rsi/terror_shield.png new file mode 100644 index 00000000000..ef176b98933 Binary files /dev/null and b/Resources/Textures/Imperial/TerrorSpider/effects.rsi/terror_shield.png differ