1// SuperTux - Boss "Yeti"
2// Copyright (C) 2005 Matthias Braun <matze@braunis.de>
3// Copyright (C) 2006 Christoph Sommer <christoph.sommer@2006.expires.deltadevelopment.de>
4//
5// This program is free software: you can redistribute it and/or modify
6// it under the terms of the GNU General Public License as published by
7// the Free Software Foundation, either version 3 of the License, or
8// (at your option) any later version.
9//
10// This program is distributed in the hope that it will be useful,
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13// GNU General Public License for more details.
14//
15// You should have received a copy of the GNU General Public License
16// along with this program. If not, see <http://www.gnu.org/licenses/>.
17
18#include "badguy/yeti.hpp"
19
20#include <math.h>
21
22#include "audio/sound_manager.hpp"
23#include "badguy/yeti_stalactite.hpp"
24#include "math/random.hpp"
25#include "object/camera.hpp"
26#include "object/player.hpp"
27#include "sprite/sprite.hpp"
28#include "supertux/sector.hpp"
29#include "util/reader_mapping.hpp"
30#include "video/surface.hpp"
31
32namespace {
33const float JUMP_DOWN_VX = 250; /**< horizontal speed while jumping off the dais */
34const float JUMP_DOWN_VY = -250; /**< vertical speed while jumping off the dais */
35
36const float RUN_VX = 350; /**< horizontal speed while running */
37
38const float JUMP_UP_VX = 350; /**< horizontal speed while jumping on the dais */
39const float JUMP_UP_VY = -700; /**< vertical speed while jumping on the dais */
40
41const float STOMP_VY = -300; /** vertical speed while stomping on the dais */
42
43const float RUN_DISTANCE = 1060; /** Distance between the x-coordinates of left and right end positions */
44const float JUMP_SPACE = 448; /** Distance between jump position and stand position */
45const float STOMP_WAIT = .5; /**< time we stay on the dais before jumping again */
46const float SAFE_TIME = .5; /**< the time we are safe when tux just hit us */
47const int INITIAL_HITPOINTS = 5; /**< number of hits we can take */
48
49const float YETI_SQUISH_TIME = 3;
50
51const float SNOW_EXPLOSIONS_FREQUENCY = 8; /**< number of snowball explosions per second */
52const int SNOW_EXPLOSIONS_COUNT = 5; /**< number of snowballs per explosion */
53const float SNOW_EXPLOSIONS_VX = 150; /**< Speed of snowballs */
54const float SNOW_EXPLOSIONS_VY = -200; /**< Speed of snowballs */
55}
56
57Yeti::Yeti(const ReaderMapping& reader) :
58 BadGuy(reader, "images/creatures/yeti/yeti.sprite"),
59 state(),
60 state_timer(),
61 safe_timer(),
62 stomp_count(),
63 hit_points(),
64 hud_head(),
65 left_stand_x(),
66 right_stand_x(),
67 left_jump_x(),
68 right_jump_x(),
69 fixed_pos(),
70 hud_icon()
71{
72 reader.get("lives", hit_points, INITIAL_HITPOINTS);
73 m_countMe = true;
74 SoundManager::current()->preload("sounds/yeti_gna.wav");
75 SoundManager::current()->preload("sounds/yeti_roar.wav");
76
77 reader.get("hud-icon", hud_icon, "images/creatures/yeti/hudlife.png");
78 hud_head = Surface::from_file(hud_icon);
79
80 initialize();
81
82 reader.get("fixed-pos", fixed_pos, false);
83 if (fixed_pos) {
84 left_stand_x = 80;
85 right_stand_x = 1140;
86 left_jump_x = 528;
87 right_jump_x = 692;
88 } else {
89 recalculate_pos();
90 }
91}
92
93void
94Yeti::initialize()
95{
96 m_dir = Direction::RIGHT;
97 jump_down();
98}
99
100void
101Yeti::recalculate_pos()
102{
103 if (m_dir == Direction::RIGHT) {
104 left_stand_x = m_col.m_bbox.get_left();
105 right_stand_x = left_stand_x + RUN_DISTANCE;
106 } else {
107 right_stand_x = m_col.m_bbox.get_left();
108 left_stand_x = right_stand_x - RUN_DISTANCE;
109 }
110
111 left_jump_x = left_stand_x + JUMP_SPACE;
112 right_jump_x = right_stand_x - JUMP_SPACE;
113}
114
115void
116Yeti::draw(DrawingContext& context)
117{
118 // we blink when we are safe
119 if (safe_timer.started() && size_t(g_game_time * 40) % 2)
120 return;
121
122 draw_hit_points(context);
123
124 BadGuy::draw(context);
125}
126
127void
128Yeti::draw_hit_points(DrawingContext& context)
129{
130 if (hud_head)
131 {
132 context.push_transform();
133 context.set_translation(Vector(0, 0));
134
135 for (int i = 0; i < hit_points; ++i)
136 {
137 context.color().draw_surface(hud_head, Vector(BORDER_X + (static_cast<float>(i * hud_head->get_width())), BORDER_Y + 1), LAYER_FOREGROUND1);
138 }
139
140 context.pop_transform();
141 }
142}
143
144void
145Yeti::active_update(float dt_sec)
146{
147 switch (state) {
148 case JUMP_DOWN:
149 m_physic.set_velocity_x((m_dir==Direction::RIGHT)?+JUMP_DOWN_VX:-JUMP_DOWN_VX);
150 break;
151 case RUN:
152 m_physic.set_velocity_x((m_dir==Direction::RIGHT)?+RUN_VX:-RUN_VX);
153 if (((m_dir == Direction::RIGHT) && (get_pos().x >= right_jump_x)) || ((m_dir == Direction::LEFT) && (get_pos().x <= left_jump_x))) jump_up();
154 break;
155 case JUMP_UP:
156 m_physic.set_velocity_x((m_dir==Direction::RIGHT)?+JUMP_UP_VX:-JUMP_UP_VX);
157 if (((m_dir == Direction::RIGHT) && (get_pos().x >= right_stand_x)) || ((m_dir == Direction::LEFT) && (get_pos().x <= left_stand_x))) be_angry();
158 break;
159 case BE_ANGRY:
160 if (state_timer.check() && on_ground()) {
161 m_physic.set_velocity_y(STOMP_VY);
162 m_sprite->set_action((m_dir==Direction::RIGHT)?"stomp-right":"stomp-left");
163 SoundManager::current()->play("sounds/yeti_gna.wav");
164 }
165 break;
166 case SQUISHED:
167 {
168 Direction newdir = (int(state_timer.get_timeleft() * SNOW_EXPLOSIONS_FREQUENCY) % 2) ? Direction::LEFT : Direction::RIGHT;
169 if (m_dir != newdir && m_dir == Direction::RIGHT) {
170 SoundManager::current()->play("sounds/stomp.wav");
171 add_snow_explosions();
172 Sector::get().get_camera().shake(.05f, 0, 5);
173 }
174 m_dir = newdir;
175 m_sprite->set_action((m_dir==Direction::RIGHT)?"jump-right":"jump-left");
176 }
177 if (state_timer.check()) {
178 BadGuy::kill_fall();
179 state = FALLING;
180 m_physic.set_velocity_y(JUMP_UP_VY / 2); // Move up a bit before falling
181 // Add some extra explosions
182 for (int i = 0; i < 10; i++) {
183 add_snow_explosions();
184 }
185 run_dead_script();
186 }
187 break;
188 case FALLING:
189 break;
190 }
191
192 m_col.m_movement = m_physic.get_movement(dt_sec);
193}
194
195void
196Yeti::jump_down()
197{
198 m_sprite->set_action((m_dir==Direction::RIGHT)?"jump-right":"jump-left");
199 m_physic.set_velocity_x((m_dir==Direction::RIGHT)?(+JUMP_DOWN_VX):(-JUMP_DOWN_VX));
200 m_physic.set_velocity_y(JUMP_DOWN_VY);
201 state = JUMP_DOWN;
202}
203
204void
205Yeti::run()
206{
207 m_sprite->set_action((m_dir==Direction::RIGHT)?"walking-right":"walking-left");
208 m_physic.set_velocity_x((m_dir==Direction::RIGHT)?(+RUN_VX):(-RUN_VX));
209 m_physic.set_velocity_y(0);
210 state = RUN;
211}
212
213void
214Yeti::jump_up()
215{
216 m_sprite->set_action((m_dir==Direction::RIGHT)?"jump-right":"jump-left");
217 m_physic.set_velocity_x((m_dir==Direction::RIGHT)?(+JUMP_UP_VX):(-JUMP_UP_VX));
218 m_physic.set_velocity_y(JUMP_UP_VY);
219 state = JUMP_UP;
220}
221
222void
223Yeti::be_angry()
224{
225 //turn around
226 m_dir = (m_dir==Direction::RIGHT) ? Direction::LEFT : Direction::RIGHT;
227
228 m_sprite->set_action((m_dir==Direction::RIGHT) ? "stand-right" : "stand-left");
229 m_physic.set_velocity_x(0);
230 stomp_count = 0;
231 state = BE_ANGRY;
232 state_timer.start(STOMP_WAIT);
233}
234
235bool
236Yeti::collision_squished(GameObject& object)
237{
238 kill_squished(object);
239
240 return true;
241}
242
243void
244Yeti::kill_squished(GameObject& object)
245{
246 auto player = dynamic_cast<Player*>(&object);
247 if (player) {
248 player->bounce(*this);
249 take_hit(*player);
250 }
251}
252
253void Yeti::take_hit(Player& )
254{
255 if (safe_timer.started())
256 return;
257
258 SoundManager::current()->play("sounds/yeti_roar.wav");
259 hit_points--;
260
261 if (hit_points <= 0) {
262 // We're dead
263 m_physic.set_velocity_x(((m_dir==Direction::RIGHT)?+RUN_VX:-RUN_VX)/5);
264 m_physic.set_velocity_y(0);
265
266 // Set the badguy layer to be above the foremost, so that
267 // this does not reveal secret tilemaps:
268 m_layer = Sector::get().get_foremost_layer() + 1;
269 state = SQUISHED;
270 state_timer.start(YETI_SQUISH_TIME);
271 set_colgroup_active(COLGROUP_MOVING_ONLY_STATIC);
272 //sprite->set_action("dead"); // This sprite does not look very good
273 }
274 else {
275 safe_timer.start(SAFE_TIME);
276 }
277}
278
279void
280Yeti::kill_fall()
281{
282 // shooting bullets or being invincible won't work :)
283}
284
285void
286Yeti::drop_stalactite()
287{
288 // make a stalactite falling down and shake camera a bit
289 Sector::get().get_camera().shake(.1f, 0, 10);
290
291 auto player = get_nearest_player();
292 if (!player) return;
293
294 for (auto& stalactite : Sector::get().get_objects_by_type<YetiStalactite>())
295 {
296 if (stalactite.is_hanging()) {
297 if (hit_points >= 3) {
298 // drop stalactites within 3 of player, going out with each jump
299 float distancex = fabsf(stalactite.get_bbox().get_middle().x - player->get_bbox().get_middle().x);
300 if (distancex < static_cast<float>(stomp_count) * 32.0f) {
301 stalactite.start_shaking();
302 }
303 }
304 else { /* if (hitpoints < 3) */
305 // drop every 3rd pair of stalactites
306 if ((((static_cast<int>(stalactite.get_pos().x) + 16) / 64) % 3) == (stomp_count % 3)) {
307 stalactite.start_shaking();
308 }
309 }
310 }
311 }
312}
313
314void
315Yeti::collision_solid(const CollisionHit& hit)
316{
317 update_on_ground_flag(hit);
318 if (hit.top || hit.bottom) {
319 // hit floor or roof
320 m_physic.set_velocity_y(0);
321 switch (state) {
322 case JUMP_DOWN:
323 run();
324 break;
325 case RUN:
326 break;
327 case JUMP_UP:
328 break;
329 case BE_ANGRY:
330 // we just landed
331 if (!state_timer.started()) {
332 m_sprite->set_action((m_dir==Direction::RIGHT)?"stand-right":"stand-left");
333 stomp_count++;
334 drop_stalactite();
335
336 // go to other side after 3 jumps
337 if (stomp_count == 3) {
338 jump_down();
339 } else {
340 // jump again
341 state_timer.start(STOMP_WAIT);
342 }
343 }
344 break;
345 case SQUISHED:
346 break;
347 case FALLING:
348 break;
349 }
350 } else if (hit.left || hit.right) {
351 // hit wall
352 if(state != SQUISHED && state != FALLING)
353 jump_up();
354 }
355}
356
357bool
358Yeti::is_flammable() const
359{
360 return false;
361}
362
363ObjectSettings
364Yeti::get_settings()
365{
366 ObjectSettings result = BadGuy::get_settings();
367
368 result.add_text("hud-icon", &hud_icon, "hud-icon", std::string("images/creatures/yeti/hudlife.png"), OPTION_HIDDEN);
369 result.add_bool(_("Fixed position"), &fixed_pos, "fixed-pos", false);
370 result.add_int(_("Lives"), &hit_points, "lives", 5);
371
372 return result;
373}
374
375void
376Yeti::add_snow_explosions()
377{
378 for (int i = 0; i < SNOW_EXPLOSIONS_COUNT; i++) {
379 Vector pos = get_pos(), velocity;
380 velocity.x = SNOW_EXPLOSIONS_VX * graphicsRandom.randf(0.5f, 2.0f) * (graphicsRandom.rand(2) ? 1.0f : -1.0f);
381 velocity.y = SNOW_EXPLOSIONS_VY * graphicsRandom.randf(0.5f, 2.0f);
382 pos.x += static_cast<float>(m_sprite->get_width()) / 2.0f;
383 pos.x += static_cast<float>(m_sprite->get_width()) * graphicsRandom.randf(0.3f, 0.5f) * ((velocity.x > 0) ? 1.0f : -1.0f);
384 pos.y += static_cast<float>(m_sprite->get_height()) * graphicsRandom.randf(-0.3f, 0.3f);
385 velocity.x += m_physic.get_velocity_x();
386 Sector::get().add<SnowExplosionParticle>(pos, velocity);
387 }
388}
389
390Yeti::SnowExplosionParticle::SnowExplosionParticle(const Vector& pos, const Vector& velocity)
391 : BadGuy(pos, (velocity.x > 0) ? Direction::RIGHT : Direction::LEFT, "images/objects/bullets/icebullet.sprite")
392{
393 m_physic.set_velocity_x(velocity.x);
394 m_physic.set_velocity_y(velocity.y);
395 m_physic.enable_gravity(true);
396 set_state(STATE_FALLING);
397 m_layer = Sector::get().get_foremost_layer() + 1;
398}
399
400/* EOF */
401