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 | |
14 | CProjectile::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 | |
52 | void CProjectile::Reset() |
53 | { |
54 | m_MarkedForDestroy = true; |
55 | } |
56 | |
57 | vec2 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 | |
109 | void 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 | |
287 | void CProjectile::TickPaused() |
288 | { |
289 | ++m_StartTick; |
290 | } |
291 | |
292 | void 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 | |
302 | void 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 | |
362 | void 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 | |
369 | void CProjectile::SetBouncing(int Value) |
370 | { |
371 | m_Bouncing = Value; |
372 | } |
373 | |
374 | bool CProjectile::(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 | |
407 | void CProjectile::(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 | |