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 | |
14 | void 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 | |
27 | void CCharacter::SetSolo(bool Solo) |
28 | { |
29 | m_Core.m_Solo = Solo; |
30 | TeamsCore()->SetSolo(ClientId: GetCid(), Value: Solo); |
31 | } |
32 | |
33 | void 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 | |
40 | bool 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 | |
51 | void 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 | |
96 | void 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 | |
105 | void 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 | |
191 | void 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 | |
201 | void 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 | |
251 | void 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 | |
471 | void 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 | |
488 | void 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 | |
499 | void 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 | |
520 | void 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 | |
549 | void 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 | |
556 | void CCharacter::ResetHook() |
557 | { |
558 | ReleaseHook(); |
559 | m_Core.m_HookPos = m_Core.m_Pos; |
560 | } |
561 | |
562 | void 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 | |
574 | void 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 | |
582 | void 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 | |
605 | void CCharacter::TickDeferred() |
606 | { |
607 | m_Core.Move(); |
608 | m_Core.Quantize(); |
609 | m_Pos = m_Core.m_Pos; |
610 | } |
611 | |
612 | bool 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 | |
621 | bool CCharacter::CanCollide(int ClientId) |
622 | { |
623 | return TeamsCore()->CanCollide(ClientId1: GetCid(), ClientId2: ClientId); |
624 | } |
625 | |
626 | bool CCharacter::SameTeam(int ClientId) |
627 | { |
628 | return TeamsCore()->SameTeam(ClientId1: GetCid(), ClientId2: ClientId); |
629 | } |
630 | |
631 | int CCharacter::Team() |
632 | { |
633 | return TeamsCore()->Team(ClientId: GetCid()); |
634 | } |
635 | |
636 | void 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 | |
700 | bool 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 | |
707 | void 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 | |
949 | void 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 | |
959 | void 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 | |
1000 | void 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 | |
1055 | bool 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 | |
1070 | bool CCharacter::Freeze() |
1071 | { |
1072 | return Freeze(Seconds: g_Config.m_SvFreezeDelay); |
1073 | } |
1074 | |
1075 | bool 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 | |
1089 | void 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 | |
1113 | void CCharacter::GiveAllWeapons() |
1114 | { |
1115 | for(int i = WEAPON_GUN; i < NUM_WEAPONS - 1; i++) |
1116 | { |
1117 | GiveWeapon(Weapon: i); |
1118 | } |
1119 | } |
1120 | |
1121 | void 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. |
1128 | void CCharacter::SetVelocity(const vec2 NewVelocity) |
1129 | { |
1130 | m_Core.m_Vel = ClampVel(MoveRestriction: m_MoveRestrictions, Vel: NewVelocity); |
1131 | } |
1132 | |
1133 | void CCharacter::SetRawVelocity(const vec2 NewVelocity) |
1134 | { |
1135 | m_Core.m_Vel = NewVelocity; |
1136 | } |
1137 | |
1138 | void CCharacter::AddVelocity(const vec2 Addition) |
1139 | { |
1140 | SetVelocity(m_Core.m_Vel + Addition); |
1141 | } |
1142 | |
1143 | void CCharacter::ApplyMoveRestrictions() |
1144 | { |
1145 | m_Core.m_Vel = ClampVel(MoveRestriction: m_MoveRestrictions, Vel: m_Core.m_Vel); |
1146 | } |
1147 | |
1148 | CTeamsCore *CCharacter::TeamsCore() |
1149 | { |
1150 | return GameWorld()->Teams(); |
1151 | } |
1152 | |
1153 | CCharacter::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 | |
1187 | void 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 | |
1223 | void 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 | |
1396 | void CCharacter::SetCoreWorld(CGameWorld *pGameWorld) |
1397 | { |
1398 | m_Core.SetCoreWorld(pWorld: &pGameWorld->m_Core, pCollision: pGameWorld->Collision(), pTeams: pGameWorld->Teams()); |
1399 | } |
1400 | |
1401 | bool CCharacter::Match(CCharacter *pChar) const |
1402 | { |
1403 | return distance(a: pChar->m_Core.m_Pos, b: m_Core.m_Pos) <= 32.f; |
1404 | } |
1405 | |
1406 | void CCharacter::SetActiveWeapon(int ActiveWeap) |
1407 | { |
1408 | m_Core.m_ActiveWeapon = ActiveWeap; |
1409 | m_LastWeaponSwitchTick = GameWorld()->GameTick(); |
1410 | } |
1411 | |
1412 | void CCharacter::SetTuneZone(int Zone) |
1413 | { |
1414 | if(Zone == m_TuneZone) |
1415 | return; |
1416 | m_TuneZone = Zone; |
1417 | m_LastTuneZoneTick = GameWorld()->GameTick(); |
1418 | } |
1419 | |
1420 | CCharacter::~CCharacter() |
1421 | { |
1422 | if(GameWorld()) |
1423 | GameWorld()->RemoveCharacter(pChar: this); |
1424 | } |
1425 | |