diff --git a/Content.Client/_CorvaxGoob/Power/Generation/FissionGenerator/NuclearReactorSystem.cs b/Content.Client/_CorvaxGoob/Power/Generation/FissionGenerator/NuclearReactorSystem.cs new file mode 100644 index 000000000000..201030748dd7 --- /dev/null +++ b/Content.Client/_CorvaxGoob/Power/Generation/FissionGenerator/NuclearReactorSystem.cs @@ -0,0 +1,94 @@ +// SPDX-FileCopyrightText: 2025 jhrushbe +// SPDX-FileCopyrightText: 2025 rottenheadphones +// SPDX-FileCopyrightText: 2025 taydeo +// +// SPDX-License-Identifier: CC-BY-NC-SA-3.0 + +using Content.Client.NodeContainer; +using Content.Shared._CorvaxGoob.Power.Generation.FissionGenerator; +using Robust.Shared.Map; +using Content.Client.Examine; +using Robust.Client.GameObjects; +using Robust.Client.ResourceManagement; + +namespace Content.Client._CorvaxGoob.Power.Generation.FissionGenerator; + +public sealed class NuclearReactorSystem : SharedNuclearReactorSystem +{ + [Dependency] private readonly SpriteSystem _sprite = default!; + [Dependency] private readonly IResourceCache _resourceCache = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnInit); + + SubscribeLocalEvent(ReactorExamined); + SubscribeLocalEvent(OnAppearanceChange); + } + + private void OnInit(EntityUid uid, NuclearReactorComponent comp, ref ComponentInit args) + { + if (!TryComp(uid, out var sprite)) + return; + + if (!_resourceCache.TryGetResource("/Textures/_CorvaxGoob/Structures/Power/Generation/FissionGenerator/reactor_component_cap.rsi", out RSIResource? resource)) + return; + + Entity entSprite = (uid, sprite); + var xspace = comp.Gridbounds[0] / 32f; + var yspace = comp.Gridbounds[1] / 32f; + var xoff = comp.Gridbounds[2] / 32f; + var yoff = comp.Gridbounds[3] / 32f; + + var gridWidth = comp.ReactorGridWidth; + var gridHeight = comp.ReactorGridHeight; + + var xAdj = (gridWidth - 1) / 2f; + var yAdj = (gridHeight - 1) / 2f; + + for (var x = 0; x < gridWidth; x++) + { + for (var y = 0; y < gridHeight; y++) + { + var layerID = _sprite.AddRsiLayer(entSprite, "empty_cap", resource.RSI); + _sprite.LayerMapSet(entSprite, FormatMap(x, y), layerID); + _sprite.LayerSetOffset(entSprite, layerID, new((xspace * (y - yAdj)) - xoff, (-yspace * (x - xAdj)) - yoff)); + _sprite.LayerSetColor(entSprite, layerID, Color.Black); + } + } + } + + private static string FormatMap(int x, int y) => "NuclearReactorCap" + x + "/" + y; + + private void ReactorExamined(EntityUid uid, NuclearReactorComponent comp, ClientExaminedEvent args) => Spawn(comp.ArrowPrototype, new EntityCoordinates(uid, 0, 0)); + + private void OnAppearanceChange(EntityUid uid, NuclearReactorComponent comp, ref AppearanceChangeEvent args) + { + for (var x = 0; x < comp.ReactorGridWidth; x++) + { + for (var y = 0; y < comp.ReactorGridHeight; y++) + { + if(comp.VisualData.TryGetValue(new(x,y), out var data)) + UpdateRodAppearance(uid, FormatMap(x,y), data.cap, data.color); + else + UpdateRodAppearance(uid, FormatMap(x, y), "empty_cap", Color.Black); + } + } + } + + private void UpdateRodAppearance(EntityUid uid, string map, string state, Color color) + { + if (!TryComp(uid, out var sprite)) + return; + + Entity entSprite = (uid, sprite); + + if (!_sprite.LayerMapTryGet(entSprite, map, out var layer, false)) + return; + + _sprite.LayerSetRsiState(entSprite, layer, state); + _sprite.LayerSetColor(entSprite, layer, color); + } +} diff --git a/Content.Client/_CorvaxGoob/Power/Generation/FissionGenerator/ReactorPartSystem.cs b/Content.Client/_CorvaxGoob/Power/Generation/FissionGenerator/ReactorPartSystem.cs new file mode 100644 index 000000000000..815ce13b2141 --- /dev/null +++ b/Content.Client/_CorvaxGoob/Power/Generation/FissionGenerator/ReactorPartSystem.cs @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: 2025 jhrushbe +// SPDX-FileCopyrightText: 2025 rottenheadphones +// SPDX-FileCopyrightText: 2025 taydeo +// +// SPDX-License-Identifier: CC-BY-NC-SA-3.0 + +using Content.Shared._CorvaxGoob.Power.Generation.FissionGenerator; +using Robust.Client.GameObjects; +using Robust.Shared.Prototypes; + +namespace Content.Client._CorvaxGoob.Power.Generation.FissionGenerator; + +public sealed class ReactorPartSystem : SharedReactorPartSystem +{ + [Dependency] private readonly SpriteSystem _sprite = default!; + [Dependency] private readonly IPrototypeManager _proto = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnAppearanceChange); + SubscribeLocalEvent(OnComponentInit); + } + + private void OnAppearanceChange(EntityUid uid, ReactorPartComponent component, ref AppearanceChangeEvent args) + { + if (args.Sprite == null) + return; + + // Re-enable if/when there are multiple sprites + //if (!_sprite.LayerMapTryGet((uid, args.Sprite), ReactorCapVisualLayers.Sprite, out var layer, false)) + // return; + + _sprite.LayerSetColor((uid, args.Sprite), 0, _proto.Index(component.Material).Color); + } + + private void OnComponentInit(Entity ent, ref ComponentInit args) + => _sprite.LayerSetColor((ent.Owner, EntityManager.GetComponent(ent.Owner)), 0, _proto.Index(ent.Comp.Material).Color); +} \ No newline at end of file diff --git a/Content.Client/_CorvaxGoob/Power/Generation/FissionGenerator/TurbineSystem.cs b/Content.Client/_CorvaxGoob/Power/Generation/FissionGenerator/TurbineSystem.cs new file mode 100644 index 000000000000..4dcc7d671bec --- /dev/null +++ b/Content.Client/_CorvaxGoob/Power/Generation/FissionGenerator/TurbineSystem.cs @@ -0,0 +1,147 @@ +using Robust.Shared.Map; +using Robust.Client.GameObjects; +using Content.Shared.Repairable; +using Content.Shared._CorvaxGoob.Power.Generation.FissionGenerator; +using Content.Client.Popups; +using Content.Client.Examine; +using Robust.Client.Animations; +using Content.Shared.Containers.ItemSlots; +using Content.Shared.Popups; + +namespace Content.Client._CorvaxGoob.Power.Generation.FissionGenerator; + +// Ported and modified from goonstation by Jhrushbe. +// CC-BY-NC-SA-3.0 +// https://github.com/goonstation/goonstation/blob/ff86b044/code/obj/nuclearreactor/turbine.dm + +public sealed class TurbineSystem : SharedTurbineSystem +{ + [Dependency] private readonly PopupSystem _popupSystem = default!; + [Dependency] private readonly AnimationPlayerSystem _animationPlayer = default!; + [Dependency] private readonly SpriteSystem _sprite = default!; + + private readonly float _threshold = 1f; + private float _accumulator = 0; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(TurbineExamined); + + SubscribeLocalEvent(OnAnimationCompleted); + + SubscribeLocalEvent(OnInsertAttempt); + SubscribeLocalEvent(OnEjectAttempt); + } + + protected override void OnRepairTurbineFinished(EntityUid uid, TurbineComponent comp, ref RepairDoAfterEvent args) + { + if (args.Cancelled) + return; + + _popupSystem.PopupClient(Loc.GetString("turbine-repair", ("target", uid), ("tool", args.Used!)), uid, args.User); + } + + private void TurbineExamined(EntityUid uid, TurbineComponent comp, ClientExaminedEvent args) => Spawn(comp.ArrowPrototype, new EntityCoordinates(uid, 0, 0)); + + #region Animation + private void OnAnimationCompleted(EntityUid uid, TurbineComponent comp, ref AnimationCompletedEvent args) => PlayAnimation(uid, comp); + + public override void FrameUpdate(float frameTime) + { + _accumulator += frameTime; + if (_accumulator >= _threshold) + { + AccUpdate(); + _accumulator = 0; + } + } + + private void AccUpdate() + { + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var component)) + { + // Makes sure the anim doesn't get stuck at low RPM + PlayAnimation(uid, component); + } + } + + private void PlayAnimation(EntityUid uid, TurbineComponent comp) + { + if (!TryComp(uid, out var sprite) || !_sprite.TryGetLayer((uid,sprite), TurbineVisualLayers.TurbineSpeed, out var layer, false)) + return; + + var state = "speedanim"; + if (comp.RPM < 1) + { + _animationPlayer.Stop(uid, state); + _sprite.LayerSetRsiState(layer, "turbine"); + comp.AnimRPM = -comp.BestRPM; // Primes it to start the instant it's spinning again + return; + } + + if (Math.Abs(comp.RPM - comp.AnimRPM) > comp.BestRPM * 0.1) + _animationPlayer.Stop(uid, state); // Current anim is stale, time for a new one + + if (_animationPlayer.HasRunningAnimation(uid, state)) + return; + + comp.AnimRPM = comp.RPM; + var layerKey = TurbineVisualLayers.TurbineSpeed; + var time = 0.5f * comp.BestRPM / comp.RPM; + var timestep = time / 12; + var animation = new global::Robust.Client.Animations.Animation + { + Length = TimeSpan.FromSeconds(time), + AnimationTracks = + { + new AnimationTrackSpriteFlick + { + LayerKey = layerKey, + KeyFrames = + { + new AnimationTrackSpriteFlick.KeyFrame("turbinerun_00", 0), + new AnimationTrackSpriteFlick.KeyFrame("turbinerun_01", timestep), + new AnimationTrackSpriteFlick.KeyFrame("turbinerun_02", timestep), + new AnimationTrackSpriteFlick.KeyFrame("turbinerun_03", timestep), + new AnimationTrackSpriteFlick.KeyFrame("turbinerun_04", timestep), + new AnimationTrackSpriteFlick.KeyFrame("turbinerun_05", timestep), + new AnimationTrackSpriteFlick.KeyFrame("turbinerun_06", timestep), + new AnimationTrackSpriteFlick.KeyFrame("turbinerun_07", timestep), + new AnimationTrackSpriteFlick.KeyFrame("turbinerun_08", timestep), + new AnimationTrackSpriteFlick.KeyFrame("turbinerun_09", timestep), + new AnimationTrackSpriteFlick.KeyFrame("turbinerun_10", timestep), + new AnimationTrackSpriteFlick.KeyFrame("turbinerun_11", timestep) + } + } + } + }; + _sprite.LayerSetVisible(layer, true); + _animationPlayer.Play(uid, animation, state); + } + #endregion + + private void OnEjectAttempt(EntityUid uid, TurbineComponent comp, ref ItemSlotEjectAttemptEvent args) + { + if (args.Cancelled) + return; + + if (comp.RPM < 1) + return; + + args.Cancelled = true; + } + + private void OnInsertAttempt(EntityUid uid, TurbineComponent comp, ref ItemSlotInsertAttemptEvent args) + { + if (args.Cancelled) + return; + + if (comp.RPM < 1) + return; + + args.Cancelled = true; + } +} diff --git a/Content.Client/_CorvaxGoob/Power/UI/NuclearReactorBoundUserInterface.cs b/Content.Client/_CorvaxGoob/Power/UI/NuclearReactorBoundUserInterface.cs new file mode 100644 index 000000000000..604bddef4cc6 --- /dev/null +++ b/Content.Client/_CorvaxGoob/Power/UI/NuclearReactorBoundUserInterface.cs @@ -0,0 +1,89 @@ +// SPDX-FileCopyrightText: 2025 jhrushbe +// SPDX-FileCopyrightText: 2025 rottenheadphones +// SPDX-FileCopyrightText: 2025 taydeo +// +// SPDX-License-Identifier: CC-BY-NC-SA-3.0 + +using System.Numerics; +using Content.Client.UserInterface; +using Content.Shared._CorvaxGoob.Power.Generation.FissionGenerator; +using Content.Shared.Atmos.Piping.Binary.Components; +using Content.Shared.Atmos.Piping.Unary.Components; +using Content.Shared.IdentityManagement; +using JetBrains.Annotations; +using Robust.Client.Timing; +using Robust.Client.UserInterface; + +namespace Content.Client._CorvaxGoob.Power.UI; + +/// +/// Initializes a and updates it when new server messages are received. +/// +[UsedImplicitly] +public sealed class NuclearReactorBoundUserInterface : BoundUserInterface +{ + [Dependency] private readonly IEntityManager _entityManager = default!; + + [ViewVariables] + private NuclearReactorWindow? _window; + + public NuclearReactorBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) + { + } + + protected override void Open() + { + EntityUid? reactorUid = null; + if (_entityManager.TryGetComponent(Owner, out var reactorMonitorComponent)) + { + if (!_entityManager.TryGetEntity(reactorMonitorComponent.reactor, out reactorUid) || reactorUid == null + || !_entityManager.TryGetComponent(reactorUid, out var monitoredReactorComponent) || monitoredReactorComponent.Melted) + return; + } + else if (!_entityManager.TryGetComponent(Owner, out var reactorComponent) || reactorComponent.Melted) + return; + + base.Open(); + + _window = this.CreateWindow(); + if (_entityManager.EntityExists(reactorUid)) + _window.SetEntity(reactorUid.Value, Owner); + else + _window.SetEntity(Owner); + + _window.ItemActionButtonPressed += OnActionButtonPressed; + _window.EjectButtonPressed += OnEjectButtonPressed; + _window.ControlRodModify += OnControlRodModify; + + Update(); + } + + protected override void UpdateState(BoundUserInterfaceState state) + { + if (state is not NuclearReactorBuiState reactorState) + return; + + _window?.Update(reactorState); + } + + private void OnActionButtonPressed(Vector2d vector) + { + if (_window is null ) return; + + SendPredictedMessage(new ReactorItemActionMessage(vector)); + } + + private void OnEjectButtonPressed() + { + if (_window is null) return; + + SendPredictedMessage(new ReactorEjectItemMessage()); + } + + private void OnControlRodModify(float amount) + { + if (_window is null) return; + + SendPredictedMessage(new ReactorControlRodModifyMessage(amount)); + } +} \ No newline at end of file diff --git a/Content.Client/_CorvaxGoob/Power/UI/NuclearReactorWindow.xaml b/Content.Client/_CorvaxGoob/Power/UI/NuclearReactorWindow.xaml new file mode 100644 index 000000000000..4a7480486925 --- /dev/null +++ b/Content.Client/_CorvaxGoob/Power/UI/NuclearReactorWindow.xaml @@ -0,0 +1,150 @@ + + + + + + + + + + + + + + + + + +