improved particle animations, took some inspiration from brio and character + kinda pogged at their change log

This commit is contained in:
choco
2025-10-16 01:29:38 +02:00
parent d5c12e81c3
commit 823dd39a9b
6 changed files with 191 additions and 390 deletions

View File

@@ -171,10 +171,4 @@ changelog:
- "Fixed owners being visible in moderator list view."
- "Removed Pin/Remove/Ban buttons on Owners when viewing as moderator."
- "Fixed nameplate bug in PvP."
- "Added 1 or 3 day options for inactive check."
- name: "Template"
tagline: ""
date: "October 15th 2025"
is_current: false
message: "Thank you for using Lightless Sync!\n\nThis update brings quality of life improvements and polish to the user experience.\nWe're committed to helping you share your character with others seamlessly.\n\nIf you have any suggestions or encounter any issues, please let us know on Discord or GitHub!\n\n- The Lightless Team"
- "Added 1 or 3 day options for inactive check."

View File

@@ -1 +0,0 @@
[Add contributor names - GitHub handles, etc.]

View File

@@ -1,2 +0,0 @@
UI design inspired by Brio's update window (Etheirys/Brio). Thanks to their team for the great UX ideas.
Special thanks to the Dalamud team and the XIV modding ecosystem for tooling & APIs.

View File

@@ -1 +0,0 @@
[Your Names Here]

View File

@@ -6,7 +6,6 @@ using LightlessSync.LightlessConfiguration;
using LightlessSync.Services;
using LightlessSync.Services.Mediator;
using Microsoft.Extensions.Logging;
using System.IO;
using System.Numerics;
using System.Reflection;
using System.Text;
@@ -16,20 +15,15 @@ using Dalamud.Interface;
namespace LightlessSync.UI;
// Inspiration taken from Brio and Character Select+ (goats)
public class UpdateNotesUi : WindowMediatorSubscriberBase
{
private readonly LightlessConfigService _configService;
private readonly UiSharedService _uiShared;
private readonly List<string> _contributors = [];
private readonly List<string> _credits = [];
private readonly List<string> _supporters = [];
private ChangelogFile _changelog = new();
private bool _scrollToTop;
private int _selectedTab;
// Particle system for visual effects
private struct Particle
{
public Vector2 Position;
@@ -37,27 +31,29 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
public float Life;
public float MaxLife;
public float Size;
public Vector4 Color;
public ParticleType Type;
public float Rotation;
public float RotationSpeed;
public List<Vector2>? Trail;
public bool IsLargeMoon;
public float Twinkle;
public float Depth;
public float Hue;
}
private enum ParticleType
{
Star,
Moon,
Sparkle,
FastFallingStar
TwinklingStar,
ShootingStar
}
private readonly List<Particle> _particles = [];
private float _particleTimer;
private readonly Random _particleRandom = new();
private Particle? _largeMoon;
private float _largeMoonTimer;
private float _particleSpawnTimer;
private readonly Random _random = new();
private const float HeaderHeight = 150f;
private const float ParticleSpawnInterval = 0.2f;
private const int MaxParticles = 50;
private const int MaxTrailLength = 50;
private const float EdgeFadeDistance = 30f;
private const float ExtendedParticleHeight = 40f;
public UpdateNotesUi(ILogger<UpdateNotesUi> logger,
LightlessMediator mediator,
@@ -66,7 +62,6 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
PerformanceCollectorService performanceCollectorService)
: base(logger, mediator, "Lightless Sync — Update Notes", performanceCollectorService)
{
_configService = configService;
_uiShared = uiShared;
AllowClickthrough = false;
@@ -106,20 +101,20 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
var windowPos = ImGui.GetWindowPos();
var windowPadding = ImGui.GetStyle().WindowPadding;
var headerWidth = (800f * ImGuiHelpers.GlobalScale) - (windowPadding.X * 2);
var headerHeight = 140f * ImGuiHelpers.GlobalScale;
var headerStart = windowPos + new Vector2(windowPadding.X, windowPadding.Y);
var headerEnd = headerStart + new Vector2(headerWidth, headerHeight);
var headerEnd = headerStart + new Vector2(headerWidth, HeaderHeight);
var headerSize = new Vector2(headerWidth, HeaderHeight);
var extendedParticleSize = new Vector2(headerWidth, HeaderHeight + ExtendedParticleHeight);
DrawGradientBackground(headerStart, headerEnd);
DrawParticleEffects(headerStart, new Vector2(headerWidth, headerHeight));
DrawHeaderText(headerStart);
DrawHeaderButtons(headerStart, headerWidth);
DrawBottomGradient(headerStart, headerEnd, headerWidth);
ImGui.SetCursorPosY(windowPadding.Y + headerHeight + 5);
// Version badge with icon
ImGui.SetCursorPosX(12);
ImGui.SetCursorPosY(windowPadding.Y + HeaderHeight + 5);
ImGui.SetCursorPosX(20);
using (ImRaii.PushFont(UiBuilder.IconFont))
{
ImGui.TextColored(UIColors.Get("LightlessGreen"), FontAwesomeIcon.Star.ToIconString());
@@ -140,16 +135,15 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
ImGui.TextColored(new Vector4(0.65f, 0.65f, 0.75f, 1.0f), $" {_changelog.Subline}");
}
}
ImGui.Separator();
ImGuiHelpers.ScaledDummy(3);
DrawParticleEffects(headerStart, extendedParticleSize);
}
private void DrawGradientBackground(Vector2 headerStart, Vector2 headerEnd)
{
var drawList = ImGui.GetWindowDrawList();
// Dark night sky background with stars pattern
var darkPurple = new Vector4(0.08f, 0.05f, 0.15f, 1.0f);
var deepPurple = new Vector4(0.12f, 0.08f, 0.20f, 1.0f);
@@ -162,8 +156,7 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
ImGui.GetColorU32(deepPurple)
);
// Add some static "distant stars" for depth
var random = new Random(42); // Fixed seed for consistent pattern
var random = new Random(42);
for (int i = 0; i < 50; i++)
{
var starPos = headerStart + new Vector2(
@@ -173,27 +166,44 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
var brightness = 0.3f + (float)random.NextDouble() * 0.4f;
drawList.AddCircleFilled(starPos, 1f, ImGui.GetColorU32(new Vector4(1f, 1f, 1f, brightness)));
}
}
private void DrawBottomGradient(Vector2 headerStart, Vector2 headerEnd, float width)
{
var drawList = ImGui.GetWindowDrawList();
var gradientHeight = 60f;
// Accent border at bottom with glow
drawList.AddLine(
new Vector2(headerStart.X, headerEnd.Y),
headerEnd,
ImGui.GetColorU32(UIColors.Get("LightlessPurple")),
2f
);
for (int i = 0; i < gradientHeight; i++)
{
var progress = i / gradientHeight;
var smoothProgress = progress * progress;
var r = 0.12f + (0.0f - 0.12f) * smoothProgress;
var g = 0.08f + (0.0f - 0.08f) * smoothProgress;
var b = 0.20f + (0.0f - 0.20f) * smoothProgress;
var alpha = 1f - smoothProgress;
var gradientColor = new Vector4(r, g, b, alpha);
drawList.AddLine(
new Vector2(headerStart.X, headerEnd.Y + i),
new Vector2(headerStart.X + width, headerEnd.Y + i),
ImGui.GetColorU32(gradientColor),
1f
);
}
}
private void DrawHeaderText(Vector2 headerStart)
{
// Title text overlay - drawn after particles so it's on top
ImGui.SetCursorScreenPos(headerStart + new Vector2(20, 30));
var textX = 20f;
var textY = 30f;
ImGui.SetCursorScreenPos(headerStart + new Vector2(textX, textY));
using (_uiShared.UidFont.Push())
{
ImGui.TextColored(new Vector4(0.95f, 0.95f, 0.95f, 1.0f), "Lightless Sync");
}
ImGui.SetCursorScreenPos(headerStart + new Vector2(20, 75));
ImGui.SetCursorScreenPos(headerStart + new Vector2(textX, textY + 45f));
ImGui.TextColored(UIColors.Get("LightlessBlue"), "Update Notes");
}
@@ -203,8 +213,6 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
var spacing = 8f * ImGuiHelpers.GlobalScale;
var rightPadding = 15f * ImGuiHelpers.GlobalScale;
var topPadding = 15f * ImGuiHelpers.GlobalScale;
// Position for buttons in top right
var buttonY = headerStart.Y + topPadding;
var gitButtonX = headerStart.X + headerWidth - rightPadding - buttonSize.X;
var discordButtonX = gitButtonX - buttonSize.X - spacing;
@@ -239,372 +247,202 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
private void DrawParticleEffects(Vector2 bannerStart, Vector2 bannerSize)
{
var deltaTime = ImGui.GetIO().DeltaTime;
_particleTimer += deltaTime;
_largeMoonTimer += deltaTime;
_particleSpawnTimer += deltaTime;
// Spawn new particles
if (_particleTimer > 0.3f && _particles.Count < 30)
if (_particleSpawnTimer > ParticleSpawnInterval && _particles.Count < MaxParticles)
{
SpawnParticle(bannerStart, bannerSize);
_particleTimer = 0f;
SpawnParticle(bannerSize);
_particleSpawnTimer = 0f;
}
// Spawn or update large moon
if (_largeMoon == null || _largeMoonTimer > 45f)
if (_random.NextDouble() < 0.003)
{
SpawnLargeMoon(bannerStart, bannerSize);
_largeMoonTimer = 0f;
SpawnShootingStar(bannerSize);
}
var drawList = ImGui.GetWindowDrawList();
// Update and draw large moon first (background layer)
if (_largeMoon != null)
{
var moon = _largeMoon.Value;
moon.Position += moon.Velocity * deltaTime;
moon.Life -= deltaTime;
// Keep moon within banner bounds with padding
var padding = moon.Size + 10;
if (moon.Life <= 0 ||
moon.Position.X < bannerStart.X - padding ||
moon.Position.X > bannerStart.X + bannerSize.X + padding ||
moon.Position.Y < bannerStart.Y - padding ||
moon.Position.Y > bannerStart.Y + bannerSize.Y + padding)
{
_largeMoon = null;
}
else
{
float alpha = Math.Min(1f, moon.Life / moon.MaxLife);
var color = moon.Color with { W = moon.Color.W * alpha };
DrawMoon(drawList, moon.Position, moon.Size, color);
_largeMoon = moon;
}
}
// Update and draw regular particles
for (int i = _particles.Count - 1; i >= 0; i--)
{
var particle = _particles[i];
// Update trail for stars
if ((particle.Type == ParticleType.Star || particle.Type == ParticleType.FastFallingStar) && particle.Trail != null)
var screenPos = bannerStart + particle.Position;
if (particle.Type == ParticleType.ShootingStar && particle.Trail != null)
{
particle.Trail.Insert(0, particle.Position);
var maxTrailLength = particle.Type == ParticleType.FastFallingStar ? 15 : 8;
if (particle.Trail.Count > maxTrailLength)
if (particle.Trail.Count > MaxTrailLength)
particle.Trail.RemoveAt(particle.Trail.Count - 1);
}
if (particle.Type == ParticleType.TwinklingStar)
{
particle.Twinkle += 0.005f * particle.Depth;
}
particle.Position += particle.Velocity * deltaTime;
particle.Life -= deltaTime;
particle.Rotation += particle.RotationSpeed * deltaTime;
if (particle.Life <= 0 ||
particle.Position.X > bannerStart.X + bannerSize.X + 50 ||
particle.Position.X < bannerStart.X - 50 ||
particle.Position.Y < bannerStart.Y - 50 ||
particle.Position.Y > bannerStart.Y + bannerSize.Y + 50)
var isOutOfBounds = particle.Position.X < -50 || particle.Position.X > bannerSize.X + 50 ||
particle.Position.Y < -50 || particle.Position.Y > bannerSize.Y + 50;
if (particle.Life <= 0 || (particle.Type != ParticleType.TwinklingStar && isOutOfBounds))
{
_particles.RemoveAt(i);
continue;
}
float alpha = Math.Min(1f, particle.Life / particle.MaxLife);
var color = particle.Color with { W = particle.Color.W * alpha };
// Draw trail for stars
if ((particle.Type == ParticleType.Star || particle.Type == ParticleType.FastFallingStar) && particle.Trail != null && particle.Trail.Count > 1)
if (particle.Type == ParticleType.TwinklingStar)
{
if (particle.Position.X < 0 || particle.Position.X > bannerSize.X)
particle.Velocity = particle.Velocity with { X = -particle.Velocity.X };
if (particle.Position.Y < 0 || particle.Position.Y > bannerSize.Y)
particle.Velocity = particle.Velocity with { Y = -particle.Velocity.Y };
}
var fadeIn = Math.Min(1f, (particle.MaxLife - particle.Life) / 20f);
var fadeOut = Math.Min(1f, particle.Life / 20f);
var lifeFade = Math.Min(fadeIn, fadeOut);
var edgeFadeX = Math.Min(
Math.Min(1f, (particle.Position.X + EdgeFadeDistance) / EdgeFadeDistance),
Math.Min(1f, (bannerSize.X - particle.Position.X + EdgeFadeDistance) / EdgeFadeDistance)
);
var edgeFadeY = Math.Min(
Math.Min(1f, (particle.Position.Y + EdgeFadeDistance) / EdgeFadeDistance),
Math.Min(1f, (bannerSize.Y - particle.Position.Y + EdgeFadeDistance) / EdgeFadeDistance)
);
var edgeFade = Math.Min(edgeFadeX, edgeFadeY);
var baseAlpha = lifeFade * edgeFade;
var finalAlpha = particle.Type == ParticleType.TwinklingStar
? baseAlpha * (0.6f + 0.4f * MathF.Sin(particle.Twinkle))
: baseAlpha;
if (particle.Type == ParticleType.ShootingStar && particle.Trail != null && particle.Trail.Count > 1)
{
var cyanColor = new Vector4(0.4f, 0.8f, 1.0f, 1.0f);
for (int t = 1; t < particle.Trail.Count; t++)
{
float trailAlpha = alpha * (1f - (t / (float)particle.Trail.Count)) * (particle.Type == ParticleType.FastFallingStar ? 0.7f : 0.5f);
var trailColor = color with { W = trailAlpha };
float thickness = particle.Type == ParticleType.FastFallingStar ? 2.5f : 1.5f;
var trailProgress = (float)t / particle.Trail.Count;
var trailAlpha = Math.Min(1f, (1f - trailProgress) * finalAlpha * 1.8f);
var trailWidth = (1f - trailProgress) * 3f + 1f;
var glowAlpha = trailAlpha * 0.4f;
drawList.AddLine(
particle.Trail[t - 1],
particle.Trail[t],
ImGui.GetColorU32(trailColor),
thickness
bannerStart + particle.Trail[t - 1],
bannerStart + particle.Trail[t],
ImGui.GetColorU32(cyanColor with { W = glowAlpha }),
trailWidth + 4f
);
drawList.AddLine(
bannerStart + particle.Trail[t - 1],
bannerStart + particle.Trail[t],
ImGui.GetColorU32(cyanColor with { W = trailAlpha }),
trailWidth
);
}
}
// Draw based on particle type
switch (particle.Type)
else if (particle.Type == ParticleType.TwinklingStar)
{
case ParticleType.Star:
case ParticleType.FastFallingStar:
DrawStar(drawList, particle.Position, particle.Size, color, particle.Rotation);
break;
case ParticleType.Moon:
DrawMoon(drawList, particle.Position, particle.Size, color);
break;
case ParticleType.Sparkle:
DrawSparkle(drawList, particle.Position, particle.Size, color, particle.Rotation);
break;
DrawTwinklingStar(drawList, screenPos, particle.Size, particle.Hue, finalAlpha, particle.Depth);
}
_particles[i] = particle;
}
}
private void DrawStar(ImDrawListPtr drawList, Vector2 position, float size, Vector4 color, float rotation)
private void DrawTwinklingStar(ImDrawListPtr drawList, Vector2 position, float size, float hue, float alpha, float depth)
{
// Draw a 5-pointed star
var points = new Vector2[10];
for (int i = 0; i < 10; i++)
{
float angle = (i * MathF.PI / 5) + rotation;
float radius = (i % 2 == 0) ? size : size * 0.4f;
points[i] = position + new Vector2(MathF.Cos(angle) * radius, MathF.Sin(angle) * radius);
}
var color = HslToRgb(hue, 1.0f, 0.85f);
color.W = alpha;
// Draw filled star
for (int i = 0; i < 5; i++)
{
drawList.AddTriangleFilled(
position,
points[i * 2],
points[(i * 2 + 2) % 10],
ImGui.GetColorU32(color)
);
}
// Glow effect
var glowColor = color with { W = color.W * 0.3f };
drawList.AddCircleFilled(position, size * 1.5f, ImGui.GetColorU32(glowColor));
}
private void DrawMoon(ImDrawListPtr drawList, Vector2 position, float size, Vector4 color)
{
// Enhanced glow for larger moons
var glowRadius = size > 15f ? 2.5f : 1.8f;
var glowColor = color with { W = color.W * (size > 15f ? 0.15f : 0.25f) };
drawList.AddCircleFilled(position, size * glowRadius, ImGui.GetColorU32(glowColor));
// Draw crescent moon
drawList.AddCircleFilled(position, size, ImGui.GetColorU32(color));
// Draw shadow circle to create crescent
var shadowColor = new Vector4(0.08f, 0.05f, 0.15f, 1.0f);
drawList.AddCircleFilled(position + new Vector2(size * 0.4f, 0), size * 0.8f, ImGui.GetColorU32(shadowColor));
// Additional glow layer for large moons
if (size > 15f)
{
var outerGlow = color with { W = color.W * 0.08f };
drawList.AddCircleFilled(position, size * 3.5f, ImGui.GetColorU32(outerGlow));
}
var glowColor = color with { W = alpha * 0.3f };
drawList.AddCircleFilled(position, size * (1.2f + depth * 0.3f), ImGui.GetColorU32(glowColor));
}
private void DrawSparkle(ImDrawListPtr drawList, Vector2 position, float size, Vector4 color, float rotation)
private static Vector4 HslToRgb(float h, float s, float l)
{
// Draw a 4-pointed sparkle (plus shape)
var thickness = size * 0.3f;
h = h / 360f;
float c = (1 - MathF.Abs(2 * l - 1)) * s;
float x = c * (1 - MathF.Abs((h * 6) % 2 - 1));
float m = l - c / 2;
// Horizontal line
drawList.AddLine(
position + new Vector2(-size, 0),
position + new Vector2(size, 0),
ImGui.GetColorU32(color),
thickness
);
float r, g, b;
if (h < 1f / 6f) { r = c; g = x; b = 0; }
else if (h < 2f / 6f) { r = x; g = c; b = 0; }
else if (h < 3f / 6f) { r = 0; g = c; b = x; }
else if (h < 4f / 6f) { r = 0; g = x; b = c; }
else if (h < 5f / 6f) { r = x; g = 0; b = c; }
else { r = c; g = 0; b = x; }
// Vertical line
drawList.AddLine(
position + new Vector2(0, -size),
position + new Vector2(0, size),
ImGui.GetColorU32(color),
thickness
);
// Center glow
drawList.AddCircleFilled(position, size * 0.4f, ImGui.GetColorU32(color));
var glowColor = color with { W = color.W * 0.4f };
drawList.AddCircleFilled(position, size * 1.2f, ImGui.GetColorU32(glowColor));
return new Vector4(r + m, g + m, b + m, 1.0f);
}
private void SpawnParticle(Vector2 bannerStart, Vector2 bannerSize)
private void SpawnParticle(Vector2 bannerSize)
{
var typeRoll = _particleRandom.Next(100);
var particleType = typeRoll switch
{
< 35 => ParticleType.Star,
< 50 => ParticleType.Moon,
< 65 => ParticleType.FastFallingStar,
_ => ParticleType.Sparkle
};
var position = new Vector2(
(float)_random.NextDouble() * bannerSize.X,
(float)_random.NextDouble() * bannerSize.Y
);
Vector2 position;
Vector2 velocity;
var depthLayers = new[] { 0.5f, 1.0f, 1.5f };
var depth = depthLayers[_random.Next(depthLayers.Length)];
// Stars: spawn from top, move diagonally down
if (particleType == ParticleType.Star)
{
// Spawn from top edge
position = new Vector2(
bannerStart.X + (float)_particleRandom.NextDouble() * bannerSize.X,
bannerStart.Y - 10
);
// Move diagonally down (shooting star effect)
var angle = MathF.PI * 0.25f + (float)(_particleRandom.NextDouble() - 0.5) * 0.5f; // 45° ± variation
var speed = 30f + (float)_particleRandom.NextDouble() * 40f;
velocity = new Vector2(MathF.Cos(angle) * speed, MathF.Sin(angle) * speed);
}
// Fast falling stars: spawn from top, fall straight down very fast
else if (particleType == ParticleType.FastFallingStar)
{
// Spawn from top edge, random X position
position = new Vector2(
bannerStart.X + (float)_particleRandom.NextDouble() * bannerSize.X,
bannerStart.Y - 10
);
// Fall almost straight down with slight horizontal drift
var horizontalDrift = -10f + (float)_particleRandom.NextDouble() * 20f;
var speed = 120f + (float)_particleRandom.NextDouble() * 80f; // Much faster!
velocity = new Vector2(horizontalDrift, speed);
}
// Moons: drift slowly across
else if (particleType == ParticleType.Moon)
{
// Spawn from left side
position = new Vector2(
bannerStart.X - 10,
bannerStart.Y + (float)_particleRandom.NextDouble() * bannerSize.Y
);
// Drift slowly to the right with slight vertical movement
velocity = new Vector2(
15f + (float)_particleRandom.NextDouble() * 10f,
-5f + (float)_particleRandom.NextDouble() * 10f
);
}
// Sparkles: float gently
else
{
position = new Vector2(
bannerStart.X + (float)_particleRandom.NextDouble() * bannerSize.X,
bannerStart.Y + (float)_particleRandom.NextDouble() * bannerSize.Y
);
velocity = new Vector2(
-5f + (float)_particleRandom.NextDouble() * 10f,
-5f + (float)_particleRandom.NextDouble() * 10f
);
}
var velocity = new Vector2(
((float)_random.NextDouble() - 0.5f) * 0.05f * depth,
((float)_random.NextDouble() - 0.5f) * 0.05f * depth
);
var particle = new Particle
var isBlue = _random.NextDouble() < 0.5;
var hue = isBlue ? 220f + (float)_random.NextDouble() * 30f : 270f + (float)_random.NextDouble() * 40f;
var size = (0.5f + (float)_random.NextDouble() * 2f) * depth;
var maxLife = 120f + (float)_random.NextDouble() * 60f;
_particles.Add(new Particle
{
Position = position,
Velocity = velocity,
MaxLife = particleType switch
{
ParticleType.Star => 3f + (float)_particleRandom.NextDouble() * 2f,
ParticleType.Moon => 8f + (float)_particleRandom.NextDouble() * 4f,
ParticleType.FastFallingStar => 1.5f + (float)_particleRandom.NextDouble() * 1f,
_ => 6f + (float)_particleRandom.NextDouble() * 4f
},
Size = particleType switch
{
ParticleType.Star => 2.5f + (float)_particleRandom.NextDouble() * 2f,
ParticleType.Moon => 3f + (float)_particleRandom.NextDouble() * 2f,
ParticleType.FastFallingStar => 3f + (float)_particleRandom.NextDouble() * 2f,
_ => 2f + (float)_particleRandom.NextDouble() * 2f
},
Color = particleType switch
{
ParticleType.Star => new Vector4(1.0f, 1.0f, 0.9f, 0.9f),
ParticleType.Moon => UIColors.Get("LightlessBlue") with { W = 0.7f },
ParticleType.FastFallingStar => new Vector4(1.0f, 0.95f, 0.85f, 1.0f), // Bright white-yellow
_ => UIColors.Get("LightlessPurple") with { W = 0.8f }
},
Type = particleType,
Rotation = (float)_particleRandom.NextDouble() * MathF.PI * 2,
RotationSpeed = particleType == ParticleType.Star || particleType == ParticleType.FastFallingStar ? 2f : -0.5f + (float)_particleRandom.NextDouble() * 1f,
Trail = particleType == ParticleType.Star || particleType == ParticleType.FastFallingStar ? new List<Vector2>() : null,
IsLargeMoon = false
};
particle.Life = particle.MaxLife;
_particles.Add(particle);
}
private void SpawnLargeMoon(Vector2 bannerStart, Vector2 bannerSize)
{
// Large moon travels across the banner like a celestial body
var spawnSide = _particleRandom.Next(4);
Vector2 position;
Vector2 velocity;
switch (spawnSide)
{
case 0:
// Spawn from left, move to right
position = new Vector2(
bannerStart.X - 50,
bannerStart.Y + (float)_particleRandom.NextDouble() * bannerSize.Y
);
velocity = new Vector2(
15f + (float)_particleRandom.NextDouble() * 10f,
-5f + (float)_particleRandom.NextDouble() * 10f
);
break;
case 1:
// Spawn from top, move down and across
position = new Vector2(
bannerStart.X + (float)_particleRandom.NextDouble() * bannerSize.X,
bannerStart.Y - 50
);
velocity = new Vector2(
-5f + (float)_particleRandom.NextDouble() * 10f,
10f + (float)_particleRandom.NextDouble() * 8f
);
break;
case 2:
// Spawn from right, move to left
position = new Vector2(
bannerStart.X + bannerSize.X + 50,
bannerStart.Y + (float)_particleRandom.NextDouble() * bannerSize.Y
);
velocity = new Vector2(
-(15f + (float)_particleRandom.NextDouble() * 10f),
-5f + (float)_particleRandom.NextDouble() * 10f
);
break;
default:
// Spawn from top-left corner, move diagonally
position = new Vector2(
bannerStart.X - 30,
bannerStart.Y - 30
);
velocity = new Vector2(
12f + (float)_particleRandom.NextDouble() * 8f,
12f + (float)_particleRandom.NextDouble() * 8f
);
break;
}
_largeMoon = new Particle
{
Position = position,
Velocity = velocity,
MaxLife = 40f,
Life = 40f,
Size = 25f + (float)_particleRandom.NextDouble() * 10f,
Color = UIColors.Get("LightlessBlue") with { W = 0.35f },
Type = ParticleType.Moon,
Rotation = 0,
RotationSpeed = 0,
Life = maxLife,
MaxLife = maxLife,
Size = size,
Type = ParticleType.TwinklingStar,
Trail = null,
IsLargeMoon = true
};
Twinkle = (float)_random.NextDouble() * MathF.PI * 2,
Depth = depth,
Hue = hue
});
}
private void SpawnShootingStar(Vector2 bannerSize)
{
var maxLife = 80f + (float)_random.NextDouble() * 40f;
var startX = bannerSize.X * (0.3f + (float)_random.NextDouble() * 0.6f);
var startY = -10f;
_particles.Add(new Particle
{
Position = new Vector2(startX, startY),
Velocity = new Vector2(
-50f - (float)_random.NextDouble() * 40f,
30f + (float)_random.NextDouble() * 40f
),
Life = maxLife,
MaxLife = maxLife,
Size = 2.5f,
Type = ParticleType.ShootingStar,
Trail = new List<Vector2>(),
Twinkle = 0,
Depth = 1.0f,
Hue = 270f
});
}
private void DrawCloseButton()
{
@@ -653,7 +491,7 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
{
var currentColor = entry.IsCurrent == true
? UIColors.Get("LightlessGreen")
: new Vector4(0.75f, 0.75f, 0.85f, 1.0f);
: new Vector4(0.95f, 0.95f, 1.0f, 1.0f);
bool isOpen;
var flags = entry.IsCurrent == true
@@ -670,7 +508,7 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
}
ImGui.SameLine();
ImGui.TextColored(new Vector4(0.75f, 0.75f, 0.85f, 1.0f), $" — {entry.Tagline}");
ImGui.TextColored(new Vector4(0.85f, 0.85f, 0.95f, 1.0f), $" — {entry.Tagline}");
if (!isOpen)
return;
@@ -711,11 +549,9 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
var backgroundMin = startPos + new Vector2(-8, -4);
var backgroundMax = startPos + new Vector2(availableWidth + 8, 28);
// Background with subtle gradient
var bgColor = new Vector4(0.12f, 0.12f, 0.15f, 0.7f);
drawList.AddRectFilled(backgroundMin, backgroundMax, ImGui.GetColorU32(bgColor), 6f);
// Accent line on left
drawList.AddRectFilled(
backgroundMin,
backgroundMin + new Vector2(4, backgroundMax.Y - backgroundMin.Y),
@@ -723,7 +559,6 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
3f
);
// Subtle glow effect
var glowColor = accentColor with { W = 0.15f };
drawList.AddRect(
backgroundMin,
@@ -745,11 +580,6 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
try
{
var assembly = Assembly.GetExecutingAssembly();
ReadLines(assembly, "LightlessSync.UI.Changelog.supporters.txt", _supporters);
ReadLines(assembly, "LightlessSync.UI.Changelog.contributors.txt", _contributors);
ReadLines(assembly, "LightlessSync.UI.Changelog.credits.txt", _credits);
using var changelogStream = assembly.GetManifestResourceStream("LightlessSync.UI.Changelog.changelog.yaml");
if (changelogStream != null)
{
@@ -767,22 +597,6 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
// Ignore - window will gracefully render with defaults
}
}
private static void ReadLines(Assembly assembly, string resourceName, List<string> target)
{
using var stream = assembly.GetManifestResourceStream(resourceName);
if (stream == null)
return;
using var reader = new StreamReader(stream, Encoding.UTF8, true, 128);
string? line;
while ((line = reader.ReadLine()) != null)
{
if (!string.IsNullOrWhiteSpace(line))
target.Add(line.Trim());
}
}
private sealed record ChangelogFile
{
public string Tagline { get; init; } = string.Empty;