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 GameWorld()->CreatePredictedSound(Pos: m_Pos, SoundId: SOUND_LASER_BOUNCE, Id: m_Id);
153 }
154 }
155 else
156 {
157 if(!HitCharacter(From: m_Pos, To))
158 {
159 m_From = m_Pos;
160 m_Pos = To;
161 m_Energy = -1;
162 }
163 }
164}
165
166void CLaser::Tick()
167{
168 float Delay = GetTuning(i: m_TuneZone)->m_LaserBounceDelay;
169
170 if(GameWorld()->m_WorldConfig.m_IsVanilla) // predict old physics on vanilla 0.6 servers
171 {
172 if(GameWorld()->GameTick() > m_EvalTick + (GameWorld()->GameTickSpeed() * Delay / 1000.0f))
173 DoBounce();
174 }
175 else
176 {
177 if((GameWorld()->GameTick() - m_EvalTick) > (GameWorld()->GameTickSpeed() * Delay / 1000.0f))
178 DoBounce();
179 }
180}
181
182CLaser::CLaser(CGameWorld *pGameWorld, int Id, CLaserData *pLaser) :
183 CEntity(pGameWorld, CGameWorld::ENTTYPE_LASER)
184{
185 m_Pos = pLaser->m_To;
186 m_From = pLaser->m_From;
187 m_EvalTick = pLaser->m_StartTick;
188 m_TuneZone = GameWorld()->m_WorldConfig.m_UseTuneZones ? Collision()->IsTune(Index: Collision()->GetMapIndex(Pos: m_Pos)) : 0;
189 m_Owner = -2;
190 m_Energy = GetTuning(i: m_TuneZone)->m_LaserReach;
191 if(pGameWorld->m_WorldConfig.m_IsFNG && m_Energy < 10.f)
192 m_Energy = 800.0f;
193
194 m_Dir = m_Pos - m_From;
195 if(length(a: m_Pos - m_From) > 0.001f)
196 m_Dir = normalize(v: m_Dir);
197 else
198 m_Energy = 0;
199 m_Type = pLaser->m_Type == LASERTYPE_SHOTGUN ? WEAPON_SHOTGUN : WEAPON_LASER;
200 m_PrevPos = m_From;
201 m_Id = Id;
202}
203
204bool CLaser::Match(CLaser *pLaser)
205{
206 if(pLaser->m_EvalTick != m_EvalTick)
207 return false;
208 if(distance(a: pLaser->m_From, b: m_From) > 2.f)
209 return false;
210 const vec2 ThisDiff = m_Pos - m_From;
211 const vec2 OtherDiff = pLaser->m_Pos - pLaser->m_From;
212 const float DirError = distance(a: normalize(v: OtherDiff) * length(a: ThisDiff), b: ThisDiff);
213 return DirError <= 2.f;
214}
215
216CLaserData CLaser::GetData() const
217{
218 CLaserData Result;
219 Result.m_From.x = m_From.x;
220 Result.m_From.y = m_From.y;
221 Result.m_To.x = m_Pos.x;
222 Result.m_To.y = m_Pos.y;
223 Result.m_StartTick = m_EvalTick;
224 Result.m_ExtraInfo = true;
225 Result.m_Owner = m_Owner;
226 Result.m_Type = m_Type == WEAPON_SHOTGUN ? LASERTYPE_SHOTGUN : LASERTYPE_RIFLE;
227 Result.m_Subtype = -1;
228 Result.m_TuneZone = m_TuneZone;
229 Result.m_SwitchNumber = m_Number;
230 return Result;
231}
232