1/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */
2/* If you are missing that file, acquire a complete release at teeworlds.com. */
3#include "items.h"
4
5#include <engine/demo.h>
6#include <engine/graphics.h>
7#include <engine/shared/config.h>
8
9#include <generated/client_data.h>
10#include <generated/protocol.h>
11
12#include <game/client/components/effects.h>
13#include <game/client/gameclient.h>
14#include <game/client/laser_data.h>
15#include <game/client/pickup_data.h>
16#include <game/client/prediction/entities/laser.h>
17#include <game/client/prediction/entities/pickup.h>
18#include <game/client/prediction/entities/projectile.h>
19#include <game/client/projectile_data.h>
20#include <game/mapitems.h>
21
22void CItems::RenderProjectile(const CProjectileData *pCurrent, int ItemId, const CScreenRect &ScreenRect)
23{
24 int CurWeapon = std::clamp(val: pCurrent->m_Type, lo: 0, hi: NUM_WEAPONS - 1);
25
26 // get positions
27 float Curvature = 0;
28 float Speed = 0;
29 const CTuningParams *pTuning = GameClient()->GetTuning(i: pCurrent->m_TuneZone);
30 if(CurWeapon == WEAPON_GRENADE)
31 {
32 Curvature = pTuning->m_GrenadeCurvature;
33 Speed = pTuning->m_GrenadeSpeed;
34 }
35 else if(CurWeapon == WEAPON_SHOTGUN)
36 {
37 Curvature = pTuning->m_ShotgunCurvature;
38 Speed = pTuning->m_ShotgunSpeed;
39 }
40 else if(CurWeapon == WEAPON_GUN)
41 {
42 Curvature = pTuning->m_GunCurvature;
43 Speed = pTuning->m_GunSpeed;
44 }
45
46 bool LocalPlayerInGame = false;
47
48 if(GameClient()->m_Snap.m_pLocalInfo)
49 LocalPlayerInGame = GameClient()->m_aClients[GameClient()->m_Snap.m_pLocalInfo->m_ClientId].m_Team != TEAM_SPECTATORS;
50
51 static float s_LastGameTickTime = Client()->GameTickTime(Conn: g_Config.m_ClDummy);
52 if(!GameClient()->IsWorldPaused() && !GameClient()->IsDemoPlaybackPaused())
53 s_LastGameTickTime = Client()->GameTickTime(Conn: g_Config.m_ClDummy);
54
55 bool IsOtherTeam = (pCurrent->m_ExtraInfo && pCurrent->m_Owner >= 0 && GameClient()->IsOtherTeam(ClientId: pCurrent->m_Owner));
56
57 int PredictionTick = Client()->GetPredictionTick();
58
59 float Ct;
60 if(GameClient()->Predict() && GameClient()->AntiPingGrenade() && LocalPlayerInGame && !IsOtherTeam)
61 Ct = ((float)(PredictionTick - 1 - pCurrent->m_StartTick) + Client()->PredIntraGameTick(Conn: g_Config.m_ClDummy)) / (float)Client()->GameTickSpeed();
62 else
63 Ct = (Client()->PrevGameTick(Conn: g_Config.m_ClDummy) - pCurrent->m_StartTick) / (float)Client()->GameTickSpeed() + s_LastGameTickTime;
64 if(Ct < 0)
65 {
66 if(Ct > -s_LastGameTickTime / 2)
67 {
68 // Fixup the timing which might be screwed during demo playback because
69 // s_LastGameTickTime depends on the system timer, while the other part
70 // (Client()->PrevGameTick(g_Config.m_ClDummy) - pCurrent->m_StartTick) / (float)Client()->GameTickSpeed()
71 // is virtually constant (for projectiles fired on the current game tick):
72 // (x - (x+2)) / 50 = -0.04
73 //
74 // We have a strict comparison for the passed time being more than the time between ticks
75 // if(CurtickStart > m_Info.m_CurrentTime) in CDemoPlayer::Update()
76 // so on the practice the typical value of s_LastGameTickTime varies from 0.02386 to 0.03999
77 // which leads to Ct from -0.00001 to -0.01614.
78 // Round up those to 0.0 to fix missing rendering of the projectile.
79 Ct = 0;
80 }
81 else
82 {
83 return; // projectile haven't been shot yet
84 }
85 }
86
87 vec2 Pos = CalcPos(Pos: pCurrent->m_StartPos, Velocity: pCurrent->m_StartVel, Curvature, Speed, Time: Ct);
88 if(!ScreenRect.Inside(Position: Pos))
89 return;
90 vec2 PrevPos = CalcPos(Pos: pCurrent->m_StartPos, Velocity: pCurrent->m_StartVel, Curvature, Speed, Time: Ct - 0.001f);
91
92 float Alpha = 1.f;
93 if(IsOtherTeam)
94 {
95 Alpha = g_Config.m_ClShowOthersAlpha / 100.0f;
96 }
97
98 vec2 Vel = Pos - PrevPos;
99
100 // add particle for this projectile
101 // don't check for validity of the projectile for the current weapon here, so particle effects are rendered for mod compatibility
102 if(CurWeapon == WEAPON_GRENADE)
103 {
104 GameClient()->m_Effects.SmokeTrail(Pos, Vel: Vel * -1, Alpha, TimePassed: 0.0f);
105 static float s_Time = 0.0f;
106 static float s_LastLocalTime = LocalTime();
107 s_Time += (LocalTime() - s_LastLocalTime) * GameClient()->GetAnimationPlaybackSpeed();
108 Graphics()->QuadsSetRotation(Angle: s_Time * pi * 2 * 2 + ItemId);
109 s_LastLocalTime = LocalTime();
110 }
111 else
112 {
113 GameClient()->m_Effects.BulletTrail(Pos, Alpha, TimePassed: 0.0f);
114
115 if(length(a: Vel) > 0.00001f)
116 Graphics()->QuadsSetRotation(Angle: angle(a: Vel));
117 else
118 Graphics()->QuadsSetRotation(Angle: 0);
119 }
120
121 if(GameClient()->m_GameSkin.m_aSpriteWeaponProjectiles[CurWeapon].IsValid())
122 {
123 Graphics()->TextureSet(Texture: GameClient()->m_GameSkin.m_aSpriteWeaponProjectiles[CurWeapon]);
124 Graphics()->SetColor(r: 1.f, g: 1.f, b: 1.f, a: Alpha);
125 Graphics()->RenderQuadContainerAsSprite(ContainerIndex: m_ItemsQuadContainerIndex, QuadOffset: m_aProjectileOffset[CurWeapon], X: Pos.x, Y: Pos.y);
126 }
127}
128
129void CItems::RenderPickup(const CNetObj_Pickup *pPrev, const CNetObj_Pickup *pCurrent, bool IsPredicted, int Flags)
130{
131 int CurWeapon = std::clamp(val: pCurrent->m_Subtype, lo: 0, hi: NUM_WEAPONS - 1);
132 int QuadOffset = 2;
133 float IntraTick = IsPredicted ? Client()->PredIntraGameTick(Conn: g_Config.m_ClDummy) : Client()->IntraGameTick(Conn: g_Config.m_ClDummy);
134 vec2 Pos = mix(a: vec2(pPrev->m_X, pPrev->m_Y), b: vec2(pCurrent->m_X, pCurrent->m_Y), amount: IntraTick);
135 if(pCurrent->m_Type == POWERUP_HEALTH)
136 {
137 QuadOffset = m_PickupHealthOffset;
138 Graphics()->TextureSet(Texture: GameClient()->m_GameSkin.m_SpritePickupHealth);
139 }
140 else if(pCurrent->m_Type == POWERUP_ARMOR)
141 {
142 QuadOffset = m_PickupArmorOffset;
143 Graphics()->TextureSet(Texture: GameClient()->m_GameSkin.m_SpritePickupArmor);
144 }
145 else if(pCurrent->m_Type == POWERUP_WEAPON)
146 {
147 QuadOffset = m_aPickupWeaponOffset[CurWeapon];
148 Graphics()->TextureSet(Texture: GameClient()->m_GameSkin.m_aSpritePickupWeapons[CurWeapon]);
149 }
150 else if(pCurrent->m_Type == POWERUP_NINJA)
151 {
152 QuadOffset = m_PickupNinjaOffset;
153 if(Flags & PICKUPFLAG_ROTATE)
154 GameClient()->m_Effects.PowerupShine(Pos, Size: vec2(18, 96), Alpha: 1.0f);
155 else
156 GameClient()->m_Effects.PowerupShine(Pos, Size: vec2(96, 18), Alpha: 1.0f);
157
158 Graphics()->TextureSet(Texture: GameClient()->m_GameSkin.m_SpritePickupNinja);
159 }
160 else if(pCurrent->m_Type >= POWERUP_ARMOR_SHOTGUN && pCurrent->m_Type <= POWERUP_ARMOR_LASER)
161 {
162 QuadOffset = m_aPickupWeaponArmorOffset[pCurrent->m_Type - POWERUP_ARMOR_SHOTGUN];
163 Graphics()->TextureSet(Texture: GameClient()->m_GameSkin.m_aSpritePickupWeaponArmor[pCurrent->m_Type - POWERUP_ARMOR_SHOTGUN]);
164 }
165 Graphics()->QuadsSetRotation(Angle: 0);
166 Graphics()->SetColor(r: 1.f, g: 1.f, b: 1.f, a: 1.f);
167
168 vec2 Scale = vec2(1, 1);
169 if(Flags & PICKUPFLAG_XFLIP)
170 Scale.x = -Scale.x;
171
172 if(Flags & PICKUPFLAG_YFLIP)
173 Scale.y = -Scale.y;
174
175 if(Flags & PICKUPFLAG_ROTATE)
176 {
177 Graphics()->QuadsSetRotation(Angle: 90.f * (pi / 180));
178 std::swap(a&: Scale.x, b&: Scale.y);
179
180 if(pCurrent->m_Type == POWERUP_NINJA)
181 {
182 if(Flags & PICKUPFLAG_XFLIP)
183 Pos.y += 10.0f;
184 else
185 Pos.y -= 10.0f;
186 }
187 }
188 else
189 {
190 if(pCurrent->m_Type == POWERUP_NINJA)
191 {
192 if(Flags & PICKUPFLAG_XFLIP)
193 Pos.x += 10.0f;
194 else
195 Pos.x -= 10.0f;
196 }
197 }
198
199 static float s_Time = 0.0f;
200 static float s_LastLocalTime = LocalTime();
201 float Offset = Pos.y / 32.0f + Pos.x / 32.0f;
202 s_Time += (LocalTime() - s_LastLocalTime) * GameClient()->GetAnimationPlaybackSpeed();
203 Pos += direction(angle: s_Time * 2.0f + Offset) * 2.5f;
204 s_LastLocalTime = LocalTime();
205
206 Graphics()->RenderQuadContainerAsSprite(ContainerIndex: m_ItemsQuadContainerIndex, QuadOffset, X: Pos.x, Y: Pos.y, ScaleX: Scale.x, ScaleY: Scale.y);
207 Graphics()->QuadsSetRotation(Angle: 0);
208}
209
210void CItems::RenderFlags()
211{
212 for(int Flag = 0; Flag < GameClient()->m_Snap.m_NumFlags; ++Flag)
213 {
214 RenderFlag(pPrev: GameClient()->m_Snap.m_apPrevFlags[Flag], pCurrent: GameClient()->m_Snap.m_apFlags[Flag],
215 pPrevGameData: GameClient()->m_Snap.m_pPrevGameDataObj, pCurGameData: GameClient()->m_Snap.m_pGameDataObj);
216 }
217}
218
219void CItems::RenderFlag(const CNetObj_Flag *pPrev, const CNetObj_Flag *pCurrent, const CNetObj_GameData *pPrevGameData, const CNetObj_GameData *pCurGameData)
220{
221 vec2 Pos = mix(a: vec2(pPrev->m_X, pPrev->m_Y), b: vec2(pCurrent->m_X, pCurrent->m_Y), amount: Client()->IntraGameTick(Conn: g_Config.m_ClDummy));
222 if(pCurGameData)
223 {
224 int FlagCarrier = (pCurrent->m_Team == TEAM_RED) ? pCurGameData->m_FlagCarrierRed : pCurGameData->m_FlagCarrierBlue;
225 // use the flagcarriers position if available
226 if(FlagCarrier >= 0 && GameClient()->m_Snap.m_aCharacters[FlagCarrier].m_Active)
227 Pos = GameClient()->m_aClients[FlagCarrier].m_RenderPos;
228
229 // make sure that the flag isn't interpolated between capture and return
230 if(pPrevGameData &&
231 ((pCurrent->m_Team == TEAM_RED && pPrevGameData->m_FlagCarrierRed != pCurGameData->m_FlagCarrierRed) ||
232 (pCurrent->m_Team == TEAM_BLUE && pPrevGameData->m_FlagCarrierBlue != pCurGameData->m_FlagCarrierBlue)))
233 Pos = vec2(pCurrent->m_X, pCurrent->m_Y);
234 }
235
236 float Size = 42.0f;
237 int QuadOffset;
238 if(pCurrent->m_Team == TEAM_RED)
239 {
240 Graphics()->TextureSet(Texture: GameClient()->m_GameSkin.m_SpriteFlagRed);
241 QuadOffset = m_RedFlagOffset;
242 }
243 else
244 {
245 Graphics()->TextureSet(Texture: GameClient()->m_GameSkin.m_SpriteFlagBlue);
246 QuadOffset = m_BlueFlagOffset;
247 }
248 Graphics()->QuadsSetRotation(Angle: 0.0f);
249 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 1.0f);
250 Graphics()->RenderQuadContainerAsSprite(ContainerIndex: m_ItemsQuadContainerIndex, QuadOffset, X: Pos.x, Y: Pos.y - Size * 0.75f);
251}
252
253void CItems::RenderLaser(const CLaserData *pCurrent, bool IsPredicted)
254{
255 int Type = std::clamp(val: pCurrent->m_Type, lo: -1, hi: NUM_LASERTYPES - 1);
256 int ColorIn, ColorOut;
257 switch(Type)
258 {
259 case LASERTYPE_RIFLE:
260 ColorOut = g_Config.m_ClLaserRifleOutlineColor;
261 ColorIn = g_Config.m_ClLaserRifleInnerColor;
262 break;
263 case LASERTYPE_SHOTGUN:
264 ColorOut = g_Config.m_ClLaserShotgunOutlineColor;
265 ColorIn = g_Config.m_ClLaserShotgunInnerColor;
266 break;
267 case LASERTYPE_DOOR:
268 ColorOut = g_Config.m_ClLaserDoorOutlineColor;
269 ColorIn = g_Config.m_ClLaserDoorInnerColor;
270 break;
271 case LASERTYPE_FREEZE:
272 ColorOut = g_Config.m_ClLaserFreezeOutlineColor;
273 ColorIn = g_Config.m_ClLaserFreezeInnerColor;
274 break;
275 case LASERTYPE_DRAGGER:
276 ColorOut = g_Config.m_ClLaserDraggerOutlineColor;
277 ColorIn = g_Config.m_ClLaserDraggerInnerColor;
278 break;
279 case LASERTYPE_GUN:
280 case LASERTYPE_PLASMA:
281 if(pCurrent->m_Subtype == LASERGUNTYPE_FREEZE || pCurrent->m_Subtype == LASERGUNTYPE_EXPFREEZE)
282 {
283 ColorOut = g_Config.m_ClLaserFreezeOutlineColor;
284 ColorIn = g_Config.m_ClLaserFreezeInnerColor;
285 }
286 else
287 {
288 ColorOut = g_Config.m_ClLaserRifleOutlineColor;
289 ColorIn = g_Config.m_ClLaserRifleInnerColor;
290 }
291 break;
292 default:
293 ColorOut = g_Config.m_ClLaserRifleOutlineColor;
294 ColorIn = g_Config.m_ClLaserRifleInnerColor;
295 }
296
297 bool IsOtherTeam = (pCurrent->m_ExtraInfo && pCurrent->m_Owner >= 0 && GameClient()->IsOtherTeam(ClientId: pCurrent->m_Owner));
298
299 float Alpha = IsOtherTeam ? g_Config.m_ClShowOthersAlpha / 100.0f : 1.f;
300
301 const ColorRGBA OuterColor = color_cast<ColorRGBA>(hsl: ColorHSLA(ColorOut).WithAlpha(alpha: Alpha));
302 const ColorRGBA InnerColor = color_cast<ColorRGBA>(hsl: ColorHSLA(ColorIn).WithAlpha(alpha: Alpha));
303
304 float Ticks;
305 float TicksHead = Client()->GameTick(Conn: g_Config.m_ClDummy);
306 if(Type == LASERTYPE_DOOR)
307 {
308 Ticks = 1.0f;
309 }
310 else if(IsPredicted)
311 {
312 int PredictionTick = Client()->GetPredictionTick();
313 Ticks = (float)(PredictionTick - pCurrent->m_StartTick) + Client()->PredIntraGameTick(Conn: g_Config.m_ClDummy);
314 TicksHead += Client()->PredIntraGameTick(Conn: g_Config.m_ClDummy);
315 }
316 else
317 {
318 Ticks = (float)(Client()->GameTick(Conn: g_Config.m_ClDummy) - pCurrent->m_StartTick) + Client()->IntraGameTick(Conn: g_Config.m_ClDummy);
319 TicksHead += Client()->IntraGameTick(Conn: g_Config.m_ClDummy);
320 }
321
322 if(Type == LASERTYPE_DRAGGER)
323 {
324 TicksHead *= (((pCurrent->m_Subtype >> 1) % 3) * 4.0f) + 1;
325 TicksHead *= (pCurrent->m_Subtype & 1) ? -1 : 1;
326 }
327 RenderLaser(From: pCurrent->m_From, Pos: pCurrent->m_To, OuterColor, InnerColor, TicksBody: Ticks, TicksHead, Type);
328}
329
330void CItems::RenderLaser(vec2 From, vec2 Pos, ColorRGBA OuterColor, ColorRGBA InnerColor, float TicksBody, float TicksHead, int Type) const
331{
332 float Len = distance(a: Pos, b: From);
333
334 if(Len > 0)
335 {
336 if(Type == LASERTYPE_DRAGGER)
337 {
338 // rubber band effect
339 float Thickness = std::sqrt(x: Len) / 5.f;
340 TicksBody = std::clamp(val: Thickness, lo: 1.0f, hi: 5.0f);
341 }
342 vec2 Dir = normalize_pre_length(v: Pos - From, len: Len);
343
344 float Ms = TicksBody * 1000.0f / Client()->GameTickSpeed();
345 float a;
346 if(Type == LASERTYPE_RIFLE || Type == LASERTYPE_SHOTGUN)
347 {
348 int TuneZone = (Client()->State() == IClient::STATE_ONLINE && GameClient()->m_GameWorld.m_WorldConfig.m_UseTuneZones) ? Collision()->IsTune(Index: Collision()->GetMapIndex(Pos: From)) : 0;
349 a = Ms / GameClient()->GetTuning(i: TuneZone)->m_LaserBounceDelay;
350 }
351 else
352 {
353 a = Ms / CTuningParams::DEFAULT.m_LaserBounceDelay;
354 }
355 a = std::clamp(val: a, lo: 0.0f, hi: 1.0f);
356 float Ia = 1 - a;
357
358 Graphics()->TextureClear();
359 Graphics()->QuadsBegin();
360
361 // do outline
362 Graphics()->SetColor(OuterColor);
363 vec2 Out = vec2(Dir.y, -Dir.x) * (7.0f * Ia);
364
365 IGraphics::CFreeformItem Freeform(
366 From - Out, From + Out,
367 Pos - Out, Pos + Out);
368 Graphics()->QuadsDrawFreeform(pArray: &Freeform, Num: 1);
369
370 // do inner
371 Out = vec2(Dir.y, -Dir.x) * (5.0f * Ia);
372 vec2 ExtraOutlinePos = Dir;
373 vec2 ExtraOutlineFrom = Type == LASERTYPE_DOOR ? vec2(0, 0) : Dir;
374 Graphics()->SetColor(InnerColor); // center
375
376 Freeform = IGraphics::CFreeformItem(
377 From - Out + ExtraOutlineFrom, From + Out + ExtraOutlineFrom,
378 Pos - Out - ExtraOutlinePos, Pos + Out - ExtraOutlinePos);
379 Graphics()->QuadsDrawFreeform(pArray: &Freeform, Num: 1);
380
381 Graphics()->QuadsEnd();
382 }
383
384 // render head
385 if(Type == LASERTYPE_DOOR)
386 {
387 Graphics()->TextureClear();
388 Graphics()->QuadsSetRotation(Angle: 0);
389 Graphics()->SetColor(OuterColor);
390 Graphics()->RenderQuadContainerEx(ContainerIndex: m_ItemsQuadContainerIndex, QuadOffset: m_DoorHeadOffset, QuadDrawNum: 1, X: Pos.x - 8.0f, Y: Pos.y - 8.0f);
391 Graphics()->SetColor(InnerColor);
392 Graphics()->RenderQuadContainerEx(ContainerIndex: m_ItemsQuadContainerIndex, QuadOffset: m_DoorHeadOffset, QuadDrawNum: 1, X: Pos.x - 6.0f, Y: Pos.y - 6.0f, ScaleX: 6.f / 8.f, ScaleY: 6.f / 8.f);
393 }
394 else if(Type == LASERTYPE_DRAGGER)
395 {
396 Graphics()->TextureSet(Texture: GameClient()->m_ExtrasSkin.m_SpritePulley);
397 for(int Inner = 0; Inner < 2; ++Inner)
398 {
399 Graphics()->SetColor(Inner ? InnerColor : OuterColor);
400
401 float Size = Inner ? 4.f / 5.f : 1.f;
402
403 // circle at laser end
404 if(Len > 0)
405 {
406 Graphics()->QuadsSetRotation(Angle: 0);
407 Graphics()->RenderQuadContainerAsSprite(ContainerIndex: m_ItemsQuadContainerIndex, QuadOffset: m_PulleyHeadOffset, X: From.x, Y: From.y, ScaleX: Size, ScaleY: Size);
408 }
409
410 //rotating orbs
411 Size = Inner ? 0.75f - 1.f / 5.f : 0.75f;
412 for(int Orb = 0; Orb < 3; ++Orb)
413 {
414 vec2 Offset(10.f, 0);
415 Offset = rotate(a: Offset, angle: Orb * 120 + TicksHead);
416 Graphics()->QuadsSetRotation(Angle: TicksHead + Orb * pi * 2.f / 3.f); // rotate the sprite as well, as it might be customized
417 Graphics()->RenderQuadContainerAsSprite(ContainerIndex: m_ItemsQuadContainerIndex, QuadOffset: m_PulleyHeadOffset, X: From.x + Offset.x, Y: From.y + Offset.y, ScaleX: Size, ScaleY: Size);
418 }
419 }
420 }
421 else if(Type == LASERTYPE_FREEZE)
422 {
423 float Pulsation = 6.f / 5.f + 1.f / 10.f * std::sin(x: TicksHead / 2.f);
424 float Angle = angle(a: Pos - From);
425 Graphics()->TextureSet(Texture: GameClient()->m_ExtrasSkin.m_SpriteHectagon);
426 Graphics()->QuadsSetRotation(Angle);
427 Graphics()->SetColor(OuterColor);
428 Graphics()->RenderQuadContainerAsSprite(ContainerIndex: m_ItemsQuadContainerIndex, QuadOffset: m_FreezeHeadOffset, X: Pos.x, Y: Pos.y, ScaleX: 6.f / 5.f * Pulsation, ScaleY: 6.f / 5.f * Pulsation);
429 Graphics()->TextureSet(Texture: GameClient()->m_ExtrasSkin.m_SpriteParticleSnowflake);
430 // snowflakes are white
431 Graphics()->SetColor(ColorRGBA(1.f, 1.f, 1.f));
432 Graphics()->RenderQuadContainerAsSprite(ContainerIndex: m_ItemsQuadContainerIndex, QuadOffset: m_FreezeHeadOffset, X: Pos.x, Y: Pos.y, ScaleX: Pulsation, ScaleY: Pulsation);
433 }
434 else
435 {
436 int CurParticle = (int)TicksHead % 3;
437 Graphics()->TextureSet(Texture: GameClient()->m_ParticlesSkin.m_aSpriteParticleSplat[CurParticle]);
438 Graphics()->QuadsSetRotation(Angle: (int)TicksHead);
439 Graphics()->SetColor(OuterColor);
440 Graphics()->RenderQuadContainerAsSprite(ContainerIndex: m_ItemsQuadContainerIndex, QuadOffset: m_aParticleSplatOffset[CurParticle], X: Pos.x, Y: Pos.y);
441 Graphics()->SetColor(InnerColor);
442 Graphics()->RenderQuadContainerAsSprite(ContainerIndex: m_ItemsQuadContainerIndex, QuadOffset: m_aParticleSplatOffset[CurParticle], X: Pos.x, Y: Pos.y, ScaleX: 20.f / 24.f, ScaleY: 20.f / 24.f);
443 }
444}
445
446void CItems::OnRender()
447{
448 if(Client()->State() != IClient::STATE_ONLINE && Client()->State() != IClient::STATE_DEMOPLAYBACK)
449 return;
450
451 bool IsSuper = GameClient()->IsLocalCharSuper();
452 int Ticks = Client()->GameTick(Conn: g_Config.m_ClDummy) % Client()->GameTickSpeed();
453 bool BlinkingPickup = (Ticks % 22) < 4;
454 bool BlinkingGun = (Ticks % 22) < 4;
455 bool BlinkingDragger = (Ticks % 22) < 4;
456 bool BlinkingProj = (Ticks % 20) < 2;
457 bool BlinkingProjEx = (Ticks % 6) < 2;
458 bool BlinkingLight = (Ticks % 6) < 2;
459 int SwitcherTeam = GameClient()->SwitchStateTeam();
460 int DraggerStartTick = std::max(a: (Client()->GameTick(Conn: g_Config.m_ClDummy) / 7) * 7, b: Client()->GameTick(Conn: g_Config.m_ClDummy) - 4);
461 int GunStartTick = (Client()->GameTick(Conn: g_Config.m_ClDummy) / 7) * 7;
462
463 bool UsePredicted = GameClient()->Predict() && GameClient()->AntiPingGunfire();
464 auto &aSwitchers = GameClient()->Switchers();
465
466 CScreenRect ScreenRectLaser = Graphics()->GetScreen();
467 CScreenRect ScreenRectProjectile = ScreenRectLaser;
468 CScreenRect ScreenRectPickup = ScreenRectLaser;
469
470 constexpr float TileSize = 64.0f;
471 ScreenRectProjectile.Expand(Size: TileSize);
472 ScreenRectLaser.Expand(Size: TileSize / 2.0f);
473 ScreenRectPickup.Expand(Width: 1.75f * TileSize, Height: 0.75f * TileSize);
474
475 auto IsLaserInside = [&](const CLaserData &LaserData) -> bool {
476 const vec2 &From = LaserData.m_From;
477 const vec2 &To = LaserData.m_To;
478 return !((From.x < ScreenRectLaser.m_TopLeft.x && To.x < ScreenRectLaser.m_TopLeft.x) || (From.x > ScreenRectLaser.m_BottomRight.x && To.x > ScreenRectLaser.m_BottomRight.x) ||
479 (From.y < ScreenRectLaser.m_TopLeft.y && To.y < ScreenRectLaser.m_TopLeft.y) || (From.y > ScreenRectLaser.m_BottomRight.y && To.y > ScreenRectLaser.m_BottomRight.y));
480 };
481
482 if(UsePredicted)
483 {
484 for(auto *pProj = (CProjectile *)GameClient()->m_PrevPredictedWorld.FindFirst(Type: CGameWorld::ENTTYPE_PROJECTILE); pProj; pProj = (CProjectile *)pProj->NextEntity())
485 {
486 if(!IsSuper && pProj->m_Number > 0 && pProj->m_Number < (int)aSwitchers.size() && !aSwitchers[pProj->m_Number].m_aStatus[SwitcherTeam] && (pProj->m_Explosive ? BlinkingProjEx : BlinkingProj))
487 continue;
488
489 CProjectileData Data = pProj->GetData();
490 RenderProjectile(pCurrent: &Data, ItemId: pProj->GetId(), ScreenRect: ScreenRectProjectile);
491 }
492 for(CEntity *pEnt = GameClient()->m_PrevPredictedWorld.FindFirst(Type: CGameWorld::ENTTYPE_LASER); pEnt; pEnt = pEnt->NextEntity())
493 {
494 auto *const pLaser = dynamic_cast<CLaser *>(pEnt);
495 if(!pLaser || pLaser->GetOwner() < 0 || !GameClient()->m_aClients[pLaser->GetOwner()].m_IsPredictedLocal)
496 continue;
497 CLaserData Data = pLaser->GetData();
498 if(!IsLaserInside(Data))
499 continue;
500 RenderLaser(pCurrent: &Data, IsPredicted: true);
501 }
502 for(auto *pPickup = (CPickup *)GameClient()->m_PrevPredictedWorld.FindFirst(Type: CGameWorld::ENTTYPE_PICKUP); pPickup; pPickup = (CPickup *)pPickup->NextEntity())
503 {
504 if(!IsSuper && pPickup->m_Layer == LAYER_SWITCH && pPickup->m_Number > 0 && pPickup->m_Number < (int)aSwitchers.size() && !aSwitchers[pPickup->m_Number].m_aStatus[SwitcherTeam] && BlinkingPickup)
505 continue;
506
507 if(pPickup->InDDNetTile())
508 {
509 if(auto *pPrev = (CPickup *)GameClient()->m_PrevPredictedWorld.GetEntity(Id: pPickup->GetId(), EntityType: CGameWorld::ENTTYPE_PICKUP))
510 {
511 CNetObj_Pickup Data, Prev;
512 pPickup->FillInfo(pPickup: &Data);
513 pPrev->FillInfo(pPickup: &Prev);
514 RenderPickup(pPrev: &Prev, pCurrent: &Data, IsPredicted: true, Flags: pPickup->Flags());
515 }
516 }
517 }
518 }
519
520 for(const CSnapEntities &Ent : GameClient()->SnapEntities())
521 {
522 const IClient::CSnapItem Item = Ent.m_Item;
523 const void *pData = Item.m_pData;
524 const CNetObj_EntityEx *pEntEx = Ent.m_pDataEx;
525
526 if(Item.m_Type == NETOBJTYPE_PROJECTILE || Item.m_Type == NETOBJTYPE_DDRACEPROJECTILE || Item.m_Type == NETOBJTYPE_DDNETPROJECTILE)
527 {
528 CProjectileData Data = ExtractProjectileInfo(NetObjType: Item.m_Type, pData, pGameWorld: &GameClient()->m_GameWorld, pEntEx);
529 bool Inactive = !IsSuper && Data.m_SwitchNumber > 0 && Data.m_SwitchNumber < (int)aSwitchers.size() && !aSwitchers[Data.m_SwitchNumber].m_aStatus[SwitcherTeam];
530 if(Inactive && (Data.m_Explosive ? BlinkingProjEx : BlinkingProj))
531 continue;
532 if(UsePredicted)
533
534 {
535 if(auto *pProj = (CProjectile *)GameClient()->m_GameWorld.FindMatch(ObjId: Item.m_Id, ObjType: Item.m_Type, pObjData: pData))
536 {
537 bool IsOtherTeam = GameClient()->IsOtherTeam(ClientId: pProj->GetOwner());
538 if(pProj->m_LastRenderTick <= 0 && (pProj->m_Type != WEAPON_SHOTGUN || (!pProj->m_Freeze && !pProj->m_Explosive)) // skip ddrace shotgun bullets
539 && (pProj->m_Type == WEAPON_SHOTGUN || absolute(a: length(a: pProj->m_Direction) - 1.f) < 0.02f) // workaround to skip grenades on ball mod
540 && (pProj->GetOwner() < 0 || !GameClient()->m_aClients[pProj->GetOwner()].m_IsPredictedLocal || IsOtherTeam) // skip locally predicted projectiles
541 && !Client()->SnapFindItem(SnapId: IClient::SNAP_PREV, Type: Item.m_Type, Id: Item.m_Id))
542 {
543 ReconstructSmokeTrail(pCurrent: &Data, DestroyTick: pProj->m_DestroyTick);
544 }
545 pProj->m_LastRenderTick = Client()->GameTick(Conn: g_Config.m_ClDummy);
546 if(!IsOtherTeam)
547 continue;
548 }
549 }
550 RenderProjectile(pCurrent: &Data, ItemId: Item.m_Id, ScreenRect: ScreenRectProjectile);
551 }
552 else if(Item.m_Type == NETOBJTYPE_PICKUP || Item.m_Type == NETOBJTYPE_DDNETPICKUP)
553 {
554 CPickupData Data = ExtractPickupInfo(NetObjType: Item.m_Type, pData, pEntEx);
555 if(!ScreenRectPickup.Inside(Position: Data.m_Pos))
556 continue;
557 bool Inactive = !IsSuper && Data.m_SwitchNumber > 0 && Data.m_SwitchNumber < (int)aSwitchers.size() && !aSwitchers[Data.m_SwitchNumber].m_aStatus[SwitcherTeam];
558
559 if(Inactive && BlinkingPickup)
560 continue;
561 if(UsePredicted)
562 {
563 auto *pPickup = (CPickup *)GameClient()->m_GameWorld.FindMatch(ObjId: Item.m_Id, ObjType: Item.m_Type, pObjData: pData);
564 if(pPickup && pPickup->InDDNetTile())
565 continue;
566 }
567 const void *pPrev = Client()->SnapFindItem(SnapId: IClient::SNAP_PREV, Type: Item.m_Type, Id: Item.m_Id);
568 if(pPrev)
569 RenderPickup(pPrev: (const CNetObj_Pickup *)pPrev, pCurrent: (const CNetObj_Pickup *)pData, IsPredicted: false, Flags: Data.m_Flags);
570 }
571 else if(Item.m_Type == NETOBJTYPE_LASER || Item.m_Type == NETOBJTYPE_DDNETLASER)
572 {
573 if(UsePredicted)
574 {
575 auto *pLaser = dynamic_cast<CLaser *>(GameClient()->m_GameWorld.FindMatch(ObjId: Item.m_Id, ObjType: Item.m_Type, pObjData: pData));
576 if(pLaser && pLaser->GetOwner() >= 0 && GameClient()->m_aClients[pLaser->GetOwner()].m_IsPredictedLocal)
577 continue;
578 }
579
580 CLaserData Data = ExtractLaserInfo(NetObjType: Item.m_Type, pData, pGameWorld: &GameClient()->m_GameWorld, pEntEx);
581 if(!IsLaserInside(Data))
582 continue;
583 bool Inactive = !IsSuper && Data.m_SwitchNumber > 0 && Data.m_SwitchNumber < (int)aSwitchers.size() && !aSwitchers[Data.m_SwitchNumber].m_aStatus[SwitcherTeam];
584
585 bool IsEntBlink = false;
586 int EntStartTick = -1;
587 if(Data.m_Type == LASERTYPE_FREEZE)
588 {
589 IsEntBlink = BlinkingLight;
590 EntStartTick = DraggerStartTick;
591 }
592 else if(Data.m_Type == LASERTYPE_GUN)
593 {
594 IsEntBlink = BlinkingGun;
595 EntStartTick = GunStartTick;
596 }
597 else if(Data.m_Type == LASERTYPE_DRAGGER)
598 {
599 IsEntBlink = BlinkingDragger;
600 EntStartTick = DraggerStartTick;
601 }
602 else if(Data.m_Type == LASERTYPE_DOOR)
603 {
604 if(Data.m_Predict && (Inactive || IsSuper))
605 {
606 Data.m_From.x = Data.m_To.x;
607 Data.m_From.y = Data.m_To.y;
608 }
609 EntStartTick = Client()->GameTick(Conn: g_Config.m_ClDummy);
610 }
611 else
612 {
613 IsEntBlink = BlinkingDragger;
614 EntStartTick = Client()->GameTick(Conn: g_Config.m_ClDummy);
615 }
616
617 if(Data.m_Predict && Inactive && IsEntBlink)
618 {
619 continue;
620 }
621
622 if(Data.m_StartTick <= 0 && EntStartTick != -1)
623 {
624 Data.m_StartTick = EntStartTick;
625 }
626
627 RenderLaser(pCurrent: &Data);
628 }
629 }
630
631 RenderFlags();
632
633 Graphics()->QuadsSetRotation(Angle: 0);
634 Graphics()->SetColor(r: 1.f, g: 1.f, b: 1.f, a: 1.f);
635}
636
637void CItems::OnInit()
638{
639 Graphics()->QuadsSetRotation(Angle: 0);
640 Graphics()->SetColor(r: 1.f, g: 1.f, b: 1.f, a: 1.f);
641
642 m_ItemsQuadContainerIndex = Graphics()->CreateQuadContainer(AutomaticUpload: false);
643
644 Graphics()->QuadsSetSubset(TopLeftU: 0, TopLeftV: 0, BottomRightU: 1, BottomRightV: 1);
645 m_RedFlagOffset = Graphics()->QuadContainerAddSprite(QuadContainerIndex: m_ItemsQuadContainerIndex, X: -21.f, Y: -42.f, Width: 42.f, Height: 84.f);
646 Graphics()->QuadsSetSubset(TopLeftU: 0, TopLeftV: 0, BottomRightU: 1, BottomRightV: 1);
647 m_BlueFlagOffset = Graphics()->QuadContainerAddSprite(QuadContainerIndex: m_ItemsQuadContainerIndex, X: -21.f, Y: -42.f, Width: 42.f, Height: 84.f);
648
649 float ScaleX, ScaleY;
650 Graphics()->GetSpriteScale(Id: SPRITE_PICKUP_HEALTH, ScaleX, ScaleY);
651 Graphics()->QuadsSetSubset(TopLeftU: 0, TopLeftV: 0, BottomRightU: 1, BottomRightV: 1);
652 m_PickupHealthOffset = Graphics()->QuadContainerAddSprite(QuadContainerIndex: m_ItemsQuadContainerIndex, Width: 64.f * ScaleX, Height: 64.f * ScaleY);
653 Graphics()->GetSpriteScale(Id: SPRITE_PICKUP_ARMOR, ScaleX, ScaleY);
654 Graphics()->QuadsSetSubset(TopLeftU: 0, TopLeftV: 0, BottomRightU: 1, BottomRightV: 1);
655 m_PickupArmorOffset = Graphics()->QuadContainerAddSprite(QuadContainerIndex: m_ItemsQuadContainerIndex, Width: 64.f * ScaleX, Height: 64.f * ScaleY);
656
657 for(int i = 0; i < NUM_WEAPONS; ++i)
658 {
659 Graphics()->GetSpriteScale(pSprite: g_pData->m_Weapons.m_aId[i].m_pSpriteBody, ScaleX, ScaleY);
660 Graphics()->QuadsSetSubset(TopLeftU: 0, TopLeftV: 0, BottomRightU: 1, BottomRightV: 1);
661 m_aPickupWeaponOffset[i] = Graphics()->QuadContainerAddSprite(QuadContainerIndex: m_ItemsQuadContainerIndex, Width: g_pData->m_Weapons.m_aId[i].m_VisualSize * ScaleX, Height: g_pData->m_Weapons.m_aId[i].m_VisualSize * ScaleY);
662 }
663 Graphics()->GetSpriteScale(Id: SPRITE_PICKUP_NINJA, ScaleX, ScaleY);
664 Graphics()->QuadsSetSubset(TopLeftU: 0, TopLeftV: 0, BottomRightU: 1, BottomRightV: 1);
665 m_PickupNinjaOffset = Graphics()->QuadContainerAddSprite(QuadContainerIndex: m_ItemsQuadContainerIndex, Width: 128.f * ScaleX, Height: 128.f * ScaleY);
666
667 for(int i = 0; i < 4; i++)
668 {
669 Graphics()->GetSpriteScale(Id: SPRITE_PICKUP_ARMOR_SHOTGUN + i, ScaleX, ScaleY);
670 Graphics()->QuadsSetSubset(TopLeftU: 0, TopLeftV: 0, BottomRightU: 1, BottomRightV: 1);
671 m_aPickupWeaponArmorOffset[i] = Graphics()->QuadContainerAddSprite(QuadContainerIndex: m_ItemsQuadContainerIndex, Width: 64.f * ScaleX, Height: 64.f * ScaleY);
672 }
673
674 for(int &ProjectileOffset : m_aProjectileOffset)
675 {
676 Graphics()->QuadsSetSubset(TopLeftU: 0, TopLeftV: 0, BottomRightU: 1, BottomRightV: 1);
677 ProjectileOffset = Graphics()->QuadContainerAddSprite(QuadContainerIndex: m_ItemsQuadContainerIndex, Size: 32.f);
678 }
679
680 for(int &ParticleSplatOffset : m_aParticleSplatOffset)
681 {
682 Graphics()->QuadsSetSubset(TopLeftU: 0, TopLeftV: 0, BottomRightU: 1, BottomRightV: 1);
683 ParticleSplatOffset = Graphics()->QuadContainerAddSprite(QuadContainerIndex: m_ItemsQuadContainerIndex, Size: 24.f);
684 }
685
686 Graphics()->GetSpriteScale(Id: SPRITE_PART_PULLEY, ScaleX, ScaleY);
687 Graphics()->QuadsSetSubset(TopLeftU: 0, TopLeftV: 0, BottomRightU: 1, BottomRightV: 1);
688 m_PulleyHeadOffset = Graphics()->QuadContainerAddSprite(QuadContainerIndex: m_ItemsQuadContainerIndex, Size: 20.f * ScaleX);
689
690 Graphics()->GetSpriteScale(Id: SPRITE_PART_HECTAGON, ScaleX, ScaleY);
691 Graphics()->QuadsSetSubset(TopLeftU: 0, TopLeftV: 0, BottomRightU: 1, BottomRightV: 1);
692 m_FreezeHeadOffset = Graphics()->QuadContainerAddSprite(QuadContainerIndex: m_ItemsQuadContainerIndex, Size: 20.f * ScaleX);
693
694 IGraphics::CQuadItem Brick(0, 0, 16.0f, 16.0f);
695 m_DoorHeadOffset = Graphics()->QuadContainerAddQuads(ContainerIndex: m_ItemsQuadContainerIndex, pArray: &Brick, Num: 1);
696
697 Graphics()->QuadContainerUpload(ContainerIndex: m_ItemsQuadContainerIndex);
698}
699
700void CItems::ReconstructSmokeTrail(const CProjectileData *pCurrent, int DestroyTick)
701{
702 bool LocalPlayerInGame = false;
703
704 if(GameClient()->m_Snap.m_pLocalInfo)
705 LocalPlayerInGame = GameClient()->m_aClients[GameClient()->m_Snap.m_pLocalInfo->m_ClientId].m_Team != TEAM_SPECTATORS;
706 if(!GameClient()->AntiPingGunfire() || !LocalPlayerInGame)
707 return;
708
709 int PredictionTick = Client()->GetPredictionTick();
710
711 if(PredictionTick == pCurrent->m_StartTick)
712 return;
713
714 // get positions
715 float Curvature = 0;
716 float Speed = 0;
717 const CTuningParams *pTuning = GameClient()->GetTuning(i: pCurrent->m_TuneZone);
718
719 if(pCurrent->m_Type == WEAPON_GRENADE)
720 {
721 Curvature = pTuning->m_GrenadeCurvature;
722 Speed = pTuning->m_GrenadeSpeed;
723 }
724 else if(pCurrent->m_Type == WEAPON_SHOTGUN)
725 {
726 Curvature = pTuning->m_ShotgunCurvature;
727 Speed = pTuning->m_ShotgunSpeed;
728 }
729 else if(pCurrent->m_Type == WEAPON_GUN)
730 {
731 Curvature = pTuning->m_GunCurvature;
732 Speed = pTuning->m_GunSpeed;
733 }
734
735 float Pt = ((float)(PredictionTick - pCurrent->m_StartTick) + Client()->PredIntraGameTick(Conn: g_Config.m_ClDummy)) / (float)Client()->GameTickSpeed();
736 if(Pt < 0)
737 return; // projectile haven't been shot yet
738
739 float Gt = (Client()->PrevGameTick(Conn: g_Config.m_ClDummy) - pCurrent->m_StartTick) / (float)Client()->GameTickSpeed() + Client()->GameTickTime(Conn: g_Config.m_ClDummy);
740
741 float Alpha = 1.f;
742 if(pCurrent->m_ExtraInfo && pCurrent->m_Owner >= 0 && GameClient()->IsOtherTeam(ClientId: pCurrent->m_Owner))
743 {
744 Alpha = g_Config.m_ClShowOthersAlpha / 100.0f;
745 }
746
747 float T = Pt;
748 if(DestroyTick >= 0)
749 T = std::min(a: Pt, b: ((float)(DestroyTick - 1 - pCurrent->m_StartTick) + Client()->PredIntraGameTick(Conn: g_Config.m_ClDummy)) / (float)Client()->GameTickSpeed());
750
751 float MinTrailSpan = 0.4f * ((pCurrent->m_Type == WEAPON_GRENADE) ? 0.5f : 0.25f);
752 float Step = std::max(a: Client()->FrameTimeAverage(), b: (pCurrent->m_Type == WEAPON_GRENADE) ? 0.02f : 0.01f);
753 for(int i = 1 + (int)(Gt / Step); i < (int)(T / Step); i++)
754 {
755 float t = Step * (float)i + 0.4f * Step * random_float(min: -0.5f, max: 0.5f);
756 vec2 Pos = CalcPos(Pos: pCurrent->m_StartPos, Velocity: pCurrent->m_StartVel, Curvature, Speed, Time: t);
757 vec2 PrevPos = CalcPos(Pos: pCurrent->m_StartPos, Velocity: pCurrent->m_StartVel, Curvature, Speed, Time: t - 0.001f);
758 vec2 Vel = Pos - PrevPos;
759 float TimePassed = Pt - t;
760 if(Pt - MinTrailSpan > 0.01f)
761 TimePassed = std::min(a: TimePassed, b: (TimePassed - MinTrailSpan) / (Pt - MinTrailSpan) * (MinTrailSpan * 0.5f) + MinTrailSpan);
762 // add particle for this projectile
763 if(pCurrent->m_Type == WEAPON_GRENADE)
764 GameClient()->m_Effects.SmokeTrail(Pos, Vel: Vel * -1, Alpha, TimePassed);
765 else
766 GameClient()->m_Effects.BulletTrail(Pos, Alpha, TimePassed);
767 }
768}
769