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 // -1 is no weapon, handled here so pain sound still plays when firing in freeze
489 if(!m_ReloadTimer && m_Core.m_ActiveWeapon != -1)
490 {
491 m_ReloadTimer = GetTuning(i: GetOverriddenTuneZone())->GetWeaponFireDelay(Weapon: m_Core.m_ActiveWeapon) * GameWorld()->GameTickSpeed();
492 }
493}
494
495void CCharacter::HandleWeapons()
496{
497 //ninja
498 HandleNinja();
499 HandleJetpack();
500
501 if(m_PainSoundTimer > 0)
502 m_PainSoundTimer--;
503
504 // check reload timer
505 if(m_ReloadTimer)
506 {
507 m_ReloadTimer--;
508 return;
509 }
510
511 // fire Weapon, if wanted
512 FireWeapon();
513}
514
515void CCharacter::GiveNinja()
516{
517 m_Core.m_Ninja.m_ActivationTick = GameWorld()->GameTick();
518 m_Core.m_aWeapons[WEAPON_NINJA].m_Got = true;
519 if(m_FreezeTime == 0)
520 m_Core.m_aWeapons[WEAPON_NINJA].m_Ammo = -1;
521 if(m_Core.m_ActiveWeapon != WEAPON_NINJA)
522 m_LastWeapon = m_Core.m_ActiveWeapon;
523 SetActiveWeapon(WEAPON_NINJA);
524
525 if(GameWorld()->m_WorldConfig.m_IsVanilla)
526 GameWorld()->CreatePredictedSound(Pos: m_Pos, SoundId: SOUND_PICKUP_NINJA, Id: GetCid());
527}
528
529void CCharacter::OnPredictedInput(const CNetObj_PlayerInput *pNewInput)
530{
531 // skip the input if chat is active
532 if(!GameWorld()->m_WorldConfig.m_BugDDRaceInput && pNewInput->m_PlayerFlags & PLAYERFLAG_CHATTING)
533 {
534 // save the reset input
535 mem_copy(dest: &m_SavedInput, source: &m_Input, size: sizeof(m_SavedInput));
536 return;
537 }
538
539 // copy new input
540 mem_copy(dest: &m_Input, source: pNewInput, size: sizeof(m_Input));
541
542 // it is not allowed to aim in the center
543 if(m_Input.m_TargetX == 0 && m_Input.m_TargetY == 0)
544 m_Input.m_TargetY = -1;
545
546 mem_copy(dest: &m_SavedInput, source: &m_Input, size: sizeof(m_SavedInput));
547}
548
549void CCharacter::OnDirectInput(const CNetObj_PlayerInput *pNewInput)
550{
551 // skip the input if chat is active
552 if(!GameWorld()->m_WorldConfig.m_BugDDRaceInput && pNewInput->m_PlayerFlags & PLAYERFLAG_CHATTING)
553 {
554 // reset input
555 ResetInput();
556 // mods that do not allow inputs to be held while chatting also do not allow to hold hook
557 m_Input.m_Hook = 0;
558 return;
559 }
560
561 m_NumInputs++;
562 mem_copy(dest: &m_LatestPrevInput, source: &m_LatestInput, size: sizeof(m_LatestInput));
563 mem_copy(dest: &m_LatestInput, source: pNewInput, size: sizeof(m_LatestInput));
564
565 // it is not allowed to aim in the center
566 if(m_LatestInput.m_TargetX == 0 && m_LatestInput.m_TargetY == 0)
567 m_LatestInput.m_TargetY = -1;
568
569 if(m_NumInputs > 1 && Team() != TEAM_SPECTATORS)
570 {
571 HandleWeaponSwitch();
572 FireWeapon();
573 }
574
575 mem_copy(dest: &m_LatestPrevInput, source: &m_LatestInput, size: sizeof(m_LatestInput));
576}
577
578void CCharacter::ReleaseHook()
579{
580 m_Core.SetHookedPlayer(-1);
581 m_Core.m_HookState = HOOK_RETRACTED;
582 m_Core.m_TriggeredEvents |= COREEVENT_HOOK_RETRACT;
583}
584
585void CCharacter::ResetHook()
586{
587 ReleaseHook();
588 m_Core.m_HookPos = m_Core.m_Pos;
589}
590
591void CCharacter::ResetInput()
592{
593 m_Input.m_Direction = 0;
594 // m_Input.m_Hook = 0;
595 // simulate releasing the fire button
596 if((m_Input.m_Fire & 1) != 0)
597 m_Input.m_Fire++;
598 m_Input.m_Fire &= INPUT_STATE_MASK;
599 m_Input.m_Jump = 0;
600 m_LatestPrevInput = m_LatestInput = m_Input;
601}
602
603void CCharacter::PreTick()
604{
605 DDRaceTick();
606
607 m_Core.m_Input = m_Input;
608 m_Core.Tick(UseInput: true, DoDeferredTick: !m_pGameWorld->m_WorldConfig.m_NoWeakHookAndBounce);
609}
610
611void CCharacter::Tick()
612{
613 if(m_pGameWorld->m_WorldConfig.m_NoWeakHookAndBounce)
614 {
615 m_Core.TickDeferred();
616 }
617 else
618 {
619 PreTick();
620 }
621
622 // handle Weapons
623 HandleWeapons();
624
625 DDRacePostCoreTick();
626
627 // Previnput
628 m_PrevInput = m_Input;
629
630 m_PrevPrevPos = m_PrevPos;
631 m_PrevPos = m_Core.m_Pos;
632}
633
634void CCharacter::TickDeferred()
635{
636 m_Core.Move();
637 m_Core.Quantize();
638 m_Pos = m_Core.m_Pos;
639}
640
641bool CCharacter::TakeDamage(vec2 Force, int Dmg, int From, int Weapon)
642{
643 vec2 Temp = m_Core.m_Vel + Force;
644 m_Core.m_Vel = ClampVel(MoveRestriction: m_MoveRestrictions, Vel: Temp);
645 return true;
646}
647
648// DDRace
649
650bool CCharacter::CanCollide(int ClientId)
651{
652 return TeamsCore()->CanCollide(ClientId1: GetCid(), ClientId2: ClientId);
653}
654
655bool CCharacter::SameTeam(int ClientId)
656{
657 return TeamsCore()->SameTeam(ClientId1: GetCid(), ClientId2: ClientId);
658}
659
660int CCharacter::Team()
661{
662 return TeamsCore()->Team(ClientId: GetCid());
663}
664
665void CCharacter::HandleSkippableTiles(int Index)
666{
667 if(Index < 0)
668 return;
669
670 // handle speedup tiles
671 if(Collision()->IsSpeedup(Index))
672 {
673 vec2 Direction, TempVel = m_Core.m_Vel;
674 int Force, Type, MaxSpeed = 0;
675 Collision()->GetSpeedup(Index, pDir: &Direction, pForce: &Force, pMaxSpeed: &MaxSpeed, pType: &Type);
676
677 if(Type == TILE_SPEED_BOOST_OLD)
678 {
679 float TeeAngle, SpeederAngle, DiffAngle, SpeedLeft, TeeSpeed;
680 if(Force == 255 && MaxSpeed)
681 {
682 m_Core.m_Vel = Direction * (MaxSpeed / 5);
683 }
684 else
685 {
686 if(MaxSpeed > 0 && MaxSpeed < 5)
687 MaxSpeed = 5;
688 if(MaxSpeed > 0)
689 {
690 if(Direction.x > 0.0000001f)
691 SpeederAngle = -std::atan(x: Direction.y / Direction.x);
692 else if(Direction.x < 0.0000001f)
693 SpeederAngle = std::atan(x: Direction.y / Direction.x) + 2.0f * std::asin(x: 1.0f);
694 else if(Direction.y > 0.0000001f)
695 SpeederAngle = std::asin(x: 1.0f);
696 else
697 SpeederAngle = std::asin(x: -1.0f);
698
699 if(SpeederAngle < 0)
700 SpeederAngle = 4.0f * std::asin(x: 1.0f) + SpeederAngle;
701
702 if(TempVel.x > 0.0000001f)
703 TeeAngle = -std::atan(x: TempVel.y / TempVel.x);
704 else if(TempVel.x < 0.0000001f)
705 TeeAngle = std::atan(x: TempVel.y / TempVel.x) + 2.0f * std::asin(x: 1.0f);
706 else if(TempVel.y > 0.0000001f)
707 TeeAngle = std::asin(x: 1.0f);
708 else
709 TeeAngle = std::asin(x: -1.0f);
710
711 if(TeeAngle < 0)
712 TeeAngle = 4.0f * std::asin(x: 1.0f) + TeeAngle;
713
714 TeeSpeed = std::sqrt(x: std::pow(x: TempVel.x, y: 2) + std::pow(x: TempVel.y, y: 2));
715
716 DiffAngle = SpeederAngle - TeeAngle;
717 SpeedLeft = MaxSpeed / 5.0f - std::cos(x: DiffAngle) * TeeSpeed;
718 if(absolute(a: (int)SpeedLeft) > Force && SpeedLeft > 0.0000001f)
719 TempVel += Direction * Force;
720 else if(absolute(a: (int)SpeedLeft) > Force)
721 TempVel += Direction * -Force;
722 else
723 TempVel += Direction * SpeedLeft;
724 }
725 else
726 TempVel += Direction * Force;
727
728 m_Core.m_Vel = ClampVel(MoveRestriction: m_MoveRestrictions, Vel: TempVel);
729 }
730 }
731 else if(Type == TILE_SPEED_BOOST)
732 {
733 constexpr float MaxSpeedScale = 5.0f;
734 if(MaxSpeed == 0)
735 {
736 float MaxRampSpeed = GetTuning(i: GetOverriddenTuneZone())->m_VelrampRange / (50 * log(x: maximum(a: (float)GetTuning(i: GetOverriddenTuneZone())->m_VelrampCurvature, b: 1.01f)));
737 MaxSpeed = maximum(a: MaxRampSpeed, b: GetTuning(i: GetOverriddenTuneZone())->m_VelrampStart / 50) * MaxSpeedScale;
738 }
739
740 // (signed) length of projection
741 float CurrentDirectionalSpeed = dot(a: Direction, b: m_Core.m_Vel);
742 float TempMaxSpeed = MaxSpeed / MaxSpeedScale;
743 if(CurrentDirectionalSpeed + Force > TempMaxSpeed)
744 TempVel += Direction * (TempMaxSpeed - CurrentDirectionalSpeed);
745 else
746 TempVel += Direction * Force;
747 m_Core.m_Vel = ClampVel(MoveRestriction: m_MoveRestrictions, Vel: TempVel);
748 }
749 }
750}
751
752bool CCharacter::IsSwitchActiveCb(int Number, void *pUser)
753{
754 CCharacter *pThis = (CCharacter *)pUser;
755 auto &aSwitchers = pThis->Switchers();
756 return !aSwitchers.empty() && pThis->Team() != TEAM_SUPER && aSwitchers[Number].m_aStatus[pThis->Team()];
757}
758
759void CCharacter::HandleTiles(int Index)
760{
761 int MapIndex = Index;
762 m_TileIndex = Collision()->GetTileIndex(Index: MapIndex);
763 m_TileFIndex = Collision()->GetFrontTileIndex(Index: MapIndex);
764 m_MoveRestrictions = Collision()->GetMoveRestrictions(pfnSwitchActive: IsSwitchActiveCb, pUser: this, Pos: m_Pos, Distance: 18.0f, OverrideCenterTileIndex: MapIndex);
765
766 if(!GameWorld()->m_WorldConfig.m_PredictTiles)
767 return;
768
769 if(Index < 0)
770 {
771 m_LastRefillJumps = false;
772 return;
773 }
774
775 int TeleCheckpoint = Collision()->IsTeleCheckpoint(Index: MapIndex);
776 if(TeleCheckpoint)
777 m_TeleCheckpoint = TeleCheckpoint;
778
779 // freeze
780 if(((m_TileIndex == TILE_FREEZE) || (m_TileFIndex == TILE_FREEZE)) && !m_Core.m_Super && !m_Core.m_Invincible && !m_Core.m_DeepFrozen)
781 {
782 Freeze();
783 }
784 else if(((m_TileIndex == TILE_UNFREEZE) || (m_TileFIndex == TILE_UNFREEZE)) && !m_Core.m_DeepFrozen)
785 {
786 Unfreeze();
787 }
788
789 // deep freeze
790 if(((m_TileIndex == TILE_DFREEZE) || (m_TileFIndex == TILE_DFREEZE)) && !m_Core.m_Super && !m_Core.m_Invincible && !m_Core.m_DeepFrozen)
791 {
792 m_Core.m_DeepFrozen = true;
793 }
794 else if(((m_TileIndex == TILE_DUNFREEZE) || (m_TileFIndex == TILE_DUNFREEZE)) && !m_Core.m_Super && !m_Core.m_Invincible && m_Core.m_DeepFrozen)
795 {
796 m_Core.m_DeepFrozen = false;
797 }
798
799 // live freeze
800 if(((m_TileIndex == TILE_LFREEZE) || (m_TileFIndex == TILE_LFREEZE)) && !m_Core.m_Super && !m_Core.m_Invincible)
801 {
802 m_Core.m_LiveFrozen = true;
803 }
804 else if(((m_TileIndex == TILE_LUNFREEZE) || (m_TileFIndex == TILE_LUNFREEZE)) && !m_Core.m_Super && !m_Core.m_Invincible)
805 {
806 m_Core.m_LiveFrozen = false;
807 }
808
809 // endless hook
810 if(((m_TileIndex == TILE_EHOOK_ENABLE) || (m_TileFIndex == TILE_EHOOK_ENABLE)) && !m_Core.m_EndlessHook)
811 {
812 m_Core.m_EndlessHook = true;
813 }
814 else if(((m_TileIndex == TILE_EHOOK_DISABLE) || (m_TileFIndex == TILE_EHOOK_DISABLE)) && m_Core.m_EndlessHook)
815 {
816 m_Core.m_EndlessHook = false;
817 }
818
819 // hit others
820 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))
821 {
822 m_Core.m_HammerHitDisabled = true;
823 m_Core.m_ShotgunHitDisabled = true;
824 m_Core.m_GrenadeHitDisabled = true;
825 m_Core.m_LaserHitDisabled = true;
826 }
827 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))
828 {
829 m_Core.m_ShotgunHitDisabled = false;
830 m_Core.m_GrenadeHitDisabled = false;
831 m_Core.m_HammerHitDisabled = false;
832 m_Core.m_LaserHitDisabled = false;
833 }
834
835 // collide with others
836 if(((m_TileIndex == TILE_NPC_DISABLE) || (m_TileFIndex == TILE_NPC_DISABLE)) && !m_Core.m_CollisionDisabled)
837 {
838 m_Core.m_CollisionDisabled = true;
839 }
840 else if(((m_TileIndex == TILE_NPC_ENABLE) || (m_TileFIndex == TILE_NPC_ENABLE)) && m_Core.m_CollisionDisabled)
841 {
842 m_Core.m_CollisionDisabled = false;
843 }
844
845 // hook others
846 if(((m_TileIndex == TILE_NPH_DISABLE) || (m_TileFIndex == TILE_NPH_DISABLE)) && !m_Core.m_HookHitDisabled)
847 {
848 m_Core.m_HookHitDisabled = true;
849 }
850 else if(((m_TileIndex == TILE_NPH_ENABLE) || (m_TileFIndex == TILE_NPH_ENABLE)) && m_Core.m_HookHitDisabled)
851 {
852 m_Core.m_HookHitDisabled = false;
853 }
854
855 // unlimited air jumps
856 if(((m_TileIndex == TILE_UNLIMITED_JUMPS_ENABLE) || (m_TileFIndex == TILE_UNLIMITED_JUMPS_ENABLE)) && !m_Core.m_EndlessJump)
857 {
858 m_Core.m_EndlessJump = true;
859 }
860 else if(((m_TileIndex == TILE_UNLIMITED_JUMPS_DISABLE) || (m_TileFIndex == TILE_UNLIMITED_JUMPS_DISABLE)) && m_Core.m_EndlessJump)
861 {
862 m_Core.m_EndlessJump = false;
863 }
864
865 // walljump
866 if((m_TileIndex == TILE_WALLJUMP) || (m_TileFIndex == TILE_WALLJUMP))
867 {
868 if(m_Core.m_Vel.y > 0 && m_Core.m_Colliding && m_Core.m_LeftWall)
869 {
870 m_Core.m_LeftWall = false;
871 m_Core.m_JumpedTotal = m_Core.m_Jumps >= 2 ? m_Core.m_Jumps - 2 : 0;
872 m_Core.m_Jumped = 1;
873 }
874 }
875
876 // jetpack gun
877 if(((m_TileIndex == TILE_JETPACK_ENABLE) || (m_TileFIndex == TILE_JETPACK_ENABLE)) && !m_Core.m_Jetpack)
878 {
879 m_Core.m_Jetpack = true;
880 }
881 else if(((m_TileIndex == TILE_JETPACK_DISABLE) || (m_TileFIndex == TILE_JETPACK_DISABLE)) && m_Core.m_Jetpack)
882 {
883 m_Core.m_Jetpack = false;
884 }
885
886 // solo part
887 if(((m_TileIndex == TILE_SOLO_ENABLE) || (m_TileFIndex == TILE_SOLO_ENABLE)) && !TeamsCore()->GetSolo(ClientId: GetCid()))
888 {
889 SetSolo(true);
890 }
891 else if(((m_TileIndex == TILE_SOLO_DISABLE) || (m_TileFIndex == TILE_SOLO_DISABLE)) && TeamsCore()->GetSolo(ClientId: GetCid()))
892 {
893 SetSolo(false);
894 }
895
896 // refill jumps
897 if(((m_TileIndex == TILE_REFILL_JUMPS) || (m_TileFIndex == TILE_REFILL_JUMPS)) && !m_LastRefillJumps)
898 {
899 m_Core.m_JumpedTotal = 0;
900 m_Core.m_Jumped = 0;
901 m_LastRefillJumps = true;
902 }
903 if((m_TileIndex != TILE_REFILL_JUMPS) && (m_TileFIndex != TILE_REFILL_JUMPS))
904 {
905 m_LastRefillJumps = false;
906 }
907
908 // Teleport gun
909 if(((m_TileIndex == TILE_TELE_GUN_ENABLE) || (m_TileFIndex == TILE_TELE_GUN_ENABLE)) && !m_Core.m_HasTelegunGun)
910 {
911 m_Core.m_HasTelegunGun = true;
912 }
913 else if(((m_TileIndex == TILE_TELE_GUN_DISABLE) || (m_TileFIndex == TILE_TELE_GUN_DISABLE)) && m_Core.m_HasTelegunGun)
914 {
915 m_Core.m_HasTelegunGun = false;
916 }
917
918 if(((m_TileIndex == TILE_TELE_GRENADE_ENABLE) || (m_TileFIndex == TILE_TELE_GRENADE_ENABLE)) && !m_Core.m_HasTelegunGrenade)
919 {
920 m_Core.m_HasTelegunGrenade = true;
921 }
922 else if(((m_TileIndex == TILE_TELE_GRENADE_DISABLE) || (m_TileFIndex == TILE_TELE_GRENADE_DISABLE)) && m_Core.m_HasTelegunGrenade)
923 {
924 m_Core.m_HasTelegunGrenade = false;
925 }
926
927 if(((m_TileIndex == TILE_TELE_LASER_ENABLE) || (m_TileFIndex == TILE_TELE_LASER_ENABLE)) && !m_Core.m_HasTelegunLaser)
928 {
929 m_Core.m_HasTelegunLaser = true;
930 }
931 else if(((m_TileIndex == TILE_TELE_LASER_DISABLE) || (m_TileFIndex == TILE_TELE_LASER_DISABLE)) && m_Core.m_HasTelegunLaser)
932 {
933 m_Core.m_HasTelegunLaser = false;
934 }
935
936 // stopper
937 if(m_Core.m_Vel.y > 0 && (m_MoveRestrictions & CANTMOVE_DOWN))
938 {
939 m_Core.m_Jumped = 0;
940 m_Core.m_JumpedTotal = 0;
941 }
942 m_Core.m_Vel = ClampVel(MoveRestriction: m_MoveRestrictions, Vel: m_Core.m_Vel);
943
944 // handle switch tiles
945 if(Collision()->GetSwitchType(Index: MapIndex) == TILE_SWITCHOPEN && Team() != TEAM_SUPER && Collision()->GetSwitchNumber(Index: MapIndex) > 0)
946 {
947 Switchers()[Collision()->GetSwitchNumber(Index: MapIndex)].m_aStatus[Team()] = true;
948 Switchers()[Collision()->GetSwitchNumber(Index: MapIndex)].m_aEndTick[Team()] = 0;
949 Switchers()[Collision()->GetSwitchNumber(Index: MapIndex)].m_aType[Team()] = TILE_SWITCHOPEN;
950 Switchers()[Collision()->GetSwitchNumber(Index: MapIndex)].m_aLastUpdateTick[Team()] = GameWorld()->GameTick();
951 }
952 else if(Collision()->GetSwitchType(Index: MapIndex) == TILE_SWITCHTIMEDOPEN && Team() != TEAM_SUPER && Collision()->GetSwitchNumber(Index: MapIndex) > 0)
953 {
954 Switchers()[Collision()->GetSwitchNumber(Index: MapIndex)].m_aStatus[Team()] = true;
955 Switchers()[Collision()->GetSwitchNumber(Index: MapIndex)].m_aEndTick[Team()] = GameWorld()->GameTick() + 1 + Collision()->GetSwitchDelay(Index: MapIndex) * GameWorld()->GameTickSpeed();
956 Switchers()[Collision()->GetSwitchNumber(Index: MapIndex)].m_aType[Team()] = TILE_SWITCHTIMEDOPEN;
957 Switchers()[Collision()->GetSwitchNumber(Index: MapIndex)].m_aLastUpdateTick[Team()] = GameWorld()->GameTick();
958 }
959 else if(Collision()->GetSwitchType(Index: MapIndex) == TILE_SWITCHTIMEDCLOSE && Team() != TEAM_SUPER && Collision()->GetSwitchNumber(Index: MapIndex) > 0)
960 {
961 Switchers()[Collision()->GetSwitchNumber(Index: MapIndex)].m_aStatus[Team()] = false;
962 Switchers()[Collision()->GetSwitchNumber(Index: MapIndex)].m_aEndTick[Team()] = GameWorld()->GameTick() + 1 + Collision()->GetSwitchDelay(Index: MapIndex) * GameWorld()->GameTickSpeed();
963 Switchers()[Collision()->GetSwitchNumber(Index: MapIndex)].m_aType[Team()] = TILE_SWITCHTIMEDCLOSE;
964 Switchers()[Collision()->GetSwitchNumber(Index: MapIndex)].m_aLastUpdateTick[Team()] = GameWorld()->GameTick();
965 }
966 else if(Collision()->GetSwitchType(Index: MapIndex) == TILE_SWITCHCLOSE && Team() != TEAM_SUPER && Collision()->GetSwitchNumber(Index: MapIndex) > 0)
967 {
968 Switchers()[Collision()->GetSwitchNumber(Index: MapIndex)].m_aStatus[Team()] = false;
969 Switchers()[Collision()->GetSwitchNumber(Index: MapIndex)].m_aEndTick[Team()] = 0;
970 Switchers()[Collision()->GetSwitchNumber(Index: MapIndex)].m_aType[Team()] = TILE_SWITCHCLOSE;
971 Switchers()[Collision()->GetSwitchNumber(Index: MapIndex)].m_aLastUpdateTick[Team()] = GameWorld()->GameTick();
972 }
973 else if(Collision()->GetSwitchType(Index: MapIndex) == TILE_FREEZE && Team() != TEAM_SUPER && !m_Core.m_Invincible)
974 {
975 if(Collision()->GetSwitchNumber(Index: MapIndex) == 0 || Switchers()[Collision()->GetSwitchNumber(Index: MapIndex)].m_aStatus[Team()])
976 {
977 Freeze(Seconds: Collision()->GetSwitchDelay(Index: MapIndex));
978 }
979 }
980 else if(Collision()->GetSwitchType(Index: MapIndex) == TILE_DFREEZE && Team() != TEAM_SUPER && !m_Core.m_Invincible)
981 {
982 if(Collision()->GetSwitchNumber(Index: MapIndex) == 0 || Switchers()[Collision()->GetSwitchNumber(Index: MapIndex)].m_aStatus[Team()])
983 m_Core.m_DeepFrozen = true;
984 }
985 else if(Collision()->GetSwitchType(Index: MapIndex) == TILE_DUNFREEZE && Team() != TEAM_SUPER && !m_Core.m_Invincible)
986 {
987 if(Collision()->GetSwitchNumber(Index: MapIndex) == 0 || Switchers()[Collision()->GetSwitchNumber(Index: MapIndex)].m_aStatus[Team()])
988 m_Core.m_DeepFrozen = false;
989 }
990 else if(Collision()->GetSwitchType(Index: MapIndex) == TILE_LFREEZE && Team() != TEAM_SUPER && !m_Core.m_Invincible)
991 {
992 if(Collision()->GetSwitchNumber(Index: MapIndex) == 0 || Switchers()[Collision()->GetSwitchNumber(Index: MapIndex)].m_aStatus[Team()])
993 {
994 m_Core.m_LiveFrozen = true;
995 }
996 }
997 else if(Collision()->GetSwitchType(Index: MapIndex) == TILE_LUNFREEZE && Team() != TEAM_SUPER && !m_Core.m_Invincible)
998 {
999 if(Collision()->GetSwitchNumber(Index: MapIndex) == 0 || Switchers()[Collision()->GetSwitchNumber(Index: MapIndex)].m_aStatus[Team()])
1000 {
1001 m_Core.m_LiveFrozen = false;
1002 }
1003 }
1004 else if(Collision()->GetSwitchType(Index: MapIndex) == TILE_HIT_ENABLE && m_Core.m_HammerHitDisabled && Collision()->GetSwitchDelay(Index: MapIndex) == WEAPON_HAMMER)
1005 {
1006 m_Core.m_HammerHitDisabled = false;
1007 }
1008 else if(Collision()->GetSwitchType(Index: MapIndex) == TILE_HIT_DISABLE && !m_Core.m_HammerHitDisabled && Collision()->GetSwitchDelay(Index: MapIndex) == WEAPON_HAMMER)
1009 {
1010 m_Core.m_HammerHitDisabled = true;
1011 }
1012 else if(Collision()->GetSwitchType(Index: MapIndex) == TILE_HIT_ENABLE && m_Core.m_ShotgunHitDisabled && Collision()->GetSwitchDelay(Index: MapIndex) == WEAPON_SHOTGUN)
1013 {
1014 m_Core.m_ShotgunHitDisabled = false;
1015 }
1016 else if(Collision()->GetSwitchType(Index: MapIndex) == TILE_HIT_DISABLE && !m_Core.m_ShotgunHitDisabled && Collision()->GetSwitchDelay(Index: MapIndex) == WEAPON_SHOTGUN)
1017 {
1018 m_Core.m_ShotgunHitDisabled = true;
1019 }
1020 else if(Collision()->GetSwitchType(Index: MapIndex) == TILE_HIT_ENABLE && m_Core.m_GrenadeHitDisabled && Collision()->GetSwitchDelay(Index: MapIndex) == WEAPON_GRENADE)
1021 {
1022 m_Core.m_GrenadeHitDisabled = false;
1023 }
1024 else if(Collision()->GetSwitchType(Index: MapIndex) == TILE_HIT_DISABLE && !m_Core.m_GrenadeHitDisabled && Collision()->GetSwitchDelay(Index: MapIndex) == WEAPON_GRENADE)
1025 {
1026 m_Core.m_GrenadeHitDisabled = true;
1027 }
1028 else if(Collision()->GetSwitchType(Index: MapIndex) == TILE_HIT_ENABLE && m_Core.m_LaserHitDisabled && Collision()->GetSwitchDelay(Index: MapIndex) == WEAPON_LASER)
1029 {
1030 m_Core.m_LaserHitDisabled = false;
1031 }
1032 else if(Collision()->GetSwitchType(Index: MapIndex) == TILE_HIT_DISABLE && !m_Core.m_LaserHitDisabled && Collision()->GetSwitchDelay(Index: MapIndex) == WEAPON_LASER)
1033 {
1034 m_Core.m_LaserHitDisabled = true;
1035 }
1036 else if(Collision()->GetSwitchType(Index: MapIndex) == TILE_JUMP)
1037 {
1038 int NewJumps = Collision()->GetSwitchDelay(Index: MapIndex);
1039 if(NewJumps == 255)
1040 {
1041 NewJumps = -1;
1042 }
1043
1044 if(NewJumps != m_Core.m_Jumps)
1045 m_Core.m_Jumps = NewJumps;
1046 }
1047}
1048
1049void CCharacter::HandleTuneLayer()
1050{
1051 int CurrentIndex = Collision()->GetMapIndex(Pos: m_Pos);
1052 SetTuneZone(GameWorld()->m_WorldConfig.m_UseTuneZones ? Collision()->IsTune(Index: CurrentIndex) : 0);
1053 m_Core.m_Tuning = *GetTuning(i: GetOverriddenTuneZone());
1054}
1055
1056void CCharacter::DDRaceTick()
1057{
1058 mem_copy(dest: &m_Input, source: &m_SavedInput, size: sizeof(m_Input));
1059 if(m_Core.m_LiveFrozen && !m_CanMoveInFreeze && !m_Core.m_Super && !m_Core.m_Invincible)
1060 {
1061 m_Input.m_Direction = 0;
1062 m_Input.m_Jump = 0;
1063 //Hook and weapons are possible in live freeze
1064 }
1065 if(m_FreezeTime > 0)
1066 {
1067 m_FreezeTime--;
1068 if(!m_CanMoveInFreeze)
1069 {
1070 m_Input.m_Direction = 0;
1071 m_Input.m_Jump = 0;
1072 m_Input.m_Hook = 0;
1073 }
1074 if(m_FreezeTime == 1)
1075 Unfreeze();
1076 }
1077
1078 HandleTuneLayer();
1079
1080 // check if the tee is in any type of freeze
1081 int Index = Collision()->GetPureMapIndex(Pos: m_Pos);
1082 const int aTiles[] = {
1083 Collision()->GetTileIndex(Index),
1084 Collision()->GetFrontTileIndex(Index),
1085 Collision()->GetSwitchType(Index)};
1086 m_Core.m_IsInFreeze = false;
1087 for(const int Tile : aTiles)
1088 {
1089 if(Tile == TILE_FREEZE || Tile == TILE_DFREEZE || Tile == TILE_LFREEZE || Tile == TILE_DEATH)
1090 {
1091 m_Core.m_IsInFreeze = true;
1092 break;
1093 }
1094 }
1095 m_Core.m_IsInFreeze |= (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()->GetCollisionAt(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 Collision()->GetFrontCollisionAt(x: m_Pos.x - GetProximityRadius() / 3.f, y: m_Pos.y + GetProximityRadius() / 3.f) == TILE_DEATH);
1103}
1104
1105void CCharacter::DDRacePostCoreTick()
1106{
1107 if(!GameWorld()->m_WorldConfig.m_PredictDDRace)
1108 return;
1109
1110 if(m_Core.m_EndlessHook)
1111 m_Core.m_HookTick = 0;
1112
1113 m_FrozenLastTick = false;
1114
1115 if(m_Core.m_DeepFrozen && !m_Core.m_Super && !m_Core.m_Invincible)
1116 Freeze();
1117
1118 // following jump rules can be overridden by tiles, like Refill Jumps, Stopper and Wall Jump
1119 if(m_Core.m_Jumps == -1)
1120 {
1121 // The player has only one ground jump, so their feet are always dark
1122 m_Core.m_Jumped |= 2;
1123 }
1124 else if(m_Core.m_Jumps == 0)
1125 {
1126 // The player has no jumps at all, so their feet are always dark
1127 m_Core.m_Jumped |= 2;
1128 }
1129 else if(m_Core.m_Jumps == 1 && m_Core.m_Jumped > 0)
1130 {
1131 // If the player has only one jump, each jump is the last one
1132 m_Core.m_Jumped |= 2;
1133 }
1134 else if(m_Core.m_JumpedTotal < m_Core.m_Jumps - 1 && m_Core.m_Jumped > 1)
1135 {
1136 // The player has not yet used up all their jumps, so their feet remain light
1137 m_Core.m_Jumped = 1;
1138 }
1139
1140 if((m_Core.m_Super || m_Core.m_EndlessJump) && m_Core.m_Jumped > 1)
1141 {
1142 // Super players and players with infinite jumps always have light feet
1143 m_Core.m_Jumped = 1;
1144 }
1145
1146 int CurrentIndex = Collision()->GetMapIndex(Pos: m_Pos);
1147 HandleSkippableTiles(Index: CurrentIndex);
1148
1149 // handle Anti-Skip tiles
1150 std::vector<int> vIndices = Collision()->GetMapIndices(PrevPos: m_PrevPos, Pos: m_Pos);
1151 if(!vIndices.empty())
1152 for(int Index : vIndices)
1153 HandleTiles(Index);
1154 else
1155 {
1156 HandleTiles(Index: CurrentIndex);
1157 }
1158}
1159
1160bool CCharacter::Freeze(int Seconds)
1161{
1162 if(!GameWorld()->m_WorldConfig.m_PredictFreeze)
1163 return false;
1164 if(Seconds <= 0 || m_Core.m_Super || m_Core.m_Invincible || m_FreezeTime > Seconds * GameWorld()->GameTickSpeed())
1165 return false;
1166 if(m_Core.m_FreezeStart < GameWorld()->GameTick() - GameWorld()->GameTickSpeed())
1167 {
1168 m_FreezeTime = Seconds * GameWorld()->GameTickSpeed();
1169 m_Core.m_FreezeStart = GameWorld()->GameTick();
1170 m_Core.m_FreezeEnd = m_Core.m_DeepFrozen ? -1 : (m_FreezeTime == 0 ? 0 : GameWorld()->GameTick() + m_FreezeTime);
1171 return true;
1172 }
1173 return false;
1174}
1175
1176bool CCharacter::Freeze()
1177{
1178 return Freeze(Seconds: g_Config.m_SvFreezeDelay);
1179}
1180
1181bool CCharacter::Unfreeze()
1182{
1183 if(m_FreezeTime > 0)
1184 {
1185 if(!m_Core.m_aWeapons[m_Core.m_ActiveWeapon].m_Got)
1186 m_Core.m_ActiveWeapon = WEAPON_GUN;
1187 m_FreezeTime = 0;
1188 m_Core.m_FreezeStart = 0;
1189 m_Core.m_FreezeEnd = m_Core.m_DeepFrozen ? -1 : 0;
1190 if(GameWorld()->m_WorldConfig.m_PredictDDRace)
1191 m_FrozenLastTick = true;
1192 return true;
1193 }
1194 return false;
1195}
1196
1197void CCharacter::GiveWeapon(int Weapon, bool Remove)
1198{
1199 if(Weapon == WEAPON_NINJA)
1200 {
1201 if(Remove)
1202 RemoveNinja();
1203 else
1204 GiveNinja();
1205 return;
1206 }
1207
1208 if(Remove)
1209 {
1210 if(GetActiveWeapon() == Weapon)
1211 SetActiveWeapon(WEAPON_GUN);
1212 }
1213 else
1214 {
1215 m_Core.m_aWeapons[Weapon].m_Ammo = -1;
1216 }
1217
1218 m_Core.m_aWeapons[Weapon].m_Got = !Remove;
1219}
1220
1221void CCharacter::GiveAllWeapons()
1222{
1223 for(int i = WEAPON_GUN; i < NUM_WEAPONS - 1; i++)
1224 {
1225 GiveWeapon(Weapon: i);
1226 }
1227}
1228
1229void CCharacter::ResetVelocity()
1230{
1231 m_Core.m_Vel = vec2(0, 0);
1232}
1233
1234// The method is needed only to reproduce 'shotgun bug' ddnet#5258
1235// Use SetVelocity() instead.
1236void CCharacter::SetVelocity(const vec2 NewVelocity)
1237{
1238 m_Core.m_Vel = ClampVel(MoveRestriction: m_MoveRestrictions, Vel: NewVelocity);
1239}
1240
1241void CCharacter::SetRawVelocity(const vec2 NewVelocity)
1242{
1243 m_Core.m_Vel = NewVelocity;
1244}
1245
1246void CCharacter::AddVelocity(const vec2 Addition)
1247{
1248 SetVelocity(m_Core.m_Vel + Addition);
1249}
1250
1251void CCharacter::ApplyMoveRestrictions()
1252{
1253 m_Core.m_Vel = ClampVel(MoveRestriction: m_MoveRestrictions, Vel: m_Core.m_Vel);
1254}
1255
1256CTeamsCore *CCharacter::TeamsCore()
1257{
1258 return GameWorld()->Teams();
1259}
1260
1261CCharacter::CCharacter(CGameWorld *pGameWorld, int Id, CNetObj_Character *pChar, CNetObj_DDNetCharacter *pExtended) :
1262 CEntity(pGameWorld, CGameWorld::ENTTYPE_CHARACTER, vec2(0, 0), CCharacterCore::PhysicalSize())
1263{
1264 m_Id = Id;
1265 m_IsLocal = false;
1266
1267 m_LastWeapon = WEAPON_HAMMER;
1268 m_QueuedWeapon = -1;
1269 m_LastRefillJumps = false;
1270 m_PrevPrevPos = m_PrevPos = m_Pos = vec2(pChar->m_X, pChar->m_Y);
1271 m_Core.Reset();
1272 m_Core.Init(pWorld: &GameWorld()->m_Core, pCollision: GameWorld()->Collision(), pTeams: GameWorld()->Teams());
1273 m_Core.m_Id = Id;
1274 mem_zero(block: &m_Core.m_Ninja, size: sizeof(m_Core.m_Ninja));
1275 m_Core.m_LeftWall = true;
1276 m_ReloadTimer = 0;
1277 m_NumObjectsHit = 0;
1278 m_LastRefillJumps = false;
1279 m_CanMoveInFreeze = false;
1280 m_TeleCheckpoint = 0;
1281 m_StrongWeakId = 0;
1282
1283 mem_zero(block: &m_Input, size: sizeof(m_Input));
1284 // never initialize both to zero
1285 m_Input.m_TargetX = 0;
1286 m_Input.m_TargetY = -1;
1287
1288 m_LatestPrevInput = m_LatestInput = m_PrevInput = m_SavedInput = m_Input;
1289
1290 ResetPrediction();
1291 Read(pChar, pExtended, IsLocal: false);
1292}
1293
1294void CCharacter::ResetPrediction()
1295{
1296 SetSolo(false);
1297 SetSuper(false);
1298 m_Core.m_EndlessHook = false;
1299 m_Core.m_HammerHitDisabled = false;
1300 m_Core.m_ShotgunHitDisabled = false;
1301 m_Core.m_GrenadeHitDisabled = false;
1302 m_Core.m_LaserHitDisabled = false;
1303 m_Core.m_EndlessJump = false;
1304 m_Core.m_Jetpack = false;
1305 m_NinjaJetpack = false;
1306 m_Core.m_Jumps = 2;
1307 m_Core.m_HookHitDisabled = false;
1308 m_Core.m_CollisionDisabled = false;
1309 m_NumInputs = 0;
1310 m_FreezeTime = 0;
1311 m_Core.m_FreezeStart = 0;
1312 m_Core.m_IsInFreeze = false;
1313 m_Core.m_DeepFrozen = false;
1314 m_Core.m_LiveFrozen = false;
1315 m_FrozenLastTick = false;
1316 for(int w = 0; w < NUM_WEAPONS; w++)
1317 {
1318 SetWeaponGot(Type: w, Value: false);
1319 SetWeaponAmmo(Type: w, Value: -1);
1320 }
1321 if(m_Core.HookedPlayer() >= 0)
1322 {
1323 m_Core.SetHookedPlayer(-1);
1324 m_Core.m_HookState = HOOK_IDLE;
1325 }
1326 m_LastWeaponSwitchTick = 0;
1327 m_LastTuneZoneTick = 0;
1328}
1329
1330void CCharacter::Read(CNetObj_Character *pChar, CNetObj_DDNetCharacter *pExtended, bool IsLocal)
1331{
1332 m_Core.Read(pObjCore: (const CNetObj_CharacterCore *)pChar);
1333 m_IsLocal = IsLocal;
1334
1335 if(pExtended)
1336 {
1337 SetSolo(pExtended->m_Flags & CHARACTERFLAG_SOLO);
1338 SetSuper(pExtended->m_Flags & CHARACTERFLAG_SUPER);
1339
1340 m_TeleCheckpoint = pExtended->m_TeleCheckpoint;
1341 m_StrongWeakId = pExtended->m_StrongWeakId;
1342 m_TuneZoneOverride = pExtended->m_TuneZoneOverride;
1343
1344 const bool Ninja = (pExtended->m_Flags & CHARACTERFLAG_WEAPON_NINJA) != 0;
1345 if(Ninja && m_Core.m_ActiveWeapon != WEAPON_NINJA)
1346 GiveNinja();
1347 else if(!Ninja && m_Core.m_ActiveWeapon == WEAPON_NINJA)
1348 RemoveNinja();
1349
1350 if(GameWorld()->m_WorldConfig.m_PredictFreeze && pExtended->m_FreezeEnd != 0)
1351 {
1352 if(pExtended->m_FreezeEnd > 0)
1353 {
1354 if(m_FreezeTime == 0)
1355 Freeze();
1356 m_FreezeTime = maximum(a: 1, b: pExtended->m_FreezeEnd - GameWorld()->GameTick());
1357 }
1358 else if(pExtended->m_FreezeEnd == -1)
1359 m_Core.m_DeepFrozen = true;
1360 }
1361 else
1362 Unfreeze();
1363
1364 m_Core.ReadDDNet(pObjDDNet: pExtended);
1365
1366 if(!GameWorld()->m_WorldConfig.m_PredictFreeze)
1367 {
1368 Unfreeze();
1369 }
1370 }
1371 else
1372 {
1373 // ddnetcharacter is not available, try to get some info from the tunings and the character netobject instead.
1374
1375 // remove weapons that are unavailable. if the current weapon is ninja just set ammo to zero in case the player is frozen
1376 if(pChar->m_Weapon != m_Core.m_ActiveWeapon)
1377 {
1378 if(pChar->m_Weapon == WEAPON_NINJA)
1379 m_Core.m_aWeapons[m_Core.m_ActiveWeapon].m_Ammo = 0;
1380 else
1381 {
1382 if(m_Core.m_ActiveWeapon == WEAPON_NINJA)
1383 {
1384 SetNinjaActivationDir(vec2(0, 0));
1385 SetNinjaActivationTick(-500);
1386 SetNinjaCurrentMoveTime(0);
1387 }
1388 if(pChar->m_Weapon == m_LastSnapWeapon)
1389 m_Core.m_aWeapons[m_Core.m_ActiveWeapon].m_Got = false;
1390 }
1391 }
1392 // add weapon
1393 if(pChar->m_Weapon >= 0 && pChar->m_Weapon != WEAPON_NINJA)
1394 {
1395 m_Core.m_aWeapons[pChar->m_Weapon].m_Got = true;
1396 }
1397
1398 // 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
1399 if(GameWorld()->m_WorldConfig.m_PredictWeapons && GetTuning(i: GetOverriddenTuneZone())->m_JetpackStrength != 0)
1400 {
1401 m_Core.m_Jetpack = true;
1402 m_Core.m_aWeapons[WEAPON_GUN].m_Got = true;
1403 m_Core.m_aWeapons[WEAPON_GUN].m_Ammo = -1;
1404 m_NinjaJetpack = pChar->m_Weapon == WEAPON_NINJA;
1405 }
1406 else if(pChar->m_Weapon != WEAPON_NINJA)
1407 {
1408 m_Core.m_Jetpack = false;
1409 }
1410
1411 // number of jumps
1412 if(GameWorld()->m_WorldConfig.m_PredictTiles)
1413 {
1414 if(pChar->m_Jumped & 2)
1415 {
1416 m_Core.m_EndlessJump = false;
1417 if(m_Core.m_Jumps > m_Core.m_JumpedTotal && m_Core.m_JumpedTotal > 0 && m_Core.m_Jumps > 2)
1418 m_Core.m_Jumps = m_Core.m_JumpedTotal + 1;
1419 }
1420 else if(m_Core.m_Jumps < 2)
1421 m_Core.m_Jumps = m_Core.m_JumpedTotal + 2;
1422 if(GetTuning(i: GetOverriddenTuneZone())->m_AirJumpImpulse == 0)
1423 {
1424 m_Core.m_Jumps = 0;
1425 m_Core.m_Jumped = 3;
1426 }
1427 }
1428
1429 // set player collision
1430 SetSolo(!GetTuning(i: GetOverriddenTuneZone())->m_PlayerCollision && !GetTuning(i: GetOverriddenTuneZone())->m_PlayerHooking);
1431 m_Core.m_CollisionDisabled = !GetTuning(i: GetOverriddenTuneZone())->m_PlayerCollision;
1432 m_Core.m_HookHitDisabled = !GetTuning(i: GetOverriddenTuneZone())->m_PlayerHooking;
1433
1434 if(m_Core.m_HookTick != 0)
1435 m_Core.m_EndlessHook = false;
1436
1437 // detect unfreeze (in case the player was frozen in the tile prediction and not correctly unfrozen)
1438 if(pChar->m_Emote != EMOTE_PAIN && pChar->m_Emote != EMOTE_NORMAL)
1439 m_Core.m_DeepFrozen = false;
1440 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)
1441 {
1442 m_Core.m_DeepFrozen = false;
1443 Unfreeze();
1444 }
1445
1446 m_TuneZoneOverride = TuneZone::OVERRIDE_NONE;
1447 }
1448
1449 vec2 PosBefore = m_Pos;
1450 m_Pos = m_Core.m_Pos;
1451
1452 if(distance(a: PosBefore, b: m_Pos) > 2.f) // misprediction, don't use prevpos
1453 m_PrevPos = m_Pos;
1454
1455 if(distance(a: m_PrevPos, b: m_Pos) > 10.f * 32.f) // reset prevpos if the distance is high
1456 m_PrevPos = m_Pos;
1457
1458 if(pChar->m_Jumped & 2)
1459 m_Core.m_JumpedTotal = m_Core.m_Jumps;
1460 m_AttackTick = pChar->m_AttackTick;
1461 m_LastSnapWeapon = pChar->m_Weapon;
1462
1463 SetTuneZone(GameWorld()->m_WorldConfig.m_UseTuneZones ? Collision()->IsTune(Index: Collision()->GetMapIndex(Pos: m_Pos)) : 0);
1464
1465 // set the current weapon
1466 if(pChar->m_Weapon != WEAPON_NINJA)
1467 {
1468 if(pChar->m_Weapon >= 0)
1469 m_Core.m_aWeapons[pChar->m_Weapon].m_Ammo = (GameWorld()->m_WorldConfig.m_InfiniteAmmo || pChar->m_Weapon == WEAPON_HAMMER) ? -1 : pChar->m_AmmoCount;
1470
1471 if(pChar->m_Weapon != m_Core.m_ActiveWeapon)
1472 SetActiveWeapon(pChar->m_Weapon);
1473 }
1474
1475 // reset all input except direction and hook for non-local players (as in vanilla prediction)
1476 if(!IsLocal)
1477 {
1478 mem_zero(block: &m_Input, size: sizeof(m_Input));
1479 mem_zero(block: &m_SavedInput, size: sizeof(m_SavedInput));
1480 m_Input.m_Direction = m_SavedInput.m_Direction = m_Core.m_Direction;
1481 m_Input.m_Hook = m_SavedInput.m_Hook = (m_Core.m_HookState != HOOK_IDLE);
1482
1483 if(pExtended && pExtended->m_TargetX != 0 && pExtended->m_TargetY != 0)
1484 {
1485 m_Input.m_TargetX = m_SavedInput.m_TargetX = pExtended->m_TargetX;
1486 m_Input.m_TargetY = m_SavedInput.m_TargetY = pExtended->m_TargetY;
1487 }
1488 else
1489 {
1490 m_Input.m_TargetX = m_SavedInput.m_TargetX = std::cos(x: pChar->m_Angle / 256.0f) * 256.0f;
1491 m_Input.m_TargetY = m_SavedInput.m_TargetY = std::sin(x: pChar->m_Angle / 256.0f) * 256.0f;
1492 }
1493 }
1494
1495 // in most cases the reload timer can be determined from the last attack tick
1496 // (this is only needed for autofire weapons to prevent the predicted reload timer from desyncing)
1497 if(IsLocal && m_Core.m_ActiveWeapon != -1 && m_Core.m_ActiveWeapon != WEAPON_HAMMER && !m_Core.m_aWeapons[WEAPON_NINJA].m_Got)
1498 {
1499 if(maximum(a: m_LastTuneZoneTick, b: m_LastWeaponSwitchTick) + GameWorld()->GameTickSpeed() < GameWorld()->GameTick())
1500 {
1501 const int FireDelayTicks = GetTuning(i: GetOverriddenTuneZone())->GetWeaponFireDelay(Weapon: m_Core.m_ActiveWeapon) * GameWorld()->GameTickSpeed();
1502 m_ReloadTimer = maximum(a: 0, b: m_AttackTick + FireDelayTicks - GameWorld()->GameTick());
1503 }
1504 }
1505}
1506
1507void CCharacter::SetCoreWorld(CGameWorld *pGameWorld)
1508{
1509 m_Core.SetCoreWorld(pWorld: &pGameWorld->m_Core, pCollision: pGameWorld->Collision(), pTeams: pGameWorld->Teams());
1510}
1511
1512bool CCharacter::Match(CCharacter *pChar) const
1513{
1514 return distance(a: pChar->m_Core.m_Pos, b: m_Core.m_Pos) <= 32.f;
1515}
1516
1517void CCharacter::SetActiveWeapon(int ActiveWeapon)
1518{
1519 if(ActiveWeapon < WEAPON_HAMMER || ActiveWeapon >= NUM_WEAPONS)
1520 {
1521 m_Core.m_ActiveWeapon = -1;
1522 }
1523 else
1524 {
1525 m_Core.m_ActiveWeapon = ActiveWeapon;
1526 }
1527 m_LastWeaponSwitchTick = GameWorld()->GameTick();
1528}
1529
1530void CCharacter::SetTuneZone(int Zone)
1531{
1532 if(Zone == m_TuneZone)
1533 return;
1534 m_TuneZone = Zone;
1535 m_LastTuneZoneTick = GameWorld()->GameTick();
1536}
1537
1538int CCharacter::GetOverriddenTuneZone() const
1539{
1540 return m_TuneZoneOverride == TuneZone::OVERRIDE_NONE ? m_TuneZone : m_TuneZoneOverride;
1541}
1542
1543int CCharacter::GetPureTuneZone() const
1544{
1545 return m_TuneZone;
1546}
1547
1548CCharacter::~CCharacter()
1549{
1550 if(GameWorld())
1551 GameWorld()->RemoveCharacter(pChar: this);
1552}
1553