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