1 #include "decor.h"
2 #include "sound.h"
3 #include "tank.h"
4 
5 #include <cassert>
6 
7 /// @brief Default constructor
DECOR(double x_,double y_,double xv_,double yv_,int32_t maxRadius,int32_t type_,int32_t delay_)8 DECOR::DECOR(double x_, double y_, double xv_, double yv_,
9 			int32_t maxRadius, int32_t type_, int32_t delay_) :
10 	PHYSICAL_OBJECT(false),
11 	curWind(global.wind),
12 	delay(delay_),
13 	maxGravAccel(-4. * env.gravity * env.FPS_mod),
14 	maxWind(env.windstrength),
15 	maxWindAccel(global.wind * env.FPS_mod),
16 	radius(maxRadius),
17 	type(type_)
18 {
19 	x = x_;
20 	y = y_;
21 	xv = xv_;
22 	yv = yv_;
23 
24 	if (DECOR_DIRT == type) {
25 		// The core data is taken from the meteors.
26 		weapType = SML_METEOR
27 		         + (maxRadius / 2); // results in (0, 1, 1, 2, 2) for radius [1;5]
28 		mass = naturals[weapType - WEAPONS].mass;
29 		drag = naturals[weapType - WEAPONS].drag / 5.;
30 
31 		// Special physics for dirt debris:
32         physType = PT_DIRTBOUNCE;
33 
34         // Only keep dirt alive while it is really moving,
35         // if it becomes too slow, only keep it for 2 seconds
36         maxAge = 2 * env.frames_per_second;
37 
38         // The diameter is just used so it does not have to
39         // be calculated each time updateDirt() is called.
40         diameter = radius * 2;
41 
42         // Calculate how many pixels are needed per call to updateDirt()
43         grabPerCall = ((diameter + 1) * (diameter + 1)) / (delay > 1 ? delay : 1);
44 	} else if (DECOR_SMOKE == type) {
45 		int32_t tempCol = 128 + (rand () % 64);
46 
47 		if (maxRadius <= 3)
48 			radius = 3;
49 		else
50 			radius = 3 + (rand () % (maxRadius - 2));
51 
52 		color  = makecol (tempCol, tempCol, tempCol);
53 		mass   = 1.0 + (static_cast<double>(rand () %  5) /  10.);
54 		drag   = 0.9 + (static_cast<double>(rand () % 90) / 100.);
55 
56 		// maximum age depends on the maximum radius and the real radius,
57 		// plus 0 to 2 extra seconds.
58 		maxAge  = ( (maxRadius - (maxRadius - radius)) / 3) + (rand() % 3);
59 		maxAge *= env.frames_per_second;
60 
61 		// Special physics for smoke, only for repulsion
62 		physType = PT_SMOKE;
63 
64 		// Smoke does not need the dirt grabber
65 		ready = true;
66 	} else
67 		destroy = true;
68 
69 	maxVel = env.maxVelocity * (1.20 + (mass / (.01 * MAX_POWER)));
70 
71 	// Add to the chain:
72 	global.addObject(this);
73 }
74 
75 
76 /// @brief Constructor with bitmap
DECOR(double x_,double y_,double xv_,double yv_,int32_t maxRadius,int32_t type_,int32_t delay_,sDebrisItem * deb_item,sDebrisItem * met_item)77 DECOR::DECOR(double x_, double y_, double xv_, double yv_,
78 			int32_t maxRadius, int32_t type_, int32_t delay_,
79 			sDebrisItem* deb_item, sDebrisItem* met_item) :
80 	DECOR(x_, y_, xv_, yv_, maxRadius, type_, delay_)
81 {
82 	// Everything done in delegated ctor, only img to set
83 	dirt   = deb_item;
84 	setBitmap(dirt ? dirt->bmp : nullptr);
85 	// Note: It is safe to distribute dirt->bmp, because bitmap normally holds
86 	// global graphics and must not be destroyed.
87 	meteor = met_item;
88 
89 	if ( (nullptr == dirt) || !hasBitmap() )
90 		// Can't work without...
91 		destroy = true;
92 }
93 
94 
95 /// @brief default destructor
~DECOR()96 DECOR::~DECOR()
97 {
98 	if (DECOR_DIRT == type) {
99 		// Draw dirt on terrain and add land slide
100 		rotate_sprite (global.terrain, dirt->bmp, x - radius, y - radius, itofix (angle));
101 		global.addLandSlide(x - radius - 1, x + radius + 1, false);
102 	}
103 
104 	if (dirt) {
105 		global.free_debris_item(dirt);
106 		dirt = nullptr;
107 	}
108 
109 	if (meteor) {
110 		global.free_debris_item(meteor);
111 		meteor = nullptr;
112 	}
113 
114 	// Update the last drawing area
115 	int32_t calcRadius = radius;
116 
117 	if (DECOR_DIRT == type)
118 			++calcRadius;
119 	else if (DECOR_SMOKE == type)
120 		// The older, the larger...
121 		calcRadius = static_cast<int32_t>(radius * (4.0 * age / maxAge));
122 
123 	setUpdateArea ( x - calcRadius - 1, y - calcRadius - 1,
124 					(calcRadius * 2) + 2, (calcRadius * 2) + 2);
125 	requireUpdate ();
126 	this->update();
127 
128 	// Take out of the chain:
129 	global.removeObject(this);
130 }
131 
132 
133 /// @brief let smoke drift and disperse with the wind
applyPhysics()134 void DECOR::applyPhysics ()
135 {
136 	if (destroy)
137 		return;
138 
139 	if (delay > 0) {
140 		--delay;
141 		return;
142 	}
143 
144 	// for detecting bounces
145 	double old_yv = yv;
146 
147 	if (DECOR_DIRT == type) {
148 
149 		// Check whether movement ended
150 		double movement = FABSDISTANCE2(xv, yv, 0, 0);
151 		bool   on_floor = isOnFloor(); // Needed again below.
152 
153 		if ( on_floor
154 		  && ( (hitSomething && (movement < 0.8))
155 		    || (movement < 0.2) ) )  {
156 
157 			// It ended!
158 
159 			// fix y:
160 			int32_t dirt_bottom = y + dirt->bmp->h;
161 			if ( ( (y - radius) > MENUHEIGHT)
162 			  && ( dirt_bottom < env.screenHeight)
163 			  && (PINK != getpixel(global.terrain, x, dirt_bottom)) )
164 				--y;
165 
166 			xv = 0.;
167 			yv = 0.;
168 			destroy = true;
169 			requireUpdate();
170 
171 		} else {
172 			hitSomething = false; // Enable checking.
173 
174 			// Now apply physics
175 			repulseDecor();
176 			PHYSICAL_OBJECT::applyPhysics();
177 
178 			// Be sure x/y values are sane (Can drift into walls
179 			// on rare wind conditions.)
180 			if (x < 2) x = 2;
181 			if (x > (env.screenWidth  - 2)) x = env.screenWidth  - 2;
182 			if (y > (env.screenHeight - 2)) y = env.screenHeight - 2;
183 
184 			// Maybe play a sound on bounce
185 			if ( !global.skippingComputerPlay
186 			  && (old_yv > .5)
187 			  && (yv < -0.1) )
188 				play_natural_sound(DIRT_FRAGMENT, x, radius * 32,
189 						1200 - (radius * 50));
190 		}
191 
192 		// raise age if movement is below 0.5
193 		if ( ( on_floor || (FABSDISTANCE2(xv, yv, 0, 0) < .5) )
194 		  && (++age > maxAge) )
195 			destroy = true;
196 
197 	} else if (DECOR_SMOKE == type) {
198 		// Apply wind first
199 		int32_t ageMod  = ROUND(std::abs(curWind / (maxWind / 2.0))) + 1;
200 
201 		/* This produces: (with max wind = 8)
202 		 * wind = 0 : round(0 / (8 / 2)) + 1 = round(0 / 4) + 1 = 0 + 1 = 1 <-- normal aging
203 		 * wind = 1 : round(1 / (8 / 2)) + 1 = round(1 / 4) + 1 = 0 + 1 = 1 <-- normal aging
204 		 * wind = 4 : round(4 / (8 / 2)) + 1 = round(4 / 4) + 1 = 1 + 1 = 2 <-- raised aging
205 		 * wind = 6 : round(6 / (8 / 2)) + 1 = round(6 / 4) + 1 = 2 + 1 = 3 <-- fast aging
206 		 * wind = 8 : round(8 / (8 / 2)) + 1 = round(8 / 4) + 1 = 2 + 1 = 3 <-- fast aging
207 		*/
208 		age += ageMod;
209 
210 		// Set further values
211 		// Try to reach half distance to the maximum values per second
212 		double xaccel = ((xv + maxWindAccel) / 2)
213 		              / static_cast<double>(env.frames_per_second);
214 		double yaccel = ((yv + maxGravAccel) / 2)
215 		              / static_cast<double>(env.frames_per_second / 10.);
216 
217 		// Apply current acceleration
218 		xv += xaccel;
219 		yv += yaccel;
220 
221 		// Add repulsion:
222 		repulseDecor();
223 
224 		// Be sure that neither xv outruns wind nor yv is
225 		// higher than reverse gravity
226 		if (std::abs(xv) > std::abs(curWind))
227 			xv = curWind;
228 		if (yv < maxGravAccel)
229 			yv = maxGravAccel;
230 
231 		// Don't push through the floor
232 		if ( (y + yv) >= env.screenHeight) {
233 			yv *= -0.5;
234 			xv *=  0.95;
235 		}
236 
237 		// The faster the smoke is blown by the wind, the less it rises:
238 		if ( (yv < -1.) && (std::abs(xv) > 1.) )
239 			yv = (yv + (yv / std::abs(xv))) / 2.;
240 		// and if the smoke is going down, halve yv
241 		else if (yv > 0.)
242 			yv /= 2.;
243 
244 		// Now the velocity can be applied.
245 		x += xv;
246 		y += yv;
247 
248 		// Destroy the smoke if it goes off-screen or is diffused
249 		int32_t calcRadius = static_cast<int32_t>(radius * (4.0 * age / maxAge));
250 
251 		if ( (x <  (1 - calcRadius))
252 		  || (x >= (env.screenWidth + calcRadius))
253 		  || (y <  (MENUHEIGHT - calcRadius))
254 		  || (age > maxAge) )
255 			destroy = true;
256 	}
257 }
258 
259 
260 /// @brief draw decor according to current settings and type.
draw()261 void DECOR::draw()
262 {
263 	if (!ready && !destroy) {
264 		updateDirt();
265 		if (ready) {
266 			// finished! See whether there are enough pixels
267 			if (gotPixels <= radius)
268 				// nope.
269 				destroy = true;
270 		}
271 	}
272 
273 	if (destroy || (delay > 0))
274 		return;
275 
276 	int32_t calcRadius = radius;
277 
278 	if (DECOR_DIRT == type) {
279 		// Rotate according to xv and yv
280 		angle += yv + ((SIGNd(xv) * 5.) - xv);
281 
282 		// Be sure the angle is in order:
283 		if (angle <   0) angle += 360;
284 		if (angle > 360) angle -= 360;
285 
286 		// And draw it:
287 		if (y > MENUHEIGHT) {
288 			VIRTUAL_OBJECT::draw();
289 			++calcRadius;
290 		}
291 	} else if (DECOR_SMOKE == type) {
292 		// The older, the larger...
293 		calcRadius = static_cast<int32_t>(radius * (4.0 * age / maxAge));
294 
295 		drawing_mode (DRAW_MODE_TRANS, NULL, 0, 0);
296 		set_trans_blender (0, 0, 0, 255 - (255 * age / maxAge));
297 		circlefill (global.canvas, x, y, calcRadius, color);
298 	}
299 
300 	drawing_mode (global.current_drawing_mode, NULL, 0, 0);
301 
302 	setUpdateArea ( x - calcRadius - 1, y - calcRadius - 1,
303 					(calcRadius * 2) + 2, (calcRadius * 2) + 2);
304 	requireUpdate ();
305 }
306 
307 
308 /// In case of too much decor for the machine, allow forced ageing
force_aging(int32_t frames)309 void DECOR::force_aging(int32_t frames)
310 {
311 	age += frames;
312 	if (age > maxAge)
313 		destroy = true;
314 }
315 
316 
317 /// return true if a dirt debris item "lies" on the floor, or is squeezed in a
318 /// dirt slide.
isOnFloor()319 bool DECOR::isOnFloor()
320 {
321 	int32_t scr_r  = env.screenWidth - 2;  // shortcut;
322 	int32_t scr_b  = env.screenHeight - 2; // ditto;
323 
324 	// If the debris is above the screen or directly on the floor,
325 	// return at once:
326 	if (y <= MENUHEIGHT)
327 		return false;
328 	if (y >= (scr_b - radius))
329 		return true;
330 
331 	// Use safe values:
332 	int32_t round_x = ROUND(x);
333 	int32_t round_y = ROUND(y);
334 
335 	// sanitize x value:
336 	if      (round_x < 1)     round_x = 1;
337 	else if (round_x > scr_r) round_x = scr_r;
338 
339 	// rounded boundaries, clipped to the screen:
340 	int32_t left    = std::max(1,          round_x - radius);
341 	int32_t top     = std::max(MENUHEIGHT, round_y - radius);
342 	int32_t right   = std::min(scr_r,      round_x + radius);
343 	int32_t bottom  = std::min(scr_b,      round_y + radius);
344 
345 	// Go from left to right and check whether the surface is above the bottom.
346 	int32_t surf_hits = 0;
347 	bool    on_floor  = false;
348 	for (int32_t i = left; !on_floor && (i <= right); ++i) {
349 		if (global.surface[i].load(ATOMIC_READ) <= bottom) {
350 			// Actually this could mean that the debris is within
351 			// a hole in a mountain that hasn't been slid down, yet.
352 			bool in_dirt = false;
353 
354 			for (int32_t j = bottom; !in_dirt && (j >= top) ; --j) {
355 				if (PINK != getpixel(global.terrain, i, j))
356 					in_dirt = true;
357 			}
358 
359 			if (in_dirt && (++surf_hits >= radius) )
360 				on_floor = true;
361 		}
362 	}
363 
364 	return on_floor;
365 }
366 
367 
368 /// DIRT and Smoke (somewhat) can be repulsed, too
repulseDecor()369 void DECOR::repulseDecor()
370 {
371 	TANK*  lt     = nullptr;
372 	double xaccel = 0;
373 	double yaccel = 0;
374 
375 	global.getHeadOfClass(CLASS_TANK, &lt);
376 
377 	while (lt) {
378 		if (!lt->destroy) {
379 
380 			if (lt->repulse (x + xv, y + yv, &xaccel, &yaccel, physType)) {
381 				xv += xaccel;
382 				yv += yaccel;
383 			}
384 		}
385 		lt->getNext(&lt);
386 	}
387 }
388 
389 
390 /// Small scale dirt grabber
updateDirt()391 void DECOR::updateDirt()
392 {
393 	int32_t togo    = grabPerCall + 1;
394 	double  deb_rad = static_cast<double>(radius);
395 
396 	while (togo) {
397 		double deb_dist = FABSDISTANCE2(static_cast<double>(grab_x),
398 										static_cast<double>(grab_y),
399 										deb_rad,
400 										deb_rad);
401 		if (deb_dist <= deb_rad) {
402 			int32_t tcol = getpixel(dirt->bmp, grab_x, grab_y);
403 
404 			// If this is a meteor and the terrain had no pixel
405 			// there, take one out of the rock instead.
406 			if ( (PINK == tcol) && meteor)
407 				tcol = getpixel(meteor->bmp, grab_x, grab_y);
408 
409 			// If this is valid, scorch the colour and put it back.
410 			if (PINK != tcol) {
411 				double deb_mod = deb_dist / deb_rad;
412 				int32_t new_r = getr(tcol) / (1.25 + deb_mod);
413 				int32_t new_g = getg(tcol) / (1.66 + deb_mod);
414 				int32_t new_b = getb(tcol) / (1.33 + deb_mod);
415 				putpixel(dirt->bmp, grab_x, grab_y, makecol(new_r, new_g, new_b));
416 				++gotPixels;
417 			}
418 		} // End of position in range
419 
420 		else
421 			// If the position is not in range, erase the surplus pixel
422 			putpixel(dirt->bmp, grab_x, grab_y, PINK);
423 
424 		// This point is done
425 		--togo;
426 
427 		// Advance coordinates
428 		if (++grab_x > diameter) {
429 			grab_x = 0;
430 			if (++grab_y > diameter) {
431 				// end of work
432 				togo  = 0;
433 				ready = true;
434 				if (meteor) {
435 					global.free_debris_item(meteor);
436 					meteor = nullptr;
437 				}
438 			}
439 		}
440 	} // End of having pixels to grab
441 }
442