diff --git a/Content.Server/Light/Components/MatchboxComponent.cs b/Content.Server/Light/Components/MatchboxComponent.cs
deleted file mode 100644
index 12cd4e3880..0000000000
--- a/Content.Server/Light/Components/MatchboxComponent.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-namespace Content.Server.Light.Components
-{
- // TODO make changes in icons when different threshold reached
- // e.g. different icons for 10% 50% 100%
- [RegisterComponent]
- public sealed partial class MatchboxComponent : Component
- {
- }
-}
diff --git a/Content.Server/Light/Components/MatchstickComponent.cs b/Content.Server/Light/Components/MatchstickComponent.cs
deleted file mode 100644
index 3c47f4c18b..0000000000
--- a/Content.Server/Light/Components/MatchstickComponent.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-using Content.Server.Light.EntitySystems;
-using Content.Shared.Smoking;
-using Robust.Shared.Audio;
-
-namespace Content.Server.Light.Components
-{
- [RegisterComponent]
- [Access(typeof(MatchstickSystem))]
- public sealed partial class MatchstickComponent : Component
- {
- ///
- /// Current state to matchstick. Can be Unlit, Lit or Burnt.
- ///
- [DataField("state")]
- public SmokableState CurrentState = SmokableState.Unlit;
-
- ///
- /// How long will matchstick last in seconds.
- ///
- [ViewVariables(VVAccess.ReadOnly)]
- [DataField("duration")]
- public int Duration = 10;
-
- ///
- /// Sound played when you ignite the matchstick.
- ///
- [DataField("igniteSound", required: true)] public SoundSpecifier IgniteSound = default!;
- }
-}
diff --git a/Content.Server/Light/EntitySystems/MatchboxSystem.cs b/Content.Server/Light/EntitySystems/MatchboxSystem.cs
deleted file mode 100644
index 9a73e44f87..0000000000
--- a/Content.Server/Light/EntitySystems/MatchboxSystem.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-using Content.Server.Light.Components;
-using Content.Server.Storage.EntitySystems;
-using Content.Shared.Interaction;
-using Content.Shared.Smoking;
-
-namespace Content.Server.Light.EntitySystems
-{
- public sealed class MatchboxSystem : EntitySystem
- {
- [Dependency] private readonly MatchstickSystem _stickSystem = default!;
-
- public override void Initialize()
- {
- base.Initialize();
- SubscribeLocalEvent(OnInteractUsing, before: new[] { typeof(StorageSystem) });
- }
-
- private void OnInteractUsing(EntityUid uid, MatchboxComponent component, InteractUsingEvent args)
- {
- if (!args.Handled
- && EntityManager.TryGetComponent(args.Used, out MatchstickComponent? matchstick)
- && matchstick.CurrentState == SmokableState.Unlit)
- {
- _stickSystem.Ignite((args.Used, matchstick), args.User);
- args.Handled = true;
- }
- }
- }
-}
diff --git a/Content.Server/Light/EntitySystems/MatchstickSystem.cs b/Content.Server/Light/EntitySystems/MatchstickSystem.cs
deleted file mode 100644
index 96e4695784..0000000000
--- a/Content.Server/Light/EntitySystems/MatchstickSystem.cs
+++ /dev/null
@@ -1,124 +0,0 @@
-using Content.Server.Atmos.EntitySystems;
-using Content.Server.Light.Components;
-using Content.Shared.Audio;
-using Content.Shared.Interaction;
-using Content.Shared.Item;
-using Content.Shared.Smoking;
-using Content.Shared.Temperature;
-using Robust.Server.GameObjects;
-using Robust.Shared.Audio;
-using Robust.Shared.Audio.Systems;
-using Robust.Shared.Player;
-
-namespace Content.Server.Light.EntitySystems
-{
- public sealed class MatchstickSystem : EntitySystem
- {
- [Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
- [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
- [Dependency] private readonly SharedAudioSystem _audio = default!;
- [Dependency] private readonly SharedItemSystem _item = default!;
- [Dependency] private readonly SharedPointLightSystem _lights = default!;
- [Dependency] private readonly TransformSystem _transformSystem = default!;
-
- private readonly HashSet> _litMatches = new();
-
- public override void Initialize()
- {
- base.Initialize();
- SubscribeLocalEvent(OnInteractUsing);
- SubscribeLocalEvent(OnIsHotEvent);
- SubscribeLocalEvent(OnShutdown);
- }
-
- private void OnShutdown(Entity ent, ref ComponentShutdown args)
- {
- _litMatches.Remove(ent);
- }
-
- public override void Update(float frameTime)
- {
- base.Update(frameTime);
-
- foreach (var match in _litMatches)
- {
- if (match.Comp.CurrentState != SmokableState.Lit || Paused(match) || match.Comp.Deleted)
- continue;
-
- var xform = Transform(match);
-
- if (xform.GridUid is not {} gridUid)
- return;
-
- var position = _transformSystem.GetGridOrMapTilePosition(match, xform);
-
- _atmosphereSystem.HotspotExpose(gridUid, position, 400, 50, match, true);
- }
- }
-
- private void OnInteractUsing(Entity ent, ref InteractUsingEvent args)
- {
- if (args.Handled || ent.Comp.CurrentState != SmokableState.Unlit)
- return;
-
- var isHotEvent = new IsHotEvent();
- RaiseLocalEvent(args.Used, isHotEvent);
-
- if (!isHotEvent.IsHot)
- return;
-
- Ignite(ent, args.User);
- args.Handled = true;
- }
-
- private void OnIsHotEvent(EntityUid uid, MatchstickComponent component, IsHotEvent args)
- {
- args.IsHot = component.CurrentState == SmokableState.Lit;
- }
-
- public void Ignite(Entity matchstick, EntityUid user)
- {
- var component = matchstick.Comp;
-
- // Play Sound
- _audio.PlayPvs(component.IgniteSound, matchstick, AudioParams.Default.WithVariation(0.125f).WithVolume(-0.125f));
-
- // Change state
- SetState(matchstick, component, SmokableState.Lit);
- _litMatches.Add(matchstick);
- matchstick.Owner.SpawnTimer(component.Duration * 1000, delegate
- {
- SetState(matchstick, component, SmokableState.Burnt);
- _litMatches.Remove(matchstick);
- });
- }
-
- private void SetState(EntityUid uid, MatchstickComponent component, SmokableState value)
- {
- component.CurrentState = value;
-
- if (_lights.TryGetLight(uid, out var pointLightComponent))
- {
- _lights.SetEnabled(uid, component.CurrentState == SmokableState.Lit, pointLightComponent);
- }
-
- if (EntityManager.TryGetComponent(uid, out ItemComponent? item))
- {
- switch (component.CurrentState)
- {
- case SmokableState.Lit:
- _item.SetHeldPrefix(uid, "lit", component: item);
- break;
- default:
- _item.SetHeldPrefix(uid, "unlit", component: item);
- break;
- }
- }
-
- if (EntityManager.TryGetComponent(uid, out AppearanceComponent? appearance))
- {
- _appearance.SetData(uid, SmokingVisuals.Smoking, component.CurrentState, appearance);
- }
- }
- }
-}
diff --git a/Content.Shared/IgnitionSource/Components/MatchboxComponent.cs b/Content.Shared/IgnitionSource/Components/MatchboxComponent.cs
new file mode 100644
index 0000000000..dda0ca131f
--- /dev/null
+++ b/Content.Shared/IgnitionSource/Components/MatchboxComponent.cs
@@ -0,0 +1,10 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.IgnitionSource.Components;
+
+///
+/// Component for entities that light matches when they interact. (E.g. striking the match on the matchbox)
+///
+[RegisterComponent, NetworkedComponent]
+public sealed partial class MatchboxComponent : Component;
+
diff --git a/Content.Shared/IgnitionSource/Components/MatchstickComponent.cs b/Content.Shared/IgnitionSource/Components/MatchstickComponent.cs
new file mode 100644
index 0000000000..d1bbae42d9
--- /dev/null
+++ b/Content.Shared/IgnitionSource/Components/MatchstickComponent.cs
@@ -0,0 +1,34 @@
+using Content.Shared.Smoking;
+using Robust.Shared.Audio;
+using Robust.Shared.GameStates;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
+
+namespace Content.Shared.IgnitionSource.Components;
+
+[NetworkedComponent, RegisterComponent, AutoGenerateComponentState, AutoGenerateComponentPause]
+public sealed partial class MatchstickComponent : Component
+{
+ ///
+ /// Current state to matchstick. Can be Unlit, Lit or Burnt.
+ ///
+ [DataField, AutoNetworkedField]
+ public SmokableState State = SmokableState.Unlit;
+
+ ///
+ /// How long the matchstick will burn for.
+ ///
+ [DataField, AutoNetworkedField]
+ public TimeSpan Duration = TimeSpan.FromSeconds(10);
+
+ ///
+ /// The time that the match will burn out. If null, that means the match is unlit.
+ ///
+ [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoNetworkedField, AutoPausedField]
+ public TimeSpan? TimeMatchWillBurnOut;
+
+ ///
+ /// Sound played when you ignite the matchstick.
+ ///
+ [DataField]
+ public SoundSpecifier? IgniteSound;
+}
diff --git a/Content.Shared/IgnitionSource/EntitySystems/MatchboxSystem.cs b/Content.Shared/IgnitionSource/EntitySystems/MatchboxSystem.cs
new file mode 100644
index 0000000000..806e1e9eb1
--- /dev/null
+++ b/Content.Shared/IgnitionSource/EntitySystems/MatchboxSystem.cs
@@ -0,0 +1,25 @@
+using Content.Shared.Storage.EntitySystems;
+using Content.Shared.Interaction;
+using Content.Shared.IgnitionSource.Components;
+
+namespace Content.Shared.IgnitionSource.EntitySystems;
+
+public sealed class MatchboxSystem : EntitySystem
+{
+ [Dependency] private readonly MatchstickSystem _match = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnInteractUsing, before: [ typeof(SharedStorageSystem) ]);
+ }
+
+ private void OnInteractUsing(Entity ent, ref InteractUsingEvent args)
+ {
+ if (args.Handled || !TryComp(args.Used, out var matchstick))
+ return;
+
+ args.Handled = _match.TryIgnite((args.Used, matchstick), args.User);
+ }
+}
diff --git a/Content.Shared/IgnitionSource/EntitySystems/MatchstickSystem.cs b/Content.Shared/IgnitionSource/EntitySystems/MatchstickSystem.cs
new file mode 100644
index 0000000000..1267af307d
--- /dev/null
+++ b/Content.Shared/IgnitionSource/EntitySystems/MatchstickSystem.cs
@@ -0,0 +1,102 @@
+using Content.Shared.Interaction;
+using Content.Shared.Item;
+using Content.Shared.Smoking;
+using Content.Shared.Temperature;
+using Robust.Shared.Audio.Systems;
+using Content.Shared.IgnitionSource.Components;
+using Robust.Shared.Timing;
+
+namespace Content.Shared.IgnitionSource.EntitySystems;
+
+public sealed partial class MatchstickSystem : EntitySystem
+{
+ [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
+ [Dependency] private readonly SharedAudioSystem _audio = default!;
+ [Dependency] private readonly SharedItemSystem _item = default!;
+ [Dependency] private readonly SharedPointLightSystem _lights = default!;
+ [Dependency] private readonly IGameTiming _timing = default!;
+ [Dependency] private readonly SharedIgnitionSourceSystem _ignition = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(OnInteractUsing);
+ }
+
+ // This is for something *else* lighting the matchstick, not the matchstick lighting something else.
+ private void OnInteractUsing(Entity ent, ref InteractUsingEvent args)
+ {
+ if (args.Handled)
+ return;
+
+ var isHotEvent = new IsHotEvent();
+ RaiseLocalEvent(args.Used, isHotEvent);
+
+ if (!isHotEvent.IsHot)
+ return;
+
+ args.Handled = TryIgnite(ent, args.User);
+ }
+
+ ///
+ /// Try to light a matchstick!
+ ///
+ /// The matchstick to light.
+ /// The user lighting the matchstick can be null if there isn't any user.
+ /// True if the matchstick was lit, false otherwise.
+ public bool TryIgnite(Entity matchstick, EntityUid? user)
+ {
+ if (matchstick.Comp.State != SmokableState.Unlit)
+ return false;
+
+ // Play Sound
+ _audio.PlayPredicted(matchstick.Comp.IgniteSound, matchstick, user);
+
+ // Change state
+ SetState(matchstick, SmokableState.Lit);
+ matchstick.Comp.TimeMatchWillBurnOut = _timing.CurTime + matchstick.Comp.Duration;
+
+ Dirty(matchstick);
+
+ return true;
+ }
+
+ private void SetState(Entity ent, SmokableState newState)
+ {
+ _lights.SetEnabled(ent, newState == SmokableState.Lit);
+
+ _appearance.SetData(ent, SmokingVisuals.Smoking, newState);
+
+ _ignition.SetIgnited(ent.Owner, newState == SmokableState.Lit);
+
+ switch (newState)
+ {
+ case SmokableState.Lit:
+ _item.SetHeldPrefix(ent, "lit");
+ break;
+ default:
+ _item.SetHeldPrefix(ent, "unlit");
+ break;
+ }
+
+ ent.Comp.State = newState;
+ Dirty(ent);
+ }
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ var query = EntityQueryEnumerator();
+
+ while (query.MoveNext(out var uid, out var match))
+ {
+ if (match.State != SmokableState.Lit)
+ continue;
+
+ // Check if the match has expired.
+ if (_timing.CurTime > match.TimeMatchWillBurnOut)
+ SetState((uid, match), SmokableState.Burnt);
+ }
+ }
+}
diff --git a/Resources/Prototypes/Entities/Objects/Tools/matches.yml b/Resources/Prototypes/Entities/Objects/Tools/matches.yml
index e8601fcf35..ee5100c999 100644
--- a/Resources/Prototypes/Entities/Objects/Tools/matches.yml
+++ b/Resources/Prototypes/Entities/Objects/Tools/matches.yml
@@ -32,6 +32,12 @@
duration: 10
igniteSound:
path: /Audio/Items/match_strike.ogg
+ params:
+ volume: -0.125
+ variation: 0.125
+ - type: IgnitionSource
+ ignited: false
+ temperature: 400.0
- type: PointLight
enabled: false
radius: 1.1