From 2502a9fffe01ce08cddb7d80b1c97b21dcfd6ca4 Mon Sep 17 00:00:00 2001
From: Astra <226853568+EmberAstra@users.noreply.github.com>
Date: Sat, 3 Jan 2026 14:56:01 +0100
Subject: [PATCH] New glimmer events! Foxfire & Restyle (#5102)
* Add glimmer restyle and foxfire
* Fix typo
* Fix typo
* Fix test fail and kobold hair bug (thanks to FaintSpeaker)
* Update Content.Server/_DV/StationEvents/Events/GlimmerRestyleRule.cs
Co-authored-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
Signed-off-by: Astra <226853568+EmberAstra@users.noreply.github.com>
* Update Content.Server/_DV/StationEvents/Events/GlimmerRestyleRule.cs
Co-authored-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
Signed-off-by: Astra <226853568+EmberAstra@users.noreply.github.com>
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Update Content.Server/_DV/StationEvents/Components/GlimmerFoxfireSpawnRuleComponent.cs
Co-authored-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
Signed-off-by: Astra <226853568+EmberAstra@users.noreply.github.com>
* Update Content.Server/_DV/StationEvents/Components/GlimmerFoxfireSpawnRuleComponent.cs
Co-authored-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
Signed-off-by: Astra <226853568+EmberAstra@users.noreply.github.com>
* Update Content.Server/_DV/StationEvents/Events/GlimmerFoxfireSpawnRule.cs
Co-authored-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
Signed-off-by: Astra <226853568+EmberAstra@users.noreply.github.com>
* Update Content.Server/_DV/StationEvents/Events/GlimmerRestyleRule.cs
Co-authored-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
Signed-off-by: Astra <226853568+EmberAstra@users.noreply.github.com>
* Only filter for potential psionics
* Lower restyle glimmer requirement
* Semicolon my beloathed
* Popup false when going bald to bald
* Update Content.Server/_DV/StationEvents/Events/GlimmerRestyleRule.cs
Signed-off-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
---------
Signed-off-by: Astra <226853568+EmberAstra@users.noreply.github.com>
Signed-off-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
Co-authored-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
---
.../GlimmerFoxfireSpawnRuleComponent.cs | 43 +++++++++++
.../Components/GlimmerRestyleRuleComponent.cs | 35 +++++++++
.../Events/GlimmerFoxfireSpawnRule.cs | 59 ++++++++++++++
.../Events/GlimmerRestyleRule.cs | 76 +++++++++++++++++++
.../Humanoid/Markings/MarkingManager.cs | 5 ++
.../Locale/en-US/_DV/abilities/psionic.ftl | 2 +
.../_DV/GameRules/glimmer_events.yml | 36 +++++++++
7 files changed, 256 insertions(+)
create mode 100644 Content.Server/_DV/StationEvents/Components/GlimmerFoxfireSpawnRuleComponent.cs
create mode 100644 Content.Server/_DV/StationEvents/Components/GlimmerRestyleRuleComponent.cs
create mode 100644 Content.Server/_DV/StationEvents/Events/GlimmerFoxfireSpawnRule.cs
create mode 100644 Content.Server/_DV/StationEvents/Events/GlimmerRestyleRule.cs
diff --git a/Content.Server/_DV/StationEvents/Components/GlimmerFoxfireSpawnRuleComponent.cs b/Content.Server/_DV/StationEvents/Components/GlimmerFoxfireSpawnRuleComponent.cs
new file mode 100644
index 0000000000..dee55c9c01
--- /dev/null
+++ b/Content.Server/_DV/StationEvents/Components/GlimmerFoxfireSpawnRuleComponent.cs
@@ -0,0 +1,43 @@
+using Content.Server._DV.StationEvents.Events;
+using Robust.Shared.Prototypes;
+
+namespace Content.Server._DV.StationEvents.Components;
+
+///
+/// Spawns a small amount of randomly-colored foxfires,
+/// centered around either the Oracle or Sophic Grammateus.
+///
+[RegisterComponent, Access(typeof(GlimmerFoxfireSpawnRule))]
+public sealed partial class GlimmerFoxfireSpawnRuleComponent : Component
+{
+ ///
+ /// Minimum+ amounts of foxfires to spawn.
+ ///
+ [DataField]
+ public int MinimumSpawned = 10;
+
+ ///
+ /// Maximum amounts of foxfires to spawn.
+ ///
+ [DataField]
+ public int MaximumSpawned = 20;
+
+ ///
+ /// Maximum distance from the Oracle or Sophic Grammateus in which the
+ /// foxfire will be spawned.
+ ///
+ [DataField]
+ public int SpawnRange = 10;
+
+ ///
+ /// Prototype to be spawned.
+ ///
+ [DataField]
+ public EntProtoId FoxfirePrototype = "Foxfire";
+
+ ///
+ /// Available colors for the foxfire's light.
+ ///
+ [DataField]
+ public List? RandomColorList = new();
+}
diff --git a/Content.Server/_DV/StationEvents/Components/GlimmerRestyleRuleComponent.cs b/Content.Server/_DV/StationEvents/Components/GlimmerRestyleRuleComponent.cs
new file mode 100644
index 0000000000..614b080d2d
--- /dev/null
+++ b/Content.Server/_DV/StationEvents/Components/GlimmerRestyleRuleComponent.cs
@@ -0,0 +1,35 @@
+using Content.Server._DV.StationEvents.Events;
+
+namespace Content.Server._DV.StationEvents.Components;
+
+///
+/// Attempts to change the hair and facial hair markings, plus their color
+/// of a small amount of people.
+///
+[RegisterComponent, Access(typeof(GlimmerRestyleRule))]
+public sealed partial class GlimmerRestyleRuleComponent : Component
+{
+ ///
+ /// Minimum number of valid targets that will get restyled.
+ ///
+ [DataField]
+ public int MinimumTargets = 1;
+
+ ///
+ /// Maximum number of valid targets that will get restyled.
+ ///
+ [DataField]
+ public int MaximumTargets = 5;
+
+ ///
+ /// Chance of completely removing all hair markings instead of selecting a random one.
+ ///
+ [DataField]
+ public float BaldChance = 0.2f;
+
+ ///
+ /// Chance of completely removing all facial hair markings instead of selecting a random one.
+ ///
+ [DataField]
+ public float CleanShavenChance = 0.5f;
+}
diff --git a/Content.Server/_DV/StationEvents/Events/GlimmerFoxfireSpawnRule.cs b/Content.Server/_DV/StationEvents/Events/GlimmerFoxfireSpawnRule.cs
new file mode 100644
index 0000000000..729b2a23e5
--- /dev/null
+++ b/Content.Server/_DV/StationEvents/Events/GlimmerFoxfireSpawnRule.cs
@@ -0,0 +1,59 @@
+using System.Numerics;
+using Content.Server._DV.StationEvents.Components;
+using Content.Server.Nyanotrasen.Research.SophicScribe;
+using Content.Server.Research.Oracle;
+using Content.Server.StationEvents.Events;
+using Content.Shared.GameTicking.Components;
+using Robust.Shared.Map;
+using Robust.Shared.Random;
+
+namespace Content.Server._DV.StationEvents.Events;
+
+public sealed class GlimmerFoxfireSpawnRule : StationEventSystem
+{
+ [Dependency] private readonly SharedPointLightSystem _light = default!;
+
+ protected override void Started(EntityUid uid,
+ GlimmerFoxfireSpawnRuleComponent comp,
+ GameRuleComponent gameRule,
+ GameRuleStartedEvent args)
+ {
+ base.Started(uid, comp, gameRule, args);
+
+ var locations = new List();
+ var queryScribe = EntityQueryEnumerator();
+ var queryOracle = EntityQueryEnumerator();
+
+ while (queryScribe.MoveNext(out _, out var transform))
+ {
+ locations.Add(transform.Coordinates);
+ }
+
+ while (queryOracle.MoveNext(out _, out var transform))
+ {
+ locations.Add(transform.Coordinates);
+ }
+
+ if (locations.Count == 0)
+ return;
+
+ var selectedLocation = RobustRandom.Pick(locations);
+
+ var amountToSpawn = RobustRandom.Next(comp.MinimumSpawned, comp.MaximumSpawned);
+ for (var i = 0; i < amountToSpawn; i++)
+ {
+ var spawnLocation = selectedLocation.Offset(new Vector2(
+ RobustRandom.Next(-comp.SpawnRange, comp.SpawnRange),
+ RobustRandom.Next(-comp.SpawnRange, comp.SpawnRange)
+ ));
+
+
+ var color = Color.GhostWhite;
+ if (comp.RandomColorList != null && comp.RandomColorList.Count != 0)
+ color = RobustRandom.Pick(comp.RandomColorList);
+
+ var fireEnt = Spawn(comp.FoxfirePrototype, spawnLocation);
+ _light.SetColor(fireEnt, color);
+ }
+ }
+}
diff --git a/Content.Server/_DV/StationEvents/Events/GlimmerRestyleRule.cs b/Content.Server/_DV/StationEvents/Events/GlimmerRestyleRule.cs
new file mode 100644
index 0000000000..46448957b1
--- /dev/null
+++ b/Content.Server/_DV/StationEvents/Events/GlimmerRestyleRule.cs
@@ -0,0 +1,76 @@
+using System.Linq;
+using Content.Server._DV.StationEvents.Components;
+using Content.Server.Psionics;
+using Content.Server.StationEvents.Events;
+using Content.Shared.Abilities.Psionics;
+using Content.Shared.GameTicking.Components;
+using Content.Shared.Humanoid;
+using Content.Shared.Humanoid.Markings;
+using Content.Shared.Mobs.Components;
+using Content.Shared.SSDIndicator;
+using Content.Shared.Mobs.Systems;
+using Content.Shared.Popups;
+using Robust.Shared.Random;
+
+namespace Content.Server._DV.StationEvents.Events;
+
+public sealed class GlimmerRestyleRule : StationEventSystem
+{
+ [Dependency] private readonly MobStateSystem _mob = default!;
+ [Dependency] private readonly SharedPopupSystem _popup = default!;
+ [Dependency] private readonly IRobustRandom _random = default!;
+ [Dependency] private readonly MarkingManager _markingManager = default!;
+
+ protected override void Started(EntityUid uid, GlimmerRestyleRuleComponent comp, GameRuleComponent gameRule, GameRuleStartedEvent args)
+ {
+ base.Started(uid, comp, gameRule, args);
+
+ var query = EntityQueryEnumerator();
+ List<(EntityUid, HumanoidAppearanceComponent)> potentialTargets = new();
+
+ while (query.MoveNext(out var entity, out var humanoid, out var mobState))
+ {
+ if (!_mob.IsAlive(entity, mobState) || HasComp(entity) || !HasComp(entity))
+ continue;
+ potentialTargets.Add((entity, humanoid));
+ }
+
+ _random.Shuffle(potentialTargets);
+ var targetsToRestyle = _random.Next(comp.MinimumTargets, comp.MaximumTargets);
+
+ foreach (var (entity, humanoid) in potentialTargets)
+ {
+ if(HasComp(entity))
+ continue;
+
+ if (targetsToRestyle-- <= 0)
+ break;
+
+ var changedHair = TryApplyRestyle((entity, humanoid), MarkingCategories.Hair, comp.BaldChance);
+ var changedFacialHair = TryApplyRestyle((entity, humanoid), MarkingCategories.FacialHair, comp.CleanShavenChance);
+ if (changedHair || changedFacialHair)
+ _popup.PopupEntity(Loc.GetString("glimmer-restyle-event"), entity, entity, PopupType.Medium);
+ Dirty(entity, humanoid);
+ }
+ }
+
+ private bool TryApplyRestyle(Entity ent, MarkingCategories category, float noMarkingsChance)
+ {
+ var newMarkingColor = new Color(_random.NextFloat(), _random.NextFloat(), _random.NextFloat());
+ var availableMarkings = _markingManager.MarkingsByCategoryAndSpecies(category, ent.Comp.Species);
+ if (availableMarkings.Count == 0)
+ return false;
+
+ var hadCategoryBefore = ent.Comp.MarkingSet.TryGetCategory(category, out _);
+ ent.Comp.MarkingSet.RemoveCategory(category);
+ if (_random.Prob(noMarkingsChance))
+ return hadCategoryBefore; //Do not show the popup if you go from no markings to no markings.
+
+ var newMarking = _random.Pick(availableMarkings.Values.ToList()).AsMarking();
+ newMarking.SetColor(newMarkingColor);
+
+ ent.Comp.MarkingSet.AddCategory(category);
+ ent.Comp.MarkingSet.AddFront(category, newMarking);
+ return true;
+ }
+}
diff --git a/Content.Shared/Humanoid/Markings/MarkingManager.cs b/Content.Shared/Humanoid/Markings/MarkingManager.cs
index 28637f9303..d443e96235 100644
--- a/Content.Shared/Humanoid/Markings/MarkingManager.cs
+++ b/Content.Shared/Humanoid/Markings/MarkingManager.cs
@@ -65,6 +65,11 @@ namespace Content.Shared.Humanoid.Markings
var markingPoints = _prototypeManager.Index(speciesProto.MarkingPoints);
var res = new Dictionary();
+ // Begin DeltaV addition - prevents errors when category is missing
+ if (!markingPoints.Points.ContainsKey(category))
+ return res;
+ // End DeltaV addition
+
foreach (var (key, marking) in MarkingsByCategory(category))
{
if ((markingPoints.OnlyWhitelisted || markingPoints.Points[category].OnlyWhitelisted) && marking.SpeciesRestrictions == null)
diff --git a/Resources/Locale/en-US/_DV/abilities/psionic.ftl b/Resources/Locale/en-US/_DV/abilities/psionic.ftl
index 66b2273ced..30f23aa7ad 100644
--- a/Resources/Locale/en-US/_DV/abilities/psionic.ftl
+++ b/Resources/Locale/en-US/_DV/abilities/psionic.ftl
@@ -92,3 +92,5 @@ fractured-form-nobodies = You have no alternate forms to switch to!
fractured-form-sleepy = You feel very sleepy... You should find somewhere to rest.
fractured-form-ssd = { CAPITALIZE(SUBJECT($ent)) } { CONJUGATE-BE($ent) } in a deep sleep. { CAPITALIZE(POSS-ADJ($ent)) } eyes seem to be darting around as if dreaming.
fractured-form-examine-self = You feel a strange connection to { OBJECT($ent) }.
+
+glimmer-restyle-event = You feel like something changed about your looks...
diff --git a/Resources/Prototypes/_DV/GameRules/glimmer_events.yml b/Resources/Prototypes/_DV/GameRules/glimmer_events.yml
index 7d47d72483..8120eb75e2 100644
--- a/Resources/Prototypes/_DV/GameRules/glimmer_events.yml
+++ b/Resources/Prototypes/_DV/GameRules/glimmer_events.yml
@@ -17,6 +17,8 @@
#- id: LockProbers
- id: PsionicNosebleedEvent
- id: MinorMassMindSwap # Delta V
+ - id: GlimmerFoxfireSpawn
+ - id: GlimmerRestyle
- type: entity
parent: BaseGameRule
@@ -197,3 +199,37 @@
- type: GlimmerEvent
minimumGlimmer: 350
- type: PsionicNosebleedRule
+
+- type: entity
+ parent: BaseGlimmerEvent
+ id: GlimmerFoxfireSpawn
+ components:
+ - type: GlimmerEvent
+ minimumGlimmer: 100
+ maximumGlimmer: 500
+ - type: GlimmerFoxfireSpawnRule
+ randomColorList:
+ - aqua
+ - betterviolet
+ - blue
+ - chartreuse
+ - cyan
+ - deeppink
+ - fuchsia
+ - green
+ - indigo
+ - lime
+ - pink
+ - red
+ - silver
+ - yellow
+
+
+- type: entity
+ id: GlimmerRestyle
+ parent: BaseGlimmerSignaturesEvent
+ components:
+ - type: GlimmerEvent
+ minimumGlimmer: 300
+ maximumGlimmer: 700
+ - type: GlimmerRestyleRule