-
-
Save PhompAng/f00eb6f16aaaa5a32c9896b450fc73e0 to your computer and use it in GitHub Desktop.
Better Airline Club + Cost Per PAX Combined & Improved
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==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/PhompAng/f00eb6f16aaaa5a32c9896b450fc73e0/raw/bca8ee0a256aa83f435e896df319439b8b124719/BetterAirlineClub.userscript.js | |
// @updateURL https://gist.githubusercontent.com/PhompAng/f00eb6f16aaaa5a32c9896b450fc73e0/raw/bca8ee0a256aa83f435e896df319439b8b124719/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 = 0; // 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 .mainPanel .section div:nth-child(2) div').css({width: '100%'}); | |
//$('#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'>" + getGradeStarsImgs(modelOwnerInfo.quality * 2) + "</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, //Name | |
9, //Family | |
8, //Price | |
6, //Capacity | |
6, //Quality | |
6, //Range | |
6, //Fuel | |
6, //Life Span | |
7, //Speed | |
7, //Runway | |
6, //Own | |
3, //Total Flight | |
5, //CPX | |
4, //%Off | |
4 //Max Circulation | |
]; | |
if (columnWidthPercents.reduce((sum, val) => sum += val, 0) !== 100) { | |
console.warn('Column widths do not equal 100%, widths:', columnWidthPercents, columnWidthPercents.reduce((sum, val) => sum += val, 0)); | |
} | |
$("#airplaneModelSortHeader").append("<div class=\"cell clickable\" data-sort-property=\"totalOwned\" data-sort-order=\"descending\" onclick=\"toggleAirplaneModelTableSortOrder($(this))\">Inventory</div>"); | |
$("#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\">%OFF</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> | |
<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 --> | |
<div class="cell" style="width: ${columnWidthPercents[14]}%; border-bottom: none;"></div><!-- New columns --> | |
`); | |
$("#airplaneCanvas .mainPanel .section .table:first").append(` | |
<div class="table-header"> | |
<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> | |
</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"); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment