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
4#include "camera.h"
5
6#include "controls.h"
7
8#include <base/log.h>
9#include <base/math.h>
10#include <base/vmath.h>
11
12#include <engine/shared/config.h>
13
14#include <game/client/gameclient.h>
15#include <game/collision.h>
16#include <game/localization.h>
17#include <game/mapitems.h>
18
19#include <limits>
20
21CCamera::CCamera()
22{
23 m_CamType = CAMTYPE_UNDEFINED;
24 m_ZoomSet = false;
25 m_Zoom = 1.0f;
26 m_Zooming = false;
27 m_ForceFreeview = false;
28 m_GotoSwitchOffset = 0;
29 m_GotoTeleOffset = 0;
30 m_GotoSwitchLastPos = ivec2(-1, -1);
31 m_GotoTeleLastPos = ivec2(-1, -1);
32
33 std::fill(first: std::begin(arr&: m_aLastPos), last: std::end(arr&: m_aLastPos), value: vec2(0.0f, 0.0f));
34 m_PrevCenter = vec2(0, 0);
35 m_Center = vec2(0, 0);
36
37 m_PrevSpecId = -1;
38 m_WasSpectating = false;
39
40 m_CameraSmoothing = false;
41
42 m_LastTargetPos = vec2(0, 0);
43 m_DyncamTargetCameraOffset = vec2(0, 0);
44 std::fill(first: std::begin(arr&: m_aDyncamCurrentCameraOffset), last: std::end(arr&: m_aDyncamCurrentCameraOffset), value: vec2(0.0f, 0.0f));
45 m_DyncamSmoothingSpeedBias = 0.5f;
46
47 m_AutoSpecCamera = true;
48 m_AutoSpecCameraZooming = false;
49 m_CanUseCameraInfo = false;
50 m_UsingAutoSpecCamera = false;
51
52 m_aAutoSpecCameraTooltip[0] = '\0';
53}
54
55float CCamera::CameraSmoothingProgress(float CurrentTime) const
56{
57 float Progress = (CurrentTime - m_CameraSmoothingStart) / (m_CameraSmoothingEnd - m_CameraSmoothingStart);
58 return 1.0 - std::pow(x: 2.0, y: -10.0 * Progress);
59}
60
61float CCamera::ZoomProgress(float CurrentTime) const
62{
63 return (CurrentTime - m_ZoomSmoothingStart) / (m_ZoomSmoothingEnd - m_ZoomSmoothingStart);
64}
65
66void CCamera::ScaleZoom(float Factor)
67{
68 float CurrentTarget = m_Zooming ? m_ZoomSmoothingTarget : m_Zoom;
69 ChangeZoom(Target: CurrentTarget * Factor, Smoothness: GameClient()->m_Snap.m_SpecInfo.m_Active && GameClient()->m_MultiViewActivated ? g_Config.m_ClMultiViewZoomSmoothness : g_Config.m_ClSmoothZoomTime, IsUser: true);
70
71 m_AutoSpecCamera = false;
72}
73
74float CCamera::MaxZoomLevel()
75{
76 return (g_Config.m_ClLimitMaxZoomLevel) ? ((Graphics()->IsTileBufferingEnabled() ? 240 : 30)) : std::numeric_limits<float>::max();
77}
78
79float CCamera::MinZoomLevel()
80{
81 return 0.01f;
82}
83
84void CCamera::ChangeZoom(float Target, int Smoothness, bool IsUser)
85{
86 if(Target > MaxZoomLevel() || Target < MinZoomLevel())
87 {
88 return;
89 }
90
91 float Now = Client()->LocalTime();
92 float Current = m_Zoom;
93 float Derivative = 0.0f;
94 if(m_Zooming)
95 {
96 float Progress = ZoomProgress(CurrentTime: Now);
97 Current = m_ZoomSmoothing.Evaluate(t: Progress);
98 Derivative = m_ZoomSmoothing.Derivative(t: Progress);
99 }
100
101 m_ZoomSmoothingTarget = Target;
102 m_ZoomSmoothing = CCubicBezier::With(Start: Current, StartDerivative: Derivative, EndDerivative: 0, End: m_ZoomSmoothingTarget);
103 m_ZoomSmoothingStart = Now;
104 m_ZoomSmoothingEnd = Now + (float)Smoothness / 1000;
105
106 if(IsUser)
107 m_UserZoomTarget = Target;
108
109 m_Zooming = true;
110}
111
112void CCamera::ResetAutoSpecCamera()
113{
114 m_AutoSpecCamera = true;
115}
116
117void CCamera::UpdateCamera()
118{
119 // use hardcoded smooth camera for spectating unless player explicitly turn it off
120 bool CanUseCameraInfo = !GameClient()->m_MultiViewActivated;
121 if(Client()->State() == IClient::STATE_DEMOPLAYBACK)
122 {
123 // only follow mode have the correct camera info
124 CanUseCameraInfo = CanUseCameraInfo && GameClient()->m_DemoSpecId == SPEC_FOLLOW;
125 }
126 else
127 {
128 CanUseCameraInfo = CanUseCameraInfo && GameClient()->m_Snap.m_SpecInfo.m_Active &&
129 GameClient()->m_Snap.m_SpecInfo.m_SpectatorId >= 0 &&
130 GameClient()->m_Snap.m_SpecInfo.m_SpectatorId != GameClient()->m_aLocalIds[0] &&
131 (!GameClient()->Client()->DummyConnected() || GameClient()->m_Snap.m_SpecInfo.m_SpectatorId != GameClient()->m_aLocalIds[1]);
132 }
133
134 bool UsingAutoSpecCamera = m_AutoSpecCamera && CanUseAutoSpecCamera();
135 float CurrentZoom = m_Zooming ? m_ZoomSmoothingTarget : m_Zoom;
136 bool ZoomChanged = false;
137 if(CanUseCameraInfo && UsingAutoSpecCamera && CurrentZoom != GameClient()->m_Snap.m_SpecInfo.m_Zoom)
138 {
139 // start spectating player / turn on auto spec camera
140 bool ChangeTarget = m_PrevSpecId != GameClient()->m_Snap.m_SpecInfo.m_SpectatorId;
141 float SmoothTime = ChangeTarget ? g_Config.m_ClSmoothSpectatingTime : 250;
142 ChangeZoom(Target: GameClient()->m_Snap.m_SpecInfo.m_Zoom, Smoothness: SmoothTime, IsUser: false);
143
144 // it is auto spec camera zooming if only the zoom is changed during activation, not at the start of the activation
145 m_AutoSpecCameraZooming = !ChangeTarget && CanUseCameraInfo && m_UsingAutoSpecCamera;
146
147 ZoomChanged = true;
148 }
149 else if((CanUseCameraInfo && !UsingAutoSpecCamera) && CurrentZoom != m_UserZoomTarget)
150 {
151 // turning off auto spec camera
152 ChangeZoom(Target: m_UserZoomTarget, Smoothness: g_Config.m_ClSmoothZoomTime, IsUser: false);
153 m_AutoSpecCameraZooming = false;
154
155 ZoomChanged = true;
156 }
157 else if(!CanUseCameraInfo && CurrentZoom != m_UserZoomTarget)
158 {
159 // stop spectating player
160 if(!GameClient()->m_MultiViewActivated)
161 ChangeZoom(Target: m_UserZoomTarget, Smoothness: g_Config.m_ClSmoothZoomTime, IsUser: false);
162 m_AutoSpecCameraZooming = false;
163
164 ZoomChanged = true;
165 }
166
167 // snap zoom when going in and out of spectating
168 if(ZoomChanged && m_WasSpectating != GameClient()->m_Snap.m_SpecInfo.m_Active)
169 {
170 m_Zoom = m_ZoomSmoothingTarget;
171 m_Zooming = false;
172 }
173
174 if(m_Zooming)
175 {
176 float Time = Client()->LocalTime();
177 if(Time >= m_ZoomSmoothingEnd)
178 {
179 m_Zoom = m_ZoomSmoothingTarget;
180 m_Zooming = false;
181 m_AutoSpecCameraZooming = false;
182 }
183 else
184 {
185 const float OldLevel = m_Zoom;
186 m_Zoom = m_ZoomSmoothing.Evaluate(t: ZoomProgress(CurrentTime: Time));
187 if((OldLevel < m_ZoomSmoothingTarget && m_Zoom > m_ZoomSmoothingTarget) || (OldLevel > m_ZoomSmoothingTarget && m_Zoom < m_ZoomSmoothingTarget))
188 {
189 m_Zoom = m_ZoomSmoothingTarget;
190 m_Zooming = false;
191 m_AutoSpecCameraZooming = false;
192 }
193 }
194 m_Zoom = std::clamp(val: m_Zoom, lo: MinZoomLevel(), hi: MaxZoomLevel());
195 }
196
197 if(!ZoomAllowed())
198 {
199 m_ZoomSet = false;
200 m_Zoom = 1.0f;
201 m_Zooming = false;
202 m_AutoSpecCameraZooming = false;
203 }
204 else if(!m_ZoomSet && g_Config.m_ClDefaultZoom != 10)
205 {
206 m_ZoomSet = true;
207 OnReset();
208 }
209
210 if(GameClient()->m_Snap.m_SpecInfo.m_Active && !GameClient()->m_Snap.m_SpecInfo.m_UsePosition)
211 {
212 m_aDyncamCurrentCameraOffset[g_Config.m_ClDummy] = vec2(0, 0);
213 m_CanUseCameraInfo = CanUseCameraInfo;
214 m_UsingAutoSpecCamera = UsingAutoSpecCamera;
215 return;
216 }
217
218 vec2 TargetPos = CanUseCameraInfo ? GameClient()->m_CursorInfo.Target() : GameClient()->m_Controls.m_aMousePos[g_Config.m_ClDummy];
219 int Smoothness = CanUseCameraInfo ? 50 : g_Config.m_ClDyncamSmoothness;
220 int Stabilizing = CanUseCameraInfo ? 50 : g_Config.m_ClDyncamStabilizing;
221 bool IsDyncam = CanUseCameraInfo ? true : g_Config.m_ClDyncam;
222
223 float DeltaTime = Client()->RenderFrameTime();
224
225 if(Smoothness > 0)
226 {
227 float CameraSpeed = (1.0f - (Smoothness / 100.0f)) * 9.5f + 0.5f;
228 float CameraStabilizingFactor = 1 + Stabilizing / 100.0f;
229
230 m_DyncamSmoothingSpeedBias += CameraSpeed * DeltaTime;
231 if(IsDyncam)
232 {
233 m_DyncamSmoothingSpeedBias -= length(a: TargetPos - m_LastTargetPos) * std::log10(x: CameraStabilizingFactor) * 0.02f;
234 m_DyncamSmoothingSpeedBias = std::clamp(val: m_DyncamSmoothingSpeedBias, lo: 0.5f, hi: CameraSpeed);
235 }
236 else
237 {
238 m_DyncamSmoothingSpeedBias = maximum(a: 5.0f, b: CameraSpeed); // make sure toggle back is fast
239 }
240 }
241
242 m_DyncamTargetCameraOffset = vec2(0, 0);
243 float l = length(a: TargetPos);
244 if(l > 0.0001f) // make sure that this isn't 0
245 {
246 float CurrentDeadzone = Deadzone();
247 float CurrentFollowFactor = FollowFactor();
248
249 // use provided camera setting from server
250 if(CanUseCameraInfo)
251 {
252 CurrentDeadzone = GameClient()->m_Snap.m_SpecInfo.m_Deadzone;
253 CurrentFollowFactor = GameClient()->m_Snap.m_SpecInfo.m_FollowFactor;
254
255 if(!UsingAutoSpecCamera)
256 {
257 // turn off dyncam if user zooms when spectating
258 CurrentDeadzone = 0;
259 CurrentFollowFactor = 0;
260 }
261 }
262
263 float OffsetAmount = maximum(a: l - CurrentDeadzone, b: 0.0f) * (CurrentFollowFactor / 100.0f);
264
265 if(CanUseCameraInfo)
266 {
267 OffsetAmount = minimum(a: OffsetAmount, b: 350.0f * m_Zoom);
268 }
269
270 m_DyncamTargetCameraOffset = normalize_pre_length(v: TargetPos, len: l) * OffsetAmount;
271 }
272
273 m_LastTargetPos = TargetPos;
274 vec2 CurrentCameraOffset = m_aDyncamCurrentCameraOffset[g_Config.m_ClDummy];
275 float SpeedBias = m_CameraSmoothing ? 50.0f : m_DyncamSmoothingSpeedBias;
276 if(Smoothness > 0)
277 CurrentCameraOffset += (m_DyncamTargetCameraOffset - CurrentCameraOffset) * minimum(a: DeltaTime * SpeedBias, b: 1.0f);
278 else
279 CurrentCameraOffset = m_DyncamTargetCameraOffset;
280
281 // directly put the camera in place when switching in and out of freeview or spectate mode
282 if(m_CanUseCameraInfo != CanUseCameraInfo)
283 {
284 CurrentCameraOffset = m_DyncamTargetCameraOffset;
285 }
286
287 m_aDyncamCurrentCameraOffset[g_Config.m_ClDummy] = CurrentCameraOffset;
288 m_CanUseCameraInfo = CanUseCameraInfo;
289 m_UsingAutoSpecCamera = UsingAutoSpecCamera;
290}
291
292void CCamera::OnRender()
293{
294 if(m_CameraSmoothing)
295 {
296 if(!GameClient()->m_Snap.m_SpecInfo.m_Active)
297 {
298 m_Center = m_CameraSmoothingTarget;
299 m_CameraSmoothing = false;
300 }
301 else
302 {
303 float Time = Client()->LocalTime();
304 if(Time >= m_CameraSmoothingEnd)
305 {
306 m_Center = m_CameraSmoothingTarget;
307 m_CameraSmoothing = false;
308 }
309 else
310 {
311 m_CameraSmoothingCenter = vec2(m_CameraSmoothingBezierX.Evaluate(t: CameraSmoothingProgress(CurrentTime: Time)), m_CameraSmoothingBezierY.Evaluate(t: CameraSmoothingProgress(CurrentTime: Time)));
312 if(distance(a: m_CameraSmoothingCenter, b: m_CameraSmoothingTarget) <= 0.1f)
313 {
314 m_Center = m_CameraSmoothingTarget;
315 m_CameraSmoothing = false;
316 }
317 }
318 }
319 }
320
321 // update camera center
322 if(GameClient()->m_Snap.m_SpecInfo.m_Active && !GameClient()->m_Snap.m_SpecInfo.m_UsePosition)
323 {
324 if(m_CamType != CAMTYPE_SPEC)
325 {
326 m_aLastPos[g_Config.m_ClDummy] = GameClient()->m_Controls.m_aMousePos[g_Config.m_ClDummy];
327 GameClient()->m_Controls.m_aMousePos[g_Config.m_ClDummy] = m_PrevCenter;
328 GameClient()->m_Controls.m_aMouseInputType[g_Config.m_ClDummy] = CControls::EMouseInputType::AUTOMATED;
329 GameClient()->m_Controls.ClampMousePos();
330 m_CamType = CAMTYPE_SPEC;
331 }
332 m_Center = GameClient()->m_Controls.m_aMousePos[g_Config.m_ClDummy];
333 }
334 else
335 {
336 if(m_CamType != CAMTYPE_PLAYER)
337 {
338 GameClient()->m_Controls.m_aMousePos[g_Config.m_ClDummy] = m_aLastPos[g_Config.m_ClDummy];
339 GameClient()->m_Controls.m_aMouseInputType[g_Config.m_ClDummy] = CControls::EMouseInputType::AUTOMATED;
340 GameClient()->m_Controls.ClampMousePos();
341 m_CamType = CAMTYPE_PLAYER;
342 }
343
344 if(GameClient()->m_Snap.m_SpecInfo.m_Active)
345 m_Center = GameClient()->m_Snap.m_SpecInfo.m_Position + m_aDyncamCurrentCameraOffset[g_Config.m_ClDummy];
346 else
347 m_Center = GameClient()->m_LocalCharacterPos + m_aDyncamCurrentCameraOffset[g_Config.m_ClDummy];
348 }
349
350 if(m_ForceFreeview && m_CamType == CAMTYPE_SPEC)
351 {
352 GameClient()->m_Controls.m_aMouseInputType[g_Config.m_ClDummy] = CControls::EMouseInputType::AUTOMATED;
353 m_Center = GameClient()->m_Controls.m_aMousePos[g_Config.m_ClDummy] = m_ForceFreeviewPos;
354 m_ForceFreeview = false;
355 }
356 else
357 m_ForceFreeviewPos = m_Center;
358
359 const int SpecId = GameClient()->m_Snap.m_SpecInfo.m_SpectatorId;
360
361 // start smoothing from the current position when the target changes
362 if(m_CameraSmoothing && SpecId != m_PrevSpecId)
363 m_CameraSmoothing = false;
364
365 if(GameClient()->m_Snap.m_SpecInfo.m_Active &&
366 (SpecId != m_PrevSpecId ||
367 (m_CameraSmoothing && m_CameraSmoothingTarget != m_Center)) && // the target is moving during camera smoothing
368 !(!m_WasSpectating && m_Center != m_PrevCenter) && // dont smooth when starting to spectate
369 m_CamType != CAMTYPE_SPEC &&
370 !GameClient()->m_MultiViewActivated)
371 {
372 float Now = Client()->LocalTime();
373 if(!m_CameraSmoothing)
374 m_CenterBeforeSmoothing = m_PrevCenter;
375
376 vec2 Derivative = {0.f, 0.f};
377 if(m_CameraSmoothing)
378 {
379 float Progress = CameraSmoothingProgress(CurrentTime: Now);
380 Derivative.x = m_CameraSmoothingBezierX.Derivative(t: Progress);
381 Derivative.y = m_CameraSmoothingBezierY.Derivative(t: Progress);
382 }
383
384 m_CameraSmoothingTarget = m_Center;
385 m_CameraSmoothingBezierX = CCubicBezier::With(Start: m_CenterBeforeSmoothing.x, StartDerivative: Derivative.x, EndDerivative: 0, End: m_CameraSmoothingTarget.x);
386 m_CameraSmoothingBezierY = CCubicBezier::With(Start: m_CenterBeforeSmoothing.y, StartDerivative: Derivative.y, EndDerivative: 0, End: m_CameraSmoothingTarget.y);
387
388 if(!m_CameraSmoothing)
389 {
390 m_CameraSmoothingStart = Now;
391 m_CameraSmoothingEnd = Now + (float)g_Config.m_ClSmoothSpectatingTime / 1000.0f;
392 }
393
394 if(!m_CameraSmoothing)
395 m_CameraSmoothingCenter = m_PrevCenter;
396
397 m_CameraSmoothing = true;
398 }
399
400 if(m_CameraSmoothing)
401 m_Center = m_CameraSmoothingCenter;
402
403 m_PrevCenter = m_Center;
404 m_PrevSpecId = SpecId;
405
406 // demo always count as spectating
407 m_WasSpectating = GameClient()->m_Snap.m_SpecInfo.m_Active;
408}
409
410void CCamera::OnConsoleInit()
411{
412 Console()->Register(pName: "zoom+", pParams: "?f[amount]", Flags: CFGFLAG_CLIENT, pfnFunc: ConZoomPlus, pUser: this, pHelp: "Zoom increase");
413 Console()->Register(pName: "zoom-", pParams: "?f[amount]", Flags: CFGFLAG_CLIENT, pfnFunc: ConZoomMinus, pUser: this, pHelp: "Zoom decrease");
414 Console()->Register(pName: "zoom", pParams: "?f", Flags: CFGFLAG_CLIENT, pfnFunc: ConZoom, pUser: this, pHelp: "Change zoom");
415 Console()->Register(pName: "set_view", pParams: "i[x]i[y]", Flags: CFGFLAG_CLIENT, pfnFunc: ConSetView, pUser: this, pHelp: "Set camera position to x and y in the map");
416 Console()->Register(pName: "set_view_relative", pParams: "i[x]i[y]", Flags: CFGFLAG_CLIENT, pfnFunc: ConSetViewRelative, pUser: this, pHelp: "Set camera position relative to current view in the map");
417 Console()->Register(pName: "goto_switch", pParams: "i[number]?i[offset]", Flags: CFGFLAG_CLIENT, pfnFunc: ConGotoSwitch, pUser: this, pHelp: "View switch found (at offset) with given number");
418 Console()->Register(pName: "goto_tele", pParams: "i[number]?i[offset]", Flags: CFGFLAG_CLIENT, pfnFunc: ConGotoTele, pUser: this, pHelp: "View tele found (at offset) with given number");
419}
420
421void CCamera::OnReset()
422{
423 m_CameraSmoothing = false;
424
425 m_Zoom = CCamera::ZoomStepsToValue(Steps: g_Config.m_ClDefaultZoom - 10);
426 m_Zooming = false;
427 m_AutoSpecCameraZooming = false;
428 m_UserZoomTarget = CCamera::ZoomStepsToValue(Steps: g_Config.m_ClDefaultZoom - 10);
429}
430
431void CCamera::ConZoomPlus(IConsole::IResult *pResult, void *pUserData)
432{
433 CCamera *pSelf = (CCamera *)pUserData;
434 if(!pSelf->ZoomAllowed())
435 return;
436
437 float ZoomAmount = pResult->NumArguments() ? pResult->GetFloat(Index: 0) : 1.0f;
438
439 pSelf->ScaleZoom(Factor: CCamera::ZoomStepsToValue(Steps: ZoomAmount));
440
441 if(pSelf->GameClient()->m_MultiViewActivated)
442 pSelf->GameClient()->m_MultiViewPersonalZoom += ZoomAmount;
443}
444void CCamera::ConZoomMinus(IConsole::IResult *pResult, void *pUserData)
445{
446 CCamera *pSelf = (CCamera *)pUserData;
447 if(!pSelf->ZoomAllowed())
448 return;
449
450 float ZoomAmount = pResult->NumArguments() ? pResult->GetFloat(Index: 0) : 1.0f;
451 ZoomAmount *= -1.0f;
452
453 pSelf->ScaleZoom(Factor: CCamera::ZoomStepsToValue(Steps: ZoomAmount));
454
455 if(pSelf->GameClient()->m_MultiViewActivated)
456 pSelf->GameClient()->m_MultiViewPersonalZoom += ZoomAmount;
457}
458void CCamera::ConZoom(IConsole::IResult *pResult, void *pUserData)
459{
460 CCamera *pSelf = (CCamera *)pUserData;
461 if(!pSelf->ZoomAllowed())
462 return;
463
464 bool IsReset = !pResult->NumArguments();
465
466 float TargetLevel = !IsReset ? pResult->GetFloat(Index: 0) : g_Config.m_ClDefaultZoom;
467
468 if(!pSelf->CanUseAutoSpecCamera() || !pSelf->m_CanUseCameraInfo)
469 pSelf->ChangeZoom(Target: CCamera::ZoomStepsToValue(Steps: TargetLevel - 10.0f), Smoothness: pSelf->GameClient()->m_Snap.m_SpecInfo.m_Active && pSelf->GameClient()->m_MultiViewActivated ? g_Config.m_ClMultiViewZoomSmoothness : g_Config.m_ClSmoothZoomTime, IsUser: true);
470 else
471 pSelf->m_UserZoomTarget = CCamera::ZoomStepsToValue(Steps: TargetLevel - 10.0f);
472
473 pSelf->m_AutoSpecCamera = IsReset;
474
475 if(pSelf->GameClient()->m_MultiViewActivated && pSelf->GameClient()->m_Snap.m_SpecInfo.m_Active)
476 pSelf->GameClient()->m_MultiViewPersonalZoom = TargetLevel - 10.0f;
477}
478void CCamera::ConSetView(IConsole::IResult *pResult, void *pUserData)
479{
480 CCamera *pSelf = (CCamera *)pUserData;
481 // wait until free view camera type to update the position
482 pSelf->SetView(Pos: ivec2(pResult->GetInteger(Index: 0), pResult->GetInteger(Index: 1)));
483}
484void CCamera::ConSetViewRelative(IConsole::IResult *pResult, void *pUserData)
485{
486 CCamera *pSelf = (CCamera *)pUserData;
487 // wait until free view camera type to update the position
488 pSelf->SetView(Pos: ivec2(pResult->GetInteger(Index: 0), pResult->GetInteger(Index: 1)), Relative: true);
489}
490void CCamera::ConGotoSwitch(IConsole::IResult *pResult, void *pUserData)
491{
492 CCamera *pSelf = (CCamera *)pUserData;
493 pSelf->GotoSwitch(Number: pResult->GetInteger(Index: 0), Offset: pResult->NumArguments() > 1 ? pResult->GetInteger(Index: 1) : -1);
494}
495void CCamera::ConGotoTele(IConsole::IResult *pResult, void *pUserData)
496{
497 CCamera *pSelf = (CCamera *)pUserData;
498 pSelf->GotoTele(Number: pResult->GetInteger(Index: 0), Offset: pResult->NumArguments() > 1 ? pResult->GetInteger(Index: 1) : -1);
499}
500
501void CCamera::SetView(ivec2 Pos, bool Relative)
502{
503 vec2 RealPos = vec2(Pos.x * 32.0, Pos.y * 32.0);
504 vec2 UntestedViewPos = Relative ? m_ForceFreeviewPos + RealPos : RealPos;
505
506 m_ForceFreeview = true;
507
508 m_ForceFreeviewPos = vec2(
509 std::clamp(val: UntestedViewPos.x, lo: 200.0f, hi: Collision()->GetWidth() * 32 - 200.0f),
510 std::clamp(val: UntestedViewPos.y, lo: 200.0f, hi: Collision()->GetHeight() * 32 - 200.0f));
511}
512
513void CCamera::GotoSwitch(int Number, int Offset)
514{
515 if(Collision()->SwitchLayer() == nullptr)
516 return;
517
518 int Match = -1;
519 ivec2 MatchPos = ivec2(-1, -1);
520
521 auto FindTile = [this, &Match, &MatchPos, &Number, &Offset]() {
522 for(int x = 0; x < Collision()->GetWidth(); x++)
523 {
524 for(int y = 0; y < Collision()->GetHeight(); y++)
525 {
526 int i = y * Collision()->GetWidth() + x;
527 if(Number == Collision()->GetSwitchNumber(Index: i))
528 {
529 Match++;
530 if(Offset != -1)
531 {
532 if(Match == Offset)
533 {
534 MatchPos = ivec2(x, y);
535 m_GotoSwitchOffset = Match;
536 return;
537 }
538 continue;
539 }
540 MatchPos = ivec2(x, y);
541 if(Match == m_GotoSwitchOffset)
542 return;
543 }
544 }
545 }
546 };
547 FindTile();
548
549 if(MatchPos == ivec2(-1, -1))
550 return;
551 if(Match < m_GotoSwitchOffset)
552 m_GotoSwitchOffset = -1;
553 SetView(Pos: MatchPos);
554 m_GotoSwitchOffset++;
555}
556
557void CCamera::GotoTele(int Number, int Offset)
558{
559 if(Collision()->TeleLayer() == nullptr)
560 return;
561 Number--;
562
563 if(m_GotoTeleLastNumber != Number)
564 m_GotoTeleLastPos = ivec2(-1, -1);
565
566 ivec2 MatchPos = ivec2(-1, -1);
567 const size_t NumTeles = Collision()->TeleAllSize(Number);
568 if(!NumTeles)
569 {
570 log_error("camera", "No teleporter with number %d found.", Number + 1);
571 return;
572 }
573
574 if(Offset != -1 || m_GotoTeleLastPos == ivec2(-1, -1))
575 {
576 if((size_t)Offset >= NumTeles || Offset < 0)
577 Offset = 0;
578 vec2 Tele = Collision()->TeleAllGet(Number, Offset);
579 MatchPos = ivec2(Tele.x / 32, Tele.y / 32);
580 m_GotoTeleOffset = Offset;
581 }
582 else
583 {
584 bool FullRound = false;
585 do
586 {
587 vec2 Tele = Collision()->TeleAllGet(Number, Offset: m_GotoTeleOffset);
588 MatchPos = ivec2(Tele.x / 32, Tele.y / 32);
589 m_GotoTeleOffset++;
590 if((size_t)m_GotoTeleOffset >= NumTeles)
591 {
592 m_GotoTeleOffset = 0;
593 if(FullRound)
594 {
595 MatchPos = m_GotoTeleLastPos;
596 break;
597 }
598 FullRound = true;
599 }
600 } while(distance(a: m_GotoTeleLastPos, b: MatchPos) < 10.0f);
601 }
602
603 if(MatchPos == ivec2(-1, -1))
604 return;
605 m_GotoTeleLastPos = MatchPos;
606 m_GotoTeleLastNumber = Number;
607 SetView(Pos: MatchPos);
608}
609
610void CCamera::SetZoom(float Target, int Smoothness, bool IsUser)
611{
612 ChangeZoom(Target, Smoothness, IsUser);
613}
614
615bool CCamera::ZoomAllowed() const
616{
617 return GameClient()->m_Snap.m_SpecInfo.m_Active ||
618 GameClient()->m_GameInfo.m_AllowZoom ||
619 Client()->State() == IClient::STATE_DEMOPLAYBACK;
620}
621
622int CCamera::Deadzone() const
623{
624 return g_Config.m_ClDyncam ? g_Config.m_ClDyncamDeadzone : g_Config.m_ClMouseDeadzone;
625}
626
627int CCamera::FollowFactor() const
628{
629 return g_Config.m_ClDyncam ? g_Config.m_ClDyncamFollowFactor : g_Config.m_ClMouseFollowfactor;
630}
631
632bool CCamera::CanUseAutoSpecCamera() const
633{
634 if(Client()->State() == IClient::STATE_DEMOPLAYBACK)
635 {
636 // only follow mode has the correct camera info
637 return GameClient()->m_Snap.m_SpecInfo.m_HasCameraInfo && GameClient()->m_DemoSpecId == SPEC_FOLLOW;
638 }
639
640 return g_Config.m_ClSpecAutoSync && GameClient()->m_Snap.m_SpecInfo.m_HasCameraInfo &&
641 GameClient()->m_Snap.m_SpecInfo.m_SpectatorId != GameClient()->m_aLocalIds[0] &&
642 (!GameClient()->Client()->DummyConnected() || GameClient()->m_Snap.m_SpecInfo.m_SpectatorId != GameClient()->m_aLocalIds[1]);
643}
644
645void CCamera::ToggleAutoSpecCamera()
646{
647 if(!g_Config.m_ClSpecAutoSync)
648 {
649 g_Config.m_ClSpecAutoSync = 1;
650 GameClient()->m_Camera.m_AutoSpecCamera = true;
651 }
652 else if(GameClient()->m_Camera.m_AutoSpecCamera && GameClient()->m_Camera.SpectatingPlayer() && GameClient()->m_Camera.CanUseAutoSpecCamera())
653 {
654 GameClient()->m_Camera.m_AutoSpecCamera = false;
655 }
656 else
657 {
658 g_Config.m_ClSpecAutoSync = 0;
659 }
660}
661
662void CCamera::UpdateAutoSpecCameraTooltip()
663{
664 const char *pFeatureText = Localize(pStr: "Auto-sync player camera");
665
666 if(!g_Config.m_ClSpecAutoSync)
667 str_format(buffer: m_aAutoSpecCameraTooltip, buffer_size: sizeof(m_aAutoSpecCameraTooltip), format: "%s: %s", pFeatureText, Localize(pStr: "Disabled", pContext: "Auto camera"));
668 else if(!SpectatingPlayer())
669 str_format(buffer: m_aAutoSpecCameraTooltip, buffer_size: sizeof(m_aAutoSpecCameraTooltip), format: "%s: %s", pFeatureText, Localize(pStr: "Enabled", pContext: "Auto camera"));
670 else if(!CanUseAutoSpecCamera())
671 str_format(buffer: m_aAutoSpecCameraTooltip, buffer_size: sizeof(m_aAutoSpecCameraTooltip), format: "%s: %s", pFeatureText, Localize(pStr: "Unavailable for this player", pContext: "Auto camera"));
672 else if(!m_AutoSpecCamera)
673 str_format(buffer: m_aAutoSpecCameraTooltip, buffer_size: sizeof(m_aAutoSpecCameraTooltip), format: "%s: %s", pFeatureText, Localize(pStr: "Inactive", pContext: "Auto camera"));
674 else
675 str_format(buffer: m_aAutoSpecCameraTooltip, buffer_size: sizeof(m_aAutoSpecCameraTooltip), format: "%s: %s", pFeatureText, Localize(pStr: "Active", pContext: "Auto camera"));
676}
677