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/DDRace.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)
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
271void CProjectile::FillInfo(CNetObj_Projectile *pProj)
272{
273 pProj->m_X = (int)m_Pos.x;
274 pProj->m_Y = (int)m_Pos.y;
275 pProj->m_VelX = (int)(m_Direction.x * 100.0f);
276 pProj->m_VelY = (int)(m_Direction.y * 100.0f);
277 pProj->m_StartTick = m_StartTick;
278 pProj->m_Type = m_Type;
279}
280
281void CProjectile::Snap(int SnappingClient)
282{
283 float Ct = (Server()->Tick() - m_StartTick) / (float)Server()->TickSpeed();
284
285 if(NetworkClipped(SnappingClient, CheckPos: GetPos(Time: Ct)))
286 return;
287
288 int SnappingClientVersion = GameServer()->GetClientVersion(ClientId: SnappingClient);
289 if(SnappingClientVersion < VERSION_DDNET_ENTITY_NETOBJS)
290 {
291 CCharacter *pSnapChar = GameServer()->GetPlayerChar(ClientId: SnappingClient);
292 int Tick = (Server()->Tick() % Server()->TickSpeed()) % ((m_Explosive) ? 6 : 20);
293 if(pSnapChar && pSnapChar->IsAlive() && (m_Layer == LAYER_SWITCH && m_Number > 0 && !Switchers()[m_Number].m_aStatus[pSnapChar->Team()] && (!Tick)))
294 return;
295 }
296
297 CCharacter *pOwnerChar = nullptr;
298 CClientMask TeamMask = CClientMask().set();
299
300 if(m_Owner >= 0)
301 pOwnerChar = GameServer()->GetPlayerChar(ClientId: m_Owner);
302
303 if(pOwnerChar && pOwnerChar->IsAlive())
304 TeamMask = pOwnerChar->TeamMask();
305
306 if(SnappingClient != SERVER_DEMO_CLIENT && m_Owner != -1 && !TeamMask.test(position: SnappingClient))
307 return;
308
309 CNetObj_DDRaceProjectile DDRaceProjectile;
310
311 if(SnappingClientVersion >= VERSION_DDNET_ENTITY_NETOBJS)
312 {
313 CNetObj_DDNetProjectile *pDDNetProjectile = static_cast<CNetObj_DDNetProjectile *>(Server()->SnapNewItem(Type: NETOBJTYPE_DDNETPROJECTILE, Id: GetId(), Size: sizeof(CNetObj_DDNetProjectile)));
314 if(!pDDNetProjectile)
315 {
316 return;
317 }
318 FillExtraInfo(pProj: pDDNetProjectile);
319 }
320 else if(SnappingClientVersion >= VERSION_DDNET_ANTIPING_PROJECTILE && FillExtraInfoLegacy(pProj: &DDRaceProjectile))
321 {
322 int Type = SnappingClientVersion < VERSION_DDNET_MSG_LEGACY ? (int)NETOBJTYPE_PROJECTILE : NETOBJTYPE_DDRACEPROJECTILE;
323 void *pProj = Server()->SnapNewItem(Type, Id: GetId(), Size: sizeof(DDRaceProjectile));
324 if(!pProj)
325 {
326 return;
327 }
328 mem_copy(dest: pProj, source: &DDRaceProjectile, size: sizeof(DDRaceProjectile));
329 }
330 else
331 {
332 CNetObj_Projectile *pProj = Server()->SnapNewItem<CNetObj_Projectile>(Id: GetId());
333 if(!pProj)
334 {
335 return;
336 }
337 FillInfo(pProj);
338 }
339}
340
341void CProjectile::SwapClients(int Client1, int Client2)
342{
343 m_Owner = m_Owner == Client1 ? Client2 : (m_Owner == Client2 ? Client1 : m_Owner);
344}
345
346// DDRace
347
348bool CProjectile::CanCollide(int ClientId)
349{
350 if(m_DDRaceTeam != GameServer()->GetDDRaceTeam(ClientId))
351 return false;
352 if(m_IsSolo)
353 return m_Owner == ClientId;
354 return true;
355}
356
357void CProjectile::SetBouncing(int Value)
358{
359 m_Bouncing = Value;
360}
361
362bool CProjectile::FillExtraInfoLegacy(CNetObj_DDRaceProjectile *pProj)
363{
364 const int MaxPos = 0x7fffffff / 100;
365 if(absolute(a: (int)m_Pos.y) + 1 >= MaxPos || absolute(a: (int)m_Pos.x) + 1 >= MaxPos)
366 {
367 //If the modified data would be too large to fit in an integer, send normal data instead
368 return false;
369 }
370 //Send additional/modified info, by modifying the fields of the netobj
371 float Angle = -std::atan2(y: m_Direction.x, x: m_Direction.y);
372
373 int Data = 0;
374 Data |= (absolute(a: m_Owner) & 255) << 0;
375 if(m_Owner < 0)
376 Data |= LEGACYPROJECTILEFLAG_NO_OWNER;
377 //This bit tells the client to use the extra info
378 Data |= LEGACYPROJECTILEFLAG_IS_DDNET;
379 // LEGACYPROJECTILEFLAG_BOUNCE_HORIZONTAL, LEGACYPROJECTILEFLAG_BOUNCE_VERTICAL
380 Data |= (m_Bouncing & 3) << 10;
381 if(m_Explosive)
382 Data |= LEGACYPROJECTILEFLAG_EXPLOSIVE;
383 if(m_Freeze)
384 Data |= LEGACYPROJECTILEFLAG_FREEZE;
385
386 pProj->m_X = (int)(m_Pos.x * 100.0f);
387 pProj->m_Y = (int)(m_Pos.y * 100.0f);
388 pProj->m_Angle = (int)(Angle * 1000000.0f);
389 pProj->m_Data = Data;
390 pProj->m_StartTick = m_StartTick;
391 pProj->m_Type = m_Type;
392 return true;
393}
394
395void CProjectile::FillExtraInfo(CNetObj_DDNetProjectile *pProj)
396{
397 int Flags = 0;
398 if(m_Bouncing & 1)
399 {
400 Flags |= PROJECTILEFLAG_BOUNCE_HORIZONTAL;
401 }
402 if(m_Bouncing & 2)
403 {
404 Flags |= PROJECTILEFLAG_BOUNCE_VERTICAL;
405 }
406 if(m_Explosive)
407 {
408 Flags |= PROJECTILEFLAG_EXPLOSIVE;
409 }
410 if(m_Freeze)
411 {
412 Flags |= PROJECTILEFLAG_FREEZE;
413 }
414
415 if(m_Owner < 0)
416 {
417 pProj->m_VelX = round_to_int(f: m_Direction.x * 1e6f);
418 pProj->m_VelY = round_to_int(f: m_Direction.y * 1e6f);
419 }
420 else
421 {
422 pProj->m_VelX = round_to_int(f: m_InitDir.x);
423 pProj->m_VelY = round_to_int(f: m_InitDir.y);
424 Flags |= PROJECTILEFLAG_NORMALIZE_VEL;
425 }
426
427 pProj->m_X = round_to_int(f: m_Pos.x * 100.0f);
428 pProj->m_Y = round_to_int(f: m_Pos.y * 100.0f);
429 pProj->m_Type = m_Type;
430 pProj->m_StartTick = m_StartTick;
431 pProj->m_Owner = m_Owner;
432 pProj->m_Flags = Flags;
433 pProj->m_SwitchNumber = m_Number;
434 pProj->m_TuneZone = m_TuneZone;
435}
436