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 "laser.h"
4
5#include "character.h"
6
7#include <engine/shared/config.h>
8
9#include <generated/protocol.h>
10
11#include <game/client/laser_data.h>
12#include <game/collision.h>
13#include <game/mapitems.h>
14
15CLaser::CLaser(CGameWorld *pGameWorld, vec2 Pos, vec2 Direction, float StartEnergy, int Owner, int Type) :
16 CEntity(pGameWorld, CGameWorld::ENTTYPE_LASER)
17{
18 m_Pos = Pos;
19 m_Owner = Owner;
20 m_Energy = StartEnergy;
21 if(pGameWorld->m_WorldConfig.m_IsFNG && m_Energy < 10.f)
22 m_Energy = 800.0f;
23 m_Dir = Direction;
24 m_Bounces = 0;
25 m_EvalTick = 0;
26 m_Type = Type;
27 m_ZeroEnergyBounceInLastTick = false;
28 m_TuneZone = GameWorld()->m_WorldConfig.m_UseTuneZones ? Collision()->IsTune(Index: Collision()->GetMapIndex(Pos: m_Pos)) : 0;
29 GameWorld()->InsertEntity(pEntity: this);
30 DoBounce();
31}
32
33bool CLaser::HitCharacter(vec2 From, vec2 To)
34{
35 static const vec2 StackedLaserShotgunBugSpeed = vec2(-2147483648.0f, -2147483648.0f);
36 vec2 At;
37 CCharacter *pOwnerChar = GameWorld()->GetCharacterById(Id: m_Owner);
38 CCharacter *pHit;
39 bool DontHitSelf = (g_Config.m_SvOldLaser || !GameWorld()->m_WorldConfig.m_IsDDRace) || (m_Bounces == 0);
40
41 if(pOwnerChar ? (!pOwnerChar->LaserHitDisabled() && m_Type == WEAPON_LASER) || (!pOwnerChar->ShotgunHitDisabled() && m_Type == WEAPON_SHOTGUN) : g_Config.m_SvHit)
42 pHit = GameWorld()->IntersectCharacter(Pos0: m_Pos, Pos1: To, Radius: 0.f, NewPos&: At, pNotThis: DontHitSelf ? pOwnerChar : nullptr, CollideWith: m_Owner);
43 else
44 pHit = GameWorld()->IntersectCharacter(Pos0: m_Pos, Pos1: To, Radius: 0.f, NewPos&: At, pNotThis: DontHitSelf ? pOwnerChar : nullptr, CollideWith: m_Owner, pThisOnly: pOwnerChar);
45
46 if(!pHit || (pHit == pOwnerChar && g_Config.m_SvOldLaser) || (pHit != pOwnerChar && pOwnerChar ? (pOwnerChar->LaserHitDisabled() && m_Type == WEAPON_LASER) || (pOwnerChar->ShotgunHitDisabled() && m_Type == WEAPON_SHOTGUN) : !g_Config.m_SvHit))
47 return false;
48 m_From = From;
49 m_Pos = At;
50 m_Energy = -1;
51 if(m_Type == WEAPON_SHOTGUN)
52 {
53 float Strength = TuningList()[m_TuneZone].m_ShotgunStrength;
54
55 const vec2 &HitPos = pHit->Core()->m_Pos;
56 if(!g_Config.m_SvOldLaser)
57 {
58 if(m_PrevPos != HitPos)
59 {
60 pHit->AddVelocity(Addition: normalize(v: m_PrevPos - HitPos) * Strength);
61 }
62 else
63 {
64 pHit->SetRawVelocity(StackedLaserShotgunBugSpeed);
65 }
66 }
67 else if(g_Config.m_SvOldLaser && pOwnerChar)
68 {
69 if(pOwnerChar->Core()->m_Pos != HitPos)
70 {
71 pHit->AddVelocity(Addition: normalize(v: pOwnerChar->Core()->m_Pos - HitPos) * Strength);
72 }
73 else
74 {
75 pHit->SetRawVelocity(StackedLaserShotgunBugSpeed);
76 }
77 }
78 else
79 {
80 // Re-apply move restrictions as a part of 'shotgun bug' reproduction
81 pHit->ApplyMoveRestrictions();
82 }
83
84 if(pOwnerChar)
85 {
86 pOwnerChar->AntiPingInterference(ClientId: pHit->GetCid());
87 }
88 }
89 else if(m_Type == WEAPON_LASER)
90 {
91 pHit->Unfreeze();
92 }
93 return true;
94}
95
96void CLaser::DoBounce()
97{
98 m_EvalTick = GameWorld()->GameTick();
99
100 if(m_Energy < 0)
101 {
102 m_MarkedForDestroy = true;
103 return;
104 }
105 m_PrevPos = m_Pos;
106 vec2 Coltile;
107
108 int Res;
109 vec2 To = m_Pos + m_Dir * m_Energy;
110
111 Res = Collision()->IntersectLineTeleWeapon(Pos0: m_Pos, Pos1: To, pOutCollision: &Coltile, pOutBeforeCollision: &To);
112
113 if(Res)
114 {
115 if(!HitCharacter(From: m_Pos, To))
116 {
117 // intersected
118 m_From = m_Pos;
119 m_Pos = To;
120
121 vec2 TempPos = m_Pos;
122 vec2 TempDir = m_Dir * 4.0f;
123
124 int f = 0;
125 if(Res == -1)
126 {
127 f = Collision()->GetTile(x: round_to_int(f: Coltile.x), y: round_to_int(f: Coltile.y));
128 Collision()->SetCollisionAt(x: round_to_int(f: Coltile.x), y: round_to_int(f: Coltile.y), Index: TILE_SOLID);
129 }
130 Collision()->MovePoint(pInoutPos: &TempPos, pInoutVel: &TempDir, Elasticity: 1.0f, pBounces: nullptr);
131 if(Res == -1)
132 {
133 Collision()->SetCollisionAt(x: round_to_int(f: Coltile.x), y: round_to_int(f: Coltile.y), Index: f);
134 }
135 m_Pos = TempPos;
136 m_Dir = normalize(v: TempDir);
137
138 const float Distance = distance(a: m_From, b: m_Pos);
139 // Prevent infinite bounces
140 if(Distance == 0.0f && m_ZeroEnergyBounceInLastTick)
141 {
142 m_Energy = -1;
143 }
144 else
145 {
146 m_Energy -= Distance + GetTuning(i: m_TuneZone)->m_LaserBounceCost;
147 }
148 m_ZeroEnergyBounceInLastTick = Distance == 0.0f;
149
150 m_Bounces++;
151
152 int BounceNum = GetTuning(i: m_TuneZone)->m_LaserBounceNum;
153
154 if(m_Bounces > BounceNum)
155 m_Energy = -1;
156
157 GameWorld()->CreatePredictedSound(Pos: m_Pos, SoundId: SOUND_LASER_BOUNCE, Id: m_Id);
158 }
159 }
160 else
161 {
162 if(!HitCharacter(From: m_Pos, To))
163 {
164 m_From = m_Pos;
165 m_Pos = To;
166 m_Energy = -1;
167 }
168 }
169}
170
171void CLaser::Tick()
172{
173 float Delay = GetTuning(i: m_TuneZone)->m_LaserBounceDelay;
174
175 if(GameWorld()->m_WorldConfig.m_IsVanilla) // predict old physics on vanilla 0.6 servers
176 {
177 if(GameWorld()->GameTick() > m_EvalTick + (GameWorld()->GameTickSpeed() * Delay / 1000.0f))
178 DoBounce();
179 }
180 else
181 {
182 if((GameWorld()->GameTick() - m_EvalTick) > (GameWorld()->GameTickSpeed() * Delay / 1000.0f))
183 DoBounce();
184 }
185}
186
187CLaser::CLaser(CGameWorld *pGameWorld, int Id, CLaserData *pLaser) :
188 CEntity(pGameWorld, CGameWorld::ENTTYPE_LASER)
189{
190 m_Pos = pLaser->m_To;
191 m_From = pLaser->m_From;
192 m_EvalTick = pLaser->m_StartTick;
193 m_TuneZone = GameWorld()->m_WorldConfig.m_UseTuneZones ? Collision()->IsTune(Index: Collision()->GetMapIndex(Pos: m_Pos)) : 0;
194 m_Owner = -2;
195 m_Energy = GetTuning(i: m_TuneZone)->m_LaserReach;
196 if(pGameWorld->m_WorldConfig.m_IsFNG && m_Energy < 10.f)
197 m_Energy = 800.0f;
198
199 m_Dir = m_Pos - m_From;
200 if(length(a: m_Pos - m_From) > 0.001f)
201 m_Dir = normalize(v: m_Dir);
202 else
203 m_Energy = 0;
204 m_Type = pLaser->m_Type == LASERTYPE_SHOTGUN ? WEAPON_SHOTGUN : WEAPON_LASER;
205 m_PrevPos = m_From;
206 m_Id = Id;
207}
208
209bool CLaser::Match(CLaser *pLaser)
210{
211 if(pLaser->m_EvalTick != m_EvalTick)
212 return false;
213 if(distance(a: pLaser->m_From, b: m_From) > 2.f)
214 return false;
215 const vec2 ThisDiff = m_Pos - m_From;
216 const vec2 OtherDiff = pLaser->m_Pos - pLaser->m_From;
217 const float DirError = distance(a: normalize(v: OtherDiff) * length(a: ThisDiff), b: ThisDiff);
218 return DirError <= 2.f;
219}
220
221CLaserData CLaser::GetData() const
222{
223 CLaserData Result;
224 Result.m_From.x = m_From.x;
225 Result.m_From.y = m_From.y;
226 Result.m_To.x = m_Pos.x;
227 Result.m_To.y = m_Pos.y;
228 Result.m_StartTick = m_EvalTick;
229 Result.m_ExtraInfo = true;
230 Result.m_Owner = m_Owner;
231 Result.m_Type = m_Type == WEAPON_SHOTGUN ? LASERTYPE_SHOTGUN : LASERTYPE_RIFLE;
232 Result.m_Subtype = -1;
233 Result.m_TuneZone = m_TuneZone;
234 Result.m_SwitchNumber = m_Number;
235 Result.m_Predict = true;
236 return Result;
237}
238