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