mirror of
https://github.com/Pax1601/DCSOlympus.git
synced 2025-10-29 16:56:34 +00:00
Merge remote-tracking branch 'origin/release-candidate' into features/redgreen-unit
This commit is contained in:
@@ -55,6 +55,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();
|
||||
|
||||
@@ -62,6 +62,8 @@ public:
|
||||
bool hasFreshData(unsigned long long time);
|
||||
bool checkFreshness(unsigned char datumIndex, unsigned long long time);
|
||||
|
||||
unsigned int computeTotalAmmo();
|
||||
|
||||
/********** Setters **********/
|
||||
virtual void setCategory(string newValue) { updateValue(category, newValue, DataIndex::category); }
|
||||
virtual void setAlive(bool newValue) { updateValue(alive, newValue, DataIndex::alive); }
|
||||
@@ -113,6 +115,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); }
|
||||
virtual void setAlarmState(string newValue) { updateValue(alarmState, newValue, DataIndex::alarmState); }
|
||||
|
||||
/********** Getters **********/
|
||||
@@ -166,6 +179,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;
|
||||
@@ -221,16 +245,30 @@ protected:
|
||||
unsigned char shotsScatter = 2;
|
||||
unsigned char shotsIntensity = 2;
|
||||
unsigned char health = 100;
|
||||
double timeToNextTasking = 0;
|
||||
double barrelHeight = 0;
|
||||
double muzzleVelocity = 0;
|
||||
double aimTime = 0;
|
||||
unsigned int shotsToFire = 0;
|
||||
double shotsBaseInterval = 0;
|
||||
double shotsBaseScatter = 0;
|
||||
double engagementRange = 0;
|
||||
double targetingRange = 0;
|
||||
double aimMethodRange = 0;
|
||||
double acquisitionRange = 0;
|
||||
|
||||
/********** Other **********/
|
||||
unsigned int taskCheckCounter = 0;
|
||||
unsigned int internalCounter = 0;
|
||||
Unit* missOnPurposeTarget = nullptr;
|
||||
bool hasTaskAssigned = false;
|
||||
double initialFuel = 0;
|
||||
map<unsigned char, unsigned long long> updateTimeMap;
|
||||
unsigned long long lastLoopTime = 0;
|
||||
bool enableTaskFailedCheck = false;
|
||||
unsigned long nextTaskingMilliseconds = 0;
|
||||
unsigned int totalShellsFired = 0;
|
||||
unsigned int shellsFiredAtTasking = 0;
|
||||
unsigned int oldAmmo = 0;
|
||||
|
||||
/********** Private methods **********/
|
||||
virtual void AIloop() = 0;
|
||||
|
||||
@@ -113,4 +113,10 @@ class Bomb : public Weapon
|
||||
{
|
||||
public:
|
||||
Bomb(json::value json, unsigned int ID);
|
||||
};
|
||||
|
||||
class Shell : public Weapon
|
||||
{
|
||||
public:
|
||||
Shell(json::value json, unsigned int ID);
|
||||
};
|
||||
@@ -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)
|
||||
@@ -83,12 +108,14 @@ void GroundUnit::setState(unsigned char newState)
|
||||
/************ Perform any action required when ENTERING a state ************/
|
||||
switch (newState) {
|
||||
case State::IDLE: {
|
||||
setTask("Idle");
|
||||
setEnableTaskCheckFailed(false);
|
||||
clearActivePath();
|
||||
resetActiveDestination();
|
||||
break;
|
||||
}
|
||||
case State::REACH_DESTINATION: {
|
||||
setTask("Reaching destination");
|
||||
setEnableTaskCheckFailed(true);
|
||||
resetActiveDestination();
|
||||
break;
|
||||
@@ -100,6 +127,7 @@ void GroundUnit::setState(unsigned char newState)
|
||||
break;
|
||||
}
|
||||
case State::FIRE_AT_AREA: {
|
||||
setTask("Firing at area");
|
||||
setTargetPosition(currentTargetPosition);
|
||||
setEnableTaskCheckFailed(true);
|
||||
clearActivePath();
|
||||
@@ -107,6 +135,7 @@ void GroundUnit::setState(unsigned char newState)
|
||||
break;
|
||||
}
|
||||
case State::SIMULATE_FIRE_FIGHT: {
|
||||
setTask("Simulating fire fight");
|
||||
setTargetPosition(currentTargetPosition);
|
||||
setEnableTaskCheckFailed(false);
|
||||
clearActivePath();
|
||||
@@ -114,12 +143,14 @@ void GroundUnit::setState(unsigned char newState)
|
||||
break;
|
||||
}
|
||||
case State::SCENIC_AAA: {
|
||||
setTask("Scenic AAA");
|
||||
setEnableTaskCheckFailed(false);
|
||||
clearActivePath();
|
||||
resetActiveDestination();
|
||||
break;
|
||||
}
|
||||
case State::MISS_ON_PURPOSE: {
|
||||
setTask("Miss on purpose");
|
||||
setEnableTaskCheckFailed(false);
|
||||
clearActivePath();
|
||||
resetActiveDestination();
|
||||
@@ -131,6 +162,7 @@ void GroundUnit::setState(unsigned char newState)
|
||||
|
||||
setHasTask(false);
|
||||
resetTaskFailedCounter();
|
||||
nextTaskingMilliseconds = 0;
|
||||
|
||||
log(unitName + " setting state from " + to_string(state) + " to " + to_string(newState));
|
||||
state = newState;
|
||||
@@ -143,17 +175,27 @@ void GroundUnit::setState(unsigned char newState)
|
||||
void GroundUnit::AIloop()
|
||||
{
|
||||
srand(static_cast<unsigned int>(time(NULL)) + ID);
|
||||
unsigned long timeNow = std::chrono::system_clock::now().time_since_epoch() / std::chrono::milliseconds(1);
|
||||
|
||||
double currentAmmo = computeTotalAmmo();
|
||||
/* Out of ammo */
|
||||
if (currentAmmo <= shotsToFire && state != State::IDLE) {
|
||||
setState(State::IDLE);
|
||||
}
|
||||
|
||||
/* Account for unit reloading */
|
||||
if (currentAmmo < oldAmmo)
|
||||
totalShellsFired += oldAmmo - currentAmmo;
|
||||
oldAmmo = currentAmmo;
|
||||
|
||||
switch (state) {
|
||||
case State::IDLE: {
|
||||
setTask("Idle");
|
||||
if (getHasTask())
|
||||
resetTask();
|
||||
|
||||
break;
|
||||
}
|
||||
case State::REACH_DESTINATION: {
|
||||
setTask("Reaching destination");
|
||||
|
||||
string enrouteTask = "";
|
||||
bool looping = false;
|
||||
|
||||
@@ -200,12 +242,15 @@ void GroundUnit::AIloop()
|
||||
break;
|
||||
}
|
||||
case State::FIRE_AT_AREA: {
|
||||
setTask("Firing at area");
|
||||
|
||||
if (!getHasTask()) {
|
||||
std::ostringstream taskSS;
|
||||
taskSS.precision(10);
|
||||
taskSS << "{id = 'FireAtPoint', lat = " << targetPosition.lat << ", lng = " << targetPosition.lng << ", radius = 100}";
|
||||
if (targetPosition.alt == NULL) {
|
||||
taskSS << "{id = 'FireAtPoint', lat = " << targetPosition.lat << ", lng = " << targetPosition.lng << ", radius = 100}";
|
||||
}
|
||||
else {
|
||||
taskSS << "{id = 'FireAtPoint', lat = " << targetPosition.lat << ", lng = " << targetPosition.lng << ", alt = " << targetPosition.alt << ", radius = 100}";
|
||||
}
|
||||
Command* command = dynamic_cast<Command*>(new SetTask(groupName, taskSS.str(), [this]() { this->setHasTaskAssigned(true); }));
|
||||
scheduler->appendCommand(command);
|
||||
setHasTask(true);
|
||||
@@ -214,115 +259,163 @@ 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 */
|
||||
Coords scatteredTargetPosition = targetPosition;
|
||||
double distance;
|
||||
double bearing1;
|
||||
double bearing2;
|
||||
Geodesic::WGS84().Inverse(getPosition().lat, getPosition().lng, scatteredTargetPosition.lat, scatteredTargetPosition.lng, distance, bearing1, bearing2);
|
||||
|
||||
/* Compute the scattered position applying a random scatter to the shot */
|
||||
double scatterDistance = distance * tan(10 /* degs */ * (ShotsScatter::LOW - shotsScatter) / 57.29577 + 2 / 57.29577 /* degs */) * RANDOM_MINUS_ONE_TO_ONE;
|
||||
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 ((totalShellsFired - shellsFiredAtTasking >= shotsToFire || timeNow >= nextTaskingMilliseconds) && targetPosition != Coords(NULL)) {
|
||||
if (scheduler->getLoad() > 100) {
|
||||
taskString = "Excessive load, skipping tasking of unit";
|
||||
setTargetPosition(Coords(NULL));
|
||||
if (getHasTask())
|
||||
resetTask();
|
||||
}
|
||||
|
||||
/* If the unit is of the indirect fire type, like a mortar, simply shoot at the target */
|
||||
if (indirectFire) {
|
||||
log(unitName + "(" + name + ")" + " simulating fire fight with indirect fire");
|
||||
std::ostringstream taskSS;
|
||||
taskSS.precision(10);
|
||||
taskSS << "{id = 'FireAtPoint', lat = " << scatteredTargetPosition.lat << ", lng = " << scatteredTargetPosition.lng << ", radius = 100}";
|
||||
Command* command = dynamic_cast<Command*>(new SetTask(groupName, taskSS.str(), [this]() { this->setHasTaskAssigned(true); }));
|
||||
scheduler->appendCommand(command);
|
||||
setHasTask(true);
|
||||
}
|
||||
/* Otherwise use the aim method */
|
||||
else {
|
||||
log(unitName + "(" + name + ")" + " simulating fire fight with aim at point method");
|
||||
aimAtPoint(scatteredTargetPosition);
|
||||
}
|
||||
/* Get the distance and bearing to the target */
|
||||
Coords scatteredTargetPosition = targetPosition;
|
||||
double distance;
|
||||
double bearing1;
|
||||
double bearing2;
|
||||
Geodesic::WGS84().Inverse(getPosition().lat, getPosition().lng, scatteredTargetPosition.lat, scatteredTargetPosition.lng, distance, bearing1, bearing2);
|
||||
|
||||
/* Wait an amout of time depending on the shots intensity */
|
||||
internalCounter = static_cast<unsigned int>(((ShotsIntensity::HIGH - shotsIntensity) * shotsBaseInterval + aimTime) / FRAMERATE_TIME_INTERVAL);
|
||||
/* Apply a scatter to the aim */
|
||||
bearing1 += RANDOM_MINUS_ONE_TO_ONE * (ShotsScatter::LOW - shotsScatter + 1) * 10;
|
||||
|
||||
/* Compute the scattered position applying a random scatter to the shot */
|
||||
double scatterDistance = distance * tan(10 /* degs */ * (ShotsScatter::LOW - shotsScatter) / 57.29577 + 2 / 57.29577 /* degs */) * RANDOM_MINUS_ONE_TO_ONE;
|
||||
Geodesic::WGS84().Direct(scatteredTargetPosition.lat, scatteredTargetPosition.lng, bearing1, scatterDistance, scatteredTargetPosition.lat, scatteredTargetPosition.lng);
|
||||
|
||||
/* Recover the data from the database */
|
||||
bool indirectFire = false;
|
||||
if (database.has_object_field(to_wstring(name))) {
|
||||
json::value databaseEntry = database[to_wstring(name)];
|
||||
if (databaseEntry.has_boolean_field(L"indirectFire"))
|
||||
indirectFire = databaseEntry[L"indirectFire"].as_bool();
|
||||
}
|
||||
|
||||
/* 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);
|
||||
taskSS << "{id = 'FireAtPoint', lat = " << scatteredTargetPosition.lat << ", lng = " << scatteredTargetPosition.lng << ", radius = 0.01}";
|
||||
Command* command = dynamic_cast<Command*>(new SetTask(groupName, taskSS.str(), [this]() { this->setHasTaskAssigned(true); }));
|
||||
scheduler->appendCommand(command);
|
||||
shellsFiredAtTasking = totalShellsFired;
|
||||
setHasTask(true);
|
||||
}
|
||||
/* 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");
|
||||
string aimTaskString = aimAtPoint(scatteredTargetPosition);
|
||||
taskString += aimTaskString;
|
||||
}
|
||||
|
||||
/* Wait an amout of time depending on the shots intensity */
|
||||
nextTaskingMilliseconds = timeNow + static_cast<unsigned long>(2 * aimTime * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
if (targetPosition == Coords(NULL))
|
||||
setState(State::IDLE);
|
||||
|
||||
/* Fallback if something went wrong */
|
||||
if (internalCounter == 0)
|
||||
internalCounter = static_cast<unsigned int>(3 / FRAMERATE_TIME_INTERVAL);
|
||||
internalCounter--;
|
||||
if (timeNow >= nextTaskingMilliseconds)
|
||||
nextTaskingMilliseconds = timeNow + static_cast<unsigned long>(3 * 1000);
|
||||
|
||||
setTimeToNextTasking(((nextTaskingMilliseconds - timeNow) / 1000.0));
|
||||
|
||||
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)) {
|
||||
double distance = 0;
|
||||
unsigned char unitCoalition = coalition == 0 ? getOperateAs() : coalition;
|
||||
unsigned char targetCoalition = unitCoalition == 2 ? 1 : 2;
|
||||
Unit* target = unitsManager->getClosestUnit(this, targetCoalition, { "Aircraft", "Helicopter" }, distance);
|
||||
|
||||
/* Recover the data from the database */
|
||||
double aimTime = 2; /* s */
|
||||
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_number_field(L"shotsBaseInterval"))
|
||||
shotsBaseInterval = databaseEntry[L"shotsBaseInterval"].as_number().to_double();
|
||||
if (totalShellsFired - shellsFiredAtTasking >= shotsToFire || timeNow >= nextTaskingMilliseconds) {
|
||||
if (scheduler->getLoad() > 100) {
|
||||
taskString = "Excessive load, skipping tasking of unit";
|
||||
setTargetPosition(Coords(NULL));
|
||||
if (getHasTask())
|
||||
resetTask();
|
||||
}
|
||||
else {
|
||||
double distance = 0;
|
||||
unsigned char unitCoalition = coalition == 0 ? getOperateAs() : coalition;
|
||||
unsigned char targetCoalition = unitCoalition == 2 ? 1 : 2;
|
||||
Unit* target = unitsManager->getClosestUnit(this, targetCoalition, { "Aircraft", "Helicopter" }, distance);
|
||||
|
||||
/* Only run if an enemy air unit is closer than 20km to avoid useless load */
|
||||
if (target != nullptr && distance < 20000 /* m */) {
|
||||
double r = 15; /* m */
|
||||
double barrelElevation = r * tan(acos(((double)(rand()) / (double)(RAND_MAX))));
|
||||
/* Recover the data from the database */
|
||||
bool flak = false;
|
||||
if (database.has_object_field(to_wstring(name))) {
|
||||
json::value databaseEntry = database[to_wstring(name)];
|
||||
if (databaseEntry.has_boolean_field(L"flak"))
|
||||
flak = databaseEntry[L"flak"].as_bool();
|
||||
}
|
||||
|
||||
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);
|
||||
/* Only run if an enemy air unit is closer than 20km to avoid useless load */
|
||||
double activationDistance = 20000;
|
||||
if (2 * engagementRange > activationDistance)
|
||||
activationDistance = 2 * engagementRange;
|
||||
|
||||
std::ostringstream taskSS;
|
||||
taskSS.precision(10);
|
||||
taskSS << "{id = 'FireAtPoint', lat = " << lat << ", lng = " << lng << ", alt = " << position.alt + barrelElevation << ", radius = 0.001}";
|
||||
Command* command = dynamic_cast<Command*>(new SetTask(groupName, taskSS.str(), [this]() { this->setHasTaskAssigned(true); }));
|
||||
scheduler->appendCommand(command);
|
||||
setHasTask(true);
|
||||
if (target != nullptr && distance < activationDistance /* m */) {
|
||||
double r = 15; /* m */
|
||||
double barrelElevation = position.alt + barrelHeight + r * tan(acos(((double)(rand()) / (double)(RAND_MAX))));
|
||||
|
||||
/* Wait an amout of time depending on the shots intensity */
|
||||
internalCounter = static_cast<unsigned int>(((ShotsIntensity::HIGH - shotsIntensity) * shotsBaseInterval + aimTime) / FRAMERATE_TIME_INTERVAL);
|
||||
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 * (1 + (ShotsScatter::LOW - shotsScatter)) * 0.01;
|
||||
lng = position.lng + RANDOM_MINUS_ONE_TO_ONE * (1 + (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 - position.alt)) + "m AGL";
|
||||
|
||||
std::ostringstream taskSS;
|
||||
taskSS.precision(10);
|
||||
taskSS << "{id = 'FireAtPoint', lat = " << lat << ", lng = " << lng << ", alt = " << barrelElevation << ", radius = 0.001 }";
|
||||
Command* command = dynamic_cast<Command*>(new SetTask(groupName, taskSS.str(), [this]() { this->setHasTaskAssigned(true); }));
|
||||
scheduler->appendCommand(command);
|
||||
shellsFiredAtTasking = totalShellsFired;
|
||||
setHasTask(true);
|
||||
|
||||
/* Wait an amout of time depending on the shots intensity */
|
||||
nextTaskingMilliseconds = timeNow + static_cast<unsigned long>(2 * aimTime * 1000);
|
||||
}
|
||||
else {
|
||||
setTargetPosition(Coords(NULL));
|
||||
if (target == nullptr)
|
||||
taskString += "Scenic AAA. No valid target.";
|
||||
else
|
||||
taskString += "Scenic AAA. Target outside max range: " + to_string((int)round(distance)) + "m.";
|
||||
|
||||
if (getHasTask())
|
||||
resetTask();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (internalCounter == 0)
|
||||
internalCounter = static_cast<unsigned int>(3 / FRAMERATE_TIME_INTERVAL);
|
||||
internalCounter--;
|
||||
if (timeNow >= nextTaskingMilliseconds)
|
||||
nextTaskingMilliseconds = timeNow + static_cast<unsigned long>(3 * 1000);
|
||||
|
||||
setTimeToNextTasking((nextTaskingMilliseconds - timeNow) / 1000.0);
|
||||
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;
|
||||
@@ -332,150 +425,157 @@ void GroundUnit::AIloop()
|
||||
canAAA = databaseEntry[L"canAAA"].as_bool();
|
||||
}
|
||||
|
||||
/* Recover the data from the database */
|
||||
bool flak = false;
|
||||
if (database.has_object_field(to_wstring(name))) {
|
||||
json::value databaseEntry = database[to_wstring(name)];
|
||||
if (databaseEntry.has_boolean_field(L"flak"))
|
||||
flak = databaseEntry[L"flak"].as_bool();
|
||||
}
|
||||
|
||||
if (canAAA) {
|
||||
/* Only perform scenic functions when the scheduler is "free" */
|
||||
/* Only run this when the internal counter reaches 0 to avoid excessive computations when no nearby target */
|
||||
if (scheduler->getLoad() < 30 && internalCounter == 0) {
|
||||
double distance = 0;
|
||||
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);
|
||||
|
||||
Unit* target = nullptr;
|
||||
unsigned int index = static_cast<unsigned int>((RANDOM_ZERO_TO_ONE * (targets.size() - 1)));
|
||||
for (auto const& p : targets) {
|
||||
if (index-- == 0) {
|
||||
target = p.first;
|
||||
distance = p.second;
|
||||
}
|
||||
}
|
||||
|
||||
/* Only do if we have a valid target close enough for AAA */
|
||||
if (target != nullptr) {
|
||||
/* Approximate the flight time */
|
||||
if (muzzleVelocity != 0)
|
||||
aimTime += distance / muzzleVelocity;
|
||||
|
||||
/* If the target is in targeting range and we are in highest precision mode, target it */
|
||||
if (distance < targetingRange && shotsScatter == ShotsScatter::LOW) {
|
||||
/* Send the command */
|
||||
std::ostringstream taskSS;
|
||||
taskSS.precision(10);
|
||||
taskSS << "{id = 'AttackUnit', unitID = " << target->getID() << " }";
|
||||
Command* command = dynamic_cast<Command*>(new SetTask(groupName, taskSS.str(), [this]() { this->setHasTaskAssigned(true); }));
|
||||
scheduler->appendCommand(command);
|
||||
setHasTask(true);
|
||||
|
||||
internalCounter = static_cast<unsigned int>((aimTime + (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 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
|
||||
|
||||
/* Send the command */
|
||||
if (distance < engagementRange) {
|
||||
/* 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 << " }";
|
||||
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);
|
||||
}
|
||||
else if (distance < aimMethodRange) {
|
||||
/* If the unit is closer than the aim method range, use the aim method range */
|
||||
aimAtPoint(Coords(aimLat, aimLng, aimAlt));
|
||||
setTargetPosition(Coords(aimLat, aimLng, target->getPosition().alt));
|
||||
internalCounter = static_cast<unsigned int>((aimTime + (ShotsIntensity::HIGH - shotsIntensity) * shotsBaseInterval + 2) / FRAMERATE_TIME_INTERVAL);
|
||||
}
|
||||
else {
|
||||
/* Else just wake the unit up with an impossible command */
|
||||
std::ostringstream taskSS;
|
||||
taskSS.precision(10);
|
||||
taskSS << "{id = 'FireAtPoint', lat = " << 0 << ", lng = " << 0 << ", alt = " << 0 << ", radius = 0.001, expendQty = " << 0 << " }";
|
||||
Command* command = dynamic_cast<Command*>(new SetTask(groupName, taskSS.str(), [this]() { this->setHasTaskAssigned(true); }));
|
||||
scheduler->appendCommand(command);
|
||||
setHasTask(true);
|
||||
setTargetPosition(Coords(NULL));
|
||||
|
||||
/* Don't wait too long before checking again */
|
||||
internalCounter = static_cast<unsigned int>(5 / FRAMERATE_TIME_INTERVAL);
|
||||
}
|
||||
}
|
||||
missOnPurposeTarget = target;
|
||||
}
|
||||
else {
|
||||
if (totalShellsFired - shellsFiredAtTasking >= shotsToFire || timeNow >= nextTaskingMilliseconds) {
|
||||
if (scheduler->getLoad() > 100) {
|
||||
taskString = "Excessive load, skipping tasking of unit";
|
||||
setTargetPosition(Coords(NULL));
|
||||
if (getHasTask())
|
||||
resetTask();
|
||||
}
|
||||
else {
|
||||
double distance = 0;
|
||||
unsigned char unitCoalition = coalition == 0 ? getOperateAs() : coalition;
|
||||
unsigned char targetCoalition = unitCoalition == 2 ? 1 : 2;
|
||||
|
||||
/* 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);
|
||||
|
||||
Unit* target = nullptr;
|
||||
unsigned int index = static_cast<unsigned int>((RANDOM_ZERO_TO_ONE * (targets.size() - 1)));
|
||||
for (auto const& p : targets) {
|
||||
if (index-- == 0) {
|
||||
target = p.first;
|
||||
distance = p.second;
|
||||
}
|
||||
}
|
||||
|
||||
/* 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";
|
||||
|
||||
// Very simplified algorithm ignoring drag
|
||||
double correctedAimTime = aimTime + 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);
|
||||
taskSS << "{id = 'AttackUnit', unitID = " << target->getID() << " }";
|
||||
Command* command = dynamic_cast<Command*>(new SetTask(groupName, taskSS.str(), [this]() { this->setHasTaskAssigned(true); }));
|
||||
scheduler->appendCommand(command);
|
||||
shellsFiredAtTasking = totalShellsFired;
|
||||
setHasTask(true);
|
||||
|
||||
nextTaskingMilliseconds = timeNow + static_cast<unsigned long>(2 * aimTime * 1000);
|
||||
}
|
||||
/* Else, do miss on purpose */
|
||||
else {
|
||||
/* Compute where the target will be in aimTime seconds. */
|
||||
double aimDistance = target->getHorizontalVelocity() * correctedAimTime;
|
||||
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();
|
||||
|
||||
if (flak) {
|
||||
aimLat += RANDOM_MINUS_ONE_TO_ONE * (1 + (ShotsScatter::LOW - shotsScatter)) * 0.01;
|
||||
aimLng += RANDOM_MINUS_ONE_TO_ONE * (1 + (ShotsScatter::LOW - shotsScatter)) * 0.01;
|
||||
aimAlt += RANDOM_MINUS_ONE_TO_ONE * (1 + (ShotsScatter::LOW - shotsScatter)) * 1000;
|
||||
}
|
||||
|
||||
/* 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 }";
|
||||
|
||||
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);
|
||||
shellsFiredAtTasking = totalShellsFired;
|
||||
setHasTask(true);
|
||||
setTargetPosition(Coords(aimLat, aimLng, target->getPosition().alt));
|
||||
nextTaskingMilliseconds = timeNow + static_cast<unsigned long>(2 * aimTime * 1000);
|
||||
}
|
||||
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 */
|
||||
string aimMethodTask = aimAtPoint(Coords(aimLat, aimLng, aimAlt));
|
||||
taskString += aimMethodTask;
|
||||
|
||||
setTargetPosition(Coords(aimLat, aimLng, target->getPosition().alt));
|
||||
nextTaskingMilliseconds = timeNow + static_cast<unsigned long>(2 * aimTime * 1000);
|
||||
}
|
||||
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);
|
||||
taskSS << "{id = 'FireAtPoint', lat = " << 0 << ", lng = " << 0 << ", alt = " << 0 << ", radius = 0.001, expendQty = " << 0 << " }";
|
||||
Command* command = dynamic_cast<Command*>(new SetTask(groupName, taskSS.str(), [this]() { this->setHasTaskAssigned(true); }));
|
||||
scheduler->appendCommand(command);
|
||||
shellsFiredAtTasking = totalShellsFired;
|
||||
setHasTask(true);
|
||||
setTargetPosition(Coords(NULL));
|
||||
|
||||
/* Don't wait too long before checking again */
|
||||
nextTaskingMilliseconds = timeNow + static_cast<unsigned long>(5 * 1000);
|
||||
}
|
||||
}
|
||||
missOnPurposeTarget = target;
|
||||
}
|
||||
else {
|
||||
taskString += "Missing on purpose. No target in range.";
|
||||
setTargetPosition(Coords(NULL));
|
||||
if (getHasTask())
|
||||
resetTask();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* If no valid target was detected */
|
||||
if (internalCounter == 0) {
|
||||
if (timeNow >= nextTaskingMilliseconds) {
|
||||
double alertnessTimeConstant = 10; /* s */
|
||||
if (database.has_object_field(to_wstring(name))) {
|
||||
json::value databaseEntry = database[to_wstring(name)];
|
||||
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);
|
||||
nextTaskingMilliseconds = timeNow + static_cast<unsigned long>((5 + RANDOM_ZERO_TO_ONE * alertnessTimeConstant) * 1000L);
|
||||
missOnPurposeTarget = nullptr;
|
||||
setTargetPosition(Coords(NULL));
|
||||
}
|
||||
internalCounter--;
|
||||
|
||||
}
|
||||
else {
|
||||
setState(State::IDLE);
|
||||
}
|
||||
|
||||
setTimeToNextTasking((nextTaskingMilliseconds - timeNow) / 1000.0);
|
||||
|
||||
if (taskString.length() > 0)
|
||||
setTask(taskString);
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
@@ -484,7 +584,8 @@ void GroundUnit::AIloop()
|
||||
}
|
||||
|
||||
|
||||
void GroundUnit::aimAtPoint(Coords aimTarget) {
|
||||
string GroundUnit::aimAtPoint(Coords aimTarget) {
|
||||
string taskString = "";
|
||||
double dist;
|
||||
double bearing1;
|
||||
double bearing2;
|
||||
@@ -493,20 +594,6 @@ void GroundUnit::aimAtPoint(Coords aimTarget) {
|
||||
/* Aim point distance */
|
||||
double r = 15; /* m */
|
||||
|
||||
/* Default gun values */
|
||||
double barrelHeight = 1.0; /* m */
|
||||
double muzzleVelocity = 860; /* m/s */
|
||||
double shotsBaseScatter = 5; /* degs */
|
||||
if (database.has_object_field(to_wstring(name))) {
|
||||
json::value databaseEntry = database[to_wstring(name)];
|
||||
if (databaseEntry.has_number_field(L"barrelHeight") && databaseEntry.has_number_field(L"muzzleVelocity")) {
|
||||
barrelHeight = databaseEntry[L"barrelHeight"].as_number().to_double();
|
||||
muzzleVelocity = databaseEntry[L"muzzleVelocity"].as_number().to_double();
|
||||
}
|
||||
if (databaseEntry.has_number_field(L"shotsBaseScatter"))
|
||||
shotsBaseScatter = databaseEntry[L"shotsBaseScatter"].as_number().to_double();
|
||||
}
|
||||
|
||||
/* Compute the elevation angle of the gun*/
|
||||
double deltaHeight = (aimTarget.alt - (position.alt + barrelHeight));
|
||||
double alpha = 9.81 / 2 * dist * dist / (muzzleVelocity * muzzleVelocity);
|
||||
@@ -521,18 +608,23 @@ 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);
|
||||
taskSS << "{id = 'FireAtPoint', lat = " << lat << ", lng = " << lng << ", alt = " << position.alt + barrelElevation + barrelHeight << ", radius = 0.001}";
|
||||
Command* command = dynamic_cast<Command*>(new SetTask(groupName, taskSS.str(), [this]() { this->setHasTaskAssigned(true); }));
|
||||
scheduler->appendCommand(command);
|
||||
shellsFiredAtTasking = totalShellsFired;
|
||||
setHasTask(true);
|
||||
}
|
||||
else {
|
||||
log("Target out of range for " + unitName + "(" + name + ")");
|
||||
taskString = +"Target out of range";
|
||||
}
|
||||
|
||||
return taskString;
|
||||
}
|
||||
|
||||
void GroundUnit::changeSpeed(string change)
|
||||
|
||||
@@ -390,6 +390,10 @@ void Scheduler::handleRequest(string key, json::value value, string username, js
|
||||
{
|
||||
vector<CloneOptions> cloneOptions;
|
||||
bool deleteOriginal = value[L"deleteOriginal"].as_bool();
|
||||
string coalition = to_string(value[L"coalition"]);
|
||||
|
||||
int spawnPoints = value[L"spawnPoints"].as_number().to_int32();
|
||||
if (!checkSpawnPoints(spawnPoints, coalition)) return;
|
||||
|
||||
for (auto unit : value[L"units"].as_array()) {
|
||||
unsigned int ID = unit[L"ID"].as_integer();
|
||||
@@ -536,6 +540,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();
|
||||
@@ -623,6 +650,11 @@ void Scheduler::handleRequest(string key, json::value value, string username, js
|
||||
double lat = value[L"location"][L"lat"].as_double();
|
||||
double lng = value[L"location"][L"lng"].as_double();
|
||||
Coords loc; loc.lat = lat; loc.lng = lng;
|
||||
|
||||
if (value[L"location"].has_number_field(L"alt")) {
|
||||
loc.alt = value[L"location"][L"alt"].as_double();
|
||||
}
|
||||
|
||||
Unit* unit = unitsManager->getGroupLeader(ID);
|
||||
if (unit != nullptr) {
|
||||
unit->setTargetPosition(loc);
|
||||
|
||||
@@ -305,6 +305,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -828,3 +839,11 @@ void Unit::setHasTaskAssigned(bool newHasTaskAssigned) {
|
||||
void Unit::triggerUpdate(unsigned char datumIndex) {
|
||||
updateTimeMap[datumIndex] = duration_cast<milliseconds>(system_clock::now().time_since_epoch()).count();
|
||||
}
|
||||
|
||||
unsigned int Unit::computeTotalAmmo()
|
||||
{
|
||||
unsigned int totalShells = 0;
|
||||
for (auto const& ammoItem : ammo)
|
||||
totalShells += ammoItem.quantity;
|
||||
return totalShells;
|
||||
}
|
||||
|
||||
@@ -113,4 +113,11 @@ Bomb::Bomb(json::value json, unsigned int ID) : Weapon(json, ID)
|
||||
{
|
||||
log("New Bomb created with ID: " + to_string(ID));
|
||||
setCategory("Bomb");
|
||||
};
|
||||
|
||||
/* Shell */
|
||||
Shell::Shell(json::value json, unsigned int ID) : Weapon(json, ID)
|
||||
{
|
||||
log("New Shell created with ID: " + to_string(ID));
|
||||
setCategory("Shell");
|
||||
};
|
||||
@@ -41,6 +41,8 @@ void WeaponsManager::update(json::value& json, double dt)
|
||||
weapons[ID] = dynamic_cast<Weapon*>(new Missile(p.second, ID));
|
||||
else if (category.compare("Bomb") == 0)
|
||||
weapons[ID] = dynamic_cast<Weapon*>(new Bomb(p.second, ID));
|
||||
else if (category.compare("Shell") == 0)
|
||||
weapons[ID] = dynamic_cast<Weapon*>(new Shell(p.second, ID));
|
||||
|
||||
/* Initialize the weapon if creation was successfull */
|
||||
if (weapons.count(ID) != 0) {
|
||||
|
||||
@@ -129,7 +129,7 @@
|
||||
"canRearm": false,
|
||||
"muzzleVelocity": 1000,
|
||||
"barrelHeight": 2,
|
||||
"aimTime": 5,
|
||||
"aimTime": 7,
|
||||
"shotsToFire": 10,
|
||||
"cost": null,
|
||||
"tags": "Optical, Radar, CA",
|
||||
@@ -587,7 +587,7 @@
|
||||
"abilities": "Combined arms, Amphibious, Transport, AA",
|
||||
"canTargetPoint": true,
|
||||
"canRearm": false,
|
||||
"aimTime": 5,
|
||||
"aimTime": 6,
|
||||
"shotsToFire": 5,
|
||||
"tags": "CA",
|
||||
"markerFile": "groundunit-apc",
|
||||
@@ -819,7 +819,7 @@
|
||||
"canRearm": false,
|
||||
"barrelHeight": 2.2,
|
||||
"muzzleVelocity": 900,
|
||||
"aimTime": 5,
|
||||
"aimTime": 6,
|
||||
"shotsToFire": 5,
|
||||
"tags": "CA",
|
||||
"markerFile": "groundunit-apc",
|
||||
@@ -955,7 +955,7 @@
|
||||
"acquisitionRange": 3000,
|
||||
"engagementRange": 1000,
|
||||
"description": "Armoured car, MRAP. Wheeled. Amphibious. 12.7 mm machine gun.",
|
||||
"abilities": "Combined arms, Amphibious, AA",
|
||||
"abilities": "Combined arms, Amphibious",
|
||||
"canTargetPoint": true,
|
||||
"canRearm": false,
|
||||
"barrelHeight": 2,
|
||||
@@ -969,7 +969,7 @@
|
||||
"shotsBaseInterval": 6,
|
||||
"shotsBaseScatter": 10,
|
||||
"alertnessTimeConstant": 4,
|
||||
"canAAA": true
|
||||
"canAAA": false
|
||||
},
|
||||
"Dog Ear radar": {
|
||||
"name": "Dog Ear radar",
|
||||
@@ -1188,7 +1188,7 @@
|
||||
"abilities": "Combined arms, AA",
|
||||
"canTargetPoint": true,
|
||||
"canRearm": false,
|
||||
"aimTime": 8,
|
||||
"aimTime": 7,
|
||||
"shotsToFire": 5,
|
||||
"cost": null,
|
||||
"tags": "Radar, CA",
|
||||
@@ -2280,7 +2280,7 @@
|
||||
"abilities": "AA, Embark",
|
||||
"canTargetPoint": true,
|
||||
"canRearm": false,
|
||||
"aimTime": 5,
|
||||
"aimTime": 3,
|
||||
"shotsToFire": 5,
|
||||
"tags": "Russian type 1",
|
||||
"markerFile": "groundunit-infantry",
|
||||
@@ -2789,7 +2789,7 @@
|
||||
"shotsBaseInterval": 6,
|
||||
"shotsToFire": 5,
|
||||
"tags": "CA",
|
||||
"aimTime": 5,
|
||||
"aimTime": 7,
|
||||
"aimMethodRange": 3000,
|
||||
"shotsBaseScatter": 5,
|
||||
"alertnessTimeConstant": 4,
|
||||
@@ -2931,7 +2931,7 @@
|
||||
"aimMethodRange": 300,
|
||||
"shotsBaseScatter": 5,
|
||||
"alertnessTimeConstant": 3,
|
||||
"canAAA": true
|
||||
"canAAA": false
|
||||
},
|
||||
"M-109": {
|
||||
"name": "M-109",
|
||||
@@ -3096,7 +3096,7 @@
|
||||
"canRearm": false,
|
||||
"barrelHeight": 2.8,
|
||||
"muzzleVelocity": 950,
|
||||
"aimTime": 5,
|
||||
"aimTime": 2.5,
|
||||
"shotsToFire": 5,
|
||||
"tags": "CA",
|
||||
"markerFile": "groundunit-apc",
|
||||
@@ -3265,7 +3265,7 @@
|
||||
"markerFile": "groundunit-tactical",
|
||||
"targetingRange": 100,
|
||||
"aimMethodRange": 2500,
|
||||
"aimTime": 5,
|
||||
"aimTime": 3,
|
||||
"shotsToFire": 5,
|
||||
"shotsBaseInterval": 5,
|
||||
"shotsBaseScatter": 5,
|
||||
@@ -3368,7 +3368,7 @@
|
||||
"canRearm": false,
|
||||
"barrelHeight": 3,
|
||||
"muzzleVelocity": 900,
|
||||
"aimTime": 5,
|
||||
"aimTime": 2.7,
|
||||
"shotsToFire": 5,
|
||||
"tags": "CA",
|
||||
"markerFile": "groundunit-apc",
|
||||
@@ -3800,7 +3800,7 @@
|
||||
"canRearm": false,
|
||||
"barrelHeight": 2.05,
|
||||
"muzzleVelocity": 800,
|
||||
"aimTime": 5,
|
||||
"aimTime": 3,
|
||||
"shotsToFire": 5,
|
||||
"tags": "CA",
|
||||
"markerFile": "groundunit-apc",
|
||||
@@ -3845,13 +3845,13 @@
|
||||
},
|
||||
"acquisitionRange": 3000,
|
||||
"engagementRange": 1000,
|
||||
"description": "Marder Infantry FIghting Vehicle. Tracked. Amphibious. 20 mm gun and 7.62 mm machine gun.",
|
||||
"description": "Marder Infantry Fighting Vehicle. Tracked. Amphibious. 20 mm gun and 7.62 mm machine gun.",
|
||||
"abilities": "Combined arms, Transport, Amphibious",
|
||||
"canTargetPoint": true,
|
||||
"canRearm": false,
|
||||
"barrelHeight": 2.82,
|
||||
"muzzleVelocity": 900,
|
||||
"aimTime": 5,
|
||||
"aimTime": 9,
|
||||
"shotsToFire": 5,
|
||||
"tags": "CA",
|
||||
"markerFile": "groundunit-apc",
|
||||
@@ -3917,7 +3917,7 @@
|
||||
"acquisitionRange": 2500,
|
||||
"engagementRange": 300,
|
||||
"description": "Russian paratrooper carrying AKS-74.",
|
||||
"abilities": "AA, Embark",
|
||||
"abilities": "Embark",
|
||||
"canTargetPoint": true,
|
||||
"canRearm": false,
|
||||
"barrelHeight": 0.9,
|
||||
@@ -3926,7 +3926,7 @@
|
||||
"shotsToFire": 5,
|
||||
"tags": "Russian Para",
|
||||
"markerFile": "groundunit-infantry",
|
||||
"canAAA": true,
|
||||
"canAAA": false,
|
||||
"targetingRange": 100,
|
||||
"aimMethodRange": 2000,
|
||||
"shotsBaseInterval": 5,
|
||||
@@ -3955,7 +3955,7 @@
|
||||
"shotsToFire": 1,
|
||||
"tags": "Russian Para",
|
||||
"markerFile": "groundunit-infantry",
|
||||
"canAAA": true,
|
||||
"canAAA": false,
|
||||
"targetingRange": 50,
|
||||
"aimMethodRange": 750,
|
||||
"shotsBaseInterval": 5,
|
||||
@@ -5613,7 +5613,7 @@
|
||||
"abilities": "AA, Embark",
|
||||
"canTargetPoint": true,
|
||||
"canRearm": false,
|
||||
"aimTime": 5,
|
||||
"aimTime": 2.5,
|
||||
"shotsToFire": 5,
|
||||
"tags": "Russian type 4",
|
||||
"markerFile": "groundunit-infantry",
|
||||
@@ -5679,7 +5679,7 @@
|
||||
"acquisitionRange": 2500,
|
||||
"engagementRange": 300,
|
||||
"description": "Solider carrying M249.",
|
||||
"abilities": "AA, Embark",
|
||||
"abilities": "Embark",
|
||||
"canTargetPoint": true,
|
||||
"canRearm": false,
|
||||
"muzzleVelocity": 915,
|
||||
@@ -5688,7 +5688,7 @@
|
||||
"barrelHeight": 0.25,
|
||||
"tags": "US",
|
||||
"markerFile": "groundunit-infantry",
|
||||
"canAAA": true,
|
||||
"canAAA": false,
|
||||
"targetingRange": 100,
|
||||
"aimMethodRange": 2000,
|
||||
"shotsBaseInterval": 5,
|
||||
@@ -5758,7 +5758,7 @@
|
||||
"acquisitionRange": 2500,
|
||||
"engagementRange": 300,
|
||||
"description": "Solider carrying M4.",
|
||||
"abilities": "AA, Embark",
|
||||
"abilities": "Embark",
|
||||
"canTargetPoint": true,
|
||||
"canRearm": false,
|
||||
"barrelHeight": 0.95,
|
||||
@@ -5767,7 +5767,7 @@
|
||||
"shotsToFire": 5,
|
||||
"tags": "Georgia",
|
||||
"markerFile": "groundunit-infantry",
|
||||
"canAAA": true,
|
||||
"canAAA": false,
|
||||
"targetingRange": 100,
|
||||
"aimMethodRange": 2000,
|
||||
"shotsBaseInterval": 5,
|
||||
@@ -5829,7 +5829,7 @@
|
||||
"acquisitionRange": 2500,
|
||||
"engagementRange": 300,
|
||||
"description": "Solider carrying M4.",
|
||||
"abilities": "AA, Embark",
|
||||
"abilities": "Embark",
|
||||
"canTargetPoint": true,
|
||||
"canRearm": false,
|
||||
"barrelHeight": 1,
|
||||
@@ -5838,7 +5838,7 @@
|
||||
"shotsToFire": 5,
|
||||
"tags": "US",
|
||||
"markerFile": "groundunit-infantry",
|
||||
"canAAA": true,
|
||||
"canAAA": false,
|
||||
"targetingRange": 100,
|
||||
"aimMethodRange": 2000,
|
||||
"shotsBaseInterval": 5,
|
||||
@@ -5909,7 +5909,7 @@
|
||||
"shotsToFire": 1,
|
||||
"tags": "Russian",
|
||||
"markerFile": "groundunit-infantry",
|
||||
"canAAA": true,
|
||||
"canAAA": false,
|
||||
"targetingRange": 50,
|
||||
"aimMethodRange": 750,
|
||||
"shotsBaseInterval": 5,
|
||||
@@ -6200,7 +6200,7 @@
|
||||
"aimMethodRange": 2500,
|
||||
"shotsBaseScatter": 5,
|
||||
"alertnessTimeConstant": 6,
|
||||
"canAAA": true
|
||||
"canAAA": false
|
||||
},
|
||||
"T-72B": {
|
||||
"name": "T-72B",
|
||||
@@ -6234,7 +6234,7 @@
|
||||
"muzzleVelocity": 700,
|
||||
"aimMethodRange": 2500,
|
||||
"alertnessTimeConstant": 4,
|
||||
"canAAA": true,
|
||||
"canAAA": false,
|
||||
"tags": "CA"
|
||||
},
|
||||
"T-80UD": {
|
||||
@@ -6310,7 +6310,7 @@
|
||||
"aimMethodRange": 3000,
|
||||
"shotsBaseScatter": 5,
|
||||
"alertnessTimeConstant": 5,
|
||||
"canAAA": true
|
||||
"canAAA": false
|
||||
},
|
||||
"T-90": {
|
||||
"name": "T-90",
|
||||
@@ -6374,7 +6374,7 @@
|
||||
"shotsToFire": 5,
|
||||
"shotsBaseInterval": 8,
|
||||
"tags": "CA",
|
||||
"canAAA": true,
|
||||
"canAAA": false,
|
||||
"alertnessTimeConstant": 3,
|
||||
"shotsBaseScatter": 5,
|
||||
"aimMethodRange": 3000
|
||||
@@ -6406,7 +6406,7 @@
|
||||
"shotsBaseInterval": 5,
|
||||
"shotsBaseScatter": 5,
|
||||
"alertnessTimeConstant": 5,
|
||||
"canAAA": true
|
||||
"canAAA": false
|
||||
},
|
||||
"Tigr_233036": {
|
||||
"name": "Tigr_233036",
|
||||
@@ -6757,7 +6757,7 @@
|
||||
"canTargetPoint": true,
|
||||
"canRearm": false,
|
||||
"shotsToFire": 5,
|
||||
"aimTime": 8,
|
||||
"aimTime": 6,
|
||||
"muzzleVelocity": 1000,
|
||||
"barrelHeight": 3,
|
||||
"cost": null,
|
||||
@@ -6795,7 +6795,7 @@
|
||||
"cost": null,
|
||||
"barrelHeight": 3,
|
||||
"muzzleVelocity": 1000,
|
||||
"aimTime": 8,
|
||||
"aimTime": 6,
|
||||
"shotsToFire": 5,
|
||||
"tags": "CA",
|
||||
"markerFile": "groundunit-aaa",
|
||||
@@ -7028,7 +7028,7 @@
|
||||
"cost": null,
|
||||
"barrelHeight": 2.5,
|
||||
"muzzleVelocity": 900,
|
||||
"aimTime": 5,
|
||||
"aimTime": 8,
|
||||
"shotsToFire": 5,
|
||||
"tags": "Radar, CA",
|
||||
"markerFile": "groundunit-aaa",
|
||||
@@ -7240,7 +7240,7 @@
|
||||
"canRearm": false,
|
||||
"barrelHeight": 1.8,
|
||||
"muzzleVelocity": 1000,
|
||||
"aimTime": 9,
|
||||
"aimTime": 13,
|
||||
"shotsToFire": 5,
|
||||
"cost": null,
|
||||
"tags": "Radar, CA",
|
||||
@@ -7325,7 +7325,7 @@
|
||||
"canTargetPoint": true,
|
||||
"canRearm": false,
|
||||
"shotsToFire": 5,
|
||||
"aimTime": 9,
|
||||
"aimTime": 6,
|
||||
"muzzleVelocity": 1000,
|
||||
"barrelHeight": 1.5,
|
||||
"cost": null,
|
||||
@@ -7377,7 +7377,7 @@
|
||||
"canTargetPoint": true,
|
||||
"canRearm": false,
|
||||
"shotsToFire": 5,
|
||||
"aimTime": 9,
|
||||
"aimTime": 6,
|
||||
"muzzleVelocity": 1000,
|
||||
"barrelHeight": 1.5,
|
||||
"cost": null,
|
||||
@@ -7413,7 +7413,7 @@
|
||||
"canTargetPoint": true,
|
||||
"canRearm": false,
|
||||
"shotsToFire": 5,
|
||||
"aimTime": 9,
|
||||
"aimTime": 6,
|
||||
"muzzleVelocity": 1000,
|
||||
"barrelHeight": 1.5,
|
||||
"cost": null,
|
||||
@@ -7795,7 +7795,7 @@
|
||||
"acquisitionRange": 2500,
|
||||
"engagementRange": 300,
|
||||
"description": "Insurgent solider carrying AK-74.",
|
||||
"abilities": "AA, Embark",
|
||||
"abilities": "Embark",
|
||||
"canTargetPoint": true,
|
||||
"canRearm": false,
|
||||
"barrelHeight": 0.9,
|
||||
@@ -7804,7 +7804,7 @@
|
||||
"shotsToFire": 5,
|
||||
"tags": "Insurgent",
|
||||
"markerFile": "groundunit-infantry",
|
||||
"canAAA": true,
|
||||
"canAAA": false,
|
||||
"aimMethodRange": 2000,
|
||||
"targetingRange": 100,
|
||||
"shotsBaseInterval": 5,
|
||||
@@ -7958,7 +7958,7 @@
|
||||
"canRearm": false,
|
||||
"barrelHeight": 0.9,
|
||||
"muzzleVelocity": 900,
|
||||
"aimTime": 5,
|
||||
"aimTime": 3,
|
||||
"shotsToFire": 5,
|
||||
"tags": "Russian type 2",
|
||||
"markerFile": "groundunit-infantry",
|
||||
@@ -8047,7 +8047,7 @@
|
||||
"acquisitionRange": 2500,
|
||||
"engagementRange": 300,
|
||||
"description": "Russian solider carrying AK-74.",
|
||||
"abilities": "AA, Embark",
|
||||
"abilities": "Embark",
|
||||
"canTargetPoint": true,
|
||||
"canRearm": false,
|
||||
"barrelHeight": 0.9,
|
||||
@@ -8056,7 +8056,7 @@
|
||||
"shotsToFire": 5,
|
||||
"tags": "Russian type 3",
|
||||
"markerFile": "groundunit-infantry",
|
||||
"canAAA": true,
|
||||
"canAAA": false,
|
||||
"aimMethodRange": 2000,
|
||||
"targetingRange": 100,
|
||||
"shotsBaseInterval": 5,
|
||||
@@ -8227,7 +8227,7 @@
|
||||
"tags": "CA",
|
||||
"aimMethodRange": 3000,
|
||||
"shotsBaseScatter": 5,
|
||||
"canAAA": true,
|
||||
"canAAA": false,
|
||||
"alertnessTimeConstant": 3
|
||||
},
|
||||
"LiAZ Bus": {
|
||||
@@ -8619,24 +8619,25 @@
|
||||
"countries": "All"
|
||||
}
|
||||
},
|
||||
"acquisitionRange": 16000,
|
||||
"engagementRange": 2000,
|
||||
"acquisitionRange": 30000,
|
||||
"engagementRange": 21000,
|
||||
"description": "KS-19. 100 mm AAA gun. Fixed manually aimed large calibre anti aircraft gun.",
|
||||
"abilities": "AA",
|
||||
"canTargetPoint": false,
|
||||
"canRearm": false,
|
||||
"muzzleVelocity": 1000,
|
||||
"aimTime": 25,
|
||||
"shotsToFire": 5,
|
||||
"muzzleVelocity": 600,
|
||||
"aimTime": 50,
|
||||
"shotsToFire": 10,
|
||||
"barrelHeight": 5,
|
||||
"cost": null,
|
||||
"markerFile": "groundunit-aaa",
|
||||
"canAAA": true,
|
||||
"targetingRange": 100,
|
||||
"aimMethodRange": 15000,
|
||||
"aimMethodRange": 100,
|
||||
"shotsBaseInterval": 5,
|
||||
"shotsBaseScatter": 5,
|
||||
"alertnessTimeConstant": 5
|
||||
"shotsBaseScatter": 15,
|
||||
"alertnessTimeConstant": 5,
|
||||
"flak": true
|
||||
},
|
||||
"SON_9": {
|
||||
"name": "SON_9",
|
||||
@@ -8969,7 +8970,7 @@
|
||||
"cost": null,
|
||||
"barrelHeight": 2,
|
||||
"muzzleVelocity": 1000,
|
||||
"aimTime": 5,
|
||||
"aimTime": 6,
|
||||
"shotsToFire": 5,
|
||||
"tags": "CA",
|
||||
"markerFile": "groundunit-aaa",
|
||||
@@ -9016,7 +9017,7 @@
|
||||
"cost": null,
|
||||
"barrelHeight": 2,
|
||||
"muzzleVelocity": 1000,
|
||||
"aimTime": 5,
|
||||
"aimTime": 6,
|
||||
"shotsToFire": 5,
|
||||
"tags": "CA",
|
||||
"markerFile": "groundunit-aaa",
|
||||
@@ -9329,7 +9330,7 @@
|
||||
"canRearm": false,
|
||||
"barrelHeight": 2.6,
|
||||
"muzzleVelocity": 900,
|
||||
"aimTime": 5,
|
||||
"aimTime": 3,
|
||||
"shotsToFire": 5,
|
||||
"tags": "CA",
|
||||
"markerFile": "groundunit-apc",
|
||||
@@ -9513,20 +9514,20 @@
|
||||
}
|
||||
},
|
||||
"acquisitionRange": 10000,
|
||||
"engagementRange": 3000,
|
||||
"engagementRange": 9000,
|
||||
"description": "ZSU-57-2. Tracked self propelled optically guided AA gun. 2 x 57 mm auto cannon.",
|
||||
"abilities": "Combined arms, AA",
|
||||
"canTargetPoint": true,
|
||||
"canRearm": false,
|
||||
"muzzleVelocity": 1200,
|
||||
"muzzleVelocity": 1000,
|
||||
"barrelHeight": 3,
|
||||
"aimTime": 20,
|
||||
"aimTime": 15,
|
||||
"shotsToFire": 5,
|
||||
"cost": null,
|
||||
"tags": "CA",
|
||||
"markerFile": "groundunit-aaa",
|
||||
"canAAA": true,
|
||||
"aimMethodRange": 9000,
|
||||
"aimMethodRange": 100,
|
||||
"targetingRange": 100,
|
||||
"shotsBaseInterval": 5,
|
||||
"shotsBaseScatter": 5,
|
||||
@@ -9558,7 +9559,7 @@
|
||||
"canTargetPoint": true,
|
||||
"canRearm": false,
|
||||
"muzzleVelocity": 1000,
|
||||
"aimTime": 5,
|
||||
"aimTime": 10,
|
||||
"shotsToFire": 5,
|
||||
"barrelHeight": 2,
|
||||
"cost": null,
|
||||
@@ -9645,7 +9646,7 @@
|
||||
"barrelHeight": 2.7,
|
||||
"shotsBaseInterval": 8,
|
||||
"shotsToFire": 5,
|
||||
"aimTime": 5,
|
||||
"aimTime": 3,
|
||||
"shotsBaseScatter": 5,
|
||||
"alertnessTimeConstant": 3,
|
||||
"aimMethodRange": 3000,
|
||||
@@ -9771,7 +9772,7 @@
|
||||
"canRearm": false,
|
||||
"barrelHeight": 2.8,
|
||||
"muzzleVelocity": 900,
|
||||
"aimTime": 5,
|
||||
"aimTime": 4,
|
||||
"shotsToFire": 5,
|
||||
"tags": "CA",
|
||||
"markerFile": "groundunit-apc",
|
||||
@@ -10634,8 +10635,8 @@
|
||||
"countries": "All"
|
||||
}
|
||||
},
|
||||
"aimTime": 25,
|
||||
"shotsToFire": 2,
|
||||
"aimTime": 50,
|
||||
"shotsToFire": 5,
|
||||
"acquisitionRange": 15000,
|
||||
"engagementRange": 12000,
|
||||
"description": "The flak 88. Fixed anti aircraft gun famously also used as an anti-tank gun. 88mm flak gun.",
|
||||
@@ -10649,9 +10650,10 @@
|
||||
"canAAA": true,
|
||||
"shotsBaseInterval": 10,
|
||||
"shotsBaseScatter": 5,
|
||||
"aimMethodRange": 15000,
|
||||
"targetingRange": 200,
|
||||
"alertnessTimeConstant": 15
|
||||
"aimMethodRange": 100,
|
||||
"targetingRange": 100,
|
||||
"alertnessTimeConstant": 15,
|
||||
"flak": true
|
||||
},
|
||||
"Pz_IV_H": {
|
||||
"name": "Pz_IV_H",
|
||||
@@ -10768,7 +10770,7 @@
|
||||
"aimMethodRange": 3000,
|
||||
"barrelHeight": 2.7,
|
||||
"muzzleVelocity": 700,
|
||||
"aimTime": 5,
|
||||
"aimTime": 3,
|
||||
"shotsToFire": 5,
|
||||
"shotsBaseInterval": 6,
|
||||
"tags": "CA",
|
||||
@@ -10879,7 +10881,7 @@
|
||||
"barrelHeight": 2.7,
|
||||
"shotsBaseInterval": 7,
|
||||
"tags": "CA",
|
||||
"aimTime": 5,
|
||||
"aimTime": 6,
|
||||
"shotsToFire": 5,
|
||||
"aimMethodRange": 3000,
|
||||
"shotsBaseScatter": 5,
|
||||
@@ -10957,7 +10959,7 @@
|
||||
"barrelHeight": 2.7,
|
||||
"shotsBaseInterval": 7,
|
||||
"shotsToFire": 5,
|
||||
"aimTime": 5,
|
||||
"aimTime": 8,
|
||||
"tags": "CA",
|
||||
"aimMethodRange": 3000,
|
||||
"shotsBaseScatter": 5,
|
||||
@@ -11195,7 +11197,7 @@
|
||||
"alertnessTimeConstant": 3,
|
||||
"shotsBaseScatter": 5,
|
||||
"aimMethodRange": 3000,
|
||||
"canAAA": true
|
||||
"canAAA": false
|
||||
},
|
||||
"ZBD04A": {
|
||||
"name": "ZBD04A",
|
||||
@@ -11357,7 +11359,7 @@
|
||||
"aimMethodRange": 3000,
|
||||
"shotsBaseScatter": 5,
|
||||
"alertnessTimeConstant": 6,
|
||||
"canAAA": true
|
||||
"canAAA": false
|
||||
},
|
||||
"Kubelwagen_82": {
|
||||
"name": "Kubelwagen_82",
|
||||
@@ -11860,7 +11862,7 @@
|
||||
"canTargetPoint": true,
|
||||
"canRearm": false,
|
||||
"markerFile": "groundunit-aaa",
|
||||
"canAAA": true
|
||||
"canAAA": false
|
||||
},
|
||||
"Flakscheinwerfer_37": {
|
||||
"name": "Flakscheinwerfer_37",
|
||||
@@ -11896,7 +11898,8 @@
|
||||
"canTargetPoint": true,
|
||||
"canRearm": false,
|
||||
"markerFile": "groundunit-aaa",
|
||||
"canAAA": true
|
||||
"canAAA": true,
|
||||
"flak": true
|
||||
},
|
||||
"Maschinensatz_33": {
|
||||
"name": "Maschinensatz_33",
|
||||
@@ -11932,7 +11935,7 @@
|
||||
"canTargetPoint": true,
|
||||
"canRearm": false,
|
||||
"markerFile": "groundunit-aaa",
|
||||
"canAAA": true
|
||||
"canAAA": false
|
||||
},
|
||||
"soldier_mauser98": {
|
||||
"name": "soldier_mauser98",
|
||||
@@ -12516,7 +12519,8 @@
|
||||
"canTargetPoint": true,
|
||||
"canRearm": false,
|
||||
"markerFile": "groundunit-aaa",
|
||||
"canAAA": true
|
||||
"canAAA": true,
|
||||
"aimTime": 6
|
||||
},
|
||||
"Allies_Director": {
|
||||
"name": "Allies_Director",
|
||||
@@ -12886,7 +12890,8 @@
|
||||
"canTargetPoint": true,
|
||||
"canRearm": false,
|
||||
"markerFile": "groundunit-aaa",
|
||||
"canAAA": true
|
||||
"canAAA": true,
|
||||
"aimTime": 6
|
||||
},
|
||||
"M1_37mm": {
|
||||
"name": "M1_37mm",
|
||||
@@ -12922,7 +12927,8 @@
|
||||
"canTargetPoint": true,
|
||||
"canRearm": false,
|
||||
"markerFile": "groundunit-aaa",
|
||||
"canAAA": true
|
||||
"canAAA": true,
|
||||
"aimTime": 5
|
||||
},
|
||||
"DR_50Ton_Flat_Wagon": {
|
||||
"name": "DR_50Ton_Flat_Wagon",
|
||||
@@ -13069,7 +13075,8 @@
|
||||
"name": "Winter",
|
||||
"countries": "All"
|
||||
}
|
||||
}
|
||||
},
|
||||
"flak": true
|
||||
},
|
||||
"flak36": {
|
||||
"name": "flak36",
|
||||
@@ -13097,7 +13104,8 @@
|
||||
"name": "Winter",
|
||||
"countries": "All"
|
||||
}
|
||||
}
|
||||
},
|
||||
"flak": true
|
||||
},
|
||||
"flak37": {
|
||||
"name": "flak37",
|
||||
@@ -13125,7 +13133,8 @@
|
||||
"name": "Winter",
|
||||
"countries": "All"
|
||||
}
|
||||
}
|
||||
},
|
||||
"flak": true
|
||||
},
|
||||
"flak38": {
|
||||
"name": "flak38",
|
||||
@@ -13153,7 +13162,8 @@
|
||||
"name": "Winter",
|
||||
"countries": "All"
|
||||
}
|
||||
}
|
||||
},
|
||||
"flak": true
|
||||
},
|
||||
"flak41": {
|
||||
"name": "flak41",
|
||||
@@ -13181,7 +13191,8 @@
|
||||
"name": "Winter",
|
||||
"countries": "All"
|
||||
}
|
||||
}
|
||||
},
|
||||
"flak": true
|
||||
},
|
||||
"HEMTT_C-RAM_Phalanx": {
|
||||
"name": "HEMTT_C-RAM_Phalanx",
|
||||
|
||||
@@ -8,13 +8,6 @@
|
||||
"build-release": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.8.1",
|
||||
"chart.js": "^4.4.7",
|
||||
"react-chartjs-2": "^5.3.0",
|
||||
"react-circular-progressbar": "^2.1.0",
|
||||
"react-clock": "^5.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.6.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.5.1",
|
||||
@@ -35,7 +28,9 @@
|
||||
"@typescript-eslint/parser": "^7.14.1",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"axios": "^1.8.1",
|
||||
"buffer": "^6.0.3",
|
||||
"chart.js": "^4.4.7",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
@@ -54,6 +49,9 @@
|
||||
"postcss": "^8.4.38",
|
||||
"prettier": "^3.3.2",
|
||||
"react": "^18.2.0",
|
||||
"react-chartjs-2": "^5.3.0",
|
||||
"react-circular-progressbar": "^2.1.0",
|
||||
"react-clock": "^5.1.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-icons": "^5.0.1",
|
||||
"react-leaflet": "^4.2.1",
|
||||
@@ -61,7 +59,7 @@
|
||||
"turf": "^3.0.14",
|
||||
"typescript-eslint": "^7.14.1",
|
||||
"usng": "^0.3.0",
|
||||
"vite": "^5.2.0",
|
||||
"vite": "^6.2.2",
|
||||
"vite-plugin-externals": "^0.6.2",
|
||||
"vite-plugin-file": "^1.0.5",
|
||||
"web-audio-peak-meter": "^3.1.0"
|
||||
|
||||
122
frontend/react/public/images/markers/flak.svg
Normal file
122
frontend/react/public/images/markers/flak.svg
Normal file
@@ -0,0 +1,122 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="52"
|
||||
height="52"
|
||||
viewBox="0 0 13.758333 13.758333"
|
||||
version="1.1"
|
||||
id="svg5"
|
||||
inkscape:version="1.3.2 (091e20e, 2023-11-25, custom)"
|
||||
sodipodi:docname="flak.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview7"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:document-units="mm"
|
||||
showgrid="false"
|
||||
width="52mm"
|
||||
units="px"
|
||||
inkscape:zoom="8.3856042"
|
||||
inkscape:cx="17.649295"
|
||||
inkscape:cy="16.516401"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1009"
|
||||
inkscape:window-x="1912"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:deskcolor="#d1d1d1" />
|
||||
<defs
|
||||
id="defs2">
|
||||
<linearGradient
|
||||
id="linearGradient4717"
|
||||
inkscape:swatch="solid">
|
||||
<stop
|
||||
style="stop-color:#0cffff;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop4715" />
|
||||
</linearGradient>
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath3">
|
||||
<path
|
||||
style="fill:#fbfbfb;fill-opacity:1;stroke:#ffffff;stroke-width:0.285001;stroke-linecap:butt;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
|
||||
d="M 6.5629959,12.964765 1.2937969,11.166296 4.7014228,11.418713 1.4831099,6.9067638 5.0484958,9.6833476 3.5024428,0.84876278 6.846965,9.6202434 8.645434,3.025857 8.0143919,9.9357643 11.737538,6.8752117 l -3.18676,4.1017713 4.354188,-0.09466 z"
|
||||
id="path3" />
|
||||
</clipPath>
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath3-3">
|
||||
<path
|
||||
style="fill:#fbfbfb;fill-opacity:1;stroke:#ffffff;stroke-width:0.285001;stroke-linecap:butt;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
|
||||
d="M 6.5629959,12.964765 1.2937969,11.166296 4.7014228,11.418713 1.4831099,6.9067638 5.0484958,9.6833476 3.5024428,0.84876278 6.846965,9.6202434 8.645434,3.025857 8.0143919,9.9357643 11.737538,6.8752117 l -3.18676,4.1017713 4.354188,-0.09466 z"
|
||||
id="path3-4" />
|
||||
</clipPath>
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath4">
|
||||
<ellipse
|
||||
style="fill:#f48800;fill-opacity:1;stroke:none;stroke-width:1.10409;stroke-linecap:butt;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
|
||||
id="ellipse4"
|
||||
cx="6.4735489"
|
||||
cy="8.0908279"
|
||||
rx="7.8196473"
|
||||
ry="8.125103"
|
||||
clip-path="url(#clipPath3)" />
|
||||
</clipPath>
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath3-3-7">
|
||||
<path
|
||||
style="fill:#fbfbfb;fill-opacity:1;stroke:#ffffff;stroke-width:0.285001;stroke-linecap:butt;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
|
||||
d="M 6.5629959,12.964765 1.2937969,11.166296 4.7014228,11.418713 1.4831099,6.9067638 5.0484958,9.6833476 3.5024428,0.84876278 6.846965,9.6202434 8.645434,3.025857 8.0143919,9.9357643 11.737538,6.8752117 l -3.18676,4.1017713 4.354188,-0.09466 z"
|
||||
id="path3-4-8" />
|
||||
</clipPath>
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath5">
|
||||
<ellipse
|
||||
style="fill:#f48800;fill-opacity:1;stroke:none;stroke-width:1.10409;stroke-linecap:butt;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
|
||||
id="ellipse5"
|
||||
cx="6.4735489"
|
||||
cy="8.0908279"
|
||||
rx="7.8196473"
|
||||
ry="8.125103"
|
||||
clip-path="url(#clipPath3-3-7)"
|
||||
transform="translate(-0.00203625,0.00601104)" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<path
|
||||
sodipodi:type="star"
|
||||
style="fill:#424242;stroke:#000000;stroke-width:1;stroke-linejoin:round;paint-order:stroke fill markers"
|
||||
id="path1"
|
||||
inkscape:flatsided="false"
|
||||
sodipodi:sides="20"
|
||||
sodipodi:cx="7.1623244"
|
||||
sodipodi:cy="7.4462934"
|
||||
sodipodi:r1="5.9949799"
|
||||
sodipodi:r2="2.9974899"
|
||||
sodipodi:arg1="-1.5760594"
|
||||
sodipodi:arg2="-1.4189798"
|
||||
inkscape:rounded="0"
|
||||
inkscape:randomized="0"
|
||||
d="M 7.1307726,1.4513965 7.6156469,4.4832806 8.9848418,1.7350576 8.509081,4.7683851 10.66051,2.5777742 9.2706852,5.3156219 11.993752,3.8970554 9.8259084,6.0714237 12.85406,5.5637607 10.120401,6.9618073 13.157221,7.4147415 10.125337,7.8996158 12.87356,9.2688107 9.8402327,8.7930499 12.030844,10.944479 9.2929959,9.5546541 10.711562,12.277721 8.5371941,10.109877 9.044857,13.138029 7.6468105,10.40437 7.1938763,13.44119 6.709002,10.409306 5.339807,13.157529 5.8155679,10.124202 3.6641385,12.314812 5.0539637,9.5769648 2.3308967,10.995531 4.4987405,8.821163 1.4705888,9.328826 4.2042475,7.9307794 1.1674276,7.4778452 4.1993117,6.9929709 1.4510887,5.623776 4.4844162,6.0995368 2.2938053,3.9481074 5.031653,5.3379326 3.6130865,2.6148657 5.7874548,4.7827094 5.2797918,1.7545577 6.6778384,4.4882164 Z"
|
||||
inkscape:transform-center-x="0.0040186351"
|
||||
inkscape:transform-center-y="-0.23212346"
|
||||
transform="matrix(0.41215918,0,0,0.41215918,4.0209941,3.8724018)" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.6 KiB |
43
frontend/react/public/images/units/map/awacs/blue/shell.svg
Normal file
43
frontend/react/public/images/units/map/awacs/blue/shell.svg
Normal file
@@ -0,0 +1,43 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="50"
|
||||
height="50"
|
||||
viewBox="0 0 50 50"
|
||||
fill="none"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
sodipodi:docname="shell.svg"
|
||||
inkscape:version="1.3.2 (091e20e, 2023-11-25, custom)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs1" />
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#505050"
|
||||
bordercolor="#eeeeee"
|
||||
borderopacity="1"
|
||||
inkscape:showpageshadow="0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#505050"
|
||||
inkscape:zoom="16.26"
|
||||
inkscape:cx="25.03075"
|
||||
inkscape:cy="25"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1009"
|
||||
inkscape:window-x="1912"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg1" />
|
||||
<path
|
||||
d="m 24.992747,19.632095 c 1.718432,1.191442 1.737049,6.906456 1.741766,10.585026 h -3.483451 c 0.07653,-3.377681 -0.03261,-9.113527 1.741685,-10.585026 z"
|
||||
fill="#5ca7ff"
|
||||
stroke="#082e44"
|
||||
stroke-width="0.814233"
|
||||
id="path1"
|
||||
sodipodi:nodetypes="cccc"
|
||||
style="fill:#424242;fill-opacity:1;stroke:#000000;stroke-opacity:1" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,43 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="50"
|
||||
height="50"
|
||||
viewBox="0 0 50 50"
|
||||
fill="none"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
sodipodi:docname="shell.svg"
|
||||
inkscape:version="1.3.2 (091e20e, 2023-11-25, custom)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs1" />
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#505050"
|
||||
bordercolor="#eeeeee"
|
||||
borderopacity="1"
|
||||
inkscape:showpageshadow="0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#505050"
|
||||
inkscape:zoom="16.26"
|
||||
inkscape:cx="25.03075"
|
||||
inkscape:cy="25"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1009"
|
||||
inkscape:window-x="1912"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg1" />
|
||||
<path
|
||||
d="m 24.992747,19.632095 c 1.718432,1.191442 1.737049,6.906456 1.741766,10.585026 h -3.483451 c 0.07653,-3.377681 -0.03261,-9.113527 1.741685,-10.585026 z"
|
||||
fill="#5ca7ff"
|
||||
stroke="#082e44"
|
||||
stroke-width="0.814233"
|
||||
id="path1"
|
||||
sodipodi:nodetypes="cccc"
|
||||
style="fill:#424242;fill-opacity:1;stroke:#000000;stroke-opacity:1" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
43
frontend/react/public/images/units/map/normal/blue/shell.svg
Normal file
43
frontend/react/public/images/units/map/normal/blue/shell.svg
Normal file
@@ -0,0 +1,43 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="50"
|
||||
height="50"
|
||||
viewBox="0 0 50 50"
|
||||
fill="none"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
sodipodi:docname="shell.svg"
|
||||
inkscape:version="1.3.2 (091e20e, 2023-11-25, custom)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs1" />
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#505050"
|
||||
bordercolor="#eeeeee"
|
||||
borderopacity="1"
|
||||
inkscape:showpageshadow="0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#505050"
|
||||
inkscape:zoom="16.26"
|
||||
inkscape:cx="25.03075"
|
||||
inkscape:cy="25"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1009"
|
||||
inkscape:window-x="1912"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg1" />
|
||||
<path
|
||||
d="m 24.992747,19.632095 c 1.718432,1.191442 1.737049,6.906456 1.741766,10.585026 h -3.483451 c 0.07653,-3.377681 -0.03261,-9.113527 1.741685,-10.585026 z"
|
||||
fill="#5ca7ff"
|
||||
stroke="#082e44"
|
||||
stroke-width="0.814233"
|
||||
id="path1"
|
||||
sodipodi:nodetypes="cccc"
|
||||
style="fill:#424242;fill-opacity:1;stroke:#000000;stroke-opacity:1" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,43 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="50"
|
||||
height="50"
|
||||
viewBox="0 0 50 50"
|
||||
fill="none"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
sodipodi:docname="shell.svg"
|
||||
inkscape:version="1.3.2 (091e20e, 2023-11-25, custom)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs1" />
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#505050"
|
||||
bordercolor="#eeeeee"
|
||||
borderopacity="1"
|
||||
inkscape:showpageshadow="0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#505050"
|
||||
inkscape:zoom="16.26"
|
||||
inkscape:cx="25.03075"
|
||||
inkscape:cy="25"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1009"
|
||||
inkscape:window-x="1912"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg1" />
|
||||
<path
|
||||
d="m 24.992747,19.632095 c 1.718432,1.191442 1.737049,6.906456 1.741766,10.585026 h -3.483451 c 0.07653,-3.377681 -0.03261,-9.113527 1.741685,-10.585026 z"
|
||||
fill="#5ca7ff"
|
||||
stroke="#082e44"
|
||||
stroke-width="0.814233"
|
||||
id="path1"
|
||||
sodipodi:nodetypes="cccc"
|
||||
style="fill:#424242;fill-opacity:1;stroke:#000000;stroke-opacity:1" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
43
frontend/react/public/images/units/map/normal/red/shell.svg
Normal file
43
frontend/react/public/images/units/map/normal/red/shell.svg
Normal file
@@ -0,0 +1,43 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="50"
|
||||
height="50"
|
||||
viewBox="0 0 50 50"
|
||||
fill="none"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
sodipodi:docname="shell.svg"
|
||||
inkscape:version="1.3.2 (091e20e, 2023-11-25, custom)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs1" />
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#505050"
|
||||
bordercolor="#eeeeee"
|
||||
borderopacity="1"
|
||||
inkscape:showpageshadow="0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#505050"
|
||||
inkscape:zoom="16.26"
|
||||
inkscape:cx="25.03075"
|
||||
inkscape:cy="25"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1009"
|
||||
inkscape:window-x="1912"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg1" />
|
||||
<path
|
||||
d="m 24.992747,19.632095 c 1.718432,1.191442 1.737049,6.906456 1.741766,10.585026 h -3.483451 c 0.07653,-3.377681 -0.03261,-9.113527 1.741685,-10.585026 z"
|
||||
fill="#5ca7ff"
|
||||
stroke="#082e44"
|
||||
stroke-width="0.814233"
|
||||
id="path1"
|
||||
sodipodi:nodetypes="cccc"
|
||||
style="fill:#424242;fill-opacity:1;stroke:#000000;stroke-opacity:1" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -2,7 +2,7 @@ import { AudioMessageType, BLUE_COMMANDER, GAME_MASTER, OlympusState, RED_COMMAN
|
||||
import { MicrophoneSource } from "./microphonesource";
|
||||
import { RadioSink } from "./radiosink";
|
||||
import { getApp } from "../olympusapp";
|
||||
import { makeID } from "../other/utils";
|
||||
import { coalitionToEnum, makeID } from "../other/utils";
|
||||
import { FileSource } from "./filesource";
|
||||
import { AudioSource } from "./audiosource";
|
||||
import { Buffer } from "buffer";
|
||||
@@ -54,7 +54,10 @@ export class AudioManager {
|
||||
|
||||
constructor() {
|
||||
ConfigLoadedEvent.on((config: OlympusConfig) => {
|
||||
config.audio.WSPort ? this.setPort(config.audio.WSPort) : this.setEndpoint(config.audio.WSEndpoint);
|
||||
if (config.audio) {
|
||||
this.setPort(config.audio.WSPort);
|
||||
this.setEndpoint(config.audio.WSEndpoint);
|
||||
} else console.error("No audio configuration found in the Olympus configuration file");
|
||||
});
|
||||
|
||||
CommandModeOptionsChangedEvent.on((options: CommandModeOptions) => {
|
||||
@@ -98,11 +101,19 @@ export class AudioManager {
|
||||
|
||||
let wsAddress = res ? res[1] : location.toString();
|
||||
if (wsAddress.at(wsAddress.length - 1) === "/") wsAddress = wsAddress.substring(0, wsAddress.length - 1);
|
||||
if (this.#endpoint) this.#socket = new WebSocket(`wss://${wsAddress}/${this.#endpoint}`);
|
||||
else if (this.#port) this.#socket = new WebSocket(`ws://${wsAddress}:${this.#port}`);
|
||||
else console.error("The audio backend was enabled but no port/endpoint was provided in the configuration");
|
||||
|
||||
if (!this.#socket) return;
|
||||
if (this.#port === undefined && this.#endpoint === undefined) {
|
||||
console.error("The audio backend was enabled but no port/endpoint was provided in the configuration");
|
||||
return;
|
||||
}
|
||||
|
||||
this.#socket = new WebSocket(`wss://${wsAddress}/${this.#endpoint}`);
|
||||
if (!this.#socket) this.#socket = new WebSocket(`ws://${wsAddress}:${this.#port}`);
|
||||
|
||||
if (!this.#socket) {
|
||||
console.error("Failed to connect to audio websocket");
|
||||
return;
|
||||
}
|
||||
|
||||
/* Log the opening of the connection */
|
||||
this.#socket.addEventListener("open", (event) => {
|
||||
@@ -376,7 +387,7 @@ export class AudioManager {
|
||||
let message = {
|
||||
type: "Settings update",
|
||||
guid: this.#guid,
|
||||
coalition: this.#coalition,
|
||||
coalition: coalitionToEnum(this.#coalition),
|
||||
settings: this.#sinks
|
||||
.filter((sink) => sink instanceof RadioSink)
|
||||
.map((radio) => {
|
||||
|
||||
@@ -329,7 +329,8 @@ export enum OlympusState {
|
||||
WARNING = "Warning modal",
|
||||
DATABASE_EDITOR = "Database editor",
|
||||
MEASURE = "Measure",
|
||||
TRAINING = "Training"
|
||||
TRAINING = "Training",
|
||||
ADMIN = "Admin",
|
||||
}
|
||||
|
||||
export const NO_SUBSTATE = "No substate";
|
||||
@@ -385,6 +386,7 @@ export enum WarningSubstate {
|
||||
NO_SUBSTATE = "No substate",
|
||||
NOT_CHROME = "Not chrome",
|
||||
NOT_SECURE = "Not secure",
|
||||
ERROR_UPLOADING_CONFIG = "Error uploading config",
|
||||
}
|
||||
|
||||
export type OlympusSubState = DrawSubState | JTACSubState | SpawnSubState | OptionsSubstate | string;
|
||||
@@ -417,7 +419,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 +499,17 @@ export enum DataIndexes {
|
||||
racetrackLength,
|
||||
racetrackAnchor,
|
||||
racetrackBearing,
|
||||
timeToNextTasking,
|
||||
barrelHeight,
|
||||
muzzleVelocity,
|
||||
aimTime,
|
||||
shotsToFire,
|
||||
shotsBaseInterval,
|
||||
shotsBaseScatter,
|
||||
engagementRange,
|
||||
targetingRange,
|
||||
aimMethodRange,
|
||||
acquisitionRange,
|
||||
endOfData = 255,
|
||||
}
|
||||
|
||||
@@ -530,7 +543,6 @@ export enum ContextActionType {
|
||||
DELETE,
|
||||
}
|
||||
|
||||
|
||||
export enum colors {
|
||||
ALICE_BLUE = "#F0F8FF",
|
||||
ANTIQUE_WHITE = "#FAEBD7",
|
||||
@@ -679,7 +691,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 +931,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 }
|
||||
);
|
||||
|
||||
@@ -230,6 +230,23 @@ export class SessionDataChangedEvent {
|
||||
export class SessionDataSavedEvent extends SessionDataChangedEvent {}
|
||||
export class SessionDataLoadedEvent extends SessionDataChangedEvent {}
|
||||
|
||||
export class AdminPasswordChangedEvent {
|
||||
static on(callback: (password: string) => void, singleShot = false) {
|
||||
document.addEventListener(
|
||||
this.name,
|
||||
(ev: CustomEventInit) => {
|
||||
callback(ev.detail.password);
|
||||
},
|
||||
{ once: singleShot }
|
||||
);
|
||||
}
|
||||
|
||||
static dispatch(password: string) {
|
||||
document.dispatchEvent(new CustomEvent(this.name, { detail: { password } }));
|
||||
console.log(`Event ${this.name} dispatched`);
|
||||
}
|
||||
}
|
||||
|
||||
/************** Map events ***************/
|
||||
export class MouseMovedEvent {
|
||||
static on(callback: (latlng: LatLng, elevation: number) => void, singleShot = false) {
|
||||
|
||||
@@ -2,13 +2,9 @@ import { LatLng } from "leaflet";
|
||||
import { Coalition, MapOptions } from "./types/types";
|
||||
|
||||
export interface OlympusConfig {
|
||||
/* Set by user */
|
||||
frontend: {
|
||||
port: number;
|
||||
customAuthHeaders: {
|
||||
enabled: boolean;
|
||||
username: string;
|
||||
group: string;
|
||||
};
|
||||
elevationProvider: {
|
||||
provider: string;
|
||||
username: string | null;
|
||||
@@ -25,17 +21,27 @@ export interface OlympusConfig {
|
||||
mapMirrors: {
|
||||
[key: string]: string;
|
||||
};
|
||||
autoconnectWhenLocal: boolean;
|
||||
/* New with v2.0.0 */
|
||||
customAuthHeaders?: {
|
||||
enabled: boolean;
|
||||
username: string;
|
||||
group: string;
|
||||
};
|
||||
autoconnectWhenLocal?: boolean;
|
||||
};
|
||||
audio: {
|
||||
/* New with v2.0.0 */
|
||||
audio?: {
|
||||
SRSPort: number;
|
||||
WSPort?: number;
|
||||
WSEndpoint?: string;
|
||||
};
|
||||
controllers: [{ type: string; coalition: Coalition; frequency: number; modulation: number; callsign: string }];
|
||||
local: boolean;
|
||||
controllers?: [{ type: string; coalition: Coalition; frequency: number; modulation: number; callsign: string }];
|
||||
profiles?: { [key: string]: ProfileOptions };
|
||||
|
||||
/* Set by server */
|
||||
local?: boolean;
|
||||
authentication?: {
|
||||
// Only sent when in localhost mode for autologin
|
||||
gameMasterPassword: string;
|
||||
blueCommanderPasword: string;
|
||||
redCommanderPassword: string;
|
||||
@@ -48,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 {
|
||||
@@ -83,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]: { active: boolean; type: string; targetPosition: { lat: number; lng: number }; sourceUnitID: number; code?: number };
|
||||
};
|
||||
sessionHash: string;
|
||||
time: number;
|
||||
@@ -261,6 +267,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 {
|
||||
@@ -402,4 +419,4 @@ export enum AlarmState {
|
||||
AUTO = 'auto',
|
||||
GREEN = 'green',
|
||||
RED = 'red'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,10 +17,12 @@ export var BoxSelect = Handler.extend({
|
||||
|
||||
addHooks: function () {
|
||||
DomEvent.on(this._container, "mousedown", this._onMouseDown, this);
|
||||
DomEvent.on(this._container, "touchstart", this._onMouseDown, this);
|
||||
},
|
||||
|
||||
removeHooks: function () {
|
||||
DomEvent.off(this._container, "mousedown", this._onMouseDown, this);
|
||||
DomEvent.off(this._container, "touchend", this._onMouseDown, this);
|
||||
},
|
||||
|
||||
moved: function () {
|
||||
@@ -37,7 +39,7 @@ export var BoxSelect = Handler.extend({
|
||||
},
|
||||
|
||||
_onMouseDown: function (e: any) {
|
||||
if (this._map.getSelectionEnabled() && e.button == 0) {
|
||||
if (this._map.getSelectionEnabled() && (e.button == 0 || e.type === "touchstart")) {
|
||||
if (this._moved) this._finish();
|
||||
|
||||
DomUtil.disableImageDrag();
|
||||
@@ -64,7 +66,7 @@ export var BoxSelect = Handler.extend({
|
||||
},
|
||||
|
||||
_onMouseUp: function (e: any) {
|
||||
if (e.button !== 0) return;
|
||||
if (e.button !== 0 && e.type !== "touchend") return;
|
||||
window.setTimeout(Util.bind(this._finish, this), 0);
|
||||
if (!this._moved) return;
|
||||
var bounds = new LatLngBounds(this._map.containerPointToLatLng(this._startPoint), this._map.containerPointToLatLng(this._point));
|
||||
|
||||
@@ -3,6 +3,7 @@ import { getApp } from "../../olympusapp";
|
||||
import { DrawingsInitEvent, DrawingsUpdatedEvent, MapOptionsChangedEvent, SessionDataLoadedEvent } from "../../events";
|
||||
import { MapOptions } from "../../types/types";
|
||||
import { Circle, DivIcon, Layer, LayerGroup, layerGroup, Marker, Polygon, Polyline } from "leaflet";
|
||||
import { NavpointMarker } from "../markers/navpointmarker";
|
||||
|
||||
export abstract class DCSDrawing {
|
||||
#name: string;
|
||||
@@ -452,6 +453,57 @@ export class DCSTextBox extends DCSDrawing {
|
||||
}
|
||||
}
|
||||
|
||||
export class DCSNavpoint extends DCSDrawing {
|
||||
#point: NavpointMarker;
|
||||
|
||||
constructor(drawingData, parent) {
|
||||
super(drawingData, parent);
|
||||
|
||||
this.#point = new NavpointMarker([drawingData.lat, drawingData.lng], drawingData.callsignStr, drawingData.comment);
|
||||
|
||||
this.setVisibility(true);
|
||||
}
|
||||
|
||||
getLayer() {
|
||||
return this.#point;
|
||||
}
|
||||
|
||||
getLabelLayer() {
|
||||
return this.#point;
|
||||
}
|
||||
|
||||
setOpacity(opacity: number): void {
|
||||
if (opacity === this.#point.options.opacity) return;
|
||||
|
||||
this.#point.options.opacity = opacity;
|
||||
|
||||
/* Hack to force marker redraw */
|
||||
const originalVisibility = this.getVisibility();
|
||||
this.setVisibility(false);
|
||||
this.setVisibility(originalVisibility);
|
||||
|
||||
getApp().getDrawingsManager().requestUpdateEventDispatch();
|
||||
}
|
||||
|
||||
setVisibility(visibility: boolean): void {
|
||||
if (visibility && !this.getParent().getLayerGroup().hasLayer(this.#point)) this.#point.addTo(this.getParent().getLayerGroup());
|
||||
//@ts-ignore Leaflet typings are wrong
|
||||
if (!visibility && this.getParent().getLayerGroup().hasLayer(this.#point)) this.#point.removeFrom(this.getParent().getLayerGroup());
|
||||
|
||||
if (visibility && !this.getParent().getVisibility()) this.getParent().setVisibility(true);
|
||||
|
||||
getApp().getDrawingsManager().requestUpdateEventDispatch();
|
||||
}
|
||||
|
||||
getOpacity(): number {
|
||||
return this.#point.options.opacity ?? 1;
|
||||
}
|
||||
|
||||
getVisibility(): boolean {
|
||||
return this.getParent().getLayerGroup().hasLayer(this.#point);
|
||||
}
|
||||
}
|
||||
|
||||
export class DCSDrawingsContainer {
|
||||
#drawings: DCSDrawing[] = [];
|
||||
#subContainers: DCSDrawingsContainer[] = [];
|
||||
@@ -475,7 +527,10 @@ export class DCSDrawingsContainer {
|
||||
initFromData(drawingsData) {
|
||||
let hasContainers = false;
|
||||
Object.keys(drawingsData).forEach((layerName: string) => {
|
||||
if (drawingsData[layerName]["name"] === undefined) {
|
||||
if (layerName === 'navpoints') {
|
||||
return;
|
||||
}
|
||||
if (drawingsData[layerName]["name"] === undefined && drawingsData[layerName]["callsignStr"] === undefined) {
|
||||
const newContainer = new DCSDrawingsContainer(layerName, this);
|
||||
this.addSubContainer(newContainer);
|
||||
newContainer.initFromData(drawingsData[layerName]);
|
||||
@@ -487,6 +542,7 @@ export class DCSDrawingsContainer {
|
||||
|
||||
Object.keys(drawingsData).forEach((layerName: string) => {
|
||||
const primitiveType = drawingsData[layerName]["primitiveType"];
|
||||
const isANavpoint = !!drawingsData[layerName]['callsignStr'];
|
||||
|
||||
// Possible primitives:
|
||||
// "Line","TextBox","Polygon","Icon"
|
||||
@@ -499,6 +555,14 @@ export class DCSDrawingsContainer {
|
||||
|
||||
let newDrawing = new DCSEmptyLayer(drawingsData[layerName], othersContainer) as DCSDrawing;
|
||||
|
||||
if (isANavpoint) {
|
||||
newDrawing = new DCSNavpoint(drawingsData[layerName], othersContainer);
|
||||
if (hasContainers) othersContainer.addDrawing(newDrawing);
|
||||
else this.addDrawing(newDrawing);
|
||||
if (othersContainer.getDrawings().length === 0) this.removeSubContainer(othersContainer); // Remove empty container
|
||||
return;
|
||||
}
|
||||
|
||||
switch (primitiveType) {
|
||||
case "Polygon":
|
||||
newDrawing = new DCSPolygon(drawingsData[layerName], othersContainer);
|
||||
@@ -521,6 +585,12 @@ export class DCSDrawingsContainer {
|
||||
if (othersContainer.getDrawings().length === 0) this.removeSubContainer(othersContainer); // Remove empty container
|
||||
}
|
||||
|
||||
initNavpoints(drawingsData) {
|
||||
const newContainer = new DCSDrawingsContainer('Navpoints', this);
|
||||
this.addSubContainer(newContainer);
|
||||
newContainer.initFromData(drawingsData);
|
||||
}
|
||||
|
||||
getLayerGroup() {
|
||||
return this.#layerGroup;
|
||||
}
|
||||
@@ -649,13 +719,16 @@ export class DrawingsManager {
|
||||
SessionDataLoadedEvent.on((sessionData) => {
|
||||
this.#sessionDataDrawings = sessionData.drawings ?? {};
|
||||
if (this.#initialized) if (this.#sessionDataDrawings["Mission drawings"]) this.#drawingsContainer.fromJSON(this.#sessionDataDrawings["Mission drawings"]);
|
||||
this.#drawingsContainer.setVisibility(getApp().getMap().getOptions().showMissionDrawings);
|
||||
});
|
||||
}
|
||||
|
||||
initDrawings(data: { drawings: Record<string, Record<string, any>> }): boolean {
|
||||
if (data && data.drawings) {
|
||||
this.#drawingsContainer.initFromData(data.drawings);
|
||||
if (data.drawings.navpoints) this.#drawingsContainer.initNavpoints(data.drawings.navpoints);
|
||||
if (this.#sessionDataDrawings["Mission drawings"]) this.#drawingsContainer.fromJSON(this.#sessionDataDrawings["Mission drawings"]);
|
||||
this.#drawingsContainer.setVisibility(getApp().getMap().getOptions().showMissionDrawings);
|
||||
DrawingsInitEvent.dispatch(this.#drawingsContainer);
|
||||
this.#initialized = true;
|
||||
return true;
|
||||
|
||||
@@ -37,6 +37,7 @@ import "./markers/stylesheets/bullseye.css";
|
||||
import "./markers/stylesheets/units.css";
|
||||
import "./markers/stylesheets/spot.css";
|
||||
import "./markers/stylesheets/measure.css";
|
||||
import "./markers/stylesheets/navpoint.css";
|
||||
import "./stylesheets/map.css";
|
||||
|
||||
import { initDraggablePath } from "./coalitionarea/draggablepath";
|
||||
@@ -67,6 +68,7 @@ import {
|
||||
import { ContextActionSet } from "../unit/contextactionset";
|
||||
import { SmokeMarker } from "./markers/smokemarker";
|
||||
import { Measure } from "./measure";
|
||||
import { FlakMarker } from "./markers/flakmarker";
|
||||
|
||||
/* Register the handler for the box selection */
|
||||
L.Map.addInitHook("addHandler", "boxSelect", BoxSelect);
|
||||
@@ -207,7 +209,9 @@ export class Map extends L.Map {
|
||||
this.on("selectionend", (e: any) => this.#onSelectionEnd(e));
|
||||
|
||||
this.on("mouseup", (e: any) => this.#onMouseUp(e));
|
||||
this.on("touchend", (e: any) => this.#onMouseUp(e));
|
||||
this.on("mousedown", (e: any) => this.#onMouseDown(e));
|
||||
this.on("touchstart", (e: any) => this.#onMouseDown(e));
|
||||
this.on("dblclick", (e: any) => this.#onDoubleClick(e));
|
||||
this.on("click", (e: any) => e.originalEvent.preventDefault());
|
||||
this.on("contextmenu", (e: any) => e.originalEvent.preventDefault());
|
||||
@@ -506,11 +510,6 @@ export class Map extends L.Map {
|
||||
altKey: false,
|
||||
ctrlKey: false,
|
||||
});
|
||||
|
||||
/* Periodically check if the camera control endpoint is available */
|
||||
this.#cameraControlTimer = window.setInterval(() => {
|
||||
this.#checkCameraPort();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
setLayerName(layerName: string) {
|
||||
@@ -766,6 +765,12 @@ export class Map extends L.Map {
|
||||
return explosionMarker;
|
||||
}
|
||||
|
||||
addFlakMarker(latlng: L.LatLng) {
|
||||
const explosionMarker = new FlakMarker(latlng, 10);
|
||||
explosionMarker.addTo(this);
|
||||
return explosionMarker;
|
||||
}
|
||||
|
||||
addSmokeMarker(latlng: L.LatLng, color: string) {
|
||||
const smokeMarker = new SmokeMarker(latlng, color);
|
||||
smokeMarker.addTo(this);
|
||||
@@ -807,6 +812,10 @@ export class Map extends L.Map {
|
||||
|
||||
setSelectionEnabled(selectionEnabled: boolean) {
|
||||
this.#selectionEnabled = selectionEnabled;
|
||||
|
||||
if (selectionEnabled) this.dragging.disable();
|
||||
else this.dragging.enable();
|
||||
|
||||
SelectionEnabledChangedEvent.dispatch(selectionEnabled);
|
||||
}
|
||||
|
||||
@@ -960,6 +969,9 @@ export class Map extends L.Map {
|
||||
#onSelectionEnd(e: any) {
|
||||
getApp().getUnitsManager().selectFromBounds(e.selectionBounds);
|
||||
|
||||
// Autodisable the selection mode if touchscreen
|
||||
if ("ontouchstart" in window) this.setSelectionEnabled(false);
|
||||
|
||||
/* Delay the event so that any other event in the queue still sees the map in selection mode */
|
||||
window.setTimeout(() => {
|
||||
this.#isSelecting = false;
|
||||
@@ -994,7 +1006,7 @@ export class Map extends L.Map {
|
||||
}
|
||||
|
||||
#onMouseDown(e: any) {
|
||||
if (e.originalEvent.button === 1) {
|
||||
if (e.originalEvent?.button === 1) {
|
||||
this.dragging.disable();
|
||||
} // Disable dragging when right clicking
|
||||
|
||||
@@ -1297,33 +1309,6 @@ export class Map extends L.Map {
|
||||
return minimapBoundaries;
|
||||
}
|
||||
|
||||
#setSlaveDCSCameraAvailable(newSlaveDCSCameraAvailable: boolean) {
|
||||
this.#slaveDCSCameraAvailable = newSlaveDCSCameraAvailable;
|
||||
}
|
||||
|
||||
/* Check if the camera control plugin is available. Right now this will only change the color of the button, no changes in functionality */
|
||||
#checkCameraPort() {
|
||||
if (this.#cameraOptionsXmlHttp?.readyState !== 4) this.#cameraOptionsXmlHttp?.abort();
|
||||
|
||||
this.#cameraOptionsXmlHttp = new XMLHttpRequest();
|
||||
|
||||
/* Using 127.0.0.1 instead of localhost because the LuaSocket version used in DCS only listens to IPv4. This avoids the lag caused by the
|
||||
browser if it first tries to send the request on the IPv6 address for localhost */
|
||||
this.#cameraOptionsXmlHttp.open("OPTIONS", `http://127.0.0.1:${this.#cameraControlPort}`);
|
||||
this.#cameraOptionsXmlHttp.onload = (res: any) => {
|
||||
if (this.#cameraOptionsXmlHttp !== null && this.#cameraOptionsXmlHttp.status == 204) this.#setSlaveDCSCameraAvailable(true);
|
||||
else this.#setSlaveDCSCameraAvailable(false);
|
||||
};
|
||||
this.#cameraOptionsXmlHttp.onerror = (res: any) => {
|
||||
this.#setSlaveDCSCameraAvailable(false);
|
||||
};
|
||||
this.#cameraOptionsXmlHttp.ontimeout = (res: any) => {
|
||||
this.#setSlaveDCSCameraAvailable(false);
|
||||
};
|
||||
this.#cameraOptionsXmlHttp.timeout = 500;
|
||||
this.#cameraOptionsXmlHttp.send("");
|
||||
}
|
||||
|
||||
#drawIPToTargetLine() {
|
||||
if (this.#targetPoint && this.#IPPoint) {
|
||||
if (!this.#IPToTargetLine) {
|
||||
|
||||
39
frontend/react/src/map/markers/flakmarker.ts
Normal file
39
frontend/react/src/map/markers/flakmarker.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { CustomMarker } from "./custommarker";
|
||||
import { DivIcon, LatLng } from "leaflet";
|
||||
import { SVGInjector } from "@tanem/svg-injector";
|
||||
import { getApp } from "../../olympusapp";
|
||||
|
||||
export class FlakMarker extends CustomMarker {
|
||||
#timer: number = 0;
|
||||
#timeout: number = 0;
|
||||
|
||||
constructor(latlng: LatLng, timeout?: number) {
|
||||
super(latlng, { interactive: false });
|
||||
|
||||
if (timeout) {
|
||||
this.#timeout = timeout;
|
||||
|
||||
this.#timer = window.setTimeout(() => {
|
||||
this.removeFrom(getApp().getMap());
|
||||
}, timeout * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
createIcon() {
|
||||
/* Set the icon */
|
||||
this.setIcon(
|
||||
new DivIcon({
|
||||
iconSize: [52, 52],
|
||||
iconAnchor: [26, 26],
|
||||
className: "leaflet-flak-marker",
|
||||
})
|
||||
);
|
||||
var el = document.createElement("div");
|
||||
el.classList.add("ol-flak-icon");
|
||||
var img = document.createElement("img");
|
||||
img.src = "images/markers/flak.svg";
|
||||
img.onload = () => SVGInjector(img);
|
||||
el.appendChild(img);
|
||||
this.getElement()?.appendChild(el);
|
||||
}
|
||||
}
|
||||
48
frontend/react/src/map/markers/navpointmarker.ts
Normal file
48
frontend/react/src/map/markers/navpointmarker.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { DivIcon, LatLngExpression, MarkerOptions } from "leaflet";
|
||||
import { CustomMarker } from "./custommarker";
|
||||
|
||||
export class NavpointMarker extends CustomMarker {
|
||||
#callsignStr: string;
|
||||
#comment: string;
|
||||
|
||||
constructor(latlng: LatLngExpression, callsignStr: string, comment?: string) {
|
||||
super(latlng, { interactive: false, draggable: false });
|
||||
this.#callsignStr = callsignStr;
|
||||
comment ? this.#comment = comment : null;
|
||||
}
|
||||
|
||||
createIcon() {
|
||||
/* Set the icon */
|
||||
let icon = new DivIcon({
|
||||
className: "leaflet-navpoint-icon",
|
||||
iconAnchor: [0, 0],
|
||||
iconSize: [50, 50],
|
||||
});
|
||||
this.setIcon(icon);
|
||||
|
||||
let el = document.createElement("div");
|
||||
el.classList.add("navpoint");
|
||||
|
||||
// Main icon
|
||||
let pointIcon = document.createElement("div");
|
||||
pointIcon.classList.add("navpoint-icon");
|
||||
el.append(pointIcon);
|
||||
|
||||
// Label
|
||||
let mainLabel: HTMLDivElement = document.createElement("div");;
|
||||
mainLabel.classList.add("navpoint-main-label");
|
||||
mainLabel.innerText = this.#callsignStr;
|
||||
el.append(mainLabel);
|
||||
|
||||
// Further description
|
||||
if (this.#comment) {
|
||||
let commentBox: HTMLDivElement = document.createElement("div");;
|
||||
commentBox.classList.add("navpoint-comment-box");
|
||||
commentBox.innerText = this.#comment;
|
||||
mainLabel.append(commentBox);
|
||||
}
|
||||
|
||||
this.getElement()?.appendChild(el);
|
||||
this.getElement()?.classList.add("ol-navpoint-marker");
|
||||
}
|
||||
}
|
||||
27
frontend/react/src/map/markers/stylesheets/navpoint.css
Normal file
27
frontend/react/src/map/markers/stylesheets/navpoint.css
Normal file
@@ -0,0 +1,27 @@
|
||||
.ol-navpoint-marker>.navpoint {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.ol-navpoint-marker>.navpoint>.navpoint-icon {
|
||||
height: 8px;
|
||||
width: 8px;
|
||||
background: white;
|
||||
flex: none;
|
||||
transform: rotate3d(0, 0, 1, 45deg);
|
||||
}
|
||||
|
||||
.ol-navpoint-marker>.navpoint>.navpoint-main-label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 10px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.ol-navpoint-marker .navpoint-comment-box {
|
||||
font-size: 8px;
|
||||
font-style: italic;
|
||||
color: white;
|
||||
max-width: 50px;
|
||||
}
|
||||
@@ -6,13 +6,20 @@ import { BLUE_COMMANDER, GAME_MASTER, NONE, RED_COMMANDER } from "../constants/c
|
||||
import { AirbasesData, BullseyesData, CommandModeOptions, DateAndTime, MissionData, SpotsData } from "../interfaces";
|
||||
import { Coalition } from "../types/types";
|
||||
import { Carrier } from "./carrier";
|
||||
import { AirbaseSelectedEvent, AppStateChangedEvent, BullseyesDataChangedEvent, CommandModeOptionsChangedEvent, EnabledCommandModesChangedEvent, MissionDataChangedEvent } from "../events";
|
||||
import {
|
||||
AirbaseSelectedEvent,
|
||||
AppStateChangedEvent,
|
||||
BullseyesDataChangedEvent,
|
||||
CommandModeOptionsChangedEvent,
|
||||
EnabledCommandModesChangedEvent,
|
||||
MissionDataChangedEvent,
|
||||
} from "../events";
|
||||
import { Spot } from "./spot";
|
||||
|
||||
/** The MissionManager */
|
||||
export class MissionManager {
|
||||
#bullseyes: { [name: string]: Bullseye } = {};
|
||||
#spots: {[key: string]: Spot} = {};
|
||||
#spots: { [key: string]: Spot } = {};
|
||||
#airbases: { [name: string]: Airbase | Carrier } = {};
|
||||
#theatre: string = "";
|
||||
#dateAndTime: DateAndTime = {
|
||||
@@ -39,7 +46,7 @@ export class MissionManager {
|
||||
constructor() {
|
||||
AppStateChangedEvent.on((state, subState) => {
|
||||
if (this.getSelectedAirbase() !== null) AirbaseSelectedEvent.dispatch(null);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
/** Update location of bullseyes
|
||||
@@ -63,7 +70,7 @@ export class MissionManager {
|
||||
this.#bullseyes[idx].setCoalition(bullseye.coalition);
|
||||
}
|
||||
|
||||
BullseyesDataChangedEvent.dispatch(this.#bullseyes)
|
||||
BullseyesDataChangedEvent.dispatch(this.#bullseyes);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,18 +79,18 @@ export class MissionManager {
|
||||
const spotID = Number(idx);
|
||||
const spot = data.spots[idx];
|
||||
if (this.#spots[spotID] === undefined) {
|
||||
this.#spots[spotID] = new Spot(spotID, spot.type, new LatLng(spot.targetPosition.lat, spot.targetPosition.lng), spot.sourceUnitID, spot.code);
|
||||
this.#spots[spotID] = new Spot(
|
||||
spotID,
|
||||
spot.type,
|
||||
new LatLng(spot.targetPosition.lat, spot.targetPosition.lng),
|
||||
spot.sourceUnitID,
|
||||
spot.active,
|
||||
spot.code
|
||||
);
|
||||
} else {
|
||||
if (spot.type === "laser")
|
||||
this.#spots[spotID].setCode(spot.code ?? 0)
|
||||
this.#spots[spotID].setTargetPosition( new LatLng(spot.targetPosition.lat, spot.targetPosition.lng));
|
||||
}
|
||||
}
|
||||
|
||||
/* Iterate the existing spots and remove all spots that where deleted */
|
||||
for (let idx in this.#spots) {
|
||||
if (data.spots[idx] === undefined) {
|
||||
delete this.#spots[idx];
|
||||
if (spot.type === "laser") this.#spots[spotID].setCode(spot.code ?? 0);
|
||||
this.#spots[spotID].setActive(spot.active);
|
||||
this.#spots[spotID].setTargetPosition(new LatLng(spot.targetPosition.lat, spot.targetPosition.lng));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -99,7 +106,7 @@ export class MissionManager {
|
||||
updateAirbases(data: AirbasesData) {
|
||||
for (let idx in data.airbases) {
|
||||
var airbase = data.airbases[idx];
|
||||
var airbaseCallsign = airbase.callsign !== ""? airbase.callsign: `carrier-${airbase.unitId}`
|
||||
var airbaseCallsign = airbase.callsign !== "" ? airbase.callsign : `carrier-${airbase.unitId}`;
|
||||
if (this.#airbases[airbaseCallsign] === undefined) {
|
||||
if (airbase.callsign != "") {
|
||||
this.#airbases[airbaseCallsign] = new Airbase({
|
||||
@@ -161,7 +168,7 @@ export class MissionManager {
|
||||
return this.#airbases;
|
||||
}
|
||||
|
||||
getSpots() {
|
||||
getSpots() {
|
||||
return this.#spots;
|
||||
}
|
||||
|
||||
@@ -279,7 +286,7 @@ export class MissionManager {
|
||||
commandModeOptions.spawnPoints.red !== this.getCommandModeOptions().spawnPoints.red ||
|
||||
commandModeOptions.spawnPoints.blue !== this.getCommandModeOptions().spawnPoints.blue ||
|
||||
commandModeOptions.restrictSpawns !== this.getCommandModeOptions().restrictSpawns ||
|
||||
commandModeOptions.restrictToCoalition !== this.getCommandModeOptions().restrictToCoalition ||
|
||||
commandModeOptions.restrictToCoalition !== this.getCommandModeOptions().restrictToCoalition ||
|
||||
commandModeOptions.setupTime !== this.getCommandModeOptions().setupTime;
|
||||
|
||||
this.#commandModeOptions = commandModeOptions;
|
||||
|
||||
@@ -2,47 +2,57 @@ import { LatLng } from "leaflet";
|
||||
import { getApp } from "../olympusapp";
|
||||
|
||||
export class Spot {
|
||||
private ID: number;
|
||||
private type: string;
|
||||
private targetPosition: LatLng;
|
||||
private sourceUnitID: number;
|
||||
private code?: number;
|
||||
#ID: number;
|
||||
#type: string;
|
||||
#targetPosition: LatLng;
|
||||
#sourceUnitID: number;
|
||||
#active: boolean;
|
||||
#code?: number;
|
||||
|
||||
constructor(ID: number, type: string, targetPosition: LatLng, sourceUnitID: number, code?: number) {
|
||||
this.ID = ID;
|
||||
this.type = type;
|
||||
this.targetPosition = targetPosition;
|
||||
this.sourceUnitID = sourceUnitID;
|
||||
this.code = code;
|
||||
constructor(ID: number, type: string, targetPosition: LatLng, sourceUnitID: number, active: boolean, code?: number) {
|
||||
this.#ID = ID;
|
||||
this.#type = type;
|
||||
this.#targetPosition = targetPosition;
|
||||
this.#sourceUnitID = sourceUnitID;
|
||||
this.#code = code;
|
||||
this.#active = active;
|
||||
}
|
||||
|
||||
// Getter methods
|
||||
getID() {
|
||||
return this.ID;
|
||||
return this.#ID;
|
||||
}
|
||||
|
||||
getType() {
|
||||
return this.type;
|
||||
return this.#type;
|
||||
}
|
||||
|
||||
getTargetPosition() {
|
||||
return this.targetPosition;
|
||||
return this.#targetPosition;
|
||||
}
|
||||
|
||||
getSourceUnitID() {
|
||||
return this.sourceUnitID;
|
||||
return this.#sourceUnitID;
|
||||
}
|
||||
|
||||
getCode() {
|
||||
return this.code;
|
||||
return this.#code;
|
||||
}
|
||||
|
||||
getActive() {
|
||||
return this.#active;
|
||||
}
|
||||
|
||||
// Setter methods
|
||||
setTargetPosition(position: LatLng) {
|
||||
this.targetPosition = position;
|
||||
this.#targetPosition = position;
|
||||
}
|
||||
|
||||
setCode(code: number) {
|
||||
this.code = code;
|
||||
this.#code = code;
|
||||
}
|
||||
|
||||
setActive(active: boolean) {
|
||||
this.#active = active;
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ import { ServerManager } from "./server/servermanager";
|
||||
import { AudioManager } from "./audio/audiomanager";
|
||||
|
||||
import { GAME_MASTER, LoginSubState, NO_SUBSTATE, OlympusState, OlympusSubState, WarningSubstate } from "./constants/constants";
|
||||
import { AppStateChangedEvent, ConfigLoadedEvent, InfoPopupEvent, MapOptionsChangedEvent, SelectedUnitsChangedEvent, ShortcutsChangedEvent } from "./events";
|
||||
import { AdminPasswordChangedEvent, AppStateChangedEvent, ConfigLoadedEvent, InfoPopupEvent, MapOptionsChangedEvent, SelectedUnitsChangedEvent, ShortcutsChangedEvent } from "./events";
|
||||
import { OlympusConfig } from "./interfaces";
|
||||
import { SessionDataManager } from "./sessiondata";
|
||||
import { ControllerManager } from "./controllers/controllermanager";
|
||||
@@ -57,6 +57,8 @@ export class OlympusApp {
|
||||
#drawingsManager: DrawingsManager;
|
||||
//#pluginsManager: // TODO
|
||||
|
||||
#adminPassword: string = "";
|
||||
|
||||
constructor() {
|
||||
SelectedUnitsChangedEvent.on((selectedUnits) => {
|
||||
if (selectedUnits.length > 0) this.setState(OlympusState.UNIT_CONTROL);
|
||||
@@ -172,7 +174,7 @@ export class OlympusApp {
|
||||
})
|
||||
.then(([result, headers]) => {
|
||||
this.#config = result;
|
||||
if (this.#config.frontend.customAuthHeaders.enabled) {
|
||||
if (this.#config.frontend.customAuthHeaders?.enabled) {
|
||||
if (headers.has(this.#config.frontend.customAuthHeaders.username) && headers.has(this.#config.frontend.customAuthHeaders.group)) {
|
||||
this.getServerManager().setUsername(headers.get(this.#config.frontend.customAuthHeaders.username));
|
||||
this.setState(OlympusState.LOGIN, LoginSubState.COMMAND_MODE);
|
||||
@@ -348,6 +350,11 @@ export class OlympusApp {
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
setAdminPassword(newAdminPassword: string) {
|
||||
this.#adminPassword = newAdminPassword;
|
||||
AdminPasswordChangedEvent.dispatch(newAdminPassword);
|
||||
}
|
||||
|
||||
startServerMode() {
|
||||
//ConfigLoadedEvent.on((config) => {
|
||||
// this.getAudioManager().start();
|
||||
|
||||
@@ -754,4 +754,12 @@ export async function getWikipediaSummary(unitName: string): Promise<string | nu
|
||||
console.error('Error fetching data from Wikipedia:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function secondsToTimeString(seconds: number) {
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
const secs = Math.floor(seconds % 60);
|
||||
|
||||
return `${zeroPad(hours, 2)}:${zeroPad(minutes, 2)}:${zeroPad(secs, 2)}`;
|
||||
}
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
TACAN,
|
||||
} from "../interfaces";
|
||||
import { MapOptionsChangedEvent, ServerStatusUpdatedEvent, WrongCredentialsEvent } from "../events";
|
||||
import { Coalition } from "../types/types";
|
||||
|
||||
export class ServerManager {
|
||||
#connected: boolean = false;
|
||||
@@ -342,9 +343,10 @@ export class ServerManager {
|
||||
this.PUT(data, callback);
|
||||
}
|
||||
|
||||
cloneUnits(units: { ID: number; location: LatLng }[], deleteOriginal: boolean, spawnPoints: number, callback: CallableFunction = () => {}) {
|
||||
cloneUnits(units: { ID: number; location: LatLng }[], deleteOriginal: boolean, spawnPoints: number, coalition: Coalition, callback: CallableFunction = () => {}) {
|
||||
var command = {
|
||||
units: units,
|
||||
coalition: coalition,
|
||||
deleteOriginal: deleteOriginal,
|
||||
spawnPoints: spawnPoints,
|
||||
};
|
||||
@@ -561,6 +563,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);
|
||||
|
||||
@@ -21,7 +21,7 @@ export class Shortcut {
|
||||
ModalEvent.on((modal) => (this.#modal = modal));
|
||||
|
||||
/* On keyup, it is enough to check the code only, not the entire combination */
|
||||
document.addEventListener("keyup", (ev: any) => {
|
||||
window.addEventListener("keyup", (ev: any) => {
|
||||
if (this.#modal) return;
|
||||
if (this.#keydown && this.getOptions().code === ev.code) {
|
||||
console.log(`Keyup for shortcut ${this.#id}`);
|
||||
@@ -32,7 +32,7 @@ export class Shortcut {
|
||||
});
|
||||
|
||||
/* Forced keyup, in case the window loses focus */
|
||||
document.addEventListener("blur", (ev: any) => {
|
||||
window.addEventListener("blur", (ev: any) => {
|
||||
if (this.#keydown) {
|
||||
console.log(`Keyup (forced by blur) for shortcut ${this.#id}`);
|
||||
ev.preventDefault();
|
||||
@@ -42,7 +42,7 @@ export class Shortcut {
|
||||
});
|
||||
|
||||
/* On keydown, check exactly if the requested key combination is being pressed */
|
||||
document.addEventListener("keydown", (ev: any) => {
|
||||
window.addEventListener("keydown", (ev: any) => {
|
||||
if (this.#modal) return;
|
||||
if (
|
||||
!(this.#keydown || keyEventWasInInput(ev) || this.getOptions().code !== ev.code) &&
|
||||
|
||||
@@ -12,7 +12,14 @@ export function OlLocation(props: { location: LatLng; className?: string; refere
|
||||
${props.className ?? ""}
|
||||
my-auto cursor-pointer bg-olympus-400 p-2 text-white
|
||||
`}
|
||||
onClick={props.onClick ? props.onClick : () => setReferenceSystem("LatLngDec")}
|
||||
onClick={
|
||||
props.onClick
|
||||
? props.onClick
|
||||
: (ev) => {
|
||||
setReferenceSystem("LatLngDec");
|
||||
ev.stopPropagation();
|
||||
}
|
||||
}
|
||||
>
|
||||
<span
|
||||
className={`
|
||||
@@ -32,7 +39,14 @@ export function OlLocation(props: { location: LatLng; className?: string; refere
|
||||
my-auto flex cursor-pointer justify-between gap-2 bg-olympus-400 p-2
|
||||
text-white
|
||||
`}
|
||||
onClick={props.onClick ? props.onClick : () => setReferenceSystem("LatLngDMS")}
|
||||
onClick={
|
||||
props.onClick
|
||||
? props.onClick
|
||||
: (ev) => {
|
||||
setReferenceSystem("LatLngDMS");
|
||||
ev.stopPropagation();
|
||||
}
|
||||
}
|
||||
>
|
||||
<div className="flex gap-2">
|
||||
<span
|
||||
@@ -64,7 +78,14 @@ export function OlLocation(props: { location: LatLng; className?: string; refere
|
||||
my-auto flex cursor-pointer justify-between gap-2 bg-olympus-400 p-2
|
||||
text-white
|
||||
`}
|
||||
onClick={props.onClick ? props.onClick : () => setReferenceSystem("MGRS")}
|
||||
onClick={
|
||||
props.onClick
|
||||
? props.onClick
|
||||
: (ev) => {
|
||||
setReferenceSystem("MGRS");
|
||||
ev.stopPropagation();
|
||||
}
|
||||
}
|
||||
>
|
||||
<div className="flex gap-2">
|
||||
<span
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -6,6 +6,12 @@ export function OlTooltip(props: {
|
||||
position?: string;
|
||||
relativeToParent?: boolean;
|
||||
}) {
|
||||
const [isTouchscreen, setIsTouchscreen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setIsTouchscreen("ontouchstart" in window);
|
||||
}, []);
|
||||
|
||||
var contentRef = useRef(null);
|
||||
|
||||
function setPosition(content: HTMLDivElement, button: HTMLButtonElement) {
|
||||
@@ -106,7 +112,7 @@ export function OlTooltip(props: {
|
||||
});
|
||||
|
||||
return (
|
||||
props.content !== "" && (
|
||||
props.content !== "" && !isTouchscreen && (
|
||||
<div
|
||||
ref={contentRef}
|
||||
className={`
|
||||
|
||||
@@ -122,8 +122,8 @@ export function SpawnContextMenu(props: {}) {
|
||||
setXPosition(xPosition - 60);
|
||||
setYPosition(yPosition - 40);
|
||||
}
|
||||
}, [blueprint, translated])
|
||||
useEffect(translateMenu, [blueprint, translated])
|
||||
}, [blueprint, translated]);
|
||||
useEffect(translateMenu, [blueprint, translated]);
|
||||
|
||||
/* Filter the blueprints according to the label */
|
||||
const filteredBlueprints: UnitBlueprint[] = [];
|
||||
@@ -306,6 +306,9 @@ export function SpawnContextMenu(props: {}) {
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{blueprints?.length === 0 && <span className={`
|
||||
text-gray-200
|
||||
`}>No aircraft available</span>}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
@@ -353,6 +356,9 @@ export function SpawnContextMenu(props: {}) {
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{blueprints?.length === 0 && <span className={`
|
||||
text-gray-200
|
||||
`}>No helicopter available</span>}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
@@ -403,6 +409,9 @@ export function SpawnContextMenu(props: {}) {
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{blueprints?.length === 0 && <span className={`
|
||||
text-gray-200
|
||||
`}>No air defence unit available</span>}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
@@ -453,6 +462,9 @@ export function SpawnContextMenu(props: {}) {
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{blueprints?.length === 0 && <span className={`
|
||||
text-gray-200
|
||||
`}>No ground unit available</span>}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
@@ -500,10 +512,13 @@ export function SpawnContextMenu(props: {}) {
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{blueprints?.length === 0 && <span className={`
|
||||
text-gray-200
|
||||
`}>No navy unit available</span>}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{openAccordion === CategoryGroup.EFFECT && (
|
||||
{openAccordion === CategoryGroup.EFFECT && commandModeOptions.commandMode === GAME_MASTER && (
|
||||
<>
|
||||
<div
|
||||
className={`
|
||||
@@ -530,6 +545,11 @@ export function SpawnContextMenu(props: {}) {
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{openAccordion === CategoryGroup.EFFECT && commandModeOptions.commandMode !== GAME_MASTER && (
|
||||
<div className="text-white">
|
||||
Not available in this mode
|
||||
</div>
|
||||
)}
|
||||
{openAccordion === CategoryGroup.SEARCH && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<OlSearchBar onChange={(value) => setFilterString(value)} text={filterString} />
|
||||
|
||||
337
frontend/react/src/ui/modals/adminmodal.tsx
Normal file
337
frontend/react/src/ui/modals/adminmodal.tsx
Normal file
@@ -0,0 +1,337 @@
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { Modal } from "./components/modal";
|
||||
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faArrowRight } from "@fortawesome/free-solid-svg-icons";
|
||||
import { getApp } from "../../olympusapp";
|
||||
import { OlympusState, WarningSubstate } from "../../constants/constants";
|
||||
import { FaPlus, FaTrash } from "react-icons/fa";
|
||||
import { sha256 } from "js-sha256";
|
||||
import { AdminPasswordChangedEvent } from "../../events";
|
||||
import { OlDropdown } from "../components/oldropdown";
|
||||
import { OlCheckbox } from "../components/olcheckbox";
|
||||
|
||||
export function AdminModal(props: { open: boolean }) {
|
||||
const [configs, setConfigs] = useState({} as { groups: { [key: string]: string[] }; users: { [key: string]: { password: string; roles: string[] } } });
|
||||
const [newUserName, setNewUserName] = useState("");
|
||||
const [newGroupName, setNewGroupName] = useState("");
|
||||
const [adminPassword, setAdminPassword] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
AdminPasswordChangedEvent.on((password) => {
|
||||
setAdminPassword(password);
|
||||
|
||||
var hash = sha256.create();
|
||||
|
||||
const requestOptions: RequestInit = {
|
||||
method: "GET", // Specify the request method
|
||||
headers: {
|
||||
Authorization: "Basic " + btoa(`Admin:${hash.update(password).hex()}`),
|
||||
}, // Specify the content type
|
||||
};
|
||||
|
||||
fetch(`./admin/config`, requestOptions)
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
console.log(`Admin password correct`);
|
||||
return response.json();
|
||||
} else {
|
||||
throw new Error("Admin password incorrect");
|
||||
}
|
||||
})
|
||||
.then((data) => {
|
||||
setConfigs(data);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(`Error reading configuration: ${error}`);
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
|
||||
const uploadNewConfig = useCallback(() => {
|
||||
var hash = sha256.create();
|
||||
|
||||
const requestOptions: RequestInit = {
|
||||
method: "PUT", // Specify the request method
|
||||
headers: {
|
||||
Authorization: "Basic " + btoa(`Admin:${hash.update(adminPassword).hex()}`),
|
||||
"Content-Type": "application/json",
|
||||
}, // Specify the content type
|
||||
body: JSON.stringify(configs),
|
||||
};
|
||||
|
||||
fetch(`./admin/config`, requestOptions)
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
console.log(`Configuration uploaded`);
|
||||
} else {
|
||||
throw new Error("Error uploading configuration");
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
getApp().setState(OlympusState.WARNING, WarningSubstate.ERROR_UPLOADING_CONFIG);
|
||||
console.error(`Error uploading configuration: ${error}`);
|
||||
});
|
||||
}, [adminPassword, configs]);
|
||||
|
||||
|
||||
return (
|
||||
<Modal open={props.open} size={"full"}>
|
||||
<div className="flex flex-col lg:flex-row w-full gap-4">
|
||||
<div className="lg:w-[40%]">
|
||||
<div className="text-white">Groups:</div>
|
||||
<div className="flex max-h-[calc(100vh-280px)] flex-col gap-1 overflow-auto p-2">
|
||||
{configs.groups &&
|
||||
Object.keys(configs.groups).map((group: any) => {
|
||||
return (
|
||||
<div
|
||||
key={group}
|
||||
className={`
|
||||
flex justify-between gap-4 text-sm text-gray-200
|
||||
`}
|
||||
>
|
||||
<div className="my-auto">{group}</div>
|
||||
<OlDropdown
|
||||
label="Enabled roles"
|
||||
className={`my-auto ml-auto min-w-48`}
|
||||
disableAutoClose={true}
|
||||
>
|
||||
{["Game master", "Blue commander", "Red commander"].map((role: any) => {
|
||||
return (
|
||||
<div key={role} className="flex gap-2 p-2">
|
||||
<OlCheckbox
|
||||
checked={configs["groups"][group].includes(role)}
|
||||
onChange={(ev) => {
|
||||
if (ev.target.checked) {
|
||||
configs["groups"][group].push(role);
|
||||
} else {
|
||||
configs["groups"][group] = configs["groups"][group].filter((r: any) => r !== role);
|
||||
}
|
||||
setConfigs({ ...configs });
|
||||
}}
|
||||
></OlCheckbox>
|
||||
{role}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</OlDropdown>
|
||||
<div
|
||||
className={`
|
||||
my-auto cursor-pointer rounded-md bg-red-600 p-2
|
||||
hover:bg-red-400
|
||||
`}
|
||||
onClick={() => {
|
||||
delete configs["users"][group];
|
||||
}}
|
||||
>
|
||||
<FaTrash className={`text-gray-50`}></FaTrash>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{(configs.groups === undefined || Object.keys(configs.groups).length === 0) && (
|
||||
<div
|
||||
className={`text-gray-400`}
|
||||
>
|
||||
No groups defined
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex justify-between gap-4">
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="new-password"
|
||||
onChange={(ev) => {
|
||||
setNewGroupName(ev.currentTarget.value);
|
||||
}}
|
||||
className={`
|
||||
rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm
|
||||
text-gray-900
|
||||
dark:border-gray-600 dark:bg-gray-700 dark:text-white
|
||||
dark:placeholder-gray-400 dark:focus:border-blue-500
|
||||
dark:focus:ring-blue-500
|
||||
focus:border-blue-500 focus:ring-blue-500
|
||||
`}
|
||||
placeholder="New group name"
|
||||
value={newGroupName}
|
||||
required
|
||||
/>
|
||||
<div
|
||||
className={`
|
||||
my-auto cursor-pointer rounded-md border-[1px] border-white
|
||||
bg-transparent p-2
|
||||
hover:bg-gray-800
|
||||
`}
|
||||
onClick={() => {
|
||||
if (newGroupName === "") return;
|
||||
configs["groups"][newGroupName] = [];
|
||||
setConfigs({ ...configs });
|
||||
setNewGroupName("");
|
||||
}}
|
||||
>
|
||||
<FaPlus className={`text-gray-50`} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex lg:w-[58%] flex-col gap-2">
|
||||
<div className="text-white">Users:</div>
|
||||
<div className={`flex max-h-[calc(100vh-280px)] flex-col gap-1 overflow-auto p-2`}>
|
||||
{configs.users &&
|
||||
Object.keys(configs.users).map((user: any) => {
|
||||
return (
|
||||
<div
|
||||
key={user.id}
|
||||
className={`
|
||||
flex justify-between gap-2 text-sm text-gray-200
|
||||
`}
|
||||
>
|
||||
<div className="my-auto">{user}</div>
|
||||
|
||||
<OlDropdown
|
||||
label="Enabled roles"
|
||||
className={`my-auto ml-auto min-w-48`}
|
||||
disableAutoClose={true}
|
||||
>
|
||||
{["Game master", "Blue commander", "Red commander"].map((role: any) => {
|
||||
return (
|
||||
<div key={role} className="flex gap-2 p-2">
|
||||
<OlCheckbox
|
||||
checked={configs["users"][user].roles.includes(role)}
|
||||
onChange={(ev) => {
|
||||
if (ev.target.checked) {
|
||||
configs["users"][user].roles.push(role);
|
||||
} else {
|
||||
configs["users"][user].roles = configs["users"][user].roles.filter((r: any) => r !== role);
|
||||
}
|
||||
setConfigs({ ...configs });
|
||||
}}
|
||||
></OlCheckbox>
|
||||
{role}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</OlDropdown>
|
||||
|
||||
<input
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
onChange={(ev) => {
|
||||
var hash = sha256.create();
|
||||
configs["users"][user].password = hash.update(ev.currentTarget.value).hex();
|
||||
setConfigs({ ...configs });
|
||||
}}
|
||||
className={`
|
||||
max-w-44 rounded-lg border border-gray-300 bg-gray-50
|
||||
p-2.5 text-sm text-gray-900
|
||||
dark:border-gray-600 dark:bg-gray-700 dark:text-white
|
||||
dark:placeholder-gray-400 dark:focus:border-blue-500
|
||||
dark:focus:ring-blue-500
|
||||
focus:border-blue-500 focus:ring-blue-500
|
||||
`}
|
||||
placeholder="Change password"
|
||||
required
|
||||
/>
|
||||
<div
|
||||
className={`
|
||||
my-auto cursor-pointer rounded-md bg-red-600 p-2
|
||||
hover:bg-red-400
|
||||
`}
|
||||
onClick={() => {
|
||||
delete configs["users"][user];
|
||||
setConfigs({ ...configs });
|
||||
}}
|
||||
>
|
||||
<FaTrash className={`text-gray-50`}></FaTrash>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{(configs.users === undefined || Object.keys(configs.users).length === 0) && (
|
||||
<div
|
||||
className={`text-gray-400`}
|
||||
>
|
||||
No users defined
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex justify-between gap-4">
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="new-password"
|
||||
onChange={(ev) => {
|
||||
setNewUserName(ev.currentTarget.value);
|
||||
}}
|
||||
className={`
|
||||
rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm
|
||||
text-gray-900
|
||||
dark:border-gray-600 dark:bg-gray-700 dark:text-white
|
||||
dark:placeholder-gray-400 dark:focus:border-blue-500
|
||||
dark:focus:ring-blue-500
|
||||
focus:border-blue-500 focus:ring-blue-500
|
||||
`}
|
||||
placeholder="New user name"
|
||||
value={newUserName}
|
||||
required
|
||||
/>
|
||||
<div
|
||||
className={`
|
||||
my-auto cursor-pointer rounded-md border-[1px] border-white
|
||||
bg-transparent p-2
|
||||
hover:bg-gray-800
|
||||
`}
|
||||
onClick={() => {
|
||||
if (newUserName === "") return;
|
||||
configs["users"][newUserName] = { password: "", roles: [] };
|
||||
setConfigs({ ...configs });
|
||||
setNewUserName("");
|
||||
}}
|
||||
>
|
||||
<FaPlus className={`text-gray-50`} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-auto flex justify-between">
|
||||
<div className="my-auto flex gap-4 text-sm text-gray-400">
|
||||
<div className="my-auto">Reset all user preferences, use with caution</div>
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => getApp().resetAllProfiles()}
|
||||
className={`
|
||||
flex content-center items-center gap-2 text-nowrap rounded-sm
|
||||
border-[1px] bg-blue-700 px-5 py-2.5 text-sm font-medium
|
||||
text-white
|
||||
dark:border-red-600 dark:bg-red-800 dark:text-gray-400
|
||||
dark:hover:bg-red-700 dark:focus:ring-blue-800
|
||||
focus:outline-none focus:ring-4 focus:ring-blue-300
|
||||
hover:bg-red-800
|
||||
`}
|
||||
>
|
||||
Reset profiles
|
||||
<FaTrash />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
uploadNewConfig();
|
||||
getApp().setState(OlympusState.IDLE)}
|
||||
}
|
||||
className={`
|
||||
my-auto flex content-center items-center gap-2 rounded-sm
|
||||
bg-blue-700 px-5 py-2.5 text-sm 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
|
||||
`}
|
||||
>
|
||||
Apply changes
|
||||
<FontAwesomeIcon className={`my-auto`} icon={faArrowRight} />
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +1,16 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { ModalEvent } from "../../../events";
|
||||
import { FaXmark } from "react-icons/fa6";
|
||||
import { getApp, OlympusApp } from "../../../olympusapp";
|
||||
import { getApp } from "../../../olympusapp";
|
||||
import { OlympusState } from "../../../constants/constants";
|
||||
|
||||
export function Modal(props: { open: boolean; children?: JSX.Element | JSX.Element[]; className?: string }) {
|
||||
export function Modal(props: {
|
||||
open: boolean;
|
||||
children?: JSX.Element | JSX.Element[];
|
||||
className?: string;
|
||||
size?: "sm" | "md" | "lg" | "full";
|
||||
disableClose?: boolean;
|
||||
}) {
|
||||
const [splash, setSplash] = useState(Math.ceil(Math.random() * 7));
|
||||
|
||||
useEffect(() => {
|
||||
@@ -15,21 +21,46 @@ export function Modal(props: { open: boolean; children?: JSX.Element | JSX.Eleme
|
||||
<>
|
||||
{props.open && (
|
||||
<>
|
||||
<div className={`fixed left-0 top-0 z-30 h-full w-full bg-[#111111]/95`}></div>
|
||||
<div
|
||||
className={`fixed left-0 top-0 z-30 h-full w-full bg-[#111111]/95`}
|
||||
></div>
|
||||
<div
|
||||
className={`
|
||||
fixed left-[50%] top-[50%] z-40 inline-flex h-[75%] max-h-[600px]
|
||||
w-[80%] max-w-[1100px] translate-x-[-50%] translate-y-[-50%]
|
||||
overflow-y-auto scroll-smooth rounded-xl border-[1px] border-solid
|
||||
border-gray-700 bg-white drop-shadow-md
|
||||
dark:bg-olympus-800
|
||||
max-md:h-full max-md:max-h-full max-md:w-full max-md:rounded-none
|
||||
max-md:border-none
|
||||
fixed left-[50%] top-[50%] z-40 inline-flex translate-x-[-50%]
|
||||
translate-y-[-50%] overflow-y-auto scroll-smooth rounded-xl
|
||||
border-[1px] border-solid border-gray-700 bg-olympus-800
|
||||
drop-shadow-md
|
||||
max-md:rounded-none max-md:border-none
|
||||
${
|
||||
props.size === "lg"
|
||||
? `
|
||||
h-[600px] w-[1100px]
|
||||
max-md:h-full max-md:w-full
|
||||
`
|
||||
: ""
|
||||
}
|
||||
${
|
||||
props.size === "md"
|
||||
? `
|
||||
h-[600px] w-[950px]
|
||||
max-md:h-full max-md:w-full
|
||||
`
|
||||
: ""
|
||||
}
|
||||
${
|
||||
props.size === "sm"
|
||||
? `
|
||||
h-[500px] w-[800px]
|
||||
max-md:h-full max-md:w-full
|
||||
`
|
||||
: ""
|
||||
}
|
||||
${props.size === "full" ? "h-full w-full" : ""}
|
||||
`}
|
||||
>
|
||||
<img
|
||||
src={`images/splash/${splash}.jpg`}
|
||||
className={`contents-center w-full object-cover opacity-[7%]`}
|
||||
className={`contents-center w-full object-cover opacity-[4%]`}
|
||||
></img>
|
||||
<div className="fixed left-0 top-0 h-full w-full">
|
||||
<div
|
||||
@@ -51,17 +82,19 @@ export function Modal(props: { open: boolean; children?: JSX.Element | JSX.Eleme
|
||||
`}
|
||||
>
|
||||
{props.children}
|
||||
<div
|
||||
className={`
|
||||
absolute right-5 top-5 cursor-pointer text-xl text-white
|
||||
`}
|
||||
>
|
||||
<FaXmark
|
||||
onClick={() => {
|
||||
getApp().setState(OlympusState.IDLE);
|
||||
}}
|
||||
/>{" "}
|
||||
</div>
|
||||
{!props.disableClose && (
|
||||
<div
|
||||
className={`
|
||||
absolute right-5 top-5 cursor-pointer text-xl text-white
|
||||
`}
|
||||
>
|
||||
<FaXmark
|
||||
onClick={() => {
|
||||
getApp().setState(OlympusState.IDLE);
|
||||
}}
|
||||
/>{" "}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -96,6 +96,7 @@ export function ImportExportModal(props: { open: boolean }) {
|
||||
|
||||
return (
|
||||
<Modal open={props.open} className={``}>
|
||||
<div className="flex h-full w-full flex-col justify-between">
|
||||
<div className={`flex flex-col justify-between gap-2`}>
|
||||
<span
|
||||
className={`
|
||||
@@ -353,6 +354,7 @@ export function ImportExportModal(props: { open: boolean }) {
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -61,7 +61,8 @@ export function KeybindModal(props: { open: boolean }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal open={props.open}>
|
||||
<Modal open={props.open} size={"sm"}>
|
||||
<div className="flex flex-col gap-4 h-full w-full">
|
||||
<div className={`flex flex-col gap-2`}>
|
||||
<span
|
||||
className={`
|
||||
@@ -104,7 +105,7 @@ export function KeybindModal(props: { open: boolean }) {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<div className="flex justify-end mt-auto ">
|
||||
{shortcut && (
|
||||
<button
|
||||
type="button"
|
||||
@@ -147,6 +148,7 @@ export function KeybindModal(props: { open: boolean }) {
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,12 +3,12 @@ import { Modal } from "./components/modal";
|
||||
import { Card } from "./components/card";
|
||||
import { ErrorCallout } from "../components/olcallout";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faArrowRight, faCheckCircle, faExternalLink } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faArrowLeft, faArrowRight, faCheckCircle, faExternalLink } from "@fortawesome/free-solid-svg-icons";
|
||||
import { getApp, VERSION } from "../../olympusapp";
|
||||
import { sha256 } from "js-sha256";
|
||||
import { LoginSubState, NO_SUBSTATE, OlympusState } from "../../constants/constants";
|
||||
import { OlDropdown, OlDropdownItem } from "../components/oldropdown";
|
||||
import { AppStateChangedEvent, EnabledCommandModesChangedEvent, MissionDataChangedEvent, WrongCredentialsEvent } from "../../events";
|
||||
import { AppStateChangedEvent, EnabledCommandModesChangedEvent, WrongCredentialsEvent } from "../../events";
|
||||
|
||||
export function LoginModal(props: { open: boolean }) {
|
||||
const [subState, setSubState] = useState(NO_SUBSTATE);
|
||||
@@ -18,6 +18,7 @@ export function LoginModal(props: { open: boolean }) {
|
||||
const [loginError, setLoginError] = useState(false);
|
||||
const [commandModes, setCommandModes] = useState(null as null | string[]);
|
||||
const [activeCommandMode, setActiveCommandMode] = useState(null as null | string);
|
||||
const [loginByRole, setLoginByRole] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
AppStateChangedEvent.on((state, subState) => {
|
||||
@@ -29,6 +30,11 @@ export function LoginModal(props: { open: boolean }) {
|
||||
});
|
||||
}, []);
|
||||
|
||||
const updateUsername = useCallback(() => {
|
||||
loginByRole ? setUsername("Game master") : setUsername("");
|
||||
}, [loginByRole]);
|
||||
useEffect(updateUsername, [loginByRole]);
|
||||
|
||||
const usernameCallback = useCallback(() => getApp()?.getServerManager().setUsername(username), [username]);
|
||||
useEffect(usernameCallback, [username]);
|
||||
|
||||
@@ -80,7 +86,7 @@ export function LoginModal(props: { open: boolean }) {
|
||||
useEffect(subStateCallback, [subState]);
|
||||
|
||||
return (
|
||||
<Modal open={props.open}>
|
||||
<Modal open={props.open} size="md" disableClose={true}>
|
||||
<div
|
||||
className={`
|
||||
flex w-full flex-row gap-6
|
||||
@@ -95,25 +101,6 @@ export function LoginModal(props: { open: boolean }) {
|
||||
>
|
||||
{!checkingPassword ? (
|
||||
<>
|
||||
<div className="flex flex-col items-start">
|
||||
<div
|
||||
className={`
|
||||
pt-1 text-xs text-gray-800
|
||||
dark:text-gray-400
|
||||
`}
|
||||
>
|
||||
Connect to
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
flex items-center justify-center gap-2 text-gray-800 text-md
|
||||
font-bold
|
||||
dark:text-gray-200
|
||||
`}
|
||||
>
|
||||
{window.location.toString()}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
flex w-[100%] flex-row content-center items-center gap-2
|
||||
@@ -147,33 +134,96 @@ export function LoginModal(props: { open: boolean }) {
|
||||
<>
|
||||
{subState === LoginSubState.CREDENTIALS && (
|
||||
<>
|
||||
<div className={`flex flex-col items-start gap-2`}>
|
||||
<label
|
||||
<div
|
||||
className={`
|
||||
peer box-content flex h-10 w-80 cursor-pointer
|
||||
justify-between rounded-lg border-4 border-gray-600
|
||||
border-transparent bg-olympus-600
|
||||
hover:bg-olympus-400
|
||||
peer-focus:outline-none peer-focus:ring-2
|
||||
peer-focus:ring-blue-800
|
||||
`}
|
||||
onClick={() => setLoginByRole(!loginByRole)}
|
||||
>
|
||||
<div
|
||||
data-login-by-role={loginByRole}
|
||||
className={`
|
||||
text-gray-800 text-md
|
||||
dark:text-white
|
||||
relative
|
||||
before:absolute before:h-10 before:w-40
|
||||
before:rounded-md before:bg-blue-500
|
||||
before:transition-transform before:content-['']
|
||||
before:data-[login-by-role='false']:translate-x-40
|
||||
`}
|
||||
></div>
|
||||
<div
|
||||
className={`
|
||||
z-40 my-auto w-[50%] text-center
|
||||
${loginByRole ? "text-white" : `
|
||||
text-gray-400 transition-colors
|
||||
`}
|
||||
`}
|
||||
>
|
||||
Username
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="username"
|
||||
onChange={(ev) => setUsername(ev.currentTarget.value)}
|
||||
Login by role
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
block w-full max-w-80 rounded-lg border
|
||||
border-gray-300 bg-gray-50 p-2.5 text-sm
|
||||
text-gray-900
|
||||
dark:border-gray-600 dark:bg-gray-700
|
||||
dark:text-white dark:placeholder-gray-400
|
||||
dark:focus:border-blue-500 dark:focus:ring-blue-500
|
||||
focus:border-blue-500 focus:ring-blue-500
|
||||
z-40 my-auto w-[50%] px-2 text-center
|
||||
${!loginByRole ? "text-white" : `
|
||||
text-gray-400 transition-colors
|
||||
`}
|
||||
`}
|
||||
placeholder="Enter display name"
|
||||
value={username}
|
||||
required
|
||||
/>
|
||||
>
|
||||
Login by name
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`flex flex-col items-start gap-2`}>
|
||||
{loginByRole ? (
|
||||
<>
|
||||
<label
|
||||
className={`
|
||||
text-gray-800 text-md
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
Role
|
||||
</label>
|
||||
<OlDropdown label={username} className={`w-full`}>
|
||||
<OlDropdownItem onClick={() => {setUsername("Game master")}}>Game master</OlDropdownItem>
|
||||
<OlDropdownItem onClick={() => {setUsername("Blue commander")}}>Blue commander</OlDropdownItem>
|
||||
<OlDropdownItem onClick={() => {setUsername("Red commander")}}>Red commander</OlDropdownItem>
|
||||
</OlDropdown>
|
||||
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<label
|
||||
className={`
|
||||
text-gray-800 text-md
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
Username
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="username"
|
||||
onChange={(ev) => setUsername(ev.currentTarget.value)}
|
||||
className={`
|
||||
block w-full rounded-lg border border-gray-300
|
||||
bg-gray-50 p-2.5 text-sm text-gray-900
|
||||
dark:border-gray-600 dark:bg-gray-700
|
||||
dark:text-white dark:placeholder-gray-400
|
||||
dark:focus:border-blue-500
|
||||
dark:focus:ring-blue-500
|
||||
focus:border-blue-500 focus:ring-blue-500
|
||||
`}
|
||||
placeholder="Enter username"
|
||||
value={username}
|
||||
required
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<label
|
||||
className={`
|
||||
text-gray-800 text-md
|
||||
@@ -186,9 +236,8 @@ export function LoginModal(props: { open: boolean }) {
|
||||
type="password"
|
||||
onChange={(ev) => setPassword(ev.currentTarget.value)}
|
||||
className={`
|
||||
block w-full max-w-80 rounded-lg border
|
||||
border-gray-300 bg-gray-50 p-2.5 text-sm
|
||||
text-gray-900
|
||||
block w-full rounded-lg border border-gray-300
|
||||
bg-gray-50 p-2.5 text-sm text-gray-900
|
||||
dark:border-gray-600 dark:bg-gray-700
|
||||
dark:text-white dark:placeholder-gray-400
|
||||
dark:focus:border-blue-500 dark:focus:ring-blue-500
|
||||
@@ -198,6 +247,7 @@ export function LoginModal(props: { open: boolean }) {
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex">
|
||||
<button
|
||||
type="button"
|
||||
@@ -234,9 +284,10 @@ export function LoginModal(props: { open: boolean }) {
|
||||
>
|
||||
Choose your role
|
||||
</label>
|
||||
<OlDropdown label={activeCommandMode ?? ""} className={`
|
||||
w-48
|
||||
`}>
|
||||
<OlDropdown
|
||||
label={activeCommandMode ?? ""}
|
||||
className={`w-48`}
|
||||
>
|
||||
{commandModes?.map((commandMode) => {
|
||||
return <OlDropdownItem onClick={() => setActiveCommandMode(commandMode)}>{commandMode}</OlDropdownItem>;
|
||||
})}
|
||||
@@ -266,21 +317,41 @@ export function LoginModal(props: { open: boolean }) {
|
||||
) : (
|
||||
<>
|
||||
<ErrorCallout
|
||||
title="Server could not be reached or password is incorrect"
|
||||
description="The Olympus Server at this address could not be reached or the password is incorrect. Check your password. If correct, check the address is correct, restart the Olympus server or reinstall Olympus. Ensure the ports set are not already used."
|
||||
title="Server could not be reached or username/password is incorrect"
|
||||
description="The Olympus Server at this address could not be reached or the password is incorrect. Check your username and password. If correct, check the address is correct, restart the Olympus server or reinstall Olympus. Ensure the ports set are not already used."
|
||||
></ErrorCallout>
|
||||
<div className={`text-sm font-medium text-gray-200`}>
|
||||
Still having issues? See our{" "}
|
||||
<a
|
||||
href=""
|
||||
<div className="flex gap-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
getApp().setState(OlympusState.LOGIN, LoginSubState.CREDENTIALS);
|
||||
setLoginError(false);
|
||||
}}
|
||||
className={`
|
||||
text-blue-300 underline
|
||||
hover:no-underline
|
||||
flex content-center items-center gap-2 rounded-sm
|
||||
bg-blue-700 px-5 py-2.5 text-sm 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
|
||||
`}
|
||||
>
|
||||
troubleshooting guide here
|
||||
</a>
|
||||
.
|
||||
<FontAwesomeIcon className={`my-auto`} icon={faArrowLeft} />
|
||||
Back
|
||||
</button>
|
||||
<div className={`my-auto text-sm font-medium text-gray-200`}>
|
||||
Still having issues? See our{" "}
|
||||
<a
|
||||
href=""
|
||||
className={`
|
||||
text-blue-300 underline
|
||||
hover:no-underline
|
||||
`}
|
||||
>
|
||||
troubleshooting guide here
|
||||
</a>
|
||||
.
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
@@ -309,6 +380,7 @@ export function LoginModal(props: { open: boolean }) {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`
|
||||
flex flex-grow flex-row content-end justify-center gap-3
|
||||
@@ -325,9 +397,9 @@ export function LoginModal(props: { open: boolean }) {
|
||||
object-cover
|
||||
`}
|
||||
></img>
|
||||
<div
|
||||
className={`mt-2 flex content-center items-center gap-2 font-bold`}
|
||||
>
|
||||
<div className={`
|
||||
mt-2 flex content-center items-center gap-2 font-bold
|
||||
`}>
|
||||
YouTube Video Guide
|
||||
<FontAwesomeIcon className={`my-auto text-xs text-gray-400`} icon={faExternalLink} />
|
||||
</div>
|
||||
@@ -348,9 +420,9 @@ export function LoginModal(props: { open: boolean }) {
|
||||
object-cover
|
||||
`}
|
||||
></img>
|
||||
<div
|
||||
className={`mt-2 flex content-center items-center gap-2 font-bold`}
|
||||
>
|
||||
<div className={`
|
||||
mt-2 flex content-center items-center gap-2 font-bold
|
||||
`}>
|
||||
Wiki Guide
|
||||
<FontAwesomeIcon className={`my-auto text-xs text-gray-400`} icon={faExternalLink} />
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,7 @@ export function ProtectionPromptModal(props: { open: boolean }) {
|
||||
return (
|
||||
<Modal
|
||||
open={props.open}
|
||||
size={"sm"}
|
||||
>
|
||||
<div className="flex h-full w-full flex-col justify-between gap-12">
|
||||
<div className={`flex flex-col items-start gap-2`}>
|
||||
|
||||
@@ -19,7 +19,7 @@ export function TrainingModal(props: { open: boolean }) {
|
||||
|
||||
<>
|
||||
{step === 0 && (
|
||||
<div className="flex gap-16">
|
||||
<div className="flex flex-col sm:flex-row gap-4 sm:gap-16">
|
||||
<img
|
||||
src="images/olympus-500x500.png"
|
||||
className={`my-auto h-40 w-40 rounded-xl`}
|
||||
@@ -74,7 +74,7 @@ export function TrainingModal(props: { open: boolean }) {
|
||||
|
||||
<>
|
||||
{step === 1 && (
|
||||
<div className="flex gap-16">
|
||||
<div className="flex flex-col sm:flex-row gap-4 sm:gap-16">
|
||||
<img
|
||||
src="images/training/step1.gif"
|
||||
className={`h-72 w-72 rounded-xl`}
|
||||
@@ -93,7 +93,7 @@ export function TrainingModal(props: { open: boolean }) {
|
||||
|
||||
<>
|
||||
{step === 2 && (
|
||||
<div className="flex gap-16">
|
||||
<div className="flex flex-col sm:flex-row gap-4 sm:gap-16">
|
||||
<img
|
||||
src="images/training/step2.gif"
|
||||
className={`h-72 w-72 rounded-xl`}
|
||||
@@ -116,7 +116,7 @@ export function TrainingModal(props: { open: boolean }) {
|
||||
|
||||
<>
|
||||
{step === 3 && (
|
||||
<div className="flex gap-16">
|
||||
<div className="flex flex-col sm:flex-row gap-4 sm:gap-16">
|
||||
<img
|
||||
src="images/training/step3.gif"
|
||||
className={`h-72 w-72 rounded-xl`}
|
||||
@@ -135,7 +135,7 @@ export function TrainingModal(props: { open: boolean }) {
|
||||
|
||||
<>
|
||||
{step === 4 && (
|
||||
<div className="flex gap-16">
|
||||
<div className="flex flex-col sm:flex-row gap-4 sm:gap-16">
|
||||
<img
|
||||
src="images/training/step4.gif"
|
||||
className={`h-72 w-72 rounded-xl`}
|
||||
@@ -154,7 +154,7 @@ export function TrainingModal(props: { open: boolean }) {
|
||||
|
||||
<>
|
||||
{step === 5 && (
|
||||
<div className="flex gap-16">
|
||||
<div className="flex flex-col sm:flex-row gap-4 sm:gap-16">
|
||||
<img
|
||||
src="images/training/step5.gif"
|
||||
className={`h-72 w-72 rounded-xl`}
|
||||
@@ -177,7 +177,7 @@ export function TrainingModal(props: { open: boolean }) {
|
||||
|
||||
<>
|
||||
{step === 6 && (
|
||||
<div className="flex gap-16">
|
||||
<div className="flex flex-col sm:flex-row gap-4 sm:gap-16">
|
||||
<img
|
||||
src="images/training/step6.gif"
|
||||
className={`h-72 w-72 rounded-xl`}
|
||||
@@ -196,7 +196,7 @@ export function TrainingModal(props: { open: boolean }) {
|
||||
|
||||
<>
|
||||
{step === 7 && (
|
||||
<div className="flex gap-16">
|
||||
<div className="flex flex-col sm:flex-row gap-4 sm:gap-16">
|
||||
<img
|
||||
src="images/training/step7.gif"
|
||||
className={`h-72 w-72 rounded-xl`}
|
||||
@@ -218,7 +218,7 @@ export function TrainingModal(props: { open: boolean }) {
|
||||
|
||||
<>
|
||||
{step === 8 && (
|
||||
<div className="flex gap-16">
|
||||
<div className="flex flex-col sm:flex-row gap-4 sm:gap-16">
|
||||
<img
|
||||
src="images/training/step8.gif"
|
||||
className={`h-72 w-72 rounded-xl`}
|
||||
@@ -238,7 +238,7 @@ export function TrainingModal(props: { open: boolean }) {
|
||||
|
||||
<>
|
||||
{step === 9 && (
|
||||
<div className="flex gap-16">
|
||||
<div className="flex flex-col sm:flex-row gap-4 sm:gap-16">
|
||||
<img
|
||||
src="images/training/unitmarker.png"
|
||||
className={`max-h-34 max-w-34 my-auto rounded-xl`}
|
||||
@@ -353,11 +353,11 @@ export function TrainingModal(props: { open: boolean }) {
|
||||
|
||||
<>
|
||||
{step === 10 && (
|
||||
<div className="flex gap-16">
|
||||
<div className="flex flex-col sm:flex-row gap-4 sm:gap-16">
|
||||
<div className="flex flex-col gap-4 text-gray-400">
|
||||
<h2 className={`text-xl font-semibold text-white`}>The unit marker (2 of 2)</h2>
|
||||
<p>The unit marker has a symbol showing the unit state, i.e. what instruction it is performing. These are all the possible values:</p>
|
||||
<div className="flex max-h-40 flex-col flex-wrap gap-4">
|
||||
<div className="flex sm:max-h-64 flex-col flex-wrap gap-4">
|
||||
<div className="flex flex-col">
|
||||
<p className="flex gap-2">
|
||||
<img src="images/states/attack.svg" />
|
||||
@@ -475,7 +475,7 @@ export function TrainingModal(props: { open: boolean }) {
|
||||
)}
|
||||
|
||||
{step > 0 && (
|
||||
<div className="my-auto flex gap-2">
|
||||
<div className="my-auto gap-2 hidden sm:flex">
|
||||
{[...Array(MAX_STEPS).keys()].map((i) => (
|
||||
<div
|
||||
key={i + 1}
|
||||
|
||||
@@ -46,19 +46,12 @@ export function WarningModal(props: { open: boolean }) {
|
||||
);
|
||||
break;
|
||||
case WarningSubstate.NOT_SECURE:
|
||||
case WarningSubstate.NOT_CHROME:
|
||||
warningText = (
|
||||
<div className="flex flex-col gap-2 text-gray-400">
|
||||
<span>Your connection to DCS Olympus is not secure.</span>
|
||||
<span>To protect your personal data some advanced DCS Olympus features like the camera plugin or the audio backend have been disabled.</span>
|
||||
<span>
|
||||
To solve this issue, DCS Olympus should be served using the{" "}
|
||||
<span
|
||||
className={`italic`}
|
||||
>
|
||||
https
|
||||
</span>{" "}
|
||||
protocol.
|
||||
To solve this issue, DCS Olympus should be served using the <span className={`italic`}>https</span> protocol.
|
||||
</span>
|
||||
<span>To do so, we suggest using a dedicated server and a reverse proxy with SSL enabled.</span>
|
||||
<div className="mt-5 flex gap-4">
|
||||
@@ -73,18 +66,26 @@ export function WarningModal(props: { open: boolean }) {
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
case WarningSubstate.ERROR_UPLOADING_CONFIG:
|
||||
warningText = (
|
||||
<div className="flex flex-col gap-2 text-gray-400">
|
||||
<span>An error has occurred uploading the admin configuration.</span>
|
||||
<span></span>
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal open={props.open}>
|
||||
<Modal open={props.open} size={"sm"}>
|
||||
<div className="flex gap-2 text-xl text-white">
|
||||
<FaExclamationTriangle className={`my-auto text-4xl text-yellow-300`} />
|
||||
<div className="my-auto">Warning</div>
|
||||
</div>
|
||||
<div className="mt-10 text-white">{warningText}</div>
|
||||
<div className="text-white">{warningText}</div>
|
||||
<div className="ml-auto mt-auto flex">
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -73,7 +73,7 @@ export function AirbaseMenu(props: { open: boolean; onClose: () => void; childre
|
||||
}
|
||||
|
||||
return (
|
||||
<Menu title={airbase?.getName() ?? "No airbase selected"} open={props.open} onClose={props.onClose} showBackButton={false} canBeHidden={true}>
|
||||
<Menu title={airbase?.getName() ?? "No airbase selected"} open={props.open} onClose={props.onClose} showBackButton={false}>
|
||||
<div
|
||||
className={`
|
||||
flex flex-col gap-2 font-normal text-gray-800
|
||||
@@ -117,10 +117,9 @@ export function AirbaseMenu(props: { open: boolean; onClose: () => void; childre
|
||||
<div key={idx}>
|
||||
{Object.keys(runway.headings[0]).map((runwayName) => {
|
||||
return (
|
||||
<div
|
||||
key={`${idx}-${runwayName}`}
|
||||
className={`flex w-full justify-between`}
|
||||
>
|
||||
<div key={`${idx}-${runwayName}`} className={`
|
||||
flex w-full justify-between
|
||||
`}>
|
||||
<span>
|
||||
{" "}
|
||||
<span className="text-gray-400">RWY</span> {runwayName}
|
||||
@@ -213,6 +212,9 @@ export function AirbaseMenu(props: { open: boolean; onClose: () => void; childre
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{filteredBlueprints.filter((blueprint) => blueprint.category === "aircraft").length === 0 && (
|
||||
<span className={`text-gray-400`}>No aircraft available</span>
|
||||
)}
|
||||
</div>
|
||||
</OlAccordion>
|
||||
<OlAccordion
|
||||
@@ -264,6 +266,9 @@ export function AirbaseMenu(props: { open: boolean; onClose: () => void; childre
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{filteredBlueprints.filter((blueprint) => blueprint.category === "helicopter").length === 0 && (
|
||||
<span className={`text-gray-400`}>No helicopter available</span>
|
||||
)}
|
||||
</div>
|
||||
</OlAccordion>
|
||||
</div>
|
||||
|
||||
@@ -116,7 +116,82 @@ export function AudioMenu(props: { open: boolean; onClose: () => void; children?
|
||||
const lineDistance = (paddingRight - 40) / lineCounters[lineCounters.length - 1];
|
||||
|
||||
return (
|
||||
<Menu title="Audio menu" open={props.open} showBackButton={false} onClose={props.onClose}>
|
||||
<Menu
|
||||
title="Audio menu"
|
||||
open={props.open}
|
||||
showBackButton={false}
|
||||
onClose={props.onClose}
|
||||
wiki={() => {
|
||||
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">Audio menu</h2>
|
||||
<div>
|
||||
The audio menu allows you to add and manage audio sources, connect them to unit loudspeakers and radios, and to tune radio frequencies using the SRS integration. In other words, it allows you to communicate over SRS without the need of using the SRS client.
|
||||
</div>
|
||||
<div>
|
||||
Because of the limitations of the browser, you need to enable the audio backend by clicking on the volume icon in the navigation header. Moreover, you need to allow the browser to access your microphone and speakers. It may take a couple of seconds for the audio backend to start.
|
||||
</div>
|
||||
<div className="text-red-500">
|
||||
For security reasons, the audio backend will only work if the page is served over HTTPS.
|
||||
</div>
|
||||
<h2 className="my-4 font-bold">Managing the audio backend</h2>
|
||||
<div>
|
||||
You can select the input and output devices for the audio backend. The input device is the microphone that will be used to transmit your voice. The output device is the speaker that will be used to play the audio from the other players.
|
||||
</div>
|
||||
<div>
|
||||
You can also select the radio coalition. This will determine the default coalition for the radios you create. If you are in command mode, you can change the radio
|
||||
coalition by clicking on the coalition toggle button. This will have no effect if radio coalition enforcing is not enabled in the SRS server.
|
||||
</div>
|
||||
<h2 className="my-4 font-bold">Creating audio sources</h2>
|
||||
<div>
|
||||
You can add audio sources by clicking on the "Add audio source" button. By default, a microphone and a text to speech source are created, but you can add file sources as well, which allow to play audio files such as music, prerecorded messages, or background noise, such as gunfire or engine sounds.
|
||||
</div>
|
||||
<div>
|
||||
The text to speech generation works using the Google Cloud speech API and by default it works in English. For it to work, a valid Google Cloud API key must be installed on the Olympus backend server machine. See the backend documentation for more information. {/* TODO: put link here */}
|
||||
</div>
|
||||
<div>
|
||||
Text to speech and file sources can be set to operate in loop mode, which will make them repeat the audio indefinitely. This is useful for background noise or music. Moreover, you can set the volume of the audio sources.
|
||||
</div>
|
||||
<h2 className="my-4 font-bold">Creating radios and loudspeakers</h2>
|
||||
<div>
|
||||
By default, two radios are created, but you can add more by clicking on the "Add radio" button. Radios can be tuned to different frequencies, and they can be set to operate in AM or FM mode. You can also set the volume of the radios, and change the balance between the left and right channels.
|
||||
</div>
|
||||
<div>
|
||||
When a new radio is created, it will NOT be in "listen" mode, so you will need to click on the "Tune to radio" button to start listening.
|
||||
</div>
|
||||
<div>
|
||||
You have three options to transmit on the radio:
|
||||
<div>
|
||||
<li>By clicking on the "Talk on frequency" button on the radio panel. This will enable continuous transmission and will remain "on" until clicked again.</li>
|
||||
<li>By clicking on the "Push to talk" button located over the mouse coordinates panel, on the bottom right corner of the map.</li>
|
||||
<li>By using the "Push to talk" keyboard shortcuts, which can be edited in the options menu.</li>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
Loudspeakers can be used to simulate environmental sounds, like 5MC calls on the carrier, or sirens. To create a loudspeaker, click on the unit that should broadcast the sound, and then click on the "Loudspeakers" button. PTT buttons for loudspeakers operate in the same way as radios.
|
||||
</div>
|
||||
<div className="text-red-500">
|
||||
The loudspeakers system uses the SRS integration, so it will only work if other players' SRS clients are running and connected to the same server as Olympus. Moreover, the loudspeaker system operates using the INTERCOM radio in SRS, and for the time being it will only work for those radios that have the INTERCOM radio enabled (i.e. usually multicrew aircraft).
|
||||
</div>
|
||||
<h2 className="my-4 font-bold">Connecting sources and radios/loudspeakers</h2>
|
||||
<div>
|
||||
Each source can be connected to one or more radios or loudspeakers. To connect a source to a radio or loudspeaker, click on the "+" button on the right of the source panel, then click on the equivalent button on the desired radio/loudspeaker. To disconnect a source from a radio or loudspeaker, click on the "-" button next to the radio/loudspeaker.
|
||||
</div>
|
||||
<div>
|
||||
The connection lines will show the connections between the sources and the radios/loudspeakers. The color of the line is randomly generated and will be different for each source.
|
||||
</div>
|
||||
<div>
|
||||
By connecting multiple sources to the same radio/loudspeaker, you can create complex audio setups, like playing background music while transmitting on the radio.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
>
|
||||
<div className="flex content-center gap-4 p-4">
|
||||
<div className="my-auto text-gray-400">
|
||||
<FaQuestionCircle />
|
||||
|
||||
@@ -32,7 +32,7 @@ export function AWACSMenu(props: { open: boolean; onClose: () => void; children?
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Menu title={"AWACS Tools"} open={props.open} onClose={props.onClose} showBackButton={false} canBeHidden={true}>
|
||||
<Menu title={"AWACS Tools"} open={props.open} onClose={props.onClose} showBackButton={false}>
|
||||
<div
|
||||
className={`
|
||||
flex flex-col gap-4 p-4 font-normal text-gray-800
|
||||
|
||||
@@ -1,46 +1,66 @@
|
||||
import { faArrowLeft, faCircleQuestion, faClose } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faArrowLeft, faCircleQuestion, faClose, faEyeSlash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { FaChevronDown, FaChevronUp } from "react-icons/fa";
|
||||
import { FaChevronRight } from "react-icons/fa6";
|
||||
|
||||
export function Menu(props: {
|
||||
title: string;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
canBeHidden?: boolean;
|
||||
onBack?: () => void;
|
||||
showBackButton?: boolean;
|
||||
children?: JSX.Element | JSX.Element[];
|
||||
wiki?: () => (JSX.Element | JSX.Element[]);
|
||||
autohide?: boolean;
|
||||
wiki?: () => JSX.Element | JSX.Element[];
|
||||
wikiDisabled?: boolean;
|
||||
}) {
|
||||
const [hide, setHide] = useState(true);
|
||||
const [wiki, setWiki] = useState(false);
|
||||
|
||||
if (!props.open && hide) setHide(false);
|
||||
useEffect(() => {
|
||||
if (props.autohide) {
|
||||
if (window.innerWidth > 640) setHide(false);
|
||||
if (!props.open) setHide(true);
|
||||
} else {
|
||||
setHide(false);
|
||||
}
|
||||
}, [props.open]);
|
||||
|
||||
return (
|
||||
<div
|
||||
data-open={props.open}
|
||||
data-wiki={wiki}
|
||||
className={`
|
||||
pointer-events-none absolute left-16 right-0 top-[58px] z-10
|
||||
h-[calc(100vh-58px)] bg-transparent transition-all ol-panel-container
|
||||
pointer-events-none absolute left-16 right-0 top-[58px] z-10 flex
|
||||
h-[calc(100vh-58px)] transition-all ol-panel-container
|
||||
data-[open='false']:-translate-x-full
|
||||
data-[wiki='true']:w-[calc(100%-58px)] data-[wiki='true']:lg:w-[800px]
|
||||
sm:w-[400px]
|
||||
`}
|
||||
tabIndex={-1}
|
||||
>
|
||||
{props.open && (
|
||||
<div className="absolute flex h-full w-[30px]">
|
||||
<div
|
||||
className={`
|
||||
pointer-events-auto my-auto flex h-[80px] w-full cursor-pointer
|
||||
justify-center rounded-r-lg bg-olympus-800/90 backdrop-blur-lg
|
||||
backdrop-grayscale
|
||||
hover:bg-olympus-400/90
|
||||
`}
|
||||
onClick={() => setHide(!hide)}
|
||||
>
|
||||
<FaChevronRight className={`my-auto text-gray-400`} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
data-hide={hide}
|
||||
data-canbehidden={props.canBeHidden}
|
||||
className={`
|
||||
pointer-events-auto h-[calc(100vh-58px)] overflow-y-auto
|
||||
overflow-x-hidden backdrop-blur-lg backdrop-grayscale
|
||||
transition-transform no-scrollbar
|
||||
pointer-events-auto h-[calc(100vh-58px)] w-full backdrop-blur-lg
|
||||
backdrop-grayscale transition-transform
|
||||
dark:bg-olympus-800/90
|
||||
data-[canbehidden='true']:h-[calc(100vh-58px-2rem)]
|
||||
data-[hide='true']:translate-y-[calc(100vh-58px)]
|
||||
data-[hide='true']:-translate-x-full
|
||||
`}
|
||||
>
|
||||
<h5
|
||||
@@ -61,12 +81,24 @@ export function Menu(props: {
|
||||
/>
|
||||
)}
|
||||
{props.title}
|
||||
{!(props.wikiDisabled === true) && (
|
||||
<FontAwesomeIcon
|
||||
onClick={() => setWiki(!wiki)}
|
||||
icon={faCircleQuestion}
|
||||
className={`
|
||||
ml-auto flex cursor-pointer items-center justify-center
|
||||
rounded-md p-2 text-lg
|
||||
dark:text-gray-500 dark:hover:bg-gray-700 dark:hover:text-white
|
||||
hover:bg-gray-200
|
||||
`}
|
||||
/>
|
||||
)}
|
||||
<FontAwesomeIcon
|
||||
onClick={() => setWiki(!wiki)}
|
||||
icon={faCircleQuestion}
|
||||
onClick={() => setHide(true)}
|
||||
icon={faEyeSlash}
|
||||
className={`
|
||||
ml-auto flex cursor-pointer items-center justify-center rounded-md
|
||||
p-2 text-lg
|
||||
flex cursor-pointer items-center justify-center rounded-md p-2
|
||||
text-lg
|
||||
dark:text-gray-500 dark:hover:bg-gray-700 dark:hover:text-white
|
||||
hover:bg-gray-200
|
||||
`}
|
||||
@@ -82,37 +114,33 @@ export function Menu(props: {
|
||||
`}
|
||||
/>
|
||||
</h5>
|
||||
<div className="flex h-[calc(100%-3rem)]">
|
||||
<div data-wiki={wiki} className={`
|
||||
w-0 overflow-hidden transition-all
|
||||
data-[wiki='true']:w-[50%]
|
||||
`}>
|
||||
{props.wiki ? props.wiki() : <div className={`p-4 text-gray-200`}>Work in progress</div>}
|
||||
</div>
|
||||
<div data-wiki={wiki} className={`
|
||||
w-full
|
||||
sm:w-[400px]
|
||||
`}>{props.children}</div>
|
||||
</div>
|
||||
</div>
|
||||
{props.canBeHidden == true && (
|
||||
<div
|
||||
className={`
|
||||
pointer-events-auto flex h-8 cursor-pointer justify-center
|
||||
bg-olympus-800/90 backdrop-blur-lg backdrop-grayscale
|
||||
hover:bg-olympus-400/90
|
||||
flex h-[calc(100%-3rem)] w-full flex-col
|
||||
sm:flex-row
|
||||
`}
|
||||
onClick={() => setHide(!hide)}
|
||||
>
|
||||
{hide ? (
|
||||
<FaChevronUp className="mx-auto my-auto text-gray-400" />
|
||||
) : (
|
||||
<FaChevronDown
|
||||
className={`mx-auto my-auto text-gray-400`}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
data-wiki={wiki}
|
||||
className={`
|
||||
h-0 w-0 overflow-hidden transition-all
|
||||
data-[wiki='true']:min-h-[50%] data-[wiki='true']:min-w-full
|
||||
sm:data-[wiki='true']:min-h-full sm:data-[wiki='true']:min-w-[50%]
|
||||
`}
|
||||
>
|
||||
{props.wiki ? props.wiki() : <div className={`p-4 text-gray-200`}>Work in progress</div>}
|
||||
</div>
|
||||
<div
|
||||
data-wiki={wiki}
|
||||
className={`
|
||||
relative overflow-y-auto overflow-x-hidden no-scrollbar
|
||||
sm:w-[400px]
|
||||
`}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { DrawSubState, MAP_OPTIONS_DEFAULTS, NO_SUBSTATE, OlympusState, OlympusS
|
||||
import { AppStateChangedEvent, ContextActionSetChangedEvent, MapOptionsChangedEvent, ShortcutsChangedEvent } from "../../events";
|
||||
import { ContextActionSet } from "../../unit/contextactionset";
|
||||
import { MapToolBar } from "./maptoolbar";
|
||||
import { CoordinatesPanel } from "./coordinatespanel";
|
||||
|
||||
export function ControlsPanel(props: {}) {
|
||||
const [controls, setControls] = useState(
|
||||
@@ -200,12 +201,12 @@ export function ControlsPanel(props: {}) {
|
||||
className={`
|
||||
absolute right-[0px] top-16
|
||||
${mapOptions.showMinimap ? `bottom-[233px]` : `bottom-[65px]`}
|
||||
pointer-events-none flex w-[310px] flex-col items-center justify-between
|
||||
pointer-events-none flex w-[288px] flex-col items-center justify-between
|
||||
gap-1 p-3 text-sm
|
||||
`}
|
||||
>
|
||||
<MapToolBar />
|
||||
{controls?.map((control) => {
|
||||
{/*controls?.map((control) => {
|
||||
return (
|
||||
<div
|
||||
key={control.text}
|
||||
@@ -246,7 +247,7 @@ export function ControlsPanel(props: {}) {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
})*/}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ export function CoordinatesPanel(props: {}) {
|
||||
const [elevation, setElevation] = useState(0);
|
||||
const [bullseyes, setBullseyes] = useState(null as null | { [name: string]: Bullseye });
|
||||
const [selectedUnits, setSelectedUnits] = useState([] as Unit[]);
|
||||
const [open, setOpen] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
MouseMovedEvent.on((latlng, elevation) => {
|
||||
@@ -27,18 +28,30 @@ export function CoordinatesPanel(props: {}) {
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
absolute bottom-[20px] right-[310px] flex min-h-12 w-[380px] flex-col
|
||||
items-center justify-between gap-2 rounded-lg bg-gray-200 px-3 py-3
|
||||
text-sm backdrop-blur-lg backdrop-grayscale
|
||||
flex w-full flex-col items-center justify-between gap-2 rounded-lg
|
||||
bg-gray-200 px-3 py-3 text-sm backdrop-blur-lg backdrop-grayscale
|
||||
dark:bg-olympus-800/90 dark:text-gray-200
|
||||
`}
|
||||
onClick={() => setOpen(!open)}
|
||||
>
|
||||
{bullseyes && (
|
||||
<div className="flex w-full items-center justify-start">
|
||||
<div className="absolute right-[12px] top-[15px]">
|
||||
{open ? (
|
||||
<FaChevronDown className="cursor-pointer" />
|
||||
) : (
|
||||
<FaChevronUp
|
||||
className={`cursor-pointer`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{open && bullseyes && (
|
||||
<div
|
||||
className={`
|
||||
flex w-full flex-col items-start justify-start gap-2
|
||||
`}
|
||||
>
|
||||
<div
|
||||
className={`
|
||||
mr-[11px] flex min-w-64 max-w-64 items-center justify-between
|
||||
gap-2
|
||||
flex flex min-w-64 max-w-64 items-start justify-between gap-2
|
||||
`}
|
||||
>
|
||||
{bullseyes[2] && (
|
||||
@@ -84,18 +97,27 @@ export function CoordinatesPanel(props: {}) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div
|
||||
className={`
|
||||
flex w-full items-center justify-between pointer-events-all
|
||||
`}
|
||||
>
|
||||
<OlLocation className="!min-w-64 !max-w-64 bg-transparent !p-0" location={latlng} />
|
||||
<span
|
||||
className={`
|
||||
mr-2 rounded-sm bg-white px-1 py-1 text-center font-bold
|
||||
text-olympus-700
|
||||
`}
|
||||
>
|
||||
<FaMountain />
|
||||
</span>
|
||||
<div className="min-w-12">{mToFt(elevation).toFixed()}ft</div>
|
||||
</div>
|
||||
|
||||
{open && (
|
||||
<div className="flex w-full items-center justify-start">
|
||||
<span
|
||||
className={`
|
||||
mr-2 rounded-sm bg-white px-1 py-1 text-center font-bold
|
||||
text-olympus-700
|
||||
`}
|
||||
>
|
||||
<FaMountain />
|
||||
</span>
|
||||
<div className="min-w-12">{mToFt(elevation).toFixed()}ft</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -92,7 +92,11 @@ export function DrawingMenu(props: { open: boolean; onClose: () => void }) {
|
||||
hover:scale-125 hover:text-gray-200
|
||||
`}
|
||||
onClick={() => {
|
||||
container.setVisibility(!container.getVisibility(), true);
|
||||
if (container === mainDrawingsContainer.container) {
|
||||
getApp().getMap().setOption("showMissionDrawings", !getApp().getMap().getOptions().showMissionDrawings);
|
||||
} else {
|
||||
container.setVisibility(!container.getVisibility(), true);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
@@ -147,12 +151,60 @@ export function DrawingMenu(props: { open: boolean; onClose: () => void }) {
|
||||
open={props.open}
|
||||
title="Draw"
|
||||
onClose={props.onClose}
|
||||
canBeHidden={true}
|
||||
showBackButton={appSubState !== DrawSubState.NO_SUBSTATE}
|
||||
onBack={() => {
|
||||
getApp().getCoalitionAreasManager().setSelectedArea(null);
|
||||
getApp().setState(OlympusState.DRAW, DrawSubState.NO_SUBSTATE);
|
||||
}}
|
||||
wiki={() => {
|
||||
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">Drawing menu</h2>
|
||||
<div>
|
||||
The drawing menu allows you to create and manage custom drawings, such as polygons and circles, and to generate IADS (Integrated Air Defense
|
||||
System) areas. Moreover, you can manage the visibility and opacity of mission drawings, i.e. drawings from the Mission Editor.
|
||||
</div>
|
||||
<h2 className="my-4 font-bold">Custom drawings and IADS</h2>
|
||||
<div>
|
||||
To create a custom drawing, click on the 'Add polygon' or 'Add circle' buttons, then click on the map to add polygons or to move the drawing.
|
||||
Double-click on the map to finish your creation. You can then edit the drawing by clicking on it. You can also move it up or down in the list, or
|
||||
delete it.
|
||||
</div>
|
||||
<div>
|
||||
You can change the name and the coalition of the area. You can also generate an IADS area by selecting the types, eras, and ranges of units you
|
||||
want to include in the area. You can also set the density and distribution of the IADS. If you check the 'Force coalition appropriate units' box,
|
||||
the IADS will only include units that are appropriate for the coalition of the area (e.g. Hawk SAMs for {""}
|
||||
<span className="text-blue-500">blue</span> and SA-6 SAMs for{" "}
|
||||
<span
|
||||
className={`text-red-500`}
|
||||
>
|
||||
red
|
||||
</span>
|
||||
).
|
||||
</div>
|
||||
<div>
|
||||
The IADS generator will create a random distribution of units in the area, based on the density and distribution you set. Units will be
|
||||
concentrated around cities, and airbases that belong to the selected coalition.
|
||||
</div>
|
||||
<h2 className="my-4 font-bold">Mission drawings</h2>
|
||||
<div>
|
||||
You can manage the visibility and opacity of mission drawings by clicking on the eye icon. Moreover, you can change the opacity of the drawing by
|
||||
using the slider. You can also hide or show all the drawings in a container.
|
||||
</div>
|
||||
<div>
|
||||
You can search for a specific drawing by typing in the search bar. The search is case-insensitive and will match any part of the drawing name.
|
||||
</div>
|
||||
<div>
|
||||
Any change you make is persistent and will be saved for the next time you reload Olympus, as long as the DCS mission was not restarted.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
>
|
||||
<>
|
||||
{appState === OlympusState.DRAW && appSubState === DrawSubState.NO_SUBSTATE && (
|
||||
@@ -297,12 +349,10 @@ export function DrawingMenu(props: { open: boolean; onClose: () => void }) {
|
||||
bg-olympus-600 p-5
|
||||
`}
|
||||
>
|
||||
<div
|
||||
className={`border-b-2 border-b-olympus-100 pb-4 text-gray-300`}
|
||||
>
|
||||
Automatic IADS generation
|
||||
</div>
|
||||
<OlDropdown className="" label="Units types">
|
||||
<div className={`
|
||||
border-b-2 border-b-olympus-100 pb-4 text-gray-300
|
||||
`}>Automatic IADS generation</div>
|
||||
<OlDropdown className="" label="Units types" disableAutoClose={true}>
|
||||
{types.map((type, idx) => {
|
||||
if (!(type in typesSelection)) {
|
||||
typesSelection[type] = true;
|
||||
@@ -323,7 +373,7 @@ export function DrawingMenu(props: { open: boolean; onClose: () => void }) {
|
||||
);
|
||||
})}
|
||||
</OlDropdown>
|
||||
<OlDropdown className="" label="Units eras">
|
||||
<OlDropdown className="" label="Units eras" disableAutoClose={true}>
|
||||
{eras.map((era) => {
|
||||
if (!(era in erasSelection)) {
|
||||
erasSelection[era] = true;
|
||||
@@ -344,7 +394,7 @@ export function DrawingMenu(props: { open: boolean; onClose: () => void }) {
|
||||
);
|
||||
})}
|
||||
</OlDropdown>
|
||||
<OlDropdown className="" label="Units ranges">
|
||||
<OlDropdown className="" label="Units ranges" disableAutoClose={true}>
|
||||
{["Short range", "Medium range", "Long range"].map((range) => {
|
||||
if (!(range in rangesSelection)) {
|
||||
rangesSelection[range] = true;
|
||||
|
||||
@@ -30,11 +30,17 @@ export function EffectSpawnMenu(props: { visible: boolean; compact: boolean; eff
|
||||
type: props.effect,
|
||||
explosionType,
|
||||
});
|
||||
else if (props.effect === "smoke")
|
||||
else if (props.effect === "smoke") {
|
||||
let colorName = "white";
|
||||
if (smokeColor === colors.BLUE) colorName = "blue";
|
||||
else if (smokeColor === colors.RED) colorName = "red";
|
||||
else if (smokeColor === colors.GREEN) colorName = "green";
|
||||
else if (smokeColor === colors.ORANGE) colorName = "orange";
|
||||
getApp()?.getMap()?.setEffectRequestTable({
|
||||
type: props.effect,
|
||||
smokeColor,
|
||||
smokeColor: colorName,
|
||||
});
|
||||
}
|
||||
getApp().setState(OlympusState.SPAWN, SpawnSubState.SPAWN_EFFECT);
|
||||
} else {
|
||||
if (appState === OlympusState.SPAWN && appSubState === SpawnSubState.SPAWN_EFFECT) getApp().setState(OlympusState.IDLE);
|
||||
@@ -129,7 +135,13 @@ export function EffectSpawnMenu(props: { visible: boolean; compact: boolean; eff
|
||||
else if (explosionType === "White phosphorous") getApp().getServerManager().spawnExplosion(50, "phosphorous", props.latlng);
|
||||
getApp().getMap().addExplosionMarker(props.latlng);
|
||||
} else if (props.effect === "smoke") {
|
||||
getApp().getServerManager().spawnSmoke(smokeColor, props.latlng);
|
||||
/* Find the name of the color */
|
||||
let colorName = "white";
|
||||
if (smokeColor === colors.BLUE) colorName = "blue";
|
||||
else if (smokeColor === colors.RED) colorName = "red";
|
||||
else if (smokeColor === colors.GREEN) colorName = "green";
|
||||
else if (smokeColor === colors.ORANGE) colorName = "orange";
|
||||
getApp().getServerManager().spawnSmoke(colorName, props.latlng);
|
||||
getApp()
|
||||
.getMap()
|
||||
.addSmokeMarker(props.latlng, smokeColor ?? colors.WHITE);
|
||||
|
||||
@@ -6,6 +6,9 @@ import { getApp } from "../../olympusapp";
|
||||
import { ServerStatus } from "../../interfaces";
|
||||
import { CommandModeOptionsChangedEvent, ServerStatusUpdatedEvent } from "../../events";
|
||||
import { BLUE_COMMANDER, COMMAND_MODE_OPTIONS_DEFAULTS, ERAS_ORDER, GAME_MASTER, RED_COMMANDER } from "../../constants/constants";
|
||||
import { secondsToTimeString } from "../../other/utils";
|
||||
import { FaQuestionCircle } from "react-icons/fa";
|
||||
import { FaMinus, FaPlus } from "react-icons/fa6";
|
||||
|
||||
export function GameMasterMenu(props: { open: boolean; onClose: () => void; children?: JSX.Element | JSX.Element[] }) {
|
||||
const [commandModeOptions, setCommandModeOptions] = useState(COMMAND_MODE_OPTIONS_DEFAULTS);
|
||||
@@ -21,53 +24,131 @@ export function GameMasterMenu(props: { open: boolean; onClose: () => void; chil
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Menu title="Game Master options" open={props.open} showBackButton={false} onClose={props.onClose}>
|
||||
<Menu title="Game Master options" open={props.open} showBackButton={false} onClose={props.onClose} wiki={() => {
|
||||
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">Game Master menu</h2>
|
||||
<div>
|
||||
The Game Master menu allows the Game Master to set up the game session for the Real Time Strategy game mode of DCS Olympus.
|
||||
</div>
|
||||
<div>
|
||||
In this mode, commanders can play against eachother in a real-time strategy game, where they can spawn a limited amount of units. Each commander can only control units belonging to their coalition. Moreover, they can only see enemy units if detected, so proper placement of radars is crucial.
|
||||
</div>
|
||||
<div>
|
||||
The Game Master can set up the game session by restricting the unit spawns, setting the setup time, and restricting the eras of the units that can be spawned. Moreover, the Game Master can set the amount of spawn points available for each coalition.
|
||||
</div>
|
||||
<div>
|
||||
During the setup time, commanders can prepare the battlefield. As long as they have sufficient spawn points, they can place units anywhere on the map. After the setup time ends, the game starts and the restrictions are enforced.
|
||||
</div>
|
||||
<div>
|
||||
When restrictions are enforced, commanders will no longer be able to spawn ground units, and air units can only be spawned from airfields.
|
||||
</div>
|
||||
<div>
|
||||
There are multiple additional modes of play. You can disable the spawn restrictions to allow commanders to spawn units freely, but can only see detected units, or you can set the spawn points to 0 to disable unit spawns entirely and force commanders to only use the units they have at the start of the game or that you provide.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}}>
|
||||
<div
|
||||
className={`
|
||||
flex flex-col gap-2 p-5 font-normal text-gray-800
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
You are operating as:
|
||||
{commandModeOptions.commandMode === GAME_MASTER && (
|
||||
<div
|
||||
className={`
|
||||
w-full rounded-md bg-olympus-400 p-2 text-center font-bold
|
||||
`}
|
||||
>
|
||||
GAME MASTER
|
||||
</div>
|
||||
)}
|
||||
{commandModeOptions.commandMode === BLUE_COMMANDER && <div className={`
|
||||
w-full rounded-md bg-blue-600 p-2 text-center font-bold
|
||||
`}>BLUE COMMANDER</div>}
|
||||
{commandModeOptions.commandMode === RED_COMMANDER && <div className={`
|
||||
w-full rounded-md bg-red-700 p-2 text-center font-bold
|
||||
`}>RED COMMANDER</div>}
|
||||
{serverStatus.elapsedTime > currentSetupTime && (
|
||||
<div
|
||||
className={`
|
||||
w-full rounded-md bg-orange-600 p-2 text-center font-bold
|
||||
`}
|
||||
>
|
||||
Setup time has ended
|
||||
</div>
|
||||
)}
|
||||
{serverStatus.elapsedTime <= currentSetupTime && (
|
||||
<div
|
||||
className={`
|
||||
w-full rounded-md bg-green-700 p-2 text-center font-bold
|
||||
`}
|
||||
>
|
||||
SETUP ends in {(currentSetupTime - serverStatus.elapsedTime)?.toFixed()} seconds
|
||||
{commandModeOptions.restrictSpawns ? (
|
||||
<>
|
||||
<div className="mb-4 flex content-center gap-4">
|
||||
<div className="my-auto text-gray-400">
|
||||
<FaQuestionCircle />
|
||||
</div>
|
||||
<div className="text-sm text-gray-400">
|
||||
Unit spawns are restricted. During the SETUP phase, commanders can spawn units according to the settings below. After the SETUP phase ends,
|
||||
ground/navy units and air spawns are disabled, and commanders can spawn aircraft/helicopters only from airfields.
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex">
|
||||
{commandModeOptions.commandMode === GAME_MASTER && (
|
||||
<button
|
||||
className={`
|
||||
h-10 rounded-s-lg bg-gray-100 p-3
|
||||
dark:bg-gray-700 dark:hover:bg-gray-600
|
||||
dark:focus:ring-blue-700
|
||||
focus:outline-none focus:ring-2 focus:ring-gray-100
|
||||
hover:bg-gray-200
|
||||
`}
|
||||
onClick={() => {
|
||||
const newCommandModeOptions = { ...commandModeOptions };
|
||||
newCommandModeOptions.setupTime = Math.max(serverStatus.elapsedTime, newCommandModeOptions.setupTime - 60);
|
||||
if (commandModeOptions.commandMode !== GAME_MASTER) return;
|
||||
setCommandModeOptions(newCommandModeOptions);
|
||||
setCurrentSetupTime(newCommandModeOptions.setupTime);
|
||||
getApp().getServerManager().setCommandModeOptions(newCommandModeOptions);
|
||||
}}
|
||||
>
|
||||
<FaMinus className="my-auto" />
|
||||
</button>
|
||||
)}
|
||||
<div className={`
|
||||
relative z-[-1] flex h-10 w-[360px] bg-olympus-600
|
||||
`}>
|
||||
<div
|
||||
className={`
|
||||
absolute my-auto w-full text-center before
|
||||
before:absolute before:left-0 before:z-[-1] before:h-10
|
||||
before:w-full before:bg-olympus-400 before:content-['']
|
||||
`}
|
||||
style={{ width: `${Math.min(100, 100 - ((currentSetupTime - serverStatus.elapsedTime) / currentSetupTime) * 100)}%` }}
|
||||
></div>
|
||||
{currentSetupTime - serverStatus.elapsedTime > 0 ? (
|
||||
<div className="mx-auto my-auto">SETUP ends in {secondsToTimeString(currentSetupTime - serverStatus.elapsedTime)}</div>
|
||||
) : (
|
||||
<div className="mx-auto my-auto animate-pulse">SETUP ended, restrictions active</div>
|
||||
)}
|
||||
</div>
|
||||
{commandModeOptions.commandMode === GAME_MASTER && (
|
||||
<button
|
||||
className={`
|
||||
h-10 rounded-e-lg bg-gray-100 p-3
|
||||
dark:bg-gray-700 dark:hover:bg-gray-600
|
||||
dark:focus:ring-blue-700
|
||||
focus:outline-none focus:ring-2 focus:ring-gray-100
|
||||
hover:bg-gray-200
|
||||
`}
|
||||
onClick={() => {
|
||||
const newCommandModeOptions = { ...commandModeOptions };
|
||||
newCommandModeOptions.setupTime = Math.max(serverStatus.elapsedTime + 60, newCommandModeOptions.setupTime + 60);
|
||||
if (commandModeOptions.commandMode !== GAME_MASTER) return;
|
||||
setCommandModeOptions(newCommandModeOptions);
|
||||
setCurrentSetupTime(newCommandModeOptions.setupTime);
|
||||
getApp().getServerManager().setCommandModeOptions(newCommandModeOptions);
|
||||
}}
|
||||
>
|
||||
<FaPlus className="my-auto" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex content-center gap-4">
|
||||
<div className="my-auto text-gray-400">
|
||||
<FaQuestionCircle />
|
||||
</div>
|
||||
<div className="text-sm text-gray-400">
|
||||
Unit spawns are NOT restricted, therefore no setup time is enforced and commanders can spawn units as desired. Only unit detection is enforced.
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<span className="mt-5">Options: </span>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex flex-col gap-3">
|
||||
<div
|
||||
className={`
|
||||
group flex flex-row rounded-md justify-content cursor-pointer
|
||||
gap-4 p-2
|
||||
gap-4 px-2
|
||||
dark:hover:bg-olympus-400
|
||||
`}
|
||||
onClick={() => {
|
||||
@@ -82,13 +163,13 @@ export function GameMasterMenu(props: { open: boolean; onClose: () => void; chil
|
||||
data-disabled={!commandModeOptions.restrictSpawns || commandModeOptions.commandMode !== GAME_MASTER}
|
||||
className={`data-[disabled='true']:text-gray-400`}
|
||||
>
|
||||
Restrict unit spanws
|
||||
Restrict unit spawns
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
group flex flex-row rounded-md justify-content cursor-pointer
|
||||
gap-4 p-2
|
||||
gap-4 px-2
|
||||
dark:hover:bg-olympus-400
|
||||
`}
|
||||
onClick={() => {
|
||||
@@ -120,7 +201,7 @@ export function GameMasterMenu(props: { open: boolean; onClose: () => void; chil
|
||||
key={era}
|
||||
className={`
|
||||
group flex flex-row rounded-md justify-content
|
||||
cursor-pointer gap-4 p-2
|
||||
cursor-pointer gap-4 px-2
|
||||
dark:hover:bg-olympus-400
|
||||
`}
|
||||
onClick={() => {
|
||||
@@ -224,56 +305,6 @@ export function GameMasterMenu(props: { open: boolean; onClose: () => void; chil
|
||||
}}
|
||||
></OlNumberInput>
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
group flex flex-row rounded-md justify-content gap-4 px-4 py-2
|
||||
`}
|
||||
>
|
||||
<span
|
||||
data-disabled={!commandModeOptions.restrictSpawns || commandModeOptions.commandMode !== GAME_MASTER}
|
||||
className={`
|
||||
my-auto mr-auto
|
||||
data-[disabled='true']:text-gray-400
|
||||
`}
|
||||
>
|
||||
Setup time (seconds)
|
||||
</span>
|
||||
<OlNumberInput
|
||||
min={0}
|
||||
max={6000}
|
||||
value={commandModeOptions.setupTime}
|
||||
onChange={(e) => {
|
||||
if (!commandModeOptions.restrictSpawns || commandModeOptions.commandMode !== GAME_MASTER) return;
|
||||
const newCommandModeOptions = { ...commandModeOptions };
|
||||
newCommandModeOptions.setupTime = parseInt(e.target.value);
|
||||
setCommandModeOptions(newCommandModeOptions);
|
||||
}}
|
||||
onIncrease={() => {
|
||||
if (!commandModeOptions.restrictSpawns || commandModeOptions.commandMode !== GAME_MASTER) return;
|
||||
const newCommandModeOptions = { ...commandModeOptions };
|
||||
newCommandModeOptions.setupTime = Math.min(newCommandModeOptions.setupTime + 10, 6000);
|
||||
setCommandModeOptions(newCommandModeOptions);
|
||||
}}
|
||||
onDecrease={() => {
|
||||
if (!commandModeOptions.restrictSpawns || commandModeOptions.commandMode !== GAME_MASTER) return;
|
||||
const newCommandModeOptions = { ...commandModeOptions };
|
||||
newCommandModeOptions.setupTime = Math.max(newCommandModeOptions.setupTime - 10, 0);
|
||||
setCommandModeOptions(newCommandModeOptions);
|
||||
}}
|
||||
></OlNumberInput>
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
group flex flex-row rounded-md justify-content gap-4 px-4 py-2
|
||||
`}
|
||||
>
|
||||
<span className="mr-auto">Elapsed time (seconds)</span>{" "}
|
||||
<span
|
||||
className={`w-32 text-center`}
|
||||
>
|
||||
{serverStatus.elapsedTime?.toFixed()}
|
||||
</span>
|
||||
</div>
|
||||
{commandModeOptions.commandMode === GAME_MASTER && (
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -50,6 +50,8 @@ import {
|
||||
import { OlympusConfig } from "../../interfaces";
|
||||
import { FaCheck, FaQuestionCircle, FaSave, FaSpinner } from "react-icons/fa";
|
||||
import { OlExpandingTooltip } from "../components/olexpandingtooltip";
|
||||
import { ftToM } from "../../other/utils";
|
||||
import { LatLng } from "leaflet";
|
||||
|
||||
export function Header() {
|
||||
const [mapHiddenTypes, setMapHiddenTypes] = useState(MAP_HIDDEN_TYPES_DEFAULTS);
|
||||
@@ -64,7 +66,9 @@ export function Header() {
|
||||
|
||||
useEffect(() => {
|
||||
HiddenTypesChangedEvent.on((hiddenTypes) => setMapHiddenTypes({ ...hiddenTypes }));
|
||||
MapOptionsChangedEvent.on((mapOptions) => setMapOptions({ ...mapOptions }));
|
||||
MapOptionsChangedEvent.on((mapOptions) => {
|
||||
setMapOptions({ ...mapOptions })
|
||||
});
|
||||
MapSourceChangedEvent.on((source) => setMapSource(source));
|
||||
ConfigLoadedEvent.on((config: OlympusConfig) => {
|
||||
// Timeout needed to make sure the map configuration has updated
|
||||
@@ -100,9 +104,16 @@ export function Header() {
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
z-10 flex w-full gap-4 border-gray-200 bg-gray-300 px-3 align-center
|
||||
relative z-10 flex w-full gap-4 border-gray-200 bg-gray-300 px-3
|
||||
align-center
|
||||
dark:border-gray-800 dark:bg-olympus-900
|
||||
`}
|
||||
onWheel={(e) => {
|
||||
if (scrollRef.current) {
|
||||
if (e.deltaY > 0) (scrollRef.current as HTMLElement).scrollLeft += 100;
|
||||
else (scrollRef.current as HTMLElement).scrollLeft -= 100;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<img src="images/icon.png" className={`my-auto h-10 w-10 rounded-md p-0`}></img>
|
||||
{!scrolledLeft && (
|
||||
@@ -145,21 +156,23 @@ export function Header() {
|
||||
{IP}
|
||||
</div>
|
||||
</div>
|
||||
{savingSessionData ? (
|
||||
<div className="text-white">
|
||||
<FaSpinner className={`animate-spin text-2xl`} />
|
||||
</div>
|
||||
) : (
|
||||
<div className={`relative text-white`}>
|
||||
<FaFloppyDisk className={`absolute -top-3 text-2xl`} />
|
||||
<FaCheck
|
||||
className={`
|
||||
<div className="w-8">
|
||||
{savingSessionData ? (
|
||||
<div className="text-white">
|
||||
<FaSpinner className={`animate-spin text-2xl`} />
|
||||
</div>
|
||||
) : (
|
||||
<div className={`relative text-white`}>
|
||||
<FaFloppyDisk className={`absolute -top-3 text-2xl`} />
|
||||
<FaCheck
|
||||
className={`
|
||||
absolute left-[9px] top-[-6px] text-2xl text-olympus-900
|
||||
`}
|
||||
/>
|
||||
<FaCheck className={`absolute left-3 top-0 text-green-500`} />
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<FaCheck className={`absolute left-3 top-0 text-green-500`} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{commandModeOptions.commandMode === BLUE_COMMANDER && (
|
||||
|
||||
@@ -60,7 +60,7 @@ export function JTACMenu(props: { open: boolean; onClose: () => void; children?:
|
||||
let targetPosition = (targetUnit ? targetUnit.getPosition() : targetLocation) ?? new LatLng(0, 0);
|
||||
|
||||
return (
|
||||
<Menu title={"JTAC Tools"} open={props.open} onClose={props.onClose} showBackButton={false} canBeHidden={true}>
|
||||
<Menu title={"JTAC Tools"} open={props.open} onClose={props.onClose} showBackButton={false}>
|
||||
<div
|
||||
className={`
|
||||
flex flex-col gap-2 p-4 font-normal text-gray-800
|
||||
|
||||
@@ -5,6 +5,8 @@ import { getApp } from "../../olympusapp";
|
||||
import { FaChevronDown, FaChevronUp } from "react-icons/fa6";
|
||||
import { MapOptionsChangedEvent, ServerStatusUpdatedEvent } from "../../events";
|
||||
import { colors, MAP_OPTIONS_DEFAULTS } from "../../constants/constants";
|
||||
import { CoordinatesPanel } from "./coordinatespanel";
|
||||
import { RadiosSummaryPanel } from "./radiossummarypanel";
|
||||
|
||||
export function MiniMapPanel(props: {}) {
|
||||
const [serverStatus, setServerStatus] = useState({} as ServerStatus);
|
||||
@@ -54,47 +56,65 @@ export function MiniMapPanel(props: {}) {
|
||||
className={`
|
||||
absolute right-[10px]
|
||||
${mapOptions.showMinimap ? `bottom-[188px]` : `bottom-[20px]`}
|
||||
flex w-[288px] items-center justify-between
|
||||
${mapOptions.showMinimap ? `rounded-t-lg` : `rounded-lg`}
|
||||
h-12 bg-gray-200 px-3 text-sm backdrop-blur-lg backdrop-grayscale
|
||||
dark:bg-olympus-800/90 dark:text-gray-200
|
||||
flex w-[288px] cursor-pointer flex-col items-center justify-between
|
||||
gap-2 text-sm backdrop-blur-lg
|
||||
`}
|
||||
|
||||
>
|
||||
{!serverStatus.connected ? (
|
||||
<div className={`flex animate-pulse items-center gap-2 font-semibold`}>
|
||||
<div className={`relative h-4 w-4 rounded-full bg-[#F05252]`}></div>
|
||||
Server disconnected
|
||||
</div>
|
||||
) : serverStatus.paused ? (
|
||||
<div className={`flex animate-pulse items-center gap-2 font-semibold`}>
|
||||
<div className={`relative h-4 w-4 rounded-full bg-[#FF9900]`}></div>
|
||||
Server paused
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex gap-2 font-semibold">
|
||||
FPS:
|
||||
<span style={{ color: frameRateColor }} className={`font-semibold`}>
|
||||
{serverStatus.frameRate}
|
||||
</span>
|
||||
<RadiosSummaryPanel />
|
||||
<CoordinatesPanel />
|
||||
<div className={`
|
||||
flex h-12 w-full items-center justify-between gap-2 px-3
|
||||
backdrop-grayscale
|
||||
dark:bg-olympus-800/90 dark:text-gray-200
|
||||
${mapOptions.showMinimap ? `rounded-t-lg` : `rounded-lg`}
|
||||
`}
|
||||
onClick={(ev) => {
|
||||
getApp().getMap().setOption("showMinimap", !mapOptions.showMinimap);
|
||||
}}>
|
||||
{!serverStatus.connected ? (
|
||||
<div className={`flex animate-pulse items-center gap-2 font-semibold`}>
|
||||
<div className={`relative h-4 w-4 rounded-full bg-[#F05252]`}></div>
|
||||
Server disconnected
|
||||
</div>
|
||||
<div className="flex gap-2 font-semibold">
|
||||
Load:
|
||||
<span style={{ color: loadColor }} className={`font-semibold`}>
|
||||
{serverStatus.load}
|
||||
</span>
|
||||
) : serverStatus.paused ? (
|
||||
<div className={`flex animate-pulse items-center gap-2 font-semibold`}>
|
||||
<div className={`relative h-4 w-4 rounded-full bg-[#FF9900]`}></div>
|
||||
Server paused
|
||||
</div>
|
||||
<div className="flex cursor-pointer gap-2 font-semibold" onClick={() => setShowMissionTime(!showMissionTime)}>
|
||||
{showMissionTime ? "MT" : "ET"}: {timeString}
|
||||
</div>
|
||||
<div className={`relative h-4 w-4 rounded-full bg-[#8BFF63]`}></div>
|
||||
</>
|
||||
)}
|
||||
{mapOptions.showMinimap ? (
|
||||
<FaChevronDown className="cursor-pointer" onClick={() => getApp().getMap().setOption("showMinimap", false)} />
|
||||
) : (
|
||||
<FaChevronUp className="cursor-pointer" onClick={() => getApp().getMap().setOption("showMinimap", true)} />
|
||||
)}
|
||||
) : (
|
||||
<>
|
||||
<div className="flex w-16 gap-1 font-semibold">
|
||||
FPS:
|
||||
<span style={{ color: frameRateColor }} className={`font-semibold`}>
|
||||
{serverStatus.frameRate}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex gap-1 font-semibold">
|
||||
Load:
|
||||
<span style={{ color: loadColor }} className={`font-semibold`}>
|
||||
{serverStatus.load}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="ml-auto flex w-24 cursor-pointer gap-2 font-semibold"
|
||||
onClick={(ev) => {
|
||||
setShowMissionTime(!showMissionTime);
|
||||
ev.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{showMissionTime ? "MT" : "ET"}: {timeString}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{mapOptions.showMinimap ? (
|
||||
<FaChevronDown className="cursor-pointer" />
|
||||
) : (
|
||||
<FaChevronUp
|
||||
className={`cursor-pointer`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,15 +9,18 @@ import { BindShortcutRequestEvent, MapOptionsChangedEvent, ShortcutsChangedEvent
|
||||
import { OlAccordion } from "../components/olaccordion";
|
||||
import { Shortcut } from "../../shortcut/shortcut";
|
||||
import { OlSearchBar } from "../components/olsearchbar";
|
||||
import { FaTrash, FaXmark } from "react-icons/fa6";
|
||||
import { FaTrash, FaUserGroup, FaXmark } from "react-icons/fa6";
|
||||
import { OlCoalitionToggle } from "../components/olcoalitiontoggle";
|
||||
import { FaQuestionCircle } from "react-icons/fa";
|
||||
import { FaCog, FaKey, FaPlus, FaQuestionCircle } from "react-icons/fa";
|
||||
import { sha256 } from "js-sha256";
|
||||
import { OlDropdown, OlDropdownItem } from "../components/oldropdown";
|
||||
|
||||
const enum Accordion {
|
||||
NONE,
|
||||
BINDINGS,
|
||||
MAP_OPTIONS,
|
||||
CAMERA_PLUGIN,
|
||||
ADMIN,
|
||||
}
|
||||
|
||||
export function OptionsMenu(props: { open: boolean; onClose: () => void; children?: JSX.Element | JSX.Element[] }) {
|
||||
@@ -25,6 +28,30 @@ export function OptionsMenu(props: { open: boolean; onClose: () => void; childre
|
||||
const [shortcuts, setShortcuts] = useState({} as { [key: string]: Shortcut });
|
||||
const [openAccordion, setOpenAccordion] = useState(Accordion.NONE);
|
||||
const [filterString, setFilterString] = useState("");
|
||||
const [admin, setAdmin] = useState(false);
|
||||
const [password, setPassword] = useState("");
|
||||
|
||||
const checkPassword = (password: string) => {
|
||||
var hash = sha256.create();
|
||||
|
||||
const requestOptions: RequestInit = {
|
||||
method: "GET", // Specify the request method
|
||||
headers: {
|
||||
Authorization: "Basic " + btoa(`Admin:${hash.update(password).hex()}`),
|
||||
}, // Specify the content type
|
||||
};
|
||||
|
||||
fetch(`./admin/config`, requestOptions).then((response) => {
|
||||
if (response.status === 200) {
|
||||
console.log(`Admin password correct`);
|
||||
getApp().setAdminPassword(password);
|
||||
getApp().setState(OlympusState.ADMIN);
|
||||
return response.json();
|
||||
} else {
|
||||
throw new Error("Admin password incorrect");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
MapOptionsChangedEvent.on((mapOptions) => setMapOptions({ ...mapOptions }));
|
||||
@@ -186,18 +213,21 @@ export function OptionsMenu(props: { open: boolean; onClose: () => void; childre
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex content-center gap-4">
|
||||
<OlCoalitionToggle onClick={() => {}} coalition={mapOptions.AWACSCoalition} />
|
||||
<span className="my-auto">Coalition of unit bullseye info</span>
|
||||
</div>
|
||||
<div className="flex gap-1 text-sm text-gray-400">
|
||||
<FaQuestionCircle className={`my-auto w-8`} />{" "}
|
||||
<div
|
||||
className={`my-auto ml-2`}
|
||||
>
|
||||
Change the coalition of the bullseye to use to provide bullseye information in the unit tooltip.
|
||||
<div className="flex content-center gap-4">
|
||||
<OlCoalitionToggle
|
||||
onClick={() => {
|
||||
mapOptions.AWACSCoalition === "blue" && getApp().getMap().setOption("AWACSCoalition", "neutral");
|
||||
mapOptions.AWACSCoalition === "neutral" && getApp().getMap().setOption("AWACSCoalition", "red");
|
||||
mapOptions.AWACSCoalition === "red" && getApp().getMap().setOption("AWACSCoalition", "blue");
|
||||
}}
|
||||
coalition={mapOptions.AWACSCoalition}
|
||||
/>
|
||||
<span className="my-auto">Coalition of unit bullseye info</span>
|
||||
</div>
|
||||
<div className="flex gap-1 text-sm text-gray-400">
|
||||
<FaQuestionCircle className={`my-auto w-8`} />{" "}
|
||||
<div className={`my-auto ml-2`}>Change the coalition of the bullseye to use to provide bullseye information in the unit tooltip.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</OlAccordion>
|
||||
@@ -207,12 +237,6 @@ export function OptionsMenu(props: { open: boolean; onClose: () => void; childre
|
||||
open={openAccordion === Accordion.CAMERA_PLUGIN}
|
||||
title="Camera plugin options"
|
||||
>
|
||||
<hr
|
||||
className={`
|
||||
m-2 my-1 w-auto border-[1px] bg-gray-700
|
||||
dark:border-olympus-500
|
||||
`}
|
||||
></hr>
|
||||
<div
|
||||
className={`
|
||||
flex flex-col content-center items-start justify-between gap-2 p-2
|
||||
@@ -270,38 +294,65 @@ export function OptionsMenu(props: { open: boolean; onClose: () => void; childre
|
||||
</div>
|
||||
</OlAccordion>
|
||||
|
||||
<div className="mt-auto flex">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => getApp().resetProfile()}
|
||||
className={`
|
||||
flex w-full content-center items-center justify-center gap-2
|
||||
rounded-sm border-[1px] bg-blue-700 px-5 py-2.5 text-sm font-medium
|
||||
text-white
|
||||
dark:border-red-600 dark:bg-gray-800 dark:text-gray-400
|
||||
dark:hover:bg-gray-700 dark:focus:ring-blue-800
|
||||
focus:outline-none focus:ring-4 focus:ring-blue-300
|
||||
hover:bg-blue-800
|
||||
`}
|
||||
>
|
||||
Reset all settings
|
||||
<FaXmark />
|
||||
</button>
|
||||
|
||||
<div className="mt-auto flex flex-col gap-2 p-2">
|
||||
<div className="flex content-center justify-between gap-4">
|
||||
<label
|
||||
className={`
|
||||
text-gray-800 text-md my-auto text-nowrap
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
Admin password
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
onChange={(ev) => {
|
||||
setPassword(ev.currentTarget.value);
|
||||
}}
|
||||
className={`
|
||||
max-w-44 rounded-lg border border-gray-300 bg-gray-50 p-2.5
|
||||
text-sm text-gray-900
|
||||
dark:border-gray-600 dark:bg-gray-700 dark:text-white
|
||||
dark:placeholder-gray-400 dark:focus:border-blue-500
|
||||
dark:focus:ring-blue-500
|
||||
focus:border-blue-500 focus:ring-blue-500
|
||||
`}
|
||||
placeholder="Enter password"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => getApp().resetProfile()}
|
||||
onClick={() => checkPassword(password)}
|
||||
className={`
|
||||
mb-2 me-2 flex content-center items-center gap-2 rounded-sm
|
||||
flex content-center items-center justify-center gap-2 rounded-sm
|
||||
border-[1px] bg-blue-700 px-5 py-2.5 text-sm font-medium
|
||||
text-white
|
||||
dark:border-red-600 dark:bg-gray-800 dark:text-gray-400
|
||||
dark:border-white dark:bg-gray-800 dark:text-gray-400
|
||||
dark:hover:bg-gray-700 dark:focus:ring-blue-800
|
||||
focus:outline-none focus:ring-4 focus:ring-blue-300
|
||||
hover:bg-blue-800
|
||||
`}
|
||||
>
|
||||
Reset profile
|
||||
<FaXmark />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => getApp().resetAllProfiles()}
|
||||
className={`
|
||||
mb-2 me-2 flex content-center items-center gap-2 rounded-sm
|
||||
border-[1px] bg-blue-700 px-5 py-2.5 text-sm font-medium
|
||||
text-white
|
||||
dark:border-red-600 dark:bg-red-800 dark:text-gray-400
|
||||
dark:hover:bg-red-700 dark:focus:ring-blue-800
|
||||
focus:outline-none focus:ring-4 focus:ring-blue-300
|
||||
hover:bg-red-800
|
||||
`}
|
||||
>
|
||||
Reset all profiles
|
||||
<FaTrash />
|
||||
Open advanced settings menu
|
||||
<FaCog />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -19,12 +19,10 @@ export function RadiosSummaryPanel(props: {}) {
|
||||
{audioSinks.length > 0 && (
|
||||
<div
|
||||
className={`
|
||||
absolute bottom-[20px] right-[700px] flex w-fit flex-col
|
||||
items-center justify-between gap-2 rounded-lg bg-transparent text-sm
|
||||
text-gray-200
|
||||
flex w-full gap-2 rounded-lg text-sm text-gray-200
|
||||
`}
|
||||
>
|
||||
<div className="flex w-full items-center justify-between gap-2">
|
||||
<div className="flex w-full flex-wrap gap-2">
|
||||
|
||||
{audioSinks.filter((audioSinks) => audioSinks instanceof RadioSink).length > 0 &&
|
||||
audioSinks
|
||||
|
||||
@@ -113,7 +113,6 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children?
|
||||
{...props}
|
||||
title="Spawn menu"
|
||||
showBackButton={blueprint !== null || effect !== null}
|
||||
canBeHidden={true}
|
||||
onBack={() => {
|
||||
getApp().setState(OlympusState.SPAWN);
|
||||
setBlueprint(null);
|
||||
@@ -125,12 +124,19 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children?
|
||||
<h2 className="mb-4 font-bold">Spawn menu</h2>
|
||||
<p>The spawn menu allows you to spawn new units in the current mission.</p>
|
||||
<p>Moreover, it allows you to spawn effects like smokes and explosions.</p>
|
||||
<p className="mt-2">You can use the search bar to quickly find a specific unit. Otherwise, open the category you are interested in, and use the filters to refine your selection. </p>
|
||||
<img src="images/training/unitfilter.png" className={`
|
||||
mx-auto my-4 w-[80%] rounded-lg
|
||||
drop-shadow-[0_0px_7px_rgba(255,255,255,0.07)]
|
||||
`} />
|
||||
<div className="mt-2">Click on a unit to enter the spawn properties menu. The menu is divided into multiple sections:
|
||||
<p className="mt-2">
|
||||
You can use the search bar to quickly find a specific unit. Otherwise, open the category you are interested in, and use the filters to refine your
|
||||
selection.{" "}
|
||||
</p>
|
||||
<img
|
||||
src="images/training/unitfilter.png"
|
||||
className={`
|
||||
mx-auto my-4 w-[80%] rounded-lg
|
||||
drop-shadow-[0_0px_7px_rgba(255,255,255,0.07)]
|
||||
`}
|
||||
/>
|
||||
<div className="mt-2">
|
||||
Click on a unit to enter the spawn properties menu. The menu is divided into multiple sections:
|
||||
<ul className="ml-4 mt-2 list-inside list-decimal">
|
||||
<li>Unit name and short description</li>
|
||||
<li>Quick access name </li>
|
||||
@@ -140,11 +146,18 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children?
|
||||
</div>
|
||||
<p>To get more info on each control, hover your cursor on it.</p>
|
||||
<h2 className="my-4 font-bold">Quick access</h2>
|
||||
<p>If you plan on reusing the same spawn multiple times during the mission, you can "star" the spawn properties. This will allow you to reuse them quickly multiple times. The starred spawn will save all settings, so you can create starred spawn with multiple variations, e.g. loadouts, or skill levels.</p>
|
||||
<img src="images/training/starred.png" className={`
|
||||
mx-auto my-4 w-[80%] rounded-lg
|
||||
drop-shadow-[0_0px_7px_rgba(255,255,255,0.07)]
|
||||
`} />
|
||||
<p>
|
||||
If you plan on reusing the same spawn multiple times during the mission, you can "star" the spawn properties. This will allow you to reuse them
|
||||
quickly multiple times. The starred spawn will save all settings, so you can create starred spawn with multiple variations, e.g. loadouts, or
|
||||
skill levels.
|
||||
</p>
|
||||
<img
|
||||
src="images/training/starred.png"
|
||||
className={`
|
||||
mx-auto my-4 w-[80%] rounded-lg
|
||||
drop-shadow-[0_0px_7px_rgba(255,255,255,0.07)]
|
||||
`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
@@ -203,6 +216,9 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children?
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{filteredBlueprints.filter((blueprint) => blueprint.category === "aircraft").length === 0 && (
|
||||
<span className={`text-gray-400`}>No aircraft available</span>
|
||||
)}
|
||||
</div>
|
||||
</OlAccordion>
|
||||
<OlAccordion
|
||||
@@ -255,6 +271,9 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children?
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{filteredBlueprints.filter((blueprint) => blueprint.category === "helicopter").length === 0 && (
|
||||
<span className={`text-gray-400`}>No helicopter available</span>
|
||||
)}
|
||||
</div>
|
||||
</OlAccordion>
|
||||
<OlAccordion
|
||||
@@ -286,6 +305,9 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children?
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{filteredBlueprints.filter((blueprint) => blueprint.category === "groundunit" && blueprint.type === "SAM Site").length === 0 && (
|
||||
<span className={`text-gray-400`}>No SAM sites available</span>
|
||||
)}
|
||||
</div>
|
||||
</OlAccordion>
|
||||
<OlAccordion
|
||||
@@ -317,6 +339,9 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children?
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{filteredBlueprints.filter((blueprint) => blueprint.canAAA).length === 0 && <span className={`
|
||||
text-gray-400
|
||||
`}>No AAA unit available</span>}
|
||||
</div>
|
||||
</OlAccordion>
|
||||
<OlAccordion
|
||||
@@ -372,6 +397,9 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children?
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{filteredBlueprints.filter((blueprint) => blueprint.category === "groundunit" && blueprint.type !== "SAM Site").length === 0 && (
|
||||
<span className={`text-gray-400`}>No ground unit available</span>
|
||||
)}
|
||||
</div>
|
||||
</OlAccordion>
|
||||
<OlAccordion
|
||||
@@ -424,41 +452,46 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children?
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{filteredBlueprints.filter((blueprint) => blueprint.category === "navyunit").length === 0 && (
|
||||
<span className={`text-gray-400`}>No navy unit available</span>
|
||||
)}
|
||||
</div>
|
||||
</OlAccordion>
|
||||
<OlAccordion
|
||||
title="Effects (smokes, explosions etc)"
|
||||
open={openAccordion == CategoryAccordion.EFFECT}
|
||||
onClick={() => {
|
||||
setOpenAccordion(openAccordion === CategoryAccordion.EFFECT ? CategoryAccordion.NONE : CategoryAccordion.EFFECT);
|
||||
setSelectedRole(null);
|
||||
setSelectedType(null);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={`
|
||||
flex max-h-[450px] flex-col gap-1 overflow-y-scroll
|
||||
no-scrollbar
|
||||
`}
|
||||
{commandModeOptions.commandMode === GAME_MASTER && (
|
||||
<OlAccordion
|
||||
title="Effects (smokes, explosions etc)"
|
||||
open={openAccordion == CategoryAccordion.EFFECT}
|
||||
onClick={() => {
|
||||
setOpenAccordion(openAccordion === CategoryAccordion.EFFECT ? CategoryAccordion.NONE : CategoryAccordion.EFFECT);
|
||||
setSelectedRole(null);
|
||||
setSelectedType(null);
|
||||
}}
|
||||
>
|
||||
<OlEffectListEntry
|
||||
key={"explosion"}
|
||||
icon={faExplosion}
|
||||
label={"Explosion"}
|
||||
onClick={() => {
|
||||
setEffect("explosion");
|
||||
}}
|
||||
/>
|
||||
<OlEffectListEntry
|
||||
key={"smoke"}
|
||||
icon={faSmog}
|
||||
label={"Smoke"}
|
||||
onClick={() => {
|
||||
setEffect("smoke");
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</OlAccordion>
|
||||
<div
|
||||
className={`
|
||||
flex max-h-[450px] flex-col gap-1 overflow-y-scroll
|
||||
no-scrollbar
|
||||
`}
|
||||
>
|
||||
<OlEffectListEntry
|
||||
key={"explosion"}
|
||||
icon={faExplosion}
|
||||
label={"Explosion"}
|
||||
onClick={() => {
|
||||
setEffect("explosion");
|
||||
}}
|
||||
/>
|
||||
<OlEffectListEntry
|
||||
key={"smoke"}
|
||||
icon={faSmog}
|
||||
label={"Smoke"}
|
||||
onClick={() => {
|
||||
setEffect("smoke");
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</OlAccordion>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import { OlCheckbox } from "../components/olcheckbox";
|
||||
import {
|
||||
ROEs,
|
||||
alarmStates,
|
||||
UnitState,
|
||||
altitudeIncrements,
|
||||
emissionsCountermeasures,
|
||||
maxAltitudeValues,
|
||||
@@ -47,7 +48,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";
|
||||
@@ -59,9 +60,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() {
|
||||
@@ -131,6 +133,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);
|
||||
|
||||
@@ -208,6 +221,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 */
|
||||
@@ -230,7 +256,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) => {
|
||||
@@ -270,25 +298,47 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
open={props.open}
|
||||
title={selectedUnits.length > 0 ? `Units selected (x${selectedUnits.length})` : `No units selected`}
|
||||
onClose={props.onClose}
|
||||
canBeHidden={true}
|
||||
autohide={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>
|
||||
|
||||
);
|
||||
}}
|
||||
>
|
||||
<>
|
||||
@@ -368,9 +418,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 (
|
||||
@@ -453,23 +508,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>
|
||||
);
|
||||
})}
|
||||
@@ -743,53 +800,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>
|
||||
@@ -920,31 +1005,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>
|
||||
@@ -995,31 +1092,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>
|
||||
@@ -1237,9 +1346,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
|
||||
@@ -1264,15 +1375,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
|
||||
@@ -1297,15 +1420,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
|
||||
@@ -1384,11 +1519,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={`
|
||||
@@ -1422,6 +1565,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>
|
||||
@@ -1903,14 +2411,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>
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ import { ServerOverlay } from "./serveroverlay";
|
||||
import { ImportExportModal } from "./modals/importexportmodal";
|
||||
import { WarningModal } from "./modals/warningmodal";
|
||||
import { TrainingModal } from "./modals/trainingmodal";
|
||||
import { AdminModal } from "./modals/adminmodal";
|
||||
|
||||
export function UI() {
|
||||
const [appState, setAppState] = useState(OlympusState.NOT_INITIALIZED);
|
||||
@@ -72,9 +73,9 @@ export function UI() {
|
||||
<ProtectionPromptModal open={appState === OlympusState.UNIT_CONTROL && appSubState == UnitControlSubState.PROTECTION} />
|
||||
<KeybindModal open={appState === OlympusState.OPTIONS && appSubState === OptionsSubstate.KEYBIND} />
|
||||
<ImportExportModal open={appState === OlympusState.IMPORT_EXPORT} />
|
||||
<LoginModal open={appState === OlympusState.LOGIN} />
|
||||
<WarningModal open={appState === OlympusState.WARNING} />
|
||||
<TrainingModal open={appState === OlympusState.TRAINING} />
|
||||
<AdminModal open={appState === OlympusState.ADMIN} />
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -109,8 +110,8 @@ export function UI() {
|
||||
|
||||
<MiniMapPanel />
|
||||
<ControlsPanel />
|
||||
<CoordinatesPanel />
|
||||
<RadiosSummaryPanel />
|
||||
|
||||
|
||||
|
||||
<SideBar />
|
||||
<InfoBar />
|
||||
|
||||
@@ -178,6 +178,19 @@ 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;
|
||||
#totalAmmo: number = 0;
|
||||
#previousTotalAmmo: number = 0;
|
||||
|
||||
/* Inputs timers */
|
||||
#debounceTimeout: number | null = null;
|
||||
@@ -336,6 +349,39 @@ export abstract class Unit extends CustomMarker {
|
||||
getAlarmState() {
|
||||
return this.#alarmState;
|
||||
}
|
||||
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;
|
||||
@@ -630,6 +676,8 @@ export abstract class Unit extends CustomMarker {
|
||||
break;
|
||||
case DataIndexes.ammo:
|
||||
this.#ammo = dataExtractor.extractAmmo();
|
||||
this.#previousTotalAmmo = this.#totalAmmo;
|
||||
this.#totalAmmo = this.#ammo.reduce((prev: number, ammo: Ammo) => prev + ammo.quantity, 0);
|
||||
break;
|
||||
case DataIndexes.contacts:
|
||||
this.#contacts = dataExtractor.extractContacts();
|
||||
@@ -664,6 +712,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -770,7 +853,18 @@ export abstract class Unit extends CustomMarker {
|
||||
racetrackLength: this.#racetrackLength,
|
||||
racetrackAnchor: this.#racetrackAnchor,
|
||||
racetrackBearing: this.#racetrackBearing,
|
||||
alarmState: this.#alarmState
|
||||
alarmState: this.#alarmState,
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1347,6 +1441,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);
|
||||
}
|
||||
@@ -1872,21 +1996,29 @@ export abstract class Unit extends CustomMarker {
|
||||
// Iterate over all spots and draw lines, edit markers, and markers
|
||||
Object.values(getApp().getMissionManager().getSpots()).forEach((spot: Spot) => {
|
||||
if (spot.getSourceUnitID() === this.ID) {
|
||||
const spotBearing = deg2rad(bearing(this.getPosition().lat, this.getPosition().lng, spot.getTargetPosition().lat, spot.getTargetPosition().lng, false));
|
||||
const spotDistance = this.getPosition().distanceTo(spot.getTargetPosition());
|
||||
const midPosition = bearingAndDistanceToLatLng(this.getPosition().lat, this.getPosition().lng, spotBearing, spotDistance / 2);
|
||||
if (spot.getActive()) {
|
||||
const spotBearing = deg2rad(
|
||||
bearing(this.getPosition().lat, this.getPosition().lng, spot.getTargetPosition().lat, spot.getTargetPosition().lng, false)
|
||||
);
|
||||
const spotDistance = this.getPosition().distanceTo(spot.getTargetPosition());
|
||||
const midPosition = bearingAndDistanceToLatLng(this.getPosition().lat, this.getPosition().lng, spotBearing, spotDistance / 2);
|
||||
|
||||
// Draw the spot line
|
||||
this.#drawSpotLine(spot, spotBearing);
|
||||
// Draw the spot line
|
||||
this.#drawSpotLine(spot, spotBearing);
|
||||
|
||||
// Draw the spot edit marker if the map is zoomed in enough
|
||||
if (getApp().getMap().getZoom() >= SPOTS_EDIT_ZOOM_TRANSITION) {
|
||||
// Draw the spot edit marker
|
||||
this.#drawSpotEditMarker(spot, midPosition, spotBearing);
|
||||
// Draw the spot edit marker if the map is zoomed in enough
|
||||
if (getApp().getMap().getZoom() >= SPOTS_EDIT_ZOOM_TRANSITION) {
|
||||
// Draw the spot edit marker
|
||||
this.#drawSpotEditMarker(spot, midPosition, spotBearing);
|
||||
}
|
||||
|
||||
// Draw the spot marker
|
||||
this.#drawSpotMarker(spot);
|
||||
} else {
|
||||
this.#spotLines[spot.getID()]?.removeFrom(getApp().getMap());
|
||||
this.#spotEditMarkers[spot.getID()]?.removeFrom(getApp().getMap());
|
||||
this.#spotMarkers[spot.getID()]?.removeFrom(getApp().getMap());
|
||||
}
|
||||
|
||||
// Draw the spot marker
|
||||
this.#drawSpotMarker(spot);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
msToKnots,
|
||||
} from "../other/utils";
|
||||
import { CoalitionPolygon } from "../map/coalitionarea/coalitionpolygon";
|
||||
import { DELETE_CYCLE_TIME, DELETE_SLOW_THRESHOLD, DataIndexes, GAME_MASTER, IADSDensities, OlympusState, UnitControlSubState, alarmStates } from "../constants/constants";
|
||||
import { BLUE_COMMANDER, DELETE_CYCLE_TIME, DELETE_SLOW_THRESHOLD, DataIndexes, GAME_MASTER, IADSDensities, OlympusState, RED_COMMANDER, UnitControlSubState, alarmStates } from "../constants/constants";
|
||||
import { DataExtractor } from "../server/dataextractor";
|
||||
import { citiesDatabase } from "./databases/citiesdatabase";
|
||||
import { TemporaryUnitMarker } from "../map/markers/temporaryunitmarker";
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
import { UnitDatabase } from "./databases/unitdatabase";
|
||||
import * as turf from "@turf/turf";
|
||||
import { PathMarker } from "../map/markers/pathmarker";
|
||||
import { Coalition } from "../types/types";
|
||||
|
||||
/** The UnitsManager handles the creation, update, and control of units. Data is strictly updated by the server ONLY. This means that any interaction from the user will always and only
|
||||
* result in a command to the server, executed by means of a REST PUT request. Any subsequent change in data will be reflected only when the new data is sent back by the server. This strategy allows
|
||||
@@ -548,6 +549,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, []);
|
||||
}
|
||||
@@ -842,16 +844,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) => {
|
||||
@@ -859,7 +869,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;
|
||||
@@ -1229,12 +1281,20 @@ export class UnitsManager {
|
||||
if (units === null) units = this.getSelectedUnits();
|
||||
units = units.filter((unit) => !unit.getHuman());
|
||||
|
||||
// TODO: maybe check units are all of same coalition?
|
||||
|
||||
let callback = (units) => {
|
||||
onExecution();
|
||||
if (this.getUnitsCategories(units).length == 1) {
|
||||
var unitsData: { ID: number; location: LatLng }[] = [];
|
||||
units.forEach((unit: Unit) => unitsData.push({ ID: unit.ID, location: unit.getPosition() }));
|
||||
getApp().getServerManager().cloneUnits(unitsData, true, 0 /* No spawn points, we delete the original units */);
|
||||
|
||||
/* Determine the coalition */
|
||||
let coalition = "all";
|
||||
if (getApp().getMissionManager().getCommandModeOptions().commandMode === BLUE_COMMANDER) coalition = "blue";
|
||||
else if (getApp().getMissionManager().getCommandModeOptions().commandMode === RED_COMMANDER) coalition = "red";
|
||||
|
||||
getApp().getServerManager().cloneUnits(unitsData, true, 0 /* No spawn points, we delete the original units */, coalition as Coalition);
|
||||
this.#showActionMessage(units, `created a group`);
|
||||
} else {
|
||||
getApp().addInfoMessage(`Groups can only be created from units of the same category`);
|
||||
@@ -1441,9 +1501,13 @@ export class UnitsManager {
|
||||
units.push({ ID: unit.ID, location: position });
|
||||
});
|
||||
|
||||
let coalition = "all";
|
||||
if (getApp().getMissionManager().getCommandModeOptions().commandMode === BLUE_COMMANDER) coalition = "blue";
|
||||
else if (getApp().getMissionManager().getCommandModeOptions().commandMode === RED_COMMANDER) coalition = "red";
|
||||
|
||||
getApp()
|
||||
.getServerManager()
|
||||
.cloneUnits(units, false, spawnPoints, (res: any) => {
|
||||
.cloneUnits(units, false, getApp().getMissionManager().getCommandModeOptions().commandMode === GAME_MASTER? 0: spawnPoints, coalition as Coalition, (res: any) => {
|
||||
if (res !== undefined) {
|
||||
markers.forEach((marker: TemporaryUnitMarker) => {
|
||||
marker.setCommandHash(res);
|
||||
@@ -1627,7 +1691,7 @@ export class UnitsManager {
|
||||
getApp().addInfoMessage("Aircrafts can be air spawned during the SETUP phase only");
|
||||
return false;
|
||||
}
|
||||
spawnPoints = units.reduce((points: number, unit: UnitSpawnTable) => {
|
||||
spawnPoints = getApp().getMissionManager().getCommandModeOptions().commandMode === GAME_MASTER? 0: units.reduce((points: number, unit: UnitSpawnTable) => {
|
||||
return points + this.getDatabase().getSpawnPointsByName(unit.unitType);
|
||||
}, 0);
|
||||
spawnFunction = () => getApp().getServerManager().spawnAircrafts(units, coalition, airbase, country, immediate, spawnPoints, callback);
|
||||
@@ -1636,7 +1700,7 @@ export class UnitsManager {
|
||||
getApp().addInfoMessage("Helicopters can be air spawned during the SETUP phase only");
|
||||
return false;
|
||||
}
|
||||
spawnPoints = units.reduce((points: number, unit: UnitSpawnTable) => {
|
||||
spawnPoints = getApp().getMissionManager().getCommandModeOptions().commandMode === GAME_MASTER? 0: units.reduce((points: number, unit: UnitSpawnTable) => {
|
||||
return points + this.getDatabase().getSpawnPointsByName(unit.unitType);
|
||||
}, 0);
|
||||
spawnFunction = () => getApp().getServerManager().spawnHelicopters(units, coalition, airbase, country, immediate, spawnPoints, callback);
|
||||
@@ -1645,7 +1709,7 @@ export class UnitsManager {
|
||||
getApp().addInfoMessage("Ground units can be spawned during the SETUP phase only");
|
||||
return false;
|
||||
}
|
||||
spawnPoints = units.reduce((points: number, unit: UnitSpawnTable) => {
|
||||
spawnPoints = getApp().getMissionManager().getCommandModeOptions().commandMode === GAME_MASTER? 0: units.reduce((points: number, unit: UnitSpawnTable) => {
|
||||
return points + this.getDatabase().getSpawnPointsByName(unit.unitType);
|
||||
}, 0);
|
||||
spawnFunction = () => getApp().getServerManager().spawnGroundUnits(units, coalition, country, immediate, spawnPoints, callback);
|
||||
@@ -1654,7 +1718,7 @@ export class UnitsManager {
|
||||
getApp().addInfoMessage("Navy units can be spawned during the SETUP phase only");
|
||||
return false;
|
||||
}
|
||||
spawnPoints = units.reduce((points: number, unit: UnitSpawnTable) => {
|
||||
spawnPoints = getApp().getMissionManager().getCommandModeOptions().commandMode === GAME_MASTER? 0: units.reduce((points: number, unit: UnitSpawnTable) => {
|
||||
return points + this.getDatabase().getSpawnPointsByName(unit.unitType);
|
||||
}, 0);
|
||||
spawnFunction = () => getApp().getServerManager().spawnNavyUnits(units, coalition, country, immediate, spawnPoints, callback);
|
||||
|
||||
@@ -20,7 +20,9 @@ export abstract class Weapon extends CustomMarker {
|
||||
|
||||
#hidden: boolean = false;
|
||||
#detectionMethods: number[] = [];
|
||||
|
||||
#speedVector: number[] = [];
|
||||
#altitude: number[] = [];
|
||||
|
||||
getAlive() {
|
||||
return this.#alive;
|
||||
}
|
||||
@@ -43,6 +45,7 @@ export abstract class Weapon extends CustomMarker {
|
||||
static getConstructor(type: string) {
|
||||
if (type === "Missile") return Missile;
|
||||
if (type === "Bomb") return Bomb;
|
||||
if (type === "Shell") return Shell;
|
||||
}
|
||||
|
||||
constructor(ID: number) {
|
||||
@@ -85,6 +88,7 @@ export abstract class Weapon extends CustomMarker {
|
||||
break;
|
||||
case DataIndexes.speed:
|
||||
this.#speed = dataExtractor.extractFloat64();
|
||||
this.#speedVector.push(this.#speed);
|
||||
updateMarker = true;
|
||||
break;
|
||||
case DataIndexes.heading:
|
||||
@@ -111,7 +115,13 @@ export abstract class Weapon extends CustomMarker {
|
||||
}
|
||||
|
||||
setAlive(newAlive: boolean) {
|
||||
if (this.#alive && !newAlive) {
|
||||
getApp().getMap().addFlakMarker(this.getLatLng());
|
||||
}
|
||||
this.#alive = newAlive;
|
||||
if (this.#speedVector.length > 0 && newAlive === false) {
|
||||
let asd = 1;
|
||||
}
|
||||
}
|
||||
|
||||
belongsToCommandedCoalition() {
|
||||
@@ -330,3 +340,40 @@ export class Bomb extends Weapon {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class Shell extends Weapon {
|
||||
constructor(ID: number) {
|
||||
super(ID);
|
||||
}
|
||||
|
||||
getCategory() {
|
||||
return "Shell";
|
||||
}
|
||||
|
||||
getMarkerCategory() {
|
||||
if (this.belongsToCommandedCoalition() || this.getDetectionMethods().includes(VISUAL) || this.getDetectionMethods().includes(OPTIC)) return "shell";
|
||||
else return "aircraft";
|
||||
}
|
||||
|
||||
getIconOptions() {
|
||||
return {
|
||||
showState: false,
|
||||
showVvi:
|
||||
!this.belongsToCommandedCoalition() &&
|
||||
!this.getDetectionMethods().some((value) => [VISUAL, OPTIC].includes(value)) &&
|
||||
this.getDetectionMethods().some((value) => [RADAR, IRST, DLINK].includes(value)),
|
||||
showHealth: false,
|
||||
showHotgroup: false,
|
||||
showUnitIcon: this.belongsToCommandedCoalition() || this.getDetectionMethods().some((value) => [VISUAL, OPTIC, RADAR, IRST, DLINK].includes(value)),
|
||||
showShortLabel: false,
|
||||
showFuel: false,
|
||||
showAmmo: false,
|
||||
showSummary:
|
||||
!this.belongsToCommandedCoalition() &&
|
||||
!this.getDetectionMethods().some((value) => [VISUAL, OPTIC].includes(value)) &&
|
||||
this.getDetectionMethods().some((value) => [RADAR, IRST, DLINK].includes(value)),
|
||||
showCallsign: false,
|
||||
rotateToHeading: this.belongsToCommandedCoalition() || this.getDetectionMethods().includes(VISUAL) || this.getDetectionMethods().includes(OPTIC),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ export class WeaponsManager {
|
||||
/** Add a new weapon to the manager
|
||||
*
|
||||
* @param ID ID of the new weapon
|
||||
* @param category Either "Missile" or "Bomb". Determines what class will be used to create the new unit accordingly.
|
||||
* @param category Either "Missile", "Bomb" or "Shell". Determines what class will be used to create the new unit accordingly.
|
||||
*/
|
||||
addWeapon(ID: number, category: string) {
|
||||
if (category) {
|
||||
|
||||
@@ -4,21 +4,22 @@
|
||||
"version": "{{OLYMPUS_VERSION_NUMBER}}",
|
||||
"scripts": {
|
||||
"build-release": "call ./scripts/build-release.bat",
|
||||
"server": "electron . --server",
|
||||
"server-electron": "electron . --server",
|
||||
"server": "node ./build/www.js",
|
||||
"client": "electron .",
|
||||
"tsc": "tsc"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@google-cloud/speech": "^6.7.0",
|
||||
"@google-cloud/text-to-speech": "^5.6.0",
|
||||
"@google-cloud/speech": "^6.7.1",
|
||||
"@google-cloud/text-to-speech": "^5.8.1",
|
||||
"appjs": "^0.0.20",
|
||||
"appjs-win32": "^0.0.19",
|
||||
"body-parser": "^1.20.3",
|
||||
"cors": "^2.8.5",
|
||||
"debug": "~4.4.0",
|
||||
"ejs": "^3.1.10",
|
||||
"electron": "^33.2.1",
|
||||
"electron": "^33.4.5",
|
||||
"express": "^4.21.2",
|
||||
"express-basic-auth": "^1.2.1",
|
||||
"http-proxy-middleware": "^3.0.3",
|
||||
@@ -30,14 +31,14 @@
|
||||
"sha256": "^0.2.0",
|
||||
"srtm-elevation": "^2.1.2",
|
||||
"tcp-ping-port": "^1.0.2",
|
||||
"uuid": "^11.0.3",
|
||||
"whatwg-url": "^14.1.0",
|
||||
"ws": "^8.18.0",
|
||||
"uuid": "^11.1.0",
|
||||
"whatwg-url": "^14.2.0",
|
||||
"ws": "^8.18.1",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.7.2"
|
||||
"typescript": "^5.8.2"
|
||||
},
|
||||
"overrides": {
|
||||
"node-fetch": "^2.7.0"
|
||||
|
||||
@@ -51,6 +51,7 @@ module.exports = function (configLocation, viteProxy) {
|
||||
/* Config specific routers */
|
||||
const elevationRouter = require("./routes/api/elevation")(configLocation);
|
||||
const resourcesRouter = require("./routes/resources")(configLocation);
|
||||
const adminRouter = require("./routes/admin")(configLocation);
|
||||
|
||||
/* Default routers */
|
||||
const airbasesRouter = require("./routes/api/airbases");
|
||||
@@ -113,6 +114,9 @@ module.exports = function (configLocation, viteProxy) {
|
||||
"Blue commander": config["authentication"]["blueCommanderPassword"],
|
||||
"Red commander": config["authentication"]["redCommanderPassword"],
|
||||
};
|
||||
if (config["authentication"]["adminPassword"]) {
|
||||
defaultUsers["Admin"] = config["authentication"]["adminPassword"];
|
||||
}
|
||||
let users = {};
|
||||
Object.keys(usersConfig).forEach(
|
||||
(user) => (users[user] = usersConfig[user].password)
|
||||
@@ -122,7 +126,13 @@ module.exports = function (configLocation, viteProxy) {
|
||||
});
|
||||
|
||||
/* Define middleware */
|
||||
app.use(logger("dev"));
|
||||
app.use(
|
||||
logger("dev", {
|
||||
skip: function (req, res) {
|
||||
return res.statusCode < 400;
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
/* Authorization middleware */
|
||||
if (
|
||||
@@ -160,7 +170,7 @@ module.exports = function (configLocation, viteProxy) {
|
||||
app.use("/olympus", async (req, res, next) => {
|
||||
/* Check if custom authorization headers are being used */
|
||||
const user =
|
||||
//@ts-ignore
|
||||
//@ts-ignore
|
||||
req.auth?.user ??
|
||||
checkCustomHeaders(config, usersConfig, groupsConfig, req);
|
||||
|
||||
@@ -215,16 +225,27 @@ module.exports = function (configLocation, viteProxy) {
|
||||
});
|
||||
|
||||
/* Proxy middleware */
|
||||
app.use(
|
||||
"/olympus",
|
||||
httpProxyMiddleware.createProxyMiddleware({
|
||||
target: `http://${
|
||||
backendAddress === "*" ? "localhost" : backendAddress
|
||||
}:${config["backend"]["port"]}/olympus`,
|
||||
changeOrigin: true,
|
||||
})
|
||||
);
|
||||
|
||||
if (config["backend"]["port"]) {
|
||||
app.use(
|
||||
"/olympus",
|
||||
httpProxyMiddleware.createProxyMiddleware({
|
||||
target: `http://${
|
||||
backendAddress === "*" ? "localhost" : backendAddress
|
||||
}:${config["backend"]["port"]}/olympus`,
|
||||
changeOrigin: true,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
app.use(
|
||||
"/olympus",
|
||||
httpProxyMiddleware.createProxyMiddleware({
|
||||
target: `https://${
|
||||
backendAddress === "*" ? "localhost" : backendAddress
|
||||
}/olympus`,
|
||||
changeOrigin: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
app.use(bodyParser.json({ limit: "50mb" }));
|
||||
app.use(bodyParser.urlencoded({ limit: "50mb", extended: true }));
|
||||
@@ -238,6 +259,9 @@ module.exports = function (configLocation, viteProxy) {
|
||||
app.use("/api/speech", speechRouter);
|
||||
app.use("/resources", resourcesRouter);
|
||||
|
||||
app.use("/admin", auth);
|
||||
app.use("/admin", adminRouter);
|
||||
|
||||
/* Set default index */
|
||||
if (viteProxy) {
|
||||
app.use(
|
||||
|
||||
115
frontend/server/src/routes/admin.ts
Normal file
115
frontend/server/src/routes/admin.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import express = require("express");
|
||||
import fs = require("fs");
|
||||
import path = require("path");
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
module.exports = function (configLocation) {
|
||||
router.get("/config", function (req, res, next) {
|
||||
if (req.auth?.user === "Admin") {
|
||||
/* Read the users configuration file */
|
||||
let usersConfig = {};
|
||||
if (
|
||||
fs.existsSync(
|
||||
path.join(path.dirname(configLocation), "olympusUsers.json")
|
||||
)
|
||||
) {
|
||||
let rawdata = fs.readFileSync(
|
||||
path.join(path.dirname(configLocation), "olympusUsers.json"),
|
||||
{ encoding: "utf-8" }
|
||||
);
|
||||
usersConfig = JSON.parse(rawdata);
|
||||
}
|
||||
|
||||
/* Read the groups configuration file */
|
||||
let groupsConfig = {};
|
||||
if (
|
||||
fs.existsSync(
|
||||
path.join(path.dirname(configLocation), "olympusGroups.json")
|
||||
)
|
||||
) {
|
||||
let rawdata = fs.readFileSync(
|
||||
path.join(path.dirname(configLocation), "olympusGroups.json"),
|
||||
{ encoding: "utf-8" }
|
||||
);
|
||||
groupsConfig = JSON.parse(rawdata);
|
||||
}
|
||||
|
||||
res.send({ users: usersConfig, groups: groupsConfig });
|
||||
res.end();
|
||||
} else {
|
||||
res.sendStatus(401);
|
||||
}
|
||||
});
|
||||
|
||||
router.put("/config", function (req, res, next) {
|
||||
if (req.auth?.user === "Admin") {
|
||||
/* Create a backup folder for the configuration files */
|
||||
let backupFolder = path.join(
|
||||
path.dirname(configLocation),
|
||||
"Olympus Configs Backup"
|
||||
);
|
||||
if (!fs.existsSync(backupFolder)) {
|
||||
fs.mkdirSync(backupFolder);
|
||||
}
|
||||
|
||||
/* Make a backup of the existing files */
|
||||
let timestamp = new Date().toISOString().replace(/:/g, "-");
|
||||
if (
|
||||
fs.existsSync(
|
||||
path.join(path.dirname(configLocation), "olympusUsers.json")
|
||||
)
|
||||
) {
|
||||
fs.copyFileSync(
|
||||
path.join(path.dirname(configLocation), "olympusUsers.json"),
|
||||
path.join(
|
||||
path.dirname(configLocation),
|
||||
"Olympus Configs Backup",
|
||||
"olympusUsers.json." + timestamp
|
||||
)
|
||||
);
|
||||
}
|
||||
if (
|
||||
fs.existsSync(
|
||||
path.join(path.dirname(configLocation), "olympusGroups.json")
|
||||
)
|
||||
) {
|
||||
fs.copyFileSync(
|
||||
path.join(path.dirname(configLocation), "olympusGroups.json"),
|
||||
path.join(
|
||||
path.dirname(configLocation),
|
||||
"Olympus Configs Backup",
|
||||
"olympusGroups.json." + timestamp
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/* Save the users configuration file */
|
||||
let usersConfig = req.body.users;
|
||||
|
||||
if (usersConfig) {
|
||||
fs.writeFileSync(
|
||||
path.join(path.dirname(configLocation), "olympusUsers.json"),
|
||||
JSON.stringify(usersConfig, null, 2)
|
||||
);
|
||||
}
|
||||
|
||||
/* Save the groups configuration file */
|
||||
let groupsConfig = req.body.groups;
|
||||
|
||||
if (groupsConfig) {
|
||||
fs.writeFileSync(
|
||||
path.join(path.dirname(configLocation), "olympusGroups.json"),
|
||||
JSON.stringify(groupsConfig, null, 2)
|
||||
);
|
||||
}
|
||||
|
||||
res.send({ users: usersConfig, groups: groupsConfig });
|
||||
res.end();
|
||||
} else {
|
||||
res.sendStatus(401);
|
||||
}
|
||||
});
|
||||
|
||||
return router;
|
||||
};
|
||||
@@ -22,9 +22,15 @@ module.exports = function (configLocation) {
|
||||
profiles = JSON.parse(rawdata);
|
||||
}
|
||||
if (fs.existsSync(configLocation)) {
|
||||
/* Read the config file */
|
||||
let rawdata = fs.readFileSync(configLocation, "utf-8");
|
||||
const local = ["127.0.0.1", "::ffff:127.0.0.1", "::1"].includes(req.connection.remoteAddress);
|
||||
const config = JSON.parse(rawdata);
|
||||
|
||||
/* Check if the connection is local */
|
||||
let local = false;
|
||||
if (config.frontend.autoconnectWhenLocal)
|
||||
local = req.headers[config.frontend.proxyHeader] === undefined;
|
||||
|
||||
let resConfig = {
|
||||
frontend: { ...config.frontend },
|
||||
audio: { ...(config.audio ?? {}) },
|
||||
@@ -32,9 +38,11 @@ module.exports = function (configLocation) {
|
||||
profiles: { ...(profiles ?? {}) },
|
||||
local: local,
|
||||
};
|
||||
|
||||
if (local) {
|
||||
resConfig["authentication"] = config["authentication"]
|
||||
}
|
||||
|
||||
res.send(
|
||||
JSON.stringify(resConfig)
|
||||
);
|
||||
|
||||
@@ -44,6 +44,16 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group srs-port port-input">
|
||||
<span>SRS port
|
||||
<img src="./icons/circle-info-solid.svg"
|
||||
title="This port is used by Olympus to communicate with SRS server. Unless you run multiple servers, the default value should be ok, otherwise edit it to be equal to your SRS server port.">
|
||||
</span>
|
||||
<div>
|
||||
<input type="number" min="1024" max="65535" value="<%= activeInstance["SRSPort"] %>"
|
||||
onchange="signal('onSRSPortChanged', this.value)">
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group backend-address">
|
||||
<span onclick="signal('onEnableAPIClicked')">
|
||||
<div class="checkbox"></div> Enable direct backend API connection
|
||||
|
||||
@@ -19,19 +19,25 @@
|
||||
<span>Game Master Password<img src="./icons/circle-info-solid.svg"
|
||||
title="This password is used to access Olympus as Game Master with full privileges.">
|
||||
</span>
|
||||
<input type="password" minlength="8" onchange="signal('onGameMasterPasswordChanged', this.value)" placeholder="<%= !activeInstance["installed"] || activeInstance["gameMasterPasswordEdited"]? '': 'Keep old password'%>">
|
||||
<input type="password" class="unique" minlength="8" onchange="signal('onGameMasterPasswordChanged', this.value)" placeholder="<%= !activeInstance["installed"] || activeInstance["gameMasterPasswordEdited"]? '': 'Keep old password'%>">
|
||||
</div>
|
||||
<div class="input-group blue-commander">
|
||||
<span>Blue Commander Password<img src="./icons/circle-info-solid.svg"
|
||||
title="This password is used to access Olympus as blue coalition Commander.">
|
||||
</span>
|
||||
<input type="password" minlength="8" onchange="signal('onBlueCommanderPasswordChanged', this.value)" placeholder="<%= !activeInstance["installed"] || activeInstance["blueCommanderPasswordEdited"]? '': 'Keep old password'%>">
|
||||
<input type="password" class="unique" minlength="8" onchange="signal('onBlueCommanderPasswordChanged', this.value)" placeholder="<%= !activeInstance["installed"] || activeInstance["blueCommanderPasswordEdited"]? '': 'Keep old password'%>">
|
||||
</div>
|
||||
<div class="input-group red-commander">
|
||||
<span>Red Commander Password<img src="./icons/circle-info-solid.svg"
|
||||
title="This password is used to access Olympus as red coalition Commander.">
|
||||
</span>
|
||||
<input type="password" minlength="8" onchange="signal('onRedCommanderPasswordChanged', this.value)" placeholder="<%= !activeInstance["installed"] || activeInstance["redCommanderPasswordEdited"]? '': 'Keep old password'%>">
|
||||
<input type="password" class="unique" minlength="8" onchange="signal('onRedCommanderPasswordChanged', this.value)" placeholder="<%= !activeInstance["installed"] || activeInstance["redCommanderPasswordEdited"]? '': 'Keep old password'%>">
|
||||
</div>
|
||||
<div class="input-group admin-password">
|
||||
<span>Admin Password<img src="./icons/circle-info-solid.svg"
|
||||
title="This password is used to set global Olympus configurations, like user access privileges.">
|
||||
</span>
|
||||
<input type="password" minlength="8" onchange="signal('onAdminPasswordChanged', this.value)" placeholder="<%= !activeInstance["installed"] || activeInstance["adminPasswordEdited"]? '': 'Keep old password'%>">
|
||||
</div>
|
||||
<div class="<%= activeInstance["installed"]? '': 'hide' %>" style="color: var(--offwhite); font-size: var(--normal); color: var(--lightgray);">
|
||||
Note: to keep the old passwords, click <b>Next</b> without editing any value.
|
||||
@@ -66,6 +72,16 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group srs-port port-input">
|
||||
<span>SRS port
|
||||
<img src="./icons/circle-info-solid.svg"
|
||||
title="This port is used by Olympus to communicate with SRS server. Unless you run multiple servers, the default value should be ok, otherwise edit it to be equal to your SRS server port.">
|
||||
</span>
|
||||
<div>
|
||||
<input type="number" min="1024" max="65535" value="<%= activeInstance["SRSPort"] %>"
|
||||
onchange="signal('onSRSPortChanged', this.value)">
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group backend-address">
|
||||
<span onclick="signal('onEnableAPIClicked')">
|
||||
<div class="checkbox"></div> Enable direct backend API connection
|
||||
@@ -80,6 +96,13 @@
|
||||
title="Install the camera control plugin, which allows direct control of the DCS camera from Olympus. It is necessary even to control the camera even if Olympus is being used remotely using a browser.">
|
||||
</span>
|
||||
</div>
|
||||
<div class="input-group autoconnect">
|
||||
<span onclick="signal('onEnableAutoconnectClicked')">
|
||||
<div class="checkbox"></div> Autoconnect when local
|
||||
<img src="./icons/circle-info-solid.svg"
|
||||
title="Autoconnect as Game Master when running Olympus on the same computer as DCS.">
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -92,6 +92,10 @@
|
||||
<div>Backend address</div>
|
||||
<div> <%= instances[i].installed? instances[i].backendAddress: "N/A" %> </div>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div>SRS port</div>
|
||||
<div> <%= instances[i].installed? instances[i].SRSPort: "N/A" %> </div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="instance-buttons">
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
<style>
|
||||
.wizard-page #passwords-page .wizard-inputs {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.wizard-page #passwords-page .wizard-inputs>div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 10px;
|
||||
}
|
||||
|
||||
.wizard-page #passwords-page .wizard-inputs {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
</style>
|
||||
<div id="passwords-page">
|
||||
<div class="instructions">
|
||||
@@ -15,26 +29,43 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="wizard-inputs">
|
||||
<div>
|
||||
<div class="input-group game-master">
|
||||
<span>Game Master Password<img src="./icons/circle-info-solid.svg"
|
||||
title="This password is used to access Olympus as Game Master with full privileges.">
|
||||
</span>
|
||||
<input type="password" minlength="8" onchange="signal('onGameMasterPasswordChanged', this.value)" placeholder="<%= state === 'INSTALL' || activeInstance["gameMasterPasswordEdited"]? '': 'Keep old password'%>">
|
||||
<input type="password" class="unique" minlength="8" onchange="signal('onGameMasterPasswordChanged', this.value)" placeholder="<%= state === 'INSTALL' || activeInstance["gameMasterPasswordEdited"]? '': 'Keep old password'%>">
|
||||
</div>
|
||||
<div class="input-group blue-commander">
|
||||
<span>Blue Commander Password<img src="./icons/circle-info-solid.svg"
|
||||
title="This password is used to access Olympus as blue coalition Commander.">
|
||||
</span>
|
||||
<input type="password" minlength="8" onchange="signal('onBlueCommanderPasswordChanged', this.value)" placeholder="<%= state === 'INSTALL' || activeInstance["blueCommanderPasswordEdited"]? '': 'Keep old password'%>">
|
||||
<input type="password" class="unique" minlength="8" onchange="signal('onBlueCommanderPasswordChanged', this.value)" placeholder="<%= state === 'INSTALL' || activeInstance["blueCommanderPasswordEdited"]? '': 'Keep old password'%>">
|
||||
</div>
|
||||
<div class="input-group red-commander">
|
||||
<span>Red Commander Password<img src="./icons/circle-info-solid.svg"
|
||||
title="This password is used to access Olympus as red coalition Commander.">
|
||||
</span>
|
||||
<input type="password" minlength="8" onchange="signal('onRedCommanderPasswordChanged', this.value)" placeholder="<%= state === 'INSTALL' || activeInstance["redCommanderPasswordEdited"]? '': 'Keep old password'%>">
|
||||
<input type="password" class="unique" minlength="8" onchange="signal('onRedCommanderPasswordChanged', this.value)" placeholder="<%= state === 'INSTALL' || activeInstance["redCommanderPasswordEdited"]? '': 'Keep old password'%>">
|
||||
</div>
|
||||
<div class="<%= state !== 'INSTALL'? '': 'hide' %>" style="color: var(--offwhite); font-size: var(--normal); color: var(--lightgray);">
|
||||
Note: to keep the old passwords, click <b>Next</b> without editing any value.
|
||||
</div>
|
||||
<div class="input-group autoconnect">
|
||||
<span onclick="signal('onEnableAutoconnectClicked')">
|
||||
<div class="checkbox <%= activeInstance['installationType'] === 'multiplayer'? '': 'checked' %>"></div> Autoconnect when local
|
||||
<img src="./icons/circle-info-solid.svg"
|
||||
title="Autoconnect as Game Master when running Olympus on the same computer as DCS.">
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="input-group admin-password <%= activeInstance['installationType'] === 'multiplayer'? '': 'hide' %>">
|
||||
<span>Admin Password<img src="./icons/circle-info-solid.svg"
|
||||
title="This password is used to set global Olympus configurations, like user access privileges.">
|
||||
</span>
|
||||
<input type="password" minlength="8" onchange="signal('onAdminPasswordChanged', this.value)" placeholder="<%= state === 'INSTALL' || activeInstance["adminPasswordEdited"]? '': 'Keep old password'%>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -69,6 +69,10 @@
|
||||
<div>Backend address</div>
|
||||
<div> <%= instances[i].installed? instances[i].backendAddress: "N/A" %> </div>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div>SRS port</div>
|
||||
<div> <%= instances[i].installed? instances[i].SRSPort: "N/A" %> </div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="logs-link" onclick="
|
||||
signal('onTextFileClicked', '<%= instances[i].folder.replaceAll('\\', '/')+'/Logs/dcs.log' %>');
|
||||
|
||||
@@ -136,6 +136,7 @@ class DCSInstance {
|
||||
blueCommanderPassword = "";
|
||||
redCommanderPassword = "";
|
||||
gameMasterPasswordHash = "";
|
||||
adminPassword = "";
|
||||
installed = false;
|
||||
error = false;
|
||||
webserverOnline = false;
|
||||
@@ -149,6 +150,9 @@ class DCSInstance {
|
||||
gameMasterPasswordEdited = false;
|
||||
blueCommanderPasswordEdited = false;
|
||||
redCommanderPasswordEdited = false;
|
||||
adminPasswordEdited = false;
|
||||
autoconnectWhenLocal = false;
|
||||
SRSPort = 5002;
|
||||
|
||||
constructor(folder) {
|
||||
this.folder = folder;
|
||||
@@ -185,9 +189,16 @@ class DCSInstance {
|
||||
this.backendAddress = config["backend"]["address"];
|
||||
this.gameMasterPasswordHash = config["authentication"]["gameMasterPassword"];
|
||||
|
||||
/* Read the new configurations added in v2.0.0 */
|
||||
if (config["frontend"]["autoconnectWhenLocal"] !== undefined)
|
||||
this.autoconnectWhenLocal = config["frontend"]["autoconnectWhenLocal"];
|
||||
if (config["frontend"]["audio"] !== undefined && config["frontend"]["audio"]["SRSPort"] !== undefined)
|
||||
this.SRSPort = config["audio"]["SRSPort"];
|
||||
|
||||
this.gameMasterPasswordEdited = false;
|
||||
this.blueCommanderPasswordEdited = false;
|
||||
this.redCommanderPasswordEdited = false;
|
||||
this.adminPasswordEdited = false;
|
||||
|
||||
} catch (err) {
|
||||
showErrorPopup(`<div class='main-message'>A critical error has occurred while reading your Olympus configuration file. </div><div class='sub-message'> Please manually reinstall Olympus in ${this.folder} using either the installation Wizard or the Expert view. </div>`)
|
||||
@@ -269,7 +280,7 @@ class DCSInstance {
|
||||
|
||||
/** Set Blue Commander password
|
||||
*
|
||||
* @param {String} newAddress The new Blue Commander password to set
|
||||
* @param {String} newPassword The new Blue Commander password to set
|
||||
*/
|
||||
setBlueCommanderPassword(newPassword) {
|
||||
this.blueCommanderPassword = newPassword;
|
||||
@@ -278,13 +289,22 @@ class DCSInstance {
|
||||
|
||||
/** Set Red Commander password
|
||||
*
|
||||
* @param {String} newAddress The new Red Commander password to set
|
||||
* @param {String} newPassword The new Red Commander password to set
|
||||
*/
|
||||
setRedCommanderPassword(newPassword) {
|
||||
this.redCommanderPassword = newPassword;
|
||||
this.redCommanderPasswordEdited = true;
|
||||
}
|
||||
|
||||
/** Set Admin password
|
||||
*
|
||||
* @param {String} newPassword The new Admin password to set
|
||||
*/
|
||||
setAdminPassword(newPassword) {
|
||||
this.adminPassword = newPassword;
|
||||
this.adminPasswordEdited = true;
|
||||
}
|
||||
|
||||
/** Checks if any password has been edited by the user
|
||||
*
|
||||
* @returns true if any password was edited
|
||||
@@ -298,7 +318,10 @@ class DCSInstance {
|
||||
* @returns true if all the password have been set
|
||||
*/
|
||||
arePasswordsSet() {
|
||||
return !(getManager().getActiveInstance().gameMasterPassword === '' || getManager().getActiveInstance().blueCommanderPassword === '' || getManager().getActiveInstance().redCommanderPassword === '');
|
||||
if (getManager().getActiveInstance().installationType === "singleplayer")
|
||||
return !(getManager().getActiveInstance().gameMasterPassword === '' || getManager().getActiveInstance().blueCommanderPassword === '' || getManager().getActiveInstance().redCommanderPassword === '');
|
||||
else
|
||||
return !(getManager().getActiveInstance().gameMasterPassword === '' || getManager().getActiveInstance().blueCommanderPassword === '' || getManager().getActiveInstance().redCommanderPassword === '' || getManager().getActiveInstance().adminPassword === '');
|
||||
}
|
||||
|
||||
/** Checks if all the passwords are different
|
||||
|
||||
@@ -66,10 +66,13 @@ 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");
|
||||
await fsp.cp(path.join(__dirname, "..", "..", "..", "DCS Olympus backups", name, "databases"), path.join(folder, "Mods", "Services", "Olympus", "databases"), { recursive: true });
|
||||
|
||||
// Changed in v2.0.0, only the mods database is copied over, if present
|
||||
//await fsp.cp(path.join(__dirname, "..", "..", "..", "DCS Olympus backups", name, "databases"), path.join(folder, "Mods", "Services", "Olympus", "databases"), { recursive: true });
|
||||
await fsp.cp(path.join(__dirname, "..", "..", "..", "DCS Olympus backups", name, "databases", "units", "mods.json"), path.join(folder, "Mods", "Services", "Olympus", "databases", "units", "mods.json"));
|
||||
}
|
||||
|
||||
if (exists(path.join(__dirname, "..", "..", "..", "DCS Olympus backups", name, "scripts", "mods.lua"))) {
|
||||
@@ -161,9 +164,14 @@ async function applyConfiguration(folder, instance) {
|
||||
|
||||
/* Apply the configuration */
|
||||
config["frontend"]["port"] = instance.frontendPort;
|
||||
config["frontend"]["autoconnectWhenLocal"] = instance.autoconnectWhenLocal;
|
||||
config["backend"]["port"] = instance.backendPort;
|
||||
config["backend"]["address"] = instance.backendAddress;
|
||||
|
||||
if (config["audio"] === undefined)
|
||||
config["audio"] = {};
|
||||
config["audio"]["SRSPort"] = instance.SRSPort;
|
||||
|
||||
if (instance.gameMasterPassword !== "")
|
||||
config["authentication"]["gameMasterPassword"] = sha256(instance.gameMasterPassword);
|
||||
|
||||
@@ -173,6 +181,9 @@ async function applyConfiguration(folder, instance) {
|
||||
if (instance.redCommanderPassword !== "")
|
||||
config["authentication"]["redCommanderPassword"] = sha256(instance.redCommanderPassword);
|
||||
|
||||
if (instance.adminPassword !== "")
|
||||
config["authentication"]["adminPassword"] = sha256(instance.adminPassword);
|
||||
|
||||
await fsp.writeFile(path.join(folder, "Config", "olympus.json"), JSON.stringify(config, null, 4));
|
||||
logger.log(`Config succesfully applied in ${folder}`)
|
||||
} else {
|
||||
|
||||
@@ -159,6 +159,7 @@ class Manager {
|
||||
if (this.getActiveInstance()) {
|
||||
this.setPort('frontend', this.getActiveInstance().frontendPort);
|
||||
this.setPort('backend', this.getActiveInstance().backendPort);
|
||||
this.expertSettingsPage.getElement().querySelector(".autoconnect .checkbox").classList.toggle("checked", this.getActiveInstance().autoconnectWhenLocal)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -332,8 +333,10 @@ class Manager {
|
||||
async onInstallTypeClicked(type) {
|
||||
this.typePage.getElement().querySelector(`.singleplayer`).classList.toggle("selected", type === 'singleplayer');
|
||||
this.typePage.getElement().querySelector(`.multiplayer`).classList.toggle("selected", type === 'multiplayer');
|
||||
if (this.getActiveInstance())
|
||||
if (this.getActiveInstance()) {
|
||||
this.getActiveInstance().installationType = type;
|
||||
this.getActiveInstance().autoconnectWhenLocal = type === 'singleplayer';
|
||||
}
|
||||
else {
|
||||
showErrorPopup(`<div class='main-message'>A critical error occurred! </div><div class='sub-message'> Check ${this.getLogLocation()} for more info. </div>`);
|
||||
}
|
||||
@@ -399,6 +402,7 @@ class Manager {
|
||||
this.activePage.hide();
|
||||
this.connectionsPage.show();
|
||||
(this.getMode() === 'basic' ? this.connectionsPage : this.expertSettingsPage).getElement().querySelector(".backend-address .checkbox").classList.toggle("checked", this.getActiveInstance().backendAddress === '*')
|
||||
(this.getMode() === 'basic' ? this.passwordsPage : this.expertSettingsPage).getElement().querySelector(".autoconnect .checkbox").classList.toggle("checked", this.getActiveInstance().autoconnectWhenLocal)
|
||||
}
|
||||
} else {
|
||||
showErrorPopup(`<div class='main-message'>A critical error occurred! </div><div class='sub-message'> Check ${this.getLogLocation()} for more info. </div>`)
|
||||
@@ -472,7 +476,7 @@ class Manager {
|
||||
}
|
||||
|
||||
async onGameMasterPasswordChanged(value) {
|
||||
for (let input of this.activePage.getElement().querySelectorAll("input[type='password']")) {
|
||||
for (let input of this.activePage.getElement().querySelectorAll("input[type='password'].unique")) {
|
||||
input.placeholder = "";
|
||||
}
|
||||
|
||||
@@ -483,7 +487,7 @@ class Manager {
|
||||
}
|
||||
|
||||
async onBlueCommanderPasswordChanged(value) {
|
||||
for (let input of this.activePage.getElement().querySelectorAll("input[type='password']")) {
|
||||
for (let input of this.activePage.getElement().querySelectorAll("input[type='password'].unique")) {
|
||||
input.placeholder = "";
|
||||
}
|
||||
|
||||
@@ -494,7 +498,7 @@ class Manager {
|
||||
}
|
||||
|
||||
async onRedCommanderPasswordChanged(value) {
|
||||
for (let input of this.activePage.getElement().querySelectorAll("input[type='password']")) {
|
||||
for (let input of this.activePage.getElement().querySelectorAll("input[type='password'].unique")) {
|
||||
input.placeholder = "";
|
||||
}
|
||||
|
||||
@@ -504,6 +508,13 @@ class Manager {
|
||||
showErrorPopup(`<div class='main-message'>A critical error occurred! </div><div class='sub-message'> Check ${this.getLogLocation()} for more info. </div>`);
|
||||
}
|
||||
|
||||
async onAdminPasswordChanged(value) {
|
||||
if (this.getActiveInstance())
|
||||
this.getActiveInstance().setAdminPassword(value);
|
||||
else
|
||||
showErrorPopup(`<div class='main-message'>A critical error occurred! </div><div class='sub-message'> Check ${this.getLogLocation()} for more info. </div>`);
|
||||
}
|
||||
|
||||
/* When the frontend port input value is changed */
|
||||
async onFrontendPortChanged(value) {
|
||||
this.setPort('frontend', Number(value));
|
||||
@@ -514,6 +525,11 @@ class Manager {
|
||||
this.setPort('backend', Number(value));
|
||||
}
|
||||
|
||||
/* When the srs port input value is changed */
|
||||
async onSRSPortChanged(value) {
|
||||
this.getActiveInstance().SRSPort = Number(value);
|
||||
}
|
||||
|
||||
/* When the "Enable API connection" checkbox is clicked */
|
||||
async onEnableAPIClicked() {
|
||||
if (this.getActiveInstance()) {
|
||||
@@ -547,6 +563,19 @@ class Manager {
|
||||
}
|
||||
}
|
||||
|
||||
async onEnableAutoconnectClicked() {
|
||||
if (this.getActiveInstance()) {
|
||||
if (this.getActiveInstance().autoconnectWhenLocal) {
|
||||
this.getActiveInstance().autoconnectWhenLocal = false;
|
||||
} else {
|
||||
this.getActiveInstance().autoconnectWhenLocal = true;
|
||||
}
|
||||
this.expertSettingsPage.getElement().querySelector(".autoconnect .checkbox").classList.toggle("checked", this.getActiveInstance().autoconnectWhenLocal)
|
||||
} else {
|
||||
showErrorPopup(`<div class='main-message'>A critical error occurred! </div><div class='sub-message'> Check ${this.getLogLocation()} for more info. </div>`)
|
||||
}
|
||||
}
|
||||
|
||||
/* When the "Return to manager" button is pressed */
|
||||
async onReturnClicked() {
|
||||
await this.reload();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dcsolympus_manager",
|
||||
"version": "1.0.0",
|
||||
"version": "{{OLYMPUS_VERSION_NUMBER}}",
|
||||
"description": "",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
@@ -11,20 +11,20 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@electron/remote": "^2.1.2",
|
||||
"adm-zip": "^0.5.10",
|
||||
"create-desktop-shortcuts": "^1.10.1",
|
||||
"adm-zip": "^0.5.16",
|
||||
"create-desktop-shortcuts": "^1.11.0",
|
||||
"dir-compare": "^4.2.0",
|
||||
"ejs": "^3.1.9",
|
||||
"electron": "^28.0.0",
|
||||
"find-process": "^1.4.7",
|
||||
"follow-redirects": "^1.15.4",
|
||||
"octokit": "^3.1.2",
|
||||
"portfinder": "^1.0.32",
|
||||
"regedit": "^5.1.2",
|
||||
"ejs": "^3.1.10",
|
||||
"electron": "^28.3.3",
|
||||
"find-process": "^1.4.10",
|
||||
"follow-redirects": "^1.15.9",
|
||||
"octokit": "^3.2.1",
|
||||
"portfinder": "^1.0.35",
|
||||
"regedit": "^5.1.3",
|
||||
"sha256": "^0.2.0",
|
||||
"win-version-info": "^6.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.2"
|
||||
"nodemon": "^3.1.9"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
50
olympus.json
50
olympus.json
@@ -1,25 +1,52 @@
|
||||
{
|
||||
"_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": {
|
||||
"address": "localhost",
|
||||
"port": 4512
|
||||
},
|
||||
|
||||
"_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": "4b8823ed9e5c2392ab4a791913bb8ce41956ea32e308b760eefb97536746dd33",
|
||||
"blueCommanderPassword": "b0ea4230c1558c5313165eda1bdb7fced008ca7f2ca6b823fb4d26292f309098",
|
||||
"redCommanderPassword": "302bcbaf2a3fdcf175b689bf102d6cdf9328f68a13d4096101bba806482bfed9"
|
||||
"gameMasterPassword": "",
|
||||
"blueCommanderPassword": "",
|
||||
"redCommanderPassword": "",
|
||||
"admin": ""
|
||||
},
|
||||
|
||||
"_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": {
|
||||
"enabled": false,
|
||||
"username": "X-Authorized",
|
||||
"group": "X-Group"
|
||||
},
|
||||
|
||||
"_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
|
||||
},
|
||||
|
||||
|
||||
"_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}",
|
||||
@@ -34,13 +61,28 @@
|
||||
"attribution": "OpenStreetMap contributors"
|
||||
}
|
||||
},
|
||||
|
||||
"_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"
|
||||
},
|
||||
"autoconnectWhenLocal": true
|
||||
|
||||
"_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"
|
||||
},
|
||||
|
||||
"_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"
|
||||
|
||||
@@ -589,6 +589,7 @@ function Olympus.fireLaser(ID, code, lat, lng)
|
||||
lat = lat,
|
||||
lng = lng
|
||||
},
|
||||
active = true,
|
||||
code = code
|
||||
}
|
||||
end
|
||||
@@ -611,13 +612,15 @@ function Olympus.fireInfrared(ID, lat, lng)
|
||||
targetPosition = {
|
||||
lat = lat,
|
||||
lng = lng
|
||||
}
|
||||
},
|
||||
active = true
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
-- Set new laser code
|
||||
function Olympus.setLaserCode(spotID, code)
|
||||
Olympus.debug("Olympus.setLaserCode " .. spotID .. " -> " .. code, 2)
|
||||
local spot = Olympus.spots[spotID]
|
||||
if spot ~= nil and spot.type == "laser" then
|
||||
spot.object:setCode(code)
|
||||
@@ -627,19 +630,21 @@ end
|
||||
|
||||
-- Move spot to a new location
|
||||
function Olympus.moveSpot(spotID, lat, lng)
|
||||
Olympus.debug("Olympus.moveSpot " .. spotID .. " -> (" .. lat .. ", " .. lng .. ")", 2)
|
||||
local spot = Olympus.spots[spotID]
|
||||
if spot ~= nil then
|
||||
spot.object:setPoint(coord.LLtoLO(lat, lng, 0))
|
||||
spot.object:setPoint(mist.utils.makeVec3GL(coord.LLtoLO(lat, lng, 0)))
|
||||
spot.targetPosition = {lat = lat, lng = lng}
|
||||
end
|
||||
end
|
||||
|
||||
-- Remove the spot
|
||||
function Olympus.deleteSpot(spotID)
|
||||
Olympus.debug("Olympus.deleteSpot " .. spotID, 2)
|
||||
local spot = Olympus.spots[spotID]
|
||||
if spot ~= nil then
|
||||
spot.object:destroy()
|
||||
Olympus.spots[spotID] = nil
|
||||
Olympus.spots[spotID]["active"] = false
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1450,6 +1455,8 @@ function Olympus.setWeaponsData(arg, time)
|
||||
table["category"] = "Missile"
|
||||
elseif weapon:getDesc().category == Weapon.Category.BOMB then
|
||||
table["category"] = "Bomb"
|
||||
--elseif weapon:getDesc().category == Weapon.Category.SHELL then
|
||||
-- table["category"] = "Shell" -- Useful for debugging but has no real use and has big impact on performance
|
||||
end
|
||||
else
|
||||
weapons[ID] = {isAlive = false}
|
||||
@@ -1560,6 +1567,7 @@ function Olympus.setMissionData(arg, time)
|
||||
type = spot.type,
|
||||
sourceUnitID = spot.sourceUnitID,
|
||||
targetPosition = spot.targetPosition,
|
||||
active = spot.active,
|
||||
}
|
||||
|
||||
-- If the spot type is "laser", add the code to the spot entry
|
||||
@@ -1575,7 +1583,7 @@ function Olympus.setMissionData(arg, time)
|
||||
Olympus.missionData["spots"] = spots
|
||||
|
||||
Olympus.OlympusDLL.setMissionData()
|
||||
return time + 1 -- For perfomance reasons weapons are updated once every second
|
||||
return time + 1 -- For perfomance reasons mission data is updated once every second
|
||||
end
|
||||
|
||||
-- Initializes the units table with all the existing ME units
|
||||
|
||||
Reference in New Issue
Block a user