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 */
36static const float PEEK_ARRIVE_RATIO = 0.1f;
37
38class CameraConfig final
39{
40public:
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
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
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
120Camera::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(),
127 m_lookahead_mode(LookaheadMode::NONE),
128 m_changetime(),
129 m_lookahead_pos(),
130 m_peek_pos(),
131 m_cached_translation(),
132 m_shaketimer(),
133 m_shakespeed(),
134 m_shakedepth_x(),
135 m_shakedepth_y(),
136 m_scroll_from(),
137 m_scroll_goal(),
138 m_scroll_to_pos(),
139 m_scrollspeed(),
140 m_config(std::make_unique<CameraConfig>())
141{
142 reload_config();
143}
144
145Camera::Camera(const ReaderMapping& reader) :
146 GameObject(reader),
147 ExposedObject<Camera, scripting::Camera>(this),
148 m_mode(Mode::NORMAL),
149 m_defaultmode(Mode::NORMAL),
150 m_screen_size(SCREEN_WIDTH, SCREEN_HEIGHT),
151 m_translation(),
152 m_lookahead_mode(LookaheadMode::NONE),
153 m_changetime(),
154 m_lookahead_pos(),
155 m_peek_pos(),
156 m_cached_translation(),
157 m_shaketimer(),
158 m_shakespeed(),
159 m_shakedepth_x(),
160 m_shakedepth_y(),
161 m_scroll_from(),
162 m_scroll_goal(),
163 m_scroll_to_pos(),
164 m_scrollspeed(),
165 m_config(std::make_unique<CameraConfig>())
166{
167 std::string modename;
168
169 reader.get("mode", modename);
170 if (modename == "normal")
171 {
172 m_mode = Mode::NORMAL;
173 }
174 else if (modename == "autoscroll")
175 {
176 m_mode = Mode::AUTOSCROLL;
177
178 init_path(reader, true);
179 }
180 else if (modename == "manual")
181 {
182 m_mode = Mode::MANUAL;
183 }
184 else
185 {
186 m_mode = Mode::NORMAL;
187 log_warning << "invalid camera mode '" << modename << "'found in worldfile." << std::endl;
188 }
189 m_defaultmode = m_mode;
190
191 if (m_name.empty()) {
192 m_name = "Camera";
193 }
194
195 reload_config();
196}
197
198Camera::~Camera()
199{
200}
201
202ObjectSettings
203Camera::get_settings()
204{
205 ObjectSettings result = GameObject::get_settings();
206
207 result.add_enum(_("Mode"), reinterpret_cast<int*>(&m_defaultmode),
208 {_("normal"), _("manual"), _("autoscroll")},
209 {"normal", "manual", "autoscroll"},
210 {}, "mode");
211
212 result.add_path_ref(_("Path"), get_path_ref(), "path-ref");
213
214 if (get_walker() && get_path()->is_valid()) {
215 result.add_walk_mode(_("Path Mode"), &get_path()->m_mode, {}, {});
216 }
217
218 return result;
219}
220
221void
222Camera::after_editor_set()
223{
224 if (get_walker() && get_path()->is_valid()) {
225 if (m_defaultmode != Mode::AUTOSCROLL) {
226 get_path()->m_nodes.clear();
227 }
228 } else {
229 if (m_defaultmode == Mode::AUTOSCROLL) {
230 init_path_pos(Vector(0,0));
231 }
232 }
233}
234
235const Vector&
236Camera::get_translation() const
237{
238 return m_translation;
239}
240
241void
242Camera::reset(const Vector& tuxpos)
243{
244 m_translation.x = tuxpos.x - static_cast<float>(m_screen_size.width) / 2.0f;
245 m_translation.y = tuxpos.y - static_cast<float>(m_screen_size.height) / 2.0f;
246
247 m_shakespeed = 0;
248 m_shaketimer.stop();
249 keep_in_bounds(m_translation);
250
251 m_cached_translation = m_translation;
252}
253
254void
255Camera::shake(float duration, float x, float y)
256{
257 m_shaketimer.start(duration);
258 m_shakedepth_x = x;
259 m_shakedepth_y = y;
260 m_shakespeed = math::PI_2 / duration;
261}
262
263void
264Camera::scroll_to(const Vector& goal, float scrolltime)
265{
266 if(scrolltime == 0.0f)
267 {
268 m_translation.x = goal.x;
269 m_translation.y = goal.y;
270 m_mode = Mode::MANUAL;
271 return;
272 }
273
274 m_scroll_from = m_translation;
275 m_scroll_goal = goal;
276 keep_in_bounds(m_scroll_goal);
277
278 m_scroll_to_pos = 0;
279 m_scrollspeed = 1.f / scrolltime;
280 m_mode = Mode::SCROLLTO;
281}
282
283static const float CAMERA_EPSILON = .00001f;
284
285void
286Camera::draw(DrawingContext& context)
287{
288 m_screen_size = Size(context.get_width(),
289 context.get_height());
290}
291
292void
293Camera::update(float dt_sec)
294{
295 switch (m_mode) {
296 case Mode::NORMAL:
297 update_scroll_normal(dt_sec);
298 break;
299 case Mode::AUTOSCROLL:
300 update_scroll_autoscroll(dt_sec);
301 break;
302 case Mode::SCROLLTO:
303 update_scroll_to(dt_sec);
304 break;
305 default:
306 break;
307 }
308 shake();
309}
310
311void
312Camera::reload_config()
313{
314 if (PHYSFS_exists("camera.cfg")) {
315 try {
316 m_config->load("camera.cfg");
317 log_info << "Loaded camera.cfg." << std::endl;
318 } catch(std::exception &e) {
319 log_debug << "Couldn't load camera.cfg, using defaults ("
320 << e.what() << ")" << std::endl;
321 }
322 }
323}
324
325void
326Camera::keep_in_bounds(Vector& translation_)
327{
328 float width = d_sector->get_width();
329 float height = d_sector->get_height();
330
331 // don't scroll before the start or after the level's end
332 translation_.x = math::clamp(translation_.x, 0.0f, width - static_cast<float>(m_screen_size.width));
333 translation_.y = math::clamp(translation_.y, 0.0f, height - static_cast<float>(m_screen_size.height));
334
335 if (height < static_cast<float>(m_screen_size.height))
336 translation_.y = height / 2.0f - static_cast<float>(m_screen_size.height) / 2.0f;
337 if (width < static_cast<float>(m_screen_size.width))
338 translation_.x = width / 2.0f - static_cast<float>(m_screen_size.width) / 2.0f;
339}
340
341void
342Camera::shake()
343{
344 if (m_shaketimer.started()) {
345 m_translation.x -= sinf(m_shaketimer.get_timegone() * m_shakespeed) * m_shakedepth_x;
346 m_translation.y -= sinf(m_shaketimer.get_timegone() * m_shakespeed) * m_shakedepth_y;
347 }
348}
349
350void
351Camera::update_scroll_normal(float dt_sec)
352{
353 const auto& config_ = *(m_config);
354 Player& player = d_sector->get_player();
355 // TODO: co-op mode needs a good camera
356 Vector player_pos(player.get_bbox().get_middle().x,
357 player.get_bbox().get_bottom());
358 static Vector last_player_pos = player_pos;
359 Vector player_delta = player_pos - last_player_pos;
360 last_player_pos = player_pos;
361
362 // check that we don't have division by zero later
363 if (dt_sec < CAMERA_EPSILON)
364 return;
365
366 /****** Vertical Scrolling part ******/
367 int ymode = config_.ymode;
368
369 if (player.is_dying() || d_sector->get_height() == 19*32) {
370 ymode = 0;
371 }
372 if (ymode == 1) {
373 m_cached_translation.y = player_pos.y - static_cast<float>(m_screen_size.height) * config_.target_y;
374 }
375 if (ymode == 2) {
376 // target_y is the height we target our scrolling at. This is not always the
377 // height of the player: while jumping upwards, we should use the
378 // position where they last touched the ground. (this probably needs
379 // exceptions for trampolines and similar things in the future)
380 float target_y;
381 if (player.m_fall_mode == Player::JUMPING)
382 target_y = player.m_last_ground_y + player.get_bbox().get_height();
383 else
384 target_y = player.get_bbox().get_bottom();
385 target_y -= static_cast<float>(static_cast<float>(m_screen_size.height)) * config_.target_y;
386
387 // delta_y is the distance we'd have to travel to directly reach target_y
388 float delta_y = m_cached_translation.y - target_y;
389 // speed is the speed the camera would need to reach target_y in this frame
390 float speed_y = delta_y / dt_sec;
391
392 // limit the camera speed when jumping upwards
393 if (player.m_fall_mode != Player::FALLING
394 && player.m_fall_mode != Player::TRAMPOLINE_JUMP) {
395 speed_y = math::clamp(speed_y, -config_.max_speed_y, config_.max_speed_y);
396 }
397
398 // scroll with calculated speed
399 m_cached_translation.y -= speed_y * dt_sec;
400 }
401 if (ymode == 3) {
402 float halfsize = config_.kirby_rectsize_y * 0.5f;
403 m_cached_translation.y = math::clamp(m_cached_translation.y,
404 player_pos.y - static_cast<float>(m_screen_size.height) * (0.5f + halfsize),
405 player_pos.y - static_cast<float>(m_screen_size.height) * (0.5f - halfsize));
406 }
407 if (ymode == 4) {
408 float upperend = static_cast<float>(m_screen_size.height) * config_.edge_x;
409 float lowerend = static_cast<float>(m_screen_size.height) * (1 - config_.edge_x);
410
411 if (player_delta.y < -CAMERA_EPSILON) {
412 // walking left
413 m_lookahead_pos.y -= player_delta.y * config_.dynamic_speed_sm;
414
415 if (m_lookahead_pos.y > lowerend) {
416 m_lookahead_pos.y = lowerend;
417 }
418 } else if (player_delta.y > CAMERA_EPSILON) {
419 // walking right
420 m_lookahead_pos.y -= player_delta.y * config_.dynamic_speed_sm;
421 if (m_lookahead_pos.y < upperend) {
422 m_lookahead_pos.y = upperend;
423 }
424 }
425
426 // adjust for level ends
427 if (player_pos.y < upperend) {
428 m_lookahead_pos.y = upperend;
429 }
430 if (player_pos.y > d_sector->get_width() - upperend) {
431 m_lookahead_pos.y = lowerend;
432 }
433
434 m_cached_translation.y = player_pos.y - m_lookahead_pos.y;
435 }
436
437 m_translation.y = m_cached_translation.y;
438
439 if (ymode != 0) {
440 float top_edge, bottom_edge;
441 if (config_.clamp_y <= 0) {
442 top_edge = 0;
443 bottom_edge = static_cast<float>(m_screen_size.height);
444 } else {
445 top_edge = static_cast<float>(m_screen_size.height) * config_.clamp_y;
446 bottom_edge = static_cast<float>(m_screen_size.height) * (1.0f - config_.clamp_y);
447 }
448
449 float peek_to = 0;
450 float translation_compensation = player_pos.y - m_translation.y;
451
452 if (player.peeking_direction_y() == Direction::UP) {
453 peek_to = bottom_edge - translation_compensation;
454 } else if (player.peeking_direction_y() == Direction::DOWN) {
455 peek_to = top_edge - translation_compensation;
456 }
457
458 float peek_move = (peek_to - m_peek_pos.y) * PEEK_ARRIVE_RATIO;
459 if (fabsf(peek_move) < 1.0f) {
460 peek_move = 0.0;
461 }
462
463 m_peek_pos.y += peek_move;
464
465 m_translation.y -= m_peek_pos.y;
466
467 if (config_.clamp_y > 0.0f) {
468 m_translation.y = math::clamp(m_translation.y,
469 player_pos.y - static_cast<float>(m_screen_size.height) * (1.0f - config_.clamp_y),
470 player_pos.y - static_cast<float>(m_screen_size.height) * config_.clamp_y);
471 m_cached_translation.y = math::clamp(m_cached_translation.y,
472 player_pos.y - static_cast<float>(m_screen_size.height) * (1.0f - config_.clamp_y),
473 player_pos.y - static_cast<float>(m_screen_size.height) * config_.clamp_y);
474 }
475 }
476
477 /****** Horizontal scrolling part *******/
478 int xmode = config_.xmode;
479
480 if (player.is_dying())
481 xmode = 0;
482
483 if (xmode == 1) {
484 m_cached_translation.x = player_pos.x - static_cast<float>(m_screen_size.width) * config_.target_x;
485 }
486 if (xmode == 2) {
487 // our camera is either in leftscrolling, rightscrolling or
488 // nonscrollingmode.
489 //
490 // when suddenly changing directions while scrolling into the other
491 // direction abort scrolling, since tux might be going left/right at a
492 // relatively small part of the map (like when jumping upwards)
493
494 // Find out direction in which the player moves
495 LookaheadMode walkDirection;
496 if (player_delta.x < -CAMERA_EPSILON) walkDirection = LookaheadMode::LEFT;
497 else if (player_delta.x > CAMERA_EPSILON) walkDirection = LookaheadMode::RIGHT;
498 else if (player.m_dir == Direction::LEFT) walkDirection = LookaheadMode::LEFT;
499 else walkDirection = LookaheadMode::RIGHT;
500
501 float LEFTEND, RIGHTEND;
502 if (config_.sensitive_x > 0) {
503 LEFTEND = static_cast<float>(m_screen_size.width) * config_.sensitive_x;
504 RIGHTEND = static_cast<float>(m_screen_size.width) * (1-config_.sensitive_x);
505 } else {
506 LEFTEND = static_cast<float>(m_screen_size.width);
507 RIGHTEND = 0.0f;
508 }
509
510 if (m_lookahead_mode == LookaheadMode::NONE) {
511 /* if we're undecided then look if we crossed the left or right
512 * "sensitive" area */
513 if (player_pos.x < m_cached_translation.x + LEFTEND) {
514 m_lookahead_mode = LookaheadMode::LEFT;
515 } else if (player_pos.x > m_cached_translation.x + RIGHTEND) {
516 m_lookahead_mode = LookaheadMode::RIGHT;
517 }
518 /* at the ends of a level it's obvious which way we will go */
519 if (player_pos.x < static_cast<float>(m_screen_size.width) * 0.5f) {
520 m_lookahead_mode = LookaheadMode::RIGHT;
521 } else if (player_pos.x >= d_sector->get_width() - static_cast<float>(m_screen_size.width) * 0.5f) {
522 m_lookahead_mode = LookaheadMode::LEFT;
523 }
524
525 m_changetime = -1;
526 } else if (m_lookahead_mode != walkDirection) {
527 /* Tux changed direction while camera was scrolling...
528 * he has to do this for a certain time to add robustness against
529 * sudden changes */
530 if (m_changetime < 0) {
531 m_changetime = g_game_time;
532 } else if (g_game_time - m_changetime > config_.dirchange_time) {
533 if (m_lookahead_mode == LookaheadMode::LEFT &&
534 player_pos.x > m_cached_translation.x + RIGHTEND) {
535 m_lookahead_mode = LookaheadMode::RIGHT;
536 } else if (m_lookahead_mode == LookaheadMode::RIGHT &&
537 player_pos.x < m_cached_translation.x + LEFTEND) {
538 m_lookahead_mode = LookaheadMode::LEFT;
539 } else {
540 m_lookahead_mode = LookaheadMode::NONE;
541 }
542 }
543 } else {
544 m_changetime = -1;
545 }
546
547 LEFTEND = static_cast<float>(m_screen_size.width) * config_.edge_x;
548 RIGHTEND = static_cast<float>(m_screen_size.width) * (1-config_.edge_x);
549
550 // calculate our scroll target depending on scroll mode
551 float target_x;
552 if (m_lookahead_mode == LookaheadMode::LEFT)
553 target_x = player_pos.x - RIGHTEND;
554 else if (m_lookahead_mode == LookaheadMode::RIGHT)
555 target_x = player_pos.x - LEFTEND;
556 else
557 target_x = m_cached_translation.x;
558
559 // that's the distance we would have to travel to reach target_x
560 float delta_x = m_cached_translation.x - target_x;
561 // the speed we'd need to travel to reach target_x in this frame
562 float speed_x = delta_x / dt_sec;
563
564 // limit our speed
565 float player_speed_x = player_delta.x / dt_sec;
566 float maxv = config_.max_speed_x + (fabsf(player_speed_x * config_.dynamic_max_speed_x));
567 speed_x = math::clamp(speed_x, -maxv, maxv);
568
569 // apply scrolling
570 m_cached_translation.x -= speed_x * dt_sec;
571 }
572 if (xmode == 3) {
573 float halfsize = config_.kirby_rectsize_x * 0.5f;
574 m_cached_translation.x = math::clamp(m_cached_translation.x,
575 player_pos.x - static_cast<float>(m_screen_size.width) * (0.5f + halfsize),
576 player_pos.x - static_cast<float>(m_screen_size.width) * (0.5f - halfsize));
577 }
578 if (xmode == 4) {
579 float LEFTEND = static_cast<float>(m_screen_size.width) * config_.edge_x;
580 float RIGHTEND = static_cast<float>(m_screen_size.width) * (1 - config_.edge_x);
581
582 if (player_delta.x < -CAMERA_EPSILON) {
583 // walking left
584 m_lookahead_pos.x -= player_delta.x * config_.dynamic_speed_sm;
585 if (m_lookahead_pos.x > RIGHTEND) {
586 m_lookahead_pos.x = RIGHTEND;
587 }
588
589 } else if (player_delta.x > CAMERA_EPSILON) {
590 // walking right
591 m_lookahead_pos.x -= player_delta.x * config_.dynamic_speed_sm;
592 if (m_lookahead_pos.x < LEFTEND) {
593 m_lookahead_pos.x = LEFTEND;
594 }
595 }
596
597 // adjust for level ends
598 if (player_pos.x < LEFTEND) {
599 m_lookahead_pos.x = LEFTEND;
600 }
601 if (player_pos.x > d_sector->get_width() - LEFTEND) {
602 m_lookahead_pos.x = RIGHTEND;
603 }
604
605 m_cached_translation.x = player_pos.x - m_lookahead_pos.x;
606 }
607
608 m_translation.x = m_cached_translation.x;
609
610 if (xmode != 0) {
611 float left_edge, right_edge;
612 if (config_.clamp_x <= 0) {
613 left_edge = 0;
614 right_edge = static_cast<float>(m_screen_size.width);
615 } else {
616 left_edge = static_cast<float>(m_screen_size.width) * config_.clamp_x;
617 right_edge = static_cast<float>(m_screen_size.width) * (1.0f - config_.clamp_x);
618 }
619
620 float peek_to = 0;
621 float translation_compensation = player_pos.x - m_translation.x;
622
623 if (player.peeking_direction_x() == ::Direction::LEFT) {
624 peek_to = right_edge - translation_compensation;
625 } else if (player.peeking_direction_x() == Direction::RIGHT) {
626 peek_to = left_edge - translation_compensation;
627 }
628
629 float peek_move = (peek_to - m_peek_pos.x) * PEEK_ARRIVE_RATIO;
630 if (fabsf(peek_move) < 1.0f) {
631 peek_move = 0.0f;
632 }
633
634 m_peek_pos.x += peek_move;
635
636 m_translation.x -= m_peek_pos.x;
637
638 if (config_.clamp_x > 0.0f) {
639 m_translation.x = math::clamp(m_translation.x,
640 player_pos.x - static_cast<float>(m_screen_size.width) * (1-config_.clamp_x),
641 player_pos.x - static_cast<float>(m_screen_size.width) * config_.clamp_x);
642
643 m_cached_translation.x = math::clamp(m_cached_translation.x,
644 player_pos.x - static_cast<float>(m_screen_size.width) * (1-config_.clamp_x),
645 player_pos.x - static_cast<float>(m_screen_size.width) * config_.clamp_x);
646 }
647 }
648
649 keep_in_bounds(m_translation);
650 keep_in_bounds(m_cached_translation);
651}
652
653void
654Camera::update_scroll_autoscroll(float dt_sec)
655{
656 Player& player = d_sector->get_player();
657 if (player.is_dying())
658 return;
659
660 get_walker()->update(dt_sec);
661 m_translation = get_walker()->get_pos();
662
663 keep_in_bounds(m_translation);
664}
665
666void
667Camera::update_scroll_to(float dt_sec)
668{
669 m_scroll_to_pos += dt_sec * m_scrollspeed;
670 if (m_scroll_to_pos >= 1.0f) {
671 m_mode = Mode::MANUAL;
672 m_translation = m_scroll_goal;
673 return;
674 }
675
676 m_translation = m_scroll_from + (m_scroll_goal - m_scroll_from) * m_scroll_to_pos;
677}
678
679Vector
680Camera::get_center() const
681{
682 return m_translation + Vector(static_cast<float>(m_screen_size.width) / 2.0f,
683 static_cast<float>(m_screen_size.height) / 2.0f);
684}
685
686void
687Camera::move(const int dx, const int dy)
688{
689 m_translation.x += static_cast<float>(dx);
690 m_translation.y += static_cast<float>(dy);
691}
692
693bool
694Camera::is_saveable() const
695{
696 return !(Level::current() &&
697 Level::current()->is_worldmap());
698}
699/* EOF */
700