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 0000000000..0555561adb Binary files /dev/null and b/Resources/Textures/Interface/Borer/chem_bg.png differ diff --git a/Resources/Textures/Mobs/Animals/borer.rsi/action_brainrelease.png b/Resources/Textures/Mobs/Animals/borer.rsi/action_brainrelease.png new file mode 100644 index 0000000000..dbd22ee940 Binary files /dev/null and b/Resources/Textures/Mobs/Animals/borer.rsi/action_brainrelease.png differ diff --git a/Resources/Textures/Mobs/Animals/borer.rsi/action_brainspeech.png b/Resources/Textures/Mobs/Animals/borer.rsi/action_brainspeech.png new file mode 100644 index 0000000000..63069bea1c Binary files /dev/null and b/Resources/Textures/Mobs/Animals/borer.rsi/action_brainspeech.png differ diff --git a/Resources/Textures/Mobs/Animals/borer.rsi/action_braintake.png b/Resources/Textures/Mobs/Animals/borer.rsi/action_braintake.png new file mode 100644 index 0000000000..6485042689 Binary files /dev/null and b/Resources/Textures/Mobs/Animals/borer.rsi/action_braintake.png differ diff --git a/Resources/Textures/Mobs/Animals/borer.rsi/action_infest.png b/Resources/Textures/Mobs/Animals/borer.rsi/action_infest.png new file mode 100644 index 0000000000..e38984e2af Binary files /dev/null and b/Resources/Textures/Mobs/Animals/borer.rsi/action_infest.png differ diff --git a/Resources/Textures/Mobs/Animals/borer.rsi/action_inject.png b/Resources/Textures/Mobs/Animals/borer.rsi/action_inject.png new file mode 100644 index 0000000000..7b9f156d96 Binary files /dev/null and b/Resources/Textures/Mobs/Animals/borer.rsi/action_inject.png differ 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 0000000000..d8d5ad20ca Binary files /dev/null and b/Resources/Textures/Mobs/Animals/borer.rsi/action_out.png differ diff --git a/Resources/Textures/Mobs/Animals/borer.rsi/action_reproduce.png b/Resources/Textures/Mobs/Animals/borer.rsi/action_reproduce.png new file mode 100644 index 0000000000..6dbe53760e Binary files /dev/null and b/Resources/Textures/Mobs/Animals/borer.rsi/action_reproduce.png differ diff --git a/Resources/Textures/Mobs/Animals/borer.rsi/action_scanreagents.png b/Resources/Textures/Mobs/Animals/borer.rsi/action_scanreagents.png new file mode 100644 index 0000000000..957a66f638 Binary files /dev/null and b/Resources/Textures/Mobs/Animals/borer.rsi/action_scanreagents.png differ diff --git a/Resources/Textures/Mobs/Animals/borer.rsi/action_stun.png b/Resources/Textures/Mobs/Animals/borer.rsi/action_stun.png new file mode 100644 index 0000000000..fe1398e314 Binary files /dev/null and b/Resources/Textures/Mobs/Animals/borer.rsi/action_stun.png differ diff --git a/Resources/Textures/Mobs/Animals/borer.rsi/borer.png b/Resources/Textures/Mobs/Animals/borer.rsi/borer.png new file mode 100644 index 0000000000..5da8e3c623 Binary files /dev/null and b/Resources/Textures/Mobs/Animals/borer.rsi/borer.png differ diff --git a/Resources/Textures/Mobs/Animals/borer.rsi/dead.png b/Resources/Textures/Mobs/Animals/borer.rsi/dead.png new file mode 100644 index 0000000000..99cbaa9357 Binary files /dev/null and b/Resources/Textures/Mobs/Animals/borer.rsi/dead.png differ diff --git a/Resources/Textures/Mobs/Animals/borer.rsi/meta.json b/Resources/Textures/Mobs/Animals/borer.rsi/meta.json new file mode 100644 index 0000000000..de181d13f9 --- /dev/null +++ b/Resources/Textures/Mobs/Animals/borer.rsi/meta.json @@ -0,0 +1,55 @@ +{ + "version": 1, + "size": { + "x": 32, + "y": 32 + }, + "license": "CC-BY-SA-3.0", + "copyright": "Created by Ogunefu", + "states": [ + { + "name": "borer", + "directions": 4 + }, + { + "name": "dead", + "directions": 1 + }, + { + "name": "action_out", + "directions": 1 + }, + { + "name": "action_infest", + "directions": 1 + }, + { + "name": "action_stun", + "directions": 1 + }, + { + "name": "action_brainspeech", + "directions": 1 + }, + { + "name": "action_inject", + "directions": 1 + }, + { + "name": "action_scanreagents", + "directions": 1 + }, + { + "name": "action_braintake", + "directions": 1 + }, + { + "name": "action_reproduce", + "directions": 1 + }, + { + "name": "action_brainrelease", + "directions": 1 + } + ] +} diff --git a/global.json b/global.json index 391ba3c2a3..d4e0b89930 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,7 @@ { "sdk": { - "version": "8.0.100", - "rollForward": "latestFeature" + "version": "8.0.0", + "rollForward": "latestFeature", + "allowPrerelease": true } -} +} \ No newline at end of file