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