Damage masks (#9402)

This commit is contained in:
metalgearsloth
2022-07-06 17:58:14 +10:00
committed by GitHub
parent 0bbdd0c1a3
commit a2a5df1990
55 changed files with 1160 additions and 930 deletions

View File

@@ -187,8 +187,6 @@ namespace Content.Client.Entry
var overlayMgr = IoCManager.Resolve<IOverlayManager>();
overlayMgr.AddOverlay(new ParallaxOverlay());
overlayMgr.AddOverlay(new SingularityOverlay());
overlayMgr.AddOverlay(new CritOverlay()); //Hopefully we can cut down on this list... don't see why a death overlay needs to be instantiated here.
overlayMgr.AddOverlay(new CircleMaskOverlay());
overlayMgr.AddOverlay(new FlashOverlay());
overlayMgr.AddOverlay(new RadiationPulseOverlay());

View File

@@ -1,4 +1,5 @@
using Content.Client.IoC;
using Content.Client.MobState;
using Content.Client.Resources;
using Content.Shared.Damage;
using Content.Shared.FixedPoint;
@@ -86,11 +87,12 @@ namespace Content.Client.HealthOverlay.UI
return;
}
var mobStateSystem = _entities.EntitySysManager.GetEntitySystem<MobStateSystem>();
FixedPoint2 threshold;
if (mobState.IsAlive())
{
if (!mobState.TryGetEarliestCriticalState(damageable.TotalDamage, out _, out threshold))
if (!mobStateSystem.TryGetEarliestCriticalState(mobState, damageable.TotalDamage, out _, out threshold))
{
CritBar.Visible = false;
HealthBar.Visible = false;
@@ -107,8 +109,8 @@ namespace Content.Client.HealthOverlay.UI
HealthBar.Ratio = 0;
HealthBar.Visible = false;
if (!mobState.TryGetPreviousCriticalState(damageable.TotalDamage, out _, out var critThreshold) ||
!mobState.TryGetEarliestDeadState(damageable.TotalDamage, out _, out var deadThreshold))
if (!mobStateSystem.TryGetPreviousCriticalState(mobState, damageable.TotalDamage, out _, out var critThreshold) ||
!mobStateSystem.TryGetEarliestDeadState(mobState, damageable.TotalDamage, out _, out var deadThreshold))
{
CritBar.Visible = false;
return;

View File

@@ -0,0 +1,132 @@
using Content.Shared.Damage;
using Content.Shared.FixedPoint;
using Content.Shared.MobState;
using Content.Shared.MobState.Components;
using Content.Shared.MobState.EntitySystems;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Shared.GameStates;
namespace Content.Client.MobState;
public sealed partial class MobStateSystem : SharedMobStateSystem
{
[Dependency] private readonly IPlayerManager _playerManager = default!;
private Overlays.DamageOverlay _overlay = default!;
public const short Levels = 7;
public override void Initialize()
{
base.Initialize();
_overlay = new Overlays.DamageOverlay();
IoCManager.Resolve<IOverlayManager>().AddOverlay(_overlay);
SubscribeLocalEvent<PlayerAttachedEvent>(OnPlayerAttach);
SubscribeLocalEvent<PlayerDetachedEvent>(OnPlayerDetach);
SubscribeLocalEvent<MobStateComponent, ComponentHandleState>(OnMobHandleState);
}
public override void Shutdown()
{
base.Shutdown();
IoCManager.Resolve<IOverlayManager>().RemoveOverlay(_overlay);
}
private void OnMobHandleState(EntityUid uid, MobStateComponent component, ref ComponentHandleState args)
{
if (args.Current is not MobStateComponentState state) return;
if (component.CurrentThreshold == state.CurrentThreshold)
return;
if (state.CurrentThreshold == null)
{
RemoveState(component);
}
else
{
UpdateState(component, state.CurrentThreshold.Value);
}
}
private void OnPlayerAttach(PlayerAttachedEvent ev)
{
if (TryComp<MobStateComponent>(ev.Entity, out var mobState) && TryComp<DamageableComponent>(ev.Entity, out var damageable))
{
SetLevel(mobState, damageable.TotalDamage);
}
}
private void OnPlayerDetach(PlayerDetachedEvent ev)
{
_overlay.State = DamageState.Alive;
_overlay.BruteLevel = 0f;
_overlay.OxygenLevel = 0f;
_overlay.CritLevel = 0f;
}
protected override void UpdateState(MobStateComponent component, DamageState? state, FixedPoint2 threshold)
{
base.UpdateState(component, state, threshold);
SetLevel(component, threshold);
}
private void SetLevel(MobStateComponent stateComponent, FixedPoint2 threshold)
{
var uid = stateComponent.Owner;
if (_playerManager.LocalPlayer?.ControlledEntity != uid) return;
_overlay.State = DamageState.Alive;
_overlay.BruteLevel = 0f;
_overlay.OxygenLevel = 0f;
_overlay.CritLevel = 0f;
if (!TryComp<DamageableComponent>(uid, out var damageable))
return;
switch (stateComponent.CurrentState)
{
case DamageState.Dead:
_overlay.State = DamageState.Dead;
return;
}
var bruteLevel = 0f;
var oxyLevel = 0f;
var critLevel = 0f;
if (TryGetEarliestIncapacitatedState(stateComponent, threshold, out _, out var earliestThreshold) && damageable.TotalDamage != 0)
{
if (damageable.DamagePerGroup.TryGetValue("Brute", out var bruteDamage))
{
bruteLevel = MathF.Min(1f, (bruteDamage / earliestThreshold).Float());
}
if (damageable.Damage.DamageDict.TryGetValue("Asphyxiation", out var oxyDamage))
{
oxyLevel = MathF.Min(1f, (oxyDamage / earliestThreshold).Float());
}
if (threshold >= earliestThreshold && TryGetEarliestDeadState(stateComponent, threshold, out _, out var earliestDeadHold))
{
critLevel = (float) Math.Clamp((damageable.TotalDamage - earliestThreshold).Double() / (earliestDeadHold - earliestThreshold).Double(), 0.1, 1);
}
}
// Don't show damage overlay if they're near enough to max.
if (bruteLevel < 0.05f)
{
bruteLevel = 0f;
}
_overlay.State = critLevel > 0f ? DamageState.Critical : DamageState.Alive;
_overlay.BruteLevel = bruteLevel;
_overlay.OxygenLevel = oxyLevel;
_overlay.CritLevel = critLevel;
}
}

View File

@@ -1,36 +0,0 @@
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Shared.Enums;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
namespace Content.Client.MobState.Overlays
{
public sealed class CircleMaskOverlay : Overlay
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
public override OverlaySpace Space => OverlaySpace.WorldSpace;
private readonly ShaderInstance _shader;
public CircleMaskOverlay()
{
IoCManager.InjectDependencies(this);
_shader = _prototypeManager.Index<ShaderPrototype>("CircleMask").Instance();
}
protected override void Draw(in OverlayDrawArgs args)
{
if (!CritOverlay.LocalPlayerHasState(_playerManager, false, true))
return;
var worldHandle = args.WorldHandle;
worldHandle.UseShader(_shader);
var viewport = args.WorldAABB;
worldHandle.DrawRect(viewport, Color.White);
}
}
}

View File

@@ -1,58 +0,0 @@
using Content.Shared.MobState.Components;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
namespace Content.Client.MobState.Overlays
{
public sealed class CritOverlay : Overlay
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IEntityManager _entities = default!;
public override OverlaySpace Space => OverlaySpace.WorldSpace;
private readonly ShaderInstance _gradientCircleShader;
public CritOverlay()
{
IoCManager.InjectDependencies(this);
_gradientCircleShader = _prototypeManager.Index<ShaderPrototype>("GradientCircleMask").Instance();
}
public static bool LocalPlayerHasState(IPlayerManager pm, bool critical, bool dead, IEntityManager? entities = null) {
if (pm.LocalPlayer?.ControlledEntity is not {Valid: true} player)
{
return false;
}
IoCManager.Resolve(ref entities);
if (entities.TryGetComponent<MobStateComponent?>(player, out var mobState))
{
if (critical)
if (mobState.IsCritical())
return true;
if (dead)
if (mobState.IsDead())
return true;
}
return false;
}
protected override void Draw(in OverlayDrawArgs args)
{
if (!LocalPlayerHasState(_playerManager, true, false, _entities))
return;
var worldHandle = args.WorldHandle;
var viewport = args.WorldAABB;
worldHandle.UseShader(_gradientCircleShader);
worldHandle.DrawRect(viewport, Color.White);
}
}
}

View File

@@ -0,0 +1,224 @@
using Content.Shared.MobState;
using Robust.Client.Graphics;
using Robust.Shared.Enums;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
namespace Content.Client.MobState.Overlays;
public sealed class DamageOverlay : Overlay
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
public override OverlaySpace Space => OverlaySpace.ScreenSpace;
private readonly ShaderInstance _critShader;
private readonly ShaderInstance _oxygenShader;
private readonly ShaderInstance _bruteShader;
public DamageState State = DamageState.Alive;
/// <summary>
/// Handles the red pulsing overlay
/// </summary>
public float BruteLevel = 0f;
private float _oldBruteLevel = 0f;
/// <summary>
/// Handles the darkening overlay.
/// </summary>
public float OxygenLevel = 0f;
private float _oldOxygenLevel = 0f;
/// <summary>
/// Handles the white overlay when crit.
/// </summary>
public float CritLevel = 0f;
private float _oldCritLevel = 0f;
private float _deadLevel = 1f;
public DamageOverlay()
{
// TODO: Replace
IoCManager.InjectDependencies(this);
_oxygenShader = _prototypeManager.Index<ShaderPrototype>("GradientCircleMask").InstanceUnique();
_critShader = _prototypeManager.Index<ShaderPrototype>("GradientCircleMask").InstanceUnique();
_bruteShader = _prototypeManager.Index<ShaderPrototype>("GradientCircleMask").InstanceUnique();
}
protected override void Draw(in OverlayDrawArgs args)
{
/*
* Here's the rundown:
* 1. There's lerping for each level so the transitions are smooth.
* 2. There's 3 overlays, 1 for brute damage, 1 for oxygen damage (that also doubles as a crit overlay),
* and a white one during crit that closes in as you progress towards death. When you die it slowly disappears.
* The crit overlay also occasionally reduces its alpha as a "blink"
*/
var viewport = args.ViewportBounds;
var handle = args.ScreenHandle;
var distance = args.ViewportBounds.Width;
var lerpRate = 0.2f;
var time = (float) _timing.RealTime.TotalSeconds;
var lastFrameTime = (float) _timing.FrameTime.TotalSeconds;
// If they just died then lerp out the white overlay.
if (State != DamageState.Dead)
{
_deadLevel = 1f;
}
else if (!_deadLevel.Equals(0f))
{
var diff = -_deadLevel;
_deadLevel += GetDiff(diff, lerpRate, lastFrameTime);
}
if (!_oldBruteLevel.Equals(BruteLevel))
{
var diff = BruteLevel - _oldBruteLevel;
_oldBruteLevel += GetDiff(diff, lerpRate, lastFrameTime);
}
if (!_oldOxygenLevel.Equals(OxygenLevel))
{
var diff = OxygenLevel - _oldOxygenLevel;
_oldOxygenLevel += GetDiff(diff, lerpRate, lastFrameTime);
}
if (!_oldCritLevel.Equals(CritLevel))
{
var diff = CritLevel - _oldCritLevel;
_oldCritLevel += GetDiff(diff, lerpRate, lastFrameTime);
}
/*
* darknessAlphaOuter is the maximum alpha for anything outside of the larger circle
* darknessAlphaInner (on the shader) is the alpha for anything inside the smallest circle
*
* outerCircleRadius is what we end at for max level for the outer circle
* outerCircleMaxRadius is what we start at for 0 level for the outer circle
*
* innerCircleRadius is what we end at for max level for the inner circle
* innerCircleMaxRadius is what we start at for 0 level for the inner circle
*/
// Makes debugging easier don't @ me
float level = 0f;
level = _oldBruteLevel;
// TODO: Lerping
if (level > 0f && _oldCritLevel <= 0f)
{
var pulseRate = 3f;
var adjustedTime = time * pulseRate;
float outerMaxLevel = 2.0f * distance;
float outerMinLevel = 0.8f * distance;
float innerMaxLevel = 0.6f * distance;
float innerMinLevel = 0.2f * distance;
var outerRadius = outerMaxLevel - level * (outerMaxLevel - outerMinLevel);
var innerRadius = innerMaxLevel - level * (innerMaxLevel - innerMinLevel);
var pulse = MathF.Max(0f, MathF.Sin(adjustedTime));
_bruteShader.SetParameter("time", pulse);
_bruteShader.SetParameter("color", new Vector3(1f, 0f, 0f));
_bruteShader.SetParameter("darknessAlphaOuter", 0.8f);
_bruteShader.SetParameter("outerCircleRadius", outerRadius);
_bruteShader.SetParameter("outerCircleMaxRadius", outerRadius + 0.2f * distance);
_bruteShader.SetParameter("innerCircleRadius", innerRadius);
_bruteShader.SetParameter("innerCircleMaxRadius", innerRadius + 0.02f * distance);
handle.UseShader(_bruteShader);
handle.DrawRect(viewport, Color.White);
}
else
{
_oldBruteLevel = BruteLevel;
}
level = State != DamageState.Critical ? _oldOxygenLevel : 1f;
if (level > 0f)
{
float outerMaxLevel = 0.6f * distance;
float outerMinLevel = 0.06f * distance;
float innerMaxLevel = 0.02f * distance;
float innerMinLevel = 0.02f * distance;
var outerRadius = outerMaxLevel - level * (outerMaxLevel - outerMinLevel);
var innerRadius = innerMaxLevel - level * (innerMaxLevel - innerMinLevel);
float outerDarkness;
float critTime;
// If in crit then just fix it; also pulse it very occasionally so they can see more.
if (_oldCritLevel > 0f)
{
var adjustedTime = time * 2f;
critTime = MathF.Max(0, MathF.Sin(adjustedTime) + 2 * MathF.Sin(2 * adjustedTime / 4f) + MathF.Sin(adjustedTime / 4f) - 3f);
if (critTime > 0f)
{
outerDarkness = 1f - critTime / 1.5f;
}
else
{
outerDarkness = 1f;
}
}
else
{
outerDarkness = MathF.Min(0.98f, 0.3f * MathF.Log(level) + 1f);
}
_oxygenShader.SetParameter("time", 0.0f);
_oxygenShader.SetParameter("color", new Vector3(0f, 0f, 0f));
_oxygenShader.SetParameter("darknessAlphaOuter", outerDarkness);
_oxygenShader.SetParameter("innerCircleRadius", innerRadius);
_oxygenShader.SetParameter("innerCircleMaxRadius", innerRadius);
_oxygenShader.SetParameter("outerCircleRadius", outerRadius);
_oxygenShader.SetParameter("outerCircleMaxRadius", outerRadius + 0.2f * distance);
handle.UseShader(_oxygenShader);
handle.DrawRect(viewport, Color.White);
}
level = State != DamageState.Dead ? _oldCritLevel : _deadLevel;
if (level > 0f)
{
float outerMaxLevel = 2.0f * distance;
float outerMinLevel = 1.0f * distance;
float innerMaxLevel = 0.6f * distance;
float innerMinLevel = 0.02f * distance;
var outerRadius = outerMaxLevel - level * (outerMaxLevel - outerMinLevel);
var innerRadius = innerMaxLevel - level * (innerMaxLevel - innerMinLevel);
var pulse = MathF.Max(0f, MathF.Sin(time));
// If in crit then just fix it; also pulse it very occasionally so they can see more.
_critShader.SetParameter("time", pulse);
_critShader.SetParameter("color", new Vector3(1f, 1f, 1f));
_critShader.SetParameter("darknessAlphaOuter", 1.0f);
_critShader.SetParameter("innerCircleRadius", innerRadius);
_critShader.SetParameter("innerCircleMaxRadius", innerRadius + 0.005f * distance);
_critShader.SetParameter("outerCircleRadius", outerRadius);
_critShader.SetParameter("outerCircleMaxRadius", outerRadius + 0.2f * distance);
handle.UseShader(_critShader);
handle.DrawRect(viewport, Color.White);
}
}
private float GetDiff(float value, float lerpRate, float lastFrameTime)
{
return Math.Clamp(value, -1 * lerpRate * lastFrameTime, lerpRate * lastFrameTime);
}
}

View File

@@ -1,8 +0,0 @@
using Content.Shared.MobState.State;
namespace Content.Client.MobState.States
{
public sealed class CriticalMobState : SharedCriticalMobState
{
}
}

View File

@@ -1,19 +0,0 @@
using Content.Shared.MobState;
using Content.Shared.MobState.State;
using Robust.Shared.GameObjects;
namespace Content.Client.MobState.States
{
public sealed class DeadMobState : SharedDeadMobState
{
public override void EnterState(EntityUid uid, IEntityManager entityManager)
{
base.EnterState(uid, entityManager);
if (entityManager.TryGetComponent(uid, out AppearanceComponent? appearance))
{
appearance.SetData(DamageStateVisuals.State, DamageState.Dead);
}
}
}
}

View File

@@ -1,11 +0,0 @@
using Content.Shared.MobState;
using Content.Shared.MobState.State;
using Robust.Client.GameObjects;
using Robust.Shared.GameObjects;
namespace Content.Client.MobState.States
{
public sealed class NormalMobState : SharedNormalMobState
{
}
}

View File

@@ -1,24 +1,14 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;
using JetBrains.Annotations;
using Nett;
using Content.Shared;
using Content.Shared.CCVar;
using Content.Client.Resources;
using Content.Client.IoC;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Utility;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Utility;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;