mirror of
https://github.com/Pax1601/DCSOlympus.git
synced 2025-10-29 16:56:34 +00:00
feat: Improvements on scenic modes
This commit is contained in:
parent
23f9eee39f
commit
34f9a8bc40
@ -54,6 +54,17 @@ namespace DataIndex {
|
||||
racetrackLength,
|
||||
racetrackAnchor,
|
||||
racetrackBearing,
|
||||
timeToNextTasking,
|
||||
barrelHeight,
|
||||
muzzleVelocity,
|
||||
aimTime,
|
||||
shotsToFire,
|
||||
shotsBaseInterval,
|
||||
shotsBaseScatter,
|
||||
engagementRange,
|
||||
targetingRange,
|
||||
aimMethodRange,
|
||||
acquisitionRange,
|
||||
lastIndex,
|
||||
endOfData = 255
|
||||
};
|
||||
|
||||
@ -17,7 +17,7 @@ public:
|
||||
virtual void setOnOff(bool newOnOff, bool force = false);
|
||||
virtual void setFollowRoads(bool newFollowRoads, bool force = false);
|
||||
|
||||
void aimAtPoint(Coords aimTarget);
|
||||
string aimAtPoint(Coords aimTarget);
|
||||
|
||||
protected:
|
||||
virtual void AIloop();
|
||||
|
||||
@ -112,6 +112,17 @@ public:
|
||||
virtual void setRacetrackLength(double newValue) { updateValue(racetrackLength, newValue, DataIndex::racetrackLength); }
|
||||
virtual void setRacetrackAnchor(Coords newValue) { updateValue(racetrackAnchor, newValue, DataIndex::racetrackAnchor); }
|
||||
virtual void setRacetrackBearing(double newValue) { updateValue(racetrackBearing, newValue, DataIndex::racetrackBearing); }
|
||||
virtual void setTimeToNextTasking(double newValue) { updateValue(timeToNextTasking, newValue, DataIndex::timeToNextTasking); }
|
||||
virtual void setBarrelHeight(double newValue) { updateValue(barrelHeight, newValue, DataIndex::barrelHeight); }
|
||||
virtual void setMuzzleVelocity(double newValue) { updateValue(muzzleVelocity, newValue, DataIndex::muzzleVelocity); }
|
||||
virtual void setAimTime(double newValue) { updateValue(aimTime, newValue, DataIndex::aimTime); }
|
||||
virtual void setShotsToFire(unsigned int newValue) { updateValue(shotsToFire, newValue, DataIndex::shotsToFire); }
|
||||
virtual void setShotsBaseInterval(double newValue) { updateValue(shotsBaseInterval, newValue, DataIndex::shotsBaseInterval); }
|
||||
virtual void setShotsBaseScatter(double newValue) { updateValue(shotsBaseScatter, newValue, DataIndex::shotsBaseScatter); }
|
||||
virtual void setEngagementRange(double newValue) { updateValue(engagementRange, newValue, DataIndex::engagementRange); }
|
||||
virtual void setTargetingRange(double newValue) { updateValue(targetingRange, newValue, DataIndex::targetingRange); }
|
||||
virtual void setAimMethodRange(double newValue) { updateValue(aimMethodRange, newValue, DataIndex::aimMethodRange); }
|
||||
virtual void setAcquisitionRange(double newValue) { updateValue(acquisitionRange, newValue, DataIndex::acquisitionRange); }
|
||||
|
||||
/********** Getters **********/
|
||||
virtual string getCategory() { return category; };
|
||||
@ -163,6 +174,17 @@ public:
|
||||
virtual double getRacetrackLength() { return racetrackLength; }
|
||||
virtual Coords getRacetrackAnchor() { return racetrackAnchor; }
|
||||
virtual double getRacetrackBearing() { return racetrackBearing; }
|
||||
virtual double getTimeToNextTasking() { return timeToNextTasking; }
|
||||
virtual double getBarrelHeight() { return barrelHeight; }
|
||||
virtual double getMuzzleVelocity() { return muzzleVelocity; }
|
||||
virtual double getAimTime() { return aimTime; }
|
||||
virtual unsigned int getShotsToFire() { return shotsToFire; }
|
||||
virtual double getShotsBaseInterval() { return shotsBaseInterval; }
|
||||
virtual double getShotsBaseScatter() { return shotsBaseScatter; }
|
||||
virtual double getEngagementRange() { return engagementRange; }
|
||||
virtual double getTargetingRange() { return targetingRange; }
|
||||
virtual double getAimMethodRange() { return aimMethodRange; }
|
||||
virtual double getAcquisitionRange() { return acquisitionRange; }
|
||||
|
||||
protected:
|
||||
unsigned int ID;
|
||||
@ -217,6 +239,17 @@ protected:
|
||||
unsigned char shotsScatter = 2;
|
||||
unsigned char shotsIntensity = 2;
|
||||
unsigned char health = 100;
|
||||
double timeToNextTasking = 0;
|
||||
double barrelHeight = 1.0; /* m */
|
||||
double muzzleVelocity = 860; /* m/s */
|
||||
double aimTime = 10; /* s */
|
||||
unsigned int shotsToFire = 10;
|
||||
double shotsBaseInterval = 15; /* s */
|
||||
double shotsBaseScatter = 2; /* degs */
|
||||
double engagementRange = 10000; /* m */
|
||||
double targetingRange = 0; /* m */
|
||||
double aimMethodRange = 0; /* m */
|
||||
double acquisitionRange = 0; /* m */
|
||||
|
||||
/********** Other **********/
|
||||
unsigned int taskCheckCounter = 0;
|
||||
|
||||
@ -49,6 +49,31 @@ void GroundUnit::setDefaults(bool force)
|
||||
setROE(ROE::WEAPON_FREE, force);
|
||||
setOnOff(onOff, force);
|
||||
setFollowRoads(followRoads, force);
|
||||
|
||||
/* Load gun values from database */
|
||||
if (database.has_object_field(to_wstring(name))) {
|
||||
json::value databaseEntry = database[to_wstring(name)];
|
||||
if (databaseEntry.has_number_field(L"barrelHeight"))
|
||||
setBarrelHeight(databaseEntry[L"barrelHeight"].as_number().to_double());
|
||||
if (databaseEntry.has_number_field(L"muzzleVelocity"))
|
||||
setMuzzleVelocity(databaseEntry[L"muzzleVelocity"].as_number().to_double());
|
||||
if (databaseEntry.has_number_field(L"aimTime"))
|
||||
setAimTime(databaseEntry[L"aimTime"].as_number().to_double());
|
||||
if (databaseEntry.has_number_field(L"shotsToFire"))
|
||||
setShotsToFire(databaseEntry[L"shotsToFire"].as_number().to_uint32());
|
||||
if (databaseEntry.has_number_field(L"engagementRange"))
|
||||
setEngagementRange(databaseEntry[L"engagementRange"].as_number().to_double());
|
||||
if (databaseEntry.has_number_field(L"shotsBaseInterval"))
|
||||
setShotsBaseInterval(databaseEntry[L"shotsBaseInterval"].as_number().to_double());
|
||||
if (databaseEntry.has_number_field(L"shotsBaseScatter"))
|
||||
setShotsBaseScatter(databaseEntry[L"shotsBaseScatter"].as_number().to_double());
|
||||
if (databaseEntry.has_number_field(L"targetingRange"))
|
||||
setTargetingRange(databaseEntry[L"targetingRange"].as_number().to_double());
|
||||
if (databaseEntry.has_number_field(L"aimMethodRange"))
|
||||
setAimMethodRange(databaseEntry[L"aimMethodRange"].as_number().to_double());
|
||||
if (databaseEntry.has_number_field(L"acquisitionRange"))
|
||||
setAcquisitionRange(databaseEntry[L"acquisitionRange"].as_number().to_double());
|
||||
}
|
||||
}
|
||||
|
||||
void GroundUnit::setState(unsigned char newState)
|
||||
@ -214,7 +239,7 @@ void GroundUnit::AIloop()
|
||||
break;
|
||||
}
|
||||
case State::SIMULATE_FIRE_FIGHT: {
|
||||
setTask("Simulating fire fight");
|
||||
string taskString = "";
|
||||
|
||||
if (internalCounter == 0 && targetPosition != Coords(NULL) && scheduler->getLoad() < 30) {
|
||||
/* Get the distance and bearing to the target */
|
||||
@ -229,21 +254,16 @@ void GroundUnit::AIloop()
|
||||
Geodesic::WGS84().Direct(scatteredTargetPosition.lat, scatteredTargetPosition.lng, bearing1 + 90, scatterDistance, scatteredTargetPosition.lat, scatteredTargetPosition.lng);
|
||||
|
||||
/* Recover the data from the database */
|
||||
double aimTime = 2; /* s */
|
||||
bool indirectFire = false;
|
||||
double shotsBaseInterval = 15; /* s */
|
||||
if (database.has_object_field(to_wstring(name))) {
|
||||
json::value databaseEntry = database[to_wstring(name)];
|
||||
if (databaseEntry.has_number_field(L"aimTime"))
|
||||
aimTime = databaseEntry[L"aimTime"].as_number().to_double();
|
||||
if (databaseEntry.has_boolean_field(L"indirectFire"))
|
||||
indirectFire = databaseEntry[L"indirectFire"].as_bool();
|
||||
if (databaseEntry.has_number_field(L"shotsBaseInterval"))
|
||||
shotsBaseInterval = databaseEntry[L"shotsBaseInterval"].as_number().to_double();
|
||||
}
|
||||
|
||||
/* If the unit is of the indirect fire type, like a mortar, simply shoot at the target */
|
||||
if (indirectFire) {
|
||||
taskString += "Simulating fire fight with indirect fire";
|
||||
log(unitName + "(" + name + ")" + " simulating fire fight with indirect fire");
|
||||
std::ostringstream taskSS;
|
||||
taskSS.precision(10);
|
||||
@ -254,8 +274,10 @@ void GroundUnit::AIloop()
|
||||
}
|
||||
/* Otherwise use the aim method */
|
||||
else {
|
||||
taskString += "Simulating fire fight with aim point method. ";
|
||||
log(unitName + "(" + name + ")" + " simulating fire fight with aim at point method");
|
||||
aimAtPoint(scatteredTargetPosition);
|
||||
string aimTaskString = aimAtPoint(scatteredTargetPosition);
|
||||
taskString += aimTaskString;
|
||||
}
|
||||
|
||||
/* Wait an amout of time depending on the shots intensity */
|
||||
@ -270,10 +292,15 @@ void GroundUnit::AIloop()
|
||||
internalCounter = static_cast<unsigned int>(3 / FRAMERATE_TIME_INTERVAL);
|
||||
internalCounter--;
|
||||
|
||||
setTimeToNextTasking(internalCounter * FRAMERATE_TIME_INTERVAL);
|
||||
|
||||
if (taskString.length() > 0)
|
||||
setTask(taskString);
|
||||
|
||||
break;
|
||||
}
|
||||
case State::SCENIC_AAA: {
|
||||
setTask("Scenic AAA");
|
||||
string taskString = "";
|
||||
|
||||
/* Only perform scenic functions when the scheduler is "free" */
|
||||
if (((!getHasTask() && scheduler->getLoad() < 30) || internalCounter == 0)) {
|
||||
@ -283,29 +310,42 @@ void GroundUnit::AIloop()
|
||||
Unit* target = unitsManager->getClosestUnit(this, targetCoalition, { "Aircraft", "Helicopter" }, distance);
|
||||
|
||||
/* Recover the data from the database */
|
||||
double aimTime = 2; /* s */
|
||||
double shotsBaseInterval = 15; /* s */
|
||||
bool flak = false;
|
||||
if (database.has_object_field(to_wstring(name))) {
|
||||
json::value databaseEntry = database[to_wstring(name)];
|
||||
if (databaseEntry.has_number_field(L"aimTime"))
|
||||
aimTime = databaseEntry[L"aimTime"].as_number().to_double();
|
||||
if (databaseEntry.has_number_field(L"shotsBaseInterval"))
|
||||
shotsBaseInterval = databaseEntry[L"shotsBaseInterval"].as_number().to_double();
|
||||
if (databaseEntry.has_boolean_field(L"flak"))
|
||||
flak = databaseEntry[L"flak"].as_bool();
|
||||
}
|
||||
|
||||
/* Only run if an enemy air unit is closer than 20km to avoid useless load */
|
||||
if (target != nullptr && distance < 20000 /* m */) {
|
||||
double activationDistance = 20000;
|
||||
if (2 * engagementRange > activationDistance)
|
||||
activationDistance = 2 * engagementRange;
|
||||
|
||||
if (target != nullptr && distance < activationDistance /* m */) {
|
||||
double r = 15; /* m */
|
||||
double barrelElevation = r * tan(acos(((double)(rand()) / (double)(RAND_MAX))));
|
||||
double barrelElevation = position.alt + barrelHeight + r * tan(acos(((double)(rand()) / (double)(RAND_MAX))));
|
||||
|
||||
double lat = 0;
|
||||
double lng = 0;
|
||||
double randomBearing = ((double)(rand()) / (double)(RAND_MAX)) * 360;
|
||||
Geodesic::WGS84().Direct(position.lat, position.lng, randomBearing, r, lat, lng);
|
||||
|
||||
if (flak) {
|
||||
lat = position.lat + RANDOM_MINUS_ONE_TO_ONE * (ShotsScatter::LOW - shotsScatter) * 0.01;
|
||||
lng = position.lng + RANDOM_MINUS_ONE_TO_ONE * (ShotsScatter::LOW - shotsScatter) * 0.01;
|
||||
barrelElevation = target->getPosition().alt + RANDOM_MINUS_ONE_TO_ONE * (ShotsScatter::LOW - shotsScatter) * 1000;
|
||||
taskString += "Flak box mode.";
|
||||
}
|
||||
else {
|
||||
taskString += "Scenic AAA. Bearing: " + to_string((int)round(randomBearing)) + "deg";
|
||||
}
|
||||
|
||||
taskString += ". Aim point elevation " + to_string((int) round(barrelElevation)) + "m AGL";
|
||||
|
||||
std::ostringstream taskSS;
|
||||
taskSS.precision(10);
|
||||
taskSS << "{id = 'FireAtPoint', lat = " << lat << ", lng = " << lng << ", alt = " << position.alt + barrelElevation << ", radius = 0.001}";
|
||||
taskSS << "{id = 'FireAtPoint', lat = " << lat << ", lng = " << lng << ", alt = " << barrelElevation << ", radius = 0.001, expendQty = " << shotsToFire << " }";
|
||||
Command* command = dynamic_cast<Command*>(new SetTask(groupName, taskSS.str(), [this]() { this->setHasTaskAssigned(true); }));
|
||||
scheduler->appendCommand(command);
|
||||
setHasTask(true);
|
||||
@ -313,16 +353,26 @@ void GroundUnit::AIloop()
|
||||
/* Wait an amout of time depending on the shots intensity */
|
||||
internalCounter = static_cast<unsigned int>(((ShotsIntensity::HIGH - shotsIntensity) * shotsBaseInterval + aimTime) / FRAMERATE_TIME_INTERVAL);
|
||||
}
|
||||
else {
|
||||
if (target == nullptr)
|
||||
taskString += "Scenic AAA. No valid target.";
|
||||
else
|
||||
taskString += "Scenic AAA. Target outside max range: " + to_string((int)round(distance)) + "m.";
|
||||
}
|
||||
}
|
||||
|
||||
if (internalCounter == 0)
|
||||
internalCounter = static_cast<unsigned int>(3 / FRAMERATE_TIME_INTERVAL);
|
||||
internalCounter--;
|
||||
|
||||
setTimeToNextTasking(internalCounter * FRAMERATE_TIME_INTERVAL);
|
||||
if (taskString.length() > 0)
|
||||
setTask(taskString);
|
||||
|
||||
break;
|
||||
}
|
||||
case State::MISS_ON_PURPOSE: {
|
||||
setTask("Missing on purpose");
|
||||
string taskString = "";
|
||||
|
||||
/* Check that the unit can perform AAA duties */
|
||||
bool canAAA = false;
|
||||
@ -340,43 +390,6 @@ void GroundUnit::AIloop()
|
||||
unsigned char unitCoalition = coalition == 0 ? getOperateAs() : coalition;
|
||||
unsigned char targetCoalition = unitCoalition == 2 ? 1 : 2;
|
||||
|
||||
/* Default gun values */
|
||||
double barrelHeight = 1.0; /* m */
|
||||
double muzzleVelocity = 860; /* m/s */
|
||||
double aimTime = 10; /* s */
|
||||
unsigned int shotsToFire = 10;
|
||||
double shotsBaseInterval = 15; /* s */
|
||||
double shotsBaseScatter = 2; /* degs */
|
||||
double engagementRange = 10000; /* m */
|
||||
double targetingRange = 0; /* m */
|
||||
double aimMethodRange = 0; /* m */
|
||||
double acquisitionRange = 0; /* m */
|
||||
|
||||
/* Load gun values from database */
|
||||
if (database.has_object_field(to_wstring(name))) {
|
||||
json::value databaseEntry = database[to_wstring(name)];
|
||||
if (databaseEntry.has_number_field(L"barrelHeight"))
|
||||
barrelHeight = databaseEntry[L"barrelHeight"].as_number().to_double();
|
||||
if (databaseEntry.has_number_field(L"muzzleVelocity"))
|
||||
muzzleVelocity = databaseEntry[L"muzzleVelocity"].as_number().to_double();
|
||||
if (databaseEntry.has_number_field(L"aimTime"))
|
||||
aimTime = databaseEntry[L"aimTime"].as_number().to_double();
|
||||
if (databaseEntry.has_number_field(L"shotsToFire"))
|
||||
shotsToFire = databaseEntry[L"shotsToFire"].as_number().to_uint32();
|
||||
if (databaseEntry.has_number_field(L"engagementRange"))
|
||||
engagementRange = databaseEntry[L"engagementRange"].as_number().to_double();
|
||||
if (databaseEntry.has_number_field(L"shotsBaseInterval"))
|
||||
shotsBaseInterval = databaseEntry[L"shotsBaseInterval"].as_number().to_double();
|
||||
if (databaseEntry.has_number_field(L"shotsBaseScatter"))
|
||||
shotsBaseScatter = databaseEntry[L"shotsBaseScatter"].as_number().to_double();
|
||||
if (databaseEntry.has_number_field(L"targetingRange"))
|
||||
targetingRange = databaseEntry[L"targetingRange"].as_number().to_double();
|
||||
if (databaseEntry.has_number_field(L"aimMethodRange"))
|
||||
aimMethodRange = databaseEntry[L"aimMethodRange"].as_number().to_double();
|
||||
if (databaseEntry.has_number_field(L"acquisitionRange"))
|
||||
acquisitionRange = databaseEntry[L"acquisitionRange"].as_number().to_double();
|
||||
}
|
||||
|
||||
/* Get all the units in range and select one at random */
|
||||
double range = max(max(engagementRange, aimMethodRange), acquisitionRange);
|
||||
map<Unit*, double> targets = unitsManager->getUnitsInRange(this, targetCoalition, { "Aircraft", "Helicopter" }, range);
|
||||
@ -392,12 +405,17 @@ void GroundUnit::AIloop()
|
||||
|
||||
/* Only do if we have a valid target close enough for AAA */
|
||||
if (target != nullptr) {
|
||||
taskString += "Missing on purpose. Valid target at range: " + to_string((int) round(distance)) + "m";
|
||||
|
||||
double correctedAimTime = aimTime;
|
||||
/* Approximate the flight time */
|
||||
if (muzzleVelocity != 0)
|
||||
aimTime += distance / muzzleVelocity;
|
||||
correctedAimTime += distance / muzzleVelocity;
|
||||
|
||||
/* If the target is in targeting range and we are in highest precision mode, target it */
|
||||
if (distance < targetingRange && shotsScatter == ShotsScatter::LOW) {
|
||||
taskString += ". Range is less than targeting range (" + to_string((int) round(targetingRange)) + "m) and scatter is LOW, aiming at target.";
|
||||
|
||||
/* Send the command */
|
||||
std::ostringstream taskSS;
|
||||
taskSS.precision(10);
|
||||
@ -406,37 +424,47 @@ void GroundUnit::AIloop()
|
||||
scheduler->appendCommand(command);
|
||||
setHasTask(true);
|
||||
|
||||
internalCounter = static_cast<unsigned int>((aimTime + (ShotsIntensity::HIGH - shotsIntensity) * shotsBaseInterval + 2) / FRAMERATE_TIME_INTERVAL);
|
||||
internalCounter = static_cast<unsigned int>((correctedAimTime + (ShotsIntensity::HIGH - shotsIntensity) * shotsBaseInterval + 2) / FRAMERATE_TIME_INTERVAL);
|
||||
}
|
||||
/* Else, do miss on purpose */
|
||||
else {
|
||||
/* Compute where the target will be in aimTime seconds, plus the effect of scatter. */
|
||||
double scatterDistance = distance * tan(shotsBaseScatter * (ShotsScatter::LOW - shotsScatter) / 57.29577) * (RANDOM_ZERO_TO_ONE - 0.1);
|
||||
double aimDistance = target->getHorizontalVelocity() * aimTime + scatterDistance;
|
||||
double aimDistance = target->getHorizontalVelocity() * correctedAimTime + scatterDistance;
|
||||
double aimLat = 0;
|
||||
double aimLng = 0;
|
||||
Geodesic::WGS84().Direct(target->getPosition().lat, target->getPosition().lng, target->getTrack() * 57.29577, aimDistance, aimLat, aimLng); /* TODO make util to convert degrees and radians function */
|
||||
double aimAlt = target->getPosition().alt + target->getVerticalVelocity() * aimTime + distance * tan(shotsBaseScatter * (ShotsScatter::LOW - shotsScatter) / 57.29577) * RANDOM_ZERO_TO_ONE; // Force to always miss high never low
|
||||
double aimAlt = target->getPosition().alt + target->getVerticalVelocity() * correctedAimTime + distance * tan(shotsBaseScatter * (ShotsScatter::LOW - shotsScatter) / 57.29577) * RANDOM_ZERO_TO_ONE; // Force to always miss high never low
|
||||
|
||||
/* Send the command */
|
||||
if (distance < engagementRange) {
|
||||
taskString += ". Range is less than engagement range (" + to_string((int) round(engagementRange)) + "m), using FIRE AT POINT method";
|
||||
|
||||
/* If the unit is closer than the engagement range, use the fire at point method */
|
||||
std::ostringstream taskSS;
|
||||
taskSS.precision(10);
|
||||
taskSS << "{id = 'FireAtPoint', lat = " << aimLat << ", lng = " << aimLng << ", alt = " << aimAlt << ", radius = 0.001, expendQty = " << shotsToFire << " }";
|
||||
|
||||
taskString += ". Aiming altitude " + to_string((int)round((aimAlt - position.alt) / 0.3048)) + "ft AGL";
|
||||
Command* command = dynamic_cast<Command*>(new SetTask(groupName, taskSS.str(), [this]() { this->setHasTaskAssigned(true); }));
|
||||
scheduler->appendCommand(command);
|
||||
setHasTask(true);
|
||||
setTargetPosition(Coords(aimLat, aimLng, target->getPosition().alt));
|
||||
internalCounter = static_cast<unsigned int>((aimTime + (ShotsIntensity::HIGH - shotsIntensity) * shotsBaseInterval + 2) / FRAMERATE_TIME_INTERVAL);
|
||||
internalCounter = static_cast<unsigned int>((correctedAimTime + (ShotsIntensity::HIGH - shotsIntensity) * shotsBaseInterval + 2) / FRAMERATE_TIME_INTERVAL);
|
||||
}
|
||||
else if (distance < aimMethodRange) {
|
||||
taskString += ". Range is less than aim method range (" + to_string((int)round(aimMethodRange / 0.3048)) + "ft), using AIM method.";
|
||||
|
||||
/* If the unit is closer than the aim method range, use the aim method range */
|
||||
aimAtPoint(Coords(aimLat, aimLng, aimAlt));
|
||||
string aimMethodTask = aimAtPoint(Coords(aimLat, aimLng, aimAlt));
|
||||
taskString += aimMethodTask;
|
||||
|
||||
setTargetPosition(Coords(aimLat, aimLng, target->getPosition().alt));
|
||||
internalCounter = static_cast<unsigned int>((aimTime + (ShotsIntensity::HIGH - shotsIntensity) * shotsBaseInterval + 2) / FRAMERATE_TIME_INTERVAL);
|
||||
internalCounter = static_cast<unsigned int>((correctedAimTime + (ShotsIntensity::HIGH - shotsIntensity) * shotsBaseInterval + 2) / FRAMERATE_TIME_INTERVAL);
|
||||
}
|
||||
else {
|
||||
taskString += ". Target is not in range of weapon, waking up unit to get ready for tasking.";
|
||||
|
||||
/* Else just wake the unit up with an impossible command */
|
||||
std::ostringstream taskSS;
|
||||
taskSS.precision(10);
|
||||
@ -453,6 +481,7 @@ void GroundUnit::AIloop()
|
||||
missOnPurposeTarget = target;
|
||||
}
|
||||
else {
|
||||
taskString += "Missing on purpose. No target in range.";
|
||||
if (getHasTask())
|
||||
resetTask();
|
||||
}
|
||||
@ -466,7 +495,7 @@ void GroundUnit::AIloop()
|
||||
if (databaseEntry.has_number_field(L"alertnessTimeConstant"))
|
||||
alertnessTimeConstant = databaseEntry[L"alertnessTimeConstant"].as_number().to_double();
|
||||
}
|
||||
internalCounter = static_cast<unsigned int>((5 + RANDOM_ZERO_TO_ONE * alertnessTimeConstant * 0 /* TODO: remove to enable alertness again */) / FRAMERATE_TIME_INTERVAL);
|
||||
internalCounter = static_cast<unsigned int>((5 + RANDOM_ZERO_TO_ONE * alertnessTimeConstant) / FRAMERATE_TIME_INTERVAL);
|
||||
missOnPurposeTarget = nullptr;
|
||||
setTargetPosition(Coords(NULL));
|
||||
}
|
||||
@ -476,6 +505,11 @@ void GroundUnit::AIloop()
|
||||
setState(State::IDLE);
|
||||
}
|
||||
|
||||
setTimeToNextTasking(internalCounter * FRAMERATE_TIME_INTERVAL);
|
||||
|
||||
if (taskString.length() > 0)
|
||||
setTask(taskString);
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
@ -484,7 +518,8 @@ void GroundUnit::AIloop()
|
||||
}
|
||||
|
||||
|
||||
void GroundUnit::aimAtPoint(Coords aimTarget) {
|
||||
string GroundUnit::aimAtPoint(Coords aimTarget) {
|
||||
string taskString = "";
|
||||
double dist;
|
||||
double bearing1;
|
||||
double bearing2;
|
||||
@ -521,7 +556,8 @@ void GroundUnit::aimAtPoint(Coords aimTarget) {
|
||||
double lng = 0;
|
||||
Geodesic::WGS84().Direct(position.lat, position.lng, bearing1, r, lat, lng);
|
||||
|
||||
log(unitName + "(" + name + ")" + " shooting with aim at point method. Barrel elevation: " + to_string(barrelElevation * 57.29577) + "°, bearing: " + to_string(bearing1) + "°");
|
||||
taskString = +"Barrel elevation: " + to_string((int) round(barrelElevation)) + "m, bearing: " + to_string((int) round(bearing1)) + "deg";
|
||||
log(unitName + "(" + name + ")" + " shooting with aim at point method. Barrel elevation: " + to_string(barrelElevation) + "m, bearing: " + to_string(bearing1) + "°");
|
||||
|
||||
std::ostringstream taskSS;
|
||||
taskSS.precision(10);
|
||||
@ -532,7 +568,10 @@ void GroundUnit::aimAtPoint(Coords aimTarget) {
|
||||
}
|
||||
else {
|
||||
log("Target out of range for " + unitName + "(" + name + ")");
|
||||
taskString = +"Target out of range";
|
||||
}
|
||||
|
||||
return taskString;
|
||||
}
|
||||
|
||||
void GroundUnit::changeSpeed(string change)
|
||||
|
||||
@ -509,6 +509,29 @@ void Scheduler::handleRequest(string key, json::value value, string username, js
|
||||
}
|
||||
}
|
||||
/************************/
|
||||
else if (key.compare("setEngagementProperties") == 0)
|
||||
{
|
||||
unsigned int ID = value[L"ID"].as_integer();
|
||||
unitsManager->acquireControl(ID);
|
||||
Unit* unit = unitsManager->getGroupLeader(ID);
|
||||
if (unit != nullptr)
|
||||
{
|
||||
/* Engagement properties tasking */
|
||||
unit->setBarrelHeight(value[L"barrelHeight"].as_number().to_double());
|
||||
unit->setMuzzleVelocity(value[L"muzzleVelocity"].as_number().to_double());
|
||||
unit->setAimTime(value[L"aimTime"].as_number().to_double());
|
||||
unit->setShotsToFire(value[L"shotsToFire"].as_number().to_uint32());
|
||||
unit->setShotsBaseInterval(value[L"shotsBaseInterval"].as_number().to_double());
|
||||
unit->setShotsBaseScatter(value[L"shotsBaseScatter"].as_number().to_double());
|
||||
unit->setEngagementRange(value[L"engagementRange"].as_number().to_double());
|
||||
unit->setTargetingRange(value[L"targetingRange"].as_number().to_double());
|
||||
unit->setAimMethodRange(value[L"aimMethodRange"].as_number().to_double());
|
||||
unit->setAcquisitionRange(value[L"acquisitionRange"].as_number().to_double());
|
||||
|
||||
log(username + " updated unit " + unit->getUnitName() + "(" + unit->getName() + ") engagementProperties", true);
|
||||
}
|
||||
}
|
||||
/************************/
|
||||
else if (key.compare("setFollowRoads") == 0)
|
||||
{
|
||||
unsigned int ID = value[L"ID"].as_integer();
|
||||
|
||||
@ -298,6 +298,17 @@ void Unit::getData(stringstream& ss, unsigned long long time)
|
||||
case DataIndex::racetrackLength: appendNumeric(ss, datumIndex, racetrackLength); break;
|
||||
case DataIndex::racetrackAnchor: appendNumeric(ss, datumIndex, racetrackAnchor); break;
|
||||
case DataIndex::racetrackBearing: appendNumeric(ss, datumIndex, racetrackBearing); break;
|
||||
case DataIndex::timeToNextTasking: appendNumeric(ss, datumIndex, timeToNextTasking); break;
|
||||
case DataIndex::barrelHeight: appendNumeric(ss, datumIndex, barrelHeight); break;
|
||||
case DataIndex::muzzleVelocity: appendNumeric(ss, datumIndex, muzzleVelocity); break;
|
||||
case DataIndex::aimTime: appendNumeric(ss, datumIndex, aimTime); break;
|
||||
case DataIndex::shotsToFire: appendNumeric(ss, datumIndex, shotsToFire); break;
|
||||
case DataIndex::shotsBaseInterval: appendNumeric(ss, datumIndex, shotsBaseInterval); break;
|
||||
case DataIndex::shotsBaseScatter: appendNumeric(ss, datumIndex, shotsBaseScatter); break;
|
||||
case DataIndex::engagementRange: appendNumeric(ss, datumIndex, engagementRange); break;
|
||||
case DataIndex::targetingRange: appendNumeric(ss, datumIndex, targetingRange); break;
|
||||
case DataIndex::aimMethodRange: appendNumeric(ss, datumIndex, aimMethodRange); break;
|
||||
case DataIndex::acquisitionRange: appendNumeric(ss, datumIndex, acquisitionRange); break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -385,7 +385,7 @@ export enum WarningSubstate {
|
||||
NO_SUBSTATE = "No substate",
|
||||
NOT_CHROME = "Not chrome",
|
||||
NOT_SECURE = "Not secure",
|
||||
ERROR_UPLOADING_CONFIG = "Error uploading config"
|
||||
ERROR_UPLOADING_CONFIG = "Error uploading config",
|
||||
}
|
||||
|
||||
export type OlympusSubState = DrawSubState | JTACSubState | SpawnSubState | OptionsSubstate | string;
|
||||
@ -418,7 +418,7 @@ export const MAP_OPTIONS_DEFAULTS: MapOptions = {
|
||||
AWACSCoalition: "blue",
|
||||
hideChromeWarning: false,
|
||||
hideSecureWarning: false,
|
||||
showMissionDrawings: false
|
||||
showMissionDrawings: false,
|
||||
};
|
||||
|
||||
export const MAP_HIDDEN_TYPES_DEFAULTS = {
|
||||
@ -497,6 +497,17 @@ export enum DataIndexes {
|
||||
racetrackLength,
|
||||
racetrackAnchor,
|
||||
racetrackBearing,
|
||||
timeToNextTasking,
|
||||
barrelHeight,
|
||||
muzzleVelocity,
|
||||
aimTime,
|
||||
shotsToFire,
|
||||
shotsBaseInterval,
|
||||
shotsBaseScatter,
|
||||
engagementRange,
|
||||
targetingRange,
|
||||
aimMethodRange,
|
||||
acquisitionRange,
|
||||
endOfData = 255,
|
||||
}
|
||||
|
||||
@ -530,7 +541,6 @@ export enum ContextActionType {
|
||||
DELETE,
|
||||
}
|
||||
|
||||
|
||||
export enum colors {
|
||||
ALICE_BLUE = "#F0F8FF",
|
||||
ANTIQUE_WHITE = "#FAEBD7",
|
||||
@ -679,7 +689,7 @@ export enum colors {
|
||||
OLYMPUS_BLUE = "#243141",
|
||||
OLYMPUS_RED = "#F05252",
|
||||
OLYMPUS_ORANGE = "#FF9900",
|
||||
OLYMPUS_GREEN = "#8BFF63"
|
||||
OLYMPUS_GREEN = "#8BFF63",
|
||||
}
|
||||
|
||||
export const CONTEXT_ACTION_COLORS = [undefined, colors.WHITE, colors.GREEN, colors.PURPLE, colors.BLUE, colors.RED];
|
||||
@ -919,7 +929,9 @@ export namespace ContextActions {
|
||||
ContextActionTarget.POINT,
|
||||
(units: Unit[], _, targetPosition: LatLng | null) => {
|
||||
if (targetPosition)
|
||||
getApp().getUnitsManager().fireInfrared(targetPosition, getApp().getMap().getKeepRelativePositions(), getApp().getMap().getDestinationRotation(), units);
|
||||
getApp()
|
||||
.getUnitsManager()
|
||||
.fireInfrared(targetPosition, getApp().getMap().getKeepRelativePositions(), getApp().getMap().getDestinationRotation(), units);
|
||||
},
|
||||
{ type: ContextActionType.ENGAGE, code: "KeyL", ctrlKey: true, shiftKey: false }
|
||||
);
|
||||
|
||||
@ -40,7 +40,8 @@ export interface OlympusConfig {
|
||||
|
||||
/* Set by server */
|
||||
local?: boolean;
|
||||
authentication?: { // Only sent when in localhost mode for autologin
|
||||
authentication?: {
|
||||
// Only sent when in localhost mode for autologin
|
||||
gameMasterPassword: string;
|
||||
blueCommanderPasword: string;
|
||||
redCommanderPassword: string;
|
||||
@ -53,12 +54,12 @@ export interface SessionData {
|
||||
unitSinks?: { ID: number }[];
|
||||
connections?: any[];
|
||||
coalitionAreas?: (
|
||||
| { type: 'circle', label: string; latlng: { lat: number; lng: number }; radius: number; coalition: Coalition }
|
||||
| { type: 'polygon', label: string; latlngs: { lat: number; lng: number }[]; coalition: Coalition }
|
||||
| { type: "circle"; label: string; latlng: { lat: number; lng: number }; radius: number; coalition: Coalition }
|
||||
| { type: "polygon"; label: string; latlngs: { lat: number; lng: number }[]; coalition: Coalition }
|
||||
)[];
|
||||
hotgroups?: {[key: string]: number[]},
|
||||
starredSpawns?: { [key: number]: SpawnRequestTable }
|
||||
drawings?: { [key: string]: {visibility: boolean, opacity: number, name: string, guid: string, containers: any, drawings: any} }
|
||||
hotgroups?: { [key: string]: number[] };
|
||||
starredSpawns?: { [key: number]: SpawnRequestTable };
|
||||
drawings?: { [key: string]: { visibility: boolean; opacity: number; name: string; guid: string; containers: any; drawings: any } };
|
||||
}
|
||||
|
||||
export interface ProfileOptions {
|
||||
@ -88,7 +89,7 @@ export interface BullseyesData {
|
||||
|
||||
export interface SpotsData {
|
||||
spots: {
|
||||
[key: string]: { type: string, targetPosition: {lat: number; lng: number}; sourceUnitID: number; code?: number };
|
||||
[key: string]: { type: string; targetPosition: { lat: number; lng: number }; sourceUnitID: number; code?: number };
|
||||
};
|
||||
sessionHash: string;
|
||||
time: number;
|
||||
@ -265,6 +266,17 @@ export interface UnitData {
|
||||
racetrackLength: number;
|
||||
racetrackAnchor: LatLng;
|
||||
racetrackBearing: number;
|
||||
timeToNextTasking: number;
|
||||
barrelHeight: number;
|
||||
muzzleVelocity: number;
|
||||
aimTime: number;
|
||||
shotsToFire: number;
|
||||
shotsBaseInterval: number;
|
||||
shotsBaseScatter: number;
|
||||
engagementRange: number;
|
||||
targetingRange: number;
|
||||
aimMethodRange: number;
|
||||
acquisitionRange: number;
|
||||
}
|
||||
|
||||
export interface LoadoutItemBlueprint {
|
||||
@ -400,4 +412,4 @@ export interface Drawing {
|
||||
hiddenOnPlanner?: boolean;
|
||||
file?: string;
|
||||
scale?: number;
|
||||
}
|
||||
}
|
||||
|
||||
@ -554,6 +554,38 @@ export class ServerManager {
|
||||
this.PUT(data, callback);
|
||||
}
|
||||
|
||||
setEngagementProperties(
|
||||
ID: number,
|
||||
barrelHeight: number,
|
||||
muzzleVelocity: number,
|
||||
aimTime: number,
|
||||
shotsToFire: number,
|
||||
shotsBaseInterval: number,
|
||||
shotsBaseScatter: number,
|
||||
engagementRange: number,
|
||||
targetingRange: number,
|
||||
aimMethodRange: number,
|
||||
acquisitionRange: number,
|
||||
callback: CallableFunction = () => {}
|
||||
) {
|
||||
var command = {
|
||||
ID: ID,
|
||||
barrelHeight: barrelHeight,
|
||||
muzzleVelocity: muzzleVelocity,
|
||||
aimTime: aimTime,
|
||||
shotsToFire: shotsToFire,
|
||||
shotsBaseInterval: shotsBaseInterval,
|
||||
shotsBaseScatter: shotsBaseScatter,
|
||||
engagementRange: engagementRange,
|
||||
targetingRange: targetingRange,
|
||||
aimMethodRange: aimMethodRange,
|
||||
acquisitionRange: acquisitionRange,
|
||||
}
|
||||
|
||||
var data = { setEngagementProperties: command };
|
||||
this.PUT(data, callback);
|
||||
}
|
||||
|
||||
setCommandModeOptions(commandModeOptions: CommandModeOptions, callback: CallableFunction = () => {}) {
|
||||
var data = { setCommandModeOptions: commandModeOptions };
|
||||
this.PUT(data, callback);
|
||||
|
||||
@ -11,6 +11,7 @@ export function OlNumberInput(props: {
|
||||
tooltip?: string | (() => JSX.Element | JSX.Element[]);
|
||||
tooltipPosition?: string;
|
||||
tooltipRelativeToParent?: boolean;
|
||||
decimalPlaces?: number;
|
||||
onDecrease: () => void;
|
||||
onIncrease: () => void;
|
||||
onChange: (e: ChangeEvent<HTMLInputElement>) => void;
|
||||
@ -89,7 +90,7 @@ export function OlNumberInput(props: {
|
||||
dark:focus:ring-blue-700
|
||||
focus:border-blue-700 focus:ring-blue-500
|
||||
`}
|
||||
value={zeroAppend(props.value, props.minLength ?? 0)}
|
||||
value={zeroAppend(props.value, props.minLength ?? 0, props.decimalPlaces !== undefined, props.decimalPlaces ?? 0)}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@ -8,6 +8,7 @@ import { OlButtonGroup, OlButtonGroupItem } from "../components/olbuttongroup";
|
||||
import { OlCheckbox } from "../components/olcheckbox";
|
||||
import {
|
||||
ROEs,
|
||||
UnitState,
|
||||
altitudeIncrements,
|
||||
emissionsCountermeasures,
|
||||
maxAltitudeValues,
|
||||
@ -46,7 +47,7 @@ import {
|
||||
olButtonsVisibilityOlympus,
|
||||
} from "../components/olicons";
|
||||
import { Coalition } from "../../types/types";
|
||||
import { convertROE, deepCopyTable, ftToM, knotsToMs, mToFt, msToKnots } from "../../other/utils";
|
||||
import { convertROE, deepCopyTable, ftToM, knotsToMs, mToFt, msToKnots, zeroAppend } from "../../other/utils";
|
||||
import { FaChevronLeft, FaCog, FaExclamationCircle, FaGasPump, FaQuestionCircle, FaSignal, FaTag } from "react-icons/fa";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { OlSearchBar } from "../components/olsearchbar";
|
||||
@ -58,9 +59,10 @@ import { OlStringInput } from "../components/olstringinput";
|
||||
import { OlFrequencyInput } from "../components/olfrequencyinput";
|
||||
import { UnitSink } from "../../audio/unitsink";
|
||||
import { AudioManagerStateChangedEvent, SelectedUnitsChangedEvent, SelectionClearedEvent, UnitsUpdatedEvent } from "../../events";
|
||||
import { IconDefinition } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faCog, IconDefinition } from "@fortawesome/free-solid-svg-icons";
|
||||
import { OlExpandingTooltip } from "../components/olexpandingtooltip";
|
||||
import { OlLocation } from "../components/ollocation";
|
||||
import { OlStateButton } from "../components/olstatebutton";
|
||||
|
||||
export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
function initializeUnitsData() {
|
||||
@ -129,6 +131,17 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
const [activeAdvancedSettings, setActiveAdvancedSettings] = useState(null as null | GeneralSettings);
|
||||
const [lastUpdateTime, setLastUpdateTime] = useState(0);
|
||||
const [showScenicModes, setShowScenicModes] = useState(true);
|
||||
const [showEngagementSettings, setShowEngagementSettings] = useState(false);
|
||||
const [barrelHeight, setBarrelHeight] = useState(0);
|
||||
const [muzzleVelocity, setMuzzleVelocity] = useState(0);
|
||||
const [aimTime, setAimTime] = useState(0);
|
||||
const [shotsToFire, setShotsToFire] = useState(0);
|
||||
const [shotsBaseInterval, setShotsBaseInterval] = useState(0);
|
||||
const [shotsBaseScatter, setShotsBaseScatter] = useState(0);
|
||||
const [engagementRange, setEngagementRange] = useState(0);
|
||||
const [targetingRange, setTargetingRange] = useState(0);
|
||||
const [aimMethodRange, setAimMethodRange] = useState(0);
|
||||
const [acquisitionRange, setAcquisitionRange] = useState(0);
|
||||
|
||||
var searchBarRef = useRef(null);
|
||||
|
||||
@ -205,6 +218,19 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
setForcedUnitsData(initializeUnitsData);
|
||||
setShowRadioSettings(false);
|
||||
setShowAdvancedSettings(false);
|
||||
|
||||
if (selectedUnits.length > 0) {
|
||||
setBarrelHeight(selectedUnits[0].getBarrelHeight());
|
||||
setMuzzleVelocity(selectedUnits[0].getMuzzleVelocity());
|
||||
setAimTime(selectedUnits[0].getAimTime());
|
||||
setShotsToFire(selectedUnits[0].getShotsToFire());
|
||||
setShotsBaseInterval(selectedUnits[0].getShotsBaseInterval());
|
||||
setShotsBaseScatter(selectedUnits[0].getShotsBaseScatter());
|
||||
setEngagementRange(selectedUnits[0].getEngagementRange());
|
||||
setTargetingRange(selectedUnits[0].getTargetingRange());
|
||||
setAimMethodRange(selectedUnits[0].getAimMethodRange());
|
||||
setAcquisitionRange(selectedUnits[0].getAcquisitionRange());
|
||||
}
|
||||
}, [selectedUnits]);
|
||||
|
||||
/* Count how many units are selected of each type, divided by coalition */
|
||||
@ -227,7 +253,9 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
const selectedCategories = getApp()?.getUnitsManager()?.getSelectedUnitsCategories() ?? [];
|
||||
|
||||
const filteredUnits = Object.values(getApp()?.getUnitsManager()?.getUnits() ?? {}).filter(
|
||||
(unit) => (unit.getUnitName().toLowerCase().indexOf(filterString.toLowerCase()) >= 0 || (unit.getBlueprint()?.label ?? "").toLowerCase()?.indexOf(filterString.toLowerCase()) >= 0 )
|
||||
(unit) =>
|
||||
unit.getUnitName().toLowerCase().indexOf(filterString.toLowerCase()) >= 0 ||
|
||||
(unit.getBlueprint()?.label ?? "").toLowerCase()?.indexOf(filterString.toLowerCase()) >= 0
|
||||
);
|
||||
|
||||
const everyUnitIsGround = selectedCategories.every((category) => {
|
||||
@ -269,23 +297,45 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
onClose={props.onClose}
|
||||
canBeHidden={true}
|
||||
wiki={() => {
|
||||
return <div className={`
|
||||
h-full flex-col overflow-auto p-4 text-gray-400 no-scrollbar flex
|
||||
gap-2
|
||||
`}>
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
h-full flex-col overflow-auto p-4 text-gray-400 no-scrollbar flex
|
||||
gap-2
|
||||
`}
|
||||
>
|
||||
<h2 className="mb-4 font-bold">Unit selection tool</h2>
|
||||
<div>
|
||||
The unit control menu serves two purposes. If no unit is currently selected, it allows you to select units based on their category, coalition, and control mode. You can also select units based on their specific type by using the search input.
|
||||
The unit control menu serves two purposes. If no unit is currently selected, it allows you to select units based on their category, coalition, and
|
||||
control mode. You can also select units based on their specific type by using the search input.
|
||||
</div>
|
||||
<h2 className="my-4 font-bold">Unit control tool</h2>
|
||||
<div>If units are selected, the menu will display the selected units and allow you to control their altitude, speed, rules of engagement, and other settings.</div>
|
||||
<div>The available controls depend on what type of unit is selected. Only controls applicable to every selected unit will be displayed, so make sure to refine your selection. </div>
|
||||
<div> You will be able to inspect the current values of the controls, e.g. the desired altitude, rules of engagement and so on. However, if multiple units are selected, you will only see the values of controls that are set to be the same for each selected unit.</div>
|
||||
<div> For example, if two airplanes are selected and they both have been instructed to fly at 1000ft, you will see the altitude slider set at that value. But if one airplane is set to fly at 1000ft and the other at 2000ft, you will see the slider display 'Different values'.</div>
|
||||
<div>
|
||||
If units are selected, the menu will display the selected units and allow you to control their altitude, speed, rules of engagement, and other
|
||||
settings.
|
||||
</div>
|
||||
<div>
|
||||
The available controls depend on what type of unit is selected. Only controls applicable to every selected unit will be displayed, so make sure to
|
||||
refine your selection.{" "}
|
||||
</div>
|
||||
<div>
|
||||
{" "}
|
||||
You will be able to inspect the current values of the controls, e.g. the desired altitude, rules of engagement and so on. However, if multiple
|
||||
units are selected, you will only see the values of controls that are set to be the same for each selected unit.
|
||||
</div>
|
||||
<div>
|
||||
{" "}
|
||||
For example, if two airplanes are selected and they both have been instructed to fly at 1000ft, you will see the altitude slider set at that
|
||||
value. But if one airplane is set to fly at 1000ft and the other at 2000ft, you will see the slider display 'Different values'.
|
||||
</div>
|
||||
<div> If at that point you move the slider, you will instruct both airplanes to fly at the same altitude.</div>
|
||||
<div> If a single unit is selected, you will also be able to see additional info on the unit, like its fuel level, position and altitude, tasking, and available ammunition. </div>
|
||||
<div>
|
||||
{" "}
|
||||
If a single unit is selected, you will also be able to see additional info on the unit, like its fuel level, position and altitude, tasking, and
|
||||
available ammunition.{" "}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
);
|
||||
}}
|
||||
>
|
||||
<>
|
||||
@ -365,9 +415,14 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
return (
|
||||
<tr key={idx}>
|
||||
<td className="flex gap-2 text-lg text-gray-200">
|
||||
<FontAwesomeIcon icon={entry[1][0] as IconDefinition} /> <div className={`
|
||||
<FontAwesomeIcon icon={entry[1][0] as IconDefinition} />{" "}
|
||||
<div
|
||||
className={`
|
||||
text-sm text-gray-400
|
||||
`}>{entry[1][1] as string}</div>
|
||||
`}
|
||||
>
|
||||
{entry[1][1] as string}
|
||||
</div>
|
||||
</td>
|
||||
{["blue", "neutral", "red"].map((coalition) => {
|
||||
return (
|
||||
@ -450,23 +505,25 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
onClick={() => {
|
||||
setSelectionID(unit.ID);
|
||||
}}
|
||||
|
||||
>
|
||||
<div data-coalition={unit.getCoalition()}
|
||||
className={`
|
||||
flex content-center justify-between border-l-4
|
||||
pl-2
|
||||
data-[coalition='blue']:border-blue-500
|
||||
data-[coalition='neutral']:border-gray-500
|
||||
data-[coalition='red']:border-red-500
|
||||
`}
|
||||
onMouseEnter={() => {
|
||||
unit.setHighlighted(true);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
unit.setHighlighted(false);
|
||||
}}
|
||||
>{unit.getUnitName()} ({unit.getBlueprint()?.label ?? ""})</div>
|
||||
<div
|
||||
data-coalition={unit.getCoalition()}
|
||||
className={`
|
||||
flex content-center justify-between border-l-4
|
||||
pl-2
|
||||
data-[coalition='blue']:border-blue-500
|
||||
data-[coalition='neutral']:border-gray-500
|
||||
data-[coalition='red']:border-red-500
|
||||
`}
|
||||
onMouseEnter={() => {
|
||||
unit.setHighlighted(true);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
unit.setHighlighted(false);
|
||||
}}
|
||||
>
|
||||
{unit.getUnitName()} ({unit.getBlueprint()?.label ?? ""})
|
||||
</div>
|
||||
</OlDropdownItem>
|
||||
);
|
||||
})}
|
||||
@ -740,53 +797,81 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
<div className="flex flex-col gap-2 px-2">
|
||||
<div className="flex content-center gap-2">
|
||||
{" "}
|
||||
<FontAwesomeIcon icon={olButtonsRoeHold} className={`
|
||||
<FontAwesomeIcon
|
||||
icon={olButtonsRoeHold}
|
||||
className={`
|
||||
my-auto min-w-8 text-white
|
||||
`} /> Hold fire: The unit will not shoot in
|
||||
any circumstance
|
||||
`}
|
||||
/>{" "}
|
||||
Hold fire: The unit will not shoot in any circumstance
|
||||
</div>
|
||||
<div className="flex content-center gap-2">
|
||||
{" "}
|
||||
<FontAwesomeIcon icon={olButtonsRoeReturn} className={`
|
||||
<FontAwesomeIcon
|
||||
icon={olButtonsRoeReturn}
|
||||
className={`
|
||||
my-auto min-w-8 text-white
|
||||
`} /> Return fire: The unit will not fire
|
||||
unless fired upon
|
||||
`}
|
||||
/>{" "}
|
||||
Return fire: The unit will not fire unless fired upon
|
||||
</div>
|
||||
<div className="flex content-center gap-2">
|
||||
{" "}
|
||||
<FontAwesomeIcon icon={olButtonsRoeDesignated} className={`
|
||||
<FontAwesomeIcon
|
||||
icon={olButtonsRoeDesignated}
|
||||
className={`
|
||||
my-auto min-w-8 text-white
|
||||
`} />{" "}
|
||||
`}
|
||||
/>{" "}
|
||||
<div>
|
||||
{" "}
|
||||
Fire on target: The unit will not fire unless fired upon <p className={`
|
||||
Fire on target: The unit will not fire unless fired upon{" "}
|
||||
<p
|
||||
className={`
|
||||
inline font-bold
|
||||
`}>or</p> ordered to do so{" "}
|
||||
`}
|
||||
>
|
||||
or
|
||||
</p>{" "}
|
||||
ordered to do so{" "}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex content-center gap-2">
|
||||
{" "}
|
||||
<FontAwesomeIcon icon={olButtonsRoeFree} className={`
|
||||
<FontAwesomeIcon
|
||||
icon={olButtonsRoeFree}
|
||||
className={`
|
||||
my-auto min-w-8 text-white
|
||||
`} /> Free: The unit will fire at any
|
||||
detected enemy in range
|
||||
`}
|
||||
/>{" "}
|
||||
Free: The unit will fire at any detected enemy in range
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
<div className="my-auto">
|
||||
<FaExclamationCircle className={`
|
||||
<FaExclamationCircle
|
||||
className={`
|
||||
animate-bounce text-xl
|
||||
`} />
|
||||
`}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
Currently, DCS blue and red ground units do not respect{" "}
|
||||
<FontAwesomeIcon icon={olButtonsRoeReturn} className={`
|
||||
<FontAwesomeIcon
|
||||
icon={olButtonsRoeReturn}
|
||||
className={`
|
||||
my-auto text-white
|
||||
`} /> and{" "}
|
||||
<FontAwesomeIcon icon={olButtonsRoeDesignated} className={`
|
||||
`}
|
||||
/>{" "}
|
||||
and{" "}
|
||||
<FontAwesomeIcon
|
||||
icon={olButtonsRoeDesignated}
|
||||
className={`
|
||||
my-auto text-white
|
||||
`} /> rules of engagement, so be careful, they
|
||||
may start shooting when you don't want them to. Use neutral units for finer control.
|
||||
`}
|
||||
/>{" "}
|
||||
rules of engagement, so be careful, they may start shooting when you don't want them to. Use neutral units for finer
|
||||
control.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -842,31 +927,43 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
<div className="flex flex-col gap-2 px-2">
|
||||
<div className="flex content-center gap-2">
|
||||
{" "}
|
||||
<FontAwesomeIcon icon={olButtonsThreatNone} className={`
|
||||
<FontAwesomeIcon
|
||||
icon={olButtonsThreatNone}
|
||||
className={`
|
||||
my-auto min-w-8 text-white
|
||||
`} /> No reaction: The unit will not
|
||||
react in any circumstance
|
||||
`}
|
||||
/>{" "}
|
||||
No reaction: The unit will not react in any circumstance
|
||||
</div>
|
||||
<div className="flex content-center gap-2">
|
||||
{" "}
|
||||
<FontAwesomeIcon icon={olButtonsThreatPassive} className={`
|
||||
<FontAwesomeIcon
|
||||
icon={olButtonsThreatPassive}
|
||||
className={`
|
||||
my-auto min-w-8 text-white
|
||||
`} /> Passive: The unit will use
|
||||
counter-measures, but will not alter its course
|
||||
`}
|
||||
/>{" "}
|
||||
Passive: The unit will use counter-measures, but will not alter its course
|
||||
</div>
|
||||
<div className="flex content-center gap-2">
|
||||
{" "}
|
||||
<FontAwesomeIcon icon={olButtonsThreatManoeuvre} className={`
|
||||
<FontAwesomeIcon
|
||||
icon={olButtonsThreatManoeuvre}
|
||||
className={`
|
||||
my-auto min-w-8 text-white
|
||||
`} /> Manouevre: The unit will try
|
||||
to evade the threat using manoeuvres, but no counter-measures
|
||||
`}
|
||||
/>{" "}
|
||||
Manouevre: The unit will try to evade the threat using manoeuvres, but no counter-measures
|
||||
</div>
|
||||
<div className="flex content-center gap-2">
|
||||
{" "}
|
||||
<FontAwesomeIcon icon={olButtonsThreatEvade} className={`
|
||||
<FontAwesomeIcon
|
||||
icon={olButtonsThreatEvade}
|
||||
className={`
|
||||
my-auto min-w-8 text-white
|
||||
`} /> Full evasion: the unit will try
|
||||
to evade the threat both manoeuvering and using counter-measures
|
||||
`}
|
||||
/>{" "}
|
||||
Full evasion: the unit will try to evade the threat both manoeuvering and using counter-measures
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -917,31 +1014,43 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
<div className="flex flex-col gap-2 px-2">
|
||||
<div className="flex content-center gap-2">
|
||||
{" "}
|
||||
<FontAwesomeIcon icon={olButtonsEmissionsSilent} className={`
|
||||
<FontAwesomeIcon
|
||||
icon={olButtonsEmissionsSilent}
|
||||
className={`
|
||||
my-auto min-w-8 text-white
|
||||
`} /> Radio silence: No radar or
|
||||
ECM will be used
|
||||
`}
|
||||
/>{" "}
|
||||
Radio silence: No radar or ECM will be used
|
||||
</div>
|
||||
<div className="flex content-center gap-2">
|
||||
{" "}
|
||||
<FontAwesomeIcon icon={olButtonsEmissionsDefend} className={`
|
||||
<FontAwesomeIcon
|
||||
icon={olButtonsEmissionsDefend}
|
||||
className={`
|
||||
my-auto min-w-8 text-white
|
||||
`} /> Defensive: The unit will turn
|
||||
radar and ECM on only when threatened
|
||||
`}
|
||||
/>{" "}
|
||||
Defensive: The unit will turn radar and ECM on only when threatened
|
||||
</div>
|
||||
<div className="flex content-center gap-2">
|
||||
{" "}
|
||||
<FontAwesomeIcon icon={olButtonsEmissionsAttack} className={`
|
||||
<FontAwesomeIcon
|
||||
icon={olButtonsEmissionsAttack}
|
||||
className={`
|
||||
my-auto min-w-8 text-white
|
||||
`} /> Attack: The unit will use
|
||||
radar and ECM when engaging other units
|
||||
`}
|
||||
/>{" "}
|
||||
Attack: The unit will use radar and ECM when engaging other units
|
||||
</div>
|
||||
<div className="flex content-center gap-2">
|
||||
{" "}
|
||||
<FontAwesomeIcon icon={olButtonsEmissionsFree} className={`
|
||||
<FontAwesomeIcon
|
||||
icon={olButtonsEmissionsFree}
|
||||
className={`
|
||||
my-auto min-w-8 text-white
|
||||
`} /> Free: the unit will use the
|
||||
radar and ECM all the time
|
||||
`}
|
||||
/>{" "}
|
||||
Free: the unit will use the radar and ECM all the time
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1159,9 +1268,11 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
>
|
||||
<div className="flex gap-4">
|
||||
<div className="my-auto">
|
||||
<FaExclamationCircle className={`
|
||||
<FaExclamationCircle
|
||||
className={`
|
||||
animate-bounce text-xl
|
||||
`} />
|
||||
`}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
Currently, DCS blue and red ground units do not respect their rules of engagement, so be careful, they may start shooting when
|
||||
@ -1186,15 +1297,27 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
<OlToggle
|
||||
toggled={selectedUnitsData.scenicAAA}
|
||||
onClick={() => {
|
||||
getApp()
|
||||
.getUnitsManager()
|
||||
.scenicAAA(null, () =>
|
||||
setForcedUnitsData({
|
||||
...forcedUnitsData,
|
||||
scenicAAA: !selectedUnitsData.scenicAAA,
|
||||
missOnPurpose: false,
|
||||
})
|
||||
);
|
||||
if (selectedUnitsData.scenicAAA) {
|
||||
getApp()
|
||||
.getUnitsManager()
|
||||
.stop(null, () =>
|
||||
setForcedUnitsData({
|
||||
...forcedUnitsData,
|
||||
missOnPurpose: false,
|
||||
scenicAAA: false,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
getApp()
|
||||
.getUnitsManager()
|
||||
.scenicAAA(null, () =>
|
||||
setForcedUnitsData({
|
||||
...forcedUnitsData,
|
||||
missOnPurpose: false,
|
||||
scenicAAA: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
}}
|
||||
tooltip={() => (
|
||||
<OlExpandingTooltip
|
||||
@ -1219,15 +1342,27 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
<OlToggle
|
||||
toggled={selectedUnitsData.missOnPurpose}
|
||||
onClick={() => {
|
||||
getApp()
|
||||
.getUnitsManager()
|
||||
.missOnPurpose(null, () =>
|
||||
setForcedUnitsData({
|
||||
...forcedUnitsData,
|
||||
scenicAAA: false,
|
||||
missOnPurpose: !selectedUnitsData.missOnPurpose,
|
||||
})
|
||||
);
|
||||
if (selectedUnitsData.missOnPurpose) {
|
||||
getApp()
|
||||
.getUnitsManager()
|
||||
.stop(null, () =>
|
||||
setForcedUnitsData({
|
||||
...forcedUnitsData,
|
||||
scenicAAA: false,
|
||||
missOnPurpose: false,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
getApp()
|
||||
.getUnitsManager()
|
||||
.missOnPurpose(null, () =>
|
||||
setForcedUnitsData({
|
||||
...forcedUnitsData,
|
||||
scenicAAA: false,
|
||||
missOnPurpose: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
}}
|
||||
tooltip={() => (
|
||||
<OlExpandingTooltip
|
||||
@ -1306,11 +1441,19 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
</OlButtonGroup>
|
||||
</div>
|
||||
{/* ============== Shots intensity END ============== */}
|
||||
<OlStateButton
|
||||
className="mt-auto"
|
||||
checked={showEngagementSettings}
|
||||
onClick={() => setShowEngagementSettings(!showEngagementSettings)}
|
||||
icon={faCog}
|
||||
></OlStateButton>
|
||||
</div>
|
||||
{/* ============== Operate as toggle START ============== */}
|
||||
{selectedUnits.every((unit) => unit.getCoalition() === "neutral") && (
|
||||
<div
|
||||
className={`flex content-center justify-between`}
|
||||
className={`
|
||||
flex content-center justify-between
|
||||
`}
|
||||
>
|
||||
<span
|
||||
className={`
|
||||
@ -1344,6 +1487,371 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
</div>
|
||||
)}
|
||||
{/* ============== Operate as toggle END ============== */}
|
||||
{showEngagementSettings && (
|
||||
<div
|
||||
className={`
|
||||
flex flex-col gap-2 text-sm text-gray-200
|
||||
`}
|
||||
>
|
||||
<div className="flex align-center gap-2">
|
||||
<div
|
||||
className={`
|
||||
my-auto
|
||||
`}
|
||||
>
|
||||
Barrel height:{" "}
|
||||
</div>
|
||||
<OlNumberInput
|
||||
decimalPlaces={1}
|
||||
className={`
|
||||
ml-auto
|
||||
`}
|
||||
value={barrelHeight}
|
||||
min={0}
|
||||
max={100}
|
||||
onChange={(ev) => {
|
||||
setBarrelHeight(Number(ev.target.value));
|
||||
}}
|
||||
onIncrease={() => {
|
||||
setBarrelHeight(barrelHeight + 0.1);
|
||||
}}
|
||||
onDecrease={() => {
|
||||
setBarrelHeight(barrelHeight - 0.1);
|
||||
}}
|
||||
></OlNumberInput>
|
||||
<div
|
||||
className={`
|
||||
my-auto
|
||||
`}
|
||||
>
|
||||
m
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex align-center gap-2">
|
||||
<div
|
||||
className={`
|
||||
my-auto
|
||||
`}
|
||||
>
|
||||
Muzzle velocity:{" "}
|
||||
</div>
|
||||
<OlNumberInput
|
||||
decimalPlaces={0}
|
||||
className={`
|
||||
ml-auto
|
||||
`}
|
||||
value={muzzleVelocity}
|
||||
min={0}
|
||||
max={10000}
|
||||
onChange={(ev) => {
|
||||
setMuzzleVelocity(Number(ev.target.value));
|
||||
}}
|
||||
onIncrease={() => {
|
||||
setMuzzleVelocity(muzzleVelocity + 10);
|
||||
}}
|
||||
onDecrease={() => {
|
||||
setMuzzleVelocity(muzzleVelocity - 10);
|
||||
}}
|
||||
></OlNumberInput>
|
||||
<div
|
||||
className={`
|
||||
my-auto
|
||||
`}
|
||||
>
|
||||
m/s
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex align-center gap-2">
|
||||
<div
|
||||
className={`
|
||||
my-auto
|
||||
`}
|
||||
>
|
||||
Aim time:{" "}
|
||||
</div>
|
||||
<OlNumberInput
|
||||
decimalPlaces={2}
|
||||
className={`
|
||||
ml-auto
|
||||
`}
|
||||
value={aimTime}
|
||||
min={0}
|
||||
max={100}
|
||||
onChange={(ev) => {
|
||||
setAimTime(Number(ev.target.value));
|
||||
}}
|
||||
onIncrease={() => {
|
||||
setAimTime(aimTime + 0.1);
|
||||
}}
|
||||
onDecrease={() => {
|
||||
setAimTime(aimTime - 0.1);
|
||||
}}
|
||||
></OlNumberInput>
|
||||
<div
|
||||
className={`
|
||||
my-auto
|
||||
`}
|
||||
>
|
||||
s
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex align-center gap-2">
|
||||
<div
|
||||
className={`
|
||||
my-auto
|
||||
`}
|
||||
>
|
||||
Shots to fire:{" "}
|
||||
</div>
|
||||
<OlNumberInput
|
||||
className={`
|
||||
ml-auto
|
||||
`}
|
||||
value={shotsToFire}
|
||||
min={0}
|
||||
max={100}
|
||||
onChange={(ev) => {
|
||||
setShotsToFire(Number(ev.target.value));
|
||||
}}
|
||||
onIncrease={() => {
|
||||
setShotsToFire(shotsToFire + 1);
|
||||
}}
|
||||
onDecrease={() => {
|
||||
setShotsToFire(shotsToFire - 1);
|
||||
}}
|
||||
></OlNumberInput>
|
||||
</div>
|
||||
<div className="flex align-center gap-2">
|
||||
<div
|
||||
className={`
|
||||
my-auto
|
||||
`}
|
||||
>
|
||||
Shots base interval:{" "}
|
||||
</div>
|
||||
<OlNumberInput
|
||||
decimalPlaces={2}
|
||||
className={`
|
||||
ml-auto
|
||||
`}
|
||||
value={shotsBaseInterval}
|
||||
min={0}
|
||||
max={100}
|
||||
onChange={(ev) => {
|
||||
setShotsBaseInterval(Number(ev.target.value));
|
||||
}}
|
||||
onIncrease={() => {
|
||||
setShotsBaseInterval(shotsBaseInterval + 0.1);
|
||||
}}
|
||||
onDecrease={() => {
|
||||
setShotsBaseInterval(shotsBaseInterval - 0.1);
|
||||
}}
|
||||
></OlNumberInput>
|
||||
<div
|
||||
className={`
|
||||
my-auto
|
||||
`}
|
||||
>
|
||||
s
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex align-center gap-2">
|
||||
<div
|
||||
className={`
|
||||
my-auto
|
||||
`}
|
||||
>
|
||||
Shots base scatter:{" "}
|
||||
</div>
|
||||
<OlNumberInput
|
||||
decimalPlaces={2}
|
||||
className={`
|
||||
ml-auto
|
||||
`}
|
||||
value={shotsBaseScatter}
|
||||
min={0}
|
||||
max={50}
|
||||
onChange={(ev) => {
|
||||
setShotsBaseScatter(Number(ev.target.value));
|
||||
}}
|
||||
onIncrease={() => {
|
||||
setShotsBaseScatter(shotsBaseScatter + 0.1);
|
||||
}}
|
||||
onDecrease={() => {
|
||||
setShotsBaseScatter(shotsBaseScatter - 0.1);
|
||||
}}
|
||||
></OlNumberInput>
|
||||
<div
|
||||
className={`
|
||||
my-auto
|
||||
`}
|
||||
>
|
||||
deg
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex align-center gap-2">
|
||||
<div
|
||||
className={`
|
||||
my-auto
|
||||
`}
|
||||
>
|
||||
Engagement range:{" "}
|
||||
</div>
|
||||
<OlNumberInput
|
||||
className={`
|
||||
ml-auto
|
||||
`}
|
||||
value={engagementRange}
|
||||
min={0}
|
||||
max={100000}
|
||||
onChange={(ev) => {
|
||||
setEngagementRange(Number(ev.target.value));
|
||||
}}
|
||||
onIncrease={() => {
|
||||
setEngagementRange(engagementRange + 100);
|
||||
}}
|
||||
onDecrease={() => {
|
||||
setEngagementRange(engagementRange - 100);
|
||||
}}
|
||||
></OlNumberInput>
|
||||
<div
|
||||
className={`
|
||||
my-auto
|
||||
`}
|
||||
>
|
||||
m
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex align-center gap-2">
|
||||
<div
|
||||
className={`
|
||||
my-auto
|
||||
`}
|
||||
>
|
||||
Targeting range:{" "}
|
||||
</div>
|
||||
<OlNumberInput
|
||||
className={`
|
||||
ml-auto
|
||||
`}
|
||||
value={targetingRange}
|
||||
min={0}
|
||||
max={100000}
|
||||
onChange={(ev) => {
|
||||
setTargetingRange(Number(ev.target.value));
|
||||
}}
|
||||
onIncrease={() => {
|
||||
setTargetingRange(targetingRange + 100);
|
||||
}}
|
||||
onDecrease={() => {
|
||||
setTargetingRange(targetingRange - 100);
|
||||
}}
|
||||
></OlNumberInput>
|
||||
<div
|
||||
className={`
|
||||
my-auto
|
||||
`}
|
||||
>
|
||||
m
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex align-center gap-2">
|
||||
<div
|
||||
className={`
|
||||
my-auto
|
||||
`}
|
||||
>
|
||||
Aim method range:{" "}
|
||||
</div>
|
||||
<OlNumberInput
|
||||
className={`
|
||||
ml-auto
|
||||
`}
|
||||
value={aimMethodRange}
|
||||
min={0}
|
||||
max={100000}
|
||||
onChange={(ev) => {
|
||||
setAimMethodRange(Number(ev.target.value));
|
||||
}}
|
||||
onIncrease={() => {
|
||||
setAimMethodRange(aimMethodRange + 100);
|
||||
}}
|
||||
onDecrease={() => {
|
||||
setAimMethodRange(aimMethodRange - 100);
|
||||
}}
|
||||
></OlNumberInput>
|
||||
<div
|
||||
className={`
|
||||
my-auto
|
||||
`}
|
||||
>
|
||||
m
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex align-center gap-2">
|
||||
<div
|
||||
className={`
|
||||
my-auto
|
||||
`}
|
||||
>
|
||||
Acquisition range:{" "}
|
||||
</div>
|
||||
<OlNumberInput
|
||||
className={`
|
||||
ml-auto
|
||||
`}
|
||||
value={acquisitionRange}
|
||||
min={0}
|
||||
max={100000}
|
||||
onChange={(ev) => {
|
||||
setAcquisitionRange(Number(ev.target.value));
|
||||
}}
|
||||
onIncrease={() => {
|
||||
setAcquisitionRange(acquisitionRange + 100);
|
||||
}}
|
||||
onDecrease={() => {
|
||||
setAcquisitionRange(acquisitionRange - 100);
|
||||
}}
|
||||
></OlNumberInput>
|
||||
<div
|
||||
className={`
|
||||
my-auto
|
||||
`}
|
||||
>
|
||||
m
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className={`
|
||||
mb-2 me-2 rounded-sm bg-blue-700 px-5 py-2.5
|
||||
text-md font-medium text-white
|
||||
dark:bg-blue-600 dark:hover:bg-blue-700
|
||||
dark:focus:ring-blue-800
|
||||
focus:outline-none focus:ring-4
|
||||
focus:ring-blue-300
|
||||
hover:bg-blue-800
|
||||
`}
|
||||
onClick={() => {
|
||||
getApp()
|
||||
.getUnitsManager()
|
||||
.setEngagementProperties(
|
||||
barrelHeight,
|
||||
muzzleVelocity,
|
||||
aimTime,
|
||||
shotsToFire,
|
||||
shotsBaseInterval,
|
||||
shotsBaseScatter,
|
||||
engagementRange,
|
||||
targetingRange,
|
||||
aimMethodRange,
|
||||
acquisitionRange
|
||||
);
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@ -1825,14 +2333,18 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="my-auto text-sm text-gray-400">
|
||||
{selectedUnits[0].getTask()}
|
||||
</div>
|
||||
<div className="my-auto text-sm text-gray-400">{selectedUnits[0].getTask()}</div>
|
||||
{([UnitState.SIMULATE_FIRE_FIGHT, UnitState.MISS_ON_PURPOSE, UnitState.SCENIC_AAA] as string[]).includes(selectedUnits[0].getState()) && (
|
||||
<div className="my-auto text-sm text-gray-400">
|
||||
Time to next tasking: {zeroAppend(selectedUnits[0].getTimeToNextTasking(), 0, true, 2)}s
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex content-center gap-2">
|
||||
<OlLocation location={selectedUnits[0].getPosition()} className={`
|
||||
w-[280px] text-sm
|
||||
`}/>
|
||||
<div className="my-auto text-gray-200">{Math.round(mToFt(selectedUnits[0].getPosition().alt ?? 0))} ft</div>
|
||||
<OlLocation location={selectedUnits[0].getPosition()} className={`
|
||||
w-[280px] text-sm
|
||||
`} />
|
||||
<div className="my-auto text-gray-200">{Math.round(mToFt(selectedUnits[0].getPosition().alt ?? 0))} ft</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -177,6 +177,17 @@ export abstract class Unit extends CustomMarker {
|
||||
#spotLines: { [key: number]: Polyline } = {};
|
||||
#spotEditMarkers: { [key: number]: SpotEditMarker } = {};
|
||||
#spotMarkers: { [key: number]: SpotMarker } = {};
|
||||
#timeToNextTasking: number = 0;
|
||||
#barrelHeight: number = 0;
|
||||
#muzzleVelocity: number = 0;
|
||||
#aimTime: number = 0;
|
||||
#shotsToFire: number = 0;
|
||||
#shotsBaseInterval: number = 0;
|
||||
#shotsBaseScatter: number = 0;
|
||||
#engagementRange: number = 0;
|
||||
#targetingRange: number = 0;
|
||||
#aimMethodRange: number = 0;
|
||||
#acquisitionRange: number = 0;
|
||||
|
||||
/* Inputs timers */
|
||||
#debounceTimeout: number | null = null;
|
||||
@ -332,6 +343,39 @@ export abstract class Unit extends CustomMarker {
|
||||
getRaceTrackBearing() {
|
||||
return this.#racetrackBearing;
|
||||
}
|
||||
getTimeToNextTasking() {
|
||||
return this.#timeToNextTasking;
|
||||
}
|
||||
getBarrelHeight() {
|
||||
return this.#barrelHeight;
|
||||
}
|
||||
getMuzzleVelocity() {
|
||||
return this.#muzzleVelocity;
|
||||
}
|
||||
getAimTime() {
|
||||
return this.#aimTime;
|
||||
}
|
||||
getShotsToFire() {
|
||||
return this.#shotsToFire;
|
||||
}
|
||||
getShotsBaseInterval() {
|
||||
return this.#shotsBaseInterval;
|
||||
}
|
||||
getShotsBaseScatter() {
|
||||
return this.#shotsBaseScatter;
|
||||
}
|
||||
getEngagementRange() {
|
||||
return this.#engagementRange;
|
||||
}
|
||||
getTargetingRange() {
|
||||
return this.#targetingRange;
|
||||
}
|
||||
getAimMethodRange() {
|
||||
return this.#aimMethodRange;
|
||||
}
|
||||
getAcquisitionRange() {
|
||||
return this.#acquisitionRange;
|
||||
}
|
||||
|
||||
static getConstructor(type: string) {
|
||||
if (type === "GroundUnit") return GroundUnit;
|
||||
@ -644,6 +688,41 @@ export abstract class Unit extends CustomMarker {
|
||||
case DataIndexes.racetrackBearing:
|
||||
this.#racetrackBearing = dataExtractor.extractFloat64();
|
||||
break;
|
||||
case DataIndexes.timeToNextTasking:
|
||||
this.#timeToNextTasking = dataExtractor.extractFloat64();
|
||||
break;
|
||||
case DataIndexes.barrelHeight:
|
||||
this.#barrelHeight = dataExtractor.extractFloat64();
|
||||
break;
|
||||
case DataIndexes.muzzleVelocity:
|
||||
this.#muzzleVelocity = dataExtractor.extractFloat64();
|
||||
break;
|
||||
case DataIndexes.aimTime:
|
||||
this.#aimTime = dataExtractor.extractFloat64();
|
||||
break;
|
||||
case DataIndexes.shotsToFire:
|
||||
this.#shotsToFire = dataExtractor.extractUInt32();
|
||||
break;
|
||||
case DataIndexes.shotsBaseInterval:
|
||||
this.#shotsBaseInterval = dataExtractor.extractFloat64();
|
||||
break;
|
||||
case DataIndexes.shotsBaseScatter:
|
||||
this.#shotsBaseScatter = dataExtractor.extractFloat64();
|
||||
break;
|
||||
case DataIndexes.engagementRange:
|
||||
this.#engagementRange = dataExtractor.extractFloat64();
|
||||
break;
|
||||
case DataIndexes.targetingRange:
|
||||
this.#targetingRange = dataExtractor.extractFloat64();
|
||||
break;
|
||||
case DataIndexes.aimMethodRange:
|
||||
this.#aimMethodRange = dataExtractor.extractFloat64();
|
||||
break;
|
||||
case DataIndexes.acquisitionRange:
|
||||
this.#acquisitionRange = dataExtractor.extractFloat64();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@ -750,6 +829,17 @@ export abstract class Unit extends CustomMarker {
|
||||
racetrackLength: this.#racetrackLength,
|
||||
racetrackAnchor: this.#racetrackAnchor,
|
||||
racetrackBearing: this.#racetrackBearing,
|
||||
timeToNextTasking: this.#timeToNextTasking,
|
||||
barrelHeight: this.#barrelHeight,
|
||||
muzzleVelocity: this.#muzzleVelocity,
|
||||
aimTime: this.#aimTime,
|
||||
shotsToFire: this.#shotsToFire,
|
||||
shotsBaseInterval: this.#shotsBaseInterval,
|
||||
shotsBaseScatter: this.#shotsBaseScatter,
|
||||
engagementRange: this.#engagementRange,
|
||||
targetingRange: this.#targetingRange,
|
||||
aimMethodRange: this.#aimMethodRange,
|
||||
acquisitionRange: this.#acquisitionRange,
|
||||
};
|
||||
}
|
||||
|
||||
@ -1307,6 +1397,36 @@ export abstract class Unit extends CustomMarker {
|
||||
if (!this.#human) getApp().getServerManager().setAdvancedOptions(this.ID, isActiveTanker, isActiveAWACS, TACAN, radio, generalSettings);
|
||||
}
|
||||
|
||||
setEngagementProperties(
|
||||
barrelHeight: number,
|
||||
muzzleVelocity: number,
|
||||
aimTime: number,
|
||||
shotsToFire: number,
|
||||
shotsBaseInterval: number,
|
||||
shotsBaseScatter: number,
|
||||
engagementRange: number,
|
||||
targetingRange: number,
|
||||
aimMethodRange: number,
|
||||
acquisitionRange: number
|
||||
) {
|
||||
if (!this.#human)
|
||||
getApp()
|
||||
.getServerManager()
|
||||
.setEngagementProperties(
|
||||
this.ID,
|
||||
barrelHeight,
|
||||
muzzleVelocity,
|
||||
aimTime,
|
||||
shotsToFire,
|
||||
shotsBaseInterval,
|
||||
shotsBaseScatter,
|
||||
engagementRange,
|
||||
targetingRange,
|
||||
aimMethodRange,
|
||||
acquisitionRange
|
||||
);
|
||||
}
|
||||
|
||||
bombPoint(latlng: LatLng) {
|
||||
getApp().getServerManager().bombPoint(this.ID, latlng);
|
||||
}
|
||||
|
||||
@ -546,6 +546,7 @@ export class UnitsManager {
|
||||
units = units.filter((unit) => !unit.getHuman());
|
||||
|
||||
let callback = (units: Unit[]) => {
|
||||
onExecution();
|
||||
for (let idx in units) {
|
||||
getApp().getServerManager().addDestination(units[idx].ID, []);
|
||||
}
|
||||
@ -819,16 +820,24 @@ export class UnitsManager {
|
||||
}
|
||||
|
||||
/** Set the advanced options for the selected units
|
||||
*
|
||||
*
|
||||
* @param isActiveTanker If true, the unit will be a tanker
|
||||
* @param isActiveAWACS If true, the unit will be an AWACS
|
||||
* @param TACAN TACAN settings
|
||||
* @param radio Radio settings
|
||||
* @param generalSettings General settings
|
||||
* @param units (Optional) Array of units to apply the control to. If not provided, the operation will be completed on all selected units.
|
||||
* @param onExecution Function to execute after the operation is completed
|
||||
*/
|
||||
setAdvancedOptions(isActiveTanker: boolean, isActiveAWACS: boolean, TACAN: TACAN, radio: Radio, generalSettings: GeneralSettings, units: Unit[] | null = null, onExecution: () => void = () => {}) {
|
||||
* @param onExecution Function to execute after the operation is completed
|
||||
*/
|
||||
setAdvancedOptions(
|
||||
isActiveTanker: boolean,
|
||||
isActiveAWACS: boolean,
|
||||
TACAN: TACAN,
|
||||
radio: Radio,
|
||||
generalSettings: GeneralSettings,
|
||||
units: Unit[] | null = null,
|
||||
onExecution: () => void = () => {}
|
||||
) {
|
||||
if (units === null) units = this.getSelectedUnits();
|
||||
units = units.filter((unit) => !unit.getHuman());
|
||||
let callback = (units) => {
|
||||
@ -836,7 +845,49 @@ export class UnitsManager {
|
||||
units.forEach((unit: Unit) => unit.setAdvancedOptions(isActiveTanker, isActiveAWACS, TACAN, radio, generalSettings));
|
||||
this.#showActionMessage(units, `advanced options set`);
|
||||
};
|
||||
|
||||
|
||||
if (getApp().getMap().getOptions().protectDCSUnits && !units.every((unit) => unit.isControlledByOlympus())) {
|
||||
getApp().setState(OlympusState.UNIT_CONTROL, UnitControlSubState.PROTECTION);
|
||||
this.#protectionCallback = callback;
|
||||
} else callback(units);
|
||||
}
|
||||
|
||||
//TODO
|
||||
setEngagementProperties(
|
||||
barrelHeight: number,
|
||||
muzzleVelocity: number,
|
||||
aimTime: number,
|
||||
shotsToFire: number,
|
||||
shotsBaseInterval: number,
|
||||
shotsBaseScatter: number,
|
||||
engagementRange: number,
|
||||
targetingRange: number,
|
||||
aimMethodRange: number,
|
||||
acquisitionRange: number,
|
||||
units: Unit[] | null = null,
|
||||
onExecution: () => void = () => {}
|
||||
) {
|
||||
if (units === null) units = this.getSelectedUnits();
|
||||
units = units.filter((unit) => !unit.getHuman());
|
||||
let callback = (units) => {
|
||||
onExecution();
|
||||
units.forEach((unit: Unit) =>
|
||||
unit.setEngagementProperties(
|
||||
barrelHeight,
|
||||
muzzleVelocity,
|
||||
aimTime,
|
||||
shotsToFire,
|
||||
shotsBaseInterval,
|
||||
shotsBaseScatter,
|
||||
engagementRange,
|
||||
targetingRange,
|
||||
aimMethodRange,
|
||||
acquisitionRange
|
||||
)
|
||||
);
|
||||
this.#showActionMessage(units, `engagement parameters set`);
|
||||
};
|
||||
|
||||
if (getApp().getMap().getOptions().protectDCSUnits && !units.every((unit) => unit.isControlledByOlympus())) {
|
||||
getApp().setState(OlympusState.UNIT_CONTROL, UnitControlSubState.PROTECTION);
|
||||
this.#protectionCallback = callback;
|
||||
|
||||
@ -66,8 +66,8 @@ async function installMod(folder, name) {
|
||||
logger.log(`Mod succesfully installed in ${folder}`)
|
||||
|
||||
/* Check if backup user-editable files exist. If true copy them over */
|
||||
logger.log(path.join(__dirname, "..", "..", "..", "DCS Olympus backups", name, "databases"));
|
||||
if (await exists(path.join(__dirname, "..", "..", "..", "DCS Olympus backups", name, "databases"))) {
|
||||
logger.log(path.join(__dirname, "..", "..", "..", "DCS Olympus backups", name, "databases", "units", "mods.json"));
|
||||
if (await exists(path.join(__dirname, "..", "..", "..", "DCS Olympus backups", name, "databases", "units", "mods.json"))) {
|
||||
logger.log("Backup databases found, copying over");
|
||||
|
||||
// Changed in v2.0.0, only the mods database is copied over, if present
|
||||
|
||||
@ -1,3 +1,11 @@
|
||||
v2.0.0 ====================
|
||||
Changes:
|
||||
1) completely redone UI using React
|
||||
2) added audio backend for SRS integration
|
||||
3) added Mission Editor drawings in DCS
|
||||
4) multiple enhancements to control scheme
|
||||
|
||||
|
||||
v1.0.4 ====================
|
||||
Changes:
|
||||
1) Added Olympus Manager for unified configuration management;
|
||||
|
||||
71
olympus.json
71
olympus.json
@ -1,53 +1,53 @@
|
||||
{
|
||||
"_comment1": "These are the address and port of the backend server, i.e. the server run by the Olympus dll mod.",
|
||||
"_comment2": "localhost should be used if the backend is running on the same machine as the frontend server, which is usually the case.",
|
||||
"_comment3": "If a direct connection is desired, e.g. for API usage, use '*' as address.",
|
||||
"_comment4": "The desired port should be available and not used by other processes.",
|
||||
"backend": {
|
||||
"_comment1": "These are the address and port of the backend server, i.e. the server run by the Olympus dll mod.",
|
||||
"_comment2": "localhost should be used if the backend is running on the same machine as the frontend server, which is usually the case.",
|
||||
"_comment3": "If a direct connection is desired, e.g. for API usage, use '*' as address.",
|
||||
"_comment4": "The desired port should be available and not used by other processes.",
|
||||
|
||||
"address": "localhost",
|
||||
"port": 4512
|
||||
},
|
||||
"authentication": {
|
||||
"_comment1": "These are the sha256 hashed passwords for the game master, the two commanders, and the admin. They are used to authenticate the users.",
|
||||
|
||||
"_comment5": "These are the sha256 hashed passwords for the game master, the two commanders, and the admin. They are used to authenticate the users.",
|
||||
"authentication": {
|
||||
"gameMasterPassword": "",
|
||||
"blueCommanderPassword": "",
|
||||
"redCommanderPassword": "",
|
||||
"admin": ""
|
||||
},
|
||||
"frontend": {
|
||||
"_comment1": "These are the settings for the frontend server, i.e. the server which hosts the Olympus GUI web interface.",
|
||||
"_comment2": "The port should be available and not used by other processes and is used to load the interface.",
|
||||
|
||||
"_comment6": "These are the settings for the frontend server, i.e. the server which hosts the Olympus GUI web interface.",
|
||||
"_comment7": "The port should be available and not used by other processes and is used to load the interface.",
|
||||
"frontend": {
|
||||
"port": 3000,
|
||||
|
||||
"_comment8": "These are the custom headers used for authentication. They are used to authenticate the users and skip the login page",
|
||||
"_comment9": "If enabled, the frontend server will look for the specified headers in the request and use them to authenticate the user.",
|
||||
"_comment10": "The username header should contain the username and the group header should contain the group of the user.",
|
||||
"_comment11": "If the headers are not present or the user is not authenticated, the user will be redirected to the login page.",
|
||||
"_comment12": "This is useful for integrating Olympus with other systems, e.g. a SSO provider",
|
||||
"_comment13": "The group should be one of the groups defined using the admin page on the web interface",
|
||||
"_comment14": "If the user is by default authorized to more than one command mode, x-command-mode header can be used to specify the default command mode",
|
||||
"_comment15": "Otherwise, the login page will be skipped, but a command more selection page will still be shown",
|
||||
"customAuthHeaders": {
|
||||
"_comment1": "These are the custom headers used for authentication. They are used to authenticate the users and skip the login page",
|
||||
"_comment2": "If enabled, the frontend server will look for the specified headers in the request and use them to authenticate the user.",
|
||||
"_comment3": "The username header should contain the username and the group header should contain the group of the user.",
|
||||
"_comment4": "If the headers are not present or the user is not authenticated, the user will be redirected to the login page.",
|
||||
"_comment5": "This is useful for integrating Olympus with other systems, e.g. a SSO provider",
|
||||
"_comment6": "The group should be one of the groups defined using the admin page on the web interface",
|
||||
"_comment7": "If the user is by default authorized to more than one command mode, x-command-mode header can be used to specify the default command mode",
|
||||
"_comment8": "Otherwise, the login page will be skipped, but a command more selection page will still be shown",
|
||||
|
||||
"enabled": false,
|
||||
"username": "X-Authorized",
|
||||
"group": "X-Group"
|
||||
},
|
||||
"elevationProvider": {
|
||||
"_comment1": "The elevation provider is used to fetch elevation data for the map. It should be a URL with {lat} and {lng} placeholders.",
|
||||
|
||||
"_comment16": "The elevation provider is used to fetch elevation data for the map. It should be a URL with {lat} and {lng} placeholders.",
|
||||
"elevationProvider": {
|
||||
"provider": "https://srtm.fasma.org/{lat}{lng}.SRTMGL3S.hgt.zip",
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"mapLayers": {
|
||||
"_comment1": "These are the map layers used by the frontend server. They are used to display the map in the interface.",
|
||||
"_comment2": "The urlTemplate should be a URL with {z}, {x}, and {y} placeholders for the zoom level and tile coordinates.",
|
||||
"_comment3": "The minZoom and maxZoom define the zoom levels at which the layer is visible.",
|
||||
"_comment4": "The attribution is the text displayed in the bottom right corner of the map.",
|
||||
|
||||
|
||||
"_comment17": "These are the map layers used by the frontend server. They are used to display the map in the interface.",
|
||||
"_comment18": "The urlTemplate should be a URL with {z}, {x}, and {y} placeholders for the zoom level and tile coordinates.",
|
||||
"_comment19": "The minZoom and maxZoom define the zoom levels at which the layer is visible.",
|
||||
"_comment20": "The attribution is the text displayed in the bottom right corner of the map.",
|
||||
"mapLayers": {
|
||||
"ArcGIS Satellite": {
|
||||
"urlTemplate": "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",
|
||||
"minZoom": 1,
|
||||
@ -61,10 +61,10 @@
|
||||
"attribution": "OpenStreetMap contributors"
|
||||
}
|
||||
},
|
||||
"mapMirrors": {
|
||||
"_comment1": "These are the map mirrors used by the frontend server. They are used to load the map tiles from different sources.",
|
||||
"_comment2": "The key is the name of the mirror and the value is the URL of the mirror.",
|
||||
|
||||
"_comment21": "These are the map mirrors used by the frontend server. They are used to load the map tiles from different sources.",
|
||||
"_comment22": "The key is the name of the mirror and the value is the URL of the mirror.",
|
||||
"mapMirrors": {
|
||||
"DCS Map (Official)": "https://maps.dcsolympus.com/maps",
|
||||
"DCS Map (Alt.)": "https://refugees.dcsolympus.com/maps"
|
||||
},
|
||||
@ -72,16 +72,17 @@
|
||||
"_comment3": "If autoconnectWhenLocal is true, the frontend server will automatically connect to the backend server when running on the same machine.",
|
||||
"_comment4": "If a proxy is used, the proxyHeader should be set to the header used to forward the client IP address to the backend server.",
|
||||
"_comment5": "This is useful when running the frontend server behind a reverse proxy, e.g. nginx, allowing to skip login when connecting locally but still authenticate when connecting remotely.",
|
||||
|
||||
"autoconnectWhenLocal": true,
|
||||
"proxyHeader": "x-forwarded-for"
|
||||
},
|
||||
"audio": {
|
||||
"_comment1": "These are the settings for the audio backend, i.e. the service which handles direct connection of Olympus to a SRS server.",
|
||||
"_comment2": "The SRSPort is the port used to connect to the SRS server and should be set to be the same as the value in SRS (5002 by default).",
|
||||
"_comment3": "The WSPort is the port used by the web interface to connect to the audio backend WebSocket. It should be available and not used by other processes.",
|
||||
"_comment4": "The WSEndpoint is the endpoint used by the web interface to connect to the audio backend WebSocket when using a reverse proxy. A websocket proxy should be set up to forward requests from this endpoint to WSPort.",
|
||||
|
||||
"_comment23": "These are the settings for the audio backend, i.e. the service which handles direct connection of Olympus to a SRS server.",
|
||||
"_comment24": "The SRSPort is the port used to connect to the SRS server and should be set to be the same as the value in SRS (5002 by default).",
|
||||
"_comment25": "The WSPort is the port used by the web interface to connect to the audio backend WebSocket. It should be available and not used by other processes.",
|
||||
"_comment26": "The WSEndpoint is the endpoint used by the web interface to connect to the audio backend WebSocket when using a reverse proxy. A websocket proxy should be set up to forward requests from this endpoint to WSPort.",
|
||||
|
||||
"audio": {
|
||||
|
||||
"SRSPort": 5002,
|
||||
"WSPort": 4000,
|
||||
"WSEndpoint": "audio"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user