Adds Skia (σκιά, shade) as a Midround Antagonist (#4152)

* AAAAAAAAadds skia

* Added to the spawn options table

* Fix EOR, Added shatter ability, Fix Reroll popup

* Fixed the death of the Skia

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Fixed taco's silly mistake

* Skia need pry-ability

* Adds Goob Nightvision

* Added some missing Protos

* Appease the Yaml gods

* Did the Delta Fixes

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Lathes fix

* Update attributions.yml

* More yaml fixes

* The Yaml Linter shall never be sated

* More fixes

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Fixed objective reroll

* Bring up to date

* aaaaaaaaaaa

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Fixed damageset, removed unneeded point light

* I'm so sorry Deltandas

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Signed-off-by: William Lemon <William.Lemon2@gmail.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
William Lemon 2025-09-01 18:46:23 +10:00 committed by GitHub
parent 88c0371931
commit 01088086dc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
124 changed files with 2868 additions and 25 deletions

View File

@ -94,7 +94,9 @@ public abstract class EquipmentHudSystem<T> : EntitySystem where T : IComponent
protected virtual void OnRefreshEquipmentHud(Entity<T> ent, ref InventoryRelayedEvent<RefreshEquipmentHudEvent<T>> args)
{
OnRefreshComponentHud(ent, ref args.Args);
// Goobstation edit
args.Args.Active = true;
args.Args.Components.Add(ent.Comp);
}
protected virtual void OnRefreshComponentHud(Entity<T> ent, ref RefreshEquipmentHudEvent<T> args)

View File

@ -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<AlertPrototype> _lightLevelDarkIcon = "LightLevelDarkIcon";
private ProtoId<AlertPrototype> _lightLevelNeutralIcon = "LightLevelNeutralIcon";
private ProtoId<AlertPrototype> _lightLevelBrightIcon = "LightLevelBrightIcon";
private ProtoId<AlertCategoryPrototype> _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<LightLevelHealthComponent>(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);
}
}

View File

@ -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<Entity<PointLightComponent>> _lightsInRange = new();
private readonly HashSet<Entity<SharedPointLightComponent>> _validLightsInRange = new();
public override HashSet<Entity<SharedPointLightComponent>> 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;
}
}

View File

@ -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 <mary@thughunt.ing>
// SPDX-FileCopyrightText: 2025 Spatison <137375981+Spatison@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 gus <august.eymann@gmail.com>
//
// 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<TComp> : 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<ShaderPrototype>("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);
}
}

View File

@ -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 <mary@thughunt.ing>
// SPDX-FileCopyrightText: 2025 Spatison <137375981+Spatison@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 gus <august.eymann@gmail.com>
//
// 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<NightVisionComponent>
{
[Dependency] private readonly IOverlayManager _overlayMan = default!;
[Dependency] private readonly ILightManager _lightManager = default!;
private BaseSwitchableOverlay<NightVisionComponent> _overlay = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<NightVisionComponent, SwitchableOverlayToggledEvent>(OnToggle);
_overlay = new BaseSwitchableOverlay<NightVisionComponent>();
}
protected override void OnRefreshComponentHud(Entity<NightVisionComponent> ent,
ref RefreshEquipmentHudEvent<NightVisionComponent> args)
{
if (!ent.Comp.IsEquipment)
base.OnRefreshComponentHud(ent, ref args);
}
protected override void OnRefreshEquipmentHud(Entity<NightVisionComponent> ent,
ref InventoryRelayedEvent<RefreshEquipmentHudEvent<NightVisionComponent>> args)
{
if (ent.Comp.IsEquipment)
base.OnRefreshEquipmentHud(ent, ref args);
}
private void OnToggle(Entity<NightVisionComponent> ent, ref SwitchableOverlayToggledEvent args)
{
RefreshOverlay();
}
protected override void UpdateInternal(RefreshEquipmentHudEvent<NightVisionComponent> 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<BaseSwitchableOverlay<NightVisionComponent>>():
_overlayMan.AddOverlay(_overlay);
break;
case null:
_overlayMan.RemoveOverlay(_overlay);
break;
}
if (_overlayMan.TryGetOverlay<BaseSwitchableOverlay<ThermalVisionComponent>>(out var overlay))
overlay.IsActive = nvComp == null;
}
}

View File

@ -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 <mary@thughunt.ing>
// SPDX-FileCopyrightText: 2025 Spatison <137375981+Spatison@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 gus <august.eymann@gmail.com>
//
// 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<ThermalVisionRenderEntry> _entries = [];
private EntityUid? _lightEntity;
public float LightRadius;
public ThermalVisionComponent? Comp;
public ThermalVisionOverlay()
{
IoCManager.InjectDependencies(this);
_container = _entity.System<ContainerSystem>();
_transform = _entity.System<TransformSystem>();
_stealth = _entity.System<StealthSystem>();
_light = _entity.System<SharedPointLightSystem>();
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<PointLightComponent>(_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<BodyComponent, SpriteComponent, TransformComponent>();
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<SpriteComponent>(owner, out var ownerSprite)
&& _entity.TryGetComponent<TransformComponent>(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<SpriteComponent, TransformComponent> 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<SpriteComponent, TransformComponent> Ent,
MapId? Map,
Angle EyeRot);

View File

@ -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 <mary@thughunt.ing>
// SPDX-FileCopyrightText: 2025 Spatison <137375981+Spatison@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 gus <august.eymann@gmail.com>
//
// 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<ThermalVisionComponent>
{
[Dependency] private readonly IOverlayManager _overlayMan = default!;
private ThermalVisionOverlay _thermalOverlay = default!;
private BaseSwitchableOverlay<ThermalVisionComponent> _overlay = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ThermalVisionComponent, SwitchableOverlayToggledEvent>(OnToggle);
_thermalOverlay = new ThermalVisionOverlay();
_overlay = new BaseSwitchableOverlay<ThermalVisionComponent>();
}
protected override void OnRefreshComponentHud(Entity<ThermalVisionComponent> ent,
ref RefreshEquipmentHudEvent<ThermalVisionComponent> args)
{
if (!ent.Comp.IsEquipment)
base.OnRefreshComponentHud(ent, ref args);
}
protected override void OnRefreshEquipmentHud(Entity<ThermalVisionComponent> ent,
ref InventoryRelayedEvent<RefreshEquipmentHudEvent<ThermalVisionComponent>> args)
{
if (ent.Comp.IsEquipment)
base.OnRefreshEquipmentHud(ent, ref args);
}
private void OnToggle(Entity<ThermalVisionComponent> ent, ref SwitchableOverlayToggledEvent args)
{
RefreshOverlay();
}
protected override void UpdateInternal(RefreshEquipmentHudEvent<ThermalVisionComponent> 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<ThermalVisionOverlay>():
_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<BaseSwitchableOverlay<ThermalVisionComponent>>():
_overlayMan.AddOverlay(_overlay);
break;
case null or { DrawOverlay: false }:
_overlayMan.RemoveOverlay(_overlay);
break;
}
// Night vision overlay is prioritized
_overlay.IsActive = !_overlayMan.HasOverlay<BaseSwitchableOverlay<NightVisionComponent>>();
}
}

View File

@ -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<FlashedComponent>(target, FlashedKey, TimeSpan.FromSeconds(flashDuration / 1000f), true) && !ignoreProtection) //DeltaV: allow flashing to ignore flash protection
if (!_statusEffectsSystem.TryAddStatusEffect<FlashedComponent>(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);
}

View File

@ -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<PsychokineticScreamPowerComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<PsychokineticScreamPowerComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<PsychokineticScreamPowerComponent, ShatterLightsActionEvent>(OnShatterLightsAction);
}
private void OnMapInit(Entity<PsychokineticScreamPowerComponent> ent, ref MapInitEvent args)
{
_actions.AddAction(ent, ref ent.Comp.PsychokineticScreamActionEntity, ent.Comp.ShatterLightsActionId);
_actions.StartUseDelay(ent.Comp.PsychokineticScreamActionEntity);
if (TryComp<PsionicComponent>(ent, out var psionic) && psionic.PsionicAbility == null)
{
psionic.PsionicAbility = ent.Comp.PsychokineticScreamActionEntity;
psionic.ActivePowers.Add(ent.Comp);
}
}
private void OnShutdown(Entity<PsychokineticScreamPowerComponent> entity, ref ComponentShutdown args)
{
_actions.RemoveAction(entity.Owner, entity.Comp.PsychokineticScreamActionEntity);
if (TryComp<PsionicComponent>(entity, out var psionic))
{
psionic.ActivePowers.Remove(entity.Comp);
}
}
private void OnShatterLightsAction(Entity<PsychokineticScreamPowerComponent> 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;
}
}

View File

@ -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<Entity<PoweredLightComponent>> _lightsInRange = new();
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ShatterLightsAbilityComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<ShatterLightsAbilityComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<ShatterLightsAbilityComponent, ShatterLightsActionEvent>(OnShatterLightsAction);
}
private void OnMapInit(Entity<ShatterLightsAbilityComponent> ent, ref MapInitEvent args)
{
_actions.AddAction(ent, ref ent.Comp.ShatterLightsActionEntity, ent.Comp.ShatterLightsActionId);
_actions.StartUseDelay(ent.Comp.ShatterLightsActionEntity);
}
private void OnShutdown(Entity<ShatterLightsAbilityComponent> entity, ref ComponentShutdown args)
{
_actions.RemoveAction(entity.Owner, entity.Comp.ShatterLightsActionEntity);
}
private void OnShatterLightsAction(Entity<ShatterLightsAbilityComponent> 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);
}
}
}

View File

@ -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<TechnokineticPulseAbilityComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<TechnokineticPulseAbilityComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<TechnokineticPulseAbilityComponent, TechnokineticPulseActionEvent>(OnTechnokineticPulseAction);
}
private void OnMapInit(Entity<TechnokineticPulseAbilityComponent> ent, ref MapInitEvent args)
{
_actions.AddAction(ent, ref ent.Comp.TechnokineticPulseActionEntity, ent.Comp.TechnokineticPulseActionId);
_actions.StartUseDelay(ent.Comp.TechnokineticPulseActionEntity);
}
private void OnShutdown(Entity<TechnokineticPulseAbilityComponent> entity, ref ComponentShutdown args)
{
_actions.RemoveAction(entity.Owner, entity.Comp.TechnokineticPulseActionEntity);
}
private void OnTechnokineticPulseAction(Entity<TechnokineticPulseAbilityComponent> 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;
}
}

View File

@ -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<LightLevelHealthComponent>();
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);
}
}
}

View File

@ -0,0 +1,4 @@
namespace Content.Server._DV.GameTicking.Rules.Components;
[RegisterComponent]
public sealed partial class SkiaRuleComponent : Component;

View File

@ -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<BreakLightsOnSpawnComponent, MapInitEvent>(OnMapInit);
}
private void OnMapInit(Entity<BreakLightsOnSpawnComponent> entity, ref MapInitEvent args)
{
_shatterLights.ShatterLightsAround(entity.Owner, entity.Comp.Radius, entity.Comp.LineOfSight);
}
}

View File

@ -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<Entity<PointLightComponent>> _lightsInRange = new();
private readonly HashSet<Entity<SharedPointLightComponent>> _validLightsInRange = new();
public override HashSet<Entity<SharedPointLightComponent>> 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;
}
}

View File

@ -0,0 +1,39 @@
using Robust.Shared.Prototypes;
namespace Content.Server._DV.Objectives.Components;
/// <summary>
/// When this objective is completed, duplicate it with a new target.
/// </summary>
[RegisterComponent]
public sealed partial class RerollAfterCompletionComponent : Component
{
/// <summary>
/// If true, the objective has already been rerolled.
/// </summary>
/// <remarks>
/// Ideally this shouldn't matter, as we delete the component once its rolled
/// </remarks>
public bool Rerolled;
/// <summary>
/// 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.
/// </summary>
[DataField]
public EntityUid MindUid = default!;
/// <summary>
/// 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.
/// </summary>
[DataField(required: true)]
public EntProtoId RerollObjectivePrototype = default!;
/// <summary>
/// Message to display when the objective is rerolled.
/// If null, no message will be displayed.
/// </summary>
[DataField]
public LocId? RerollObjectiveMessage;
}

View File

@ -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<RerollAfterCompletionComponent> _objectivesToAdd = new();
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<RerollAfterCompletionComponent, ObjectiveAfterAssignEvent>(OnObjectiveAfterAssign);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
_objectivesToAdd.Clear();
var query = EntityQueryEnumerator<RerollAfterCompletionComponent>();
while (query.MoveNext(out var uid, out var component))
{
if (component.Rerolled) // If already rerolled, skip.
continue;
if (!HasComp<ObjectiveComponent>(uid))
continue; // If the entity doesn't have an ObjectiveComponent, skip.
if (!TryComp<MindComponent>(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<RerollAfterCompletionComponent>(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<MindComponent>(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<TargetObjectiveComponent>(newObjUid, out var targetComp) && TryComp<MindComponent>(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;
}
}

View File

@ -0,0 +1,9 @@
using Content.Shared.Roles;
namespace Content.Shared._DV.Roles;
/// <summary>
/// Added to mind role entities to tag that they are a Skia.
/// </summary>
[RegisterComponent]
public sealed partial class SkiaRoleComponent : BaseMindRoleComponent;

View File

@ -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
{
/// <summary>
/// The radius in which lights will be broken.
/// </summary>
[DataField]
public float Radius = 10f;
/// <summary>
/// If true, lights will only be broken if the entity has line of sight to them.
/// </summary>
[DataField]
public bool LineOfSight = false;
/// <summary>
/// The action that triggers the psychokinetic scream ability.
/// </summary>
[DataField]
public EntProtoId ShatterLightsActionId = "ActionPsychokineticScream";
/// <summary>
/// Standing reference to the action entity, if it exists.
/// </summary>
[DataField, AutoNetworkedField]
public EntityUid? PsychokineticScreamActionEntity;
/// <summary>
/// The sound to play when the ability is used.
/// </summary>
[DataField]
public SoundSpecifier AbilitySound = new SoundPathSpecifier("/Audio/_DV/Effects/creepyshriek.ogg");
}
public sealed partial class ShatterLightsActionEvent : InstantActionEvent;

View File

@ -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
{
/// <summary>
/// The radius in which lights will be broken.
/// </summary>
[DataField]
public float Radius = 10f;
/// <summary>
/// If true, lights will only be broken if the entity has line of sight to them.
/// </summary>
[DataField]
public bool LineOfSight = false;
/// <summary>
/// The action that triggers the shatter lights ability.
/// </summary>
[DataField]
public EntProtoId ShatterLightsActionId = "ActionShatterLights";
/// <summary>
/// Standing reference to the action entity, if it exists.
/// </summary>
[DataField, AutoNetworkedField]
public EntityUid? ShatterLightsActionEntity;
/// <summary>
/// The sound to play when the ability is used.
/// </summary>
[DataField]
public SoundSpecifier AbilitySound = new SoundPathSpecifier("/Audio/_DV/Effects/creepyshriek.ogg");
}
public sealed partial class ShatterLightsActionEvent : InstantActionEvent;

View File

@ -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
{
/// <summary>
/// The radius in which to EMP
/// </summary>
[DataField]
public float Range = 1.0f;
/// <summary>
/// The amount of power to drain from batteries
/// </summary>
[DataField]
public float EnergyConsumption = 20000;
/// <summary>
/// The duration for which devices are disabled.
/// </summary>
[DataField]
public float DisableDuration = 20f;
/// <summary>
/// The action that triggers the technokinetic pulse ability.
/// </summary>
[DataField]
public EntProtoId TechnokineticPulseActionId = "ActionTechnokineticPulse";
/// <summary>
/// Standing reference to the action entity, if it exists.
/// </summary>
[DataField, AutoNetworkedField]
public EntityUid? TechnokineticPulseActionEntity;
}
public sealed partial class TechnokineticPulseActionEvent : InstantActionEvent;

View File

@ -0,0 +1,30 @@
using Content.Shared.Damage;
using Robust.Shared.GameStates;
namespace Content.Shared._DV.Body;
/// <summary>
/// Component that allows a body to deal or receive modified damage amounts based on their light level.
/// Requires <see cref="LightLevelHealthComponent"/> to function.
/// </summary>
[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!;
}

View File

@ -0,0 +1,58 @@
using Content.Shared.Damage;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
namespace Content.Shared._DV.Body;
/// <summary>
/// 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.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class LightLevelHealthComponent : Component
{
/// <summary>
/// Level of light that, when below, we are considered in darkness.
/// </summary>
[DataField]
public float DarkThreshold = 0.2f;
/// <summary>
/// Level of light that, when above, we are considered in light.
/// </summary>
[DataField]
public float LightThreshold = 0.8f;
/// <summary>
/// Amount of health or damage per second when in darkness. Positive values harm, negative values heal.
/// </summary>
[DataField(required: true)]
public DamageSpecifier DarkDamage = default!;
/// <summary>
/// Amount of health or damage per second when in light. Positive values harm, negative values heal.
/// </summary>
[DataField(required: true)]
public DamageSpecifier LightDamage = default!;
/// <summary>
/// Movement speed multiplier when in darkness.
/// </summary>
[DataField]
public float DarkMovementSpeedMultiplier = 1.0f;
/// <summary>
/// Movement speed multiplier when in light.
/// </summary>
[DataField]
public float LightMovementSpeedMultiplier = 1.0f;
/// <summary>
/// Sound to play when the entity is damaged by light or darkness.
/// </summary>
[DataField]
public SoundSpecifier SizzleSoundPath = new SoundPathSpecifier("/Audio/Effects/lightburn.ogg");
/// <summary>
/// The current light threshhold for this component.
/// -1 for darkness, 1 for light.
/// 0 for neither.
/// </summary>
[DataField]
public int CurrentThreshold = 0;
}

View File

@ -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<LightLevelHealthComponent, RefreshMovementSpeedModifiersEvent>(OnGetMoveSpeedModifiers);
SubscribeLocalEvent<LightLevelDamageMultComponent, DamageModifyEvent>(OnDamageModify);
SubscribeLocalEvent<LightLevelDamageMultComponent, GetMeleeDamageEvent>(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<LightLevelHealthComponent> target, DamageSpecifier damage)
{
if (damage.AnyPositive())
{
_audio.PlayPvs(target.Comp.SizzleSoundPath, target);
}
_damageable.TryChangeDamage(target, damage, true, false);
}
private void OnGetMoveSpeedModifiers(Entity<LightLevelHealthComponent> ent, ref RefreshMovementSpeedModifiersEvent args)
{
if (!TryComp<LightReactiveComponent>(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<LightLevelDamageMultComponent> ent, ref DamageModifyEvent args)
{
if (!TryComp<LightLevelHealthComponent>(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<LightLevelDamageMultComponent> ent, ref GetMeleeDamageEvent args)
{
if (!TryComp<LightLevelHealthComponent>(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);
}
}

View File

@ -0,0 +1,19 @@
using Robust.Shared.GameStates;
namespace Content.Shared._DV.Light;
[RegisterComponent, NetworkedComponent]
public sealed partial class BreakLightsOnSpawnComponent : Component
{
/// <summary>
/// The radius in which lights will be broken.
/// </summary>
[DataField]
public float Radius = 10f;
/// <summary>
/// If true, lights will only be broken if the entity has line of sight to them.
/// </summary>
[DataField]
public bool LineOfSight = false;
}

View File

@ -0,0 +1,44 @@
using Robust.Shared.GameStates;
namespace Content.Shared._DV.Light;
/// <summary>
/// A component that reacts to changes in light levels.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(SharedLightReactiveSystem))]
public sealed partial class LightReactiveComponent : Component
{
/// <summary>
/// The frequency at which the component checks for light level changes.
/// There should be very little reason it should be higher than this.
/// </summary>
[DataField]
public TimeSpan UpdateFrequency = TimeSpan.FromSeconds(1);
/// <summary>
/// Whether the component should only update while the entity is alive.
/// If false, it will update even if the entity is dead.
/// </summary>
[DataField]
public bool OnlyWhileAlive = true;
/// <summary>
/// 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.
/// </summary>
[DataField]
public bool Manual = false;
/// <summary>
/// The next time the component should update.
/// </summary>
[AutoNetworkedField]
public TimeSpan NextUpdate = TimeSpan.Zero;
/// <summary>
/// The current light level of this entity.
/// </summary>
[DataField]
public float CurrentLightLevel = 0f;
}

View File

@ -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<LightReactiveComponent> _lightReactive;
public override void Initialize()
{
base.Initialize();
_lightReactive = GetEntityQuery<LightReactiveComponent>();
}
public override void Update(float frameTime)
{
var query = EntityQueryEnumerator<LightReactiveComponent>();
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<Entity<SharedPointLightComponent>> GetLights(EntityUid targetEntity);
/// <summary>
/// Gets the current light level of an entity.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
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;
}
/// <summary>
/// Gets the light level at a specific point in the world.
/// Avoid calling this too often, as it can be expensive.
/// </summary>
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;
}
}

View File

@ -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 <mary@thughunt.ing>
// SPDX-FileCopyrightText: 2025 Spatison <137375981+Spatison@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 gus <august.eymann@gmail.com>
//
// 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;
}

View File

@ -0,0 +1,39 @@
// SPDX-FileCopyrightText: 2025 Aiden <28298836+Aidenkrz@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
// SPDX-FileCopyrightText: 2025 Misandry <mary@thughunt.ing>
// SPDX-FileCopyrightText: 2025 Solstice <solsticeofthewinter@gmail.com>
// SPDX-FileCopyrightText: 2025 SolsticeOfTheWinter <solsticeofthewinter@gmail.com>
// SPDX-FileCopyrightText: 2025 gus <august.eymann@gmail.com>
// SPDX-FileCopyrightText: 2025 pheenty <fedorlukin2006@gmail.com>
//
// 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<InventoryComponent, FlashDurationMultiplierEvent>(RelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, RefreshEquipmentHudEvent<Overlays.NightVisionComponent>>(RefRelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, RefreshEquipmentHudEvent<Overlays.ThermalVisionComponent>>(RefRelayInventoryEvent);
}
private void RefRelayInventoryEvent<T>(EntityUid uid, InventoryComponent component, ref T args) where T : IInventoryRelayEvent
{
_inventorySystem.RelayEvent((uid, component), ref args);
}
private void RelayInventoryEvent<T>(EntityUid uid, InventoryComponent component, T args) where T : IInventoryRelayEvent
{
_inventorySystem.RelayEvent((uid, component), args);
}
}

View File

@ -0,0 +1,17 @@
// SPDX-FileCopyrightText: 2025 Aiden <28298836+Aidenkrz@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 Misandry <mary@thughunt.ing>
// SPDX-FileCopyrightText: 2025 gus <august.eymann@gmail.com>
//
// 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();
}
}

View File

@ -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 <mary@thughunt.ing>
// SPDX-FileCopyrightText: 2025 Spatison <137375981+Spatison@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 gus <august.eymann@gmail.com>
//
// 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;
}

View File

@ -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 <mary@thughunt.ing>
// SPDX-FileCopyrightText: 2025 Spatison <137375981+Spatison@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 gus <august.eymann@gmail.com>
//
// 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;

View File

@ -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 <mary@thughunt.ing>
// SPDX-FileCopyrightText: 2025 Spatison <137375981+Spatison@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 gus <august.eymann@gmail.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace Content.Shared._Goobstation.Overlays;
public sealed class SharedNightVisionSystem : SwitchableOverlaySystem<NightVisionComponent, ToggleNightVisionEvent>;

View File

@ -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 <mary@thughunt.ing>
// SPDX-FileCopyrightText: 2025 Spatison <137375981+Spatison@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 gus <august.eymann@gmail.com>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace Content.Shared._Goobstation.Overlays;
public sealed class SharedThermalVisionSystem : SwitchableOverlaySystem<ThermalVisionComponent, ToggleThermalVisionEvent>;

View File

@ -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 <mary@thughunt.ing>
// SPDX-FileCopyrightText: 2025 Spatison <137375981+Spatison@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 gus <august.eymann@gmail.com>
//
// 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<TComp, TEvent> : 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<TComp, TEvent>(OnToggle);
SubscribeLocalEvent<TComp, ComponentInit>(OnInit);
SubscribeLocalEvent<TComp, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<TComp, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<TComp, GetItemActionsEvent>(OnGetItemActions);
SubscribeLocalEvent<TComp, ComponentGetState>(OnGetState);
SubscribeLocalEvent<TComp, ComponentHandleState>(OnHandleState);
SubscribeLocalEvent<TComp, FlashDurationMultiplierEvent>(OnGetFlashMultiplier);
SubscribeLocalEvent<TComp, InventoryRelayedEvent<FlashDurationMultiplierEvent>>(OnGetInventoryFlashMultiplier);
}
private void OnGetFlashMultiplier(Entity<TComp> ent, ref FlashDurationMultiplierEvent args)
{
if (!ent.Comp.IsEquipment)
args.Multiplier *= GetFlashMultiplier(ent);
}
private void OnGetInventoryFlashMultiplier(Entity<TComp> ent,
ref InventoryRelayedEvent<FlashDurationMultiplierEvent> 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<TComp>();
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<TComp> 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);

View File

@ -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 <mary@thughunt.ing>
// SPDX-FileCopyrightText: 2025 Spatison <137375981+Spatison@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 gus <august.eymann@gmail.com>
//
// 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;
/// <summary>
/// Whether it should grant equipment enhanced vision or is it mob vision
/// </summary>
[DataField]
public bool IsEquipment;
/// <summary>
/// If it is greater than 0, overlay isn't toggled but pulsed instead
/// </summary>
[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;
}

View File

@ -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 <mary@thughunt.ing>
// SPDX-FileCopyrightText: 2025 Spatison <137375981+Spatison@users.noreply.github.com>
// SPDX-FileCopyrightText: 2025 gus <august.eymann@gmail.com>
//
// 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;

View File

@ -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"

Binary file not shown.

Binary file not shown.

View File

@ -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"

Binary file not shown.

View File

@ -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.

View File

@ -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

View File

@ -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)}.

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -6,6 +6,7 @@
TelegnosisPower: 1
PsionicRegenerationPower: 1
PrecognitionPower: 1 # DeltaV
PsychokineticScreamPower: 1
MassSleepPower: 0.3
NoosphericZapPower: 0.3
# PsionicInvisibilityPower: 0.15

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,2 @@
- type: alertCategory
id: Light

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -5,7 +5,7 @@
jumpsuit: ClothingUniformJumpsuitOperative
back: ClothingBackpackDuffelSyndicate
mask: ClothingMaskGasSyndicate
eyes: ClothingEyesHudSyndicate
eyes: ClothingEyesNightVisionGogglesNukie # Goobstation
ears: ClothingHeadsetAltSyndicate
gloves: ClothingHandsGlovesCombat
outerClothing: ClothingOuterVestWeb

View File

@ -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

View File

@ -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

View File

@ -173,3 +173,6 @@
- type: Tag
id: CleaningGrenade
- type: Tag
id: Hoodie

View File

@ -0,0 +1,15 @@
# SPDX-FileCopyrightText: 2025 Aiden <28298836+Aidenkrz@users.noreply.github.com>
# SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
# SPDX-FileCopyrightText: 2025 PunishedJoe <PunishedJoeseph@proton.me>
# SPDX-FileCopyrightText: 2025 Roudenn <romabond091@gmail.com>
# SPDX-FileCopyrightText: 2025 SX_7 <sn1.test.preria.2002@gmail.com>
# SPDX-FileCopyrightText: 2025 Ted Lukin <66275205+pheenty@users.noreply.github.com>
# SPDX-FileCopyrightText: 2025 pheenty <fedorlukin2006@gmail.com>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
## Dynamic
- type: latheRecipePack # WD
id: SpecOpsGoogles
recipes:
- ClothingEyesNightVisionGoggles
- ClothingEyesGlassesThermal

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,39 @@
# SPDX-FileCopyrightText: 2025 Aiden <28298836+Aidenkrz@users.noreply.github.com>
# SPDX-FileCopyrightText: 2025 Aiden <aiden@djkraz.com>
# SPDX-FileCopyrightText: 2025 Aviu00 <93730715+Aviu00@users.noreply.github.com>
# SPDX-FileCopyrightText: 2025 FaDeOkno <143940725+FaDeOkno@users.noreply.github.com>
# SPDX-FileCopyrightText: 2025 FaDeOkno <logkedr18@gmail.com>
# 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

View File

@ -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 <eris@erisws.com>
# SPDX-FileCopyrightText: 2025 GoobBot <uristmchands@proton.me>
# SPDX-FileCopyrightText: 2025 SX-7 <sn1.test.preria.2002@gmail.com>
# 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"

View File

@ -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"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 760 B

View File

@ -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"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 934 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 482 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 820 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 819 B

View File

@ -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
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 729 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 667 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 678 B

View File

@ -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
}
]
}

View File

@ -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"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 609 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 661 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 898 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 859 B

View File

@ -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"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 746 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 613 B

View File

@ -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"
}
]
}

Some files were not shown because too many files have changed in this diff Show More