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