From 41a8b1bef8fbcb5c6314b534ade930f11a775318 Mon Sep 17 00:00:00 2001 From: Peptide90 <78795277+Peptide90@users.noreply.github.com> Date: Sat, 14 Sep 2024 21:06:39 +0100 Subject: [PATCH 01/55] Weather Block Marker, Rad Markers and Invisible Walls (#862) Ported from Nuclear14 by request of @OldDanceJacket as they wanted the weather blocking markers. Tought you may as well get the rest of the stuff for rad markers and invisible walls too. Useful for planets. --------- Signed-off-by: Peptide90 <78795277+Peptide90@users.noreply.github.com> Signed-off-by: VMSolidus Co-authored-by: VMSolidus Co-authored-by: DEATHB4DEFEAT <77995199+DEATHB4DEFEAT@users.noreply.github.com> --- .../Entities/Markers/environmental.yml | 100 ++++++++++++++++++ .../Markers/environment.rsi/base-blue.png | Bin 0 -> 250 bytes .../Markers/environment.rsi/base-green.png | Bin 0 -> 198 bytes .../Markers/environment.rsi/base-red.png | Bin 0 -> 255 bytes .../Textures/Markers/environment.rsi/fire.png | Bin 0 -> 227 bytes .../Markers/environment.rsi/meta.json | 32 ++++++ .../Textures/Markers/environment.rsi/rad.png | Bin 0 -> 201 bytes .../Textures/Markers/environment.rsi/wall.png | Bin 0 -> 265 bytes .../Markers/environment.rsi/weather.png | Bin 0 -> 161 bytes 9 files changed, 132 insertions(+) create mode 100644 Resources/Prototypes/Entities/Markers/environmental.yml create mode 100644 Resources/Textures/Markers/environment.rsi/base-blue.png create mode 100644 Resources/Textures/Markers/environment.rsi/base-green.png create mode 100644 Resources/Textures/Markers/environment.rsi/base-red.png create mode 100644 Resources/Textures/Markers/environment.rsi/fire.png create mode 100644 Resources/Textures/Markers/environment.rsi/meta.json create mode 100644 Resources/Textures/Markers/environment.rsi/rad.png create mode 100644 Resources/Textures/Markers/environment.rsi/wall.png create mode 100644 Resources/Textures/Markers/environment.rsi/weather.png diff --git a/Resources/Prototypes/Entities/Markers/environmental.yml b/Resources/Prototypes/Entities/Markers/environmental.yml new file mode 100644 index 00000000000..06425183561 --- /dev/null +++ b/Resources/Prototypes/Entities/Markers/environmental.yml @@ -0,0 +1,100 @@ +# Radiation +- type: entity + name: Marker Radiation + id: MarkerRadiation1 + parent: MarkerBase + suffix: intensity 1 + components: + - type: Sprite + layers: + - sprite: Markers/environment.rsi + state: base-green + shader: unshaded + - sprite: Markers/environment.rsi + shader: unshaded + state: rad + - type: RadiationSource + intensity: 1 + +- type: entity + parent: MarkerRadiation1 + id: MarkerRadiation2 + suffix: intensity 2 + components: + - type: RadiationSource + intensity: 2 + +- type: entity + parent: MarkerRadiation1 + id: MarkerRadiation3 + suffix: intensity 3 + components: + - type: RadiationSource + intensity: 3 + +- type: entity + parent: MarkerRadiation1 + id: MarkerRadiation4 + suffix: intensity 4 + components: + - type: RadiationSource + intensity: 4 + +- type: entity + parent: MarkerRadiation1 + id: MarkerRadiation5 + suffix: intensity 5 + components: + - type: RadiationSource + intensity: 5 + +- type: entity + parent: MarkerRadiation1 + id: MarkerRadiation10 + suffix: intensity 10 + components: + - type: RadiationSource + intensity: 10 + +# Invisible Walls +- type: entity + name: Marker Blocker + id: MarkerBlocker + parent: MarkerBase + suffix: invisible wall + components: + - type: Sprite + layers: + - sprite: Markers/environment.rsi + state: base-blue + shader: unshaded + - sprite: Markers/environment.rsi + shader: unshaded + state: wall + - type: PlacementReplacement + key: blocker + - type: Fixtures + fixtures: + fix1: + shape: + !type:PhysShapeAabb + bounds: "-0.5,-0.5,0.5,0.5" + mask: + - FullTileMask + layer: + - WallLayer + density: 1000 + - type: Physics + bodyType: Static + + +# Weather Blocker +- type: entity + name: Marker Weather Blocker + id: MarkerWeatherblocker + parent: MarkerBase + components: + - type: Sprite + sprite: Markers/environment.rsi + state: weather + - type: BlockWeather diff --git a/Resources/Textures/Markers/environment.rsi/base-blue.png b/Resources/Textures/Markers/environment.rsi/base-blue.png new file mode 100644 index 0000000000000000000000000000000000000000..ee77bb448aca3d323b31aa128d8ba2832b121ae9 GIT binary patch literal 250 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdzwj^(N7lsBP{NFO^{{x^1XMsm# zF$061G6*wPEVVBK3bL1Y`ns||!MP|ku_QG` zp**uBL&4qCHz2%`PaLQy$l=9+40u>Ca-Z^uY_M~#Tye!9N<)Bofx?|T z@(1cB#ok!DHF?qut_#2CU8?E%%*Nm{WoeS&=iEll4HIP;UpDO8;L_*Nz`)4F!XcpG n(9n>7{?qgD-`1+XZTB;HckoTPJ9nxQ&^887S3j3^P6aDG!CxKcSJYD@<);T3K0RS;VHRAvP literal 0 HcmV?d00001 diff --git a/Resources/Textures/Markers/environment.rsi/base-red.png b/Resources/Textures/Markers/environment.rsi/base-red.png new file mode 100644 index 0000000000000000000000000000000000000000..e0d68f8b9e52b47e75c9a8f158e1eece39c501f9 GIT binary patch literal 255 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdzwj^(N7l!{JxM1({$v_d#0*}aI z1_nK45N51cYF`EvWH0gbb!C6b!z3WdDIK@{I#5Wm#5JPCIX^cyHLrxhxhOTUBsE2$ zJhLQ2!QIn0AiR-J9H=PW)5S3);_%z+8+jWHcvvrTpYqUg;7k%NJHfZ6Nh5JffC8I? zWU=bsSd5KwSv rU|?io;Sl)nHtOGU9-nVZbkR9}!xfMvUl?3?(|7Z9Q1R*zjKoXNZT^vI!dRs4b^ED`Num(tc z{$FqD9PmUj_3Ya_k=L27B=4AcXJKK>F-wPHHxCz^;+3Kn&wcK#E@-$mV7&(Ht;w0Ux?vdKE@$f`?^zTG%v z`Ah6xZTEw9u3vV$K4O;It{Y^oTV!5e`fj)D)8FgY$=LgJvISm!EXfCS9)qW=pUXO@ GgeCxf5^ZPz literal 0 HcmV?d00001 diff --git a/Resources/Textures/Markers/environment.rsi/weather.png b/Resources/Textures/Markers/environment.rsi/weather.png new file mode 100644 index 0000000000000000000000000000000000000000..adb9f53a4739a2f117e9348c5af8d9a0a3f1ae4e GIT binary patch literal 161 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz#^NA%Cx&(BWL^R}(Vi}jArY-_ zFI?nmP~d5O7|g$|{ryqD@L9ZPSYwa=KPXU|=gcYIp5tkI%kGKX&ze2*_ols_y2SZ* z?%bzGcHjHVsMT8JccPzp8hb`-nDdjg`F|ar?q&oU{2<)#!gJk)5^p&t19=Rdu6{1- HoD!M Date: Mon, 16 Sep 2024 00:41:44 -0400 Subject: [PATCH 02/55] Harpy Flight System (#919) # Description This PR adds a generic system which gives an entity the ability to fly. Optionally increasing their speed in exchange for a continuous stamina drain, which can, and **will** stamcrit them if left unchecked. --- # Technical Details? We normally dont have this section but I'd like to outline the changes since I messed with quite a few systems: - Introduces a `FlightComponent` which can be added to any entity in YML, needs to be tied to an action with an event of type `ToggleFlightEvent` This component holds properties for: - Toggling animations on and off, either at the entity level or the layer level. - Altering shader animation properties - Altering speed, stamina drain, sounds played, delay between sounds, etc etc. - Adds a `FlyingVisualizerSystem` that can take a given `AnimationKey` which points to a shader, and optionally can apply it to either the entire sprite, or a given layer. - Adds a check in `SharedGravitySystem` for making the entity weightless when it has the `FlightComponent` and is flying. - Adds a check in `SharedCuffableSystem` to disable cuffing when the target has the `FlightComponent` and is flying. - Introduces a new field in the `StaminaComponent` which serves as a dictionary for persistent drains, with the key being the source (UID) of where it came from. The drains can also indicate if they should apply the stamina slowdown or not (relevant for both this PR, and for an eventual sprinting PR) ---

Media

[![Flight Demo](https://i.ytimg.com/vi/Wndv9hYaZ_s/maxresdefault.jpg)](https://youtu.be/Wndv9hYaZ_s "Flight Demo")

--- # Changelog :cl: Mocho - add: Harpies are now able to fly on station for limited periods of time, moving faster at the cost of stamina. --------- Signed-off-by: gluesniffler <159397573+gluesniffler@users.noreply.github.com> Co-authored-by: VMSolidus --- .../Components/FlightVisualsComponent.cs | 40 +++++ Content.Client/Flight/FlightSystem.cs | 67 ++++++++ .../Flight/FlyingVisualizerSystem.cs | 64 +++++++ Content.Server/Flight/FlightSystem.cs | 158 ++++++++++++++++++ Content.Shared/Cuffs/SharedCuffableSystem.cs | 10 +- .../Damage/Components/StaminaComponent.cs | 9 +- .../Damage/Systems/StaminaSystem.cs | 48 ++++-- Content.Shared/Flight/Events.cs | 24 +++ Content.Shared/Flight/FlightComponent.cs | 101 +++++++++++ Content.Shared/Flight/SharedFlightSystem.cs | 104 ++++++++++++ .../Gravity/SharedFloatingVisualizerSystem.cs | 18 +- Content.Shared/Gravity/SharedGravitySystem.cs | 6 +- Resources/Audio/Effects/Flight/wingflap1.ogg | Bin 0 -> 10302 bytes Resources/Audio/Effects/Flight/wingflap2.ogg | Bin 0 -> 13035 bytes Resources/Audio/Effects/Flight/wingflap3.ogg | Bin 0 -> 10881 bytes .../cuffs/components/handcuff-component.ftl | 1 + .../Locale/en-US/flight/flight_system.ftl | 2 + .../Entities/Mobs/Species/harpy.yml | 16 ++ Resources/Prototypes/Shaders/shaders.yml | 7 + .../Prototypes/SoundCollections/flight.yml | 6 + .../Actions/flight.rsi/flight_off.png | Bin 0 -> 15819 bytes .../Actions/flight.rsi/flight_on.png | Bin 0 -> 16238 bytes .../Interface/Actions/flight.rsi/meta.json | 17 ++ Resources/Textures/Shaders/flap.swsl | 35 ++++ 24 files changed, 718 insertions(+), 15 deletions(-) create mode 100644 Content.Client/Flight/Components/FlightVisualsComponent.cs create mode 100644 Content.Client/Flight/FlightSystem.cs create mode 100644 Content.Client/Flight/FlyingVisualizerSystem.cs create mode 100644 Content.Server/Flight/FlightSystem.cs create mode 100644 Content.Shared/Flight/Events.cs create mode 100644 Content.Shared/Flight/FlightComponent.cs create mode 100644 Content.Shared/Flight/SharedFlightSystem.cs create mode 100644 Resources/Audio/Effects/Flight/wingflap1.ogg create mode 100644 Resources/Audio/Effects/Flight/wingflap2.ogg create mode 100644 Resources/Audio/Effects/Flight/wingflap3.ogg create mode 100644 Resources/Locale/en-US/flight/flight_system.ftl create mode 100644 Resources/Prototypes/SoundCollections/flight.yml create mode 100644 Resources/Textures/Interface/Actions/flight.rsi/flight_off.png create mode 100644 Resources/Textures/Interface/Actions/flight.rsi/flight_on.png create mode 100644 Resources/Textures/Interface/Actions/flight.rsi/meta.json create mode 100644 Resources/Textures/Shaders/flap.swsl diff --git a/Content.Client/Flight/Components/FlightVisualsComponent.cs b/Content.Client/Flight/Components/FlightVisualsComponent.cs new file mode 100644 index 00000000000..3f378f60ef2 --- /dev/null +++ b/Content.Client/Flight/Components/FlightVisualsComponent.cs @@ -0,0 +1,40 @@ +using Robust.Client.Graphics; +using Robust.Shared.GameStates; + +namespace Content.Client.Flight.Components; + +[RegisterComponent] +public sealed partial class FlightVisualsComponent : Component +{ + /// + /// How long does the animation last + /// + [DataField] + public float Speed; + + /// + /// How far it goes in any direction. + /// + [DataField] + public float Multiplier; + + /// + /// How much the limbs (if there are any) rotate. + /// + [DataField] + public float Offset; + + /// + /// Are we animating layers or the entire sprite? + /// + public bool AnimateLayer = false; + public int? TargetLayer; + + [DataField] + public string AnimationKey = "default"; + + [ViewVariables(VVAccess.ReadWrite)] + public ShaderInstance Shader = default!; + + +} \ No newline at end of file diff --git a/Content.Client/Flight/FlightSystem.cs b/Content.Client/Flight/FlightSystem.cs new file mode 100644 index 00000000000..bd1a6767bd9 --- /dev/null +++ b/Content.Client/Flight/FlightSystem.cs @@ -0,0 +1,67 @@ +using Robust.Client.GameObjects; +using Content.Shared.Flight; +using Content.Shared.Flight.Events; +using Content.Client.Flight.Components; + +namespace Content.Client.Flight; +public sealed class FlightSystem : SharedFlightSystem +{ + [Dependency] private readonly IEntityManager _entityManager = default!; + public override void Initialize() + { + base.Initialize(); + + SubscribeNetworkEvent(OnFlight); + + } + + private void OnFlight(FlightEvent args) + { + var uid = GetEntity(args.Uid); + if (!_entityManager.TryGetComponent(uid, out SpriteComponent? sprite) + || !args.IsAnimated + || !_entityManager.TryGetComponent(uid, out FlightComponent? flight)) + return; + + + int? targetLayer = null; + if (flight.IsLayerAnimated && flight.Layer is not null) + { + targetLayer = GetAnimatedLayer(uid, flight.Layer, sprite); + if (targetLayer == null) + return; + } + + if (args.IsFlying && args.IsAnimated && flight.AnimationKey != "default") + { + var comp = new FlightVisualsComponent + { + AnimateLayer = flight.IsLayerAnimated, + AnimationKey = flight.AnimationKey, + Multiplier = flight.ShaderMultiplier, + Offset = flight.ShaderOffset, + Speed = flight.ShaderSpeed, + TargetLayer = targetLayer, + }; + AddComp(uid, comp); + } + if (!args.IsFlying) + RemComp(uid); + } + + public int? GetAnimatedLayer(EntityUid uid, string targetLayer, SpriteComponent? sprite = null) + { + if (!Resolve(uid, ref sprite)) + return null; + + int index = 0; + foreach (var layer in sprite.AllLayers) + { + // This feels like absolute shitcode, isn't there a better way to check for it? + if (layer.Rsi?.Path.ToString() == targetLayer) + return index; + index++; + } + return null; + } +} \ No newline at end of file diff --git a/Content.Client/Flight/FlyingVisualizerSystem.cs b/Content.Client/Flight/FlyingVisualizerSystem.cs new file mode 100644 index 00000000000..6dde6cf5638 --- /dev/null +++ b/Content.Client/Flight/FlyingVisualizerSystem.cs @@ -0,0 +1,64 @@ +using Content.Client.Flight.Components; +using Robust.Client.GameObjects; +using Robust.Client.Graphics; +using Robust.Shared.Prototypes; + +namespace Content.Client.Flight; + +/// +/// Handles offsetting an entity while flying +/// +public sealed class FlyingVisualizerSystem : EntitySystem +{ + [Dependency] private readonly IPrototypeManager _protoMan = default!; + [Dependency] private readonly SpriteSystem _spriteSystem = default!; + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnStartup); + SubscribeLocalEvent(OnShutdown); + SubscribeLocalEvent(OnBeforeShaderPost); + } + + private void OnStartup(EntityUid uid, FlightVisualsComponent comp, ComponentStartup args) + { + comp.Shader = _protoMan.Index(comp.AnimationKey).InstanceUnique(); + AddShader(uid, comp.Shader, comp.AnimateLayer, comp.TargetLayer); + SetValues(comp, comp.Speed, comp.Offset, comp.Multiplier); + } + + private void OnShutdown(EntityUid uid, FlightVisualsComponent comp, ComponentShutdown args) + { + AddShader(uid, null, comp.AnimateLayer, comp.TargetLayer); + } + + private void AddShader(Entity entity, ShaderInstance? shader, bool animateLayer, int? layer) + { + if (!Resolve(entity, ref entity.Comp, false)) + return; + + if (!animateLayer) + entity.Comp.PostShader = shader; + + if (animateLayer && layer is not null) + entity.Comp.LayerSetShader(layer.Value, shader); + + entity.Comp.GetScreenTexture = shader is not null; + entity.Comp.RaiseShaderEvent = shader is not null; + } + + /// + /// This function can be used to modify the shader's values while its running. + /// + private void OnBeforeShaderPost(EntityUid uid, FlightVisualsComponent comp, ref BeforePostShaderRenderEvent args) + { + SetValues(comp, comp.Speed, comp.Offset, comp.Multiplier); + } + + private void SetValues(FlightVisualsComponent comp, float speed, float offset, float multiplier) + { + comp.Shader.SetParameter("Speed", speed); + comp.Shader.SetParameter("Offset", offset); + comp.Shader.SetParameter("Multiplier", multiplier); + } +} \ No newline at end of file diff --git a/Content.Server/Flight/FlightSystem.cs b/Content.Server/Flight/FlightSystem.cs new file mode 100644 index 00000000000..e056fc24ec0 --- /dev/null +++ b/Content.Server/Flight/FlightSystem.cs @@ -0,0 +1,158 @@ + +using Content.Shared.Cuffs.Components; +using Content.Shared.Damage.Components; +using Content.Shared.DoAfter; +using Content.Shared.Flight; +using Content.Shared.Flight.Events; +using Content.Shared.Mobs; +using Content.Shared.Popups; +using Content.Shared.Stunnable; +using Content.Shared.Zombies; +using Robust.Shared.Audio.Systems; + +namespace Content.Server.Flight; +public sealed class FlightSystem : SharedFlightSystem +{ + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; + [Dependency] private readonly SharedPopupSystem _popupSystem = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnToggleFlight); + SubscribeLocalEvent(OnFlightDoAfter); + SubscribeLocalEvent(OnMobStateChangedEvent); + SubscribeLocalEvent(OnZombified); + SubscribeLocalEvent(OnKnockedDown); + SubscribeLocalEvent(OnStunned); + SubscribeLocalEvent(OnSleep); + } + public override void Update(float frameTime) + { + base.Update(frameTime); + + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var component)) + { + if (!component.On) + continue; + + component.TimeUntilFlap -= frameTime; + + if (component.TimeUntilFlap > 0f) + continue; + + _audio.PlayPvs(component.FlapSound, uid); + component.TimeUntilFlap = component.FlapInterval; + + } + } + + #region Core Functions + private void OnToggleFlight(EntityUid uid, FlightComponent component, ToggleFlightEvent args) + { + // If the user isnt flying, we check for conditionals and initiate a doafter. + if (!component.On) + { + if (!CanFly(uid, component)) + return; + + var doAfterArgs = new DoAfterArgs(EntityManager, + uid, component.ActivationDelay, + new FlightDoAfterEvent(), uid, target: uid) + { + BlockDuplicate = true, + BreakOnTargetMove = true, + BreakOnUserMove = true, + BreakOnDamage = true, + NeedHand = true + }; + + if (!_doAfter.TryStartDoAfter(doAfterArgs)) + return; + } + else + ToggleActive(uid, false, component); + } + + private void OnFlightDoAfter(EntityUid uid, FlightComponent component, FlightDoAfterEvent args) + { + if (args.Handled || args.Cancelled) + return; + + ToggleActive(uid, true, component); + args.Handled = true; + } + + #endregion + + #region Conditionals + + private bool CanFly(EntityUid uid, FlightComponent component) + { + if (TryComp(uid, out var cuffableComp) && !cuffableComp.CanStillInteract) + { + _popupSystem.PopupEntity(Loc.GetString("no-flight-while-restrained"), uid, uid, PopupType.Medium); + return false; + } + + if (HasComp(uid)) + { + _popupSystem.PopupEntity(Loc.GetString("no-flight-while-zombified"), uid, uid, PopupType.Medium); + return false; + } + return true; + } + + private void OnMobStateChangedEvent(EntityUid uid, FlightComponent component, MobStateChangedEvent args) + { + if (!component.On + || args.NewMobState is MobState.Critical or MobState.Dead) + return; + + ToggleActive(args.Target, false, component); + } + + private void OnZombified(EntityUid uid, FlightComponent component, ref EntityZombifiedEvent args) + { + if (!component.On) + return; + + ToggleActive(args.Target, false, component); + if (!TryComp(uid, out var stamina)) + return; + Dirty(uid, stamina); + } + + private void OnKnockedDown(EntityUid uid, FlightComponent component, ref KnockedDownEvent args) + { + if (!component.On) + return; + + ToggleActive(uid, false, component); + } + + private void OnStunned(EntityUid uid, FlightComponent component, ref StunnedEvent args) + { + if (!component.On) + return; + + ToggleActive(uid, false, component); + } + + private void OnSleep(EntityUid uid, FlightComponent component, ref SleepStateChangedEvent args) + { + if (!component.On + || !args.FellAsleep) + return; + + ToggleActive(uid, false, component); + if (!TryComp(uid, out var stamina)) + return; + + Dirty(uid, stamina); + } + #endregion +} \ No newline at end of file diff --git a/Content.Shared/Cuffs/SharedCuffableSystem.cs b/Content.Shared/Cuffs/SharedCuffableSystem.cs index ebbafef7f0e..9777b239884 100644 --- a/Content.Shared/Cuffs/SharedCuffableSystem.cs +++ b/Content.Shared/Cuffs/SharedCuffableSystem.cs @@ -7,6 +7,7 @@ using Content.Shared.Contests; using Content.Shared.Cuffs.Components; using Content.Shared.Database; +using Content.Shared.Flight; using Content.Shared.DoAfter; using Content.Shared.Hands; using Content.Shared.Hands.Components; @@ -479,6 +480,13 @@ public bool TryCuffing(EntityUid user, EntityUid target, EntityUid handcuff, Han return true; } + if (TryComp(target, out var flight) && flight.On) + { + _popup.PopupClient(Loc.GetString("handcuff-component-target-flying-error", + ("targetName", Identity.Name(target, EntityManager, user))), user, user); + return true; + } + var cuffTime = handcuffComponent.CuffTime; if (HasComp(target)) @@ -731,4 +739,4 @@ private sealed partial class AddCuffDoAfterEvent : SimpleDoAfterEvent { } } -} +} \ No newline at end of file diff --git a/Content.Shared/Damage/Components/StaminaComponent.cs b/Content.Shared/Damage/Components/StaminaComponent.cs index 65c025c3adf..b78fe978090 100644 --- a/Content.Shared/Damage/Components/StaminaComponent.cs +++ b/Content.Shared/Damage/Components/StaminaComponent.cs @@ -39,6 +39,13 @@ public sealed partial class StaminaComponent : Component [ViewVariables(VVAccess.ReadWrite), DataField, AutoNetworkedField] public float CritThreshold = 100f; + /// + /// A dictionary of active stamina drains, with the key being the source of the drain, + /// DrainRate how much it changes per tick, and ModifiesSpeed if it should slow down the user. + /// + [DataField, AutoNetworkedField] + public Dictionary ActiveDrains = new(); + /// /// How long will this mob be stunned for? /// @@ -63,4 +70,4 @@ public sealed partial class StaminaComponent : Component /// [DataField, AutoNetworkedField] public float SlowdownMultiplier = 0.75f; -} +} \ No newline at end of file diff --git a/Content.Shared/Damage/Systems/StaminaSystem.cs b/Content.Shared/Damage/Systems/StaminaSystem.cs index f8a0f7c62ba..e4840a6630b 100644 --- a/Content.Shared/Damage/Systems/StaminaSystem.cs +++ b/Content.Shared/Damage/Systems/StaminaSystem.cs @@ -258,7 +258,7 @@ public bool TryTakeStamina(EntityUid uid, float value, StaminaComponent? compone } public void TakeStaminaDamage(EntityUid uid, float value, StaminaComponent? component = null, - EntityUid? source = null, EntityUid? with = null, bool visual = true, SoundSpecifier? sound = null) + EntityUid? source = null, EntityUid? with = null, bool visual = true, SoundSpecifier? sound = null, bool? allowsSlowdown = true) { if (!Resolve(uid, ref component, false) || value == 0) @@ -284,8 +284,8 @@ public void TakeStaminaDamage(EntityUid uid, float value, StaminaComponent? comp if (component.NextUpdate < nextUpdate) component.NextUpdate = nextUpdate; } - - _movementSpeed.RefreshMovementSpeedModifiers(uid); + if (allowsSlowdown == true) + _movementSpeed.RefreshMovementSpeedModifiers(uid); SetStaminaAlert(uid, component); if (!component.Critical) @@ -328,27 +328,51 @@ public void TakeStaminaDamage(EntityUid uid, float value, StaminaComponent? comp } } + public void ToggleStaminaDrain(EntityUid target, float drainRate, bool enabled, bool modifiesSpeed, EntityUid? source = null) + { + if (!TryComp(target, out var stamina)) + return; + + // If theres no source, we assume its the target that caused the drain. + var actualSource = source ?? target; + + if (enabled) + { + stamina.ActiveDrains[actualSource] = (drainRate, modifiesSpeed); + EnsureComp(target); + } + else + stamina.ActiveDrains.Remove(actualSource); + + Dirty(target, stamina); + } + public override void Update(float frameTime) { base.Update(frameTime); - if (!_timing.IsFirstTimePredicted) return; var stamQuery = GetEntityQuery(); var query = EntityQueryEnumerator(); var curTime = _timing.CurTime; - while (query.MoveNext(out var uid, out _)) { // Just in case we have active but not stamina we'll check and account for it. if (!stamQuery.TryGetComponent(uid, out var comp) || - comp.StaminaDamage <= 0f && !comp.Critical) + comp.StaminaDamage <= 0f && !comp.Critical && comp.ActiveDrains.Count == 0) { RemComp(uid); continue; } - + if (comp.ActiveDrains.Count > 0) + foreach (var (source, (drainRate, modifiesSpeed)) in comp.ActiveDrains) + TakeStaminaDamage(uid, + drainRate * frameTime, + comp, + source: source, + visual: false, + allowsSlowdown: modifiesSpeed); // Shouldn't need to consider paused time as we're only iterating non-paused stamina components. var nextUpdate = comp.NextUpdate; @@ -363,8 +387,11 @@ public override void Update(float frameTime) } comp.NextUpdate += TimeSpan.FromSeconds(1f); - TakeStaminaDamage(uid, -comp.Decay, comp); - Dirty(comp); + // If theres no active drains, recover stamina. + if (comp.ActiveDrains.Count == 0) + TakeStaminaDamage(uid, -comp.Decay, comp); + + Dirty(uid, comp); } } @@ -380,7 +407,6 @@ private void EnterStamCrit(EntityUid uid, StaminaComponent? component = null) component.StaminaDamage = component.CritThreshold; _stunSystem.TryParalyze(uid, component.StunTime, true); - // Give them buffer before being able to be re-stunned component.NextUpdate = _timing.CurTime + component.StunTime + StamCritBufferTime; EnsureComp(uid); @@ -407,4 +433,4 @@ private void ExitStamCrit(EntityUid uid, StaminaComponent? component = null) /// Raised before stamina damage is dealt to allow other systems to cancel it. /// [ByRefEvent] -public record struct BeforeStaminaDamageEvent(float Value, bool Cancelled = false); +public record struct BeforeStaminaDamageEvent(float Value, bool Cancelled = false); \ No newline at end of file diff --git a/Content.Shared/Flight/Events.cs b/Content.Shared/Flight/Events.cs new file mode 100644 index 00000000000..6666971b539 --- /dev/null +++ b/Content.Shared/Flight/Events.cs @@ -0,0 +1,24 @@ +using Robust.Shared.Serialization; +using Content.Shared.DoAfter; + +namespace Content.Shared.Flight.Events; + +[Serializable, NetSerializable] +public sealed partial class DashDoAfterEvent : SimpleDoAfterEvent { } + +[Serializable, NetSerializable] +public sealed partial class FlightDoAfterEvent : SimpleDoAfterEvent { } + +[Serializable, NetSerializable] +public sealed class FlightEvent : EntityEventArgs +{ + public NetEntity Uid { get; } + public bool IsFlying { get; } + public bool IsAnimated { get; } + public FlightEvent(NetEntity uid, bool isFlying, bool isAnimated) + { + Uid = uid; + IsFlying = isFlying; + IsAnimated = isAnimated; + } +} diff --git a/Content.Shared/Flight/FlightComponent.cs b/Content.Shared/Flight/FlightComponent.cs new file mode 100644 index 00000000000..d250744544d --- /dev/null +++ b/Content.Shared/Flight/FlightComponent.cs @@ -0,0 +1,101 @@ +using Robust.Shared.Audio; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; + +namespace Content.Shared.Flight; + +/// +/// Adds an action that allows the user to become temporarily +/// weightless at the cost of stamina and hand usage. +/// +[RegisterComponent, NetworkedComponent(), AutoGenerateComponentState] +public sealed partial class FlightComponent : Component +{ + [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))] + public string? ToggleAction = "ActionToggleFlight"; + + [DataField, AutoNetworkedField] + public EntityUid? ToggleActionEntity; + + /// + /// Is the user flying right now? + /// + [DataField, AutoNetworkedField] + public bool On; + + /// + /// Stamina drain per second when flying + /// + [DataField, AutoNetworkedField] + public float StaminaDrainRate = 6.0f; + + /// + /// DoAfter delay until the user becomes weightless. + /// + [DataField, AutoNetworkedField] + public float ActivationDelay = 1.0f; + + /// + /// Speed modifier while in flight + /// + [DataField, AutoNetworkedField] + public float SpeedModifier = 2.0f; + + /// + /// Path to a sound specifier or collection for the noises made during flight + /// + [DataField] + public SoundSpecifier FlapSound = new SoundCollectionSpecifier("WingFlaps"); + + /// + /// Is the flight animated? + /// + [DataField] + public bool IsAnimated = true; + + /// + /// Does the animation animate a layer?. + /// + [DataField] + public bool IsLayerAnimated; + + /// + /// Which RSI layer path does this animate? + /// + [DataField] + public string? Layer; + + /// + /// Whats the speed of the shader? + /// + [DataField] + public float ShaderSpeed = 6.0f; + + /// + /// How much are the values in the shader's calculations multiplied by? + /// + [DataField] + public float ShaderMultiplier = 0.01f; + + /// + /// What is the offset on the shader? + /// + [DataField] + public float ShaderOffset = 0.25f; + + /// + /// What animation does the flight use? + /// + + [DataField] + public string AnimationKey = "default"; + + /// + /// Time between sounds being played + /// + [DataField] + public float FlapInterval = 1.0f; + + public float TimeUntilFlap; +} diff --git a/Content.Shared/Flight/SharedFlightSystem.cs b/Content.Shared/Flight/SharedFlightSystem.cs new file mode 100644 index 00000000000..281c6d70f08 --- /dev/null +++ b/Content.Shared/Flight/SharedFlightSystem.cs @@ -0,0 +1,104 @@ +using Content.Shared.Actions; +using Content.Shared.Movement.Systems; +using Content.Shared.Damage.Systems; +using Content.Shared.Hands.Components; +using Content.Shared.Hands.EntitySystems; +using Content.Shared.Interaction.Components; +using Content.Shared.Inventory.VirtualItem; +using Content.Shared.Flight.Events; + +namespace Content.Shared.Flight; +public abstract class SharedFlightSystem : EntitySystem +{ + [Dependency] private readonly SharedActionsSystem _actionsSystem = default!; + [Dependency] private readonly SharedVirtualItemSystem _virtualItem = default!; + [Dependency] private readonly StaminaSystem _staminaSystem = default!; + [Dependency] private readonly SharedHandsSystem _hands = default!; + [Dependency] private readonly MovementSpeedModifierSystem _movementSpeed = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnStartup); + SubscribeLocalEvent(OnShutdown); + SubscribeLocalEvent(OnRefreshMoveSpeed); + } + + #region Core Functions + private void OnStartup(EntityUid uid, FlightComponent component, ComponentStartup args) + { + _actionsSystem.AddAction(uid, ref component.ToggleActionEntity, component.ToggleAction); + } + + private void OnShutdown(EntityUid uid, FlightComponent component, ComponentShutdown args) + { + _actionsSystem.RemoveAction(uid, component.ToggleActionEntity); + } + + public void ToggleActive(EntityUid uid, bool active, FlightComponent component) + { + component.On = active; + component.TimeUntilFlap = 0f; + _actionsSystem.SetToggled(component.ToggleActionEntity, component.On); + RaiseNetworkEvent(new FlightEvent(GetNetEntity(uid), component.On, component.IsAnimated)); + _staminaSystem.ToggleStaminaDrain(uid, component.StaminaDrainRate, active, false); + _movementSpeed.RefreshMovementSpeedModifiers(uid); + UpdateHands(uid, active); + Dirty(uid, component); + } + + private void UpdateHands(EntityUid uid, bool flying) + { + if (!TryComp(uid, out var handsComponent)) + return; + + if (flying) + BlockHands(uid, handsComponent); + else + FreeHands(uid); + } + + private void BlockHands(EntityUid uid, HandsComponent handsComponent) + { + var freeHands = 0; + foreach (var hand in _hands.EnumerateHands(uid, handsComponent)) + { + if (hand.HeldEntity == null) + { + freeHands++; + continue; + } + + // Is this entity removable? (they might have handcuffs on) + if (HasComp(hand.HeldEntity) && hand.HeldEntity != uid) + continue; + + _hands.DoDrop(uid, hand, true, handsComponent); + freeHands++; + if (freeHands == 2) + break; + } + if (_virtualItem.TrySpawnVirtualItemInHand(uid, uid, out var virtItem1)) + EnsureComp(virtItem1.Value); + + if (_virtualItem.TrySpawnVirtualItemInHand(uid, uid, out var virtItem2)) + EnsureComp(virtItem2.Value); + } + + private void FreeHands(EntityUid uid) + { + _virtualItem.DeleteInHandsMatching(uid, uid); + } + + private void OnRefreshMoveSpeed(EntityUid uid, FlightComponent component, RefreshMovementSpeedModifiersEvent args) + { + if (!component.On) + return; + + args.ModifySpeed(component.SpeedModifier, component.SpeedModifier); + } + + #endregion +} +public sealed partial class ToggleFlightEvent : InstantActionEvent { } diff --git a/Content.Shared/Gravity/SharedFloatingVisualizerSystem.cs b/Content.Shared/Gravity/SharedFloatingVisualizerSystem.cs index 57136116caa..8fe9e00e7eb 100644 --- a/Content.Shared/Gravity/SharedFloatingVisualizerSystem.cs +++ b/Content.Shared/Gravity/SharedFloatingVisualizerSystem.cs @@ -1,5 +1,6 @@ using System.Numerics; using Robust.Shared.Map; +using Content.Shared.Flight.Events; namespace Content.Shared.Gravity; @@ -17,6 +18,7 @@ public override void Initialize() SubscribeLocalEvent(OnComponentStartup); SubscribeLocalEvent(OnGravityChanged); SubscribeLocalEvent(OnEntParentChanged); + SubscribeNetworkEvent(OnFlight); } /// @@ -62,10 +64,24 @@ private void OnGravityChanged(ref GravityChangedEvent args) } } + private void OnFlight(FlightEvent args) + { + var uid = GetEntity(args.Uid); + if (!TryComp(uid, out var floating)) + return; + floating.CanFloat = args.IsFlying; + + if (!args.IsFlying + || !args.IsAnimated) + return; + + FloatAnimation(uid, floating.Offset, floating.AnimationKey, floating.AnimationTime); + } + private void OnEntParentChanged(EntityUid uid, FloatingVisualsComponent component, ref EntParentChangedMessage args) { var transform = args.Transform; if (CanFloat(uid, component, transform)) FloatAnimation(uid, component.Offset, component.AnimationKey, component.AnimationTime); } -} +} \ No newline at end of file diff --git a/Content.Shared/Gravity/SharedGravitySystem.cs b/Content.Shared/Gravity/SharedGravitySystem.cs index 100d2ee74fb..55187bf14ac 100644 --- a/Content.Shared/Gravity/SharedGravitySystem.cs +++ b/Content.Shared/Gravity/SharedGravitySystem.cs @@ -8,6 +8,7 @@ using Robust.Shared.Physics.Components; using Robust.Shared.Serialization; using Robust.Shared.Timing; +using Content.Shared.Flight; namespace Content.Shared.Gravity { @@ -24,6 +25,9 @@ public bool IsWeightless(EntityUid uid, PhysicsComponent? body = null, Transform if ((body?.BodyType & (BodyType.Static | BodyType.Kinematic)) != 0) return false; + if (TryComp(uid, out var flying) && flying.On) + return true; + if (TryComp(uid, out var ignoreGravityComponent)) return ignoreGravityComponent.Weightless; @@ -142,4 +146,4 @@ public GravityComponentState(bool enabled) } } } -} +} \ No newline at end of file diff --git a/Resources/Audio/Effects/Flight/wingflap1.ogg b/Resources/Audio/Effects/Flight/wingflap1.ogg new file mode 100644 index 0000000000000000000000000000000000000000..724ac3ecd204decb15d6e33758fbaef885afd800 GIT binary patch literal 10302 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 literal 0 HcmV?d00001 diff --git a/Resources/Audio/Effects/Flight/wingflap2.ogg b/Resources/Audio/Effects/Flight/wingflap2.ogg new file mode 100644 index 0000000000000000000000000000000000000000..b0264a13170816a51da02b40af98ee349b17be61 GIT binary patch literal 13035 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 literal 0 HcmV?d00001 diff --git a/Resources/Audio/Effects/Flight/wingflap3.ogg b/Resources/Audio/Effects/Flight/wingflap3.ogg new file mode 100644 index 0000000000000000000000000000000000000000..aeb0e21acf6818fd352ccf6a343a4fe5d08963fc GIT binary patch literal 10881 zcmaiZ2RNKh*Y~|duhEIHgs@8Vuv!p;)mJAZy48{>(OFTV_ufME)q59-=)HH6NDwUv zLPGG}$#kHNht~CLy^m0{9jK$_FqQAAl(4<)b@^svklY% zWv+L{9;yr#2MAOJqt zBcKZHMvukn0suJxJZ6KDCRi)O3KDZUJyR2vE?eFFVX28>-EseH^4J}&iT-n|+2?Mv&wUM3gN>Vf4V!|E=Yma+gU#qd{wcqF zSC22t)z+a9Amuqe99_a~|jAF7i5%Ltcd2LKrX#_nf~{h&6Gypzr%`YuwlE^4!& zDlRqhKR*FicmX%Ul4YH2hsGC*HM>#^8e~hCi+`W^uOx6lXzpid`dIqGFq|~~OHK(@ z#^l>DoeYlVlGKbV3p$L@uYtCN4WxDFq>ZJ!j3^VQ7yNB`Qe@zjmrQ_t^c$I^J@jVa zIMhTsUdg-xIM;A2eZ9Du3T%ZXW^mLu>-^>S4=hSdGe_RSKY}yD7BcLc2~5D&Z_CWR zVS>xP{|+A+&~9dPbUj%wpuW+#=2R?&ED21mF{BAO0)4Q2rl{x#h>}1}Bt1ug>x-+M zLjfR&{0hbYs;;2?7sdJU;amfpwL^k~JeO5j*NFIbRTuevVgV4vJR%^9qw3eQT`J&8 z_67AbM%)F7NCd3luSbDKg<8hS;?iF^Nl2>m0IUe~ivKj+cec^%H^={@jQ(KMqF@Kh zxFD~#kd&^DuKsgJ!xXp0#$b~=v?2c;SpVfY0O&M9S3a3&9x2e98K^Et z@Ylfq@|y>X2Facn9zY-$rc!~4P-2g2i&3R=Qy+SDfF)NYeBR@!$>CPl0!b*-kK zTg^9E88`YGHvHo-f8Azv*6lw$=h8)3;n8bK@z?(CIXRrspTweXDaW%J#^3f#wh2ow zNz49P@|y6!d5&#(QEGTmX!u5GG-p^cDl8q-{MuolV!P#kE&t^?iq33c0X;{-ne9J3 zr&ECOKIly~JZcAj_b4?ED%4eh_CE~(0G+YKDpzqt3&}f;6q!cy>gq`S&mIF(r-juf zg+aw80RTM!+yadpL>}%OE9WF-fG@DlhLa1&&3#W7R75UB3@2fAjg{q)YKn(<5g&X& za#G8y21c=qMng&76+Tq{B8Cr!y=#CL0H6i>1m4p*k0|%&$d3pRWF_{&ISLXclwrKd zbV4xpLJEtu%)ANZ{`^EbmGG>53X8CEa1wwA{)lqNVyPiAQ~+R3j3k5wCn85+>#@iI zfiH|m1iUCydjyVuGiX2nH!F}FR=}t|0_$ZA8j}5z88{}(2>{Se2>6rZ6t+|V$T@%y zPuWHjgjJ~6RVWoksjOxx)n=1~hlNG7baV$Pb*FW77irXV8`#w-)zl7kbSV{dsdXol z)pY9}M}@8Cb##|$bf+7v7HRzSligN@)C|Gb8IbBWo9z2&#?d78A69BqSFG;p&Nhf> zx$0`^S{dr-8a}r&Jl55p@l^q-+G_LEy1EUz^FJMRlYKwB>YC_V%^&L;()bzA27f&A z{YZDod3M?jq?*lGnKinta-z~vcCU*u>6{o$38ujjQ&DdGdK2>+Gf-oLsVEz$sVuE< z*aoQ;#h4l%42Gw)rtB2Mv)xdPX(=D5smiD*-ER0s)Wx&i@)T21QChQeNo^_L?s>M| z!tYZE8!Rl{YAI=HaoTQqHq_wAn1|Ww;2L~aHBj?yyM?dLjYUh>Z8_0oO&{Ec<0JQM zY-(RWNRYQNa? z>{NX1$Stpun4mWX#0KON8kBRpB1cn3WtTEk zz%PVLO!H4YFzTSO0U0!bRnBDyxpZ6EP~?a*irje!&P5nsa4Iv~kBmm?=FYbYYaR-;afG%*1E}zPR zoV9=sv`Yb%1*ls(7g9McdW#WRPFRp8f(sT*j!pcqAwe z+){}%F{rz4y-Px((GC=ZhJ(m+au8NMM)wir-3Im*ECn;>Wg@d6i0jwW?GYfsez+L` zmT>TZD(o8ycRA|^bDToD%hUt>$X@0jkstwl z?Ewk!4aUnV(QB$A80Ao((MOc^0~LEd=m1v5xHP>MKyLEY4)s5x0BSiy`@ z1TW}v2=LWBQX~Wk4(XmVFB8b0K!W~BXHf<^kdZyObbDS{{fv2JJ*a;$Ld=*)h|Cxf z0Uw;60%)bGXCX=XweQA1T-llhOt$5&w0(InK!Uc&xkL+#JOV`3B}Q0+Kr{U93yS@( zK63Gmzt92!wefdcqNdyxwD5DmKG;nVWSmzmcCbb8H-WnXV-N+H$|ZuF|Mn$PfHJPq zyF}zAB0<$$60V?n)goZIRM*vDmcMf^b3qWI*q2fG)+Srt0IP%W$9LG8wtWZyO$-3f z=Ipj%GkwtR%%EhIpjc;e^E=m(;c&%jD<%4Fy6HrL0xE5!{br_lq?SgO1wx>!EKv`^ z8TVqsxvc*A5O}nN;sgBTEJ6V?5GuMpP*Bh{7?4xdIVSM%+O^LVf`r6`y+JM$C~SZ{ zsE;@<)PR_R5)<&+TpTcX__z-bO&k?f7eTZs*okAZ@@0jO|M#eJ?W+Cvq%7m~mwb6rz7*r~Vffxn=A@*AsPNF}sFalC zl$6BGgqMk-;ZXsRQPEjBsSY) z#~0Lzq5|F-835o|raa1J1lIs%b|+m&-@ahOomi~L&2vLMxHV+#{E^;I;%N5fI`y(a z3xDr&pZ$Dcu62=hUijA+Zm@GNQ<2qMx181%k|afzr;nmYVM#p$KDYclmM^Mfn&Y|0 z2}lwx$5@A|DmTs;G=6Boq;`K#lIj;d61ol)jt6+$8_S7ul(xYF&TTRqz0Gz{Ca7*6 zJ~>aXA~(;Y;x9o+#em_UY3Kqg+IvyFI1cspsOE{6_`w?%5hLy1?w1=)t1q;6!b8k< z7#feVOiNZM+xJ51WR;k1hlpdFRGbJ`dXy*#FvHIjJI0@X+H~59VRNhzxwS_}lG2q3ePgU6KZx;RYhZyH(JIvMI=EDg&bp|ZG^ zYH-9mcLY0DT=(fcV|`Q@S582|*pC7hwa3m;#2SK3qxNfE;&JR*Wg{liGyX8qZHj`v zf<@Owr42J@$1%Drkt#8)wu%O7BOG8yyu)_M^vG`5OMCw92k%oFz>dkbG(J@mjQkCL zLNkThemS?I`53BEk{3^tcO7l!K1r=dzm6%jb?hKW_=b?$c9HNum+M8p4^CpDtsHhQ z5A|IXXPh1_F-MlaKa)o6CapkQgXr(Zek_`Fm%FK#KJbA%9b^#)@(q5v{4D{uXUI5!#Ay4nApBC-W&*4Qs z^FmL%v-Mx*iZ^`=KC|R`!7Ia^?aCGOhi+Kv4erEdu5ZvzipPC9Lo^WedveXPo^oqX z*T|~nRvUs2h28XJiWKsci&b18Vyde71|i4n59GW z@H?hEBVViR`#y*x>(p}(CZSu1YgCO<{<5V>dNKbXrHFUSGpbnt7EY`r*h--Cbt2h8 z9{$Xk=IazYv8dFR%AdOf%v~|)Q5>M-SDV9eEc*vC*LwgEh!*(i11khQzk&uJqFt9> zJ;_>q)uGhBaHO43`c-;|6_bxp32iyAC#&h@(3Q9#)-84EYi5~OUbCGYw;^D?N4|YqoaP`SMsYfBi{Xi$ZXnn*j z1Bzojuijoxy=K*_oz+*B42nI_K^e0U{8bAhbRl-iaJiACW#p=oTLzKG8 z-pj4wNLMw?$HWOHdl8$RXY4Ad=${U#@hvQMD`-pgpq%o1ZH#OlYiC10C)vZS>yw3_%bP@Wy-FJMUFSu>du zdMqB!jrM9kt`l3G|9l_tN~q3Cd<7dN`*HapV(vn12uDF}Ms$C)ZkdVC1u@)5;?3LS zC3(*okZ*4NIiqis7-XdH6>hr95w2(HtcrRxqc`j`xU;|ku+0HXO%A&mmdfnBF_rWX4d{KV zsLK-_< zzLPhPupZqob5=`RGg*gyQeF(gc`=xWZ%KzbFMErJczZ3ozil}MN+yhh_uFJcpnBNy#lK?!{xzMMyh1*#;S)Um$UA0H*V84`-Sl+TJ36BT79Eq&>Vz$6fAI z2i~K4$od{6cq!;N=m&17S)G24s3@JGc;1aXbuTo82r>L9EI{{l_Eq$Mw=e!aJkA*! zK6xiSLDXdz?}2-fIIkMcVF*R0;XTvR(oXbW3%LYU&Pu&9Ji;jB2l{1)l>j_ z!N+S^Ezr7NTkTD^)xnw1P#-&U$K-~opns{#uKk}Mo9ZerS(>Q6E&XCZF_ZKN@Q6M9 z=+yT4#zKSjA9A|S+!PXCiqd%)6~45?mYZs%)2vyXnouQzR2Fxvi>KHGjj`9!j{IjE zV@{sJTbMSHoHU|5)AD6?=zyQ>9cy8q9afMs6O$ zojs(b`;K1qf2(iI5N38;>8p@jP)?tgs_$PW$=-iIZJ^>MKXo5*`~^;r4dbr(@+LW+ ztUHUpwzV)pFec@YHxJ)Erb@@%BA=PejeU83nwo;XR5FeBZRDRPa(<7T%cdeix(WG9FsbC~5MYjXQ{`=NC74nu;xg2nYSoSQQZ#(!)>Bgy zAOcgg-k}RA3FxQ#4)_&IMmGnzagw&(OI4<6C+IYoPLmfW1V6WWeu7iwj?ag)VzP|t znEZA;O&>G*RR4Br9O8Cv%|zYcjSefo?dYp6V=#`MxAv`lGaY%*6?O=}yScV5*rl-* zjy)1r^;y(|yZhX_RE46KFp8hD{?++jau!RM3^j+oK`z34a_EPem<&;IskO>yufL*K)Bk2pdF;Ubsu5h2 zj~bR~*HpkWzq^Jv(%z)`JzonPE^%u0sLpR2F$ zHWKVpcd2>hmzDOWnWdRkixC@<@#gr`as$|WI6}<~a-t7Ey?gGPmjVFrx_rj)yARRs zfsb>68-Sm?CR!NsM^xuc+!zU+ z<(%BLgsjCyMuW7=MvrIsp)n|+>pYn!hB|2{M_uUaMGr9Lw+&Nh~bxpr@85A<^>YWvMS(^t-Ks&b``|brY+lEHu?PP_Y^z9 z7WXH(1jGhaxItY3`$}MbCS7@_zAgX}h2N0*B*X9bv<_e5dmN!Ly=>IUEBb1UQ{8W@ zrPAg%S*y+rRgFKlW5sJtq?y*+v%c)(vHYFgp^P7^Vm4ea+X>e!!ui=t@KnT0v8FGZ z$tP=Tf~`ZdKatNdeui+Mn7KZkvjgV?CW}M-{08XSIcN9+RSisl)SJK?#2oy zVgYE8F4C7bjHiLElVK~xQT>$B#`MG&;yxkGr3x~zNRIVsQyr% zSly(@c`WL9NEsz@HunU`!_p;iN$Bfx*BXfFb&0>#=Gb0t=?v+VoD7R?Jly{M>YtG8`IeC!H|LG5u&R&pDw8x#XGZ%nb@A`hjz?>;j+QKhixF;rY z`kJ0b&fD{gsW-I$+UMX(LR?~uhvIlqVCGzWhp&}TVU{x& zJ~7}{=k>Wh!>9!3F?Qm+PI` z#GT&v-80~67p&uF_0k@`)El2>qSI^R@|DEQ?`=s1A|G&DqI=3tCyVW=w=z)?X<7P@ z-kZxGIvWAM2YcbJBt@} zgP1HWa5Vd#s2sI~wLl~7)_~Aty^?!jaEF?A?;|$PU4y5SyU;AAlyqTz!iP9<2APbG zKT$J1x~ZD-teBXQlq#aeeVuaeeZy=?ih|GU%_NRQx@1izPePe{k05!=qqc|R#6dgS~(;Ni^0 zdRx8rr(QZr%G@-zyyRWm`8>r}!uF!f8*JLg^TgTZX@N#x;!L?5D9lx?D!!?6%jkaG z#<{J!Bmzk{H?|!Evc-K1Z<#URU&wX@j9F7ZE+dq;rw7o@FKaHO582LoQs8N#j~peM zJa4(ENb)DlWM0(w%ara$_LwB7%-xHk9HX24n2-KR2 zd}*&y4Pgk>r_IgCzhHFhR-e1;c{WA8oyt?u=RhHMog#AJ^DPl)P2`vUl&(8#)Vr#z)+ z1lOXi_+4vOyON&-3=jC4I~l^1Ozu@evY$q+aX@87#f3d1*kE6hZ1|(nMretd2$GST39NZ&Hl2z%6%D!RCVct8I&RBM|@#yxpIIT;W zh~4rVp*b1`hV+I0RG#`i&HUMBCTihH-cR*a$>k8KQCOt%*)rqg!#GKDW4RTr$>E8d zj5vx}T~-SkO)mEzgWD`>WR=PlY-&~2JGj9@TFjpuNlH&kH4nlkd>Cey%vb`hLydEE zyqZ+V0n_bvjaevV{t3>mFRyi#{3I|*$l(lXPsVI}TDCev;I?(YBW&vx%1-l>|Muj8 z?BF5Ht}4{NikO(wj;FO@7icLO^RngkO^rCFjoevlYij>0Q(&(rh!fMv#~l$Nhf-7+ zK+O$!uOV+`@2r= z0O#UTr|g?@5%Mp-90jFX^)+C)1hs+A1ip2)-J*AIWoNYy&cqHKG(%slzTB@q`NdXu zHmUpK`($+lk1z32shIuCn%CQD%fBUp!gsfNBVAcvo?~q~t=;#WrRo_MG!tOi5BwTv z(k@>iG`Xo1KCMR^_yOy2z$!+W+WE0S&C8&4IS+fhm-(dntYIp(hd3*yf$LLLP8gqp zr~0wLN`~*v7;=JNke?xrKW(10Vv(03j==vDJm4N9DJO==vdYz!c3;0_pXVGl`AI)M z$%rkQkI&IcMu_llIM*jYUKQD%*fQzBL*vOMx`&$kd)6r5DzJ_$gx~+5LEX_rLU#Ts z)#3In98Rf;=*p{PWxf&+QqZ;6~(5V zKL|zH{xpzgi?<&MDcm<^*>rl#V}OD5G+-|8KzwS>2KM?tsHN4wN3z|_zEzwBlhLpJ z>5j@*y`4dt|5388efpVdLx!<4gYiPF;E$4{pJ!D;Iu4%AW)u~{W>4*F8UM(O9Y(MNnNtn6c_K`nWSbUk(+ZGlb{qmd7CmS4Ux{J=P zIhbUd%#ftYi1)Q?Bl|@tbtzhSGx0v&ms35iQky@ky?2)wi%1kgy$}YE??XB~r51QS zfR3Dwu!t&1Jv{-Pc8NSGn7V$~AS{|C$s9|5^HXj>ectNHEXwO~q3SC-f+w;rdr#m9 zQ6|sWd^j4frq5{1fAvlY`wqEb=M8B>3e}T{qm9F&uVKnMKE8WL zC#qqkxb0F|#Na`X{pMa#!0z^BLnMEelf<^-4@*W$!U&h;_M%MUR zP9?^&3%sDY!_$-5c$+@TT5Tz|dGgUqqJdSil87|1&-KM@&CB(~l4?aO20uk<{ECY< zN6H2lRU`N2=L;g#&)0PptEu_BjvEW}IeUx;x$=i(0fN;8m3#)NMujO~#Dl{!-f17c zWi=cEi|7@|%Lx%*44t0t#P`(d&7Q|tOh&$kDstC85G`bIGGv&cq|q3}pappNT2r`B zDnX^&CwaO9Gj@E_f5AFjv+;)sF+>jw$jumMEfs&#GSNvrIYIKK0#lLhSbnP(e_}k? z-dokB!j8tCedJW;-q0!|cX7!5dy=zMbbi$L51!QqJeFaI3mGj|VA;L(C||Dq z=h2BFn!|%CtmVT%2p+JK=DXMUn2JsG`v}c-8Tqo2C<&8LG-JO{{?%kFQlj%6P@zP!bX3wja#?4uJVb9&`bPq2m@dW z_f?&JO!EEOk*5tVQZ9u1=OFn1fF#|8JY^DF3?J6s)r1?Fkt>Gv1F%+`40dTUkw5U_wzjI*mD@DoZ#xKD0F^O?%}R z_ss2{s!htMh2*Qip~H77qLv$~EeXo{9^+XqBXlxY=Z%@YwP+;t0^8&b8KC*)%Vex8 z_`mB&GLSR#Y*)$t;WRI^p3rGy#+kPNsgLK)R>@WcWh-ry@H$b`-kj5Ln3USabW%P0_+}aUVOqS*2Y)=k^p|9>N)QQ~kL(%f+y)S?F}+am#G_3HG5G#+lgn6Risjr?l1{`;XE2f@V=Ay zfm)aB@VTvpX-vAKJFnse?yGD?@jOb7L#SW+(nWlMzmc)>lwsQQZ3W|7(I}_?~l|xBper$=@~g(d_5@{x*FIow=Z5C?QTReg2DPu$e`HMLQ&d#LH=aFs(px9YRvXcT7R==)5yeiktCS$wKgau&4y;0yLGPHc{Z1K(j!ws zEomuW{IhqhmH*M#8e40ZA%|SPGwC4F6`v^LKkxg9J{$6FeU*-Z*L(2IhlZDJezUCH zPj|dGJ@{}*?;ej)Re5wg^MS4FK`KYy4e#to;Uh_Uluz~_d0y$5SH<1lj9MzIYu_DF sOb3?v)f@dv5oc`ls2`pnePu1w=!EIz6BP^QekF_1gz9xhfRyzA0Kw>Dwg3PC literal 0 HcmV?d00001 diff --git a/Resources/Locale/en-US/cuffs/components/handcuff-component.ftl b/Resources/Locale/en-US/cuffs/components/handcuff-component.ftl index 16447f42515..1f8a895164c 100644 --- a/Resources/Locale/en-US/cuffs/components/handcuff-component.ftl +++ b/Resources/Locale/en-US/cuffs/components/handcuff-component.ftl @@ -3,6 +3,7 @@ handcuff-component-cuffs-broken-error = The restraints are broken! handcuff-component-target-has-no-hands-error = {$targetName} has no hands! handcuff-component-target-has-no-free-hands-error = {$targetName} has no free hands! handcuff-component-too-far-away-error = You are too far away to use the restraints! +handcuff-component-target-flying-error = You cannot reach {$targetName}'s hands! handcuff-component-start-cuffing-observer = {$user} starts restraining {$target}! handcuff-component-start-cuffing-target-message = You start restraining {$targetName}. handcuff-component-start-cuffing-by-other-message = {$otherName} starts restraining you! diff --git a/Resources/Locale/en-US/flight/flight_system.ftl b/Resources/Locale/en-US/flight/flight_system.ftl new file mode 100644 index 00000000000..12693cc8467 --- /dev/null +++ b/Resources/Locale/en-US/flight/flight_system.ftl @@ -0,0 +1,2 @@ +no-flight-while-restrained = You can't fly right now. +no-flight-while-zombified = You can't use your wings right now. \ No newline at end of file diff --git a/Resources/Prototypes/Entities/Mobs/Species/harpy.yml b/Resources/Prototypes/Entities/Mobs/Species/harpy.yml index 788b21eafb0..05ac3de8bb3 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/harpy.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/harpy.yml @@ -5,6 +5,10 @@ id: MobHarpyBase abstract: true components: + - type: Flight + isLayerAnimated: true + layer: "/Textures/Mobs/Customization/Harpy/harpy_wings.rsi" + animationKey: "Flap" - type: Singer proto: HarpySinger - type: Sprite @@ -197,3 +201,15 @@ icon: DeltaV/Interface/Actions/harpy_syrinx.png itemIconStyle: BigAction event: !type:VoiceMaskSetNameEvent + +- type: entity + id: ActionToggleFlight + name: Fly + description: Make use of your wings to fly. Beat the flightless bird allegations. + noSpawn: true + components: + - type: InstantAction + checkCanInteract: false + icon: { sprite: Interface/Actions/flight.rsi, state: flight_off } + iconOn: { sprite : Interface/Actions/flight.rsi, state: flight_on } + event: !type:ToggleFlightEvent \ No newline at end of file diff --git a/Resources/Prototypes/Shaders/shaders.yml b/Resources/Prototypes/Shaders/shaders.yml index b495490201c..3f0cb5ae1fb 100644 --- a/Resources/Prototypes/Shaders/shaders.yml +++ b/Resources/Prototypes/Shaders/shaders.yml @@ -104,3 +104,10 @@ id: SaturationScale kind: source path: "/Textures/Shaders/saturationscale.swsl" + + # Flight shaders + +- type: shader + id: Flap + kind: source + path: "/Textures/Shaders/flap.swsl" \ No newline at end of file diff --git a/Resources/Prototypes/SoundCollections/flight.yml b/Resources/Prototypes/SoundCollections/flight.yml new file mode 100644 index 00000000000..cca2cfb014a --- /dev/null +++ b/Resources/Prototypes/SoundCollections/flight.yml @@ -0,0 +1,6 @@ +- type: soundCollection + id: WingFlaps + files: + - /Audio/Effects/Flight/wingflap1.ogg + - /Audio/Effects/Flight/wingflap2.ogg + - /Audio/Effects/Flight/wingflap3.ogg diff --git a/Resources/Textures/Interface/Actions/flight.rsi/flight_off.png b/Resources/Textures/Interface/Actions/flight.rsi/flight_off.png new file mode 100644 index 0000000000000000000000000000000000000000..852dd300e919fe5740d947caa63d110e07e2a55b GIT binary patch literal 15819 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 literal 0 HcmV?d00001 diff --git a/Resources/Textures/Interface/Actions/flight.rsi/flight_on.png b/Resources/Textures/Interface/Actions/flight.rsi/flight_on.png new file mode 100644 index 0000000000000000000000000000000000000000..c47b923a6810a11e66ab6b3a4a859849d71d704b GIT binary patch literal 16238 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 H+Bf!pP?ZtD literal 0 HcmV?d00001 diff --git a/Resources/Textures/Interface/Actions/flight.rsi/meta.json b/Resources/Textures/Interface/Actions/flight.rsi/meta.json new file mode 100644 index 00000000000..b4a013190db --- /dev/null +++ b/Resources/Textures/Interface/Actions/flight.rsi/meta.json @@ -0,0 +1,17 @@ +{ + "copyright" : "Made by dootythefrooty (273243513800622090)", + "license" : "CC-BY-SA-3.0", + "version": 1, + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "flight_off" + }, + { + "name": "flight_on" + } + ] + } \ No newline at end of file diff --git a/Resources/Textures/Shaders/flap.swsl b/Resources/Textures/Shaders/flap.swsl new file mode 100644 index 00000000000..3082e19b496 --- /dev/null +++ b/Resources/Textures/Shaders/flap.swsl @@ -0,0 +1,35 @@ +preset raw; + +varying highp vec4 VtxModulate; +varying highp vec2 Pos; + +uniform highp float Speed; +uniform highp float Multiplier; +uniform highp float Offset; + +void fragment() { + highp vec4 texColor = zTexture(UV); + lowp vec3 lightSample = texture2D(lightMap, Pos).rgb; + COLOR = texColor * VtxModulate * vec4(lightSample, 1.0); +} + +void vertex() { + vec2 pos = aPos; + + // Apply MVP transformation first + vec2 transformedPos = apply_mvp(pos); + + // Calculate vertical movement in screen space + float verticalOffset = (sin(TIME * Speed) + Offset) * Multiplier; + + // Apply vertical movement after MVP transformation + transformedPos.y += verticalOffset; + + // Assign the final position + VERTEX = transformedPos; + + // Keep the original UV coordinates + UV = mix(modifyUV.xy, modifyUV.zw, tCoord); + Pos = (VERTEX + 1.0) / 2.0; + VtxModulate = zFromSrgb(modulate); +} \ No newline at end of file From 2f5e697bc85d0d2a55165d429e293e3ae0a79bd7 Mon Sep 17 00:00:00 2001 From: SimpleStation Changelogs Date: Mon, 16 Sep 2024 04:42:11 +0000 Subject: [PATCH 03/55] Automatic Changelog Update (#919) --- Resources/Changelog/Changelog.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 0be6b68d7ec..6a8111cec5e 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -6372,3 +6372,12 @@ Entries: id: 6362 time: '2024-09-14T18:01:21.0000000+00:00' url: https://github.com/Simple-Station/Einstein-Engines/pull/868 +- author: Mocho + changes: + - type: Add + message: >- + Harpies are now able to fly on station for limited periods of time, + moving faster at the cost of stamina. + id: 6363 + time: '2024-09-16T04:41:44.0000000+00:00' + url: https://github.com/Simple-Station/Einstein-Engines/pull/919 From 5004cf83d86683dace5d1da068b67fdfd95b9f44 Mon Sep 17 00:00:00 2001 From: Mnemotechnican <69920617+Mnemotechnician@users.noreply.github.com> Date: Wed, 18 Sep 2024 02:38:45 +0300 Subject: [PATCH 04/55] Physical Pulling (#883) # Description Fixes pulling being obnoxiously unphysical. Entities now exert a specific amount of force defined as the product of their mass and the SpecificForce set in their PullerComponent. Doing a ctrl-right click now sets a target position where your entity begins to continuously push the pulled entity towards, instead of throwing it or pushing it once (however, it should still be possible to drag people and disposals and the like). Additionally, your entity will properly experience recoil from dragging someone around. # TODO - [X] Do stuff - [X] Fix stuff - [X] Fix aghosts not being able to push (?) - [X] Fix pulling giving you free kinetic energy while in spess (how?) - [ ] Test if throwing felinids into disposals via pulling still works :trollface:

Media

https://github.com/user-attachments/assets/2498ef8c-f743-4702-b73c-b2da0c16e9aa

# Changelog :cl: - add: Pulling has been reworked. Your character will now try to continuously push the pulled entity when you use the push keybind. - remove: You can no longer push the pulled entity while walking, and pushing now follows the momentum conservation laws. --- .../Pulling/Components/PullableComponent.cs | 7 + .../Pulling/Components/PullerComponent.cs | 38 ++++- .../Movement/Pulling/Systems/PullingSystem.cs | 137 +++++++++++++++--- .../Entities/Mobs/Player/admin_ghost.yml | 2 + 4 files changed, 154 insertions(+), 30 deletions(-) diff --git a/Content.Shared/Movement/Pulling/Components/PullableComponent.cs b/Content.Shared/Movement/Pulling/Components/PullableComponent.cs index db889e7e3bd..01ce0efaae6 100644 --- a/Content.Shared/Movement/Pulling/Components/PullableComponent.cs +++ b/Content.Shared/Movement/Pulling/Components/PullableComponent.cs @@ -36,4 +36,11 @@ public sealed partial class PullableComponent : Component [Access(typeof(Systems.PullingSystem), Other = AccessPermissions.ReadExecute)] [AutoNetworkedField, DataField] public bool PrevFixedRotation; + + /// + /// Whether the entity is currently being actively pushed by the puller. + /// If true, the entity will be able to enter disposals upon colliding with them, and the like. + /// + [DataField, AutoNetworkedField] + public bool BeingActivelyPushed = false; } diff --git a/Content.Shared/Movement/Pulling/Components/PullerComponent.cs b/Content.Shared/Movement/Pulling/Components/PullerComponent.cs index 1fc9b731bd5..c13baa28eff 100644 --- a/Content.Shared/Movement/Pulling/Components/PullerComponent.cs +++ b/Content.Shared/Movement/Pulling/Components/PullerComponent.cs @@ -1,5 +1,6 @@ using Content.Shared.Movement.Pulling.Systems; using Robust.Shared.GameStates; +using Robust.Shared.Map; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; namespace Content.Shared.Movement.Pulling.Components; @@ -11,16 +12,17 @@ namespace Content.Shared.Movement.Pulling.Components; [Access(typeof(PullingSystem))] public sealed partial class PullerComponent : Component { - // My raiding guild /// - /// Next time the puller can throw what is being pulled. - /// Used to avoid spamming it for infinite spin + velocity. + /// Next time the puller change where they are pulling the target towards. /// [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoNetworkedField] - public TimeSpan NextThrow; + public TimeSpan NextPushTargetChange; + + [DataField, AutoNetworkedField] + public TimeSpan NextPushStop; [DataField] - public TimeSpan ThrowCooldown = TimeSpan.FromSeconds(1); + public TimeSpan PushChangeCooldown = TimeSpan.FromSeconds(0.1f), PushDuration = TimeSpan.FromSeconds(2f); // Before changing how this is updated, please see SharedPullerSystem.RefreshMovementSpeed public float WalkSpeedModifier => Pulling == default ? 1.0f : 0.95f; @@ -28,14 +30,38 @@ public sealed partial class PullerComponent : Component public float SprintSpeedModifier => Pulling == default ? 1.0f : 0.95f; /// - /// Entity currently being pulled if applicable. + /// Entity currently being pulled/pushed if applicable. /// [AutoNetworkedField, DataField] public EntityUid? Pulling; + /// + /// The position the entity is currently being pulled towards. + /// + [AutoNetworkedField, DataField] + public EntityCoordinates? PushingTowards; + /// /// Does this entity need hands to be able to pull something? /// [DataField] public bool NeedsHands = true; + + /// + /// The maximum acceleration of pushing, in meters per second squared. + /// + [DataField] + public float PushAcceleration = 0.3f; + + /// + /// The maximum speed to which the pulled entity may be accelerated relative to the push direction, in meters per second. + /// + [DataField] + public float MaxPushSpeed = 1.2f; + + /// + /// The maximum distance between the puller and the point towards which the puller may attempt to pull it, in meters. + /// + [DataField] + public float MaxPushRange = 2f; } diff --git a/Content.Shared/Movement/Pulling/Systems/PullingSystem.cs b/Content.Shared/Movement/Pulling/Systems/PullingSystem.cs index 3c265d5a027..e6acabc33c5 100644 --- a/Content.Shared/Movement/Pulling/Systems/PullingSystem.cs +++ b/Content.Shared/Movement/Pulling/Systems/PullingSystem.cs @@ -8,16 +8,19 @@ using Content.Shared.Hands.EntitySystems; using Content.Shared.Input; using Content.Shared.Interaction; +using Content.Shared.Movement.Components; using Content.Shared.Movement.Events; using Content.Shared.Movement.Pulling.Components; using Content.Shared.Movement.Pulling.Events; using Content.Shared.Movement.Systems; +using Content.Shared.Projectiles; using Content.Shared.Pulling.Events; using Content.Shared.Throwing; using Content.Shared.Verbs; using Robust.Shared.Containers; using Robust.Shared.Input.Binding; using Robust.Shared.Map; +using Robust.Shared.Network; using Robust.Shared.Physics; using Robust.Shared.Physics.Components; using Robust.Shared.Physics.Events; @@ -34,6 +37,7 @@ public sealed class PullingSystem : EntitySystem { [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; + [Dependency] private readonly INetManager _net = default!; [Dependency] private readonly ActionBlockerSystem _blocker = default!; [Dependency] private readonly AlertsSystem _alertsSystem = default!; [Dependency] private readonly MovementSpeedModifierSystem _modifierSystem = default!; @@ -43,7 +47,7 @@ public sealed class PullingSystem : EntitySystem [Dependency] private readonly SharedInteractionSystem _interaction = default!; [Dependency] private readonly SharedPhysicsSystem _physics = default!; [Dependency] private readonly SharedTransformSystem _xformSys = default!; - [Dependency] private readonly ThrowingSystem _throwing = default!; + [Dependency] private readonly ThrownItemSystem _thrownItem = default!; public override void Initialize() { @@ -57,7 +61,9 @@ public override void Initialize() SubscribeLocalEvent(OnJointRemoved); SubscribeLocalEvent>(AddPullVerbs); SubscribeLocalEvent(OnPullableContainerInsert); + SubscribeLocalEvent(OnPullableCollide); + SubscribeLocalEvent(OnPullerMoveInput); SubscribeLocalEvent(OnPullerContainerInsert); SubscribeLocalEvent(OnPullerUnpaused); SubscribeLocalEvent(OnVirtualItemDeleted); @@ -69,6 +75,86 @@ public override void Initialize() .Register(); } + public override void Shutdown() + { + base.Shutdown(); + CommandBinds.Unregister(); + } + + public override void Update(float frameTime) + { + if (_net.IsClient) // Client cannot predict this + return; + + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var puller, out var pullerComp, out var pullerPhysics, out var pullerXForm)) + { + // If not pulling, reset the pushing cooldowns and exit + if (pullerComp.Pulling is not { } pulled || !TryComp(pulled, out var pulledComp)) + { + pullerComp.PushingTowards = null; + pullerComp.NextPushTargetChange = TimeSpan.Zero; + continue; + } + + pulledComp.BeingActivelyPushed = false; // Temporarily set to false; if the checks below pass, it will be set to true again + + // If pulling but the pullee is invalid or is on a different map, stop pulling + var pulledXForm = Transform(pulled); + if (!TryComp(pulled, out var pulledPhysics) + || pulledPhysics.BodyType == BodyType.Static + || pulledXForm.MapUid != pullerXForm.MapUid) + { + StopPulling(pulled, pulledComp); + continue; + } + + if (pullerComp.PushingTowards is null) + continue; + + // If pushing but the target position is invalid, or the push action has expired or finished, stop pushing + if (pullerComp.NextPushStop < _timing.CurTime + || !(pullerComp.PushingTowards.Value.ToMap(EntityManager, _xformSys) is var pushCoordinates) + || pushCoordinates.MapId != pulledXForm.MapID) + { + pullerComp.PushingTowards = null; + pullerComp.NextPushTargetChange = TimeSpan.Zero; + continue; + } + + // Actual force calculation. All the Vector2's below are in map coordinates. + var desiredDeltaPos = pushCoordinates.Position - Transform(pulled).Coordinates.ToMapPos(EntityManager, _xformSys); + if (desiredDeltaPos.LengthSquared() < 0.1f) + { + pullerComp.PushingTowards = null; + continue; + } + + var velocityAndDirectionAngle = new Angle(pulledPhysics.LinearVelocity) - new Angle(desiredDeltaPos); + var currentRelativeSpeed = pulledPhysics.LinearVelocity.Length() * (float) Math.Cos(velocityAndDirectionAngle.Theta); + var desiredAcceleration = MathF.Max(0f, pullerComp.MaxPushSpeed - currentRelativeSpeed); + + var desiredImpulse = pulledPhysics.Mass * desiredDeltaPos; + var maxSourceImpulse = MathF.Min(pullerComp.PushAcceleration, desiredAcceleration) * pullerPhysics.Mass; + var actualImpulse = desiredImpulse.LengthSquared() > maxSourceImpulse * maxSourceImpulse ? desiredDeltaPos.Normalized() * maxSourceImpulse : desiredImpulse; + + // Ideally we'd want to apply forces instead of impulses, however... + // We cannot use ApplyForce here because it will be cleared on the next physics substep which will render it ultimately useless + // The alternative is to run this function on every physics substep, but that is way too expensive for such a minor system + _physics.ApplyLinearImpulse(pulled, actualImpulse); + _physics.ApplyLinearImpulse(puller, -actualImpulse); + pulledComp.BeingActivelyPushed = true; + } + query.Dispose(); + } + + private void OnPullerMoveInput(EntityUid uid, PullerComponent component, ref MoveInputEvent args) + { + // Stop pushing + component.PushingTowards = null; + component.NextPushStop = TimeSpan.Zero; + } + private void OnPullerContainerInsert(Entity ent, ref EntGotInsertedIntoContainerMessage args) { if (ent.Comp.Pulling == null) return; @@ -84,15 +170,26 @@ private void OnPullableContainerInsert(Entity ent, ref EntGot TryStopPull(ent.Owner, ent.Comp); } - public override void Shutdown() + private void OnPullableCollide(Entity ent, ref StartCollideEvent args) { - base.Shutdown(); - CommandBinds.Unregister(); + if (!ent.Comp.BeingActivelyPushed || args.OtherEntity == ent.Comp.Puller) + return; + + // This component isn't actually needed anywhere besides the thrownitemsyste`m itself, so we just fake it + var fakeThrown = new ThrownItemComponent() + { + Owner = ent.Owner, + Animate = false, + Landed = false, + PlayLandSound = false, + Thrower = ent.Comp.Puller + }; + _thrownItem.ThrowCollideInteraction(fakeThrown, ent, args.OtherEntity); } private void OnPullerUnpaused(EntityUid uid, PullerComponent component, ref EntityUnpausedEvent args) { - component.NextThrow += args.PausedTime; + component.NextPushTargetChange += args.PausedTime; } private void OnVirtualItemDeleted(EntityUid uid, PullerComponent component, VirtualItemDeletedEvent args) @@ -234,31 +331,22 @@ public bool IsPulled(EntityUid uid, PullableComponent? component = null) private bool OnRequestMovePulledObject(ICommonSession? session, EntityCoordinates coords, EntityUid uid) { - if (session?.AttachedEntity is not { } player || - !player.IsValid()) - { - return false; - } - - if (!TryComp(player, out var pullerComp)) + if (session?.AttachedEntity is not { } player + || !player.IsValid() + || !TryComp(player, out var pullerComp)) return false; var pulled = pullerComp.Pulling; - - if (!HasComp(pulled)) - return false; - - if (_containerSystem.IsEntityInContainer(player)) + if (!HasComp(pulled) + || _containerSystem.IsEntityInContainer(player) + || _timing.CurTime < pullerComp.NextPushTargetChange) return false; - // Cooldown buddy - if (_timing.CurTime < pullerComp.NextThrow) - return false; - - pullerComp.NextThrow = _timing.CurTime + pullerComp.ThrowCooldown; + pullerComp.NextPushTargetChange = _timing.CurTime + pullerComp.PushChangeCooldown; + pullerComp.NextPushStop = _timing.CurTime + pullerComp.PushDuration; // Cap the distance - const float range = 2f; + var range = pullerComp.MaxPushRange; var fromUserCoords = coords.WithEntityId(player, EntityManager); var userCoords = new EntityCoordinates(player, Vector2.Zero); @@ -268,8 +356,9 @@ private bool OnRequestMovePulledObject(ICommonSession? session, EntityCoordinate fromUserCoords = userCoords.Offset(userDirection.Normalized() * range); } + pullerComp.PushingTowards = fromUserCoords; Dirty(player, pullerComp); - _throwing.TryThrow(pulled.Value, fromUserCoords, user: player, strength: 4f, animated: false, recoil: false, playSound: false, doSpin: false); + return false; } diff --git a/Resources/Prototypes/Entities/Mobs/Player/admin_ghost.yml b/Resources/Prototypes/Entities/Mobs/Player/admin_ghost.yml index 6c9ec2049f4..e0300fa5033 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/admin_ghost.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/admin_ghost.yml @@ -25,6 +25,8 @@ - type: GhostHearing - type: Hands - type: Puller + pushAcceleration: 1000000 # Will still be capped in max speed + maxPushRange: 20 - type: CombatMode - type: Physics ignorePaused: true From b65a5d853baeccaa15734a5e09dec03996333cac Mon Sep 17 00:00:00 2001 From: VMSolidus Date: Tue, 17 Sep 2024 19:39:07 -0400 Subject: [PATCH 05/55] Core Fullsize (Underfueled) AME (#927) # Description By request. A typical engineering team takes as much as 40 minutes to setup a Supermatter engine with no experience. Core is designed to **FORCE** players to setup a Singulo, which takes 15 minutes, therefore Core loses power in 20 minutes with its comically undersized and underpowered AME. I increased the size of Core's AME so that engineers have more time to setup Supermatter. A lot more time actually. # Changelog :cl: - tweak: Core now has a 6-Core AME, which is supplied with two jars of fuel. This should give Engineers significantly more than 20 minutes of time to setup the Supermatter engine. --- Resources/Maps/core.yml | 292 +++++++++++++++++++++------------------- 1 file changed, 155 insertions(+), 137 deletions(-) diff --git a/Resources/Maps/core.yml b/Resources/Maps/core.yml index 0f31cea8379..d8ad22c30ac 100644 --- a/Resources/Maps/core.yml +++ b/Resources/Maps/core.yml @@ -97,11 +97,11 @@ entities: version: 6 0,-2: ind: 0,-2 - tiles: aAAAAAAAaAAAAAAAXQAAAAADXQAAAAADaAAAAAACXQAAAAABXQAAAAADXQAAAAABaAAAAAABXQAAAAADaAAAAAADXQAAAAAAXQAAAAABXQAAAAAAXQAAAAADXQAAAAADTgAAAAABTgAAAAABaAAAAAACaAAAAAACaAAAAAABaAAAAAAAaAAAAAABaAAAAAACaAAAAAABXQAAAAACfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAXQAAAAACXQAAAAABXQAAAAAAXQAAAAAAaAAAAAADXQAAAAABXQAAAAABXQAAAAACXQAAAAADXQAAAAABfgAAAAAAHwAAAAAAJAAAAAAAJAAAAAABJAAAAAACJAAAAAACfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAXQAAAAAAXQAAAAABXQAAAAAAXQAAAAADXQAAAAABfgAAAAAAHwAAAAACJAAAAAABfgAAAAAAfgAAAAAAfgAAAAAAHwAAAAAAfgAAAAAAJAAAAAADegAAAAAAfgAAAAAAHwAAAAACHwAAAAAAHwAAAAACHwAAAAAAJAAAAAADfgAAAAAAHwAAAAADJAAAAAABfgAAAAAAfgAAAAAAfgAAAAAAHwAAAAADHwAAAAADegAAAAAAegAAAAAAfgAAAAAAHwAAAAAAHwAAAAACHwAAAAADHwAAAAABHwAAAAABfgAAAAAAHwAAAAACJAAAAAADfgAAAAAAfgAAAAAAfgAAAAAAHwAAAAACfgAAAAAAegAAAAAAegAAAAADfgAAAAAAfgAAAAAAfgAAAAAAHwAAAAABfgAAAAAAfgAAAAAAfgAAAAAAHwAAAAADJAAAAAABJAAAAAAAJAAAAAADJAAAAAADfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAXQAAAAADXQAAAAACaAAAAAABXQAAAAABHwAAAAABfgAAAAAAHwAAAAACJAAAAAABJAAAAAACJAAAAAAAJAAAAAABXQAAAAAAXQAAAAAAJAAAAAAAfgAAAAAAJAAAAAACaAAAAAADaAAAAAAAaAAAAAACXQAAAAAAHwAAAAADfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAHwAAAAABfgAAAAAATgAAAAACTgAAAAADJAAAAAABfgAAAAAAaAAAAAADaAAAAAAAaAAAAAABaAAAAAACXQAAAAACHwAAAAABHwAAAAACHwAAAAADHwAAAAAAHwAAAAACHwAAAAAAHwAAAAADXQAAAAAAXQAAAAABXQAAAAABaAAAAAADaAAAAAADaAAAAAACaAAAAAADaAAAAAACXQAAAAABHwAAAAACHwAAAAAAHwAAAAABHwAAAAABXQAAAAADXQAAAAADaAAAAAAAXQAAAAAAXQAAAAABXQAAAAADfgAAAAAAaAAAAAACaAAAAAADaAAAAAACaAAAAAADXQAAAAAAHwAAAAAAHwAAAAAAfgAAAAAAHwAAAAACXQAAAAADXQAAAAABaAAAAAAAHwAAAAACHwAAAAAAHwAAAAABfgAAAAAAaAAAAAAAaAAAAAAAaAAAAAAAaAAAAAADXQAAAAACfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAXQAAAAADfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAaAAAAAACaAAAAAABaAAAAAABaAAAAAABXQAAAAABfgAAAAAAJAAAAAACJAAAAAAAJAAAAAABXQAAAAABXQAAAAADaAAAAAAAXQAAAAAAXQAAAAADXQAAAAACfgAAAAAAXQAAAAABXQAAAAABXQAAAAABXQAAAAADXQAAAAAAfgAAAAAAJAAAAAACHwAAAAACHwAAAAACXQAAAAAAXQAAAAADaAAAAAADXQAAAAABXQAAAAACXQAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAbAAAAAAAfgAAAAAAJAAAAAAAHwAAAAADHwAAAAABXQAAAAADXQAAAAAAaAAAAAAD + tiles: aAAAAAAAaAAAAAAAXQAAAAADXQAAAAADaAAAAAACXQAAAAABXQAAAAADXQAAAAABaAAAAAABXQAAAAADaAAAAAADXQAAAAAAXQAAAAABXQAAAAAAXQAAAAADXQAAAAADTgAAAAABTgAAAAABaAAAAAACaAAAAAACaAAAAAABaAAAAAAAaAAAAAABaAAAAAACaAAAAAABXQAAAAACfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAXQAAAAACXQAAAAABXQAAAAAAXQAAAAAAaAAAAAADXQAAAAABXQAAAAABXQAAAAACXQAAAAADXQAAAAABfgAAAAAAHwAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAXQAAAAAAXQAAAAABXQAAAAAAXQAAAAADXQAAAAABfgAAAAAAHwAAAAACfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAHwAAAAAAfgAAAAAAJAAAAAADegAAAAAAfgAAAAAAHwAAAAACHwAAAAAAHwAAAAACHwAAAAAAJAAAAAADfgAAAAAAHwAAAAADfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAHwAAAAADHwAAAAADegAAAAAAegAAAAAAfgAAAAAAHwAAAAAAHwAAAAACHwAAAAADHwAAAAABHwAAAAABfgAAAAAAHwAAAAACfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAHwAAAAACfgAAAAAAegAAAAAAegAAAAADfgAAAAAAfgAAAAAAfgAAAAAAHwAAAAABfgAAAAAAfgAAAAAAfgAAAAAAHwAAAAADJAAAAAABJAAAAAAAJAAAAAADJAAAAAADfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAXQAAAAADXQAAAAACaAAAAAABXQAAAAABHwAAAAABfgAAAAAAHwAAAAACJAAAAAABJAAAAAACJAAAAAAAJAAAAAABXQAAAAAAXQAAAAAAJAAAAAAAfgAAAAAAJAAAAAACaAAAAAADaAAAAAAAaAAAAAACXQAAAAAAHwAAAAADfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAHwAAAAABfgAAAAAATgAAAAACTgAAAAADJAAAAAABfgAAAAAAaAAAAAADaAAAAAAAaAAAAAABaAAAAAACXQAAAAACHwAAAAABHwAAAAACHwAAAAADHwAAAAAAHwAAAAACHwAAAAAAHwAAAAADXQAAAAAAXQAAAAABXQAAAAABaAAAAAADaAAAAAADaAAAAAACaAAAAAADaAAAAAACXQAAAAABHwAAAAACHwAAAAAAHwAAAAABHwAAAAABXQAAAAADXQAAAAADaAAAAAAAXQAAAAAAXQAAAAABXQAAAAADfgAAAAAAaAAAAAACaAAAAAADaAAAAAACaAAAAAADXQAAAAAAHwAAAAAAHwAAAAAAfgAAAAAAHwAAAAACXQAAAAADXQAAAAABaAAAAAAAHwAAAAACHwAAAAAAHwAAAAABfgAAAAAAaAAAAAAAaAAAAAAAaAAAAAAAaAAAAAADXQAAAAACfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAXQAAAAADfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAaAAAAAACaAAAAAABaAAAAAABaAAAAAABXQAAAAABfgAAAAAAJAAAAAACJAAAAAAAJAAAAAABXQAAAAABXQAAAAADaAAAAAAAXQAAAAAAXQAAAAADXQAAAAACfgAAAAAAXQAAAAABXQAAAAABXQAAAAABXQAAAAADXQAAAAAAfgAAAAAAJAAAAAACHwAAAAACHwAAAAACXQAAAAAAXQAAAAADaAAAAAADXQAAAAABXQAAAAACXQAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAbAAAAAAAfgAAAAAAJAAAAAAAHwAAAAADHwAAAAABXQAAAAADXQAAAAAAaAAAAAAD version: 6 1,-2: ind: 1,-2 - tiles: XQAAAAAAXQAAAAABaAAAAAAAXQAAAAAAaAAAAAAAXQAAAAACfgAAAAAAHwAAAAACaAAAAAADaAAAAAABaAAAAAABaAAAAAADaAAAAAABHwAAAAABfgAAAAAAHwAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAXQAAAAADaAAAAAABXQAAAAABfgAAAAAAHwAAAAAAaAAAAAADaAAAAAAAaAAAAAADaAAAAAABaAAAAAABHwAAAAAAfgAAAAAAHwAAAAAAJAAAAAABHwAAAAACfgAAAAAAXQAAAAABaAAAAAACXQAAAAACfgAAAAAAHwAAAAAAaAAAAAAAaAAAAAABaAAAAAABXQAAAAADaAAAAAACHwAAAAADHwAAAAACHwAAAAACJAAAAAAAHwAAAAADfgAAAAAAXQAAAAADaAAAAAAAXQAAAAABfgAAAAAAHwAAAAADaAAAAAABaAAAAAABaAAAAAABaAAAAAADaAAAAAADHwAAAAABfgAAAAAAHwAAAAAAJAAAAAACHwAAAAABfgAAAAAAaAAAAAAAaAAAAAABaAAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAHwAAAAABJAAAAAADHwAAAAAAfgAAAAAAXQAAAAADaAAAAAACXQAAAAAAJAAAAAABXQAAAAAAXQAAAAADXQAAAAABXQAAAAABXQAAAAACXQAAAAAAXQAAAAACfgAAAAAAfgAAAAAAJAAAAAADHwAAAAABfgAAAAAAXQAAAAACaAAAAAACaAAAAAAAaAAAAAABaAAAAAACaAAAAAACaAAAAAABaAAAAAADaAAAAAABaAAAAAADXQAAAAACfgAAAAAAHwAAAAABJAAAAAAAHwAAAAADfgAAAAAAXQAAAAACXQAAAAADXQAAAAABXQAAAAACXQAAAAABXQAAAAAAXQAAAAADXQAAAAABXQAAAAABaAAAAAAAXQAAAAACfgAAAAAAXQAAAAABfgAAAAAAfgAAAAAAfgAAAAAAXQAAAAACXQAAAAABXQAAAAABfgAAAAAAXQAAAAACfgAAAAAAAwAAAAAAAwAAAAAAXQAAAAACaAAAAAADXQAAAAACfgAAAAAAXQAAAAABfgAAAAAAHwAAAAAAHwAAAAACXQAAAAABXQAAAAAAXQAAAAACfgAAAAAAXQAAAAABfgAAAAAAAwAAAAAAAwAAAAAAXQAAAAABaAAAAAACXQAAAAABfgAAAAAAfgAAAAAAfgAAAAAAHwAAAAAAHwAAAAAAXQAAAAACXQAAAAACXQAAAAAAXQAAAAACXQAAAAACfgAAAAAAAwAAAAAAAwAAAAAAXQAAAAABaAAAAAAAXQAAAAACfgAAAAAAXQAAAAAAfgAAAAAAHwAAAAAAJAAAAAAAXQAAAAACXQAAAAACXQAAAAADfgAAAAAAHwAAAAABfgAAAAAAAwAAAAAAAwAAAAAAXQAAAAAAaAAAAAABXQAAAAAAaAAAAAABXQAAAAABfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAXQAAAAABfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAXQAAAAABaAAAAAABaAAAAAABaAAAAAAAaAAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAHwAAAAABHwAAAAABHwAAAAABHwAAAAADfgAAAAAAXQAAAAABXQAAAAABXQAAAAACaAAAAAAAXQAAAAACfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAHwAAAAABHwAAAAAAHwAAAAABHwAAAAABfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAHwAAAAADHwAAAAACHwAAAAADHwAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfQAAAAAA + tiles: XQAAAAAAXQAAAAABaAAAAAAAXQAAAAAAaAAAAAAAXQAAAAACfgAAAAAAHwAAAAACaAAAAAADaAAAAAABaAAAAAABaAAAAAADaAAAAAABHwAAAAABfgAAAAAAHwAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAXQAAAAADaAAAAAABXQAAAAABfgAAAAAAHwAAAAAAaAAAAAADaAAAAAAAaAAAAAADaAAAAAABaAAAAAABHwAAAAAAfgAAAAAAHwAAAAAAfgAAAAAAHwAAAAACfgAAAAAAXQAAAAABaAAAAAACXQAAAAACfgAAAAAAHwAAAAAAaAAAAAAAaAAAAAABaAAAAAABXQAAAAADaAAAAAACHwAAAAADHwAAAAACHwAAAAACfgAAAAAAHwAAAAADfgAAAAAAXQAAAAADaAAAAAAAXQAAAAABfgAAAAAAHwAAAAADaAAAAAABaAAAAAABaAAAAAABaAAAAAADaAAAAAADHwAAAAABfgAAAAAAHwAAAAAAfgAAAAAAHwAAAAABfgAAAAAAaAAAAAAAaAAAAAABaAAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAHwAAAAABfgAAAAAAHwAAAAAAfgAAAAAAXQAAAAADaAAAAAACXQAAAAAAJAAAAAABXQAAAAAAXQAAAAADXQAAAAABXQAAAAABXQAAAAACXQAAAAAAXQAAAAACfgAAAAAAfgAAAAAAJAAAAAADHwAAAAABfgAAAAAAXQAAAAACaAAAAAACaAAAAAAAaAAAAAABaAAAAAACaAAAAAACaAAAAAABaAAAAAADaAAAAAABaAAAAAADXQAAAAACfgAAAAAAHwAAAAABJAAAAAAAHwAAAAADfgAAAAAAXQAAAAACXQAAAAADXQAAAAABXQAAAAACXQAAAAABXQAAAAAAXQAAAAADXQAAAAABXQAAAAABaAAAAAAAXQAAAAACfgAAAAAAXQAAAAABfgAAAAAAfgAAAAAAfgAAAAAAXQAAAAACXQAAAAABXQAAAAABfgAAAAAAXQAAAAACfgAAAAAAAwAAAAAAAwAAAAAAXQAAAAACaAAAAAADXQAAAAACfgAAAAAAXQAAAAABfgAAAAAAHwAAAAAAHwAAAAACXQAAAAABXQAAAAAAXQAAAAACfgAAAAAAXQAAAAABfgAAAAAAAwAAAAAAAwAAAAAAXQAAAAABaAAAAAACXQAAAAABfgAAAAAAfgAAAAAAfgAAAAAAHwAAAAAAHwAAAAAAXQAAAAACXQAAAAACXQAAAAAAXQAAAAACXQAAAAACfgAAAAAAAwAAAAAAAwAAAAAAXQAAAAABaAAAAAAAXQAAAAACfgAAAAAAXQAAAAAAfgAAAAAAHwAAAAAAJAAAAAAAXQAAAAACXQAAAAACXQAAAAADfgAAAAAAHwAAAAABfgAAAAAAAwAAAAAAAwAAAAAAXQAAAAAAaAAAAAABXQAAAAAAaAAAAAABXQAAAAABfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAXQAAAAABfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAXQAAAAABaAAAAAABaAAAAAABaAAAAAAAaAAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAHwAAAAABHwAAAAABHwAAAAABHwAAAAADfgAAAAAAXQAAAAABXQAAAAABXQAAAAACaAAAAAAAXQAAAAACfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAHwAAAAABHwAAAAAAHwAAAAABHwAAAAABfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAHwAAAAADHwAAAAACHwAAAAADHwAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfQAAAAAA version: 6 1,-1: ind: 1,-1 @@ -3793,7 +3793,6 @@ entities: 5754: 12,-22 5755: 15,-26 5756: 12,-26 - 5757: 12,-29 5758: 7,-30 5759: 8,-32 5760: 7,-32 @@ -3860,15 +3859,6 @@ entities: 6417: 39,-3 6418: 37,-3 6419: 37,-3 - - node: - cleanable: True - color: '#FFFFFF47' - id: Dirt - decals: - 6314: 15,-30 - 6315: 15,-30 - 6316: 16,-27 - 6317: 16,-27 - node: cleanable: True color: '#FFFFFFFF' @@ -5194,7 +5184,6 @@ entities: 4911: 13,-17 4912: 11,-30 4913: 11,-28 - 4914: 11,-27 4915: 11,-26 4916: 17,-26 4917: 17,-28 @@ -5955,15 +5944,12 @@ entities: 3363: 10,-16 3364: 10,-18 3365: 10,-18 - 3366: 12,-29 - 3367: 12,-28 3368: 12,-26 3369: 13,-25 3370: 14,-25 3371: 14,-26 3372: 15,-25 3373: 16,-26 - 3374: 16,-29 3375: -4,-36 3376: -4,-37 3377: -6,-37 @@ -6482,10 +6468,6 @@ entities: id: DirtHeavyMonotile decals: 2847: -44,17 - 6310: 13,-30 - 6311: 16,-30 - 6312: 12,-30 - 6313: 12,-30 - node: cleanable: True angle: -6.283185307179586 rad @@ -8333,12 +8315,6 @@ entities: decals: 6401: 49,-11 6402: 52,-11 - - node: - color: '#EFB34118' - id: WarnCornerSmallNE - decals: - 6325: 12,-30 - 6326: 12,-30 - node: color: '#EFB34131' id: WarnCornerSmallNE @@ -8347,11 +8323,6 @@ entities: 6471: 14,39 6472: 14,39 6473: 14,39 - - node: - color: '#EFB3416C' - id: WarnCornerSmallNE - decals: - 6324: 12,-30 - node: color: '#EFB34196' id: WarnCornerSmallNE @@ -8368,12 +8339,6 @@ entities: 6071: 51,-11 6748: 46,15 6846: 57,-5 - - node: - color: '#EFB34118' - id: WarnCornerSmallNW - decals: - 6327: 16,-30 - 6328: 16,-30 - node: color: '#EFB34131' id: WarnCornerSmallNW @@ -8382,11 +8347,6 @@ entities: 6467: 16,39 6468: 16,39 6469: 16,39 - - node: - color: '#EFB3416C' - id: WarnCornerSmallNW - decals: - 6323: 16,-30 - node: color: '#EFB34196' id: WarnCornerSmallNW @@ -8469,9 +8429,6 @@ entities: 85: -4,-19 116: -4,-16 119: -2,-15 - 189: 12,-27 - 190: 12,-28 - 191: 12,-29 265: 25,-16 266: 25,-15 276: 29,-13 @@ -8648,9 +8605,6 @@ entities: 88: -10,-17 89: -10,-16 128: 0,-21 - 186: 16,-27 - 187: 16,-28 - 188: 16,-29 194: 13,-21 195: 13,-20 196: 13,-19 @@ -8677,16 +8631,6 @@ entities: 6370: -33,-11 6743: 46,16 6824: 61,-4 - - node: - color: '#EFB34118' - id: WarnLineW - decals: - 6329: 13,-30 - 6330: 13,-30 - 6331: 14,-30 - 6332: 14,-30 - 6333: 15,-30 - 6334: 15,-30 - node: color: '#EFB34131' id: WarnLineW @@ -8695,13 +8639,6 @@ entities: 6451: 15,39 6452: 15,39 6453: 15,39 - - node: - color: '#EFB3416C' - id: WarnLineW - decals: - 6320: 13,-30 - 6321: 14,-30 - 6322: 15,-30 - node: color: '#EFB34196' id: WarnLineW @@ -17945,10 +17882,15 @@ entities: parent: 2 - proto: AmeJar entities: - - uid: 94 + - uid: 695 components: - type: Transform - pos: 17.51151,-27.35627 + pos: 15.659197,-24.407036 + parent: 2 + - uid: 1534 + components: + - type: Transform + pos: 15.346697,-24.39141 parent: 2 - proto: AmePartFlatpack entities: @@ -17997,6 +17939,61 @@ entities: - type: Transform pos: 16.69675,-24.555443 parent: 2 + - uid: 693 + components: + - type: Transform + pos: 16.299822,-24.36016 + parent: 2 + - uid: 694 + components: + - type: Transform + pos: 17.768572,-25.313286 + parent: 2 + - uid: 6126 + components: + - type: Transform + pos: 16.299822,-24.20391 + parent: 2 + - uid: 10573 + components: + - type: Transform + pos: 16.299822,-24.563286 + parent: 2 + - uid: 22532 + components: + - type: Transform + pos: 17.768572,-25.563286 + parent: 2 + - uid: 22533 + components: + - type: Transform + pos: 17.768572,-25.73516 + parent: 2 + - uid: 22534 + components: + - type: Transform + pos: 17.284197,-25.70391 + parent: 2 + - uid: 22535 + components: + - type: Transform + pos: 17.284197,-25.48516 + parent: 2 + - uid: 22536 + components: + - type: Transform + pos: 17.284197,-25.26641 + parent: 2 + - uid: 22538 + components: + - type: Transform + pos: 17.737322,-26.20391 + parent: 2 + - uid: 22539 + components: + - type: Transform + pos: 17.518572,-26.344536 + parent: 2 - proto: AmmoniaCanister entities: - uid: 22510 @@ -51029,50 +51026,37 @@ entities: - uid: 641 components: - type: Transform - rot: -1.5707963267948966 rad - pos: 13.5,-26.5 - parent: 2 - - uid: 642 - components: - - type: Transform - rot: -1.5707963267948966 rad - pos: 13.5,-27.5 + pos: 12.5,-28.5 parent: 2 - uid: 643 components: - type: Transform - rot: -1.5707963267948966 rad - pos: 13.5,-28.5 + pos: 12.5,-27.5 parent: 2 - uid: 647 components: - type: Transform - rot: -1.5707963267948966 rad - pos: 14.5,-26.5 + pos: 13.5,-29.5 parent: 2 - uid: 648 components: - type: Transform - rot: -1.5707963267948966 rad - pos: 14.5,-27.5 + pos: 12.5,-29.5 parent: 2 - uid: 650 components: - type: Transform - rot: -1.5707963267948966 rad - pos: 15.5,-27.5 + pos: 13.5,-27.5 parent: 2 - uid: 651 components: - type: Transform - rot: -1.5707963267948966 rad - pos: 15.5,-28.5 + pos: 14.5,-26.5 parent: 2 - uid: 652 components: - type: Transform - rot: -1.5707963267948966 rad - pos: 15.5,-26.5 + pos: 13.5,-28.5 parent: 2 - uid: 653 components: @@ -51212,6 +51196,11 @@ entities: rot: 1.5707963267948966 rad pos: 16.5,-13.5 parent: 2 + - uid: 692 + components: + - type: Transform + pos: 13.5,-26.5 + parent: 2 - uid: 753 components: - type: Transform @@ -51400,6 +51389,11 @@ entities: rot: -1.5707963267948966 rad pos: -22.5,25.5 parent: 2 + - uid: 2189 + components: + - type: Transform + pos: 12.5,-26.5 + parent: 2 - uid: 2199 components: - type: Transform @@ -51660,11 +51654,6 @@ entities: - type: Transform pos: 40.5,21.5 parent: 2 - - uid: 6126 - components: - - type: Transform - pos: 14.5,-28.5 - parent: 2 - uid: 6203 components: - type: Transform @@ -56376,6 +56365,61 @@ entities: - type: Transform pos: -6.5,-12.5 parent: 2 + - uid: 22521 + components: + - type: Transform + pos: 14.5,-27.5 + parent: 2 + - uid: 22522 + components: + - type: Transform + pos: 14.5,-28.5 + parent: 2 + - uid: 22523 + components: + - type: Transform + pos: 14.5,-29.5 + parent: 2 + - uid: 22524 + components: + - type: Transform + pos: 15.5,-29.5 + parent: 2 + - uid: 22525 + components: + - type: Transform + pos: 15.5,-28.5 + parent: 2 + - uid: 22526 + components: + - type: Transform + pos: 15.5,-27.5 + parent: 2 + - uid: 22527 + components: + - type: Transform + pos: 15.5,-26.5 + parent: 2 + - uid: 22528 + components: + - type: Transform + pos: 16.5,-26.5 + parent: 2 + - uid: 22529 + components: + - type: Transform + pos: 16.5,-27.5 + parent: 2 + - uid: 22530 + components: + - type: Transform + pos: 16.5,-28.5 + parent: 2 + - uid: 22531 + components: + - type: Transform + pos: 16.5,-29.5 + parent: 2 - proto: Chair entities: - uid: 678 @@ -56452,28 +56496,6 @@ entities: rot: 3.141592653589793 rad pos: 6.5,-24.5 parent: 2 - - uid: 692 - components: - - type: Transform - pos: 11.5,-26.5 - parent: 2 - - uid: 693 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: 11.5,-28.5 - parent: 2 - - uid: 694 - components: - - type: Transform - rot: 3.141592653589793 rad - pos: 17.5,-28.5 - parent: 2 - - uid: 695 - components: - - type: Transform - pos: 17.5,-26.5 - parent: 2 - uid: 696 components: - type: Transform @@ -62611,10 +62633,10 @@ entities: parent: 2 - proto: DefaultStationBeaconAME entities: - - uid: 10573 + - uid: 642 components: - type: Transform - pos: 14.5,-27.5 + pos: 13.5,-25.5 parent: 2 - proto: DefaultStationBeaconAnomalyGenerator entities: @@ -105392,7 +105414,7 @@ entities: - type: Transform pos: 14.5,32.5 parent: 2 -- proto: Oracle +- proto: OracleSpawner entities: - uid: 22459 components: @@ -105879,13 +105901,6 @@ entities: - type: Transform pos: -5.47442,23.570398 parent: 2 -- proto: PaperWrittenAMEScribbles - entities: - - uid: 1534 - components: - - type: Transform - pos: 11.480261,-27.434395 - parent: 2 - proto: ParticleAcceleratorComputerCircuitboard entities: - uid: 5471 @@ -110183,6 +110198,11 @@ entities: parent: 2 - proto: Rack entities: + - uid: 94 + components: + - type: Transform + pos: 15.5,-24.5 + parent: 2 - uid: 1605 components: - type: Transform @@ -110242,6 +110262,11 @@ entities: - type: Transform pos: -40.5,-33.5 parent: 2 + - uid: 2190 + components: + - type: Transform + pos: 17.5,-25.5 + parent: 2 - uid: 3526 components: - type: Transform @@ -110615,6 +110640,11 @@ entities: - type: Transform pos: 3.5,-4.5 parent: 2 + - uid: 22537 + components: + - type: Transform + pos: 17.5,-26.5 + parent: 2 - proto: RadiationCollectorNoTank entities: - uid: 607 @@ -121356,7 +121386,7 @@ entities: - type: Transform pos: -35.5,-42.5 parent: 2 -- proto: SophicScribe +- proto: SophicScribeSpawner entities: - uid: 16214 components: @@ -125063,18 +125093,6 @@ entities: - type: Transform pos: 5.5,-23.5 parent: 2 - - uid: 2189 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 11.5,-27.5 - parent: 2 - - uid: 2190 - components: - - type: Transform - rot: 1.5707963267948966 rad - pos: 17.5,-27.5 - parent: 2 - uid: 2191 components: - type: Transform From 3dc28cfc2c8f8077c55c6f54b1f5d1064f1d2d7b Mon Sep 17 00:00:00 2001 From: SimpleStation Changelogs Date: Tue, 17 Sep 2024 23:39:12 +0000 Subject: [PATCH 06/55] Automatic Changelog Update (#883) --- Resources/Changelog/Changelog.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 6a8111cec5e..43088dac5c4 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -6381,3 +6381,16 @@ Entries: id: 6363 time: '2024-09-16T04:41:44.0000000+00:00' url: https://github.com/Simple-Station/Einstein-Engines/pull/919 +- author: Mnemotechnician + changes: + - type: Add + message: >- + Pulling has been reworked. Your character will now try to continuously + push the pulled entity when you use the push keybind. + - type: Remove + message: >- + You can no longer push the pulled entity while walking, and pushing now + follows the momentum conservation laws. + id: 6364 + time: '2024-09-17T23:38:45.0000000+00:00' + url: https://github.com/Simple-Station/Einstein-Engines/pull/883 From 0079259a5772b56ed0fc07d2717b54f0a4ebaf92 Mon Sep 17 00:00:00 2001 From: SimpleStation Changelogs Date: Tue, 17 Sep 2024 23:39:39 +0000 Subject: [PATCH 07/55] Automatic Changelog Update (#927) --- Resources/Changelog/Changelog.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 43088dac5c4..27f4befdd04 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -6394,3 +6394,13 @@ Entries: id: 6364 time: '2024-09-17T23:38:45.0000000+00:00' url: https://github.com/Simple-Station/Einstein-Engines/pull/883 +- author: VMSolidus + changes: + - type: Tweak + message: >- + Core now has a 6-Core AME, which is supplied with two jars of fuel. This + should give Engineers significantly more than 20 minutes of time to + setup the Supermatter engine. + id: 6365 + time: '2024-09-17T23:39:07.0000000+00:00' + url: https://github.com/Simple-Station/Einstein-Engines/pull/927 From 2f8d9e8e422ff81a4b23d69e46b67207a7cac110 Mon Sep 17 00:00:00 2001 From: Jay Jacobs <84914277+Flybik@users.noreply.github.com> Date: Wed, 18 Sep 2024 02:39:57 +0300 Subject: [PATCH 08/55] Fixes to Bench and Barber Chair Rotation (#925) # Description Changed the textures of wooden and metal benches. Now wooden benches are turned correctly, and metal ones have the correct texture of the left and right halves --- # TODO - [x] Task - [x] Completed Task --- ![Screenshot_80](https://github.com/user-attachments/assets/ff51a698-5387-4b4e-8d2e-4408c2893577) ![Screenshot_81](https://github.com/user-attachments/assets/235f4dbd-c379-45cd-86ea-821a145f0862) ![Screenshot_82](https://github.com/user-attachments/assets/04757bd0-c317-4f3f-91eb-91c1c2d52dec) ![Screenshot_83](https://github.com/user-attachments/assets/dcf006e9-50d6-443f-80b1-c2cce22b1b9a) --- # Changelog :cl: JayJacobs - tweak: Changed the sprite of the barber chair. - fix: Fixed bench textures. Co-authored-by: Flybik --- .../Structures/Specific/bay12barbershop.yml | 2 +- .../Furniture/Benches/pews.rsi/left.png | Bin 1976 -> 1924 bytes .../Furniture/Benches/pews.rsi/right.png | Bin 1999 -> 1897 bytes .../Benches/steel_bench.rsi/left.png | Bin 6061 -> 6022 bytes .../Benches/steel_bench.rsi/right.png | Bin 6022 -> 6061 bytes .../Benches/steel_bench_white.rsi/left.png | Bin 7349 -> 7243 bytes .../Benches/steel_bench_white.rsi/right.png | Bin 7243 -> 7349 bytes .../Specific/barberchair.rsi/barberchair.png | Bin 0 -> 1781 bytes .../Specific/barberchair.rsi/meta.json | 15 +++++++++++++++ .../Specific/barbershop.rsi/barberchair.png | Bin 586 -> 0 bytes .../Specific/barbershop.rsi/meta.json | 3 --- 11 files changed, 16 insertions(+), 4 deletions(-) create mode 100644 Resources/Textures/Structures/Specific/barberchair.rsi/barberchair.png create mode 100644 Resources/Textures/Structures/Specific/barberchair.rsi/meta.json delete mode 100644 Resources/Textures/Structures/Specific/barbershop.rsi/barberchair.png diff --git a/Resources/Prototypes/Entities/Structures/Specific/bay12barbershop.yml b/Resources/Prototypes/Entities/Structures/Specific/bay12barbershop.yml index e66ae347d97..2b0f2a788a6 100644 --- a/Resources/Prototypes/Entities/Structures/Specific/bay12barbershop.yml +++ b/Resources/Prototypes/Entities/Structures/Specific/bay12barbershop.yml @@ -10,7 +10,7 @@ - type: Anchorable - type: Rotatable - type: Sprite - sprite: Structures/Specific/barbershop.rsi + sprite: Structures/Specific/barberchair.rsi state: barberchair - type: entity diff --git a/Resources/Textures/SimpleStation14/Structures/Furniture/Benches/pews.rsi/left.png b/Resources/Textures/SimpleStation14/Structures/Furniture/Benches/pews.rsi/left.png index 9631b4b17147124ef7fa9e4e4edffcf0f163f337..beb8cd4a0b298f142f8dd562dcf8456b21aa563a 100644 GIT binary patch delta 1888 zcmV-m2cP)34}=epIe#`uL_t(|ob8)!ZyU!E$A7bXca#)Mc4AqUl31zh!cg7XM$w`` z-lTwCqv&@l`f(ZMHf#Wx4j>_<}1+Lo&(+&^PHQHxc3?)HxdcF&fBty6BZhRj~(Yu zdhl@V-`RnMtAB!Mai+2F!%m^fwxHkp_n!q^WSp56Zm%mL7-sEJP5>{M7ruJ|XOGrq z0*KDe_@Pg-`Z93+c0jx!0%k^s{FuQ;Y9tnz2+FNThALcKoU_OecYV>#7iCI(xHvu? zv;Hz=Ha4~mUZ%#k57yg~sj-n8&cHC#00)2kC`R0Hy?r`KO z+Yh2wS%0~sbu`@(-48rf?>DFFxH~>%L?2Am6AG+uD39M1XMizs0z|FoYuIpJ`NLw& zo#$}>Sv^PHDyYpEIoGN7;}aFtY>kWQvaF6Ew;F0DppZLBYI^P}U?dt~B`1SX8OEqC zWenPu(EI&?P`;JJkw)!IL|a)s40P^!nT+BD6MqmZ$jS}sFqYAi*EP8$*A9y`>}Q~G z5(3SX*sD}23MB{Ah?c5(f!ta%6xRUJx8FbY&6Tx+w@#E@?`16Mc%SaM*8up<;{1f2tSJF(q* z^MBFtDOT1tx$`8D2HO{Zn;M&`qGp5=!}QzJ3-)e9n_>5U8Mof#eEp^%qt{{u+tC83 zF(sB~Df2Ti&vRw@?po=*Ya2zHFz;fSpKl+A)7nkm2#TZS8oKcCmVKan_s&tzt~Bg@46v6*23=`45iebJG)kGIsciS|8MSlPa6J zG3kXUa^O7|j|sP51@1oG4$G@g_ieblapT5Lg+|2jP9)4Ej@b#%iAKw0{O=%5)4nPR z?$V`8J6{mzIFbmB#FI>Tel;I6isVI-Bm)-&<2df?yAI&`_3H^V3lYKaK{+!GE`Kt- zuisAgyNjI=>2Jd#fXkOJrvOpZ;9>S7q_DcBMZ_i}w^Mx~P17(r*{G}qyG+bK@7@>_(O=TYuZtNOaB(tO2SDB~2CGezPY5bJGb|S5p3TIv#XA zyKwbRHQVM7&=oR}5$uJzDbLk=?S(tv-vkJC$gBXA0T{i1q8qhB@Nwaz|$=s2{?A39nI)}R4! zWT_%1rNoR)iRv)nq)4m3-d4W&A?NXy;>7sH-0bGQv7=aEH(a}R?NE{rYLMo}Y+}qd zgm))|=~#GYGMd^ucB}}va^=e5)y2efZayZN@H7(7kwi*u*I3$sqJNv*P_xQu(_n3% z}}BxB~_-eBp?q)77RlxC*%ys@{W{qBz%5PckZfMsn;Gm0xmAjnp(um44f8$ zmNP4D#@ctSMOXUE3{E)ZsTSv#wQb|({k2jq$BKXpr{-)T5#s1=CCY9PvRs)q={bY-hQFiIW}smUrwz1mc^fPm5PuN)vK0RYO&APWH;G a0RIK1*Zs(-9p(H00000wg1Z6c@|B}xJXQZNK)6u{WN+uP3U*=HNuaJKQ< zg-&$g{@J_x+nL|a%#JMp;CT3OMuh-S&;jz!&;`X9&IC@yH(v5v%I0!DfTTuazABkGAE5C{OsX3|}KjsVAI;BW@M^kFS+&MGEw z_6hRUeFZCfE`njh)YzbUxU|u75HtV`2i@Z^V736;VsI5_VsqJp$+3%ydLSSmiE5BE z`+Rk~#gLGABh`;L+jNAr7}Qko1W zN0Q1Ct?h#b;P#I{iEqF8N}hLoKuv@#Z2V%c1M{y8Eq@Tcf;|s4kk}9Z`P3{f&Yz9i z#L6vgS_3dWJIY5i&!784T)p6blUJ*?KnSnI05?j@7`e(>74gpZA z6XxE$SbuQF<$nMQCk!1@ID=AC13d3+FkuM*NRRh}PiH887-G7IyCovS0h84j(z=*sjW*mu-0^rL60zOEb{>@#pW)Lhq^k0w?d!6$k*eGdEBMNFQ z@Vf}qpoQYSA^?7{^vx}~peP$7Y3_nD=$kU1P7J9oblfJ+Gx+}3)w*!otMwn_*@QZOFaqw zP{#@MeVldLm6BBm=!3Cg)gSZoNLG&VAnn1!bO6Rt4WglipQaZ>0G@8_SX1K()sG&a z=jNat*}<24L2A;g!NC7yPr^GbAD4C1Lw}P(nc<_J|Gu=|lFk(X&|Bn(1ES+(Zf=fG zPfs^R)pzdPiO;#0UEiea3-!K!x5CmJ&W`~>?Xt=26mJA$Syo?4Re@XIv#f;Bv_C2d~$^b z-@MB^i$Pto*E#mRY5)*yZ6Ujb)lEyvghyPfi$p|ypcg}WfMj>HAsEqssDB(lul~9A zGB)*QBAY-{;Nmz%1c< zR%ek178wA}dE*RN2MK&JkyI&UZ`X^Wapf97&W@+*(^MnNAg&;hJwc|i^56i8Fspb) zfL?k{!)Gc;aL^j%;`{Z|XMbc32nHcqZUPen0I9UVrObyO5#;Tn2${ex9vQzr2D0TU}7k_ z&zXXa16)qlf^t>5?y*kqW@#A!u^2zsYx!km^Qi*_LYx=?rD^$&kbg1&c$KpG)L%s4 zC)VeV8azoE0Mf54nG;5H>s6s|aM#wN-x>a~i1zq_jBm@Ls`i%aP=SwcME;HcVq8-{>NdfntN9&YH b6ZjYC()tGM^A6S)00000NkvXXu0mjf?02AJ diff --git a/Resources/Textures/SimpleStation14/Structures/Furniture/Benches/pews.rsi/right.png b/Resources/Textures/SimpleStation14/Structures/Furniture/Benches/pews.rsi/right.png index ea0922930feeae3e2d339219736dbea2bb32d6b4..c1f2949a71932dfb13691245550c05ef1949ecc0 100644 GIT binary patch delta 1861 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 XuM(MODQ_T(00000NkvXXu0mjfLV}!8 diff --git a/Resources/Textures/SimpleStation14/Structures/Furniture/Benches/steel_bench.rsi/right.png b/Resources/Textures/SimpleStation14/Structures/Furniture/Benches/steel_bench.rsi/right.png index b6c1ef00fcb980741e21dddf4fdccbfce1c18b58..260c7298f776908f184289f4075028adae1d8e7c 100644 GIT binary patch delta 6045 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 XuM(MODQ_T(00000NkvXXu0mjfLV}!8 delta 5991 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 RixU6<002ovPDHLkV1gY^#RC8U delta 7290 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 RixU6<002ovPDHLkV1gY^#RC8U diff --git a/Resources/Textures/Structures/Specific/barberchair.rsi/barberchair.png b/Resources/Textures/Structures/Specific/barberchair.rsi/barberchair.png new file mode 100644 index 0000000000000000000000000000000000000000..244a875dca497cf2e0f2bbe1483ab527bd7b4c74 GIT binary patch literal 1781 zcmVPx#1ZP1_K>z@;j|==^1pojBg-Jv~RCr$Pn@wmPRTO}4{E4P58ntN_NwZNG(jeG^ zZ)HgZYeia=px~xO5L(og6|!@YUBFEiY9nsiEW}EQw3ZYEjU}76sM)0G)>0QH{v7Y5atxpbiSlrdWhx zG#bis0LDKTG<;y|vyW7b-vhCoPT^F@6x3hB>$7CIrCFNxF$*c!B_J@=Z6 z2)5C7`13sZ)&pJ+5X4~HXhhO%%5s3+-Try(19RqP?ybgsJ6{s14%lK0;a9asU_S=m z10q)fWg-$SlCLO9)edau+s~%u6VfBV7{o8`D69}94Q--W6= zy}x3f3W-K!9H|ZfjG|FNjs>GZk;7XXGQ6cB;)F5~LwKh|8!+M%fm-k!BDza)07J?P z!H3V~;A4Y2#b>Y#gyKekobx>SqT}+cymLO#AAD@>x<7QR%mIKs7XTawf~X^n=gCL@ z@NPV*5nvk;HP(gn21Sh*FyiT;dlTLAevbUwQXaXF@Vt-lkob^%sL-Dh=Az7n+@oML zmQsK+K8F7I?P7vvH;-=L>!shT4FKE2cs?r8U!-gve`0s6^z}Vim&dmEnjC(3IpB?x zqp@9}Ez%Y8d@ER~{Z*H1*M6zX%B>r9$%AF=gIS|0sg5il|`qP;&jIH<Up z$HvC=yUU1xsQ$&pMRoP+RecYxU%#%8dGx0`0Od!I9#zZB%j)})cXc{_a*q`|j0u=1 zg@I$o^>KD~Ru`hE18n^;H#et`F1owB&6&S`uPQJxF=5Uc#xv9v;hJ;-ut+9+&WgGO7TGejl>vcRIkgQ0s{}M|k%F8@e;5(;wr;}!f z(*b6vP^x>l>zI>^Y;B(FQ9{2VKl ziqj5f;@c0YH8w$Qlt%DOO{tG>WM~AL+S*7Y*^}4{Y=lNIS1$mqn%rzRX?DaqK%gG2 zw-KS#e;*PfU?X&cX%XOT8JQ1YF+^s6XhrD>E)RzrC2Z`y1G~<&zPF$Z=JdA4jj~V% zB7j~@rs^07<(?0aC}6h-YksmSD?(abtirVd-f!!3l|^=?_(pGP1hCOzdZjTIa1D;y zJzz>J0};R`ZogJ(nr3Dp9j1Xp}hKA#v=-xzZwyMM?ZYNqP-`*053lILi0uh zR~nQ#dUW+dX=On_QT+b5RYhdPML?nh$SC0iJI+dc(n=!(gq3WNK!A-CjJiZxf&G5;fn5#>cPT67 zru@<_#1R_-0*jKn_Z-yczb>q*-W_I(hQXBvKU_Mek44rBWkFQ*+oY8R{kHDRRlp|i z1_kmJb%4CbBzY6nn|mApyv!C^okjK^c&}+?R>Wj2h3LD{hh8Z|5&d2caIw0&s)hBs zBxuBPN$+Rd;o9tx{1MAn~Xw;JZ~zR7FdSs9z?pXo7-bOzgn%c1JE;g zp7+c|AOOC&TCLy`g`E*vg2`k8!*m)N*50oV+1i?PzYyhff9|&sOLx>885uFhM ziqwP%0r)ru5tZQ%#t@ttK$hhMPQpUKcrCd0i(~rny>`3pd3!PeVCVLFJ#xUqbP9X? z`VbB0bFy-yk?pJ1YQD1|Gz0YI{ow!~UZdC{>;o>9BDS&Hk=5_##|%I<%;n(mxcWhm z`ELb9ag_?wIA#XGn@zFoZnsOyG9Hh~9viS&ECSw#olfWU#c|#RZZ(@t5~Sbnn}flC z`0T(Db=`MCkgyE1*^Jm6$1(o_xflS`j)AfMrBccEQwX%9fJ6qM!BNg8JXetcXamNj Y9|80Cs(CLv=l}o!07*qoM6N<$g6bFx`Tzg` diff --git a/Resources/Textures/Structures/Specific/barbershop.rsi/meta.json b/Resources/Textures/Structures/Specific/barbershop.rsi/meta.json index 1226428bcfb..6d57cc380d5 100644 --- a/Resources/Textures/Structures/Specific/barbershop.rsi/meta.json +++ b/Resources/Textures/Structures/Specific/barbershop.rsi/meta.json @@ -18,9 +18,6 @@ ] ] }, - { - "name": "barberchair" - }, { "name": "dyedispenser" }, From 34d9e0d60cf33823ae425fbb3c5a4c2bedeb7c5e Mon Sep 17 00:00:00 2001 From: zelezniciar1 <39102800+zelezniciar1@users.noreply.github.com> Date: Tue, 17 Sep 2024 23:40:13 +0000 Subject: [PATCH 09/55] Re-Adds Fireaxe Prying (#921) # Description - Makes the fireaxe once again able to perform its primary function. - There is now a way to emergency space a room without using an RCD. - Viva Atmosia. --- # TODO [x] Return Fireaxe Prying ---

Media

--- # Changelog :cl: zelezniciar - tweak: The fireaxe once again can pry subfloors Co-authored-by: SsalamethVersaach --- Resources/Locale/en-US/tools/tool-qualities.ftl | 5 ++++- .../Prototypes/Entities/Objects/Weapons/Melee/fireaxe.yml | 1 + Resources/Prototypes/Tiles/plating.yml | 6 ++++++ Resources/Prototypes/tool_qualities.yml | 7 +++++++ 4 files changed, 18 insertions(+), 1 deletion(-) diff --git a/Resources/Locale/en-US/tools/tool-qualities.ftl b/Resources/Locale/en-US/tools/tool-qualities.ftl index 14e42390a76..2482dca5174 100644 --- a/Resources/Locale/en-US/tools/tool-qualities.ftl +++ b/Resources/Locale/en-US/tools/tool-qualities.ftl @@ -32,4 +32,7 @@ tool-quality-rolling-name = Rolling tool-quality-rolling-tool-name = Rolling Pin tool-quality-digging-name = Digging -tool-quality-digging-tool-name = Shovel \ No newline at end of file +tool-quality-digging-tool-name = Shovel + +tool-quality-axing-name = Axing +tool-quality-axing-tool-name = Fireaxe \ No newline at end of file diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Melee/fireaxe.yml b/Resources/Prototypes/Entities/Objects/Weapons/Melee/fireaxe.yml index b30a2855796..6630a22ea7d 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Melee/fireaxe.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Melee/fireaxe.yml @@ -43,6 +43,7 @@ - type: Tool qualities: - Prying + - Axing - type: ToolTileCompatible - type: Prying - type: UseDelay diff --git a/Resources/Prototypes/Tiles/plating.yml b/Resources/Prototypes/Tiles/plating.yml index 7edb1ae784c..0a74ef9fd43 100644 --- a/Resources/Prototypes/Tiles/plating.yml +++ b/Resources/Prototypes/Tiles/plating.yml @@ -4,6 +4,7 @@ sprite: /Textures/Tiles/plating.png baseTurf: Lattice isSubfloor: true + deconstructTools: [ Axing ] footstepSounds: collection: FootstepPlating friction: 0.3 @@ -20,6 +21,7 @@ - 1.0 baseTurf: Lattice isSubfloor: true + deconstructTools: [ Axing ] footstepSounds: collection: FootstepPlating friction: 0.3 @@ -31,6 +33,7 @@ sprite: /Textures/Tiles/plating_burnt.png baseTurf: Lattice isSubfloor: true + deconstructTools: [ Axing ] footstepSounds: collection: FootstepPlating friction: 0.3 @@ -42,6 +45,7 @@ sprite: /Textures/Tiles/Asteroid/asteroid_plating.png baseTurf: Lattice isSubfloor: true + deconstructTools: [ Axing ] footstepSounds: collection: FootstepPlating friction: 0.3 @@ -53,6 +57,7 @@ sprite: /Textures/Tiles/Misc/clockwork/clockwork_floor.png baseTurf: Lattice isSubfloor: true + deconstructTools: [ Axing ] footstepSounds: collection: FootstepPlating friction: 0.3 @@ -64,6 +69,7 @@ sprite: /Textures/Tiles/snow_plating.png #Not in the snow planet RSI because it doesn't have any metadata. Should probably be moved to its own folder later. baseTurf: Lattice isSubfloor: true + deconstructTools: [ Axing ] footstepSounds: collection: FootstepPlating friction: 0.15 #a little less then actual snow diff --git a/Resources/Prototypes/tool_qualities.yml b/Resources/Prototypes/tool_qualities.yml index ff55d9fcf14..4508ea94b71 100644 --- a/Resources/Prototypes/tool_qualities.yml +++ b/Resources/Prototypes/tool_qualities.yml @@ -67,3 +67,10 @@ toolName: tool-quality-rolling-tool-name spawn: RollingPin icon: { sprite: Objects/Tools/rolling_pin.rsi, state: icon } + +- type: tool + id: Axing + name: tool-quality-axing-name + toolName: tool-quality-axing-tool-name + spawn: FireAxe + icon: { sprite: Objects/Weapons/Melee/fireaxe.rsi, state: icon } From ff618e9a6444d1224381ae2d854c359f3d53b6e1 Mon Sep 17 00:00:00 2001 From: SimpleStation Changelogs Date: Tue, 17 Sep 2024 23:40:23 +0000 Subject: [PATCH 10/55] Automatic Changelog Update (#925) --- Resources/Changelog/Changelog.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 27f4befdd04..66a257b98b2 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -6404,3 +6404,12 @@ Entries: id: 6365 time: '2024-09-17T23:39:07.0000000+00:00' url: https://github.com/Simple-Station/Einstein-Engines/pull/927 +- author: JayJacobs + changes: + - type: Tweak + message: Changed the sprite of the barber chair. + - type: Fix + message: Fixed bench textures. + id: 6366 + time: '2024-09-17T23:39:57.0000000+00:00' + url: https://github.com/Simple-Station/Einstein-Engines/pull/925 From f5275e295deb97ee4e5e07f547b869ae19361bd5 Mon Sep 17 00:00:00 2001 From: VMSolidus Date: Tue, 17 Sep 2024 19:40:34 -0400 Subject: [PATCH 11/55] 9 More Traits (#918) # Description This "ZERO C#" Pr adds 10 additional traits to the game, which only make use of pre-existing components, along with some of the new TraitSystem functionality. These are the following new traits: - **Photophobia**: Functions as per Vulpkanin Light Sensitivity - **Clumsy**: Functions as per Clown clumsiness - **Small**: Functions as per Felinid PseudoItem, requires a character be Felinid sized - **Temperature Tolerance**: Functions as per Vulpkanin low temperature resistance - **Spinarette**: Functions as per the same ability from Arachnids. - **Talons**: Replaces your melee natural attacks with Piercing talons - **Claws**: Replaces your melee natural attacks with Slashing claws - **Striking Calluses**: [Human only, requires Martial Artist or Boxer job], increases your base unarmed attack to 6 damage, from 5. - **Natural Weapon Removal**: For species with melee attacks other than blunt, replaces Talons or Claws with Fist. ## TODO: - [ ] Adjust the points costs/additions for these traits to ensure balance. Although I can't imagine most of them being changed. I do still wish for a decent number of traits to be within the 1 to 3 point range.

Media

![image](https://github.com/user-attachments/assets/c2e7a42b-2b1f-43ea-8f0d-5275c06c51ae) ![image](https://github.com/user-attachments/assets/bb60ffbe-0755-483b-b3a4-028764b465a7)

# Changelog :cl: - add: 10 new Traits have been added to the game. Photophobia, Clumsy, Small, Temperature Tolerance, Claws, Talons, Natural Weapon Removal, Striking Calluses, and Spinarette. --------- Signed-off-by: VMSolidus Co-authored-by: DEATHB4DEFEAT <77995199+DEATHB4DEFEAT@users.noreply.github.com> --- Resources/Locale/en-US/traits/traits.ftl | 64 ++++++- Resources/Prototypes/Traits/disabilities.yml | 38 +++++ Resources/Prototypes/Traits/physical.yml | 170 +++++++++++++++++++ 3 files changed, 271 insertions(+), 1 deletion(-) diff --git a/Resources/Locale/en-US/traits/traits.ftl b/Resources/Locale/en-US/traits/traits.ftl index ff329a12a71..b034f69b9e8 100644 --- a/Resources/Locale/en-US/traits/traits.ftl +++ b/Resources/Locale/en-US/traits/traits.ftl @@ -228,6 +228,68 @@ trait-description-AnomalousPositronics = your positronic brain lacks its standard psionic insulation. As a being that can be argued to have a soul, this by extension means that it is possible for you to be influenced by the Noosphere. +trait-name-Photophobia = Photophobia +trait-description-Photophobia = + Your eyes are extremely sensitive to bright lights. + As a result, you may be blinded for a greater duration than others when exposed to sudden flashes of light. + Your eyes are also more likely to be injured by flashes. + +trait-name-Clumsy = Clumsy +trait-description-Clumsy = + You have a severe deficiency in hand-eye-coordination, resulting in an inability to do some things that others would take for granted. + Any weapons you may try to use are more likely to injure yourself than others. You are unable to climb any objects without injuring yourself. + +trait-name-Small = Small +trait-description-Small = + You are much smaller than a typical person, and can climb into spaces others would not normally be able to fit into, such as duffel bags. + This trait does not in any way modify your character's size, it merely requires that your character be at most the size of a standard Felinid. + +trait-name-TemperatureTolerance = Temperature Tolerance +trait-description-TemperatureTolerance = + You have a notable tolerance for lower temperatures. You can stand for extended periods of time + in conditions just slightly below freezing, such as the inside of a kitchen fridge, + or the sunlit mountainside of the famous Glacier station. + +trait-name-Talons = Talons +trait-description-Talons = + Your fingertips have been replaced with piercing talons. + These could come from gene modifications, vatgrown implants, + or even hard plastic retractable talons incorpoated into a prosthetic limb. + Your unarmed melee attacks deal Piercing damage instead of the standard damage type for your species. + This has no effect on damage dealt with any form of armed melee. + +trait-name-Claws = Claws +trait-description-Claws = + Your fingertips have been replaced with sharp claws. + These could come from gene modifications, vatgrown implants, + or even hard plastic retractable claws incorpoated into a prosthetic limb. + Your unarmed melee attacks deal Slashing damage instead of the standard damage type for your species. + This has no effect on damage dealt with any form of armed melee. + +trait-name-NaturalWeaponRemoval = Natural Weapons Removal +trait-description-NaturalWeaponRemoval = + Whatever "Natural Weapons" your species are normally born with have been surgically removed. + This could have been done to better fit in with terran space stations, or as a cosmetic choice. + As a result, your unarmed attacks deal Blunt damage instead of the standard damage type for your species. + This has no effect on damage dealt with any form of armed melee. + +trait-name-StrikingCalluses = Striking Calluses +trait-description-StrikingCalluses = + An iconic enhancement commonly found in the world of cyberenhanced martial arts. + Striking Calluses consist of bony dermal deposits grafted into a user's hands, either inside the palm + for "Tiger Style" fighting, or just below the knuckles for those who favor traditional boxing. + Owners of prosthetic or bionic limbs would instead have a hard plastic shell over their knuckles. + These enhancements increase your unarmed strike damage by 1 point base, but do not confer + any benefits to any form of armed melee. + +trait-name-Spinarette = Bionic Spinarette +trait-description-Spinarette = + This vatgrown organ-- trademarked and patented by the Cybersun Corporation, is marketed as a highly + utilitarian enhancement, and sold in clinics all across known space. It consists of a nodule that is traditionally + implanted right below the wrist, which absorbs bodily lipids to convert into all natural silk. A small opening + in the palm allows the user to 'spin' this thread. Users of this enhancement typically require twice as much food + as a standard Sol Human, owing to the high metabolic cost of artificial Sericulture. + trait-name-AddictionNicotine = Nicotine Addiction trait-description-AddictionNicotine = - You have an addiction to Nicotine, and will require frequent smoke breaks to keep your mood in check. \ No newline at end of file + You have an addiction to Nicotine, and will require frequent smoke breaks to keep your mood in check. diff --git a/Resources/Prototypes/Traits/disabilities.yml b/Resources/Prototypes/Traits/disabilities.yml index c24e8927693..3afc3bfba43 100644 --- a/Resources/Prototypes/Traits/disabilities.yml +++ b/Resources/Prototypes/Traits/disabilities.yml @@ -191,3 +191,41 @@ damageModifiers: coefficients: Blunt: 1.1 + +- type: trait + id: Photophobia + category: Visual + points: 1 + requirements: + - !type:CharacterSpeciesRequirement + inverted: true + species: + - Vulpkanin # This trait functions exactly as-is for the Vulpkanin trait. + components: + - type: Flashable + eyeDamageChance: 0.3 + eyeDamage: 1 + durationMultiplier: 1.5 + +- type: trait + id: Clumsy + category: Physical + points: 1 + requirements: + - !type:CharacterJobRequirement + inverted: true + jobs: + - Clown # This trait functions exactly as is for the Clown's trait. + - !type:CharacterDepartmentRequirement + inverted: true + departments: + - Command # Because I know for a fact people will play Captain and grief with their inability to fight back. + - Security # Because I know for a fact people will play Security and grief with their inability to use guns. + components: + - type: Clumsy + clumsyDamage: + types: + Blunt: 5 + Piercing: 4 + groups: + Burn: 3 diff --git a/Resources/Prototypes/Traits/physical.yml b/Resources/Prototypes/Traits/physical.yml index cfc15d4ee25..8debf7ffbfb 100644 --- a/Resources/Prototypes/Traits/physical.yml +++ b/Resources/Prototypes/Traits/physical.yml @@ -231,3 +231,173 @@ Blunt: 1.5 Slash: 1.5 Piercing: 1.5 + +- type: trait + id: Small + category: Physical + points: -2 + requirements: + - !type:CharacterSpeciesRequirement + inverted: true + species: + - Felinid # Felinids already have this feature by default. + - !type:CharacterHeightRequirement + max: 150 + - !type:CharacterWidthRequirement + max: 32 + components: + - type: PseudoItem + storedOffset: 0,17 + shape: + - 0,0,1,4 + - 0,2,3,4 + - 4,0,5,4 + +- type: trait + id: TemperatureTolerance + category: Physical + points: -1 + requirements: + - !type:CharacterSpeciesRequirement + inverted: true + species: + - Vulpkanin # This trait functions exactly as-is for the Vulpkanin trait. + components: + - type: TemperatureProtection + coefficient: 0.1 # Enough resistance to walk into the chef's freezer, or tolerate daytime temperatures on Glacier without a jacket. + +# These traits largely exist to demonstrate more of the "Component Removals" functionality. This way contributors +# can get used to seeing that they can "Remove and Replace" a pre-existing component. +# When declared, componentRemovals work like a "RemComp" that activates upon joining a round. +- type: trait + id: Talons + category: Physical + points: -1 + requirements: + - !type:CharacterSpeciesRequirement + inverted: true + species: + - Harpy # Harpies already have talons + - Arachnid # Apparently they have a "piercing" bite + - !type:CharacterTraitRequirement + inverted: true + traits: + - Claws + componentRemovals: + - MeleeWeapon # Remove the original innate melee attack, so that it can be replaced with a new one. + components: + - type: MeleeWeapon + soundHit: + collection: AlienClaw + animation: WeaponArcClaw + damage: + types: + Piercing: 5 # No, this isn't "OP", this is literally the worst brute damage type in the game. + # Same deal as Slash, except that a majority of all armor provides Piercing resistance. + +- type: trait + id: Claws + category: Physical + points: -1 + requirements: + - !type:CharacterSpeciesRequirement + inverted: true + species: + - Felinid # Felinids already have cat claws. + - Reptilian # Reptilians also have cat claws. + # - Vulpkanin # Vulpkanin have "Blunt" claws. One could argue this trait "Sharpens" their claws. + - !type:CharacterTraitRequirement + inverted: true + traits: + - Talons + componentRemovals: + - MeleeWeapon # Remove the original innate melee attack, so that it can be replaced with a new one. + components: + - type: MeleeWeapon + soundHit: + collection: AlienClaw + angle: 30 + animation: WeaponArcClaw + damage: + types: + Slash: 5 # Trade stamina damage on hit for a very minor amount of extra bleed. + # Blunt also deals bleed damage, so this is more of a sidegrade. + +- type: trait + id: NaturalWeaponRemoval + category: Physical + points: 0 + requirements: + - !type:CharacterSpeciesRequirement + inverted: true + species: + - Human + - Oni + - SlimePerson + - !type:CharacterTraitRequirement + inverted: true + traits: + - Talons + - Claws + componentRemovals: + - MeleeWeapon # Remove the original innate melee attack, so that it can be replaced with a new one. + components: + - type: MeleeWeapon + soundHit: + collection: Punch + angle: 30 + animation: WeaponArcFist + damage: + types: + Blunt: 5 + +- type: trait + id: StrikingCalluses + category: Physical + points: -4 + requirements: + - !type:CharacterSpeciesRequirement + species: + - Human # Entirely arbitrary, I've decided I want a trait unique to humans. Since they don't normally get anything exciting. + # When we get the Character Records system in, I also want to make this require certain Backgrounds. + - !type:CharacterTraitRequirement + inverted: true + traits: + - Claws + - Talons + - !type:CharacterLogicOrRequirement + requirements: + - !type:CharacterTraitRequirement + traits: + - MartialArtist + - !type:CharacterJobRequirement + jobs: + - Boxer + componentRemovals: + - MeleeWeapon + components: + - type: MeleeWeapon + soundHit: + collection: Punch + angle: 30 + animation: WeaponArcFist + damage: + types: + Blunt: 6 + +- type: trait + id: Spinarette + category: Physical + points: -4 + requirements: + - !type:CharacterSpeciesRequirement + inverted: true + species: + - Arachnid + - Arachne + components: + - type: Sericulture + action: ActionSericulture + productionLength: 2 + entityProduced: MaterialWebSilk1 + hungerCost: 4 From 3dd57ca04aa3b8efeca47819ad2f03be488e153d Mon Sep 17 00:00:00 2001 From: VMSolidus Date: Tue, 17 Sep 2024 19:41:14 -0400 Subject: [PATCH 12/55] Open Market Tacsuits (#845) # Description I've spoken with this subject with quite a few people so far, that there's some inherent issues with the game's setting as regards to corporations, and people readily identifying certain companies as being "Syndicate Companies". It makes absolutely no sense for things like that to be completely public knowledge that X-Company is explicitly antagonistic, when publicly they would just be companies that sell products to an open market-- an open market that NanoTrasen buys from. And while we've been doing this lately with adding non-NT company names to the names and descriptions of hardsuits, one thing has been bugging me and other people for awhile. "If the Cybersun tacsuits are meant to be something that the Nukies literally bought on the open market and painted red. Why can't the station also purchase these suits from the same market? If a manufacturer of these suits notably declined to sell its products to another company, it would be an extremely obvious tell that they were overtly hostile to that corporation. It makes more sense if they openly sell said products at an inflated cost, so that they can put up public appearances of neutrality". This also brings to mind that the Reverse Engineering Machine, in it's current rendition, is extremely problematic for establishing the game as a serious setting, given that it currently functions as what is essentially "Press button to commit industrial espionage". Having talked with its original creator, Rane also agreed that- especially in the context of the "Blukie Suits", this presents an issue for the game's setting presentation. It's a plot hole that makes very little sense. So, what this PR does, is make it so that station Logistics can purchase crates containing CSA branded Tacsuits, painted in the blue, at a massively overinflated cost. These crates come with Security locks, meaning that they would be bought for use solely by the station's security. Additionally, this removes the ability for the Reverse Engineering machine to violate the intellectual property of other corporations, which in-universe would be something highly illegal under a truly neutral space law. I actually also believe that other hardsuits and tacsuits, including ones purchased from Five-Points-Armory, Hephaeustus, etc, should all be purchasable from cargo. Technically, any weapon, hardsuit, or armor, should also be purchasable, but making it so that cargo can order a Sec-Crate with a C-20r in it would be extremely controversial, and is best left to a separate PR. # Changelog :cl: - add: CSA-51a "Shanlin" and CSA-80UA "Guan-Yu" Tacsuits can now be purchased from Logistics for use by station security personnel, at an extremely overinflated price. These suits come prepainted in blue. - remove: Reverse Engineering machines can no longer violate the intellectual property rights of publicly traded corporations. This means that Cybersun Tacsuits; regardless of where they were obtained from or what color they were painted, can no longer be reverse engineered. --------- Signed-off-by: VMSolidus Co-authored-by: DEATHB4DEFEAT <77995199+DEATHB4DEFEAT@users.noreply.github.com> --- .../clothing/head/hardsuit-helmets.ftl | 4 ++++ .../clothing/outerClothing/hardsuits.ftl | 6 ++++++ Resources/Maps/glacier.yml | 2 -- .../Catalog/Cargo/cargo_security.yml | 20 ++++++++++++++++++ .../Catalog/Fills/Crates/security.yml | 20 ++++++++++++++++++ .../Clothing/OuterClothing/hardsuits.yml | 10 --------- .../Entities/Structures/Machines/lathe.yml | 2 -- .../Clothing/OuterClothing/hardsuits.yml | 4 ---- .../Nyanotrasen/Recipes/Lathes/hardsuits.yml | 21 ------------------- 9 files changed, 50 insertions(+), 39 deletions(-) delete mode 100644 Resources/Prototypes/Nyanotrasen/Recipes/Lathes/hardsuits.yml diff --git a/Resources/Locale/en-US/prototypes/entities/clothing/head/hardsuit-helmets.ftl b/Resources/Locale/en-US/prototypes/entities/clothing/head/hardsuit-helmets.ftl index 2bbacf2abca..4e003d2704d 100644 --- a/Resources/Locale/en-US/prototypes/entities/clothing/head/hardsuit-helmets.ftl +++ b/Resources/Locale/en-US/prototypes/entities/clothing/head/hardsuit-helmets.ftl @@ -55,6 +55,8 @@ ent-ClothingHeadHelmetHardsuitLuxury = HpI-20c helmet .desc = A modified helmet for the Minos hardsuit, fashioned after the Logistics Officer's colors. It's been modified for greater mobility at the expense of physical trauma protection. ent-ClothingHeadHelmetHardsuitSyndie = CSA-51a helmet .desc = An armored helmet deployed over a Shanlin tacsuit. This one has been painted blood red. +ent-ClothingHeadHelmetHardsuitSyndieReverseEngineered = CSA-51a helmet + .desc = An armored helmet deployed over a Shanlin tacsuit. This one has been painted blue. ent-ClothingHeadHelmetHardsuitSyndieMedic = CSA-51m helmet .desc = An armored helmet deployed over a Zhongyao tacsuit. features optic integrations for nearly every medical hud on the market. Designed to enable the survival of combat medics in the most dangerous of environments. @@ -64,6 +66,8 @@ ent-ClothingHeadHelmetHardsuitSyndieCommander = CSA-54c helmet .desc = A bulked up version of the Shanlin tacsuit's helmet, purpose-built for commanders of special operation squads. This one has been painted blood-red. ent-ClothingHeadHelmetHardsuitCybersun = CSA-80UA helmet .desc = An incredibly sturdy looking helmet designed for the Guan Yu tacsuit. +ent-ClothingHeadHelmetHardsuitJuggernautReverseEngineered = CSA-80UA helmet + .desc = An incredibly sturdy looking helmet designed for the Guan Yu tacsuit. This one has been painted blue. ent-ClothingHeadHelmetHardsuitWizard = WZD-84 helmet .desc = A bizarre, gem-encrusted helmet from unknown origins. It provides some protection to its wearer without restricting their movements. ent-ClothingHeadHelmetHardsuitLing = organic space helmet diff --git a/Resources/Locale/en-US/prototypes/entities/clothing/outerClothing/hardsuits.ftl b/Resources/Locale/en-US/prototypes/entities/clothing/outerClothing/hardsuits.ftl index 84f03af5c92..188f08ae260 100644 --- a/Resources/Locale/en-US/prototypes/entities/clothing/outerClothing/hardsuits.ftl +++ b/Resources/Locale/en-US/prototypes/entities/clothing/outerClothing/hardsuits.ftl @@ -74,6 +74,9 @@ ent-ClothingOuterHardsuitLuxury = HpI-20c - "Minos" hardsuit ent-ClothingOuterHardsuitSyndie = CSA-51a - "Shanlin" tacsuit .desc = A tactical combat hardsuit produced by the Cybersun-Armaments Corporation, the suit's tags indicate it provides moderate protection against most forms of damage. This one has been painted blood red. It feels incredibly light. +ent-ClothingOuterHardsuitSyndieReverseEngineered = CSA-51a - "Shanlin" tacsuit + .desc = A tactical combat hardsuit produced by the Cybersun-Armaments Corporation, the suit's tags indicate it provides moderate protection against most forms of damage. + This one has been painted blue. It feels incredibly light. ent-ClothingOuterHardsuitSyndieMedic = CSA-51m - "Zhongyao" tacsuit .desc = A tactical combat hardsuit produced by the Cybersun-Armaments Corporation, the suit's tags indicate it provides moderate protection against most forms of damage. Half of the suit is painted blood red, the rest bears galactic-standard medical markings. It feels incredibly light. @@ -87,6 +90,9 @@ ent-ClothingOuterHardsuitSyndieCommander = CSA-54c - "Tianming" tacsuit ent-ClothingOuterHardsuitJuggernaut = CSA-80UA - "Guan Yu" tacsuit .desc = The pride and joy of the Cybersun-Armaments Corporation, named after an ancient Sol' War God. Commonly known throughout the galaxy as a "Juggernaut". Matching its bulky appearance, it protects against all forms of damage. It feels VERY heavy. +end-ClothingOuterHardsuitJuggernautReverseEngineered = CSA-80UA - "Guan Yu" tacsuit + .desc = The pride and joy of the Cybersun-Armaments Corporation, named after an ancient Sol' War God. Commonly known throughout the galaxy as a "Juggernaut". + Matching its bulky appearance, it protects against all forms of damage. It feels VERY heavy. ent-ClothingOuterHardsuitWizard = WZD-84 - "Mana" tacsuit .desc = A bizarre gem-encrusted hardsuit. Famously used by members of the Wizard Federation in their operations. Contrary to it's appearance, it can protect its wearer from space and considerable amounts of physical trauma, it feels somewhat light. diff --git a/Resources/Maps/glacier.yml b/Resources/Maps/glacier.yml index 2cba2930f0e..5891d0ca9b6 100644 --- a/Resources/Maps/glacier.yml +++ b/Resources/Maps/glacier.yml @@ -52876,8 +52876,6 @@ entities: - HamtrLLeg - HamtrRLeg - VimHarness - - ClothingOuterHardsuitJuggernautReverseEngineered - - ClothingOuterHardsuitSyndieReverseEngineered - JetpackBlue - JetpackMini - proto: ExtinguisherCabinetFilled diff --git a/Resources/Prototypes/Catalog/Cargo/cargo_security.yml b/Resources/Prototypes/Catalog/Cargo/cargo_security.yml index 2ad7628ddb3..daa5e95777b 100644 --- a/Resources/Prototypes/Catalog/Cargo/cargo_security.yml +++ b/Resources/Prototypes/Catalog/Cargo/cargo_security.yml @@ -77,3 +77,23 @@ cost: 1000 category: cargoproduct-category-name-security group: market + +- type: cargoProduct + id: SecurityShanlinTacsuit + icon: + sprite: Nyanotrasen/Clothing/OuterClothing/ReverseEngineering/syndicate.rsi + state: icon + product: CrateSecurityShanlinTacsuit + cost: 17500 + category: cargoproduct-category-name-security + group: market + +- type: cargoProduct + id: SecurityGuanYuTacsuit + icon: + sprite: Nyanotrasen/Clothing/OuterClothing/ReverseEngineering/juggernaut.rsi + state: icon + product: CrateSecurityGuanYuTacsuit + cost: 30000 + category: cargoproduct-category-name-security + group: market diff --git a/Resources/Prototypes/Catalog/Fills/Crates/security.yml b/Resources/Prototypes/Catalog/Fills/Crates/security.yml index 592c1535083..b1ef5eb0d1c 100644 --- a/Resources/Prototypes/Catalog/Fills/Crates/security.yml +++ b/Resources/Prototypes/Catalog/Fills/Crates/security.yml @@ -113,4 +113,24 @@ - id: TrackingImplanter amount: 4 +- type: entity + id: CrateSecurityShanlinTacsuit + parent: CrateSecgear + name: shanlin tacsuit crate + description: Contains a single CSA-51a Shanlin tacsuit. Requires Security access to open. + components: + - type: StorageFill + contents: + - id: ClothingOuterHardsuitSyndieReverseEngineered + +- type: entity + id: CrateSecurityGuanYuTacsuit + parent: CrateSecgear + name: shanlin tacsuit crate + description: Contains a single CSA-80UA Guan-Yu tacsuit. Requires Security access to open. + components: + - type: StorageFill + contents: + - id: ClothingOuterHardsuitJuggernautReverseEngineered + # Cosmetic Crates diff --git a/Resources/Prototypes/Entities/Clothing/OuterClothing/hardsuits.yml b/Resources/Prototypes/Entities/Clothing/OuterClothing/hardsuits.yml index ba889285a16..6a0f445fc94 100644 --- a/Resources/Prototypes/Entities/Clothing/OuterClothing/hardsuits.yml +++ b/Resources/Prototypes/Entities/Clothing/OuterClothing/hardsuits.yml @@ -61,11 +61,6 @@ - type: HeldSpeedModifier - type: ToggleableClothing clothingPrototype: ClothingHeadHelmetHardsuitAtmos - - type: ReverseEngineering # Nyano - difficulty: 5 - newItem: ClothingOuterHardsuitJuggernautReverseEngineered - recipes: - - ClothingOuterHardsuitJuggernautReverseEngineered #Engineering Hardsuit - type: entity @@ -557,11 +552,6 @@ - type: HeldSpeedModifier - type: ToggleableClothing clothingPrototype: ClothingHeadHelmetHardsuitSyndie - - type: ReverseEngineering # Nyano - difficulty: 5 - newItem: ClothingOuterHardsuitSyndieReverseEngineered - recipes: - - ClothingOuterHardsuitSyndieReverseEngineered - type: Tag tags: - MonkeyWearable diff --git a/Resources/Prototypes/Entities/Structures/Machines/lathe.yml b/Resources/Prototypes/Entities/Structures/Machines/lathe.yml index 2d4bc9e0d8f..e5a9cb35417 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/lathe.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/lathe.yml @@ -628,8 +628,6 @@ - HamtrRLeg - VimHarness # Begin Nyano additions - - ClothingOuterHardsuitJuggernautReverseEngineered - - ClothingOuterHardsuitSyndieReverseEngineered - JetpackBlue - JetpackMini # End Nyano additions diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Clothing/OuterClothing/hardsuits.yml b/Resources/Prototypes/Nyanotrasen/Entities/Clothing/OuterClothing/hardsuits.yml index 6498eda075e..4e988c27406 100644 --- a/Resources/Prototypes/Nyanotrasen/Entities/Clothing/OuterClothing/hardsuits.yml +++ b/Resources/Prototypes/Nyanotrasen/Entities/Clothing/OuterClothing/hardsuits.yml @@ -19,8 +19,6 @@ sprite: Nyanotrasen/Clothing/OuterClothing/ReverseEngineering/syndicate.rsi - type: ToggleableClothing clothingPrototype: ClothingHeadHelmetHardsuitSyndieReverseEngineered - - type: ReverseEngineering - newItem: null - type: entity parent: ClothingOuterHardsuitJuggernaut @@ -35,8 +33,6 @@ sprite: Nyanotrasen/Clothing/OuterClothing/ReverseEngineering/juggernaut.rsi - type: ToggleableClothing clothingPrototype: ClothingHeadHelmetHardsuitJuggernautReverseEngineered - - type: ReverseEngineering - newItem: null - type: entity parent: ClothingOuterHardsuitERTLeader diff --git a/Resources/Prototypes/Nyanotrasen/Recipes/Lathes/hardsuits.yml b/Resources/Prototypes/Nyanotrasen/Recipes/Lathes/hardsuits.yml deleted file mode 100644 index 560c03db49c..00000000000 --- a/Resources/Prototypes/Nyanotrasen/Recipes/Lathes/hardsuits.yml +++ /dev/null @@ -1,21 +0,0 @@ -- type: latheRecipe - id: ClothingOuterHardsuitSyndieReverseEngineered - result: ClothingOuterHardsuitSyndieReverseEngineered - completetime: 25 - materials: - Steel: 6000 - Glass: 1500 - Uranium: 100 - Plastic: 100 - Gold: 200 - -- type: latheRecipe - id: ClothingOuterHardsuitJuggernautReverseEngineered - result: ClothingOuterHardsuitJuggernautReverseEngineered - completetime: 25 - materials: - Steel: 12000 - Glass: 3000 - Uranium: 500 - Plastic: 500 - Gold: 250 From 9e293ea531cac81779457e90a6841784c2c96cc6 Mon Sep 17 00:00:00 2001 From: VMSolidus Date: Tue, 17 Sep 2024 19:41:27 -0400 Subject: [PATCH 13/55] Psionic Power Rolling Rework (#830) # Description This PR implements a long overdue rework of the underlying system for actually giving Psions their powers, by making it a semi-deterministic system that favors people having a small handful of powers, unless said person has inherent modifiers that let them generate them more consistently. A common complaint I received from the new Psionic System is that stations tended to become "Space Hogwarts" given enough time, since people could very easily game the system to consistently get new powers every 20 minutes, until a majority of the station was throwing fireballs at each other. Now, obtaining new powers gets more difficult the more powers you have, exponentially so. Powers can also declare in their prototype how many "Slots" they occupy, meaning that certain powers can make this even more difficult, or not at all, as desired. # Changelog :cl: VMSolidus & Rane - add: Psionic Power "Rerolls" have been reworked. Psions now have a stat called Potentia, which is increased by sources of Psionic Rerolls such as Space Drugs, Loto Oil, and Random Events. Instead of being an all-or-nothing "D100 with modifiers" roll, whatever you roll is added to your Potentia stat, and when Potentia reaches a certain threshold, it is automatically "Spent" to buy a random new power. Your first power costs 100 Potentia, your 2nd power costs 200, 3rd costs 400, and so on. The more powers you have, the more difficult it is to obtain new ones. - tweak: Some powers, such as Telepathy and Xenoglossy, do not count as a power for the purpose of Potentia Cost --------- Signed-off-by: VMSolidus --- .../Psionics/PsionicAbilitiesSystem.cs | 24 ++ Content.Server/Psionics/PsionicsSystem.cs | 325 ++++++++++++------ .../StationEvents/Events/NoosphericZapRule.cs | 4 +- Content.Shared/Psionics/PsionicComponent.cs | 28 +- .../Psionics/PsionicPowerPrototype.cs | 6 + .../Locale/en-US/psionics/psionic-powers.ftl | 4 +- .../Roles/Jobs/Epistemics/forensicmantis.yml | 1 + Resources/Prototypes/Psionics/psionics.yml | 3 + .../Roles/Jobs/Science/research_director.yml | 1 + 9 files changed, 278 insertions(+), 118 deletions(-) diff --git a/Content.Server/Abilities/Psionics/PsionicAbilitiesSystem.cs b/Content.Server/Abilities/Psionics/PsionicAbilitiesSystem.cs index 32e51d3c101..536235b6d63 100644 --- a/Content.Server/Abilities/Psionics/PsionicAbilitiesSystem.cs +++ b/Content.Server/Abilities/Psionics/PsionicAbilitiesSystem.cs @@ -12,6 +12,7 @@ using System.Linq; using Robust.Server.Player; using Content.Server.Chat.Managers; +using Content.Server.Psionics.Glimmer; namespace Content.Server.Abilities.Psionics { @@ -122,6 +123,8 @@ public void InitializePsionicPower(EntityUid uid, PsionicPowerPrototype proto, P AddPsionicStatSources(proto, psionic); RefreshPsionicModifiers(uid, psionic); SendFeedbackMessage(uid, proto, playFeedback); + UpdatePowerSlots(psionic); + //UpdatePsionicDanger(uid, psionic); // TODO: After Glimmer Refactor //SendFeedbackAudio(uid, proto, playPopup); // TODO: This one is coming next! } @@ -297,6 +300,27 @@ private void SendFeedbackMessage(EntityUid uid, PsionicPowerPrototype proto, boo session.Channel); } + private void UpdatePowerSlots(PsionicComponent psionic) + { + var slotsUsed = 0; + foreach (var power in psionic.ActivePowers) + slotsUsed += power.PowerSlotCost; + + psionic.PowerSlotsTaken = slotsUsed; + } + + /// + /// Psions over a certain power threshold become a glimmer source. This cannot be fully implemented until after I rework Glimmer + /// + //private void UpdatePsionicDanger(EntityUid uid, PsionicComponent psionic) + //{ + // if (psionic.PowerSlotsTaken <= psionic.PowerSlots) + // return; + // + // EnsureComp(uid, out var glimmerSource); + // glimmerSource.SecondsPerGlimmer = 10 / (psionic.PowerSlotsTaken - psionic.PowerSlots); + //} + /// /// Remove all Psychic Actions listed in an entity's Psionic Component. Unfortunately, removing actions associated with a specific Power Prototype is not supported. /// diff --git a/Content.Server/Psionics/PsionicsSystem.cs b/Content.Server/Psionics/PsionicsSystem.cs index 23cf6aeb80b..0d05000a3cd 100644 --- a/Content.Server/Psionics/PsionicsSystem.cs +++ b/Content.Server/Psionics/PsionicsSystem.cs @@ -11,139 +11,242 @@ using Robust.Shared.Audio.Systems; using Robust.Shared.Configuration; using Robust.Shared.Random; +using Content.Shared.Popups; +using Content.Shared.Chat; +using Robust.Server.Player; +using Content.Server.Chat.Managers; +using Robust.Shared.Prototypes; +using Content.Shared.Psionics; -namespace Content.Server.Psionics +namespace Content.Server.Psionics; + +public sealed class PsionicsSystem : EntitySystem { - public sealed class PsionicsSystem : EntitySystem + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly PsionicAbilitiesSystem _psionicAbilitiesSystem = default!; + [Dependency] private readonly StatusEffectsSystem _statusEffects = default!; + [Dependency] private readonly ElectrocutionSystem _electrocutionSystem = default!; + [Dependency] private readonly MindSwapPowerSystem _mindSwapPowerSystem = default!; + [Dependency] private readonly GlimmerSystem _glimmerSystem = default!; + [Dependency] private readonly NpcFactionSystem _npcFactonSystem = default!; + [Dependency] private readonly IConfigurationManager _cfg = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedPopupSystem _popups = default!; + [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly IChatManager _chatManager = default!; + [Dependency] private readonly IPrototypeManager _protoMan = default!; + + private const string BaselineAmplification = "Baseline Amplification"; + private const string BaselineDampening = "Baseline Dampening"; + + // Yes these are a mirror of what's normally default datafields on the PsionicPowerPrototype. + // We haven't generated a prototype yet, and I'm not going to duplicate them on the PsionicComponent. + private const string PsionicRollFailedMessage = "psionic-roll-failed"; + private const string PsionicRollFailedColor = "#8A00C2"; + private const int PsionicRollFailedFontSize = 12; + private const ChatChannel PsionicRollFailedChatChannel = ChatChannel.Emotes; + + /// + /// Unfortunately, since spawning as a normal role and anything else is so different, + /// this is the only way to unify them, for now at least. + /// + Queue<(PsionicComponent component, EntityUid uid)> _rollers = new(); + public override void Update(float frameTime) { - [Dependency] private readonly IRobustRandom _random = default!; - [Dependency] private readonly PsionicAbilitiesSystem _psionicAbilitiesSystem = default!; - [Dependency] private readonly StatusEffectsSystem _statusEffects = default!; - [Dependency] private readonly ElectrocutionSystem _electrocutionSystem = default!; - [Dependency] private readonly MindSwapPowerSystem _mindSwapPowerSystem = default!; - [Dependency] private readonly GlimmerSystem _glimmerSystem = default!; - [Dependency] private readonly NpcFactionSystem _npcFactonSystem = default!; - [Dependency] private readonly IConfigurationManager _cfg = default!; - [Dependency] private readonly SharedAudioSystem _audio = default!; - - private const string BaselineAmplification = "Baseline Amplification"; - private const string BaselineDampening = "Baseline Dampening"; - - /// - /// Unfortunately, since spawning as a normal role and anything else is so different, - /// this is the only way to unify them, for now at least. - /// - Queue<(PsionicComponent component, EntityUid uid)> _rollers = new(); - public override void Update(float frameTime) - { - base.Update(frameTime); - foreach (var roller in _rollers) - RollPsionics(roller.uid, roller.component, false); - _rollers.Clear(); - } - public override void Initialize() - { - base.Initialize(); - SubscribeLocalEvent(OnStartup); - SubscribeLocalEvent(OnMeleeHit); - SubscribeLocalEvent(OnStamHit); + base.Update(frameTime); + foreach (var roller in _rollers) + RollPsionics(roller.uid, roller.component, false); + _rollers.Clear(); + } + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnStartup); + SubscribeLocalEvent(OnMeleeHit); + SubscribeLocalEvent(OnStamHit); - SubscribeLocalEvent(OnInit); - SubscribeLocalEvent(OnRemove); - } + SubscribeLocalEvent(OnInit); + SubscribeLocalEvent(OnRemove); + } - private void OnStartup(EntityUid uid, PsionicComponent component, MapInitEvent args) - { - _rollers.Enqueue((component, uid)); - } + private void OnStartup(EntityUid uid, PsionicComponent component, MapInitEvent args) + { + if (!component.Removable + || !component.CanReroll) + return; - private void OnMeleeHit(EntityUid uid, AntiPsionicWeaponComponent component, MeleeHitEvent args) - { - foreach (var entity in args.HitEntities) - { - if (HasComp(entity)) - { - _audio.PlayPvs("/Audio/Effects/lightburn.ogg", entity); - args.ModifiersList.Add(component.Modifiers); - if (_random.Prob(component.DisableChance)) - _statusEffects.TryAddStatusEffect(entity, component.DisableStatus, TimeSpan.FromSeconds(component.DisableDuration), true, component.DisableStatus); - } - - if (TryComp(entity, out var swapped)) - { - _mindSwapPowerSystem.Swap(entity, swapped.OriginalEntity, true); - return; - } - - if (component.Punish && !HasComp(entity) && _random.Prob(component.PunishChances)) - _electrocutionSystem.TryDoElectrocution(args.User, null, component.PunishSelfDamage, TimeSpan.FromSeconds(component.PunishStunDuration), false); - } - } + CheckPowerCost(uid, component); + _rollers.Enqueue((component, uid)); + } - private void OnInit(EntityUid uid, PsionicComponent component, ComponentStartup args) - { - component.AmplificationSources.Add(BaselineAmplification, _random.NextFloat(component.BaselineAmplification.Item1, component.BaselineAmplification.Item2)); - component.DampeningSources.Add(BaselineDampening, _random.NextFloat(component.BaselineDampening.Item1, component.BaselineDampening.Item2)); + /// + /// On MapInit, PsionicComponent isn't going to contain any powers. + /// So before we send a Latent Psychic into the roundstart roll queue, we need to calculate their power cost in advance. + /// + private void CheckPowerCost(EntityUid uid, PsionicComponent component) + { + if (!TryComp(uid, out var innate)) + return; - if (!component.Removable - || !TryComp(uid, out var factions) - || _npcFactonSystem.ContainsFaction(uid, "GlimmerMonster", factions)) - return; + var powerCount = 0; + foreach (var powerId in innate.PowersToAdd) + if (_protoMan.TryIndex(powerId, out var power)) + powerCount += power.PowerSlotCost; - _npcFactonSystem.AddFaction(uid, "PsionicInterloper"); - } + component.NextPowerCost = 100 * MathF.Pow(2, powerCount); + } - private void OnRemove(EntityUid uid, PsionicComponent component, ComponentRemove args) + private void OnMeleeHit(EntityUid uid, AntiPsionicWeaponComponent component, MeleeHitEvent args) + { + foreach (var entity in args.HitEntities) + CheckAntiPsionic(entity, component, args); + } + + private void CheckAntiPsionic(EntityUid entity, AntiPsionicWeaponComponent component, MeleeHitEvent args) + { + if (HasComp(entity)) { - if (!HasComp(uid)) + _audio.PlayPvs("/Audio/Effects/lightburn.ogg", entity); + args.ModifiersList.Add(component.Modifiers); + + if (!_random.Prob(component.DisableChance)) return; - _npcFactonSystem.RemoveFaction(uid, "PsionicInterloper"); + _statusEffects.TryAddStatusEffect(entity, component.DisableStatus, TimeSpan.FromSeconds(component.DisableDuration), true, component.DisableStatus); } - private void OnStamHit(EntityUid uid, AntiPsionicWeaponComponent component, TakeStaminaDamageEvent args) - { - if (HasComp(args.Target)) - args.FlatModifier += component.PsychicStaminaDamage; - } + if (TryComp(entity, out var swapped)) + _mindSwapPowerSystem.Swap(entity, swapped.OriginalEntity, true); - public void RollPsionics(EntityUid uid, PsionicComponent component, bool applyGlimmer = true, float rollEventMultiplier = 1f) - { - if (!_cfg.GetCVar(CCVars.PsionicRollsEnabled) - || !component.Removable) - return; + if (!component.Punish + || HasComp(entity) + || !_random.Prob(component.PunishChances)) + return; + + _electrocutionSystem.TryDoElectrocution(args.User, null, component.PunishSelfDamage, TimeSpan.FromSeconds(component.PunishStunDuration), false); + } - // Calculate the initial odds based on the innate potential - var baselineChance = component.Chance - * component.PowerRollMultiplier - + component.PowerRollFlatBonus; + private void OnInit(EntityUid uid, PsionicComponent component, ComponentStartup args) + { + component.AmplificationSources.Add(BaselineAmplification, _random.NextFloat(component.BaselineAmplification.Item1, component.BaselineAmplification.Item2)); + component.DampeningSources.Add(BaselineDampening, _random.NextFloat(component.BaselineDampening.Item1, component.BaselineDampening.Item2)); - // Increase the initial odds based on Glimmer. - // TODO: Change this equation when I do my Glimmer Refactor - baselineChance += applyGlimmer - ? (float) _glimmerSystem.Glimmer / 1000 //Convert from Glimmer to %chance - : 0; + if (!component.Removable + || !TryComp(uid, out var factions) + || _npcFactonSystem.ContainsFaction(uid, "GlimmerMonster", factions)) + return; - // Certain sources of power rolls provide their own multiplier. - baselineChance *= rollEventMultiplier; + _npcFactonSystem.AddFaction(uid, "PsionicInterloper"); + } - // Ask if the Roller has any other effects to contribute, such as Traits. - var ev = new OnRollPsionicsEvent(uid, baselineChance); - RaiseLocalEvent(uid, ref ev); + private void OnRemove(EntityUid uid, PsionicComponent component, ComponentRemove args) + { + if (!HasComp(uid)) + return; - if (_random.Prob(Math.Clamp(ev.BaselineChance, 0, 1))) - _psionicAbilitiesSystem.AddPsionics(uid); - } + _npcFactonSystem.RemoveFaction(uid, "PsionicInterloper"); + } - public void RerollPsionics(EntityUid uid, PsionicComponent? psionic = null, float bonusMuliplier = 1f) - { - if (!Resolve(uid, ref psionic, false) - || !psionic.Removable - || psionic.CanReroll) - return; + private void OnStamHit(EntityUid uid, AntiPsionicWeaponComponent component, TakeStaminaDamageEvent args) + { + if (!HasComp(args.Target)) + return; - RollPsionics(uid, psionic, true, bonusMuliplier); - psionic.CanReroll = true; - } + args.FlatModifier += component.PsychicStaminaDamage; + } + + /// + /// Now we handle Potentia calculations, the more powers you have, the harder it is to obtain psionics, but the content of your roll carries over to the next roll. + /// Your first power costs 100(2^0 is always 1), your second power costs 200, your 3rd power costs 400, and so on. This also considers people with roundstart powers. + /// Such that a Mystagogue(who has 3 powers at roundstart) needs 800 Potentia to gain his 4th power. + /// + /// + /// This exponential cost is mainly done to prevent stations from becoming "Space Hogwarts", + /// which was a common complaint with Psionic Refactor opening up the opportunity for people to have multiple powers. + /// + private bool HandlePotentiaCalculations(EntityUid uid, PsionicComponent component, float psionicChance) + { + component.Potentia += _random.NextFloat(0 + psionicChance, 100 + psionicChance); + + if (component.Potentia < component.NextPowerCost) + return false; + + component.Potentia -= component.NextPowerCost; + _psionicAbilitiesSystem.AddPsionics(uid); + component.NextPowerCost = 100 * MathF.Pow(2, component.PowerSlotsTaken); + return true; + } + + /// + /// Provide the player with feedback about their roll failure, so they don't just think nothing happened. + /// TODO: Add an audio cue to this and other areas of psionic player feedback. + /// + private void HandleRollFeedback(EntityUid uid) + { + if (!_playerManager.TryGetSessionByEntity(uid, out var session) + || !Loc.TryGetString(PsionicRollFailedMessage, out var rollFailedMessage)) + return; + + _popups.PopupEntity(rollFailedMessage, uid, uid, PopupType.MediumCaution); + + // Popups only last a few seconds, and are easily ignored. + // So we also put a message in chat to make it harder to miss. + var feedbackMessage = $"[font size={PsionicRollFailedFontSize}][color={PsionicRollFailedColor}]{rollFailedMessage}[/color][/font]"; + _chatManager.ChatMessageToOne( + PsionicRollFailedChatChannel, + feedbackMessage, + feedbackMessage, + EntityUid.Invalid, + false, + session.Channel); + } + + /// + /// This function attempts to generate a psionic power by incrementing a Psion's Potentia stat by a random amount, then checking if it beats a certain threshold. + /// Please consider going through RerollPsionics or PsionicAbilitiesSystem.InitializePsionicPower instead of this function, particularly if you don't have a good reason to call this directly. + /// + public void RollPsionics(EntityUid uid, PsionicComponent component, bool applyGlimmer = true, float rollEventMultiplier = 1f) + { + if (!_cfg.GetCVar(CCVars.PsionicRollsEnabled) + || !component.Removable) + return; + + // Calculate the initial odds based on the innate potential + var baselineChance = component.Chance + * component.PowerRollMultiplier + + component.PowerRollFlatBonus; + + // Increase the initial odds based on Glimmer. + // TODO: Change this equation when I do my Glimmer Refactor + baselineChance += applyGlimmer + ? (float) _glimmerSystem.Glimmer / 1000 //Convert from Glimmer to %chance + : 0; + + // Certain sources of power rolls provide their own multiplier. + baselineChance *= rollEventMultiplier; + + // Ask if the Roller has any other effects to contribute, such as Traits. + var ev = new OnRollPsionicsEvent(uid, baselineChance); + RaiseLocalEvent(uid, ref ev); + + if (HandlePotentiaCalculations(uid, component, ev.BaselineChance)) + return; + + HandleRollFeedback(uid); + } + + /// + /// Each person has a single free reroll for their Psionics, which certain conditions can restore. + /// This function attempts to "Spend" a reroll, if one is available. + /// + public void RerollPsionics(EntityUid uid, PsionicComponent? psionic = null, float bonusMuliplier = 1f) + { + if (!Resolve(uid, ref psionic, false) + || !psionic.Removable + || !psionic.CanReroll) + return; + + RollPsionics(uid, psionic, true, bonusMuliplier); + psionic.CanReroll = false; } } diff --git a/Content.Server/StationEvents/Events/NoosphericZapRule.cs b/Content.Server/StationEvents/Events/NoosphericZapRule.cs index 4be7b6e63fc..96c33612036 100644 --- a/Content.Server/StationEvents/Events/NoosphericZapRule.cs +++ b/Content.Server/StationEvents/Events/NoosphericZapRule.cs @@ -36,9 +36,9 @@ protected override void Started(EntityUid uid, NoosphericZapRuleComponent compon _stunSystem.TryParalyze(psion, TimeSpan.FromSeconds(component.StunDuration), false); _statusEffectsSystem.TryAddStatusEffect(psion, "Stutter", TimeSpan.FromSeconds(component.StutterDuration), false, "StutteringAccent"); - if (psionicComponent.CanReroll) + if (!psionicComponent.CanReroll) { - psionicComponent.CanReroll = false; + psionicComponent.CanReroll = true; _popupSystem.PopupEntity(Loc.GetString("noospheric-zap-seize-potential-regained"), psion, psion, Shared.Popups.PopupType.LargeCaution); } else diff --git a/Content.Shared/Psionics/PsionicComponent.cs b/Content.Shared/Psionics/PsionicComponent.cs index 63529576152..85b7e380fea 100644 --- a/Content.Shared/Psionics/PsionicComponent.cs +++ b/Content.Shared/Psionics/PsionicComponent.cs @@ -8,11 +8,19 @@ namespace Content.Shared.Abilities.Psionics public sealed partial class PsionicComponent : Component { /// - /// How close a Psion is to awakening a new power. - /// TODO: Implement this in a separate PR. + /// How close a Psion is to generating a new power. When Potentia reaches the NextPowerCost, it is "Spent" in order to "Buy" a random new power. + /// TODO: Psi-Potentiometry should be able to read how much Potentia a person has. + /// + [DataField] + public float Potentia; + + /// + /// Each time a Psion rolls for a new power, they roll a number between 0 and 100, adding any relevant modifiers. This number is then added to Potentia, + /// meaning that it carries over between rolls. When a character has an amount of potentia equal to at least 100 * 2^(total powers), the potentia is then spent, and a power is generated. + /// This variable stores the cost of the next power. /// [DataField] - public float Potentia = 0; + public float NextPowerCost = 100; /// /// The baseline chance of obtaining a psionic power when rolling for one. @@ -24,7 +32,7 @@ public sealed partial class PsionicComponent : Component /// Whether or not a Psion has an available "Reroll" to spend on attempting to gain powers. /// [DataField] - public bool CanReroll; + public bool CanReroll = true; /// /// The Base amount of time (in minutes) this Psion is given the stutter effect if they become mindbroken. @@ -142,6 +150,18 @@ private set public float CurrentDampening; /// + /// How many "Slots" an entity has for psionic powers. This is not a hard limit, and is instead used for calculating the cost to generate new powers. + /// Exceeding this limit causes an entity to become a Glimmer Source. + /// + [DataField] + public int PowerSlots = 1; + + /// + /// How many "Slots" are currently occupied by psionic powers. + /// + [ViewVariables(VVAccess.ReadWrite)] + public int PowerSlotsTaken; + /// List of descriptors this entity will bring up for psychognomy. Used to remove /// unneccesary subs for unique psionic entities like e.g. Oracle. /// diff --git a/Content.Shared/Psionics/PsionicPowerPrototype.cs b/Content.Shared/Psionics/PsionicPowerPrototype.cs index 3d389f6cdbe..c4a3326733a 100644 --- a/Content.Shared/Psionics/PsionicPowerPrototype.cs +++ b/Content.Shared/Psionics/PsionicPowerPrototype.cs @@ -86,4 +86,10 @@ public sealed partial class PsionicPowerPrototype : IPrototype ///

[DataField] public float DampeningModifier = 0; + + /// + /// How many "Power Slots" this power occupies. + /// + [DataField] + public int PowerSlotCost = 1; } \ No newline at end of file diff --git a/Resources/Locale/en-US/psionics/psionic-powers.ftl b/Resources/Locale/en-US/psionics/psionic-powers.ftl index ad6bbef02bd..c68bb2a4968 100644 --- a/Resources/Locale/en-US/psionics/psionic-powers.ftl +++ b/Resources/Locale/en-US/psionics/psionic-powers.ftl @@ -86,7 +86,9 @@ telepathy-power-initialization-feedback = The voices I've heard all my life begin to clear, yet they do not leave me. Before, they were as incoherent whispers, now my senses broaden, I come to a realization that they are part of a communal shared hallucination. Behind every voice is a glimmering sentience. +# Psionic System Messages mindbreaking-feedback = The light of life vanishes from {CAPITALIZE($entity)}'s eyes, leaving behind a husk pretending at sapience examine-mindbroken-message = Eyes unblinking, staring deep into the horizon. {CAPITALIZE($entity)} is a sack of meat pretending it has a soul. - There is nothing behind its gaze, no evidence there can be found of the divine light of creation. \ No newline at end of file + There is nothing behind its gaze, no evidence there can be found of the divine light of creation. +psionic-roll-failed = For a moment, my consciousness expands, yet I feel that it is not enough. \ No newline at end of file diff --git a/Resources/Prototypes/Nyanotrasen/Roles/Jobs/Epistemics/forensicmantis.yml b/Resources/Prototypes/Nyanotrasen/Roles/Jobs/Epistemics/forensicmantis.yml index 1f78015cd63..f7a31205706 100644 --- a/Resources/Prototypes/Nyanotrasen/Roles/Jobs/Epistemics/forensicmantis.yml +++ b/Resources/Prototypes/Nyanotrasen/Roles/Jobs/Epistemics/forensicmantis.yml @@ -32,6 +32,7 @@ - !type:AddComponentSpecial components: - type: Psionic + powerSlots: 2 - !type:AddComponentSpecial components: - type: InnatePsionicPowers diff --git a/Resources/Prototypes/Psionics/psionics.yml b/Resources/Prototypes/Psionics/psionics.yml index b8f798d4b63..461098eab21 100644 --- a/Resources/Prototypes/Psionics/psionics.yml +++ b/Resources/Prototypes/Psionics/psionics.yml @@ -119,6 +119,7 @@ - type: UniversalLanguageSpeaker initializationFeedback: xenoglossy-power-initialization-feedback metapsionicFeedback: psionic-language-power-feedback # Reuse for telepathy, clairaudience, etc + powerSlotCost: 0 - type: psionicPower id: PsychognomyPower #i.e. reverse physiognomy @@ -128,6 +129,7 @@ - type: Psychognomist initializationFeedback: psychognomy-power-initialization-feedback metapsionicFeedback: psionic-language-power-feedback + powerSlotCost: 0 - type: psionicPower id: TelepathyPower @@ -137,3 +139,4 @@ - type: Telepathy initializationFeedback: telepathy-power-initialization-feedback metapsionicFeedback: psionic-language-power-feedback # Reuse for telepathy, clairaudience, etc + powerSlotCost: 0 diff --git a/Resources/Prototypes/Roles/Jobs/Science/research_director.yml b/Resources/Prototypes/Roles/Jobs/Science/research_director.yml index e2d06f3af96..46d91ee00ee 100644 --- a/Resources/Prototypes/Roles/Jobs/Science/research_director.yml +++ b/Resources/Prototypes/Roles/Jobs/Science/research_director.yml @@ -38,6 +38,7 @@ components: - type: BibleUser # Nyano - Lets them heal with bibles - type: Psionic # Nyano - They start with telepathic chat + powerSlots: 3 - !type:AddImplantSpecial implants: [ MindShieldImplant ] - !type:AddComponentSpecial From a55390985846a3262023b5c512f1f828d9e007d2 Mon Sep 17 00:00:00 2001 From: VMSolidus Date: Tue, 17 Sep 2024 19:42:47 -0400 Subject: [PATCH 14/55] IPC Refactor (#771) # Description IPCs were ported from a codebase that didn't necessarily follow all of our repo's coding standards. And while I had done my part to cleanup as much of the system as was practical within the bounds of a Maintainer Review, there were a lot of things that I felt were inappropriate to leave to review, and wished to go over with a fine lense. Thus, here is my Refactor of IPC code. Do not merge this without first testing that nothing was broken. Because I haven't tested it myself yet. --- .../Commands/SetOutfitCommand.cs | 8 +- .../SiliconEmitSoundOnDrainedComponent.cs | 2 - .../Power/Systems/BatteryDrinkerSystem.cs | 56 ++---- .../Systems/BatteryElectrocuteChargeSystem.cs | 9 +- .../SiliconEmitSoundOnDrainedSystem.cs | 7 +- .../BatterySlotRequiresLockSystem.cs | 15 +- .../BlindHealing/BlindHealingComponent.cs | 39 ++-- .../BlindHealing/BlindHealingSystem.cs | 161 +++++++--------- .../Components/SiliconDownOnDeadComponent.cs | 4 +- .../Systems/SiliconChargeDeathSystem.cs | 9 +- .../Charge/Systems/SiliconChargeSystem.cs | 52 ++--- .../DeadStartupButtonSystem.cs | 26 +-- .../EmitBuzzWhileDamagedSystem.cs | 33 ++-- .../EncryptionHolderRequiresLockSystem.cs | 6 +- .../IPC/InternalEncryptionKeySpawner.cs | 42 ++-- .../Silicon/Systems/SiliconMiscSystem.cs | 20 -- .../WeldingHealableComponent.cs | 12 +- .../WeldingHealable/WeldingHealableSystem.cs | 179 ++++++++---------- .../WeldingHealing/WeldingHealingComponent.cs | 83 ++++---- .../Station/Systems/StationSpawningSystem.cs | 4 +- Content.Shared/Lock/LockComponent.cs | 54 +++--- Content.Shared/Lock/LockSystem.cs | 1 - .../EncryptionKeyHolderComponent.cs | 18 +- .../EntitySystems/EncryptionKeySystem.cs | 12 +- Content.Shared/Silicon/BatteryDrinkerEvent.cs | 4 +- .../BlindHealing/SharedBlindHealingSystem.cs | 4 +- .../Silicon/Components/SiliconComponent.cs | 30 +-- .../DeadStartupButtonComponent.cs | 14 +- .../SharedDeadStartupButtonSystem.cs | 23 +-- .../EmitBuzzWhileDamagedComponent.cs | 14 +- .../Silicon/Systems/SharedSiliconSystem.cs | 68 ++++--- .../Station/SharedStationSpawningSystem.cs | 1 - 32 files changed, 424 insertions(+), 586 deletions(-) delete mode 100644 Content.Server/Silicon/Systems/SiliconMiscSystem.cs diff --git a/Content.Server/Administration/Commands/SetOutfitCommand.cs b/Content.Server/Administration/Commands/SetOutfitCommand.cs index 2f979f4340b..12312286518 100644 --- a/Content.Server/Administration/Commands/SetOutfitCommand.cs +++ b/Content.Server/Administration/Commands/SetOutfitCommand.cs @@ -13,6 +13,7 @@ using Robust.Shared.Player; using Robust.Shared.Prototypes; using Content.Server.Silicon.IPC; +using Content.Shared.Radio.Components; namespace Content.Server.Administration.Commands { @@ -127,7 +128,12 @@ public static bool SetOutfit(EntityUid target, string gear, IEntityManager entit handsSystem.TryPickup(target, inhandEntity, checkActionBlocker: false, handsComp: handsComponent); } } - InternalEncryptionKeySpawner.TryInsertEncryptionKey(target, startingGear, entityManager, profile); + + if (entityManager.HasComponent(target)) + { + var encryption = new InternalEncryptionKeySpawner(); + encryption.TryInsertEncryptionKey(target, startingGear, entityManager); + } return true; } } diff --git a/Content.Server/Power/Components/SiliconEmitSoundOnDrainedComponent.cs b/Content.Server/Power/Components/SiliconEmitSoundOnDrainedComponent.cs index 4e5121d6076..5740d7822d0 100644 --- a/Content.Server/Power/Components/SiliconEmitSoundOnDrainedComponent.cs +++ b/Content.Server/Power/Components/SiliconEmitSoundOnDrainedComponent.cs @@ -1,6 +1,4 @@ -using System.ComponentModel.DataAnnotations; using Robust.Shared.Audio; -using Content.Server.Sound.Components; namespace Content.Server.Silicon; diff --git a/Content.Server/Power/Systems/BatteryDrinkerSystem.cs b/Content.Server/Power/Systems/BatteryDrinkerSystem.cs index 9a06d4181c9..e42783c4d8d 100644 --- a/Content.Server/Power/Systems/BatteryDrinkerSystem.cs +++ b/Content.Server/Power/Systems/BatteryDrinkerSystem.cs @@ -1,4 +1,3 @@ -using System.Diagnostics.CodeAnalysis; using System.Linq; using Content.Server.Power.Components; using Content.Shared.Containers.ItemSlots; @@ -12,8 +11,6 @@ using Content.Server.Popups; using Content.Server.PowerCell; using Content.Shared.Popups; -using Content.Shared.Silicon.Components; -using FastAccessors; using Robust.Shared.Audio.Systems; using Robust.Shared.Containers; @@ -21,13 +18,11 @@ namespace Content.Server.Power; public sealed class BatteryDrinkerSystem : EntitySystem { - [Dependency] private readonly ItemSlotsSystem _slots = default!; [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly BatterySystem _battery = default!; [Dependency] private readonly SiliconChargeSystem _silicon = default!; [Dependency] private readonly PopupSystem _popup = default!; - [Dependency] private readonly PowerCellSystem _powerCell = default!; [Dependency] private readonly SharedContainerSystem _container = default!; public override void Initialize() @@ -41,12 +36,10 @@ public override void Initialize() private void AddAltVerb(EntityUid uid, BatteryComponent batteryComponent, GetVerbsEvent args) { - if (!args.CanAccess || !args.CanInteract) - return; - - if (!TryComp(args.User, out var drinkerComp) || - !TestDrinkableBattery(uid, drinkerComp) || - !_silicon.TryGetSiliconBattery(args.User, out var drinkerBattery)) + if (!args.CanAccess || !args.CanInteract + || !TryComp(args.User, out var drinkerComp) + || !TestDrinkableBattery(uid, drinkerComp) + || !_silicon.TryGetSiliconBattery(args.User, out var _)) return; AlternativeVerb verb = new() @@ -80,6 +73,7 @@ private void DrinkBattery(EntityUid target, EntityUid user, BatteryDrinkerCompon { BreakOnDamage = true, BreakOnTargetMove = true, + BreakOnUserMove = true, Broadcast = false, DistanceThreshold = 1.35f, RequireCanInteract = true, @@ -91,39 +85,28 @@ private void DrinkBattery(EntityUid target, EntityUid user, BatteryDrinkerCompon private void OnDoAfter(EntityUid uid, BatteryDrinkerComponent drinkerComp, DoAfterEvent args) { - if (args.Cancelled || args.Target == null) + if (args.Cancelled || args.Target == null + || !TryComp(args.Target.Value, out var sourceBattery) + || !_silicon.TryGetSiliconBattery(uid, out var drinkerBatteryComponent) + || !TryComp(uid, out PowerCellSlotComponent? batterySlot) + || !TryComp(args.Target.Value, out var sourceComp) + || !_container.TryGetContainer(uid, batterySlot.CellSlotId, out var container) + || container.ContainedEntities is null) return; var source = args.Target.Value; - var drinker = uid; - var sourceBattery = Comp(source); - - _silicon.TryGetSiliconBattery(drinker, out var drinkerBatteryComponent); - - if (!TryComp(uid, out PowerCellSlotComponent? batterySlot)) - return; - - var container = _container.GetContainer(uid, batterySlot.CellSlotId); var drinkerBattery = container.ContainedEntities.First(); - - TryComp(source, out var sourceComp); - - DebugTools.AssertNotNull(drinkerBattery); - - if (drinkerBattery == null) - return; - var amountToDrink = drinkerComp.DrinkMultiplier * 1000; amountToDrink = MathF.Min(amountToDrink, sourceBattery.CurrentCharge); amountToDrink = MathF.Min(amountToDrink, drinkerBatteryComponent!.MaxCharge - drinkerBatteryComponent.CurrentCharge); - if (sourceComp != null && sourceComp.MaxAmount > 0) + if (sourceComp.MaxAmount > 0) amountToDrink = MathF.Min(amountToDrink, (float) sourceComp.MaxAmount); if (amountToDrink <= 0) { - _popup.PopupEntity(Loc.GetString("battery-drinker-empty", ("target", source)), drinker, drinker); + _popup.PopupEntity(Loc.GetString("battery-drinker-empty", ("target", source)), uid, uid); return; } @@ -135,10 +118,11 @@ private void OnDoAfter(EntityUid uid, BatteryDrinkerComponent drinkerComp, DoAft _battery.SetCharge(source, 0); } - if (sourceComp != null && sourceComp.DrinkSound != null){ - _popup.PopupEntity(Loc.GetString("ipc-recharge-tip"), drinker, drinker, PopupType.SmallCaution); - _audio.PlayPvs(sourceComp.DrinkSound, source); - Spawn("EffectSparks", Transform(source).Coordinates); - } + if (sourceComp.DrinkSound is null) + return; + + _popup.PopupEntity(Loc.GetString("ipc-recharge-tip"), uid, uid, PopupType.SmallCaution); + _audio.PlayPvs(sourceComp.DrinkSound, source); + Spawn("EffectSparks", Transform(source).Coordinates); } } diff --git a/Content.Server/Power/Systems/BatteryElectrocuteChargeSystem.cs b/Content.Server/Power/Systems/BatteryElectrocuteChargeSystem.cs index 9993c151b1e..707d7c679e8 100644 --- a/Content.Server/Power/Systems/BatteryElectrocuteChargeSystem.cs +++ b/Content.Server/Power/Systems/BatteryElectrocuteChargeSystem.cs @@ -4,7 +4,6 @@ using Content.Server.Power.EntitySystems; using Content.Shared.Electrocution; using Robust.Shared.Random; -using Robust.Shared.Timing; namespace Content.Server.Power.Systems; @@ -26,10 +25,10 @@ private void OnElectrocuted(EntityUid uid, BatteryComponent battery, Electrocute if (args.ShockDamage == null || args.ShockDamage <= 0) return; - var damagePerWatt = ElectrocutionSystem.ElectrifiedDamagePerWatt * 2; - - var damage = args.ShockDamage.Value * args.SiemensCoefficient; - var charge = Math.Min(damage / damagePerWatt, battery.MaxCharge * 0.25f) * _random.NextFloat(0.75f, 1.25f); + var charge = Math.Min(args.ShockDamage.Value * args.SiemensCoefficient + / ElectrocutionSystem.ElectrifiedDamagePerWatt * 2, + battery.MaxCharge * 0.25f) + * _random.NextFloat(0.75f, 1.25f); _battery.SetCharge(uid, battery.CurrentCharge + charge); diff --git a/Content.Server/Power/Systems/SiliconEmitSoundOnDrainedSystem.cs b/Content.Server/Power/Systems/SiliconEmitSoundOnDrainedSystem.cs index 28a46cc7f70..f95a940aaeb 100644 --- a/Content.Server/Power/Systems/SiliconEmitSoundOnDrainedSystem.cs +++ b/Content.Server/Power/Systems/SiliconEmitSoundOnDrainedSystem.cs @@ -2,7 +2,6 @@ using Content.Shared.Sound.Components; using Content.Server.Sound; using Content.Shared.Mobs; -using Content.Shared.Silicon.Systems; namespace Content.Server.Silicon; @@ -35,7 +34,9 @@ private void OnAlive(EntityUid uid, SiliconEmitSoundOnDrainedComponent component public void OnStateChange(EntityUid uid, SiliconEmitSoundOnDrainedComponent component, MobStateChangedEvent args) { - if (args.NewMobState == MobState.Dead) - RemComp(uid); + if (args.NewMobState != MobState.Dead) + return; + + RemComp(uid); } } diff --git a/Content.Server/Silicon/BatteryLocking/BatterySlotRequiresLockSystem.cs b/Content.Server/Silicon/BatteryLocking/BatterySlotRequiresLockSystem.cs index 98e1cb5584c..12e86691281 100644 --- a/Content.Server/Silicon/BatteryLocking/BatterySlotRequiresLockSystem.cs +++ b/Content.Server/Silicon/BatteryLocking/BatterySlotRequiresLockSystem.cs @@ -2,7 +2,7 @@ using Content.Shared.Lock; using Content.Shared.Popups; using Content.Shared.Silicon.Components; -using Content.Shared.IdentityManagement; +using Content.Shared.IdentityManagement; namespace Content.Server.Silicon.BatteryLocking; @@ -10,19 +10,18 @@ public sealed class BatterySlotRequiresLockSystem : EntitySystem { [Dependency] private readonly ItemSlotsSystem _itemSlotsSystem = default!; - [Dependency] private readonly SharedPopupSystem _popupSystem = default!; + [Dependency] private readonly SharedPopupSystem _popupSystem = default!; - /// public override void Initialize() { base.Initialize(); SubscribeLocalEvent(LockToggled); - SubscribeLocalEvent(LockToggleAttempted); - + SubscribeLocalEvent(LockToggleAttempted); } + private void LockToggled(EntityUid uid, BatterySlotRequiresLockComponent component, LockToggledEvent args) { - if (!TryComp(uid, out var lockComp) + if (!TryComp(uid, out var lockComp) || !TryComp(uid, out var itemslots) || !_itemSlotsSystem.TryGetSlot(uid, component.ItemSlot, out var slot, itemslots)) return; @@ -33,9 +32,9 @@ private void LockToggled(EntityUid uid, BatterySlotRequiresLockComponent compone private void LockToggleAttempted(EntityUid uid, BatterySlotRequiresLockComponent component, LockToggleAttemptEvent args) { if (args.User == uid - || !TryComp(uid, out var siliconComp)) + || !HasComp(uid)) return; - + _popupSystem.PopupEntity(Loc.GetString("batteryslotrequireslock-component-alert-owner", ("user", Identity.Entity(args.User, EntityManager))), uid, uid, PopupType.Large); } diff --git a/Content.Server/Silicon/BlindHealing/BlindHealingComponent.cs b/Content.Server/Silicon/BlindHealing/BlindHealingComponent.cs index 4c3c478c4b8..fe218491708 100644 --- a/Content.Server/Silicon/BlindHealing/BlindHealingComponent.cs +++ b/Content.Server/Silicon/BlindHealing/BlindHealingComponent.cs @@ -1,28 +1,23 @@ -using Content.Shared.Damage; -using Content.Shared.Tools; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; +namespace Content.Server.Silicon.BlindHealing; -namespace Content.Server.Silicon.BlindHealing +[RegisterComponent] +public sealed partial class BlindHealingComponent : Component { - [RegisterComponent] - public sealed partial class BlindHealingComponent : Component - { - [DataField] - public int DoAfterDelay = 3; + [DataField] + public int DoAfterDelay = 3; - /// - /// A multiplier that will be applied to the above if an entity is repairing themselves. - /// - [DataField] - public float SelfHealPenalty = 3f; + /// + /// A multiplier that will be applied to the above if an entity is repairing themselves. + /// + [DataField] + public float SelfHealPenalty = 3f; - /// - /// Whether or not an entity is allowed to repair itself. - /// - [DataField] - public bool AllowSelfHeal = true; + /// + /// Whether or not an entity is allowed to repair itself. + /// + [DataField] + public bool AllowSelfHeal = true; - [DataField(required: true)] - public List DamageContainers; - } + [DataField(required: true)] + public List DamageContainers; } diff --git a/Content.Server/Silicon/BlindHealing/BlindHealingSystem.cs b/Content.Server/Silicon/BlindHealing/BlindHealingSystem.cs index 6cf60e6ef3f..b9d26b59f7c 100644 --- a/Content.Server/Silicon/BlindHealing/BlindHealingSystem.cs +++ b/Content.Server/Silicon/BlindHealing/BlindHealingSystem.cs @@ -12,110 +12,87 @@ using Content.Shared.Popups; using Content.Shared.Stacks; -namespace Content.Server.Silicon.BlindHealing +namespace Content.Server.Silicon.BlindHealing; + +public sealed class BlindHealingSystem : SharedBlindHealingSystem { - public sealed class BlindHealingSystem : SharedBlindHealingSystem + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly IAdminLogManager _adminLogger = default!; + [Dependency] private readonly BlindableSystem _blindableSystem = default!; + [Dependency] private readonly StackSystem _stackSystem = default!; + [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; + + public override void Initialize() { - [Dependency] private readonly SharedPopupSystem _popup = default!; - [Dependency] private readonly IAdminLogManager _adminLogger= default!; - [Dependency] private readonly BlindableSystem _blindableSystem = default!; - [Dependency] private readonly StackSystem _stackSystem = default!; - [Dependency] private readonly DamageableSystem _damageableSystem = default!; - [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; - - public override void Initialize() - { - SubscribeLocalEvent(OnUse); - SubscribeLocalEvent(OnInteract); - SubscribeLocalEvent(OnHealingFinished); - } - - private void OnHealingFinished(EntityUid uid, BlindHealingComponent component, HealingDoAfterEvent args) - { - Log.Info("event started!"); - - if (args.Cancelled || args.Target == null) - return; - - EntityUid target = (EntityUid) args.Target; - - if(!EntityManager.TryGetComponent(target, out BlindableComponent? blindcomp) - || blindcomp is { EyeDamage: 0 }) - return; - - if(EntityManager.TryGetComponent(uid, out StackComponent? stackComponent)) - { - double price = 1; - if (EntityManager.TryGetComponent(uid, out StackPriceComponent? stackPrice)) - price = stackPrice.Price; - _stackSystem.SetCount(uid, (int) (_stackSystem.GetCount(uid, stackComponent) - price), stackComponent); + SubscribeLocalEvent(OnUse); + SubscribeLocalEvent(OnInteract); + SubscribeLocalEvent(OnHealingFinished); + } - } + private void OnHealingFinished(EntityUid uid, BlindHealingComponent component, HealingDoAfterEvent args) + { + if (args.Cancelled || args.Target == null + || !TryComp(args.Target, out var blindComp) + || blindComp is { EyeDamage: 0 }) + return; - _blindableSystem.AdjustEyeDamage((target, blindcomp), -blindcomp!.EyeDamage); + if (TryComp(uid, out var stackComponent) + && TryComp(uid, out var stackPrice)) + _stackSystem.SetCount(uid, (int) (_stackSystem.GetCount(uid, stackComponent) - stackPrice.Price), stackComponent); - _adminLogger.Add(LogType.Healed, $"{ToPrettyString(args.User):user} repaired {ToPrettyString(uid):target}'s vision"); + _blindableSystem.AdjustEyeDamage((args.Target.Value, blindComp), -blindComp.EyeDamage); - var str = Loc.GetString("comp-repairable-repair", - ("target", uid), - ("tool", args.Used!)); - _popup.PopupEntity(str, uid, args.User); + _adminLogger.Add(LogType.Healed, $"{ToPrettyString(args.User):user} repaired {ToPrettyString(uid):target}'s vision"); - } + var str = Loc.GetString("comp-repairable-repair", + ("target", uid), + ("tool", args.Used!)); + _popup.PopupEntity(str, uid, args.User); - private bool TryHealBlindness(EntityUid uid, EntityUid user, EntityUid target, float delay) - { - var doAfterEventArgs = - new DoAfterArgs(EntityManager, user, delay, new HealingDoAfterEvent(), uid, target: target, used: uid) - { - NeedHand = true, - BreakOnUserMove = true, - BreakOnWeightlessMove = false, - }; + } - _doAfter.TryStartDoAfter(doAfterEventArgs); - return true; - } + private bool TryHealBlindness(EntityUid uid, EntityUid user, EntityUid target, float delay) + { + var doAfterEventArgs = + new DoAfterArgs(EntityManager, user, delay, new HealingDoAfterEvent(), uid, target: target, used: uid) + { + NeedHand = true, + BreakOnUserMove = true, + BreakOnWeightlessMove = false, + }; - private void OnInteract(EntityUid uid, BlindHealingComponent component, ref AfterInteractEvent args) - { + _doAfter.TryStartDoAfter(doAfterEventArgs); + return true; + } - if (args.Handled - || !TryComp(args.User, out DamageableComponent? damageable) - || damageable.DamageContainerID != null - && !component.DamageContainers.Contains(damageable.DamageContainerID) - || !TryComp(args.User, out BlindableComponent? blindcomp) - || blindcomp is { EyeDamage: 0 }) - return; + private void OnInteract(EntityUid uid, BlindHealingComponent component, ref AfterInteractEvent args) + { - float delay = component.DoAfterDelay; + if (args.Handled + || !TryComp(args.User, out var damageable) + || damageable.DamageContainerID != null && !component.DamageContainers.Contains(damageable.DamageContainerID) + || !TryComp(args.User, out var blindcomp) + || blindcomp.EyeDamage == 0 + || args.User == args.Target && !component.AllowSelfHeal) + return; + + TryHealBlindness(uid, args.User, args.User, + args.User == args.Target + ? component.DoAfterDelay * component.SelfHealPenalty + : component.DoAfterDelay); + } - if (args.User == args.Target) - { - if (!component.AllowSelfHeal) - return; - delay *= component.SelfHealPenalty; - } - - TryHealBlindness(uid, args.User, args.User, delay); - } - - private void OnUse(EntityUid uid, BlindHealingComponent component, ref UseInHandEvent args) - { - if (args.Handled - || !TryComp(args.User, out DamageableComponent? damageable) - || damageable.DamageContainerID != null - && !component.DamageContainers.Contains(damageable.DamageContainerID) - || !TryComp(args.User, out BlindableComponent? blindcomp) - || blindcomp is { EyeDamage: 0 } - || !component.AllowSelfHeal) - return; - - float delay = component.DoAfterDelay; - delay *= component.SelfHealPenalty; - - TryHealBlindness(uid, args.User, args.User, delay); - - } + private void OnUse(EntityUid uid, BlindHealingComponent component, ref UseInHandEvent args) + { + if (args.Handled + || !TryComp(args.User, out var damageable) + || damageable.DamageContainerID != null && !component.DamageContainers.Contains(damageable.DamageContainerID) + || !TryComp(args.User, out var blindcomp) + || blindcomp.EyeDamage == 0 + || !component.AllowSelfHeal) + return; + + TryHealBlindness(uid, args.User, args.User, + component.DoAfterDelay * component.SelfHealPenalty); } } diff --git a/Content.Server/Silicon/Charge/Components/SiliconDownOnDeadComponent.cs b/Content.Server/Silicon/Charge/Components/SiliconDownOnDeadComponent.cs index 3080144cd4a..4b3aad33ab6 100644 --- a/Content.Server/Silicon/Charge/Components/SiliconDownOnDeadComponent.cs +++ b/Content.Server/Silicon/Charge/Components/SiliconDownOnDeadComponent.cs @@ -1,5 +1,3 @@ -using System.Threading; - namespace Content.Server.Silicon.Death; /// @@ -15,5 +13,5 @@ public sealed partial class SiliconDownOnDeadComponent : Component /// Is this Silicon currently dead? /// [ViewVariables(VVAccess.ReadOnly)] - public bool Dead { get; set; } = false; + public bool Dead; } diff --git a/Content.Server/Silicon/Charge/Systems/SiliconChargeDeathSystem.cs b/Content.Server/Silicon/Charge/Systems/SiliconChargeDeathSystem.cs index d6fa07a1a14..d4d1db5ed96 100644 --- a/Content.Server/Silicon/Charge/Systems/SiliconChargeDeathSystem.cs +++ b/Content.Server/Silicon/Charge/Systems/SiliconChargeDeathSystem.cs @@ -2,14 +2,9 @@ using Content.Shared.Silicon.Systems; using Content.Server.Bed.Sleep; using Content.Shared.Bed.Sleep; -using Content.Server.Sound.Components; using Content.Server.Silicon.Charge; -using System.Threading; using Content.Server.Humanoid; using Content.Shared.Humanoid; -using Content.Shared.Humanoid.Markings; -using Robust.Shared.Utility; -using Timer = Robust.Shared.Timing.Timer; namespace Content.Server.Silicon.Death; @@ -40,7 +35,7 @@ private void OnSiliconChargeStateUpdate(EntityUid uid, SiliconDownOnDeadComponen if (args.ChargePercent == 0 && !siliconDeadComp.Dead) SiliconDead(uid, siliconDeadComp, batteryComp, uid); else if (args.ChargePercent != 0 && siliconDeadComp.Dead) - SiliconUnDead(uid, siliconDeadComp, batteryComp, uid); + SiliconUnDead(uid, siliconDeadComp, batteryComp, uid); } private void SiliconDead(EntityUid uid, SiliconDownOnDeadComponent siliconDeadComp, BatteryComponent? batteryComp, EntityUid batteryUid) @@ -54,7 +49,7 @@ private void SiliconDead(EntityUid uid, SiliconDownOnDeadComponent siliconDeadCo EntityManager.EnsureComponent(uid); EntityManager.EnsureComponent(uid); - if (TryComp(uid, out HumanoidAppearanceComponent? humanoidAppearanceComponent)) + if (TryComp(uid, out var humanoidAppearanceComponent)) { var layers = HumanoidVisualLayersExtension.Sublayers(HumanoidVisualLayers.HeadSide); _humanoidAppearanceSystem.SetLayersVisibility(uid, layers, false, true, humanoidAppearanceComponent); diff --git a/Content.Server/Silicon/Charge/Systems/SiliconChargeSystem.cs b/Content.Server/Silicon/Charge/Systems/SiliconChargeSystem.cs index de50c828bd7..d8b034a69f5 100644 --- a/Content.Server/Silicon/Charge/Systems/SiliconChargeSystem.cs +++ b/Content.Server/Silicon/Charge/Systems/SiliconChargeSystem.cs @@ -10,20 +10,16 @@ using Content.Shared.Silicon.Systems; using Content.Shared.Movement.Systems; using Content.Server.Body.Components; -using Content.Server.Power.EntitySystems; using Robust.Shared.Containers; using Content.Shared.Mind.Components; using System.Diagnostics.CodeAnalysis; using Content.Server.PowerCell; using Robust.Shared.Timing; using Robust.Shared.Configuration; -using Robust.Shared.Audio.Systems; using Robust.Shared.Utility; using Content.Shared.CCVar; using Content.Shared.PowerCell.Components; -using Content.Shared.Mind; using Content.Shared.Alert; -using Content.Server.Silicon.Death; namespace Content.Server.Silicon.Charge; @@ -34,7 +30,6 @@ public sealed class SiliconChargeSystem : EntitySystem [Dependency] private readonly FlammableSystem _flammable = default!; [Dependency] private readonly PopupSystem _popup = default!; [Dependency] private readonly MovementSpeedModifierSystem _moveMod = default!; - [Dependency] private readonly SharedContainerSystem _container = default!; [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly IConfigurationManager _config = default!; [Dependency] private readonly PowerCellSystem _powerCell = default!; @@ -49,27 +44,23 @@ public override void Initialize() public bool TryGetSiliconBattery(EntityUid silicon, [NotNullWhen(true)] out BatteryComponent? batteryComp) { batteryComp = null; - if (!TryComp(silicon, out SiliconComponent? siliconComp)) + if (!HasComp(silicon)) return false; - // try get a battery directly on the inserted entity if (TryComp(silicon, out batteryComp) || _powerCell.TryGetBatteryFromSlot(silicon, out batteryComp)) return true; - //DebugTools.Assert("SiliconComponent does not contain Battery"); return false; } private void OnSiliconStartup(EntityUid uid, SiliconComponent component, ComponentStartup args) { - if (!TryComp(uid, out PowerCellSlotComponent? batterySlot)) + if (!HasComp(uid)) return; - var container = _container.GetContainer(uid, batterySlot.CellSlotId); - if (component.EntityType.GetType() != typeof(SiliconType)) DebugTools.Assert("SiliconComponent.EntityType is not a SiliconType enum."); } @@ -82,7 +73,8 @@ public override void Update(float frameTime) var query = EntityQueryEnumerator(); while (query.MoveNext(out var silicon, out var siliconComp)) { - if (!siliconComp.BatteryPowered) + if (_mobState.IsDead(silicon) + || !siliconComp.BatteryPowered) continue; // Check if the Silicon is an NPC, and if so, follow the delay as specified in the CVAR. @@ -107,20 +99,11 @@ public override void Update(float frameTime) continue; } - // If the silicon is dead, skip it. - if (_mobState.IsDead(silicon)) + // If the silicon ghosted or is SSD while still being powered, skip it. + if (TryComp(silicon, out var mindContComp) + && !mindContComp.HasMind) continue; - // If the silicon ghosted or is SSD while still being powered, skip it. - DeltaV - if (EntityManager.TryGetComponent(silicon, out var mindContComp) - && EntityManager.TryGetComponent(silicon, out var siliconDeathComp)) - { - if ((mindContComp.HasMind == false || CompOrNull(mindContComp.Mind)?.Session == null) && !siliconDeathComp.Dead) - { - continue; - } - } - var drainRate = siliconComp.DrainPerSecond; // All multipliers will be subtracted by 1, and then added together, and then multiplied by the drain rate. This is then added to the base drain rate. @@ -131,7 +114,7 @@ public override void Update(float frameTime) // Maybe use something similar to refreshmovespeedmodifiers, where it's stored in the component. // Maybe it doesn't matter, and stuff should just use static drain? if (!siliconComp.EntityType.Equals(SiliconType.Npc)) // Don't bother checking heat if it's an NPC. It's a waste of time, and it'd be delayed due to the update time. - drainRateFinalAddi += SiliconHeatEffects(silicon, frameTime) - 1; // This will need to be changed at some point if we allow external batteries, since the heat of the Silicon might not be applicable. + drainRateFinalAddi += SiliconHeatEffects(silicon, siliconComp, frameTime) - 1; // This will need to be changed at some point if we allow external batteries, since the heat of the Silicon might not be applicable. // Ensures that the drain rate is at least 10% of normal, // and would allow at least 4 minutes of life with a max charge, to prevent cheese. @@ -166,14 +149,12 @@ public void UpdateChargeState(EntityUid uid, short chargePercent, SiliconCompone } } - private float SiliconHeatEffects(EntityUid silicon, float frameTime) + private float SiliconHeatEffects(EntityUid silicon, SiliconComponent siliconComp, float frameTime) { - if (!EntityManager.TryGetComponent(silicon, out var temperComp) - || !EntityManager.TryGetComponent(silicon, out var thermalComp)) + if (!TryComp(silicon, out var temperComp) + || !TryComp(silicon, out var thermalComp)) return 0; - var siliconComp = EntityManager.GetComponent(silicon); - // If the Silicon is hot, drain the battery faster, if it's cold, drain it slower, capped. var upperThresh = thermalComp.NormalBodyTemperature + thermalComp.ThermalRegulationTemperatureThreshold; var upperThreshHalf = thermalComp.NormalBodyTemperature + thermalComp.ThermalRegulationTemperatureThreshold * 0.5f; @@ -198,12 +179,11 @@ private float SiliconHeatEffects(EntityUid silicon, float frameTime) return hotTempMulti; _popup.PopupEntity(Loc.GetString("silicon-overheating"), silicon, silicon, PopupType.MediumCaution); - if (_random.Prob(Math.Clamp(temperComp.CurrentTemperature / (upperThresh * 5), 0.001f, 0.9f))) - { - //MaximumFireStacks and MinimumFireStacks doesn't exists on EE - _flammable.AdjustFireStacks(silicon, Math.Clamp(siliconComp.FireStackMultiplier, -10, 10), flamComp); - _flammable.Ignite(silicon, silicon, flamComp); - } + if (!_random.Prob(Math.Clamp(temperComp.CurrentTemperature / (upperThresh * 5), 0.001f, 0.9f))) + return hotTempMulti; + + _flammable.AdjustFireStacks(silicon, Math.Clamp(siliconComp.FireStackMultiplier, -10, 10), flamComp); + _flammable.Ignite(silicon, silicon, flamComp); return hotTempMulti; } diff --git a/Content.Server/Silicon/DeadStartupButtonSystem/DeadStartupButtonSystem.cs b/Content.Server/Silicon/DeadStartupButtonSystem/DeadStartupButtonSystem.cs index a25f2a22545..1c43a3cff91 100644 --- a/Content.Server/Silicon/DeadStartupButtonSystem/DeadStartupButtonSystem.cs +++ b/Content.Server/Silicon/DeadStartupButtonSystem/DeadStartupButtonSystem.cs @@ -1,4 +1,3 @@ -using Content.Server.Chat.Systems; using Content.Server.Lightning; using Content.Server.Popups; using Content.Server.PowerCell; @@ -25,7 +24,6 @@ public sealed class DeadStartupButtonSystem : SharedDeadStartupButtonSystem [Dependency] private readonly LightningSystem _lightning = default!; [Dependency] private readonly SiliconChargeSystem _siliconChargeSystem = default!; [Dependency] private readonly PowerCellSystem _powerCell = default!; - [Dependency] private readonly ChatSystem _chatSystem = default!; /// public override void Initialize() @@ -39,14 +37,13 @@ public override void Initialize() private void OnDoAfter(EntityUid uid, DeadStartupButtonComponent comp, OnDoAfterButtonPressedEvent args) { if (args.Handled || args.Cancelled - || !TryComp(uid, out MobStateComponent? mobStateComponent) + || !TryComp(uid, out var mobStateComponent) || !_mobState.IsDead(uid, mobStateComponent) - || !TryComp(uid, out MobThresholdsComponent? mobThresholdsComponent) - || !TryComp(uid, out DamageableComponent? damageable)) + || !TryComp(uid, out var mobThresholdsComponent) + || !TryComp(uid, out var damageable) + || !_mobThreshold.TryGetThresholdForState(uid, MobState.Critical, out var criticalThreshold, mobThresholdsComponent)) return; - var criticalThreshold = _mobThreshold.GetThresholdForState(uid, MobState.Critical, mobThresholdsComponent); - if (damageable.TotalDamage < criticalThreshold) _mobState.ChangeMobState(uid, MobState.Alive, mobStateComponent); else @@ -59,9 +56,9 @@ private void OnDoAfter(EntityUid uid, DeadStartupButtonComponent comp, OnDoAfter private void OnElectrocuted(EntityUid uid, DeadStartupButtonComponent comp, ElectrocutedEvent args) { - if (!TryComp(uid, out MobStateComponent? mobStateComponent) + if (!TryComp(uid, out var mobStateComponent) || !_mobState.IsDead(uid, mobStateComponent) - || !_siliconChargeSystem.TryGetSiliconBattery(uid, out var bateria) + || !_siliconChargeSystem.TryGetSiliconBattery(uid, out var bateria) || bateria.CurrentCharge <= 0) return; @@ -72,13 +69,10 @@ private void OnElectrocuted(EntityUid uid, DeadStartupButtonComponent comp, Elec private void OnMobStateChanged(EntityUid uid, DeadStartupButtonComponent comp, MobStateChangedEvent args) { + if (args.NewMobState != MobState.Alive) + return; - if (args.NewMobState == MobState.Alive) - { - _popup.PopupEntity(Loc.GetString("dead-startup-system-reboot-success", ("target", MetaData(uid).EntityName)), uid); - _audio.PlayPvs(comp.Sound, uid); - } - + _popup.PopupEntity(Loc.GetString("dead-startup-system-reboot-success", ("target", MetaData(uid).EntityName)), uid); + _audio.PlayPvs(comp.Sound, uid); } - } diff --git a/Content.Server/Silicon/EmitBuzzWhileDamaged/EmitBuzzWhileDamagedSystem.cs b/Content.Server/Silicon/EmitBuzzWhileDamaged/EmitBuzzWhileDamagedSystem.cs index 05e5f5af89b..161a912d061 100644 --- a/Content.Server/Silicon/EmitBuzzWhileDamaged/EmitBuzzWhileDamagedSystem.cs +++ b/Content.Server/Silicon/EmitBuzzWhileDamaged/EmitBuzzWhileDamagedSystem.cs @@ -1,13 +1,13 @@ using Content.Server.Popups; using Content.Shared.Silicon.EmitBuzzWhileDamaged; using Content.Shared.Audio; -using Content.Shared.Body.Components; using Content.Shared.Damage; using Content.Shared.Mobs; using Content.Shared.Mobs.Systems; using Robust.Shared.Audio.Systems; using Robust.Shared.Random; using Robust.Shared.Timing; +using Content.Shared.Mobs.Components; namespace Content.Server.Silicon.EmitBuzzOnCrit; @@ -27,34 +27,29 @@ public override void Update(float frameTime) { base.Update(frameTime); - var query = EntityQueryEnumerator(); + var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var emitBuzzOnCritComponent, out var body)) + while (query.MoveNext(out var uid, out var emitBuzzOnCritComponent, out var mobStateComponent, out var thresholdsComponent, out var damageableComponent)) { - - if (_mobState.IsDead(uid) - || !_mobThreshold.TryGetThresholdForState(uid, MobState.Critical, out var threshold) - || !TryComp(uid, out DamageableComponent? damageableComponent) - || damageableComponent.TotalDamage < (threshold/2)) + if (_mobState.IsDead(uid, mobStateComponent) + || !_mobThreshold.TryGetThresholdForState(uid, MobState.Critical, out var threshold, thresholdsComponent) + || damageableComponent.TotalDamage < threshold / 2) continue; - + // Check update time emitBuzzOnCritComponent.AccumulatedFrametime += frameTime; - if (emitBuzzOnCritComponent.AccumulatedFrametime < emitBuzzOnCritComponent.CycleDelay) continue; emitBuzzOnCritComponent.AccumulatedFrametime -= emitBuzzOnCritComponent.CycleDelay; + if (_gameTiming.CurTime <= emitBuzzOnCritComponent.LastBuzzPopupTime + emitBuzzOnCritComponent.BuzzPopupCooldown) + continue; - // start buzzing - if (_gameTiming.CurTime >= emitBuzzOnCritComponent.LastBuzzPopupTime + emitBuzzOnCritComponent.BuzzPopupCooldown) - { - emitBuzzOnCritComponent.LastBuzzPopupTime = _gameTiming.CurTime; - _popupSystem.PopupEntity(Loc.GetString("silicon-behavior-buzz"), uid); - Spawn("EffectSparks", Transform(uid).Coordinates); - _audio.PlayPvs(emitBuzzOnCritComponent.Sound, uid, AudioHelpers.WithVariation(0.05f, _robustRandom)); - } + // Start buzzing + emitBuzzOnCritComponent.LastBuzzPopupTime = _gameTiming.CurTime; + _popupSystem.PopupEntity(Loc.GetString("silicon-behavior-buzz"), uid); + Spawn("EffectSparks", Transform(uid).Coordinates); + _audio.PlayPvs(emitBuzzOnCritComponent.Sound, uid, AudioHelpers.WithVariation(0.05f, _robustRandom)); } } - } diff --git a/Content.Server/Silicon/EncryptionHolderRequiresLock/EncryptionHolderRequiresLockSystem.cs b/Content.Server/Silicon/EncryptionHolderRequiresLock/EncryptionHolderRequiresLockSystem.cs index 5eb4f25c49b..6efb51096c3 100644 --- a/Content.Server/Silicon/EncryptionHolderRequiresLock/EncryptionHolderRequiresLockSystem.cs +++ b/Content.Server/Silicon/EncryptionHolderRequiresLock/EncryptionHolderRequiresLockSystem.cs @@ -1,4 +1,3 @@ -using Content.Shared.Containers.ItemSlots; using Content.Shared.Lock; using Content.Shared.Radio.Components; using Content.Shared.Radio.EntitySystems; @@ -8,7 +7,6 @@ namespace Content.Server.Silicon.EncryptionHolderRequiresLock; public sealed class EncryptionHolderRequiresLockSystem : EntitySystem { - [Dependency] private readonly ItemSlotsSystem _itemSlotsSystem = default!; [Dependency] private readonly EncryptionKeySystem _encryptionKeySystem = default!; /// @@ -16,11 +14,11 @@ public override void Initialize() { base.Initialize(); SubscribeLocalEvent(LockToggled); - } + private void LockToggled(EntityUid uid, EncryptionHolderRequiresLockComponent component, LockToggledEvent args) { - if (!TryComp(uid, out var lockComp) + if (!TryComp(uid, out var lockComp) || !TryComp(uid, out var keyHolder)) return; diff --git a/Content.Server/Silicon/IPC/InternalEncryptionKeySpawner.cs b/Content.Server/Silicon/IPC/InternalEncryptionKeySpawner.cs index eb01409e854..7f5d216c92f 100644 --- a/Content.Server/Silicon/IPC/InternalEncryptionKeySpawner.cs +++ b/Content.Server/Silicon/IPC/InternalEncryptionKeySpawner.cs @@ -1,39 +1,31 @@ -using Content.Shared.Preferences; using Content.Shared.Roles; using Content.Shared.Radio.Components; using Content.Shared.Containers; using Robust.Shared.Containers; +using Content.Server.Cargo.Components; - // Pretty much copied from StationSpawningSystem.SpawnStartingGear namespace Content.Server.Silicon.IPC; -public static class InternalEncryptionKeySpawner +public sealed partial class InternalEncryptionKeySpawner : EntitySystem { - public static void TryInsertEncryptionKey(EntityUid target, StartingGearPrototype startingGear, IEntityManager entityManager, HumanoidCharacterProfile? profile) + [Dependency] private readonly SharedContainerSystem _container = default!; + public void TryInsertEncryptionKey(EntityUid target, StartingGearPrototype startingGear, IEntityManager entityManager) { - if (entityManager.TryGetComponent(target, out var keyHolderComp)) - { - var earEquipString = startingGear.GetGear("ears", profile); - var containerMan = entityManager.System(); + if (!TryComp(target, out var keyHolderComp) + || !startingGear.Equipment.TryGetValue("ears", out var earEquipString) + || string.IsNullOrEmpty(earEquipString)) + return; - if (!string.IsNullOrEmpty(earEquipString)) - { - var earEntity = entityManager.SpawnEntity(earEquipString, entityManager.GetComponent(target).Coordinates); + var earEntity = entityManager.SpawnEntity(earEquipString, entityManager.GetComponent(target).Coordinates); + if (!entityManager.HasComponent(earEntity) + || !entityManager.TryGetComponent(earEntity, out var fillComp) + || !fillComp.Containers.TryGetValue(EncryptionKeyHolderComponent.KeyContainerName, out var defaultKeys)) + return; - if (entityManager.TryGetComponent(earEntity, out _) && // I had initially wanted this to spawn the headset, and simply move all the keys over, but the headset didn't seem to have any keys in it when spawned... - entityManager.TryGetComponent(earEntity, out var fillComp) && - fillComp.Containers.TryGetValue(EncryptionKeyHolderComponent.KeyContainerName, out var defaultKeys)) - { - containerMan.CleanContainer(keyHolderComp.KeyContainer); + _container.CleanContainer(keyHolderComp.KeyContainer); - foreach (var key in defaultKeys) - { - var keyEntity = entityManager.SpawnEntity(key, entityManager.GetComponent(target).Coordinates); - containerMan.Insert(keyEntity, keyHolderComp.KeyContainer, force: true); - } - } + foreach (var key in defaultKeys) + entityManager.SpawnInContainerOrDrop(key, target, keyHolderComp.KeyContainer.ID, out _); - entityManager.QueueDeleteEntity(earEntity); - } - } + entityManager.QueueDeleteEntity(earEntity); } } diff --git a/Content.Server/Silicon/Systems/SiliconMiscSystem.cs b/Content.Server/Silicon/Systems/SiliconMiscSystem.cs deleted file mode 100644 index 85d76c77bc8..00000000000 --- a/Content.Server/Silicon/Systems/SiliconMiscSystem.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Content.Shared.Silicon.Components; -using Content.Shared.Bed.Sleep; - -namespace Content.Server.Silicon.Misc; - -// This entire thing is fucking stupid and I hate it. -public sealed class SiliconMiscSystem : EntitySystem -{ - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnTryingToSleep); - } - - private void OnTryingToSleep(EntityUid uid, SiliconComponent component, ref TryingToSleepEvent args) - { - args.Cancelled = true; - } -} diff --git a/Content.Server/Silicon/WeldingHealable/WeldingHealableComponent.cs b/Content.Server/Silicon/WeldingHealable/WeldingHealableComponent.cs index 115e9308707..9f4b6c3483f 100644 --- a/Content.Server/Silicon/WeldingHealable/WeldingHealableComponent.cs +++ b/Content.Server/Silicon/WeldingHealable/WeldingHealableComponent.cs @@ -1,9 +1,5 @@ -using Content.Shared.Damage; -using Content.Shared.Tools; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; +namespace Content.Server.Silicon.WeldingHealable; + +[RegisterComponent] +public sealed partial class WeldingHealableComponent : Component { } -namespace Content.Server.Silicon.WeldingHealable -{ - [RegisterComponent] - public sealed partial class WeldingHealableComponent : Component { } -} diff --git a/Content.Server/Silicon/WeldingHealable/WeldingHealableSystem.cs b/Content.Server/Silicon/WeldingHealable/WeldingHealableSystem.cs index 09a2ee99fcd..e0783db0c8b 100644 --- a/Content.Server/Silicon/WeldingHealable/WeldingHealableSystem.cs +++ b/Content.Server/Silicon/WeldingHealable/WeldingHealableSystem.cs @@ -1,121 +1,102 @@ -using System.Diagnostics; using Content.Server.Silicon.WeldingHealing; -using Content.Server.Administration.Logs; -using Content.Server.Stack; using Content.Server.Tools.Components; using Content.Shared.Silicon.WeldingHealing; -using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.Components.SolutionManager; using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Damage; -using Content.Shared.Database; -using Content.Shared.Eye.Blinding.Components; -using Content.Shared.Eye.Blinding.Systems; -using Content.Shared.FixedPoint; using Content.Shared.Interaction; using Content.Shared.Popups; -using Content.Shared.Tools; -using Content.Shared.Stacks; using SharedToolSystem = Content.Shared.Tools.Systems.SharedToolSystem; -namespace Content.Server.Silicon.WeldingHealable -{ - public sealed class WeldingHealableSystem : SharedWeldingHealableSystem - { - [Dependency] private readonly SharedToolSystem _toolSystem = default!; - [Dependency] private readonly DamageableSystem _damageableSystem = default!; - [Dependency] private readonly SharedPopupSystem _popup = default!; - [Dependency] private readonly IAdminLogManager _adminLogger= default!; - [Dependency] private readonly SharedSolutionContainerSystem _solutionContainer= default!; - - public override void Initialize() - { - SubscribeLocalEvent(Repair); - SubscribeLocalEvent(OnRepairFinished); - } - - private void OnRepairFinished(EntityUid uid, WeldingHealableComponent healableComponentcomponent, SiliconRepairFinishedEvent args) - { - if (args.Cancelled || args.Used == null - || !EntityManager.TryGetComponent(args.Target, out DamageableComponent? damageable) - || !EntityManager.TryGetComponent(args.Used, out WeldingHealingComponent? component) - || damageable.DamageContainerID != null - && !component.DamageContainers.Contains(damageable.DamageContainerID)) - return; - - var damageChanged = _damageableSystem.TryChangeDamage(uid, component.Damage, true, false, origin: args.User); - - - if (!HasDamage(damageable, component)) - return; - - if (TryComp(args.Used, out WelderComponent? welder) && - TryComp(args.Used, out SolutionContainerManagerComponent? solutionContainer)) - { - if (!_solutionContainer.ResolveSolution(((EntityUid) args.Used, solutionContainer), welder.FuelSolutionName, ref welder.FuelSolution, out var solution)) - return; - _solutionContainer.RemoveReagent(welder.FuelSolution.Value, welder.FuelReagent, component.FuelCost); - } - - var str = Loc.GetString("comp-repairable-repair", - ("target", uid), - ("tool", args.Used!)); - _popup.PopupEntity(str, uid, args.User); - - - if (args.Used.HasValue) - { - args.Handled = _toolSystem.UseTool(args.Used.Value, args.User, uid, args.Delay, component.QualityNeeded, new SiliconRepairFinishedEvent - { - Delay = args.Delay - }); - } - } - - +namespace Content.Server.Silicon.WeldingHealable; - private async void Repair(EntityUid uid, WeldingHealableComponent healableComponent, InteractUsingEvent args) - { - if (args.Handled - || !EntityManager.TryGetComponent(args.Used, out WeldingHealingComponent? component) - || !EntityManager.TryGetComponent(args.Target, out DamageableComponent? damageable) - || damageable.DamageContainerID != null - && !component.DamageContainers.Contains(damageable.DamageContainerID) - || !HasDamage(damageable, component) - || !_toolSystem.HasQuality(args.Used, component.QualityNeeded)) - return; +public sealed class WeldingHealableSystem : SharedWeldingHealableSystem +{ + [Dependency] private readonly SharedToolSystem _toolSystem = default!; + [Dependency] private readonly DamageableSystem _damageableSystem = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!; - float delay = component.DoAfterDelay; + public override void Initialize() + { + SubscribeLocalEvent(Repair); + SubscribeLocalEvent(OnRepairFinished); + } - // Add a penalty to how long it takes if the user is repairing itself - if (args.User == args.Target) + private void OnRepairFinished(EntityUid uid, WeldingHealableComponent healableComponent, SiliconRepairFinishedEvent args) + { + if (args.Cancelled || args.Used == null + || !TryComp(args.Target, out var damageable) + || !TryComp(args.Used, out var component) + || damageable.DamageContainerID is null + || !component.DamageContainers.Contains(damageable.DamageContainerID) + || !HasDamage(damageable, component) + || !TryComp(args.Used, out var welder) + || !TryComp(args.Used, out var solutionContainer) + || !_solutionContainer.ResolveSolution(((EntityUid) args.Used, solutionContainer), welder.FuelSolutionName, ref welder.FuelSolution)) + return; + + _damageableSystem.TryChangeDamage(uid, component.Damage, true, false, origin: args.User); + + _solutionContainer.RemoveReagent(welder.FuelSolution.Value, welder.FuelReagent, component.FuelCost); + + var str = Loc.GetString("comp-repairable-repair", + ("target", uid), + ("tool", args.Used!)); + _popup.PopupEntity(str, uid, args.User); + + if (!args.Used.HasValue) + return; + + args.Handled = _toolSystem.UseTool + (args.Used.Value, + args.User, + uid, + args.Delay, + component.QualityNeeded, + new SiliconRepairFinishedEvent { - if (!component.AllowSelfHeal) - return; - - delay *= component.SelfHealPenalty; - } + Delay = args.Delay + }); + } - // Run the repairing doafter - args.Handled = _toolSystem.UseTool(args.Used, args.User, args.Target, delay, component.QualityNeeded, new SiliconRepairFinishedEvent + private async void Repair(EntityUid uid, WeldingHealableComponent healableComponent, InteractUsingEvent args) + { + if (args.Handled + || !EntityManager.TryGetComponent(args.Used, out WeldingHealingComponent? component) + || !EntityManager.TryGetComponent(args.Target, out DamageableComponent? damageable) + || damageable.DamageContainerID is null + || !component.DamageContainers.Contains(damageable.DamageContainerID) + || !HasDamage(damageable, component) + || !_toolSystem.HasQuality(args.Used, component.QualityNeeded) + || args.User == args.Target && !component.AllowSelfHeal) + return; + + float delay = args.User == args.Target + ? component.DoAfterDelay * component.SelfHealPenalty + : component.DoAfterDelay; + + args.Handled = _toolSystem.UseTool + (args.Used, + args.User, + args.Target, + delay, + component.QualityNeeded, + new SiliconRepairFinishedEvent { Delay = delay, }); + } - } - private bool HasDamage(DamageableComponent component, WeldingHealingComponent healable) - { - var damageableDict = component.Damage.DamageDict; - var healingDict = healable.Damage.DamageDict; - foreach (var type in healingDict) - { - if (damageableDict[type.Key].Value > 0) - { - return true; - } - } - + private bool HasDamage(DamageableComponent component, WeldingHealingComponent healable) + { + if (healable.Damage.DamageDict is null) return false; - } + + foreach (var type in healable.Damage.DamageDict) + if (component.Damage.DamageDict[type.Key].Value > 0) + return true; + return false; } } + diff --git a/Content.Server/Silicon/WeldingHealing/WeldingHealingComponent.cs b/Content.Server/Silicon/WeldingHealing/WeldingHealingComponent.cs index c381d547180..a7aa1707938 100644 --- a/Content.Server/Silicon/WeldingHealing/WeldingHealingComponent.cs +++ b/Content.Server/Silicon/WeldingHealing/WeldingHealingComponent.cs @@ -2,47 +2,46 @@ using Content.Shared.Tools; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; -namespace Content.Server.Silicon.WeldingHealing +namespace Content.Server.Silicon.WeldingHealing; + +[RegisterComponent] +public sealed partial class WeldingHealingComponent : Component { - [RegisterComponent] - public sealed partial class WeldingHealingComponent : Component - { - /// - /// All the damage to change information is stored in this . - /// - /// - /// If this data-field is specified, it will change damage by this amount instead of setting all damage to 0. - /// in order to heal/repair the damage values have to be negative. - /// - - [DataField(required: true)] - public DamageSpecifier Damage; - - [DataField(customTypeSerializer:typeof(PrototypeIdSerializer))] - public string QualityNeeded = "Welding"; - - /// - /// The fuel amount needed to repair physical related damage - /// - [DataField] - public int FuelCost = 5; - - [DataField] - public int DoAfterDelay = 3; - - /// - /// A multiplier that will be applied to the above if an entity is repairing themselves. - /// - [DataField] - public float SelfHealPenalty = 3f; - - /// - /// Whether or not an entity is allowed to repair itself. - /// - [DataField] - public bool AllowSelfHeal = true; - - [DataField(required: true)] - public List DamageContainers; - } + /// + /// All the damage to change information is stored in this . + /// + /// + /// If this data-field is specified, it will change damage by this amount instead of setting all damage to 0. + /// in order to heal/repair the damage values have to be negative. + /// + + [DataField(required: true)] + public DamageSpecifier Damage; + + [DataField(customTypeSerializer:typeof(PrototypeIdSerializer))] + public string QualityNeeded = "Welding"; + + /// + /// The fuel amount needed to repair physical related damage + /// + [DataField] + public int FuelCost = 5; + + [DataField] + public int DoAfterDelay = 3; + + /// + /// A multiplier that will be applied to the above if an entity is repairing themselves. + /// + [DataField] + public float SelfHealPenalty = 3f; + + /// + /// Whether or not an entity is allowed to repair itself. + /// + [DataField] + public bool AllowSelfHeal = true; + + [DataField(required: true)] + public List DamageContainers; } diff --git a/Content.Server/Station/Systems/StationSpawningSystem.cs b/Content.Server/Station/Systems/StationSpawningSystem.cs index 61fc1e0a51b..92f16de5c73 100644 --- a/Content.Server/Station/Systems/StationSpawningSystem.cs +++ b/Content.Server/Station/Systems/StationSpawningSystem.cs @@ -47,7 +47,7 @@ public sealed class StationSpawningSystem : SharedStationSpawningSystem [Dependency] private readonly SharedAccessSystem _accessSystem = default!; [Dependency] private readonly IdentitySystem _identity = default!; [Dependency] private readonly MetaDataSystem _metaSystem = default!; - + [Dependency] private readonly InternalEncryptionKeySpawner _internalEncryption = default!; [Dependency] private readonly ArrivalsSystem _arrivalsSystem = default!; [Dependency] private readonly ContainerSpawnPointSystem _containerSpawnPointSystem = default!; @@ -176,7 +176,7 @@ public EntityUid SpawnPlayerMob( EquipStartingGear(entity.Value, startingGear); if (profile != null) EquipIdCard(entity.Value, profile.Name, prototype, station); - InternalEncryptionKeySpawner.TryInsertEncryptionKey(entity.Value, startingGear, EntityManager, profile); // Parkstation - IPC + _internalEncryption.TryInsertEncryptionKey(entity.Value, startingGear, EntityManager); } if (profile != null) diff --git a/Content.Shared/Lock/LockComponent.cs b/Content.Shared/Lock/LockComponent.cs index 9606540a74a..875451b5a14 100644 --- a/Content.Shared/Lock/LockComponent.cs +++ b/Content.Shared/Lock/LockComponent.cs @@ -14,88 +14,82 @@ namespace Content.Shared.Lock; public sealed partial class LockComponent : Component { /// - /// Whether or not the lock is locked. + /// Whether or not the lock is locked. /// - [DataField("locked"), ViewVariables(VVAccess.ReadWrite)] - [AutoNetworkedField] - public bool Locked = true; + [DataField, AutoNetworkedField] + public bool Locked = true; /// - /// Whether or not the lock is toggled by simply clicking. + /// Whether or not the lock is toggled by simply clicking. /// - [DataField("lockOnClick"), ViewVariables(VVAccess.ReadWrite)] - [AutoNetworkedField] + [DataField, AutoNetworkedField] public bool LockOnClick; /// - /// Whether or not the lock is unlocked by simply clicking. + /// Whether or not the lock is unlocked by simply clicking. /// - [DataField("unlockOnClick"), ViewVariables(VVAccess.ReadWrite)] - [AutoNetworkedField] + [DataField, AutoNetworkedField] public bool UnlockOnClick = true; /// - /// The sound played when unlocked. + /// The sound played when unlocked. /// - [DataField("unlockingSound"), ViewVariables(VVAccess.ReadWrite)] + [DataField] public SoundSpecifier UnlockSound = new SoundPathSpecifier("/Audio/Machines/door_lock_off.ogg") { Params = AudioParams.Default.WithVolume(-5f), }; /// - /// The sound played when locked. + /// The sound played when locked. /// - [DataField("lockingSound"), ViewVariables(VVAccess.ReadWrite)] + [DataField] public SoundSpecifier LockSound = new SoundPathSpecifier("/Audio/Machines/door_lock_on.ogg") { Params = AudioParams.Default.WithVolume(-5f) }; /// - /// Whether or not an emag disables it. + /// Whether or not an emag disables it. /// - [DataField("breakOnEmag")] - [AutoNetworkedField] + [DataField, AutoNetworkedField] public bool BreakOnEmag = true; /// - /// Amount of do-after time needed to lock the entity. + /// Amount of do-after time needed to lock the entity. /// /// - /// If set to zero, no do-after will be used. + /// If set to zero, no do-after will be used. /// - [DataField] - [AutoNetworkedField] + [DataField, AutoNetworkedField] public TimeSpan LockTime; /// - /// Amount of do-after time needed to unlock the entity. + /// Amount of do-after time needed to unlock the entity. /// /// - /// If set to zero, no do-after will be used. + /// If set to zero, no do-after will be used. /// - [DataField] - [AutoNetworkedField] + [DataField, AutoNetworkedField] public TimeSpan UnlockTime; } /// -/// Event raised on the lock when a toggle is attempted. -/// Can be cancelled to prevent it. +/// Event raised on the lock when a toggle is attempted. +/// Can be cancelled to prevent it. /// [ByRefEvent] public record struct LockToggleAttemptEvent(EntityUid User, bool Silent = false, bool Cancelled = false); /// -/// Event raised on a lock after it has been toggled. +/// Event raised on a lock after it has been toggled. /// [ByRefEvent] public readonly record struct LockToggledEvent(bool Locked); /// -/// Used to lock a lockable entity that has a lock time configured. +/// Used to lock a lockable entity that has a lock time configured. /// /// /// @@ -109,7 +103,7 @@ public override DoAfterEvent Clone() } /// -/// Used to unlock a lockable entity that has an unlock time configured. +/// Used to unlock a lockable entity that has an unlock time configured. /// /// /// diff --git a/Content.Shared/Lock/LockSystem.cs b/Content.Shared/Lock/LockSystem.cs index b74f17b1525..bd533663876 100644 --- a/Content.Shared/Lock/LockSystem.cs +++ b/Content.Shared/Lock/LockSystem.cs @@ -54,7 +54,6 @@ private void OnStartup(EntityUid uid, LockComponent lockComp, ComponentStartup a private void OnActivated(EntityUid uid, LockComponent lockComp, ActivateInWorldEvent args) { - if (args.Handled) return; diff --git a/Content.Shared/Radio/Components/EncryptionKeyHolderComponent.cs b/Content.Shared/Radio/Components/EncryptionKeyHolderComponent.cs index 6c50acb9660..d84a1618602 100644 --- a/Content.Shared/Radio/Components/EncryptionKeyHolderComponent.cs +++ b/Content.Shared/Radio/Components/EncryptionKeyHolderComponent.cs @@ -15,27 +15,22 @@ public sealed partial class EncryptionKeyHolderComponent : Component /// /// Whether or not encryption keys can be removed from the headset. /// - [ViewVariables(VVAccess.ReadWrite)] - [DataField("keysUnlocked")] + [DataField] public bool KeysUnlocked = true; /// /// The tool required to extract the encryption keys from the headset. /// - [ViewVariables(VVAccess.ReadWrite)] - [DataField("keysExtractionMethod", customTypeSerializer: typeof(PrototypeIdSerializer))] + [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))] public string KeysExtractionMethod = "Screwing"; - [ViewVariables(VVAccess.ReadWrite)] - [DataField("keySlots")] + [DataField] public int KeySlots = 2; - [ViewVariables(VVAccess.ReadWrite)] - [DataField("keyExtractionSound")] + [DataField] public SoundSpecifier KeyExtractionSound = new SoundPathSpecifier("/Audio/Items/pistol_magout.ogg"); - [ViewVariables(VVAccess.ReadWrite)] - [DataField("keyInsertionSound")] + [DataField] public SoundSpecifier KeyInsertionSound = new SoundPathSpecifier("/Audio/Items/pistol_magin.ogg"); [ViewVariables] @@ -45,8 +40,7 @@ public sealed partial class EncryptionKeyHolderComponent : Component /// /// Whether or not the headset can be examined to see the encryption keys while the keys aren't accessible. /// - [ViewVariables(VVAccess.ReadWrite)] - [DataField("examineWhileLocked")] + [DataField] public bool ExamineWhileLocked = true; /// diff --git a/Content.Shared/Radio/EntitySystems/EncryptionKeySystem.cs b/Content.Shared/Radio/EntitySystems/EncryptionKeySystem.cs index 712debbafa0..a15b6f76e90 100644 --- a/Content.Shared/Radio/EntitySystems/EncryptionKeySystem.cs +++ b/Content.Shared/Radio/EntitySystems/EncryptionKeySystem.cs @@ -6,10 +6,8 @@ using Content.Shared.Interaction; using Content.Shared.Popups; using Content.Shared.Radio.Components; -using Content.Shared.Tools; using Content.Shared.Tools.Components; using Content.Shared.Wires; -using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; using Robust.Shared.Containers; using Robust.Shared.Network; @@ -175,13 +173,9 @@ private void OnStartup(EntityUid uid, EncryptionKeyHolderComponent component, Co private void OnHolderExamined(EntityUid uid, EncryptionKeyHolderComponent component, ExaminedEvent args) { - if (!args.IsInDetailsRange) - return; - - if (!component.ExamineWhileLocked && !component.KeysUnlocked) - return; - - if (!component.ExamineWhileLocked && TryComp(uid, out var panel) && !panel.Open) + if (!args.IsInDetailsRange + || !component.ExamineWhileLocked && !component.KeysUnlocked + || !component.ExamineWhileLocked && TryComp(uid, out var panel) && !panel.Open) return; if (component.KeyContainer.ContainedEntities.Count == 0) diff --git a/Content.Shared/Silicon/BatteryDrinkerEvent.cs b/Content.Shared/Silicon/BatteryDrinkerEvent.cs index 4d9a610055d..99af03df3a1 100644 --- a/Content.Shared/Silicon/BatteryDrinkerEvent.cs +++ b/Content.Shared/Silicon/BatteryDrinkerEvent.cs @@ -6,7 +6,5 @@ namespace Content.Shared.Silicon; [Serializable, NetSerializable] public sealed partial class BatteryDrinkerDoAfterEvent : SimpleDoAfterEvent { - public BatteryDrinkerDoAfterEvent() - { - } + public BatteryDrinkerDoAfterEvent() { } } diff --git a/Content.Shared/Silicon/BlindHealing/SharedBlindHealingSystem.cs b/Content.Shared/Silicon/BlindHealing/SharedBlindHealingSystem.cs index be4be9e5d38..bfc5092b644 100644 --- a/Content.Shared/Silicon/BlindHealing/SharedBlindHealingSystem.cs +++ b/Content.Shared/Silicon/BlindHealing/SharedBlindHealingSystem.cs @@ -6,8 +6,6 @@ namespace Content.Shared.Silicon.BlindHealing; public abstract partial class SharedBlindHealingSystem : EntitySystem { [Serializable, NetSerializable] - protected sealed partial class HealingDoAfterEvent : SimpleDoAfterEvent - { - } + protected sealed partial class HealingDoAfterEvent : SimpleDoAfterEvent { } } diff --git a/Content.Shared/Silicon/Components/SiliconComponent.cs b/Content.Shared/Silicon/Components/SiliconComponent.cs index c80d9397d93..bcee4d161a5 100644 --- a/Content.Shared/Silicon/Components/SiliconComponent.cs +++ b/Content.Shared/Silicon/Components/SiliconComponent.cs @@ -1,7 +1,6 @@ using Robust.Shared.GameStates; using Content.Shared.Silicon.Systems; using Robust.Shared.Serialization.TypeSerializers.Implementations; -using Robust.Shared.Containers; using Robust.Shared.Prototypes; using Content.Shared.Alert; @@ -36,7 +35,7 @@ public sealed partial class SiliconComponent : Component /// /// Is the Silicon currently dead? /// - public bool Dead = false; + public bool Dead; // BatterySystem took issue with how this was used, so I'm coming back to it at a later date, when more foundational Silicon stuff is implemented. // /// @@ -52,7 +51,7 @@ public sealed partial class SiliconComponent : Component /// Any new types of Silicons should be added to the enum. /// Setting this to Npc will delay charge state updates by LastDrainTime and skip battery heat calculations /// - [DataField("entityType", customTypeSerializer: typeof(EnumSerializer))] + [DataField(customTypeSerializer: typeof(EnumSerializer))] public Enum EntityType = SiliconType.Npc; /// @@ -61,13 +60,13 @@ public sealed partial class SiliconComponent : Component /// /// If true, should go along with a battery component. One will not be added automatically. /// - [DataField("batteryPowered"), ViewVariables(VVAccess.ReadWrite)] - public bool BatteryPowered = false; + [DataField] + public bool BatteryPowered; /// /// How much power is drained by this Silicon every second by default. /// - [DataField("drainPerSecond"), ViewVariables(VVAccess.ReadWrite)] + [DataField] public float DrainPerSecond = 50f; @@ -79,15 +78,15 @@ public sealed partial class SiliconComponent : Component /// Setting a value to null will disable that state. /// Setting Critical to 0 will cause the Silicon to never enter the dead state. /// - [DataField("chargeThresholdMid"), ViewVariables(VVAccess.ReadWrite)] + [DataField] public float? ChargeThresholdMid = 0.5f; /// - [DataField("chargeThresholdLow"), ViewVariables(VVAccess.ReadWrite)] + [DataField] public float? ChargeThresholdLow = 0.25f; /// - [DataField("chargeThresholdCritical"), ViewVariables(VVAccess.ReadWrite)] + [DataField] public float? ChargeThresholdCritical = 0.1f; [DataField] @@ -100,9 +99,16 @@ public sealed partial class SiliconComponent : Component /// /// The amount the Silicon will be slowed at each charge state. /// - [DataField("speedModifierThresholds", required: true)] - public Dictionary SpeedModifierThresholds = default!; + [DataField(required: true)] + public Dictionary SpeedModifierThresholds = default!; - [DataField("fireStackMultiplier"), ViewVariables(VVAccess.ReadWrite)] + [DataField] public float FireStackMultiplier = 1f; + + /// + /// Whether or not a Silicon will cancel all sleep events. + /// Maybe you want an android that can sleep as well as drink APCs? I'm not going to judge. + /// + [DataField] + public bool DoSiliconsDreamOfElectricSheep; } diff --git a/Content.Shared/Silicon/DeadStartupButton/DeadStartupButtonComponent.cs b/Content.Shared/Silicon/DeadStartupButton/DeadStartupButtonComponent.cs index 3390a76439b..9c2e5baf573 100644 --- a/Content.Shared/Silicon/DeadStartupButton/DeadStartupButtonComponent.cs +++ b/Content.Shared/Silicon/DeadStartupButton/DeadStartupButtonComponent.cs @@ -3,26 +3,26 @@ namespace Content.Shared.Silicon.DeadStartupButton; /// -/// This is used for... +/// This is used for Silicon entities such as IPCs, Cyborgs, Androids, anything "living" with a button people can touch. /// [RegisterComponent] public sealed partial class DeadStartupButtonComponent : Component { - [DataField("verbText")] + [DataField] public string VerbText = "dead-startup-button-verb"; - [DataField("sound")] + [DataField] public SoundSpecifier Sound = new SoundPathSpecifier("/Audio/Effects/Arcade/newgame.ogg"); - [DataField("buttonSound")] + [DataField] public SoundSpecifier ButtonSound = new SoundPathSpecifier("/Audio/Machines/button.ogg"); - [DataField("doAfterInterval"), ViewVariables(VVAccess.ReadWrite)] + [DataField] public float DoAfterInterval = 1f; - [DataField("buzzSound")] + [DataField] public SoundSpecifier BuzzSound = new SoundCollectionSpecifier("buzzes"); - [DataField("verbPriority"), ViewVariables(VVAccess.ReadWrite)] + [DataField] public int VerbPriority = 1; } diff --git a/Content.Shared/Silicon/DeadStartupButton/SharedDeadStartupButtonSystem.cs b/Content.Shared/Silicon/DeadStartupButton/SharedDeadStartupButtonSystem.cs index 2faa6dfde00..605ca48e6cb 100644 --- a/Content.Shared/Silicon/DeadStartupButton/SharedDeadStartupButtonSystem.cs +++ b/Content.Shared/Silicon/DeadStartupButton/SharedDeadStartupButtonSystem.cs @@ -1,5 +1,4 @@ using Content.Shared.DoAfter; -using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Systems; using Content.Shared.Verbs; using Robust.Shared.Audio.Systems; @@ -10,19 +9,16 @@ namespace Content.Shared.Silicon.DeadStartupButton; /// -/// This creates a Button that can be activated after an entity, usually a silicon or an IPC, died. -/// This will activate a doAfter and then revive the entity, playing a custom afterward sound. +/// This creates a Button that can be activated after an entity, usually a silicon or an IPC, died. +/// This will activate a doAfter and then revive the entity, playing a custom afterward sound. /// -public partial class SharedDeadStartupButtonSystem : EntitySystem +public abstract partial class SharedDeadStartupButtonSystem : EntitySystem { [Dependency] private readonly MobStateSystem _mobState = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!; [Dependency] private readonly INetManager _net = default!; - - - /// public override void Initialize() { @@ -31,10 +27,8 @@ public override void Initialize() private void AddTurnOnVerb(EntityUid uid, DeadStartupButtonComponent component, GetVerbsEvent args) { - if (!args.CanAccess || !args.CanInteract || args.Hands == null) - return; - - if (!TryComp(uid, out MobStateComponent? mobStateComponent) || !_mobState.IsDead(uid, mobStateComponent)) + if (!_mobState.IsDead(uid) + || !args.CanAccess || !args.CanInteract || args.Hands == null) return; args.Verbs.Add(new AlternativeVerb() @@ -50,6 +44,7 @@ private void TryStartup(EntityUid user, EntityUid target, DeadStartupButtonCompo { if (!_net.IsServer) return; + _audio.PlayPvs(comp.ButtonSound, target); var args = new DoAfterArgs(EntityManager, user, comp.DoAfterInterval, new OnDoAfterButtonPressedEvent(), target, target:target) { @@ -60,9 +55,5 @@ private void TryStartup(EntityUid user, EntityUid target, DeadStartupButtonCompo } [Serializable, NetSerializable] - public sealed partial class OnDoAfterButtonPressedEvent : SimpleDoAfterEvent - { - } - - + public sealed partial class OnDoAfterButtonPressedEvent : SimpleDoAfterEvent { } } diff --git a/Content.Shared/Silicon/EmitBuzzWhileDamaged/EmitBuzzWhileDamagedComponent.cs b/Content.Shared/Silicon/EmitBuzzWhileDamaged/EmitBuzzWhileDamagedComponent.cs index a8362610e25..986292551d0 100644 --- a/Content.Shared/Silicon/EmitBuzzWhileDamaged/EmitBuzzWhileDamagedComponent.cs +++ b/Content.Shared/Silicon/EmitBuzzWhileDamaged/EmitBuzzWhileDamagedComponent.cs @@ -1,26 +1,26 @@ -using System.ComponentModel.DataAnnotations; using Robust.Shared.Audio; namespace Content.Shared.Silicon.EmitBuzzWhileDamaged; /// -/// This is used for controlling the cadence of the buzzing emitted by EmitBuzzOnCritSystem. -/// This component is used by mechanical species that can get to critical health. +/// This is used for controlling the cadence of the buzzing emitted by EmitBuzzOnCritSystem. +/// This component is used by mechanical species that can get to critical health. /// [RegisterComponent] public sealed partial class EmitBuzzWhileDamagedComponent : Component { - [DataField("buzzPopupCooldown")] - public TimeSpan BuzzPopupCooldown { get; private set; } = TimeSpan.FromSeconds(8); + [DataField] + public TimeSpan BuzzPopupCooldown = TimeSpan.FromSeconds(8); [ViewVariables] public TimeSpan LastBuzzPopupTime; - [DataField("cycleDelay")] + [DataField] public float CycleDelay = 2.0f; + [ViewVariables] public float AccumulatedFrametime; - [DataField("sound")] + [DataField] public SoundSpecifier Sound = new SoundCollectionSpecifier("buzzes"); } diff --git a/Content.Shared/Silicon/Systems/SharedSiliconSystem.cs b/Content.Shared/Silicon/Systems/SharedSiliconSystem.cs index aab9b6e7522..8fe87e162bc 100644 --- a/Content.Shared/Silicon/Systems/SharedSiliconSystem.cs +++ b/Content.Shared/Silicon/Systems/SharedSiliconSystem.cs @@ -1,17 +1,17 @@ using Content.Shared.Silicon.Components; using Content.Shared.Alert; -using Robust.Shared.Serialization; -using Content.Shared.Movement.Systems; +using Content.Shared.Bed.Sleep; using Content.Shared.Containers.ItemSlots; +using Content.Shared.Movement.Systems; using Content.Shared.PowerCell.Components; +using Robust.Shared.Serialization; namespace Content.Shared.Silicon.Systems; - public sealed class SharedSiliconChargeSystem : EntitySystem { [Dependency] private readonly AlertsSystem _alertsSystem = default!; - [Dependency] protected readonly ItemSlotsSystem ItemSlots = default!; + [Dependency] private readonly ItemSlotsSystem _itemSlots = default!; public override void Initialize() { @@ -22,47 +22,50 @@ public override void Initialize() SubscribeLocalEvent(OnRefreshMovespeed); SubscribeLocalEvent(OnItemSlotInsertAttempt); SubscribeLocalEvent(OnItemSlotEjectAttempt); + SubscribeLocalEvent(OnTryingToSleep); } - private void OnItemSlotInsertAttempt(EntityUid uid, SiliconComponent component, ref ItemSlotInsertAttemptEvent args) + /// + /// Silicon entities can now also be Living player entities. We may want to prevent them from sleeping if they can't sleep. + /// + private void OnTryingToSleep(EntityUid uid, SiliconComponent component, ref TryingToSleepEvent args) { - if (args.Cancelled) - return; - - if (!TryComp(uid, out var cellSlotComp)) - return; + args.Cancelled = !component.DoSiliconsDreamOfElectricSheep; + } - if (!ItemSlots.TryGetSlot(uid, cellSlotComp.CellSlotId, out var cellSlot) || cellSlot != args.Slot) + private void OnItemSlotInsertAttempt(EntityUid uid, SiliconComponent component, ref ItemSlotInsertAttemptEvent args) + { + if (args.Cancelled + || !TryComp(uid, out var cellSlotComp) + || !_itemSlots.TryGetSlot(uid, cellSlotComp.CellSlotId, out var cellSlot) + || cellSlot != args.Slot || args.User != uid) return; - if (args.User == uid) - args.Cancelled = true; + args.Cancelled = true; } private void OnItemSlotEjectAttempt(EntityUid uid, SiliconComponent component, ref ItemSlotEjectAttemptEvent args) { - if (args.Cancelled) - return; - - if (!TryComp(uid, out var cellSlotComp)) - return; - - if (!ItemSlots.TryGetSlot(uid, cellSlotComp.CellSlotId, out var cellSlot) || cellSlot != args.Slot) + if (args.Cancelled + || !TryComp(uid, out var cellSlotComp) + || !_itemSlots.TryGetSlot(uid, cellSlotComp.CellSlotId, out var cellSlot) + || cellSlot != args.Slot || args.User != uid) return; - if (args.User == uid) - args.Cancelled = true; + args.Cancelled = true; } private void OnSiliconInit(EntityUid uid, SiliconComponent component, ComponentInit args) { - if (component.BatteryPowered) - _alertsSystem.ShowAlert(uid, AlertType.BorgBattery, component.ChargeState); + if (!component.BatteryPowered) + return; + + _alertsSystem.ShowAlert(uid, AlertType.BorgBattery, component.ChargeState); } private void OnSiliconChargeStateUpdate(EntityUid uid, SiliconComponent component, SiliconChargeStateUpdateEvent ev) { - _alertsSystem.ShowAlert(uid, AlertType.BorgBattery, (short) ev.ChargePercent); + _alertsSystem.ShowAlert(uid, AlertType.BorgBattery, ev.ChargePercent); } private void OnRefreshMovespeed(EntityUid uid, SiliconComponent component, RefreshMovementSpeedModifiersEvent args) @@ -70,17 +73,12 @@ private void OnRefreshMovespeed(EntityUid uid, SiliconComponent component, Refre if (!component.BatteryPowered) return; - var speedModThresholds = component.SpeedModifierThresholds; - - var closest = 0f; - - foreach (var state in speedModThresholds) - { - if (component.ChargeState >= state.Key && (float) state.Key > closest) - closest = (float) state.Key; - } + var closest = 0; + foreach (var state in component.SpeedModifierThresholds) + if (component.ChargeState >= state.Key && state.Key > closest) + closest = state.Key; - var speedMod = speedModThresholds[(short) closest]; + var speedMod = component.SpeedModifierThresholds[closest]; args.ModifySpeed(speedMod, speedMod); } diff --git a/Content.Shared/Station/SharedStationSpawningSystem.cs b/Content.Shared/Station/SharedStationSpawningSystem.cs index 56a8a846b07..9e933181034 100644 --- a/Content.Shared/Station/SharedStationSpawningSystem.cs +++ b/Content.Shared/Station/SharedStationSpawningSystem.cs @@ -49,7 +49,6 @@ public void EquipStartingGear(EntityUid entity, StartingGearPrototype? startingG var equipmentEntity = EntityManager.SpawnEntity(equipmentStr, EntityManager.GetComponent(entity).Coordinates); InventorySystem.TryEquip(entity, equipmentEntity, slot.Name, true, force:true); } - } if (TryComp(entity, out HandsComponent? handsComponent)) From ca88477c46b910140073b91946832c3196cf952c Mon Sep 17 00:00:00 2001 From: SimpleStation Changelogs Date: Tue, 17 Sep 2024 23:43:07 +0000 Subject: [PATCH 15/55] Automatic Changelog Update (#921) --- Resources/Changelog/Changelog.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 66a257b98b2..72a55cd6271 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -6413,3 +6413,10 @@ Entries: id: 6366 time: '2024-09-17T23:39:57.0000000+00:00' url: https://github.com/Simple-Station/Einstein-Engines/pull/925 +- author: zelezniciar + changes: + - type: Tweak + message: The fireaxe once again can pry subfloors + id: 6367 + time: '2024-09-17T23:40:13.0000000+00:00' + url: https://github.com/Simple-Station/Einstein-Engines/pull/921 From 46cbafabeb723478796b4a3f046ced6b80d455ad Mon Sep 17 00:00:00 2001 From: VMSolidus Date: Tue, 17 Sep 2024 21:16:56 -0400 Subject: [PATCH 16/55] Filespace ContestsSystem.cs (#931) # Description I CAN'T BELIEVE I MISSED THIS. --- Content.Shared/Contests/ContestsSystem.cs | 921 +++++++++++----------- 1 file changed, 460 insertions(+), 461 deletions(-) diff --git a/Content.Shared/Contests/ContestsSystem.cs b/Content.Shared/Contests/ContestsSystem.cs index 35a52d7fc3c..4dc89e2bbc9 100644 --- a/Content.Shared/Contests/ContestsSystem.cs +++ b/Content.Shared/Contests/ContestsSystem.cs @@ -7,468 +7,467 @@ using Robust.Shared.Configuration; using Robust.Shared.Physics.Components; -namespace Content.Shared.Contests +namespace Content.Shared.Contests; + +public sealed partial class ContestsSystem : EntitySystem { - public sealed partial class ContestsSystem : EntitySystem + [Dependency] private readonly IConfigurationManager _cfg = default!; + [Dependency] private readonly MobThresholdSystem _mobThreshold = default!; + + /// + /// The presumed average mass of a player entity + /// Defaulted to the average mass of an adult human + /// + private const float AverageMass = 71f; + + /// + /// The presumed average sum of a Psionic's Baseline Amplification and Baseline Dampening. + /// Since Baseline casting stats are a random value between 0.4 and 1.2, this is defaulted to 0.8 + 0.8. + /// + private const float AveragePsionicPotential = 1.6f; + + #region Mass Contests + /// + /// Outputs the ratio of mass between a performer and the average human mass + /// + /// + /// bypassClamp is a deprecated input intended for supporting legacy Nyanotrasen systems. Do not use it if you don't know what you're doing. + /// + public float MassContest(EntityUid performerUid, bool bypassClamp = false, float rangeFactor = 1f, float otherMass = AverageMass) + { + if (!_cfg.GetCVar(CCVars.DoContestsSystem) + || !_cfg.GetCVar(CCVars.DoMassContests) + || !TryComp(performerUid, out var performerPhysics) + || performerPhysics.Mass == 0) + return 1f; + + return ContestClamp(ContestClampOverride(bypassClamp) + ? performerPhysics.Mass / otherMass + : Math.Clamp(performerPhysics.Mass / otherMass, + 1 - _cfg.GetCVar(CCVars.MassContestsMaxPercentage) * rangeFactor, + 1 + _cfg.GetCVar(CCVars.MassContestsMaxPercentage) * rangeFactor)); + } + + /// + /// + /// MaybeMassContest, in case your entity doesn't exist + /// + public float MassContest(EntityUid? performerUid, bool bypassClamp = false, float rangeFactor = 1f, float otherMass = AverageMass) + { + if (!_cfg.GetCVar(CCVars.DoContestsSystem) + || !_cfg.GetCVar(CCVars.DoMassContests) + || performerUid is null) + return 1f; + + return MassContest(performerUid.Value, bypassClamp, rangeFactor, otherMass); + } + + /// + /// Outputs the ratio of mass between a performer and the average human mass + /// If a function already has the performer's physics component, this is faster + /// + /// + /// bypassClamp is a deprecated input intended for supporting legacy Nyanotrasen systems. Do not use it if you don't know what you're doing. + /// + public float MassContest(PhysicsComponent performerPhysics, bool bypassClamp = false, float rangeFactor = 1f, float otherMass = AverageMass) + { + if (!_cfg.GetCVar(CCVars.DoContestsSystem) + || !_cfg.GetCVar(CCVars.DoMassContests) + || performerPhysics.Mass == 0) + return 1f; + + return ContestClamp(ContestClampOverride(bypassClamp) + ? performerPhysics.Mass / otherMass + : Math.Clamp(performerPhysics.Mass / otherMass, + 1 - _cfg.GetCVar(CCVars.MassContestsMaxPercentage) * rangeFactor, + 1 + _cfg.GetCVar(CCVars.MassContestsMaxPercentage) * rangeFactor)); + } + + /// + /// Outputs the ratio of mass between a performer and a target, accepts either EntityUids or PhysicsComponents in any combination + /// If you have physics components already in your function, use instead + /// + /// + /// bypassClamp is a deprecated input intended for supporting legacy Nyanotrasen systems. Do not use it if you don't know what you're doing. + /// + public float MassContest(EntityUid performerUid, EntityUid targetUid, bool bypassClamp = false, float rangeFactor = 1f) + { + if (!_cfg.GetCVar(CCVars.DoContestsSystem) + || !_cfg.GetCVar(CCVars.DoMassContests) + || !TryComp(performerUid, out var performerPhysics) + || !TryComp(targetUid, out var targetPhysics) + || performerPhysics.Mass == 0 + || targetPhysics.InvMass == 0) + return 1f; + + return ContestClamp(ContestClampOverride(bypassClamp) + ? performerPhysics.Mass * targetPhysics.InvMass + : Math.Clamp(performerPhysics.Mass * targetPhysics.InvMass, + 1 - _cfg.GetCVar(CCVars.MassContestsMaxPercentage) * rangeFactor, + 1 + _cfg.GetCVar(CCVars.MassContestsMaxPercentage) * rangeFactor)); + } + + /// + public float MassContest(EntityUid performerUid, PhysicsComponent targetPhysics, bool bypassClamp = false, float rangeFactor = 1f) + { + if (!_cfg.GetCVar(CCVars.DoContestsSystem) + || !_cfg.GetCVar(CCVars.DoMassContests) + || !TryComp(performerUid, out var performerPhysics) + || performerPhysics.Mass == 0 + || targetPhysics.InvMass == 0) + return 1f; + + return ContestClamp(ContestClampOverride(bypassClamp) + ? performerPhysics.Mass * targetPhysics.InvMass + : Math.Clamp(performerPhysics.Mass * targetPhysics.InvMass, + 1 - _cfg.GetCVar(CCVars.MassContestsMaxPercentage) * rangeFactor, + 1 + _cfg.GetCVar(CCVars.MassContestsMaxPercentage) * rangeFactor)); + } + + /// + public float MassContest(PhysicsComponent performerPhysics, EntityUid targetUid, bool bypassClamp = false, float rangeFactor = 1f) + { + if (!_cfg.GetCVar(CCVars.DoContestsSystem) + || !_cfg.GetCVar(CCVars.DoMassContests) + || !TryComp(targetUid, out var targetPhysics) + || performerPhysics.Mass == 0 + || targetPhysics.InvMass == 0) + return 1f; + + return ContestClamp(ContestClampOverride(bypassClamp) + ? performerPhysics.Mass * targetPhysics.InvMass + : Math.Clamp(performerPhysics.Mass * targetPhysics.InvMass, + 1 - _cfg.GetCVar(CCVars.MassContestsMaxPercentage) * rangeFactor, + 1 + _cfg.GetCVar(CCVars.MassContestsMaxPercentage) * rangeFactor)); + } + + /// + public float MassContest(PhysicsComponent performerPhysics, PhysicsComponent targetPhysics, bool bypassClamp = false, float rangeFactor = 1f) + { + if (!_cfg.GetCVar(CCVars.DoContestsSystem) + || !_cfg.GetCVar(CCVars.DoMassContests) + || performerPhysics.Mass == 0 + || targetPhysics.InvMass == 0) + return 1f; + + return ContestClamp(ContestClampOverride(bypassClamp) + ? performerPhysics.Mass * targetPhysics.InvMass + : Math.Clamp(performerPhysics.Mass * targetPhysics.InvMass, + 1 - _cfg.GetCVar(CCVars.MassContestsMaxPercentage) * rangeFactor, + 1 + _cfg.GetCVar(CCVars.MassContestsMaxPercentage) * rangeFactor)); + } + + #endregion + #region Stamina Contests + + /// + /// Outputs 1 minus the percentage of an Entity's Stamina, with a Range of [Epsilon, 1 - 0.25 * rangeFactor], or a range of [Epsilon, 1 - Epsilon] if bypassClamp is true. + /// This will never return a value >1. + /// + /// + /// bypassClamp is a deprecated input intended for supporting legacy Nyanotrasen systems. Do not use it if you don't know what you're doing. + /// + public float StaminaContest(EntityUid performer, bool bypassClamp = false, float rangeFactor = 1f) + { + if (!TryComp(performer, out var perfStamina) + || perfStamina.StaminaDamage == 0) + return 1f; + + return StaminaContest(perfStamina, bypassClamp, rangeFactor); + } + + /// + public float StaminaContest(StaminaComponent perfStamina, bool bypassClamp = false, float rangeFactor = 1f) + { + if (!_cfg.GetCVar(CCVars.DoContestsSystem) + || !_cfg.GetCVar(CCVars.DoStaminaContests)) + return 1f; + + return ContestClamp(ContestClampOverride(bypassClamp) + ? 1 - perfStamina.StaminaDamage / perfStamina.CritThreshold + : 1 - Math.Clamp(perfStamina.StaminaDamage / perfStamina.CritThreshold, 0, 0.25f * rangeFactor)); + } + + /// + /// Outputs the ratio of percentage of an Entity's Stamina and a Target Entity's Stamina, with a Range of [Epsilon, 0.25 * rangeFactor], or a range of [Epsilon, +inf] if bypassClamp is true. + /// This does NOT produce the same kind of outputs as a Single-Entity StaminaContest. 2Entity StaminaContest returns the product of two Solo Stamina Contests, and so its values can be very strange. + /// + /// + /// bypassClamp is a deprecated input intended for supporting legacy Nyanotrasen systems. Do not use it if you don't know what you're doing. + /// + public float StaminaContest(EntityUid performer, EntityUid target, bool bypassClamp = false, float rangeFactor = 1f) + { + if (!_cfg.GetCVar(CCVars.DoContestsSystem) + || !_cfg.GetCVar(CCVars.DoStaminaContests) + || !TryComp(performer, out var perfStamina) + || !TryComp(target, out var targetStamina)) + return 1f; + + return ContestClamp(ContestClampOverride(bypassClamp) + ? (1 - perfStamina.StaminaDamage / perfStamina.CritThreshold) + / (1 - targetStamina.StaminaDamage / targetStamina.CritThreshold) + : (1 - Math.Clamp(perfStamina.StaminaDamage / perfStamina.CritThreshold, 0, 0.25f * rangeFactor)) + / (1 - Math.Clamp(targetStamina.StaminaDamage / targetStamina.CritThreshold, 0, 0.25f * rangeFactor))); + } + + #endregion + + #region Health Contests + + /// + /// Outputs 1 minus the percentage of an Entity's Health, with a Range of [Epsilon, 1 - 0.25 * rangeFactor], or a range of [Epsilon, 1 - Epsilon] if bypassClamp is true. + /// This will never return a value >1. + /// + /// + /// bypassClamp is a deprecated input intended for supporting legacy Nyanotrasen systems. Do not use it if you don't know what you're doing. + /// + public float HealthContest(EntityUid performer, bool bypassClamp = false, float rangeFactor = 1f) + { + if (!_cfg.GetCVar(CCVars.DoContestsSystem) + || !_cfg.GetCVar(CCVars.DoHealthContests) + || !TryComp(performer, out var damage) + || !_mobThreshold.TryGetThresholdForState(performer, Mobs.MobState.Critical, out var threshold)) + return 1f; + + return ContestClamp(ContestClampOverride(bypassClamp) + ? 1 - damage.TotalDamage.Float() / threshold.Value.Float() + : 1 - Math.Clamp(damage.TotalDamage.Float() / threshold.Value.Float(), 0, 0.25f * rangeFactor)); + } + + /// + /// Outputs the ratio of percentage of an Entity's Health and a Target Entity's Health, with a Range of [Epsilon, 0.25 * rangeFactor], or a range of [Epsilon, +inf] if bypassClamp is true. + /// This does NOT produce the same kind of outputs as a Single-Entity HealthContest. 2Entity HealthContest returns the product of two Solo Health Contests, and so its values can be very strange. + /// + /// + /// bypassClamp is a deprecated input intended for supporting legacy Nyanotrasen systems. Do not use it if you don't know what you're doing. + /// + public float HealthContest(EntityUid performer, EntityUid target, bool bypassClamp = false, float rangeFactor = 1f) + { + if (!_cfg.GetCVar(CCVars.DoContestsSystem) + || !_cfg.GetCVar(CCVars.DoHealthContests) + || !TryComp(performer, out var perfDamage) + || !TryComp(target, out var targetDamage) + || !_mobThreshold.TryGetThresholdForState(performer, Mobs.MobState.Critical, out var perfThreshold) + || !_mobThreshold.TryGetThresholdForState(target, Mobs.MobState.Critical, out var targetThreshold)) + return 1f; + + return ContestClamp(ContestClampOverride(bypassClamp) + ? (1 - perfDamage.TotalDamage.Float() / perfThreshold.Value.Float()) + / (1 - targetDamage.TotalDamage.Float() / targetThreshold.Value.Float()) + : (1 - Math.Clamp(perfDamage.TotalDamage.Float() / perfThreshold.Value.Float(), 0, 0.25f * rangeFactor)) + / (1 - Math.Clamp(targetDamage.TotalDamage.Float() / targetThreshold.Value.Float(), 0, 0.25f * rangeFactor))); + } + #endregion + + #region Mind Contests + + /// + /// Returns the ratio of casting stats between a performer and the presumed average latent psionic. + /// Uniquely among Contests, not being Psionic is not a failure condition, and is instead a variable. + /// If you do not have a PsionicComponent, your combined casting stats are assumed to be 0.1f + /// + /// + /// This can produce some truly astounding modifiers, so be ready to meet god if you bypass the clamp. + /// By bypassing this function's clamp you hereby agree to forfeit your soul to VMSolidus should unintended bugs occur. + /// + public float MindContest(EntityUid performer, bool bypassClamp = false, float rangeFactor = 1f, float otherPsion = AveragePsionicPotential) + { + if (!_cfg.GetCVar(CCVars.DoContestsSystem) + || !_cfg.GetCVar(CCVars.DoMindContests)) + return 1f; + + var performerPotential = TryComp(performer, out var performerPsionic) + ? performerPsionic.CurrentAmplification + performerPsionic.CurrentDampening + : 0.1f; + + if (performerPotential == otherPsion) + return 1f; + + return ContestClamp(ContestClampOverride(bypassClamp) + ? performerPotential / otherPsion + : Math.Clamp(performerPotential / otherPsion, + 1 - _cfg.GetCVar(CCVars.MassContestsMaxPercentage) * rangeFactor, + 1 + _cfg.GetCVar(CCVars.MassContestsMaxPercentage) * rangeFactor)); + } + + /// + /// Returns the ratio of casting stats between a performer and a target. + /// Like with single-Uid MindContests, if an entity does not possess a PsionicComponent, its casting stats are assumed to be 0.1f. + /// + /// + /// This can produce some truly astounding modifiers, so be ready to meet god if you bypass the clamp. + /// By bypassing this function's clamp you hereby agree to forfeit your soul to VMSolidus should unintended bugs occur. + /// + public float MindContest(EntityUid performer, EntityUid target, bool bypassClamp = false, float rangeFactor = 1f) + { + if (!_cfg.GetCVar(CCVars.DoContestsSystem) + || !_cfg.GetCVar(CCVars.DoMindContests)) + return 1f; + + var performerPotential = TryComp(performer, out var performerPsionic) + ? performerPsionic.CurrentAmplification + performerPsionic.CurrentDampening + : 0.1f; + + var targetPotential = TryComp(target, out var targetPsionic) + ? targetPsionic.CurrentAmplification + targetPsionic.CurrentDampening + : 0.1f; + + if (performerPotential == targetPotential) + return 1f; + + return ContestClamp(ContestClampOverride(bypassClamp) + ? performerPotential / targetPotential + : Math.Clamp(performerPotential / targetPotential, + 1 - _cfg.GetCVar(CCVars.MassContestsMaxPercentage) * rangeFactor, + 1 + _cfg.GetCVar(CCVars.MassContestsMaxPercentage) * rangeFactor)); + } + + #endregion + + #region Mood Contests + + /// + /// Outputs the ratio of an Entity's mood level and its Neutral Mood threshold. + /// + /// + /// bypassClamp is a deprecated input intended for supporting legacy Nyanotrasen systems. Do not use it if you don't know what you're doing. + /// + public float MoodContest(EntityUid performer, bool bypassClamp = false, float rangeFactor = 1f) + { + if (!_cfg.GetCVar(CCVars.DoContestsSystem) + || !_cfg.GetCVar(CCVars.DoMoodContests) + || !TryComp(performer, out var mood)) + return 1f; + + return ContestClamp(ContestClampOverride(bypassClamp) + ? mood.CurrentMoodLevel / mood.NeutralMoodThreshold + : Math.Clamp(mood.CurrentMoodLevel / mood.NeutralMoodThreshold, + 1 - _cfg.GetCVar(CCVars.MassContestsMaxPercentage) * rangeFactor, + 1 + _cfg.GetCVar(CCVars.MassContestsMaxPercentage) * rangeFactor)); + } + + /// + /// Outputs the ratio of mood level between two Entities. + /// + /// + /// bypassClamp is a deprecated input intended for supporting legacy Nyanotrasen systems. Do not use it if you don't know what you're doing. + /// + public float MoodContest(EntityUid performer, EntityUid target, bool bypassClamp = false, float rangeFactor = 1f) + { + if (!_cfg.GetCVar(CCVars.DoContestsSystem) + || !_cfg.GetCVar(CCVars.DoMoodContests) + || !TryComp(performer, out var performerMood) + || !TryComp(target, out var targetMood)) + return 1f; + + return ContestClamp(ContestClampOverride(bypassClamp) + ? performerMood.CurrentMoodLevel / targetMood.CurrentMoodLevel + : Math.Clamp(performerMood.CurrentMoodLevel / targetMood.CurrentMoodLevel, + 1 - _cfg.GetCVar(CCVars.MassContestsMaxPercentage) * rangeFactor, + 1 + _cfg.GetCVar(CCVars.MassContestsMaxPercentage) * rangeFactor)); + } + + #endregion + + #region EVERY CONTESTS + + /// + /// EveryContest takes either the Sum or Product of all existing contests, for if you want to just check if somebody is absolutely fucked up. + /// + /// + /// If it's not immediately obvious that a function with 16 optional inputs is a joke, please take a step back and re-evaluate why you're using this function. + /// All prior warnings also apply here. Bypass the clamps at your own risk. By calling this function in your system, you hereby agree to forfeit your soul to VMSolidus if bugs occur. + /// + public float EveryContest( + EntityUid performer, + bool bypassClampMass = false, + bool bypassClampStamina = false, + bool bypassClampHealth = false, + bool bypassClampMind = false, + bool bypassClampMood = false, + float rangeFactorMass = 1f, + float rangeFactorStamina = 1f, + float rangeFactorHealth = 1f, + float rangeFactorMind = 1f, + float rangeFactorMood = 1f, + float weightMass = 1f, + float weightStamina = 1f, + float weightHealth = 1f, + float weightMind = 1f, + float weightMood = 1f, + bool sumOrMultiply = false) + { + if (!_cfg.GetCVar(CCVars.DoContestsSystem)) + return 1f; + + var weightTotal = weightMass + weightStamina + weightHealth + weightMind + weightMood; + var massMultiplier = weightMass / weightTotal; + var staminaMultiplier = weightStamina / weightTotal; + var healthMultiplier = weightHealth / weightTotal; + var mindMultiplier = weightMind / weightTotal; + var moodMultiplier = weightMood / weightTotal; + + return sumOrMultiply + ? MassContest(performer, bypassClampMass, rangeFactorMass) * massMultiplier + + StaminaContest(performer, bypassClampStamina, rangeFactorStamina) * staminaMultiplier + + HealthContest(performer, bypassClampHealth, rangeFactorHealth) * healthMultiplier + + MindContest(performer, bypassClampMind, rangeFactorMind) * mindMultiplier + + MoodContest(performer, bypassClampMood, rangeFactorMood) * moodMultiplier + : ContestClamp(MassContest(performer, bypassClampMass, rangeFactorMass) * massMultiplier + * StaminaContest(performer, bypassClampStamina, rangeFactorStamina) * staminaMultiplier + * HealthContest(performer, bypassClampHealth, rangeFactorHealth) * healthMultiplier + * MindContest(performer, bypassClampMind, rangeFactorMind) * mindMultiplier + * MoodContest(performer, bypassClampMood, rangeFactorMood) * moodMultiplier); + } + + /// + /// EveryContest takes either the Sum or Product of all existing contests, for if you want to just check if somebody is absolutely fucked up. + /// + /// + /// If it's not immediately obvious that a function with 16 optional inputs is a joke, please take a step back and re-evaluate why you're using this function. + /// All prior warnings also apply here. Bypass the clamps at your own risk. By calling this function in your system, you hereby agree to forfeit your soul to VMSolidus if bugs occur. + /// + public float EveryContest( + EntityUid performer, + EntityUid target, + bool bypassClampMass = false, + bool bypassClampStamina = false, + bool bypassClampHealth = false, + bool bypassClampMind = false, + bool bypassClampMood = false, + float rangeFactorMass = 1f, + float rangeFactorStamina = 1f, + float rangeFactorHealth = 1f, + float rangeFactorMind = 1f, + float rangeFactorMood = 1f, + float weightMass = 1f, + float weightStamina = 1f, + float weightHealth = 1f, + float weightMind = 1f, + float weightMood = 1f, + bool sumOrMultiply = false) { - [Dependency] private readonly IConfigurationManager _cfg = default!; - [Dependency] private readonly MobThresholdSystem _mobThreshold = default!; - - /// - /// The presumed average mass of a player entity - /// Defaulted to the average mass of an adult human - /// - private const float AverageMass = 71f; - - /// - /// The presumed average sum of a Psionic's Baseline Amplification and Baseline Dampening. - /// Since Baseline casting stats are a random value between 0.4 and 1.2, this is defaulted to 0.8 + 0.8. - /// - private const float AveragePsionicPotential = 1.6f; - - #region Mass Contests - /// - /// Outputs the ratio of mass between a performer and the average human mass - /// - /// - /// bypassClamp is a deprecated input intended for supporting legacy Nyanotrasen systems. Do not use it if you don't know what you're doing. - /// - public float MassContest(EntityUid performerUid, bool bypassClamp = false, float rangeFactor = 1f, float otherMass = AverageMass) - { - if (!_cfg.GetCVar(CCVars.DoContestsSystem) - || !_cfg.GetCVar(CCVars.DoMassContests) - || !TryComp(performerUid, out var performerPhysics) - || performerPhysics.Mass == 0) - return 1f; - - return ContestClamp(ContestClampOverride(bypassClamp) - ? performerPhysics.Mass / otherMass - : Math.Clamp(performerPhysics.Mass / otherMass, - 1 - _cfg.GetCVar(CCVars.MassContestsMaxPercentage) * rangeFactor, - 1 + _cfg.GetCVar(CCVars.MassContestsMaxPercentage) * rangeFactor)); - } - - /// - /// - /// MaybeMassContest, in case your entity doesn't exist - /// - public float MassContest(EntityUid? performerUid, bool bypassClamp = false, float rangeFactor = 1f, float otherMass = AverageMass) - { - if (!_cfg.GetCVar(CCVars.DoContestsSystem) - || !_cfg.GetCVar(CCVars.DoMassContests) - || performerUid is null) - return 1f; - - return MassContest(performerUid.Value, bypassClamp, rangeFactor, otherMass); - } - - /// - /// Outputs the ratio of mass between a performer and the average human mass - /// If a function already has the performer's physics component, this is faster - /// - /// - /// bypassClamp is a deprecated input intended for supporting legacy Nyanotrasen systems. Do not use it if you don't know what you're doing. - /// - public float MassContest(PhysicsComponent performerPhysics, bool bypassClamp = false, float rangeFactor = 1f, float otherMass = AverageMass) - { - if (!_cfg.GetCVar(CCVars.DoContestsSystem) - || !_cfg.GetCVar(CCVars.DoMassContests) - || performerPhysics.Mass == 0) - return 1f; - - return ContestClamp(ContestClampOverride(bypassClamp) - ? performerPhysics.Mass / otherMass - : Math.Clamp(performerPhysics.Mass / otherMass, - 1 - _cfg.GetCVar(CCVars.MassContestsMaxPercentage) * rangeFactor, - 1 + _cfg.GetCVar(CCVars.MassContestsMaxPercentage) * rangeFactor)); - } - - /// - /// Outputs the ratio of mass between a performer and a target, accepts either EntityUids or PhysicsComponents in any combination - /// If you have physics components already in your function, use instead - /// - /// - /// bypassClamp is a deprecated input intended for supporting legacy Nyanotrasen systems. Do not use it if you don't know what you're doing. - /// - public float MassContest(EntityUid performerUid, EntityUid targetUid, bool bypassClamp = false, float rangeFactor = 1f) - { - if (!_cfg.GetCVar(CCVars.DoContestsSystem) - || !_cfg.GetCVar(CCVars.DoMassContests) - || !TryComp(performerUid, out var performerPhysics) - || !TryComp(targetUid, out var targetPhysics) - || performerPhysics.Mass == 0 - || targetPhysics.InvMass == 0) - return 1f; - - return ContestClamp(ContestClampOverride(bypassClamp) - ? performerPhysics.Mass * targetPhysics.InvMass - : Math.Clamp(performerPhysics.Mass * targetPhysics.InvMass, - 1 - _cfg.GetCVar(CCVars.MassContestsMaxPercentage) * rangeFactor, - 1 + _cfg.GetCVar(CCVars.MassContestsMaxPercentage) * rangeFactor)); - } - - /// - public float MassContest(EntityUid performerUid, PhysicsComponent targetPhysics, bool bypassClamp = false, float rangeFactor = 1f) - { - if (!_cfg.GetCVar(CCVars.DoContestsSystem) - || !_cfg.GetCVar(CCVars.DoMassContests) - || !TryComp(performerUid, out var performerPhysics) - || performerPhysics.Mass == 0 - || targetPhysics.InvMass == 0) - return 1f; - - return ContestClamp(ContestClampOverride(bypassClamp) - ? performerPhysics.Mass * targetPhysics.InvMass - : Math.Clamp(performerPhysics.Mass * targetPhysics.InvMass, - 1 - _cfg.GetCVar(CCVars.MassContestsMaxPercentage) * rangeFactor, - 1 + _cfg.GetCVar(CCVars.MassContestsMaxPercentage) * rangeFactor)); - } - - /// - public float MassContest(PhysicsComponent performerPhysics, EntityUid targetUid, bool bypassClamp = false, float rangeFactor = 1f) - { - if (!_cfg.GetCVar(CCVars.DoContestsSystem) - || !_cfg.GetCVar(CCVars.DoMassContests) - || !TryComp(targetUid, out var targetPhysics) - || performerPhysics.Mass == 0 - || targetPhysics.InvMass == 0) - return 1f; - - return ContestClamp(ContestClampOverride(bypassClamp) - ? performerPhysics.Mass * targetPhysics.InvMass - : Math.Clamp(performerPhysics.Mass * targetPhysics.InvMass, - 1 - _cfg.GetCVar(CCVars.MassContestsMaxPercentage) * rangeFactor, - 1 + _cfg.GetCVar(CCVars.MassContestsMaxPercentage) * rangeFactor)); - } - - /// - public float MassContest(PhysicsComponent performerPhysics, PhysicsComponent targetPhysics, bool bypassClamp = false, float rangeFactor = 1f) - { - if (!_cfg.GetCVar(CCVars.DoContestsSystem) - || !_cfg.GetCVar(CCVars.DoMassContests) - || performerPhysics.Mass == 0 - || targetPhysics.InvMass == 0) - return 1f; - - return ContestClamp(ContestClampOverride(bypassClamp) - ? performerPhysics.Mass * targetPhysics.InvMass - : Math.Clamp(performerPhysics.Mass * targetPhysics.InvMass, - 1 - _cfg.GetCVar(CCVars.MassContestsMaxPercentage) * rangeFactor, - 1 + _cfg.GetCVar(CCVars.MassContestsMaxPercentage) * rangeFactor)); - } - - #endregion - #region Stamina Contests - - /// - /// Outputs 1 minus the percentage of an Entity's Stamina, with a Range of [Epsilon, 1 - 0.25 * rangeFactor], or a range of [Epsilon, 1 - Epsilon] if bypassClamp is true. - /// This will never return a value >1. - /// - /// - /// bypassClamp is a deprecated input intended for supporting legacy Nyanotrasen systems. Do not use it if you don't know what you're doing. - /// - public float StaminaContest(EntityUid performer, bool bypassClamp = false, float rangeFactor = 1f) - { - if (!TryComp(performer, out var perfStamina) - || perfStamina.StaminaDamage == 0) - return 1f; - - return StaminaContest(perfStamina, bypassClamp, rangeFactor); - } - - /// - public float StaminaContest(StaminaComponent perfStamina, bool bypassClamp = false, float rangeFactor = 1f) - { - if (!_cfg.GetCVar(CCVars.DoContestsSystem) - || !_cfg.GetCVar(CCVars.DoStaminaContests)) - return 1f; - - return ContestClamp(ContestClampOverride(bypassClamp) - ? 1 - perfStamina.StaminaDamage / perfStamina.CritThreshold - : 1 - Math.Clamp(perfStamina.StaminaDamage / perfStamina.CritThreshold, 0, 0.25f * rangeFactor)); - } - - /// - /// Outputs the ratio of percentage of an Entity's Stamina and a Target Entity's Stamina, with a Range of [Epsilon, 0.25 * rangeFactor], or a range of [Epsilon, +inf] if bypassClamp is true. - /// This does NOT produce the same kind of outputs as a Single-Entity StaminaContest. 2Entity StaminaContest returns the product of two Solo Stamina Contests, and so its values can be very strange. - /// - /// - /// bypassClamp is a deprecated input intended for supporting legacy Nyanotrasen systems. Do not use it if you don't know what you're doing. - /// - public float StaminaContest(EntityUid performer, EntityUid target, bool bypassClamp = false, float rangeFactor = 1f) - { - if (!_cfg.GetCVar(CCVars.DoContestsSystem) - || !_cfg.GetCVar(CCVars.DoStaminaContests) - || !TryComp(performer, out var perfStamina) - || !TryComp(target, out var targetStamina)) - return 1f; - - return ContestClamp(ContestClampOverride(bypassClamp) - ? (1 - perfStamina.StaminaDamage / perfStamina.CritThreshold) - / (1 - targetStamina.StaminaDamage / targetStamina.CritThreshold) - : (1 - Math.Clamp(perfStamina.StaminaDamage / perfStamina.CritThreshold, 0, 0.25f * rangeFactor)) - / (1 - Math.Clamp(targetStamina.StaminaDamage / targetStamina.CritThreshold, 0, 0.25f * rangeFactor))); - } - - #endregion - - #region Health Contests - - /// - /// Outputs 1 minus the percentage of an Entity's Health, with a Range of [Epsilon, 1 - 0.25 * rangeFactor], or a range of [Epsilon, 1 - Epsilon] if bypassClamp is true. - /// This will never return a value >1. - /// - /// - /// bypassClamp is a deprecated input intended for supporting legacy Nyanotrasen systems. Do not use it if you don't know what you're doing. - /// - public float HealthContest(EntityUid performer, bool bypassClamp = false, float rangeFactor = 1f) - { - if (!_cfg.GetCVar(CCVars.DoContestsSystem) - || !_cfg.GetCVar(CCVars.DoHealthContests) - || !TryComp(performer, out var damage) - || !_mobThreshold.TryGetThresholdForState(performer, Mobs.MobState.Critical, out var threshold)) - return 1f; - - return ContestClamp(ContestClampOverride(bypassClamp) - ? 1 - damage.TotalDamage.Float() / threshold.Value.Float() - : 1 - Math.Clamp(damage.TotalDamage.Float() / threshold.Value.Float(), 0, 0.25f * rangeFactor)); - } - - /// - /// Outputs the ratio of percentage of an Entity's Health and a Target Entity's Health, with a Range of [Epsilon, 0.25 * rangeFactor], or a range of [Epsilon, +inf] if bypassClamp is true. - /// This does NOT produce the same kind of outputs as a Single-Entity HealthContest. 2Entity HealthContest returns the product of two Solo Health Contests, and so its values can be very strange. - /// - /// - /// bypassClamp is a deprecated input intended for supporting legacy Nyanotrasen systems. Do not use it if you don't know what you're doing. - /// - public float HealthContest(EntityUid performer, EntityUid target, bool bypassClamp = false, float rangeFactor = 1f) - { - if (!_cfg.GetCVar(CCVars.DoContestsSystem) - || !_cfg.GetCVar(CCVars.DoHealthContests) - || !TryComp(performer, out var perfDamage) - || !TryComp(target, out var targetDamage) - || !_mobThreshold.TryGetThresholdForState(performer, Mobs.MobState.Critical, out var perfThreshold) - || !_mobThreshold.TryGetThresholdForState(target, Mobs.MobState.Critical, out var targetThreshold)) - return 1f; - - return ContestClamp(ContestClampOverride(bypassClamp) - ? (1 - perfDamage.TotalDamage.Float() / perfThreshold.Value.Float()) - / (1 - targetDamage.TotalDamage.Float() / targetThreshold.Value.Float()) - : (1 - Math.Clamp(perfDamage.TotalDamage.Float() / perfThreshold.Value.Float(), 0, 0.25f * rangeFactor)) - / (1 - Math.Clamp(targetDamage.TotalDamage.Float() / targetThreshold.Value.Float(), 0, 0.25f * rangeFactor))); - } - #endregion - - #region Mind Contests - - /// - /// Returns the ratio of casting stats between a performer and the presumed average latent psionic. - /// Uniquely among Contests, not being Psionic is not a failure condition, and is instead a variable. - /// If you do not have a PsionicComponent, your combined casting stats are assumed to be 0.1f - /// - /// - /// This can produce some truly astounding modifiers, so be ready to meet god if you bypass the clamp. - /// By bypassing this function's clamp you hereby agree to forfeit your soul to VMSolidus should unintended bugs occur. - /// - public float MindContest(EntityUid performer, bool bypassClamp = false, float rangeFactor = 1f, float otherPsion = AveragePsionicPotential) - { - if (!_cfg.GetCVar(CCVars.DoContestsSystem) - || !_cfg.GetCVar(CCVars.DoMindContests)) - return 1f; - - var performerPotential = TryComp(performer, out var performerPsionic) - ? performerPsionic.CurrentAmplification + performerPsionic.CurrentDampening - : 0.1f; - - if (performerPotential == otherPsion) - return 1f; - - return ContestClamp(ContestClampOverride(bypassClamp) - ? performerPotential / otherPsion - : Math.Clamp(performerPotential / otherPsion, - 1 - _cfg.GetCVar(CCVars.MassContestsMaxPercentage) * rangeFactor, - 1 + _cfg.GetCVar(CCVars.MassContestsMaxPercentage) * rangeFactor)); - } - - /// - /// Returns the ratio of casting stats between a performer and a target. - /// Like with single-Uid MindContests, if an entity does not possess a PsionicComponent, its casting stats are assumed to be 0.1f. - /// - /// - /// This can produce some truly astounding modifiers, so be ready to meet god if you bypass the clamp. - /// By bypassing this function's clamp you hereby agree to forfeit your soul to VMSolidus should unintended bugs occur. - /// - public float MindContest(EntityUid performer, EntityUid target, bool bypassClamp = false, float rangeFactor = 1f) - { - if (!_cfg.GetCVar(CCVars.DoContestsSystem) - || !_cfg.GetCVar(CCVars.DoMindContests)) - return 1f; - - var performerPotential = TryComp(performer, out var performerPsionic) - ? performerPsionic.CurrentAmplification + performerPsionic.CurrentDampening - : 0.1f; - - var targetPotential = TryComp(target, out var targetPsionic) - ? targetPsionic.CurrentAmplification + targetPsionic.CurrentDampening - : 0.1f; - - if (performerPotential == targetPotential) - return 1f; - - return ContestClamp(ContestClampOverride(bypassClamp) - ? performerPotential / targetPotential - : Math.Clamp(performerPotential / targetPotential, - 1 - _cfg.GetCVar(CCVars.MassContestsMaxPercentage) * rangeFactor, - 1 + _cfg.GetCVar(CCVars.MassContestsMaxPercentage) * rangeFactor)); - } - - #endregion - - #region Mood Contests - - /// - /// Outputs the ratio of an Entity's mood level and its Neutral Mood threshold. - /// - /// - /// bypassClamp is a deprecated input intended for supporting legacy Nyanotrasen systems. Do not use it if you don't know what you're doing. - /// - public float MoodContest(EntityUid performer, bool bypassClamp = false, float rangeFactor = 1f) - { - if (!_cfg.GetCVar(CCVars.DoContestsSystem) - || !_cfg.GetCVar(CCVars.DoMoodContests) - || !TryComp(performer, out var mood)) - return 1f; - - return ContestClamp(ContestClampOverride(bypassClamp) - ? mood.CurrentMoodLevel / mood.NeutralMoodThreshold - : Math.Clamp(mood.CurrentMoodLevel / mood.NeutralMoodThreshold, - 1 - _cfg.GetCVar(CCVars.MassContestsMaxPercentage) * rangeFactor, - 1 + _cfg.GetCVar(CCVars.MassContestsMaxPercentage) * rangeFactor)); - } - - /// - /// Outputs the ratio of mood level between two Entities. - /// - /// - /// bypassClamp is a deprecated input intended for supporting legacy Nyanotrasen systems. Do not use it if you don't know what you're doing. - /// - public float MoodContest(EntityUid performer, EntityUid target, bool bypassClamp = false, float rangeFactor = 1f) - { - if (!_cfg.GetCVar(CCVars.DoContestsSystem) - || !_cfg.GetCVar(CCVars.DoMoodContests) - || !TryComp(performer, out var performerMood) - || !TryComp(target, out var targetMood)) - return 1f; - - return ContestClamp(ContestClampOverride(bypassClamp) - ? performerMood.CurrentMoodLevel / targetMood.CurrentMoodLevel - : Math.Clamp(performerMood.CurrentMoodLevel / targetMood.CurrentMoodLevel, - 1 - _cfg.GetCVar(CCVars.MassContestsMaxPercentage) * rangeFactor, - 1 + _cfg.GetCVar(CCVars.MassContestsMaxPercentage) * rangeFactor)); - } - - #endregion - - #region EVERY CONTESTS - - /// - /// EveryContest takes either the Sum or Product of all existing contests, for if you want to just check if somebody is absolutely fucked up. - /// - /// - /// If it's not immediately obvious that a function with 16 optional inputs is a joke, please take a step back and re-evaluate why you're using this function. - /// All prior warnings also apply here. Bypass the clamps at your own risk. By calling this function in your system, you hereby agree to forfeit your soul to VMSolidus if bugs occur. - /// - public float EveryContest( - EntityUid performer, - bool bypassClampMass = false, - bool bypassClampStamina = false, - bool bypassClampHealth = false, - bool bypassClampMind = false, - bool bypassClampMood = false, - float rangeFactorMass = 1f, - float rangeFactorStamina = 1f, - float rangeFactorHealth = 1f, - float rangeFactorMind = 1f, - float rangeFactorMood = 1f, - float weightMass = 1f, - float weightStamina = 1f, - float weightHealth = 1f, - float weightMind = 1f, - float weightMood = 1f, - bool sumOrMultiply = false) - { - if (!_cfg.GetCVar(CCVars.DoContestsSystem)) - return 1f; - - var weightTotal = weightMass + weightStamina + weightHealth + weightMind + weightMood; - var massMultiplier = weightMass / weightTotal; - var staminaMultiplier = weightStamina / weightTotal; - var healthMultiplier = weightHealth / weightTotal; - var mindMultiplier = weightMind / weightTotal; - var moodMultiplier = weightMood / weightTotal; - - return sumOrMultiply - ? MassContest(performer, bypassClampMass, rangeFactorMass) * massMultiplier - + StaminaContest(performer, bypassClampStamina, rangeFactorStamina) * staminaMultiplier - + HealthContest(performer, bypassClampHealth, rangeFactorHealth) * healthMultiplier - + MindContest(performer, bypassClampMind, rangeFactorMind) * mindMultiplier - + MoodContest(performer, bypassClampMood, rangeFactorMood) * moodMultiplier - : ContestClamp(MassContest(performer, bypassClampMass, rangeFactorMass) * massMultiplier - * StaminaContest(performer, bypassClampStamina, rangeFactorStamina) * staminaMultiplier - * HealthContest(performer, bypassClampHealth, rangeFactorHealth) * healthMultiplier - * MindContest(performer, bypassClampMind, rangeFactorMind) * mindMultiplier - * MoodContest(performer, bypassClampMood, rangeFactorMood) * moodMultiplier); - } - - /// - /// EveryContest takes either the Sum or Product of all existing contests, for if you want to just check if somebody is absolutely fucked up. - /// - /// - /// If it's not immediately obvious that a function with 16 optional inputs is a joke, please take a step back and re-evaluate why you're using this function. - /// All prior warnings also apply here. Bypass the clamps at your own risk. By calling this function in your system, you hereby agree to forfeit your soul to VMSolidus if bugs occur. - /// - public float EveryContest( - EntityUid performer, - EntityUid target, - bool bypassClampMass = false, - bool bypassClampStamina = false, - bool bypassClampHealth = false, - bool bypassClampMind = false, - bool bypassClampMood = false, - float rangeFactorMass = 1f, - float rangeFactorStamina = 1f, - float rangeFactorHealth = 1f, - float rangeFactorMind = 1f, - float rangeFactorMood = 1f, - float weightMass = 1f, - float weightStamina = 1f, - float weightHealth = 1f, - float weightMind = 1f, - float weightMood = 1f, - bool sumOrMultiply = false) - { - if (!_cfg.GetCVar(CCVars.DoContestsSystem)) - return 1f; - - var weightTotal = weightMass + weightStamina + weightHealth + weightMind + weightMood; - var massMultiplier = weightMass / weightTotal; - var staminaMultiplier = weightStamina / weightTotal; - var healthMultiplier = weightHealth / weightTotal; - var mindMultiplier = weightMind / weightTotal; - var moodMultiplier = weightMood / weightTotal; - - return sumOrMultiply - ? MassContest(performer, target, bypassClampMass, rangeFactorMass) * massMultiplier - + StaminaContest(performer, target, bypassClampStamina, rangeFactorStamina) * staminaMultiplier - + HealthContest(performer, target, bypassClampHealth, rangeFactorHealth) * healthMultiplier - + MindContest(performer, target, bypassClampMind, rangeFactorMind) * mindMultiplier - + MoodContest(performer, target, bypassClampMood, rangeFactorMood) * moodMultiplier - : ContestClamp(MassContest(performer, target, bypassClampMass, rangeFactorMass) * massMultiplier - * StaminaContest(performer, target, bypassClampStamina, rangeFactorStamina) * staminaMultiplier - * HealthContest(performer, target, bypassClampHealth, rangeFactorHealth) * healthMultiplier - * MindContest(performer, target, bypassClampMind, rangeFactorMind) * mindMultiplier - * MoodContest(performer, target, bypassClampMood, rangeFactorMood) * moodMultiplier); - } - #endregion + if (!_cfg.GetCVar(CCVars.DoContestsSystem)) + return 1f; + + var weightTotal = weightMass + weightStamina + weightHealth + weightMind + weightMood; + var massMultiplier = weightMass / weightTotal; + var staminaMultiplier = weightStamina / weightTotal; + var healthMultiplier = weightHealth / weightTotal; + var mindMultiplier = weightMind / weightTotal; + var moodMultiplier = weightMood / weightTotal; + + return sumOrMultiply + ? MassContest(performer, target, bypassClampMass, rangeFactorMass) * massMultiplier + + StaminaContest(performer, target, bypassClampStamina, rangeFactorStamina) * staminaMultiplier + + HealthContest(performer, target, bypassClampHealth, rangeFactorHealth) * healthMultiplier + + MindContest(performer, target, bypassClampMind, rangeFactorMind) * mindMultiplier + + MoodContest(performer, target, bypassClampMood, rangeFactorMood) * moodMultiplier + : ContestClamp(MassContest(performer, target, bypassClampMass, rangeFactorMass) * massMultiplier + * StaminaContest(performer, target, bypassClampStamina, rangeFactorStamina) * staminaMultiplier + * HealthContest(performer, target, bypassClampHealth, rangeFactorHealth) * healthMultiplier + * MindContest(performer, target, bypassClampMind, rangeFactorMind) * mindMultiplier + * MoodContest(performer, target, bypassClampMood, rangeFactorMood) * moodMultiplier); } + #endregion } From 364b2378bec71ec63a543091a7c80a71680f5d4b Mon Sep 17 00:00:00 2001 From: VMSolidus Date: Wed, 18 Sep 2024 00:30:38 -0400 Subject: [PATCH 17/55] Fix Uncloneable Feedback Message (#926) # Description Clone Pod isn't the one that has the ability to say the message, the cloning console does. This fixes a bug where Uncloneable messages aren't being played. # Changelog :cl: - fix: Cloning Consoles will now correctly state when a body has the Uncloneable trait. --- Content.Server/Cloning/CloningSystem.Utility.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Content.Server/Cloning/CloningSystem.Utility.cs b/Content.Server/Cloning/CloningSystem.Utility.cs index 408e1cf24a3..aa2ce27902c 100644 --- a/Content.Server/Cloning/CloningSystem.Utility.cs +++ b/Content.Server/Cloning/CloningSystem.Utility.cs @@ -68,9 +68,10 @@ private bool CheckUncloneable(EntityUid uid, EntityUid bodyToClone, CloningPodCo if (ev.Cancelled && ev.CloningFailMessage is not null) { - _chatSystem.TrySendInGameICMessage(uid, - Loc.GetString(ev.CloningFailMessage), - InGameICChatType.Speak, false); + if (clonePod.ConnectedConsole is not null) + _chatSystem.TrySendInGameICMessage(clonePod.ConnectedConsole.Value, + Loc.GetString(ev.CloningFailMessage), + InGameICChatType.Speak, false); return false; } From 4d4afb6b83d354bdaa4dbceb43c69981bc500c70 Mon Sep 17 00:00:00 2001 From: SimpleStation Changelogs Date: Wed, 18 Sep 2024 04:31:05 +0000 Subject: [PATCH 18/55] Automatic Changelog Update (#926) --- Resources/Changelog/Changelog.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 72a55cd6271..bf97bb66370 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -6420,3 +6420,12 @@ Entries: id: 6367 time: '2024-09-17T23:40:13.0000000+00:00' url: https://github.com/Simple-Station/Einstein-Engines/pull/921 +- author: VMSolidus + changes: + - type: Fix + message: >- + Cloning Consoles will now correctly state when a body has the + Uncloneable trait. + id: 6368 + time: '2024-09-18T04:30:39.0000000+00:00' + url: https://github.com/Simple-Station/Einstein-Engines/pull/926 From d5f0c550a3bebe1426d76640f7275ebdbe23b84f Mon Sep 17 00:00:00 2001 From: VMSolidus Date: Wed, 18 Sep 2024 22:18:35 -0400 Subject: [PATCH 19/55] Better Lying Down System (From White Dream) (#815) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Description Port of https://github.com/WWhiteDreamProject/wwdpublic/pull/2 And now also https://github.com/WWhiteDreamProject/wwdpublic/pull/8 Because Lying Down System is dependent on the Telescope System. # TODO - [x] Reconcile the code with core code, do code cleanup. I'll undraft this when I'm done. Probably not going to be tonight, because I will have to get some sleep soon to get up early for my calculus classes. # Changelog :cl: Spatison (White Dream) - add: Added lying down system / Добавлена система лежания - tweak: Lying down now uses do-afters that are visible to other people to indicate what is going on. - add: Added telescope system / Добавлена система прицеливания - tweak: Now you can aim from Hristov / Теперь можно прицеливаться из Христова --------- Signed-off-by: VMSolidus Co-authored-by: Spatison <137375981+Spatison@users.noreply.github.com> Co-authored-by: DEATHB4DEFEAT <77995199+DEATHB4DEFEAT@users.noreply.github.com> --- Content.Client/Buckle/BuckleSystem.cs | 12 +- Content.Client/Input/ContentContexts.cs | 1 + .../Options/UI/Tabs/KeyRebindTab.xaml.cs | 15 ++ Content.Client/Standing/LayingDownSystem.cs | 69 +++++ Content.Client/Telescope/TelescopeSystem.cs | 128 +++++++++ .../Standing/LayingDownComponent.cs | 14 - Content.Server/Standing/LayingDownSystem.cs | 95 +------ Content.Server/Telescope/TelescopeSystem.cs | 5 + .../Assorted/LayingDownModifierSystem.cs | 6 +- Content.Shared/CCVar/CCVars.cs | 10 + Content.Shared/Input/ContentKeyFunctions.cs | 1 + .../Standing/LayingDownComponent.cs | 26 ++ .../Standing/SharedLayingDownSystem.cs | 155 +++++++++++ .../Standing/StandingStateComponent.cs | 41 +-- .../Standing/StandingStateSystem.cs | 242 +++++++++--------- Content.Shared/Stunnable/SharedStunSystem.cs | 71 +++-- .../Telescope/SharedTelescopeSystem.cs | 110 ++++++++ .../Telescope/TelescopeComponent.cs | 16 ++ .../en-US/escape-menu/ui/options-menu.ftl | 5 + .../Locale/ru-RU/Escape-Menu/options-menu.ftl | 3 + .../Objects/Weapons/Guns/Snipers/snipers.yml | 1 + Resources/keybinds.yml | 3 + 22 files changed, 742 insertions(+), 287 deletions(-) create mode 100644 Content.Client/Standing/LayingDownSystem.cs create mode 100644 Content.Client/Telescope/TelescopeSystem.cs delete mode 100644 Content.Server/Standing/LayingDownComponent.cs create mode 100644 Content.Server/Telescope/TelescopeSystem.cs create mode 100644 Content.Shared/Standing/LayingDownComponent.cs create mode 100644 Content.Shared/Standing/SharedLayingDownSystem.cs create mode 100644 Content.Shared/Telescope/SharedTelescopeSystem.cs create mode 100644 Content.Shared/Telescope/TelescopeComponent.cs create mode 100644 Resources/Locale/ru-RU/Escape-Menu/options-menu.ftl diff --git a/Content.Client/Buckle/BuckleSystem.cs b/Content.Client/Buckle/BuckleSystem.cs index fea18e5cf3c..d4614210d9f 100644 --- a/Content.Client/Buckle/BuckleSystem.cs +++ b/Content.Client/Buckle/BuckleSystem.cs @@ -50,17 +50,11 @@ private void OnBuckleAfterAutoHandleState(EntityUid uid, BuckleComponent compone private void OnAppearanceChange(EntityUid uid, BuckleComponent component, ref AppearanceChangeEvent args) { - if (!TryComp(uid, out var rotVisuals)) + if (!TryComp(uid, out var rotVisuals) + || !Appearance.TryGetData(uid, BuckleVisuals.Buckled, out var buckled, args.Component) + || !buckled || args.Sprite == null) return; - if (!Appearance.TryGetData(uid, BuckleVisuals.Buckled, out var buckled, args.Component) || - !buckled || - args.Sprite == null) - { - _rotationVisualizerSystem.SetHorizontalAngle((uid, rotVisuals), rotVisuals.DefaultRotation); - return; - } - // Animate strapping yourself to something at a given angle // TODO: Dump this when buckle is better _rotationVisualizerSystem.AnimateSpriteRotation(uid, args.Sprite, rotVisuals.HorizontalRotation, 0.125f); diff --git a/Content.Client/Input/ContentContexts.cs b/Content.Client/Input/ContentContexts.cs index 503a9ac953b..0e56153752d 100644 --- a/Content.Client/Input/ContentContexts.cs +++ b/Content.Client/Input/ContentContexts.cs @@ -82,6 +82,7 @@ public static void SetupContexts(IInputContextContainer contexts) human.AddFunction(ContentKeyFunctions.Arcade1); human.AddFunction(ContentKeyFunctions.Arcade2); human.AddFunction(ContentKeyFunctions.Arcade3); + human.AddFunction(ContentKeyFunctions.LookUp); // actions should be common (for ghosts, mobs, etc) common.AddFunction(ContentKeyFunctions.OpenActionsMenu); diff --git a/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs b/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs index 198eb5e4fb0..ab4ebd83fa7 100644 --- a/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs +++ b/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs @@ -97,6 +97,12 @@ private void HandleToggleWalk(BaseButton.ButtonToggledEventArgs args) _deferCommands.Add(_inputManager.SaveToUserData); } + private void HandleHoldLookUp(BaseButton.ButtonToggledEventArgs args) + { + _cfg.SetCVar(CCVars.HoldLookUp, args.Pressed); + _cfg.SaveToFile(); + } + private void HandleDefaultWalk(BaseButton.ButtonToggledEventArgs args) { _cfg.SetCVar(CCVars.DefaultWalk, args.Pressed); @@ -109,6 +115,12 @@ private void HandleStaticStorageUI(BaseButton.ButtonToggledEventArgs args) _cfg.SaveToFile(); } + private void HandleToggleAutoGetUp(BaseButton.ButtonToggledEventArgs args) + { + _cfg.SetCVar(CCVars.AutoGetUp, args.Pressed); + _cfg.SaveToFile(); + } + public KeyRebindTab() { IoCManager.InjectDependencies(this); @@ -193,6 +205,9 @@ void AddCheckBox(string checkBoxName, bool currentState, Action(OnMovementInput); + + SubscribeNetworkEvent(OnCheckAutoGetUp); + } + + private void OnMovementInput(EntityUid uid, LayingDownComponent component, MoveEvent args) + { + if (!_timing.IsFirstTimePredicted + || !_actionBlocker.CanMove(uid) + || _animation.HasRunningAnimation(uid, "rotate") + || !TryComp(uid, out var transform) + || !TryComp(uid, out var sprite) + || !TryComp(uid, out var rotationVisuals)) + return; + + var rotation = transform.LocalRotation + (_eyeManager.CurrentEye.Rotation - (transform.LocalRotation - transform.WorldRotation)); + + if (rotation.GetDir() is Direction.SouthEast or Direction.East or Direction.NorthEast or Direction.North) + { + rotationVisuals.HorizontalRotation = Angle.FromDegrees(270); + sprite.Rotation = Angle.FromDegrees(270); + return; + } + + rotationVisuals.HorizontalRotation = Angle.FromDegrees(90); + sprite.Rotation = Angle.FromDegrees(90); + } + + private void OnCheckAutoGetUp(CheckAutoGetUpEvent ev, EntitySessionEventArgs args) + { + if (!_timing.IsFirstTimePredicted) + return; + + var uid = GetEntity(ev.User); + + if (!TryComp(uid, out var transform) || !TryComp(uid, out var rotationVisuals)) + return; + + var rotation = transform.LocalRotation + (_eyeManager.CurrentEye.Rotation - (transform.LocalRotation - transform.WorldRotation)); + + if (rotation.GetDir() is Direction.SouthEast or Direction.East or Direction.NorthEast or Direction.North) + { + rotationVisuals.HorizontalRotation = Angle.FromDegrees(270); + return; + } + + rotationVisuals.HorizontalRotation = Angle.FromDegrees(90); + } +} diff --git a/Content.Client/Telescope/TelescopeSystem.cs b/Content.Client/Telescope/TelescopeSystem.cs new file mode 100644 index 00000000000..ac2270aa971 --- /dev/null +++ b/Content.Client/Telescope/TelescopeSystem.cs @@ -0,0 +1,128 @@ +using System.Numerics; +using Content.Client.Viewport; +using Content.Shared.CCVar; +using Content.Shared.Telescope; +using Content.Shared.Input; +using Robust.Client.GameObjects; +using Robust.Client.Graphics; +using Robust.Client.Input; +using Robust.Client.Player; +using Robust.Client.UserInterface; +using Robust.Shared.Configuration; +using Robust.Shared.Input; +using Robust.Shared.Input.Binding; +using Robust.Shared.Timing; + +namespace Content.Client.Telescope; + +public sealed class TelescopeSystem : SharedTelescopeSystem +{ + [Dependency] private readonly InputSystem _inputSystem = default!; + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly IPlayerManager _player = default!; + [Dependency] private readonly IInputManager _input = default!; + [Dependency] private readonly IEyeManager _eyeManager = default!; + [Dependency] private readonly IUserInterfaceManager _uiManager = default!; + [Dependency] private readonly IConfigurationManager _cfg = default!; + + private ScalingViewport? _viewport; + private bool _holdLookUp; + private bool _toggled; + + public override void Initialize() + { + base.Initialize(); + + _cfg.OnValueChanged(CCVars.HoldLookUp, + val => + { + var input = val ? null : InputCmdHandler.FromDelegate(_ => _toggled = !_toggled); + _input.SetInputCommand(ContentKeyFunctions.LookUp, input); + _holdLookUp = val; + _toggled = false; + }, + true); + } + + public override void FrameUpdate(float frameTime) + { + base.FrameUpdate(frameTime); + + if (_timing.ApplyingState + || !_timing.IsFirstTimePredicted + || !_input.MouseScreenPosition.IsValid) + return; + + var player = _player.LocalEntity; + + var telescope = GetRightTelescope(player); + + if (telescope == null) + { + _toggled = false; + return; + } + + if (!TryComp(player, out var eye)) + return; + + var offset = Vector2.Zero; + + if (_holdLookUp) + { + if (_inputSystem.CmdStates.GetState(ContentKeyFunctions.LookUp) != BoundKeyState.Down) + { + RaiseEvent(offset); + return; + } + } + else if (!_toggled) + { + RaiseEvent(offset); + return; + } + + var mousePos = _input.MouseScreenPosition; + + if (_uiManager.MouseGetControl(mousePos) as ScalingViewport is { } viewport) + _viewport = viewport; + + if (_viewport == null) + return; + + var centerPos = _eyeManager.WorldToScreen(eye.Eye.Position.Position + eye.Offset); + + var diff = mousePos.Position - centerPos; + var len = diff.Length(); + + var size = _viewport.PixelSize; + + var maxLength = Math.Min(size.X, size.Y) * 0.4f; + var minLength = maxLength * 0.2f; + + if (len > maxLength) + { + diff *= maxLength / len; + len = maxLength; + } + + var divisor = maxLength * telescope.Divisor; + + if (len > minLength) + { + diff -= diff * minLength / len; + offset = new Vector2(diff.X / divisor, -diff.Y / divisor); + offset = new Angle(-eye.Rotation.Theta).RotateVec(offset); + } + + RaiseEvent(offset); + } + + private void RaiseEvent(Vector2 offset) + { + RaisePredictiveEvent(new EyeOffsetChangedEvent + { + Offset = offset + }); + } +} diff --git a/Content.Server/Standing/LayingDownComponent.cs b/Content.Server/Standing/LayingDownComponent.cs deleted file mode 100644 index 7921749f148..00000000000 --- a/Content.Server/Standing/LayingDownComponent.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Content.Server.Standing; - -[RegisterComponent] -public sealed partial class LayingDownComponent : Component -{ - [DataField] - public float DownedSpeedMultiplier = 0.15f; - - [DataField] - public TimeSpan Cooldown = TimeSpan.FromSeconds(2.5f); - - [DataField] - public TimeSpan NextToggleAttempt = TimeSpan.Zero; -} diff --git a/Content.Server/Standing/LayingDownSystem.cs b/Content.Server/Standing/LayingDownSystem.cs index 73a929fdfc4..e5054bdd706 100644 --- a/Content.Server/Standing/LayingDownSystem.cs +++ b/Content.Server/Standing/LayingDownSystem.cs @@ -1,101 +1,28 @@ -using Content.Shared.ActionBlocker; -using Content.Shared.Input; -using Content.Shared.Movement.Systems; -using Content.Shared.Popups; using Content.Shared.Standing; -using Robust.Shared.Input.Binding; -using Robust.Shared.Player; -using Robust.Shared.Timing; +using Content.Shared.CCVar; +using Robust.Shared.Configuration; namespace Content.Server.Standing; -/// Unfortunately cannot be shared because some standing conditions are server-side only -public sealed class LayingDownSystem : EntitySystem +public sealed class LayingDownSystem : SharedLayingDownSystem { - [Dependency] private readonly ActionBlockerSystem _actionBlocker = default!; - [Dependency] private readonly MovementSpeedModifierSystem _movement = default!; - [Dependency] private readonly SharedPopupSystem _popups = default!; - [Dependency] private readonly Shared.Standing.StandingStateSystem _standing = default!; // WHY IS THERE TWO DIFFERENT STANDING SYSTEMS?! - [Dependency] private readonly IGameTiming _timing = default!; - + [Dependency] private readonly INetConfigurationManager _cfg = default!; public override void Initialize() { - CommandBinds.Builder - .Bind(ContentKeyFunctions.ToggleStanding, InputCmdHandler.FromDelegate(ToggleStanding, handle: false, outsidePrediction: false)) - .Register(); - - SubscribeLocalEvent(DoRefreshMovementSpeed); - SubscribeLocalEvent(DoRefreshMovementSpeed); - SubscribeLocalEvent(OnRefreshMovementSpeed); - SubscribeLocalEvent(OnParentChanged); - } - - public override void Shutdown() - { - base.Shutdown(); - - CommandBinds.Unregister(); - } + base.Initialize(); - private void DoRefreshMovementSpeed(EntityUid uid, LayingDownComponent component, object args) - { - _movement.RefreshMovementSpeedModifiers(uid); + SubscribeNetworkEvent(OnCheckAutoGetUp); } - private void OnRefreshMovementSpeed(EntityUid uid, LayingDownComponent component, RefreshMovementSpeedModifiersEvent args) + private void OnCheckAutoGetUp(CheckAutoGetUpEvent ev, EntitySessionEventArgs args) { - if (TryComp(uid, out var standingState) && standingState.Standing) - return; + var uid = GetEntity(ev.User); - args.ModifySpeed(component.DownedSpeedMultiplier, component.DownedSpeedMultiplier, bypassImmunity: true); - } - - private void OnParentChanged(EntityUid uid, LayingDownComponent component, EntParentChangedMessage args) - { - // If the entity is not on a grid, try to make it stand up to avoid issues - if (!TryComp(uid, out var standingState) - || standingState.Standing - || Transform(uid).GridUid != null) + if (!TryComp(uid, out LayingDownComponent? layingDown)) return; - _standing.Stand(uid, standingState); - } - - private void ToggleStanding(ICommonSession? session) - { - if (session is not { AttachedEntity: { Valid: true } uid } playerSession - || !Exists(uid) - || !TryComp(uid, out var standingState) - || !TryComp(uid, out var layingDown)) - return; - - // If successful, show popup to self and others. Otherwise, only to self. - if (ToggleStandingImpl(uid, standingState, layingDown, out var popupBranch)) - { - _popups.PopupEntity(Loc.GetString($"laying-comp-{popupBranch}-other", ("entity", uid)), uid, Filter.PvsExcept(uid), true); - layingDown.NextToggleAttempt = _timing.CurTime + layingDown.Cooldown; - } - - _popups.PopupEntity(Loc.GetString($"laying-comp-{popupBranch}-self", ("entity", uid)), uid, uid); - } - - private bool ToggleStandingImpl(EntityUid uid, StandingStateComponent standingState, LayingDownComponent layingDown, out string popupBranch) - { - var success = layingDown.NextToggleAttempt <= _timing.CurTime; - - if (_standing.IsDown(uid, standingState)) - { - success = success && _standing.Stand(uid, standingState, force: false); - popupBranch = success ? "stand-success" : "stand-fail"; - } - else - { - success = success && Transform(uid).GridUid != null; // Do not allow laying down when not on a surface. - success = success && _standing.Down(uid, standingState: standingState, playSound: true, dropHeldItems: false); - popupBranch = success ? "lay-success" : "lay-fail"; - } - - return success; + layingDown.AutoGetUp = _cfg.GetClientCVar(args.SenderSession.Channel, CCVars.AutoGetUp); + Dirty(uid, layingDown); } } diff --git a/Content.Server/Telescope/TelescopeSystem.cs b/Content.Server/Telescope/TelescopeSystem.cs new file mode 100644 index 00000000000..0e53cc15a20 --- /dev/null +++ b/Content.Server/Telescope/TelescopeSystem.cs @@ -0,0 +1,5 @@ +using Content.Shared.Telescope; + +namespace Content.Server.Telescope; + +public sealed class TelescopeSystem : SharedTelescopeSystem; diff --git a/Content.Server/Traits/Assorted/LayingDownModifierSystem.cs b/Content.Server/Traits/Assorted/LayingDownModifierSystem.cs index dc6dcd2de3b..e4a63d6108b 100644 --- a/Content.Server/Traits/Assorted/LayingDownModifierSystem.cs +++ b/Content.Server/Traits/Assorted/LayingDownModifierSystem.cs @@ -1,5 +1,5 @@ using Content.Server.Traits.Assorted; -using Content.Server.Standing; +using Content.Shared.Standing; namespace Content.Shared.Traits.Assorted.Systems; @@ -16,7 +16,7 @@ private void OnStartup(EntityUid uid, LayingDownModifierComponent component, Com if (!TryComp(uid, out var layingDown)) return; - layingDown.Cooldown *= component.LayingDownCooldownMultiplier; - layingDown.DownedSpeedMultiplier *= component.DownedSpeedMultiplierMultiplier; + layingDown.StandingUpTime *= component.LayingDownCooldownMultiplier; + layingDown.SpeedModify *= component.DownedSpeedMultiplierMultiplier; } } diff --git a/Content.Shared/CCVar/CCVars.cs b/Content.Shared/CCVar/CCVars.cs index bad706f8154..27ad9641070 100644 --- a/Content.Shared/CCVar/CCVars.cs +++ b/Content.Shared/CCVar/CCVars.cs @@ -2457,6 +2457,16 @@ public static readonly CVarDef #endregion + #region Lying Down System + + public static readonly CVarDef AutoGetUp = + CVarDef.Create("rest.auto_get_up", true, CVar.CLIENT | CVar.ARCHIVE | CVar.REPLICATED); + + public static readonly CVarDef HoldLookUp = + CVarDef.Create("rest.hold_look_up", false, CVar.CLIENT | CVar.ARCHIVE); + + #endregion + #region Material Reclaimer /// diff --git a/Content.Shared/Input/ContentKeyFunctions.cs b/Content.Shared/Input/ContentKeyFunctions.cs index dac780783c7..f85983282c7 100644 --- a/Content.Shared/Input/ContentKeyFunctions.cs +++ b/Content.Shared/Input/ContentKeyFunctions.cs @@ -57,6 +57,7 @@ public static class ContentKeyFunctions public static readonly BoundKeyFunction ResetZoom = "ResetZoom"; public static readonly BoundKeyFunction OfferItem = "OfferItem"; public static readonly BoundKeyFunction ToggleStanding = "ToggleStanding"; + public static readonly BoundKeyFunction LookUp = "LookUp"; public static readonly BoundKeyFunction ArcadeUp = "ArcadeUp"; public static readonly BoundKeyFunction ArcadeDown = "ArcadeDown"; diff --git a/Content.Shared/Standing/LayingDownComponent.cs b/Content.Shared/Standing/LayingDownComponent.cs new file mode 100644 index 00000000000..1499704c53b --- /dev/null +++ b/Content.Shared/Standing/LayingDownComponent.cs @@ -0,0 +1,26 @@ +using Robust.Shared.GameStates; +using Robust.Shared.Serialization; + +namespace Content.Shared.Standing; + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class LayingDownComponent : Component +{ + [DataField, AutoNetworkedField] + public float StandingUpTime { get; set; } = 1f; + + [DataField, AutoNetworkedField] + public float SpeedModify { get; set; } = 0.4f; + + [DataField, AutoNetworkedField] + public bool AutoGetUp; +} + +[Serializable, NetSerializable] +public sealed class ChangeLayingDownEvent : CancellableEntityEventArgs; + +[Serializable, NetSerializable] +public sealed class CheckAutoGetUpEvent(NetEntity user) : CancellableEntityEventArgs +{ + public NetEntity User = user; +} diff --git a/Content.Shared/Standing/SharedLayingDownSystem.cs b/Content.Shared/Standing/SharedLayingDownSystem.cs new file mode 100644 index 00000000000..47ad01949cb --- /dev/null +++ b/Content.Shared/Standing/SharedLayingDownSystem.cs @@ -0,0 +1,155 @@ +using Content.Shared.DoAfter; +using Content.Shared.Gravity; +using Content.Shared.Input; +using Content.Shared.Mobs.Systems; +using Content.Shared.Movement.Systems; +using Content.Shared.Standing; +using Content.Shared.Stunnable; +using Robust.Shared.Input.Binding; +using Robust.Shared.Player; +using Robust.Shared.Serialization; + +namespace Content.Shared.Standing; + +public abstract class SharedLayingDownSystem : EntitySystem +{ + [Dependency] private readonly MobStateSystem _mobState = default!; + [Dependency] private readonly StandingStateSystem _standing = default!; + [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; + [Dependency] private readonly SharedGravitySystem _gravity = default!; + + public override void Initialize() + { + CommandBinds.Builder + .Bind(ContentKeyFunctions.ToggleStanding, InputCmdHandler.FromDelegate(ToggleStanding)) + .Register(); + + SubscribeNetworkEvent(OnChangeState); + + SubscribeLocalEvent(OnStandingUpDoAfter); + SubscribeLocalEvent(OnRefreshMovementSpeed); + SubscribeLocalEvent(OnParentChanged); + } + + public override void Shutdown() + { + base.Shutdown(); + + CommandBinds.Unregister(); + } + + private void ToggleStanding(ICommonSession? session) + { + if (session?.AttachedEntity == null + || !HasComp(session.AttachedEntity) + || _gravity.IsWeightless(session.AttachedEntity.Value)) + return; + + RaiseNetworkEvent(new ChangeLayingDownEvent()); + } + + private void OnChangeState(ChangeLayingDownEvent ev, EntitySessionEventArgs args) + { + if (!args.SenderSession.AttachedEntity.HasValue) + return; + + var uid = args.SenderSession.AttachedEntity.Value; + + // TODO: Wizard + //if (HasComp(uid)) + // return; + + if (!TryComp(uid, out StandingStateComponent? standing) + || !TryComp(uid, out LayingDownComponent? layingDown)) + return; + + RaiseNetworkEvent(new CheckAutoGetUpEvent(GetNetEntity(uid))); + + if (HasComp(uid) + || !_mobState.IsAlive(uid)) + return; + + if (_standing.IsDown(uid, standing)) + TryStandUp(uid, layingDown, standing); + else + TryLieDown(uid, layingDown, standing); + } + + private void OnStandingUpDoAfter(EntityUid uid, StandingStateComponent component, StandingUpDoAfterEvent args) + { + if (args.Handled || args.Cancelled + || HasComp(uid) + || _mobState.IsIncapacitated(uid) + || !_standing.Stand(uid)) + component.CurrentState = StandingState.Lying; + + component.CurrentState = StandingState.Standing; + } + + private void OnRefreshMovementSpeed(EntityUid uid, LayingDownComponent component, RefreshMovementSpeedModifiersEvent args) + { + if (_standing.IsDown(uid)) + args.ModifySpeed(component.SpeedModify, component.SpeedModify); + else + args.ModifySpeed(1f, 1f); + } + + private void OnParentChanged(EntityUid uid, LayingDownComponent component, EntParentChangedMessage args) + { + // If the entity is not on a grid, try to make it stand up to avoid issues + if (!TryComp(uid, out var standingState) + || standingState.CurrentState is StandingState.Standing + || Transform(uid).GridUid != null) + return; + + _standing.Stand(uid, standingState); + } + + public bool TryStandUp(EntityUid uid, LayingDownComponent? layingDown = null, StandingStateComponent? standingState = null) + { + if (!Resolve(uid, ref standingState, false) + || !Resolve(uid, ref layingDown, false) + || standingState.CurrentState is not StandingState.Lying + || !_mobState.IsAlive(uid) + || TerminatingOrDeleted(uid)) + return false; + + var args = new DoAfterArgs(EntityManager, uid, layingDown.StandingUpTime, new StandingUpDoAfterEvent(), uid) + { + BreakOnHandChange = false, + RequireCanInteract = false + }; + + if (!_doAfter.TryStartDoAfter(args)) + return false; + + standingState.CurrentState = StandingState.GettingUp; + return true; + } + + public bool TryLieDown(EntityUid uid, LayingDownComponent? layingDown = null, StandingStateComponent? standingState = null, DropHeldItemsBehavior behavior = DropHeldItemsBehavior.NoDrop) + { + if (!Resolve(uid, ref standingState, false) + || !Resolve(uid, ref layingDown, false) + || standingState.CurrentState is not StandingState.Standing) + { + if (behavior == DropHeldItemsBehavior.AlwaysDrop) + RaiseLocalEvent(uid, new DropHandItemsEvent()); + + return false; + } + + _standing.Down(uid, true, behavior != DropHeldItemsBehavior.NoDrop, standingState); + return true; + } +} + +[Serializable, NetSerializable] +public sealed partial class StandingUpDoAfterEvent : SimpleDoAfterEvent; + +public enum DropHeldItemsBehavior : byte +{ + NoDrop, + DropIfStanding, + AlwaysDrop +} diff --git a/Content.Shared/Standing/StandingStateComponent.cs b/Content.Shared/Standing/StandingStateComponent.cs index 5d7bb0a59fd..5b9759a0252 100644 --- a/Content.Shared/Standing/StandingStateComponent.cs +++ b/Content.Shared/Standing/StandingStateComponent.cs @@ -1,24 +1,31 @@ using Robust.Shared.Audio; using Robust.Shared.GameStates; -namespace Content.Shared.Standing +namespace Content.Shared.Standing; + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class StandingStateComponent : Component { - [RegisterComponent, NetworkedComponent, AutoGenerateComponentState] - [Access(typeof(StandingStateSystem))] - public sealed partial class StandingStateComponent : Component - { - [ViewVariables(VVAccess.ReadWrite)] - [DataField] - public SoundSpecifier DownSound { get; private set; } = new SoundCollectionSpecifier("BodyFall"); + [DataField] + public SoundSpecifier DownSound { get; private set; } = new SoundCollectionSpecifier("BodyFall"); + + [DataField, AutoNetworkedField] + public StandingState CurrentState { get; set; } = StandingState.Standing; - [DataField, AutoNetworkedField] - public bool Standing { get; set; } = true; + [DataField, AutoNetworkedField] + public bool Standing { get; set; } = true; - /// - /// List of fixtures that had their collision mask changed when the entity was downed. - /// Required for re-adding the collision mask. - /// - [DataField, AutoNetworkedField] - public List ChangedFixtures = new(); - } + /// + /// List of fixtures that had their collision mask changed when the entity was downed. + /// Required for re-adding the collision mask. + /// + [DataField, AutoNetworkedField] + public List ChangedFixtures = new(); +} + +public enum StandingState +{ + Lying, + GettingUp, + Standing, } diff --git a/Content.Shared/Standing/StandingStateSystem.cs b/Content.Shared/Standing/StandingStateSystem.cs index 517831b8a1b..69e72033bba 100644 --- a/Content.Shared/Standing/StandingStateSystem.cs +++ b/Content.Shared/Standing/StandingStateSystem.cs @@ -1,166 +1,160 @@ +using Content.Shared.Buckle; +using Content.Shared.Buckle.Components; using Content.Shared.Hands.Components; +using Content.Shared.Movement.Systems; using Content.Shared.Physics; using Content.Shared.Rotation; -using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; using Robust.Shared.Physics; using Robust.Shared.Physics.Systems; -namespace Content.Shared.Standing +namespace Content.Shared.Standing; + +public sealed class StandingStateSystem : EntitySystem { - public sealed class StandingStateSystem : EntitySystem - { - [Dependency] private readonly SharedAppearanceSystem _appearance = default!; - [Dependency] private readonly SharedAudioSystem _audio = default!; - [Dependency] private readonly SharedPhysicsSystem _physics = default!; + [Dependency] private readonly SharedAppearanceSystem _appearance = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedPhysicsSystem _physics = default!; + [Dependency] private readonly MovementSpeedModifierSystem _movement = default!; + [Dependency] private readonly SharedBuckleSystem _buckle = default!; - // If StandingCollisionLayer value is ever changed to more than one layer, the logic needs to be edited. - private const int StandingCollisionLayer = (int) CollisionGroup.MidImpassable; + // If StandingCollisionLayer value is ever changed to more than one layer, the logic needs to be edited. + private const int StandingCollisionLayer = (int) CollisionGroup.MidImpassable; - public bool IsDown(EntityUid uid, StandingStateComponent? standingState = null) - { - if (!Resolve(uid, ref standingState, false)) - return false; - - return !standingState.Standing; - } + public bool IsDown(EntityUid uid, StandingStateComponent? standingState = null) + { + if (!Resolve(uid, ref standingState, false)) + return false; - public bool Down(EntityUid uid, bool playSound = true, bool dropHeldItems = true, - StandingStateComponent? standingState = null, - AppearanceComponent? appearance = null, - HandsComponent? hands = null) - { - // TODO: This should actually log missing comps... - if (!Resolve(uid, ref standingState, false)) - return false; + return standingState.CurrentState is StandingState.Lying or StandingState.GettingUp; + } - // Optional component. - Resolve(uid, ref appearance, ref hands, false); + public bool Down(EntityUid uid, bool playSound = true, bool dropHeldItems = true, + StandingStateComponent? standingState = null, + AppearanceComponent? appearance = null, + HandsComponent? hands = null) + { + // TODO: This should actually log missing comps... + if (!Resolve(uid, ref standingState, false)) + return false; - if (!standingState.Standing) - return true; + // Optional component. + Resolve(uid, ref appearance, ref hands, false); - // This is just to avoid most callers doing this manually saving boilerplate - // 99% of the time you'll want to drop items but in some scenarios (e.g. buckling) you don't want to. - // We do this BEFORE downing because something like buckle may be blocking downing but we want to drop hand items anyway - // and ultimately this is just to avoid boilerplate in Down callers + keep their behavior consistent. - if (dropHeldItems && hands != null) - { - RaiseLocalEvent(uid, new DropHandItemsEvent(), false); - } + if (standingState.CurrentState is StandingState.Lying or StandingState.GettingUp) + return true; - var msg = new DownAttemptEvent(); - RaiseLocalEvent(uid, msg, false); + // This is just to avoid most callers doing this manually saving boilerplate + // 99% of the time you'll want to drop items but in some scenarios (e.g. buckling) you don't want to. + // We do this BEFORE downing because something like buckle may be blocking downing but we want to drop hand items anyway + // and ultimately this is just to avoid boilerplate in Down callers + keep their behavior consistent. + if (dropHeldItems && hands != null) + RaiseLocalEvent(uid, new DropHandItemsEvent(), false); - if (msg.Cancelled) - return false; + if (TryComp(uid, out BuckleComponent? buckle) && buckle.Buckled && !_buckle.TryUnbuckle(uid, uid, buckleComp: buckle)) + return false; - standingState.Standing = false; - Dirty(standingState); - RaiseLocalEvent(uid, new DownedEvent(), false); + var msg = new DownAttemptEvent(); + RaiseLocalEvent(uid, msg, false); - // Seemed like the best place to put it - _appearance.SetData(uid, RotationVisuals.RotationState, RotationState.Horizontal, appearance); + if (msg.Cancelled) + return false; - // Change collision masks to allow going under certain entities like flaps and tables - if (TryComp(uid, out FixturesComponent? fixtureComponent)) - { - foreach (var (key, fixture) in fixtureComponent.Fixtures) - { - if ((fixture.CollisionMask & StandingCollisionLayer) == 0) - continue; - - standingState.ChangedFixtures.Add(key); - _physics.SetCollisionMask(uid, key, fixture, fixture.CollisionMask & ~StandingCollisionLayer, manager: fixtureComponent); - } - } + standingState.CurrentState = StandingState.Lying; + Dirty(standingState); + RaiseLocalEvent(uid, new DownedEvent(), false); - // check if component was just added or streamed to client - // if true, no need to play sound - mob was down before player could seen that - if (standingState.LifeStage <= ComponentLifeStage.Starting) - return true; + // Seemed like the best place to put it + _appearance.SetData(uid, RotationVisuals.RotationState, RotationState.Horizontal, appearance); - if (playSound) + // Change collision masks to allow going under certain entities like flaps and tables + if (TryComp(uid, out FixturesComponent? fixtureComponent)) + foreach (var (key, fixture) in fixtureComponent.Fixtures) { - _audio.PlayPredicted(standingState.DownSound, uid, uid); + if ((fixture.CollisionMask & StandingCollisionLayer) == 0) + continue; + + standingState.ChangedFixtures.Add(key); + _physics.SetCollisionMask(uid, key, fixture, fixture.CollisionMask & ~StandingCollisionLayer, manager: fixtureComponent); } + // check if component was just added or streamed to client + // if true, no need to play sound - mob was down before player could seen that + if (standingState.LifeStage <= ComponentLifeStage.Starting) return true; - } - public bool Stand(EntityUid uid, - StandingStateComponent? standingState = null, - AppearanceComponent? appearance = null, - bool force = false) - { - // TODO: This should actually log missing comps... - if (!Resolve(uid, ref standingState, false)) - return false; + if (playSound) + _audio.PlayPredicted(standingState.DownSound, uid, null); - // Optional component. - Resolve(uid, ref appearance, false); + _movement.RefreshMovementSpeedModifiers(uid); + return true; + } - if (standingState.Standing) - return true; + public bool Stand(EntityUid uid, + StandingStateComponent? standingState = null, + AppearanceComponent? appearance = null, + bool force = false) + { + // TODO: This should actually log missing comps... + if (!Resolve(uid, ref standingState, false)) + return false; - if (!force) - { - var msg = new StandAttemptEvent(); - RaiseLocalEvent(uid, msg, false); + // Optional component. + Resolve(uid, ref appearance, false); - if (msg.Cancelled) - return false; - } + if (standingState.CurrentState is StandingState.Standing + || TryComp(uid, out BuckleComponent? buckle) + && buckle.Buckled && !_buckle.TryUnbuckle(uid, uid, buckleComp: buckle)) + return true; - standingState.Standing = true; - Dirty(uid, standingState); - RaiseLocalEvent(uid, new StoodEvent(), false); + if (!force) + { + var msg = new StandAttemptEvent(); + RaiseLocalEvent(uid, msg, false); - _appearance.SetData(uid, RotationVisuals.RotationState, RotationState.Vertical, appearance); + if (msg.Cancelled) + return false; + } - if (TryComp(uid, out FixturesComponent? fixtureComponent)) + standingState.CurrentState = StandingState.Standing; + Dirty(uid, standingState); + RaiseLocalEvent(uid, new StoodEvent(), false); + + _appearance.SetData(uid, RotationVisuals.RotationState, RotationState.Vertical, appearance); + + if (TryComp(uid, out FixturesComponent? fixtureComponent)) + { + foreach (var key in standingState.ChangedFixtures) { - foreach (var key in standingState.ChangedFixtures) - { - if (fixtureComponent.Fixtures.TryGetValue(key, out var fixture)) - _physics.SetCollisionMask(uid, key, fixture, fixture.CollisionMask | StandingCollisionLayer, fixtureComponent); - } + if (fixtureComponent.Fixtures.TryGetValue(key, out var fixture)) + _physics.SetCollisionMask(uid, key, fixture, fixture.CollisionMask | StandingCollisionLayer, fixtureComponent); } - standingState.ChangedFixtures.Clear(); - - return true; } - } + standingState.ChangedFixtures.Clear(); + _movement.RefreshMovementSpeedModifiers(uid); - public sealed class DropHandItemsEvent : EventArgs - { + return true; } +} - /// - /// Subscribe if you can potentially block a down attempt. - /// - public sealed class DownAttemptEvent : CancellableEntityEventArgs - { - } +public sealed class DropHandItemsEvent : EventArgs { } - /// - /// Subscribe if you can potentially block a stand attempt. - /// - public sealed class StandAttemptEvent : CancellableEntityEventArgs - { - } +/// +/// Subscribe if you can potentially block a down attempt. +/// +public sealed class DownAttemptEvent : CancellableEntityEventArgs { } - /// - /// Raised when an entity becomes standing - /// - public sealed class StoodEvent : EntityEventArgs - { - } +/// +/// Subscribe if you can potentially block a stand attempt. +/// +public sealed class StandAttemptEvent : CancellableEntityEventArgs { } - /// - /// Raised when an entity is not standing - /// - public sealed class DownedEvent : EntityEventArgs - { - } -} +/// +/// Raised when an entity becomes standing +/// +public sealed class StoodEvent : EntityEventArgs { } + +/// +/// Raised when an entity is not standing +/// +public sealed class DownedEvent : EntityEventArgs { } diff --git a/Content.Shared/Stunnable/SharedStunSystem.cs b/Content.Shared/Stunnable/SharedStunSystem.cs index 976b0ab500d..52225843f23 100644 --- a/Content.Shared/Stunnable/SharedStunSystem.cs +++ b/Content.Shared/Stunnable/SharedStunSystem.cs @@ -19,6 +19,7 @@ using Content.Shared.Throwing; using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; +using Robust.Shared.Containers; using Robust.Shared.GameStates; using Robust.Shared.Player; @@ -32,6 +33,8 @@ public abstract class SharedStunSystem : EntitySystem [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly StandingStateSystem _standingState = default!; [Dependency] private readonly StatusEffectsSystem _statusEffect = default!; + [Dependency] private readonly SharedLayingDownSystem _layingDown = default!; + [Dependency] private readonly SharedContainerSystem _container = default!; /// /// Friction modifier for knocked down players. @@ -76,15 +79,12 @@ public override void Initialize() private void OnMobStateChanged(EntityUid uid, MobStateComponent component, MobStateChangedEvent args) { if (!TryComp(uid, out var status)) - { return; - } + switch (args.NewMobState) { case MobState.Alive: - { break; - } case MobState.Critical: { _statusEffect.TryRemoveStatusEffect(uid, "Stun"); @@ -109,12 +109,23 @@ private void UpdateCanMove(EntityUid uid, StunnedComponent component, EntityEven private void OnKnockInit(EntityUid uid, KnockedDownComponent component, ComponentInit args) { - _standingState.Down(uid); + RaiseNetworkEvent(new CheckAutoGetUpEvent(GetNetEntity(uid))); + _layingDown.TryLieDown(uid, null, null, DropHeldItemsBehavior.DropIfStanding); } private void OnKnockShutdown(EntityUid uid, KnockedDownComponent component, ComponentShutdown args) { - _standingState.Stand(uid); + if (!TryComp(uid, out StandingStateComponent? standing)) + return; + + if (TryComp(uid, out LayingDownComponent? layingDown)) + { + if (layingDown.AutoGetUp && !_container.IsEntityInContainer(uid)) + _layingDown.TryStandUp(uid, layingDown); + return; + } + + _standingState.Stand(uid, standing); } private void OnStandAttempt(EntityUid uid, KnockedDownComponent component, StandAttemptEvent args) @@ -148,13 +159,9 @@ private void OnRefreshMovespeed(EntityUid uid, SlowedDownComponent component, Re public bool TryStun(EntityUid uid, TimeSpan time, bool refresh, StatusEffectsComponent? status = null) { - if (time <= TimeSpan.Zero) - return false; - - if (!Resolve(uid, ref status, false)) - return false; - - if (!_statusEffect.TryAddStatusEffect(uid, "Stun", time, refresh)) + if (time <= TimeSpan.Zero + || !Resolve(uid, ref status, false) + || !_statusEffect.TryAddStatusEffect(uid, "Stun", time, refresh)) return false; var ev = new StunnedEvent(); @@ -170,13 +177,9 @@ public bool TryStun(EntityUid uid, TimeSpan time, bool refresh, public bool TryKnockdown(EntityUid uid, TimeSpan time, bool refresh, StatusEffectsComponent? status = null) { - if (time <= TimeSpan.Zero) - return false; - - if (!Resolve(uid, ref status, false)) - return false; - - if (!_statusEffect.TryAddStatusEffect(uid, "KnockedDown", time, refresh)) + if (time <= TimeSpan.Zero + || !Resolve(uid, ref status, false) + || !_statusEffect.TryAddStatusEffect(uid, "KnockedDown", time, refresh)) return false; var ev = new KnockedDownEvent(); @@ -204,10 +207,8 @@ public bool TrySlowdown(EntityUid uid, TimeSpan time, bool refresh, float walkSpeedMultiplier = 1f, float runSpeedMultiplier = 1f, StatusEffectsComponent? status = null) { - if (!Resolve(uid, ref status, false)) - return false; - - if (time <= TimeSpan.Zero) + if (!Resolve(uid, ref status, false) + || time <= TimeSpan.Zero) return false; if (_statusEffect.TryAddStatusEffect(uid, "SlowedDown", time, refresh, status)) @@ -230,14 +231,8 @@ public bool TrySlowdown(EntityUid uid, TimeSpan time, bool refresh, private void OnInteractHand(EntityUid uid, KnockedDownComponent knocked, InteractHandEvent args) { - // This is currently disabled in favor of an interaction verb with the same effect, but more obvious usage. - return; - - if (args.Handled || knocked.HelpTimer > 0f) - return; - - // TODO: This should be an event. - if (HasComp(uid)) + if (args.Handled || knocked.HelpTimer > 0f + || HasComp(uid)) return; // Set it to half the help interval so helping is actually useful... @@ -273,15 +268,19 @@ private void OnAttempt(EntityUid uid, StunnedComponent stunned, CancellableEntit private void OnEquipAttempt(EntityUid uid, StunnedComponent stunned, IsEquippingAttemptEvent args) { // is this a self-equip, or are they being stripped? - if (args.Equipee == uid) - args.Cancel(); + if (args.Equipee != uid) + return; + + args.Cancel(); } private void OnUnequipAttempt(EntityUid uid, StunnedComponent stunned, IsUnequippingAttemptEvent args) { // is this a self-equip, or are they being stripped? - if (args.Unequipee == uid) - args.Cancel(); + if (args.Unequipee != uid) + return; + + args.Cancel(); } #endregion diff --git a/Content.Shared/Telescope/SharedTelescopeSystem.cs b/Content.Shared/Telescope/SharedTelescopeSystem.cs new file mode 100644 index 00000000000..5f9896cc359 --- /dev/null +++ b/Content.Shared/Telescope/SharedTelescopeSystem.cs @@ -0,0 +1,110 @@ +using System.Numerics; +using Content.Shared.Camera; +using Content.Shared.Hands; +using Content.Shared.Hands.Components; +using Content.Shared.Item; +using Robust.Shared.Serialization; + +namespace Content.Shared.Telescope; + +public abstract class SharedTelescopeSystem : EntitySystem +{ + [Dependency] private readonly SharedEyeSystem _eye = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeAllEvent(OnEyeOffsetChanged); + SubscribeLocalEvent(OnUnequip); + SubscribeLocalEvent(OnHandDeselected); + SubscribeLocalEvent(OnShutdown); + } + + private void OnShutdown(Entity ent, ref ComponentShutdown args) + { + if (!TryComp(ent.Comp.LastEntity, out EyeComponent? eye) + || ent.Comp.LastEntity == ent && TerminatingOrDeleted(ent)) + return; + + SetOffset((ent.Comp.LastEntity.Value, eye), Vector2.Zero, ent); + } + + private void OnHandDeselected(Entity ent, ref HandDeselectedEvent args) + { + if (!TryComp(args.User, out EyeComponent? eye)) + return; + + SetOffset((args.User, eye), Vector2.Zero, ent); + } + + private void OnUnequip(Entity ent, ref GotUnequippedHandEvent args) + { + if (!TryComp(args.User, out EyeComponent? eye) + || !HasComp(ent.Owner)) + return; + + SetOffset((args.User, eye), Vector2.Zero, ent); + } + + public TelescopeComponent? GetRightTelescope(EntityUid? ent) + { + TelescopeComponent? telescope = null; + + if (TryComp(ent, out var hands) + && hands.ActiveHandEntity.HasValue + && TryComp(hands.ActiveHandEntity, out var handTelescope)) + telescope = handTelescope; + else if (TryComp(ent, out var entityTelescope)) + telescope = entityTelescope; + + return telescope; + } + + private void OnEyeOffsetChanged(EyeOffsetChangedEvent msg, EntitySessionEventArgs args) + { + if (args.SenderSession.AttachedEntity is not { } ent + || !TryComp(ent, out var eye)) + return; + + var telescope = GetRightTelescope(ent); + + if (telescope == null) + return; + + var offset = Vector2.Lerp(eye.Offset, msg.Offset, telescope.LerpAmount); + + SetOffset((ent, eye), offset, telescope); + } + + private void SetOffset(Entity ent, Vector2 offset, TelescopeComponent telescope) + { + telescope.LastEntity = ent; + + if (TryComp(ent, out CameraRecoilComponent? recoil)) + { + recoil.BaseOffset = offset; + _eye.SetOffset(ent, offset + recoil.CurrentKick, ent); + } + else + { + _eye.SetOffset(ent, offset, ent); + } + } + + public void SetParameters(Entity ent, float? divisor = null, float? lerpAmount = null) + { + var telescope = ent.Comp; + + telescope.Divisor = divisor ?? telescope.Divisor; + telescope.LerpAmount = lerpAmount ?? telescope.LerpAmount; + + Dirty(ent.Owner, telescope); + } +} + +[Serializable, NetSerializable] +public sealed class EyeOffsetChangedEvent : EntityEventArgs +{ + public Vector2 Offset; +} diff --git a/Content.Shared/Telescope/TelescopeComponent.cs b/Content.Shared/Telescope/TelescopeComponent.cs new file mode 100644 index 00000000000..529cc58324f --- /dev/null +++ b/Content.Shared/Telescope/TelescopeComponent.cs @@ -0,0 +1,16 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Telescope; + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class TelescopeComponent : Component +{ + [DataField, AutoNetworkedField] + public float Divisor = 0.1f; + + [DataField, AutoNetworkedField] + public float LerpAmount = 0.1f; + + [ViewVariables] + public EntityUid? LastEntity; +} diff --git a/Resources/Locale/en-US/escape-menu/ui/options-menu.ftl b/Resources/Locale/en-US/escape-menu/ui/options-menu.ftl index 7d6af11f379..eaab10df027 100644 --- a/Resources/Locale/en-US/escape-menu/ui/options-menu.ftl +++ b/Resources/Locale/en-US/escape-menu/ui/options-menu.ftl @@ -269,3 +269,8 @@ ui-options-net-pvs-leave-tooltip = This limits the rate at which the client will ## Toggle window console command cmd-options-desc = Opens options menu, optionally with a specific tab selected. cmd-options-help = Usage: options [tab] + +## Combat Options +ui-options-function-look-up = Look up/Take aim +ui-options-function-auto-get-up = Automatically get up after falling +ui-options-function-hold-look-up = Hold down the key to aim diff --git a/Resources/Locale/ru-RU/Escape-Menu/options-menu.ftl b/Resources/Locale/ru-RU/Escape-Menu/options-menu.ftl new file mode 100644 index 00000000000..a39bc37af90 --- /dev/null +++ b/Resources/Locale/ru-RU/Escape-Menu/options-menu.ftl @@ -0,0 +1,3 @@ +ui-options-function-look-up = Присмотреться/Прицелиться +ui-options-function-auto-get-up = Автоматически вставать при падении +ui-options-function-hold-look-up = Удерживать клавишу для прицеливания diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Snipers/snipers.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Snipers/snipers.yml index 4b58642c307..c85314e91ca 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Snipers/snipers.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Snipers/snipers.yml @@ -81,6 +81,7 @@ - CartridgeAntiMateriel capacity: 5 proto: CartridgeAntiMateriel + - type: Telescope - type: entity name: musket diff --git a/Resources/keybinds.yml b/Resources/keybinds.yml index 2cca749317a..33b4166161a 100644 --- a/Resources/keybinds.yml +++ b/Resources/keybinds.yml @@ -543,3 +543,6 @@ binds: - function: Hotbar9 type: State key: Num9 +- function: LookUp + type: State + key: Space From a8e435cd9b9db45db4f7f1a3e543f2c469ad7641 Mon Sep 17 00:00:00 2001 From: SimpleStation Changelogs Date: Thu, 19 Sep 2024 02:18:59 +0000 Subject: [PATCH 20/55] Automatic Changelog Update (#815) --- Resources/Changelog/Changelog.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index bf97bb66370..6f10c74b5ff 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -6429,3 +6429,18 @@ Entries: id: 6368 time: '2024-09-18T04:30:39.0000000+00:00' url: https://github.com/Simple-Station/Einstein-Engines/pull/926 +- author: Spatison + changes: + - type: Add + message: Added lying down system / Добавлена система лежания + - type: Tweak + message: >- + Lying down now uses do-afters that are visible to other people to + indicate what is going on. + - type: Add + message: Added telescope system / Добавлена система прицеливания + - type: Tweak + message: Now you can aim from Hristov / Теперь можно прицеливаться из Христова + id: 6369 + time: '2024-09-19T02:18:35.0000000+00:00' + url: https://github.com/Simple-Station/Einstein-Engines/pull/815 From 2bfbc72d2dcfb4b00d3f568ac64bd463f70801e2 Mon Sep 17 00:00:00 2001 From: FoxxoTrystan <45297731+FoxxoTrystan@users.noreply.github.com> Date: Thu, 19 Sep 2024 04:24:24 +0200 Subject: [PATCH 21/55] Remove DeltaV Options (#928) # Description This PR Remove and the DeltaV Option Tab while moving the specie filter to the general accessibility tab. View Media for images. This PR also Rebase the option and removed useless duplicated, CVars are moved, .ftl files. Tho cvar name is unchanged so the option will still be enable if you enabled it before. This PR fixes #481 and put PR #630 stale. ---

Media

![image](https://github.com/user-attachments/assets/55fdf75d-2c02-4c25-b0df-e76cce564a33) ![image](https://github.com/user-attachments/assets/88a170fb-ba32-467c-b432-b0a8c554a489)

--- # Changelog :cl: - remove: DeltaV Option Tab (Options moved) --- .../DeltaV/Options/UI/Tabs/DeltaTab.xaml | 23 ---------- .../DeltaV/Options/UI/Tabs/DeltaTab.xaml.cs | 46 ------------------- Content.Client/Options/UI/OptionsMenu.xaml | 2 - Content.Client/Options/UI/OptionsMenu.xaml.cs | 1 - Content.Client/Options/UI/Tabs/MiscTab.xaml | 1 + .../Options/UI/Tabs/MiscTab.xaml.cs | 7 ++- Content.Client/Overlays/DogVisionSystem.cs | 8 ++-- Content.Client/Overlays/UltraVisionSystem.cs | 8 ++-- Content.Shared/CCVar/CCVars.cs | 6 +++ Content.Shared/DeltaV/CCVars/DCCVars.cs | 6 --- .../en-US/deltav/escape-menu/options-menu.ftl | 4 -- .../en-US/escape-menu/ui/options-menu.ftl | 1 + 12 files changed, 22 insertions(+), 91 deletions(-) delete mode 100644 Content.Client/DeltaV/Options/UI/Tabs/DeltaTab.xaml delete mode 100644 Content.Client/DeltaV/Options/UI/Tabs/DeltaTab.xaml.cs delete mode 100644 Resources/Locale/en-US/deltav/escape-menu/options-menu.ftl diff --git a/Content.Client/DeltaV/Options/UI/Tabs/DeltaTab.xaml b/Content.Client/DeltaV/Options/UI/Tabs/DeltaTab.xaml deleted file mode 100644 index f1dae68077d..00000000000 --- a/Content.Client/DeltaV/Options/UI/Tabs/DeltaTab.xaml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - -
public static readonly CVarDef RoundEndPacifist = CVarDef.Create("game.round_end_pacifist", false, CVar.SERVERONLY); - - /// - /// Disables all vision filters for species like Vulpkanin or Harpies. There are good reasons someone might want to disable these. - /// - public static readonly CVarDef NoVisionFilters = - CVarDef.Create("accessibility.no_vision_filters", false, CVar.CLIENTONLY | CVar.ARCHIVE); } diff --git a/Resources/Locale/en-US/deltav/escape-menu/options-menu.ftl b/Resources/Locale/en-US/deltav/escape-menu/options-menu.ftl deleted file mode 100644 index 50d55cb76d0..00000000000 --- a/Resources/Locale/en-US/deltav/escape-menu/options-menu.ftl +++ /dev/null @@ -1,4 +0,0 @@ -ui-options-tab-deltav = DeltaV -ui-options-general-forknotice = Note: These settings are fork-specific and might not apply on other servers. - -ui-options-no-filters = Disable species vision filters diff --git a/Resources/Locale/en-US/escape-menu/ui/options-menu.ftl b/Resources/Locale/en-US/escape-menu/ui/options-menu.ftl index eaab10df027..ea24439f708 100644 --- a/Resources/Locale/en-US/escape-menu/ui/options-menu.ftl +++ b/Resources/Locale/en-US/escape-menu/ui/options-menu.ftl @@ -52,6 +52,7 @@ ui-options-fancy-speech = Show names in speech bubbles ui-options-fancy-name-background = Add background to speech bubble names ui-options-enable-color-name = Add colors to character names ui-options-colorblind-friendly = Colorblind friendly mode +ui-options-no-filters = Disable species vision filters ui-options-reduced-motion = Reduce motion of visual effects ui-options-chat-window-opacity = Chat window opacity ui-options-chat-window-opacity-percent = { TOSTRING($opacity, "P0") } From 0cf7803501f319461a86f7faff8e0fde3e20b392 Mon Sep 17 00:00:00 2001 From: SimpleStation Changelogs Date: Thu, 19 Sep 2024 02:24:51 +0000 Subject: [PATCH 22/55] Automatic Changelog Update (#928) --- Resources/Changelog/Changelog.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 6f10c74b5ff..eb0238bdb2a 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -6444,3 +6444,10 @@ Entries: id: 6369 time: '2024-09-19T02:18:35.0000000+00:00' url: https://github.com/Simple-Station/Einstein-Engines/pull/815 +- author: FoxxoTrystan + changes: + - type: Remove + message: DeltaV Option Tab (Options moved) + id: 6370 + time: '2024-09-19T02:24:25.0000000+00:00' + url: https://github.com/Simple-Station/Einstein-Engines/pull/928 From d6dd2ea8bc95388ffc127325865d52a53751349a Mon Sep 17 00:00:00 2001 From: VMSolidus Date: Wed, 18 Sep 2024 23:29:28 -0400 Subject: [PATCH 23/55] Fix SpawnAndDeleteAllEntities (#932) # Description After doing an archaological exploration of the fucking SpawnAndDeleteAllEntities test fail, I eventually tracked down the bug to an issue where InternalEncryptionKeySpawner is randomly being handed a Null EntityUid which was Null Forgiven to make the compiler shut up. The actual EntityUid factually cannot be null during ordinary operation, except for the dumbass race condition provided by TestSpawnAndDeleteAllEntities. --- Content.Server/Silicon/IPC/InternalEncryptionKeySpawner.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Content.Server/Silicon/IPC/InternalEncryptionKeySpawner.cs b/Content.Server/Silicon/IPC/InternalEncryptionKeySpawner.cs index 7f5d216c92f..5f799a102e7 100644 --- a/Content.Server/Silicon/IPC/InternalEncryptionKeySpawner.cs +++ b/Content.Server/Silicon/IPC/InternalEncryptionKeySpawner.cs @@ -2,7 +2,6 @@ using Content.Shared.Radio.Components; using Content.Shared.Containers; using Robust.Shared.Containers; -using Content.Server.Cargo.Components; namespace Content.Server.Silicon.IPC; public sealed partial class InternalEncryptionKeySpawner : EntitySystem @@ -10,7 +9,11 @@ public sealed partial class InternalEncryptionKeySpawner : EntitySystem [Dependency] private readonly SharedContainerSystem _container = default!; public void TryInsertEncryptionKey(EntityUid target, StartingGearPrototype startingGear, IEntityManager entityManager) { - if (!TryComp(target, out var keyHolderComp) +#pragma warning disable CS8073 + if (target == null // target can be null during race conditions intentionally created by awful tests. +#pragma warning restore CS8073 + || !TryComp(target, out var keyHolderComp) + || keyHolderComp is null || !startingGear.Equipment.TryGetValue("ears", out var earEquipString) || string.IsNullOrEmpty(earEquipString)) return; From dee5a409aecd50ded7610f7c6f8d727f4b76e97e Mon Sep 17 00:00:00 2001 From: VMSolidus Date: Wed, 18 Sep 2024 23:29:41 -0400 Subject: [PATCH 24/55] [Port] StepTriggerGroup From WhiteDream (#929) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Description This is a port of https://github.com/WWhiteDreamProject/wwdpublic/pull/53 from White Dream. This PR improves the StepTriggerImmune component by making it operate on a more granular Blacklist system, such that StepTriggerImmune entities can further clarify via prototypes which kinds of floor traps they are immune to, such as landmines/mousetraps, and not have blanket immunity to everything. Because it turns out things like Lava and Soap also were caught by the immunity, when really we just wanted Harpies & Felinids to not trigger landmines.

Media

> # Описание > Необходимо настроить модификатор урона, чтобы IPC не получали урон от осколков стекла. > > Иммунитет StepTriggerImmuneComponent доработан. Теперь имеются несколько типов (types): Lava - тип тайла, наступив на который появляется урон. Это собственно лава и LiquidPlasma Landmine - мины. Chasm - дырка в карте, куда можно провалиться Mousetrap - Мышеловка SlipTile - Все, что должно подскальзывать игроков, имеющее размер тайла SlipEntity - Все, что должно подскальзывать игроков, имеющее развер энтити. Разделено для баланса. Самые ловки могут игнорировать мелкие предметы (энтити), т.е. уворачиваться от них. Но большие по площади вещи (тайлы по типу разлитой воды, бананиума) просчитываются отдельно. > > # Изменения > * [x] Улучшить StepTriggerSystem (Immune) > * [x] Добавлены типы триггера. - Lava Landmine Shard Chasm Mousetrap SlipTile SlipEntity > * [x] Исправить осколки у IPC > * [x] Исправить отсутствие урона от лавы и падение в дыры у фелинидов и гарпий. > > 🆑 Hell_Cat > > * Feature: StepTriggerSystem is improved | Улучшена StepTriggerSystem > * fix: IPC: Immunity for shards and SpiderWeb | Иммунитет осколкам. > * fix: Felinid | Фелиниды : Immunity for Shard Landmine Mousetrap SlipEntities | Иммунитет для осколков, жидкости, мин, мышеловок, мыла и бананов. > * fix: Harpy | Гарпия : Immunity for Shards Landmine Mousetrap | Иммунитет для осколков, жидкости, мин и мышеловок. > * fix: Mice | Мыши : Don't blow up on landmines | Мыши не подрываются на минах.

# Changelog :cl: Hell_Cat Feature: StepTriggerSystem has been improved with new StepTriggerGroups. Additionally, the StepTriggerImmune component now allows declaring for specific StepTriggerGroups for a given entity to be immune to. Some examples may be, Felinids, Mice, and Harpies being unable to set off Landmines. --------- Signed-off-by: VMSolidus Co-authored-by: Ivan <126400932+HellCatten@users.noreply.github.com> Co-authored-by: FoxxoTrystan <45297731+FoxxoTrystan@users.noreply.github.com> --- .../Components/StepTriggerComponent.cs | 14 ++-- .../Components/StepTriggerImmuneComponent.cs | 12 +++- .../Prototypes/StepTriggerGroup.cs | 72 +++++++++++++++++++ .../Prototypes/StepTriggerTypePrototype.cs | 15 ++++ .../StepTrigger/Systems/StepTriggerSystem.cs | 9 ++- .../Entities/Effects/chemistry_effects.yml | 5 +- .../Prototypes/Entities/Effects/puddle.yml | 3 + .../Prototypes/Entities/Mobs/NPCs/animals.yml | 4 ++ .../Prototypes/Entities/Mobs/NPCs/silicon.yml | 3 + .../Prototypes/Entities/Mobs/Player/ipc.yml | 4 ++ .../Entities/Mobs/Species/diona.yml | 4 ++ .../Entities/Mobs/Species/harpy.yml | 5 ++ .../Objects/Consumable/Food/produce.yml | 3 + .../Entities/Objects/Devices/mousetrap.yml | 3 + .../Entities/Objects/Devices/pda.yml | 3 + .../Prototypes/Entities/Objects/Fun/dice.yml | 3 + .../Entities/Objects/Materials/shards.yml | 3 + .../Entities/Objects/Misc/land_mine.yml | 3 + .../Entities/Objects/Misc/spider_web.yml | 3 + .../Objects/Specific/Janitorial/soap.yml | 9 +++ .../Prototypes/Entities/Tiles/bananium.yml | 3 + Resources/Prototypes/Entities/Tiles/chasm.yml | 9 ++- Resources/Prototypes/Entities/Tiles/lava.yml | 3 + .../Entities/Tiles/liquid_plasma.yml | 3 + .../Entities/Mobs/Species/felinid.yml | 6 ++ .../StepTrigger/StepTriggerTypes.yml | 20 ++++++ Resources/Prototypes/Traits/skills.yml | 6 ++ 27 files changed, 218 insertions(+), 12 deletions(-) create mode 100644 Content.Shared/StepTrigger/Prototypes/StepTriggerGroup.cs create mode 100644 Content.Shared/StepTrigger/Prototypes/StepTriggerTypePrototype.cs create mode 100644 Resources/Prototypes/StepTrigger/StepTriggerTypes.yml diff --git a/Content.Shared/StepTrigger/Components/StepTriggerComponent.cs b/Content.Shared/StepTrigger/Components/StepTriggerComponent.cs index b8483d021a4..d12c2c983ee 100644 --- a/Content.Shared/StepTrigger/Components/StepTriggerComponent.cs +++ b/Content.Shared/StepTrigger/Components/StepTriggerComponent.cs @@ -1,3 +1,4 @@ +using Content.Shared.StepTrigger.Prototypes; using Content.Shared.StepTrigger.Systems; using Content.Shared.Whitelist; using Robust.Shared.GameStates; @@ -53,15 +54,18 @@ public sealed partial class StepTriggerComponent : Component public bool IgnoreWeightless; /// - /// Does this have separate "StepOn" and "StepOff" triggers. + /// Does this have separate "StepOn" and "StepOff" triggers. /// [DataField, AutoNetworkedField] public bool StepOn = false; + + /// + /// If TriggerGroups is specified, it will check StepTriggerImmunityComponent to have the same TriggerType to activate immunity + /// + [DataField] + public StepTriggerGroup? TriggerGroups; } [RegisterComponent] [Access(typeof(StepTriggerSystem))] -public sealed partial class StepTriggerActiveComponent : Component -{ - -} +public sealed partial class StepTriggerActiveComponent : Component { } diff --git a/Content.Shared/StepTrigger/Components/StepTriggerImmuneComponent.cs b/Content.Shared/StepTrigger/Components/StepTriggerImmuneComponent.cs index 4321334a3ae..1b92905fa61 100644 --- a/Content.Shared/StepTrigger/Components/StepTriggerImmuneComponent.cs +++ b/Content.Shared/StepTrigger/Components/StepTriggerImmuneComponent.cs @@ -1,3 +1,5 @@ +using Content.Shared.StepTrigger.Prototypes; +using Content.Shared.StepTrigger.Systems; using Robust.Shared.GameStates; namespace Content.Shared.StepTrigger.Components; @@ -12,4 +14,12 @@ namespace Content.Shared.StepTrigger.Components; /// Consider using a subscription to StepTriggerAttemptEvent if you wish to be more selective. /// [RegisterComponent, NetworkedComponent] -public sealed partial class StepTriggerImmuneComponent : Component { } +[Access(typeof(StepTriggerSystem))] +public sealed partial class StepTriggerImmuneComponent : Component +{ + /// + /// WhiteList of immunity step triggers. + /// + [DataField] + public StepTriggerGroup? Whitelist; +} diff --git a/Content.Shared/StepTrigger/Prototypes/StepTriggerGroup.cs b/Content.Shared/StepTrigger/Prototypes/StepTriggerGroup.cs new file mode 100644 index 00000000000..5b64085e9a9 --- /dev/null +++ b/Content.Shared/StepTrigger/Prototypes/StepTriggerGroup.cs @@ -0,0 +1,72 @@ +using Content.Shared.StepTrigger.Components; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; + +namespace Content.Shared.StepTrigger.Prototypes; + +/// +/// A group of +/// Used to determine StepTriggerTypes like Tags. +/// Used for better work with Immunity. +/// StepTriggerTypes in StepTriggerTypes.yml +/// +/// +/// stepTriggerGroups: +/// types: +/// - Lava +/// - Landmine +/// - Shard +/// - Chasm +/// - Mousetrap +/// - SlipTile +/// - SlipEntity +/// +[DataDefinition] +[Serializable, NetSerializable] +public sealed partial class StepTriggerGroup +{ + [DataField] + public List>? Types = null; + + /// + /// Checks if types of this StepTriggerGroup is similar to types of AnotherGroup + /// + public bool IsValid(StepTriggerGroup? anotherGroup) + { + if (Types is null) + return false; + + foreach (var type in Types) + { + if (anotherGroup != null + && anotherGroup.Types != null + && anotherGroup.Types.Contains(type)) + return true; + } + return false; + } + + /// + /// Checks validation (if types of this StepTriggerGroup are similar to types of + /// another StepTriggerComponent. + /// + public bool IsValid(StepTriggerComponent component) + { + if (component.TriggerGroups is null) + return false; + + return IsValid(component.TriggerGroups); + } + + /// + /// Checks validation (if types of this StepTriggerGroup are similar to types of + /// another StepTriggerImmuneComponent. + /// + public bool IsValid(StepTriggerImmuneComponent component) + { + if (component.Whitelist is null) + return false; + + return IsValid(component.Whitelist); + } +} diff --git a/Content.Shared/StepTrigger/Prototypes/StepTriggerTypePrototype.cs b/Content.Shared/StepTrigger/Prototypes/StepTriggerTypePrototype.cs new file mode 100644 index 00000000000..732eb4b732e --- /dev/null +++ b/Content.Shared/StepTrigger/Prototypes/StepTriggerTypePrototype.cs @@ -0,0 +1,15 @@ +using Robust.Shared.Prototypes; + +namespace Content.Shared.StepTrigger.Prototypes; + +/// +/// Prototype representing a StepTriggerType in YAML. +/// Meant to only have an ID property, as that is the only thing that +/// gets saved in StepTriggerGroup. +/// +[Prototype] +public sealed partial class StepTriggerTypePrototype : IPrototype +{ + [ViewVariables, IdDataField] + public string ID { get; private set; } = default!; +} diff --git a/Content.Shared/StepTrigger/Systems/StepTriggerSystem.cs b/Content.Shared/StepTrigger/Systems/StepTriggerSystem.cs index 89655afac23..d0cd5c4b4e2 100644 --- a/Content.Shared/StepTrigger/Systems/StepTriggerSystem.cs +++ b/Content.Shared/StepTrigger/Systems/StepTriggerSystem.cs @@ -118,11 +118,16 @@ private void UpdateColliding(EntityUid uid, StepTriggerComponent component, Tran private bool CanTrigger(EntityUid uid, EntityUid otherUid, StepTriggerComponent component) { - if (HasComp(otherUid) - || !component.Active + if (!component.Active || component.CurrentlySteppedOn.Contains(otherUid)) return false; + // Immunity checks + if (TryComp(otherUid, out var stepTriggerImmuneComponent) + && component.TriggerGroups != null + && component.TriggerGroups.IsValid(stepTriggerImmuneComponent)) + return false; + // Can't trigger if we don't ignore weightless entities // and the entity is flying or currently weightless // Makes sense simulation wise to have this be part of steptrigger directly IMO diff --git a/Resources/Prototypes/Entities/Effects/chemistry_effects.yml b/Resources/Prototypes/Entities/Effects/chemistry_effects.yml index 096e88bcb6f..469bab32782 100644 --- a/Resources/Prototypes/Entities/Effects/chemistry_effects.yml +++ b/Resources/Prototypes/Entities/Effects/chemistry_effects.yml @@ -77,6 +77,9 @@ animationState: foam-dissolve - type: Slippery - type: StepTrigger + triggerGroups: + types: + - SlipTile # disabled until foam reagent duplication is fixed #- type: ScoopableSolution # solution: solutionArea @@ -135,7 +138,7 @@ - type: RCDDeconstructable cost: 2 delay: 2 - fx: EffectRCDDeconstruct2 + fx: EffectRCDDeconstruct2 - type: Clickable - type: InteractionOutline - type: Sprite diff --git a/Resources/Prototypes/Entities/Effects/puddle.yml b/Resources/Prototypes/Entities/Effects/puddle.yml index 2c845e1d0f0..d3156c50a3f 100644 --- a/Resources/Prototypes/Entities/Effects/puddle.yml +++ b/Resources/Prototypes/Entities/Effects/puddle.yml @@ -149,6 +149,9 @@ - type: EdgeSpreader id: Puddle - type: StepTrigger + triggerGroups: + types: + - SlipTile - type: Drink delay: 3 transferAmount: 1 diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml index 29234ea34cf..01eed043139 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml @@ -1689,6 +1689,10 @@ - type: FireVisuals sprite: Mobs/Effects/onfire.rsi normalState: Mouse_burning + - type: StepTriggerImmune + whitelist: + types: + - Landmine - type: entity parent: MobMouse diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml b/Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml index d01fc8b8de2..90d9a5e5c97 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml @@ -137,6 +137,9 @@ - type: Speech speechVerb: Cluwne - type: StepTrigger + triggerGroups: + types: + - SlipEntity intersectRatio: 0.2 - type: Fixtures fixtures: diff --git a/Resources/Prototypes/Entities/Mobs/Player/ipc.yml b/Resources/Prototypes/Entities/Mobs/Player/ipc.yml index 1bed477b7d1..247226dc7d5 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/ipc.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/ipc.yml @@ -111,6 +111,10 @@ - type: OfferItem - type: LayingDown - type: Carriable + - type: StepTriggerImmune + whitelist: + types: + - Shard - type: entity save: false diff --git a/Resources/Prototypes/Entities/Mobs/Species/diona.yml b/Resources/Prototypes/Entities/Mobs/Species/diona.yml index 42383d9a426..81c9e596163 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/diona.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/diona.yml @@ -114,6 +114,10 @@ walkModifier: 0.75 - type: SpeedModifierImmunity - type: NoSlip + - type: StepTriggerImmune + whitelist: + types: + - Shard - type: entity parent: BaseSpeciesDummy diff --git a/Resources/Prototypes/Entities/Mobs/Species/harpy.yml b/Resources/Prototypes/Entities/Mobs/Species/harpy.yml index 05ac3de8bb3..8882da868b1 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/harpy.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/harpy.yml @@ -123,6 +123,11 @@ - GalacticCommon - SolCommon - type: StepTriggerImmune + whitelist: + types: + - Shard + - Landmine + - Mousetrap - type: entity save: false diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/produce.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/produce.yml index 3f0277e1bc3..930d7fa64dc 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/produce.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/produce.yml @@ -314,6 +314,9 @@ launchForwardsMultiplier: 1.5 - type: StepTrigger intersectRatio: 0.2 + triggerGroups: + types: + - SlipEntity - type: CollisionWake enabled: false - type: Physics diff --git a/Resources/Prototypes/Entities/Objects/Devices/mousetrap.yml b/Resources/Prototypes/Entities/Objects/Devices/mousetrap.yml index c3bd74dd751..a93cd545bfc 100644 --- a/Resources/Prototypes/Entities/Objects/Devices/mousetrap.yml +++ b/Resources/Prototypes/Entities/Objects/Devices/mousetrap.yml @@ -13,6 +13,9 @@ - type: StepTrigger intersectRatio: 0.2 requiredTriggeredSpeed: 0 + triggerGroups: + types: + - Mousetrap - type: Mousetrap - type: TriggerOnStepTrigger - type: ShoesRequiredStepTrigger diff --git a/Resources/Prototypes/Entities/Objects/Devices/pda.yml b/Resources/Prototypes/Entities/Objects/Devices/pda.yml index f0d07f9434a..1a388997835 100644 --- a/Resources/Prototypes/Entities/Objects/Devices/pda.yml +++ b/Resources/Prototypes/Entities/Objects/Devices/pda.yml @@ -272,6 +272,9 @@ paralyzeTime: 4 launchForwardsMultiplier: 1.5 - type: StepTrigger + triggerGroups: + types: + - SlipEntity - type: CollisionWake enabled: false - type: Physics diff --git a/Resources/Prototypes/Entities/Objects/Fun/dice.yml b/Resources/Prototypes/Entities/Objects/Fun/dice.yml index 852a1c2699c..f7395ef9ca6 100644 --- a/Resources/Prototypes/Entities/Objects/Fun/dice.yml +++ b/Resources/Prototypes/Entities/Objects/Fun/dice.yml @@ -119,6 +119,9 @@ mask: - ItemMask - type: StepTrigger + triggerGroups: + types: + - Shard intersectRatio: 0.2 - type: TriggerOnStepTrigger - type: ShoesRequiredStepTrigger diff --git a/Resources/Prototypes/Entities/Objects/Materials/shards.yml b/Resources/Prototypes/Entities/Objects/Materials/shards.yml index 6cdc066cf10..34685343145 100644 --- a/Resources/Prototypes/Entities/Objects/Materials/shards.yml +++ b/Resources/Prototypes/Entities/Objects/Materials/shards.yml @@ -64,6 +64,9 @@ - !type:DoActsBehavior acts: [ "Destruction" ] - type: StepTrigger + triggerGroups: + types: + - Shard intersectRatio: 0.2 - type: ShoesRequiredStepTrigger - type: Slippery diff --git a/Resources/Prototypes/Entities/Objects/Misc/land_mine.yml b/Resources/Prototypes/Entities/Objects/Misc/land_mine.yml index a3e3485bc65..97053660a19 100644 --- a/Resources/Prototypes/Entities/Objects/Misc/land_mine.yml +++ b/Resources/Prototypes/Entities/Objects/Misc/land_mine.yml @@ -40,6 +40,9 @@ params: maxDistance: 10 - type: StepTrigger + triggerGroups: + types: + - Landmine requiredTriggeredSpeed: 0 stepOn: true diff --git a/Resources/Prototypes/Entities/Objects/Misc/spider_web.yml b/Resources/Prototypes/Entities/Objects/Misc/spider_web.yml index 9561fa3538f..bb284000a76 100644 --- a/Resources/Prototypes/Entities/Objects/Misc/spider_web.yml +++ b/Resources/Prototypes/Entities/Objects/Misc/spider_web.yml @@ -111,6 +111,9 @@ paralyzeTime: 2 launchForwardsMultiplier: 1.5 - type: StepTrigger + triggerGroups: + types: + - SlipTile intersectRatio: 0.2 - type: Physics - type: Fixtures diff --git a/Resources/Prototypes/Entities/Objects/Specific/Janitorial/soap.yml b/Resources/Prototypes/Entities/Objects/Specific/Janitorial/soap.yml index 5678de6bafc..5fe88f8d0cf 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Janitorial/soap.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Janitorial/soap.yml @@ -24,6 +24,9 @@ paralyzeTime: 2 launchForwardsMultiplier: 1.5 - type: StepTrigger + triggerGroups: + types: + - SlipEntity intersectRatio: 0.2 - type: CollisionWake enabled: false @@ -157,6 +160,9 @@ paralyzeTime: 5 launchForwardsMultiplier: 2.5 - type: StepTrigger + triggerGroups: + types: + - SlipEntity intersectRatio: 0.04 - type: Item heldPrefix: syndie @@ -198,6 +204,9 @@ - type: Slippery paralyzeTime: 2 - type: StepTrigger + triggerGroups: + types: + - SlipEntity - type: Item heldPrefix: gibs - type: FlavorProfile diff --git a/Resources/Prototypes/Entities/Tiles/bananium.yml b/Resources/Prototypes/Entities/Tiles/bananium.yml index c9a6ec28441..9e8a46b2c35 100644 --- a/Resources/Prototypes/Entities/Tiles/bananium.yml +++ b/Resources/Prototypes/Entities/Tiles/bananium.yml @@ -47,6 +47,9 @@ paralyzeTime: 2 launchForwardsMultiplier: 1.5 - type: StepTrigger + triggerGroups: + types: + - SlipTile intersectRatio: 0.2 - type: Physics bodyType: Static diff --git a/Resources/Prototypes/Entities/Tiles/chasm.yml b/Resources/Prototypes/Entities/Tiles/chasm.yml index 23f3ad83950..85bc7b5ab34 100644 --- a/Resources/Prototypes/Entities/Tiles/chasm.yml +++ b/Resources/Prototypes/Entities/Tiles/chasm.yml @@ -14,6 +14,9 @@ blacklist: tags: - Catwalk + triggerGroups: + types: + - Chasm - type: Transform anchored: true - type: Clickable @@ -55,7 +58,7 @@ sprite: Tiles/Planet/Chasms/chromite_chasm.rsi - type: Icon sprite: Tiles/Planet/Chasms/chromite_chasm.rsi - + - type: entity parent: FloorChasmEntity id: FloorDesertChasm @@ -65,7 +68,7 @@ sprite: Tiles/Planet/Chasms/desert_chasm.rsi - type: Icon sprite: Tiles/Planet/Chasms/desert_chasm.rsi - + - type: entity parent: FloorChasmEntity id: FloorSnowChasm @@ -74,4 +77,4 @@ - type: Sprite sprite: Tiles/Planet/Chasms/snow_chasm.rsi - type: Icon - sprite: Tiles/Planet/Chasms/snow_chasm.rsi \ No newline at end of file + sprite: Tiles/Planet/Chasms/snow_chasm.rsi diff --git a/Resources/Prototypes/Entities/Tiles/lava.yml b/Resources/Prototypes/Entities/Tiles/lava.yml index 72641309b31..9d61304af9d 100644 --- a/Resources/Prototypes/Entities/Tiles/lava.yml +++ b/Resources/Prototypes/Entities/Tiles/lava.yml @@ -13,6 +13,9 @@ blacklist: tags: - Catwalk + triggerGroups: + types: + - Lava - type: Lava fireStacks: 0.75 - type: Transform diff --git a/Resources/Prototypes/Entities/Tiles/liquid_plasma.yml b/Resources/Prototypes/Entities/Tiles/liquid_plasma.yml index 08f46538631..500286ead31 100644 --- a/Resources/Prototypes/Entities/Tiles/liquid_plasma.yml +++ b/Resources/Prototypes/Entities/Tiles/liquid_plasma.yml @@ -13,6 +13,9 @@ blacklist: tags: - Catwalk + triggerGroups: + types: + - Lava - type: Lava fireStacks: 0.75 - type: Transform diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/felinid.yml b/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/felinid.yml index a1ca357b080..411eb13444b 100644 --- a/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/felinid.yml +++ b/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/felinid.yml @@ -80,6 +80,12 @@ stripTimeReduction: 0 stripTimeMultiplier: 0.667 - type: StepTriggerImmune + whitelist: + types: + - Shard + - Landmine + - Mousetrap + - SlipEntity - type: entity save: false diff --git a/Resources/Prototypes/StepTrigger/StepTriggerTypes.yml b/Resources/Prototypes/StepTrigger/StepTriggerTypes.yml new file mode 100644 index 00000000000..6d03908a17f --- /dev/null +++ b/Resources/Prototypes/StepTrigger/StepTriggerTypes.yml @@ -0,0 +1,20 @@ +- type: stepTriggerType + id: Lava + +- type: stepTriggerType + id: Landmine + +- type: stepTriggerType + id: Shard + +- type: stepTriggerType + id: Chasm + +- type: stepTriggerType + id: Mousetrap + +- type: stepTriggerType + id: SlipTile + +- type: stepTriggerType + id: SlipEntity diff --git a/Resources/Prototypes/Traits/skills.yml b/Resources/Prototypes/Traits/skills.yml index d757cbcd564..fa79666c7ab 100644 --- a/Resources/Prototypes/Traits/skills.yml +++ b/Resources/Prototypes/Traits/skills.yml @@ -270,6 +270,12 @@ points: -3 components: - type: StepTriggerImmune + whitelist: + types: + - Shard + - Landmine + - Mousetrap + - SlipEntity requirements: - !type:CharacterSpeciesRequirement inverted: true From 6d1aae9844dcdb31d28d6070c0f0d067c8ee8cca Mon Sep 17 00:00:00 2001 From: SimpleStation Changelogs Date: Thu, 19 Sep 2024 03:30:24 +0000 Subject: [PATCH 25/55] Automatic Changelog Update (#929) --- Resources/Changelog/Changelog.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index eb0238bdb2a..d7568628f6f 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -6451,3 +6451,8 @@ Entries: id: 6370 time: '2024-09-19T02:24:25.0000000+00:00' url: https://github.com/Simple-Station/Einstein-Engines/pull/928 +- author: Hell_Cat + changes: [] + id: 6371 + time: '2024-09-19T03:29:41.0000000+00:00' + url: https://github.com/Simple-Station/Einstein-Engines/pull/929 From 0aa49c14917355fecdd44cfc65d85c1f334e2d7e Mon Sep 17 00:00:00 2001 From: VMSolidus Date: Thu, 19 Sep 2024 16:37:51 -0400 Subject: [PATCH 26/55] Fix Laying Down Bugs (#933) # Description This fixes prediction issues, both where pressing the Lay down key causes all entities on your screen to glitch and lay down, as well as the bug where upon receiving ANY movement input, a player would be visually changed on their screen to be laying down. # Changelog :cl: - fix: Fixed issues with the LayingDownSystem. Laying down no longer causes all entities on your screen to lay down at once. Using any movement input no longer causes the character to appear to the player as to be laying down. --- Content.Client/Standing/LayingDownSystem.cs | 8 +++++--- Content.Shared/Standing/SharedLayingDownSystem.cs | 3 ++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Content.Client/Standing/LayingDownSystem.cs b/Content.Client/Standing/LayingDownSystem.cs index 6cc5f7921ac..594883ac001 100644 --- a/Content.Client/Standing/LayingDownSystem.cs +++ b/Content.Client/Standing/LayingDownSystem.cs @@ -1,4 +1,4 @@ -using Content.Shared.ActionBlocker; +using Content.Shared.Buckle; using Content.Shared.Rotation; using Content.Shared.Standing; using Robust.Client.GameObjects; @@ -11,8 +11,9 @@ public sealed class LayingDownSystem : SharedLayingDownSystem { [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly IEyeManager _eyeManager = default!; + [Dependency] private readonly StandingStateSystem _standing = default!; [Dependency] private readonly AnimationPlayerSystem _animation = default!; - [Dependency] private readonly ActionBlockerSystem _actionBlocker = default!; + [Dependency] private readonly SharedBuckleSystem _buckle = default!; public override void Initialize() { @@ -26,7 +27,8 @@ public override void Initialize() private void OnMovementInput(EntityUid uid, LayingDownComponent component, MoveEvent args) { if (!_timing.IsFirstTimePredicted - || !_actionBlocker.CanMove(uid) + || !_standing.IsDown(uid) + || _buckle.IsBuckled(uid) || _animation.HasRunningAnimation(uid, "rotate") || !TryComp(uid, out var transform) || !TryComp(uid, out var sprite) diff --git a/Content.Shared/Standing/SharedLayingDownSystem.cs b/Content.Shared/Standing/SharedLayingDownSystem.cs index 47ad01949cb..7a82d4b7aa8 100644 --- a/Content.Shared/Standing/SharedLayingDownSystem.cs +++ b/Content.Shared/Standing/SharedLayingDownSystem.cs @@ -40,7 +40,8 @@ public override void Shutdown() private void ToggleStanding(ICommonSession? session) { - if (session?.AttachedEntity == null + if (session is not { AttachedEntity: { Valid: true } uid } _ + || !Exists(uid) || !HasComp(session.AttachedEntity) || _gravity.IsWeightless(session.AttachedEntity.Value)) return; From 7bce1e92a30359ff674a585925caf4884d01017c Mon Sep 17 00:00:00 2001 From: SimpleStation Changelogs Date: Thu, 19 Sep 2024 20:38:15 +0000 Subject: [PATCH 27/55] Automatic Changelog Update (#933) --- Resources/Changelog/Changelog.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index d7568628f6f..a233a61a2d4 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -6456,3 +6456,14 @@ Entries: id: 6371 time: '2024-09-19T03:29:41.0000000+00:00' url: https://github.com/Simple-Station/Einstein-Engines/pull/929 +- author: VMSolidus + changes: + - type: Fix + message: >- + Fixed issues with the LayingDownSystem. Laying down no longer causes all + entities on your screen to lay down at once. Using any movement input no + longer causes the character to appear to the player as to be laying + down. + id: 6372 + time: '2024-09-19T20:37:51.0000000+00:00' + url: https://github.com/Simple-Station/Einstein-Engines/pull/933 From 8b5dcd6efe52b7af65094a6cbf0de4cee40b84cc Mon Sep 17 00:00:00 2001 From: VMSolidus Date: Thu, 19 Sep 2024 21:47:52 -0400 Subject: [PATCH 28/55] Non-Destructive Universal Language (#899) # Description UniversalLanguageSpeakerComponent has NUMEROUS issues, which previously were easy to ignore since it was a language only obtainable by Admins. But now that it's both a Psionic Power(And a trait that adds said power), the issues with it have been highlighted. Here's just a FEW 1. UniversalLanguageSpeaker overwrites all known languages, preventing you from speaking anything else 2. It overwrites the ability to use sign language. 3. It also overwrites the Mute trait. To fix that, I've made it follow *MOSTLY* the logic all the other languages use, so that it's less of a special snowflake case. Now if you have access to it, it will appear in your language list alongside other languages, rather than fully replacing the entire list. That way you can intentionally choose not to speak in a language understood by all. Fuck it, I also added the ability for the TraitSystem to just call LanguageSystem and directly add arbitrarily any desired language, rather than needing a component to do so. # Changelog :cl: - fix: UniversalLanguageSpeaker(And Xenoglossy by extension) will now appear in your language menu alongside other known languages, rather than replace all known languages. You can effectively now choose whether or not to speak it, or to use a normal language. - add: Traits can now add Languages directly. - add: Traits can now remove Languages directly. --------- Signed-off-by: VMSolidus Co-authored-by: Mnemotechnican <69920617+Mnemotechnician@users.noreply.github.com> Co-authored-by: FoxxoTrystan <45297731+FoxxoTrystan@users.noreply.github.com> Co-authored-by: DEATHB4DEFEAT <77995199+DEATHB4DEFEAT@users.noreply.github.com> --- .../Chemistry/ReagentEffects/MakeSentient.cs | 2 +- .../Language/LanguageSystem.Networking.cs | 7 +- Content.Server/Language/LanguageSystem.cs | 70 +++++++++---------- Content.Server/Language/TranslatorSystem.cs | 10 +-- .../Traits/Assorted/ForeignerTraitSystem.cs | 2 +- .../LanguageKnowledgeModifierComponent.cs | 23 ------ .../LanguageKnowledgeModifierSystem.cs | 35 ---------- Content.Server/Traits/TraitSystem.cs | 70 +++++++++++++++++++ .../Traits/Prototypes/TraitPrototype.cs | 24 +++++++ .../Entities/Structures/Specific/oracle.yml | 7 ++ .../Structures/Research/sophicscribe.yml | 7 ++ Resources/Prototypes/Traits/skills.yml | 10 ++- 12 files changed, 154 insertions(+), 113 deletions(-) delete mode 100644 Content.Server/Traits/Assorted/LanguageKnowledgeModifierComponent.cs delete mode 100644 Content.Server/Traits/Assorted/LanguageKnowledgeModifierSystem.cs diff --git a/Content.Server/Chemistry/ReagentEffects/MakeSentient.cs b/Content.Server/Chemistry/ReagentEffects/MakeSentient.cs index 5654f9067b5..a84e21b997e 100644 --- a/Content.Server/Chemistry/ReagentEffects/MakeSentient.cs +++ b/Content.Server/Chemistry/ReagentEffects/MakeSentient.cs @@ -41,7 +41,7 @@ public override void Effect(ReagentEffectArgs args) if (!knowledge.SpokenLanguages.Contains(fallback)) knowledge.SpokenLanguages.Add(fallback); - IoCManager.Resolve().GetEntitySystem().UpdateEntityLanguages(uid, speaker); + IoCManager.Resolve().GetEntitySystem().UpdateEntityLanguages(uid); // Stops from adding a ghost role to things like people who already have a mind if (entityManager.TryGetComponent(uid, out var mindContainer) && mindContainer.HasMind) diff --git a/Content.Server/Language/LanguageSystem.Networking.cs b/Content.Server/Language/LanguageSystem.Networking.cs index 572e2961fde..5f7f2742734 100644 --- a/Content.Server/Language/LanguageSystem.Networking.cs +++ b/Content.Server/Language/LanguageSystem.Networking.cs @@ -64,12 +64,7 @@ private void SendLanguageStateToClient(ICommonSession session, LanguageSpeakerCo // TODO this is really stupid and can be avoided if we just make everything shared... private void SendLanguageStateToClient(EntityUid uid, ICommonSession session, LanguageSpeakerComponent? component = null) { - var isUniversal = HasComp(uid); - if (!isUniversal) - Resolve(uid, ref component, logMissing: false); - - // I really don't want to call 3 getter methods here, so we'll just have this slightly hardcoded solution - var message = isUniversal || component == null + var message = !Resolve(uid, ref component, logMissing: false) ? new LanguagesUpdatedMessage(UniversalPrototype, [UniversalPrototype], [UniversalPrototype]) : new LanguagesUpdatedMessage(component.CurrentLanguage, component.SpokenLanguages, component.UnderstoodLanguages); diff --git a/Content.Server/Language/LanguageSystem.cs b/Content.Server/Language/LanguageSystem.cs index e68489e9e28..a1c30997e2d 100644 --- a/Content.Server/Language/LanguageSystem.cs +++ b/Content.Server/Language/LanguageSystem.cs @@ -2,7 +2,6 @@ using Content.Server.Language.Events; using Content.Shared.Language; using Content.Shared.Language.Components; -using Content.Shared.Language.Events; using Content.Shared.Language.Systems; using UniversalLanguageSpeakerComponent = Content.Shared.Language.Components.UniversalLanguageSpeakerComponent; @@ -16,8 +15,19 @@ public override void Initialize() InitializeNet(); SubscribeLocalEvent(OnInitLanguageSpeaker); + SubscribeLocalEvent(OnUniversalInit); + SubscribeLocalEvent(OnUniversalShutdown); } + private void OnUniversalShutdown(EntityUid uid, UniversalLanguageSpeakerComponent component, ComponentShutdown args) + { + RemoveLanguage(uid, UniversalPrototype); + } + + private void OnUniversalInit(EntityUid uid, UniversalLanguageSpeakerComponent component, MapInitEvent args) + { + AddLanguage(uid, UniversalPrototype); + } #region public api @@ -48,10 +58,9 @@ public bool CanSpeak(EntityUid speaker, string language, LanguageSpeakerComponen ///
public LanguagePrototype GetLanguage(EntityUid speaker, LanguageSpeakerComponent? component = null) { - if (HasComp(speaker) || !Resolve(speaker, ref component, logMissing: false)) - return Universal; // Serves both as a fallback and uhhh something (TODO: fix this comment) - - if (string.IsNullOrEmpty(component.CurrentLanguage) || !_prototype.TryIndex(component.CurrentLanguage, out var proto)) + if (!Resolve(speaker, ref component, logMissing: false) + || string.IsNullOrEmpty(component.CurrentLanguage) + || !_prototype.TryIndex(component.CurrentLanguage, out var proto)) return Universal; return proto; @@ -63,13 +72,10 @@ public LanguagePrototype GetLanguage(EntityUid speaker, LanguageSpeakerComponent /// Typically, checking is sufficient. public List GetSpokenLanguages(EntityUid uid) { - if (HasComp(uid)) - return [UniversalPrototype]; + if (!TryComp(uid, out var component)) + return []; - if (TryComp(uid, out var component)) - return component.SpokenLanguages; - - return []; + return component.SpokenLanguages; } /// @@ -78,21 +84,17 @@ public List GetSpokenLanguages(EntityUid uid) /// Typically, checking is sufficient. public List GetUnderstoodLanguages(EntityUid uid) { - if (HasComp(uid)) - return [UniversalPrototype]; // This one is tricky because... well, they understand all of them, not just one. - - if (TryComp(uid, out var component)) - return component.UnderstoodLanguages; + if (!TryComp(uid, out var component)) + return []; - return []; + return component.UnderstoodLanguages; } public void SetLanguage(EntityUid speaker, string language, LanguageSpeakerComponent? component = null) { - if (!CanSpeak(speaker, language) || (HasComp(speaker) && language != UniversalPrototype)) - return; - - if (!Resolve(speaker, ref component) || component.CurrentLanguage == language) + if (!CanSpeak(speaker, language) + || !Resolve(speaker, ref component) + || component.CurrentLanguage == language) return; component.CurrentLanguage = language; @@ -106,12 +108,10 @@ public void AddLanguage( EntityUid uid, string language, bool addSpoken = true, - bool addUnderstood = true, - LanguageKnowledgeComponent? knowledge = null, - LanguageSpeakerComponent? speaker = null) + bool addUnderstood = true) { - if (knowledge == null) - knowledge = EnsureComp(uid); + EnsureComp(uid, out var knowledge); + EnsureComp(uid); if (addSpoken && !knowledge.SpokenLanguages.Contains(language)) knowledge.SpokenLanguages.Add(language); @@ -119,7 +119,7 @@ public void AddLanguage( if (addUnderstood && !knowledge.UnderstoodLanguages.Contains(language)) knowledge.UnderstoodLanguages.Add(language); - UpdateEntityLanguages(uid, speaker); + UpdateEntityLanguages(uid); } /// @@ -129,12 +129,10 @@ public void RemoveLanguage( EntityUid uid, string language, bool removeSpoken = true, - bool removeUnderstood = true, - LanguageKnowledgeComponent? knowledge = null, - LanguageSpeakerComponent? speaker = null) + bool removeUnderstood = true) { - if (knowledge == null) - knowledge = EnsureComp(uid); + if (!TryComp(uid, out var knowledge)) + return; if (removeSpoken) knowledge.SpokenLanguages.Remove(language); @@ -142,7 +140,7 @@ public void RemoveLanguage( if (removeUnderstood) knowledge.UnderstoodLanguages.Remove(language); - UpdateEntityLanguages(uid, speaker); + UpdateEntityLanguages(uid); } /// @@ -168,9 +166,9 @@ public bool EnsureValidLanguage(EntityUid entity, LanguageSpeakerComponent? comp /// /// Immediately refreshes the cached lists of spoken and understood languages for the given entity. /// - public void UpdateEntityLanguages(EntityUid entity, LanguageSpeakerComponent? languages = null) + public void UpdateEntityLanguages(EntityUid entity) { - if (!Resolve(entity, ref languages)) + if (!TryComp(entity, out var languages)) return; var ev = new DetermineEntityLanguagesEvent(); @@ -205,7 +203,7 @@ private void OnInitLanguageSpeaker(EntityUid uid, LanguageSpeakerComponent compo if (string.IsNullOrEmpty(component.CurrentLanguage)) component.CurrentLanguage = component.SpokenLanguages.FirstOrDefault(UniversalPrototype); - UpdateEntityLanguages(uid, component); + UpdateEntityLanguages(uid); } #endregion diff --git a/Content.Server/Language/TranslatorSystem.cs b/Content.Server/Language/TranslatorSystem.cs index a893993e884..c48b93a3930 100644 --- a/Content.Server/Language/TranslatorSystem.cs +++ b/Content.Server/Language/TranslatorSystem.cs @@ -85,8 +85,8 @@ private void OnTranslatorParentChanged(EntityUid translator, HandheldTranslatorC // If that is not the case, then OnProxyDetermineLanguages will remove this translator from HoldsTranslatorComponent.Translators. Timer.Spawn(0, () => { - if (Exists(args.OldParent) && TryComp(args.OldParent, out var speaker)) - _language.UpdateEntityLanguages(args.OldParent.Value, speaker); + if (Exists(args.OldParent) && HasComp(args.OldParent)) + _language.UpdateEntityLanguages(args.OldParent.Value); }); } @@ -108,7 +108,7 @@ private void OnTranslatorToggle(EntityUid translator, HandheldTranslatorComponen { // The first new spoken language added by this translator, or null var firstNewLanguage = translatorComp.SpokenLanguages.FirstOrDefault(it => !languageComp.SpokenLanguages.Contains(it)); - _language.UpdateEntityLanguages(holder, languageComp); + _language.UpdateEntityLanguages(holder); // Update the current language of the entity if necessary if (isEnabled && translatorComp.SetLanguageOnInteract && firstNewLanguage is {}) @@ -131,8 +131,8 @@ private void OnPowerCellSlotEmpty(EntityUid translator, HandheldTranslatorCompon _powerCell.SetPowerCellDrawEnabled(translator, false); OnAppearanceChange(translator, component); - if (_containers.TryGetContainingContainer(translator, out var holderCont) && TryComp(holderCont.Owner, out var languageComp)) - _language.UpdateEntityLanguages(holderCont.Owner, languageComp); + if (_containers.TryGetContainingContainer(translator, out var holderCont) && HasComp(holderCont.Owner)) + _language.UpdateEntityLanguages(holderCont.Owner); } private void CopyLanguages(BaseTranslatorComponent from, DetermineEntityLanguagesEvent to, LanguageKnowledgeComponent knowledge) diff --git a/Content.Server/Traits/Assorted/ForeignerTraitSystem.cs b/Content.Server/Traits/Assorted/ForeignerTraitSystem.cs index 58e974227ce..2c7274a13d5 100644 --- a/Content.Server/Traits/Assorted/ForeignerTraitSystem.cs +++ b/Content.Server/Traits/Assorted/ForeignerTraitSystem.cs @@ -46,7 +46,7 @@ private void OnSpawn(Entity entity, ref ComponentInit a if (TryGiveTranslator(entity.Owner, entity.Comp.BaseTranslator, entity.Comp.BaseLanguage, alternateLanguage, out var translator)) { - _languages.RemoveLanguage(entity, entity.Comp.BaseLanguage, entity.Comp.CantSpeak, entity.Comp.CantUnderstand, knowledge); + _languages.RemoveLanguage(entity, entity.Comp.BaseLanguage, entity.Comp.CantSpeak, entity.Comp.CantUnderstand); } } diff --git a/Content.Server/Traits/Assorted/LanguageKnowledgeModifierComponent.cs b/Content.Server/Traits/Assorted/LanguageKnowledgeModifierComponent.cs deleted file mode 100644 index 170dae40fa6..00000000000 --- a/Content.Server/Traits/Assorted/LanguageKnowledgeModifierComponent.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Content.Shared.Language; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List; - -namespace Content.Server.Traits.Assorted; - -/// -/// Used for traits that modify entities' language knowledge. -/// -[RegisterComponent] -public sealed partial class LanguageKnowledgeModifierComponent : Component -{ - /// - /// List of languages this entity will learn to speak. - /// - [DataField("speaks")] - public List NewSpokenLanguages = new(); - - /// - /// List of languages this entity will learn to understand. - /// - [DataField("understands")] - public List NewUnderstoodLanguages = new(); -} diff --git a/Content.Server/Traits/Assorted/LanguageKnowledgeModifierSystem.cs b/Content.Server/Traits/Assorted/LanguageKnowledgeModifierSystem.cs deleted file mode 100644 index 9053c9404fe..00000000000 --- a/Content.Server/Traits/Assorted/LanguageKnowledgeModifierSystem.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Linq; -using Content.Server.Language; -using Content.Shared.Language.Components; - -namespace Content.Server.Traits.Assorted; - -public sealed class LanguageKnowledgeModifierSystem : EntitySystem -{ - [Dependency] private readonly LanguageSystem _languages = default!; - - public override void Initialize() - { - base.Initialize(); - SubscribeLocalEvent(OnStartup); - } - - private void OnStartup(Entity entity, ref ComponentInit args) - { - if (!TryComp(entity, out var knowledge)) - { - Log.Warning($"Entity {entity.Owner} does not have a LanguageKnowledge but has a LanguageKnowledgeModifier!"); - return; - } - - foreach (var spokenLanguage in entity.Comp.NewSpokenLanguages) - { - _languages.AddLanguage(entity, spokenLanguage, true, false, knowledge); - } - - foreach (var understoodLanguage in entity.Comp.NewUnderstoodLanguages) - { - _languages.AddLanguage(entity, understoodLanguage, false, true, knowledge); - } - } -} diff --git a/Content.Server/Traits/TraitSystem.cs b/Content.Server/Traits/TraitSystem.cs index 39812f65b6d..bd36b4ecefb 100644 --- a/Content.Server/Traits/TraitSystem.cs +++ b/Content.Server/Traits/TraitSystem.cs @@ -12,6 +12,7 @@ using Robust.Shared.Utility; using Content.Server.Abilities.Psionics; using Content.Shared.Psionics; +using Content.Server.Language; using Content.Shared.Mood; namespace Content.Server.Traits; @@ -26,6 +27,7 @@ public sealed class TraitSystem : EntitySystem [Dependency] private readonly SharedActionsSystem _actions = default!; [Dependency] private readonly PsionicAbilitiesSystem _psionicAbilities = default!; [Dependency] private readonly IComponentFactory _componentFactory = default!; + [Dependency] private readonly LanguageSystem _languageSystem = default!; public override void Initialize() { @@ -66,6 +68,8 @@ public void AddTrait(EntityUid uid, TraitPrototype traitPrototype) AddTraitComponents(uid, traitPrototype); AddTraitActions(uid, traitPrototype); AddTraitPsionics(uid, traitPrototype); + AddTraitLanguage(uid, traitPrototype); + RemoveTraitLanguage(uid, traitPrototype); AddTraitMoodlets(uid, traitPrototype); } @@ -142,6 +146,72 @@ public void AddTraitPsionics(EntityUid uid, TraitPrototype traitPrototype) _psionicAbilities.InitializePsionicPower(uid, psionicPower, false); } + /// + /// Initialize languages given by a Trait. + /// + private void AddTraitLanguage(EntityUid uid, TraitPrototype traitPrototype) + { + AddTraitLanguagesSpoken(uid, traitPrototype); + AddTraitLanguagesUnderstood(uid, traitPrototype); + } + + /// + /// If a trait includes any Spoken Languages, this sends them to LanguageSystem to be initialized. + /// + public void AddTraitLanguagesSpoken(EntityUid uid, TraitPrototype traitPrototype) + { + if (traitPrototype.LanguagesSpoken is null) + return; + + foreach (var language in traitPrototype.LanguagesSpoken) + _languageSystem.AddLanguage(uid, language, true, false); + } + + /// + /// If a trait includes any Understood Languages, this sends them to LanguageSystem to be initialized. + /// + public void AddTraitLanguagesUnderstood(EntityUid uid, TraitPrototype traitPrototype) + { + if (traitPrototype.LanguagesUnderstood is null) + return; + + foreach (var language in traitPrototype.LanguagesUnderstood) + _languageSystem.AddLanguage(uid, language, false, true); + } + + /// + /// Remove Languages given by a Trait. + /// + private void RemoveTraitLanguage(EntityUid uid, TraitPrototype traitPrototype) + { + RemoveTraitLanguagesSpoken(uid, traitPrototype); + RemoveTraitLanguagesUnderstood(uid, traitPrototype); + } + + /// + /// Removes any Spoken Languages if defined by a trait. + /// + public void RemoveTraitLanguagesSpoken(EntityUid uid, TraitPrototype traitPrototype) + { + if (traitPrototype.RemoveLanguagesSpoken is null) + return; + + foreach (var language in traitPrototype.RemoveLanguagesSpoken) + _languageSystem.RemoveLanguage(uid, language, true, false); + } + + /// + /// Removes any Understood Languages if defined by a trait. + /// + public void RemoveTraitLanguagesUnderstood(EntityUid uid, TraitPrototype traitPrototype) + { + if (traitPrototype.RemoveLanguagesUnderstood is null) + return; + + foreach (var language in traitPrototype.RemoveLanguagesUnderstood) + _languageSystem.RemoveLanguage(uid, language, false, true); + } + /// /// If a trait includes any moodlets, this adds the moodlets to the receiving entity. /// While I can't stop you, you shouldn't use this to add temporary moodlets. diff --git a/Content.Shared/Traits/Prototypes/TraitPrototype.cs b/Content.Shared/Traits/Prototypes/TraitPrototype.cs index cd4b02a1e63..7c0e429a691 100644 --- a/Content.Shared/Traits/Prototypes/TraitPrototype.cs +++ b/Content.Shared/Traits/Prototypes/TraitPrototype.cs @@ -58,6 +58,30 @@ public sealed partial class TraitPrototype : IPrototype [DataField] public List? PsionicPowers { get; private set; } = default!; + /// + /// The list of all Spoken Languages that this trait adds. + /// + [DataField] + public List? LanguagesSpoken { get; private set; } = default!; + + /// + /// The list of all Understood Languages that this trait adds. + /// + [DataField] + public List? LanguagesUnderstood { get; private set; } = default!; + + /// + /// The list of all Spoken Languages that this trait removes. + /// + [DataField] + public List? RemoveLanguagesSpoken { get; private set; } = default!; + + /// + /// The list of all Understood Languages that this trait removes. + /// + [DataField] + public List? RemoveLanguagesUnderstood { get; private set; } = default!; + /// /// The list of all Moodlets that this trait adds. /// diff --git a/Resources/Prototypes/Entities/Structures/Specific/oracle.yml b/Resources/Prototypes/Entities/Structures/Specific/oracle.yml index b60906b55e0..2161c6d80da 100644 --- a/Resources/Prototypes/Entities/Structures/Specific/oracle.yml +++ b/Resources/Prototypes/Entities/Structures/Specific/oracle.yml @@ -67,6 +67,13 @@ demandWhitelist: components: - Item + - type: LanguageSpeaker + currentLanguage: GalacticCommon + - type: LanguageKnowledge + speaks: + - GalacticCommon + understands: + - GalacticCommon - type: weightedRandomEntity diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Structures/Research/sophicscribe.yml b/Resources/Prototypes/Nyanotrasen/Entities/Structures/Research/sophicscribe.yml index 1a065b001c1..533cf314999 100644 --- a/Resources/Prototypes/Nyanotrasen/Entities/Structures/Research/sophicscribe.yml +++ b/Resources/Prototypes/Nyanotrasen/Entities/Structures/Research/sophicscribe.yml @@ -48,3 +48,10 @@ - type: GuideHelp guides: - Psionics + - type: LanguageSpeaker + currentLanguage: GalacticCommon + - type: LanguageKnowledge + speaks: + - GalacticCommon + understands: + - GalacticCommon diff --git a/Resources/Prototypes/Traits/skills.yml b/Resources/Prototypes/Traits/skills.yml index fa79666c7ab..015c9f50233 100644 --- a/Resources/Prototypes/Traits/skills.yml +++ b/Resources/Prototypes/Traits/skills.yml @@ -102,12 +102,10 @@ id: SignLanguage category: Visual points: -2 - components: - - type: LanguageKnowledgeModifier - speaks: - - Sign - understands: - - Sign + languagesSpoken: + - Sign + languagesUnderstood: + - Sign - type: trait id: Voracious From 80daa94e6a6e0875264053da48ed9e827a5dcbe0 Mon Sep 17 00:00:00 2001 From: SimpleStation Changelogs Date: Fri, 20 Sep 2024 01:48:18 +0000 Subject: [PATCH 29/55] Automatic Changelog Update (#899) --- Resources/Changelog/Changelog.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index a233a61a2d4..a458042c4ae 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -6467,3 +6467,18 @@ Entries: id: 6372 time: '2024-09-19T20:37:51.0000000+00:00' url: https://github.com/Simple-Station/Einstein-Engines/pull/933 +- author: VMSolidus + changes: + - type: Fix + message: >- + UniversalLanguageSpeaker(And Xenoglossy by extension) will now appear in + your language menu alongside other known languages, rather than replace + all known languages. You can effectively now choose whether or not to + speak it, or to use a normal language. + - type: Add + message: Traits can now add Languages directly. + - type: Add + message: Traits can now remove Languages directly. + id: 6373 + time: '2024-09-20T01:47:52.0000000+00:00' + url: https://github.com/Simple-Station/Einstein-Engines/pull/899 From 7119dd00376419f88eaa7d439529eb90daf99172 Mon Sep 17 00:00:00 2001 From: VMSolidus Date: Thu, 19 Sep 2024 22:48:43 -0400 Subject: [PATCH 30/55] NyanoCombat Part 4: MeleeSystem Refactor Part 2, Electric Boogaloo (#934) # Description Surprise motherfucker, here's a massive fucking refactor of SharedMeleeWeaponSystem. The MeleeWeaponComponent now allows for melee weapons to directly configure how and what members of the ContestsSystem they wish to utilize for influencing damage. Additionally, the damage of a melee weapon modified by Contests is now also visible when examined(Although this only reflects damage as per the current condition of the user). # Changelog :cl: - add: Melee Weapons can now individually define their interactions with the ContestsSystem. - add: Added the ContestArgs type, allowing arbitrarily any component to contain a list of arguments for ContestSystems. - add: Added the ContestConstructor, a new type of meta-Contest that enables other systems to use components to define all possible contest behaviors, rather than needing to hardcode specific interactions. --- .../Contests/ContestsSystem.Utilities.cs | 262 ++++++++++++++++++ .../Weapons/Melee/MeleeWeaponComponent.cs | 15 + .../Weapons/Melee/SharedMeleeWeaponSystem.cs | 13 +- 3 files changed, 283 insertions(+), 7 deletions(-) diff --git a/Content.Shared/Contests/ContestsSystem.Utilities.cs b/Content.Shared/Contests/ContestsSystem.Utilities.cs index 42a69bff478..0278d75e943 100644 --- a/Content.Shared/Contests/ContestsSystem.Utilities.cs +++ b/Content.Shared/Contests/ContestsSystem.Utilities.cs @@ -1,4 +1,5 @@ using Content.Shared.CCVar; +using Robust.Shared.Serialization; namespace Content.Shared.Contests; public sealed partial class ContestsSystem @@ -18,4 +19,265 @@ private bool ContestClampOverride(bool bypassClamp) { return _cfg.GetCVar(CCVars.AllowClampOverride) && bypassClamp; } + + /// + /// Constructor for feeding options from a given set of ContestArgs into the ContestsSystem. + /// Just multiply by this and give it a user EntityUid and a ContestArgs variable. That's all you need to know. + /// + public float ContestConstructor(EntityUid user, ContestArgs args) + { + if (!_cfg.GetCVar(CCVars.DoContestsSystem)) + return 1; + + if (!args.DoEveryInteraction) + return args.DoMassInteraction ? ((!args.MassDisadvantage + ? MassContest(user, args.MassBypassClamp, args.MassRangeModifier) + : 1 / MassContest(user, args.MassBypassClamp, args.MassRangeModifier)) + + args.MassOffset) + : 1 + * (args.DoStaminaInteraction ? ((!args.StaminaDisadvantage + ? StaminaContest(user, args.StaminaBypassClamp, args.StaminaRangeModifier) + : 1 / StaminaContest(user, args.StaminaBypassClamp, args.StaminaRangeModifier)) + + args.StaminaOffset) + : 1) + * (args.DoHealthInteraction ? ((!args.HealthDisadvantage + ? HealthContest(user, args.HealthBypassClamp, args.HealthRangeModifier) + : 1 / HealthContest(user, args.HealthBypassClamp, args.HealthRangeModifier)) + + args.HealthOffset) + : 1) + * (args.DoMindInteraction ? ((!args.MindDisadvantage + ? MindContest(user, args.MindBypassClamp, args.MindRangeModifier) + : 1 / MindContest(user, args.MindBypassClamp, args.MindRangeModifier)) + + args.MindOffset) + : 1) + * (args.DoMoodInteraction ? ((!args.MoodDisadvantage + ? MoodContest(user, args.MoodBypassClamp, args.MoodRangeModifier) + : 1 / MoodContest(user, args.MoodBypassClamp, args.MoodRangeModifier)) + + args.MoodOffset) + : 1); + + var everyContest = EveryContest(user, + args.MassBypassClamp, + args.StaminaBypassClamp, + args.HealthBypassClamp, + args.MindBypassClamp, + args.MoodBypassClamp, + args.MassRangeModifier, + args.StaminaRangeModifier, + args.HealthRangeModifier, + args.MindRangeModifier, + args.MoodRangeModifier, + args.EveryMassWeight, + args.EveryStaminaWeight, + args.EveryHealthWeight, + args.EveryMindWeight, + args.EveryMoodWeight, + args.EveryInteractionSumOrMultiply); + + return !args.EveryDisadvantage ? everyContest : 1 / everyContest; + } } + +[Serializable, NetSerializable, DataDefinition] +public sealed partial class ContestArgs +{ + /// + /// Controls whether this melee weapon allows for mass to factor into damage. + /// + [DataField] + public bool DoMassInteraction; + + /// + /// When true, mass provides a disadvantage. + /// + [DataField] + public bool MassDisadvantage; + + /// + /// When true, mass contests ignore clamp limitations for a melee weapon. + /// + [DataField] + public bool MassBypassClamp; + + /// + /// Multiplies the acceptable range of outputs provided by mass contests for melee. + /// + [DataField] + public float MassRangeModifier = 1; + + /// + /// The output of a mass contest is increased by this amount. + /// + [DataField] + public float MassOffset; + + /// + /// Controls whether this melee weapon allows for stamina to factor into damage. + /// + [DataField] + public bool DoStaminaInteraction; + + /// + /// When true, stamina provides a disadvantage. + /// + [DataField] + public bool StaminaDisadvantage; + + /// + /// When true, stamina contests ignore clamp limitations for a melee weapon. + /// + [DataField] + public bool StaminaBypassClamp; + + /// + /// Multiplies the acceptable range of outputs provided by mass contests for melee. + /// + [DataField] + public float StaminaRangeModifier = 1; + + /// + /// The output of a stamina contest is increased by this amount. + /// + [DataField] + public float StaminaOffset; + + /// + /// Controls whether this melee weapon allows for health to factor into damage. + /// + [DataField] + public bool DoHealthInteraction; + + /// + /// When true, health contests provide a disadvantage. + /// + [DataField] + public bool HealthDisadvantage; + + /// + /// When true, health contests ignore clamp limitations for a melee weapon. + /// + [DataField] + public bool HealthBypassClamp; + + /// + /// Multiplies the acceptable range of outputs provided by mass contests for melee. + /// + [DataField] + public float HealthRangeModifier = 1; + + /// + /// The output of health contests is increased by this amount. + /// + [DataField] + public float HealthOffset; + + /// + /// Controls whether this melee weapon allows for psychic casting stats to factor into damage. + /// + [DataField] + public bool DoMindInteraction; + + /// + /// When true, high psychic casting stats provide a disadvantage. + /// + [DataField] + public bool MindDisadvantage; + + /// + /// When true, mind contests ignore clamp limitations for a melee weapon. + /// + [DataField] + public bool MindBypassClamp; + + /// + /// Multiplies the acceptable range of outputs provided by mind contests for melee. + /// + [DataField] + public float MindRangeModifier = 1; + + /// + /// The output of a mind contest is increased by this amount. + /// + [DataField] + public float MindOffset; + + /// + /// Controls whether this melee weapon allows mood to factor into damage. + /// + [DataField] + public bool DoMoodInteraction; + + /// + /// When true, mood provides a disadvantage. + /// + [DataField] + public bool MoodDisadvantage; + + /// + /// When true, mood contests ignore clamp limitations for a melee weapon. + /// + [DataField] + public bool MoodBypassClamp; + + /// + /// Multiplies the acceptable range of outputs provided by mood contests for melee. + /// + [DataField] + public float MoodRangeModifier = 1; + + /// + /// The output of mood contests is increased by this amount. + /// + [DataField] + public float MoodOffset; + + /// + /// Enables the EveryContest interaction for a melee weapon. + /// IF YOU PUT THIS ON ANY WEAPON OTHER THAN AN ADMEME, I WILL COME TO YOUR HOUSE AND SEND YOU TO MEET YOUR CREATOR WHEN THE PLAYERS COMPLAIN. + /// + [DataField] + public bool DoEveryInteraction; + + /// + /// When true, EveryContest provides a disadvantage. + /// + [DataField] + public bool EveryDisadvantage; + + /// + /// How much Mass is considered for an EveryContest. + /// + [DataField] + public float EveryMassWeight = 1; + + /// + /// How much Stamina is considered for an EveryContest. + /// + [DataField] + public float EveryStaminaWeight = 1; + + /// + /// How much Health is considered for an EveryContest. + /// + [DataField] + public float EveryHealthWeight = 1; + + /// + /// How much psychic casting stats are considered for an EveryContest. + /// + [DataField] + public float EveryMindWeight = 1; + + /// + /// How much mood is considered for an EveryContest. + /// + [DataField] + public float EveryMoodWeight = 1; + + /// + /// When true, the EveryContest sums the results of all contests rather than multiplying them, + /// probably giving you a very, very, very large multiplier... + /// + [DataField] + public bool EveryInteractionSumOrMultiply; +} \ No newline at end of file diff --git a/Content.Shared/Weapons/Melee/MeleeWeaponComponent.cs b/Content.Shared/Weapons/Melee/MeleeWeaponComponent.cs index c242d448f27..43e3096d158 100644 --- a/Content.Shared/Weapons/Melee/MeleeWeaponComponent.cs +++ b/Content.Shared/Weapons/Melee/MeleeWeaponComponent.cs @@ -1,3 +1,4 @@ +using Content.Shared.Contests; using Content.Shared.Damage; using Content.Shared.FixedPoint; using Robust.Shared.Audio; @@ -156,6 +157,20 @@ public sealed partial class MeleeWeaponComponent : Component /// [DataField, AutoNetworkedField] public SoundSpecifier SoundNoDamage { get; set; } = new SoundCollectionSpecifier("WeakHit"); + + /// + /// Arguments for the MeleeContestInteractions constructor + /// + [DataField] + public ContestArgs ContestArgs = new ContestArgs + { + DoStaminaInteraction = true, + StaminaDisadvantage = true, + StaminaRangeModifier = 2, + StaminaOffset = 0.25f, + DoHealthInteraction = true, + HealthRangeModifier = 1.5f, + }; } /// diff --git a/Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs b/Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs index b5a537b7e15..fd77ad31a45 100644 --- a/Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs +++ b/Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs @@ -32,7 +32,7 @@ namespace Content.Shared.Weapons.Melee; -public abstract class SharedMeleeWeaponSystem : EntitySystem +public abstract partial class SharedMeleeWeaponSystem : EntitySystem { [Dependency] protected readonly ISharedAdminLogManager AdminLogger = default!; [Dependency] protected readonly ActionBlockerSystem Blocker = default!; @@ -225,6 +225,9 @@ public DamageSpecifier GetDamage(EntityUid uid, EntityUid user, MeleeWeaponCompo var ev = new GetMeleeDamageEvent(uid, new (component.Damage), new(), user); RaiseLocalEvent(uid, ref ev); + if (component.ContestArgs is not null) + ev.Damage *= _contests.ContestConstructor(user, component.ContestArgs); + return DamageSpecifier.ApplyModifierSets(ev.Damage, ev.Modifiers); } @@ -249,9 +252,7 @@ public FixedPoint2 GetHeavyDamageModifier(EntityUid uid, EntityUid user, MeleeWe return ev.DamageModifier * ev.Multipliers - * component.HeavyDamageBaseModifier - * _contests.StaminaContest(user, false, 2f) //Taking stamina damage reduces wide swing damage by up to 50% - / _contests.HealthContest(user, false, 0.8f); //Being injured grants up to 20% more wide swing damage + * component.HeavyDamageBaseModifier; } public bool TryGetWeapon(EntityUid entity, out EntityUid weaponUid, [NotNullWhen(true)] out MeleeWeaponComponent? melee) @@ -440,9 +441,7 @@ private bool AttemptAttack(EntityUid user, EntityUid weaponUid, MeleeWeaponCompo protected virtual void DoLightAttack(EntityUid user, LightAttackEvent ev, EntityUid meleeUid, MeleeWeaponComponent component, ICommonSession? session) { - var damage = GetDamage(meleeUid, user, component) - * _contests.StaminaContest(user) //Taking stamina damage reduces light attack damage by up to 25% - / _contests.HealthContest(user, false, 0.8f); //Being injured grants up to 20% more damage; + var damage = GetDamage(meleeUid, user, component); var target = GetEntity(ev.Target); // For consistency with wide attacks stuff needs damageable. From c1aef1b4394b76a936cf36e9a002aa0f63591669 Mon Sep 17 00:00:00 2001 From: SimpleStation Changelogs Date: Fri, 20 Sep 2024 02:49:07 +0000 Subject: [PATCH 31/55] Automatic Changelog Update (#934) --- Resources/Changelog/Changelog.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index a458042c4ae..a3fe3c90d2a 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -6482,3 +6482,21 @@ Entries: id: 6373 time: '2024-09-20T01:47:52.0000000+00:00' url: https://github.com/Simple-Station/Einstein-Engines/pull/899 +- author: VMSolidus + changes: + - type: Add + message: >- + Melee Weapons can now individually define their interactions with the + ContestsSystem. + - type: Add + message: >- + Added the ContestArgs type, allowing arbitrarily any component to + contain a list of arguments for ContestSystems. + - type: Add + message: >- + Added the ContestConstructor, a new type of meta-Contest that enables + other systems to use components to define all possible contest + behaviors, rather than needing to hardcode specific interactions. + id: 6374 + time: '2024-09-20T02:48:43.0000000+00:00' + url: https://github.com/Simple-Station/Einstein-Engines/pull/934 From b01ae7087366022dc00cd3b74c7ed2bd1c87b93f Mon Sep 17 00:00:00 2001 From: gluesniffler <159397573+gluesniffler@users.noreply.github.com> Date: Fri, 20 Sep 2024 15:34:02 -0400 Subject: [PATCH 32/55] Allow Crawling Entities To Go Under Tables (#939) # Description Adds a CVAR that when enabled, allows entities to crawl under tables/flaps by lowering their DrawDepth, which in turn protects them from being targeted for projectiles while crawling. Additionally tables and plastic flaps were given collision properties along with reduced damage thresholds, so guns can target & destroy them easily if your mouse is on top of them. ---

Media

https://github.com/user-attachments/assets/77a04198-11cb-4895-bf2d-6f82b7f2bb5b

--- # Changelog :cl: - add: Adds an optional server variable which allows entities to crawl under tables. - tweak: Tables and plastic flaps are less resistant to damage, and can now be targeted by guns by aiming on top of them. --------- Signed-off-by: gluesniffler <159397573+gluesniffler@users.noreply.github.com> Co-authored-by: VMSolidus --- Content.Client/Standing/LayingDownSystem.cs | 26 +++++++++++++++++++ Content.Server/Flight/FlightSystem.cs | 18 +++++++++++++ Content.Shared/CCVar/CCVars.cs | 12 +++++++-- .../Standing/LayingDownComponent.cs | 9 +++++++ .../Standing/StandingStateSystem.cs | 8 ++++++ .../Locale/en-US/flight/flight_system.ftl | 3 ++- .../Furniture/Tables/base_structuretables.yml | 4 ++- .../Structures/Furniture/Tables/tables.yml | 16 ++++++------ .../Entities/Structures/plastic_flaps.yml | 11 +++++--- 9 files changed, 91 insertions(+), 16 deletions(-) diff --git a/Content.Client/Standing/LayingDownSystem.cs b/Content.Client/Standing/LayingDownSystem.cs index 594883ac001..3a1f438df05 100644 --- a/Content.Client/Standing/LayingDownSystem.cs +++ b/Content.Client/Standing/LayingDownSystem.cs @@ -4,6 +4,7 @@ using Robust.Client.GameObjects; using Robust.Client.Graphics; using Robust.Shared.Timing; +using DrawDepth = Content.Shared.DrawDepth.DrawDepth; namespace Content.Client.Standing; @@ -20,6 +21,8 @@ public override void Initialize() base.Initialize(); SubscribeLocalEvent(OnMovementInput); + SubscribeNetworkEvent(OnDowned); + SubscribeLocalEvent(OnStood); SubscribeNetworkEvent(OnCheckAutoGetUp); } @@ -48,6 +51,29 @@ private void OnMovementInput(EntityUid uid, LayingDownComponent component, MoveE sprite.Rotation = Angle.FromDegrees(90); } + private void OnDowned(DrawDownedEvent args) + { + var uid = GetEntity(args.Uid); + + if (!TryComp(uid, out var sprite) + || !TryComp(uid, out var component)) + return; + + if (!component.OriginalDrawDepth.HasValue) + component.OriginalDrawDepth = sprite.DrawDepth; + + sprite.DrawDepth = (int) DrawDepth.SmallMobs; + } + + private void OnStood(EntityUid uid, LayingDownComponent component, StoodEvent args) + { + if (!TryComp(uid, out var sprite) + || !component.OriginalDrawDepth.HasValue) + return; + + sprite.DrawDepth = component.OriginalDrawDepth.Value; + } + private void OnCheckAutoGetUp(CheckAutoGetUpEvent ev, EntitySessionEventArgs args) { if (!_timing.IsFirstTimePredicted) diff --git a/Content.Server/Flight/FlightSystem.cs b/Content.Server/Flight/FlightSystem.cs index e056fc24ec0..39321b1e66c 100644 --- a/Content.Server/Flight/FlightSystem.cs +++ b/Content.Server/Flight/FlightSystem.cs @@ -6,6 +6,7 @@ using Content.Shared.Flight.Events; using Content.Shared.Mobs; using Content.Shared.Popups; +using Content.Shared.Standing; using Content.Shared.Stunnable; using Content.Shared.Zombies; using Robust.Shared.Audio.Systems; @@ -16,6 +17,7 @@ public sealed class FlightSystem : SharedFlightSystem [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; [Dependency] private readonly SharedPopupSystem _popupSystem = default!; + [Dependency] private readonly StandingStateSystem _standing = default!; public override void Initialize() { @@ -27,6 +29,7 @@ public override void Initialize() SubscribeLocalEvent(OnZombified); SubscribeLocalEvent(OnKnockedDown); SubscribeLocalEvent(OnStunned); + SubscribeLocalEvent(OnDowned); SubscribeLocalEvent(OnSleep); } public override void Update(float frameTime) @@ -103,6 +106,13 @@ private bool CanFly(EntityUid uid, FlightComponent component) _popupSystem.PopupEntity(Loc.GetString("no-flight-while-zombified"), uid, uid, PopupType.Medium); return false; } + + if (HasComp(uid) && _standing.IsDown(uid)) + { + _popupSystem.PopupEntity(Loc.GetString("no-flight-while-lying"), uid, uid, PopupType.Medium); + return false; + } + return true; } @@ -142,6 +152,14 @@ private void OnStunned(EntityUid uid, FlightComponent component, ref StunnedEven ToggleActive(uid, false, component); } + private void OnDowned(EntityUid uid, FlightComponent component, ref DownedEvent args) + { + if (!component.On) + return; + + ToggleActive(uid, false, component); + } + private void OnSleep(EntityUid uid, FlightComponent component, ref SleepStateChangedEvent args) { if (!component.On diff --git a/Content.Shared/CCVar/CCVars.cs b/Content.Shared/CCVar/CCVars.cs index 417817b5419..1b3bef5e333 100644 --- a/Content.Shared/CCVar/CCVars.cs +++ b/Content.Shared/CCVar/CCVars.cs @@ -2470,9 +2470,16 @@ public static readonly CVarDef public static readonly CVarDef HoldLookUp = CVarDef.Create("rest.hold_look_up", false, CVar.CLIENT | CVar.ARCHIVE); - + + /// + /// When true, entities that fall to the ground will be able to crawl under tables and + /// plastic flaps, allowing them to take cover from gunshots. + /// + public static readonly CVarDef CrawlUnderTables = + CVarDef.Create("rest.crawlundertables", false, CVar.REPLICATED); + #endregion - + #region Material Reclaimer /// @@ -2498,5 +2505,6 @@ public static readonly CVarDef CVarDef.Create("jetpack.enable_in_no_gravity", true, CVar.REPLICATED); #endregion + } } diff --git a/Content.Shared/Standing/LayingDownComponent.cs b/Content.Shared/Standing/LayingDownComponent.cs index 1499704c53b..363c6f15987 100644 --- a/Content.Shared/Standing/LayingDownComponent.cs +++ b/Content.Shared/Standing/LayingDownComponent.cs @@ -14,6 +14,9 @@ public sealed partial class LayingDownComponent : Component [DataField, AutoNetworkedField] public bool AutoGetUp; + + [DataField, AutoNetworkedField] + public int? OriginalDrawDepth { get; set; } } [Serializable, NetSerializable] @@ -24,3 +27,9 @@ public sealed class CheckAutoGetUpEvent(NetEntity user) : CancellableEntityEvent { public NetEntity User = user; } + +[Serializable, NetSerializable] +public sealed class DrawDownedEvent(NetEntity uid) : EntityEventArgs +{ + public NetEntity Uid = uid; +} diff --git a/Content.Shared/Standing/StandingStateSystem.cs b/Content.Shared/Standing/StandingStateSystem.cs index 69e72033bba..04c0a04547d 100644 --- a/Content.Shared/Standing/StandingStateSystem.cs +++ b/Content.Shared/Standing/StandingStateSystem.cs @@ -1,12 +1,15 @@ using Content.Shared.Buckle; using Content.Shared.Buckle.Components; +using Content.Shared.CCVar; using Content.Shared.Hands.Components; using Content.Shared.Movement.Systems; using Content.Shared.Physics; using Content.Shared.Rotation; using Robust.Shared.Audio.Systems; +using Robust.Shared.Configuration; using Robust.Shared.Physics; using Robust.Shared.Physics.Systems; +using Robust.Shared.Serialization; namespace Content.Shared.Standing; @@ -17,6 +20,7 @@ public sealed class StandingStateSystem : EntitySystem [Dependency] private readonly SharedPhysicsSystem _physics = default!; [Dependency] private readonly MovementSpeedModifierSystem _movement = default!; [Dependency] private readonly SharedBuckleSystem _buckle = default!; + [Dependency] private readonly IConfigurationManager _config = default!; // If StandingCollisionLayer value is ever changed to more than one layer, the logic needs to be edited. private const int StandingCollisionLayer = (int) CollisionGroup.MidImpassable; @@ -64,6 +68,10 @@ public bool Down(EntityUid uid, bool playSound = true, bool dropHeldItems = true Dirty(standingState); RaiseLocalEvent(uid, new DownedEvent(), false); + // Raising this event will lower the entity's draw depth to the same as a small mob. + if (_config.GetCVar(CCVars.CrawlUnderTables)) + RaiseNetworkEvent(new DrawDownedEvent(GetNetEntity(uid))); + // Seemed like the best place to put it _appearance.SetData(uid, RotationVisuals.RotationState, RotationState.Horizontal, appearance); diff --git a/Resources/Locale/en-US/flight/flight_system.ftl b/Resources/Locale/en-US/flight/flight_system.ftl index 12693cc8467..3558ab9dc88 100644 --- a/Resources/Locale/en-US/flight/flight_system.ftl +++ b/Resources/Locale/en-US/flight/flight_system.ftl @@ -1,2 +1,3 @@ no-flight-while-restrained = You can't fly right now. -no-flight-while-zombified = You can't use your wings right now. \ No newline at end of file +no-flight-while-zombified = You can't use your wings right now. +no-flight-while-lying = You can't fly right now. \ No newline at end of file diff --git a/Resources/Prototypes/Entities/Structures/Furniture/Tables/base_structuretables.yml b/Resources/Prototypes/Entities/Structures/Furniture/Tables/base_structuretables.yml index c1b0cf0423b..dbef5a7504a 100644 --- a/Resources/Prototypes/Entities/Structures/Furniture/Tables/base_structuretables.yml +++ b/Resources/Prototypes/Entities/Structures/Furniture/Tables/base_structuretables.yml @@ -20,6 +20,7 @@ - TableMask layer: - TableLayer + - BulletImpassable - type: SpriteFade - type: Sprite - type: Icon @@ -38,7 +39,8 @@ - type: FootstepModifier footstepSoundCollection: collection: FootstepHull - + - type: RequireProjectileTarget + - type: entity id: CounterBase parent: TableBase diff --git a/Resources/Prototypes/Entities/Structures/Furniture/Tables/tables.yml b/Resources/Prototypes/Entities/Structures/Furniture/Tables/tables.yml index e424b1b40b4..4c8427e46ee 100644 --- a/Resources/Prototypes/Entities/Structures/Furniture/Tables/tables.yml +++ b/Resources/Prototypes/Entities/Structures/Furniture/Tables/tables.yml @@ -137,7 +137,7 @@ acts: [ "Destruction" ] - trigger: !type:DamageTrigger - damage: 25 + damage: 15 behaviors: - !type:PlaySoundBehavior sound: @@ -178,7 +178,7 @@ acts: [ "Destruction" ] - trigger: !type:DamageTrigger - damage: 25 + damage: 15 behaviors: - !type:PlaySoundBehavior sound: @@ -216,7 +216,7 @@ acts: [ "Destruction" ] - trigger: !type:DamageTrigger - damage: 75 + damage: 30 behaviors: - !type:PlaySoundBehavior sound: @@ -263,7 +263,7 @@ thresholds: - trigger: !type:DamageTrigger - damage: 25 + damage: 15 behaviors: - !type:DoActsBehavior acts: [ "Destruction" ] @@ -379,7 +379,7 @@ collection: GlassBreak - trigger: !type:DamageTrigger - damage: 50 + damage: 30 behaviors: - !type:PlaySoundBehavior sound: @@ -426,7 +426,7 @@ acts: [ "Destruction" ] - trigger: !type:DamageTrigger - damage: 25 + damage: 40 behaviors: - !type:PlaySoundBehavior sound: @@ -546,7 +546,7 @@ thresholds: - trigger: !type:DamageTrigger - damage: 50 + damage: 40 behaviors: - !type:PlaySoundBehavior sound: @@ -573,7 +573,7 @@ thresholds: - trigger: !type:DamageTrigger - damage: 50 + damage: 20 behaviors: - !type:DoActsBehavior acts: [ "Destruction" ] diff --git a/Resources/Prototypes/Entities/Structures/plastic_flaps.yml b/Resources/Prototypes/Entities/Structures/plastic_flaps.yml index c4ee507395f..207305d2327 100644 --- a/Resources/Prototypes/Entities/Structures/plastic_flaps.yml +++ b/Resources/Prototypes/Entities/Structures/plastic_flaps.yml @@ -26,6 +26,7 @@ - TabletopMachineMask layer: - MidImpassable + - BulletImpassable - type: Damageable damageContainer: StructuralInorganic damageModifierSet: Metallic @@ -33,7 +34,7 @@ thresholds: - trigger: !type:DamageTrigger - damage: 100 + damage: 50 behaviors: - !type:DoActsBehavior acts: ["Destruction"] @@ -45,7 +46,8 @@ node: plasticFlaps - type: StaticPrice price: 83 - + - type: RequireProjectileTarget + - type: entity id: PlasticFlapsOpaque parent: PlasticFlapsClear @@ -65,6 +67,7 @@ layer: - Opaque - MidImpassable + - BulletImpassable - type: Occluder - type: Construction graph: PlasticFlapsGraph @@ -81,7 +84,7 @@ thresholds: - trigger: !type:DamageTrigger - damage: 150 + damage: 75 behaviors: - !type:DoActsBehavior acts: ["Destruction"] @@ -103,7 +106,7 @@ thresholds: - trigger: !type:DamageTrigger - damage: 150 + damage: 75 behaviors: - !type:DoActsBehavior acts: ["Destruction"] From 7ff002a552606b8ebdfea9b29c44dd8b42e6cecb Mon Sep 17 00:00:00 2001 From: SimpleStation Changelogs Date: Fri, 20 Sep 2024 19:34:28 +0000 Subject: [PATCH 33/55] Automatic Changelog Update (#939) --- Resources/Changelog/Changelog.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index a3fe3c90d2a..a9dad4eeec5 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -6500,3 +6500,16 @@ Entries: id: 6374 time: '2024-09-20T02:48:43.0000000+00:00' url: https://github.com/Simple-Station/Einstein-Engines/pull/934 +- author: gluesniffler + changes: + - type: Add + message: >- + Adds an optional server variable which allows entities to crawl under + tables. + - type: Tweak + message: >- + Tables and plastic flaps are less resistant to damage, and can now be + targeted by guns by aiming on top of them. + id: 6375 + time: '2024-09-20T19:34:02.0000000+00:00' + url: https://github.com/Simple-Station/Einstein-Engines/pull/939 From 14d8f81382e622211cfe621fdc2286135dcb8aeb Mon Sep 17 00:00:00 2001 From: OldDanceJacket <98985560+OldDanceJacket@users.noreply.github.com> Date: Fri, 20 Sep 2024 12:34:46 -0700 Subject: [PATCH 34/55] Melee Part 5: Less Exhaustion (#938) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # A followup PR to the previous melee rebalancing, taking in feedback from multiple sources. There were some unintended changes bundled with the previous melee changes that didn't flow well together. I'm documenting everything here but would like to preface this by saying that, while a number of folks have had issues with the changes, there's been a surprisingly quiet amount of people who have just come forward and said that they've liked a number of the changes. Instead of just being satisfied with this, I would like to appease the folks who have had difficulties adjusting to the melee changes by improving the experience on a number of weapons and strange outlier cases that have cropped up. Below is the changelog any item not listed was either only changed by the Default Wide Swing change or had no change at all: **Default Wide Swing** Angle 45 -> 60 Stamina Cost 10 -> 2.5 MaxTargets 1 -> 3 _-I wasn't aware that the max targets and angle was being dropped when I made my initial changes, the stamina cost was a bit too high(was originally supposed to be 5 at one point). 2.5 should be rarely felt and does more to halt stamina regen than directly impact the stamina pool. Any weapon denoted below with a ^ symbol is buffed by this change._ **ElectricGuitar** heavyStaminaCost 10 -> 7.5 **BassGuitar** heavyStaminaCost 10 -> 7.5 **RockGuitar** heavyStaminaCost 15 -> 10 **Banjo** heavyStaminaCost 10 -> 7.5 _-All of the guitars have received less of a stamina cost, excluding the acoustic which is fine at 10._ **RubberDuck** range 1.3 -> 1.5 heavyStaminaCost 5 -> default _Fear the duck._ **BrokenBottle** attackRate 1.5 -> 1.4 range 1.3 -> 1.4 _-Normalizing the range to feel less ass, but pulling back the attack rate a smidge as a result._ **FireExtinguisher** heavyStaminaCost 10 -> 7.5 maxTargets 8 -> 6 **HolyBible** maxTargets 3 -> 4 **MiniHoe** heavyStaminaCost 5 -> default **PlantClippers** heavyStaminaCost 5 -> default **Hatchet** ^ range 1.25 -> 1.4 **Mops** heavyStaminaCost 10 -> 7.5 _-Mopfu is real._ **Drill** range 1.3 -> 1.4 **Scalpel** range 1.25 -> 1.4 **CircularSaw + AdvancedCircularSaw** attackRate 1.25 -> 1.15 range 1.4 -> 1.5 heavyStaminaCost 15 -> 10 _-The heavy attack for these weapons is a 360 spin, try it out._ **SecLight** BluntStamina 1.5 -> 2 heavyStaminaCost 5 -> default _-This now does about 14 stamina damage on hit, for when you really need to harm baton._ **GasTanks** heavyStaminaCost 15 -> 10 maxTargets 1 -> 3 angle 140 -> 100 _-Been told this is a strong weapon, but a bit odd and unreliable. This should be easier to use but still keep it's flavor._ **JawsOfLife** attackRate 0.75 -> 0.85 range 1.75 -> 1.65 heavyStaminaCost 10 -> 5 **Toolboxes** ^ heavyStaminaCost 10 -> 7.5 **BaseTools** heavyStaminaCost 5 -> default **RollingPin** heavyStaminaCost 5 -> default maxTargets 1 -> 2 **BaseballBat** heavyStaminaCost 15 -> 10 **RitualDagger** range 1.4 -> 1.5 **EldritchBlade** range 1.75 -> 1.65 heavyStaminaCost 10 -> 7.5 **UnholyHalberd** attackrate 0.75 -> 0.85 heavyStaminaCost 10 -> 7.5 **HomeRunBat** heavyStaminaCost 25 -> 15 maxTargets 2 -> Default **Knife** range 1.4 -> 1.5 heavyStaminaCost 5 -> default _-This change parents over to majority of knives including the cleaver._ **Shiv** range 0.75 -> 1.4 _-Whoops!_ **Crusher** ^ attackRate 0.75 -> 0.85 range 1.75 -> 1.65 heavyStaminaCost 10 -> 7.5 **CrusherDagger** heavyStaminaCost 5 -> default **Pickaxe** attackRate 0.75 -> 0.85 range 1.75 -> 1.5 heavyStaminaCost 5 -> default maxTargets 2 -> 5 angle 60 -> 80 **Drill** attackRate 0.5 -> 1.2 range 1.4 -> 1.5 Blunt 9 -> 6 heavyStaminaCost 10 -> default maxTargets (default) _-I really despise the current balance of mining/salvage, the gameplay loops require you to destroy fifty rocks to get two plasma and a bar of gold. Your tools are now fixed._ **Sledgehammer** attackRate 0.75 -> 0.8 heavyStaminaCost 15 -> 7.5 _-This can hit 10 people btw._ **Spears** _-No changes here, just a remark that they are pretty strong and a good option yet again._ **Stunprod** attackRate 0.8 -> 1 range 1.4 -> 1.5 heavyStaminaCost 5 -> default maxtargets (default) angle (default _-So when not powered this thing does 15 stam damage when you smack someone with it, so if it runs out of power you can still smack the person a bit to stun them, but here, it's been buffed a bit more._ **Katana** heavyRange 3 -> 2.75 heavyStaminaCost 10 -> 15 _-The heavy attack of the Katana is comically overpowered, it has an absurd range. This is still really funny and cool but a little more balanced._ **Machete** ^ heavyStaminaCost 10 -> 7.5 **Claymore** heavyStaminaCost 20 -> 15 **Cutlass** heavyStaminaCost 10 -> 7.5 **StunBaton** bluntStamina 2.0 -> 2.5 heavyStaminaCost 5 -> 1 maxTargets (default) angle (default) _-Wow, from all the reports I got about this I thought I messed up on the balance of the stun baton, but honestly it was just a case of security players actually just sucking, lmao, this one is a full problème de compétence but whatever, I'm changing the stamina cost to 1, which shouldn't drop you at all, but will prevent your stamina from recovering if you just spam your wide swing. I've also further buffed the harm baton functionality._ **Truncheon** heavyStaminaCost 10 -> 7.5 . And that's it. Realistically this is just an overall adjustment of stamina costs to be a bit less punishing but this is all fairly redundant, a number of the 'issues' were spawned by default melee values not being updated properly to some codebases and people just spamming their power attack because that's how its been for a few months. This should be considerably better but please left click a little instead of flailing around. :cl: ODJ - tweak: Tweaked melee; Less stamina usage on heavy attacks. Co-authored-by: jcsmithing --- .../Weapons/Melee/MeleeWeaponComponent.cs | 6 +++--- .../Fun/Instruments/instruments_string.yml | 8 ++++---- .../Prototypes/Entities/Objects/Fun/toys.yml | 3 +-- .../Entities/Objects/Misc/broken_bottle.yml | 4 ++-- .../Entities/Objects/Misc/fire_extinguisher.yml | 4 ++-- .../Entities/Objects/Specific/Chapel/bibles.yml | 2 +- .../Objects/Specific/Hydroponics/tools.yml | 4 +--- .../Objects/Specific/Janitorial/janitor.yml | 4 ++-- .../Objects/Specific/Medical/surgery.yml | 17 +++++++++-------- .../Entities/Objects/Tools/flashlights.yml | 3 +-- .../Entities/Objects/Tools/gas_tanks.yml | 6 +++--- .../Entities/Objects/Tools/jaws_of_life.yml | 6 +++--- .../Entities/Objects/Tools/toolbox.yml | 2 +- .../Prototypes/Entities/Objects/Tools/tools.yml | 13 +++---------- .../Objects/Weapons/Melee/baseball_bat.yml | 2 +- .../Entities/Objects/Weapons/Melee/cult.yml | 10 +++++----- .../Objects/Weapons/Melee/home_run_bat.yml | 3 +-- .../Entities/Objects/Weapons/Melee/knife.yml | 7 ++----- .../Entities/Objects/Weapons/Melee/mining.yml | 7 +++---- .../Entities/Objects/Weapons/Melee/pickaxe.yml | 17 +++++++---------- .../Objects/Weapons/Melee/sledgehammer.yml | 4 ++-- .../Entities/Objects/Weapons/Melee/stunprod.yml | 7 ++----- .../Entities/Objects/Weapons/Melee/sword.yml | 10 +++++----- .../Entities/Objects/Weapons/security.yml | 8 +++----- 24 files changed, 67 insertions(+), 90 deletions(-) diff --git a/Content.Shared/Weapons/Melee/MeleeWeaponComponent.cs b/Content.Shared/Weapons/Melee/MeleeWeaponComponent.cs index 43e3096d158..f86bfc32ed3 100644 --- a/Content.Shared/Weapons/Melee/MeleeWeaponComponent.cs +++ b/Content.Shared/Weapons/Melee/MeleeWeaponComponent.cs @@ -110,7 +110,7 @@ public sealed partial class MeleeWeaponComponent : Component /// Total width of the angle for wide attacks. /// [DataField, AutoNetworkedField] - public Angle Angle = Angle.FromDegrees(45); + public Angle Angle = Angle.FromDegrees(60); [DataField, AutoNetworkedField] public EntProtoId Animation = "WeaponArcPunch"; @@ -129,10 +129,10 @@ public sealed partial class MeleeWeaponComponent : Component public bool SwingLeft; [DataField, AutoNetworkedField] - public float HeavyStaminaCost = 10f; + public float HeavyStaminaCost = 2.5f; [DataField, AutoNetworkedField] - public int MaxTargets = 1; + public int MaxTargets = 3; // Sounds diff --git a/Resources/Prototypes/Entities/Objects/Fun/Instruments/instruments_string.yml b/Resources/Prototypes/Entities/Objects/Fun/Instruments/instruments_string.yml index 947a973bbf6..730d532930b 100644 --- a/Resources/Prototypes/Entities/Objects/Fun/Instruments/instruments_string.yml +++ b/Resources/Prototypes/Entities/Objects/Fun/Instruments/instruments_string.yml @@ -25,7 +25,7 @@ bluntStaminaDamageFactor: 1.5 heavyRateModifier: 0.75 heavyDamageBaseModifier: 1.2 - heavyStaminaCost: 10 + heavyStaminaCost: 7.5 angle: 75 - type: Item size: Normal @@ -67,7 +67,7 @@ bluntStaminaDamageFactor: 1.5 heavyRateModifier: 0.75 heavyDamageBaseModifier: 1.2 - heavyStaminaCost: 10 + heavyStaminaCost: 7.5 angle: 75 - type: Item size: Normal @@ -110,7 +110,7 @@ bluntStaminaDamageFactor: 1.5 heavyRateModifier: 0.75 heavyDamageBaseModifier: 1.2 - heavyStaminaCost: 15 + heavyStaminaCost: 10 angle: 160 - type: Wieldable - type: IncreaseDamageOnWield @@ -234,7 +234,7 @@ bluntStaminaDamageFactor: 2 heavyRateModifier: 0.75 heavyDamageBaseModifier: 1.2 - heavyStaminaCost: 10 + heavyStaminaCost: 7.5 angle: 75 - type: entity diff --git a/Resources/Prototypes/Entities/Objects/Fun/toys.yml b/Resources/Prototypes/Entities/Objects/Fun/toys.yml index 9e9740a0a82..d7a5f3542dc 100644 --- a/Resources/Prototypes/Entities/Objects/Fun/toys.yml +++ b/Resources/Prototypes/Entities/Objects/Fun/toys.yml @@ -630,12 +630,11 @@ state: icon - type: MeleeWeapon attackRate: 1.5 - range: 1.3 + range: 1.5 damage: types: Blunt: 0.1 heavyDamageBaseModifier: 2 - heavyStaminaCost: 5 maxTargets: 8 angle: 25 - type: Clothing diff --git a/Resources/Prototypes/Entities/Objects/Misc/broken_bottle.yml b/Resources/Prototypes/Entities/Objects/Misc/broken_bottle.yml index f8dbabd07a1..a6cbe9a6e7e 100644 --- a/Resources/Prototypes/Entities/Objects/Misc/broken_bottle.yml +++ b/Resources/Prototypes/Entities/Objects/Misc/broken_bottle.yml @@ -6,8 +6,8 @@ components: - type: Sharp - type: MeleeWeapon - attackRate: 1.5 - range: 1.3 + attackRate: 1.4 + range: 1.4 damage: types: Slash: 4 diff --git a/Resources/Prototypes/Entities/Objects/Misc/fire_extinguisher.yml b/Resources/Prototypes/Entities/Objects/Misc/fire_extinguisher.yml index f1802e426fb..27a81a783a6 100644 --- a/Resources/Prototypes/Entities/Objects/Misc/fire_extinguisher.yml +++ b/Resources/Prototypes/Entities/Objects/Misc/fire_extinguisher.yml @@ -46,8 +46,8 @@ Blunt: 8 heavyRateModifier: 0.8 heavyDamageBaseModifier: 2 - heavyStaminaCost: 15 - maxTargets: 8 + heavyStaminaCost: 7.5 + maxTargets: 6 soundHit: path: /Audio/Weapons/smash.ogg - type: Tool diff --git a/Resources/Prototypes/Entities/Objects/Specific/Chapel/bibles.yml b/Resources/Prototypes/Entities/Objects/Specific/Chapel/bibles.yml index 9ab53cebc96..50bea2a8b24 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Chapel/bibles.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Chapel/bibles.yml @@ -50,7 +50,7 @@ heavyRateModifier: 0.8 heavyDamageBaseModifier: 1 heavyStaminaCost: 5 - maxTargets: 3 + maxTargets: 4 - type: Tag tags: - Book diff --git a/Resources/Prototypes/Entities/Objects/Specific/Hydroponics/tools.yml b/Resources/Prototypes/Entities/Objects/Specific/Hydroponics/tools.yml index 37b8daddc27..c43cce1f8b1 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Hydroponics/tools.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Hydroponics/tools.yml @@ -21,7 +21,6 @@ heavyRateModifier: 1 heavyRangeModifier: 1 heavyDamageBaseModifier: 1.2 - heavyStaminaCost: 5 maxTargets: 5 angle: 100 - type: Item @@ -48,7 +47,6 @@ heavyRateModifier: 0.9 heavyRangeModifier: 1.25 heavyDamageBaseModifier: 1.2 - heavyStaminaCost: 5 maxTargets: 1 angle: 20 - type: Item @@ -103,7 +101,7 @@ wideAnimationRotation: 135 swingLeft: true attackRate: 1.25 - range: 1.25 + range: 1.4 damage: types: Slash: 10 diff --git a/Resources/Prototypes/Entities/Objects/Specific/Janitorial/janitor.yml b/Resources/Prototypes/Entities/Objects/Specific/Janitorial/janitor.yml index de5c33671a8..f45331d5897 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Janitorial/janitor.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Janitorial/janitor.yml @@ -16,7 +16,7 @@ heavyRateModifier: 0.8 heavyRangeModifier: 1.25 heavyDamageBaseModifier: 1.25 - heavyStaminaCost: 10 + heavyStaminaCost: 7.5 maxTargets: 2 angle: 180 soundHit: @@ -64,7 +64,7 @@ heavyRateModifier: 0.8 heavyRangeModifier: 1.25 heavyDamageBaseModifier: 1.25 - heavyStaminaCost: 10 + heavyStaminaCost: 7.5 maxTargets: 2 angle: 180 soundHit: diff --git a/Resources/Prototypes/Entities/Objects/Specific/Medical/surgery.yml b/Resources/Prototypes/Entities/Objects/Specific/Medical/surgery.yml index c81768da4d3..027d53f5bcd 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Medical/surgery.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Medical/surgery.yml @@ -51,7 +51,7 @@ - 1,1,1,1 - type: MeleeWeapon attackRate: 0.75 - range: 1.3 + range: 1.4 damage: types: Piercing: 8 @@ -87,7 +87,7 @@ wideAnimationRotation: 90 swingLeft: true attackRate: 1.25 - range: 1.25 + range: 1.4 damage: types: Slash: 7.5 @@ -208,7 +208,8 @@ heavyStaminaCost: 20 maxTargets: 8 angle: 20 -# ~~No melee for regular saw because have you ever seen someone use a band saw as a weapon? It's dumb.~~ No, I'm going to saw through your bones. +# --No melee for regular saw because have you ever seen someone use a band saw as a weapon? It's dumb.-- +# No, I'm going to saw through your bones. - type: entity name: choppa @@ -252,7 +253,7 @@ storedRotation: 90 - type: MeleeWeapon attackRate: 1.15 - range: 1.4 + range: 1.5 bluntStaminaDamageFactor: 3.0 damage: types: @@ -260,7 +261,7 @@ Slash: 5.5 heavyRateModifier: 0.5 heavyDamageBaseModifier: 1 - heavyStaminaCost: 15 + heavyStaminaCost: 10 maxTargets: 8 angle: 360 soundHit: @@ -282,8 +283,8 @@ heldPrefix: advanced storedRotation: 90 - type: MeleeWeapon - attackRate: 1.25 - range: 1.4 + attackRate: 1.15 + range: 1.5 bluntStaminaDamageFactor: 5.0 damage: types: @@ -291,7 +292,7 @@ Slash: 7.5 heavyRateModifier: 0.5 heavyDamageBaseModifier: 1 - heavyStaminaCost: 15 + heavyStaminaCost: 10 maxTargets: 8 angle: 360 soundHit: diff --git a/Resources/Prototypes/Entities/Objects/Tools/flashlights.yml b/Resources/Prototypes/Entities/Objects/Tools/flashlights.yml index 2b75a7e3dd9..2a982ed6f79 100644 --- a/Resources/Prototypes/Entities/Objects/Tools/flashlights.yml +++ b/Resources/Prototypes/Entities/Objects/Tools/flashlights.yml @@ -120,9 +120,8 @@ damage: types: Blunt: 6.5 - bluntStaminaDamageFactor: 1.5 + bluntStaminaDamageFactor: 2 heavyRateModifier: 0.9 - heavyStaminaCost: 5 maxTargets: 1 angle: 20 soundHit: diff --git a/Resources/Prototypes/Entities/Objects/Tools/gas_tanks.yml b/Resources/Prototypes/Entities/Objects/Tools/gas_tanks.yml index f739de251cb..2618666ed9a 100644 --- a/Resources/Prototypes/Entities/Objects/Tools/gas_tanks.yml +++ b/Resources/Prototypes/Entities/Objects/Tools/gas_tanks.yml @@ -41,9 +41,9 @@ bluntStaminaDamageFactor: 2.5 heavyRateModifier: 0.8 heavyDamageBaseModifier: 1.5 - heavyStaminaCost: 15 - maxTargets: 1 - angle: 140 + heavyStaminaCost: 10 + maxTargets: 3 + angle: 100 - type: PhysicalComposition materialComposition: Steel: 185 diff --git a/Resources/Prototypes/Entities/Objects/Tools/jaws_of_life.yml b/Resources/Prototypes/Entities/Objects/Tools/jaws_of_life.yml index 12521ff644f..936e832224f 100644 --- a/Resources/Prototypes/Entities/Objects/Tools/jaws_of_life.yml +++ b/Resources/Prototypes/Entities/Objects/Tools/jaws_of_life.yml @@ -44,8 +44,8 @@ changeSound: /Audio/Items/change_jaws.ogg - type: MeleeWeapon wideAnimationRotation: 90 - attackRate: 0.75 - range: 1.75 + attackRate: 0.85 + range: 1.65 damage: types: Blunt: 10 @@ -53,7 +53,7 @@ bluntStaminaDamageFactor: 2.0 heavyRateModifier: 0.8 heavyDamageBaseModifier: 1.5 - heavyStaminaCost: 10 + heavyStaminaCost: 5 maxTargets: 1 angle: 20 soundHit: diff --git a/Resources/Prototypes/Entities/Objects/Tools/toolbox.yml b/Resources/Prototypes/Entities/Objects/Tools/toolbox.yml index 6702ae39d69..b242843634d 100644 --- a/Resources/Prototypes/Entities/Objects/Tools/toolbox.yml +++ b/Resources/Prototypes/Entities/Objects/Tools/toolbox.yml @@ -28,7 +28,7 @@ Blunt: 9 bluntStaminaDamageFactor: 2.0 heavyRateModifier: 0.8 - heavyStaminaCost: 10 + heavyStaminaCost: 7.5 angle: 80.5 soundHit: path: "/Audio/Weapons/smash.ogg" diff --git a/Resources/Prototypes/Entities/Objects/Tools/tools.yml b/Resources/Prototypes/Entities/Objects/Tools/tools.yml index c0d4fa179f2..87d0c2f4003 100644 --- a/Resources/Prototypes/Entities/Objects/Tools/tools.yml +++ b/Resources/Prototypes/Entities/Objects/Tools/tools.yml @@ -38,9 +38,7 @@ Blunt: 6.5 heavyRateModifier: 0.9 heavyDamageBaseModifier: 1.2 - heavyStaminaCost: 5 maxTargets: 4 - angle: 60 soundHit: path: "/Audio/Items/wirecutter.ogg" - type: Tool @@ -103,7 +101,6 @@ Piercing: 6 heavyRateModifier: 0.75 heavyDamageBaseModifier: 1.5 - heavyStaminaCost: 5 maxTargets: 1 angle: 20 soundHit: @@ -165,7 +162,6 @@ bluntStaminaDamageFactor: 1.5 heavyRateModifier: 0.75 heavyDamageBaseModifier: 1.75 - heavyStaminaCost: 5 angle: 100 soundHit: collection: MetalThud @@ -279,7 +275,6 @@ Shock: 2 heavyRateModifier: 0.9 heavyDamageBaseModifier: 1.2 - heavyStaminaCost: 5 maxTargets: 1 angle: 20 - type: Item @@ -424,7 +419,6 @@ Piercing: 8 heavyRateModifier: 0.9 heavyDamageBaseModifier: 1.2 - heavyStaminaCost: 5 maxTargets: 1 angle: 20 soundHit: @@ -641,14 +635,14 @@ - type: MeleeWeapon wideAnimationRotation: 45 attackRate: 0.8 - range: 2.0 + range: 1.75 damage: types: Blunt: 8 bluntStaminaDamageFactor: 1.5 heavyRateModifier: 0.9 heavyDamageBaseModifier: 1.5 - heavyStaminaCost: 10 + heavyStaminaCost: 7.5 angle: 100 soundHit: collection: MetalThud @@ -696,8 +690,7 @@ bluntStaminaDamageFactor: 2.0 heavyRateModifier: 0.8 heavyDamageBaseModifier: 1.5 - heavyStaminaCost: 5 - maxTargets: 1 + maxTargets: 2 angle: 20 soundHit: collection: MetalThud diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Melee/baseball_bat.yml b/Resources/Prototypes/Entities/Objects/Weapons/Melee/baseball_bat.yml index 8780d377e05..350ea94bd9f 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Melee/baseball_bat.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Melee/baseball_bat.yml @@ -17,7 +17,7 @@ bluntStaminaDamageFactor: 2.0 heavyRateModifier: 0.5 heavyDamageBaseModifier: 1.75 - heavyStaminaCost: 15 + heavyStaminaCost: 10 maxTargets: 2 angle: 120 soundHit: diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Melee/cult.yml b/Resources/Prototypes/Entities/Objects/Weapons/Melee/cult.yml index 5e9d789b658..a681ef52ebf 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Melee/cult.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Melee/cult.yml @@ -11,7 +11,7 @@ - type: MeleeWeapon wideAnimationRotation: -135 attackRate: 1.25 - range: 1.4 + range: 1.5 damage: types: Slash: 8 @@ -39,12 +39,12 @@ - type: MeleeWeapon wideAnimationRotation: -135 attackRate: 0.75 - range: 1.75 + range: 1.65 damage: types: Slash: 12 heavyDamageBaseModifier: 1.2 - heavyStaminaCost: 10 + heavyStaminaCost: 7.5 maxTargets: 6 angle: 90 - type: Item @@ -70,7 +70,7 @@ state: icon - type: MeleeWeapon wideAnimationRotation: -135 - attackRate: 0.75 + attackRate: 0.85 range: 1.75 damage: types: @@ -79,7 +79,7 @@ Structural: 5 heavyRateModifier: 0.9 heavyDamageBaseModifier: 1.2 - heavyStaminaCost: 10 + heavyStaminaCost: 7.5 angle: 100 soundHit: collection: MetalThud diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Melee/home_run_bat.yml b/Resources/Prototypes/Entities/Objects/Weapons/Melee/home_run_bat.yml index 5cb5795c8a5..43a4fb6c59b 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Melee/home_run_bat.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Melee/home_run_bat.yml @@ -14,8 +14,7 @@ bluntStaminaDamageFactor: 2.0 heavyRateModifier: 0.5 heavyDamageBaseModifier: 1.75 - heavyStaminaCost: 25 - maxTargets: 2 + heavyStaminaCost: 15 angle: 120 soundHit: collection: ExplosionSmall diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Melee/knife.yml b/Resources/Prototypes/Entities/Objects/Weapons/Melee/knife.yml index 68f8863d116..970a00ddf8f 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Melee/knife.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Melee/knife.yml @@ -13,13 +13,12 @@ - type: MeleeWeapon wideAnimationRotation: -135 attackRate: 1.25 - range: 1.4 + range: 1.5 damage: types: Slash: 8 heavyRateModifier: 0.8 heavyDamageBaseModifier: 1.2 - heavyStaminaCost: 5 maxTargets: 3 angle: 40 soundHit: @@ -95,7 +94,6 @@ - type: MeleeWeapon wideAnimationRotation: -135 attackRate: 1.5 - range: 1.4 damage: types: Slash: 9 @@ -122,7 +120,6 @@ - type: MeleeWeapon wideAnimationRotation: -135 attackRate: 1.25 - range: 1.5 damage: types: Slash: 8 @@ -204,7 +201,7 @@ state: icon - type: MeleeWeapon attackRate: 1.75 - range: 0.75 + range: 1.4 damage: types: Slash: 5.5 diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Melee/mining.yml b/Resources/Prototypes/Entities/Objects/Weapons/Melee/mining.yml index a1addba2625..38a203ce908 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Melee/mining.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Melee/mining.yml @@ -43,8 +43,8 @@ capacity: 1 count: 1 - type: MeleeWeapon - attackRate: 0.75 - range: 1.75 + attackRate: 0.85 + range: 1.65 wideAnimationRotation: -135 damage: types: @@ -53,7 +53,7 @@ bluntStaminaDamageFactor: 2.0 heavyRateModifier: 0.75 heavyDamageBaseModifier: 1.2 - heavyStaminaCost: 10 + heavyStaminaCost: 7.5 angle: 120 soundHit: collection: MetalThud @@ -92,7 +92,6 @@ Slash: 9 heavyRateModifier: 0.9 heavyDamageBaseModifier: 1.2 - heavyStaminaCost: 5 maxTargets: 2 angle: 20 - type: Tag diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Melee/pickaxe.yml b/Resources/Prototypes/Entities/Objects/Weapons/Melee/pickaxe.yml index 6ba659ccb40..fb1eb3d7fe9 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Melee/pickaxe.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Melee/pickaxe.yml @@ -11,8 +11,8 @@ sprite: Objects/Weapons/Melee/pickaxe.rsi state: pickaxe - type: MeleeWeapon - attackRate: 0.75 - range: 1.75 + attackRate: 0.85 + range: 1.5 wideAnimationRotation: -135 soundHit: path: "/Audio/Weapons/smash.ogg" @@ -24,9 +24,8 @@ Pierce: 3 bluntStaminaDamageFactor: 2.0 heavyDamageBaseModifier: 1.75 - heavyStaminaCost: 5 - maxTargets: 2 - angle: 60 + maxTargets: 5 + angle: 80 - type: Wieldable - type: IncreaseDamageOnWield damage: @@ -61,19 +60,17 @@ wideAnimationRotation: -90 soundHit: path: "/Audio/Items/drill_hit.ogg" - attackRate: 0.5 - range: 1.4 + attackRate: 1.2 + range: 1.5 damage: types: - Blunt: 9 + Blunt: 6 Slash: 3 Structural: 12 bluntStaminaDamageFactor: 4.0 heavyRateModifier: 1 heavyRangeModifier: 2 heavyDamageBaseModifier: 1 - heavyStaminaCost: 10 - maxTargets: 3 angle: 20 - type: ReverseEngineering # Nyano diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Melee/sledgehammer.yml b/Resources/Prototypes/Entities/Objects/Weapons/Melee/sledgehammer.yml index ecc84e50073..ffe791ce0c4 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Melee/sledgehammer.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Melee/sledgehammer.yml @@ -9,7 +9,7 @@ state: icon - type: MeleeWeapon wideAnimationRotation: -135 - attackRate: 0.8 + attackRate: 0.75 range: 1.75 damage: types: @@ -18,7 +18,7 @@ bluntStaminaDamageFactor: 2.0 heavyRateModifier: 0.75 heavyDamageBaseModifier: 1.75 - heavyStaminaCost: 15 + heavyStaminaCost: 7.5 maxTargets: 10 angle: 120 soundHit: diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Melee/stunprod.yml b/Resources/Prototypes/Entities/Objects/Weapons/Melee/stunprod.yml index d8955b4defe..991a5553c79 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Melee/stunprod.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Melee/stunprod.yml @@ -30,17 +30,14 @@ energyPerUse: 70 - type: MeleeWeapon wideAnimationRotation: -135 - attackRate: 0.8 - range: 1.4 + attackRate: 1 + range: 1.5 damage: types: Blunt: 7.5 bluntStaminaDamageFactor: 2.0 heavyRateModifier: 0.8 heavyDamageBaseModifier: 1.2 - heavyStaminaCost: 5 - maxTargets: 3 - angle: 60 animation: WeaponArcThrust - type: StaminaDamageOnHit damage: 22 diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Melee/sword.yml b/Resources/Prototypes/Entities/Objects/Weapons/Melee/sword.yml index 82b99ce37e3..5757f9c7168 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Melee/sword.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Melee/sword.yml @@ -57,9 +57,9 @@ types: Slash: 12 heavyRateModifier: 0.5 - heavyRangeModifier: 3 #Superior Japanese folded steel + heavyRangeModifier: 2.75 #Superior Japanese folded steel heavyDamageBaseModifier: 1.25 - heavyStaminaCost: 10 + heavyStaminaCost: 15 maxTargets: 1 angle: 20 - type: Item @@ -120,7 +120,7 @@ heavyRateModifier: 0.8 heavyRangeModifier: 1.25 heavyDamageBaseModifier: 1.2 - heavyStaminaCost: 10 + heavyStaminaCost: 7.5 angle: 80 soundHit: path: /Audio/Weapons/bladeslice.ogg @@ -151,7 +151,7 @@ heavyRateModifier: 0.5 heavyRangeModifier: 1 heavyDamageBaseModifier: 1 - heavyStaminaCost: 20 + heavyStaminaCost: 15 maxTargets: 10 angle: 200 soundHit: @@ -186,7 +186,7 @@ heavyRateModifier: 0.8 heavyRangeModifier: 1.2 heavyDamageBaseModifier: 1.2 - heavyStaminaCost: 10 + heavyStaminaCost: 7.5 maxTargets: 3 angle: 40 soundHit: diff --git a/Resources/Prototypes/Entities/Objects/Weapons/security.yml b/Resources/Prototypes/Entities/Objects/Weapons/security.yml index a952713dd5f..2c72afa1d27 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/security.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/security.yml @@ -34,12 +34,10 @@ damage: types: Blunt: 7 - bluntStaminaDamageFactor: 2.0 + bluntStaminaDamageFactor: 2.5 heavyRateModifier: 0.75 heavyDamageBaseModifier: 1.75 - heavyStaminaCost: 5 - maxTargets: 3 - angle: 60 + heavyStaminaCost: 1 animation: WeaponArcSlash - type: StaminaDamageOnHit damage: 35 @@ -106,7 +104,7 @@ bluntStaminaDamageFactor: 2 heavyRateModifier: 1 heavyDamageBaseModifier: 1.2 - heavyStaminaCost: 10 + heavyStaminaCost: 7.5 - type: Item size: Normal - type: Clothing From 6a653c550215e6794340d2089e3a52b172f8ff3f Mon Sep 17 00:00:00 2001 From: SimpleStation Changelogs Date: Fri, 20 Sep 2024 19:35:15 +0000 Subject: [PATCH 35/55] Automatic Changelog Update (#938) --- Resources/Changelog/Changelog.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index a9dad4eeec5..10df4525ab9 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -6513,3 +6513,10 @@ Entries: id: 6375 time: '2024-09-20T19:34:02.0000000+00:00' url: https://github.com/Simple-Station/Einstein-Engines/pull/939 +- author: ODJ + changes: + - type: Tweak + message: Tweaked melee; Less stamina usage on heavy attacks. + id: 6376 + time: '2024-09-20T19:34:46.0000000+00:00' + url: https://github.com/Simple-Station/Einstein-Engines/pull/938 From 104ee3eca531e2e3604d6f3dcbd9a826dd8dcbfa Mon Sep 17 00:00:00 2001 From: VMSolidus Date: Fri, 20 Sep 2024 16:05:31 -0400 Subject: [PATCH 36/55] Variable NPC Juke Aggression (#935) # Description This PR un-hardcodes the JukeSystem timer, such that individual NPC HTN Blackboards can directly state how often, and how far they wish to juke. The effect of this is that NPCs are no longer completely impossible to hit with left-clicks in melee combat, while also allowing for more "Elite" enemies that juke more aggressively and more often to be created. Additionally, by introducing an exit condition based on this new "Juke Cooldown", I have made the Juke Operator run a metric shitload of expensive calculations 5000 times less often.

Media

Melee enemy with the new default juke settings, 0.5 second juke duration, with 5 second juke Cooldown. The reagent slime will attempt to close to melee with its enemy, but will now only attempt to evade melee attacks once per 5 seconds. They will otherwise attempt to stay within melee range. https://github.com/user-attachments/assets/653e2064-e404-4be6-a958-da43096de502

# Changelog :cl: - tweak: JukeOperator now allows for JukeDuration and JukeCooldown arguments. JukeCooldown is a new feature where enemies must wait an amount of time in seconds equal to the JukeCooldown, before they are allowed to attempt to dodge a melee attack. - tweak: By default, NPCs will only attempt to dodge attacks once every 5 seconds. - fix: JukeOperator now performs extremely expensive math 5000 times less often. EXIT CONDITIONS PEOPLE! --- .../NPC/Components/NPCJukeComponent.cs | 12 ++++--- .../Operators/Combat/JukeOperator.cs | 18 ++++++++-- Content.Server/NPC/Systems/NPCJukeSystem.cs | 33 ++++++++----------- 3 files changed, 37 insertions(+), 26 deletions(-) diff --git a/Content.Server/NPC/Components/NPCJukeComponent.cs b/Content.Server/NPC/Components/NPCJukeComponent.cs index 2c4136c24b9..768feeca6fc 100644 --- a/Content.Server/NPC/Components/NPCJukeComponent.cs +++ b/Content.Server/NPC/Components/NPCJukeComponent.cs @@ -1,4 +1,3 @@ -using Content.Server.NPC.HTN.PrimitiveTasks.Operators.Combat; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; namespace Content.Server.NPC.Components; @@ -6,17 +5,20 @@ namespace Content.Server.NPC.Components; [RegisterComponent, AutoGenerateComponentPause] public sealed partial class NPCJukeComponent : Component { - [DataField("jukeType")] + [DataField] public JukeType JukeType = JukeType.Away; - [DataField("jukeDuration")] + [DataField] public float JukeDuration = 0.5f; - [DataField("nextJuke", customTypeSerializer:typeof(TimeOffsetSerializer))] + [DataField] + public float JukeCooldown = 3f; + + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] [AutoPausedField] public TimeSpan NextJuke; - [DataField("targetTile")] + [DataField] public Vector2i? TargetTile; } diff --git a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Combat/JukeOperator.cs b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Combat/JukeOperator.cs index 02a3b085104..68029f5a4c2 100644 --- a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Combat/JukeOperator.cs +++ b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Combat/JukeOperator.cs @@ -6,17 +6,31 @@ public sealed partial class JukeOperator : HTNOperator, IHtnConditionalShutdown { [Dependency] private readonly IEntityManager _entManager = default!; - [DataField("jukeType")] + [DataField] public JukeType JukeType = JukeType.AdjacentTile; - [DataField("shutdownState")] + [DataField] public HTNPlanState ShutdownState { get; private set; } = HTNPlanState.PlanFinished; + /// + /// Controls how long(in seconds) the NPC will move while juking. + /// + [DataField] + public float JukeDuration = 0.5f; + + /// + /// Controls how often (in seconds) an NPC will try to juke. + /// + [DataField] + public float JukeCooldown = 3f; + public override void Startup(NPCBlackboard blackboard) { base.Startup(blackboard); var juke = _entManager.EnsureComponent(blackboard.GetValue(NPCBlackboard.Owner)); juke.JukeType = JukeType; + juke.JukeDuration = JukeDuration; + juke.JukeCooldown = JukeCooldown; } public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTime) diff --git a/Content.Server/NPC/Systems/NPCJukeSystem.cs b/Content.Server/NPC/Systems/NPCJukeSystem.cs index da9fa1f7615..5a724762ef6 100644 --- a/Content.Server/NPC/Systems/NPCJukeSystem.cs +++ b/Content.Server/NPC/Systems/NPCJukeSystem.cs @@ -3,6 +3,7 @@ using Content.Server.NPC.Events; using Content.Server.NPC.HTN.PrimitiveTasks.Operators.Combat; using Content.Server.Weapons.Melee; +using Content.Shared.Coordinates.Helpers; using Content.Shared.NPC; using Content.Shared.Weapons.Melee; using Robust.Shared.Collections; @@ -38,22 +39,19 @@ public override void Initialize() private void OnJukeSteering(EntityUid uid, NPCJukeComponent component, ref NPCSteeringEvent args) { - if (component.JukeType == JukeType.AdjacentTile) + if (_timing.CurTime < component.NextJuke) { - if (_npcRangedQuery.TryGetComponent(uid, out var ranged) && - ranged.Status == CombatStatus.NotInSight) - { - component.TargetTile = null; - return; - } + component.TargetTile = null; + return; + } - if (_timing.CurTime < component.NextJuke) - { - component.TargetTile = null; - return; - } + component.NextJuke = _timing.CurTime + TimeSpan.FromSeconds(component.JukeCooldown); - if (!TryComp(args.Transform.GridUid, out var grid)) + if (component.JukeType == JukeType.AdjacentTile) + { + if (_npcRangedQuery.TryGetComponent(uid, out var ranged) + && ranged.Status is CombatStatus.NotInSight + || !TryComp(args.Transform.GridUid, out var grid)) { component.TargetTile = null; return; @@ -107,12 +105,11 @@ private void OnJukeSteering(EntityUid uid, NPCJukeComponent component, ref NPCSt var elapsed = _timing.CurTime - component.NextJuke; - // Finished juke, reset timer. - if (elapsed.TotalSeconds > component.JukeDuration || - currentTile == component.TargetTile) + // Finished juke. + if (elapsed.TotalSeconds > component.JukeDuration + || currentTile == component.TargetTile) { component.TargetTile = null; - component.NextJuke = _timing.CurTime + TimeSpan.FromSeconds(component.JukeDuration); return; } @@ -155,9 +152,7 @@ private void OnJukeSteering(EntityUid uid, NPCJukeComponent component, ref NPCSt var obstacleDirection = _transform.GetWorldPosition(melee.Target) - args.WorldPosition; if (obstacleDirection == Vector2.Zero) - { obstacleDirection = _random.NextVector2(); - } // If they're moving away then pursue anyway. // If just hit then always back up a bit. From 68772a8f1c9e405001c78e439d4dab25e35272ab Mon Sep 17 00:00:00 2001 From: SimpleStation Changelogs Date: Fri, 20 Sep 2024 20:05:56 +0000 Subject: [PATCH 37/55] Automatic Changelog Update (#935) --- Resources/Changelog/Changelog.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 10df4525ab9..c559dbcb140 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -6520,3 +6520,22 @@ Entries: id: 6376 time: '2024-09-20T19:34:46.0000000+00:00' url: https://github.com/Simple-Station/Einstein-Engines/pull/938 +- author: VMSolidus + changes: + - type: Tweak + message: >- + JukeOperator now allows for JukeDuration and JukeCooldown arguments. + JukeCooldown is a new feature where enemies must wait an amount of time + in seconds equal to the JukeCooldown, before they are allowed to attempt + to dodge a melee attack. + - type: Tweak + message: >- + By default, NPCs will only attempt to dodge attacks once every 5 + seconds. + - type: Fix + message: >- + JukeOperator now performs extremely expensive math 5000 times less + often. EXIT CONDITIONS PEOPLE! + id: 6377 + time: '2024-09-20T20:05:31.0000000+00:00' + url: https://github.com/Simple-Station/Einstein-Engines/pull/935 From 981c4f6de57474e55481820349482082ea31cac7 Mon Sep 17 00:00:00 2001 From: VMSolidus Date: Fri, 20 Sep 2024 16:16:40 -0400 Subject: [PATCH 38/55] Update SetOutfitCommand.cs (#941) # Description I hate when people don't use nullables correctly, and SOMEHOW a null reference is still being handed into IPCs. --- Content.Server/Administration/Commands/SetOutfitCommand.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Content.Server/Administration/Commands/SetOutfitCommand.cs b/Content.Server/Administration/Commands/SetOutfitCommand.cs index 12312286518..e19c5b72fa4 100644 --- a/Content.Server/Administration/Commands/SetOutfitCommand.cs +++ b/Content.Server/Administration/Commands/SetOutfitCommand.cs @@ -14,6 +14,7 @@ using Robust.Shared.Prototypes; using Content.Server.Silicon.IPC; using Content.Shared.Radio.Components; +using Content.Shared.Cluwne; namespace Content.Server.Administration.Commands { @@ -129,6 +130,8 @@ public static bool SetOutfit(EntityUid target, string gear, IEntityManager entit } } + if (entityManager.HasComponent(target)) + return true; //Fuck it, nuclear option for not Cluwning an IPC because that causes a crash that SOMEHOW ignores null checks. if (entityManager.HasComponent(target)) { var encryption = new InternalEncryptionKeySpawner(); From 81bbf6b3d501db2df04391ea79a59f6164ec9c38 Mon Sep 17 00:00:00 2001 From: VMSolidus Date: Fri, 20 Sep 2024 16:35:43 -0400 Subject: [PATCH 39/55] Longarms Require Wielding (#913) # Description Take 1, I hope it's this simple. This is an important balancing feature that means you can't use longarms (Rifles, Machine Guns, Shotguns, etc.) without holding them in two hands. I might revisit this later and create options that can bypass this requirement, but for now this is needed so that Nukies in EE don't just "Buy Jugsuit, L6, Eshield, Auto-Win". # Changelog :cl: - add: All "Long-arms", Rifles, Light Machine Guns, Shotguns, now require wielding to use. --- .../Entities/Objects/Weapons/Guns/LMGs/lmgs.yml | 1 + .../Entities/Objects/Weapons/Guns/Rifles/rifles.yml | 2 ++ .../Objects/Weapons/Guns/Shotguns/shotguns.yml | 10 ++++++++++ .../Entities/Objects/Weapons/Guns/Snipers/snipers.yml | 2 ++ 4 files changed, 15 insertions(+) diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/LMGs/lmgs.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/LMGs/lmgs.yml index 49b2eeaada1..a721f254c89 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/LMGs/lmgs.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/LMGs/lmgs.yml @@ -14,6 +14,7 @@ slots: - Back - type: Wieldable + - type: GunRequiresWield - type: GunWieldBonus minAngle: -20 maxAngle: -20 diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Rifles/rifles.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Rifles/rifles.yml index c55b2b6b091..2e7265a2c77 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Rifles/rifles.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Rifles/rifles.yml @@ -14,6 +14,8 @@ slots: - Back - suitStorage + - type: Wieldable + - type: GunRequiresWield - type: AmmoCounter - type: Gun fireRate: 5 diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Shotguns/shotguns.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Shotguns/shotguns.yml index 52b05b6d60b..d1635f49791 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Shotguns/shotguns.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Shotguns/shotguns.yml @@ -77,6 +77,8 @@ soundEmpty: path: /Audio/Weapons/Guns/Empty/empty.ogg fireOnDropChance: 0.3 + - type: Wieldable + - type: GunRequiresWield - type: ItemSlots slots: gun_magazine: @@ -118,6 +120,8 @@ - type: Gun fireRate: 2 fireOnDropChance: 0.5 + - type: Wieldable + - type: GunRequiresWield - type: BallisticAmmoProvider capacity: 2 - type: Construction @@ -146,6 +150,8 @@ - type: Clothing sprite: DeltaV/Objects/Weapons/Guns/Shotguns/enforcer.rsi # Delta-V - type: BallisticAmmoProvider + - type: Wieldable + - type: GunRequiresWield - type: entity parent: WeaponShotgunEnforcer @@ -176,6 +182,8 @@ - type: Tag tags: - WeaponShotgunKammerer + - type: Wieldable + - type: GunRequiresWield - type: entity name: sawn-off shotgun @@ -260,6 +268,8 @@ capacity: 1 - type: StaticPrice price: 0 + - type: Wieldable + - type: GunRequiresWield - type: entity name: improvised shotgun diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Snipers/snipers.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Snipers/snipers.yml index c85314e91ca..c4f7a8cd15b 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Snipers/snipers.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Snipers/snipers.yml @@ -18,6 +18,8 @@ - Back - suitStorage - type: AmmoCounter + - type: Wieldable + - type: GunRequiresWield - type: Gun fireRate: 0.75 selectedMode: SemiAuto From 82f497183c533796e4d0a4d17f7c7d69e364844d Mon Sep 17 00:00:00 2001 From: SimpleStation Changelogs Date: Fri, 20 Sep 2024 20:36:09 +0000 Subject: [PATCH 40/55] Automatic Changelog Update (#913) --- Resources/Changelog/Changelog.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index c559dbcb140..00938b31cd4 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -6539,3 +6539,12 @@ Entries: id: 6377 time: '2024-09-20T20:05:31.0000000+00:00' url: https://github.com/Simple-Station/Einstein-Engines/pull/935 +- author: VMSolidus + changes: + - type: Add + message: >- + All "Long-arms", Rifles, Light Machine Guns, Shotguns, now require + wielding to use. + id: 6378 + time: '2024-09-20T20:35:43.0000000+00:00' + url: https://github.com/Simple-Station/Einstein-Engines/pull/913 From 328d71a7cdbc4d4050dc9b4e19661fea96404b82 Mon Sep 17 00:00:00 2001 From: zelezniciar1 <39102800+zelezniciar1@users.noreply.github.com> Date: Fri, 20 Sep 2024 17:46:38 -0400 Subject: [PATCH 41/55] Ports Atmospheric Alerts Computer (#922) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Description - Ports the Atmospheric Alerts Computer, as implemented on WizDen --- # TODO [x] small fixes n/a ---

Media

![Snímka obrazovky 2024-09-13 225958](https://github.com/user-attachments/assets/e03a5535-4e1d-427b-97b3-91b515e7df59)

--- # Changelog :cl: zelezniciar - add: Added Atmospheric Alerts Computer Co-authored-by: SsalamethVersaach --- .../Consoles/AtmosAlarmEntryContainer.xaml | 81 +++ .../Consoles/AtmosAlarmEntryContainer.xaml.cs | 215 +++++++ .../AtmosAlertsComputerBoundUserInterface.cs | 52 ++ .../Consoles/AtmosAlertsComputerWindow.xaml | 108 ++++ .../AtmosAlertsComputerWindow.xaml.cs | 550 ++++++++++++++++++ .../Consoles/AtmosAlertsComputerSystem.cs | 356 ++++++++++++ .../Atmos/Monitor/Systems/AirAlarmSystem.cs | 15 + .../AtmosAlertsComputerComponent.cs | 235 ++++++++ .../Components/AtmosAlertsDeviceComponent.cs | 14 + .../SharedAtmosAlertsComputerSystem.cs | 24 + .../en-US/atmos/atmos-alerts-console.ftl | 35 ++ .../Devices/Circuitboards/computer.yml | 4 +- .../Machines/Computers/computers.yml | 30 +- .../Structures/Wallmounts/air_alarm.yml | 2 + .../Structures/Wallmounts/fire_alarm.yml | 2 + .../Interface/AtmosMonitoring/status_bg.png | Bin 0 -> 157 bytes 16 files changed, 1718 insertions(+), 5 deletions(-) create mode 100644 Content.Client/Atmos/Consoles/AtmosAlarmEntryContainer.xaml create mode 100644 Content.Client/Atmos/Consoles/AtmosAlarmEntryContainer.xaml.cs create mode 100644 Content.Client/Atmos/Consoles/AtmosAlertsComputerBoundUserInterface.cs create mode 100644 Content.Client/Atmos/Consoles/AtmosAlertsComputerWindow.xaml create mode 100644 Content.Client/Atmos/Consoles/AtmosAlertsComputerWindow.xaml.cs create mode 100644 Content.Server/Atmos/Consoles/AtmosAlertsComputerSystem.cs create mode 100644 Content.Shared/Atmos/Consoles/Components/AtmosAlertsComputerComponent.cs create mode 100644 Content.Shared/Atmos/Consoles/Components/AtmosAlertsDeviceComponent.cs create mode 100644 Content.Shared/Atmos/Consoles/SharedAtmosAlertsComputerSystem.cs create mode 100644 Resources/Locale/en-US/atmos/atmos-alerts-console.ftl create mode 100644 Resources/Textures/Interface/AtmosMonitoring/status_bg.png diff --git a/Content.Client/Atmos/Consoles/AtmosAlarmEntryContainer.xaml b/Content.Client/Atmos/Consoles/AtmosAlarmEntryContainer.xaml new file mode 100644 index 00000000000..96f136abf0e --- /dev/null +++ b/Content.Client/Atmos/Consoles/AtmosAlarmEntryContainer.xaml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Content.Client/Atmos/Consoles/AtmosAlarmEntryContainer.xaml.cs b/Content.Client/Atmos/Consoles/AtmosAlarmEntryContainer.xaml.cs new file mode 100644 index 00000000000..b0d0365ef6b --- /dev/null +++ b/Content.Client/Atmos/Consoles/AtmosAlarmEntryContainer.xaml.cs @@ -0,0 +1,215 @@ +using Content.Client.Stylesheets; +using Content.Shared.Atmos; +using Content.Shared.Atmos.Components; +using Content.Shared.Atmos.Monitor; +using Content.Shared.FixedPoint; +using Content.Shared.Temperature; +using Robust.Client.AutoGenerated; +using Robust.Client.Graphics; +using Robust.Client.ResourceManagement; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; +using Robust.Shared.Map; +using System.Linq; + +namespace Content.Client.Atmos.Consoles; + +[GenerateTypedNameReferences] +public sealed partial class AtmosAlarmEntryContainer : BoxContainer +{ + public NetEntity NetEntity; + public EntityCoordinates? Coordinates; + + private readonly IEntityManager _entManager; + private readonly IResourceCache _cache; + + private Dictionary _alarmStrings = new Dictionary() + { + [AtmosAlarmType.Invalid] = "atmos-alerts-window-invalid-state", + [AtmosAlarmType.Normal] = "atmos-alerts-window-normal-state", + [AtmosAlarmType.Warning] = "atmos-alerts-window-warning-state", + [AtmosAlarmType.Danger] = "atmos-alerts-window-danger-state", + }; + + private Dictionary _gasShorthands = new Dictionary() + { + [Gas.Ammonia] = "NH₃", + [Gas.CarbonDioxide] = "CO₂", + [Gas.Frezon] = "F", + [Gas.Nitrogen] = "N₂", + [Gas.NitrousOxide] = "N₂O", + [Gas.Oxygen] = "O₂", + [Gas.Plasma] = "P", + [Gas.Tritium] = "T", + [Gas.WaterVapor] = "H₂O", + }; + + public AtmosAlarmEntryContainer(NetEntity uid, EntityCoordinates? coordinates) + { + RobustXamlLoader.Load(this); + + _entManager = IoCManager.Resolve(); + _cache = IoCManager.Resolve(); + + NetEntity = uid; + Coordinates = coordinates; + + // Load fonts + var headerFont = new VectorFont(_cache.GetResource("/Fonts/NotoSans/NotoSans-Bold.ttf"), 11); + var normalFont = new VectorFont(_cache.GetResource("/Fonts/NotoSansDisplay/NotoSansDisplay-Regular.ttf"), 11); + var smallFont = new VectorFont(_cache.GetResource("/Fonts/NotoSans/NotoSans-Regular.ttf"), 10); + + // Set fonts + TemperatureHeaderLabel.FontOverride = headerFont; + PressureHeaderLabel.FontOverride = headerFont; + OxygenationHeaderLabel.FontOverride = headerFont; + GasesHeaderLabel.FontOverride = headerFont; + + TemperatureLabel.FontOverride = normalFont; + PressureLabel.FontOverride = normalFont; + OxygenationLabel.FontOverride = normalFont; + + NoDataLabel.FontOverride = headerFont; + + SilenceCheckBox.Label.FontOverride = smallFont; + SilenceCheckBox.Label.FontColorOverride = Color.DarkGray; + } + + public void UpdateEntry(AtmosAlertsComputerEntry entry, bool isFocus, AtmosAlertsFocusDeviceData? focusData = null) + { + NetEntity = entry.NetEntity; + Coordinates = _entManager.GetCoordinates(entry.Coordinates); + + // Load fonts + var normalFont = new VectorFont(_cache.GetResource("/Fonts/NotoSansDisplay/NotoSansDisplay-Regular.ttf"), 11); + + // Update alarm state + if (!_alarmStrings.TryGetValue(entry.AlarmState, out var alarmString)) + alarmString = "atmos-alerts-window-invalid-state"; + + AlarmStateLabel.Text = Loc.GetString(alarmString); + AlarmStateLabel.FontColorOverride = GetAlarmStateColor(entry.AlarmState); + + // Update alarm name + AlarmNameLabel.Text = Loc.GetString("atmos-alerts-window-alarm-label", ("name", entry.EntityName), ("address", entry.Address)); + + // Focus updates + FocusContainer.Visible = isFocus; + + if (isFocus) + SetAsFocus(); + else + RemoveAsFocus(); + + if (isFocus && entry.Group == AtmosAlertsComputerGroup.AirAlarm) + { + MainDataContainer.Visible = (entry.AlarmState != AtmosAlarmType.Invalid); + NoDataLabel.Visible = (entry.AlarmState == AtmosAlarmType.Invalid); + + if (focusData != null) + { + // Update temperature + var tempK = (FixedPoint2)focusData.Value.TemperatureData.Item1; + var tempC = (FixedPoint2)TemperatureHelpers.KelvinToCelsius(tempK.Float()); + + TemperatureLabel.Text = Loc.GetString("atmos-alerts-window-temperature-value", ("valueInC", tempC), ("valueInK", tempK)); + TemperatureLabel.FontColorOverride = GetAlarmStateColor(focusData.Value.TemperatureData.Item2); + + // Update pressure + PressureLabel.Text = Loc.GetString("atmos-alerts-window-pressure-value", ("value", (FixedPoint2)focusData.Value.PressureData.Item1)); + PressureLabel.FontColorOverride = GetAlarmStateColor(focusData.Value.PressureData.Item2); + + // Update oxygenation + var oxygenPercent = (FixedPoint2)0f; + var oxygenAlert = AtmosAlarmType.Invalid; + + if (focusData.Value.GasData.TryGetValue(Gas.Oxygen, out var oxygenData)) + { + oxygenPercent = oxygenData.Item2 * 100f; + oxygenAlert = oxygenData.Item3; + } + + OxygenationLabel.Text = Loc.GetString("atmos-alerts-window-oxygenation-value", ("value", oxygenPercent)); + OxygenationLabel.FontColorOverride = GetAlarmStateColor(oxygenAlert); + + // Update other present gases + GasGridContainer.RemoveAllChildren(); + + var gasData = focusData.Value.GasData.Where(g => g.Key != Gas.Oxygen); + + if (gasData.Count() == 0) + { + // No other gases + var gasLabel = new Label() + { + Text = Loc.GetString("atmos-alerts-window-other-gases-value-nil"), + FontOverride = normalFont, + FontColorOverride = StyleNano.DisabledFore, + HorizontalAlignment = HAlignment.Center, + VerticalAlignment = VAlignment.Center, + HorizontalExpand = true, + Margin = new Thickness(0, 2, 0, 0), + SetHeight = 24f, + }; + + GasGridContainer.AddChild(gasLabel); + } + + else + { + // Add an entry for each gas + foreach ((var gas, (var mol, var percent, var alert)) in gasData) + { + var gasPercent = (FixedPoint2)0f; + gasPercent = percent * 100f; + + if (!_gasShorthands.TryGetValue(gas, out var gasShorthand)) + gasShorthand = "X"; + + var gasLabel = new Label() + { + Text = Loc.GetString("atmos-alerts-window-other-gases-value", ("shorthand", gasShorthand), ("value", gasPercent)), + FontOverride = normalFont, + FontColorOverride = GetAlarmStateColor(alert), + HorizontalAlignment = HAlignment.Center, + VerticalAlignment = VAlignment.Center, + HorizontalExpand = true, + Margin = new Thickness(0, 2, 0, 0), + SetHeight = 24f, + }; + + GasGridContainer.AddChild(gasLabel); + } + } + } + } + } + + public void SetAsFocus() + { + FocusButton.AddStyleClass(StyleNano.StyleClassButtonColorGreen); + ArrowTexture.TexturePath = "/Textures/Interface/Nano/inverted_triangle.svg.png"; + } + + public void RemoveAsFocus() + { + FocusButton.RemoveStyleClass(StyleNano.StyleClassButtonColorGreen); + ArrowTexture.TexturePath = "/Textures/Interface/Nano/triangle_right.png"; + FocusContainer.Visible = false; + } + + private Color GetAlarmStateColor(AtmosAlarmType alarmType) + { + switch (alarmType) + { + case AtmosAlarmType.Normal: + return StyleNano.GoodGreenFore; + case AtmosAlarmType.Warning: + return StyleNano.ConcerningOrangeFore; + case AtmosAlarmType.Danger: + return StyleNano.DangerousRedFore; + } + + return StyleNano.DisabledFore; + } +} \ No newline at end of file diff --git a/Content.Client/Atmos/Consoles/AtmosAlertsComputerBoundUserInterface.cs b/Content.Client/Atmos/Consoles/AtmosAlertsComputerBoundUserInterface.cs new file mode 100644 index 00000000000..08cae979b9b --- /dev/null +++ b/Content.Client/Atmos/Consoles/AtmosAlertsComputerBoundUserInterface.cs @@ -0,0 +1,52 @@ +using Content.Shared.Atmos.Components; + +namespace Content.Client.Atmos.Consoles; + +public sealed class AtmosAlertsComputerBoundUserInterface : BoundUserInterface +{ + [ViewVariables] + private AtmosAlertsComputerWindow? _menu; + + public AtmosAlertsComputerBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) { } + + protected override void Open() + { + _menu = new AtmosAlertsComputerWindow(this, Owner); + _menu.OpenCentered(); + _menu.OnClose += Close; + + EntMan.TryGetComponent(Owner, out var xform); + } + + protected override void UpdateState(BoundUserInterfaceState state) + { + base.UpdateState(state); + + var castState = (AtmosAlertsComputerBoundInterfaceState) state; + + if (castState == null) + return; + + EntMan.TryGetComponent(Owner, out var xform); + _menu?.UpdateUI(xform?.Coordinates, castState.AirAlarms, castState.FireAlarms, castState.FocusData); + } + + public void SendFocusChangeMessage(NetEntity? netEntity) + { + SendMessage(new AtmosAlertsComputerFocusChangeMessage(netEntity)); + } + + public void SendDeviceSilencedMessage(NetEntity netEntity, bool silenceDevice) + { + SendMessage(new AtmosAlertsComputerDeviceSilencedMessage(netEntity, silenceDevice)); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + if (!disposing) + return; + + _menu?.Dispose(); + } +} diff --git a/Content.Client/Atmos/Consoles/AtmosAlertsComputerWindow.xaml b/Content.Client/Atmos/Consoles/AtmosAlertsComputerWindow.xaml new file mode 100644 index 00000000000..8824a776ee6 --- /dev/null +++ b/Content.Client/Atmos/Consoles/AtmosAlertsComputerWindow.xaml @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Content.Client/Atmos/Consoles/AtmosAlertsComputerWindow.xaml.cs b/Content.Client/Atmos/Consoles/AtmosAlertsComputerWindow.xaml.cs new file mode 100644 index 00000000000..a55321833cd --- /dev/null +++ b/Content.Client/Atmos/Consoles/AtmosAlertsComputerWindow.xaml.cs @@ -0,0 +1,550 @@ +using Content.Client.Message; +using Content.Client.Pinpointer.UI; +using Content.Client.Stylesheets; +using Content.Client.UserInterface.Controls; +using Content.Shared.Atmos.Components; +using Content.Shared.Atmos.Monitor; +using Content.Shared.Pinpointer; +using Robust.Client.AutoGenerated; +using Robust.Client.GameObjects; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; +using Robust.Shared.Map; +using Robust.Shared.Timing; +using Robust.Shared.Utility; +using Robust.Shared.ContentPack; +using Robust.Shared.Prototypes; +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +namespace Content.Client.Atmos.Consoles; + +[GenerateTypedNameReferences] +public sealed partial class AtmosAlertsComputerWindow : FancyWindow +{ + private readonly IEntityManager _entManager; + private readonly SpriteSystem _spriteSystem; + + private EntityUid? _owner; + private NetEntity? _trackedEntity; + + private AtmosAlertsComputerEntry[]? _airAlarms = null; + private AtmosAlertsComputerEntry[]? _fireAlarms = null; + private IEnumerable? _allAlarms = null; + + private IEnumerable? _activeAlarms = null; + private Dictionary _deviceSilencingProgress = new(); + + public event Action? SendFocusChangeMessageAction; + public event Action? SendDeviceSilencedMessageAction; + + private bool _autoScrollActive = false; + private bool _autoScrollAwaitsUpdate = false; + + private const float SilencingDuration = 2.5f; + + public AtmosAlertsComputerWindow(AtmosAlertsComputerBoundUserInterface userInterface, EntityUid? owner) + { + RobustXamlLoader.Load(this); + _entManager = IoCManager.Resolve(); + _spriteSystem = _entManager.System(); + + // Pass the owner to nav map + _owner = owner; + NavMap.Owner = _owner; + + // Set nav map colors + NavMap.WallColor = new Color(64, 64, 64); + NavMap.TileColor = Color.DimGray * NavMap.WallColor; + + // Set nav map grid uid + var stationName = Loc.GetString("atmos-alerts-window-unknown-location"); + + if (_entManager.TryGetComponent(owner, out var xform)) + { + NavMap.MapUid = xform.GridUid; + + // Assign station name + if (_entManager.TryGetComponent(xform.GridUid, out var stationMetaData)) + stationName = stationMetaData.EntityName; + + var msg = new FormattedMessage(); + msg.AddMarkup(Loc.GetString("atmos-alerts-window-station-name", ("stationName", stationName))); + + StationName.SetMessage(msg); + } + + else + { + StationName.SetMessage(stationName); + NavMap.Visible = false; + } + + // Set trackable entity selected action + NavMap.TrackedEntitySelectedAction += SetTrackedEntityFromNavMap; + + // Update nav map + NavMap.ForceNavMapUpdate(); + + // Set tab container headers + MasterTabContainer.SetTabTitle(0, Loc.GetString("atmos-alerts-window-tab-no-alerts")); + MasterTabContainer.SetTabTitle(1, Loc.GetString("atmos-alerts-window-tab-air-alarms")); + MasterTabContainer.SetTabTitle(2, Loc.GetString("atmos-alerts-window-tab-fire-alarms")); + + // Set UI toggles + ShowInactiveAlarms.OnToggled += _ => OnShowAlarmsToggled(ShowInactiveAlarms, AtmosAlarmType.Invalid); + ShowNormalAlarms.OnToggled += _ => OnShowAlarmsToggled(ShowNormalAlarms, AtmosAlarmType.Normal); + ShowWarningAlarms.OnToggled += _ => OnShowAlarmsToggled(ShowWarningAlarms, AtmosAlarmType.Warning); + ShowDangerAlarms.OnToggled += _ => OnShowAlarmsToggled(ShowDangerAlarms, AtmosAlarmType.Danger); + + // Set atmos monitoring message action + SendFocusChangeMessageAction += userInterface.SendFocusChangeMessage; + SendDeviceSilencedMessageAction += userInterface.SendDeviceSilencedMessage; + } + + #region Toggle handling + + private void OnShowAlarmsToggled(CheckBox toggle, AtmosAlarmType toggledAlarmState) + { + if (_owner == null) + return; + + if (!_entManager.TryGetComponent(_owner.Value, out var console)) + return; + + foreach (var device in console.AtmosDevices) + { + var alarmState = GetAlarmState(device.NetEntity); + + if (toggledAlarmState != alarmState) + continue; + + if (toggle.Pressed) + AddTrackedEntityToNavMap(device, alarmState); + + else + NavMap.TrackedEntities.Remove(device.NetEntity); + } + } + + private void OnSilenceAlertsToggled(NetEntity netEntity, bool toggleState) + { + if (!_entManager.TryGetComponent(_owner, out var console)) + return; + + if (toggleState) + _deviceSilencingProgress[netEntity] = SilencingDuration; + + else + _deviceSilencingProgress.Remove(netEntity); + + foreach (AtmosAlarmEntryContainer entryContainer in AlertsTable.Children) + { + if (entryContainer.NetEntity == netEntity) + entryContainer.SilenceAlarmProgressBar.Visible = toggleState; + } + + SendDeviceSilencedMessageAction?.Invoke(netEntity, toggleState); + } + + #endregion + + public void UpdateUI(EntityCoordinates? consoleCoords, AtmosAlertsComputerEntry[] airAlarms, AtmosAlertsComputerEntry[] fireAlarms, AtmosAlertsFocusDeviceData? focusData) + { + if (_owner == null) + return; + + if (!_entManager.TryGetComponent(_owner.Value, out var console)) + return; + + if (_trackedEntity != focusData?.NetEntity) + { + SendFocusChangeMessageAction?.Invoke(_trackedEntity); + focusData = null; + } + + // Retain alarm data for use inbetween updates + _airAlarms = airAlarms; + _fireAlarms = fireAlarms; + _allAlarms = airAlarms.Concat(fireAlarms); + + var silenced = console.SilencedDevices; + + _activeAlarms = _allAlarms.Where(x => x.AlarmState > AtmosAlarmType.Normal && + (!silenced.Contains(x.NetEntity) || _deviceSilencingProgress.ContainsKey(x.NetEntity))); + + // Reset nav map data + NavMap.TrackedCoordinates.Clear(); + NavMap.TrackedEntities.Clear(); + + // Add tracked entities to the nav map + foreach (var device in console.AtmosDevices) + { + if (!NavMap.Visible) + continue; + + var alarmState = GetAlarmState(device.NetEntity); + + if (_trackedEntity != device.NetEntity) + { + // Skip air alarms if the appropriate overlay is off + if (!ShowInactiveAlarms.Pressed && alarmState == AtmosAlarmType.Invalid) + continue; + + if (!ShowNormalAlarms.Pressed && alarmState == AtmosAlarmType.Normal) + continue; + + if (!ShowWarningAlarms.Pressed && alarmState == AtmosAlarmType.Warning) + continue; + + if (!ShowDangerAlarms.Pressed && alarmState == AtmosAlarmType.Danger) + continue; + } + + AddTrackedEntityToNavMap(device, alarmState); + } + + // Show the monitor location + var consoleUid = _entManager.GetNetEntity(_owner); + + if (consoleCoords != null && consoleUid != null) + { + var texture = _spriteSystem.Frame0(new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_circle.png"))); + var blip = new NavMapBlip(consoleCoords.Value, texture, Color.Cyan, true, false); + NavMap.TrackedEntities[consoleUid.Value] = blip; + } + + // Update the nav map + NavMap.ForceNavMapUpdate(); + + // Clear excess children from the tables + var activeAlarmCount = _activeAlarms.Count(); + + while (AlertsTable.ChildCount > activeAlarmCount) + AlertsTable.RemoveChild(AlertsTable.GetChild(AlertsTable.ChildCount - 1)); + + while (AirAlarmsTable.ChildCount > airAlarms.Length) + AirAlarmsTable.RemoveChild(AirAlarmsTable.GetChild(AirAlarmsTable.ChildCount - 1)); + + while (FireAlarmsTable.ChildCount > fireAlarms.Length) + FireAlarmsTable.RemoveChild(FireAlarmsTable.GetChild(FireAlarmsTable.ChildCount - 1)); + + // Update all entries in each table + for (int index = 0; index < _activeAlarms.Count(); index++) + { + var entry = _activeAlarms.ElementAt(index); + UpdateUIEntry(entry, index, AlertsTable, console, focusData); + } + + for (int index = 0; index < airAlarms.Count(); index++) + { + var entry = airAlarms.ElementAt(index); + UpdateUIEntry(entry, index, AirAlarmsTable, console, focusData); + } + + for (int index = 0; index < fireAlarms.Count(); index++) + { + var entry = fireAlarms.ElementAt(index); + UpdateUIEntry(entry, index, FireAlarmsTable, console, focusData); + } + + // If no alerts are active, display a message + if (MasterTabContainer.CurrentTab == 0 && activeAlarmCount == 0) + { + var label = new RichTextLabel() + { + HorizontalExpand = true, + VerticalExpand = true, + HorizontalAlignment = HAlignment.Center, + VerticalAlignment = VAlignment.Center, + }; + + label.SetMarkup(Loc.GetString("atmos-alerts-window-no-active-alerts", ("color", StyleNano.GoodGreenFore.ToHexNoAlpha()))); + + AlertsTable.AddChild(label); + } + + // Update the alerts tab with the number of active alerts + if (activeAlarmCount == 0) + MasterTabContainer.SetTabTitle(0, Loc.GetString("atmos-alerts-window-tab-no-alerts")); + + else + MasterTabContainer.SetTabTitle(0, Loc.GetString("atmos-alerts-window-tab-alerts", ("value", activeAlarmCount))); + + // Auto-scroll re-enable + if (_autoScrollAwaitsUpdate) + { + _autoScrollActive = true; + _autoScrollAwaitsUpdate = false; + } + } + + private void AddTrackedEntityToNavMap(AtmosAlertsDeviceNavMapData metaData, AtmosAlarmType alarmState) + { + var data = GetBlipTexture(alarmState); + + if (data == null) + return; + + var texture = data.Value.Item1; + var color = data.Value.Item2; + var coords = _entManager.GetCoordinates(metaData.NetCoordinates); + + if (_trackedEntity != null && _trackedEntity != metaData.NetEntity) + color *= Color.DimGray; + + var selectable = true; + var blip = new NavMapBlip(coords, _spriteSystem.Frame0(texture), color, _trackedEntity == metaData.NetEntity, selectable); + + NavMap.TrackedEntities[metaData.NetEntity] = blip; + } + + private void UpdateUIEntry(AtmosAlertsComputerEntry entry, int index, Control table, AtmosAlertsComputerComponent console, AtmosAlertsFocusDeviceData? focusData = null) + { + // Make new UI entry if required + if (index >= table.ChildCount) + { + var newEntryContainer = new AtmosAlarmEntryContainer(entry.NetEntity, _entManager.GetCoordinates(entry.Coordinates)); + + // On click + newEntryContainer.FocusButton.OnButtonUp += args => + { + if (_trackedEntity == newEntryContainer.NetEntity) + { + _trackedEntity = null; + } + + else + { + _trackedEntity = newEntryContainer.NetEntity; + + if (newEntryContainer.Coordinates != null) + NavMap.CenterToCoordinates(newEntryContainer.Coordinates.Value); + } + + // Send message to console that the focus has changed + SendFocusChangeMessageAction?.Invoke(_trackedEntity); + + // Update affected UI elements across all tables + UpdateConsoleTable(console, AlertsTable, _trackedEntity); + UpdateConsoleTable(console, AirAlarmsTable, _trackedEntity); + UpdateConsoleTable(console, FireAlarmsTable, _trackedEntity); + }; + + // On toggling the silence check box + newEntryContainer.SilenceCheckBox.OnToggled += _ => OnSilenceAlertsToggled(newEntryContainer.NetEntity, newEntryContainer.SilenceCheckBox.Pressed); + + // Add the entry to the current table + table.AddChild(newEntryContainer); + } + + // Update values and UI elements + var tableChild = table.GetChild(index); + + if (tableChild is not AtmosAlarmEntryContainer) + { + table.RemoveChild(tableChild); + UpdateUIEntry(entry, index, table, console, focusData); + + return; + } + + var entryContainer = (AtmosAlarmEntryContainer)tableChild; + + entryContainer.UpdateEntry(entry, entry.NetEntity == _trackedEntity, focusData); + + if (_trackedEntity != entry.NetEntity) + { + var silenced = console.SilencedDevices; + entryContainer.SilenceCheckBox.Pressed = (silenced.Contains(entry.NetEntity) || _deviceSilencingProgress.ContainsKey(entry.NetEntity)); + } + + entryContainer.SilenceAlarmProgressBar.Visible = (table == AlertsTable && _deviceSilencingProgress.ContainsKey(entry.NetEntity)); + } + + private void UpdateConsoleTable(AtmosAlertsComputerComponent console, Control table, NetEntity? currTrackedEntity) + { + foreach (var tableChild in table.Children) + { + if (tableChild is not AtmosAlarmEntryContainer) + continue; + + var entryContainer = (AtmosAlarmEntryContainer)tableChild; + + if (entryContainer.NetEntity != currTrackedEntity) + entryContainer.RemoveAsFocus(); + + else if (entryContainer.NetEntity == currTrackedEntity) + entryContainer.SetAsFocus(); + } + } + + private void SetTrackedEntityFromNavMap(NetEntity? netEntity) + { + if (netEntity == null) + return; + + if (!_entManager.TryGetComponent(_owner, out var console)) + return; + + _trackedEntity = netEntity; + + if (netEntity != null) + { + // Tab switching + if (MasterTabContainer.CurrentTab != 0 || _activeAlarms?.Any(x => x.NetEntity == netEntity) == false) + { + var device = console.AtmosDevices.FirstOrNull(x => x.NetEntity == netEntity); + + switch (device?.Group) + { + case AtmosAlertsComputerGroup.AirAlarm: + MasterTabContainer.CurrentTab = 1; break; + case AtmosAlertsComputerGroup.FireAlarm: + MasterTabContainer.CurrentTab = 2; break; + } + } + + // Get the scroll position of the selected entity on the selected button the UI + ActivateAutoScrollToFocus(); + } + + // Send message to console that the focus has changed + SendFocusChangeMessageAction?.Invoke(_trackedEntity); + } + + protected override void FrameUpdate(FrameEventArgs args) + { + AutoScrollToFocus(); + + // Device silencing update + foreach ((var device, var remainingTime) in _deviceSilencingProgress) + { + var t = remainingTime - args.DeltaSeconds; + + if (t <= 0) + { + _deviceSilencingProgress.Remove(device); + + if (device == _trackedEntity) + _trackedEntity = null; + } + + else + _deviceSilencingProgress[device] = t; + } + } + + private void ActivateAutoScrollToFocus() + { + _autoScrollActive = false; + _autoScrollAwaitsUpdate = true; + } + + private void AutoScrollToFocus() + { + if (!_autoScrollActive) + return; + + var scroll = MasterTabContainer.Children.ElementAt(MasterTabContainer.CurrentTab) as ScrollContainer; + if (scroll == null) + return; + + if (!TryGetVerticalScrollbar(scroll, out var vScrollbar)) + return; + + if (!TryGetNextScrollPosition(out float? nextScrollPosition)) + return; + + vScrollbar.ValueTarget = nextScrollPosition.Value; + + if (MathHelper.CloseToPercent(vScrollbar.Value, vScrollbar.ValueTarget)) + _autoScrollActive = false; + } + + private bool TryGetVerticalScrollbar(ScrollContainer scroll, [NotNullWhen(true)] out VScrollBar? vScrollBar) + { + vScrollBar = null; + + foreach (var child in scroll.Children) + { + if (child is not VScrollBar) + continue; + + var castChild = child as VScrollBar; + + if (castChild != null) + { + vScrollBar = castChild; + return true; + } + } + + return false; + } + + private bool TryGetNextScrollPosition([NotNullWhen(true)] out float? nextScrollPosition) + { + nextScrollPosition = null; + + var scroll = MasterTabContainer.Children.ElementAt(MasterTabContainer.CurrentTab) as ScrollContainer; + if (scroll == null) + return false; + + var container = scroll.Children.ElementAt(0) as BoxContainer; + if (container == null || container.Children.Count() == 0) + return false; + + // Exit if the heights of the children haven't been initialized yet + if (!container.Children.Any(x => x.Height > 0)) + return false; + + nextScrollPosition = 0; + + foreach (var control in container.Children) + { + if (control == null || control is not AtmosAlarmEntryContainer) + continue; + + if (((AtmosAlarmEntryContainer)control).NetEntity == _trackedEntity) + return true; + + nextScrollPosition += control.Height; + } + + // Failed to find control + nextScrollPosition = null; + + return false; + } + + private AtmosAlarmType GetAlarmState(NetEntity netEntity) + { + var alarmState = _allAlarms?.FirstOrNull(x => x.NetEntity == netEntity)?.AlarmState; + + if (alarmState == null) + return AtmosAlarmType.Invalid; + + return alarmState.Value; + } + + private (SpriteSpecifier.Texture, Color)? GetBlipTexture(AtmosAlarmType alarmState) + { + (SpriteSpecifier.Texture, Color)? output = null; + + switch (alarmState) + { + case AtmosAlarmType.Invalid: + output = (new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_circle.png")), StyleNano.DisabledFore); break; + case AtmosAlarmType.Normal: + output = (new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_circle.png")), Color.LimeGreen); break; + case AtmosAlarmType.Warning: + output = (new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_triangle.png")), new Color(255, 182, 72)); break; + case AtmosAlarmType.Danger: + output = (new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_square.png")), new Color(255, 67, 67)); break; + } + + return output; + } +} \ No newline at end of file diff --git a/Content.Server/Atmos/Consoles/AtmosAlertsComputerSystem.cs b/Content.Server/Atmos/Consoles/AtmosAlertsComputerSystem.cs new file mode 100644 index 00000000000..758fde88f13 --- /dev/null +++ b/Content.Server/Atmos/Consoles/AtmosAlertsComputerSystem.cs @@ -0,0 +1,356 @@ +using Content.Server.Atmos.Monitor.Components; +using Content.Server.DeviceNetwork.Components; +using Content.Server.Power.Components; +using Content.Shared.Atmos; +using Content.Shared.Atmos.Components; +using Content.Shared.Atmos.Consoles; +using Content.Shared.Atmos.Monitor; +using Content.Shared.Atmos.Monitor.Components; +using Content.Shared.Pinpointer; +using Robust.Server.GameObjects; +using Robust.Shared.Map.Components; +using Robust.Shared.Player; +using Robust.Shared.ContentPack; +using Robust.Shared.Prototypes; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Content.Shared.Access.Components; +using Content.Shared.Database; +using Content.Shared.NameIdentifier; +using Content.Shared.Stacks; +using JetBrains.Annotations; +using Robust.Shared.Utility; + +namespace Content.Server.Atmos.Monitor.Systems; + +public sealed class AtmosAlertsComputerSystem : SharedAtmosAlertsComputerSystem +{ + [Dependency] private readonly UserInterfaceSystem _uiSystem = default!; + [Dependency] private readonly AirAlarmSystem _airAlarmSystem = default!; + [Dependency] private readonly AtmosDeviceNetworkSystem _atmosDevNet = default!; + [Dependency] private readonly SharedAppearanceSystem _appearance = default!; + + private const float UpdateTime = 1.0f; + + // Note: this data does not need to be saved + private float _updateTimer = 1.0f; + + public override void Initialize() + { + base.Initialize(); + + // Console events + SubscribeLocalEvent(OnConsoleInit); + SubscribeLocalEvent(OnConsoleParentChanged); + SubscribeLocalEvent(OnFocusChangedMessage); + + // Grid events + SubscribeLocalEvent(OnGridSplit); + SubscribeLocalEvent(OnDeviceAnchorChanged); + } + + #region Event handling + + private void OnConsoleInit(EntityUid uid, AtmosAlertsComputerComponent component, ComponentInit args) + { + InitalizeConsole(uid, component); + } + + private void OnConsoleParentChanged(EntityUid uid, AtmosAlertsComputerComponent component, EntParentChangedMessage args) + { + InitalizeConsole(uid, component); + } + + private void OnFocusChangedMessage(EntityUid uid, AtmosAlertsComputerComponent component, AtmosAlertsComputerFocusChangeMessage args) + { + component.FocusDevice = args.FocusDevice; + } + + private void OnGridSplit(ref GridSplitEvent args) + { + // Collect grids + var allGrids = args.NewGrids.ToList(); + + if (!allGrids.Contains(args.Grid)) + allGrids.Add(args.Grid); + + // Update atmos monitoring consoles that stand upon an updated grid + var query = AllEntityQuery(); + while (query.MoveNext(out var ent, out var entConsole, out var entXform)) + { + if (entXform.GridUid == null) + continue; + + if (!allGrids.Contains(entXform.GridUid.Value)) + continue; + + InitalizeConsole(ent, entConsole); + } + } + + private void OnDeviceAnchorChanged(EntityUid uid, AtmosAlertsDeviceComponent component, AnchorStateChangedEvent args) + { + var xform = Transform(uid); + var gridUid = xform.GridUid; + + if (gridUid == null) + return; + + if (!TryGetAtmosDeviceNavMapData(uid, component, xform, gridUid.Value, out var data)) + return; + + var netEntity = EntityManager.GetNetEntity(uid); + + var query = AllEntityQuery(); + while (query.MoveNext(out var ent, out var entConsole, out var entXform)) + { + if (gridUid != entXform.GridUid) + continue; + + if (args.Anchored) + entConsole.AtmosDevices.Add(data.Value); + + else if (!args.Anchored) + entConsole.AtmosDevices.RemoveWhere(x => x.NetEntity == netEntity); + } + } + + #endregion + + public override void Update(float frameTime) + { + base.Update(frameTime); + + _updateTimer += frameTime; + + if (_updateTimer >= UpdateTime) + { + _updateTimer -= UpdateTime; + + // Keep a list of UI entries for each gridUid, in case multiple consoles stand on the same grid + var airAlarmEntriesForEachGrid = new Dictionary(); + var fireAlarmEntriesForEachGrid = new Dictionary(); + + var query = AllEntityQuery(); + while (query.MoveNext(out var ent, out var entConsole, out var entXform)) + { + if (entXform?.GridUid == null) + continue; + + // Make a list of alarm state data for all the air and fire alarms on the grid + if (!airAlarmEntriesForEachGrid.TryGetValue(entXform.GridUid.Value, out var airAlarmEntries)) + { + airAlarmEntries = GetAlarmStateData(entXform.GridUid.Value, AtmosAlertsComputerGroup.AirAlarm).ToArray(); + airAlarmEntriesForEachGrid[entXform.GridUid.Value] = airAlarmEntries; + } + + if (!fireAlarmEntriesForEachGrid.TryGetValue(entXform.GridUid.Value, out var fireAlarmEntries)) + { + fireAlarmEntries = GetAlarmStateData(entXform.GridUid.Value, AtmosAlertsComputerGroup.FireAlarm).ToArray(); + fireAlarmEntriesForEachGrid[entXform.GridUid.Value] = fireAlarmEntries; + } + + // Determine the highest level of alert for the console (based on non-silenced alarms) + var highestAlert = AtmosAlarmType.Invalid; + + foreach (var entry in airAlarmEntries) + { + if (entry.AlarmState > highestAlert && !entConsole.SilencedDevices.Contains(entry.NetEntity)) + highestAlert = entry.AlarmState; + } + + foreach (var entry in fireAlarmEntries) + { + if (entry.AlarmState > highestAlert && !entConsole.SilencedDevices.Contains(entry.NetEntity)) + highestAlert = entry.AlarmState; + } + + // Update the appearance of the console based on the highest recorded level of alert + if (TryComp(ent, out var entAppearance)) + _appearance.SetData(ent, AtmosAlertsComputerVisuals.ComputerLayerScreen, (int) highestAlert, entAppearance); + + // If the console UI is open, send UI data to each subscribed session + UpdateUIState(ent, airAlarmEntries, fireAlarmEntries, entConsole, entXform); + } + } + } + + public void UpdateUIState + (EntityUid uid, + AtmosAlertsComputerEntry[] airAlarmStateData, + AtmosAlertsComputerEntry[] fireAlarmStateData, + AtmosAlertsComputerComponent component, + TransformComponent xform) + { + if (!_uiSystem.IsUiOpen(uid, AtmosAlertsComputerUiKey.Key)) + return; + + var gridUid = xform.GridUid!.Value; + + if (!HasComp(gridUid)) + return; + + // The grid must have a NavMapComponent to visualize the map in the UI + EnsureComp(gridUid); + + // Gathering remaining data to be send to the client + var focusAlarmData = GetFocusAlarmData(uid, GetEntity(component.FocusDevice), gridUid); + + // Set the UI state + _uiSystem.TrySetUiState(uid, AtmosAlertsComputerUiKey.Key, + new AtmosAlertsComputerBoundInterfaceState(airAlarmStateData, fireAlarmStateData, focusAlarmData)); + } + + private List GetAlarmStateData(EntityUid gridUid, AtmosAlertsComputerGroup group) + { + var alarmStateData = new List(); + + var queryAlarms = AllEntityQuery(); + while (queryAlarms.MoveNext(out var ent, out var entDevice, out var entAtmosAlarmable, out var entDeviceNetwork, out var entXform)) + { + if (entXform.GridUid != gridUid) + continue; + + if (!entXform.Anchored) + continue; + + if (entDevice.Group != group) + continue; + + // If emagged, change the alarm type to normal + var alarmState = (entAtmosAlarmable.LastAlarmState == AtmosAlarmType.Emagged) ? AtmosAlarmType.Normal : entAtmosAlarmable.LastAlarmState; + + // Unpowered alarms can't sound + if (TryComp(ent, out var entAPCPower) && !entAPCPower.Powered) + alarmState = AtmosAlarmType.Invalid; + + var entry = new AtmosAlertsComputerEntry + (GetNetEntity(ent), + GetNetCoordinates(entXform.Coordinates), + entDevice.Group, + alarmState, + MetaData(ent).EntityName, + entDeviceNetwork.Address); + + alarmStateData.Add(entry); + } + + return alarmStateData; + } + + private AtmosAlertsFocusDeviceData? GetFocusAlarmData(EntityUid uid, EntityUid? focusDevice, EntityUid gridUid) + { + if (focusDevice == null) + return null; + + var focusDeviceXform = Transform(focusDevice.Value); + + if (!focusDeviceXform.Anchored || + focusDeviceXform.GridUid != gridUid || + !TryComp(focusDevice.Value, out var focusDeviceAirAlarm)) + { + return null; + } + + // Force update the sensors attached to the alarm + if (!_uiSystem.IsUiOpen(focusDevice.Value, SharedAirAlarmInterfaceKey.Key)) + { + _atmosDevNet.Register(focusDevice.Value, null); + _atmosDevNet.Sync(focusDevice.Value, null); + + foreach ((var address, var _) in focusDeviceAirAlarm.SensorData) + _atmosDevNet.Register(uid, null); + } + + // Get the sensor data + var temperatureData = (_airAlarmSystem.CalculateTemperatureAverage(focusDeviceAirAlarm), AtmosAlarmType.Normal); + var pressureData = (_airAlarmSystem.CalculatePressureAverage(focusDeviceAirAlarm), AtmosAlarmType.Normal); + var gasData = new Dictionary(); + + foreach ((var address, var sensorData) in focusDeviceAirAlarm.SensorData) + { + if (sensorData.TemperatureThreshold.CheckThreshold(sensorData.Temperature, out var temperatureState) && + (int) temperatureState > (int) temperatureData.Item2) + { + temperatureData = (temperatureData.Item1, temperatureState); + } + + if (sensorData.PressureThreshold.CheckThreshold(sensorData.Pressure, out var pressureState) && + (int) pressureState > (int) pressureData.Item2) + { + pressureData = (pressureData.Item1, pressureState); + } + + if (focusDeviceAirAlarm.SensorData.Sum(g => g.Value.TotalMoles) > 1e-8) + { + foreach ((var gas, var threshold) in sensorData.GasThresholds) + { + if (!gasData.ContainsKey(gas)) + { + float mol = _airAlarmSystem.CalculateGasMolarConcentrationAverage(focusDeviceAirAlarm, gas, out var percentage); + + if (mol < 1e-8) + continue; + + gasData[gas] = (mol, percentage, AtmosAlarmType.Normal); + } + + if (threshold.CheckThreshold(gasData[gas].Item2, out var gasState) && + (int) gasState > (int) gasData[gas].Item3) + { + gasData[gas] = (gasData[gas].Item1, gasData[gas].Item2, gasState); + } + } + } + } + + return new AtmosAlertsFocusDeviceData(GetNetEntity(focusDevice.Value), temperatureData, pressureData, gasData); + } + + private HashSet GetAllAtmosDeviceNavMapData(EntityUid gridUid) + { + var atmosDeviceNavMapData = new HashSet(); + + var query = AllEntityQuery(); + while (query.MoveNext(out var ent, out var entComponent, out var entXform)) + { + if (TryGetAtmosDeviceNavMapData(ent, entComponent, entXform, gridUid, out var data)) + atmosDeviceNavMapData.Add(data.Value); + } + + return atmosDeviceNavMapData; + } + + private bool TryGetAtmosDeviceNavMapData + (EntityUid uid, + AtmosAlertsDeviceComponent component, + TransformComponent xform, + EntityUid gridUid, + [NotNullWhen(true)] out AtmosAlertsDeviceNavMapData? output) + { + output = null; + + if (xform.GridUid != gridUid) + return false; + + if (!xform.Anchored) + return false; + + output = new AtmosAlertsDeviceNavMapData(GetNetEntity(uid), GetNetCoordinates(xform.Coordinates), component.Group); + + return true; + } + + private void InitalizeConsole(EntityUid uid, AtmosAlertsComputerComponent component) + { + var xform = Transform(uid); + + if (xform.GridUid == null) + return; + + var grid = xform.GridUid.Value; + component.AtmosDevices = GetAllAtmosDeviceNavMapData(grid); + + Dirty(uid, component); + } +} diff --git a/Content.Server/Atmos/Monitor/Systems/AirAlarmSystem.cs b/Content.Server/Atmos/Monitor/Systems/AirAlarmSystem.cs index 2922d0796a9..240f21ad42e 100644 --- a/Content.Server/Atmos/Monitor/Systems/AirAlarmSystem.cs +++ b/Content.Server/Atmos/Monitor/Systems/AirAlarmSystem.cs @@ -593,6 +593,21 @@ public float CalculateTemperatureAverage(AirAlarmComponent alarm) : 0f; } + public float CalculateGasMolarConcentrationAverage(AirAlarmComponent alarm, Gas gas, out float percentage) + { + percentage = 0f; + + var data = alarm.SensorData.Values.SelectMany(v => v.Gases.Where(g => g.Key == gas)); + + if (data.Count() == 0) + return 0f; + + var averageMol = data.Select(kvp => kvp.Value).Average(); + percentage = data.Select(kvp => kvp.Value).Sum() / alarm.SensorData.Values.Select(v => v.TotalMoles).Sum(); + + return averageMol; + } + public void UpdateUI(EntityUid uid, AirAlarmComponent? alarm = null, DeviceNetworkComponent? devNet = null, AtmosAlarmableComponent? alarmable = null) { if (!Resolve(uid, ref alarm, ref devNet, ref alarmable)) diff --git a/Content.Shared/Atmos/Consoles/Components/AtmosAlertsComputerComponent.cs b/Content.Shared/Atmos/Consoles/Components/AtmosAlertsComputerComponent.cs new file mode 100644 index 00000000000..d64c8907afb --- /dev/null +++ b/Content.Shared/Atmos/Consoles/Components/AtmosAlertsComputerComponent.cs @@ -0,0 +1,235 @@ +using Content.Shared.Atmos.Consoles; +using Content.Shared.Atmos.Monitor; +using Robust.Shared.GameStates; +using Robust.Shared.Map; +using Robust.Shared.Serialization; + +namespace Content.Shared.Atmos.Components; + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +[Access(typeof(SharedAtmosAlertsComputerSystem))] +public sealed partial class AtmosAlertsComputerComponent : Component +{ + /// + /// The current entity of interest (selected via the console UI) + /// + [ViewVariables] + public NetEntity? FocusDevice; + + /// + /// A list of all the atmos devices that will be used to populate the nav map + /// + [ViewVariables, AutoNetworkedField] + public HashSet AtmosDevices = new(); + + /// + /// A list of all the air alarms that have had their alerts silenced on this particular console + /// + [ViewVariables, AutoNetworkedField] + public HashSet SilencedDevices = new(); +} + +[Serializable, NetSerializable] +public struct AtmosAlertsDeviceNavMapData +{ + /// + /// The entity in question + /// + public NetEntity NetEntity; + + /// + /// Location of the entity + /// + public NetCoordinates NetCoordinates; + + /// + /// Used to determine what map icons to use + /// + public AtmosAlertsComputerGroup Group; + + /// + /// Populate the atmos monitoring console nav map with a single entity + /// + public AtmosAlertsDeviceNavMapData(NetEntity netEntity, NetCoordinates netCoordinates, AtmosAlertsComputerGroup group) + { + NetEntity = netEntity; + NetCoordinates = netCoordinates; + Group = group; + } +} + +[Serializable, NetSerializable] +public struct AtmosAlertsFocusDeviceData +{ + /// + /// Focus entity + /// + public NetEntity NetEntity; + + /// + /// Temperature (K) and related alert state + /// + public (float, AtmosAlarmType) TemperatureData; + + /// + /// Pressure (kPA) and related alert state + /// + public (float, AtmosAlarmType) PressureData; + + /// + /// Moles, percentage, and related alert state, for all detected gases + /// + public Dictionary GasData; + + /// + /// Populates the atmos monitoring console focus entry with atmospheric data + /// + public AtmosAlertsFocusDeviceData + (NetEntity netEntity, + (float, AtmosAlarmType) temperatureData, + (float, AtmosAlarmType) pressureData, + Dictionary gasData) + { + NetEntity = netEntity; + TemperatureData = temperatureData; + PressureData = pressureData; + GasData = gasData; + } +} + +[Serializable, NetSerializable] +public sealed class AtmosAlertsComputerBoundInterfaceState : BoundUserInterfaceState +{ + /// + /// A list of all air alarms + /// + public AtmosAlertsComputerEntry[] AirAlarms; + + /// + /// A list of all fire alarms + /// + public AtmosAlertsComputerEntry[] FireAlarms; + + /// + /// Data for the UI focus (if applicable) + /// + public AtmosAlertsFocusDeviceData? FocusData; + + /// + /// Sends data from the server to the client to populate the atmos monitoring console UI + /// + public AtmosAlertsComputerBoundInterfaceState(AtmosAlertsComputerEntry[] airAlarms, AtmosAlertsComputerEntry[] fireAlarms, AtmosAlertsFocusDeviceData? focusData) + { + AirAlarms = airAlarms; + FireAlarms = fireAlarms; + FocusData = focusData; + } +} + +[Serializable, NetSerializable] +public struct AtmosAlertsComputerEntry +{ + /// + /// The entity in question + /// + public NetEntity NetEntity; + + /// + /// Location of the entity + /// + public NetCoordinates Coordinates; + + /// + /// The type of entity + /// + public AtmosAlertsComputerGroup Group; + + /// + /// Current alarm state + /// + public AtmosAlarmType AlarmState; + + /// + /// Localised device name + /// + public string EntityName; + + /// + /// Device network address + /// + public string Address; + + /// + /// Used to populate the atmos monitoring console UI with data from a single air alarm + /// + public AtmosAlertsComputerEntry + (NetEntity entity, + NetCoordinates coordinates, + AtmosAlertsComputerGroup group, + AtmosAlarmType alarmState, + string entityName, + string address) + { + NetEntity = entity; + Coordinates = coordinates; + Group = group; + AlarmState = alarmState; + EntityName = entityName; + Address = address; + } +} + +[Serializable, NetSerializable] +public sealed class AtmosAlertsComputerFocusChangeMessage : BoundUserInterfaceMessage +{ + public NetEntity? FocusDevice; + + /// + /// Used to inform the server that the specified focus for the atmos monitoring console has been changed by the client + /// + public AtmosAlertsComputerFocusChangeMessage(NetEntity? focusDevice) + { + FocusDevice = focusDevice; + } +} + +[Serializable, NetSerializable] +public sealed class AtmosAlertsComputerDeviceSilencedMessage : BoundUserInterfaceMessage +{ + public NetEntity AtmosDevice; + public bool SilenceDevice = true; + + /// + /// Used to inform the server that the client has silenced alerts from the specified device to this atmos monitoring console + /// + public AtmosAlertsComputerDeviceSilencedMessage(NetEntity atmosDevice, bool silenceDevice = true) + { + AtmosDevice = atmosDevice; + SilenceDevice = silenceDevice; + } +} + +/// +/// List of all the different atmos device groups +/// +public enum AtmosAlertsComputerGroup +{ + Invalid, + AirAlarm, + FireAlarm, +} + +[NetSerializable, Serializable] +public enum AtmosAlertsComputerVisuals +{ + ComputerLayerScreen, +} + +/// +/// UI key associated with the atmos monitoring console +/// +[Serializable, NetSerializable] +public enum AtmosAlertsComputerUiKey +{ + Key +} diff --git a/Content.Shared/Atmos/Consoles/Components/AtmosAlertsDeviceComponent.cs b/Content.Shared/Atmos/Consoles/Components/AtmosAlertsDeviceComponent.cs new file mode 100644 index 00000000000..881d60b084c --- /dev/null +++ b/Content.Shared/Atmos/Consoles/Components/AtmosAlertsDeviceComponent.cs @@ -0,0 +1,14 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Atmos.Components; + +[RegisterComponent, NetworkedComponent] +[Access([])] +public sealed partial class AtmosAlertsDeviceComponent : Component +{ + /// + /// The group that the entity belongs to + /// + [DataField, ViewVariables] + public AtmosAlertsComputerGroup Group; +} diff --git a/Content.Shared/Atmos/Consoles/SharedAtmosAlertsComputerSystem.cs b/Content.Shared/Atmos/Consoles/SharedAtmosAlertsComputerSystem.cs new file mode 100644 index 00000000000..7e2b2b04670 --- /dev/null +++ b/Content.Shared/Atmos/Consoles/SharedAtmosAlertsComputerSystem.cs @@ -0,0 +1,24 @@ +using Content.Shared.Atmos.Components; + +namespace Content.Shared.Atmos.Consoles; + +public abstract partial class SharedAtmosAlertsComputerSystem : EntitySystem +{ + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnDeviceSilencedMessage); + } + + private void OnDeviceSilencedMessage(EntityUid uid, AtmosAlertsComputerComponent component, AtmosAlertsComputerDeviceSilencedMessage args) + { + if (args.SilenceDevice) + component.SilencedDevices.Add(args.AtmosDevice); + + else + component.SilencedDevices.Remove(args.AtmosDevice); + + Dirty(uid, component); + } +} diff --git a/Resources/Locale/en-US/atmos/atmos-alerts-console.ftl b/Resources/Locale/en-US/atmos/atmos-alerts-console.ftl new file mode 100644 index 00000000000..a1640c5e9d5 --- /dev/null +++ b/Resources/Locale/en-US/atmos/atmos-alerts-console.ftl @@ -0,0 +1,35 @@ +atmos-alerts-window-title = Atmospheric Alerts Computer +atmos-alerts-window-station-name = [color=white][font size=14]{$stationName}[/font][/color] +atmos-alerts-window-unknown-location = Unknown location + +atmos-alerts-window-tab-no-alerts = Alerts +atmos-alerts-window-tab-alerts = Alerts ({$value}) +atmos-alerts-window-tab-air-alarms = Air alarms +atmos-alerts-window-tab-fire-alarms = Fire alarms + +atmos-alerts-window-alarm-label = {CAPITALIZE($name)} ({$address}) +atmos-alerts-window-temperature-label = Temperature +atmos-alerts-window-temperature-value = {$valueInC} °C ({$valueInK} K) +atmos-alerts-window-pressure-label = Pressure +atmos-alerts-window-pressure-value = {$value} kPa +atmos-alerts-window-oxygenation-label = Oxygenation +atmos-alerts-window-oxygenation-value = {$value}% +atmos-alerts-window-other-gases-label = Other present gases +atmos-alerts-window-other-gases-value = {$shorthand} ({$value}%) +atmos-alerts-window-other-gases-value-nil = None +atmos-alerts-window-silence-alerts = Silence alerts from this alarm + +atmos-alerts-window-label-alert-types = Alert levels: +atmos-alerts-window-normal-state = Normal +atmos-alerts-window-warning-state = Warning +atmos-alerts-window-danger-state = Danger! +atmos-alerts-window-invalid-state = Inactive + +atmos-alerts-window-no-active-alerts = [font size=16][color=white]No active alerts -[/color] [color={$color}]situation normal[/color][/font] +atmos-alerts-window-no-data-available = No data available +atmos-alerts-window-alerts-being-silenced = Silencing alerts... + +atmos-alerts-window-toggle-overlays = Toggle alarm display + +atmos-alerts-window-flavor-left = Contact an atmospheric technician for assistance +atmos-alerts-window-flavor-right = v1.8 \ No newline at end of file diff --git a/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/computer.yml b/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/computer.yml index 1210f302fcc..35a941e759c 100644 --- a/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/computer.yml +++ b/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/computer.yml @@ -21,8 +21,8 @@ - type: entity parent: BaseComputerCircuitboard id: AlertsComputerCircuitboard - name: alerts computer board - description: A computer printed circuit board for an alerts computer. + name: atmospheric alerts computer board + description: A computer printed circuit board for an atmospheric alerts computer. components: - type: ComputerBoard prototype: ComputerAlert diff --git a/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml b/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml index 4fd2129f7a5..39fd34016b9 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml @@ -1,8 +1,8 @@ - type: entity parent: BaseComputer id: ComputerAlert - name: alerts computer - description: Used to access the station's automated alert system. + name: atmospheric alerts computer + description: Used to access the station's automated atmospheric alert system. components: - type: Computer board: AlertsComputerCircuitboard @@ -13,9 +13,33 @@ - map: ["computerLayerKeyboard"] state: generic_keyboard - map: ["computerLayerScreen"] - state: alert-2 + state: alert-0 - map: ["computerLayerKeys"] state: atmos_key + - type: GenericVisualizer + visuals: + enum.ComputerVisuals.Powered: + computerLayerScreen: + True: { visible: true, shader: unshaded } + False: { visible: false } + computerLayerKeys: + True: { visible: true, shader: unshaded } + False: { visible: true, shader: shaded } + enum.AtmosAlertsComputerVisuals.ComputerLayerScreen: + computerLayerScreen: + 0: { state: alert-0 } + 1: { state: alert-0 } + 2: { state: alert-1 } + 3: { state: alert-2 } + 4: { state: alert-2 } + - type: AtmosAlertsComputer + - type: ActivatableUI + singleUser: true + key: enum.AtmosAlertsComputerUiKey.Key + - type: UserInterface + interfaces: + - key: enum.AtmosAlertsComputerUiKey.Key + type: AtmosAlertsComputerBoundUserInterface - type: entity parent: BaseComputer diff --git a/Resources/Prototypes/Entities/Structures/Wallmounts/air_alarm.yml b/Resources/Prototypes/Entities/Structures/Wallmounts/air_alarm.yml index 62f2000593b..70e204f694e 100644 --- a/Resources/Prototypes/Entities/Structures/Wallmounts/air_alarm.yml +++ b/Resources/Prototypes/Entities/Structures/Wallmounts/air_alarm.yml @@ -53,6 +53,8 @@ - AirAlarm - type: AtmosDevice - type: AirAlarm + - type: AtmosAlertsDevice + group: AirAlarm - type: Clickable - type: InteractionOutline - type: UserInterface diff --git a/Resources/Prototypes/Entities/Structures/Wallmounts/fire_alarm.yml b/Resources/Prototypes/Entities/Structures/Wallmounts/fire_alarm.yml index 05988fbc217..c39adcde3b6 100644 --- a/Resources/Prototypes/Entities/Structures/Wallmounts/fire_alarm.yml +++ b/Resources/Prototypes/Entities/Structures/Wallmounts/fire_alarm.yml @@ -42,6 +42,8 @@ - type: Clickable - type: InteractionOutline - type: FireAlarm + - type: AtmosAlertsDevice + group: FireAlarm - type: ContainerFill containers: board: [ FireAlarmElectronics ] diff --git a/Resources/Textures/Interface/AtmosMonitoring/status_bg.png b/Resources/Textures/Interface/AtmosMonitoring/status_bg.png new file mode 100644 index 0000000000000000000000000000000000000000..165a9b9d9f1e32fa1975aae8ed6a8702f55fd351 GIT binary patch literal 157 zcmeAS@N?(olHy`uVBq!ia0vp^6+kS=3&n&wN;ytu2~|XS;+%o}s~=q=X$u4-|AaKK#bq x&$~BPj6p1c5rWus4lqFwPecQ2M8nrq{@1g0*lq-EC@_ Date: Fri, 20 Sep 2024 17:46:47 -0400 Subject: [PATCH 42/55] Basic Language Menu (#936) # Description This is a very _early_ take on a Language menu, making use of the new CharacterItemGroups functionality, as well as functionality added to the trait system that allows it to add and remove languages directly. This PR adds in the languages Tau-Cetic Basic(taking the place of Galactic Common), Tradeband, Freespeak, Elyran Standard, and Sol Common(I'm replacing the extremely basic bastardized Sol Common we had before with the fully realized version of it from Bay12).

Media

![image](https://github.com/user-attachments/assets/68d054b6-975b-42db-b039-d517e337ace3)

# Changelog :cl: - add: A basic Languages menu has been added into the Traits tab. - add: Tau-Ceti Basic, Tradeband, Freespeak, Elyran Standard, Sol Common, and Sign Language have been added to the Languages tab. --- .../Assorted/ForeignerTraitComponent.cs | 2 +- .../Language/Systems/SharedLanguageSystem.cs | 2 +- Resources/Locale/en-US/language/commands.ftl | 10 +- Resources/Locale/en-US/language/languages.ftl | 38 +- .../Locale/en-US/loadouts/itemgroups.ftl | 4 + Resources/Locale/en-US/traits/categories.ftl | 5 +- Resources/Locale/en-US/traits/traits.ftl | 23 +- .../CharacterItemGroups/languageGroups.yml | 31 + .../DeltaV/Entities/Mobs/NPCs/animals.yml | 2 +- .../DeltaV/Entities/Mobs/NPCs/familiars.yml | 4 +- .../DeltaV/Entities/Mobs/NPCs/nukiemouse.yml | 2 +- .../Entities/Mobs/Species/vulpkanin.yml | 4 +- .../Prototypes/DeltaV/Traits/neutral.yml | 5 +- .../Mobs/Cyborgs/base_borg_chassis.yml | 4 +- .../Prototypes/Entities/Mobs/NPCs/animals.yml | 10 +- .../Prototypes/Entities/Mobs/NPCs/dogs.yml | 2 +- .../Prototypes/Entities/Mobs/NPCs/pets.yml | 20 +- .../Entities/Mobs/NPCs/regalrat.yml | 6 +- .../Prototypes/Entities/Mobs/NPCs/silicon.yml | 4 +- .../Prototypes/Entities/Mobs/NPCs/xeno.yml | 4 +- .../Prototypes/Entities/Mobs/Player/ipc.yml | 4 +- .../Prototypes/Entities/Mobs/Species/base.yml | 4 +- .../Entities/Mobs/Species/diona.yml | 4 +- .../Entities/Mobs/Species/dwarf.yml | 4 +- .../Entities/Mobs/Species/harpy.yml | 8 +- .../Entities/Mobs/Species/human.yml | 4 +- .../Prototypes/Entities/Mobs/Species/moth.yml | 4 +- .../Entities/Mobs/Species/reptilian.yml | 4 +- .../Entities/Mobs/Species/slime.yml | 4 +- .../Objects/Devices/translator_implants.yml | 24 +- .../Entities/Objects/Devices/translators.yml | 50 +- .../Objects/Misc/translator_implanters.yml | 4 +- .../Computers/base_structurecomputers.yml | 6 +- .../Structures/Machines/vending_machines.yml | 4 +- .../Entities/Structures/Specific/oracle.yml | 6 +- .../Language/Species-Specific/diona.yml | 17 + .../Language/Species-Specific/moth.yml | 69 ++ .../Language/Species-Specific/nekomimetic.yml | 60 ++ .../Language/Species-Specific/reptilian.yml | 95 +++ .../Language/Species-Specific/slimeperson.yml | 17 + .../Language/Species-Specific/vulpkanin.yml | 68 ++ .../Prototypes/Language/Standard/elyran.yml | 86 +++ .../Language/Standard/freespeak.yml | 259 ++++++++ .../Language/Standard/solcommon.yml | 258 ++++++++ .../Language/Standard/taucetibasic.yml | 256 ++++++++ .../Language/Standard/tradeband.yml | 258 ++++++++ Resources/Prototypes/Language/animal.yml | 186 ++++++ .../Prototypes/Language/genericlanguages.yml | 45 ++ Resources/Prototypes/Language/languages.yml | 614 ------------------ .../Nyanotrasen/Entities/Mobs/Species/Oni.yml | 4 +- .../Entities/Mobs/Species/felinid.yml | 6 +- .../Structures/Research/sophicscribe.yml | 6 +- Resources/Prototypes/Traits/categories.yml | 17 + Resources/Prototypes/Traits/disabilities.yml | 6 +- .../Prototypes/Traits/inconveniences.yml | 8 +- Resources/Prototypes/Traits/languages.yml | 67 ++ Resources/Prototypes/Traits/neutral.yml | 16 +- Resources/Prototypes/Traits/skills.yml | 9 - 58 files changed, 1987 insertions(+), 756 deletions(-) create mode 100644 Resources/Prototypes/CharacterItemGroups/languageGroups.yml create mode 100644 Resources/Prototypes/Language/Species-Specific/diona.yml create mode 100644 Resources/Prototypes/Language/Species-Specific/moth.yml create mode 100644 Resources/Prototypes/Language/Species-Specific/nekomimetic.yml create mode 100644 Resources/Prototypes/Language/Species-Specific/reptilian.yml create mode 100644 Resources/Prototypes/Language/Species-Specific/slimeperson.yml create mode 100644 Resources/Prototypes/Language/Species-Specific/vulpkanin.yml create mode 100644 Resources/Prototypes/Language/Standard/elyran.yml create mode 100644 Resources/Prototypes/Language/Standard/freespeak.yml create mode 100644 Resources/Prototypes/Language/Standard/solcommon.yml create mode 100644 Resources/Prototypes/Language/Standard/taucetibasic.yml create mode 100644 Resources/Prototypes/Language/Standard/tradeband.yml create mode 100644 Resources/Prototypes/Language/animal.yml create mode 100644 Resources/Prototypes/Language/genericlanguages.yml delete mode 100644 Resources/Prototypes/Language/languages.yml create mode 100644 Resources/Prototypes/Traits/languages.yml diff --git a/Content.Server/Traits/Assorted/ForeignerTraitComponent.cs b/Content.Server/Traits/Assorted/ForeignerTraitComponent.cs index 756f44e7429..a35972f1c86 100644 --- a/Content.Server/Traits/Assorted/ForeignerTraitComponent.cs +++ b/Content.Server/Traits/Assorted/ForeignerTraitComponent.cs @@ -13,7 +13,7 @@ public sealed partial class ForeignerTraitComponent : Component { /// /// The "base" language that is to be removed and substituted with a translator. - /// By default, equals to the fallback language, which is GalacticCommon. + /// By default, equals to the fallback language, which is TauCetiBasic. /// [DataField] public ProtoId BaseLanguage = SharedLanguageSystem.FallbackLanguagePrototype; diff --git a/Content.Shared/Language/Systems/SharedLanguageSystem.cs b/Content.Shared/Language/Systems/SharedLanguageSystem.cs index 0a03086ebe1..d9e882147c0 100644 --- a/Content.Shared/Language/Systems/SharedLanguageSystem.cs +++ b/Content.Shared/Language/Systems/SharedLanguageSystem.cs @@ -10,7 +10,7 @@ public abstract class SharedLanguageSystem : EntitySystem /// The language used as a fallback in cases where an entity suddenly becomes a language speaker (e.g. the usage of make-sentient) ///
[ValidatePrototypeId] - public static readonly string FallbackLanguagePrototype = "GalacticCommon"; + public static readonly string FallbackLanguagePrototype = "TauCetiBasic"; /// /// The language whose speakers are assumed to understand and speak every language. Should never be added directly. diff --git a/Resources/Locale/en-US/language/commands.ftl b/Resources/Locale/en-US/language/commands.ftl index 65959e3f28f..ccbe2570c96 100644 --- a/Resources/Locale/en-US/language/commands.ftl +++ b/Resources/Locale/en-US/language/commands.ftl @@ -2,10 +2,10 @@ command-list-langs-desc = List languages your current entity can speak at the cu command-list-langs-help = Usage: {$command} command-saylang-desc = Send a message in a specific language. To choose a language, you can use either the name of the language, or its position in the list of languages. -command-saylang-help = Usage: {$command} . Example: {$command} GalacticCommon "Hello World!". Example: {$command} 1 "Hello World!" +command-saylang-help = Usage: {$command} . Example: {$command} TauCetiBasic "Hello World!". Example: {$command} 1 "Hello World!" command-language-select-desc = Select the currently spoken language of your entity. You can use either the name of the language, or its position in the list of languages. -command-language-select-help = Usage: {$command} . Example: {$command} 1. Example: {$command} GalacticCommon +command-language-select-help = Usage: {$command} . Example: {$command} 1. Example: {$command} TauCetiBasic command-language-spoken = Spoken: command-language-understood = Understood: @@ -18,14 +18,14 @@ command-language-invalid-language = The language {$id} does not exist or you can # toolshed command-description-language-add = Adds a new language to the piped entity. The two last arguments indicate whether it should be spoken/understood. Example: 'self language:add "Canilunzt" true true' -command-description-language-rm = Removes a language from the piped entity. Works similarly to language:add. Example: 'self language:rm "GalacticCommon" true true'. +command-description-language-rm = Removes a language from the piped entity. Works similarly to language:add. Example: 'self language:rm "TauCetiBasic" true true'. command-description-language-lsspoken = Lists all languages the entity can speak. Example: 'self language:lsspoken' command-description-language-lsunderstood = Lists all languages the entity can understand. Example: 'self language:lssunderstood' command-description-translator-addlang = Adds a new target language to the piped translator entity. See language:add for details. command-description-translator-rmlang = Removes a target language from the piped translator entity. See language:rm for details. -command-description-translator-addrequired = Adds a new required language to the piped translator entity. Example: 'ent 1234 translator:addrequired "GalacticCommon"' -command-description-translator-rmrequired = Removes a required language from the piped translator entity. Example: 'ent 1234 translator:rmrequired "GalacticCommon"' +command-description-translator-addrequired = Adds a new required language to the piped translator entity. Example: 'ent 1234 translator:addrequired "TauCetiBasic"' +command-description-translator-rmrequired = Removes a required language from the piped translator entity. Example: 'ent 1234 translator:rmrequired "TauCetiBasic"' command-description-translator-lsspoken = Lists all spoken languages for the piped translator entity. Example: 'ent 1234 translator:lsspoken' command-description-translator-lsunderstood = Lists all understood languages for the piped translator entity. Example: 'ent 1234 translator:lssunderstood' command-description-translator-lsrequired = Lists all required languages for the piped translator entity. Example: 'ent 1234 translator:lsrequired' diff --git a/Resources/Locale/en-US/language/languages.ftl b/Resources/Locale/en-US/language/languages.ftl index 68dc80f51da..7ad66d1aabe 100644 --- a/Resources/Locale/en-US/language/languages.ftl +++ b/Resources/Locale/en-US/language/languages.ftl @@ -1,9 +1,6 @@ language-Universal-name = Universal language-Universal-description = What are you? -language-GalacticCommon-name = Galactic common -language-GalacticCommon-description = The standard Galatic language, most commonly used for inter-species communications and legal work. - language-Bubblish-name = Bubblish language-Bubblish-description = The language of Slimes. Being a mixture of bubbling noises and pops it's very difficult to speak for humans without the use of mechanical aids. @@ -17,10 +14,37 @@ language-Draconic-name = Draconic language-Draconic-description = The common language of lizard-people, composed of sibilant hisses and rattles. language-SolCommon-name = Sol common -language-SolCommon-description = The language common to species from the Sol System. +language-SolCommon-description = + With its roots in Mandarin Chinese - Common evolved as the official language of the Sol Alliance - with officials working to tie it together with a common tongue. + It's spoken by state officials - taught in schools - and spoken by those who either feel a sense of national pride in the Alliance or otherwise fell sway to the culture. + +language-TauCetiBasic-name = Tau-Ceti Basic +language-TauCetiBasic-description = + A spiritual successor of Esperanto, established in 2404 in Tau Ceti by Ceti intellectuals. + Its unique, fully customized alphabet and structure allow it to be spoken even by most alien species. + It's the official language of Tau Ceti and has growing traction in diplomatic circles and Universalists across human space. + +language-Tradeband-name = Tradeband +language-Tradeband-description = + Descended from latin and romance languages of old Earth - Tradeband remains the main tongue of the upper class of humanity. + The language sounds elegant and well structured to most ears. It remains in popular use with traders - diplomats - and those seeking to hold onto a piece of a romantic past. + +language-Freespeak-name = Freespeak +language-Freespeak-description = + A language of renegades and frontiersmen descending from various languages from Earth-- like Hindi, + combined into a multi-rooted jumble that sounds incoherent or even barbarian to non-native speakers. + This language is the only common cultural identity for humans in the frontier. Speaking this language in itself boldly declares the speaker a free spirit. + It is often called 'Gutter' by Alliance citizens. + +language-Elyran-name = Elyran Standard +language-Elyran-description = + Elyran Standard is the official tongue of the Republic of Elyra. Constructed using elements of Farsi - Arabic - and Turkish. + Influence from all three of these languages can be seen throughout its grammar and vocabulary. language-Canilunzt-name = Canilunzt -language-Canilunzt-description = The guttural language spoken and utilized by the inhabitants of the Vazzend system, composed of growls, barks, yaps, and heavy utilization of ears and tail movements. Vulpkanin speak this language with ease. +language-Canilunzt-description = + The guttural language spoken and utilized by the inhabitants of the Vazzend system, + composed of growls, barks, yaps, and heavy utilization of ears and tail movements. Vulpkanin speak this language with ease. language-Moffic-name = Moffic language-Moffic-description = The language of the mothpeople borders on complete unintelligibility. @@ -28,8 +52,8 @@ language-Moffic-description = The language of the mothpeople borders on complete language-RobotTalk-name = RobotTalk language-RobotTalk-description = A language consisting of harsh binary chirps, whistles, hisses, and whines. Organic tongues cannot speak it without aid from special translators. -language-Sign-name = Galactic Sign Language -language-Sign-description = GSL for short, this sign language is prevalent among mute and deaf people. +language-Sign-name = Tau-Ceti Basic Sign Language +language-Sign-description = TCB-SL for short, this sign language is prevalent among mute and deaf people. language-Cat-name = Cat language-Cat-description = Meow diff --git a/Resources/Locale/en-US/loadouts/itemgroups.ftl b/Resources/Locale/en-US/loadouts/itemgroups.ftl index 29aca3ddb9b..97fcfa69848 100644 --- a/Resources/Locale/en-US/loadouts/itemgroups.ftl +++ b/Resources/Locale/en-US/loadouts/itemgroups.ftl @@ -64,3 +64,7 @@ character-item-group-LoadoutBartenderWeapon = Bartender Weapon # Service - Musician character-item-group-LoadoutMusicianInstruments = Musician Instruments + +# Traits - Languages +character-item-group-TraitsLanguagesBasic = Basic Languages +character-item-group-TraitsAccents = Accents \ No newline at end of file diff --git a/Resources/Locale/en-US/traits/categories.ftl b/Resources/Locale/en-US/traits/categories.ftl index 56f0adeb479..2bd4b7ba49f 100644 --- a/Resources/Locale/en-US/traits/categories.ftl +++ b/Resources/Locale/en-US/traits/categories.ftl @@ -5,4 +5,7 @@ trait-category-Auditory = Auditory trait-category-Mental = Mental trait-category-Physical = Physical trait-category-Speech = Speech -trait-category-Visual = Visual +trait-category-TraitsSpeechUncategorized = Uncategorized +trait-category-TraitsSpeechAccents = Accents +trait-category-TraitsSpeechLanguages = Languages +trait-category-Visual = Visual \ No newline at end of file diff --git a/Resources/Locale/en-US/traits/traits.ftl b/Resources/Locale/en-US/traits/traits.ftl index b034f69b9e8..4db6055e46b 100644 --- a/Resources/Locale/en-US/traits/traits.ftl +++ b/Resources/Locale/en-US/traits/traits.ftl @@ -152,9 +152,30 @@ trait-description-Lethargy = trait-name-SignLanguage = Sign Language trait-description-SignLanguage = - You can understand and use Galactic Sign Language (GSL). + You can understand and use Tau-Ceti Basic Sign Language (TCB-SL). If you are mute for any reason, you can still communicate with sign language. +trait-name-SolCommon = Sol Common +trait-description-SolCommon = + With its roots in Mandarin Chinese - Common evolved as the official language of the Sol Alliance - with officials working to tie it together with a common tongue. + It's spoken by state officials - taught in schools - and spoken by those who either feel a sense of national pride in the Alliance or otherwise fell sway to the culture. + +trait-name-Tradeband = Tradeband +trait-description-Tradeband = + Descended from latin and romance languages of old Earth - Tradeband remains the main tongue of the upper class of humanity. + The language sounds elegant and well structured to most ears. It remains in popular use with traders - diplomats - and those seeking to hold onto a piece of a romantic past. + +trait-name-Freespeak = Freespeak (Gutter) +trait-description-Freespeak = + A language of renegades and frontiersmen descending from various languages from Earth like Hindi combined into a multi-rooted jumble that sounds incoherent or even barbarian to non-native speakers. + This language is the only common cultural identity for humans in the frontier. Speaking this language in itself boldly declares the speaker a free spirit. + Often called 'Gutter' by Alliance citizens. + +trait-name-Elyran = Elyran Standard +trait-description-Elyran = + Elyran Standard is the official tongue of the Republic of Elyra. + Constructed using elements of Farsi - Arabic - and Turkish - influence from all three of these languages can be seen throughout its grammar and vocabulary. + trait-name-Voracious = Voracious trait-description-Voracious = Nothing gets between you and your food. diff --git a/Resources/Prototypes/CharacterItemGroups/languageGroups.yml b/Resources/Prototypes/CharacterItemGroups/languageGroups.yml new file mode 100644 index 00000000000..244792e4c67 --- /dev/null +++ b/Resources/Prototypes/CharacterItemGroups/languageGroups.yml @@ -0,0 +1,31 @@ +- type: characterItemGroup + id: TraitsLanguagesBasic + maxItems: 1 + items: + - type: trait + id: SignLanguage + - type: trait + id: SolCommon + - type: trait + id: Tradeband + - type: trait + id: Freespeak + - type: trait + id: Elyran + +- type: characterItemGroup + id: TraitsAccents + maxItems: 1 + items: + - type: trait + id: FrontalLisp + - type: trait + id: Stutter + - type: trait + id: PirateAccent + - type: trait + id: Accentless + - type: trait + id: Southern + - type: trait + id: ScottishAccent \ No newline at end of file diff --git a/Resources/Prototypes/DeltaV/Entities/Mobs/NPCs/animals.yml b/Resources/Prototypes/DeltaV/Entities/Mobs/NPCs/animals.yml index dd59d74d3f0..de0f022e1f7 100644 --- a/Resources/Prototypes/DeltaV/Entities/Mobs/NPCs/animals.yml +++ b/Resources/Prototypes/DeltaV/Entities/Mobs/NPCs/animals.yml @@ -184,4 +184,4 @@ - Dog understands: - Dog - - GalacticCommon + - TauCetiBasic diff --git a/Resources/Prototypes/DeltaV/Entities/Mobs/NPCs/familiars.yml b/Resources/Prototypes/DeltaV/Entities/Mobs/NPCs/familiars.yml index 364907ff9c4..4d3d139e8e0 100644 --- a/Resources/Prototypes/DeltaV/Entities/Mobs/NPCs/familiars.yml +++ b/Resources/Prototypes/DeltaV/Entities/Mobs/NPCs/familiars.yml @@ -111,9 +111,9 @@ - NanoTrasen - type: LanguageKnowledge speaks: - - GalacticCommon + - TauCetiBasic understands: - - GalacticCommon + - TauCetiBasic - type: GhostTakeoverAvailable - type: GhostRole makeSentient: true diff --git a/Resources/Prototypes/DeltaV/Entities/Mobs/NPCs/nukiemouse.yml b/Resources/Prototypes/DeltaV/Entities/Mobs/NPCs/nukiemouse.yml index 8968f0e77ad..59c7fe2b212 100644 --- a/Resources/Prototypes/DeltaV/Entities/Mobs/NPCs/nukiemouse.yml +++ b/Resources/Prototypes/DeltaV/Entities/Mobs/NPCs/nukiemouse.yml @@ -101,7 +101,7 @@ - Mouse understands: - Mouse - - GalacticCommon + - TauCetiBasic - type: Tag tags: - VimPilot diff --git a/Resources/Prototypes/DeltaV/Entities/Mobs/Species/vulpkanin.yml b/Resources/Prototypes/DeltaV/Entities/Mobs/Species/vulpkanin.yml index 07152d74ef0..b3027839b1a 100644 --- a/Resources/Prototypes/DeltaV/Entities/Mobs/Species/vulpkanin.yml +++ b/Resources/Prototypes/DeltaV/Entities/Mobs/Species/vulpkanin.yml @@ -106,10 +106,10 @@ - type: Wagging - type: LanguageKnowledge speaks: - - GalacticCommon + - TauCetiBasic - Canilunzt understands: - - GalacticCommon + - TauCetiBasic - Canilunzt - type: ConsumeDelayModifier foodDelayMultiplier: 0.5 diff --git a/Resources/Prototypes/DeltaV/Traits/neutral.yml b/Resources/Prototypes/DeltaV/Traits/neutral.yml index 6168d7045a9..469fa5b34b7 100644 --- a/Resources/Prototypes/DeltaV/Traits/neutral.yml +++ b/Resources/Prototypes/DeltaV/Traits/neutral.yml @@ -1,6 +1,9 @@ - type: trait id: ScottishAccent - category: Speech + category: TraitsSpeechAccents points: 0 + requirements: + - !type:CharacterItemGroupRequirement + group: TraitsAccents components: - type: ScottishAccent diff --git a/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml b/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml index 6340cb888b6..81bd0332a58 100644 --- a/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml +++ b/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml @@ -218,10 +218,10 @@ - Normal - type: LanguageKnowledge speaks: - - GalacticCommon + - TauCetiBasic - RobotTalk understands: - - GalacticCommon + - TauCetiBasic - RobotTalk - type: PsionicInsulation diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml index 01eed043139..343a39ca70f 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml @@ -1370,7 +1370,7 @@ understands: - Monkey - Kobold - - GalacticCommon + - TauCetiBasic - type: NpcFactionMember factions: - Syndicate @@ -2002,9 +2002,9 @@ bloodMaxVolume: 50 - type: LanguageKnowledge speaks: - - GalacticCommon + - TauCetiBasic understands: - - GalacticCommon + - TauCetiBasic - type: entity name: penguin @@ -2903,7 +2903,7 @@ - Cat understands: - Cat - - GalacticCommon + - TauCetiBasic - type: entity name: space cat @@ -3450,7 +3450,7 @@ speaks: - RootSpeak understands: - - GalacticCommon + - TauCetiBasic - RootSpeak - type: entity diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/dogs.yml b/Resources/Prototypes/Entities/Mobs/NPCs/dogs.yml index 22da7583723..2bcd3776260 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/dogs.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/dogs.yml @@ -89,7 +89,7 @@ - Dog understands: - Dog - - GalacticCommon + - TauCetiBasic - type: entity parent: MobPibble diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/pets.yml b/Resources/Prototypes/Entities/Mobs/NPCs/pets.yml index 6eb43fb89ae..4ec4ee6a12f 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/pets.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/pets.yml @@ -40,7 +40,7 @@ speaks: - Dog understands: - - GalacticCommon + - TauCetiBasic - Dog - type: entity @@ -131,7 +131,7 @@ speaks: - Cat understands: - - GalacticCommon + - TauCetiBasic - Cat - type: entity @@ -155,7 +155,7 @@ speaks: - Cat understands: - - GalacticCommon + - TauCetiBasic - Cat - type: entity @@ -310,7 +310,7 @@ speaks: - Dog understands: - - GalacticCommon + - TauCetiBasic - Dog - type: InteractionPopup successChance: 0.5 @@ -415,7 +415,7 @@ speaks: - Dog understands: - - GalacticCommon + - TauCetiBasic - Dog - type: InteractionPopup successChance: 0.7 @@ -580,7 +580,7 @@ speaks: - Fox understands: - - GalacticCommon + - TauCetiBasic - Fox - type: entity @@ -633,7 +633,7 @@ speaks: - Mouse understands: - - GalacticCommon + - TauCetiBasic - Mouse - type: entity @@ -811,7 +811,7 @@ speaks: - Bubblish understands: - - GalacticCommon + - TauCetiBasic - Bubblish - type: entity @@ -851,7 +851,7 @@ speaks: - Monkey understands: - - GalacticCommon + - TauCetiBasic - Monkey - Kobold @@ -885,5 +885,5 @@ speaks: - Crab understands: - - GalacticCommon + - TauCetiBasic - Crab diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/regalrat.yml b/Resources/Prototypes/Entities/Mobs/NPCs/regalrat.yml index cf563989bf5..e1ea11cc2c3 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/regalrat.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/regalrat.yml @@ -120,10 +120,10 @@ gender: male - type: LanguageKnowledge speaks: - - GalacticCommon + - TauCetiBasic - Mouse understands: - - GalacticCommon + - TauCetiBasic - Mouse - type: entity @@ -301,7 +301,7 @@ speaks: - Mouse understands: - - GalacticCommon + - TauCetiBasic - Mouse - type: FireVisuals sprite: Mobs/Effects/onfire.rsi diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml b/Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml index 90d9a5e5c97..0e279a07eb6 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml @@ -108,10 +108,10 @@ - type: ZombieImmune - type: LanguageKnowledge speaks: - - GalacticCommon + - TauCetiBasic - RobotTalk understands: - - GalacticCommon + - TauCetiBasic - RobotTalk - type: PsionicInsulation diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml b/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml index d618e407134..fc0f879b1ce 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml @@ -239,10 +239,10 @@ - CannotSuicide - type: LanguageKnowledge speaks: - - GalacticCommon + - TauCetiBasic - Xeno understands: - - GalacticCommon + - TauCetiBasic - Xeno - type: entity diff --git a/Resources/Prototypes/Entities/Mobs/Player/ipc.yml b/Resources/Prototypes/Entities/Mobs/Player/ipc.yml index 247226dc7d5..0e1e1a1ae7d 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/ipc.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/ipc.yml @@ -101,10 +101,10 @@ - type: CanHostGuardian - type: LanguageKnowledge speaks: - - GalacticCommon + - TauCetiBasic - RobotTalk understands: - - GalacticCommon + - TauCetiBasic - RobotTalk - type: WeldingHealable - type: PsionicInsulation diff --git a/Resources/Prototypes/Entities/Mobs/Species/base.yml b/Resources/Prototypes/Entities/Mobs/Species/base.yml index 33635eeec20..7420f168ca6 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/base.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/base.yml @@ -218,9 +218,9 @@ - type: CanEscapeInventory # Carrying system from nyanotrasen. - type: LanguageKnowledge # This is here so even if species doesn't have a defined language, they at least speak GC speaks: - - GalacticCommon + - TauCetiBasic understands: - - GalacticCommon + - TauCetiBasic - type: Tag tags: - CanPilot diff --git a/Resources/Prototypes/Entities/Mobs/Species/diona.yml b/Resources/Prototypes/Entities/Mobs/Species/diona.yml index 81c9e596163..9a26d0ec909 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/diona.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/diona.yml @@ -104,10 +104,10 @@ - Dead - type: LanguageKnowledge speaks: - - GalacticCommon + - TauCetiBasic - RootSpeak understands: - - GalacticCommon + - TauCetiBasic - RootSpeak - type: TraitSpeedModifier sprintModifier: 0.75 diff --git a/Resources/Prototypes/Entities/Mobs/Species/dwarf.yml b/Resources/Prototypes/Entities/Mobs/Species/dwarf.yml index 055c6522ddc..4b75e65ca91 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/dwarf.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/dwarf.yml @@ -53,10 +53,10 @@ speechSounds: Bass - type: LanguageKnowledge speaks: - - GalacticCommon + - TauCetiBasic - SolCommon understands: - - GalacticCommon + - TauCetiBasic - SolCommon - type: LightweightDrunk boozeStrengthMultiplier: 0.5 diff --git a/Resources/Prototypes/Entities/Mobs/Species/harpy.yml b/Resources/Prototypes/Entities/Mobs/Species/harpy.yml index 8882da868b1..8d657d3532f 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/harpy.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/harpy.yml @@ -117,11 +117,11 @@ - ShoesRequiredStepTriggerImmune - type: LanguageKnowledge speaks: - - GalacticCommon - - SolCommon + - TauCetiBasic + - Tradeband understands: - - GalacticCommon - - SolCommon + - TauCetiBasic + - Tradeband - type: StepTriggerImmune whitelist: types: diff --git a/Resources/Prototypes/Entities/Mobs/Species/human.yml b/Resources/Prototypes/Entities/Mobs/Species/human.yml index ac373725ce4..121cbd3ca78 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/human.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/human.yml @@ -18,10 +18,10 @@ amount: 5 - type: LanguageKnowledge speaks: - - GalacticCommon + - TauCetiBasic - SolCommon understands: - - GalacticCommon + - TauCetiBasic - SolCommon - type: entity diff --git a/Resources/Prototypes/Entities/Mobs/Species/moth.yml b/Resources/Prototypes/Entities/Mobs/Species/moth.yml index 1c55dcf0df1..7e5716a212f 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/moth.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/moth.yml @@ -25,10 +25,10 @@ speechVerb: Moth - type: LanguageKnowledge speaks: - - GalacticCommon + - TauCetiBasic - Moffic understands: - - GalacticCommon + - TauCetiBasic - Moffic - type: TypingIndicator proto: moth diff --git a/Resources/Prototypes/Entities/Mobs/Species/reptilian.yml b/Resources/Prototypes/Entities/Mobs/Species/reptilian.yml index 35f9e9fa393..3dfb5cb3267 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/reptilian.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/reptilian.yml @@ -61,10 +61,10 @@ - type: Wagging - type: LanguageKnowledge speaks: - - GalacticCommon + - TauCetiBasic - Draconic understands: - - GalacticCommon + - TauCetiBasic - Draconic - type: entity diff --git a/Resources/Prototypes/Entities/Mobs/Species/slime.yml b/Resources/Prototypes/Entities/Mobs/Species/slime.yml index 0e05b0c8276..2982cdb5b27 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/slime.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/slime.yml @@ -76,10 +76,10 @@ maxSaturation: 15 - type: LanguageKnowledge speaks: - - GalacticCommon + - TauCetiBasic - Bubblish understands: - - GalacticCommon + - TauCetiBasic - Bubblish - type: entity diff --git a/Resources/Prototypes/Entities/Objects/Devices/translator_implants.yml b/Resources/Prototypes/Entities/Objects/Devices/translator_implants.yml index da42b2774b1..d430f5ea5dc 100644 --- a/Resources/Prototypes/Entities/Objects/Devices/translator_implants.yml +++ b/Resources/Prototypes/Entities/Objects/Devices/translator_implants.yml @@ -1,26 +1,26 @@ - type: entity parent: BaseSubdermalImplant - id: BasicGalacticCommonTranslatorImplant + id: BasicTauCetiBasicTranslatorImplant name: basic common translator implant description: Provides your illiterate friends the ability to understand the common galactic tongue. noSpawn: true components: - type: TranslatorImplant understood: - - GalacticCommon + - TauCetiBasic - type: entity parent: BaseSubdermalImplant - id: GalacticCommonTranslatorImplant + id: TauCetiBasicTranslatorImplant name: advanced common translator implant description: A more advanced version of the translator implant, teaches your illiterate friends the ability to both speak and understand the galactic tongue! noSpawn: true components: - type: TranslatorImplant understood: - - GalacticCommon + - TauCetiBasic spoken: - - GalacticCommon + - TauCetiBasic - type: entity parent: BaseSubdermalImplant @@ -35,7 +35,7 @@ spoken: - Bubblish requires: - - GalacticCommon + - TauCetiBasic - type: entity parent: BaseSubdermalImplant @@ -50,7 +50,7 @@ spoken: - Nekomimetic requires: - - GalacticCommon + - TauCetiBasic - type: entity parent: BaseSubdermalImplant @@ -65,7 +65,7 @@ spoken: - Draconic requires: - - GalacticCommon + - TauCetiBasic - type: entity parent: BaseSubdermalImplant @@ -80,7 +80,7 @@ spoken: - Canilunzt requires: - - GalacticCommon + - TauCetiBasic - type: entity parent: BaseSubdermalImplant @@ -95,7 +95,7 @@ spoken: - SolCommon requires: - - GalacticCommon + - TauCetiBasic - type: entity parent: BaseSubdermalImplant @@ -110,7 +110,7 @@ spoken: - RootSpeak requires: - - GalacticCommon + - TauCetiBasic - type: entity parent: BaseSubdermalImplant @@ -125,4 +125,4 @@ spoken: - Moffic requires: - - GalacticCommon + - TauCetiBasic diff --git a/Resources/Prototypes/Entities/Objects/Devices/translators.yml b/Resources/Prototypes/Entities/Objects/Devices/translators.yml index 6aa7947c82d..4a14e371565 100644 --- a/Resources/Prototypes/Entities/Objects/Devices/translators.yml +++ b/Resources/Prototypes/Entities/Objects/Devices/translators.yml @@ -67,13 +67,13 @@ components: - type: HandheldTranslator spoken: - - GalacticCommon + - TauCetiBasic - Canilunzt understood: - - GalacticCommon + - TauCetiBasic - Canilunzt requires: - - GalacticCommon + - TauCetiBasic - Canilunzt - type: entity @@ -84,13 +84,13 @@ components: - type: HandheldTranslator spoken: - - GalacticCommon + - TauCetiBasic - Bubblish understood: - - GalacticCommon + - TauCetiBasic - Bubblish requires: - - GalacticCommon + - TauCetiBasic - Bubblish - type: entity @@ -101,13 +101,13 @@ components: - type: HandheldTranslator spoken: - - GalacticCommon + - TauCetiBasic - Nekomimetic understood: - - GalacticCommon + - TauCetiBasic - Nekomimetic requires: - - GalacticCommon + - TauCetiBasic - Nekomimetic - type: entity @@ -118,13 +118,13 @@ components: - type: HandheldTranslator spoken: - - GalacticCommon + - TauCetiBasic - Draconic understood: - - GalacticCommon + - TauCetiBasic - Draconic requires: - - GalacticCommon + - TauCetiBasic - Draconic - type: entity @@ -135,13 +135,13 @@ components: - type: HandheldTranslator spoken: - - GalacticCommon + - TauCetiBasic - SolCommon understood: - - GalacticCommon + - TauCetiBasic - SolCommon requires: - - GalacticCommon + - TauCetiBasic - SolCommon - type: entity @@ -152,13 +152,13 @@ components: - type: HandheldTranslator spoken: - - GalacticCommon + - TauCetiBasic - RootSpeak understood: - - GalacticCommon + - TauCetiBasic - RootSpeak requires: - - GalacticCommon + - TauCetiBasic - RootSpeak - type: entity @@ -169,13 +169,13 @@ components: - type: HandheldTranslator spoken: - - GalacticCommon + - TauCetiBasic - Moffic understood: - - GalacticCommon + - TauCetiBasic - Moffic requires: - - GalacticCommon + - TauCetiBasic - Moffic - type: entity @@ -186,13 +186,13 @@ components: - type: HandheldTranslator spoken: - - GalacticCommon + - TauCetiBasic - Xeno understood: - - GalacticCommon + - TauCetiBasic - Xeno requires: - - GalacticCommon + - TauCetiBasic - type: entity id: AnimalTranslator @@ -217,5 +217,5 @@ - Kobold - Hissing requires: - - GalacticCommon + - TauCetiBasic setLanguageOnInteract: false diff --git a/Resources/Prototypes/Entities/Objects/Misc/translator_implanters.yml b/Resources/Prototypes/Entities/Objects/Misc/translator_implanters.yml index 8b5b262ff8a..92e21e51404 100644 --- a/Resources/Prototypes/Entities/Objects/Misc/translator_implanters.yml +++ b/Resources/Prototypes/Entities/Objects/Misc/translator_implanters.yml @@ -10,7 +10,7 @@ name: basic common translator implanter components: - type: Implanter - implant: BasicGalacticCommonTranslatorImplant + implant: BasicTauCetiBasicTranslatorImplant - type: entity id: AdvancedGalaticCommonTranslatorImplanter @@ -18,7 +18,7 @@ name: advanced common translator implanter components: - type: Implanter - implant: GalacticCommonTranslatorImplant + implant: TauCetiBasicTranslatorImplant - type: entity id: BubblishTranslatorImplanter diff --git a/Resources/Prototypes/Entities/Structures/Machines/Computers/base_structurecomputers.yml b/Resources/Prototypes/Entities/Structures/Machines/Computers/base_structurecomputers.yml index b7ea7c6f6ca..9468ecd4523 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/Computers/base_structurecomputers.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/Computers/base_structurecomputers.yml @@ -62,11 +62,11 @@ priority: 1 - type: RequireProjectileTarget - type: LanguageSpeaker - currentLanguage: GalacticCommon + currentLanguage: TauCetiBasic - type: LanguageKnowledge speaks: - - GalacticCommon + - TauCetiBasic - RobotTalk understands: - - GalacticCommon + - TauCetiBasic - RobotTalk diff --git a/Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml b/Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml index 02cdd80af35..f3f68660b2d 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml @@ -105,10 +105,10 @@ - type: WiresVisuals - type: LanguageKnowledge speaks: - - GalacticCommon + - TauCetiBasic - RobotTalk understands: - - GalacticCommon + - TauCetiBasic - RobotTalk - type: VendingMachine soundVend: /Audio/SimpleStation14/Machines/machine_vend.ogg diff --git a/Resources/Prototypes/Entities/Structures/Specific/oracle.yml b/Resources/Prototypes/Entities/Structures/Specific/oracle.yml index 2161c6d80da..1e3b9a5b7e8 100644 --- a/Resources/Prototypes/Entities/Structures/Specific/oracle.yml +++ b/Resources/Prototypes/Entities/Structures/Specific/oracle.yml @@ -68,12 +68,12 @@ components: - Item - type: LanguageSpeaker - currentLanguage: GalacticCommon + currentLanguage: TauCetiBasic - type: LanguageKnowledge speaks: - - GalacticCommon + - TauCetiBasic understands: - - GalacticCommon + - TauCetiBasic - type: weightedRandomEntity diff --git a/Resources/Prototypes/Language/Species-Specific/diona.yml b/Resources/Prototypes/Language/Species-Specific/diona.yml new file mode 100644 index 00000000000..76b62163ff3 --- /dev/null +++ b/Resources/Prototypes/Language/Species-Specific/diona.yml @@ -0,0 +1,17 @@ +# Spoken by dionas. +# TODO: Replace this with a much better language. +- type: language + id: RootSpeak + speech: + color: "#ce5e14dd" + fontId: Noganas + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 5 + replacement: + - hs + - zt + - kr + - st + - sh \ No newline at end of file diff --git a/Resources/Prototypes/Language/Species-Specific/moth.yml b/Resources/Prototypes/Language/Species-Specific/moth.yml new file mode 100644 index 00000000000..ed0a6009c59 --- /dev/null +++ b/Resources/Prototypes/Language/Species-Specific/moth.yml @@ -0,0 +1,69 @@ +# Spoken by moths. +# TODO: Replace this with a much better language. +- type: language + id: Moffic + speech: + color: "#c7df2edd" + fontId: Copperplate + obfuscation: + !type:SyllableObfuscation + minSyllables: 2 # Replacements are really short + maxSyllables: 4 + replacement: + - år + - i + - går + - sek + - mo + - ff + - ok + - gj + - ø + - gå + - la + - le + - lit + - ygg + - van + - dår + - næ + - møt + - idd + - hvo + - ja + - på + - han + - så + - ån + - det + - att + - nå + - gö + - bra + - int + - tyc + - om + - när + - två + - må + - dag + - sjä + - vii + - vuo + - eil + - tun + - käyt + - teh + - vä + - hei + - huo + - suo + - ää + - ten + - ja + - heu + - stu + - uhr + - kön + - we + - hön \ No newline at end of file diff --git a/Resources/Prototypes/Language/Species-Specific/nekomimetic.yml b/Resources/Prototypes/Language/Species-Specific/nekomimetic.yml new file mode 100644 index 00000000000..3610e5fa50e --- /dev/null +++ b/Resources/Prototypes/Language/Species-Specific/nekomimetic.yml @@ -0,0 +1,60 @@ +# A mess of broken Japanese, spoken by Felinds and Oni +# TODO: Replace this with a much better language. +- type: language + id: Nekomimetic + speech: + color: "#df57aaee" + fontId: Manga + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 3 # May be too long even, we'll see. + replacement: + - neko + - nyan + - mimi + - moe + - mofu + - fuwa + - kyaa + - kawaii + - poka + - munya + - puni + - munyu + - ufufu + - icha + - doki + - kyun + - kusu + - nya + - nyaa + - desu + - kis + - ama + - chuu + - baka + - hewo + - boop + - gato + - kit + - sune + - yori + - sou + - baka + - chan + - san + - kun + - mahou + - yatta + - suki + - usagi + - domo + - ori + - uwa + - zaazaa + - shiku + - puru + - ira + - heto + - etto \ No newline at end of file diff --git a/Resources/Prototypes/Language/Species-Specific/reptilian.yml b/Resources/Prototypes/Language/Species-Specific/reptilian.yml new file mode 100644 index 00000000000..b22a62ee0e3 --- /dev/null +++ b/Resources/Prototypes/Language/Species-Specific/reptilian.yml @@ -0,0 +1,95 @@ +# Spoken by the Lizard race. +# TODO: Replace this with a much better language. +- type: language + id: Draconic + speech: + color: "#2aca2add" + obfuscation: + !type:SyllableObfuscation + minSyllables: 2 + maxSyllables: 4 + replacement: + - za + - az + - ze + - ez + - zi + - iz + - zo + - oz + - zu + - uz + - zs + - sz + - ha + - ah + - he + - eh + - hi + - ih + - ho + - oh + - hu + - uh + - hs + - sh + - la + - al + - le + - el + - li + - il + - lo + - ol + - lu + - ul + - ls + - sl + - ka + - ak + - ke + - ek + - ki + - ik + - ko + - ok + - ku + - uk + - ks + - sk + - sa + - as + - se + - es + - si + - is + - so + - os + - su + - us + - ss + - ss + - ra + - ar + - re + - er + - ri + - ir + - ro + - or + - ru + - ur + - rs + - sr + - a + - a + - e + - e + - i + - i + - o + - o + - u + - u + - s + - s \ No newline at end of file diff --git a/Resources/Prototypes/Language/Species-Specific/slimeperson.yml b/Resources/Prototypes/Language/Species-Specific/slimeperson.yml new file mode 100644 index 00000000000..a3c1f91a170 --- /dev/null +++ b/Resources/Prototypes/Language/Species-Specific/slimeperson.yml @@ -0,0 +1,17 @@ +# Spoken by slimes. +# TODO: Replace this with a much better language. +- type: language + id: Bubblish + speech: + color: "#00a3e2dd" + fontId: RubikBubbles + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 3 + replacement: + - blob + - plop + - pop + - bop + - boop \ No newline at end of file diff --git a/Resources/Prototypes/Language/Species-Specific/vulpkanin.yml b/Resources/Prototypes/Language/Species-Specific/vulpkanin.yml new file mode 100644 index 00000000000..17a48493a49 --- /dev/null +++ b/Resources/Prototypes/Language/Species-Specific/vulpkanin.yml @@ -0,0 +1,68 @@ +# Spoken by the Vulpkanin race. +# TODO: Replace this with a much better language. +- type: language + id: Canilunzt + speech: + color: "#d69b3dcc" + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 4 + replacement: + - rur + - ya + - cen + - rawr + - bar + - kuk + - tek + - qat + - uk + - wu + - vuh + - tah + - tch + - schz + - auch + - ist + - ein + - entch + - zwichs + - tut + - mir + - wo + - bis + - es + - vor + - nic + - gro + - enem + - zandt + - tzch + - noch + - hel + - ischt + - far + - wa + - baram + - iereng + - tech + - lach + - sam + - mak + - lich + - gen + - or + - ag + - eck + - gec + - stag + - onn + - bin + - ket + - jarl + - vulf + - einech + - cresthz + - azunein + - ghzth \ No newline at end of file diff --git a/Resources/Prototypes/Language/Standard/elyran.yml b/Resources/Prototypes/Language/Standard/elyran.yml new file mode 100644 index 00000000000..c2c90e7b6f0 --- /dev/null +++ b/Resources/Prototypes/Language/Standard/elyran.yml @@ -0,0 +1,86 @@ +- type: language + id: Elyran + speech: + color: "#8282fbaa" + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 4 + replacement: + - af + - if + - ba + - ta + - tha + - id + - jem + - ha + - kha + - dal + - dhl + - ra + - zay + - sen + - um + - shn + - sid + - ad + - ta + - za + - ayn + - gha + - zir + - yn + - fa + - qaf + - iam + - mim + - al + - ja + - non + - ha + - waw + - ya + - hem + - zah + - hml + - ks + - ini + - da + - ks + - iga + - ih + - la + - ulf + - xe + - ayw + - sit + - ah + - aarah + - jalaa + - sirt + - kurt + - turhk + - ust + - irk + - kir + - mir + - ach + - oglu + - bolu + - shek + - she + - ghoz + - miya + - ejdan + - haaz + - quq + - taab + - shanha + - an + - saa + - seh + - an' + - e' + - a' + - em' \ No newline at end of file diff --git a/Resources/Prototypes/Language/Standard/freespeak.yml b/Resources/Prototypes/Language/Standard/freespeak.yml new file mode 100644 index 00000000000..831c2a67ff8 --- /dev/null +++ b/Resources/Prototypes/Language/Standard/freespeak.yml @@ -0,0 +1,259 @@ +- type: language + id: Freespeak + speech: + color: "#597d35" + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 3 + replacement: + - a + - aan + - aas + - ab + - aba + - ad + - aee + - aft + - ag + - ai + - aise + - ak + - akee + - aq + - ar + - ata + - aur + - aus + - ba + - baat + - bach + - bad + - bahe + - band + - be + - ben + - ber + - bhaa + - bhu + - bra + - burt + - cap + - cer + - ch + - cha + - chaar + - chale + - chalo + - chil + - com + - da + - daa + - daaj + - dat + - de + - dee + - dhaa + - di + - die + - dik + - din + - diz + - do + - dos + - dosh + - durch + - eer + - ek + - er + - es + - fal + - fang + - fra + - fun + - ga + - gan + - gao + - gee + - geet + - gern + - gir + - gon + - gren + - gri + - gu + - guda + - ha + - haa + - hai + - hain + - har + - hat + - he + - hee + - heer + - hekt + - heu + - hit + - hn + - ho + - hua + - huk + - hul + - ich + - ig + - in + - isch + - ja + - jaa + - jad + - jan + - jao + - jar + - jas + - jee + - jiao + - jin + - jing + - un + - ka + - kaha + - kana + - kar + - kara + - karo + - ke + - kee + - keln + - kha + - khada + - khe + - khi + - ko + - koo + - ky + - la + - laa + - laat + - lad + - lada + - lana + - lane + - le + - lee + - leiden + - leis + - len + - lie + - lo + - maa + - maan + - mod + - most + - muj + - mujhe + - mukt + - na + - naya + - ne + - nee + - net + - neta + - nir + - nka + - oon + - oop + - pa + - paa + - pet + - phen + - phot + - pi + - plo + - pra + - que + - ra + - raa + - rahe + - raho + - ran + - rana + - rar + - re + - ri + - rie + - rin + - ro + - rona + - rosh + - rtiv + - saa + - saal + - saath + - san + - santu + - sch + - se + - sen + - sh + - sha + - shee + - shi + - shn + - sht + - shuo + - soch + - sol + - soo + - ssa + - ster + - suk + - sur + - ta + - taan + - tak + - taka + - tal + - tan + - tar + - ten + - tend + - th + - tho + - tili + - to + - ton + - tr + - tu + - tum + - tung + - udaa + - ugr + - unge + - ut + - va + - vaa + - vaad + - vaib + - ve + - ven + - ver + - vi + - vis + - vol + - wic + - wu + - wut + - xi + - xiao + - ya + - yah + - yon + - you + - zas + - ze + - zhu + - zi + - zo + - zorn + - zt diff --git a/Resources/Prototypes/Language/Standard/solcommon.yml b/Resources/Prototypes/Language/Standard/solcommon.yml new file mode 100644 index 00000000000..88c5470154b --- /dev/null +++ b/Resources/Prototypes/Language/Standard/solcommon.yml @@ -0,0 +1,258 @@ +- type: language + id: SolCommon + speech: + color: "#8282fbaa" + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 4 + replacement: + - a + - abe + - ade + - ai + - an + - ana + - ba + - bae + - bai + - bang + - bao + - bei + - ben + - beo + - bi + - bian + - bing + - bo + - bu + - bugu + - bun + - cai + - can + - cao + - cau + - chan + - chen + - cheong + - chiu + - chong + - chyo + - da + - dan + - dao + - de + - deun + - duo + - eon + - eun + - eusi + - feng + - fu + - ga + - gak + - gan + - gang + - gao + - ge + - gei + - gen + - geo + - gil + - go + - gou + - gu + - gua + - gui + - gul + - gun + - guo + - gwi + - ha + - hai + - hal + - han + - hap + - hara + - he + - hego + - hen + - hon + - hoo + - hu + - hua + - hun + - hyeong + - i + - jae + - jeo + - jeon + - ji + - jia + - jian + - jiang + - jie + - jong + - ju + - jue + - juede + - jung + - juzi + - ka + - kang + - kawa + - ke + - keun + - ki + - kin + - ko + - kore + - kou + - ku + - kuda + - kun + - kyu + - lang + - lao + - leng + - leung + - li + - lian + - liang + - lie + - ling + - lizi + - lleo + - long + - lu + - ma + - mah + - me + - mei + - meinu + - men + - meng + - meog + - meoni + - mi + - mian + - min + - mo + - mot + - mu + - mun + - na + - nae + - nai + - nari + - ne + - ni + - nii + - nim + - nin + - nop + - nu + - o + - oba + - oga + - oji + - oka + - ong + - op + - oto + - pa + - pai + - pang + - pin + - ping + - pong + - pu + - pum + - pye + - qi + - qie + - qing + - ra + - rei + - ren + - ri + - ru + - ruan + - sa + - sai + - sama + - san + - sang + - se + - sei + - sen + - seo + - seon + - seong + - shang + - shen + - sheng + - shi + - sho + - shui + - si + - su + - sui + - sum + - sun + - swi + - ta + - tae + - tai + - tame + - tamen + - tan + - te + - tei + - ti + - tian + - to + - ton + - tsu + - ul + - wa + - wan + - wang + - wei + - wo + - xi + - xian + - xiao + - xing + - xiong + - xiu + - xu + - xuan + - xue + - ya + - yan + - yang + - yeong + - yi + - yige + - yin + - ying + - yiqi + - yong + - you + - yu + - yuli + - yuyi + - zai + - zao + - zhan + - zhang + - zhe + - zhen + - zheng + - zhuo + - zi + - zo + - zu + - zun + - zuo \ No newline at end of file diff --git a/Resources/Prototypes/Language/Standard/taucetibasic.yml b/Resources/Prototypes/Language/Standard/taucetibasic.yml new file mode 100644 index 00000000000..96426f6c24f --- /dev/null +++ b/Resources/Prototypes/Language/Standard/taucetibasic.yml @@ -0,0 +1,256 @@ +# The "Default Language" other than Universal. +- type: language + id: TauCetiBasic + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 3 + replacement: + - a + - ado + - ago + - aj + - ajn + - al + - alt + - am + - amas + - an + - ang + - ante + - ap + - ard + - arma + - aro + - as + - aur + - aut + - aw + - ba + - bal + - bao + - be + - beau + - bel + - bi + - bit + - blu + - bo + - bod + - boj + - bojn + - bu + - but + - ca + - caj + - ce + - cer + - chun + - ci + - cion + - coj + - cor + - da + - daj + - dan + - de + - den + - dis + - do + - dor + - dorm + - eco + - ego + - ek + - eks + - en + - ero + - es + - est + - et + - eve + - fa + - fe + - fel + - fla + - foj + - fra + - fraz + - fros + - ful + - fut + - ga + - gan + - gar + - gi + - gis + - go + - gran + - ha + - han + - hav + - hom + - hong + - hu + - hum + - hushi + - ia + - iaj + - ica + - id + - idon + - il + - in + - ing + - io + - is + - iton + - iza + - ja + - ji + - jirou + - joj + - ka + - kaj + - kajo + - kan + - ke + - ket + - ki + - kna + - krio + - ku + - kui + - kuk + - kun + - kur + - la + - laca + - leng + - les + - li + - liao + - lib + - ling + - lis + - lo + - lon + - long + - lu + - lud + - ma + - mal + - man + - me + - mego + - mero + - mi + - mia + - min + - mo + - moj + - mola + - mon + - mul + - ne + - ner + - ni + - nio + - nu + - of + - oj + - om + - ou + - pe + - pi + - plan + - pli + - po + - por + - post + - pre + - prin + - pru + - pu + - pur + - qiu + - que + - ra + - ras + - re + - ri + - rig + - ril + - ro + - roj + - ron + - roso + - rou + - ru + - sa + - san + - sci + - sek + - shi + - shiia + - shiue + - shiwu + - shu + - shui + - si + - siaj + - sku + - so + - som + - sti + - str + - stre + - su + - suno + - ta + - tan + - tas + - te + - tel + - tem + - the + - ti + - tian + - tita + - tiu + - to + - toj + - ton + - tran + - tre + - tri + - trin + - tro + - trus + - un + - undo + - uno + - uz + - va + - var + - varm + - vas + - ve + - vek + - ven + - ves + - vi + - via + - vin + - vino + - vint + - vir + - von + - vu + - whe + - wu + - yong + - zem + - zo + - zoj + - zon \ No newline at end of file diff --git a/Resources/Prototypes/Language/Standard/tradeband.yml b/Resources/Prototypes/Language/Standard/tradeband.yml new file mode 100644 index 00000000000..96415dc5a6e --- /dev/null +++ b/Resources/Prototypes/Language/Standard/tradeband.yml @@ -0,0 +1,258 @@ +- type: language + id: Tradeband + speech: + color: "#597d35" + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 3 + replacement: + - a + - acc + - ai + - al + - ali + - am + - ama + - ami + - amo + - an + - ang + - arme + - ave + - ba + - bai + - bar + - bat + - bi + - blie + - bris + - ca + - can + - cant + - car + - care + - ce + - ci + - cis + - cit + - cla + - co + - cul + - cur + - curt + - da + - dam + - dans + - de + - di + - dier + - dim + - dins + - dorm + - du + - duro + - e + - eaux + - ec + - ecto + - ees + - ego + - el + - en + - ent + - er + - ere + - eres + - eri + - ero + - es + - et + - ex + - far + - fi + - fic + - fine + - fol + - foll + - fri + - fro + - gen + - gil + - go + - gran + - hab + - ho + - huc + - ia + - iam + - ibus + - idor + - ie + - iens + - ier + - ieur + - iis + - il + - in + - ine + - int + - ir + - is + - ise + - it + - itt + - jar + - je + - jo + - jor + - la + - lar + - lav + - le + - lees + - ler + - les + - li + - lib + - lie + - lo + - lu + - ma + - man + - manu + - mar + - mari + - mas + - me + - mea + - mee + - mejo + - men + - mes + - meum + - meus + - mi + - mier + - min + - mine + - mit + - mo + - moi + - mon + - mons + - mors + - mou + - mul + - na + - nam + - ne + - nee + - nent + - nes + - ni + - nit + - nom + - nu + - num + - o + - oc + - occ + - oja + - om + - omni + - or + - ori + - oro + - os + - ou + - oub + - pa + - par + - pars + - pas + - plu + - pluv + - po + - pol + - pos + - pou + - pous + - pre + - pu + - pug + - pus + - que + - qui + - re + - ri + - ric + - riga + - rito + - ro + - rom + - sa + - sal + - se + - ser + - sers + - ses + - sim + - sion + - so + - sol + - som + - sou + - sper + - sse + - ste + - su + - suis + - sul + - sur + - ta + - tar + - te + - teau + - tem + - temp + - ten + - tene + - tes + - ti + - tibus + - tien + - tion + - to + - tol + - ton + - tons + - tout + - tra + - trai + - tre + - trou + - tuo + - tus + - tut + - ues + - ui + - ul + - um + - un + - upa + - us + - ut + - ux + - va + - vail + - ve + - ven + - veni + - vi + - viam + - vie + - vo + - xus + - za + - zio diff --git a/Resources/Prototypes/Language/animal.yml b/Resources/Prototypes/Language/animal.yml new file mode 100644 index 00000000000..46178200011 --- /dev/null +++ b/Resources/Prototypes/Language/animal.yml @@ -0,0 +1,186 @@ +# Languages spoken by various critters. +- type: language + id: Cat + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 2 + replacement: + - murr + - meow + - purr + - mrow + +- type: language + id: Dog + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 2 + replacement: + - woof + - bark + - ruff + - bork + - raff + - garr + +- type: language + id: Fox + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 2 + replacement: + - ruff + - raff + - garr + - yip + - yap + - myah + +- type: language + id: Xeno + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 8 # I was crazy once + replacement: + - s + - S + +- type: language + id: Monkey + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 8 # They locked me in a room... + replacement: + - o + - k + +- type: language + id: Mouse + obfuscation: + !type:SyllableObfuscation + minSyllables: 2 + maxSyllables: 3 + replacement: + - squ + - eak + - pi + - ep + - chuu + - ee + - fwi + - he + +- type: language + id: Chicken + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 3 + replacement: + - co + - coo + - ot + +- type: language + id: Duck + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 3 + replacement: + - qu + - ack + - quack + +- type: language + id: Cow + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 3 + replacement: + - moo + - mooo + +- type: language + id: Sheep + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 3 + replacement: + - ba + - baa + - aa + +- type: language + id: Kangaroo + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 3 + replacement: + - shre + - ack + - chuu + - choo + +- type: language + id: Pig + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 3 + replacement: + - oink # Please someone come up with something better + +- type: language + id: Crab + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 3 + replacement: + - click + - clack + - ti + - pi + - tap + - cli + - ick + +- type: language + id: Kobold + obfuscation: + !type:SyllableObfuscation + minSyllables: 2 + maxSyllables: 4 + replacement: + - yip + - yap + - gar + - grr + - ar + - scre + - et + - gronk + - hiss + - ss + - ee + +- type: language + id: Hissing + obfuscation: + !type:SyllableObfuscation + minSyllables: 2 + maxSyllables: 4 + replacement: + - hss + - iss + - ss + - is diff --git a/Resources/Prototypes/Language/genericlanguages.yml b/Resources/Prototypes/Language/genericlanguages.yml new file mode 100644 index 00000000000..8d172cdae9c --- /dev/null +++ b/Resources/Prototypes/Language/genericlanguages.yml @@ -0,0 +1,45 @@ +# The universal language. This is technically used as a fallback for simulating the pre-languages +# style of Chat, and is not normally accessible by players. It is however used by characters +# with the Xenoglossy psionic power, as well as Ghosts, and AGhosts. +- type: language + id: Universal + obfuscation: + !type:ReplacementObfuscation + replacement: + - "*incomprehensible*" # Never actually used + +# Used by Robots. +# TODO: Replace this with much better languages. Yes, robots can have languages. +- type: language + id: RobotTalk + speech: + fontId: Monospace + obfuscation: + !type:SyllableObfuscation + minSyllables: 1 + maxSyllables: 10 # Crazy + replacement: + - 0 + - 1 + +# Example of a sign language. Used by the Sign Language trait. +- type: language + id: Sign + speech: + allowRadio: false + requireSpeech: false + color: "#dddddd" + messageWrapOverrides: + Speak: chat-sign-language-message-wrap + Whisper: chat-sign-language-whisper-wrap + speechVerbOverrides: + - chat-speech-verb-sign-1 + - chat-speech-verb-sign-2 + - chat-speech-verb-sign-3 + obfuscation: + !type:ReplacementObfuscation + replacement: + - something + - a cryptic message + - a signal + - a message diff --git a/Resources/Prototypes/Language/languages.yml b/Resources/Prototypes/Language/languages.yml deleted file mode 100644 index 65af93e02d4..00000000000 --- a/Resources/Prototypes/Language/languages.yml +++ /dev/null @@ -1,614 +0,0 @@ -# The universal language, assumed if the entity has a UniversalLanguageSpeakerComponent. -# Do not use otherwise. Making an entity explicitly understand/speak this language will NOT have the desired effect. -- type: language - id: Universal - obfuscation: - !type:ReplacementObfuscation - replacement: - - "*incomprehensible*" # Never actually used - -# The common galactic tongue. -- type: language - id: GalacticCommon - obfuscation: - !type:SyllableObfuscation - minSyllables: 1 - maxSyllables: 3 - replacement: - - blah - - blah - - blah - - dingle-doingle - - dingle - - dangle - - jibber-jabber - - jubber - - bleh - - zippity - - zoop - - wibble - - wobble - - wiggle - - yada - - meh - - neh - - nah - - wah - -# Spoken by slimes. -- type: language - id: Bubblish - speech: - color: "#00a3e2dd" - fontId: RubikBubbles - obfuscation: - !type:SyllableObfuscation - minSyllables: 1 - maxSyllables: 3 - replacement: - - blob - - plop - - pop - - bop - - boop - -# Spoken by moths. -- type: language - id: Moffic - speech: - color: "#c7df2edd" - fontId: Copperplate - obfuscation: - !type:SyllableObfuscation - minSyllables: 2 # Replacements are really short - maxSyllables: 4 - replacement: - - år - - i - - går - - sek - - mo - - ff - - ok - - gj - - ø - - gå - - la - - le - - lit - - ygg - - van - - dår - - næ - - møt - - idd - - hvo - - ja - - på - - han - - så - - ån - - det - - att - - nå - - gö - - bra - - int - - tyc - - om - - när - - två - - må - - dag - - sjä - - vii - - vuo - - eil - - tun - - käyt - - teh - - vä - - hei - - huo - - suo - - ää - - ten - - ja - - heu - - stu - - uhr - - kön - - we - - hön - -# Spoken by dionas. -- type: language - id: RootSpeak - speech: - color: "#ce5e14dd" - fontId: Noganas - obfuscation: - !type:SyllableObfuscation - minSyllables: 1 - maxSyllables: 5 - replacement: - - hs - - zt - - kr - - st - - sh - -# A mess of broken Japanese, spoken by Felinds and Oni -- type: language - id: Nekomimetic - speech: - color: "#df57aaee" - fontId: Manga - obfuscation: - !type:SyllableObfuscation - minSyllables: 1 - maxSyllables: 3 # May be too long even, we'll see. - replacement: - - neko - - nyan - - mimi - - moe - - mofu - - fuwa - - kyaa - - kawaii - - poka - - munya - - puni - - munyu - - ufufu - - icha - - doki - - kyun - - kusu - - nya - - nyaa - - desu - - kis - - ama - - chuu - - baka - - hewo - - boop - - gato - - kit - - sune - - yori - - sou - - baka - - chan - - san - - kun - - mahou - - yatta - - suki - - usagi - - domo - - ori - - uwa - - zaazaa - - shiku - - puru - - ira - - heto - - etto - -# Spoken by the Lizard race. -- type: language - id: Draconic - speech: - color: "#2aca2add" - obfuscation: - !type:SyllableObfuscation - minSyllables: 2 - maxSyllables: 4 - replacement: - - za - - az - - ze - - ez - - zi - - iz - - zo - - oz - - zu - - uz - - zs - - sz - - ha - - ah - - he - - eh - - hi - - ih - - ho - - oh - - hu - - uh - - hs - - sh - - la - - al - - le - - el - - li - - il - - lo - - ol - - lu - - ul - - ls - - sl - - ka - - ak - - ke - - ek - - ki - - ik - - ko - - ok - - ku - - uk - - ks - - sk - - sa - - as - - se - - es - - si - - is - - so - - os - - su - - us - - ss - - ss - - ra - - ar - - re - - er - - ri - - ir - - ro - - or - - ru - - ur - - rs - - sr - - a - - a - - e - - e - - i - - i - - o - - o - - u - - u - - s - - s - -# Spoken by the Vulpkanin race. -- type: language - id: Canilunzt - speech: - color: "#d69b3dcc" - obfuscation: - !type:SyllableObfuscation - minSyllables: 1 - maxSyllables: 4 - replacement: - - rur - - ya - - cen - - rawr - - bar - - kuk - - tek - - qat - - uk - - wu - - vuh - - tah - - tch - - schz - - auch - - ist - - ein - - entch - - zwichs - - tut - - mir - - wo - - bis - - es - - vor - - nic - - gro - - enem - - zandt - - tzch - - noch - - hel - - ischt - - far - - wa - - baram - - iereng - - tech - - lach - - sam - - mak - - lich - - gen - - or - - ag - - eck - - gec - - stag - - onn - - bin - - ket - - jarl - - vulf - - einech - - cresthz - - azunein - - ghzth - -# The common language of the Sol system. -- type: language - id: SolCommon - speech: - color: "#8282fbaa" - obfuscation: - !type:SyllableObfuscation - minSyllables: 1 - maxSyllables: 4 - replacement: - - tao - - shi - - tzu - - yi - - com - - be - - is - - i - - op - - vi - - ed - - lec - - mo - - cle - - te - - dis - - e - -- type: language - id: RobotTalk - speech: - fontId: Monospace - obfuscation: - !type:SyllableObfuscation - minSyllables: 1 - maxSyllables: 10 # Crazy - replacement: - - 0 - - 1 - -# Languages spoken by various critters. -- type: language - id: Cat - obfuscation: - !type:SyllableObfuscation - minSyllables: 1 - maxSyllables: 2 - replacement: - - murr - - meow - - purr - - mrow - -- type: language - id: Dog - obfuscation: - !type:SyllableObfuscation - minSyllables: 1 - maxSyllables: 2 - replacement: - - woof - - bark - - ruff - - bork - - raff - - garr - -- type: language - id: Fox - obfuscation: - !type:SyllableObfuscation - minSyllables: 1 - maxSyllables: 2 - replacement: - - ruff - - raff - - garr - - yip - - yap - - myah - -- type: language - id: Xeno - obfuscation: - !type:SyllableObfuscation - minSyllables: 1 - maxSyllables: 8 # I was crazy once - replacement: - - s - - S - -- type: language - id: Monkey - obfuscation: - !type:SyllableObfuscation - minSyllables: 1 - maxSyllables: 8 # They locked me in a room... - replacement: - - o - - k - -- type: language - id: Mouse - obfuscation: - !type:SyllableObfuscation - minSyllables: 2 - maxSyllables: 3 - replacement: - - squ - - eak - - pi - - ep - - chuu - - ee - - fwi - - he - -- type: language - id: Chicken - obfuscation: - !type:SyllableObfuscation - minSyllables: 1 - maxSyllables: 3 - replacement: - - co - - coo - - ot - -- type: language - id: Duck - obfuscation: - !type:SyllableObfuscation - minSyllables: 1 - maxSyllables: 3 - replacement: - - qu - - ack - - quack - -- type: language - id: Cow - obfuscation: - !type:SyllableObfuscation - minSyllables: 1 - maxSyllables: 3 - replacement: - - moo - - mooo - -- type: language - id: Sheep - obfuscation: - !type:SyllableObfuscation - minSyllables: 1 - maxSyllables: 3 - replacement: - - ba - - baa - - aa - -- type: language - id: Kangaroo - obfuscation: - !type:SyllableObfuscation - minSyllables: 1 - maxSyllables: 3 - replacement: - - shre - - ack - - chuu - - choo - -- type: language - id: Pig - obfuscation: - !type:SyllableObfuscation - minSyllables: 1 - maxSyllables: 3 - replacement: - - oink # Please someone come up with something better - -- type: language - id: Crab - obfuscation: - !type:SyllableObfuscation - minSyllables: 1 - maxSyllables: 3 - replacement: - - click - - clack - - ti - - pi - - tap - - cli - - ick - -- type: language - id: Kobold - obfuscation: - !type:SyllableObfuscation - minSyllables: 2 - maxSyllables: 4 - replacement: - - yip - - yap - - gar - - grr - - ar - - scre - - et - - gronk - - hiss - - ss - - ee - -- type: language - id: Hissing - obfuscation: - !type:SyllableObfuscation - minSyllables: 2 - maxSyllables: 4 - replacement: - - hss - - iss - - ss - - is - -# Example of a sign language. Not currently used anyhow. -- type: language - id: Sign - speech: - allowRadio: false - requireSpeech: false - color: "#dddddd" - messageWrapOverrides: - Speak: chat-sign-language-message-wrap - Whisper: chat-sign-language-whisper-wrap - speechVerbOverrides: - - chat-speech-verb-sign-1 - - chat-speech-verb-sign-2 - - chat-speech-verb-sign-3 - obfuscation: - !type:ReplacementObfuscation - replacement: - - something - - a cryptic message to you - - a signal to you - - a message - - a rude expression to you - - a sad expression to you - - a happy expression to you diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/Oni.yml b/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/Oni.yml index c1655287fdc..da723e7c43d 100644 --- a/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/Oni.yml +++ b/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/Oni.yml @@ -40,10 +40,10 @@ proto: oni - type: LanguageKnowledge speaks: - - GalacticCommon + - TauCetiBasic - Nekomimetic understands: - - GalacticCommon + - TauCetiBasic - Nekomimetic - type: entity diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/felinid.yml b/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/felinid.yml index 411eb13444b..44779fe9508 100644 --- a/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/felinid.yml +++ b/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/felinid.yml @@ -67,12 +67,10 @@ - type: NoShoesSilentFootsteps - type: LanguageKnowledge speaks: - - GalacticCommon - - SolCommon + - TauCetiBasic - Nekomimetic understands: - - GalacticCommon - - SolCommon + - TauCetiBasic - Nekomimetic - type: Thieving ignoreStripHidden: true diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Structures/Research/sophicscribe.yml b/Resources/Prototypes/Nyanotrasen/Entities/Structures/Research/sophicscribe.yml index 533cf314999..511262d3186 100644 --- a/Resources/Prototypes/Nyanotrasen/Entities/Structures/Research/sophicscribe.yml +++ b/Resources/Prototypes/Nyanotrasen/Entities/Structures/Research/sophicscribe.yml @@ -49,9 +49,9 @@ guides: - Psionics - type: LanguageSpeaker - currentLanguage: GalacticCommon + currentLanguage: TauCetiBasic - type: LanguageKnowledge speaks: - - GalacticCommon + - TauCetiBasic understands: - - GalacticCommon + - TauCetiBasic diff --git a/Resources/Prototypes/Traits/categories.yml b/Resources/Prototypes/Traits/categories.yml index e413706bfa0..fcd89d5bbe8 100644 --- a/Resources/Prototypes/Traits/categories.yml +++ b/Resources/Prototypes/Traits/categories.yml @@ -19,7 +19,24 @@ - type: traitCategory id: Speech root: true + subCategories: + - TraitsSpeechUncategorized + - TraitsSpeechAccents + - TraitsSpeechLanguages + +- type: traitCategory + id: TraitsSpeechUncategorized + +- type: traitCategory + id: TraitsSpeechAccents + +- type: traitCategory + id: TraitsSpeechLanguages - type: traitCategory id: Visual root: true + +- type: traitCategory + id: Language + root: true \ No newline at end of file diff --git a/Resources/Prototypes/Traits/disabilities.yml b/Resources/Prototypes/Traits/disabilities.yml index 3afc3bfba43..2b37aadb9a9 100644 --- a/Resources/Prototypes/Traits/disabilities.yml +++ b/Resources/Prototypes/Traits/disabilities.yml @@ -51,7 +51,7 @@ - type: trait id: Muted - category: Speech + category: Mental points: 4 requirements: - !type:CharacterJobRequirement @@ -77,7 +77,7 @@ - type: trait id: FrontalLisp - category: Speech + category: TraitsSpeechAccents requirements: - !type:CharacterJobRequirement inverted: true @@ -88,6 +88,8 @@ inverted: true species: - IPC + - !type:CharacterItemGroupRequirement + group: TraitsAccents components: - type: FrontalLisp diff --git a/Resources/Prototypes/Traits/inconveniences.yml b/Resources/Prototypes/Traits/inconveniences.yml index b08116dc480..2c0df79264f 100644 --- a/Resources/Prototypes/Traits/inconveniences.yml +++ b/Resources/Prototypes/Traits/inconveniences.yml @@ -23,13 +23,15 @@ - type: trait id: Stutter - category: Mental + category: TraitsSpeechAccents requirements: - !type:CharacterJobRequirement inverted: true jobs: - Borg - MedicalBorg + - !type:CharacterItemGroupRequirement + group: TraitsAccents components: - type: StutteringAccent matchRandomProb: 0.1 @@ -39,7 +41,7 @@ - type: trait id: ForeignerLight - category: Mental + category: TraitsSpeechLanguages points: 2 requirements: - !type:CharacterTraitRequirement @@ -53,7 +55,7 @@ - type: trait id: Foreigner - category: Mental + category: TraitsSpeechLanguages points: 4 requirements: # TODO: Add a requirement to know at least 1 non-gc language - !type:CharacterTraitRequirement diff --git a/Resources/Prototypes/Traits/languages.yml b/Resources/Prototypes/Traits/languages.yml new file mode 100644 index 00000000000..5fc1f3bb918 --- /dev/null +++ b/Resources/Prototypes/Traits/languages.yml @@ -0,0 +1,67 @@ +- type: trait + id: SignLanguage + category: TraitsSpeechLanguages + points: 0 + requirements: + - !type:CharacterItemGroupRequirement + group: TraitsLanguagesBasic + languagesSpoken: + - Sign + languagesUnderstood: + - Sign + +- type: trait + id: SolCommon + category: TraitsSpeechLanguages + points: 0 + requirements: + - !type:CharacterItemGroupRequirement + group: TraitsLanguagesBasic + - !type:CharacterSpeciesRequirement + inverted: true + species: + - Human + languagesSpoken: + - SolCommon + languagesUnderstood: + - SolCommon + +- type: trait + id: Tradeband + category: TraitsSpeechLanguages + points: 0 + requirements: + - !type:CharacterItemGroupRequirement + group: TraitsLanguagesBasic + - !type:CharacterSpeciesRequirement + inverted: true + species: + - Harpy + languagesSpoken: + - Tradeband + languagesUnderstood: + - Tradeband + +- type: trait + id: Freespeak + category: TraitsSpeechLanguages + points: 0 + requirements: + - !type:CharacterItemGroupRequirement + group: TraitsLanguagesBasic + languagesSpoken: + - Freespeak + languagesUnderstood: + - Freespeak + +- type: trait + id: Elyran + category: TraitsSpeechLanguages + points: 0 + requirements: + - !type:CharacterItemGroupRequirement + group: TraitsLanguagesBasic + languagesSpoken: + - Elyran + languagesUnderstood: + - Elyran \ No newline at end of file diff --git a/Resources/Prototypes/Traits/neutral.yml b/Resources/Prototypes/Traits/neutral.yml index a103f5abeed..6186c3382b7 100644 --- a/Resources/Prototypes/Traits/neutral.yml +++ b/Resources/Prototypes/Traits/neutral.yml @@ -1,19 +1,24 @@ - type: trait id: PirateAccent - category: Speech + category: TraitsSpeechAccents + requirements: + - !type:CharacterItemGroupRequirement + group: TraitsAccents components: - type: PirateAccent - type: trait id: Accentless - category: Speech - points: -2 + category: TraitsSpeechAccents + points: -1 requirements: - !type:CharacterJobRequirement inverted: true jobs: - Borg - MedicalBorg + - !type:CharacterItemGroupRequirement + group: TraitsAccents components: - type: Accentless removes: @@ -24,7 +29,10 @@ - type: trait id: Southern - category: Speech + category: TraitsSpeechAccents + requirements: + - !type:CharacterItemGroupRequirement + group: TraitsAccents components: - type: SouthernAccent diff --git a/Resources/Prototypes/Traits/skills.yml b/Resources/Prototypes/Traits/skills.yml index 015c9f50233..0e4868f19bc 100644 --- a/Resources/Prototypes/Traits/skills.yml +++ b/Resources/Prototypes/Traits/skills.yml @@ -98,15 +98,6 @@ species: - Felinid -- type: trait - id: SignLanguage - category: Visual - points: -2 - languagesSpoken: - - Sign - languagesUnderstood: - - Sign - - type: trait id: Voracious category: Physical From 20158ffa1ae8326cc04b678adc63f12715dc5903 Mon Sep 17 00:00:00 2001 From: SimpleStation Changelogs Date: Fri, 20 Sep 2024 21:47:04 +0000 Subject: [PATCH 43/55] Automatic Changelog Update (#922) --- Resources/Changelog/Changelog.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 00938b31cd4..061e11f63ae 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -6548,3 +6548,10 @@ Entries: id: 6378 time: '2024-09-20T20:35:43.0000000+00:00' url: https://github.com/Simple-Station/Einstein-Engines/pull/913 +- author: zelezniciar + changes: + - type: Add + message: Added Atmospheric Alerts Computer + id: 6379 + time: '2024-09-20T21:46:38.0000000+00:00' + url: https://github.com/Simple-Station/Einstein-Engines/pull/922 From 5247c9760e642cedd4951b367d18dc4e9b0b2756 Mon Sep 17 00:00:00 2001 From: VMSolidus Date: Fri, 20 Sep 2024 17:47:16 -0400 Subject: [PATCH 44/55] Remove Become Psionic Objective (#940) # Description This should have been removed awhile ago. This objective shouldn't exist because you automatically win it entirely by default if you selected the Latent Psychic trait in Chargen. And you permanently fail the objective if you didn't select said trait. # Changelog :cl: - remove: Removed the "Become Psionic" traitor objective. It was literally impossible to fail if you took the Latent Psychic trait, and literally impossible to greentext if you didn't. --- .../BecomePsionicConditionComponent.cs | 11 ------- .../Systems/BecomePsionicConditionSystem.cs | 32 ------------------- .../Nyanotrasen/Objectives/traitor.yml | 23 ------------- .../Prototypes/Objectives/objectiveGroups.yml | 2 -- 4 files changed, 68 deletions(-) delete mode 100644 Content.Server/Nyanotrasen/Objectives/Components/BecomePsionicConditionComponent.cs delete mode 100644 Content.Server/Nyanotrasen/Objectives/Systems/BecomePsionicConditionSystem.cs diff --git a/Content.Server/Nyanotrasen/Objectives/Components/BecomePsionicConditionComponent.cs b/Content.Server/Nyanotrasen/Objectives/Components/BecomePsionicConditionComponent.cs deleted file mode 100644 index 3b677bab2d4..00000000000 --- a/Content.Server/Nyanotrasen/Objectives/Components/BecomePsionicConditionComponent.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Content.Server.Objectives.Systems; - -namespace Content.Server.Objectives.Components; - -/// -/// Requires that the player dies to be complete. -/// -[RegisterComponent, Access(typeof(BecomePsionicConditionSystem))] -public sealed partial class BecomePsionicConditionComponent : Component -{ -} \ No newline at end of file diff --git a/Content.Server/Nyanotrasen/Objectives/Systems/BecomePsionicConditionSystem.cs b/Content.Server/Nyanotrasen/Objectives/Systems/BecomePsionicConditionSystem.cs deleted file mode 100644 index d090c320a41..00000000000 --- a/Content.Server/Nyanotrasen/Objectives/Systems/BecomePsionicConditionSystem.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Content.Shared.Abilities.Psionics; -using Content.Server.Objectives.Components; -using Content.Shared.Mind; -using Content.Shared.Objectives.Components; - -namespace Content.Server.Objectives.Systems -{ - public sealed class BecomePsionicConditionSystem : EntitySystem - { - private EntityQuery _metaQuery; - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnGetProgress); - } - - private void OnGetProgress(EntityUid uid, BecomePsionicConditionComponent comp, ref ObjectiveGetProgressEvent args) - { - args.Progress = GetProgress(args.Mind); - } - - private float GetProgress(MindComponent mind) - { - var entMan = IoCManager.Resolve(); - if (HasComp(mind.CurrentEntity)) - return 1; - return 0; - } - } -} diff --git a/Resources/Prototypes/Nyanotrasen/Objectives/traitor.yml b/Resources/Prototypes/Nyanotrasen/Objectives/traitor.yml index 53910a54a92..e6e497003d5 100644 --- a/Resources/Prototypes/Nyanotrasen/Objectives/traitor.yml +++ b/Resources/Prototypes/Nyanotrasen/Objectives/traitor.yml @@ -9,29 +9,6 @@ stealGroup: AntiPsychicKnife owner: job-name-mantis -- type: entity - id: BecomePsionicObjective - parent: BaseTraitorObjective - name: Become psionic - description: We need you to acquire psionics and keep them until your mission is complete. - noSpawn: true - components: - - type: NotJobsRequirement - jobs: - - Mime - - ForensicMantis - - type: Objective - difficulty: 2.5 - #unique: false - icon: - sprite: Nyanotrasen/Icons/psi.rsi - state: psi - - type: ObjectiveBlacklistRequirement - blacklist: - components: - - BecomeGolemCondition - - type: BecomePsionicCondition - #- type: entity # id: BecomeGolemObjective # parent: BaseTraitorObjective diff --git a/Resources/Prototypes/Objectives/objectiveGroups.yml b/Resources/Prototypes/Objectives/objectiveGroups.yml index 263494f7987..6929d1e1a47 100644 --- a/Resources/Prototypes/Objectives/objectiveGroups.yml +++ b/Resources/Prototypes/Objectives/objectiveGroups.yml @@ -40,8 +40,6 @@ EscapeShuttleObjective: 1 # DieObjective: 0.05 # DeltaV - Disable the lrp objective aka murderbone justification #HijackShuttleObjective: 0.02 - BecomePsionicObjective: 1 # Nyanotrasen - Become Psionic objective, see Resources/Prototypes/Nyanotrasen/Objectives/traitor.yml - #BecomeGolemObjective: 0.5 # Nyanotrasen - Become a golem objective, see Resources/Prototypes/Nyanotrasen/Objectives/traitor.yml - type: weightedRandom id: TraitorObjectiveGroupSocial From cce7e3fea37fe3fcb1b8a4174178f95400adf2b9 Mon Sep 17 00:00:00 2001 From: SimpleStation Changelogs Date: Fri, 20 Sep 2024 21:47:31 +0000 Subject: [PATCH 45/55] Automatic Changelog Update (#936) --- Resources/Changelog/Changelog.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 061e11f63ae..5603c64b1f3 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -6555,3 +6555,14 @@ Entries: id: 6379 time: '2024-09-20T21:46:38.0000000+00:00' url: https://github.com/Simple-Station/Einstein-Engines/pull/922 +- author: VMSolidus + changes: + - type: Add + message: 'A basic Languages menu has been added into the Traits tab. ' + - type: Add + message: >- + Tau-Ceti Basic, Tradeband, Freespeak, Elyran Standard, Sol Common, and + Sign Language have been added to the Languages tab. + id: 6380 + time: '2024-09-20T21:46:47.0000000+00:00' + url: https://github.com/Simple-Station/Einstein-Engines/pull/936 From 75c39d57f1c2ac1cb734dee78b759b4d199fb077 Mon Sep 17 00:00:00 2001 From: SimpleStation Changelogs Date: Fri, 20 Sep 2024 21:48:35 +0000 Subject: [PATCH 46/55] Automatic Changelog Update (#940) --- Resources/Changelog/Changelog.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 5603c64b1f3..f4bc13c1173 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -6566,3 +6566,13 @@ Entries: id: 6380 time: '2024-09-20T21:46:47.0000000+00:00' url: https://github.com/Simple-Station/Einstein-Engines/pull/936 +- author: VMSolidus + changes: + - type: Remove + message: >- + Removed the "Become Psionic" traitor objective. It was literally + impossible to fail if you took the Latent Psychic trait, and literally + impossible to greentext if you didn't. + id: 6381 + time: '2024-09-20T21:47:16.0000000+00:00' + url: https://github.com/Simple-Station/Einstein-Engines/pull/940 From 0e187aa895cc4754d0423e7eb4e1cb97da49fa89 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 21 Sep 2024 01:32:44 -0700 Subject: [PATCH 47/55] Update Credits (#923) This is an automated Pull Request. This PR updates the GitHub contributors in the credits section. Co-authored-by: SimpleStation Changelogs --- Resources/Credits/GitHub.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Resources/Credits/GitHub.txt b/Resources/Credits/GitHub.txt index 1a2d582a05b..22e7aef8a92 100644 --- a/Resources/Credits/GitHub.txt +++ b/Resources/Credits/GitHub.txt @@ -1 +1 @@ -0x6273, 2013HORSEMEATSCANDAL, 20kdc, 21Melkuu, 4dplanner, 612git, 778b, Ablankmann, Acruid, actioninja, adamsong, Admiral-Obvious-001, Adrian16199, Aerocrux, Aexxie, africalimedrop, Agoichi, Ahion, AJCM-git, AjexRose, Alekshhh, AlexMorgan3817, AlmondFlour, AlphaQwerty, Altoids1, amylizzle, ancientpower, angelofallars, ArchPigeon, Arendian, arimah, Arteben, AruMoon, as334, AsikKEsel, asperger-sind, aspiringLich, avghdev, AzzyIsNotHere, BananaFlambe, BasedUser, beck-thompson, BGare, BingoJohnson-zz, BismarckShuffle, Bixkitts, Blackern5000, Blazeror, BlueHNT, Boaz1111, BobdaBiscuit, brainfood1183, BramvanZijp, Brandon-Huu, Bribrooo, Bright0, brndd, BubblegumBlue, BYONDFuckery, c4llv07e, CaasGit, CakeQ, CaptainSqrBeard, Carbonhell, Carolyn3114, CatTheSystem, Centronias, chairbender, Charlese2, Cheackraze, cheesePizza2, Chief-Engineer, chromiumboy, Chronophylos, clorl, Clyybber, CodedCrow, ColdAutumnRain, Colin-Tel, collinlunn, ComicIronic, coolmankid12345, corentt, crazybrain23, creadth, CrigCrag, Crotalus, CrudeWax, CrzyPotato, Cyberboss, d34d10cc, Daemon, daerSeebaer, dahnte, dakamakat, dakimasu, DamianX, DangerRevolution, daniel-cr, Darkenson, DawBla, dch-GH, Deahaka, DEATHB4DEFEAT, DeathCamel58, deathride58, DebugOk, Decappi, Deeeeja, deepdarkdepths, Delete69, deltanedas, DeltaV-Bot, DerbyX, DoctorBeard, DogZeroX, dontbetank, dootythefrooty, Doru991, DoubleRiceEddiedd, DrMelon, DrSmugleaf, drteaspoon420, DTanxxx, DubiousDoggo, Duddino, Dutch-VanDerLinde, Easypoller, eclips_e, EdenTheLiznerd, EEASAS, Efruit, ElectroSR, elthundercloud, Emisse, EmoGarbage404, Endecc, enumerate0, eoineoineoin, ERORR404V1, Errant-4, estacaoespacialpirata, Evgencheg, exincore, exp111, Fahasor, FairlySadPanda, Fansana, ficcialfaint, Fildrance, FillerVK, Fishfish458, Flareguy, FluffiestFloof, FluidRock, FoLoKe, fooberticus, Fortune117, FoxxoTrystan, freeman2651, Froffy025, Fromoriss, FungiFellow, GalacticChimp, gbasood, Geekyhobo, geraeumig, Git-Nivrak, github-actions[bot], gituhabu, gluesniffler, GNF54, Golinth, GoodWheatley, graevy, GreyMario, Guess-My-Name, gusxyz, h3half, Hanzdegloker, Hardly3D, harikattar, HerCoyote23, HoofedEar, Hoolny, hord-brayden, hubismal, Hugal31, Huxellberger, iacore, IamVelcroboy, icekot8, igorsaux, ike709, Illiux, Ilya246, IlyaElDunaev, Injazz, Insineer, Interrobang01, IProduceWidgets, ItsMeThom, Jackal298, Jackrost, jamessimo, janekvap, JerryImMouse, Jessetriesagain, jessicamaybe, Jezithyr, jicksaw, JiimBob, JoeHammad1844, JohnGinnane, johnku1, joshepvodka, jproads, Jrpl, juliangiebel, JustArt1m, JustCone14, JustinTrotter, KaiShibaa, kalane15, kalanosh, Kelrak, kerisargit, keronshb, KIBORG04, Killerqu00, KingFroozy, kira-er, Kit0vras, KittenColony, Ko4ergaPunk, komunre, koteq, Krunklehorn, kxvvv, Lamrr, LankLTE, lapatison, Leander-0, leonardo-dabepis, LetterN, Level10Cybermancer, lever1209, liltenhead, LittleBuilderJane, Lomcastar, LordCarve, LordEclipse, LovelyLophi, Lukasz825700516, lunarcomets, luringens, lvvova1, lzimann, lzk228, MACMAN2003, Macoron, MagnusCrowe, ManelNavola, Matz05, MehimoNemo, MeltedPixel, MemeProof, Menshin, Mervill, metalgearsloth, mhamsterr, MilenVolf, Minty642, Mirino97, mirrorcult, misandrie, MishaUnity, MisterMecky, Mith-randalf, Mnemotechnician, Moneyl, Moomoobeef, moony, Morb0, Mr0maks, musicmanvr, Myakot, Myctai, N3X15, Nairodian, Naive817, namespace-Memory, NickPowers43, nikthechampiongr, Nimfar11, Nirnael, nmajask, nok-ko, notafet, notquitehadouken, noudoit, noverd, nuke-haus, NULL882, nyeogmi, OCOtheOmega, OctoRocket, OldDanceJacket, onoira, osjarw, Owai-Seek, pali6, Pangogie, patrikturi, PaulRitter, Peptide90, peptron1, Phantom-Lily, PHCodes, PixelTheKermit, PJB3005, Plykiya, pofitlo, pointer-to-null, PolterTzi, PoorMansDreams, potato1234x, ProfanedBane, PrPleGoo, ps3moira, Pspritechologist, Psychpsyo, psykzz, PuroSlavKing, quatre, QuietlyWhisper, qwerltaz, Radosvik, Radrark, Rainbeon, Rainfey, Rane, ravage123321, rbertoche, Redict, RedlineTriad, RednoWCirabrab, RemberBM, RemieRichards, RemTim, rene-descartes2021, RiceMar1244, RieBi, Rinkashikachi, Rockdtben, rolfero, rosieposieeee, Saakra, Samsterious, SaphireLattice, ScalyChimp, scrato, Scribbles0, Serkket, ShadowCommander, Shadowtheprotogen546, ShatteredSwords, SignalWalker, SimpleStation14, Simyon264, Sirionaut, siyengar04, Skarletto, Skrauz, Skyedra, SlamBamActionman, slarticodefast, Slava0135, SleepyScarecrow, Snowni, snowsignal, SonicHDC, SoulSloth, SpaceManiac, SpeltIncorrectyl, spoogemonster, ssdaniel24, stalengd, Stealthbomber16, stellar-novas, StrawberryMoses, superjj18, SweptWasTaken, Szunti, TadJohnson00, takemysoult, TaralGit, Tayrtahn, tday93, TekuNut, TemporalOroboros, tentekal, tgrkzus, thatrandomcanadianguy, TheArturZh, theashtronaut, thedraccx, themias, Theomund, theOperand, TheShuEd, TimrodDX, Titian3, tkdrg, Tmanzxd, tmtmtl30, TokenStyle, tom-leys, tomasalves8, Tomeno, Tornado-Technology, tosatur, Tryded, TsjipTsjip, Tunguso4ka, TurboTrackerss14, Tyler-IN, Tyzemol, UbaserB, UKNOWH, UnicornOnLSD, Uriende, UristMcDorf, Vaaankas, Varen, VasilisThePikachu, veliebm, Veritius, Vermidia, Verslebas, VigersRay, Visne, VMSolidus, volundr-, Voomra, Vordenburg, vulppine, wafehling, WarMechanic, waylon531, weaversam8, whateverusername0, Willhelm53, Winkarst-cpu, wixoaGit, WlarusFromDaSpace, wrexbe, xRiriq, yathxyz, Ygg01, YotaXP, YuriyKiss, zach-hill, Zandario, Zap527, Zealith-Gamer, ZelteHonor, zerorulez, zionnBE, zlodo, ZNixian, ZoldorfTheWizard, Zumorica, Zymem +0x6273, 2013HORSEMEATSCANDAL, 20kdc, 21Melkuu, 4dplanner, 612git, 778b, Ablankmann, Acruid, actioninja, adamsong, Admiral-Obvious-001, Adrian16199, Aerocrux, Aexxie, africalimedrop, Agoichi, Ahion, AJCM-git, AjexRose, Alekshhh, AlexMorgan3817, AlmondFlour, AlphaQwerty, Altoids1, amylizzle, ancientpower, angelofallars, ArchPigeon, Arendian, arimah, Arteben, AruMoon, as334, AsikKEsel, asperger-sind, aspiringLich, avghdev, AzzyIsNotHere, BananaFlambe, BasedUser, beck-thompson, BGare, BingoJohnson-zz, BismarckShuffle, Bixkitts, Blackern5000, Blazeror, BlueHNT, Boaz1111, BobdaBiscuit, brainfood1183, BramvanZijp, Brandon-Huu, Bribrooo, Bright0, brndd, BubblegumBlue, BYONDFuckery, c4llv07e, CaasGit, CakeQ, CaptainSqrBeard, Carbonhell, Carolyn3114, CatTheSystem, Centronias, chairbender, Charlese2, Cheackraze, cheesePizza2, Chief-Engineer, chromiumboy, Chronophylos, CilliePaint, clorl, Clyybber, CodedCrow, ColdAutumnRain, Colin-Tel, collinlunn, ComicIronic, coolmankid12345, corentt, crazybrain23, creadth, CrigCrag, Crotalus, CrudeWax, CrzyPotato, Cyberboss, d34d10cc, Daemon, daerSeebaer, dahnte, dakamakat, dakimasu, DamianX, DangerRevolution, daniel-cr, Darkenson, DawBla, dch-GH, Deahaka, DEATHB4DEFEAT, DeathCamel58, deathride58, DebugOk, Decappi, Deeeeja, deepdarkdepths, Delete69, deltanedas, DeltaV-Bot, DerbyX, DoctorBeard, DogZeroX, dontbetank, dootythefrooty, Doru991, DoubleRiceEddiedd, DrMelon, DrSmugleaf, drteaspoon420, DTanxxx, DubiousDoggo, Duddino, Dutch-VanDerLinde, Easypoller, eclips_e, EdenTheLiznerd, EEASAS, Efruit, ElectroSR, elthundercloud, Emisse, EmoGarbage404, Endecc, enumerate0, eoineoineoin, ERORR404V1, Errant-4, estacaoespacialpirata, Evgencheg, exincore, exp111, Fahasor, FairlySadPanda, Fansana, ficcialfaint, Fildrance, FillerVK, Fishfish458, Flareguy, FluffiestFloof, FluidRock, FoLoKe, fooberticus, Fortune117, FoxxoTrystan, freeman2651, Froffy025, Fromoriss, FungiFellow, GalacticChimp, gbasood, Geekyhobo, geraeumig, Git-Nivrak, github-actions[bot], gituhabu, gluesniffler, GNF54, Golinth, GoodWheatley, graevy, GreyMario, Guess-My-Name, gusxyz, h3half, Hanzdegloker, Hardly3D, harikattar, HerCoyote23, HoofedEar, Hoolny, hord-brayden, hubismal, Hugal31, Huxellberger, iacore, IamVelcroboy, icekot8, igorsaux, ike709, Illiux, Ilya246, IlyaElDunaev, Injazz, Insineer, Interrobang01, IProduceWidgets, ItsMeThom, Jackal298, Jackrost, jamessimo, janekvap, JerryImMouse, Jessetriesagain, jessicamaybe, Jezithyr, jicksaw, JiimBob, JoeHammad1844, JohnGinnane, johnku1, joshepvodka, jproads, Jrpl, juliangiebel, JustArt1m, JustCone14, JustinTrotter, KaiShibaa, kalane15, kalanosh, Kelrak, kerisargit, keronshb, KIBORG04, Killerqu00, KingFroozy, kira-er, Kit0vras, KittenColony, Ko4ergaPunk, komunre, koteq, Krunklehorn, kxvvv, Lamrr, LankLTE, lapatison, Leander-0, leonardo-dabepis, LetterN, Level10Cybermancer, lever1209, liltenhead, LittleBuilderJane, Lomcastar, LordCarve, LordEclipse, LovelyLophi, Lukasz825700516, lunarcomets, luringens, lvvova1, lzimann, lzk228, MACMAN2003, Macoron, MagnusCrowe, ManelNavola, Matz05, MehimoNemo, MeltedPixel, MemeProof, Menshin, Mervill, metalgearsloth, mhamsterr, MilenVolf, Minty642, Mirino97, mirrorcult, misandrie, MishaUnity, MisterMecky, Mith-randalf, Mnemotechnician, Moneyl, Moomoobeef, moony, Morb0, Mr0maks, musicmanvr, Myakot, Myctai, N3X15, Nairodian, Naive817, namespace-Memory, NickPowers43, nikthechampiongr, Nimfar11, Nirnael, nmajask, nok-ko, notafet, notquitehadouken, noudoit, nuke-haus, NULL882, nyeogmi, OCOtheOmega, OctoRocket, OldDanceJacket, onoira, osjarw, Owai-Seek, pali6, Pangogie, patrikturi, PaulRitter, Peptide90, peptron1, Phantom-Lily, PHCodes, PixelTheKermit, PJB3005, Plykiya, pofitlo, pointer-to-null, PolterTzi, PoorMansDreams, potato1234x, ProfanedBane, PrPleGoo, ps3moira, Pspritechologist, Psychpsyo, psykzz, PuroSlavKing, quatre, QuietlyWhisper, qwerltaz, Radosvik, Radrark, Rainbeon, Rainfey, Rane, ravage123321, rbertoche, Redict, RedlineTriad, RednoWCirabrab, RemberBM, RemieRichards, RemTim, rene-descartes2021, RiceMar1244, RieBi, Rinkashikachi, Rockdtben, rolfero, rosieposieeee, Saakra, Samsterious, SaphireLattice, ScalyChimp, scrato, Scribbles0, Serkket, ShadowCommander, Shadowtheprotogen546, ShatteredSwords, SignalWalker, SimpleStation14, Simyon264, Sirionaut, siyengar04, Skarletto, Skrauz, Skyedra, SlamBamActionman, slarticodefast, Slava0135, SleepyScarecrow, Snowni, snowsignal, SonicHDC, SoulSloth, SpaceManiac, SpeltIncorrectyl, spoogemonster, ssdaniel24, stalengd, Stealthbomber16, stellar-novas, StrawberryMoses, superjj18, SweptWasTaken, Szunti, TadJohnson00, takemysoult, TaralGit, Tayrtahn, tday93, TekuNut, TemporalOroboros, tentekal, tgrkzus, thatrandomcanadianguy, TheArturZh, theashtronaut, thedraccx, themias, Theomund, theOperand, TheShuEd, TimrodDX, Titian3, tkdrg, Tmanzxd, tmtmtl30, TokenStyle, tom-leys, tomasalves8, Tomeno, Tornado-Technology, tosatur, Tryded, TsjipTsjip, Tunguso4ka, TurboTrackerss14, Tyler-IN, Tyzemol, UbaserB, UKNOWH, UnicornOnLSD, Uriende, UristMcDorf, Vaaankas, Varen, VasilisThePikachu, veliebm, Veritius, Vermidia, Verslebas, VigersRay, Visne, VMSolidus, volundr-, Voomra, Vordenburg, vulppine, wafehling, WarMechanic, waylon531, weaversam8, whateverusername0, Willhelm53, Winkarst-cpu, wixoaGit, WlarusFromDaSpace, wrexbe, xRiriq, yathxyz, Ygg01, YotaXP, YuriyKiss, zach-hill, Zandario, Zap527, Zealith-Gamer, ZelteHonor, zerorulez, zionnBE, ZNixian, ZoldorfTheWizard, Zumorica, Zymem From 49d128c3ae9634e0848c04bfbf2fbe988ed4ba46 Mon Sep 17 00:00:00 2001 From: VMSolidus Date: Sat, 21 Sep 2024 18:46:49 -0400 Subject: [PATCH 48/55] Reimplement Part Upgrading (#917) # Description By extremely popular demand(Both internally, and from our downstreams), this PR reimplements Part Upgrading. Since some of the systems that this PR touches were substantially changed since the removal of Parts, I had to do a lot of very in depth by-hand edits of individual systems. Shockingly, the only one that really proved any trouble was Cloning System, so I'm genuinely surprised wizden didn't substantially touch any of these codes since removing parts.. # Changelog :cl: - add: Part Upgrading has returned! --------- Signed-off-by: VMSolidus Co-authored-by: DEATHB4DEFEAT <77995199+DEATHB4DEFEAT@users.noreply.github.com> --- .../Interaction/MachineConstruction.cs | 1 - .../Interaction/InteractionTest.Constants.cs | 5 +- .../Anomaly/AnomalySystem.Vessel.cs | 7 + .../Binary/Components/GasRecyclerComponent.cs | 25 +- .../Binary/EntitySystems/GasRecyclerSystem.cs | 18 ++ .../Portable/PortableScrubberComponent.cs | 46 +++- .../Atmos/Portable/PortableScrubberSystem.cs | 18 ++ Content.Server/Bed/BedSystem.cs | 19 +- .../Bed/Components/StasisBedComponent.cs | 11 +- .../Components/SeedExtractorComponent.cs | 30 ++- .../Botany/Systems/SeedExtractorSystem.cs | 20 +- .../Cargo/Systems/CargoSystem.Telepad.cs | 15 ++ .../Components/SolutionHeaterComponent.cs | 22 +- .../EntitySystems/SolutionHeaterSystem.cs | 15 ++ Content.Server/Cloning/CloningSystem.cs | 20 ++ .../ConstructionSystem.Machine.cs | 5 +- .../Construction/PartExchangerSystem.cs | 3 +- .../Gravity/GravityGeneratorComponent.cs | 3 + .../Gravity/GravityGeneratorSystem.cs | 8 + .../Kitchen/Components/MicrowaveComponent.cs | 60 ++--- .../Components/ReagentGrinderComponent.cs | 21 +- .../Kitchen/EntitySystems/MicrowaveSystem.cs | 13 ++ .../EntitySystems/ReagentGrinderSystem.cs | 21 ++ .../Materials/MaterialReclaimerSystem.cs | 16 ++ .../BiomassReclaimerComponent.cs | 93 ++++++-- .../BiomassReclaimerSystem.cs | 23 ++ .../Components/MedicalScannerComponent.cs | 12 +- .../Medical/MedicalScannerSystem.cs | 15 ++ .../Components/FatExtractorComponent.cs | 65 ++++-- .../EntitySystems/FatExtractorSystem.cs | 15 ++ .../Components/UpgradeBatteryComponent.cs | 28 +++ .../Components/UpgradePowerDrawComponent.cs | 41 ++++ .../UpgradePowerSupplierComponent.cs | 36 +++ .../UpgradePowerSupplyRampingComponent.cs | 36 +++ .../EntitySystems/UpgradeBatterySystem.cs | 39 ++++ .../Power/EntitySystems/UpgradePowerSystem.cs | 151 ++++++++++++ .../Power/Generator/GeneratorSystem.cs | 7 +- .../Shuttles/Components/ThrusterComponent.cs | 28 ++- .../Shuttles/Systems/ThrusterSystem.cs | 22 ++ .../EntitySystems/EmitterSystem.cs | 17 ++ .../Components/TraversalDistorterComponent.cs | 17 +- .../Systems/TraversalDistorterSystem.cs | 20 +- .../XenoArtifacts/ArtifactSystem.cs | 1 + .../Components/SharedCargoTelepadComponent.cs | 35 ++- Content.Shared/Cloning/CloningPodComponent.cs | 25 ++ .../Construction/MachinePartSystem.cs | 15 ++ .../Materials/MaterialReclaimerComponent.cs | 81 ++++--- .../Components/SharedEmitterComponent.cs | 101 +++++--- Resources/Locale/en-US/anomaly/anomaly.ftl | 1 + .../en-US/atmos/gas-recycler-system.ftl | 3 + .../Locale/en-US/atmos/portable-scrubber.ftl | 3 + .../components/seed-extractor-component.ftl | 2 + .../en-US/cargo/cargo-console-component.ftl | 2 + .../components/solution-heater-component.ftl | 1 + .../components/machine-part-component.ftl | 2 + .../components/microwave-component.ftl | 1 + .../components/reagent-grinder-component.ftl | 3 + Resources/Locale/en-US/machine/machine.ftl | 5 + .../Locale/en-US/materials/materials.ftl | 3 + .../biomass-reclaimer-component.ftl | 3 + .../components/cloning-pod-component.ftl | 2 + .../components/medical-scanner-component.ftl | 2 + .../components/stasis-bed-component.ftl | 1 + .../nutrition/components/fat-extractor.ftl | 2 + Resources/Locale/en-US/shuttles/thruster.ftl | 2 + .../components/emitter-component.ftl | 3 + .../xenoarchaeology/traversal-distorter.ftl | 2 + Resources/Migrations/migration.yml | 22 -- .../Markers/Spawners/Random/salvage.yml | 93 +++++++- .../Entities/Objects/Misc/machine_parts.yml | 219 ++++++++++++++++++ .../Objects/Specific/Research/rped.yml | 3 + .../Entities/Structures/Dispensers/chem.yml | 3 + .../Structures/Machines/anomaly_equipment.yml | 4 +- .../Structures/Machines/anomaly_sync.yml | 5 +- .../Structures/Machines/artifact_analyzer.yml | 3 + .../Entities/Structures/Machines/lathe.yml | 6 + .../Structures/Machines/seed_extractor.yml | 3 + .../Power/Generation/portable_generator.yml | 6 + .../Entities/Structures/Power/smes.yml | 7 +- .../Entities/Structures/Power/substation.yml | 7 +- .../Structures/Shuttles/thrusters.yml | 5 + Resources/Prototypes/Guidebook/science.yml | 6 + Resources/Prototypes/Recipes/Lathes/Parts.yml | 60 +++++ .../Prototypes/Research/experimental.yml | 28 +++ .../XenoArch/Effects/normal_effects.yml | 18 ++ .../Guidebook/Science/MachineUpgrading.xml | 38 +++ 86 files changed, 1704 insertions(+), 220 deletions(-) create mode 100644 Content.Server/Power/Components/UpgradeBatteryComponent.cs create mode 100644 Content.Server/Power/Components/UpgradePowerDrawComponent.cs create mode 100644 Content.Server/Power/Components/UpgradePowerSupplierComponent.cs create mode 100644 Content.Server/Power/Components/UpgradePowerSupplyRampingComponent.cs create mode 100644 Content.Server/Power/EntitySystems/UpgradeBatterySystem.cs create mode 100644 Content.Server/Power/EntitySystems/UpgradePowerSystem.cs create mode 100644 Resources/Locale/en-US/chemistry/components/solution-heater-component.ftl create mode 100644 Resources/Locale/en-US/construction/components/machine-part-component.ftl create mode 100644 Resources/Locale/en-US/medical/components/stasis-bed-component.ftl create mode 100644 Resources/ServerInfo/Guidebook/Science/MachineUpgrading.xml diff --git a/Content.IntegrationTests/Tests/Construction/Interaction/MachineConstruction.cs b/Content.IntegrationTests/Tests/Construction/Interaction/MachineConstruction.cs index f52f820a4ce..cd95a85f205 100644 --- a/Content.IntegrationTests/Tests/Construction/Interaction/MachineConstruction.cs +++ b/Content.IntegrationTests/Tests/Construction/Interaction/MachineConstruction.cs @@ -57,4 +57,3 @@ public async Task ChangeMachine() AssertPrototype("Autolathe"); } } - diff --git a/Content.IntegrationTests/Tests/Interaction/InteractionTest.Constants.cs b/Content.IntegrationTests/Tests/Interaction/InteractionTest.Constants.cs index 11381fb8ccd..a915e5d47d1 100644 --- a/Content.IntegrationTests/Tests/Interaction/InteractionTest.Constants.cs +++ b/Content.IntegrationTests/Tests/Interaction/InteractionTest.Constants.cs @@ -1,3 +1,4 @@ + namespace Content.IntegrationTests.Tests.Interaction; // This partial class contains various constant prototype IDs common to interaction tests. @@ -27,8 +28,6 @@ public abstract partial class InteractionTest // Parts protected const string Bin1 = "MatterBinStockPart"; - protected const string Cap1 = "CapacitorStockPart"; protected const string Manipulator1 = "MicroManipulatorStockPart"; - protected const string Battery1 = "PowerCellSmall"; - protected const string Battery4 = "PowerCellHyper"; } + diff --git a/Content.Server/Anomaly/AnomalySystem.Vessel.cs b/Content.Server/Anomaly/AnomalySystem.Vessel.cs index 35de5c1a646..9a6c99f8208 100644 --- a/Content.Server/Anomaly/AnomalySystem.Vessel.cs +++ b/Content.Server/Anomaly/AnomalySystem.Vessel.cs @@ -1,4 +1,5 @@ using Content.Server.Anomaly.Components; +using Content.Server.Construction; using Content.Server.Power.EntitySystems; using Content.Shared.Anomaly; using Content.Shared.Anomaly.Components; @@ -20,6 +21,7 @@ private void InitializeVessel() { SubscribeLocalEvent(OnVesselShutdown); SubscribeLocalEvent(OnVesselMapInit); + SubscribeLocalEvent(OnUpgradeExamine); SubscribeLocalEvent(OnVesselInteractUsing); SubscribeLocalEvent(OnExamined); SubscribeLocalEvent(OnVesselGetPointsPerSecond); @@ -65,6 +67,11 @@ private void OnVesselMapInit(EntityUid uid, AnomalyVesselComponent component, Ma UpdateVesselAppearance(uid, component); } + private void OnUpgradeExamine(EntityUid uid, AnomalyVesselComponent component, UpgradeExamineEvent args) + { + args.AddPercentageUpgrade("anomaly-vessel-component-upgrade-output", component.PointMultiplier); + } + private void OnVesselInteractUsing(EntityUid uid, AnomalyVesselComponent component, InteractUsingEvent args) { if (component.Anomaly != null || diff --git a/Content.Server/Atmos/Piping/Binary/Components/GasRecyclerComponent.cs b/Content.Server/Atmos/Piping/Binary/Components/GasRecyclerComponent.cs index e1eb0072b9d..aa7e3e0b360 100644 --- a/Content.Server/Atmos/Piping/Binary/Components/GasRecyclerComponent.cs +++ b/Content.Server/Atmos/Piping/Binary/Components/GasRecyclerComponent.cs @@ -1,11 +1,12 @@ using Content.Shared.Atmos; +using Content.Shared.Construction.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; namespace Content.Server.Atmos.Piping.Binary.Components { [RegisterComponent] public sealed partial class GasRecyclerComponent : Component { - [ViewVariables(VVAccess.ReadOnly)] [DataField("reacting")] public Boolean Reacting { get; set; } = false; @@ -17,10 +18,28 @@ public sealed partial class GasRecyclerComponent : Component [DataField("outlet")] public string OutletName { get; set; } = "outlet"; - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] public float MinTemp = 300 + Atmospherics.T0C; - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] + public float BaseMinTemp = 300 + Atmospherics.T0C; + + [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))] + public string MachinePartMinTemp = "Capacitor"; + + [DataField] + public float PartRatingMinTempMultiplier = 0.95f; + + [ViewVariables(VVAccess.ReadWrite)] public float MinPressure = 30 * Atmospherics.OneAtmosphere; + + [DataField] + public float BaseMinPressure = 30 * Atmospherics.OneAtmosphere; + + [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))] + public string MachinePartMinPressure = "Manipulator"; + + [DataField] + public float PartRatingMinPressureMultiplier = 0.8f; } } diff --git a/Content.Server/Atmos/Piping/Binary/EntitySystems/GasRecyclerSystem.cs b/Content.Server/Atmos/Piping/Binary/EntitySystems/GasRecyclerSystem.cs index 3ebc5094926..40b9d88846f 100644 --- a/Content.Server/Atmos/Piping/Binary/EntitySystems/GasRecyclerSystem.cs +++ b/Content.Server/Atmos/Piping/Binary/EntitySystems/GasRecyclerSystem.cs @@ -1,6 +1,7 @@ using Content.Server.Atmos.EntitySystems; using Content.Server.Atmos.Piping.Binary.Components; using Content.Server.Atmos.Piping.Components; +using Content.Server.Construction; using Content.Server.NodeContainer; using Content.Server.NodeContainer.EntitySystems; using Content.Server.NodeContainer.Nodes; @@ -28,6 +29,8 @@ public override void Initialize() SubscribeLocalEvent(OnUpdate); SubscribeLocalEvent(OnDisabled); SubscribeLocalEvent(OnExamined); + SubscribeLocalEvent(OnRefreshParts); + SubscribeLocalEvent(OnUpgradeExamine); } private void OnEnabled(EntityUid uid, GasRecyclerComponent comp, ref AtmosDeviceEnabledEvent args) @@ -116,5 +119,20 @@ private void UpdateAppearance(EntityUid uid, GasRecyclerComponent? comp = null) _appearance.SetData(uid, PumpVisuals.Enabled, comp.Reacting); } + + private void OnRefreshParts(EntityUid uid, GasRecyclerComponent component, RefreshPartsEvent args) + { + var ratingTemp = args.PartRatings[component.MachinePartMinTemp]; + var ratingPressure = args.PartRatings[component.MachinePartMinPressure]; + + component.MinTemp = component.BaseMinTemp * MathF.Pow(component.PartRatingMinTempMultiplier, ratingTemp - 1); + component.MinPressure = component.BaseMinPressure * MathF.Pow(component.PartRatingMinPressureMultiplier, ratingPressure - 1); + } + + private void OnUpgradeExamine(EntityUid uid, GasRecyclerComponent component, UpgradeExamineEvent args) + { + args.AddPercentageUpgrade("gas-recycler-upgrade-min-temp", component.MinTemp / component.BaseMinTemp); + args.AddPercentageUpgrade("gas-recycler-upgrade-min-pressure", component.MinPressure / component.BaseMinPressure); + } } } diff --git a/Content.Server/Atmos/Portable/PortableScrubberComponent.cs b/Content.Server/Atmos/Portable/PortableScrubberComponent.cs index ae9a5da9639..fbe2d3f95a0 100644 --- a/Content.Server/Atmos/Portable/PortableScrubberComponent.cs +++ b/Content.Server/Atmos/Portable/PortableScrubberComponent.cs @@ -1,4 +1,6 @@ using Content.Shared.Atmos; +using Content.Shared.Construction.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; namespace Content.Server.Atmos.Portable { @@ -37,13 +39,51 @@ public sealed partial class PortableScrubberComponent : Component /// /// Maximum internal pressure before it refuses to take more. /// - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] public float MaxPressure = 2500; /// - /// The speed at which gas is scrubbed from the environment. + /// The base amount of maximum internal pressure /// - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] + public float BaseMaxPressure = 2500; + + /// + /// The machine part that modifies the maximum internal pressure + /// + [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))] + public string MachinePartMaxPressure = "MatterBin"; + + /// + /// How much the will affect the pressure. + /// The value will be multiplied by this amount for each increasing part tier. + /// + [DataField] + public float PartRatingMaxPressureModifier = 1.5f; + + /// + /// The speed at which gas is scrubbed from the environment. + /// + [ViewVariables(VVAccess.ReadWrite)] public float TransferRate = 800; + + /// + /// The base speed at which gas is scrubbed from the environment. + /// + [DataField] + public float BaseTransferRate = 800; + + /// + /// The machine part which modifies the speed of + /// + [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))] + public string MachinePartTransferRate = "Manipulator"; + + /// + /// How much the will modify the rate. + /// The value will be multiplied by this amount for each increasing part tier. + /// + [DataField] + public float PartRatingTransferRateModifier = 1.4f; } } diff --git a/Content.Server/Atmos/Portable/PortableScrubberSystem.cs b/Content.Server/Atmos/Portable/PortableScrubberSystem.cs index f9043c091a8..f657d713d28 100644 --- a/Content.Server/Atmos/Portable/PortableScrubberSystem.cs +++ b/Content.Server/Atmos/Portable/PortableScrubberSystem.cs @@ -12,6 +12,7 @@ using Content.Server.NodeContainer.NodeGroups; using Content.Server.Audio; using Content.Server.Administration.Logs; +using Content.Server.Construction; using Content.Server.NodeContainer.EntitySystems; using Content.Shared.Database; @@ -38,6 +39,8 @@ public override void Initialize() SubscribeLocalEvent(OnExamined); SubscribeLocalEvent(OnDestroyed); SubscribeLocalEvent(OnScrubberAnalyzed); + SubscribeLocalEvent(OnRefreshParts); + SubscribeLocalEvent(OnUpgradeExamine); } private bool IsFull(PortableScrubberComponent component) @@ -156,5 +159,20 @@ private void OnScrubberAnalyzed(EntityUid uid, PortableScrubberComponent compone if (_nodeContainer.TryGetNode(uid, component.PortName, out PipeNode? port)) args.GasMixtures.Add(component.PortName, port.Air); } + + private void OnRefreshParts(EntityUid uid, PortableScrubberComponent component, RefreshPartsEvent args) + { + var pressureRating = args.PartRatings[component.MachinePartMaxPressure]; + var transferRating = args.PartRatings[component.MachinePartTransferRate]; + + component.MaxPressure = component.BaseMaxPressure * MathF.Pow(component.PartRatingMaxPressureModifier, pressureRating - 1); + component.TransferRate = component.BaseTransferRate * MathF.Pow(component.PartRatingTransferRateModifier, transferRating - 1); + } + + private void OnUpgradeExamine(EntityUid uid, PortableScrubberComponent component, UpgradeExamineEvent args) + { + args.AddPercentageUpgrade("portable-scrubber-component-upgrade-max-pressure", component.MaxPressure / component.BaseMaxPressure); + args.AddPercentageUpgrade("portable-scrubber-component-upgrade-transfer-rate", component.TransferRate / component.BaseTransferRate); + } } } diff --git a/Content.Server/Bed/BedSystem.cs b/Content.Server/Bed/BedSystem.cs index f1bd9482e5e..976ef5139c3 100644 --- a/Content.Server/Bed/BedSystem.cs +++ b/Content.Server/Bed/BedSystem.cs @@ -2,6 +2,7 @@ using Content.Server.Bed.Components; using Content.Server.Bed.Sleep; using Content.Server.Body.Systems; +using Content.Server.Construction; using Content.Server.Power.Components; using Content.Server.Power.EntitySystems; using Content.Shared.Bed; @@ -9,6 +10,7 @@ using Content.Shared.Body.Components; using Content.Shared.Buckle.Components; using Content.Shared.Damage; +using Content.Shared.Emag.Components; using Content.Shared.Emag.Systems; using Content.Shared.Mobs.Systems; using Robust.Shared.Timing; @@ -32,6 +34,8 @@ public override void Initialize() SubscribeLocalEvent(OnBuckleChange); SubscribeLocalEvent(OnPowerChanged); SubscribeLocalEvent(OnEmagged); + SubscribeLocalEvent(OnRefreshParts); + SubscribeLocalEvent(OnUpgradeExamine); } private void ManageUpdateList(EntityUid uid, HealOnBuckleComponent component, ref BuckleChangeEvent args) @@ -66,7 +70,7 @@ public override void Update(float frameTime) foreach (var healedEntity in strapComponent.BuckledEntities) { - if (_mobStateSystem.IsDead(healedEntity) + if (_mobStateSystem.IsDead(healedEntity) || HasComp(healedEntity)) continue; @@ -126,5 +130,18 @@ private void UpdateMetabolisms(EntityUid uid, StasisBedComponent component, bool RaiseLocalEvent(buckledEntity, ref metabolicEvent); } } + + private void OnRefreshParts(EntityUid uid, StasisBedComponent component, RefreshPartsEvent args) + { + var metabolismRating = args.PartRatings[component.MachinePartMetabolismModifier]; + component.Multiplier = component.BaseMultiplier * metabolismRating; // Linear scaling so it's not OP + if (HasComp(uid)) + component.Multiplier = 1f / component.Multiplier; + } + + private void OnUpgradeExamine(EntityUid uid, StasisBedComponent component, UpgradeExamineEvent args) + { + args.AddPercentageUpgrade("stasis-bed-component-upgrade-stasis", component.Multiplier / component.BaseMultiplier); + } } } diff --git a/Content.Server/Bed/Components/StasisBedComponent.cs b/Content.Server/Bed/Components/StasisBedComponent.cs index e2175d6e646..bb4096a2a5e 100644 --- a/Content.Server/Bed/Components/StasisBedComponent.cs +++ b/Content.Server/Bed/Components/StasisBedComponent.cs @@ -1,12 +1,21 @@ +using Content.Shared.Construction.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; + namespace Content.Server.Bed.Components { [RegisterComponent] public sealed partial class StasisBedComponent : Component { + [DataField] + public float BaseMultiplier = 10f; + /// - /// What the metabolic update rate will be multiplied by (higher = slower metabolism) + /// What the metabolic update rate will be multiplied by (higher = slower metabolism) /// [ViewVariables(VVAccess.ReadWrite)] public float Multiplier = 10f; + + [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))] + public string MachinePartMetabolismModifier = "Capacitor"; } } diff --git a/Content.Server/Botany/Components/SeedExtractorComponent.cs b/Content.Server/Botany/Components/SeedExtractorComponent.cs index ddb04f213d1..d765e079cec 100644 --- a/Content.Server/Botany/Components/SeedExtractorComponent.cs +++ b/Content.Server/Botany/Components/SeedExtractorComponent.cs @@ -1,4 +1,7 @@ using Content.Server.Botany.Systems; +using Content.Server.Construction; +using Content.Shared.Construction.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; namespace Content.Server.Botany.Components; @@ -7,14 +10,33 @@ namespace Content.Server.Botany.Components; public sealed partial class SeedExtractorComponent : Component { /// - /// The minimum amount of seed packets dropped. + /// The minimum amount of seed packets dropped with no machine upgrades. /// - [DataField("baseMinSeeds"), ViewVariables(VVAccess.ReadWrite)] + [DataField] public int BaseMinSeeds = 1; /// - /// The maximum amount of seed packets dropped. + /// The maximum amount of seed packets dropped with no machine upgrades. /// - [DataField("baseMaxSeeds"), ViewVariables(VVAccess.ReadWrite)] + [DataField] public int BaseMaxSeeds = 3; + + /// + /// Modifier to the amount of seeds outputted, set on . + /// + [ViewVariables(VVAccess.ReadWrite)] + public float SeedAmountMultiplier; + + /// + /// Machine part whose rating modifies the amount of seed packets dropped. + /// + [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))] + public string MachinePartSeedAmount = "Manipulator"; + + /// + /// How much the machine part quality affects the amount of seeds outputted. + /// Going up a tier will multiply the seed output by this amount. + /// + [DataField] + public float PartRatingSeedAmountMultiplier = 1.5f; } diff --git a/Content.Server/Botany/Systems/SeedExtractorSystem.cs b/Content.Server/Botany/Systems/SeedExtractorSystem.cs index f1ae6c9f11a..4c547b96f09 100644 --- a/Content.Server/Botany/Systems/SeedExtractorSystem.cs +++ b/Content.Server/Botany/Systems/SeedExtractorSystem.cs @@ -1,8 +1,10 @@ using Content.Server.Botany.Components; +using Content.Server.Construction; using Content.Server.Popups; using Content.Server.Power.EntitySystems; using Content.Shared.Interaction; using Content.Shared.Popups; +using Robust.Shared.Player; using Robust.Shared.Random; namespace Content.Server.Botany.Systems; @@ -18,6 +20,8 @@ public override void Initialize() base.Initialize(); SubscribeLocalEvent(OnInteractUsing); + SubscribeLocalEvent(OnRefreshParts); + SubscribeLocalEvent(OnUpgradeExamine); } private void OnInteractUsing(EntityUid uid, SeedExtractorComponent seedExtractor, InteractUsingEvent args) @@ -25,8 +29,7 @@ private void OnInteractUsing(EntityUid uid, SeedExtractorComponent seedExtractor if (!this.IsPowered(uid, EntityManager)) return; - if (!TryComp(args.Used, out ProduceComponent? produce)) - return; + if (!TryComp(args.Used, out ProduceComponent? produce)) return; if (!_botanySystem.TryGetSeed(produce, out var seed) || seed.Seedless) { _popupSystem.PopupCursor(Loc.GetString("seed-extractor-component-no-seeds",("name", args.Used)), @@ -39,7 +42,7 @@ private void OnInteractUsing(EntityUid uid, SeedExtractorComponent seedExtractor QueueDel(args.Used); - var amount = _random.Next(seedExtractor.BaseMinSeeds, seedExtractor.BaseMaxSeeds + 1); + var amount = (int) _random.NextFloat(seedExtractor.BaseMinSeeds, seedExtractor.BaseMaxSeeds + 1) * seedExtractor.SeedAmountMultiplier; var coords = Transform(uid).Coordinates; if (amount > 1) @@ -50,4 +53,15 @@ private void OnInteractUsing(EntityUid uid, SeedExtractorComponent seedExtractor _botanySystem.SpawnSeedPacket(seed, coords, args.User); } } + + private void OnRefreshParts(EntityUid uid, SeedExtractorComponent seedExtractor, RefreshPartsEvent args) + { + var manipulatorQuality = args.PartRatings[seedExtractor.MachinePartSeedAmount]; + seedExtractor.SeedAmountMultiplier = MathF.Pow(seedExtractor.PartRatingSeedAmountMultiplier, manipulatorQuality - 1); + } + + private void OnUpgradeExamine(EntityUid uid, SeedExtractorComponent seedExtractor, UpgradeExamineEvent args) + { + args.AddPercentageUpgrade("seed-extractor-component-upgrade-seed-yield", seedExtractor.SeedAmountMultiplier); + } } diff --git a/Content.Server/Cargo/Systems/CargoSystem.Telepad.cs b/Content.Server/Cargo/Systems/CargoSystem.Telepad.cs index 42aabf2578e..1c25b0e79d2 100644 --- a/Content.Server/Cargo/Systems/CargoSystem.Telepad.cs +++ b/Content.Server/Cargo/Systems/CargoSystem.Telepad.cs @@ -1,4 +1,6 @@ using Content.Server.Cargo.Components; +using Content.Server.Construction; +using Content.Server.Paper; using Content.Server.Power.Components; using Content.Shared.Cargo; using Content.Shared.Cargo.Components; @@ -13,6 +15,8 @@ public sealed partial class CargoSystem private void InitializeTelepad() { SubscribeLocalEvent(OnInit); + SubscribeLocalEvent(OnRefreshParts); + SubscribeLocalEvent(OnUpgradeExamine); SubscribeLocalEvent(OnTelepadPowerChange); // Shouldn't need re-anchored event SubscribeLocalEvent(OnTelepadAnchorChange); @@ -79,6 +83,17 @@ private void OnInit(EntityUid uid, CargoTelepadComponent telepad, ComponentInit _linker.EnsureSinkPorts(uid, telepad.ReceiverPort); } + private void OnRefreshParts(EntityUid uid, CargoTelepadComponent component, RefreshPartsEvent args) + { + var rating = args.PartRatings[component.MachinePartTeleportDelay] - 1; + component.Delay = component.BaseDelay * MathF.Pow(component.PartRatingTeleportDelay, rating); + } + + private void OnUpgradeExamine(EntityUid uid, CargoTelepadComponent component, UpgradeExamineEvent args) + { + args.AddPercentageUpgrade("cargo-telepad-delay-upgrade", component.Delay / component.BaseDelay); + } + private void SetEnabled(EntityUid uid, CargoTelepadComponent component, ApcPowerReceiverComponent? receiver = null, TransformComponent? xform = null) { diff --git a/Content.Server/Chemistry/Components/SolutionHeaterComponent.cs b/Content.Server/Chemistry/Components/SolutionHeaterComponent.cs index c1841e022c7..bc1d44e82f1 100644 --- a/Content.Server/Chemistry/Components/SolutionHeaterComponent.cs +++ b/Content.Server/Chemistry/Components/SolutionHeaterComponent.cs @@ -4,8 +4,26 @@ namespace Content.Server.Chemistry.Components; public sealed partial class SolutionHeaterComponent : Component { /// - /// How much heat is added per second to the solution, taking upgrades into account. + /// How much heat is added per second to the solution, with no upgrades. /// - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] + public float BaseHeatPerSecond = 120; + + /// + /// How much heat is added per second to the solution, taking upgrades into account. + /// + [ViewVariables(VVAccess.ReadWrite)] public float HeatPerSecond; + + /// + /// The machine part that affects the heat multiplier. + /// + [DataField] + public string MachinePartHeatMultiplier = "Capacitor"; + + /// + /// How much each upgrade multiplies the heat by. + /// + [DataField] + public float PartRatingHeatMultiplier = 1.5f; } diff --git a/Content.Server/Chemistry/EntitySystems/SolutionHeaterSystem.cs b/Content.Server/Chemistry/EntitySystems/SolutionHeaterSystem.cs index 6e6373e10bf..1ef589ab5cb 100644 --- a/Content.Server/Chemistry/EntitySystems/SolutionHeaterSystem.cs +++ b/Content.Server/Chemistry/EntitySystems/SolutionHeaterSystem.cs @@ -1,5 +1,6 @@ using Content.Server.Chemistry.Components; using Content.Server.Chemistry.Containers.EntitySystems; +using Content.Server.Construction; using Content.Server.Power.Components; using Content.Server.Power.EntitySystems; using Content.Shared.Chemistry; @@ -20,6 +21,8 @@ public override void Initialize() base.Initialize(); SubscribeLocalEvent(OnPowerChanged); + SubscribeLocalEvent(OnRefreshParts); + SubscribeLocalEvent(OnUpgradeExamine); SubscribeLocalEvent(OnItemPlaced); SubscribeLocalEvent(OnItemRemoved); } @@ -61,6 +64,18 @@ private void OnPowerChanged(Entity entity, ref PowerCha } } + private void OnRefreshParts(Entity entity, ref RefreshPartsEvent args) + { + var heatRating = args.PartRatings[entity.Comp.MachinePartHeatMultiplier] - 1; + + entity.Comp.HeatPerSecond = entity.Comp.BaseHeatPerSecond * MathF.Pow(entity.Comp.PartRatingHeatMultiplier, heatRating); + } + + private void OnUpgradeExamine(Entity entity, ref UpgradeExamineEvent args) + { + args.AddPercentageUpgrade("solution-heater-upgrade-heat", entity.Comp.HeatPerSecond / entity.Comp.BaseHeatPerSecond); + } + private void OnItemPlaced(Entity entity, ref ItemPlacedEvent args) { TryTurnOn(entity); diff --git a/Content.Server/Cloning/CloningSystem.cs b/Content.Server/Cloning/CloningSystem.cs index 7931fae4778..72104bc381f 100644 --- a/Content.Server/Cloning/CloningSystem.cs +++ b/Content.Server/Cloning/CloningSystem.cs @@ -1,6 +1,7 @@ using Content.Server.Atmos.EntitySystems; using Content.Server.Chat.Systems; using Content.Server.Cloning.Components; +using Content.Server.Construction; using Content.Server.DeviceLinking.Systems; using Content.Server.EUI; using Content.Server.Fluids.EntitySystems; @@ -9,6 +10,10 @@ using Content.Server.Materials; using Content.Server.Popups; using Content.Server.Power.EntitySystems; +using Content.Server.Traits.Assorted; +using Content.Shared.Atmos; +using Content.Shared.CCVar; +using Content.Shared.Chemistry.Components; using Content.Shared.Cloning; using Content.Shared.Damage; using Content.Shared.DeviceLinking.Events; @@ -90,8 +95,23 @@ public override void Initialize() SubscribeLocalEvent(OnExamined); SubscribeLocalEvent(OnEmagged); SubscribeLocalEvent(OnPowerChanged); + SubscribeLocalEvent(OnPartsRefreshed); + SubscribeLocalEvent(OnUpgradeExamine); + } + private void OnPartsRefreshed(EntityUid uid, CloningPodComponent component, RefreshPartsEvent args) + { + var materialRating = args.PartRatings[component.MachinePartMaterialUse]; + var speedRating = args.PartRatings[component.MachinePartCloningSpeed]; + + component.BiomassCostMultiplier = MathF.Pow(component.PartRatingMaterialMultiplier, materialRating - 1); + component.CloningTime = component.CloningTime * MathF.Pow(component.PartRatingSpeedMultiplier, speedRating - 1); } + private void OnUpgradeExamine(EntityUid uid, CloningPodComponent component, UpgradeExamineEvent args) + { + args.AddPercentageUpgrade("cloning-pod-component-upgrade-speed", component.CloningTime / component.CloningTime); + args.AddPercentageUpgrade("cloning-pod-component-upgrade-biomass-requirement", component.BiomassCostMultiplier); + } private void OnPortDisconnected(EntityUid uid, CloningPodComponent pod, PortDisconnectedEvent args) { pod.ConnectedConsole = null; diff --git a/Content.Server/Construction/ConstructionSystem.Machine.cs b/Content.Server/Construction/ConstructionSystem.Machine.cs index 2e670dbe40d..65b0b704761 100644 --- a/Content.Server/Construction/ConstructionSystem.Machine.cs +++ b/Content.Server/Construction/ConstructionSystem.Machine.cs @@ -5,6 +5,7 @@ using Content.Shared.Construction.Prototypes; using Content.Shared.Verbs; using Robust.Shared.Containers; +using Robust.Shared.Map.Components; using Robust.Shared.Utility; namespace Content.Server.Construction; @@ -143,7 +144,7 @@ private void CreateBoardAndStockParts(EntityUid uid, MachineComponent component) var p = EntityManager.SpawnEntity(partProto.StockPartPrototype, xform.Coordinates); if (!_container.Insert(p, partContainer)) - throw new Exception($"Couldn't insert machine part of type {part} to machine with prototype {partProto.StockPartPrototype}!"); + throw new Exception($"Couldn't insert machine part of type {part} to machine with prototype {partProto.StockPartPrototype ?? "N/A"}!"); } } @@ -183,7 +184,7 @@ public sealed class RefreshPartsEvent : EntityEventArgs { public IReadOnlyList Parts = new List(); - public Dictionary PartRatings = new(); + public Dictionary PartRatings = new Dictionary(); } public sealed class UpgradeExamineEvent : EntityEventArgs diff --git a/Content.Server/Construction/PartExchangerSystem.cs b/Content.Server/Construction/PartExchangerSystem.cs index ee5edcbd0a0..f364d1b547d 100644 --- a/Content.Server/Construction/PartExchangerSystem.cs +++ b/Content.Server/Construction/PartExchangerSystem.cs @@ -10,6 +10,7 @@ using Robust.Shared.Containers; using Robust.Shared.Utility; using Content.Shared.Wires; +using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; using Robust.Shared.Collections; @@ -42,7 +43,7 @@ private void OnDoAfter(EntityUid uid, PartExchangerComponent component, DoAfterE if (args.Handled || args.Args.Target == null) return; - if (!TryComp(uid, out var storage)) + if (!TryComp(uid, out var storage) || storage.Container == null) return; //the parts are stored in here var machinePartQuery = GetEntityQuery(); diff --git a/Content.Server/Gravity/GravityGeneratorComponent.cs b/Content.Server/Gravity/GravityGeneratorComponent.cs index f9462920384..f47d3979391 100644 --- a/Content.Server/Gravity/GravityGeneratorComponent.cs +++ b/Content.Server/Gravity/GravityGeneratorComponent.cs @@ -37,6 +37,9 @@ public sealed partial class GravityGeneratorComponent : SharedGravityGeneratorCo // 0 -> 1 [ViewVariables(VVAccess.ReadWrite)] [DataField("charge")] public float Charge { get; set; } = 1; + [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))] + public string MachinePartMaxChargeMultiplier = "Capacitor"; + /// /// Is the gravity generator currently "producing" gravity? /// diff --git a/Content.Server/Gravity/GravityGeneratorSystem.cs b/Content.Server/Gravity/GravityGeneratorSystem.cs index ec5646457e2..b1696e6a713 100644 --- a/Content.Server/Gravity/GravityGeneratorSystem.cs +++ b/Content.Server/Gravity/GravityGeneratorSystem.cs @@ -1,5 +1,6 @@ using Content.Server.Administration.Logs; using Content.Server.Audio; +using Content.Server.Construction; using Content.Server.Power.Components; using Content.Server.Emp; using Content.Shared.Database; @@ -27,6 +28,7 @@ public override void Initialize() SubscribeLocalEvent(OnComponentShutdown); SubscribeLocalEvent(OnParentChanged); // Or just anchor changed? SubscribeLocalEvent(OnInteractHand); + SubscribeLocalEvent(OnRefreshParts); SubscribeLocalEvent( OnSwitchGenerator); @@ -257,6 +259,12 @@ public void UpdateState(Entity ent, AppearanceComponent? appearance) { _ambientSoundSystem.SetAmbience(ent, false); diff --git a/Content.Server/Kitchen/Components/MicrowaveComponent.cs b/Content.Server/Kitchen/Components/MicrowaveComponent.cs index 815ba8f5213..1e343e5e332 100644 --- a/Content.Server/Kitchen/Components/MicrowaveComponent.cs +++ b/Content.Server/Kitchen/Components/MicrowaveComponent.cs @@ -11,95 +11,99 @@ namespace Content.Server.Kitchen.Components [RegisterComponent] public sealed partial class MicrowaveComponent : Component { - [DataField("cookTimeMultiplier"), ViewVariables(VVAccess.ReadWrite)] + [DataField] public float CookTimeMultiplier = 1; - - [DataField("baseHeatMultiplier"), ViewVariables(VVAccess.ReadWrite)] + [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))] + public string MachinePartCookTimeMultiplier = "Capacitor"; + [DataField] + public float CookTimeScalingConstant = 0.5f; + [DataField] public float BaseHeatMultiplier = 100; - [DataField("objectHeatMultiplier"), ViewVariables(VVAccess.ReadWrite)] + [DataField] public float ObjectHeatMultiplier = 100; - [DataField("failureResult", customTypeSerializer: typeof(PrototypeIdSerializer))] + [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))] public string BadRecipeEntityId = "FoodBadRecipe"; #region audio - [DataField("beginCookingSound")] + [DataField] public SoundSpecifier StartCookingSound = new SoundPathSpecifier("/Audio/Machines/microwave_start_beep.ogg"); - [DataField("foodDoneSound")] + [DataField] public SoundSpecifier FoodDoneSound = new SoundPathSpecifier("/Audio/Machines/microwave_done_beep.ogg"); - [DataField("clickSound")] + [DataField] public SoundSpecifier ClickSound = new SoundPathSpecifier("/Audio/Machines/machine_switch.ogg"); - [DataField("ItemBreakSound")] + [DataField] public SoundSpecifier ItemBreakSound = new SoundPathSpecifier("/Audio/Effects/clang.ogg"); public EntityUid? PlayingStream; - [DataField("loopingSound")] + [DataField] public SoundSpecifier LoopingSound = new SoundPathSpecifier("/Audio/Machines/microwave_loop.ogg"); #endregion [ViewVariables] public bool Broken; - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] public ProtoId OnPort = "On"; /// - /// This is a fixed offset of 5. - /// The cook times for all recipes should be divisible by 5,with a minimum of 1 second. - /// For right now, I don't think any recipe cook time should be greater than 60 seconds. + /// This is a fixed offset of 5. + /// The cook times for all recipes should be divisible by 5,with a minimum of 1 second. + /// For right now, I don't think any recipe cook time should be greater than 60 seconds. /// - [DataField("currentCookTimerTime"), ViewVariables(VVAccess.ReadWrite)] + [DataField] public uint CurrentCookTimerTime = 0; /// - /// Tracks the elapsed time of the current cook timer. + /// Tracks the elapsed time of the current cook timer. /// - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] public TimeSpan CurrentCookTimeEnd = TimeSpan.Zero; /// - /// The maximum number of seconds a microwave can be set to. - /// This is currently only used for validation and the client does not check this. + /// The maximum number of seconds a microwave can be set to. + /// This is currently only used for validation and the client does not check this. /// - [DataField("maxCookTime"), ViewVariables(VVAccess.ReadWrite)] + [DataField] public uint MaxCookTime = 30; /// /// The max temperature that this microwave can heat objects to. /// - [DataField("temperatureUpperThreshold")] + [DataField] public float TemperatureUpperThreshold = 373.15f; public int CurrentCookTimeButtonIndex; public Container Storage = default!; - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] public int Capacity = 10; - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] public ProtoId MaxItemSize = "Normal"; /// - /// How frequently the microwave can malfunction. + /// How frequently the microwave can malfunction. /// [DataField] public float MalfunctionInterval = 1.0f; /// - /// Chance of an explosion occurring when we microwave a metallic object + /// Chance of an explosion occurring when we microwave a metallic object /// - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] public float ExplosionChance = .1f; /// - /// Chance of lightning occurring when we microwave a metallic object - [DataField, ViewVariables(VVAccess.ReadWrite)] + /// Chance of lightning occurring when we microwave a metallic object + /// + [DataField] public float LightningChance = .75f; } diff --git a/Content.Server/Kitchen/Components/ReagentGrinderComponent.cs b/Content.Server/Kitchen/Components/ReagentGrinderComponent.cs index 5bbbe2dc8da..4f4531206c7 100644 --- a/Content.Server/Kitchen/Components/ReagentGrinderComponent.cs +++ b/Content.Server/Kitchen/Components/ReagentGrinderComponent.cs @@ -1,6 +1,8 @@ using Content.Shared.Kitchen; using Content.Server.Kitchen.EntitySystems; +using Content.Shared.Construction.Prototypes; using Robust.Shared.Audio; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; namespace Content.Server.Kitchen.Components { @@ -13,15 +15,30 @@ namespace Content.Server.Kitchen.Components [Access(typeof(ReagentGrinderSystem)), RegisterComponent] public sealed partial class ReagentGrinderComponent : Component { - [DataField] + [ViewVariables(VVAccess.ReadWrite)] public int StorageMaxEntities = 6; [DataField] - public TimeSpan WorkTime = TimeSpan.FromSeconds(3.5); // Roughly matches the grind/juice sounds. + public int BaseStorageMaxEntities = 4; + + [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))] + public string MachinePartStorageMax = "MatterBin"; + + [DataField] + public int StoragePerPartRating = 4; [DataField] + public TimeSpan WorkTime = TimeSpan.FromSeconds(3.5); // Roughly matches the grind/juice sounds. + + [ViewVariables(VVAccess.ReadWrite)] public float WorkTimeMultiplier = 1; + [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))] + public string MachinePartWorkTime = "Manipulator"; + + [DataField] + public float PartRatingWorkTimerMulitplier = 0.6f; + [DataField] public SoundSpecifier ClickSound { get; set; } = new SoundPathSpecifier("/Audio/Machines/machine_switch.ogg"); diff --git a/Content.Server/Kitchen/EntitySystems/MicrowaveSystem.cs b/Content.Server/Kitchen/EntitySystems/MicrowaveSystem.cs index 212383c463a..3de7051f54d 100644 --- a/Content.Server/Kitchen/EntitySystems/MicrowaveSystem.cs +++ b/Content.Server/Kitchen/EntitySystems/MicrowaveSystem.cs @@ -76,6 +76,8 @@ public override void Initialize() SubscribeLocalEvent(OnPowerChanged); SubscribeLocalEvent(OnAnchorChanged); SubscribeLocalEvent(OnSuicide); + SubscribeLocalEvent(OnRefreshParts); + SubscribeLocalEvent(OnUpgradeExamine); SubscribeLocalEvent(OnSignalReceived); @@ -342,6 +344,17 @@ private void OnAnchorChanged(EntityUid uid, MicrowaveComponent component, ref An _container.EmptyContainer(component.Storage); } + private void OnRefreshParts(Entity ent, ref RefreshPartsEvent args) + { + var cookRating = args.PartRatings[ent.Comp.MachinePartCookTimeMultiplier]; + ent.Comp.CookTimeMultiplier = MathF.Pow(ent.Comp.CookTimeScalingConstant, cookRating - 1); + } + + private void OnUpgradeExamine(Entity ent, ref UpgradeExamineEvent args) + { + args.AddPercentageUpgrade("microwave-component-upgrade-cook-time", ent.Comp.CookTimeMultiplier); + } + private void OnSignalReceived(Entity ent, ref SignalReceivedEvent args) { if (args.Port != ent.Comp.OnPort) diff --git a/Content.Server/Kitchen/EntitySystems/ReagentGrinderSystem.cs b/Content.Server/Kitchen/EntitySystems/ReagentGrinderSystem.cs index e8ee4539860..aad33fea678 100644 --- a/Content.Server/Kitchen/EntitySystems/ReagentGrinderSystem.cs +++ b/Content.Server/Kitchen/EntitySystems/ReagentGrinderSystem.cs @@ -1,4 +1,5 @@ using Content.Server.Chemistry.Containers.EntitySystems; +using Content.Server.Construction; using Content.Server.Kitchen.Components; using Content.Server.Power.Components; using Content.Server.Power.EntitySystems; @@ -48,6 +49,8 @@ public override void Initialize() SubscribeLocalEvent((uid, _, _) => UpdateUiState(uid)); SubscribeLocalEvent((EntityUid uid, ReagentGrinderComponent _, ref PowerChangedEvent _) => UpdateUiState(uid)); SubscribeLocalEvent(OnInteractUsing); + SubscribeLocalEvent(OnRefreshParts); + SubscribeLocalEvent(OnUpgradeExamine); SubscribeLocalEvent(OnContainerModified); SubscribeLocalEvent(OnContainerModified); @@ -197,6 +200,24 @@ private void OnInteractUsing(Entity entity, ref Interac args.Handled = true; } + /// + /// Gotta be efficient, you know? you're saving a whole extra second here and everything. + /// + private void OnRefreshParts(Entity entity, ref RefreshPartsEvent args) + { + var ratingWorkTime = args.PartRatings[entity.Comp.MachinePartWorkTime]; + var ratingStorage = args.PartRatings[entity.Comp.MachinePartStorageMax]; + + entity.Comp.WorkTimeMultiplier = MathF.Pow(entity.Comp.PartRatingWorkTimerMulitplier, ratingWorkTime - 1); + entity.Comp.StorageMaxEntities = entity.Comp.BaseStorageMaxEntities + (int) (entity.Comp.StoragePerPartRating * (ratingStorage - 1)); + } + + private void OnUpgradeExamine(Entity entity, ref UpgradeExamineEvent args) + { + args.AddPercentageUpgrade("reagent-grinder-component-upgrade-work-time", entity.Comp.WorkTimeMultiplier); + args.AddNumberUpgrade("reagent-grinder-component-upgrade-storage", entity.Comp.StorageMaxEntities - entity.Comp.BaseStorageMaxEntities); + } + private void UpdateUiState(EntityUid uid) { ReagentGrinderComponent? grinderComp = null; diff --git a/Content.Server/Materials/MaterialReclaimerSystem.cs b/Content.Server/Materials/MaterialReclaimerSystem.cs index aa24fde44b7..de82f125985 100644 --- a/Content.Server/Materials/MaterialReclaimerSystem.cs +++ b/Content.Server/Materials/MaterialReclaimerSystem.cs @@ -1,4 +1,6 @@ using Content.Server.Chemistry.Containers.EntitySystems; +using Content.Server.Chemistry.EntitySystems; +using Content.Server.Construction; using Content.Server.Fluids.EntitySystems; using Content.Server.GameTicking; using Content.Server.Popups; @@ -45,6 +47,8 @@ public override void Initialize() base.Initialize(); SubscribeLocalEvent(OnStartup); + SubscribeLocalEvent(OnRefreshParts); + SubscribeLocalEvent(OnUpgradeExamine); SubscribeLocalEvent(OnPowerChanged); SubscribeLocalEvent(OnInteractUsing, before: new []{typeof(WiresSystem), typeof(SolutionTransferSystem)}); @@ -56,6 +60,18 @@ private void OnStartup(Entity entity, ref ComponentS _solutionContainer.EnsureSolution(entity.Owner, entity.Comp.SolutionContainerId); } + private void OnUpgradeExamine(Entity entity, ref UpgradeExamineEvent args) + { + args.AddPercentageUpgrade(Loc.GetString("material-reclaimer-upgrade-process-rate"), entity.Comp.MaterialProcessRate / entity.Comp.BaseMaterialProcessRate); + } + + private void OnRefreshParts(Entity entity, ref RefreshPartsEvent args) + { + var rating = args.PartRatings[entity.Comp.MachinePartProcessRate] - 1; + entity.Comp.MaterialProcessRate = entity.Comp.BaseMaterialProcessRate * MathF.Pow(entity.Comp.PartRatingProcessRateMultiplier, rating); + Dirty(entity); + } + private void OnPowerChanged(Entity entity, ref PowerChangedEvent args) { AmbientSound.SetAmbience(entity.Owner, entity.Comp.Enabled && args.Powered); diff --git a/Content.Server/Medical/BiomassReclaimer/BiomassReclaimerComponent.cs b/Content.Server/Medical/BiomassReclaimer/BiomassReclaimerComponent.cs index 61d36f98b96..1358bfbcbbc 100644 --- a/Content.Server/Medical/BiomassReclaimer/BiomassReclaimerComponent.cs +++ b/Content.Server/Medical/BiomassReclaimer/BiomassReclaimerComponent.cs @@ -1,4 +1,8 @@ +using System.Threading; +using Content.Shared.Construction.Prototypes; using Content.Shared.Storage; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; namespace Content.Server.Medical.BiomassReclaimer { @@ -6,72 +10,111 @@ namespace Content.Server.Medical.BiomassReclaimer public sealed partial class BiomassReclaimerComponent : Component { /// - /// This gets set for each mob it processes. - /// When it hits 0, there is a chance for the reclaimer to either spill blood or throw an item. + /// This gets set for each mob it processes. + /// When it hits 0, there is a chance for the reclaimer to either spill blood or throw an item. /// [ViewVariables] public float RandomMessTimer = 0f; /// - /// The interval for . + /// The interval for . /// - [ViewVariables(VVAccess.ReadWrite), DataField] + [DataField] public TimeSpan RandomMessInterval = TimeSpan.FromSeconds(5); /// - /// This gets set for each mob it processes. - /// When it hits 0, spit out biomass. + /// This gets set for each mob it processes. + /// When it hits 0, spit out biomass. /// [ViewVariables] - public float ProcessingTimer = default; + public float ProcessingTimer; /// - /// Amount of biomass that the mob being processed will yield. - /// This is calculated from the YieldPerUnitMass. - /// Also stores non-integer leftovers. + /// Amount of biomass that the mob being processed will yield. + /// This is calculated from the YieldPerUnitMass. + /// Also stores non-integer leftovers. /// [ViewVariables] - public float CurrentExpectedYield = 0f; + public float CurrentExpectedYield; /// - /// The reagent that will be spilled while processing a mob. + /// The reagent that will be spilled while processing a mob. /// [ViewVariables] public string? BloodReagent; /// - /// Entities that can be randomly spawned while processing a mob. + /// Entities that can be randomly spawned while processing a mob. /// public List SpawnedEntities = new(); /// - /// How many units of biomass it produces for each unit of mass. + /// How many units of biomass it produces for each unit of mass. /// - [DataField, ViewVariables(VVAccess.ReadWrite)] - public float YieldPerUnitMass = 0.4f; + [ViewVariables(VVAccess.ReadWrite)] + public float YieldPerUnitMass = default; /// - /// How many seconds to take to insert an entity per unit of its mass. + /// The base yield per mass unit when no components are upgraded. /// - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] + public float BaseYieldPerUnitMass = 0.4f; + + /// + /// Machine part whose rating modifies the yield per mass. + /// + [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))] + public string MachinePartYieldAmount = "MatterBin"; + + /// + /// How much the machine part quality affects the yield. + /// Going up a tier will multiply the yield by this amount. + /// + [DataField] + public float PartRatingYieldAmountMultiplier = 1.25f; + + /// + /// How many seconds to take to insert an entity per unit of its mass. + /// + [DataField] public float BaseInsertionDelay = 0.1f; /// - /// How much to multiply biomass yield from botany produce. + /// How much to multiply biomass yield from botany produce. /// - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] public float ProduceYieldMultiplier = 0.25f; /// - /// The time it takes to process a mob, per mass. + /// The time it takes to process a mob, per mass. + /// + [ViewVariables(VVAccess.ReadWrite)] + public float ProcessingTimePerUnitMass; + + /// + /// The base time per mass unit that it takes to process a mob + /// when no components are upgraded. + /// + [DataField] + public float BaseProcessingTimePerUnitMass = 0.5f; + + /// + /// The machine part that increses the processing speed. + /// + [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))] + public string MachinePartProcessingSpeed = "Manipulator"; + + /// + /// How much the machine part quality affects the yield. + /// Going up a tier will multiply the speed by this amount. /// - [DataField, ViewVariables(VVAccess.ReadWrite)] - public float ProcessingTimePerUnitMass = 0.5f; + [DataField] + public float PartRatingSpeedMultiplier = 1.35f; /// - /// Will this refuse to gib a living mob? + /// Will this refuse to gib a living mob? /// - [ViewVariables(VVAccess.ReadWrite), DataField] + [DataField] public bool SafetyEnabled = true; } } diff --git a/Content.Server/Medical/BiomassReclaimer/BiomassReclaimerSystem.cs b/Content.Server/Medical/BiomassReclaimer/BiomassReclaimerSystem.cs index eaf04d64b2b..97a758a5ed3 100644 --- a/Content.Server/Medical/BiomassReclaimer/BiomassReclaimerSystem.cs +++ b/Content.Server/Medical/BiomassReclaimer/BiomassReclaimerSystem.cs @@ -1,6 +1,7 @@ using System.Numerics; using Content.Server.Body.Components; using Content.Server.Botany.Components; +using Content.Server.Construction; using Content.Server.Fluids.EntitySystems; using Content.Server.Materials; using Content.Server.Power.Components; @@ -99,6 +100,8 @@ public override void Initialize() SubscribeLocalEvent(OnUnanchorAttempt); SubscribeLocalEvent(OnAfterInteractUsing); SubscribeLocalEvent(OnClimbedOn); + SubscribeLocalEvent(OnRefreshParts); + SubscribeLocalEvent(OnUpgradeExamine); SubscribeLocalEvent(OnPowerChanged); SubscribeLocalEvent(OnSuicide); SubscribeLocalEvent(OnDoAfter); @@ -173,6 +176,26 @@ private void OnClimbedOn(Entity reclaimer, ref Climbe StartProcessing(args.Climber, reclaimer); } + private void OnRefreshParts(EntityUid uid, BiomassReclaimerComponent component, RefreshPartsEvent args) + { + var laserRating = args.PartRatings[component.MachinePartProcessingSpeed]; + var manipRating = args.PartRatings[component.MachinePartYieldAmount]; + + // Processing time slopes downwards with part rating. + component.ProcessingTimePerUnitMass = + component.BaseProcessingTimePerUnitMass / MathF.Pow(component.PartRatingSpeedMultiplier, laserRating - 1); + + // Yield slopes upwards with part rating. + component.YieldPerUnitMass = + component.BaseYieldPerUnitMass * MathF.Pow(component.PartRatingYieldAmountMultiplier, manipRating - 1); + } + + private void OnUpgradeExamine(EntityUid uid, BiomassReclaimerComponent component, UpgradeExamineEvent args) + { + args.AddPercentageUpgrade("biomass-reclaimer-component-upgrade-speed", component.BaseProcessingTimePerUnitMass / component.ProcessingTimePerUnitMass); + args.AddPercentageUpgrade("biomass-reclaimer-component-upgrade-biomass-yield", component.YieldPerUnitMass / component.BaseYieldPerUnitMass); + } + private void OnDoAfter(Entity reclaimer, ref ReclaimerDoAfterEvent args) { if (args.Handled diff --git a/Content.Server/Medical/Components/MedicalScannerComponent.cs b/Content.Server/Medical/Components/MedicalScannerComponent.cs index 96de6499875..15ca6cd2bd7 100644 --- a/Content.Server/Medical/Components/MedicalScannerComponent.cs +++ b/Content.Server/Medical/Components/MedicalScannerComponent.cs @@ -1,5 +1,4 @@ using Content.Shared.Construction.Prototypes; -using Content.Shared.DragDrop; using Content.Shared.MedicalScanner; using Robust.Shared.Containers; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; @@ -13,10 +12,15 @@ public sealed partial class MedicalScannerComponent : SharedMedicalScannerCompon public ContainerSlot BodyContainer = default!; public EntityUid? ConnectedConsole; - [DataField, ViewVariables(VVAccess.ReadWrite)] + [ViewVariables(VVAccess.ReadWrite)] public float CloningFailChanceMultiplier = 1f; - - // Nyano, needed for Metem Machine. + public float MetemKarmaBonus = 0.25f; + + [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))] + public string MachinePartCloningFailChance = "Capacitor"; + + [DataField] + public float PartRatingFailMultiplier = 0.75f; } } diff --git a/Content.Server/Medical/MedicalScannerSystem.cs b/Content.Server/Medical/MedicalScannerSystem.cs index a6ce43c4081..ab6918e373b 100644 --- a/Content.Server/Medical/MedicalScannerSystem.cs +++ b/Content.Server/Medical/MedicalScannerSystem.cs @@ -7,6 +7,7 @@ using Content.Shared.Verbs; using Robust.Shared.Containers; using Content.Server.Cloning.Components; +using Content.Server.Construction; using Content.Server.DeviceLinking.Systems; using Content.Shared.DeviceLinking.Events; using Content.Server.Power.EntitySystems; @@ -44,6 +45,8 @@ public override void Initialize() SubscribeLocalEvent(OnDragDropOn); SubscribeLocalEvent(OnPortDisconnected); SubscribeLocalEvent(OnAnchorChanged); + SubscribeLocalEvent(OnRefreshParts); + SubscribeLocalEvent(OnUpgradeExamine); SubscribeLocalEvent(OnCanDragDropOn); } @@ -220,5 +223,17 @@ public void EjectBody(EntityUid uid, MedicalScannerComponent? scannerComponent) _climbSystem.ForciblySetClimbing(contained, uid); UpdateAppearance(uid, scannerComponent); } + + private void OnRefreshParts(EntityUid uid, MedicalScannerComponent component, RefreshPartsEvent args) + { + var ratingFail = args.PartRatings[component.MachinePartCloningFailChance]; + + component.CloningFailChanceMultiplier = MathF.Pow(component.PartRatingFailMultiplier, ratingFail - 1); + } + + private void OnUpgradeExamine(EntityUid uid, MedicalScannerComponent component, UpgradeExamineEvent args) + { + args.AddPercentageUpgrade("medical-scanner-upgrade-cloning", component.CloningFailChanceMultiplier); + } } } diff --git a/Content.Server/Nutrition/Components/FatExtractorComponent.cs b/Content.Server/Nutrition/Components/FatExtractorComponent.cs index e23c557236c..fa6edc911e1 100644 --- a/Content.Server/Nutrition/Components/FatExtractorComponent.cs +++ b/Content.Server/Nutrition/Components/FatExtractorComponent.cs @@ -1,4 +1,5 @@ using Content.Server.Nutrition.EntitySystems; +using Content.Shared.Construction.Prototypes; using Content.Shared.Nutrition.Components; using Robust.Shared.Audio; using Robust.Shared.Prototypes; @@ -8,67 +9,87 @@ namespace Content.Server.Nutrition.Components; /// -/// This is used for a machine that extracts hunger from entities and creates meat. Yum! +/// This is used for a machine that extracts hunger from entities and creates meat. Yum! /// [RegisterComponent, Access(typeof(FatExtractorSystem)), AutoGenerateComponentPause] public sealed partial class FatExtractorComponent : Component { /// - /// Whether or not the extractor is currently extracting fat from someone + /// Whether or not the extractor is currently extracting fat from someone /// - [DataField("processing")] + [DataField] public bool Processing = true; /// - /// How much nutrition is extracted per second. + /// How much nutrition is extracted per second. /// - [DataField("nutritionPerSecond"), ViewVariables(VVAccess.ReadWrite)] + [ViewVariables(VVAccess.ReadWrite)] public int NutritionPerSecond = 10; /// - /// An accumulator which tracks extracted nutrition to determine - /// when to spawn a meat. + /// The base rate of extraction /// - [DataField("nutrientAccumulator"), ViewVariables(VVAccess.ReadWrite)] + [DataField] + public int BaseNutritionPerSecond = 10; + + #region Machine Upgrade + /// + /// Which machine part affects the nutrition rate + /// + [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))] + public string MachinePartNutritionRate = "Manipulator"; + + /// + /// The increase in rate per each rating above 1. + /// + [DataField] + public float PartRatingRateMultiplier = 10; + #endregion + + /// + /// An accumulator which tracks extracted nutrition to determine + /// when to spawn a meat. + /// + [DataField] public int NutrientAccumulator; /// - /// How high has to be to spawn meat + /// How high has to be to spawn meat /// - [DataField("nutrientPerMeat"), ViewVariables(VVAccess.ReadWrite)] - public int NutrientPerMeat = 30; + [DataField] + public int NutrientPerMeat = 60; /// - /// Meat spawned by the extractor. + /// Meat spawned by the extractor. /// - [DataField("meatPrototype", customTypeSerializer: typeof(PrototypeIdSerializer)), ViewVariables(VVAccess.ReadWrite)] + [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))] public string MeatPrototype = "FoodMeat"; /// - /// When the next update will occur + /// When the next update will occur /// - [DataField("nextUpdate", customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite)] + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] [AutoPausedField] public TimeSpan NextUpdate; /// - /// How long each update takes + /// How long each update takes /// - [DataField("updateTime"), ViewVariables(VVAccess.ReadWrite)] + [DataField] public TimeSpan UpdateTime = TimeSpan.FromSeconds(1); /// - /// The sound played when extracting + /// The sound played when extracting /// - [DataField("processSound")] + [DataField] public SoundSpecifier? ProcessSound; public EntityUid? Stream; /// - /// A minium hunger threshold for extracting nutrition. - /// Ignored when emagged. + /// A minium hunger threshold for extracting nutrition. + /// Ignored when emagged. /// - [DataField("minHungerThreshold")] + [DataField] public HungerThreshold MinHungerThreshold = HungerThreshold.Okay; } diff --git a/Content.Server/Nutrition/EntitySystems/FatExtractorSystem.cs b/Content.Server/Nutrition/EntitySystems/FatExtractorSystem.cs index 180e40d1e42..dc1f67c7400 100644 --- a/Content.Server/Nutrition/EntitySystems/FatExtractorSystem.cs +++ b/Content.Server/Nutrition/EntitySystems/FatExtractorSystem.cs @@ -1,5 +1,6 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; +using Content.Server.Construction; using Content.Server.Nutrition.Components; using Content.Server.Power.Components; using Content.Server.Power.EntitySystems; @@ -9,6 +10,7 @@ using Content.Shared.Nutrition.Components; using Content.Shared.Nutrition.EntitySystems; using Content.Shared.Storage.Components; +using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; using Robust.Shared.Timing; @@ -27,12 +29,25 @@ public sealed class FatExtractorSystem : EntitySystem /// public override void Initialize() { + SubscribeLocalEvent(OnRefreshParts); + SubscribeLocalEvent(OnUpgradeExamine); SubscribeLocalEvent(OnGotEmagged); SubscribeLocalEvent(OnClosed); SubscribeLocalEvent(OnOpen); SubscribeLocalEvent(OnPowerChanged); } + private void OnRefreshParts(EntityUid uid, FatExtractorComponent component, RefreshPartsEvent args) + { + var rating = args.PartRatings[component.MachinePartNutritionRate] - 1; + component.NutritionPerSecond = component.BaseNutritionPerSecond + (int) (component.PartRatingRateMultiplier * rating); + } + + private void OnUpgradeExamine(EntityUid uid, FatExtractorComponent component, UpgradeExamineEvent args) + { + args.AddPercentageUpgrade("fat-extractor-component-rate", (float) component.NutritionPerSecond / component.BaseNutritionPerSecond); + } + private void OnGotEmagged(EntityUid uid, FatExtractorComponent component, ref GotEmaggedEvent args) { args.Handled = true; diff --git a/Content.Server/Power/Components/UpgradeBatteryComponent.cs b/Content.Server/Power/Components/UpgradeBatteryComponent.cs new file mode 100644 index 00000000000..b676883b711 --- /dev/null +++ b/Content.Server/Power/Components/UpgradeBatteryComponent.cs @@ -0,0 +1,28 @@ +using Content.Shared.Construction.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; + +namespace Content.Server.Power.Components +{ + + [RegisterComponent] + public sealed partial class UpgradeBatteryComponent : Component + { + /// + /// The machine part that affects the power capacity. + /// + [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))] + public string MachinePartPowerCapacity = "PowerCell"; + + /// + /// The machine part rating is raised to this power when calculating power gain + /// + [DataField] + public float MaxChargeMultiplier = 2f; + + /// + /// Power gain scaling + /// + [DataField] + public float BaseMaxCharge = 8000000; + } +} diff --git a/Content.Server/Power/Components/UpgradePowerDrawComponent.cs b/Content.Server/Power/Components/UpgradePowerDrawComponent.cs new file mode 100644 index 00000000000..23db4905cc5 --- /dev/null +++ b/Content.Server/Power/Components/UpgradePowerDrawComponent.cs @@ -0,0 +1,41 @@ +using Content.Server.Construction.Components; +using Content.Shared.Construction.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; + +namespace Content.Server.Power.Components; + +/// +/// This is used for machines whose power draw +/// can be decreased through machine part upgrades. +/// +[RegisterComponent] +public sealed partial class UpgradePowerDrawComponent : Component +{ + /// + /// The base power draw of the machine. + /// Prioritizes hv/mv draw over lv draw. + /// Value is initializezd on map init from + /// + [ViewVariables(VVAccess.ReadWrite)] + public float BaseLoad; + + /// + /// The machine part that affects the power draw. + /// + [DataField(customTypeSerializer: typeof(PrototypeIdSerializer)), ViewVariables(VVAccess.ReadWrite)] + public string MachinePartPowerDraw = "Capacitor"; + + /// + /// The multiplier used for scaling the power draw. + /// + [DataField(required: true), ViewVariables(VVAccess.ReadWrite)] + public float PowerDrawMultiplier = 1f; + + /// + /// What type of scaling is being used? + /// + [DataField(required: true), ViewVariables(VVAccess.ReadWrite)] + public MachineUpgradeScalingType Scaling; +} + + diff --git a/Content.Server/Power/Components/UpgradePowerSupplierComponent.cs b/Content.Server/Power/Components/UpgradePowerSupplierComponent.cs new file mode 100644 index 00000000000..012c38a6b90 --- /dev/null +++ b/Content.Server/Power/Components/UpgradePowerSupplierComponent.cs @@ -0,0 +1,36 @@ +using Content.Server.Construction.Components; +using Content.Shared.Construction.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; + +namespace Content.Server.Power.Components; + +[RegisterComponent] +public sealed partial class UpgradePowerSupplierComponent : Component +{ + [ViewVariables(VVAccess.ReadWrite)] + public float BaseSupplyRate; + + /// + /// The machine part that affects the power supplu. + /// + [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))] + public string MachinePartPowerSupply = "Capacitor"; + + /// + /// The multiplier used for scaling the power supply. + /// + [DataField(required: true)] + public float PowerSupplyMultiplier = 1f; + + /// + /// What type of scaling is being used? + /// + [DataField(required: true)] + public MachineUpgradeScalingType Scaling; + + /// + /// The current value that the power supply is being scaled by, + /// + [DataField] + public float ActualScalar = 1f; +} diff --git a/Content.Server/Power/Components/UpgradePowerSupplyRampingComponent.cs b/Content.Server/Power/Components/UpgradePowerSupplyRampingComponent.cs new file mode 100644 index 00000000000..61a654b383b --- /dev/null +++ b/Content.Server/Power/Components/UpgradePowerSupplyRampingComponent.cs @@ -0,0 +1,36 @@ +using Content.Server.Construction.Components; +using Content.Shared.Construction.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; + +namespace Content.Server.Power.Components; + +[RegisterComponent] +public sealed partial class UpgradePowerSupplyRampingComponent : Component +{ + [ViewVariables(VVAccess.ReadWrite)] + public float BaseRampRate; + + /// + /// The machine part that affects the power supply ramping + /// + [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))] + public string MachinePartRampRate = "Capacitor"; + + /// + /// The multiplier used for scaling the power supply ramping + /// + [DataField] + public float SupplyRampingMultiplier = 1f; + + /// + /// What type of scaling is being used? + /// + [DataField(required: true)] + public MachineUpgradeScalingType Scaling; + + /// + /// The current value that the power supply is being scaled by + /// + [DataField] + public float ActualScalar = 1f; +} diff --git a/Content.Server/Power/EntitySystems/UpgradeBatterySystem.cs b/Content.Server/Power/EntitySystems/UpgradeBatterySystem.cs new file mode 100644 index 00000000000..734cf9d89ce --- /dev/null +++ b/Content.Server/Power/EntitySystems/UpgradeBatterySystem.cs @@ -0,0 +1,39 @@ +using Content.Server.Construction; +using Content.Server.Power.Components; +using JetBrains.Annotations; + +namespace Content.Server.Power.EntitySystems +{ + [UsedImplicitly] + public sealed class UpgradeBatterySystem : EntitySystem + { + [Dependency] private readonly BatterySystem _batterySystem = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnRefreshParts); + SubscribeLocalEvent(OnUpgradeExamine); + } + + public void OnRefreshParts(EntityUid uid, UpgradeBatteryComponent component, RefreshPartsEvent args) + { + var powerCellRating = args.PartRatings[component.MachinePartPowerCapacity]; + + if (TryComp(uid, out var batteryComp)) + { + _batterySystem.SetMaxCharge(uid, MathF.Pow(component.MaxChargeMultiplier, powerCellRating - 1) * component.BaseMaxCharge, batteryComp); + } + } + + private void OnUpgradeExamine(EntityUid uid, UpgradeBatteryComponent component, UpgradeExamineEvent args) + { + // UpgradeBatteryComponent.MaxChargeMultiplier is not the actual multiplier, so we have to do this. + if (TryComp(uid, out var batteryComp)) + { + args.AddPercentageUpgrade("upgrade-max-charge", batteryComp.MaxCharge / component.BaseMaxCharge); + } + } + } +} diff --git a/Content.Server/Power/EntitySystems/UpgradePowerSystem.cs b/Content.Server/Power/EntitySystems/UpgradePowerSystem.cs new file mode 100644 index 00000000000..d2f6ee4f568 --- /dev/null +++ b/Content.Server/Power/EntitySystems/UpgradePowerSystem.cs @@ -0,0 +1,151 @@ +using Content.Server.Construction; +using Content.Server.Construction.Components; +using Content.Server.Power.Components; + +namespace Content.Server.Power.EntitySystems; + +/// +/// This handles using upgraded machine parts +/// to modify the power supply/generation of a machine. +/// +public sealed class UpgradePowerSystem : EntitySystem +{ + /// + public override void Initialize() + { + SubscribeLocalEvent(OnMapInit); + SubscribeLocalEvent(OnRefreshParts); + SubscribeLocalEvent(OnUpgradeExamine); + + SubscribeLocalEvent(OnSupplierMapInit); + SubscribeLocalEvent(OnSupplierRefreshParts); + SubscribeLocalEvent(OnSupplierUpgradeExamine); + + SubscribeLocalEvent(OnSupplyRampingMapInit); + SubscribeLocalEvent(OnSupplyRampingRefreshParts); + SubscribeLocalEvent(OnSupplyRampingUpgradeExamine); + } + + private void OnMapInit(EntityUid uid, UpgradePowerDrawComponent component, MapInitEvent args) + { + if (TryComp(uid, out var powa)) + component.BaseLoad = powa.DrawRate; + else if (TryComp(uid, out var powa2)) + component.BaseLoad = powa2.Load; + } + + private void OnRefreshParts(EntityUid uid, UpgradePowerDrawComponent component, RefreshPartsEvent args) + { + var load = component.BaseLoad; + var rating = args.PartRatings[component.MachinePartPowerDraw]; + switch (component.Scaling) + + { + case MachineUpgradeScalingType.Linear: + load += component.PowerDrawMultiplier * (rating - 1); + break; + case MachineUpgradeScalingType.Exponential: + load *= MathF.Pow(component.PowerDrawMultiplier, rating - 1); + break; + default: + Log.Error($"invalid power scaling type for {ToPrettyString(uid)}."); + load = 0; + break; + } + + if (TryComp(uid, out var powa)) + powa.Load = load; + if (TryComp(uid, out var powa2)) + powa2.DrawRate = load; + } + + private void OnUpgradeExamine(EntityUid uid, UpgradePowerDrawComponent component, UpgradeExamineEvent args) + { + // UpgradePowerDrawComponent.PowerDrawMultiplier is not the actual multiplier, so we have to do this. + var powerDrawMultiplier = CompOrNull(uid)?.Load / component.BaseLoad + ?? CompOrNull(uid)?.DrawRate / component.BaseLoad; + + if (powerDrawMultiplier is not null) + args.AddPercentageUpgrade("upgrade-power-draw", powerDrawMultiplier.Value); + } + + private void OnSupplierMapInit(EntityUid uid, UpgradePowerSupplierComponent component, MapInitEvent args) + { + if (!TryComp(uid, out var supplier)) + return; + + component.BaseSupplyRate = supplier.MaxSupply; + } + + private void OnSupplierRefreshParts(EntityUid uid, UpgradePowerSupplierComponent component, RefreshPartsEvent args) + { + var supply = component.BaseSupplyRate; + var rating = args.PartRatings[component.MachinePartPowerSupply]; + switch (component.Scaling) + + { + case MachineUpgradeScalingType.Linear: + supply += component.PowerSupplyMultiplier * component.BaseSupplyRate * (rating - 1); + break; + case MachineUpgradeScalingType.Exponential: + supply *= MathF.Pow(component.PowerSupplyMultiplier, rating - 1); + break; + default: + Log.Error($"invalid power scaling type for {ToPrettyString(uid)}."); + supply = component.BaseSupplyRate; + break; + } + + component.ActualScalar = supply / component.BaseSupplyRate; + + if (!TryComp(uid, out var powa)) + return; + + powa.MaxSupply = supply; + } + + private void OnSupplierUpgradeExamine(EntityUid uid, UpgradePowerSupplierComponent component, UpgradeExamineEvent args) + { + args.AddPercentageUpgrade("upgrade-power-supply", component.ActualScalar); + } + + private void OnSupplyRampingMapInit(EntityUid uid, UpgradePowerSupplyRampingComponent component, MapInitEvent args) + { + if (!TryComp(uid, out var battery)) + return; + + component.BaseRampRate = battery.SupplyRampRate; + } + + private void OnSupplyRampingRefreshParts(EntityUid uid, UpgradePowerSupplyRampingComponent component, RefreshPartsEvent args) + { + var rampRate = component.BaseRampRate; + var rating = args.PartRatings[component.MachinePartRampRate]; + switch (component.Scaling) + + { + case MachineUpgradeScalingType.Linear: + rampRate += component.SupplyRampingMultiplier * component.BaseRampRate * (rating - 1); + break; + case MachineUpgradeScalingType.Exponential: + rampRate *= MathF.Pow(component.SupplyRampingMultiplier, rating - 1); + break; + default: + Log.Error($"invalid power supply ramping type for {ToPrettyString(uid)}."); + rampRate = component.BaseRampRate; + break; + } + + component.ActualScalar = rampRate / component.BaseRampRate; + + if (!TryComp(uid, out var battery)) + return; + + battery.SupplyRampRate = rampRate; + } + + private void OnSupplyRampingUpgradeExamine(EntityUid uid, UpgradePowerSupplyRampingComponent component, UpgradeExamineEvent args) + { + args.AddPercentageUpgrade("upgrade-power-supply-ramping", component.ActualScalar); + } +} diff --git a/Content.Server/Power/Generator/GeneratorSystem.cs b/Content.Server/Power/Generator/GeneratorSystem.cs index a75d1e4113d..721a959820b 100644 --- a/Content.Server/Power/Generator/GeneratorSystem.cs +++ b/Content.Server/Power/Generator/GeneratorSystem.cs @@ -26,8 +26,11 @@ public sealed class GeneratorSystem : SharedGeneratorSystem [Dependency] private readonly PopupSystem _popup = default!; [Dependency] private readonly PuddleSystem _puddle = default!; + private EntityQuery _upgradeQuery; + public override void Initialize() { + _upgradeQuery = GetEntityQuery(); UpdatesBefore.Add(typeof(PowerNetSystem)); @@ -225,7 +228,9 @@ public override void Update(float frameTime) supplier.Enabled = true; - supplier.MaxSupply = gen.TargetPower; + var upgradeMultiplier = _upgradeQuery.CompOrNull(uid)?.ActualScalar ?? 1f; + + supplier.MaxSupply = gen.TargetPower * upgradeMultiplier; var eff = 1 / CalcFuelEfficiency(gen.TargetPower, gen.OptimalPower, gen); var consumption = gen.OptimalBurnRate * frameTime * eff; diff --git a/Content.Server/Shuttles/Components/ThrusterComponent.cs b/Content.Server/Shuttles/Components/ThrusterComponent.cs index 3bba9b5a7f0..f64d9bdcbf0 100644 --- a/Content.Server/Shuttles/Components/ThrusterComponent.cs +++ b/Content.Server/Shuttles/Components/ThrusterComponent.cs @@ -1,8 +1,10 @@ using System.Numerics; using Content.Server.Shuttles.Systems; +using Content.Shared.Construction.Prototypes; using Content.Shared.Damage; using Robust.Shared.GameStates; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; namespace Content.Server.Shuttles.Components { @@ -13,7 +15,7 @@ public sealed partial class ThrusterComponent : Component /// /// Whether the thruster has been force to be enabled / disabled (e.g. VV, interaction, etc.) /// - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] public bool Enabled { get; set; } = true; /// @@ -22,9 +24,12 @@ public sealed partial class ThrusterComponent : Component public bool IsOn; // Need to serialize this because RefreshParts isn't called on Init and this will break post-mapinit maps! - [ViewVariables(VVAccess.ReadWrite), DataField("thrust")] + [DataField] public float Thrust = 100f; + [DataField] + public float BaseThrust = 100f; + [DataField("thrusterType")] public ThrusterType Type = ThrusterType.Linear; @@ -37,24 +42,31 @@ public sealed partial class ThrusterComponent : Component }; /// - /// How much damage is done per second to anything colliding with our thrust. + /// How much damage is done per second to anything colliding with our thrust. /// - [DataField("damage")] public DamageSpecifier? Damage = new(); + [DataField] + public DamageSpecifier? Damage = new(); - [DataField("requireSpace")] + [DataField] public bool RequireSpace = true; // Used for burns public List Colliding = new(); - public bool Firing = false; + public bool Firing; /// - /// Next time we tick damage for anyone colliding. + /// Next time we tick damage for anyone colliding. /// - [ViewVariables(VVAccess.ReadWrite), DataField("nextFire", customTypeSerializer:typeof(TimeOffsetSerializer))] + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] public TimeSpan NextFire; + + [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))] + public string MachinePartThrust = "Capacitor"; + + [DataField] + public float PartRatingThrustMultiplier = 1.5f; } public enum ThrusterType diff --git a/Content.Server/Shuttles/Systems/ThrusterSystem.cs b/Content.Server/Shuttles/Systems/ThrusterSystem.cs index be55cd9a62a..ee131d7857a 100644 --- a/Content.Server/Shuttles/Systems/ThrusterSystem.cs +++ b/Content.Server/Shuttles/Systems/ThrusterSystem.cs @@ -1,5 +1,6 @@ using System.Numerics; using Content.Server.Audio; +using Content.Server.Construction; using Content.Server.Power.Components; using Content.Server.Power.EntitySystems; using Content.Server.Shuttles.Components; @@ -51,6 +52,9 @@ public override void Initialize() SubscribeLocalEvent(OnThrusterExamine); + SubscribeLocalEvent(OnRefreshParts); + SubscribeLocalEvent(OnUpgradeExamine); + SubscribeLocalEvent(OnShuttleTileChange); } @@ -580,6 +584,24 @@ public void SetAngularThrust(ShuttleComponent component, bool on) } } + private void OnRefreshParts(EntityUid uid, ThrusterComponent component, RefreshPartsEvent args) + { + if (component.IsOn) // safely disable thruster to prevent negative thrust + DisableThruster(uid, component); + + var thrustRating = args.PartRatings[component.MachinePartThrust]; + + component.Thrust = component.BaseThrust * MathF.Pow(component.PartRatingThrustMultiplier, thrustRating - 1); + + if (component.Enabled && CanEnable(uid, component)) + EnableThruster(uid, component); + } + + private void OnUpgradeExamine(EntityUid uid, ThrusterComponent component, UpgradeExamineEvent args) + { + args.AddPercentageUpgrade("thruster-comp-upgrade-thrust", component.Thrust / component.BaseThrust); + } + #endregion private int GetFlagIndex(DirectionFlag flag) diff --git a/Content.Server/Singularity/EntitySystems/EmitterSystem.cs b/Content.Server/Singularity/EntitySystems/EmitterSystem.cs index a9763b64d90..652ca236e44 100644 --- a/Content.Server/Singularity/EntitySystems/EmitterSystem.cs +++ b/Content.Server/Singularity/EntitySystems/EmitterSystem.cs @@ -1,6 +1,7 @@ using System.Numerics; using System.Threading; using Content.Server.Administration.Logs; +using Content.Server.Construction; using Content.Server.DeviceLinking.Events; using Content.Server.Power.Components; using Content.Server.Power.EntitySystems; @@ -47,6 +48,8 @@ public override void Initialize() SubscribeLocalEvent(OnInteractHand); SubscribeLocalEvent>(OnGetVerb); SubscribeLocalEvent(OnExamined); + SubscribeLocalEvent(OnRefreshParts); + SubscribeLocalEvent(OnUpgradeExamine); SubscribeLocalEvent(OnAnchorStateChanged); SubscribeLocalEvent(OnSignalReceived); } @@ -176,6 +179,20 @@ private void OnApcChanged(EntityUid uid, EmitterComponent component, ref PowerCh } } + private void OnRefreshParts(EntityUid uid, EmitterComponent component, RefreshPartsEvent args) + { + var fireRateRating = args.PartRatings[component.MachinePartFireRate]; + + component.FireInterval = component.BaseFireInterval * MathF.Pow(component.FireRateMultiplier, fireRateRating - 1); + component.FireBurstDelayMin = component.BaseFireBurstDelayMin * MathF.Pow(component.FireRateMultiplier, fireRateRating - 1); + component.FireBurstDelayMax = component.BaseFireBurstDelayMax * MathF.Pow(component.FireRateMultiplier, fireRateRating - 1); + } + + private void OnUpgradeExamine(EntityUid uid, EmitterComponent component, UpgradeExamineEvent args) + { + args.AddPercentageUpgrade("emitter-component-upgrade-fire-rate", (float) (component.BaseFireInterval.TotalSeconds / component.FireInterval.TotalSeconds)); + } + public void SwitchOff(EntityUid uid, EmitterComponent component) { component.IsOn = false; diff --git a/Content.Server/Xenoarchaeology/Equipment/Components/TraversalDistorterComponent.cs b/Content.Server/Xenoarchaeology/Equipment/Components/TraversalDistorterComponent.cs index ec16083c538..d6a39fe4f40 100644 --- a/Content.Server/Xenoarchaeology/Equipment/Components/TraversalDistorterComponent.cs +++ b/Content.Server/Xenoarchaeology/Equipment/Components/TraversalDistorterComponent.cs @@ -1,4 +1,7 @@ -namespace Content.Server.Xenoarchaeology.Equipment.Components; +using Content.Shared.Construction.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; + +namespace Content.Server.Xenoarchaeology.Equipment.Components; /// /// This is used for a machine that biases @@ -7,6 +10,18 @@ [RegisterComponent] public sealed partial class TraversalDistorterComponent : Component { + [ViewVariables(VVAccess.ReadWrite)] + public float BiasChance; + + [DataField] + public float BaseBiasChance = 0.7f; + + [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))] + public string MachinePartBiasChance = "Manipulator"; + + [DataField] + public float PartRatingBiasChance = 1.1f; + [ViewVariables(VVAccess.ReadWrite)] public BiasDirection BiasDirection = BiasDirection.In; diff --git a/Content.Server/Xenoarchaeology/Equipment/Systems/TraversalDistorterSystem.cs b/Content.Server/Xenoarchaeology/Equipment/Systems/TraversalDistorterSystem.cs index 230e639af49..bb662925a92 100644 --- a/Content.Server/Xenoarchaeology/Equipment/Systems/TraversalDistorterSystem.cs +++ b/Content.Server/Xenoarchaeology/Equipment/Systems/TraversalDistorterSystem.cs @@ -1,9 +1,12 @@ -using Content.Server.Popups; +using Content.Server.Construction; +using Content.Server.Popups; using Content.Server.Power.EntitySystems; using Content.Server.Xenoarchaeology.Equipment.Components; +using Content.Server.Xenoarchaeology.XenoArtifacts; using Content.Shared.Examine; using Content.Shared.Interaction; using Content.Shared.Placeable; +using Robust.Shared.Player; using Robust.Shared.Timing; namespace Content.Server.Xenoarchaeology.Equipment.Systems; @@ -20,6 +23,8 @@ public override void Initialize() SubscribeLocalEvent(OnInteract); SubscribeLocalEvent(OnExamine); + SubscribeLocalEvent(OnRefreshParts); + SubscribeLocalEvent(OnUpgradeExamine); SubscribeLocalEvent(OnItemPlaced); SubscribeLocalEvent(OnItemRemoved); @@ -68,10 +73,21 @@ private void OnExamine(EntityUid uid, TraversalDistorterComponent component, Exa examine = Loc.GetString("traversal-distorter-desc-out"); break; } - args.PushMarkup(examine); } + private void OnRefreshParts(EntityUid uid, TraversalDistorterComponent component, RefreshPartsEvent args) + { + var biasRating = args.PartRatings[component.MachinePartBiasChance]; + + component.BiasChance = component.BaseBiasChance * MathF.Pow(component.PartRatingBiasChance, biasRating - 1); + } + + private void OnUpgradeExamine(EntityUid uid, TraversalDistorterComponent component, UpgradeExamineEvent args) + { + args.AddPercentageUpgrade("traversal-distorter-upgrade-bias", component.BiasChance / component.BaseBiasChance); + } + private void OnItemPlaced(EntityUid uid, TraversalDistorterComponent component, ref ItemPlacedEvent args) { var bias = EnsureComp(args.OtherEntity); diff --git a/Content.Server/Xenoarchaeology/XenoArtifacts/ArtifactSystem.cs b/Content.Server/Xenoarchaeology/XenoArtifacts/ArtifactSystem.cs index 955fe827d72..a7948aa7ff9 100644 --- a/Content.Server/Xenoarchaeology/XenoArtifacts/ArtifactSystem.cs +++ b/Content.Server/Xenoarchaeology/XenoArtifacts/ArtifactSystem.cs @@ -206,6 +206,7 @@ public void ForceActivateArtifact(EntityUid uid, EntityUid? user = null, Artifac if (TryComp(uid, out var bias) && TryComp(bias.Provider, out var trav) && + _random.Prob(trav.BiasChance) && this.IsPowered(bias.Provider, EntityManager)) { switch (trav.BiasDirection) diff --git a/Content.Shared/Cargo/Components/SharedCargoTelepadComponent.cs b/Content.Shared/Cargo/Components/SharedCargoTelepadComponent.cs index 911ea41cca5..7c8a3625227 100644 --- a/Content.Shared/Cargo/Components/SharedCargoTelepadComponent.cs +++ b/Content.Shared/Cargo/Components/SharedCargoTelepadComponent.cs @@ -1,3 +1,4 @@ +using Content.Shared.Construction.Prototypes; using Content.Shared.DeviceLinking; using Robust.Shared.Audio; using Robust.Shared.GameStates; @@ -13,29 +14,47 @@ namespace Content.Shared.Cargo.Components; public sealed partial class CargoTelepadComponent : Component { /// - /// The actual amount of time it takes to teleport from the telepad + /// The base amount of time it takes to teleport from the telepad /// - [DataField("delay"), ViewVariables(VVAccess.ReadWrite)] + [DataField] + public float BaseDelay = 10f; + + /// + /// The actual amount of time it takes to teleport from the telepad + /// + [DataField] public float Delay = 10f; /// - /// How much time we've accumulated until next teleport. + /// The machine part that affects + /// + [DataField(customTypeSerializer: typeof(PrototypeIdSerializer)), ViewVariables(VVAccess.ReadWrite)] + public string MachinePartTeleportDelay = "Capacitor"; + + /// + /// A multiplier applied to for each level of + /// + [DataField] + public float PartRatingTeleportDelay = 0.8f; + + /// + /// How much time we've accumulated until next teleport. /// - [DataField("accumulator"), ViewVariables(VVAccess.ReadWrite)] + [DataField] public float Accumulator; - [DataField("currentState")] + [DataField] public CargoTelepadState CurrentState = CargoTelepadState.Unpowered; - [DataField("teleportSound")] + [DataField] public SoundSpecifier TeleportSound = new SoundPathSpecifier("/Audio/Machines/phasein.ogg"); /// /// The paper-type prototype to spawn with the order information. /// - [DataField("printerOutput", customTypeSerializer: typeof(PrototypeIdSerializer)), ViewVariables(VVAccess.ReadWrite)] + [DataField(customTypeSerializer: typeof(PrototypeIdSerializer)), ViewVariables(VVAccess.ReadWrite)] public string PrinterOutput = "PaperCargoInvoice"; - [DataField("receiverPort", customTypeSerializer: typeof(PrototypeIdSerializer)), ViewVariables(VVAccess.ReadWrite)] + [DataField(customTypeSerializer: typeof(PrototypeIdSerializer)), ViewVariables(VVAccess.ReadWrite)] public string ReceiverPort = "OrderReceiver"; } diff --git a/Content.Shared/Cloning/CloningPodComponent.cs b/Content.Shared/Cloning/CloningPodComponent.cs index 082b92e8b14..c9a6fd4500b 100644 --- a/Content.Shared/Cloning/CloningPodComponent.cs +++ b/Content.Shared/Cloning/CloningPodComponent.cs @@ -1,3 +1,4 @@ +using Content.Shared.Construction.Prototypes; using Content.Shared.DeviceLinking; using Content.Shared.Materials; using Content.Shared.Random; @@ -38,6 +39,18 @@ public sealed partial class CloningPodComponent : Component [DataField] public ProtoId RequiredMaterial = "Biomass"; + /// + /// The multiplier for cloning duration + /// + [DataField] + public float PartRatingSpeedMultiplier = 0.75f; + + /// + /// The machine part that affects cloning speed + /// + [DataField] + public ProtoId MachinePartCloningSpeed = "Manipulator"; + /// /// The current amount of time it takes to clone a body /// @@ -66,6 +79,18 @@ public sealed partial class CloningPodComponent : Component Params = AudioParams.Default.WithVolume(4), }; + /// + /// The machine part that affects how much biomass is needed to clone a body. + /// + [DataField] + public float PartRatingMaterialMultiplier = 0.85f; + + /// + /// The machine part that decreases the amount of material needed for cloning + /// + [DataField] + public ProtoId MachinePartMaterialUse = "MatterBin"; + [ViewVariables(VVAccess.ReadWrite)] public CloningPodStatus Status; diff --git a/Content.Shared/Construction/MachinePartSystem.cs b/Content.Shared/Construction/MachinePartSystem.cs index 1a19040b410..01db7fbade3 100644 --- a/Content.Shared/Construction/MachinePartSystem.cs +++ b/Content.Shared/Construction/MachinePartSystem.cs @@ -21,6 +21,7 @@ public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnMachineBoardExamined); + SubscribeLocalEvent(OnMachinePartExamined); } private void OnMachineBoardExamined(EntityUid uid, MachineBoardComponent component, ExaminedEvent args) @@ -61,6 +62,20 @@ private void OnMachineBoardExamined(EntityUid uid, MachineBoardComponent compone } } + private void OnMachinePartExamined(EntityUid uid, MachinePartComponent component, ExaminedEvent args) + { + if (!args.IsInDetailsRange) + return; + + using (args.PushGroup(nameof(MachinePartComponent))) + { + args.PushMarkup(Loc.GetString("machine-part-component-on-examine-rating-text", + ("rating", component.Rating))); + args.PushMarkup(Loc.GetString("machine-part-component-on-examine-type-text", ("type", + Loc.GetString(_prototype.Index(component.PartType).Name)))); + } + } + public Dictionary GetMachineBoardMaterialCost(Entity entity, int coefficient = 1) { var (_, comp) = entity; diff --git a/Content.Shared/Materials/MaterialReclaimerComponent.cs b/Content.Shared/Materials/MaterialReclaimerComponent.cs index 3e72baf6041..2fd6cd6fc82 100644 --- a/Content.Shared/Materials/MaterialReclaimerComponent.cs +++ b/Content.Shared/Materials/MaterialReclaimerComponent.cs @@ -1,100 +1,121 @@ -using Content.Shared.Whitelist; +using Content.Shared.Construction.Prototypes; +using Content.Shared.Whitelist; using Robust.Shared.Audio; using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; using Robust.Shared.Serialization; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; namespace Content.Shared.Materials; /// -/// This is a machine that handles converting entities -/// into the raw materials and chemicals that make them up. +/// This is a machine that handles converting entities +/// into the raw materials and chemicals that make them up. /// [RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause] [Access(typeof(SharedMaterialReclaimerSystem))] public sealed partial class MaterialReclaimerComponent : Component { /// - /// Whether or not the machine has power. We put it here - /// so we can network and predict it. + /// Whether or not the machine has power. We put it here + /// so we can network and predict it. /// - [DataField, AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)] + [DataField, AutoNetworkedField] public bool Powered; /// - /// An "enable" toggle for things like interfacing with machine linking + /// An "enable" toggle for things like interfacing with machine linking /// - [DataField, AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)] + [DataField, AutoNetworkedField] public bool Enabled = true; /// - /// How efficiently the materials are reclaimed. - /// In practice, a multiplier per material when calculating the output of the reclaimer. + /// How efficiently the materials are reclaimed. + /// In practice, a multiplier per material when calculating the output of the reclaimer. /// - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] public float Efficiency = 1f; /// - /// Whether or not the process - /// speed scales with the amount of materials being processed - /// or if it's just + /// Whether or not the process + /// speed scales with the amount of materials being processed + /// or if it's just /// [DataField] public bool ScaleProcessSpeed = true; /// - /// How quickly it takes to consume X amount of materials per second. - /// For example, with a rate of 50, an entity with 100 total material takes 2 seconds to process. + /// How quickly it takes to consume X amount of materials per second. + /// For example, with a rate of 50, an entity with 100 total material takes 2 seconds to process. /// - [DataField, AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)] + [DataField] + public float BaseMaterialProcessRate = 100f; + + /// + /// How quickly it takes to consume X amount of materials per second. + /// For example, with a rate of 50, an entity with 100 total material takes 2 seconds to process. + /// + [DataField, AutoNetworkedField] public float MaterialProcessRate = 100f; /// - /// The minimum amount fo time it can take to process an entity. - /// this value supercedes the calculated one using + /// Machine part whose rating modifies + /// + [DataField] + public ProtoId MachinePartProcessRate = "Manipulator"; + + /// + /// How much the machine part quality affects the /// - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] + public float PartRatingProcessRateMultiplier = 1.5f; + + /// + /// The minimum amount fo time it can take to process an entity. + /// this value supercedes the calculated one using + /// + [DataField] public TimeSpan MinimumProcessDuration = TimeSpan.FromSeconds(0.5f); /// - /// The id of our output solution + /// The id of our output solution /// - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] public string SolutionContainerId = "output"; /// - /// a whitelist for what entities can be inserted into this reclaimer + /// a whitelist for what entities can be inserted into this reclaimer /// [DataField] public EntityWhitelist? Whitelist; /// - /// a blacklist for what entities cannot be inserted into this reclaimer + /// a blacklist for what entities cannot be inserted into this reclaimer /// [DataField] public EntityWhitelist? Blacklist; /// - /// The sound played when something is being processed. + /// The sound played when something is being processed. /// [DataField] public SoundSpecifier? Sound; /// - /// whether or not we cut off the sound early when the reclaiming ends. + /// whether or not we cut off the sound early when the reclaiming ends. /// [DataField] public bool CutOffSound = true; /// - /// When the next sound will be allowed to be played. Used to prevent spam. + /// When the next sound will be allowed to be played. Used to prevent spam. /// [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] [AutoPausedField] public TimeSpan NextSound; /// - /// Minimum time inbetween each + /// Minimum time inbetween each /// [DataField] public TimeSpan SoundCooldown = TimeSpan.FromSeconds(0.8f); @@ -102,10 +123,10 @@ public sealed partial class MaterialReclaimerComponent : Component public EntityUid? Stream; /// - /// A counter of how many items have been processed + /// A counter of how many items have been processed /// /// - /// I saw this on the recycler and i'm porting it because it's cute af + /// I saw this on the recycler and i'm porting it because it's cute af /// [DataField, AutoNetworkedField] public int ItemsProcessed; diff --git a/Content.Shared/Singularity/Components/SharedEmitterComponent.cs b/Content.Shared/Singularity/Components/SharedEmitterComponent.cs index c2e7af717b1..cc6e8aeede2 100644 --- a/Content.Shared/Singularity/Components/SharedEmitterComponent.cs +++ b/Content.Shared/Singularity/Components/SharedEmitterComponent.cs @@ -1,4 +1,5 @@ using System.Threading; +using Content.Shared.Construction.Prototypes; using Content.Shared.DeviceLinking; using Robust.Shared.GameStates; using Robust.Shared.Prototypes; @@ -14,90 +15,130 @@ public sealed partial class EmitterComponent : Component { public CancellationTokenSource? TimerCancel; - // whether the power switch is in "on" - [ViewVariables] public bool IsOn; - // Whether the power switch is on AND the machine has enough power (so is actively firing) - [ViewVariables] public bool IsPowered; + /// + /// Whether the power switch is on + /// + [ViewVariables] + public bool IsOn; /// - /// counts the number of consecutive shots fired. + /// Whether the power switch is on AND the machine has enough power (so is actively firing) + /// + [ViewVariables] + public bool IsPowered; + + /// + /// counts the number of consecutive shots fired. /// [ViewVariables] public int FireShotCounter; /// - /// The entity that is spawned when the emitter fires. + /// The entity that is spawned when the emitter fires. /// - [DataField("boltType", customTypeSerializer: typeof(PrototypeIdSerializer))] + [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))] public string BoltType = "EmitterBolt"; - [DataField("selectableTypes", customTypeSerializer: typeof(PrototypeIdListSerializer))] + [DataField(customTypeSerializer: typeof(PrototypeIdListSerializer))] public List SelectableTypes = new(); /// - /// The current amount of power being used. + /// The current amount of power being used. /// - [DataField("powerUseActive")] + [DataField] public int PowerUseActive = 600; /// - /// The amount of shots that are fired in a single "burst" + /// The amount of shots that are fired in a single "burst" /// - [DataField("fireBurstSize")] + [DataField] public int FireBurstSize = 3; /// - /// The time between each shot during a burst. + /// The time between each shot during a burst. /// - [DataField("fireInterval")] + [DataField] public TimeSpan FireInterval = TimeSpan.FromSeconds(2); /// - /// The current minimum delay between bursts. + /// The base amount of time between each shot during a burst. /// - [DataField("fireBurstDelayMin")] + [DataField] + public TimeSpan BaseFireInterval = TimeSpan.FromSeconds(2); + + /// + /// The current minimum delay between bursts. + /// + [DataField] public TimeSpan FireBurstDelayMin = TimeSpan.FromSeconds(4); /// - /// The current maximum delay between bursts. + /// The current maximum delay between bursts. /// - [DataField("fireBurstDelayMax")] + [DataField] public TimeSpan FireBurstDelayMax = TimeSpan.FromSeconds(10); /// - /// The visual state that is set when the emitter is turned on + /// The base minimum delay between shot bursts. + /// Used for machine part rating calculations. + /// + [DataField] + public TimeSpan BaseFireBurstDelayMin = TimeSpan.FromSeconds(4); + + /// + /// The base maximum delay between shot bursts. + /// Used for machine part rating calculations. + /// + [DataField] + public TimeSpan BaseFireBurstDelayMax = TimeSpan.FromSeconds(10); + + /// + /// The multiplier for the base delay between shot bursts as well as + /// the fire interval + /// + [DataField] + public float FireRateMultiplier = 0.8f; + + /// + /// The machine part that affects burst delay. + /// + [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))] + public string MachinePartFireRate = "Capacitor"; + + /// + /// The visual state that is set when the emitter is turned on /// - [DataField("onState")] + [DataField] public string? OnState = "beam"; /// - /// The visual state that is set when the emitter doesn't have enough power. + /// The visual state that is set when the emitter doesn't have enough power. /// - [DataField("underpoweredState")] + [DataField] public string? UnderpoweredState = "underpowered"; /// - /// Signal port that turns on the emitter. + /// Signal port that turns on the emitter. /// - [DataField("onPort", customTypeSerializer: typeof(PrototypeIdSerializer))] + [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))] public string OnPort = "On"; /// - /// Signal port that turns off the emitter. + /// Signal port that turns off the emitter. /// - [DataField("offPort", customTypeSerializer: typeof(PrototypeIdSerializer))] + [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))] public string OffPort = "Off"; /// - /// Signal port that toggles the emitter on or off. + /// Signal port that toggles the emitter on or off. /// - [DataField("togglePort", customTypeSerializer: typeof(PrototypeIdSerializer))] + [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))] public string TogglePort = "Toggle"; /// - /// Map of signal ports to entity prototype IDs of the entity that will be fired. + /// Map of signal ports to entity prototype IDs of the entity that will be fired. /// - [DataField("setTypePorts", customTypeSerializer: typeof(PrototypeIdDictionarySerializer))] + [DataField(customTypeSerializer: typeof(PrototypeIdDictionarySerializer))] public Dictionary SetTypePorts = new(); } diff --git a/Resources/Locale/en-US/anomaly/anomaly.ftl b/Resources/Locale/en-US/anomaly/anomaly.ftl index da5882fa62f..1609d77d914 100644 --- a/Resources/Locale/en-US/anomaly/anomaly.ftl +++ b/Resources/Locale/en-US/anomaly/anomaly.ftl @@ -3,6 +3,7 @@ anomaly-component-contact-damage = The anomaly sears off your skin! anomaly-vessel-component-anomaly-assigned = Anomaly assigned to vessel. anomaly-vessel-component-not-assigned = This vessel is not assigned to any anomaly. Try using a scanner on it. anomaly-vessel-component-assigned = This vessel is currently assigned to an anomaly. +anomaly-vessel-component-upgrade-output = point output anomaly-particles-delta = Delta particles anomaly-particles-epsilon = Epsilon particles diff --git a/Resources/Locale/en-US/atmos/gas-recycler-system.ftl b/Resources/Locale/en-US/atmos/gas-recycler-system.ftl index cc527adf5c8..a72e137732c 100644 --- a/Resources/Locale/en-US/atmos/gas-recycler-system.ftl +++ b/Resources/Locale/en-US/atmos/gas-recycler-system.ftl @@ -1,3 +1,6 @@ gas-recycler-reacting = It is [color=green]converting[/color] waste gases. gas-recycler-low-pressure = The input pressure is [color=darkred]too low[/color]. gas-recycler-low-temperature = The input temperature is [color=darkred]too low[/color]. + +gas-recycler-upgrade-min-temp = Minimum temperature +gas-recycler-upgrade-min-pressure = Minimum pressure diff --git a/Resources/Locale/en-US/atmos/portable-scrubber.ftl b/Resources/Locale/en-US/atmos/portable-scrubber.ftl index c4071b4acce..8aadf076d96 100644 --- a/Resources/Locale/en-US/atmos/portable-scrubber.ftl +++ b/Resources/Locale/en-US/atmos/portable-scrubber.ftl @@ -1 +1,4 @@ portable-scrubber-fill-level = It's at about [color=yellow]{$percent}%[/color] of its maximum internal pressure. + +portable-scrubber-component-upgrade-max-pressure = max pressure +portable-scrubber-component-upgrade-transfer-rate = transfer rate diff --git a/Resources/Locale/en-US/botany/components/seed-extractor-component.ftl b/Resources/Locale/en-US/botany/components/seed-extractor-component.ftl index 84d5a5ed283..c586a594a9a 100644 --- a/Resources/Locale/en-US/botany/components/seed-extractor-component.ftl +++ b/Resources/Locale/en-US/botany/components/seed-extractor-component.ftl @@ -2,3 +2,5 @@ seed-extractor-component-interact-message = You extract some seeds from the { THE($name) }. seed-extractor-component-no-seeds = { CAPITALIZE(THE($name)) } has no seeds! + +seed-extractor-component-upgrade-seed-yield = seed yield diff --git a/Resources/Locale/en-US/cargo/cargo-console-component.ftl b/Resources/Locale/en-US/cargo/cargo-console-component.ftl index b56f4730ccd..1caa810f1b7 100644 --- a/Resources/Locale/en-US/cargo/cargo-console-component.ftl +++ b/Resources/Locale/en-US/cargo/cargo-console-component.ftl @@ -45,3 +45,5 @@ cargo-shuttle-console-station-unknown = Unknown cargo-shuttle-console-shuttle-not-found = Not found cargo-shuttle-console-organics = Detected organic lifeforms on the shuttle cargo-no-shuttle = No cargo shuttle found! + +cargo-telepad-delay-upgrade = Teleport delay diff --git a/Resources/Locale/en-US/chemistry/components/solution-heater-component.ftl b/Resources/Locale/en-US/chemistry/components/solution-heater-component.ftl new file mode 100644 index 00000000000..cecf15550c7 --- /dev/null +++ b/Resources/Locale/en-US/chemistry/components/solution-heater-component.ftl @@ -0,0 +1 @@ +solution-heater-upgrade-heat = Heat strength diff --git a/Resources/Locale/en-US/construction/components/machine-part-component.ftl b/Resources/Locale/en-US/construction/components/machine-part-component.ftl new file mode 100644 index 00000000000..0613f505161 --- /dev/null +++ b/Resources/Locale/en-US/construction/components/machine-part-component.ftl @@ -0,0 +1,2 @@ +machine-part-component-on-examine-rating-text = [color=white]Rating:[/color] [color=cyan]{$rating}[/color] +machine-part-component-on-examine-type-text = [color=white]Type:[/color] [color=cyan]{$type}[/color] diff --git a/Resources/Locale/en-US/kitchen/components/microwave-component.ftl b/Resources/Locale/en-US/kitchen/components/microwave-component.ftl index 0603b3c8463..12346ee75dc 100644 --- a/Resources/Locale/en-US/kitchen/components/microwave-component.ftl +++ b/Resources/Locale/en-US/kitchen/components/microwave-component.ftl @@ -9,6 +9,7 @@ microwave-component-suicide-multi-head-others-message = {$victim} is trying to c microwave-component-suicide-others-message = {$victim} is trying to cook their head! microwave-component-suicide-multi-head-message = You cook your heads! microwave-component-suicide-message = You cook your head! +microwave-component-upgrade-cook-time = cook time microwave-component-interact-full = It's full. microwave-component-interact-item-too-big = { CAPITALIZE(THE($item)) } is too big to fit in the microwave! diff --git a/Resources/Locale/en-US/kitchen/components/reagent-grinder-component.ftl b/Resources/Locale/en-US/kitchen/components/reagent-grinder-component.ftl index 8a3ca9eef85..34951282745 100644 --- a/Resources/Locale/en-US/kitchen/components/reagent-grinder-component.ftl +++ b/Resources/Locale/en-US/kitchen/components/reagent-grinder-component.ftl @@ -4,6 +4,9 @@ reagent-grinder-bound-user-interface-instant-button = INSTANT reagent-grinder-bound-user-interface-cook-time-label = COOK TIME reagent-grinder-component-cannot-put-entity-message = You can't put this in the reagent grinder! +reagent-grinder-component-upgrade-work-time = Work time +reagent-grinder-component-upgrade-storage = Storage + grinder-menu-title = All-In-One Grinder 3000 grinder-menu-grind-button = Grind grinder-menu-juice-button = Juice diff --git a/Resources/Locale/en-US/machine/machine.ftl b/Resources/Locale/en-US/machine/machine.ftl index ce8873df6f8..26059505160 100644 --- a/Resources/Locale/en-US/machine/machine.ftl +++ b/Resources/Locale/en-US/machine/machine.ftl @@ -13,6 +13,11 @@ machine-part-name-manipulator = Manipulator machine-part-name-matter-bin = Matter Bin machine-part-name-power-cell = Power Cell +upgrade-power-draw = power draw +upgrade-max-charge = max charge +upgrade-power-supply = power supply +upgrade-power-supply-ramping = power ramp rate + two-way-lever-left = push left two-way-lever-right = push right two-way-lever-cant = can't push the lever that way! diff --git a/Resources/Locale/en-US/materials/materials.ftl b/Resources/Locale/en-US/materials/materials.ftl index dca520b5b49..5a4413348e8 100644 --- a/Resources/Locale/en-US/materials/materials.ftl +++ b/Resources/Locale/en-US/materials/materials.ftl @@ -35,3 +35,6 @@ materials-raw-plasma = raw plasma materials-raw-uranium = raw uranium materials-raw-bananium = raw bananium materials-raw-salt = raw salt + +# Material Reclaimer +material-reclaimer-upgrade-process-rate = process rate diff --git a/Resources/Locale/en-US/medical/components/biomass-reclaimer-component.ftl b/Resources/Locale/en-US/medical/components/biomass-reclaimer-component.ftl index 443429c1ef3..0c0b8faf59e 100644 --- a/Resources/Locale/en-US/medical/components/biomass-reclaimer-component.ftl +++ b/Resources/Locale/en-US/medical/components/biomass-reclaimer-component.ftl @@ -1 +1,4 @@ biomass-reclaimer-suicide-others = {CAPITALIZE(THE($victim))} threw themselves into the biomass reclaimer! + +biomass-reclaimer-component-upgrade-speed = speed +biomass-reclaimer-component-upgrade-biomass-yield = biomass yield diff --git a/Resources/Locale/en-US/medical/components/cloning-pod-component.ftl b/Resources/Locale/en-US/medical/components/cloning-pod-component.ftl index b222d707a0a..e92ac86a1e4 100644 --- a/Resources/Locale/en-US/medical/components/cloning-pod-component.ftl +++ b/Resources/Locale/en-US/medical/components/cloning-pod-component.ftl @@ -1,3 +1,5 @@ cloning-pod-biomass = It currently has [color=red]{$number}[/color] units of biomass. +cloning-pod-component-upgrade-speed = cloning speed +cloning-pod-component-upgrade-biomass-requirement = biomass requirement cloning-pod-component-upgrade-emag-requirement = The card zaps something inside the cloning pod. diff --git a/Resources/Locale/en-US/medical/components/medical-scanner-component.ftl b/Resources/Locale/en-US/medical/components/medical-scanner-component.ftl index c4b19426545..da4dc7a3847 100644 --- a/Resources/Locale/en-US/medical/components/medical-scanner-component.ftl +++ b/Resources/Locale/en-US/medical/components/medical-scanner-component.ftl @@ -2,3 +2,5 @@ medical-scanner-verb-enter = Enter medical-scanner-verb-noun-occupant = occupant + +medical-scanner-upgrade-cloning = Cloning fail chance diff --git a/Resources/Locale/en-US/medical/components/stasis-bed-component.ftl b/Resources/Locale/en-US/medical/components/stasis-bed-component.ftl new file mode 100644 index 00000000000..2d8a18c263e --- /dev/null +++ b/Resources/Locale/en-US/medical/components/stasis-bed-component.ftl @@ -0,0 +1 @@ +stasis-bed-component-upgrade-stasis = stasis effect diff --git a/Resources/Locale/en-US/nutrition/components/fat-extractor.ftl b/Resources/Locale/en-US/nutrition/components/fat-extractor.ftl index 20a31cd8c40..611b8ef5406 100644 --- a/Resources/Locale/en-US/nutrition/components/fat-extractor.ftl +++ b/Resources/Locale/en-US/nutrition/components/fat-extractor.ftl @@ -1,3 +1,5 @@ +fat-extractor-component-rate = extraction rate + fat-extractor-fact-1 = Fats are triglycerides made up of a combination of different building blocks; glycerol and fatty acids. fat-extractor-fact-2 = Adults should get a recommended 20-35% of their energy intake from fat. fat-extractor-fact-3 = Being overweight or obese puts you at an increased risk of chronic diseases, such as cardiovascular diseases, metabolic syndrome, type 2 diabetes, and some types of cancers. diff --git a/Resources/Locale/en-US/shuttles/thruster.ftl b/Resources/Locale/en-US/shuttles/thruster.ftl index 94035811c73..faed6e8dd28 100644 --- a/Resources/Locale/en-US/shuttles/thruster.ftl +++ b/Resources/Locale/en-US/shuttles/thruster.ftl @@ -3,3 +3,5 @@ thruster-comp-disabled = The thruster is turned [color=red]off[/color]. thruster-comp-nozzle-direction = The nozzle is facing [color=yellow]{$direction}[/color]. thruster-comp-nozzle-exposed = The nozzle [color=green]exposed[/color] to space. thruster-comp-nozzle-not-exposed = The nozzle [color=red]is not exposed[/color] to space. + +thruster-comp-upgrade-thrust = Thrust strength diff --git a/Resources/Locale/en-US/singularity/components/emitter-component.ftl b/Resources/Locale/en-US/singularity/components/emitter-component.ftl index c71b3d6bdfd..c7db1a93bba 100644 --- a/Resources/Locale/en-US/singularity/components/emitter-component.ftl +++ b/Resources/Locale/en-US/singularity/components/emitter-component.ftl @@ -11,5 +11,8 @@ comp-emitter-turned-off = The {$target} turns off. # Shows if the user attempts to activate the emitter while it's un-anchored. comp-emitter-not-anchored = The {$target} isn't anchored to the ground! +# Upgrades +emitter-component-upgrade-fire-rate = fire rate + emitter-component-current-type = The current selected type is: {$type}. emitter-component-type-set = Type set to: {$type} diff --git a/Resources/Locale/en-US/xenoarchaeology/traversal-distorter.ftl b/Resources/Locale/en-US/xenoarchaeology/traversal-distorter.ftl index 5c9eac57a5d..af3039e864e 100644 --- a/Resources/Locale/en-US/xenoarchaeology/traversal-distorter.ftl +++ b/Resources/Locale/en-US/xenoarchaeology/traversal-distorter.ftl @@ -3,3 +3,5 @@ traversal-distorter-set-out = Traversal bias set to "out" traversal-distorter-desc-in = The affected artifact's traversal now favors moving inwards to the beginning. traversal-distorter-desc-out = The affected artifact's traversal now favors moving outwards towards more dangerous nodes. + +traversal-distorter-upgrade-bias = Bias effectiveness diff --git a/Resources/Migrations/migration.yml b/Resources/Migrations/migration.yml index 7f19699ad8a..3b4a7178cf8 100644 --- a/Resources/Migrations/migration.yml +++ b/Resources/Migrations/migration.yml @@ -126,28 +126,6 @@ DrinkGoldschlagerGlass: DrinkGildlagerGlass ClosetBase: ClosetSteelBase MonkeyCubeBox: VariantCubeBox -# 2024-01-08 -SalvagePartsT4Spawner: SalvageLootSpawner -SalvagePartsT3Spawner: SalvageLootSpawner -SalvagePartsT3T4Spawner: SalvageLootSpawner -SalvagePartsT2Spawner: SalvageLootSpawner -AdvancedCapacitorStockPart: CapacitorStockPart -SuperCapacitorStockPart: CapacitorStockPart -QuadraticCapacitorStockPart: CapacitorStockPart -NanoManipulatorStockPart: MicroManipulatorStockPart -PicoManipulatorStockPart: MicroManipulatorStockPart -FemtoManipulatorStockPart: MicroManipulatorStockPart -AdvancedMatterBinStockPart: MatterBinStockPart -SuperMatterBinStockPart: MatterBinStockPart -BluespaceMatterBinStockPart: MatterBinStockPart -AnsibleSubspaceStockPart: null -FilterSubspaceStockPart: null -AmplifierSubspaceStockPart: null -TreatmentSubspaceStockPart: null -AnalyzerSubspaceStockPart: null -CrystalSubspaceStockPart: null -TransmitterSubspaceStockPart: null - # 2024-01-10 ClothingHeadHatHoodRad: null diff --git a/Resources/Prototypes/Entities/Markers/Spawners/Random/salvage.yml b/Resources/Prototypes/Entities/Markers/Spawners/Random/salvage.yml index 34bf32d8d7b..4beffbc9c22 100644 --- a/Resources/Prototypes/Entities/Markers/Spawners/Random/salvage.yml +++ b/Resources/Prototypes/Entities/Markers/Spawners/Random/salvage.yml @@ -63,23 +63,78 @@ offset: 0.0 - type: entity - name: salvage loot spawner - id: SalvageLootSpawner + name: Salvage T2 Machine Parts Spawner + id: SalvagePartsT2Spawner parent: MarkerBase components: - type: Sprite layers: - state: red - - sprite: Objects/Weapons/Melee/crusher.rsi - state: icon + - sprite: Objects/Misc/stock_parts.rsi + state: advanced_matter_bin - type: RandomSpawner prototypes: - - WeaponCrusher - - WeaponCrusherDagger - - WeaponCrusherGlaive - - MiningDrill + - AdvancedCapacitorStockPart + - NanoManipulatorStockPart + - AdvancedMatterBinStockPart offset: 0.0 +- type: entity + parent: MarkerBase + id: SalvagePartsT3T4Spawner + name: tier 3/4 machine part + components: + - type: Sprite + layers: + - sprite: Objects/Misc/stock_parts.rsi + state: super_matter_bin + - type: RandomSpawner + rarePrototypes: + - QuadraticCapacitorStockPart + - FemtoManipulatorStockPart + - BluespaceMatterBinStockPart + rareChance: 0.05 + prototypes: + - SuperCapacitorStockPart + - PicoManipulatorStockPart + - SuperMatterBinStockPart + chance: 0.95 + offset: 0.0 + +- type: entity + parent: MarkerBase + id: SalvagePartsT3Spawner + name: tier 3 machine part + suffix: Spawner + components: + - type: Sprite + layers: + - sprite: Objects/Misc/stock_parts.rsi + state: super_matter_bin + - type: RandomSpawner + prototypes: + - SuperCapacitorStockPart + - PicoManipulatorStockPart + - SuperMatterBinStockPart + offset: 0.0 + +- type: entity + parent: MarkerBase + id: SalvagePartsT4Spawner + name: tier 4 machine part + suffix: Spawner + components: + - type: Sprite + layers: + - sprite: Objects/Misc/stock_parts.rsi + state: bluespace_matter_bin + - type: RandomSpawner + prototypes: + - QuadraticCapacitorStockPart + - PicoManipulatorStockPart + - BluespaceMatterBinStockPart + offset: 0.0 + - type: entity name: Salvage Mob Spawner id: SalvageMobSpawner @@ -228,3 +283,25 @@ - MobFleshLoverSalvage chance: 1 offset: 0.2 + +- type: entity + name: Salvage Loot Spawner + id: SalvageLootSpawner + parent: MarkerBase + components: + - type: Sprite + layers: + - state: red + - sprite: Structures/Storage/Crates/generic.rsi + state: icon + - type: RandomSpawner + rarePrototypes: + - SalvagePartsT2Spawner + - SalvagePartsT3Spawner + - SalvagePartsT3T4Spawner + - SalvagePartsT4Spawner + rareChance: 0.4 + prototypes: + - CrateSalvageAssortedGoodies + chance: 0.9 + offset: 0.2 diff --git a/Resources/Prototypes/Entities/Objects/Misc/machine_parts.yml b/Resources/Prototypes/Entities/Objects/Misc/machine_parts.yml index 341acb52f0b..d90b03528e5 100644 --- a/Resources/Prototypes/Entities/Objects/Misc/machine_parts.yml +++ b/Resources/Prototypes/Entities/Objects/Misc/machine_parts.yml @@ -16,6 +16,10 @@ sound: /Audio/SimpleStation14/Items/Handling/component_drop.ogg - type: EmitSoundOnLand sound: /Audio/SimpleStation14/Items/Handling/component_drop.ogg +# Rating 1 + - type: GuideHelp + guides: + - MachineUpgrading # Rating 1 @@ -69,3 +73,218 @@ - type: ReverseEngineering # Nyano recipes: - MatterBinStockPart + +# Rating 2 + +- type: entity + id: AdvancedCapacitorStockPart + name: advanced capacitor + parent: CapacitorStockPart + description: An advanced capacitor used in the construction of a variety of devices. + suffix: Rating 2 + components: + - type: Sprite + state: adv_capacitor + - type: MachinePart + rating: 2 + - type: ReverseEngineering # Nyano + difficulty: 2 + recipes: + - AdvancedCapacitorStockPart + +- type: entity + id: NanoManipulatorStockPart + name: advanced manipulator + parent: MicroManipulatorStockPart + description: An advanced manipulator used in the construction of a variety of devices. + suffix: Rating 2 + components: + - type: Sprite + state: nano_mani + - type: MachinePart + rating: 2 + - type: ReverseEngineering # Nyano + difficulty: 2 + recipes: + - NanoManipulatorStockPart + +- type: entity + id: AdvancedMatterBinStockPart + name: advanced matter bin + parent: MatterBinStockPart + description: An advanced matter bin used in the construction of a variety of devices. + suffix: Rating 2 + components: + - type: Sprite + state: advanced_matter_bin + - type: MachinePart + rating: 2 + - type: ReverseEngineering # Nyano + difficulty: 2 + recipes: + - AdvancedMatterBinStockPart + +# Rating 3 + +- type: entity + id: SuperCapacitorStockPart + name: super capacitor + parent: CapacitorStockPart + description: A super capacitor used in the construction of a variety of devices. + suffix: Rating 3 + components: + - type: Sprite + state: super_capacitor + - type: MachinePart + rating: 3 + - type: ReverseEngineering # Nyano + difficulty: 3 + recipes: + - SuperCapacitorStockPart + +- type: entity + id: PicoManipulatorStockPart + name: super manipulator + parent: MicroManipulatorStockPart + description: A super manipulator used in the construction of a variety of devices. + suffix: Rating 3 + components: + - type: Sprite + state: pico_mani + - type: MachinePart + rating: 3 + - type: ReverseEngineering # Nyano + difficulty: 3 + recipes: + - PicoManipulatorStockPart + +- type: entity + id: SuperMatterBinStockPart + name: super matter bin + parent: MatterBinStockPart + description: A super matter bin used in the construction of a variety of devices. + suffix: Rating 3 + components: + - type: Sprite + state: super_matter_bin + - type: MachinePart + rating: 3 + - type: ReverseEngineering # Nyano + difficulty: 3 + recipes: + - SuperMatterBinStockPart + +# Rating 4 + +- type: entity + id: QuadraticCapacitorStockPart + name: bluespace capacitor + parent: CapacitorStockPart + description: A bluespace capacitor used in the construction of a variety of devices. + suffix: Rating 4 + components: + - type: Sprite + state: quadratic_capacitor + - type: MachinePart + rating: 4 + - type: ReverseEngineering # Nyano + difficulty: 4 + recipes: + - QuadraticCapacitorStockPart + +- type: entity + id: FemtoManipulatorStockPart + name: bluespace manipulator + parent: MicroManipulatorStockPart + description: A bluespace manipulator used in the construction of a variety of devices. + suffix: Rating 4 + components: + - type: Sprite + state: femto_mani + - type: MachinePart + rating: 4 + - type: ReverseEngineering # Nyano + difficulty: 4 + recipes: + - FemtoManipulatorStockPart + +- type: entity + id: BluespaceMatterBinStockPart + name: bluespace matter bin + parent: MatterBinStockPart + description: A bluespace matter bin used in the construction of a variety of devices. + suffix: Rating 4 + components: + - type: Sprite + state: bluespace_matter_bin + - type: MachinePart + rating: 4 + - type: ReverseEngineering # Nyano + difficulty: 4 + recipes: + - BluespaceMatterBinStockPart + +# Subspace stock parts (REMOVE THESE) + +- type: entity + id: AnsibleSubspaceStockPart + name: subspace ansible + parent: BaseStockPart + description: A compact module capable of sensing extradimensional activity. + components: + - type: Sprite + state: subspace_ansible + +- type: entity + id: FilterSubspaceStockPart + name: hyperwave filter + parent: BaseStockPart + description: A tiny device capable of filtering and converting super-intense radiowaves. + components: + - type: Sprite + state: hyperwave_filter + +- type: entity + id: AmplifierSubspaceStockPart + name: subspace amplifier + parent: BaseStockPart + description: A compact micro-machine capable of amplifying weak subspace transmissions. + components: + - type: Sprite + state: subspace_amplifier + +- type: entity + id: TreatmentSubspaceStockPart + name: subspace treatment disk + parent: BaseStockPart + description: A compact micro-machine capable of stretching out hyper-compressed radio waves. + components: + - type: Sprite + state: treatment_disk + +- type: entity + id: AnalyzerSubspaceStockPart + name: subspace wavelength analyzer + parent: BaseStockPart + description: A sophisticated analyzer capable of analyzing cryptic subspace wavelengths. + components: + - type: Sprite + state: wavelength_analyzer + +- type: entity + id: CrystalSubspaceStockPart + name: ansible crystal + parent: BaseStockPart + description: A crystal made from pure glass used to transmit laser databursts to subspace. + components: + - type: Sprite + state: ansible_crystal + +- type: entity + id: TransmitterSubspaceStockPart + name: subspace transmitter + parent: BaseStockPart + description: A large piece of equipment used to open a window into the subspace dimension. + components: + - type: Sprite + state: subspace_transmitter diff --git a/Resources/Prototypes/Entities/Objects/Specific/Research/rped.yml b/Resources/Prototypes/Entities/Objects/Specific/Research/rped.yml index 84a51b4f345..fca8c8ae85f 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Research/rped.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Research/rped.yml @@ -10,6 +10,9 @@ - type: Item sprite: Objects/Specific/Research/rped.rsi size: Normal + - type: GuideHelp + guides: + - MachineUpgrading - type: PartExchanger - type: Storage grid: diff --git a/Resources/Prototypes/Entities/Structures/Dispensers/chem.yml b/Resources/Prototypes/Entities/Structures/Dispensers/chem.yml index 681f0a390c8..6e331a13a8c 100644 --- a/Resources/Prototypes/Entities/Structures/Dispensers/chem.yml +++ b/Resources/Prototypes/Entities/Structures/Dispensers/chem.yml @@ -31,6 +31,9 @@ acts: ["Destruction"] - type: Machine board: ChemDispenserMachineCircuitboard + - type: UpgradePowerDraw + powerDrawMultiplier: 0.75 + scaling: Exponential - type: GuideHelp guides: - Chemicals diff --git a/Resources/Prototypes/Entities/Structures/Machines/anomaly_equipment.yml b/Resources/Prototypes/Entities/Structures/Machines/anomaly_equipment.yml index ab09a03fef7..e6f08fe8467 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/anomaly_equipment.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/anomaly_equipment.yml @@ -176,8 +176,8 @@ SetParticleZeta: AnomalousParticleZeta SetParticleSigma: AnomalousParticleSigma fireBurstSize: 1 - fireBurstDelayMin: 2 - fireBurstDelayMax: 6 + baseFireBurstDelayMin: 2 + baseFireBurstDelayMax: 6 - type: ApcPowerReceiver powerLoad: 100 - type: GuideHelp diff --git a/Resources/Prototypes/Entities/Structures/Machines/anomaly_sync.yml b/Resources/Prototypes/Entities/Structures/Machines/anomaly_sync.yml index 3019b8adf83..eeaae611c37 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/anomaly_sync.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/anomaly_sync.yml @@ -41,7 +41,7 @@ bounds: "-0.35,-0.35,0.35,0.35" density: 100 mask: - - ItemMask + - ItemMask hard: True - type: Transform anchored: true @@ -49,6 +49,9 @@ - type: ApcPowerReceiver powerLoad: 2500 needsPower: true + - type: UpgradePowerDraw + powerDrawMultiplier: 0.80 + scaling: Exponential - type: ItemPlacer whitelist: components: diff --git a/Resources/Prototypes/Entities/Structures/Machines/artifact_analyzer.yml b/Resources/Prototypes/Entities/Structures/Machines/artifact_analyzer.yml index 18999a6ab2a..2bea530e908 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/artifact_analyzer.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/artifact_analyzer.yml @@ -107,6 +107,9 @@ hard: False - type: Transform noRot: false + - type: UpgradePowerDraw + powerDrawMultiplier: 0.80 + scaling: Exponential - type: TraversalDistorter - type: ItemPlacer # don't limit the number of artifacts that can be biased diff --git a/Resources/Prototypes/Entities/Structures/Machines/lathe.yml b/Resources/Prototypes/Entities/Structures/Machines/lathe.yml index e5a9cb35417..52e9096791b 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/lathe.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/lathe.yml @@ -294,6 +294,12 @@ - Implanter - PillCanister - ChemistryEmptyBottle01 + - AdvancedCapacitorStockPart + - AdvancedMatterBinStockPart + - NanoManipulatorStockPart + - SuperCapacitorStockPart + - SuperMatterBinStockPart + - PicoManipulatorStockPart - AdvMopItem - WeaponSprayNozzle - ClothingBackpackWaterTank diff --git a/Resources/Prototypes/Entities/Structures/Machines/seed_extractor.yml b/Resources/Prototypes/Entities/Structures/Machines/seed_extractor.yml index ec07294bfb1..4ecd1c29e27 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/seed_extractor.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/seed_extractor.yml @@ -35,3 +35,6 @@ - type: SeedExtractor - type: Machine board: SeedExtractorMachineCircuitboard + - type: UpgradePowerDraw + powerDrawMultiplier: 0.75 + scaling: Exponential diff --git a/Resources/Prototypes/Entities/Structures/Power/Generation/portable_generator.yml b/Resources/Prototypes/Entities/Structures/Power/Generation/portable_generator.yml index 4fa1bcbd918..35d65ffe87a 100644 --- a/Resources/Prototypes/Entities/Structures/Power/Generation/portable_generator.yml +++ b/Resources/Prototypes/Entities/Structures/Power/Generation/portable_generator.yml @@ -171,6 +171,9 @@ materialWhiteList: [Plasma] - type: PortableGenerator startChance: 0.8 + - type: UpgradePowerSupplier + powerSupplyMultiplier: 1.25 + scaling: Exponential - type: GeneratorExhaustGas gasType: CarbonDioxide # 2 moles of gas for every sheet of plasma. @@ -228,6 +231,9 @@ storageLimit: 3000 materialWhiteList: [Uranium] - type: PortableGenerator + - type: UpgradePowerSupplier + powerSupplyMultiplier: 1.25 + scaling: Exponential - type: PowerMonitoringDevice group: Generator loadNodes: diff --git a/Resources/Prototypes/Entities/Structures/Power/smes.yml b/Resources/Prototypes/Entities/Structures/Power/smes.yml index 1e3559e5a95..762e8d375f6 100644 --- a/Resources/Prototypes/Entities/Structures/Power/smes.yml +++ b/Resources/Prototypes/Entities/Structures/Power/smes.yml @@ -29,10 +29,15 @@ state: "smes-op1" shader: unshaded - type: Smes + - type: UpgradeBattery + maxChargeMultiplier: 2 + baseMaxCharge: 8000000 + - type: UpgradePowerSupplyRamping + scaling: Linear + supplyRampingMultiplier: 1 - type: Appearance - type: Battery startingCharge: 0 - maxCharge: 8000000 - type: ExaminableBattery - type: NodeContainer examinable: true diff --git a/Resources/Prototypes/Entities/Structures/Power/substation.yml b/Resources/Prototypes/Entities/Structures/Power/substation.yml index 347b18ecaee..489cfff6597 100644 --- a/Resources/Prototypes/Entities/Structures/Power/substation.yml +++ b/Resources/Prototypes/Entities/Structures/Power/substation.yml @@ -17,8 +17,13 @@ shader: unshaded - state: full shader: unshaded + - type: UpgradeBattery + maxChargeMultiplier: 2 + baseMaxCharge: 2500000 + - type: UpgradePowerSupplyRamping + scaling: Linear + supplyRampingMultiplier: 1 - type: Battery - maxCharge: 2500000 startingCharge: 0 - type: ExaminableBattery - type: PointLight diff --git a/Resources/Prototypes/Entities/Structures/Shuttles/thrusters.yml b/Resources/Prototypes/Entities/Structures/Shuttles/thrusters.yml index f87be426598..eb299e3f3a6 100644 --- a/Resources/Prototypes/Entities/Structures/Shuttles/thrusters.yml +++ b/Resources/Prototypes/Entities/Structures/Shuttles/thrusters.yml @@ -143,6 +143,7 @@ - type: Thruster thrusterType: Angular requireSpace: false + baseThrust: 2000 thrust: 2000 machinePartThrust: Manipulator - type: Sprite @@ -192,6 +193,9 @@ collection: MetalBreak - !type:ChangeConstructionNodeBehavior node: machineFrame + - type: UpgradePowerDraw + powerDrawMultiplier: 0.75 + scaling: Exponential - type: Damageable damageContainer: Inorganic damageModifierSet: Electronic @@ -216,6 +220,7 @@ - type: Thruster thrusterType: Angular requireSpace: false + baseThrust: 100 thrust: 100 - type: ApcPowerReceiver needsPower: false diff --git a/Resources/Prototypes/Guidebook/science.yml b/Resources/Prototypes/Guidebook/science.yml index a21be1678ce..c73b666f687 100644 --- a/Resources/Prototypes/Guidebook/science.yml +++ b/Resources/Prototypes/Guidebook/science.yml @@ -7,6 +7,7 @@ - AnomalousResearch - Xenoarchaeology - Robotics + - MachineUpgrading - Psionics # Nyanotrasen - Psionics guidebook # - AltarsGolemancy # When it's added # Nyanotrasen - Golemancy guidebook - ReverseEngineering # Nyanotrasen - Reverse Engineering guidebook @@ -60,6 +61,11 @@ name: guide-entry-traversal-distorter text: "/ServerInfo/Guidebook/Science/TraversalDistorter.xml" +- type: guideEntry + id: MachineUpgrading + name: guide-entry-machine-upgrading + text: "/ServerInfo/Guidebook/Science/MachineUpgrading.xml" + - type: guideEntry id: Cyborgs name: guide-entry-cyborgs diff --git a/Resources/Prototypes/Recipes/Lathes/Parts.yml b/Resources/Prototypes/Recipes/Lathes/Parts.yml index 90cff2174d6..496bc3a8a48 100644 --- a/Resources/Prototypes/Recipes/Lathes/Parts.yml +++ b/Resources/Prototypes/Recipes/Lathes/Parts.yml @@ -1,3 +1,4 @@ +#Rating 1 - type: latheRecipe id: CapacitorStockPart result: CapacitorStockPart @@ -24,3 +25,62 @@ materials: Steel: 50 Plastic: 50 + +#Rating 2 +- type: latheRecipe + id: AdvancedCapacitorStockPart + result: AdvancedCapacitorStockPart + completetime: 3 + materials: + Steel: 80 + Plastic: 80 + Plasma: 75 + +- type: latheRecipe + id: AdvancedMatterBinStockPart + result: AdvancedMatterBinStockPart + completetime: 3 + materials: + Steel: 80 + Plastic: 80 + Plasma: 75 + +- type: latheRecipe + id: NanoManipulatorStockPart + result: NanoManipulatorStockPart + completetime: 3 + materials: + Steel: 80 + Plastic: 80 + Plasma: 75 + +#Rating 3 +- type: latheRecipe + id: SuperCapacitorStockPart + result: SuperCapacitorStockPart + completetime: 3 + materials: + Steel: 150 + Plastic: 150 + Plasma: 75 + Gold: 75 + +- type: latheRecipe + id: SuperMatterBinStockPart + result: SuperMatterBinStockPart + completetime: 3 + materials: + Steel: 150 + Plastic: 150 + Plasma: 75 + Gold: 75 + +- type: latheRecipe + id: PicoManipulatorStockPart + result: PicoManipulatorStockPart + completetime: 3 + materials: + Steel: 150 + Plastic: 150 + Plasma: 75 + Gold: 75 diff --git a/Resources/Prototypes/Research/experimental.yml b/Resources/Prototypes/Research/experimental.yml index d46e1db144e..85523033f86 100644 --- a/Resources/Prototypes/Research/experimental.yml +++ b/Resources/Prototypes/Research/experimental.yml @@ -84,6 +84,20 @@ # Tier 2 +- type: technology + id: AdvancedParts + name: research-technology-advanced-parts + icon: + sprite: Objects/Misc/stock_parts.rsi + state: advanced_matter_bin + discipline: Experimental + tier: 2 + cost: 10000 + recipeUnlocks: + - AdvancedCapacitorStockPart + - AdvancedMatterBinStockPart + - NanoManipulatorStockPart + - type: technology id: AbnormalArtifactManipulation name: research-technology-abnormal-artifact-manipulation @@ -155,6 +169,20 @@ # Tier 3 +- type: technology + id: SuperParts + name: research-technology-super-parts + icon: + sprite: Objects/Misc/stock_parts.rsi + state: super_matter_bin + discipline: Experimental + tier: 3 + cost: 15000 + recipeUnlocks: + - SuperCapacitorStockPart + - SuperMatterBinStockPart + - PicoManipulatorStockPart + #- type: technology # DeltaV - LRP # id: GravityManipulation # name: research-technology-gravity-manipulation diff --git a/Resources/Prototypes/XenoArch/Effects/normal_effects.yml b/Resources/Prototypes/XenoArch/Effects/normal_effects.yml index b9564c0366b..0a0b9bcbcc3 100644 --- a/Resources/Prototypes/XenoArch/Effects/normal_effects.yml +++ b/Resources/Prototypes/XenoArch/Effects/normal_effects.yml @@ -584,6 +584,24 @@ messages: - shuffle-artifact-popup +- type: artifactEffect + id: EffectT4PartsSpawn + targetDepth: 3 + effectHint: artifact-effect-hint-creation + components: + - type: SpawnArtifact + maxSpawns: 10 + spawns: + - id: QuadraticCapacitorStockPart + prob: 0.5 + maxAmount: 3 + - id: FemtoManipulatorStockPart + prob: 0.5 + maxAmount: 3 + - id: BluespaceMatterBinStockPart + prob: 0.5 + maxAmount: 3 + - type: artifactEffect id: EffectFoamDangerous targetDepth: 3 diff --git a/Resources/ServerInfo/Guidebook/Science/MachineUpgrading.xml b/Resources/ServerInfo/Guidebook/Science/MachineUpgrading.xml new file mode 100644 index 00000000000..286219b4d9e --- /dev/null +++ b/Resources/ServerInfo/Guidebook/Science/MachineUpgrading.xml @@ -0,0 +1,38 @@ + +# Machine Upgrading + +Machines help the station run smoothly, and as a scientist, you can help them run even better! + +## Parts +Stock Parts: + + + + + +You can examine each machine part to see both the type and the rating, which range from 1 to 4. + +Parts of higher levels can be researched as well as found through artifacts or salvage. + + + + + + +## Upgrading +To know if a machine can be upgraded, you can examine it and check for the [color=#a4885c]lightning bolt[/color] icon in the lower right corner. Clicking on it will allow you to see what kinds of upgrades the machine has. + +To check what parts a machine needs, you can examine its board. Try it here: + + + + + + +You can use any rating part for any part requirement. Using higher rated parts will increase how effective the machine is. + +If you want to upgrade an existing machine, simply deconstruct it with a screwdriver and crowbar, and replace the existing parts with parts of a higher level. + +You can also quickly upgrade machines by using an RPED, loading it with machine parts, and then clicking on a machine. It will quickly be upgraded with whatever parts were inserted. + + From f126bb4149d01050760818211bcec10d0a418f2f Mon Sep 17 00:00:00 2001 From: VMSolidus Date: Sat, 21 Sep 2024 18:46:59 -0400 Subject: [PATCH 49/55] [Port] Uplink Discounts From White Dream (#930) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Description This is a port of https://github.com/WWhiteDreamProject/wwdpublic/pull/11 from White Dream. This feature selects random items in the traitor uplink each round to be discounted and moved to the Discount tab, which are the same for every traitor. This in theory helps encourage players to be spontaneous, and use items that they otherwise might not normally consider using, which helps mix things up from round to round.

Media

> # Описание PR > Порт скидок в аплинке. > > # Изменения > 🆑 Spatison > > * add: Added discounts in uplink / Добавлены скидки в аплинк

# Changelog :cl: Spatison add: Added discounts in uplink / Добавлены скидки в аплинк --------- Signed-off-by: VMSolidus Co-authored-by: Spatison <137375981+Spatison@users.noreply.github.com> Co-authored-by: DEATHB4DEFEAT <77995199+DEATHB4DEFEAT@users.noreply.github.com> --- Content.Client/Store/Ui/StoreMenu.xaml.cs | 5 ++ .../Store/Systems/StoreSystem.Ui.cs | 6 ++ Content.Server/Store/Systems/StoreSystem.cs | 6 +- .../StoreDiscount/StoreDiscountSystem.cs | 55 +++++++++++++++++++ .../Store/ListingLocalisationHelpers.cs | 5 ++ Content.Shared/Store/ListingPrototype.cs | 18 ++++++ Content.Shared/Store/StorePresetPrototype.cs | 4 ++ .../StoreDiscount/SalesSpecifier.cs | 38 +++++++++++++ Resources/Locale/en-US/store/sales.ftl | 2 + Resources/Locale/ru-RU/store/sales.ftl | 2 + .../Prototypes/Catalog/uplink_catalog.yml | 38 ++++++++++++- Resources/Prototypes/Store/presets.yml | 8 +++ .../Prototypes/_White/Store/categories.yml | 4 ++ 13 files changed, 189 insertions(+), 2 deletions(-) create mode 100644 Content.Server/StoreDiscount/StoreDiscountSystem.cs create mode 100644 Content.Shared/StoreDiscount/SalesSpecifier.cs create mode 100644 Resources/Locale/en-US/store/sales.ftl create mode 100644 Resources/Locale/ru-RU/store/sales.ftl create mode 100644 Resources/Prototypes/_White/Store/categories.yml diff --git a/Content.Client/Store/Ui/StoreMenu.xaml.cs b/Content.Client/Store/Ui/StoreMenu.xaml.cs index b7a2c285fe5..7eb597f2f39 100644 --- a/Content.Client/Store/Ui/StoreMenu.xaml.cs +++ b/Content.Client/Store/Ui/StoreMenu.xaml.cs @@ -3,6 +3,7 @@ using Content.Client.Message; using Content.Shared.FixedPoint; using Content.Shared.Store; +using Content.Client.Stylesheets; using Robust.Client.AutoGenerated; using Robust.Client.GameObjects; using Robust.Client.Graphics; @@ -147,6 +148,10 @@ private void AddListingGui(ListingData listing) } var newListing = new StoreListingControl(listing, GetListingPriceString(listing), hasBalance, texture); + + if (listing.DiscountValue > 0) + newListing.StoreItemBuyButton.AddStyleClass(StyleNano.ButtonColorDangerDefault.ToString()); + newListing.StoreItemBuyButton.OnButtonDown += args => OnListingButtonPressed?.Invoke(args, listing); diff --git a/Content.Server/Store/Systems/StoreSystem.Ui.cs b/Content.Server/Store/Systems/StoreSystem.Ui.cs index 25f64ba4b64..7187a704768 100644 --- a/Content.Server/Store/Systems/StoreSystem.Ui.cs +++ b/Content.Server/Store/Systems/StoreSystem.Ui.cs @@ -265,6 +265,12 @@ private void OnBuyRequest(EntityUid uid, StoreComponent component, StoreBuyListi listing.PurchaseAmount++; //track how many times something has been purchased _audio.PlayEntity(component.BuySuccessSound, msg.Session, uid); //cha-ching! + if (listing.SaleLimit != 0 && listing.DiscountValue > 0 && listing.PurchaseAmount >= listing.SaleLimit) + { + listing.DiscountValue = 0; + listing.Cost = listing.OldCost; + } + UpdateUserInterface(buyer, uid, component); } diff --git a/Content.Server/Store/Systems/StoreSystem.cs b/Content.Server/Store/Systems/StoreSystem.cs index 72aeb29d195..a86dffef633 100644 --- a/Content.Server/Store/Systems/StoreSystem.cs +++ b/Content.Server/Store/Systems/StoreSystem.cs @@ -10,6 +10,7 @@ using Robust.Server.GameObjects; using Robust.Shared.Prototypes; using System.Linq; +using Content.Server.StoreDiscount; using Robust.Shared.Utility; namespace Content.Server.Store.Systems; @@ -22,6 +23,7 @@ public sealed partial class StoreSystem : EntitySystem { [Dependency] private readonly IPrototypeManager _proto = default!; [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly StoreDiscountSystem _storeDiscount = default!; public override void Initialize() { @@ -199,6 +201,8 @@ public void InitializeFromPreset(StorePresetPrototype preset, EntityUid uid, Sto if (component.Balance == new Dictionary() && preset.InitialBalance != null) //if we don't have a value stored, use the preset TryAddCurrency(preset.InitialBalance, uid, component); + _storeDiscount.ApplyDiscounts(component.Listings, preset); + var ui = _ui.GetUiOrNull(uid, StoreUiKey.Key); if (ui != null) { @@ -225,7 +229,7 @@ public CurrencyInsertAttemptEvent(EntityUid user, EntityUid target, EntityUid us /// -/// Nyano/DeltaV Code. For penguin bombs and what not. +/// Nyano/DeltaV Code. For penguin bombs and what not. /// Raised on an item when it is purchased. /// An item may need to set it upself up for its purchaser. /// For example, to make sure it isn't hostile to them or diff --git a/Content.Server/StoreDiscount/StoreDiscountSystem.cs b/Content.Server/StoreDiscount/StoreDiscountSystem.cs new file mode 100644 index 00000000000..8f77eba7801 --- /dev/null +++ b/Content.Server/StoreDiscount/StoreDiscountSystem.cs @@ -0,0 +1,55 @@ +using System.Linq; +using Content.Shared.FixedPoint; +using Content.Shared.Store; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; + +namespace Content.Server.StoreDiscount; + +public sealed class StoreDiscountSystem : EntitySystem +{ + [Dependency] private readonly IRobustRandom _random = default!; + + public void ApplyDiscounts(IEnumerable listings, StorePresetPrototype store) + { + if (!store.Sales.Enabled) + return; + + var count = _random.Next(store.Sales.MinItems, store.Sales.MaxItems + 1); + + listings = listings + .Where(l => + !l.SaleBlacklist + && l.Cost.Any(x => x.Value > 1) + && store.Categories.Overlaps(ChangedFormatCategories(l.Categories))) + .OrderBy(_ => _random.Next()).Take(count).ToList(); + + foreach (var listing in listings) + { + var sale = GetDiscount(store.Sales.MinMultiplier, store.Sales.MaxMultiplier); + var newCost = listing.Cost.ToDictionary(x => x.Key, + x => FixedPoint2.New(Math.Max(1, (int) MathF.Round(x.Value.Float() * sale)))); + + if (listing.Cost.All(x => x.Value.Int() == newCost[x.Key].Int())) + continue; + + var key = listing.Cost.First(x => x.Value > 0).Key; + listing.OldCost = listing.Cost; + listing.DiscountValue = 100 - (newCost[key] / listing.Cost[key] * 100).Int(); + listing.Cost = newCost; + listing.Categories = new() {store.Sales.SalesCategory}; + } + } + + private IEnumerable ChangedFormatCategories(List> categories) + { + var modified = from p in categories select p.Id; + + return modified; + } + + private float GetDiscount(float minMultiplier, float maxMultiplier) + { + return _random.NextFloat() * (maxMultiplier - minMultiplier) + minMultiplier; + } +} diff --git a/Content.Shared/Store/ListingLocalisationHelpers.cs b/Content.Shared/Store/ListingLocalisationHelpers.cs index 882300109ce..19cd029488f 100644 --- a/Content.Shared/Store/ListingLocalisationHelpers.cs +++ b/Content.Shared/Store/ListingLocalisationHelpers.cs @@ -18,6 +18,11 @@ public static string GetLocalisedNameOrEntityName(ListingData listingData, IProt else if (listingData.ProductEntity != null) name = prototypeManager.Index(listingData.ProductEntity.Value).Name; + if (listingData.DiscountValue > 0) + name += " " + Loc.GetString("store-sales-amount", ("amount", listingData.DiscountValue)); + else if (listingData.OldCost.Count > 0) + name += " " + Loc.GetString("store-sales-over"); + return name; } diff --git a/Content.Shared/Store/ListingPrototype.cs b/Content.Shared/Store/ListingPrototype.cs index d3d2e13cdfd..0b59ab48cb6 100644 --- a/Content.Shared/Store/ListingPrototype.cs +++ b/Content.Shared/Store/ListingPrototype.cs @@ -109,6 +109,19 @@ public partial class ListingData : IEquatable, ICloneable [DataField] public TimeSpan RestockTime = TimeSpan.Zero; + [DataField] + public int SaleLimit = 3; + + [DataField] + public bool SaleBlacklist; + + public int DiscountValue; + + public Dictionary, FixedPoint2> OldCost = new(); + + [DataField] + public List Components = new(); + public bool Equals(ListingData? listing) { if (listing == null) @@ -166,6 +179,11 @@ public object Clone() ProductEvent = ProductEvent, PurchaseAmount = PurchaseAmount, RestockTime = RestockTime, + SaleLimit = SaleLimit, + SaleBlacklist = SaleBlacklist, + DiscountValue = DiscountValue, + OldCost = OldCost, + Components = Components, }; } } diff --git a/Content.Shared/Store/StorePresetPrototype.cs b/Content.Shared/Store/StorePresetPrototype.cs index ce7f0312b60..41ee510bd8e 100644 --- a/Content.Shared/Store/StorePresetPrototype.cs +++ b/Content.Shared/Store/StorePresetPrototype.cs @@ -1,3 +1,4 @@ +using Content.Shared.StoreDiscount; using Robust.Shared.Prototypes; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary; @@ -38,4 +39,7 @@ public sealed partial class StorePresetPrototype : IPrototype /// [DataField("currencyWhitelist", customTypeSerializer: typeof(PrototypeIdHashSetSerializer))] public HashSet CurrencyWhitelist { get; private set; } = new(); + + [DataField] + public SalesSpecifier Sales { get; private set; } = new(); } diff --git a/Content.Shared/StoreDiscount/SalesSpecifier.cs b/Content.Shared/StoreDiscount/SalesSpecifier.cs new file mode 100644 index 00000000000..21539f85d41 --- /dev/null +++ b/Content.Shared/StoreDiscount/SalesSpecifier.cs @@ -0,0 +1,38 @@ +namespace Content.Shared.StoreDiscount; + +[DataDefinition] +public sealed partial class SalesSpecifier +{ + [DataField] + public bool Enabled { get; private set; } + + [DataField] + public float MinMultiplier { get; private set; } + + [DataField] + public float MaxMultiplier { get; private set; } + + [DataField] + public int MinItems { get; private set; } + + [DataField] + public int MaxItems { get; private set; } + + [DataField] + public string SalesCategory { get; private set; } = string.Empty; + + public SalesSpecifier() + { + } + + public SalesSpecifier(bool enabled, float minMultiplier, float maxMultiplier, int minItems, int maxItems, + string salesCategory) + { + Enabled = enabled; + MinMultiplier = minMultiplier; + MaxMultiplier = maxMultiplier; + MinItems = minItems; + MaxItems = maxItems; + SalesCategory = salesCategory; + } +} diff --git a/Resources/Locale/en-US/store/sales.ftl b/Resources/Locale/en-US/store/sales.ftl new file mode 100644 index 00000000000..7223a8a0dcd --- /dev/null +++ b/Resources/Locale/en-US/store/sales.ftl @@ -0,0 +1,2 @@ +store-sales-amount = [DISCOUNT] { $amount }%! +store-sales-over = [The sale is over] diff --git a/Resources/Locale/ru-RU/store/sales.ftl b/Resources/Locale/ru-RU/store/sales.ftl new file mode 100644 index 00000000000..93f1798fe79 --- /dev/null +++ b/Resources/Locale/ru-RU/store/sales.ftl @@ -0,0 +1,2 @@ +store-sales-amount = [СКИДКА] { $amount }%! +store-sales-over = [Скидка закончилась] diff --git a/Resources/Prototypes/Catalog/uplink_catalog.yml b/Resources/Prototypes/Catalog/uplink_catalog.yml index a1a60e3fef0..bdd27e83a89 100644 --- a/Resources/Prototypes/Catalog/uplink_catalog.yml +++ b/Resources/Prototypes/Catalog/uplink_catalog.yml @@ -10,6 +10,7 @@ Telecrystal: 3 categories: - UplinkWeapons + saleLimit: 1 - type: listing id: UplinkRevolverPython @@ -20,6 +21,7 @@ Telecrystal: 8 # Originally was 13 TC but was not used due to high cost categories: - UplinkWeapons + saleLimit: 1 # Inbuilt suppressor so it's sneaky + more expensive. - type: listing @@ -31,6 +33,7 @@ Telecrystal: 4 categories: - UplinkWeapons + saleLimit: 1 # Poor accuracy, slow to fire, cheap option - type: listing @@ -42,6 +45,7 @@ Telecrystal: 1 categories: - UplinkWeapons + saleLimit: 1 - type: listing id: UplinkEsword @@ -53,6 +57,7 @@ Telecrystal: 8 categories: - UplinkWeapons + saleLimit: 2 - type: listing id: UplinkEnergyDagger @@ -64,6 +69,7 @@ Telecrystal: 2 categories: - UplinkWeapons + saleLimit: 1 - type: listing id: UplinkThrowingKnivesKit @@ -85,6 +91,7 @@ Telecrystal: 8 categories: - UplinkWeapons + saleLimit: 1 - type: listing id: UplinkDisposableTurret @@ -100,6 +107,7 @@ blacklist: tags: - NukeOpsUplink + saleLimit: 2 - type: listing id: BaseBallBatHomeRun @@ -112,6 +120,7 @@ Telecrystal: 16 categories: - UplinkWeapons + saleLimit: 1 # Explosives @@ -214,6 +223,7 @@ whitelist: tags: - NukeOpsUplink + saleLimit: 1 - type: listing id: UplinkC4Bundle @@ -224,6 +234,7 @@ Telecrystal: 12 #you're buying bulk so its a 25% discount categories: - UplinkExplosives + saleLimit: 1 - type: listing id: UplinkEmpGrenade @@ -261,6 +272,7 @@ blacklist: tags: - NukeOpsUplink + saleLimit: 1 - type: listing id: UplinkSyndicateBombNukie @@ -276,6 +288,7 @@ whitelist: tags: - NukeOpsUplink + saleLimit: 1 - type: listing id: UplinkClusterGrenade @@ -451,6 +464,7 @@ blacklist: tags: - NukeOpsUplink + saleLimit: 1 - type: listing id: UplinkReinforcementRadioSyndicateNukeops # Version for Nukeops that spawns an agent with the NukeOperative component. @@ -467,6 +481,7 @@ whitelist: tags: - NukeOpsUplink + saleLimit: 1 - type: listing id: UplinkReinforcementRadioSyndicateCyborgAssault @@ -483,6 +498,7 @@ whitelist: tags: - NukeOpsUplink + saleLimit: 1 - type: listing id: UplinkReinforcementRadioSyndicateMonkey @@ -515,6 +531,7 @@ whitelist: tags: - NukeOpsUplink + saleLimit: 1 - type: listing id: UplinkStealthBox @@ -545,7 +562,6 @@ productEntity: EncryptionKeyBinary cost: Telecrystal: 1 - categories: - UplinkUtility @@ -748,6 +764,7 @@ blacklist: tags: - NukeOpsUplink + saleBlacklist: true - type: listing id: UplinkDeathRattle @@ -821,6 +838,7 @@ blacklist: components: - SurplusBundle + saleLimit: 1 - type: listing id: UplinkChemistryKitBundle @@ -862,6 +880,7 @@ blacklist: components: - SurplusBundle + saleLimit: 1 - type: listing id: UplinkSniperBundle @@ -873,6 +892,7 @@ Telecrystal: 12 categories: - UplinkBundles + saleLimit: 1 - type: listing id: UplinkC20RBundle @@ -884,6 +904,7 @@ Telecrystal: 17 categories: - UplinkBundles + saleLimit: 1 - type: listing id: UplinkBulldogBundle @@ -895,6 +916,7 @@ Telecrystal: 20 categories: - UplinkBundles + saleLimit: 1 - type: listing id: UplinkGrenadeLauncherBundle @@ -906,6 +928,7 @@ Telecrystal: 25 categories: - UplinkBundles + saleLimit: 1 - type: listing id: UplinkL6SawBundle @@ -917,6 +940,7 @@ Telecrystal: 30 categories: - UplinkBundles + saleLimit: 1 - type: listing id: UplinkZombieBundle @@ -937,6 +961,7 @@ blacklist: components: - SurplusBundle + saleLimit: 1 - type: listing id: UplinkSurplusBundle @@ -956,6 +981,7 @@ blacklist: components: - SurplusBundle + saleBlacklist: true - type: listing id: UplinkSuperSurplusBundle @@ -975,6 +1001,7 @@ blacklist: components: - SurplusBundle + saleBlacklist: true # Tools @@ -1113,6 +1140,7 @@ - !type:BuyerJobCondition whitelist: - Chaplain + saleLimit: 1 - type: listing id: uplinkRevolverCapGunFake @@ -1128,6 +1156,7 @@ whitelist: - Mime - Clown + saleLimit: 1 - type: listing id: uplinkBananaPeelExplosive @@ -1172,6 +1201,7 @@ - !type:BuyerJobCondition whitelist: - Clown + saleLimit: 1 - type: listing id: uplinkHotPotato @@ -1335,6 +1365,7 @@ Telecrystal: 8 categories: - UplinkArmor + saleLimit: 1 - type: listing id: UplinkHardsuitSyndieElite @@ -1346,6 +1377,7 @@ Telecrystal: 10 categories: - UplinkArmor + saleLimit: 1 - type: listing id: UplinkClothingOuterHardsuitJuggernaut @@ -1357,6 +1389,7 @@ Telecrystal: 12 categories: - UplinkArmor + saleLimit: 1 # Misc @@ -1425,6 +1458,7 @@ whitelist: tags: - NukeOpsUplink + saleLimit: 1 - type: listing id: UplinkSoapSyndie @@ -1515,6 +1549,7 @@ Telecrystal: 12 categories: - UplinkMisc + saleLimit: 1 - type: listing id: UplinkBribe @@ -1541,6 +1576,7 @@ whitelist: tags: - NukeOpsUplink + saleLimit: 1 - type: listing id: UplinkBackpackSyndicate diff --git a/Resources/Prototypes/Store/presets.yml b/Resources/Prototypes/Store/presets.yml index e623f4c8cd6..bbf96997956 100644 --- a/Resources/Prototypes/Store/presets.yml +++ b/Resources/Prototypes/Store/presets.yml @@ -13,5 +13,13 @@ - UplinkJob - UplinkArmor - UplinkPointless + - UplinkSales currencyWhitelist: - Telecrystal + sales: + enabled: true + minMultiplier: 0.2 + maxMultiplier: 0.8 + minItems: 3 + maxItems: 8 + salesCategory: UplinkSales diff --git a/Resources/Prototypes/_White/Store/categories.yml b/Resources/Prototypes/_White/Store/categories.yml new file mode 100644 index 00000000000..cb9cfbc88f8 --- /dev/null +++ b/Resources/Prototypes/_White/Store/categories.yml @@ -0,0 +1,4 @@ +- type: storeCategory + id: UplinkSales + name: Sales + priority: 10 From 331d68d0681fba2fddda866fd63f5fc0ad4ae488 Mon Sep 17 00:00:00 2001 From: SimpleStation Changelogs Date: Sat, 21 Sep 2024 22:47:34 +0000 Subject: [PATCH 50/55] Automatic Changelog Update (#917) --- Resources/Changelog/Changelog.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index f4bc13c1173..bbb35f6773e 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -6576,3 +6576,10 @@ Entries: id: 6381 time: '2024-09-20T21:47:16.0000000+00:00' url: https://github.com/Simple-Station/Einstein-Engines/pull/940 +- author: VMSolidus + changes: + - type: Add + message: Part Upgrading has returned! + id: 6382 + time: '2024-09-21T22:46:49.0000000+00:00' + url: https://github.com/Simple-Station/Einstein-Engines/pull/917 From 1b431230b14a978da5d7230ec09277d57857095a Mon Sep 17 00:00:00 2001 From: SimpleStation Changelogs Date: Sat, 21 Sep 2024 22:48:59 +0000 Subject: [PATCH 51/55] Automatic Changelog Update (#930) --- Resources/Changelog/Changelog.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index bbb35f6773e..2477a35b914 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -6583,3 +6583,10 @@ Entries: id: 6382 time: '2024-09-21T22:46:49.0000000+00:00' url: https://github.com/Simple-Station/Einstein-Engines/pull/917 +- author: Spatison + changes: + - type: Add + message: Added discounts in uplink / Добавлены скидки в аплинк + id: 6383 + time: '2024-09-21T22:46:59.0000000+00:00' + url: https://github.com/Simple-Station/Einstein-Engines/pull/930 From 61e1c8cc97c1d129c273b852c53206df333a52d0 Mon Sep 17 00:00:00 2001 From: VMSolidus Date: Sat, 21 Sep 2024 18:50:00 -0400 Subject: [PATCH 52/55] Psionic "Heal Other" Powers (#942) # Description This PR introduces two new Psionic Powers, Healing Word, and Breath of Life, both utilizing a new PsionicHealOtherSystem, which operates on datafield event arguments rather than a "hardcoded" component. Thus, any number of powers can be created which share this system. Healing Word is a power that features a short cast time, and heals a small amount of each damage type to a target(while reducing the target's rot timer slightly). It has a relatively short cooldown, and a low glimmer cost. Breath of Life by contrast, is an extremely rare power with a longer cast time, healing a much larger amount of each damage type to a target, reduces rot significantly, and attempts to revive the target. It has a 2 minute cooldown, and a high glimmer cost.

Media

![image](https://github.com/user-attachments/assets/ba01ccce-639f-4b03-84bb-55f96b5aeda3)

# Changelog :cl: - add: Healing Word has been added as a new Psionic Power. When cast on another person, it heals a small amount of every damage type(scaling with Casting Stats), while also reducing rot timers. Healing Word has a very short cooldown, and a fairly low Glimmer cost. - add: Breath of Life has been added as a new extremely rare Psionic Power. When cast on another person, it heals a large amount of damage(scaling with Casting Stats), while also substantially reducing rot timers. Additionally, it will revive the target if it is possible to do so. Breath of Life has an incredibly long cooldown, a long interuptable cast time, and an extraordinarily high glimmer cost(A typical Psion will spike glimmer by more than 50 points when casting it). - add: The Chaplain now starts with the Healing Word power. --------- Signed-off-by: VMSolidus Co-authored-by: DEATHB4DEFEAT <77995199+DEATHB4DEFEAT@users.noreply.github.com> --- .../Abilities/HealOtherPowerSystem.cs | 152 ++++++++++++++++++ .../PsionicHealOtherPowerActionEvent.cs | 59 +++++++ Content.Shared/Psionics/Events.cs | 61 +++++-- Content.Shared/Psionics/PsionicComponent.cs | 11 +- .../Locale/en-US/psionics/psionic-powers.ftl | 23 +++ Resources/Prototypes/Actions/psionics.yml | 73 +++++++++ .../Prototypes/Nyanotrasen/psionicPowers.yml | 2 + Resources/Prototypes/Psionics/psionics.yml | 22 +++ .../Roles/Jobs/Civilian/chaplain.yml | 9 +- .../Actions/psionics.rsi/healing_word.png | Bin 0 -> 2189 bytes .../Interface/Actions/psionics.rsi/meta.json | 17 ++ .../Actions/psionics.rsi/revivify.png | Bin 0 -> 1977 bytes .../Interface/VerbIcons/ATTRIBUTION.txt | 3 + 13 files changed, 412 insertions(+), 20 deletions(-) create mode 100644 Content.Server/Abilities/Psionics/Abilities/HealOtherPowerSystem.cs create mode 100644 Content.Shared/Actions/Events/PsionicHealOtherPowerActionEvent.cs create mode 100644 Resources/Textures/Interface/Actions/psionics.rsi/healing_word.png create mode 100644 Resources/Textures/Interface/Actions/psionics.rsi/meta.json create mode 100644 Resources/Textures/Interface/Actions/psionics.rsi/revivify.png diff --git a/Content.Server/Abilities/Psionics/Abilities/HealOtherPowerSystem.cs b/Content.Server/Abilities/Psionics/Abilities/HealOtherPowerSystem.cs new file mode 100644 index 00000000000..85bae78dc6b --- /dev/null +++ b/Content.Server/Abilities/Psionics/Abilities/HealOtherPowerSystem.cs @@ -0,0 +1,152 @@ +using Robust.Shared.Player; +using Content.Server.DoAfter; +using Content.Shared.Abilities.Psionics; +using Content.Shared.Damage; +using Content.Shared.DoAfter; +using Content.Shared.Popups; +using Content.Shared.Psionics.Events; +using Content.Shared.Examine; +using static Content.Shared.Examine.ExamineSystemShared; +using Robust.Shared.Timing; +using Content.Shared.Actions.Events; +using Robust.Server.Audio; +using Content.Server.Atmos.Rotting; +using Content.Shared.Mobs.Systems; +using Content.Shared.Mobs; +using Content.Shared.Mobs.Components; +using Content.Shared.Psionics.Glimmer; + +namespace Content.Server.Abilities.Psionics; + +public sealed class RevivifyPowerSystem : EntitySystem +{ + [Dependency] private readonly AudioSystem _audioSystem = default!; + [Dependency] private readonly DoAfterSystem _doAfterSystem = default!; + [Dependency] private readonly SharedPopupSystem _popupSystem = default!; + [Dependency] private readonly SharedPsionicAbilitiesSystem _psionics = default!; + [Dependency] private readonly IGameTiming _gameTiming = default!; + [Dependency] private readonly ExamineSystemShared _examine = default!; + [Dependency] private readonly DamageableSystem _damageable = default!; + [Dependency] private readonly RottingSystem _rotting = default!; + [Dependency] private readonly MobThresholdSystem _mobThreshold = default!; + [Dependency] private readonly MobStateSystem _mobState = default!; + [Dependency] private readonly GlimmerSystem _glimmer = default!; + + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnPowerUsed); + SubscribeLocalEvent(OnDispelled); + SubscribeLocalEvent(OnDoAfter); + } + + + private void OnPowerUsed(EntityUid uid, PsionicComponent component, PsionicHealOtherPowerActionEvent args) + { + if (component.DoAfter is not null) + return; + + if (!args.Immediate) + AttemptDoAfter(uid, component, args); + else ActivatePower(uid, component, args); + + if (args.PopupText is not null + && _glimmer.Glimmer > args.GlimmerObviousPopupThreshold * component.CurrentDampening) + _popupSystem.PopupEntity(Loc.GetString(args.PopupText, ("entity", uid)), uid, + Filter.Pvs(uid).RemoveWhereAttachedEntity(entity => !_examine.InRangeUnOccluded(uid, entity, ExamineRange, null)), + true, + args.PopupType); + + if (args.PlaySound + && _glimmer.Glimmer > args.GlimmerObviousSoundThreshold * component.CurrentDampening) + _audioSystem.PlayPvs(args.SoundUse, uid, args.AudioParams); + + // Sanitize the Glimmer inputs because otherwise the game will crash if someone makes MaxGlimmer lower than MinGlimmer. + var minGlimmer = (int) Math.Round(MathF.MinMagnitude(args.MinGlimmer, args.MaxGlimmer) + + component.CurrentAmplification - component.CurrentDampening); + var maxGlimmer = (int) Math.Round(MathF.MaxMagnitude(args.MinGlimmer, args.MaxGlimmer) + + component.CurrentAmplification - component.CurrentDampening); + + _psionics.LogPowerUsed(uid, args.PowerName, minGlimmer, maxGlimmer); + args.Handled = true; + } + + private void AttemptDoAfter(EntityUid uid, PsionicComponent component, PsionicHealOtherPowerActionEvent args) + { + var ev = new PsionicHealOtherDoAfterEvent(_gameTiming.CurTime); + ev.HealingAmount = args.HealingAmount; + ev.RotReduction = args.RotReduction; + ev.DoRevive = args.DoRevive; + var doAfterArgs = new DoAfterArgs(EntityManager, uid, args.UseDelay, ev, uid, target: args.Target) + { + BreakOnUserMove = args.BreakOnUserMove, + BreakOnTargetMove = args.BreakOnTargetMove, + }; + + if (!_doAfterSystem.TryStartDoAfter(doAfterArgs, out var doAfterId)) + return; + + component.DoAfter = doAfterId; + } + + private void OnDispelled(EntityUid uid, PsionicComponent component, DispelledEvent args) + { + if (component.DoAfter is null) + return; + + _doAfterSystem.Cancel(component.DoAfter); + component.DoAfter = null; + args.Handled = true; + } + + private void OnDoAfter(EntityUid uid, PsionicComponent component, PsionicHealOtherDoAfterEvent args) + { + // It's entirely possible for the caster to stop being Psionic(due to mindbreaking) mid cast + if (component is null) + return; + component.DoAfter = null; + + // The target can also cease existing mid-cast + if (args.Target is null) + return; + + _rotting.ReduceAccumulator(args.Target.Value, TimeSpan.FromSeconds(args.RotReduction * component.CurrentAmplification)); + + if (!TryComp(args.Target.Value, out var damageableComponent)) + return; + + _damageable.TryChangeDamage(args.Target.Value, args.HealingAmount * component.CurrentAmplification, true, false, damageableComponent, uid); + + if (!args.DoRevive + || !TryComp(args.Target, out var mob) + || !_mobThreshold.TryGetThresholdForState(args.Target.Value, MobState.Dead, out var threshold) + || damageableComponent.TotalDamage > threshold) + return; + + _mobState.ChangeMobState(args.Target.Value, MobState.Critical, mob, uid); + } + + // This would be the same thing as OnDoAfter, except that here the target isn't nullable, so I have to reuse code with different arguments. + private void ActivatePower(EntityUid uid, PsionicComponent component, PsionicHealOtherPowerActionEvent args) + { + if (component is null) + return; + + _rotting.ReduceAccumulator(args.Target, TimeSpan.FromSeconds(args.RotReduction * component.CurrentAmplification)); + + if (!TryComp(args.Target, out var damageableComponent)) + return; + + _damageable.TryChangeDamage(args.Target, args.HealingAmount * component.CurrentAmplification, true, false, damageableComponent, uid); + + if (!args.DoRevive + || !TryComp(args.Target, out var mob) + || !_mobThreshold.TryGetThresholdForState(args.Target, MobState.Dead, out var threshold) + || damageableComponent.TotalDamage > threshold) + return; + + _mobState.ChangeMobState(args.Target, MobState.Critical, mob, uid); + } +} diff --git a/Content.Shared/Actions/Events/PsionicHealOtherPowerActionEvent.cs b/Content.Shared/Actions/Events/PsionicHealOtherPowerActionEvent.cs new file mode 100644 index 00000000000..8cf11b9e66d --- /dev/null +++ b/Content.Shared/Actions/Events/PsionicHealOtherPowerActionEvent.cs @@ -0,0 +1,59 @@ +using Robust.Shared.Audio; +using Content.Shared.Damage; +using Content.Shared.Popups; + +namespace Content.Shared.Actions.Events; +public sealed partial class PsionicHealOtherPowerActionEvent : EntityTargetActionEvent +{ + [DataField] + public DamageSpecifier HealingAmount = default!; + + [DataField] + public string PowerName; + + /// Controls whether or not a power fires immediately and with no DoAfter + [DataField] + public bool Immediate; + + [DataField] + public string? PopupText; + + [DataField] + public float RotReduction; + + [DataField] + public bool DoRevive; + + [DataField] + public bool BreakOnUserMove = true; + + [DataField] + public bool BreakOnTargetMove = false; + + [DataField] + public float UseDelay = 8f; + + [DataField] + public int MinGlimmer = 8; + + [DataField] + public int MaxGlimmer = 12; + + [DataField] + public int GlimmerObviousSoundThreshold; + + [DataField] + public int GlimmerObviousPopupThreshold; + + [DataField] + public PopupType PopupType = PopupType.Medium; + + [DataField] + public AudioParams AudioParams = default!; + + [DataField] + public bool PlaySound; + + [DataField] + public SoundSpecifier SoundUse = new SoundPathSpecifier("/Audio/Psionics/heartbeat_fast.ogg"); +} diff --git a/Content.Shared/Psionics/Events.cs b/Content.Shared/Psionics/Events.cs index cf9a50c6e18..f110c9c405f 100644 --- a/Content.Shared/Psionics/Events.cs +++ b/Content.Shared/Psionics/Events.cs @@ -1,28 +1,59 @@ using Robust.Shared.Serialization; +using Content.Shared.Damage; using Content.Shared.DoAfter; -namespace Content.Shared.Psionics.Events +namespace Content.Shared.Psionics.Events; + +[Serializable, NetSerializable] +public sealed partial class PsionicRegenerationDoAfterEvent : DoAfterEvent { - [Serializable, NetSerializable] - public sealed partial class PsionicRegenerationDoAfterEvent : DoAfterEvent + [DataField("startedAt", required: true)] + public TimeSpan StartedAt; + + public PsionicRegenerationDoAfterEvent(TimeSpan startedAt) { - [DataField("startedAt", required: true)] - public TimeSpan StartedAt; + StartedAt = startedAt; + } + + public override DoAfterEvent Clone() => this; +} - private PsionicRegenerationDoAfterEvent() - { - } +[Serializable, NetSerializable] +public sealed partial class GlimmerWispDrainDoAfterEvent : SimpleDoAfterEvent { } - public PsionicRegenerationDoAfterEvent(TimeSpan startedAt) - { - StartedAt = startedAt; - } +[Serializable, NetSerializable] +public sealed partial class HealingWordDoAfterEvent : DoAfterEvent +{ + [DataField(required: true)] + public TimeSpan StartedAt; - public override DoAfterEvent Clone() => this; + public HealingWordDoAfterEvent(TimeSpan startedAt) + { + StartedAt = startedAt; } - [Serializable, NetSerializable] - public sealed partial class GlimmerWispDrainDoAfterEvent : SimpleDoAfterEvent + public override DoAfterEvent Clone() => this; +} + +[Serializable, NetSerializable] +public sealed partial class PsionicHealOtherDoAfterEvent : DoAfterEvent +{ + [DataField(required: true)] + public TimeSpan StartedAt; + + [DataField] + public DamageSpecifier HealingAmount = default!; + + [DataField] + public float RotReduction; + + [DataField] + public bool DoRevive; + + public PsionicHealOtherDoAfterEvent(TimeSpan startedAt) { + StartedAt = startedAt; } + + public override DoAfterEvent Clone() => this; } diff --git a/Content.Shared/Psionics/PsionicComponent.cs b/Content.Shared/Psionics/PsionicComponent.cs index 85b7e380fea..37d0a9a7ef4 100644 --- a/Content.Shared/Psionics/PsionicComponent.cs +++ b/Content.Shared/Psionics/PsionicComponent.cs @@ -1,3 +1,4 @@ +using Content.Shared.DoAfter; using Content.Shared.Psionics; using Robust.Shared.GameStates; using Robust.Shared.Prototypes; @@ -166,6 +167,14 @@ private set /// unneccesary subs for unique psionic entities like e.g. Oracle. ///
[DataField] - public List? PsychognomicDescriptors = null; + public List? PsychognomicDescriptors = null; + + /// Used for tracking what spell a Psion is actively casting + [DataField] + public DoAfterId? DoAfter; + + /// Popup to play if a Psion attempts to start casting a power while already casting one + [DataField] + public string AlreadyCasting = "already-casting"; } } diff --git a/Resources/Locale/en-US/psionics/psionic-powers.ftl b/Resources/Locale/en-US/psionics/psionic-powers.ftl index c68bb2a4968..01b50cce326 100644 --- a/Resources/Locale/en-US/psionics/psionic-powers.ftl +++ b/Resources/Locale/en-US/psionics/psionic-powers.ftl @@ -1,4 +1,5 @@ generic-power-initialization-feedback = I Awaken. +arleady-casting = I cannot channel more than one power at a time. # Dispel dispel-power-description = Dispel summoned entities such as familiars or forcewalls. @@ -48,6 +49,28 @@ psionic-regeneration-power-initialization-feedback = I look within myself, finding a wellspring of life. psionic-regeneration-power-metapsionic-feedback = {CAPITALIZE($entity)} possesses an overwhelming will to live +# Healing Word +action-name-healing-word = Healing Word +action-description-healing-word = Speak the Lesser Secret Of Life, and restore health to another. +healing-word-power-description = Speak the Lesser Secret Of Life, and restore health to another. +healing-word-power-initialization-feedback = + At the beginning of time, a word was spoken that brought life into the Spheres. + Though it taxes my mind to know it, this Secret is known to me now. + I need only speak it. +healing-word-power-metapsionic-feedback = {CAPITALIZE($entity)} bears the Lesser Secret of Life. +healing-word-begin = {CAPITALIZE($entity)} mutters a word that brings both joy and pain alike to those who hear it. + +# Revivify +action-name-revivify = Breath of Life +action-description-revivify = Speak the Greater Secret of Life, and restore another to life. +revivify-power-description = Speak the Greater Secret of Life, and restore another to life. +revivify-power-initialization-feedback = + For a moment, my soul journeys across time and space to the beginning of it all, there I hear it. + The Secret of Life in its fullness. I feel my entire existence burning out from within, merely by knowing it. + Power flows through me as a mighty river, begging to be released with a simple spoken word. +revivify-power-metapsionic-feedback = {CAPITALIZE($entity)} bears the Greater Secret of Life. +revivify-word-begin = {CAPITALIZE($entity)} enunciates a word of such divine power, that those who hear it weep from joy. + # Telegnosis telegnosis-power-description = Create a telegnostic projection to remotely observe things. telegnosis-power-initialization-feedback = diff --git a/Resources/Prototypes/Actions/psionics.yml b/Resources/Prototypes/Actions/psionics.yml index 981d53884ea..d38608a469b 100644 --- a/Resources/Prototypes/Actions/psionics.yml +++ b/Resources/Prototypes/Actions/psionics.yml @@ -146,3 +146,76 @@ icon: Interface/VerbIcons/psionic_invisibility_off.png event: !type:RemovePsionicInvisibilityOffPowerActionEvent +- type: entity + id: ActionHealingWord + name: action-name-healing-word + description: action-description-healing-word + noSpawn: true + components: + - type: EntityTargetAction + icon: { sprite : Interface/Actions/psionics.rsi, state: healing_word } + useDelay: 10 + checkCanAccess: false + range: 6 + itemIconStyle: BigAction + canTargetSelf: true + blacklist: + components: + - PsionicInsulation + - Mindbroken + event: !type:PsionicHealOtherPowerActionEvent + healingAmount: + groups: # These all get divided by the number of damage types in the group. So they're all -2.5. + Genetic: -2.5 + Toxin: -5 + Airloss: -5 + Brute: -7.5 + Burn: -10 + rotReduction: 10 + useDelay: 1 + doRevive: true + powerName: Healing Word + popupText: healing-word-begin + playSound: true + minGlimmer: 2 + maxGlimmer: 4 + glimmerObviousSoundThreshold: 100 + glimmerObviousPopupThreshold: 200 + +- type: entity + id: ActionRevivify + name: action-name-revivify + description: action-description-revivify + noSpawn: true + components: + - type: EntityTargetAction + icon: { sprite : Interface/Actions/psionics.rsi, state: revivify } + useDelay: 120 + checkCanAccess: false + range: 2 + itemIconStyle: BigAction + canTargetSelf: false + blacklist: + components: + - PsionicInsulation + - Mindbroken + event: !type:PsionicHealOtherPowerActionEvent + healingAmount: + # These all get divided by the number of damage types in the group. So they're all -15 + # Additionally, they're multiplied by the caster's Amplification, which, + # assuming this is the only power they have, the multiplier is between 2.9-3.9 + groups: + Genetic: -15 + Toxin: -30 + Airloss: -60 # Except airloss, which heals 30 per type + Brute: -45 + Burn: -60 + rotReduction: 60 + doRevive: true + powerName: Revivify + popupText: revivify-begin + playSound: true + minGlimmer: 10 # These also get multiplied by caster stats. So, + maxGlimmer: 15 # keeping in mind the ~3.5x multiplier, this spikes glimmer by as much as 60 points. + glimmerObviousSoundThreshold: 50 + glimmerObviousPopupThreshold: 100 diff --git a/Resources/Prototypes/Nyanotrasen/psionicPowers.yml b/Resources/Prototypes/Nyanotrasen/psionicPowers.yml index 0781122b8ec..2aee2273a18 100644 --- a/Resources/Prototypes/Nyanotrasen/psionicPowers.yml +++ b/Resources/Prototypes/Nyanotrasen/psionicPowers.yml @@ -11,3 +11,5 @@ # PsionicInvisibilityPower: 0.15 MindSwapPower: 0.15 TelepathyPower: 1 + HealingWordPower: 0.85 + RevivifyPower: 0.1 diff --git a/Resources/Prototypes/Psionics/psionics.yml b/Resources/Prototypes/Psionics/psionics.yml index 461098eab21..b881f1f11fb 100644 --- a/Resources/Prototypes/Psionics/psionics.yml +++ b/Resources/Prototypes/Psionics/psionics.yml @@ -140,3 +140,25 @@ initializationFeedback: telepathy-power-initialization-feedback metapsionicFeedback: psionic-language-power-feedback # Reuse for telepathy, clairaudience, etc powerSlotCost: 0 + +- type: psionicPower + id: HealingWordPower + name: HealingWord + description: healing-word-power-description + actions: + - ActionHealingWord + initializationFeedback: healing-word-power-initialization-feedback + metapsionicFeedback: healing-word-power-feedback + amplificationModifier: 0.5 + dampeningModifier: 0.5 + +- type: psionicPower + id: RevivifyPower + name: Revivify + description: revivify-power-description + actions: + - ActionRevivify + initializationFeedback: revivify-power-initialization-feedback + metapsionicFeedback: revivify-power-feedback + amplificationModifier: 2.5 # An extremely rare and dangerous power + powerSlotCost: 2 diff --git a/Resources/Prototypes/Roles/Jobs/Civilian/chaplain.yml b/Resources/Prototypes/Roles/Jobs/Civilian/chaplain.yml index c7395ff3e2d..d4f8bdb0671 100644 --- a/Resources/Prototypes/Roles/Jobs/Civilian/chaplain.yml +++ b/Resources/Prototypes/Roles/Jobs/Civilian/chaplain.yml @@ -5,8 +5,8 @@ playTimeTracker: JobChaplain requirements: - !type:CharacterDepartmentTimeRequirement - department: Epistemics # DeltaV - Epistemics Department replacing Science - min: 14400 #DeltaV 4 hours + department: Epistemics # Chaplain is now one of the station's "Crew-Aligned Wizards" + min: 14400 # 4 hours - !type:CharacterLogicOrRequirement requirements: - !type:CharacterSpeciesRequirement @@ -21,7 +21,7 @@ supervisors: job-supervisors-rd access: - Chapel - - Research # DeltaV - Move Chaplain into Epistemics + - Research - Maintenance special: - !type:AddComponentSpecial @@ -32,6 +32,7 @@ - type: InnatePsionicPowers powersToAdd: - TelepathyPower + - HealingWordPower - type: startingGear id: ChaplainGear @@ -40,7 +41,7 @@ back: ClothingBackpackChaplainFilled shoes: ClothingShoesColorBlack id: ChaplainPDA - ears: ClothingHeadsetScience # DeltaV - Move Chaplain into Epistemics + ears: ClothingHeadsetScience innerClothingSkirt: ClothingUniformJumpskirtChaplain satchel: ClothingBackpackSatchelChaplainFilled duffelbag: ClothingBackpackDuffelChaplainFilled diff --git a/Resources/Textures/Interface/Actions/psionics.rsi/healing_word.png b/Resources/Textures/Interface/Actions/psionics.rsi/healing_word.png new file mode 100644 index 0000000000000000000000000000000000000000..800e2a46e6139ae17b21fb4509242b317c6445c5 GIT binary patch literal 2189 zcmV;82y*v{P)9?Ncm1Y5zzB48of z_$vGd+6uM`f}jW@me#%+k@Zd@BuJcP_I{k1bD22<_Fz`anJqrZ<%>or;CC-Ym)*`6 z+8CyfL3}aI)aS!<5&j*Yci2?D^{QU~?`t|b6G>A8Tf6X-rW-}!8R6i1QCGYz+^c0` z3E@NGaYGI$e5Lewjh_{#Yy7U*h=gXru6nxD4IFP^Eo0<_$Amq(Y)Sh)kGe@$GQSv> zIv{fhlMn&?xQSC@jVwjHQuC5obDyv3Tc98wP5wG%3~{8%h<9Anl5|ZO^grk^BUY6hGVQ4pyq-dL6nbu;W2KhW?>Z;<5ZRC?x`w@d7?u65)gMjJXf8H>^ zJath878q+2OxT#PaA736l+>sT;>L|8mc+g3LXC|kE)X>?T-3NRv9S>CqFB2iEmR{5 zH9oMZ#UDs2t@k^(=l0xrbARUNzG8Wk-0|#Luf3U?cIb_ln{7riUuCT8tBe&kfoDrV!dj@H5(Vf$0nQ}* zyk(dDt0X2k0Wkda!9n%MzpL;d`7cJ^Rds9I(X(y2k~a{zTonmzRXHDk%J#(3 z7n860Gx^=*CLq*SL1l4OLO|Q%&osXTmIa#cKX9y88OS^`f)apL0uVGwf)1Vy;``he z_p#c|Y(WWN!)Jc_s{zBOGasjj;SLhKPK>UiDu@YAz{2HuHMR7;0tpOGf*k&KgzobN zP@nvGRxM87Abb}u&Lr+*Qb=eLf)mhx&tWA=03ZOwJ+v)zcPNk`8$ZiZg-KDNhzOnr zVc7Bh7gDKi;hRzQ%U5->8L6XUobLZ&0zK;N*CkF?e3#Fh0U`4TFPw(za z5g>h-pX(&a7oZ%FKy6Q~rit@00eZiNo0d0%xd7&cA7jO&gaLl+w^5QiuPA2-&IIs# z6F7FEqEu;#In_O=0x&XwE~tnE75gy=i34C91o3=KjYgji(|3SJpDijT2w6kAhYtR( z<@3~UNv*n>43M^DI4J;uhc7uH3_(Bx$lkVx=)z~*=LVG!36Uxi@FEM~K1h&tf^?x% zpMOn5@pCS)Y=LusYXy|qsUgbOcHO=*$AJd_WD(cvN z|HedN{LF;}s{;{$Kl$o$it=(x%O^Tm=`{G*B&^^^r{hu>4gCNB3V{kx1xgZFN!(x7 z3H&aCBx1ySWABin@V?8I*~tZ9nB3s#smZhw><0T^>Q-C(7S-mi>*_-1IQ@(7xQ`2s z-Gmh($-%v+YR8iKQDgWtYZs7{l{wsy{QU*P1rJb?7?1#0fod>@ z-k>g&+j`0Eq5xY0HlV6Q+LF>kb$0r*4{3e`tSXPSI2O2oHN9i)0vH4IW$a=_0FOY`*!GQ7kWfV!1$?AHQ=TtQd)cPr-LWt08j&4D0tLos zi)^Y5Y{+mT0J(UZ>ZEx%4KjEiz$U{R$HT6&Lq(+lmEG);HlSr+Cju}4R-fCM`JvKz zzO12H^@2>1k9D3<2UiAkRe}=%GVrjkW2InpR-P{dyy#uj3bIZ(GdZO8skm%*ZMJDJ znFlpMNbqF|~qz?yXB+!#WYbyFzk^t zpr&S8c~|*+5uh66@E@B(uU*UOp;iC@01jnXNoGw=04e|g00;m8000000Mb*F P00000NkvXXu0mjf%9`?P literal 0 HcmV?d00001 diff --git a/Resources/Textures/Interface/Actions/psionics.rsi/meta.json b/Resources/Textures/Interface/Actions/psionics.rsi/meta.json new file mode 100644 index 00000000000..085cc412814 --- /dev/null +++ b/Resources/Textures/Interface/Actions/psionics.rsi/meta.json @@ -0,0 +1,17 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Created by leonardo_dabepis (discord)", + "size": { + "x": 64, + "y": 64 + }, + "states": [ + { + "name": "healing_word" + }, + { + "name": "revivify" + } + ] +} diff --git a/Resources/Textures/Interface/Actions/psionics.rsi/revivify.png b/Resources/Textures/Interface/Actions/psionics.rsi/revivify.png new file mode 100644 index 0000000000000000000000000000000000000000..087f40b4c98a073de7c2e0258f6128a1c3ff755b GIT binary patch literal 1977 zcmV;q2S)gbP)9?Ncm1Y5zzB48of z_$vGd+6uM`f}jW@me#%+k@Zd@BuJcP_I{k1bD22<_Fz`anJqrZ<%>or;CC-Ym)*`6 z+8CyfL3}aI)aS!<5&j*Yci2?D^{QU~?`t|b6G>A8Tf6X-rW-}!8R6i1QCGYz+^c0` z3E@NGaYGI$e5Lewjh_{#Yy7U*h=gXru6nxD4IFP^Eo0<_$Amq(Y)Sh)kGe@$GQSv> zIv{fhlMn&?xQSC@jVwjHQuC5obDyv3Tc98wP5wG%3~{8%h<9Anl5|ZO^grk^BUY6hGVQ4pyq-dL6nbu;W2KhW?>Z;<5ZRC?x`w@d7?u65)gMjJXf8h;(q6Rd$W6&-JQwJYU2(Zm%E+YdEd{xdGp?Eh@ut|hzLXk zA_5VC|1E(_!cboK`hsblaZ*p6HE!QL9m7<91M#gbi&TDM_l?uUv}p)N0kHVhl*K3O3?H&ir-jyO~R zEWD$mGbgm4XzL5FiqAbepA8*^G60>zMizeS&1FL5O-o`~2o?Zt0I}!Ioy7giuV~2g z2*2@FlO-hpI)#l}ye-_4cIh)!)Z6^7L*$kxx6tl4cGLN@=P8{@kPIP6QKect1O_mh z!UisnnU}@SwAiQqx|$dQWpcd&7gUP1L!eYxx3Ec{OAxR3_WSN~XvXaCeW=&|4#MSD zHt!ee?&_vY<~N5I_(jRMV(sAT0lK+8T)%DIT48VH!pjTV?H29sG=IT-f)@Z}kR1Z` z11e9ID$+9%(iJZYNsbPRkKc|Z?H6CiEW2l&Kc6^}5^+GKan%V3STcwienHdM15^y~ z>MO5*z^JDx(Org6a$$tZE+b;ksJL-4jelo3fwc@wHKPDBS6)N+-?B{D-uLn6wg!#| z5X>z#%n>gozB)emC8bYKn$m6G*C+rUW%Q>%sI#*}5@YQO+mh6}YX|L!Ws{%x4<2&N zbZO~Tl=XnqbnT*YksAbw1FFP~6hkm}b^9h^OTKt=z*ty)yXePm4&iw5X9U7kEAFM2 zpMTs=kW@u_^)~{52aHuNbgC z$i4TwRSa!ANShC=7eQ~s#`R*>PbN;%ZFF9~D z&u}D1xc2ZPo9%1x0zhC-fCsE?dQfDAuk`FSA|5w25(M{s_ztamV55LQR2V{R#qvB+ zfWVa@wnunI$%VmxykW5s2u1)*jQvPRfDi*10<0gib_28}uFQ~g0nP(hDZcO9pLKmK zjf-LzfRVqRnWV3e9}y7H06`WnyFQDBc(^*}26)Gda{&|Lm6rf=?H~_1U~D3lJ@pyb z7pFgeKS!^1_vE=SPJ?b6t|jWqZi6)qHKGqgK;TnNhgH{!@)D3ASa=PFI({T&<>XdA zfTi&Cb6e@XJ-xyc_?_O$?f($pxO5pEJvB_rtFvh`W?v-@4r-fhw=w_~!hq1<_cq0( zBd{-y9HzFGHH7z-)Mn2*#LLY+`#R!+*y?*8pnDd;gQJvXFL0*AMDl?s`m+~IPXA2D zKK)AM189Yg#=OL-oC)~m0XSpBgGup#(mHI=X$XjD$O&*pfOm+;fyhhSx7#m7dbCqR zKiF&Eqj5lS=SU27If5qF28acm9pXf&x#Vt}XLbGcV(p_U{Al14$LyNBM&+CIU_Zc3%002-H<9rb30qtw*HaV)_)kN{87YmAq`_FU^2yuSR=&?9M@G`>h2^MGq3CL0&Iz0u}+cJx*LS^o{zq!wmtW_p#i|a5VqmW6XHFQ$UPrq zqfDi{UK0~40NId#Lu?@szIniG-2P(tZ3MvdEQJ-!bajLh1OCep0;j?%glAho$z!P$ zHYzr9Un2l;^I||SEgp6n1K@_BGdrqdSO literal 0 HcmV?d00001 diff --git a/Resources/Textures/Interface/VerbIcons/ATTRIBUTION.txt b/Resources/Textures/Interface/VerbIcons/ATTRIBUTION.txt index 0b8ae856bd8..69aa4650b32 100644 --- a/Resources/Textures/Interface/VerbIcons/ATTRIBUTION.txt +++ b/Resources/Textures/Interface/VerbIcons/ATTRIBUTION.txt @@ -7,3 +7,6 @@ https://game-icons.net/1x1/lorc/padlock.html unlock.svg by Delapouite under CC BY 3.0 https://game-icons.net/1x1/delapouite/padlock-open.html + +healing_word.png by LeonardoDaBepis under CC BY 3.0 +revivify.png by LeonardoDaBepis under CC BY 3.0 \ No newline at end of file From 272b4ece2f952d7d14a4ce531a62f5e311ebe597 Mon Sep 17 00:00:00 2001 From: SimpleStation Changelogs Date: Sat, 21 Sep 2024 22:51:07 +0000 Subject: [PATCH 53/55] Automatic Changelog Update (#942) --- Resources/Changelog/Changelog.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 2477a35b914..ad3de9ce823 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -6590,3 +6590,25 @@ Entries: id: 6383 time: '2024-09-21T22:46:59.0000000+00:00' url: https://github.com/Simple-Station/Einstein-Engines/pull/930 +- author: VMSolidus + changes: + - type: Add + message: >- + Healing Word has been added as a new Psionic Power. When cast on another + person, it heals a small amount of every damage type(scaling with + Casting Stats), while also reducing rot timers. Healing Word has a very + short cooldown, and a fairly low Glimmer cost. + - type: Add + message: >- + Breath of Life has been added as a new extremely rare Psionic Power. + When cast on another person, it heals a large amount of damage(scaling + with Casting Stats), while also substantially reducing rot timers. + Additionally, it will revive the target if it is possible to do so. + Breath of Life has an incredibly long cooldown, a long interuptable cast + time, and an extraordinarily high glimmer cost(A typical Psion will + spike glimmer by more than 50 points when casting it). + - type: Add + message: The Chaplain now starts with the Healing Word power. + id: 6384 + time: '2024-09-21T22:50:01.0000000+00:00' + url: https://github.com/Simple-Station/Einstein-Engines/pull/942 From 5bf5d5718a58eadf1989ef30e1ff32515a7bdd4e Mon Sep 17 00:00:00 2001 From: VMSolidus Date: Sat, 21 Sep 2024 19:22:42 -0400 Subject: [PATCH 54/55] More Objects Hit By Projectiles (#943) # Description This PR is a follow up to https://github.com/Simple-Station/Einstein-Engines/pull/939 By increasing the number of items that are struck by bullets, or can be targeted by bullets. To add to the "Cinematic" experience of a gunfight, Bottles, Cups, and Plates of all kinds are automatically struck by bullets when fired over, and *probably* destroyed given that they only have 5 hit points. This also serves to aid in limiting the amount of things that can be hidden underneath when crawling under a table, since the station's bar counter will likely explode into a ton of glass and spilled booze when a gunfight gets near it. I also added Chairs to the "RequireProjectileTarget" feature, so that if you click on a chair, you'll shoot the chair and (probably) destroy it. # Changelog :cl: - add: Bottles, Drink Glasses, Plates, and all liquid containers are now struck by bullets(and most likely destroyed in the process). We hope that this will offer both a small tactical advantage/disadvantage, as well as contribute to making gunfights around the bar more "Cinematic". - add: Chairs are now hit by projectiles if a shooter clicks on them, in addition to Tables. --- .../Objects/Consumable/Drinks/drinks.yml | 25 +++++++++++++++++++ .../Consumable/Drinks/drinks_bottles.yml | 3 +++ .../Consumable/Food/Containers/plate.yml | 11 ++++++++ .../Entities/Structures/Furniture/chairs.yml | 3 ++- 4 files changed, 41 insertions(+), 1 deletion(-) diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks.yml b/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks.yml index 93d4b957fe7..743d9a17925 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks.yml @@ -48,6 +48,31 @@ path: /Audio/SimpleStation14/Items/Handling/drinkglass_drop.ogg params: volume: -2 + - type: Fixtures + fixtures: + fix1: + shape: + !type:PhysShapeAabb + bounds: "-0.25,-0.25,0.25,0.25" + density: 30 + mask: + - ItemMask + layer: + - BulletImpassable # Ever seen a John Woo movie? + - type: Destructible + thresholds: + - trigger: + !type:DamageTrigger + damage: 5 + behaviors: + - !type:PlaySoundBehavior + sound: + collection: MetalBreak + params: + volume: -6 + - !type:SpillBehavior { } + - !type:DoActsBehavior + acts: [ "Destruction" ] - type: entity parent: DrinkBase diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_bottles.yml b/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_bottles.yml index a6752286dd2..b2489325e0a 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_bottles.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_bottles.yml @@ -31,6 +31,9 @@ !type:DamageTrigger damage: 5 behaviors: + - !type:PlaySoundBehavior + sound: + collection: GlassBreak - !type:SpillBehavior { } - !type:DoActsBehavior acts: [ "Destruction" ] diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/Containers/plate.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/Containers/plate.yml index 63c47df67e7..3bd66e8a481 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/Containers/plate.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/Containers/plate.yml @@ -54,6 +54,17 @@ materialComposition: Glass: 60 - type: SpaceGarbage + - type: Fixtures + fixtures: + fix1: + shape: + !type:PhysShapeAabb + bounds: "-0.25,-0.25,0.25,0.25" + density: 25 + mask: + - ItemMask + layer: + - BulletImpassable # Ever seen a John Woo movie? - type: entity name: broken plate diff --git a/Resources/Prototypes/Entities/Structures/Furniture/chairs.yml b/Resources/Prototypes/Entities/Structures/Furniture/chairs.yml index 0fb69b4fdbd..14b3270ba88 100644 --- a/Resources/Prototypes/Entities/Structures/Furniture/chairs.yml +++ b/Resources/Prototypes/Entities/Structures/Furniture/chairs.yml @@ -33,7 +33,7 @@ thresholds: - trigger: !type:DamageTrigger - damage: 100 + damage: 25 behaviors: - !type:DoActsBehavior acts: [ "Destruction" ] @@ -48,6 +48,7 @@ collection: MetalBreak - type: StaticPrice price: 10 + - type: RequireProjectileTarget #Starts unanchored, cannot be rotated while anchored - type: entity From 900630cafd30a53ad04a1ed55257b23dc840c8e2 Mon Sep 17 00:00:00 2001 From: SimpleStation Changelogs Date: Sat, 21 Sep 2024 23:23:09 +0000 Subject: [PATCH 55/55] Automatic Changelog Update (#943) --- Resources/Changelog/Changelog.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index ad3de9ce823..f85816dfafa 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -6612,3 +6612,18 @@ Entries: id: 6384 time: '2024-09-21T22:50:01.0000000+00:00' url: https://github.com/Simple-Station/Einstein-Engines/pull/942 +- author: VMSolidus + changes: + - type: Add + message: >- + Bottles, Drink Glasses, Plates, and all liquid containers are now struck + by bullets(and most likely destroyed in the process). We hope that this + will offer both a small tactical advantage/disadvantage, as well as + contribute to making gunfights around the bar more "Cinematic". + - type: Add + message: >- + Chairs are now hit by projectiles if a shooter clicks on them, in + addition to Tables. + id: 6385 + time: '2024-09-21T23:22:43.0000000+00:00' + url: https://github.com/Simple-Station/Einstein-Engines/pull/943