void MapPanel::DrawMissions() const { // Draw a pointer for each active or available mission. map<const System *, Angle> angle; Color black(0., 1.); Color white(1., 1.); const Set<Color> &colors = GameData::Colors(); const Color &availableColor = *colors.Get("available job"); const Color &unavailableColor = *colors.Get("unavailable job"); const Color ¤tColor = *colors.Get("active mission"); const Color &blockedColor = *colors.Get("blocked mission"); const Color &waypointColor = *colors.Get("waypoint"); for(const Mission &mission : player.AvailableJobs()) { const System *system = mission.Destination()->GetSystem(); Angle a = (angle[system] += Angle(30.)); Point pos = system->Position() + center; PointerShader::Draw(pos, a.Unit(), 14., 19., -4., black); PointerShader::Draw(pos, a.Unit(), 8., 15., -6., mission.HasSpace(player) ? availableColor : unavailableColor); } ++step; for(const Mission &mission : player.Missions()) { if(!mission.IsVisible()) continue; const System *system = mission.Destination()->GetSystem(); Angle a = (angle[system] += Angle(30.)); bool blink = false; if(mission.HasDeadline()) { int days = min(5, mission.Deadline() - player.GetDate()) + 1; if(days > 0) blink = (step % (10 * days) > 5 * days); } Point pos = system->Position() + center; PointerShader::Draw(pos, a.Unit(), 14., 19., -4., black); if(!blink) PointerShader::Draw(pos, a.Unit(), 8., 15., -6., IsSatisfied(mission) ? currentColor : blockedColor); for(const System *waypoint : mission.Waypoints()) { Angle a = (angle[waypoint] += Angle(30.)); Point pos = waypoint->Position() + center; PointerShader::Draw(pos, a.Unit(), 14., 19., -4., black); PointerShader::Draw(pos, a.Unit(), 8., 15., -6., waypointColor); } } if(specialSystem) { Angle a = (angle[specialSystem] += Angle(30.)); Point pos = specialSystem->Position() + center; PointerShader::Draw(pos, a.Unit(), 20., 27., -4., black); PointerShader::Draw(pos, a.Unit(), 11.5, 21.5, -6., white); } }
double AI::TurnBackward(const Ship &ship) { Angle angle = ship.Facing(); bool left = ship.Velocity().Cross(angle.Unit()) > 0.; double turn = left - !left; // Check if the ship will still be pointing to the same side of the target // angle if it turns by this amount. angle += ship.TurnRate() * turn; bool stillLeft = ship.Velocity().Cross(angle.Unit()) > 0.; if(left == stillLeft) return turn; // If we're within one step of the correct direction, stop turning. return 0.; }
// Do the randomization to make a ship enter or be in the given system. void Fleet::Enter(const System &system, Ship &ship) { if(!system.Links().size()) { Place(system, ship); return; } const System *source = system.Links()[Random::Int(system.Links().size())]; Angle angle = Angle::Random(); Point pos = angle.Unit() * Random::Real() * 1000.; ship.Place(pos, angle.Unit(), angle); ship.SetSystem(source); ship.SetTargetSystem(&system); }
void Fleet::Place(const System &system, Ship &ship) { // Count the inhabited planets in this system. int planets = 0; for(const StellarObject &object : system.Objects()) if(object.GetPlanet() && object.GetPlanet()->HasSpaceport()) ++planets; // Determine where the fleet is going to or coming from. Point center; if(planets) { int index = Random::Int(planets); for(const StellarObject &object : system.Objects()) if(object.GetPlanet() && object.GetPlanet()->HasSpaceport()) if(!index--) center = object.Position(); } // Move out a random distance from that object, facing toward it or away. center += Angle::Random().Unit() * (Random::Real() * 2. - 1.); Point pos = center + Angle::Random().Unit() * Random::Real() * 400.; double velocity = Random::Real() * ship.MaxVelocity(); ship.SetSystem(&system); Angle angle = Angle::Random(); ship.Place(pos, velocity * angle.Unit(), angle); }
void MapPanel::DrawPointer(const System *system, Angle &angle, const Color &color) const { static const Color black(0., 1.); angle += Angle(30.); Point pos = Zoom() * (system->Position() + center); PointerShader::Draw(pos, angle.Unit(), 14., 19., -4., black); PointerShader::Draw(pos, angle.Unit(), 8., 15., -6., color); }
void Fleet::Place(const System &system, Ship &ship) { // Move out a random distance from that object, facing toward it or away. Point pos = ChooseCenter(system) + Angle::Random().Unit() * Random::Real() * 400.; double velocity = Random::Real() * ship.MaxVelocity(); ship.SetSystem(&system); Angle angle = Angle::Random(); ship.Place(pos, velocity * angle.Unit(), angle); }
// Fire this weapon. If it is a turret, it automatically points toward // the given ship's target. If the weapon requires ammunition, it will // be subtracted from the given ship. void Armament::Weapon::Fire(Ship &ship, list<Projectile> &projectiles, list<Effect> &effects) { // Since this is only called internally by Armament (no one else has non- // const access), assume Armament checked that this is a valid call. Angle aim = ship.Facing(); // Get projectiles to start at the right position. They are drawn at an // offset of (.5 * velocity) and that velocity includes the velocity of the // ship that fired them. Point start = ship.Position() + aim.Rotate(point) - .5 * ship.Velocity(); shared_ptr<const Ship> target = ship.GetTargetShip(); // If you are boarding your target, do not fire on it. if(ship.IsBoarding() || ship.Commands().Has(Command::BOARD)) target.reset(); if(!isTurret || !target || target->GetSystem() != ship.GetSystem()) aim += angle; else { Point p = target->Position() - start + ship.GetPersonality().Confusion(); Point v = target->Velocity() - ship.Velocity(); double steps = RendezvousTime(p, v, outfit->Velocity()); // Special case: RendezvousTime() may return NaN. But in that case, this // comparison will return false. if(!(steps < outfit->TotalLifetime())) steps = outfit->TotalLifetime(); p += steps * v; aim = Angle(TO_DEG * atan2(p.X(), -p.Y())); } projectiles.emplace_back(ship, start, aim, outfit); if(outfit->WeaponSound()) Audio::Play(outfit->WeaponSound(), start); double force = outfit->FiringForce(); if(force) ship.ApplyForce(aim.Unit() * -force); for(const auto &eit : outfit->FireEffects()) for(int i = 0; i < eit.second; ++i) { effects.push_back(*eit.first); effects.back().Place(start, ship.Velocity(), aim); } Fire(ship); }
// Check if this ship is currently able to enter hyperspace to it target. int Ship::CheckHyperspace() const { if(commands.Has(Command::WAIT)) return 0; // Find out where we're going and how we're getting there, const System *destination = GetTargetSystem(); int type = HyperspaceType(); // You can't jump to a system with no link to it. if(!type) return 0; if(fuel < type) return 0; Point direction = destination->Position() - currentSystem->Position(); // The ship can only enter hyperspace if it is traveling slowly enough // and pointed in the right direction. if(type == 150) { double deviation = fabs(direction.Unit().Cross(velocity)); if(deviation > attributes.Get("scram drive")) return 0; } else if(velocity.Length() > attributes.Get("jump speed")) return 0; if(type != 200) { // Figure out if we're within one turn step of facing this system. bool left = direction.Cross(angle.Unit()) < 0.; Angle turned = angle + TurnRate() * (left - !left); bool stillLeft = direction.Cross(turned.Unit()) < 0.; if(left == stillLeft) return 0; } return type; }
// Place a fleet in the given system, already "in action." void Fleet::Place(const System &system, list<shared_ptr<Ship>> &ships, bool carried) const { if(!total || !government || variants.empty()) return; // Pick a fleet variant to instantiate. const Variant &variant = ChooseVariant(); if(variant.ships.empty()) return; // Determine where the fleet is going to or coming from. Point center = ChooseCenter(system); // Place all the ships in the chosen fleet variant. shared_ptr<Ship> flagship; vector<shared_ptr<Ship>> placed = Instantiate(variant); for(shared_ptr<Ship> &ship : placed) { // If this is a fighter and someone can carry it, no need to position it. if(carried && PlaceFighter(ship, placed)) continue; Angle angle = Angle::Random(); Point pos = center + Angle::Random().Unit() * Random::Real() * 400.; double velocity = Random::Real() * ship->MaxVelocity(); ships.push_front(ship); ship->SetSystem(&system); ship->Place(pos, velocity * angle.Unit(), angle); if(flagship) ship->SetParent(flagship); else flagship = ship; SetCargo(&*ship); } }
void Fleet::Enter(const System &system, list<shared_ptr<Ship>> &ships, const Planet *planet) const { if(!total || !government) return; // Pick a random variant based on the weights. unsigned index = 0; for(int choice = Random::Int(total); choice >= variants[index].weight; ++index) choice -= variants[index].weight; if(variants[index].ships.empty()) return; bool isEnemy = system.GetGovernment()->IsEnemy(government); bool hasJump = variants[index].ships.front()->Attributes().Get("jump drive"); const vector<const System *> &linkVector = hasJump ? system.Neighbors() : system.Links(); int links = linkVector.size(); // Count the inhabited planets in this system. int planets = 0; if(!isEnemy && !personality.IsSurveillance()) for(const StellarObject &object : system.Objects()) if(object.GetPlanet() && object.GetPlanet()->HasSpaceport()) ++planets; if(!(links + planets)) return; int choice = Random::Int(links + planets); const System *source = &system; const System *target = &system; Point position; unsigned radius = 0; if(planet) { for(const StellarObject &object : system.Objects()) if(object.GetPlanet() == planet) { position = object.Position(); radius = max(0, static_cast<int>(object.Radius())); break; } } else if(choice >= links) { choice -= links; planets = 0; for(const StellarObject &object : system.Objects()) if(object.GetPlanet() && object.GetPlanet()->HasSpaceport()) { if(planets++ != choice) continue; position = object.Position(); planet = object.GetPlanet(); radius = max(0, static_cast<int>(object.Radius())); break; } if(links) target = linkVector[Random::Int(links)]; } else { radius = 1000; source = linkVector[choice]; } vector<shared_ptr<Ship>> placed; for(const Ship *ship : variants[index].ships) { if(ship->CanBeCarried()) { shared_ptr<Ship> fighter(new Ship(*ship)); fighter->SetGovernment(government); fighter->SetName((fighterNames ? fighterNames : names)->Get()); fighter->SetPersonality(personality); for(const shared_ptr<Ship> &parent : placed) if(parent->AddFighter(fighter)) break; continue; } Angle angle = Angle::Random(360.); Point pos = position + angle.Unit() * (Random::Int(radius + 1)); ships.push_front(shared_ptr<Ship>(new Ship(*ship))); ships.front()->SetSystem(source); ships.front()->SetPlanet(planet); if(source == &system) ships.front()->Place(pos, angle.Unit(), angle); else { Point offset = system.Position() - source->Position(); angle = TO_DEG * atan2(offset.X(), -offset.Y()); ships.front()->Place(pos, Point(), angle); } ships.front()->SetTargetSystem(target); ships.front()->SetGovernment(government); ships.front()->SetName(names->Get()); ships.front()->SetPersonality(personality); if(!placed.empty()) ships.front()->SetParent(placed.front()); placed.push_back(ships.front()); SetCargo(&*ships.front()); } }
// Move this ship. A ship may create effects as it moves, in particular if // it is in the process of blowing up. If this returns false, the ship // should be deleted. bool Ship::Move(list<Effect> &effects) { // Check if this ship has been in a different system from the player for so // long that it should be "forgotten." Also eliminate ships that have no // system set because they just entered a fighter bay. forget += !isInSystem; if((!isSpecial && forget >= 1000) || !currentSystem) return false; isInSystem = false; if(!fuel || !(attributes.Get("hyperdrive") || attributes.Get("jump drive"))) hyperspaceSystem = nullptr; // Handle ionization effects. if(ionization) { ionization *= .99; const Effect *effect = GameData::Effects().Get("ion spark"); double ion = ionization * .1; while(!forget) { ion -= Random::Real(); if(ion <= 0.) break; Point point((Random::Real() - .5) * .5 * sprite.Width(), (Random::Real() - .5) * .5 * sprite.Height()); if(sprite.GetMask(0).Contains(point, Angle())) { effects.push_back(*effect); effects.back().Place(angle.Rotate(point) + position, velocity, angle); } } } // Jettisoned cargo effects. static const int JETTISON_BOX = 5; if(jettisoned >= JETTISON_BOX) { jettisoned -= JETTISON_BOX; effects.push_back(*GameData::Effects().Get("box")); effects.back().Place(position, velocity, angle); } // When ships recharge, what actually happens is that they can exceed their // maximum capacity for the rest of the turn, but must be clamped to the // maximum here before they gain more. This is so that, for example, a ship // with no batteries but a good generator can still move. energy = min(energy, attributes.Get("energy capacity")); heat *= heatDissipation; if(heat > Mass() * 100.) isOverheated = true; else if(heat < Mass() * 90.) isOverheated = false; double maxShields = attributes.Get("shields"); shields = min(shields, maxShields); double maxHull = attributes.Get("hull"); hull = min(hull, maxHull); isDisabled = isOverheated || IsDisabled(); // Update ship supply levels. if(!isDisabled) { // If you have a ramscoop, you recharge enough fuel to make one jump in // a little less than a minute - enough to be an inconvenience without // being totally aggravating. if(attributes.Get("ramscoop")) TransferFuel(-.03 * sqrt(attributes.Get("ramscoop")), nullptr); energy += attributes.Get("energy generation") - ionization; energy = max(0., energy); heat += attributes.Get("heat generation"); heat -= attributes.Get("cooling"); heat = max(0., heat); // Hull repair. double oldHull = hull; hull = min(hull + attributes.Get("hull repair rate"), maxHull); static const double HULL_EXCHANGE_RATE = 1.; energy -= HULL_EXCHANGE_RATE * (hull - oldHull); // Recharge shields, but only up to the max. If there is extra shield // energy, use it to recharge fighters and drones. shields += attributes.Get("shield generation"); static const double SHIELD_EXCHANGE_RATE = 1.; energy -= SHIELD_EXCHANGE_RATE * attributes.Get("shield generation"); double excessShields = max(0., shields - maxShields); shields -= excessShields; for(Bay &bay : fighterBays) { if(!bay.ship) continue; double myGen = bay.ship->Attributes().Get("shield generation"); double myMax = bay.ship->Attributes().Get("shields"); bay.ship->shields = min(myMax, bay.ship->shields + myGen); if(excessShields > 0. && bay.ship->shields < myMax) { double extra = min(myMax - bay.ship->shields, excessShields); bay.ship->shields += extra; excessShields -= extra; } } for(Bay &bay : droneBays) { if(!bay.ship) continue; double myGen = bay.ship->Attributes().Get("shield generation"); double myMax = bay.ship->Attributes().Get("shields"); bay.ship->shields = min(myMax, bay.ship->shields + myGen); if(excessShields > 0. && bay.ship->shields < myMax) { double extra = min(myMax - bay.ship->shields, excessShields); bay.ship->shields += extra; excessShields -= extra; } } // If you do not need the shield generation, apply the extra back to // your energy. On the other hand, if recharging shields drives your // energy negative, undo that part of the recharge. energy += SHIELD_EXCHANGE_RATE * excessShields; if(energy < 0.) { shields += energy / SHIELD_EXCHANGE_RATE; energy = 0.; } } if(IsDestroyed()) { // Once we've created enough little explosions, die. if(explosionCount == explosionTotal || forget) { if(!forget) { const Effect *effect = GameData::Effects().Get("smoke"); double scale = .015 * (sprite.Width() + sprite.Height()) + .5; double radius = .1 * (sprite.Width() + sprite.Height()); int debrisCount = attributes.Get("mass") * .07; for(int i = 0; i < debrisCount; ++i) { effects.push_back(*effect); Angle angle = Angle::Random(); Point effectVelocity = velocity + angle.Unit() * (scale * Random::Real()); Point effectPosition = position + radius * angle.Unit(); effects.back().Place(effectPosition, effectVelocity, angle); } for(unsigned i = 0; i < explosionTotal / 2; ++i) CreateExplosion(effects, true); } energy = 0.; heat = 0.; ionization = 0.; fuel = 0.; return false; } // If the ship is dead, it first creates explosions at an increasing // rate, then disappears in one big explosion. ++explosionRate; if(Random::Int(1024) < explosionRate) CreateExplosion(effects); } else if(hyperspaceSystem || hyperspaceCount) { fuel -= (hyperspaceSystem != nullptr); // Enter hyperspace. int direction = (hyperspaceSystem != nullptr) - (hyperspaceSystem == nullptr); hyperspaceCount += direction; static const int HYPER_C = 100; static const double HYPER_A = 2.; static const double HYPER_D = 1000.; bool hasJumpDrive = (hyperspaceType == 200); // Create the particle effects for the jump drive. This may create 100 // or more particles per ship per turn at the peak of the jump. if(hasJumpDrive && !forget) { int count = hyperspaceCount; count *= sprite.Width() * sprite.Height(); count /= 160000; const Effect *effect = GameData::Effects().Get("jump drive"); while(--count >= 0) { Point point((Random::Real() - .5) * .5 * sprite.Width(), (Random::Real() - .5) * .5 * sprite.Height()); if(sprite.GetMask(0).Contains(point, Angle())) { effects.push_back(*effect); Point vel = velocity + 5. * Angle::Random(360.).Unit(); effects.back().Place(angle.Rotate(point) + position, vel, angle); } } } if(hyperspaceCount == HYPER_C) { currentSystem = hyperspaceSystem; // If the jump fuel is higher than 100, expend extra fuel now. fuel -= hyperspaceType - HYPER_C; hyperspaceSystem = nullptr; SetTargetSystem(nullptr); SetTargetPlanet(nullptr); direction = -1; Point target; for(const StellarObject &object : currentSystem->Objects()) if(object.GetPlanet() && object.GetPlanet()->HasSpaceport()) { target = object.Position(); break; } if(GetDestination()) for(const StellarObject &object : currentSystem->Objects()) if(object.GetPlanet() == GetDestination()) { target = object.Position(); break; } if(hasJumpDrive) { position = target + Angle::Random().Unit() * 300. * (Random::Real() + 1.); return true; } // Have all ships exit hyperspace at the same distance so that // your escorts always stay with you. double distance = (HYPER_C * HYPER_C) * .5 * HYPER_A + HYPER_D; position = (target - distance * angle.Unit()); position += hyperspaceOffset; // Make sure your velocity is in exactly the direction you are // traveling in, so that when you decelerate there will not be a // sudden shift in direction at the end. velocity = velocity.Length() * angle.Unit(); } if(!hasJumpDrive) { velocity += (HYPER_A * direction) * angle.Unit(); if(!hyperspaceSystem) { // Exit hyperspace far enough from the planet to be able to land. // This does not take drag into account, so it is always an over- // estimate of how long it will take to stop. // We start decellerating after rotating about 150 degrees (that // is, about acos(.8) from the proper angle). So: // Stopping distance = .5*a*(v/a)^2 + (150/turn)*v. // Exit distance = HYPER_D + .25 * v^2 = stopping distance. double exitV = MaxVelocity(); double a = (.5 / Acceleration() - .25); double b = 150. / TurnRate(); double discriminant = b * b - 4. * a * -HYPER_D; if(discriminant > 0.) { double altV = (-b + sqrt(discriminant)) / (2. * a); if(altV > 0. && altV < exitV) exitV = altV; } if(velocity.Length() <= exitV) { velocity = angle.Unit() * exitV; hyperspaceCount = 0; } } } position += velocity; if(GetParent() && GetParent()->currentSystem == currentSystem) { hyperspaceOffset = position - GetParent()->position; double length = hyperspaceOffset.Length(); if(length > 1000.) hyperspaceOffset *= 1000. / length; } return true; } else if(landingPlanet || zoom < 1.) { // Special ships do not disappear forever when they land; they // just slowly refuel. if(landingPlanet && zoom) { // Move the ship toward the center of the planet while landing. if(GetTargetPlanet()) position = .97 * position + .03 * GetTargetPlanet()->Position(); zoom -= .02; if(zoom < 0.) { // If this is not a special ship, it ceases to exist when it // lands on a true planet. If this is a wormhole, the ship is // instantly transported. if(landingPlanet->IsWormhole()) { currentSystem = landingPlanet->WormholeDestination(currentSystem); for(const StellarObject &object : currentSystem->Objects()) if(object.GetPlanet() == landingPlanet) position = object.Position(); SetTargetPlanet(nullptr); landingPlanet = nullptr; } else if(!isSpecial || personality.IsFleeing()) return false; zoom = 0.; } } // Only refuel if this planet has a spaceport. else if(fuel == attributes.Get("fuel capacity") || !landingPlanet || !landingPlanet->HasSpaceport()) { zoom = min(1., zoom + .02); landingPlanet = nullptr; } else fuel = min(fuel + 1., attributes.Get("fuel capacity")); // Move the ship at the velocity it had when it began landing, but // scaled based on how small it is now. position += velocity * zoom; return true; } if(commands.Has(Command::LAND) && CanLand()) landingPlanet = GetTargetPlanet()->GetPlanet(); else if(commands.Has(Command::JUMP)) { hyperspaceType = CheckHyperspace(); if(hyperspaceType) hyperspaceSystem = GetTargetSystem(); } double cloakingSpeed = attributes.Get("cloak"); bool canCloak = (zoom == 1. && !isDisabled && !hyperspaceCount && cloakingSpeed && fuel >= attributes.Get("cloaking fuel") && energy >= attributes.Get("cloaking energy")); if(commands.Has(Command::CLOAK) && canCloak) { cloak = min(1., cloak + cloakingSpeed); fuel -= attributes.Get("cloaking fuel"); energy -= attributes.Get("cloaking energy"); } else if(cloakingSpeed) cloak = max(0., cloak - cloakingSpeed); else cloak = 0.; int requiredCrew = RequiredCrew(); if(pilotError) --pilotError; else if(pilotOkay) --pilotOkay; else if(requiredCrew && static_cast<int>(Random::Int(requiredCrew)) >= Crew()) { pilotError = 30; Messages::Add("Your ship is moving erratically because you do not have enough crew to pilot it."); } else pilotOkay = 30; // This ship is not landing or entering hyperspace. So, move it. If it is // disabled, all it can do is slow down to a stop. double mass = Mass(); if(isDisabled) velocity *= 1. - attributes.Get("drag") / mass; else if(!pilotError) { double thrustCommand = commands.Has(Command::FORWARD) - commands.Has(Command::BACK); Point acceleration; if(thrustCommand) { // Check if we are able to apply this thrust. double cost = attributes.Get((thrustCommand > 0.) ? "thrusting energy" : "reverse thrusting energy"); if(energy < cost) thrustCommand = 0.; else { // If a reverse thrust is commanded and the capability does not // exist, ignore it (do not even slow under drag). double thrust = attributes.Get((thrustCommand > 0.) ? "thrust" : "reverse thrust"); if(!thrust) thrustCommand = 0.; else { energy -= cost; heat += attributes.Get((thrustCommand > 0.) ? "thrusting heat" : "reverse thrusting heat"); acceleration += angle.Unit() * (thrustCommand * thrust / mass); } } } bool applyAfterburner = commands.Has(Command::AFTERBURNER) && !CannotAct(); if(applyAfterburner) { double thrust = attributes.Get("afterburner thrust"); double cost = attributes.Get("afterburner fuel"); double energyCost = attributes.Get("afterburner energy"); if(!thrust || fuel < cost || energy < energyCost) applyAfterburner = false; else { heat += attributes.Get("afterburner heat"); fuel -= cost; energy -= energyCost; acceleration += angle.Unit() * thrust / mass; if(!forget) for(const Point &point : enginePoints) { Point pos = angle.Rotate(point) * .5 * Zoom() + position; for(const auto &it : attributes.AfterburnerEffects()) for(int i = 0; i < it.second; ++i) { effects.push_back(*it.first); effects.back().Place(pos + velocity, velocity - 6. * angle.Unit(), angle); } } } } if(acceleration) { Point dragAcceleration = acceleration - velocity * (attributes.Get("drag") / mass); // Make sure dragAcceleration has nonzero length, to avoid divide by zero. if(dragAcceleration) { // What direction will the net acceleration be if this drag is applied? // If the net acceleration will be opposite the thrust, do not apply drag. dragAcceleration *= .5 * (acceleration.Unit().Dot(dragAcceleration.Unit()) + 1.); velocity += dragAcceleration; } } if(commands.Turn()) { // Check if we are able to turn. double cost = attributes.Get("turning energy"); if(energy < cost) commands.SetTurn(0.); else { energy -= cost; heat += attributes.Get("turning heat"); angle += commands.Turn() * TurnRate(); } } } // Boarding: if(isBoarding && (commands.Has(Command::FORWARD | Command::BACK) || commands.Turn())) isBoarding = false; shared_ptr<const Ship> target = (CanBeCarried() ? GetParent() : GetTargetShip()); if(target && !isDisabled) { Point dp = (target->position - position); double distance = dp.Length(); Point dv = (target->velocity - velocity); double speed = dv.Length(); isBoarding |= (distance < 50. && speed < 1. && commands.Has(Command::BOARD)); if(isBoarding && !CanBeCarried()) { if(!target->IsDisabled() && government->IsEnemy(target->government)) isBoarding = false; else if(target->IsDestroyed() || target->IsLanding() || target->IsHyperspacing() || target->GetSystem() != GetSystem()) isBoarding = false; } if(isBoarding && !pilotError) { Angle facing = angle; bool left = target->Unit().Cross(facing.Unit()) < 0.; double turn = left - !left; // Check if the ship will still be pointing to the same side of the target // angle if it turns by this amount. facing += TurnRate() * turn; bool stillLeft = target->Unit().Cross(facing.Unit()) < 0.; if(left != stillLeft) turn = 0.; angle += TurnRate() * turn; velocity += dv.Unit() * .1; position += dp.Unit() * .5; if(distance < 10. && speed < 1. && (CanBeCarried() || !turn)) { isBoarding = false; if(government->IsEnemy(target->government) && target->Attributes().Get("self destruct")) { Messages::Add("The " + target->ModelName() + " \"" + target->Name() + "\" has activated its self-destruct mechanism."); shared_ptr<Ship> victim = targetShip.lock(); victim->hull = -1.; victim->explosionRate = 1024; } else hasBoarded = true; } } } // And finally: move the ship! position += velocity; return true; }
void Fleet::Enter(const System &system, list<shared_ptr<Ship>> &ships, const Planet *planet) const { if(!total || !government || variants.empty()) return; // Pick a fleet variant to instantiate. const Variant &variant = ChooseVariant(); if(variant.ships.empty()) return; // Where this fleet can come from depends on whether it is friendly to any // planets in this system and whether it has jump drives. vector<const System *> linkVector; // Find out what the "best" jump method the fleet has is. Assume that if the // others don't have that jump method, they are being carried as fighters. // That is, content creators should avoid creating fleets with a mix of jump // drives and hyperdrives. int jumpType = 0; for(const Ship *ship : variant.ships) jumpType = max(jumpType, ship->Attributes().Get("jump drive") ? 200 : ship->Attributes().Get("hyperdrive") ? 100 : 0); if(jumpType) { // Don't try to make a fleet "enter" from another system if none of the // ships have jump drives. bool isWelcomeHere = !system.GetGovernment()->IsEnemy(government); for(const System *neighbor : (jumpType == 200 ? system.Neighbors() : system.Links())) { // If this ship is not "welcome" in the current system, prefer to have // it enter from a system that is friendly to it. (This is for realism, // so attack fleets don't come from what ought to be a safe direction.) if(isWelcomeHere || neighbor->GetGovernment()->IsEnemy(government)) linkVector.push_back(neighbor); else linkVector.insert(linkVector.end(), 4, neighbor); } } // Find all the inhabited planets this fleet could take off from. vector<const Planet *> planetVector; if(!personality.IsSurveillance()) for(const StellarObject &object : system.Objects()) if(object.GetPlanet() && object.GetPlanet()->HasSpaceport() && !object.GetPlanet()->GetGovernment()->IsEnemy(government)) planetVector.push_back(object.GetPlanet()); // If there is nowhere for this fleet to come from, don't create it. size_t options = linkVector.size() + planetVector.size(); if(!options) return; // Choose a random planet or star system to come from. size_t choice = Random::Int(options); // Figure out what system the ship is starting in, where it is going, and // what position it should start from in the system. const System *source = &system; const System *target = &system; Point position; double radius = 0.; // If a planet is chosen, also pick a system to travel to after taking off. if(choice >= linkVector.size()) { planet = planetVector[choice - linkVector.size()]; if(!linkVector.empty()) target = linkVector[Random::Int(linkVector.size())]; } // Find the stellar object for the given planet, and place the ships there. if(planet) { for(const StellarObject &object : system.Objects()) if(object.GetPlanet() == planet) { position = object.Position(); radius = object.Radius(); break; } } else if(choice < linkVector.size()) { // We are entering this system via hyperspace, not taking off from a planet. radius = 1000.; source = linkVector[choice]; } // Place all the ships in the chosen fleet variant. shared_ptr<Ship> flagship; vector<shared_ptr<Ship>> placed = Instantiate(variant); for(shared_ptr<Ship> &ship : placed) { // If this is a fighter and someone can carry it, no need to position it. if(PlaceFighter(ship, placed)) continue; Angle angle = Angle::Random(360.); Point pos = position + angle.Unit() * (Random::Real() * radius); ships.push_front(ship); ship->SetSystem(source); ship->SetPlanet(planet); if(source == &system) ship->Place(pos, angle.Unit(), angle); else { // Place the ship stationary and pointed in the right direction. angle = Angle(system.Position() - source->Position()); ship->Place(pos, Point(), angle); } if(target != source) ship->SetTargetSystem(target); if(flagship) ship->SetParent(flagship); else flagship = ship; SetCargo(&*ship); } }
// Place a fleet in the given system, already "in action." void Fleet::Place(const System &system, list<shared_ptr<Ship>> &ships, bool carried) const { if(!total || !government) return; // Pick a random variant based on the weights. unsigned index = 0; for(int choice = Random::Int(total); choice >= variants[index].weight; ++index) choice -= variants[index].weight; // Count the inhabited planets in this system. int planets = 0; for(const StellarObject &object : system.Objects()) if(object.GetPlanet() && object.GetPlanet()->HasSpaceport()) ++planets; // Determine where the fleet is going to or coming from. Point center; if(planets) { int index = Random::Int(planets); for(const StellarObject &object : system.Objects()) if(object.GetPlanet() && object.GetPlanet()->HasSpaceport()) if(!index--) center = object.Position(); } // Move out a random distance from that object, facing toward it or away. Angle angle = Angle::Random(); center += angle.Unit() * (Random::Real() * 2. - 1.); vector<shared_ptr<Ship>> placed; for(const Ship *ship : variants[index].ships) { if(carried && ship->CanBeCarried()) { shared_ptr<Ship> fighter(new Ship(*ship)); fighter->SetGovernment(government); fighter->SetName((fighterNames ? fighterNames : names)->Get()); fighter->SetPersonality(personality); for(const shared_ptr<Ship> &parent : placed) if(parent->AddFighter(fighter)) break; continue; } Angle angle = Angle::Random(); Point pos = center + Angle::Random().Unit() * Random::Real() * 400.; double velocity = Random::Real() * ship->MaxVelocity(); ships.push_front(shared_ptr<Ship>(new Ship(*ship))); ships.front()->SetSystem(&system); ships.front()->Place(pos, velocity * angle.Unit(), angle); ships.front()->SetGovernment(government); ships.front()->SetName(names->Get()); ships.front()->SetPersonality(personality); if(!placed.empty()) ships.front()->SetParent(placed.front()); placed.push_back(ships.front()); SetCargo(&*ships.front()); } }
void Engine::Place() { ships.clear(); EnterSystem(); auto it = ships.end(); // Add the player's flagship and escorts to the list of ships. The TakeOff() // code already took care of loading up fighters and assigning parents. for(const shared_ptr<Ship> &ship : player.Ships()) if(!ship->IsParked() && ship->GetSystem()) { ships.push_back(ship); if(it == ships.end()) --it; } // Add NPCs to the list of ships. Fighters have to be assigned to carriers, // and all but "uninterested" ships should follow the player. shared_ptr<Ship> flagship = player.FlagshipPtr(); for(const Mission &mission : player.Missions()) for(const NPC &npc : mission.NPCs()) { map<Ship *, int> droneCarriers; map<Ship *, int> fighterCarriers; for(const shared_ptr<Ship> &ship : npc.Ships()) { // Skip ships that have been destroyed. if(ship->IsDestroyed() || ship->IsDisabled()) continue; if(ship->BaysFree(false)) droneCarriers[&*ship] = ship->BaysFree(false); if(ship->BaysFree(true)) fighterCarriers[&*ship] = ship->BaysFree(true); // Redo the loading up of fighters. ship->UnloadBays(); } for(const shared_ptr<Ship> &ship : npc.Ships()) { // Skip ships that have been destroyed. if(ship->IsDestroyed()) continue; if(!ship->IsDisabled()) ship->Recharge(); if(ship->CanBeCarried()) { bool docked = false; map<Ship *, int> &carriers = (ship->Attributes().Category() == "Drone") ? droneCarriers : fighterCarriers; for(auto &it : carriers) if(it.second && it.first->Carry(ship)) { --it.second; docked = true; break; } if(docked) continue; } ships.push_back(ship); if(!ship->GetPersonality().IsUninterested()) ship->SetParent(flagship); } } // Get the coordinates of the planet the player is leaving. Point planetPos; double planetRadius = 0.; const StellarObject *object = player.GetStellarObject(); if(object) { planetPos = object->Position(); planetRadius = object->Radius(); } // Give each ship a random heading and position. The iterator points to the // first ship that was an escort or NPC (i.e. the first ship after any // fleets that were placed starting out in this system). while(it != ships.end()) { const shared_ptr<Ship> &ship = *it++; Point pos; Angle angle = Angle::Random(360.); Point velocity = angle.Unit(); // Any ships in the same system as the player should be either // taking off from the player's planet or nearby. bool isHere = (ship->GetSystem() == player.GetSystem()); if(isHere) pos = planetPos; // Check whether this ship should take off with you. if(isHere && !ship->IsDisabled() && (player.GetPlanet()->CanLand(*ship) || ship->GetGovernment()->IsPlayer()) && !(ship->GetPersonality().IsStaying() || ship->GetPersonality().IsWaiting())) { if(player.GetPlanet()) ship->SetPlanet(player.GetPlanet()); pos += angle.Unit() * Random::Real() * planetRadius; } else { ship->SetPlanet(nullptr); pos = planetPos + Angle::Random().Unit() * ((Random::Real() + 1.) * 400. + 2. * planetRadius); velocity *= Random::Real() * ship->MaxVelocity(); } ship->Place(pos, ship->IsDisabled() ? Point() : velocity, angle); } player.SetPlanet(nullptr); }
// Draw a frame. void Engine::Draw() const { GameData::Background().Draw(position, velocity); draw[drawTickTock].Draw(); for(const auto &it : statuses) { if(it.hull <= 0.) continue; static const Color color[4] = { Color(0., .5, 0., .25), Color(.5, .15, 0., .25), Color(.45, .5, 0., .25), Color(.5, .3, 0., .25) }; RingShader::Draw(it.position, it.radius + 3., 1.5, it.shields, color[it.isEnemy]); RingShader::Draw(it.position, it.radius, 1.5, it.hull, color[2 + it.isEnemy], 20.); } if(flash) FillShader::Fill(Point(), Point(Screen::Width(), Screen::Height()), Color(flash, flash)); // Draw messages. const Font &font = FontSet::Get(14); const vector<Messages::Entry> &messages = Messages::Get(step); Point messagePoint( Screen::Left() + 120., Screen::Bottom() - 20. * messages.size()); for(const auto &it : messages) { float alpha = (it.step + 1000 - step) * .001f; Color color(alpha, 0.); font.Draw(it.message, messagePoint, color); messagePoint.Y() += 20.; } // Draw crosshairs around anything that is targeted. for(const Target &target : targets) { Angle a = target.angle; Angle da(90.); for(int i = 0; i < 4; ++i) { PointerShader::Draw(target.center, a.Unit(), 10., 10., -target.radius, Radar::GetColor(target.type)); a += da; } } GameData::Interfaces().Get("status")->Draw(info); GameData::Interfaces().Get("targets")->Draw(info); // Draw ammo status. Point pos(Screen::Right() - 80, Screen::Bottom()); const Sprite *selectedSprite = SpriteSet::Get("ui/ammo selected"); const Sprite *unselectedSprite = SpriteSet::Get("ui/ammo unselected"); Color selectedColor = *GameData::Colors().Get("bright"); Color unselectedColor = *GameData::Colors().Get("dim"); for(const pair<const Outfit *, int> &it : ammo) { pos.Y() -= 30.; bool isSelected = it.first == player.SelectedWeapon(); SpriteShader::Draw(it.first->Icon(), pos); SpriteShader::Draw( isSelected ? selectedSprite : unselectedSprite, pos + Point(35., 0.)); string amount = to_string(it.second); Point textPos = pos + Point(55 - font.Width(amount), -(30 - font.Height()) / 2); font.Draw(amount, textPos, isSelected ? selectedColor : unselectedColor); } // Draw escort status. escorts.Draw(); if(Preferences::Has("Show CPU / GPU load")) { string loadString = to_string(static_cast<int>(load * 100. + .5)) + "% CPU"; Color color = *GameData::Colors().Get("medium"); FontSet::Get(14).Draw(loadString, Point(-10 - font.Width(loadString), Screen::Height() * -.5 + 5.), color); } }
void Engine::Place() { ships.clear(); EnterSystem(); for(const shared_ptr<Ship> &ship : player.Ships()) { if(ship->IsParked()) continue; ships.push_back(ship); Point pos; Angle angle = Angle::Random(360.); // All your ships that are in system with the player act as if they are // leaving the planet along with you. if(player.GetPlanet() && ship->GetSystem() == player.GetSystem() && !ship->IsDisabled()) { ship->SetPlanet(player.GetPlanet()); for(const StellarObject &object : ship->GetSystem()->Objects()) if(object.GetPlanet() == player.GetPlanet()) { pos = object.Position() + angle.Unit() * Random::Real() * object.Radius(); } } else if(ship->GetSystem()) { pos = Angle::Random().Unit() * ((Random::Real() + 1.) * 600.); for(const StellarObject &object : ship->GetSystem()->Objects()) if(object.GetPlanet() == player.GetPlanet()) pos += object.Position(); } ship->Place(pos, angle.Unit(), angle); } shared_ptr<Ship> flagship; if(!player.Ships().empty()) flagship = player.Ships().front(); for(const Mission &mission : player.Missions()) for(const NPC &npc : mission.NPCs()) { map<Ship *, int> droneCarriers; map<Ship *, int> fighterCarriers; for(const shared_ptr<Ship> &ship : npc.Ships()) { // Skip ships that have been destroyed. if(ship->IsDestroyed() || ship->IsDisabled()) continue; if(ship->DroneBaysFree()) droneCarriers[&*ship] = ship->DroneBaysFree(); if(ship->FighterBaysFree()) fighterCarriers[&*ship] = ship->FighterBaysFree(); // Redo the loading up of fighters. ship->UnloadFighters(); } for(const shared_ptr<Ship> &ship : npc.Ships()) { // Skip ships that have been destroyed. if(ship->IsDestroyed()) continue; if(!ship->IsDisabled()) ship->Recharge(); if(ship->CanBeCarried()) { bool docked = false; if(ship->Attributes().Category() == "Drone") { for(auto &it : droneCarriers) if(it.second) { it.first->AddFighter(ship); --it.second; docked = true; break; } } else if(ship->Attributes().Category() == "Fighter") { for(auto &it : fighterCarriers) if(it.second) { it.first->AddFighter(ship); --it.second; docked = true; break; } } if(docked) continue; } ships.push_back(ship); if(!ship->GetPersonality().IsUninterested()) ship->SetParent(flagship); Point pos; Angle angle = Angle::Random(360.); // All your ships that are in system with the player act as if they are // leaving the planet along with you. if(player.GetPlanet() && ship->GetSystem() == player.GetSystem() && !ship->IsDisabled() && (player.GetPlanet()->CanLand(*ship) || ship->GetGovernment()->IsPlayer()) && !(ship->GetPersonality().IsStaying() || ship->GetPersonality().IsWaiting())) { ship->SetPlanet(player.GetPlanet()); for(const StellarObject &object : ship->GetSystem()->Objects()) if(object.GetPlanet() == player.GetPlanet()) pos = object.Position() + angle.Unit() * Random::Real() * object.Radius(); } else if(ship->GetSystem()) { pos = Angle::Random().Unit() * ((Random::Real() + 1.) * 600.); for(const StellarObject &object : ship->GetSystem()->Objects()) if(object.GetPlanet() == player.GetPlanet()) pos += object.Position(); } ship->Place(pos, angle.Unit(), angle); } } player.SetPlanet(nullptr); }
// Draw a frame. void Engine::Draw() const { GameData::Background().Draw(center, centerVelocity); // Draw any active planet labels. for(const PlanetLabel &label : labels) label.Draw(); draw[drawTickTock].Draw(); for(const auto &it : statuses) { if(it.hull <= 0.) continue; static const Color color[4] = { Color(0., .5, 0., .25), Color(.5, .15, 0., .25), Color(.45, .5, 0., .25), Color(.5, .3, 0., .25) }; RingShader::Draw(it.position, it.radius + 3., 1.5, it.shields, color[it.isEnemy]); RingShader::Draw(it.position, it.radius, 1.5, it.hull, color[2 + it.isEnemy], 20.); } if(flash) FillShader::Fill(Point(), Point(Screen::Width(), Screen::Height()), Color(flash, flash)); // Draw messages. const Font &font = FontSet::Get(14); const vector<Messages::Entry> &messages = Messages::Get(step); Point messagePoint( Screen::Left() + 120., Screen::Bottom() - 20. * messages.size()); auto it = messages.begin(); double firstY = Screen::Top() - font.Height(); if(messagePoint.Y() < firstY) { int skip = (firstY - messagePoint.Y()) / 20.; it += skip; messagePoint.Y() += 20. * skip; } for( ; it != messages.end(); ++it) { float alpha = (it->step + 1000 - step) * .001f; Color color(alpha, 0.); font.Draw(it->message, messagePoint, color); messagePoint.Y() += 20.; } // Draw crosshairs around anything that is targeted. for(const Target &target : targets) { Angle a = target.angle; Angle da(90.); for(int i = 0; i < 4; ++i) { PointerShader::Draw(target.center, a.Unit(), 12., 14., -target.radius, Radar::GetColor(target.type)); a += da; } } const Interface *interfaces[2] = { GameData::Interfaces().Get("status"), GameData::Interfaces().Get("targets") }; for(const Interface *interface : interfaces) { interface->Draw(info); if(interface->HasPoint("radar")) { radar[drawTickTock].Draw( interface->GetPoint("radar"), .025, interface->GetSize("radar").X(), interface->GetSize("radar").Y()); } if(interface->HasPoint("target") && targetAngle) { Point center = interface->GetPoint("target"); double radius = interface->GetSize("target").X(); PointerShader::Draw(center, targetAngle, 10., 10., radius, Color(1.)); } } if(jumpCount && Preferences::Has("Show mini-map")) MapPanel::DrawMiniMap(player, .5 * min(1., jumpCount / 30.), jumpInProgress, step); // Draw ammo status. Point pos(Screen::Right() - 80, Screen::Bottom()); const Sprite *selectedSprite = SpriteSet::Get("ui/ammo selected"); const Sprite *unselectedSprite = SpriteSet::Get("ui/ammo unselected"); Color selectedColor = *GameData::Colors().Get("bright"); Color unselectedColor = *GameData::Colors().Get("dim"); for(const pair<const Outfit *, int> &it : ammo) { pos.Y() -= 30.; bool isSelected = it.first == player.SelectedWeapon(); SpriteShader::Draw(it.first->Icon(), pos); SpriteShader::Draw( isSelected ? selectedSprite : unselectedSprite, pos + Point(35., 0.)); // Some secondary weapons may not have limited ammo. In that case, just // show the icon without a number. if(it.second < 0) continue; string amount = to_string(it.second); Point textPos = pos + Point(55 - font.Width(amount), -(30 - font.Height()) / 2); font.Draw(amount, textPos, isSelected ? selectedColor : unselectedColor); } // Draw escort status. escorts.Draw(); if(Preferences::Has("Show CPU / GPU load")) { string loadString = to_string(static_cast<int>(load * 100. + .5)) + "% CPU"; Color color = *GameData::Colors().Get("medium"); font.Draw(loadString, Point(-10 - font.Width(loadString), Screen::Height() * -.5 + 5.), color); } }