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 | |
60 | Sector* Sector::s_current = nullptr; |
61 | |
62 | namespace { |
63 | |
64 | PlayerStatus dummy_player_status; |
65 | |
66 | } // namespace |
67 | |
68 | Sector::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 | |
94 | Sector::~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 | |
108 | void |
109 | Sector::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 | |
159 | Level& |
160 | Sector::get_level() const |
161 | { |
162 | return m_level; |
163 | } |
164 | |
165 | void |
166 | Sector::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 | |
188 | void |
189 | Sector::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 | |
258 | void |
259 | Sector::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 | |
277 | Rectf |
278 | Sector::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 | |
287 | int |
288 | Sector::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 | |
309 | int |
310 | Sector::get_foremost_layer() const |
311 | { |
312 | return m_foremost_layer; |
313 | } |
314 | |
315 | void |
316 | Sector::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 | |
331 | bool |
332 | Sector::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 | |
363 | void |
364 | Sector::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 | |
375 | void |
376 | Sector::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 | |
394 | bool |
395 | Sector::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 | |
400 | bool |
401 | Sector::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 | |
408 | bool |
409 | Sector::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 | |
415 | bool |
416 | Sector::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 | |
422 | bool |
423 | Sector::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 | |
436 | bool |
437 | Sector::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 | |
452 | Size |
453 | Sector::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 | |
469 | void |
470 | Sector::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 | |
492 | void |
493 | Sector::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 | |
500 | void |
501 | Sector::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 | |
511 | float |
512 | Sector::get_gravity() const |
513 | { |
514 | return m_gravity; |
515 | } |
516 | |
517 | Player* |
518 | Sector::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 | |
539 | std::vector<MovingObject*> |
540 | Sector::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 | |
553 | void |
554 | Sector::stop_looping_sounds() |
555 | { |
556 | for (auto& object : get_objects()) { |
557 | object->stop_looping_sounds(); |
558 | } |
559 | } |
560 | |
561 | void Sector::play_looping_sounds() |
562 | { |
563 | for (const auto& object : get_objects()) { |
564 | object->play_looping_sounds(); |
565 | } |
566 | } |
567 | |
568 | void |
569 | Sector::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 | |
609 | void |
610 | Sector::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 | |
668 | void |
669 | Sector::run_script(const std::string& script, const std::string& sourcename) |
670 | { |
671 | m_squirrel_environment->run_script(script, sourcename); |
672 | } |
673 | |
674 | Camera& |
675 | Sector::get_camera() const |
676 | { |
677 | return get_singleton_by_type<Camera>(); |
678 | } |
679 | |
680 | Player& |
681 | Sector::get_player() const |
682 | { |
683 | return get_singleton_by_type<Player>(); |
684 | } |
685 | |
686 | DisplayEffect& |
687 | Sector::get_effect() const |
688 | { |
689 | return get_singleton_by_type<DisplayEffect>(); |
690 | } |
691 | |
692 | /* EOF */ |
693 | |