void MainPanel::Draw() const { FrameTimer loadTimer; glClear(GL_COLOR_BUFFER_BIT); engine.Draw(); if(Preferences::Has("Show CPU / GPU load")) { string loadString = to_string(static_cast<int>(load * 100. + .5)) + "% GPU"; Color color = *GameData::Colors().Get("medium"); FontSet::Get(14).Draw(loadString, Point(10., Screen::Height() * -.5 + 5.), color); loadSum += loadTimer.Time(); if(++loadCount == 60) { load = loadSum; loadSum = 0.; loadCount = 0; } } }
void MainPanel::Draw() { FrameTimer loadTimer; glClear(GL_COLOR_BUFFER_BIT); engine.Draw(); if(isDragging) { if(canDrag) { const Color &dragColor = *GameData::Colors().Get("drag select"); LineShader::Draw(dragSource, Point(dragSource.X(), dragPoint.Y()), .8f, dragColor); LineShader::Draw(Point(dragSource.X(), dragPoint.Y()), dragPoint, .8f, dragColor); LineShader::Draw(dragPoint, Point(dragPoint.X(), dragSource.Y()), .8f, dragColor); LineShader::Draw(Point(dragPoint.X(), dragSource.Y()), dragSource, .8f, dragColor); } else isDragging = false; } if(Preferences::Has("Show CPU / GPU load")) { string loadString = to_string(lround(load * 100.)) + "% GPU"; const Color &color = *GameData::Colors().Get("medium"); FontSet::Get(14).Draw(loadString, Point(10., Screen::Height() * -.5 + 5.), color); loadSum += loadTimer.Time(); if(++loadCount == 60) { load = loadSum; loadSum = 0.; loadCount = 0; } } }
void Engine::CalculateStep() { FrameTimer loadTimer; // Clear the list of objects to draw. draw[calcTickTock].Clear(step); radar[calcTickTock].Clear(); if(!player.GetSystem()) return; // Now, all the ships must decide what they are doing next. ai.Step(ships, player); const Ship *flagship = player.Flagship(); bool wasHyperspacing = (flagship && flagship->IsEnteringHyperspace()); // Now, move all the ships. We must finish moving all of them before any of // them fire, or their turrets will be targeting where a given ship was // instead of where it is now. This is also where ships get deleted, and // where they may create explosions if they are dying. for(auto it = ships.begin(); it != ships.end(); ) { // Give the ship the list of effects so that if it is dying, it can // create explosions. Eventually ships might create other effects too. // Note that engine flares are handled separately, so that they will be // drawn immediately under the ship. if(!(*it)->Move(effects)) it = ships.erase(it); else { // Boarding: bool autoPlunder = !(*it)->GetGovernment()->IsPlayer(); shared_ptr<Ship> victim = (*it)->Board(autoPlunder); if(victim) eventQueue.emplace_back(*it, victim, (*it)->GetGovernment()->IsEnemy(victim->GetGovernment()) ? ShipEvent::BOARD : ShipEvent::ASSIST); ++it; } } if(!wasHyperspacing && flagship && flagship->IsEnteringHyperspace()) Audio::Play(Audio::Get(flagship->Attributes().Get("jump drive") ? "jump_drive" : "hyperspace")); // If the player has entered a new system, update the asteroids, etc. if(wasHyperspacing && !flagship->IsEnteringHyperspace()) { doFlash = true; EnterSystem(); } else if(flagship && player.GetSystem() != flagship->GetSystem()) { // Wormhole travel: player.ClearTravel(); doFlash = true; EnterSystem(); } // Now we know the player's current position. Draw the planets. Point center; Point centerVelocity; if(flagship) { center = flagship->Position(); centerVelocity = flagship->Velocity(); } else if(player.GetPlanet()) { for(const StellarObject &object : player.GetSystem()->Objects()) if(object.GetPlanet() == player.GetPlanet()) center = object.Position(); } if(!flagship) doClick = false; for(const StellarObject &object : player.GetSystem()->Objects()) if(!object.GetSprite().IsEmpty()) { Point position = object.Position(); Point unit = object.Unit(); position -= center; int type = object.IsStar() ? Radar::SPECIAL : !object.GetPlanet() ? Radar::INACTIVE : object.GetPlanet()->IsWormhole() ? Radar::ANOMALOUS : object.GetPlanet()->CanLand() ? Radar::FRIENDLY : Radar::HOSTILE; double r = max(2., object.Radius() * .03 + .5); // Don't apply motion blur to very large planets and stars. bool isBig = (object.GetSprite().Width() >= 280); draw[calcTickTock].Add(object.GetSprite(), position, unit, isBig ? Point() : -centerVelocity); radar[calcTickTock].Add(type, position, r, r - 1.); if(doClick && object.GetPlanet() && (clickPoint - position).Length() < object.Radius()) player.Flagship()->SetTargetPlanet(&object); } // Add all neighboring systems to the radar. const System *targetSystem = flagship ? flagship->GetTargetSystem() : nullptr; for(const System *system : player.GetSystem()->Links()) radar[calcTickTock].AddPointer( (system == targetSystem) ? Radar::SPECIAL : Radar::INACTIVE, system->Position() - player.GetSystem()->Position()); // Now that the planets have been drawn, we can draw the asteroids on top // of them. This could be done later, as long as it is done before the // collision detection. asteroids.Step(); asteroids.Draw(draw[calcTickTock], center, centerVelocity); // Move existing projectiles. Do this before ships fire, which will create // new projectiles, since those should just stay where they are created for // this turn. This is also where projectiles get deleted, which may also // result in a "die" effect or a sub-munition being created. We could not // move the projectiles before this because some of them are homing and need // to know the current positions of the ships. list<Projectile> newProjectiles; for(auto it = projectiles.begin(); it != projectiles.end(); ) { if(!it->Move(effects)) { it->MakeSubmunitions(newProjectiles); it = projectiles.erase(it); } else ++it; } projectiles.splice(projectiles.end(), newProjectiles); // Keep track of the relative strength of each government in this system. Do // not add more ships to make a winning team even stronger. This is mostly // to avoid having the player get mobbed by pirates, say, if they hang out // in one system for too long. map<const Government *, int64_t> strength; // Now, ships fire new projectiles, which includes launching fighters. If an // anti-missile system is ready to fire, it does not actually fire unless a // missile is detected in range during collision detection, below. vector<Ship *> hasAntiMissile; for(shared_ptr<Ship> &ship : ships) if(ship->GetSystem() == player.GetSystem()) { strength[ship->GetGovernment()] += ship->Cost(); // Note: if a ship "fires" a fighter, that fighter was already in // existence and under the control of the same AI as the ship, but // its system was null to mark that it was not active. ship->Launch(ships); if(ship->Fire(projectiles, effects)) hasAntiMissile.push_back(ship.get()); int scan = ship->Scan(); if(scan) { shared_ptr<Ship> target = ship->GetTargetShip(); if(target && target->IsTargetable()) eventQueue.emplace_back(ship, target, scan); } // This is a good opportunity to draw all the ships in system. if(ship->GetSprite().IsEmpty()) continue; Point position = ship->Position() - center; if(ship->IsThrusting()) { for(const Point &point : ship->EnginePoints()) { Point pos = ship->Facing().Rotate(point) * .5 * ship->Zoom() + position; for(const auto &it : ship->Attributes().FlareSprites()) for(int i = 0; i < it.second; ++i) { if(ship->Cloaking()) { draw[calcTickTock].Add( it.first.GetSprite(), pos, ship->Unit(), ship->Velocity() - centerVelocity, ship->Cloaking()); } else { draw[calcTickTock].Add( it.first, pos, ship->Unit(), ship->Velocity() - centerVelocity); } } } if(ship.get() == flagship) { for(const auto &it : ship->Attributes().FlareSounds()) if(it.second > 0) Audio::Play(it.first); } } bool isPlayer = ship->GetGovernment()->IsPlayer(); if(ship->Cloaking()) { if(isPlayer) { Animation animation = ship->GetSprite(); animation.SetSwizzle(7); draw[calcTickTock].Add( animation, position, ship->Unit(), ship->Velocity() - centerVelocity); } draw[calcTickTock].Add( ship->GetSprite().GetSprite(), position, ship->Unit(), ship->Velocity() - centerVelocity, ship->Cloaking(), ship->GetSprite().GetSwizzle()); } else { draw[calcTickTock].Add( ship->GetSprite(), position, ship->Unit(), ship->Velocity() - centerVelocity); } // Do not show cloaked ships on the radar, except the player's ships. if(ship->Cloaking() == 1. && !isPlayer) continue; if(doClick && &*ship != player.Flagship()) { const Mask &mask = ship->GetSprite().GetMask(step); if(mask.WithinRange(clickPoint - position, ship->Facing(), 50.)) { player.Flagship()->SetTargetShip(ship); // If we've found an enemy within the click zone, favor // targeting it rather than any other ship. Otherwise, keep // checking for hits because another ship might be an enemy. if(ship->GetGovernment()->IsEnemy()) doClick = false; } } auto target = ship->GetTargetShip(); radar[calcTickTock].Add( (ship->GetGovernment()->IsPlayer() || ship->GetPersonality().IsEscort()) ? Radar::PLAYER : (ship->IsDisabled() || ship->IsOverheated()) ? Radar::INACTIVE : !ship->GetGovernment()->IsEnemy() ? Radar::FRIENDLY : (target && target->GetGovernment()->IsPlayer()) ? Radar::HOSTILE : Radar::UNFRIENDLY, position, sqrt(ship->GetSprite().Width() + ship->GetSprite().Height()) * .1 + .5); } // Collision detection: for(Projectile &projectile : projectiles) { // The asteroids can collide with projectiles, the same as any other // object. If the asteroid turns out to be closer than the ship, it // shields the ship (unless the projectile has blast radius). Point hitVelocity; double closestHit = 0.; shared_ptr<Ship> hit; const Government *gov = projectile.GetGovernment(); // If this "projectile" is a ship explosion, it always explodes. if(gov) { closestHit = asteroids.Collide(projectile, step, &hitVelocity); // Projectiles can only collide with ships that are in the current // system and are not landing, and that are hostile to this projectile. for(shared_ptr<Ship> &ship : ships) if(ship->GetSystem() == player.GetSystem() && !ship->IsLanding() && ship->Cloaking() < 1.) { if(ship.get() != projectile.Target() && !gov->IsEnemy(ship->GetGovernment())) continue; // This returns a value of 0 if the projectile has a trigger // radius and the ship is within it. double range = projectile.CheckCollision(*ship, step); if(range < closestHit) { closestHit = range; hit = ship; hitVelocity = ship->Velocity(); } } } if(closestHit < 1.) { // Create the explosion the given distance along the projectile's // motion path for this step. projectile.Explode(effects, closestHit, hitVelocity); // If this projectile has a blast radius, find all ships within its // radius. Otherwise, only one is damaged. if(projectile.HasBlastRadius()) { // Even friendly ships can be hit by the blast. for(shared_ptr<Ship> &ship : ships) if(ship->GetSystem() == player.GetSystem() && ship->Zoom() == 1.) if(projectile.InBlastRadius(*ship, step)) { int eventType = ship->TakeDamage(projectile, ship != hit); if(eventType) eventQueue.emplace_back( projectile.GetGovernment(), ship, eventType); } } else if(hit) { int eventType = hit->TakeDamage(projectile); if(eventType) eventQueue.emplace_back( projectile.GetGovernment(), hit, eventType); } if(hit) DoGrudge(hit, projectile.GetGovernment()); } else if(projectile.MissileStrength()) { radar[calcTickTock].Add( Radar::SPECIAL, projectile.Position() - center, 1.); // If the projectile did not hit anything, give the anti-missile // systems a chance to shoot it down. for(Ship *ship : hasAntiMissile) if(ship == projectile.Target() || gov->IsEnemy(ship->GetGovernment()) || ship->GetGovernment()->IsEnemy(gov)) if(ship->FireAntiMissile(projectile, effects)) { projectile.Kill(); break; } } else if(projectile.HasBlastRadius()) radar[calcTickTock].Add( Radar::SPECIAL, projectile.Position() - center, 1.8); // Now, we can draw the projectile. The motion blur should be reduced // depending on how much motion blur is in the sprite itself: double innateVelocity = 2. * projectile.GetWeapon().Velocity(); Point relativeVelocity = projectile.Velocity() - centerVelocity - projectile.Unit() * innateVelocity; draw[calcTickTock].Add( projectile.GetSprite(), projectile.Position() - center + .5 * projectile.Velocity(), projectile.Unit(), relativeVelocity, closestHit); } // Finally, draw all the effects, and then move them (because their motion // is not dependent on anything else, and this way we do all the work on // them in a single place. for(auto it = effects.begin(); it != effects.end(); ) { draw[calcTickTock].Add( it->GetSprite(), it->Position() - center, it->Unit()); if(!it->Move()) it = effects.erase(it); else ++it; } // Add incoming ships. for(const System::FleetProbability &fleet : player.GetSystem()->Fleets()) if(!Random::Int(fleet.Period())) { const Government *gov = fleet.Get()->GetGovernment(); if(!gov) continue; int64_t enemyStrength = 0; for(const auto &it : strength) if(gov->IsEnemy(it.first)) enemyStrength += it.second; if(enemyStrength && strength[gov] > 2 * enemyStrength) continue; fleet.Get()->Enter(*player.GetSystem(), ships); } if(!Random::Int(36000)) { // Loop through all persons once to see if there are any who can enter // this system. int sum = 0; for(const auto &it : GameData::Persons()) sum += it.second.Frequency(player.GetSystem()); if(sum) { // Adjustment factor: special persons will appear once every ten // minutes, but much less frequently if the game only specifies a // few of them. This way, they will become more common as I add // more, without needing to change the 10-minute constant above. sum = Random::Int(sum + 1000); for(const auto &it : GameData::Persons()) { const Person &person = it.second; sum -= person.Frequency(player.GetSystem()); if(sum < 0) { shared_ptr<Ship> ship = person.GetShip(); ship->Recharge(true); ship->SetName(it.first); ship->SetGovernment(person.GetGovernment()); ship->SetPersonality(person.GetPersonality()); ship->SetHail(person.GetHail()); Fleet::Enter(*player.GetSystem(), *ship); ships.push_front(ship); break; } } } } // Occasionally have some ship hail you. if(!Random::Int(600) && !ships.empty()) { shared_ptr<Ship> source; unsigned i = Random::Int(ships.size()); for(const shared_ptr<Ship> &it : ships) if(!i--) { source = it; break; } if(source->GetGovernment() && !source->GetGovernment()->IsPlayer() && !source->IsDisabled()) { string message = source->GetHail(); if(!message.empty() && source->GetSystem() == player.GetSystem()) Messages::Add(source->GetGovernment()->GetName() + " ship \"" + source->Name() + "\": " + message); } } // A mouse click should only be active for a single step. doClick = false; // Keep track of how much of the CPU time we are using. loadSum += loadTimer.Time(); if(++loadCount == 60) { load = loadSum; loadSum = 0.; loadCount = 0; } }
void Engine::CalculateStep() { FrameTimer loadTimer; // Clear the list of objects to draw. draw[calcTickTock].Clear(step); radar[calcTickTock].Clear(); if(!player.GetSystem()) return; // Now, all the ships must decide what they are doing next. ai.Step(ships, player); const Ship *flagship = player.Flagship(); bool wasHyperspacing = (flagship && flagship->IsEnteringHyperspace()); // Now, move all the ships. We must finish moving all of them before any of // them fire, or their turrets will be targeting where a given ship was // instead of where it is now. This is also where ships get deleted, and // where they may create explosions if they are dying. for(auto it = ships.begin(); it != ships.end(); ) { int hyperspaceType = (*it)->HyperspaceType(); bool wasHere = (flagship && (*it)->GetSystem() == flagship->GetSystem()); bool wasHyperspacing = (*it)->IsHyperspacing(); // Give the ship the list of effects so that it can draw explosions, // ion sparks, jump drive flashes, etc. if(!(*it)->Move(effects, flotsam)) { // If Move() returns false, it means the ship should be removed from // play. That may be because it was destroyed, because it is an // ordinary ship that has been out of system for long enough to be // "forgotten," or because it is a fighter that just docked with its // mothership. Report it destroyed if that's really what happened: if((*it)->IsDestroyed()) eventQueue.emplace_back(nullptr, *it, ShipEvent::DESTROY); it = ships.erase(it); } else { if(&**it != flagship) { // Did this ship just begin hyperspacing? if(wasHere && !wasHyperspacing && (*it)->IsHyperspacing()) Audio::Play( Audio::Get(hyperspaceType >= 200 ? "jump out" : "hyperdrive out"), (*it)->Position()); // Did this ship just jump into the player's system? if(!wasHere && flagship && (*it)->GetSystem() == flagship->GetSystem()) Audio::Play( Audio::Get(hyperspaceType >= 200 ? "jump in" : "hyperdrive in"), (*it)->Position()); } // Boarding: bool autoPlunder = !(*it)->GetGovernment()->IsPlayer(); shared_ptr<Ship> victim = (*it)->Board(autoPlunder); if(victim) eventQueue.emplace_back(*it, victim, (*it)->GetGovernment()->IsEnemy(victim->GetGovernment()) ? ShipEvent::BOARD : ShipEvent::ASSIST); ++it; } } if(!wasHyperspacing && flagship && flagship->IsEnteringHyperspace()) Audio::Play(Audio::Get(flagship->HyperspaceType() >= 200 ? "jump drive" : "hyperdrive")); if(flagship && player.GetSystem() != flagship->GetSystem()) { // Wormhole travel: if(!wasHyperspacing) for(const auto &it : player.GetSystem()->Objects()) if(it.GetPlanet() && it.GetPlanet()->IsWormhole() && it.GetPlanet()->WormholeDestination(player.GetSystem()) == flagship->GetSystem()) player.Visit(it.GetPlanet()); doFlash = Preferences::Has("Show hyperspace flash"); player.SetSystem(flagship->GetSystem()); EnterSystem(); } // Draw the planets. Point newCenter = center; Point newCenterVelocity; if(flagship) { newCenter = flagship->Position(); newCenterVelocity = flagship->Velocity(); } else doClick = false; draw[calcTickTock].SetCenter(newCenter, newCenterVelocity); radar[calcTickTock].SetCenter(newCenter); for(const StellarObject &object : player.GetSystem()->Objects()) if(object.HasSprite()) { // Don't apply motion blur to very large planets and stars. if(object.Width() >= 280.) draw[calcTickTock].AddUnblurred(object); else draw[calcTickTock].Add(object); double r = max(2., object.Radius() * .03 + .5); radar[calcTickTock].Add(RadarType(object), object.Position(), r, r - 1.); if(object.GetPlanet()) object.GetPlanet()->DeployDefense(ships); Point position = object.Position() - newCenter; if(doClick && object.GetPlanet() && (clickPoint - position).Length() < object.Radius()) { if(&object == player.Flagship()->GetTargetPlanet()) { if(!object.GetPlanet()->CanLand()) Messages::Add("The authorities on " + object.GetPlanet()->Name() + " refuse to let you land."); else { clickCommands |= Command::LAND; Messages::Add("Landing on " + object.GetPlanet()->Name() + "."); } } else player.Flagship()->SetTargetPlanet(&object); } } // Add all neighboring systems to the radar. const System *targetSystem = flagship ? flagship->GetTargetSystem() : nullptr; const vector<const System *> &links = (flagship && flagship->Attributes().Get("jump drive")) ? player.GetSystem()->Neighbors() : player.GetSystem()->Links(); for(const System *system : links) radar[calcTickTock].AddPointer( (system == targetSystem) ? Radar::SPECIAL : Radar::INACTIVE, system->Position() - player.GetSystem()->Position()); // Now that the planets have been drawn, we can draw the asteroids on top // of them. This could be done later, as long as it is done before the // collision detection. asteroids.Step(); asteroids.Draw(draw[calcTickTock], newCenter); // Move existing projectiles. Do this before ships fire, which will create // new projectiles, since those should just stay where they are created for // this turn. This is also where projectiles get deleted, which may also // result in a "die" effect or a sub-munition being created. We could not // move the projectiles before this because some of them are homing and need // to know the current positions of the ships. list<Projectile> newProjectiles; for(auto it = projectiles.begin(); it != projectiles.end(); ) { if(!it->Move(effects)) { it->MakeSubmunitions(newProjectiles); it = projectiles.erase(it); } else ++it; } projectiles.splice(projectiles.end(), newProjectiles); // Move the flotsam, which should be drawn underneath the ships. for(auto it = flotsam.begin(); it != flotsam.end(); ) { if(!it->Move(effects)) { it = flotsam.erase(it); continue; } Ship *collector = nullptr; for(const shared_ptr<Ship> &ship : ships) { if(ship->GetSystem() != player.GetSystem() || ship->CannotAct()) continue; if(ship.get() == it->Source() || ship->Cargo().Free() < it->UnitSize()) continue; const Mask &mask = ship->GetMask(step); if(mask.Contains(it->Position() - ship->Position(), ship->Facing())) { collector = ship.get(); break; } } if(collector) { string name; if(collector->IsYours()) { if(collector->GetParent()) name = "Your ship \"" + collector->Name() + "\" picked up "; else name = "You picked up "; } string commodity; int amount = 0; if(it->OutfitType()) { amount = collector->Cargo().Add(it->OutfitType(), it->Count()); if(!name.empty()) { if(it->OutfitType()->Get("installable") < 0.) commodity = it->OutfitType()->Name(); else Messages::Add(name + Format::Number(amount) + " " + it->OutfitType()->Name() + (amount == 1 ? "." : "s.")); } } else { amount = collector->Cargo().Add(it->CommodityType(), it->Count()); if(!name.empty()) commodity = it->CommodityType(); } if(!commodity.empty()) Messages::Add(name + (amount == 1 ? "a ton" : Format::Number(amount) + " tons") + " of " + Format::LowerCase(commodity) + "."); it = flotsam.erase(it); continue; } // Draw this flotsam. draw[calcTickTock].Add(*it); ++it; } // Keep track of the relative strength of each government in this system. Do // not add more ships to make a winning team even stronger. This is mostly // to avoid having the player get mobbed by pirates, say, if they hang out // in one system for too long. map<const Government *, int64_t> strength; // Now, ships fire new projectiles, which includes launching fighters. If an // anti-missile system is ready to fire, it does not actually fire unless a // missile is detected in range during collision detection, below. vector<Ship *> hasAntiMissile; double clickRange = 50.; const Ship *previousTarget = nullptr; const Ship *clickTarget = nullptr; if(player.Flagship() && player.Flagship()->GetTargetShip()) previousTarget = &*player.Flagship()->GetTargetShip(); bool showFlagship = false; bool hasHostiles = false; for(shared_ptr<Ship> &ship : ships) if(ship->GetSystem() == player.GetSystem()) { strength[ship->GetGovernment()] += ship->Cost(); // Note: if a ship "fires" a fighter, that fighter was already in // existence and under the control of the same AI as the ship, but // its system was null to mark that it was not active. ship->Launch(ships); if(ship->Fire(projectiles, effects)) hasAntiMissile.push_back(ship.get()); int scan = ship->Scan(); if(scan) { shared_ptr<Ship> target = ship->GetTargetShip(); if(target && target->IsTargetable()) eventQueue.emplace_back(ship, target, scan); } // This is a good opportunity to draw all the ships in system. if(!ship->HasSprite()) continue; // Draw the flagship separately, on top of everything else. if(ship.get() != flagship) { AddSprites(*ship); if(ship->IsThrusting()) { for(const auto &it : ship->Attributes().FlareSounds()) if(it.second > 0) Audio::Play(it.first, ship->Position()); } } else showFlagship = true; // Do not show cloaked ships on the radar, except the player's ships. bool isPlayer = ship->GetGovernment()->IsPlayer(); if(ship->Cloaking() == 1. && !isPlayer) continue; if(doClick && &*ship != player.Flagship() && ship->IsTargetable()) { Point position = ship->Position() - newCenter; const Mask &mask = ship->GetMask(step); double range = mask.Range(clickPoint - position, ship->Facing()); if(range <= clickRange) { clickRange = range; clickTarget = ship.get(); player.Flagship()->SetTargetShip(ship); // If we've found an enemy within the click zone, favor // targeting it rather than any other ship. Otherwise, keep // checking for hits because another ship might be an enemy. if(!range && ship->GetGovernment()->IsEnemy()) doClick = false; } } double size = sqrt(ship->Width() + ship->Height()) * .14 + .5; bool isYourTarget = (flagship && ship == flagship->GetTargetShip()); int type = RadarType(*ship); hasHostiles |= (type == Radar::HOSTILE); radar[calcTickTock].Add(isYourTarget ? Radar::SPECIAL : type, ship->Position(), size); } if(flagship && showFlagship) { AddSprites(*flagship); if(flagship->IsThrusting()) { for(const auto &it : flagship->Attributes().FlareSounds()) if(it.second > 0) Audio::Play(it.first); } } if(clickTarget && clickTarget == previousTarget) clickCommands |= Command::BOARD; if(alarmTime) --alarmTime; else if(hasHostiles && !hadHostiles) { if(Preferences::Has("Warning siren")) Audio::Play(Audio::Get("alarm")); alarmTime = 180; hadHostiles = true; } else if(!hasHostiles) hadHostiles = false; // Collision detection: if(grudgeTime) --grudgeTime; for(Projectile &projectile : projectiles) { // The asteroids can collide with projectiles, the same as any other // object. If the asteroid turns out to be closer than the ship, it // shields the ship (unless the projectile has a blast radius). Point hitVelocity; double closestHit = 0.; shared_ptr<Ship> hit; const Government *gov = projectile.GetGovernment(); // If this "projectile" is a ship explosion, it always explodes. if(gov) { closestHit = asteroids.Collide(projectile, step, &hitVelocity); // Projectiles can only collide with ships that are in the current // system and are not landing, and that are hostile to this projectile. for(shared_ptr<Ship> &ship : ships) if(ship->GetSystem() == player.GetSystem() && !ship->IsLanding() && ship->Cloaking() < 1.) { if(ship.get() != projectile.Target() && !gov->IsEnemy(ship->GetGovernment())) continue; // This returns a value of 0 if the projectile has a trigger // radius and the ship is within it. double range = projectile.CheckCollision(*ship, step); if(range < closestHit) { closestHit = range; hit = ship; hitVelocity = ship->Velocity(); } } } if(closestHit < 1.) { // Create the explosion the given distance along the projectile's // motion path for this step. projectile.Explode(effects, closestHit, hitVelocity); // If this projectile has a blast radius, find all ships within its // radius. Otherwise, only one is damaged. if(projectile.HasBlastRadius()) { // Even friendly ships can be hit by the blast. for(shared_ptr<Ship> &ship : ships) if(ship->GetSystem() == player.GetSystem() && ship->Zoom() == 1.) if(projectile.InBlastRadius(*ship, step, closestHit)) { int eventType = ship->TakeDamage(projectile, ship != hit); if(eventType) eventQueue.emplace_back( projectile.GetGovernment(), ship, eventType); } } else if(hit) { int eventType = hit->TakeDamage(projectile); if(eventType) eventQueue.emplace_back( projectile.GetGovernment(), hit, eventType); } if(hit) DoGrudge(hit, projectile.GetGovernment()); } else if(projectile.MissileStrength()) { bool isEnemy = projectile.GetGovernment() && projectile.GetGovernment()->IsEnemy(); radar[calcTickTock].Add( isEnemy ? Radar::SPECIAL : Radar::INACTIVE, projectile.Position(), 1.); // If the projectile did not hit anything, give the anti-missile // systems a chance to shoot it down. for(Ship *ship : hasAntiMissile) if(ship == projectile.Target() || gov->IsEnemy(ship->GetGovernment()) || ship->GetGovernment()->IsEnemy(gov)) if(ship->FireAntiMissile(projectile, effects)) { projectile.Kill(); break; } } else if(projectile.HasBlastRadius()) radar[calcTickTock].Add(Radar::SPECIAL, projectile.Position(), 1.8); // Now, we can draw the projectile. The motion blur should be reduced // depending on how much motion blur is in the sprite itself: double innateVelocity = 2. * projectile.GetWeapon().Velocity(); Point relativeVelocity = projectile.Velocity() - projectile.Unit() * innateVelocity; draw[calcTickTock].AddProjectile(projectile, relativeVelocity, closestHit); } // Finally, draw all the effects, and then move them (because their motion // is not dependent on anything else, and this way we do all the work on // them in a single place. for(auto it = effects.begin(); it != effects.end(); ) { draw[calcTickTock].AddUnblurred(*it); if(!it->Move()) it = effects.erase(it); else ++it; } // Add incoming ships. for(const System::FleetProbability &fleet : player.GetSystem()->Fleets()) if(!Random::Int(fleet.Period())) { const Government *gov = fleet.Get()->GetGovernment(); if(!gov) continue; int64_t enemyStrength = 0; for(const auto &it : strength) if(gov->IsEnemy(it.first)) enemyStrength += it.second; if(enemyStrength && strength[gov] > 2 * enemyStrength) continue; fleet.Get()->Enter(*player.GetSystem(), ships); } if(!Random::Int(36000) && !player.GetSystem()->Links().empty()) { // Loop through all persons once to see if there are any who can enter // this system. int sum = 0; for(const auto &it : GameData::Persons()) sum += it.second.Frequency(player.GetSystem()); if(sum) { // Adjustment factor: special persons will appear once every ten // minutes, but much less frequently if the game only specifies a // few of them. This way, they will become more common as I add // more, without needing to change the 10-minute constant above. sum = Random::Int(sum + 1000); for(const auto &it : GameData::Persons()) { const Person &person = it.second; sum -= person.Frequency(player.GetSystem()); if(sum < 0) { shared_ptr<Ship> ship = person.GetShip(); ship->Recharge(); ship->SetName(it.first); ship->SetGovernment(person.GetGovernment()); ship->SetPersonality(person.GetPersonality()); ship->SetHail(person.GetHail()); Fleet::Enter(*player.GetSystem(), *ship); ships.push_front(ship); break; } } } } // Occasionally have some ship hail you. if(!Random::Int(600) && !ships.empty()) { shared_ptr<Ship> source; unsigned i = Random::Int(ships.size()); for(const shared_ptr<Ship> &it : ships) if(!i--) { source = it; break; } if(source->GetGovernment() && !source->GetGovernment()->IsPlayer() && !source->IsDisabled() && source->Crew()) { string message = source->GetHail(); if(!message.empty() && source->GetSystem() == player.GetSystem()) Messages::Add(source->GetGovernment()->GetName() + " ship \"" + source->Name() + "\": " + message); } } // A mouse click should only be active for a single step. doClick = false; // Keep track of how much of the CPU time we are using. loadSum += loadTimer.Time(); if(++loadCount == 60) { load = loadSum; loadSum = 0.; loadCount = 0; } }