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 "projectile.h"
4
5#include "character.h"
6
7#include <engine/shared/config.h>
8
9#include <generated/protocol.h>
10
11#include <game/mapitems.h>
12#include <game/server/gamecontext.h>
13#include <game/server/gamemodes/ddnet.h>
14
15CProjectile::CProjectile(
16 CGameWorld *pGameWorld,
17 int Type,
18 int Owner,
19 vec2 Pos,
20 vec2 Dir,
21 int Span,
22 bool Freeze,
23 bool Explosive,
24 int SoundImpact,
25 vec2 InitDir,
26 int Layer,
27 int Number) :
28 CEntity(pGameWorld, CGameWorld::ENTTYPE_PROJECTILE, true)
29{
30 m_Type = Type;
31 m_Pos = Pos;
32 m_Direction = Dir;
33 m_LifeSpan = Span;
34 m_Owner = Owner;
35 m_SoundImpact = SoundImpact;
36 m_StartTick = Server()->Tick();
37 m_Explosive = Explosive;
38
39 m_Layer = Layer;
40 m_Number = Number;
41 m_Bouncing = 0;
42 m_Freeze = Freeze;
43
44 m_InitDir = InitDir;
45 m_TuneZone = GameServer()->Collision()->IsTune(Index: GameServer()->Collision()->GetMapIndex(Pos: m_Pos));
46
47 CCharacter *pOwnerChar = GameServer()->GetPlayerChar(ClientId: m_Owner);
48 m_BelongsToPracticeTeam = pOwnerChar && pOwnerChar->Teams()->IsPractice(Team: pOwnerChar->Team());
49 m_DDRaceTeam = m_Owner == -1 ? 0 : GameServer()->GetDDRaceTeam(ClientId: m_Owner);
50 m_IsSolo = pOwnerChar && pOwnerChar->GetCore().m_Solo;
51
52 GameWorld()->InsertEntity(pEntity: this);
53}
54
55void CProjectile::Reset()
56{
57 m_MarkedForDestroy = true;
58}
59
60vec2 CProjectile::GetPos(float Time)
61{
62 float Curvature = 0;
63 float Speed = 0;
64 CTuningParams *pTuning = GetTuning(i: m_TuneZone);
65
66 switch(m_Type)
67 {
68 case WEAPON_GRENADE:
69 Curvature = pTuning->m_GrenadeCurvature;
70 Speed = pTuning->m_GrenadeSpeed;
71 break;
72
73 case WEAPON_SHOTGUN:
74 Curvature = pTuning->m_ShotgunCurvature;
75 Speed = pTuning->m_ShotgunSpeed;
76 break;
77
78 case WEAPON_GUN:
79 Curvature = pTuning->m_GunCurvature;
80 Speed = pTuning->m_GunSpeed;
81 break;
82 }
83
84 return CalcPos(Pos: m_Pos, Velocity: m_Direction, Curvature, Speed, Time);
85}
86
87void CProjectile::Tick()
88{
89 float Pt = (Server()->Tick() - m_StartTick - 1) / (float)Server()->TickSpeed();
90 float Ct = (Server()->Tick() - m_StartTick) / (float)Server()->TickSpeed();
91 vec2 PrevPos = GetPos(Time: Pt);
92 vec2 CurPos = GetPos(Time: Ct);
93 vec2 ColPos;
94 vec2 NewPos;
95 int Collide = GameServer()->Collision()->IntersectLine(Pos0: PrevPos, Pos1: CurPos, pOutCollision: &ColPos, pOutBeforeCollision: &NewPos);
96 CCharacter *pOwnerChar = nullptr;
97
98 if(m_Owner >= 0)
99 pOwnerChar = GameServer()->GetPlayerChar(ClientId: m_Owner);
100
101 CCharacter *pTargetChr = nullptr;
102
103 if(pOwnerChar ? !pOwnerChar->GrenadeHitDisabled() : g_Config.m_SvHit)
104 pTargetChr = GameServer()->m_World.IntersectCharacter(Pos0: PrevPos, Pos1: ColPos, Radius: m_Freeze ? 1.0f : 6.0f, NewPos&: ColPos, pNotThis: pOwnerChar, CollideWith: m_Owner);
105
106 if(m_LifeSpan > -1)
107 m_LifeSpan--;
108
109 CClientMask TeamMask = CClientMask().set();
110 bool IsWeaponCollide = false;
111 if(
112 pOwnerChar &&
113 pTargetChr &&
114 pOwnerChar->IsAlive() &&
115 pTargetChr->IsAlive() &&
116 !pTargetChr->CanCollide(ClientId: m_Owner))
117 {
118 IsWeaponCollide = true;
119 }
120 if(pOwnerChar && pOwnerChar->IsAlive())
121 {
122 TeamMask = pOwnerChar->TeamMask();
123 }
124 else if(m_Owner >= 0 && (m_Type != WEAPON_GRENADE || g_Config.m_SvDestroyBulletsOnDeath || m_BelongsToPracticeTeam))
125 {
126 m_MarkedForDestroy = true;
127 return;
128 }
129
130 if(((pTargetChr && (pOwnerChar ? !pOwnerChar->GrenadeHitDisabled() : g_Config.m_SvHit || m_Owner == -1 || pTargetChr == pOwnerChar)) || Collide || GameLayerClipped(CheckPos: CurPos)) && !IsWeaponCollide)
131 {
132 if(m_Explosive /*??*/ && (!pTargetChr || (pTargetChr && (!m_Freeze || (m_Type == WEAPON_SHOTGUN && Collide)))))
133 {
134 int Number = 1;
135 if(GameServer()->EmulateBug(Bug: BUG_GRENADE_DOUBLEEXPLOSION) && m_LifeSpan == -1)
136 {
137 Number = 2;
138 }
139 for(int i = 0; i < Number; i++)
140 {
141 GameServer()->CreateExplosion(Pos: ColPos, Owner: m_Owner, Weapon: m_Type, NoDamage: m_Owner == -1, ActivatedTeam: (!pTargetChr ? -1 : pTargetChr->Team()),
142 Mask: (m_Owner != -1) ? TeamMask : CClientMask().set());
143 GameServer()->CreateSound(Pos: ColPos, Sound: m_SoundImpact,
144 Mask: (m_Owner != -1) ? TeamMask : CClientMask().set());
145 }
146 }
147 else if(m_Freeze)
148 {
149 CEntity *apEnts[MAX_CLIENTS];
150 int Num = GameWorld()->FindEntities(Pos: CurPos, Radius: 1.0f, ppEnts: apEnts, Max: MAX_CLIENTS, Type: CGameWorld::ENTTYPE_CHARACTER);
151 for(int i = 0; i < Num; ++i)
152 {
153 auto *pChr = static_cast<CCharacter *>(apEnts[i]);
154 if(pChr && (m_Layer != LAYER_SWITCH || (m_Layer == LAYER_SWITCH && m_Number > 0 && Switchers()[m_Number].m_aStatus[pChr->Team()])))
155 pChr->Freeze();
156 }
157 }
158 else if(pTargetChr)
159 pTargetChr->TakeDamage(Force: vec2(0, 0), Dmg: 0, From: m_Owner, Weapon: m_Type);
160
161 if(pOwnerChar && !GameLayerClipped(CheckPos: ColPos) &&
162 ((m_Type == WEAPON_GRENADE && pOwnerChar->HasTelegunGrenade()) || (m_Type == WEAPON_GUN && pOwnerChar->HasTelegunGun())))
163 {
164 int MapIndex = GameServer()->Collision()->GetPureMapIndex(Pos: pTargetChr ? pTargetChr->m_Pos : ColPos);
165 int TileFIndex = GameServer()->Collision()->GetFrontTileIndex(Index: MapIndex);
166 bool IsSwitchTeleGun = GameServer()->Collision()->GetSwitchType(Index: MapIndex) == TILE_ALLOW_TELE_GUN;
167 bool IsBlueSwitchTeleGun = GameServer()->Collision()->GetSwitchType(Index: MapIndex) == TILE_ALLOW_BLUE_TELE_GUN;
168
169 if(IsSwitchTeleGun || IsBlueSwitchTeleGun)
170 {
171 // Delay specifies which weapon the tile should work for.
172 // Delay = 0 means all.
173 int Delay = GameServer()->Collision()->GetSwitchDelay(Index: MapIndex);
174
175 if(Delay == 1 && m_Type != WEAPON_GUN)
176 IsSwitchTeleGun = IsBlueSwitchTeleGun = false;
177 if(Delay == 2 && m_Type != WEAPON_GRENADE)
178 IsSwitchTeleGun = IsBlueSwitchTeleGun = false;
179 if(Delay == 3 && m_Type != WEAPON_LASER)
180 IsSwitchTeleGun = IsBlueSwitchTeleGun = false;
181 }
182
183 if(TileFIndex == TILE_ALLOW_TELE_GUN || TileFIndex == TILE_ALLOW_BLUE_TELE_GUN || IsSwitchTeleGun || IsBlueSwitchTeleGun || pTargetChr)
184 {
185 bool Found;
186 vec2 PossiblePos;
187
188 if(!Collide)
189 Found = GetNearestAirPosPlayer(PlayerPos: pTargetChr ? pTargetChr->m_Pos : ColPos, pOutPos: &PossiblePos);
190 else
191 Found = GetNearestAirPos(Pos: NewPos, PrevPos: CurPos, pOutPos: &PossiblePos);
192
193 if(Found)
194 {
195 pOwnerChar->m_TeleGunPos = PossiblePos;
196 pOwnerChar->m_TeleGunTeleport = true;
197 pOwnerChar->m_IsBlueTeleGunTeleport = TileFIndex == TILE_ALLOW_BLUE_TELE_GUN || IsBlueSwitchTeleGun;
198 }
199 }
200 }
201
202 if(Collide && m_Bouncing != 0)
203 {
204 m_StartTick = Server()->Tick();
205 m_Pos = NewPos + (-(m_Direction * 4));
206 if(m_Bouncing == 1)
207 m_Direction.x = -m_Direction.x;
208 else if(m_Bouncing == 2)
209 m_Direction.y = -m_Direction.y;
210 if(absolute(a: m_Direction.x) < 1e-6f)
211 m_Direction.x = 0;
212 if(absolute(a: m_Direction.y) < 1e-6f)
213 m_Direction.y = 0;
214 m_Pos += m_Direction;
215 }
216 else if(m_Type == WEAPON_GUN)
217 {
218 GameServer()->CreateDamageInd(Pos: CurPos, AngleMod: -std::atan2(y: m_Direction.x, x: m_Direction.y), Amount: 10, Mask: (m_Owner != -1) ? TeamMask : CClientMask().set());
219 m_MarkedForDestroy = true;
220 return;
221 }
222 else
223 {
224 if(!m_Freeze)
225 {
226 m_MarkedForDestroy = true;
227 return;
228 }
229 }
230 }
231 if(m_LifeSpan == -1)
232 {
233 if(m_Explosive)
234 {
235 if(m_Owner >= 0)
236 pOwnerChar = GameServer()->GetPlayerChar(ClientId: m_Owner);
237
238 TeamMask = CClientMask().set();
239 if(pOwnerChar && pOwnerChar->IsAlive())
240 {
241 TeamMask = pOwnerChar->TeamMask();
242 }
243
244 GameServer()->CreateExplosion(Pos: ColPos, Owner: m_Owner, Weapon: m_Type, NoDamage: m_Owner == -1, ActivatedTeam: (!pOwnerChar ? -1 : pOwnerChar->Team()),
245 Mask: (m_Owner != -1) ? TeamMask : CClientMask().set());
246 GameServer()->CreateSound(Pos: ColPos, Sound: m_SoundImpact,
247 Mask: (m_Owner != -1) ? TeamMask : CClientMask().set());
248 }
249 m_MarkedForDestroy = true;
250 return;
251 }
252
253 int x = GameServer()->Collision()->GetIndex(PrevPos, Pos: CurPos);
254 int z;
255 if(g_Config.m_SvOldTeleportWeapons)
256 z = GameServer()->Collision()->IsTeleport(Index: x);
257 else
258 z = GameServer()->Collision()->IsTeleportWeapon(Index: x);
259 if(z && !GameServer()->Collision()->TeleOuts(Number: z - 1).empty())
260 {
261 int TeleOut = GameServer()->m_World.m_Core.RandomOr0(BelowThis: GameServer()->Collision()->TeleOuts(Number: z - 1).size());
262 m_Pos = GameServer()->Collision()->TeleOuts(Number: z - 1)[TeleOut];
263 m_StartTick = Server()->Tick();
264 }
265}
266
267void CProjectile::TickPaused()
268{
269 ++m_StartTick;
270}
271
272CNetObj_Projectile CProjectile::NetInfoVanilla() const
273{
274 CNetObj_Projectile Result = {};
275 Result.m_X = (int)m_Pos.x;
276 Result.m_Y = (int)m_Pos.y;
277 Result.m_VelX = (int)(m_Direction.x * 100.0f);
278 Result.m_VelY = (int)(m_Direction.y * 100.0f);
279 Result.m_StartTick = m_StartTick;
280 Result.m_Type = m_Type;
281 return Result;
282}
283
284bool CProjectile::NetIsInfoLegacyCompatible() const
285{
286 const int MaxPos = 0x7fffffff / 100;
287 if(absolute(a: (int)m_Pos.y) + 1 >= MaxPos || absolute(a: (int)m_Pos.x) + 1 >= MaxPos)
288 {
289 //If the modified data would be too large to fit in an integer, send normal data instead
290 return false;
291 }
292 return true;
293}
294
295CNetObj_DDRaceProjectile CProjectile::NetInfoLegacy() const
296{
297 dbg_assert(NetIsInfoLegacyCompatible(), "can't send incompatible projectile");
298
299 //Send additional/modified info, by modifying the fields of the netobj
300 float Angle = -std::atan2(y: m_Direction.x, x: m_Direction.y);
301
302 int Data = 0;
303 Data |= (absolute(a: m_Owner) & 255) << 0;
304 if(m_Owner < 0)
305 Data |= LEGACYPROJECTILEFLAG_NO_OWNER;
306 //This bit tells the client to use the extra info
307 Data |= LEGACYPROJECTILEFLAG_IS_DDNET;
308 // LEGACYPROJECTILEFLAG_BOUNCE_HORIZONTAL, LEGACYPROJECTILEFLAG_BOUNCE_VERTICAL
309 Data |= (m_Bouncing & 3) << 10;
310 if(m_Explosive)
311 Data |= LEGACYPROJECTILEFLAG_EXPLOSIVE;
312 if(m_Freeze)
313 Data |= LEGACYPROJECTILEFLAG_FREEZE;
314
315 CNetObj_DDRaceProjectile Result = {};
316 Result.m_X = (int)(m_Pos.x * 100.0f);
317 Result.m_Y = (int)(m_Pos.y * 100.0f);
318 Result.m_Angle = (int)(Angle * 1000000.0f);
319 Result.m_Data = Data;
320 Result.m_StartTick = m_StartTick;
321 Result.m_Type = m_Type;
322 return Result;
323}
324
325CNetObj_DDNetProjectile CProjectile::NetInfo() const
326{
327 CNetObj_DDNetProjectile Result = {};
328
329 int Flags = 0;
330 if(m_Bouncing & 1)
331 {
332 Flags |= PROJECTILEFLAG_BOUNCE_HORIZONTAL;
333 }
334 if(m_Bouncing & 2)
335 {
336 Flags |= PROJECTILEFLAG_BOUNCE_VERTICAL;
337 }
338 if(m_Explosive)
339 {
340 Flags |= PROJECTILEFLAG_EXPLOSIVE;
341 }
342 if(m_Freeze)
343 {
344 Flags |= PROJECTILEFLAG_FREEZE;
345 }
346
347 if(m_Owner < 0)
348 {
349 Result.m_VelX = round_to_int(f: m_Direction.x * 1e6f);
350 Result.m_VelY = round_to_int(f: m_Direction.y * 1e6f);
351 }
352 else
353 {
354 Result.m_VelX = round_to_int(f: m_InitDir.x);
355 Result.m_VelY = round_to_int(f: m_InitDir.y);
356 Flags |= PROJECTILEFLAG_NORMALIZE_VEL;
357 }
358
359 Result.m_X = round_to_int(f: m_Pos.x * 100.0f);
360 Result.m_Y = round_to_int(f: m_Pos.y * 100.0f);
361 Result.m_Type = m_Type;
362 Result.m_StartTick = m_StartTick;
363 Result.m_Owner = m_Owner;
364 Result.m_SwitchNumber = m_Number;
365 Result.m_TuneZone = m_TuneZone;
366 Result.m_Flags = Flags;
367 return Result;
368}
369
370void CProjectile::Snap(int SnappingClient)
371{
372 float Ct = (Server()->Tick() - m_StartTick) / (float)Server()->TickSpeed();
373
374 if(NetworkClipped(SnappingClient, CheckPos: GetPos(Time: Ct)) || !GetId().has_value())
375 return;
376
377 int SnappingClientVersion = GameServer()->GetClientVersion(ClientId: SnappingClient);
378 if(SnappingClientVersion < VERSION_DDNET_ENTITY_NETOBJS)
379 {
380 CCharacter *pSnapChar = GameServer()->GetPlayerChar(ClientId: SnappingClient);
381 int Tick = (Server()->Tick() % Server()->TickSpeed()) % ((m_Explosive) ? 6 : 20);
382 if(pSnapChar && pSnapChar->IsAlive() && (m_Layer == LAYER_SWITCH && m_Number > 0 && !Switchers()[m_Number].m_aStatus[pSnapChar->Team()] && (!Tick)))
383 return;
384 }
385
386 CCharacter *pOwnerChar = nullptr;
387 CClientMask TeamMask = CClientMask().set();
388
389 if(m_Owner >= 0)
390 pOwnerChar = GameServer()->GetPlayerChar(ClientId: m_Owner);
391
392 if(pOwnerChar && pOwnerChar->IsAlive())
393 TeamMask = pOwnerChar->TeamMask();
394
395 if(SnappingClient != SERVER_DEMO_CLIENT && m_Owner != -1 && !TeamMask.test(position: SnappingClient))
396 return;
397
398 if(SnappingClientVersion >= VERSION_DDNET_ENTITY_NETOBJS)
399 {
400 Server()->SnapNewItem(Id: GetId().value(), Data: NetInfo());
401 }
402 else if(SnappingClientVersion >= VERSION_DDNET_ANTIPING_PROJECTILE && NetIsInfoLegacyCompatible())
403 {
404 if(SnappingClientVersion >= VERSION_DDNET_MSG_LEGACY)
405 {
406 Server()->SnapNewItem(Id: GetId().value(), Data: NetInfoLegacy());
407 }
408 else
409 {
410 CNetObj_DDRaceProjectile DDRaceProjectile = NetInfoLegacy();
411 CNetObj_Projectile Projectile = {};
412 static_assert(sizeof(DDRaceProjectile) == sizeof(Projectile));
413 mem_copy(dest: &Projectile, source: &DDRaceProjectile, size: sizeof(Projectile));
414 Server()->SnapNewItem(Id: GetId().value(), Data: Projectile);
415 }
416 }
417 else
418 {
419 Server()->SnapNewItem(Id: GetId().value(), Data: NetInfoVanilla());
420 }
421}
422
423void CProjectile::SwapClients(int Client1, int Client2)
424{
425 m_Owner = m_Owner == Client1 ? Client2 : (m_Owner == Client2 ? Client1 : m_Owner);
426}
427
428// DDRace
429
430bool CProjectile::CanCollide(int ClientId)
431{
432 if(m_DDRaceTeam != GameServer()->GetDDRaceTeam(ClientId))
433 return false;
434 if(m_IsSolo)
435 return m_Owner == ClientId;
436 return true;
437}
438
439void CProjectile::SetBouncing(int Value)
440{
441 m_Bouncing = Value;
442}
443