sandpypi/Sand.App/SandApp.Update.cs

333 lines
12 KiB
C#

using Raylib_cs;
using Sand.Core;
using System.Numerics;
using System.Diagnostics;
namespace Sand.App;
internal static partial class SandApp
{
private static void Update(AppState state)
{
var updateStart = Stopwatch.GetTimestamp();
var dt = Raylib.GetFrameTime();
var mouse = Raylib.GetMousePosition();
var worldRect = new Rectangle(SidebarWidth, 0, state.SimWidth * ParticleSize, state.SimHeight * ParticleSize);
var overSidebar = mouse.X < SidebarWidth;
var settingsRect = GetSettingsRect(state);
HandleHotkeys(state);
HandleMouseWheel(state, mouse, overSidebar);
HandleWorldPainting(state, mouse, worldRect, overSidebar, settingsRect);
HandleSidebarClick(state, mouse);
StepSimulation(state, dt, mouse);
state.PreviousMouse = mouse;
state.LastUpdateTimeMicroseconds = ToMicroseconds(updateStart, Stopwatch.GetTimestamp());
}
private static void HandleHotkeys(AppState state)
{
if (Raylib.IsKeyPressed(KeyboardKey.Space))
{
state.Settings.PauseSim = !state.Settings.PauseSim;
}
if (Raylib.IsKeyPressed(KeyboardKey.C))
{
state.Simulation.Clear();
}
if (Raylib.IsKeyPressed(KeyboardKey.Z))
{
state.ZoomEnabled = !state.ZoomEnabled;
}
if (Raylib.IsKeyPressed(KeyboardKey.F3))
{
state.Settings.EnableDebug = !state.Settings.EnableDebug;
}
if (Raylib.IsKeyPressed(KeyboardKey.Tab))
{
state.SettingsVisible = !state.SettingsVisible;
}
}
private static void HandleMouseWheel(AppState state, Vector2 mouse, bool overSidebar)
{
var wheel = Raylib.GetMouseWheelMove();
if (MathF.Abs(wheel) <= 0f)
{
return;
}
var settingsRect = GetSettingsRect(state);
if (state.SettingsVisible && Raylib.CheckCollisionPointRec(mouse, settingsRect))
{
var maxOffset = Math.Max(0, state.SettingItems.Length - GetVisibleSettingsRows(settingsRect));
state.SettingsScrollOffset = Math.Clamp(state.SettingsScrollOffset - (int)wheel, 0, maxOffset);
return;
}
var overCategoryList = overSidebar && mouse.Y >= 60 && mouse.Y < 240;
if (overCategoryList)
{
var maxOffset = Math.Max(0, state.Categories.Count - GetVisibleCategoryRows());
state.CategoryScrollOffset = Math.Clamp(state.CategoryScrollOffset - (int)wheel, 0, maxOffset);
return;
}
var overParticleList = overSidebar && mouse.Y >= 240 && mouse.Y <= Raylib.GetScreenHeight() - 164;
if (overParticleList)
{
var visibleRows = GetVisibleParticleRows();
var maxOffset = Math.Max(0, state.Categories[state.CurrentCategory].Count - visibleRows);
state.ParticleScrollOffset = Math.Clamp(state.ParticleScrollOffset - (int)wheel, 0, maxOffset);
return;
}
state.BrushRadius = Math.Clamp(state.BrushRadius + (int)wheel, 1, 16);
}
private static void HandleWorldPainting(AppState state, Vector2 mouse, Rectangle worldRect, bool overSidebar, Rectangle settingsRect)
{
var canPaintWorld =
!overSidebar &&
(!state.SettingsVisible || !Raylib.CheckCollisionPointRec(mouse, settingsRect)) &&
Raylib.CheckCollisionPointRec(mouse, worldRect);
state.PendingWorldAction = default;
if (Raylib.IsMouseButtonDown(MouseButton.Left) && canPaintWorld)
{
var simX = (int)((mouse.X - SidebarWidth) / ParticleSize) * ParticleSize;
var simY = (int)(mouse.Y / ParticleSize) * ParticleSize;
state.PendingWorldAction = new PendingWorldAction(true, false, simX, simY, mouse);
if (Raylib.IsMouseButtonPressed(MouseButton.Left))
{
ApplyWorldAction(state, state.PendingWorldAction, updateWindDirection: true);
}
}
if (Raylib.IsMouseButtonDown(MouseButton.Right) && canPaintWorld)
{
var simX = (int)((mouse.X - SidebarWidth) / ParticleSize) * ParticleSize;
var simY = (int)(mouse.Y / ParticleSize) * ParticleSize;
state.PendingWorldAction = new PendingWorldAction(true, true, simX, simY, mouse);
if (Raylib.IsMouseButtonPressed(MouseButton.Right))
{
ApplyWorldAction(state, state.PendingWorldAction, updateWindDirection: false);
}
}
}
private static void ApplyWorldAction(AppState state, PendingWorldAction action, bool updateWindDirection, bool continuous = false)
{
if (!action.Active)
{
return;
}
if (action.IsErase)
{
state.Simulation.ClearParticlePourAtPixel(action.SimX, action.SimY, state.BrushRadius, GetContinuousEraseCount(state), state.Simulation.Frame);
return;
}
if (updateWindDirection && state.CurrentParticle is "wind" or "air")
{
var drag = action.Mouse - state.PreviousMouse;
if (drag.LengthSquared() > 0.25f)
{
state.LastWindDirection = Vector2.Normalize(drag);
}
}
switch (state.CurrentParticle)
{
case "wind":
state.Simulation.ApplyWindBrushAtPixel(action.SimX, action.SimY, state.BrushRadius, state.LastWindDirection.X, state.LastWindDirection.Y);
break;
case "air":
state.Simulation.ApplyAirBrushAtPixel(action.SimX, action.SimY, state.BrushRadius, state.LastWindDirection.X, state.LastWindDirection.Y);
break;
case "gravity_well":
state.Simulation.ApplyGravityBrushAtPixel(action.SimX, action.SimY, state.BrushRadius, 3.5f);
break;
case "repulsor":
state.Simulation.ApplyRepulsorBrushAtPixel(action.SimX, action.SimY, state.BrushRadius, 3f);
break;
default:
if (continuous)
{
var pourCount = GetContinuousPaintCount(state);
state.Simulation.CreateParticlePourAtPixel(action.SimX, action.SimY, state.BrushRadius, state.CurrentParticle, pourCount, state.Simulation.Frame);
}
else
{
state.Simulation.CreateParticleCircle(action.SimX, action.SimY, state.BrushRadius, state.CurrentParticle);
}
break;
}
}
private static void HandleSidebarClick(AppState state, Vector2 mouse)
{
if (!Raylib.IsMouseButtonPressed(MouseButton.Left))
{
return;
}
HandleSidebar(state, mouse);
if (state.SettingsVisible)
{
HandleSettingsPanel(state, GetSettingsRect(state), mouse);
}
}
private static void StepSimulation(AppState state, float dt, Vector2 mouse)
{
var simStart = Stopwatch.GetTimestamp();
state.CategoryScrollOffset = Math.Clamp(state.CategoryScrollOffset, 0, Math.Max(0, state.Categories.Count - GetVisibleCategoryRows()));
state.ParticleScrollOffset = Math.Clamp(state.ParticleScrollOffset, 0, Math.Max(0, state.Categories[state.CurrentCategory].Count - GetVisibleParticleRows()));
state.SettingsScrollOffset = Math.Clamp(state.SettingsScrollOffset, 0, Math.Max(0, state.SettingItems.Length - GetVisibleSettingsRows(GetSettingsRect(state))));
state.LastSimulationStepCount = 0;
if (state.Settings.PauseSim)
{
if (state.PendingWorldAction.Active)
{
ApplyWorldAction(state, state.PendingWorldAction, updateWindDirection: true, continuous: ShouldUseContinuousPaint(state));
}
state.LastSimulationLoopTimeMicroseconds = ToMicroseconds(simStart, Stopwatch.GetTimestamp());
return;
}
state.SimulationAccumulator += dt;
var fixedStep = 1f / MathF.Max(1f, state.Settings.SimulationStepsPerSecond);
var overload = IsInteractiveOverloaded(state, fixedStep);
var maxSteps = overload ? 2 : 4;
while (state.SimulationAccumulator >= fixedStep && maxSteps-- > 0)
{
if (state.PendingWorldAction.Active)
{
ApplyWorldAction(state, state.PendingWorldAction with { Mouse = mouse }, updateWindDirection: true, continuous: ShouldUseContinuousPaint(state));
}
state.Simulation.Step(fixedStep * state.Settings.TimeScale);
state.LastSimulationStepCount++;
state.SimulationAccumulator -= fixedStep;
}
if (state.SimulationAccumulator > fixedStep * (overload ? 1.5f : 3f))
{
state.SimulationAccumulator = fixedStep;
}
state.LastSimulationLoopTimeMicroseconds = ToMicroseconds(simStart, Stopwatch.GetTimestamp());
}
private static Rectangle GetSettingsRect(AppState state)
{
return new Rectangle(
Raylib.GetScreenWidth() - SettingsWidth - 12,
100,
SettingsWidth,
MathF.Min(32 + (state.SettingItems.Length * 34), Raylib.GetScreenHeight() - 120));
}
private static int GetVisibleSettingsRows(Rectangle panelRect)
{
return Math.Max(1, (int)((panelRect.Height - 68f) / 34f));
}
private static bool ShouldUseContinuousPaint(AppState state)
{
if (state.CurrentParticle is "wind" or "air" or "gravity_well" or "repulsor")
{
return true;
}
var typeId = state.Library.GetTypeId(state.CurrentParticle);
var particle = state.Library.GetDefinition(typeId);
return particle.Kind == ParticleKind.Gas
|| particle.Kind == ParticleKind.Liquid
|| (particle.Kind == ParticleKind.Solid && !particle.IsStatic);
}
private static int GetContinuousPaintCount(AppState state)
{
var typeId = state.Library.GetTypeId(state.CurrentParticle);
var particle = state.Library.GetDefinition(typeId);
var baseCount = GetContinuousPaintCount(particle, state.BrushRadius);
return ApplyInteractiveLoadScaling(state, baseCount, particle.Kind == ParticleKind.Gas);
}
internal static int GetContinuousPaintCount(ParticleDef particle, int brushRadius)
{
return particle.Kind switch
{
ParticleKind.Gas => Math.Clamp(brushRadius + 1, 1, 8),
ParticleKind.Liquid => Math.Clamp((brushRadius * 2) + 1, 1, 24),
ParticleKind.Solid when !particle.IsStatic => Math.Clamp((brushRadius * 2) + 1, 1, 24),
_ => 0,
};
}
private static int GetContinuousEraseCount(AppState state)
{
var baseCount = Math.Clamp((state.BrushRadius * 2) + 2, 1, 18);
return ApplyInteractiveLoadScaling(state, baseCount, gasBiased: true);
}
private static bool IsInteractiveOverloaded(AppState state, float fixedStep)
{
var stepBudgetMicros = fixedStep * 1_000_000f;
if (state.LastSimulationStepCount >= 4)
{
return true;
}
if (state.LastSimulationLoopTimeMicroseconds > stepBudgetMicros * 2.25f)
{
return true;
}
return state.LastAppFrameTimeMicroseconds > 40_000;
}
private static int ApplyInteractiveLoadScaling(AppState state, int baseCount, bool gasBiased)
{
if (baseCount <= 1)
{
return baseCount;
}
var severeOverload =
state.LastSimulationStepCount >= 4 ||
state.LastSimulationLoopTimeMicroseconds > 45_000 ||
state.LastAppFrameTimeMicroseconds > 60_000;
if (severeOverload)
{
return gasBiased
? Math.Max(1, baseCount / 4)
: Math.Max(1, baseCount / 3);
}
var overload =
state.LastSimulationStepCount >= 2 ||
state.LastSimulationLoopTimeMicroseconds > 25_000 ||
state.LastAppFrameTimeMicroseconds > 33_000;
if (overload)
{
return gasBiased
? Math.Max(1, baseCount / 2)
: Math.Max(1, (baseCount * 2) / 3);
}
return baseCount;
}
}