Rework the Adrenaline system

This commit is contained in:
Ritchie Frodomar 2024-11-29 23:09:06 -05:00
parent fb06178b0b
commit c97724967b
No known key found for this signature in database
GPG key ID: 47384A02C174B15F
8 changed files with 242 additions and 85 deletions

View file

@ -452,7 +452,7 @@ public class Typeface : Font
if (fontSize <= 12)
{
// Disable anti-aliasing for low font sizes to make them less blurry.
flags = FT_LOAD.FT_LOAD_MONOCHROME | FT_LOAD.FT_LOAD_FORCE_AUTOHINT;
flags = FT_LOAD.FT_LOAD_MONOCHROME;
}
else
{

View file

@ -204,7 +204,7 @@ namespace SociallyDistant.Core.Core.Scripting
if (GiveFlowBonuses)
{
var charactersTypedPerMinute = (int) Math.Floor(scriptText.Length / typingSlowness);
var charactersTypedPerMinute = (int) Math.Floor(scriptText.Length / (typingSlowness / 60));
this.potentialFlow += charactersTypedPerMinute;
}

View file

@ -5,6 +5,7 @@ using AcidicGUI.VisualStyles;
using AcidicGUI.Widgets;
using Microsoft.Xna.Framework;
using SociallyDistant.Core.UI.Visuals;
using SociallyDistant.Player;
namespace SociallyDistant.Applets;
@ -44,8 +45,10 @@ public sealed class FlowMeterWidget : Widget,
this.InvalidateGeometry();
}
public void SetAdrenaline(bool isInAdrenaline)
public void SetAdrenaline(AdrenalineState adrenalineState)
{
var isInAdrenaline = adrenalineState == AdrenalineState.HighEnergy || adrenalineState == AdrenalineState.Adrenaline;
if (this.visualState.AdrenalineMode == isInAdrenaline)
return;

View file

@ -1,5 +1,7 @@
using System.Runtime.InteropServices;
using Microsoft.Xna.Framework;
using AcidicGUI.Layout;
using AcidicGUI.TextRendering;
using AcidicGUI.Widgets;
using SociallyDistant.Core.UI.Recycling.SettingsWidgets;
using SociallyDistant.Player;
using SociallyDistant.UI;
@ -11,9 +13,15 @@ namespace SociallyDistant.Applets;
internal sealed class FlowMeter : IStatusApplet
{
private readonly PlayerState playerSTate;
private readonly FlowMeterWidget widget = new();
private readonly StackPanel root = new();
private readonly StackPanel indicators = new();
private readonly TextWidget earnIndicator = new();
private readonly TextWidget lossIndicator = new();
private readonly FlowMeterWidget widget = new();
private IDisposable? flowObserver;
private IDisposable? adrenalineObserver;
private IDisposable? lossObserver;
private IDisposable? earnObserver;
public IAppletSlot? CurrentSlot { get; set; }
@ -21,15 +29,38 @@ internal sealed class FlowMeter : IStatusApplet
public FlowMeter(PlayerState playerSTate)
{
root.Direction = Direction.Horizontal;
indicators.Direction = Direction.Vertical;
earnIndicator.TextColorOverride = Color.Cyan;
lossIndicator.TextColorOverride = Color.Magenta;
earnIndicator.FontSize = 8;
lossIndicator.FontSize = 8;
earnIndicator.FontFamily = PresetFontFamily.Monospace;
lossIndicator.FontFamily = PresetFontFamily.Monospace;
root.Spacing = 3;
indicators.MinimumWidth = 40;
indicators.MaximumWidth = indicators.MinimumWidth;
root.ChildWidgets.Add(widget);
root.ChildWidgets.Add(indicators);
indicators.ChildWidgets.Add(earnIndicator);
indicators.ChildWidgets.Add(lossIndicator);
this.playerSTate = playerSTate;
}
public void Build(IAppletSlot slot)
{
this.CurrentSlot = slot;
this.CurrentSlot.Content = widget;
this.CurrentSlot.Content = root;
flowObserver = playerSTate.FlowPercentageObservable.Subscribe(widget.SetFlow);
adrenalineObserver = playerSTate.AdrenalineObservable.Subscribe(widget.SetAdrenaline);
earnObserver = playerSTate.FlowEarnedObservable.Subscribe(OnFlowEarned);
lossObserver = playerSTate.FlowLossObservable.Subscribe(OnFlowLost);
}
public void Update()
@ -42,6 +73,24 @@ internal sealed class FlowMeter : IStatusApplet
this.CurrentSlot = null;
flowObserver?.Dispose();
adrenalineObserver?.Dispose();
earnObserver?.Dispose();
lossObserver?.Dispose();
}
private void OnFlowEarned(long amount)
{
this.earnIndicator.Text = $"+ {amount}";
this.earnIndicator.RenderOpacity = amount == 0
? 0
: 1;
}
private void OnFlowLost(long amount)
{
this.lossIndicator.Text = $"+ {amount}";
this.lossIndicator.RenderOpacity = amount == 0
? 0
: 1;
}
}

View file

@ -142,9 +142,12 @@ namespace SociallyDistant.Core
world.Wipe();
}
internal void UpdateWorldClock()
internal void UpdateWorldClock(bool adrenaline)
{
float clockDelta = Time.DeltaTime * World.GlobalWorldState.Value.TimeScale;
if (adrenaline)
clockDelta *= 0.5f;
if (clockDelta == 0)
return;

View file

@ -4,20 +4,40 @@ using SociallyDistant.Core.Core.Events;
namespace SociallyDistant.Player;
public enum AdrenalineState : byte
{
NoFlow,
LowEnergy,
Adrenaline,
HighEnergy
}
internal sealed class PlayerState
{
private readonly Subject<float> flowPercentageSubject = new();
private readonly Subject<bool> flowStateSubject = new();
private readonly uint flowNeededForAdrenaline = 100;
private uint flow;
private bool inPerfectFlow;
private float timeLeftInAdrenaline = 0;
private float timeSpentAfterAdrenaline;
private float timeUntilNormalDecay = 0;
private readonly Subject<float> flowPercentageSubject = new();
private readonly Subject<AdrenalineState> flowStateSubject = new();
private readonly Subject<long> flowLossSubject = new();
private readonly Subject<long> flowEarnSibject = new();
private const long flowNeededForAdrenaline = 1000;
private const int flowPerSecond = 200;
private long flowEarned;
private long flowLost;
private long flow;
private AdrenalineState inPerfectFlow;
private float timeLeftInAdrenaline = 0;
private float timeSpentAfterAdrenaline;
private float timeUntilNormalDecay = 0;
private float timeSinceFlowEarn = 0;
private float timeUntilAdrenaline;
public IObservable<bool> AdrenalineObservable => flowStateSubject;
public IObservable<long> FlowEarnedObservable => flowEarnSibject;
public IObservable<long> FlowLossObservable => flowLossSubject;
public IObservable<AdrenalineState> AdrenalineObservable => flowStateSubject;
public IObservable<float> FlowPercentageObservable => flowPercentageSubject;
public bool IsHoppedUpOnAdrenaline => inPerfectFlow == AdrenalineState.Adrenaline || inPerfectFlow == AdrenalineState.HighEnergy;
public AdrenalineState Adrenaline => inPerfectFlow;
public PlayerState()
{
EventBus.Listen<FlowEvent>(OnFlowEvent);
@ -28,99 +48,181 @@ internal sealed class PlayerState
var amount = (uint) Math.Abs(flowEvent.FlowToAdd);
var isPenalty = Math.Sign(flowEvent.FlowToAdd) == -1;
if (!isPenalty && this.inPerfectFlow)
var isAdrenaline = inPerfectFlow == AdrenalineState.Adrenaline || inPerfectFlow == AdrenalineState.HighEnergy;
if (!isPenalty && isAdrenaline)
amount *= 2;
if (isPenalty)
{
var newAmount = (long) flow - amount;
if (newAmount < 0)
{
this.flow = 0;
this.inPerfectFlow = false;
}
else
{
flow = (uint)newAmount;
}
if (!isAdrenaline)
flowLost += amount;
}
else
{
flow += amount;
timeUntilNormalDecay = 5;
if (!inPerfectFlow && flow >= flowNeededForAdrenaline)
{
timeLeftInAdrenaline = 60;
inPerfectFlow = true;
}
flowEarned = Math.Min(flowEarned + amount, flowNeededForAdrenaline);
}
SendUpdates();
}
private void SendUpdates()
{
float flowPercentage = MathHelper.Clamp((float)flow / flowNeededForAdrenaline, 0, 1);
this.flowEarnSibject.OnNext(flowEarned);
this.flowLossSubject.OnNext(flowLost);
this.flowPercentageSubject.OnNext(flowPercentage);
this.flowStateSubject.OnNext(inPerfectFlow);
}
public void Update(float deltaTime)
{
if (inPerfectFlow)
switch (inPerfectFlow)
{
if (timeLeftInAdrenaline > 0)
case AdrenalineState.Adrenaline:
{
timeSpentAfterAdrenaline = 0;
timeLeftInAdrenaline -= deltaTime;
return;
}
// Once the adrenaline timer ends, we steadily decay flow until adrenaline actually
// depletes. Every 10 seconds, the decay gets faster. This allows the player to keep
// the adrenaline pumping for a while if they keep earning flow, but eventually it'll
// always run out.
timeSpentAfterAdrenaline += deltaTime / 10f;
if (timeUntilAdrenaline <= 0)
{
inPerfectFlow = AdrenalineState.HighEnergy;
break;
}
var newFlow = (long)flow - (int) Math.Floor(timeSpentAfterAdrenaline);
if (newFlow <= 0)
timeUntilAdrenaline -= deltaTime;
break;
}
case AdrenalineState.HighEnergy:
{
flow = 0;
inPerfectFlow = false;
timeSpentAfterAdrenaline = 0;
// You cannot lose flow when in Adrenaline.
flowLost = 0;
// Any flow earned while in Adrenaline gets doubled an extends Adrenaline.
if (flowEarned > 0)
{
timeSinceFlowEarn += deltaTime * flowPerSecond * 2;
if (timeSinceFlowEarn >= 1)
{
int intervalsPassed = (int) Math.Floor(timeSinceFlowEarn);
long flowEarnedNow = Math.Min(flowEarned, intervalsPassed);
timeSinceFlowEarn -= intervalsPassed;
SendUpdates();
return;
flowEarned -= flowEarnedNow;
flow += flowEarnedNow;
// Only extend Adrenaline if we still have time left. Otherwise, the flow increase will give a small boost
// even though Adrenaline is depleted.
if (timeLeftInAdrenaline > 0)
timeLeftInAdrenaline += ((float)flowEarnedNow / (float)flowPerSecond);
SendUpdates();
}
}
if (timeLeftInAdrenaline > 0)
{
timeSpentAfterAdrenaline = 0;
timeLeftInAdrenaline -= deltaTime;
return;
}
// Once the adrenaline timer ends, we steadily decay flow until adrenaline actually
// depletes. Every 10 seconds, the decay gets faster. This allows the player to keep
// the adrenaline pumping for a while if they keep earning flow, but eventually it'll
// always run out.
timeSpentAfterAdrenaline += deltaTime / 10f;
var newFlow = (long)flow - (int)Math.Floor(timeSpentAfterAdrenaline);
if (newFlow <= 0)
{
flow = 0;
inPerfectFlow = AdrenalineState.NoFlow;
timeSpentAfterAdrenaline = 0;
SendUpdates();
return;
}
else
{
flow = (uint)newFlow;
SendUpdates();
}
break;
}
else
default:
{
flow = (uint)newFlow;
SendUpdates();
}
}
else
{
if (timeUntilNormalDecay > 0)
{
timeUntilNormalDecay -= deltaTime;
return;
}
// Decay at a rate of 5 per second
var newFlow = (long)flow - (int)Math.Ceiling(deltaTime * 5);
if (newFlow <= 0)
{
flow = 0;
SendUpdates();
return;
}
else
{
flow = (uint)newFlow;
SendUpdates();
if (flowLost > 0)
{
if (flowEarned > 0)
{
long newBalance = flowEarned - flowLost;
flowEarned = Math.Max(flowEarned - flowLost, 0);
long debt = Math.Abs(flowEarned - newBalance);
flowLost = debt;
}
var flowLostNow = (long)Math.Floor((double)flowLost * (deltaTime * flowPerSecond));
if (flowLostNow <= 0)
flowLostNow = flowLost;
flowLost -= flowLostNow;
flow -= flowLostNow;
if (flow < 0)
{
flow = 0;
flowLost = 0;
}
SendUpdates();
}
if (flowEarned > 0)
{
timeSinceFlowEarn += deltaTime * flowPerSecond;
if (timeSinceFlowEarn >= 1)
{
int intervalsPassed = (int) Math.Floor(timeSinceFlowEarn);
long flowEarnedNow = Math.Min(flowEarned, intervalsPassed);
timeSinceFlowEarn -= intervalsPassed;
flowEarned -= flowEarnedNow;
flow += flowEarnedNow;
if (flow >= flowNeededForAdrenaline && inPerfectFlow != AdrenalineState.Adrenaline)
{
timeUntilAdrenaline = 1;
inPerfectFlow = AdrenalineState.Adrenaline;
timeSpentAfterAdrenaline = 0;
timeLeftInAdrenaline = 60;
}
timeUntilNormalDecay = 5;
SendUpdates();
}
}
if (timeUntilNormalDecay > 0)
{
timeUntilNormalDecay -= deltaTime;
return;
}
// Decay at a rate of 5 per second
var newFlow = (long)flow - (int)Math.Ceiling(deltaTime * 5);
if (newFlow <= 0)
{
flow = 0;
SendUpdates();
return;
}
else
{
flow = (uint)newFlow;
SendUpdates();
}
break;
}
}
}

View file

@ -562,7 +562,7 @@ internal sealed class SociallyDistantGame :
{
debugOverlay.UpdateKeyboard();
worldManager.UpdateWorldClock();
worldManager.UpdateWorldClock(playerManager.PlayerStateInternal.IsHoppedUpOnAdrenaline);
virtualScreen?.Activate();
// Run any scheduled actions

View file

@ -150,9 +150,9 @@ public class GuiController : GameComponent,
{
statusApplets.Add(userApplet);
statusApplets.Add(new WorkspaceSwitcher(this, floatingWindowArea));
statusApplets.Add(new SpacerApplet());
statusApplets.Add(new FlowMeter(playerManager.PlayerStateInternal));
statusApplets.Add(new SpacerApplet());
statusApplets.Add(new SpacerApplet());
statusApplets.Add(new SystemTrayApplet(trayModel));