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/mapitems.h>
12#include <game/server/gamecontext.h>
13#include <game/server/gamemodes/DDRace.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 m_Dir = Direction;
22 m_Bounces = 0;
23 m_EvalTick = 0;
24 m_TelePos = vec2(0, 0);
25 m_WasTele = false;
26 m_Type = Type;
27 m_TeleportCancelled = false;
28 m_IsBlueTeleport = false;
29 m_ZeroEnergyBounceInLastTick = false;
30 m_TuneZone = GameServer()->Collision()->IsTune(Index: GameServer()->Collision()->GetMapIndex(Pos: m_Pos));
31 CCharacter *pOwnerChar = GameServer()->GetPlayerChar(ClientId: m_Owner);
32 m_TeamMask = pOwnerChar ? pOwnerChar->TeamMask() : CClientMask();
33 m_BelongsToPracticeTeam = pOwnerChar && pOwnerChar->Teams()->IsPractice(Team: pOwnerChar->Team());
34
35 GameWorld()->InsertEntity(pEntity: this);
36 DoBounce();
37}
38
39bool CLaser::HitCharacter(vec2 From, vec2 To)
40{
41 static const vec2 StackedLaserShotgunBugSpeed = vec2(-2147483648.0f, -2147483648.0f);
42 vec2 At;
43 CCharacter *pOwnerChar = GameServer()->GetPlayerChar(ClientId: m_Owner);
44 CCharacter *pHit;
45 bool pDontHitSelf = g_Config.m_SvOldLaser || (m_Bounces == 0 && !m_WasTele);
46
47 if(pOwnerChar ? (!pOwnerChar->LaserHitDisabled() && m_Type == WEAPON_LASER) || (!pOwnerChar->ShotgunHitDisabled() && m_Type == WEAPON_SHOTGUN) : g_Config.m_SvHit)
48 pHit = GameWorld()->IntersectCharacter(Pos0: m_Pos, Pos1: To, Radius: 0.f, NewPos&: At, pNotThis: pDontHitSelf ? pOwnerChar : nullptr, CollideWith: m_Owner);
49 else
50 pHit = GameWorld()->IntersectCharacter(Pos0: m_Pos, Pos1: To, Radius: 0.f, NewPos&: At, pNotThis: pDontHitSelf ? pOwnerChar : nullptr, CollideWith: m_Owner, pThisOnly: pOwnerChar);
51
52 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))
53 return false;
54 m_From = From;
55 m_Pos = At;
56 m_Energy = -1;
57 if(m_Type == WEAPON_SHOTGUN)
58 {
59 float Strength = TuningList()[m_TuneZone].m_ShotgunStrength;
60 const vec2 &HitPos = pHit->Core()->m_Pos;
61 if(!g_Config.m_SvOldLaser)
62 {
63 if(m_PrevPos != HitPos)
64 {
65 pHit->AddVelocity(Addition: normalize(v: m_PrevPos - HitPos) * Strength);
66 }
67 else
68 {
69 pHit->SetRawVelocity(StackedLaserShotgunBugSpeed);
70 }
71 }
72 else if(g_Config.m_SvOldLaser && pOwnerChar)
73 {
74 if(pOwnerChar->Core()->m_Pos != HitPos)
75 {
76 pHit->AddVelocity(Addition: normalize(v: pOwnerChar->Core()->m_Pos - HitPos) * Strength);
77 }
78 else
79 {
80 pHit->SetRawVelocity(StackedLaserShotgunBugSpeed);
81 }
82 }
83 else
84 {
85 // Re-apply move restrictions as a part of 'shotgun bug' reproduction
86 pHit->ApplyMoveRestrictions();
87 }
88 }
89 else if(m_Type == WEAPON_LASER)
90 {
91 pHit->UnFreeze();
92 }
93 pHit->TakeDamage(Force: vec2(0, 0), Dmg: 0, From: m_Owner, Weapon: m_Type);
94 return true;
95}
96
97void CLaser::DoBounce()
98{
99 m_EvalTick = Server()->Tick();
100
101 if(m_Energy < 0)
102 {
103 m_MarkedForDestroy = true;
104 return;
105 }
106 m_PrevPos = m_Pos;
107 vec2 Coltile;
108
109 int Res;
110 int z;
111
112 if(m_WasTele)
113 {
114 m_PrevPos = m_TelePos;
115 m_Pos = m_TelePos;
116 m_TelePos = vec2(0, 0);
117 }
118
119 vec2 To = m_Pos + m_Dir * m_Energy;
120
121 Res = GameServer()->Collision()->IntersectLineTeleWeapon(Pos0: m_Pos, Pos1: To, pOutCollision: &Coltile, pOutBeforeCollision: &To, pTeleNr: &z);
122
123 if(Res)
124 {
125 if(!HitCharacter(From: m_Pos, To))
126 {
127 // intersected
128 m_From = m_Pos;
129 m_Pos = To;
130
131 vec2 TempPos = m_Pos;
132 vec2 TempDir = m_Dir * 4.0f;
133
134 int f = 0;
135 if(Res == -1)
136 {
137 f = GameServer()->Collision()->GetTile(x: round_to_int(f: Coltile.x), y: round_to_int(f: Coltile.y));
138 GameServer()->Collision()->SetCollisionAt(x: round_to_int(f: Coltile.x), y: round_to_int(f: Coltile.y), Index: TILE_SOLID);
139 }
140 GameServer()->Collision()->MovePoint(pInoutPos: &TempPos, pInoutVel: &TempDir, Elasticity: 1.0f, pBounces: nullptr);
141 if(Res == -1)
142 {
143 GameServer()->Collision()->SetCollisionAt(x: round_to_int(f: Coltile.x), y: round_to_int(f: Coltile.y), Index: f);
144 }
145 m_Pos = TempPos;
146 m_Dir = normalize(v: TempDir);
147
148 const float Distance = distance(a: m_From, b: m_Pos);
149 // Prevent infinite bounces
150 if(Distance == 0.0f && m_ZeroEnergyBounceInLastTick)
151 {
152 m_Energy = -1;
153 }
154 else
155 {
156 m_Energy -= Distance + GameServer()->TuningList()[m_TuneZone].m_LaserBounceCost;
157 }
158 m_ZeroEnergyBounceInLastTick = Distance == 0.0f;
159
160 if(Res == TILE_TELEINWEAPON && !GameServer()->Collision()->TeleOuts(Number: z - 1).empty())
161 {
162 int TeleOut = GameServer()->m_World.m_Core.RandomOr0(BelowThis: GameServer()->Collision()->TeleOuts(Number: z - 1).size());
163 m_TelePos = GameServer()->Collision()->TeleOuts(Number: z - 1)[TeleOut];
164 m_WasTele = true;
165 }
166 else
167 {
168 m_Bounces++;
169 m_WasTele = false;
170 }
171
172 int BounceNum = TuningList()[m_TuneZone].m_LaserBounceNum;
173
174 if(m_Bounces > BounceNum)
175 m_Energy = -1;
176
177 GameServer()->CreateSound(Pos: m_Pos, Sound: SOUND_LASER_BOUNCE, Mask: m_TeamMask);
178 }
179 }
180 else
181 {
182 if(!HitCharacter(From: m_Pos, To))
183 {
184 m_From = m_Pos;
185 m_Pos = To;
186 m_Energy = -1;
187 }
188 }
189
190 CCharacter *pOwnerChar = GameServer()->GetPlayerChar(ClientId: m_Owner);
191 if(m_Owner >= 0 && m_Energy <= 0 && !m_TeleportCancelled && pOwnerChar &&
192 pOwnerChar->IsAlive() && pOwnerChar->HasTelegunLaser() && m_Type == WEAPON_LASER)
193 {
194 vec2 PossiblePos;
195 bool Found = false;
196
197 // Check if the laser hits a player.
198 bool pDontHitSelf = g_Config.m_SvOldLaser || (m_Bounces == 0 && !m_WasTele);
199 vec2 At;
200 CCharacter *pHit;
201 if(pOwnerChar ? (!pOwnerChar->LaserHitDisabled() && m_Type == WEAPON_LASER) : g_Config.m_SvHit)
202 pHit = GameServer()->m_World.IntersectCharacter(Pos0: m_Pos, Pos1: To, Radius: 0.f, NewPos&: At, pNotThis: pDontHitSelf ? pOwnerChar : nullptr, CollideWith: m_Owner);
203 else
204 pHit = GameServer()->m_World.IntersectCharacter(Pos0: m_Pos, Pos1: To, Radius: 0.f, NewPos&: At, pNotThis: pDontHitSelf ? pOwnerChar : nullptr, CollideWith: m_Owner, pThisOnly: pOwnerChar);
205
206 if(pHit)
207 Found = GetNearestAirPosPlayer(PlayerPos: pHit->m_Pos, pOutPos: &PossiblePos);
208 else
209 Found = GetNearestAirPos(Pos: m_Pos, PrevPos: m_From, pOutPos: &PossiblePos);
210
211 if(Found)
212 {
213 pOwnerChar->m_TeleGunPos = PossiblePos;
214 pOwnerChar->m_TeleGunTeleport = true;
215 pOwnerChar->m_IsBlueTeleGunTeleport = m_IsBlueTeleport;
216 }
217 }
218 else if(m_Owner >= 0)
219 {
220 int MapIndex = GameServer()->Collision()->GetPureMapIndex(Pos: Coltile);
221 int TileFIndex = GameServer()->Collision()->GetFrontTileIndex(Index: MapIndex);
222 bool IsSwitchTeleGun = GameServer()->Collision()->GetSwitchType(Index: MapIndex) == TILE_ALLOW_TELE_GUN;
223 bool IsBlueSwitchTeleGun = GameServer()->Collision()->GetSwitchType(Index: MapIndex) == TILE_ALLOW_BLUE_TELE_GUN;
224 int IsTeleInWeapon = GameServer()->Collision()->IsTeleportWeapon(Index: MapIndex);
225
226 if(!IsTeleInWeapon)
227 {
228 if(IsSwitchTeleGun || IsBlueSwitchTeleGun)
229 {
230 // Delay specifies which weapon the tile should work for.
231 // Delay = 0 means all.
232 const int Delay = GameServer()->Collision()->GetSwitchDelay(Index: MapIndex);
233
234 if((Delay != 3 && Delay != 0) && m_Type == WEAPON_LASER)
235 {
236 IsSwitchTeleGun = IsBlueSwitchTeleGun = false;
237 }
238 }
239
240 m_IsBlueTeleport = TileFIndex == TILE_ALLOW_BLUE_TELE_GUN || IsBlueSwitchTeleGun;
241
242 // Teleport is canceled if the last bounce tile is not a TILE_ALLOW_TELE_GUN.
243 // Teleport also works if laser didn't bounce.
244 m_TeleportCancelled =
245 m_Type == WEAPON_LASER && (TileFIndex != TILE_ALLOW_TELE_GUN && TileFIndex != TILE_ALLOW_BLUE_TELE_GUN && !IsSwitchTeleGun && !IsBlueSwitchTeleGun);
246 }
247 }
248}
249
250void CLaser::Reset()
251{
252 m_MarkedForDestroy = true;
253}
254
255void CLaser::Tick()
256{
257 if((g_Config.m_SvDestroyLasersOnDeath || m_BelongsToPracticeTeam) && m_Owner >= 0)
258 {
259 CCharacter *pOwnerChar = GameServer()->GetPlayerChar(ClientId: m_Owner);
260 if(!(pOwnerChar && pOwnerChar->IsAlive()))
261 {
262 Reset();
263 }
264 }
265
266 float Delay = TuningList()[m_TuneZone].m_LaserBounceDelay;
267 if((Server()->Tick() - m_EvalTick) > (Server()->TickSpeed() * Delay / 1000.0f))
268 DoBounce();
269}
270
271void CLaser::TickPaused()
272{
273 ++m_EvalTick;
274}
275
276void CLaser::Snap(int SnappingClient)
277{
278 if(NetworkClipped(SnappingClient) && NetworkClipped(SnappingClient, CheckPos: m_From))
279 return;
280 CCharacter *pOwnerChar = nullptr;
281 if(m_Owner >= 0)
282 pOwnerChar = GameServer()->GetPlayerChar(ClientId: m_Owner);
283 if(!pOwnerChar)
284 return;
285
286 pOwnerChar = nullptr;
287 CClientMask TeamMask = CClientMask().set();
288
289 if(m_Owner >= 0)
290 pOwnerChar = GameServer()->GetPlayerChar(ClientId: m_Owner);
291
292 if(pOwnerChar && pOwnerChar->IsAlive())
293 TeamMask = pOwnerChar->TeamMask();
294
295 if(SnappingClient != SERVER_DEMO_CLIENT && !TeamMask.test(position: SnappingClient))
296 return;
297
298 int SnappingClientVersion = GameServer()->GetClientVersion(ClientId: SnappingClient);
299 int LaserType = m_Type == WEAPON_LASER ? LASERTYPE_RIFLE : (m_Type == WEAPON_SHOTGUN ? LASERTYPE_SHOTGUN : -1);
300
301 GameServer()->SnapLaserObject(Context: CSnapContext(SnappingClientVersion, Server()->IsSixup(ClientId: SnappingClient), SnappingClient), SnapId: GetId(),
302 To: m_Pos, From: m_From, StartTick: m_EvalTick, Owner: m_Owner, LaserType, Subtype: 0, SwitchNumber: m_Number);
303}
304
305void CLaser::SwapClients(int Client1, int Client2)
306{
307 m_Owner = m_Owner == Client1 ? Client2 : (m_Owner == Client2 ? Client1 : m_Owner);
308}
309