Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Content.Client/Clothing/ClientClothingSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
7 changes: 7 additions & 0 deletions Content.Client/Lobby/UI/HumanoidProfileEditor.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,13 @@ SPDX-License-Identifier: AGPL-3.0-or-later
<Control HorizontalExpand="True"/>
<OptionButton Name="SpawnPriorityButton" HorizontalAlignment="Right" />
</BoxContainer>
<!-- Arcane-Start -->
<BoxContainer HorizontalExpand="True">
<Label Text="{Loc 'humanoid-profile-editor-erp-preference-label'}" />
<Control HorizontalExpand="True"/>
<OptionButton Name="ErpPreferenceButton" HorizontalAlignment="Right" />
</BoxContainer>
<!-- Arcane-End -->
<!-- Height Slider --> <!-- begin Goobstation: port EE height/width sliders -->
<BoxContainer HorizontalExpand="True">
<Label Name="HeightLabel" MinWidth="110" />
Expand Down
37 changes: 37 additions & 0 deletions Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -627,6 +628,23 @@ public HumanoidProfileEditor(

#endregion SpawnPriority

// Arcane-Start
#region ErpPreference

foreach (var value in Enum.GetValues<ErpPreference>())
{
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 =>
Expand Down Expand Up @@ -1349,6 +1367,7 @@ public void SetProfile(HumanoidCharacterProfile? profile, int? slot)
UpdateGenderControls();
UpdateSkinColor();
UpdateSpawnPriorityControls();
UpdateErpPreferenceControls(); // Arcane-edit
UpdateAgeEdit();
UpdateEyePickers();
UpdateSaveButton();
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
194 changes: 194 additions & 0 deletions Content.Server/_Arcane/ERP/ArousalSystem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
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<AlertCategoryPrototype> AlertCategory = "Arousal";
private static readonly ProtoId<AlertPrototype> AlertAroused = "ArousalAroused";
private static readonly ProtoId<AlertPrototype> AlertHeated = "ArousalHeated";

public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ArousalComponent, ComponentInit>(OnInit);
SubscribeLocalEvent<ArousalComponent, ErpPreferenceChangedEvent>(OnErpPreferenceChanged);
}

private void OnInit(Entity<ArousalComponent> ent, ref ComponentInit args)
{
SetArousal(ent, 0f);
}

private void OnErpPreferenceChanged(Entity<ArousalComponent> ent, ref ErpPreferenceChangedEvent args)
{
if (args.NewPreference != ErpPreference.No)
return;

ent.Comp.PassiveSources.Clear();
SetArousal(ent, 0f);
}

/// <summary>
/// Returns current arousal accounting for passive gain/decay since last authoritative set.
/// Passive gain is suppressed during the refractory period.
/// </summary>
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);
}
Comment thread
MataVsn marked this conversation as resolved.

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;

var before = GetArousal(comp);
var target = Math.Clamp(before + amount, 0f, comp.MaxArousal);
SetArousal((uid, comp), target);
if (target > before)
RaiseLocalEvent(uid, new ArousedEvent(before, target));
}

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<ArousalComponent>();
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<ArousalComponent> 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<ArousalComponent> 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);
var orgasmEv = new ArousalOrgasmEvent();
RaiseLocalEvent(entity.Owner, ref orgasmEv);
RaiseLocalEvent(entity.Owner, new ArousalPhaseChangedEvent(previous, 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<ErpStatusComponent>(uid, out var status)
&& status.Preference == ErpPreference.No;
}
}
80 changes: 80 additions & 0 deletions Content.Server/_Arcane/ERP/EroticCoverageSystem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
using Content.Shared._Arcane.ERP;
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<HumanoidAppearanceComponent, EroticOrgansSpawnedEvent>(OnOrgansSpawned);
SubscribeLocalEvent<HumanoidAppearanceComponent, ClothingDidEquippedEvent>(OnEquipped);
SubscribeLocalEvent<HumanoidAppearanceComponent, ClothingDidUnequippedEvent>(OnUnequipped);
}

private void OnOrgansSpawned(Entity<HumanoidAppearanceComponent> ent, ref EroticOrgansSpawnedEvent args)
{
RefreshOrganVisibility(ent);
}

private void OnEquipped(Entity<HumanoidAppearanceComponent> ent, ref ClothingDidEquippedEvent args)
{
RefreshOrganVisibility(ent);
}

private void OnUnequipped(Entity<HumanoidAppearanceComponent> ent, ref ClothingDidUnequippedEvent args)
{
RefreshOrganVisibility(ent);
}
Comment thread
MataVsn marked this conversation as resolved.

private void RefreshOrganVisibility(EntityUid uid)
{
var coverage = SlotFlags.NONE;
var enumerator = _inventory.GetSlotEnumerator(uid, GroinCovering | ChestCovering);
while (enumerator.NextItem(out var item))
{
if (TryComp<ClothingComponent>(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<EroticOrganComponent>((uid, null)))
{
if (!_containers.TryGetContainingContainer(organ.Owner, out var container))
continue;

if (!TryComp<BodyPartComponent>(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);
}
}
}
Loading
Loading