Untitled
unknown
c_cpp
2 years ago
19 kB
6
Indexable
#include <allegro5/allegro.h> #include <algorithm> #include <cmath> #include <fstream> #include <functional> #include <vector> #include <queue> #include <string> #include <memory> #include "AudioHelper.hpp" #include "DirtyEffect.hpp" #include "Enemy.hpp" #include "GameEngine.hpp" #include "Group.hpp" #include "IObject.hpp" #include "Image.hpp" #include "Label.hpp" // Turret #include "PlugGunTurret.hpp" #include "MachineGunTurret.hpp" #include "MachineGun2Turret.hpp" #include "FourTurret.hpp" #include "Shifter.hpp" #include "Shovel.hpp" #include "Plane.hpp" // Enemy #include "AllEnemy.hpp" #include "Dice2Enemy.hpp" #include "PlayScene.hpp" #include "Resources.hpp" #include "Sprite.hpp" #include "Turret.hpp" #include "TurretButton.hpp" #include "LOG.hpp" bool PlayScene::DebugMode = false; const std::vector<Engine::Point> PlayScene::directions = { Engine::Point(-1, 0), Engine::Point(0, -1), Engine::Point(1, 0), Engine::Point(0, 1) }; const int PlayScene::MapWidth = 20, PlayScene::MapHeight = 13; const int PlayScene::BlockSize = 64; const float PlayScene::DangerTime = 7.61; const Engine::Point PlayScene::SpawnGridPoint = Engine::Point(-1, 0); const Engine::Point PlayScene::EndGridPoint = Engine::Point(MapWidth, MapHeight - 1); // TODO 5 (2/3): Set the cheat code correctly. const std::vector<int> PlayScene::code = { ALLEGRO_KEY_UP,ALLEGRO_KEY_DOWN,ALLEGRO_KEY_LEFT,ALLEGRO_KEY_RIGHT,ALLEGRO_KEY_LEFT,ALLEGRO_KEY_RIGHT,ALLEGRO_KEY_ENTER }; Engine::Point PlayScene::GetClientSize() { return Engine::Point(MapWidth * BlockSize, MapHeight * BlockSize); } void PlayScene::Initialize() { // TODO 6 (1/2): There's a bug in this file, which crashes the game when you win. Try to find it. // TODO 6 (2/2): There's a bug in this file, which doesn't update the player's life correctly when getting the first attack. Try to find it. mapState.clear(); keyStrokes.clear(); ticks = 0; deathCountDown = -1; lives = 10; money = 150; SpeedMult = 1; // Add groups from bottom to top. AddNewObject(TileMapGroup = new Group()); AddNewObject(GroundEffectGroup = new Group()); AddNewObject(DebugIndicatorGroup = new Group()); AddNewObject(TowerGroup = new Group()); AddNewObject(EnemyGroup = new Group()); AddNewObject(BulletGroup = new Group()); AddNewObject(EffectGroup = new Group()); // Should support buttons. AddNewControlObject(UIGroup = new Group()); ReadMap(); ReadEnemyWave(); mapDistance = CalculateBFSDistance(); ConstructUI(); imgTarget = new Engine::Image("play/target.png", 0, 0); imgTarget->Visible = false; preview = nullptr; UIGroup->AddNewObject(imgTarget); // Preload Lose Scene deathBGMInstance = Engine::Resources::GetInstance().GetSampleInstance("astronomia.ogg"); Engine::Resources::GetInstance().GetBitmap("lose/benjamin-happy.png"); // Start BGM. // bgmId = AudioHelper::PlayBGM("play.ogg"); if (!mute) bgmInstance = AudioHelper::PlaySample("play.ogg", true, AudioHelper::BGMVolume); else bgmInstance = AudioHelper::PlaySample("play.ogg", true, 0.0); } void PlayScene::Terminate() { AudioHelper::StopBGM(bgmId); AudioHelper::StopSample(deathBGMInstance); deathBGMInstance = std::shared_ptr<ALLEGRO_SAMPLE_INSTANCE>(); IScene::Terminate(); } void PlayScene::Update(float deltaTime) { // If we use deltaTime directly, then we might have Bullet-through-paper problem. // Reference: Bullet-Through-Paper if (SpeedMult == 0) deathCountDown = -1; else if (deathCountDown != -1) SpeedMult = 1; // Calculate danger zone. std::vector<float> reachEndTimes; for (auto& it : EnemyGroup->GetObjects()) { reachEndTimes.push_back(dynamic_cast<Enemy*>(it)->reachEndTime); } // Can use Heap / Priority-Queue instead. But since we won't have too many enemies, sorting is fast enough. std::sort(reachEndTimes.begin(), reachEndTimes.end()); float newDeathCountDown = -1; int danger = lives; for (auto& it : reachEndTimes) { if (it <= DangerTime) { danger--; if (danger <= 0) { // Death Countdown float pos = DangerTime - it; if (it > deathCountDown) { // Restart Death Count Down BGM. AudioHelper::StopSample(deathBGMInstance); if (SpeedMult != 0) deathBGMInstance = AudioHelper::PlaySample("astronomia.ogg", false, AudioHelper::BGMVolume, pos); } float alpha = pos / DangerTime; alpha = std::max(0, std::min(255, static_cast<int>(alpha * alpha * 255))); dangerIndicator->Tint = al_map_rgba(255, 255, 255, alpha); newDeathCountDown = it; break; } } } deathCountDown = newDeathCountDown; if (SpeedMult == 0) AudioHelper::StopSample(deathBGMInstance); if (deathCountDown == -1 && lives > 0) { AudioHelper::StopSample(deathBGMInstance); dangerIndicator->Tint.a = 0; } if (SpeedMult == 0) deathCountDown = -1; for (int i = 0; i < SpeedMult; i++) { IScene::Update(deltaTime); // Check if we should create new enemy. ticks += deltaTime; if (enemyWaveData.empty()) { if (EnemyGroup->GetObjects().empty()) { // Free resources. /*delete TileMapGroup; delete GroundEffectGroup; delete DebugIndicatorGroup; delete TowerGroup; delete EnemyGroup; delete BulletGroup; delete EffectGroup; delete UIGroup; delete imgTarget;*/ Engine::GameEngine::GetInstance().ChangeScene("win"); } continue; } auto current = enemyWaveData.front(); if (ticks < current.second) continue; ticks -= current.second; enemyWaveData.pop_front(); const Engine::Point SpawnCoordinate = Engine::Point(SpawnGridPoint.x * BlockSize + BlockSize / 2, SpawnGridPoint.y * BlockSize + BlockSize / 2); Enemy* enemy; switch (current.first) { case 1: EnemyGroup->AddNewObject(enemy = new RedNormalEnemy(SpawnCoordinate.x, SpawnCoordinate.y)); break; case 2: EnemyGroup->AddNewObject(enemy = new DiceEnemy(SpawnCoordinate.x, SpawnCoordinate.y)); break; case 3: EnemyGroup->AddNewObject(enemy = new Dice2Enemy(SpawnCoordinate.x, SpawnCoordinate.y)); break; // TODO 2 (2/3): You need to modify 'resources/enemy1.txt', or 'resources/enemy2.txt' to spawn the new enemy. // The format is "[EnemyId] [TimeDelay] [Repeat]". // TODO 2 (3/3): Enable the creation of the new enemy. default: continue; } enemy->UpdatePath(mapDistance); // Compensate the time lost. enemy->Update(ticks); } if (preview) { preview->Position = Engine::GameEngine::GetInstance().GetMousePosition(); // To keep responding when paused. preview->Update(deltaTime); } } void PlayScene::Draw() const { IScene::Draw(); if (DebugMode) { // Draw reverse BFS distance on all reachable blocks. for (int i = 0; i < MapHeight; i++) { for (int j = 0; j < MapWidth; j++) { if (mapDistance[i][j] != -1) { // Not elegant nor efficient, but it's quite enough for debugging. Engine::Label label(std::to_string(mapDistance[i][j]), "pirulen.ttf", 32, (j + 0.5) * BlockSize, (i + 0.5) * BlockSize); label.Anchor = Engine::Point(0.5, 0.5); label.Draw(); } } } } } void PlayScene::OnMouseDown(int button, int mx, int my) { if ((button & 1) && !imgTarget->Visible && preview) { // Cancel turret construct. UIGroup->RemoveObject(preview->GetObjectIterator()); preview = nullptr; } IScene::OnMouseDown(button, mx, my); } void PlayScene::OnMouseMove(int mx, int my) { IScene::OnMouseMove(mx, my); const int x = mx / BlockSize; const int y = my / BlockSize; if (!preview || x < 0 || x >= MapWidth || y < 0 || y >= MapHeight) { imgTarget->Visible = false; return; } imgTarget->Visible = true; imgTarget->Position.x = x * BlockSize; imgTarget->Position.y = y * BlockSize; } void PlayScene::OnMouseUp(int button, int mx, int my) { IScene::OnMouseUp(button, mx, my); if (!imgTarget->Visible) return; const int x = mx / BlockSize; const int y = my / BlockSize; if (button & 1) { if (mapState[y][x] != TILE_OCCUPIED ||( mapState[y][x] == TILE_OCCUPIED && preview->shovel) || (mapState[y][x] == TILE_OCCUPIED && preview->shifter)) { if (!preview) return; if ((!preview->machine && !preview->shovel && !preview->shifter) && mapState[y][x] == MACHINE_GUN_OCCUPIED) return; // Check if valid. if (!CheckSpaceValid(x, y)) { Engine::Sprite* sprite; GroundEffectGroup->AddNewObject(sprite = new DirtyEffect("play/target-invalid.png", 1, x * BlockSize + BlockSize / 2, y * BlockSize + BlockSize / 2)); sprite->Rotation = 0; return; } // Purchase. EarnMoney(-preview->GetPrice()); // Remove Preview. preview->GetObjectIterator()->first = false; UIGroup->RemoveObject(preview->GetObjectIterator()); for (auto& it : TowerGroup->GetObjects()) { Turret* turret = dynamic_cast<Turret*>(it); int position_x = it->Position.x / BlockSize; int position_y = it->Position.y / BlockSize; if (preview->machine && position_x == x && position_y == y) { TowerGroup->RemoveObject(it->GetObjectIterator()); preview = new MachineGun2Turret(0,0); preview->machine2 = 1; preview->machine = 0; } else if (preview->shovel && position_x == x && position_y == y) { EarnMoney(turret->GetPrice() / 2); TowerGroup->RemoveObject(it->GetObjectIterator()); } else if (preview->shifter && position_x == x && position_y == y) { std::cout << turret->machine <<' '<<turret->machine2 ; if (!turret->machine && !turret->machine2) preview = new PlugGunTurret(0, 0); else if(turret->machine) preview = new MachineGunTurret(0, 0); else if (turret->machine2) preview = new MachineGun2Turret(0, 0); TowerGroup->RemoveObject(it->GetObjectIterator()); } } if (!(preview->shovel == 1 || preview->shifter == 1)) { // Construct real turret. preview->Position.x = x * BlockSize + BlockSize / 2; preview->Position.y = y * BlockSize + BlockSize / 2; preview->Enabled = true; preview->Preview = false; preview->Tint = al_map_rgba(255, 255, 255, 255); TowerGroup->AddNewObject(preview); } if (preview->machine) mapState[y][x] = MACHINE_GUN_OCCUPIED; else if(preview->shovel || preview->shifter) mapState[y][x] = TILE_FLOOR; else mapState[y][x] = TILE_OCCUPIED; preview->shovel = 0; preview->shifter = 0; preview->machine2 = 0; // To keep responding when paused. preview->Update(0); // Remove Preview. preview = nullptr; OnMouseMove(mx, my); } } } void PlayScene::OnKeyDown(int keyCode) { IScene::OnKeyDown(keyCode); if (keyCode == ALLEGRO_KEY_TAB) { // TODO 5 (1/3): Set Tab as a code to active / de-active the debug mode. if (DebugMode == true) DebugMode = false; else DebugMode = true; } else { keyStrokes.push_back(keyCode); if (keyStrokes.size() > code.size()) keyStrokes.pop_front(); // TODO 5 (3/3): Check whether the input sequence corresponds to the code. If so, active a plane and earn 10000 money. // Active a plane : EffectGroup->AddNewObject(new Plane()); // Earn money : money += 10000; std::list<int>::iterator it; int i = 0; for (it = keyStrokes.begin(); it != keyStrokes.end(); it++, i++) { if (*it != code[i]) break; if (i == 6) { EffectGroup->AddNewObject(new Plane()); money += 10000; } } } if (keyCode == ALLEGRO_KEY_Q) { // Hotkey for PlugGunTurret. UIBtnClicked(0); } // TODO 3 (5/5): Make the W key to create the new turret. else if (keyCode == ALLEGRO_KEY_W) { // Hotkey for new turret. UIBtnClicked(1); } else if (keyCode >= ALLEGRO_KEY_0 && keyCode <= ALLEGRO_KEY_9) { // Hotkey for Speed up. SpeedMult = keyCode - ALLEGRO_KEY_0; } else if (keyCode == ALLEGRO_KEY_M) { // Hotkey for mute / unmute. if (mute) AudioHelper::ChangeSampleVolume(bgmInstance, AudioHelper::BGMVolume); else AudioHelper::ChangeSampleVolume(bgmInstance, 0.0); mute = !mute; } } void PlayScene::Hit() { UILives->Text = std::string("Life ") + std::to_string(--lives); if (lives <= 0) { Engine::GameEngine::GetInstance().ChangeScene("lose"); } } int PlayScene::GetMoney() const { return money; } void PlayScene::EarnMoney(int money) { this->money += money; UIMoney->Text = std::string("$") + std::to_string(this->money); } void PlayScene::ReadMap() { std::string filename = std::string("resources/map") + std::to_string(MapId) + ".txt"; // Read map file. char c; std::vector<bool> mapData; std::ifstream fin(filename); while (fin >> c) { switch (c) { case '0': mapData.push_back(false); break; case '1': mapData.push_back(true); break; case '\n': case '\r': if (static_cast<int>(mapData.size()) / MapWidth != 0) throw std::ios_base::failure("Map data is corrupted."); break; default: throw std::ios_base::failure("Map data is corrupted."); } } fin.close(); // Validate map data. if (static_cast<int>(mapData.size()) != MapWidth * MapHeight) throw std::ios_base::failure("Map data is corrupted."); // Store map in 2d array. mapState = std::vector<std::vector<TileType>>(MapHeight, std::vector<TileType>(MapWidth)); for (int i = 0; i < MapHeight; i++) { for (int j = 0; j < MapWidth; j++) { const int num = mapData[i * MapWidth + j]; mapState[i][j] = num ? TILE_FLOOR : TILE_DIRT; if (num) TileMapGroup->AddNewObject(new Engine::Image("play/floor.png", j * BlockSize, i * BlockSize, BlockSize, BlockSize)); else TileMapGroup->AddNewObject(new Engine::Image("play/dirt.png", j * BlockSize, i * BlockSize, BlockSize, BlockSize)); } } } void PlayScene::ReadEnemyWave() { std::string filename = std::string("resources/enemy") + std::to_string(MapId) + ".txt"; // Read enemy file. float type, wait, repeat; enemyWaveData.clear(); std::ifstream fin(filename); while (fin >> type && fin >> wait && fin >> repeat) { for (int i = 0; i < repeat; i++) enemyWaveData.emplace_back(type, wait); } fin.close(); } void PlayScene::ConstructUI() { // Background UIGroup->AddNewObject(new Engine::Image("play/sand.png", 1280, 0, 320, 832)); // Text UIGroup->AddNewObject(new Engine::Label(std::string("Stage ") + std::to_string(MapId), "pirulen.ttf", 32, 1294, 0)); UIGroup->AddNewObject(UIMoney = new Engine::Label(std::string("$") + std::to_string(money), "pirulen.ttf", 24, 1294, 48)); UIGroup->AddNewObject(UILives = new Engine::Label(std::string("Life ") + std::to_string(lives), "pirulen.ttf", 24, 1294, 88)); // Buttons ConstructButton(0, "play/turret-6.png", PlugGunTurret::Price); ConstructButton(1, "play/turret-1.png", MachineGunTurret::Price); ConstructButton(2, "play/ufo.png", FourTurret::Price); ConstructButton(3, "play/shovel.png", Shovel::Price); ConstructButton(4, "play/shifter.png", Shifter::Price); // TODO 3 (3/5): Create a button to support constructing the new turret. int w = Engine::GameEngine::GetInstance().GetScreenSize().x; int h = Engine::GameEngine::GetInstance().GetScreenSize().y; int shift = 135 + 25; dangerIndicator = new Engine::Sprite("play/benjamin.png", w - shift, h - shift); dangerIndicator->Tint.a = 0; UIGroup->AddNewObject(dangerIndicator); } void PlayScene::ConstructButton(int id, std::string sprite, int price) { TurretButton* btn; if (id < 2) btn = new TurretButton("play/floor.png", "play/dirt.png", Engine::Sprite("play/tower-base.png", 1294 + id * 76, 136, 0, 0, 0, 0), Engine::Sprite(sprite, 1294 + id * 76, 136 - 8, 0, 0, 0, 0) , 1294 + id * 76, 136, price); else if (id == 2) btn = new TurretButton("play/floor.png", "play/dirt.png", Engine::Sprite("play/tower-base.png", 1294 + id * 76, 136, 0, 0, 0, 0), Engine::Sprite(sprite, 1294 + id * 76, 136, 0, 0, 0, 0) , 1294 + id * 76, 136, price); else btn = new TurretButton("play/floor.png", "play/dirt.png", Engine::Sprite("play/tower-base.png", -100, -100, 0, 0, 0, 0), Engine::Sprite(sprite, 1294 + (id-3) * 76, 136 + 76, 0, 0, 0, 0) , 1294 + (id-3) * 76, 136 + 76, price); // Reference: Class Member Function Pointer and std::bind. btn->SetOnClickCallback(std::bind(&PlayScene::UIBtnClicked, this, id)); UIGroup->AddNewControlObject(btn); } void PlayScene::UIBtnClicked(int id) { if (preview) { UIGroup->RemoveObject(preview->GetObjectIterator()); preview = nullptr; } if (id == 0 && money >= PlugGunTurret::Price) { preview = new PlugGunTurret(0, 0); preview->machine = 0; preview->machine2 = 0; } if (id == 1 && money >= MachineGunTurret::Price) { preview = new MachineGunTurret(0, 0); preview->machine = 1; } if (id == 2 && money >= FourTurret::Price) preview = new FourTurret(0, 0); if (id == 3 && money >= Shovel::Price) { preview = new Shovel(0, 0); preview->shovel = 1; } if (id == 4 && money >= Shifter::Price) { preview = new Shifter(0, 0); preview->shifter = 1; } // TODO 3 (4/5): On the new turret button callback, create the new turret. if (!preview) return; preview->Position = Engine::GameEngine::GetInstance().GetMousePosition(); preview->Tint = al_map_rgba(255, 255, 255, 200); preview->Enabled = false; preview->Preview = true; UIGroup->AddNewObject(preview); OnMouseMove(Engine::GameEngine::GetInstance().GetMousePosition().x, Engine::GameEngine::GetInstance().GetMousePosition().y); } bool PlayScene::CheckSpaceValid(int x, int y) { if (x < 0 || x >= MapWidth || y < 0 || y >= MapHeight) return false; auto map00 = mapState[y][x]; mapState[y][x] = TILE_OCCUPIED; std::vector<std::vector<int>> map = CalculateBFSDistance(); mapState[y][x] = map00; if (map[0][0] == -1) return false; for (auto& it : EnemyGroup->GetObjects()) { Engine::Point pnt; pnt.x = floor(it->Position.x / BlockSize); pnt.y = floor(it->Position.y / BlockSize); if (pnt.x < 0) pnt.x = 0; if (pnt.x >= MapWidth) pnt.x = MapWidth - 1; if (pnt.y < 0) pnt.y = 0; if (pnt.y >= MapHeight) pnt.y = MapHeight - 1; if (map[pnt.y][pnt.x] == -1) return false; } // All enemy have path to exit. mapState[y][x] = TILE_OCCUPIED; mapDistance = map; for (auto& it : EnemyGroup->GetObjects()) dynamic_cast<Enemy*>(it)->UpdatePath(mapDistance); return true; } std::vector<std::vector<int>> PlayScene::CalculateBFSDistance() { // Reverse BFS to find path. std::vector<std::vector<int>> map(MapHeight, std::vector<int>(std::vector<int>(MapWidth, -1))); std::queue<Engine::Point> que; // Push end point. // BFS from end point. if (mapState[MapHeight - 1][MapWidth - 1] != TILE_DIRT) return map; que.push(Engine::Point(MapWidth - 1, MapHeight - 1)); map[MapHeight - 1][MapWidth - 1] = 0; while (!que.empty()) { Engine::Point p = que.front(); que.pop(); for (auto &c : directions) { int x = p.x + c.x; int y = p.y + c.y; if (x < 0 || x >= MapWidth || y < 0 || y >= MapHeight || map[y][x] != -1 || mapState[y][x] != TILE_DIRT) { continue; } else { map[y][x] = map[p.y][p.x] + 1; que.push(Engine::Point(x, y)); } } } return map; }
Editor is loading...