--- /dev/null
+using Content.Shared.Clock;
+using Robust.Client.GameObjects;
+
+namespace Content.Client.Clock;
+
+public sealed class ClockSystem : SharedClockSystem
+{
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ var query = EntityQueryEnumerator<ClockComponent, SpriteComponent>();
+ while (query.MoveNext(out var uid, out var comp, out var sprite))
+ {
+ if (!sprite.LayerMapTryGet(ClockVisualLayers.HourHand, out var hourLayer) ||
+ !sprite.LayerMapTryGet(ClockVisualLayers.MinuteHand, out var minuteLayer))
+ continue;
+
+ var time = GetClockTime((uid, comp));
+ var hourState = $"{comp.HoursBase}{time.Hours % 12}";
+ var minuteState = $"{comp.MinutesBase}{time.Minutes / 5}";
+ sprite.LayerSetState(hourLayer, hourState);
+ sprite.LayerSetState(minuteLayer, minuteState);
+ }
+ }
+}
--- /dev/null
+using Content.Server.GameTicking.Events;
+using Content.Shared.Clock;
+using Content.Shared.Destructible;
+using Robust.Server.GameStates;
+using Robust.Shared.Random;
+
+namespace Content.Server.Clock;
+
+public sealed class ClockSystem : SharedClockSystem
+{
+ [Dependency] private readonly PvsOverrideSystem _pvsOverride = default!;
+ [Dependency] private readonly IRobustRandom _robustRandom = default!;
+
+ /// <inheritdoc/>
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent<RoundStartingEvent>(OnRoundStart);
+ SubscribeLocalEvent<GlobalTimeManagerComponent, MapInitEvent>(OnMapInit);
+ SubscribeLocalEvent<ClockComponent, BreakageEventArgs>(OnBreak);
+ }
+
+ private void OnRoundStart(RoundStartingEvent ev)
+ {
+ var manager = Spawn();
+ AddComp<GlobalTimeManagerComponent>(manager);
+ }
+
+ private void OnMapInit(Entity<GlobalTimeManagerComponent> ent, ref MapInitEvent args)
+ {
+ ent.Comp.TimeOffset = TimeSpan.FromHours(_robustRandom.NextFloat(0, 24));
+ _pvsOverride.AddGlobalOverride(ent);
+ Dirty(ent);
+ }
+
+ private void OnBreak(Entity<ClockComponent> ent, ref BreakageEventArgs args)
+ {
+ ent.Comp.StuckTime = GetClockTime(ent);
+ Dirty(ent, ent.Comp);
+ }
+}
--- /dev/null
+using Robust.Shared.GameStates;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Clock;
+
+[RegisterComponent, NetworkedComponent]
+[Access(typeof(SharedClockSystem))]
+[AutoGenerateComponentState]
+public sealed partial class ClockComponent : Component
+{
+ /// <summary>
+ /// If not null, this time will be permanently shown.
+ /// </summary>
+ [DataField, AutoNetworkedField]
+ public TimeSpan? StuckTime;
+
+ /// <summary>
+ /// The format in which time is displayed.
+ /// </summary>
+ [DataField, AutoNetworkedField]
+ public ClockType ClockType = ClockType.TwelveHour;
+
+ [DataField]
+ public string HoursBase = "hours_";
+
+ [DataField]
+ public string MinutesBase = "minutes_";
+}
+
+[Serializable, NetSerializable]
+public enum ClockType : byte
+{
+ TwelveHour,
+ TwentyFourHour
+}
+
+[Serializable, NetSerializable]
+public enum ClockVisualLayers : byte
+{
+ HourHand,
+ MinuteHand
+}
--- /dev/null
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Clock;
+
+/// <summary>
+/// This is used for globally managing the time on-station
+/// </summary>
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause, Access(typeof(SharedClockSystem))]
+public sealed partial class GlobalTimeManagerComponent : Component
+{
+ /// <summary>
+ /// A fixed random offset, used to fuzz the time between shifts.
+ /// </summary>
+ [DataField, AutoPausedField, AutoNetworkedField]
+ public TimeSpan TimeOffset;
+}
--- /dev/null
+using System.Linq;
+using Content.Shared.Examine;
+using Content.Shared.GameTicking;
+
+namespace Content.Shared.Clock;
+
+public abstract class SharedClockSystem : EntitySystem
+{
+ [Dependency] private readonly SharedGameTicker _ticker = default!;
+
+ /// <inheritdoc/>
+ public override void Initialize()
+ {
+ SubscribeLocalEvent<ClockComponent, ExaminedEvent>(OnExamined);
+ }
+
+ private void OnExamined(Entity<ClockComponent> ent, ref ExaminedEvent args)
+ {
+ if (!args.IsInDetailsRange)
+ return;
+
+ args.PushMarkup(Loc.GetString("clock-examine", ("time", GetClockTimeText(ent))));
+ }
+
+ public string GetClockTimeText(Entity<ClockComponent> ent)
+ {
+ var time = GetClockTime(ent);
+ switch (ent.Comp.ClockType)
+ {
+ case ClockType.TwelveHour:
+ return time.ToString(@"h\:mm");
+ case ClockType.TwentyFourHour:
+ return time.ToString(@"hh\:mm");
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+ }
+
+ private TimeSpan GetGlobalTime()
+ {
+ return (EntityQuery<GlobalTimeManagerComponent>().FirstOrDefault()?.TimeOffset ?? TimeSpan.Zero) + _ticker.RoundDuration();
+ }
+
+ public TimeSpan GetClockTime(Entity<ClockComponent> ent)
+ {
+ var comp = ent.Comp;
+
+ if (comp.StuckTime != null)
+ return comp.StuckTime.Value;
+
+ var time = GetGlobalTime();
+
+ switch (comp.ClockType)
+ {
+ case ClockType.TwelveHour:
+ var adjustedHours = time.Hours % 12;
+ if (adjustedHours == 0)
+ adjustedHours = 12;
+ return new TimeSpan(adjustedHours, time.Minutes, time.Seconds);
+ case ClockType.TwentyFourHour:
+ return time;
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+ }
+}
--- /dev/null
+clock-examine = The time reads: [color=white]{$time}[/color]
prob: 0.20
- id: BarberScissors
prob: 0.05
+ - id: Wristwatch
+ prob: 0.05
- id: BookRandomStory
prob: 0.1
# Syndicate loot
- ClothingHeadHatCowboyBountyHunter
- ClothingNeckAutismPin
- ClothingNeckGoldAutismPin
+ - WristwatchGold
rareChance: 0.01
prototypes:
- Lighter
- ClothingShoesTourist
- ClothingUniformJumpsuitLoungewear
- ClothingHeadHatCowboyRed
+ - Wristwatch
chance: 0.6
offset: 0.0
--- /dev/null
+- type: entity
+ id: Wristwatch
+ parent: BaseItem
+ name: wristwatch
+ description: A cheap watch for telling time. How much did you waste playing Space Station 14?
+ components:
+ - type: Sprite
+ sprite: Objects/Devices/wristwatch.rsi
+ layers:
+ - state: wristwatch
+ - map: [ "enum.ClockVisualLayers.MinuteHand"]
+ - map: [ "enum.ClockVisualLayers.HourHand"]
+ - type: Clock
+ - type: Item
+ sprite: Objects/Devices/wristwatch.rsi
+ size: Small
+ - type: Clothing
+ sprite: Objects/Devices/wristwatch.rsi
+ slots:
+ - gloves
+ - type: Appearance
+ - type: Damageable
+ damageContainer: Inorganic
+ - type: StaticPrice
+ price: 50
+ - type: Destructible
+ thresholds:
+ - trigger:
+ !type:DamageTrigger
+ damage: 100
+ behaviors:
+ - !type:DoActsBehavior
+ acts: [ "Destruction" ]
+ - trigger:
+ !type:DamageGroupTrigger
+ damageGroup: Brute
+ damage: 25
+ behaviors:
+ - !type:DoActsBehavior
+ acts: [ "Breakage" ]
+
+- type: entity
+ id: WristwatchGold
+ parent: Wristwatch
+ name: gold watch
+ description: A fancy watch worth more than your kidney. It was owned by the notorious Syndicate mobster Vunibaldo "200 Pound Horse Meat Grinder" Frediani.
+ components:
+ - type: Sprite
+ sprite: Objects/Devices/goldwatch.rsi
+ layers:
+ - state: goldwatch
+ - map: [ "enum.ClockVisualLayers.MinuteHand"]
+ - map: [ "enum.ClockVisualLayers.HourHand"]
+ - type: Item
+ sprite: Objects/Devices/goldwatch.rsi
+ - type: Clothing
+ sprite: Objects/Devices/goldwatch.rsi
+ - type: StaticPrice
+ price: 500 #if you ever increase the price of kidneys, increase this too.
--- /dev/null
+{
+ "version": 1,
+ "license": "CC-BY-SA-3.0",
+ "copyright": "Taken from vgstation13 at https://github.com/vgstation-coders/vgstation13/blob/223bf37a7386249ecf1fe425ca8cef25821ca9d7/icons/obj/watches/goldwatch.dmi, https://github.com/vgstation-coders/vgstation13/blob/223bf37a7386249ecf1fe425ca8cef25821ca9d7/icons/mob/in-hand/right/clothing_accessories.dmi, https://github.com/vgstation-coders/vgstation13/blob/223bf37a7386249ecf1fe425ca8cef25821ca9d7/icons/mob/in-hand/left/clothing_accessories.dmi, https://github.com/vgstation-coders/vgstation13/blob/30f9caeb59b0dd9da1dbcd4c69307ae182033a74/icons/mob/clothing_accessories.dmi",
+ "size": {
+ "x": 32,
+ "y": 32
+ },
+ "states": [
+ {
+ "name": "goldwatch"
+ },
+ {
+ "name": "hours_1"
+ },
+ {
+ "name": "hours_2"
+ },
+ {
+ "name": "hours_3"
+ },
+ {
+ "name": "hours_4"
+ },
+ {
+ "name": "hours_5"
+ },
+ {
+ "name": "hours_6"
+ },
+ {
+ "name": "hours_7"
+ },
+ {
+ "name": "hours_8"
+ },
+ {
+ "name": "hours_9"
+ },
+ {
+ "name": "hours_10"
+ },
+ {
+ "name": "hours_11"
+ },
+ {
+ "name": "hours_0"
+ },
+ {
+ "name": "minutes_1"
+ },
+ {
+ "name": "minutes_2"
+ },
+ {
+ "name": "minutes_3"
+ },
+ {
+ "name": "minutes_4"
+ },
+ {
+ "name": "minutes_5"
+ },
+ {
+ "name": "minutes_6"
+ },
+ {
+ "name": "minutes_7"
+ },
+ {
+ "name": "minutes_8"
+ },
+ {
+ "name": "minutes_9"
+ },
+ {
+ "name": "minutes_10"
+ },
+ {
+ "name": "minutes_11"
+ },
+ {
+ "name": "minutes_0"
+ },
+ {
+ "name": "inhand-right",
+ "directions": 4
+ },
+ {
+ "name": "inhand-left",
+ "directions": 4
+ },
+ {
+ "name": "equipped-HAND",
+ "directions": 4
+ }
+ ]
+}
\ No newline at end of file
--- /dev/null
+{
+ "version": 1,
+ "license": "CC-BY-SA-3.0",
+ "copyright": "Taken from vgstation13 at https://github.com/vgstation-coders/vgstation13/blob/223bf37a7386249ecf1fe425ca8cef25821ca9d7/icons/obj/watches/wristwatch.dmi, https://github.com/vgstation-coders/vgstation13/blob/30f9caeb59b0dd9da1dbcd4c69307ae182033a74/icons/obj/clothing/accessory_overlays.dmi, https://github.com/vgstation-coders/vgstation13/blob/223bf37a7386249ecf1fe425ca8cef25821ca9d7/icons/mob/in-hand/right/clothing_accessories.dmi, https://github.com/vgstation-coders/vgstation13/blob/223bf37a7386249ecf1fe425ca8cef25821ca9d7/icons/mob/in-hand/right/clothing_accessories.dmi, https://github.com/vgstation-coders/vgstation13/blob/223bf37a7386249ecf1fe425ca8cef25821ca9d7/icons/mob/in-hand/left/clothing_accessories.dmi, https://github.com/vgstation-coders/vgstation13/blob/30f9caeb59b0dd9da1dbcd4c69307ae182033a74/icons/mob/clothing_accessories.dmi",
+ "size": {
+ "x": 32,
+ "y": 32
+ },
+ "states": [
+ {
+ "name": "wristwatch"
+ },
+ {
+ "name": "hours_1"
+ },
+ {
+ "name": "hours_2"
+ },
+ {
+ "name": "hours_3"
+ },
+ {
+ "name": "hours_4"
+ },
+ {
+ "name": "hours_5"
+ },
+ {
+ "name": "hours_6"
+ },
+ {
+ "name": "hours_7"
+ },
+ {
+ "name": "hours_8"
+ },
+ {
+ "name": "hours_9"
+ },
+ {
+ "name": "hours_10"
+ },
+ {
+ "name": "hours_11"
+ },
+ {
+ "name": "hours_0"
+ },
+ {
+ "name": "minutes_1"
+ },
+ {
+ "name": "minutes_2"
+ },
+ {
+ "name": "minutes_3"
+ },
+ {
+ "name": "minutes_4"
+ },
+ {
+ "name": "minutes_5"
+ },
+ {
+ "name": "minutes_6"
+ },
+ {
+ "name": "minutes_7"
+ },
+ {
+ "name": "minutes_8"
+ },
+ {
+ "name": "minutes_9"
+ },
+ {
+ "name": "minutes_10"
+ },
+ {
+ "name": "minutes_11"
+ },
+ {
+ "name": "minutes_0"
+ },
+ {
+ "name": "inhand-right",
+ "directions": 4
+ },
+ {
+ "name": "inhand-left",
+ "directions": 4
+ },
+ {
+ "name": "equipped-HAND",
+ "directions": 4
+ }
+ ]
+}