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/client/projectile_data.h>
12#include <game/collision.h>
13#include <game/mapitems.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 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_SoundImpact = SoundImpact;
35 m_StartTick = GameWorld()->GameTick();
36 m_Explosive = Explosive;
37
38 m_Layer = Layer;
39 m_Number = Number;
40 m_Freeze = Freeze;
41
42 m_TuneZone = GameWorld()->m_WorldConfig.m_UseTuneZones ? Collision()->IsTune(Index: Collision()->GetMapIndex(Pos: m_Pos)) : 0;
43
44 GameWorld()->InsertEntity(pEntity: this);
45}
46
47vec2 CProjectile::GetPos(float Time)
48{
49 float Curvature = 0;
50 float Speed = 0;
51 CTuningParams *pTuning = GetTuning(i: m_TuneZone);
52
53 switch(m_Type)
54 {
55 case WEAPON_GRENADE:
56 Curvature = pTuning->m_GrenadeCurvature;
57 Speed = pTuning->m_GrenadeSpeed;
58 break;
59
60 case WEAPON_SHOTGUN:
61 Curvature = pTuning->m_ShotgunCurvature;
62 Speed = pTuning->m_ShotgunSpeed;
63 break;
64
65 case WEAPON_GUN:
66 Curvature = pTuning->m_GunCurvature;
67 Speed = pTuning->m_GunSpeed;
68 break;
69 }
70
71 return CalcPos(Pos: m_Pos, Velocity: m_Direction, Curvature, Speed, Time);
72}
73
74void CProjectile::Tick()
75{
76 float Pt = (GameWorld()->GameTick() - m_StartTick - 1) / (float)GameWorld()->GameTickSpeed();
77 float Ct = (GameWorld()->GameTick() - m_StartTick) / (float)GameWorld()->GameTickSpeed();
78 vec2 PrevPos = GetPos(Time: Pt);
79 vec2 CurPos = GetPos(Time: Ct);
80 vec2 ColPos;
81 vec2 NewPos;
82 int Collide = Collision()->IntersectLine(Pos0: PrevPos, Pos1: CurPos, pOutCollision: &ColPos, pOutBeforeCollision: &NewPos);
83 CCharacter *pOwnerChar = GameWorld()->GetCharacterById(Id: m_Owner);
84
85 CCharacter *pTargetChr = GameWorld()->IntersectCharacter(Pos0: PrevPos, Pos1: ColPos, Radius: m_Freeze ? 1.0f : 6.0f, NewPos&: ColPos, pNotThis: pOwnerChar, CollideWith: m_Owner);
86
87 if(GameWorld()->m_WorldConfig.m_IsSolo && !(m_Type == WEAPON_SHOTGUN && GameWorld()->m_WorldConfig.m_IsDDRace))
88 pTargetChr = nullptr;
89
90 if(m_LifeSpan > -1)
91 m_LifeSpan--;
92
93 bool IsWeaponCollide = false;
94 if(
95 pOwnerChar &&
96 pTargetChr &&
97 !pTargetChr->CanCollide(ClientId: m_Owner))
98 {
99 IsWeaponCollide = true;
100 }
101
102 if(((pTargetChr && (pOwnerChar ? !pOwnerChar->GrenadeHitDisabled() : g_Config.m_SvHit || m_Owner == -1 || pTargetChr == pOwnerChar)) || Collide || GameLayerClipped(CheckPos: CurPos)) && !IsWeaponCollide)
103 {
104 if(m_Explosive && (!pTargetChr || (!m_Freeze || (m_Type == WEAPON_SHOTGUN && Collide))))
105 {
106 GameWorld()->CreateExplosion(Pos: ColPos, Owner: m_Owner, Weapon: m_Type, NoDamage: m_Owner == -1, ActivatedTeam: (!pTargetChr ? -1 : pTargetChr->Team()), Mask: CClientMask().set(), Id: m_StartTick);
107 if(GameWorld()->m_WorldConfig.m_IsDDRace && GameWorld()->m_WorldConfig.m_PredictDDRace)
108 {
109 // vanilla has different projectile physics
110 GameWorld()->CreatePredictedSound(Pos: ColPos, SoundId: m_SoundImpact, Id: m_StartTick);
111 }
112 }
113 else if(m_Freeze)
114 {
115 CEntity *apEnts[MAX_CLIENTS];
116 int Num = GameWorld()->FindEntities(Pos: CurPos, Radius: 1.0f, ppEnts: apEnts, Max: MAX_CLIENTS, Type: CGameWorld::ENTTYPE_CHARACTER);
117 for(int i = 0; i < Num; ++i)
118 {
119 auto *pChr = static_cast<CCharacter *>(apEnts[i]);
120 if(pChr && (m_Layer != LAYER_SWITCH || (m_Layer == LAYER_SWITCH && m_Number > 0 && m_Number < (int)Switchers().size() && Switchers()[m_Number].m_aStatus[pChr->Team()])))
121 pChr->Freeze();
122 }
123 }
124 if(Collide && m_Bouncing != 0)
125 {
126 m_StartTick = GameWorld()->GameTick();
127 m_Pos = NewPos + (-(m_Direction * 4));
128 if(m_Bouncing == 1)
129 m_Direction.x = -m_Direction.x;
130 else if(m_Bouncing == 2)
131 m_Direction.y = -m_Direction.y;
132 if(absolute(a: m_Direction.x) < 1e-6f)
133 m_Direction.x = 0;
134 if(absolute(a: m_Direction.y) < 1e-6f)
135 m_Direction.y = 0;
136 m_Pos += m_Direction;
137 }
138 else if(m_Type == WEAPON_GUN)
139 {
140 if(GameWorld()->m_WorldConfig.m_IsDDRace && GameWorld()->m_WorldConfig.m_PredictDDRace)
141 GameWorld()->CreatePredictedDamageIndEvent(Pos: CurPos, Angle: -std::atan2(y: m_Direction.x, x: m_Direction.y), Amount: 10, Id: m_StartTick);
142 m_MarkedForDestroy = true;
143 }
144 else if(!m_Freeze)
145 m_MarkedForDestroy = true;
146 }
147 if(m_LifeSpan == -1)
148 {
149 if(m_Explosive)
150 {
151 if(m_Owner >= 0)
152 pOwnerChar = GameWorld()->GetCharacterById(Id: m_Owner);
153
154 GameWorld()->CreateExplosion(Pos: ColPos, Owner: m_Owner, Weapon: m_Type, NoDamage: m_Owner == -1, ActivatedTeam: (!pOwnerChar ? -1 : pOwnerChar->Team()), Mask: CClientMask().set(), Id: m_StartTick);
155 GameWorld()->CreatePredictedSound(Pos: ColPos, SoundId: m_SoundImpact, Id: m_StartTick);
156 }
157 m_MarkedForDestroy = true;
158 }
159}
160
161// DDRace
162
163void CProjectile::SetBouncing(int Value)
164{
165 m_Bouncing = Value;
166}
167
168CProjectile::CProjectile(CGameWorld *pGameWorld, int Id, const CProjectileData *pProj) :
169 CEntity(pGameWorld, CGameWorld::ENTTYPE_PROJECTILE)
170{
171 m_Pos = pProj->m_StartPos;
172 m_Direction = pProj->m_StartVel;
173 if(pProj->m_ExtraInfo)
174 {
175 m_Owner = pProj->m_Owner;
176 m_Explosive = pProj->m_Explosive;
177 m_Bouncing = pProj->m_Bouncing;
178 m_Freeze = pProj->m_Freeze;
179 }
180 else
181 {
182 m_Owner = -1;
183 m_Bouncing = 0;
184 m_Freeze = false;
185 m_Explosive = (pProj->m_Type == WEAPON_GRENADE) && (absolute(a: 1.0f - length(a: m_Direction)) < 0.015f);
186 }
187 m_Type = pProj->m_Type;
188 m_StartTick = pProj->m_StartTick;
189 m_TuneZone = pProj->m_TuneZone;
190
191 int Lifetime = 20 * GameWorld()->GameTickSpeed();
192 m_SoundImpact = -1;
193 if(m_Type == WEAPON_GRENADE)
194 {
195 Lifetime = GetTuning(i: m_TuneZone)->m_GrenadeLifetime * GameWorld()->GameTickSpeed();
196 m_SoundImpact = SOUND_GRENADE_EXPLODE;
197 }
198 else if(m_Type == WEAPON_GUN)
199 Lifetime = GetTuning(i: m_TuneZone)->m_GunLifetime * GameWorld()->GameTickSpeed();
200 else if(m_Type == WEAPON_SHOTGUN && !GameWorld()->m_WorldConfig.m_IsDDRace)
201 Lifetime = GetTuning(i: m_TuneZone)->m_ShotgunLifetime * GameWorld()->GameTickSpeed();
202 m_LifeSpan = Lifetime - (pGameWorld->GameTick() - m_StartTick);
203 m_Id = Id;
204 m_Number = pProj->m_SwitchNumber;
205 m_Layer = m_Number > 0 ? LAYER_SWITCH : LAYER_GAME;
206}
207
208CProjectileData CProjectile::GetData() const
209{
210 CProjectileData Result;
211 Result.m_StartPos = m_Pos;
212 Result.m_StartVel = m_Direction;
213 Result.m_Type = m_Type;
214 Result.m_StartTick = m_StartTick;
215 Result.m_ExtraInfo = true;
216 Result.m_Owner = m_Owner;
217 Result.m_Explosive = m_Explosive;
218 Result.m_Bouncing = m_Bouncing;
219 Result.m_Freeze = m_Freeze;
220 Result.m_TuneZone = m_TuneZone;
221 Result.m_SwitchNumber = m_Number;
222 return Result;
223}
224
225bool CProjectile::Match(CProjectile *pProj)
226{
227 if(pProj->m_Type != m_Type)
228 return false;
229 if(pProj->m_StartTick != m_StartTick)
230 return false;
231 if(distance(a: pProj->m_Pos, b: m_Pos) > 2.f)
232 return false;
233 if(distance(a: pProj->m_Direction, b: m_Direction) > 2.f)
234 return false;
235 return true;
236}
237