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