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