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