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