1// SuperTux - A Jump'n Run
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 "supertux/sector.hpp"
18
19#include <physfs.h>
20#include <algorithm>
21
22#include "audio/sound_manager.hpp"
23#include "badguy/badguy.hpp"
24#include "collision/collision.hpp"
25#include "collision/collision_system.hpp"
26#include "editor/editor.hpp"
27#include "math/aatriangle.hpp"
28#include "math/rect.hpp"
29#include "object/ambient_light.hpp"
30#include "object/background.hpp"
31#include "object/bullet.hpp"
32#include "object/camera.hpp"
33#include "object/display_effect.hpp"
34#include "object/gradient.hpp"
35#include "object/music_object.hpp"
36#include "object/player.hpp"
37#include "object/portable.hpp"
38#include "object/pulsing_light.hpp"
39#include "object/smoke_cloud.hpp"
40#include "object/spawnpoint.hpp"
41#include "object/text_array_object.hpp"
42#include "object/text_object.hpp"
43#include "object/tilemap.hpp"
44#include "physfs/ifile_stream.hpp"
45#include "scripting/sector.hpp"
46#include "squirrel/squirrel_environment.hpp"
47#include "supertux/constants.hpp"
48#include "supertux/debug.hpp"
49#include "supertux/game_object_factory.hpp"
50#include "supertux/game_session.hpp"
51#include "supertux/level.hpp"
52#include "supertux/player_status_hud.hpp"
53#include "supertux/savegame.hpp"
54#include "supertux/tile.hpp"
55#include "util/file_system.hpp"
56#include "util/writer.hpp"
57#include "video/video_system.hpp"
58#include "video/viewport.hpp"
59
60Sector* Sector::s_current = nullptr;
61
62namespace {
63
64PlayerStatus dummy_player_status;
65
66} // namespace
67
68Sector::Sector(Level& parent) :
69 m_level(parent),
70 m_name(),
71 m_fully_constructed(false),
72 m_init_script(),
73 m_foremost_layer(),
74 m_squirrel_environment(new SquirrelEnvironment(SquirrelVirtualMachine::current()->get_vm(), "sector")),
75 m_collision_system(new CollisionSystem(*this)),
76 m_gravity(10.0)
77{
78 Savegame* savegame = (Editor::current() && Editor::is_active()) ?
79 Editor::current()->m_savegame.get() :
80 GameSession::current() ? &GameSession::current()->get_savegame() : nullptr;
81 PlayerStatus& player_status = savegame ? savegame->get_player_status() : dummy_player_status;
82
83 if (savegame && m_level.m_name != "Credits" && !savegame->is_title_screen()) {
84 add<PlayerStatusHUD>(player_status);
85 }
86 add<Player>(player_status, "Tux");
87 add<DisplayEffect>("Effect");
88 add<TextObject>("Text");
89 add<TextArrayObject>("TextArray");
90
91 SoundManager::current()->preload("sounds/shoot.wav");
92}
93
94Sector::~Sector()
95{
96 try
97 {
98 deactivate();
99 }
100 catch(const std::exception& err)
101 {
102 log_warning << err.what() << std::endl;
103 }
104
105 clear_objects();
106}
107
108void
109Sector::finish_construction(bool editable)
110{
111 flush_game_objects();
112
113 if (!editable) {
114 convert_tiles2gameobject();
115
116 bool has_background = std::any_of(get_objects().begin(), get_objects().end(),
117 [](const auto& obj) {
118 return (dynamic_cast<Background*>(obj.get()) ||
119 dynamic_cast<Gradient*>(obj.get()));
120 });
121 if (!has_background) {
122 auto& gradient = add<Gradient>();
123 gradient.set_gradient(Color(0.3f, 0.4f, 0.75f), Color(1.f, 1.f, 1.f));
124 }
125 }
126
127 if (get_solid_tilemaps().empty()) {
128 log_warning << "sector '" << get_name() << "' does not contain a solid tile layer." << std::endl;
129 }
130
131 if (!get_object_by_type<Camera>()) {
132 log_warning << "sector '" << get_name() << "' does not contain a camera." << std::endl;
133 add<Camera>("Camera");
134 }
135
136 if (!get_object_by_type<AmbientLight>()) {
137 add<AmbientLight>(Color::WHITE);
138 }
139
140 if (!get_object_by_type<MusicObject>()) {
141 add<MusicObject>();
142 }
143
144 flush_game_objects();
145
146 m_foremost_layer = calculate_foremost_layer();
147
148 process_resolve_requests();
149
150 for (auto& object : get_objects()) {
151 object->finish_construction();
152 }
153
154 flush_game_objects();
155
156 m_fully_constructed = true;
157}
158
159Level&
160Sector::get_level() const
161{
162 return m_level;
163}
164
165void
166Sector::activate(const std::string& spawnpoint)
167{
168 SpawnPointMarker* sp = nullptr;
169 for (auto& spawn_point : get_objects_by_type<SpawnPointMarker>()) {
170 if (spawn_point.get_name() == spawnpoint) {
171 sp = &spawn_point;
172 break;
173 }
174 }
175
176 if (!sp) {
177 log_warning << "Spawnpoint '" << spawnpoint << "' not found." << std::endl;
178 if (spawnpoint != "main") {
179 activate("main");
180 } else {
181 activate(Vector(0, 0));
182 }
183 } else {
184 activate(sp->get_pos());
185 }
186}
187
188void
189Sector::activate(const Vector& player_pos)
190{
191 BIND_SECTOR(*this);
192
193 if (s_current != this) {
194 if (s_current != nullptr)
195 s_current->deactivate();
196 s_current = this;
197
198 m_squirrel_environment->expose_self();
199
200 for (auto& object : get_objects()) {
201 m_squirrel_environment->try_expose(*object);
202 }
203 }
204
205 // The Sector object is called 'settings' as it is accessed as 'sector.settings'
206 m_squirrel_environment->expose("settings", std::make_unique<scripting::Sector>(this));
207
208 // two-player hack: move other players to main player's position
209 // Maybe specify 2 spawnpoints in the level?
210 for (auto& player : get_objects_by_type<Player>()) {
211 // spawn smalltux below spawnpoint
212 if (!player.is_big()) {
213 player.move(player_pos + Vector(0,32));
214 } else {
215 player.move(player_pos);
216 }
217
218 // spawning tux in the ground would kill him
219 if (!is_free_of_tiles(player.get_bbox())) {
220 std::string current_level = "[" + Sector::get().get_level().m_filename + "] ";
221 log_warning << current_level << "Tried spawning Tux in solid matter. Compensating." << std::endl;
222 Vector npos = player.get_bbox().p1();
223 npos.y-=32;
224 player.move(npos);
225 }
226 }
227
228 { //FIXME: This is a really dirty workaround for this strange camera jump
229 Player& player = get_player();
230 Camera& camera = get_camera();
231 player.move(player.get_pos()+Vector(-32, 0));
232 camera.reset(player.get_pos());
233 camera.update(1);
234 player.move(player.get_pos()+(Vector(32, 0)));
235 camera.update(1);
236 }
237
238 flush_game_objects();
239
240 //Run default.nut just before init script
241 //Check to see if it's in a levelset (info file)
242 std::string basedir = FileSystem::dirname(get_level().m_filename);
243 if (PHYSFS_exists((basedir + "/info").c_str())) {
244 try {
245 IFileStream in(basedir + "/default.nut");
246 m_squirrel_environment->run_script(in, "default.nut");
247 } catch(std::exception& ) {
248 // doesn't exist or erroneous; do nothing
249 }
250 }
251
252 // Run init script
253 if (!m_init_script.empty() && !Editor::is_active()) {
254 run_script(m_init_script, "init-script");
255 }
256}
257
258void
259Sector::deactivate()
260{
261 BIND_SECTOR(*this);
262
263 if (s_current != this)
264 return;
265
266 m_squirrel_environment->unexpose_self();
267
268 for (const auto& object: get_objects()) {
269 m_squirrel_environment->try_unexpose(*object);
270 }
271
272 m_squirrel_environment->unexpose("settings");
273
274 s_current = nullptr;
275}
276
277Rectf
278Sector::get_active_region() const
279{
280 Camera& camera = get_camera();
281 return Rectf(
282 camera.get_translation() - Vector(1600, 1200),
283 camera.get_translation() + Vector(1600, 1200) + Vector(static_cast<float>(SCREEN_WIDTH),
284 static_cast<float>(SCREEN_HEIGHT)));
285}
286
287int
288Sector::calculate_foremost_layer() const
289{
290 int layer = LAYER_BACKGROUND0;
291 for (auto& tm : get_objects_by_type<TileMap>())
292 {
293 if (tm.get_layer() > layer)
294 {
295 if ( (tm.get_alpha() < 1.0f) )
296 {
297 layer = tm.get_layer() - 1;
298 }
299 else
300 {
301 layer = tm.get_layer() + 1;
302 }
303 }
304 }
305 log_debug << "Calculated baduy falling layer was: " << layer << std::endl;
306 return layer;
307}
308
309int
310Sector::get_foremost_layer() const
311{
312 return m_foremost_layer;
313}
314
315void
316Sector::update(float dt_sec)
317{
318 assert(m_fully_constructed);
319
320 BIND_SECTOR(*this);
321
322 m_squirrel_environment->update(dt_sec);
323
324 GameObjectManager::update(dt_sec);
325
326 /* Handle all possible collisions. */
327 m_collision_system->update();
328 flush_game_objects();
329}
330
331bool
332Sector::before_object_add(GameObject& object)
333{
334 if (object.is_singleton())
335 {
336 const auto& objects = get_objects_by_type_index(std::type_index(typeid(object)));
337 if (!objects.empty())
338 {
339 log_warning << "Can't insert multiple GameObject of type '" << typeid(object).name() << "', ignoring" << std::endl;
340 return false;
341 }
342 }
343
344 auto movingobject = dynamic_cast<MovingObject*>(&object);
345 if (movingobject)
346 {
347 m_collision_system->add(movingobject->get_collision_object());
348 }
349
350 if (s_current == this) {
351 m_squirrel_environment->try_expose(object);
352 }
353
354 if (m_fully_constructed) {
355 // if the sector is already fully constructed, finish the object
356 // constructions, as there should be no more named references to resolve
357 object.finish_construction();
358 }
359
360 return true;
361}
362
363void
364Sector::before_object_remove(GameObject& object)
365{
366 auto moving_object = dynamic_cast<MovingObject*>(&object);
367 if (moving_object) {
368 m_collision_system->remove(moving_object->get_collision_object());
369 }
370
371 if (s_current == this)
372 m_squirrel_environment->try_unexpose(object);
373}
374
375void
376Sector::draw(DrawingContext& context)
377{
378 BIND_SECTOR(*this);
379
380 Camera& camera = get_camera();
381
382 context.push_transform();
383 context.set_translation(camera.get_translation());
384
385 GameObjectManager::draw(context);
386
387 if (g_debug.show_collision_rects) {
388 m_collision_system->draw(context);
389 }
390
391 context.pop_transform();
392}
393
394bool
395Sector::is_free_of_tiles(const Rectf& rect, const bool ignoreUnisolid) const
396{
397 return m_collision_system->is_free_of_tiles(rect, ignoreUnisolid);
398}
399
400bool
401Sector::is_free_of_statics(const Rectf& rect, const MovingObject* ignore_object, const bool ignoreUnisolid) const
402{
403 return m_collision_system->is_free_of_statics(rect,
404 ignore_object ? ignore_object->get_collision_object() : nullptr,
405 ignoreUnisolid);
406}
407
408bool
409Sector::is_free_of_movingstatics(const Rectf& rect, const MovingObject* ignore_object) const
410{
411 return m_collision_system->is_free_of_movingstatics(rect,
412 ignore_object ? ignore_object->get_collision_object() : nullptr);
413}
414
415bool
416Sector::free_line_of_sight(const Vector& line_start, const Vector& line_end, const MovingObject* ignore_object) const
417{
418 return m_collision_system->free_line_of_sight(line_start, line_end,
419 ignore_object ? ignore_object->get_collision_object() : nullptr);
420}
421
422bool
423Sector::can_see_player(const Vector& eye) const
424{
425 for (const auto& player : get_objects_by_type<Player>()) {
426 // test for free line of sight to any of all four corners and the middle of the player's bounding box
427 if (free_line_of_sight(eye, player.get_bbox().p1(), &player)) return true;
428 if (free_line_of_sight(eye, Vector(player.get_bbox().get_right(), player.get_bbox().get_top()), &player)) return true;
429 if (free_line_of_sight(eye, player.get_bbox().p2(), &player)) return true;
430 if (free_line_of_sight(eye, Vector(player.get_bbox().get_left(), player.get_bbox().get_bottom()), &player)) return true;
431 if (free_line_of_sight(eye, player.get_bbox().get_middle(), &player)) return true;
432 }
433 return false;
434}
435
436bool
437Sector::inside(const Rectf& rect) const
438{
439 for (const auto& solids : get_solid_tilemaps()) {
440 Rectf bbox = solids->get_bbox();
441
442 // the top of the sector extends to infinity
443 if (bbox.get_left() <= rect.get_left() &&
444 rect.get_right() <= bbox.get_right() &&
445 rect.get_bottom() <= bbox.get_bottom()) {
446 return true;
447 }
448 }
449 return false;
450}
451
452Size
453Sector::get_editor_size() const
454{
455 // Find the solid tilemap with the greatest surface
456 size_t max_surface = 0;
457 Size size;
458 for (const auto& solids: get_solid_tilemaps()) {
459 size_t surface = solids->get_width() * solids->get_height();
460 if (surface > max_surface) {
461 max_surface = surface;
462 size = solids->get_size();
463 }
464 }
465
466 return size;
467}
468
469void
470Sector::resize_sector(const Size& old_size, const Size& new_size, const Size& resize_offset)
471{
472 bool is_offset = resize_offset.width || resize_offset.height;
473 Vector obj_shift = Vector(static_cast<float>(resize_offset.width) * 32.0f,
474 static_cast<float>(resize_offset.height) * 32.0f);
475 for (const auto& object : get_objects()) {
476 auto tilemap = dynamic_cast<TileMap*>(object.get());
477 if (tilemap) {
478 if (tilemap->get_size() == old_size) {
479 tilemap->resize(new_size, resize_offset);
480 } else if (is_offset) {
481 tilemap->move_by(obj_shift);
482 }
483 } else if (is_offset) {
484 auto moving_object = dynamic_cast<MovingObject*>(object.get());
485 if (moving_object) {
486 moving_object->move_to(moving_object->get_pos() + obj_shift);
487 }
488 }
489 }
490}
491
492void
493Sector::change_solid_tiles(uint32_t old_tile_id, uint32_t new_tile_id)
494{
495 for (auto& solids: get_solid_tilemaps()) {
496 solids->change_all(old_tile_id, new_tile_id);
497 }
498}
499
500void
501Sector::set_gravity(float gravity)
502{
503 if (gravity != 10.0f)
504 {
505 log_warning << "Changing a Sector's gravitational constant might have unforeseen side-effects: " << gravity << std::endl;
506 }
507
508 m_gravity = gravity;
509}
510
511float
512Sector::get_gravity() const
513{
514 return m_gravity;
515}
516
517Player*
518Sector::get_nearest_player (const Vector& pos) const
519{
520 Player *nearest_player = nullptr;
521 float nearest_dist = std::numeric_limits<float>::max();
522
523 for (auto& player : get_objects_by_type<Player>())
524 {
525 if (player.is_dying() || player.is_dead())
526 continue;
527
528 float dist = player.get_bbox ().distance(pos);
529
530 if (dist < nearest_dist) {
531 nearest_player = &player;
532 nearest_dist = dist;
533 }
534 }
535
536 return nearest_player;
537}
538
539std::vector<MovingObject*>
540Sector::get_nearby_objects(const Vector& center, float max_distance) const
541{
542 std::vector<MovingObject*> result;
543 for (auto& object : m_collision_system->get_nearby_objects(center, max_distance))
544 {
545 auto* moving_object = dynamic_cast<MovingObject*>(&object->get_listener());
546 if (moving_object) {
547 result.push_back(moving_object);
548 }
549 }
550 return result;
551}
552
553void
554Sector::stop_looping_sounds()
555{
556 for (auto& object : get_objects()) {
557 object->stop_looping_sounds();
558 }
559}
560
561void Sector::play_looping_sounds()
562{
563 for (const auto& object : get_objects()) {
564 object->play_looping_sounds();
565 }
566}
567
568void
569Sector::save(Writer &writer)
570{
571 BIND_SECTOR(*this);
572
573 writer.start_list("sector", false);
574
575 writer.write("name", m_name, false);
576
577 if (!m_level.is_worldmap()) {
578 if (m_gravity != 10.0f) {
579 writer.write("gravity", m_gravity);
580 }
581 }
582
583 if (m_init_script.size()) {
584 writer.write("init-script", m_init_script,false);
585 }
586
587 // saving objects;
588 std::vector<GameObject*> objects(get_objects().size());
589 std::transform(get_objects().begin(), get_objects().end(), objects.begin(), [] (auto& obj) {
590 return obj.get();
591 });
592
593 std::stable_sort(objects.begin(), objects.end(),
594 [](const GameObject* lhs, GameObject* rhs) {
595 return lhs->get_class() < rhs->get_class();
596 });
597
598 for (auto& obj : objects) {
599 if (obj->is_saveable()) {
600 writer.start_list(obj->get_class());
601 obj->save(writer);
602 writer.end_list(obj->get_class());
603 }
604 }
605
606 writer.end_list("sector");
607}
608
609void
610Sector::convert_tiles2gameobject()
611{
612 // add lights for special tiles
613 for (auto& tm : get_objects_by_type<TileMap>())
614 {
615 for (int x=0; x < tm.get_width(); ++x)
616 {
617 for (int y=0; y < tm.get_height(); ++y)
618 {
619 const Tile& tile = tm.get_tile(x, y);
620
621 if (!tile.get_object_name().empty())
622 {
623 // If a tile is associated with an object, insert that
624 // object and remove the tile
625 if (tile.get_object_name() == "decal" ||
626 tm.is_solid())
627 {
628 Vector pos = tm.get_tile_position(x, y);
629 try {
630 auto object = GameObjectFactory::instance().create(tile.get_object_name(), pos, Direction::AUTO, tile.get_object_data());
631 add_object(std::move(object));
632 tm.change(x, y, 0);
633 } catch(std::exception& e) {
634 log_warning << e.what() << "" << std::endl;
635 }
636 }
637 }
638 else
639 {
640 // add lights for fire tiles
641 uint32_t attributes = tile.get_attributes();
642 Vector pos = tm.get_tile_position(x, y);
643 Vector center = pos + Vector(16, 16);
644
645 if (attributes & Tile::FIRE) {
646 if (attributes & Tile::HURTS) {
647 // lava or lavaflow
648 // space lights a bit
649 if ((tm.get_tile(x-1, y).get_attributes() != attributes || x%3 == 0)
650 && (tm.get_tile(x, y-1).get_attributes() != attributes || y%3 == 0)) {
651 float pseudo_rnd = static_cast<float>(static_cast<int>(pos.x) % 10) / 10;
652 add<PulsingLight>(center, 1.0f + pseudo_rnd, 0.8f, 1.0f,
653 Color(1.0f, 0.3f, 0.0f, 1.0f));
654 }
655 } else {
656 // torch
657 float pseudo_rnd = static_cast<float>(static_cast<int>(pos.x) % 10) / 10;
658 add<PulsingLight>(center, 1.0f + pseudo_rnd, 0.9f, 1.0f,
659 Color(1.0f, 1.0f, 0.6f, 1.0f));
660 }
661 }
662 }
663 }
664 }
665 }
666}
667
668void
669Sector::run_script(const std::string& script, const std::string& sourcename)
670{
671 m_squirrel_environment->run_script(script, sourcename);
672}
673
674Camera&
675Sector::get_camera() const
676{
677 return get_singleton_by_type<Camera>();
678}
679
680Player&
681Sector::get_player() const
682{
683 return get_singleton_by_type<Player>();
684}
685
686DisplayEffect&
687Sector::get_effect() const
688{
689 return get_singleton_by_type<DisplayEffect>();
690}
691
692/* EOF */
693