1// SuperTux
2// Copyright (C) 2009 Ingo Ruhnke <grumbel@gmail.com>
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/bonus_block.hpp"
18
19#include "audio/sound_manager.hpp"
20#include "badguy/badguy.hpp"
21#include "editor/editor.hpp"
22#include "object/bouncy_coin.hpp"
23#include "object/coin_explode.hpp"
24#include "object/coin_rain.hpp"
25#include "object/flower.hpp"
26#include "object/growup.hpp"
27#include "object/oneup.hpp"
28#include "object/player.hpp"
29#include "object/portable.hpp"
30#include "object/powerup.hpp"
31#include "object/specialriser.hpp"
32#include "object/star.hpp"
33#include "object/trampoline.hpp"
34#include "sprite/sprite_manager.hpp"
35#include "supertux/constants.hpp"
36#include "supertux/game_object_factory.hpp"
37#include "supertux/level.hpp"
38#include "supertux/sector.hpp"
39#include "util/reader_collection.hpp"
40#include "util/reader_mapping.hpp"
41#include "util/writer.hpp"
42#include "video/drawing_context.hpp"
43#include "video/surface.hpp"
44
45namespace {
46
47std::unique_ptr<MovingObject> to_moving_object(std::unique_ptr<GameObject> object)
48{
49 if (dynamic_cast<MovingObject*>(object.get()) != nullptr) {
50 return std::unique_ptr<MovingObject>(static_cast<MovingObject*>(object.release()));
51 } else {
52 return std::unique_ptr<MovingObject>();
53 }
54}
55
56} // namespace
57
58BonusBlock::BonusBlock(const Vector& pos, int tile_data) :
59 Block(SpriteManager::current()->create("images/objects/bonus_block/bonusblock.sprite")),
60 m_contents(),
61 m_object(),
62 m_hit_counter(1),
63 m_script(),
64 m_lightsprite(),
65 m_custom_sx()
66{
67 m_default_sprite_name = "images/objects/bonus_block/bonusblock.sprite";
68
69 m_col.m_bbox.set_pos(pos);
70 m_sprite->set_action("normal");
71 m_contents = get_content_by_data(tile_data);
72 preload_contents(tile_data);
73}
74
75BonusBlock::BonusBlock(const ReaderMapping& mapping) :
76 Block(mapping, "images/objects/bonus_block/bonusblock.sprite"),
77 m_contents(Content::COIN),
78 m_object(),
79 m_hit_counter(1),
80 m_script(),
81 m_lightsprite(),
82 m_custom_sx()
83{
84 m_default_sprite_name = "images/objects/bonus_block/bonusblock.sprite";
85
86 auto iter = mapping.get_iter();
87 while (iter.next()) {
88 const std::string& token = iter.get_key();
89 if (token == "x" || token == "y" || token == "sprite") {
90 // already initialized in Block::Block
91 } else if (token == "count") {
92 iter.get(m_hit_counter);
93 } else if (token == "script") {
94 iter.get(m_script);
95 } else if (token == "data") {
96 int d = 0;
97 iter.get(d);
98 m_contents = get_content_by_data(d);
99 preload_contents(d);
100 } else if (token == "contents") {
101 std::string contentstring;
102 iter.get(contentstring);
103
104 m_contents = get_content_from_string(contentstring);
105
106 if (m_contents == Content::CUSTOM)
107 {
108 if (Editor::is_active()) {
109 mapping.get("custom-contents", m_custom_sx);
110 } else {
111 boost::optional<ReaderCollection> content_collection;
112 if (!mapping.get("custom-contents", content_collection))
113 {
114 log_warning << "bonusblock is missing 'custom-contents' tag" << std::endl;
115 }
116 else
117 {
118 const auto& object_specs = content_collection->get_objects();
119 if (!object_specs.empty()) {
120 if (object_specs.size() > 1) {
121 log_warning << "only one object allowed in bonusblock 'custom-contents', ignoring the rest" << std::endl;
122 }
123
124 const ReaderObject& spec = object_specs[0];
125 auto game_object = GameObjectFactory::instance().create(spec.get_name(), spec.get_mapping());
126 m_object = to_moving_object(std::move(game_object));
127 if (!m_object) {
128 log_warning << "Only MovingObjects are allowed inside BonusBlocks" << std::endl;
129 }
130 }
131 }
132 }
133 }
134 } else if (token == "custom-contents") {
135 // handled elsewhere
136 } else {
137 if (m_contents == Content::CUSTOM && !m_object) {
138 // FIXME: This an ugly mess, could probably be removed as of
139 // 16. Aug 2018 no level in either the supertux or the
140 // addon-src repository is using this anymore
141 ReaderMapping object_mapping = iter.as_mapping();
142 auto game_object = GameObjectFactory::instance().create(token, object_mapping);
143
144 m_object = to_moving_object(std::move(game_object));
145 if (!m_object) {
146 throw std::runtime_error("Only MovingObjects are allowed inside BonusBlocks");
147 }
148 } else {
149 log_warning << "Invalid element '" << token << "' in bonusblock" << std::endl;
150 }
151 }
152 }
153
154 if (!Editor::is_active()) {
155 if (m_contents == Content::CUSTOM && !m_object) {
156 throw std::runtime_error("Need to specify content object for custom block");
157 }
158 }
159
160 if (m_contents == Content::LIGHT) {
161 SoundManager::current()->preload("sounds/switch.ogg");
162 m_lightsprite = Surface::from_file("/images/objects/lightmap_light/bonusblock_light.png");
163 }
164}
165
166BonusBlock::Content
167BonusBlock::get_content_by_data(int tile_data) const
168{
169 // Warning: 'tile_data' can't be cast to 'Content', this manual
170 // conversion is necessary
171 switch (tile_data) {
172 case 1: return Content::COIN;
173 case 2: return Content::FIREGROW;
174 case 3: return Content::STAR;
175 case 4: return Content::ONEUP;
176 case 5: return Content::ICEGROW;
177 case 6: return Content::LIGHT;
178 case 7: return Content::TRAMPOLINE;
179 case 8: return Content::CUSTOM; // Trampoline
180 case 9: return Content::CUSTOM; // Rock
181 case 10: return Content::RAIN;
182 case 11: return Content::EXPLODE;
183 case 12: return Content::CUSTOM; // Red potion
184 case 13: return Content::AIRGROW;
185 case 14: return Content::EARTHGROW;
186 default:
187 log_warning << "Invalid box contents" << std::endl;
188 return Content::COIN;
189 }
190}
191
192BonusBlock::~BonusBlock()
193{
194}
195
196ObjectSettings
197BonusBlock::get_settings()
198{
199 ObjectSettings result = Block::get_settings();
200
201 result.add_script(_("Script"), &m_script, "script");
202 result.add_int(_("Count"), &m_hit_counter, "count", 1);
203 result.add_enum(_("Content"), reinterpret_cast<int*>(&m_contents),
204 {_("Coin"), _("Growth (fire flower)"), _("Growth (ice flower)"), _("Growth (air flower)"),
205 _("Growth (earth flower)"), _("Star"), _("Tux doll"), _("Custom"), _("Script"), _("Light"),
206 _("Trampoline"), _("Coin rain"), _("Coin explosion")},
207 {"coin", "firegrow", "icegrow", "airgrow", "earthgrow", "star",
208 "1up", "custom", "script", "light", "trampoline", "rain", "explode"},
209 static_cast<int>(Content::COIN), "contents");
210 result.add_sexp(_("Custom Content"), "custom-contents", m_custom_sx);
211
212 result.reorder({"script", "count", "contents", "sprite", "x", "y"});
213
214 return result;
215}
216
217
218void
219BonusBlock::hit(Player& player)
220{
221 try_open(&player);
222}
223
224HitResponse
225BonusBlock::collision(GameObject& other, const CollisionHit& hit_)
226{
227 auto player = dynamic_cast<Player*> (&other);
228 if (player) {
229 if (player->m_does_buttjump)
230 try_drop(player);
231 }
232
233 auto badguy = dynamic_cast<BadGuy*> (&other);
234 if (badguy) {
235 // hit contains no information for collisions with blocks.
236 // Badguy's bottom has to be below the top of the block
237 // SHIFT_DELTA is required to slide over one tile gaps.
238 if ( badguy->can_break() && ( badguy->get_bbox().get_bottom() > m_col.m_bbox.get_top() + SHIFT_DELTA ) ) {
239 try_open(player);
240 }
241 }
242 auto portable = dynamic_cast<Portable*> (&other);
243 if (portable) {
244 auto moving = dynamic_cast<MovingObject*> (&other);
245 if (moving->get_bbox().get_top() > m_col.m_bbox.get_bottom() - SHIFT_DELTA) {
246 try_open(player);
247 }
248 }
249 return Block::collision(other, hit_);
250}
251
252void
253BonusBlock::try_open(Player* player)
254{
255 if (m_sprite->get_action() == "empty") {
256 SoundManager::current()->play("sounds/brick.wav");
257 return;
258 }
259
260 if (player == nullptr)
261 player = &Sector::get().get_player();
262
263 if (player == nullptr)
264 return;
265
266 Direction direction = (player->get_bbox().get_middle().x > m_col.m_bbox.get_middle().x) ? Direction::LEFT : Direction::RIGHT;
267
268 switch (m_contents) {
269 case Content::COIN:
270 {
271 Sector::get().add<BouncyCoin>(get_pos(), true);
272 player->get_status().add_coins(1);
273 if (m_hit_counter != 0)
274 Sector::get().get_level().m_stats.m_coins++;
275 break;
276 }
277
278 case Content::FIREGROW:
279 {
280 raise_growup_bonus(player, FIRE_BONUS, direction);
281 break;
282 }
283
284 case Content::ICEGROW:
285 {
286 raise_growup_bonus(player, ICE_BONUS, direction);
287 break;
288 }
289
290 case Content::AIRGROW:
291 {
292 raise_growup_bonus(player, AIR_BONUS, direction);
293 break;
294 }
295
296 case Content::EARTHGROW:
297 {
298 raise_growup_bonus(player, EARTH_BONUS, direction);
299 break;
300 }
301
302 case Content::STAR:
303 {
304 Sector::get().add<Star>(get_pos() + Vector(0, -32), direction);
305 SoundManager::current()->play("sounds/upgrade.wav");
306 break;
307 }
308
309 case Content::ONEUP:
310 {
311 Sector::get().add<OneUp>(get_pos(), direction);
312 SoundManager::current()->play("sounds/upgrade.wav");
313 break;
314 }
315
316 case Content::CUSTOM:
317 {
318 Sector::get().add<SpecialRiser>(get_pos(), std::move(m_object));
319 SoundManager::current()->play("sounds/upgrade.wav");
320 break;
321 }
322
323 case Content::SCRIPT:
324 { break; } // because scripts always run, this prevents default contents from being assumed
325
326 case Content::LIGHT:
327 {
328 if (m_sprite->get_action() == "on")
329 m_sprite->set_action("off");
330 else
331 m_sprite->set_action("on");
332 SoundManager::current()->play("sounds/switch.ogg");
333 break;
334 }
335 case Content::TRAMPOLINE:
336 {
337 Sector::get().add<SpecialRiser>(get_pos(), std::make_unique<Trampoline>(get_pos(), false));
338 SoundManager::current()->play("sounds/upgrade.wav");
339 break;
340 }
341 case Content::RAIN:
342 {
343 m_hit_counter = 1; // multiple hits of coin rain is not allowed
344 Sector::get().add<CoinRain>(get_pos(), true);
345 SoundManager::current()->play("sounds/upgrade.wav");
346 break;
347 }
348 case Content::EXPLODE:
349 {
350 m_hit_counter = 1; // multiple hits of coin explode is not allowed
351 Sector::get().add<CoinExplode>(get_pos() + Vector (0, -40));
352 SoundManager::current()->play("sounds/upgrade.wav");
353 break;
354 }
355 }
356
357 if (!m_script.empty()) { // scripts always run if defined
358 Sector::get().run_script(m_script, "BonusBlockScript");
359 }
360
361 start_bounce(player);
362 if (m_hit_counter <= 0 || m_contents == Content::LIGHT) { //use 0 to allow infinite hits
363 } else if (m_hit_counter == 1) {
364 m_sprite->set_action("empty");
365 } else {
366 m_hit_counter--;
367 }
368}
369
370void
371BonusBlock::try_drop(Player *player)
372{
373 if (m_sprite->get_action() == "empty") {
374 SoundManager::current()->play("sounds/brick.wav");
375 return;
376 }
377
378 // First what's below the bonus block, if solid send it up anyway (excepting doll)
379 Rectf dest_;
380 dest_.set_left(m_col.m_bbox.get_left() + 1);
381 dest_.set_top(m_col.m_bbox.get_bottom() + 1);
382 dest_.set_right(m_col.m_bbox.get_right() - 1);
383 dest_.set_bottom(dest_.get_top() + 30);
384
385 if (!Sector::get().is_free_of_statics(dest_, this, true) && !(m_contents == Content::ONEUP))
386 {
387 try_open(player);
388 return;
389 }
390
391 if (player == nullptr)
392 player = &Sector::get().get_player();
393
394 if (player == nullptr)
395 return;
396
397 Direction direction = (player->get_bbox().get_middle().x > m_col.m_bbox.get_middle().x) ? Direction::LEFT : Direction::RIGHT;
398
399 bool countdown = false;
400
401 switch (m_contents) {
402 case Content::COIN:
403 {
404 try_open(player);
405 break;
406 }
407
408 case Content::FIREGROW:
409 {
410 drop_growup_bonus("images/powerups/fireflower/fireflower.sprite", countdown);
411 break;
412 }
413
414 case Content::ICEGROW:
415 {
416 drop_growup_bonus("images/powerups/iceflower/iceflower.sprite", countdown);
417 break;
418 }
419
420 case Content::AIRGROW:
421 {
422 drop_growup_bonus("images/powerups/airflower/airflower.sprite", countdown);
423 break;
424 }
425
426 case Content::EARTHGROW:
427 {
428 drop_growup_bonus("images/powerups/earthflower/earthflower.sprite", countdown);
429 break;
430 }
431
432 case Content::STAR:
433 {
434 Sector::get().add<Star>(get_pos() + Vector(0, 32), direction);
435 SoundManager::current()->play("sounds/upgrade.wav");
436 countdown = true;
437 break;
438 }
439
440 case Content::ONEUP:
441 {
442 Sector::get().add<OneUp>(get_pos(), Direction::DOWN);
443 SoundManager::current()->play("sounds/upgrade.wav");
444 countdown = true;
445 break;
446 }
447
448 case Content::CUSTOM:
449 {
450 //NOTE: non-portable trampolines could be moved to Content::CUSTOM, but they should not drop
451 m_object->set_pos(get_pos() + Vector(0, 32));
452 Sector::get().add_object(std::move(m_object));
453 SoundManager::current()->play("sounds/upgrade.wav");
454 countdown = true;
455 break;
456 }
457
458 case Content::SCRIPT:
459 {
460 countdown = true;
461 break;
462 } // because scripts always run, this prevents default contents from being assumed
463
464 case Content::LIGHT:
465 case Content::TRAMPOLINE:
466 case Content::RAIN:
467 {
468 try_open(player);
469 break;
470 }
471 case Content::EXPLODE:
472 {
473 m_hit_counter = 1; // multiple hits of coin explode is not allowed
474 Sector::get().add<CoinExplode>(get_pos() + Vector (0, 40));
475 SoundManager::current()->play("sounds/upgrade.wav");
476 countdown = true;
477 break;
478 }
479 }
480
481 if (!m_script.empty()) { // scripts always run if defined
482 Sector::get().run_script(m_script, "powerup-script");
483 }
484
485 if (countdown) { // only decrease hit counter if try_open was not called
486 if (m_hit_counter == 1) {
487 m_sprite->set_action("empty");
488 } else {
489 m_hit_counter--;
490 }
491 }
492}
493
494void
495BonusBlock::raise_growup_bonus(Player* player, const BonusType& bonus, const Direction& dir)
496{
497 std::unique_ptr<MovingObject> obj;
498 if (player->get_status().bonus == NO_BONUS) {
499 obj = std::make_unique<GrowUp>(dir);
500 } else {
501 obj = std::make_unique<Flower>(bonus);
502 }
503
504 Sector::get().add<SpecialRiser>(get_pos(), std::move(obj));
505 SoundManager::current()->play("sounds/upgrade.wav");
506}
507
508void
509BonusBlock::drop_growup_bonus(const std::string& bonus_sprite_name, bool& countdown)
510{
511 Sector::get().add<PowerUp>(get_pos() + Vector(0, 32), bonus_sprite_name);
512 SoundManager::current()->play("sounds/upgrade.wav");
513 countdown = true;
514}
515
516void
517BonusBlock::draw(DrawingContext& context)
518{
519 // do the regular drawing first
520 Block::draw(context);
521 // then Draw the light if on.
522 if (m_sprite->get_action() == "on") {
523 Vector pos = get_pos() + (m_col.m_bbox.get_size().as_vector() - Vector(static_cast<float>(m_lightsprite->get_width()),
524 static_cast<float>(m_lightsprite->get_height()))) / 2.0f;
525 context.light().draw_surface(m_lightsprite, pos, 10);
526 }
527}
528
529BonusBlock::Content
530BonusBlock::get_content_from_string(const std::string& contentstring) const
531{
532 if (contentstring == "coin") {
533 return Content::COIN;
534 } else if (contentstring == "firegrow") {
535 return Content::FIREGROW;
536 } else if (contentstring == "icegrow") {
537 return Content::ICEGROW;
538 } else if (contentstring == "airgrow") {
539 return Content::AIRGROW;
540 } else if (contentstring == "earthgrow") {
541 return Content::EARTHGROW;
542 } else if (contentstring == "star") {
543 return Content::STAR;
544 } else if (contentstring == "1up") {
545 return Content::ONEUP;
546 } else if (contentstring == "custom") {
547 return Content::CUSTOM;
548 } else if (contentstring == "script") { // use when bonusblock is to contain ONLY a script
549 return Content::SCRIPT;
550 } else if (contentstring == "light") {
551 return Content::LIGHT;
552 } else if (contentstring == "trampoline") {
553 return Content::TRAMPOLINE;
554 } else if (contentstring == "rain") {
555 return Content::RAIN;
556 } else if (contentstring == "explode") {
557 return Content::EXPLODE;
558 } else {
559 log_warning << "Invalid box contents '" << contentstring << "'" << std::endl;
560 return Content::COIN;
561 }
562}
563
564std::string
565BonusBlock::contents_to_string(const BonusBlock::Content& content) const
566{
567 switch (m_contents)
568 {
569 case Content::COIN: return "coin";
570 case Content::FIREGROW: return "firegrow";
571 case Content::ICEGROW: return "icegrow";
572 case Content::AIRGROW: return "airgrow";
573 case Content::EARTHGROW: return "earthgrow";
574 case Content::STAR: return "star";
575 case Content::ONEUP: return "1up";
576 case Content::CUSTOM: return "custom";
577 case Content::SCRIPT: return "script";
578 case Content::LIGHT: return "light";
579 case Content::TRAMPOLINE: return "trampoline";
580 case Content::RAIN: return "rain";
581 case Content::EXPLODE: return "explode";
582 default: return "coin";
583 }
584}
585
586void
587BonusBlock::preload_contents(int d)
588{
589 switch (d)
590 {
591 case 6: // Light
592 SoundManager::current()->preload("sounds/switch.ogg");
593 m_lightsprite=Surface::from_file("/images/objects/lightmap_light/bonusblock_light.png");
594 break;
595
596 case 7:
597 //object = new Trampoline(get_pos(), false); //needed if this is to be moved to custom
598 break;
599
600 case 8: // Trampoline
601 m_object = std::make_unique<Trampoline>(get_pos(), true);
602 break;
603
604 case 9: // Rock
605 m_object = std::make_unique<Rock>(get_pos(), "images/objects/rock/rock.sprite");
606 break;
607
608 case 12: // Red potion
609 m_object = std::make_unique<PowerUp>(get_pos(), "images/powerups/potions/red-potion.sprite");
610 break;
611
612 default:
613 break;
614 }
615}
616
617/* EOF */
618