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 "character.h"
4
5#include "laser.h"
6#include "projectile.h"
7
8#include <engine/shared/config.h>
9
10#include <generated/client_data.h>
11
12#include <game/collision.h>
13#include <game/mapitems.h>
14
15// Character, "physical" player's part
16
17void CCharacter::SetWeapon(int Weapon)
18{
19 if(Weapon == m_Core.m_ActiveWeapon)
20 return;
21
22 m_LastWeapon = m_Core.m_ActiveWeapon;
23 m_QueuedWeapon = -1;
24 SetActiveWeapon(Weapon);
25}
26
27void CCharacter::SetSolo(bool Solo)
28{
29 m_Core.m_Solo = Solo;
30 TeamsCore()->SetSolo(ClientId: GetCid(), Value: Solo);
31}
32
33void CCharacter::SetSuper(bool Super)
34{
35 m_Core.m_Super = Super;
36 if(m_Core.m_Super)
37 TeamsCore()->Team(ClientId: GetCid(), Team: TeamsCore()->m_IsDDRace16 ? VANILLA_TEAM_SUPER : TEAM_SUPER);
38}
39
40bool CCharacter::IsGrounded()
41{
42 if(Collision()->CheckPoint(x: m_Pos.x + GetProximityRadius() / 2, y: m_Pos.y + GetProximityRadius() / 2 + 5))
43 return true;
44 if(Collision()->CheckPoint(x: m_Pos.x - GetProximityRadius() / 2, y: m_Pos.y + GetProximityRadius() / 2 + 5))
45 return true;
46
47 int MoveRestrictionsBelow = Collision()->GetMoveRestrictions(Pos: m_Pos + vec2(0, GetProximityRadius() / 2 + 4), Distance: 0.0f);
48 return (MoveRestrictionsBelow & CANTMOVE_DOWN) != 0;
49}
50
51void CCharacter::HandleJetpack()
52{
53 if(m_NumInputs < 2)
54 return;
55
56 vec2 Direction = normalize(v: vec2(m_LatestInput.m_TargetX, m_LatestInput.m_TargetY));
57
58 bool FullAuto = false;
59 if(m_Core.m_ActiveWeapon == WEAPON_GRENADE || m_Core.m_ActiveWeapon == WEAPON_SHOTGUN || m_Core.m_ActiveWeapon == WEAPON_LASER)
60 FullAuto = true;
61 if(m_Core.m_Jetpack && m_Core.m_ActiveWeapon == WEAPON_GUN)
62 FullAuto = true;
63
64 // check if we gonna fire
65 bool WillFire = false;
66 if(CountInput(Prev: m_LatestPrevInput.m_Fire, Cur: m_LatestInput.m_Fire).m_Presses)
67 WillFire = true;
68
69 if(FullAuto && (m_LatestInput.m_Fire & 1) && m_Core.m_aWeapons[m_Core.m_ActiveWeapon].m_Ammo)
70 WillFire = true;
71
72 if(!WillFire)
73 return;
74
75 // check for ammo
76 if(!m_Core.m_aWeapons[m_Core.m_ActiveWeapon].m_Ammo || m_FreezeTime)
77 {
78 return;
79 }
80
81 switch(m_Core.m_ActiveWeapon)
82 {
83 case WEAPON_GUN:
84 {
85 if(m_Core.m_Jetpack)
86 {
87 float Strength = GetTuning(i: GetOverriddenTuneZone())->m_JetpackStrength;
88 TakeDamage(Force: Direction * -1.0f * (Strength / 100.0f / 6.11f), Dmg: 0, From: GetCid(), Weapon: m_Core.m_ActiveWeapon);
89 }
90 }
91 }
92}
93
94void CCharacter::RemoveNinja()
95{
96 m_Core.m_Ninja.m_CurrentMoveTime = 0;
97 m_Core.m_aWeapons[WEAPON_NINJA].m_Got = false;
98 m_Core.m_ActiveWeapon = m_LastWeapon;
99
100 SetWeapon(m_Core.m_ActiveWeapon);
101}
102
103void CCharacter::HandleNinja()
104{
105 if(m_Core.m_ActiveWeapon != WEAPON_NINJA)
106 return;
107
108 if((GameWorld()->GameTick() - m_Core.m_Ninja.m_ActivationTick) > (g_pData->m_Weapons.m_Ninja.m_Duration * GameWorld()->GameTickSpeed() / 1000))
109 {
110 // time's up, return
111 RemoveNinja();
112 return;
113 }
114
115 // force ninja Weapon
116 SetWeapon(WEAPON_NINJA);
117
118 m_Core.m_Ninja.m_CurrentMoveTime--;
119
120 if(m_Core.m_Ninja.m_CurrentMoveTime == 0)
121 {
122 // reset velocity
123 m_Core.m_Vel = m_Core.m_Ninja.m_ActivationDir * m_Core.m_Ninja.m_OldVelAmount;
124 }
125
126 if(m_Core.m_Ninja.m_CurrentMoveTime > 0)
127 {
128 // Set velocity
129 m_Core.m_Vel = m_Core.m_Ninja.m_ActivationDir * g_pData->m_Weapons.m_Ninja.m_Velocity;
130 vec2 OldPos = m_Pos;
131 Collision()->MoveBox(pInoutPos: &m_Core.m_Pos, pInoutVel: &m_Core.m_Vel, Size: vec2(m_ProximityRadius, m_ProximityRadius), Elasticity: vec2(GetTuning(i: GetOverriddenTuneZone())->m_GroundElasticityX, GetTuning(i: GetOverriddenTuneZone())->m_GroundElasticityY));
132
133 // reset velocity so the client doesn't predict stuff
134 m_Core.m_Vel = vec2(0.f, 0.f);
135
136 // check if we Hit anything along the way
137 {
138 CEntity *apEnts[MAX_CLIENTS];
139 float Radius = m_ProximityRadius * 2.0f;
140 int Num = GameWorld()->FindEntities(Pos: OldPos, Radius, ppEnts: apEnts, Max: MAX_CLIENTS, Type: CGameWorld::ENTTYPE_CHARACTER);
141
142 // check that we're not in solo part
143 if(TeamsCore()->GetSolo(ClientId: GetCid()))
144 return;
145
146 for(int i = 0; i < Num; ++i)
147 {
148 auto *pChr = static_cast<CCharacter *>(apEnts[i]);
149 if(pChr == this)
150 continue;
151
152 // Don't hit players in other teams
153 if(Team() != pChr->Team())
154 continue;
155
156 const int ClientId = pChr->GetCid();
157
158 // Don't hit players in solo parts
159 if(TeamsCore()->GetSolo(ClientId))
160 return;
161
162 // make sure we haven't Hit this object before
163 bool AlreadyHit = false;
164 for(int j = 0; j < m_NumObjectsHit; j++)
165 {
166 if(m_aHitObjects[j] == ClientId)
167 AlreadyHit = true;
168 }
169 if(AlreadyHit)
170 continue;
171
172 // check so we are sufficiently close
173 if(distance(a: pChr->m_Pos, b: m_Pos) > Radius)
174 continue;
175
176 // Hit a player, give them damage and stuffs...
177 // set his velocity to fast upward (for now)
178 dbg_assert(m_NumObjectsHit < MAX_CLIENTS, "m_aHitObjects overflow");
179 m_aHitObjects[m_NumObjectsHit++] = ClientId;
180
181 pChr->TakeDamage(Force: vec2(0, -10.0f), Dmg: g_pData->m_Weapons.m_Ninja.m_pBase->m_Damage, From: GetCid(), Weapon: WEAPON_NINJA);
182 }
183 }
184
185 return;
186 }
187}
188
189void CCharacter::DoWeaponSwitch()
190{
191 // make sure we can switch
192 if(m_ReloadTimer != 0 || m_QueuedWeapon == -1 || m_Core.m_aWeapons[WEAPON_NINJA].m_Got || !m_Core.m_aWeapons[m_QueuedWeapon].m_Got)
193 return;
194
195 // switch Weapon
196 SetWeapon(m_QueuedWeapon);
197}
198
199void CCharacter::HandleWeaponSwitch()
200{
201 if(m_NumInputs < 2)
202 return;
203
204 int WantedWeapon = m_Core.m_ActiveWeapon;
205 if(m_QueuedWeapon != -1)
206 WantedWeapon = m_QueuedWeapon;
207
208 bool Anything = false;
209 for(int i = 0; i < NUM_WEAPONS - 1; ++i)
210 if(m_Core.m_aWeapons[i].m_Got)
211 Anything = true;
212 if(!Anything)
213 return;
214 // select Weapon
215 int Next = CountInput(Prev: m_LatestPrevInput.m_NextWeapon, Cur: m_LatestInput.m_NextWeapon).m_Presses;
216 int Prev = CountInput(Prev: m_LatestPrevInput.m_PrevWeapon, Cur: m_LatestInput.m_PrevWeapon).m_Presses;
217
218 if(Next < 128) // make sure we only try sane stuff
219 {
220 while(Next) // Next Weapon selection
221 {
222 WantedWeapon = (WantedWeapon + 1) % NUM_WEAPONS;
223 if(m_Core.m_aWeapons[WantedWeapon].m_Got)
224 Next--;
225 }
226 }
227
228 if(Prev < 128) // make sure we only try sane stuff
229 {
230 while(Prev) // Prev Weapon selection
231 {
232 WantedWeapon = (WantedWeapon - 1) < 0 ? NUM_WEAPONS - 1 : WantedWeapon - 1;
233 if(m_Core.m_aWeapons[WantedWeapon].m_Got)
234 Prev--;
235 }
236 }
237
238 // Direct Weapon selection
239 if(m_LatestInput.m_WantedWeapon)
240 WantedWeapon = m_Input.m_WantedWeapon - 1;
241
242 // check for insane values
243 if(WantedWeapon >= 0 && WantedWeapon < NUM_WEAPONS && WantedWeapon != m_Core.m_ActiveWeapon && m_Core.m_aWeapons[WantedWeapon].m_Got)
244 m_QueuedWeapon = WantedWeapon;
245
246 DoWeaponSwitch();
247}
248
249void CCharacter::FireWeapon()
250{
251 if(m_NumInputs < 2)
252 return;
253
254 if(!GameWorld()->m_WorldConfig.m_PredictWeapons)
255 return;
256
257 if(m_ReloadTimer != 0)
258 return;
259
260 DoWeaponSwitch();
261 vec2 Direction = normalize(v: vec2(m_LatestInput.m_TargetX, m_LatestInput.m_TargetY));
262
263 bool FullAuto = false;
264 if(m_Core.m_ActiveWeapon == WEAPON_GRENADE || m_Core.m_ActiveWeapon == WEAPON_SHOTGUN || m_Core.m_ActiveWeapon == WEAPON_LASER)
265 FullAuto = true;
266 if(m_Core.m_Jetpack && m_Core.m_ActiveWeapon == WEAPON_GUN)
267 FullAuto = true;
268 if(m_FrozenLastTick)
269 FullAuto = true;
270
271 // don't fire hammer when player is deep and sv_deepfly is disabled
272 if(!g_Config.m_SvDeepfly && m_Core.m_ActiveWeapon == WEAPON_HAMMER && m_Core.m_DeepFrozen)
273 return;
274
275 // check if we gonna fire
276 bool WillFire = false;
277 if(CountInput(Prev: m_LatestPrevInput.m_Fire, Cur: m_LatestInput.m_Fire).m_Presses)
278 WillFire = true;
279
280 if(FullAuto && (m_LatestInput.m_Fire & 1) && m_Core.m_aWeapons[m_Core.m_ActiveWeapon].m_Ammo)
281 WillFire = true;
282
283 if(!WillFire)
284 return;
285
286 // check for ammo
287 if(!m_Core.m_aWeapons[m_Core.m_ActiveWeapon].m_Ammo || m_FreezeTime)
288 {
289 return;
290 }
291
292 vec2 ProjStartPos = m_Pos + Direction * m_ProximityRadius * 0.75f;
293
294 switch(m_Core.m_ActiveWeapon)
295 {
296 case WEAPON_HAMMER:
297 {
298 if(m_Core.m_HammerHitDisabled)
299 break;
300
301 CEntity *apEnts[MAX_CLIENTS];
302 int Hits = 0;
303 int Num = GameWorld()->FindEntities(Pos: ProjStartPos, Radius: m_ProximityRadius * 0.5f, ppEnts: apEnts,
304 Max: MAX_CLIENTS, Type: CGameWorld::ENTTYPE_CHARACTER);
305
306 for(int i = 0; i < Num; ++i)
307 {
308 auto *pTarget = static_cast<CCharacter *>(apEnts[i]);
309
310 if((pTarget == this || !CanCollide(ClientId: pTarget->GetCid())))
311 continue;
312
313 // set his velocity to fast upward (for now)
314
315 vec2 Dir;
316 if(length(a: pTarget->m_Pos - m_Pos) > 0.0f)
317 Dir = normalize(v: pTarget->m_Pos - m_Pos);
318 else
319 Dir = vec2(0.f, -1.f);
320
321 float Strength = GetTuning(i: GetOverriddenTuneZone())->m_HammerStrength;
322
323 vec2 Temp = pTarget->m_Core.m_Vel + normalize(v: Dir + vec2(0.f, -1.1f)) * 10.0f;
324 Temp = ClampVel(MoveRestriction: pTarget->m_MoveRestrictions, Vel: Temp);
325 Temp -= pTarget->m_Core.m_Vel;
326
327 vec2 Force = vec2(0.f, -1.0f) + Temp;
328
329 if(GameWorld()->m_WorldConfig.m_IsFNG)
330 {
331 if(m_GameTeam == pTarget->m_GameTeam && pTarget->m_LastSnapWeapon == WEAPON_NINJA) // melt hammer
332 {
333 Force.x *= 50 * 0.01f;
334 Force.y *= 50 * 0.01f;
335 }
336 else
337 {
338 Force.x *= 320 * 0.01f;
339 Force.y *= 120 * 0.01f;
340 }
341 }
342 else
343 Force *= Strength;
344
345 pTarget->TakeDamage(Force, Dmg: g_pData->m_Weapons.m_Hammer.m_pBase->m_Damage,
346 From: GetCid(), Weapon: m_Core.m_ActiveWeapon);
347 pTarget->UnFreeze();
348
349 Hits++;
350 }
351
352 // if we Hit anything, we have to wait for the reload
353 if(Hits)
354 {
355 float FireDelay = GetTuning(i: GetOverriddenTuneZone())->m_HammerHitFireDelay;
356 m_ReloadTimer = FireDelay * GameWorld()->GameTickSpeed() / 1000;
357 }
358 }
359 break;
360
361 case WEAPON_GUN:
362 {
363 if(!m_Core.m_Jetpack)
364 {
365 int Lifetime = (int)(GameWorld()->GameTickSpeed() * GetTuning(i: GetOverriddenTuneZone())->m_GunLifetime);
366
367 new CProjectile(
368 GameWorld(),
369 WEAPON_GUN, //Type
370 GetCid(), //Owner
371 ProjStartPos, //Pos
372 Direction, //Dir
373 Lifetime, //Span
374 false, //Freeze
375 false, //Explosive
376 0, //Force
377 -1 //SoundImpact
378 );
379 }
380 }
381 break;
382
383 case WEAPON_SHOTGUN:
384 {
385 if(GameWorld()->m_WorldConfig.m_IsVanilla)
386 {
387 int ShotSpread = 2;
388 for(int i = -ShotSpread; i <= ShotSpread; ++i)
389 {
390 const float aSpreading[] = {-0.185f, -0.070f, 0, 0.070f, 0.185f};
391 float a = angle(a: Direction);
392 a += aSpreading[i + 2];
393 float v = 1 - (absolute(a: i) / (float)ShotSpread);
394 float Speed = mix(a: (float)GlobalTuning()->m_ShotgunSpeeddiff, b: 1.0f, amount: v);
395 new CProjectile(
396 GameWorld(),
397 WEAPON_SHOTGUN, //Type
398 GetCid(), //Owner
399 ProjStartPos, //Pos
400 direction(angle: a) * Speed, //Dir
401 (int)(GameWorld()->GameTickSpeed() * GlobalTuning()->m_ShotgunLifetime), //Span
402 false, //Freeze
403 false, //Explosive
404 -1 //SoundImpact
405 );
406 }
407 }
408 else if(GameWorld()->m_WorldConfig.m_IsDDRace)
409 {
410 float LaserReach = GetTuning(i: GetOverriddenTuneZone())->m_LaserReach;
411
412 new CLaser(GameWorld(), m_Pos, Direction, LaserReach, GetCid(), WEAPON_SHOTGUN);
413 }
414 }
415 break;
416
417 case WEAPON_GRENADE:
418 {
419 int Lifetime = (int)(GameWorld()->GameTickSpeed() * GetTuning(i: GetOverriddenTuneZone())->m_GrenadeLifetime);
420
421 new CProjectile(
422 GameWorld(),
423 WEAPON_GRENADE, //Type
424 GetCid(), //Owner
425 ProjStartPos, //Pos
426 Direction, //Dir
427 Lifetime, //Span
428 false, //Freeze
429 true, //Explosive
430 SOUND_GRENADE_EXPLODE //SoundImpact
431 ); //SoundImpact
432 }
433 break;
434
435 case WEAPON_LASER:
436 {
437 float LaserReach = GetTuning(i: GetOverriddenTuneZone())->m_LaserReach;
438
439 new CLaser(GameWorld(), m_Pos, Direction, LaserReach, GetCid(), WEAPON_LASER);
440 }
441 break;
442
443 case WEAPON_NINJA:
444 {
445 // reset Hit objects
446 m_NumObjectsHit = 0;
447
448 m_Core.m_Ninja.m_ActivationDir = Direction;
449 m_Core.m_Ninja.m_CurrentMoveTime = g_pData->m_Weapons.m_Ninja.m_Movetime * GameWorld()->GameTickSpeed() / 1000;
450
451 // clamp to prevent massive MoveBox calculation lag with SG bug
452 m_Core.m_Ninja.m_OldVelAmount = std::clamp(val: length(a: m_Core.m_Vel), lo: 0.0f, hi: 6000.0f);
453 }
454 break;
455 }
456
457 m_AttackTick = GameWorld()->GameTick(); // NOLINT(clang-analyzer-unix.Malloc)
458
459 if(!m_ReloadTimer)
460 {
461 float FireDelay;
462 GetTuning(i: GetOverriddenTuneZone())->Get(offsetof(CTuningParams, m_HammerFireDelay) / sizeof(CTuneParam) + m_Core.m_ActiveWeapon, pValue: &FireDelay);
463
464 m_ReloadTimer = FireDelay * GameWorld()->GameTickSpeed() / 1000;
465 }
466}
467
468void CCharacter::HandleWeapons()
469{
470 //ninja
471 HandleNinja();
472 HandleJetpack();
473
474 // check reload timer
475 if(m_ReloadTimer)
476 {
477 m_ReloadTimer--;
478 return;
479 }
480
481 // fire Weapon, if wanted
482 FireWeapon();
483}
484
485void CCharacter::GiveNinja()
486{
487 m_Core.m_Ninja.m_ActivationTick = GameWorld()->GameTick();
488 m_Core.m_aWeapons[WEAPON_NINJA].m_Got = true;
489 if(m_FreezeTime == 0)
490 m_Core.m_aWeapons[WEAPON_NINJA].m_Ammo = -1;
491 if(m_Core.m_ActiveWeapon != WEAPON_NINJA)
492 m_LastWeapon = m_Core.m_ActiveWeapon;
493 SetActiveWeapon(WEAPON_NINJA);
494}
495
496void CCharacter::OnPredictedInput(const CNetObj_PlayerInput *pNewInput)
497{
498 // skip the input if chat is active
499 if(!GameWorld()->m_WorldConfig.m_BugDDRaceInput && pNewInput->m_PlayerFlags & PLAYERFLAG_CHATTING)
500 {
501 // save the reset input
502 mem_copy(dest: &m_SavedInput, source: &m_Input, size: sizeof(m_SavedInput));
503 return;
504 }
505
506 // copy new input
507 mem_copy(dest: &m_Input, source: pNewInput, size: sizeof(m_Input));
508
509 // it is not allowed to aim in the center
510 if(m_Input.m_TargetX == 0 && m_Input.m_TargetY == 0)
511 m_Input.m_TargetY = -1;
512
513 mem_copy(dest: &m_SavedInput, source: &m_Input, size: sizeof(m_SavedInput));
514}
515
516void CCharacter::OnDirectInput(const CNetObj_PlayerInput *pNewInput)
517{
518 // skip the input if chat is active
519 if(!GameWorld()->m_WorldConfig.m_BugDDRaceInput && pNewInput->m_PlayerFlags & PLAYERFLAG_CHATTING)
520 {
521 // reset input
522 ResetInput();
523 // mods that do not allow inputs to be held while chatting also do not allow to hold hook
524 m_Input.m_Hook = 0;
525 return;
526 }
527
528 m_NumInputs++;
529 mem_copy(dest: &m_LatestPrevInput, source: &m_LatestInput, size: sizeof(m_LatestInput));
530 mem_copy(dest: &m_LatestInput, source: pNewInput, size: sizeof(m_LatestInput));
531
532 // it is not allowed to aim in the center
533 if(m_LatestInput.m_TargetX == 0 && m_LatestInput.m_TargetY == 0)
534 m_LatestInput.m_TargetY = -1;
535
536 if(m_NumInputs > 1 && Team() != TEAM_SPECTATORS)
537 {
538 HandleWeaponSwitch();
539 FireWeapon();
540 }
541
542 mem_copy(dest: &m_LatestPrevInput, source: &m_LatestInput, size: sizeof(m_LatestInput));
543}
544
545void CCharacter::ReleaseHook()
546{
547 m_Core.SetHookedPlayer(-1);
548 m_Core.m_HookState = HOOK_RETRACTED;
549 m_Core.m_TriggeredEvents |= COREEVENT_HOOK_RETRACT;
550}
551
552void CCharacter::ResetHook()
553{
554 ReleaseHook();
555 m_Core.m_HookPos = m_Core.m_Pos;
556}
557
558void CCharacter::ResetInput()
559{
560 m_Input.m_Direction = 0;
561 // m_Input.m_Hook = 0;
562 // simulate releasing the fire button
563 if((m_Input.m_Fire & 1) != 0)
564 m_Input.m_Fire++;
565 m_Input.m_Fire &= INPUT_STATE_MASK;
566 m_Input.m_Jump = 0;
567 m_LatestPrevInput = m_LatestInput = m_Input;
568}
569
570void CCharacter::PreTick()
571{
572 DDRaceTick();
573
574 m_Core.m_Input = m_Input;
575 m_Core.Tick(UseInput: true, DoDeferredTick: !m_pGameWorld->m_WorldConfig.m_NoWeakHookAndBounce);
576}
577
578void CCharacter::Tick()
579{
580 if(m_pGameWorld->m_WorldConfig.m_NoWeakHookAndBounce)
581 {
582 m_Core.TickDeferred();
583 }
584 else
585 {
586 PreTick();
587 }
588
589 // handle Weapons
590 HandleWeapons();
591
592 DDRacePostCoreTick();
593
594 // Previnput
595 m_PrevInput = m_Input;
596
597 m_PrevPrevPos = m_PrevPos;
598 m_PrevPos = m_Core.m_Pos;
599}
600
601void CCharacter::TickDeferred()
602{
603 m_Core.Move();
604 m_Core.Quantize();
605 m_Pos = m_Core.m_Pos;
606}
607
608bool CCharacter::TakeDamage(vec2 Force, int Dmg, int From, int Weapon)
609{
610 vec2 Temp = m_Core.m_Vel + Force;
611 m_Core.m_Vel = ClampVel(MoveRestriction: m_MoveRestrictions, Vel: Temp);
612 return true;
613}
614
615// DDRace
616
617bool CCharacter::CanCollide(int ClientId)
618{
619 return TeamsCore()->CanCollide(ClientId1: GetCid(), ClientId2: ClientId);
620}
621
622bool CCharacter::SameTeam(int ClientId)
623{
624 return TeamsCore()->SameTeam(ClientId1: GetCid(), ClientId2: ClientId);
625}
626
627int CCharacter::Team()
628{
629 return TeamsCore()->Team(ClientId: GetCid());
630}
631
632void CCharacter::HandleSkippableTiles(int Index)
633{
634 if(Index < 0)
635 return;
636
637 // handle speedup tiles
638 if(Collision()->IsSpeedup(Index))
639 {
640 vec2 Direction, TempVel = m_Core.m_Vel;
641 int Force, Type, MaxSpeed = 0;
642 Collision()->GetSpeedup(Index, pDir: &Direction, pForce: &Force, pMaxSpeed: &MaxSpeed, pType: &Type);
643
644 if(Type == TILE_SPEED_BOOST_OLD)
645 {
646 float TeeAngle, SpeederAngle, DiffAngle, SpeedLeft, TeeSpeed;
647 if(Force == 255 && MaxSpeed)
648 {
649 m_Core.m_Vel = Direction * (MaxSpeed / 5);
650 }
651 else
652 {
653 if(MaxSpeed > 0 && MaxSpeed < 5)
654 MaxSpeed = 5;
655 if(MaxSpeed > 0)
656 {
657 if(Direction.x > 0.0000001f)
658 SpeederAngle = -std::atan(x: Direction.y / Direction.x);
659 else if(Direction.x < 0.0000001f)
660 SpeederAngle = std::atan(x: Direction.y / Direction.x) + 2.0f * std::asin(x: 1.0f);
661 else if(Direction.y > 0.0000001f)
662 SpeederAngle = std::asin(x: 1.0f);
663 else
664 SpeederAngle = std::asin(x: -1.0f);
665
666 if(SpeederAngle < 0)
667 SpeederAngle = 4.0f * std::asin(x: 1.0f) + SpeederAngle;
668
669 if(TempVel.x > 0.0000001f)
670 TeeAngle = -std::atan(x: TempVel.y / TempVel.x);
671 else if(TempVel.x < 0.0000001f)
672 TeeAngle = std::atan(x: TempVel.y / TempVel.x) + 2.0f * std::asin(x: 1.0f);
673 else if(TempVel.y > 0.0000001f)
674 TeeAngle = std::asin(x: 1.0f);
675 else
676 TeeAngle = std::asin(x: -1.0f);
677
678 if(TeeAngle < 0)
679 TeeAngle = 4.0f * std::asin(x: 1.0f) + TeeAngle;
680
681 TeeSpeed = std::sqrt(x: std::pow(x: TempVel.x, y: 2) + std::pow(x: TempVel.y, y: 2));
682
683 DiffAngle = SpeederAngle - TeeAngle;
684 SpeedLeft = MaxSpeed / 5.0f - std::cos(x: DiffAngle) * TeeSpeed;
685 if(absolute(a: (int)SpeedLeft) > Force && SpeedLeft > 0.0000001f)
686 TempVel += Direction * Force;
687 else if(absolute(a: (int)SpeedLeft) > Force)
688 TempVel += Direction * -Force;
689 else
690 TempVel += Direction * SpeedLeft;
691 }
692 else
693 TempVel += Direction * Force;
694
695 m_Core.m_Vel = ClampVel(MoveRestriction: m_MoveRestrictions, Vel: TempVel);
696 }
697 }
698 else if(Type == TILE_SPEED_BOOST)
699 {
700 constexpr float MaxSpeedScale = 5.0f;
701 if(MaxSpeed == 0)
702 {
703 float MaxRampSpeed = GetTuning(i: GetOverriddenTuneZone())->m_VelrampRange / (50 * log(x: maximum(a: (float)GetTuning(i: GetOverriddenTuneZone())->m_VelrampCurvature, b: 1.01f)));
704 MaxSpeed = maximum(a: MaxRampSpeed, b: GetTuning(i: GetOverriddenTuneZone())->m_VelrampStart / 50) * MaxSpeedScale;
705 }
706
707 // (signed) length of projection
708 float CurrentDirectionalSpeed = dot(a: Direction, b: m_Core.m_Vel);
709 float TempMaxSpeed = MaxSpeed / MaxSpeedScale;
710 if(CurrentDirectionalSpeed + Force > TempMaxSpeed)
711 TempVel += Direction * (TempMaxSpeed - CurrentDirectionalSpeed);
712 else
713 TempVel += Direction * Force;
714 m_Core.m_Vel = ClampVel(MoveRestriction: m_MoveRestrictions, Vel: TempVel);
715 }
716 }
717}
718
719bool CCharacter::IsSwitchActiveCb(int Number, void *pUser)
720{
721 CCharacter *pThis = (CCharacter *)pUser;
722 auto &aSwitchers = pThis->Switchers();
723 return !aSwitchers.empty() && pThis->Team() != TEAM_SUPER && aSwitchers[Number].m_aStatus[pThis->Team()];
724}
725
726void CCharacter::HandleTiles(int Index)
727{
728 int MapIndex = Index;
729 m_TileIndex = Collision()->GetTileIndex(Index: MapIndex);
730 m_TileFIndex = Collision()->GetFrontTileIndex(Index: MapIndex);
731 m_MoveRestrictions = Collision()->GetMoveRestrictions(pfnSwitchActive: IsSwitchActiveCb, pUser: this, Pos: m_Pos, Distance: 18.0f, OverrideCenterTileIndex: MapIndex);
732
733 if(!GameWorld()->m_WorldConfig.m_PredictTiles)
734 return;
735
736 if(Index < 0)
737 {
738 m_LastRefillJumps = false;
739 return;
740 }
741
742 int TeleCheckpoint = Collision()->IsTeleCheckpoint(Index: MapIndex);
743 if(TeleCheckpoint)
744 m_TeleCheckpoint = TeleCheckpoint;
745
746 // freeze
747 if(((m_TileIndex == TILE_FREEZE) || (m_TileFIndex == TILE_FREEZE)) && !m_Core.m_Super && !m_Core.m_Invincible && !m_Core.m_DeepFrozen)
748 {
749 Freeze();
750 }
751 else if(((m_TileIndex == TILE_UNFREEZE) || (m_TileFIndex == TILE_UNFREEZE)) && !m_Core.m_DeepFrozen)
752 {
753 UnFreeze();
754 }
755
756 // deep freeze
757 if(((m_TileIndex == TILE_DFREEZE) || (m_TileFIndex == TILE_DFREEZE)) && !m_Core.m_Super && !m_Core.m_Invincible && !m_Core.m_DeepFrozen)
758 {
759 m_Core.m_DeepFrozen = true;
760 }
761 else if(((m_TileIndex == TILE_DUNFREEZE) || (m_TileFIndex == TILE_DUNFREEZE)) && !m_Core.m_Super && !m_Core.m_Invincible && m_Core.m_DeepFrozen)
762 {
763 m_Core.m_DeepFrozen = false;
764 }
765
766 // live freeze
767 if(((m_TileIndex == TILE_LFREEZE) || (m_TileFIndex == TILE_LFREEZE)) && !m_Core.m_Super && !m_Core.m_Invincible)
768 {
769 m_Core.m_LiveFrozen = true;
770 }
771 else if(((m_TileIndex == TILE_LUNFREEZE) || (m_TileFIndex == TILE_LUNFREEZE)) && !m_Core.m_Super && !m_Core.m_Invincible)
772 {
773 m_Core.m_LiveFrozen = false;
774 }
775
776 // endless hook
777 if(((m_TileIndex == TILE_EHOOK_ENABLE) || (m_TileFIndex == TILE_EHOOK_ENABLE)) && !m_Core.m_EndlessHook)
778 {
779 m_Core.m_EndlessHook = true;
780 }
781 else if(((m_TileIndex == TILE_EHOOK_DISABLE) || (m_TileFIndex == TILE_EHOOK_DISABLE)) && m_Core.m_EndlessHook)
782 {
783 m_Core.m_EndlessHook = false;
784 }
785
786 // hit others
787 if(((m_TileIndex == TILE_HIT_DISABLE) || (m_TileFIndex == TILE_HIT_DISABLE)) && (!m_Core.m_HammerHitDisabled || !m_Core.m_ShotgunHitDisabled || !m_Core.m_GrenadeHitDisabled || !m_Core.m_LaserHitDisabled))
788 {
789 m_Core.m_HammerHitDisabled = true;
790 m_Core.m_ShotgunHitDisabled = true;
791 m_Core.m_GrenadeHitDisabled = true;
792 m_Core.m_LaserHitDisabled = true;
793 }
794 else if(((m_TileIndex == TILE_HIT_ENABLE) || (m_TileFIndex == TILE_HIT_ENABLE)) && (m_Core.m_HammerHitDisabled || m_Core.m_ShotgunHitDisabled || m_Core.m_GrenadeHitDisabled || m_Core.m_LaserHitDisabled))
795 {
796 m_Core.m_ShotgunHitDisabled = false;
797 m_Core.m_GrenadeHitDisabled = false;
798 m_Core.m_HammerHitDisabled = false;
799 m_Core.m_LaserHitDisabled = false;
800 }
801
802 // collide with others
803 if(((m_TileIndex == TILE_NPC_DISABLE) || (m_TileFIndex == TILE_NPC_DISABLE)) && !m_Core.m_CollisionDisabled)
804 {
805 m_Core.m_CollisionDisabled = true;
806 }
807 else if(((m_TileIndex == TILE_NPC_ENABLE) || (m_TileFIndex == TILE_NPC_ENABLE)) && m_Core.m_CollisionDisabled)
808 {
809 m_Core.m_CollisionDisabled = false;
810 }
811
812 // hook others
813 if(((m_TileIndex == TILE_NPH_DISABLE) || (m_TileFIndex == TILE_NPH_DISABLE)) && !m_Core.m_HookHitDisabled)
814 {
815 m_Core.m_HookHitDisabled = true;
816 }
817 else if(((m_TileIndex == TILE_NPH_ENABLE) || (m_TileFIndex == TILE_NPH_ENABLE)) && m_Core.m_HookHitDisabled)
818 {
819 m_Core.m_HookHitDisabled = false;
820 }
821
822 // unlimited air jumps
823 if(((m_TileIndex == TILE_UNLIMITED_JUMPS_ENABLE) || (m_TileFIndex == TILE_UNLIMITED_JUMPS_ENABLE)) && !m_Core.m_EndlessJump)
824 {
825 m_Core.m_EndlessJump = true;
826 }
827 else if(((m_TileIndex == TILE_UNLIMITED_JUMPS_DISABLE) || (m_TileFIndex == TILE_UNLIMITED_JUMPS_DISABLE)) && m_Core.m_EndlessJump)
828 {
829 m_Core.m_EndlessJump = false;
830 }
831
832 // walljump
833 if((m_TileIndex == TILE_WALLJUMP) || (m_TileFIndex == TILE_WALLJUMP))
834 {
835 if(m_Core.m_Vel.y > 0 && m_Core.m_Colliding && m_Core.m_LeftWall)
836 {
837 m_Core.m_LeftWall = false;
838 m_Core.m_JumpedTotal = m_Core.m_Jumps >= 2 ? m_Core.m_Jumps - 2 : 0;
839 m_Core.m_Jumped = 1;
840 }
841 }
842
843 // jetpack gun
844 if(((m_TileIndex == TILE_JETPACK_ENABLE) || (m_TileFIndex == TILE_JETPACK_ENABLE)) && !m_Core.m_Jetpack)
845 {
846 m_Core.m_Jetpack = true;
847 }
848 else if(((m_TileIndex == TILE_JETPACK_DISABLE) || (m_TileFIndex == TILE_JETPACK_DISABLE)) && m_Core.m_Jetpack)
849 {
850 m_Core.m_Jetpack = false;
851 }
852
853 // solo part
854 if(((m_TileIndex == TILE_SOLO_ENABLE) || (m_TileFIndex == TILE_SOLO_ENABLE)) && !TeamsCore()->GetSolo(ClientId: GetCid()))
855 {
856 SetSolo(true);
857 }
858 else if(((m_TileIndex == TILE_SOLO_DISABLE) || (m_TileFIndex == TILE_SOLO_DISABLE)) && TeamsCore()->GetSolo(ClientId: GetCid()))
859 {
860 SetSolo(false);
861 }
862
863 // refill jumps
864 if(((m_TileIndex == TILE_REFILL_JUMPS) || (m_TileFIndex == TILE_REFILL_JUMPS)) && !m_LastRefillJumps)
865 {
866 m_Core.m_JumpedTotal = 0;
867 m_Core.m_Jumped = 0;
868 m_LastRefillJumps = true;
869 }
870 if((m_TileIndex != TILE_REFILL_JUMPS) && (m_TileFIndex != TILE_REFILL_JUMPS))
871 {
872 m_LastRefillJumps = false;
873 }
874
875 // Teleport gun
876 if(((m_TileIndex == TILE_TELE_GUN_ENABLE) || (m_TileFIndex == TILE_TELE_GUN_ENABLE)) && !m_Core.m_HasTelegunGun)
877 {
878 m_Core.m_HasTelegunGun = true;
879 }
880 else if(((m_TileIndex == TILE_TELE_GUN_DISABLE) || (m_TileFIndex == TILE_TELE_GUN_DISABLE)) && m_Core.m_HasTelegunGun)
881 {
882 m_Core.m_HasTelegunGun = false;
883 }
884
885 if(((m_TileIndex == TILE_TELE_GRENADE_ENABLE) || (m_TileFIndex == TILE_TELE_GRENADE_ENABLE)) && !m_Core.m_HasTelegunGrenade)
886 {
887 m_Core.m_HasTelegunGrenade = true;
888 }
889 else if(((m_TileIndex == TILE_TELE_GRENADE_DISABLE) || (m_TileFIndex == TILE_TELE_GRENADE_DISABLE)) && m_Core.m_HasTelegunGrenade)
890 {
891 m_Core.m_HasTelegunGrenade = false;
892 }
893
894 if(((m_TileIndex == TILE_TELE_LASER_ENABLE) || (m_TileFIndex == TILE_TELE_LASER_ENABLE)) && !m_Core.m_HasTelegunLaser)
895 {
896 m_Core.m_HasTelegunLaser = true;
897 }
898 else if(((m_TileIndex == TILE_TELE_LASER_DISABLE) || (m_TileFIndex == TILE_TELE_LASER_DISABLE)) && m_Core.m_HasTelegunLaser)
899 {
900 m_Core.m_HasTelegunLaser = false;
901 }
902
903 // stopper
904 if(m_Core.m_Vel.y > 0 && (m_MoveRestrictions & CANTMOVE_DOWN))
905 {
906 m_Core.m_Jumped = 0;
907 m_Core.m_JumpedTotal = 0;
908 }
909 m_Core.m_Vel = ClampVel(MoveRestriction: m_MoveRestrictions, Vel: m_Core.m_Vel);
910
911 // handle switch tiles
912 if(Collision()->GetSwitchType(Index: MapIndex) == TILE_SWITCHOPEN && Team() != TEAM_SUPER && Collision()->GetSwitchNumber(Index: MapIndex) > 0)
913 {
914 Switchers()[Collision()->GetSwitchNumber(Index: MapIndex)].m_aStatus[Team()] = true;
915 Switchers()[Collision()->GetSwitchNumber(Index: MapIndex)].m_aEndTick[Team()] = 0;
916 Switchers()[Collision()->GetSwitchNumber(Index: MapIndex)].m_aType[Team()] = TILE_SWITCHOPEN;
917 Switchers()[Collision()->GetSwitchNumber(Index: MapIndex)].m_aLastUpdateTick[Team()] = GameWorld()->GameTick();
918 }
919 else if(Collision()->GetSwitchType(Index: MapIndex) == TILE_SWITCHTIMEDOPEN && Team() != TEAM_SUPER && Collision()->GetSwitchNumber(Index: MapIndex) > 0)
920 {
921 Switchers()[Collision()->GetSwitchNumber(Index: MapIndex)].m_aStatus[Team()] = true;
922 Switchers()[Collision()->GetSwitchNumber(Index: MapIndex)].m_aEndTick[Team()] = GameWorld()->GameTick() + 1 + Collision()->GetSwitchDelay(Index: MapIndex) * GameWorld()->GameTickSpeed();
923 Switchers()[Collision()->GetSwitchNumber(Index: MapIndex)].m_aType[Team()] = TILE_SWITCHTIMEDOPEN;
924 Switchers()[Collision()->GetSwitchNumber(Index: MapIndex)].m_aLastUpdateTick[Team()] = GameWorld()->GameTick();
925 }
926 else if(Collision()->GetSwitchType(Index: MapIndex) == TILE_SWITCHTIMEDCLOSE && Team() != TEAM_SUPER && Collision()->GetSwitchNumber(Index: MapIndex) > 0)
927 {
928 Switchers()[Collision()->GetSwitchNumber(Index: MapIndex)].m_aStatus[Team()] = false;
929 Switchers()[Collision()->GetSwitchNumber(Index: MapIndex)].m_aEndTick[Team()] = GameWorld()->GameTick() + 1 + Collision()->GetSwitchDelay(Index: MapIndex) * GameWorld()->GameTickSpeed();
930 Switchers()[Collision()->GetSwitchNumber(Index: MapIndex)].m_aType[Team()] = TILE_SWITCHTIMEDCLOSE;
931 Switchers()[Collision()->GetSwitchNumber(Index: MapIndex)].m_aLastUpdateTick[Team()] = GameWorld()->GameTick();
932 }
933 else if(Collision()->GetSwitchType(Index: MapIndex) == TILE_SWITCHCLOSE && Team() != TEAM_SUPER && Collision()->GetSwitchNumber(Index: MapIndex) > 0)
934 {
935 Switchers()[Collision()->GetSwitchNumber(Index: MapIndex)].m_aStatus[Team()] = false;
936 Switchers()[Collision()->GetSwitchNumber(Index: MapIndex)].m_aEndTick[Team()] = 0;
937 Switchers()[Collision()->GetSwitchNumber(Index: MapIndex)].m_aType[Team()] = TILE_SWITCHCLOSE;
938 Switchers()[Collision()->GetSwitchNumber(Index: MapIndex)].m_aLastUpdateTick[Team()] = GameWorld()->GameTick();
939 }
940 else if(Collision()->GetSwitchType(Index: MapIndex) == TILE_FREEZE && Team() != TEAM_SUPER && !m_Core.m_Invincible)
941 {
942 if(Collision()->GetSwitchNumber(Index: MapIndex) == 0 || Switchers()[Collision()->GetSwitchNumber(Index: MapIndex)].m_aStatus[Team()])
943 {
944 Freeze(Seconds: Collision()->GetSwitchDelay(Index: MapIndex));
945 }
946 }
947 else if(Collision()->GetSwitchType(Index: MapIndex) == TILE_DFREEZE && Team() != TEAM_SUPER && !m_Core.m_Invincible)
948 {
949 if(Collision()->GetSwitchNumber(Index: MapIndex) == 0 || Switchers()[Collision()->GetSwitchNumber(Index: MapIndex)].m_aStatus[Team()])
950 m_Core.m_DeepFrozen = true;
951 }
952 else if(Collision()->GetSwitchType(Index: MapIndex) == TILE_DUNFREEZE && Team() != TEAM_SUPER && !m_Core.m_Invincible)
953 {
954 if(Collision()->GetSwitchNumber(Index: MapIndex) == 0 || Switchers()[Collision()->GetSwitchNumber(Index: MapIndex)].m_aStatus[Team()])
955 m_Core.m_DeepFrozen = false;
956 }
957 else if(Collision()->GetSwitchType(Index: MapIndex) == TILE_LFREEZE && Team() != TEAM_SUPER && !m_Core.m_Invincible)
958 {
959 if(Collision()->GetSwitchNumber(Index: MapIndex) == 0 || Switchers()[Collision()->GetSwitchNumber(Index: MapIndex)].m_aStatus[Team()])
960 {
961 m_Core.m_LiveFrozen = true;
962 }
963 }
964 else if(Collision()->GetSwitchType(Index: MapIndex) == TILE_LUNFREEZE && Team() != TEAM_SUPER && !m_Core.m_Invincible)
965 {
966 if(Collision()->GetSwitchNumber(Index: MapIndex) == 0 || Switchers()[Collision()->GetSwitchNumber(Index: MapIndex)].m_aStatus[Team()])
967 {
968 m_Core.m_LiveFrozen = false;
969 }
970 }
971 else if(Collision()->GetSwitchType(Index: MapIndex) == TILE_HIT_ENABLE && m_Core.m_HammerHitDisabled && Collision()->GetSwitchDelay(Index: MapIndex) == WEAPON_HAMMER)
972 {
973 m_Core.m_HammerHitDisabled = false;
974 }
975 else if(Collision()->GetSwitchType(Index: MapIndex) == TILE_HIT_DISABLE && !m_Core.m_HammerHitDisabled && Collision()->GetSwitchDelay(Index: MapIndex) == WEAPON_HAMMER)
976 {
977 m_Core.m_HammerHitDisabled = true;
978 }
979 else if(Collision()->GetSwitchType(Index: MapIndex) == TILE_HIT_ENABLE && m_Core.m_ShotgunHitDisabled && Collision()->GetSwitchDelay(Index: MapIndex) == WEAPON_SHOTGUN)
980 {
981 m_Core.m_ShotgunHitDisabled = false;
982 }
983 else if(Collision()->GetSwitchType(Index: MapIndex) == TILE_HIT_DISABLE && !m_Core.m_ShotgunHitDisabled && Collision()->GetSwitchDelay(Index: MapIndex) == WEAPON_SHOTGUN)
984 {
985 m_Core.m_ShotgunHitDisabled = true;
986 }
987 else if(Collision()->GetSwitchType(Index: MapIndex) == TILE_HIT_ENABLE && m_Core.m_GrenadeHitDisabled && Collision()->GetSwitchDelay(Index: MapIndex) == WEAPON_GRENADE)
988 {
989 m_Core.m_GrenadeHitDisabled = false;
990 }
991 else if(Collision()->GetSwitchType(Index: MapIndex) == TILE_HIT_DISABLE && !m_Core.m_GrenadeHitDisabled && Collision()->GetSwitchDelay(Index: MapIndex) == WEAPON_GRENADE)
992 {
993 m_Core.m_GrenadeHitDisabled = true;
994 }
995 else if(Collision()->GetSwitchType(Index: MapIndex) == TILE_HIT_ENABLE && m_Core.m_LaserHitDisabled && Collision()->GetSwitchDelay(Index: MapIndex) == WEAPON_LASER)
996 {
997 m_Core.m_LaserHitDisabled = false;
998 }
999 else if(Collision()->GetSwitchType(Index: MapIndex) == TILE_HIT_DISABLE && !m_Core.m_LaserHitDisabled && Collision()->GetSwitchDelay(Index: MapIndex) == WEAPON_LASER)
1000 {
1001 m_Core.m_LaserHitDisabled = true;
1002 }
1003 else if(Collision()->GetSwitchType(Index: MapIndex) == TILE_JUMP)
1004 {
1005 int NewJumps = Collision()->GetSwitchDelay(Index: MapIndex);
1006 if(NewJumps == 255)
1007 {
1008 NewJumps = -1;
1009 }
1010
1011 if(NewJumps != m_Core.m_Jumps)
1012 m_Core.m_Jumps = NewJumps;
1013 }
1014}
1015
1016void CCharacter::HandleTuneLayer()
1017{
1018 int CurrentIndex = Collision()->GetMapIndex(Pos: m_Pos);
1019 SetTuneZone(GameWorld()->m_WorldConfig.m_UseTuneZones ? Collision()->IsTune(Index: CurrentIndex) : 0);
1020 m_Core.m_Tuning = *GetTuning(i: GetOverriddenTuneZone());
1021}
1022
1023void CCharacter::DDRaceTick()
1024{
1025 mem_copy(dest: &m_Input, source: &m_SavedInput, size: sizeof(m_Input));
1026 if(m_Core.m_LiveFrozen && !m_CanMoveInFreeze && !m_Core.m_Super && !m_Core.m_Invincible)
1027 {
1028 m_Input.m_Direction = 0;
1029 m_Input.m_Jump = 0;
1030 //Hook and weapons are possible in live freeze
1031 }
1032 if(m_FreezeTime > 0)
1033 {
1034 m_FreezeTime--;
1035 if(!m_CanMoveInFreeze)
1036 {
1037 m_Input.m_Direction = 0;
1038 m_Input.m_Jump = 0;
1039 m_Input.m_Hook = 0;
1040 }
1041 if(m_FreezeTime == 1)
1042 UnFreeze();
1043 }
1044
1045 HandleTuneLayer();
1046
1047 // check if the tee is in any type of freeze
1048 int Index = Collision()->GetPureMapIndex(Pos: m_Pos);
1049 const int aTiles[] = {
1050 Collision()->GetTileIndex(Index),
1051 Collision()->GetFrontTileIndex(Index),
1052 Collision()->GetSwitchType(Index)};
1053 m_Core.m_IsInFreeze = false;
1054 for(const int Tile : aTiles)
1055 {
1056 if(Tile == TILE_FREEZE || Tile == TILE_DFREEZE || Tile == TILE_LFREEZE || Tile == TILE_DEATH)
1057 {
1058 m_Core.m_IsInFreeze = true;
1059 break;
1060 }
1061 }
1062 m_Core.m_IsInFreeze |= (Collision()->GetCollisionAt(x: m_Pos.x + GetProximityRadius() / 3.f, y: m_Pos.y - GetProximityRadius() / 3.f) == TILE_DEATH ||
1063 Collision()->GetCollisionAt(x: m_Pos.x + GetProximityRadius() / 3.f, y: m_Pos.y + GetProximityRadius() / 3.f) == TILE_DEATH ||
1064 Collision()->GetCollisionAt(x: m_Pos.x - GetProximityRadius() / 3.f, y: m_Pos.y - GetProximityRadius() / 3.f) == TILE_DEATH ||
1065 Collision()->GetCollisionAt(x: m_Pos.x - GetProximityRadius() / 3.f, y: m_Pos.y + GetProximityRadius() / 3.f) == TILE_DEATH ||
1066 Collision()->GetFrontCollisionAt(x: m_Pos.x + GetProximityRadius() / 3.f, y: m_Pos.y - GetProximityRadius() / 3.f) == TILE_DEATH ||
1067 Collision()->GetFrontCollisionAt(x: m_Pos.x + GetProximityRadius() / 3.f, y: m_Pos.y + GetProximityRadius() / 3.f) == TILE_DEATH ||
1068 Collision()->GetFrontCollisionAt(x: m_Pos.x - GetProximityRadius() / 3.f, y: m_Pos.y - GetProximityRadius() / 3.f) == TILE_DEATH ||
1069 Collision()->GetFrontCollisionAt(x: m_Pos.x - GetProximityRadius() / 3.f, y: m_Pos.y + GetProximityRadius() / 3.f) == TILE_DEATH);
1070}
1071
1072void CCharacter::DDRacePostCoreTick()
1073{
1074 if(!GameWorld()->m_WorldConfig.m_PredictDDRace)
1075 return;
1076
1077 if(m_Core.m_EndlessHook)
1078 m_Core.m_HookTick = 0;
1079
1080 m_FrozenLastTick = false;
1081
1082 if(m_Core.m_DeepFrozen && !m_Core.m_Super && !m_Core.m_Invincible)
1083 Freeze();
1084
1085 // following jump rules can be overridden by tiles, like Refill Jumps, Stopper and Wall Jump
1086 if(m_Core.m_Jumps == -1)
1087 {
1088 // The player has only one ground jump, so his feet are always dark
1089 m_Core.m_Jumped |= 2;
1090 }
1091 else if(m_Core.m_Jumps == 0)
1092 {
1093 // The player has no jumps at all, so his feet are always dark
1094 m_Core.m_Jumped |= 2;
1095 }
1096 else if(m_Core.m_Jumps == 1 && m_Core.m_Jumped > 0)
1097 {
1098 // If the player has only one jump, each jump is the last one
1099 m_Core.m_Jumped |= 2;
1100 }
1101 else if(m_Core.m_JumpedTotal < m_Core.m_Jumps - 1 && m_Core.m_Jumped > 1)
1102 {
1103 // The player has not yet used up all his jumps, so his feet remain light
1104 m_Core.m_Jumped = 1;
1105 }
1106
1107 if((m_Core.m_Super || m_Core.m_EndlessJump) && m_Core.m_Jumped > 1)
1108 {
1109 // Super players and players with infinite jumps always have light feet
1110 m_Core.m_Jumped = 1;
1111 }
1112
1113 int CurrentIndex = Collision()->GetMapIndex(Pos: m_Pos);
1114 HandleSkippableTiles(Index: CurrentIndex);
1115
1116 // handle Anti-Skip tiles
1117 std::vector<int> vIndices = Collision()->GetMapIndices(PrevPos: m_PrevPos, Pos: m_Pos);
1118 if(!vIndices.empty())
1119 for(int Index : vIndices)
1120 HandleTiles(Index);
1121 else
1122 {
1123 HandleTiles(Index: CurrentIndex);
1124 }
1125}
1126
1127bool CCharacter::Freeze(int Seconds)
1128{
1129 if(!GameWorld()->m_WorldConfig.m_PredictFreeze)
1130 return false;
1131 if(Seconds <= 0 || m_Core.m_Super || m_Core.m_Invincible || m_FreezeTime > Seconds * GameWorld()->GameTickSpeed())
1132 return false;
1133 if(m_Core.m_FreezeStart < GameWorld()->GameTick() - GameWorld()->GameTickSpeed())
1134 {
1135 m_FreezeTime = Seconds * GameWorld()->GameTickSpeed();
1136 m_Core.m_FreezeStart = GameWorld()->GameTick();
1137 m_Core.m_FreezeEnd = m_Core.m_DeepFrozen ? -1 : (m_FreezeTime == 0 ? 0 : GameWorld()->GameTick() + m_FreezeTime);
1138 return true;
1139 }
1140 return false;
1141}
1142
1143bool CCharacter::Freeze()
1144{
1145 return Freeze(Seconds: g_Config.m_SvFreezeDelay);
1146}
1147
1148bool CCharacter::UnFreeze()
1149{
1150 if(m_FreezeTime > 0)
1151 {
1152 if(!m_Core.m_aWeapons[m_Core.m_ActiveWeapon].m_Got)
1153 m_Core.m_ActiveWeapon = WEAPON_GUN;
1154 m_FreezeTime = 0;
1155 m_Core.m_FreezeStart = 0;
1156 m_Core.m_FreezeEnd = m_Core.m_DeepFrozen ? -1 : 0;
1157 if(GameWorld()->m_WorldConfig.m_PredictDDRace)
1158 m_FrozenLastTick = true;
1159 return true;
1160 }
1161 return false;
1162}
1163
1164void CCharacter::GiveWeapon(int Weapon, bool Remove)
1165{
1166 if(Weapon == WEAPON_NINJA)
1167 {
1168 if(Remove)
1169 RemoveNinja();
1170 else
1171 GiveNinja();
1172 return;
1173 }
1174
1175 if(Remove)
1176 {
1177 if(GetActiveWeapon() == Weapon)
1178 SetActiveWeapon(WEAPON_GUN);
1179 }
1180 else
1181 {
1182 m_Core.m_aWeapons[Weapon].m_Ammo = -1;
1183 }
1184
1185 m_Core.m_aWeapons[Weapon].m_Got = !Remove;
1186}
1187
1188void CCharacter::GiveAllWeapons()
1189{
1190 for(int i = WEAPON_GUN; i < NUM_WEAPONS - 1; i++)
1191 {
1192 GiveWeapon(Weapon: i);
1193 }
1194}
1195
1196void CCharacter::ResetVelocity()
1197{
1198 m_Core.m_Vel = vec2(0, 0);
1199}
1200
1201// The method is needed only to reproduce 'shotgun bug' ddnet#5258
1202// Use SetVelocity() instead.
1203void CCharacter::SetVelocity(const vec2 NewVelocity)
1204{
1205 m_Core.m_Vel = ClampVel(MoveRestriction: m_MoveRestrictions, Vel: NewVelocity);
1206}
1207
1208void CCharacter::SetRawVelocity(const vec2 NewVelocity)
1209{
1210 m_Core.m_Vel = NewVelocity;
1211}
1212
1213void CCharacter::AddVelocity(const vec2 Addition)
1214{
1215 SetVelocity(m_Core.m_Vel + Addition);
1216}
1217
1218void CCharacter::ApplyMoveRestrictions()
1219{
1220 m_Core.m_Vel = ClampVel(MoveRestriction: m_MoveRestrictions, Vel: m_Core.m_Vel);
1221}
1222
1223CTeamsCore *CCharacter::TeamsCore()
1224{
1225 return GameWorld()->Teams();
1226}
1227
1228CCharacter::CCharacter(CGameWorld *pGameWorld, int Id, CNetObj_Character *pChar, CNetObj_DDNetCharacter *pExtended) :
1229 CEntity(pGameWorld, CGameWorld::ENTTYPE_CHARACTER, vec2(0, 0), CCharacterCore::PhysicalSize())
1230{
1231 m_Id = Id;
1232 m_IsLocal = false;
1233
1234 m_LastWeapon = WEAPON_HAMMER;
1235 m_QueuedWeapon = -1;
1236 m_LastRefillJumps = false;
1237 m_PrevPrevPos = m_PrevPos = m_Pos = vec2(pChar->m_X, pChar->m_Y);
1238 m_Core.Reset();
1239 m_Core.Init(pWorld: &GameWorld()->m_Core, pCollision: GameWorld()->Collision(), pTeams: GameWorld()->Teams());
1240 m_Core.m_Id = Id;
1241 mem_zero(block: &m_Core.m_Ninja, size: sizeof(m_Core.m_Ninja));
1242 m_Core.m_LeftWall = true;
1243 m_ReloadTimer = 0;
1244 m_NumObjectsHit = 0;
1245 m_LastRefillJumps = false;
1246 m_CanMoveInFreeze = false;
1247 m_TeleCheckpoint = 0;
1248 m_StrongWeakId = 0;
1249
1250 mem_zero(block: &m_Input, size: sizeof(m_Input));
1251 // never initialize both to zero
1252 m_Input.m_TargetX = 0;
1253 m_Input.m_TargetY = -1;
1254
1255 m_LatestPrevInput = m_LatestInput = m_PrevInput = m_SavedInput = m_Input;
1256
1257 ResetPrediction();
1258 Read(pChar, pExtended, IsLocal: false);
1259}
1260
1261void CCharacter::ResetPrediction()
1262{
1263 SetSolo(false);
1264 SetSuper(false);
1265 m_Core.m_EndlessHook = false;
1266 m_Core.m_HammerHitDisabled = false;
1267 m_Core.m_ShotgunHitDisabled = false;
1268 m_Core.m_GrenadeHitDisabled = false;
1269 m_Core.m_LaserHitDisabled = false;
1270 m_Core.m_EndlessJump = false;
1271 m_Core.m_Jetpack = false;
1272 m_NinjaJetpack = false;
1273 m_Core.m_Jumps = 2;
1274 m_Core.m_HookHitDisabled = false;
1275 m_Core.m_CollisionDisabled = false;
1276 m_NumInputs = 0;
1277 m_FreezeTime = 0;
1278 m_Core.m_FreezeStart = 0;
1279 m_Core.m_IsInFreeze = false;
1280 m_Core.m_DeepFrozen = false;
1281 m_Core.m_LiveFrozen = false;
1282 m_FrozenLastTick = false;
1283 for(int w = 0; w < NUM_WEAPONS; w++)
1284 {
1285 SetWeaponGot(Type: w, Value: false);
1286 SetWeaponAmmo(Type: w, Value: -1);
1287 }
1288 if(m_Core.HookedPlayer() >= 0)
1289 {
1290 m_Core.SetHookedPlayer(-1);
1291 m_Core.m_HookState = HOOK_IDLE;
1292 }
1293 m_LastWeaponSwitchTick = 0;
1294 m_LastTuneZoneTick = 0;
1295}
1296
1297void CCharacter::Read(CNetObj_Character *pChar, CNetObj_DDNetCharacter *pExtended, bool IsLocal)
1298{
1299 m_Core.Read(pObjCore: (const CNetObj_CharacterCore *)pChar);
1300 m_IsLocal = IsLocal;
1301
1302 if(pExtended)
1303 {
1304 SetSolo(pExtended->m_Flags & CHARACTERFLAG_SOLO);
1305 SetSuper(pExtended->m_Flags & CHARACTERFLAG_SUPER);
1306
1307 m_TeleCheckpoint = pExtended->m_TeleCheckpoint;
1308 m_StrongWeakId = pExtended->m_StrongWeakId;
1309 m_TuneZoneOverride = pExtended->m_TuneZoneOverride;
1310
1311 const bool Ninja = (pExtended->m_Flags & CHARACTERFLAG_WEAPON_NINJA) != 0;
1312 if(Ninja && m_Core.m_ActiveWeapon != WEAPON_NINJA)
1313 GiveNinja();
1314 else if(!Ninja && m_Core.m_ActiveWeapon == WEAPON_NINJA)
1315 RemoveNinja();
1316
1317 if(GameWorld()->m_WorldConfig.m_PredictFreeze && pExtended->m_FreezeEnd != 0)
1318 {
1319 if(pExtended->m_FreezeEnd > 0)
1320 {
1321 if(m_FreezeTime == 0)
1322 Freeze();
1323 m_FreezeTime = maximum(a: 1, b: pExtended->m_FreezeEnd - GameWorld()->GameTick());
1324 }
1325 else if(pExtended->m_FreezeEnd == -1)
1326 m_Core.m_DeepFrozen = true;
1327 }
1328 else
1329 UnFreeze();
1330
1331 m_Core.ReadDDNet(pObjDDNet: pExtended);
1332
1333 if(!GameWorld()->m_WorldConfig.m_PredictFreeze)
1334 {
1335 UnFreeze();
1336 }
1337 }
1338 else
1339 {
1340 // ddnetcharacter is not available, try to get some info from the tunings and the character netobject instead.
1341
1342 // remove weapons that are unavailable. if the current weapon is ninja just set ammo to zero in case the player is frozen
1343 if(pChar->m_Weapon != m_Core.m_ActiveWeapon)
1344 {
1345 if(pChar->m_Weapon == WEAPON_NINJA)
1346 m_Core.m_aWeapons[m_Core.m_ActiveWeapon].m_Ammo = 0;
1347 else
1348 {
1349 if(m_Core.m_ActiveWeapon == WEAPON_NINJA)
1350 {
1351 SetNinjaActivationDir(vec2(0, 0));
1352 SetNinjaActivationTick(-500);
1353 SetNinjaCurrentMoveTime(0);
1354 }
1355 if(pChar->m_Weapon == m_LastSnapWeapon)
1356 m_Core.m_aWeapons[m_Core.m_ActiveWeapon].m_Got = false;
1357 }
1358 }
1359 // add weapon
1360 if(pChar->m_Weapon >= 0 && pChar->m_Weapon != WEAPON_NINJA)
1361 {
1362 m_Core.m_aWeapons[pChar->m_Weapon].m_Got = true;
1363 }
1364
1365 // without ddnetcharacter we don't know if we have jetpack, so try to predict jetpack if strength isn't 0, on vanilla it's always 0
1366 if(GameWorld()->m_WorldConfig.m_PredictWeapons && GetTuning(i: GetOverriddenTuneZone())->m_JetpackStrength != 0)
1367 {
1368 m_Core.m_Jetpack = true;
1369 m_Core.m_aWeapons[WEAPON_GUN].m_Got = true;
1370 m_Core.m_aWeapons[WEAPON_GUN].m_Ammo = -1;
1371 m_NinjaJetpack = pChar->m_Weapon == WEAPON_NINJA;
1372 }
1373 else if(pChar->m_Weapon != WEAPON_NINJA)
1374 {
1375 m_Core.m_Jetpack = false;
1376 }
1377
1378 // number of jumps
1379 if(GameWorld()->m_WorldConfig.m_PredictTiles)
1380 {
1381 if(pChar->m_Jumped & 2)
1382 {
1383 m_Core.m_EndlessJump = false;
1384 if(m_Core.m_Jumps > m_Core.m_JumpedTotal && m_Core.m_JumpedTotal > 0 && m_Core.m_Jumps > 2)
1385 m_Core.m_Jumps = m_Core.m_JumpedTotal + 1;
1386 }
1387 else if(m_Core.m_Jumps < 2)
1388 m_Core.m_Jumps = m_Core.m_JumpedTotal + 2;
1389 if(GetTuning(i: GetOverriddenTuneZone())->m_AirJumpImpulse == 0)
1390 {
1391 m_Core.m_Jumps = 0;
1392 m_Core.m_Jumped = 3;
1393 }
1394 }
1395
1396 // set player collision
1397 SetSolo(!GetTuning(i: GetOverriddenTuneZone())->m_PlayerCollision && !GetTuning(i: GetOverriddenTuneZone())->m_PlayerHooking);
1398 m_Core.m_CollisionDisabled = !GetTuning(i: GetOverriddenTuneZone())->m_PlayerCollision;
1399 m_Core.m_HookHitDisabled = !GetTuning(i: GetOverriddenTuneZone())->m_PlayerHooking;
1400
1401 if(m_Core.m_HookTick != 0)
1402 m_Core.m_EndlessHook = false;
1403
1404 // detect unfreeze (in case the player was frozen in the tile prediction and not correctly unfrozen)
1405 if(pChar->m_Emote != EMOTE_PAIN && pChar->m_Emote != EMOTE_NORMAL)
1406 m_Core.m_DeepFrozen = false;
1407 if(pChar->m_Weapon != WEAPON_NINJA || pChar->m_AttackTick > m_Core.m_FreezeStart || absolute(a: pChar->m_VelX) == 256 * 10 || !GameWorld()->m_WorldConfig.m_PredictFreeze)
1408 {
1409 m_Core.m_DeepFrozen = false;
1410 UnFreeze();
1411 }
1412
1413 m_TuneZoneOverride = TuneZone::OVERRIDE_NONE;
1414 }
1415
1416 vec2 PosBefore = m_Pos;
1417 m_Pos = m_Core.m_Pos;
1418
1419 if(distance(a: PosBefore, b: m_Pos) > 2.f) // misprediction, don't use prevpos
1420 m_PrevPos = m_Pos;
1421
1422 if(distance(a: m_PrevPos, b: m_Pos) > 10.f * 32.f) // reset prevpos if the distance is high
1423 m_PrevPos = m_Pos;
1424
1425 if(pChar->m_Jumped & 2)
1426 m_Core.m_JumpedTotal = m_Core.m_Jumps;
1427 m_AttackTick = pChar->m_AttackTick;
1428 m_LastSnapWeapon = pChar->m_Weapon;
1429
1430 SetTuneZone(GameWorld()->m_WorldConfig.m_UseTuneZones ? Collision()->IsTune(Index: Collision()->GetMapIndex(Pos: m_Pos)) : 0);
1431
1432 // set the current weapon
1433 if(pChar->m_Weapon >= 0 && pChar->m_Weapon != WEAPON_NINJA)
1434 {
1435 m_Core.m_aWeapons[pChar->m_Weapon].m_Ammo = (GameWorld()->m_WorldConfig.m_InfiniteAmmo || pChar->m_Weapon == WEAPON_HAMMER) ? -1 : pChar->m_AmmoCount;
1436 if(pChar->m_Weapon != m_Core.m_ActiveWeapon)
1437 SetActiveWeapon(pChar->m_Weapon);
1438 }
1439
1440 // reset all input except direction and hook for non-local players (as in vanilla prediction)
1441 if(!IsLocal)
1442 {
1443 mem_zero(block: &m_Input, size: sizeof(m_Input));
1444 mem_zero(block: &m_SavedInput, size: sizeof(m_SavedInput));
1445 m_Input.m_Direction = m_SavedInput.m_Direction = m_Core.m_Direction;
1446 m_Input.m_Hook = m_SavedInput.m_Hook = (m_Core.m_HookState != HOOK_IDLE);
1447
1448 if(pExtended && pExtended->m_TargetX != 0 && pExtended->m_TargetY != 0)
1449 {
1450 m_Input.m_TargetX = m_SavedInput.m_TargetX = pExtended->m_TargetX;
1451 m_Input.m_TargetY = m_SavedInput.m_TargetY = pExtended->m_TargetY;
1452 }
1453 else
1454 {
1455 m_Input.m_TargetX = m_SavedInput.m_TargetX = std::cos(x: pChar->m_Angle / 256.0f) * 256.0f;
1456 m_Input.m_TargetY = m_SavedInput.m_TargetY = std::sin(x: pChar->m_Angle / 256.0f) * 256.0f;
1457 }
1458 }
1459
1460 // in most cases the reload timer can be determined from the last attack tick
1461 // (this is only needed for autofire weapons to prevent the predicted reload timer from desyncing)
1462 if(IsLocal && m_Core.m_ActiveWeapon != WEAPON_HAMMER && !m_Core.m_aWeapons[WEAPON_NINJA].m_Got)
1463 {
1464 if(maximum(a: m_LastTuneZoneTick, b: m_LastWeaponSwitchTick) + GameWorld()->GameTickSpeed() < GameWorld()->GameTick())
1465 {
1466 float FireDelay;
1467 GetTuning(i: GetOverriddenTuneZone())->Get(offsetof(CTuningParams, m_HammerFireDelay) / sizeof(CTuneParam) + m_Core.m_ActiveWeapon, pValue: &FireDelay);
1468 const int FireDelayTicks = FireDelay * GameWorld()->GameTickSpeed() / 1000;
1469 m_ReloadTimer = maximum(a: 0, b: m_AttackTick + FireDelayTicks - GameWorld()->GameTick());
1470 }
1471 }
1472}
1473
1474void CCharacter::SetCoreWorld(CGameWorld *pGameWorld)
1475{
1476 m_Core.SetCoreWorld(pWorld: &pGameWorld->m_Core, pCollision: pGameWorld->Collision(), pTeams: pGameWorld->Teams());
1477}
1478
1479bool CCharacter::Match(CCharacter *pChar) const
1480{
1481 return distance(a: pChar->m_Core.m_Pos, b: m_Core.m_Pos) <= 32.f;
1482}
1483
1484void CCharacter::SetActiveWeapon(int ActiveWeapon)
1485{
1486 if(ActiveWeapon < WEAPON_HAMMER || ActiveWeapon >= NUM_WEAPONS)
1487 {
1488 m_Core.m_ActiveWeapon = WEAPON_HAMMER;
1489 }
1490 else
1491 {
1492 m_Core.m_ActiveWeapon = ActiveWeapon;
1493 }
1494 m_LastWeaponSwitchTick = GameWorld()->GameTick();
1495}
1496
1497void CCharacter::SetTuneZone(int Zone)
1498{
1499 if(Zone == m_TuneZone)
1500 return;
1501 m_TuneZone = Zone;
1502 m_LastTuneZoneTick = GameWorld()->GameTick();
1503}
1504
1505int CCharacter::GetOverriddenTuneZone() const
1506{
1507 return m_TuneZoneOverride == TuneZone::OVERRIDE_NONE ? m_TuneZone : m_TuneZoneOverride;
1508}
1509
1510int CCharacter::GetPureTuneZone() const
1511{
1512 return m_TuneZone;
1513}
1514
1515CCharacter::~CCharacter()
1516{
1517 if(GameWorld())
1518 GameWorld()->RemoveCharacter(pChar: this);
1519}
1520