From dabf4ee820e7c7183333427b8139924a2b348fe4 Mon Sep 17 00:00:00 2001
From: MataVsn <113342160+MataVsn@users.noreply.github.com>
Date: Mon, 18 May 2026 22:49:48 +0300
Subject: [PATCH 01/13] Base
---
.../Clothing/ClientClothingSystem.cs | 1 +
.../Lobby/UI/HumanoidProfileEditor.xaml | 7 +
.../Lobby/UI/HumanoidProfileEditor.xaml.cs | 37 +++
.../SharedGoobHumanoidAppearanceSystem.cs | 1 +
Content.Server/_Arcane/ERP/ArousalSystem.cs | 210 ++++++++++++++++++
.../_Arcane/ERP/EroticOrganSpawnSystem.cs | 117 ++++++++++
.../_Arcane/ERP/ErpExamineSystem.cs | 49 ++++
Content.Server/_Arcane/ERP/ErpStatusSystem.cs | 34 +++
.../Humanoid/HumanoidVisualLayersExtension.cs | 4 +-
.../Humanoid/Prototypes/SpeciesPrototype.cs | 2 +-
Content.Shared/Humanoid/Sex.cs | 1 +
.../Preferences/HumanoidCharacterProfile.cs | 15 ++
.../_Arcane/ERP/ArousalComponent.cs | 98 ++++++++
Content.Shared/_Arcane/ERP/Erp.cs | 20 ++
.../_Arcane/ERP/ErpStatusComponent.cs | 10 +
.../ERP/Organs/EroticOrganComponent.cs | 26 +++
.../_Arcane/ERP/Organs/EroticOrganTypes.cs | 24 ++
.../Pain/Components/NerveSystemComponent.cs | 42 ++++
Resources/Locale/en-US/_Arcane/ERP/erp.ftl | 16 ++
.../ui/humanoid-profile-editor.ftl | 1 +
Resources/Locale/ru-RU/_Arcane/ERP/erp.ftl | 16 ++
.../ru-RU/_Arcane/body/Organs/erotic.ftl | 17 ++
.../ui/humanoid-profile-editor.ftl | 1 +
.../Prototypes/_Arcane/Alerts/arousal.yml | 22 ++
.../Prototypes/_Arcane/Body/Organs/erotic.yml | 107 +++++++++
.../Prototypes/_Arcane/Species/demon.yml | 2 +-
.../_EinsteinEngines/Species/shadowkin.yml | 1 +
.../_Floofstation/Species/resomi.yml | 1 +
.../Textures/_Arcane/Interface/heartIcon.png | Bin 0 -> 362 bytes
29 files changed, 879 insertions(+), 3 deletions(-)
create mode 100644 Content.Server/_Arcane/ERP/ArousalSystem.cs
create mode 100644 Content.Server/_Arcane/ERP/EroticOrganSpawnSystem.cs
create mode 100644 Content.Server/_Arcane/ERP/ErpExamineSystem.cs
create mode 100644 Content.Server/_Arcane/ERP/ErpStatusSystem.cs
create mode 100644 Content.Shared/_Arcane/ERP/ArousalComponent.cs
create mode 100644 Content.Shared/_Arcane/ERP/Erp.cs
create mode 100644 Content.Shared/_Arcane/ERP/ErpStatusComponent.cs
create mode 100644 Content.Shared/_Arcane/ERP/Organs/EroticOrganComponent.cs
create mode 100644 Content.Shared/_Arcane/ERP/Organs/EroticOrganTypes.cs
create mode 100644 Resources/Locale/en-US/_Arcane/ERP/erp.ftl
create mode 100644 Resources/Locale/ru-RU/_Arcane/ERP/erp.ftl
create mode 100644 Resources/Locale/ru-RU/_Arcane/body/Organs/erotic.ftl
create mode 100644 Resources/Prototypes/_Arcane/Alerts/arousal.yml
create mode 100644 Resources/Prototypes/_Arcane/Body/Organs/erotic.yml
create mode 100644 Resources/Textures/_Arcane/Interface/heartIcon.png
diff --git a/Content.Client/Clothing/ClientClothingSystem.cs b/Content.Client/Clothing/ClientClothingSystem.cs
index bd1b4c40a4f..713596e5059 100644
--- a/Content.Client/Clothing/ClientClothingSystem.cs
+++ b/Content.Client/Clothing/ClientClothingSystem.cs
@@ -337,6 +337,7 @@ private void RenderEquipment(EntityUid equipee, EntityUid equipment, string slot
displacementData = inventory.MaleDisplacements.GetValueOrDefault(slot);
break;
case Sex.Female:
+ case Sex.Futanari: // Arcane-edit
if (inventory.FemaleDisplacements.Count > 0)
displacementData = inventory.FemaleDisplacements.GetValueOrDefault(slot);
break;
diff --git a/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml b/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml
index 6498adb1472..913a0597cd2 100644
--- a/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml
+++ b/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml
@@ -119,6 +119,13 @@ SPDX-License-Identifier: AGPL-3.0-or-later
+
+
+
+
+
+
+
diff --git a/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs b/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs
index 6a87eefb488..aa20b30e5f3 100644
--- a/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs
+++ b/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs
@@ -195,6 +195,7 @@
using Direction = Robust.Shared.Maths.Direction;
using Content.Goobstation.Common.CCVar; // Goob Station - Barks
using Content.Goobstation.Common.Barks; // Goob Station - Barks
+using Content.Shared._Arcane.ERP; // Arcane-edit
namespace Content.Client.Lobby.UI
{
[GenerateTypedNameReferences]
@@ -627,6 +628,23 @@ public HumanoidProfileEditor(
#endregion SpawnPriority
+ // Arcane-Start
+ #region ErpPreference
+
+ foreach (var value in Enum.GetValues())
+ {
+ ErpPreferenceButton.AddItem(Loc.GetString($"humanoid-profile-editor-erp-preference-{value.ToString().ToLower()}"), (int) value);
+ }
+
+ ErpPreferenceButton.OnItemSelected += args =>
+ {
+ ErpPreferenceButton.SelectId(args.Id);
+ SetErpPreference((ErpPreference) args.Id);
+ };
+
+ #endregion ErpPreference
+ // Arcane-End
+
#region Eyes
EyeColorPicker.OnEyeColorPicked += newColor =>
@@ -1349,6 +1367,7 @@ public void SetProfile(HumanoidCharacterProfile? profile, int? slot)
UpdateGenderControls();
UpdateSkinColor();
UpdateSpawnPriorityControls();
+ UpdateErpPreferenceControls(); // Arcane-edit
UpdateAgeEdit();
UpdateEyePickers();
UpdateSaveButton();
@@ -1958,6 +1977,14 @@ private void SetSpawnPriority(SpawnPriorityPreference newSpawnPriority)
SetDirty();
}
+ // Arcane-Start
+ private void SetErpPreference(ErpPreference preference)
+ {
+ Profile = Profile?.WithErpPreference(preference);
+ SetDirty();
+ }
+ // Arcane-End
+
// Goob Station - Start
private void SetProfileHeight(float height)
{
@@ -2226,6 +2253,16 @@ private void UpdateSpawnPriorityControls()
SpawnPriorityButton.SelectId((int) Profile.SpawnPriority);
}
+ // Arcane-Start
+ private void UpdateErpPreferenceControls()
+ {
+ if (Profile == null)
+ return;
+
+ ErpPreferenceButton.SelectId((int) Profile.ErpPreference);
+ }
+ // Arcane-End
+
// begin Goobstation: port EE height/width sliders
private void UpdateHeightWidthSliders()
{
diff --git a/Content.Goobstation.Shared/Humanoid/SharedGoobHumanoidAppearanceSystem.cs b/Content.Goobstation.Shared/Humanoid/SharedGoobHumanoidAppearanceSystem.cs
index a648dfc1b5f..ba5ecf1269e 100644
--- a/Content.Goobstation.Shared/Humanoid/SharedGoobHumanoidAppearanceSystem.cs
+++ b/Content.Goobstation.Shared/Humanoid/SharedGoobHumanoidAppearanceSystem.cs
@@ -22,6 +22,7 @@ public void SwapSex(EntityUid uid, HumanoidAppearanceComponent? humanoid = null)
switch (humanoid.Sex)
{
case Sex.Unsexed:
+ case Sex.Futanari: // Arcane-edit: no meaningful swap
default: break;
case Sex.Male: newGender = Gender.Female; newSex = Sex.Female; break;
case Sex.Female: newGender = Gender.Male; newSex = Sex.Male; break;
diff --git a/Content.Server/_Arcane/ERP/ArousalSystem.cs b/Content.Server/_Arcane/ERP/ArousalSystem.cs
new file mode 100644
index 00000000000..33152f8d415
--- /dev/null
+++ b/Content.Server/_Arcane/ERP/ArousalSystem.cs
@@ -0,0 +1,210 @@
+using Content.Shared._Arcane.ERP;
+using Content.Shared.Alert;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Timing;
+
+namespace Content.Server._Arcane.ERP;
+
+public sealed class ArousalSystem : EntitySystem
+{
+ [Dependency] private readonly IGameTiming _timing = default!;
+ [Dependency] private readonly AlertsSystem _alerts = default!;
+
+ private static readonly TimeSpan PhaseCheckRate = TimeSpan.FromSeconds(5);
+
+ private static readonly ProtoId AlertCategory = "Arousal";
+ private static readonly ProtoId AlertAroused = "ArousalAroused";
+ private static readonly ProtoId AlertHeated = "ArousalHeated";
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(OnInit);
+ SubscribeLocalEvent(OnErpPreferenceChanged);
+ }
+
+ private void OnInit(EntityUid uid, ArousalComponent comp, ComponentInit args)
+ {
+ SetArousal((uid, comp), 0f);
+ }
+
+ private void OnErpPreferenceChanged(EntityUid uid, ArousalComponent comp, ErpPreferenceChangedEvent args)
+ {
+ if (args.NewPreference != ErpPreference.No)
+ return;
+
+ comp.PassiveSources.Clear();
+ SetArousal((uid, comp), 0f);
+ }
+
+ ///
+ /// Returns current arousal accounting for passive gain/decay since last authoritative set.
+ /// Passive gain is suppressed during the refractory period.
+ ///
+ public float GetArousal(ArousalComponent comp)
+ {
+ var sinceChange = _timing.CurTime - comp.LastChangeTime;
+ var elapsed = (float)sinceChange.TotalSeconds;
+ var passiveRate = IsRefractory(comp) ? 0f : comp.PassiveGainRate;
+ var netRate = passiveRate - comp.DecayRate;
+ return Math.Clamp(comp.LastValue + netRate * elapsed, 0f, comp.MaxArousal);
+ }
+
+ public bool IsRefractory(ArousalComponent comp)
+ {
+ return _timing.CurTime < comp.RefractoryUntil;
+ }
+
+ public void SetPassiveSource(EntityUid uid, string sourceId, float rate, ArousalComponent? comp = null)
+ {
+ if (!Resolve(uid, ref comp))
+ return;
+
+ if (IsErpDisabled(uid))
+ return;
+
+ // Snapshot current value before changing rate to prevent retroactive application.
+ comp.LastValue = GetArousal(comp);
+ comp.LastChangeTime = _timing.CurTime;
+ comp.PassiveSources[sourceId] = rate;
+ Dirty(uid, comp);
+ }
+
+ public void RemovePassiveSource(EntityUid uid, string sourceId, ArousalComponent? comp = null)
+ {
+ if (!Resolve(uid, ref comp))
+ return;
+
+ // Snapshot current value before removing source to prevent retroactive application.
+ comp.LastValue = GetArousal(comp);
+ comp.LastChangeTime = _timing.CurTime;
+ comp.PassiveSources.Remove(sourceId);
+ Dirty(uid, comp);
+ }
+
+ public void AddArousal(EntityUid uid, float amount, ArousalComponent? comp = null)
+ {
+ if (!Resolve(uid, ref comp))
+ return;
+
+ if (IsErpDisabled(uid))
+ return;
+
+ if (IsRefractory(comp))
+ return;
+
+ RaiseLocalEvent(uid, new ArousedEvent(amount));
+ SetArousal((uid, comp), GetArousal(comp) + amount);
+ }
+
+ public void ReduceArousal(EntityUid uid, float amount, ArousalComponent? comp = null)
+ {
+ if (!Resolve(uid, ref comp))
+ return;
+
+ SetArousal((uid, comp), GetArousal(comp) - amount);
+ }
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ var now = _timing.CurTime;
+ var query = EntityQueryEnumerator();
+ while (query.MoveNext(out var uid, out var comp))
+ {
+ if (now < comp.NextPhaseCheckAt)
+ continue;
+
+ comp.NextPhaseCheckAt = now + PhaseCheckRate;
+ UpdatePhase((uid, comp));
+ }
+ }
+
+ private void SetArousal(Entity entity, float value)
+ {
+ var comp = entity.Comp;
+ value = Math.Clamp(value, 0f, comp.MaxArousal);
+
+ comp.LastValue = value;
+ comp.LastChangeTime = _timing.CurTime;
+ Dirty(entity.Owner, comp);
+
+ UpdatePhase(entity);
+ }
+
+ private void UpdatePhase(Entity entity)
+ {
+ var comp = entity.Comp;
+ var newPhase = comp.ComputePhase(GetArousal(comp));
+ if (newPhase == comp.CurrentPhase)
+ return;
+
+ var previous = comp.CurrentPhase;
+ comp.CurrentPhase = newPhase;
+
+ if (newPhase == ArousalPhase.Peak)
+ {
+ // Reset arousal inline to avoid re-entering UpdatePhase via SetArousal.
+ // Alerts go straight to Calm — no single-tick Peak flash.
+ comp.LastOrgasmAt = _timing.CurTime;
+ comp.RefractoryUntil = _timing.CurTime + comp.RefractoryDuration;
+ comp.LastValue = 0f;
+ comp.LastChangeTime = _timing.CurTime;
+ comp.CurrentPhase = ArousalPhase.Calm;
+ Dirty(entity.Owner, comp);
+
+ UpdateAlerts(entity.Owner, ArousalPhase.Calm);
+ RaiseLocalEvent(entity.Owner, new ArousalPhaseChangedEvent(previous, ArousalPhase.Peak));
+ RaiseLocalEvent(entity.Owner, new ArousalOrgasmEvent());
+ RaiseLocalEvent(entity.Owner, new ArousalPhaseChangedEvent(ArousalPhase.Peak, ArousalPhase.Calm));
+ }
+ else
+ {
+ Dirty(entity.Owner, comp);
+ UpdateAlerts(entity.Owner, newPhase);
+ RaiseLocalEvent(entity.Owner, new ArousalPhaseChangedEvent(previous, newPhase));
+ }
+ }
+
+ private void UpdateAlerts(EntityUid uid, ArousalPhase phase)
+ {
+ switch (phase)
+ {
+ case ArousalPhase.Aroused:
+ _alerts.ShowAlert(uid, AlertAroused);
+ break;
+ case ArousalPhase.Heated:
+ _alerts.ShowAlert(uid, AlertHeated);
+ break;
+ default:
+ _alerts.ClearAlertCategory(uid, AlertCategory);
+ break;
+ }
+ }
+
+ private bool IsErpDisabled(EntityUid uid)
+ {
+ return TryComp(uid, out var status)
+ && status.Preference == ErpPreference.No;
+ }
+}
+
+public sealed class ArousedEvent(float amount) : EntityEventArgs
+{
+ public float Amount = amount;
+}
+
+public sealed class ArousalPhaseChangedEvent : EntityEventArgs
+{
+ public ArousalPhase Previous;
+ public ArousalPhase Current;
+
+ public ArousalPhaseChangedEvent(ArousalPhase previous, ArousalPhase current)
+ {
+ Previous = previous;
+ Current = current;
+ }
+}
+
+public sealed class ArousalOrgasmEvent : EntityEventArgs { }
diff --git a/Content.Server/_Arcane/ERP/EroticOrganSpawnSystem.cs b/Content.Server/_Arcane/ERP/EroticOrganSpawnSystem.cs
new file mode 100644
index 00000000000..977e28618c8
--- /dev/null
+++ b/Content.Server/_Arcane/ERP/EroticOrganSpawnSystem.cs
@@ -0,0 +1,117 @@
+using Content.Shared._Arcane.ERP.Organs;
+using Content.Shared._Shitmed.Humanoid.Events;
+using Content.Shared.Body.Organ;
+using Content.Shared.Body.Part;
+using Content.Shared.Body.Systems;
+using Content.Shared.Humanoid;
+using Robust.Shared.Containers;
+using Robust.Shared.Prototypes;
+
+namespace Content.Server._Arcane.ERP;
+
+public sealed class EroticOrganSpawnSystem : EntitySystem
+{
+ [Dependency] private readonly SharedBodySystem _body = default!;
+ [Dependency] private readonly IPrototypeManager _proto = default!;
+ [Dependency] private readonly SharedContainerSystem _containers = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(OnProfileLoaded);
+ SubscribeLocalEvent(OnSexChanged);
+ }
+
+ private void OnProfileLoaded(EntityUid uid, HumanoidAppearanceComponent humanoid, ProfileLoadFinishedEvent args)
+ {
+ SpawnEroticOrgans(uid, humanoid.Sex);
+ }
+
+ private void OnSexChanged(EntityUid uid, HumanoidAppearanceComponent humanoid, SexChangedEvent args)
+ {
+ RemoveEroticOrgans(uid);
+ SpawnEroticOrgans(uid, args.NewSex);
+ }
+
+ private void SpawnEroticOrgans(EntityUid uid, Sex sex)
+ {
+ if (sex == Sex.Unsexed)
+ return;
+
+ var groin = GetBodyPartOfType(uid, BodyPartType.Groin);
+ var chest = GetBodyPartOfType(uid, BodyPartType.Chest);
+
+ if (groin.HasValue)
+ TrySpawnOrgan(uid, groin.Value, "OrganAnus", "anus");
+
+ switch (sex)
+ {
+ case Sex.Male:
+ if (groin.HasValue)
+ {
+ TrySpawnOrgan(uid, groin.Value, "OrganPenis", "penis");
+ TrySpawnOrgan(uid, groin.Value, "OrganTesticles", "testicles");
+ }
+ break;
+
+ case Sex.Female:
+ if (groin.HasValue)
+ {
+ TrySpawnOrgan(uid, groin.Value, "OrganVagina", "vagina");
+ TrySpawnOrgan(uid, groin.Value, "OrganUterus", "uterus");
+ }
+ if (chest.HasValue)
+ TrySpawnOrgan(uid, chest.Value, "OrganBreasts", "breasts");
+ break;
+
+ case Sex.Futanari:
+ if (groin.HasValue)
+ {
+ TrySpawnOrgan(uid, groin.Value, "OrganPenis", "penis");
+ TrySpawnOrgan(uid, groin.Value, "OrganTesticles", "testicles");
+ TrySpawnOrgan(uid, groin.Value, "OrganVagina", "vagina");
+ TrySpawnOrgan(uid, groin.Value, "OrganUterus", "uterus");
+ }
+ if (chest.HasValue)
+ TrySpawnOrgan(uid, chest.Value, "OrganBreasts", "breasts");
+ break;
+ }
+ }
+
+ private void RemoveEroticOrgans(EntityUid bodyUid)
+ {
+ var organs = _body.GetBodyOrganEntityComps((bodyUid, null));
+ foreach (var organ in organs)
+ {
+ _body.RemoveOrgan(organ.Owner, organ.Comp2);
+ QueueDel(organ.Owner);
+ }
+ }
+
+ private void TrySpawnOrgan(EntityUid bodyUid, EntityUid partUid, string protoId, string slotId)
+ {
+ if (!_proto.HasIndex(protoId))
+ return;
+
+ // Create slot if it doesn't exist yet
+ _body.TryCreateOrganSlot(partUid, slotId, out _);
+
+ // Don't double-spawn
+ var containerId = SharedBodySystem.GetOrganContainerId(slotId);
+ if (_containers.TryGetContainer(partUid, containerId, out var container)
+ && container.ContainedEntities.Count > 0)
+ return;
+
+ var organEnt = Spawn(protoId, Transform(partUid).Coordinates);
+ if (!_body.InsertOrgan(partUid, organEnt, slotId))
+ QueueDel(organEnt);
+ }
+
+ private EntityUid? GetBodyPartOfType(EntityUid bodyUid, BodyPartType partType)
+ {
+ foreach (var (partUid, _) in _body.GetBodyChildrenOfType(bodyUid, partType))
+ return partUid;
+
+ return null;
+ }
+}
diff --git a/Content.Server/_Arcane/ERP/ErpExamineSystem.cs b/Content.Server/_Arcane/ERP/ErpExamineSystem.cs
new file mode 100644
index 00000000000..85ee4934005
--- /dev/null
+++ b/Content.Server/_Arcane/ERP/ErpExamineSystem.cs
@@ -0,0 +1,49 @@
+using Content.Shared._Arcane.ERP;
+using Content.Shared.Examine;
+using Content.Shared.Verbs;
+using Robust.Shared.Utility;
+
+namespace Content.Server._Arcane.ERP;
+
+public sealed class ErpExamineSystem : EntitySystem
+{
+ [Dependency] private readonly ExamineSystemShared _examine = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent>(OnGetExamineVerbs);
+ }
+
+ private void OnGetExamineVerbs(EntityUid uid, ErpStatusComponent comp, GetVerbsEvent args)
+ {
+ var inRange = _examine.IsInDetailsRange(args.User, uid);
+
+ var verb = new ExamineVerb
+ {
+ Act = () =>
+ {
+ var statusKey = comp.Preference switch
+ {
+ ErpPreference.Yes => "erp-examine-status-yes",
+ ErpPreference.Ask => "erp-examine-status-ask",
+ _ => "erp-examine-status-no",
+ };
+
+ var msg = new FormattedMessage();
+ msg.AddMarkupOrThrow(Loc.GetString("erp-examine-status-header"));
+ msg.PushNewline();
+ msg.AddMarkupOrThrow(Loc.GetString(statusKey));
+
+ _examine.SendExamineTooltip(args.User, uid, msg, false, false);
+ },
+ Text = Loc.GetString("erp-examine-verb-text"),
+ Category = VerbCategory.Examine,
+ Disabled = !inRange,
+ Message = inRange ? null : Loc.GetString("erp-examine-verb-disabled"),
+ Icon = new SpriteSpecifier.Texture(new("/Textures/_Arcane/Interface/heartIcon.png")),
+ };
+
+ args.Verbs.Add(verb);
+ }
+}
diff --git a/Content.Server/_Arcane/ERP/ErpStatusSystem.cs b/Content.Server/_Arcane/ERP/ErpStatusSystem.cs
new file mode 100644
index 00000000000..6e7bccd54db
--- /dev/null
+++ b/Content.Server/_Arcane/ERP/ErpStatusSystem.cs
@@ -0,0 +1,34 @@
+using Content.Server.Preferences.Managers;
+using Content.Shared._Arcane.ERP;
+using Content.Shared.Humanoid;
+using Content.Shared.Preferences;
+using Robust.Shared.Player;
+
+namespace Content.Server._Arcane.ERP;
+
+public sealed class ErpStatusSystem : EntitySystem
+{
+ [Dependency] private readonly IServerPreferencesManager _prefs = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(OnPlayerAttached);
+ }
+
+ private void OnPlayerAttached(EntityUid uid, HumanoidAppearanceComponent humanoid, ref PlayerAttachedEvent args)
+ {
+ var profile = _prefs.GetPreferences(args.Player.UserId).SelectedCharacter as HumanoidCharacterProfile;
+ var preference = profile?.ErpPreference ?? ErpPreference.Ask;
+
+ EnsureComp(uid);
+
+ var comp = EnsureComp(uid);
+ var oldPreference = comp.Preference;
+ comp.Preference = preference;
+ Dirty(uid, comp);
+
+ if (oldPreference != preference)
+ RaiseLocalEvent(uid, new ErpPreferenceChangedEvent(oldPreference, preference));
+ }
+}
diff --git a/Content.Shared/Humanoid/HumanoidVisualLayersExtension.cs b/Content.Shared/Humanoid/HumanoidVisualLayersExtension.cs
index c4d514a6e4b..bf5719f27d0 100644
--- a/Content.Shared/Humanoid/HumanoidVisualLayersExtension.cs
+++ b/Content.Shared/Humanoid/HumanoidVisualLayersExtension.cs
@@ -38,7 +38,9 @@ public static string GetSexMorph(HumanoidVisualLayers layer, Sex sex, string id)
if (!HasSexMorph(layer) || sex == Sex.Unsexed)
return id;
- return $"{id}{sex}";
+ // Futanari uses female sprite layers — no separate Futanari layer prototypes exist - Arcane-edit.
+ var visualSex = sex == Sex.Futanari ? Sex.Female : sex; // Arcane-edit
+ return $"{id}{visualSex}";
}
///
diff --git a/Content.Shared/Humanoid/Prototypes/SpeciesPrototype.cs b/Content.Shared/Humanoid/Prototypes/SpeciesPrototype.cs
index f6079166147..e71d7c928a6 100644
--- a/Content.Shared/Humanoid/Prototypes/SpeciesPrototype.cs
+++ b/Content.Shared/Humanoid/Prototypes/SpeciesPrototype.cs
@@ -130,7 +130,7 @@ public sealed partial class SpeciesPrototype : IPrototype
public SpeciesNaming Naming { get; private set; } = SpeciesNaming.FirstLast;
[DataField]
- public List Sexes { get; private set; } = new() { Sex.Male, Sex.Female };
+ public List Sexes { get; private set; } = new() { Sex.Male, Sex.Female, Sex.Futanari }; // Arcane-edit
///
/// Characters younger than this are too young to be hired by Nanotrasen.
diff --git a/Content.Shared/Humanoid/Sex.cs b/Content.Shared/Humanoid/Sex.cs
index 8c40c6fa191..d0aa1e63790 100644
--- a/Content.Shared/Humanoid/Sex.cs
+++ b/Content.Shared/Humanoid/Sex.cs
@@ -24,6 +24,7 @@ public enum Sex : byte
Male,
Female,
Unsexed,
+ Futanari, // Arcane-edit
}
///
diff --git a/Content.Shared/Preferences/HumanoidCharacterProfile.cs b/Content.Shared/Preferences/HumanoidCharacterProfile.cs
index 5bb464e9ed0..f17aea62a19 100644
--- a/Content.Shared/Preferences/HumanoidCharacterProfile.cs
+++ b/Content.Shared/Preferences/HumanoidCharacterProfile.cs
@@ -58,6 +58,7 @@
using Content.Shared.Preferences.Loadouts;
using Content.Shared.Roles;
using Content.Goobstation.Common.Barks; // Goob Station - Barks
+using Content.Shared._Arcane.ERP; // Arcane-edit
using Content.Shared.Traits;
using Robust.Shared.Collections;
using Robust.Shared.Configuration;
@@ -155,6 +156,11 @@ public sealed partial class HumanoidCharacterProfile : ICharacterProfile
public string NsfwTagsFlavorText { get; set; } = string.Empty;
// Orion-End
+ // Arcane-Start
+ [DataField]
+ public ErpPreference ErpPreference { get; set; } = ErpPreference.Ask;
+ // Arcane-End
+
///
/// Associated for this profile.
///
@@ -327,6 +333,7 @@ public HumanoidCharacterProfile(HumanoidCharacterProfile other)
new Dictionary(other.Loadouts),
other.BarkVoice) // Goob Station - Barks
{
+ ErpPreference = other.ErpPreference; // Arcane-edit
}
///
@@ -539,6 +546,13 @@ public HumanoidCharacterProfile WithBarkVoice(BarkPrototype barkVoice)
}
// Goob Station - Barks End
+ // Arcane-Start
+ public HumanoidCharacterProfile WithErpPreference(ErpPreference preference)
+ {
+ return new(this) { ErpPreference = preference };
+ }
+ // Arcane-End
+
public HumanoidCharacterProfile WithJobPriorities(IEnumerable, JobPriority>> jobPriorities)
{
var dictionary = new Dictionary, JobPriority>(jobPriorities);
@@ -740,6 +754,7 @@ public void EnsureValid(ICommonSession session, IDependencyCollection collection
Sex.Male => Sex.Male,
Sex.Female => Sex.Female,
Sex.Unsexed => Sex.Unsexed,
+ Sex.Futanari => Sex.Futanari, // Arcane-edit
_ => Sex.Male // Invalid enum values.
};
diff --git a/Content.Shared/_Arcane/ERP/ArousalComponent.cs b/Content.Shared/_Arcane/ERP/ArousalComponent.cs
new file mode 100644
index 00000000000..638cf006c46
--- /dev/null
+++ b/Content.Shared/_Arcane/ERP/ArousalComponent.cs
@@ -0,0 +1,98 @@
+using Robust.Shared.GameStates;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
+
+namespace Content.Shared._Arcane.ERP;
+
+public enum ArousalPhase : byte
+{
+ Calm,
+ Interested,
+ Aroused,
+ Heated,
+ Peak,
+}
+
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause]
+public sealed partial class ArousalComponent : Component
+{
+ ///
+ /// Authoritative arousal value as of LastChangeTime.
+ /// Use ArousalSystem.GetArousal to get the current value accounting for decay.
+ ///
+ [AutoNetworkedField]
+ public float LastValue;
+
+ ///
+ /// Time at which LastValue was last set.
+ ///
+ [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoNetworkedField]
+ public TimeSpan LastChangeTime;
+
+ ///
+ /// Passive decay per second.
+ ///
+ [DataField, AutoNetworkedField]
+ public float DecayRate = 0.3f;
+
+ [DataField]
+ public float MaxArousal = 100f;
+
+ ///
+ /// Registered passive arousal sources. Key is source ID, value is gain per second.
+ /// Use ArousalSystem.SetPassiveSource and ArousalSystem.RemovePassiveSource.
+ ///
+ public Dictionary PassiveSources = [];
+
+ ///
+ /// Total passive gain per second — sum of all .
+ ///
+ public float PassiveGainRate
+ {
+ get
+ {
+ var total = 0f;
+ foreach (var rate in PassiveSources.Values)
+ total += rate;
+ return total;
+ }
+ }
+
+ ///
+ /// When to next check for phase transitions from passive decay.
+ ///
+ [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
+ [AutoNetworkedField, AutoPausedField]
+ public TimeSpan NextPhaseCheckAt;
+
+ [AutoNetworkedField]
+ public ArousalPhase CurrentPhase;
+
+ ///
+ /// When the last orgasm occurred.
+ ///
+ [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
+ [AutoPausedField]
+ public TimeSpan LastOrgasmAt;
+
+ ///
+ /// Arousal gain is blocked until this time (refractory period after orgasm).
+ ///
+ [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
+ [AutoNetworkedField, AutoPausedField]
+ public TimeSpan RefractoryUntil;
+
+ ///
+ /// How long the refractory period lasts after orgasm.
+ ///
+ [DataField]
+ public TimeSpan RefractoryDuration = TimeSpan.FromSeconds(90);
+
+ public ArousalPhase ComputePhase(float arousal) => arousal switch
+ {
+ < 20f => ArousalPhase.Calm,
+ < 40f => ArousalPhase.Interested,
+ < 70f => ArousalPhase.Aroused,
+ < 95f => ArousalPhase.Heated,
+ _ => ArousalPhase.Peak,
+ };
+}
diff --git a/Content.Shared/_Arcane/ERP/Erp.cs b/Content.Shared/_Arcane/ERP/Erp.cs
new file mode 100644
index 00000000000..d17faf27f97
--- /dev/null
+++ b/Content.Shared/_Arcane/ERP/Erp.cs
@@ -0,0 +1,20 @@
+namespace Content.Shared._Arcane.ERP;
+
+public enum ErpPreference : byte
+{
+ Yes,
+ Ask,
+ No,
+}
+
+public sealed class ErpPreferenceChangedEvent : EntityEventArgs
+{
+ public ErpPreference OldPreference;
+ public ErpPreference NewPreference;
+
+ public ErpPreferenceChangedEvent(ErpPreference oldPref, ErpPreference newPref)
+ {
+ OldPreference = oldPref;
+ NewPreference = newPref;
+ }
+}
diff --git a/Content.Shared/_Arcane/ERP/ErpStatusComponent.cs b/Content.Shared/_Arcane/ERP/ErpStatusComponent.cs
new file mode 100644
index 00000000000..d535309dd4e
--- /dev/null
+++ b/Content.Shared/_Arcane/ERP/ErpStatusComponent.cs
@@ -0,0 +1,10 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared._Arcane.ERP;
+
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+public sealed partial class ErpStatusComponent : Component
+{
+ [DataField, AutoNetworkedField]
+ public ErpPreference Preference = ErpPreference.Ask;
+}
diff --git a/Content.Shared/_Arcane/ERP/Organs/EroticOrganComponent.cs b/Content.Shared/_Arcane/ERP/Organs/EroticOrganComponent.cs
new file mode 100644
index 00000000000..d324414c042
--- /dev/null
+++ b/Content.Shared/_Arcane/ERP/Organs/EroticOrganComponent.cs
@@ -0,0 +1,26 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared._Arcane.ERP.Organs;
+
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+public sealed partial class EroticOrganComponent : Component
+{
+ ///
+ /// Base sensitivity multiplier for arousal calculations. 1.0 = normal.
+ ///
+ [DataField, AutoNetworkedField]
+ public float Sensitivity = 1.0f;
+
+ ///
+ /// Size modifier. 1.0 = average. Used in interaction condition checks and descriptions.
+ ///
+ [DataField, AutoNetworkedField]
+ public float Size = 1.0f;
+
+ ///
+ /// Whether this organ is currently exposed (not covered by clothing).
+ /// Managed by the clothing coverage system.
+ ///
+ [AutoNetworkedField]
+ public bool Visible = true;
+}
diff --git a/Content.Shared/_Arcane/ERP/Organs/EroticOrganTypes.cs b/Content.Shared/_Arcane/ERP/Organs/EroticOrganTypes.cs
new file mode 100644
index 00000000000..eccf90e7309
--- /dev/null
+++ b/Content.Shared/_Arcane/ERP/Organs/EroticOrganTypes.cs
@@ -0,0 +1,24 @@
+namespace Content.Shared._Arcane.ERP.Organs;
+
+// Marker components — each is a tag for interaction condition checks.
+// All sexes
+[RegisterComponent]
+public sealed partial class AnusOrganComponent : Component;
+
+// Male + Futanari
+[RegisterComponent]
+public sealed partial class PenisOrganComponent : Component;
+
+[RegisterComponent]
+public sealed partial class TesticlesOrganComponent : Component;
+
+// Female + Futanari
+[RegisterComponent]
+public sealed partial class VaginaOrganComponent : Component;
+
+[RegisterComponent]
+public sealed partial class UterusOrganComponent : Component;
+
+// Female + Futanari
+[RegisterComponent]
+public sealed partial class BreastsOrganComponent : Component;
diff --git a/Content.Shared/_Shitmed/Surgery/Pain/Components/NerveSystemComponent.cs b/Content.Shared/_Shitmed/Surgery/Pain/Components/NerveSystemComponent.cs
index 3b98c2a2549..869e25b4788 100644
--- a/Content.Shared/_Shitmed/Surgery/Pain/Components/NerveSystemComponent.cs
+++ b/Content.Shared/_Shitmed/Surgery/Pain/Components/NerveSystemComponent.cs
@@ -102,6 +102,12 @@ public sealed partial class NerveSystemComponent : Component
Params = AudioParams.Default.WithVariation(0.2f),
}
},
+ {
+ Sex.Futanari, new SoundCollectionSpecifier("PainScreamsShortFemale") // Arcane-edit
+ {
+ Params = AudioParams.Default.WithVariation(0.04f),
+ }
+ },
};
[DataField]
@@ -125,6 +131,12 @@ public sealed partial class NerveSystemComponent : Component
Params = AudioParams.Default.WithVariation(0.2f),
}
},
+ {
+ Sex.Futanari, new SoundCollectionSpecifier("AgonyScreamsFemale") // Arcane-edit
+ {
+ Params = AudioParams.Default.WithVariation(0.04f),
+ }
+ },
};
[DataField]
@@ -148,6 +160,12 @@ public sealed partial class NerveSystemComponent : Component
Params = AudioParams.Default.WithVariation(0.2f),
}
},
+ {
+ Sex.Futanari, new SoundCollectionSpecifier("PainShockScreamsFemale") // Arcane-edit
+ {
+ Params = AudioParams.Default.WithVariation(0.05f),
+ }
+ },
};
[DataField]
@@ -171,6 +189,12 @@ public sealed partial class NerveSystemComponent : Component
Params = AudioParams.Default,
}
},
+ {
+ Sex.Futanari, new SoundCollectionSpecifier("CritWhimpersFemale") // Arcane-edit
+ {
+ Params = AudioParams.Default,
+ }
+ },
};
[DataField]
@@ -194,6 +218,12 @@ public sealed partial class NerveSystemComponent : Component
Params = AudioParams.Default,
}
},
+ {
+ Sex.Futanari, new SoundCollectionSpecifier("PainShockWhimpersFemale") // Arcane-edit
+ {
+ Params = AudioParams.Default,
+ }
+ },
};
[DataField]
@@ -217,6 +247,12 @@ public sealed partial class NerveSystemComponent : Component
Params = AudioParams.Default,
}
},
+ {
+ Sex.Futanari, new SoundCollectionSpecifier("OrganDamagePainedFemale") // Arcane-edit
+ {
+ Params = AudioParams.Default,
+ }
+ },
};
[DataField]
@@ -240,6 +276,12 @@ public sealed partial class NerveSystemComponent : Component
Params = AudioParams.Default,
}
},
+ {
+ Sex.Futanari, new SoundCollectionSpecifier("OrganDamageWhimpersFemale") // Arcane-edit
+ {
+ Params = AudioParams.Default,
+ }
+ },
};
[DataField("reflexThresholds"), ViewVariables(VVAccess.ReadOnly)]
diff --git a/Resources/Locale/en-US/_Arcane/ERP/erp.ftl b/Resources/Locale/en-US/_Arcane/ERP/erp.ftl
new file mode 100644
index 00000000000..928d998bf40
--- /dev/null
+++ b/Resources/Locale/en-US/_Arcane/ERP/erp.ftl
@@ -0,0 +1,16 @@
+humanoid-profile-editor-erp-preference-label = ERP
+humanoid-profile-editor-erp-preference-yes = Yes
+humanoid-profile-editor-erp-preference-ask = Ask
+humanoid-profile-editor-erp-preference-no = No
+
+erp-examine-verb-text = ERP Status
+erp-examine-verb-disabled = Too far away
+erp-examine-status-header = ERP status:
+erp-examine-status-yes = [color=#44ff44]Yes[/color]
+erp-examine-status-ask = [color=#ffcc00]Ask first[/color]
+erp-examine-status-no = [color=#ff4444]Disabled[/color]
+
+alerts-arousal-aroused-name = Aroused
+alerts-arousal-aroused-desc = You feel aroused.
+alerts-arousal-heated-name = Heated
+alerts-arousal-heated-desc = You feel very aroused.
diff --git a/Resources/Locale/en-US/preferences/ui/humanoid-profile-editor.ftl b/Resources/Locale/en-US/preferences/ui/humanoid-profile-editor.ftl
index 8324cde89b1..92368f1d661 100644
--- a/Resources/Locale/en-US/preferences/ui/humanoid-profile-editor.ftl
+++ b/Resources/Locale/en-US/preferences/ui/humanoid-profile-editor.ftl
@@ -35,6 +35,7 @@ humanoid-profile-editor-sex-label = Sex:
humanoid-profile-editor-sex-male-text = Male
humanoid-profile-editor-sex-female-text = Female
humanoid-profile-editor-sex-unsexed-text = None
+humanoid-profile-editor-sex-futanari-text = Futanari
humanoid-profile-editor-age-label = Age:
humanoid-profile-editor-skin-color-label = Skin color:
humanoid-profile-editor-species-label = Species:
diff --git a/Resources/Locale/ru-RU/_Arcane/ERP/erp.ftl b/Resources/Locale/ru-RU/_Arcane/ERP/erp.ftl
new file mode 100644
index 00000000000..01a6be20181
--- /dev/null
+++ b/Resources/Locale/ru-RU/_Arcane/ERP/erp.ftl
@@ -0,0 +1,16 @@
+humanoid-profile-editor-erp-preference-label = ERP
+humanoid-profile-editor-erp-preference-yes = Да
+humanoid-profile-editor-erp-preference-ask = Спрашивать
+humanoid-profile-editor-erp-preference-no = Нет
+
+erp-examine-verb-text = ERP-статус
+erp-examine-verb-disabled = Слишком далеко
+erp-examine-status-header = ERP-статус:
+erp-examine-status-yes = [color=#44ff44]Да[/color]
+erp-examine-status-ask = [color=#ffcc00]Спрашивать[/color]
+erp-examine-status-no = [color=#ff4444]Отключено[/color]
+
+alerts-arousal-aroused-name = Возбуждён
+alerts-arousal-aroused-desc = Вы чувствуете возбуждение.
+alerts-arousal-heated-name = Сильное возбуждение
+alerts-arousal-heated-desc = Вы сильно возбуждены.
diff --git a/Resources/Locale/ru-RU/_Arcane/body/Organs/erotic.ftl b/Resources/Locale/ru-RU/_Arcane/body/Organs/erotic.ftl
new file mode 100644
index 00000000000..97ec4727dbb
--- /dev/null
+++ b/Resources/Locale/ru-RU/_Arcane/body/Organs/erotic.ftl
@@ -0,0 +1,17 @@
+ent-OrganAnus = анус
+ .desc = Внутренняя мышца-сфинктер.
+
+ent-OrganPenis = пенис
+ .desc = Мужской половой орган.
+
+ent-OrganTesticles = яички
+ .desc = Мужские половые железы.
+
+ent-OrganVagina = вагина
+ .desc = Женский половой орган.
+
+ent-OrganUterus = матка
+ .desc = Женский репродуктивный орган.
+
+ent-OrganBreasts = грудь
+ .desc = Молочные железы.
diff --git a/Resources/Locale/ru-RU/preferences/ui/humanoid-profile-editor.ftl b/Resources/Locale/ru-RU/preferences/ui/humanoid-profile-editor.ftl
index 77f2f665a1e..e8433a53377 100644
--- a/Resources/Locale/ru-RU/preferences/ui/humanoid-profile-editor.ftl
+++ b/Resources/Locale/ru-RU/preferences/ui/humanoid-profile-editor.ftl
@@ -8,6 +8,7 @@ humanoid-profile-editor-sex-label = Пол:
humanoid-profile-editor-sex-male-text = Мужской
humanoid-profile-editor-sex-female-text = Женский
humanoid-profile-editor-sex-unsexed-text = Отсутствует
+humanoid-profile-editor-sex-futanari-text = Футанари
humanoid-profile-editor-age-label = Возраст:
humanoid-profile-editor-skin-color-label = Цвет кожи:
humanoid-profile-editor-species-label = Раса:
diff --git a/Resources/Prototypes/_Arcane/Alerts/arousal.yml b/Resources/Prototypes/_Arcane/Alerts/arousal.yml
new file mode 100644
index 00000000000..96b7a2bbac2
--- /dev/null
+++ b/Resources/Prototypes/_Arcane/Alerts/arousal.yml
@@ -0,0 +1,22 @@
+- type: alertCategory
+ id: Arousal
+
+# Shown at ArousalPhase.Aroused (40+)
+- type: alert
+ id: ArousalAroused
+ category: Arousal
+ icons:
+ - sprite: /Textures/Interface/Alerts/hunger.rsi
+ state: peckish
+ name: alerts-arousal-aroused-name
+ description: alerts-arousal-aroused-desc
+
+# Shown at ArousalPhase.Heated (70+)
+- type: alert
+ id: ArousalHeated
+ category: Arousal
+ icons:
+ - sprite: /Textures/Interface/Alerts/hunger.rsi
+ state: starving
+ name: alerts-arousal-heated-name
+ description: alerts-arousal-heated-desc
diff --git a/Resources/Prototypes/_Arcane/Body/Organs/erotic.yml b/Resources/Prototypes/_Arcane/Body/Organs/erotic.yml
new file mode 100644
index 00000000000..87f58525188
--- /dev/null
+++ b/Resources/Prototypes/_Arcane/Body/Organs/erotic.yml
@@ -0,0 +1,107 @@
+# Base for erotic organs — internal only, not physical items.
+- type: entity
+ id: BaseEroticOrgan
+ abstract: true
+ components:
+ - type: Organ
+ canEnable: true
+ removable: false
+
+# Universal (all sexes)
+# Internal muscle — no integrity display in health scanner.
+- type: entity
+ id: OrganAnus
+ parent: BaseEroticOrgan
+ name: anus
+ description: An internal sphincter muscle.
+ components:
+ - type: Organ
+ slotId: anus
+ removable: false
+ - type: EroticOrgan
+ sensitivity: 1.2
+ - type: AnusOrgan
+
+# Male / Futanari
+# External organ — shows in health scanner.
+- type: entity
+ id: OrganPenis
+ parent: BaseEroticOrgan
+ name: penis
+ description: A male genital organ.
+ components:
+ - type: Organ
+ intCap: 15
+ integrity: 15
+ integrityThresholds:
+ Normal: 15
+ Damaged: 6
+ Destroyed: 0
+ slotId: penis
+ removable: false
+ - type: EroticOrgan
+ sensitivity: 1.5
+ - type: PenisOrgan
+
+# Internal gonads — no integrity display in health scanner.
+- type: entity
+ id: OrganTesticles
+ parent: BaseEroticOrgan
+ name: testicles
+ description: Male gonads.
+ components:
+ - type: Organ
+ slotId: testicles
+ removable: false
+ - type: EroticOrgan
+ sensitivity: 1.0
+ - type: TesticlesOrgan
+
+# Female / Futanari
+# Mostly internal — no integrity display in health scanner.
+- type: entity
+ id: OrganVagina
+ parent: BaseEroticOrgan
+ name: vagina
+ description: A female genital organ.
+ components:
+ - type: Organ
+ slotId: vagina
+ removable: false
+ - type: EroticOrgan
+ sensitivity: 1.5
+ - type: VaginaOrgan
+
+# Deeply internal — no integrity display in health scanner.
+- type: entity
+ id: OrganUterus
+ parent: BaseEroticOrgan
+ name: uterus
+ description: A female reproductive organ.
+ components:
+ - type: Organ
+ slotId: uterus
+ removable: false
+ - type: EroticOrgan
+ sensitivity: 0.8
+ - type: UterusOrgan
+
+# External tissue — shows in health scanner.
+- type: entity
+ id: OrganBreasts
+ parent: BaseEroticOrgan
+ name: breasts
+ description: Mammary glands.
+ components:
+ - type: Organ
+ intCap: 15
+ integrity: 15
+ integrityThresholds:
+ Normal: 15
+ Damaged: 6
+ Destroyed: 0
+ slotId: breasts
+ removable: false
+ - type: EroticOrgan
+ sensitivity: 1.1
+ - type: BreastsOrgan
diff --git a/Resources/Prototypes/_Arcane/Species/demon.yml b/Resources/Prototypes/_Arcane/Species/demon.yml
index a9f5bd34ec6..2ca0e9ff34d 100644
--- a/Resources/Prototypes/_Arcane/Species/demon.yml
+++ b/Resources/Prototypes/_Arcane/Species/demon.yml
@@ -9,7 +9,7 @@
dollPrototype: MobDemonDummy
skinColoration: Hues
naming: firstlast
- sexes: [Male, Female]
+ sexes: [Male, Female, Futanari]
category: Unusual
description: "/ServerInfo/_Arcane/SpeciesDescriptions/Demon.xml"
pros:
diff --git a/Resources/Prototypes/_EinsteinEngines/Species/shadowkin.yml b/Resources/Prototypes/_EinsteinEngines/Species/shadowkin.yml
index 95b26d124f3..580d0cc0b85 100644
--- a/Resources/Prototypes/_EinsteinEngines/Species/shadowkin.yml
+++ b/Resources/Prototypes/_EinsteinEngines/Species/shadowkin.yml
@@ -24,6 +24,7 @@
sexes:
- Male
- Female
+ - Futanari # Arcane-edit
# Orion-Start
category: Unusual
description: "/ServerInfo/_Orion/SpeciesDescriptions/Shadekin.xml"
diff --git a/Resources/Prototypes/_Floofstation/Species/resomi.yml b/Resources/Prototypes/_Floofstation/Species/resomi.yml
index b08a93e547c..f4048a12cae 100644
--- a/Resources/Prototypes/_Floofstation/Species/resomi.yml
+++ b/Resources/Prototypes/_Floofstation/Species/resomi.yml
@@ -24,6 +24,7 @@
- Unsexed
- Male
- Female
+ - Futanari # Arcane-edit
# Orion-Start
category: Classic
description: "/ServerInfo/_Orion/SpeciesDescriptions/Resomi.xml"
diff --git a/Resources/Textures/_Arcane/Interface/heartIcon.png b/Resources/Textures/_Arcane/Interface/heartIcon.png
new file mode 100644
index 0000000000000000000000000000000000000000..2b84bd40f3680280bd6e9e4c31b4d97b63136ae0
GIT binary patch
literal 362
zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE-VV{wqX6T`Z5GB1G~mUKs7M+SzC
z{oH>NS%Lgto-U3d7XDW!ZR9#+AkcPMDM`G&X>#}G9}KLT`S?8*PPl*e42VeyJxb^4vV*Rwl+RW2$^-S#r-`O6DOTz`hlF1o&w^Xc59H*1o<
z9sj)cvdAfBzLbyIrpDd
zrTA
Date: Mon, 18 May 2026 23:53:47 +0300
Subject: [PATCH 02/13] Rabbit
---
.../Preferences/HumanoidCharacterProfile.cs | 11 +++++++++++
Resources/Locale/en-US/_Arcane/ERP/erp.ftl | 2 +-
Resources/Locale/ru-RU/_Arcane/ERP/erp.ftl | 2 +-
3 files changed, 13 insertions(+), 2 deletions(-)
diff --git a/Content.Shared/Preferences/HumanoidCharacterProfile.cs b/Content.Shared/Preferences/HumanoidCharacterProfile.cs
index f17aea62a19..ebee454b8ef 100644
--- a/Content.Shared/Preferences/HumanoidCharacterProfile.cs
+++ b/Content.Shared/Preferences/HumanoidCharacterProfile.cs
@@ -735,6 +735,7 @@ public bool MemberwiseEquals(ICharacterProfile maybeOther)
if (NsfwLinksFlavorText != other.NsfwLinksFlavorText) return false;
if (NsfwTagsFlavorText != other.NsfwTagsFlavorText) return false;
// Orion-End
+ if (ErpPreference != other.ErpPreference) return false; // Arcane-edit
return Appearance.MemberwiseEquals(other.Appearance);
}
@@ -962,6 +963,14 @@ public void EnsureValid(ICommonSession session, IDependencyCollection collection
_ => PreferenceUnavailableMode.StayInLobby // Invalid enum values.
};
+ var erpPreference = ErpPreference switch
+ {
+ ErpPreference.Yes => ErpPreference.Yes,
+ ErpPreference.Ask => ErpPreference.Ask,
+ ErpPreference.No => ErpPreference.No,
+ _ => ErpPreference.Ask // Invalid enum values.
+ };
+
var spawnPriority = SpawnPriority switch
{
SpawnPriorityPreference.None => SpawnPriorityPreference.None,
@@ -1021,6 +1030,7 @@ public void EnsureValid(ICommonSession session, IDependencyCollection collection
Gender = gender;
Appearance = appearance;
SpawnPriority = spawnPriority;
+ ErpPreference = erpPreference; // Arcane-edit
_jobPriorities.Clear();
@@ -1156,6 +1166,7 @@ public override int GetHashCode()
hashCode.Add(BarkVoice); // Goob Station - Barks
hashCode.Add((int) SpawnPriority);
hashCode.Add((int) PreferenceUnavailable);
+ hashCode.Add((int) ErpPreference); // Arcane-edit
return hashCode.ToHashCode();
}
diff --git a/Resources/Locale/en-US/_Arcane/ERP/erp.ftl b/Resources/Locale/en-US/_Arcane/ERP/erp.ftl
index 928d998bf40..6db28216ca6 100644
--- a/Resources/Locale/en-US/_Arcane/ERP/erp.ftl
+++ b/Resources/Locale/en-US/_Arcane/ERP/erp.ftl
@@ -1,7 +1,7 @@
humanoid-profile-editor-erp-preference-label = ERP
humanoid-profile-editor-erp-preference-yes = Yes
humanoid-profile-editor-erp-preference-ask = Ask
-humanoid-profile-editor-erp-preference-no = No
+humanoid-profile-editor-erp-preference-no = Disabled
erp-examine-verb-text = ERP Status
erp-examine-verb-disabled = Too far away
diff --git a/Resources/Locale/ru-RU/_Arcane/ERP/erp.ftl b/Resources/Locale/ru-RU/_Arcane/ERP/erp.ftl
index 01a6be20181..c1f8030f6e9 100644
--- a/Resources/Locale/ru-RU/_Arcane/ERP/erp.ftl
+++ b/Resources/Locale/ru-RU/_Arcane/ERP/erp.ftl
@@ -1,7 +1,7 @@
humanoid-profile-editor-erp-preference-label = ERP
humanoid-profile-editor-erp-preference-yes = Да
humanoid-profile-editor-erp-preference-ask = Спрашивать
-humanoid-profile-editor-erp-preference-no = Нет
+humanoid-profile-editor-erp-preference-no = Отключено
erp-examine-verb-text = ERP-статус
erp-examine-verb-disabled = Слишком далеко
From f6f4c022b14bd8515ea2bf77978aaf8d63714cda Mon Sep 17 00:00:00 2001
From: MataVsn <113342160+MataVsn@users.noreply.github.com>
Date: Tue, 19 May 2026 00:26:39 +0300
Subject: [PATCH 03/13] NPC organs
---
Content.Server/_Arcane/ERP/EroticOrganSpawnSystem.cs | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/Content.Server/_Arcane/ERP/EroticOrganSpawnSystem.cs b/Content.Server/_Arcane/ERP/EroticOrganSpawnSystem.cs
index 977e28618c8..86c42ee748c 100644
--- a/Content.Server/_Arcane/ERP/EroticOrganSpawnSystem.cs
+++ b/Content.Server/_Arcane/ERP/EroticOrganSpawnSystem.cs
@@ -18,12 +18,19 @@ public sealed class EroticOrganSpawnSystem : EntitySystem
public override void Initialize()
{
base.Initialize();
+ SubscribeLocalEvent(OnMapInit);
SubscribeLocalEvent(OnProfileLoaded);
SubscribeLocalEvent(OnSexChanged);
}
+ private void OnMapInit(EntityUid uid, HumanoidAppearanceComponent humanoid, MapInitEvent args)
+ {
+ SpawnEroticOrgans(uid, humanoid.Sex);
+ }
+
private void OnProfileLoaded(EntityUid uid, HumanoidAppearanceComponent humanoid, ProfileLoadFinishedEvent args)
{
+ RemoveEroticOrgans(uid);
SpawnEroticOrgans(uid, humanoid.Sex);
}
From 5126e7013ec0e0836bdb20900495ef0a50e567e9 Mon Sep 17 00:00:00 2001
From: MataVsn <113342160+MataVsn@users.noreply.github.com>
Date: Tue, 19 May 2026 00:44:41 +0300
Subject: [PATCH 04/13] Clothing coverage for erotic organs
---
.../_Arcane/ERP/EroticCoverageSystem.cs | 73 +++++++++++++++++++
1 file changed, 73 insertions(+)
create mode 100644 Content.Server/_Arcane/ERP/EroticCoverageSystem.cs
diff --git a/Content.Server/_Arcane/ERP/EroticCoverageSystem.cs b/Content.Server/_Arcane/ERP/EroticCoverageSystem.cs
new file mode 100644
index 00000000000..bfe597e7e57
--- /dev/null
+++ b/Content.Server/_Arcane/ERP/EroticCoverageSystem.cs
@@ -0,0 +1,73 @@
+using Content.Shared._Arcane.ERP.Organs;
+using Content.Shared.Body.Part;
+using Content.Shared.Body.Systems;
+using Content.Shared.Clothing;
+using Content.Shared.Clothing.Components;
+using Content.Shared.Humanoid;
+using Content.Shared.Inventory;
+using Robust.Shared.Containers;
+
+namespace Content.Server._Arcane.ERP;
+
+public sealed class EroticCoverageSystem : EntitySystem
+{
+ [Dependency] private readonly SharedBodySystem _body = default!;
+ [Dependency] private readonly InventorySystem _inventory = default!;
+ [Dependency] private readonly SharedContainerSystem _containers = default!;
+
+ private const SlotFlags GroinCovering = SlotFlags.INNERCLOTHING | SlotFlags.OUTERCLOTHING | SlotFlags.LEGS | SlotFlags.UNDERWEAR;
+ private const SlotFlags ChestCovering = SlotFlags.INNERCLOTHING | SlotFlags.OUTERCLOTHING | SlotFlags.UNDERSHIRT;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(OnEquipped);
+ SubscribeLocalEvent(OnUnequipped);
+ }
+
+ private void OnEquipped(EntityUid uid, HumanoidAppearanceComponent _, ref ClothingDidEquippedEvent args)
+ {
+ RefreshOrganVisibility(uid);
+ }
+
+ private void OnUnequipped(EntityUid uid, HumanoidAppearanceComponent _, ref ClothingDidUnequippedEvent args)
+ {
+ RefreshOrganVisibility(uid);
+ }
+
+ private void RefreshOrganVisibility(EntityUid uid)
+ {
+ var coverage = SlotFlags.NONE;
+ var enumerator = _inventory.GetSlotEnumerator(uid, GroinCovering | ChestCovering);
+ while (enumerator.NextItem(out var item))
+ {
+ if (TryComp(item, out var clothing))
+ coverage |= clothing.Slots;
+ }
+
+ var groinCovered = (coverage & GroinCovering) != SlotFlags.NONE;
+ var chestCovered = (coverage & ChestCovering) != SlotFlags.NONE;
+
+ foreach (var organ in _body.GetBodyOrganEntityComps((uid, null)))
+ {
+ if (!_containers.TryGetContainingContainer(organ.Owner, out var container))
+ continue;
+
+ if (!TryComp(container.Owner, out var part))
+ continue;
+
+ var visible = part.PartType switch
+ {
+ BodyPartType.Groin => !groinCovered,
+ BodyPartType.Chest => !chestCovered,
+ _ => true,
+ };
+
+ if (organ.Comp1.Visible == visible)
+ continue;
+
+ organ.Comp1.Visible = visible;
+ Dirty(organ.Owner, organ.Comp1);
+ }
+ }
+}
From 10697be98574e1b4b739f7d114e8a0835624627a Mon Sep 17 00:00:00 2001
From: MataVsn <113342160+MataVsn@users.noreply.github.com>
Date: Tue, 19 May 2026 00:58:26 +0300
Subject: [PATCH 05/13] Fix
---
Content.Server/_Arcane/ERP/EroticCoverageSystem.cs | 6 ++++++
Content.Server/_Arcane/ERP/EroticOrganSpawnSystem.cs | 4 ++++
2 files changed, 10 insertions(+)
diff --git a/Content.Server/_Arcane/ERP/EroticCoverageSystem.cs b/Content.Server/_Arcane/ERP/EroticCoverageSystem.cs
index bfe597e7e57..99e7d4bc6ac 100644
--- a/Content.Server/_Arcane/ERP/EroticCoverageSystem.cs
+++ b/Content.Server/_Arcane/ERP/EroticCoverageSystem.cs
@@ -21,10 +21,16 @@ public sealed class EroticCoverageSystem : EntitySystem
public override void Initialize()
{
base.Initialize();
+ SubscribeLocalEvent(OnOrgansSpawned);
SubscribeLocalEvent(OnEquipped);
SubscribeLocalEvent(OnUnequipped);
}
+ private void OnOrgansSpawned(EntityUid uid, HumanoidAppearanceComponent _, EroticOrgansSpawnedEvent args)
+ {
+ RefreshOrganVisibility(uid);
+ }
+
private void OnEquipped(EntityUid uid, HumanoidAppearanceComponent _, ref ClothingDidEquippedEvent args)
{
RefreshOrganVisibility(uid);
diff --git a/Content.Server/_Arcane/ERP/EroticOrganSpawnSystem.cs b/Content.Server/_Arcane/ERP/EroticOrganSpawnSystem.cs
index 86c42ee748c..69aca17db5a 100644
--- a/Content.Server/_Arcane/ERP/EroticOrganSpawnSystem.cs
+++ b/Content.Server/_Arcane/ERP/EroticOrganSpawnSystem.cs
@@ -83,6 +83,8 @@ private void SpawnEroticOrgans(EntityUid uid, Sex sex)
TrySpawnOrgan(uid, chest.Value, "OrganBreasts", "breasts");
break;
}
+
+ RaiseLocalEvent(uid, new EroticOrgansSpawnedEvent());
}
private void RemoveEroticOrgans(EntityUid bodyUid)
@@ -122,3 +124,5 @@ private void TrySpawnOrgan(EntityUid bodyUid, EntityUid partUid, string protoId,
return null;
}
}
+
+public sealed class EroticOrgansSpawnedEvent : EntityEventArgs;
From 60898802d3fde2807a36450398273826d0c39ddd Mon Sep 17 00:00:00 2001
From: MataVsn <113342160+MataVsn@users.noreply.github.com>
Date: Tue, 19 May 2026 01:32:18 +0300
Subject: [PATCH 06/13] NPC - organs fix
---
Content.Server/_Arcane/ERP/EroticOrganSpawnSystem.cs | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/Content.Server/_Arcane/ERP/EroticOrganSpawnSystem.cs b/Content.Server/_Arcane/ERP/EroticOrganSpawnSystem.cs
index 69aca17db5a..3d979cec688 100644
--- a/Content.Server/_Arcane/ERP/EroticOrganSpawnSystem.cs
+++ b/Content.Server/_Arcane/ERP/EroticOrganSpawnSystem.cs
@@ -18,7 +18,8 @@ public sealed class EroticOrganSpawnSystem : EntitySystem
public override void Initialize()
{
base.Initialize();
- SubscribeLocalEvent(OnMapInit);
+ // Run after SharedBodySystem so body parts are already spawned when we look for them.
+ SubscribeLocalEvent(OnMapInit, after: [typeof(SharedBodySystem)]);
SubscribeLocalEvent(OnProfileLoaded);
SubscribeLocalEvent(OnSexChanged);
}
From dd36ea43448ae6b9a3acb9d327698f508b6059a8 Mon Sep 17 00:00:00 2001
From: MataVsn <113342160+MataVsn@users.noreply.github.com>
Date: Tue, 19 May 2026 01:55:51 +0300
Subject: [PATCH 07/13] Physics fix
---
Resources/Prototypes/_Arcane/Body/Organs/erotic.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Resources/Prototypes/_Arcane/Body/Organs/erotic.yml b/Resources/Prototypes/_Arcane/Body/Organs/erotic.yml
index 87f58525188..6e44911ffde 100644
--- a/Resources/Prototypes/_Arcane/Body/Organs/erotic.yml
+++ b/Resources/Prototypes/_Arcane/Body/Organs/erotic.yml
@@ -1,6 +1,6 @@
-# Base for erotic organs — internal only, not physical items.
- type: entity
id: BaseEroticOrgan
+ parent: BaseItem
abstract: true
components:
- type: Organ
From 1e22ee00533df100dbcf30dcc9220f9b5815fdb1 Mon Sep 17 00:00:00 2001
From: MataVsn <113342160+MataVsn@users.noreply.github.com>
Date: Tue, 19 May 2026 02:36:28 +0300
Subject: [PATCH 08/13] New sprites. Arch fixes, standartization
---
Content.Server/_Arcane/ERP/ArousalSystem.cs | 32 ++----
.../_Arcane/ERP/EroticCoverageSystem.cs | 13 +--
.../_Arcane/ERP/ErpExamineSystem.cs | 9 +-
Content.Server/_Arcane/ERP/ErpStatusSystem.cs | 10 +-
.../Humanoid/HumanoidVisualLayersExtension.cs | 5 +-
Content.Shared/_Arcane/ERP/ArousalEvents.cs | 15 +++
.../_Arcane/ERP/EroticOrganSpawnSystem.cs | 97 +++++++++---------
.../_Arcane/ERP/EroticOrgansSpawnedEvent.cs | 4 +
Content.Shared/_Arcane/ERP/Erp.cs | 12 +--
.../Pain/Components/NerveSystemComponent.cs | 28 +++--
.../preferences/humanoid-profile-editor.ftl | 1 +
.../ui/humanoid-profile-editor.ftl | 1 -
.../preferences/humanoid-profile-editor.ftl | 4 +
.../ui/humanoid-profile-editor.ftl | 4 -
.../Prototypes/_Arcane/Alerts/arousal.yml | 2 -
.../Prototypes/_Arcane/Body/Organs/erotic.yml | 18 ++++
.../_Arcane/Mobs/organs/anus.rsi/default.png | Bin 0 -> 120 bytes
.../_Arcane/Mobs/organs/anus.rsi/meta.json | 15 +++
.../Mobs/organs/breasts.rsi/default.png | Bin 0 -> 282 bytes
.../_Arcane/Mobs/organs/breasts.rsi/meta.json | 15 +++
.../_Arcane/Mobs/organs/penis.rsi/default.png | Bin 0 -> 270 bytes
.../_Arcane/Mobs/organs/penis.rsi/meta.json | 15 +++
.../Mobs/organs/testicles.rsi/default.png | Bin 0 -> 306 bytes
.../Mobs/organs/testicles.rsi/meta.json | 15 +++
.../Mobs/organs/uterus.rsi/default.png | Bin 0 -> 336 bytes
.../_Arcane/Mobs/organs/uterus.rsi/meta.json | 15 +++
.../Mobs/organs/vagina.rsi/default.png | Bin 0 -> 359 bytes
.../_Arcane/Mobs/organs/vagina.rsi/meta.json | 15 +++
28 files changed, 233 insertions(+), 112 deletions(-)
create mode 100644 Content.Shared/_Arcane/ERP/ArousalEvents.cs
rename {Content.Server => Content.Shared}/_Arcane/ERP/EroticOrganSpawnSystem.cs (54%)
create mode 100644 Content.Shared/_Arcane/ERP/EroticOrgansSpawnedEvent.cs
create mode 100644 Resources/Locale/en-US/_Arcane/preferences/humanoid-profile-editor.ftl
create mode 100644 Resources/Locale/ru-RU/_Arcane/preferences/humanoid-profile-editor.ftl
create mode 100644 Resources/Textures/_Arcane/Mobs/organs/anus.rsi/default.png
create mode 100644 Resources/Textures/_Arcane/Mobs/organs/anus.rsi/meta.json
create mode 100644 Resources/Textures/_Arcane/Mobs/organs/breasts.rsi/default.png
create mode 100644 Resources/Textures/_Arcane/Mobs/organs/breasts.rsi/meta.json
create mode 100644 Resources/Textures/_Arcane/Mobs/organs/penis.rsi/default.png
create mode 100644 Resources/Textures/_Arcane/Mobs/organs/penis.rsi/meta.json
create mode 100644 Resources/Textures/_Arcane/Mobs/organs/testicles.rsi/default.png
create mode 100644 Resources/Textures/_Arcane/Mobs/organs/testicles.rsi/meta.json
create mode 100644 Resources/Textures/_Arcane/Mobs/organs/uterus.rsi/default.png
create mode 100644 Resources/Textures/_Arcane/Mobs/organs/uterus.rsi/meta.json
create mode 100644 Resources/Textures/_Arcane/Mobs/organs/vagina.rsi/default.png
create mode 100644 Resources/Textures/_Arcane/Mobs/organs/vagina.rsi/meta.json
diff --git a/Content.Server/_Arcane/ERP/ArousalSystem.cs b/Content.Server/_Arcane/ERP/ArousalSystem.cs
index 33152f8d415..ad657629c98 100644
--- a/Content.Server/_Arcane/ERP/ArousalSystem.cs
+++ b/Content.Server/_Arcane/ERP/ArousalSystem.cs
@@ -23,18 +23,18 @@ public override void Initialize()
SubscribeLocalEvent(OnErpPreferenceChanged);
}
- private void OnInit(EntityUid uid, ArousalComponent comp, ComponentInit args)
+ private void OnInit(Entity ent, ref ComponentInit args)
{
- SetArousal((uid, comp), 0f);
+ SetArousal(ent, 0f);
}
- private void OnErpPreferenceChanged(EntityUid uid, ArousalComponent comp, ErpPreferenceChangedEvent args)
+ private void OnErpPreferenceChanged(Entity ent, ref ErpPreferenceChangedEvent args)
{
if (args.NewPreference != ErpPreference.No)
return;
- comp.PassiveSources.Clear();
- SetArousal((uid, comp), 0f);
+ ent.Comp.PassiveSources.Clear();
+ SetArousal(ent, 0f);
}
///
@@ -156,7 +156,8 @@ private void UpdatePhase(Entity entity)
UpdateAlerts(entity.Owner, ArousalPhase.Calm);
RaiseLocalEvent(entity.Owner, new ArousalPhaseChangedEvent(previous, ArousalPhase.Peak));
- RaiseLocalEvent(entity.Owner, new ArousalOrgasmEvent());
+ var orgasmEv = new ArousalOrgasmEvent();
+ RaiseLocalEvent(entity.Owner, ref orgasmEv);
RaiseLocalEvent(entity.Owner, new ArousalPhaseChangedEvent(ArousalPhase.Peak, ArousalPhase.Calm));
}
else
@@ -189,22 +190,3 @@ private bool IsErpDisabled(EntityUid uid)
&& status.Preference == ErpPreference.No;
}
}
-
-public sealed class ArousedEvent(float amount) : EntityEventArgs
-{
- public float Amount = amount;
-}
-
-public sealed class ArousalPhaseChangedEvent : EntityEventArgs
-{
- public ArousalPhase Previous;
- public ArousalPhase Current;
-
- public ArousalPhaseChangedEvent(ArousalPhase previous, ArousalPhase current)
- {
- Previous = previous;
- Current = current;
- }
-}
-
-public sealed class ArousalOrgasmEvent : EntityEventArgs { }
diff --git a/Content.Server/_Arcane/ERP/EroticCoverageSystem.cs b/Content.Server/_Arcane/ERP/EroticCoverageSystem.cs
index 99e7d4bc6ac..28d1edeb726 100644
--- a/Content.Server/_Arcane/ERP/EroticCoverageSystem.cs
+++ b/Content.Server/_Arcane/ERP/EroticCoverageSystem.cs
@@ -1,3 +1,4 @@
+using Content.Shared._Arcane.ERP;
using Content.Shared._Arcane.ERP.Organs;
using Content.Shared.Body.Part;
using Content.Shared.Body.Systems;
@@ -26,19 +27,19 @@ public override void Initialize()
SubscribeLocalEvent(OnUnequipped);
}
- private void OnOrgansSpawned(EntityUid uid, HumanoidAppearanceComponent _, EroticOrgansSpawnedEvent args)
+ private void OnOrgansSpawned(Entity ent, ref EroticOrgansSpawnedEvent args)
{
- RefreshOrganVisibility(uid);
+ RefreshOrganVisibility(ent);
}
- private void OnEquipped(EntityUid uid, HumanoidAppearanceComponent _, ref ClothingDidEquippedEvent args)
+ private void OnEquipped(Entity ent, ref ClothingDidEquippedEvent args)
{
- RefreshOrganVisibility(uid);
+ RefreshOrganVisibility(ent);
}
- private void OnUnequipped(EntityUid uid, HumanoidAppearanceComponent _, ref ClothingDidUnequippedEvent args)
+ private void OnUnequipped(Entity ent, ref ClothingDidUnequippedEvent args)
{
- RefreshOrganVisibility(uid);
+ RefreshOrganVisibility(ent);
}
private void RefreshOrganVisibility(EntityUid uid)
diff --git a/Content.Server/_Arcane/ERP/ErpExamineSystem.cs b/Content.Server/_Arcane/ERP/ErpExamineSystem.cs
index 85ee4934005..25700c2428c 100644
--- a/Content.Server/_Arcane/ERP/ErpExamineSystem.cs
+++ b/Content.Server/_Arcane/ERP/ErpExamineSystem.cs
@@ -15,15 +15,16 @@ public override void Initialize()
SubscribeLocalEvent>(OnGetExamineVerbs);
}
- private void OnGetExamineVerbs(EntityUid uid, ErpStatusComponent comp, GetVerbsEvent args)
+ private void OnGetExamineVerbs(Entity ent, ref GetVerbsEvent args)
{
- var inRange = _examine.IsInDetailsRange(args.User, uid);
+ var user = args.User;
+ var inRange = _examine.IsInDetailsRange(user, ent);
var verb = new ExamineVerb
{
Act = () =>
{
- var statusKey = comp.Preference switch
+ var statusKey = ent.Comp.Preference switch
{
ErpPreference.Yes => "erp-examine-status-yes",
ErpPreference.Ask => "erp-examine-status-ask",
@@ -35,7 +36,7 @@ private void OnGetExamineVerbs(EntityUid uid, ErpStatusComponent comp, GetVerbsE
msg.PushNewline();
msg.AddMarkupOrThrow(Loc.GetString(statusKey));
- _examine.SendExamineTooltip(args.User, uid, msg, false, false);
+ _examine.SendExamineTooltip(user, ent, msg, false, false);
},
Text = Loc.GetString("erp-examine-verb-text"),
Category = VerbCategory.Examine,
diff --git a/Content.Server/_Arcane/ERP/ErpStatusSystem.cs b/Content.Server/_Arcane/ERP/ErpStatusSystem.cs
index 6e7bccd54db..39ff0f543cb 100644
--- a/Content.Server/_Arcane/ERP/ErpStatusSystem.cs
+++ b/Content.Server/_Arcane/ERP/ErpStatusSystem.cs
@@ -16,19 +16,19 @@ public override void Initialize()
SubscribeLocalEvent(OnPlayerAttached);
}
- private void OnPlayerAttached(EntityUid uid, HumanoidAppearanceComponent humanoid, ref PlayerAttachedEvent args)
+ private void OnPlayerAttached(Entity ent, ref PlayerAttachedEvent args)
{
var profile = _prefs.GetPreferences(args.Player.UserId).SelectedCharacter as HumanoidCharacterProfile;
var preference = profile?.ErpPreference ?? ErpPreference.Ask;
- EnsureComp(uid);
+ EnsureComp(ent);
- var comp = EnsureComp(uid);
+ var comp = EnsureComp(ent);
var oldPreference = comp.Preference;
comp.Preference = preference;
- Dirty(uid, comp);
+ Dirty(ent, comp);
if (oldPreference != preference)
- RaiseLocalEvent(uid, new ErpPreferenceChangedEvent(oldPreference, preference));
+ RaiseLocalEvent(ent, new ErpPreferenceChangedEvent(oldPreference, preference));
}
}
diff --git a/Content.Shared/Humanoid/HumanoidVisualLayersExtension.cs b/Content.Shared/Humanoid/HumanoidVisualLayersExtension.cs
index bf5719f27d0..8a048089a32 100644
--- a/Content.Shared/Humanoid/HumanoidVisualLayersExtension.cs
+++ b/Content.Shared/Humanoid/HumanoidVisualLayersExtension.cs
@@ -38,9 +38,10 @@ public static string GetSexMorph(HumanoidVisualLayers layer, Sex sex, string id)
if (!HasSexMorph(layer) || sex == Sex.Unsexed)
return id;
- // Futanari uses female sprite layers — no separate Futanari layer prototypes exist - Arcane-edit.
- var visualSex = sex == Sex.Futanari ? Sex.Female : sex; // Arcane-edit
+ // Arcane-start: Futanari uses female sprite layers — no separate Futanari layer prototypes exist.
+ var visualSex = sex == Sex.Futanari ? Sex.Female : sex;
return $"{id}{visualSex}";
+ // Arcane-end
}
///
diff --git a/Content.Shared/_Arcane/ERP/ArousalEvents.cs b/Content.Shared/_Arcane/ERP/ArousalEvents.cs
new file mode 100644
index 00000000000..14da002c59e
--- /dev/null
+++ b/Content.Shared/_Arcane/ERP/ArousalEvents.cs
@@ -0,0 +1,15 @@
+namespace Content.Shared._Arcane.ERP;
+
+public sealed class ArousedEvent(float amount) : EntityEventArgs
+{
+ public float Amount = amount;
+}
+
+public sealed class ArousalPhaseChangedEvent(ArousalPhase previous, ArousalPhase current) : EntityEventArgs
+{
+ public ArousalPhase Previous = previous;
+ public ArousalPhase Current = current;
+}
+
+[ByRefEvent]
+public record struct ArousalOrgasmEvent;
diff --git a/Content.Server/_Arcane/ERP/EroticOrganSpawnSystem.cs b/Content.Shared/_Arcane/ERP/EroticOrganSpawnSystem.cs
similarity index 54%
rename from Content.Server/_Arcane/ERP/EroticOrganSpawnSystem.cs
rename to Content.Shared/_Arcane/ERP/EroticOrganSpawnSystem.cs
index 3d979cec688..688ee8cf673 100644
--- a/Content.Server/_Arcane/ERP/EroticOrganSpawnSystem.cs
+++ b/Content.Shared/_Arcane/ERP/EroticOrganSpawnSystem.cs
@@ -1,4 +1,4 @@
-using Content.Shared._Arcane.ERP.Organs;
+using Content.Shared._Arcane.ERP.Organs;
using Content.Shared._Shitmed.Humanoid.Events;
using Content.Shared.Body.Organ;
using Content.Shared.Body.Part;
@@ -7,7 +7,7 @@
using Robust.Shared.Containers;
using Robust.Shared.Prototypes;
-namespace Content.Server._Arcane.ERP;
+namespace Content.Shared._Arcane.ERP;
public sealed class EroticOrganSpawnSystem : EntitySystem
{
@@ -15,6 +15,28 @@ public sealed class EroticOrganSpawnSystem : EntitySystem
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly SharedContainerSystem _containers = default!;
+ private static readonly (string Proto, string Slot)[] GroinCommon =
+ [
+ ("OrganAnus", "anus"),
+ ];
+
+ private static readonly (string Proto, string Slot)[] GroinMaleOrgans =
+ [
+ ("OrganPenis", "penis"),
+ ("OrganTesticles", "testicles"),
+ ];
+
+ private static readonly (string Proto, string Slot)[] GroinFemaleOrgans =
+ [
+ ("OrganVagina", "vagina"),
+ ("OrganUterus", "uterus"),
+ ];
+
+ private static readonly (string Proto, string Slot)[] ChestFemaleOrgans =
+ [
+ ("OrganBreasts", "breasts"),
+ ];
+
public override void Initialize()
{
base.Initialize();
@@ -24,21 +46,21 @@ public override void Initialize()
SubscribeLocalEvent(OnSexChanged);
}
- private void OnMapInit(EntityUid uid, HumanoidAppearanceComponent humanoid, MapInitEvent args)
+ private void OnMapInit(Entity ent, ref MapInitEvent args)
{
- SpawnEroticOrgans(uid, humanoid.Sex);
+ SpawnEroticOrgans(ent, ent.Comp.Sex);
}
- private void OnProfileLoaded(EntityUid uid, HumanoidAppearanceComponent humanoid, ProfileLoadFinishedEvent args)
+ private void OnProfileLoaded(Entity ent, ref ProfileLoadFinishedEvent args)
{
- RemoveEroticOrgans(uid);
- SpawnEroticOrgans(uid, humanoid.Sex);
+ RemoveEroticOrgans(ent);
+ SpawnEroticOrgans(ent, ent.Comp.Sex);
}
- private void OnSexChanged(EntityUid uid, HumanoidAppearanceComponent humanoid, SexChangedEvent args)
+ private void OnSexChanged(Entity ent, ref SexChangedEvent args)
{
- RemoveEroticOrgans(uid);
- SpawnEroticOrgans(uid, args.NewSex);
+ RemoveEroticOrgans(ent);
+ SpawnEroticOrgans(ent, args.NewSex);
}
private void SpawnEroticOrgans(EntityUid uid, Sex sex)
@@ -50,42 +72,21 @@ private void SpawnEroticOrgans(EntityUid uid, Sex sex)
var chest = GetBodyPartOfType(uid, BodyPartType.Chest);
if (groin.HasValue)
- TrySpawnOrgan(uid, groin.Value, "OrganAnus", "anus");
-
- switch (sex)
{
- case Sex.Male:
- if (groin.HasValue)
- {
- TrySpawnOrgan(uid, groin.Value, "OrganPenis", "penis");
- TrySpawnOrgan(uid, groin.Value, "OrganTesticles", "testicles");
- }
- break;
-
- case Sex.Female:
- if (groin.HasValue)
- {
- TrySpawnOrgan(uid, groin.Value, "OrganVagina", "vagina");
- TrySpawnOrgan(uid, groin.Value, "OrganUterus", "uterus");
- }
- if (chest.HasValue)
- TrySpawnOrgan(uid, chest.Value, "OrganBreasts", "breasts");
- break;
-
- case Sex.Futanari:
- if (groin.HasValue)
- {
- TrySpawnOrgan(uid, groin.Value, "OrganPenis", "penis");
- TrySpawnOrgan(uid, groin.Value, "OrganTesticles", "testicles");
- TrySpawnOrgan(uid, groin.Value, "OrganVagina", "vagina");
- TrySpawnOrgan(uid, groin.Value, "OrganUterus", "uterus");
- }
- if (chest.HasValue)
- TrySpawnOrgan(uid, chest.Value, "OrganBreasts", "breasts");
- break;
+ TrySpawnOrgans(uid, groin.Value, GroinCommon);
+
+ if (sex is Sex.Male or Sex.Futanari)
+ TrySpawnOrgans(uid, groin.Value, GroinMaleOrgans);
+
+ if (sex is Sex.Female or Sex.Futanari)
+ TrySpawnOrgans(uid, groin.Value, GroinFemaleOrgans);
}
- RaiseLocalEvent(uid, new EroticOrgansSpawnedEvent());
+ if (chest.HasValue && sex is Sex.Female or Sex.Futanari)
+ TrySpawnOrgans(uid, chest.Value, ChestFemaleOrgans);
+
+ var ev = new EroticOrgansSpawnedEvent();
+ RaiseLocalEvent(uid, ref ev);
}
private void RemoveEroticOrgans(EntityUid bodyUid)
@@ -98,15 +99,19 @@ private void RemoveEroticOrgans(EntityUid bodyUid)
}
}
+ private void TrySpawnOrgans(EntityUid bodyUid, EntityUid partUid, (string Proto, string Slot)[] organs)
+ {
+ foreach (var (proto, slot) in organs)
+ TrySpawnOrgan(bodyUid, partUid, proto, slot);
+ }
+
private void TrySpawnOrgan(EntityUid bodyUid, EntityUid partUid, string protoId, string slotId)
{
if (!_proto.HasIndex(protoId))
return;
- // Create slot if it doesn't exist yet
_body.TryCreateOrganSlot(partUid, slotId, out _);
- // Don't double-spawn
var containerId = SharedBodySystem.GetOrganContainerId(slotId);
if (_containers.TryGetContainer(partUid, containerId, out var container)
&& container.ContainedEntities.Count > 0)
@@ -125,5 +130,3 @@ private void TrySpawnOrgan(EntityUid bodyUid, EntityUid partUid, string protoId,
return null;
}
}
-
-public sealed class EroticOrgansSpawnedEvent : EntityEventArgs;
diff --git a/Content.Shared/_Arcane/ERP/EroticOrgansSpawnedEvent.cs b/Content.Shared/_Arcane/ERP/EroticOrgansSpawnedEvent.cs
new file mode 100644
index 00000000000..3f13079c57c
--- /dev/null
+++ b/Content.Shared/_Arcane/ERP/EroticOrgansSpawnedEvent.cs
@@ -0,0 +1,4 @@
+namespace Content.Shared._Arcane.ERP;
+
+[ByRefEvent]
+public record struct EroticOrgansSpawnedEvent;
diff --git a/Content.Shared/_Arcane/ERP/Erp.cs b/Content.Shared/_Arcane/ERP/Erp.cs
index d17faf27f97..c95e0c41794 100644
--- a/Content.Shared/_Arcane/ERP/Erp.cs
+++ b/Content.Shared/_Arcane/ERP/Erp.cs
@@ -7,14 +7,8 @@ public enum ErpPreference : byte
No,
}
-public sealed class ErpPreferenceChangedEvent : EntityEventArgs
+public sealed class ErpPreferenceChangedEvent(ErpPreference oldPreference, ErpPreference newPreference) : EntityEventArgs
{
- public ErpPreference OldPreference;
- public ErpPreference NewPreference;
-
- public ErpPreferenceChangedEvent(ErpPreference oldPref, ErpPreference newPref)
- {
- OldPreference = oldPref;
- NewPreference = newPref;
- }
+ public ErpPreference OldPreference = oldPreference;
+ public ErpPreference NewPreference = newPreference;
}
diff --git a/Content.Shared/_Shitmed/Surgery/Pain/Components/NerveSystemComponent.cs b/Content.Shared/_Shitmed/Surgery/Pain/Components/NerveSystemComponent.cs
index 869e25b4788..d9eb9bf5dd4 100644
--- a/Content.Shared/_Shitmed/Surgery/Pain/Components/NerveSystemComponent.cs
+++ b/Content.Shared/_Shitmed/Surgery/Pain/Components/NerveSystemComponent.cs
@@ -102,12 +102,14 @@ public sealed partial class NerveSystemComponent : Component
Params = AudioParams.Default.WithVariation(0.2f),
}
},
+ // Arcane-start
{
- Sex.Futanari, new SoundCollectionSpecifier("PainScreamsShortFemale") // Arcane-edit
+ Sex.Futanari, new SoundCollectionSpecifier("PainScreamsShortFemale")
{
Params = AudioParams.Default.WithVariation(0.04f),
}
},
+ // Arcane-end
};
[DataField]
@@ -131,12 +133,14 @@ public sealed partial class NerveSystemComponent : Component
Params = AudioParams.Default.WithVariation(0.2f),
}
},
+ // Arcane-start
{
- Sex.Futanari, new SoundCollectionSpecifier("AgonyScreamsFemale") // Arcane-edit
+ Sex.Futanari, new SoundCollectionSpecifier("AgonyScreamsFemale")
{
Params = AudioParams.Default.WithVariation(0.04f),
}
},
+ // Arcane-end
};
[DataField]
@@ -160,12 +164,14 @@ public sealed partial class NerveSystemComponent : Component
Params = AudioParams.Default.WithVariation(0.2f),
}
},
+ // Arcane-start
{
- Sex.Futanari, new SoundCollectionSpecifier("PainShockScreamsFemale") // Arcane-edit
+ Sex.Futanari, new SoundCollectionSpecifier("PainShockScreamsFemale")
{
Params = AudioParams.Default.WithVariation(0.05f),
}
},
+ // Arcane-end
};
[DataField]
@@ -189,12 +195,14 @@ public sealed partial class NerveSystemComponent : Component
Params = AudioParams.Default,
}
},
+ // Arcane-start
{
- Sex.Futanari, new SoundCollectionSpecifier("CritWhimpersFemale") // Arcane-edit
+ Sex.Futanari, new SoundCollectionSpecifier("CritWhimpersFemale")
{
Params = AudioParams.Default,
}
},
+ // Arcane-end
};
[DataField]
@@ -218,12 +226,14 @@ public sealed partial class NerveSystemComponent : Component
Params = AudioParams.Default,
}
},
+ // Arcane-start
{
- Sex.Futanari, new SoundCollectionSpecifier("PainShockWhimpersFemale") // Arcane-edit
+ Sex.Futanari, new SoundCollectionSpecifier("PainShockWhimpersFemale")
{
Params = AudioParams.Default,
}
},
+ // Arcane-end
};
[DataField]
@@ -247,12 +257,14 @@ public sealed partial class NerveSystemComponent : Component
Params = AudioParams.Default,
}
},
+ // Arcane-start
{
- Sex.Futanari, new SoundCollectionSpecifier("OrganDamagePainedFemale") // Arcane-edit
+ Sex.Futanari, new SoundCollectionSpecifier("OrganDamagePainedFemale")
{
Params = AudioParams.Default,
}
},
+ // Arcane-end
};
[DataField]
@@ -276,12 +288,14 @@ public sealed partial class NerveSystemComponent : Component
Params = AudioParams.Default,
}
},
+ // Arcane-start
{
- Sex.Futanari, new SoundCollectionSpecifier("OrganDamageWhimpersFemale") // Arcane-edit
+ Sex.Futanari, new SoundCollectionSpecifier("OrganDamageWhimpersFemale")
{
Params = AudioParams.Default,
}
},
+ // Arcane-end
};
[DataField("reflexThresholds"), ViewVariables(VVAccess.ReadOnly)]
diff --git a/Resources/Locale/en-US/_Arcane/preferences/humanoid-profile-editor.ftl b/Resources/Locale/en-US/_Arcane/preferences/humanoid-profile-editor.ftl
new file mode 100644
index 00000000000..f1dc78f1553
--- /dev/null
+++ b/Resources/Locale/en-US/_Arcane/preferences/humanoid-profile-editor.ftl
@@ -0,0 +1 @@
+humanoid-profile-editor-sex-futanari-text = Futanari
diff --git a/Resources/Locale/en-US/preferences/ui/humanoid-profile-editor.ftl b/Resources/Locale/en-US/preferences/ui/humanoid-profile-editor.ftl
index 92368f1d661..8324cde89b1 100644
--- a/Resources/Locale/en-US/preferences/ui/humanoid-profile-editor.ftl
+++ b/Resources/Locale/en-US/preferences/ui/humanoid-profile-editor.ftl
@@ -35,7 +35,6 @@ humanoid-profile-editor-sex-label = Sex:
humanoid-profile-editor-sex-male-text = Male
humanoid-profile-editor-sex-female-text = Female
humanoid-profile-editor-sex-unsexed-text = None
-humanoid-profile-editor-sex-futanari-text = Futanari
humanoid-profile-editor-age-label = Age:
humanoid-profile-editor-skin-color-label = Skin color:
humanoid-profile-editor-species-label = Species:
diff --git a/Resources/Locale/ru-RU/_Arcane/preferences/humanoid-profile-editor.ftl b/Resources/Locale/ru-RU/_Arcane/preferences/humanoid-profile-editor.ftl
new file mode 100644
index 00000000000..70a014ac235
--- /dev/null
+++ b/Resources/Locale/ru-RU/_Arcane/preferences/humanoid-profile-editor.ftl
@@ -0,0 +1,4 @@
+humanoid-profile-editor-sex-male-text = Мужской
+humanoid-profile-editor-sex-female-text = Женский
+humanoid-profile-editor-sex-unsexed-text = Отсутствует
+humanoid-profile-editor-sex-futanari-text = Футанари
diff --git a/Resources/Locale/ru-RU/preferences/ui/humanoid-profile-editor.ftl b/Resources/Locale/ru-RU/preferences/ui/humanoid-profile-editor.ftl
index e8433a53377..8902ce72285 100644
--- a/Resources/Locale/ru-RU/preferences/ui/humanoid-profile-editor.ftl
+++ b/Resources/Locale/ru-RU/preferences/ui/humanoid-profile-editor.ftl
@@ -5,10 +5,6 @@ humanoid-profile-editor-appearance-tab = Внешность
humanoid-profile-editor-clothing = Отображать одежду
humanoid-profile-editor-clothing-show = Переключить
humanoid-profile-editor-sex-label = Пол:
-humanoid-profile-editor-sex-male-text = Мужской
-humanoid-profile-editor-sex-female-text = Женский
-humanoid-profile-editor-sex-unsexed-text = Отсутствует
-humanoid-profile-editor-sex-futanari-text = Футанари
humanoid-profile-editor-age-label = Возраст:
humanoid-profile-editor-skin-color-label = Цвет кожи:
humanoid-profile-editor-species-label = Раса:
diff --git a/Resources/Prototypes/_Arcane/Alerts/arousal.yml b/Resources/Prototypes/_Arcane/Alerts/arousal.yml
index 96b7a2bbac2..12ec2034dbd 100644
--- a/Resources/Prototypes/_Arcane/Alerts/arousal.yml
+++ b/Resources/Prototypes/_Arcane/Alerts/arousal.yml
@@ -1,7 +1,6 @@
- type: alertCategory
id: Arousal
-# Shown at ArousalPhase.Aroused (40+)
- type: alert
id: ArousalAroused
category: Arousal
@@ -11,7 +10,6 @@
name: alerts-arousal-aroused-name
description: alerts-arousal-aroused-desc
-# Shown at ArousalPhase.Heated (70+)
- type: alert
id: ArousalHeated
category: Arousal
diff --git a/Resources/Prototypes/_Arcane/Body/Organs/erotic.yml b/Resources/Prototypes/_Arcane/Body/Organs/erotic.yml
index 6e44911ffde..ecc94a08318 100644
--- a/Resources/Prototypes/_Arcane/Body/Organs/erotic.yml
+++ b/Resources/Prototypes/_Arcane/Body/Organs/erotic.yml
@@ -15,6 +15,9 @@
name: anus
description: An internal sphincter muscle.
components:
+ - type: Sprite
+ sprite: _Arcane/Mobs/organs/anus.rsi
+ state: default
- type: Organ
slotId: anus
removable: false
@@ -30,6 +33,9 @@
name: penis
description: A male genital organ.
components:
+ - type: Sprite
+ sprite: _Arcane/Mobs/organs/penis.rsi
+ state: default
- type: Organ
intCap: 15
integrity: 15
@@ -50,6 +56,9 @@
name: testicles
description: Male gonads.
components:
+ - type: Sprite
+ sprite: _Arcane/Mobs/organs/testicles.rsi
+ state: default
- type: Organ
slotId: testicles
removable: false
@@ -65,6 +74,9 @@
name: vagina
description: A female genital organ.
components:
+ - type: Sprite
+ sprite: _Arcane/Mobs/organs/vagina.rsi
+ state: default
- type: Organ
slotId: vagina
removable: false
@@ -79,6 +91,9 @@
name: uterus
description: A female reproductive organ.
components:
+ - type: Sprite
+ sprite: _Arcane/Mobs/organs/uterus.rsi
+ state: default
- type: Organ
slotId: uterus
removable: false
@@ -93,6 +108,9 @@
name: breasts
description: Mammary glands.
components:
+ - type: Sprite
+ sprite: _Arcane/Mobs/organs/breasts.rsi
+ state: default
- type: Organ
intCap: 15
integrity: 15
diff --git a/Resources/Textures/_Arcane/Mobs/organs/anus.rsi/default.png b/Resources/Textures/_Arcane/Mobs/organs/anus.rsi/default.png
new file mode 100644
index 0000000000000000000000000000000000000000..bff66f659d6478c4920d0a5ae23f449c7c7a861a
GIT binary patch
literal 120
zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdzdrud~kcv5Pr#K2S7;qeR(mi|m
zkM)g|FOEqKcUWhX^ZN$P?PHq3Fx_CDqTM2+KnCF{olG+p_=p@}NnONnTe#*}Ama@u
SJ4Jn-
zK}u0lcVqzM2#|-cnEQ;HjqVhQtd&>}poEc7T_9+~fuK3&W6N@<2DcIi;Ey;Egn*~_
zQQ@@CDaP9*_U~$ZO0FIN-
zZ9+mIy{>n!edO}dRH<0=_w5l6K-Nn1-+temn>`EQ7sOn1D^?M;5E#bcYi`Bsfe50c
g?#KYh5g-rY2ZecJ6M5pyg8%>k07*qoM6N<$g7#E=jsO4v
literal 0
HcmV?d00001
diff --git a/Resources/Textures/_Arcane/Mobs/organs/breasts.rsi/meta.json b/Resources/Textures/_Arcane/Mobs/organs/breasts.rsi/meta.json
new file mode 100644
index 00000000000..76917f4cfbb
--- /dev/null
+++ b/Resources/Textures/_Arcane/Mobs/organs/breasts.rsi/meta.json
@@ -0,0 +1,15 @@
+{
+ "version": 1,
+ "size": {
+ "x": 32,
+ "y": 32
+ },
+ "license": "CC-BY-SA-3.0",
+ "copyright": "Taken from SPLURT-Station-13",
+ "states": [
+ {
+ "name": "default",
+ "directions": 1
+ }
+ ]
+}
diff --git a/Resources/Textures/_Arcane/Mobs/organs/penis.rsi/default.png b/Resources/Textures/_Arcane/Mobs/organs/penis.rsi/default.png
new file mode 100644
index 0000000000000000000000000000000000000000..97f2eeafbd158572453ca9771c13c46b5520ec87
GIT binary patch
literal 270
zcmV+p0rCEcP))K8HEsO5VQU>Zo@ljQ(v`krJ5P+ICy
z>;Pl{3Vup`4+11P0Av}g)T0()B&S&ptMMrHJ=qSRCSV#++Zje3z%UvG45Nt<0EY}>
UfIK&2WB>pF07*qoM6N<$g4H5un{Lao|U`9u^Q(E0e9}yFM?C40)eW*c~Sq{
z$!G=WlL`b5-91gi5C{YX%~o~ZoBY2#=F1307VQ_#$L_m=$(#a$f*vUm^0WfvT&{IN
zpva=|POLNxDu^}+8}o3<_57PgRNRs};O0FiaRknK+jz3FMHkX8=fzP~SgibLaQ1AM
zB%O~oV3Yu1Pdva|9|@iE(Wq3*)ovjq*zR}1EylJ{Q0XF80yGP
z156A9444S{J~hG_#rJe|04^XYCIQX`=vk2*z=*3hg!vv6-|s)ZN0HM~)r4pk7Gan*
zbtT#H4+3}-A_N
z|9^e=9KyK-p+w#l{ioCsOr&b~dT1>J$RQy7=EhBiU+;?k)5QV&dYTN}LXr&JcEJ!@
zNRld!U?N7**DodiL2-@_sNo1EVl01lU?12L5e;>?r6BNX!xV}g!9+*_$a1hlpq3yz
zLM$MMVn;9$QlRQ{juC_jIRXZLvyqmrnTS=0&k?YM31WY|be%R1z~>0$bV1XS5Cr6{
z92g!RI{>lp1_Rg;;9!RqPEuA|8ENVO7y$Vi1VAAHvXq;FJitPVVo(YP`5qSJuqdGf
zFp;7dNC9001tMvL-~o56}Pr002ovPDHLk
FV1n#si-Q0F
literal 0
HcmV?d00001
diff --git a/Resources/Textures/_Arcane/Mobs/organs/vagina.rsi/meta.json b/Resources/Textures/_Arcane/Mobs/organs/vagina.rsi/meta.json
new file mode 100644
index 00000000000..76917f4cfbb
--- /dev/null
+++ b/Resources/Textures/_Arcane/Mobs/organs/vagina.rsi/meta.json
@@ -0,0 +1,15 @@
+{
+ "version": 1,
+ "size": {
+ "x": 32,
+ "y": 32
+ },
+ "license": "CC-BY-SA-3.0",
+ "copyright": "Taken from SPLURT-Station-13",
+ "states": [
+ {
+ "name": "default",
+ "directions": 1
+ }
+ ]
+}
From 66da585b5b63ee3df203f972097448dda1b31399 Mon Sep 17 00:00:00 2001
From: MataVsn <113342160+MataVsn@users.noreply.github.com>
Date: Tue, 19 May 2026 02:40:56 +0300
Subject: [PATCH 09/13] =?UTF-8?q?Fix=20anus=20organ=20sprite=20=E2=80=94?=
=?UTF-8?q?=20replace=20blank=20placeholder=20with=20donut=5F7=5Fs?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Co-Authored-By: Claude Sonnet 4.6
---
.../_Arcane/Mobs/organs/anus.rsi/default.png | Bin 120 -> 261 bytes
1 file changed, 0 insertions(+), 0 deletions(-)
diff --git a/Resources/Textures/_Arcane/Mobs/organs/anus.rsi/default.png b/Resources/Textures/_Arcane/Mobs/organs/anus.rsi/default.png
index bff66f659d6478c4920d0a5ae23f449c7c7a861a..880fb824f30c3bbc4220db72d5b65d1bcb43f84e 100644
GIT binary patch
delta 232
zcmVA|Bo)Fx~Y+oE+KFmYWc5EAK)05qp0qH+enr_fANN4<>u{Rni5B_
zkg5S@`JvON8IE7Q3Kp|7HD!=wU|?Y56QEBvJcML9hz9Y=Vg*d(Xaf1zQd=9$2GJls
zxqyXK4WM8K1vku6aPUirkQMwOz({Jd962E(JBr+_$V93Jm?a=fVVGRYL4f2C7V(;nV7*a9k?G#5r1_O@6PP%6=|FOQ2^2IT!;STGJa(>^SxqVDC7^WM{
uQ?y%T6v!YvrITsK0w0kBEUAkaZVT5O3uL_EWT&Xl00f?{elF{r5}E)nV<6!G
From f47650394db57cc0b089aefb050b87f7de5db4b2 Mon Sep 17 00:00:00 2001
From: MataVsn <113342160+MataVsn@users.noreply.github.com>
Date: Tue, 19 May 2026 17:46:25 +0300
Subject: [PATCH 10/13] Hardcode remove.
---
Content.Server/_Arcane/ERP/ArousalSystem.cs | 11 ++--
.../_Arcane/ERP/EroticOrganSpawnSystem.cs | 66 +++++++------------
.../ERP/Organs/EroticOrgansComponent.cs | 41 ++++++++++++
.../Entities/Mobs/Species/human.yml | 19 ++++++
.../Prototypes/_Arcane/Body/Organs/erotic.yml | 2 +-
5 files changed, 93 insertions(+), 46 deletions(-)
create mode 100644 Content.Shared/_Arcane/ERP/Organs/EroticOrgansComponent.cs
diff --git a/Content.Server/_Arcane/ERP/ArousalSystem.cs b/Content.Server/_Arcane/ERP/ArousalSystem.cs
index ad657629c98..2e10db2d8fb 100644
--- a/Content.Server/_Arcane/ERP/ArousalSystem.cs
+++ b/Content.Server/_Arcane/ERP/ArousalSystem.cs
@@ -93,8 +93,12 @@ public void AddArousal(EntityUid uid, float amount, ArousalComponent? comp = nul
if (IsRefractory(comp))
return;
- RaiseLocalEvent(uid, new ArousedEvent(amount));
- SetArousal((uid, comp), GetArousal(comp) + amount);
+ var before = GetArousal(comp);
+ var target = Math.Clamp(before + amount, 0f, comp.MaxArousal);
+ SetArousal((uid, comp), target);
+ var actual = target - before;
+ if (actual > 0f)
+ RaiseLocalEvent(uid, new ArousedEvent(actual));
}
public void ReduceArousal(EntityUid uid, float amount, ArousalComponent? comp = null)
@@ -155,10 +159,9 @@ private void UpdatePhase(Entity entity)
Dirty(entity.Owner, comp);
UpdateAlerts(entity.Owner, ArousalPhase.Calm);
- RaiseLocalEvent(entity.Owner, new ArousalPhaseChangedEvent(previous, ArousalPhase.Peak));
var orgasmEv = new ArousalOrgasmEvent();
RaiseLocalEvent(entity.Owner, ref orgasmEv);
- RaiseLocalEvent(entity.Owner, new ArousalPhaseChangedEvent(ArousalPhase.Peak, ArousalPhase.Calm));
+ RaiseLocalEvent(entity.Owner, new ArousalPhaseChangedEvent(previous, ArousalPhase.Calm));
}
else
{
diff --git a/Content.Shared/_Arcane/ERP/EroticOrganSpawnSystem.cs b/Content.Shared/_Arcane/ERP/EroticOrganSpawnSystem.cs
index 688ee8cf673..fef605a8058 100644
--- a/Content.Shared/_Arcane/ERP/EroticOrganSpawnSystem.cs
+++ b/Content.Shared/_Arcane/ERP/EroticOrganSpawnSystem.cs
@@ -15,55 +15,39 @@ public sealed class EroticOrganSpawnSystem : EntitySystem
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly SharedContainerSystem _containers = default!;
- private static readonly (string Proto, string Slot)[] GroinCommon =
- [
- ("OrganAnus", "anus"),
- ];
-
- private static readonly (string Proto, string Slot)[] GroinMaleOrgans =
- [
- ("OrganPenis", "penis"),
- ("OrganTesticles", "testicles"),
- ];
-
- private static readonly (string Proto, string Slot)[] GroinFemaleOrgans =
- [
- ("OrganVagina", "vagina"),
- ("OrganUterus", "uterus"),
- ];
-
- private static readonly (string Proto, string Slot)[] ChestFemaleOrgans =
- [
- ("OrganBreasts", "breasts"),
- ];
-
public override void Initialize()
{
base.Initialize();
// Run after SharedBodySystem so body parts are already spawned when we look for them.
- SubscribeLocalEvent(OnMapInit, after: [typeof(SharedBodySystem)]);
- SubscribeLocalEvent(OnProfileLoaded);
- SubscribeLocalEvent(OnSexChanged);
+ SubscribeLocalEvent(OnMapInit, after: [typeof(SharedBodySystem)]);
+ SubscribeLocalEvent(OnProfileLoaded);
+ SubscribeLocalEvent(OnSexChanged);
}
- private void OnMapInit(Entity ent, ref MapInitEvent args)
+ private void OnMapInit(Entity ent, ref MapInitEvent args)
{
- SpawnEroticOrgans(ent, ent.Comp.Sex);
+ if (!TryComp(ent, out var humanoid))
+ return;
+
+ SpawnEroticOrgans(ent, ent.Comp, humanoid.Sex);
}
- private void OnProfileLoaded(Entity ent, ref ProfileLoadFinishedEvent args)
+ private void OnProfileLoaded(Entity ent, ref ProfileLoadFinishedEvent args)
{
+ if (!TryComp(ent, out var humanoid))
+ return;
+
RemoveEroticOrgans(ent);
- SpawnEroticOrgans(ent, ent.Comp.Sex);
+ SpawnEroticOrgans(ent, ent.Comp, humanoid.Sex);
}
- private void OnSexChanged(Entity ent, ref SexChangedEvent args)
+ private void OnSexChanged(Entity ent, ref SexChangedEvent args)
{
RemoveEroticOrgans(ent);
- SpawnEroticOrgans(ent, args.NewSex);
+ SpawnEroticOrgans(ent, ent.Comp, args.NewSex);
}
- private void SpawnEroticOrgans(EntityUid uid, Sex sex)
+ private void SpawnEroticOrgans(EntityUid uid, EroticOrgansComponent def, Sex sex)
{
if (sex == Sex.Unsexed)
return;
@@ -73,17 +57,17 @@ private void SpawnEroticOrgans(EntityUid uid, Sex sex)
if (groin.HasValue)
{
- TrySpawnOrgans(uid, groin.Value, GroinCommon);
+ TrySpawnOrgans(uid, groin.Value, def.GroinCommon);
if (sex is Sex.Male or Sex.Futanari)
- TrySpawnOrgans(uid, groin.Value, GroinMaleOrgans);
+ TrySpawnOrgans(uid, groin.Value, def.GroinMale);
if (sex is Sex.Female or Sex.Futanari)
- TrySpawnOrgans(uid, groin.Value, GroinFemaleOrgans);
+ TrySpawnOrgans(uid, groin.Value, def.GroinFemale);
}
if (chest.HasValue && sex is Sex.Female or Sex.Futanari)
- TrySpawnOrgans(uid, chest.Value, ChestFemaleOrgans);
+ TrySpawnOrgans(uid, chest.Value, def.ChestFemale);
var ev = new EroticOrgansSpawnedEvent();
RaiseLocalEvent(uid, ref ev);
@@ -99,15 +83,15 @@ private void RemoveEroticOrgans(EntityUid bodyUid)
}
}
- private void TrySpawnOrgans(EntityUid bodyUid, EntityUid partUid, (string Proto, string Slot)[] organs)
+ private void TrySpawnOrgans(EntityUid bodyUid, EntityUid partUid, List organs)
{
- foreach (var (proto, slot) in organs)
- TrySpawnOrgan(bodyUid, partUid, proto, slot);
+ foreach (var entry in organs)
+ TrySpawnOrgan(bodyUid, partUid, entry.Proto, entry.Slot);
}
- private void TrySpawnOrgan(EntityUid bodyUid, EntityUid partUid, string protoId, string slotId)
+ private void TrySpawnOrgan(EntityUid bodyUid, EntityUid partUid, EntProtoId protoId, string slotId)
{
- if (!_proto.HasIndex(protoId))
+ if (!_proto.HasIndex(protoId))
return;
_body.TryCreateOrganSlot(partUid, slotId, out _);
diff --git a/Content.Shared/_Arcane/ERP/Organs/EroticOrgansComponent.cs b/Content.Shared/_Arcane/ERP/Organs/EroticOrgansComponent.cs
new file mode 100644
index 00000000000..9d3beb25b54
--- /dev/null
+++ b/Content.Shared/_Arcane/ERP/Organs/EroticOrgansComponent.cs
@@ -0,0 +1,41 @@
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared._Arcane.ERP.Organs;
+
+[RegisterComponent]
+public sealed partial class EroticOrgansComponent : Component
+{
+ ///
+ /// Organs spawned for all sexes in the groin slot.
+ ///
+ [DataField]
+ public List GroinCommon = [];
+
+ ///
+ /// Organs spawned for Male and Futanari in the groin slot.
+ ///
+ [DataField]
+ public List GroinMale = [];
+
+ ///
+ /// Organs spawned for Female and Futanari in the groin slot.
+ ///
+ [DataField]
+ public List GroinFemale = [];
+
+ ///
+ /// Organs spawned for Female and Futanari in the chest slot.
+ ///
+ [DataField]
+ public List ChestFemale = [];
+}
+
+[DataDefinition]
+public sealed partial class EroticOrganEntry
+{
+ [DataField(required: true)]
+ public EntProtoId Proto = default!;
+
+ [DataField(required: true)]
+ public string Slot = default!;
+}
diff --git a/Resources/Prototypes/Entities/Mobs/Species/human.yml b/Resources/Prototypes/Entities/Mobs/Species/human.yml
index 044dbbb1cf3..38634787f01 100644
--- a/Resources/Prototypes/Entities/Mobs/Species/human.yml
+++ b/Resources/Prototypes/Entities/Mobs/Species/human.yml
@@ -152,6 +152,25 @@
state: jumpsuit-female
- type: Sprinter # Goob Change: human major
staminaDrainRate: 8
+ # Arcane-start
+ - type: EroticOrgans
+ groinCommon:
+ - proto: OrganAnus
+ slot: anus
+ groinMale:
+ - proto: OrganPenis
+ slot: penis
+ - proto: OrganTesticles
+ slot: testicles
+ groinFemale:
+ - proto: OrganVagina
+ slot: vagina
+ - proto: OrganUterus
+ slot: uterus
+ chestFemale:
+ - proto: OrganBreasts
+ slot: breasts
+ # Arcane-end
- type: entity
diff --git a/Resources/Prototypes/_Arcane/Body/Organs/erotic.yml b/Resources/Prototypes/_Arcane/Body/Organs/erotic.yml
index ecc94a08318..0031d56dcf1 100644
--- a/Resources/Prototypes/_Arcane/Body/Organs/erotic.yml
+++ b/Resources/Prototypes/_Arcane/Body/Organs/erotic.yml
@@ -1,6 +1,6 @@
- type: entity
id: BaseEroticOrgan
- parent: BaseItem
+ parent: BaseHumanOrgan
abstract: true
components:
- type: Organ
From b2d839e87e5a69bbd09cc47f001b869bc0f25b19 Mon Sep 17 00:00:00 2001
From: MataVsn <113342160+MataVsn@users.noreply.github.com>
Date: Tue, 19 May 2026 17:59:25 +0300
Subject: [PATCH 11/13] sync fix
---
Content.Shared/_Arcane/ERP/EroticOrganSpawnSystem.cs | 11 +++++++++++
1 file changed, 11 insertions(+)
diff --git a/Content.Shared/_Arcane/ERP/EroticOrganSpawnSystem.cs b/Content.Shared/_Arcane/ERP/EroticOrganSpawnSystem.cs
index fef605a8058..744468764af 100644
--- a/Content.Shared/_Arcane/ERP/EroticOrganSpawnSystem.cs
+++ b/Content.Shared/_Arcane/ERP/EroticOrganSpawnSystem.cs
@@ -5,6 +5,7 @@
using Content.Shared.Body.Systems;
using Content.Shared.Humanoid;
using Robust.Shared.Containers;
+using Robust.Shared.Network;
using Robust.Shared.Prototypes;
namespace Content.Shared._Arcane.ERP;
@@ -14,6 +15,7 @@ public sealed class EroticOrganSpawnSystem : EntitySystem
[Dependency] private readonly SharedBodySystem _body = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly SharedContainerSystem _containers = default!;
+ [Dependency] private readonly INetManager _net = default!;
public override void Initialize()
{
@@ -26,6 +28,9 @@ public override void Initialize()
private void OnMapInit(Entity ent, ref MapInitEvent args)
{
+ if (!_net.IsServer)
+ return;
+
if (!TryComp(ent, out var humanoid))
return;
@@ -34,6 +39,9 @@ private void OnMapInit(Entity ent, ref MapInitEvent args)
private void OnProfileLoaded(Entity ent, ref ProfileLoadFinishedEvent args)
{
+ if (!_net.IsServer)
+ return;
+
if (!TryComp(ent, out var humanoid))
return;
@@ -43,6 +51,9 @@ private void OnProfileLoaded(Entity ent, ref ProfileLoadF
private void OnSexChanged(Entity ent, ref SexChangedEvent args)
{
+ if (!_net.IsServer)
+ return;
+
RemoveEroticOrgans(ent);
SpawnEroticOrgans(ent, ent.Comp, args.NewSex);
}
From 19e6f3585eae5220ed1a81d3a066e5d7449dce48 Mon Sep 17 00:00:00 2001
From: MataVsn <113342160+MataVsn@users.noreply.github.com>
Date: Tue, 19 May 2026 18:03:26 +0300
Subject: [PATCH 12/13] line
---
Resources/Textures/_Arcane/Mobs/organs/uterus.rsi/meta.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Resources/Textures/_Arcane/Mobs/organs/uterus.rsi/meta.json b/Resources/Textures/_Arcane/Mobs/organs/uterus.rsi/meta.json
index 605f6223bac..76917f4cfbb 100644
--- a/Resources/Textures/_Arcane/Mobs/organs/uterus.rsi/meta.json
+++ b/Resources/Textures/_Arcane/Mobs/organs/uterus.rsi/meta.json
@@ -12,4 +12,4 @@
"directions": 1
}
]
-}
\ No newline at end of file
+}
From 777f6efbac77b84bf3259cd5fd3ee968a2f9392e Mon Sep 17 00:00:00 2001
From: MataVsn <113342160+MataVsn@users.noreply.github.com>
Date: Tue, 19 May 2026 18:18:58 +0300
Subject: [PATCH 13/13] Delta Arousal event
---
Content.Server/_Arcane/ERP/ArousalSystem.cs | 5 ++---
Content.Shared/_Arcane/ERP/ArousalEvents.cs | 9 +++++++--
2 files changed, 9 insertions(+), 5 deletions(-)
diff --git a/Content.Server/_Arcane/ERP/ArousalSystem.cs b/Content.Server/_Arcane/ERP/ArousalSystem.cs
index 2e10db2d8fb..5a2318f2fb0 100644
--- a/Content.Server/_Arcane/ERP/ArousalSystem.cs
+++ b/Content.Server/_Arcane/ERP/ArousalSystem.cs
@@ -96,9 +96,8 @@ public void AddArousal(EntityUid uid, float amount, ArousalComponent? comp = nul
var before = GetArousal(comp);
var target = Math.Clamp(before + amount, 0f, comp.MaxArousal);
SetArousal((uid, comp), target);
- var actual = target - before;
- if (actual > 0f)
- RaiseLocalEvent(uid, new ArousedEvent(actual));
+ if (target > before)
+ RaiseLocalEvent(uid, new ArousedEvent(before, target));
}
public void ReduceArousal(EntityUid uid, float amount, ArousalComponent? comp = null)
diff --git a/Content.Shared/_Arcane/ERP/ArousalEvents.cs b/Content.Shared/_Arcane/ERP/ArousalEvents.cs
index 14da002c59e..4c16a063991 100644
--- a/Content.Shared/_Arcane/ERP/ArousalEvents.cs
+++ b/Content.Shared/_Arcane/ERP/ArousalEvents.cs
@@ -1,8 +1,13 @@
namespace Content.Shared._Arcane.ERP;
-public sealed class ArousedEvent(float amount) : EntityEventArgs
+public sealed class ArousedEvent(float before, float after) : EntityEventArgs
{
- public float Amount = amount;
+ public readonly float Before = before;
+ public readonly float After = after;
+ public float GetDelta()
+ {
+ return After - Before;
+ }
}
public sealed class ArousalPhaseChangedEvent(ArousalPhase previous, ArousalPhase current) : EntityEventArgs