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