1 /*
2 * effects.cc - Special effects.
3 *
4 * Copyright (C) 2000-2013 The Exult Team
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, write to the Free Software
18 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
19 */
20
21 #ifdef HAVE_CONFIG_H
22 # include <config.h>
23 #endif
24
25 #include "gamewin.h"
26 #include "gamemap.h"
27 #include "gameclk.h"
28 #include "actors.h"
29 #include "effects.h"
30 #include "Zombie.h"
31 #include "dir.h"
32 #include "chunks.h"
33 #include "Audio.h"
34 #include "Gump_manager.h"
35 #include "game.h"
36 #include "Gump.h"
37 #include "egg.h"
38 #include "shapeinf.h"
39 #include "ammoinf.h"
40 #include "weaponinf.h"
41 #include "ucmachine.h"
42 #include "ignore_unused_variable_warning.h"
43
44 #include "SDL_timer.h"
45
46 #include "AudioMixer.h"
47 using namespace Pentagram;
48
49 using std::rand;
50 using std::string;
51 using std::strlen;
52 using std::vector;
53
54 int Cloud::randcnt = 0;
55 int Lightning_effect::active = 0;
56
57 /**
58 * Clean up.
59 */
60
~Effects_manager()61 Effects_manager::~Effects_manager() {
62 remove_all_effects(false);
63 }
64
65 /**
66 * Add text over a given item.
67 * @param msg text to add
68 * @param item Item text ID's, or null.
69 */
70
add_text(const char * msg,Game_object * item)71 void Effects_manager::add_text(
72 const char *msg,
73 Game_object *item
74 ) {
75 if (!msg) // Happens with edited games.
76 return;
77 // Don't duplicate for item.
78 if (std::find_if(texts.cbegin(), texts.cend(),
79 [item](const auto& el) { return el->is_text(item); }) != texts.cend())
80 return; // Already have text on this.
81
82 // txt->paint(this); // Draw it.
83 // painted = 1;
84 texts.emplace_front(std::make_unique<Text_effect>(msg, item, gwin));
85 }
86
87 /**
88 * Add a text object at a given spot.
89 * @param msg text to add
90 * @param x x coord. on screen.
91 * @param y y coord. on screen.
92 */
93
add_text(const char * msg,int x,int y)94 void Effects_manager::add_text(
95 const char *msg,
96 int x, int y
97 ) {
98
99 texts.emplace_front(std::make_unique<Text_effect>(
100 msg,
101 gwin->get_scrolltx() + x / c_tilesize,
102 gwin->get_scrollty() + y / c_tilesize,
103 gwin)
104 );
105 }
106
107 /**
108 * Add a text object in the center of the screen
109 */
110
center_text(const char * msg)111 void Effects_manager::center_text(
112 const char *msg
113 ) {
114 remove_text_effects();
115 Shape_manager *sman = Shape_manager::get_instance();
116 add_text(msg, (gwin->get_width() - sman->get_text_width(0, msg)) / 2,
117 gwin->get_height() / 2);
118 }
119
120 /**
121 * Add an effect at the start of the chain.
122 */
123
add_effect(std::unique_ptr<Special_effect> effect)124 void Effects_manager::add_effect(
125 std::unique_ptr<Special_effect> effect
126 ) {
127 effects.emplace_front(std::move(effect));
128 }
129
130 /**
131 * Remove a given object's text effect.
132 */
133
remove_text_effect(Game_object * item)134 void Effects_manager::remove_text_effect(
135 Game_object *item // Item text was added for.
136 ) {
137 auto effectToDelete = std::find_if(texts.begin(), texts.end(),
138 [item](const auto& el) { return el->is_text(item); });
139 if (effectToDelete == texts.end())
140 return;
141 else {
142 texts.erase(effectToDelete);
143 gwin->paint();
144 }
145 }
146
147 /**
148 * Remove a sprite from the chain and return it to caller
149 */
150
remove_effect(Special_effect * effect)151 std::unique_ptr<Special_effect> Effects_manager::remove_effect(
152 Special_effect *effect
153 ) {
154 auto toDelete = std::find_if(std::begin(effects), std::end(effects),
155 [effect](const auto& ef) { return ef.get() == effect; });
156 decltype(effects)::value_type result;
157 if (toDelete != effects.end()) {
158 result = std::move(*toDelete);
159 effects.erase(toDelete);
160 }
161 return result;
162 }
163
164 /**
165 * Remove text from the chain and delete it.
166 */
167
remove_text_effect(Text_effect * txt)168 void Effects_manager::remove_text_effect(
169 Text_effect *txt
170 ) {
171 texts.remove_if([txt](const auto& el) { return el.get() == txt; });
172 }
173
174 /**
175 * Remove all text items.
176 */
177
remove_all_effects(bool repaint)178 void Effects_manager::remove_all_effects(
179 bool repaint
180 ) {
181 if (effects.empty() && texts.empty())
182 return;
183 effects.clear();
184 texts.clear();
185 if (repaint)
186 gwin->paint(); // Just paint whole screen.
187 }
188
189 /**
190 * Remove text effects.
191 */
192
remove_text_effects()193 void Effects_manager::remove_text_effects(
194 ) {
195 texts.clear();
196 gwin->set_all_dirty();
197 }
198
199 /**
200 * Remove weather effects.
201 * @param dist Only remove those from eggs at least this far away.
202 */
203
remove_weather_effects(int dist)204 void Effects_manager::remove_weather_effects(
205 int dist
206 ) {
207 Actor *main_actor = gwin->get_main_actor();
208 Tile_coord apos = main_actor ? main_actor->get_tile()
209 : Tile_coord(-1, -1, -1);
210 effects.remove_if([&](const auto& ef) {
211 return ef->is_weather() &&
212 (!dist ||
213 static_cast<Weather_effect*>(ef.get())->out_of_range(apos, dist));
214 });
215 gwin->set_all_dirty();
216 }
217
218 /**
219 * Remove lightning effects.
220 */
221
remove_usecode_lightning()222 void Effects_manager::remove_usecode_lightning(
223 ) {
224 effects.remove_if([](const auto& ef) {
225 return ef->is_usecode_lightning();
226 });
227 gwin->set_all_dirty();
228 }
229
230 /**
231 * Find last numbered weather effect added.
232 */
233
get_weather()234 int Effects_manager::get_weather(
235 ) {
236 auto found = std::find_if(effects.cbegin(), effects.cend(), [](const auto& ef) {
237 return ef->is_weather() &&
238 (static_cast<Weather_effect *>(ef.get())->get_num() >= 0);
239 });
240 return found != effects.cend()
241 ? static_cast<Weather_effect *>((*found).get())->get_num()
242 : 0;
243 }
244
245 /**
246 * Construct and store game window singleton
247 * TODO check if passing it from other scope
248 * won't be better
249 */
250
Special_effect()251 Special_effect::Special_effect(
252 ) : gwin(Game_window::get_instance())
253 {}
254
255 /**
256 * Destructor
257 * Takes advantage of the fact that gwin
258 * is stored.
259 */
~Special_effect()260 Special_effect::~Special_effect(
261 )
262 {
263 if (in_queue())
264 gwin->get_tqueue()->remove(this);
265 }
266
267 /**
268 * Paint them all.
269 */
270
paint()271 void Effects_manager::paint(
272 ) {
273 for (auto& effect : effects)
274 effect->paint();
275 }
276
277 /**
278 * Paint all text.
279 */
280
paint_text()281 void Effects_manager::paint_text(
282 ) {
283 for (auto& txt : texts)
284 txt->paint();
285 }
286
287 /**
288 * Paint all text.
289 */
update_dirty_text()290 void Effects_manager::update_dirty_text(
291 ) {
292 for (auto& txt : texts)
293 txt->update_dirty();
294 }
295 /**
296 * Some special effects may not need painting.
297 */
298
paint()299 void Special_effect::paint(
300 ) {
301 }
302
303 /**
304 * Create an animation from the 'sprites.vga' file.
305 */
306
Sprites_effect(int num,Tile_coord const & p,int dx,int dy,int delay,int frm,int rps)307 Sprites_effect::Sprites_effect(
308 int num, // Index.
309 Tile_coord const &p, // Position within world.
310 int dx, int dy, // Add to offset for each frame.
311 int delay, // Delay (msecs) before starting.
312 int frm, // Starting frame.
313 int rps // Reps, or <0 to go through frames.
314 ) : sprite(num, frm, SF_SPRITES_VGA), pos(p),
315 xoff(0), yoff(0), deltax(dx), deltay(dy), reps(rps) {
316 frames = sprite.get_num_frames();
317 // Start.
318 gwin->get_tqueue()->add(Game::get_ticks() + delay, this);
319 }
320
321 /**
322 * Create an animation on an object.
323 */
324
Sprites_effect(int num,Game_object * it,int xf,int yf,int dx,int dy,int frm,int rps)325 Sprites_effect::Sprites_effect(
326 int num, // Index.
327 Game_object *it, // Item to put effect by.
328 int xf, int yf, // Offset from actor in pixels.
329 int dx, int dy, // Add to offset on each frame.
330 int frm, // Starting frame.
331 int rps // Reps, or <0 to go through frames.
332 ) : sprite(num, frm, SF_SPRITES_VGA), item(weak_from_obj(it)), xoff(xf),
333 yoff(yf), deltax(dx), deltay(dy), reps(rps) {
334 pos = it->get_tile();
335 frames = sprite.get_num_frames();
336 // Start immediately.
337 gwin->get_tqueue()->add(Game::get_ticks(), this);
338 }
339
340 /**
341 * Add a dirty rectangle for the current position and frame.
342 */
343
add_dirty(int frnum)344 inline void Sprites_effect::add_dirty(
345 int frnum
346 ) {
347 if (pos.tx == -1 || frnum == -1)
348 return; // Already at destination.
349 Shape_frame *shape = sprite.get_shape();
350 int lp = pos.tz / 2;
351
352 gwin->add_dirty(gwin->clip_to_win(gwin->get_shape_rect(shape,
353 xoff + (pos.tx - lp - gwin->get_scrolltx())*c_tilesize,
354 yoff + (pos.ty - lp - gwin->get_scrollty())*c_tilesize
355 ).enlarge((3 * c_tilesize) / 2)));
356 }
357
358 /**
359 * Animation.
360 */
361
handle_event(unsigned long curtime,uintptr udata)362 void Sprites_effect::handle_event(
363 unsigned long curtime, // Current time of day.
364 uintptr udata
365 ) {
366 int frame_num = sprite.get_framenum();
367 int delay = gwin->get_std_delay();// Delay between frames. Needs to
368 // match usecode animations.
369 if (!reps || (reps < 0 && frame_num == frames)) { // At end?
370 // Remove & delete this.
371 gwin->set_all_dirty();
372 auto ownHandle = eman->remove_effect(this);
373 return;
374 }
375 add_dirty(frame_num); // Clear out old.
376 gwin->set_painted();
377 Game_object_shared item_obj = item.lock();
378 if (item_obj) // Following actor?
379 pos = item_obj->get_tile();
380 xoff += deltax; // Add deltas.
381 yoff += deltay;
382 frame_num++; // Next frame.
383 if (reps > 0) { // Given a count?
384 --reps;
385 frame_num %= frames;
386 }
387 add_dirty(frame_num); // Want to paint new frame.
388 sprite.set_frame(frame_num);
389 // Add back to queue for next time.
390 gwin->get_tqueue()->add(curtime + delay, this, udata);
391 }
392
393 /**
394 * Render.
395 */
396
paint()397 void Sprites_effect::paint(
398 ) {
399 if (sprite.get_framenum() >= frames)
400 return;
401 int lp = pos.tz / 2; // Account for lift.
402 sprite.paint_shape(
403 xoff + (pos.tx - lp - gwin->get_scrolltx())*c_tilesize - gwin->get_scrolltx_lo(),
404 yoff + (pos.ty - lp - gwin->get_scrollty())*c_tilesize - gwin->get_scrolltx_lo());
405 }
406
get_explosion_shape(int weap,int proj)407 static inline int get_explosion_shape(
408 int weap,
409 int proj
410 ) {
411 int shp = proj >= 0 ? proj : (weap >= 0 ? weap : 704);
412 return ShapeID::get_info(shp).get_explosion_sprite();
413 }
414
get_explosion_sfx(int weap,int proj)415 static inline int get_explosion_sfx(
416 int weap,
417 int proj
418 ) {
419 int shp = proj >= 0 ? proj : (weap >= 0 ? weap : 704);
420 return ShapeID::get_info(shp).get_explosion_sfx();
421 }
422
423 /**
424 * Start explosion.
425 */
426
Explosion_effect(Tile_coord const & p,Game_object * exp,int delay,int weap,int proj,Game_object * att)427 Explosion_effect::Explosion_effect(
428 Tile_coord const &p,
429 Game_object *exp,
430 int delay, // Delay before starting (msecs).
431 int weap, // Weapon to use for damage calcs., or
432 // -1 for default(704 = powder keg).
433 int proj, // Projectile for e.g., burst arrows, 0 otherwise
434 Game_object *att //who is responsible for the explosion
435 // or nullptr for default
436 //Different sprites for different explosion types
437 ) : Sprites_effect(get_explosion_shape(weap, proj), p, 0, 0, delay),
438 explode(weak_from_obj(exp)),
439 weapon(weap >= 0 ? weap : (proj >= 0 ? proj : 704)),
440 projectile(proj), exp_sfx(get_explosion_sfx(weap, proj)),
441 attacker(weak_from_obj(att)) {
442 //if (exp && exp->get_shapenum() == 704) // powderkeg
443 if (exp && exp->get_info().is_explosive()) // powderkeg
444 exp->set_quality(1); // mark as detonating
445
446 if (!att || !att->as_actor())
447 // Blame avatar: if we have no living attacker.
448 attacker = weak_from_obj(gwin->get_main_actor());
449 }
450
451
handle_event(unsigned long curtime,uintptr udata)452 void Explosion_effect::handle_event(
453 unsigned long curtime, // Current time of day.
454 uintptr udata
455 ) {
456 int frnum = sprite.get_framenum();
457 if (!frnum) { // Max. volume, with stereo position.
458 Audio::get_ptr()->play_sound_effect(exp_sfx, pos, AUDIO_MAX_VOLUME);
459 }
460 if (frnum == frames / 4) {
461 // this was in ~Explosion_effect before
462 Game_object_shared exp_obj = explode.lock();
463 if (exp_obj && !exp_obj->is_pos_invalid()) {
464 Game_window::get_instance()->add_dirty(exp_obj.get());
465 exp_obj->remove_this();
466 explode = Game_object_weak();
467 }
468 Shape_frame *shape = sprite.get_shape();
469 int width = shape->get_width(); //Get the sprite's width
470 Game_object_vector vec; // Find objects near explosion.
471 Game_object::find_nearby(vec, pos, c_any_shapenum,
472 width / (2 * c_tilesize), 0);
473 // Objects could disappear due to Avatar being killed and teleported.
474 vector<Game_object_weak> cvec(vec.size());
475 Game_object::obj_vec_to_weak(cvec, vec);
476 for (auto& it : cvec) {
477 Game_object_shared obj = it.lock();
478 if (obj) {
479 Game_object_shared att_obj = attacker.lock();
480 obj->attacked(att_obj.get(), weapon, projectile,
481 true);
482 }
483 }
484 }
485 Sprites_effect::handle_event(curtime, udata);
486 }
487
488 /**
489 * Get direction in 1/16's starting from North.
490 */
491
Get_dir16(Tile_coord & t1,Tile_coord & t2)492 inline int Get_dir16(
493 Tile_coord &t1,
494 Tile_coord &t2
495 ) {
496 // Treat as cartesian coords.
497 return Get_direction16(t1.ty - t2.ty, t2.tx - t1.tx);
498 }
499
500 /**
501 * Initialize path & add to time queue.
502 */
503
init(Tile_coord const & s,Tile_coord const & d)504 void Projectile_effect::init(
505 Tile_coord const &s, // Source,
506 Tile_coord const &d // Destination.
507 ) {
508 no_blocking = false; // We'll check the ammo & weapon.
509 const Weapon_info *winfo = ShapeID::get_info(weapon).get_weapon_info();
510 if (winfo) {
511 no_blocking = no_blocking || winfo->no_blocking();
512 if (speed < 0)
513 speed = winfo->get_missile_speed();
514 autohit = winfo->autohits();
515 }
516 if (speed < 0)
517 speed = 4;
518 const Ammo_info *ainfo = ShapeID::get_info(projectile_shape).get_ammo_info();
519 if (ainfo) {
520 no_blocking = no_blocking || ainfo->no_blocking();
521 autohit = autohit || ainfo->autohits();
522 }
523 Game_object_shared att_obj = attacker.lock();
524 Game_object_shared tgt_obj = target.lock();
525 if (att_obj) { // Try to set start better.
526 int dir = tgt_obj ?
527 att_obj->get_direction(tgt_obj.get()) :
528 att_obj->get_direction(d);
529 pos = att_obj->get_missile_tile(dir);
530 } else
531 pos = s; // Get starting position.
532
533 Tile_coord dst = d;
534 if (tgt_obj) // Try to set end better.
535 dst = tgt_obj->get_center_tile();
536 else
537 dst.tz = pos.tz;
538 path = new Zombie(); // Create simple pathfinder.
539 // Find path. Should never fail.
540 bool explodes = (winfo && winfo->explodes()) || (ainfo && ainfo->explodes());
541 if (explodes && ainfo && ainfo->is_homing())
542 path->NewPath(pos, pos, nullptr); //A bit of a hack, I know...
543 else {
544 path->NewPath(pos, dst, nullptr);
545 if (att_obj) {
546 // Getprojectile out of shooter's volume.
547 Block vol = att_obj->get_block();
548 Tile_coord t;
549 bool done;
550 while (path->GetNextStep(t, done))
551 if (!vol.has_world_point(t.tx, t.ty, t.tz))
552 break;
553 pos = t;
554 }
555 }
556 int sprite_shape = sprite.get_shapenum();
557 set_sprite_shape(sprite_shape);
558 // Start after a slight delay.
559 gwin->get_tqueue()->add(Game::get_ticks(), this, gwin->get_std_delay() / 2);
560 }
561
set_sprite_shape(int s)562 void Projectile_effect::set_sprite_shape(
563 int s
564 ) {
565 if (s < 0) {
566 skip_render = true;
567 sprite.set_shape(s);
568 sprite.set_frame(0);
569 return;
570 }
571 sprite.set_shape(s);
572 frames = sprite.get_num_frames();
573 if (frames >= 24) { // Use frames 8-23, for direction
574 // going clockwise from North.
575 Tile_coord src = path->get_src();
576 Tile_coord dest = path->get_dest();
577 int dir = Get_dir16(src, dest);
578 sprite.set_frame(8 + dir);
579 }
580 //else if (frames == 1 && sprite.get_shapenum() != 704)
581 else if (frames == 1 && sprite.get_info().is_explosive())
582 sprite.set_frame(0); // (Don't show powder keg!)
583 else
584 skip_render = true; // We just won't show it.
585 add_dirty(); // Paint immediately.
586 }
587
588 /**
589 * Create a projectile animation.
590 */
Projectile_effect(Game_object * att,Game_object * to,int weap,int proj,int spr,int attpts,int spd)591 Projectile_effect::Projectile_effect(
592 Game_object *att, // Source of spell/attack.
593 Game_object *to, // End here, 'attack' it with shape.
594 int weap, // Weapon (bow, gun, etc.) shape.
595 int proj, // Projectile shape # in 'shapes.vga'.
596 int spr, // Shape to render on-screen or -1 for none.
597 int attpts, // Attack points of projectile.
598 int spd // Projectile speed, or -1 to use default.
599 ) : attacker(weak_from_obj(att)), target(weak_from_obj(to)),
600 weapon(weap), projectile_shape(proj),
601 sprite(spr, 0), return_path(false), skip_render(spr < 0),
602 speed(spd), attval(attpts), autohit(false) {
603 init(att->get_tile(), to->get_tile());
604 }
605
606 /**
607 * Constructor used by missile eggs & fire_projectile intrinsic.
608 */
609
Projectile_effect(Game_object * att,Tile_coord const & d,int weap,int proj,int spr,int attpts,int spd,bool retpath)610 Projectile_effect::Projectile_effect(
611 Game_object *att, // Source of spell/attack.
612 Tile_coord const &d, // End here.
613 int weap, // Weapon (bow, gun, etc.) shape.
614 int proj, // Projectile shape # in 'shapes.vga'.
615 int spr, // Shape to render on-screen or -1 for none.
616 int attpts, // Attack points of projectile.
617 int spd, // Projectile speed, or -1 to use default.
618 bool retpath // Return of a boomerang.
619 ) : attacker(weak_from_obj(att)), weapon(weap),
620 projectile_shape(proj),
621 sprite(spr, 0), return_path(retpath), skip_render(spr < 0),
622 speed(spd), attval(attpts), autohit(false) {
623 init(att->get_tile(), d);
624 }
625
626 /*
627 * Another used by missile eggs and for 'boomerangs'.
628 */
629
Projectile_effect(Tile_coord const & s,Game_object * to,int weap,int proj,int spr,int attpts,int spd,bool retpath)630 Projectile_effect::Projectile_effect(
631 Tile_coord const &s, // Start here.
632 Game_object *to, // End here, 'attack' it with shape.
633 int weap, // Weapon (bow, gun, etc.) shape.
634 int proj, // Projectile shape # in 'shapes.vga'.
635 int spr, // Shape to render on-screen or -1 for none.
636 int attpts, // Attack points of projectile.
637 int spd, // Projectile speed, or -1 to use default.
638 bool retpath // Return of a boomerang.
639 ) : target(weak_from_obj(to)),
640 weapon(weap), projectile_shape(proj),
641 sprite(spr, 0), return_path(retpath), skip_render(spr < 0),
642 speed(spd), attval(attpts), autohit(false) {
643 init(s, to->get_tile());
644 }
645
646 /**
647 * Delete.
648 */
649
~Projectile_effect()650 Projectile_effect::~Projectile_effect(
651 ) {
652 delete path;
653 }
654
655 /**
656 * Add a dirty rectangle for the current position and frame.
657 */
658
add_dirty()659 inline void Projectile_effect::add_dirty(
660 ) {
661 if (skip_render)
662 return;
663 Shape_frame *shape = sprite.get_shape();
664 // Force repaint of prev. position.
665 int liftpix = pos.tz * c_tilesize / 2;
666 gwin->add_dirty(gwin->clip_to_win(gwin->get_shape_rect(shape,
667 (pos.tx - gwin->get_scrolltx())*c_tilesize - liftpix,
668 (pos.ty - gwin->get_scrollty())*c_tilesize - liftpix
669 ).enlarge(c_tilesize / 2)));
670 }
671
672 /**
673 * See if something was hit.
674 *
675 * @return target hit, or 0.
676 */
677
Find_target(Game_window * gwin,Tile_coord pos)678 inline Game_object_shared Find_target(
679 Game_window *gwin,
680 Tile_coord pos
681 ) {
682 ignore_unused_variable_warning(gwin);
683 if (pos.tz % 5 == 0) // On floor?
684 pos.tz++; // Look up 1 tile.
685 Tile_coord dest = pos; // This gets modified.
686 if (!Map_chunk::is_blocked(pos, 1, MOVE_FLY, 0) &&
687 dest == pos)
688 return nullptr;
689 Game_object *ptr = Game_object::find_blocking(pos);
690 return shared_from_obj(ptr);
691 }
692
693 /**
694 * Animation.
695 */
696
handle_event(unsigned long curtime,uintptr udata)697 void Projectile_effect::handle_event(
698 unsigned long curtime, // Current time of day.
699 uintptr udata
700 ) {
701 int delay = gwin->get_std_delay() / 2;
702 add_dirty(); // Force repaint of old pos.
703 const Weapon_info *winf = ShapeID::get_info(weapon).get_weapon_info();
704 if (winf && winf->get_rotation_speed()) {
705 // The missile rotates (such as axes/boomerangs)
706 int new_frame = sprite.get_framenum() + winf->get_rotation_speed();
707 sprite.set_frame(new_frame > 23 ? ((new_frame - 8) % 16) + 8 : new_frame);
708 }
709 bool path_finished = false;
710 Game_object_shared att_obj = attacker.lock();
711 Game_object_shared tgt_obj = target.lock();
712 for (int i = 0; i < speed; i++) {
713 // This speeds up the missile.
714 path_finished = !path->GetNextStep(pos) || // Get next spot.
715 // If missile egg, detect target.
716 (!tgt_obj && !no_blocking &&
717 (tgt_obj = Find_target(gwin, pos)) != nullptr);
718 if (path_finished) {
719 target = Game_object_weak(tgt_obj);
720 break;
721 }
722 }
723 const Ammo_info *ainf = ShapeID::get_info(projectile_shape).get_ammo_info();
724 if (path_finished) {
725 // Done?
726 bool explodes = (winf && winf->explodes()) || (ainf && ainf->explodes());
727 if (return_path) { // Returned a boomerang?
728 Ireg_game_object_shared obj =
729 gmap->create_ireg_object(sprite.get_shapenum(), 0);
730 if (!tgt_obj || !tgt_obj->add(obj.get())) {
731 obj->set_flag(Obj_flags::okay_to_take);
732 obj->set_flag(Obj_flags::is_temporary);
733 obj->move(pos.tx, pos.ty, pos.tz, -1);
734 }
735 } else if (explodes) { // Do this here (don't want to explode
736 // returning weapon).
737 Tile_coord offset;
738 if (tgt_obj)
739 offset = Tile_coord(0, 0, tgt_obj->get_info().get_3d_height() / 2);
740 else
741 offset = Tile_coord(0, 0, 0);
742 if (ainf && ainf->is_homing())
743 eman->add_effect(std::make_unique<Homing_projectile>(weapon,
744 att_obj.get(), tgt_obj.get(), pos, pos + offset));
745 else
746 eman->add_effect(std::make_unique<Explosion_effect>(pos + offset,
747 nullptr, 0, weapon, projectile_shape, att_obj.get()));
748 target = Game_object_weak(); // Takes care of attack.
749 } else {
750 // Not teleported away ?
751 bool returns = (winf && winf->returns()) || (ainf && ainf->returns());
752 bool hit = false;
753 if (tgt_obj && att_obj != tgt_obj &&
754 // Aims for center tile, so check center tile.
755 tgt_obj->get_center_tile().distance(pos) < 3) {
756 hit = autohit || tgt_obj->try_to_hit(att_obj.get(), attval);
757 if (hit) {
758 tgt_obj->play_hit_sfx(weapon, true);
759 tgt_obj->attacked(att_obj.get(),
760 weapon, projectile_shape, false);
761 }
762 } else {
763 // Hack warning: this exists solely to make Mind Blast (SI)
764 // work as it does in the original when you target the
765 // avatar with the spell.
766 if (winf && winf->get_usecode() > 0)
767 ucmachine->call_usecode(winf->get_usecode(), nullptr,
768 Usecode_machine::weapon);
769 }
770 if (returns && att_obj && // boomerangs
771 att_obj->distance(pos) < 50) {
772 // not teleported away
773 auto proj = std::make_unique<Projectile_effect>(
774 pos, att_obj.get(), weapon, projectile_shape,
775 sprite.get_shapenum(), attval, speed, true);
776 proj->set_speed(speed);
777 proj->set_sprite_shape(sprite.get_shapenum());
778 eman->add_effect(std::move(proj));
779 } else {
780 // See if we should drop projectile.
781 bool drop = false;
782 // Seems to match originals quite well.
783 if (!winf)
784 drop = true;
785 else if (ainf) {
786 int ammo = winf->get_ammo_consumed();
787 int type = ainf->get_drop_type();
788 drop = (ammo >= 0 || ammo == -3) &&
789 (type == Ammo_info::always_drop ||
790 (!hit && type != Ammo_info::never_drop));
791 }
792 if (drop) {
793 Tile_coord dpos = Map_chunk::find_spot(pos, 3,
794 sprite.get_shapenum(), 0, 1);
795 if (dpos.tx != -1) {
796 Game_object_shared aobj = gmap->create_ireg_object(
797 sprite.get_shapenum(), 0);
798 if (!att_obj || att_obj->get_flag(Obj_flags::is_temporary))
799 aobj->set_flag(Obj_flags::is_temporary);
800 aobj->set_flag(Obj_flags::okay_to_take);
801 aobj->move(dpos);
802 }
803 }
804 }
805 }
806 add_dirty();
807 skip_render = true;
808 auto ownHandle = eman->remove_effect(this);
809 return;
810 }
811 add_dirty(); // Paint new spot/frame.
812 // Add back to queue for next time.
813 gwin->get_tqueue()->add(curtime + delay, this, udata);
814 }
815
816 /**
817 * Render.
818 */
819
paint()820 void Projectile_effect::paint(
821 ) {
822 if (skip_render)
823 return;
824 int liftpix = pos.tz * c_tilesize / 2;
825 sprite.paint_shape(
826 (pos.tx - gwin->get_scrolltx())*c_tilesize - liftpix - gwin->get_scrolltx_lo(),
827 (pos.ty - gwin->get_scrollty())*c_tilesize - liftpix - gwin->get_scrollty_lo());
828 }
829
830 /**
831 * Create a 'death vortex' or an 'energy mist'.
832 */
833
Homing_projectile(int shnum,Game_object * att,Game_object * trg,Tile_coord const & sp,Tile_coord const & tp)834 Homing_projectile::Homing_projectile( // A better name is welcome...
835 int shnum, // The projectile shape.
836 Game_object *att, // Who cast the spell.
837 Game_object *trg, // What to aim for.
838 Tile_coord const &sp, // Where to start.
839 Tile_coord const &tp // Target pos, if trg isn't an actor.
840 ) : sprite(ShapeID::get_info(shnum).get_explosion_sprite(), 0, SF_SPRITES_VGA),
841 next_damage_time(0), sfx(ShapeID::get_info(shnum).get_explosion_sfx()),
842 channel(-1) {
843 weapon = shnum;
844 attacker = weak_from_obj(att);
845 pos = sp;
846 dest = tp;
847 target = trg ? trg->as_actor() : nullptr;
848 stationary = target == nullptr; //If true, the sprite will 'park' at dest
849 frames = sprite.get_num_frames();
850 // Go for 20 seconds.
851 stop_time = Game::get_ticks() + 20 * 1000;
852 // Start immediately.
853 gwin->get_tqueue()->add(Game::get_ticks(), this);
854 channel = Audio::get_ptr()->play_sound_effect(sfx, pos, -1);
855 }
856
857 /**
858 * Add a dirty rectangle for the current position and frame.
859 *
860 * @return Width in pixels.
861 */
862
add_dirty()863 inline int Homing_projectile::add_dirty(
864 ) {
865 Shape_frame *shape = sprite.get_shape();
866 int liftpix = pos.tz * c_tilesize / 2;
867 gwin->add_dirty(gwin->clip_to_win(gwin->get_shape_rect(shape,
868 (pos.tx - gwin->get_scrolltx())*c_tilesize - liftpix,
869 (pos.ty - gwin->get_scrollty())*c_tilesize - liftpix
870 ).enlarge(c_tilesize / 2)));
871 return shape->get_width();
872 }
873
874 /**
875 * Animation.
876 */
877
handle_event(unsigned long curtime,uintptr udata)878 void Homing_projectile::handle_event(
879 unsigned long curtime, // Current time of day.
880 uintptr udata
881 ) {
882 int width = add_dirty(); // Repaint old area.
883
884 if ((target && !target->is_dead()) || stationary) {
885 //Move to target/destination if needed
886 Tile_coord tpos = stationary ? dest : target->get_tile();
887 int deltax = tpos.tx - pos.tx;
888 int deltay = tpos.ty - pos.ty;
889 int deltaz = tpos.tz +
890 (stationary ? 0 : target->get_info().get_3d_height() / 2) - pos.tz;
891 int absx = deltax >= 0 ? deltax : -deltax;
892 int absy = deltay >= 0 ? deltay : -deltay;
893 int absz = deltaz >= 0 ? deltaz : -deltaz;
894 uint32 dist = absx * absx + absy * absy + absz * absz;
895 if (dist > 1) {
896 if (deltax)
897 pos.tx += deltax / absx;
898 if (deltay)
899 pos.ty += deltay / absy;
900 if (deltaz)
901 pos.tz += deltaz / absz;
902 }
903 } else {
904 //The target has been killed; find another one
905 Actor_vector npcs; // Find NPC's.
906 Game_object::find_nearby_actors(npcs, pos, -1, 30);
907 Actor *nearest = nullptr;
908 uint32 bestdist = 100000;
909 for (auto *npc : npcs) {
910 if (!npc->is_in_party() && !npc->is_dead() &&
911 (npc->get_effective_alignment() >= Actor::evil)) {
912 Tile_coord npos = npc->get_tile();
913 int dx = npos.tx - pos.tx;
914 int dy = npos.ty - pos.ty;
915 int dz = npos.tz - pos.tz;
916 uint32 dist = dx * dx + dy * dy + dz * dz;
917 if (dist < bestdist) {
918 bestdist = dist;
919 nearest = npc;
920 }
921 }
922 }
923 target = nearest;
924 }
925 if (curtime > next_damage_time) { // Time to cause damage.
926 // Do it every second.
927 next_damage_time = curtime + 1000;
928 Actor_vector npcs; // Find NPC's.
929 Game_object::find_nearby_actors(npcs, pos, -1, width / (2 * c_tilesize));
930 for (auto *npc : npcs) {
931 if (!npc->is_in_party()) {
932 //Still powerful, but no longer overkill...
933 //also makes the enemy react, which is good
934 Game_object_shared att_obj = attacker.lock();
935 npc->attacked(att_obj.get(), weapon, weapon, true);
936 }
937 }
938 }
939 sprite.set_frame((sprite.get_framenum() + 1) % frames);
940
941 add_dirty(); // Paint new.
942 if (curtime < stop_time) { // Keep going?
943 gwin->get_tqueue()->add(curtime + 100, this, udata);
944 if (channel < 0)
945 return;
946 channel = Audio::get_ptr()->update_sound_effect(channel, pos);
947 } else {
948 if (channel >= 0) {
949 AudioMixer::get_instance()->stopSample(channel);
950 channel = -1;
951 }
952 gwin->set_all_dirty();
953 auto ownHandle = eman->remove_effect(this);
954 return;
955 }
956 }
957
958 /**
959 * Render.
960 */
961
paint()962 void Homing_projectile::paint(
963 ) {
964 int liftpix = pos.tz * c_tilesize / 2;
965 sprite.paint_shape(
966 (pos.tx - gwin->get_scrolltx())*c_tilesize - liftpix - gwin->get_scrolltx_lo(),
967 (pos.ty - gwin->get_scrollty())*c_tilesize - liftpix - gwin->get_scrollty_lo());
968 }
969
970 /**
971 * Figure text position.
972 */
973
Figure_text_pos()974 TileRect Text_effect::Figure_text_pos() {
975 Game_object_shared item_obj = item.lock();
976 if (item_obj) {
977 Gump_manager *gumpman = gwin->get_gump_man();
978 // See if it's in a gump.
979 Gump *gump = gumpman->find_gump(item_obj.get());
980 if (gump)
981 return gump->get_shape_rect(item_obj.get());
982 else {
983 Game_object *outer = item_obj->get_outermost();
984 if (!outer->get_chunk()) return pos;
985 TileRect r = gwin->get_shape_rect(outer);
986 r.x -= gwin->get_scrolltx_lo();
987 r.y -= gwin->get_scrollty_lo();
988 return r;
989 }
990 } else {
991 int x;
992 int y;
993 gwin->get_shape_location(tpos, x, y);
994 return TileRect(x, y, c_tilesize, c_tilesize);
995 }
996 }
997
998 /**
999 * Add dirty rectangle for current position.
1000 */
1001
add_dirty()1002 void Text_effect::add_dirty(
1003 ) {
1004 // Repaint slightly bigger rectangle.
1005 TileRect rect(pos.x - c_tilesize,
1006 pos.y - c_tilesize,
1007 width + 2 * c_tilesize, height + 2 * c_tilesize);
1008 gwin->add_dirty(gwin->clip_to_win(rect));
1009 }
1010
1011 /**
1012 * Initialize.
1013 */
1014
init()1015 void Text_effect::init(
1016 ) {
1017 set_always(true); // Always execute in time queue, even
1018 // when paused.
1019 width = 8 + sman->get_text_width(0, msg.c_str());
1020 height = 8 + sman->get_text_height(0);
1021 add_dirty(); // Force first paint.
1022 // Start immediately.
1023 gwin->get_tqueue()->add(Game::get_ticks(), this);
1024 if (msg[0] == '@')
1025 msg[0] = '"';
1026 int len = msg.size();
1027 if (msg[len - 1] == '@')
1028 msg[len - 1] = '"';
1029 }
1030
1031 /**
1032 * Create a text effect for a given object.
1033 */
1034
Text_effect(const string & m,Game_object * it,Game_window * gwin_)1035 Text_effect::Text_effect(
1036 const string &m, // A copy is made.
1037 Game_object *it, // Item text is on, or null.
1038 Game_window *gwin_ // Back-reference to gwin from Effects_manager
1039 ) : gwin(gwin_), msg(m), item(weak_from_obj(it)), pos(Figure_text_pos()), num_ticks(0) {
1040 init();
1041 }
1042
1043 /**
1044 * Create a text object.
1045 */
1046
Text_effect(const string & m,int t_x,int t_y,Game_window * gwin_)1047 Text_effect::Text_effect(
1048 const string &m, // A copy is made.
1049 int t_x, int t_y, // Abs. tile coords.
1050 Game_window *gwin_ // Back-reference to gwin from Effects_manager
1051 ) : gwin(gwin_), msg(m), tpos(t_x, t_y, 0), pos(Figure_text_pos()), num_ticks(0) {
1052 init();
1053 }
1054
1055 /**
1056 *
1057 */
1058
handle_event(unsigned long curtime,uintptr udata)1059 void Text_effect::handle_event(
1060 unsigned long curtime, // Current time of day.
1061 uintptr udata // Ignored.
1062 ) {
1063 ignore_unused_variable_warning(curtime, udata);
1064 if (++num_ticks == 10) { // About 1-2 seconds.
1065 // All done.
1066 add_dirty();
1067 eman->remove_text_effect(this);
1068 return;
1069 }
1070 // Back into queue.
1071 gwin->get_tqueue()->add(Game::get_ticks() + gwin->get_std_delay(), this);
1072
1073 update_dirty();
1074 }
1075
1076 /**
1077 * Reposition if necessary.
1078 */
1079
update_dirty()1080 void Text_effect::update_dirty(
1081 ) {
1082 // See if moved.
1083 TileRect npos = Figure_text_pos();
1084 if (npos == pos) // No change?
1085 return;
1086 add_dirty(); // Force repaint of old area.
1087 pos = npos; // Then set to repaint new.
1088 add_dirty();
1089 }
1090
~Text_effect()1091 Text_effect::~Text_effect(
1092 ) {
1093 if (in_queue())
1094 gwin->get_tqueue()->remove(this);
1095 }
1096
1097
1098 /**
1099 * Render.
1100 */
1101
paint()1102 void Text_effect::paint(
1103 ) {
1104 const char *ptr = msg.c_str();
1105 int len = strlen(ptr);
1106 sman->paint_text(0, ptr, len,
1107 pos.x,
1108 pos.y);
1109 }
1110
1111 /**
1112 * Init. a weather effect.
1113 */
1114
Weather_effect(int duration,int delay,int n,Game_object * egg)1115 Weather_effect::Weather_effect(
1116 int duration, // Length in game minutes.
1117 int delay, // Delay before starting.
1118 int n, // Weather number.
1119 Game_object *egg // Egg that started it, or null.
1120 ) : num(n) {
1121 if (egg)
1122 eggloc = egg->get_tile();
1123 else
1124 eggloc = Tile_coord(-1, -1, -1);
1125 stop_time = Game::get_ticks() + delay + duration * gwin->get_std_delay() * ticks_per_minute;
1126 // Start immediately.
1127 gwin->get_tqueue()->add(Game::get_ticks() + delay, this);
1128 }
1129
1130 /**
1131 * Are we far enough away from this to cancel it?
1132 * @param avpos Avatar's position.
1133 * @param dist Distance in tiles to cancel at.
1134 */
1135
out_of_range(Tile_coord & avpos,int dist)1136 bool Weather_effect::out_of_range(
1137 Tile_coord &avpos,
1138 int dist
1139 ) {
1140 if (eggloc.tx == -1) // Not created by an egg?
1141 return false;
1142 return eggloc.distance(avpos) >= dist;
1143 }
1144
1145
1146 /*
1147 * A generic raindrop/snowflake/magic sparkle particle:
1148 */
1149 class Particle : public ShapeID {
1150 long ax = -1, ay = -1; // Coords. where drawn in abs. pixels.
1151 bool forward = true;
1152 public:
Particle()1153 Particle()
1154 : ShapeID(0, -1, SF_SPRITES_VGA)
1155 { }
1156 // Move to next position.
move(long dx,long dy)1157 void move(long dx, long dy) {
1158 ax = dx;
1159 ay = dy;
1160 }
get_ax() const1161 long get_ax() const {
1162 return ax;
1163 }
get_ay() const1164 long get_ay() const {
1165 return ay;
1166 }
get_forward() const1167 bool get_forward() const {
1168 return forward;
1169 }
toggle_forward()1170 void toggle_forward() {
1171 forward = !forward;
1172 }
1173 };
1174
1175 class Particledrop {
1176 protected:
do_move(Particle & drop,int x,int y,int w,int h,int ascrollx,int ascrolly)1177 virtual void do_move(Particle &drop, int x, int y, int w, int h,
1178 int ascrollx, int ascrolly) {
1179 ignore_unused_variable_warning(drop, x, y, w, h, ascrollx, ascrolly);
1180 }
1181 public:
1182 virtual ~Particledrop() = default;
move(Particle & drop,int scrolltx,int scrollty,int w,int h)1183 void move
1184 (
1185 Particle &drop,
1186 int scrolltx, int scrollty,
1187 int w, int h
1188 ) {
1189 int frame = drop.get_framenum();
1190 uint32 ascrollx = scrolltx * static_cast<uint32>(c_tilesize);
1191 uint32 ascrolly = scrollty * static_cast<uint32>(c_tilesize);
1192 int ax = drop.get_ax();
1193 int ay = drop.get_ay();
1194 int x = ax - ascrollx;
1195 int y = ay - ascrolly;
1196 // Still on screen? Restore pix.
1197 if (frame >= 0 && x >= 0 && y >= 0 && x < w && y < h) {
1198 Game_window *gwin = Game_window::get_instance();
1199 gwin->add_dirty(gwin->clip_to_win(gwin->get_shape_rect(
1200 drop.get_shape(), x, y).enlarge(c_tilesize / 2)));
1201 }
1202 do_move(drop, x, y, w, h, ascrollx, ascrolly);
1203 }
paint(Particle & drop,int scrolltx,int scrollty,int w,int h)1204 void paint
1205 (
1206 Particle &drop,
1207 int scrolltx, int scrollty,
1208 int w, int h
1209 ) {
1210 uint32 ascrollx = scrolltx * static_cast<uint32>(c_tilesize);
1211 uint32 ascrolly = scrollty * static_cast<uint32>(c_tilesize);
1212 int ax = drop.get_ax();
1213 int ay = drop.get_ay();
1214 int x = ax - ascrollx;
1215 int y = ay - ascrolly;
1216 // Still on screen? Restore pix.
1217 if (x >= 0 && y >= 0 && x < w && y < h) {
1218 Game_window *gwin = Game_window::get_instance();
1219 drop.paint_shape(x, y);
1220 gwin->add_dirty(gwin->clip_to_win(gwin->get_shape_rect(
1221 drop.get_shape(), x, y).enlarge(c_tilesize / 2)));
1222 }
1223 }
1224 };
1225
1226 template<int fra0, int fraN, bool randomize>
set_frame(Particle & drop)1227 static inline void set_frame(Particle &drop) {
1228 int frame = drop.get_framenum();
1229 if (frame < 0) {
1230 if (randomize) {
1231 int dir = rand() % 2;
1232 if (dir)
1233 drop.toggle_forward();
1234 frame = fra0 + rand() % (fraN - fra0) + dir;
1235 } else
1236 frame = fra0;
1237 } else if (drop.get_forward()) {
1238 if (++frame == fraN)
1239 drop.toggle_forward();
1240 } else if (--frame == fra0)
1241 drop.toggle_forward();
1242 drop.set_frame(frame);
1243 }
1244
1245
1246 template<int fra0, int fraN, int delta, bool randomize>
1247 class Basicdrop : public Particledrop {
1248 protected:
do_move(Particle & drop,int x,int y,int w,int h,int ascrollx,int ascrolly)1249 void do_move(Particle &drop, int x, int y, int w, int h,
1250 int ascrollx, int ascrolly) override {
1251 set_frame<fra0, fraN, randomize>(drop);
1252 // Time to restart?
1253 if (x < 0 || x >= w || y < 0 || y >= h) {
1254 int r = rand();
1255 drop.move(ascrollx + r % (w - w / 8), ascrolly + r % (h - h / 4));
1256 } else // Next spot.
1257 drop.move(drop.get_ax() + delta, drop.get_ay() + delta);
1258 }
1259 };
1260
1261 // This looks slightly cooler:
1262 //using Raindrop = Basicdrop< 3, 6, 6, false>;
1263 using Raindrop = Basicdrop< 3, 7, 6, false>;
1264 using Snowflake = Basicdrop<13, 20, 1, false>;
1265 using Sparkle = Basicdrop<21, 27, 12, true>;
1266
1267 /*
1268 * Raining.
1269 */
1270 #define MAXDROPS 200
1271 template<typename Functor>
1272 class Rain_effect : public Weather_effect {
1273 protected:
1274 Particle drops[MAXDROPS]; // Drops moving down the screen.
1275 int num_drops; // # to actually use.
1276 bool gradual;
1277 Functor do_drop; // Controls how drops move.
change_ndrops(unsigned long curtime)1278 void change_ndrops(unsigned long curtime) {
1279 if (!gradual)
1280 return;
1281 if ((curtime > stop_time - 2500) && num_drops) {
1282 // End gradually.
1283 num_drops -= (rand() % 15);
1284 if (num_drops < 0)
1285 num_drops = 0;
1286 } else if (gradual && curtime < stop_time) { // Keep going?
1287 // Start gradually.
1288 if (num_drops < MAXDROPS)
1289 num_drops += (rand() % 5);
1290 if (num_drops > MAXDROPS)
1291 num_drops = MAXDROPS;
1292 }
1293 }
1294 public:
Rain_effect(int duration,int delay=0,int ndrops=MAXDROPS,int n=-1,Game_object * egg=nullptr)1295 Rain_effect(int duration, int delay = 0,
1296 int ndrops = MAXDROPS, int n = -1, Game_object *egg = nullptr)
1297 : Weather_effect(duration, delay, n, egg),
1298 num_drops(ndrops), gradual(ndrops == 0)
1299 { }
1300 // Execute when due.
handle_event(unsigned long curtime,uintptr udata)1301 void handle_event
1302 (
1303 unsigned long curtime, // Current time of day.
1304 uintptr udata
1305 ) override {
1306 // Gradual start/end.
1307 change_ndrops(curtime);
1308
1309 if (!gwin->is_main_actor_inside() &&
1310 !gumpman->showing_gumps(true)) {
1311 // Don't show rain inside buildings!
1312 Image_window8 *win = gwin->get_win();
1313 int w = win->get_game_width();
1314 int h = win->get_game_height();
1315 // Get transform table.
1316 int scrolltx = gwin->get_scrolltx();
1317 int scrollty = gwin->get_scrollty();
1318 // Move drops.
1319 for (int i = 0; i < num_drops; i++)
1320 do_drop.move(drops[i], scrolltx, scrollty, w, h);
1321 gwin->set_painted();
1322 }
1323 if (curtime >= stop_time) {
1324 gwin->set_all_dirty();
1325 eman->remove_effect(this);
1326 return;
1327 }
1328 gwin->get_tqueue()->add(curtime + 100, this, udata);
1329 }
1330 // Render.
paint()1331 void paint
1332 (
1333 ) override {
1334 if (gwin->is_main_actor_inside())
1335 return; // Inside.
1336 // Get transform table.
1337 int scrolltx = gwin->get_scrolltx();
1338 int scrollty = gwin->get_scrollty();
1339 Image_window8 *win = gwin->get_win();
1340 int w = win->get_game_width();
1341 int h = win->get_game_height();
1342 for (int i = 0; i < num_drops; i++)
1343 do_drop.paint(drops[i], scrolltx, scrollty, w, h);
1344 gwin->set_painted();
1345 }
1346 };
1347
1348 /**
1349 * End of lightning.
1350 */
1351
~Lightning_effect()1352 Lightning_effect::~Lightning_effect(
1353 ) {
1354 if (flashing) // Be sure palette is restored.
1355 Game_window::get_instance()->get_clock()->set_palette();
1356 }
1357
1358 /**
1359 * Lightning.
1360 */
1361
handle_event(unsigned long curtime,uintptr udata)1362 void Lightning_effect::handle_event(
1363 unsigned long curtime, // Current time of day.
1364 uintptr udata
1365 ) {
1366 int r = rand(); // Get a random #.
1367 int delay = 100; // Delay for next time.
1368 if (flashing) { // Just turned white? Restore.
1369 gclock->set_palette();
1370 flashing = false;
1371 active = false;
1372 if (curtime >= stop_time) {
1373 // Time to stop.
1374 eman->remove_effect(this);
1375 return;
1376 }
1377 if (r % 50 == 0) // Occassionally flash again.
1378 delay = (1 + r % 7) * 40;
1379 else // Otherwise, wait several secs.
1380 delay = (4000 + r % 3000);
1381 } else if ((fromusecode || !gwin->is_in_dungeon()) && !active) { // Time to flash.
1382 // Play thunder.
1383 Audio::get_ptr()->play_sound_effect(Audio::game_sfx(62));
1384 active = true;
1385 flashing = true;
1386 gwin->get_pal()->set(PALETTE_LIGHTNING);
1387 delay = (1 + r % 2) * 25;
1388 }
1389 gwin->get_tqueue()->add(curtime + delay, this, udata);
1390 }
1391
1392 /**
1393 * Start a storm.
1394 */
1395
Storm_effect(int duration,int delay,Game_object * egg)1396 Storm_effect::Storm_effect(
1397 int duration, // In game minutes.
1398 int delay, // In msecs.
1399 Game_object *egg // Egg that caused it, or null.
1400 ) : Weather_effect(duration, delay, 2, egg), start(true) {
1401 // Start raining soon.
1402 eman->add_effect(std::make_unique<Clouds_effect>(duration + 1, delay));
1403 int rain_delay = 20 + rand() % 1000;
1404 eman->add_effect(std::make_unique<Rain_effect<Raindrop>>(duration + 2, rain_delay, 0));
1405 int lightning_delay = rain_delay + rand() % 500;
1406 eman->add_effect(std::make_unique<Lightning_effect>(duration - 2, lightning_delay));
1407 }
1408
1409 /**
1410 * Start/end of storm.
1411 */
handle_event(unsigned long curtime,uintptr udata)1412 void Storm_effect::handle_event(
1413 unsigned long curtime, // Current time of day.
1414 uintptr udata
1415 ) {
1416 ignore_unused_variable_warning(curtime);
1417 if (start) {
1418 start = false;
1419 // Nothing more to do but end.
1420 gwin->get_tqueue()->add(stop_time, this, udata);
1421 } else { // Must be time to stop.
1422 auto ownHandle = eman->remove_effect(this);
1423 return;
1424 }
1425 }
1426
1427 /**
1428 * Start a snowstorm.
1429 */
1430
Snowstorm_effect(int duration,int delay,Game_object * egg)1431 Snowstorm_effect::Snowstorm_effect(
1432 int duration, // In game minutes.
1433 int delay, // In msecs.
1434 Game_object *egg // Egg that caused it, or null.
1435 ) : Weather_effect(duration, delay, 1, egg), start(true) {
1436 // Start snowing soon.
1437 eman->add_effect(std::make_unique<Clouds_effect>(duration + 1, delay));
1438 eman->add_effect(std::make_unique<Rain_effect<Snowflake>>(duration + 2, 20 + rand() % 1000, 0));
1439 }
1440
1441 /**
1442 * Start/end of snowstorm.
1443 */
handle_event(unsigned long curtime,uintptr udata)1444 void Snowstorm_effect::handle_event(
1445 unsigned long curtime, // Current time of day.
1446 uintptr udata
1447 ) {
1448 ignore_unused_variable_warning(curtime);
1449 if (start) {
1450 start = false;
1451 // Nothing more to do but end.
1452 gwin->get_tqueue()->add(stop_time, this, udata);
1453 } else { // Must be time to stop.
1454 auto ownHandle = eman->remove_effect(this);
1455 return;
1456 }
1457 }
1458
1459 /**
1460 * Start anti magic storm.
1461 */
1462
Sparkle_effect(int duration,int delay,Game_object * egg)1463 Sparkle_effect::Sparkle_effect(
1464 int duration, // In game minutes.
1465 int delay, // In msecs.
1466 Game_object *egg // Egg that caused it, or null.
1467 ) : Weather_effect(duration, delay, 3, egg), start(true) {
1468 // Start snowing soon.
1469 eman->add_effect(std::make_unique<Rain_effect<Sparkle>>(duration, delay, MAXDROPS / 10, 3));
1470 }
1471
1472 /**
1473 * Sparkles (in Ambrosia and generators).
1474 */
1475
handle_event(unsigned long curtime,uintptr udata)1476 void Sparkle_effect::handle_event(
1477 unsigned long curtime, // Current time of day.
1478 uintptr udata
1479 ) {
1480 ignore_unused_variable_warning(curtime);
1481 if (start) {
1482 start = false;
1483 // Nothing more to do but end.
1484 gwin->get_tqueue()->add(stop_time, this, udata);
1485 } else { // Must be time to stop.
1486 auto ownHandle = eman->remove_effect(this);
1487 return;
1488 }
1489 }
1490
1491 /**
1492 * Fog.
1493 */
1494
~Fog_effect()1495 Fog_effect::~Fog_effect() {
1496 gclock->set_fog(false);
1497 }
1498
Fog_effect(int duration,int delay,Game_object * egg)1499 Fog_effect::Fog_effect(
1500 int duration, // In game minutes.
1501 int delay, // In msecs.
1502 Game_object *egg // Egg that caused it, or null.
1503 ) : Weather_effect(duration, delay, 4, egg), start(true) {
1504 // SI adds sparkle/raindrops to the fog palaette shift
1505 // let's do that for all games
1506 int rain_delay = 250 + rand() % 1000;
1507 eman->add_effect(std::make_unique<Rain_effect<Sparkle>>(duration, rain_delay, MAXDROPS/2));
1508 }
1509
handle_event(unsigned long curtime,uintptr udata)1510 void Fog_effect::handle_event(unsigned long curtime, uintptr udata) {
1511 ignore_unused_variable_warning(curtime);
1512 if (start) {
1513 start = false;
1514 // Nothing more to do but end.
1515 gwin->get_tqueue()->add(stop_time, this, udata);
1516 gclock->set_fog(true);
1517 } else { // Must be time to stop.
1518 auto ownHandle = eman->remove_effect(this);
1519 return;
1520 }
1521 }
1522
1523 /**
1524 * Create a cloud.
1525 */
1526
1527 const int CLOUD = 2; // Shape #.
1528
Cloud(short dx,short dy)1529 Cloud::Cloud(
1530 short dx, short dy // Deltas for movement.
1531 ) : cloud(CLOUD, 0, SF_SPRITES_VGA), wx(0), wy(0), deltax(dx), deltay(dy), count(-1) {
1532 Game_window *gwin = Game_window::get_instance();
1533 // Get abs. values.
1534 int adx = deltax > 0 ? deltax : -deltax;
1535 int ady = deltay > 0 ? deltay : -deltay;
1536 if (adx < ady)
1537 max_count = 2 * gwin->get_height() / ady;
1538 else
1539 max_count = 2 * gwin->get_width() / adx;
1540 start_time = 0;
1541 }
1542
1543 /**
1544 * Set starting screen position according to direction.
1545 */
1546
set_start_pos(Shape_frame * shape,int w,int h,int & x,int & y)1547 void Cloud::set_start_pos(
1548 Shape_frame *shape,
1549 int w, int h, // Dims. of window.
1550 int &x, int &y // Screen pos. returned.
1551 ) {
1552 if (!deltax) { // Special cases first.
1553 x = rand() % w;
1554 y = deltay > 0 ? -shape->get_ybelow() :
1555 h + shape->get_yabove();
1556 return;
1557 }
1558 if (!deltay) {
1559 y = rand() % h;
1560 x = deltax > 0 ? -shape->get_xright() : w + shape->get_xleft();
1561 return;
1562 }
1563 int halfp = w + h; // 1/2 perimeter.
1564 int r = rand() % halfp; // Start on one of two sides.
1565 if (r > h) { // Start on top/bottom.
1566 x = r - h;
1567 y = deltay > 0 ? -shape->get_ybelow() :
1568 h + shape->get_yabove();
1569 return;
1570 }
1571 y = r; // On left or right side.
1572 if (deltax > 0) // Going right?
1573 x = -shape->get_xright();
1574 else // Going left?
1575 x = w + shape->get_xleft();
1576 }
1577
1578 /**
1579 * Move cloud
1580 */
1581
next(Game_window * gwin,unsigned long curtime,int w,int h)1582 inline void Cloud::next(
1583 Game_window *gwin, // Game window.
1584 unsigned long curtime, // Current time of day.
1585 int w, int h // Dims. of window.
1586 ) {
1587 if (curtime < start_time)
1588 return; // Not yet.
1589 // Get top-left world pos.
1590 long scrollx = gwin->get_scrolltx() * c_tilesize;
1591 long scrolly = gwin->get_scrollty() * c_tilesize;
1592 Shape_frame *shape = cloud.get_shape();
1593 gwin->add_dirty(gwin->clip_to_win(gwin->get_shape_rect(
1594 shape, wx - scrollx, wy - scrolly).enlarge(c_tilesize / 2)));
1595 if (count <= 0) { // Time to restart?
1596 // Set start time randomly.
1597 start_time = curtime + 2000 * randcnt + rand() % 2000;
1598 randcnt = (randcnt + 1) % 4;
1599 start_time = Game::get_ticks() + 2000 * randcnt + rand() % 500;
1600 count = max_count;
1601 cloud.set_frame(rand() % cloud.get_num_frames());
1602 int x;
1603 int y; // Get screen pos.
1604 set_start_pos(shape, w, h, x, y);
1605 wx = x + scrollx;
1606 wy = y + scrolly;
1607 } else {
1608 wx += deltax;
1609 wy += deltay;
1610 count--;
1611 }
1612 gwin->add_dirty(gwin->clip_to_win(gwin->get_shape_rect(
1613 shape, wx - scrollx, wy - scrolly).enlarge(c_tilesize / 2)));
1614 }
1615
1616 /**
1617 * Paint cloud.
1618 */
1619
paint()1620 void Cloud::paint(
1621 ) {
1622 Game_window *gwin = Game_window::get_instance();
1623 if (count > 0) // Might not have been started.
1624 cloud.paint_shape(
1625 wx - gwin->get_scrolltx()*c_tilesize - gwin->get_scrolltx_lo(),
1626 wy - gwin->get_scrollty()*c_tilesize - gwin->get_scrollty_lo());
1627 }
1628
1629 /**
1630 * Create a few clouds to float across the screen.
1631 */
1632
Clouds_effect(int duration,int delay,Game_object * egg,int n)1633 Clouds_effect::Clouds_effect(
1634 int duration, // In game minutes.
1635 int delay, // In msecs.
1636 Game_object *egg, // Egg that caused it, or null.
1637 int n
1638 ) : Weather_effect(duration, delay, n, egg), overcast(n != 6) {
1639 Game_clock *gclock = Game_window::get_instance()->get_clock();
1640 if (overcast)
1641 gclock->set_overcast(true);
1642 else
1643 gclock->set_overcast(false);
1644
1645 num_clouds = 2 + rand() % 5; // Pick #.
1646 if (overcast)
1647 num_clouds += rand() % 2;
1648 // Figure wind direction.
1649 int dx = rand() % 5 - 2;
1650 int dy = rand() % 5 - 2;
1651 if (!dx && !dy) {
1652 dx = 1 + rand() % 2;
1653 dy = 1 - rand() % 3;
1654 }
1655 for (int i = 0; i < num_clouds; i++) {
1656 // Modify speed of some.
1657 int deltax = dx;
1658 int deltay = dy;
1659 if (rand() % 2 == 0) {
1660 deltax += deltax / 2;
1661 deltay += deltay / 2;
1662 }
1663 clouds.emplace_back(std::make_unique<Cloud>(deltax, deltay));
1664 }
1665 }
1666
1667 /**
1668 * Cloud drift.
1669 */
1670
handle_event(unsigned long curtime,uintptr udata)1671 void Clouds_effect::handle_event(
1672 unsigned long curtime, // Current time of day.
1673 uintptr udata
1674 ) {
1675 const int delay = 100;
1676 if (curtime >= stop_time) {
1677 // Time to stop.
1678 auto ownHandle = eman->remove_effect(this);
1679 gwin->set_all_dirty();
1680 return;
1681 }
1682 int w = gwin->get_width();
1683 int h = gwin->get_height();
1684 for (auto& cloud : clouds)
1685 cloud->next(gwin, curtime, w, h);
1686 gwin->get_tqueue()->add(curtime + delay, this, udata);
1687 }
1688
1689 /**
1690 * Render.
1691 */
1692
paint()1693 void Clouds_effect::paint(
1694 ) {
1695 if (!gwin->is_main_actor_inside())
1696 for (auto& cloud : clouds)
1697 cloud->paint();
1698 }
1699
1700 /**
1701 * Destructor.
1702 */
1703
~Clouds_effect()1704 Clouds_effect::~Clouds_effect() {
1705 if (overcast)
1706 Game_window::get_instance()->get_clock()->set_overcast(false);
1707 }
1708
1709 /**
1710 * Shake the screen.
1711 */
1712
handle_event(unsigned long curtime,uintptr udata)1713 void Earthquake::handle_event(
1714 unsigned long curtime, // Current time of day.
1715 uintptr udata
1716 ) {
1717 static int eqsoundonce;
1718
1719 if (eqsoundonce != 1) {
1720 eqsoundonce = 1;
1721 // Play earthquake SFX once
1722 Audio::get_ptr()->play_sound_effect(Audio::game_sfx(60));
1723 }
1724
1725 Game_window *gwin = Game_window::get_instance();
1726 Image_window *win = gwin->get_win();
1727 int w = win->get_game_width();
1728 int h = win->get_game_height();
1729 int sx = 0;
1730 int sy = 0;
1731 int dx = rand() % 9 - 4;
1732 int dy = rand() % 9 - 4;
1733 if (dx > 0)
1734 w -= dx;
1735 else {
1736 w += dx;
1737 sx -= dx;
1738 dx = 0;
1739 }
1740 if (dy > 0)
1741 h -= dy;
1742 else {
1743 h += dy;
1744 sy -= dy;
1745 dy = 0;
1746 }
1747 win->copy(sx, sy, w, h, dx, dy);
1748 gwin->set_painted();
1749 gwin->show();
1750 // Shake back.
1751 win->copy(dx, dy, w, h, sx, sy);
1752 if (++i < len) // More to do? Put back in queue.
1753 gwin->get_tqueue()->add(curtime + 100, this, udata);
1754 else {
1755 eqsoundonce = 0;
1756 delete this;
1757 }
1758 }
1759
1760 /**
1761 * Create a fire field that will last for about 4 seconds.
1762 */
1763
Fire_field_effect(Tile_coord const & t)1764 Fire_field_effect::Fire_field_effect(
1765 Tile_coord const &t // Where to create it.
1766 ) {
1767 Game_object_shared field_shared = gmap->create_ireg_object(895, 0);
1768 Game_object *field_obj = field_shared.get();
1769 field = Game_object_weak(field_shared);
1770 field_obj->set_flag(Obj_flags::is_temporary);
1771 field_obj->move(t.tx, t.ty, t.tz);
1772 gwin->get_tqueue()->add(Game::get_ticks() + 3000 + rand() % 2000, this);
1773 }
1774
1775 /**
1776 * Remove the field.
1777 */
1778
handle_event(unsigned long curtime,uintptr udata)1779 void Fire_field_effect::handle_event(
1780 unsigned long curtime, // Current time of day.
1781 uintptr udata
1782 ) {
1783 Game_object_shared field_obj = field.lock();
1784 int frnum = field_obj ? field_obj->get_framenum() : 0;
1785 if (frnum == 0) { // All done?
1786 if (field_obj)
1787 field_obj->remove_this();
1788 auto ownHandle = eman->remove_effect(this);
1789 return;
1790 } else {
1791 if (frnum > 3) { // Starting to wind down?
1792 field_obj->as_egg()->stop_animation();
1793 frnum = 3;
1794 } else
1795 frnum--;
1796 gwin->add_dirty(field_obj.get());
1797 field_obj->change_frame(frnum);
1798 gwin->get_tqueue()->add(curtime + gwin->get_std_delay(), this, udata);
1799 }
1800 }
1801