From 5fc774e5c0715ace09c9faf16f86bccc1dac5859 Mon Sep 17 00:00:00 2001 From: Spatison <137375981+Spatison@users.noreply.github.com> Date: Fri, 23 Aug 2024 04:45:30 +0300 Subject: [PATCH 01/14] =?UTF-8?q?[Port]=20Telescope=20System=20/=20=D0=A1?= =?UTF-8?q?=D0=B8=D1=81=D1=82=D0=B5=D0=BC=D0=B0=20=D0=9F=D1=80=D0=B8=D1=86?= =?UTF-8?q?=D0=B5=D0=BB=D0=B8=D0=B2=D0=B0=D0=BD=D0=B8=D1=8F=20(#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add: TelescopeSystem * fix * add: RU loc --- Content.Client/Input/ContentContexts.cs | 1 + .../Options/UI/Tabs/KeyRebindTab.xaml.cs | 9 ++ .../_White/Telescope/TelescopeSystem.cs | 126 ++++++++++++++++++ .../_White/Telescope/TelescopeSystem.cs | 5 + Content.Shared/Input/ContentKeyFunctions.cs | 1 + Content.Shared/_White/CVars.cs | 14 ++ .../_White/Telescope/SharedTelescopeSystem.cs | 117 ++++++++++++++++ .../_White/Telescope/TelescopeComponent.cs | 16 +++ .../en-US/_white/escape-menu/options-menu.ftl | 2 + .../ru-RU/_white/escape-menu/options-menu.ftl | 2 + .../Objects/Weapons/Guns/Snipers/snipers.yml | 1 + Resources/keybinds.yml | 4 + 12 files changed, 298 insertions(+) create mode 100644 Content.Client/_White/Telescope/TelescopeSystem.cs create mode 100644 Content.Server/_White/Telescope/TelescopeSystem.cs create mode 100644 Content.Shared/_White/CVars.cs create mode 100644 Content.Shared/_White/Telescope/SharedTelescopeSystem.cs create mode 100644 Content.Shared/_White/Telescope/TelescopeComponent.cs create mode 100644 Resources/Locale/en-US/_white/escape-menu/options-menu.ftl create mode 100644 Resources/Locale/ru-RU/_white/escape-menu/options-menu.ftl diff --git a/Content.Client/Input/ContentContexts.cs b/Content.Client/Input/ContentContexts.cs index 503a9ac953..ae5763e280 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); // WD EDIT // 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 13e456985a..9484606951 100644 --- a/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs +++ b/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs @@ -1,5 +1,6 @@ using System.Numerics; using Content.Client.Stylesheets; +using Content.Shared._White; using Content.Shared.CCVar; using Content.Shared.Input; using Robust.Client.AutoGenerated; @@ -97,6 +98,12 @@ private void HandleToggleWalk(BaseButton.ButtonToggledEventArgs args) _deferCommands.Add(_inputManager.SaveToUserData); } + private void HandleHoldLookUp(BaseButton.ButtonToggledEventArgs args) // WD EDIT + { + _cfg.SetCVar(WhiteCVars.HoldLookUp, args.Pressed); + _cfg.SaveToFile(); + } + private void HandleStaticStorageUI(BaseButton.ButtonToggledEventArgs args) { _cfg.SetCVar(CCVars.StaticStorageUI, args.Pressed); @@ -186,6 +193,8 @@ void AddCheckBox(string checkBoxName, bool currentState, Action + { + 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/_White/Telescope/TelescopeSystem.cs b/Content.Server/_White/Telescope/TelescopeSystem.cs new file mode 100644 index 0000000000..223654fd98 --- /dev/null +++ b/Content.Server/_White/Telescope/TelescopeSystem.cs @@ -0,0 +1,5 @@ +using Content.Shared._White.Telescope; + +namespace Content.Server._White.Telescope; + +public sealed class TelescopeSystem : SharedTelescopeSystem; diff --git a/Content.Shared/Input/ContentKeyFunctions.cs b/Content.Shared/Input/ContentKeyFunctions.cs index dac780783c..f25646d24a 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"; // WD EDIT public static readonly BoundKeyFunction ArcadeUp = "ArcadeUp"; public static readonly BoundKeyFunction ArcadeDown = "ArcadeDown"; diff --git a/Content.Shared/_White/CVars.cs b/Content.Shared/_White/CVars.cs new file mode 100644 index 0000000000..065956993e --- /dev/null +++ b/Content.Shared/_White/CVars.cs @@ -0,0 +1,14 @@ +using Robust.Shared.Configuration; + +namespace Content.Shared._White; + +[CVarDefs] +public sealed class WhiteCVars +{ + #region Keybind + + public static readonly CVarDef HoldLookUp = + CVarDef.Create("white.hold_look_up", false, CVar.CLIENT | CVar.ARCHIVE); + + #endregion +} diff --git a/Content.Shared/_White/Telescope/SharedTelescopeSystem.cs b/Content.Shared/_White/Telescope/SharedTelescopeSystem.cs new file mode 100644 index 0000000000..78bcf757ea --- /dev/null +++ b/Content.Shared/_White/Telescope/SharedTelescopeSystem.cs @@ -0,0 +1,117 @@ +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._White.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)) + return; + + if (!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) + return; + + if (!TryComp(ent, out EyeComponent? 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/_White/Telescope/TelescopeComponent.cs b/Content.Shared/_White/Telescope/TelescopeComponent.cs new file mode 100644 index 0000000000..599770e886 --- /dev/null +++ b/Content.Shared/_White/Telescope/TelescopeComponent.cs @@ -0,0 +1,16 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared._White.Telescope; + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class TelescopeComponent : Component +{ + [DataField, ViewVariables(VVAccess.ReadWrite), AutoNetworkedField] + public float Divisor = 0.1f; + + [DataField, ViewVariables(VVAccess.ReadWrite), AutoNetworkedField] + public float LerpAmount = 0.1f; + + [ViewVariables] + public EntityUid? LastEntity; +} diff --git a/Resources/Locale/en-US/_white/escape-menu/options-menu.ftl b/Resources/Locale/en-US/_white/escape-menu/options-menu.ftl new file mode 100644 index 0000000000..b94df35ba6 --- /dev/null +++ b/Resources/Locale/en-US/_white/escape-menu/options-menu.ftl @@ -0,0 +1,2 @@ +ui-options-function-look-up = Look up/Take aim +ui-options-function-hold-look-up = Hold down the key to aim \ No newline at end of file diff --git a/Resources/Locale/ru-RU/_white/escape-menu/options-menu.ftl b/Resources/Locale/ru-RU/_white/escape-menu/options-menu.ftl new file mode 100644 index 0000000000..b8667e14c0 --- /dev/null +++ b/Resources/Locale/ru-RU/_white/escape-menu/options-menu.ftl @@ -0,0 +1,2 @@ +ui-options-function-look-up = Присмотреться/Прицелиться +ui-options-function-hold-look-up = Удерживать клавишу для прицеливания \ No newline at end of file diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Snipers/snipers.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Snipers/snipers.yml index adb8e323f4..bd6dfdf10f 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Snipers/snipers.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Snipers/snipers.yml @@ -71,6 +71,7 @@ - CartridgeAntiMateriel capacity: 5 proto: CartridgeAntiMateriel + - type: Telescope # WD EDIT - type: entity name: musket diff --git a/Resources/keybinds.yml b/Resources/keybinds.yml index 2cca749317..7f4000edff 100644 --- a/Resources/keybinds.yml +++ b/Resources/keybinds.yml @@ -543,3 +543,7 @@ binds: - function: Hotbar9 type: State key: Num9 + #WD EDIT +- function: LookUp + type: State + key: Space From 0d8fdc6b8b0edf2117c6e1a45c3d31786de19a12 Mon Sep 17 00:00:00 2001 From: WWDP Changelogs Date: Fri, 23 Aug 2024 01:45:56 +0000 Subject: [PATCH 02/14] Automatic Changelog Update (#8) --- Resources/Changelog/Changelog.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index d69eb51b73..d809ab77ef 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -5399,3 +5399,11 @@ Entries: Добавляет спрайты листов стали, пластали, стекла (всех видов), пластика. id: 6270 time: '2024-08-21T10:53:44.0000000+00:00' +- author: Spatison + changes: + - type: Add + message: Added telescope system / Добавлена система прицеливания + - type: Tweak + message: Now you can aim from Hristov / Теперь можно прицеливаться из Христова + id: 6271 + time: '2024-08-23T01:45:30.0000000+00:00' From 932f1f6c7e8f711bc560ec4986d8b411a1e869ce Mon Sep 17 00:00:00 2001 From: Ivan <126400932+HellCatten@users.noreply.github.com> Date: Fri, 23 Aug 2024 05:53:41 +0300 Subject: [PATCH 03/14] Emp Flashlight (#5) * EmpFlashlight added * Using namespace cleared * Code Review problems solved --- .../_White/EmpFlashlight/EmpOnHitComponent.cs | 24 +++++++++ .../_White/EmpFlashlight/EmpOnHitSystem.cs | 52 +++++++++++++++++++ .../en-US/_white/store/uplink-catalog.ftl | 2 + .../entities/objects/tools/empflashlight.ftl | 2 + .../ru-RU/_white/store/uplink-catalog.ftl | 2 + .../_White/Catalog/uplink_catalog.yml | 9 ++++ .../Entities/Objects/Tools/EmpFlashlight.yml | 26 ++++++++++ 7 files changed, 117 insertions(+) create mode 100644 Content.Server/_White/EmpFlashlight/EmpOnHitComponent.cs create mode 100644 Content.Server/_White/EmpFlashlight/EmpOnHitSystem.cs create mode 100644 Resources/Locale/en-US/_white/store/uplink-catalog.ftl create mode 100644 Resources/Locale/ru-RU/_white/prototypes/entities/objects/tools/empflashlight.ftl create mode 100644 Resources/Locale/ru-RU/_white/store/uplink-catalog.ftl create mode 100644 Resources/Prototypes/_White/Catalog/uplink_catalog.yml create mode 100644 Resources/Prototypes/_White/Entities/Objects/Tools/EmpFlashlight.yml diff --git a/Content.Server/_White/EmpFlashlight/EmpOnHitComponent.cs b/Content.Server/_White/EmpFlashlight/EmpOnHitComponent.cs new file mode 100644 index 0000000000..071de4ce80 --- /dev/null +++ b/Content.Server/_White/EmpFlashlight/EmpOnHitComponent.cs @@ -0,0 +1,24 @@ +using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.ViewVariables; +using Content.Server._White.EmpFlashlight; +using Content.Server.Emp; + +namespace Content.Server._White.EmpFlashlight; + +/// +/// Upon being triggered will EMP target. +/// +[RegisterComponent] +[Access(typeof(EmpOnHitSystem))] + +public sealed partial class EmpOnHitComponent: Component +{ + [DataField("range"), ViewVariables(VVAccess.ReadWrite)] + public float Range = 1.0f; + + [DataField("energyConsumption"), ViewVariables(VVAccess.ReadWrite)] + public float EnergyConsumption; + + [DataField("disableDuration"), ViewVariables(VVAccess.ReadWrite)] + public float DisableDuration = 60f; +} diff --git a/Content.Server/_White/EmpFlashlight/EmpOnHitSystem.cs b/Content.Server/_White/EmpFlashlight/EmpOnHitSystem.cs new file mode 100644 index 0000000000..9fd86c20be --- /dev/null +++ b/Content.Server/_White/EmpFlashlight/EmpOnHitSystem.cs @@ -0,0 +1,52 @@ +using Content.Shared.Weapons.Melee.Events; +using Content.Server.Emp; +using Content.Shared.Charges.Systems; +using Content.Shared.Charges.Components; + +namespace Content.Server._White.EmpFlashlight; + +public sealed class EmpOnHitSystem : EntitySystem +{ + + [Dependency] private readonly EmpSystem _emp = default!; + [Dependency] private readonly SharedChargesSystem _charges = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(HandleEmpHit); + } + + public bool TryEmpHit(EntityUid uid, EmpOnHitComponent comp, MeleeHitEvent args) + { + + if (!TryComp(uid, out LimitedChargesComponent? charges)) + return false; + + if (_charges.IsEmpty(uid, charges)) + return false; + + if (args.HitEntities.Count > 0) + { + _charges.UseCharge(uid,charges); + return true; + } + + return false; + } + + private void HandleEmpHit(EntityUid uid, EmpOnHitComponent comp, MeleeHitEvent args) + { + if (!TryEmpHit(uid, comp, args)) + return; + + foreach (var affected in args.HitEntities) + { + _emp.EmpPulse(_transform.GetMapCoordinates(affected), comp.Range, comp.EnergyConsumption, comp.DisableDuration); + } + + args.Handled = true; + } +} + diff --git a/Resources/Locale/en-US/_white/store/uplink-catalog.ftl b/Resources/Locale/en-US/_white/store/uplink-catalog.ftl new file mode 100644 index 0000000000..7e0b0b791e --- /dev/null +++ b/Resources/Locale/en-US/_white/store/uplink-catalog.ftl @@ -0,0 +1,2 @@ +uplink-emp-flashlight-name = Emp Flashlight +uplink-emp-flashlight-desc = A rechargeable device disguised as a flashlight designed to disrupt electronic systems. Useful for disrupting communications, security's energy weapons, and APCs when you're in a tight spot. diff --git a/Resources/Locale/ru-RU/_white/prototypes/entities/objects/tools/empflashlight.ftl b/Resources/Locale/ru-RU/_white/prototypes/entities/objects/tools/empflashlight.ftl new file mode 100644 index 0000000000..e9ffe8e039 --- /dev/null +++ b/Resources/Locale/ru-RU/_white/prototypes/entities/objects/tools/empflashlight.ftl @@ -0,0 +1,2 @@ +ent-FlashlightEmp = Фонарик + .desc = Он озаряет путь к свободе. diff --git a/Resources/Locale/ru-RU/_white/store/uplink-catalog.ftl b/Resources/Locale/ru-RU/_white/store/uplink-catalog.ftl new file mode 100644 index 0000000000..5bb4067557 --- /dev/null +++ b/Resources/Locale/ru-RU/_white/store/uplink-catalog.ftl @@ -0,0 +1,2 @@ +uplink-emp-flashlight-name = Электромагнитный фонарик +uplink-emp-flashlight-desc = Замаскированное под фонарик устройство. При ударе выпускает ЭМИ, поражающий электрические устройства. diff --git a/Resources/Prototypes/_White/Catalog/uplink_catalog.yml b/Resources/Prototypes/_White/Catalog/uplink_catalog.yml new file mode 100644 index 0000000000..ed2e10a2ee --- /dev/null +++ b/Resources/Prototypes/_White/Catalog/uplink_catalog.yml @@ -0,0 +1,9 @@ +- type: listing + id: UplinkEmpFlashlight + name: uplink-emp-flashlight-name + description: uplink-emp-flashlight-desc + productEntity: FlashlightEmp + cost: + Telecrystal: 3 + categories: + - UplinkUtility diff --git a/Resources/Prototypes/_White/Entities/Objects/Tools/EmpFlashlight.yml b/Resources/Prototypes/_White/Entities/Objects/Tools/EmpFlashlight.yml new file mode 100644 index 0000000000..1da304ba1a --- /dev/null +++ b/Resources/Prototypes/_White/Entities/Objects/Tools/EmpFlashlight.yml @@ -0,0 +1,26 @@ +- type: entity + parent: FlashlightLantern + id: FlashlightEmp + name: flashlight + description: It lights the way to freedom. + suffix: EMP + components: + - type: ItemSlots + slots: + cell_slot: + name: power-cell-clot-component-spot-name-default + startingItem: PowerCellHigh + - type: MeleeWeapon + wideAnimationRotation: -135 + damage: + types: + Blunt: 12 + angle: 60 + animation: WeaponArcThrust + - type: EmpOnHit + range: 0.1 + energyConsumption: 100000 + disableDuration: 100 + - type: LimitedCharges + - type: AutoRecharge + rechargeDuration: 60 From b985fc96016bc49936dd31f50bb5afd4f4900e2b Mon Sep 17 00:00:00 2001 From: WWDP Changelogs Date: Fri, 23 Aug 2024 02:54:11 +0000 Subject: [PATCH 04/14] Automatic Changelog Update (#5) --- Resources/Changelog/Changelog.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index d809ab77ef..89759da52f 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -5407,3 +5407,9 @@ Entries: message: Now you can aim from Hristov / Теперь можно прицеливаться из Христова id: 6271 time: '2024-08-23T01:45:30.0000000+00:00' +- author: Hell_Cat + changes: + - type: Add + message: Добавлен предмет аплинка - ЭМИ фонарик. + id: 6272 + time: '2024-08-23T02:53:41.0000000+00:00' From 58f3ff4f5cf37716c2b104cdd2eae8d0f3ce2ecc Mon Sep 17 00:00:00 2001 From: Spatison <137375981+Spatison@users.noreply.github.com> Date: Fri, 23 Aug 2024 16:30:07 +0300 Subject: [PATCH 05/14] =?UTF-8?q?[Port]=20Lying=20Down=20System=20/=20?= =?UTF-8?q?=D0=A1=D0=B8=D1=81=D1=82=D0=B5=D0=BC=D0=B0=20=D0=9B=D0=B5=D0=B6?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D1=8F=20(#2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add: White lay down * fix: rotation * fix * fix: rotation * fix --- Content.Client/Buckle/BuckleSystem.cs | 1 - .../Options/UI/Tabs/KeyRebindTab.xaml.cs | 7 + .../_White/Standing/LayingDownSystem.cs | 82 +++++++++ .../Standing/LayingDownComponent.cs | 14 -- Content.Server/Standing/LayingDownSystem.cs | 97 ++--------- .../Assorted/LayingDownModifierSystem.cs | 6 +- .../Standing/StandingStateComponent.cs | 45 +++-- .../Standing/StandingStateSystem.cs | 26 ++- Content.Shared/Stunnable/SharedStunSystem.cs | 21 ++- Content.Shared/_White/CVars.cs | 3 + .../_White/Standing/LayingDownComponent.cs | 25 +++ .../_White/Standing/SharedLayingDownSystem.cs | 162 ++++++++++++++++++ .../en-US/_white/escape-menu/options-menu.ftl | 1 + .../ru-RU/_white/escape-menu/options-menu.ftl | 1 + 14 files changed, 362 insertions(+), 129 deletions(-) create mode 100644 Content.Client/_White/Standing/LayingDownSystem.cs delete mode 100644 Content.Server/Standing/LayingDownComponent.cs create mode 100644 Content.Shared/_White/Standing/LayingDownComponent.cs create mode 100644 Content.Shared/_White/Standing/SharedLayingDownSystem.cs diff --git a/Content.Client/Buckle/BuckleSystem.cs b/Content.Client/Buckle/BuckleSystem.cs index fea18e5cf3..b899a88106 100644 --- a/Content.Client/Buckle/BuckleSystem.cs +++ b/Content.Client/Buckle/BuckleSystem.cs @@ -57,7 +57,6 @@ private void OnAppearanceChange(EntityUid uid, BuckleComponent component, ref Ap !buckled || args.Sprite == null) { - _rotationVisualizerSystem.SetHorizontalAngle((uid, rotVisuals), rotVisuals.DefaultRotation); return; } diff --git a/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs b/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs index 9484606951..403161e0c6 100644 --- a/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs +++ b/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs @@ -110,6 +110,12 @@ private void HandleStaticStorageUI(BaseButton.ButtonToggledEventArgs args) _cfg.SaveToFile(); } + private void HandleToggleAutoGetUp(BaseButton.ButtonToggledEventArgs args) // WD EDIT + { + _cfg.SetCVar(WhiteCVars.AutoGetUp, args.Pressed); + _cfg.SaveToFile(); + } + public KeyRebindTab() { IoCManager.InjectDependencies(this); @@ -194,6 +200,7 @@ void AddCheckBox(string checkBoxName, bool currentState, Action(OnMovementInput); + + SubscribeNetworkEvent(OnCheckAutoGetUp); + } + + private void OnMovementInput(EntityUid uid, LayingDownComponent component, MoveEvent args) + { + if (!_timing.IsFirstTimePredicted) + return; + + if (!_standing.IsDown(uid)) + return; + + if (_buckle.IsBuckled(uid)) + return; + + if (_animation.HasRunningAnimation(uid, "rotate")) + return; + + if (!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.Server/Standing/LayingDownComponent.cs b/Content.Server/Standing/LayingDownComponent.cs deleted file mode 100644 index 7921749f14..0000000000 --- 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 73a929fdfc..eccbf0773b 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._White; +using Content.Shared._White.Standing; +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 // WD EDIT { - [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, WhiteCVars.AutoGetUp); + Dirty(uid, layingDown); } } diff --git a/Content.Server/Traits/Assorted/LayingDownModifierSystem.cs b/Content.Server/Traits/Assorted/LayingDownModifierSystem.cs index dc6dcd2de3..7d2c16e3c4 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._White.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/Standing/StandingStateComponent.cs b/Content.Shared/Standing/StandingStateComponent.cs index 5d7bb0a59f..3a7cb2a008 100644 --- a/Content.Shared/Standing/StandingStateComponent.cs +++ b/Content.Shared/Standing/StandingStateComponent.cs @@ -1,24 +1,35 @@ 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"); + [ViewVariables(VVAccess.ReadWrite)] + [DataField] + public SoundSpecifier DownSound { get; private set; } = new SoundCollectionSpecifier("BodyFall"); + + // WD EDIT START + [DataField, AutoNetworkedField] + public StandingState CurrentState { get; set; } = StandingState.Standing; + // WD EDIT END - [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(); +} +// WD EDIT START +public enum StandingState +{ + Lying, + GettingUp, + Standing, } +// WD EDIT END diff --git a/Content.Shared/Standing/StandingStateSystem.cs b/Content.Shared/Standing/StandingStateSystem.cs index 517831b8a1..212e1d06d1 100644 --- a/Content.Shared/Standing/StandingStateSystem.cs +++ b/Content.Shared/Standing/StandingStateSystem.cs @@ -1,7 +1,9 @@ +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; @@ -13,6 +15,8 @@ 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 MovementSpeedModifierSystem _movement = default!; // WD EDIT + [Dependency] private readonly SharedBuckleSystem _buckle = default!; // WD EDIT // If StandingCollisionLayer value is ever changed to more than one layer, the logic needs to be edited. private const int StandingCollisionLayer = (int) CollisionGroup.MidImpassable; @@ -22,7 +26,7 @@ public bool IsDown(EntityUid uid, StandingStateComponent? standingState = null) if (!Resolve(uid, ref standingState, false)) return false; - return !standingState.Standing; + return standingState.CurrentState is StandingState.Lying or StandingState.GettingUp; } public bool Down(EntityUid uid, bool playSound = true, bool dropHeldItems = true, @@ -37,7 +41,7 @@ public bool Down(EntityUid uid, bool playSound = true, bool dropHeldItems = true // Optional component. Resolve(uid, ref appearance, ref hands, false); - if (!standingState.Standing) + if (standingState.CurrentState is StandingState.Lying or StandingState.GettingUp) return true; // This is just to avoid most callers doing this manually saving boilerplate @@ -49,13 +53,16 @@ public bool Down(EntityUid uid, bool playSound = true, bool dropHeldItems = true RaiseLocalEvent(uid, new DropHandItemsEvent(), false); } + if (TryComp(uid, out BuckleComponent? buckle) && buckle.Buckled && !_buckle.TryUnbuckle(uid, uid, buckleComp: buckle)) // WD EDIT + return false; + var msg = new DownAttemptEvent(); RaiseLocalEvent(uid, msg, false); if (msg.Cancelled) return false; - standingState.Standing = false; + standingState.CurrentState = StandingState.Lying; Dirty(standingState); RaiseLocalEvent(uid, new DownedEvent(), false); @@ -82,9 +89,10 @@ public bool Down(EntityUid uid, bool playSound = true, bool dropHeldItems = true if (playSound) { - _audio.PlayPredicted(standingState.DownSound, uid, uid); + _audio.PlayPredicted(standingState.DownSound, uid, null); } + _movement.RefreshMovementSpeedModifiers(uid); // WD EDIT return true; } @@ -100,9 +108,12 @@ public bool Stand(EntityUid uid, // Optional component. Resolve(uid, ref appearance, false); - if (standingState.Standing) + if (standingState.CurrentState is StandingState.Standing) return true; + if (TryComp(uid, out BuckleComponent? buckle) && buckle.Buckled && !_buckle.TryUnbuckle(uid, uid, buckleComp: buckle)) // WD EDIT + return false; + if (!force) { var msg = new StandAttemptEvent(); @@ -112,7 +123,7 @@ public bool Stand(EntityUid uid, return false; } - standingState.Standing = true; + standingState.CurrentState = StandingState.Standing; Dirty(uid, standingState); RaiseLocalEvent(uid, new StoodEvent(), false); @@ -127,6 +138,7 @@ public bool Stand(EntityUid uid, } } standingState.ChangedFixtures.Clear(); + _movement.RefreshMovementSpeedModifiers(uid); // WD EDIT return true; } diff --git a/Content.Shared/Stunnable/SharedStunSystem.cs b/Content.Shared/Stunnable/SharedStunSystem.cs index c447f8c8bc..3f6ef8999d 100644 --- a/Content.Shared/Stunnable/SharedStunSystem.cs +++ b/Content.Shared/Stunnable/SharedStunSystem.cs @@ -1,3 +1,4 @@ +using Content.Shared._White.Standing; using Content.Shared.ActionBlocker; using Content.Shared.Administration.Logs; using Content.Shared.Audio; @@ -19,6 +20,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 +34,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!; // WD EDIT + [Dependency] private readonly SharedContainerSystem _container = default!; // WD EDIT /// /// Friction modifier for knocked down players. @@ -109,12 +113,25 @@ 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))); // WD EDIT + _layingDown.TryLieDown(uid, null, null, DropHeldItemsBehavior.DropIfStanding); // WD EDIT } private void OnKnockShutdown(EntityUid uid, KnockedDownComponent component, ComponentShutdown args) { - _standingState.Stand(uid); + // WD EDIT START + 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); + // WD EDIT END } private void OnStandAttempt(EntityUid uid, KnockedDownComponent component, StandAttemptEvent args) diff --git a/Content.Shared/_White/CVars.cs b/Content.Shared/_White/CVars.cs index 065956993e..3748706b39 100644 --- a/Content.Shared/_White/CVars.cs +++ b/Content.Shared/_White/CVars.cs @@ -7,6 +7,9 @@ public sealed class WhiteCVars { #region Keybind + public static readonly CVarDef AutoGetUp = + CVarDef.Create("white.auto_get_up", true, CVar.CLIENT | CVar.ARCHIVE | CVar.REPLICATED); + public static readonly CVarDef HoldLookUp = CVarDef.Create("white.hold_look_up", false, CVar.CLIENT | CVar.ARCHIVE); diff --git a/Content.Shared/_White/Standing/LayingDownComponent.cs b/Content.Shared/_White/Standing/LayingDownComponent.cs new file mode 100644 index 0000000000..6dce32ac24 --- /dev/null +++ b/Content.Shared/_White/Standing/LayingDownComponent.cs @@ -0,0 +1,25 @@ +using Robust.Shared.GameStates; +using Robust.Shared.Serialization; + +namespace Content.Shared._White.Standing; + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class LayingDownComponent : Component +{ + [DataField, AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)] + public float StandingUpTime { get; set; } = 1f; + + [DataField, AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)] + public float SpeedModify { get; set; } = 0.4f; + + [DataField, AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)] + 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/_White/Standing/SharedLayingDownSystem.cs b/Content.Shared/_White/Standing/SharedLayingDownSystem.cs new file mode 100644 index 0000000000..2406d19a37 --- /dev/null +++ b/Content.Shared/_White/Standing/SharedLayingDownSystem.cs @@ -0,0 +1,162 @@ +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._White.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/Resources/Locale/en-US/_white/escape-menu/options-menu.ftl b/Resources/Locale/en-US/_white/escape-menu/options-menu.ftl index b94df35ba6..c2ebf31844 100644 --- a/Resources/Locale/en-US/_white/escape-menu/options-menu.ftl +++ b/Resources/Locale/en-US/_white/escape-menu/options-menu.ftl @@ -1,2 +1,3 @@ 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 \ No newline at end of file diff --git a/Resources/Locale/ru-RU/_white/escape-menu/options-menu.ftl b/Resources/Locale/ru-RU/_white/escape-menu/options-menu.ftl index b8667e14c0..eebde7a272 100644 --- a/Resources/Locale/ru-RU/_white/escape-menu/options-menu.ftl +++ b/Resources/Locale/ru-RU/_white/escape-menu/options-menu.ftl @@ -1,2 +1,3 @@ ui-options-function-look-up = Присмотреться/Прицелиться +ui-options-function-auto-get-up = Автоматически вставать при падении ui-options-function-hold-look-up = Удерживать клавишу для прицеливания \ No newline at end of file From b0e9d090c949fafaac0951348714cf92e7d7876a Mon Sep 17 00:00:00 2001 From: WWDP Changelogs Date: Fri, 23 Aug 2024 13:30:35 +0000 Subject: [PATCH 06/14] Automatic Changelog Update (#2) --- Resources/Changelog/Changelog.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 89759da52f..ad7dc85bf0 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -5413,3 +5413,9 @@ Entries: message: Добавлен предмет аплинка - ЭМИ фонарик. id: 6272 time: '2024-08-23T02:53:41.0000000+00:00' +- author: Spatison + changes: + - type: Add + message: Added lying down system / Добавлена система лежания + id: 6273 + time: '2024-08-23T13:30:07.0000000+00:00' From 010fea32cfc1a23f8d2a56e00ce7ebdc96d3a1c8 Mon Sep 17 00:00:00 2001 From: Spatison <137375981+Spatison@users.noreply.github.com> Date: Fri, 23 Aug 2024 16:30:51 +0300 Subject: [PATCH 07/14] =?UTF-8?q?[Tweak]=20Press=20To=20Walk=20/=20=D0=9D?= =?UTF-8?q?=D0=B0=D0=B6=D0=B0=D1=82=D1=8C=20=D0=A7=D1=82=D0=BE=D0=B1=D1=8B?= =?UTF-8?q?=20=D0=98=D0=B4=D1=82=D0=B8=20(#13)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tweak: press to walk --- Content.Shared/CCVar/CCVars.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Content.Shared/CCVar/CCVars.cs b/Content.Shared/CCVar/CCVars.cs index e419a67f16..9abe53210c 100644 --- a/Content.Shared/CCVar/CCVars.cs +++ b/Content.Shared/CCVar/CCVars.cs @@ -400,7 +400,7 @@ public static readonly CVarDef /// When true, you have to press the change speed button to sprint. /// public static readonly CVarDef GamePressToSprint = - CVarDef.Create("game.press_to_sprint", true, CVar.REPLICATED); + CVarDef.Create("game.press_to_sprint", false, CVar.REPLICATED); #if EXCEPTION_TOLERANCE /// From 2ea161980b2aecd4e4b3c058bcdb055da690c5c4 Mon Sep 17 00:00:00 2001 From: WWDP Changelogs Date: Fri, 23 Aug 2024 13:31:48 +0000 Subject: [PATCH 08/14] Automatic Changelog Update (#13) --- Resources/Changelog/Changelog.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index ad7dc85bf0..26bc3f5f9f 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -5419,3 +5419,9 @@ Entries: message: Added lying down system / Добавлена система лежания id: 6273 time: '2024-08-23T13:30:07.0000000+00:00' +- author: Spatison + changes: + - type: Tweak + message: Press to walk / Нажать чтобы идти + id: 6274 + time: '2024-08-23T13:30:51.0000000+00:00' From 69f34523a36df238336d64780c915b9eb6ab18cd Mon Sep 17 00:00:00 2001 From: Remuchi <72476615+Remuchi@users.noreply.github.com> Date: Sat, 24 Aug 2024 11:45:51 +0700 Subject: [PATCH 09/14] [Fix] Antags Refactor Fix (#14) * fix: fix loadouts not spawning items inhands * Prevent SecretRule from picking invalid presets (#27456) * Prevent SecretRule from picking invalid presets * remove lonely semicolon --------- Co-authored-by: Nemanja <98561806+EmoGarbage404@users.noreply.github.com> * fix: fix makethief adminverb icon * fix antag selection being evil (#28197) * fix antag selection being evil * fix test * untroll the other tests * remove role timer troll * Allow tests to modify antag preferences * Fix antag selection * Misc test fixes * Add AntagPreferenceTest * Fix lazy mistakes * Test cleanup * Try stop players in lobbies from being assigned mid-round antags * ranting * I am going insane --------- Co-authored-by: deltanedas <@deltanedas:kde.org> Co-authored-by: ElectroJr # Conflicts: # Content.Server/Antag/AntagSelectionSystem.API.cs # Content.Server/Antag/AntagSelectionSystem.cs # Content.Server/Preferences/Managers/ServerPreferencesManager.cs * Fix under-selecting antags (#28327) Fix under selecting antags * fix: antag adminverbs now target actual target * fix: test --------- Co-authored-by: Leon Friedrich <60421075+ElectroJr@users.noreply.github.com> Co-authored-by: Nemanja <98561806+EmoGarbage404@users.noreply.github.com> Co-authored-by: deltanedas <39013340+deltanedas@users.noreply.github.com> --- .../Pair/TestPair.Helpers.cs | 28 ++++ Content.IntegrationTests/PoolManager.cs | 4 +- .../Tests/GameRules/AntagPreferenceTest.cs | 76 +++++++++ .../Tests/GameRules/NukeOpsTest.cs | 4 + .../Click/InteractionSystemTests.cs | 1 - .../Tests/ResettingEntitySystemTests.cs | 3 - .../Systems/AdminVerbSystem.Antags.cs | 14 +- .../Antag/AntagSelectionSystem.API.cs | 35 +++- Content.Server/Antag/AntagSelectionSystem.cs | 84 +++++----- .../GameTicking/GameTicker.RoundFlow.cs | 24 ++- .../GameTicking/Rules/SecretRuleSystem.cs | 150 +++++++++++++++--- .../Managers/IServerPreferencesManager.cs | 2 + .../Managers/ServerPreferencesManager.cs | 75 ++++----- Content.Shared/Antag/AntagAcceptability.cs | 8 + Content.Shared/Roles/Jobs/SharedJobSystem.cs | 4 +- .../Station/SharedStationSpawningSystem.cs | 7 +- 16 files changed, 391 insertions(+), 128 deletions(-) create mode 100644 Content.IntegrationTests/Tests/GameRules/AntagPreferenceTest.cs diff --git a/Content.IntegrationTests/Pair/TestPair.Helpers.cs b/Content.IntegrationTests/Pair/TestPair.Helpers.cs index 0ea6d3e2dc..f46b83165f 100644 --- a/Content.IntegrationTests/Pair/TestPair.Helpers.cs +++ b/Content.IntegrationTests/Pair/TestPair.Helpers.cs @@ -2,6 +2,9 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; +using Content.Server.Preferences.Managers; +using Content.Shared.Preferences; +using Content.Shared.Roles; using Robust.Shared.GameObjects; using Robust.Shared.Map; using Robust.Shared.Prototypes; @@ -128,4 +131,29 @@ public List GetPrototypesWithComponent( return list; } + + /// + /// Helper method for enabling or disabling a antag role + /// + public async Task SetAntagPref(ProtoId id, bool value) + { + var prefMan = Server.ResolveDependency(); + + var prefs = prefMan.GetPreferences(Client.User!.Value); + // what even is the point of ICharacterProfile if we always cast it to HumanoidCharacterProfile to make it usable? + var profile = (HumanoidCharacterProfile) prefs.SelectedCharacter; + + Assert.That(profile.AntagPreferences.Any(preference => preference == id), Is.EqualTo(!value)); + var newProfile = profile.WithAntagPreference(id, value); + + await Server.WaitPost(() => + { + prefMan.SetProfile(Client.User.Value, prefs.SelectedCharacterIndex, newProfile).Wait(); + }); + + // And why the fuck does it always create a new preference and profile object instead of just reusing them? + var newPrefs = prefMan.GetPreferences(Client.User.Value); + var newProf = (HumanoidCharacterProfile) newPrefs.SelectedCharacter; + Assert.That(newProf.AntagPreferences.Any(preference => preference == id), Is.EqualTo(value)); + } } diff --git a/Content.IntegrationTests/PoolManager.cs b/Content.IntegrationTests/PoolManager.cs index b544fe2854..3f489de649 100644 --- a/Content.IntegrationTests/PoolManager.cs +++ b/Content.IntegrationTests/PoolManager.cs @@ -68,11 +68,11 @@ public static partial class PoolManager options.BeforeStart += () => { + // Server-only systems (i.e., systems that subscribe to events with server-only components) var entSysMan = IoCManager.Resolve(); - entSysMan.LoadExtraSystemType(); - entSysMan.LoadExtraSystemType(); entSysMan.LoadExtraSystemType(); entSysMan.LoadExtraSystemType(); + IoCManager.Resolve().GetSawmill("loc").Level = LogLevel.Error; IoCManager.Resolve() .OnValueChanged(RTCVars.FailureLogLevel, value => logHandler.FailureLevel = value, true); diff --git a/Content.IntegrationTests/Tests/GameRules/AntagPreferenceTest.cs b/Content.IntegrationTests/Tests/GameRules/AntagPreferenceTest.cs new file mode 100644 index 0000000000..662ea3b974 --- /dev/null +++ b/Content.IntegrationTests/Tests/GameRules/AntagPreferenceTest.cs @@ -0,0 +1,76 @@ +#nullable enable +using System.Collections.Generic; +using System.Linq; +using Content.Server.Antag; +using Content.Server.Antag.Components; +using Content.Server.GameTicking; +using Content.Shared.GameTicking; +using Robust.Shared.GameObjects; +using Robust.Shared.Player; +using Robust.Shared.Random; + +namespace Content.IntegrationTests.Tests.GameRules; + +// Once upon a time, players in the lobby weren't ever considered eligible for antag roles. +// Lets not let that happen again. +[TestFixture] +public sealed class AntagPreferenceTest +{ + [Test] + public async Task TestLobbyPlayersValid() + { + await using var pair = await PoolManager.GetServerClient(new PoolSettings + { + DummyTicker = false, + Connected = true, + InLobby = true + }); + + var server = pair.Server; + var client = pair.Client; + var ticker = server.System(); + var sys = server.System(); + + // Initially in the lobby + Assert.That(ticker.RunLevel, Is.EqualTo(GameRunLevel.PreRoundLobby)); + Assert.That(client.AttachedEntity, Is.Null); + Assert.That(ticker.PlayerGameStatuses[client.User!.Value], Is.EqualTo(PlayerGameStatus.NotReadyToPlay)); + + EntityUid uid = default; + await server.WaitPost(() => uid = server.EntMan.Spawn("Traitor")); + var rule = new Entity(uid, server.EntMan.GetComponent(uid)); + var def = rule.Comp.Definitions.Single(); + + // IsSessionValid & IsEntityValid are preference agnostic and should always be true for players in the lobby. + // Though maybe that will change in the future, but then GetPlayerPool() needs to be updated to reflect that. + Assert.That(sys.IsSessionValid(rule, pair.Player, def), Is.True); + Assert.That(sys.IsEntityValid(client.AttachedEntity, def), Is.True); + + // By default, traitor/antag preferences are disabled, so the pool should be empty. + var sessions = new List{pair.Player!}; + var pool = sys.GetPlayerPool(rule, sessions, def); + Assert.That(pool.Count, Is.EqualTo(0)); + + // Opt into the traitor role. + await pair.SetAntagPref("Traitor", true); + + Assert.That(sys.IsSessionValid(rule, pair.Player, def), Is.True); + Assert.That(sys.IsEntityValid(client.AttachedEntity, def), Is.True); + pool = sys.GetPlayerPool(rule, sessions, def); + Assert.That(pool.Count, Is.EqualTo(1)); + pool.TryPickAndTake(pair.Server.ResolveDependency(), out var picked); + Assert.That(picked, Is.EqualTo(pair.Player)); + Assert.That(sessions.Count, Is.EqualTo(1)); + + // opt back out + await pair.SetAntagPref("Traitor", false); + + Assert.That(sys.IsSessionValid(rule, pair.Player, def), Is.True); + Assert.That(sys.IsEntityValid(client.AttachedEntity, def), Is.True); + pool = sys.GetPlayerPool(rule, sessions, def); + Assert.That(pool.Count, Is.EqualTo(0)); + + await server.WaitPost(() => server.EntMan.DeleteEntity(uid)); + await pair.CleanReturnAsync(); + } +} diff --git a/Content.IntegrationTests/Tests/GameRules/NukeOpsTest.cs b/Content.IntegrationTests/Tests/GameRules/NukeOpsTest.cs index f539daee36..62fa93c999 100644 --- a/Content.IntegrationTests/Tests/GameRules/NukeOpsTest.cs +++ b/Content.IntegrationTests/Tests/GameRules/NukeOpsTest.cs @@ -60,6 +60,9 @@ public async Task TryStopNukeOpsFromConstantlyFailing() Assert.That(client.AttachedEntity, Is.Null); Assert.That(ticker.PlayerGameStatuses[client.User!.Value], Is.EqualTo(PlayerGameStatus.NotReadyToPlay)); + // Opt into the nukies role. + await pair.SetAntagPref("NukeopsCommander", true); + // There are no grids or maps Assert.That(entMan.Count(), Is.Zero); Assert.That(entMan.Count(), Is.Zero); @@ -201,6 +204,7 @@ public async Task TryStopNukeOpsFromConstantlyFailing() //ticker.SetGamePreset((GamePresetPrototype?)null); WD edit server.CfgMan.SetCVar(CCVars.GridFill, false); + await pair.SetAntagPref("NukeopsCommander", false); await pair.CleanReturnAsync(); } } diff --git a/Content.IntegrationTests/Tests/Interaction/Click/InteractionSystemTests.cs b/Content.IntegrationTests/Tests/Interaction/Click/InteractionSystemTests.cs index 317aa10400..4415eddf37 100644 --- a/Content.IntegrationTests/Tests/Interaction/Click/InteractionSystemTests.cs +++ b/Content.IntegrationTests/Tests/Interaction/Click/InteractionSystemTests.cs @@ -407,7 +407,6 @@ await server.WaitAssertion(() => await pair.CleanReturnAsync(); } - [Reflect(false)] public sealed class TestInteractionSystem : EntitySystem { public EntityEventHandler? InteractUsingEvent; diff --git a/Content.IntegrationTests/Tests/ResettingEntitySystemTests.cs b/Content.IntegrationTests/Tests/ResettingEntitySystemTests.cs index d5c2a9124d..40457f5488 100644 --- a/Content.IntegrationTests/Tests/ResettingEntitySystemTests.cs +++ b/Content.IntegrationTests/Tests/ResettingEntitySystemTests.cs @@ -9,7 +9,6 @@ namespace Content.IntegrationTests.Tests [TestOf(typeof(RoundRestartCleanupEvent))] public sealed class ResettingEntitySystemTests { - [Reflect(false)] public sealed class TestRoundRestartCleanupEvent : EntitySystem { public bool HasBeenReset { get; set; } @@ -49,8 +48,6 @@ await server.WaitAssertion(() => system.HasBeenReset = false; - Assert.That(system.HasBeenReset, Is.False); - gameTicker.RestartRound(); Assert.That(system.HasBeenReset); diff --git a/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs b/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs index df77a3a1a7..4103b8a8aa 100644 --- a/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs +++ b/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs @@ -44,9 +44,11 @@ private void AddAntagVerbs(GetVerbsEvent args) if (!_adminManager.HasAdminFlag(player, AdminFlags.Fun)) return; - if (!HasComp(args.Target)) + if (!HasComp(args.Target) || !TryComp(args.Target, out var targetActor)) return; + var targetPlayer = targetActor.PlayerSession; + Verb traitor = new() { Text = Loc.GetString("admin-verb-text-make-traitor"), @@ -54,7 +56,7 @@ private void AddAntagVerbs(GetVerbsEvent args) Icon = new SpriteSpecifier.Rsi(new ResPath("/Textures/Structures/Wallmounts/posters.rsi"), "poster5_contraband"), Act = () => { - _antag.ForceMakeAntag(player, DefaultTraitorRule); + _antag.ForceMakeAntag(targetPlayer, DefaultTraitorRule); }, Impact = LogImpact.High, Message = Loc.GetString("admin-verb-make-traitor"), @@ -83,7 +85,7 @@ private void AddAntagVerbs(GetVerbsEvent args) Icon = new SpriteSpecifier.Rsi(new("/Textures/Structures/Wallmounts/signs.rsi"), "radiation"), Act = () => { - _antag.ForceMakeAntag(player, DefaultNukeOpRule); + _antag.ForceMakeAntag(targetPlayer, DefaultNukeOpRule); }, Impact = LogImpact.High, Message = Loc.GetString("admin-verb-make-nuclear-operative"), @@ -112,7 +114,7 @@ private void AddAntagVerbs(GetVerbsEvent args) Icon = new SpriteSpecifier.Rsi(new("/Textures/Interface/Misc/job_icons.rsi"), "HeadRevolutionary"), Act = () => { - _antag.ForceMakeAntag(player, DefaultRevsRule); + _antag.ForceMakeAntag(targetPlayer, DefaultRevsRule); }, Impact = LogImpact.High, Message = Loc.GetString("admin-verb-make-head-rev"), @@ -123,10 +125,10 @@ private void AddAntagVerbs(GetVerbsEvent args) { Text = Loc.GetString("admin-verb-text-make-thief"), Category = VerbCategory.Antag, - Icon = new SpriteSpecifier.Rsi(new ResPath("/Textures/Clothing/Hands/Gloves/ihscombat.rsi"), "icon"), + Icon = new SpriteSpecifier.Rsi(new ResPath("/Textures/Clothing/Hands/Gloves/Color/black.rsi"), "icon"), Act = () => { - _antag.ForceMakeAntag(player, DefaultThiefRule); + _antag.ForceMakeAntag(targetPlayer, DefaultThiefRule); }, Impact = LogImpact.High, Message = Loc.GetString("admin-verb-make-thief"), diff --git a/Content.Server/Antag/AntagSelectionSystem.API.cs b/Content.Server/Antag/AntagSelectionSystem.API.cs index 470f98fca1..59bf05fe03 100644 --- a/Content.Server/Antag/AntagSelectionSystem.API.cs +++ b/Content.Server/Antag/AntagSelectionSystem.API.cs @@ -7,6 +7,7 @@ using Content.Shared.Mind; using JetBrains.Annotations; using Robust.Shared.Audio; +using Robust.Shared.Enums; using Robust.Shared.Player; namespace Content.Server.Antag; @@ -26,6 +27,11 @@ public bool TryGetNextAvailableDefinition(Entity ent, if (mindCount >= totalTargetCount) return false; + // TODO ANTAG fix this + // If here are two definitions with 1/10 and 10/10 slots filled, this will always return the second definition + // even though it has already met its target + // AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA I fucking hate game ticker code. + // It needs to track selected minds for each definition independently. foreach (var def in ent.Comp.Definitions) { var target = GetTargetAntagCount(ent, null, def); @@ -46,12 +52,26 @@ public bool TryGetNextAvailableDefinition(Entity ent, /// Gets the number of antagonists that should be present for a given rule based on the provided pool. /// A null pool will simply use the player count. /// - public int GetTargetAntagCount(Entity ent, AntagSelectionPlayerPool? pool = null) + public int GetTargetAntagCount(Entity ent, int? playerCount = null) { var count = 0; foreach (var def in ent.Comp.Definitions) { - count += GetTargetAntagCount(ent, pool, def); + count += GetTargetAntagCount(ent, playerCount, def); + } + + return count; + } + + public int GetTotalPlayerCount(IList pool) + { + var count = 0; + foreach (var session in pool) + { + if (session.Status is SessionStatus.Disconnected or SessionStatus.Zombie) + continue; + + count++; } return count; @@ -61,9 +81,14 @@ public int GetTargetAntagCount(Entity ent, AntagSelecti /// Gets the number of antagonists that should be present for a given antag definition based on the provided pool. /// A null pool will simply use the player count. /// - public int GetTargetAntagCount(Entity ent, AntagSelectionPlayerPool? pool, AntagSelectionDefinition def) + public int GetTargetAntagCount(Entity ent, int? playerCount, AntagSelectionDefinition def) { - var poolSize = pool?.Count ?? _playerManager.Sessions.Length; + // TODO ANTAG + // make pool non-nullable + // Review uses and ensure that people are INTENTIONALLY including players in the lobby if this is a mid-round + // antag selection. + var poolSize = playerCount ?? GetTotalPlayerCount(_playerManager.Sessions); + // factor in other definitions' affect on the count. var countOffset = 0; foreach (var otherDef in ent.Comp.Definitions) @@ -278,7 +303,7 @@ public void ForceMakeAntag(ICommonSession? player, string defaultRule) where if (!TryGetNextAvailableDefinition(rule, out var def)) def = rule.Comp.Definitions.Last(); - + MakeAntag(rule, player, def.Value); } diff --git a/Content.Server/Antag/AntagSelectionSystem.cs b/Content.Server/Antag/AntagSelectionSystem.cs index 6bfb7394f5..d74824dd2d 100644 --- a/Content.Server/Antag/AntagSelectionSystem.cs +++ b/Content.Server/Antag/AntagSelectionSystem.cs @@ -13,6 +13,7 @@ using Content.Server.Shuttles.Components; using Content.Server.Station.Systems; using Content.Shared.Antag; +using Content.Shared.GameTicking; using Content.Shared.Ghost; using Content.Shared.Humanoid; using Content.Shared.Players; @@ -24,6 +25,7 @@ using Robust.Shared.Map; using Robust.Shared.Player; using Robust.Shared.Random; +using Robust.Shared.Utility; namespace Content.Server.Antag; @@ -82,10 +84,9 @@ private void OnPlayerSpawning(RulePlayerSpawningEvent args) continue; if (comp.SelectionsComplete) - return; + continue; ChooseAntags((uid, comp), pool); - comp.SelectionsComplete = true; foreach (var session in comp.SelectedSessions) { @@ -103,11 +104,7 @@ private void OnJobsAssigned(RulePlayerJobsAssignedEvent args) if (comp.SelectionTime != AntagSelectionTime.PostPlayerSpawn) continue; - if (comp.SelectionsComplete) - continue; - - ChooseAntags((uid, comp)); - comp.SelectionsComplete = true; + ChooseAntags((uid, comp), args.Players); } } @@ -123,12 +120,18 @@ private void OnSpawnComplete(PlayerSpawnCompleteEvent args) var query = QueryActiveRules(); while (query.MoveNext(out var uid, out _, out var antag, out _)) { + // TODO ANTAG + // what why aasdiuhasdopiuasdfhksad + // stop this insanity please + // probability of antag assignment shouldn't depend on the order in which rules are returned by the query. if (!RobustRandom.Prob(LateJoinRandomChance)) continue; if (!antag.Definitions.Any(p => p.LateJoinAdditional)) continue; + DebugTools.AssertEqual(antag.SelectionTime, AntagSelectionTime.PostPlayerSpawn); + if (!TryGetNextAvailableDefinition((uid, antag), out var def)) continue; @@ -161,46 +164,43 @@ protected override void Started(EntityUid uid, AntagSelectionComponent component { base.Started(uid, component, gameRule, args); - if (component.SelectionsComplete) - return; - + // If the round has not yet started, we defer antag selection until roundstart if (GameTicker.RunLevel != GameRunLevel.InRound) return; - if (GameTicker.RunLevel == GameRunLevel.InRound && component.SelectionTime == AntagSelectionTime.PrePlayerSpawn) + if (component.SelectionsComplete) return; - ChooseAntags((uid, component)); - component.SelectionsComplete = true; - } + var players = _playerManager.Sessions + .Where(x => GameTicker.PlayerGameStatuses[x.UserId] == PlayerGameStatus.JoinedGame) + .ToList(); - /// - /// Chooses antagonists from the current selection of players - /// - public void ChooseAntags(Entity ent) - { - var sessions = _playerManager.Sessions.ToList(); - ChooseAntags(ent, sessions); + ChooseAntags((uid, component), players); } /// /// Chooses antagonists from the given selection of players /// - public void ChooseAntags(Entity ent, List pool) + public void ChooseAntags(Entity ent, IList pool) { + if (ent.Comp.SelectionsComplete) + return; + foreach (var def in ent.Comp.Definitions) { ChooseAntags(ent, pool, def); } + + ent.Comp.SelectionsComplete = true; } /// /// Chooses antagonists from the given selection of players for the given antag definition. /// - public void ChooseAntags(Entity ent, List pool, AntagSelectionDefinition def) + public void ChooseAntags(Entity ent, IList pool, AntagSelectionDefinition def) { var playerPool = GetPlayerPool(ent, pool, def); - var count = GetTargetAntagCount(ent, playerPool, def); + var count = GetTargetAntagCount(ent, GetTotalPlayerCount(pool), def); for (var i = 0; i < count; i++) { @@ -308,11 +308,7 @@ public void MakeAntag(Entity ent, ICommonSession? sessi _mind.TransferTo(curMind.Value, antagEnt, ghostCheckOverride: true); _role.MindAddRoles(curMind.Value, def.MindComponents); ent.Comp.SelectedMinds.Add((curMind.Value, Name(player))); - } - - if (def.Briefing is { } briefing) - { - SendBriefing(session, briefing); + SendBriefing(session, def.Briefing); } var afterEv = new AfterAntagEntitySelectedEvent(session, player, ent, def); @@ -322,20 +318,15 @@ public void MakeAntag(Entity ent, ICommonSession? sessi /// /// Gets an ordered player pool based on player preferences and the antagonist definition. /// - public AntagSelectionPlayerPool GetPlayerPool(Entity ent, List sessions, AntagSelectionDefinition def) + public AntagSelectionPlayerPool GetPlayerPool(Entity ent, IList sessions, AntagSelectionDefinition def) { var preferredList = new List(); - var secondBestList = new List(); - var unwantedList = new List(); - var invalidList = new List(); + var fallbackList = new List(); foreach (var session in sessions) { if (!IsSessionValid(ent, session, def) || !IsEntityValid(session.AttachedEntity, def)) - { - invalidList.Add(session); continue; - } var pref = (HumanoidCharacterProfile) _pref.GetPreferences(session.UserId).SelectedCharacter; if (def.PrefRoles.Count != 0 && pref.AntagPreferences.Any(p => def.PrefRoles.Contains(p))) @@ -344,15 +335,11 @@ public AntagSelectionPlayerPool GetPlayerPool(Entity en } else if (def.FallbackRoles.Count != 0 && pref.AntagPreferences.Any(p => def.FallbackRoles.Contains(p))) { - secondBestList.Add(session); - } - else - { - unwantedList.Add(session); + fallbackList.Add(session); } } - return new AntagSelectionPlayerPool(new() { preferredList, secondBestList, unwantedList, invalidList }); + return new AntagSelectionPlayerPool(new() { preferredList, fallbackList }); } /// @@ -363,14 +350,18 @@ public bool IsSessionValid(Entity ent, ICommonSession? if (session == null) return true; - mind ??= session.GetMind(); - if (session.Status is SessionStatus.Disconnected or SessionStatus.Zombie) return false; if (ent.Comp.SelectedSessions.Contains(session)) return false; + mind ??= session.GetMind(); + + // If the player has not spawned in as any entity (e.g., in the lobby), they can be given an antag role/entity. + if (mind == null) + return true; + //todo: we need some way to check that we're not getting the same role twice. (double picking thieves or zombies through midrounds) switch (def.MultiAntagSetting) @@ -399,10 +390,11 @@ public bool IsSessionValid(Entity ent, ICommonSession? /// /// Checks if a given entity (mind/session not included) is valid for a given antagonist. /// - private bool IsEntityValid(EntityUid? entity, AntagSelectionDefinition def) + public bool IsEntityValid(EntityUid? entity, AntagSelectionDefinition def) { + // If the player has not spawned in as any entity (e.g., in the lobby), they can be given an antag role/entity. if (entity == null) - return false; + return true; if (HasComp(entity)) return false; diff --git a/Content.Server/GameTicking/GameTicker.RoundFlow.cs b/Content.Server/GameTicking/GameTicker.RoundFlow.cs index 154244ace1..0fc984abed 100644 --- a/Content.Server/GameTicking/GameTicker.RoundFlow.cs +++ b/Content.Server/GameTicking/GameTicker.RoundFlow.cs @@ -175,6 +175,26 @@ public IReadOnlyList LoadGameMap(GameMapPrototype map, MapId targetMa return gridUids; } + public int ReadyPlayerCount() + { + var total = 0; + foreach (var (userId, status) in _playerGameStatuses) + { + if (LobbyEnabled && status == PlayerGameStatus.NotReadyToPlay) + continue; + + if (!_playerManager.TryGetSessionById(userId, out _)) + continue; + + if (_banManager.GetRoleBans(userId) == null) + continue; + + total++; + } + + return total; + } + public void StartRound(bool force = false) { #if EXCEPTION_TOLERANCE @@ -230,6 +250,8 @@ public void StartRound(bool force = false) readyPlayerProfiles.Add(userId, profile); } + DebugTools.AssertEqual(readyPlayers.Count, ReadyPlayerCount()); + // Just in case it hasn't been loaded previously we'll try loading it. LoadMaps(); @@ -771,7 +793,7 @@ public RulePlayerSpawningEvent(List playerPool, IReadOnlyDiction } /// - /// Event raised after players were assigned jobs by the GameTicker. + /// Event raised after players were assigned jobs by the GameTicker and have been spawned in. /// You can give on-station people special roles by listening to this event. /// public sealed class RulePlayerJobsAssignedEvent diff --git a/Content.Server/GameTicking/Rules/SecretRuleSystem.cs b/Content.Server/GameTicking/Rules/SecretRuleSystem.cs index d5adb8fdb7..95bf5986a5 100644 --- a/Content.Server/GameTicking/Rules/SecretRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/SecretRuleSystem.cs @@ -1,15 +1,17 @@ +using System.Diagnostics.CodeAnalysis; +using System.Linq; using Content.Server.Administration.Logs; using Content.Server.GameTicking.Components; using Content.Server.Chat.Managers; using Content.Server.GameTicking.Presets; using Content.Server.GameTicking.Rules.Components; using Content.Shared.Random; -using Content.Shared.Random.Helpers; using Content.Shared.CCVar; using Content.Shared.Database; using Robust.Shared.Prototypes; using Robust.Shared.Random; using Robust.Shared.Configuration; +using Robust.Shared.Utility; namespace Content.Server.GameTicking.Rules; @@ -20,11 +22,46 @@ public sealed class SecretRuleSystem : GameRuleSystem [Dependency] private readonly IConfigurationManager _configurationManager = default!; [Dependency] private readonly IAdminLogManager _adminLogger = default!; [Dependency] private readonly IChatManager _chatManager = default!; + [Dependency] private readonly IComponentFactory _compFact = default!; + + private string _ruleCompName = default!; + + public override void Initialize() + { + base.Initialize(); + _ruleCompName = _compFact.GetComponentName(typeof(GameRuleComponent)); + } protected override void Added(EntityUid uid, SecretRuleComponent component, GameRuleComponent gameRule, GameRuleAddedEvent args) { base.Added(uid, component, gameRule, args); - PickRule(component); + var weights = _configurationManager.GetCVar(CCVars.SecretWeightPrototype); + + if (!TryPickPreset(weights, out var preset)) + { + Log.Error($"{ToPrettyString(uid)} failed to pick any preset. Removing rule."); + Del(uid); + return; + } + + Log.Info($"Selected {preset.ID} as the secret preset."); + _adminLogger.Add(LogType.EventStarted, $"Selected {preset.ID} as the secret preset."); + _chatManager.SendAdminAnnouncement(Loc.GetString("rule-secret-selected-preset", ("preset", preset.ID))); + + foreach (var rule in preset.Rules) + { + EntityUid ruleEnt; + + // if we're pre-round (i.e. will only be added) + // then just add rules. if we're added in the middle of the round (or at any other point really) + // then we want to start them as well + if (GameTicker.RunLevel <= GameRunLevel.InRound) + ruleEnt = GameTicker.AddGameRule(rule); + else + GameTicker.StartGameRule(rule, out ruleEnt); + + component.AdditionalGameRules.Add(ruleEnt); + } } protected override void Ended(EntityUid uid, SecretRuleComponent component, GameRuleComponent gameRule, GameRuleEndedEvent args) @@ -37,32 +74,101 @@ protected override void Ended(EntityUid uid, SecretRuleComponent component, Game } } - private void PickRule(SecretRuleComponent component) + private bool TryPickPreset(ProtoId weights, [NotNullWhen(true)] out GamePresetPrototype? preset) { - // TODO: This doesn't consider what can't start due to minimum player count, - // but currently there's no way to know anyway as they use cvars. - var presetString = _configurationManager.GetCVar(CCVars.SecretWeightPrototype); - var preset = _prototypeManager.Index(presetString).Pick(_random); - Log.Info($"Selected {preset} for secret."); - _adminLogger.Add(LogType.EventStarted, $"Selected {preset} for secret."); - _chatManager.SendAdminAnnouncement(Loc.GetString("rule-secret-selected-preset", ("preset", preset))); - - var rules = _prototypeManager.Index(preset).Rules; - foreach (var rule in rules) + var options = _prototypeManager.Index(weights).Weights.ShallowClone(); + var players = GameTicker.ReadyPlayerCount(); + + GamePresetPrototype? selectedPreset = null; + var sum = options.Values.Sum(); + while (options.Count > 0) { - EntityUid ruleEnt; + var accumulated = 0f; + var rand = _random.NextFloat(sum); + foreach (var (key, weight) in options) + { + accumulated += weight; + if (accumulated < rand) + continue; - // if we're pre-round (i.e. will only be added) - // then just add rules. if we're added in the middle of the round (or at any other point really) - // then we want to start them as well - if (GameTicker.RunLevel <= GameRunLevel.InRound) - ruleEnt = GameTicker.AddGameRule(rule); - else + if (!_prototypeManager.TryIndex(key, out selectedPreset)) + Log.Error($"Invalid preset {selectedPreset} in secret rule weights: {weights}"); + + options.Remove(key); + sum -= weight; + break; + } + + if (CanPick(selectedPreset, players)) { - GameTicker.StartGameRule(rule, out ruleEnt); + preset = selectedPreset; + return true; } - component.AdditionalGameRules.Add(ruleEnt); + if (selectedPreset != null) + Log.Info($"Excluding {selectedPreset.ID} from secret preset selection."); + } + + preset = null; + return false; + } + + public bool CanPickAny() + { + var secretPresetId = _configurationManager.GetCVar(CCVars.SecretWeightPrototype); + return CanPickAny(secretPresetId); + } + + /// + /// Can any of the given presets be picked, taking into account the currently available player count? + /// + public bool CanPickAny(ProtoId weightedPresets) + { + var ids = _prototypeManager.Index(weightedPresets).Weights.Keys + .Select(x => new ProtoId(x)); + + return CanPickAny(ids); + } + + /// + /// Can any of the given presets be picked, taking into account the currently available player count? + /// + public bool CanPickAny(IEnumerable> protos) + { + var players = GameTicker.ReadyPlayerCount(); + foreach (var id in protos) + { + if (!_prototypeManager.TryIndex(id, out var selectedPreset)) + Log.Error($"Invalid preset {selectedPreset} in secret rule weights: {id}"); + + if (CanPick(selectedPreset, players)) + return true; } + + return false; + } + + /// + /// Can the given preset be picked, taking into account the currently available player count? + /// + private bool CanPick([NotNullWhen(true)] GamePresetPrototype? selected, int players) + { + if (selected == null) + return false; + + foreach (var ruleId in selected.Rules) + { + if (!_prototypeManager.TryIndex(ruleId, out EntityPrototype? rule) + || !rule.TryGetComponent(_ruleCompName, out GameRuleComponent? ruleComp)) + { + Log.Error($"Encountered invalid rule {ruleId} in preset {selected.ID}"); + return false; + } + + if (ruleComp.MinPlayers > players) + return false; + } + + return true; } } diff --git a/Content.Server/Preferences/Managers/IServerPreferencesManager.cs b/Content.Server/Preferences/Managers/IServerPreferencesManager.cs index 1808592ef5..f26c998495 100644 --- a/Content.Server/Preferences/Managers/IServerPreferencesManager.cs +++ b/Content.Server/Preferences/Managers/IServerPreferencesManager.cs @@ -19,5 +19,7 @@ public interface IServerPreferencesManager PlayerPreferences? GetPreferencesOrNull(NetUserId? userId); IEnumerable> GetSelectedProfilesForPlayers(List userIds); bool HavePreferencesLoaded(ICommonSession session); + + Task SetProfile(NetUserId userId, int slot, ICharacterProfile profile); } } diff --git a/Content.Server/Preferences/Managers/ServerPreferencesManager.cs b/Content.Server/Preferences/Managers/ServerPreferencesManager.cs index c3efe14be9..95eb9591fb 100644 --- a/Content.Server/Preferences/Managers/ServerPreferencesManager.cs +++ b/Content.Server/Preferences/Managers/ServerPreferencesManager.cs @@ -3,17 +3,12 @@ using System.Threading; using System.Threading.Tasks; using Content.Server.Database; -using Content.Server.Humanoid; using Content.Shared.CCVar; -using Content.Shared.Humanoid.Prototypes; using Content.Shared.Preferences; -using Content.Shared.Roles; using Robust.Server.Player; using Robust.Shared.Configuration; using Robust.Shared.Network; using Robust.Shared.Player; -using Robust.Shared.Prototypes; - namespace Content.Server.Preferences.Managers { @@ -27,12 +22,14 @@ public sealed class ServerPreferencesManager : IServerPreferencesManager [Dependency] private readonly IConfigurationManager _cfg = default!; [Dependency] private readonly IServerDbManager _db = default!; [Dependency] private readonly IPlayerManager _playerManager = default!; - [Dependency] private readonly IPrototypeManager _protos = default!; + [Dependency] private readonly ILogManager _log = default!; // Cache player prefs on the server so we don't need as much async hell related to them. private readonly Dictionary _cachedPlayerPrefs = new(); + private ISawmill _sawmill = default!; + private int MaxCharacterSlots => _cfg.GetCVar(CCVars.GameMaxCharacterSlots); public void Init() @@ -41,6 +38,7 @@ public void Init() _netManager.RegisterNetMessage(HandleSelectCharacterMessage); _netManager.RegisterNetMessage(HandleUpdateCharacterMessage); _netManager.RegisterNetMessage(HandleDeleteCharacterMessage); + _sawmill = _log.GetSawmill("prefs"); } private async void HandleSelectCharacterMessage(MsgSelectCharacter message) @@ -77,27 +75,25 @@ private async void HandleSelectCharacterMessage(MsgSelectCharacter message) private async void HandleUpdateCharacterMessage(MsgUpdateCharacter message) { - var slot = message.Slot; - var profile = message.Profile; var userId = message.MsgChannel.UserId; - if (profile == null) - { - Logger.WarningS("prefs", - $"User {userId} sent a {nameof(MsgUpdateCharacter)} with a null profile in slot {slot}."); - return; - } + // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + if (message.Profile == null) + _sawmill.Error($"User {userId} sent a {nameof(MsgUpdateCharacter)} with a null profile in slot {message.Slot}."); + else + await SetProfile(userId, message.Slot, message.Profile); + } + public async Task SetProfile(NetUserId userId, int slot, ICharacterProfile profile) + { if (!_cachedPlayerPrefs.TryGetValue(userId, out var prefsData) || !prefsData.PrefsLoaded) { - Logger.WarningS("prefs", $"User {userId} tried to modify preferences before they loaded."); + _sawmill.Error($"Tried to modify user {userId} preferences before they loaded."); return; } if (slot < 0 || slot >= MaxCharacterSlots) - { return; - } var curPrefs = prefsData.Prefs!; var session = _playerManager.GetSessionById(userId); @@ -112,10 +108,8 @@ private async void HandleUpdateCharacterMessage(MsgUpdateCharacter message) prefsData.Prefs = new PlayerPreferences(profiles, slot, curPrefs.AdminOOCColor); - if (ShouldStorePrefs(message.MsgChannel.AuthType)) - { - await _db.SaveCharacterSlotAsync(message.MsgChannel.UserId, message.Profile, message.Slot); - } + if (ShouldStorePrefs(session.Channel.AuthType)) + await _db.SaveCharacterSlotAsync(userId, profile, slot); } private async void HandleDeleteCharacterMessage(MsgDeleteCharacter message) @@ -142,7 +136,7 @@ private async void HandleDeleteCharacterMessage(MsgDeleteCharacter message) if (curPrefs.SelectedCharacterIndex == slot) { // That ! on the end is because Rider doesn't like .NET 5. - var (ns, profile) = curPrefs.Characters.FirstOrDefault(p => p.Key != message.Slot)!; + var (ns, profile) = curPrefs.Characters.FirstOrDefault(p => p.Key != message.Slot); if (profile == null) { // Only slot left, can't delete. @@ -157,16 +151,18 @@ private async void HandleDeleteCharacterMessage(MsgDeleteCharacter message) prefsData.Prefs = new PlayerPreferences(arr, nextSlot ?? curPrefs.SelectedCharacterIndex, curPrefs.AdminOOCColor); - if (ShouldStorePrefs(message.MsgChannel.AuthType)) + if (!ShouldStorePrefs(message.MsgChannel.AuthType)) { - if (nextSlot != null) - { - await _db.DeleteSlotAndSetSelectedIndex(userId, slot, nextSlot.Value); - } - else - { - await _db.SaveCharacterSlotAsync(userId, null, slot); - } + return; + } + + if (nextSlot != null) + { + await _db.DeleteSlotAndSetSelectedIndex(userId, slot, nextSlot.Value); + } + else + { + await _db.SaveCharacterSlotAsync(userId, null, slot); } } @@ -200,11 +196,13 @@ async Task LoadPrefs() prefsData.Prefs = prefs; prefsData.PrefsLoaded = true; - var msg = new MsgPreferencesAndSettings(); - msg.Preferences = prefs; - msg.Settings = new GameSettings + var msg = new MsgPreferencesAndSettings { - MaxCharacterSlots = MaxCharacterSlots + Preferences = prefs, + Settings = new GameSettings + { + MaxCharacterSlots = MaxCharacterSlots + } }; _netManager.ServerSendMessage(msg, session.Channel); } @@ -221,7 +219,6 @@ public bool HavePreferencesLoaded(ICommonSession session) return _cachedPlayerPrefs.ContainsKey(session.UserId); } - /// /// Tries to get the preferences from the cache /// @@ -289,10 +286,8 @@ private PlayerPreferences SanitizePreferences(ICommonSession session, PlayerPref { // Clean up preferences in case of changes to the game, // such as removed jobs still being selected. - return new PlayerPreferences(prefs.Characters.Select(p => - { - return new KeyValuePair(p.Key, p.Value.Validated(session, collection)); - }), prefs.SelectedCharacterIndex, prefs.AdminOOCColor); + return new PlayerPreferences(prefs.Characters.Select(p => new KeyValuePair(p.Key, + p.Value.Validated(session, collection))), prefs.SelectedCharacterIndex, prefs.AdminOOCColor); } public IEnumerable> GetSelectedProfilesForPlayers( diff --git a/Content.Shared/Antag/AntagAcceptability.cs b/Content.Shared/Antag/AntagAcceptability.cs index 02d0b5f58f..f56be97503 100644 --- a/Content.Shared/Antag/AntagAcceptability.cs +++ b/Content.Shared/Antag/AntagAcceptability.cs @@ -22,6 +22,14 @@ public enum AntagAcceptability public enum AntagSelectionTime : byte { + /// + /// Antag roles are assigned before players are assigned jobs and spawned in. + /// This prevents antag selection from happening if the round is on-going. + /// PrePlayerSpawn, + + /// + /// Antag roles get assigned after players have been assigned jobs and have spawned in. + /// PostPlayerSpawn } diff --git a/Content.Shared/Roles/Jobs/SharedJobSystem.cs b/Content.Shared/Roles/Jobs/SharedJobSystem.cs index 04ac45c4c5..fcf7605278 100644 --- a/Content.Shared/Roles/Jobs/SharedJobSystem.cs +++ b/Content.Shared/Roles/Jobs/SharedJobSystem.cs @@ -146,8 +146,10 @@ public string MindTryGetJobName([NotNullWhen(true)] EntityUid? mindId) public bool CanBeAntag(ICommonSession player) { + // If the player does not have any mind associated with them (e.g., has not spawned in or is in the lobby), then + // they are eligible to be given an antag role/entity. if (_playerSystem.ContentData(player) is not { Mind: { } mindId }) - return false; + return true; if (!MindTryGetJob(mindId, out _, out var prototype)) return true; diff --git a/Content.Shared/Station/SharedStationSpawningSystem.cs b/Content.Shared/Station/SharedStationSpawningSystem.cs index ea0898824b..9e93318103 100644 --- a/Content.Shared/Station/SharedStationSpawningSystem.cs +++ b/Content.Shared/Station/SharedStationSpawningSystem.cs @@ -52,7 +52,6 @@ public void EquipStartingGear(EntityUid entity, StartingGearPrototype? startingG } if (TryComp(entity, out HandsComponent? handsComponent)) - return; { var inhand = startingGear.Inhand; var coords = EntityManager.GetComponent(entity).Coordinates; @@ -61,8 +60,10 @@ public void EquipStartingGear(EntityUid entity, StartingGearPrototype? startingG var inhandEntity = EntityManager.SpawnEntity(prototype, coords); if (_handsSystem.TryGetEmptyHand(entity, out var emptyHand, handsComponent)) + { _handsSystem.TryPickup(entity, inhandEntity, emptyHand, checkActionBlocker: false, handsComp: handsComponent); + } } } @@ -78,7 +79,9 @@ public void EquipStartingGear(EntityUid entity, StartingGearPrototype? startingG continue; foreach (var ent in entProtos) + { ents.Add(Spawn(ent, coords)); + } if (inventoryComp == null || !InventorySystem.TryGetSlotEntity(entity, slot, out var slotEnt, @@ -87,7 +90,9 @@ public void EquipStartingGear(EntityUid entity, StartingGearPrototype? startingG continue; foreach (var ent in ents) + { _storage.Insert(slotEnt.Value, ent, out _, storageComp: storage, playSound: false); + } } } } From a625d9e73d4dbabb3d38b627b6d319e8cddfded9 Mon Sep 17 00:00:00 2001 From: WWDP Changelogs Date: Sat, 24 Aug 2024 04:46:16 +0000 Subject: [PATCH 10/14] Automatic Changelog Update (#14) --- Resources/Changelog/Changelog.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 26bc3f5f9f..472ea981d9 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -5425,3 +5425,11 @@ Entries: message: Press to walk / Нажать чтобы идти id: 6274 time: '2024-08-23T13:30:51.0000000+00:00' +- author: Remuchi + changes: + - type: Fix + message: >- + Thieves and Ninjas now properly spawn with their gear / Ниндзя и воры + теперь корректно спавнятся со своим снаряжением. + id: 6275 + time: '2024-08-24T04:45:51.0000000+00:00' From fa17362f121d0b2b1ca59445ec60e2133ad74873 Mon Sep 17 00:00:00 2001 From: Spatison <137375981+Spatison@users.noreply.github.com> Date: Tue, 27 Aug 2024 13:01:15 +0300 Subject: [PATCH 11/14] =?UTF-8?q?[Port]=20BlinkSystem=20/=20=D0=A1=D0=B8?= =?UTF-8?q?=D1=81=D1=82=D0=B5=D0=BC=D0=B0=20=D0=A2=D0=B5=D0=BB=D0=B5=D0=BF?= =?UTF-8?q?=D0=BE=D1=80=D1=82=D0=B0=D1=86=D0=B8=D0=B8=20(#19)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit add: betrayal dagger --- .../Weapons/Melee/MeleeWeaponSystem.cs | 22 ++++++ Content.Server/_White/Blink/BlinkSystem.cs | 65 ++++++++++++++++++ Content.Shared/_White/Blink/BlinkComponent.cs | 31 +++++++++ .../_White/Standing/SharedLayingDownSystem.cs | 14 ++++ .../objects/weapons/melee/daggers.ftl | 2 + .../Objects/Weapons/Melee/daggers.yml | 32 +++++++++ .../Melee/Daggers/betrayal_knife.rsi/icon.png | Bin 0 -> 388 bytes .../betrayal_knife.rsi/inhand-left.png | Bin 0 -> 334 bytes .../betrayal_knife.rsi/inhand-right.png | Bin 0 -> 339 bytes .../Daggers/betrayal_knife.rsi/meta.json | 22 ++++++ 10 files changed, 188 insertions(+) create mode 100644 Content.Server/_White/Blink/BlinkSystem.cs create mode 100644 Content.Shared/_White/Blink/BlinkComponent.cs create mode 100644 Resources/Locale/ru-RU/_white/prototypes/entities/objects/weapons/melee/daggers.ftl create mode 100644 Resources/Prototypes/_White/Entities/Objects/Weapons/Melee/daggers.yml create mode 100644 Resources/Textures/_White/Objects/Weapons/Melee/Daggers/betrayal_knife.rsi/icon.png create mode 100644 Resources/Textures/_White/Objects/Weapons/Melee/Daggers/betrayal_knife.rsi/inhand-left.png create mode 100644 Resources/Textures/_White/Objects/Weapons/Melee/Daggers/betrayal_knife.rsi/inhand-right.png create mode 100644 Resources/Textures/_White/Objects/Weapons/Melee/Daggers/betrayal_knife.rsi/meta.json diff --git a/Content.Client/Weapons/Melee/MeleeWeaponSystem.cs b/Content.Client/Weapons/Melee/MeleeWeaponSystem.cs index 98528c691d..fbb4a6904d 100644 --- a/Content.Client/Weapons/Melee/MeleeWeaponSystem.cs +++ b/Content.Client/Weapons/Melee/MeleeWeaponSystem.cs @@ -1,5 +1,6 @@ using System.Linq; using Content.Client.Gameplay; +using Content.Shared._White.Blink; using Content.Shared.CCVar; using Content.Shared.CombatMode; using Content.Shared.Effects; @@ -128,6 +129,27 @@ public override void Update(float frameTime) return; } + // WD EDIT START + if (HasComp(weaponUid)) + { + if (!_xformQuery.TryGetComponent(entity, out var userXform) || !Timing.IsFirstTimePredicted) + { + return; + } + + var targetMap = coordinates.ToMap(EntityManager, TransformSystem); + + if (targetMap.MapId != userXform.MapID) + return; + + var userPos = TransformSystem.GetWorldPosition(userXform); + var direction = targetMap.Position - userPos; + + RaiseNetworkEvent(new BlinkEvent(GetNetEntity(weaponUid), direction)); + return; + } + // WD EDIT END + ClientHeavyAttack(entity, coordinates, weaponUid, weapon); return; } diff --git a/Content.Server/_White/Blink/BlinkSystem.cs b/Content.Server/_White/Blink/BlinkSystem.cs new file mode 100644 index 0000000000..afcf9b436d --- /dev/null +++ b/Content.Server/_White/Blink/BlinkSystem.cs @@ -0,0 +1,65 @@ +using System.Linq; +using System.Numerics; +using Content.Shared._White.Blink; +using Content.Shared._White.Standing; +using Content.Shared.Physics; +using Robust.Server.Audio; +using Robust.Server.GameObjects; +using Robust.Shared.Physics; +using Robust.Shared.Timing; + +namespace Content.Server._White.Blink; + +public sealed class BlinkSystem : EntitySystem +{ + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly AudioSystem _audio = default!; + [Dependency] private readonly TransformSystem _transform = default!; + [Dependency] private readonly PhysicsSystem _physics = default!; + [Dependency] private readonly SharedLayingDownSystem _layingDown = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeAllEvent(OnBlink); + } + + private void OnBlink(BlinkEvent msg, EntitySessionEventArgs args) + { + if (args.SenderSession.AttachedEntity == null) + return; + + var user = args.SenderSession.AttachedEntity.Value; + + if (!TryComp(user, out TransformComponent? xform)) + return; + + if (!TryComp(GetEntity(msg.Weapon), out BlinkComponent? blink)) + return; + + if (blink.NextBlink > _timing.CurTime) + return; + + var blinkRate = TimeSpan.FromSeconds(1f / blink.BlinkRate); + + blink.NextBlink = _timing.CurTime + blinkRate; + + var coords = _transform.GetWorldPosition(xform); + var dir = msg.Direction.Normalized(); + var range = MathF.Min(blink.Distance, msg.Direction.Length()); + + var ray = new CollisionRay(coords, dir, (int) (CollisionGroup.Impassable | CollisionGroup.InteractImpassable)); + var rayResults = _physics.IntersectRayWithPredicate(xform.MapID, ray, range, x => x == user, false).ToList(); + + Vector2 targetPos; + if (rayResults.Count > 0) + targetPos = rayResults.MinBy(x => (x.HitPos - coords).Length()).HitPos - dir; + else + targetPos = coords + (msg.Direction.Length() > blink.Distance ? dir * blink.Distance : msg.Direction); + + _transform.SetWorldPosition(user, targetPos); + _layingDown.LieDownInRange(user, xform.Coordinates); + _audio.PlayPvs(blink.BlinkSound, user); + } +} diff --git a/Content.Shared/_White/Blink/BlinkComponent.cs b/Content.Shared/_White/Blink/BlinkComponent.cs new file mode 100644 index 0000000000..d60a69c66d --- /dev/null +++ b/Content.Shared/_White/Blink/BlinkComponent.cs @@ -0,0 +1,31 @@ +using System.Numerics; +using Robust.Shared.Audio; +using Robust.Shared.GameStates; +using Robust.Shared.Serialization; + +namespace Content.Shared._White.Blink; + +[RegisterComponent, NetworkedComponent] +public sealed partial class BlinkComponent : Component +{ + [DataField, ViewVariables(VVAccess.ReadWrite)] + public float Distance = 5f; + + [DataField, ViewVariables(VVAccess.ReadWrite)] + public float BlinkRate = 1f; + + public TimeSpan NextBlink; + + [DataField, ViewVariables(VVAccess.ReadWrite)] + public SoundSpecifier BlinkSound = new SoundPathSpecifier("/Audio/Magic/blink.ogg") + { + Params = AudioParams.Default.WithVolume(5f) + }; +} + +[Serializable, NetSerializable] +public sealed class BlinkEvent(NetEntity weapon, Vector2 direction) : EntityEventArgs +{ + public readonly NetEntity Weapon = weapon; + public readonly Vector2 Direction = direction; +} diff --git a/Content.Shared/_White/Standing/SharedLayingDownSystem.cs b/Content.Shared/_White/Standing/SharedLayingDownSystem.cs index 2406d19a37..629fdea3fe 100644 --- a/Content.Shared/_White/Standing/SharedLayingDownSystem.cs +++ b/Content.Shared/_White/Standing/SharedLayingDownSystem.cs @@ -1,3 +1,4 @@ +using System.Linq; using Content.Shared.DoAfter; using Content.Shared.Gravity; using Content.Shared.Input; @@ -6,6 +7,7 @@ using Content.Shared.Standing; using Content.Shared.Stunnable; using Robust.Shared.Input.Binding; +using Robust.Shared.Map; using Robust.Shared.Player; using Robust.Shared.Serialization; @@ -17,6 +19,7 @@ public abstract class SharedLayingDownSystem : EntitySystem [Dependency] private readonly StandingStateSystem _standing = default!; [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; [Dependency] private readonly SharedGravitySystem _gravity = default!; + [Dependency] private readonly EntityLookupSystem _lookup = default!; public override void Initialize() { @@ -149,6 +152,17 @@ public bool TryLieDown(EntityUid uid, LayingDownComponent? layingDown = null, St _standing.Down(uid, true, behavior != DropHeldItemsBehavior.NoDrop, standingState); return true; } + + public void LieDownInRange(EntityUid uid, EntityCoordinates coords, float range = 0.4f) + { + var ents = new HashSet>(); + _lookup.GetEntitiesInRange(coords, range, ents); + + foreach (var ent in ents.Where(ent => ent.Owner != uid)) + { + TryLieDown(ent, behavior:DropHeldItemsBehavior.DropIfStanding); + } + } } [Serializable, NetSerializable] diff --git a/Resources/Locale/ru-RU/_white/prototypes/entities/objects/weapons/melee/daggers.ftl b/Resources/Locale/ru-RU/_white/prototypes/entities/objects/weapons/melee/daggers.ftl new file mode 100644 index 0000000000..00ceb91748 --- /dev/null +++ b/Resources/Locale/ru-RU/_white/prototypes/entities/objects/weapons/melee/daggers.ftl @@ -0,0 +1,2 @@ +ent-BetrayalKnife = предательский кинжал + .desc = Береги спину. \ No newline at end of file diff --git a/Resources/Prototypes/_White/Entities/Objects/Weapons/Melee/daggers.yml b/Resources/Prototypes/_White/Entities/Objects/Weapons/Melee/daggers.yml new file mode 100644 index 0000000000..815e2192d9 --- /dev/null +++ b/Resources/Prototypes/_White/Entities/Objects/Weapons/Melee/daggers.yml @@ -0,0 +1,32 @@ +- type: entity + name: betrayal dagger + description: Watch your back. + parent: BaseKnife + id: BetrayalKnife + components: + - type: Sprite + sprite: _White/Objects/Weapons/Melee/Daggers/betrayal_knife.rsi + state: icon + - type: Item + size: Small + - type: MeleeWeapon + wideAnimationRotation: 180 + attackRate: 1.5 + damage: + types: + Slash: 17.5 + soundHit: + path: /Audio/Weapons/bladeslice.ogg + - type: Sharp + - type: EmbeddableProjectile + sound: /Audio/Weapons/star_hit.ogg + - type: ThrowingAngle + angle: 180 + - type: DamageOtherOnHit + damage: + types: + Slash: 20 + - type: DisarmMalus + malus: 0.225 + - type: Blink + blinkRate: 0.33 diff --git a/Resources/Textures/_White/Objects/Weapons/Melee/Daggers/betrayal_knife.rsi/icon.png b/Resources/Textures/_White/Objects/Weapons/Melee/Daggers/betrayal_knife.rsi/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..99d2f8453fedf5d41eb8e52e62f935713ca808be GIT binary patch literal 388 zcmV-~0ek+5P)-Rm=OUWB@?0}mvDFbt0vi_zr* iQ4~p8mTh~?Bk&JE#)-0qoticP0000;TVCQh)P!ecjs0j&Hk3BPsq1N}r-t^eIU#cyx z_r*W*G*qxT*f1;1V-z^g(2>q?#F*icac_%&gxmdKI;Vst0M-S0yZ`_I literal 0 HcmV?d00001 diff --git a/Resources/Textures/_White/Objects/Weapons/Melee/Daggers/betrayal_knife.rsi/inhand-right.png b/Resources/Textures/_White/Objects/Weapons/Melee/Daggers/betrayal_knife.rsi/inhand-right.png new file mode 100644 index 0000000000000000000000000000000000000000..8fff278de291f0f3e7baeac1dfb440fcf0cd0bbe GIT binary patch literal 339 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=oCO|{#S9F5M?jcysy3fA0|O(g zr;B4q#hkadF7moKh`2qB-_&mv(7+_JfSFapMUX47!R|_m@ub?rT**TJisj5V720$1 z7uy^0Gt6XfIK%Alj8Wk;!-PtP6E+Mfatzt_e1{FT=gxkq`u6N*u7K-bvjq;xB+gqK z#@My!#Inp;^ZNwe?bkV-I&W>Z*?q>)(A38k=W6etkG*cor+BA)-t*Hk)z{zdW0}3I z^3H?(eRd}Mm~dExD%|lieWU;6;5mNo%sX(UBa|JT=%{e?@^XR-osSS&te@Ne5 u?f=loq3da<^>>jUToBhn4cW~8gXzwV;B@b6ED69+V(@hJb6Mw<&;$V4#D1{= literal 0 HcmV?d00001 diff --git a/Resources/Textures/_White/Objects/Weapons/Melee/Daggers/betrayal_knife.rsi/meta.json b/Resources/Textures/_White/Objects/Weapons/Melee/Daggers/betrayal_knife.rsi/meta.json new file mode 100644 index 0000000000..0b2fd88c33 --- /dev/null +++ b/Resources/Textures/_White/Objects/Weapons/Melee/Daggers/betrayal_knife.rsi/meta.json @@ -0,0 +1,22 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from tgstation at https://github.com/tgstation/tgstation/pull/49264/commits/d0dffe7ca643db2624424fdcebf45863f85c0448", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "icon" + }, + { + "name": "inhand-left", + "directions": 4 + }, + { + "name": "inhand-right", + "directions": 4 + } + ] +} From 959b3d2dc9552d0120815d79c9c7dfb498544f14 Mon Sep 17 00:00:00 2001 From: WWDP Changelogs Date: Tue, 27 Aug 2024 10:01:43 +0000 Subject: [PATCH 12/14] Automatic Changelog Update (#19) --- Resources/Changelog/Changelog.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 472ea981d9..dd1ae1e923 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -5433,3 +5433,9 @@ Entries: теперь корректно спавнятся со своим снаряжением. id: 6275 time: '2024-08-24T04:45:51.0000000+00:00' +- author: Spatison + changes: + - type: Add + message: added BlinkSystem / Добавлена система телепортации + id: 6276 + time: '2024-08-27T10:01:15.0000000+00:00' From b3d7b955889df1e742d213991d5bbc906c53f7e4 Mon Sep 17 00:00:00 2001 From: Spatison <137375981+Spatison@users.noreply.github.com> Date: Tue, 27 Aug 2024 13:55:17 +0300 Subject: [PATCH 13/14] =?UTF-8?q?[Port]=20=20Discounts=20In=20Uplink=20/?= =?UTF-8?q?=20=D0=A1=D0=BA=D0=B8=D0=B4=D0=BA=D0=B8=20=D0=92=20=D0=90=D0=BF?= =?UTF-8?q?=D0=BB=D0=B8=D0=BD=D0=BA=D0=B5=20(#11)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add: uplink discount * test * tweak --- Content.Client/Store/Ui/StoreMenu.xaml.cs | 4 ++ .../Store/Systems/StoreSystem.Ui.cs | 8 +++ Content.Server/Store/Systems/StoreSystem.cs | 6 ++- .../StoreDiscount/StoreDiscountSystem.cs | 53 +++++++++++++++++++ .../Store/ListingLocalisationHelpers.cs | 7 +++ Content.Shared/Store/ListingPrototype.cs | 22 ++++++++ Content.Shared/Store/StorePresetPrototype.cs | 6 +++ .../_White/StoreDiscount/SalesSpecifier.cs | 38 +++++++++++++ Resources/Locale/en-US/_white/store/sales.ftl | 2 + Resources/Locale/ru-RU/_white/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, 196 insertions(+), 2 deletions(-) create mode 100644 Content.Server/_White/StoreDiscount/StoreDiscountSystem.cs create mode 100644 Content.Shared/_White/StoreDiscount/SalesSpecifier.cs create mode 100644 Resources/Locale/en-US/_white/store/sales.ftl create mode 100644 Resources/Locale/ru-RU/_white/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 b7a2c285fe..145fc7261d 100644 --- a/Content.Client/Store/Ui/StoreMenu.xaml.cs +++ b/Content.Client/Store/Ui/StoreMenu.xaml.cs @@ -147,6 +147,10 @@ private void AddListingGui(ListingData listing) } var newListing = new StoreListingControl(listing, GetListingPriceString(listing), hasBalance, texture); + + if (listing.DiscountValue > 0) // WD EDIT + newListing.StoreItemBuyButton.AddStyleClass("ButtonColorRed"); + 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 25f64ba4b6..a7aac47434 100644 --- a/Content.Server/Store/Systems/StoreSystem.Ui.cs +++ b/Content.Server/Store/Systems/StoreSystem.Ui.cs @@ -265,6 +265,14 @@ 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! + //WD EDIT START + if (listing.SaleLimit != 0 && listing.DiscountValue > 0 && listing.PurchaseAmount >= listing.SaleLimit) + { + listing.DiscountValue = 0; + listing.Cost = listing.OldCost; + } + //WD EDIT END + UpdateUserInterface(buyer, uid, component); } diff --git a/Content.Server/Store/Systems/StoreSystem.cs b/Content.Server/Store/Systems/StoreSystem.cs index 72aeb29d19..39129e154a 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._White.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!; // WD EDIT 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); // WD EDIT + 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/_White/StoreDiscount/StoreDiscountSystem.cs b/Content.Server/_White/StoreDiscount/StoreDiscountSystem.cs new file mode 100644 index 0000000000..412af7b8ea --- /dev/null +++ b/Content.Server/_White/StoreDiscount/StoreDiscountSystem.cs @@ -0,0 +1,53 @@ +using System.Linq; +using Content.Shared.FixedPoint; +using Content.Shared.Store; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; + +namespace Content.Server._White.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 882300109c..9be37b533f 100644 --- a/Content.Shared/Store/ListingLocalisationHelpers.cs +++ b/Content.Shared/Store/ListingLocalisationHelpers.cs @@ -18,6 +18,13 @@ public static string GetLocalisedNameOrEntityName(ListingData listingData, IProt else if (listingData.ProductEntity != null) name = prototypeManager.Index(listingData.ProductEntity.Value).Name; + // WD START + 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"); + // WD END + return name; } diff --git a/Content.Shared/Store/ListingPrototype.cs b/Content.Shared/Store/ListingPrototype.cs index d3d2e13cdf..f39958dc15 100644 --- a/Content.Shared/Store/ListingPrototype.cs +++ b/Content.Shared/Store/ListingPrototype.cs @@ -109,6 +109,21 @@ public partial class ListingData : IEquatable, ICloneable [DataField] public TimeSpan RestockTime = TimeSpan.Zero; + // WD START + [DataField] + public int SaleLimit = 3; + + [DataField] + public bool SaleBlacklist; + + public int DiscountValue; + + public Dictionary, FixedPoint2> OldCost = new(); + + [DataField] + public List Components = new(); + // WD END + public bool Equals(ListingData? listing) { if (listing == null) @@ -166,6 +181,13 @@ public object Clone() ProductEvent = ProductEvent, PurchaseAmount = PurchaseAmount, RestockTime = RestockTime, + // WD START + SaleLimit = SaleLimit, + SaleBlacklist = SaleBlacklist, + DiscountValue = DiscountValue, + OldCost = OldCost, + Components = Components, + // WD END }; } } diff --git a/Content.Shared/Store/StorePresetPrototype.cs b/Content.Shared/Store/StorePresetPrototype.cs index ce7f0312b6..ac3d439f46 100644 --- a/Content.Shared/Store/StorePresetPrototype.cs +++ b/Content.Shared/Store/StorePresetPrototype.cs @@ -1,3 +1,4 @@ +using Content.Shared._White.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,9 @@ public sealed partial class StorePresetPrototype : IPrototype /// [DataField("currencyWhitelist", customTypeSerializer: typeof(PrototypeIdHashSetSerializer))] public HashSet CurrencyWhitelist { get; private set; } = new(); + + // WD EDIT START + [DataField] + public SalesSpecifier Sales { get; private set; } = new(); + // WD EDIT END } diff --git a/Content.Shared/_White/StoreDiscount/SalesSpecifier.cs b/Content.Shared/_White/StoreDiscount/SalesSpecifier.cs new file mode 100644 index 0000000000..31131dc85f --- /dev/null +++ b/Content.Shared/_White/StoreDiscount/SalesSpecifier.cs @@ -0,0 +1,38 @@ +namespace Content.Shared._White.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/_white/store/sales.ftl b/Resources/Locale/en-US/_white/store/sales.ftl new file mode 100644 index 0000000000..018006b3bf --- /dev/null +++ b/Resources/Locale/en-US/_white/store/sales.ftl @@ -0,0 +1,2 @@ +store-sales-amount = [DISCOUNT] { $amount }%! +store-sales-over = [The sale is over] \ No newline at end of file diff --git a/Resources/Locale/ru-RU/_white/store/sales.ftl b/Resources/Locale/ru-RU/_white/store/sales.ftl new file mode 100644 index 0000000000..1f0e3b83d3 --- /dev/null +++ b/Resources/Locale/ru-RU/_white/store/sales.ftl @@ -0,0 +1,2 @@ +store-sales-amount = [СКИДКА] { $amount }%! +store-sales-over = [Скидка закончилась] \ No newline at end of file diff --git a/Resources/Prototypes/Catalog/uplink_catalog.yml b/Resources/Prototypes/Catalog/uplink_catalog.yml index a1a60e3fef..94a5cc94a6 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 # WD EDIT - 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 # WD EDIT # Inbuilt suppressor so it's sneaky + more expensive. - type: listing @@ -31,6 +33,7 @@ Telecrystal: 4 categories: - UplinkWeapons + saleLimit: 1 # WD EDIT # Poor accuracy, slow to fire, cheap option - type: listing @@ -42,6 +45,7 @@ Telecrystal: 1 categories: - UplinkWeapons + saleLimit: 1 # WD EDIT - type: listing id: UplinkEsword @@ -53,6 +57,7 @@ Telecrystal: 8 categories: - UplinkWeapons + saleLimit: 2 # WD EDIT - type: listing id: UplinkEnergyDagger @@ -64,6 +69,7 @@ Telecrystal: 2 categories: - UplinkWeapons + saleLimit: 1 # WD EDIT - type: listing id: UplinkThrowingKnivesKit @@ -85,6 +91,7 @@ Telecrystal: 8 categories: - UplinkWeapons + saleLimit: 1 # WD EDIT - type: listing id: UplinkDisposableTurret @@ -100,6 +107,7 @@ blacklist: tags: - NukeOpsUplink + saleLimit: 2 # WD EDIT - type: listing id: BaseBallBatHomeRun @@ -112,6 +120,7 @@ Telecrystal: 16 categories: - UplinkWeapons + saleLimit: 1 # WD EDIT # Explosives @@ -214,6 +223,7 @@ whitelist: tags: - NukeOpsUplink + saleLimit: 1 # WD EDIT - type: listing id: UplinkC4Bundle @@ -224,6 +234,7 @@ Telecrystal: 12 #you're buying bulk so its a 25% discount categories: - UplinkExplosives + saleLimit: 1 # WD EDIT - type: listing id: UplinkEmpGrenade @@ -261,6 +272,7 @@ blacklist: tags: - NukeOpsUplink + saleLimit: 1 # WD EDIT - type: listing id: UplinkSyndicateBombNukie @@ -276,6 +288,7 @@ whitelist: tags: - NukeOpsUplink + saleLimit: 1 # WD EDIT - type: listing id: UplinkClusterGrenade @@ -451,6 +464,7 @@ blacklist: tags: - NukeOpsUplink + saleLimit: 1 # WD EDIT - type: listing id: UplinkReinforcementRadioSyndicateNukeops # Version for Nukeops that spawns an agent with the NukeOperative component. @@ -467,6 +481,7 @@ whitelist: tags: - NukeOpsUplink + saleLimit: 1 # WD EDIT - type: listing id: UplinkReinforcementRadioSyndicateCyborgAssault @@ -483,6 +498,7 @@ whitelist: tags: - NukeOpsUplink + saleLimit: 1 # WD EDIT - type: listing id: UplinkReinforcementRadioSyndicateMonkey @@ -515,6 +531,7 @@ whitelist: tags: - NukeOpsUplink + saleLimit: 1 # WD EDIT - type: listing id: UplinkStealthBox @@ -545,7 +562,6 @@ productEntity: EncryptionKeyBinary cost: Telecrystal: 1 - categories: - UplinkUtility @@ -748,6 +764,7 @@ blacklist: tags: - NukeOpsUplink + saleBlacklist: true # WD EDIT - type: listing id: UplinkDeathRattle @@ -821,6 +838,7 @@ blacklist: components: - SurplusBundle + saleLimit: 1 # WD EDIT - type: listing id: UplinkChemistryKitBundle @@ -862,6 +880,7 @@ blacklist: components: - SurplusBundle + saleLimit: 1 # WD EDIT - type: listing id: UplinkSniperBundle @@ -873,6 +892,7 @@ Telecrystal: 12 categories: - UplinkBundles + saleLimit: 1 # WD EDIT - type: listing id: UplinkC20RBundle @@ -884,6 +904,7 @@ Telecrystal: 17 categories: - UplinkBundles + saleLimit: 1 # WD EDIT - type: listing id: UplinkBulldogBundle @@ -895,6 +916,7 @@ Telecrystal: 20 categories: - UplinkBundles + saleLimit: 1 # WD EDIT - type: listing id: UplinkGrenadeLauncherBundle @@ -906,6 +928,7 @@ Telecrystal: 25 categories: - UplinkBundles + saleLimit: 1 # WD EDIT - type: listing id: UplinkL6SawBundle @@ -917,6 +940,7 @@ Telecrystal: 30 categories: - UplinkBundles + saleLimit: 1 # WD EDIT - type: listing id: UplinkZombieBundle @@ -937,6 +961,7 @@ blacklist: components: - SurplusBundle + saleLimit: 1 # WD EDIT - type: listing id: UplinkSurplusBundle @@ -956,6 +981,7 @@ blacklist: components: - SurplusBundle + saleBlacklist: true # WD EDIT - type: listing id: UplinkSuperSurplusBundle @@ -975,6 +1001,7 @@ blacklist: components: - SurplusBundle + saleBlacklist: true # WD EDIT # Tools @@ -1113,6 +1140,7 @@ - !type:BuyerJobCondition whitelist: - Chaplain + saleLimit: 1 # WD EDIT - type: listing id: uplinkRevolverCapGunFake @@ -1128,6 +1156,7 @@ whitelist: - Mime - Clown + saleLimit: 1 # WD EDIT - type: listing id: uplinkBananaPeelExplosive @@ -1172,6 +1201,7 @@ - !type:BuyerJobCondition whitelist: - Clown + saleLimit: 1 # WD EDIT - type: listing id: uplinkHotPotato @@ -1335,6 +1365,7 @@ Telecrystal: 8 categories: - UplinkArmor + saleLimit: 1 # WD EDIT - type: listing id: UplinkHardsuitSyndieElite @@ -1346,6 +1377,7 @@ Telecrystal: 10 categories: - UplinkArmor + saleLimit: 1 # WD EDIT - type: listing id: UplinkClothingOuterHardsuitJuggernaut @@ -1357,6 +1389,7 @@ Telecrystal: 12 categories: - UplinkArmor + saleLimit: 1 # WD EDIT # Misc @@ -1425,6 +1458,7 @@ whitelist: tags: - NukeOpsUplink + saleLimit: 1 # WD EDIT - type: listing id: UplinkSoapSyndie @@ -1515,6 +1549,7 @@ Telecrystal: 12 categories: - UplinkMisc + saleLimit: 1 # WD EDIT - type: listing id: UplinkBribe @@ -1541,6 +1576,7 @@ whitelist: tags: - NukeOpsUplink + saleLimit: 1 # WD EDIT - type: listing id: UplinkBackpackSyndicate diff --git a/Resources/Prototypes/Store/presets.yml b/Resources/Prototypes/Store/presets.yml index e623f4c8cd..43b457a393 100644 --- a/Resources/Prototypes/Store/presets.yml +++ b/Resources/Prototypes/Store/presets.yml @@ -13,5 +13,13 @@ - UplinkJob - UplinkArmor - UplinkPointless + - UplinkSales # WD EDIT currencyWhitelist: - Telecrystal + sales: # WD EDIT + 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 0000000000..90755f4f2f --- /dev/null +++ b/Resources/Prototypes/_White/Store/categories.yml @@ -0,0 +1,4 @@ +- type: storeCategory + id: UplinkSales + name: Sales! + priority: 10 From 419edd6e93c20d472fa9a4279f2985fba335622e Mon Sep 17 00:00:00 2001 From: WWDP Changelogs Date: Tue, 27 Aug 2024 10:55:39 +0000 Subject: [PATCH 14/14] Automatic Changelog Update (#11) --- Resources/Changelog/Changelog.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index dd1ae1e923..1be47047d0 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -5439,3 +5439,9 @@ Entries: message: added BlinkSystem / Добавлена система телепортации id: 6276 time: '2024-08-27T10:01:15.0000000+00:00' +- author: Spatison + changes: + - type: Add + message: Added discounts in uplink / Добавлены скидки в аплинк + id: 6277 + time: '2024-08-27T10:55:17.0000000+00:00'