Skip to content

Commit

Permalink
Health bar (#13)
Browse files Browse the repository at this point in the history
<!-- Guidelines:
https://docs.spacestation14.io/en/getting-started/pr-guideline -->

IT'S A HEALTH BAR HOLY SHID

## About the PR
<!-- What did you change? -->

## Why / Balance
<!-- Discuss how this would affect game balance or explain why it was
changed. Link any relevant discussions or issues. -->

## Technical details
<!-- Summary of code changes for easier review. -->

## Media
<!-- Attach media if the PR makes ingame changes (clothing, items,
features, etc).
Small fixes/refactors are exempt. Media may be used in SS14 progress
reports with credit. -->


![image](https://github.com/user-attachments/assets/ba664f75-f463-41ad-a5c3-2c28b7193015)

![image](https://github.com/user-attachments/assets/565f838b-9e57-4e4b-8dba-d3da94046cc8)

## Requirements
<!-- Confirm the following by placing an X in the brackets [X]: -->
- [X] I have read and am following the [Pull Request and Changelog
Guidelines](https://docs.spacestation14.com/en/general-development/codebase-info/pull-request-guidelines.html).
- [X] I have added media to this PR or it does not require an ingame
showcase.
<!-- You should understand that not following the above may get your PR
closed at maintainer’s discretion -->

## Breaking changes
<!-- List any breaking changes, including namespaces, public
class/method/field changes, prototype renames; and provide instructions
for fixing them.
This will be posted in #codebase-changes. -->

**Changelog**
<!-- Add a Changelog entry to make players aware of new features or
changes that could affect gameplay.
Make sure to read the guidelines and take this Changelog template out of
the comment block in order for it to show up.
Changelog must have a 🆑 symbol, so the bot recognizes the changes and
adds them to the game's changelog. -->
<!--
🆑
- add: Added fun!
- remove: Removed fun!
- tweak: Changed fun!
- fix: Fixed fun!
-->
🆑
- add: Status UI showing health, role and timer.
  • Loading branch information
Simyon264 authored Oct 24, 2024
2 parents 541dd19 + 6a45c1a commit 127199f
Show file tree
Hide file tree
Showing 19 changed files with 458 additions and 17 deletions.
2 changes: 2 additions & 0 deletions Content.Client/UserInterface/Screens/DefaultGameScreen.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
xmlns:hotbar="clr-namespace:Content.Client.UserInterface.Systems.Hotbar.Widgets"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:inventory="clr-namespace:Content.Client.UserInterface.Systems.Inventory.Widgets"
xmlns:sss="clr-namespace:Content.Client._SSS.UserInterface.Widgets"
Name="DefaultHud"
VerticalExpand="False"
VerticalAlignment="Bottom"
Expand All @@ -30,4 +31,5 @@
<hotbar:HotbarGui Name="Hotbar" Access="Protected" />
<chat:ResizableChatBox Name="Chat" Access="Protected" />
<alerts:AlertsUI Name="Alerts" Access="Protected" />
<sss:SSSStatusGui Name="SSSStatus" Access="Protected" />
</screens:DefaultGameScreen>
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ public DefaultGameScreen()
SetAnchorAndMarginPreset(Chat, LayoutPreset.TopRight, margin: 10);
SetAnchorAndMarginPreset(Alerts, LayoutPreset.TopRight, margin: 10);

SetAnchorAndMarginPreset(SSSStatus, LayoutPreset.BottomRight, margin: 10);

Chat.OnResized += ChatOnResized;
Chat.OnChatResizeFinish += ChatOnResizeFinish;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
xmlns:inventory="clr-namespace:Content.Client.UserInterface.Systems.Inventory.Widgets"
xmlns:sss="clr-namespace:Content.Client._SSS.UserInterface.Widgets"
Name="SeparatedChatHud"
VerticalExpand="False"
VerticalAlignment="Bottom"
Expand All @@ -25,6 +26,7 @@
<BoxContainer Name="VoteMenu" Access="Public" Orientation="Vertical"/>
</BoxContainer>
<alerts:AlertsUI Name="Alerts" Access="Protected" />
<sss:SSSStatusGui Name="SSSStatus" Access="Protected" />
</LayoutContainer>
<PanelContainer HorizontalExpand="True" MinWidth="300">
<PanelContainer.PanelOverride>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ public SeparatedChatGameScreen()
SetAnchorAndMarginPreset(Hotbar, LayoutPreset.BottomWide, margin: 5);
SetAnchorAndMarginPreset(Alerts, LayoutPreset.CenterRight, margin: 10);

SetAnchorAndMarginPreset(SSSStatus, LayoutPreset.BottomRight, margin: 10);

ScreenContainer.OnSplitResizeFinished += () =>
OnChatResized?.Invoke(new Vector2(ScreenContainer.SplitFraction, 0));

Expand Down
253 changes: 253 additions & 0 deletions Content.Client/_SSS/UserInterface/SSSStatusUIController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
using Content.Client._SSS.UserInterface.Widgets;
using Content.Client.Gameplay;
using Content.Client.GameTicking.Managers;
using Content.Shared._SSS.SuspicionGameRule;
using Content.Shared._SSS.SuspicionGameRule.Components;
using Content.Shared.Damage;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Robust.Client.Player;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controllers;
using Robust.Shared.Timing;

namespace Content.Client._SSS.UserInterface;

public sealed class SSSStatusUIController : UIController, IOnSystemChanged<SSSStatusUISystem>, IOnStateChanged<GameplayState>
{
private ISawmill _log = default!;

[Dependency] private readonly IPlayerManager _playerManager = default!;

[UISystemDependency] private readonly MobThresholdSystem? _mobThreshold = default!;
[UISystemDependency] private readonly ClientGameTicker? _clientGameTicker = default!;

public override void Initialize()
{
base.Initialize();

_log = Logger.GetSawmill("StatusUI");

SubscribeNetworkEvent<SuspicionRuleTimerUpdate>(UpdateTimerEnd);
SubscribeNetworkEvent<SuspicionRulePreroundStarted>(PreroundStarted);
SubscribeNetworkEvent<SuspicionRuleUpdateRole>(UpdateRoleDisplay);
SubscribeNetworkEvent<SuspicionRulePlayerSpawn>(UpdatePlayerSpawn);
}

private TimeSpan _lastEndTime;
private TimeSpan _lastDrawnSeconds;

private (string, Color)? _queuedRole = null;
private (string, float, float)? _queuedHealth = null;

public override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);

// I LOVE THIS HACK I LOVE THIS HACK I LOVE THIS HACK I LOVE THIS HACK
SetRoleFromQueued();
SetHealthBarFromQueued();

// TODO: limit this to only update when the timer is not the same
if (_clientGameTicker is null)
return;

if (_lastEndTime == TimeSpan.MinValue)
return;

var drawTime = _lastEndTime.Subtract(_clientGameTicker.RoundDuration());
if ((int)drawTime.TotalSeconds != (int)_lastDrawnSeconds.TotalSeconds)
{
UpdateTimer(drawTime);
_lastDrawnSeconds = drawTime;
}
}

private SSSStatusGui? StatusUI => UIManager.GetActiveUIWidgetOrNull<SSSStatusGui>();

public void UpdateHealth(Entity<DamageableComponent> ent)
{

if (!EntityManager.TryGetComponent<MobStateComponent>(ent, out var mobState))
return;

if (mobState.CurrentState == MobState.Dead)
{
SetHealthBarUI(Loc.GetString("suspicion-status-ui-health-dead"), 0, 100);
return;
}

if (!EntityManager.TryGetComponent<MobThresholdsComponent>(ent, out var mobThresholds))
return;

if (_mobThreshold is null)
return;

var maxHp = _mobThreshold.GetThresholdForState(ent, MobState.Critical, mobThresholds);
var hp = Math.Max(Math.Ceiling((maxHp - ent.Comp.TotalDamage).Double()), 0);

SetHealthBar((float)hp, maxHp.Float());
}

private void SetHealthBarUI(string text, float value, float maxValue)
{
var ui = StatusUI;

if (ui == null)
{
_queuedHealth = (text, value, maxValue);
return;
}

ui.SetHealthBar(text, value, maxValue);
}

private bool SetHealthBarFromQueued()
{
if (_queuedHealth is null)
return false;

var ui = StatusUI;
if (ui is null)
return false;

var (text, value, maxValue) = _queuedHealth.Value;
ui.SetHealthBar(text, value, maxValue);
_queuedHealth = null;

return true;
}

private void SetHealthBar(float hp, float maxHp)
{
SetHealthBarUI($"\u2665 {hp}", hp, maxHp);
}

private void UpdateTimer(TimeSpan ts)
{
var ui = StatusUI;

if (ui == null)
return;

if (ts < TimeSpan.Zero)
{
ts = TimeSpan.Zero;
}

// nice job c#, TimeSpan.ToString doesn't support having no leading zeros
ui.TimerText.Text = $"{ts.Minutes}:{ts.Seconds:00}";
}

public void UpdateTimerEnd(SuspicionRuleTimerUpdate ev, EntitySessionEventArgs args)
{
_lastEndTime = ev.EndTime;
}

public void PreroundStarted(SuspicionRulePreroundStarted ev, EntitySessionEventArgs args)
{
_lastEndTime = ev.PreroundEndTime;
SetRoleToPreround();
}

private void SetRoleUI(string role, Color color)
{
var ui = StatusUI;

if (ui is null)
{
_queuedRole = (role, color);
return;
}

ui.SetRole(role, color);
}

private bool SetRoleFromQueued()
{
if (_queuedRole is null)
return false;

var ui = StatusUI;
if (ui is null)
return false;

var (role, color) = _queuedRole.Value;
ui.SetRole(role, color);
_queuedRole = null;

return true;
}

private void SetRoleToPreround()
{
SetRoleUI(Loc.GetString("suspicion-status-ui-role-preround"), Color.Gray);
}

private void SetRoleToObserbing()
{
SetRoleUI(Loc.GetString("suspicion-status-ui-role-obserbing"), Color.Gray);
}

public void UpdateRoleDisplay(SuspicionRuleUpdateRole ev, EntitySessionEventArgs args)
{
var roleName = Loc.GetString(ev.NewRole switch
{
SuspicionRole.Traitor => "roles-antag-suspicion-traitor-name",
SuspicionRole.Detective => "roles-antag-suspicion-detective-name",
SuspicionRole.Innocent => "roles-antag-suspicion-innocent-name",
_ => "roles-antag-suspicion-unknown",
});
SetRoleUI(roleName, Color.FromName(ev.NewRole.GetRoleColor()));
}

public void UpdatePlayerSpawn(SuspicionRulePlayerSpawn ev, EntitySessionEventArgs args)
{
if (ev.GameState == SuspicionGameState.Preparing)
{
SetRoleToPreround();

if (EntityManager.TryGetComponent<DamageableComponent>(_playerManager.LocalEntity!.Value, out var damagable))
UpdateHealth((_playerManager.LocalEntity!.Value, damagable));
}
else
{
SetRoleToObserbing();
SetHealthBar(0, 100);
}

_lastEndTime = ev.EndTime;
}

public void OnSystemLoaded(SSSStatusUISystem system)
{
system.OnPlayerDamageChanged += UpdateHealth;
}

public void OnSystemUnloaded(SSSStatusUISystem system)
{
system.OnPlayerDamageChanged -= UpdateHealth;
}

public void OnStateEntered(GameplayState state)
{
if (EntityManager.TryGetComponent<DamageableComponent>(_playerManager.LocalEntity!.Value, out var damagable))
UpdateHealth((_playerManager.LocalEntity!.Value, damagable));
else
SetHealthBar(0, 100);

UpdateTimer(TimeSpan.Zero);

SetRoleUI("-", Color.Black);
}

public void OnStateExited(GameplayState state)
{
SetHealthBarUI("-", 0, 100);

UpdateTimer(TimeSpan.Zero);

SetRoleUI("-", Color.Black);
}
}
28 changes: 28 additions & 0 deletions Content.Client/_SSS/UserInterface/SSSStatusUISystem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using Content.Shared.Damage;
using Robust.Shared.Player;

namespace Content.Client._SSS.UserInterface;

public sealed partial class SSSStatusUISystem : EntitySystem
{
[Dependency] private readonly ISharedPlayerManager _playerManager = default!;

public event Action<Entity<DamageableComponent>>? OnPlayerDamageChanged;

public override void Initialize()
{
base.Initialize();

SubscribeLocalEvent<DamageableComponent, DamageChangedEvent>(OnDamageChanged);
}

private void OnDamageChanged(Entity<DamageableComponent> ent, ref DamageChangedEvent args)
{
var (uid, damagable) = ent;

if (uid == _playerManager.LocalEntity)
{
OnPlayerDamageChanged?.Invoke(ent);
}
}
}
35 changes: 35 additions & 0 deletions Content.Client/_SSS/UserInterface/Widgets/SSSStatusGui.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<widgets:SSSStatusGui
xmlns="https://spacestation14.io"
xmlns:widgets="clr-namespace:Content.Client._SSS.UserInterface.Widgets"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
VerticalAlignment="Bottom"
HorizontalAlignment="Right"
>

<Control>

<PanelContainer StyleClasses="AngleRect" />
<BoxContainer Orientation="Vertical" Margin="8">
<BoxContainer Orientation="Horizontal">
<Control>
<PanelContainer Name="RoleBG" Access="Public">
</PanelContainer>
<Label Name="RoleText" Access="Public" Text="Traitor?" Margin="4"/>
</Control>
<Label Name="TimerText" Access="Public" Text="5:00" Align="Right" HorizontalExpand="True" />
</BoxContainer>
<Control MinSize="0 4" />
<Control MinSize="200 20">
<ProgressBar Name="HealthBar" Access="Public" Value="75" MaxValue="100">
<ProgressBar.ForegroundStyleBoxOverride>
<graphics:StyleBoxFlat BackgroundColor="#DE3A3A" />
</ProgressBar.ForegroundStyleBoxOverride>
</ProgressBar>
<Label Name="HealthNumber" Access="Public" Text="100" Align="Right" />
</Control>
</BoxContainer>

</Control>

</widgets:SSSStatusGui>
Loading

0 comments on commit 127199f

Please sign in to comment.