diff --git a/Content.Client/_Funkystation/Footprints/FootprintSystem.cs b/Content.Client/_Funkystation/Footprints/FootprintSystem.cs new file mode 100644 index 000000000000..2acc548625db --- /dev/null +++ b/Content.Client/_Funkystation/Footprints/FootprintSystem.cs @@ -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 entity, ref ComponentStartup args) + { + UpdateVisuals(entity); + } + + [SubscribeLocalEvent] + private void OnComponentState(Entity entity, ref AfterAutoHandleStateEvent args) + { + UpdateVisuals(entity); + } + + private void UpdateVisuals(Entity entity) + { + if (!TryComp(entity, out var spriteComp)) + return; + + var sprite = new Entity(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)); + } + } +} diff --git a/Content.Client/_Funkystation/ReagentFires/Systems/ReagentPuddleFireVisualsSystem.cs b/Content.Client/_Funkystation/ReagentFires/Systems/ReagentPuddleFireVisualsSystem.cs new file mode 100644 index 000000000000..ed6377c65739 --- /dev/null +++ b/Content.Client/_Funkystation/ReagentFires/Systems/ReagentPuddleFireVisualsSystem.cs @@ -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 _emitters = new(); + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnCompStartup); + SubscribeLocalEvent(OnAppearanceChange); + SubscribeLocalEvent(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(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(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); + } + } + } +} diff --git a/Content.Client/_Funkystation/Stains/StainSystem.cs b/Content.Client/_Funkystation/Stains/StainSystem.cs new file mode 100644 index 000000000000..92f19e9deba9 --- /dev/null +++ b/Content.Client/_Funkystation/Stains/StainSystem.cs @@ -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(OnAppearanceChanged); + SubscribeLocalEvent(OnEquipmentVisuals, after: [typeof(ClientClothingSystem)]); + SubscribeLocalEvent(OnInhandVisuals, after: [typeof(ItemSystem)]); + } + + private void OnAppearanceChanged(Entity ent, ref AppearanceChangeEvent args) + { + if (args.Sprite == null) + return; + + var spriteEnt = new Entity(ent.Owner, args.Sprite); + + var layers = new List(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 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 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 ent, List 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); + } + } +} diff --git a/Content.Client/_Funkystation/WallStains/Systems/FlammableWallFireVisualsSystem.cs b/Content.Client/_Funkystation/WallStains/Systems/FlammableWallFireVisualsSystem.cs new file mode 100644 index 000000000000..4945eda02cad --- /dev/null +++ b/Content.Client/_Funkystation/WallStains/Systems/FlammableWallFireVisualsSystem.cs @@ -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 _emitters = new(); + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnCompStartup); + SubscribeLocalEvent(OnAppearanceChange); + SubscribeLocalEvent(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(uid, ReagentPuddleFireVisuals.FireState, out var state)) + fireState = state; + + var color = Color.White; + if (_appearance.TryGetData(uid, ReagentPuddleFireVisuals.FireColor, out var c)) + color = c; + + if (TryComp(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; + } +} diff --git a/Content.Client/_Funkystation/WallStains/Systems/WallStainOverlaySystem.cs b/Content.Client/_Funkystation/WallStains/Systems/WallStainOverlaySystem.cs new file mode 100644 index 000000000000..e96c0488128f --- /dev/null +++ b/Content.Client/_Funkystation/WallStains/Systems/WallStainOverlaySystem.cs @@ -0,0 +1,24 @@ +using Robust.Client.Graphics; + +namespace Content.Client._Funkystation.WallStains.Systems; + +public sealed partial class WallStainOverlaySystem : EntitySystem +{ + [Dependency] private IOverlayManager _overlayManager = null!; + + private WallStainOverlay _overlay = null!; + + public override void Initialize() + { + base.Initialize(); + + _overlay = new WallStainOverlay(); + _overlayManager.AddOverlay(_overlay); + } + + public override void Shutdown() + { + base.Shutdown(); + _overlayManager.RemoveOverlay(); + } +} diff --git a/Content.Client/_Funkystation/WallStains/Systems/WallStainVisualsSystem.cs b/Content.Client/_Funkystation/WallStains/Systems/WallStainVisualsSystem.cs new file mode 100644 index 000000000000..10f693b6e615 --- /dev/null +++ b/Content.Client/_Funkystation/WallStains/Systems/WallStainVisualsSystem.cs @@ -0,0 +1,32 @@ +using Content.Shared._Funkystation.WallStains; +using Content.Shared._Funkystation.WallStains.Components; +using Robust.Client.GameObjects; + +namespace Content.Client._Funkystation.WallStains.Systems; + +public sealed partial class WallStainVisualsSystem : EntitySystem +{ + [Dependency] private SharedAppearanceSystem _appearance = null!; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnAppearanceChanged); + } + + private void OnAppearanceChanged(EntityUid uid, WallStainComponent component, ref AppearanceChangeEvent args) + { + if (args.Sprite == null) + return; + + if (_appearance.TryGetData(uid, WallStainVisuals.Color, out var color, args.Component)) + { + args.Sprite.Color = color; + } + + if (_appearance.TryGetData(uid, WallStainVisuals.State, out var state, args.Component)) + { + args.Sprite.LayerSetState(0, state); + } + } +} diff --git a/Content.Client/_Funkystation/WallStains/WallStainOverlay.cs b/Content.Client/_Funkystation/WallStains/WallStainOverlay.cs new file mode 100644 index 000000000000..2229279c02e6 --- /dev/null +++ b/Content.Client/_Funkystation/WallStains/WallStainOverlay.cs @@ -0,0 +1,235 @@ +using System.Numerics; +using Content.Client.Graphics; +using Content.Client.Light; +using Content.Shared._Funkystation.WallStains.Components; +using Content.Shared.Tag; +using Robust.Client.GameObjects; +using Robust.Client.Graphics; +using Robust.Shared.Enums; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; +using Robust.Shared.Utility; + +namespace Content.Client._Funkystation.WallStains; + +public sealed partial class WallStainOverlay : Overlay +{ + private static readonly ProtoId UnshadedShader = "unshaded"; + private static readonly ProtoId StencilMaskShader = "StencilMask"; + private static readonly ProtoId StencilEqualDrawShader = "StencilEqualDraw"; + + private static readonly ProtoId DirectionalWindowTag = "DirectionalWindow"; + private static readonly ProtoId WallTag = "Wall"; + private static readonly ProtoId WindowTag = "Window"; + private static readonly ProtoId AirlockTag = "Airlock"; + + [Dependency] private IClyde _clyde = null!; + [Dependency] private IEntityManager _entityManager = null!; + [Dependency] private IPrototypeManager _prototypeManager = null!; + [Dependency] private IGameTiming _gameTiming = null!; + private readonly SharedMapSystem _maps; + + private readonly TransformSystem _transformSystem; + private readonly SpriteSystem _spriteSystem; + private readonly EntityLookupSystem _entityLookupSystem; + private readonly TagSystem _tagSystem; + + private readonly EntityQuery _transformQuery; + + public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowFOV; + + private readonly HashSet> _visibleStains = []; + private readonly HashSet _tempEntities = []; + private readonly HashSet _intersectingEntities = []; + private readonly OverlayResourceCache _resources = new(); + + private const float DblPixelsPerMeter = 2f * EyeManager.PixelsPerMeter; + + public WallStainOverlay() + { + IoCManager.InjectDependencies(this); + + _maps = _entityManager.System(); + _transformSystem = _entityManager.System(); + _spriteSystem = _entityManager.System(); + _entityLookupSystem = _entityManager.System(); + _tagSystem = _entityManager.System(); + + _transformQuery = _entityManager.GetEntityQuery(); + + ZIndex = AfterLightTargetOverlay.ContentZIndex + 1; + } + + protected override void Draw(in OverlayDrawArgs args) + { + var viewport = args.Viewport; + var mapId = args.MapId; + var worldBounds = args.WorldBounds; + var worldHandle = args.WorldHandle; + var target = viewport.RenderTarget; + var invMatrix = viewport.GetWorldToLocalMatrix(); + var realTime = _gameTiming.RealTime; + + _visibleStains.Clear(); + _entityLookupSystem.GetEntitiesIntersecting(mapId, worldBounds, _visibleStains); + + if (_visibleStains.Count == 0) + return; + + var res = _resources.GetForViewport(viewport, static _ => new CachedResources()); + + if (res.StainTarget?.Texture.Size != target.Size) + { + res.StainTarget?.Dispose(); + res.StainTarget = _clyde.CreateRenderTarget(target.Size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "stain-stencil-target"); + } + + args.WorldHandle.RenderInRenderTarget(res.StainTarget, + () => + { + _intersectingEntities.Clear(); + _tempEntities.Clear(); + + worldHandle.UseShader(_prototypeManager.Index(UnshadedShader).Instance()); + + foreach (var stainEntity in _visibleStains) + { + if (!_transformQuery.TryGetComponent(stainEntity.Owner, out var stainXform)) + continue; + + var stainWorldPos = _transformSystem.GetWorldPosition(stainXform); + var queryBox = Box2.CenteredAround(stainWorldPos, new Vector2(3.0f, 3.0f)); + + _entityLookupSystem.GetEntitiesIntersecting(mapId, queryBox, _tempEntities, LookupFlags.Static); + + foreach (var uid in _tempEntities) + { + // We only want to draw stencil masks on entities that have a Sprite and Transform + if (!_transformQuery.TryGetComponent(uid, out var transformComponent) || + !_entityManager.TryGetComponent(uid, out _)) + continue; + + // Andddd only draw stains onto anchored entities + if (!transformComponent.Anchored) + continue; + + // AAAAAAAAAAND directional windows don't cover the full tile, so skip them to avoid floating stains + if (_tagSystem.HasTag(uid, DirectionalWindowTag)) + continue; + + // Finally, make sure the entity is one of the following: + if (!_tagSystem.HasTag(uid, WallTag) && + !_tagSystem.HasTag(uid, WindowTag) && + !_tagSystem.HasTag(uid, AirlockTag)) + { + continue; + } + + _intersectingEntities.Add(uid); + } + _tempEntities.Clear(); + } + + foreach (var uid in _intersectingEntities) + { + if (!_transformQuery.TryGetComponent(uid, out var transformComponent) || + !_entityManager.TryGetComponent(uid, out var spriteComponent)) + continue; + + if (transformComponent.GridUid == null) + continue; + + var gridUid = transformComponent.GridUid.Value; + var localMatrix = Matrix3x2.Multiply(_transformSystem.GetWorldMatrix(gridUid, _transformQuery), invMatrix); + worldHandle.SetTransform(localMatrix); + + var bounds = _spriteSystem.CalculateBounds((uid, spriteComponent), transformComponent.Coordinates.Position, transformComponent.LocalRotation, viewport.Eye?.Rotation ?? Angle.Zero); + worldHandle.DrawRect(bounds, Color.White); + } + + }, + Color.Transparent); + + worldHandle.SetTransform(Matrix3x2.Identity); + + worldHandle.UseShader(_prototypeManager.Index(StencilMaskShader).Instance()); + worldHandle.DrawTextureRect(res.StainTarget.Texture, worldBounds); + + worldHandle.UseShader(_prototypeManager.Index(StencilEqualDrawShader).Instance()); + + foreach (var stainEntity in _visibleStains) + { + var uid = stainEntity.Owner; + var stain = stainEntity.Comp; + + if (!_transformQuery.TryGetComponent(uid, out var xform)) + continue; + + var state = string.IsNullOrEmpty(stain.StainState) ? "splatter" : stain.StainState; + var rsiSpec = new SpriteSpecifier.Rsi(new ResPath("/Textures/Effects/crayondecals.rsi"), state); + + Texture? texture; + try + { + texture = _spriteSystem.GetFrame(rsiSpec, realTime); + } + catch (Exception) + { + try + { + var fallbackSpec = new SpriteSpecifier.Rsi(new ResPath("/Textures/Effects/crayondecals.rsi"), "splatter"); + texture = _spriteSystem.GetFrame(fallbackSpec, realTime); + } + catch (Exception) + { + continue; + } + } + + var convertedTextureWidth = texture.Width / DblPixelsPerMeter; + var convertedTextureHeight = texture.Height / DblPixelsPerMeter; + + var (_, _, worldMatrix) = _transformSystem.GetWorldPositionRotationMatrix(xform); + worldHandle.SetTransform(worldMatrix); + + var scaleX = 1.0f; + var scaleY = 1.0f; + + if (stain.Direction.Y != 0) + { + scaleX = 2.2f; + } + else if (stain.Direction.X != 0) + { + scaleY = 2.2f; + } + + var rect = new Box2(-convertedTextureWidth * scaleX, -convertedTextureHeight * scaleY, convertedTextureWidth * scaleX, convertedTextureHeight * scaleY); + + worldHandle.DrawTextureRect( + texture, + rect, + modulate: stain.Color + ); + } + + worldHandle.SetTransform(Matrix3x2.Identity); + worldHandle.UseShader(null); + } + + protected override void DisposeBehavior() + { + _resources.Dispose(); + base.DisposeBehavior(); + } + + private sealed class CachedResources : IDisposable + { + public IRenderTexture? StainTarget; + + public void Dispose() + { + StainTarget?.Dispose(); + } + } +} diff --git a/Content.Client/_Funkystation/WashingMachine/WashingMachineSystem.cs b/Content.Client/_Funkystation/WashingMachine/WashingMachineSystem.cs new file mode 100644 index 000000000000..be11cdda5ba9 --- /dev/null +++ b/Content.Client/_Funkystation/WashingMachine/WashingMachineSystem.cs @@ -0,0 +1,5 @@ +using Content.Shared._Funkystation.WashingMachine; + +namespace Content.Client._Funkystation.WashingMachine; + +public sealed class WashingMachineSystem : SharedWashingMachineSystem; diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Hotspot.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Hotspot.cs index 30dff799e178..341897890c1e 100644 --- a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Hotspot.cs +++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Hotspot.cs @@ -1,3 +1,4 @@ +using Content.Server._Funkystation.Atmos.Events; // Funky using Content.Server.Atmos.Components; using Content.Server.Decals; using Content.Shared.Atmos; @@ -204,6 +205,10 @@ private void HotspotExpose(GridAtmosphereComponent gridAtmosphere, if (tile.Air == null) return; + // Funky start + var ev = new TileExposedEvent(tile.GridIndices, exposedTemperature, exposedVolume, sparkSourceUid); + RaiseLocalEvent(gridAtmosphere.Owner, ref ev); + // Funky end var oxygen = tile.Air.GetMoles(Gas.Oxygen); if (oxygen < 0.5f) diff --git a/Content.Server/Chemistry/EntitySystems/VaporSystem.cs b/Content.Server/Chemistry/EntitySystems/VaporSystem.cs index 6b3e39208018..c69e95493cdd 100644 --- a/Content.Server/Chemistry/EntitySystems/VaporSystem.cs +++ b/Content.Server/Chemistry/EntitySystems/VaporSystem.cs @@ -15,6 +15,7 @@ using Robust.Shared.Prototypes; using Robust.Shared.Spawners; using System.Numerics; +using Content.Shared._Funkystation.WallStains; // Funky Wall Stains using Content.Shared.Vapor; namespace Content.Server.Chemistry.EntitySystems @@ -40,8 +41,22 @@ public override void Initialize() private void HandleCollide(Entity entity, ref StartCollideEvent args) { - var solution = Comp(entity).Solution; - _reactive.DoEntityReaction(args.OtherEntity, solution, ReactionMethod.Touch); + // Funky Wall Stains + var hitWall = (args.OtherFixture.CollisionLayer & (int)CollisionGroup.Impassable) != 0 + && args.OtherFixture.Hard; + + foreach (var (_, soln) in _solutionContainer.EnumerateSolutions(entity.Owner)) + { + var solution = soln.Comp.Solution; + _reactive.DoEntityReaction(args.OtherEntity, solution, ReactionMethod.Touch); + + // Funky Wall Stains + if (hitWall && solution.Volume > 0) + { + var splashEv = new SplashOnWallEvent(Transform(entity.Owner).Coordinates, solution.Clone()); + RaiseLocalEvent(ref splashEv); + } + } // Check for collision with a impassable object (e.g. wall) and stop if ((args.OtherFixture.CollisionLayer & (int)CollisionGroup.Impassable) != 0 && args.OtherFixture.Hard) diff --git a/Content.Server/Doors/Systems/FirelockSystem.cs b/Content.Server/Doors/Systems/FirelockSystem.cs index ee78f5cf1f6b..157117c4eb4b 100644 --- a/Content.Server/Doors/Systems/FirelockSystem.cs +++ b/Content.Server/Doors/Systems/FirelockSystem.cs @@ -60,7 +60,8 @@ public override void Update(float frameTime) // only bother to check pressure on doors that are some variation of closed. if (door.State != DoorState.Closed && door.State != DoorState.Welded - && door.State != DoorState.Denying) + && door.State != DoorState.Denying + && door.State != DoorState.Open) // Funky change { continue; } @@ -69,17 +70,29 @@ public override void Update(float frameTime) && xformQuery.TryGetComponent(uid, out var xform) && appearanceQuery.TryGetComponent(uid, out var appearance)) { - var (pressure, fire) = CheckPressureAndFire(uid, firelock, xform, airtight, airtightQuery); - _appearance.SetData(uid, DoorVisuals.ClosedLights, fire || pressure, appearance); - firelock.Temperature = fire; - firelock.Pressure = pressure; - _appearance.SetData(uid, FirelockVisuals.PressureWarning, pressure, appearance); - _appearance.SetData(uid, FirelockVisuals.TemperatureWarning, fire, appearance); - Dirty(uid, firelock); - - if (pointLightQuery.TryComp(uid, out var pointLight)) + var (pressure, fire) = CheckPressureAndFire(uid, firelock, xform, airtight, airtightQuery, door.State == DoorState.Open); // Funky change + + // Funky change + if (door.State == DoorState.Open) + { + if (pressure || fire) + { + EmergencyPressureStop(uid, firelock, door); + } + } + else { - _pointLight.SetEnabled(uid, fire | pressure, pointLight); + _appearance.SetData(uid, DoorVisuals.ClosedLights, fire || pressure, appearance); + firelock.Temperature = fire; + firelock.Pressure = pressure; + _appearance.SetData(uid, FirelockVisuals.PressureWarning, pressure, appearance); + _appearance.SetData(uid, FirelockVisuals.TemperatureWarning, fire, appearance); + Dirty(uid, firelock); + + if (pointLightQuery.TryComp(uid, out var pointLight)) + { + _pointLight.SetEnabled(uid, fire | pressure, pointLight); + } } } } @@ -113,13 +126,14 @@ private void OnAtmosAlarm(EntityUid uid, FirelockComponent component, AtmosAlarm } public (bool Pressure, bool Fire) CheckPressureAndFire( - EntityUid uid, - FirelockComponent firelock, - TransformComponent xform, - AirtightComponent airtight, - EntityQuery airtightQuery) + EntityUid uid, + FirelockComponent firelock, + TransformComponent xform, + AirtightComponent airtight, + EntityQuery airtightQuery, + bool checkEvenIfOpen = false) // Funky change { - if (!airtight.AirBlocked) + if (!checkEvenIfOpen && !airtight.AirBlocked) // Funky change return (false, false); if (TryComp(uid, out DockingComponent? dock) && dock.Docked) @@ -128,8 +142,13 @@ private void OnAtmosAlarm(EntityUid uid, FirelockComponent component, AtmosAlarm return (false, false); } - if (!HasComp(xform.ParentUid)) + // Funky change + if (!HasComp(xform.ParentUid) || + !HasComp(xform.ParentUid) || + !HasComp(xform.MapUid)) + { return (false, false); + } var grid = Comp(xform.ParentUid); var pos = _mapping.CoordinatesToTile(xform.ParentUid, grid, xform.Coordinates); diff --git a/Content.Server/Fluids/EntitySystems/PuddleSystem.cs b/Content.Server/Fluids/EntitySystems/PuddleSystem.cs index d1c6bd3d3b33..6bf915b6c39e 100644 --- a/Content.Server/Fluids/EntitySystems/PuddleSystem.cs +++ b/Content.Server/Fluids/EntitySystems/PuddleSystem.cs @@ -1,3 +1,4 @@ +using Content.Server._Funkystation.ReagentFires.Systems; using Content.Server.Fluids.Components; using Content.Server.Spreader; using Content.Shared.Chemistry; @@ -14,9 +15,18 @@ using Content.Shared.Maps; using Content.Shared.Popups; using Content.Shared.Slippery; +using Content.Shared.Inventory; +using Content.Shared._Funkystation.Fluids; +using Content.Shared.Gravity; +using Content.Shared.Standing; +using Content.Shared.StepTrigger.Systems; +using Content.Shared._Funkystation.Footprints; +using Content.Shared._Funkystation.WallStains; using Robust.Shared.Collections; using Robust.Shared.Map; using Robust.Shared.Map.Components; +using Robust.Shared.Physics.Components; +using Robust.Shared.Physics.Events; using Robust.Shared.Player; using Robust.Shared.Prototypes; using Robust.Shared.Random; @@ -36,8 +46,11 @@ public sealed partial class PuddleSystem : SharedPuddleSystem [Dependency] private SharedSolutionContainerSystem _solutionContainerSystem = default!; [Dependency] private SharedTransformSystem _transform = default!; [Dependency] private TurfSystem _turf = default!; + [Dependency] private EntityQuery _puddleQuery = default!; + [Dependency] private EntityQuery _evaporationSparklesQuery = default!; - private EntityQuery _puddleQuery; + [Dependency] private EntityQuery _footprintQuery; // Moff - Funky footprints + [Dependency] private ReagentFireSystem _fireSystem = default!; // Funky - Reagent Fires /* * TODO: Need some sort of way to do blood slash / vomit solution spill on its own @@ -265,6 +278,18 @@ private void OnPuddleSlip(Entity entity, ref SlipEvent args) // Take 15% of the puddle solution var splitSol = _solutionContainerSystem.SplitSolution(entity.Comp.Solution.Value, solution.Volume * 0.15f); Reactive.DoEntityReaction(args.Slipped, splitSol, ReactionMethod.Touch); + + // Funky - Start - Clothing stains + if (splitSol.Volume > 0) + { + var stainEv = new SpilledOnEvent(entity.Owner, splitSol.Clone()); + RaiseLocalEvent(args.Slipped, stainEv); + + // Funky Wall Stains + var splashEv = new SplashOnWallEvent(Transform(entity.Owner).Coordinates, splitSol.Clone()); + RaiseLocalEvent(ref splashEv); + } + // Funky - End } /// @@ -430,6 +455,12 @@ public override bool TrySplashSpillAt(EntityUid entity, targets.Add(owner); Reactive.DoEntityReaction(owner, splitSolution, ReactionMethod.Touch); + + // Funky - Start - Clothing stains + if (splitSolution.Volume > 0) + RaiseLocalEvent(owner, new SpilledOnEvent(entity, splitSolution.Clone())); + // Funky - End + Popups.PopupEntity(Loc.GetString("spill-land-spilled-on-other", ("spillable", entity), ("target", Identity.Entity(owner, EntityManager))), @@ -440,6 +471,10 @@ public override bool TrySplashSpillAt(EntityUid entity, _color.RaiseEffect(spilled.GetColor(_prototypeManager), targets, Filter.Pvs(entity, entityManager: EntityManager)); + // Funky Wall Stains + var splashEv = new SplashOnWallEvent(coordinates, spilled.Clone()); + RaiseLocalEvent(ref splashEv); + return TrySpillAt(coordinates, spilled, out puddleUid, sound); } @@ -532,6 +567,11 @@ public override bool TrySpillAt(TileRef tileRef, Solution solution, out EntityUi if (!puddleQuery.TryGetComponent(ent, out var puddle)) continue; + // Funky start - footprints + if (_footprintQuery.HasComponent(ent.Value)) + continue; + // Funky end + if (TryAddSolution(ent.Value, solution, sound, puddleComponent: puddle)) { EnsureComp(ent.Value); @@ -572,10 +612,24 @@ public bool TryGetPuddle(TileRef tile, out EntityUid puddleUid) if (!puddleQuery.HasComponent(ent.Value)) continue; + // Funky start - footprints + if (_footprintQuery.HasComponent(ent.Value)) + continue; + // Funky end + puddleUid = ent.Value; return true; } return false; } + + // Funky edit - handle reagent fire + protected override void OnSolutionUpdate(Entity entity, ref SolutionChangedEvent args) + { + base.OnSolutionUpdate(entity, ref args); + _fireSystem.UpdateFire(entity); + } + // Funky edit end + } diff --git a/Content.Server/_Funkystation/Atmos/Events/TileExposedEvent.cs b/Content.Server/_Funkystation/Atmos/Events/TileExposedEvent.cs new file mode 100644 index 000000000000..600c66d469ef --- /dev/null +++ b/Content.Server/_Funkystation/Atmos/Events/TileExposedEvent.cs @@ -0,0 +1,5 @@ +namespace Content.Server._Funkystation.Atmos.Events +{ + [ByRefEvent] + public readonly record struct TileExposedEvent(Vector2i Tile, float Temperature, float Volume, EntityUid? SparkSource); +} diff --git a/Content.Server/_Funkystation/ReagentFires/Components/ReagentPuddleFireComponent.cs b/Content.Server/_Funkystation/ReagentFires/Components/ReagentPuddleFireComponent.cs new file mode 100644 index 000000000000..3faabb4e34ce --- /dev/null +++ b/Content.Server/_Funkystation/ReagentFires/Components/ReagentPuddleFireComponent.cs @@ -0,0 +1,38 @@ +using Robust.Shared.Audio; + +namespace Content.Server._Funkystation.ReagentFires.Components +{ + /// + /// Added to puddles that contain flammable reagents and are currently burning. + /// + [RegisterComponent] + public sealed partial class ReagentPuddleFireComponent : Component + { + [ViewVariables] + public bool OnFire { get; set; } + + [ViewVariables] + public int FireState { get; set; } = 4; + + [ViewVariables] + public int Flammability { get; set; } + + [ViewVariables] + public bool SelfOxidizing { get; set; } + + [ViewVariables] + public float Accumulator { get; set; } + + [ViewVariables] + public EntityUid? PlayingStream { get; set; } + + [ViewVariables] + public EntityUid? FireEffectEntity { get; set; } + + [ViewVariables(VVAccess.ReadWrite), DataField("sound")] + public SoundSpecifier LoopingSound { get; set; } = new SoundPathSpecifier("/Audio/_Funkystation/Effects/Fire/bigfire.ogg"); + + [ViewVariables] + public float VolumeFactor { get; set; } = 1f; + } +} diff --git a/Content.Server/_Funkystation/ReagentFires/Systems/ReagentFireSystem.cs b/Content.Server/_Funkystation/ReagentFires/Systems/ReagentFireSystem.cs new file mode 100644 index 000000000000..1a7e2b4962fb --- /dev/null +++ b/Content.Server/_Funkystation/ReagentFires/Systems/ReagentFireSystem.cs @@ -0,0 +1,556 @@ +using Content.Server._Funkystation.Atmos.Events; +using Content.Server._Funkystation.ReagentFires.Components; +using Content.Server.Atmos.Components; +using Content.Server.Atmos.EntitySystems; +using Content.Server.Decals; +using Content.Shared._Funkystation.CCVar; +using Content.Shared._Funkystation.Footprints; +using Content.Shared._Funkystation.ReagentFires; +using Content.Shared.Atmos; +using Content.Shared.Chemistry.EntitySystems; +using Content.Shared.Clothing.Components; +using Content.Shared.Damage; +using Content.Shared.Damage.Components; +using Content.Shared.Damage.Prototypes; +using Content.Shared.Damage.Systems; +using Content.Shared.FixedPoint; +using Content.Shared.Fluids.Components; +using Content.Shared.Inventory; +using Content.Shared.Mobs.Components; +using Robust.Server.GameObjects; +using Robust.Shared.Audio; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Configuration; +using Robust.Shared.Map; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; + +namespace Content.Server._Funkystation.ReagentFires.Systems +{ + public sealed partial class ReagentFireSystem : EntitySystem + { + [Dependency] private AtmosphereSystem _atmos = null!; + [Dependency] private SharedTransformSystem _transform = null!; + [Dependency] private SharedSolutionContainerSystem _solutionContainerSystem = null!; + [Dependency] private IPrototypeManager _prototypeManager = null!; + [Dependency] private SharedAppearanceSystem _appearance = null!; + [Dependency] private EntityLookupSystem _lookup = null!; + [Dependency] private SharedAudioSystem _audio = null!; + [Dependency] private SharedPointLightSystem _light = null!; + [Dependency] private DecalSystem _decalSystem = null!; + [Dependency] private IRobustRandom _random = null!; + [Dependency] private DamageableSystem _damageable = null!; + [Dependency] private IConfigurationManager _cfg = null!; + [Dependency] private InventorySystem _inventory = null!; + + private readonly List _toExtinguish = new(); + private readonly string[] _burntDecals = ["burnt1", "burnt2", "burnt3", "burnt4"]; + private float _puddleDamageMultiplier = 1.0f; + private readonly List<(EntityUid Uid, ReagentPuddleFireComponent FireComp, PuddleComponent Puddle, TransformComponent Xform)> _activeFires = new(); + private const string StructuralDamage = "Structural"; + private const string HeatDamage = "Heat"; + private bool _footprintsFlammable = true; + private float _fireProtectionEffectiveness = 1.0f; + private bool _volumeScalingEnabled = true; + private float _volumeScalingReference = 20f; + private float _volumeScalingCurve = 1.5f; + private float _smallPuddleBurnThreshold = 1.0f; + private float _smallPuddleBurnPercent = 0.5f; + + public override void Initialize() + { + base.Initialize(); + Subs.CVar(_cfg, ReagentFireCVars.PuddleFireDamageMultiplier, value => _puddleDamageMultiplier = value, true); + Subs.CVar(_cfg, ReagentFireCVars.FootprintsFlammable, value => _footprintsFlammable = value, true); + Subs.CVar(_cfg, ReagentFireCVars.FireProtectionEffectiveness, value => _fireProtectionEffectiveness = value, true); + Subs.CVar(_cfg, ReagentFireCVars.VolumeScalingEnabled, value => _volumeScalingEnabled = value, true); + Subs.CVar(_cfg, ReagentFireCVars.VolumeScalingReference, value => _volumeScalingReference = value, true); + Subs.CVar(_cfg, ReagentFireCVars.VolumeScalingCurve, value => _volumeScalingCurve = value, true); + Subs.CVar(_cfg, ReagentFireCVars.SmallPuddleBurnThreshold, value => _smallPuddleBurnThreshold = value, true); + Subs.CVar(_cfg, ReagentFireCVars.SmallPuddleBurnPercent, value => _smallPuddleBurnPercent = value, true); + SubscribeLocalEvent(OnTileExposed); + SubscribeLocalEvent(OnPuddleTileFire); + SubscribeLocalEvent(OnFireShutdown); + } + + private void OnFireShutdown(EntityUid uid, ReagentPuddleFireComponent component, ref ComponentShutdown args) + { + if (component.PlayingStream != null) + { + _audio.Stop(component.PlayingStream); + component.PlayingStream = null; + } + + if (component.FireEffectEntity != null) + { + QueueDel(component.FireEffectEntity.Value); + component.FireEffectEntity = null; + } + } + + /// + /// 0-1 intensity factor based on solution volume relative to the reference volume + /// Small puddles burn proportionally weaker instead of matching a full puddle + /// + private float GetVolumeFactor(FixedPoint2 volume) + { + if (!_volumeScalingEnabled || _volumeScalingReference <= 0f) + return 1f; + + var ratio = Math.Clamp(volume.Float() / _volumeScalingReference, 0f, 1f); + return MathF.Pow(ratio, _volumeScalingCurve); + } + + public void UpdateFire(Entity ent) + { + if (ent.Comp.Solution == null) + return; + + if (!_footprintsFlammable && HasComp(ent)) + { + if (HasComp(ent)) + Extinguish(ent); + return; + } + + var solution = ent.Comp.Solution.Value.Comp.Solution; + var flammability = solution.GetSolutionFlammability(_prototypeManager); + var selfOxidizing = solution.IsSolutionSelfOxidizing(_prototypeManager); + + if (flammability <= 0) + { + if (HasComp(ent)) + { + Extinguish(ent); + } + return; + } + + var fireComp = EnsureComp(ent); + fireComp.Flammability = flammability; + fireComp.SelfOxidizing = selfOxidizing; + fireComp.VolumeFactor = GetVolumeFactor(solution.Volume); + + var effectiveFlammability = flammability * fireComp.VolumeFactor; + + if (effectiveFlammability > 10) + fireComp.FireState = 6; + else if (effectiveFlammability > 5) + fireComp.FireState = 5; + else + fireComp.FireState = 4; + + if (fireComp.OnFire) + { + var fireColor = GetFireColor(fireComp.Flammability); + if (fireComp.FireEffectEntity != null) + { + _appearance.SetData(fireComp.FireEffectEntity.Value, ReagentPuddleFireVisuals.FireState, fireComp.FireState); + _appearance.SetData(fireComp.FireEffectEntity.Value, ReagentPuddleFireVisuals.FireColor, fireColor); + } + + if (TryComp(ent, out var light)) + { + _light.SetRadius(ent, MathF.Max(2f, fireComp.FireState - 1f), light); + _light.SetColor(ent, fireColor, light); + } + } + } + + private void OnTileExposed(EntityUid gridUid, TransformComponent component, ref TileExposedEvent args) + { + var tilePos = args.Tile; + var puddles = GetPuddlesOnTile(gridUid, tilePos); + + foreach (var puddle in puddles) + { + if (!TryComp(puddle, out var fireComp) || fireComp.OnFire) + continue; + + // use cached volume factor so tiny puddles need a hotter tile to ignite + var effectiveFlammability = fireComp.Flammability * fireComp.VolumeFactor; + var ignitionTemp = 573.15f - (50f * effectiveFlammability); + if (args.Temperature >= ignitionTemp) + { + Ignite(puddle, fireComp); + _atmos.GetTileMixture(gridUid, null, tilePos, excite: true); + } + } + } + + private void OnPuddleTileFire(EntityUid uid, PuddleComponent component, ref TileFireEvent args) + { + if (TryComp(uid, out var fireComp) && !fireComp.OnFire) + { + var effectiveFlammability = fireComp.Flammability * fireComp.VolumeFactor; + var ignitionTemp = 573.15f - (50f * effectiveFlammability); + if (args.Temperature >= ignitionTemp) + { + Ignite(uid, fireComp); + } + } + } + + private IEnumerable GetPuddlesOnTile(EntityUid gridUid, Vector2i tilePos) + { + var results = new List(); + var entities = new HashSet(); + _lookup.GetLocalEntitiesIntersecting(gridUid, tilePos, entities, 0f); + foreach (var ent in entities) + { + if (HasComp(ent)) + results.Add(ent); + } + return results; + } + + private Color GetFireColor(int flammability) + { + return flammability switch + { + <= 1 => Color.FromHex("#FF5500"), + 2 => Color.FromHex("#FF9000"), + 3 => Color.FromHex("#FFD000"), + 4 => Color.FromHex("#FFFFE0"), + _ => Color.FromHex("#FFFFFF") + }; + } + + private void Ignite(EntityUid uid, ReagentPuddleFireComponent? fireComp = null) + { + if (!Resolve(uid, ref fireComp)) + return; + + if (fireComp.OnFire) + return; + + fireComp.OnFire = true; + + if (fireComp.PlayingStream == null) + { + var audio = _audio.PlayPvs(fireComp.LoopingSound, uid, AudioParams.Default.WithLoop(true).WithVolume(-5f)); + if (audio != null) + { + fireComp.PlayingStream = audio.Value.Entity; + } + } + + var fireColor = GetFireColor(fireComp.Flammability); + + var light = EnsureComp(uid); + _light.SetEnabled(uid, true, light); + _light.SetRadius(uid, MathF.Max(2f, fireComp.FireState - 1f), light); + _light.SetColor(uid, fireColor, light); + _light.SetEnergy(uid, 2f, light); + + if (fireComp.FireEffectEntity == null) + { + var xform = Transform(uid); + var fireEnt = Spawn("ReagentPuddleFireEffect", xform.Coordinates); + _transform.SetParent(fireEnt, uid); + fireComp.FireEffectEntity = fireEnt; + } + + if (fireComp.FireEffectEntity != null) + { + _appearance.SetData(fireComp.FireEffectEntity.Value, ReagentPuddleFireVisuals.FireState, fireComp.FireState); + _appearance.SetData(fireComp.FireEffectEntity.Value, ReagentPuddleFireVisuals.FireColor, fireColor); + } + } + + private void Extinguish(EntityUid uid) + { + if (!TryComp(uid, out var fireComp)) + return; + + fireComp.OnFire = false; + + if (fireComp.PlayingStream != null) + { + _audio.Stop(fireComp.PlayingStream); + fireComp.PlayingStream = null; + } + + RemComp(uid); + + if (fireComp.FireEffectEntity != null) + { + QueueDel(fireComp.FireEffectEntity.Value); + fireComp.FireEffectEntity = null; + } + + RemComp(uid); + } + + private float GetFireProtectionReduction(EntityUid uid) + { + if (!TryComp(uid, out var inv)) + return 0f; + + var survivalFactor = 1f; + foreach (var slot in inv.Slots) + { + if (!_inventory.TryGetSlotEntity(uid, slot.Name, out var slotEnt, inv)) + continue; + + if (TryComp(slotEnt, out var protection)) + survivalFactor *= (1f - Math.Clamp(protection.Reduction, 0f, 1f)); + } + + return 1f - survivalFactor; + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + _toExtinguish.Clear(); + _activeFires.Clear(); + + var activeQuery = EntityQueryEnumerator(); + while (activeQuery.MoveNext(out var uid, out var fireComp, out var puddle, out var xform)) + { + _activeFires.Add((uid, fireComp, puddle, xform)); + } + + foreach (var (uid, fireComp, puddle, xform) in _activeFires) + { + if (Deleted(uid)) + continue; + + if (!fireComp.OnFire) + { + if (fireComp.Flammability <= 0) + continue; + + var gridId = xform.GridUid; + if (gridId != null) + { + var ambientPos = _transform.GetGridTilePositionOrDefault((uid, xform)); + var ambientMix = _atmos.GetTileMixture(gridId.Value, null, ambientPos, excite: false); + if (ambientMix != null) + { + // factor volume into auto-ignition too + var ambientEffectiveFlammability = fireComp.Flammability * fireComp.VolumeFactor; + var autoIgnitionTemp = 773.15f - (50f * ambientEffectiveFlammability); + var ambientOxygen = ambientMix.GetMoles(Gas.Oxygen); + + if (ambientMix.Temperature >= autoIgnitionTemp && (fireComp.SelfOxidizing || ambientOxygen > 0.1f)) + { + Ignite(uid, fireComp); + _atmos.GetTileMixture(gridId.Value, null, ambientPos, excite: true); + } + } + } + continue; + } + + fireComp.Accumulator += frameTime; + if (fireComp.Accumulator < 1f) + continue; + + fireComp.Accumulator -= 1f; + + var gridUid = xform.GridUid; + if (gridUid == null) + { + _toExtinguish.Add(uid); + continue; + } + + var tilePos = _transform.GetGridTilePositionOrDefault((uid, xform)); + var tileMix = _atmos.GetTileMixture(gridUid.Value, null, tilePos, excite: true); + + var oxygenMoles = tileMix?.GetMoles(Gas.Oxygen) ?? 0f; + if (!fireComp.SelfOxidizing && oxygenMoles <= 0.1f) + { + _toExtinguish.Add(uid); + continue; + } + + if (!_solutionContainerSystem.ResolveSolution(uid, puddle.SolutionName, ref puddle.Solution, out var solution)) + { + _toExtinguish.Add(uid); + continue; + } + + var burnFraction = 0.05f / MathF.Pow(MathF.Max(1f, fireComp.Flammability), 3f); + + var currentVolume = solution.Volume.Float(); + if (currentVolume > 0f && currentVolume < _smallPuddleBurnThreshold) + { + var acceleratedFraction = _smallPuddleBurnPercent / MathF.Max(1f, fireComp.Flammability); + burnFraction = MathF.Max(burnFraction, acceleratedFraction); + } + + _solutionContainerSystem.BurnFlammableReagents(puddle.Solution.Value, burnFraction); + + var flammability = solution.GetSolutionFlammability(_prototypeManager); + var selfOxidizing = solution.IsSolutionSelfOxidizing(_prototypeManager); + + if (flammability <= 0) + { + _toExtinguish.Add(uid); + continue; + } + + fireComp.SelfOxidizing = selfOxidizing; + fireComp.VolumeFactor = GetVolumeFactor(solution.Volume); // refresh cache with live volume + + var effectiveFlammability = flammability * fireComp.VolumeFactor; + + if (effectiveFlammability > 10) + fireComp.FireState = 6; + else if (effectiveFlammability > 5) + fireComp.FireState = 5; + else + fireComp.FireState = 4; + + var fireColor = GetFireColor(fireComp.Flammability); + + if (fireComp.FireEffectEntity != null) + { + _appearance.SetData(fireComp.FireEffectEntity.Value, ReagentPuddleFireVisuals.FireState, fireComp.FireState); + _appearance.SetData(fireComp.FireEffectEntity.Value, ReagentPuddleFireVisuals.FireColor, fireColor); + } + + if (TryComp(uid, out var light)) + { + _light.SetRadius(uid, MathF.Max(2f, fireComp.FireState - 1f), light); + _light.SetColor(uid, fireColor, light); + } + + if (tileMix != null) + { + // use effectiveFlammability for heat output + var maxTemp = Atmospherics.T0C + 100f * MathF.Pow(effectiveFlammability, 1.5f); + if (tileMix.Temperature < maxTemp) + { + var heatRate = 10f * effectiveFlammability; + tileMix.Temperature = MathF.Min(tileMix.Temperature + heatRate, maxTemp); + } + + if (!fireComp.SelfOxidizing) + { + var burnAmount = MathF.Min(0.2f * effectiveFlammability, oxygenMoles); + tileMix.AdjustMoles(Gas.Oxygen, -burnAmount); + tileMix.AdjustMoles(Gas.CarbonDioxide, burnAmount * 0.6f); + tileMix.AdjustMoles(Gas.WaterVapor, burnAmount * 0.8f); + } + else + { + var burnAmount = 0.2f * effectiveFlammability; + tileMix.AdjustMoles(Gas.CarbonDioxide, burnAmount * 0.6f); + tileMix.AdjustMoles(Gas.WaterVapor, burnAmount * 0.8f); + } + } + + var tileDecals = _decalSystem.GetDecalsInRange(gridUid.Value, tilePos); + var tileBurntDecals = 0; + foreach (var set in tileDecals) + { + if (Array.IndexOf(_burntDecals, set.Decal.Id) == -1) + continue; + tileBurntDecals++; + if (tileBurntDecals > 4) + break; + } + + if (tileBurntDecals < 4 && _random.Prob(0.25f)) + { + _decalSystem.TryAddDecal(_burntDecals[_random.Next(_burntDecals.Length)], + new EntityCoordinates(gridUid.Value, tilePos), + out _, + cleanable: true); + } + + var directions = new[] { new Vector2i(0, 1), new Vector2i(0, -1), new Vector2i(1, 0), new Vector2i(-1, 0) }; + foreach (var offset in directions) + { + var adjacentPos = tilePos + offset; + var adjMix = _atmos.GetTileMixture(gridUid.Value, null, adjacentPos, excite: true); + + var isBlocked = false; + var adjacentEntities = new HashSet(); + _lookup.GetLocalEntitiesIntersecting(gridUid.Value, adjacentPos, adjacentEntities, 0f); + foreach (var ent in adjacentEntities) + { + if (TryComp(ent, out var airtight) && airtight.AirBlocked) + { + isBlocked = true; + break; + } + } + + if (!isBlocked && adjMix != null && tileMix is { Temperature: > Atmospherics.FireMinimumTemperatureToSpread }) + { + var radiatedTemp = tileMix.Temperature * Atmospherics.FireSpreadRadiosityScale; + if (adjMix.Temperature < radiatedTemp) + { + adjMix.Temperature = MathF.Max(adjMix.Temperature, radiatedTemp); + } + } + + foreach (var adjPuddle in GetPuddlesOnTile(gridUid.Value, adjacentPos)) + { + if (TryComp(adjPuddle, out var adjFireComp) && !adjFireComp.OnFire) + { + Ignite(adjPuddle, adjFireComp); + } + } + } + + var standingEntities = new HashSet(); + _lookup.GetLocalEntitiesIntersecting(gridUid.Value, tilePos, standingEntities, 0f); + + var structuralProto = _prototypeManager.Index(StructuralDamage); + var heatProto = _prototypeManager.Index(HeatDamage); + + // use effectiveFlammability for damage output + var structuralDamage = new DamageSpecifier(structuralProto, 2f * effectiveFlammability * _puddleDamageMultiplier); + var heatDamage = new DamageSpecifier(heatProto, 2f * effectiveFlammability * _puddleDamageMultiplier); + + var totalDamage = structuralDamage + heatDamage; + + var fireVolume = 50f * effectiveFlammability; + var fireEvent = new TileFireEvent(tileMix?.Temperature ?? (Atmospherics.T0C + 50f * effectiveFlammability), fireVolume); + + var xformQuery = GetEntityQuery(); + foreach (var ent in standingEntities) + { + if (ent == uid || Deleted(ent)) + continue; + + if (!xformQuery.TryGetComponent(ent, out var entXform)) + continue; + + if (_transform.GetGridTilePositionOrDefault((ent, entXform)) != tilePos) + continue; + + if (HasComp(ent)) + { + var ignoreResistances = !HasComp(ent); + + var appliedDamage = totalDamage; + if (!ignoreResistances) + { + var reduction = Math.Clamp(GetFireProtectionReduction(ent) * _fireProtectionEffectiveness, 0f, 1f); + appliedDamage = totalDamage * (1f - reduction); + } + + _damageable.TryChangeDamage(ent, appliedDamage, ignoreResistances: ignoreResistances); + } + + if (Deleted(ent)) + continue; + + RaiseLocalEvent(ent, ref fireEvent); + } + } + + foreach (var uid in _toExtinguish) + { + Extinguish(uid); + } + } + } +} diff --git a/Content.Server/_Funkystation/Stains/FlammableStainsSystem.cs b/Content.Server/_Funkystation/Stains/FlammableStainsSystem.cs new file mode 100644 index 000000000000..2587507601f7 --- /dev/null +++ b/Content.Server/_Funkystation/Stains/FlammableStainsSystem.cs @@ -0,0 +1,189 @@ +using Content.Server._Funkystation.Atmos.Events; +using Content.Server.Atmos.Components; +using Content.Server.Atmos.EntitySystems; +using Content.Shared._Funkystation.Stains.Components; +using Content.Shared.Atmos; +using Content.Shared.Atmos.Components; +using Content.Shared.Chemistry.EntitySystems; +using Content.Shared.Chemistry.Reagent; +using Content.Shared.Database; +using Content.Shared.Inventory; +using Robust.Shared.Configuration; +using Robust.Shared.Prototypes; +using Content.Server.Administration.Logs; +using Content.Shared._Funkystation.CCVar; + +namespace Content.Server._Funkystation.Stains +{ + public sealed class FlammableStainsSystem : EntitySystem + { + [Dependency] private readonly FlammableSystem _flammable = null!; + [Dependency] private readonly InventorySystem _inventory = null!; + [Dependency] private readonly SharedSolutionContainerSystem _solution = null!; + [Dependency] private readonly IPrototypeManager _prototypeManager = null!; + [Dependency] private readonly EntityLookupSystem _lookup = null!; + [Dependency] private readonly IConfigurationManager _cfg = null!; + [Dependency] private readonly IAdminLogManager _adminLogger = default!; + + // Fraction of a stain's flammable reagents consumed per second while on fire + private const float StainBurnRatePerSecond = 0.2f; + private float _stainStackMultiplier = 1.0f; + + public override void Initialize() + { + base.Initialize(); + Subs.CVar(_cfg, ReagentFireCVars.StainFireStackMultiplier, value => _stainStackMultiplier = value, true); + SubscribeLocalEvent(OnTileFire, before: [typeof(FlammableSystem)]); + SubscribeLocalEvent(OnTileExposed); + } + + private void OnTileFire(EntityUid uid, InventoryComponent component, ref TileFireEvent args) + { + var totalStainFlammability = GetTotalStainFlammability(uid, component); + if (totalStainFlammability <= 0) + return; + + // Don't keep adding fire stacks every tick if they're already burning... + if (TryComp(uid, out var flammable) && !flammable.OnFire) + { + // Non-linear scaling. lower flammability values are mild, high values ramp up BADLY + var extraStacks = (args.Volume / 100f) * (0.5f * MathF.Pow(totalStainFlammability, 1.5f)) * _stainStackMultiplier; + _flammable.AdjustFireStacks(uid, extraStacks, flammable); + } + } + + private void OnTileExposed(EntityUid gridUid, GridAtmosphereComponent component, ref TileExposedEvent args) + { + var tilePos = args.Tile; + var entities = new HashSet(); + _lookup.GetLocalEntitiesIntersecting(gridUid, tilePos, entities, 0f); + + foreach (var ent in entities) + { + if (!TryComp(ent, out var inv) || !TryComp(ent, out var flammable)) + continue; + + if (flammable.OnFire) + continue; + + var totalStainFlammability = GetTotalStainFlammability(ent, inv); + if (totalStainFlammability <= 0) + continue; + + // Non-linear scaling + var ignitionTemp = 573.15f - (50f * MathF.Pow(totalStainFlammability, 1.5f)); + if (args.Temperature >= ignitionTemp) + { + var fireStacks = (1f + (0.5f * MathF.Pow(totalStainFlammability, 1.5f))) * _stainStackMultiplier; + _flammable.AdjustFireStacks(ent, fireStacks, flammable); + + var igniter = args.SparkSource ?? gridUid; + _flammable.Ignite(ent, igniter, flammable); + + var reagents = GetFlammableStainsString(ent, inv); + _adminLogger.Add(LogType.Flammable, LogImpact.High, + $"{ToPrettyString(ent):entity} was ignited by their flammable stains ({reagents}) reacting to a hotspot (Igniter: {ToPrettyString(igniter):entity})."); + } + } + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + // Actively burn off stains while the wearer is on fire, same as puddles. + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var flammable, out var inv)) + { + if (!flammable.OnFire) + continue; + + BurnStains(uid, inv, frameTime); + } + } + + private void BurnStains(EntityUid uid, InventoryComponent inv, float frameTime) + { + foreach (var slot in inv.Slots) + { + if (!_inventory.TryGetSlotEntity(uid, slot.Name, out var slotEnt, inv)) + continue; + + if (IsSlotStainBlocked(uid, slot, inv)) + continue; + + if (!TryComp(slotEnt, out var stain) || + !_solution.TryGetSolution(slotEnt.Value, stain.SolutionName, out var soln, out var solution)) + continue; + + if (solution.GetSolutionFlammability(_prototypeManager) <= 0) + continue; + + _solution.BurnFlammableReagents(soln.Value, StainBurnRatePerSecond * frameTime); + } + } + + private int GetTotalStainFlammability(EntityUid uid, InventoryComponent inv) + { + var total = 0; + foreach (var slot in inv.Slots) + { + if (!_inventory.TryGetSlotEntity(uid, slot.Name, out var slotEnt, inv)) + continue; + + if (IsSlotStainBlocked(uid, slot, inv)) + continue; + + if (TryComp(slotEnt, out var stain) && + _solution.TryGetSolution(slotEnt.Value, stain.SolutionName, out _, out var solution)) + { + total += solution.GetSolutionFlammability(_prototypeManager); + } + } + return total; + } + + private bool IsSlotStainBlocked(EntityUid wearer, SlotDefinition slotDef, InventoryComponent inv) + { + foreach (var slot in inv.Slots) + { + if (!_inventory.TryGetSlotEntity(wearer, slot.Name, out var slotEnt, inv)) + continue; + + if (TryComp(slotEnt, out var blocker)) + { + if ((blocker.BlockedSlots & slotDef.SlotFlags) != 0) + return true; + } + } + return false; + } + + private string GetFlammableStainsString(EntityUid uid, InventoryComponent inv) + { + var names = new List(); + foreach (var slot in inv.Slots) + { + if (!_inventory.TryGetSlotEntity(uid, slot.Name, out var slotEnt, inv)) + continue; + + if (IsSlotStainBlocked(uid, slot, inv)) + continue; + + if (TryComp(slotEnt, out var stain) && + _solution.TryGetSolution(slotEnt.Value, stain.SolutionName, out _, out var solution)) + { + foreach (var (reagentId, _) in solution.Contents) + { + if (_prototypeManager.TryIndex(reagentId.Prototype, out var proto) && proto.Flammability > 0) + { + names.Add(proto.LocalizedName); + } + } + } + } + + return names.Count > 0 ? string.Join(", ", new HashSet(names)) : "unknown chemicals"; + } + } +} diff --git a/Content.Server/_Funkystation/Stains/StainSystem.cs b/Content.Server/_Funkystation/Stains/StainSystem.cs new file mode 100644 index 000000000000..ad9468b74ae1 --- /dev/null +++ b/Content.Server/_Funkystation/Stains/StainSystem.cs @@ -0,0 +1,21 @@ +using Content.Shared._Funkystation.Stains.Components; +using Content.Shared._Funkystation.Stains.Systems; +using Content.Shared.Chemistry.Components; +using Content.Shared.Tag; +using Robust.Shared.Prototypes; + +namespace Content.Server._Funkystation.Stains; + +public sealed partial class StainSystem : SharedStainSystem +{ + [Dependency] private TagSystem _tag = null!; + + private static readonly ProtoId Tag = "DNASolutionScannable"; + + protected override void OnStained(Entity ent, Entity solution) + { + base.OnStained(ent, solution); + + _tag.AddTag(ent.Owner, Tag); + } +} diff --git a/Content.Server/_Funkystation/StationEvents/Components/UtilityLineRuptureRuleComponent.cs b/Content.Server/_Funkystation/StationEvents/Components/UtilityLineRuptureRuleComponent.cs new file mode 100644 index 000000000000..9f343b186264 --- /dev/null +++ b/Content.Server/_Funkystation/StationEvents/Components/UtilityLineRuptureRuleComponent.cs @@ -0,0 +1,41 @@ +using Robust.Shared.Map; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; +using Content.Shared.FixedPoint; + +namespace Content.Server._Funkystation.StationEvents.Components; + +/// +/// This event announces a utility line rupture and then spills and ignites a large quantity of flammable reagent after a delay +/// +[RegisterComponent, Access(typeof(Events.UtilityLineRuptureRule))] +public sealed partial class UtilityLineRuptureRuleComponent : Component +{ + /// + /// When the actual spill and fire should occur + /// + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] + public TimeSpan? RuptureTime; + + /// + /// The location where the fire will happen + /// + public EntityCoordinates? TargetCoordinates; + + /// + /// Volume of reagent to spill + /// + [DataField] + public FixedPoint2 SpillVolume = FixedPoint2.New(1000); + + /// + /// List of possible flammable reagents to be spilled + /// + [DataField] + public List PossibleReagents = ["WeldingFuel", "Acetone", "Phlogiston"]; + + /// + /// The chosen flammable reagent that will be spilled and ignited. Selected at event start. + /// + [DataField] + public string Reagent = "WeldingFuel"; +} diff --git a/Content.Server/_Funkystation/StationEvents/Events/UtilityLineRuptureRule.cs b/Content.Server/_Funkystation/StationEvents/Events/UtilityLineRuptureRule.cs new file mode 100644 index 000000000000..6ffb8e3b3a58 --- /dev/null +++ b/Content.Server/_Funkystation/StationEvents/Events/UtilityLineRuptureRule.cs @@ -0,0 +1,73 @@ +using Content.Server.Chat.Systems; +using Content.Server.Fluids.EntitySystems; +using Content.Server.Pinpointer; +using Content.Server.StationEvents.Events; +using Content.Server._Funkystation.StationEvents.Components; +using Content.Shared.Atmos; +using Content.Shared.Chemistry.Components; +using Content.Shared.GameTicking.Components; +using Robust.Shared.Timing; +using Robust.Shared.Utility; +using Robust.Shared.Random; + +namespace Content.Server._Funkystation.StationEvents.Events; + +public sealed partial class UtilityLineRuptureRule : StationEventSystem +{ + [Dependency] private NavMapSystem _navMap = null!; + [Dependency] private SharedTransformSystem _transform = null!; + [Dependency] private ChatSystem _chat = null!; + [Dependency] private IGameTiming _timing = null!; + [Dependency] private PuddleSystem _puddle = null!; + [Dependency] private IRobustRandom _random = null!; + + protected override void Started(EntityUid uid, UtilityLineRuptureRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args) + { + base.Started(uid, component, gameRule, args); + + // Pick a reagent from the list + if (component.PossibleReagents.Count > 0) + { + component.Reagent = _random.Pick(component.PossibleReagents); + } + + if (!TryFindRandomTile(out _, out var targetStation, out _, out var targetCoords)) + return; + + component.TargetCoordinates = targetCoords; + component.RuptureTime = _timing.CurTime + TimeSpan.FromSeconds(10); + + var mapCoords = _transform.ToMapCoordinates(targetCoords); + var locationName = FormattedMessage.RemoveMarkupPermissive(_navMap.GetNearestBeaconString(mapCoords)); + + { + // Announce 10 seconds before it happens (if you weren't paying attention you get round removed bye) + var msg = Loc.GetString("utility-line-rupture-announcement", ("location", locationName)); + _chat.DispatchStationAnnouncement(targetStation.Value, msg, Loc.GetString("central-command-sender"), playDefaultSound: true, colorOverride: Color.FromHex("#f9a524")); // Starlight, SISTER -> CentComm + } + } + + protected override void ActiveTick(EntityUid uid, UtilityLineRuptureRuleComponent component, GameRuleComponent gameRule, float frameTime) + { + base.ActiveTick(uid, component, gameRule, frameTime); + + if (component.RuptureTime == null || _timing.CurTime < component.RuptureTime) + return; + + component.RuptureTime = null; + + if (component.TargetCoordinates is not { } coords) + return; + + // Set up the large spill + var solution = new Solution(); + solution.AddReagent(component.Reagent, component.SpillVolume); + + if (_puddle.TrySpillAt(coords, solution, out var puddleUid)) + { + // Ignite yum yum yum + var fireEv = new TileFireEvent(1000f, 100f); + RaiseLocalEvent(puddleUid, ref fireEv); + } + } +} diff --git a/Content.Server/_Funkystation/WallStains/Components/ActiveFlammableWallStainComponent.cs b/Content.Server/_Funkystation/WallStains/Components/ActiveFlammableWallStainComponent.cs new file mode 100644 index 000000000000..8ab9dbceed62 --- /dev/null +++ b/Content.Server/_Funkystation/WallStains/Components/ActiveFlammableWallStainComponent.cs @@ -0,0 +1,10 @@ +namespace Content.Server._Funkystation.WallStains.Components; + +/// +/// Marker added to a flammable wall stain entity while it is actively on fire +/// + +[RegisterComponent] +public sealed partial class ActiveFlammableWallStainComponent : Component +{ +} diff --git a/Content.Server/_Funkystation/WallStains/Systems/FlammableWallStainSystem.cs b/Content.Server/_Funkystation/WallStains/Systems/FlammableWallStainSystem.cs new file mode 100644 index 000000000000..588f474895ba --- /dev/null +++ b/Content.Server/_Funkystation/WallStains/Systems/FlammableWallStainSystem.cs @@ -0,0 +1,360 @@ +using Content.Server._Funkystation.Atmos.Events; +using Content.Server._Funkystation.WallStains.Components; +using Content.Server.Atmos.EntitySystems; +using Content.Shared._Funkystation.ReagentFires; +using Content.Shared._Funkystation.WallStains.Components; +using Content.Shared.Atmos; +using Content.Shared.Chemistry.EntitySystems; +using Content.Shared.Damage; +using Content.Shared.Damage.Components; +using Content.Shared.Damage.Systems; +using Content.Shared.Fluids.Components; +using Robust.Server.GameObjects; +using Robust.Shared.Audio; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Map.Components; +using Robust.Shared.Prototypes; + +namespace Content.Server._Funkystation.WallStains.Systems; + +public sealed partial class FlammableWallStainSystem : EntitySystem +{ + [Dependency] private AtmosphereSystem _atmos = null!; + [Dependency] private SharedTransformSystem _transform = null!; + [Dependency] private SharedSolutionContainerSystem _solution = null!; + [Dependency] private IPrototypeManager _proto = null!; + [Dependency] private DamageableSystem _damageable = null!; + [Dependency] private SharedAudioSystem _audio = null!; + [Dependency] private SharedPointLightSystem _light = null!; + [Dependency] private EntityLookupSystem _lookup = null!; + [Dependency] private SharedAppearanceSystem _appearance = null!; + [Dependency] private SharedMapSystem _map = null!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnTileExposed); + SubscribeLocalEvent(OnTileFire); + SubscribeLocalEvent(OnShutdown); + } + + private void OnShutdown(EntityUid uid, FlammableWallStainComponent component, ref ComponentShutdown args) + { + Extinguish(uid, component); + } + + private void OnTileExposed(EntityUid gridUid, MapGridComponent component, ref TileExposedEvent args) + { + var fireTile = args.Tile; + + if (!TryComp(gridUid, out var grid)) + return; + + var toIgnite = new List<(EntityUid Stain, FlammableWallStainComponent Comp)>(); + + var offsets = new[] { Vector2i.Zero, new Vector2i(0, 1), new Vector2i(0, -1), new Vector2i(1, 0), new Vector2i(-1, 0) }; + foreach (var offset in offsets) + { + var wallTile = fireTile + offset; + var enumerator = _map.GetAnchoredEntitiesEnumerator(gridUid, grid, wallTile); + + while (enumerator.MoveNext(out var ent)) + { + var children = Transform(ent.Value).ChildEnumerator; + while (children.MoveNext(out var child)) + { + if (TryComp(child, out var fireComp) && !fireComp.OnFire && + TryComp(child, out var stain)) + { + if (wallTile + stain.Direction == fireTile || offset == Vector2i.Zero) + { + if (_solution.TryGetSolution(child, stain.SolutionName, out var solComp)) + fireComp.Flammability = solComp.Value.Comp.Solution.GetSolutionFlammability(_proto); + else + fireComp.Flammability = 0; + + if (fireComp.Flammability <= 0) + continue; + + var ignitionTemp = 573.15f - (50f * fireComp.Flammability); + if (args.Temperature >= ignitionTemp) + toIgnite.Add((child, fireComp)); + } + } + } + } + } + + foreach (var (stainUid, fireComp) in toIgnite) + { + Ignite(stainUid, fireComp); + } + } + + private void OnTileFire(EntityUid uid, FlammableWallStainComponent component, ref TileFireEvent args) + { + if (component.OnFire) + return; + + if (TryComp(uid, out var stain) && + _solution.TryGetSolution(uid, stain.SolutionName, out var solComp)) + { + component.Flammability = solComp.Value.Comp.Solution.GetSolutionFlammability(_proto); + } + else + { + component.Flammability = 0; + } + + if (component.Flammability <= 0f) + return; + + var ignitionTemp = 573.15f - (50f * component.Flammability); + if (args.Temperature >= ignitionTemp) + Ignite(uid, component); + } + + private Color GetFireColor(int flammability) + { + return flammability switch + { + <= 1 => Color.FromHex("#FF5500"), + 2 => Color.FromHex("#FF9000"), + 3 => Color.FromHex("#FFD000"), + 4 => Color.FromHex("#FFFFE0"), + _ => Color.FromHex("#FFFFFF") + }; + } + + private void Ignite(EntityUid uid, FlammableWallStainComponent fireComp) + { + if (fireComp.OnFire || fireComp.Flammability <= 0) // Extra safety check! + return; + + fireComp.OnFire = true; + fireComp.FireState = fireComp.Flammability > 10 ? 6 : fireComp.Flammability > 5 ? 5 : 4; + var fireColor = GetFireColor(fireComp.Flammability); + + EnsureComp(uid); + + var light = EnsureComp(uid); + _light.SetEnabled(uid, true, light); + _light.SetRadius(uid, MathF.Max(1.5f, fireComp.FireState - 2f), light); + _light.SetColor(uid, fireColor, light); + _light.SetEnergy(uid, 1.5f, light); + + var wantedSoundPath = fireComp.Flammability >= 4 + ? "/Audio/_Funkystation/Effects/Fire/hissing.ogg" + : "/Audio/_Funkystation/Effects/Fire/bigfire.ogg"; + + fireComp.PlayingStream = _audio.PlayPvs(new SoundPathSpecifier(wantedSoundPath), uid, AudioParams.Default.WithLoop(true).WithVolume(-8f))?.Entity; + fireComp.CurrentPlayingSound = wantedSoundPath; + + if (fireComp.FireEffectEntity == null) + { + var parentWall = Transform(uid).ParentUid; + if (parentWall.IsValid()) + { + var fireEnt = Spawn("WallStainFireEffect", Transform(parentWall).Coordinates); + _transform.SetParent(fireEnt, parentWall); + _transform.SetLocalPosition(fireEnt, System.Numerics.Vector2.Zero); + fireComp.FireEffectEntity = fireEnt; + } + } + + if (fireComp.FireEffectEntity is { } fireEntEffect) + { + _appearance.SetData(fireEntEffect, ReagentPuddleFireVisuals.FireState, fireComp.FireState); + _appearance.SetData(fireEntEffect, ReagentPuddleFireVisuals.FireColor, fireColor); + } + } + + private void Extinguish(EntityUid uid, FlammableWallStainComponent fireComp) + { + if (!fireComp.OnFire) + return; + + fireComp.OnFire = false; + + RemCompDeferred(uid); + + RemComp(uid); + + if (fireComp.PlayingStream != null) + { + _audio.Stop(fireComp.PlayingStream); + fireComp.PlayingStream = null; + } + + fireComp.CurrentPlayingSound = null; + + if (fireComp.FireEffectEntity != null) + { + QueueDel(fireComp.FireEffectEntity.Value); + fireComp.FireEffectEntity = null; + } + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var activeStains = new List<(EntityUid Uid, FlammableWallStainComponent FireComp, WallStainComponent Stain, TransformComponent Xform)>(); + + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out _, out var fireComp, out var stain, out var xform)) + { + activeStains.Add((uid, fireComp, stain, xform)); + } + + foreach (var (uid, currentFireComp, currentStain, currentXform) in activeStains) + { + if (Deleted(uid)) + continue; + + if (!_solution.TryGetSolution(uid, currentStain.SolutionName, out var solComp)) + continue; + + var flammability = solComp.Value.Comp.Solution.GetSolutionFlammability(_proto); + var selfOxidizing = solComp.Value.Comp.Solution.IsSolutionSelfOxidizing(_proto); + currentFireComp.Flammability = flammability; + + if (flammability <= 0) + { + Extinguish(uid, currentFireComp); + continue; + } + + var gridId = currentXform.GridUid; + if (gridId == null) + continue; + + var wallPos = _transform.GetGridTilePositionOrDefault((uid, currentXform)); + var atmosTilePos = wallPos + currentStain.Direction; + + currentFireComp.Accumulator += frameTime; + if (currentFireComp.Accumulator < 1f) + continue; + currentFireComp.Accumulator -= 1f; + + var tileMix = _atmos.GetTileMixture(gridId.Value, null, atmosTilePos, excite: true); + var currentOxygen = tileMix?.GetMoles(Gas.Oxygen) ?? 0f; + + if (!selfOxidizing && currentOxygen <= 0.1f) + { + Extinguish(uid, currentFireComp); + continue; + } + + var burnFraction = 0.05f / MathF.Pow(MathF.Max(1f, flammability), 3f); + _solution.BurnFlammableReagents(solComp.Value, burnFraction); + + if (tileMix != null) + { + var maxTemp = Atmospherics.T0C + 100f * MathF.Pow(flammability, 1.5f); + if (tileMix.Temperature < maxTemp) + tileMix.Temperature = MathF.Min(tileMix.Temperature + 10f * flammability, maxTemp); + + var burnAmount = selfOxidizing ? 0.2f * flammability : MathF.Min(0.2f * flammability, currentOxygen); + if (!selfOxidizing) + tileMix.AdjustMoles(Gas.Oxygen, -burnAmount); + tileMix.AdjustMoles(Gas.CarbonDioxide, burnAmount * 0.6f); + tileMix.AdjustMoles(Gas.WaterVapor, burnAmount * 0.8f); + } + + currentFireComp.FireState = flammability > 10 ? 6 : flammability > 5 ? 5 : 4; + var fireColor = GetFireColor(currentFireComp.Flammability); + + if (currentFireComp.FireEffectEntity is { } fireEnt) + { + _appearance.SetData(fireEnt, ReagentPuddleFireVisuals.FireState, currentFireComp.FireState); + _appearance.SetData(fireEnt, ReagentPuddleFireVisuals.FireColor, fireColor); + } + + var wantedSoundPath = flammability >= 4 + ? "/Audio/_Funkystation/Effects/Fire/hissing.ogg" + : "/Audio/_Funkystation/Effects/Fire/bigfire.ogg"; + + if (currentFireComp.CurrentPlayingSound != wantedSoundPath) + { + if (currentFireComp.PlayingStream != null) + _audio.Stop(currentFireComp.PlayingStream); + + currentFireComp.PlayingStream = _audio.PlayPvs(new SoundPathSpecifier(wantedSoundPath), uid, AudioParams.Default.WithLoop(true).WithVolume(-8f))?.Entity; + currentFireComp.CurrentPlayingSound = wantedSoundPath; + } + + if (TryComp(uid, out var light)) + { + _light.SetRadius(uid, MathF.Max(1.5f, currentFireComp.FireState - 2f), light); + _light.SetColor(uid, fireColor, light); + } + + if (flammability >= 4) + { + var parent = currentXform.ParentUid; + if (parent.IsValid() && HasComp(parent)) + { + var damage = new DamageSpecifier(); + damage.DamageDict.Add("Structural", 2.5f * flammability); + damage.DamageDict.Add("Heat", 1.5f * flammability); + _damageable.TryChangeDamage(parent, damage, ignoreResistances: true); + } + } + + var entities = new HashSet(); + _lookup.GetLocalEntitiesIntersecting(gridId.Value, atmosTilePos, entities, 0f); + foreach (var ent in entities) + { + if (HasComp(ent)) + { + var fireEvent = new TileFireEvent(tileMix?.Temperature ?? 600f, 50f * flammability); + RaiseLocalEvent(ent, ref fireEvent); + } + } + + var spreadOffsets = new[] { Vector2i.Zero, new Vector2i(0, 1), new Vector2i(0, -1), new Vector2i(1, 0), new Vector2i(-1, 0) }; + if (TryComp(gridId.Value, out var grid)) + { + var adjacentStainsToIgnite = new List<(EntityUid, FlammableWallStainComponent)>(); + + foreach (var offset in spreadOffsets) + { + var checkWallTile = wallPos + offset; + var enumerator = _map.GetAnchoredEntitiesEnumerator(gridId.Value, grid, checkWallTile); + while (enumerator.MoveNext(out var ent)) + { + var children = Transform(ent.Value).ChildEnumerator; + while (children.MoveNext(out var child)) + { + if (child == uid) + continue; + + if (TryComp(child, out var adjacentFire) && !adjacentFire.OnFire) + { + if (TryComp(child, out var adjacentStain) && + _solution.TryGetSolution(child, adjacentStain.SolutionName, out var adjSol)) + { + adjacentFire.Flammability = adjSol.Value.Comp.Solution.GetSolutionFlammability(_proto); + } + else + { + adjacentFire.Flammability = 0; + } + + if (adjacentFire.Flammability > 0) + adjacentStainsToIgnite.Add((child, adjacentFire)); + } + } + } + } + + foreach (var (stainUid, fireCompAdjacent) in adjacentStainsToIgnite) + { + Ignite(stainUid, fireCompAdjacent); + } + } + } + } +} diff --git a/Content.Server/_Funkystation/WallStains/Systems/WallStainSystem.cs b/Content.Server/_Funkystation/WallStains/Systems/WallStainSystem.cs new file mode 100644 index 000000000000..73cd0396af5d --- /dev/null +++ b/Content.Server/_Funkystation/WallStains/Systems/WallStainSystem.cs @@ -0,0 +1,430 @@ +using Content.Server.Atmos.Components; +using Content.Server.Forensics; +using Content.Shared._Funkystation.WallStains; +using Content.Shared._Funkystation.WallStains.Components; +using Content.Shared.Chemistry; +using Content.Shared.Chemistry.Components; +using Content.Shared.Chemistry.EntitySystems; +using Content.Shared.Chemistry.Reaction; +using Content.Shared.Chemistry.Reagent; +using Content.Shared.DoAfter; +using Content.Shared.FixedPoint; +using Content.Shared.Fluids; +using Content.Shared.Fluids.Components; +using Content.Shared.Interaction; +using Content.Shared.Popups; +using Content.Shared.Tag; +using Robust.Shared.Audio; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Map; +using Robust.Shared.Map.Components; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; + +namespace Content.Server._Funkystation.WallStains.Systems; + +public sealed partial class WallStainSystem : EntitySystem +{ + private static readonly ProtoId WallTag = "Wall"; + private static readonly ProtoId WindowTag = "Window"; + private static readonly ProtoId SoapTag = "Soap"; + + private static readonly ProtoId WaterReagent = "Water"; + private static readonly ProtoId SpaceCleanerReagent = "SpaceCleaner"; + + [Dependency] private SharedMapSystem _map = null!; + [Dependency] private SharedTransformSystem _transform = null!; + [Dependency] private SharedSolutionContainerSystem _solution = null!; + [Dependency] private ForensicsSystem _forensics = null!; + [Dependency] private SharedDoAfterSystem _doAfter = null!; + [Dependency] private SharedPopupSystem _popup = null!; + [Dependency] private TagSystem _tag = null!; + [Dependency] private IRobustRandom _random = null!; + [Dependency] private IPrototypeManager _prototype = null!; + [Dependency] private SharedPuddleSystem _puddle = null!; + [Dependency] private SharedAudioSystem _audio = null!; + + private Shared.Chemistry.Reaction.ReactiveReagentEffectEntry _stainCleanEffectEntry = null!; + + private float _evaporationAccumulator; + + public override void Initialize() + { + base.Initialize(); + + _stainCleanEffectEntry = new Shared.Chemistry.Reaction.ReactiveReagentEffectEntry() + { + Methods = [ReactionMethod.Touch], + Reagents = ["SpaceCleaner", "Bleach"], + Effects = [new CleanWallStainReaction()] + }; + + SubscribeLocalEvent(OnInteractUsing); + SubscribeLocalEvent(OnCleanDoAfter); + SubscribeLocalEvent(OnCleanEvent); + SubscribeLocalEvent(OnSpillableAfterInteract); + SubscribeLocalEvent(OnPourDoAfter); + SubscribeLocalEvent(OnSplashOnWall); + } + + private void OnSplashOnWall(ref SplashOnWallEvent args) + { + TrySplashOnWalls(args.Coordinates, args.Solution); + } + + private void TrySplashOnWalls(EntityCoordinates coords, Solution solution) + { + if (solution.Volume <= 0) + return; + + var gridUid = _transform.GetGrid(coords); + if (!TryComp(gridUid, out var grid)) + return; + + var tilePos = _map.TileIndicesFor(gridUid.Value, grid, coords); + var checkOffsets = new[] + { + new Vector2i(0, 0), + new Vector2i(0, 1), + new Vector2i(0, -1), + new Vector2i(1, 0), + new Vector2i(-1, 0) + }; + + foreach (var offset in checkOffsets) + { + var targetTile = tilePos + offset; + var anchored = _map.GetAnchoredEntitiesEnumerator(gridUid.Value, grid, targetTile); + while (anchored.MoveNext(out var ent)) + { + if (!IsWall(ent.Value)) + continue; + + ApplyStainToWall(ent.Value, solution, -offset, fraction: 0.25f); + } + } + } + + private bool IsWall(EntityUid uid) + { + return HasComp(uid) || _tag.HasTag(uid, WallTag) || _tag.HasTag(uid, WindowTag); + } + + private FixedPoint2 ApplyStainToWall(EntityUid wallUid, Solution solution, Vector2i direction, float fraction = 1.0f) + { + EnsureComp(wallUid); + + var reactive = EnsureComp(wallUid); + reactive.Reactions ??= new(); + if (!reactive.Reactions.Contains(_stainCleanEffectEntry)) + reactive.Reactions.Add(_stainCleanEffectEntry); + + var stainUid = EntityUid.Invalid; + WallStainComponent? stainComp = null; + + var children = Transform(wallUid).ChildEnumerator; + while (children.MoveNext(out var child)) + { + if (!TryComp(child, out var existingStain) || existingStain.Direction != direction) + continue; + stainUid = child; + stainComp = existingStain; + break; + } + + if (stainUid == EntityUid.Invalid) + { + stainUid = Spawn("WallStain", Transform(wallUid).Coordinates); + _transform.SetParent(stainUid, wallUid); + + var baseOffset = new System.Numerics.Vector2(direction.X * 0.48f, direction.Y * 0.48f); + if (direction.X != 0) + baseOffset.Y += _random.NextFloat(-0.35f, 0.35f); + if (direction.Y != 0) + baseOffset.X += _random.NextFloat(-0.35f, 0.35f); + if (direction == Vector2i.Zero) + baseOffset = new System.Numerics.Vector2(_random.NextFloat(-0.4f, 0.4f), _random.NextFloat(-0.4f, 0.4f)); + + _transform.SetLocalPosition(stainUid, baseOffset); + _transform.SetLocalRotation(stainUid, direction != Vector2i.Zero ? Angle.Zero : _random.NextAngle()); + + stainComp = Comp(stainUid); + stainComp.Direction = direction; + Dirty(stainUid, stainComp); + } + + var actualTransfer = FixedPoint2.Zero; + + if (stainComp != null && _solution.TryGetSolution(stainUid, stainComp.SolutionName, out var stainSolution)) + { + actualTransfer = FixedPoint2.Min(solution.Volume * fraction, stainComp.MaxStainVolume - stainSolution.Value.Comp.Solution.Volume); + + if (actualTransfer > 0) + { + var split = solution.Clone().SplitSolution(actualTransfer); + _solution.TryAddSolution(stainSolution.Value, split); + + var wallForensics = EnsureComp(wallUid); + var dnas = _forensics.GetSolutionsDNA(split); + wallForensics.DNAs.UnionWith(dnas); + } + } + + UpdateVisuals(stainUid, stainComp); + return actualTransfer; + } + + private void OnSpillableAfterInteract(EntityUid uid, SpillableComponent component, AfterInteractEvent args) + { + if (args.Handled || args.Target == null || !args.CanReach) + return; + + if (!IsWall(args.Target.Value)) + return; + + if (!_solution.TryGetSolution(uid, component.SolutionName, out var solComp) || solComp.Value.Comp.Solution.Volume <= 0) + return; + + var solution = solComp.Value.Comp.Solution; + if (solution.GetTotalPrototypeQuantity(WaterReagent) == solution.Volume) + { + _popup.PopupEntity(Loc.GetString("wall-stain-pour-water-blocked"), args.Target.Value, args.User); + return; + } + + args.Handled = true; + _popup.PopupEntity(Loc.GetString("wall-stain-pour-start", ("container", uid)), args.Target.Value, args.User); + + var doAfterArgs = new DoAfterArgs(EntityManager, args.User, TimeSpan.FromSeconds(5), new PourOnWallDoAfterEvent(), uid, target: args.Target.Value, used: uid) + { + BreakOnMove = true, + BreakOnDamage = true, + NeedHand = true + }; + + _doAfter.TryStartDoAfter(doAfterArgs); + } + + private void OnPourDoAfter(EntityUid uid, SpillableComponent component, PourOnWallDoAfterEvent args) + { + if (args.Cancelled || args.Handled || args.Target == null) + return; + + args.Handled = true; + + if (!_solution.TryGetSolution(uid, component.SolutionName, out var solComp) || solComp.Value.Comp.Solution.Volume <= 0) + return; + + var wallUid = args.Target.Value; + + var wallPos = _transform.GetGridTilePositionOrDefault(wallUid); + var userPos = _transform.GetGridTilePositionOrDefault(args.User); + var direction = userPos - wallPos; + direction.X = Math.Clamp(direction.X, -1, 1); + direction.Y = Math.Clamp(direction.Y, -1, 1); + + var maxPour = FixedPoint2.Min(FixedPoint2.New(15), solComp.Value.Comp.Solution.Volume); + var pourSolution = solComp.Value.Comp.Solution.Clone().SplitSolution(maxPour); + + var transferred = ApplyStainToWall(wallUid, pourSolution, direction, fraction: 1.0f); + + if (transferred > 0) + { + _solution.SplitSolution(solComp.Value, transferred); + + _audio.PlayPvs(new SoundPathSpecifier("/Audio/Effects/Fluids/splat.ogg"), wallUid); + _popup.PopupEntity(Loc.GetString("wall-stain-pour-success", ("container", uid)), wallUid, args.User); + } + else + { + _popup.PopupEntity(Loc.GetString("wall-stain-pour-full"), wallUid, args.User); + } + } + + private void OnInteractUsing(EntityUid uid, StainedWallComponent component, InteractUsingEvent args) + { + if (args.Handled) + return; + + var tool = args.Used; + var user = args.User; + + if (!IsCleaningTool(tool)) + return; + if (TryComp(tool, out var absorbent)) + { + if (_solution.TryGetSolution(tool, absorbent.SolutionName, out _, out var solution)) + { + var absorbentReagents = _puddle.GetAbsorbentReagents(solution); + if (solution.GetTotalPrototypeQuantity(absorbentReagents) <= 0) + { + _popup.PopupEntity(Loc.GetString("wall-stain-cleaning-dry-rag"), tool, user); + args.Handled = true; + return; + } + } + } + + _popup.PopupEntity(Loc.GetString("wall-stain-cleaning-start"), uid, user); + + var doAfterArgs = new DoAfterArgs(EntityManager, user, TimeSpan.FromSeconds(5), new CleanWallStainDoAfterEvent(), uid, target: uid, used: tool) + { + BreakOnMove = true, + BreakOnDamage = true, + NeedHand = true + }; + + if (_doAfter.TryStartDoAfter(doAfterArgs)) + { + args.Handled = true; + } + } + + private void OnCleanDoAfter(EntityUid uid, StainedWallComponent component, CleanWallStainDoAfterEvent args) + { + if (args.Cancelled || args.Handled) + return; + + args.Handled = true; + _popup.PopupEntity(Loc.GetString("wall-stain-cleaning-success"), uid, args.User); + + RaiseLocalEvent(uid, new CleanWallStainsEvent(transformToWater: false)); + } + + private void OnCleanEvent(EntityUid uid, StainedWallComponent component, CleanWallStainsEvent args) + { + if (args.TransformToWater) + { + var children = Transform(uid).ChildEnumerator; + while (children.MoveNext(out var child)) + { + if (!TryComp(child, out var stain) || + !_solution.TryGetSolution(child, stain.SolutionName, out var solComp)) + continue; + var totalVolume = solComp.Value.Comp.Solution.Volume; + if (totalVolume <= 0) + continue; + _solution.RemoveAllSolution(solComp.Value); + _solution.TryAddReagent(solComp.Value, WaterReagent, totalVolume, out _); + UpdateVisuals(child, stain); + } + + if (TryComp(uid, out var forensics)) + { + forensics.DNAs.Clear(); + } + } + else + { + var children = Transform(uid).ChildEnumerator; + while (children.MoveNext(out var child)) + { + if (HasComp(child)) + QueueDel(child); + } + + if (TryComp(uid, out var forensics)) + { + forensics.DNAs.Clear(); + } + + RemCompDeferred(uid); + } + } + + private bool IsCleaningTool(EntityUid uid) + { + return HasComp(uid) || _tag.HasTag(uid, SoapTag); + } + + private void UpdateVisuals(EntityUid uid, WallStainComponent? comp = null) + { + if (!Resolve(uid, ref comp)) + return; + + if (!_solution.TryGetSolution(uid, comp.SolutionName, out _, out var solution)) + return; + + var color = solution.GetColor(_prototype); + comp.Color = color.WithAlpha(color.A * 0.6f); + comp.StainState = solution.ContainsPrototype(WaterReagent) || solution.ContainsPrototype(SpaceCleanerReagent) ? "drip" : "splatter"; + Dirty(uid, comp); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + _evaporationAccumulator += frameTime; + if (_evaporationAccumulator < 1f) + return; + + _evaporationAccumulator -= 1f; + + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var stain)) + { + if (!_solution.TryGetSolution(uid, stain.SolutionName, out var solComp)) + continue; + + var solution = solComp.Value.Comp.Solution; + if (solution.Volume <= 0) + { + QueueDel(uid); + continue; + } + + var waterQty = solution.GetTotalPrototypeQuantity(WaterReagent); + var cleanerQty = solution.GetTotalPrototypeQuantity(SpaceCleanerReagent); + + if (waterQty > 0 || cleanerQty > 0) + { + var evaporationAmount = FixedPoint2.New(0.5f); + + if (waterQty > 0) + { + var toRemove = FixedPoint2.Min(evaporationAmount, waterQty); + _solution.RemoveReagent(solComp.Value, WaterReagent, toRemove); + evaporationAmount -= toRemove; + } + + if (evaporationAmount > 0 && cleanerQty > 0) + { + var toRemove = FixedPoint2.Min(evaporationAmount, cleanerQty); + _solution.RemoveReagent(solComp.Value, SpaceCleanerReagent, toRemove); + } + + _solution.UpdateChemicals(solComp.Value); + UpdateVisuals(uid, stain); + } + + if (solution.Volume > 0) + continue; + var parent = Transform(uid).ParentUid; + + Spawn("WallStainSparkle", Transform(uid).Coordinates); + + QueueDel(uid); + + if (!parent.IsValid()) + continue; + var hasOtherStains = false; + var children = Transform(parent).ChildEnumerator; + while (children.MoveNext(out var child)) + { + if (child == uid || !HasComp(child)) + continue; + hasOtherStains = true; + break; + } + + if (hasOtherStains) + continue; + RemCompDeferred(parent); + if (TryComp(parent, out var reactive)) + { + reactive.Reactions?.Remove(_stainCleanEffectEntry); + } + } + } +} diff --git a/Content.Server/_Funkystation/WashingMachine/WashingMachineSystem.cs b/Content.Server/_Funkystation/WashingMachine/WashingMachineSystem.cs new file mode 100644 index 000000000000..7e3f52e754b1 --- /dev/null +++ b/Content.Server/_Funkystation/WashingMachine/WashingMachineSystem.cs @@ -0,0 +1,52 @@ +using Content.Shared._Funkystation.WashingMachine; +using Content.Shared._Funkystation.Stains.Components; +using Content.Shared._Funkystation.Stains.Systems; +using Content.Shared.Chemistry.EntitySystems; +using Content.Shared.Chemistry.Components; +using Content.Shared.Damage; +using Content.Shared.Damage.Prototypes; +using Content.Shared.Destructible; +using Content.Shared.Storage.Components; +using Content.Server.Forensics; +using Content.Shared.Clothing.Components; +using Robust.Shared.Audio; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; +using System.Linq; +using Content.Shared.Chemistry; +using Content.Shared.Damage.Systems; + +namespace Content.Server._Funkystation.WashingMachine; + +public sealed partial class WashingMachineSystem : SharedWashingMachineSystem +{ + [Dependency] private SharedSolutionContainerSystem _solution = null!; + [Dependency] private ForensicsSystem _forensics = null!; + + protected override void UpdateForensics(Entity ent, HashSet items) + { + if (!TryComp(ent.Owner, out var forensics)) + return; + + foreach (var item in items) + { + // Pull DNA out of the item's stain solution before the shared FinishWash washes it out. + if (TryComp(item, out var stain) + && _solution.TryGetSolution(item, stain.SolutionName, out var sol)) + { + forensics.DNAs.UnionWith(_forensics.GetSolutionsDNA(sol.Value.Comp.Solution)); + } + + if (!TryComp(item, out var fiber)) + continue; + + var fiberText = fiber.FiberColor == null + ? Loc.GetString("forensic-fibers", ("material", fiber.FiberMaterial)) + : Loc.GetString("forensic-fibers-colored", + ("color", fiber.FiberColor), + ("material", fiber.FiberMaterial)); + + forensics.Fibers.Add(fiberText); + } + } +} diff --git a/Content.Shared/Chemistry/Components/RefillableSolutionComponent.Starlight.cs b/Content.Shared/Chemistry/Components/RefillableSolutionComponent.Starlight.cs new file mode 100644 index 000000000000..1a806091c5cd --- /dev/null +++ b/Content.Shared/Chemistry/Components/RefillableSolutionComponent.Starlight.cs @@ -0,0 +1,18 @@ +using Content.Shared.Chemistry.Reagent; +using Robust.Shared.Prototypes; +using Robust.Shared.GameStates; + +namespace Content.Shared.Chemistry.Components; + +/// +/// Just contains the reagent whitelist part of it, so you can accept more than just one. +/// +public sealed partial class RefillableSolutionComponent : Component +{ + /// + /// Reagents that are allowed to be transferred into this solution. + /// Null allows all reagents. + /// + [DataField] + public HashSet>? ReagentWhitelist; +} diff --git a/Content.Shared/Chemistry/Components/Solution.cs b/Content.Shared/Chemistry/Components/Solution.cs index ec911bb12d83..ae9bc2d92b11 100644 --- a/Content.Shared/Chemistry/Components/Solution.cs +++ b/Content.Shared/Chemistry/Components/Solution.cs @@ -922,6 +922,62 @@ public Color GetColor(IPrototypeManager? protoMan) return GetColorWithout(protoMan); } + // Funky start + public int GetSolutionFlammability(IPrototypeManager? protoMan) + { + if (Volume <= 0) + return 0; + + IoCManager.Resolve(ref protoMan); + var totalFlammability = 0f; + foreach (var (reagent, quantity) in Contents) + { + if (protoMan.TryIndex(reagent.Prototype, out var proto)) + { + totalFlammability += proto.Flammability * (quantity.Float() / Volume.Float()); + } + } + return (int) MathF.Round(totalFlammability); + } + + public bool IsSolutionSelfOxidizing(IPrototypeManager? protoMan) + { + if (Volume <= 0) + return false; + + IoCManager.Resolve(ref protoMan); + foreach (var (reagent, _) in Contents) + { + if (protoMan.TryIndex(reagent.Prototype, out var proto) && proto.SelfOxidizing) + { + return true; + } + } + return false; + } + + public void BurnFlammableReagents(float fraction, IPrototypeManager? protoMan) + { + IoCManager.Resolve(ref protoMan); + var clone = Clone(); + foreach (var (reagent, quantity) in Contents) + { + if (!protoMan.TryIndex(reagent.Prototype, out var proto) || proto.Flammability <= 0) + continue; + + var rawBurn = quantity.Float() * fraction * proto.Flammability; + var roundedBurn = MathF.Ceiling(rawBurn * 100f) / 100f; + if (roundedBurn <= 0f) + continue; + + clone.RemoveReagent(reagent, FixedPoint2.New(roundedBurn)); + } + Contents = clone.Contents; + Volume = clone.Volume; + _heatCapacityDirty = true; + ValidateSolution(); + } + // Funky end public Color GetColorWithOnly(IPrototypeManager? protoMan, params ProtoId[] included) { if (Volume == FixedPoint2.Zero) diff --git a/Content.Shared/Chemistry/EntitySystems/SharedSolutionContainerSystem.cs b/Content.Shared/Chemistry/EntitySystems/SharedSolutionContainerSystem.cs index 8fc579a66c79..a426b8a5ac7b 100644 --- a/Content.Shared/Chemistry/EntitySystems/SharedSolutionContainerSystem.cs +++ b/Content.Shared/Chemistry/EntitySystems/SharedSolutionContainerSystem.cs @@ -464,6 +464,14 @@ public void RemoveAllSolution(Entity soln) UpdateChemicals(soln); } + // Funky start + public void BurnFlammableReagents(Entity soln, float fraction) + { + soln.Comp.Solution.BurnFlammableReagents(fraction, PrototypeManager); + UpdateChemicals(soln); + } + // Funky end + /// /// Sets the capacity (maximum volume) of a solution to a new value. /// diff --git a/Content.Shared/Chemistry/EntitySystems/SolutionTransferSystem.Starlight.cs b/Content.Shared/Chemistry/EntitySystems/SolutionTransferSystem.Starlight.cs new file mode 100644 index 000000000000..7da13a710ad2 --- /dev/null +++ b/Content.Shared/Chemistry/EntitySystems/SolutionTransferSystem.Starlight.cs @@ -0,0 +1,27 @@ +using Content.Shared.Chemistry.Components; + +namespace Content.Shared.Chemistry.EntitySystems; + +public sealed partial class SolutionTransferSystem : EntitySystem +{ + // Multiple reagent whitelist + private void OnRefillTransferAttempt( + Entity ent, + ref SolutionTransferAttemptEvent args) + { + if (args.To != ent.Owner || + ent.Comp.ReagentWhitelist is not { } whitelist) + { + return; + } + + foreach (var (reagent, _) in args.SolutionEntity.Comp.Solution) + { + if (whitelist.Contains(reagent.Prototype)) + continue; + + args.Cancel(Loc.GetString("comp-solution-transfer-reagent-not-allowed")); + return; + } + } +} diff --git a/Content.Shared/Chemistry/EntitySystems/SolutionTransferSystem.cs b/Content.Shared/Chemistry/EntitySystems/SolutionTransferSystem.cs index efc400264c60..cd0b31e574d5 100644 --- a/Content.Shared/Chemistry/EntitySystems/SolutionTransferSystem.cs +++ b/Content.Shared/Chemistry/EntitySystems/SolutionTransferSystem.cs @@ -42,6 +42,8 @@ public override void Initialize() SubscribeLocalEvent(OnSolutionDrainTransferDoAfter); SubscribeLocalEvent(OnSolutionFillTransferDoAfter); + SubscribeLocalEvent(OnRefillTransferAttempt); // Starlight + _refillableQuery = GetEntityQuery(); _drainableQuery = GetEntityQuery(); } diff --git a/Content.Shared/Chemistry/Reagent/ReagentPrototype.cs b/Content.Shared/Chemistry/Reagent/ReagentPrototype.cs index c7772ebc74b7..26c7ff9c3d02 100644 --- a/Content.Shared/Chemistry/Reagent/ReagentPrototype.cs +++ b/Content.Shared/Chemistry/Reagent/ReagentPrototype.cs @@ -169,6 +169,18 @@ public sealed partial class ReagentPrototype : IPrototype, IInheritingPrototype [DataField] public bool WorksOnTheDead; + /// + /// Funky - How flammable this reagent is. Higher values make it catch fire more easily and burn hotter. + /// + [DataField] + public int Flammability; + + /// + /// Funky - If true, this reagent acts as its own oxidizer and can burn in vacuums or oxygen-deprived environments. + /// + [DataField] + public bool SelfOxidizing; + [DataField, AlwaysPushInheritance] public ReagentMetabolisms? Metabolisms; diff --git a/Content.Shared/Fluids/AbsorbentComponent.cs b/Content.Shared/Fluids/AbsorbentComponent.cs index 9931a04a550d..073e34357abe 100644 --- a/Content.Shared/Fluids/AbsorbentComponent.cs +++ b/Content.Shared/Fluids/AbsorbentComponent.cs @@ -56,4 +56,15 @@ public sealed partial class AbsorbentComponent : Component /// [DataField] public bool UseAbsorberSolution = true; + + // Funky start - Footprints + [DataField] + public float FootprintCleaningRange = 0.2f; + + /// + /// How many footprints within FootprintCleaningRange can be cleaned at once. + /// + [DataField] + public int MaxCleanedFootprints = 9; + // Funky end } diff --git a/Content.Shared/Fluids/Components/PuddleComponent.cs b/Content.Shared/Fluids/Components/PuddleComponent.cs index 2138d0414923..a0124b042448 100644 --- a/Content.Shared/Fluids/Components/PuddleComponent.cs +++ b/Content.Shared/Fluids/Components/PuddleComponent.cs @@ -1,4 +1,5 @@ using Content.Shared.Chemistry.Components; +using Content.Shared.Chemistry.Reagent; using Content.Shared.FixedPoint; using Robust.Shared.Audio; using Robust.Shared.GameStates; @@ -27,5 +28,28 @@ public sealed partial class PuddleComponent : Component [ViewVariables] public Entity? Solution; + + // Funky start - Footprints + /// + /// Whether or not this puddle applies the effects of its contents' and + /// . + /// + [DataField] + public bool AffectsMovement = true; + + /// + /// Whether or not this puddle applies the effects of its contents' . + /// + [DataField] + public bool AffectsSound = true; + // Funky end + + // Moff start - footprints + /// + /// Whether or not this puddle can apply stains. + /// + [DataField] + public bool CausesStains = true; + // Moff end } } diff --git a/Content.Shared/Fluids/SharedAbsorbentSystem.cs b/Content.Shared/Fluids/SharedAbsorbentSystem.cs index 3c8c963743a7..b279b50aff8f 100644 --- a/Content.Shared/Fluids/SharedAbsorbentSystem.cs +++ b/Content.Shared/Fluids/SharedAbsorbentSystem.cs @@ -1,4 +1,5 @@ using System.Numerics; +using Content.Shared._Funkystation.Footprints; using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.EntitySystems; using Content.Shared.FixedPoint; @@ -357,6 +358,18 @@ private bool TryPuddleInteract(Entity ab _melee.DoLunge(user, absorbEnt, Angle.Zero, localPos, null); + // Funky start - Footprints + var ev = new FootprintCleanEvent(); + RaiseLocalEvent(target, ref ev); + // Funky end + + // Moff start - Footprint cleaning sounds + if (ev.Handled) + { + _audio.PlayPredicted(absorber.PickupSound, user, user); + } + // Moff end + return true; } } diff --git a/Content.Shared/Fluids/SharedPuddleSystem.Spillable.cs b/Content.Shared/Fluids/SharedPuddleSystem.Spillable.cs index 5a65d5678982..fd4dcae6de13 100644 --- a/Content.Shared/Fluids/SharedPuddleSystem.Spillable.cs +++ b/Content.Shared/Fluids/SharedPuddleSystem.Spillable.cs @@ -16,6 +16,7 @@ using Content.Shared.Verbs; using Content.Shared.Weapons.Melee; using Content.Shared.Weapons.Melee.Events; +using Content.Shared._Funkystation.Fluids; using Robust.Shared.Player; namespace Content.Shared.Fluids; @@ -163,6 +164,14 @@ private void SplashOnMeleeHit(Entity entity, ref MeleeHitEve var splitSolution = _solutionContainerSystem.SplitSolution(soln.Value, totalSplit / hitCount); + // Forky - Start - Stains + if (splitSolution.Volume > 0) + { + var stainEv = new SpilledOnEvent(entity.Owner, splitSolution.Clone()); + RaiseLocalEvent(hit, stainEv); + } + // Forky - End + AdminLogger.Add(LogType.MeleeHit, $"{ToPrettyString(args.User):actor} " + $"splashed {SharedSolutionContainerSystem.ToPrettyString(splitSolution):solution} " diff --git a/Content.Shared/Fluids/SharedPuddleSystem.cs b/Content.Shared/Fluids/SharedPuddleSystem.cs index b786912b102a..43eba09a55b5 100644 --- a/Content.Shared/Fluids/SharedPuddleSystem.cs +++ b/Content.Shared/Fluids/SharedPuddleSystem.cs @@ -1,4 +1,5 @@ using System.Linq; +using Content.Shared._Funkystation.Fluids; using Content.Shared.Administration.Logs; using Content.Shared.Chemistry; using Content.Shared.Chemistry.Components; @@ -10,17 +11,22 @@ using Content.Shared.FixedPoint; using Content.Shared.Fluids.Components; using Content.Shared.Friction; +using Content.Shared.Gravity; +using Content.Shared.Inventory; using Content.Shared.Movement.Components; using Content.Shared.Movement.Events; using Content.Shared.Movement.Systems; using Content.Shared.Nutrition.EntitySystems; using Content.Shared.Popups; using Content.Shared.Slippery; +using Content.Shared.Standing; using Content.Shared.StepTrigger.Components; using Content.Shared.StepTrigger.Systems; using Robust.Shared.Audio.Systems; using Robust.Shared.Containers; using Robust.Shared.Map; +using Robust.Shared.Physics.Components; +using Robust.Shared.Physics.Events; using Robust.Shared.Prototypes; using Robust.Shared.Timing; @@ -41,6 +47,13 @@ public abstract partial class SharedPuddleSystem : EntitySystem [Dependency] private SpeedModifierContactsSystem _speedModContacts = default!; [Dependency] private StepTriggerSystem _stepTrigger = default!; [Dependency] private TileFrictionController _tile = default!; + [Dependency] private InventorySystem _inventory = default!; // Funky - Clothing stains + [Dependency] private StandingStateSystem _standing = default!; // Moff - Clothing stains + [Dependency] private SharedGravitySystem _gravity = default!; // Moff - Clothing Stains + + [Dependency] private EntityQuery _stepTriggerQuery = default!; + [Dependency] private EntityQuery _reactiveQuery = default!; + [Dependency] private EntityQuery _evaporationQuery = default!; private ProtoId[] _standoutReagents = []; @@ -55,10 +68,6 @@ public abstract partial class SharedPuddleSystem : EntitySystem // loses & then gains reagents in a single tick. private HashSet _deletionQueue = []; - private EntityQuery _stepTriggerQuery; - private EntityQuery _reactiveQuery; - private EntityQuery _evaporationQuery; - public override void Initialize() { base.Initialize(); @@ -98,6 +107,44 @@ public override void Update(float frameTime) TickEvaporation(); } + // Moff start - we basically rewrote this function compared to what funky has + // Using startcollide rather than onstep, since the onstep is messed with by slippable... its bleak + [SubscribeLocalEvent] + private void OnStepInPuddle(Entity ent, ref StartCollideEvent args) + { + // If it dont stain it dont stain + if (!ent.Comp.CausesStains) + return; + + // The thing stepping in the puddle. Because I keep forgetting which is which + var stepper = args.OtherEntity; + + if (!_solutionContainerSystem.ResolveSolution(ent.Owner, ent.Comp.SolutionName, ref ent.Comp.Solution, out var solution)) + return; + + if (solution.Volume <= FixedPoint2.Zero) + return; + + // Check if its in air... because... if you're not on the ground you don't get spilled on + if (TryComp(stepper, out var physicsComp) + && (physicsComp.BodyStatus == BodyStatus.InAir || _gravity.IsWeightless(stepper))) + return; + + // Choose le target... + // if standing and have shoes, just get it on their shoes + EntityUid target; + if (_standing.IsDown(stepper)) // on the ground, spill it on them in general + target = stepper; + else if (_inventory.TryGetSlotEntity(stepper, "shoes", out var shoes) && shoes is { } shoeUid) + target = shoeUid; + else + return; + + var spilledEvent = new SpilledOnEvent(ent.Owner, solution); + RaiseLocalEvent(target, spilledEvent); + } + // Moff end + private void OnPrototypesReloaded(PrototypesReloadedEventArgs ev) { if (ev.WasModified()) @@ -112,7 +159,7 @@ private void CacheStandsout() _standoutReagents = [.. _prototypeManager.EnumeratePrototypes().Where(x => x.Standsout).Select(x => x.ID)]; } - private void OnSolutionUpdate(Entity entity, ref SolutionChangedEvent args) + protected virtual void OnSolutionUpdate(Entity entity, ref SolutionChangedEvent args) // Starlight { // The changes are already networked as part of the same game state. if (_timing.ApplyingState) @@ -129,13 +176,18 @@ private void OnSolutionUpdate(Entity entity, ref SolutionChange _deletionQueue.Remove(entity); UpdateSlip((entity, entity.Comp), args.Solution.Comp.Solution); - UpdateSlow(entity, args.Solution.Comp.Solution); + UpdateSlow(entity, args.Solution.Comp.Solution, entity.Comp); // Funky - Pass component here UpdateEvaporation(entity, args.Solution.Comp.Solution); UpdateAppearance((entity, entity.Comp)); } private void OnGetFootstepSound(Entity entity, ref GetFootstepSoundEvent args) { + // Funky start - footprints + if (!entity.Comp.AffectsSound) + return; + // Funky end + if (!_solutionContainerSystem.ResolveSolution(entity.Owner, entity.Comp.SolutionName, ref entity.Comp.Solution, out var solution)) return; @@ -190,7 +242,7 @@ private void OnEntRemoved(Entity ent, ref EntRemovedFromContain private void UpdateAppearance(Entity ent) { var (uid, puddle, appearance) = ent; - if (!Resolve(ent, ref puddle, ref appearance)) + if (!Resolve(ent, ref puddle, ref appearance, false)) // Funky - Uses TryComp behind the scenes now, protecting Footprints which lack it return; var volume = FixedPoint2.Zero; @@ -322,8 +374,16 @@ private void UpdateSlip(Entity entity, Solution solution) Dirty(entity, slipComp); } - private void UpdateSlow(EntityUid uid, Solution solution) + private void UpdateSlow(EntityUid uid, Solution solution, PuddleComponent puddle) // Funky - added puddlecomponent { + // Funky start - Footprints + if (!puddle.AffectsMovement) + { + RemComp(uid); + return; + } + // Funky end + var maxViscosity = 0f; foreach (var (reagent, _) in solution.Contents) { diff --git a/Content.Shared/Inventory/InventorySystem.Relay.cs b/Content.Shared/Inventory/InventorySystem.Relay.cs index e2d9e5b95b10..ffbde15fe32b 100644 --- a/Content.Shared/Inventory/InventorySystem.Relay.cs +++ b/Content.Shared/Inventory/InventorySystem.Relay.cs @@ -1,3 +1,4 @@ +using Content.Shared._Funkystation.Fluids; using Content.Shared.Armor; using Content.Shared.Atmos; using Content.Shared.Chat; @@ -112,6 +113,7 @@ public void InitializeRelay() SubscribeLocalEvent>(OnGetEquipmentVerbs); SubscribeLocalEvent>(OnGetInnateVerbs); + SubscribeLocalEvent(RelayInventoryEvent); // Funky - Stains } protected void RefRelayInventoryEvent(EntityUid uid, InventoryComponent component, ref T args) where T : IInventoryRelayEvent diff --git a/Content.Shared/Medical/VomitSystem.cs b/Content.Shared/Medical/VomitSystem.cs index 0eb59ec7bcb6..7440c61cf5d0 100644 --- a/Content.Shared/Medical/VomitSystem.cs +++ b/Content.Shared/Medical/VomitSystem.cs @@ -12,6 +12,8 @@ using Content.Shared.Nutrition.Components; using Content.Shared.Nutrition.EntitySystems; using Content.Shared.Popups; +using Content.Shared._Funkystation.Fluids; +using Content.Shared._Funkystation.WallStains; // Funky Wall Stains using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; using Robust.Shared.Network; @@ -135,6 +137,15 @@ public void Vomit(EntityUid uid, float thirstAdded = -40f, float hungerAdded = - solution.AddReagent(new ReagentId(VomitPrototype, _bloodstream.GetEntityBloodData((uid, bloodStream))), vomitAmount); } + // Forky - Start - Stains + var stainEv = new SpilledOnEvent(uid, solution.Clone()); + RaiseLocalEvent(uid, stainEv); + // Forky - End + + // Funky Wall Stains + var splashEv = new SplashOnWallEvent(Transform(uid).Coordinates, solution.Clone()); + RaiseLocalEvent(ref splashEv); + if (_puddle.TrySpillAt(uid, solution, out var puddle, false)) { _forensics.TransferDna(puddle, uid, false); diff --git a/Content.Shared/Slippery/SlipperySystem.cs b/Content.Shared/Slippery/SlipperySystem.cs index 305fc6788976..6a71aa9124d1 100644 --- a/Content.Shared/Slippery/SlipperySystem.cs +++ b/Content.Shared/Slippery/SlipperySystem.cs @@ -15,6 +15,7 @@ using Robust.Shared.Physics.Components; using Robust.Shared.Physics.Systems; using Robust.Shared.Physics.Events; +using Robust.Shared.Timing; namespace Content.Shared.Slippery; @@ -30,6 +31,7 @@ public sealed partial class SlipperySystem : EntitySystem [Dependency] private SharedContainerSystem _container = default!; [Dependency] private SharedPhysicsSystem _physics = default!; [Dependency] private SpeedModifierContactsSystem _speedModifier = default!; + [Dependency] private IGameTiming _timing = default!; // Starlight private EntityQuery _knockedDownQuery; private EntityQuery _physicsQuery; @@ -101,6 +103,9 @@ private bool CanSlip(EntityUid uid, EntityUid toSlip) public void TrySlip(EntityUid uid, SlipperyComponent component, EntityUid other, bool requiresContact = true) { + if (_timing.ApplyingState) // Starlight + return; // Starlight + var knockedDown = _knockedDownQuery.HasComp(other); if (knockedDown && !component.SlipData.SuperSlippery) return; diff --git a/Content.Shared/_Funkystation/CCVar/ReagentFireCVars.cs b/Content.Shared/_Funkystation/CCVar/ReagentFireCVars.cs new file mode 100644 index 000000000000..172a726a87c7 --- /dev/null +++ b/Content.Shared/_Funkystation/CCVar/ReagentFireCVars.cs @@ -0,0 +1,61 @@ +using Robust.Shared.Configuration; + +namespace Content.Shared._Funkystation.CCVar; + +[CVarDefs] +public sealed class ReagentFireCVars +{ + /// + /// Multiplier for the amount of fire stacks applied by flammable stains when ignited + /// + public static readonly CVarDef StainFireStackMultiplier = + CVarDef.Create("funkystation.reagent_fire.stain_stack_multiplier", 1.0f, CVar.SERVERONLY); + + /// + /// Multiplier for the structural and heat damage dealt by reagent puddle fires + /// + public static readonly CVarDef PuddleFireDamageMultiplier = + CVarDef.Create("funkystation.reagent_fire.puddle_damage_multiplier", 1.0f, CVar.SERVERONLY); + + /// + /// Defines whether footprints are flammable. + /// + public static readonly CVarDef FootprintsFlammable = + CVarDef.Create("funkystation.reagent_fire.footprints_flammable", true, CVar.SERVERONLY); + + /// + /// Multiplier for effectiveness of fire protection from equipment. + /// + public static readonly CVarDef FireProtectionEffectiveness = + CVarDef.Create("funkystation.reagent_fire.fire_protection_effectiveness", 1.0f, CVar.SERVERONLY); + + /// + /// Whether puddle volume scales down effective fire intensity for small amounts of liquid + /// + public static readonly CVarDef VolumeScalingEnabled = + CVarDef.Create("funkystation.reagent_fire.volume_scaling_enabled", true, CVar.SERVERONLY); + + /// + /// The solution volume in units at which fire intensity is considered full strength. Puddles below this volume burn proportionally weaker + /// + public static readonly CVarDef VolumeScalingReference = + CVarDef.Create("funkystation.reagent_fire.volume_scaling_reference", 20f, CVar.SERVERONLY); + + /// + /// Exponent applied to the volume ratio. Higher values punish small puddles harder. 1.0 is linear falloff. + /// + public static readonly CVarDef VolumeScalingCurve = + CVarDef.Create("funkystation.reagent_fire.volume_scaling_curve", 1.5f, CVar.SERVERONLY); + + /// + /// Solution volume in units below which puddles burn out rapidly instead of following the normal rate + /// + public static readonly CVarDef SmallPuddleBurnThreshold = + CVarDef.Create("funkystation.reagent_fire.small_puddle_burn_threshold", 5.0f, CVar.SERVERONLY); + + /// + /// Percentage of remaining volume consumed per second once a puddle is below the above threshold + /// + public static readonly CVarDef SmallPuddleBurnPercent = + CVarDef.Create("funkystation.reagent_fire.small_puddle_burn_percent", 0.5f, CVar.SERVERONLY); +} diff --git a/Content.Shared/_Funkystation/Fluids/SpilledOnEvent.cs b/Content.Shared/_Funkystation/Fluids/SpilledOnEvent.cs new file mode 100644 index 000000000000..05dc19bbd9ee --- /dev/null +++ b/Content.Shared/_Funkystation/Fluids/SpilledOnEvent.cs @@ -0,0 +1,15 @@ +using Content.Shared.Chemistry.Components; +using Content.Shared.Inventory; + +namespace Content.Shared._Funkystation.Fluids; + +/// +/// Raised when a fluid is spilled on an entity +/// +public sealed class SpilledOnEvent(EntityUid source, Solution solution) : EntityEventArgs, IInventoryRelayEvent +{ + public SlotFlags TargetSlots { get; } = SlotFlags.WITHOUT_POCKET; + + public EntityUid Source = source; + public Solution Solution = solution; +} diff --git a/Content.Shared/_Funkystation/Footprints/FootprintComponent.cs b/Content.Shared/_Funkystation/Footprints/FootprintComponent.cs new file mode 100644 index 000000000000..4b865ac23225 --- /dev/null +++ b/Content.Shared/_Funkystation/Footprints/FootprintComponent.cs @@ -0,0 +1,19 @@ +using System.Numerics; +using Robust.Shared.GameStates; +using Robust.Shared.Serialization; +using Robust.Shared.Utility; + +namespace Content.Shared._Funkystation.Footprints; + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(raiseAfterAutoHandleState: true)] +public sealed partial class FootprintComponent : Component +{ + [AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)] + public List Prints = new(); + + [DataField] + public ResPath Sprites = new("/Textures/_Funkystation/Effects/footprints.rsi"); +} + +[Serializable, NetSerializable] +public readonly record struct FootprintData(Vector2 Offset, Angle Rotation, Color Color, string State); diff --git a/Content.Shared/_Funkystation/Footprints/FootprintEvents.cs b/Content.Shared/_Funkystation/Footprints/FootprintEvents.cs new file mode 100644 index 000000000000..a2c49c350d12 --- /dev/null +++ b/Content.Shared/_Funkystation/Footprints/FootprintEvents.cs @@ -0,0 +1,7 @@ +using Robust.Shared.Audio; +using Robust.Shared.Serialization; + +namespace Content.Shared._Funkystation.Footprints; + +[ByRefEvent] +public record struct FootprintCleanEvent(bool Handled = false); // Moff - Track handling to play audio diff --git a/Content.Shared/_Funkystation/Footprints/FootprintOwnerComponent.cs b/Content.Shared/_Funkystation/Footprints/FootprintOwnerComponent.cs new file mode 100644 index 000000000000..9ac9ff9562cd --- /dev/null +++ b/Content.Shared/_Funkystation/Footprints/FootprintOwnerComponent.cs @@ -0,0 +1,27 @@ +namespace Content.Shared._Funkystation.Footprints; + +[RegisterComponent] +public sealed partial class FootprintOwnerComponent : Component +{ + // Moff start - Divide all these values by 10. We dont want them messing with the puddle volume too much. + // Of course, we can change this if the specific volumes become important, but for now, they're not. + [DataField] public float MaxFootVolume = 1f; + [DataField] public float MaxBodyVolume = 2f; + + [DataField] public float MinPrintVolume = 0.05f; + [DataField] public float MaxFootprintVolume = 0.1f; + + [DataField] public float MinBodyPrintVolume = 0.2f; + [DataField] public float MaxBodyprintVolume = 0.5f; + // Moff end + + [DataField] public float PrintMixAmount = 0.1f; // Moff - When the print volume is full, how much should other puddles be mixed when theyre stepped in. + + [DataField] public float FootstepDistance = 0.5f; + [DataField] public float DragDistance = 1f; + + [ViewVariables(VVAccess.ReadWrite)] + public float DistanceWalked; + + [DataField] public float AlternateStepOffset = 0.0625f; +} diff --git a/Content.Shared/_Funkystation/Footprints/FootprintSystem.cs b/Content.Shared/_Funkystation/Footprints/FootprintSystem.cs new file mode 100644 index 000000000000..d28565461789 --- /dev/null +++ b/Content.Shared/_Funkystation/Footprints/FootprintSystem.cs @@ -0,0 +1,298 @@ +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Numerics; +using Content.Shared.Chemistry.Components; +using Content.Shared.Chemistry.EntitySystems; +using Content.Shared.FixedPoint; +using Content.Shared.Fluids; +using Content.Shared.Fluids.Components; +using Content.Shared.Inventory; +using Content.Shared.Random.Helpers; +using Content.Shared.Standing; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Map; +using Robust.Shared.Map.Components; +using Robust.Shared.Player; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; +using Robust.Shared.Timing; +using Robust.Shared.Utility; + +namespace Content.Shared._Funkystation.Footprints; + +public sealed partial class FootprintSystem : EntitySystem +{ + [Dependency] private SharedTransformSystem _transform = null!; + [Dependency] private SharedMapSystem _map = null!; + [Dependency] private SharedSolutionContainerSystem _solutionContainer = null!; + [Dependency] private SharedPuddleSystem _puddle = null!; + [Dependency] private IPrototypeManager _prototypeManager = null!; + [Dependency] private InventorySystem _inventory = null!; + [Dependency] private IGameTiming _timing = default!; + [Dependency] private SharedAudioSystem _audio = default!; + + + private static readonly FixedPoint2 MaxVolumePerTile = 50; + private static readonly EntProtoId FootprintEntityId = "Footprint"; + private const string PrintSolutionName = "print"; + private const string PuddleTargetSolution = "puddle"; + + private static readonly string[] DragStates = + [ + "dragging-1", + "dragging-2", + "dragging-3", + "dragging-4", + "dragging-5" + ]; + + public override void Initialize() + { + base.Initialize(); + + // Listen for chemical changes (like Space Cleaner) + // Moff, after sharedpuddle + SubscribeLocalEvent(OnSolutionChanged, after: [typeof(SharedPuddleSystem)]); + } + + private void OnSolutionChanged(EntityUid uid, FootprintComponent component, ref SolutionChangedEvent args) + { + UpdatePrintColors(uid, component); + } + + private void UpdatePrintColors(EntityUid uid, FootprintComponent component) + { + if (!_solutionContainer.TryGetSolution(uid, PrintSolutionName, out var solution, out _)) + return; + + var newBaseColor = solution.Value.Comp.Solution.GetColor(_prototypeManager); + + for (var i = 0; i < component.Prints.Count; i++) + { + var print = component.Prints[i]; + // Update the RGB while preserving the original alpha (transparency) of that specific step + var updatedColor = newBaseColor.WithAlpha(print.Color.A); + component.Prints[i] = print with { Color = updatedColor }; + } + + Dirty(uid, component); + } + + [SubscribeLocalEvent] + private void OnFootprintCleaned(Entity ent, ref FootprintCleanEvent args) + { + TurnIntoPuddle(ent.Owner); + args.Handled = true; + } + + [SubscribeLocalEvent] + private void OnEntityMoved(Entity ent, ref MoveEvent args) + { + if (HasComp(ent.Owner)) + return; + + if (_inventory.TryGetSlotEntity(ent.Owner, "shoes", out var shoes) && HasComp(shoes)) + return; + + if (!args.OldPosition.IsValid(EntityManager) || !args.NewPosition.IsValid(EntityManager)) + return; + + var prevPos = _transform.ToMapCoordinates(args.OldPosition).Position; + var currentPos = _transform.ToMapCoordinates(args.NewPosition).Position; + + ent.Comp.DistanceWalked += Vector2.Distance(currentPos, prevPos); + + var isStanding = !TryComp(ent.Owner, out var standing) || standing.Standing; + var requiredDistance = isStanding ? ent.Comp.FootstepDistance : ent.Comp.DragDistance; + + if (ent.Comp.DistanceWalked < requiredDistance) + return; + + ent.Comp.DistanceWalked -= requiredDistance; + + var xform = Transform(ent.Owner); + if (xform.GridUid is not { } gridUid || !TryComp(gridUid, out var grid)) + return; + + var oldLocal = _map.WorldToLocal(gridUid, grid, prevPos); + var newLocal = _map.WorldToLocal(gridUid, grid, currentPos); + var moveVector = newLocal - oldLocal; + + if (moveVector.LengthSquared() < 0.0001f) + return; + + var walkAngle = moveVector.ToAngle(); + var rotation = walkAngle + Angle.FromDegrees(90); + + var stepOffset = isStanding ? ent.Comp.AlternateStepOffset : 0f; + ent.Comp.AlternateStepOffset = -ent.Comp.AlternateStepOffset; + + var rightVector = new Angle(walkAngle.Theta - Math.PI / 2).ToVec(); + var offsetPos = newLocal + rightVector * stepOffset; + + var coords = new EntityCoordinates(gridUid, offsetPos); + var tileIndices = _map.CoordinatesToTile(gridUid, grid, coords); + + if (ProcessPuddleStepping(ent.Owner, ent.Comp, gridUid, grid, tileIndices, isStanding)) + return; + + CreateFootprint(ent.Owner, ent.Comp, gridUid, grid, tileIndices, coords, rotation, isStanding); + } + + private bool ProcessPuddleStepping(EntityUid uid, FootprintOwnerComponent component, EntityUid gridUid, MapGridComponent grid, Vector2i tile, bool isStanding) + { + if (!TryGetAnchoredPuddle(gridUid, grid, tile, out var puddleUid, out var puddle)) + return false; + + if (!_solutionContainer.TryGetSolution(puddleUid, PuddleTargetSolution, out var puddleSolution, out _)) + return false; + + var maxStorage = isStanding ? component.MaxFootVolume : component.MaxBodyVolume; + + // Moff start - Use a non-deprecated method to get the solution + if (!_solutionContainer.TryGetSolution(uid, PrintSolutionName, out var s) || s is not {} ownerSolution) + return false; + // Moff End + + if (maxStorage - ownerSolution.Comp.Solution.Volume <= 0) + { + var split = _solutionContainer.SplitSolution(ownerSolution, component.PrintMixAmount); + _puddle.TrySpillAt(Transform(puddleUid).Coordinates, split, out _, false); + } + var spaceLeft = FixedPoint2.Max(0, maxStorage - ownerSolution.Comp.Solution.Volume); + _solutionContainer.TryTransferSolution(ownerSolution, puddleSolution.Value.Comp.Solution, spaceLeft); + return true; + } + + private void CreateFootprint(EntityUid uid, FootprintOwnerComponent component, EntityUid gridUid, MapGridComponent grid, Vector2i tile, EntityCoordinates coords, Angle rotation, bool isStanding) + { + if (!_solutionContainer.TryGetSolution(uid, PrintSolutionName, out var ownerSolution, out _)) + return; + + var transferAmount = CalculateTransferVolume(component, ownerSolution.Value, isStanding); + if (transferAmount < component.MinPrintVolume) + return; + + if (!TryGetAnchoredFootprint(gridUid, grid, tile, out var printUid, out var printComp)) + { + printUid = PredictedSpawnAtPosition(FootprintEntityId, coords); + printComp = Comp(printUid); + } + + // Moff start - Use a non-deprecated method to get the solution + if (_solutionContainer.EnumerateSolutions(printUid) + .Where(s => s.Name == PrintSolutionName) + .FirstOrNull() is not { } printSolution) + return; + // Moff end + + // Moff start - Make alpha calulation better + // Calculate colors and volume + var minVol = isStanding ? component.MinPrintVolume : component.MinBodyPrintVolume; + var maxVol = isStanding ? component.MaxFootprintVolume : component.MaxBodyprintVolume; + var alpha = MathHelper.Clamp01((transferAmount.Float() - minVol) / (maxVol - minVol)); + var color = ownerSolution.Value.Comp.Solution.GetColor(_prototypeManager).WithAlpha(alpha); + // Moff end + + _solutionContainer.TryTransferSolution(printSolution.Solution, ownerSolution.Value.Comp.Solution, transferAmount); + + if (printSolution.Solution.Comp.Solution.Volume >= MaxVolumePerTile) + { + var solClone = printSolution.Solution.Comp.Solution.Clone(); + PredictedQueueDel(printUid); + _puddle.TrySpillAt(coords, solClone, out _, false); + return; + } + + var localPosition = coords.Position; + var normX = (localPosition.X / grid.TileSize) - MathF.Floor(localPosition.X / grid.TileSize) - (grid.TileSize / 2f); + var normY = (localPosition.Y / grid.TileSize) - MathF.Floor(localPosition.Y / grid.TileSize) - (grid.TileSize / 2f); + + var random = SharedRandomExtensions.PredictedRandom(_timing, GetNetEntity(uid)); + var state = isStanding ? "foot" : random.Pick(DragStates); + + printComp.Prints.Add(new FootprintData(new Vector2(normX, normY), rotation, color, state)); + Dirty(printUid, printComp); + } + + [SubscribeLocalEvent] + private void OnPuddleInit(Entity ent, ref MapInitEvent args) + { + if (HasComp(ent.Owner)) + return; + + var xform = Transform(ent.Owner); + if (xform.GridUid is not { } gridUid || !TryComp(gridUid, out var grid)) + return; + + var tile = _map.CoordinatesToTile(gridUid, grid, xform.Coordinates); + if (TryGetAnchoredFootprint(gridUid, grid, tile, out var printUid, out _)) + { + TurnIntoPuddle(printUid, xform.Coordinates); + } + } + + private void TurnIntoPuddle(EntityUid printUid, EntityCoordinates? coords = null) + { + var targetCoords = coords ?? Transform(printUid).Coordinates; + + if (_solutionContainer.TryGetSolution(printUid, PrintSolutionName, out _, out var printSolution)) + { + var clone = printSolution.Clone(); + PredictedQueueDel(printUid); + _puddle.TrySpillAt(targetCoords, clone, out _, false); + } + else + { + PredictedQueueDel(printUid); + } + } + + private FixedPoint2 CalculateTransferVolume(FootprintOwnerComponent component, Entity sol, bool isStanding) + { + var vol = sol.Comp.Solution.Volume; + var maxVolume = isStanding ? component.MaxFootVolume : component.MaxBodyVolume; + var maxPrintVolume = isStanding ? component.MaxFootprintVolume : component.MaxBodyprintVolume; + var minPrintVolume = isStanding ? component.MinPrintVolume : component.MinBodyPrintVolume; + + var fraction = vol / maxVolume; + var spread = maxPrintVolume - minPrintVolume; + return FixedPoint2.Max(FixedPoint2.Min(vol, (spread * fraction) + minPrintVolume), 0f); + } + + private bool TryGetAnchoredPuddle(EntityUid gridUid, MapGridComponent grid, Vector2i tile, out EntityUid entityUid, [NotNullWhen(true)] out PuddleComponent? component) + { + var enumerator = _map.GetAnchoredEntitiesEnumerator(gridUid, grid, tile); + while (enumerator.MoveNext(out var uid)) + { + // CRITICAL: Explicitly ignore footprints so players don't wash their feet with them and delete them! + if (HasComp(uid)) + continue; + + if (TryComp(uid, out component)) + { + entityUid = uid.Value; + return true; + } + } + entityUid = EntityUid.Invalid; + component = null; + return false; + } + + private bool TryGetAnchoredFootprint(EntityUid gridUid, MapGridComponent grid, Vector2i tile, out EntityUid entityUid, [NotNullWhen(true)] out FootprintComponent? component) + { + var enumerator = _map.GetAnchoredEntitiesEnumerator(gridUid, grid, tile); + while (enumerator.MoveNext(out var uid)) + { + if (TryComp(uid, out component)) + { + entityUid = uid.Value; + return true; + } + } + entityUid = EntityUid.Invalid; + component = null; + return false; + } +} diff --git a/Content.Shared/_Funkystation/Footprints/NoFootprintsComponent.cs b/Content.Shared/_Funkystation/Footprints/NoFootprintsComponent.cs new file mode 100644 index 000000000000..1b2040b99fb5 --- /dev/null +++ b/Content.Shared/_Funkystation/Footprints/NoFootprintsComponent.cs @@ -0,0 +1,12 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared._Funkystation.Footprints; + +/// +/// When placed on an entity or an entity's equipped shoes, +/// prevents them from leaving behind footprints. +/// +[RegisterComponent, NetworkedComponent] +public sealed partial class NoFootprintsComponent : Component +{ +} diff --git a/Content.Shared/_Funkystation/ReagentFires/ReagentPuddleFireVisuals.cs b/Content.Shared/_Funkystation/ReagentFires/ReagentPuddleFireVisuals.cs new file mode 100644 index 000000000000..aaa4a198c468 --- /dev/null +++ b/Content.Shared/_Funkystation/ReagentFires/ReagentPuddleFireVisuals.cs @@ -0,0 +1,18 @@ +using Robust.Shared.GameStates; +using Robust.Shared.Serialization; + +namespace Content.Shared._Funkystation.ReagentFires +{ + [Serializable, NetSerializable] + public enum ReagentPuddleFireVisuals : byte + { + OnFire, + FireState, + FireColor + } + + [RegisterComponent, NetworkedComponent] + public sealed partial class ReagentPuddleFireEffectComponent : Component + { + } +} diff --git a/Content.Shared/_Funkystation/Stains/Components/StainBlockerComponent.cs b/Content.Shared/_Funkystation/Stains/Components/StainBlockerComponent.cs new file mode 100644 index 000000000000..1292ed4c8c41 --- /dev/null +++ b/Content.Shared/_Funkystation/Stains/Components/StainBlockerComponent.cs @@ -0,0 +1,14 @@ +using Content.Shared.Inventory; +using Robust.Shared.GameStates; + +namespace Content.Shared._Funkystation.Stains.Components; + +/// +/// Prevents entities equipped in specific slots underneath this item from getting stained +/// +[RegisterComponent, NetworkedComponent] +public sealed partial class StainBlockerComponent : Component +{ + [DataField("slots", required: true)] + public SlotFlags BlockedSlots; +} diff --git a/Content.Shared/_Funkystation/Stains/Components/StainableComponent.cs b/Content.Shared/_Funkystation/Stains/Components/StainableComponent.cs new file mode 100644 index 000000000000..370606bcbb0d --- /dev/null +++ b/Content.Shared/_Funkystation/Stains/Components/StainableComponent.cs @@ -0,0 +1,48 @@ +using Content.Shared.DoAfter; +using Content.Shared.FixedPoint; +using Content.Shared.Tag; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; + +namespace Content.Shared._Funkystation.Stains.Components; + +[RegisterComponent, NetworkedComponent] +public sealed partial class StainableComponent : Component +{ + [DataField] + public string SolutionName = "stain"; + + // Moff start - Reduce stain volume + // Reduce stain volume so its not messing with puddles so much + // right now the specific volume doesnt matter that much, if that changes we can tweak it. + [DataField] + public FixedPoint2 MaxStainVolume = FixedPoint2.New(1); + + [DataField] + public FixedPoint2 SpillTransferAmount = 0.1f; + // Moff end + + [DataField] + public float WringDoAfterDuration = 10f; // Starlight, 15s is too long. + + [DataField] + public Dictionary> ClothingVisuals = new(); + + [DataField] + public Dictionary> ItemVisuals = new(); + + [DataField] + public List IconVisuals = new(); + + [ViewVariables] + public HashSet RevealedLayers = new(); + + // Moff start - Stains not guaranteed + [DataField] + public float StainChance = 0.5f; + // Moff end +} + +[Serializable, NetSerializable] +public sealed partial class WringStainDoAfterEvent : SimpleDoAfterEvent; diff --git a/Content.Shared/_Funkystation/Stains/Systems/SharedStainSystem.cs b/Content.Shared/_Funkystation/Stains/Systems/SharedStainSystem.cs new file mode 100644 index 000000000000..f7f350decec8 --- /dev/null +++ b/Content.Shared/_Funkystation/Stains/Systems/SharedStainSystem.cs @@ -0,0 +1,189 @@ +using Content.Shared._Funkystation.Fluids; +using Content.Shared._Funkystation.Stains.Components; +using Content.Shared.Chemistry.Components; +using Content.Shared.Chemistry.EntitySystems; +using Content.Shared.Chemistry.Reagent; +using Content.Shared.DoAfter; +using Content.Shared.Fluids; +using Content.Shared.Fluids.Components; +using Content.Shared.Inventory; +using Content.Shared.Item; +using Content.Shared.Popups; +using Content.Shared.Random.Helpers; +using Content.Shared.Verbs; +using Robust.Shared.Containers; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; +using Robust.Shared.Serialization; +using Robust.Shared.Timing; +using Robust.Shared.Utility; + +namespace Content.Shared._Funkystation.Stains.Systems; + +[Serializable, NetSerializable] +public enum StainVisuals : byte +{ + Toggle +} + +public abstract partial class SharedStainSystem : EntitySystem +{ + [Dependency] private SharedSolutionContainerSystem _solution = null!; + [Dependency] private SharedItemSystem _item = null!; + [Dependency] private SharedAppearanceSystem _appearance = null!; + [Dependency] private SharedContainerSystem _container = null!; + [Dependency] private InventorySystem _inventory = null!; + [Dependency] private SharedDoAfterSystem _doAfter = null!; + [Dependency] private SharedPuddleSystem _puddle = null!; + [Dependency] private SharedPopupSystem _popup = null!; + [Dependency] private IPrototypeManager _prototype = default!; + [Dependency] private IGameTiming _timing = default!; + + public override void Initialize() + { + base.Initialize(); + + Subs.SubscribeWithRelay(OnSpilledOn); + SubscribeLocalEvent>(OnGetVerbs); + SubscribeLocalEvent(OnWring); + SubscribeLocalEvent(OnSolutionChanged); + } + + private void OnSolutionChanged(Entity ent, ref SolutionChangedEvent args) + { + if (args.Solution.Comp.Id == ent.Comp.SolutionName) + UpdateVisuals(ent); + } + + // Moff start - we basically rewrote this whole function + private void OnSpilledOn(Entity ent, ref SpilledOnEvent args) + { + if (IsStainBlocked(ent)) + return; + + if (!_solution.TryGetSolution(ent.Owner, ent.Comp.SolutionName, out var stainSolution)) + return; + + // Random chance that stains aren't applied + var rand = SharedRandomExtensions.PredictedRandom(_timing, GetNetEntity(ent.Owner)); + if (!rand.Prob(ent.Comp.StainChance)) + return; + + // Get the puddle's solution component, so that we can split the puddle's solution in a way that + // gets networked and updated properly + if (!TryComp(args.Source, out var puddleSolutionComp)) + return; + var split = _solution.SplitSolution((args.Source, puddleSolutionComp), ent.Comp.SpillTransferAmount); + + // fuck water (and similar absorbent substances!) + for (var i = split.Contents.Count - 1; i >= 0; i--) + { + if (_prototype.TryIndex(split.Contents[i].Reagent.Prototype, out var reagentProto) + && reagentProto.Absorbent) + split.RemoveReagent(split.Contents[i].Reagent, split.Contents[i].Quantity); + } + + // Transfer our stuff in + if (split.Volume > 0) + { + // If there's no room, spill out stuff onto floor to make room + // This may end up making it loop a tad, but whatever + // This is kinda stupid when the solution is one thing, but neat for mixing in other reagents + if (split.Volume > stainSolution.Value.Comp.Solution.AvailableVolume) + { + var puddleSplit = _solution.SplitSolution(stainSolution.Value, split.Volume - stainSolution.Value.Comp.Solution.AvailableVolume); + _puddle.TrySpillAt(Transform(args.Source).Coordinates, puddleSplit, out _, false); + } + _solution.TryAddSolution(stainSolution.Value, split); + UpdateVisuals(ent); + OnStained(ent, stainSolution.Value); + } + } + // Moff end + + protected virtual void OnStained(Entity ent, Entity solution) { } + + private bool IsStainBlocked(Entity ent) + { + if (!_container.TryGetContainingContainer(ent.Owner, out var container) || !TryComp(container.Owner, out var inv)) + return false; + + if (!_inventory.TryGetSlot(container.Owner, container.ID, out var slotDef, inv)) + return false; + + foreach (var slot in inv.Slots) + { + if (!_inventory.TryGetSlotEntity(container.Owner, slot.Name, out var slotEnt, inv)) + continue; + + if (TryComp(slotEnt, out var blocker) && (blocker.BlockedSlots & slotDef.SlotFlags) != 0) + return true; + } + + return false; + } + + public void UpdateVisuals(Entity ent) + { + _item.VisualsChanged(ent.Owner); + + if (TryComp(ent.Owner, out var app)) + { + var toggled = true; + if (_appearance.TryGetData(ent.Owner, StainVisuals.Toggle, out bool current, app)) + toggled = !current; + + _appearance.SetData(ent.Owner, StainVisuals.Toggle, toggled, app); + } + if (_container.TryGetContainingContainer(ent.Owner, out var container)) + { + if (TryComp(container.Owner, out var wearerApp)) + { + _appearance.QueueUpdate(container.Owner, wearerApp); + + Dirty(container.Owner, wearerApp); + } + } + } + + private void OnGetVerbs(Entity ent, ref GetVerbsEvent args) + { + if (!args.CanInteract || !args.CanAccess || args.Using != ent.Owner) + return; + + if (!_solution.TryGetSolution(ent.Owner, ent.Comp.SolutionName, out _, out var sol) || sol.Volume <= 0) + return; + + var user = args.User; + args.Verbs.Add(new Verb + { + Text = Loc.GetString("stain-verb-wring"), + Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/bubbles.svg.192dpi.png")), + Act = () => + { + _doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, user, ent.Comp.WringDoAfterDuration, new WringStainDoAfterEvent(), ent.Owner) + { + BreakOnMove = true, + BreakOnDamage = true, + NeedHand = true + }); + } + }); + } + + private void OnWring(Entity ent, ref WringStainDoAfterEvent args) + { + if (args.Handled || args.Cancelled) + return; + args.Handled = true; + + if (!_solution.TryGetSolution(ent.Owner, ent.Comp.SolutionName, out var solComp, out var sol)) + return; + + var split = _solution.SplitSolution(solComp.Value, sol.Volume); + UpdateVisuals(ent); + + if (_puddle.TrySpillAt(args.User, split, out _)) + _popup.PopupEntity(Loc.GetString("stain-verb-wring-success"), args.User, args.User); + } +} diff --git a/Content.Shared/_Funkystation/WallStains/CleanWallStainReaction.cs b/Content.Shared/_Funkystation/WallStains/CleanWallStainReaction.cs new file mode 100644 index 000000000000..b3b5ecc60108 --- /dev/null +++ b/Content.Shared/_Funkystation/WallStains/CleanWallStainReaction.cs @@ -0,0 +1,17 @@ +using Content.Shared._Funkystation.WallStains.Components; +using Content.Shared.EntityEffects; + +namespace Content.Shared._Funkystation.WallStains; + +public sealed partial class CleanWallStainReaction : EntityEffect +{ + public override void RaiseEvent(EntityUid target, IEntityEffectRaiser args, float amount, EntityUid? origin) + { + var entMan = IoCManager.Resolve(); + + if (!entMan.HasComponent(target)) + return; + + entMan.EventBus.RaiseLocalEvent(target, new CleanWallStainsEvent(transformToWater: true)); + } +} diff --git a/Content.Shared/_Funkystation/WallStains/Components/FlammableWallStainComponent.cs b/Content.Shared/_Funkystation/WallStains/Components/FlammableWallStainComponent.cs new file mode 100644 index 000000000000..8b60f994372c --- /dev/null +++ b/Content.Shared/_Funkystation/WallStains/Components/FlammableWallStainComponent.cs @@ -0,0 +1,28 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared._Funkystation.WallStains.Components; + +[RegisterComponent, NetworkedComponent] +public sealed partial class FlammableWallStainComponent : Component +{ + [ViewVariables] + public bool OnFire { get; set; } + + [ViewVariables] + public int Flammability { get; set; } + + [ViewVariables] + public float Accumulator { get; set; } + + [ViewVariables] + public EntityUid? PlayingStream { get; set; } + + [ViewVariables] + public string? CurrentPlayingSound { get; set; } + + [ViewVariables] + public EntityUid? FireEffectEntity { get; set; } + + [ViewVariables] + public int FireState { get; set; } = 4; +} diff --git a/Content.Shared/_Funkystation/WallStains/Components/FlammableWallStainVisualsComponent.cs b/Content.Shared/_Funkystation/WallStains/Components/FlammableWallStainVisualsComponent.cs new file mode 100644 index 000000000000..47b1c566a340 --- /dev/null +++ b/Content.Shared/_Funkystation/WallStains/Components/FlammableWallStainVisualsComponent.cs @@ -0,0 +1,16 @@ +using Robust.Shared.GameStates; +using Robust.Shared.Serialization; + +namespace Content.Shared._Funkystation.WallStains.Components; + +[RegisterComponent, NetworkedComponent] +public sealed partial class WallStainFireVisualsComponent : Component +{ +} + +[Serializable, NetSerializable] +public enum WallStainFireVisuals : byte +{ + FireState, + FireColor +} diff --git a/Content.Shared/_Funkystation/WallStains/Components/StainedWallComponent.cs b/Content.Shared/_Funkystation/WallStains/Components/StainedWallComponent.cs new file mode 100644 index 000000000000..6e51083f8866 --- /dev/null +++ b/Content.Shared/_Funkystation/WallStains/Components/StainedWallComponent.cs @@ -0,0 +1,11 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared._Funkystation.WallStains.Components; + +/// +/// Added to walls that currently have WallStain entities on them +/// +[RegisterComponent, NetworkedComponent] +public sealed partial class StainedWallComponent : Component +{ +} diff --git a/Content.Shared/_Funkystation/WallStains/Components/WallStainComponent.cs b/Content.Shared/_Funkystation/WallStains/Components/WallStainComponent.cs new file mode 100644 index 000000000000..e5ffa0c594f9 --- /dev/null +++ b/Content.Shared/_Funkystation/WallStains/Components/WallStainComponent.cs @@ -0,0 +1,24 @@ +using Content.Shared.FixedPoint; +using Robust.Shared.GameStates; + +namespace Content.Shared._Funkystation.WallStains.Components; + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class WallStainComponent : Component +{ + [DataField, AutoNetworkedField] + public string SolutionName = "stain"; + + [DataField, AutoNetworkedField] + public FixedPoint2 MaxStainVolume = FixedPoint2.New(5); + + [DataField, AutoNetworkedField] + public Color Color { get; set; } = Color.White; + + [DataField, AutoNetworkedField] + public string StainState { get; set; } = "splatter"; + + // Tracks which face of the wall this stain is applied to + [DataField, AutoNetworkedField] + public Vector2i Direction { get; set; } = Vector2i.Zero; +} diff --git a/Content.Shared/_Funkystation/WallStains/WallStainEvents.cs b/Content.Shared/_Funkystation/WallStains/WallStainEvents.cs new file mode 100644 index 000000000000..a485df0b3396 --- /dev/null +++ b/Content.Shared/_Funkystation/WallStains/WallStainEvents.cs @@ -0,0 +1,28 @@ +using Content.Shared.Chemistry.Components; +using Content.Shared.DoAfter; +using Robust.Shared.Map; +using Robust.Shared.Serialization; + +namespace Content.Shared._Funkystation.WallStains; + +[Serializable, NetSerializable] +public enum WallStainVisuals : byte +{ + Color, + State +} + +[Serializable, NetSerializable] +public sealed partial class CleanWallStainDoAfterEvent : SimpleDoAfterEvent; + +[Serializable, NetSerializable] +public sealed partial class PourOnWallDoAfterEvent : SimpleDoAfterEvent; + +[Serializable, NetSerializable] +public sealed class CleanWallStainsEvent(bool transformToWater = false) : EntityEventArgs +{ + public bool TransformToWater = transformToWater; +} + +[ByRefEvent] +public readonly record struct SplashOnWallEvent(EntityCoordinates Coordinates, Solution Solution); diff --git a/Content.Shared/_Funkystation/WashingMachine/SharedWashingMachineSystem.cs b/Content.Shared/_Funkystation/WashingMachine/SharedWashingMachineSystem.cs new file mode 100644 index 000000000000..c126faa49f53 --- /dev/null +++ b/Content.Shared/_Funkystation/WashingMachine/SharedWashingMachineSystem.cs @@ -0,0 +1,260 @@ +using Content.Shared.Interaction; +using Content.Shared.Popups; +using Content.Shared.Power.EntitySystems; +using Content.Shared.Storage.Components; +using Content.Shared.Storage.EntitySystems; +using Content.Shared.Verbs; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Timing; +using Robust.Shared.Utility; +using System.Linq; +using Content.Shared._Funkystation.Stains.Components; +using Content.Shared._Funkystation.Stains.Systems; +using Content.Shared.Chemistry; +using Content.Shared.Chemistry.Components; +using Content.Shared.Chemistry.EntitySystems; +using Content.Shared.Clothing.Components; +using Content.Shared.Damage; +using Content.Shared.Damage.Prototypes; +using Content.Shared.Damage.Systems; +using Content.Shared.Destructible; +using Content.Shared.Random.Helpers; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; + +namespace Content.Shared._Funkystation.WashingMachine; + +public abstract partial class SharedWashingMachineSystem : EntitySystem +{ + [Dependency] private IGameTiming _timing = null!; + [Dependency] private SharedAudioSystem _audio = null!; + [Dependency] private SharedPowerReceiverSystem _power = null!; + [Dependency] private SharedEntityStorageSystem _storage = null!; + [Dependency] private SharedAppearanceSystem _appearance = null!; + [Dependency] private SharedPopupSystem _popup = null!; + [Dependency] private IPrototypeManager _proto = default!; + [Dependency] private DamageableSystem _damageable = null!; + [Dependency] private ReactiveSystem _reactive = null!; + [Dependency] private SharedSolutionContainerSystem _solution = default!; + [Dependency] private SharedStainSystem _stains = default!; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnActivate, before: [typeof(SharedEntityStorageSystem)]); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var comp)) + { + if (comp.State != WashingMachineState.Washing) + continue; + + if (_timing.CurTime >= comp.WashFinishTime) + { + FinishWash(uid, comp); + continue; + } + + ProcessWashingHazards(uid, comp, frameTime); + } + } + + [SubscribeLocalEvent] + private void OnMapInit(Entity ent, ref MapInitEvent args) + { + _appearance.SetData(ent.Owner, WashingMachineVisuals.State, ent.Comp.State); + } + + [SubscribeLocalEvent] + private void OnBreak(Entity ent, ref BreakageEventArgs args) + { + ent.Comp.State = WashingMachineState.Broken; + ent.Comp.WashFinishTime = null; + ent.Comp.AudioStream = _audio.Stop(ent.Comp.AudioStream); + Dirty(ent.Owner, ent.Comp); + _appearance.SetData(ent.Owner, WashingMachineVisuals.State, WashingMachineState.Broken); + } + + [SubscribeLocalEvent] + private void OnStorageOpenAttempt(Entity ent, ref StorageOpenAttemptEvent args) + { + if (ent.Comp.State != WashingMachineState.Idle) + args.Cancelled = true; + } + + [SubscribeLocalEvent] + private void OnGetVerbs(Entity ent, ref GetVerbsEvent args) + { + if (!args.CanInteract || !args.CanComplexInteract) + return; + + if (ent.Comp.State != WashingMachineState.Idle || !_power.IsPowered(ent.Owner) || _storage.IsOpen(ent.Owner)) + return; + + if (!TryComp(ent, out var storage) || storage.Contents.ContainedEntities.Count == 0) + return; + + var user = args.User; + args.Verbs.Add(new ActivationVerb + { + Text = Loc.GetString("washing-machine-start"), + Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/Spare/poweronoff.svg.192dpi.png")), + Act = () => + { + if (_timing.CurTime < ent.Comp.NextWashAllowed) + { + _popup.PopupEntity(Loc.GetString("washing-machine-cooldown"), ent.Owner, user); + return; + } + TryStartWash(ent, user); + } + }); + } + + private void OnActivate(Entity ent, ref ActivateInWorldEvent args) + { + if (args.Handled || !args.Complex) + return; + + if (ent.Comp.State != WashingMachineState.Idle || !_power.IsPowered(ent.Owner) || _storage.IsOpen(ent.Owner)) + return; + + if (!TryComp(ent, out var storage) || storage.Contents.ContainedEntities.Count == 0) + return; + + if (_timing.CurTime < ent.Comp.NextWashAllowed) + { + _popup.PopupEntity(Loc.GetString("washing-machine-cooldown"), ent.Owner, args.User); + args.Handled = true; + return; + } + + args.Handled = true; + TryStartWash(ent, args.User); + } + + private void ProcessWashingHazards(EntityUid uid, WashingMachineComponent comp, float frameTime) + { + if (!TryComp(uid, out var storage) || storage.Contents.ContainedEntities.Count == 0) + return; + + var damageProto = _proto.Index(comp.WashingDamageType); + var damage = new DamageSpecifier(damageProto, comp.BluntDamagePerSecond * frameTime); + + var waterSpray = new Solution(); + waterSpray.AddReagent(comp.WaterSprayReagent, comp.WaterSprayAmount); + + var rand = SharedRandomExtensions.PredictedRandom(_timing, GetNetEntity(uid)); + + var sprayWater = rand.Prob(comp.WaterSprayChance * frameTime); + + var hasHeavyItems = false; + + foreach (var item in storage.Contents.ContainedEntities) + { + _damageable.TryChangeDamage(item, damage, true); + + if (sprayWater) + _reactive.DoEntityReaction(item, waterSpray, ReactionMethod.Touch); + + if (!hasHeavyItems && !HasComp(item)) + hasHeavyItems = true; + } + + if (hasHeavyItems) + { + if (rand.Prob(comp.ThumpSoundChance * frameTime)) + _audio.PlayPredicted(comp.HitSound, uid, uid); + + comp.AccumulatedSelfDamage += comp.SelfDamagePerSecond * frameTime; + } + } + + private void FinishWash(EntityUid uid, WashingMachineComponent comp) + { + comp.State = WashingMachineState.Idle; + comp.WashFinishTime = null; + comp.NextWashAllowed = _timing.CurTime + comp.Cooldown; + + comp.AudioStream = _audio.Stop(comp.AudioStream); + _audio.PlayLocal(comp.WashFinishedSound, uid, null); + _appearance.SetData(uid, WashingMachineVisuals.State, WashingMachineState.Idle); + + HashSet items = new(); + if (TryComp(uid, out var storage)) + { + items = storage.Contents.ContainedEntities.ToHashSet(); + + // Clean off prints and DNA + UpdateForensics((uid, comp), items); + + foreach (var item in items) + { + if (TryComp(item, out var stain) && _solution.TryGetSolution(item, stain.SolutionName, out var sol)) + { + _solution.RemoveAllSolution(sol.Value); + _stains.UpdateVisuals((item, stain)); + } + } + } + + var machineEv = new WashingMachineFinishedWashingEvent(items); + RaiseLocalEvent(uid, ref machineEv); + + var itemEv = new WashingMachineWashedEvent(uid, items); + foreach (var item in items) + { + RaiseLocalEvent(item, ref itemEv); + } + + if (comp.AccumulatedSelfDamage > 0) + { + var damageProto = _proto.Index(comp.WashingDamageType); + var selfDamage = new DamageSpecifier(damageProto, comp.AccumulatedSelfDamage); + _damageable.TryChangeDamage(uid, selfDamage, ignoreResistances: true); + comp.AccumulatedSelfDamage = 0; + } + + Dirty(uid, comp); + _storage.OpenStorage(uid); + } + + private void TryStartWash(Entity ent, EntityUid user) + { + if (ent.Comp.State != WashingMachineState.Idle || !_power.IsPowered(ent.Owner) || _storage.IsOpen(ent.Owner)) + return; + + if (_timing.CurTime < ent.Comp.NextWashAllowed) + return; + + if (!TryComp(ent, out var storage) || storage.Contents.ContainedEntities.Count == 0) + return; + + ent.Comp.State = WashingMachineState.Washing; + ent.Comp.WashFinishTime = _timing.CurTime + ent.Comp.WashTime; + + Dirty(ent.Owner, ent.Comp); + _appearance.SetData(ent.Owner, WashingMachineVisuals.State, WashingMachineState.Washing); + + var items = storage.Contents.ContainedEntities.ToHashSet(); + + var machineEv = new WashingMachineStartedWashingEvent(items); + RaiseLocalEvent(ent.Owner, ref machineEv); + + var itemEv = new WashingMachineIsBeingWashed(ent.Owner, items); + foreach (var item in items) + { + RaiseLocalEvent(item, ref itemEv); + } + + ent.Comp.AudioStream ??= _audio.PlayPredicted(ent.Comp.WashLoopSound, ent.Owner, user)?.Entity; + } + + protected virtual void UpdateForensics(Entity ent, HashSet items) + { + } +} diff --git a/Content.Shared/_Funkystation/WashingMachine/WashingMachineComponent.cs b/Content.Shared/_Funkystation/WashingMachine/WashingMachineComponent.cs new file mode 100644 index 000000000000..77b205dea50f --- /dev/null +++ b/Content.Shared/_Funkystation/WashingMachine/WashingMachineComponent.cs @@ -0,0 +1,76 @@ +using Content.Shared.Damage; +using Content.Shared.Damage.Prototypes; +using Robust.Shared.Audio; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; + +namespace Content.Shared._Funkystation.WashingMachine; + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class WashingMachineComponent : Component +{ + [DataField, AutoNetworkedField] + public TimeSpan WashTime = TimeSpan.FromSeconds(5); + + [DataField, AutoNetworkedField] + public TimeSpan? WashFinishTime; + + [DataField, AutoNetworkedField] + public TimeSpan Cooldown = TimeSpan.FromSeconds(6); + + [DataField, AutoNetworkedField] + public TimeSpan NextWashAllowed; + + [DataField] + public SoundSpecifier? WashLoopSound; + + [DataField] + public SoundSpecifier? WashFinishedSound; + + [DataField, AutoNetworkedField] + public WashingMachineState State = WashingMachineState.Idle; + + public EntityUid? AudioStream; + + [DataField] + public ProtoId WashingDamageType = "Blunt"; + + [DataField, AutoNetworkedField] + public float BluntDamagePerSecond = 6.0f; + + [DataField, AutoNetworkedField] + public float ThumpSoundChance = 0.8f; + + [DataField, AutoNetworkedField] + public string WaterSprayReagent = "Water"; + + [DataField, AutoNetworkedField] + public float WaterSprayAmount = 150.0f; + + [DataField, AutoNetworkedField] + public float WaterSprayChance = 1.0f; + + [DataField, AutoNetworkedField] + public float SelfDamagePerSecond = 6.0f; + + [ViewVariables, AutoNetworkedField] + public float AccumulatedSelfDamage = 0f; + + [DataField] + public SoundSpecifier HitSound = new SoundCollectionSpecifier("MetalThud"); +} + +[Serializable, NetSerializable] +public enum WashingMachineState : byte +{ + Idle, + Washing, + Broken +} + +[Serializable, NetSerializable] +public enum WashingMachineVisuals : byte +{ + State +} diff --git a/Content.Shared/_Funkystation/WashingMachine/WashingMachineEvents.cs b/Content.Shared/_Funkystation/WashingMachine/WashingMachineEvents.cs new file mode 100644 index 000000000000..733902cb9270 --- /dev/null +++ b/Content.Shared/_Funkystation/WashingMachine/WashingMachineEvents.cs @@ -0,0 +1,13 @@ +namespace Content.Shared._Funkystation.WashingMachine; + +[ByRefEvent] +public record struct WashingMachineIsBeingWashed(EntityUid WashingMachine, HashSet Items); + +[ByRefEvent] +public record struct WashingMachineStartedWashingEvent(HashSet Items); + +[ByRefEvent] +public record struct WashingMachineWashedEvent(EntityUid WashingMachine, HashSet Items); + +[ByRefEvent] +public record struct WashingMachineFinishedWashingEvent(HashSet Items); diff --git a/Content.Shared/_Starlight/Medical/Body/Systems/SharedBloodstreamSystem.cs b/Content.Shared/_Starlight/Medical/Body/Systems/SharedBloodstreamSystem.cs index cab2dcede99d..4ed90398873b 100644 --- a/Content.Shared/_Starlight/Medical/Body/Systems/SharedBloodstreamSystem.cs +++ b/Content.Shared/_Starlight/Medical/Body/Systems/SharedBloodstreamSystem.cs @@ -16,11 +16,14 @@ using Content.Shared.Gibbing; using Content.Shared.HealthExaminable; using Content.Shared.Humanoid; +using Content.Shared.Inventory; using Content.Shared.Mobs.Systems; using Content.Shared.Popups; using Content.Shared.Random.Helpers; using Content.Shared.Rejuvenate; using Content.Shared.StatusEffectNew; +using Content.Shared._Funkystation.Fluids; +using Content.Shared._Funkystation.WallStains; using Robust.Shared.Audio.Systems; using Robust.Shared.Containers; using Robust.Shared.Prototypes; @@ -44,6 +47,7 @@ public abstract partial class SharedBloodstreamSystem : EntitySystem [Dependency] private AlertsSystem _alertsSystem = default!; [Dependency] private MobStateSystem _mobStateSystem = default!; [Dependency] private DamageableSystem _damageableSystem = default!; + [Dependency] private EntityLookupSystem _lookup = default!; // Funky - Stains public override void Initialize() { @@ -217,6 +221,20 @@ private void OnDamageChanged(Entity ent, ref DamageChanged var totalFloat = total.Float(); TryModifyBleedAmount(ent.AsNullable(), totalFloat); + // Funky Wall Stains + if (totalFloat >= 2f + && SolutionContainer.ResolveSolution(ent.Owner, ent.Comp.BloodSolutionName, ref ent.Comp.BloodSolution, out var bloodForSplatter) + && bloodForSplatter.Volume > 0) + { + var splatterAmount = FixedPoint2.Min(FixedPoint2.New(totalFloat * 0.15f), bloodForSplatter.Volume); + if (splatterAmount > 0) + { + var splatterSolution = SolutionContainer.SplitSolution(ent.Comp.BloodSolution.Value, splatterAmount); + var splashEv = new SplashOnWallEvent(Transform(ent.Owner).Coordinates, splatterSolution); + RaiseLocalEvent(ref splashEv); + } + } + /// Critical hit. Causes target to lose blood, using the bleed rate modifier of the weapon, currently divided by 5 /// The crit chance is currently the bleed rate modifier divided by 25. /// Higher damage weapons have a higher chance to crit! @@ -502,6 +520,27 @@ public bool TryBleedOut(Entity ent, FixedPoint2 amount) if (tempSolution.Volume > ent.Comp.BleedPuddleThreshold) { + // Forky - start - Clothing stains + var stainEv = new SpilledOnEvent(ent.Owner, tempSolution); + RaiseLocalEvent(ent.Owner, stainEv); + + var xform = Transform(ent.Owner); + foreach (var neighbor in _lookup.GetEntitiesInRange(xform.Coordinates, 1.5f)) + { + if (neighbor == ent.Owner || !HasComp(neighbor)) + continue; + + RaiseLocalEvent(neighbor, new SpilledOnEvent(ent.Owner, tempSolution)); + + if (tempSolution.Volume <= 0) + break; + } + + // Funky Wall Stains + var splashEv = new SplashOnWallEvent(xform.Coordinates, tempSolution.Clone()); + RaiseLocalEvent(ref splashEv); + // Forky - end + _puddle.TrySpillAt(ent.Owner, tempSolution, out _, sound: false); tempSolution.RemoveAllSolution(); @@ -561,6 +600,27 @@ public void SpillAllSolutions(Entity ent) SolutionContainer.RemoveAllSolution(ent.Comp.TemporarySolution.Value); } + // Forky - Start - Clothing stains + var stainEv = new SpilledOnEvent(ent.Owner, tempSol); + RaiseLocalEvent(ent.Owner, stainEv); + + var xform = Transform(ent.Owner); + foreach (var neighbor in _lookup.GetEntitiesInRange(xform.Coordinates, 1.5f)) + { + if (neighbor == ent.Owner || !HasComp(neighbor)) + continue; + + RaiseLocalEvent(neighbor, new SpilledOnEvent(ent.Owner, tempSol)); + + if (tempSol.Volume <= 0) + break; + } + + // Funky Wall Stains + var splashEv = new SplashOnWallEvent(xform.Coordinates, tempSol.Clone()); + RaiseLocalEvent(ref splashEv); + // Forky - End + _puddle.TrySpillAt(ent, tempSol, out _); } diff --git a/Resources/Audio/_Funkystation/Effects/Fire/attributions.yml b/Resources/Audio/_Funkystation/Effects/Fire/attributions.yml new file mode 100644 index 000000000000..a75877ae1faf --- /dev/null +++ b/Resources/Audio/_Funkystation/Effects/Fire/attributions.yml @@ -0,0 +1,9 @@ +- files: ["bigfire.ogg"] + license: "CC-BY-3.0" + copyright: "Taken from Dynamicell via freesound.org, mono'd by AraiMaia for Funkystation" + source: "https://freesound.org/people/Dynamicell/sounds/17548/" + +- files: ["hissing.ogg"] + license: "CC-BY-3.0" + copyright: "Taken from Tomlija via freesound.org, mono'd by AraiMaia for Funkystation" + source: "https://freesound.org/people/Tomlija/sounds/103333/" diff --git a/Resources/Audio/_Funkystation/Effects/Fire/bigfire.ogg b/Resources/Audio/_Funkystation/Effects/Fire/bigfire.ogg new file mode 100644 index 000000000000..92d8d9b07790 Binary files /dev/null and b/Resources/Audio/_Funkystation/Effects/Fire/bigfire.ogg differ diff --git a/Resources/Audio/_Funkystation/Effects/Fire/hissing.ogg b/Resources/Audio/_Funkystation/Effects/Fire/hissing.ogg new file mode 100644 index 000000000000..042e154dfef0 Binary files /dev/null and b/Resources/Audio/_Funkystation/Effects/Fire/hissing.ogg differ diff --git a/Resources/Audio/_Funkystation/Machines/washing_loop.ogg b/Resources/Audio/_Funkystation/Machines/washing_loop.ogg new file mode 100644 index 000000000000..a2f1b822b053 Binary files /dev/null and b/Resources/Audio/_Funkystation/Machines/washing_loop.ogg differ diff --git a/Resources/Audio/_Funkystation/Machines/washing_open.ogg b/Resources/Audio/_Funkystation/Machines/washing_open.ogg new file mode 100644 index 000000000000..0f3b3d60926a Binary files /dev/null and b/Resources/Audio/_Funkystation/Machines/washing_open.ogg differ diff --git a/Resources/Locale/en-US/_Funkystation/stains/wall_stains.ftl b/Resources/Locale/en-US/_Funkystation/stains/wall_stains.ftl new file mode 100644 index 000000000000..a68f7a337d9c --- /dev/null +++ b/Resources/Locale/en-US/_Funkystation/stains/wall_stains.ftl @@ -0,0 +1,6 @@ +wall-stain-cleaning-start = You start cleaning the stain with all your might... +wall-stain-cleaning-success = You manage to clean the stain off. +wall-stain-cleaning-dry-rag = The rag is too dry! Wet it first. +wall-stain-pour-start = You start carefully pouring the contents of {THE($container)} onto the wall... +wall-stain-pour-success = You pour the contents of {THE($container)} onto the wall. +wall-stain-pour-full = The wall is too soaked to hold any more liquid. diff --git a/Resources/Locale/en-US/_Funkystation/stains/washingmachine.ftl b/Resources/Locale/en-US/_Funkystation/stains/washingmachine.ftl new file mode 100644 index 000000000000..152817238227 --- /dev/null +++ b/Resources/Locale/en-US/_Funkystation/stains/washingmachine.ftl @@ -0,0 +1,2 @@ +washing-machine-start = Start washing machine +washing-machine-cooldown = The tank is still draining. diff --git a/Resources/Locale/en-US/_Funkystation/stains/wring.ftl b/Resources/Locale/en-US/_Funkystation/stains/wring.ftl new file mode 100644 index 000000000000..168c322d5110 --- /dev/null +++ b/Resources/Locale/en-US/_Funkystation/stains/wring.ftl @@ -0,0 +1,2 @@ +stain-verb-wring = Wring out clothes +stain-verb-wring-success = You wring out the cloth, spilling liquid all over the floor. diff --git a/Resources/Locale/en-US/_Funkystation/station-events/utility-line-rupture.ftl b/Resources/Locale/en-US/_Funkystation/station-events/utility-line-rupture.ftl new file mode 100644 index 000000000000..d04975c55312 --- /dev/null +++ b/Resources/Locale/en-US/_Funkystation/station-events/utility-line-rupture.ftl @@ -0,0 +1,2 @@ +utility-line-rupture-announcement = Systems detect a high-pressure utility line nearing rupture point {$location}. Expulsion of flammable materials is highly probable. Engineering personnel are requested to intervene urgently. +utility-line-rupture-sender = SIS/TR v3.20 diff --git a/Resources/Locale/en-US/_Starlight/chemistry/components/solution-transfer-component.ftl b/Resources/Locale/en-US/_Starlight/chemistry/components/solution-transfer-component.ftl new file mode 100644 index 000000000000..8bd36e011325 --- /dev/null +++ b/Resources/Locale/en-US/_Starlight/chemistry/components/solution-transfer-component.ftl @@ -0,0 +1 @@ +comp-solution-transfer-reagent-not-allowed = You cannot refill it with that reagent. diff --git a/Resources/Prototypes/Catalog/Fills/Lockers/engineer.yml b/Resources/Prototypes/Catalog/Fills/Lockers/engineer.yml index 2ac429eb0fc5..409e0da67cdf 100644 --- a/Resources/Prototypes/Catalog/Fills/Lockers/engineer.yml +++ b/Resources/Prototypes/Catalog/Fills/Lockers/engineer.yml @@ -168,6 +168,7 @@ - id: trayScanner # Starlight: Add t-ray scanner to atmos lockers - id: ClothingHandsGlovesColorYellow # Starlight: Add insulated gloves to atmos lockers - id: HoloprojectorEngineering # Starlight + - id: FireExtinguisherAtmos # Starlight - id: PlushieLizardJobAtmospherictechnician prob: 0.02 diff --git a/Resources/Prototypes/Catalog/Fills/Lockers/heads.yml b/Resources/Prototypes/Catalog/Fills/Lockers/heads.yml index bee550a71d82..7745c829da44 100644 --- a/Resources/Prototypes/Catalog/Fills/Lockers/heads.yml +++ b/Resources/Prototypes/Catalog/Fills/Lockers/heads.yml @@ -233,6 +233,7 @@ - id: EndymionMonumentFlatpack - id: PrintedDocumentEndymionMemorial - id: Multitool + - id: WashingMachineFlatpack # Starlight End - id: PlushieLizardJobHeadofpersonnel prob: 0.02 @@ -274,6 +275,7 @@ # - id: RCDAmmo # Starlight-edit: CE RCD self-recharges - id: RubberStampCE # - id: MetalFoamGrenade # Starlight-edit: in the EngiVend + - id: FireExtinguisherAtmos # Starlight - id: EngineeringTechFabFlatpack # Starlight - id: BookSecureTerminalManual # Starlight - id: PrintedDocumentGreenshiftAlert #SL @@ -358,7 +360,8 @@ presets: - Greenshift - Sandbox - - id: JawsOfLifeMed #Starlight end + - id: JawsOfLifeMed + - id: WashingMachineFlatpack #Starlight end - id: PlushieLizardJobChiefmedicalofficer prob: 0.02 diff --git a/Resources/Prototypes/Catalog/Fills/Lockers/service.yml b/Resources/Prototypes/Catalog/Fills/Lockers/service.yml index 24d36a2f360c..b9146f42b785 100644 --- a/Resources/Prototypes/Catalog/Fills/Lockers/service.yml +++ b/Resources/Prototypes/Catalog/Fills/Lockers/service.yml @@ -113,6 +113,7 @@ - id: Plunger - id: WireBrush - id: MoproachBox #Starlight + - id: WashingMachineFlatpack # Starlight - id: PlushieLizardJobJanitor prob: 0.02 diff --git a/Resources/Prototypes/Entities/Clothing/Hands/base_clothinghands.yml b/Resources/Prototypes/Entities/Clothing/Hands/base_clothinghands.yml index aa0ca6124aea..e49d87641bb7 100644 --- a/Resources/Prototypes/Entities/Clothing/Hands/base_clothinghands.yml +++ b/Resources/Prototypes/Entities/Clothing/Hands/base_clothinghands.yml @@ -25,6 +25,20 @@ damageProtection: flatReductions: Heat: 5 # the average lightbulb only does around four damage! + # Forky - start - clothing stains + - type: Appearance + - type: Stainable + clothingVisuals: + gloves: + - sprite: _Funkystation/Effects/blood.rsi + state: gloveblood + iconVisuals: + - sprite: _Funkystation/Effects/blood.rsi + state: glovebloodicon + - type: SolutionManager + solutions: + - SolutionStain + # Forky - end - type: entity abstract: true diff --git a/Resources/Prototypes/Entities/Clothing/Head/base_clothinghead.yml b/Resources/Prototypes/Entities/Clothing/Head/base_clothinghead.yml index c5dec6bf3991..5e986fd4223f 100644 --- a/Resources/Prototypes/Entities/Clothing/Head/base_clothinghead.yml +++ b/Resources/Prototypes/Entities/Clothing/Head/base_clothinghead.yml @@ -22,6 +22,20 @@ tags: - ClothMade - WhitelistChameleon + # Forky - start - clothing stains + - type: Appearance + - type: Stainable + clothingVisuals: + head: + - sprite: _Funkystation/Effects/blood.rsi + state: helmetblood + iconVisuals: + - sprite: _Funkystation/Effects/blood.rsi + state: helmetbloodicon + - type: SolutionManager + solutions: + - SolutionStain + # Forky - end - type: entity abstract: true @@ -142,6 +156,10 @@ - HeadTop - HeadSide - FacialHair + # Forky - start - clothing stains + - type: StainBlocker + slots: [MASK] + # Forky - End - type: entity abstract: true @@ -193,6 +211,22 @@ - HeadTop - HeadSide - FacialHair + # Forky - start - clothing stains + - type: Appearance + - type: Stainable + clothingVisuals: + head: + - sprite: _Funkystation/Effects/blood.rsi + state: helmetblood + iconVisuals: + - sprite: _Funkystation/Effects/blood.rsi + state: helmetbloodicon + - type: SolutionManager + solutions: + - SolutionStain + - type: StainBlocker + slots: [MASK] + # Forky - end - type: entity abstract: true @@ -283,3 +317,17 @@ - Hair - HeadTop - HeadSide + # Forky - start - clothing stains + - type: Appearance + - type: Stainable + clothingVisuals: + head: + - sprite: _Funkystation/Effects/blood.rsi + state: helmetblood + iconVisuals: + - sprite: _Funkystation/Effects/blood.rsi + state: helmetbloodicon + - type: SolutionManager + solutions: + - SolutionStain + # Forky - end diff --git a/Resources/Prototypes/Entities/Clothing/Masks/base_clothingmask.yml b/Resources/Prototypes/Entities/Clothing/Masks/base_clothingmask.yml index f0bbac31e531..4a726f265b04 100644 --- a/Resources/Prototypes/Entities/Clothing/Masks/base_clothingmask.yml +++ b/Resources/Prototypes/Entities/Clothing/Masks/base_clothingmask.yml @@ -11,6 +11,20 @@ slots: [mask] - type: StaticPrice price: 25 + # Forky - start - clothing stains + - type: Appearance + - type: Stainable + clothingVisuals: + mask: + - sprite: _Funkystation/Effects/blood.rsi + state: maskblood + iconVisuals: + - sprite: _Funkystation/Effects/blood.rsi + state: maskbloodicon + - type: SolutionManager + solutions: + - SolutionStain + # Forky - end - type: entity abstract: true diff --git a/Resources/Prototypes/Entities/Clothing/OuterClothing/armor.yml b/Resources/Prototypes/Entities/Clothing/OuterClothing/armor.yml index 68ef50b5f469..4d4e03260e93 100644 --- a/Resources/Prototypes/Entities/Clothing/OuterClothing/armor.yml +++ b/Resources/Prototypes/Entities/Clothing/OuterClothing/armor.yml @@ -26,6 +26,16 @@ - type: ExplosionResistance damageCoefficient: 0.90 - type: ArmorSparkEffect #starlight + # Forky - Start - Clothing stains + - type: Stainable + clothingVisuals: + outerClothing: + - sprite: _Funkystation/Effects/blood.rsi + state: armorblood + iconVisuals: + - sprite: _Funkystation/Effects/blood.rsi + state: armorbloodicon + # Forky - End #Standard armor vest, allowed for security and bartenders - type: entity diff --git a/Resources/Prototypes/Entities/Clothing/OuterClothing/base_clothingouter.yml b/Resources/Prototypes/Entities/Clothing/OuterClothing/base_clothingouter.yml index cc3d6df1e1e4..0c72e3f1e1a7 100644 --- a/Resources/Prototypes/Entities/Clothing/OuterClothing/base_clothingouter.yml +++ b/Resources/Prototypes/Entities/Clothing/OuterClothing/base_clothingouter.yml @@ -8,6 +8,29 @@ - outerClothing - type: Sprite state: icon + # Forky - start - clothing stains + - type: Appearance + - type: Stainable + clothingVisuals: + outerClothing: + - sprite: _Funkystation/Effects/blood.rsi + state: outerclothing + iconVisuals: + - sprite: _Funkystation/Effects/blood.rsi + state: outerclothing + - type: Solution + id: food + solution: + maxVol: 30 + reagents: + - ReagentId: Fiber + Quantity: 30 + - type: SolutionManager + solutions: + - SolutionStain + - type: StainBlocker + slots: [INNERCLOTHING] + # Forky - end - type: entity abstract: true @@ -23,6 +46,18 @@ # walkModifier: 0.9 # sprintModifier: 0.9 # - type: HeldSpeedModifier + # Forky - start - clothing stains + - type: Stainable + clothingVisuals: + outerClothing: + - sprite: _Funkystation/Effects/blood.rsi + state: suitblood + iconVisuals: + - sprite: _Funkystation/Effects/blood.rsi + state: suitbloodicon + - type: StainBlocker + slots: [INNERCLOTHING, FEET, GLOVES] + # Forky - end - type: entity abstract: true @@ -48,6 +83,16 @@ - type: SwitchableEquipMode # Starlight - type: StaticPrice price: 70 + # Forky - start - clothing stains + - type: Stainable + clothingVisuals: + outerClothing: + - sprite: _Funkystation/Effects/blood.rsi + state: coatblood + iconVisuals: + - sprite: _Funkystation/Effects/blood.rsi + state: coatbloodicon + # Forky - end - type: entity abstract: true @@ -82,6 +127,16 @@ - state: icon-open map: ["foldedLayer"] visible: false + # Forky - start - clothing stains + - type: Stainable + clothingVisuals: + outerClothing: + - sprite: _Funkystation/Effects/blood.rsi + state: outerclothing + iconVisuals: + - sprite: _Funkystation/Effects/blood.rsi + state: outerclothing + # Forky - end - type: entity abstract: true @@ -189,6 +244,10 @@ tags: - MechReactor # Starlight-end + # Forky - start - clothing stains + - type: StainBlocker + slots: [INNERCLOTHING, FEET, GLOVES] + # Forky - end - type: entity abstract: true @@ -226,6 +285,10 @@ slots: - Tail - type: HeatRadiationBlocker # Starlight - Prevents the body from radiating heat + # Forky - start - clothing stains + - type: StainBlocker + slots: [INNERCLOTHING, FEET, GLOVES] + # Forky - end - type: entity parent: ClothingOuterBase diff --git a/Resources/Prototypes/Entities/Clothing/Shoes/base_clothingshoes.yml b/Resources/Prototypes/Entities/Clothing/Shoes/base_clothingshoes.yml index 19ea2539b6d2..4ce2792207d1 100644 --- a/Resources/Prototypes/Entities/Clothing/Shoes/base_clothingshoes.yml +++ b/Resources/Prototypes/Entities/Clothing/Shoes/base_clothingshoes.yml @@ -23,6 +23,20 @@ - Recyclable - WhitelistChameleon - type: ProtectedFromStepTriggers + # Forky - start - clothing stains + - type: Appearance + - type: Stainable + clothingVisuals: + shoes: + - sprite: _Funkystation/Effects/blood.rsi + state: shoeblood + iconVisuals: + - sprite: _Funkystation/Effects/blood.rsi + state: shoebloodicon + - type: SolutionManager + solutions: + - SolutionStain + # Forky - end - type: entity abstract: true diff --git a/Resources/Prototypes/Entities/Clothing/Shoes/specific.yml b/Resources/Prototypes/Entities/Clothing/Shoes/specific.yml index 13e8c63ae9bd..8f96f1ba8ae8 100644 --- a/Resources/Prototypes/Entities/Clothing/Shoes/specific.yml +++ b/Resources/Prototypes/Entities/Clothing/Shoes/specific.yml @@ -117,6 +117,7 @@ - type: SpeedModifierContactCapClothing maxContactSprintSlowdown: 0.7 maxContactWalkSlowdown: 0.7 + - type: NoFootprints # Funky change - type: StealTarget # Starlight stealGroup: GaloshesCollection # Starlight diff --git a/Resources/Prototypes/Entities/Clothing/Uniforms/base_clothinguniforms.yml b/Resources/Prototypes/Entities/Clothing/Uniforms/base_clothinguniforms.yml index 287dcb80c8c3..02ab6fec81c5 100644 --- a/Resources/Prototypes/Entities/Clothing/Uniforms/base_clothinguniforms.yml +++ b/Resources/Prototypes/Entities/Clothing/Uniforms/base_clothinguniforms.yml @@ -28,6 +28,27 @@ - ClothMade - Recyclable - WhitelistChameleon + # Forky - start - clothing stains + - type: Appearance + - type: Stainable + clothingVisuals: + jumpsuit: + - sprite: _Funkystation/Effects/blood.rsi + state: uniformblood + itemVisuals: + left: + - sprite: Effects/Stains/jumpsuit.rsi + state: inhand-right + right: + - sprite: Effects/Stains/jumpsuit.rsi + state: inhand-left + iconVisuals: + - sprite: _Funkystation/Effects/blood.rsi + state: uniformbloodicon + - type: SolutionManager + solutions: + - SolutionStain + # Forky - end - type: entity abstract: true @@ -37,6 +58,23 @@ - type: Clothing slots: [innerclothing] femaleMask: UniformTop + # Forky - start - clothing stains + - type: Stainable + clothingVisuals: + jumpsuit: + - sprite: _Funkystation/Effects/blood.rsi + state: uniformblood + itemVisuals: + left: + - sprite: Effects/Stains/jumpskirt.rsi + state: inhand-right + right: + - sprite: Effects/Stains/jumppskirt.rsi + state: inhand-left + iconVisuals: + - sprite: _Funkystation/Effects/blood.rsi + state: uniformbloodicon + # Forky - end - type: entity @@ -61,6 +99,23 @@ - type: Clothing slots: [innerclothing] femaleMask: UniformTop + # Forky - start - clothing stains + - type: Stainable + clothingVisuals: + jumpsuit: + - sprite: _Funkystation/Effects/blood.rsi + state: uniformblood + itemVisuals: + left: + - sprite: Effects/Stains/jumpskirt.rsi + state: inhand-right + right: + - sprite: Effects/Stains/jumpskirt.rsi + state: inhand-left + iconVisuals: + - sprite: _Funkystation/Effects/blood.rsi + state: uniformbloodicon + # Forky - end - type: entity abstract: true diff --git a/Resources/Prototypes/Entities/Effects/puddle.yml b/Resources/Prototypes/Entities/Effects/puddle.yml index 05b3106ad304..19ba9c2495f3 100644 --- a/Resources/Prototypes/Entities/Effects/puddle.yml +++ b/Resources/Prototypes/Entities/Effects/puddle.yml @@ -183,6 +183,7 @@ - type: EdgeSpreader id: Puddle - type: StepTrigger + stepOn: true - type: Edible edible: Drink delay: 3 @@ -199,3 +200,51 @@ - type: Tag tags: - DNASolutionScannable + +# Funky footprints +- type: entity + parent: SolutionPrint + id: Footprint + name: footprint + save: false + description: Huh? Whose footprints are these? + placement: + mode: SnapgridCenter + components: + - type: Appearance + - type: Clickable + - type: Transform + noRot: false + anchored: true + - type: Sprite + drawdepth: FloorObjects + - type: Physics + bodyType: Static + - type: Fixtures + fixtures: + slipFixture: + shape: + !type:PhysShapeAabb + bounds: "-0.4,-0.4,0.4,0.4" + mask: + - ItemMask + layer: + - SlipLayer + hard: false + - type: Footprint + - type: Puddle + solution: print + affectsMovement: false + affectsSound: false + causesStains: false # Moff - Its weird trust me, basically puddles with nothing in them. + - type: ExaminableSolution + solution: print + - type: MixableSolution + solution: print + - type: DrawableSolution + solution: print + - type: BadDrink + - type: IgnoresFingerprints + - type: Tag + tags: + - DNASolutionScannable diff --git a/Resources/Prototypes/Entities/Objects/Misc/fire_extinguisher.yml b/Resources/Prototypes/Entities/Objects/Misc/fire_extinguisher.yml index 942474e87151..c8255c581625 100644 --- a/Resources/Prototypes/Entities/Objects/Misc/fire_extinguisher.yml +++ b/Resources/Prototypes/Entities/Objects/Misc/fire_extinguisher.yml @@ -64,7 +64,7 @@ sprite: Objects/Misc/fire_extinguisher.rsi size: Normal - type: MeleeWeapon - wideAnimationRotation: 180 + wideAnimationRotation: 0 # Starlight, you actually want to be able to grab this thing... damage: types: Blunt: 10 diff --git a/Resources/Prototypes/Entities/Objects/Specific/Janitorial/janitor.yml b/Resources/Prototypes/Entities/Objects/Specific/Janitorial/janitor.yml index 978d9aa4c5c6..01d54ca348c1 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Janitorial/janitor.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Janitorial/janitor.yml @@ -38,6 +38,8 @@ size: Large sprite: Objects/Specific/Janitorial/mop.rsi - type: Absorbent + footprintCleaningRange: 0.45 # Funky change + maxCleanedFootprints: 25 # Funky change useAbsorberSolution: true - type: UseDelay delay: 1 @@ -88,6 +90,8 @@ sprite: Objects/Specific/Janitorial/advmop.rsi - type: Absorbent pickupAmount: 100 + footprintCleaningRange: 0.75 # Funky change + maxCleanedFootprints: 25 # Funky change - type: SolutionRegeneration generated: reagents: diff --git a/Resources/Prototypes/Entities/Structures/Windows/window.yml b/Resources/Prototypes/Entities/Structures/Windows/window.yml index 22dd95d3de9c..3430be7e503f 100644 --- a/Resources/Prototypes/Entities/Structures/Windows/window.yml +++ b/Resources/Prototypes/Entities/Structures/Windows/window.yml @@ -150,6 +150,7 @@ - type: Tag tags: - Window + - DirectionalWindow - type: MeleeSound soundGroups: Brute: diff --git a/Resources/Prototypes/GameRules/events.yml b/Resources/Prototypes/GameRules/events.yml index 230afe23b53c..de8f1ebf23cd 100644 --- a/Resources/Prototypes/GameRules/events.yml +++ b/Resources/Prototypes/GameRules/events.yml @@ -42,6 +42,7 @@ - id: FullMoonHowl # Starlight End - id: MalignRiftSpawn # Funky + - id: UtilityLineRupture # Funky - type: entityTable id: BasicAntagEventsTable @@ -828,3 +829,19 @@ range: min: 1 max: 2 # Starlight end + +# Funky Station +- type: entity + parent: BaseStationEvent + id: UtilityLineRupture + name: Utility Line Rupture + components: + - type: UtilityLineRuptureRule + - type: StationEvent + earliestStart: 15 + weight: 6.5 + duration: null + - type: GameRule + delay: + min: 0 + max: 0 diff --git a/Resources/Prototypes/Reagents/Consumable/Drink/alcohol.yml b/Resources/Prototypes/Reagents/Consumable/Drink/alcohol.yml index ef8da3c35563..98e79fa8f6ef 100644 --- a/Resources/Prototypes/Reagents/Consumable/Drink/alcohol.yml +++ b/Resources/Prototypes/Reagents/Consumable/Drink/alcohol.yml @@ -9,6 +9,7 @@ physicalDesc: reagent-physical-desc-strong-smelling flavor: absinthe color: "#33EE00" + flammability: 2 # Funky change metamorphicSprite: sprite: Objects/Consumable/Drinks/absintheglass.rsi state: icon_empty @@ -131,6 +132,7 @@ physicalDesc: reagent-physical-desc-aromatic flavor: bluecuracao color: "#0099FF" + flammability: 1 # Funky change metamorphicSprite: sprite: Objects/Consumable/Drinks/curacaoglass.rsi state: icon_empty @@ -150,6 +152,7 @@ physicalDesc: reagent-physical-desc-strong-smelling flavor: cognac color: "#AB3C05" + flammability: 1 # Funky change recognizable: true metamorphicSprite: sprite: Objects/Consumable/Drinks/cognacglass.rsi @@ -179,6 +182,7 @@ physicalDesc: reagent-physical-desc-strong-smelling flavor: rum color: "#664300" + flammability: 1 # Funky change metamorphicSprite: sprite: Objects/Consumable/Drinks/rumglass.rsi state: icon_empty @@ -198,6 +202,7 @@ physicalDesc: reagent-physical-desc-strong-smelling flavor: alcohol color: "#b05b3c" + flammability: 2 # Funky change boilingPoint: 78.2 meltingPoint: -114.1 metabolisms: @@ -289,6 +294,7 @@ physicalDesc: reagent-physical-desc-strong-smelling flavor: gin color: "#664300" + flammability: 1 # Funky change recognizable: true metamorphicSprite: sprite: Objects/Consumable/Drinks/ginvodkaglass.rsi @@ -363,6 +369,7 @@ physicalDesc: reagent-physical-desc-strong-smelling flavor: bitter color: "#990066" + flammability: 3 # Funky change metamorphicSprite: sprite: Objects/Consumable/Drinks/pwineglass.rsi state: icon_empty @@ -389,6 +396,7 @@ physicalDesc: reagent-physical-desc-strong-smelling flavor: rum color: "#f09f42" + flammability: 1 # Funky change recognizable: true metamorphicSprite: sprite: Objects/Consumable/Drinks/rumglass.rsi @@ -418,6 +426,7 @@ physicalDesc: reagent-physical-desc-strong-smelling flavor: sake color: "#DDDDDD" + flammability: 1 # Funky change metamorphicSprite: sprite: Objects/Consumable/Drinks/sakeglass.rsi state: icon_empty @@ -437,6 +446,7 @@ physicalDesc: reagent-physical-desc-strong-smelling flavor: tequila color: "#d7d1d155" + flammability: 1 # Funky change metamorphicSprite: sprite: Objects/Consumable/Drinks/tequillaglass.rsi state: icon_empty @@ -496,6 +506,7 @@ physicalDesc: reagent-physical-desc-strong-smelling flavor: vodka color: "#d1d1d155" + flammability: 2 # Funky change recognizable: true metamorphicSprite: sprite: Objects/Consumable/Drinks/ginvodkaglass.rsi @@ -526,6 +537,7 @@ physicalDesc: reagent-physical-desc-strong-smelling flavor: whiskey color: "#ee7732" + flammability: 2 # Funky change recognizable: true metamorphicSprite: sprite: Objects/Consumable/Drinks/whiskeyglass.rsi diff --git a/Resources/Prototypes/Reagents/Consumable/Drink/drinks.yml b/Resources/Prototypes/Reagents/Consumable/Drink/drinks.yml index 09878d8e218a..e2980094834f 100644 --- a/Resources/Prototypes/Reagents/Consumable/Drink/drinks.yml +++ b/Resources/Prototypes/Reagents/Consumable/Drink/drinks.yml @@ -452,6 +452,7 @@ recognizable: true boilingPoint: 100.0 meltingPoint: 0.0 + flammability: -4 # Funky change friction: 0.4 # Starlight edit start - Make Water work like Ipecac for Dwarfs, but even worse, also let Diona treat water as food metabolisms: @@ -499,6 +500,7 @@ physicalDesc: reagent-physical-desc-frosty flavor: cold color: "#bed8e6" + flammability: -4 # Funky change recognizable: true meltingPoint: 0.0 boilingPoint: 100.0 diff --git a/Resources/Prototypes/Reagents/Consumable/Food/ingredients.yml b/Resources/Prototypes/Reagents/Consumable/Food/ingredients.yml index 7021e113957f..9719bb755693 100644 --- a/Resources/Prototypes/Reagents/Consumable/Food/ingredients.yml +++ b/Resources/Prototypes/Reagents/Consumable/Food/ingredients.yml @@ -192,6 +192,7 @@ flavorMinimum: 0.05 recognizable: true color: "#b67823" + flammability: 1 # Funky change boilingPoint: 300.0 meltingPoint: -16.0 tileReactions: diff --git a/Resources/Prototypes/Reagents/chemicals.yml b/Resources/Prototypes/Reagents/chemicals.yml index b575ad47bd4a..1d9ef1b242ae 100644 --- a/Resources/Prototypes/Reagents/chemicals.yml +++ b/Resources/Prototypes/Reagents/chemicals.yml @@ -5,6 +5,8 @@ physicalDesc: reagent-physical-desc-acidic flavor: bitter color: "#AF14B7" + flammability: 3 # Funky change + selfOxidizing: true # Funky change boilingPoint: 55.5 meltingPoint: -50.0 @@ -34,6 +36,7 @@ color: "#22282b" boilingPoint: 4200.0 meltingPoint: 3550.0 + flammability: 2 metabolisms: Bloodstream: effects: @@ -120,6 +123,8 @@ allowedJobs: - Chemist color: "#E7EA91" + flammability: 2 # Funky change + selfOxidizing: true # Funky change boilingPoint: 353.2 meltingPoint: 278.7 metabolisms: diff --git a/Resources/Prototypes/Reagents/elements.yml b/Resources/Prototypes/Reagents/elements.yml index d92429975e21..e34d70ac2935 100644 --- a/Resources/Prototypes/Reagents/elements.yml +++ b/Resources/Prototypes/Reagents/elements.yml @@ -131,6 +131,7 @@ physicalDesc: reagent-physical-desc-gaseous flavor: bitter color: "#cccccc" + flammability: 3 # Funky change boilingPoint: -253.0 meltingPoint: -259.2 diff --git a/Resources/Prototypes/Reagents/gases.yml b/Resources/Prototypes/Reagents/gases.yml index ce230e0b648c..b0d3edccf776 100644 --- a/Resources/Prototypes/Reagents/gases.yml +++ b/Resources/Prototypes/Reagents/gases.yml @@ -82,6 +82,7 @@ physicalDesc: reagent-physical-desc-gaseous flavor: bitter color: "#7e009e" + flammability: 3 # Funky change recognizable: true boilingPoint: -127.3 # Random values picked between the actual values for CO2 and O2 meltingPoint: -186.4 @@ -127,6 +128,7 @@ - Engineering flavor: bitter color: "#66ff33" + flammability: 4 # Funky change tileReactions: - !type:FlammableTileReaction temperatureMultiplier: 2.0 @@ -165,6 +167,7 @@ physicalDesc: reagent-physical-desc-odorless flavor: bitter color: "#66ff33" + flammability: -30 # Funky change metabolisms: Bloodstream: effects: @@ -360,6 +363,7 @@ - Chemist flavor: bitter color: "#3a758c" + flammability: -50 # Funky change boilingPoint: -195.8 meltingPoint: -210.0 metabolisms: diff --git a/Resources/Prototypes/Reagents/pyrotechnic.yml b/Resources/Prototypes/Reagents/pyrotechnic.yml index 736cb22f82c4..fdd25e6c14aa 100644 --- a/Resources/Prototypes/Reagents/pyrotechnic.yml +++ b/Resources/Prototypes/Reagents/pyrotechnic.yml @@ -24,6 +24,8 @@ color: "#757245" boilingPoint: 2977.0 # Aluminum oxide meltingPoint: 2030.0 + flammability: 5 # Funky change + selfOxidizing: true # Funky change tileReactions: - !type:FlammableTileReaction temperatureMultiplier: 2 @@ -56,6 +58,7 @@ contrabandSeverity: Major flavor: bitter color: "#FA00AF" + flammability: 4 # Funky change tileReactions: - !type:FlammableTileReaction temperatureMultiplier: 5 @@ -86,6 +89,8 @@ contrabandSeverity: Major flavor: bitter color: "#D4872A" + flammability: 5 # Funky change + selfOxidizing: true # Funky change metabolisms: Bloodstream: effects: @@ -115,6 +120,8 @@ physicalDesc: reagent-physical-desc-blazing contrabandSeverity: Major flavor: bitter + flammability: 5 # Funky change + selfOxidizing: true # Funky change color: "#FFC8C8" tileReactions: - !type:PryTileReaction @@ -172,6 +179,7 @@ recognizable: true boilingPoint: -84.7 # Acetylene. Close enough. meltingPoint: -80.7 + flammability: 2 # Funky change friction: 0.4 tileReactions: - !type:FlammableTileReaction {} diff --git a/Resources/Prototypes/Recipes/Lathes/Packs/service.yml b/Resources/Prototypes/Recipes/Lathes/Packs/service.yml index b9827d3ab337..53c44d8ede03 100644 --- a/Resources/Prototypes/Recipes/Lathes/Packs/service.yml +++ b/Resources/Prototypes/Recipes/Lathes/Packs/service.yml @@ -46,6 +46,7 @@ - SmartFridgeCircuitboard - PersonalCircuitboardDesktop #Starlight - TP14DeepFryerCircuitboardRecipe #starlight/tp14 port + - WashingMachineCircuitboard # Forky - clothing stains ## Dynamic diff --git a/Resources/Prototypes/_Funkystation/Catalog/Cargo/cargo_service.yml b/Resources/Prototypes/_Funkystation/Catalog/Cargo/cargo_service.yml new file mode 100644 index 000000000000..2e0787468f99 --- /dev/null +++ b/Resources/Prototypes/_Funkystation/Catalog/Cargo/cargo_service.yml @@ -0,0 +1,9 @@ +- type: cargoProduct + id: ServiceWashingMachineKit + icon: + sprite: _Funkystation/Structures/Machines/washing_machine.rsi + state: base + product: CrateServiceWashingMachineSet + cost: 1500 + category: cargoproduct-category-name-service + group: market diff --git a/Resources/Prototypes/_Funkystation/Catalog/Fills/Crates/service.yml b/Resources/Prototypes/_Funkystation/Catalog/Fills/Crates/service.yml new file mode 100644 index 000000000000..93ffb5b753cb --- /dev/null +++ b/Resources/Prototypes/_Funkystation/Catalog/Fills/Crates/service.yml @@ -0,0 +1,13 @@ +- type: entity + id: CrateServiceWashingMachineSet + parent: CratePlastic + name: DIY washing machine kit + description: A Nanotrasen Commercial Model-C washing machine, disassembled and ready for shipping. Contains small parts that may be ingested by infants. + components: + - type: StorageFill + contents: + - id: SheetSteel1 + amount: 6 + - id: CableApcStack10 + - id: MicroManipulatorStockPart + - id: WashingMachineCircuitboard diff --git a/Resources/Prototypes/_Funkystation/Entities/Effects/reagent_fire.yml b/Resources/Prototypes/_Funkystation/Entities/Effects/reagent_fire.yml new file mode 100644 index 000000000000..80b9fa4a2a26 --- /dev/null +++ b/Resources/Prototypes/_Funkystation/Entities/Effects/reagent_fire.yml @@ -0,0 +1,54 @@ +- type: entity + id: ReagentPuddleFireEffect + categories: [ HideSpawnMenu ] + components: + - type: Transform + - type: Sprite + sprite: _Funkystation/Effects/reagentfire.rsi + layers: + - state: "4" + shader: unshaded + visible: true + drawdepth: Overlays + - type: Appearance + - type: ReagentPuddleFireEffect + +- type: particleEffect + id: ReagentFireContinuous + parent: SfFireContinuous + sprite: + sprite: _Starfall/Particles/generic.rsi + state: dot + emissionRate: 35 + maxCount: 150 + speed: 1.2 + speedVariance: 0.6 + gravity: -0.8 + sizeVariance: 0.4 + stretchFactor: 0.15 + noiseStrength: 0.5 + noiseFrequency: 1.8 + spreadAngle: 45 + colorOverLifetime: + - time: 0.0 + color: "#FFFFFFFF" + - time: 0.2 + color: "#FFFFFFFF" + - time: 0.5 + color: "#FFFFFFFF" + - time: 0.8 + color: "#FFFFFFBB" + - time: 1.0 + color: "#FFFFFF00" + +- type: particleEffect + id: ReagentFireSmoke + parent: SfFireSmoke + sprite: + sprite: Effects/chemsmoke.rsi + state: chemsmoke + renderLayer: 1 + gravity: -1.2 + spawnOffset: 0, 0.4 + startColor: "#FFFFFF66" + endColor: "#FFFFFF00" diff --git a/Resources/Prototypes/_Funkystation/Entities/Effects/wall_stains.yml b/Resources/Prototypes/_Funkystation/Entities/Effects/wall_stains.yml new file mode 100644 index 000000000000..a28ac18d49f0 --- /dev/null +++ b/Resources/Prototypes/_Funkystation/Entities/Effects/wall_stains.yml @@ -0,0 +1,193 @@ +- type: entity + id: WallStain + categories: [ HideSpawnMenu ] + components: + - type: Transform + anchored: false + - type: Sprite + visible: false + sprite: _Funkystation/Effects/wallstain.rsi + state: splatter + - type: WallStain + - type: Solution + id: stain + solution: + maxVol: 5 + - type: Forensics + - type: FlammableWallStain + - type: Appearance + +- type: entity + id: WallStainFireEffect + name: fire + description: Oops... + categories: [ HideSpawnMenu ] + components: + - type: Transform + - type: Sprite + sprite: _Funkystation/Effects/reagentfire.rsi + layers: + - state: "4" + shader: unshaded + visible: true + drawdepth: Overlays + - type: Appearance + - type: WallStainFireVisuals + - type: PointLight + radius: 1.5 + energy: 1.5 + color: "#FF5500" + enabled: false + +- type: entity + parent: PuddleSparkle + id: WallStainSparkle + categories: [ HideSpawnMenu ] + components: + - type: Sprite + drawdepth: Overlays + +- type: particleEffect + id: WallFire + sprite: + sprite: _Starfall/Particles/generic.rsi + state: curl + shader: unshaded + startColor: "#FFFFFFFF" + endColor: "#FFFFFF00" + alphaOverLifetime: + - time: 0.0 + value: 0.0 + - time: 0.05 + value: 1.0 + - time: 0.7 + value: 1.0 + - time: 1.0 + value: 0.0 + emissionRate: 25 + maxCount: 150 + particleSize: 0.4 + gravity: -1.0 + drag: 0.5 + noiseStrength: 0.4 + noiseFrequency: 1.5 + spreadAngle: 45 + emitAngle: 0 + shape: + type: Box + boxExtents: 0.4, 0.2 + +- type: particleEffect + id: WallFireEmbers + sprite: + sprite: _Starfall/Particles/generic.rsi + state: dot + shader: unshaded + startColor: "#FFFFFFFF" + endColor: "#FFFFFF00" + alphaOverLifetime: + - time: 0.0 + value: 1.0 + - time: 0.8 + value: 1.0 + - time: 1.0 + value: 0.0 + lifetime: 1.5 + lifetimeVariance: 0.5 + speed: 3.5 + speedVariance: 2.0 + particleSize: 0.05 + sizeVariance: 0.02 + emissionRate: 40 + maxCount: 100 + gravity: -0.5 + drag: 1.0 + noiseStrength: 1.5 + noiseFrequency: 2.0 + spreadAngle: 180 + shape: + type: Box + boxExtents: 0.4, 0.4 + +- type: particleEffect + id: WallFireSlag + sprite: + sprite: _Starfall/Particles/generic.rsi + state: drop + shader: unshaded + startColor: "#FFFFFFFF" + endColor: "#FFFFFF00" + alphaOverLifetime: + - time: 0.0 + value: 1.0 + - time: 0.6 + value: 1.0 + - time: 1.0 + value: 0.0 + lifetime: 0.8 + lifetimeVariance: 0.4 + speed: 4.0 + speedVariance: 2.0 + particleSize: 0.15 + sizeVariance: 0.05 + gravity: 12.0 + drag: 0.2 + spreadAngle: 45 + emitAngle: 180 + alignToVelocity: true + shape: + type: Box + boxExtents: 0.3, 0.1 + +- type: particleEffect + id: WallFireSparks + sprite: + sprite: _Starfall/Particles/generic.rsi + state: curl + shader: unshaded + startColor: "#FFFFFFFF" + endColor: "#FFFFFF00" + alphaOverLifetime: + - time: 0.0 + value: 1.0 + - time: 0.35 + value: 1.0 + - time: 1.0 + value: 0.0 + lifetime: 0.4 + lifetimeVariance: 0.2 + speed: 8.0 + speedVariance: 4.0 + particleSize: 0.25 + sizeVariance: 0.1 + stretchFactor: 0.4 + emissionRate: 25 + maxCount: 60 + gravity: 4.0 + drag: 0.1 + spreadAngle: 120 + emitAngle: 0 + shape: + type: Point + +- type: particleEffect + id: WallFireFumes + sprite: + sprite: Effects/chemsmoke.rsi + state: chemsmoke + startColor: "#FFFFFFFF" + endColor: "#FFFFFF00" + lifetime: 2.5 + lifetimeVariance: 0.5 + speed: 0.6 + speedVariance: 0.2 + particleSize: 0.45 + sizeVariance: 0.15 + emissionRate: 12 + maxCount: 40 + gravity: -0.6 + drag: 0.2 + noiseStrength: 0.3 + noiseFrequency: 0.8 + spreadAngle: 40 + emitAngle: 0 diff --git a/Resources/Prototypes/_Funkystation/Entities/Objects/Devices/Circuitboards/Machine/production.yml b/Resources/Prototypes/_Funkystation/Entities/Objects/Devices/Circuitboards/Machine/production.yml index 1d241e35041c..8f55bd377607 100644 --- a/Resources/Prototypes/_Funkystation/Entities/Objects/Devices/Circuitboards/Machine/production.yml +++ b/Resources/Prototypes/_Funkystation/Entities/Objects/Devices/Circuitboards/Machine/production.yml @@ -13,3 +13,16 @@ Steel: 5 Manipulator: 2 CableHV: 5 + +- type: entity + parent: BaseMachineCircuitboard + id: WashingMachineCircuitboard + name: Nanotrasen Commercial Model-C washing machine board + description: A machine printed circuit board for an industrial-grade washing machine. + components: + - type: MachineBoard + prototype: WashingMachine + stackRequirements: + Steel: 1 + Manipulator: 1 + Cable: 1 diff --git a/Resources/Prototypes/_Funkystation/Entities/Objects/Devices/flatpack.yml b/Resources/Prototypes/_Funkystation/Entities/Objects/Devices/flatpack.yml new file mode 100644 index 000000000000..ff5b6cee6717 --- /dev/null +++ b/Resources/Prototypes/_Funkystation/Entities/Objects/Devices/flatpack.yml @@ -0,0 +1,8 @@ +- type: entity + parent: BaseFlatpack + id: FlatpackWashingMachine + name: Nanotrasen Commercial Model-C washing machine flatpack + description: An industrial-grade washing machine, mechanically compressed into a small flatpack. + components: + - type: Flatpack + entity: WashingMachine diff --git a/Resources/Prototypes/_Funkystation/Entities/Objects/base_solution.yml b/Resources/Prototypes/_Funkystation/Entities/Objects/base_solution.yml new file mode 100644 index 000000000000..30e8b6625cd3 --- /dev/null +++ b/Resources/Prototypes/_Funkystation/Entities/Objects/base_solution.yml @@ -0,0 +1,21 @@ +- type: entity + parent: Solution + id: SolutionStain + categories: [ HideSpawnMenu ] + components: + - type: Solution + id: stain + solution: + maxVol: 5 + canReact: false + +- type: entity + parent: Solution + id: SolutionPrint + categories: [ HideSpawnMenu ] + components: + - type: Solution + id: print + solution: + maxVol: 20 + canReact: false diff --git a/Resources/Prototypes/_Funkystation/Entities/Structures/Machines/washing_machine.yml b/Resources/Prototypes/_Funkystation/Entities/Structures/Machines/washing_machine.yml new file mode 100644 index 000000000000..9e13ffe23884 --- /dev/null +++ b/Resources/Prototypes/_Funkystation/Entities/Structures/Machines/washing_machine.yml @@ -0,0 +1,91 @@ +- type: entity + id: WashingMachine + parent: [ BaseMachinePowered, ConstructibleMachine ] + name: Nanotrasen Commercial Model-C washing machine + description: An industrial-grade washing machine designed to clean even the grimiest of uniforms. Keep hands, hard objects and small crewmembers away from drum during cycle. + placement: + mode: SnapgridCenter + components: + - type: Sprite + sprite: _Funkystation/Structures/Machines/washing_machine.rsi + snapCardinals: true + layers: + - state: base + map: [ "enum.StorageVisualLayers.Base" ] + - state: empty + map: [ "content" ] + - state: door-closed + map: [ "enum.StorageVisualLayers.Door" ] + - state: running + map: [ "washing" ] + visible: false + - type: WashingMachine + washTime: 5 + cooldown: 6 + bluntDamagePerSecond: 6 + selfDamagePerSecond: 6 + waterSprayAmount: 150 + washLoopSound: + path: /Audio/_Funkystation/Machines/washing_loop.ogg + params: + loop: true + washFinishedSound: + path: /Audio/_Funkystation/Machines/washing_open.ogg + - type: EntityStorage + isCollidableWhenOpen: false + open: false + capacity: 4 + - type: Forensics + canDnaBeCleaned: false + - type: PlaceableSurface + isPlaceable: false + - type: Appearance + - type: GenericVisualizer + visuals: + enum.StorageVisuals.Open: + content: + True: { visible: false } + False: { visible: true } + enum.StorageVisuals.HasContents: + content: + True: { state: full } + False: { state: empty } + enum.WashingMachineVisuals.State: + washing: + Idle: { visible: false } + Washing: { visible: true } + Broken: { visible: false } + - type: EntityStorageVisuals + stateBaseClosed: base + stateBaseOpen: base + stateDoorOpen: door-open + stateDoorClosed: door-closed + - type: Machine + board: WashingMachineCircuitboard + - type: ContainerContainer + containers: + machine_board: !type:Container + machine_parts: !type:Container + entity_storage: !type:Container + - type: Construction + graph: Machine + node: machine + containers: [machine_parts, machine_board, entity_storage] + - type: Damageable + damageModifierSet: Metallic + #- type: Injurable # Starlight, don't have this pr yet + damageContainer: Inorganic # Starlight + - type: Destructible + thresholds: + - trigger: + !type:DamageTrigger + damage: 50 + behaviors: + - !type:ExplodeBehavior + - !type:PlaySoundBehavior + sound: + collection: MetalBreak + - !type:ChangeConstructionNodeBehavior + node: machineFrame + - !type:DoActsBehavior + acts: ["Destruction"] diff --git a/Resources/Prototypes/_Funkystation/Recipes/Lathes/machine_boards.yml b/Resources/Prototypes/_Funkystation/Recipes/Lathes/machine_boards.yml index ebf356cdd9f6..cbc133b56441 100644 --- a/Resources/Prototypes/_Funkystation/Recipes/Lathes/machine_boards.yml +++ b/Resources/Prototypes/_Funkystation/Recipes/Lathes/machine_boards.yml @@ -5,3 +5,11 @@ parent: [ BaseCircuitboardRecipe, BaseEngineeringMachineRecipeCategory ] id: ElectrolyzerMachineCircuitboard result: ElectrolyzerMachineCircuitboard + +## Service + +# Washing machine +- type: latheRecipe + parent: BaseCircuitboardRecipe + id: WashingMachineCircuitboard + result: WashingMachineCircuitboard diff --git a/Resources/Prototypes/_Funkystation/tags.yml b/Resources/Prototypes/_Funkystation/tags.yml index 3973e5899385..9bf5997a31f7 100644 --- a/Resources/Prototypes/_Funkystation/tags.yml +++ b/Resources/Prototypes/_Funkystation/tags.yml @@ -16,6 +16,9 @@ - type: Tag id: DefibrillatorCompact +- type: Tag # Funky Wall Stains + id: DirectionalWindow + - type: Tag id: OxygenTank diff --git a/Resources/Prototypes/_Starlight/Entities/Clothing/Shoes/specific.yml b/Resources/Prototypes/_Starlight/Entities/Clothing/Shoes/specific.yml index d65b000e0f2f..9bb995e3641a 100644 --- a/Resources/Prototypes/_Starlight/Entities/Clothing/Shoes/specific.yml +++ b/Resources/Prototypes/_Starlight/Entities/Clothing/Shoes/specific.yml @@ -9,6 +9,7 @@ sprite: Clothing/Shoes/Specific/galoshes.rsi - type: Clothing sprite: Clothing/Shoes/Specific/galoshes.rsi + - type: NoFootprints # Tangible benefit, I guess. - type: StealTarget # Starlight stealGroup: GaloshesCollection # Starlight diff --git a/Resources/Prototypes/_Starlight/Entities/Mobs/Species/base.yml b/Resources/Prototypes/_Starlight/Entities/Mobs/Species/base.yml index 14fd5d2589f2..9d1120189e51 100644 --- a/Resources/Prototypes/_Starlight/Entities/Mobs/Species/base.yml +++ b/Resources/Prototypes/_Starlight/Entities/Mobs/Species/base.yml @@ -252,6 +252,12 @@ # Starlight End - type: CosmicCenserTarget # Stellar - Cosmic Cult - type: Scent # Starlight - smell system + # Moff start - Funky footprints + - type: FootprintOwner + - type: SolutionManager + solutions: + - SolutionPrint + # Moff end - type: entity save: false diff --git a/Resources/Prototypes/_Starlight/Entities/Objects/Devices/flatpack.yml b/Resources/Prototypes/_Starlight/Entities/Objects/Devices/flatpack.yml index c81593e207bf..d24601eaa2cb 100644 --- a/Resources/Prototypes/_Starlight/Entities/Objects/Devices/flatpack.yml +++ b/Resources/Prototypes/_Starlight/Entities/Objects/Devices/flatpack.yml @@ -171,3 +171,12 @@ entity: EndymionStatueLarge1 randomEntities: #Structured to be able to add more options once we have sprites for more - EndymionStatueLarge1 + +- type: entity + parent: BaseFlatpack + id: WashingMachineFlatpack + name: washing machine flatpack + description: A flatpack used for constructing a washing machine. + components: + - type: Flatpack + entity: WashingMachine diff --git a/Resources/Prototypes/_Starlight/Entities/Objects/Misc/fire_extinguisher.yml b/Resources/Prototypes/_Starlight/Entities/Objects/Misc/fire_extinguisher.yml new file mode 100644 index 000000000000..168618713a40 --- /dev/null +++ b/Resources/Prototypes/_Starlight/Entities/Objects/Misc/fire_extinguisher.yml @@ -0,0 +1,56 @@ +- type: entity + parent: [SolutionFireExtinguisherVeryLarge, FireExtinguisher, BaseAtmosContraband] + id: FireExtinguisherAtmos + name: atmos fire extinguisher + description: A bright, pressurized fire extinguisher for the brave warriors of atmosia. Refill with your selection of water and space cleaner at your leisure. + components: + - type: Sprite + sprite: _Starlight/Objects/Misc/fire_extinguisher_atmos.rsi + layers: + - state: fire_extinguisher_closed + map: [ "enum.ToggleableVisuals.Layer" ] + - type: Item + sprite: _Starlight/Objects/Misc/fire_extinguisher_atmos.rsi + size: Large + shape: + - 0,0,1,2 + - type: Clothing + quickEquip: false + sprite: _Starlight/Objects/Misc/fire_extinguisher_atmos.rsi + slots: + - Back + - suitStorage + - type: MeleeWeapon + wideAnimationRotation: 0 + damage: + types: + Blunt: 12 # They can get 2 more damage, in case they need to smash something. It's like a variant fire axe. + Structural: 5 # totals to 17, still 8 less than fireaxe + soundHit: + path: /Audio/Weapons/smash.ogg + - type: Spray + transferAmount: 20 + pushbackAmount: 60 + spraySound: + path: /Audio/Effects/extinguish.ogg + sprayedPrototype: ExtinguisherSpray + vaporAmount: 5 + vaporSpread: 110 + sprayVelocity: 5.0 + - type: RefillableSolution + solution: spray + reagentWhitelist: + - Water + - SpaceCleaner + - SyndicateSpaceCleaner + +- type: entity + parent: [SolutionSpray, SolutionVeryLarge] + id: SolutionFireExtinguisherVeryLarge + categories: [ HideSpawnMenu ] + components: + - type: Solution + solution: + reagents: + - ReagentId: Water + Quantity: 360 diff --git a/Resources/Prototypes/_Starlight/Entities/Objects/base_contraband.yml b/Resources/Prototypes/_Starlight/Entities/Objects/base_contraband.yml index ca6a80834f6e..2b6d9f7079ff 100644 --- a/Resources/Prototypes/_Starlight/Entities/Objects/base_contraband.yml +++ b/Resources/Prototypes/_Starlight/Entities/Objects/base_contraband.yml @@ -22,6 +22,15 @@ - type: Contraband allowedJobs: [ Roboticist ] +# Atmos +- type: entity + id: BaseAtmosContraband + parent: BaseRestrictedContraband + abstract: true + components: + - type: Contraband + allowedJobs: [ AtmosphericTechnician ] + #Advanced Cyberlimbs - type: entity id: BaseAdvancedCyberlimbContraband diff --git a/Resources/Prototypes/_Starlight/Entities/Objects/base_solution.yml b/Resources/Prototypes/_Starlight/Entities/Objects/base_solution.yml new file mode 100644 index 000000000000..e4c3053e452b --- /dev/null +++ b/Resources/Prototypes/_Starlight/Entities/Objects/base_solution.yml @@ -0,0 +1,8 @@ +- type: entity + parent: Solution + id: SolutionVeryLarge + categories: [ HideSpawnMenu ] + components: + - type: Solution + solution: + maxVol: 360 diff --git a/Resources/Textures/_Funkystation/Effects/blood.rsi/armorblood.png b/Resources/Textures/_Funkystation/Effects/blood.rsi/armorblood.png new file mode 100644 index 000000000000..58930714c5fc Binary files /dev/null and b/Resources/Textures/_Funkystation/Effects/blood.rsi/armorblood.png differ diff --git a/Resources/Textures/_Funkystation/Effects/blood.rsi/armorbloodicon.png b/Resources/Textures/_Funkystation/Effects/blood.rsi/armorbloodicon.png new file mode 100644 index 000000000000..4a8ed82478e7 Binary files /dev/null and b/Resources/Textures/_Funkystation/Effects/blood.rsi/armorbloodicon.png differ diff --git a/Resources/Textures/_Funkystation/Effects/blood.rsi/coatblood.png b/Resources/Textures/_Funkystation/Effects/blood.rsi/coatblood.png new file mode 100644 index 000000000000..fd298c159bc0 Binary files /dev/null and b/Resources/Textures/_Funkystation/Effects/blood.rsi/coatblood.png differ diff --git a/Resources/Textures/_Funkystation/Effects/blood.rsi/coatbloodicon.png b/Resources/Textures/_Funkystation/Effects/blood.rsi/coatbloodicon.png new file mode 100644 index 000000000000..57047cff430c Binary files /dev/null and b/Resources/Textures/_Funkystation/Effects/blood.rsi/coatbloodicon.png differ diff --git a/Resources/Textures/_Funkystation/Effects/blood.rsi/gloveblood.png b/Resources/Textures/_Funkystation/Effects/blood.rsi/gloveblood.png new file mode 100644 index 000000000000..b352832729fc Binary files /dev/null and b/Resources/Textures/_Funkystation/Effects/blood.rsi/gloveblood.png differ diff --git a/Resources/Textures/_Funkystation/Effects/blood.rsi/glovebloodicon.png b/Resources/Textures/_Funkystation/Effects/blood.rsi/glovebloodicon.png new file mode 100644 index 000000000000..966aeedeccf4 Binary files /dev/null and b/Resources/Textures/_Funkystation/Effects/blood.rsi/glovebloodicon.png differ diff --git a/Resources/Textures/_Funkystation/Effects/blood.rsi/helmetblood.png b/Resources/Textures/_Funkystation/Effects/blood.rsi/helmetblood.png new file mode 100644 index 000000000000..b02e1d9241fe Binary files /dev/null and b/Resources/Textures/_Funkystation/Effects/blood.rsi/helmetblood.png differ diff --git a/Resources/Textures/_Funkystation/Effects/blood.rsi/helmetbloodicon.png b/Resources/Textures/_Funkystation/Effects/blood.rsi/helmetbloodicon.png new file mode 100644 index 000000000000..db377b9242cc Binary files /dev/null and b/Resources/Textures/_Funkystation/Effects/blood.rsi/helmetbloodicon.png differ diff --git a/Resources/Textures/_Funkystation/Effects/blood.rsi/itemblood.png b/Resources/Textures/_Funkystation/Effects/blood.rsi/itemblood.png new file mode 100644 index 000000000000..8aaa3a04bc83 Binary files /dev/null and b/Resources/Textures/_Funkystation/Effects/blood.rsi/itemblood.png differ diff --git a/Resources/Textures/_Funkystation/Effects/blood.rsi/maskblood.png b/Resources/Textures/_Funkystation/Effects/blood.rsi/maskblood.png new file mode 100644 index 000000000000..998d8a0ef2c9 Binary files /dev/null and b/Resources/Textures/_Funkystation/Effects/blood.rsi/maskblood.png differ diff --git a/Resources/Textures/_Funkystation/Effects/blood.rsi/maskbloodicon.png b/Resources/Textures/_Funkystation/Effects/blood.rsi/maskbloodicon.png new file mode 100644 index 000000000000..3073c2e26f35 Binary files /dev/null and b/Resources/Textures/_Funkystation/Effects/blood.rsi/maskbloodicon.png differ diff --git a/Resources/Textures/_Funkystation/Effects/blood.rsi/meta.json b/Resources/Textures/_Funkystation/Effects/blood.rsi/meta.json new file mode 100644 index 000000000000..d853ad5d57a0 --- /dev/null +++ b/Resources/Textures/_Funkystation/Effects/blood.rsi/meta.json @@ -0,0 +1,74 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from tgstation at https://github.com/tgstation/tgstation/blob/master/icons/effects/blood.dmi and modified by Will-Oliver-Br", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "uniformblood", + "directions": 4 + }, + { + "name": "armorblood", + "directions": 4 + }, + { + "name": "helmetblood", + "directions": 4 + }, + { + "name": "suitblood", + "directions": 4 + }, + { + "name": "maskblood", + "directions": 4 + }, + { + "name": "shoeblood", + "directions": 4 + }, + { + "name": "coatblood", + "directions": 4 + }, + { + "name": "gloveblood", + "directions": 4 + }, + { + "name": "outerclothing", + "directions": 4 + }, + { + "name": "itemblood" + }, + { + "name": "glovebloodicon" + }, + { + "name": "coatbloodicon" + }, + { + "name": "shoebloodicon" + }, + { + "name": "maskbloodicon" + }, + { + "name": "suitbloodicon" + }, + { + "name": "helmetbloodicon" + }, + { + "name": "armorbloodicon" + }, + { + "name": "uniformbloodicon" + } + ] +} diff --git a/Resources/Textures/_Funkystation/Effects/blood.rsi/outerclothing.png b/Resources/Textures/_Funkystation/Effects/blood.rsi/outerclothing.png new file mode 100644 index 000000000000..009ccd95c5de Binary files /dev/null and b/Resources/Textures/_Funkystation/Effects/blood.rsi/outerclothing.png differ diff --git a/Resources/Textures/_Funkystation/Effects/blood.rsi/shoeblood.png b/Resources/Textures/_Funkystation/Effects/blood.rsi/shoeblood.png new file mode 100644 index 000000000000..506c269bf104 Binary files /dev/null and b/Resources/Textures/_Funkystation/Effects/blood.rsi/shoeblood.png differ diff --git a/Resources/Textures/_Funkystation/Effects/blood.rsi/shoebloodicon.png b/Resources/Textures/_Funkystation/Effects/blood.rsi/shoebloodicon.png new file mode 100644 index 000000000000..6b12b49cf83d Binary files /dev/null and b/Resources/Textures/_Funkystation/Effects/blood.rsi/shoebloodicon.png differ diff --git a/Resources/Textures/_Funkystation/Effects/blood.rsi/suitblood.png b/Resources/Textures/_Funkystation/Effects/blood.rsi/suitblood.png new file mode 100644 index 000000000000..0ac5af35c306 Binary files /dev/null and b/Resources/Textures/_Funkystation/Effects/blood.rsi/suitblood.png differ diff --git a/Resources/Textures/_Funkystation/Effects/blood.rsi/suitbloodicon.png b/Resources/Textures/_Funkystation/Effects/blood.rsi/suitbloodicon.png new file mode 100644 index 000000000000..1a88f15cd76a Binary files /dev/null and b/Resources/Textures/_Funkystation/Effects/blood.rsi/suitbloodicon.png differ diff --git a/Resources/Textures/_Funkystation/Effects/blood.rsi/uniformblood.png b/Resources/Textures/_Funkystation/Effects/blood.rsi/uniformblood.png new file mode 100644 index 000000000000..5fef80c9db05 Binary files /dev/null and b/Resources/Textures/_Funkystation/Effects/blood.rsi/uniformblood.png differ diff --git a/Resources/Textures/_Funkystation/Effects/blood.rsi/uniformbloodicon.png b/Resources/Textures/_Funkystation/Effects/blood.rsi/uniformbloodicon.png new file mode 100644 index 000000000000..a7904e6b8361 Binary files /dev/null and b/Resources/Textures/_Funkystation/Effects/blood.rsi/uniformbloodicon.png differ diff --git a/Resources/Textures/_Funkystation/Effects/footprints.rsi/dragging-1.png b/Resources/Textures/_Funkystation/Effects/footprints.rsi/dragging-1.png new file mode 100644 index 000000000000..dbfd645b2a39 Binary files /dev/null and b/Resources/Textures/_Funkystation/Effects/footprints.rsi/dragging-1.png differ diff --git a/Resources/Textures/_Funkystation/Effects/footprints.rsi/dragging-2.png b/Resources/Textures/_Funkystation/Effects/footprints.rsi/dragging-2.png new file mode 100644 index 000000000000..341b3877d29e Binary files /dev/null and b/Resources/Textures/_Funkystation/Effects/footprints.rsi/dragging-2.png differ diff --git a/Resources/Textures/_Funkystation/Effects/footprints.rsi/dragging-3.png b/Resources/Textures/_Funkystation/Effects/footprints.rsi/dragging-3.png new file mode 100644 index 000000000000..f1245abd8893 Binary files /dev/null and b/Resources/Textures/_Funkystation/Effects/footprints.rsi/dragging-3.png differ diff --git a/Resources/Textures/_Funkystation/Effects/footprints.rsi/dragging-4.png b/Resources/Textures/_Funkystation/Effects/footprints.rsi/dragging-4.png new file mode 100644 index 000000000000..741b27c9a5b0 Binary files /dev/null and b/Resources/Textures/_Funkystation/Effects/footprints.rsi/dragging-4.png differ diff --git a/Resources/Textures/_Funkystation/Effects/footprints.rsi/dragging-5.png b/Resources/Textures/_Funkystation/Effects/footprints.rsi/dragging-5.png new file mode 100644 index 000000000000..1cf15bdf45ed Binary files /dev/null and b/Resources/Textures/_Funkystation/Effects/footprints.rsi/dragging-5.png differ diff --git a/Resources/Textures/_Funkystation/Effects/footprints.rsi/foot.png b/Resources/Textures/_Funkystation/Effects/footprints.rsi/foot.png new file mode 100644 index 000000000000..80051d9bd47b Binary files /dev/null and b/Resources/Textures/_Funkystation/Effects/footprints.rsi/foot.png differ diff --git a/Resources/Textures/_Funkystation/Effects/footprints.rsi/meta.json b/Resources/Textures/_Funkystation/Effects/footprints.rsi/meta.json new file mode 100644 index 000000000000..c7691043d393 --- /dev/null +++ b/Resources/Textures/_Funkystation/Effects/footprints.rsi/meta.json @@ -0,0 +1,29 @@ +{ + "version": 1, + "size": { + "x": 32, + "y": 32 + }, + "license": "CC-BY-4.0", + "copyright": "foot.png created by evgen99 (673243657935257617). IMPERIAL SPACE (all besides following), dotCatshark (Dragging-2 & -5), Ratwood keep (https://github.com/Rotwood-Vale/Ratwood-Keep) (Dragging-1 & -3), (Goonstation https://github.com/goonstation/goonstation/) (Dragging-4).", + "states": [ + { + "name": "foot" + }, + { + "name": "dragging-1" + }, + { + "name": "dragging-2" + }, + { + "name": "dragging-3" + }, + { + "name": "dragging-4" + }, + { + "name": "dragging-5" + } + ] +} diff --git a/Resources/Textures/_Funkystation/Effects/reagentfire.rsi/4.png b/Resources/Textures/_Funkystation/Effects/reagentfire.rsi/4.png new file mode 100644 index 000000000000..eb1a2dc7f093 Binary files /dev/null and b/Resources/Textures/_Funkystation/Effects/reagentfire.rsi/4.png differ diff --git a/Resources/Textures/_Funkystation/Effects/reagentfire.rsi/5.png b/Resources/Textures/_Funkystation/Effects/reagentfire.rsi/5.png new file mode 100644 index 000000000000..42a90e078375 Binary files /dev/null and b/Resources/Textures/_Funkystation/Effects/reagentfire.rsi/5.png differ diff --git a/Resources/Textures/_Funkystation/Effects/reagentfire.rsi/6.png b/Resources/Textures/_Funkystation/Effects/reagentfire.rsi/6.png new file mode 100644 index 000000000000..f3fa943e53e8 Binary files /dev/null and b/Resources/Textures/_Funkystation/Effects/reagentfire.rsi/6.png differ diff --git a/Resources/Textures/_Funkystation/Effects/reagentfire.rsi/meta.json b/Resources/Textures/_Funkystation/Effects/reagentfire.rsi/meta.json new file mode 100644 index 000000000000..e12ffcef223d --- /dev/null +++ b/Resources/Textures/_Funkystation/Effects/reagentfire.rsi/meta.json @@ -0,0 +1,44 @@ +{ + "version": 1, + "size": { + "x": 32, + "y": 32 + }, + "license": "CC-BY-SA-3.0", + "copyright": "sprites 4, 5, and 6 taken from cmss13 at https://github.com/cmss13-devs/cmss13/blob/ce01adda18ccca3bb0105615391861567c766330/icons/effects/fire.dmi", + "states": [ + { + "name": "4", + "delays": [ + [ + 0.1, + 0.2, + 0.1, + 0.2 + ] + ] + }, + { + "name": "5", + "delays": [ + [ + 0.1, + 0.2, + 0.1, + 0.2 + ] + ] + }, + { + "name": "6", + "delays": [ + [ + 0.1, + 0.2, + 0.1, + 0.2 + ] + ] + } + ] +} diff --git a/Resources/Textures/_Funkystation/Effects/wallstain.rsi/meta.json b/Resources/Textures/_Funkystation/Effects/wallstain.rsi/meta.json new file mode 100644 index 000000000000..e1706d37c222 --- /dev/null +++ b/Resources/Textures/_Funkystation/Effects/wallstain.rsi/meta.json @@ -0,0 +1,15 @@ +{ + "version": 1, + "size": { + "x": 32, + "y": 32 + }, + "license": "CC-BY-SA-3.0", + "copyright": "From https://github.com/tgstation/tgstation/blob/master/icons/effects/crayondecal.dmi at c6803492ab2a5e523aae7b9b1a9e847ba155f1cf", + "states": [ + { + "name": "splatter" + + } + ] +} diff --git a/Resources/Textures/_Funkystation/Effects/wallstain.rsi/splatter.png b/Resources/Textures/_Funkystation/Effects/wallstain.rsi/splatter.png new file mode 100644 index 000000000000..ac54cad8b07b Binary files /dev/null and b/Resources/Textures/_Funkystation/Effects/wallstain.rsi/splatter.png differ diff --git a/Resources/Textures/_Funkystation/Structures/Machines/washing_machine.rsi/base.png b/Resources/Textures/_Funkystation/Structures/Machines/washing_machine.rsi/base.png new file mode 100644 index 000000000000..83f0cc5d4b0f Binary files /dev/null and b/Resources/Textures/_Funkystation/Structures/Machines/washing_machine.rsi/base.png differ diff --git a/Resources/Textures/_Funkystation/Structures/Machines/washing_machine.rsi/door-closed.png b/Resources/Textures/_Funkystation/Structures/Machines/washing_machine.rsi/door-closed.png new file mode 100644 index 000000000000..595b7e7539cf Binary files /dev/null and b/Resources/Textures/_Funkystation/Structures/Machines/washing_machine.rsi/door-closed.png differ diff --git a/Resources/Textures/_Funkystation/Structures/Machines/washing_machine.rsi/door-open.png b/Resources/Textures/_Funkystation/Structures/Machines/washing_machine.rsi/door-open.png new file mode 100644 index 000000000000..469b77a37060 Binary files /dev/null and b/Resources/Textures/_Funkystation/Structures/Machines/washing_machine.rsi/door-open.png differ diff --git a/Resources/Textures/_Funkystation/Structures/Machines/washing_machine.rsi/empty.png b/Resources/Textures/_Funkystation/Structures/Machines/washing_machine.rsi/empty.png new file mode 100644 index 000000000000..89acf4fe29e7 Binary files /dev/null and b/Resources/Textures/_Funkystation/Structures/Machines/washing_machine.rsi/empty.png differ diff --git a/Resources/Textures/_Funkystation/Structures/Machines/washing_machine.rsi/full.png b/Resources/Textures/_Funkystation/Structures/Machines/washing_machine.rsi/full.png new file mode 100644 index 000000000000..08ca5ef74d4d Binary files /dev/null and b/Resources/Textures/_Funkystation/Structures/Machines/washing_machine.rsi/full.png differ diff --git a/Resources/Textures/_Funkystation/Structures/Machines/washing_machine.rsi/meta.json b/Resources/Textures/_Funkystation/Structures/Machines/washing_machine.rsi/meta.json new file mode 100644 index 000000000000..a10b246cea48 --- /dev/null +++ b/Resources/Textures/_Funkystation/Structures/Machines/washing_machine.rsi/meta.json @@ -0,0 +1,43 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "By AraiMaia for Funky Station", + "size": + { + "x": 32, + "y": 32 + }, + "states": + [{ + "name": "base" + }, + { + "name": "empty" + }, + { + "name": "door-open" + }, + { + "name": "door-closed" + }, + { + "name": "full" + }, + { + "name": "panel" + }, + { + "name": "running", + "delays": + [ + [ + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1 + ] + ] + }] +} diff --git a/Resources/Textures/_Funkystation/Structures/Machines/washing_machine.rsi/panel.png b/Resources/Textures/_Funkystation/Structures/Machines/washing_machine.rsi/panel.png new file mode 100644 index 000000000000..265f90bb5bca Binary files /dev/null and b/Resources/Textures/_Funkystation/Structures/Machines/washing_machine.rsi/panel.png differ diff --git a/Resources/Textures/_Funkystation/Structures/Machines/washing_machine.rsi/running.png b/Resources/Textures/_Funkystation/Structures/Machines/washing_machine.rsi/running.png new file mode 100644 index 000000000000..67300706b3bb Binary files /dev/null and b/Resources/Textures/_Funkystation/Structures/Machines/washing_machine.rsi/running.png differ diff --git a/Resources/Textures/_Starlight/Objects/Misc/fire_extinguisher_atmos.rsi/equipped-BACKPACK.png b/Resources/Textures/_Starlight/Objects/Misc/fire_extinguisher_atmos.rsi/equipped-BACKPACK.png new file mode 100644 index 000000000000..f591cc4adc31 Binary files /dev/null and b/Resources/Textures/_Starlight/Objects/Misc/fire_extinguisher_atmos.rsi/equipped-BACKPACK.png differ diff --git a/Resources/Textures/_Starlight/Objects/Misc/fire_extinguisher_atmos.rsi/equipped-SUITSTORAGE-cat.png b/Resources/Textures/_Starlight/Objects/Misc/fire_extinguisher_atmos.rsi/equipped-SUITSTORAGE-cat.png new file mode 100644 index 000000000000..8901d332beaa Binary files /dev/null and b/Resources/Textures/_Starlight/Objects/Misc/fire_extinguisher_atmos.rsi/equipped-SUITSTORAGE-cat.png differ diff --git a/Resources/Textures/_Starlight/Objects/Misc/fire_extinguisher_atmos.rsi/equipped-SUITSTORAGE-dog.png b/Resources/Textures/_Starlight/Objects/Misc/fire_extinguisher_atmos.rsi/equipped-SUITSTORAGE-dog.png new file mode 100644 index 000000000000..b1a01a1eb97d Binary files /dev/null and b/Resources/Textures/_Starlight/Objects/Misc/fire_extinguisher_atmos.rsi/equipped-SUITSTORAGE-dog.png differ diff --git a/Resources/Textures/_Starlight/Objects/Misc/fire_extinguisher_atmos.rsi/equipped-SUITSTORAGE-fox.png b/Resources/Textures/_Starlight/Objects/Misc/fire_extinguisher_atmos.rsi/equipped-SUITSTORAGE-fox.png new file mode 100644 index 000000000000..e7ae79dd5e5f Binary files /dev/null and b/Resources/Textures/_Starlight/Objects/Misc/fire_extinguisher_atmos.rsi/equipped-SUITSTORAGE-fox.png differ diff --git a/Resources/Textures/_Starlight/Objects/Misc/fire_extinguisher_atmos.rsi/equipped-SUITSTORAGE-hamster.png b/Resources/Textures/_Starlight/Objects/Misc/fire_extinguisher_atmos.rsi/equipped-SUITSTORAGE-hamster.png new file mode 100644 index 000000000000..60b09baa674d Binary files /dev/null and b/Resources/Textures/_Starlight/Objects/Misc/fire_extinguisher_atmos.rsi/equipped-SUITSTORAGE-hamster.png differ diff --git a/Resources/Textures/_Starlight/Objects/Misc/fire_extinguisher_atmos.rsi/equipped-SUITSTORAGE-kangaroo.png b/Resources/Textures/_Starlight/Objects/Misc/fire_extinguisher_atmos.rsi/equipped-SUITSTORAGE-kangaroo.png new file mode 100644 index 000000000000..07c6658d88f9 Binary files /dev/null and b/Resources/Textures/_Starlight/Objects/Misc/fire_extinguisher_atmos.rsi/equipped-SUITSTORAGE-kangaroo.png differ diff --git a/Resources/Textures/_Starlight/Objects/Misc/fire_extinguisher_atmos.rsi/equipped-SUITSTORAGE-pig.png b/Resources/Textures/_Starlight/Objects/Misc/fire_extinguisher_atmos.rsi/equipped-SUITSTORAGE-pig.png new file mode 100644 index 000000000000..b73c6a30bab4 Binary files /dev/null and b/Resources/Textures/_Starlight/Objects/Misc/fire_extinguisher_atmos.rsi/equipped-SUITSTORAGE-pig.png differ diff --git a/Resources/Textures/_Starlight/Objects/Misc/fire_extinguisher_atmos.rsi/equipped-SUITSTORAGE-possum.png b/Resources/Textures/_Starlight/Objects/Misc/fire_extinguisher_atmos.rsi/equipped-SUITSTORAGE-possum.png new file mode 100644 index 000000000000..5cb482a6fa06 Binary files /dev/null and b/Resources/Textures/_Starlight/Objects/Misc/fire_extinguisher_atmos.rsi/equipped-SUITSTORAGE-possum.png differ diff --git a/Resources/Textures/_Starlight/Objects/Misc/fire_extinguisher_atmos.rsi/equipped-SUITSTORAGE-puppy.png b/Resources/Textures/_Starlight/Objects/Misc/fire_extinguisher_atmos.rsi/equipped-SUITSTORAGE-puppy.png new file mode 100644 index 000000000000..7c6f884e4e01 Binary files /dev/null and b/Resources/Textures/_Starlight/Objects/Misc/fire_extinguisher_atmos.rsi/equipped-SUITSTORAGE-puppy.png differ diff --git a/Resources/Textures/_Starlight/Objects/Misc/fire_extinguisher_atmos.rsi/equipped-SUITSTORAGE-sloth.png b/Resources/Textures/_Starlight/Objects/Misc/fire_extinguisher_atmos.rsi/equipped-SUITSTORAGE-sloth.png new file mode 100644 index 000000000000..2c5a9fffc021 Binary files /dev/null and b/Resources/Textures/_Starlight/Objects/Misc/fire_extinguisher_atmos.rsi/equipped-SUITSTORAGE-sloth.png differ diff --git a/Resources/Textures/_Starlight/Objects/Misc/fire_extinguisher_atmos.rsi/equipped-SUITSTORAGE.png b/Resources/Textures/_Starlight/Objects/Misc/fire_extinguisher_atmos.rsi/equipped-SUITSTORAGE.png new file mode 100644 index 000000000000..f591cc4adc31 Binary files /dev/null and b/Resources/Textures/_Starlight/Objects/Misc/fire_extinguisher_atmos.rsi/equipped-SUITSTORAGE.png differ diff --git a/Resources/Textures/_Starlight/Objects/Misc/fire_extinguisher_atmos.rsi/fire_extinguisher_closed.png b/Resources/Textures/_Starlight/Objects/Misc/fire_extinguisher_atmos.rsi/fire_extinguisher_closed.png new file mode 100644 index 000000000000..79d6f6204da6 Binary files /dev/null and b/Resources/Textures/_Starlight/Objects/Misc/fire_extinguisher_atmos.rsi/fire_extinguisher_closed.png differ diff --git a/Resources/Textures/_Starlight/Objects/Misc/fire_extinguisher_atmos.rsi/fire_extinguisher_open.png b/Resources/Textures/_Starlight/Objects/Misc/fire_extinguisher_atmos.rsi/fire_extinguisher_open.png new file mode 100644 index 000000000000..4ea21cbc1009 Binary files /dev/null and b/Resources/Textures/_Starlight/Objects/Misc/fire_extinguisher_atmos.rsi/fire_extinguisher_open.png differ diff --git a/Resources/Textures/_Starlight/Objects/Misc/fire_extinguisher_atmos.rsi/inhand-left.png b/Resources/Textures/_Starlight/Objects/Misc/fire_extinguisher_atmos.rsi/inhand-left.png new file mode 100644 index 000000000000..9e48d41c8390 Binary files /dev/null and b/Resources/Textures/_Starlight/Objects/Misc/fire_extinguisher_atmos.rsi/inhand-left.png differ diff --git a/Resources/Textures/_Starlight/Objects/Misc/fire_extinguisher_atmos.rsi/inhand-right.png b/Resources/Textures/_Starlight/Objects/Misc/fire_extinguisher_atmos.rsi/inhand-right.png new file mode 100644 index 000000000000..071150623289 Binary files /dev/null and b/Resources/Textures/_Starlight/Objects/Misc/fire_extinguisher_atmos.rsi/inhand-right.png differ diff --git a/Resources/Textures/_Starlight/Objects/Misc/fire_extinguisher_atmos.rsi/meta.json b/Resources/Textures/_Starlight/Objects/Misc/fire_extinguisher_atmos.rsi/meta.json new file mode 100644 index 000000000000..85196d93e464 --- /dev/null +++ b/Resources/Textures/_Starlight/Objects/Misc/fire_extinguisher_atmos.rsi/meta.json @@ -0,0 +1,93 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "State based copyright", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "fire_extinguisher_open", + "copyright": "TG Station at commit 9bebd81ae0b0a7f952b59886a765c681205de31f, modified into atmos by wonderfulnewworld (GitHub)" + }, + { + "name": "fire_extinguisher_closed", + "copyright": "TG Station, wonderfulnewworld" + }, + { + "name": "inhand-right", + "directions": 4, + "copyright": "TG Station, wonderfulnewworld" + }, + { + "name": "inhand-left", + "directions": 4, + "copyright": "TG Station, wonderfulnewworld" + }, + { + "name": "equipped-BACKPACK", + "directions": 4, + "copyright": "Ubaser, wonderfulnewworld" + }, + { + "name": "equipped-SUITSTORAGE", + "directions": 4, + "copyright": "Ubaser, wonderfulnewworld" + }, + { + "name": "equipped-SUITSTORAGE-dog", + "directions": 4, + "delays": [[1],[1],[1],[1]], + "copyright": "Ubaser, wonderfulnewworld" + }, + { + "name": "equipped-SUITSTORAGE-puppy", + "directions": 4, + "delays": [[1],[1],[1],[1]], + "copyright": "Ubaser, wonderfulnewworld" + }, + { + "name": "equipped-SUITSTORAGE-fox", + "directions": 4, + "delays": [[1],[1],[1],[1]], + "copyright": "Ubaser, wonderfulnewworld" + }, + { + "name": "equipped-SUITSTORAGE-cat", + "directions": 4, + "delays": [[1],[1],[1],[1]], + "copyright": "Ubaser, wonderfulnewworld" + }, + { + "name": "equipped-SUITSTORAGE-sloth", + "directions": 4, + "delays": [[1],[1],[1],[1]], + "copyright": "Ubaser, wonderfulnewworld" + }, + { + "name": "equipped-SUITSTORAGE-hamster", + "directions": 4, + "delays": [[1],[1],[1],[1]], + "copyright": "Ubaser, wonderfulnewworld" + }, + { + "name": "equipped-SUITSTORAGE-kangaroo", + "directions": 4, + "delays": [[1],[1],[1],[1]], + "copyright": "Ubaser, wonderfulnewworld" + }, + { + "name": "equipped-SUITSTORAGE-possum", + "directions": 4, + "delays": [[1],[1],[1],[1]], + "copyright": "Ubaser, wonderfulnewworld" + }, + { + "name": "equipped-SUITSTORAGE-pig", + "directions": 4, + "delays": [[1],[1],[1],[1]], + "copyright": "Ubaser, wonderfulnewworld" + } + ] +}