Version of Robotron arcade game using LPC1768, a Gameduino shield, a serial EEPROM (for high scores), two microswitch joysticks and two buttons plus a box to put it in. 20 levels of mayhem.

Dependencies:   25LCxxx_SPI CommonTypes Gameduino mbed

LevelNormal.cpp

Committer:
RichardE
Date:
2013-06-17
Revision:
18:70190f956a24
Parent:
13:50779b12ff51

File content as of revision 18:70190f956a24:

/*
 * SOURCE FILE : LevelNormal.cpp
 *
 * Definition of class LevelNormal.
 * Base class for all "normal" levels.
 * i.e. Levels that are not special attract modes
 * but have enemies who are trying to kill you
 * and so on.
 *
 */

// Define this for debugging messages.
#define CHATTY

#ifdef CHATTY
    #include "mbed.h"
    extern Serial pc;
#endif

#include "LevelNormal.h"
#include "GameObjectLocator.h"
#include "FrameCounter.h"
#include "SpriteNumber.h"
#include "ArenaConst.h"
#include "EnemyFactory.h"

// Current instance being processed.
LevelNormal *LevelNormal::currentInstance;

/***************/
/* CONSTRUCTOR */
/***************/
// Pass pointer to level descriptor data in data parameter.
LevelNormal::LevelNormal( const LevelDescriptor *data ) :
    descriptorData( data )
{
    // Normal levels are always dynamically alocated.
    IsDynamicallyAllocated = true;
}

/**************/
/* DESTRUCTOR */
/**************/
LevelNormal::~LevelNormal()
{
}

/********************/
/* INITIALISE LEVEL */
/********************/
// Pass pointer to Gameduino to draw on in gd.
// Note that this is called at the start of every level but also when the player
// is killed and the level restarts with the remaining enemies.
void LevelNormal::InitialiseLevel( Gameduino *gd )
{
    // Note that if you re-arrange the following code you may need to adjust the
    // SpriteNumber enumeration in SpriteNumber.h.
    UInt8 spriteNumber = FirstEnemySprite;
    // Initialise enemies.
    GameObject::InitialiseAll( dataForLevel.Enemies, LevelData::MaxEnemies, &spriteNumber );
    // Initialise humans.
    spriteNumber = FirstHumanSprite;
    GameObject::InitialiseAll( dataForLevel.Humans, LevelData::MaxHumans, &spriteNumber );
    // Use next free sprite number for player.
    player->SpriteNumber = PlayerSprite;
    // Do futher initialisation for all enemies.
    EnemyObject *object;
    for( UInt8 e = 0; e < LevelData::MaxEnemies; ++e ) {
        object = (EnemyObject*)dataForLevel.Enemies[ e ];
        if( object != (EnemyObject*)NULL ) {
            // Perform any further initialisation required
            // at level start or restart.
            object->LevelRestart();
            // Get enemy to chase the player.
            object->SetChaseObject( player );
            // Pass array of all enemies to this enemy.
            object->Enemies = dataForLevel.Enemies;
            // If enemy is a brain then tell it about the humans to chase.
            if( object->GetEnemyType() == Brain ) {
                ((BrainObject*)object)->HumansToChase = dataForLevel.Humans;
            }
        }
    }
    // Put player in the centre of the arena.
    player->Xco = PLAYER_START_X;
    player->Yco = PLAYER_START_Y;
    // Kill off all player's bullets.
    player->KillAllBullets( gd );
    // Kill off all explosions.
    ExplosionManager::Instance.KillAllExplosions();
}

/**********************************/
/* DRAW SCORE AND NUMBER OF LIVES */
/**********************************/
void LevelNormal::DrawScoreAndLives( void )
{
    GDExtra::WriteBCDNumber( gd, 16, 0, player->Score, 8 );
    // Display number of lives but limit this to 20 lives displayed.
    UInt8 lives = ( player->Lives > 20 ) ? 20 : player->Lives;
    gd->fill( Gameduino::RAM_PIC + VISIBLE_CHAR_WIDTH - lives, MiniPlayer, lives );
}

/******************/
/* DRAW THE LEVEL */
/******************/
void LevelNormal::DrawLevel( void )
{
    // Set screen background to black.
    gd->wr( Gameduino::BG_COLOR, Gameduino::RGB( 0, 0, 0 ) );
    // Clear the screen to zero characters.
    GDExtra::ClearScreen( gd, TransparentChar );
    // Hide all sprties.
    GDExtra::HideAllSprites( gd );
    // Display level number.
    GDExtra::WriteProgString( gd, 0, 0, StringData::LevelString );
    GDExtra::WriteUInt16( gd, 6, 0, LevelNumber, 10, 2 );
    // Display score.
    GDExtra::WriteProgString( gd, 10, 0, StringData::ScoreString );
    // Update score and lives.
    DrawScoreAndLives();
    // Draw border around screen.
    CharFrame::Draw(
        gd,
        ARENA_BORDER_X,
        ARENA_BORDER_Y,
        ARENA_BORDER_WIDTH,
        ARENA_BORDER_HEIGHT
    );
}

/************************************************/
/* HANDLE COLLISIONS BETWEEN HUMANS AND ENEMIES */
/************************************************/
// Pass index of human in level's humans array in humanIndex.
// Pass sprite number of sprite that it hit in spriteNumber.
void LevelNormal::HandleHumanCollision( UInt8 humanIndex, UInt8 spriteNumber )
{
    // Point to array of enemy object pointers.
    GameObject **enemies = currentInstance->dataForLevel.Enemies;
    EnemyObject *enemy;
    UInt8 enemyIndex, mutantIndex;
    // Find an enemy with given sprite number.
    if( GameObject::FindSpriteNumber( enemies, LevelData::MaxEnemies, spriteNumber, &enemyIndex ) ) {
        // Found enemy. Check if it squashes humans.
        enemy = (EnemyObject*)enemies[ enemyIndex ];
        // Get hold of the human that is doomed.
        GameObject **humans = currentInstance->dataForLevel.Humans;
        HumanObject *human = (HumanObject*)humans[ humanIndex ];
        // Human must be walking around. Not rescued or already dead.
        if( human->CurrentState == HumanObject::WalkingAbout ) {
            if( enemy->SquashesHumans ) {
                // Change human to dead state.
                human->CurrentState = HumanObject::Dead;
                // Make a noise.
                SoundManager::Instance.PlaySound( Sounds::HumanDies, 0, 0 );
            } else if( enemy->GetEnemyType() == Brain ) {
                // Kill off human by writing a NULL into the array.
                // DO NOT change human to dead state as this will generate a dead human animation
                // and this human is not dead, but a mutant.
                humans[ humanIndex ] = (HumanObject*)NULL;
                // Find a free slot for a new enemy.
                if( GameObject::FindUnusedObject( enemies, LevelData::MaxEnemies, &mutantIndex ) ) {
                    // Write a pointer to a new mutant into the enemy array.
                    MutantObject *mutant = (MutantObject*)EnemyFactory::Instance.MakeEnemy( Mutant );
                    if( mutant != (MutantObject*)NULL ) {
                        enemies[ mutantIndex ] = mutant;
                        // Initialise mutant at coordinates of human and chasing the player.
                        mutant->Start( human, currentInstance->player );
                        // Make a noise.
                        // TODO : SoundManager::Instance.PlaySound( Sounds::HumanMutates, 0, 0 );
                    }
                } else {
                    // Could not find a free slot for a new enemy so just erase the human sprite.
                    GDExtra::HideSprite( currentInstance->gd, human->SpriteNumber );
                }
            }
        }
    }
}

/********************************************************/
/* HANDLE COLLISIONS BETWEEN PLAYER BULLETS AND ENEMIES */
/********************************************************/
// Pass index of bullet in player's bullet array in bulletIndex.
// Pass sprite number of sprite that it hit in spriteNumber.
void LevelNormal::HandleBulletCollision( UInt8 bulletIndex, UInt8 spriteNumber )
{
    // Point to array of enemy object pointers.
    GameObject **enemies = currentInstance->dataForLevel.Enemies;
    EnemyObject *enemy;
    UInt8 enemyIndex;
    // Find an enemy with given sprite number.
    if( GameObject::FindSpriteNumber( enemies, LevelData::MaxEnemies, spriteNumber, &enemyIndex ) ) {
        // Found enemy. Check if it is indestructable.
        enemy = (EnemyObject*)enemies[ enemyIndex ];
        // Remember coordinates for explosion.
        Int16 explodeX = enemy->Xco;
        Int16 explodeY = enemy->Yco;
        if( enemy->HitPoints != EnemyObject::Indestructable ) {
            // Enemy is not indestructable. Decrement hit points and die when it reaches zero.
            enemy->HitPoints--;
            if( enemy->HitPoints == 0 ) {
                // Hide the enemy sprite.
                GDExtra::HideSprite( currentInstance->gd, enemy->SpriteNumber );
                // Add points to player's score.
                currentInstance->player->AddToScore( enemy->GetPoints() );
                // Kill enemy by deleting it and then inserting a NULL into enemies array.
                EnemyFactory::Instance.DeleteEnemy( enemy );
                enemy = (EnemyObject*)NULL;
                enemies[ enemyIndex ] = (GameObject*)NULL;
            }
        }
        // If enemy is still alive then tell enemy it has been hit by a bullet.
        if( enemy != (EnemyObject*)NULL ) {
            enemy->RegisterHitByBullet();
        }
        // Kill off the bullet.
        currentInstance->player->KillBullet( currentInstance->gd, bulletIndex );
        // Make a noise.
        SoundManager::Instance.PlaySound( Sounds::Explosion, 0, 0 );
        // Start explosion animation.
        ExplosionManager::Instance.StartExplosion( explodeX, explodeY );
    }
}

/*********************************************************/
/* CHECK FOR COLLISIONS BETWEEN PLAYER AND OTHER OBJECTS */
/*********************************************************/
// Pass pointer to a flag that will be set true if player is dead in isDead parameter.
void LevelNormal::CheckPlayerCollisions( bool *isDead )
{
    UInt8 enemyIndex, humanIndex;
    // Check if player sprite has hit another sprite.
    UInt8 hitSpriteNumber = gd->rd( Gameduino::COLLISION + player->SpriteNumber );
    // If you get 0xFF then no collision found.
    if( hitSpriteNumber != 0xFF ) {
        // Check for collision with an enemy.
        if(
            GameObject::FindSpriteNumber(
                dataForLevel.Enemies, LevelData::MaxEnemies, hitSpriteNumber, &enemyIndex
            )
        ) {
            // Hit an enemy. Player is dead.
            *isDead = true;
        }
        // Check for collision with a human that has not already been rescued or killed.
        else if(
            GameObject::FindSpriteNumber(
                dataForLevel.Humans, LevelData::MaxHumans, hitSpriteNumber, &humanIndex
            )
        ) {
            HumanObject *human = (HumanObject*)dataForLevel.Humans[ humanIndex ];
            if( human->CurrentState == HumanObject::WalkingAbout ) {
                // Change human state to rescued.
                human->CurrentState = HumanObject::Rescued;
                // Give player 50 points (in BCD!).
                player->AddToScore( 0x50 );
                // Make a noise.
                SoundManager::Instance.PlaySound( Sounds::RescueHuman, 0, 0 );
            }
        }
    }
}

/***********************************************************************************/
/* WAIT UNTIL SLOT FREE FOR A NEW SOUND, PLAY IT AND WAIT FOR ALL SOUNDS TO FINISH */
/***********************************************************************************/
// Pass sound to play in soundToPlay parameter.
void LevelNormal::PlaySoundAndWait( const UInt8 *soundToPlay )
{
    // Keep trying to play sound until it works and meanwhile
    // keep currently playing sounds going.
    while( ! SoundManager::Instance.PlaySound( soundToPlay, 0, 0 ) ) {
        // Update sound manager.
        SoundManager::Instance.Update();
        // Wait for frame flyback.
        gd->waitvblank();
    }
    // Now wait until all sounds have finished.
    while( SoundManager::Instance.CountSoundsPlaying() > 0 ) {
        // Update sound manager.
        SoundManager::Instance.Update();
        // Wait for frame flyback.
        gd->waitvblank();
    }
}

/**************/
/* PLAY LEVEL */
/**************/
// Returns code indicating how level ended.
Level::LevelExitCode LevelNormal::Play( void ) {
  UInt8 enemyCount = 0;
  GameObject **ptr = dataForLevel.Enemies;
  // Repeat for all enemy types.
  for( UInt8 et = 0; et < (int)EnemyTypeCount; ++et ) {
    // Get number of this enemy type on this level.
    UInt8 eCount = descriptorData->GetEnemyCount( (EnemyType)et );
    // Create required number of enemies.
    for( UInt8 eNum = 0; eNum < eCount; ++eNum ) {
        if( enemyCount < LevelData::MaxEnemies ) {
          GameObject *newEnemy = EnemyFactory::Instance.MakeEnemy( (EnemyType)et );
          if( newEnemy != (GameObject*)NULL ) {
            *ptr++ = newEnemy;
            enemyCount++;
          }
        }
    }
  }
  // Create required number of humans.
  HumanObject humans[ LevelData::MaxHumans ];
  ptr = dataForLevel.Humans;
  UInt8 humanCount = descriptorData->HumanCount;
  humanCount = ( humanCount > LevelData::MaxHumans ) ? LevelData::MaxHumans : humanCount;
  for( UInt8 i = 0; i < humanCount; ++i ) {
    *ptr++ = humans + i;
  }
  // Play the level. Returns when game is over or level is complete, but NOT
  // if the player gets killed and has lives remaining.
  Level::LevelExitCode exitCode = PlayLoop();
  // Make sure all memory allocated to enemies is freed.
  ptr = dataForLevel.Enemies;
  for( UInt8 i = 0; i < LevelData::MaxEnemies; ++i ) {
    if( *ptr != (GameObject*)NULL ) {
      EnemyFactory::Instance.DeleteEnemy( (EnemyObject*)*ptr );
      *ptr = (GameObject*)NULL;
    }
    ptr++;
  }
  return exitCode;
}

/*************/
/* PLAY LOOP */
/*************/
// Returns code indicating how level ended.
// This method should be called from the Play method after the
// level data has been initialised and the return value returned
// by the Play method.
Level::LevelExitCode LevelNormal::PlayLoop( void )
{
    // Do nothing if Gameduino has not been specified, level data is NULL or player has not been specified.
    if( ( gd != (Gameduino*)NULL ) || ( player == (PlayerObject*)NULL ) ) {
        // Point static pointer to current instance.
        currentInstance = this;
        // Do some initialisation first.
        InitialiseLevel( gd );
        // Redraw the screen.
        DrawLevel();
        // Wait for frame flyback once before entering loop so collision data is recalculated.
        // At this point there should not be any sprites on the screen so no collisions
        // should be found.
        gd->waitvblank();
        // Repeat until all enemies are dead or player is dead.
        bool allEnemiesAreDead = false;
        bool playerIsDead = false;
        bool gameIsOver = false;
        bool firstDraw = true;
        while( ! allEnemiesAreDead && ! gameIsOver ) {
            // Update sound manager.
            SoundManager::Instance.Update();
            // Wait for frame flyback.
            gd->waitvblank();
            // Check for collisions between player and other objects.
            CheckPlayerCollisions( &playerIsDead );
            // Check for collisions between humans and enemies that squash.
            GameObject::FindCollisions( gd, dataForLevel.Humans, LevelData::MaxHumans, &LevelNormal::HandleHumanCollision );
            // Check for collisions between player bullets and enemies.
            GameObject::FindCollisions( gd, player->GetBullets(), BulletManager::MaxBullets, &LevelNormal::HandleBulletCollision );
            // Redraw the player's score and number of lives.
            DrawScoreAndLives();
            // Draw all the enemies.
            GameObject::DrawAll( gd, dataForLevel.Enemies, LevelData::MaxEnemies );
            // Draw all the humans.
            GameObject::DrawAll( gd, dataForLevel.Humans, LevelData::MaxHumans );
            // Draw all the explosions.
            GameObject::DrawAll( gd, ExplosionManager::Instance.GetExplosions(), ExplosionManager::MaxExplosions );
            // Draw the player.
            player->Draw( gd );
            // Draw the player's bullets.
            GameObject::DrawAll( gd, player->GetBullets(), BulletManager::MaxBullets );
            // Increment the frame counter.
            FrameCounter++;
            // After first redraw play level start sound and wait for it to end.
            if( firstDraw ) {
                PlaySoundAndWait( Sounds::StartLevel );
                firstDraw = false;
            }
            // If player was killed then play death march and wait for it to finish.
            if( playerIsDead ) {
                // Player got killed.
                PlaySoundAndWait( Sounds::PlayerDead );
                // One less life for player.
                if( player->Lives > 0 ) {
                    player->Lives--;
                }
                // Game is over when player has no more lives.
                gameIsOver = ( player->Lives == 0 );
                // If game is not over then re-initialise level using any remaining enemies.
                if( ! gameIsOver ) {
                    // Remove all objects that do not survive a level restart (like enemy bullets).
                    GameObject::RemoveUnretainedObjects( dataForLevel.Enemies, LevelData::MaxEnemies );
                    InitialiseLevel( gd );
                    DrawLevel();
                    gd->waitvblank();
                    playerIsDead = false;
                    firstDraw = true;
                }
            }
            else {
                // Move all the enemies and check if all dead.
                allEnemiesAreDead = ! GameObject::MoveAll( dataForLevel.Enemies, LevelData::MaxEnemies );
                // If there are still some enemies alive then check if those that remain are indestructable.
                // If only indestructable enemies survive then act as if all enemies are dead.
                // You need to do this or you would never be able to complete a level that had indestructable
                // enemies on it.
                if( ! allEnemiesAreDead ) {
                    allEnemiesAreDead = EnemyObject::AreAllIndestructable(
                        (const EnemyObject**)dataForLevel.Enemies,
                        LevelData::MaxEnemies
                    );
                }
                // Move all the humans.
                GameObject::MoveAll( dataForLevel.Humans, LevelData::MaxHumans );
                // Move (update) all the explosions.
                GameObject::MoveAll( ExplosionManager::Instance.GetExplosions(), ExplosionManager::MaxExplosions );
                // Read the player's controls.
                player->ReadControls();
                // Move the player.
                player->Move();
                // Move the player's bullets.
                GameObject::MoveAll( player->GetBullets(), BulletManager::MaxBullets );
            }
        }
        // Player completed level or game is over.
        return gameIsOver ? GameOver : Completed;
    }
    else {
        // Level data or player were not specified.
        return Completed;
    }
}