Merge branch 'release-candidate' of https://github.com/Pax1601/DCSOlympus into release-candidate

This commit is contained in:
Pax1601
2025-03-14 18:43:25 +01:00
16 changed files with 350 additions and 84 deletions

View File

@@ -62,6 +62,8 @@ public:
bool hasFreshData(unsigned long long time); bool hasFreshData(unsigned long long time);
bool checkFreshness(unsigned char datumIndex, unsigned long long time); bool checkFreshness(unsigned char datumIndex, unsigned long long time);
unsigned int computeTotalAmmo();
/********** Setters **********/ /********** Setters **********/
virtual void setCategory(string newValue) { updateValue(category, newValue, DataIndex::category); } virtual void setCategory(string newValue) { updateValue(category, newValue, DataIndex::category); }
virtual void setAlive(bool newValue) { updateValue(alive, newValue, DataIndex::alive); } virtual void setAlive(bool newValue) { updateValue(alive, newValue, DataIndex::alive); }
@@ -240,26 +242,29 @@ protected:
unsigned char shotsIntensity = 2; unsigned char shotsIntensity = 2;
unsigned char health = 100; unsigned char health = 100;
double timeToNextTasking = 0; double timeToNextTasking = 0;
double barrelHeight = 1.0; /* m */ double barrelHeight = 0;
double muzzleVelocity = 860; /* m/s */ double muzzleVelocity = 0;
double aimTime = 10; /* s */ double aimTime = 0;
unsigned int shotsToFire = 10; unsigned int shotsToFire = 0;
double shotsBaseInterval = 15; /* s */ double shotsBaseInterval = 0;
double shotsBaseScatter = 2; /* degs */ double shotsBaseScatter = 0;
double engagementRange = 10000; /* m */ double engagementRange = 0;
double targetingRange = 0; /* m */ double targetingRange = 0;
double aimMethodRange = 0; /* m */ double aimMethodRange = 0;
double acquisitionRange = 0; /* m */ double acquisitionRange = 0;
/********** Other **********/ /********** Other **********/
unsigned int taskCheckCounter = 0; unsigned int taskCheckCounter = 0;
unsigned int internalCounter = 0;
Unit* missOnPurposeTarget = nullptr; Unit* missOnPurposeTarget = nullptr;
bool hasTaskAssigned = false; bool hasTaskAssigned = false;
double initialFuel = 0; double initialFuel = 0;
map<unsigned char, unsigned long long> updateTimeMap; map<unsigned char, unsigned long long> updateTimeMap;
unsigned long long lastLoopTime = 0; unsigned long long lastLoopTime = 0;
bool enableTaskFailedCheck = false; bool enableTaskFailedCheck = false;
unsigned long nextTaskingMilliseconds = 0;
unsigned int totalShellsFired = 0;
unsigned int shellsFiredAtTasking = 0;
unsigned int oldAmmo = 0;
/********** Private methods **********/ /********** Private methods **********/
virtual void AIloop() = 0; virtual void AIloop() = 0;

View File

@@ -113,4 +113,10 @@ class Bomb : public Weapon
{ {
public: public:
Bomb(json::value json, unsigned int ID); Bomb(json::value json, unsigned int ID);
};
class Shell : public Weapon
{
public:
Shell(json::value json, unsigned int ID);
}; };

View File

@@ -168,6 +168,18 @@ void GroundUnit::setState(unsigned char newState)
void GroundUnit::AIloop() void GroundUnit::AIloop()
{ {
srand(static_cast<unsigned int>(time(NULL)) + ID); 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) { switch (state) {
case State::IDLE: { case State::IDLE: {
@@ -241,7 +253,11 @@ void GroundUnit::AIloop()
case State::SIMULATE_FIRE_FIGHT: { case State::SIMULATE_FIRE_FIGHT: {
string taskString = ""; string taskString = "";
if (internalCounter == 0 && targetPosition != Coords(NULL) && scheduler->getLoad() < 30) { if (
(totalShellsFired - shellsFiredAtTasking >= shotsToFire || timeNow >= nextTaskingMilliseconds) &&
targetPosition != Coords(NULL) &&
scheduler->getLoad() < 30
) {
/* Get the distance and bearing to the target */ /* Get the distance and bearing to the target */
Coords scatteredTargetPosition = targetPosition; Coords scatteredTargetPosition = targetPosition;
double distance; double distance;
@@ -249,9 +265,12 @@ void GroundUnit::AIloop()
double bearing2; double bearing2;
Geodesic::WGS84().Inverse(getPosition().lat, getPosition().lng, scatteredTargetPosition.lat, scatteredTargetPosition.lng, distance, bearing1, bearing2); Geodesic::WGS84().Inverse(getPosition().lat, getPosition().lng, scatteredTargetPosition.lat, scatteredTargetPosition.lng, distance, bearing1, bearing2);
/* 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 */ /* 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; 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); Geodesic::WGS84().Direct(scatteredTargetPosition.lat, scatteredTargetPosition.lng, bearing1, scatterDistance, scatteredTargetPosition.lat, scatteredTargetPosition.lng);
/* Recover the data from the database */ /* Recover the data from the database */
bool indirectFire = false; bool indirectFire = false;
@@ -267,9 +286,10 @@ void GroundUnit::AIloop()
log(unitName + "(" + name + ")" + " simulating fire fight with indirect fire"); log(unitName + "(" + name + ")" + " simulating fire fight with indirect fire");
std::ostringstream taskSS; std::ostringstream taskSS;
taskSS.precision(10); taskSS.precision(10);
taskSS << "{id = 'FireAtPoint', lat = " << scatteredTargetPosition.lat << ", lng = " << scatteredTargetPosition.lng << ", radius = 100}"; 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); })); Command* command = dynamic_cast<Command*>(new SetTask(groupName, taskSS.str(), [this]() { this->setHasTaskAssigned(true); }));
scheduler->appendCommand(command); scheduler->appendCommand(command);
shellsFiredAtTasking = totalShellsFired;
setHasTask(true); setHasTask(true);
} }
/* Otherwise use the aim method */ /* Otherwise use the aim method */
@@ -281,18 +301,17 @@ void GroundUnit::AIloop()
} }
/* Wait an amout of time depending on the shots intensity */ /* Wait an amout of time depending on the shots intensity */
internalCounter = static_cast<unsigned int>(((ShotsIntensity::HIGH - shotsIntensity) * shotsBaseInterval + aimTime) / FRAMERATE_TIME_INTERVAL); nextTaskingMilliseconds = timeNow + static_cast<unsigned long>(2 * aimTime * 1000);
} }
if (targetPosition == Coords(NULL)) if (targetPosition == Coords(NULL))
setState(State::IDLE); setState(State::IDLE);
/* Fallback if something went wrong */ /* Fallback if something went wrong */
if (internalCounter == 0) if (timeNow >= nextTaskingMilliseconds)
internalCounter = static_cast<unsigned int>(3 / FRAMERATE_TIME_INTERVAL); nextTaskingMilliseconds = timeNow + static_cast<unsigned long>(3 * 1000);
internalCounter--;
setTimeToNextTasking(internalCounter * FRAMERATE_TIME_INTERVAL); setTimeToNextTasking(((nextTaskingMilliseconds - timeNow) / 1000.0));
if (taskString.length() > 0) if (taskString.length() > 0)
setTask(taskString); setTask(taskString);
@@ -303,7 +322,8 @@ void GroundUnit::AIloop()
string taskString = ""; string taskString = "";
/* Only perform scenic functions when the scheduler is "free" */ /* Only perform scenic functions when the scheduler is "free" */
if (((!getHasTask() && scheduler->getLoad() < 30) || internalCounter == 0)) { if ((totalShellsFired - shellsFiredAtTasking >= shotsToFire || timeNow >= nextTaskingMilliseconds) &&
scheduler->getLoad() < 30) {
double distance = 0; double distance = 0;
unsigned char unitCoalition = coalition == 0 ? getOperateAs() : coalition; unsigned char unitCoalition = coalition == 0 ? getOperateAs() : coalition;
unsigned char targetCoalition = unitCoalition == 2 ? 1 : 2; unsigned char targetCoalition = unitCoalition == 2 ? 1 : 2;
@@ -341,17 +361,18 @@ void GroundUnit::AIloop()
taskString += "Scenic AAA. Bearing: " + to_string((int)round(randomBearing)) + "deg"; taskString += "Scenic AAA. Bearing: " + to_string((int)round(randomBearing)) + "deg";
} }
taskString += ". Aim point elevation " + to_string((int) round(barrelElevation)) + "m AGL"; taskString += ". Aim point elevation " + to_string((int) round(barrelElevation - position.alt)) + "m AGL";
std::ostringstream taskSS; std::ostringstream taskSS;
taskSS.precision(10); taskSS.precision(10);
taskSS << "{id = 'FireAtPoint', lat = " << lat << ", lng = " << lng << ", alt = " << barrelElevation << ", radius = 0.001, expendQty = " << shotsToFire << " }"; 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); })); Command* command = dynamic_cast<Command*>(new SetTask(groupName, taskSS.str(), [this]() { this->setHasTaskAssigned(true); }));
scheduler->appendCommand(command); scheduler->appendCommand(command);
shellsFiredAtTasking = totalShellsFired;
setHasTask(true); setHasTask(true);
/* Wait an amout of time depending on the shots intensity */ /* Wait an amout of time depending on the shots intensity */
internalCounter = static_cast<unsigned int>(((ShotsIntensity::HIGH - shotsIntensity) * shotsBaseInterval + aimTime) / FRAMERATE_TIME_INTERVAL); nextTaskingMilliseconds = timeNow + static_cast<unsigned long>(2 * aimTime * 1000);
} }
else { else {
if (target == nullptr) if (target == nullptr)
@@ -361,11 +382,10 @@ void GroundUnit::AIloop()
} }
} }
if (internalCounter == 0) if (timeNow >= nextTaskingMilliseconds)
internalCounter = static_cast<unsigned int>(3 / FRAMERATE_TIME_INTERVAL); nextTaskingMilliseconds = timeNow + static_cast<unsigned long>(3 * 1000);
internalCounter--;
setTimeToNextTasking(internalCounter * FRAMERATE_TIME_INTERVAL); setTimeToNextTasking((nextTaskingMilliseconds - timeNow) / 1000.0);
if (taskString.length() > 0) if (taskString.length() > 0)
setTask(taskString); setTask(taskString);
@@ -385,7 +405,8 @@ void GroundUnit::AIloop()
if (canAAA) { if (canAAA) {
/* Only perform scenic functions when the scheduler is "free" */ /* 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 */ /* Only run this when the internal counter reaches 0 to avoid excessive computations when no nearby target */
if (scheduler->getLoad() < 30 && internalCounter == 0) { if ((totalShellsFired - shellsFiredAtTasking >= shotsToFire || timeNow >= nextTaskingMilliseconds) &&
scheduler->getLoad() < 30) {
double distance = 0; double distance = 0;
unsigned char unitCoalition = coalition == 0 ? getOperateAs() : coalition; unsigned char unitCoalition = coalition == 0 ? getOperateAs() : coalition;
unsigned char targetCoalition = unitCoalition == 2 ? 1 : 2; unsigned char targetCoalition = unitCoalition == 2 ? 1 : 2;
@@ -422,9 +443,10 @@ void GroundUnit::AIloop()
taskSS << "{id = 'AttackUnit', unitID = " << target->getID() << " }"; taskSS << "{id = 'AttackUnit', unitID = " << target->getID() << " }";
Command* command = dynamic_cast<Command*>(new SetTask(groupName, taskSS.str(), [this]() { this->setHasTaskAssigned(true); })); Command* command = dynamic_cast<Command*>(new SetTask(groupName, taskSS.str(), [this]() { this->setHasTaskAssigned(true); }));
scheduler->appendCommand(command); scheduler->appendCommand(command);
shellsFiredAtTasking = totalShellsFired;
setHasTask(true); setHasTask(true);
internalCounter = static_cast<unsigned int>((correctedAimTime + (ShotsIntensity::HIGH - shotsIntensity) * shotsBaseInterval + 2) / FRAMERATE_TIME_INTERVAL); nextTaskingMilliseconds = timeNow + static_cast<unsigned long>(2 * aimTime * 1000);
} }
/* Else, do miss on purpose */ /* Else, do miss on purpose */
else { else {
@@ -443,14 +465,15 @@ void GroundUnit::AIloop()
/* If the unit is closer than the engagement range, use the fire at point method */ /* If the unit is closer than the engagement range, use the fire at point method */
std::ostringstream taskSS; std::ostringstream taskSS;
taskSS.precision(10); taskSS.precision(10);
taskSS << "{id = 'FireAtPoint', lat = " << aimLat << ", lng = " << aimLng << ", alt = " << aimAlt << ", radius = 0.001, expendQty = " << shotsToFire << " }"; 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"; 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); })); Command* command = dynamic_cast<Command*>(new SetTask(groupName, taskSS.str(), [this]() { this->setHasTaskAssigned(true); }));
scheduler->appendCommand(command); scheduler->appendCommand(command);
shellsFiredAtTasking = totalShellsFired;
setHasTask(true); setHasTask(true);
setTargetPosition(Coords(aimLat, aimLng, target->getPosition().alt)); setTargetPosition(Coords(aimLat, aimLng, target->getPosition().alt));
internalCounter = static_cast<unsigned int>((correctedAimTime + (ShotsIntensity::HIGH - shotsIntensity) * shotsBaseInterval + 2) / FRAMERATE_TIME_INTERVAL); nextTaskingMilliseconds = timeNow + static_cast<unsigned long>(2 * aimTime * 1000);
} }
else if (distance < aimMethodRange) { else if (distance < aimMethodRange) {
taskString += ". Range is less than aim method range (" + to_string((int)round(aimMethodRange / 0.3048)) + "ft), using AIM method."; taskString += ". Range is less than aim method range (" + to_string((int)round(aimMethodRange / 0.3048)) + "ft), using AIM method.";
@@ -460,7 +483,7 @@ void GroundUnit::AIloop()
taskString += aimMethodTask; taskString += aimMethodTask;
setTargetPosition(Coords(aimLat, aimLng, target->getPosition().alt)); setTargetPosition(Coords(aimLat, aimLng, target->getPosition().alt));
internalCounter = static_cast<unsigned int>((correctedAimTime + (ShotsIntensity::HIGH - shotsIntensity) * shotsBaseInterval + 2) / FRAMERATE_TIME_INTERVAL); nextTaskingMilliseconds = timeNow + static_cast<unsigned long>(2 * aimTime * 1000);
} }
else { else {
taskString += ". Target is not in range of weapon, waking up unit to get ready for tasking."; taskString += ". Target is not in range of weapon, waking up unit to get ready for tasking.";
@@ -471,11 +494,12 @@ void GroundUnit::AIloop()
taskSS << "{id = 'FireAtPoint', lat = " << 0 << ", lng = " << 0 << ", alt = " << 0 << ", radius = 0.001, expendQty = " << 0 << " }"; 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); })); Command* command = dynamic_cast<Command*>(new SetTask(groupName, taskSS.str(), [this]() { this->setHasTaskAssigned(true); }));
scheduler->appendCommand(command); scheduler->appendCommand(command);
shellsFiredAtTasking = totalShellsFired;
setHasTask(true); setHasTask(true);
setTargetPosition(Coords(NULL)); setTargetPosition(Coords(NULL));
/* Don't wait too long before checking again */ /* Don't wait too long before checking again */
internalCounter = static_cast<unsigned int>(5 / FRAMERATE_TIME_INTERVAL); nextTaskingMilliseconds = timeNow + static_cast<unsigned long>(5 * 1000);
} }
} }
missOnPurposeTarget = target; missOnPurposeTarget = target;
@@ -488,24 +512,24 @@ void GroundUnit::AIloop()
} }
/* If no valid target was detected */ /* If no valid target was detected */
if (internalCounter == 0) { if (timeNow >= nextTaskingMilliseconds) {
double alertnessTimeConstant = 10; /* s */ double alertnessTimeConstant = 10; /* s */
if (database.has_object_field(to_wstring(name))) { if (database.has_object_field(to_wstring(name))) {
json::value databaseEntry = database[to_wstring(name)]; json::value databaseEntry = database[to_wstring(name)];
if (databaseEntry.has_number_field(L"alertnessTimeConstant")) if (databaseEntry.has_number_field(L"alertnessTimeConstant"))
alertnessTimeConstant = databaseEntry[L"alertnessTimeConstant"].as_number().to_double(); alertnessTimeConstant = databaseEntry[L"alertnessTimeConstant"].as_number().to_double();
} }
internalCounter = static_cast<unsigned int>((5 + RANDOM_ZERO_TO_ONE * alertnessTimeConstant) / FRAMERATE_TIME_INTERVAL); nextTaskingMilliseconds = timeNow + static_cast<unsigned long>((5 + RANDOM_ZERO_TO_ONE * alertnessTimeConstant) * 1000L);
missOnPurposeTarget = nullptr; missOnPurposeTarget = nullptr;
setTargetPosition(Coords(NULL)); setTargetPosition(Coords(NULL));
} }
internalCounter--;
} }
else { else {
setState(State::IDLE); setState(State::IDLE);
} }
setTimeToNextTasking(internalCounter * FRAMERATE_TIME_INTERVAL); setTimeToNextTasking((nextTaskingMilliseconds - timeNow) / 1000.0);
if (taskString.length() > 0) if (taskString.length() > 0)
setTask(taskString); setTask(taskString);
@@ -528,20 +552,6 @@ string GroundUnit::aimAtPoint(Coords aimTarget) {
/* Aim point distance */ /* Aim point distance */
double r = 15; /* m */ 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*/ /* Compute the elevation angle of the gun*/
double deltaHeight = (aimTarget.alt - (position.alt + barrelHeight)); double deltaHeight = (aimTarget.alt - (position.alt + barrelHeight));
double alpha = 9.81 / 2 * dist * dist / (muzzleVelocity * muzzleVelocity); double alpha = 9.81 / 2 * dist * dist / (muzzleVelocity * muzzleVelocity);
@@ -564,6 +574,7 @@ string GroundUnit::aimAtPoint(Coords aimTarget) {
taskSS << "{id = 'FireAtPoint', lat = " << lat << ", lng = " << lng << ", alt = " << position.alt + barrelElevation + barrelHeight << ", radius = 0.001}"; 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); })); Command* command = dynamic_cast<Command*>(new SetTask(groupName, taskSS.str(), [this]() { this->setHasTaskAssigned(true); }));
scheduler->appendCommand(command); scheduler->appendCommand(command);
shellsFiredAtTasking = totalShellsFired;
setHasTask(true); setHasTask(true);
} }
else { else {

View File

@@ -825,3 +825,11 @@ void Unit::setHasTaskAssigned(bool newHasTaskAssigned) {
void Unit::triggerUpdate(unsigned char datumIndex) { void Unit::triggerUpdate(unsigned char datumIndex) {
updateTimeMap[datumIndex] = duration_cast<milliseconds>(system_clock::now().time_since_epoch()).count(); 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;
}

View File

@@ -113,4 +113,11 @@ Bomb::Bomb(json::value json, unsigned int ID) : Weapon(json, ID)
{ {
log("New Bomb created with ID: " + to_string(ID)); log("New Bomb created with ID: " + to_string(ID));
setCategory("Bomb"); 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");
}; };

View File

@@ -41,6 +41,8 @@ void WeaponsManager::update(json::value& json, double dt)
weapons[ID] = dynamic_cast<Weapon*>(new Missile(p.second, ID)); weapons[ID] = dynamic_cast<Weapon*>(new Missile(p.second, ID));
else if (category.compare("Bomb") == 0) else if (category.compare("Bomb") == 0)
weapons[ID] = dynamic_cast<Weapon*>(new Bomb(p.second, ID)); 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 */ /* Initialize the weapon if creation was successfull */
if (weapons.count(ID) != 0) { if (weapons.count(ID) != 0) {

View 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

View 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

View 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

View 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

View 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

View File

@@ -507,11 +507,6 @@ export class Map extends L.Map {
altKey: false, altKey: false,
ctrlKey: false, ctrlKey: false,
}); });
/* Periodically check if the camera control endpoint is available */
this.#cameraControlTimer = window.setInterval(() => {
this.#checkCameraPort();
}, 1000);
} }
setLayerName(layerName: string) { setLayerName(layerName: string) {
@@ -1298,33 +1293,6 @@ export class Map extends L.Map {
return minimapBoundaries; 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() { #drawIPToTargetLine() {
if (this.#targetPoint && this.#IPPoint) { if (this.#targetPoint && this.#IPPoint) {
if (!this.#IPToTargetLine) { if (!this.#IPToTargetLine) {

View File

@@ -188,6 +188,8 @@ export abstract class Unit extends CustomMarker {
#targetingRange: number = 0; #targetingRange: number = 0;
#aimMethodRange: number = 0; #aimMethodRange: number = 0;
#acquisitionRange: number = 0; #acquisitionRange: number = 0;
#totalAmmo: number = 0;
#previousTotalAmmo: number = 0;
/* Inputs timers */ /* Inputs timers */
#debounceTimeout: number | null = null; #debounceTimeout: number | null = null;
@@ -654,6 +656,8 @@ export abstract class Unit extends CustomMarker {
break; break;
case DataIndexes.ammo: case DataIndexes.ammo:
this.#ammo = dataExtractor.extractAmmo(); this.#ammo = dataExtractor.extractAmmo();
this.#previousTotalAmmo = this.#totalAmmo;
this.#totalAmmo = this.#ammo.reduce((prev: number, ammo: Ammo) => prev + ammo.quantity, 0);
break; break;
case DataIndexes.contacts: case DataIndexes.contacts:
this.#contacts = dataExtractor.extractContacts(); this.#contacts = dataExtractor.extractContacts();

View File

@@ -43,6 +43,7 @@ export abstract class Weapon extends CustomMarker {
static getConstructor(type: string) { static getConstructor(type: string) {
if (type === "Missile") return Missile; if (type === "Missile") return Missile;
if (type === "Bomb") return Bomb; if (type === "Bomb") return Bomb;
if (type === "Shell") return Shell;
} }
constructor(ID: number) { constructor(ID: number) {
@@ -330,3 +331,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),
};
}
}

View File

@@ -38,7 +38,7 @@ export class WeaponsManager {
/** Add a new weapon to the manager /** Add a new weapon to the manager
* *
* @param ID ID of the new weapon * @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) { addWeapon(ID: number, category: string) {
if (category) { if (category) {

View File

@@ -1415,6 +1415,8 @@ function Olympus.setWeaponsData(arg, time)
table["category"] = "Missile" table["category"] = "Missile"
elseif weapon:getDesc().category == Weapon.Category.BOMB then elseif weapon:getDesc().category == Weapon.Category.BOMB then
table["category"] = "Bomb" table["category"] = "Bomb"
elseif weapon:getDesc().category == Weapon.Category.SHELL then
table["category"] = "Shell"
end end
else else
weapons[ID] = {isAlive = false} weapons[ID] = {isAlive = false}