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