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