diff --git a/Content.Client/Overlays/EquipmentHudSystem.cs b/Content.Client/Overlays/EquipmentHudSystem.cs index f3c556961a..ad915264ec 100644 --- a/Content.Client/Overlays/EquipmentHudSystem.cs +++ b/Content.Client/Overlays/EquipmentHudSystem.cs @@ -94,7 +94,9 @@ public abstract class EquipmentHudSystem : EntitySystem where T : IComponent protected virtual void OnRefreshEquipmentHud(Entity ent, ref InventoryRelayedEvent> args) { - OnRefreshComponentHud(ent, ref args.Args); + // Goobstation edit + args.Args.Active = true; + args.Args.Components.Add(ent.Comp); } protected virtual void OnRefreshComponentHud(Entity ent, ref RefreshEquipmentHudEvent args) diff --git a/Content.Client/_DV/Body/LightLevelHealthSystem.cs b/Content.Client/_DV/Body/LightLevelHealthSystem.cs new file mode 100644 index 0000000000..28021396c1 --- /dev/null +++ b/Content.Client/_DV/Body/LightLevelHealthSystem.cs @@ -0,0 +1,52 @@ +using Content.Client._DV.Light; +using Content.Shared._DV.Body; +using Content.Shared.Alert; +using Robust.Client.Player; +using Robust.Shared.Prototypes; + +namespace Content.Client._DV.Body; + +public sealed class LightLevelHealthSystem : SharedLightLevelHealthSystem +{ + [Dependency] private readonly AlertsSystem _alerts = default!; + [Dependency] private readonly IPlayerManager _player = default!; + [Dependency] private readonly IPrototypeManager _prototype = default!; + [Dependency] private readonly LightReactiveSystem _lightReactive = default!; + + private ProtoId _lightLevelDarkIcon = "LightLevelDarkIcon"; + private ProtoId _lightLevelNeutralIcon = "LightLevelNeutralIcon"; + private ProtoId _lightLevelBrightIcon = "LightLevelBrightIcon"; + private ProtoId _lightAlertCategory = "Light"; + + private int _lastThreshold = 0; + + public override void Update(float frameTime) + { + base.Update(frameTime); + + // If we're a LightLevelHealthComponent client, update our alerts. + if (_player.LocalSession?.AttachedEntity is not { } ent) + return; + + if (!TryComp(ent, out var lightLevelHealth)) + return; + + var currentThreshold = CurrentThreshold(_lightReactive.GetLightLevelForPoint(ent), lightLevelHealth); + var alertIcon = currentThreshold switch + { + -1 => _lightLevelDarkIcon, + 1 => _lightLevelBrightIcon, + _ => _lightLevelNeutralIcon, + }; + + if (currentThreshold != _lastThreshold) + { + var alertCategory = _prototype.Index(_lightAlertCategory); + _alerts.ClearAlertCategory(ent, alertCategory); + } + _lastThreshold = currentThreshold; + + var alertProto = _prototype.Index(alertIcon); + _alerts.ShowAlert(ent, alertProto); + } +} diff --git a/Content.Client/_DV/Light/LightReactiveSystem.cs b/Content.Client/_DV/Light/LightReactiveSystem.cs new file mode 100644 index 0000000000..420b3d027f --- /dev/null +++ b/Content.Client/_DV/Light/LightReactiveSystem.cs @@ -0,0 +1,24 @@ +using Content.Shared._DV.Light; +using Robust.Client.GameObjects; + +namespace Content.Client._DV.Light; + +public sealed partial class LightReactiveSystem : SharedLightReactiveSystem +{ + [Dependency] private readonly EntityLookupSystem _lookup = default!; + + private readonly HashSet> _lightsInRange = new(); + private readonly HashSet> _validLightsInRange = new(); + public override HashSet> GetLights(EntityUid targetEntity) + { + _lightsInRange.Clear(); + _lookup.GetEntitiesInRange(Transform(targetEntity).Coordinates, 10f, _lightsInRange); + _validLightsInRange.Clear(); + foreach (var light in _lightsInRange) + { + if(light.Comp.Enabled && !light.Comp.Deleted && light.Comp.NetSyncEnabled) + _validLightsInRange.Add(new(light.Owner, light.Comp)); + } + return _validLightsInRange; + } +} diff --git a/Content.Client/_Goobstation/Overlays/BaseSwitchableOverlay.cs b/Content.Client/_Goobstation/Overlays/BaseSwitchableOverlay.cs new file mode 100644 index 0000000000..e6070794ad --- /dev/null +++ b/Content.Client/_Goobstation/Overlays/BaseSwitchableOverlay.cs @@ -0,0 +1,56 @@ +// SPDX-FileCopyrightText: 2025 Aiden <28298836+Aidenkrz@users.noreply.github.com> +// SPDX-FileCopyrightText: 2025 Aviu00 <93730715+Aviu00@users.noreply.github.com> +// SPDX-FileCopyrightText: 2025 Misandry +// SPDX-FileCopyrightText: 2025 Spatison <137375981+Spatison@users.noreply.github.com> +// SPDX-FileCopyrightText: 2025 gus +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +using System.Numerics; +using Content.Shared._Goobstation.Overlays; +using Robust.Client.Graphics; +using Robust.Shared.Enums; +using Robust.Shared.Prototypes; + +namespace Content.Client._Goobstation.Overlays; + +public sealed class BaseSwitchableOverlay : Overlay where TComp : SwitchableVisionOverlayComponent +{ + [Dependency] private readonly IPrototypeManager _prototype = default!; + + public override bool RequestScreenTexture => true; + public override OverlaySpace Space => OverlaySpace.WorldSpace; + + private readonly ShaderInstance _shader; + + public TComp? Comp = null; + + public bool IsActive = true; + + public BaseSwitchableOverlay() + { + IoCManager.InjectDependencies(this); + _shader = _prototype.Index("NightVision").InstanceUnique(); + } + + protected override void Draw(in OverlayDrawArgs args) + { + if (ScreenTexture is null || Comp is null || !IsActive) + return; + + _shader.SetParameter("SCREEN_TEXTURE", ScreenTexture); + _shader.SetParameter("tint", Comp.Tint); + _shader.SetParameter("luminance_threshold", Comp.Strength); + _shader.SetParameter("noise_amount", Comp.Noise); + + var worldHandle = args.WorldHandle; + + var accumulator = Math.Clamp(Comp.PulseAccumulator, 0f, Comp.PulseTime); + var alpha = Comp.PulseTime <= 0f ? 1f : float.Lerp(1f, 0f, accumulator / Comp.PulseTime); + + worldHandle.SetTransform(Matrix3x2.Identity); + worldHandle.UseShader(_shader); + worldHandle.DrawRect(args.WorldBounds, Comp.Color.WithAlpha(alpha)); + worldHandle.UseShader(null); + } +} diff --git a/Content.Client/_Goobstation/Overlays/NightVisionSystem.cs b/Content.Client/_Goobstation/Overlays/NightVisionSystem.cs new file mode 100644 index 0000000000..e71da09cfe --- /dev/null +++ b/Content.Client/_Goobstation/Overlays/NightVisionSystem.cs @@ -0,0 +1,111 @@ +// SPDX-FileCopyrightText: 2025 Aiden <28298836+Aidenkrz@users.noreply.github.com> +// SPDX-FileCopyrightText: 2025 Aviu00 <93730715+Aviu00@users.noreply.github.com> +// SPDX-FileCopyrightText: 2025 Misandry +// SPDX-FileCopyrightText: 2025 Spatison <137375981+Spatison@users.noreply.github.com> +// SPDX-FileCopyrightText: 2025 gus +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +using Content.Client.Overlays; +using Content.Shared._Goobstation.Overlays; +using Content.Shared.Inventory; +using Content.Shared.Inventory.Events; +using Robust.Client.Graphics; + +namespace Content.Client._Goobstation.Overlays; + +public sealed class NightVisionSystem : EquipmentHudSystem +{ + [Dependency] private readonly IOverlayManager _overlayMan = default!; + [Dependency] private readonly ILightManager _lightManager = default!; + + private BaseSwitchableOverlay _overlay = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnToggle); + + _overlay = new BaseSwitchableOverlay(); + } + + protected override void OnRefreshComponentHud(Entity ent, + ref RefreshEquipmentHudEvent args) + { + if (!ent.Comp.IsEquipment) + base.OnRefreshComponentHud(ent, ref args); + } + + protected override void OnRefreshEquipmentHud(Entity ent, + ref InventoryRelayedEvent> args) + { + if (ent.Comp.IsEquipment) + base.OnRefreshEquipmentHud(ent, ref args); + } + + private void OnToggle(Entity ent, ref SwitchableOverlayToggledEvent args) + { + RefreshOverlay(); + } + + protected override void UpdateInternal(RefreshEquipmentHudEvent args) + { + base.UpdateInternal(args); + + var active = false; + NightVisionComponent? nvComp = null; + foreach (var comp in args.Components) + { + if (comp.IsActive || comp.PulseTime > 0f && comp.PulseAccumulator < comp.PulseTime) + active = true; + else + continue; + + if (comp.DrawOverlay) + { + if (nvComp == null) + nvComp = comp; + else if (nvComp.PulseTime > 0f && comp.PulseTime <= 0f) + nvComp = comp; + } + + if (active && nvComp is { PulseTime: <= 0 }) + break; + } + + UpdateNightVision(active); + UpdateOverlay(nvComp); + } + + protected override void DeactivateInternal() + { + base.DeactivateInternal(); + + UpdateNightVision(false); + UpdateOverlay(null); + } + + private void UpdateNightVision(bool active) + { + _lightManager.DrawLighting = !active; + } + + private void UpdateOverlay(NightVisionComponent? nvComp) + { + _overlay.Comp = nvComp; + + switch (nvComp) + { + case not null when !_overlayMan.HasOverlay>(): + _overlayMan.AddOverlay(_overlay); + break; + case null: + _overlayMan.RemoveOverlay(_overlay); + break; + } + + if (_overlayMan.TryGetOverlay>(out var overlay)) + overlay.IsActive = nvComp == null; + } +} diff --git a/Content.Client/_Goobstation/Overlays/ThermalVisionOverlay.cs b/Content.Client/_Goobstation/Overlays/ThermalVisionOverlay.cs new file mode 100644 index 0000000000..d99741bd55 --- /dev/null +++ b/Content.Client/_Goobstation/Overlays/ThermalVisionOverlay.cs @@ -0,0 +1,168 @@ +// SPDX-FileCopyrightText: 2025 Aiden <28298836+Aidenkrz@users.noreply.github.com> +// SPDX-FileCopyrightText: 2025 Aviu00 <93730715+Aviu00@users.noreply.github.com> +// SPDX-FileCopyrightText: 2025 Misandry +// SPDX-FileCopyrightText: 2025 Spatison <137375981+Spatison@users.noreply.github.com> +// SPDX-FileCopyrightText: 2025 gus +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +using System.Linq; +using System.Numerics; +using Content.Client.Stealth; +using Content.Shared._Goobstation.Overlays; +using Content.Shared.Body.Components; +using Content.Shared.Stealth.Components; +using Robust.Client.GameObjects; +using Robust.Client.Graphics; +using Robust.Client.Player; +using Robust.Shared.Enums; +using Robust.Shared.Map; +using Robust.Shared.Timing; + +namespace Content.Client._Goobstation.Overlays; + +public sealed class ThermalVisionOverlay : Overlay +{ + [Dependency] private readonly IEntityManager _entity = default!; + [Dependency] private readonly IPlayerManager _player = default!; + [Dependency] private readonly IGameTiming _timing = default!; + + private readonly TransformSystem _transform; + private readonly StealthSystem _stealth; + private readonly ContainerSystem _container; + private readonly SharedPointLightSystem _light; + + public override bool RequestScreenTexture => true; + public override OverlaySpace Space => OverlaySpace.WorldSpace; + + private readonly List _entries = []; + + private EntityUid? _lightEntity; + + public float LightRadius; + + public ThermalVisionComponent? Comp; + + public ThermalVisionOverlay() + { + IoCManager.InjectDependencies(this); + + _container = _entity.System(); + _transform = _entity.System(); + _stealth = _entity.System(); + _light = _entity.System(); + + ZIndex = -1; + } + + protected override void Draw(in OverlayDrawArgs args) + { + if (ScreenTexture is null || Comp is null) + return; + + var worldHandle = args.WorldHandle; + var eye = args.Viewport.Eye; + + if (eye == null) + return; + + var player = _player.LocalEntity; + + if (!_entity.TryGetComponent(player, out TransformComponent? playerXform)) + return; + + var accumulator = Math.Clamp(Comp.PulseAccumulator, 0f, Comp.PulseTime); + var alpha = Comp.PulseTime <= 0f ? 1f : float.Lerp(1f, 0f, accumulator / Comp.PulseTime); + + // Thermal vision grants some night vision (clientside light) + if (LightRadius > 0) + { + _lightEntity ??= _entity.SpawnAttachedTo(null, playerXform.Coordinates); + _transform.SetParent(_lightEntity.Value, player.Value); + var light = _entity.EnsureComponent(_lightEntity.Value); + _light.SetRadius(_lightEntity.Value, LightRadius, light); + _light.SetEnergy(_lightEntity.Value, alpha, light); + _light.SetColor(_lightEntity.Value, Comp.Color, light); + } + else + ResetLight(); + + var mapId = eye.Position.MapId; + var eyeRot = eye.Rotation; + + _entries.Clear(); + var entities = _entity.EntityQueryEnumerator(); + while (entities.MoveNext(out var uid, out var body, out var sprite, out var xform)) + { + if (!CanSee(uid, sprite)) + continue; + + var entity = uid; + + if (_container.TryGetOuterContainer(uid, xform, out var container)) + { + var owner = container.Owner; + if (_entity.TryGetComponent(owner, out var ownerSprite) + && _entity.TryGetComponent(owner, out var ownerXform)) + { + entity = owner; + sprite = ownerSprite; + xform = ownerXform; + } + } + + if (_entries.Any(e => e.Ent.Owner == entity)) + continue; + + _entries.Add(new ThermalVisionRenderEntry((entity, sprite, xform), mapId, eyeRot)); + } + + foreach (var entry in _entries) + { + Render(entry.Ent, entry.Map, worldHandle, entry.EyeRot, Comp.Color, alpha); + } + + worldHandle.SetTransform(Matrix3x2.Identity); + } + + private void Render(Entity ent, + MapId? map, + DrawingHandleWorld handle, + Angle eyeRot, + Color color, + float alpha) + { + var (uid, sprite, xform) = ent; + if (xform.MapID != map || !CanSee(uid, sprite)) + return; + + var position = _transform.GetWorldPosition(xform); + var rotation = _transform.GetWorldRotation(xform); + + + var originalColor = sprite.Color; + sprite.Color = color.WithAlpha(alpha); + sprite.Render(handle, eyeRot, rotation, position: position); + sprite.Color = originalColor; + } + + private bool CanSee(EntityUid uid, SpriteComponent sprite) + { + return sprite.Visible && (!_entity.TryGetComponent(uid, out StealthComponent? stealth) || + _stealth.GetVisibility(uid, stealth) > 0.5f); + } + + public void ResetLight(bool checkFirstTimePredicted = true) + { + if (_lightEntity == null || checkFirstTimePredicted && !_timing.IsFirstTimePredicted) + return; + + _entity.DeleteEntity(_lightEntity); + _lightEntity = null; + } +} + +public record struct ThermalVisionRenderEntry( + Entity Ent, + MapId? Map, + Angle EyeRot); diff --git a/Content.Client/_Goobstation/Overlays/ThermalVisionSystem.cs b/Content.Client/_Goobstation/Overlays/ThermalVisionSystem.cs new file mode 100644 index 0000000000..873db395e2 --- /dev/null +++ b/Content.Client/_Goobstation/Overlays/ThermalVisionSystem.cs @@ -0,0 +1,120 @@ +// SPDX-FileCopyrightText: 2025 Aiden <28298836+Aidenkrz@users.noreply.github.com> +// SPDX-FileCopyrightText: 2025 Aviu00 <93730715+Aviu00@users.noreply.github.com> +// SPDX-FileCopyrightText: 2025 Misandry +// SPDX-FileCopyrightText: 2025 Spatison <137375981+Spatison@users.noreply.github.com> +// SPDX-FileCopyrightText: 2025 gus +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +using Content.Client.Overlays; +using Content.Shared._Goobstation.Overlays; +using Content.Shared.Inventory; +using Content.Shared.Inventory.Events; +using Robust.Client.Graphics; + +namespace Content.Client._Goobstation.Overlays; + +public sealed class ThermalVisionSystem : EquipmentHudSystem +{ + [Dependency] private readonly IOverlayManager _overlayMan = default!; + + private ThermalVisionOverlay _thermalOverlay = default!; + private BaseSwitchableOverlay _overlay = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnToggle); + + _thermalOverlay = new ThermalVisionOverlay(); + _overlay = new BaseSwitchableOverlay(); + } + + protected override void OnRefreshComponentHud(Entity ent, + ref RefreshEquipmentHudEvent args) + { + if (!ent.Comp.IsEquipment) + base.OnRefreshComponentHud(ent, ref args); + } + + protected override void OnRefreshEquipmentHud(Entity ent, + ref InventoryRelayedEvent> args) + { + if (ent.Comp.IsEquipment) + base.OnRefreshEquipmentHud(ent, ref args); + } + + private void OnToggle(Entity ent, ref SwitchableOverlayToggledEvent args) + { + RefreshOverlay(); + } + + protected override void UpdateInternal(RefreshEquipmentHudEvent args) + { + base.UpdateInternal(args); + ThermalVisionComponent? tvComp = null; + var lightRadius = 0f; + foreach (var comp in args.Components) + { + if (!comp.IsActive && (comp.PulseTime <= 0f || comp.PulseAccumulator >= comp.PulseTime)) + continue; + + if (tvComp == null) + tvComp = comp; + else if (!tvComp.DrawOverlay && comp.DrawOverlay) + tvComp = comp; + else if (tvComp.DrawOverlay == comp.DrawOverlay && tvComp.PulseTime > 0f && comp.PulseTime <= 0f) + tvComp = comp; + + lightRadius = MathF.Max(lightRadius, comp.LightRadius); + } + + UpdateThermalOverlay(tvComp, lightRadius); + UpdateOverlay(tvComp); + } + + protected override void DeactivateInternal() + { + base.DeactivateInternal(); + + _thermalOverlay.ResetLight(false); + UpdateOverlay(null); + UpdateThermalOverlay(null, 0f); + } + + private void UpdateThermalOverlay(ThermalVisionComponent? comp, float lightRadius) + { + _thermalOverlay.LightRadius = lightRadius; + _thermalOverlay.Comp = comp; + + switch (comp) + { + case not null when !_overlayMan.HasOverlay(): + _overlayMan.AddOverlay(_thermalOverlay); + break; + case null: + _overlayMan.RemoveOverlay(_thermalOverlay); + _thermalOverlay.ResetLight(); + break; + } + } + + private void UpdateOverlay(ThermalVisionComponent? tvComp) + { + _overlay.Comp = tvComp; + + switch (tvComp) + { + case { DrawOverlay: true } when !_overlayMan.HasOverlay>(): + _overlayMan.AddOverlay(_overlay); + break; + case null or { DrawOverlay: false }: + _overlayMan.RemoveOverlay(_overlay); + break; + } + + // Night vision overlay is prioritized + _overlay.IsActive = !_overlayMan.HasOverlay>(); + } +} diff --git a/Content.Server/Flash/FlashSystem.cs b/Content.Server/Flash/FlashSystem.cs index f8819bbdc8..200647aaa9 100644 --- a/Content.Server/Flash/FlashSystem.cs +++ b/Content.Server/Flash/FlashSystem.cs @@ -22,6 +22,7 @@ using Robust.Shared.Audio; using Robust.Shared.Random; using InventoryComponent = Content.Shared.Inventory.InventoryComponent; using Robust.Shared.Prototypes; +using Content.Shared._Goobstation.Flashbang; namespace Content.Server.Flash { @@ -132,8 +133,14 @@ namespace Content.Server.Flash if (attempt.Cancelled) return; + // Goobstation start + var multiplierEv = new FlashDurationMultiplierEvent(); + RaiseLocalEvent(target, multiplierEv); + var multiplier = multiplierEv.Multiplier; + // Goobstation end + // don't paralyze, slowdown or convert to rev if the target is immune to flashes - if (!_statusEffectsSystem.TryAddStatusEffect(target, FlashedKey, TimeSpan.FromSeconds(flashDuration / 1000f), true) && !ignoreProtection) //DeltaV: allow flashing to ignore flash protection + if (!_statusEffectsSystem.TryAddStatusEffect(target, FlashedKey, TimeSpan.FromSeconds(flashDuration * multiplier / 1000f), true) && !ignoreProtection) //DeltaV: allow flashing to ignore flash protection return; if (stunDuration != null) @@ -142,7 +149,7 @@ namespace Content.Server.Flash } else { - _stun.TrySlowdown(target, TimeSpan.FromSeconds(flashDuration / 1000f), true, + _stun.TrySlowdown(target, TimeSpan.FromSeconds(flashDuration * multiplier / 1000f), true, slowTo, slowTo); } diff --git a/Content.Server/_DV/Abilities/Psionics/PsychokineticScreamPowerSystem.cs b/Content.Server/_DV/Abilities/Psionics/PsychokineticScreamPowerSystem.cs new file mode 100644 index 0000000000..134aa1fb26 --- /dev/null +++ b/Content.Server/_DV/Abilities/Psionics/PsychokineticScreamPowerSystem.cs @@ -0,0 +1,56 @@ +using Content.Shared._DV.Abilities; +using Content.Shared.Abilities.Psionics; +using Content.Shared.Actions; +using Robust.Server.Audio; + +namespace Content.Server._DV.Abilities; + +public sealed partial class PsychokineticScreamPowerSystem : EntitySystem +{ + [Dependency] private readonly AudioSystem _audio = default!; + [Dependency] private readonly SharedActionsSystem _actions = default!; + [Dependency] private readonly ShatterLightsAbilitySystem _shatterLights = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnMapInit); + SubscribeLocalEvent(OnShutdown); + SubscribeLocalEvent(OnShatterLightsAction); + } + + private void OnMapInit(Entity ent, ref MapInitEvent args) + { + _actions.AddAction(ent, ref ent.Comp.PsychokineticScreamActionEntity, ent.Comp.ShatterLightsActionId); + _actions.StartUseDelay(ent.Comp.PsychokineticScreamActionEntity); + + if (TryComp(ent, out var psionic) && psionic.PsionicAbility == null) + { + psionic.PsionicAbility = ent.Comp.PsychokineticScreamActionEntity; + psionic.ActivePowers.Add(ent.Comp); + } + } + + private void OnShutdown(Entity entity, ref ComponentShutdown args) + { + _actions.RemoveAction(entity.Owner, entity.Comp.PsychokineticScreamActionEntity); + if (TryComp(entity, out var psionic)) + { + psionic.ActivePowers.Remove(entity.Comp); + } + } + + private void OnShatterLightsAction(Entity entity, ref ShatterLightsActionEvent args) + { + if (args.Handled) + return; + + if (entity.Comp.AbilitySound != null) + _audio.PlayPvs(entity.Comp.AbilitySound, entity); + + _shatterLights.ShatterLightsAround(entity.Owner, entity.Comp.Radius, entity.Comp.LineOfSight); + args.Handled = true; + } + +} diff --git a/Content.Server/_DV/Abilities/ShatterLightsAbilitySystem.cs b/Content.Server/_DV/Abilities/ShatterLightsAbilitySystem.cs new file mode 100644 index 0000000000..1e3b310af1 --- /dev/null +++ b/Content.Server/_DV/Abilities/ShatterLightsAbilitySystem.cs @@ -0,0 +1,82 @@ +using Content.Server.Light.Components; +using Content.Server.Light.EntitySystems; +using Content.Shared._DV.Abilities; +using Content.Shared.Actions; +using Content.Shared.Physics; +using Robust.Server.Audio; +using Robust.Shared.Map; +using Robust.Shared.Physics; +using Robust.Shared.Physics.Systems; +using System.Linq; +using System.Numerics; + +namespace Content.Server._DV.Abilities; + +public sealed partial class ShatterLightsAbilitySystem : EntitySystem +{ + [Dependency] private readonly AudioSystem _audio = default!; + [Dependency] private readonly EntityLookupSystem _lookup = default!; + [Dependency] private readonly PoweredLightSystem _light = default!; + [Dependency] private readonly SharedActionsSystem _actions = default!; + [Dependency] private readonly SharedPhysicsSystem _physics = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + + private readonly HashSet> _lightsInRange = new(); + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnMapInit); + SubscribeLocalEvent(OnShutdown); + SubscribeLocalEvent(OnShatterLightsAction); + } + + private void OnMapInit(Entity ent, ref MapInitEvent args) + { + _actions.AddAction(ent, ref ent.Comp.ShatterLightsActionEntity, ent.Comp.ShatterLightsActionId); + _actions.StartUseDelay(ent.Comp.ShatterLightsActionEntity); + } + + private void OnShutdown(Entity entity, ref ComponentShutdown args) + { + _actions.RemoveAction(entity.Owner, entity.Comp.ShatterLightsActionEntity); + } + + private void OnShatterLightsAction(Entity entity, ref ShatterLightsActionEvent args) + { + if (args.Handled) + return; + + if (entity.Comp.AbilitySound != null) + _audio.PlayPvs(entity.Comp.AbilitySound, entity); + + ShatterLightsAround(entity.Owner, entity.Comp.Radius, entity.Comp.LineOfSight); + args.Handled = true; + } + + public void ShatterLightsAround(EntityUid center, float range, bool lineOfSight) + { + var pos = _transform.GetWorldPosition(center); + + // Get all light entities within the specified radius + _lightsInRange.Clear(); + _lookup.GetEntitiesInRange(Transform(center).Coordinates, range, _lightsInRange); + foreach (var light in _lightsInRange) + { + if (lineOfSight) // If LoS is required, test it. + { + var lightPos = _transform.GetWorldPosition(light); + var sqrDistance = Vector2.DistanceSquared(pos, lightPos); + var ray = new CollisionRay(pos, (lightPos - pos).Normalized(), (int)CollisionGroup.Opaque); + var hit = _physics.IntersectRay(_transform.GetMapId(center), ray, MathF.Sqrt(sqrDistance) - 0.5f, returnOnFirstHit: true); + if (hit.Any() && hit.First().Distance != 0) + continue; + } + + // If we reach here, the light is unobstructed and within range, break it. + _light.TryDestroyBulb(light, light.Comp); + } + } + +} diff --git a/Content.Server/_DV/Abilities/TechnokineticPulseAbilitySystem.cs b/Content.Server/_DV/Abilities/TechnokineticPulseAbilitySystem.cs new file mode 100644 index 0000000000..89767d3c14 --- /dev/null +++ b/Content.Server/_DV/Abilities/TechnokineticPulseAbilitySystem.cs @@ -0,0 +1,43 @@ +using Content.Server.Emp; +using Content.Shared._DV.Abilities; +using Content.Shared.Actions; + +namespace Content.Server._DV.Abilities; + +public sealed partial class TechnokineticPulseAbilitySystem : EntitySystem +{ + [Dependency] private readonly SharedActionsSystem _actions = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + [Dependency] private readonly EmpSystem _emp = default!; + + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnMapInit); + SubscribeLocalEvent(OnShutdown); + SubscribeLocalEvent(OnTechnokineticPulseAction); + } + + private void OnMapInit(Entity ent, ref MapInitEvent args) + { + _actions.AddAction(ent, ref ent.Comp.TechnokineticPulseActionEntity, ent.Comp.TechnokineticPulseActionId); + _actions.StartUseDelay(ent.Comp.TechnokineticPulseActionEntity); + } + + private void OnShutdown(Entity entity, ref ComponentShutdown args) + { + _actions.RemoveAction(entity.Owner, entity.Comp.TechnokineticPulseActionEntity); + } + + private void OnTechnokineticPulseAction(Entity entity, ref TechnokineticPulseActionEvent args) + { + if (args.Handled) + return; + + _emp.EmpPulse(_transform.GetMapCoordinates(entity), entity.Comp.Range, entity.Comp.EnergyConsumption, entity.Comp.DisableDuration); + + args.Handled = true; + } +} diff --git a/Content.Server/_DV/Body/LightLevelHealthSystem.cs b/Content.Server/_DV/Body/LightLevelHealthSystem.cs new file mode 100644 index 0000000000..1f7895a505 --- /dev/null +++ b/Content.Server/_DV/Body/LightLevelHealthSystem.cs @@ -0,0 +1,45 @@ +using Content.Shared._DV.Body; +using Content.Shared._DV.Light; +using Content.Shared.Mobs.Systems; +using Content.Shared.Movement.Systems; +using Content.Shared.Popups; +using Robust.Shared.Timing; + +namespace Content.Server._DV.Body; + +public sealed class LightLevelHealthSystem : SharedLightLevelHealthSystem +{ + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly MobStateSystem _mobState = default!; + [Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifier = default!; + [Dependency] private readonly SharedLightReactiveSystem _lightReactive = default!; + + private TimeSpan _nextUpdate = TimeSpan.MinValue; + public override void Update(float frameTime) + { + if (_timing.CurTime < _nextUpdate) + return; + _nextUpdate = _timing.CurTime + TimeSpan.FromSeconds(1); + + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var comp)) + { + if (_mobState.IsDead(uid)) + continue; // Don't apply damage if the mob is dead + // Get the light level at the entity's position + var lightLevel = _lightReactive.GetLightLevel(uid, true); + + int currentThreshold = CurrentThreshold(lightLevel, comp); + if (currentThreshold != comp.CurrentThreshold) + { + comp.CurrentThreshold = currentThreshold; + _movementSpeedModifier.RefreshMovementSpeedModifiers(uid); + } + + if (currentThreshold == -1) + TryDealDamage(new(uid, comp), comp.DarkDamage); + if (currentThreshold == 1) + TryDealDamage(new(uid, comp), comp.LightDamage); + } + } +} diff --git a/Content.Server/_DV/GameTicking/Rules/Components/SkiaRuleComponent.cs b/Content.Server/_DV/GameTicking/Rules/Components/SkiaRuleComponent.cs new file mode 100644 index 0000000000..420705a942 --- /dev/null +++ b/Content.Server/_DV/GameTicking/Rules/Components/SkiaRuleComponent.cs @@ -0,0 +1,4 @@ +namespace Content.Server._DV.GameTicking.Rules.Components; + +[RegisterComponent] +public sealed partial class SkiaRuleComponent : Component; diff --git a/Content.Server/_DV/Light/BreakLightsOnSpawnSystem.cs b/Content.Server/_DV/Light/BreakLightsOnSpawnSystem.cs new file mode 100644 index 0000000000..b240782443 --- /dev/null +++ b/Content.Server/_DV/Light/BreakLightsOnSpawnSystem.cs @@ -0,0 +1,20 @@ +using Content.Server._DV.Abilities; +using Content.Shared._DV.Light; + +namespace Content.Server._DV.Light; + +public sealed partial class BreakLightsOnSpawnSystem : EntitySystem +{ + [Dependency] private readonly ShatterLightsAbilitySystem _shatterLights = default!; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnMapInit); + } + + private void OnMapInit(Entity entity, ref MapInitEvent args) + { + _shatterLights.ShatterLightsAround(entity.Owner, entity.Comp.Radius, entity.Comp.LineOfSight); + } +} diff --git a/Content.Server/_DV/Light/LightReactiveSystem.cs b/Content.Server/_DV/Light/LightReactiveSystem.cs new file mode 100644 index 0000000000..f2a062c054 --- /dev/null +++ b/Content.Server/_DV/Light/LightReactiveSystem.cs @@ -0,0 +1,24 @@ +using Content.Shared._DV.Light; +using Robust.Server.GameObjects; + +namespace Content.Server._DV.Light; + +public sealed partial class LightReactiveSystem : SharedLightReactiveSystem +{ + [Dependency] private readonly EntityLookupSystem _lookup = default!; + + private readonly HashSet> _lightsInRange = new(); + private readonly HashSet> _validLightsInRange = new(); + public override HashSet> GetLights(EntityUid targetEntity) + { + _lightsInRange.Clear(); + _lookup.GetEntitiesInRange(Transform(targetEntity).Coordinates, 10f, _lightsInRange); + _validLightsInRange.Clear(); + foreach (var light in _lightsInRange) + { + if(light.Comp.Enabled && !light.Comp.Deleted && light.Comp.NetSyncEnabled) + _validLightsInRange.Add(new(light.Owner, light.Comp)); + } + return _validLightsInRange; + } +} diff --git a/Content.Server/_DV/Objectives/Components/RerollAfterCompletionComponent.cs b/Content.Server/_DV/Objectives/Components/RerollAfterCompletionComponent.cs new file mode 100644 index 0000000000..a8ca7fac19 --- /dev/null +++ b/Content.Server/_DV/Objectives/Components/RerollAfterCompletionComponent.cs @@ -0,0 +1,39 @@ +using Robust.Shared.Prototypes; + +namespace Content.Server._DV.Objectives.Components; + +/// +/// When this objective is completed, duplicate it with a new target. +/// +[RegisterComponent] +public sealed partial class RerollAfterCompletionComponent : Component +{ + /// + /// If true, the objective has already been rerolled. + /// + /// + /// Ideally this shouldn't matter, as we delete the component once its rolled + /// + public bool Rerolled; + + /// + /// Tracks a reference of the owner of this objective. + /// From what I can see, there is no normaly way to get a mind from an objective, as they're usually passed together. + /// + [DataField] + public EntityUid MindUid = default!; + + /// + /// Prototype of the objective to use for rerolling. + /// Probably the same as this entity (If you want a potentially infinite number of objectives), but could be different if you want it to be a different objective. + /// + [DataField(required: true)] + public EntProtoId RerollObjectivePrototype = default!; + + /// + /// Message to display when the objective is rerolled. + /// If null, no message will be displayed. + /// + [DataField] + public LocId? RerollObjectiveMessage; +} diff --git a/Content.Server/_DV/Objectives/Systems/RerollAfterCompletionSystem.cs b/Content.Server/_DV/Objectives/Systems/RerollAfterCompletionSystem.cs new file mode 100644 index 0000000000..534e739989 --- /dev/null +++ b/Content.Server/_DV/Objectives/Systems/RerollAfterCompletionSystem.cs @@ -0,0 +1,95 @@ +using Content.Server._DV.Objectives.Components; +using Content.Server.Objectives.Components; +using Content.Server.Roles.Jobs; +using Content.Shared.Mind; +using Content.Shared.Objectives.Components; +using Content.Shared.Objectives.Systems; +using Content.Shared.Popups; +using Robust.Shared.Prototypes; + +namespace Content.Server._DV.Objectives.Systems; + +public sealed class RerollAfterCompletionSystem : EntitySystem +{ + [Dependency] private readonly SharedObjectivesSystem _objectives = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly JobSystem _job = default!; + [Dependency] private readonly SharedMindSystem _mind = default!; + + private readonly HashSet _objectivesToAdd = new(); + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnObjectiveAfterAssign); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + _objectivesToAdd.Clear(); + var query = EntityQueryEnumerator(); + + while (query.MoveNext(out var uid, out var component)) + { + if (component.Rerolled) // If already rerolled, skip. + continue; + + if (!HasComp(uid)) + continue; // If the entity doesn't have an ObjectiveComponent, skip. + + if (!TryComp(component.MindUid, out var mind)) + continue; // If the mind component is missing, skip. + + // Check that this objective has been completed. + if (!_objectives.IsCompleted(uid, new(component.MindUid, mind))) + continue; + + // Destroy this commponent as it is no longer needed, and this will speed up the next check. + RemCompDeferred(uid); + + component.Rerolled = true; + + // I'd be a lot happier if I could do all the rerolling here + // But creating the new objective causes the Query to freak out + // And I need the objective to do everything else. + _objectivesToAdd.Add(component); + } + + foreach (var component in _objectivesToAdd) + { + var mind = component.MindUid; + if (!TryComp(mind, out var mindComponent)) + continue; + // Create a new objective with the specified prototype. + if (_objectives.TryCreateObjective(mind, mindComponent, component.RerollObjectivePrototype) is not { } newObjUid) + continue; + if (component.RerollObjectiveMessage is null) + continue; + + var bodyUid = mindComponent.CurrentEntity ?? component.MindUid; + + // Check if this has a target component, and if so, get it's name for Localization. + if (TryComp(newObjUid, out var targetComp) && TryComp(targetComp.Target, out var targetMindComp)) + { + var newTarget = targetMindComp.CharacterName ?? "Unknown"; + var targetJob = _job.MindTryGetJobName(targetComp.Target); + _popup.PopupEntity(Loc.GetString(component.RerollObjectiveMessage, ("targetName", newTarget), ("job", targetJob)), bodyUid, bodyUid, PopupType.Large); + } + else + { + _popup.PopupEntity(Loc.GetString(component.RerollObjectiveMessage), bodyUid, bodyUid, PopupType.Large); + } + _mind.AddObjective(mind, mindComponent, newObjUid); + } + } + + private void OnObjectiveAfterAssign(EntityUid uid, RerollAfterCompletionComponent comp, ref ObjectiveAfterAssignEvent args) + { + // If the objective is assigned, we can set the mind UID. + if (args.Mind != null) + comp.MindUid = args.MindId; + } +} diff --git a/Content.Server/_DV/Roles/SkiaRoleComponent.cs b/Content.Server/_DV/Roles/SkiaRoleComponent.cs new file mode 100644 index 0000000000..0280a7a7ad --- /dev/null +++ b/Content.Server/_DV/Roles/SkiaRoleComponent.cs @@ -0,0 +1,9 @@ +using Content.Shared.Roles; + +namespace Content.Shared._DV.Roles; + +/// +/// Added to mind role entities to tag that they are a Skia. +/// +[RegisterComponent] +public sealed partial class SkiaRoleComponent : BaseMindRoleComponent; diff --git a/Content.Shared/_DV/Abilities/Psionics/PsychokineticScreamPowerComponent.cs b/Content.Shared/_DV/Abilities/Psionics/PsychokineticScreamPowerComponent.cs new file mode 100644 index 0000000000..48f7ebc07c --- /dev/null +++ b/Content.Shared/_DV/Abilities/Psionics/PsychokineticScreamPowerComponent.cs @@ -0,0 +1,42 @@ +using Content.Shared.Actions; +using Robust.Shared.Audio; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared._DV.Abilities; + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class PsychokineticScreamPowerComponent : Component +{ + /// + /// The radius in which lights will be broken. + /// + [DataField] + public float Radius = 10f; + + /// + /// If true, lights will only be broken if the entity has line of sight to them. + /// + [DataField] + public bool LineOfSight = false; + + /// + /// The action that triggers the psychokinetic scream ability. + /// + [DataField] + public EntProtoId ShatterLightsActionId = "ActionPsychokineticScream"; + + /// + /// Standing reference to the action entity, if it exists. + /// + [DataField, AutoNetworkedField] + public EntityUid? PsychokineticScreamActionEntity; + + /// + /// The sound to play when the ability is used. + /// + [DataField] + public SoundSpecifier AbilitySound = new SoundPathSpecifier("/Audio/_DV/Effects/creepyshriek.ogg"); +} + +public sealed partial class ShatterLightsActionEvent : InstantActionEvent; diff --git a/Content.Shared/_DV/Abilities/ShatterLightsAbilityComponent.cs b/Content.Shared/_DV/Abilities/ShatterLightsAbilityComponent.cs new file mode 100644 index 0000000000..fb5412ef52 --- /dev/null +++ b/Content.Shared/_DV/Abilities/ShatterLightsAbilityComponent.cs @@ -0,0 +1,42 @@ +using Content.Shared.Actions; +using Robust.Shared.Audio; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared._DV.Abilities; + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class ShatterLightsAbilityComponent : Component +{ + /// + /// The radius in which lights will be broken. + /// + [DataField] + public float Radius = 10f; + + /// + /// If true, lights will only be broken if the entity has line of sight to them. + /// + [DataField] + public bool LineOfSight = false; + + /// + /// The action that triggers the shatter lights ability. + /// + [DataField] + public EntProtoId ShatterLightsActionId = "ActionShatterLights"; + + /// + /// Standing reference to the action entity, if it exists. + /// + [DataField, AutoNetworkedField] + public EntityUid? ShatterLightsActionEntity; + + /// + /// The sound to play when the ability is used. + /// + [DataField] + public SoundSpecifier AbilitySound = new SoundPathSpecifier("/Audio/_DV/Effects/creepyshriek.ogg"); +} + +public sealed partial class ShatterLightsActionEvent : InstantActionEvent; diff --git a/Content.Shared/_DV/Abilities/TechnokineticPulseAbilityComponent.cs b/Content.Shared/_DV/Abilities/TechnokineticPulseAbilityComponent.cs new file mode 100644 index 0000000000..e41e089f8b --- /dev/null +++ b/Content.Shared/_DV/Abilities/TechnokineticPulseAbilityComponent.cs @@ -0,0 +1,42 @@ +using Content.Shared.Actions; +using Robust.Shared.Audio; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared._DV.Abilities; + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class TechnokineticPulseAbilityComponent : Component +{ + /// + /// The radius in which to EMP + /// + [DataField] + public float Range = 1.0f; + + /// + /// The amount of power to drain from batteries + /// + [DataField] + public float EnergyConsumption = 20000; + + /// + /// The duration for which devices are disabled. + /// + [DataField] + public float DisableDuration = 20f; + + /// + /// The action that triggers the technokinetic pulse ability. + /// + [DataField] + public EntProtoId TechnokineticPulseActionId = "ActionTechnokineticPulse"; + + /// + /// Standing reference to the action entity, if it exists. + /// + [DataField, AutoNetworkedField] + public EntityUid? TechnokineticPulseActionEntity; +} + +public sealed partial class TechnokineticPulseActionEvent : InstantActionEvent; diff --git a/Content.Shared/_DV/Body/LightLevelDamageMultComponent.cs b/Content.Shared/_DV/Body/LightLevelDamageMultComponent.cs new file mode 100644 index 0000000000..63655e4b5a --- /dev/null +++ b/Content.Shared/_DV/Body/LightLevelDamageMultComponent.cs @@ -0,0 +1,30 @@ +using Content.Shared.Damage; +using Robust.Shared.GameStates; + +namespace Content.Shared._DV.Body; + +/// +/// Component that allows a body to deal or receive modified damage amounts based on their light level. +/// Requires to function. +/// +[RegisterComponent, NetworkedComponent] +public sealed partial class LightLevelDamageMultComponent : Component +{ + [DataField] + public float DarkReceivedMultiplier = 1.0f; + [DataField] + public float LightReceivedMultiplier = 1.0f; + [DataField] + public float LightDealtMultiplier = 1.0f; + [DataField] + public float DarkDealtMultiplier = 1.0f; + + [DataField] + public DamageModifierSet? DarkReceivedModifiers = default!; + [DataField] + public DamageModifierSet? LightReceivedModifiers = default!; + [DataField] + public DamageModifierSet? DarkDealtModifiers = default!; + [DataField] + public DamageModifierSet? LightDealtModifiers = default!; +} diff --git a/Content.Shared/_DV/Body/LightLevelHealthComponent.cs b/Content.Shared/_DV/Body/LightLevelHealthComponent.cs new file mode 100644 index 0000000000..d9d766c45a --- /dev/null +++ b/Content.Shared/_DV/Body/LightLevelHealthComponent.cs @@ -0,0 +1,58 @@ +using Content.Shared.Damage; +using Robust.Shared.Audio; +using Robust.Shared.GameStates; + +namespace Content.Shared._DV.Body; + +/// +/// Component that allows a body to have health that is affected by light levels. +/// Either damaged or healed by certain light levels. +/// This is used for the Skia, which is a creature that is harmed by light. +/// +[RegisterComponent, NetworkedComponent] +public sealed partial class LightLevelHealthComponent : Component +{ + /// + /// Level of light that, when below, we are considered in darkness. + /// + [DataField] + public float DarkThreshold = 0.2f; + /// + /// Level of light that, when above, we are considered in light. + /// + [DataField] + public float LightThreshold = 0.8f; + /// + /// Amount of health or damage per second when in darkness. Positive values harm, negative values heal. + /// + [DataField(required: true)] + public DamageSpecifier DarkDamage = default!; + /// + /// Amount of health or damage per second when in light. Positive values harm, negative values heal. + /// + [DataField(required: true)] + public DamageSpecifier LightDamage = default!; + /// + /// Movement speed multiplier when in darkness. + /// + [DataField] + public float DarkMovementSpeedMultiplier = 1.0f; + /// + /// Movement speed multiplier when in light. + /// + [DataField] + public float LightMovementSpeedMultiplier = 1.0f; + /// + /// Sound to play when the entity is damaged by light or darkness. + /// + [DataField] + public SoundSpecifier SizzleSoundPath = new SoundPathSpecifier("/Audio/Effects/lightburn.ogg"); + + /// + /// The current light threshhold for this component. + /// -1 for darkness, 1 for light. + /// 0 for neither. + /// + [DataField] + public int CurrentThreshold = 0; +} diff --git a/Content.Shared/_DV/Body/SharedLightLevelHealthSystem.cs b/Content.Shared/_DV/Body/SharedLightLevelHealthSystem.cs new file mode 100644 index 0000000000..415b101d97 --- /dev/null +++ b/Content.Shared/_DV/Body/SharedLightLevelHealthSystem.cs @@ -0,0 +1,102 @@ +using Content.Shared._DV.Body; +using Content.Shared._DV.Light; +using Content.Shared.Damage; +using Content.Shared.Mobs.Systems; +using Content.Shared.Movement.Systems; +using Content.Shared.Popups; +using Content.Shared.Weapons.Melee.Events; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Timing; + +namespace Content.Shared._DV.Body; + +public abstract class SharedLightLevelHealthSystem : EntitySystem +{ + [Dependency] private readonly DamageableSystem _damageable = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnGetMoveSpeedModifiers); + SubscribeLocalEvent(OnDamageModify); + SubscribeLocalEvent(OnGetMeleeDamage); + } + + public int CurrentThreshold(float lightLevel, LightLevelHealthComponent comp) + { + bool lowLight = lightLevel < comp.DarkThreshold; + bool highLight = lightLevel > comp.LightThreshold; + return lowLight && !highLight ? -1 + : !lowLight && highLight ? 1 + : 0; + } + + public void TryDealDamage(Entity target, DamageSpecifier damage) + { + if (damage.AnyPositive()) + { + _audio.PlayPvs(target.Comp.SizzleSoundPath, target); + } + _damageable.TryChangeDamage(target, damage, true, false); + } + + private void OnGetMoveSpeedModifiers(Entity ent, ref RefreshMovementSpeedModifiersEvent args) + { + if (!TryComp(ent, out var lightReactive)) + return; + + if (lightReactive.CurrentLightLevel < ent.Comp.DarkThreshold) + args.ModifySpeed(ent.Comp.DarkMovementSpeedMultiplier); + else if (lightReactive.CurrentLightLevel > ent.Comp.LightThreshold) + args.ModifySpeed(ent.Comp.LightMovementSpeedMultiplier); + } + + private void OnDamageModify(Entity ent, ref DamageModifyEvent args) + { + if (!TryComp(ent, out var lightHealth)) + return; + + // On the receiving end. + args.Damage *= lightHealth.CurrentThreshold switch + { + -1 => ent.Comp.DarkReceivedMultiplier, + 1 => ent.Comp.LightReceivedMultiplier, + _ => 1.0f + }; + + var modifiers = lightHealth.CurrentThreshold switch + { + -1 => ent.Comp.DarkReceivedModifiers, + 1 => ent.Comp.LightReceivedModifiers, + _ => null + }; + + if (modifiers != null) + args.Damage = DamageSpecifier.ApplyModifierSet(args.Damage, modifiers); + } + + private void OnGetMeleeDamage(Entity ent, ref GetMeleeDamageEvent args) + { + if (!TryComp(ent, out var lightHealth)) + return; + + args.Damage *= lightHealth.CurrentThreshold switch + { + -1 => ent.Comp.DarkDealtMultiplier, + 1 => ent.Comp.LightDealtMultiplier, + _ => 1.0f + }; + + var modifiers = lightHealth.CurrentThreshold switch + { + -1 => ent.Comp.DarkDealtModifiers, + 1 => ent.Comp.LightDealtModifiers, + _ => null + }; + + if (modifiers != null) + args.Damage = DamageSpecifier.ApplyModifierSet(args.Damage, modifiers); + } + +} diff --git a/Content.Shared/_DV/Light/BreakLightsOnSpawnComponent.cs b/Content.Shared/_DV/Light/BreakLightsOnSpawnComponent.cs new file mode 100644 index 0000000000..ae15820281 --- /dev/null +++ b/Content.Shared/_DV/Light/BreakLightsOnSpawnComponent.cs @@ -0,0 +1,19 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared._DV.Light; + +[RegisterComponent, NetworkedComponent] +public sealed partial class BreakLightsOnSpawnComponent : Component +{ + /// + /// The radius in which lights will be broken. + /// + [DataField] + public float Radius = 10f; + + /// + /// If true, lights will only be broken if the entity has line of sight to them. + /// + [DataField] + public bool LineOfSight = false; +} diff --git a/Content.Shared/_DV/Light/LightReactiveComponent.cs b/Content.Shared/_DV/Light/LightReactiveComponent.cs new file mode 100644 index 0000000000..10838ce6cc --- /dev/null +++ b/Content.Shared/_DV/Light/LightReactiveComponent.cs @@ -0,0 +1,44 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared._DV.Light; + +/// +/// A component that reacts to changes in light levels. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(SharedLightReactiveSystem))] +public sealed partial class LightReactiveComponent : Component +{ + /// + /// The frequency at which the component checks for light level changes. + /// There should be very little reason it should be higher than this. + /// + [DataField] + public TimeSpan UpdateFrequency = TimeSpan.FromSeconds(1); + + /// + /// Whether the component should only update while the entity is alive. + /// If false, it will update even if the entity is dead. + /// + [DataField] + public bool OnlyWhileAlive = true; + + /// + /// Should this update its light level automatically, or only when asked to by another system? + /// If true, it will update its light level automatically. + /// If false, it will only update when explicitly requested. + /// + [DataField] + public bool Manual = false; + + /// + /// The next time the component should update. + /// + [AutoNetworkedField] + public TimeSpan NextUpdate = TimeSpan.Zero; + + /// + /// The current light level of this entity. + /// + [DataField] + public float CurrentLightLevel = 0f; +} diff --git a/Content.Shared/_DV/Light/SharedLightReactiveSystem.cs b/Content.Shared/_DV/Light/SharedLightReactiveSystem.cs new file mode 100644 index 0000000000..468c6f203c --- /dev/null +++ b/Content.Shared/_DV/Light/SharedLightReactiveSystem.cs @@ -0,0 +1,109 @@ +using Content.Shared.Mobs.Systems; +using Content.Shared.Physics; +using Robust.Shared.Map.Components; +using Robust.Shared.Physics; +using Robust.Shared.Physics.Systems; +using Robust.Shared.Timing; +using System.Linq; +using System.Numerics; + +namespace Content.Shared._DV.Light; + +public abstract class SharedLightReactiveSystem : EntitySystem +{ + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly MobStateSystem _mobState = default!; + [Dependency] private readonly SharedPhysicsSystem _physics = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + + + private EntityQuery _lightReactive; + + public override void Initialize() + { + base.Initialize(); + _lightReactive = GetEntityQuery(); + } + + public override void Update(float frameTime) + { + + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var comp)) + { + if (_timing.CurTime < comp.NextUpdate) + return; + comp.NextUpdate = _timing.CurTime + TimeSpan.FromSeconds(1); + if (_mobState.IsDead(uid) && comp.OnlyWhileAlive) + continue; // Don't apply damage / healing if the mob is dead + // Get the light level at the entity's position + comp.CurrentLightLevel = GetLightLevelForPoint(uid); + } + } + + public abstract HashSet> GetLights(EntityUid targetEntity); + + /// + /// Gets the current light level of an entity. + /// + /// + /// This is a cached value that is updated periodically. + /// I could add arguments to either: Force check if it's not a ReactiveComponent, or force update, + /// but I don't need them so you don't get them. Add it if you want it. + /// + public float GetLightLevel(EntityUid uid, bool forceUpdate = false) + { + if (_lightReactive.TryComp(uid, out LightReactiveComponent? comp)) + { + if (forceUpdate) + comp.CurrentLightLevel = GetLightLevelForPoint(uid); + return comp.CurrentLightLevel; + } + return 0.0f; + } + + /// + /// Gets the light level at a specific point in the world. + /// Avoid calling this too often, as it can be expensive. + /// + public float GetLightLevelForPoint(EntityUid uid) + { + float val = 0.0f; + // Get the current map entity so we can get a MapLightComponent from it if it has one + var map = _transform.GetMap(uid); + if (TryComp(map, out MapLightComponent? mapLight)) + val += (mapLight.AmbientLightColor.R + mapLight.AmbientLightColor.G + mapLight.AmbientLightColor.B) / 3f; + var pos = _transform.GetWorldPosition(uid); + + foreach (var (lightUid, lightComp) in GetLights(uid)) + { + // Ensure we're on the same grid as the light source + if (_transform.GetMap(lightUid) != map) + continue; + + // Ensure we're within the light's radius. + var lightPos = _transform.GetWorldPosition(lightUid); + var sqrDistance = Vector2.DistanceSquared(pos, lightPos); + if (sqrDistance > lightComp.Radius * lightComp.Radius) + continue; + + if (sqrDistance < 0.01f) + { + // If we're right on top of the light, just add its full energy value. + val += lightComp.Energy; + continue; + } + + // Collision ray check from the entity to the light source + var ray = new CollisionRay(pos, (lightPos - pos).Normalized(), (int)CollisionGroup.Opaque); + var hit = _physics.IntersectRay(_transform.GetMapId(uid), ray, MathF.Sqrt(sqrDistance) - 0.5f, returnOnFirstHit: true); + if (hit.Any() && hit.First().Distance != 0) + continue; + // If we reach here, the light is unobstructed and within range, calculate a light value to add. + val += lightComp.Energy * (1.0f - sqrDistance / (lightComp.Radius * lightComp.Radius)); + } + + + return val; + } +} diff --git a/Content.Shared/_Goobstation/FlashEvents.cs b/Content.Shared/_Goobstation/FlashEvents.cs new file mode 100644 index 0000000000..2d4ce49035 --- /dev/null +++ b/Content.Shared/_Goobstation/FlashEvents.cs @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: 2025 Aiden <28298836+Aidenkrz@users.noreply.github.com> +// SPDX-FileCopyrightText: 2025 Aviu00 <93730715+Aviu00@users.noreply.github.com> +// SPDX-FileCopyrightText: 2025 Misandry +// SPDX-FileCopyrightText: 2025 Spatison <137375981+Spatison@users.noreply.github.com> +// SPDX-FileCopyrightText: 2025 gus +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +using Content.Shared.Inventory; + +namespace Content.Shared._Goobstation.Flashbang; + +public sealed class FlashDurationMultiplierEvent : EntityEventArgs, IInventoryRelayEvent +{ + public float Multiplier = 1f; + + public SlotFlags TargetSlots => SlotFlags.EYES | SlotFlags.HEAD | SlotFlags.MASK; +} diff --git a/Content.Shared/_Goobstation/Inventory/GoobInventorySystem.Relays.cs b/Content.Shared/_Goobstation/Inventory/GoobInventorySystem.Relays.cs new file mode 100644 index 0000000000..6a7a2fa602 --- /dev/null +++ b/Content.Shared/_Goobstation/Inventory/GoobInventorySystem.Relays.cs @@ -0,0 +1,39 @@ + +// SPDX-FileCopyrightText: 2025 Aiden <28298836+Aidenkrz@users.noreply.github.com> +// SPDX-FileCopyrightText: 2025 GoobBot +// SPDX-FileCopyrightText: 2025 Misandry +// SPDX-FileCopyrightText: 2025 Solstice +// SPDX-FileCopyrightText: 2025 SolsticeOfTheWinter +// SPDX-FileCopyrightText: 2025 gus +// SPDX-FileCopyrightText: 2025 pheenty +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +using Content.Shared._Goobstation.Flashbang; +using Content.Shared.Inventory; +using Content.Shared.Inventory.Events; + +namespace Content.Shared._Goobstation.Inventory; + +public partial class GoobInventorySystem +{ + [Dependency] private readonly InventorySystem _inventorySystem = default!; + + public void InitializeRelays() + { + base.Initialize(); + SubscribeLocalEvent(RelayInventoryEvent); + SubscribeLocalEvent>(RefRelayInventoryEvent); + SubscribeLocalEvent>(RefRelayInventoryEvent); + } + + private void RefRelayInventoryEvent(EntityUid uid, InventoryComponent component, ref T args) where T : IInventoryRelayEvent + { + _inventorySystem.RelayEvent((uid, component), ref args); + } + + private void RelayInventoryEvent(EntityUid uid, InventoryComponent component, T args) where T : IInventoryRelayEvent + { + _inventorySystem.RelayEvent((uid, component), args); + } +} diff --git a/Content.Shared/_Goobstation/Inventory/GoobInventorySystem.cs b/Content.Shared/_Goobstation/Inventory/GoobInventorySystem.cs new file mode 100644 index 0000000000..981c28bf2d --- /dev/null +++ b/Content.Shared/_Goobstation/Inventory/GoobInventorySystem.cs @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: 2025 Aiden <28298836+Aidenkrz@users.noreply.github.com> +// SPDX-FileCopyrightText: 2025 Misandry +// SPDX-FileCopyrightText: 2025 gus +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +namespace Content.Shared._Goobstation.Inventory; + +public sealed partial class GoobInventorySystem : EntitySystem +{ + + public override void Initialize() + { + base.Initialize(); + InitializeRelays(); + } +} diff --git a/Content.Shared/_Goobstation/Overlays/BaseVisionOverlayComponent.cs b/Content.Shared/_Goobstation/Overlays/BaseVisionOverlayComponent.cs new file mode 100644 index 0000000000..5469c8f225 --- /dev/null +++ b/Content.Shared/_Goobstation/Overlays/BaseVisionOverlayComponent.cs @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: 2025 Aiden <28298836+Aidenkrz@users.noreply.github.com> +// SPDX-FileCopyrightText: 2025 Aviu00 <93730715+Aviu00@users.noreply.github.com> +// SPDX-FileCopyrightText: 2025 Misandry +// SPDX-FileCopyrightText: 2025 Spatison <137375981+Spatison@users.noreply.github.com> +// SPDX-FileCopyrightText: 2025 gus +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +namespace Content.Shared._Goobstation.Overlays; + +public abstract partial class BaseVisionOverlayComponent : Component +{ + [DataField, ViewVariables(VVAccess.ReadOnly)] + public virtual Vector3 Tint { get; set; } = new(0.3f, 0.3f, 0.3f); + + [DataField, ViewVariables(VVAccess.ReadOnly)] + public virtual float Strength { get; set; } = 2f; + + [DataField, ViewVariables(VVAccess.ReadOnly)] + public virtual float Noise { get; set; } = 0.5f; + + [DataField, ViewVariables(VVAccess.ReadOnly)] + public virtual Color Color { get; set; } = Color.White; +} diff --git a/Content.Shared/_Goobstation/Overlays/NightVisionComponent.cs b/Content.Shared/_Goobstation/Overlays/NightVisionComponent.cs new file mode 100644 index 0000000000..feeeb1109c --- /dev/null +++ b/Content.Shared/_Goobstation/Overlays/NightVisionComponent.cs @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2025 Aiden <28298836+Aidenkrz@users.noreply.github.com> +// SPDX-FileCopyrightText: 2025 Aviu00 <93730715+Aviu00@users.noreply.github.com> +// SPDX-FileCopyrightText: 2025 Misandry +// SPDX-FileCopyrightText: 2025 Spatison <137375981+Spatison@users.noreply.github.com> +// SPDX-FileCopyrightText: 2025 gus +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +using Content.Shared.Actions; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared._Goobstation.Overlays; + +[RegisterComponent, NetworkedComponent] +public sealed partial class NightVisionComponent : SwitchableVisionOverlayComponent +{ + public override EntProtoId? ToggleAction { get; set; } = "ToggleNightVision"; + + public override Color Color { get; set; } = Color.FromHex("#98FB98"); +} + +public sealed partial class ToggleNightVisionEvent : InstantActionEvent; diff --git a/Content.Shared/_Goobstation/Overlays/SharedNightVisionSystem.cs b/Content.Shared/_Goobstation/Overlays/SharedNightVisionSystem.cs new file mode 100644 index 0000000000..ac0ba6b9e9 --- /dev/null +++ b/Content.Shared/_Goobstation/Overlays/SharedNightVisionSystem.cs @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: 2025 Aiden <28298836+Aidenkrz@users.noreply.github.com> +// SPDX-FileCopyrightText: 2025 Aviu00 <93730715+Aviu00@users.noreply.github.com> +// SPDX-FileCopyrightText: 2025 Misandry +// SPDX-FileCopyrightText: 2025 Spatison <137375981+Spatison@users.noreply.github.com> +// SPDX-FileCopyrightText: 2025 gus +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +namespace Content.Shared._Goobstation.Overlays; + +public sealed class SharedNightVisionSystem : SwitchableOverlaySystem; diff --git a/Content.Shared/_Goobstation/Overlays/SharedThermalVisionSystem.cs b/Content.Shared/_Goobstation/Overlays/SharedThermalVisionSystem.cs new file mode 100644 index 0000000000..ae5be95211 --- /dev/null +++ b/Content.Shared/_Goobstation/Overlays/SharedThermalVisionSystem.cs @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: 2025 Aiden <28298836+Aidenkrz@users.noreply.github.com> +// SPDX-FileCopyrightText: 2025 Aviu00 <93730715+Aviu00@users.noreply.github.com> +// SPDX-FileCopyrightText: 2025 Misandry +// SPDX-FileCopyrightText: 2025 Spatison <137375981+Spatison@users.noreply.github.com> +// SPDX-FileCopyrightText: 2025 gus +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +namespace Content.Shared._Goobstation.Overlays; + +public sealed class SharedThermalVisionSystem : SwitchableOverlaySystem; diff --git a/Content.Shared/_Goobstation/Overlays/SwitchableOverlaySystem.cs b/Content.Shared/_Goobstation/Overlays/SwitchableOverlaySystem.cs new file mode 100644 index 0000000000..31f5406d45 --- /dev/null +++ b/Content.Shared/_Goobstation/Overlays/SwitchableOverlaySystem.cs @@ -0,0 +1,202 @@ +// SPDX-FileCopyrightText: 2025 Aiden <28298836+Aidenkrz@users.noreply.github.com> +// SPDX-FileCopyrightText: 2025 Aviu00 <93730715+Aviu00@users.noreply.github.com> +// SPDX-FileCopyrightText: 2025 Misandry +// SPDX-FileCopyrightText: 2025 Spatison <137375981+Spatison@users.noreply.github.com> +// SPDX-FileCopyrightText: 2025 gus +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +using Content.Shared._Goobstation.Flashbang; +using Content.Shared.Actions; +using Content.Shared.Inventory; +using Robust.Shared.Audio.Systems; +using Robust.Shared.GameStates; +using Robust.Shared.Network; +using Robust.Shared.Player; +using Robust.Shared.Timing; + +namespace Content.Shared._Goobstation.Overlays; + +public abstract class SwitchableOverlaySystem : EntitySystem // this should get move to a white module if we ever do anything with forks.. + where TComp : SwitchableVisionOverlayComponent + where TEvent : InstantActionEvent +{ + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedActionsSystem _actions = default!; + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly INetManager _net = default!; + + public override void Initialize() + { + SubscribeLocalEvent(OnToggle); + SubscribeLocalEvent(OnInit); + SubscribeLocalEvent(OnMapInit); + SubscribeLocalEvent(OnShutdown); + SubscribeLocalEvent(OnGetItemActions); + SubscribeLocalEvent(OnGetState); + SubscribeLocalEvent(OnHandleState); + SubscribeLocalEvent(OnGetFlashMultiplier); + SubscribeLocalEvent>(OnGetInventoryFlashMultiplier); + } + + private void OnGetFlashMultiplier(Entity ent, ref FlashDurationMultiplierEvent args) + { + if (!ent.Comp.IsEquipment) + args.Multiplier *= GetFlashMultiplier(ent); + } + + private void OnGetInventoryFlashMultiplier(Entity ent, + ref InventoryRelayedEvent args) + { + if (ent.Comp.IsEquipment) + args.Args.Multiplier *= GetFlashMultiplier(ent); + } + + private float GetFlashMultiplier(TComp comp) + { + if (!comp.IsActive && (comp.PulseTime <= 0f || comp.PulseAccumulator >= comp.PulseTime)) + return 1f; + + return comp.FlashDurationMultiplier; + } + + public override void FrameUpdate(float frameTime) + { + base.FrameUpdate(frameTime); + + if (_net.IsClient) + ActiveTick(frameTime); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + if (_net.IsServer) + ActiveTick(frameTime); + } + + private void ActiveTick(float frameTime) + { + var query = EntityQueryEnumerator(); + + while (query.MoveNext(out var uid, out var comp)) + { + if (comp.PulseTime <= 0f || comp.PulseAccumulator >= comp.PulseTime) + continue; + + comp.PulseAccumulator += frameTime; + + if (comp.PulseAccumulator < comp.PulseTime) + continue; + + Toggle(uid, comp, false, false); + RaiseSwitchableOverlayToggledEvent(uid, uid, comp.IsActive); + RaiseSwitchableOverlayToggledEvent(uid, Transform(uid).ParentUid, comp.IsActive); + } + } + + private void OnGetState(EntityUid uid, TComp component, ref ComponentGetState args) + { + args.State = new SwitchableVisionOverlayComponentState + { + Color = component.Color, + IsActive = component.IsActive, + FlashDurationMultiplier = component.FlashDurationMultiplier, + ActivateSound = component.ActivateSound, + DeactivateSound = component.DeactivateSound, + ToggleAction = component.ToggleAction, + LightRadius = component is ThermalVisionComponent thermal ? thermal.LightRadius : 0f, + }; + } + + private void OnHandleState(EntityUid uid, TComp component, ref ComponentHandleState args) + { + if (args.Current is not SwitchableVisionOverlayComponentState state) + return; + + component.Color = state.Color; + component.FlashDurationMultiplier = state.FlashDurationMultiplier; + component.ActivateSound = state.ActivateSound; + component.DeactivateSound = state.DeactivateSound; + + if (component.ToggleAction != state.ToggleAction) + { + _actions.RemoveAction(uid, component.ToggleActionEntity); + component.ToggleAction = state.ToggleAction; + if (component.ToggleAction != null) + _actions.AddAction(uid, ref component.ToggleActionEntity, component.ToggleAction); + } + + if (component is ThermalVisionComponent thermal) + thermal.LightRadius = state.LightRadius; + + if (component.IsActive == state.IsActive) + return; + + component.IsActive = state.IsActive; + + RaiseSwitchableOverlayToggledEvent(uid, + component.IsEquipment ? Transform(uid).ParentUid : uid, + component.IsActive); + } + + private void OnGetItemActions(Entity ent, ref GetItemActionsEvent args) + { + if (ent.Comp.IsEquipment && ent.Comp.ToggleAction != null && args.SlotFlags is not SlotFlags.POCKET and not null) + args.AddAction(ref ent.Comp.ToggleActionEntity, ent.Comp.ToggleAction); + } + + private void OnShutdown(EntityUid uid, TComp component, ComponentShutdown args) + { + if (!component.IsEquipment) + _actions.RemoveAction(uid, component.ToggleActionEntity); + } + + private void OnInit(EntityUid uid, TComp component, ComponentInit args) + { + component.PulseAccumulator = component.PulseTime; + } + + private void OnMapInit(EntityUid uid, TComp component, MapInitEvent args) + { + if (component is { IsEquipment: false, ToggleActionEntity: null, ToggleAction: not null }) + _actions.AddAction(uid, ref component.ToggleActionEntity, component.ToggleAction); + } + + private void OnToggle(EntityUid uid, TComp component, TEvent args) + { + Toggle(uid, component, !component.IsActive); + RaiseSwitchableOverlayToggledEvent(uid, args.Performer, component.IsActive); + args.Handled = true; + } + + private void Toggle(EntityUid uid, TComp component, bool activate, bool playSound = true) + { + if (playSound && _net.IsClient && _timing.IsFirstTimePredicted) + { + _audio.PlayEntity(activate ? component.ActivateSound : component.DeactivateSound, + Filter.Local(), + uid, + false); + } + + if (component.PulseTime > 0f) + { + component.PulseAccumulator = activate ? 0f : component.PulseTime; + return; + } + + component.IsActive = activate; + Dirty(uid, component); + } + + private void RaiseSwitchableOverlayToggledEvent(EntityUid uid, EntityUid user, bool activated) + { + var ev = new SwitchableOverlayToggledEvent(user, activated); + RaiseLocalEvent(uid, ref ev); + } +} + +[ByRefEvent] +public record struct SwitchableOverlayToggledEvent(EntityUid User, bool Activated); diff --git a/Content.Shared/_Goobstation/Overlays/SwitchableVisionOverlayComponent.cs b/Content.Shared/_Goobstation/Overlays/SwitchableVisionOverlayComponent.cs new file mode 100644 index 0000000000..76adf293fc --- /dev/null +++ b/Content.Shared/_Goobstation/Overlays/SwitchableVisionOverlayComponent.cs @@ -0,0 +1,64 @@ +// SPDX-FileCopyrightText: 2025 Aiden <28298836+Aidenkrz@users.noreply.github.com> +// SPDX-FileCopyrightText: 2025 Aviu00 <93730715+Aviu00@users.noreply.github.com> +// SPDX-FileCopyrightText: 2025 Misandry +// SPDX-FileCopyrightText: 2025 Spatison <137375981+Spatison@users.noreply.github.com> +// SPDX-FileCopyrightText: 2025 gus +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +using Robust.Shared.Audio; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; + +namespace Content.Shared._Goobstation.Overlays; + +public abstract partial class SwitchableVisionOverlayComponent : BaseVisionOverlayComponent +{ + [DataField] + public bool IsActive; + + [DataField] + public bool DrawOverlay = true; + + /// + /// Whether it should grant equipment enhanced vision or is it mob vision + /// + [DataField] + public bool IsEquipment; + + /// + /// If it is greater than 0, overlay isn't toggled but pulsed instead + /// + [DataField] + public float PulseTime; + + [ViewVariables(VVAccess.ReadOnly)] + public float PulseAccumulator; + + [DataField] + public float FlashDurationMultiplier = 1f; + + [DataField] + public SoundSpecifier? ActivateSound = new SoundPathSpecifier("/Audio/_White/Items/Goggles/activate.ogg"); + + [DataField] + public SoundSpecifier? DeactivateSound = new SoundPathSpecifier("/Audio/_White/Items/Goggles/deactivate.ogg"); + + [DataField] + public virtual EntProtoId? ToggleAction { get; set; } + + [ViewVariables] + public EntityUid? ToggleActionEntity; +} + +[Serializable, NetSerializable] +public sealed class SwitchableVisionOverlayComponentState : IComponentState +{ + public Color Color; + public bool IsActive; + public float FlashDurationMultiplier; + public SoundSpecifier? ActivateSound; + public SoundSpecifier? DeactivateSound; + public EntProtoId? ToggleAction; + public float LightRadius; +} diff --git a/Content.Shared/_Goobstation/Overlays/ThermalVisionComponent.cs b/Content.Shared/_Goobstation/Overlays/ThermalVisionComponent.cs new file mode 100644 index 0000000000..136a701d95 --- /dev/null +++ b/Content.Shared/_Goobstation/Overlays/ThermalVisionComponent.cs @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: 2025 Aiden <28298836+Aidenkrz@users.noreply.github.com> +// SPDX-FileCopyrightText: 2025 Armok <155400926+ARMOKS@users.noreply.github.com> +// SPDX-FileCopyrightText: 2025 Aviu00 <93730715+Aviu00@users.noreply.github.com> +// SPDX-FileCopyrightText: 2025 Misandry +// SPDX-FileCopyrightText: 2025 Spatison <137375981+Spatison@users.noreply.github.com> +// SPDX-FileCopyrightText: 2025 gus +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +using Content.Shared.Actions; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared._Goobstation.Overlays; + +[RegisterComponent, NetworkedComponent] +public sealed partial class ThermalVisionComponent : SwitchableVisionOverlayComponent +{ + public override EntProtoId? ToggleAction { get; set; } = "ToggleThermalVision"; + + public override Color Color { get; set; } = Color.FromHex("#d06764"); + + [DataField] + public float LightRadius = 2f; +} + +public sealed partial class ToggleThermalVisionEvent : InstantActionEvent; diff --git a/Resources/Audio/_DV/Effects/attributions.yml b/Resources/Audio/_DV/Effects/attributions.yml index 6a4a98a7ee..090bb2ba8f 100644 --- a/Resources/Audio/_DV/Effects/attributions.yml +++ b/Resources/Audio/_DV/Effects/attributions.yml @@ -2,3 +2,7 @@ license: "CC-BY-NC-3.0" copyright: "Freesound user BristolStories" source: "https://freesound.org/people/BristolStories/sounds/65915/" +- files: ["creepyshriek.ogg"] + license: "CC-BY-NC-3.0" + copyright: "Aurorastation" + source: "https://github.com/Aurorastation/Aurora.3/blob/master/sound/effects/creepyshriek.ogg" diff --git a/Resources/Audio/_DV/Effects/creepyshriek.ogg b/Resources/Audio/_DV/Effects/creepyshriek.ogg new file mode 100644 index 0000000000..86933130d5 Binary files /dev/null and b/Resources/Audio/_DV/Effects/creepyshriek.ogg differ diff --git a/Resources/Audio/_White/Items/Goggles/activate.ogg b/Resources/Audio/_White/Items/Goggles/activate.ogg new file mode 100644 index 0000000000..96cdb288fe Binary files /dev/null and b/Resources/Audio/_White/Items/Goggles/activate.ogg differ diff --git a/Resources/Audio/_White/Items/Goggles/attributions.yml b/Resources/Audio/_White/Items/Goggles/attributions.yml new file mode 100644 index 0000000000..35a9a7b38b --- /dev/null +++ b/Resources/Audio/_White/Items/Goggles/attributions.yml @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: 2025 Aiden <28298836+Aidenkrz@users.noreply.github.com> +# SPDX-FileCopyrightText: 2025 Aviu00 <93730715+Aviu00@users.noreply.github.com> +# SPDX-FileCopyrightText: 2025 Spatison <137375981+Spatison@users.noreply.github.com> +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +- files: ["activate.ogg"] + license: "CC-BY-NC-SA-3.0" + copyright: "Taken from TGstation" + source: "https://github.com/tgstation/tgstation" + +- files: ["deactivate.ogg"] + license: "CC-BY-NC-SA-3.0" + copyright: "Taken from TGstation" + source: "https://github.com/tgstation/tgstation" \ No newline at end of file diff --git a/Resources/Audio/_White/Items/Goggles/deactivate.ogg b/Resources/Audio/_White/Items/Goggles/deactivate.ogg new file mode 100644 index 0000000000..e1e8f4fd82 Binary files /dev/null and b/Resources/Audio/_White/Items/Goggles/deactivate.ogg differ diff --git a/Resources/Locale/en-US/_DV/abilities/psionic.ftl b/Resources/Locale/en-US/_DV/abilities/psionic.ftl index b8854b545e..3cfe306558 100644 --- a/Resources/Locale/en-US/_DV/abilities/psionic.ftl +++ b/Resources/Locale/en-US/_DV/abilities/psionic.ftl @@ -24,6 +24,7 @@ psionic-power-precognition-breaker-flip-result-message = You see torches snuff a psionic-power-precognition-bureaucratic-error-result-message = You see a vision of yourself trapped in a room, trying to solve a puzzle with both missing and duplicate pieces. psionic-power-precognition-clerical-error-result-message = You see faces you once knew being obscured in a fog of static, identities lost. psionic-power-precognition-closet-skeleton-result-message = You hear a crackling laugh echo and clinking bones in the dusty recesses of the station. +psionic-power-precognition-skia-result-message = The shadows around you gnash and scratch at you, a great beast of the noösphere is stalking you. You feel its breath on your neck. psionic-power-precognition-dragon-spawn-result-message = Reality around you bulges and breaks as a great beast cries for war. The smell of salty sea and blood fills the air. psionic-power-precognition-colossus-spawn-result-message = You see the vast shadow of a monstrosity so large that it casts all beneath it into darkness. The noösphere shifts precariously in its wake. psionic-power-precognition-ninja-spawn-result-message = You see a vision of shadows brought to life, hounds of war howling their cries as they chase it through dark corners of the station. diff --git a/Resources/Locale/en-US/_DV/ghost/roles/skia.ftl b/Resources/Locale/en-US/_DV/ghost/roles/skia.ftl new file mode 100644 index 0000000000..ee00929f66 --- /dev/null +++ b/Resources/Locale/en-US/_DV/ghost/roles/skia.ftl @@ -0,0 +1,11 @@ +ghost-role-information-skia-name = Skia +ghost-role-information-skia-description = The fates have cut the thread of a soul, stalk the shadows to find them and reap what is owed. +ghost-role-information-skia-rules = You are a [color=red][bold]Solo Antagonist[/bold][/color]. + You may kill your target, you do not need to ensure they stay dead. + You should avoid attacking those other than your target, except to protect yourself or to ensure your target dies. + +skia-role-briefing = + You are a Skia, a Shade of the Noösphere. + You are bound to darkness and shadows, use the darkness to your advantage. + Find your prey and reap their soul (kill them). Avoid ending the lives of innocents. +skia-round-end-name = Skia diff --git a/Resources/Locale/en-US/_DV/objectives/conditions/skia.ftl b/Resources/Locale/en-US/_DV/objectives/conditions/skia.ftl new file mode 100644 index 0000000000..5ba9091414 --- /dev/null +++ b/Resources/Locale/en-US/_DV/objectives/conditions/skia.ftl @@ -0,0 +1,3 @@ +objective-issuer-skia = [color=#c7e1eb]The Fates[/color] +objective-condition-reap-soul-title = Sever the soul of {$targetName}, {CAPITALIZE($job)}. +objective-condition-reap-soul-reroll-message = The Fates have severed another soul. Your new target is {$targetName}, {CAPITALIZE($job)}. diff --git a/Resources/Locale/en-US/_DV/prototypes/roles/antags.ftl b/Resources/Locale/en-US/_DV/prototypes/roles/antags.ftl index b7c83ee4b7..be47429361 100644 --- a/Resources/Locale/en-US/_DV/prototypes/roles/antags.ftl +++ b/Resources/Locale/en-US/_DV/prototypes/roles/antags.ftl @@ -22,5 +22,8 @@ roles-antag-nuclear-operative-corpsman-objective = The team's combat medic taske roles-antag-menace-skeleton-name = Menace Skeleton roles-antag-menace-skeleton-objective = Rattle 'Em +roles-antag-skia-name = Skia +roles-antag-skia-objective = Rend souls from the living. + roles-antag-syndicate-armsdealer-name = Arms Dealer roles-antag-syndicate-armsdealer-objective = Sell your firearms and stay away from the law. diff --git a/Resources/Locale/en-US/_DV/recipes/tags.ftl b/Resources/Locale/en-US/_DV/recipes/tags.ftl index 0f7295a3b2..835d8d6895 100644 --- a/Resources/Locale/en-US/_DV/recipes/tags.ftl +++ b/Resources/Locale/en-US/_DV/recipes/tags.ftl @@ -34,3 +34,6 @@ construction-graph-tag-carp-plushie = carp plushie # Moth plushie construction-graph-tag-mothroach-hide = mothroach hide + +# Hoodies +construction-graph-tag-hoodie = hoodie diff --git a/Resources/Locale/en-US/_white/research/technologies.ftl b/Resources/Locale/en-US/_white/research/technologies.ftl new file mode 100644 index 0000000000..b8108d7727 --- /dev/null +++ b/Resources/Locale/en-US/_white/research/technologies.ftl @@ -0,0 +1,7 @@ +# SPDX-FileCopyrightText: 2025 Aiden <28298836+Aidenkrz@users.noreply.github.com> +# SPDX-FileCopyrightText: 2025 Aviu00 <93730715+Aviu00@users.noreply.github.com> +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +research-technology-night-vision = Night Vision Technology +research-technology-thermal-vision = Thermal Vision Technology diff --git a/Resources/Prototypes/Entities/Clothing/Eyes/glasses.yml b/Resources/Prototypes/Entities/Clothing/Eyes/glasses.yml index 0dd1dd6888..79c807f9c9 100644 --- a/Resources/Prototypes/Entities/Clothing/Eyes/glasses.yml +++ b/Resources/Prototypes/Entities/Clothing/Eyes/glasses.yml @@ -229,25 +229,6 @@ - type: IdentityBlocker coverage: EYES -#Make a scanner category when these actually function and we get the trayson -- type: entity - parent: ClothingEyesBase - id: ClothingEyesGlassesThermal - name: optical thermal scanner - description: Thermals in the shape of glasses. - components: - - type: Sprite - sprite: Clothing/Eyes/Glasses/thermal.rsi - - type: Clothing - sprite: Clothing/Eyes/Glasses/thermal.rsi - - type: Armor - modifiers: - coefficients: - Heat: 0.95 - - type: GroupExamine - - type: IdentityBlocker - coverage: EYES - - type: entity parent: ClothingEyesBase id: ClothingEyesGlassesChemical diff --git a/Resources/Prototypes/Entities/Clothing/OuterClothing/misc.yml b/Resources/Prototypes/Entities/Clothing/OuterClothing/misc.yml index a29eb93dbc..2cd0e82d39 100644 --- a/Resources/Prototypes/Entities/Clothing/OuterClothing/misc.yml +++ b/Resources/Prototypes/Entities/Clothing/OuterClothing/misc.yml @@ -82,6 +82,10 @@ sprite: Clothing/OuterClothing/Misc/black_hoodie.rsi - type: Clothing sprite: Clothing/OuterClothing/Misc/black_hoodie.rsi + - type: Tag # DeltaV - It's a hoodie + tags: + - WhitelistChameleon + - Hoodie - type: entity parent: ClothingOuterBase @@ -93,6 +97,10 @@ sprite: Clothing/OuterClothing/Misc/grey_hoodie.rsi - type: Clothing sprite: Clothing/OuterClothing/Misc/grey_hoodie.rsi + - type: Tag # DeltaV - It's a hoodie + tags: + - WhitelistChameleon + - Hoodie - type: entity parent: ClothingOuterBase @@ -122,6 +130,10 @@ sprite: Clothing/OuterClothing/Misc/chaplain_hoodie.rsi - type: ToggleableClothing clothingPrototype: ClothingHeadHatHoodChaplainHood + - type: Tag # DeltaV - It's a hoodie + tags: + - WhitelistChameleon + - Hoodie - type: entity parent: ClothingOuterBase diff --git a/Resources/Prototypes/Entities/Structures/Machines/lathe.yml b/Resources/Prototypes/Entities/Structures/Machines/lathe.yml index 7ddde13783..0b6cb2199f 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/lathe.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/lathe.yml @@ -228,6 +228,7 @@ - Equipment - FauxTiles - Botany # DeltaV + - SpecOpsGoogles # DeltaV - Surgery # Shitmed change - type: EmagLatheRecipes emagDynamicPacks: @@ -442,6 +443,7 @@ - SecurityRubberAmmoStatic - PrisonerSoftsuits - EVASuit + - SpecOpsGoogles # End DeltaV Additions dynamicPacks: - SalvageSecurityBoards diff --git a/Resources/Prototypes/Nyanotrasen/psionicPowers.yml b/Resources/Prototypes/Nyanotrasen/psionicPowers.yml index 9504d259bf..2a3c4646cc 100644 --- a/Resources/Prototypes/Nyanotrasen/psionicPowers.yml +++ b/Resources/Prototypes/Nyanotrasen/psionicPowers.yml @@ -6,6 +6,7 @@ TelegnosisPower: 1 PsionicRegenerationPower: 1 PrecognitionPower: 1 # DeltaV + PsychokineticScreamPower: 1 MassSleepPower: 0.3 NoosphericZapPower: 0.3 # PsionicInvisibilityPower: 0.15 diff --git a/Resources/Prototypes/Roles/Antags/nukeops.yml b/Resources/Prototypes/Roles/Antags/nukeops.yml index 7a3f8c1958..61cf08f141 100644 --- a/Resources/Prototypes/Roles/Antags/nukeops.yml +++ b/Resources/Prototypes/Roles/Antags/nukeops.yml @@ -57,7 +57,7 @@ jumpsuit: ClothingUniformJumpsuitOperative back: ClothingBackpackDuffelSyndicate mask: ClothingMaskGasSyndicate - eyes: ClothingEyesHudSyndicate + eyes: ClothingEyesNightVisionGogglesNukie # Goobstation ears: ClothingHeadsetAltSyndicate gloves: ClothingHandsGlovesCombat outerClothing: ClothingOuterHardsuitSyndie @@ -115,7 +115,7 @@ parent: SyndicateOperativeGearFull equipment: pocket2: AgentUplinkRadio45TC # DeltaV - allows them to buy Agent armor - eyes: ClothingEyesHudSyndicateAgent + eyes: ClothingEyesNightVisionGogglesNukie # Goobstation outerClothing: ClothingOuterCoatCybersunWindbreaker # DeltaV - removal of armor # shoes: ClothingShoesBootsMagSyndie # DeltaV - removal of armor id: SyndiAgentPDA diff --git a/Resources/Prototypes/_DV/Actions/psionic.yml b/Resources/Prototypes/_DV/Actions/psionic.yml index 1b29fc921e..1d19b0fdc9 100644 --- a/Resources/Prototypes/_DV/Actions/psionic.yml +++ b/Resources/Prototypes/_DV/Actions/psionic.yml @@ -211,3 +211,14 @@ checkConsciousness: false - type: InstantAction event: !type:PrecognitionPowerActionEvent + +- type: entity + id: ActionPsychokineticScream + name: Psychokinetic Scream + description: Emit a blood-curdling scream that shatters all lights in the area. + components: + - type: Action + icon: { sprite: _DV/Actions/shatterlights.rsi, state: shatter-lights } + useDelay: 60 + - type: InstantAction + event: !type:ShatterLightsActionEvent diff --git a/Resources/Prototypes/_DV/Actions/skia.yml b/Resources/Prototypes/_DV/Actions/skia.yml new file mode 100644 index 0000000000..06fdd429fe --- /dev/null +++ b/Resources/Prototypes/_DV/Actions/skia.yml @@ -0,0 +1,21 @@ +- type: entity + id: ActionShatterLights + name: Shatter Lights + description: Emit a blood-curdling scream that shatters all lights in the area. + components: + - type: Action + icon: { sprite: _DV/Actions/shatterlights.rsi, state: shatter-lights } + useDelay: 30 + - type: InstantAction + event: !type:ShatterLightsActionEvent + +- type: entity + id: ActionTechnokineticPulse + name: Technokinetic Pulse + description: Unleash a burst of Technokinetic energy, disabling and destroying nearby electronics. + components: + - type: Action + icon: { sprite: _DV/Actions/technokineticpulse.rsi, state: technokinetic-pulse } + useDelay: 60 + - type: InstantAction + event: !type:TechnokineticPulseActionEvent diff --git a/Resources/Prototypes/_DV/Alerts/categories.yml b/Resources/Prototypes/_DV/Alerts/categories.yml new file mode 100644 index 0000000000..99b3615bfb --- /dev/null +++ b/Resources/Prototypes/_DV/Alerts/categories.yml @@ -0,0 +1,2 @@ +- type: alertCategory + id: Light diff --git a/Resources/Prototypes/_DV/Alerts/light.yml b/Resources/Prototypes/_DV/Alerts/light.yml new file mode 100644 index 0000000000..ad5c5212bd --- /dev/null +++ b/Resources/Prototypes/_DV/Alerts/light.yml @@ -0,0 +1,26 @@ +- type: alert + id: LightLevelDarkIcon + category: Light + icons: + - sprite: /Textures/_DV/Interface/Alerts/light_level.rsi + state: dark + name: alerts-light-level-dark-name + description: alerts-light-level-dark-desc + +- type: alert + id: LightLevelNeutralIcon + category: Light + icons: + - sprite: /Textures/_DV/Interface/Alerts/light_level.rsi + state: neutral + name: alerts-light-level-neutral-name + description: alerts-light-level-neutral-desc + +- type: alert + id: LightLevelBrightIcon + category: Light + icons: + - sprite: /Textures/_DV/Interface/Alerts/light_level.rsi + state: bright + name: alerts-light-level-bright-name + description: alerts-light-level-bright-desc diff --git a/Resources/Prototypes/_DV/Entities/Clothing/Head/hoods.yml b/Resources/Prototypes/_DV/Entities/Clothing/Head/hoods.yml index 5e992d97c6..95d40780b1 100644 --- a/Resources/Prototypes/_DV/Entities/Clothing/Head/hoods.yml +++ b/Resources/Prototypes/_DV/Entities/Clothing/Head/hoods.yml @@ -9,3 +9,21 @@ sprite: _DV/Clothing/Head/Hoods/interdynechemhood.rsi - type: Clothing sprite: _DV/Clothing/Head/Hoods/interdynechemhood.rsi + +- type: entity + parent: ClothingHeadBase + id: ClothingHeadHatHoodSkiaHood + categories: [ HideSpawnMenu ] + name: skia hood + description: Smells like Tartarus in here. + components: + - type: Sprite + sprite: _DV/Clothing/Head/Hoods/skia_hoodie.rsi + - type: Clothing + sprite: _DV/Clothing/Head/Hoods/skia_hoodie.rsi + - type: Tag + tags: + - WhitelistChameleon + - type: HideLayerClothing + slots: + - Hair diff --git a/Resources/Prototypes/_DV/Entities/Clothing/OuterClothing/misc.yml b/Resources/Prototypes/_DV/Entities/Clothing/OuterClothing/misc.yml index 3f8c44f9f2..23612a85bf 100644 --- a/Resources/Prototypes/_DV/Entities/Clothing/OuterClothing/misc.yml +++ b/Resources/Prototypes/_DV/Entities/Clothing/OuterClothing/misc.yml @@ -30,3 +30,23 @@ Caustic: 0.40 - type: ToggleableClothing clothingPrototype: ClothingHeadHatInterdyneChemistryHood + +- type: entity + parent: ClothingOuterBaseToggleable + id: ClothingOuterHoodieSkia + name: skia hoodie + description: The robes worn by a shade. + components: + - type: Sprite + sprite: _DV/Clothing/OuterClothing/Misc/skia_hoodie.rsi + - type: Clothing + sprite: _DV/Clothing/OuterClothing/Misc/skia_hoodie.rsi + - type: ToggleableClothing + clothingPrototype: ClothingHeadHatHoodSkiaHood + - type: Construction + graph: SkiaHoodie + node: skiaHoodie + - type: Tag + tags: + - Hoodie + - WhitelistChameleon diff --git a/Resources/Prototypes/_DV/Entities/Effects/weapon_arc.yml b/Resources/Prototypes/_DV/Entities/Effects/weapon_arc.yml new file mode 100644 index 0000000000..4c6a05fb8d --- /dev/null +++ b/Resources/Prototypes/_DV/Entities/Effects/weapon_arc.yml @@ -0,0 +1,29 @@ +- type: entity + id: WeaponArcSkiaLunge + parent: WeaponArcStatic + categories: [ HideSpawnMenu ] + components: + - type: WeaponArcVisuals + - type: Sprite + scale: .5, .5 + layers: + - sprite: _DV/Effects/arcs.rsi + state: skialunge + shader: unshaded + - type: TimedDespawn + lifetime: 0.399 + +- type: entity + id: WeaponArcSkiaSwipe + parent: WeaponArcSlash + categories: [ HideSpawnMenu ] + components: + - type: WeaponArcVisuals + - type: Sprite + scale: .5, .5 + layers: + - sprite: _DV/Effects/arcs.rsi + state: skiaswipe + shader: unshaded + - type: TimedDespawn + lifetime: 0.399 diff --git a/Resources/Prototypes/_DV/Entities/Markers/Spawners/ghost_roles.yml b/Resources/Prototypes/_DV/Entities/Markers/Spawners/ghost_roles.yml index a5982ba671..b94e12e8e6 100644 --- a/Resources/Prototypes/_DV/Entities/Markers/Spawners/ghost_roles.yml +++ b/Resources/Prototypes/_DV/Entities/Markers/Spawners/ghost_roles.yml @@ -155,6 +155,22 @@ - !type:OverallPlaytimeRequirement time: 172800 # 48h +- type: entity + categories: [ HideSpawnMenu, Spawner ] + parent: BaseAntagSpawner + id: SpawnPointSkia + name: skia spawn point + components: + - type: GhostRole + name: ghost-role-information-skia-name + description: ghost-role-information-skia-description + rules: ghost-role-information-skia-rules + mindRoles: + - MindRoleSkia + requirements: # keep in sync with the antag prototype + - !type:OverallPlaytimeRequirement + time: 172800 # 24h + - type: entity categories: [ HideSpawnMenu, Spawner ] parent: BaseAntagSpawner diff --git a/Resources/Prototypes/_DV/Entities/Mobs/NPCs/skia.yml b/Resources/Prototypes/_DV/Entities/Mobs/NPCs/skia.yml new file mode 100644 index 0000000000..b5e35b1054 --- /dev/null +++ b/Resources/Prototypes/_DV/Entities/Mobs/NPCs/skia.yml @@ -0,0 +1,109 @@ +- type: entity + parent: [ SimpleSpaceMobBase, MobCombat ] + id: MobSkia + name: skia + description: A shadow given form, lashing out at anything that comes too close. + components: + - type: Sprite + drawdepth: Mobs + sprite: _DV/Mobs/Animals/skia.rsi + layers: + - map: ["enum.DamageStateVisualLayers.Base"] + state: skia + - type: DamageStateVisuals + states: + Alive: + Base: skia + Dead: + Base: dead + - type: MobState + allowedStates: + - Alive + - Dead + - type: MobThresholds + thresholds: + 0: Alive + 120: Dead + - type: Damageable + damageModifierSet: ManifestedSpirit + damageContainer: BiologicalMetaphysical + - type: Armor + modifiers: + coefficients: + Blunt: 0.4 + Slash: 0.4 + Piercing: 0.4 + Heat: 0.8 + Radiation: 0.2 + Caustic: 0.2 + - type: MovementSpeedModifier + baseWalkSpeed: 2.25 + baseSprintSpeed: 3.75 + - type: CombatMode + - type: MeleeWeapon + soundHit: + path: /Audio/Weapons/Xeno/alien_claw_flesh3.ogg + angle: 90 + animation: WeaponArcSkiaLunge + wideAnimation: WeaponArcSkiaSwipe + wideAnimationRotation: -90 + swingLeft: true + attackRate: 1.0 + range: 2.0 + altDisarm: false + damage: + types: + Slash: 19 + Pierce: 8 + Cold: 8 + - type: NpcFactionMember + factions: + - SimpleHostile + - type: Tag + tags: + - DoorBumpOpener + - type: Speech + speechVerb: Ghost + - type: Insulated + - type: LightReactive + manual: true + - type: LightLevelHealth + darkThreshold: 0.3 + lightThreshold: 0.7 + darkDamage: + groups: + Brute: -5 + Burn: -5 + Toxin: -5 + Airloss: -5 + Genetic: -5 + Metaphysical: -1 + lightDamage: + groups: + Brute: 5 + darkMovementSpeedMultiplier: 1.25 + lightMovementSpeedMultiplier: 1.0 + - type: LightLevelDamageMult + lightReceivedMultiplier: 2.0 + lightDealtMultiplier: 0.5 + - type: Bloodstream + maxBleedAmount: 0 + - type: ShatterLightsAbility + lineOfSight: true + - type: TechnokineticPulseAbility + range: 5.0 + energyConsumption: 20000 + disableDuration: 20.0 + - type: Butcherable + spawned: + - id: Ectoplasm + amount: 1 + - type: Prying + pryPowered: true + force: true + speedModifier: 2.5 # needs to be fast because they'll get ganked otherwise + useSound: + path: /Audio/Items/crowbar.ogg + - type: NightVision + drawOverlay: false + - type: Targeting diff --git a/Resources/Prototypes/_DV/Entities/Structures/Machines/lathe.yml b/Resources/Prototypes/_DV/Entities/Structures/Machines/lathe.yml index 93bcc9b58f..721aa65fe6 100644 --- a/Resources/Prototypes/_DV/Entities/Structures/Machines/lathe.yml +++ b/Resources/Prototypes/_DV/Entities/Structures/Machines/lathe.yml @@ -89,6 +89,7 @@ - EngineeringWeapons - FauxTiles - Equipment + - SpecOpsGoogles - UpgradeKits - UpgradeKits_Goob - EngineeringHardsuits @@ -130,6 +131,7 @@ dynamicPacks: - Equipment - Mining + - SpecOpsGoogles - SalvageWeapons - SalvageHardsuits - type: Machine @@ -216,6 +218,7 @@ - ScienceEquipment - ScienceClothing - ScienceWeapons + - SpecOpsGoogles - Chemistry - PowerCells - type: Machine diff --git a/Resources/Prototypes/_DV/GameRules/events.yml b/Resources/Prototypes/_DV/GameRules/events.yml index 920e994d74..7c4c3547e2 100644 --- a/Resources/Prototypes/_DV/GameRules/events.yml +++ b/Resources/Prototypes/_DV/GameRules/events.yml @@ -14,6 +14,7 @@ - id: ListeningPost - id: RoboNeuroticist - id: MenaceSkeleton + - id: SkiaSpawn # Replaces upstream meteor events until they're good - type: entity @@ -300,3 +301,33 @@ sound: /Audio/Effects/clang.ogg mindRoles: - MindRoleMenaceSkeleton + +- type: entity + parent: BaseMidRoundAntag + id: SkiaSpawn + components: + - type: StationEvent + weight: 6 + duration: null + minimumPlayers: 30 + - type: PrecognitionResult + message: psionic-power-precognition-skia-result-message + - type: AntagSpawner + prototype: MobSkia + - type: AntagObjectives + objectives: + - SkiaReapObjective + - type: AntagSelection + agentName: skia-round-end-name + definitions: + - spawnerPrototype: SpawnPointSkia + min: 1 + max: 1 + pickPlayer: false + mindRoles: + - MindRoleSkia + components: + - type: EmitSoundOnSpawn + sound: /Audio/_DV/Effects/creepyshriek.ogg + - type: BreakLightsOnSpawn + radius: 10 diff --git a/Resources/Prototypes/_DV/Objectives/skia.yml b/Resources/Prototypes/_DV/Objectives/skia.yml new file mode 100644 index 0000000000..4c31fb0a02 --- /dev/null +++ b/Resources/Prototypes/_DV/Objectives/skia.yml @@ -0,0 +1,25 @@ +- type: entity + parent: BaseObjective + id: SkiaReapObjective + name: Reap the damned soul. + description: Ensure this soul is released from its body at least once. + components: + - type: Objective + difficulty: 1.5 + issuer: objective-issuer-skia + unique: false + icon: + sprite: Mobs/Ghosts/ghost_human.rsi + state: icon + - type: RoleRequirement + roles: + - SkiaRole + - type: KillPersonCondition + requireDead: true + - type: TargetObjective + title: objective-condition-reap-soul-title + - type: PickRandomPerson + onlyChoosableJobs: true + - type: RerollAfterCompletion + rerollObjectivePrototype: SkiaReapObjective + rerollObjectiveMessage: objective-condition-reap-soul-reroll-message diff --git a/Resources/Prototypes/_DV/Recipes/Construction/Graphs/clothing/skia_hoodie.yml b/Resources/Prototypes/_DV/Recipes/Construction/Graphs/clothing/skia_hoodie.yml new file mode 100644 index 0000000000..3f1ada85bd --- /dev/null +++ b/Resources/Prototypes/_DV/Recipes/Construction/Graphs/clothing/skia_hoodie.yml @@ -0,0 +1,21 @@ +- type: constructionGraph + id: SkiaHoodie + start: start + graph: + - node: start + edges: + - to: skiaHoodie + steps: + - tag: Hoodie + name: construction-graph-tag-hoodie + icon: + sprite: Clothing/OuterClothing/Misc/chaplain_hoodie.rsi + state: icon + - tag: Ectoplasm + name: construction-graph-tag-ectoplasm + icon: + sprite: _DV/Mobs/Ghosts/revenant.rsi + state: ectoplasm + doAfter: 10 + - node: skiaHoodie + entity: ClothingOuterHoodieSkia diff --git a/Resources/Prototypes/_DV/Recipes/Construction/clothing.yml b/Resources/Prototypes/_DV/Recipes/Construction/clothing.yml index ac7ecf0673..a1f00039c2 100644 --- a/Resources/Prototypes/_DV/Recipes/Construction/clothing.yml +++ b/Resources/Prototypes/_DV/Recipes/Construction/clothing.yml @@ -45,3 +45,11 @@ targetNode: sharkminnowShoes category: construction-category-clothing objectType: Item + +- type: construction + id: SkiaHoodie + graph: SkiaHoodie + startNode: start + targetNode: skiaHoodie + category: construction-category-clothing + objectType: Item diff --git a/Resources/Prototypes/_DV/Roles/Antags/nukeops.yml b/Resources/Prototypes/_DV/Roles/Antags/nukeops.yml index 6a03bfafa4..914045b067 100644 --- a/Resources/Prototypes/_DV/Roles/Antags/nukeops.yml +++ b/Resources/Prototypes/_DV/Roles/Antags/nukeops.yml @@ -5,7 +5,7 @@ jumpsuit: ClothingUniformJumpsuitOperative back: ClothingBackpackDuffelSyndicate mask: ClothingMaskGasSyndicate - eyes: ClothingEyesHudSyndicate + eyes: ClothingEyesNightVisionGogglesNukie # Goobstation ears: ClothingHeadsetAltSyndicate gloves: ClothingHandsGlovesCombat outerClothing: ClothingOuterVestWeb diff --git a/Resources/Prototypes/_DV/Roles/Antags/skia.yml b/Resources/Prototypes/_DV/Roles/Antags/skia.yml new file mode 100644 index 0000000000..c848d0b659 --- /dev/null +++ b/Resources/Prototypes/_DV/Roles/Antags/skia.yml @@ -0,0 +1,8 @@ +- type: antag + id: Skia + name: roles-antag-skia-name + objective: roles-antag-skia-objective + antagonist: true + requirements: + - !type:OverallPlaytimeRequirement + time: 172800 # 48 hours overall diff --git a/Resources/Prototypes/_DV/Roles/MindRoles/mind_roles.yml b/Resources/Prototypes/_DV/Roles/MindRoles/mind_roles.yml index 6bfb750e59..6d928246e3 100644 --- a/Resources/Prototypes/_DV/Roles/MindRoles/mind_roles.yml +++ b/Resources/Prototypes/_DV/Roles/MindRoles/mind_roles.yml @@ -74,6 +74,19 @@ - type: RoleBriefing briefing: skeleton-role-briefing +- type: entity + parent: BaseMindRoleAntag + id: MindRoleSkia + name: Skia Role + components: + - type: MindRole + roleType: TeamAntagonist + antagPrototype: Skia + exclusiveAntag: true + - type: SkiaRole + - type: RoleBriefing + briefing: skia-role-briefing + - type: entity parent: BaseMindRoleAntag id: MindRoleArmsdealer diff --git a/Resources/Prototypes/_DV/tags.yml b/Resources/Prototypes/_DV/tags.yml index 2635f8e95c..82560e73d3 100644 --- a/Resources/Prototypes/_DV/tags.yml +++ b/Resources/Prototypes/_DV/tags.yml @@ -173,3 +173,6 @@ - type: Tag id: CleaningGrenade + +- type: Tag + id: Hoodie diff --git a/Resources/Prototypes/_Goobstation/Recipes/Lathes/Packs/misc.yml b/Resources/Prototypes/_Goobstation/Recipes/Lathes/Packs/misc.yml new file mode 100644 index 0000000000..5e308ff40e --- /dev/null +++ b/Resources/Prototypes/_Goobstation/Recipes/Lathes/Packs/misc.yml @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: 2025 Aiden <28298836+Aidenkrz@users.noreply.github.com> +# SPDX-FileCopyrightText: 2025 GoobBot +# SPDX-FileCopyrightText: 2025 PunishedJoe +# SPDX-FileCopyrightText: 2025 Roudenn +# SPDX-FileCopyrightText: 2025 SX_7 +# SPDX-FileCopyrightText: 2025 Ted Lukin <66275205+pheenty@users.noreply.github.com> +# SPDX-FileCopyrightText: 2025 pheenty +# +# SPDX-License-Identifier: AGPL-3.0-or-later +## Dynamic +- type: latheRecipePack # WD + id: SpecOpsGoogles + recipes: + - ClothingEyesNightVisionGoggles + - ClothingEyesGlassesThermal \ No newline at end of file diff --git a/Resources/Prototypes/_White/Actions/types.yml b/Resources/Prototypes/_White/Actions/types.yml new file mode 100644 index 0000000000..124829a6c6 --- /dev/null +++ b/Resources/Prototypes/_White/Actions/types.yml @@ -0,0 +1,48 @@ +# SPDX-FileCopyrightText: 2025 Aiden <28298836+Aidenkrz@users.noreply.github.com> +# SPDX-FileCopyrightText: 2025 Aviu00 <93730715+Aviu00@users.noreply.github.com> +# SPDX-FileCopyrightText: 2025 Spatison <137375981+Spatison@users.noreply.github.com> +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +- type: entity + id: ToggleNightVision + name: Toggle Night Vision + description: Toggles night vision. + categories: [ HideSpawnMenu ] + components: + - type: Action + itemIconStyle: BigAction + priority: -20 + useDelay: 1 + icon: + sprite: _White/Clothing/Eyes/Goggles/nightvision.rsi + state: icon + - type: InstantAction + event: !type:ToggleNightVisionEvent + +- type: entity + id: ToggleThermalVision + name: Toggle Thermal Vision + description: Toggles thermal vision. + categories: [ HideSpawnMenu ] + components: + - type: Action + itemIconStyle: BigAction + priority: -20 + useDelay: 1 + icon: + sprite: _White/Clothing/Eyes/Goggles/thermal.rsi + state: icon + - type: InstantAction + event: !type:ToggleThermalVisionEvent + +- type: entity + id: PulseThermalVision + parent: ToggleThermalVision + name: Pulse Thermal Vision + description: Activate thermal vision temporarily. + categories: [ HideSpawnMenu ] + components: + - type: Action + useDelay: 4 + - type: InstantAction diff --git a/Resources/Prototypes/_White/Entities/Clothing/Eyes/goggles.yml b/Resources/Prototypes/_White/Entities/Clothing/Eyes/goggles.yml new file mode 100644 index 0000000000..964cf293d0 --- /dev/null +++ b/Resources/Prototypes/_White/Entities/Clothing/Eyes/goggles.yml @@ -0,0 +1,94 @@ +# SPDX-FileCopyrightText: 2025 Aiden <28298836+Aidenkrz@users.noreply.github.com> +# SPDX-FileCopyrightText: 2025 Armok <155400926+ARMOKS@users.noreply.github.com> +# SPDX-FileCopyrightText: 2025 Aviu00 <93730715+Aviu00@users.noreply.github.com> +# SPDX-FileCopyrightText: 2025 Spatison <137375981+Spatison@users.noreply.github.com> +# +# SPDX-License-Identifier: AGPL-3.0-or-later +# Night Vision Goggles + +- type: entity + parent: ClothingEyesBase + id: ClothingEyesNightVisionGoggles + name: night vision goggles + description: Now you can see in the dark! + components: + - type: Sprite + sprite: _White/Clothing/Eyes/Goggles/nightvision.rsi + - type: Clothing + sprite: _White/Clothing/Eyes/Goggles/nightvision.rsi + - type: NightVision + flashDurationMultiplier: 2 + isEquipment: true + - type: IdentityBlocker + +- type: entity + parent: [ClothingEyesBase,BaseSyndicateContraband] + id: ClothingEyesNightVisionGogglesSyndie + name: syndicate night vision goggles + description: A high-tech pair of night vision goggles. Has medical analysis technology. + components: + - type: Sprite + sprite: _White/Clothing/Eyes/Goggles/snightvision.rsi + - type: Clothing + sprite: _White/Clothing/Eyes/Goggles/snightvision.rsi + - type: NightVision + flashDurationMultiplier: 2 + isEquipment: true + - type: IdentityBlocker + coverage: EYES + - type: ShowHealthBars + damageContainers: + - Biological + +- type: entity + parent: ClothingEyesNightVisionGogglesSyndie + id: ClothingEyesNightVisionGogglesNukie + suffix: "NukeOps" + components: + - type: ShowSyndicateIcons + +# Thermal Vision Goggles + +- type: entity + parent: ClothingEyesBase + id: ClothingEyesGlassesThermal + name: thermal vision goggles + description: Now you can see everyone! + components: + - type: Sprite + sprite: _White/Clothing/Eyes/Goggles/thermal.rsi + - type: Clothing + sprite: _White/Clothing/Eyes/Goggles/thermal.rsi + - type: ThermalVision + flashDurationMultiplier: 2 + pulseTime: 2 + isEquipment: true + toggleAction: PulseThermalVision + - type: IdentityBlocker + coverage: EYES + +- type: entity + parent: [ClothingEyesBase, BaseSyndicateContraband] + id: ClothingEyesThermalVisionGogglesSyndie + name: thermal vision goggles + description: A high-tech pair of thermal goggles. + components: + - type: Sprite + sprite: _White/Clothing/Eyes/Goggles/sthermals.rsi + - type: Clothing + sprite: _White/Clothing/Eyes/Goggles/sthermals.rsi + - type: ThermalVision + flashDurationMultiplier: 2 + isEquipment: true + toggleAction: ToggleThermalVision + - type: IdentityBlocker + coverage: EYES + +- type: entity + parent: ClothingEyesThermalVisionGogglesSyndie + id: ClothingEyesThermalVisionGogglesNukie + suffix: "NukeOps" + components: + - type: ShowSyndicateIcons + - type: ShowJobIcons + - type: ShowMindShieldIcons diff --git a/Resources/Prototypes/_White/Recipes/Lathes/devices.yml b/Resources/Prototypes/_White/Recipes/Lathes/devices.yml new file mode 100644 index 0000000000..428b892d62 --- /dev/null +++ b/Resources/Prototypes/_White/Recipes/Lathes/devices.yml @@ -0,0 +1,25 @@ +# SPDX-FileCopyrightText: 2025 Aiden <28298836+Aidenkrz@users.noreply.github.com> +# SPDX-FileCopyrightText: 2025 Aviu00 <93730715+Aviu00@users.noreply.github.com> +# SPDX-FileCopyrightText: 2025 Spatison <137375981+Spatison@users.noreply.github.com> +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +- type: latheRecipe + id: ClothingEyesNightVisionGoggles + result: ClothingEyesNightVisionGoggles + completetime: 2 + materials: + Steel: 200 + Glass: 100 + Silver: 100 + Gold: 100 + +- type: latheRecipe + id: ClothingEyesGlassesThermal + result: ClothingEyesGlassesThermal + completetime: 2 + materials: + Steel: 200 + Glass: 100 + Silver: 100 + Gold: 100 \ No newline at end of file diff --git a/Resources/Prototypes/_White/Research/experimental.yml b/Resources/Prototypes/_White/Research/experimental.yml new file mode 100644 index 0000000000..34b80578da --- /dev/null +++ b/Resources/Prototypes/_White/Research/experimental.yml @@ -0,0 +1,39 @@ +# SPDX-FileCopyrightText: 2025 Aiden <28298836+Aidenkrz@users.noreply.github.com> +# SPDX-FileCopyrightText: 2025 Aiden +# SPDX-FileCopyrightText: 2025 Aviu00 <93730715+Aviu00@users.noreply.github.com> +# SPDX-FileCopyrightText: 2025 FaDeOkno <143940725+FaDeOkno@users.noreply.github.com> +# SPDX-FileCopyrightText: 2025 FaDeOkno +# SPDX-FileCopyrightText: 2025 Spatison <137375981+Spatison@users.noreply.github.com> +# SPDX-FileCopyrightText: 2025 coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> +# SPDX-FileCopyrightText: 2025 gluesniffler <159397573+gluesniffler@users.noreply.github.com> +# +# SPDX-License-Identifier: AGPL-3.0-or-later +# Tier 2 + +- type: technology + id: NightVisionTech + name: research-technology-night-vision + icon: + sprite: _White/Clothing/Eyes/Goggles/nightvision.rsi + state: icon + discipline: Experimental + tier: 2 + cost: 10000 + recipeUnlocks: + - ClothingEyesNightVisionGoggles + technologyPrerequisites: + - MagnetsTech + +- type: technology + id: ThermalVisionTech + name: research-technology-thermal-vision + icon: + sprite: _White/Clothing/Eyes/Goggles/thermal.rsi + state: icon + discipline: Experimental + tier: 2 + cost: 10000 + recipeUnlocks: + - ClothingEyesGlassesThermal + technologyPrerequisites: + - MagnetsTech diff --git a/Resources/Prototypes/_White/Shaders/shaders.yml b/Resources/Prototypes/_White/Shaders/shaders.yml new file mode 100644 index 0000000000..fb669441a5 --- /dev/null +++ b/Resources/Prototypes/_White/Shaders/shaders.yml @@ -0,0 +1,13 @@ +# SPDX-FileCopyrightText: 2025 Aiden <28298836+Aidenkrz@users.noreply.github.com> +# SPDX-FileCopyrightText: 2025 Aviu00 <93730715+Aviu00@users.noreply.github.com> +# SPDX-FileCopyrightText: 2025 Eris +# SPDX-FileCopyrightText: 2025 GoobBot +# SPDX-FileCopyrightText: 2025 SX-7 +# SPDX-FileCopyrightText: 2025 Spatison <137375981+Spatison@users.noreply.github.com> +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +- type: shader + id: NightVision + kind: source + path: "/Textures/_White/Shaders/nightvision.swsl" diff --git a/Resources/Textures/_DV/Actions/shatterlights.rsi/meta.json b/Resources/Textures/_DV/Actions/shatterlights.rsi/meta.json new file mode 100644 index 0000000000..86da737b92 --- /dev/null +++ b/Resources/Textures/_DV/Actions/shatterlights.rsi/meta.json @@ -0,0 +1,14 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Created by TehFlaminTaco (github) for SS14", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "shatter-lights" + } + ] +} diff --git a/Resources/Textures/_DV/Actions/shatterlights.rsi/shatter-lights.png b/Resources/Textures/_DV/Actions/shatterlights.rsi/shatter-lights.png new file mode 100644 index 0000000000..df34b71dca Binary files /dev/null and b/Resources/Textures/_DV/Actions/shatterlights.rsi/shatter-lights.png differ diff --git a/Resources/Textures/_DV/Actions/technokineticpulse.rsi/meta.json b/Resources/Textures/_DV/Actions/technokineticpulse.rsi/meta.json new file mode 100644 index 0000000000..6ad8461fea --- /dev/null +++ b/Resources/Textures/_DV/Actions/technokineticpulse.rsi/meta.json @@ -0,0 +1,14 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Created by TehFlaminTaco (github) for SS14", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "technokinetic-pulse" + } + ] +} diff --git a/Resources/Textures/_DV/Actions/technokineticpulse.rsi/technokinetic-pulse.png b/Resources/Textures/_DV/Actions/technokineticpulse.rsi/technokinetic-pulse.png new file mode 100644 index 0000000000..79c497a12c Binary files /dev/null and b/Resources/Textures/_DV/Actions/technokineticpulse.rsi/technokinetic-pulse.png differ diff --git a/Resources/Textures/_DV/Clothing/Head/Hoods/skia_hoodie.rsi/equipped-HELMET.png b/Resources/Textures/_DV/Clothing/Head/Hoods/skia_hoodie.rsi/equipped-HELMET.png new file mode 100644 index 0000000000..7945becf5e Binary files /dev/null and b/Resources/Textures/_DV/Clothing/Head/Hoods/skia_hoodie.rsi/equipped-HELMET.png differ diff --git a/Resources/Textures/_DV/Clothing/Head/Hoods/skia_hoodie.rsi/icon.png b/Resources/Textures/_DV/Clothing/Head/Hoods/skia_hoodie.rsi/icon.png new file mode 100644 index 0000000000..d2e55b4c66 Binary files /dev/null and b/Resources/Textures/_DV/Clothing/Head/Hoods/skia_hoodie.rsi/icon.png differ diff --git a/Resources/Textures/_DV/Clothing/Head/Hoods/skia_hoodie.rsi/inhand-left.png b/Resources/Textures/_DV/Clothing/Head/Hoods/skia_hoodie.rsi/inhand-left.png new file mode 100644 index 0000000000..756edfccbc Binary files /dev/null and b/Resources/Textures/_DV/Clothing/Head/Hoods/skia_hoodie.rsi/inhand-left.png differ diff --git a/Resources/Textures/_DV/Clothing/Head/Hoods/skia_hoodie.rsi/inhand-right.png b/Resources/Textures/_DV/Clothing/Head/Hoods/skia_hoodie.rsi/inhand-right.png new file mode 100644 index 0000000000..47c9aca3ec Binary files /dev/null and b/Resources/Textures/_DV/Clothing/Head/Hoods/skia_hoodie.rsi/inhand-right.png differ diff --git a/Resources/Textures/_DV/Clothing/Head/Hoods/skia_hoodie.rsi/meta.json b/Resources/Textures/_DV/Clothing/Head/Hoods/skia_hoodie.rsi/meta.json new file mode 100644 index 0000000000..371400dc4d --- /dev/null +++ b/Resources/Textures/_DV/Clothing/Head/Hoods/skia_hoodie.rsi/meta.json @@ -0,0 +1,26 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Created by TehFlaminTaco (github) for SS14", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "icon" + }, + { + "name": "equipped-HELMET", + "directions": 4 + }, + { + "name": "inhand-left", + "directions": 4 + }, + { + "name": "inhand-right", + "directions": 4 + } + ] +} diff --git a/Resources/Textures/_DV/Clothing/OuterClothing/Misc/skia_hoodie.rsi/equipped-OUTERCLOTHING.png b/Resources/Textures/_DV/Clothing/OuterClothing/Misc/skia_hoodie.rsi/equipped-OUTERCLOTHING.png new file mode 100644 index 0000000000..d1fb37873a Binary files /dev/null and b/Resources/Textures/_DV/Clothing/OuterClothing/Misc/skia_hoodie.rsi/equipped-OUTERCLOTHING.png differ diff --git a/Resources/Textures/_DV/Clothing/OuterClothing/Misc/skia_hoodie.rsi/icon.png b/Resources/Textures/_DV/Clothing/OuterClothing/Misc/skia_hoodie.rsi/icon.png new file mode 100644 index 0000000000..03717e4b7e Binary files /dev/null and b/Resources/Textures/_DV/Clothing/OuterClothing/Misc/skia_hoodie.rsi/icon.png differ diff --git a/Resources/Textures/_DV/Clothing/OuterClothing/Misc/skia_hoodie.rsi/inhand-left.png b/Resources/Textures/_DV/Clothing/OuterClothing/Misc/skia_hoodie.rsi/inhand-left.png new file mode 100644 index 0000000000..098354f8c0 Binary files /dev/null and b/Resources/Textures/_DV/Clothing/OuterClothing/Misc/skia_hoodie.rsi/inhand-left.png differ diff --git a/Resources/Textures/_DV/Clothing/OuterClothing/Misc/skia_hoodie.rsi/inhand-right.png b/Resources/Textures/_DV/Clothing/OuterClothing/Misc/skia_hoodie.rsi/inhand-right.png new file mode 100644 index 0000000000..105b063887 Binary files /dev/null and b/Resources/Textures/_DV/Clothing/OuterClothing/Misc/skia_hoodie.rsi/inhand-right.png differ diff --git a/Resources/Textures/_DV/Clothing/OuterClothing/Misc/skia_hoodie.rsi/meta.json b/Resources/Textures/_DV/Clothing/OuterClothing/Misc/skia_hoodie.rsi/meta.json new file mode 100644 index 0000000000..3fd46b294a --- /dev/null +++ b/Resources/Textures/_DV/Clothing/OuterClothing/Misc/skia_hoodie.rsi/meta.json @@ -0,0 +1,26 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Created by TehFlaminTaco (github) for SS14", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "icon" + }, + { + "name": "equipped-OUTERCLOTHING", + "directions": 4 + }, + { + "name": "inhand-left", + "directions": 4 + }, + { + "name": "inhand-right", + "directions": 4 + } + ] +} diff --git a/Resources/Textures/_DV/Effects/arcs.rsi/meta.json b/Resources/Textures/_DV/Effects/arcs.rsi/meta.json new file mode 100644 index 0000000000..07f75ed4c3 --- /dev/null +++ b/Resources/Textures/_DV/Effects/arcs.rsi/meta.json @@ -0,0 +1,17 @@ +{ + "version": 1, + "size": { + "x": 32, + "y": 32 + }, + "license": "CC-BY-SA-3.0", + "copyright": "Created by TehFlaminTaco (github) for SS14", + "states": [ + { + "name": "skialunge" + }, + { + "name": "skiaswipe" + } + ] +} diff --git a/Resources/Textures/_DV/Effects/arcs.rsi/skialunge.png b/Resources/Textures/_DV/Effects/arcs.rsi/skialunge.png new file mode 100644 index 0000000000..2f273b8059 Binary files /dev/null and b/Resources/Textures/_DV/Effects/arcs.rsi/skialunge.png differ diff --git a/Resources/Textures/_DV/Effects/arcs.rsi/skiaswipe.png b/Resources/Textures/_DV/Effects/arcs.rsi/skiaswipe.png new file mode 100644 index 0000000000..6a7d1f35a8 Binary files /dev/null and b/Resources/Textures/_DV/Effects/arcs.rsi/skiaswipe.png differ diff --git a/Resources/Textures/_DV/Interface/Alerts/light_level.rsi/bright.png b/Resources/Textures/_DV/Interface/Alerts/light_level.rsi/bright.png new file mode 100644 index 0000000000..0285258884 Binary files /dev/null and b/Resources/Textures/_DV/Interface/Alerts/light_level.rsi/bright.png differ diff --git a/Resources/Textures/_DV/Interface/Alerts/light_level.rsi/dark.png b/Resources/Textures/_DV/Interface/Alerts/light_level.rsi/dark.png new file mode 100644 index 0000000000..b9efba3686 Binary files /dev/null and b/Resources/Textures/_DV/Interface/Alerts/light_level.rsi/dark.png differ diff --git a/Resources/Textures/_DV/Interface/Alerts/light_level.rsi/meta.json b/Resources/Textures/_DV/Interface/Alerts/light_level.rsi/meta.json new file mode 100644 index 0000000000..9d6e71f387 --- /dev/null +++ b/Resources/Textures/_DV/Interface/Alerts/light_level.rsi/meta.json @@ -0,0 +1,20 @@ +{ + "version": 1, + "size": { + "x": 24, + "y": 24 + }, + "license": "CC-BY-SA-3.0", + "copyright": "Original art by by TehFlaminTaco (github) for ss14", + "states": [ + { + "name": "dark" + }, + { + "name": "neutral" + }, + { + "name": "bright" + } + ] +} diff --git a/Resources/Textures/_DV/Interface/Alerts/light_level.rsi/neutral.png b/Resources/Textures/_DV/Interface/Alerts/light_level.rsi/neutral.png new file mode 100644 index 0000000000..04e443515d Binary files /dev/null and b/Resources/Textures/_DV/Interface/Alerts/light_level.rsi/neutral.png differ diff --git a/Resources/Textures/_DV/Mobs/Animals/skia.rsi/dead.png b/Resources/Textures/_DV/Mobs/Animals/skia.rsi/dead.png new file mode 100644 index 0000000000..d9c9aa0f82 Binary files /dev/null and b/Resources/Textures/_DV/Mobs/Animals/skia.rsi/dead.png differ diff --git a/Resources/Textures/_DV/Mobs/Animals/skia.rsi/meta.json b/Resources/Textures/_DV/Mobs/Animals/skia.rsi/meta.json new file mode 100644 index 0000000000..d704d36f40 --- /dev/null +++ b/Resources/Textures/_DV/Mobs/Animals/skia.rsi/meta.json @@ -0,0 +1,18 @@ +{ + "version": 1, + "size": { + "x": 32, + "y": 32 + }, + "license": "CC-BY-SA-3.0", + "copyright": "Original art by by TehFlaminTaco (github) for ss14", + "states": [ + { + "name": "skia", + "directions": 4 + }, + { + "name": "dead" + } + ] +} diff --git a/Resources/Textures/_DV/Mobs/Animals/skia.rsi/skia.png b/Resources/Textures/_DV/Mobs/Animals/skia.rsi/skia.png new file mode 100644 index 0000000000..c02f3c3d44 Binary files /dev/null and b/Resources/Textures/_DV/Mobs/Animals/skia.rsi/skia.png differ diff --git a/Resources/Textures/_White/Clothing/Eyes/Goggles/nightvision.rsi/equipped-EYES-off.png b/Resources/Textures/_White/Clothing/Eyes/Goggles/nightvision.rsi/equipped-EYES-off.png new file mode 100644 index 0000000000..b63f30fc71 Binary files /dev/null and b/Resources/Textures/_White/Clothing/Eyes/Goggles/nightvision.rsi/equipped-EYES-off.png differ diff --git a/Resources/Textures/_White/Clothing/Eyes/Goggles/nightvision.rsi/equipped-EYES.png b/Resources/Textures/_White/Clothing/Eyes/Goggles/nightvision.rsi/equipped-EYES.png new file mode 100644 index 0000000000..199c44122b Binary files /dev/null and b/Resources/Textures/_White/Clothing/Eyes/Goggles/nightvision.rsi/equipped-EYES.png differ diff --git a/Resources/Textures/_White/Clothing/Eyes/Goggles/nightvision.rsi/icon.png b/Resources/Textures/_White/Clothing/Eyes/Goggles/nightvision.rsi/icon.png new file mode 100644 index 0000000000..bf770f70f8 Binary files /dev/null and b/Resources/Textures/_White/Clothing/Eyes/Goggles/nightvision.rsi/icon.png differ diff --git a/Resources/Textures/_White/Clothing/Eyes/Goggles/nightvision.rsi/inhand-left.png b/Resources/Textures/_White/Clothing/Eyes/Goggles/nightvision.rsi/inhand-left.png new file mode 100644 index 0000000000..995b37471b Binary files /dev/null and b/Resources/Textures/_White/Clothing/Eyes/Goggles/nightvision.rsi/inhand-left.png differ diff --git a/Resources/Textures/_White/Clothing/Eyes/Goggles/nightvision.rsi/inhand-right.png b/Resources/Textures/_White/Clothing/Eyes/Goggles/nightvision.rsi/inhand-right.png new file mode 100644 index 0000000000..c3efa67f83 Binary files /dev/null and b/Resources/Textures/_White/Clothing/Eyes/Goggles/nightvision.rsi/inhand-right.png differ diff --git a/Resources/Textures/_White/Clothing/Eyes/Goggles/nightvision.rsi/meta.json b/Resources/Textures/_White/Clothing/Eyes/Goggles/nightvision.rsi/meta.json new file mode 100644 index 0000000000..987b20b9af --- /dev/null +++ b/Resources/Textures/_White/Clothing/Eyes/Goggles/nightvision.rsi/meta.json @@ -0,0 +1,48 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from tgstation at https://github.com/tgstation/tgstation", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "equipped-EYES", + "directions": 4, + "delays": [ + [ + 0.1, + 0.1 + ], + [ + 0.1, + 0.1 + ], + [ + 0.1, + 0.1 + ], + [ + 0.1, + 0.1 + ] + ] + }, + { + "name": "equipped-EYES-off", + "directions": 4 + }, + { + "name": "icon" + }, + { + "name": "inhand-left", + "directions": 4 + }, + { + "name": "inhand-right", + "directions": 4 + } + ] +} diff --git a/Resources/Textures/_White/Clothing/Eyes/Goggles/snightvision.rsi/equipped-EYES-off.png b/Resources/Textures/_White/Clothing/Eyes/Goggles/snightvision.rsi/equipped-EYES-off.png new file mode 100644 index 0000000000..4d8f2fdb66 Binary files /dev/null and b/Resources/Textures/_White/Clothing/Eyes/Goggles/snightvision.rsi/equipped-EYES-off.png differ diff --git a/Resources/Textures/_White/Clothing/Eyes/Goggles/snightvision.rsi/equipped-EYES.png b/Resources/Textures/_White/Clothing/Eyes/Goggles/snightvision.rsi/equipped-EYES.png new file mode 100644 index 0000000000..0f9e4efee0 Binary files /dev/null and b/Resources/Textures/_White/Clothing/Eyes/Goggles/snightvision.rsi/equipped-EYES.png differ diff --git a/Resources/Textures/_White/Clothing/Eyes/Goggles/snightvision.rsi/icon.png b/Resources/Textures/_White/Clothing/Eyes/Goggles/snightvision.rsi/icon.png new file mode 100644 index 0000000000..2576acf8a4 Binary files /dev/null and b/Resources/Textures/_White/Clothing/Eyes/Goggles/snightvision.rsi/icon.png differ diff --git a/Resources/Textures/_White/Clothing/Eyes/Goggles/snightvision.rsi/inhand-left.png b/Resources/Textures/_White/Clothing/Eyes/Goggles/snightvision.rsi/inhand-left.png new file mode 100644 index 0000000000..995b37471b Binary files /dev/null and b/Resources/Textures/_White/Clothing/Eyes/Goggles/snightvision.rsi/inhand-left.png differ diff --git a/Resources/Textures/_White/Clothing/Eyes/Goggles/snightvision.rsi/inhand-right.png b/Resources/Textures/_White/Clothing/Eyes/Goggles/snightvision.rsi/inhand-right.png new file mode 100644 index 0000000000..c3efa67f83 Binary files /dev/null and b/Resources/Textures/_White/Clothing/Eyes/Goggles/snightvision.rsi/inhand-right.png differ diff --git a/Resources/Textures/_White/Clothing/Eyes/Goggles/snightvision.rsi/meta.json b/Resources/Textures/_White/Clothing/Eyes/Goggles/snightvision.rsi/meta.json new file mode 100644 index 0000000000..987b20b9af --- /dev/null +++ b/Resources/Textures/_White/Clothing/Eyes/Goggles/snightvision.rsi/meta.json @@ -0,0 +1,48 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from tgstation at https://github.com/tgstation/tgstation", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "equipped-EYES", + "directions": 4, + "delays": [ + [ + 0.1, + 0.1 + ], + [ + 0.1, + 0.1 + ], + [ + 0.1, + 0.1 + ], + [ + 0.1, + 0.1 + ] + ] + }, + { + "name": "equipped-EYES-off", + "directions": 4 + }, + { + "name": "icon" + }, + { + "name": "inhand-left", + "directions": 4 + }, + { + "name": "inhand-right", + "directions": 4 + } + ] +} diff --git a/Resources/Textures/_White/Clothing/Eyes/Goggles/sthermals.rsi/equipped-EYES.png b/Resources/Textures/_White/Clothing/Eyes/Goggles/sthermals.rsi/equipped-EYES.png new file mode 100644 index 0000000000..d60650a5c5 Binary files /dev/null and b/Resources/Textures/_White/Clothing/Eyes/Goggles/sthermals.rsi/equipped-EYES.png differ diff --git a/Resources/Textures/_White/Clothing/Eyes/Goggles/sthermals.rsi/icon.png b/Resources/Textures/_White/Clothing/Eyes/Goggles/sthermals.rsi/icon.png new file mode 100644 index 0000000000..37e91c6f82 Binary files /dev/null and b/Resources/Textures/_White/Clothing/Eyes/Goggles/sthermals.rsi/icon.png differ diff --git a/Resources/Textures/_White/Clothing/Eyes/Goggles/sthermals.rsi/inhand-left.png b/Resources/Textures/_White/Clothing/Eyes/Goggles/sthermals.rsi/inhand-left.png new file mode 100644 index 0000000000..bf67e35d40 Binary files /dev/null and b/Resources/Textures/_White/Clothing/Eyes/Goggles/sthermals.rsi/inhand-left.png differ diff --git a/Resources/Textures/_White/Clothing/Eyes/Goggles/sthermals.rsi/inhand-right.png b/Resources/Textures/_White/Clothing/Eyes/Goggles/sthermals.rsi/inhand-right.png new file mode 100644 index 0000000000..4ede078291 Binary files /dev/null and b/Resources/Textures/_White/Clothing/Eyes/Goggles/sthermals.rsi/inhand-right.png differ diff --git a/Resources/Textures/_White/Clothing/Eyes/Goggles/sthermals.rsi/meta.json b/Resources/Textures/_White/Clothing/Eyes/Goggles/sthermals.rsi/meta.json new file mode 100644 index 0000000000..205508acfa --- /dev/null +++ b/Resources/Textures/_White/Clothing/Eyes/Goggles/sthermals.rsi/meta.json @@ -0,0 +1,26 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from tgstation at https://github.com/tgstation/tgstation", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "icon" + }, + { + "name": "equipped-EYES", + "directions": 4 + }, + { + "name": "inhand-left", + "directions": 4 + }, + { + "name": "inhand-right", + "directions": 4 + } + ] +} diff --git a/Resources/Textures/_White/Clothing/Eyes/Goggles/thermal.rsi/equipped-EYES.png b/Resources/Textures/_White/Clothing/Eyes/Goggles/thermal.rsi/equipped-EYES.png new file mode 100644 index 0000000000..9bef0a8c05 Binary files /dev/null and b/Resources/Textures/_White/Clothing/Eyes/Goggles/thermal.rsi/equipped-EYES.png differ diff --git a/Resources/Textures/_White/Clothing/Eyes/Goggles/thermal.rsi/icon.png b/Resources/Textures/_White/Clothing/Eyes/Goggles/thermal.rsi/icon.png new file mode 100644 index 0000000000..3d5f8ef9b6 Binary files /dev/null and b/Resources/Textures/_White/Clothing/Eyes/Goggles/thermal.rsi/icon.png differ diff --git a/Resources/Textures/_White/Clothing/Eyes/Goggles/thermal.rsi/inhand-left.png b/Resources/Textures/_White/Clothing/Eyes/Goggles/thermal.rsi/inhand-left.png new file mode 100644 index 0000000000..bf67e35d40 Binary files /dev/null and b/Resources/Textures/_White/Clothing/Eyes/Goggles/thermal.rsi/inhand-left.png differ diff --git a/Resources/Textures/_White/Clothing/Eyes/Goggles/thermal.rsi/inhand-right.png b/Resources/Textures/_White/Clothing/Eyes/Goggles/thermal.rsi/inhand-right.png new file mode 100644 index 0000000000..4ede078291 Binary files /dev/null and b/Resources/Textures/_White/Clothing/Eyes/Goggles/thermal.rsi/inhand-right.png differ diff --git a/Resources/Textures/_White/Clothing/Eyes/Goggles/thermal.rsi/meta.json b/Resources/Textures/_White/Clothing/Eyes/Goggles/thermal.rsi/meta.json new file mode 100644 index 0000000000..205508acfa --- /dev/null +++ b/Resources/Textures/_White/Clothing/Eyes/Goggles/thermal.rsi/meta.json @@ -0,0 +1,26 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from tgstation at https://github.com/tgstation/tgstation", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "icon" + }, + { + "name": "equipped-EYES", + "directions": 4 + }, + { + "name": "inhand-left", + "directions": 4 + }, + { + "name": "inhand-right", + "directions": 4 + } + ] +} diff --git a/Resources/Textures/_White/Shaders/nightvision.swsl b/Resources/Textures/_White/Shaders/nightvision.swsl new file mode 100644 index 0000000000..8a3e7706ad --- /dev/null +++ b/Resources/Textures/_White/Shaders/nightvision.swsl @@ -0,0 +1,38 @@ +light_mode unshaded; + +uniform sampler2D SCREEN_TEXTURE; +uniform highp vec3 tint; // Colour of the tint +uniform highp float luminance_threshold; // number between 0 and 1 +uniform highp float noise_amount; // number between 0 and 1 + +lowp float rand (lowp vec2 n) { + return 0.5 + 0.5 * fract (sin (dot (n.xy, vec2 (12.9898, 78.233)))* 43758.5453); +} + +void fragment() { + + highp vec4 color = zTextureSpec(SCREEN_TEXTURE, FRAGCOORD.xy * SCREEN_PIXEL_SIZE); + + // convert color to grayscale using luminance + highp float grey = dot(color.rgb, vec3(0.298, 0.5882, 0.1137)); + + // calculate local threshold + highp float threshold = grey * luminance_threshold; + + // amplify low luminance parts + if (grey < threshold) { + grey += (threshold - grey) * 0.5; + if (grey > 1.0) { + grey = 1.0; + } + } + + // apply night vision color tint + color.rgb = mix(color.rgb, tint, grey); + + // add some noise for realism + lowp float noise = rand(FRAGCOORD.xy + TIME) * noise_amount / 10.0; + color.rgb += noise; + + COLOR = color; +}