Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
1e5c455
Clothing Stains (#1545)
DuckManZach Jul 6, 2026
1a716c9
we really need to find that injurable pr
wonderfulnewworld Aug 31, 2026
5fc98a3
Small Stain Adjustments (#1647)
DuckManZach Jul 19, 2026
4ec162a
Foobrints (#1626)
DuckManZach Jul 21, 2026
eee9c6d
build fix
wonderfulnewworld Aug 31, 2026
f23170a
Merge branch 'moff/after-all-this-time-finally-stains' of https://git…
wonderfulnewworld Aug 31, 2026
b73cad7
Predict washing machines (#1695)
DuckManZach Jul 23, 2026
255b857
audio prediction port
wonderfulnewworld Aug 31, 2026
9bfcc34
Merge branch 'moff/after-all-this-time-finally-stains' of https://git…
wonderfulnewworld Aug 31, 2026
22379f0
lower wring out time to 10s (from 15)
wonderfulnewworld Aug 31, 2026
4e2e407
Merge branch 'moff/after-all-this-time-finally-stains' of https://git…
wonderfulnewworld Aug 31, 2026
9a4bd41
hop, cmo, and janitors get washing machines
wonderfulnewworld Aug 31, 2026
a3154d3
Merge branch 'moff/after-all-this-time-finally-stains' of https://git…
wonderfulnewworld Aug 31, 2026
f63c5a7
Merge branch 'upstream/solutions-rebirth' of https://github.com/wonde…
wonderfulnewworld Sep 2, 2026
8ec81cc
Merge branch 'moff/after-all-this-time-finally-stains' of https://git…
wonderfulnewworld Sep 2, 2026
b6bb9cb
Reagent Fires, Flammable Clothing Stains & Firelock Edit (#127)
YaraaraY Jun 21, 2026
72d9eae
Reagent Wall Stains & Flammable Wall Stains (#153)
YaraaraY Jun 28, 2026
16ae89f
Flammable Reagent Stains Adjustments, Admin Logging, CCVars (#156)
YaraaraY Jun 25, 2026
ce75162
Utility Pipe Rupture Event (#164)
YaraaraY Jun 28, 2026
0f08e5f
more fire cvars, behaviour improvements and fixes (#187)
YaraaraY Jul 5, 2026
0521835
kill imapmanager
wonderfulnewworld Sep 2, 2026
c67c003
port complete
wonderfulnewworld Sep 2, 2026
77f7ba9
Merge branch 'upstream/solutions-rebirth' of https://github.com/wonde…
wonderfulnewworld Sep 2, 2026
332a2ad
fix misprediction + galoshnts don't leave prints
wonderfulnewworld Sep 2, 2026
22d20ba
Merge branch 'moff/shatter-the-twilight-reverie' of https://github.co…
wonderfulnewworld Sep 2, 2026
9195fee
add atmos fire extinguisher
wonderfulnewworld Sep 2, 2026
08e01c4
loc typo
wonderfulnewworld Sep 2, 2026
1ed61a2
buf transfer amount
wonderfulnewworld Sep 2, 2026
8bf90fa
atmos extinguisher tweaks
wonderfulnewworld Sep 2, 2026
8cd5a21
reagent whitelist for refillable solutions
wonderfulnewworld Sep 2, 2026
998ec34
extinguisher back storage
wonderfulnewworld Sep 2, 2026
84093ae
comment saying what it takes
wonderfulnewworld Sep 2, 2026
c303e21
editor config
wonderfulnewworld Sep 2, 2026
dd0dec1
Merge branch 'upstream/solutions-rebirth' of https://github.com/wonde…
wonderfulnewworld Sep 3, 2026
0f8df08
Merge branch 'moff/after-all-this-time-finally-stains' of https://git…
wonderfulnewworld Sep 3, 2026
7ce08a4
Merge branch 'moff/shatter-the-twilight-reverie' of https://github.co…
wonderfulnewworld Sep 3, 2026
95b85c7
Merge branch 'starlight-dev' into forky/the-everlasting-promised-reag…
walksanatora Sep 3, 2026
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
46 changes: 46 additions & 0 deletions Content.Client/_Funkystation/Footprints/FootprintSystem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using System.Linq;
using Content.Shared._Funkystation.Footprints;
using Robust.Client.GameObjects;
using Robust.Shared.Utility;

namespace Content.Client._Funkystation.Footprints;

public sealed partial class FootprintSystem : EntitySystem
{
[Dependency] private SpriteSystem _sprite = default!;

[SubscribeLocalEvent]
private void OnStartup(Entity<FootprintComponent> entity, ref ComponentStartup args)
{
UpdateVisuals(entity);
}

[SubscribeLocalEvent]
private void OnComponentState(Entity<FootprintComponent> entity, ref AfterAutoHandleStateEvent args)
{
UpdateVisuals(entity);
}

private void UpdateVisuals(Entity<FootprintComponent> entity)
{
if (!TryComp<SpriteComponent>(entity, out var spriteComp))
return;

var sprite = new Entity<SpriteComponent>(entity, spriteComp);
var spriteNullable = sprite.AsNullable();

var printsAndLayers = entity.Comp.Prints.Select((print, index) => (
print,
layer: _sprite.TryGetLayer(spriteNullable, index, out var l, logMissing: false)
? l
: _sprite.AddBlankLayer(sprite, index)
));
foreach (var (print, layer) in printsAndLayers)
{
_sprite.LayerSetOffset(layer, print.Offset);
_sprite.LayerSetRotation(layer, print.Rotation);
_sprite.LayerSetColor(layer, print.Color);
_sprite.LayerSetSprite(layer, new SpriteSpecifier.Rsi(entity.Comp.Sprites, print.State));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
using Content.Client._Starfall.Particles;
using Content.Shared._Funkystation.ReagentFires;
using Content.Shared._Starfall.Particles;
using Robust.Client.GameObjects;

namespace Content.Client._Funkystation.ReagentFires.Systems
{
public sealed partial class ReagentPuddleFireVisualsSystem : EntitySystem
{
[Dependency] private AppearanceSystem _appearance = null!;
[Dependency] private ParticleSystem _particles = null!;
[Dependency] private SharedTransformSystem _transform = null!;

private readonly Dictionary<EntityUid, (ActiveEmitter? Fire, ActiveEmitter? Smoke)> _emitters = new();

public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ReagentPuddleFireEffectComponent, ComponentStartup>(OnCompStartup);
SubscribeLocalEvent<ReagentPuddleFireEffectComponent, AppearanceChangeEvent>(OnAppearanceChange);
SubscribeLocalEvent<ReagentPuddleFireEffectComponent, ComponentShutdown>(OnShutdown);
}

private void OnCompStartup(EntityUid uid, ReagentPuddleFireEffectComponent component, ref ComponentStartup args)
{
UpdateVisuals(uid, null);
}

private void OnShutdown(EntityUid uid, ReagentPuddleFireEffectComponent component, ref ComponentShutdown args)
{
if (_emitters.Remove(uid, out var pair))
{
_particles.RemoveParticle(pair.Fire);
_particles.RemoveParticle(pair.Smoke);
}
}

private void OnAppearanceChange(EntityUid uid, ReagentPuddleFireEffectComponent component, ref AppearanceChangeEvent args)
{
UpdateVisuals(uid, args.Sprite);
}

public override void FrameUpdate(float frameTime)
{
base.FrameUpdate(frameTime);

foreach (var (uid, pair) in _emitters)
{
if (Deleted(uid))
continue;

var coords = _transform.GetMapCoordinates(uid);

if (pair.Fire is { Exhausted: false })
pair.Fire.MapCoords = coords;

if (pair.Smoke is { Exhausted: false })
pair.Smoke.MapCoords = coords;
}
}

private void UpdateVisuals(EntityUid uid, SpriteComponent? sprite)
{
if (sprite == null && !TryComp(uid, out sprite))
return;

if (!_emitters.TryGetValue(uid, out var pair))
{
pair = (null, null);
}

var coords = _transform.GetMapCoordinates(uid);
var updated = false;

if (pair.Fire == null || pair.Fire.Exhausted)
{
pair.Fire = _particles.SpawnEffect("ReagentFireContinuous", coords, uid);
updated = true;
}

if (pair.Smoke == null || pair.Smoke.Exhausted)
{
pair.Smoke = _particles.SpawnEffect("ReagentFireSmoke", coords, uid);
updated = true;
}

if (updated)
{
_emitters[uid] = pair;
}

if (!_appearance.TryGetData<int>(uid, ReagentPuddleFireVisuals.FireState, out var fireState))
{
fireState = 4;
}

var stateString = fireState.ToString();
sprite.LayerSetState(0, stateString);

var intensity = 1f;
var smokeSize = 0.8f;
if (fireState == 5)
{
intensity = 1.5f;
smokeSize = 1.2f;
}
else if (fireState == 6)
{
intensity = 2.0f;
smokeSize = 1.6f;
}

if (pair.Fire != null)
pair.Fire.Intensity = intensity;
if (pair.Smoke != null)
{
pair.Smoke.Intensity = intensity;
var smokeOverrides = new ParticleRuntimeOverrides { ParticleSize = smokeSize };
ParticleSystem.UpdateRuntime(pair.Smoke, smokeOverrides);
}

// Apply synchronized flame color dynamically to the decoupled sprite and standard particle emitters
if (_appearance.TryGetData<Color>(uid, ReagentPuddleFireVisuals.FireColor, out var color))
{
sprite.Color = color;
if (pair.Fire != null)
pair.Fire.ColorOverride = color;
// Soften the smoke tint opacity slightly so it acts as a subtle background element
if (pair.Smoke != null)
pair.Smoke.ColorOverride = color.WithAlpha(0.25f);
}
}
}
}
76 changes: 76 additions & 0 deletions Content.Client/_Funkystation/Stains/StainSystem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using Content.Client.Clothing;
using Content.Client.Items.Systems;
using Content.Shared._Funkystation.Stains.Components;
using Content.Shared._Funkystation.Stains.Systems;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Clothing;
using Content.Shared.FixedPoint;
using Content.Shared.Hands;
using Robust.Client.GameObjects;
using Robust.Shared.Prototypes;

namespace Content.Client._Funkystation.Stains;

public sealed partial class StainSystem : SharedStainSystem
{
[Dependency] private IPrototypeManager _prototypeManager = null!;
[Dependency] private SharedSolutionContainerSystem _solution = null!;
[Dependency] private SpriteSystem _sprite = null!;

public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<StainableComponent, AppearanceChangeEvent>(OnAppearanceChanged);
SubscribeLocalEvent<StainableComponent, GetEquipmentVisualsEvent>(OnEquipmentVisuals, after: [typeof(ClientClothingSystem)]);
SubscribeLocalEvent<StainableComponent, GetInhandVisualsEvent>(OnInhandVisuals, after: [typeof(ItemSystem)]);
}

private void OnAppearanceChanged(Entity<StainableComponent> ent, ref AppearanceChangeEvent args)
{
if (args.Sprite == null)
return;

var spriteEnt = new Entity<SpriteComponent?>(ent.Owner, args.Sprite);

var layers = new List<int>(ent.Comp.RevealedLayers);
layers.Sort((a, b) => b.CompareTo(a));

foreach (var layer in layers)
{
_sprite.RemoveLayer(spriteEnt, layer);
}

ent.Comp.RevealedLayers.Clear();

foreach (var (_, layerData) in BuildVisuals(ent, ent.Comp.IconVisuals, "icon"))
{
ent.Comp.RevealedLayers.Add(_sprite.AddLayer(spriteEnt, layerData, null));
}
}

private void OnEquipmentVisuals(Entity<StainableComponent> ent, ref GetEquipmentVisualsEvent args)
{
if (ent.Comp.ClothingVisuals.TryGetValue(args.Slot, out var layers))
args.Layers.AddRange(BuildVisuals(ent, layers, args.Slot));
}

private void OnInhandVisuals(Entity<StainableComponent> ent, ref GetInhandVisualsEvent args)
{
if (ent.Comp.ItemVisuals.TryGetValue(args.Location.ToString(), out var layers))
args.Layers.AddRange(BuildVisuals(ent, layers, args.Location.ToString()));
}

private IEnumerable<(string, PrototypeLayerData)> BuildVisuals(Entity<StainableComponent> ent, List<PrototypeLayerData> templates, string prefix)
{
if (!_solution.TryGetSolution(ent.Owner, ent.Comp.SolutionName, out _, out var sol) || sol.Volume <= FixedPoint2.Zero)
yield break;

var color = sol.GetColor(_prototypeManager);
for (var i = 0; i < templates.Count; i++)
{
var layer = templates[i];
layer.Color = color;
yield return ($"stain-{prefix}-{i}", layer);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
using Content.Client._Starfall.Particles;
using Content.Shared._Funkystation.ReagentFires;
using Content.Shared._Funkystation.WallStains.Components;
using Robust.Client.GameObjects;
using Robust.Shared.Map;

namespace Content.Client._Funkystation.WallStains.Systems;

public sealed partial class WallStainFireVisualsSystem : EntitySystem
{
[Dependency] private ParticleSystem _particles = null!;
[Dependency] private SharedTransformSystem _transform = null!;
[Dependency] private AppearanceSystem _appearance = default!;

private struct StainEmitters
{
public ActiveEmitter? Fire;
public ActiveEmitter? Embers;
public ActiveEmitter? Slag;
public ActiveEmitter? Sparks;
public ActiveEmitter? Fumes;
}

private readonly Dictionary<EntityUid, StainEmitters> _emitters = new();

public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<WallStainFireVisualsComponent, ComponentStartup>(OnCompStartup);
SubscribeLocalEvent<WallStainFireVisualsComponent, AppearanceChangeEvent>(OnAppearanceChange);
SubscribeLocalEvent<WallStainFireVisualsComponent, ComponentShutdown>(OnShutdown);
}

private void OnCompStartup(EntityUid uid, WallStainFireVisualsComponent component, ref ComponentStartup args)
{
UpdateVisuals(uid);
}

private void OnShutdown(EntityUid uid, WallStainFireVisualsComponent component, ref ComponentShutdown args)
{
if (_emitters.Remove(uid, out var pair))
{
_particles.RemoveParticle(pair.Fire);
_particles.RemoveParticle(pair.Embers);
_particles.RemoveParticle(pair.Slag);
_particles.RemoveParticle(pair.Sparks);
_particles.RemoveParticle(pair.Fumes);
}
}

private void OnAppearanceChange(EntityUid uid, WallStainFireVisualsComponent component, ref AppearanceChangeEvent args)
{
UpdateVisuals(uid);
}

public override void FrameUpdate(float frameTime)
{
base.FrameUpdate(frameTime);
foreach (var (uid, pair) in _emitters)
{
if (Deleted(uid))
continue;
var coords = _transform.GetMapCoordinates(uid);
if (pair.Fire is { Exhausted: false })
pair.Fire.MapCoords = coords;
if (pair.Embers is { Exhausted: false })
pair.Embers.MapCoords = coords;
if (pair.Slag is { Exhausted: false })
pair.Slag.MapCoords = coords;
if (pair.Sparks is { Exhausted: false })
pair.Sparks.MapCoords = coords;
if (pair.Fumes is { Exhausted: false })
pair.Fumes.MapCoords = coords;
}
}

private void UpdateEmitter(ref ActiveEmitter? emitter, string effectId, MapCoordinates coords, EntityUid uid, float intensity, Color color)
{
if (emitter == null || emitter.Exhausted)
emitter = _particles.SpawnEffect(effectId, coords, uid);

if (emitter != null)
{
emitter.Intensity = intensity;
emitter.ColorOverride = color;
}
}

private void UpdateVisuals(EntityUid uid)
{
if (!_emitters.TryGetValue(uid, out var pair))
pair = new StainEmitters();

var coords = _transform.GetMapCoordinates(uid);

var fireState = 4;
if (_appearance.TryGetData<int>(uid, ReagentPuddleFireVisuals.FireState, out var state))
fireState = state;

var color = Color.White;
if (_appearance.TryGetData<Color>(uid, ReagentPuddleFireVisuals.FireColor, out var c))
color = c;

if (TryComp<SpriteComponent>(uid, out var sprite))
{
sprite.LayerSetState(0, fireState.ToString());
sprite.Color = color;
}

var baseIntensity = fireState == 6 ? 2.0f : fireState == 5 ? 1.5f : 1.0f;
var metalFireIntensity = fireState >= 5 ? baseIntensity : 0f;

UpdateEmitter(ref pair.Fire, "WallFire", coords, uid, baseIntensity, color);
UpdateEmitter(ref pair.Embers, "WallFireEmbers", coords, uid, baseIntensity, color);
UpdateEmitter(ref pair.Slag, "WallFireSlag", coords, uid, metalFireIntensity, color);
UpdateEmitter(ref pair.Sparks, "WallFireSparks", coords, uid, metalFireIntensity, color);
UpdateEmitter(ref pair.Fumes, "WallFireFumes", coords, uid, metalFireIntensity, color.WithAlpha(0.35f));

_emitters[uid] = pair;
}
}
Loading
Loading