From ff26505b11b088adc1bcd86e57aa35750f2f8023 Mon Sep 17 00:00:00 2001 From: Ogunefu <115596981+ogunefu@users.noreply.github.com> Date: Sat, 3 Feb 2024 20:31:56 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9C=D0=BE=D0=B7=D0=B3=D0=BE=D0=B2=D0=BE?= =?UTF-8?q?=D0=B9=20=D0=A7=D0=B5=D1=80=D0=B2=D1=8C=20(#17)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * - add: Added Cortic Borer. * - fix: Removed unnecessary imports, unused fields, variables, methods. * - fix: Изменён принцип вселения: теперь не создаётся новый энтити с переходом разума, вместо этого хост хранит в себе контейнер для червя, в который последний и погружается * - fix: Убрано использование устаревших методов и полей, исправлена ошибка, из-за которой при вселении в носителя уровень сахара не проверялся * - fix: Изменено тестировочное значение добавления очков химикатов * - fix: Borer can't speak now * - fix: Some bug and shitcode fixes * - fix: Some bug and shitcode fixes * - fix: Added cooldown after releasing the humanoid's body * - fix: fix * - add: Added russian localization * - add: Убрал использование метода _chatManager.ChatMessageToOne в некоторых местах, т.к. popup включает в себя вывод сообщения в чат. * - fix: fix * - fix: fix --- Content.Client/Borer/BorerOverlay.cs | 55 ++ .../Borer/BorerScannerUIController.cs | 60 ++ Content.Client/Borer/ClientBorerSystem.cs | 31 + Content.Client/Borer/ReagentUIController.cs | 110 ++++ Content.Client/Borer/ReagentWindow.cs | 15 + Content.Client/Borer/ReagentWindow.xaml | 13 + Content.Client/Borer/ScannerWindow.cs | 16 + Content.Client/Borer/ScannerWindow.xaml | 17 + Content.Server/Borer/ServerBorerHostSystem.cs | 28 + Content.Server/Borer/ServerBorerSystem.cs | 541 ++++++++++++++++++ .../Borer/Components/BorerComponent.cs | 23 + .../Borer/Components/BorerHostComponent.cs | 14 + .../Components/InfestedBorerComponent.cs | 79 +++ .../Events/BorerBrainResistAfterEvent.cs | 10 + .../Borer/Events/BorerBrainTakeAfterEvent.cs | 10 + .../Borer/Events/BorerInfestDoAfterEvent.cs | 10 + .../Borer/Events/BorerOverlayResponceEvent.cs | 9 + .../Borer/Events/BorerReproduceAfterEvent.cs | 10 + .../Borer/Events/BorerScanDoAfterEvent.cs | 16 + Content.Shared/Borer/SharedBorerSystem.cs | 176 ++++++ Resources/Locale/en-US/borer/borer.ftl | 52 ++ Resources/Locale/ru-RU/borer/borer.ftl | 52 ++ Resources/Prototypes/Actions/borer.yml | 133 +++++ .../Prototypes/Entities/Mobs/NPCs/borer.yml | 72 +++ Resources/Prototypes/GameRules/events.yml | 19 + .../Textures/Interface/Borer/chem_bg.png | Bin 0 -> 1337 bytes .../Animals/borer.rsi/action_brainrelease.png | Bin 0 -> 1596 bytes .../Animals/borer.rsi/action_brainspeech.png | Bin 0 -> 1882 bytes .../Animals/borer.rsi/action_braintake.png | Bin 0 -> 1641 bytes .../Mobs/Animals/borer.rsi/action_infest.png | Bin 0 -> 1504 bytes .../Mobs/Animals/borer.rsi/action_inject.png | Bin 0 -> 1266 bytes .../Mobs/Animals/borer.rsi/action_out.png | Bin 0 -> 2381 bytes .../Animals/borer.rsi/action_reproduce.png | Bin 0 -> 1597 bytes .../Animals/borer.rsi/action_scanreagents.png | Bin 0 -> 1327 bytes .../Mobs/Animals/borer.rsi/action_stun.png | Bin 0 -> 1416 bytes .../Textures/Mobs/Animals/borer.rsi/borer.png | Bin 0 -> 1381 bytes .../Textures/Mobs/Animals/borer.rsi/dead.png | Bin 0 -> 524 bytes .../Textures/Mobs/Animals/borer.rsi/meta.json | 55 ++ global.json | 7 +- 39 files changed, 1630 insertions(+), 3 deletions(-) create mode 100644 Content.Client/Borer/BorerOverlay.cs create mode 100644 Content.Client/Borer/BorerScannerUIController.cs create mode 100644 Content.Client/Borer/ClientBorerSystem.cs create mode 100644 Content.Client/Borer/ReagentUIController.cs create mode 100644 Content.Client/Borer/ReagentWindow.cs create mode 100644 Content.Client/Borer/ReagentWindow.xaml create mode 100644 Content.Client/Borer/ScannerWindow.cs create mode 100644 Content.Client/Borer/ScannerWindow.xaml create mode 100644 Content.Server/Borer/ServerBorerHostSystem.cs create mode 100644 Content.Server/Borer/ServerBorerSystem.cs create mode 100644 Content.Shared/Borer/Components/BorerComponent.cs create mode 100644 Content.Shared/Borer/Components/BorerHostComponent.cs create mode 100644 Content.Shared/Borer/Components/InfestedBorerComponent.cs create mode 100644 Content.Shared/Borer/Events/BorerBrainResistAfterEvent.cs create mode 100644 Content.Shared/Borer/Events/BorerBrainTakeAfterEvent.cs create mode 100644 Content.Shared/Borer/Events/BorerInfestDoAfterEvent.cs create mode 100644 Content.Shared/Borer/Events/BorerOverlayResponceEvent.cs create mode 100644 Content.Shared/Borer/Events/BorerReproduceAfterEvent.cs create mode 100644 Content.Shared/Borer/Events/BorerScanDoAfterEvent.cs create mode 100644 Content.Shared/Borer/SharedBorerSystem.cs create mode 100644 Resources/Locale/en-US/borer/borer.ftl create mode 100644 Resources/Locale/ru-RU/borer/borer.ftl create mode 100644 Resources/Prototypes/Actions/borer.yml create mode 100644 Resources/Prototypes/Entities/Mobs/NPCs/borer.yml create mode 100644 Resources/Textures/Interface/Borer/chem_bg.png create mode 100644 Resources/Textures/Mobs/Animals/borer.rsi/action_brainrelease.png create mode 100644 Resources/Textures/Mobs/Animals/borer.rsi/action_brainspeech.png create mode 100644 Resources/Textures/Mobs/Animals/borer.rsi/action_braintake.png create mode 100644 Resources/Textures/Mobs/Animals/borer.rsi/action_infest.png create mode 100644 Resources/Textures/Mobs/Animals/borer.rsi/action_inject.png create mode 100644 Resources/Textures/Mobs/Animals/borer.rsi/action_out.png create mode 100644 Resources/Textures/Mobs/Animals/borer.rsi/action_reproduce.png create mode 100644 Resources/Textures/Mobs/Animals/borer.rsi/action_scanreagents.png create mode 100644 Resources/Textures/Mobs/Animals/borer.rsi/action_stun.png create mode 100644 Resources/Textures/Mobs/Animals/borer.rsi/borer.png create mode 100644 Resources/Textures/Mobs/Animals/borer.rsi/dead.png create mode 100644 Resources/Textures/Mobs/Animals/borer.rsi/meta.json diff --git a/Content.Client/Borer/BorerOverlay.cs b/Content.Client/Borer/BorerOverlay.cs new file mode 100644 index 0000000000..a75f58d90a --- /dev/null +++ b/Content.Client/Borer/BorerOverlay.cs @@ -0,0 +1,55 @@ +using System.Numerics; +using Content.Shared.Borer; +using Robust.Client.Graphics; +using Robust.Client.Player; +using Robust.Client.ResourceManagement; +using Robust.Shared.Enums; + +namespace Content.Client.Borer; + +public sealed class BorerOverlay : Overlay +{ + public override OverlaySpace Space => OverlaySpace.ScreenSpace; + [Dependency] private readonly IResourceCache _client = default!; + private IPlayerManager _playerManager; + private IEntityManager _entManager; + private Font _font; + private Texture _chemBackground; + + int points; + float X, Y; + + public BorerOverlay(IEntityManager entManager, IPlayerManager playerManager, IResourceCache client) + { + _entManager = entManager; + _playerManager = playerManager; + _client = client; + _font = new VectorFont(_client.GetResource("/Fonts/Boxfont-round/Boxfont Round.ttf"), 30); + _chemBackground = _client.GetResource("/Textures/Interface/Borer/chem_bg.png"); + } + protected override void Draw(in OverlayDrawArgs args) + { + var localPlayer = _playerManager.LocalEntity; + if (_entManager.TryGetComponent(localPlayer, out BorerComponent? borComp)) + points = borComp.Points; + else if (_entManager.TryGetComponent(localPlayer, out InfestedBorerComponent? infestedComp)) + { + if (infestedComp.ControllingBrain) + return; + points = infestedComp.Points; + } + else + return; + + if (args.ViewportControl != null) + { + X = (args.ViewportControl.Window!.Size.X / 2f) - 32; + Y = args.ViewportControl.Window!.Size.Y - 130f; + } + + var screenHandle = args.ScreenHandle; + + screenHandle.DrawTextureRect(_chemBackground, new UIBox2(new Vector2(X,Y), new Vector2(X+128f,Y+128f))); + screenHandle.DrawString(_font, new Vector2(X+18, Y+42), points.ToString(), new Color(30, 200, 30)); + } +} diff --git a/Content.Client/Borer/BorerScannerUIController.cs b/Content.Client/Borer/BorerScannerUIController.cs new file mode 100644 index 0000000000..13800e44ac --- /dev/null +++ b/Content.Client/Borer/BorerScannerUIController.cs @@ -0,0 +1,60 @@ +using Content.Client.UserInterface.Systems.Gameplay; +using Content.Shared.Borer; +using Robust.Client.Player; +using Robust.Client.UserInterface.Controllers; +using Robust.Client.UserInterface.Controls; +using Robust.Shared.Utility; + +namespace Content.Client.Borer; + + +public sealed class BorerScannerUIController : UIController +{ + [Dependency] private readonly GameplayStateLoadController _gameplayStateLoad = default!; + [Dependency] private readonly IPlayerManager _playerManager = default!; + + private ScannerWindow? _window; + + public override void Initialize() + { + base.Initialize(); + + _gameplayStateLoad.OnScreenLoad += LoadGui; + _gameplayStateLoad.OnScreenUnload += UnloadGui; + + SubscribeNetworkEvent(OpenWindow); + + } + + private void LoadGui() + { + DebugTools.Assert(_window == null); + _window = UIManager.CreateWindow(); + LayoutContainer.SetAnchorPreset(_window, LayoutContainer.LayoutPreset.CenterTop); + } + + private void UnloadGui() + { + if (_window != null) + { + _window.Dispose(); + _window = null; + } + } + private void OpenWindow(BorerScanDoAfterEvent msg, EntitySessionEventArgs args) + { + var ent = _playerManager.LocalEntity; + if (_window == null || _window.IsOpen || ent != args.SenderSession.AttachedEntity) + return; + _window.SolutionContainer.DisposeAllChildren(); + foreach (var reagent in msg.Solution) + { + var reagLabel = new Label(); + reagLabel.Text = reagent.Key + " - " + reagent.Value + "u"; + _window.SolutionContainer.Children.Add(reagLabel); + } + + _window.Open(); + } + +} diff --git a/Content.Client/Borer/ClientBorerSystem.cs b/Content.Client/Borer/ClientBorerSystem.cs new file mode 100644 index 0000000000..7cdf8b9ffd --- /dev/null +++ b/Content.Client/Borer/ClientBorerSystem.cs @@ -0,0 +1,31 @@ +using Content.Shared.Borer; +using Robust.Client.Graphics; +using Robust.Client.Player; +using Robust.Client.ResourceManagement; + +namespace Content.Client.Borer; + +/// +/// This handles... +/// +public sealed class ClientBorerSystem : EntitySystem +{ + [Dependency] private readonly IResourceCache _client = default!; + [Dependency] private readonly IEntityManager _entManager = default!; + [Dependency] private readonly IOverlayManager _overlayManager = default!; + [Dependency] private readonly IPlayerManager _playerMgr = default!; + + public override void Initialize() + { + SubscribeNetworkEvent(OnOverlayResponce); + } + + private void OnOverlayResponce(BorerOverlayResponceEvent ev) + { + if(!_overlayManager.HasOverlay()) + _overlayManager.AddOverlay(new BorerOverlay( + _entManager, + _playerMgr, + _client)); + } +} diff --git a/Content.Client/Borer/ReagentUIController.cs b/Content.Client/Borer/ReagentUIController.cs new file mode 100644 index 0000000000..b22b3346c2 --- /dev/null +++ b/Content.Client/Borer/ReagentUIController.cs @@ -0,0 +1,110 @@ +using Content.Client.Actions; +using Content.Client.Gameplay; +using Content.Client.UserInterface.Systems.Actions; +using Content.Client.UserInterface.Systems.Gameplay; +using Content.Shared.Actions; +using Content.Shared.Borer; +using Content.Shared.Input; +using Robust.Client.Player; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controllers; +using Robust.Client.UserInterface.Controls; +using Robust.Shared.Input.Binding; +using Robust.Shared.Utility; + +namespace Content.Client.Borer; + + +public sealed class ReagentUIController : UIController, IOnSystemChanged, IOnStateEntered +{ + [Dependency] private readonly GameplayStateLoadController _gameplayStateLoad = default!; + [UISystemDependency] private readonly SharedBorerSystem _borerSystem = default!; + [Dependency] private readonly IPlayerManager _playerManager = default!; + + private ReagentWindow? _window; + + private bool _reagentsLoaded = false; + + public override void Initialize() + { + base.Initialize(); + + + _gameplayStateLoad.OnScreenLoad += LoadGui; + _gameplayStateLoad.OnScreenUnload += UnloadGui; + + SubscribeLocalEvent(ev => + { + OpenWindow(); + }); + } + + private void LoadGui() + { + DebugTools.Assert(_window == null); + _window = UIManager.CreateWindow(); + LayoutContainer.SetAnchorPreset(_window, LayoutContainer.LayoutPreset.CenterTop); + } + + private void UnloadGui() + { + if (_window != null) + { + _window.Dispose(); + _window = null; + } + } + + private void OnInjectReagent(string protoId, int cost) + { + _borerSystem.RaiseInjectEvent(protoId, cost); + } + private void OpenWindow() + { + var ent = _playerManager.LocalEntity; + if (_window == null || _window.IsOpen || !ent.HasValue) + return; + if (!_reagentsLoaded) + { + foreach (var reagent in _borerSystem.GetReagents(ent.Value)) + { + var button = new Button(); + button.Text = Loc.GetString("borer-ui-secrete-inject-label", + ("reagent",Loc.GetString("reagent-name-"+ + reagent.Key.ToLower().Replace("spacedrugs", "space-drugs"))), + ("cost", reagent.Value)); + button.OnPressed += _ => OnInjectReagent(reagent.Key, reagent.Value); + _window.MainContainer.AddChild(button); + } + _reagentsLoaded = true; + } + + _window.Open(); + } + + public void OnSystemLoaded(ActionsSystem system) + { + system.LinkActions += OnComponentLinked; + } + + public void OnSystemUnloaded(ActionsSystem system) + { + system.LinkActions -= OnComponentLinked; + } + + private void OnComponentLinked(ActionsComponent component) + { + } + + public void OnStateEntered(GameplayState state) + { + CommandBinds.Builder + .Bind(ContentKeyFunctions.OpenActionsMenu, InputCmdHandler.FromDelegate(_ => OpenWindow())) + .Register(); + } + + public void OnStateExited(GameplayState state) + { + CommandBinds.Unregister(); + } +} diff --git a/Content.Client/Borer/ReagentWindow.cs b/Content.Client/Borer/ReagentWindow.cs new file mode 100644 index 0000000000..66003eeeeb --- /dev/null +++ b/Content.Client/Borer/ReagentWindow.cs @@ -0,0 +1,15 @@ +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface.CustomControls; +using Robust.Client.UserInterface.XAML; + +namespace Content.Client.Borer; + +[GenerateTypedNameReferences] +public sealed partial class ReagentWindow : DefaultWindow +{ + public ReagentWindow() + { + RobustXamlLoader.Load(this); + } +} + diff --git a/Content.Client/Borer/ReagentWindow.xaml b/Content.Client/Borer/ReagentWindow.xaml new file mode 100644 index 0000000000..627f5aa310 --- /dev/null +++ b/Content.Client/Borer/ReagentWindow.xaml @@ -0,0 +1,13 @@ + + + + diff --git a/Content.Client/Borer/ScannerWindow.cs b/Content.Client/Borer/ScannerWindow.cs new file mode 100644 index 0000000000..0b24e076d8 --- /dev/null +++ b/Content.Client/Borer/ScannerWindow.cs @@ -0,0 +1,16 @@ +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.CustomControls; +using Robust.Client.UserInterface.XAML; + +namespace Content.Client.Borer; + +[GenerateTypedNameReferences] +public sealed partial class ScannerWindow : DefaultWindow +{ + public ScannerWindow() + { + RobustXamlLoader.Load(this); + } +} + diff --git a/Content.Client/Borer/ScannerWindow.xaml b/Content.Client/Borer/ScannerWindow.xaml new file mode 100644 index 0000000000..bff33d894c --- /dev/null +++ b/Content.Client/Borer/ScannerWindow.xaml @@ -0,0 +1,17 @@ + + + + diff --git a/Content.Server/Borer/ServerBorerHostSystem.cs b/Content.Server/Borer/ServerBorerHostSystem.cs new file mode 100644 index 0000000000..166c964920 --- /dev/null +++ b/Content.Server/Borer/ServerBorerHostSystem.cs @@ -0,0 +1,28 @@ +using Content.Shared.Borer; +using Content.Shared.Mobs; +using Robust.Server.Containers; + +namespace Content.Server.Borer; + + +public sealed class ServerBorerHostSystem : EntitySystem +{ + [Dependency] private ServerBorerSystem _borerSystem = default!; + /// + public override void Initialize() + { + SubscribeLocalEvent(OnDamageChanged); + } + + private void OnDamageChanged(EntityUid uid, BorerHostComponent component, MobStateChangedEvent args) + { + if (args.NewMobState == MobState.Critical) + { + RaiseLocalEvent(uid, new BorerBrainReleaseEvent(), true); + } + else if (args.NewMobState == MobState.Dead) + { + _borerSystem.GetOut(component.BorerContainer.ContainedEntities[0]); + } + } +} diff --git a/Content.Server/Borer/ServerBorerSystem.cs b/Content.Server/Borer/ServerBorerSystem.cs new file mode 100644 index 0000000000..7eee27f477 --- /dev/null +++ b/Content.Server/Borer/ServerBorerSystem.cs @@ -0,0 +1,541 @@ +using Content.Server.Administration; +using Content.Server.Body.Components; +using Content.Server.Body.Systems; +using Content.Server.Chat.Managers; +using Content.Server.Chemistry.Containers.EntitySystems; +using Content.Server.Medical; +using Content.Server.Popups; +using Content.Server.Stunnable; +using Content.Shared.Actions; +using Content.Shared.Borer; +using Content.Shared.Changeling; +using Content.Shared.Chat; +using Content.Shared.Chemistry; +using Content.Shared.Chemistry.Components; +using Content.Shared.Chemistry.Reagent; +using Content.Shared.DoAfter; +using Content.Shared.FixedPoint; +using Content.Shared.Humanoid; +using Content.Shared.Mind; +using Content.Shared.Mobs; +using Content.Shared.Mobs.Components; +using Content.Shared.Movement.Systems; +using Content.Shared.Popups; +using Content.Shared.Silicons.Borgs.Components; +using Robust.Server.Containers; +using Robust.Shared.Containers; +using Robust.Shared.Physics; +using Robust.Shared.Physics.Systems; +using Robust.Shared.Player; +using Robust.Shared.Timing; + +namespace Content.Server.Borer; + +public sealed class ServerBorerSystem : EntitySystem +{ + [Dependency] private readonly SharedActionsSystem _action = default!; + + [Dependency] private readonly BloodstreamSystem _bloodstreamSystem = default!; + [Dependency] private readonly IChatManager _chatManager = default!; + [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!; + [Dependency] private readonly IEntityManager _entityManager = default!; + [Dependency] private readonly SharedMindSystem _mindSystem = default!; + [Dependency] private readonly PopupSystem _popup = default!; + + [Dependency] private readonly QuickDialogSystem _quickDialog = default!; + [Dependency] private readonly ReactiveSystem _reactiveSystem = default!; + [Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!; + + [Dependency] private readonly StunSystem _stuns = default!; + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly VomitSystem _vomitSystem = default!; + [Dependency] private readonly ContainerSystem _container = default!; + + [Dependency] private readonly SharedBorerSystem _sharedBorerSystem = default!; + + [Dependency] private readonly MetaDataSystem _metaData = default!; + + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnInfest); + SubscribeLocalEvent(OnInfestAfter); + + SubscribeLocalEvent(OnGetOut); + SubscribeLocalEvent(OnTelepathicSpeech); + + SubscribeNetworkEvent(OnInjectChemicals); + + SubscribeLocalEvent(OnBorerScan); + SubscribeLocalEvent(OnStunEvent); + + SubscribeLocalEvent(OnTakeControl); + SubscribeLocalEvent(OnTakeControlAfter); + + SubscribeLocalEvent(OnReleaseControl); + + SubscribeLocalEvent(OnReproduce); + SubscribeLocalEvent(OnReproduceAfter); + SubscribeLocalEvent(OnResistAfterControl); + } + + private void OnReproduceAfter(EntityUid uid, BorerHostComponent component, BorerReproduceAfterEvent args) + { + if (args.Cancelled || !TryComp(component.BorerContainer.ContainedEntities[0], out InfestedBorerComponent? borerComp) + || !WithrawPoints(component.BorerContainer.ContainedEntities[0], borerComp.ReproduceCost) + || !TryComp(uid, out TransformComponent? targetTransform)) + return; + args.Handled = true; + _vomitSystem.Vomit(uid, -30, -30); + _entityManager.SpawnEntity("MobSimpleBorer", targetTransform.Coordinates); + } + + private void OnReproduce(EntityUid uid, BorerHostComponent component, BorerReproduceEvent args) + { + if (!TryComp(component.BorerContainer.ContainedEntities[0], out InfestedBorerComponent? borerComp)) + return; + if (GetPoints(component.BorerContainer.ContainedEntities[0]) < borerComp.ReproduceCost) + { + _popup.PopupEntity(Loc.GetString("borer-popup-lowchem"), + uid, + uid, PopupType.LargeCaution); + return; + } + + args.Handled = true; + + _doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, + uid, + TimeSpan.FromSeconds(3), + new BorerReproduceAfterEvent(), uid) + { + Hidden = true + }); + } + + public void ReleaseControl(EntityUid borerUid) + { + if (!TryComp(borerUid, out ActionsComponent? wormActComp) || + !TryComp(borerUid, out InfestedBorerComponent? borComp) || + !borComp.Host.HasValue || + !borComp.ControllingBrain) + return; + + TryComp(borComp.Host.Value, out ActionsComponent? bodyActComp); + var wormHasMind = _mindSystem.TryGetMind(borerUid, out var hostMindId, out var hostMind); + var bodyHasMind = _mindSystem.TryGetMind(borComp.Host.Value, out var mindId, out var mind); + if (!bodyHasMind && !wormHasMind) + return; + + if (wormHasMind) + _mindSystem.TransferTo(hostMindId, borComp.Host.Value, mind: hostMind); + if (bodyHasMind) + _mindSystem.TransferTo(mindId, borerUid, mind: mind); + + + _sharedBorerSystem.AddInfestedAbilities(borerUid, borComp); + + _action.RemoveAction(borerUid, borComp.ActionBorerBrainResistEntity, wormActComp); + + _action.RemoveAction(borComp.Host.Value, borComp.ActionBorerBrainReleaseEntity, bodyActComp); + + _action.RemoveAction(borComp.Host.Value, borComp.ActionBorerReproduceEntity, bodyActComp); + + borComp.ControllingBrain = false; + Dirty(borerUid, borComp); + } + + private void OnReleaseControl(EntityUid uid, BorerHostComponent component, BorerBrainReleaseEvent args) + { + args.Handled = true; + ReleaseControl(component.BorerContainer.ContainedEntities[0]); + } + + private void OnResistAfterControl(EntityUid uid, InfestedBorerComponent component, BorerBrainResistAfterEvent args) + { + if (args.Cancelled) + return; + ReleaseControl(uid); + } + + private void OnTakeControl(EntityUid uid, InfestedBorerComponent component, BorerBrainTakeEvent args) + { + if (GetPoints(uid) < component.AssumeControlCost) + { + _popup.PopupEntity(Loc.GetString("borer-popup-lowchem"), + uid, + uid, PopupType.LargeCaution); + return; + } + + if (GetSugarQuantityInHost(uid) > 0) + { + _popup.PopupEntity(Loc.GetString("borer-popup-toomuchsugar"), uid, + uid, PopupType.LargeCaution); + } + + if (TryComp(component.Host, out MobStateComponent? state) && + state.CurrentState == MobState.Critical) + { + _popup.PopupEntity(Loc.GetString("borer-popup-braintake-critical"), uid, + uid, PopupType.LargeCaution); + return; + } + + args.Handled = true; + _doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, + uid, + TimeSpan.FromSeconds(30), + new BorerBrainTakeAfterEvent(), uid) + { + Hidden = true + }); + } + + private void OnTakeControlAfter(EntityUid uid, InfestedBorerComponent component, BorerBrainTakeAfterEvent args) + { + if (!TryComp(uid, out ActionsComponent? comp) || + !TryComp(component.Host, out ActionsComponent? hostComp)) + return; + + else if (TryComp(component.Host, out MobStateComponent? state) && + state.CurrentState == MobState.Critical) + { + _popup.PopupEntity(Loc.GetString("borer-popup-braintake-critical"), uid, + uid, PopupType.LargeCaution); + return; + } + else if (GetSugarQuantityInHost(uid) > 0) + { + _popup.PopupEntity(Loc.GetString("borer-popup-toomuchsugar"), uid, + uid, PopupType.LargeCaution); + } + else if (args.Cancelled || !WithrawPoints(uid, component.AssumeControlCost)) + return; + + var borHasMind = _mindSystem.TryGetMind(uid, out var mindId, out var mind); + var hostHasMind = _mindSystem.TryGetMind(component.Host.Value, out var hostMindId, out var hostMind); + + if (!borHasMind && !hostHasMind) + return; + if (borHasMind) + { + _mindSystem.TransferTo(mindId, component.Host, mind: mind); + _popup.PopupEntity(Loc.GetString("borer-popup-braintake-success"), component.Host.Value, + component.Host.Value, PopupType.Large); + + + } + + if (hostHasMind) + { + _mindSystem.TransferTo(hostMindId, uid, mind: hostMind); + _popup.PopupEntity(Loc.GetString("borer-popup-braintake-alert"), uid, uid, PopupType.LargeCaution); + if (EntityManager.TryGetComponent(uid, out ActorComponent? actor)) + { + _chatManager.ChatMessageToOne(ChatChannel.Local, + Loc.GetString("borer-message-braintake-alert"), + Loc.GetString("borer-message-braintake-alert"), + EntityUid.Invalid, false, actor.PlayerSession.Channel); + } + } + + _action.RemoveAction(uid, component.ActionBorerOutEntity, comp); + _action.RemoveAction(uid, component.ActionBorerScanEntity, comp); + _action.RemoveAction(uid, component.ActionBorerBrainTakeEntity, comp); + _action.RemoveAction(uid, component.ActionBorerInjectWindowOpenEntity, comp); + _action.RemoveAction(uid, component.ActionBorerReproduceEntity, comp); + + _action.AddAction(uid, ref component.ActionBorerBrainResistEntity, + component.ActionBorerBrainResist, component: comp); + + _action.AddAction(component.Host.Value, ref component.ActionBorerBrainReleaseEntity, + component.ActionBorerBrainRelease, component: hostComp); + _action.AddAction(component.Host.Value, ref component.ActionBorerReproduceEntity, + component.ActionBorerReproduce, component: hostComp); + + if(component.ActionBorerReproduceEntity.HasValue) + _metaData.SetEntityName(component.ActionBorerReproduceEntity.Value, + $"{Loc.GetString("borer-abilities-reproduce-name")} ([color=red]{component.ReproduceCost}c[/color])"); + + component.ControllingBrain = true; + Dirty(uid, component); + } + + private void OnStunEvent(EntityUid uid, BorerComponent component, BorerStunActionEvent args) + { + _stuns.TryParalyze(args.Target, TimeSpan.FromSeconds(5.7f), true); + args.Handled = true; + } + + private void OnInfest(EntityUid uid, BorerComponent component, BorerInfestActionEvent args) + { + if (!HasComp(args.Target) + || HasComp(args.Target) + || HasComp(args.Target)) + { + _popup.PopupEntity(Loc.GetString("borer-popup-infest-failed"), uid, uid); + return; + } + + if (TryComp(args.Target, out MobStateComponent? state) && + state.CurrentState == MobState.Dead) + return; + + if (HasComp(args.Target)) + { + _popup.PopupEntity(Loc.GetString("borer-popup-infest-occupied"), uid, uid); + args.Handled = true; + return; + } + + if (GetSugarQuantityInEntity(args.Target) > 10) + { + _popup.PopupEntity(Loc.GetString("borer-popup-infest-sugar"), uid, uid, PopupType.LargeCaution); + args.Handled = true; + return; + } + + StartInfest(uid, args.Target, component); + args.Handled = true; + } + + private void OnInfestAfter(EntityUid uid, BorerComponent component, BorerInfestDoAfterEvent args) + { + if (args.Cancelled) + return; + + if (!HasComp(args.Target)) + return; + if (TryComp(args.Target, out MobStateComponent? state) && + state.CurrentState == MobState.Dead) + return; + + if (HasComp(args.Target)) + { + _popup.PopupEntity(Loc.GetString("borer-popup-infest-occupied"), uid, uid); + args.Handled = true; + return; + } + + if (GetSugarQuantityInHost(uid) > 10) + { + _popup.PopupEntity(Loc.GetString("borer-popup-infest-sugar"), uid, uid, PopupType.LargeCaution); + args.Handled = true; + return; + } + + var hostComp = AddComp(args.Target.Value); + hostComp.BorerContainer = _container.EnsureContainer(args.Target.Value, "borerContainer"); + _container.Insert(uid, hostComp.BorerContainer); + + var infestedComponent = AddComp(uid); + infestedComponent.Host = args.Target; + infestedComponent.Points = component.Points; + Dirty(uid, infestedComponent); + + RemComp(uid); + } + + private void StartInfest(EntityUid user, EntityUid target, BorerComponent comp) + { + _doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, + user, + TimeSpan.FromSeconds(5), + new BorerInfestDoAfterEvent(), user, target) + { + BreakOnTargetMove = true, + BreakOnUserMove = true, + Hidden = true + }); + } + + private void OnBorerScan(EntityUid uid, InfestedBorerComponent component, BorerScanInstantActionEvent args) + { + if (!component.Host.HasValue) + return; + Dictionary solution = new(); + if (EntityManager.TryGetComponent(component.Host.Value, + out BloodstreamComponent? bloodContainer)) + { + if(_solutionContainerSystem.TryGetSolution(component.Host.Value, bloodContainer.ChemicalSolutionName, + out var sol)) + { + foreach (var reagent in sol.Value.Comp.Solution) + { + solution.Add(reagent.Reagent.ToString(), reagent.Quantity); + } + } + } + + RaiseNetworkEvent(new BorerScanDoAfterEvent(solution), uid); + args.Handled = true; + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var infestedQuery = EntityQueryEnumerator(); + while (infestedQuery.MoveNext(out var uid, out var comp) && comp.Host is not null) + { + if (comp.PointUpdateNext == TimeSpan.Zero) + { + comp.PointUpdateNext = _timing.CurTime + comp.PointUpdateRate; + continue; + } + + if (_timing.CurTime < comp.PointUpdateNext) + continue; + + if (GetSugarQuantityInHost(uid) > 30) + { + GetOut(uid); + _popup.PopupEntity(Loc.GetString("borer-popup-sugarleave"), uid, uid, PopupType.LargeCaution); + return; + } + + comp.PointUpdateNext += comp.PointUpdateRate; + AddPoints(uid, comp.PointUpdateValue); + Dirty(uid, comp); + } + } + + private void OnTelepathicSpeech(EntityUid uid, InfestedBorerComponent component, BorerBrainSpeechActionEvent args) + { + if (!EntityManager.TryGetComponent(uid, out ActorComponent? actor)) + return; + + _quickDialog.OpenDialog(actor.PlayerSession, Loc.GetString("borer-ui-converse-title"), + Loc.GetString("borer-ui-converse-message"), (string message) => + { + _popup.PopupEntity(message, uid, uid, PopupType.Medium); + + if (EntityManager.TryGetComponent(component.Host, out ActorComponent? hostActor)) + { + _popup.PopupEntity(message, component.Host.Value, component.Host.Value, + PopupType.Medium); + } + }); + args.Handled = true; + } + + private void OnInjectChemicals(BorerInjectActionEvent injectEvent, EntitySessionEventArgs eventArgs) + { + var borerEn = eventArgs.SenderSession.AttachedEntity; + if (EntityManager.TryGetComponent(borerEn, + out InfestedBorerComponent? infestedComponent) && + infestedComponent.Host.HasValue) + { + if (!WithrawPoints(borerEn.Value, injectEvent.Cost)) + return; + + var solution = new Solution(); + solution.AddReagent(injectEvent.ProtoId, 10); + _bloodstreamSystem.TryAddToChemicals(infestedComponent.Host.Value, solution); + _reactiveSystem.DoEntityReaction(infestedComponent.Host.Value, solution, ReactionMethod.Injection); + + _popup.PopupEntity(Loc.GetString("borer-popup-injected", ("reagent", Loc.GetString("reagent-name-"+ + injectEvent.ProtoId.ToLower().Replace("spacedrugs", "space-drugs")))), + borerEn.Value, borerEn.Value, PopupType.Medium); + } + } + + public bool AddPoints(EntityUid borerUid, int value) + { + if (!EntityManager.TryGetComponent(borerUid, + out InfestedBorerComponent? infestedComponent)) + return false; + + infestedComponent.Points += value; + Dirty(borerUid, infestedComponent); + RaiseNetworkEvent(new BorerPointsUpdateEvent()); + return true; + } + + public int GetPoints(EntityUid borerUid) + { + if (!EntityManager.TryGetComponent(borerUid, + out InfestedBorerComponent? infestedComponent)) + return 0; + + return infestedComponent.Points; + } + + public bool WithrawPoints(EntityUid borerUid, int value) + { + if (!EntityManager.TryGetComponent(borerUid, + out InfestedBorerComponent? infestedComponent) || infestedComponent.Points < value) + { + _popup.PopupEntity(Loc.GetString("borer-popup-lowchem"), + borerUid, + borerUid, PopupType.LargeCaution); + return false; + } + + infestedComponent.Points -= value; + Dirty(borerUid, infestedComponent); + RaiseNetworkEvent(new BorerPointsUpdateEvent()); + return true; + } + + private void OnGetOut(EntityUid uid, InfestedBorerComponent component, BorerOutActionEvent args) + { + GetOut(uid); + } + + public void GetOut(EntityUid uid) + { + if (!TryComp(uid, out InfestedBorerComponent? component) || + !TryComp(component.Host, out BorerHostComponent? hostComponent)) + return; + ReleaseControl(uid); + + _vomitSystem.Vomit(component.Host.Value, -20, -20); + _container.Remove(uid, hostComponent.BorerContainer); + RemComp(component.Host.Value); + + var borerComponent = AddComp(uid); + borerComponent.Points = component.Points; + Dirty(uid, borerComponent); + + RemComp(uid); + _action.SetCooldown(borerComponent.ActionStunEntity, _timing.CurTime, _timing.CurTime+TimeSpan.FromSeconds(20)); + _action.SetCooldown(borerComponent.ActionInfestEntity, _timing.CurTime, _timing.CurTime+TimeSpan.FromSeconds(20)); + } + + public int GetSugarQuantityInHost(EntityUid borerUid) + { + var sugarQuantity = 0; + if (EntityManager.TryGetComponent(borerUid, + out InfestedBorerComponent? component) && + component.Host.HasValue) + { + sugarQuantity = GetSugarQuantityInEntity(component.Host.Value); + } + + return sugarQuantity; + } + + public int GetSugarQuantityInEntity(EntityUid uid) + { + if (EntityManager.TryGetComponent(uid, + out BloodstreamComponent? bloodContainer)) + { + if(_solutionContainerSystem.TryGetSolution(uid, bloodContainer.ChemicalSolutionName, + out var sol)) + { + foreach (var reagent in sol.Value.Comp.Solution) + { + if (reagent.Reagent.ToString() == "Sugar") + { + return reagent.Quantity.Int(); + } + } + } + } + return 0; + } +} diff --git a/Content.Shared/Borer/Components/BorerComponent.cs b/Content.Shared/Borer/Components/BorerComponent.cs new file mode 100644 index 0000000000..2d6f1717ec --- /dev/null +++ b/Content.Shared/Borer/Components/BorerComponent.cs @@ -0,0 +1,23 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Borer; + +/// +/// This is used for... +/// +[RegisterComponent, NetworkedComponent] +[AutoGenerateComponentState] +public sealed partial class BorerComponent : Component +{ + public string ActionInfest = "ActionInfest"; + + public EntityUid? ActionInfestEntity; + + public string ActionStun = "ActionBorerStunVictim"; + + public EntityUid? ActionStunEntity; + + [AutoNetworkedField] + [ViewVariables(VVAccess.ReadOnly)] + public int Points = 0; +} diff --git a/Content.Shared/Borer/Components/BorerHostComponent.cs b/Content.Shared/Borer/Components/BorerHostComponent.cs new file mode 100644 index 0000000000..8b88e39071 --- /dev/null +++ b/Content.Shared/Borer/Components/BorerHostComponent.cs @@ -0,0 +1,14 @@ +using Robust.Shared.Containers; +using Robust.Shared.GameStates; + +namespace Content.Shared.Borer; + +/// +/// This is used for... +/// +[RegisterComponent, NetworkedComponent] +public sealed partial class BorerHostComponent : Component +{ + //public EntityUid Borer; + public Container BorerContainer; +} diff --git a/Content.Shared/Borer/Components/InfestedBorerComponent.cs b/Content.Shared/Borer/Components/InfestedBorerComponent.cs new file mode 100644 index 0000000000..6d0e2cb88d --- /dev/null +++ b/Content.Shared/Borer/Components/InfestedBorerComponent.cs @@ -0,0 +1,79 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Borer; + +/// +/// This is used for... +/// +[RegisterComponent, NetworkedComponent] +[AutoGenerateComponentState] +public sealed partial class InfestedBorerComponent : Component +{ + [DataField("reproduceCost")] + public int ReproduceCost = 100; + + [DataField("assumeControlCost")] + public int AssumeControlCost = 250; + + [AutoNetworkedField] + [ViewVariables(VVAccess.ReadOnly)] + public bool ControllingBrain = false; + + public TimeSpan PointUpdateNext = TimeSpan.Zero; + + public TimeSpan PointUpdateRate = TimeSpan.FromSeconds(2); + + public readonly int PointUpdateValue = 1; + + public string ActionBorerOut = "ActionBorerOut"; + + public EntityUid? ActionBorerOutEntity; + + public string ActionBorerBrainSpeech = "ActionBorerBrainSpeech"; + + public EntityUid? ActionBorerBrainSpeechEntity; + + public string ActionBorerInjectWindowOpen = "ActionBorerInjectWindowOpen"; + + public EntityUid? ActionBorerInjectWindowOpenEntity; + + public string ActionBorerScan = "ActionBorerScan"; + + public EntityUid? ActionBorerScanEntity; + + public string ActionBorerBrainTake = "ActionBorerBrainTake"; + + public EntityUid? ActionBorerBrainTakeEntity; + + public string ActionBorerBrainRelease = "ActionBorerBrainRelease"; + + public EntityUid? ActionBorerBrainReleaseEntity; + + public string ActionBorerBrainResist = "ActionBorerBrainResist"; + + public EntityUid? ActionBorerBrainResistEntity; + + public string ActionBorerReproduce = "ActionBorerReproduce"; + + public EntityUid? ActionBorerReproduceEntity; + + [AutoNetworkedField] + [ViewVariables(VVAccess.ReadOnly)] + public EntityUid? Host; + + [AutoNetworkedField] + [ViewVariables(VVAccess.ReadOnly)] + public int Points = 0; + + [ViewVariables(VVAccess.ReadOnly)] + public readonly Dictionary AvailableReagents = new() + { + { "Epinephrine", 30 }, + { "Bicaridine", 30 }, + { "Kelotane", 30 }, + { "Dylovene", 30 }, + { "Dexalin", 30 }, + { "SpaceDrugs", 75 }, + { "Leporazine", 75 } + }; +} diff --git a/Content.Shared/Borer/Events/BorerBrainResistAfterEvent.cs b/Content.Shared/Borer/Events/BorerBrainResistAfterEvent.cs new file mode 100644 index 0000000000..42ffb2b6fb --- /dev/null +++ b/Content.Shared/Borer/Events/BorerBrainResistAfterEvent.cs @@ -0,0 +1,10 @@ +using Content.Shared.DoAfter; +using Robust.Shared.Serialization; + +namespace Content.Shared.Borer; + +[Serializable, NetSerializable] +public sealed partial class BorerBrainResistAfterEvent : SimpleDoAfterEvent +{ + +} diff --git a/Content.Shared/Borer/Events/BorerBrainTakeAfterEvent.cs b/Content.Shared/Borer/Events/BorerBrainTakeAfterEvent.cs new file mode 100644 index 0000000000..312bde1252 --- /dev/null +++ b/Content.Shared/Borer/Events/BorerBrainTakeAfterEvent.cs @@ -0,0 +1,10 @@ +using Content.Shared.DoAfter; +using Robust.Shared.Serialization; + +namespace Content.Shared.Borer; + +[Serializable, NetSerializable] +public sealed partial class BorerBrainTakeAfterEvent : SimpleDoAfterEvent +{ + +} diff --git a/Content.Shared/Borer/Events/BorerInfestDoAfterEvent.cs b/Content.Shared/Borer/Events/BorerInfestDoAfterEvent.cs new file mode 100644 index 0000000000..1a62af9f0c --- /dev/null +++ b/Content.Shared/Borer/Events/BorerInfestDoAfterEvent.cs @@ -0,0 +1,10 @@ +using Content.Shared.DoAfter; +using Robust.Shared.Serialization; + +namespace Content.Shared.Borer; + +[Serializable, NetSerializable] +public sealed partial class BorerInfestDoAfterEvent : SimpleDoAfterEvent +{ + +} diff --git a/Content.Shared/Borer/Events/BorerOverlayResponceEvent.cs b/Content.Shared/Borer/Events/BorerOverlayResponceEvent.cs new file mode 100644 index 0000000000..9df49fcd81 --- /dev/null +++ b/Content.Shared/Borer/Events/BorerOverlayResponceEvent.cs @@ -0,0 +1,9 @@ +using Robust.Shared.Serialization; + +namespace Content.Shared.Borer; + +[Serializable, NetSerializable] +public sealed partial class BorerOverlayResponceEvent : EntityEventArgs +{ + +} diff --git a/Content.Shared/Borer/Events/BorerReproduceAfterEvent.cs b/Content.Shared/Borer/Events/BorerReproduceAfterEvent.cs new file mode 100644 index 0000000000..fac2902f01 --- /dev/null +++ b/Content.Shared/Borer/Events/BorerReproduceAfterEvent.cs @@ -0,0 +1,10 @@ +using Content.Shared.DoAfter; +using Robust.Shared.Serialization; + +namespace Content.Shared.Borer; + +[Serializable, NetSerializable] +public sealed partial class BorerReproduceAfterEvent : SimpleDoAfterEvent +{ + +} diff --git a/Content.Shared/Borer/Events/BorerScanDoAfterEvent.cs b/Content.Shared/Borer/Events/BorerScanDoAfterEvent.cs new file mode 100644 index 0000000000..d2389548ad --- /dev/null +++ b/Content.Shared/Borer/Events/BorerScanDoAfterEvent.cs @@ -0,0 +1,16 @@ +using Content.Shared.DoAfter; +using Content.Shared.FixedPoint; +using Robust.Shared.Serialization; + +namespace Content.Shared.Borer; + +[Serializable, NetSerializable] +public sealed partial class BorerScanDoAfterEvent : EntityEventArgs +{ + public Dictionary Solution; + + public BorerScanDoAfterEvent(Dictionary solution) + { + Solution = solution; + } +} diff --git a/Content.Shared/Borer/SharedBorerSystem.cs b/Content.Shared/Borer/SharedBorerSystem.cs new file mode 100644 index 0000000000..35549f59d4 --- /dev/null +++ b/Content.Shared/Borer/SharedBorerSystem.cs @@ -0,0 +1,176 @@ +using Content.Shared.Actions; +using Content.Shared.Alert; +using Content.Shared.DoAfter; +using Content.Shared.Examine; +using Content.Shared.Mind; +using Robust.Shared.Serialization; + +namespace Content.Shared.Borer; + +/// +/// This handles... +/// +public sealed class SharedBorerSystem : EntitySystem +{ + + [Dependency] private readonly SharedActionsSystem _action = default!; + [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!; + [Dependency] private readonly MetaDataSystem _metaData = default!; + /// + public override void Initialize() + { + SubscribeLocalEvent(OnStartup); + SubscribeLocalEvent(OnRemove); + + SubscribeLocalEvent(OnStartup); + SubscribeLocalEvent(OnRemove); + + SubscribeLocalEvent(OnExamineAttempt); + SubscribeLocalEvent(OnExamineAttempt); + + SubscribeLocalEvent(OnResistControl); + + } + + private void OnRemove(EntityUid uid, InfestedBorerComponent component, ComponentRemove args) + { + if (!TryComp(uid, out ActionsComponent? borerActComponent)) + return; + _action.RemoveAction(uid, component.ActionBorerOutEntity, borerActComponent); + _action.RemoveAction(uid, component.ActionBorerScanEntity, borerActComponent); + _action.RemoveAction(uid, component.ActionBorerBrainTakeEntity, borerActComponent); + _action.RemoveAction(uid, component.ActionBorerBrainSpeechEntity, borerActComponent); + _action.RemoveAction(uid, component.ActionBorerInjectWindowOpenEntity, borerActComponent); + } + + private void OnRemove(EntityUid uid, BorerComponent component, ComponentRemove args) + { + if (!TryComp(uid, out ActionsComponent? borerActComponent)) + return; + _action.RemoveAction(uid, component.ActionInfestEntity, borerActComponent); + _action.RemoveAction(uid, component.ActionStunEntity, borerActComponent); + } + + private void OnResistControl(EntityUid uid, InfestedBorerComponent component, BorerBrainResistEvent args) + { + args.Handled = true; + _doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, + uid, + TimeSpan.FromSeconds(30), + new BorerBrainResistAfterEvent(), uid) + { + Hidden = true + }); + } + + private void OnStartup(EntityUid uid, BorerComponent component, ComponentStartup args) + { + if (!TryComp(uid, out ActionsComponent? comp)) + return; + _action.AddAction(uid, ref component.ActionInfestEntity, component.ActionInfest, component: comp); + _action.AddAction(uid, ref component.ActionStunEntity, component.ActionStun, component: comp); + _metaData.SetEntityName(uid, Loc.GetString("borer-entity-name")); + _metaData.SetEntityDescription(uid, Loc.GetString("borer-entity-description")); + RaiseNetworkEvent(new BorerOverlayResponceEvent()); + } + + private void OnStartup(EntityUid uid, InfestedBorerComponent component, ComponentStartup args) + { + AddInfestedAbilities(uid, component); + } + private void OnExamineAttempt(EntityUid uid, InfestedBorerComponent component, ExamineAttemptEvent args) + { + args.Cancel(); + } + + private void OnExamineAttempt(EntityUid uid, BorerComponent component, ExamineAttemptEvent args) + { + args.Cancel(); + } + + public void RaiseInjectEvent(string protoId, int cost) + { + RaiseNetworkEvent(new BorerInjectActionEvent(protoId, cost)); + } + + public Dictionary GetReagents(EntityUid borerUid) + { + if (TryComp(borerUid, out InfestedBorerComponent? infestedComp)) + return infestedComp.AvailableReagents; + else + return new(); + } + + public bool AddInfestedAbilities(EntityUid uid, InfestedBorerComponent component) + { + if (!TryComp(uid, out ActionsComponent? comp)) + return false; + _action.AddAction(uid, ref component.ActionBorerOutEntity, component.ActionBorerOut, component: comp); + _action.AddAction(uid, ref component.ActionBorerBrainSpeechEntity, component.ActionBorerBrainSpeech, component: comp); + _action.AddAction(uid, ref component.ActionBorerInjectWindowOpenEntity, component.ActionBorerInjectWindowOpen, component: comp); + _action.AddAction(uid, ref component.ActionBorerScanEntity, component.ActionBorerScan, component: comp); + _action.AddAction(uid, ref component.ActionBorerBrainTakeEntity, component.ActionBorerBrainTake, component: comp); + if (component.ActionBorerBrainTakeEntity.HasValue) + { + _metaData.SetEntityName(component.ActionBorerBrainTakeEntity.Value, + $"{Loc.GetString("borer-abilities-control-name")} ([color=red]{component.AssumeControlCost}c[/color])"); + } + + return true; + } + + public int GetPoints(EntityUid borerUid) + { + if (TryComp(borerUid, out InfestedBorerComponent? infestedComp)) + return infestedComp.Points; + else return 0; + + } + + public EntityUid? GetHost(EntityUid borerUid) + { + if (TryComp(borerUid, out InfestedBorerComponent? infestedComp)) + return infestedComp.Host; + else + return null; + + } +} + +public sealed partial class BorerInfestActionEvent : EntityTargetActionEvent {} + +public sealed partial class BorerOutActionEvent : InstantActionEvent {} + +public sealed partial class BorerBrainSpeechActionEvent : InstantActionEvent {} + +public sealed partial class BorerInjectWindowOpenEvent : InstantActionEvent{} + +public sealed partial class BorerBrainTakeEvent : InstantActionEvent{} + +public sealed partial class BorerBrainReleaseEvent : InstantActionEvent{} + +public sealed partial class BorerBrainResistEvent : InstantActionEvent{} + +public sealed partial class BorerStunActionEvent : EntityTargetActionEvent{} + +public sealed partial class BorerReproduceEvent : InstantActionEvent { } + +[Serializable, NetSerializable] +public sealed partial class BorerPointsUpdateEvent : EntityEventArgs{} + +[Serializable, NetSerializable] +public sealed partial class BorerInjectActionEvent : EntityEventArgs +{ + public string ProtoId; + + public int Cost; + + public BorerInjectActionEvent(string protoId, int cost) + { + ProtoId = protoId; + Cost = cost; + } +} +public sealed partial class BorerScanInstantActionEvent : InstantActionEvent +{ +} diff --git a/Resources/Locale/en-US/borer/borer.ftl b/Resources/Locale/en-US/borer/borer.ftl new file mode 100644 index 0000000000..2032bf5cf1 --- /dev/null +++ b/Resources/Locale/en-US/borer/borer.ftl @@ -0,0 +1,52 @@ +ghost-role-information-borer-name = Cortical Borer +ghost-role-information-borer-description = We are Borer! + +borer-entity-name = Cortical Borer +borer-entity-description = It looks like it's making you lose your mind. + +borer-abilities-infest-name = Infest +borer-abilities-infest-description = Allows you to [color=red]bury[/color] yourself into a host +borer-abilities-paralyze-name = Paralyze Victim +borer-abilities-paralyze-description = Sending a [color=red]psychic lance[/color] straight to an unsuspecting victim. +borer-abilities-release-host-name = Release Host +borer-abilities-release-host-description = Starts to [color=red]leave[/color] the host +borer-abilities-converse-name = Converse with Host +borer-abilities-converse-description = [color=red]Talks[/color] to the host. Nobody can intercept this. Only the host. +borer-abilities-secrete-name = Secrete Chemicals +borer-abilities-secrete-description = [color=red]Injects[/color] different kinda of chemicals into the host, from meth to bicardine. +borer-abilities-scan-name = Chemical Scanning +borer-abilities-scan-description = [color=red]Scan the host's blood[/color] for the presence of reagents in it. +borer-abilities-control-name = Assume Control +borer-abilities-control-description = Allows you to [color=red]assume direct control[/color] of your host. +borer-abilities-reproduce-name = Reproduce +borer-abilities-reproduce-description = Create one of your own kind. +borer-abilities-restore-name = Restore Control +borer-abilities-restore-description = [color=red]Restores[/color] control over the host's body. +borer-abilities-resist-name = Resist +borer-abilities-resist-description = [color=red]Resisting the control[/color] of your body. + +borer-ui-scan-title = Scanning results +borer-ui-scan-label = Reagents in the host's blood: +borer-ui-secrete-title = Secrete chemicals +borer-ui-secrete-inject-label = Inject {$reagent}(10u) - {$cost}c + +borer-ui-converse-title = Converse +borer-ui-converse-message = Message + +borer-popup-infest-occupied = This creature's brain is already occupied +borer-popup-infest-sugar = The creature has too much sugar in it's blood +borer-popup-infest-failed = You can't infest into this creature + +borer-popup-braintake-alert = Your brain has been taken over! +borer-popup-braintake-success = You've taken control of the host's brain! +borer-popup-braintake-critical = You cannot control the brain of a creature in critical condition + +borer-popup-toomuchsugar = Your host's blood sugar prevents you from doing that +borer-popup-lowchem = Not enough chemicals! +borer-popup-injected = {$reagent}(10u) successfully injected! +borer-popup-sugarleave = The host has too much sugar in his blood, you can't be in his body anymore + +borer-message-braintake-success = You've taken control of the host's brain! The host may resist this and try to regain control. Have fun while you still have time! +borer-message-braintake-alert = Your brain has been taken over! You can resist or just let it happen + +borer-event-announcement = Detected unidentified life forms on the board. Secure all exterior entrances and exits, including ventilation and hoods. \ No newline at end of file diff --git a/Resources/Locale/ru-RU/borer/borer.ftl b/Resources/Locale/ru-RU/borer/borer.ftl new file mode 100644 index 0000000000..13255c94a8 --- /dev/null +++ b/Resources/Locale/ru-RU/borer/borer.ftl @@ -0,0 +1,52 @@ +ghost-role-information-borer-name = Аскарида космическая +ghost-role-information-borer-description = Мы - червь! + +borer-entity-name = Аскарида космическая +borer-entity-description = Выглядит так, будто от неё можно сойти с ума + +borer-abilities-infest-name = Вселение +borer-abilities-infest-description = [color=red]Поселиться в мозгу[/color] носителя. +borer-abilities-paralyze-name = Паралич +borer-abilities-paralyze-description = [color=red]Парализовать[/color] ничего не подозревающую жертву . +borer-abilities-release-host-name = Покинуть носителя +borer-abilities-release-host-description = [color=red]Покинуть[/color] тело носителя. +borer-abilities-converse-name = Связь с носителем +borer-abilities-converse-description = [color=red]Пообщаться[/color] с носителем, посылая сообщения прямо в его мозг. +borer-abilities-secrete-name = Впрыск реагентов +borer-abilities-secrete-description = [color=red]Вводит[/color] разлиные химикаты в кровь носителя. +borer-abilities-scan-name = Анализ крови +borer-abilities-scan-description = [color=red]Сканирует кровь[/color] носителя на наличие реагентов. +borer-abilities-control-name = Захватить контроль +borer-abilities-control-description = Отобрать [color=red]контроль над телом[/color] у носителя. +borer-abilities-reproduce-name = Репродукция +borer-abilities-reproduce-description = Создать себе подобного. +borer-abilities-restore-name = Вернуть контроль +borer-abilities-restore-description = [color=red]Возвращает[/color] контроль над телом разуму носителя. +borer-abilities-resist-name = Сопротивляться +borer-abilities-resist-description = Попытаться [color=red]вернуть[/color] себе контроль над телом. + +borer-ui-scan-title = Scanning result +borer-ui-scan-label = Реагенты в крови носителя: +borer-ui-secrete-title = Ввод реагентов +borer-ui-secrete-inject-label = Ввести {$reagent}(10u) - {$cost}c + +borer-ui-converse-title = Связь +borer-ui-converse-message = Сообщение + +borer-popup-infest-occupied = Мозг этого существа уже кем-то занят +borer-popup-infest-sugar = В крови существа слишком много сахара +borer-popup-infest-failed = Вы не можете вселиться в это существо + +borer-popup-braintake-alert = Вы не управляете своим телом! +borer-popup-braintake-success = Вы забрали контроль над телом! +borer-popup-braintake-critical = Вы не можете забрать контроль над телом у существа в критическом состоянии! + +borer-popup-toomuchsugar = Сахар в крови носителя не позволяет Вам сделать это +borer-popup-lowchem = Недостаточно химикатов! +borer-popup-injected = {$reagent}(10u) успешно введено! +borer-popup-sugarleave = В крови носителя слишком много сахара, Вы не можете больше находиться в его теле + +borer-message-braintake-success = Вы захватили контроль над мозгом носителя! Носитель может воспротивиться этому и попытаться вернуть контроль. Веселитесь, пока есть время! +borer-message-braintake-alert = Ваш мозг захвачен! Вы можете сопротивляться или просто позволить этому случиться + +borer-event-announcement = Обнаружены неопознанные формы жизни на борту. Перекройте все внешние входы и выходы, включая вентиляцию и вытяжки. \ No newline at end of file diff --git a/Resources/Prototypes/Actions/borer.yml b/Resources/Prototypes/Actions/borer.yml new file mode 100644 index 0000000000..199508dd29 --- /dev/null +++ b/Resources/Prototypes/Actions/borer.yml @@ -0,0 +1,133 @@ +- type: entity + id: ActionInfest + name: borer-abilities-infest-name + description: borer-abilities-infest-description + noSpawn: true + components: + - type: EntityTargetAction + icon: + sprite: Mobs/Animals/borer.rsi + state: action_infest + event: !type:BorerInfestActionEvent + itemIconStyle: BigAction + canTargetSelf: false + useDelay: 15 + +- type: entity + id: ActionBorerStunVictim + name: borer-abilities-paralyze-name + description: borer-abilities-paralyze-description + noSpawn: true + components: + - type: EntityTargetAction + icon: + sprite: Mobs/Animals/borer.rsi + state: action_stun + event: !type:BorerStunActionEvent + itemIconStyle: BigAction + canTargetSelf: false + useDelay: 40 + +- type: entity + id: ActionBorerOut + name: borer-abilities-release-host-name + description: borer-abilities-release-host-description + noSpawn: true + components: + - type: InstantAction + icon: + sprite: Mobs/Animals/borer.rsi + state: action_out + event: !type:BorerOutActionEvent + useDelay: 4 + +- type: entity + id: ActionBorerBrainSpeech + name: borer-abilities-converse-name + description: borer-abilities-converse-description + noSpawn: true + components: + - type: InstantAction + icon: + sprite: Mobs/Animals/borer.rsi + state: action_brainspeech + event: !type:BorerBrainSpeechActionEvent + useDelay: 4 + +- type: entity + id: ActionBorerInjectWindowOpen + name: borer-abilities-secrete-name + description: borer-abilities-secrete-description + noSpawn: true + components: + - type: InstantAction + icon: + sprite: Mobs/Animals/borer.rsi + state: action_inject + event: !type:BorerInjectWindowOpenEvent + useDelay: 4 + +- type: entity + id: ActionBorerScan + name: borer-abilities-scan-name + description: borer-abilities-scan-description + noSpawn: true + components: + - type: InstantAction + icon: + sprite: Mobs/Animals/borer.rsi + state: action_scanreagents + event: !type:BorerScanInstantActionEvent + useDelay: 5 + +- type: entity + id: ActionBorerBrainTake + name: borer-abilities-control-name + description: borer-abilities-control-description + noSpawn: true + components: + - type: InstantAction + icon: + sprite: Mobs/Animals/borer.rsi + state: action_braintake + event: !type:BorerBrainTakeEvent + useDelay: 4 + +- type: entity + id: ActionBorerReproduce + name: borer-abilities-reproduce-name + description: borer-abilities-reproduce-description + noSpawn: true + components: + - type: InstantAction + icon: + sprite: Mobs/Animals/borer.rsi + state: action_reproduce + event: !type:BorerReproduceEvent + useDelay: 30 + +- type: entity + id: ActionBorerBrainRelease + name: borer-abilities-restore-name + description: borer-abilities-restore-description + noSpawn: true + components: + - type: InstantAction + icon: + sprite: Mobs/Animals/borer.rsi + state: action_brainrelease + event: !type:BorerBrainReleaseEvent + useDelay: 4 + +- type: entity + id: ActionBorerBrainResist + name: borer-abilities-resist-name + description: borer-abilities-resist-description + noSpawn: true + components: + - type: InstantAction + icon: + sprite: Mobs/Animals/borer.rsi + state: action_brainrelease + event: !type:BorerBrainResistEvent + useDelay: 4 \ No newline at end of file diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/borer.yml b/Resources/Prototypes/Entities/Mobs/NPCs/borer.yml new file mode 100644 index 0000000000..baee1f402d --- /dev/null +++ b/Resources/Prototypes/Entities/Mobs/NPCs/borer.yml @@ -0,0 +1,72 @@ +- type: entity + name: basic borer + id: MobBorerBase + parent: SimpleSpaceMobBase + abstract: true + description: It looks like it's making you lose your mind. + components: + - type: Sprite + drawdepth: SmallMobs + sprite: Mobs/Animals/borer.rsi + layers: + - map: [ "enum.DamageStateVisualLayers.Base" ] + state: borer + - type: DamageStateVisuals + states: + Alive: + Base: borer + Dead: + Base: dead + - type: Fixtures + fixtures: + fix1: + shape: + !type:PhysShapeCircle + radius: 0.2 + density: 100 + mask: + - SmallMobMask + layer: + - SmallMobLayer + - type: MobThresholds + thresholds: + 0: Alive + 20: Dead + - type: MovementSpeedModifier + baseWalkSpeed: 2 + baseSprintSpeed: 4 + - type: Tag + tags: + - CannotSuicide + maxSaturation: 15 + - type: Bloodstream + bloodReagent: Slime + bloodlossDamage: + types: + Bloodloss: + 1 + bloodlossHealDamage: + types: + Bloodloss: + -0.25 + - type: NoSlip + - type: GhostTakeoverAvailable + - type: GhostRole + makeSentient: true + name: ghost-role-information-borer-name + description: ghost-role-information-borer-description + - type: CombatMode + combatToggleAction: ActionCombatModeToggleOff + - type: Actions + - type: ActionsContainer + - type: Speech + enabled: false + +- type: entity + name: Cortical Borer + parent: MobBorerBase + id: MobSimpleBorer + components: + - type: Borer + reproduceCost: 100 + assumeControlCost: 250 \ No newline at end of file diff --git a/Resources/Prototypes/GameRules/events.yml b/Resources/Prototypes/GameRules/events.yml index 368bf11de3..d8c27ac5e8 100644 --- a/Resources/Prototypes/GameRules/events.yml +++ b/Resources/Prototypes/GameRules/events.yml @@ -373,6 +373,25 @@ entries: - id: MobGiantSpiderAngry prob: 0.05 + +- type: entity + id: BorerSpawn + parent: BaseGameRule + noSpawn: true + components: + - type: StationEvent + earliestStart: 20 + minimumPlayers: 15 + weight: 5 + duration: 60 + startDelay: 30 + startAnnouncement: borer-event-announcement + startAudio: + path: /Audio/Announcements/attention.ogg + - type: VentCrittersRule + entries: + - id: MobSimpleBorer + prob: 0.04 - type: entity id: SpiderClownSpawn diff --git a/Resources/Textures/Interface/Borer/chem_bg.png b/Resources/Textures/Interface/Borer/chem_bg.png new file mode 100644 index 0000000000000000000000000000000000000000..0555561adb8154e581258ebdce9501945c0a7743 GIT binary patch literal 1337 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 literal 0 HcmV?d00001 diff --git a/Resources/Textures/Mobs/Animals/borer.rsi/action_out.png b/Resources/Textures/Mobs/Animals/borer.rsi/action_out.png new file mode 100644 index 0000000000000000000000000000000000000000..d8d5ad20cabb18e0e3d79145bcfddbbc758e7a1e GIT binary patch literal 2381 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