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 else if(m_Type == WEAPON_LASER)
85 {
86 pHit->UnFreeze();
87 }
88 return true;
89}
90
91void CLaser::DoBounce()
92{
93 m_EvalTick = GameWorld()->GameTick();
94
95 if(m_Energy < 0)
96 {
97 m_MarkedForDestroy = true;
98 return;
99 }
100 m_PrevPos = m_Pos;
101 vec2 Coltile;
102
103 int Res;
104 vec2 To = m_Pos + m_Dir * m_Energy;
105
106 Res = Collision()->IntersectLineTeleWeapon(Pos0: m_Pos, Pos1: To, pOutCollision: &Coltile, pOutBeforeCollision: &To);
107
108 if(Res)
109 {
110 if(!HitCharacter(From: m_Pos, To))
111 {
112 // intersected
113 m_From = m_Pos;
114 m_Pos = To;
115
116 vec2 TempPos = m_Pos;
117 vec2 TempDir = m_Dir * 4.0f;
118
119 int f = 0;
120 if(Res == -1)
121 {
122 f = Collision()->GetTile(x: round_to_int(f: Coltile.x), y: round_to_int(f: Coltile.y));
123 Collision()->SetCollisionAt(x: round_to_int(f: Coltile.x), y: round_to_int(f: Coltile.y), Index: TILE_SOLID);
124 }
125 Collision()->MovePoint(pInoutPos: &TempPos, pInoutVel: &TempDir, Elasticity: 1.0f, pBounces: nullptr);
126 if(Res == -1)
127 {
128 Collision()->SetCollisionAt(x: round_to_int(f: Coltile.x), y: round_to_int(f: Coltile.y), Index: f);
129 }
130 m_Pos = TempPos;
131 m_Dir = normalize(v: TempDir);
132
133 const float Distance = distance(a: m_From, b: m_Pos);
134 // Prevent infinite bounces
135 if(Distance == 0.0f && m_ZeroEnergyBounceInLastTick)
136 {
137 m_Energy = -1;
138 }
139 else
140 {
141 m_Energy -= Distance + GetTuning(i: m_TuneZone)->m_LaserBounceCost;
142 }
143 m_ZeroEnergyBounceInLastTick = Distance == 0.0f;
144
145 m_Bounces++;
146
147 int BounceNum = GetTuning(i: m_TuneZone)->m_LaserBounceNum;
148
149 if(m_Bounces > BounceNum)
150 m_Energy = -1;
151 }
152 }
153 else
154 {
155 if(!HitCharacter(From: m_Pos, To))
156 {
157 m_From = m_Pos;
158 m_Pos = To;
159 m_Energy = -1;
160 }
161 }
162}
163
164void CLaser::Tick()
165{
166 float Delay = GetTuning(i: m_TuneZone)->m_LaserBounceDelay;
167
168 if(GameWorld()->m_WorldConfig.m_IsVanilla) // predict old physics on vanilla 0.6 servers
169 {
170 if(GameWorld()->GameTick() > m_EvalTick + (GameWorld()->GameTickSpeed() * Delay / 1000.0f))
171 DoBounce();
172 }
173 else
174 {
175 if((GameWorld()->GameTick() - m_EvalTick) > (GameWorld()->GameTickSpeed() * Delay / 1000.0f))
176 DoBounce();
177 }
178}
179
180CLaser::CLaser(CGameWorld *pGameWorld, int Id, CLaserData *pLaser) :
181 CEntity(pGameWorld, CGameWorld::ENTTYPE_LASER)
182{
183 m_Pos = pLaser->m_To;
184 m_From = pLaser->m_From;
185 m_EvalTick = pLaser->m_StartTick;
186 m_TuneZone = GameWorld()->m_WorldConfig.m_UseTuneZones ? Collision()->IsTune(Index: Collision()->GetMapIndex(Pos: m_Pos)) : 0;
187 m_Owner = -2;
188 m_Energy = GetTuning(i: m_TuneZone)->m_LaserReach;
189 if(pGameWorld->m_WorldConfig.m_IsFNG && m_Energy < 10.f)
190 m_Energy = 800.0f;
191
192 m_Dir = m_Pos - m_From;
193 if(length(a: m_Pos - m_From) > 0.001f)
194 m_Dir = normalize(v: m_Dir);
195 else
196 m_Energy = 0;
197 m_Type = pLaser->m_Type == LASERTYPE_SHOTGUN ? WEAPON_SHOTGUN : WEAPON_LASER;
198 m_PrevPos = m_From;
199 m_Id = Id;
200}
201
202bool CLaser::Match(CLaser *pLaser)
203{
204 if(pLaser->m_EvalTick != m_EvalTick)
205 return false;
206 if(distance(a: pLaser->m_From, b: m_From) > 2.f)
207 return false;
208 const vec2 ThisDiff = m_Pos - m_From;
209 const vec2 OtherDiff = pLaser->m_Pos - pLaser->m_From;
210 const float DirError = distance(a: normalize(v: OtherDiff) * length(a: ThisDiff), b: ThisDiff);
211 return DirError <= 2.f;
212}
213
214CLaserData CLaser::GetData() const
215{
216 CLaserData Result;
217 Result.m_From.x = m_From.x;
218 Result.m_From.y = m_From.y;
219 Result.m_To.x = m_Pos.x;
220 Result.m_To.y = m_Pos.y;
221 Result.m_StartTick = m_EvalTick;
222 Result.m_ExtraInfo = true;
223 Result.m_Owner = m_Owner;
224 Result.m_Type = m_Type == WEAPON_SHOTGUN ? LASERTYPE_SHOTGUN : LASERTYPE_RIFLE;
225 Result.m_Subtype = -1;
226 Result.m_TuneZone = m_TuneZone;
227 Result.m_SwitchNumber = m_Number;
228 return Result;
229}
230