1 /* $Id: CosmoSmashEngine.cpp,v 1.70 2017/12/09 18:20:43 sarrazip Exp $
2
3 cosmosmash - A space rock shooting video game.
4 Copyright (C) 2000-2011 Pierre Sarrazin <http://sarrazip.com/>
5
6 This program is free software; you can redistribute it and/or
7 modify it under the terms of the GNU General Public License
8 as published by the Free Software Foundation; either version 2
9 of the License, or (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
17 License along with this program; if not, write to the Free
18 Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
19 Boston, MA 02110-1301, USA.
20 */
21
22 #include "CosmoSmashEngine.h"
23
24 #include "Controller.h"
25
26 #include <flatzebra/PixmapLoadError.h>
27
28 #include <assert.h>
29 #include <stdlib.h>
30 #include <stdio.h>
31 #include <string.h>
32 #include <ctype.h>
33 #include <math.h>
34 #include <stdint.h>
35
36 #include <map>
37 #include <vector>
38 #include <string>
39 #include <algorithm>
40 #include <iomanip>
41 #include <sstream>
42
43 #include "base.xpm"
44 #include "boosted-base.xpm"
45 #include "basebullet.xpm"
46 #include "bigrock0.xpm"
47 #include "bigrock1.xpm"
48 #include "bigrock2.xpm"
49 #include "bigrock3.xpm"
50 #include "bigrock4.xpm"
51 #include "bigrock5.xpm"
52 #include "smallrock0.xpm"
53 #include "smallrock1.xpm"
54 #include "smallrock2.xpm"
55 #include "smallrock3.xpm"
56 #include "smallrock4.xpm"
57 #include "smallrock5.xpm"
58 #include "explosion0.xpm"
59 #include "explosion1.xpm"
60 #include "bigspinner_backslash.xpm"
61 #include "bigspinner_horizontal.xpm"
62 #include "bigspinner_slash.xpm"
63 #include "bigspinner_vertical.xpm"
64 #include "smallspinner_backslash.xpm"
65 #include "smallspinner_horizontal.xpm"
66 #include "smallspinner_slash.xpm"
67 #include "smallspinner_vertical.xpm"
68 #include "pulsar_empty.xpm"
69 #include "pulsar_mid.xpm"
70 #include "pulsar_full.xpm"
71 #include "saucer0.xpm"
72 #include "saucer1.xpm"
73 #include "saucerbullet_empty.xpm"
74 #include "saucerbullet_mid.xpm"
75 #include "saucerbullet_full.xpm"
76 #include "question0.xpm"
77 #include "question1.xpm"
78
79 using namespace std;
80 using namespace flatzebra;
81
82 #ifdef _MSC_VER
83 #define snprintf _snprintf
84 #endif
85
86
87 /* Various constants:
88 */
89
90 static const size_t
91 FRAMES_PER_SECOND = 20;
92
93 static const Couple theDrawingPixmapSize(WINDOW_WIDTH, WINDOW_HEIGHT);
94
95
96 static const int
97 PLAYER_SPEED = 15,
98 PLAYER_BULLET_SPEED = 36,
99 PULSAR_SPEED = 20,
100 SAUCER_SPEED = 8,
101 SAUCER_BULLET_SPEED = 20,
102 QUESTION_SPEED = 16;
103
104 static const int
105 SAUCER_Y_POS = 75,
106 NUM_INIT_LIVES = 5;
107
108 static const double TWO_PI = 8.0 * atan(1.0);
109
110 static const long
111 SCORE_LOST_BASE = -100,
112 SCORE_BIG_ROCK_LANDING = -5,
113 SCORE_SMALL_ROCK_LANDING = -10,
114 SCORE_BIG_ROCK = 10,
115 SCORE_SMALL_ROCK = 20,
116 SCORE_BIG_SPINNER = 40,
117 SCORE_SMALL_SPINNER = 80,
118 SCORE_PULSAR = 50,
119 SCORE_SAUCER_BULLET = 50,
120 SCORE_SAUCER = 100,
121 SCORE_QUESTION = 50;
122
123
124 // Defined as constants for efficiency:
125 static const size_t
126 NUM_SPINNER_IMAGES = 4,
127 NUM_PULSAR_IMAGES = 3,
128 NUM_ROCK_IMAGES = 6,
129 NUM_SAUCER_IMAGES = 2,
130 NUM_SAUCER_BULLET_IMAGES = 3,
131 NUM_STARS = 40,
132 NUM_MOUNTAIN_RANGE_POINTS = 10;
133
134
135 inline int
getLevelNum(long score)136 getLevelNum(long score)
137 {
138 if (score >= 100000)
139 return 6;
140 if (score >= 50000)
141 return 5;
142 if (score >= 20000)
143 return 4;
144 if (score >= 5000)
145 return 3;
146 if (score >= 1000)
147 return 2;
148 return 1;
149 }
150
151
152 inline int
discreteUniformRand(const Couple & limits)153 discreteUniformRand(const Couple &limits)
154 /*
155 Returns a random integer between limits.x and limits.y inclusively.
156 limits.x must be <= limits.y.
157 */
158 {
159 return rand() % (limits.y - limits.x + 1) + limits.x;
160 }
161
162
163 inline int
rnd(int lowerLimit,int upperLimit)164 rnd(int lowerLimit, int upperLimit)
165 {
166 return rand() % (upperLimit - lowerLimit + 1) + lowerLimit;
167 }
168
169
170 inline int
mulRound(int n,double x)171 mulRound(int n, double x)
172 /* Multiply the arguments, and round the result to the nearest integer
173 according to its sign.
174 */
175 {
176 double product = n * x;
177 if (product >= 0.0)
178 return (int) (product + 0.5);
179 return (int) (product - 0.5);
180 }
181
182 inline int
183 (min)(int a, int b)
184 {
185 return (a < b ? a : b);
186 }
187
188
189 inline int
190 (max)(int a, int b)
191 {
192 return (a > b ? a : b);
193 }
194
195
196 inline int
divideAmplitude(int n,int divisor)197 divideAmplitude(int n, int divisor)
198 {
199 if (n == 0)
200 return 0;
201 if (n < 0)
202 return min(n / divisor, -1);
203 return max(n / divisor, +1);
204 }
205
206
207 inline Couple
divideAmplitude(const Couple & c,int divisor)208 divideAmplitude(const Couple &c, int divisor)
209 {
210 return Couple(divideAmplitude(c.x, divisor), divideAmplitude(c.y, divisor));
211 }
212
213
214 inline int
decrementAmplitude(int n)215 decrementAmplitude(int n)
216 {
217 if (n > 0)
218 return n - 1;
219 if (n < 0)
220 return n + 1;
221 return 0;
222 }
223
224
225 inline Couple
decrementAmplitude(const Couple & c)226 decrementAmplitude(const Couple &c)
227 {
228 return Couple(decrementAmplitude(c.x), decrementAmplitude(c.y));
229 }
230
231
232 inline void
removeNullElementsFromSpriteList(SpriteList & slist)233 removeNullElementsFromSpriteList(SpriteList &slist)
234 {
235 SpriteList::iterator it =
236 remove(slist.begin(), slist.end(), (Sprite *) NULL);
237 /* remove() has "packed" the remaining elements at the beginning
238 of the sequence, but has not shortened the list. This must
239 be done by a call to the erase() method. Doesn't this seem
240 unintuitive? I thought remove() removed stuff.
241 @sarrazip 20010501
242 */
243 slist.erase(it, slist.end());
244 }
245
246
247 ///////////////////////////////////////////////////////////////////////////////
248
249
CosmoSmashEngine(long initialScore,bool _mirrorHyperspace,bool _useSound,bool fullScreen,Controller * _controller,ostream * _gameScriptStream,bool processActiveEvent)250 CosmoSmashEngine::CosmoSmashEngine(
251 long initialScore,
252 bool _mirrorHyperspace, bool _useSound,
253 bool fullScreen, Controller *_controller,
254 ostream *_gameScriptStream,
255 bool processActiveEvent)
256 : GameEngine(theDrawingPixmapSize, "Cosmosmash", fullScreen, processActiveEvent),
257 // may throw string exception
258
259 pauseState(PLAYING),
260 tickCount(0),
261 useSound(_useSound),
262
263 levelColors(),
264 whiteColor(SDL_MapRGB(theSDLScreen->format, 0xff, 0xff, 0xff)),
265 greenColor(SDL_MapRGB(theSDLScreen->format, 0x00, 0xff, 0x00)),
266 explosionBGColor(SDL_MapRGB(theSDLScreen->format, 0xff, 0x80, 0x00)),
267
268 mirrorHyperspace(_mirrorHyperspace),
269 useGameExtensions(false),
270 playerBoostTicks(0),
271 playerBoostType(NO_BOOST),
272
273 playerPA(2),
274 playerSprite(NULL),
275
276 baseBulletPA(1),
277 baseBulletSprites(),
278
279 bigRockPA(NUM_ROCK_IMAGES),
280 smallRockPA(NUM_ROCK_IMAGES),
281 rockSprites(),
282
283 bigSpinnerPA(NUM_SPINNER_IMAGES),
284 smallSpinnerPA(NUM_SPINNER_IMAGES),
285 spinnerSprites(),
286
287 pulsarPA(NUM_PULSAR_IMAGES),
288 pulsarSprites(),
289
290 saucerPA(NUM_SAUCER_IMAGES),
291 saucerSprites(),
292
293 saucerBulletPA(NUM_SAUCER_BULLET_IMAGES),
294 saucerBulletSprites(),
295
296 explosionPA(2),
297 explosionSprites(),
298
299 questionPA(2),
300 questionSprites(),
301
302 groundPos(0),
303 groundHeight(0),
304 mountainTopPos(0),
305
306 controller(_controller),
307 quitKS(SDLK_ESCAPE),
308
309 initScore(initialScore),
310 theScore(0),
311 thePeakScore(0),
312 theLevel(0),
313 scoreAreaPos(),
314 peakScoreAreaPos(),
315 levelNumAreaPos(),
316
317 numLives(0),
318 updateNumLives(true),
319 numLivesAreaPos(),
320
321 timeBeforeNextPulsar(0),
322 timeBeforeNextSaucer(0),
323 timeBeforeNextQuestion(0),
324
325 starPositions(),
326 mountainRangePositions(),
327
328 theSoundMixer(NULL),
329 playerHitSound(),
330 pulsarBeepSound(),
331 saucerShootingSound(),
332 hyperspaceSound(),
333 timeBeforeNextCadenceSound(0),
334
335 gameScriptStream(_gameScriptStream)
336 {
337 assert(controller != NULL);
338
339 levelColors[0] = 0; // unused
340 levelColors[1] = SDL_MapRGB(theSDLScreen->format, 0x00, 0x00, 0x00);
341 levelColors[2] = SDL_MapRGB(theSDLScreen->format, 0x00, 0x00, 0xff);
342 levelColors[3] = SDL_MapRGB(theSDLScreen->format, 0x80, 0x00, 0x80);
343 levelColors[4] = SDL_MapRGB(theSDLScreen->format, 0x00, 0x7f, 0x7f);
344 levelColors[5] = SDL_MapRGB(theSDLScreen->format, 0x7f, 0x7f, 0x7f);
345 levelColors[6] = SDL_MapRGB(theSDLScreen->format, 0x00, 0x00, 0x00);
346
347 assert(initScore >= 0);
348 assert(theSDLScreen != NULL);
349 assert(theSDLScreen->format != NULL);
350
351 try
352 {
353 initializeSprites();
354 }
355 catch (PixmapLoadError &e)
356 {
357 string msg = "Could not load pixmap " + e.getFilename();
358 throw msg;
359 }
360
361 initializeMisc(useSound); // may throw a 'string'
362 }
363
364
~CosmoSmashEngine()365 CosmoSmashEngine::~CosmoSmashEngine()
366 {
367 // Sprites must be killed to avoid memory leaks.
368 killAllAutomaticCharacters();
369 killSpritesInList(baseBulletSprites);
370
371 roundbeetle::Engine::destroy();
372 theSoundMixer = NULL;
373
374 delete controller;
375 delete playerSprite;
376 }
377
378
379 void
initializeSprites()380 CosmoSmashEngine::initializeSprites()
381 {
382 groundPos = theDrawingPixmapSize.y - 50;
383 groundHeight = 10;
384
385 // Load some pixmaps:
386 loadPixmap(base_xpm, playerPA, 0);
387 loadPixmap(boosted_base_xpm, playerPA, 1);
388
389 // Create and position the player sprite:
390 playerSprite = new Sprite(playerPA,
391 Couple((theDrawingPixmapSize.x - playerPA.getImageSize().x) / 2,
392 groundPos - playerPA.getImageSize().y),
393 Couple(0, 0), Couple(0, 0),
394 Couple(6, 6), playerPA.getImageSize() - Couple(12, 12));
395 // this Sprite object does not own playerPA
396
397
398 // Base bullets:
399 loadPixmap(basebullet_xpm, baseBulletPA, 0);
400
401 // Rocks:
402 loadPixmap(bigrock0_xpm, bigRockPA, 0);
403 loadPixmap(bigrock1_xpm, bigRockPA, 1);
404 loadPixmap(bigrock2_xpm, bigRockPA, 2);
405 loadPixmap(bigrock3_xpm, bigRockPA, 3);
406 loadPixmap(bigrock4_xpm, bigRockPA, 4);
407 loadPixmap(bigrock5_xpm, bigRockPA, 5);
408 loadPixmap(smallrock0_xpm, smallRockPA, 0);
409 loadPixmap(smallrock1_xpm, smallRockPA, 1);
410 loadPixmap(smallrock2_xpm, smallRockPA, 2);
411 loadPixmap(smallrock3_xpm, smallRockPA, 3);
412 loadPixmap(smallrock4_xpm, smallRockPA, 4);
413 loadPixmap(smallrock5_xpm, smallRockPA, 5);
414
415 // Big spinner:
416 loadPixmap(bigspinner_horizontal_xpm, bigSpinnerPA, 0);
417 loadPixmap(bigspinner_slash_xpm, bigSpinnerPA, 1);
418 loadPixmap(bigspinner_vertical_xpm, bigSpinnerPA, 2);
419 loadPixmap(bigspinner_backslash_xpm, bigSpinnerPA, 3);
420
421 // Small spinner:
422 loadPixmap(smallspinner_horizontal_xpm, smallSpinnerPA, 0);
423 loadPixmap(smallspinner_slash_xpm, smallSpinnerPA, 1);
424 loadPixmap(smallspinner_vertical_xpm, smallSpinnerPA, 2);
425 loadPixmap(smallspinner_backslash_xpm, smallSpinnerPA, 3);
426
427 // Pulsar:
428 loadPixmap(pulsar_empty_xpm, pulsarPA, 0);
429 loadPixmap(pulsar_mid_xpm, pulsarPA, 1);
430 loadPixmap(pulsar_full_xpm, pulsarPA, 2);
431
432 // Saucer:
433 loadPixmap(saucer0_xpm, saucerPA, 0);
434 loadPixmap(saucer1_xpm, saucerPA, 1);
435
436 // Saucer bullets:
437 loadPixmap(saucerbullet_empty_xpm, saucerBulletPA, 0);
438 loadPixmap(saucerbullet_mid_xpm, saucerBulletPA, 1);
439 loadPixmap(saucerbullet_full_xpm, saucerBulletPA, 2);
440
441 // Explosion:
442 loadPixmap(explosion0_xpm, explosionPA, 0);
443 loadPixmap(explosion1_xpm, explosionPA, 1);
444
445 // Question (surprise) sprites:
446 loadPixmap(question0_xpm, questionPA, 0);
447 loadPixmap(question1_xpm, questionPA, 1);
448 }
449
450
451 static
452 string
getDir(const char * defaultValue,const char * envVarName)453 getDir(const char *defaultValue, const char *envVarName)
454 {
455 string dir;
456 const char *s = getenv(envVarName);
457 if (s != NULL)
458 dir = s;
459 else
460 dir = defaultValue;
461
462 if (!dir.empty() && dir[dir.length() - 1] != '/')
463 dir += '/';
464
465 return dir;
466 }
467
468
469 void
initializeMisc(bool useSound)470 CosmoSmashEngine::initializeMisc(bool useSound)
471 /*
472 Throws an error message in a 'string' if an error occurs.
473 */
474 {
475 theScore = initScore;
476 thePeakScore = initScore;
477 theLevel = getLevelNum(theScore);
478 scoreAreaPos = Couple(4, theDrawingPixmapSize.y - 31);
479 peakScoreAreaPos = scoreAreaPos + Couple(0, 15);
480 numLives = 0;
481 updateNumLives = true;
482 numLivesAreaPos = Couple(theDrawingPixmapSize.x - 79, peakScoreAreaPos.y);
483 levelNumAreaPos = Couple(numLivesAreaPos.x, scoreAreaPos.y);
484 playerBoostTicks = 0;
485 playerBoostType = NO_BOOST;
486
487
488 setTimeBeforeNextPulsar();
489 setTimeBeforeNextSaucer();
490 setTimeBeforeNextQuestion();
491
492
493 /* Initialize mountain range positions:
494 */
495 int y0 = groundPos - 100;
496 int y1 = groundPos - 70;
497 int y2 = groundPos - 50;
498
499 mountainTopPos = y0;
500
501 size_t i;
502 for (i = 0; i < NUM_MOUNTAIN_RANGE_POINTS; i++)
503 {
504 int x = i * theDrawingPixmapSize.x / (NUM_MOUNTAIN_RANGE_POINTS - 1);
505 int y;
506 if (i & 1)
507 y = rnd(y0, y1);
508 else
509 y = rnd(y1, y2);
510 mountainRangePositions.push_back(Couple(x, y));
511 }
512
513
514 /* Initialize star positions:
515 */
516 const int padding = 8;
517 for (i = 0; i < NUM_STARS; i++)
518 {
519 int x = rnd(padding, theDrawingPixmapSize.x - padding);
520 int y = rnd(padding, mountainTopPos - 10);
521 starPositions.push_back(Couple(x, y));
522 }
523
524
525 /* Sound effects:
526 */
527 if (useSound)
528 {
529 string pkgsounddir = getDir(PKGSOUNDDIR, "PKGSOUNDDIR");
530
531 // A high sample rate is necessary for the sound renderer,
532 // to make sure the spinner square wave's frequency
533 // decreases smoothly to the human ear.
534 //
535 int status = roundbeetle::Engine::create(44100, 0, NULL);
536 if (status != 0)
537 {
538 stringstream ss;
539 ss << "RoundBeetle sound engine initialization error #" << status;
540 throw ss.str();
541 }
542
543 theSoundMixer = &roundbeetle::Engine::instance();
544 assert(theSoundMixer != NULL);
545 theSoundMixer->postBusInit();
546
547 try
548 {
549 for (size_t i = 0; i < NUM_ROCK_HIT_SOUNDS; i++)
550 {
551 string filename = pkgsounddir + "rock-hit-" + char('0' + i) + ".wav";
552 rockHitSounds[i].init(filename);
553 }
554
555 playerHitSound.init(pkgsounddir + "player-hit.wav");
556 pulsarBeepSound.init(pkgsounddir + "pulsar-beep.wav");
557 saucerShootingSound.init(pkgsounddir + "saucer-shooting.wav");
558 hyperspaceSound.init(pkgsounddir + "hyperspace.wav");
559
560 timeBeforeNextCadenceSound = 0;
561 }
562 catch (const string &e)
563 {
564 throw e;
565 }
566 }
567 }
568
569
570 void
setTimeBeforeNextPulsar()571 CosmoSmashEngine::setTimeBeforeNextPulsar()
572 {
573 int min = 13 - 2 * theLevel;
574 int max = 53 - 8 * theLevel;
575 timeBeforeNextPulsar = rnd(20 * min, 20 * max);
576 }
577
578
579 void
setTimeBeforeNextSaucer()580 CosmoSmashEngine::setTimeBeforeNextSaucer()
581 {
582 int lev = theLevel - 4;
583 int min = 5 - lev * 2;
584 int max = 30 - lev * 10;
585 timeBeforeNextSaucer = rnd(20 * min, 20 * max);
586 }
587
588
589 void
setTimeBeforeNextQuestion()590 CosmoSmashEngine::setTimeBeforeNextQuestion()
591 {
592 static const char *q = getenv("Q");
593 timeBeforeNextQuestion = (q != NULL ? 100 : rnd(20 * 10, 20 * 30));
594 }
595
596
597 ///////////////////////////////////////////////////////////////////////////////
598
599
600 /*virtual*/
601 void
processKey(SDLKey keysym,bool pressed)602 CosmoSmashEngine::processKey(SDLKey keysym, bool pressed)
603 {
604 controller->processKey(keysym, pressed);
605
606 quitKS.check(keysym, pressed);
607 }
608
609
610 /*virtual*/
611 void
processActivation(bool appActive)612 CosmoSmashEngine::processActivation(bool appActive)
613 {
614 if (!appActive) // if loss of focus:
615 {
616 assert(pauseState == PLAYING || pauseState == AWAITING_RESUME);
617
618 if (pauseState == PLAYING)
619 {
620 pause();
621
622 if (numLives > 0)
623 {
624 // Display the pause message here because tick() will not be called
625 // until this method is called again to reactivate the application.
626 //
627 displayPauseMessage();
628 }
629 }
630
631 pauseState = LOST_FOCUS;
632 }
633 else // gaining focus:
634 {
635 assert(pauseState == LOST_FOCUS);
636
637 // No resume here. Game waits for user to press resume key.
638 //
639 pauseState = AWAITING_RESUME;
640 }
641
642 registerTickEnd();
643 }
644
645
646 // CAUTION: does not change 'pauseState', which must be managed
647 // by the caller.
648 //
649 void
pause()650 CosmoSmashEngine::pause()
651 {
652 registerCommand(COMMAND_PAUSE);
653 if (theSoundMixer != NULL)
654 (void) theSoundMixer->pauseEngine();
655 }
656
657
658 void
resume()659 CosmoSmashEngine::resume()
660 {
661 registerCommand(COMMAND_RESUME);
662 if (theSoundMixer != NULL)
663 (void) theSoundMixer->resumeEngine();
664 }
665
666
667 /*virtual*/
668 bool
tick()669 CosmoSmashEngine::tick()
670 {
671 if (quitKS.isPressed())
672 return false;
673
674 controller->startOfTick();
675
676 bool ret = true;
677
678 if (controller->isFullScreenToggleRequested())
679 setVideoMode(theScreenSizeInPixels, !inFullScreenMode()); // ignore failure
680
681 if (pauseState == AWAITING_RESUME)
682 {
683 if (controller->isResumeRequested())
684 {
685 resume();
686 pauseState = PLAYING;
687 }
688 else
689 displayPauseMessage();
690 }
691 else
692 {
693 tickCount++;
694
695 detectCollisions();
696
697 if (!animatePlayer())
698 ret = false;
699 else
700 {
701 animateAutomaticCharacters();
702 restoreBackground();
703 drawSprites();
704
705
706 /* If the game is in demo mode, the Space bar starts a new game.
707 */
708 if (numLives == 0)
709 {
710 if (controller->isStartRequested(useGameExtensions))
711 {
712 registerCommand(COMMAND_START);
713
714 displayStartMessage(false);
715
716 killAllAutomaticCharacters();
717 centerPlayerSprite();
718 theScore = initScore;
719 thePeakScore = initScore;
720 numLives = NUM_INIT_LIVES;
721
722 addToScore(0);
723 addToNumLives(0);
724 }
725 else
726 {
727 /* At the end of a game, wait for the last base to have
728 finished exploding before displaying the start
729 message again.
730 */
731 bool baseExploding = (playerSprite->getTimeToLive() != 0);
732 if (! baseExploding)
733 displayStartMessage(true);
734 }
735 }
736 else
737 playCadenceSound();
738 }
739 }
740
741 controller->endOfTick();
742
743 registerTickEnd();
744
745 return ret;
746 }
747
748
749 void
fireBullet(Couple pos)750 CosmoSmashEngine::fireBullet(Couple pos)
751 {
752 Couple bbSize = baseBulletPA.getImageSize();
753 Sprite *s = new Sprite(baseBulletPA,
754 pos, Couple(0, -PLAYER_BULLET_SPEED), Couple(0, 0),
755 Couple(-4, 0), bbSize + Couple(4, 0));
756 baseBulletSprites.push_back(s);
757
758 if (theLevel >= 5) // double bullet in higher levels
759 {
760 int n = (theLevel == 5 ? bbSize.y / 2 : bbSize.y);
761 Couple pos2 = pos;
762 pos2.y -= n;
763 Sprite *s = new Sprite(baseBulletPA,
764 pos2, Couple(0, -PLAYER_BULLET_SPEED), Couple(0, 0),
765 Couple(0, 0), bbSize);
766 baseBulletSprites.push_back(s);
767 }
768
769 if (playerBoostTicks > 0 && playerBoostType == TRIPLE_BULLETS)
770 {
771 int vx = 3;
772 Couple pos2 = pos;
773 pos2.x -= bbSize.x + 1;
774 Sprite *s = new Sprite(baseBulletPA,
775 pos2, Couple(-vx, -PLAYER_BULLET_SPEED), Couple(0, 0),
776 Couple(0, 0), bbSize);
777 baseBulletSprites.push_back(s);
778
779 pos2 = pos;
780 pos2.x += bbSize.x + 1;
781 s = new Sprite(baseBulletPA,
782 pos2, Couple(+vx, -PLAYER_BULLET_SPEED), Couple(0, 0),
783 Couple(0, 0), bbSize);
784 baseBulletSprites.push_back(s);
785 }
786 }
787
788
789 bool
animatePlayer()790 CosmoSmashEngine::animatePlayer()
791 /*
792 Returns true if the game must continue, or false to have it stop.
793 */
794 {
795 // Inform the controller of the current state of the game:
796 {
797 controller->beginGameState();
798
799 controller->setPlayerPos(playerSprite->getCenterPos());
800
801 SpriteList::iterator its;
802 for (its = rockSprites.begin(); its != rockSprites.end(); its++)
803 controller->addRockPos((*its)->getCenterPos());
804 for (its = spinnerSprites.begin(); its != spinnerSprites.end(); its++)
805 controller->addSpinnerPos((*its)->getCenterPos());
806 for (its = pulsarSprites.begin(); its != pulsarSprites.end(); its++)
807 controller->addPulsarPos((*its)->getCenterPos());
808 for (its = saucerBulletSprites.begin(); its != saucerBulletSprites.end(); its++)
809 controller->addPulsarPos((*its)->getCenterPos());
810
811 controller->endGameState();
812 }
813
814 if (numLives > 0 && controller->isPauseRequested())
815 {
816 pause();
817 pauseState = AWAITING_RESUME;
818 return true;
819 }
820
821 if (playerSprite->decTimeToLive() != 0) // if player base exploding
822 return true;
823
824 if (controller->isEndRequested())
825 numLives = 0;
826
827 if (! playerIsActive())
828 return true;
829
830
831 Couple &playerPos = playerSprite->getPos();
832 const Couple &playerSize = playerSprite->getSize();
833
834
835 /* Fire a bullet once in a while:
836 */
837 int ticksBetweenBullets =
838 (playerBoostTicks > 0 && playerBoostType == CLOSER_BULLETS ? 2 : 4);
839 if (tickCount % ticksBetweenBullets == 0 && controller->isShootingActive())
840 {
841 registerCommand(COMMAND_SHOOT);
842
843 Couple bbSize = baseBulletPA.getImageSize();
844 Couple pos = playerPos + Couple((playerSize.x - bbSize.x) / 2, 0);
845
846 fireBullet(pos);
847 }
848 if (playerBoostTicks > 0)
849 playerBoostTicks--;
850
851
852 Couple &playerSpeed = playerSprite->getSpeed();
853
854 /* Get and process user commands:
855 */
856
857 playerSpeed.zero();
858
859 if (controller->isHyperspaceRequested())
860 {
861 registerCommand(COMMAND_HYPERSPACE);
862
863 Couple size = playerSprite->getSize();
864 int newX;
865 if (mirrorHyperspace)
866 {
867 // Mirror the player's position WRT the center:
868 Couple center = playerSprite->getCenterPos();
869 int newXCenter = theScreenSizeInPixels.x - center.x;
870 newX = newXCenter - size.x / 2;
871 if (newX < 0)
872 newX = 0; // just to be sure...
873 }
874 else
875 newX = rnd(0, theScreenSizeInPixels.x - size.x);
876
877 Couple newPos(newX, groundPos - playerPA.getImageSize().y);
878 repositionPlayerSprite(newPos);
879
880 playSoundEffect(hyperspaceSound);
881
882 return true;
883 }
884
885 if (controller->isLeftMoveRequested())
886 {
887 registerCommand(COMMAND_MOVE_LEFT);
888 playerSpeed.x = -PLAYER_SPEED;
889 }
890 if (controller->isRightMoveRequested())
891 {
892 registerCommand(COMMAND_MOVE_RIGHT);
893 playerSpeed.x = +PLAYER_SPEED;
894 }
895
896 if (playerSpeed.isZero())
897 return true;
898
899 Couple newPos = playerPos + playerSpeed;
900 if (newPos.x < 0)
901 newPos.x = 0;
902 else if (newPos.x + playerSize.x > theDrawingPixmapSize.x)
903 newPos.x = theDrawingPixmapSize.x - playerSize.x;
904
905 playerPos = newPos;
906
907 return true;
908 }
909
910
911 void
killSpritesInList(SpriteList & sl)912 CosmoSmashEngine::killSpritesInList(SpriteList &sl)
913 {
914 for (SpriteList::iterator its = sl.begin(); its != sl.end(); its++)
915 delete *its;
916
917 sl.clear();
918 assert(sl.size() == 0);
919 }
920
921
922 void
centerPlayerSprite()923 CosmoSmashEngine::centerPlayerSprite()
924 {
925 Couple newPos(
926 (theDrawingPixmapSize.x - playerPA.getImageSize().x) / 2,
927 groundPos - playerPA.getImageSize().y);
928 repositionPlayerSprite(newPos);
929 }
930
931
932 void
repositionPlayerSprite(Couple newPos)933 CosmoSmashEngine::repositionPlayerSprite(Couple newPos)
934 {
935 playerSprite->setPos(newPos);
936 }
937
938
939 void
killAllAutomaticCharacters()940 CosmoSmashEngine::killAllAutomaticCharacters()
941 {
942 killSpritesInList(rockSprites);
943 killSpritesInList(spinnerSprites);
944 killSpritesInList(pulsarSprites);
945 killSpritesInList(saucerSprites);
946 killSpritesInList(saucerBulletSprites);
947 killSpritesInList(questionSprites);
948 }
949
950
951 void
killPlayerBase()952 CosmoSmashEngine::killPlayerBase()
953 {
954 static const char *noKillEnvVar = getenv("NOKILL");
955
956 if (noKillEnvVar != NULL)
957 return;
958
959 playSoundEffect(playerHitSound);
960
961 addToScore(SCORE_LOST_BASE);
962
963 Couple pos = playerSprite->getCenterPos();
964 pos.y = playerSprite->getLowerRightPos().y - 1;
965 static const Couple directions[] =
966 {
967 Couple( 10, 0 ),
968 Couple( 7, -7 ),
969 Couple( 0, -10 ),
970 Couple( -7, -7 ),
971 Couple( -10, 0 ),
972 Couple( 0, 0 ) // zero marks the end
973 };
974
975 const int explosionTime = 40; // in ticks
976
977 for (size_t i = 0; directions[i].isNonZero(); i++)
978 {
979 Sprite *explosion = createExplosionSprite(pos, explosionTime);
980 explosion->setSpeed(directions[i]);
981 }
982
983 playerSprite->setTimeToLive(explosionTime);
984 /* The player's "time to live" value is used to mark
985 the period during which it is exploding. The player's
986 sprite is not displayed during that time. It is
987 repositioned for when it starts being displayed again.
988 */
989 centerPlayerSprite();
990 addToNumLives(-1);
991
992 playerBoostTicks = 0;
993 playerBoostType = NO_BOOST;
994
995 killAllAutomaticCharacters();
996 /* We kill falling sprites to avoid situations where e.g.,
997 spinners keep landing while the player is exploding.
998 Such situations could make the player lose many lives in a row.
999 */
1000
1001 killSpritesInList(baseBulletSprites);
1002 }
1003
1004
1005 inline bool
playerIsActive() const1006 CosmoSmashEngine::playerIsActive() const
1007 /*
1008 Returns true iff the player still has lives and not currently exploding...
1009 */
1010 {
1011 return (numLives > 0 && playerSprite->getTimeToLive() == 0);
1012 }
1013
1014
1015 pair<bool, size_t>
animateFallingObjects(SpriteList & sprites)1016 CosmoSmashEngine::animateFallingObjects(SpriteList &sprites)
1017 /*
1018 Moves the sprites in the designated sprite list.
1019
1020 If one of the sprites collides with the player, the player dies
1021 and the sprite is eliminated.
1022 Sprites that land are eliminated.
1023 Sprites that go off to the left or right of the screen are
1024 silently eliminated.
1025
1026 Returns a boolean value that indicates if the player dies, and the
1027 number of falling objects that landed.
1028
1029 When this method exits, pointers to eliminated sprites have been
1030 removed from 'sprites'.
1031 */
1032 {
1033 bool playerDies = false;
1034 size_t numLandings = 0; // number of objects that touch the ground
1035
1036 for (SpriteList::iterator it = sprites.begin(); it != sprites.end(); it++)
1037 {
1038 Sprite *s = *it;
1039 assert(s != NULL);
1040 s->addSpeedToPos();
1041
1042 if (playerIsActive() && s->collidesWithSprite(*playerSprite))
1043 {
1044 playerDies = true;
1045
1046 delete s;
1047 *it = NULL;
1048 continue;
1049 }
1050
1051 // Get y coordinate of bottom of sprite's collision box:
1052 int collBoxBottom = (s->getPos() + s->getCollBoxPos() + s->getCollBoxSize()).y;
1053 int height = groundPos - collBoxBottom;
1054
1055 if (height <= - groundHeight) // if sprite low enough, it dies
1056 {
1057 numLandings++;
1058
1059 delete s;
1060 *it = NULL;
1061 continue;
1062 }
1063
1064 const Couple newPos = s->getPos();
1065 if (newPos.x >= theScreenSizeInPixels.x
1066 || newPos.x + s->getSize().x <= 0)
1067 {
1068 // object disappears without consequence
1069 delete s;
1070 *it = NULL;
1071 continue;
1072 }
1073 }
1074
1075 removeNullElementsFromSpriteList(sprites);
1076
1077 return pair<bool, size_t>(playerDies, numLandings);
1078 }
1079
1080
1081 void
animateSaucers()1082 CosmoSmashEngine::animateSaucers()
1083 {
1084 bool playerDies = false;
1085
1086 for (SpriteList::iterator it = saucerSprites.begin();
1087 it != saucerSprites.end(); it++)
1088 {
1089 Sprite *saucer = *it;
1090 Couple &pos = saucer->getPos();
1091 const Couple &size = saucer->getSize();
1092
1093 pos += saucer->getSpeed();
1094
1095 // if the saucer is now off the screen:
1096 if (pos.x >= theScreenSizeInPixels.x || pos.x + size.x <= 0)
1097 {
1098 delete saucer;
1099 *it = NULL;
1100 continue;
1101 }
1102
1103 // make saucer shoot periodically:
1104 if (tickCount % 8 == 0 && playerSprite->getTimeToLive() == 0)
1105 {
1106 const Couple size = saucerBulletPA.getImageSize();
1107 Couple bulletPos = pos;
1108
1109 // Choose speed in direction of player base:
1110 Couple diff = playerSprite->getPos() - bulletPos;
1111 double k = SAUCER_BULLET_SPEED / hypot(diff.x, diff.y);
1112 Couple speed(mulRound(diff.x, k), mulRound(diff.y, k));
1113 speed = accelerate(speed);
1114 if (speed.isZero())
1115 speed = Couple(0, SAUCER_BULLET_SPEED);
1116 Sprite *s = new Sprite(saucerBulletPA,
1117 pos, speed, Couple(0, 0),
1118 Couple(2, 2), size - Couple(2, 2));
1119 saucerBulletSprites.push_back(s);
1120
1121 playSoundEffect(saucerShootingSound);
1122 }
1123 }
1124 removeNullElementsFromSpriteList(saucerSprites);
1125
1126 if (playerDies)
1127 killPlayerBase();
1128 }
1129
1130
1131 void
startSpinnerSound(SpinnerSprite & s)1132 CosmoSmashEngine::startSpinnerSound(SpinnerSprite &s)
1133 {
1134 if (theSoundMixer == NULL)
1135 return;
1136 roundbeetle::ADSR adsr(.0f, .1f, 0, 0, 3600, 0); // longer than needed
1137 int req = theSoundMixer->requestSquareWave(s.getLinearMovingFreq(),
1138 adsr,
1139 1,
1140 theSoundMixer->getMainBus());
1141 s.setSoundReq(req);
1142 }
1143
1144
1145 void
animateAutomaticCharacters()1146 CosmoSmashEngine::animateAutomaticCharacters()
1147 {
1148 /* If there are not enough ordinary falling sprites, add some:
1149 */
1150 size_t numFallingObjects = rockSprites.size() + spinnerSprites.size();
1151 size_t min = (theLevel < 5 ? 2 : 3);
1152 if (numFallingObjects < min && playerSprite->getTimeToLive() == 0)
1153 {
1154 int rockFreq = 8;
1155 if (theLevel >= 2)
1156 --rockFreq;
1157 if (theLevel >= 3)
1158 --rockFreq;
1159 if (rand() % rockFreq == 0)
1160 {
1161 // New spinner (more small spinners starting at level 2):
1162 int n = (theLevel < 2 ? 3 : 2);
1163 bool small = (rand() % n == 0);
1164 const PixmapArray *pa = (small ? &smallSpinnerPA : &bigSpinnerPA);
1165 const Couple size = pa->getImageSize();
1166 Couple pos(rnd(0, theScreenSizeInPixels.x - size.x), - size.y);
1167 Couple speed(rnd(-2, +2), rnd(3, 9));
1168 speed = accelerate(speed);
1169
1170 // Specify square wave sound:
1171 float startFreq = 400 + (small ? 40 : 0) + rand() % 40;
1172 float endFreq = 80 + (small ? 20 : 0) + rand() % 20;
1173 float durationInS = float(WINDOW_HEIGHT - groundHeight)
1174 / speed.y / FRAMES_PER_SECOND;
1175
1176 SpinnerSprite *s = new SpinnerSprite(*pa, pos, speed,
1177 size / 3, size / 3,
1178 theSoundMixer,
1179 startFreq,
1180 endFreq,
1181 durationInS);
1182 if (playerIsActive())
1183 startSpinnerSound(*s);
1184 spinnerSprites.push_back(s);
1185 }
1186 else
1187 {
1188 // New rock:
1189 const PixmapArray *pa =
1190 (rand() % 3 == 0 ? &smallRockPA : &bigRockPA);
1191 const Couple size = pa->getImageSize();
1192 Couple pos(rnd(0, theScreenSizeInPixels.x - size.x), - size.y);
1193 Couple speed(rnd(-2, +2), rnd(3 + theLevel, 20 + theLevel));
1194 speed = accelerate(speed);
1195 Sprite *s = new Sprite(*pa, pos, speed, Couple(0, 0),
1196 Couple(4, 4), size - Couple(4, 4));
1197 s->currentPixmapIndex = rnd(0, NUM_ROCK_IMAGES - 1);
1198 rockSprites.push_back(s);
1199 }
1200 }
1201
1202
1203 /* Send a new pulsar once in a while.
1204 */
1205 if (playerIsActive()
1206 && playerSprite->getTimeToLive() == 0
1207 && --timeBeforeNextPulsar == 0)
1208 {
1209 setTimeBeforeNextPulsar();
1210 const Couple size = pulsarPA.getImageSize();
1211 Couple pos(rnd(0, theScreenSizeInPixels.x - size.x), - size.y);
1212 Couple speed(0, PULSAR_SPEED + 3 * (theLevel >= 4));
1213 speed = accelerate(speed);
1214 Sprite *s = new Sprite(pulsarPA,
1215 pos, speed, Couple(0, 0),
1216 size / 4, size / 2);
1217 s->setTimeToLive(4 * 20);
1218 pulsarSprites.push_back(s);
1219 }
1220
1221
1222 /* Send a new saucer once in a while.
1223 */
1224 if (playerIsActive()
1225 && theLevel >= 4
1226 && playerSprite->getTimeToLive() == 0
1227 && --timeBeforeNextSaucer == 0)
1228 {
1229 setTimeBeforeNextSaucer();
1230 const Couple size = saucerPA.getImageSize();
1231
1232 Couple pos, speed;
1233 int v = SAUCER_SPEED + 2 * (theLevel >= 5);
1234 if (rnd(0, 1) == 0)
1235 {
1236 pos = Couple(- size.x, SAUCER_Y_POS);
1237 speed = Couple(v, 0);
1238 }
1239 else
1240 {
1241 pos = Couple(theScreenSizeInPixels.x, SAUCER_Y_POS);
1242 speed = Couple(- v, 0);
1243 }
1244
1245 Sprite *s = new Sprite(saucerPA,
1246 pos, speed, Couple(0, 0),
1247 Couple(2, 2), size - Couple(2, 2));
1248 s->currentPixmapIndex = 0;
1249 saucerSprites.push_back(s);
1250 }
1251
1252
1253 /* Send a new question mark once in a while.
1254 */
1255 if (useGameExtensions
1256 && playerIsActive()
1257 && theLevel >= 0
1258 && playerSprite->getTimeToLive() == 0
1259 && --timeBeforeNextQuestion == 0)
1260 {
1261 setTimeBeforeNextQuestion();
1262 const Couple size = questionPA.getImageSize();
1263
1264 Couple pos, speed;
1265 pos = Couple(
1266 rnd(size.x * 4, theScreenSizeInPixels.x - size.x * 4),
1267 - size.y);
1268 speed = Couple(rnd(-2, +2), QUESTION_SPEED + 2 * (theLevel >= 5));
1269
1270 Sprite *s = new Sprite(questionPA,
1271 pos, speed, Couple(0, 0),
1272 Couple(2, 2), size - Couple(2, 2));
1273 s->currentPixmapIndex = 0;
1274 questionSprites.push_back(s);
1275 }
1276
1277
1278 /* Let the rocks fall. If they hit the ground or go off the screen
1279 on the left or right, kill them. If they hit the player (while it
1280 is not currently exploding), then make the player explode.
1281 */
1282 pair<bool, size_t> p = animateFallingObjects(rockSprites);
1283 if (p.second > 0 && numLives > 0) // if rocks landed and player has lives
1284 addToScore(SCORE_BIG_ROCK_LANDING * p.second);
1285 // NOTE: landing of small rock still scored as big rock
1286 if (p.first) // if collision with player
1287 killPlayerBase();
1288
1289
1290 /* Let the question marks fall...
1291 */
1292 if (useGameExtensions)
1293 {
1294 pair<bool, size_t> p = animateFallingObjects(questionSprites);
1295 // We don't care when the question marks land.
1296 if (p.first) // if collision with player
1297 killPlayerBase();
1298 }
1299
1300
1301 /* Let the spinners fall. If they hit the ground, the player dies.
1302 If they go off the screen on the left or right, kill them.
1303 If they hit the player (while it is not currently exploding),
1304 then the player dies.
1305 */
1306 p = animateFallingObjects(spinnerSprites);
1307 if (p.second > 0 && playerIsActive())
1308 killPlayerBase();
1309 else if (p.first)
1310 killPlayerBase();
1311
1312
1313 /* Let the saucer bullets fall. If they hit the ground, kill them.
1314 If they go off the screen on the left or right, kill them.
1315 If they hit the player (while it is not currently exploding),
1316 then the player dies.
1317 */
1318 p = animateFallingObjects(saucerBulletSprites);
1319 if (p.first)
1320 killPlayerBase();
1321
1322
1323 /* Let the pulsars fall. If they hit the ground, kill them.
1324 If they go off the screen on the left or right, kill them.
1325 If they hit the player (while it is not currently exploding),
1326 then the player dies.
1327 Pulsars disappear after some time.
1328 Once in a while, redirect them towards the player.
1329 */
1330 p = animateFallingObjects(pulsarSprites);
1331 if (p.first)
1332 killPlayerBase();
1333
1334
1335 int n = (theLevel < 3 ? 8 : 6); // ticks between direction changes
1336 SpriteList::iterator it;
1337 for (it = pulsarSprites.begin(); it != pulsarSprites.end(); it++)
1338 {
1339 Sprite *pulsar = *it;
1340 assert(pulsar != NULL);
1341 if (pulsar->decTimeToLive() == 0)
1342 {
1343 delete pulsar;
1344 *it = NULL;
1345 }
1346 else if (tickCount % n == 0)
1347 {
1348 Couple diff = playerSprite->getPos() - pulsar->getPos();
1349 // NOTE: use projection instead of trig; see animateSaucers()
1350 double angle = atan2((double) diff.y, (double) diff.x);
1351 Couple newSpeed(int(PULSAR_SPEED * cos(angle)),
1352 int(PULSAR_SPEED * sin(angle)));
1353 if (newSpeed.isZero())
1354 newSpeed = Couple(0, PULSAR_SPEED);
1355 pulsar->setSpeed(newSpeed);
1356 }
1357 }
1358 removeNullElementsFromSpriteList(pulsarSprites);
1359
1360
1361 /* Move the saucers.
1362 */
1363 animateSaucers();
1364
1365
1366 /* Move the player's bullets:
1367 */
1368 for (it = baseBulletSprites.begin(); it != baseBulletSprites.end(); it++)
1369 {
1370 Sprite *s = *it;
1371 assert(s != NULL);
1372 s->addSpeedToPos();
1373 if (s->getPos().y + s->getSize().y <= 0) // if completely above screen
1374 {
1375 delete s;
1376 *it = NULL;
1377 }
1378 }
1379 removeNullElementsFromSpriteList(baseBulletSprites);
1380
1381
1382 /* Animate explosions:
1383 */
1384 for (it = explosionSprites.begin(); it != explosionSprites.end(); it++)
1385 {
1386 Sprite *s = *it;
1387 assert(s != NULL);
1388
1389 if (s->currentPixmapIndex > 1)
1390 {
1391 fprintf(stderr, "s->currentPixmapIndex = %lu\n", (unsigned long) s->currentPixmapIndex);
1392 assert(false);
1393 }
1394 if (s->decTimeToLive() == 0)
1395 {
1396 delete s;
1397 *it = NULL;
1398 continue;
1399 }
1400
1401 s->addSpeedToPos(); // some explosions move
1402
1403 s->currentPixmapIndex ^= 1; // switch between 0 and 1
1404 assert(s->currentPixmapIndex <= 1);
1405 }
1406 removeNullElementsFromSpriteList(explosionSprites);
1407 }
1408
1409
1410 Sprite *
createExplosionSprite(Couple posOfCenterOfExplosion,int timeToLive)1411 CosmoSmashEngine::createExplosionSprite(Couple posOfCenterOfExplosion,
1412 int timeToLive)
1413 /*
1414 Creates an explosion sprite centered at 'posOfCenterOfExplosion'.
1415 Sets the "time to live" value for the new sprite to 'timeToLive',
1416 which must be expressed in ticks.
1417 Inserts the address of this sprite in the list 'explosionSprites'.
1418 The sprite has no speed.
1419 Returns a pointer to the created sprite.
1420 */
1421 {
1422 Couple size = explosionPA.getImageSize();
1423 Couple pos = posOfCenterOfExplosion - size / 2;
1424 Sprite *s = new Sprite(explosionPA, pos,
1425 Couple(0, 0), Couple(0, 0),
1426 size / 4, size / 2);
1427 s->setTimeToLive(timeToLive);
1428 explosionSprites.push_back(s);
1429 return s;
1430 }
1431
1432
1433 pair<Sprite *, Sprite *>
splitBigRock(const Sprite & bigRock) const1434 CosmoSmashEngine::splitBigRock(const Sprite &bigRock) const
1435 /*
1436 Returns two new rock sprites which have not been inserted in
1437 any sprite list.
1438 */
1439 {
1440 const Couple bigRockPos = bigRock.getPos();
1441 const Couple bigRockSize = bigRock.getSize();
1442
1443 const Couple smallRockSize = smallRockPA.getImageSize();
1444 int smallRockY = bigRockPos.y + bigRockSize.y / 2 - smallRockSize.y / 2;
1445 Couple pos1(bigRockPos.x + bigRockSize.x * 1 / 4, smallRockY);
1446 Couple pos2(bigRockPos.x + bigRockSize.x * 3 / 4, smallRockY);
1447 pos2.x -= smallRockSize.x;
1448
1449 Couple speed1(rnd(-4, -2), bigRock.getSpeed().y); // must go to the left
1450 Couple speed2(- speed1.x, speed1.y); // must go to the right
1451
1452 // Rotate the speeds of the small rocks some times:
1453 int dir = rand() % 3;
1454 for (int times = rand() % 2 + 1; times > 0; times--)
1455 switch (dir)
1456 {
1457 case 0:
1458 speed1.x--;
1459 speed2.y++;
1460 break;
1461 case 1:
1462 speed2.x++;
1463 speed1.y++;
1464 break;
1465 default:
1466 ;
1467 }
1468
1469 speed1 = accelerate(speed1);
1470 speed2 = accelerate(speed2);
1471 Sprite *smallRock1 = new Sprite(smallRockPA,
1472 pos1, speed1, Couple(0, 0), Couple(2, 2),
1473 smallRockSize - Couple(2, 2));
1474 smallRock1->currentPixmapIndex = bigRock.currentPixmapIndex;
1475 Sprite *smallRock2 = new Sprite(smallRockPA,
1476 pos2, speed2, Couple(0, 0), Couple(2, 2),
1477 smallRockSize - Couple(2, 2));
1478 smallRock2->currentPixmapIndex = bigRock.currentPixmapIndex;
1479
1480 //rockSprites.push_back(smallRock1);
1481 //rockSprites.push_back(smallRock2);
1482 /* We do not insert the two new sprites in the rockSprites list
1483 right now because this method is called during an iteration
1484 over that list. Insertions in the list at this point could
1485 reallocate the internal data structure and thus disrupt
1486 the iteration. That is why we return a pair of newly
1487 created sprites which the caller can insert in the list
1488 when appropriate.
1489 */
1490 return pair<Sprite *, Sprite *>(smallRock1, smallRock2);
1491 }
1492
1493
1494 void
makeSpriteExplode(const Sprite * target,const Sprite * bullet,long points)1495 CosmoSmashEngine::makeSpriteExplode(const Sprite *target,
1496 const Sprite *bullet,
1497 long points)
1498 /*
1499 Makes the 'target' sprite explode.
1500 'bullet' must be the sprite that triggers the explosion; it is erased
1501 from the screen.
1502 The target is erased from the screen and an explosion is started
1503 at its position.
1504 'points' is the number of points to add to the player's score.
1505 The object designated by 'target' is not destroyed.
1506 'target' and 'bullet' must not be null.
1507 */
1508 {
1509 assert(target != NULL);
1510 assert(bullet != NULL);
1511
1512 playShortExplosionSound();
1513
1514 Couple targetPos = target->getPos();
1515
1516 Couple expPos = target->getCenterPos();
1517 createExplosionSprite(expPos, 6);
1518
1519 addToScore(points);
1520 }
1521
1522
1523 bool
detectCollisionsWithBullet(const Sprite & bullet,SpriteList & slist,int scoreOnCollision)1524 CosmoSmashEngine::detectCollisionsWithBullet(const Sprite &bullet,
1525 SpriteList &slist,
1526 int scoreOnCollision)
1527 /*
1528 Detects fatal collisions between a bullet and sprites in 'slist'.
1529
1530 'slist' is allowed to contain null pointers; they are ignored.
1531
1532 If a collision is detected between 'bullet' and an element
1533 of 'slist', this element is made null in 'slist' and the
1534 corresponding Sprite object is destroyed with operator delete;
1535 also, an explosion is created at the point of collision and the
1536 method returns true.
1537
1538 The method returns false if no collision is detected.
1539 */
1540 {
1541 bool collisionDetected = false;
1542
1543 for (SpriteList::iterator its = slist.begin(); its != slist.end(); its++)
1544 {
1545 Sprite *aSprite = *its;
1546 if (aSprite == NULL) // if already dead
1547 continue;
1548
1549 if (bullet.collidesWithSprite(*aSprite))
1550 {
1551 makeSpriteExplode(aSprite, &bullet, scoreOnCollision);
1552
1553 delete aSprite;
1554 *its = NULL;
1555 collisionDetected = true;
1556 }
1557 }
1558
1559 return collisionDetected;
1560 }
1561
1562
1563 bool
detectCollisionsWithExplosions()1564 CosmoSmashEngine::detectCollisionsWithExplosions()
1565 /*
1566 Kills certain sprites if they touch an explosion sprite.
1567 Explosion sprites do not disappear because of a collision.
1568 Sprite lists containing killed sprites will contain null
1569 elements after this method finishes. The caller is
1570 responsible for cleaning up those null elements.
1571
1572 Returns true if the player dies, or false otherwise.
1573 */
1574 {
1575 if (!playerIsActive())
1576 return false;
1577
1578 /* Make a local copy of the list of explosion sprites, and iterate
1579 on the copy.
1580 We cannot iterate over 'explosionSprites' because the calls
1581 to detectCollisionsWithBullet() will add elements to it;
1582 this could invalidate an iterator that would point into the list.
1583
1584 This technique is not efficient in principle, but the number of
1585 explosions is normally low, so no visible performance hit should
1586 be observed.
1587 */
1588 SpriteList tempList(explosionSprites.size());
1589 copy(explosionSprites.begin(), explosionSprites.end(), tempList.begin());
1590
1591 assert(tempList.size() == explosionSprites.size());
1592
1593 SpriteList::const_iterator it;
1594 for (it = tempList.begin(); it != tempList.end(); it++)
1595 {
1596 const Sprite *anExplosion = *it;
1597 if (anExplosion == NULL)
1598 continue;
1599
1600 // Consider the explosion sprite as a bullet:
1601 (void) detectCollisionsWithBullet(*anExplosion, rockSprites, 0);
1602 (void) detectCollisionsWithBullet(*anExplosion, spinnerSprites, 0);
1603 (void) detectCollisionsWithBullet(*anExplosion, pulsarSprites, 0);
1604 (void) detectCollisionsWithBullet(*anExplosion, saucerSprites, 0);
1605 (void) detectCollisionsWithBullet(*anExplosion, saucerBulletSprites, 0);
1606
1607 #if 0 // this makes the explosions kill the base also.
1608 if (anExplosion->collidesWithSprite(*playerSprite))
1609 {
1610 killPlayerBase(); // this kills all meanies
1611 return true; // no need to continue
1612 }
1613 #endif
1614 }
1615
1616 return false;
1617 }
1618
1619
detectCollisionsBetweenFallingObjects(SpriteList & slist)1620 void CosmoSmashEngine::detectCollisionsBetweenFallingObjects(SpriteList &slist)
1621 /*
1622 Sprite lists containing killed sprites will contain null
1623 elements after this method finishes. The caller is
1624 responsible for cleaning up those null elements.
1625 */
1626 {
1627 SpriteList::iterator it;
1628 for (it = slist.begin(); it != slist.end(); it++)
1629 {
1630 const Sprite *s = *it;
1631 if (s == NULL)
1632 continue;
1633
1634 bool coll = false;
1635 if (!coll)
1636 coll = detectCollisionsWithBullet(*s, rockSprites, 0);
1637 if (!coll)
1638 coll = detectCollisionsWithBullet(*s, saucerSprites, 0);
1639 if (!coll)
1640 coll = detectCollisionsWithBullet(*s, saucerBulletSprites, 0);
1641 if (coll)
1642 {
1643 *it = NULL;
1644 delete s;
1645 }
1646 }
1647 }
1648
1649
1650 void
detectCollisions()1651 CosmoSmashEngine::detectCollisions()
1652 /*
1653 NOTE: Many collisions are detected here, but there are some others
1654 that are detected elsewhere, as in animateFallingObjects(), for example.
1655 */
1656 {
1657 SpriteList newRocks;
1658 bool playerBoosted = false;
1659
1660 /* Detect collisions involving the player's bullets:
1661 */
1662 for (SpriteList::iterator itb = baseBulletSprites.begin();
1663 itb != baseBulletSprites.end(); itb++)
1664 {
1665 Sprite *bullet = *itb;
1666 assert(bullet != NULL);
1667
1668 for (SpriteList::iterator itr = rockSprites.begin();
1669 itr != rockSprites.end(); itr++)
1670 {
1671 Sprite *rock = *itr;
1672 if (rock == NULL) // if already dead
1673 continue; // next rock
1674
1675 assert(bullet != NULL);
1676
1677 if (bullet->collidesWithSprite(*rock))
1678 {
1679 long points;
1680 bool explode = true;
1681 if (rock->getSize() == bigRockPA.getImageSize())
1682 {
1683 points = SCORE_BIG_ROCK;
1684
1685 /* Some big rocks break into two small rocks.
1686 */
1687 if (rand() % 3 == 0)
1688 {
1689 playShortExplosionSound();
1690
1691 pair<Sprite *, Sprite *> p = splitBigRock(*rock);
1692 newRocks.push_back(p.first);
1693 newRocks.push_back(p.second);
1694
1695
1696 /* We don't create an explosion when a big rock splits
1697 because an explosion would instantly destroy the
1698 two new small rocks. @sarrazip 20010530
1699 */
1700 explode = false;
1701 }
1702 }
1703 else
1704 points = SCORE_SMALL_ROCK;
1705
1706 if (explode)
1707 makeSpriteExplode(rock, bullet, points);
1708
1709
1710 // don't destroy 'bullet' -- only mark it for deletion:
1711 *itb = NULL;
1712 delete rock;
1713 *itr = NULL;
1714 break;
1715 }
1716 }
1717
1718
1719 assert(bullet != NULL); // must still be alive for next for()
1720
1721
1722 SpriteList::iterator its;
1723 for (its = spinnerSprites.begin(); its != spinnerSprites.end(); its++)
1724 {
1725 Sprite *spinner = *its;
1726 if (spinner == NULL) // if already dead
1727 continue; // next spinner
1728
1729 assert(bullet != NULL);
1730 if (bullet->collidesWithSprite(*spinner))
1731 {
1732 long points;
1733 if (spinner->getSize() == bigSpinnerPA.getImageSize())
1734 points = SCORE_BIG_SPINNER;
1735 else
1736 points = SCORE_SMALL_SPINNER;
1737
1738 makeSpriteExplode(spinner, bullet, points);
1739
1740 // don't destroy 'bullet' -- only mark it for deletion:
1741 *itb = NULL;
1742 delete spinner;
1743 *its = NULL;
1744 break;
1745 }
1746 }
1747
1748
1749 assert(bullet != NULL); // must still be alive for next for()
1750
1751
1752 if (detectCollisionsWithBullet(
1753 *bullet, pulsarSprites, SCORE_PULSAR))
1754 *itb = NULL; // this marks that the bullet hit something
1755
1756 if (detectCollisionsWithBullet(
1757 *bullet, saucerSprites, SCORE_SAUCER))
1758 *itb = NULL;
1759
1760 if (detectCollisionsWithBullet(
1761 *bullet, saucerBulletSprites, SCORE_SAUCER_BULLET))
1762 *itb = NULL;
1763
1764 if (detectCollisionsWithBullet(
1765 *bullet, questionSprites, SCORE_QUESTION))
1766 {
1767 *itb = NULL;
1768 playerBoosted = true;
1769 }
1770
1771 if (*itb == NULL) // if bullet hit something
1772 delete bullet;
1773
1774 } // for each bullet
1775
1776
1777 detectCollisionsBetweenFallingObjects(pulsarSprites);
1778 detectCollisionsBetweenFallingObjects(questionSprites);
1779
1780 (void) detectCollisionsWithExplosions();
1781
1782 removeNullElementsFromSpriteList(baseBulletSprites);
1783 removeNullElementsFromSpriteList(rockSprites);
1784 removeNullElementsFromSpriteList(spinnerSprites);
1785 removeNullElementsFromSpriteList(pulsarSprites);
1786 removeNullElementsFromSpriteList(saucerSprites);
1787 removeNullElementsFromSpriteList(saucerBulletSprites);
1788 removeNullElementsFromSpriteList(questionSprites);
1789
1790 rockSprites.insert(rockSprites.end(), newRocks.begin(), newRocks.end());
1791
1792 if (playerBoosted)
1793 boostPlayer();
1794 }
1795
1796
1797 void
boostPlayer()1798 CosmoSmashEngine::boostPlayer()
1799 {
1800 playerBoostTicks = rnd(100, 150);
1801
1802 switch (rnd(0, 1))
1803 {
1804 case 0: playerBoostType = TRIPLE_BULLETS; break;
1805 case 1: playerBoostType = CLOSER_BULLETS; break;
1806 }
1807 }
1808
1809
1810 ///////////////////////////////////////////////////////////////////////////////
1811
1812
1813 void
restoreBackground()1814 CosmoSmashEngine::restoreBackground()
1815 {
1816 bool baseExploding = (playerSprite->getTimeToLive() != 0);
1817
1818 // Background:
1819 assert(theLevel >= 1 && theLevel <= NUM_LEVELS);
1820 Uint32 bgColor = (baseExploding ? explosionBGColor : levelColors[theLevel]);
1821 fillRect(0, 0, theDrawingPixmapSize.x, theDrawingPixmapSize.y, bgColor);
1822
1823 // Ground:
1824 fillRect(0, groundPos, theDrawingPixmapSize.x, groundHeight, greenColor);
1825
1826 // Stars:
1827 bool moveStars = (numLives > 0 && ! baseExploding && tickCount % 4 == 0);
1828 CoupleList::iterator it;
1829 for (it = starPositions.begin(); it != starPositions.end(); it++)
1830 {
1831 Couple &pos = *it;
1832 drawPixel(pos.x, pos.y, whiteColor);
1833
1834 if (moveStars)
1835 {
1836 pos.y--;
1837 if (pos.y < 0)
1838 pos.y = mountainTopPos - 1;
1839 }
1840 }
1841
1842 // Mountain range:
1843 Couple previous(-1, -1);
1844 for (it = mountainRangePositions.begin(); it != mountainRangePositions.end(); it++)
1845 {
1846 if (previous.x != -1) // do nothing the first time
1847 drawLine(previous.x, previous.y, (*it).x, (*it).y, greenColor);
1848 previous = Couple((*it).x, (*it).y);
1849 }
1850 }
1851
1852
1853 void
drawSprites()1854 CosmoSmashEngine::drawSprites()
1855 {
1856 if (playerIsActive())
1857 {
1858 int pixmapIndex = (playerBoostTicks > 0 ? ((tickCount & 4) != 0) : 0);
1859 copySpritePixmap(*playerSprite, pixmapIndex, playerSprite->getPos());
1860 }
1861
1862 SpriteList::const_iterator it;
1863 for (it = rockSprites.begin(); it != rockSprites.end(); it++)
1864 copySpritePixmap(**it, (**it).currentPixmapIndex, (**it).getPos());
1865
1866 if (useGameExtensions)
1867 for (it = questionSprites.begin(); it != questionSprites.end(); it++)
1868 {
1869 size_t &index = (**it).currentPixmapIndex;
1870 index = (tickCount / 8) % 2;
1871 copySpritePixmap(**it, index, (**it).getPos());
1872 }
1873
1874 for (it = spinnerSprites.begin(); it != spinnerSprites.end(); it++)
1875 {
1876 size_t &index = (**it).currentPixmapIndex;
1877 index = (tickCount / 4) % NUM_SPINNER_IMAGES;
1878 copySpritePixmap(**it, index, (**it).getPos());
1879 }
1880
1881 if (!pulsarSprites.empty() && tickCount % (NUM_PULSAR_IMAGES * 4) == 0)
1882 playSoundEffect(pulsarBeepSound);
1883 for (it = pulsarSprites.begin(); it != pulsarSprites.end(); it++)
1884 {
1885 size_t &index = (**it).currentPixmapIndex;
1886 index = (tickCount / 2) % NUM_PULSAR_IMAGES;
1887 copySpritePixmap(**it, index, (**it).getPos());
1888 }
1889
1890 for (it = saucerSprites.begin(); it != saucerSprites.end(); it++)
1891 {
1892 size_t &index = (**it).currentPixmapIndex;
1893 index = (tickCount / 4) % NUM_SAUCER_IMAGES;
1894 copySpritePixmap(**it, index, (**it).getPos());
1895 }
1896
1897 for (it = saucerBulletSprites.begin(); it != saucerBulletSprites.end(); it++)
1898 {
1899 size_t &index = (**it).currentPixmapIndex;
1900 index = (tickCount / 2) % NUM_SAUCER_BULLET_IMAGES;
1901 copySpritePixmap(**it, index, (**it).getPos());
1902 }
1903
1904 for (it = baseBulletSprites.begin(); it != baseBulletSprites.end(); it++)
1905 copySpritePixmap(**it, 0, (**it).getPos());
1906
1907 for (it = explosionSprites.begin(); it != explosionSprites.end(); it++)
1908 {
1909 assert(*it != NULL);
1910 assert((**it).currentPixmapIndex <= 1);
1911 copySpritePixmap(**it, (**it).currentPixmapIndex, (**it).getPos());
1912 }
1913
1914 {
1915 int ext = (useGameExtensions ? '*' : ' ');
1916 char s[64];
1917 snprintf(s, sizeof(s), " Score :%8ld%c", theScore, ext);
1918 writeString(s, scoreAreaPos);
1919 snprintf(s, sizeof(s), " Peak Score :%8ld%c", thePeakScore, ext);
1920 writeString(s, peakScoreAreaPos);
1921 }
1922
1923 {
1924 char s[64];
1925 snprintf(s, sizeof(s), " Level:%3d ", theLevel);
1926 writeString(s, levelNumAreaPos);
1927 snprintf(s, sizeof(s), " Lives:%3d ", numLives);
1928 writeString(s, numLivesAreaPos);
1929 updateNumLives = false;
1930 }
1931 }
1932
1933
1934 void
addToScore(long n)1935 CosmoSmashEngine::addToScore(long n)
1936 {
1937 theScore += n * theLevel;
1938 if (theScore > thePeakScore)
1939 {
1940 /* Maintain the peak score. Give the player a new life each time
1941 the peak score crosses some boundary.
1942 */
1943 const long newLifeScoreInterval =
1944 (useGameExtensions ? 10000 : 1000);
1945 long before = thePeakScore / newLifeScoreInterval;
1946 thePeakScore = theScore;
1947 long after = thePeakScore / newLifeScoreInterval;
1948 if (before != after)
1949 addToNumLives(+1);
1950 }
1951 theLevel = getLevelNum(theScore);
1952 }
1953
1954
1955 void
addToNumLives(int n)1956 CosmoSmashEngine::addToNumLives(int n)
1957 {
1958 numLives += n;
1959 updateNumLives = true;
1960 }
1961
1962
1963 Couple
accelerate(Couple speed) const1964 CosmoSmashEngine::accelerate(Couple speed) const
1965 {
1966 int coef = (theScore >= 100000 ? theScore - 100000 : 0) / 25000 + 1;
1967 if (speed.x < 0)
1968 speed.x -= coef;
1969 else if (speed.x > 0)
1970 speed.x += coef;
1971 if (speed.y < 0)
1972 speed.y -= coef;
1973 else if (speed.y > 0)
1974 speed.y += coef;
1975 return speed;
1976 }
1977
1978
1979 void
displayPauseMessage()1980 CosmoSmashEngine::displayPauseMessage()
1981 {
1982 displayMessage(10, "PAUSED -- press P to resume");
1983 }
1984
1985
1986 void
displayStartMessage(bool display)1987 CosmoSmashEngine::displayStartMessage(bool display)
1988 /*
1989 Displays the start message if 'display' is true, or erases the
1990 corresponding region if 'display' is false.
1991 */
1992 {
1993 if (display)
1994 {
1995 int lineNum = 0;
1996 displayMessage(lineNum++, "Cosmosmash " VERSION " - by Pierre Sarrazin ");
1997 displayMessage(lineNum++, " ");
1998 displayMessage(lineNum++, "Press SPACE to start a standard game ");
1999 displayMessage(lineNum++, "Press E to start a game with extensions");
2000 displayMessage(lineNum++, " ");
2001 displayMessage(lineNum++, "Move: left/right arrows ");
2002 displayMessage(lineNum++, "Hyperspace: down arrow ");
2003 displayMessage(lineNum++, "Pause: P ");
2004 displayMessage(lineNum++, "Toggle Full Screen: F11 ");
2005
2006 if (useSound && theSoundMixer == NULL)
2007 {
2008 lineNum++;
2009 lineNum++;
2010 displayMessage(lineNum++, "*** Sound device not available *** ");
2011 }
2012 }
2013 }
2014
2015
2016 void
displayMessage(int row,const char * msg)2017 CosmoSmashEngine::displayMessage(int row, const char *msg)
2018 {
2019 const Couple fontdim = getFontDimensions();
2020
2021 const size_t len = strlen(msg);
2022 int x = (theScreenSizeInPixels.x - len * fontdim.x) / 2;
2023 int y = theScreenSizeInPixels.y + (row - 26) * fontdim.y;
2024
2025 writeString(msg, Couple(x, y));
2026 }
2027
2028
2029 void
playShortExplosionSound()2030 CosmoSmashEngine::playShortExplosionSound()
2031 {
2032 int soundNo = rand() % NUM_ROCK_HIT_SOUNDS;
2033 playSoundEffect(rockHitSounds[soundNo]);
2034 }
2035
2036
2037 void
playSoundEffect(const roundbeetle::WaveFileBuffer & wfb)2038 CosmoSmashEngine::playSoundEffect(const roundbeetle::WaveFileBuffer &wfb)
2039 {
2040 if (! playerIsActive() || theSoundMixer == NULL)
2041 return;
2042
2043 roundbeetle::Bus &mainBus = theSoundMixer->getMainBus();
2044 int reqHandle = theSoundMixer->requestWaveFileSound(wfb, mainBus);
2045 if (reqHandle == -1)
2046 cout << PACKAGE ": failed to play sound " << &wfb << endl;
2047 }
2048
2049
2050 void
registerCommand(Command command)2051 CosmoSmashEngine::registerCommand(Command command)
2052 {
2053 if (gameScriptStream == NULL)
2054 return;
2055
2056 *gameScriptStream << static_cast<int>(command) << ' ';
2057 }
2058
2059
2060 void
registerTickEnd()2061 CosmoSmashEngine::registerTickEnd()
2062 {
2063 if (gameScriptStream == NULL)
2064 return;
2065
2066 *gameScriptStream << '\n';
2067 }
2068
2069
2070 void
playCadenceSound()2071 CosmoSmashEngine::playCadenceSound()
2072 {
2073 if (! playerIsActive() || theSoundMixer == NULL)
2074 return;
2075
2076 if (timeBeforeNextCadenceSound > 0)
2077 {
2078 --timeBeforeNextCadenceSound;
2079 return;
2080 }
2081
2082 // Compute a period in seconds between cadence sounds.
2083 // The cadence accelerates with the score.
2084 // The cadence period has a minimum.
2085 //
2086 const double startPeriod = 0.750; // period in seconds when score is 0
2087 const double endPeriod = 0.300; // period in seconds when score is scoreOfFastestPeriod
2088 const long scoreOfFastestPeriod = 50000;
2089 double t = double(theScore) / scoreOfFastestPeriod;
2090 t = std::max(t, 0.0);
2091 t = std::min(t, 1.0);
2092 double cadenceInSeconds = startPeriod + t * (endPeriod - startPeriod);
2093
2094 timeBeforeNextCadenceSound = (unsigned long) (cadenceInSeconds * 20);
2095 /*cout << theScore << " -> " << cadenceInSeconds << " s, "
2096 << timeBeforeNextCadenceSound << " ticks" << endl;*/
2097
2098 // Play a white noise with decay and release time that fit
2099 // the computed cadence period.
2100 //
2101 float decayTime = cadenceInSeconds * .267f;
2102 float releaseTime = cadenceInSeconds * .4f;
2103 roundbeetle::ADSR adsr(.25f, .15f, .0f, decayTime, .0f, releaseTime);
2104 /*int req =*/ theSoundMixer->requestWhiteNoise(adsr, 1, theSoundMixer->getMainBus());
2105 //if (req == -1)
2106 // cout << PACKAGE ": failed to play cadence sound" << endl;
2107 }
2108