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 "badguy/dispenser.hpp"
18
19#include "audio/sound_manager.hpp"
20#include "editor/editor.hpp"
21#include "math/random.hpp"
22#include "object/bullet.hpp"
23#include "object/player.hpp"
24#include "sprite/sprite.hpp"
25#include "supertux/game_object_factory.hpp"
26#include "supertux/sector.hpp"
27#include "util/reader_mapping.hpp"
28
29Dispenser::DispenserType
30Dispenser::DispenserType_from_string(const std::string& type_string)
31{
32 if (type_string == "dropper") {
33 return DispenserType::DROPPER;
34 } else if (type_string == "rocketlauncher") {
35 return DispenserType::ROCKETLAUNCHER;
36 } else if (type_string == "cannon") {
37 return DispenserType::CANNON;
38 } else if (type_string == "point") {
39 return DispenserType::POINT;
40 } else {
41 throw std::exception();
42 }
43}
44
45std::string
46Dispenser::DispenserType_to_string(DispenserType type)
47{
48 switch (type)
49 {
50 case DispenserType::DROPPER:
51 return "dropper";
52 case DispenserType::ROCKETLAUNCHER:
53 return "rocketlauncher";
54 case DispenserType::CANNON:
55 return "cannon";
56 case DispenserType::POINT:
57 return "point";
58 default:
59 return "unknown";
60 }
61}
62
63Dispenser::Dispenser(const ReaderMapping& reader) :
64 BadGuy(reader, "images/creatures/dispenser/dispenser.sprite"),
65 ExposedObject<Dispenser, scripting::Dispenser>(this),
66 m_cycle(),
67 m_badguys(),
68 m_next_badguy(0),
69 m_dispense_timer(),
70 m_autotarget(false),
71 m_swivel(false),
72 m_broken(false),
73 m_random(),
74 m_type(),
75 m_type_str(),
76 m_limit_dispensed_badguys(),
77 m_max_concurrent_badguys(),
78 m_current_badguys()
79{
80 set_colgroup_active(COLGROUP_MOVING_STATIC);
81 SoundManager::current()->preload("sounds/squish.wav");
82 reader.get("cycle", m_cycle, 5.0f);
83 if ( !reader.get("badguy", m_badguys)) m_badguys.clear();
84 reader.get("random", m_random, false);
85 std::string type_s = "dropper"; //default
86 reader.get("type", type_s, "");
87 try
88 {
89 m_type = DispenserType_from_string(type_s);
90 }
91 catch(std::exception&)
92 {
93 if (!Editor::is_active())
94 {
95 if (type_s.empty()) {
96 log_warning << "No dispenser type set, setting to dropper." << std::endl;
97 }
98 else {
99 log_warning << "Unknown type of dispenser:" << type_s << ", setting to dropper." << std::endl;
100 }
101 }
102 m_type = DispenserType::DROPPER;
103 }
104
105 m_type_str = DispenserType_to_string(m_type);
106
107 reader.get("limit-dispensed-badguys", m_limit_dispensed_badguys, false);
108 reader.get("max-concurrent-badguys", m_max_concurrent_badguys, 0);
109
110// if (badguys.size() <= 0)
111// throw std::runtime_error("No badguys in dispenser.");
112
113 switch (m_type)
114 {
115 case DispenserType::DROPPER:
116 m_sprite->set_action("dropper");
117 break;
118
119 case DispenserType::ROCKETLAUNCHER:
120 m_sprite->set_action(m_dir == Direction::LEFT ? "working-left" : "working-right");
121 set_colgroup_active(COLGROUP_MOVING); //if this were COLGROUP_MOVING_STATIC MrRocket would explode on launch.
122
123 if (m_start_dir == Direction::AUTO) {
124 m_autotarget = true;
125 }
126 break;
127
128 case DispenserType::CANNON:
129 m_sprite->set_action("working");
130 break;
131
132 case DispenserType::POINT:
133 m_sprite->set_action("invisible");
134 set_colgroup_active(COLGROUP_DISABLED);
135 break;
136
137 default:
138 break;
139 }
140
141 m_col.m_bbox.set_size(m_sprite->get_current_hitbox_width(), m_sprite->get_current_hitbox_height());
142 m_countMe = false;
143}
144
145void
146Dispenser::draw(DrawingContext& context)
147{
148 if (m_type != DispenserType::POINT || Editor::is_active()) {
149 BadGuy::draw(context);
150 }
151}
152
153void
154Dispenser::activate()
155{
156 if (m_broken){
157 return;
158 }
159 if (m_autotarget && !m_swivel){ // auto cannon sprite might be wrong
160 auto* player = get_nearest_player();
161 if (player) {
162 m_dir = (player->get_pos().x > get_pos().x) ? Direction::RIGHT : Direction::LEFT;
163 m_sprite->set_action(m_dir == Direction::LEFT ? "working-left" : "working-right");
164 }
165 }
166 m_dispense_timer.start(m_cycle, true);
167 launch_badguy();
168}
169
170void
171Dispenser::deactivate()
172{
173 m_dispense_timer.stop();
174}
175
176//TODO: Add launching velocity to certain badguys
177bool
178Dispenser::collision_squished(GameObject& object)
179{
180 //Cannon launching MrRocket can be broken by jumping on it
181 //other dispensers are not that fragile.
182 if (m_broken || m_type != DispenserType::ROCKETLAUNCHER) {
183 return false;
184 }
185
186 if (m_frozen) {
187 unfreeze();
188 }
189
190 m_sprite->set_action(m_dir == Direction::LEFT ? "broken-left" : "broken-right");
191 m_dispense_timer.start(0);
192 set_colgroup_active(COLGROUP_MOVING_STATIC); // Tux can stand on broken cannon.
193 auto player = dynamic_cast<Player*>(&object);
194 if (player){
195 player->bounce(*this);
196 }
197 SoundManager::current()->play("sounds/squish.wav", get_pos());
198 m_broken = true;
199 return true;
200}
201
202HitResponse
203Dispenser::collision(GameObject& other, const CollisionHit& hit)
204{
205 auto player = dynamic_cast<Player*> (&other);
206 if (player) {
207 // hit from above?
208 if (player->get_bbox().get_bottom() < (m_col.m_bbox.get_top() + 16)) {
209 collision_squished(*player);
210 return FORCE_MOVE;
211 }
212 if (m_frozen && m_type != DispenserType::CANNON){
213 unfreeze();
214 }
215 return FORCE_MOVE;
216 }
217
218 auto bullet = dynamic_cast<Bullet*> (&other);
219 if (bullet){
220 return collision_bullet(*bullet, hit);
221 }
222
223 return FORCE_MOVE;
224}
225
226void
227Dispenser::active_update(float )
228{
229 if (m_dispense_timer.check()) {
230 // auto always shoots in Tux's direction
231 if (m_autotarget) {
232 if ( m_sprite->animation_done()) {
233 m_sprite->set_action(m_dir == Direction::LEFT ? "working-left" : "working-right");
234 m_swivel = false;
235 }
236
237 auto player = get_nearest_player();
238 if (player && !m_swivel){
239 Direction targetdir = (player->get_pos().x > get_pos().x) ? Direction::RIGHT : Direction::LEFT;
240 if ( m_dir != targetdir ){ // no target: swivel cannon
241 m_swivel = true;
242 m_dir = targetdir;
243 m_sprite->set_action(m_dir == Direction::LEFT ? "swivel-left" : "swivel-right", 1);
244 } else { // tux in sight: shoot
245 launch_badguy();
246 }
247 }
248 } else {
249 launch_badguy();
250 }
251 }
252}
253
254void
255Dispenser::launch_badguy()
256{
257 if (m_badguys.empty()) return;
258 if (m_frozen) return;
259 if (m_limit_dispensed_badguys &&
260 m_current_badguys >= m_max_concurrent_badguys)
261 return;
262
263 //FIXME: Does is_offscreen() work right here?
264 if (!is_offscreen() && !Editor::is_active()) {
265 Direction launchdir = m_dir;
266 if ( !m_autotarget && m_start_dir == Direction::AUTO ){
267 Player* player = get_nearest_player();
268 if ( player ){
269 launchdir = (player->get_pos().x > get_pos().x) ? Direction::RIGHT : Direction::LEFT;
270 }
271 }
272
273 if (m_badguys.size() > 1) {
274 if (m_random) {
275 m_next_badguy = static_cast<unsigned int>(gameRandom.rand(static_cast<int>(m_badguys.size())));
276 }
277 else {
278 m_next_badguy++;
279
280 if (m_next_badguy >= m_badguys.size())
281 m_next_badguy = 0;
282 }
283 }
284
285 std::string badguy = m_badguys[m_next_badguy];
286
287 if (badguy == "random") {
288 log_warning << "random is outdated; use a list of badguys to select from." << std::endl;
289 return;
290 }
291 if (badguy == "goldbomb") {
292 log_warning << "goldbomb is not allowed to be dispensed" << std::endl;
293 return;
294 }
295
296 try {
297 /* Need to allocate the badguy first to figure out its bounding box. */
298 auto game_object = GameObjectFactory::instance().create(badguy, get_pos(), launchdir);
299 if (game_object == nullptr)
300 throw std::runtime_error("Creating " + badguy + " object failed.");
301
302 auto& bad_guy = dynamic_cast<BadGuy&>(*game_object);
303
304 Rectf object_bbox = bad_guy.get_bbox();
305
306 Vector spawnpoint;
307 switch (m_type)
308 {
309 case DispenserType::DROPPER:
310 spawnpoint = get_anchor_pos (m_col.m_bbox, ANCHOR_BOTTOM);
311 spawnpoint.x -= 0.5f * object_bbox.get_width();
312 break;
313
314 case DispenserType::ROCKETLAUNCHER:
315 case DispenserType::CANNON:
316 spawnpoint = get_pos(); /* top-left corner of the cannon */
317 if (launchdir == Direction::LEFT)
318 spawnpoint.x -= object_bbox.get_width() + 1;
319 else
320 spawnpoint.x += m_col.m_bbox.get_width() + 1;
321 break;
322
323 case DispenserType::POINT:
324 spawnpoint = m_col.m_bbox.p1();
325 break;
326
327 default:
328 break;
329 }
330
331 /* Now we set the real spawn position */
332 bad_guy.set_pos(spawnpoint);
333
334 /* We don't want to count dispensed badguys in level stats */
335 bad_guy.m_countMe = false;
336
337 /* Set reference to dispenser in badguy itself */
338 if (m_limit_dispensed_badguys)
339 {
340 bad_guy.set_parent_dispenser(this);
341 m_current_badguys++;
342 }
343
344 Sector::get().add_object(std::move(game_object));
345 } catch(const std::exception& e) {
346 log_warning << "Error dispensing badguy: " << e.what() << std::endl;
347 return;
348 }
349 }
350}
351
352void
353Dispenser::freeze()
354{
355 if (m_broken) {
356 return;
357 }
358
359 set_group(COLGROUP_MOVING_STATIC);
360 m_frozen = true;
361
362 if (m_type == DispenserType::ROCKETLAUNCHER && m_sprite->has_action("iced-left"))
363 // Only swivel dispensers can use their left/right iced actions.
364 m_sprite->set_action(m_dir == Direction::LEFT ? "iced-left" : "iced-right", 1);
365 // when the sprite doesn't have separate actions for left and right or isn't a rocketlauncher,
366 // it tries to use an universal one.
367 else
368 {
369 if (m_type == DispenserType::CANNON && m_sprite->has_action("iced"))
370 m_sprite->set_action("iced", 1);
371 // When is the dispenser a cannon, it uses the "iced" action.
372 else
373 {
374 if (m_sprite->has_action("dropper-iced"))
375 m_sprite->set_action("dropper-iced", 1);
376 // When is the dispenser a dropper, it uses the "dropper-iced".
377 else
378 {
379 m_sprite->set_color(Color(0.6f, 0.72f, 0.88f));
380 m_sprite->stop_animation();
381 // When is the dispenser something else (unprobable), or has no matching iced sprite, it shades to blue.
382 }
383 }
384 }
385 m_dispense_timer.stop();
386}
387
388void
389Dispenser::unfreeze()
390{
391 /*set_group(colgroup_active);
392 frozen = false;
393
394 sprite->set_color(Color(1.00, 1.00, 1.00f));*/
395 BadGuy::unfreeze();
396
397 set_correct_action();
398 activate();
399}
400
401bool
402Dispenser::is_freezable() const
403{
404 return true;
405}
406
407bool
408Dispenser::is_flammable() const
409{
410 return false;
411}
412
413void
414Dispenser::set_correct_action()
415{
416 switch (m_type) {
417 case DispenserType::DROPPER:
418 m_sprite->set_action("dropper");
419 break;
420 case DispenserType::ROCKETLAUNCHER:
421 m_sprite->set_action(m_dir == Direction::LEFT ? "working-left" : "working-right");
422 break;
423 case DispenserType::CANNON:
424 m_sprite->set_action("working");
425 break;
426 case DispenserType::POINT:
427 m_sprite->set_action("invisible");
428 break;
429 default:
430 break;
431 }
432}
433
434ObjectSettings
435Dispenser::get_settings()
436{
437 ObjectSettings result = BadGuy::get_settings();
438
439 result.add_float(_("Interval (seconds)"), &m_cycle, "cycle");
440 result.add_bool(_("Random"), &m_random, "random", false);
441 result.add_badguy(_("Enemies"), &m_badguys, "badguy");
442 result.add_bool(_("Limit dispensed badguys"), &m_limit_dispensed_badguys,
443 "limit-dispensed-badguys", false);
444 result.add_int(_("Max concurrent badguys"), &m_max_concurrent_badguys,
445 "max-concurrent-badguys", 0);
446 result.add_enum(_("Type"), reinterpret_cast<int*>(&m_type),
447 {_("dropper"), _("rocket launcher"), _("cannon"), _("invisible")},
448 {"dropper", "rocketlauncher", "cannon", "point"},
449 static_cast<int>(DispenserType::DROPPER), "type");
450
451 result.reorder({"cycle", "random", "type", "badguy", "direction", "limit-dispensed-badguys", "max-concurrent-badguys", "x", "y"});
452
453 return result;
454}
455
456void
457Dispenser::after_editor_set()
458{
459 set_correct_action();
460}
461
462/* EOF */
463