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