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 m_TeamMask = CClientMask().set();
47
48 CCharacter *pOwnerChar = GameServer()->GetPlayerChar(ClientId: m_Owner);
49 m_BelongsToPracticeTeam = pOwnerChar && pOwnerChar->Teams()->IsPractice(Team: pOwnerChar->Team());
50 m_DDRaceTeam = m_Owner == -1 ? 0 : GameServer()->GetDDRaceTeam(ClientId: m_Owner);
51 m_IsSolo = pOwnerChar && pOwnerChar->GetCore().m_Solo;
52
53 GameWorld()->InsertEntity(pEntity: this);
54}
55
56void CProjectile::Reset()
57{
58 m_MarkedForDestroy = true;
59}
60
61vec2 CProjectile::GetPos(float Time)
62{
63 float Curvature = 0;
64 float Speed = 0;
65 CTuningParams *pTuning = GetTuning(i: m_TuneZone);
66
67 switch(m_Type)
68 {
69 case WEAPON_GRENADE:
70 Curvature = pTuning->m_GrenadeCurvature;
71 Speed = pTuning->m_GrenadeSpeed;
72 break;
73
74 case WEAPON_SHOTGUN:
75 Curvature = pTuning->m_ShotgunCurvature;
76 Speed = pTuning->m_ShotgunSpeed;
77 break;
78
79 case WEAPON_GUN:
80 Curvature = pTuning->m_GunCurvature;
81 Speed = pTuning->m_GunSpeed;
82 break;
83 }
84
85 return CalcPos(Pos: m_Pos, Velocity: m_Direction, Curvature, Speed, Time);
86}
87
88void CProjectile::Tick()
89{
90 float Pt = (Server()->Tick() - m_StartTick - 1) / (float)Server()->TickSpeed();
91 float Ct = (Server()->Tick() - m_StartTick) / (float)Server()->TickSpeed();
92 vec2 PrevPos = GetPos(Time: Pt);
93 vec2 CurPos = GetPos(Time: Ct);
94 vec2 ColPos;
95 vec2 NewPos;
96 int Collide = GameServer()->Collision()->IntersectLine(Pos0: PrevPos, Pos1: CurPos, pOutCollision: &ColPos, pOutBeforeCollision: &NewPos);
97 CCharacter *pOwnerChar = nullptr;
98
99 if(m_Owner >= 0)
100 pOwnerChar = GameServer()->GetPlayerChar(ClientId: m_Owner);
101
102 CCharacter *pTargetChr = nullptr;
103
104 if(pOwnerChar ? !pOwnerChar->GrenadeHitDisabled() : g_Config.m_SvHit)
105 pTargetChr = GameServer()->m_World.IntersectCharacter(Pos0: PrevPos, Pos1: ColPos, Radius: m_Freeze ? 1.0f : 6.0f, NewPos&: ColPos, pNotThis: pOwnerChar, CollideWith: m_Owner);
106
107 if(m_LifeSpan > -1)
108 m_LifeSpan--;
109
110 m_TeamMask = CClientMask().set();
111 bool IsWeaponCollide = false;
112 if(
113 pOwnerChar &&
114 pTargetChr &&
115 pOwnerChar->IsAlive() &&
116 pTargetChr->IsAlive() &&
117 !pTargetChr->CanCollide(ClientId: m_Owner))
118 {
119 IsWeaponCollide = true;
120 }
121 if(pOwnerChar && pOwnerChar->IsAlive())
122 {
123 m_TeamMask = pOwnerChar->TeamMask();
124 }
125 else if(m_Owner >= 0 && (m_Type != WEAPON_GRENADE || g_Config.m_SvDestroyBulletsOnDeath || m_BelongsToPracticeTeam))
126 {
127 m_MarkedForDestroy = true;
128 return;
129 }
130
131 if(((pTargetChr && (pOwnerChar ? !pOwnerChar->GrenadeHitDisabled() : g_Config.m_SvHit || m_Owner == -1 || pTargetChr == pOwnerChar)) || Collide || GameLayerClipped(CheckPos: CurPos)) && !IsWeaponCollide)
132 {
133 if(m_Explosive /*??*/ && (!pTargetChr || (pTargetChr && (!m_Freeze || (m_Type == WEAPON_SHOTGUN && Collide)))))
134 {
135 int Number = 1;
136 if(GameServer()->EmulateBug(Bug: BUG_GRENADE_DOUBLEEXPLOSION) && m_LifeSpan == -1)
137 {
138 Number = 2;
139 }
140 for(int i = 0; i < Number; i++)
141 {
142 GameServer()->CreateExplosion(Pos: ColPos, Owner: m_Owner, Weapon: m_Type, NoDamage: m_Owner == -1, ActivatedTeam: (!pTargetChr ? -1 : pTargetChr->Team()), Mask: m_TeamMask);
143 GameServer()->CreateSound(Pos: ColPos, Sound: m_SoundImpact, Mask: m_TeamMask);
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_TeamMask);
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 GameServer()->CreateExplosion(Pos: ColPos, Owner: m_Owner, Weapon: m_Type, NoDamage: m_Owner == -1, ActivatedTeam: (!pOwnerChar ? -1 : pOwnerChar->Team()), Mask: m_TeamMask);
235 GameServer()->CreateSound(Pos: ColPos, Sound: m_SoundImpact, Mask: m_TeamMask);
236 }
237 m_MarkedForDestroy = true;
238 return;
239 }
240
241 int x = GameServer()->Collision()->GetIndex(PrevPos, Pos: CurPos);
242 int z;
243 if(g_Config.m_SvOldTeleportWeapons)
244 z = GameServer()->Collision()->IsTeleport(Index: x);
245 else
246 z = GameServer()->Collision()->IsTeleportWeapon(Index: x);
247 if(z && !GameServer()->Collision()->TeleOuts(Number: z - 1).empty())
248 {
249 int TeleOut = GameServer()->m_World.m_Core.RandomOr0(BelowThis: GameServer()->Collision()->TeleOuts(Number: z - 1).size());
250 m_Pos = GameServer()->Collision()->TeleOuts(Number: z - 1)[TeleOut];
251 m_StartTick = Server()->Tick();
252 }
253}
254
255void CProjectile::TickPaused()
256{
257 ++m_StartTick;
258}
259
260CNetObj_Projectile CProjectile::NetInfoVanilla() const
261{
262 CNetObj_Projectile Result = {};
263 Result.m_X = (int)m_Pos.x;
264 Result.m_Y = (int)m_Pos.y;
265 Result.m_VelX = (int)(m_Direction.x * 100.0f);
266 Result.m_VelY = (int)(m_Direction.y * 100.0f);
267 Result.m_StartTick = m_StartTick;
268 Result.m_Type = m_Type;
269 return Result;
270}
271
272bool CProjectile::NetIsInfoLegacyCompatible() const
273{
274 const int MaxPos = 0x7fffffff / 100;
275 if(absolute(a: (int)m_Pos.y) + 1 >= MaxPos || absolute(a: (int)m_Pos.x) + 1 >= MaxPos)
276 {
277 //If the modified data would be too large to fit in an integer, send normal data instead
278 return false;
279 }
280 return true;
281}
282
283CNetObj_DDRaceProjectile CProjectile::NetInfoLegacy(int SnappingClient)
284{
285 dbg_assert(NetIsInfoLegacyCompatible(), "can't send incompatible projectile");
286
287 //Send additional/modified info, by modifying the fields of the netobj
288 float Angle = -std::atan2(y: m_Direction.x, x: m_Direction.y);
289
290 int Owner = m_Owner;
291 if(!Server()->Translate(Target&: Owner, ClientId: SnappingClient))
292 Owner = -1;
293
294 int Data = 0;
295 Data |= (absolute(a: Owner) & 255) << 0;
296 if(Owner < 0)
297 Data |= LEGACYPROJECTILEFLAG_NO_OWNER;
298 //This bit tells the client to use the extra info
299 Data |= LEGACYPROJECTILEFLAG_IS_DDNET;
300 // LEGACYPROJECTILEFLAG_BOUNCE_HORIZONTAL, LEGACYPROJECTILEFLAG_BOUNCE_VERTICAL
301 Data |= (m_Bouncing & 3) << 10;
302 if(m_Explosive)
303 Data |= LEGACYPROJECTILEFLAG_EXPLOSIVE;
304 if(m_Freeze)
305 Data |= LEGACYPROJECTILEFLAG_FREEZE;
306
307 CNetObj_DDRaceProjectile Result = {};
308 Result.m_X = (int)(m_Pos.x * 100.0f);
309 Result.m_Y = (int)(m_Pos.y * 100.0f);
310 Result.m_Angle = (int)(Angle * 1000000.0f);
311 Result.m_Data = Data;
312 Result.m_StartTick = m_StartTick;
313 Result.m_Type = m_Type;
314 return Result;
315}
316
317CNetObj_DDNetProjectile CProjectile::NetInfo(int SnappingClient)
318{
319 CNetObj_DDNetProjectile Result = {};
320
321 int Flags = 0;
322 if(m_Bouncing & 1)
323 {
324 Flags |= PROJECTILEFLAG_BOUNCE_HORIZONTAL;
325 }
326 if(m_Bouncing & 2)
327 {
328 Flags |= PROJECTILEFLAG_BOUNCE_VERTICAL;
329 }
330 if(m_Explosive)
331 {
332 Flags |= PROJECTILEFLAG_EXPLOSIVE;
333 }
334 if(m_Freeze)
335 {
336 Flags |= PROJECTILEFLAG_FREEZE;
337 }
338
339 int Owner = m_Owner;
340 if(!Server()->Translate(Target&: Owner, ClientId: SnappingClient))
341 Owner = -1;
342
343 if(Owner < 0)
344 {
345 Result.m_VelX = round_to_int(f: m_Direction.x * 1e6f);
346 Result.m_VelY = round_to_int(f: m_Direction.y * 1e6f);
347 }
348 else
349 {
350 Result.m_VelX = round_to_int(f: m_InitDir.x);
351 Result.m_VelY = round_to_int(f: m_InitDir.y);
352 Flags |= PROJECTILEFLAG_NORMALIZE_VEL;
353 }
354
355 Result.m_X = round_to_int(f: m_Pos.x * 100.0f);
356 Result.m_Y = round_to_int(f: m_Pos.y * 100.0f);
357 Result.m_Type = m_Type;
358 Result.m_StartTick = m_StartTick;
359 Result.m_Owner = Owner;
360 Result.m_SwitchNumber = m_Number;
361 Result.m_TuneZone = m_TuneZone;
362 Result.m_Flags = Flags;
363 return Result;
364}
365
366void CProjectile::Snap(int SnappingClient)
367{
368 float Ct = (Server()->Tick() - m_StartTick) / (float)Server()->TickSpeed();
369
370 if(NetworkClipped(SnappingClient, CheckPos: GetPos(Time: Ct)) || !GetId().has_value())
371 return;
372
373 int SnappingClientVersion = GameServer()->GetClientVersion(ClientId: SnappingClient);
374 if(SnappingClientVersion < VERSION_DDNET_ENTITY_NETOBJS)
375 {
376 CCharacter *pSnapChar = GameServer()->GetPlayerChar(ClientId: SnappingClient);
377 int Tick = (Server()->Tick() % Server()->TickSpeed()) % ((m_Explosive) ? 6 : 20);
378 if(pSnapChar && pSnapChar->IsAlive() && (m_Layer == LAYER_SWITCH && m_Number > 0 && !Switchers()[m_Number].m_aStatus[pSnapChar->Team()] && (!Tick)))
379 return;
380 }
381
382 if(SnappingClient != SERVER_DEMO_CLIENT && !m_TeamMask.test(position: SnappingClient))
383 return;
384
385 if(SnappingClientVersion >= VERSION_DDNET_ENTITY_NETOBJS)
386 {
387 Server()->SnapNewItem(Id: GetId().value(), Data: NetInfo(SnappingClient));
388 }
389 else if(SnappingClientVersion >= VERSION_DDNET_ANTIPING_PROJECTILE && NetIsInfoLegacyCompatible())
390 {
391 if(SnappingClientVersion >= VERSION_DDNET_MSG_LEGACY)
392 {
393 Server()->SnapNewItem(Id: GetId().value(), Data: NetInfoLegacy(SnappingClient));
394 }
395 else
396 {
397 CNetObj_DDRaceProjectile DDRaceProjectile = NetInfoLegacy(SnappingClient);
398 CNetObj_Projectile Projectile = {};
399 static_assert(sizeof(DDRaceProjectile) == sizeof(Projectile));
400 mem_copy(dest: &Projectile, source: &DDRaceProjectile, size: sizeof(Projectile));
401 Server()->SnapNewItem(Id: GetId().value(), Data: Projectile);
402 }
403 }
404 else
405 {
406 Server()->SnapNewItem(Id: GetId().value(), Data: NetInfoVanilla());
407 }
408}
409
410void CProjectile::SwapClients(int Client1, int Client2)
411{
412 m_Owner = m_Owner == Client1 ? Client2 : (m_Owner == Client2 ? Client1 : m_Owner);
413}
414
415// DDRace
416
417bool CProjectile::CanCollide(int ClientId)
418{
419 if(m_DDRaceTeam != GameServer()->GetDDRaceTeam(ClientId))
420 return false;
421 if(m_IsSolo)
422 return m_Owner == ClientId;
423 return true;
424}
425
426void CProjectile::SetBouncing(int Value)
427{
428 m_Bouncing = Value;
429}
430