Port Cortical Borers (real) (#4466)

* ITS ALL C# GOOD LORD

BORN TO PORT
WORLD IS A PR
鬼神 Merge Em All 2025
I am yaml man
410,757,864,530 UPSTREAM MERGE CONFLICTS

* that was, uh. too easy.

* very small tweak

* comment and reuse fixes

* forgot the guidebook Oops

* the guidebookening

* the reusening
This commit is contained in:
KOTOB 2025-10-15 22:11:24 -07:00 committed by GitHub
parent 2e77b9f6d7
commit c6addd2e9e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
66 changed files with 2230 additions and 20 deletions

View File

@ -0,0 +1,90 @@
using System.Numerics;
using Content.Shared.Alert.Components;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
namespace Content.Client.Alerts;
/// <summary>
/// This handles <see cref="GenericCounterAlertComponent"/>
/// </summary>
public sealed class GenericCounterAlertSystem : EntitySystem
{
[Dependency] private readonly SpriteSystem _sprite = default!;
/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<GenericCounterAlertComponent, UpdateAlertSpriteEvent>(OnUpdateAlertSprite);
}
private void OnUpdateAlertSprite(Entity<GenericCounterAlertComponent> ent, ref UpdateAlertSpriteEvent args)
{
var sprite = args.SpriteViewEnt.Comp;
var ev = new GetGenericAlertCounterAmountEvent(args.Alert);
RaiseLocalEvent(args.ViewerEnt, ref ev);
if (!ev.Handled)
return;
// It cannot be null if its handled, but good to check to avoid ugly null ignores.
if (ev.Amount == null)
return;
// How many digits can we display
var maxDigitCount = GetMaxDigitCount((ent, ent, sprite));
// Clamp it to a positive number that we can actually display in full (no rollover to 0)
var amount = (int) Math.Clamp(ev.Amount.Value, 0, Math.Pow(10, maxDigitCount) - 1);
// This is super wack but ig it works?
var digitCount = ent.Comp.HideLeadingZeroes
? amount.ToString().Length
: maxDigitCount;
if (ent.Comp.HideLeadingZeroes)
{
for (var i = 0; i < ent.Comp.DigitKeys.Count; i++)
{
if (!_sprite.LayerMapTryGet(ent.Owner, ent.Comp.DigitKeys[i], out var layer, false))
continue;
_sprite.LayerSetVisible(ent.Owner, layer, i <= digitCount - 1);
}
}
// ReSharper disable once PossibleLossOfFraction
var baseOffset = (ent.Comp.AlertSize.X - digitCount * ent.Comp.GlyphWidth) / 2 * (1f / EyeManager.PixelsPerMeter);
for (var i = 0; i < ent.Comp.DigitKeys.Count; i++)
{
if (!_sprite.LayerMapTryGet(ent.Owner, ent.Comp.DigitKeys[i], out var layer, false))
continue;
var result = amount / (int) Math.Pow(10, i) % 10;
_sprite.LayerSetRsiState(ent.Owner, layer, result.ToString());
if (ent.Comp.CenterGlyph)
{
var offset = baseOffset + (digitCount - 1 - i) * ent.Comp.GlyphWidth * (1f / EyeManager.PixelsPerMeter);
_sprite.LayerSetOffset(ent.Owner, layer, new Vector2(offset, 0));
}
}
}
/// <summary>
/// Gets the number of digits that we can display.
/// </summary>
/// <returns>The number of digits.</returns>
private int GetMaxDigitCount(Entity<GenericCounterAlertComponent, SpriteComponent> ent)
{
for (var i = ent.Comp1.DigitKeys.Count - 1; i >= 0; i--)
{
if (_sprite.LayerExists((ent.Owner, ent.Comp2), ent.Comp1.DigitKeys[i]))
return i + 1;
}
return 0;
}
}

View File

@ -11,11 +11,14 @@ public record struct UpdateAlertSpriteEvent
{
public Entity<SpriteComponent> SpriteViewEnt;
public EntityUid ViewerEnt;
public AlertPrototype Alert;
public UpdateAlertSpriteEvent(Entity<SpriteComponent> spriteViewEnt, AlertPrototype alert)
public UpdateAlertSpriteEvent(Entity<SpriteComponent> spriteViewEnt, EntityUid viewerEnt, AlertPrototype alert)
{
SpriteViewEnt = spriteViewEnt;
ViewerEnt = viewerEnt;
Alert = alert;
}
}

View File

@ -1,4 +1,6 @@
using Content.Client.Alerts;
using Content.Shared.Alert;
using Content.Shared.Alert.Components;
using Content.Shared.Revenant;
using Content.Shared.Revenant.Components;
using Robust.Client.GameObjects;
@ -15,7 +17,7 @@ public sealed class RevenantSystem : EntitySystem
base.Initialize();
SubscribeLocalEvent<RevenantComponent, AppearanceChangeEvent>(OnAppearanceChange);
SubscribeLocalEvent<RevenantComponent, UpdateAlertSpriteEvent>(OnUpdateAlert);
SubscribeLocalEvent<RevenantComponent, GetGenericAlertCounterAmountEvent>(OnGetCounterAmount);
}
private void OnAppearanceChange(EntityUid uid, RevenantComponent component, ref AppearanceChangeEvent args)
@ -40,14 +42,14 @@ public sealed class RevenantSystem : EntitySystem
}
}
private void OnUpdateAlert(Entity<RevenantComponent> ent, ref UpdateAlertSpriteEvent args)
private void OnGetCounterAmount(Entity<RevenantComponent> ent, ref GetGenericAlertCounterAmountEvent args)
{
if (args.Alert.ID != ent.Comp.EssenceAlert)
if (args.Handled)
return;
var essence = Math.Clamp(ent.Comp.Essence.Int(), 0, 999);
_sprite.LayerSetRsiState(args.SpriteViewEnt.AsNullable(), RevenantVisualLayers.Digit1, $"{(essence / 100) % 10}");
_sprite.LayerSetRsiState(args.SpriteViewEnt.AsNullable(), RevenantVisualLayers.Digit2, $"{(essence / 10) % 10}");
_sprite.LayerSetRsiState(args.SpriteViewEnt.AsNullable(), RevenantVisualLayers.Digit3, $"{essence % 10}");
if (ent.Comp.EssenceAlert != args.Alert)
return;
args.Amount = ent.Comp.Essence.Int();
}
}

View File

@ -98,7 +98,8 @@ public sealed class AlertsUIController : UIController, IOnStateEntered<GameplayS
if (!EntityManager.TryGetComponent<SpriteComponent>(spriteViewEnt, out var sprite))
return;
var ev = new UpdateAlertSpriteEvent((spriteViewEnt, sprite), alert);
var ev = new UpdateAlertSpriteEvent((spriteViewEnt, sprite), player, alert);
EntityManager.EventBus.RaiseLocalEvent(player, ref ev);
EntityManager.EventBus.RaiseLocalEvent(spriteViewEnt, ref ev);
}
}

View File

@ -57,10 +57,15 @@ namespace Content.Client.UserInterface.Systems.Alerts.Controls
_sprite = _entityManager.System<SpriteSystem>();
TooltipSupplier = SupplyTooltip;
Alert = alert;
HorizontalAlignment = HAlignment.Left;
_severity = severity;
_icon = new SpriteView
{
Scale = new Vector2(2, 2)
Scale = new Vector2(2, 2),
MaxSize = new Vector2(64, 64),
Stretch = SpriteView.StretchMode.None,
HorizontalAlignment = HAlignment.Left
};
SetupIcon();

View File

@ -0,0 +1,50 @@
// SPDX-FileCopyrightText: 2025 Coenx-flex
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Client.UserInterface.Controls;
using Content.Shared._Mono.CorticalBorer;
using JetBrains.Annotations;
using Robust.Client.UserInterface;
namespace Content.Client._Mono.CorticalBorer
{
[UsedImplicitly]
public sealed class CorticalBorerDispenserBoundUserInterface : BoundUserInterface
{
private CorticalBorerDispenserWindow? _window;
public CorticalBorerDispenserBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
}
protected override void Open()
{
base.Open();
_window = this.CreateWindow<CorticalBorerDispenserWindow>();
_window.SetInfoFromEntity(EntMan, Owner);
// Setup static button actions.
_window.AmountGrid.OnButtonPressed += s => SendMessage(new CorticalBorerDispenserSetInjectAmountMessage(s));
_window.OnDispenseReagentButtonPressed += id => SendMessage(new CorticalBorerDispenserInjectMessage(id));
}
/// <summary>
/// Update the UI each time new state data is sent from the server.
/// </summary>
/// <param name="state">
/// Data of the <see cref="ReagentDispenserComponent"/> that this UI represents.
/// Sent from the server.
/// </param>
protected override void UpdateState(BoundUserInterfaceState state)
{
base.UpdateState(state);
var castState = (CorticalBorerDispenserBoundUserInterfaceState) state;
_window?.UpdateState(castState);
}
}
}

View File

@ -0,0 +1,43 @@
<controls:FancyWindow
xmlns="https://spacestation14.io"
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:ui="clr-namespace:Content.Client.Chemistry.UI"
Title="{Loc 'reagent-dispenser-bound-user-interface-title'}"
MinSize="400 175"
SetSize="600 300">
<BoxContainer Orientation="Horizontal">
<BoxContainer Orientation="Vertical" MinWidth="170">
<Label Text="{Loc 'reagent-dispenser-window-amount-to-dispense-label'}" HorizontalAlignment="Center" />
<ui:ButtonGrid
Name="AmountGrid"
Access="Public"
Columns="3"
HorizontalAlignment="Center"
Margin="5"
ButtonList="1,5,10,15,20,25,30,50,100"
RadioGroup="True">
</ui:ButtonGrid>
</BoxContainer>
<SplitContainer Orientation="Vertical"
HorizontalExpand="True"
VerticalExpand="True">
<ScrollContainer HScrollEnabled="False"
HorizontalExpand="True"
VerticalExpand="True"
MinHeight="50"
SizeFlagsStretchRatio="2.5">
<GridContainer Name="ReagentList"
HorizontalExpand="True"
VerticalExpand="True"
Access="Public"
Columns="3" />
</ScrollContainer>
<ScrollContainer HScrollEnabled="False"
HorizontalExpand="True"
VerticalExpand="True"
MinHeight="50">
</ScrollContainer>
</SplitContainer>
</BoxContainer>
</controls:FancyWindow>

View File

@ -0,0 +1,51 @@
// SPDX-FileCopyrightText: 2025 Coenx-flex
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Client.UserInterface.Controls;
using Content.Shared._Mono.CorticalBorer;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.XAML;
namespace Content.Client._Mono.CorticalBorer;
[GenerateTypedNameReferences]
public sealed partial class CorticalBorerDispenserWindow : FancyWindow
{
public event Action<string>? OnDispenseReagentButtonPressed;
public CorticalBorerDispenserWindow()
{
RobustXamlLoader.Load(this);
}
public void UpdateState(BoundUserInterfaceState state)
{
var castState = (CorticalBorerDispenserBoundUserInterfaceState)state;
UpdateReagentsList(castState.DisList);
AmountGrid.Selected = castState.SelectedDispenseAmount.ToString();
}
public void UpdateReagentsList(List<CorticalBorerDispenserItem> chemicals)
{
if (ReagentList == null)
return;
ReagentList.Children.Clear();
chemicals.Sort((x, y) => x.ReagentName.CompareTo(y.ReagentName));
foreach (var chem in chemicals)
{
var card = new CorticalBorerReagentCardControl(chem);
card.OnPressed += OnDispenseReagentButtonPressed;
ReagentList.Children.Add(card);
}
}
}

View File

@ -0,0 +1,33 @@
// SPDX-FileCopyrightText: 2025 Coenx-flex
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared._Mono.CorticalBorer;
using Content.Shared.Chemistry;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.XAML;
namespace Content.Client._Mono.CorticalBorer;
[GenerateTypedNameReferences]
public sealed partial class CorticalBorerReagentCardControl : Control
{
public Action<string>? OnPressed;
public CorticalBorerReagentCardControl(CorticalBorerDispenserItem set)
{
RobustXamlLoader.Load(this);
ColorPanel.PanelOverride = new StyleBoxFlat{ BackgroundColor = set.ReagentColor};
ReagentNameLabel.Text = set.ReagentName;
FillLabel.Text = Loc.GetString("cortical-borer-dispenser-window-cost", ("cost", set.Cost * set.Amount));
// disable the button if you can't afford it, makes it easier
MainButton.Disabled = set.Chems < set.Cost * set.Amount;
MainButton.OnPressed += args => OnPressed?.Invoke(set.ReagentId);
}
}

View File

@ -0,0 +1,27 @@
<Control xmlns="https://spacestation14.io" HorizontalExpand="True">
<BoxContainer Name="MainContainer"
Orientation="Horizontal"
HorizontalExpand="True">
<PanelContainer Name="ColorPanel"
VerticalExpand="True"
SetWidth="7"
Margin="0 1 0 0" />
<Button Name="MainButton"
HorizontalExpand="True"
VerticalExpand="True"
StyleClasses="ButtonSquare"
Margin="-1 0 0 0">
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
<BoxContainer Orientation="Vertical"
VerticalExpand="True"
HorizontalExpand="True"
Margin="-5 0 0 0">
<Label Name="ReagentNameLabel" />
<Label Name="FillLabel"
StyleClasses="LabelSubText"
Margin="0 -5 0 0" />
</BoxContainer>
</BoxContainer>
</Button>
</BoxContainer>
</Control>

View File

@ -0,0 +1,31 @@
// SPDX-FileCopyrightText: 2025 Coenx-flex
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared._Mono.CorticalBorer;
using Content.Shared.Alert.Components;
namespace Content.Client._Mono.CorticalBorer;
/// <inheritdoc/>
public sealed class CorticalBorerSystem : SharedCorticalBorerSystem
{
/// <inheritdoc/>
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<CorticalBorerComponent, GetGenericAlertCounterAmountEvent>(OnGetCounterAmount);
}
private void OnGetCounterAmount(Entity<CorticalBorerComponent> ent, ref GetGenericAlertCounterAmountEvent args)
{
if (args.Handled)
return;
if (ent.Comp.ChemicalAlert != args.Alert)
return;
args.Amount = ent.Comp.ChemicalPoints;
}
}

View File

@ -251,6 +251,17 @@ public sealed partial class ChatSystem : SharedChatSystem
if (string.IsNullOrEmpty(message))
return;
// Begin Mono Changes - Is this being sent direct
var targetEv = new CheckTargetedSpeechEvent();
RaiseLocalEvent(source, targetEv);
if (targetEv.Targets.Count > 0)
{
SendEntityDirect(source, message, range, nameOverride, targetEv.Targets);
return;
}
// End Mono Changes - Is this being sent direct
// This message may have a radio prefix, and should then be whispered to the resolved radio channel
if (checkRadioPrefix)
{
@ -591,6 +602,72 @@ public sealed partial class ChatSystem : SharedChatSystem
}
}
// Begin Mono Changes
private void SendEntityDirect(
EntityUid source,
string originalMessage,
ChatTransmitRange range,
string? nameOverride,
List<EntityUid> recipients,
bool hideLog = false,
bool ignoreActionBlocker = false)
{
var message = TransformSpeech(source, FormattedMessage.RemoveMarkupOrThrow(originalMessage));
if (message.Length == 0)
return;
string name;
if (nameOverride != null)
{
name = nameOverride;
}
else
{
var nameEv = new TransformSpeakerNameEvent(source, Name(source));
RaiseLocalEvent(source, nameEv);
name = nameEv.VoiceName;
}
name = FormattedMessage.EscapeText(name);
var wrappedMessage = Loc.GetString("chat-manager-entity-whisper-wrap-message",
("entityName", name), ("message", FormattedMessage.EscapeText(message)));
foreach (var (session, data) in GetRecipients(source, WhisperMuffledRange))
{
EntityUid listener;
if (session.AttachedEntity is not { Valid: true } playerEntity)
continue;
listener = session.AttachedEntity.Value;
if (MessageRangeCheck(session, data, range) != MessageRangeCheckResult.Full ||
!recipients.Contains(listener) &&
!HasComp<GhostComponent>(listener))
continue;
_chatManager.ChatMessageToOne(ChatChannel.Local, message, wrappedMessage, source, false, session.Channel); // DeltaV - no collective mind chat channel, use local..?
}
if (!hideLog)
if (originalMessage == message)
{
if (name != Name(source))
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Direct messaged from {ToPrettyString(source):user} as {name}: {originalMessage}.");
else
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Direct messaged from {ToPrettyString(source):user}: {originalMessage}.");
}
else
{
if (name != Name(source))
_adminLogger.Add(LogType.Chat, LogImpact.Low,
$"Direct messaged from {ToPrettyString(source):user} as {name}, original: {originalMessage}, transformed: {message}.");
else
_adminLogger.Add(LogType.Chat, LogImpact.Low,
$"Direct messaged from {ToPrettyString(source):user}, original: {originalMessage}, transformed: {message}.");
}
}
// End Mono Changes
private void SendEntityEmote(
EntityUid source,
string action,
@ -959,6 +1036,13 @@ public sealed class CheckIgnoreSpeechBlockerEvent : EntityEventArgs
}
}
// Begin Mono Changes
public sealed class CheckTargetedSpeechEvent : EntityEventArgs
{
public List<EntityUid> Targets = new List<EntityUid>();
}
// End Mono Changes
/// <summary>
/// Raised on an entity when it speaks, either through 'say' or 'whisper'.
/// </summary>

View File

@ -186,7 +186,7 @@ public sealed class HealthAnalyzerSystem : EntitySystem
/// <param name="healthAnalyzer">The health analyzer that should receive the updates</param>
/// <param name="target">The entity to start analyzing</param>
/// <param name="part">Shitmed Change: The body part to analyze, if any</param>
private void BeginAnalyzingEntity(Entity<HealthAnalyzerComponent> healthAnalyzer, EntityUid target, EntityUid? part = null)
public void BeginAnalyzingEntity(Entity<HealthAnalyzerComponent> healthAnalyzer, EntityUid target, EntityUid? part = null) // Mono - make public
{
//Link the health analyzer to the scanned entity
healthAnalyzer.Comp.ScannedEntity = target;
@ -202,7 +202,7 @@ public sealed class HealthAnalyzerSystem : EntitySystem
/// </summary>
/// <param name="healthAnalyzer">The health analyzer that's receiving the updates</param>
/// <param name="target">The entity to analyze</param>
private void StopAnalyzingEntity(Entity<HealthAnalyzerComponent> healthAnalyzer, EntityUid target)
public void StopAnalyzingEntity(Entity<HealthAnalyzerComponent> healthAnalyzer, EntityUid target) // Mono - make public
{
//Unlink the analyzer
healthAnalyzer.Comp.ScannedEntity = null;

View File

@ -0,0 +1,86 @@
// SPDX-FileCopyrightText: 2025 Coenx-flex
// SPDX-FileCopyrightText: 2025 Cojoke
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared._Mono.CorticalBorer;
using Content.Shared._Shitmed.Body.Events;
using Content.Shared.Body.Part;
using Content.Shared.Examine;
using Content.Shared.Mind.Components;
using Content.Shared.Mobs;
using Robust.Server.Containers;
using Robust.Shared.Containers;
using Robust.Shared.Timing;
namespace Content.Server._Mono.CorticalBorer;
public sealed class CorticalBorerInfestedSystem : EntitySystem
{
[Dependency] private readonly ContainerSystem _container = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly CorticalBorerSystem _borer = default!;
/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<CorticalBorerInfestedComponent, MapInitEvent>(OnInit);
SubscribeLocalEvent<CorticalBorerInfestedComponent, ExaminedEvent>(OnExaminedInfested);
SubscribeLocalEvent<CorticalBorerInfestedComponent, BodyPartRemovedEvent>(OnBodyPartRemoved);
SubscribeLocalEvent<CorticalBorerInfestedComponent, MobStateChangedEvent>(OnStateChange);
SubscribeLocalEvent<CorticalBorerInfestedComponent, MindRemovedMessage>(OnMindRemoved);
}
private void OnInit(Entity<CorticalBorerInfestedComponent> infested, ref MapInitEvent args)
{
infested.Comp.ControlContainer = _container.EnsureContainer<Container>(infested, "ControlContainer");
infested.Comp.InfestationContainer = _container.EnsureContainer<Container>(infested, "InfestationContainer");
}
private void OnExaminedInfested(Entity<CorticalBorerInfestedComponent> infected, ref ExaminedEvent args)
{
if (!args.IsInDetailsRange
|| args.Examined != args.Examiner)
return;
if (!infected.Comp.Borer.Comp.ControlingHost)
return;
if (infected.Comp.ControlTimeEnd is { } cte)
{
var timeRemaining = Math.Floor((cte - _timing.CurTime).TotalSeconds);
args.PushMarkup(Loc.GetString("infested-control-examined", ("timeremaining", timeRemaining)));
}
args.PushMarkup(Loc.GetString("cortical-borer-self-examine", ("chempoints", infected.Comp.Borer.Comp.ChemicalPoints)));
}
private void OnStateChange(Entity<CorticalBorerInfestedComponent> infected, ref MobStateChangedEvent args)
{
if (args.NewMobState != MobState.Dead)
return;
if(infected.Comp.Borer.Comp.ControlingHost)
_borer.EndControl(infected.Comp.Borer);
}
private void OnBodyPartRemoved(Entity<CorticalBorerInfestedComponent> infected, ref BodyPartRemovedEvent args)
{
if (TryComp<BodyPartComponent>(args.Part, out var part) &&
part.PartType == BodyPartType.Head)
{
_borer.EndControl(infected.Comp.Borer);
_borer.TryEjectBorer(infected.Comp.Borer);
}
}
private void OnMindRemoved(Entity<CorticalBorerInfestedComponent> infected, ref MindRemovedMessage args)
{
if (infected.Comp.Borer.Comp.ControlingHost)
{
_borer.EndControl(infected.Comp.Borer);
_borer.TryEjectBorer(infected.Comp.Borer);
}
}
}

View File

@ -0,0 +1,224 @@
// SPDX-FileCopyrightText: 2025 Coenx-flex
// SPDX-FileCopyrightText: 2025 Cojoke
// SPDX-FileCopyrightText: 2025 ark1368
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Server.Body.Components;
using Content.Server.Medical;
using Content.Shared._Mono.CorticalBorer;
using Content.Shared._Shitmed.Medical.Surgery;
using Content.Shared.Body.Components;
using Content.Shared.DoAfter;
using Content.Shared.IdentityManagement;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Content.Shared.Popups;
namespace Content.Server._Mono.CorticalBorer;
public sealed partial class CorticalBorerSystem
{
[Dependency] private readonly VomitSystem _vomit = default!;
private void SubscribeAbilities()
{
SubscribeLocalEvent<CorticalBorerComponent, CorticalInfestEvent>(OnInfest);
SubscribeLocalEvent<CorticalBorerComponent, CorticalInfestDoAfterEvent>(OnInfestDoAfter);
SubscribeLocalEvent<CorticalBorerComponent, CorticalEjectEvent>(OnEjectHost);
SubscribeLocalEvent<CorticalBorerComponent, CorticalTakeControlEvent>(OnTakeControl);
SubscribeLocalEvent<CorticalBorerComponent, CorticalChemMenuActionEvent>(OnChemcialMenu);
SubscribeLocalEvent<CorticalBorerComponent, CorticalCheckBloodEvent>(OnCheckBlood);
SubscribeLocalEvent<CorticalBorerInfestedComponent, CorticalEndControlEvent>(OnEndControl);
SubscribeLocalEvent<CorticalBorerInfestedComponent, CorticalLayEggEvent>(OnLayEgg);
}
private void OnChemcialMenu(Entity<CorticalBorerComponent> ent, ref CorticalChemMenuActionEvent args)
{
if(!TryComp<UserInterfaceComponent>(ent, out var uic))
return;
if (ent.Comp.Host is null)
{
_popup.PopupEntity(Loc.GetString("cortical-borer-no-host"), ent, ent, PopupType.Medium);
return;
}
_ui.TryToggleUi((ent, uic), CorticalBorerDispenserUiKey.Key, ent);
}
private void OnInfest(Entity<CorticalBorerComponent> ent, ref CorticalInfestEvent args)
{
var (uid, comp) = ent;
var target = args.Target;
var targetIdentity = Identity.Entity(target, EntityManager);
if (comp.Host is not null)
{
_popup.PopupEntity(Loc.GetString("cortical-borer-has-host"), uid, uid, PopupType.Medium);
return;
}
if (HasComp<CorticalBorerInfestedComponent>(target))
{
_popup.PopupEntity(Loc.GetString("cortical-borer-host-already-infested", ("target", targetIdentity)), uid, uid, PopupType.Medium);
return;
}
// anything with bloodstream
if (!HasComp<BloodstreamComponent>(target))
{
_popup.PopupEntity(Loc.GetString("cortical-borer-invalid-host", ("target", targetIdentity)), uid, uid, PopupType.Medium);
return;
}
// target is on sugar for some reason, can't go in there
if (!CanUseAbility(ent, target))
return;
var infestAttempt = new InfestHostAttempt();
RaiseLocalEvent(target, infestAttempt);
if (infestAttempt.Cancelled)
{
_popup.PopupEntity(Loc.GetString("cortical-borer-face-covered", ("target", targetIdentity)), uid, uid, PopupType.Medium);
return;
}
_popup.PopupEntity(Loc.GetString("cortical-borer-start-infest", ("target", targetIdentity)), uid, uid, PopupType.Medium);
var infestArgs = new DoAfterArgs(EntityManager, uid, TimeSpan.FromSeconds(3), new CorticalInfestDoAfterEvent(), uid, target)
{
DistanceThreshold = 1.5f,
BreakOnDamage = true,
BreakOnMove = true,
BreakOnWeightlessMove = true,
AttemptFrequency = AttemptFrequency.StartAndEnd,
Hidden = true,
};
_doAfter.TryStartDoAfter(infestArgs);
}
private void OnInfestDoAfter(Entity<CorticalBorerComponent> ent, ref CorticalInfestDoAfterEvent args)
{
if (args.Handled)
return;
if (args.Args.Target is not { } target)
return;
if (args.Cancelled || HasComp<CorticalBorerInfestedComponent>(target))
return;
InfestTarget(ent, target);
args.Handled = true;
}
private void OnEjectHost(Entity<CorticalBorerComponent> ent, ref CorticalEjectEvent args)
{
if (args.Handled)
return;
var (uid, comp) = ent;
if (comp.Host is null)
{
_popup.PopupEntity(Loc.GetString("cortical-borer-no-host"), uid, uid, PopupType.Medium);
return;
}
if (!CanUseAbility(ent, comp.Host.Value))
return;
TryEjectBorer(ent);
args.Handled = true;
}
private void OnCheckBlood(Entity<CorticalBorerComponent> ent, ref CorticalCheckBloodEvent args)
{
if (args.Handled)
return;
if (ent.Comp.Host is null)
{
_popup.PopupEntity(Loc.GetString("cortical-borer-no-host"), ent, ent, PopupType.Medium);
return;
}
TryToggleCheckBlood(ent);
args.Handled = true;
}
private void OnTakeControl(Entity<CorticalBorerComponent> ent, ref CorticalTakeControlEvent args)
{
if (args.Handled)
return;
if (ent.Comp.Host is null)
{
_popup.PopupEntity(Loc.GetString("cortical-borer-no-host"), ent, ent, PopupType.Medium);
return;
}
// Host is dead, you can't take control
if (TryComp<MobStateComponent>(ent.Comp.Host, out var mobState) &&
mobState.CurrentState == MobState.Dead)
{
_popup.PopupEntity(Loc.GetString("cortical-borer-dead-host"), ent, ent, PopupType.Medium);
return;
}
if (!TryComp<CorticalBorerInfestedComponent>(ent.Comp.Host, out var infestedComp))
return;
if (!CanUseAbility(ent, ent.Comp.Host.Value))
return;
// idk how you would cause this...
if (ent.Comp.ControlingHost)
{
_popup.PopupEntity(Loc.GetString("cortical-borer-already-control"), ent, ent, PopupType.Medium);
return;
}
TakeControlHost(ent, infestedComp);
args.Handled = true;
}
private void OnEndControl(Entity<CorticalBorerInfestedComponent> host, ref CorticalEndControlEvent args)
{
if (args.Handled)
return;
EndControl(host.Comp.Borer);
args.Handled = true;
}
private void OnLayEgg(Entity<CorticalBorerInfestedComponent> host, ref CorticalLayEggEvent args)
{
if (args.Handled)
return;
var borer = host.Comp.Borer;
if (borer.Comp.EggCost > borer.Comp.ChemicalPoints)
{
_popup.PopupEntity(Loc.GetString("cortical-borer-not-enough-chem"), host, host, PopupType.Medium);
return;
}
_vomit.Vomit(host, -20, -20); // half as much chem vomit, a lot that is coming up is the egg
LayEgg(borer);
UpdateChems(borer, -borer.Comp.EggCost);
args.Handled = true;
}
}

View File

@ -0,0 +1,390 @@
// SPDX-FileCopyrightText: 2025 Coenx-flex
// SPDX-FileCopyrightText: 2025 Cojoke
// SPDX-FileCopyrightText: 2025 ScyronX
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Server.Body.Components;
using Content.Server.Body.Systems;
using Content.Server.Chat.Managers;
using Content.Server.Chat.Systems;
using Content.Server.DoAfter;
using Content.Server.Ghost.Roles;
using Content.Server.Ghost.Roles.Components;
using Content.Server.Medical;
using Content.Server.Medical.Components;
using Content.Server.Nutrition.Components;
using Content.Shared._Mono.CorticalBorer;
using Content.Shared.Administration.Logs;
using Content.Shared.Alert;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Chat; // Einstein Engines - Languages
using Content.Shared.Database;
using Content.Shared.Inventory;
using Content.Shared.MedicalScanner;
using Content.Shared.Mind;
using Content.Shared.Mind.Components;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Content.Shared.Nutrition.Components; // DeltaV - IngestionBlocker in shared
using Content.Shared.Popups;
using Content.Shared.SSDIndicator;
using Robust.Server.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
namespace Content.Server._Mono.CorticalBorer;
public sealed partial class CorticalBorerSystem : SharedCorticalBorerSystem
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly BloodstreamSystem _blood = default!;
[Dependency] private readonly HealthAnalyzerSystem _analyzer = default!;
[Dependency] private readonly DoAfterSystem _doAfter = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly InventorySystem _inventory = default!;
[Dependency] private readonly UserInterfaceSystem _userInterfaceSystem = default!;
[Dependency] private readonly ISharedAdminLogManager _admin = default!;
[Dependency] private readonly SharedMindSystem _mind = default!;
[Dependency] private readonly IChatManager _chat = default!;
[Dependency] private readonly AlertsSystem _alerts = default!;
[Dependency] private readonly GhostRoleSystem _ghost = default!;
public override void Initialize()
{
SubscribeAbilities();
SubscribeLocalEvent<CorticalBorerComponent, ComponentStartup>(OnStartup);
SubscribeLocalEvent<CorticalBorerComponent, CorticalBorerDispenserInjectMessage>(OnInjectReagentMessage);
SubscribeLocalEvent<CorticalBorerComponent, CorticalBorerDispenserSetInjectAmountMessage>(OnSetInjectAmountMessage);
SubscribeLocalEvent<InventoryComponent, InfestHostAttempt>(OnInfestHostAttempt);
SubscribeLocalEvent<CorticalBorerComponent, CheckTargetedSpeechEvent>(OnSpeakEvent);
SubscribeLocalEvent<CorticalBorerComponent, MindRemovedMessage>(OnMindRemoved);
}
private void OnStartup(Entity<CorticalBorerComponent> ent, ref ComponentStartup args)
{
//add actions
foreach (var actionId in ent.Comp.InitialCorticalBorerActions)
_actions.AddAction(ent, actionId);
_alerts.ShowAlert(ent, ent.Comp.ChemicalAlert);
UpdateUiState(ent);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
foreach (var comp in EntityManager.EntityQuery<CorticalBorerComponent>())
{
if (_timing.CurTime < comp.UpdateTimer)
continue;
comp.UpdateTimer = _timing.CurTime + TimeSpan.FromSeconds(comp.UpdateCooldown);
if (comp.Host != null)
UpdateChems((comp.Owner, comp), comp.ChemicalGenerationRate);
}
foreach (var comp in EntityManager.EntityQuery<CorticalBorerInfestedComponent>())
{
if (_timing.CurTime >= comp.ControlTimeEnd)
EndControl(comp.Borer);
}
}
private void OnSpeakEvent(Entity<CorticalBorerComponent> ent, ref CheckTargetedSpeechEvent args)
{
//args.ChatTypeIgnore.Add(InGameICChatType.CollectiveMind); // DeltaV - eradicate language code
if (ent.Comp.Host.HasValue)
{
args.Targets.Add(ent);
args.Targets.Add(ent.Comp.Host.Value);
}
}
public void UpdateChems(Entity<CorticalBorerComponent> ent, int change)
{
var (_, comp) = ent;
if (comp.ChemicalPoints + change >= comp.ChemicalPointCap)
comp.ChemicalPoints = comp.ChemicalPointCap;
else if (comp.ChemicalPoints + change <= 0)
comp.ChemicalPoints = 0;
else
comp.ChemicalPoints += change;
if (comp.ChemicalPoints % comp.UiUpdateInterval == 0)
UpdateUiState(ent);
_alerts.ShowAlert(ent, ent.Comp.ChemicalAlert);
Dirty(ent);
}
public void OnInfestHostAttempt(Entity<InventoryComponent> entity, ref InfestHostAttempt args)
{
IngestionBlockerComponent? blocker;
if (_inventory.TryGetSlotEntity(entity.Owner, "head", out var headUid) &&
TryComp(headUid, out blocker) &&
blocker.Enabled)
{
args.Blocker = headUid;
args.Cancel();
}
}
/// <summary>
/// Attempts to inject the Borer's host with chems
/// </summary>
public bool TryInjectHost(Entity<CorticalBorerComponent> ent,
CorticalBorerChemicalPrototype chemicalPrototype,
float chemAmount)
{
var (uid, comp) = ent;
// Need a host to inject something
if (!comp.Host.HasValue)
{
_popup.PopupEntity(Loc.GetString("cortical-borer-no-host"), uid, uid, PopupType.Medium);
return false;
}
// Sugar block from injecting stuff
if (!CanUseAbility(ent, comp.Host.Value))
return false;
// Make sure you can even hold the amount of chems you need
if (chemicalPrototype.Cost > comp.ChemicalPointCap)
{
_popup.PopupEntity(Loc.GetString("cortical-borer-not-enough-chem-storage"), uid, uid, PopupType.Medium);
return false;
}
// Make sure you have enough chems
if (chemicalPrototype.Cost > comp.ChemicalPoints)
{
_popup.PopupEntity(Loc.GetString("cortical-borer-not-enough-chem"), uid, uid, PopupType.Medium);
return false;
}
// no injecting things that don't have blood silly
if (!TryComp<BloodstreamComponent>(comp.Host, out var bloodstream))
return false;
var solution = new Solution();
solution.AddReagent(chemicalPrototype.Reagent, chemAmount);
// add the chemicals to the bloodstream of the host
if (!_blood.TryAddToChemicals(comp.Host.Value, solution, bloodstream))
return false;
UpdateChems(ent, -((int)chemAmount * chemicalPrototype.Cost));
return true;
}
private void OnInjectReagentMessage(Entity<CorticalBorerComponent> ent, ref CorticalBorerDispenserInjectMessage message)
{
CorticalBorerChemicalPrototype? chemProto = null;
foreach (var chem in _proto.EnumeratePrototypes<CorticalBorerChemicalPrototype>())
{
if (chem.Reagent.Equals(message.ChemProtoId))
{
chemProto = chem;
break;
}
}
if (chemProto != null)
TryInjectHost(ent, chemProto, ent.Comp.InjectAmount);
UpdateUiState(ent);
}
private void OnSetInjectAmountMessage(Entity<CorticalBorerComponent> ent, ref CorticalBorerDispenserSetInjectAmountMessage message)
{
ent.Comp.InjectAmount = message.CorticalBorerDispenserDispenseAmount;
UpdateUiState(ent);
}
private List<CorticalBorerDispenserItem> GetAllBorerChemicals(Entity<CorticalBorerComponent> ent)
{
var clones = new List<CorticalBorerDispenserItem>();
foreach (var prototype in _proto.EnumeratePrototypes<CorticalBorerChemicalPrototype>())
{
if (!_proto.TryIndex(prototype.Reagent, out ReagentPrototype? proto))
continue;
var reagentName = proto.LocalizedName;
var reagentId = proto.ID;
var cost = prototype.Cost;
var amount = ent.Comp.InjectAmount;
var chems = ent.Comp.ChemicalPoints;
var color = proto.SubstanceColor;
clones.Add(new CorticalBorerDispenserItem(reagentName,reagentId, cost, amount, chems, color)); // need color and name
}
return clones;
}
private void UpdateUiState(Entity<CorticalBorerComponent> ent)
{
var chems = GetAllBorerChemicals(ent);
var state = new CorticalBorerDispenserBoundUserInterfaceState(chems, (int)ent.Comp.InjectAmount);
_userInterfaceSystem.SetUiState(ent.Owner, CorticalBorerDispenserUiKey.Key, state);
}
public bool TryToggleCheckBlood(Entity<CorticalBorerComponent> ent)
{
if(!TryComp<UserInterfaceComponent>(ent, out var uic))
return false;
if (!TryComp<HealthAnalyzerComponent>(ent, out var health))
return false;
_ui.TryToggleUi((ent, uic), HealthAnalyzerUiKey.Key, ent);
if (health.ScannedEntity is null && ent.Comp.Host.HasValue)
OpenCheckBlood(ent, uic);
return true;
}
public void OpenCheckBlood(Entity<CorticalBorerComponent> ent, UserInterfaceComponent uic)
{
if (!ent.Comp.Host.HasValue)
return;
if (!TryComp<HealthAnalyzerComponent>(ent, out var health))
return;
if (!_ui.IsUiOpen((ent,uic), HealthAnalyzerUiKey.Key))
_ui.OpenUi((ent, uic), HealthAnalyzerUiKey.Key, ent);
_analyzer.BeginAnalyzingEntity((ent, health), ent.Comp.Host.Value);
}
public void CloseCheckBlood(Entity<CorticalBorerComponent> ent, UserInterfaceComponent uic)
{
if (!ent.Comp.Host.HasValue)
return;
if (!TryComp<HealthAnalyzerComponent>(ent, out var health))
return;
if(!health.ScannedEntity.HasValue)
return;
_ui.CloseUi((ent, uic), HealthAnalyzerUiKey.Key, ent);
_analyzer.StopAnalyzingEntity((ent, health), health.ScannedEntity.Value);
}
public void TakeControlHost(Entity<CorticalBorerComponent> ent, CorticalBorerInfestedComponent infestedComp)
{
var (worm, comp) = ent;
if (comp.Host is not { } host)
return;
// make sure they aren't dead, would throw the worm into a ghost mode and just kill em
if (TryComp<MobStateComponent>(ent.Comp.Host, out var mobState) &&
mobState.CurrentState == MobState.Dead)
return;
if (TryComp<MindContainerComponent>(host, out var mindContainer) &&
mindContainer.HasMind ||
HasComp<GhostRoleComponent>(host))
infestedComp.ControlTimeEnd = _timing.CurTime + comp.ControlDuration;
if (_mind.TryGetMind(worm, out var wormMind, out _))
infestedComp.BorerMindId = wormMind;
if (_mind.TryGetMind(host, out var controledMind, out _))
{
infestedComp.OrigininalMindId = controledMind; // set this var here just in case somehow the mind changes from when the infestation started
// fish head...
var dummy = Spawn("FoodMeatFish", MapCoordinates.Nullspace);
_container.Insert(dummy, infestedComp.ControlContainer);
_mind.TransferTo(controledMind, dummy);
}
else
{
infestedComp.OrigininalMindId = null;
}
comp.ControlingHost = true;
_mind.TransferTo(wormMind, host);
if (TryComp<GhostRoleComponent>(worm, out var ghostRole))
_ghost.UnregisterGhostRole((worm, ghostRole)); // prevent players from taking the worm role once mind isn't in the worm
// add the end control and vomit egg action
if (_actions.AddAction(host, "ActionEndControlHost") is {} actionEnd)
infestedComp.RemoveAbilities.Add(actionEnd);
if (comp.CanReproduce &&
infestedComp.ControlTimeEnd != null) // you can't lay eggs with something you can control forever
{
if (_actions.AddAction(host, "ActionLayEggHost") is {} actionLay)
infestedComp.RemoveAbilities.Add(actionLay);
}
var str = $"{ToPrettyString(worm)} has taken control over {ToPrettyString(host)}";
Log.Info(str);
_admin.Add(LogType.Mind, LogImpact.High, $"{ToPrettyString(worm)} has taken control over {ToPrettyString(host)}");
_chat.SendAdminAlert(str);
}
public void EndControl(Entity<CorticalBorerComponent> worm)
{
var (uid, comp) = worm;
if (comp.Host is not { } host)
return;
if (!TryComp<CorticalBorerInfestedComponent>(host, out var infestedComp))
return;
// not controlling anyone
if (!comp.ControlingHost)
return;
comp.ControlingHost = false;
// remove all the actions set to remove
foreach (var ability in infestedComp.RemoveAbilities)
{
_actions.RemoveAction(host, ability);
}
infestedComp.RemoveAbilities = new(); // clear out the list
if (TryComp<GhostRoleComponent>(worm, out var ghostRole))
_ghost.RegisterGhostRole((worm, ghostRole)); // re-enable the ghost role after you return to the body
// Return everyone to their own bodies
if (!TerminatingOrDeleted(infestedComp.BorerMindId))
_mind.TransferTo(infestedComp.BorerMindId, infestedComp.Borer);
if (!TerminatingOrDeleted(infestedComp.OrigininalMindId) && infestedComp.OrigininalMindId.HasValue)
_mind.TransferTo(infestedComp.OrigininalMindId.Value, host);
infestedComp.ControlTimeEnd = null;
_container.CleanContainer(infestedComp.ControlContainer);
}
private void OnMindRemoved(Entity<CorticalBorerComponent> ent, ref MindRemovedMessage args)
{
if (!ent.Comp.ControlingHost)
TryEjectBorer(ent); // No storing them in hosts if you don't have a soul
}
}

View File

@ -0,0 +1,63 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Alert.Components;
/// <summary>
/// This is used for an alert which simply displays a generic number over a texture.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class GenericCounterAlertComponent : Component
{
/// <summary>
/// The width, in pixels, of an individual glyph, accounting for the space between glyphs.
/// A 3 pixel wide glyph with one pixel of space between it and the next would be a width of 4.
/// </summary>
[DataField]
public int GlyphWidth = 6;
/// <summary>
/// Whether the numbers should be centered on the glyph or just follow a static position.
/// </summary>
[DataField]
public bool CenterGlyph = true;
/// <summary>
/// Whether leading zeros should be hidden.
/// If true, "005" would display as "5".
/// </summary>
[DataField]
public bool HideLeadingZeroes = true;
/// <summary>
/// The size of the alert sprite.
/// Used to calculate offsets.
/// </summary>
[DataField]
public Vector2i AlertSize = new(32, 32);
/// <summary>
/// Digits that can be displayed by the alert, represented by their sprite layer.
/// Order defined corresponds to the digit it affects. 1st defined will affect 1st digit, 2nd affect 2nd digit and so on.
/// In this case ones would be on layer "1", tens on layer "10" etc.
/// </summary>
[DataField]
public List<string> DigitKeys = new()
{
"1",
"10",
"100",
"1000",
"10000"
};
}
/// <summary>
/// Event raised to gather the amount the alert will display.
/// </summary>
/// <param name="Alert">The alert which is currently requesting an update.</param>
/// <param name="Amount">The number to display on the alert.</param>
[ByRefEvent]
public record struct GetGenericAlertCounterAmountEvent(AlertPrototype Alert, int? Amount = null)
{
public bool Handled => Amount.HasValue;
}

View File

@ -48,6 +48,12 @@ public sealed partial class GenericStatusEffect : EntityEffect
{
statusSys.TryAddStatusEffect(args.TargetEntity, Key, TimeSpan.FromSeconds(time), Refresh, Component);
}
// Begin Mono Changes - just add status without component
else if (Type == StatusEffectMetabolismType.Add)
{
statusSys.TryAddStatusEffect(args.TargetEntity, Key, TimeSpan.FromSeconds(time), Refresh);
}
// End Mono Changes - just add status without component
else if (Type == StatusEffectMetabolismType.Remove)
{
statusSys.TryRemoveTime(args.TargetEntity, Key, TimeSpan.FromSeconds(time));

View File

@ -104,7 +104,7 @@ public enum RevenantVisuals : byte
}
[NetSerializable, Serializable]
public enum RevenantVisualLayers : byte
public enum RevenantVisualLayers : byte // DeltaV - kept for impstation rev compatibility
{
Digit1,
Digit2,

View File

@ -0,0 +1,109 @@
// SPDX-FileCopyrightText: 2025 Coenx-flex
// SPDX-FileCopyrightText: 2025 Cojoke
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared.Alert;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
namespace Content.Shared._Mono.CorticalBorer;
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class CorticalBorerComponent : Component
{
/// <summary>
/// Host of this Borer
/// </summary>
[ViewVariables]
public EntityUid? Host = null;
/// <summary>
/// Current number of chemical points this Borer has, used to level up and buy chems
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[AutoNetworkedField]
[DataField]
public int ChemicalPoints = 50;
/// <summary>
/// Chemicals added every second WHILE IN A HOST
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField]
public int ChemicalGenerationRate = 1;
/// <summary>
/// Max Chemicals that can be held
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField]
public int ChemicalPointCap = 250;
/// <summary>
/// Reagent injection amount
/// </summary>
public int InjectAmount = 10;
/// <summary>
/// At what interval does the chem ui update
/// </summary>
public int UiUpdateInterval = 5; // every 6 to prevent constant update on cap
/// <summary>
/// The max duration you can take control of your host
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField]
public TimeSpan ControlDuration = TimeSpan.FromSeconds(40);
/// <summary>
/// Cooldown between chem regen events.
/// </summary>
public TimeSpan UpdateTimer = TimeSpan.Zero;
public float UpdateCooldown = 1f;
/// <summary>
/// Can this borer make more
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField]
public bool CanReproduce = true;
/// <summary>
/// What does it vomit out of its mouth when it lays an egg
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField]
public string EggProto = "CorticalBorerEgg";
/// <summary>
/// cost to lay an egg... will not update ability desc if changed
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField]
public int EggCost = 200;
[DataField]
public bool ControlingHost;
[DataField]
public ComponentRegistry? AddOnInfest;
[DataField]
public ComponentRegistry? RemoveOnInfest;
[DataField]
public ProtoId<AlertPrototype> ChemicalAlert = "Chemicals";
public readonly List<EntProtoId> InitialCorticalBorerActions = new()
{
"ActionCorticalBorerInfest",
"ActionCorticalBorerEject",
"ActionCorticalBorerChemMenu",
"ActionCheckBlood",
"ActionControlHost",
};
}

View File

@ -0,0 +1,50 @@
// SPDX-FileCopyrightText: 2025 Coenx-flex
// SPDX-FileCopyrightText: 2025 Cojoke
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Robust.Shared.GameStates;
using Robust.Shared.Containers;
namespace Content.Shared._Mono.CorticalBorer;
[RegisterComponent, NetworkedComponent]
public sealed partial class CorticalBorerInfestedComponent : Robust.Shared.GameObjects.Component
{
/// <summary>
/// Borer in the person
/// </summary>
[ViewVariables]
public Entity<CorticalBorerComponent> Borer = new();
/// <summary>
/// Container for borer
/// </summary>
public Container InfestationContainer = new();
/// <summary>
/// is the person under the borer's control
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public TimeSpan? ControlTimeEnd;
[ViewVariables]
public EntityUid? OrigininalMindId;
[ViewVariables]
public EntityUid BorerMindId;
/// <summary>
/// Where the mind gets hidden when the worm takes control
/// </summary>
public Container ControlContainer;
/// <summary>
/// Abilities to be removed once host gets control back
/// </summary>
public List<EntityUid> RemoveAbilities = new();
}
[RegisterComponent, NetworkedComponent]
public sealed partial class SurgeryCorticalBorerConditionComponent : Robust.Shared.GameObjects.Component;

View File

@ -0,0 +1,10 @@
// SPDX-FileCopyrightText: 2025 Coenx-flex
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Robust.Shared.GameStates;
namespace Content.Shared._Mono.CorticalBorer;
[RegisterComponent, NetworkedComponent]
public sealed partial class SurgeryStepRemoveCorticalBorerComponent : Component { }

View File

@ -0,0 +1,15 @@
// SPDX-FileCopyrightText: 2025 Coenx-flex
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared.Actions;
namespace Content.Shared._Mono.CorticalBorer;
public sealed partial class CorticalInfestEvent : EntityTargetActionEvent { }
public sealed partial class CorticalEjectEvent : InstantActionEvent { }
public sealed partial class CorticalChemMenuActionEvent : InstantActionEvent {}
public sealed partial class CorticalCheckBloodEvent : InstantActionEvent {}
public sealed partial class CorticalTakeControlEvent : InstantActionEvent {}
public sealed partial class CorticalEndControlEvent : InstantActionEvent {}
public sealed partial class CorticalLayEggEvent : InstantActionEvent {}

View File

@ -0,0 +1,11 @@
// SPDX-FileCopyrightText: 2025 Coenx-flex
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared.DoAfter;
using Robust.Shared.Serialization;
namespace Content.Shared._Mono.CorticalBorer;
[Serializable, NetSerializable]
public sealed partial class CorticalInfestDoAfterEvent : SimpleDoAfterEvent { }

View File

@ -0,0 +1,27 @@
// SPDX-FileCopyrightText: 2025 Coenx-flex
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using Content.Shared.Chemistry.Reagent;
using Robust.Shared.Prototypes;
namespace Content.Shared._Mono.CorticalBorer;
[Prototype("borerChemical")]
public sealed partial class CorticalBorerChemicalPrototype : IPrototype
{
[IdDataField]
public string ID { get; } = default!;
/// <summary>
/// Chemical cost per u of reagent
/// </summary>
[DataField]
public int Cost { get; set; } = 5;
/// <summary>
/// Reagent to inject into host
/// </summary>
[DataField]
public string Reagent { get; set; } = "";
}

View File

@ -0,0 +1,241 @@
// SPDX-FileCopyrightText: 2025 Coenx-flex
// SPDX-FileCopyrightText: 2025 Cojoke
//
// SPDX-License-Identifier: AGPL-3.0-or-later
using System.Linq;
using Content.Shared.Actions;
using Content.Shared.Body.Components;
using Content.Shared.Body.Part;
using Content.Shared.Body.Systems;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Examine;
using Content.Shared.MedicalScanner;
using Content.Shared.Popups;
using Content.Shared.StatusEffect;
using Content.Shared.Coordinates;
using Content.Shared.Damage;
using Content.Shared.IdentityManagement;
using Robust.Shared.Containers;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager;
namespace Content.Shared._Mono.CorticalBorer;
public partial class SharedCorticalBorerSystem : EntitySystem
{
[Dependency] private readonly SharedBodySystem _bodySystem = default!;
[Dependency] private readonly StatusEffectsSystem _statusEffects = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly ISerializationManager _serManager = default!;
[Dependency] private readonly DamageableSystem _damage = default!;
[Dependency] protected readonly SharedPopupSystem _popup = default!;
[Dependency] protected readonly SharedUserInterfaceSystem _ui = default!;
[Dependency] protected readonly SharedActionsSystem _actions = default!;
[Dependency] protected readonly SharedContainerSystem _container = default!;
public bool CanUseAbility(Entity<CorticalBorerComponent> ent, EntityUid target)
{
if (_statusEffects.HasStatusEffect(target,
"CorticalBorerProtection")) // hardcoded the status effect because...
{
_popup.PopupEntity(Loc.GetString("cortical-borer-sugar-block"), ent.Owner, ent.Owner, PopupType.Medium);
return false;
}
return true;
}
public void InfestTarget(Entity<CorticalBorerComponent> ent, EntityUid target)
{
var (uid, comp) = ent;
// Make sure the infected person is infected right
var infestedComp = EnsureComp<CorticalBorerInfestedComponent>(target);
// Make sure they get into the target
if (!_container.Insert(uid, infestedComp.InfestationContainer))
{
RemCompDeferred<CorticalBorerInfestedComponent>(target); // oh no it didn't work somehow so remove the comp you just added...
return;
}
// Set up the Borer
infestedComp.Borer = ent;
comp.Host = target;
if (comp.AddOnInfest is not null)
{
foreach (var (key, compReg) in comp.AddOnInfest)
{
var compType = compReg.Component.GetType();
if (HasComp(ent, compType))
continue;
var newComp = (Component) _serManager.CreateCopy(compReg.Component, notNullableOverride: true);
EntityManager.AddComponent(ent, newComp, true);
}
}
if (comp.RemoveOnInfest is not null)
{
foreach (var (key, compReg) in comp.RemoveOnInfest)
RemCompDeferred(ent, compReg.Component.GetType());
}
if (TryComp<DamageableComponent>(ent, out var damComp))
_damage.SetAllDamage(ent, damComp, 0);
}
public bool TryEjectBorer(Entity<CorticalBorerComponent> ent)
{
var (uid, comp) = ent;
if (ent.Comp.Host is not { } host)
return false;
// Make sure they get out of the host
if (!_container.TryRemoveFromContainer(uid))
return false;
// close all the UIs that relate to host
if (TryComp<UserInterfaceComponent>(ent, out var uic))
{
_ui.CloseUi((ent.Owner,uic), HealthAnalyzerUiKey.Key);
_ui.CloseUi((ent.Owner,uic), CorticalBorerDispenserUiKey.Key);
}
RemCompDeferred<CorticalBorerInfestedComponent>(ent.Comp.Host.Value);
ent.Comp.Host = null;
if (comp.RemoveOnInfest is not null)
{
foreach (var (key, compReg) in comp.RemoveOnInfest)
{
var compType = compReg.Component.GetType();
if (HasComp(ent, compType))
continue;
var newComp = (Component) _serManager.CreateCopy(compReg.Component, notNullableOverride: true);
EntityManager.AddComponent(ent, newComp, true);
}
}
if (comp.AddOnInfest is not null)
{
foreach (var (key, compReg) in comp.AddOnInfest)
RemCompDeferred(ent, compReg.Component.GetType());
}
return true;
}
public void LayEgg(Entity<CorticalBorerComponent> ent)
{
if (ent.Comp.Host is not { } host)
return;
if (ent.Comp.EggProto is not {} egg)
return;
var coordinates = _transform.ToMapCoordinates(host.ToCoordinates());
var spawnedEgg = Spawn(egg, coordinates);
}
}
public sealed class InfestHostAttempt : CancellableEntityEventArgs
{
/// <summary>
/// The equipment that is blocking the entrance
/// </summary>
public EntityUid? Blocker = null;
}
[Serializable, NetSerializable]
public enum CorticalBorerDispenserUiKey
{
Key
}
[Serializable, NetSerializable]
public sealed class CorticalBorerDispenserSetInjectAmountMessage : BoundUserInterfaceMessage
{
public readonly int CorticalBorerDispenserDispenseAmount;
public CorticalBorerDispenserSetInjectAmountMessage(int amount)
{
CorticalBorerDispenserDispenseAmount = amount;
}
public CorticalBorerDispenserSetInjectAmountMessage(String s)
{
switch (s)
{
case "1":
CorticalBorerDispenserDispenseAmount = 1;
break;
case "5":
CorticalBorerDispenserDispenseAmount = 5;
break;
case "10":
CorticalBorerDispenserDispenseAmount = 10;
break;
case "15":
CorticalBorerDispenserDispenseAmount = 15;
break;
case "20":
CorticalBorerDispenserDispenseAmount = 20;
break;
case "25":
CorticalBorerDispenserDispenseAmount = 25;
break;
case "30":
CorticalBorerDispenserDispenseAmount = 30;
break;
case "50":
CorticalBorerDispenserDispenseAmount = 50;
break;
case "100":
CorticalBorerDispenserDispenseAmount = 100;
break;
default:
throw new Exception($"Cannot convert the string `{s}` into a valid DispenseAmount");
}
}
}
[Serializable, NetSerializable]
public sealed class CorticalBorerDispenserInjectMessage : BoundUserInterfaceMessage
{
public readonly string ChemProtoId;
public CorticalBorerDispenserInjectMessage(string proto)
{
ChemProtoId = proto;
}
}
[Serializable, NetSerializable]
public sealed class CorticalBorerDispenserBoundUserInterfaceState : BoundUserInterfaceState
{
public readonly List<CorticalBorerDispenserItem> DisList;
public readonly int SelectedDispenseAmount;
public CorticalBorerDispenserBoundUserInterfaceState(List<CorticalBorerDispenserItem> disList, int dispenseAmount)
{
DisList = disList;
SelectedDispenseAmount = dispenseAmount;
}
}
[Serializable, NetSerializable]
public sealed class CorticalBorerDispenserItem(string reagentName, string reagentId, int cost, int amount, int chems, Color reagentColor)
{
public string ReagentName = reagentName;
public string ReagentId = reagentId;
public int Cost = cost;
public int Amount = amount;
public int Chems = chems;
public Color ReagentColor = reagentColor;
}

View File

@ -26,6 +26,7 @@ using Content.Shared.Popups;
using Robust.Shared.Prototypes;
using Robust.Shared.Toolshed.TypeParsers;
using System.Linq;
using Content.Shared._Mono.CorticalBorer; // Mono
namespace Content.Shared._Shitmed.Medical.Surgery;
@ -47,6 +48,7 @@ public abstract partial class SharedSurgerySystem
SubSurgery<SurgeryTendWoundsEffectComponent>(OnTendWoundsStep, OnTendWoundsCheck);
SubSurgery<SurgeryStepCavityEffectComponent>(OnCavityStep, OnCavityCheck);
SubSurgery<SurgeryStepRemoveCorticalBorerComponent>(OnCorticalBorerRemovalStep, OnCorticalBorerRemovalCheck); // Mono
SubSurgery<SurgeryAddPartStepComponent>(OnAddPartStep, OnAddPartCheck);
SubSurgery<SurgeryAffixPartStepComponent>(OnAffixPartStep, OnAffixPartCheck);
SubSurgery<SurgeryRemovePartStepComponent>(OnRemovePartStep, OnRemovePartCheck);
@ -427,6 +429,21 @@ public abstract partial class SharedSurgerySystem
args.Cancelled = true;
}
// Begin Mono Changes - borer
private void OnCorticalBorerRemovalStep(Entity<SurgeryStepRemoveCorticalBorerComponent> ent, ref SurgeryStepEvent args)
{
if (TryComp<CorticalBorerInfestedComponent>(args.Body, out var infested) &&
infested.InfestationContainer.ContainedEntities.Count != 0)
_corticalBorer.TryEjectBorer(infested.Borer);
}
private void OnCorticalBorerRemovalCheck(Entity<SurgeryStepRemoveCorticalBorerComponent> ent, ref SurgeryStepCompleteCheckEvent args)
{
if (HasComp<CorticalBorerInfestedComponent>(args.Body))
args.Cancelled = true;
}
// End Mono Changes - borer
private void OnAddPartStep(Entity<SurgeryAddPartStepComponent> ent, ref SurgeryStepEvent args)
{
if (!TryComp(args.Surgery, out SurgeryPartRemovedConditionComponent? removedComp))

View File

@ -1,4 +1,5 @@
using System.Linq;
using Content.Shared._Mono.CorticalBorer; // Mono
using Content.Shared._Shitmed.Medical.Surgery.Conditions;
using Content.Shared._Shitmed.Medical.Surgery.Effects.Complete;
using Content.Shared.Body.Systems;
@ -50,6 +51,7 @@ public abstract partial class SharedSurgerySystem : EntitySystem
[Dependency] private readonly StandingStateSystem _standing = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly TagSystem _tagSystem = default!; // DeltaV: surgery can operate through some clothing
[Dependency] private readonly SharedCorticalBorerSystem _corticalBorer = default!; // Mono
/// <summary>
/// Cache of all surgery prototypes' singleton entities.
@ -87,6 +89,7 @@ public abstract partial class SharedSurgerySystem : EntitySystem
SubscribeLocalEvent<SurgeryPartComponentConditionComponent, SurgeryValidEvent>(OnPartComponentConditionValid);
SubscribeLocalEvent<SurgeryOrganOnAddConditionComponent, SurgeryValidEvent>(OnOrganOnAddConditionValid);
//SubscribeLocalEvent<SurgeryRemoveLarvaComponent, SurgeryCompletedEvent>(OnRemoveLarva);
SubscribeLocalEvent<SurgeryCorticalBorerConditionComponent, SurgeryValidEvent>(OnCorticalBorerValid); // Mono
SubscribeLocalEvent<PrototypesReloadedEventArgs>(OnPrototypesReloaded);
InitializeSteps();
@ -161,6 +164,15 @@ public abstract partial class SharedSurgerySystem : EntitySystem
args.Cancelled = true;
}*/
// Begin Mono Changes - borer
private void OnCorticalBorerValid(Entity<SurgeryCorticalBorerConditionComponent> ent, ref SurgeryValidEvent args)
{
if (!HasComp<CorticalBorerInfestedComponent>(args.Body) ||
!HasComp<IncisionOpenComponent>(args.Part))
args.Cancelled = true;
}
// End Mono Changes - borer
private void OnBodyComponentConditionValid(Entity<SurgeryBodyComponentConditionComponent> ent, ref SurgeryValidEvent args)
{
var present = true;

View File

@ -0,0 +1,2 @@
alerts-borer-chemical-name = Chemicals
alerts-borer-chemical-desc = Chemicals made in your body, used for your abilities.

View File

@ -0,0 +1,28 @@
## Infest Messages
cortical-borer-has-host = You already have a host.
cortical-borer-host-already-infested = {THE($target)} is already infested.
cortical-borer-invalid-host = {THE($target)} is not a valid host.
cortical-borer-face-covered = {THE($target)}'s face is covered.
cortical-borer-headless = {THE($target)} does not have a head!
cortical-borer-start-infest = You begin to crawl into {THE($target)}.
## Generic messages
cortical-borer-no-host = You do not have a host.
cortical-borer-dead-host = Your host is dead.
cortical-borer-not-enough-chem = You do not have enough chemicals.
cortical-borer-not-enough-chem-storage = You cannot hold enough chemicals.
cortical-borer-sugar-block = You taste something sweet.
## Control messages
cortical-borer-already-control = You are already controlling your host.
cortical-borer-vomit = {$name} vomits out a {$egg}!
## UI
cortical-borer-dispenser-window-cost = {$cost} chemicals
cortical-borer-ghostrole-name = Cortical Borer
cortical-borer-ghostrole-desc = A space worm with the sole purpose in life to enter peoples heads and lay its eggs.
## Examine Text
infested-control-examined = You have [color=#d842fc]{$timeremaining}[/color] seconds left controlling this body.
cortical-borer-self-examine = You have [color=#d842fc]{$chempoints}[/color] chemicals.

View File

@ -0,0 +1 @@
surgery-popup-step-SurgeryStepRemoveCorticalBorer = {$user} is removing the Cortical Borer from {$target}'s {$part}!

View File

@ -1,4 +1,4 @@
- type: alert
- type: alert
id: Essence
category: Health #it's like ghostie health
icons:
@ -18,12 +18,15 @@
id: AlertEssenceSpriteView
categories: [ HideSpawnMenu ]
components:
- type: GenericCounterAlert
centerGlyph: false
hideLeadingZeroes: false
- type: Sprite
sprite: /Textures/Interface/Alerts/essence_counter.rsi
layers:
- map: [ "enum.AlertVisualLayers.Base" ]
- map: [ "enum.RevenantVisualLayers.Digit1" ]
- map: [ "enum.RevenantVisualLayers.Digit2" ]
offset: 0.125, 0
- map: [ "enum.RevenantVisualLayers.Digit3" ]
- map: [ "1" ]
offset: 0.25, 0
- map: [ "10" ]
offset: 0.125, 0
- map: [ "100" ]

View File

@ -42,6 +42,8 @@
- id: MobWhimperlet # DeltaV - added Whimperlet
weight: 0.03
#- id: MobMouseCancer # DeltaV - no
- id: MobCorticalBorer # DeltaV - ported Borer
weight: 0.015 # DeltaV - original probability was 0.001 but that seems WAY too low
# Events always spawn a critter regardless of Probability https://github.com/space-wizards/space-station-14/issues/28480 I added the Rat King to their own event with a player cap.
- type: entity
@ -96,6 +98,8 @@
weight: 0.21
- id: MobWhimperlet # DeltaV - Added Whimperlet and its weight
weight: 0.02
- id: MobCorticalBorer # DeltaV - ported Borer
weight: 0.015 # DeltaV - original probability was 0.001 but that seems WAY too low
- type: entity
id: SnailMigrationLowPop
@ -140,7 +144,9 @@
- id: MobSnail
weight: 0.84
- id: MobSnailSpeed
weight: 0.08
weight: 0.07 # DeltaV - was 0.08
- id: MobSnailMoth
weight: 0.08
weight: 0.07 # DeltaV - was 0.08
#- id: MobSnailInstantDeath # DeltaV - no
- id: MobCorticalBorer # DeltaV - ported Borer
weight: 0.02 # DeltaV - 2% chance on snails

View File

@ -90,6 +90,11 @@
reagent: Nutriment
min: 0.1
factor: 1
- !type:GenericStatusEffect # Mono change
key: CorticalBorerProtection
type: Add
time: 3 # May be too much, needs testing
refresh: false
plantMetabolism:
- !type:PlantAdjustNutrition
amount: 0.1

View File

@ -0,0 +1,87 @@
# SPDX-FileCopyrightText: 2025 Coenx-flex
# SPDX-FileCopyrightText: 2025 Cojoke
#
# SPDX-License-Identifier: AGPL-3.0-or-later
- type: entity # DeltaV - rework for action refactor
id: ActionCorticalBorerChemMenu
name: Chemicals
description: Open the chemicals menu.
components:
- type: Action
icon: _Mono/Interface/Action/borer_chem.png
- type: InstantAction
event: !type:CorticalChemMenuActionEvent
- type: entity # DeltaV - rework for action refactor
id: ActionCorticalBorerInfest
name: Infest
description: Infest target Humanoid. Target host must have a bloodstream.
components:
- type: Action
itemIconStyle: NoItem
icon: _Mono/Interface/Action/borer_infest.png
useDelay: 5
- type: TargetAction
interactOnMiss: false
- type: EntityTargetAction
whitelist:
components:
- Body
canTargetSelf: false
event: !type:CorticalInfestEvent {}
- type: entity # DeltaV - rework for action refactor
id: ActionCorticalBorerEject
name: Eject Host
description: Eject yourself from your host.
components:
- type: Action
icon: _Mono/Interface/Action/borer_eject.png
useDelay: 5
- type: InstantAction
event: !type:CorticalEjectEvent
- type: entity # DeltaV - rework for action refactor
id: ActionCheckBlood
name: Check Blood
description: Inspect the blood of your host to see how they are doing.
components:
- type: Action
icon: _Mono/Interface/Action/borer_bloodcheck.png
useDelay: 5
- type: InstantAction
event: !type:CorticalCheckBloodEvent
- type: entity # DeltaV - rework for action refactor
id: ActionControlHost
name: Take Control
description: Take full control of your host, lasts for 40 seconds on awake hosts.
components:
- type: Action
icon: _Mono/Interface/Action/borer_control.png
useDelay: 340 #5 minutes and 40 seconds
- type: InstantAction
event: !type:CorticalTakeControlEvent
- type: entity # DeltaV - rework for action refactor
id: ActionEndControlHost
name: Relinquish Control
description: Give up hold of the host's body early.
components:
- type: Action
icon: _Mono/Interface/Action/borer_eject.png
useDelay: 5
- type: InstantAction
event: !type:CorticalEndControlEvent
- type: entity # DeltaV - rework for action refactor
id: ActionLayEggHost
name: Lay Egg
description: Force your host to vomit an egg. Costs 200 chemicals. # needs to be changed if the base chem cost is changed
components:
- type: Action
icon: _Mono/Interface/Action/borer_egg.png
useDelay: 5
- type: InstantAction
event: !type:CorticalLayEggEvent

View File

@ -0,0 +1,28 @@
# SPDX-FileCopyrightText: 2025 Coenx-flex
# SPDX-FileCopyrightText: 2025 Cojoke
#
# SPDX-License-Identifier: AGPL-3.0-or-later
- type: alert
id: Chemicals
category: Battery
icons:
- sprite: /Textures/Interface/Alerts/generic_counter.rsi
state: base
alertViewEntity: AlertChemicalsSpriteView
name: alerts-borer-chemical-name
description: alerts-borer-chemical-desc
- type: entity
id: AlertChemicalsSpriteView
categories: [ HideSpawnMenu ]
components:
- type: GenericCounterAlert
hideLeadingZeroes: false
- type: Sprite
sprite: /Textures/Interface/Alerts/generic_counter.rsi
layers:
- map: [ "enum.AlertVisualLayers.Base" ]
- map: [ "1" ]
- map: [ "10" ]
- map: [ "100" ]

View File

@ -0,0 +1,64 @@
# SPDX-FileCopyrightText: 2025 Coenx-flex
#
# SPDX-License-Identifier: AGPL-3.0-or-later
- type: borerChemical
id: borerBicaridine
cost: 4
reagent: Bicaridine
- type: borerChemical
id: borerKelotane
cost: 4
reagent: Kelotane
- type: borerChemical
id: borerSaline
cost: 2
reagent: Saline
- type: borerChemical
id: borerEthanol
cost: 5
reagent: Ethanol
- type: borerChemical
id: borerMuteToxin
cost: 10 # 1u is 10 seconds, 1 chem per second of mute
reagent: MuteToxin
- type: borerChemical
id: borerCharcoal
cost: 3
reagent: Charcoal
- type: borerChemical
id: borerHappiness
cost: 5
reagent: Happiness
- type: borerChemical
id: borerEphedrine
cost: 10
reagent: Ephedrine
- type: borerChemical
id: borerNorepinephricAcid
cost: 2 # needs 20u of the acid to even start the blind
reagent: NorepinephricAcid
- type: borerChemical
id: borerSalbutamol # DeltaV - dex+ replaced with salbu
cost: 10
reagent: Salbutamol
- type: borerChemical
id: borerHeartbreakerToxin
cost: 10
reagent: HeartbreakerToxin
- type: borerChemical
id: borerNocturine
cost: 10 # DeltaV - need way less of it here to sleep, doubled price
reagent: Nocturine

View File

@ -0,0 +1,149 @@
# SPDX-FileCopyrightText: 2025 Ark
# SPDX-FileCopyrightText: 2025 Coenx-flex
# SPDX-FileCopyrightText: 2025 Cojoke
# SPDX-FileCopyrightText: 2025 Redrover1760
# SPDX-FileCopyrightText: 2025 ark1368
# SPDX-FileCopyrightText: 2025 tonotom
#
# SPDX-License-Identifier: AGPL-3.0-or-later
- type: entity
name: Cortical Borer
parent: SimpleSpaceMobBase
id: MobCorticalBorer
description: A worm that burrows into brains and lays its eggs. # DeltaV - added a period
components:
- type: GhostRole
makeSentient: true
allowSpeech: False
allowMovement: true
requirements:
- !type:OverallPlaytimeRequirement
time: 36000 # 10 hrs
name: cortical-borer-ghostrole-name
description: cortical-borer-ghostrole-desc
rules: ghost-role-information-freeagent-rules
mindRoles:
- MindRoleGhostRoleFreeAgent
- type: GhostTakeoverAvailable
- type: Sprite
drawdepth: SmallMobs
sprite: _Mono/Mobs/Aliens/cortical_borer.rsi
layers:
- map: [ "enum.DamageStateVisualLayers.Base" ]
state: cortical_borer
- type: DamageStateVisuals
states:
Alive:
Base: cortical_borer
Dead:
Base: dead
- type: Item
size: Tiny
heldPrefix: 0
- type: Physics
- type: Fixtures
fixtures:
fix1:
shape:
!type:PhysShapeCircle
radius: 0.2
density: 100
mask:
- SmallMobMask
layer:
- SmallMobLayer
- type: MobState
- type: MobThresholds
thresholds:
0: Alive
20: Dead
- type: Barotrauma # take away space immunity but still not gasp
damage:
types:
Blunt: 0.15
- type: Bloodstream
bloodlossDamage:
types:
Bloodloss: 0.5
bloodlossHealDamage:
types:
Bloodloss: -1
bleedReductionAmount: 2 # bleeding stops faster
- type: MovementSpeedModifier
baseWalkSpeed : 1.9
baseSprintSpeed : 7.5
- type: Tag
tags:
- Meat
- type: ThermalVision
color: "#ae65bf"
lightRadius: 15
activateSound: null
deactivateSound: null
- type: CorticalBorer
eggProto: "CorticalBorerEgg"
addOnInfest:
- type: PressureImmunity
removeOnInfest:
- type: CanEscapeInventory
- type: UserInterface
interfaces:
enum.CorticalBorerDispenserUiKey.Key:
type: CorticalBorerDispenserBoundUserInterface
enum.HealthAnalyzerUiKey.Key:
type: HealthAnalyzerBoundUserInterface
#- type: CollectiveMind # DeltaV - don't got it, maybe port it
# channel: CorticalBorer
- type: Speech
enabled: false
- type: HealthAnalyzer
scanningEndSound:
path: "/Audio/Voice/Slime/slime_squish.ogg"
maxScanRange: .5
- type: CanEscapeInventory
#- type: GuideHelp # DeltaV - nuh uh
# guides:
# - CorticalBorerGuide
- type: Body
#thermalVisibility: false # DeltaV - we dont got this (sad!)
#- type: UniversalLanguageSpeaker # DeltaV - no language (sad!)
- type: entity
parent: BaseItem
id: CorticalBorerEgg
name: cortical borer egg
description: This egg is so nice and wet and soft...
components:
- type: Sprite
sprite: _Mono/Mobs/Aliens/cortical_borer_egg.rsi
state: icon
- type: Item
size: Tiny
- type: GhostRole
name: cortical-borer-ghostrole-name
description: cortical-borer-ghostrole-desc
rules: ghost-role-information-freeagent-rules
requirements:
- !type:OverallPlaytimeRequirement
time: 36000 # 10 hrs
mindRoles:
- MindRoleGhostRoleFreeAgent
- type: GhostRoleMobSpawner
prototype: MobCorticalBorer
- type: Damageable
damageContainer: Biological
- type: Destructible
thresholds:
- trigger:
!type:DamageTrigger
damage: 1
behaviors:
- !type:PlaySoundBehavior
sound:
collection: desecration
- !type:DoActsBehavior
acts: [ "Destruction" ]
#- type: GuideHelp # DeltaV - nuh uh
# guides:
# - CorticalBorerGuide

View File

@ -0,0 +1,17 @@
- type: entity
parent: SurgeryBase
id: SurgeryCorticalBorerRemoval
name: Remove Cortical Borer
description: Removal the Cortical Borer infestation from the body.
categories: [ HideSpawnMenu ]
components:
- type: Surgery
priority: -1
requirement: SurgeryOpenIncision
steps:
- SurgeryStepSawBones
- SurgeryStepClampInternalBleeders
- SurgeryStepRemoveCorticalBorer
- type: SurgeryCorticalBorerCondition
- type: SurgeryPartCondition
part: Head

View File

@ -0,0 +1,15 @@
- type: entity
parent: SurgeryStepBase
id: SurgeryStepRemoveCorticalBorer
name: Remove the Cortical Borer
categories: [ HideSpawnMenu ]
components:
- type: SurgeryStep
tool:
- type: Tweezers
duration: 8
- type: Sprite
sprite: _Shitmed/Objects/Specific/Medical/Surgery/hemostat.rsi
state: hemostat
- type: SurgeryStepRemoveCorticalBorer
- type: SurgeryStepEmoteEffect

View File

@ -0,0 +1,7 @@
# SPDX-FileCopyrightText: 2025 Coenx-flex
#
# SPDX-License-Identifier: AGPL-3.0-or-later
- type: statusEffect
id: CorticalBorerProtection
alwaysAllowed: true

View File

@ -74,4 +74,16 @@
Slimes and spiders have no remarkable features, but will [color=cyan]infest the station[/color] from time to time regardless. Both will give chase and attack anything they see.
- Slimes may [bold]deal extra cellular or poison damage[/bold], based upon their color. Water hurts them just as it would hurt a slime person.
- Spiders have a venomous bite and can [bold]create webs[/bold] that are hard to move though. Webs are easily destroyed with a blade. They can also pry open doors, windoors, and airlocks.
<!-- DeltaV - start cortical borer changes--># Cortical Borer
<Box>
<GuideEntityEmbed Entity="MobCorticalBorer"/>
</Box>
Cortical Borers are a worm-like parasitic species that inhabit the sector. They burrow into one's brain and feed off the body. It's said they can speak to their host, often to try to persuade them to do certain things for unknown reasons.
- As a Borer, you can view the health of your host, and inject them with harmful or helpful chemicals. When in a host, you regain chemicals over time, to a maximum of 250.
- You can additionally [color=yellow]control[/color] your host for up to 40 seconds, during which time you can [color=yellow]lay an egg[/color] if you have at least 200 chemicals stored. Said egg can hatch into a new borer once it's ready...
- A borer's abilities will temporarily stop working if the host ingests sugar. The borer can be removed from a host via surgery.
<!-- DeltaV - end cortical borer changes-->
</Document>

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 B

View File

@ -0,0 +1,44 @@
{
"version": 1,
"license": "CC-BY-SA-3.0",
"copyright": "Created by EmoGarbage404",
"size": {
"x": 32,
"y": 32
},
"states": [
{
"name": "base"
},
{
"name": "0"
},
{
"name": "1"
},
{
"name": "2"
},
{
"name": "3"
},
{
"name": "4"
},
{
"name": "5"
},
{
"name": "6"
},
{
"name": "7"
},
{
"name": "8"
},
{
"name": "9"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 631 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 670 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 835 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 309 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 583 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 810 B

View File

@ -0,0 +1,29 @@
{
"version": 1,
"size": {
"x": 32,
"y": 32
},
"license": "CC-BY-SA-3.0",
"copyright": "borer actions taken from Monkestation https://github.com/Monkestation/Monkestation2.0/pull/976, edited by Cojoke",
"states": [
{
"name": "borer_chem"
},
{
"name": "borer_infest"
},
{
"name": "borer_bloodcheck"
},
{
"name": "borer_eject"
},
{
"name": "borer_egg"
},
{
"name": "borer_control"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 696 B

View File

@ -0,0 +1,18 @@
{
"version": 1,
"license": "CC-BY-NC-SA-3.0",
"copyright": "Taken from Monkestation https://github.com/Monkestation/Monkestation2.0/pull/976",
"size": {
"x": 32,
"y": 32
},
"states": [
{
"name": "cortical_borer",
"directions": 4
},
{
"name": "dead"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 309 B

View File

@ -0,0 +1,14 @@
{
"version": 1,
"license": "CC-BY-NC-SA-3.0",
"copyright": "Taken from Monkestation https://github.com/Monkestation/Monkestation2.0/pull/976",
"size": {
"x": 32,
"y": 32
},
"states": [
{
"name": "icon"
}
]
}