Repository: ddnet/ddnet
Pull request #9898: Fix switch door twitching (and refactor)
Author: login: AssassinTee
Created: 2025-03-20T18:20:24Z | Merged: 2025-03-25T17:09:54Z

<!-- What is the motivation for the changes of this pull request? -->

<!-- Note that builds and other checks will be run for your change. Don't feel intimidated by failures in some of the checks. If you can't resolve them yourself, experienced devs can also resolve them before merging your pull request. -->

Decouples Laser-Rendering from Laser object and introduces various optimizations. Some variables are moved into their context and the decoupling creates a lower complexity.

The door-twitch-fixing is removed by setting a fixed tick, since the state of the door is indicated differently anyway.

Also allows a followup for #9057

## Checklist

- [x] Tested the change ingame
- [x] Provided screenshots if it is a visual change
- [x] Tested in combination with possibly related configuration options
- [ ] Written a unit test (especially base/) or added coverage to integration test
- [ ] Considered possible null pointers and out of bounds array indexing
- [x] Changed no physics that affect existing maps
- [ ] Tested the change with [ASan+UBSan or valgrind's memcheck](https://github.com/ddnet/ddnet/#using-addresssanitizer--undefinedbehavioursanitizer-or-valgrinds-memcheck) (optional)



Diff:
diff --git a/src/game/client/components/items.cpp b/src/game/client/components/items.cpp
index 85186fa4b88..433f47ca501 100644
--- a/src/game/client/components/items.cpp
+++ b/src/game/client/components/items.cpp
@@ -243,12 +243,6 @@ void CItems::RenderFlag(const CNetObj_Flag *pPrev, const CNetObj_Flag *pCurrent,
 void CItems::RenderLaser(const CLaserData *pCurrent, bool IsPredicted)
 {
 	int Type = clamp(pCurrent->m_Type, -1, NUM_LASERTYPES - 1);
-
-	ColorRGBA RGB;
-	vec2 Pos = pCurrent->m_To;
-	vec2 From = pCurrent->m_From;
-	float Len = distance(Pos, From);
-
 	int ColorIn, ColorOut;
 	switch(Type)
 	{
@@ -287,45 +281,50 @@ void CItems::RenderLaser(const CLaserData *pCurrent, bool IsPredicted)
 		ColorIn = g_Config.m_ClLaserRifleInnerColor;
 	}
 
-	RGB = color_cast<ColorRGBA>(ColorHSLA(ColorOut));
-	ColorRGBA OuterColor(RGB.r, RGB.g, RGB.b, 1.0f);
-	RGB = color_cast<ColorRGBA>(ColorHSLA(ColorIn));
-	ColorRGBA InnerColor(RGB.r, RGB.g, RGB.b, 1.0f);
-
-	int TuneZone = GameClient()->m_GameWorld.m_WorldConfig.m_UseTuneZones ? Collision()->IsTune(Collision()->GetMapIndex(From)) : 0;
 	bool IsOtherTeam = (pCurrent->m_ExtraInfo && pCurrent->m_Owner >= 0 && m_pClient->IsOtherTeam(pCurrent->m_Owner));
 
-	float Alpha = 1.f;
-	if(IsOtherTeam)
+	float Alpha = IsOtherTeam ? g_Config.m_ClShowOthersAlpha / 100.0f : 1.f;
+
+	const ColorRGBA OuterColor = color_cast<ColorRGBA>(ColorHSLA(ColorOut).WithAlpha(Alpha));
+	const ColorRGBA InnerColor = color_cast<ColorRGBA>(ColorHSLA(ColorIn).WithAlpha(Alpha));
+
+	float Ticks;
+	if(Type == LASERTYPE_DOOR)
 	{
-		Alpha = g_Config.m_ClShowOthersAlpha / 100.0f;
+		Ticks = 1.0f;
+	}
+	else if(IsPredicted)
+	{
+		int PredictionTick = Client()->GetPredictionTick();
+		Ticks = (float)(PredictionTick - pCurrent->m_StartTick) + Client()->PredIntraGameTick(g_Config.m_ClDummy);
 	}
+	else
+		Ticks = (float)(Client()->GameTick(g_Config.m_ClDummy) - pCurrent->m_StartTick) + Client()->IntraGameTick(g_Config.m_ClDummy);
+
+	float TicksHead = Client()->GameTick(g_Config.m_ClDummy);
+	RenderLaser(pCurrent->m_From, pCurrent->m_To, OuterColor, InnerColor, Ticks, TicksHead, Type);
+}
+
+void CItems::RenderLaser(vec2 From, vec2 Pos, ColorRGBA OuterColor, ColorRGBA InnerColor, float TicksBody, float TicksHead, int Type)
+{
+	int TuneZone = (Client()->State() == IClient::STATE_ONLINE && GameClient()->m_GameWorld.m_WorldConfig.m_UseTuneZones) ? Collision()->IsTune(Collision()->GetMapIndex(From)) : 0;
+	float Len = distance(Pos, From);
 
-	vec2 Dir;
 	if(Len > 0)
 	{
-		Dir = normalize_pre_length(Pos - From, Len);
-
-		int PredictionTick = Client()->GetPredictionTick();
+		vec2 Dir = normalize_pre_length(Pos - From, Len);
 
-		float Ticks;
-		if(IsPredicted)
-			Ticks = (float)(PredictionTick - pCurrent->m_StartTick) + Client()->PredIntraGameTick(g_Config.m_ClDummy);
-		else
-			Ticks = (float)(Client()->GameTick(g_Config.m_ClDummy) - pCurrent->m_StartTick) + Client()->IntraGameTick(g_Config.m_ClDummy);
-		float Ms = (Ticks / Client()->GameTickSpeed()) * 1000.0f;
+		float Ms = TicksBody * 1000.0f / Client()->GameTickSpeed();
 		float a = Ms / m_pClient->GetTuning(TuneZone)->m_LaserBounceDelay;
 		a = clamp(a, 0.0f, 1.0f);
 		float Ia = 1 - a;
 
-		vec2 Out;
-
 		Graphics()->TextureClear();
 		Graphics()->QuadsBegin();
 
 		// do outline
-		Graphics()->SetColor(OuterColor.r, OuterColor.g, OuterColor.b, Alpha);
-		Out = vec2(Dir.y, -Dir.x) * (7.0f * Ia);
+		Graphics()->SetColor(OuterColor);
+		vec2 Out = vec2(Dir.y, -Dir.x) * (7.0f * Ia);
 
 		IGraphics::CFreeformItem Freeform(
 			From.x - Out.x, From.y - Out.y,
@@ -336,7 +335,7 @@ void CItems::RenderLaser(const CLaserData *pCurrent, bool IsPredicted)
 
 		// do inner
 		Out = vec2(Dir.y, -Dir.x) * (5.0f * Ia);
-		Graphics()->SetColor(InnerColor.r, InnerColor.g, InnerColor.b, Alpha); // center
+		Graphics()->SetColor(InnerColor); // center
 
 		Freeform = IGraphics::CFreeformItem(
 			From.x - Out.x, From.y - Out.y,
@@ -350,12 +349,12 @@ void CItems::RenderLaser(const CLaserData *pCurrent, bool IsPredicted)
 
 	// render head
 	{
-		int CurParticle = (Client()->GameTick(g_Config.m_ClDummy) % 3);
+		int CurParticle = (int)TicksHead % 3;
 		Graphics()->TextureSet(GameClient()->m_ParticlesSkin.m_aSpriteParticleSplat[CurParticle]);
-		Graphics()->QuadsSetRotation(Client()->GameTick(g_Config.m_ClDummy));
-		Graphics()->SetColor(OuterColor.r, OuterColor.g, OuterColor.b, Alpha);
+		Graphics()->QuadsSetRotation((int)TicksHead);
+		Graphics()->SetColor(OuterColor);
 		Graphics()->RenderQuadContainerAsSprite(m_ItemsQuadContainerIndex, m_aParticleSplatOffset[CurParticle], Pos.x, Pos.y);
-		Graphics()->SetColor(InnerColor.r, InnerColor.g, InnerColor.b, Alpha);
+		Graphics()->SetColor(InnerColor);
 		Graphics()->RenderQuadContainerAsSprite(m_ItemsQuadContainerIndex, m_aParticleSplatOffset[CurParticle], Pos.x, Pos.y, 20.f / 24.f, 20.f / 24.f);
 	}
 }
diff --git a/src/game/client/components/items.h b/src/game/client/components/items.h
index b9d4a5664a4..fb09363821e 100644
--- a/src/game/client/components/items.h
+++ b/src/game/client/components/items.h
@@ -23,6 +23,7 @@ class CItems : public CComponent
 	virtual void OnInit() override;
 
 	void ReconstructSmokeTrail(const CProjectileData *pCurrent, int DestroyTick);
+	void RenderLaser(vec2 From, vec2 Pos, ColorRGBA OuterColor, ColorRGBA InnerColor, float TicksBody, float TicksHead, int Type);
 
 private:
 	int m_BlueFlagOffset;
diff --git a/src/game/client/components/menus.cpp b/src/game/client/components/menus.cpp
index efd89b21481..97d054f2303 100644
--- a/src/game/client/components/menus.cpp
+++ b/src/game/client/components/menus.cpp
@@ -303,44 +303,16 @@ int CMenus::DoButton_CheckBox_Common(const void *pId, const char *pText, const c
 
 void CMenus::DoLaserPreview(const CUIRect *pRect, const ColorHSLA LaserOutlineColor, const ColorHSLA LaserInnerColor, const int LaserType)
 {
-	ColorRGBA LaserRGB;
 	CUIRect Section = *pRect;
 	vec2 From = vec2(Section.x + 50.0f, Section.y + Section.h / 2.0f);
 	vec2 Pos = vec2(Section.x + Section.w - 10.0f, Section.y + Section.h / 2.0f);
 
-	Graphics()->BlendNormal();
-	Graphics()->TextureClear();
-	Graphics()->QuadsBegin();
-
-	LaserRGB = color_cast<ColorRGBA, ColorHSLA>(LaserOutlineColor);
-	ColorRGBA OuterColor(LaserRGB.r, LaserRGB.g, LaserRGB.b, 1.0f);
-	Graphics()->SetColor(LaserRGB.r, LaserRGB.g, LaserRGB.b, 1.0f);
-	vec2 Out = vec2(0.0f, -1.0f) * (3.15f);
+	const ColorRGBA OuterColor = color_cast<ColorRGBA>(ColorHSLA(LaserOutlineColor));
+	const ColorRGBA InnerColor = color_cast<ColorRGBA>(ColorHSLA(LaserInnerColor));
+	const float TicksHead = Client()->GlobalTime() * Client()->GameTickSpeed();
 
-	IGraphics::CFreeformItem Freeform(From.x - Out.x, From.y - Out.y, From.x + Out.x, From.y + Out.y, Pos.x - Out.x, Pos.y - Out.y, Pos.x + Out.x, Pos.y + Out.y);
-	Graphics()->QuadsDrawFreeform(&Freeform, 1);
-
-	LaserRGB = color_cast<ColorRGBA, ColorHSLA>(LaserInnerColor);
-	ColorRGBA InnerColor(LaserRGB.r, LaserRGB.g, LaserRGB.b, 1.0f);
-	Out = vec2(0.0f, -1.0f) * (2.25f);
-	Graphics()->SetColor(InnerColor.r, InnerColor.g, InnerColor.b, 1.0f);
-
-	Freeform = IGraphics::CFreeformItem(From.x - Out.x, From.y - Out.y, From.x + Out.x, From.y + Out.y, Pos.x - Out.x, Pos.y - Out.y, Pos.x + Out.x, Pos.y + Out.y);
-	Graphics()->QuadsDrawFreeform(&Freeform, 1);
-	Graphics()->QuadsEnd();
-
-	Graphics()->BlendNormal();
-	int SpriteIndex = time_get() % 3;
-	Graphics()->TextureSet(GameClient()->m_ParticlesSkin.m_aSpriteParticleSplat[SpriteIndex]);
-	Graphics()->QuadsBegin();
-	Graphics()->QuadsSetRotation(time_get());
-	Graphics()->SetColor(OuterColor.r, OuterColor.g, OuterColor.b, 1.0f);
-	IGraphics::CQuadItem QuadItem(Pos.x, Pos.y, 24, 24);
-	Graphics()->QuadsDraw(&QuadItem, 1);
-	Graphics()->SetColor(InnerColor.r, InnerColor.g, InnerColor.b, 1.0f);
-	QuadItem = IGraphics::CQuadItem(Pos.x, Pos.y, 20, 20);
-	Graphics()->QuadsDraw(&QuadItem, 1);
-	Graphics()->QuadsEnd();
+	// TicksBody = 4.0 for less laser width for weapon alignment
+	GameClient()->m_Items.RenderLaser(From, Pos, OuterColor, InnerColor, 4.0f, TicksHead, LaserType);
 
 	switch(LaserType)
 	{
@@ -361,14 +333,7 @@ void CMenus::DoLaserPreview(const CUIRect *pRect, const ColorHSLA LaserOutlineCo
 		Graphics()->QuadsEnd();
 		break;
 	default:
-		Graphics()->QuadsBegin();
-		Graphics()->SetColor(OuterColor.r, OuterColor.g, OuterColor.b, 1.0f);
-		QuadItem = IGraphics::CQuadItem(From.x, From.y, 24, 24);
-		Graphics()->QuadsDraw(&QuadItem, 1);
-		Graphics()->SetColor(InnerColor.r, InnerColor.g, InnerColor.b, 1.0f);
-		QuadItem = IGraphics::CQuadItem(From.x, From.y, 20, 20);
-		Graphics()->QuadsDraw(&QuadItem, 1);
-		Graphics()->QuadsEnd();
+		GameClient()->m_Items.RenderLaser(From, From, OuterColor, InnerColor, 4.0f, TicksHead, LaserType);
 	}
 }
 


Reviews:
user: login: AssassinTee
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T18:37:06Z
user: login: Robyt3
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-03-20T20:29:54Z
user: login: AssassinTee
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T21:10:11Z
user: login: AssassinTee
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T21:12:10Z
user: login: Robyt3
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T21:20:22Z
user: login: Robyt3
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T21:23:29Z
user: login: AssassinTee
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T09:51:13Z
user: login: Robyt3
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-03-21T17:20:55Z
user: login: Robyt3
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-21T17:21:45Z
user: login: AssassinTee
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T18:09:30Z
user: login: Robyt3
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-21T18:19:57Z
user: login: Robyt3
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-21T18:21:12Z
user: login: AssassinTee
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-22T09:11:17Z
user: login: Robyt3
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-03-23T12:49:51Z
user: login: Robyt3
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-23T12:55:25Z
user: login: AssassinTee
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-23T14:42:10Z
user: login: Robyt3
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-23T16:22:31Z

Review comments:
diff_hunk: @@ -301,19 +289,39 @@ void CItems::RenderLaser(const CLaserData *pCurrent, bool IsPredicted)
 		Alpha = g_Config.m_ClShowOthersAlpha / 100.0f;
 	}
 
+	ColorRGBA RGB;
+	RGB = color_cast<ColorRGBA>(ColorHSLA(ColorOut));
+	ColorRGBA OuterColor(RGB.r, RGB.g, RGB.b, Alpha);
+	RGB = color_cast<ColorRGBA>(ColorHSLA(ColorIn));
+	ColorRGBA InnerColor(RGB.r, RGB.g, RGB.b, Alpha);
path: src/game/client/components/items.cpp
user: login: AssassinTee
body: @AssassinTee this can be further refactored
created_at: 2025-03-20T18:37:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -301,19 +289,38 @@ void CItems::RenderLaser(const CLaserData *pCurrent, bool IsPredicted)
 		Alpha = g_Config.m_ClShowOthersAlpha / 100.0f;
 	}
 
+	ColorRGBA OuterColor = color_cast<ColorRGBA>(ColorHSLA(ColorOut));
+	ColorRGBA InnerColor = color_cast<ColorRGBA>(ColorHSLA(ColorIn));
+	OuterColor.a = Alpha;
+	InnerColor.a = Alpha;
+
+	float Ticks;
+	if(Type == LASERTYPE_DOOR)
+	{
+		Ticks = 0.02f * Client()->GameTickSpeed();
path: src/game/client/components/items.cpp
user: login: Robyt3
body: Why so roundabout?

```suggestion
		Ticks = 1.0f;
```

Even if the tick speed would be different, this should still just always be one tick, right?
created_at: 2025-03-20T20:22:28Z
author_association: MEMBER
diff_hunk: @@ -301,19 +289,38 @@ void CItems::RenderLaser(const CLaserData *pCurrent, bool IsPredicted)
 		Alpha = g_Config.m_ClShowOthersAlpha / 100.0f;
 	}
 
+	ColorRGBA OuterColor = color_cast<ColorRGBA>(ColorHSLA(ColorOut));
+	ColorRGBA InnerColor = color_cast<ColorRGBA>(ColorHSLA(ColorIn));
+	OuterColor.a = Alpha;
+	InnerColor.a = Alpha;
+
+	float Ticks;
+	if(Type == LASERTYPE_DOOR)
+	{
+		Ticks = 0.02f * Client()->GameTickSpeed();
+	}
+	else if(IsPredicted)
+	{
+		int PredictionTick = Client()->GetPredictionTick();
+		Ticks = (float)(PredictionTick - pCurrent->m_StartTick) + Client()->PredIntraGameTick(g_Config.m_ClDummy);
+	}
+	else
+		Ticks = (float)(Client()->GameTick(g_Config.m_ClDummy) - pCurrent->m_StartTick) + Client()->IntraGameTick(g_Config.m_ClDummy);
+
+	RenderLaser(pCurrent->m_From, pCurrent->m_To, OuterColor, InnerColor, Ticks);
+}
+
+void CItems::RenderLaser(const vec2 &From, const vec2 &Pos, ColorRGBA &OuterColor, ColorRGBA &InnerColor, float Ticks)
path: src/game/client/components/items.cpp
user: login: Robyt3
body: I think this separate function is unnecessary, it's only used once.
created_at: 2025-03-20T20:25:25Z
author_association: MEMBER
diff_hunk: @@ -301,19 +289,38 @@ void CItems::RenderLaser(const CLaserData *pCurrent, bool IsPredicted)
 		Alpha = g_Config.m_ClShowOthersAlpha / 100.0f;
 	}
 
+	ColorRGBA OuterColor = color_cast<ColorRGBA>(ColorHSLA(ColorOut));
+	ColorRGBA InnerColor = color_cast<ColorRGBA>(ColorHSLA(ColorIn));
+	OuterColor.a = Alpha;
+	InnerColor.a = Alpha;
+
+	float Ticks;
+	if(Type == LASERTYPE_DOOR)
+	{
+		Ticks = 0.02f * Client()->GameTickSpeed();
+	}
+	else if(IsPredicted)
+	{
+		int PredictionTick = Client()->GetPredictionTick();
+		Ticks = (float)(PredictionTick - pCurrent->m_StartTick) + Client()->PredIntraGameTick(g_Config.m_ClDummy);
+	}
+	else
+		Ticks = (float)(Client()->GameTick(g_Config.m_ClDummy) - pCurrent->m_StartTick) + Client()->IntraGameTick(g_Config.m_ClDummy);
+
+	RenderLaser(pCurrent->m_From, pCurrent->m_To, OuterColor, InnerColor, Ticks);
+}
+
+void CItems::RenderLaser(const vec2 &From, const vec2 &Pos, ColorRGBA &OuterColor, ColorRGBA &InnerColor, float Ticks)
+{
+	int TuneZone = GameClient()->m_GameWorld.m_WorldConfig.m_UseTuneZones ? Collision()->IsTune(Collision()->GetMapIndex(From)) : 0;
+	float Len = distance(Pos, From);
+
 	vec2 Dir;
 	if(Len > 0)
path: src/game/client/components/items.cpp
user: login: Robyt3
body: `Dir` is unused unless the `if` branch is entered. Could move the declaration inside.

Also, this could be more readable if you invert the condition and therefore reduce the indentation of most of the code.

```cpp
if(Len == 0.0f)
{
	return;
}

...
```
created_at: 2025-03-20T20:27:24Z
author_association: MEMBER
diff_hunk: @@ -301,19 +289,38 @@ void CItems::RenderLaser(const CLaserData *pCurrent, bool IsPredicted)
 		Alpha = g_Config.m_ClShowOthersAlpha / 100.0f;
 	}
 
+	ColorRGBA OuterColor = color_cast<ColorRGBA>(ColorHSLA(ColorOut));
+	ColorRGBA InnerColor = color_cast<ColorRGBA>(ColorHSLA(ColorIn));
+	OuterColor.a = Alpha;
+	InnerColor.a = Alpha;
path: src/game/client/components/items.cpp
user: login: Robyt3
body: ```suggestion
	const ColorRGBA OuterColor = color_cast<ColorRGBA>(ColorHSLA(ColorOut).WithAlpha(Alpha));
	const ColorRGBA InnerColor = color_cast<ColorRGBA>(ColorHSLA(ColorIn).WithAlpha(Alpha));
```
created_at: 2025-03-20T20:28:26Z
author_association: MEMBER
diff_hunk: @@ -301,19 +289,38 @@ void CItems::RenderLaser(const CLaserData *pCurrent, bool IsPredicted)
 		Alpha = g_Config.m_ClShowOthersAlpha / 100.0f;
 	}
 
+	ColorRGBA OuterColor = color_cast<ColorRGBA>(ColorHSLA(ColorOut));
+	ColorRGBA InnerColor = color_cast<ColorRGBA>(ColorHSLA(ColorIn));
+	OuterColor.a = Alpha;
+	InnerColor.a = Alpha;
+
+	float Ticks;
+	if(Type == LASERTYPE_DOOR)
+	{
+		Ticks = 0.02f * Client()->GameTickSpeed();
+	}
+	else if(IsPredicted)
+	{
+		int PredictionTick = Client()->GetPredictionTick();
+		Ticks = (float)(PredictionTick - pCurrent->m_StartTick) + Client()->PredIntraGameTick(g_Config.m_ClDummy);
+	}
+	else
+		Ticks = (float)(Client()->GameTick(g_Config.m_ClDummy) - pCurrent->m_StartTick) + Client()->IntraGameTick(g_Config.m_ClDummy);
+
+	RenderLaser(pCurrent->m_From, pCurrent->m_To, OuterColor, InnerColor, Ticks);
+}
+
+void CItems::RenderLaser(const vec2 &From, const vec2 &Pos, ColorRGBA &OuterColor, ColorRGBA &InnerColor, float Ticks)
path: src/game/client/components/items.cpp
user: login: AssassinTee
body: I disagree, why stuff everything into one function if it improves readability by so much?
created_at: 2025-03-20T21:10:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -301,19 +289,38 @@ void CItems::RenderLaser(const CLaserData *pCurrent, bool IsPredicted)
 		Alpha = g_Config.m_ClShowOthersAlpha / 100.0f;
 	}
 
+	ColorRGBA OuterColor = color_cast<ColorRGBA>(ColorHSLA(ColorOut));
+	ColorRGBA InnerColor = color_cast<ColorRGBA>(ColorHSLA(ColorIn));
+	OuterColor.a = Alpha;
+	InnerColor.a = Alpha;
+
+	float Ticks;
+	if(Type == LASERTYPE_DOOR)
+	{
+		Ticks = 0.02f * Client()->GameTickSpeed();
+	}
+	else if(IsPredicted)
+	{
+		int PredictionTick = Client()->GetPredictionTick();
+		Ticks = (float)(PredictionTick - pCurrent->m_StartTick) + Client()->PredIntraGameTick(g_Config.m_ClDummy);
+	}
+	else
+		Ticks = (float)(Client()->GameTick(g_Config.m_ClDummy) - pCurrent->m_StartTick) + Client()->IntraGameTick(g_Config.m_ClDummy);
+
+	RenderLaser(pCurrent->m_From, pCurrent->m_To, OuterColor, InnerColor, Ticks);
+}
+
+void CItems::RenderLaser(const vec2 &From, const vec2 &Pos, ColorRGBA &OuterColor, ColorRGBA &InnerColor, float Ticks)
+{
+	int TuneZone = GameClient()->m_GameWorld.m_WorldConfig.m_UseTuneZones ? Collision()->IsTune(Collision()->GetMapIndex(From)) : 0;
+	float Len = distance(Pos, From);
+
 	vec2 Dir;
 	if(Len > 0)
path: src/game/client/components/items.cpp
user: login: AssassinTee
body: This is wrong, there is code below which is not dead if Len == 0.0f. I can check for Len != 0.0f here
created_at: 2025-03-20T21:12:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -301,19 +289,38 @@ void CItems::RenderLaser(const CLaserData *pCurrent, bool IsPredicted)
 		Alpha = g_Config.m_ClShowOthersAlpha / 100.0f;
 	}
 
+	ColorRGBA OuterColor = color_cast<ColorRGBA>(ColorHSLA(ColorOut));
+	ColorRGBA InnerColor = color_cast<ColorRGBA>(ColorHSLA(ColorIn));
+	OuterColor.a = Alpha;
+	InnerColor.a = Alpha;
+
+	float Ticks;
+	if(Type == LASERTYPE_DOOR)
+	{
+		Ticks = 0.02f * Client()->GameTickSpeed();
+	}
+	else if(IsPredicted)
+	{
+		int PredictionTick = Client()->GetPredictionTick();
+		Ticks = (float)(PredictionTick - pCurrent->m_StartTick) + Client()->PredIntraGameTick(g_Config.m_ClDummy);
+	}
+	else
+		Ticks = (float)(Client()->GameTick(g_Config.m_ClDummy) - pCurrent->m_StartTick) + Client()->IntraGameTick(g_Config.m_ClDummy);
+
+	RenderLaser(pCurrent->m_From, pCurrent->m_To, OuterColor, InnerColor, Ticks);
+}
+
+void CItems::RenderLaser(const vec2 &From, const vec2 &Pos, ColorRGBA &OuterColor, ColorRGBA &InnerColor, float Ticks)
+{
+	int TuneZone = GameClient()->m_GameWorld.m_WorldConfig.m_UseTuneZones ? Collision()->IsTune(Collision()->GetMapIndex(From)) : 0;
+	float Len = distance(Pos, From);
+
 	vec2 Dir;
 	if(Len > 0)
path: src/game/client/components/items.cpp
user: login: Robyt3
body: I see, I missed that. But you can move the `Dir` variable fully inside this `if`?
created_at: 2025-03-20T21:20:22Z
author_association: MEMBER
diff_hunk: @@ -301,19 +289,38 @@ void CItems::RenderLaser(const CLaserData *pCurrent, bool IsPredicted)
 		Alpha = g_Config.m_ClShowOthersAlpha / 100.0f;
 	}
 
+	ColorRGBA OuterColor = color_cast<ColorRGBA>(ColorHSLA(ColorOut));
+	ColorRGBA InnerColor = color_cast<ColorRGBA>(ColorHSLA(ColorIn));
+	OuterColor.a = Alpha;
+	InnerColor.a = Alpha;
+
+	float Ticks;
+	if(Type == LASERTYPE_DOOR)
+	{
+		Ticks = 0.02f * Client()->GameTickSpeed();
+	}
+	else if(IsPredicted)
+	{
+		int PredictionTick = Client()->GetPredictionTick();
+		Ticks = (float)(PredictionTick - pCurrent->m_StartTick) + Client()->PredIntraGameTick(g_Config.m_ClDummy);
+	}
+	else
+		Ticks = (float)(Client()->GameTick(g_Config.m_ClDummy) - pCurrent->m_StartTick) + Client()->IntraGameTick(g_Config.m_ClDummy);
+
+	RenderLaser(pCurrent->m_From, pCurrent->m_To, OuterColor, InnerColor, Ticks);
+}
+
+void CItems::RenderLaser(const vec2 &From, const vec2 &Pos, ColorRGBA &OuterColor, ColorRGBA &InnerColor, float Ticks)
path: src/game/client/components/items.cpp
user: login: Robyt3
body: Doesn't necessarily improve readability to me to split it either.

Pass the arguments by value instead of by reference, because they are small, if you want a separate function:

```suggestion
void CItems::RenderLaser(vec2 From, vec2 Pos, ColorRGBA OuterColor, ColorRGBA InnerColor, float Ticks)
```
created_at: 2025-03-20T21:23:29Z
author_association: MEMBER
diff_hunk: @@ -303,44 +303,13 @@ int CMenus::DoButton_CheckBox_Common(const void *pId, const char *pText, const c
 
 void CMenus::DoLaserPreview(const CUIRect *pRect, const ColorHSLA LaserOutlineColor, const ColorHSLA LaserInnerColor, const int LaserType)
 {
-	ColorRGBA LaserRGB;
 	CUIRect Section = *pRect;
 	vec2 From = vec2(Section.x + 50.0f, Section.y + Section.h / 2.0f);
 	vec2 Pos = vec2(Section.x + Section.w - 10.0f, Section.y + Section.h / 2.0f);
 
-	Graphics()->BlendNormal();
-	Graphics()->TextureClear();
-	Graphics()->QuadsBegin();
-
-	LaserRGB = color_cast<ColorRGBA, ColorHSLA>(LaserOutlineColor);
-	ColorRGBA OuterColor(LaserRGB.r, LaserRGB.g, LaserRGB.b, 1.0f);
-	Graphics()->SetColor(LaserRGB.r, LaserRGB.g, LaserRGB.b, 1.0f);
-	vec2 Out = vec2(0.0f, -1.0f) * (3.15f);
-
-	IGraphics::CFreeformItem Freeform(From.x - Out.x, From.y - Out.y, From.x + Out.x, From.y + Out.y, Pos.x - Out.x, Pos.y - Out.y, Pos.x + Out.x, Pos.y + Out.y);
-	Graphics()->QuadsDrawFreeform(&Freeform, 1);
-
-	LaserRGB = color_cast<ColorRGBA, ColorHSLA>(LaserInnerColor);
-	ColorRGBA InnerColor(LaserRGB.r, LaserRGB.g, LaserRGB.b, 1.0f);
-	Out = vec2(0.0f, -1.0f) * (2.25f);
-	Graphics()->SetColor(InnerColor.r, InnerColor.g, InnerColor.b, 1.0f);
-
-	Freeform = IGraphics::CFreeformItem(From.x - Out.x, From.y - Out.y, From.x + Out.x, From.y + Out.y, Pos.x - Out.x, Pos.y - Out.y, Pos.x + Out.x, Pos.y + Out.y);
-	Graphics()->QuadsDrawFreeform(&Freeform, 1);
-	Graphics()->QuadsEnd();
-
-	Graphics()->BlendNormal();
-	int SpriteIndex = time_get() % 3;
-	Graphics()->TextureSet(GameClient()->m_ParticlesSkin.m_aSpriteParticleSplat[SpriteIndex]);
-	Graphics()->QuadsBegin();
-	Graphics()->QuadsSetRotation(time_get());
-	Graphics()->SetColor(OuterColor.r, OuterColor.g, OuterColor.b, 1.0f);
-	IGraphics::CQuadItem QuadItem(Pos.x, Pos.y, 24, 24);
-	Graphics()->QuadsDraw(&QuadItem, 1);
-	Graphics()->SetColor(InnerColor.r, InnerColor.g, InnerColor.b, 1.0f);
-	QuadItem = IGraphics::CQuadItem(Pos.x, Pos.y, 20, 20);
-	Graphics()->QuadsDraw(&QuadItem, 1);
-	Graphics()->QuadsEnd();
+	const ColorRGBA OuterColor = color_cast<ColorRGBA>(ColorHSLA(LaserOutlineColor));
+	const ColorRGBA InnerColor = color_cast<ColorRGBA>(ColorHSLA(LaserInnerColor));
+	GameClient()->m_Items.RenderLaser(From, Pos, OuterColor, InnerColor, 4.0f);
path: src/game/client/components/menus.cpp
user: login: AssassinTee
body: Before you stumble across it: The ticks is set to 4, so the laser is smaller in width and more aligned with the weapons. This was previously also done.
created_at: 2025-03-21T09:51:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -287,45 +281,49 @@ void CItems::RenderLaser(const CLaserData *pCurrent, bool IsPredicted)
 		ColorIn = g_Config.m_ClLaserRifleInnerColor;
 	}
 
-	RGB = color_cast<ColorRGBA>(ColorHSLA(ColorOut));
-	ColorRGBA OuterColor(RGB.r, RGB.g, RGB.b, 1.0f);
-	RGB = color_cast<ColorRGBA>(ColorHSLA(ColorIn));
-	ColorRGBA InnerColor(RGB.r, RGB.g, RGB.b, 1.0f);
-
-	int TuneZone = GameClient()->m_GameWorld.m_WorldConfig.m_UseTuneZones ? Collision()->IsTune(Collision()->GetMapIndex(From)) : 0;
 	bool IsOtherTeam = (pCurrent->m_ExtraInfo && pCurrent->m_Owner >= 0 && m_pClient->IsOtherTeam(pCurrent->m_Owner));
 
-	float Alpha = 1.f;
-	if(IsOtherTeam)
+	float Alpha = IsOtherTeam ? g_Config.m_ClShowOthersAlpha / 100.0f : 1.f;
+
+	const ColorRGBA OuterColor = color_cast<ColorRGBA>(ColorHSLA(ColorOut).WithAlpha(Alpha));
+	const ColorRGBA InnerColor = color_cast<ColorRGBA>(ColorHSLA(ColorIn).WithAlpha(Alpha));
+
+	float Ticks;
+	if(Type == LASERTYPE_DOOR)
 	{
-		Alpha = g_Config.m_ClShowOthersAlpha / 100.0f;
+		Ticks = 1.0f;
+	}
+	else if(IsPredicted)
+	{
+		int PredictionTick = Client()->GetPredictionTick();
+		Ticks = (float)(PredictionTick - pCurrent->m_StartTick) + Client()->PredIntraGameTick(g_Config.m_ClDummy);
 	}
+	else
+		Ticks = (float)(Client()->GameTick(g_Config.m_ClDummy) - pCurrent->m_StartTick) + Client()->IntraGameTick(g_Config.m_ClDummy);
+
+	RenderLaser(pCurrent->m_From, pCurrent->m_To, OuterColor, InnerColor, Ticks);
+}
+
+void CItems::RenderLaser(vec2 From, vec2 Pos, ColorRGBA OuterColor, ColorRGBA InnerColor, float Ticks)
+{
+	int TuneZone = GameClient()->m_GameWorld.m_WorldConfig.m_UseTuneZones ? Collision()->IsTune(Collision()->GetMapIndex(From)) : 0;
path: src/game/client/components/items.cpp
user: login: Robyt3
body: Accessing `m_WorldConfig` and the collision data is not allowed while offline, i.e. when you use this function to render the preview.
created_at: 2025-03-21T17:08:44Z
author_association: MEMBER
diff_hunk: @@ -353,9 +351,9 @@ void CItems::RenderLaser(const CLaserData *pCurrent, bool IsPredicted)
 		int CurParticle = (Client()->GameTick(g_Config.m_ClDummy) % 3);
path: src/game/client/components/items.cpp
user: login: Robyt3
body: It was like this before, but it would be more correct to also use the `Ticks` variable here (and below) so the sprite being used and the rotation also depend on the predicted tick when prediction is enabled. Though that would break the sprite changing for doors because of your fix of setting `Ticks = 1.0f` means it never changes here as well. Maybe we should separate the tick for the laser body and head.
created_at: 2025-03-21T17:14:13Z
author_association: MEMBER
diff_hunk: @@ -353,9 +351,9 @@ void CItems::RenderLaser(const CLaserData *pCurrent, bool IsPredicted)
 		int CurParticle = (Client()->GameTick(g_Config.m_ClDummy) % 3);
path: src/game/client/components/items.cpp
user: login: Robyt3
body: Also, reusing this function to render the preview has broken the sprite changing/rotating while offline. The heads don't rotate and change sprite anymore when offline. I think something like the above should work, i.e. split into `TicksBody` and `TicksHead` and then also pass a value that increases at tick speed for the `TicksHead` variable when offline. Use `Client()->GlobalTime()` for this. Previously this used `time_get() % 3` to select the sprite which changes several magnitudes faster than `GameTick() % 3` did ingame, so this would also fix the preview speed.
created_at: 2025-03-21T17:19:21Z
author_association: MEMBER
diff_hunk: @@ -303,44 +303,13 @@ int CMenus::DoButton_CheckBox_Common(const void *pId, const char *pText, const c
 
 void CMenus::DoLaserPreview(const CUIRect *pRect, const ColorHSLA LaserOutlineColor, const ColorHSLA LaserInnerColor, const int LaserType)
 {
-	ColorRGBA LaserRGB;
 	CUIRect Section = *pRect;
 	vec2 From = vec2(Section.x + 50.0f, Section.y + Section.h / 2.0f);
 	vec2 Pos = vec2(Section.x + Section.w - 10.0f, Section.y + Section.h / 2.0f);
 
-	Graphics()->BlendNormal();
-	Graphics()->TextureClear();
-	Graphics()->QuadsBegin();
-
-	LaserRGB = color_cast<ColorRGBA, ColorHSLA>(LaserOutlineColor);
-	ColorRGBA OuterColor(LaserRGB.r, LaserRGB.g, LaserRGB.b, 1.0f);
-	Graphics()->SetColor(LaserRGB.r, LaserRGB.g, LaserRGB.b, 1.0f);
-	vec2 Out = vec2(0.0f, -1.0f) * (3.15f);
-
-	IGraphics::CFreeformItem Freeform(From.x - Out.x, From.y - Out.y, From.x + Out.x, From.y + Out.y, Pos.x - Out.x, Pos.y - Out.y, Pos.x + Out.x, Pos.y + Out.y);
-	Graphics()->QuadsDrawFreeform(&Freeform, 1);
-
-	LaserRGB = color_cast<ColorRGBA, ColorHSLA>(LaserInnerColor);
-	ColorRGBA InnerColor(LaserRGB.r, LaserRGB.g, LaserRGB.b, 1.0f);
-	Out = vec2(0.0f, -1.0f) * (2.25f);
-	Graphics()->SetColor(InnerColor.r, InnerColor.g, InnerColor.b, 1.0f);
-
-	Freeform = IGraphics::CFreeformItem(From.x - Out.x, From.y - Out.y, From.x + Out.x, From.y + Out.y, Pos.x - Out.x, Pos.y - Out.y, Pos.x + Out.x, Pos.y + Out.y);
-	Graphics()->QuadsDrawFreeform(&Freeform, 1);
-	Graphics()->QuadsEnd();
-
-	Graphics()->BlendNormal();
-	int SpriteIndex = time_get() % 3;
-	Graphics()->TextureSet(GameClient()->m_ParticlesSkin.m_aSpriteParticleSplat[SpriteIndex]);
-	Graphics()->QuadsBegin();
-	Graphics()->QuadsSetRotation(time_get());
-	Graphics()->SetColor(OuterColor.r, OuterColor.g, OuterColor.b, 1.0f);
-	IGraphics::CQuadItem QuadItem(Pos.x, Pos.y, 24, 24);
-	Graphics()->QuadsDraw(&QuadItem, 1);
-	Graphics()->SetColor(InnerColor.r, InnerColor.g, InnerColor.b, 1.0f);
-	QuadItem = IGraphics::CQuadItem(Pos.x, Pos.y, 20, 20);
-	Graphics()->QuadsDraw(&QuadItem, 1);
-	Graphics()->QuadsEnd();
+	const ColorRGBA OuterColor = color_cast<ColorRGBA>(ColorHSLA(LaserOutlineColor));
+	const ColorRGBA InnerColor = color_cast<ColorRGBA>(ColorHSLA(LaserInnerColor));
+	GameClient()->m_Items.RenderLaser(From, Pos, OuterColor, InnerColor, 4.0f);
path: src/game/client/components/menus.cpp
user: login: Robyt3
body: This would be good as a code comment to explain the reason in the code.
created_at: 2025-03-21T17:21:44Z
author_association: MEMBER
diff_hunk: @@ -287,45 +281,49 @@ void CItems::RenderLaser(const CLaserData *pCurrent, bool IsPredicted)
 		ColorIn = g_Config.m_ClLaserRifleInnerColor;
 	}
 
-	RGB = color_cast<ColorRGBA>(ColorHSLA(ColorOut));
-	ColorRGBA OuterColor(RGB.r, RGB.g, RGB.b, 1.0f);
-	RGB = color_cast<ColorRGBA>(ColorHSLA(ColorIn));
-	ColorRGBA InnerColor(RGB.r, RGB.g, RGB.b, 1.0f);
-
-	int TuneZone = GameClient()->m_GameWorld.m_WorldConfig.m_UseTuneZones ? Collision()->IsTune(Collision()->GetMapIndex(From)) : 0;
 	bool IsOtherTeam = (pCurrent->m_ExtraInfo && pCurrent->m_Owner >= 0 && m_pClient->IsOtherTeam(pCurrent->m_Owner));
 
-	float Alpha = 1.f;
-	if(IsOtherTeam)
+	float Alpha = IsOtherTeam ? g_Config.m_ClShowOthersAlpha / 100.0f : 1.f;
+
+	const ColorRGBA OuterColor = color_cast<ColorRGBA>(ColorHSLA(ColorOut).WithAlpha(Alpha));
+	const ColorRGBA InnerColor = color_cast<ColorRGBA>(ColorHSLA(ColorIn).WithAlpha(Alpha));
+
+	float Ticks;
+	if(Type == LASERTYPE_DOOR)
 	{
-		Alpha = g_Config.m_ClShowOthersAlpha / 100.0f;
+		Ticks = 1.0f;
+	}
+	else if(IsPredicted)
+	{
+		int PredictionTick = Client()->GetPredictionTick();
+		Ticks = (float)(PredictionTick - pCurrent->m_StartTick) + Client()->PredIntraGameTick(g_Config.m_ClDummy);
 	}
+	else
+		Ticks = (float)(Client()->GameTick(g_Config.m_ClDummy) - pCurrent->m_StartTick) + Client()->IntraGameTick(g_Config.m_ClDummy);
+
+	RenderLaser(pCurrent->m_From, pCurrent->m_To, OuterColor, InnerColor, Ticks);
+}
+
+void CItems::RenderLaser(vec2 From, vec2 Pos, ColorRGBA OuterColor, ColorRGBA InnerColor, float Ticks)
+{
+	int TuneZone = GameClient()->m_GameWorld.m_WorldConfig.m_UseTuneZones ? Collision()->IsTune(Collision()->GetMapIndex(From)) : 0;
path: src/game/client/components/items.cpp
user: login: AssassinTee
body: how do we fix this? Also this seems to work and defaults to 0 🤔
created_at: 2025-03-21T18:09:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -287,45 +281,49 @@ void CItems::RenderLaser(const CLaserData *pCurrent, bool IsPredicted)
 		ColorIn = g_Config.m_ClLaserRifleInnerColor;
 	}
 
-	RGB = color_cast<ColorRGBA>(ColorHSLA(ColorOut));
-	ColorRGBA OuterColor(RGB.r, RGB.g, RGB.b, 1.0f);
-	RGB = color_cast<ColorRGBA>(ColorHSLA(ColorIn));
-	ColorRGBA InnerColor(RGB.r, RGB.g, RGB.b, 1.0f);
-
-	int TuneZone = GameClient()->m_GameWorld.m_WorldConfig.m_UseTuneZones ? Collision()->IsTune(Collision()->GetMapIndex(From)) : 0;
 	bool IsOtherTeam = (pCurrent->m_ExtraInfo && pCurrent->m_Owner >= 0 && m_pClient->IsOtherTeam(pCurrent->m_Owner));
 
-	float Alpha = 1.f;
-	if(IsOtherTeam)
+	float Alpha = IsOtherTeam ? g_Config.m_ClShowOthersAlpha / 100.0f : 1.f;
+
+	const ColorRGBA OuterColor = color_cast<ColorRGBA>(ColorHSLA(ColorOut).WithAlpha(Alpha));
+	const ColorRGBA InnerColor = color_cast<ColorRGBA>(ColorHSLA(ColorIn).WithAlpha(Alpha));
+
+	float Ticks;
+	if(Type == LASERTYPE_DOOR)
 	{
-		Alpha = g_Config.m_ClShowOthersAlpha / 100.0f;
+		Ticks = 1.0f;
+	}
+	else if(IsPredicted)
+	{
+		int PredictionTick = Client()->GetPredictionTick();
+		Ticks = (float)(PredictionTick - pCurrent->m_StartTick) + Client()->PredIntraGameTick(g_Config.m_ClDummy);
 	}
+	else
+		Ticks = (float)(Client()->GameTick(g_Config.m_ClDummy) - pCurrent->m_StartTick) + Client()->IntraGameTick(g_Config.m_ClDummy);
+
+	RenderLaser(pCurrent->m_From, pCurrent->m_To, OuterColor, InnerColor, Ticks);
+}
+
+void CItems::RenderLaser(vec2 From, vec2 Pos, ColorRGBA OuterColor, ColorRGBA InnerColor, float Ticks)
+{
+	int TuneZone = GameClient()->m_GameWorld.m_WorldConfig.m_UseTuneZones ? Collision()->IsTune(Collision()->GetMapIndex(From)) : 0;
path: src/game/client/components/items.cpp
user: login: Robyt3
body: Probably just check for `Client()->State() == IClient::STATE_ONLINE`.
created_at: 2025-03-21T18:19:57Z
author_association: MEMBER
diff_hunk: @@ -287,45 +281,49 @@ void CItems::RenderLaser(const CLaserData *pCurrent, bool IsPredicted)
 		ColorIn = g_Config.m_ClLaserRifleInnerColor;
 	}
 
-	RGB = color_cast<ColorRGBA>(ColorHSLA(ColorOut));
-	ColorRGBA OuterColor(RGB.r, RGB.g, RGB.b, 1.0f);
-	RGB = color_cast<ColorRGBA>(ColorHSLA(ColorIn));
-	ColorRGBA InnerColor(RGB.r, RGB.g, RGB.b, 1.0f);
-
-	int TuneZone = GameClient()->m_GameWorld.m_WorldConfig.m_UseTuneZones ? Collision()->IsTune(Collision()->GetMapIndex(From)) : 0;
 	bool IsOtherTeam = (pCurrent->m_ExtraInfo && pCurrent->m_Owner >= 0 && m_pClient->IsOtherTeam(pCurrent->m_Owner));
 
-	float Alpha = 1.f;
-	if(IsOtherTeam)
+	float Alpha = IsOtherTeam ? g_Config.m_ClShowOthersAlpha / 100.0f : 1.f;
+
+	const ColorRGBA OuterColor = color_cast<ColorRGBA>(ColorHSLA(ColorOut).WithAlpha(Alpha));
+	const ColorRGBA InnerColor = color_cast<ColorRGBA>(ColorHSLA(ColorIn).WithAlpha(Alpha));
+
+	float Ticks;
+	if(Type == LASERTYPE_DOOR)
 	{
-		Alpha = g_Config.m_ClShowOthersAlpha / 100.0f;
+		Ticks = 1.0f;
+	}
+	else if(IsPredicted)
+	{
+		int PredictionTick = Client()->GetPredictionTick();
+		Ticks = (float)(PredictionTick - pCurrent->m_StartTick) + Client()->PredIntraGameTick(g_Config.m_ClDummy);
 	}
+	else
+		Ticks = (float)(Client()->GameTick(g_Config.m_ClDummy) - pCurrent->m_StartTick) + Client()->IntraGameTick(g_Config.m_ClDummy);
+
+	RenderLaser(pCurrent->m_From, pCurrent->m_To, OuterColor, InnerColor, Ticks);
+}
+
+void CItems::RenderLaser(vec2 From, vec2 Pos, ColorRGBA OuterColor, ColorRGBA InnerColor, float Ticks)
+{
+	int TuneZone = GameClient()->m_GameWorld.m_WorldConfig.m_UseTuneZones ? Collision()->IsTune(Collision()->GetMapIndex(From)) : 0;
path: src/game/client/components/items.cpp
user: login: Robyt3
body: > Also this seems to work and defaults to 0 🤔

I don't find any code to reset `m_GameWorld.m_WorldConfig` though, so it could stay `1` after joining a server that set it once.
created_at: 2025-03-21T18:21:12Z
author_association: MEMBER
diff_hunk: @@ -353,9 +351,9 @@ void CItems::RenderLaser(const CLaserData *pCurrent, bool IsPredicted)
 		int CurParticle = (Client()->GameTick(g_Config.m_ClDummy) % 3);
path: src/game/client/components/items.cpp
user: login: AssassinTee
body: if I use GlobalTime(), which is a float, it updates once a second. I tried to use GlobalTime() * GameTickSpeed(), but this is too fast. I get an acceptable result with GlobalTime() * 1000, but I don't understand why
created_at: 2025-03-22T09:11:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -303,44 +303,16 @@ int CMenus::DoButton_CheckBox_Common(const void *pId, const char *pText, const c
 
 void CMenus::DoLaserPreview(const CUIRect *pRect, const ColorHSLA LaserOutlineColor, const ColorHSLA LaserInnerColor, const int LaserType)
 {
-	ColorRGBA LaserRGB;
 	CUIRect Section = *pRect;
 	vec2 From = vec2(Section.x + 50.0f, Section.y + Section.h / 2.0f);
 	vec2 Pos = vec2(Section.x + Section.w - 10.0f, Section.y + Section.h / 2.0f);
 
-	Graphics()->BlendNormal();
-	Graphics()->TextureClear();
-	Graphics()->QuadsBegin();
-
-	LaserRGB = color_cast<ColorRGBA, ColorHSLA>(LaserOutlineColor);
-	ColorRGBA OuterColor(LaserRGB.r, LaserRGB.g, LaserRGB.b, 1.0f);
-	Graphics()->SetColor(LaserRGB.r, LaserRGB.g, LaserRGB.b, 1.0f);
-	vec2 Out = vec2(0.0f, -1.0f) * (3.15f);
+	const ColorRGBA OuterColor = color_cast<ColorRGBA>(ColorHSLA(LaserOutlineColor));
+	const ColorRGBA InnerColor = color_cast<ColorRGBA>(ColorHSLA(LaserInnerColor));
+	float TicksHead = Client()->GlobalTime() * 1000;
path: src/game/client/components/menus.cpp
user: login: Robyt3
body: ```suggestion
	const float TicksHead = Client()->GlobalTime() * Client()->GameTickSpeed();
```

That _should_ result in the same sprite change and rotation speed as ingame.

Though for some reason it looks like the sprite is changing much slower ingame. Not required to fix for this PR if we can't figure it out.
created_at: 2025-03-23T12:42:44Z
author_association: MEMBER
diff_hunk: @@ -350,12 +349,12 @@ void CItems::RenderLaser(const CLaserData *pCurrent, bool IsPredicted)
 
 	// render head
 	{
-		int CurParticle = (Client()->GameTick(g_Config.m_ClDummy) % 3);
+		int CurParticle = (int)TicksHead % 3;
 		Graphics()->TextureSet(GameClient()->m_ParticlesSkin.m_aSpriteParticleSplat[CurParticle]);
 		Graphics()->QuadsSetRotation(Client()->GameTick(g_Config.m_ClDummy));
path: src/game/client/components/items.cpp
user: login: Robyt3
body: ```suggestion
		Graphics()->QuadsSetRotation(TicksHead);
```
created_at: 2025-03-23T12:46:33Z
author_association: MEMBER
diff_hunk: @@ -353,9 +351,9 @@ void CItems::RenderLaser(const CLaserData *pCurrent, bool IsPredicted)
 		int CurParticle = (Client()->GameTick(g_Config.m_ClDummy) % 3);
path: src/game/client/components/items.cpp
user: login: Robyt3
body: Doesn't look slower with 1000 for me either, maybe it's dependent on FPS or refresh rate somehow.
created_at: 2025-03-23T12:55:25Z
author_association: MEMBER
diff_hunk: @@ -350,12 +349,12 @@ void CItems::RenderLaser(const CLaserData *pCurrent, bool IsPredicted)
 
 	// render head
 	{
-		int CurParticle = (Client()->GameTick(g_Config.m_ClDummy) % 3);
+		int CurParticle = (int)TicksHead % 3;
 		Graphics()->TextureSet(GameClient()->m_ParticlesSkin.m_aSpriteParticleSplat[CurParticle]);
 		Graphics()->QuadsSetRotation(Client()->GameTick(g_Config.m_ClDummy));
path: src/game/client/components/items.cpp
user: login: AssassinTee
body: omg 🙈 good catch
created_at: 2025-03-23T14:42:10Z
author_association: CONTRIBUTOR

PR comments:
user: login: AssassinTee
created_at: 2025-03-21T09:18:50Z
body: @Robyt3 I found a usecase for the second function, just take a look at `CMenus::DoLaserPreview`

Should I make a new PR or refactor this also here?

Edit: I am doing this here
author_association: CONTRIBUTOR
user: login: AssassinTee
created_at: 2025-03-21T09:50:00Z
body: Menu:
![screenshot_2025-03-21_10-43-27](https://github.com/user-attachments/assets/8775894a-f9d4-47e9-8473-3e365a74d052)
![screenshot_2025-03-21_10-45-04](https://github.com/user-attachments/assets/0b2517cb-dbb9-464f-be66-3eb6e27e2130)

author_association: CONTRIBUTOR