| 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 | |
| 22 | void 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 | |
| 139 | void 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 | |
| 230 | void 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 | |
| 239 | void 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 | |
| 273 | void 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 | |
| 350 | void 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 = Dir; |
| 393 | vec2 = 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 | |
| 466 | void 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 | |
| 634 | void 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 | |
| 697 | void 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 | |