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