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