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