-
-
Save aphix/fdeeefbc4bef1ec580d72639bbc05f2d to your computer and use it in GitHub Desktop.
// ==UserScript== | |
// @name Better Airline Club + Cost Per PAX Combined & Improved | |
// @namespace http://tampermonkey.net/ | |
// @version 1.1.2 | |
// @description Enhances airline-club.com and v2.airline-club.com airline management game (protip: Sign into your 2 accounts with one on each domain to avoid extra logout/login). Install this script with automatic updates by first installing TamperMonkey/ViolentMonkey/GreaseMonkey and installing it as a userscript. | |
// @author Aphix/Torus (original "Cost Per PAX" portion by Alrianne @ https://github.com/wolfnether/Airline_Club_Mod/) | |
// @match https://*.airline-club.com/ | |
// @icon https://www.google.com/s2/favicons?domain=airline-club.com | |
// @downloadURL https://gist.githubusercontent.com/aphix/fdeeefbc4bef1ec580d72639bbc05f2d/raw/BetterAirlineClub.userscript.js | |
// @updateURL https://gist.githubusercontent.com/aphix/fdeeefbc4bef1ec580d72639bbc05f2d/raw/BetterAirlineClub.userscript.js | |
// @grant none | |
// ==/UserScript== | |
// ---- BEGIN of Section where user is expected to tweak things to make it how they like ------- | |
var MIN_PLANES_TO_HIGHLIGHT = 500; // Changes which planes get the gold shadow/highlight on plane purchase table (not affected by filters in table header) | |
var REMOVE_MOVING_BACKGROUND = true; // perf enhancement, less noisy (gradients & transparency are still expensive in 2024, and my GPU has AI work to better spend it's time on) | |
// Default filter values for plane purchase table header: | |
var DEFAULT_MIN_PLANES_IN_CIRCULATION_FILTER = 50; // Changes default minimum number of planes in circulation to remove from plane purchase table | |
var DEFAULT_MIN_FLIGHT_RANGE_FILTER = 1000; | |
var DEFAULT_RUNWAY_LENGTH_FILTER = 3000; | |
var DEFAULT_MIN_CAPACITY_FILTER = 0; | |
// ---- END of Section where user is expected to tweak things to make it how they like ------- | |
// Plugin code starts here and goes to the end... | |
// Feel free to leave a comment on the gist if you have any questions or requests: https://gist.github.com/aphix/fdeeefbc4bef1ec580d72639bbc05f2d | |
// Want to donate? Don't. Buy yourself some ETH. If that works out nice and you want to pay it back later then find me on github. | |
function reportAjaxError(jqXHR, textStatus, errorThrown) { | |
console.error(JSON.stringify(jqXHR)); | |
console.error("AJAX error: " + textStatus + ' : ' + errorThrown); | |
// throw errorThrown; | |
} | |
function _request(url, method = 'GET', data = undefined) { | |
return new Promise((resolve, reject) => { | |
$.ajax({ | |
url, | |
type: method, | |
contentType: 'application/json; charset=utf-8', | |
data: data ? JSON.stringify(data) : data, | |
dataType: 'json', | |
success: resolve, | |
error: (...args) => { | |
reportAjaxError(...args); | |
reject(...args); | |
} | |
}) | |
}) | |
} | |
function getFactorPercent(consumption, subType) { | |
return (consumption.capacity[subType] > 0) | |
? parseInt(consumption.soldSeats[subType] / consumption.capacity[subType] * 100) | |
: null; | |
} | |
function getLoadFactorsFor(consumption) { | |
var factor = {}; | |
for (let key in consumption.capacity) { | |
factor[key] = getFactorPercent(consumption, key) || '-'; | |
} | |
return factor; | |
} | |
function _seekSubVal(val, ...subKeys) { | |
if (subKeys.length === 0) { | |
return val; | |
} | |
return _seekSubVal(val[subKeys[0]], ...subKeys.slice(1)); | |
} | |
function averageFromSubKey(array, ...subKeys) { | |
return array.map(obj => _seekSubVal(obj, ...subKeys)).reduce((sum, val) => sum += (val || 0), 0) / array.length; | |
} | |
function _populateDerivedFieldsOnLink(link) { | |
link.totalCapacity = link.capacity.economy + link.capacity.business + link.capacity.first | |
link.totalCapacityHistory = link.capacityHistory.economy + link.capacityHistory.business + link.capacityHistory.first | |
link.totalPassengers = link.passengers.economy + link.passengers.business + link.passengers.first | |
link.totalLoadFactor = link.totalCapacityHistory > 0 ? Math.round(link.totalPassengers / link.totalCapacityHistory * 100) : 0 | |
var assignedModel | |
if (link.assignedAirplanes && link.assignedAirplanes.length > 0) { | |
assignedModel = link.assignedAirplanes[0].airplane.name | |
} else { | |
assignedModel = "-" | |
} | |
link.model = assignedModel //so this can be sorted | |
link.profitMarginPercent = link.revenue === 0 | |
? 0 | |
: ((link.profit + link.revenue) / link.revenue) * 100; | |
link.profitMargin = link.profitMarginPercent > 100 | |
? link.profitMarginPercent - 100 | |
: (100 - link.profitMarginPercent) * -1; | |
link.profitPerPax = link.totalPassengers === 0 | |
? 0 | |
:link.profit / link.totalPassengers; | |
link.profitPerFlight = link.profit / link.frequency; | |
link.profitPerHour = link.profit / link.duration; | |
//console.dir(link); | |
} | |
function plotHistory(linkConsumptions) { | |
plotLinkCharts(linkConsumptions) | |
$("#linkHistoryDetails").show() | |
} | |
function getShortModelName(airplaneName) { | |
var sections = airplaneName.trim().split(' ').slice(1); | |
return sections | |
.map(str => (str.includes('-') | |
|| str.length < 4 | |
|| /^[A-Z0-9\-]+[a-z]{0,4}$/.test(str)) | |
? str | |
: str[0].toUpperCase()) | |
.join(' '); | |
} | |
function getStyleFromTier(tier) { | |
const stylesFromGoodToBad = [ | |
'color:#29FF66;', | |
'color:#5AB874;', | |
'color:inherit;', | |
'color:#FA8282;', | |
//'color:#FF3D3D;', | |
//'color:#B30E0E;text-shadow:0px 0px 2px #CCC;', | |
'color:#FF6969;', | |
'color:#FF3D3D;font-weight: bold;', | |
// 'color:#FF3D3D;text-decoration:underline', | |
]; | |
return stylesFromGoodToBad[tier]; | |
} | |
function getTierFromPercent(val, min = 0, max = 100) { | |
var availableRange = max - min; | |
var ranges = [ | |
.95, | |
.80, | |
.75, | |
.6, | |
.5 | |
].map(multiplier => (availableRange * multiplier) + min); | |
var tier; | |
if (val > ranges[0]) { | |
return 0; | |
} else if (val > ranges[1]) { | |
return 1; | |
} else if (val > ranges[2]) { | |
return 2; | |
} else if (val > ranges[3]) { | |
return 3; | |
} else if (val > ranges[4]) { | |
return 4; | |
} | |
return 5; | |
} | |
async function loadCompetitionForLink(airlineId, link) { | |
const linkConsumptions = await _request(`airports/${link.fromAirportId}/to/${link.toAirportId}`); | |
$("#linkCompetitons .data-row").remove() | |
$.each(linkConsumptions, function(index, linkConsumption) { | |
var row = $("<div class='table-row data-row'><div style='display: table-cell;'>" + linkConsumption.airlineName | |
+ "</div><div style='display: table-cell;'>" + toLinkClassValueString(linkConsumption.price, "$") | |
+ "</div><div style='display: table-cell; text-align: right;'>" + toLinkClassValueString(linkConsumption.capacity) | |
+ "</div><div style='display: table-cell; text-align: right;'>" + linkConsumption.quality | |
+ "</div><div style='display: table-cell; text-align: right;'>" + linkConsumption.frequency + "</div></div>") | |
if (linkConsumption.airlineId == airlineId) { | |
$("#linkCompetitons .table-header").after(row) //self is always on top | |
} else { | |
$("#linkCompetitons").append(row) | |
} | |
}) | |
if ($("#linkCompetitons .data-row").length == 0) { | |
$("#linkCompetitons").append("<div class='table-row data-row'><div style='display: table-cell;'>-</div><div style='display: table-cell;'>-</div><div style='display: table-cell;'>-</div><div style='display: table-cell;'>-</div><div style='display: table-cell;'>-</div></div>") | |
} | |
$("#linkCompetitons").show() | |
assignAirlineColors(linkConsumptions, "airlineId") | |
plotPie(linkConsumptions, null, $("#linkCompetitionsPie"), "airlineName", "soldSeats") | |
return linkConsumptions; | |
} | |
function _isFullPax(link, key) { | |
return link.passengers[key] === link.capacity[key]; | |
} | |
function _getPricesFor(link) { | |
var linkPrices = {}; | |
for (var key in link.price) { | |
if (key === 'total') continue; | |
linkPrices[key] = link.price[key] - 5; | |
// linkPrices[key] = link.price[key] - (_isFullPax(link, key) ? 0 : 5); | |
} | |
return linkPrices; | |
} | |
async function _doAutomaticPriceUpdateFor(link) { | |
var priceUpdate = { | |
fromAirportId: link.fromAirportId, | |
toAirportId: link.toAirportId, | |
assignedDelegates: 0, | |
airplanes: {}, | |
airlineId: link.assignedAirplanes[0].airplane.ownerId, | |
price: _getPricesFor(link), | |
model: link.assignedAirplanes[0].airplane.modelId, | |
rawQuality: link.rawQuality | |
} | |
for (var p of link.assignedAirplanes) { | |
if (!p.frequency) continue; | |
priceUpdate.airplanes[p.airplane.id] = p.frequency; | |
} | |
const updateResult = await _request(`/airlines/${priceUpdate.airlineId}/links`, 'PUT', priceUpdate); | |
} | |
//load history | |
async function loadHistoryForLink(airlineId, linkId, cycleCount, link) { | |
const linkHistory = await _request(`airlines/${airlineId}/link-consumptions/${linkId}?cycleCount=${cycleCount}`); | |
if (jQuery.isEmptyObject(linkHistory)) { | |
$("#linkHistoryPrice").text("-") | |
$("#linkHistoryCapacity").text("-") | |
$("#linkLoadFactor").text("-") | |
$("#linkProfit").text("-") | |
$("#linkRevenue").text("-") | |
$("#linkFuelCost").text("-") | |
$("#linkCrewCost").text("-") | |
$("#linkAirportFees").text("-") | |
$("#linkDepreciation").text("-") | |
$("#linkCompensation").text("-") | |
$("#linkLoungeCost").text("-") | |
$("#linkServiceSupplies").text("-") | |
$("#linkMaintenance").text("-") | |
$("#linkOtherCosts").text("-") | |
$("#linkDelays").text("-") | |
$("#linkCancellations").text("-") | |
disableButton($("#linkDetails .button.viewLinkHistory"), "Passenger Map is not yet available for this route - please wait for the simulation (time estimation on top left of the screen).") | |
disableButton($("#linkDetails .button.viewLinkComposition"), "Passenger Survey is not yet available for this route - please wait for the simulation (time estimation on top left of the screen).") | |
plotHistory(linkHistory); | |
return; | |
} | |
if (!$("#linkAverageLoadFactor").length) { | |
$("#linkLoadFactor").parent().after(`<div class="table-row" style="color:#999"> | |
<div class="label" style="color:#999"><h5>Avg. Load Factor:</h5></div> | |
<div class="value" id="linkAverageLoadFactor"></div> | |
</div>`) | |
} | |
if (!$("#linkAverageProfit").length) { | |
$("#linkProfit").parent().after(`<div class="table-row" style="color:#999"> | |
<div class="label" style="color:#999"><h5>Avg. Profit:</h5></div> | |
<div class="value" id="linkAverageProfit"></div> | |
</div>`) | |
} | |
//if (!$("#doAutomaticPriceUpdate").length) { | |
// $("#linkLoadFactor").parent().after(`<div class="table-row" style="color:#999"> | |
// <div class="button" id="doAutomaticPriceUpdate">Auto Manage</div> | |
// </div>`) | |
//} | |
const averageLoadFactor = getLoadFactorsFor({ | |
soldSeats: { | |
economy: averageFromSubKey(linkHistory, 'soldSeats', 'economy'), | |
business: averageFromSubKey(linkHistory, 'soldSeats', 'business'), | |
first: averageFromSubKey(linkHistory, 'soldSeats', 'first'), | |
}, | |
capacity: { | |
economy: averageFromSubKey(linkHistory, 'capacity', 'economy'), | |
business: averageFromSubKey(linkHistory, 'capacity', 'business'), | |
first: averageFromSubKey(linkHistory, 'capacity', 'first'), | |
} | |
}); | |
var latestLinkData = linkHistory[0] | |
$("#linkHistoryPrice").text(toLinkClassValueString(latestLinkData.price, "$")) | |
$("#linkHistoryCapacity").text(toLinkClassValueString(latestLinkData.capacity)) | |
if (latestLinkData.totalLoadFactor !== 100) { | |
let originalLink = link; | |
//console.dir(originalLink); | |
$("#doAutomaticPriceUpdate").click(() => { | |
_doAutomaticPriceUpdateFor(originalLink); | |
}); | |
$("#doAutomaticPriceUpdate").show(); | |
} else { | |
$("#doAutomaticPriceUpdate").hide(); | |
} | |
$("#linkLoadFactor").text(toLinkClassValueString(getLoadFactorsFor(latestLinkData), "", "%")) | |
$("#linkAverageLoadFactor").text(toLinkClassValueString(averageLoadFactor, "", "%")) | |
const dollarValuesByElementId = { | |
linkProfit: latestLinkData.profit, | |
linkAverageProfit: Math.round(averageFromSubKey(linkHistory, 'profit')), | |
linkRevenue: latestLinkData.revenue, | |
linkFuelCost: latestLinkData.fuelCost, | |
linkCrewCost: latestLinkData.crewCost, | |
linkAirportFees: latestLinkData.airportFees, | |
linkDepreciation: latestLinkData.depreciation, | |
linkCompensation: latestLinkData.delayCompensation, | |
linkLoungeCost: latestLinkData.loungeCost, | |
linkServiceSupplies: latestLinkData.inflightCost, | |
linkMaintenance: latestLinkData.maintenanceCost, | |
}; | |
for (const elementId in dollarValuesByElementId) { | |
$('#'+elementId).text('$' + commaSeparateNumber(dollarValuesByElementId[elementId])); | |
} | |
if (latestLinkData.minorDelayCount == 0 && latestLinkData.majorDelayCount == 0) { | |
$("#linkDelays").removeClass("warning") | |
$("#linkDelays").text("-") | |
} else { | |
$("#linkDelays").addClass("warning") | |
$("#linkDelays").text(latestLinkData.minorDelayCount + " minor " + latestLinkData.majorDelayCount + " major") | |
} | |
if (latestLinkData.cancellationCount == 0) { | |
$("#linkCancellations").removeClass("warning") | |
$("#linkCancellations").text("-") | |
} else { | |
$("#linkCancellations").addClass("warning") | |
$("#linkCancellations").text(latestLinkData.cancellationCount) | |
} | |
enableButton($("#linkDetails .button.viewLinkHistory")) | |
enableButton($("#linkDetails .button.viewLinkComposition")) | |
plotHistory(linkHistory); | |
return linkHistory; | |
} | |
async function loadLink(airlineId, linkId) { | |
const link = await _request(`airlines/${airlineId}/links/${linkId}`) | |
$("#linkFromAirport").attr("onclick", "showAirportDetails(" + link.fromAirportId + ")").html(getCountryFlagImg(link.fromCountryCode) + getAirportText(link.fromAirportCity, link.fromAirportCode)) | |
//$("#linkFromAirportExpectedQuality").attr("onclick", "loadLinkExpectedQuality(" + link.fromAirportId + "," + link.toAirportId + "," + link.fromAirportId + ")") | |
$("#linkToAirport").attr("onclick", "showAirportDetails(" + link.toAirportId + ")").html(getCountryFlagImg(link.toCountryCode) + getAirportText(link.toAirportCity, link.toAirportCode)) | |
//$("#linkToAirportExpectedQuality").attr("onclick", "loadLinkExpectedQuality(" + link.fromAirportId + "," + link.toAirportId + "," + link.toAirportId + ")") | |
$("#linkFlightCode").text(link.flightCode) | |
if (link.assignedAirplanes && link.assignedAirplanes.length > 0) { | |
$('#linkAirplaneModel').text(link.assignedAirplanes[0].airplane.name + "(" + link.assignedAirplanes.length + ")") | |
} else { | |
$('#linkAirplaneModel').text("-") | |
} | |
$("#linkCurrentPrice").text(toLinkClassValueString(link.price, "$")) | |
$("#linkDistance").text(link.distance + " km (" + link.flightType + ")") | |
$("#linkQuality").html(getGradeStarsImgs(Math.round(link.computedQuality / 10)) + link.computedQuality) | |
$("#linkCurrentCapacity").text(toLinkClassValueString(link.capacity)) | |
if (link.future) { | |
$("#linkCurrentDetails .future .capacity").text(toLinkClassValueString(link.future.capacity)) | |
$("#linkCurrentDetails .future").show() | |
} else { | |
$("#linkCurrentDetails .future").hide() | |
} | |
$("#linkCurrentDetails").show() | |
$("#linkToAirportId").val(link.toAirportId) | |
$("#linkFromAirportId").val(link.fromAirportId) | |
const plotUnit = $("#linkDetails #switchMonth").is(':checked') ? window.plotUnitEnum.MONTH : window.plotUnitEnum.QUARTER | |
const cycleCount = plotUnit.maxWeek; | |
const [ | |
linkCompetition, | |
linkHistory, | |
] = await Promise.all([ | |
loadCompetitionForLink(airlineId, link), | |
loadHistoryForLink(airlineId, linkId, cycleCount, link), | |
]) | |
return { | |
link, | |
linkCompetition, | |
linkHistory, | |
}; | |
} | |
async function _updateLatestOilPriceInHeader() { | |
const oilPrices = await _request('oil-prices'); | |
const latestPrice = oilPrices.slice(-1)[0].price; | |
if (!$('.topBarDetails .latestOilPriceShortCut').length) { | |
$('.topBarDetails .delegatesShortcut').after(` | |
<span style="margin: 0px 10px; padding: 0 5px" title="Latest Oil Price" class="latestOilPriceShortCut clickable" onclick="showOilCanvas()"> | |
<span class="latest-price label" style=""></span> | |
</span> | |
`); | |
} | |
const tierForPrice = 5 - getTierFromPercent(latestPrice, 40, 80); | |
if (tierForPrice < 2) { | |
$('.latestOilPriceShortCut') | |
.addClass('glow') | |
.addClass('button'); | |
} else { | |
$('.latestOilPriceShortCut') | |
.removeClass('glow') | |
.removeClass('button'); | |
} | |
$('.topBarDetails .latest-price') | |
.text('$'+commaSeparateNumber(latestPrice)) | |
.attr({style: getStyleFromTier(tierForPrice)}); | |
setTimeout(() => { | |
_updateLatestOilPriceInHeader(); | |
}, Math.round(Math.max(durationTillNextTick / 2, 30000))); | |
} | |
function commaSeparateNumberForLinks(val) { | |
const over1k = val > 1000 || val < -1000; | |
const isNegative = (val < 0); | |
if (val !== 0) { | |
const withDecimal = Math.abs(over1k ? val / 1000 : val); | |
const remainderTenths = Math.round((withDecimal % 1) * 10) / 10; | |
val = Math.floor(withDecimal) + remainderTenths; | |
while (/(\d+)(\d{3})/.test(val.toString())) { | |
val = val.toString().replace(/(\d+)(\d{3})/, '$1'+','+'$2'); | |
} | |
} | |
const valWithSuffix = over1k ? val + 'k' : val; | |
return isNegative ? '(' + valWithSuffix + ')' : valWithSuffix; | |
} | |
function launch(){ | |
window.plotUnitEnum = { | |
"MONTH": { | |
"value": 1, | |
"maxWeek": 104, | |
"weeksPerMark": 4, | |
"maxMark": 28 | |
}, | |
"QUARTER": { | |
"value": 2, | |
"maxWeek": 208, | |
"weeksPerMark": 8, | |
"maxMark": 52 | |
} | |
} | |
window.commaSeparateNumberForLinks = commaSeparateNumberForLinks; | |
var cachedTotalsById = window.cachedTotalsById = {}; | |
window.cachedTotalsById = cachedTotalsById; | |
window.loadAirplaneModelStats = async function loadAirplaneModelStats(modelInfo, opts = {}) { | |
var url | |
var favoriteIcon = $("#airplaneModelDetail .favorite") | |
var model = loadedModelsById[modelInfo.id] | |
if (activeAirline) { | |
url = "airlines/" + activeAirline.id + "/airplanes/model/" + model.id + "/stats", | |
favoriteIcon.show() | |
} else { | |
url = "airplane-models/" + model.id + "/stats" | |
favoriteIcon.hide() | |
} | |
if (opts && opts.totalOnly && model.in_use && model.in_use !== -1) { | |
return; | |
} | |
if (opts && opts.totalOnly && cachedTotalsById[model.id]) { | |
model.in_use = cachedTotalsById[model.id]; | |
return; | |
} | |
const stats = await _request(url); | |
if (opts && opts.totalOnly) { | |
cachedTotalsById[model.id] = model.in_use = stats.total; | |
return; | |
} | |
updateTopOperatorsTable(stats) | |
$('#airplaneCanvas .total').text(stats.total) | |
cachedTotalsById[model.id] = model.in_use = stats.total; | |
if (stats.favorite === undefined) { | |
return; | |
} | |
favoriteIcon.off() //remove all listeners | |
if (stats.favorite.rejection) { | |
$("#setFavoriteModal").data("rejection", stats.favorite.rejection) | |
} else { | |
$("#setFavoriteModal").removeData("rejection") | |
} | |
if (modelInfo.isFavorite) { | |
favoriteIcon.attr("src", "assets/images/icons/heart.png") | |
$("#setFavoriteModal").data("rejection", "This is already the Favorite") | |
} else { | |
favoriteIcon.attr("src", "assets/images/icons/heart-empty.png") | |
} | |
$("#setFavoriteModal").data("model", model) | |
} | |
window.updateCustomLinkTableHeader = function updateCustomLinkTableHeader() { | |
if ($('#linksTableSortHeader').children().length === 15) { | |
return; | |
} | |
$('#linksCanvas .mainPanel').css({width: '62%'}); | |
$('#linksCanvas .sidePanel').css({width: '38%'}); | |
$('#canvas .mainPanel').css({width: '62%'}); | |
$('#canvas .sidePanel').css({width: '38%'}); | |
const widths = [ | |
8, | |
8, | |
8, | |
7, | |
9, | |
5, | |
5, | |
5, | |
9, | |
8, | |
6, | |
6, | |
7, | |
7, | |
2, //tiers, 1st | |
]; | |
const sum = widths.reduce((acc, val) => acc + val, 0); | |
if (sum !== 100) { | |
console.warn(`Column widths to not add up to 100: ${sum} (${widths.join(',')}) -- ${sum < 100 ? 'Remaining' : 'Over by'}: ${sum < 100 ? 100 - sum : sum - 100}%`) | |
} | |
$('#linksTableSortHeader').html(` | |
<div class="cell clickable" style="width: ${widths[14]}%" data-sort-property="tiersRank" data-sort-order="descending" onclick="toggleLinksTableSortOrder($(this))" title="Aggregated Rank">#</div> | |
<div class="cell clickable" style="width: ${widths[0]}%" data-sort-property="fromAirportCode" data-sort-order="descending" onclick="toggleLinksTableSortOrder($(this))">From</div> | |
<div class="cell clickable" style="width: 0%" data-sort-property="lastUpdate" data-sort-order="ascending" id="hiddenLinkSortBy"></div> <!--hidden column for last update (cannot be first otherwise the left round corner would not work --> | |
<div class="cell clickable" style="width: ${widths[1]}%" data-sort-property="toAirportCode" data-sort-order="descending" onclick="toggleLinksTableSortOrder($(this))">To</div> | |
<div class="cell clickable" style="width: ${widths[2]}%" data-sort-property="model" data-sort-order="ascending" onclick="toggleLinksTableSortOrder($(this))">Model</div> | |
<div class="cell clickable" style="width: ${widths[3]}%" align="right" data-sort-property="distance" data-sort-order="ascending" onclick="toggleLinksTableSortOrder($(this))">Dist.</div> | |
<div class="cell clickable" style="width: ${widths[4]}%" align="right" data-sort-property="totalCapacity" data-sort-order="ascending" onclick="toggleLinksTableSortOrder($(this))">Capacity (Freq.)</div> | |
<div class="cell clickable" style="width: ${widths[5]}%" align="right" data-sort-property="totalPassengers" data-sort-order="ascending" onclick="toggleLinksTableSortOrder($(this))">Pax</div> | |
<div class="cell clickable" style="width: ${widths[6]}%" align="right" data-sort-property="totalLoadFactor" data-sort-order="ascending" onclick="toggleLinksTableSortOrder($(this))" title="Load Factor">LF</div> | |
<div class="cell clickable" style="width: ${widths[7]}%" align="right" data-sort-property="satisfaction" data-sort-order="ascending" onclick="toggleLinksTableSortOrder($(this))" title="Satisfaction Factor">SF</div> | |
<div class="cell clickable" style="width: ${widths[8]}%" align="right" data-sort-property="revenue" data-sort-order="ascending" onclick="toggleLinksTableSortOrder($(this))">Revenue</div> | |
<div class="cell clickable" style="width: ${widths[9]}%" align="right" data-sort-property="profit" data-sort-order="descending" onclick="toggleLinksTableSortOrder($(this))">Profit</div> | |
<div class="cell clickable" style="width: ${widths[10]}%" align="right" data-sort-property="profitMargin" data-sort-order="ascending" onclick="toggleLinksTableSortOrder($(this))">Gain</div> | |
<div class="cell clickable" style="width: ${widths[11]}%" align="right" data-sort-property="profitPerPax" data-sort-order="ascending" onclick="toggleLinksTableSortOrder($(this))">$/🧍</div> | |
<div class="cell clickable" style="width: ${widths[12]}%" align="right" data-sort-property="profitPerFlight" data-sort-order="ascending" onclick="toggleLinksTableSortOrder($(this))">$/✈</div> | |
<div class="cell clickable" style="width: ${widths[13]}%" align="right" data-sort-property="profitPerHour" data-sort-order="ascending" onclick="toggleLinksTableSortOrder($(this))">$/⏲</div> | |
`); | |
$('#linksTable .table-header').html(` | |
<div class="cell" style="width: ${widths[14]}%; border-bottom: none;"></div> | |
<div class="cell" style="width: ${widths[0]}%; border-bottom: none;"></div> | |
<div class="cell" style="width: ${widths[1]}%; border-bottom: none;"></div> | |
<div class="cell" style="width: ${widths[2]}%; border-bottom: none;"></div> | |
<div class="cell" style="width: ${widths[3]}%; border-bottom: none;"></div> | |
<div class="cell" style="width: ${widths[4]}%; border-bottom: none;"></div> | |
<div class="cell" style="width: ${widths[5]}%; border-bottom: none;"></div> | |
<div class="cell" style="width: ${widths[6]}%; border-bottom: none;"></div> | |
<div class="cell" style="width: ${widths[7]}%; border-bottom: none;"></div> | |
<div class="cell" style="width: ${widths[8]}%; border-bottom: none;"></div> | |
<div class="cell" style="width: ${widths[9]}%; border-bottom: none;"></div> | |
<div class="cell" style="width: ${widths[10]}%; border-bottom: none;"></div> | |
<div class="cell" style="width: ${widths[11]}%; border-bottom: none;"></div> | |
<div class="cell" style="width: ${widths[12]}%; border-bottom: none;"></div> | |
<div class="cell" style="width: ${widths[13]}%; border-bottom: none;"></div> | |
`); | |
} | |
window.loadLinksTable = async function loadLinksTable() { | |
const links = await _request(`airlines/${activeAirline.id}/links-details`); | |
updateCustomLinkTableHeader(); | |
updateLoadedLinks(links); | |
$.each(links, (key, link) => _populateDerivedFieldsOnLink(link)); | |
var selectedSortHeader = $('#linksTableSortHeader .cell.selected') | |
updateLinksTable(selectedSortHeader.data('sort-property'), selectedSortHeader.data('sort-order')) | |
} | |
var colorKeyMaps = {}; | |
window.updateLinksTable = function updateLinksTable(sortProperty, sortOrder) { | |
var linksTable = $("#linksCanvas #linksTable") | |
linksTable.children("div.table-row").remove() | |
loadedLinks = sortPreserveOrder(loadedLinks, sortProperty, sortOrder == "ascending") | |
function getKeyedStyleFromLink(link, keyName, ...args) { | |
if (!colorKeyMaps[keyName]) { | |
colorKeyMaps[keyName] = new WeakMap(); | |
} else if (colorKeyMaps[keyName].has(link)) { | |
return colorKeyMaps[keyName].get(link); | |
} | |
var data = loadedLinks.map(l => l[keyName]); | |
var avg = data.reduce((sum, acc) => sum += acc, 0) / loadedLinks.length; | |
var max = Math.max(...data); | |
var min = Math.max(Math.min(...data), 0); | |
var tier = getTierFromPercent(link[keyName], args[0] !== undefined ? args[0] : min, args[1] || (avg * .618)); | |
if (!link.tiers) { | |
link.tiers = {}; | |
} | |
link.tiers[keyName] = tier; | |
var colorResult = getStyleFromTier(tier); | |
colorKeyMaps[keyName].set(link, colorResult); | |
return colorResult; | |
} | |
$.each(loadedLinks, function(index, link) { | |
var row = $("<div class='table-row clickable' onclick='selectLinkFromTable($(this), " + link.id + ")'></div>") | |
var srcAirportFull = getAirportText(link.fromAirportCity, link.fromAirportCode); | |
var destAirportFull = getAirportText(link.toAirportCity, link.toAirportCode); | |
// COMMENT one set or the other to test both: | |
// Truncated | |
// | |
row.append("<div class='cell' title='"+ srcAirportFull +"'>" + getCountryFlagImg(link.fromCountryCode) + ' ' + srcAirportFull.slice(-4, -1) + "</div>") | |
row.append("<div class='cell' title='"+ destAirportFull +"'>" + getCountryFlagImg(link.toCountryCode) + ' ' + destAirportFull.slice(-4, -1) + "</div>") | |
// | |
// OR | |
// | |
// Original/Full airport names | |
// | |
//row.append("<div class='cell'>" + getCountryFlagImg(link.fromCountryCode) + ' ' + srcAirportFull + "</div>") | |
//row.append("<div class='cell'>" + getCountryFlagImg(link.toCountryCode) + ' ' + destAirportFull + "</div>") | |
// | |
// OR | |
// | |
// Reversed, IATA/ICAO first w/ truncation | |
// | |
//row.append("<div class='cell' style='text-overflow: ellipsis;overflow: hidden;white-space: pre;' title='"+ srcAirportFull +"'>" + getCountryFlagImg(link.fromCountryCode) + ' ' + srcAirportFull.slice(-4, -1) + ' | ' + srcAirportFull.slice(0, -5) + "</div>") | |
//row.append("<div class='cell' style='text-overflow: ellipsis;overflow: hidden;white-space: pre;' title='"+ destAirportFull +"'>" + getCountryFlagImg(link.toCountryCode) + ' ' + destAirportFull.slice(-4, -1) + ' | ' + destAirportFull.slice(0, -5) + "</div>") | |
// | |
row.append("<div class='cell' style='text-overflow: ellipsis;overflow: hidden;white-space: pre;'>" + getShortModelName(link.model) + "</div>") | |
row.append("<div class='cell' align='right'>" + link.distance + "km</div>") | |
row.append("<div class='cell' align='right'>" + link.totalCapacity + " (" + link.frequency + ")</div>") | |
row.append("<div class='cell' align='right'>" + link.totalPassengers + "</div>") | |
// row.append("<div style='"+getKeyedStyleFromLink(link, 'totalLoadFactor', 0, 100)+"' class='cell' align='right'>" + link.totalLoadFactor + '%' + "</div>") | |
const lfBreakdown = { | |
economy: link.passengers.economy / link.capacity.economy, | |
business: link.passengers.business / link.capacity.business, | |
first: link.passengers.first / link.capacity.first, | |
}; | |
lfBreakdownText = link.totalLoadFactor === 100 | |
? '100' | |
: [lfBreakdown.economy, lfBreakdown.business, lfBreakdown.first].map(v => v ? Math.floor(100 * v) : '-').join('/') | |
row.append("<div style='"+getKeyedStyleFromLink(link, 'totalLoadFactor', 0, 100)+"' class='cell' align='right'>" + lfBreakdownText + '%' + "</div>") | |
row.append("<div style='"+getKeyedStyleFromLink(link, 'satisfaction', 0, 1)+"' class='cell' align='right'>" + Math.round(link.satisfaction * 100) + '%' + "</div>") | |
row.append("<div style='"+getKeyedStyleFromLink(link, 'revenue')+"' class='cell' align='right' title='$"+ commaSeparateNumber(link.revenue) +"'>" + '$' + commaSeparateNumberForLinks(link.revenue) + "</div>") | |
row.append("<div style='"+getKeyedStyleFromLink(link, 'profit')+"' class='cell' align='right' title='$"+ commaSeparateNumber(link.profit) +"'>" + '$' + commaSeparateNumberForLinks(link.profit) +"</div>") | |
//row.append("<div style='color:"+textColor+";' class='cell' align='right'>" + (link.profitMargin > 0 ? '+' : '') + Math.round(link.profitMargin) + "%</div>") | |
row.append("<div style='"+getKeyedStyleFromLink(link, 'profitMarginPercent', 0, 136.5)+"' class='cell' align='right'>" + (link.profitMargin > 0 ? '+' : '') + Math.round(link.profitMargin) + "%</div>") | |
row.append("<div style='"+getKeyedStyleFromLink(link, 'profitPerPax')+"' class='cell' align='right' title='$"+ commaSeparateNumber(link.profitPerPax) +"'>" + '$' + commaSeparateNumberForLinks(link.profitPerPax) + "</div>") | |
row.append("<div style='"+getKeyedStyleFromLink(link, 'profitPerFlight')+"' class='cell' align='right' title='$"+ commaSeparateNumber(link.profitPerFlight) +"'>" + '$' + commaSeparateNumberForLinks(link.profitPerFlight) + "</div>") | |
row.append("<div style='"+getKeyedStyleFromLink(link, 'profitPerHour')+"' class='cell' align='right' title='$"+ commaSeparateNumber(link.profitPerHour) +"'>" + '$' + commaSeparateNumberForLinks(link.profitPerHour) + "</div>") | |
if (selectedLink == link.id) { | |
row.addClass("selected") | |
} | |
const tiersRank = link.tiersRank = Object.keys(link.tiers).reduce((sum, key) => sum + link.tiers[key] + (key === 'profit' && link.tiers[key] === 0 ? -1 : 0), 0); | |
row.prepend("<div class='cell'>" + link.tiersRank + "</div>") | |
if (tiersRank < 2) { | |
row.css({'text-shadow': '0 0 3px gold'}); | |
} | |
if (tiersRank > 27) { | |
row.css({'text-shadow': '0 0 3px red'}); | |
} | |
linksTable.append(row) | |
}); | |
} | |
window.refreshLinkDetails = async function refreshLinkDetails(linkId) { | |
const airlineId = activeAirline.id | |
$("#linkCompetitons .data-row").remove() | |
$("#actionLinkId").val(linkId) | |
// load link | |
const linkDetailsPromise = loadLink(airlineId, linkId); // not awaiting yet so we can kickoff the panel open animation while loading | |
setActiveDiv($("#linkDetails")) | |
hideActiveDiv($("#extendedPanel #airplaneModelDetails")) | |
$('#sidePanel').fadeIn(200); | |
const { link, linkCompetition, linkHistory } = await linkDetailsPromise; // link details loaded if needed for something later | |
} | |
function _addAllianceTooltipsToMap(airportMarkers) { | |
//now add extra listener for alliance airports | |
$.each(airportMarkers, function(key, marker) { | |
marker.addListener('mouseover', function(event) { | |
closeAlliancePopups() | |
var baseInfo = marker.baseInfo | |
$("#allianceBasePopup .city").html(getCountryFlagImg(baseInfo.countryCode) + " " + baseInfo.city) | |
$("#allianceBasePopup .airportName").text(baseInfo.airportName) | |
$("#allianceBasePopup .iata").html(baseInfo.airportCode) | |
$("#allianceBasePopup .airlineName").html(getAirlineLogoImg(baseInfo.airlineId) + " " + baseInfo.airlineName) | |
$("#allianceBasePopup .baseScale").html(baseInfo.scale) | |
var infoWindow = new google.maps.InfoWindow({ maxWidth : 1200}); | |
var popup = $("#allianceBasePopup").clone() | |
popup.show() | |
infoWindow.setContent(popup[0]) | |
//infoWindow.setPosition(event.latLng); | |
infoWindow.open(map, marker); | |
map.allianceBasePopup = infoWindow | |
}) | |
marker.addListener('mouseout', function(event) { | |
closeAlliancePopups() | |
}) | |
}) | |
switchMap(); | |
$("#worldMapCanvas").data("initCallback", function() { //if go back to world map, re-init the map | |
map.controls[google.maps.ControlPosition.TOP_CENTER].clear() | |
clearAllPaths() | |
updateAirportMarkers(activeAirline) | |
updateLinksInfo() //redraw all flight paths | |
closeAlliancePopups() | |
}) | |
window.setTimeout(addExitButton , 1000); //delay otherwise it doesn't push to center | |
} | |
window.showAllianceMap = async function showAllianceMap() { | |
clearAllPaths() | |
deselectLink() | |
var alliancePaths = [] | |
$('body .loadingSpinner').show() | |
const result = await _request(`alliances/${selectedAlliance.id}/details`); | |
$('body .loadingSpinner').hide() | |
$.each(result.links, function(index, link) { | |
alliancePaths.push(drawAllianceLink(link)) | |
}) | |
var allianceBases = [] | |
$.each(result.members, function(index, airline) { | |
if (airline.role != "APPLICANT") { | |
$.merge(allianceBases, airline.bases) | |
} | |
}) | |
window.lastAllianceInfo = { | |
allianceBases, | |
alliancePaths, | |
updateAirportBaseMarkers: () => { | |
var markers = updateAirportBaseMarkers(allianceBases, alliancePaths); | |
_addAllianceTooltipsToMap(markers); | |
} | |
}; | |
} | |
_updateLatestOilPriceInHeader(); | |
}; | |
$(document).ready(() => setTimeout(() => launch(), 1000)); | |
// Begin Cost per PAX | |
// Begin Cost per PAX | |
// Begin Cost per PAX | |
// Begin Cost per PAX | |
console.log("Plane score script loading"); | |
function calcFlightTime(plane, distance){ | |
let min = Math.min; | |
let max = Math.max; | |
let speed = plane.speed * (plane.airplaneType.toUpperCase() == "SUPERSONIC" ? 1.5 : 1); | |
let a = min(distance, 300); | |
let b = min(max(0, distance-a), 400); | |
let c = min(max(0, distance-(a+b)), 400); | |
let d = max(0, distance-(a+b+c)); | |
let time_flight = a / min(speed, 350) + b / min(speed, 500) + c / min(speed, 700) + d / speed; | |
return time_flight * 60; | |
} | |
function calcFuelBurn(plane, distance){ | |
let timeFlight = calcFlightTime(plane, distance); | |
if (timeFlight > 1.5){ | |
return plane.fuelBurn * (405 + timeFlight); | |
} else { | |
return plane.fuelBurn * timeFlight * 5.5; | |
} | |
} | |
let initialAirplaneModelStatsLoading = true; | |
window.updateAirplaneModelTable = function(sortProperty, sortOrder) { | |
let distance = parseInt($("#fightRange").val(), 10); | |
let runway = parseInt($("#runway").val(), 10); | |
let min_capacity = parseInt($("#min_capacity").val(), 10); | |
let min_circulation = parseInt($("#min_circulation").val(), 10); | |
let owned_only = document.getElementById("owned_only").checked; | |
let use_flight_total =document.getElementById("use_flight_total").checked; | |
for (let plane of loadedModelsOwnerInfo) { | |
plane.isOwned = ((plane.assignedAirplanes.length + plane.availableAirplanes.length + plane.constructingAirplanes.length) !== 0); | |
if(plane.range < distance || plane.runwayRequirement > runway) { | |
plane.cpp = -1; | |
plane.max_rotation = -1; | |
//continue; | |
} | |
var plane_category = -1; | |
switch (plane.airplaneType.toUpperCase()) { | |
case 'LIGHT': | |
case 'SMALL' :plane_category=1;break; | |
case 'REGIONAL' : plane_category=3;break; | |
case 'MEDIUM' : plane_category=8;break; | |
case 'LARGE' : plane_category=12;break; | |
case 'EXTRA LARGE' : | |
case 'X_LARGE' : plane_category=15;break; | |
case 'JUMBO' : plane_category=18;break; | |
case 'SUPERSONIC' : plane_category=12 ;break; | |
} | |
let flightDuration = calcFlightTime(plane, distance) ; | |
let price = plane.price; | |
if( plane.originalPrice){ | |
price = plane.originalPrice; | |
} | |
let maxFlightMinutes = 4 * 24 * 60; | |
let frequency = Math.floor(maxFlightMinutes / ((flightDuration + plane.turnaroundTime)*2)); | |
let flightTime = frequency * 2 * (flightDuration + plane.turnaroundTime); | |
let availableFlightMinutes = maxFlightMinutes - flightTime; | |
let utilisation = flightTime / (maxFlightMinutes - availableFlightMinutes); | |
let planeUtilisation = (maxFlightMinutes - availableFlightMinutes) / maxFlightMinutes; | |
let decayRate = 100 / (plane.lifespan * 3) * (1 + 2 * planeUtilisation); | |
let depreciationRate = Math.floor(price * (decayRate / 100) * utilisation); | |
let maintenance = plane.capacity * 100 * utilisation; | |
let airport_fee = (500 * plane_category + plane.capacity * 10) * 2; | |
let crew_cost = plane.capacity * (flightDuration / 60) * 12 ; | |
let inflight_cost = (20 + 8 * flightDuration / 60) * plane.capacity * 2; | |
plane.max_rotation = frequency; | |
plane.fbpf = calcFuelBurn(plane, distance); | |
plane.fbpp = plane.fbpf / plane.capacity; | |
plane.fbpw = plane.fbpf * plane.max_rotation; | |
plane.fuel_total = ((plane.fbpf * 0.08 + airport_fee + inflight_cost + crew_cost) * plane.max_rotation + depreciationRate + maintenance); | |
plane.cpp = plane.fuel_total / (plane.capacity * plane.max_rotation); | |
plane.max_capacity = plane.capacity * plane.max_rotation; | |
plane.discountPercent = (plane.originalPrice) ? Math.round(100 - (plane.price / plane.originalPrice * 100)) : 0; | |
if (!plane.in_use) { | |
plane.in_use = -1; | |
loadAirplaneModelStats(plane, {totalOnly: true}).then(() => { | |
// This could probably be in a debounce but I'm cool with this for a final reload once stats are done. | |
if (!initialAirplaneModelStatsLoading) { | |
return; | |
} | |
if (window.cachedTotalsById && Object.keys(window.cachedTotalsById).length === loadedModelsOwnerInfo.length) { | |
initialAirplaneModelStatsLoading = false; | |
updateAirplaneModelTable(); | |
} | |
}); | |
} | |
plane.shouldShow = ((plane.cpp === -1) | |
|| (plane.max_capacity < min_capacity) | |
|| (plane.range < distance) | |
|| (plane.runwayRequirement > runway) | |
|| (plane.in_use < min_circulation && !plane.isOwned) | |
|| (owned_only && !plane.isOwned)) === false; | |
} | |
if (!sortProperty && !sortOrder) { | |
var selectedSortHeader = $('#airplaneModelSortHeader .cell.selected') | |
sortProperty = selectedSortHeader.data('sort-property') | |
if (sortProperty === 'capacity') { | |
sortProperty = 'max_capacity'; | |
} else if (sortProperty === 'cpp' && use_flight_total) { | |
sortProperty = 'fuel_total'; | |
} | |
sortOrder = selectedSortHeader.data('sort-order') | |
} | |
//sort the list | |
loadedModelsOwnerInfo.sort(sortByProperty(sortProperty, sortOrder == "ascending")); | |
var airplaneModelTable = $("#airplaneModelTable") | |
airplaneModelTable.children("div.table-row").remove() | |
var cppValues = loadedModelsOwnerInfo.filter(l => l.shouldShow).map(l => l.cpp); | |
var cppMax = Math.max(...cppValues); | |
var cppMin = Math.max(Math.min(...cppValues), 0); | |
$.each(loadedModelsOwnerInfo, function(index, modelOwnerInfo) { | |
if (!modelOwnerInfo.shouldShow) { | |
return; | |
} | |
var row = $("<div class='table-row clickable' style='"+ (modelOwnerInfo.isOwned ? "background: green;" : '') +"' data-model-id='" + modelOwnerInfo.id + "' onclick='selectAirplaneModel(loadedModelsById[" + modelOwnerInfo.id + "])'></div>") | |
if (modelOwnerInfo.isFavorite) { | |
row.append("<div class='cell'>" + modelOwnerInfo.name + "<img src='assets/images/icons/heart.png' height='10px'></div>") | |
} else { | |
row.append("<div class='cell'>" + modelOwnerInfo.name + "</div>") | |
} | |
row.append("<div class='cell' style='text-overflow: ellipsis;text-wrap: nowrap;overflow: clip;' title='"+modelOwnerInfo.family+"'>" + modelOwnerInfo.family + "</div>") | |
row.append("<div class='cell' align='right'>" + commaSeparateNumber(modelOwnerInfo.price) + "</div>") | |
row.append("<div class='cell' align='right'>" + modelOwnerInfo.capacity + " (" + (modelOwnerInfo.capacity * modelOwnerInfo.max_rotation) + ")</div>") | |
row.append("<div class='cell' align='right'>" + modelOwnerInfo.range + " km</div>") | |
row.append("<div class='cell' align='right'>" + modelOwnerInfo.fuelBurn + "</div>") | |
row.append("<div class='cell' align='right'>" + modelOwnerInfo.lifespan / 52 + " yrs</div>") | |
row.append("<div class='cell' align='right'>" + modelOwnerInfo.speed + " km/h</div>") | |
row.append("<div class='cell' align='right'>" + modelOwnerInfo.runwayRequirement + " m</div>") | |
row.append("<div class='cell' align='right'>" + modelOwnerInfo.assignedAirplanes.length + "/" + modelOwnerInfo.availableAirplanes.length + "/" + modelOwnerInfo.constructingAirplanes.length + "</div>") | |
row.append("<div class='cell' align='right'>" + modelOwnerInfo.max_rotation + "</div>") | |
row.append("<div class='cell' align='right' style='"+ getStyleFromTier(getTierFromPercent(-1*modelOwnerInfo.cpp, -1*cppMax, -1*cppMin)) +"' title='"+commaSeparateNumber(Math.round(modelOwnerInfo.fuel_total))+"/total ("+commaSeparateNumber(Math.round(modelOwnerInfo.cpp * modelOwnerInfo.capacity))+"/flight)'>" + commaSeparateNumber(Math.round(modelOwnerInfo.cpp)) + "</div>") | |
let discountTier; | |
if (modelOwnerInfo.discountPercent > 40) { | |
discountTier = 0; | |
} else if (modelOwnerInfo.discountPercent > 10) { | |
discountTier = 1; | |
} else if (modelOwnerInfo.discountPercent > 0) { | |
discountTier = 2; | |
} else { | |
discountTier = 3; | |
} | |
row.append("<div class='cell' align='right' style='"+ getStyleFromTier(discountTier) +"' >" + modelOwnerInfo.discountPercent + "</div>") | |
row.append("<div class='cell' style='"+ (modelOwnerInfo.in_use >= MIN_PLANES_TO_HIGHLIGHT ? "text-shadow: gold 0px 0px 3px;" : '') +"' align='right'>" + modelOwnerInfo.in_use + "</div>") | |
if (selectedModelId == modelOwnerInfo.id) { | |
row.addClass("selected") | |
selectAirplaneModel(modelOwnerInfo) | |
} | |
airplaneModelTable.append(row) | |
}); | |
} | |
const columnWidthPercents = [ | |
17, | |
9, | |
8, | |
7, | |
7, | |
7, | |
7, | |
9, | |
7, | |
6, | |
3, | |
5, | |
4, | |
4 | |
]; | |
if (columnWidthPercents.reduce((sum, val) => sum += val, 0) !== 100) { | |
console.warn('Column widths do not equal 100%, widths:', columnWidthPercents); | |
} | |
$("#airplaneModelSortHeader").append("<div class=\"cell clickable\" title=\"Max flight rotations (uses user-set distance above)\" data-sort-property=\"max_rotation\" data-sort-order=\"ascending\" onclick=\"toggleAirplaneModelTableSortOrder($(this))\" align=\"right\">⏲</div>"); | |
$("#airplaneModelSortHeader").append("<div class=\"cell clickable\" title=\"Cost Per Pax\" data-sort-property=\"cpp\" data-sort-order=\"ascending\" onclick=\"toggleAirplaneModelTableSortOrder($(this))\" align=\"right\">$/🧍</div>"); | |
$("#airplaneModelSortHeader").append("<div class=\"cell clickable\" title=\"Discount Percent (influcenced by demand & brand loyalties)\" data-sort-property=\"discountPercent\" data-sort-order=\"descending\" onclick=\"toggleAirplaneModelTableSortOrder($(this))\" align=\"right\">%🔽</div>"); | |
$("#airplaneModelSortHeader").append("<div class=\"cell clickable\" title=\"Total number in circulation (all players, game wide)\" data-sort-property=\"in_use\" data-sort-order=\"ascending\" onclick=\"toggleAirplaneModelTableSortOrder($(this))\" align=\"right\">#✈</div>"); | |
const headerCells = document.querySelectorAll('#airplaneModelSortHeader .cell'); | |
for (var i = 0; i < headerCells.length; i++) { | |
headerCells[i].style = `width: ${columnWidthPercents[i]}%` | |
} | |
$('#airplaneModelTable .table-header').html(` | |
<div class="cell" style="width: ${columnWidthPercents[0]}%; border-bottom: none;"></div> | |
<div class="cell" style="width: ${columnWidthPercents[1]}%; border-bottom: none;"></div> | |
<div class="cell" style="width: ${columnWidthPercents[2]}%; border-bottom: none;"></div> | |
<div class="cell" style="width: ${columnWidthPercents[3]}%; border-bottom: none;"></div> | |
<div class="cell" style="width: ${columnWidthPercents[4]}%; border-bottom: none;"></div> | |
<div class="cell" style="width: ${columnWidthPercents[5]}%; border-bottom: none;"></div> | |
<div class="cell" style="width: ${columnWidthPercents[6]}%; border-bottom: none;"></div> | |
<div class="cell" style="width: ${columnWidthPercents[7]}%; border-bottom: none;"></div> | |
<div class="cell" style="width: ${columnWidthPercents[8]}%; border-bottom: none;"></div> | |
<div class="cell" style="width: ${columnWidthPercents[9]}%; border-bottom: none;"></div> | |
<div class="cell" style="width: ${columnWidthPercents[10]}%; border-bottom: none;"></div><!-- New columns --> | |
<div class="cell" style="width: ${columnWidthPercents[11]}%; border-bottom: none;"></div><!-- New columns --> | |
<div class="cell" style="width: ${columnWidthPercents[12]}%; border-bottom: none;"></div><!-- New columns --> | |
<div class="cell" style="width: ${columnWidthPercents[13]}%; border-bottom: none;"></div><!-- New columns --> | |
`); | |
$("#airplaneCanvas .mainPanel .section .table .table-header:first").append(` | |
<div class="cell detailsSelection">Distance: <input type="text" id="fightRange" value="${DEFAULT_MIN_FLIGHT_RANGE_FILTER}" /></div> | |
<div class="cell detailsSelection">Runway length: <input type="text" id="runway" value="${DEFAULT_RUNWAY_LENGTH_FILTER}" /></div> | |
<div class="cell detailsSelection">Min. Capacity: <input type="text" id="min_capacity" value="${DEFAULT_MIN_CAPACITY_FILTER}" /></div> | |
<div class="cell detailsSelection">Min. Circulation: <input type="text" id="min_circulation" value="${DEFAULT_MIN_PLANES_IN_CIRCULATION_FILTER}" /></div> | |
<div class="cell detailsSelection" style="min-width: 160px; text-align:right"> | |
<label for="owned_only">Owned Only <input type="checkbox" id="owned_only" /></label> | |
<label for="use_flight_total">Flight Fuel Total <input type="checkbox" id="use_flight_total" /></label> | |
</div> | |
`); | |
$("#airplaneCanvas .mainPanel .section .detailsGroup .market.details").attr({style: 'width: 100%; height: calc(100% - 30px); display: block;'}); | |
$('[data-sort-property="totalOwned"]').text('Owned') | |
$('[data-sort-property="totalOwned"]').attr({style: 'width: 6%;'}); | |
var newDataFilterElements = [ | |
'#fightRange', | |
'#runway', | |
'#min_capacity', | |
'#min_circulation', | |
'#owned_only', | |
'#use_flight_total', | |
] | |
for (var el of newDataFilterElements) { | |
$(el).change(function(){window.updateAirplaneModelTable()}); | |
} | |
//* Link Cost Preview | |
let _updatePlanLinkInfo = window.updatePlanLinkInfo; | |
let _updateTotalValues = window.updateTotalValues; | |
let activeLink; | |
let idFrom = -1; | |
let idTo = -1; | |
let airportFrom; | |
let airportTo; | |
let _modelId = -1; | |
let observer = new MutationObserver(function(mutations) { | |
updateModelInfo(_modelId); | |
}); | |
observer.observe( | |
document.getElementById('planLinkServiceLevel'), { | |
attributes: true, | |
attributeFilter: ['value'] | |
} | |
); | |
window.updateTotalValues = function(){ | |
_updateTotalValues(); | |
window.updateModelInfo(_modelId); | |
} | |
window.updatePlanLinkInfo = function(linkInfo){ | |
//console.log(linkInfo); | |
activeLink = linkInfo; | |
for (let model of activeLink.modelPlanLinkInfo){ | |
for (let airplane of model.airplanes){ | |
airplane.airplane.frequency = airplane.frequency; | |
} | |
} | |
if (idFrom != linkInfo.fromAirportId){ | |
idFrom = linkInfo.fromAirportId | |
$.ajax({ | |
url:"airports/" + linkInfo.fromAirportId, | |
async : false, | |
success: function(result){airportFrom = result}, | |
}); | |
} | |
if (idTo != linkInfo.toAirportId){ | |
idTo = linkInfo.toAirportId | |
$.ajax({ | |
url:"airports/" + linkInfo.toAirportId, | |
async : false, | |
success: function(result){airportTo = result}, | |
}); | |
} | |
_updatePlanLinkInfo(linkInfo); | |
} | |
let _updateModelInfo = window.updateModelInfo; | |
window.updateModelInfo = function(modelId) { | |
if (_modelId != modelId){ | |
_updateModelInfo(modelId); | |
} | |
_modelId = modelId; | |
let model = loadedModelsById[modelId]; | |
let linkModel = activeLink.modelPlanLinkInfo.find(plane => plane.modelId == modelId); | |
//console.log({loadedModelsById, model, linkModel}) | |
let serviceLevel = parseInt($("#planLinkServiceLevel").val()); | |
let frequency = 0; | |
let plane_category = 0; | |
switch (model.airplaneType.toUpperCase()) { | |
case 'LIGHT': | |
case 'SMALL' :plane_category=1;break; | |
case 'REGIONAL' : plane_category=3;break; | |
case 'MEDIUM' : plane_category=8;break; | |
case 'LARGE' : plane_category=12;break; | |
case 'EXTRA LARGE' : | |
case 'X_LARGE' : plane_category=15;break; | |
case 'JUMBO' : plane_category=18;break; | |
case 'SUPERSONIC' : plane_category=12 ;break; | |
default: console.error("CPP E1:updateAirplaneModelTable unknown airplane type: " + model.airplaneType); | |
} | |
let baseSlotFee = 0; | |
switch (airportFrom.size){ | |
case 1 : | |
case 2 : baseSlotFee=50;break; | |
case 3 : baseSlotFee=80;break; | |
case 4 : baseSlotFee=150;break; | |
case 5 : baseSlotFee=250;break; | |
case 6 : baseSlotFee=350;break; | |
default: baseSlotFee=500;break; | |
} | |
switch (airportTo.size){ | |
case 1 : | |
case 2 : baseSlotFee+=50;break; | |
case 3 : baseSlotFee+=80;break; | |
case 4 : baseSlotFee+=150;break; | |
case 5 : baseSlotFee+=250;break; | |
case 6 : baseSlotFee+=350;break; | |
default: baseSlotFee+=500;break; | |
} | |
let serviceLevelCost = 1; | |
switch (serviceLevel) { | |
case 2:serviceLevelCost=4;break; | |
case 3:serviceLevelCost=8;break; | |
case 4:serviceLevelCost=13;break; | |
case 5:serviceLevelCost=20;break; | |
} | |
let basic = 0; | |
let multiplyFactor = 2; | |
if (airportFrom.countryCode == airportTo.countryCode) { | |
if (activeLink.distance <= 1000) { | |
basic = 8; | |
} else if (activeLink.distance <= 3000) { | |
basic = 10; | |
} else { | |
basic = 12; | |
} | |
} else if (airportFrom.zone == airportTo.zone){ | |
if (activeLink.distance <= 2000) { | |
basic = 10; | |
} else if (activeLink.distance <= 4000) { | |
basic = 15; | |
} else { | |
basic = 20; | |
} | |
} else { | |
if (activeLink.distance <= 2000) { | |
basic = 15; | |
multiplyFactor = 3; | |
} else if (activeLink.distance <= 5000) { | |
basic = 25; | |
multiplyFactor = 3; | |
} else if (activeLink.distance <= 12000) { | |
basic = 30; | |
multiplyFactor = 4; | |
} else { | |
basic = 30; | |
multiplyFactor = 4; | |
} | |
} | |
let staffPerFrequency = multiplyFactor * 0.4; | |
let staffPer1000Pax = multiplyFactor; | |
let durationInHour = linkModel.duration / 60; | |
let price = model.price; | |
if( model.originalPrice){ | |
price = model.originalPrice; | |
} | |
let baseDecayRate = 100 / model.lifespan; | |
let maintenance = 0; | |
let depreciationRate = 0; | |
for (let row of $(".frequencyDetail .airplaneRow")) { | |
let airplane = $(row).data("airplane"); | |
let freq = parseInt($(row).children(".frequency").val()); | |
let futureFreq = freq - airplane.frequency; | |
let flightTime = freq * 2 * (linkModel.duration + model.turnaroundTime); | |
let availableFlightMinutes = airplane.availableFlightMinutes - (futureFreq * 2 * (linkModel.duration + model.turnaroundTime)); | |
let utilisation = flightTime / (airplane.maxFlightMinutes - availableFlightMinutes); | |
let planeUtilisation = (airplane.maxFlightMinutes - availableFlightMinutes) / airplane.maxFlightMinutes; | |
let decayRate = 100 / (model.lifespan * 3) * (1 + 2 * planeUtilisation); | |
depreciationRate += Math.floor(price * (decayRate / 100) * utilisation); | |
maintenance += model.capacity * 100 * utilisation; | |
frequency += freq; | |
} | |
if (frequency == 0){ | |
let maxFlightMinutes = 4 * 24 * 60; | |
frequency = Math.floor(maxFlightMinutes / ((linkModel.duration + model.turnaroundTime)*2)); | |
let flightTime = frequency * 2 * (linkModel.duration + model.turnaroundTime); | |
let availableFlightMinutes = maxFlightMinutes - flightTime; | |
let utilisation = flightTime / (maxFlightMinutes - availableFlightMinutes); | |
let planeUtilisation = (maxFlightMinutes - availableFlightMinutes) / maxFlightMinutes; | |
let decayRate = 100 / (model.lifespan * 3) * (1 + 2 * planeUtilisation); | |
depreciationRate += Math.floor(price * (decayRate / 100) * utilisation); | |
maintenance += model.capacity * 100 * utilisation; | |
} | |
let fuelCost = frequency; | |
if (linkModel.duration <= 90){ | |
fuelCost *= model.fuelBurn * linkModel.duration * 5.5 * 0.08; | |
}else{ | |
fuelCost *= model.fuelBurn * (linkModel.duration + 405) * 0.08; | |
} | |
let crewCost = model.capacity * durationInHour * 12 * frequency; | |
let airportFees = (baseSlotFee * plane_category + (Math.min(3, airportTo.size) + Math.min(3, airportFrom.size)) * model.capacity) * frequency; | |
let servicesCost = (20 + serviceLevelCost * durationInHour) * model.capacity * 2 * frequency; | |
let cost = fuelCost + crewCost + airportFees + depreciationRate + servicesCost + maintenance; | |
let staffTotal = Math.floor(basic + staffPerFrequency * frequency + staffPer1000Pax * model.capacity * frequency / 1000); | |
$('#airplaneModelDetails #FCPF').text("$" + commaSeparateNumber(Math.floor(fuelCost))); | |
$('#airplaneModelDetails #CCPF').text("$" + commaSeparateNumber(Math.floor(crewCost))); | |
$('#airplaneModelDetails #AFPF').text("$" + commaSeparateNumber(airportFees)); | |
$('#airplaneModelDetails #depreciation').text("$" + commaSeparateNumber(Math.floor(depreciationRate))); | |
$('#airplaneModelDetails #SSPF').text("$" + commaSeparateNumber(Math.floor(servicesCost))); | |
$('#airplaneModelDetails #maintenance').text("$" + commaSeparateNumber(Math.floor(maintenance))); | |
$('#airplaneModelDetails #cpp').text("$" + commaSeparateNumber(Math.floor(cost / (model.capacity * frequency))) + " * " + (model.capacity * frequency)); | |
$('#airplaneModelDetails #cps').text("$" + commaSeparateNumber(Math.floor(cost / staffTotal)) + " * " + staffTotal); | |
} | |
$("#airplaneModelDetails #speed").parent().after(` | |
<div class="table-row"> | |
<div class="label">‍</div> | |
</div> | |
<div class="table-row"> | |
<div class="label"> | |
<h5>-- Costs --</h5> | |
</div> | |
</div> | |
<div class="table-row"> | |
<div class="label"> | |
<h5>Fuel cost:</h5> | |
</div> | |
<div class="value" id="FCPF"></div> | |
</div> | |
<div class="table-row"> | |
<div class="label"> | |
<h5>Crew cost:</h5> | |
</div> | |
<div class="value" id="CCPF"></div> | |
</div> | |
<div class="table-row"> | |
<div class="label"> | |
<h5>Airport fees:</h5> | |
</div> | |
<div class="value" id="AFPF"></div> | |
</div> | |
<div class="table-row"> | |
<div class="label"> | |
<h5>Depreciation (wip):</h5> | |
</div> | |
<div class="value" id="depreciation"></div> | |
</div> | |
<div class="table-row"> | |
<div class="label"> | |
<h5>Service supplies:</h5> | |
</div> | |
<div class="value" id="SSPF"></div> | |
</div> | |
<div class="table-row"> | |
<div class="label"> | |
<h5>Maintenance (wip):</h5> | |
</div> | |
<div class="value" id="maintenance"></div> | |
</div> | |
<div class="table-row"> | |
<div class="label"> | |
<h5>Cost per PAX:</h5> | |
</div> | |
<div class="value" id="cpp"></div> | |
</div> | |
<div class="table-row"> | |
<div class="label"> | |
<h5>Cost per staff:</h5> | |
</div> | |
<div class="value" id="cps"></div> | |
</div> | |
<div class="table-row"> | |
<div class="label">‍</div> | |
</div>`); | |
if (REMOVE_MOVING_BACKGROUND === true) { | |
$('body').attr({style:'background: rgb(83, 85, 113);'}); | |
} | |
console.log("Plane score script loaded"); | |
aphix
commented
Jun 29, 2022
How is the profit per flight hour calculated?
How come there is such a difference between the Airplane table CPP and the CPP when editing a flight. I have with my Embraer ERJ140 244 CPP when entering 664 kilometers, but the CPP when editing the route with a frequency of 1/w is 218 or 380 with another plane with the same model. What is the realistic value?
How come there is such a difference between the Airplane table CPP and the CPP when editing a flight.
When the flight is real it factors in the condition of the plane, I believe.
How is the profit per flight hour calculated?
See lines 105 & 106: https://gist.github.com/aphix/fdeeefbc4bef1ec580d72639bbc05f2d#file-betterairlineclub-userscript-js-L105-L106
It's just profit / duration (but it should also factor in turnaround time, realistically).
Updated to how I run it at this point (saves GPU).
the new planes introduced a few days ago are not showing while using the latest script
1.1.2
Updates to plane purchase table:
- Added auto-tiering colors to cost-per-pax (stratified by what is currently visible based on filters)
- Added auto-reloading all the planes once game-wide ownership details are populated (finally!)
- Added %OFF column (discount percentage) for purchase of each plane (since "price" column only shows current/reduced price)
General changes:
- Linked to this gist for updates, comments, and for download
- Added some tooltips/title-text to column headers in case stuff wasn't obvious (it wasn't)
- Moved some parts that users might want to tweak to the top and added some comments/variables to clarify
the new planes introduced a few days ago are not showing while using the latest script
Got any examples for me to look for?
Updated the "%OFF" to "%🔽" in the aircraft table header since it fit better