SurgeryBui.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  1. // SPDX-FileCopyrightText: 2025 Aiden <28298836+Aidenkrz@users.noreply.github.com>
  2. // SPDX-FileCopyrightText: 2025 Piras314 <p1r4s@proton.me>
  3. // SPDX-FileCopyrightText: 2025 deltanedas <39013340+deltanedas@users.noreply.github.com>
  4. // SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org>
  5. // SPDX-FileCopyrightText: 2025 gluesniffler <159397573+gluesniffler@users.noreply.github.com>
  6. //
  7. // SPDX-License-Identifier: AGPL-3.0-or-later
  8. using Content.Client._Shitmed.Choice.UI;
  9. using Content.Client.Administration.UI.CustomControls;
  10. using Content.Shared._Shitmed.Medical.Surgery;
  11. using Content.Shared.Body.Components;
  12. using Content.Shared.Body.Part;
  13. using JetBrains.Annotations;
  14. using Robust.Client.GameObjects;
  15. using Robust.Client.Player;
  16. using Robust.Shared.Prototypes;
  17. using Robust.Shared.Utility;
  18. namespace Content.Client._Shitmed.Medical.Surgery;
  19. [UsedImplicitly]
  20. public sealed class SurgeryBui : BoundUserInterface
  21. {
  22. [Dependency] private readonly IEntityManager _entities = default!;
  23. [Dependency] private readonly IPlayerManager _player = default!;
  24. private readonly SurgerySystem _system;
  25. [ViewVariables]
  26. private SurgeryWindow? _window;
  27. private EntityUid? _part;
  28. private bool _isBody;
  29. private (EntityUid Ent, EntProtoId Proto)? _surgery;
  30. private readonly List<EntProtoId> _previousSurgeries = new();
  31. public SurgeryBui(EntityUid owner, Enum uiKey) : base(owner, uiKey) => _system = _entities.System<SurgerySystem>();
  32. protected override void ReceiveMessage(BoundUserInterfaceMessage message)
  33. {
  34. if (_window is null
  35. || message is not SurgeryBuiRefreshMessage)
  36. return;
  37. RefreshUI();
  38. }
  39. protected override void UpdateState(BoundUserInterfaceState state)
  40. {
  41. if (state is not SurgeryBuiState s)
  42. return;
  43. Update(s);
  44. }
  45. protected override void Dispose(bool disposing)
  46. {
  47. base.Dispose(disposing);
  48. if (disposing)
  49. _window?.Dispose();
  50. }
  51. private void Update(SurgeryBuiState state)
  52. {
  53. if (_window == null)
  54. {
  55. _window = new SurgeryWindow();
  56. _window.OnClose += Close;
  57. _window.Title = Loc.GetString("surgery-ui-window-title");
  58. _window.PartsButton.OnPressed += _ =>
  59. {
  60. _part = null;
  61. _isBody = false;
  62. _surgery = null;
  63. _previousSurgeries.Clear();
  64. View(ViewType.Parts);
  65. };
  66. _window.SurgeriesButton.OnPressed += _ =>
  67. {
  68. _surgery = null;
  69. _previousSurgeries.Clear();
  70. if (!_entities.TryGetNetEntity(_part, out var netPart)
  71. || State is not SurgeryBuiState s
  72. || !s.Choices.TryGetValue(netPart.Value, out var surgeries))
  73. return;
  74. OnPartPressed(netPart.Value, surgeries);
  75. };
  76. _window.StepsButton.OnPressed += _ =>
  77. {
  78. if (!_entities.TryGetNetEntity(_part, out var netPart)
  79. || _previousSurgeries.Count == 0)
  80. return;
  81. var last = _previousSurgeries[^1];
  82. _previousSurgeries.RemoveAt(_previousSurgeries.Count - 1);
  83. if (_system.GetSingleton(last) is not { } previousId
  84. || !_entities.TryGetComponent(previousId, out SurgeryComponent? previous))
  85. return;
  86. OnSurgeryPressed((previousId, previous), netPart.Value, last);
  87. };
  88. }
  89. _window.Surgeries.DisposeAllChildren();
  90. _window.Steps.DisposeAllChildren();
  91. _window.Parts.DisposeAllChildren();
  92. View(ViewType.Parts);
  93. var oldSurgery = _surgery;
  94. var oldPart = _part;
  95. _part = null;
  96. _surgery = null;
  97. var options = new List<(NetEntity netEntity, EntityUid entity, string Name, BodyPartType? PartType)>();
  98. foreach (var choice in state.Choices.Keys)
  99. if (_entities.TryGetEntity(choice, out var ent))
  100. {
  101. if (_entities.TryGetComponent(ent, out BodyPartComponent? part))
  102. options.Add((choice, ent.Value, _entities.GetComponent<MetaDataComponent>(ent.Value).EntityName, part.PartType));
  103. else if (_entities.TryGetComponent(ent, out BodyComponent? body))
  104. options.Add((choice, ent.Value, _entities.GetComponent<MetaDataComponent>(ent.Value).EntityName, null));
  105. }
  106. options.Sort((a, b) =>
  107. {
  108. int GetScore(BodyPartType? partType)
  109. {
  110. return partType switch
  111. {
  112. BodyPartType.Head => 1,
  113. BodyPartType.Torso => 2,
  114. BodyPartType.Arm => 3,
  115. BodyPartType.Hand => 4,
  116. BodyPartType.Leg => 5,
  117. BodyPartType.Foot => 6,
  118. // BodyPartType.Tail => 7, No tails yet!
  119. BodyPartType.Other => 8,
  120. _ => 9
  121. };
  122. }
  123. return GetScore(a.PartType) - GetScore(b.PartType);
  124. });
  125. foreach (var (netEntity, entity, partName, _) in options)
  126. {
  127. //var netPart = _entities.GetNetEntity(part.Owner);
  128. var surgeries = state.Choices[netEntity];
  129. var partButton = new ChoiceControl();
  130. partButton.Set(partName, null);
  131. partButton.Button.OnPressed += _ => OnPartPressed(netEntity, surgeries);
  132. _window.Parts.AddChild(partButton);
  133. foreach (var surgeryId in surgeries)
  134. {
  135. if (_system.GetSingleton(surgeryId) is not { } surgery ||
  136. !_entities.TryGetComponent(surgery, out SurgeryComponent? surgeryComp))
  137. continue;
  138. if (oldPart == entity && oldSurgery?.Proto == surgeryId)
  139. OnSurgeryPressed((surgery, surgeryComp), netEntity, surgeryId);
  140. }
  141. if (oldPart == entity && oldSurgery == null)
  142. OnPartPressed(netEntity, surgeries);
  143. }
  144. if (!_window.IsOpen)
  145. _window.OpenCentered();
  146. }
  147. private void AddStep(EntProtoId stepId, NetEntity netPart, EntProtoId surgeryId)
  148. {
  149. if (_window == null
  150. || _system.GetSingleton(stepId) is not { } step)
  151. return;
  152. var stepName = new FormattedMessage();
  153. stepName.AddText(_entities.GetComponent<MetaDataComponent>(step).EntityName);
  154. var stepButton = new SurgeryStepButton { Step = step };
  155. stepButton.Button.OnPressed += _ => SendMessage(new SurgeryStepChosenBuiMsg(netPart, surgeryId, stepId, _isBody));
  156. _window.Steps.AddChild(stepButton);
  157. }
  158. private void OnSurgeryPressed(Entity<SurgeryComponent> surgery, NetEntity netPart, EntProtoId surgeryId)
  159. {
  160. if (_window == null)
  161. return;
  162. _part = _entities.GetEntity(netPart);
  163. _isBody = _entities.HasComponent<BodyComponent>(_part);
  164. _surgery = (surgery, surgeryId);
  165. _window.Steps.DisposeAllChildren();
  166. // This apparently does not consider if theres multiple surgery requirements in one surgery. Maybe thats fine.
  167. if (surgery.Comp.Requirement is { } requirementId && _system.GetSingleton(requirementId) is { } requirement)
  168. {
  169. var label = new ChoiceControl();
  170. label.Button.OnPressed += _ =>
  171. {
  172. _previousSurgeries.Add(surgeryId);
  173. if (_entities.TryGetComponent(requirement, out SurgeryComponent? requirementComp))
  174. OnSurgeryPressed((requirement, requirementComp), netPart, requirementId);
  175. };
  176. var msg = new FormattedMessage();
  177. var surgeryName = _entities.GetComponent<MetaDataComponent>(requirement).EntityName;
  178. msg.AddMarkup($"[bold]{Loc.GetString("surgery-ui-window-require")}: {surgeryName}[/bold]");
  179. label.Set(msg, null);
  180. _window.Steps.AddChild(label);
  181. _window.Steps.AddChild(new HSeparator { Margin = new Thickness(0, 0, 0, 1) });
  182. }
  183. foreach (var stepId in surgery.Comp.Steps)
  184. AddStep(stepId, netPart, surgeryId);
  185. View(ViewType.Steps);
  186. RefreshUI();
  187. }
  188. private void OnPartPressed(NetEntity netPart, List<EntProtoId> surgeryIds)
  189. {
  190. if (_window == null)
  191. return;
  192. _part = _entities.GetEntity(netPart);
  193. _isBody = _entities.HasComponent<BodyComponent>(_part);
  194. _window.Surgeries.DisposeAllChildren();
  195. var surgeries = new List<(Entity<SurgeryComponent> Ent, EntProtoId Id, string Name)>();
  196. foreach (var surgeryId in surgeryIds)
  197. {
  198. if (_system.GetSingleton(surgeryId) is not { } surgery ||
  199. !_entities.TryGetComponent(surgery, out SurgeryComponent? surgeryComp))
  200. {
  201. continue;
  202. }
  203. var name = _entities.GetComponent<MetaDataComponent>(surgery).EntityName;
  204. surgeries.Add(((surgery, surgeryComp), surgeryId, name));
  205. }
  206. surgeries.Sort((a, b) =>
  207. {
  208. var priority = a.Ent.Comp.Priority.CompareTo(b.Ent.Comp.Priority);
  209. if (priority != 0)
  210. return priority;
  211. return string.Compare(a.Name, b.Name, StringComparison.Ordinal);
  212. });
  213. foreach (var surgery in surgeries)
  214. {
  215. var surgeryButton = new ChoiceControl();
  216. surgeryButton.Set(surgery.Name, null);
  217. surgeryButton.Button.OnPressed += _ => OnSurgeryPressed(surgery.Ent, netPart, surgery.Id);
  218. _window.Surgeries.AddChild(surgeryButton);
  219. }
  220. RefreshUI();
  221. View(ViewType.Surgeries);
  222. }
  223. private void RefreshUI()
  224. {
  225. if (_window == null
  226. || !_window.IsOpen
  227. || _part == null
  228. || !_entities.HasComponent<SurgeryComponent>(_surgery?.Ent)
  229. || !_entities.TryGetComponent(_player.LocalEntity ?? EntityUid.Invalid, out SurgeryTargetComponent? surgeryComp)
  230. || !surgeryComp.CanOperate)
  231. return;
  232. var next = _system.GetNextStep(Owner, _part.Value, _surgery.Value.Ent);
  233. var i = 0;
  234. foreach (var child in _window.Steps.Children)
  235. {
  236. if (child is not SurgeryStepButton stepButton)
  237. continue;
  238. var status = StepStatus.Incomplete;
  239. if (next == null)
  240. status = StepStatus.Complete;
  241. else if (next.Value.Surgery.Owner != _surgery.Value.Ent)
  242. status = StepStatus.Incomplete;
  243. else if (next.Value.Step == i)
  244. status = StepStatus.Next;
  245. else if (i < next.Value.Step)
  246. status = StepStatus.Complete;
  247. stepButton.Button.Disabled = status != StepStatus.Next;
  248. var stepName = new FormattedMessage();
  249. stepName.AddText(_entities.GetComponent<MetaDataComponent>(stepButton.Step).EntityName);
  250. if (status == StepStatus.Complete)
  251. stepButton.Button.Modulate = Color.Green;
  252. else
  253. {
  254. stepButton.Button.Modulate = Color.White;
  255. if (_player.LocalEntity is { } player
  256. && status == StepStatus.Next
  257. && !_system.CanPerformStep(player, Owner, _part.Value, stepButton.Step, false, out var popup, out var reason, out _))
  258. stepButton.ToolTip = popup;
  259. }
  260. var texture = _entities.GetComponentOrNull<SpriteComponent>(stepButton.Step)?.Icon?.Default;
  261. stepButton.Set(stepName, texture);
  262. i++;
  263. }
  264. }
  265. private void View(ViewType type)
  266. {
  267. if (_window == null)
  268. return;
  269. _window.PartsButton.Parent!.Margin = new Thickness(0, 0, 0, 10);
  270. _window.Parts.Visible = type == ViewType.Parts;
  271. _window.PartsButton.Disabled = type == ViewType.Parts;
  272. _window.Surgeries.Visible = type == ViewType.Surgeries;
  273. _window.SurgeriesButton.Disabled = type != ViewType.Steps;
  274. _window.Steps.Visible = type == ViewType.Steps;
  275. _window.StepsButton.Disabled = type != ViewType.Steps || _previousSurgeries.Count == 0;
  276. if (_entities.TryGetComponent(_part, out MetaDataComponent? partMeta) &&
  277. _entities.TryGetComponent(_surgery?.Ent, out MetaDataComponent? surgeryMeta))
  278. _window.Title = $"Surgery - {partMeta.EntityName}, {surgeryMeta.EntityName}";
  279. else if (partMeta != null)
  280. _window.Title = $"Surgery - {partMeta.EntityName}";
  281. else
  282. _window.Title = "Surgery";
  283. }
  284. private enum ViewType
  285. {
  286. Parts,
  287. Surgeries,
  288. Steps
  289. }
  290. private enum StepStatus
  291. {
  292. Next,
  293. Complete,
  294. Incomplete
  295. }
  296. }