1 // SuperTux
2 // Copyright (C) 2006 Matthias Braun <matze@braunis.de>
3 //
4 // This program is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // This program is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with this program. If not, see <http://www.gnu.org/licenses/>.
16
17 #include "object/camera.hpp"
18
19 #include <math.h>
20 #include <physfs.h>
21
22 #include "math/util.hpp"
23 #include "object/player.hpp"
24 #include "supertux/level.hpp"
25 #include "supertux/sector.hpp"
26 #include "util/reader_document.hpp"
27 #include "util/reader_mapping.hpp"
28 #include "util/writer.hpp"
29 #include "video/drawing_context.hpp"
30 #include "video/video_system.hpp"
31 #include "video/viewport.hpp"
32
33 /* this is the fractional distance toward the peek
34 position to move each frame; lower is slower,
35 0 is never get there, 1 is instant */
36 static const float PEEK_ARRIVE_RATIO = 0.1f;
37
38 class CameraConfig final
39 {
40 public:
41 // 0 = No, 1 = Fix, 2 = Mario/Yoshi, 3 = Kirby, 4 = Super Metroid-like
42 int xmode;
43 // as above
44 int ymode;
45 float kirby_rectsize_x;
46 float kirby_rectsize_y;
47 // where to fix the player (used for Yoshi and Fix camera)
48 float target_x;
49 float target_y;
50 // maximum scrolling speed in Y direction
51 float max_speed_x;
52 float max_speed_y;
53 // factor to dynamically increase max_speed_x based on player speed
54 float dynamic_max_speed_x;
55
56 // time the player has to face into the other direction before we assume a
57 // changed direction
58 float dirchange_time;
59 // edge_x
60 float edge_x;
61 // when too change from noscroll mode back to lookahead left/right mode
62 // set to <= 0 to disable noscroll mode
63 float sensitive_x;
64
65 float clamp_x;
66 float clamp_y;
67
68 float dynamic_speed_sm;
69
CameraConfig()70 CameraConfig() :
71 xmode(4),
72 ymode(3),
73 kirby_rectsize_x(0.2f),
74 kirby_rectsize_y(0.34f),
75 target_x(.5f),
76 target_y(.5f),
77 max_speed_x(100),
78 max_speed_y(100),
79 dynamic_max_speed_x(1.0),
80 dirchange_time(0.2f),
81 edge_x(0.4f),
82 sensitive_x(-1),
83 clamp_x(0.1666f),
84 clamp_y(0.3f),
85 dynamic_speed_sm(0.8f)
86 {
87 }
88
load(const std::string & filename)89 void load(const std::string& filename)
90 {
91 auto doc = ReaderDocument::from_file(filename);
92 auto root = doc.get_root();
93 if (root.get_name() == "camera-config")
94 {
95 throw std::runtime_error("file is not a camera config file.");
96 }
97 else
98 {
99 auto camconfig = root.get_mapping();
100
101 camconfig.get("xmode", xmode);
102 camconfig.get("ymode", ymode);
103 camconfig.get("target-x", target_x);
104 camconfig.get("target-y", target_y);
105 camconfig.get("max-speed-x", max_speed_x);
106 camconfig.get("max-speed-y", max_speed_y);
107 camconfig.get("dynamic-max-speed-x", dynamic_max_speed_x);
108 camconfig.get("dirchange-time", dirchange_time);
109 camconfig.get("clamp-x", clamp_x);
110 camconfig.get("clamp-y", clamp_y);
111 camconfig.get("kirby-rectsize-x", kirby_rectsize_x);
112 camconfig.get("kirby-rectsize-y", kirby_rectsize_y);
113 camconfig.get("edge-x", edge_x);
114 camconfig.get("sensitive-x", sensitive_x);
115 camconfig.get("dynamic-speed-sm", dynamic_speed_sm);
116 }
117 }
118 };
119
Camera(const std::string & name)120 Camera::Camera(const std::string& name) :
121 GameObject(name),
122 ExposedObject<Camera, scripting::Camera>(this),
123 m_mode(Mode::NORMAL),
124 m_defaultmode(Mode::NORMAL),
125 m_screen_size(SCREEN_WIDTH, SCREEN_HEIGHT),
126 m_translation(0.0f, 0.0f),
127 m_lookahead_mode(LookaheadMode::NONE),
128 m_changetime(),
129 m_lookahead_pos(0.0f, 0.0f),
130 m_peek_pos(0.0f, 0.0f),
131 m_cached_translation(0.0f, 0.0f),
132 m_shaketimer(),
133 m_shakespeed(),
134 m_shakedepth_x(),
135 m_shakedepth_y(),
136 m_scroll_from(0.0f, 0.0f),
137 m_scroll_goal(0.0f, 0.0f),
138 m_scroll_to_pos(),
139 m_scrollspeed(),
140 m_config(std::make_unique<CameraConfig>()),
141 m_scale(1.f),
142 m_scale_origin(1.f),
143 m_scale_target(1.f),
144 m_scale_time_total(0.f),
145 m_scale_time_remaining(0.f),
146 m_scale_easing()
147 {
148 reload_config();
149 }
150
Camera(const ReaderMapping & reader)151 Camera::Camera(const ReaderMapping& reader) :
152 GameObject(reader),
153 ExposedObject<Camera, scripting::Camera>(this),
154 m_mode(Mode::NORMAL),
155 m_defaultmode(Mode::NORMAL),
156 m_screen_size(SCREEN_WIDTH, SCREEN_HEIGHT),
157 m_translation(0.0f, 0.0f),
158 m_lookahead_mode(LookaheadMode::NONE),
159 m_changetime(),
160 m_lookahead_pos(0.0f, 0.0f),
161 m_peek_pos(0.0f, 0.0f),
162 m_cached_translation(0.0f, 0.0f),
163 m_shaketimer(),
164 m_shakespeed(),
165 m_shakedepth_x(),
166 m_shakedepth_y(),
167 m_scroll_from(0.0f, 0.0f),
168 m_scroll_goal(0.0f, 0.0f),
169 m_scroll_to_pos(),
170 m_scrollspeed(),
171 m_config(std::make_unique<CameraConfig>()),
172 m_scale(1.f),
173 m_scale_origin(1.f),
174 m_scale_target(1.f),
175 m_scale_time_total(0.f),
176 m_scale_time_remaining(0.f),
177 m_scale_easing()
178 {
179 std::string modename;
180
181 reader.get("mode", modename);
182 if (modename == "normal")
183 {
184 m_mode = Mode::NORMAL;
185 }
186 else if (modename == "autoscroll")
187 {
188 m_mode = Mode::AUTOSCROLL;
189
190 init_path(reader, true);
191 }
192 else if (modename == "manual")
193 {
194 m_mode = Mode::MANUAL;
195 }
196 else
197 {
198 m_mode = Mode::NORMAL;
199 log_warning << "invalid camera mode '" << modename << "'found in worldfile." << std::endl;
200 }
201 m_defaultmode = m_mode;
202
203 if (m_name.empty()) {
204 m_name = "Camera";
205 }
206
207 reload_config();
208 }
209
~Camera()210 Camera::~Camera()
211 {
212 }
213
214 ObjectSettings
get_settings()215 Camera::get_settings()
216 {
217 ObjectSettings result = GameObject::get_settings();
218
219 result.add_enum(_("Mode"), reinterpret_cast<int*>(&m_defaultmode),
220 {_("normal"), _("manual"), _("autoscroll")},
221 {"normal", "manual", "autoscroll"},
222 {}, "mode");
223
224 result.add_path_ref(_("Path"), *this, get_path_ref(), "path-ref");
225
226 if (get_walker() && get_path()->is_valid()) {
227 result.add_walk_mode(_("Path Mode"), &get_path()->m_mode, {}, {});
228 result.add_bool(_("Adapt Speed"), &get_path()->m_adapt_speed, {}, {});
229 }
230
231 return result;
232 }
233
234 void
after_editor_set()235 Camera::after_editor_set()
236 {
237 if (get_walker() && get_path()->is_valid()) {
238 if (m_defaultmode != Mode::AUTOSCROLL) {
239 get_path()->m_nodes.clear();
240 auto path_obj = get_path_gameobject();
241 if(path_obj != nullptr)
242 {
243 path_obj->editor_delete();
244 }
245 }
246 } else {
247 if (m_defaultmode == Mode::AUTOSCROLL) {
248 init_path_pos(Vector(0,0));
249 }
250 }
251 }
252
253 const Vector
get_translation() const254 Camera::get_translation() const
255 {
256 Vector screen_size = Sizef(m_screen_size).as_vector();
257 return m_translation + ((screen_size * (m_scale - 1.f)) / 2.f);
258 }
259
260 void
reset(const Vector & tuxpos)261 Camera::reset(const Vector& tuxpos)
262 {
263 m_translation.x = tuxpos.x - static_cast<float>(m_screen_size.width) / 2.0f;
264 m_translation.y = tuxpos.y - static_cast<float>(m_screen_size.height) / 2.0f;
265
266 m_shakespeed = 0;
267 m_shaketimer.stop();
268 keep_in_bounds(m_translation);
269
270 m_cached_translation = m_translation;
271 }
272
273 void
shake(float duration,float x,float y)274 Camera::shake(float duration, float x, float y)
275 {
276 m_shaketimer.start(duration);
277 m_shakedepth_x = x;
278 m_shakedepth_y = y;
279 m_shakespeed = math::PI_2 / duration;
280 }
281
282 void
scroll_to(const Vector & goal,float scrolltime)283 Camera::scroll_to(const Vector& goal, float scrolltime)
284 {
285 if(scrolltime == 0.0f)
286 {
287 m_translation.x = goal.x;
288 m_translation.y = goal.y;
289 m_mode = Mode::MANUAL;
290 return;
291 }
292
293 m_scroll_from = m_translation;
294 m_scroll_goal = goal;
295 keep_in_bounds(m_scroll_goal);
296
297 m_scroll_to_pos = 0;
298 m_scrollspeed = 1.f / scrolltime;
299 m_mode = Mode::SCROLLTO;
300 }
301
302 static const float CAMERA_EPSILON = .00001f;
303
304 void
draw(DrawingContext & context)305 Camera::draw(DrawingContext& context)
306 {
307 m_screen_size = Size(context.get_width(),
308 context.get_height());
309 }
310
311 void
update(float dt_sec)312 Camera::update(float dt_sec)
313 {
314 switch (m_mode) {
315 case Mode::NORMAL:
316 update_scroll_normal(dt_sec);
317 break;
318 case Mode::AUTOSCROLL:
319 update_scroll_autoscroll(dt_sec);
320 break;
321 case Mode::SCROLLTO:
322 update_scroll_to(dt_sec);
323 break;
324 default:
325 break;
326 }
327 update_scale(dt_sec);
328 shake();
329 }
330
331 void
reload_config()332 Camera::reload_config()
333 {
334 if (PHYSFS_exists("camera.cfg")) {
335 try {
336 m_config->load("camera.cfg");
337 log_info << "Loaded camera.cfg." << std::endl;
338 } catch(std::exception &e) {
339 log_debug << "Couldn't load camera.cfg, using defaults ("
340 << e.what() << ")" << std::endl;
341 }
342 }
343 }
344
345 void
keep_in_bounds(Vector & translation_)346 Camera::keep_in_bounds(Vector& translation_)
347 {
348 float width = d_sector->get_width();
349 float height = d_sector->get_height();
350
351 // don't scroll before the start or after the level's end
352 translation_.x = math::clamp(translation_.x, 0.0f, width - static_cast<float>(m_screen_size.width));
353 translation_.y = math::clamp(translation_.y, 0.0f, height - static_cast<float>(m_screen_size.height));
354
355 if (height < static_cast<float>(m_screen_size.height))
356 translation_.y = height / 2.0f - static_cast<float>(m_screen_size.height) / 2.0f;
357 if (width < static_cast<float>(m_screen_size.width))
358 translation_.x = width / 2.0f - static_cast<float>(m_screen_size.width) / 2.0f;
359 }
360
361 void
shake()362 Camera::shake()
363 {
364 if (m_shaketimer.started()) {
365 m_translation.x -= sinf(m_shaketimer.get_timegone() * m_shakespeed) * m_shakedepth_x;
366 m_translation.y -= sinf(m_shaketimer.get_timegone() * m_shakespeed) * m_shakedepth_y;
367 }
368 }
369
370 void
update_scroll_normal(float dt_sec)371 Camera::update_scroll_normal(float dt_sec)
372 {
373 const auto& config_ = *(m_config);
374 Player& player = d_sector->get_player();
375 // TODO: co-op mode needs a good camera
376 Vector player_pos(player.get_bbox().get_left(),
377 player.get_bbox().get_bottom());
378 static Vector last_player_pos = player_pos;
379 Vector player_delta = player_pos - last_player_pos;
380 last_player_pos = player_pos;
381
382 // check that we don't have division by zero later
383 if (dt_sec < CAMERA_EPSILON)
384 return;
385
386 /****** Vertical Scrolling part ******/
387 int ymode = config_.ymode;
388
389 if (player.is_dying() || d_sector->get_height() == 19*32) {
390 ymode = 0;
391 }
392 if (ymode == 1) {
393 m_cached_translation.y = player_pos.y - static_cast<float>(m_screen_size.height) * config_.target_y;
394 }
395 if (ymode == 2) {
396 // target_y is the height we target our scrolling at. This is not always the
397 // height of the player: while jumping upwards, we should use the
398 // position where they last touched the ground. (this probably needs
399 // exceptions for trampolines and similar things in the future)
400 float target_y;
401 if (player.m_fall_mode == Player::JUMPING)
402 target_y = player.m_last_ground_y + player.get_bbox().get_height();
403 else
404 target_y = player.get_bbox().get_bottom();
405 target_y -= static_cast<float>(static_cast<float>(m_screen_size.height)) * config_.target_y;
406
407 // delta_y is the distance we'd have to travel to directly reach target_y
408 float delta_y = m_cached_translation.y - target_y;
409 // speed is the speed the camera would need to reach target_y in this frame
410 float speed_y = delta_y / dt_sec;
411
412 // limit the camera speed when jumping upwards
413 if (player.m_fall_mode != Player::FALLING
414 && player.m_fall_mode != Player::TRAMPOLINE_JUMP) {
415 speed_y = math::clamp(speed_y, -config_.max_speed_y, config_.max_speed_y);
416 }
417
418 // scroll with calculated speed
419 m_cached_translation.y -= speed_y * dt_sec;
420 }
421 if (ymode == 3) {
422 float halfsize = config_.kirby_rectsize_y * 0.5f;
423 m_cached_translation.y = math::clamp(m_cached_translation.y,
424 player_pos.y - static_cast<float>(m_screen_size.height) * (0.5f + halfsize),
425 player_pos.y - static_cast<float>(m_screen_size.height) * (0.5f - halfsize));
426 }
427 if (ymode == 4) {
428 float upperend = static_cast<float>(m_screen_size.height) * config_.edge_x;
429 float lowerend = static_cast<float>(m_screen_size.height) * (1 - config_.edge_x);
430
431 if (player_delta.y < -CAMERA_EPSILON) {
432 // walking left
433 m_lookahead_pos.y -= player_delta.y * config_.dynamic_speed_sm;
434
435 if (m_lookahead_pos.y > lowerend) {
436 m_lookahead_pos.y = lowerend;
437 }
438 } else if (player_delta.y > CAMERA_EPSILON) {
439 // walking right
440 m_lookahead_pos.y -= player_delta.y * config_.dynamic_speed_sm;
441 if (m_lookahead_pos.y < upperend) {
442 m_lookahead_pos.y = upperend;
443 }
444 }
445
446 // adjust for level ends
447 if (player_pos.y < upperend) {
448 m_lookahead_pos.y = upperend;
449 }
450 if (player_pos.y > d_sector->get_width() - upperend) {
451 m_lookahead_pos.y = lowerend;
452 }
453
454 m_cached_translation.y = player_pos.y - m_lookahead_pos.y;
455 }
456
457 m_translation.y = m_cached_translation.y;
458
459 if (ymode != 0) {
460 float top_edge, bottom_edge;
461 if (config_.clamp_y <= 0) {
462 top_edge = 0;
463 bottom_edge = static_cast<float>(m_screen_size.height);
464 } else {
465 top_edge = static_cast<float>(m_screen_size.height) * config_.clamp_y;
466 bottom_edge = static_cast<float>(m_screen_size.height) * (1.0f - config_.clamp_y);
467 }
468
469 float peek_to = 0;
470 float translation_compensation = player_pos.y - m_translation.y;
471
472 if (player.peeking_direction_y() == Direction::UP) {
473 peek_to = bottom_edge - translation_compensation;
474 } else if (player.peeking_direction_y() == Direction::DOWN) {
475 peek_to = top_edge - translation_compensation;
476 }
477
478 float peek_move = (peek_to - m_peek_pos.y) * PEEK_ARRIVE_RATIO;
479 if (fabsf(peek_move) < 1.0f) {
480 peek_move = 0.0;
481 }
482
483 m_peek_pos.y += peek_move;
484
485 m_translation.y -= m_peek_pos.y;
486
487 if (config_.clamp_y > 0.0f) {
488 m_translation.y = math::clamp(m_translation.y,
489 player_pos.y - static_cast<float>(m_screen_size.height) * (1.0f - config_.clamp_y),
490 player_pos.y - static_cast<float>(m_screen_size.height) * config_.clamp_y);
491 m_cached_translation.y = math::clamp(m_cached_translation.y,
492 player_pos.y - static_cast<float>(m_screen_size.height) * (1.0f - config_.clamp_y),
493 player_pos.y - static_cast<float>(m_screen_size.height) * config_.clamp_y);
494 }
495 }
496
497 /****** Horizontal scrolling part *******/
498 int xmode = config_.xmode;
499
500 if (player.is_dying())
501 xmode = 0;
502
503 if (xmode == 1) {
504 m_cached_translation.x = player_pos.x - static_cast<float>(m_screen_size.width) * config_.target_x;
505 }
506 if (xmode == 2) {
507 // our camera is either in leftscrolling, rightscrolling or
508 // nonscrollingmode.
509 //
510 // when suddenly changing directions while scrolling into the other
511 // direction abort scrolling, since tux might be going left/right at a
512 // relatively small part of the map (like when jumping upwards)
513
514 // Find out direction in which the player moves
515 LookaheadMode walkDirection;
516 if (player_delta.x < -CAMERA_EPSILON) walkDirection = LookaheadMode::LEFT;
517 else if (player_delta.x > CAMERA_EPSILON) walkDirection = LookaheadMode::RIGHT;
518 else if (player.m_dir == Direction::LEFT) walkDirection = LookaheadMode::LEFT;
519 else walkDirection = LookaheadMode::RIGHT;
520
521 float LEFTEND, RIGHTEND;
522 if (config_.sensitive_x > 0) {
523 LEFTEND = static_cast<float>(m_screen_size.width) * config_.sensitive_x;
524 RIGHTEND = static_cast<float>(m_screen_size.width) * (1-config_.sensitive_x);
525 } else {
526 LEFTEND = static_cast<float>(m_screen_size.width);
527 RIGHTEND = 0.0f;
528 }
529
530 if (m_lookahead_mode == LookaheadMode::NONE) {
531 /* if we're undecided then look if we crossed the left or right
532 * "sensitive" area */
533 if (player_pos.x < m_cached_translation.x + LEFTEND) {
534 m_lookahead_mode = LookaheadMode::LEFT;
535 } else if (player_pos.x > m_cached_translation.x + RIGHTEND) {
536 m_lookahead_mode = LookaheadMode::RIGHT;
537 }
538 /* at the ends of a level it's obvious which way we will go */
539 if (player_pos.x < static_cast<float>(m_screen_size.width) * 0.5f) {
540 m_lookahead_mode = LookaheadMode::RIGHT;
541 } else if (player_pos.x >= d_sector->get_width() - static_cast<float>(m_screen_size.width) * 0.5f) {
542 m_lookahead_mode = LookaheadMode::LEFT;
543 }
544
545 m_changetime = -1;
546 } else if (m_lookahead_mode != walkDirection) {
547 /* Tux changed direction while camera was scrolling...
548 * he has to do this for a certain time to add robustness against
549 * sudden changes */
550 if (m_changetime < 0) {
551 m_changetime = g_game_time;
552 } else if (g_game_time - m_changetime > config_.dirchange_time) {
553 if (m_lookahead_mode == LookaheadMode::LEFT &&
554 player_pos.x > m_cached_translation.x + RIGHTEND) {
555 m_lookahead_mode = LookaheadMode::RIGHT;
556 } else if (m_lookahead_mode == LookaheadMode::RIGHT &&
557 player_pos.x < m_cached_translation.x + LEFTEND) {
558 m_lookahead_mode = LookaheadMode::LEFT;
559 } else {
560 m_lookahead_mode = LookaheadMode::NONE;
561 }
562 }
563 } else {
564 m_changetime = -1;
565 }
566
567 LEFTEND = static_cast<float>(m_screen_size.width) * config_.edge_x;
568 RIGHTEND = static_cast<float>(m_screen_size.width) * (1-config_.edge_x);
569
570 // calculate our scroll target depending on scroll mode
571 float target_x;
572 if (m_lookahead_mode == LookaheadMode::LEFT)
573 target_x = player_pos.x - RIGHTEND;
574 else if (m_lookahead_mode == LookaheadMode::RIGHT)
575 target_x = player_pos.x - LEFTEND;
576 else
577 target_x = m_cached_translation.x;
578
579 // that's the distance we would have to travel to reach target_x
580 float delta_x = m_cached_translation.x - target_x;
581 // the speed we'd need to travel to reach target_x in this frame
582 float speed_x = delta_x / dt_sec;
583
584 // limit our speed
585 float player_speed_x = player_delta.x / dt_sec;
586 float maxv = config_.max_speed_x + (fabsf(player_speed_x * config_.dynamic_max_speed_x));
587 speed_x = math::clamp(speed_x, -maxv, maxv);
588
589 // apply scrolling
590 m_cached_translation.x -= speed_x * dt_sec;
591 }
592 if (xmode == 3) {
593 float halfsize = config_.kirby_rectsize_x * 0.5f;
594 m_cached_translation.x = math::clamp(m_cached_translation.x,
595 player_pos.x - static_cast<float>(m_screen_size.width) * (0.5f + halfsize),
596 player_pos.x - static_cast<float>(m_screen_size.width) * (0.5f - halfsize));
597 }
598 if (xmode == 4) {
599 float LEFTEND = static_cast<float>(m_screen_size.width) * config_.edge_x;
600 float RIGHTEND = static_cast<float>(m_screen_size.width) * (1 - config_.edge_x);
601
602 if (player_delta.x < -CAMERA_EPSILON) {
603 // walking left
604 m_lookahead_pos.x -= player_delta.x * config_.dynamic_speed_sm;
605 if (m_lookahead_pos.x > RIGHTEND) {
606 m_lookahead_pos.x = RIGHTEND;
607 }
608
609 } else if (player_delta.x > CAMERA_EPSILON) {
610 // walking right
611 m_lookahead_pos.x -= player_delta.x * config_.dynamic_speed_sm;
612 if (m_lookahead_pos.x < LEFTEND) {
613 m_lookahead_pos.x = LEFTEND;
614 }
615 }
616
617 if (m_lookahead_pos.x > RIGHTEND) {
618 m_lookahead_pos.x = RIGHTEND;
619 }
620 if (m_lookahead_pos.x < LEFTEND) {
621 m_lookahead_pos.x = LEFTEND;
622 }
623
624 // adjust for level ends
625 if (player_pos.x < LEFTEND) {
626 m_lookahead_pos.x = LEFTEND;
627 }
628 if (player_pos.x > d_sector->get_width() - LEFTEND) {
629 m_lookahead_pos.x = RIGHTEND;
630 }
631
632 m_cached_translation.x = player_pos.x - m_lookahead_pos.x;
633 }
634
635 m_translation.x = m_cached_translation.x;
636
637 if (xmode != 0) {
638 float left_edge, right_edge;
639 if (config_.clamp_x <= 0) {
640 left_edge = 0;
641 right_edge = static_cast<float>(m_screen_size.width);
642 } else {
643 left_edge = static_cast<float>(m_screen_size.width) * config_.clamp_x;
644 right_edge = static_cast<float>(m_screen_size.width) * (1.0f - config_.clamp_x);
645 }
646
647 float peek_to = 0;
648 float translation_compensation = player_pos.x - m_translation.x;
649
650 if (player.peeking_direction_x() == ::Direction::LEFT) {
651 peek_to = right_edge - translation_compensation;
652 } else if (player.peeking_direction_x() == Direction::RIGHT) {
653 peek_to = left_edge - translation_compensation;
654 }
655
656 float peek_move = (peek_to - m_peek_pos.x) * PEEK_ARRIVE_RATIO;
657 if (fabsf(peek_move) < 1.0f) {
658 peek_move = 0.0f;
659 }
660
661 m_peek_pos.x += peek_move;
662
663 m_translation.x -= m_peek_pos.x;
664
665 if (config_.clamp_x > 0.0f) {
666 m_translation.x = math::clamp(m_translation.x,
667 player_pos.x - static_cast<float>(m_screen_size.width) * (1-config_.clamp_x),
668 player_pos.x - static_cast<float>(m_screen_size.width) * config_.clamp_x);
669
670 m_cached_translation.x = math::clamp(m_cached_translation.x,
671 player_pos.x - static_cast<float>(m_screen_size.width) * (1-config_.clamp_x),
672 player_pos.x - static_cast<float>(m_screen_size.width) * config_.clamp_x);
673 }
674 }
675
676 keep_in_bounds(m_translation);
677 keep_in_bounds(m_cached_translation);
678 }
679
680 void
update_scroll_autoscroll(float dt_sec)681 Camera::update_scroll_autoscroll(float dt_sec)
682 {
683 Player& player = d_sector->get_player();
684 if (player.is_dying())
685 return;
686
687 get_walker()->update(dt_sec);
688 m_translation = get_walker()->get_pos();
689
690 keep_in_bounds(m_translation);
691 }
692
693 void
update_scroll_to(float dt_sec)694 Camera::update_scroll_to(float dt_sec)
695 {
696 m_scroll_to_pos += dt_sec * m_scrollspeed;
697 if (m_scroll_to_pos >= 1.0f) {
698 m_mode = Mode::MANUAL;
699 m_translation = m_scroll_goal;
700 return;
701 }
702
703 m_translation = m_scroll_from + (m_scroll_goal - m_scroll_from) * m_scroll_to_pos;
704 }
705
706 void
update_scale(float dt_sec)707 Camera::update_scale(float dt_sec)
708 {
709 if (m_scale_time_remaining > 0.f)
710 {
711 m_scale_time_remaining -= dt_sec;
712
713 if (m_scale_time_remaining <= 0.f)
714 {
715 m_scale = m_scale_target;
716 m_scale_time_remaining = 0.f;
717 }
718 else
719 {
720 float progress = (m_scale_time_total - m_scale_time_remaining)
721 / m_scale_time_total;
722 float true_progress = static_cast<float>(m_scale_easing(
723 static_cast<double>(progress)));
724 m_scale = m_scale_origin +
725 (m_scale_target - m_scale_origin) * true_progress;
726 }
727
728 // Re-center camera when zooming
729 m_lookahead_pos /= 1.01f;
730 }
731
732 Vector screen_size = Sizef(m_screen_size).as_vector();
733 m_translation += screen_size * (1.f - m_scale) / 2.f;
734 }
735
736 void
ease_scale(float scale,float time,easing ease)737 Camera::ease_scale(float scale, float time, easing ease)
738 {
739 if (time <= 0.f) {
740 m_scale = scale;
741 } else {
742 m_scale_origin = m_scale;
743 m_scale_target = scale;
744 m_scale_time_total = time;
745 m_scale_time_remaining = time;
746 m_scale_easing = ease;
747 }
748 }
749
750 Vector
get_center() const751 Camera::get_center() const
752 {
753 return m_translation + Vector(static_cast<float>(m_screen_size.width) / 2.0f,
754 static_cast<float>(m_screen_size.height) / 2.0f);
755 }
756
757 void
move(const int dx,const int dy)758 Camera::move(const int dx, const int dy)
759 {
760 m_translation.x += static_cast<float>(dx);
761 m_translation.y += static_cast<float>(dy);
762 }
763
764 bool
is_saveable() const765 Camera::is_saveable() const
766 {
767 return !(Level::current() &&
768 Level::current()->is_worldmap());
769 }
770 /* EOF */
771