1// IceCrusher - A block to stand on, which can drop down to crush the player
2// Copyright (C) 2008 Christoph Sommer <christoph.sommer@2008.expires.deltadevelopment.de>
3// Copyright (C) 2010 Florian Forster <supertux at octo.it>
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 "object/icecrusher.hpp"
19
20#include <algorithm>
21#include <math.h>
22
23#include "audio/sound_manager.hpp"
24#include "badguy/badguy.hpp"
25#include "object/coin.hpp"
26#include "object/camera.hpp"
27#include "object/particles.hpp"
28#include "object/player.hpp"
29#include "sprite/sprite.hpp"
30#include "supertux/sector.hpp"
31
32namespace {
33/* Maximum movement speed in pixels per LOGICAL_FPS */
34const float MAX_DROP_SPEED = 10.0f;
35const float RECOVER_SPEED_NORMAL = -3.125f;
36const float RECOVER_SPEED_LARGE = -2.0f;
37const float DROP_ACTIVATION_DISTANCE = 4.0f;
38const float PAUSE_TIME_NORMAL = 0.5f;
39const float PAUSE_TIME_LARGE = 1.0f;
40}
41
42IceCrusher::IceCrusher(const ReaderMapping& reader) :
43 MovingSprite(reader, "images/creatures/icecrusher/icecrusher.sprite", LAYER_OBJECTS, COLGROUP_STATIC),
44 state(IDLE),
45 start_position(m_col.m_bbox.p1()),
46 physic(),
47 cooldown_timer(0.0),
48 lefteye(),
49 righteye(),
50 whites(),
51 ic_size(NORMAL)
52{
53 // TODO: icecrusher hitting deserves its own sounds-
54 // one for hitting the ground, one for hitting Tux
55 if ( m_sprite_name.find("rock_crusher") != std::string::npos ||
56 m_sprite_name.find("moss_crusher") != std::string::npos )
57 {
58 SoundManager::current()->preload("sounds/thud.ogg");
59 }
60 else
61 {
62 SoundManager::current()->preload("sounds/brick.wav");
63 }
64
65 set_state(state, true);
66 after_sprite_set();
67}
68
69void
70IceCrusher::set_state(IceCrusherState state_, bool force)
71{
72 if ((state == state_) && (!force)) return;
73 switch (state_) {
74 case IDLE:
75 set_group(COLGROUP_STATIC);
76 physic.enable_gravity (false);
77 m_sprite->set_action("idle");
78 break;
79 case CRUSHING:
80 set_group(COLGROUP_MOVING_STATIC);
81 physic.reset ();
82 physic.enable_gravity (true);
83 m_sprite->set_action("crushing");
84 break;
85 case RECOVERING:
86 set_group(COLGROUP_MOVING_STATIC);
87 physic.enable_gravity (false);
88 m_sprite->set_action("recovering");
89 break;
90 default:
91 log_debug << "IceCrusher in invalid state" << std::endl;
92 break;
93 }
94 state = state_;
95}
96
97HitResponse
98IceCrusher::collision(GameObject& other, const CollisionHit& hit)
99{
100 auto player = dynamic_cast<Player*>(&other);
101
102 // If the other object is the player, and the collision is at the
103 // bottom of the ice crusher, hurt the player.
104 if (player && hit.bottom) {
105 SoundManager::current()->play("sounds/brick.wav");
106 if (state == CRUSHING)
107 set_state(RECOVERING);
108 if (player->is_invincible()) {
109 return ABORT_MOVE;
110 }
111 player->kill(false);
112 return FORCE_MOVE;
113 }
114 auto badguy = dynamic_cast<BadGuy*>(&other);
115 if (badguy) {
116 badguy->kill_fall();
117 }
118
119 auto heavy_coin = dynamic_cast<HeavyCoin*>(&other);
120 if(heavy_coin) {
121 return ABORT_MOVE;
122 }
123 return FORCE_MOVE;
124}
125
126void
127IceCrusher::collision_solid(const CollisionHit& hit)
128{
129 switch (state) {
130 case RECOVERING:
131 case IDLE:
132 break;
133 case CRUSHING:
134 if (hit.bottom) {
135 if (ic_size == LARGE) {
136 cooldown_timer = PAUSE_TIME_LARGE;
137 Sector::get().get_camera().shake (0.125f, 0.0f, 16.0f);
138 SoundManager::current()->play("sounds/brick.wav");
139 // throw some particles, bigger and more for large icecrusher
140 for (int j = 0; j < 9; j++)
141 {
142 Sector::get().add<Particles>(
143 Vector(m_col.m_bbox.get_right() - static_cast<float>(j) * 8.0f - 4.0f, m_col.m_bbox.get_bottom()),
144 0, 90-5*j, 140, 380, Vector(0.0f, 300.0f),
145 1, Color(.6f, .6f, .6f), 5, 1.8f, LAYER_OBJECTS+1);
146 Sector::get().add<Particles>(
147 Vector(m_col.m_bbox.get_left() + static_cast<float>(j) * 8.0f + 4.0f, m_col.m_bbox.get_bottom()),
148 270+5*j, 360, 140, 380, Vector(0.0f, 300.0f),
149 1, Color(.6f, .6f, .6f), 5, 1.8f, LAYER_OBJECTS+1);
150 }
151 }
152 else {
153 cooldown_timer = PAUSE_TIME_NORMAL;
154 Sector::get().get_camera().shake (0.1f, 0.0, 8.0);
155 if ( m_sprite_name.find("rock_crusher") != std::string::npos ||
156 m_sprite_name.find("moss_crusher") != std::string::npos )
157 {
158 SoundManager::current()->play("sounds/thud.ogg");
159 }
160 else
161 {
162 SoundManager::current()->play("sounds/brick.wav");
163 }
164 // throw some particles
165 for (int j = 0; j < 5; j++)
166 {
167 Sector::get().add<Particles>(
168 Vector(m_col.m_bbox.get_right() - static_cast<float>(j) * 8.0f - 4.0f,
169 m_col.m_bbox.get_bottom()),
170 0, 90+10*j, 140, 260, Vector(0, 300),
171 1, Color(.6f, .6f, .6f), 4, 1.6f, LAYER_OBJECTS+1);
172 Sector::get().add<Particles>(
173 Vector(m_col.m_bbox.get_left() + static_cast<float>(j) * 8.0f + 4.0f,
174 m_col.m_bbox.get_bottom()),
175 270+10*j, 360, 140, 260, Vector(0, 300),
176 1, Color(.6f, .6f, .6f), 4, 1.6f, LAYER_OBJECTS+1);
177 }
178 }
179 set_state(RECOVERING);
180 }
181 break;
182 default:
183 log_debug << "IceCrusher in invalid state" << std::endl;
184 break;
185 }
186}
187
188void
189IceCrusher::update(float dt_sec)
190{
191 if (cooldown_timer >= dt_sec)
192 {
193 cooldown_timer -= dt_sec;
194 return;
195 }
196 else if (cooldown_timer != 0.0f)
197 {
198 dt_sec -= cooldown_timer;
199 cooldown_timer = 0.0;
200 }
201
202 switch (state) {
203 case IDLE:
204 m_col.m_movement = Vector (0, 0);
205 if (found_victim())
206 set_state(CRUSHING);
207 break;
208 case CRUSHING:
209 m_col.m_movement = physic.get_movement (dt_sec);
210 if (m_col.m_movement.y > MAX_DROP_SPEED)
211 m_col.m_movement.y = MAX_DROP_SPEED;
212 break;
213 case RECOVERING:
214 if (m_col.m_bbox.get_top() <= start_position.y+1) {
215 set_pos(start_position);
216 m_col.m_movement = Vector (0, 0);
217 if (ic_size == LARGE)
218 cooldown_timer = PAUSE_TIME_LARGE;
219 else
220 cooldown_timer = PAUSE_TIME_NORMAL;
221 set_state(IDLE);
222 }
223 else {
224 if (ic_size == LARGE)
225 m_col.m_movement = Vector (0, RECOVER_SPEED_LARGE);
226 else
227 m_col.m_movement = Vector (0, RECOVER_SPEED_NORMAL);
228 }
229 break;
230 default:
231 log_debug << "IceCrusher in invalid state" << std::endl;
232 break;
233 }
234}
235
236void
237IceCrusher::draw(DrawingContext& context)
238{
239 m_sprite->draw(context.color(), get_pos(), m_layer+2);
240 if (!(state == CRUSHING) && m_sprite->has_action("whites"))
241 {
242 // draw icecrusher's eyes slightly behind
243 lefteye->draw(context.color(), get_pos()+eye_position(false), m_layer+1);
244 righteye->draw(context.color(), get_pos()+eye_position(true), m_layer+1);
245 // draw the whites of icecrusher's eyes even further behind
246 whites->draw(context.color(), get_pos(), m_layer);
247 }
248}
249
250void
251IceCrusher::after_editor_set() {
252 MovingSprite::after_editor_set();
253 after_sprite_set();
254}
255
256bool
257IceCrusher::found_victim() const
258{
259 if (auto* player = Sector::get().get_nearest_player(m_col.m_bbox))
260 {
261 const Rectf& player_bbox = player->get_bbox();
262 Rectf crush_area = Rectf(m_col.m_bbox.get_left()+1, m_col.m_bbox.get_bottom(),
263 m_col.m_bbox.get_right()-1, std::max(m_col.m_bbox.get_bottom(),player_bbox.get_top()-1));
264 if ((player_bbox.get_top() >= m_col.m_bbox.get_bottom()) /* player is below crusher */
265 && (player_bbox.get_right() > (m_col.m_bbox.get_left() - DROP_ACTIVATION_DISTANCE))
266 && (player_bbox.get_left() < (m_col.m_bbox.get_right() + DROP_ACTIVATION_DISTANCE))
267 && (Sector::get().is_free_of_statics(crush_area, this, false)) /* and area to player is free of objects */) {
268 return true;
269 }
270 }
271
272 return false;
273}
274
275Vector
276IceCrusher::eye_position(bool right) const
277{
278 if (state == IDLE)
279 {
280 if (auto* player = Sector::get().get_nearest_player (m_col.m_bbox))
281 {
282 // Icecrusher focuses on approximate position of player's head
283 const float player_focus_x = (player->get_bbox().get_right() + player->get_bbox().get_left()) * 0.5f;
284 const float player_focus_y = player->get_bbox().get_bottom() * 0.25f + player->get_bbox().get_top() * 0.75f;
285 // Icecrusher's approximate origin of line-of-sight
286 const float crusher_origin_x = m_col.m_bbox.get_middle().x;
287 const float crusher_origin_y = m_col.m_bbox.get_middle().y;
288 // Line-of-sight displacement from icecrusher to player
289 const float displacement_x = player_focus_x - crusher_origin_x;
290 const float displacement_y = player_focus_y - crusher_origin_y;
291 const float displacement_mag = powf(powf(displacement_x, 2.0f) + powf(displacement_y, 2.0f), 0.5f);
292 // Determine weighting for eye displacement along x given icecrusher eye shape
293 int weight_x = m_sprite->get_width()/64 * (((displacement_x > 0) == right) ? 1 : 4);
294 int weight_y = m_sprite->get_width()/64 * 2;
295
296 return Vector(displacement_x / displacement_mag * static_cast<float>(weight_x),
297 displacement_y / displacement_mag * static_cast<float>(weight_y) - static_cast<float>(weight_y));
298 }
299 }
300 else if (state == RECOVERING)
301 {
302 // Eyes spin while icecrusher is recovering, giving a dazed impression
303 return Vector(sinf((right ? 1 : -1) * // X motion of each eye is opposite of the other
304 (get_pos().y/13 - // Phase factor due to y position
305 (ic_size==NORMAL ? RECOVER_SPEED_NORMAL : RECOVER_SPEED_LARGE) + cooldown_timer * 13.0f)) * //Phase factor due to cooldown timer
306 static_cast<float>(m_sprite->get_width()) / 64.0f * 2.0f - (right ? 1 : -1) * // Amplitude dependent on size
307 static_cast<float>(m_sprite->get_width()) / 64.0f * 2.0f, // Offset to keep eyes visible
308
309 cosf((right ? 3.1415f : 0.0f) + // Eyes spin out of phase of eachother
310 get_pos().y / 13.0f - // Phase factor due to y position
311 (ic_size==NORMAL ? RECOVER_SPEED_NORMAL : RECOVER_SPEED_LARGE) + cooldown_timer * 13.0f) * //Phase factor due to cooldown timer
312 static_cast<float>(m_sprite->get_width()) / 64.0f * 2.0f - // Amplitude dependent on size
313 static_cast<float>(m_sprite->get_width()) / 64.0f * 2.0f); // Offset to keep eyes visible
314 }
315
316 return Vector(0,0);
317}
318
319void
320IceCrusher::after_sprite_set()
321{
322 float sprite_width = static_cast<float>(m_sprite->get_width());
323 if (sprite_width >= 128.0f)
324 ic_size = LARGE;
325
326 if (!m_sprite->has_action("whites"))
327 {
328 lefteye.reset();
329 righteye.reset();
330 whites.reset();
331 }
332 else
333 {
334 lefteye = m_sprite->clone();
335 lefteye->set_action("lefteye");
336 righteye = m_sprite->clone();
337 righteye->set_action("righteye");
338 whites = m_sprite->clone();
339 whites->set_action("whites");
340 }
341}
342
343/* EOF */
344