Scripts/route-planner-interface.js |
"use strict";
this.name = "RoutePlanner_Interface";
this.author = "Alnivel";
this.copyright = "2022 Alnivel";
this.version = "0.2";
this.licence = "CC BY-NC-SA 4.0";
this.$ILib = null;
this.$screens = {};
this.$inverseMapTo = function (map) {
return Object.keys(map).reduce(function (invMap, key) {
return invMap[map[key]] = key;
}.bind(this), {})
}
this.$findKeyByValue = function (dictionary, value) {
var keys = Object.keys(dictionary);
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
if (dictionary[key] === value)
return key;
}
return undefined;
}
this.$colorToColorSpecifierMap = {
"Green": "greenColor",
"Yellow": "yellowColor",
"Red": "redColor",
"Blue": "blueColor",
"Cyan": "cyanColor",
}
this.$colorSpecifierToColorMap = this.$inverseMapTo(this.$colorToColorSpecifierMap)
this.$markerShapes = {
"Cross": "MARKER_X",
"Plus": "MARKER_PLUS",
"Square": "MARKER_SQUARE",
"Diamond": "MARKER_DIAMOND"
}
this.$configureInterface = function () {
let interfaceScript = this;
let mainScript = worldScripts["RoutePlanner"];
let ILib = this.$ILib = worldScripts["RoutePlanner_InterfaceLib"];
mainScreen: {
const mainScreen = this.$screens.main = new ILib.PagedScreenController({
title: "Route Planning Interface",
screenID: "RoutePlanner_Main",
allowInterrupt: true,
exitScreen: "GUI_SCREEN_INTERFACES"
});
mainScreen.addToContent(new ILib.LabelLine("General"));
mainScreen.addToContent(
new ILib.CycleLine(
expandDescription("[RoutePlanner_interface_main_wp-order-mode]"),
mainScript.$waypointOrderModes,
mainScript.$currentWaypointOrderMode)
.onSelected(function (selectedIndex, selectedValue) {
mainScript.$currentWaypointOrderMode = selectedValue;
})
);
mainScreen.addToContent(
new ILib.ButtonLine(expandDescription("[RoutePlanner_interface_main_edit-route]"))
.onUpdate(function () {
return {
unselectable: mainScript.$currentWaypointOrderMode === "None"
};
})
.onSelected(function () {
this.$screens.editRouteInstructions.runScreen(mainScript.$currentWaypointOrderMode)
return true; // returning true prevent current screen redrawing
}.bind(this))
);
mainScreen.addToContent(
new ILib.ButtonLine(expandDescription("[RoutePlanner_interface_main_route-statistics]"))
.onUpdate(function () {
return {
unselectable: mainScript.$currentWaypointOrderMode === "None"
};
})
.onSelected(function () {
this.$screens.routeStatisticsScreen.runScreen()
return true;
}.bind(this))
);
const wpTypes = mainScript.$wpTypes;
const wpTypesList = Object.keys(wpTypes);
for (let i = 0; i < wpTypesList.length; i++)
{
let typeKey = wpTypesList[i];
let type = wpTypes[typeKey];
let labelText = expandDescription(
"[RoutePlanner_interface_main_route-through-toggler]",
{waypointType: expandDescription(type.nameKey)});
mainScreen.addToContent(
new ILib.ToggleLine(labelText, type.isUsed)
.onSelected(function (newState) {
type.isUsed = newState;
})
)
}
mainScreen.addToContent(
new ILib.ButtonLine(expandDescription("[RoutePlanner_interface_main_appearance-settings]"))
.onSelected(function () {
this.$screens.appearanceSettings.runScreen()
return true; // returning true prevent current screen redrawing
}.bind(this))
);
}
const returnToMain = this.$screens.main.returnToScreen.bind(this.$screens.main);
appearanceSettingsScreen: {
const appearanceSettingsScreen = this.$screens.appearanceSettings = new ILib.PagedScreenController({
title: "Route Planning Interface: Appearance Settings",
screenID: "RoutePlanner_Main_AppearanceSettings",
allowInterrupt: true,
exitScreen: "GUI_SCREEN_INTERFACES"
}, returnToMain);
const colors = this.$colorToColorSpecifierMap;
const appearance = mainScript.$appearance;
appearanceSettingsScreen.addToContent(
new ILib.CycleLine(
expandDescription("[RoutePlanner_interface_appearance_user-wp-marker-shape]"),
this.$markerShapes,
this.$findKeyByValue(this.$markerShapes, appearance.userWaypointShape))
.onSelected(function (selectedShapeIndex, selectedShape) {
appearance.userWaypointShape = selectedShape;
})
);
appearanceSettingsScreen.addToContent(
new ILib.CycleLine(
expandDescription("[RoutePlanner_interface_appearance_user-wp-marker-color]"),
colors,
this.$findKeyByValue(colors, appearance.userWaypointColor))
.onSelected(function (selectedColorIndex, selectedColor) {
appearance.userWaypointColor = selectedColor;
})
);
appearanceSettingsScreen.addToContent(
new ILib.CycleLine(
expandDescription("[RoutePlanner_interface_appearance_user-route-color-jumps]"),
colors,
this.$findKeyByValue(colors, appearance.routeLineColorByJumps))
.onSelected(function (selectedColorIndex, selectedColor) {
appearance.routeLineColorByJumps = selectedColor;
})
);
appearanceSettingsScreen.addToContent(
new ILib.CycleLine(
expandDescription("[RoutePlanner_interface_appearance_user-route-color-time]"),
colors,
this.$findKeyByValue(colors, appearance.routeLineColorByTime))
.onSelected(function (selectedColorIndex, selectedColor) {
appearance.routeLineColorByTime = selectedColor;
})
);
}
function formatWaypoints(waypoints, highlightedSystemId, waypointOrderMode) {
function getSystemSubstitution(id) {
if (id < 10) return "%J00" + id;
if (id < 100) return "%J0" + id;
else return "%J" + id;
}
const wpSubtitutions = waypoints.map(function (wp) {
if (highlightedSystemId === wp)
return " \[" + getSystemSubstitution(wp) + "\] ";
else
return getSystemSubstitution(wp);
});
const wpSeparator = waypointOrderMode === "Auto" ? ", " : " → ";
return wpSubtitutions.length? expandDescription(wpSubtitutions.join(wpSeparator)) : "None";
}
routeEditingInstruction: {
const instructionSP = {
title: "Route Planning Interface: Route editing instruction",
screenID: "RoutePlanner_EditRoute_Instructions",
allowInterrupt: false,
exitScreen: "GUI_SCREEN_SHORT_RANGE_CHART",
}
instructionSP.choices = {
"00_SPACER" : { text: "" },
"01_RETURN": {
text: ILib.$padTextOnSide(
expandDescription("[RoutePlanner_interface_route-editing-instructions_return]"),
"right", 32),
color: "cyanColor"
},
"02_ADD_START": {
text: ILib.$padTextOnSide(
expandDescription("[RoutePlanner_interface_route-editing-instructions_add-start]"),
"right", 32),
color: "cyanColor"
},
"03_CONTINUE": {
text: ILib.$padTextOnSide(
expandDescription("[RoutePlanner_interface_route-editing-instructions_start-edit-on-chart]"),
"right", 32),
color: "cyanColor"
},
"04_SPACER": { text: "" },
"05_SPACER": { text: "" },
}
this.$screens.editRouteInstructions = {
screenParameters: instructionSP,
returnToMain: returnToMain,
userWaypoints: mainScript.$userWaypoints,
runScreen: function (waypointOrderMode) {
let targetSystem = player.ship.targetSystem;
let firstUserWaypoint = this.userWaypoints.length === 0? -1 : this.userWaypoints[0];
const addToStartChoise = this.screenParameters.choices["02_ADD_START"];
if(system.ID === targetSystem || firstUserWaypoint === targetSystem)
{
addToStartChoise.unselectable = true;
addToStartChoise.color = "darkGrayColor";
}
else
{
addToStartChoise.unselectable = false;
addToStartChoise.color = "cyanColor";
}
let callback = function (choice) {
if (choice === "01_RETURN") this.returnToMain();
else if (choice === "02_ADD_START")
{
this.userWaypoints.unshift(targetSystem);
interfaceScript.$screens.editRouteInstructions.runScreen(waypointOrderMode);
}
else mainScript.$routeEditingEnabled = true;
};
instructionSP.message = expandMissionText("RoutePlanner_interface_route-editing-instructions", {
waypointOrderMode: waypointOrderMode,
userWaypoints: formatWaypoints(this.userWaypoints, targetSystem, waypointOrderMode)
});
mission.runScreen(this.screenParameters, callback, this);
},
}
}
routeEditingActions: {
const routeEditActionSP = {
title: "Route Planning Interface: <systemname>",
screenID: "RoutePlanner_EditRoute_SystemAction",
allowInterrupt: false,
exitScreen: "GUI_SCREEN_SHORT_RANGE_CHART",
choices: {}
}
this.$screens.editRouteSystemAction = {
screenParameters: routeEditActionSP,
returnToMain: returnToMain,
userWaypoints: mainScript.$userWaypoints,
selectedSystemId: null,
selectedSystemIndex: null,
systemId: null,
systemIndex: null,
runScreen: function (systemId, waypointOrderMode) {
const systemIndex = this.userWaypoints.indexOf(systemId);
const selectedSystemId = this.selectedSystemId;
this.systemId = systemId;
this.systemIndex = systemIndex;
const systemInWaypoints = systemIndex !== -1;
const systemName = System.systemNameForID(systemId);
let selectedSystemName = undefined;
let systemIsSelected = false;
if (selectedSystemId === null)
this.selectedSystemIndex = null;
else {
this.selectedSystemIndex = this.userWaypoints.indexOf(selectedSystemId);
selectedSystemName = System.systemNameForID(selectedSystemId);
systemIsSelected = systemId === selectedSystemId;
}
this.screenParameters.message = this.getText(systemId, waypointOrderMode);
this.screenParameters.choices = this.getChoices(systemInWaypoints, systemIsSelected, selectedSystemName);
this.screenParameters.title = "Route Planning Interface: " + systemName;
mission.runScreen(this.screenParameters, this.screenCallback, this);
},
getText: function (highlightedSystemId, waypointOrderMode) {
return expandMissionText("RoutePlanner_interface_route-editing-actions", {
waypointOrderMode: waypointOrderMode,
userWaypoints: formatWaypoints(this.userWaypoints, highlightedSystemId, waypointOrderMode)
});
},
getChoices: function (systemInWaypoints, systemIsSelected, selectedSystemName) {
const choices = {};
if (systemInWaypoints) {
if (systemIsSelected)
choices["01_DESELECT"] = "Deselect";
else
choices["01_SELECT"] = "Select";
choices["02_REMOVE"] = "Remove";
if (selectedSystemName) {
choices["03_SWAP"] = "Swap with " + selectedSystemName;
}
// choices["03_SPACER"] = "";
}
else {
if (selectedSystemName) {
choices["01_ADD_BEFORE"] = "Add before " + selectedSystemName;
choices["02_ADD_AFTER"] = "Add after " + selectedSystemName;
}
else {
choices["01_SPACER"] = "";
choices["02_SPACER"] = "";
}
choices["03_ADD_END"] = "Add to the end";
}
choices["04_SPACER"] = ""
choices["05_BACK_TO_CHART"] = "Back to chart";
choices["06_STOP_EDIT"] = "Stop route editing";
return choices;
},
screenCallback: function (choice) {
if (choice === "05_BACK_TO_CHART") return;
else if (choice === "06_STOP_EDIT") {
mainScript.$routeEditingEnabled = false;
this.returnToMain();
return;
}
if (this.systemIndex !== -1) {
if (choice === "01_SELECT")
this.selectedSystemId = this.systemId;
else if (choice === "01_DESELECT")
this.selectedSystemId = null;
else if (choice === "02_REMOVE") {
this.userWaypoints.splice(this.systemIndex, 1);
if (this.selectedSystemIndex === this.systemIndex)
this.selectedSystemId = null
}
else if (choice === "03_SWAP") {
let temp = this.userWaypoints[this.systemIndex];
this.userWaypoints[this.systemIndex] = this.userWaypoints[this.selectedSystemIndex];
this.userWaypoints[this.selectedSystemIndex] = temp;
this.selectedSystemIndex = this.systemIndex;
}
}
else {
if (choice == "01_ADD_BEFORE")
this.userWaypoints.splice(this.selectedSystemIndex, 0, this.systemId)
else if (choice == "02_ADD_AFTER")
this.userWaypoints.splice(this.selectedSystemIndex + 1, 0, this.systemId)
else if (choice == "03_ADD_END")
this.userWaypoints.push(this.systemId)
}
}
}
}
routeStatistics: {
const routeStatisticsScreen = this.$screens.routeStatisticsScreen = new ILib.PagedScreenController({
title: "Route Planning Interface: Statistics",
screenID: "RoutePlanner_Main_RouteStatistics",
allowInterrupt: true,
exitScreen: "GUI_SCREEN_INTERFACES"
}, returnToMain);
let formatToColumns = function (name, type, remainTime, travelTime, distance){
return "" +
ILib.$padTextOnSide(name, "right", 6) +
ILib.$padTextAround(type, 8) +
ILib.$padTextOnSide(remainTime, "left", 6) +
ILib.$padTextOnSide(travelTime, "left", 6) +
ILib.$padTextOnSide(distance, "left", 6);
}
routeStatisticsScreen.addToHeader(
new ILib.LabelLine(formatToColumns("Name", "Type", "Remain time", "Travel time", "Distance, LY"))
);
routeStatisticsScreen.addToHeader(new ILib.LabelLine(ILib.$repeatPattern("-", 32)));
let rsScreenRun = routeStatisticsScreen.runScreen.bind(routeStatisticsScreen);
let updateTable = function (){
const playerShip = player.ship;
const targetSystem = playerShip.targetSystem;
const memoizedRoutes = mainScript.$memoizedRoutes;
const routeMode = playerShip.routeMode;
const distanceMeasurement = routeMode === "OPTIMIZED_BY_JUMPS" ? "distance" : "distanceWhenTime";
const timeMeasurement = routeMode === "OPTIMIZED_BY_JUMPS" ? "timeWhenDistance" : "time";
const waypointsOnCurrentRoute = mainScript.$waypointsOnCurrentRoute;
const waypointsOnCurrentRouteCount = waypointsOnCurrentRoute.length;
const storedRoutes = mainScript.$storedRoutes;
const storedRoutesCount = storedRoutes.length;
routeStatisticsScreen.clearContent();
let totalTime = 0;
let totalDistance = 0;
let previousWaypointId = system.ID;
for(let i = waypointsOnCurrentRouteCount - 1; i >= 0; i--)
{
let waypointInfo = waypointsOnCurrentRoute[i];
if(waypointInfo === undefined) // if something went wrong then just ignore it and hope for better
continue;
let waypointId = waypointInfo.ID;
let name = System.systemNameForID(waypointId);
let costs = memoizedRoutes.routeCost(previousWaypointId, waypointId);
totalDistance += costs[distanceMeasurement];
totalTime += costs[timeMeasurement];
let type = "", remainTime = "";
if(waypointInfo.type)
type = expandDescription(waypointInfo.type.nameKey);
if(waypointId === targetSystem)
type += ", " + expandDescription("[RoutePlanner_wpType_targetSystem]");
if(waypointInfo.eta)
remainTime = ((waypointInfo.eta - clock.seconds) / 3600).toFixed(1);
routeStatisticsScreen.addToContent(
new ILib.LabelLine(formatToColumns(name, type, remainTime, totalTime.toFixed(1), totalDistance.toFixed(1)))
);
previousWaypointId = waypointId;
}
if(targetSystem !== previousWaypointId) {
let name = System.systemNameForID(targetSystem);
let costs = memoizedRoutes.routeCost(system.ID, targetSystem);
totalDistance = costs[distanceMeasurement];
totalTime = costs[timeMeasurement];
routeStatisticsScreen.addToContent(
new ILib.LabelLine(formatToColumns(
name, expandDescription("[RoutePlanner_wpType_targetSystem]"),
"", totalTime.toFixed(1), totalDistance.toFixed(1)))
);
previousWaypointId = targetSystem;
}
for(let i = 0; i < storedRoutesCount; i++)
{
let waypointInfo = storedRoutes[i].endWaypointInfo;
if(waypointInfo === undefined) // if something went wrong then just ignore it and hope for better 2
continue;
let waypointId = waypointInfo.ID;
let name = System.systemNameForID(waypointId);
let costs = memoizedRoutes.routeCost(previousWaypointId, waypointId);
totalDistance += costs[distanceMeasurement];
totalTime += costs[timeMeasurement];
let type = "", remainTime = "";
if(waypointInfo.type)
type = expandDescription(waypointInfo.type.nameKey);
if(waypointInfo.eta)
remainTime = ((waypointInfo.eta - clock.seconds) / 3600).toFixed(1);
routeStatisticsScreen.addToContent(
new ILib.LabelLine(formatToColumns(name, type, remainTime, totalTime.toFixed(1), totalDistance.toFixed(1)))
);
previousWaypointId = waypointId;
}
routeStatisticsScreen.clearFooter();
routeStatisticsScreen.addToFooter(new ILib.LabelLine(ILib.$repeatPattern("-", 32)));
routeStatisticsScreen.addToFooter(
new ILib.LabelLine(formatToColumns("Total", "", "", totalTime.toFixed(1), totalDistance.toFixed(1)))
);
}
routeStatisticsScreen.runScreen = function() { updateTable(); rsScreenRun(); }
}
}
|
Scripts/route-planner-interfaceLib.js |
"use strict";
this.name = "RoutePlanner_InterfaceLib";
this.author = "Alnivel";
this.copyright = "2022 Alnivel";
this.version = "0.2";
this.licence = "CC BY-NC-SA 4.0";
(function() {
let ILib = this;
this.__BasePagedController = function __BasePagedController(screenParameters) {
__BasePagedController_init(screenParameters);
}
this.__BasePagedController.prototype.__BasePagedController_init = function (screenParameters) {
this.screenParameters = screenParameters;
this.headerLines = [];
this.contentLines = [];
this.footerLines = [];
this.headerPrefix = "00HEADER";
this.contentPrefix = "20CONTENT";
this.footerPrefix = "40FOOTER";
this.currentPage = 0;
this.pages = 0;
this.selectedChoicesKey = undefined;
}
this.__BasePagedController.prototype.addToHeader = function addToHeader(line) {
const headerLinesCount = this.headerLines.length;
const key = (headerLinesCount < 10 ? ("0" + headerLinesCount) : headerLinesCount) + "_NO_KEY";
this.headerLines.push({ key: key, line: line });
}
this.__BasePagedController.prototype.addToContent = function addToContent(line) {
const contentLinesCount = this.contentLines.length;
const key = (contentLinesCount < 10 ? ("0" + contentLinesCount) : contentLinesCount) + "_NO_KEY";
this.contentLines.push({ key: key, line: line });
}
this.__BasePagedController.prototype.addToFooter = function addToFooter(line) {
const footerLinesCount = this.footerLines.length;
const key = (footerLinesCount < 10 ? ("0" + footerLinesCount) : footerLinesCount) + "_NO_KEY";
this.footerLines.push({ key: key, line: line });
}
/*
Remove line from ...
this.__BasePagedScreenController.prototype.removeLineFromContent = function removeFromContent(line) {
let removePos = -1;
let i = this.contentLines.length;
while (i--) {
if (this.contentLines[i].line === line) {
removePos = i;
break;
}
}
if (removePos === -1)
return false;
else {
this.contentLines.splice(removePos, 1);
return true;
}
}
*/
this.__BasePagedController.prototype.clearHeader = function clearHeader() {
this.headerLines.length = 0;
}
this.__BasePagedController.prototype.clearContent = function clearHeader() {
this.contentLines.length = 0;
}
this.__BasePagedController.prototype.clearFooter = function clearHeader() {
this.footerLines.length = 0;
}
/***
* Update every line in header and fill "choises" parameter with them
* @param {object} choises
* @returns {number} Header height in lines
*/
this.__BasePagedController.prototype.updateHeader = function updateHeader(choises) {
const linesForHeader = this.headerLines.length;
for (let i = 0; i < linesForHeader; i++) {
let pair = this.headerLines[i];
let key = this.headerPrefix + (i < 10 ? ("0" + i) : i) + "_" + pair.key;
choises[key] = pair.line.update();
}
return linesForHeader;
}
/**
* Update content lines on specified page and fill "choises" parameter with them
* @param {object} choises
* @param {number} linesPerPage Maximum number of lines per page
* @param {number} page Page number
* @returns {number} Content height on specified page in lines
*/
this.__BasePagedController.prototype.updateContent = function updateContent(choises, linesPerPage, page) {
let linesStartIndex = linesPerPage * page;
let linesEndIndex = linesStartIndex + linesPerPage;
let contentLinesCount = this.contentLines.length;
let linesEndIndexLimited = contentLinesCount < linesEndIndex ? contentLinesCount : linesEndIndex;
for (let i = linesStartIndex; i < linesEndIndexLimited; i++) {
let pair = this.contentLines[i];
let key = this.contentPrefix + (i < 10 ? ("0" + i) : i) + "_" + pair.key;
choises[key] = pair.line.update();
}
return linesEndIndexLimited - linesStartIndex;
}
/***
* Update every line in footer and fill "choises" parameter with them
* @param {object} choises
* @returns {number} Footer height in lines
*/
this.__BasePagedController.prototype.updateFooter = function updateFooter(choises) {
const linesForFooter = this.footerLines.length;
for (let i = 0; i < linesForFooter; i++) {
let pair = this.footerLines[i];
let key = this.footerPrefix + (i < 10 ? ("0" + i) : i) + "_" + pair.key;
choises[key] = pair.line.update();
}
return linesForFooter;
}
this.__BasePagedController.prototype.proccessChoises = function proccessChoises(choise) {
for (let i = 0; i < this.headerLines.length; i++) {
let pair = this.headerLines[i];
let key = this.headerPrefix + (i < 10 ? ("0" + i) : i) + "_" + pair.key;
let line = pair.line;
if (choise === key) {
return line.select && line.select(); // if true returned then don`t redraw this screen
}
}
for (let i = 0; i < this.contentLines.length; i++) {
let pair = this.contentLines[i];
let key = this.contentPrefix + (i < 10 ? ("0" + i) : i) + "_" + pair.key;
let line = pair.line;
if (choise === key) {
return line.select && line.select();
}
}
for (let i = 0; i < this.footerLines.length; i++) {
let pair = this.footerLines[i];
let key = this.footerPrefix + (i < 10 ? ("0" + i) : i) + "_" + pair.key;
let line = pair.line;
if (choise === key) {
return line.select && line.select();
}
}
// Restarting the screen, if necessary, is performed by the child
/*
this.screenParameters.choices = this.getPage(this.currentPage);
this.screenParameters.initialChoicesKey = initialChoicesKey;
mission.runScreen(this.screenParameters, this.onChoiceSelected.bind(this));
*/
}
this.__BasePagedController.prototype.runScreen = function runScreen() {
log("ROUTEPLANER_INTERFACELIB_DEBUG", "Started " + this.screenParameters.title + " mission screen");
this.currentPage = 0;
this.screenParameters.choices = this.getPage(0);
mission.runScreen(this.screenParameters, this.onChoiceSelected.bind(this));
}
this.__BasePagedController.prototype.returnToScreen = function returnToScreen() {
if(this.currentPage === undefined || this.currentPage > this.pages)
this.currentPage = 0;
log("ROUTEPLANER_INTERFACELIB_DEBUG", "Returned to " + this.screenParameters.title + " mission screen on page " + this.currentPage);
this.screenParameters.choices = this.getPage(this.currentPage);
this.screenParameters.initialChoicesKey = this.selectedChoicesKey;
mission.runScreen(this.screenParameters, this.onChoiceSelected.bind(this));
}
/**
* Create paged screen that can be filled with line objects.
* Each time the line is selected, it will re-render,
* unless the Return option is selected (then will be called callbackOnReturn)
* or the onSelect callback of the selected line returns true
* @param {*} screenParameters
* @param {Function} [callbackOnReturn]
*/
this.PagedScreenController = function PagedScreenController(screenParameters, callbackOnReturn) {
this.__BasePagedController_init(screenParameters); // Don`t know how to get base constructor properly
this.callbackOnReturn = callbackOnReturn ? callbackOnReturn : null;
}
this.PagedScreenController.prototype = Object.create(__BasePagedController.prototype);
this.PagedScreenController.prototype.onReturn = function onReturn(callback) {
this.callbackOnReturn = callback;
}
this.PagedScreenController.prototype.getPage = function getPage(page) {
const screenHeight = (player.ship.hudAllowsBigGui ? 27 : 21);
const choises = {};
const headerHeight = this.updateHeader(choises);
const footerHeight = this.updateFooter(choises);
const controlsHeight = 5;
const maxContentHeight = screenHeight - headerHeight - footerHeight - controlsHeight;
const pages = this.pages = Math.ceil(this.contentLines.length / maxContentHeight);
const contentHeight = this.updateContent(choises, maxContentHeight, page);
const remainHeight = maxContentHeight - contentHeight;
log("ROUTEPLANER_INTERFACELIB_DEBUG", "Remain lines after content " + remainHeight + " from " + maxContentHeight)
/*** Fill empty ***/
const spacer = { text: "" };
for (let i = 0; i < remainHeight; i++) {
let key = "21EMPTY_" + (i < 10 ? ("0" + i) : i) + "_EMPTY";
choises[key] = spacer;
}
/*** Controlls ***/
if (pages > 1) {
choises["70_SPACER"] = spacer;
choises["71_PAGE_NUM"] = {
text: "Page " + (1 + page) + " of " + pages,
unselectable: true
}
choises["73_NEXT_PAGE"] = {
text: "Next page",
unselectable: page + 1 === pages
};
choises["74_PREV_PAGE"] = {
text: "Previous page",
unselectable: page === 0
};
} else {
choises["70_SPACER"] = spacer;
choises["71_SPACER"] = spacer;
choises["73_SPACER"] = spacer;
choises["74_SPACER"] = spacer;
}
choises["80_RETURN"] = { text: "Return" };
return choises;
}
this.PagedScreenController.prototype.onChoiceSelected = function onChoiceSelected(choise) {
if (choise === "80_RETURN") {
if (this.callbackOnReturn) this.callbackOnReturn();
return;
}
this.selectedChoicesKey = choise;
if (choise === "74_PREV_PAGE") {
this.currentPage--;
if (this.currentPage === 0)
this.selectedChoicesKey = "73_NEXT_PAGE";
}
else if (choise === "73_NEXT_PAGE") {
this.currentPage++;
if (this.currentPage + 1 === this.pages)
this.selectedChoicesKey = "74_PREV_PAGE";
}
else {
const stopRestartingScreen = this.proccessChoises(choise);
if(stopRestartingScreen)
{
log("ROUTEPLANER_INTERFACELIB_DEBUG", "Screen \"" + this.screenParameters.title + "\" restarting has been stopped by key " + choise)
return;
}
}
this.screenParameters.choices = this.getPage(this.currentPage);
this.screenParameters.initialChoicesKey = this.selectedChoicesKey;
mission.runScreen(this.screenParameters, this.onChoiceSelected.bind(this));
}
this.$limitText = function $limitText(text, limitWidth) {
const ellipsis = "…";
let tmp = text;
while (defaultFont.measureString(tmp) > limitWidth) {
tmp = tmp.substring(0, tmp.length - 2) + ellipsis;
}
return tmp;
}
/**
*
* @param {string} text
* @param {"left"|"right"} padSide
* @param {number} desiredWidth
* @returns Padded text
*/
this.$padTextOnSide = function $padTextOnSide(text, padSide, desiredWidth) {
const hairSpace = String.fromCharCode(31);
const hairSpaceWidth = defaultFont.measureString(hairSpace);
const textWidth = defaultFont.measureString(text);
const padsNeeded = Math.floor((desiredWidth - textWidth) / hairSpaceWidth);
let resultText;
if (padsNeeded > 1)
if (padSide == "left")
resultText = new Array(padsNeeded).join(hairSpace) + text;
else
resultText = text + new Array(padsNeeded).join(hairSpace);
else
resultText = this.$limitText(text, desiredWidth);
return resultText;
}
this.$padTextAround = function $padTextAround(text, desiredWidth) {
const hairSpace = String.fromCharCode(31);
const hairSpaceWidth = defaultFont.measureString(hairSpace);
const textWidth = defaultFont.measureString(text);
const padsNeeded = Math.floor((desiredWidth - textWidth) / (2 * hairSpaceWidth));
let resultText;
if (padsNeeded > 1) {
let sidePadding = new Array(padsNeeded).join(hairSpace);
resultText = sidePadding + text + sidePadding;
}
else
resultText = this.$limitText(text, desiredWidth);
return resultText;
}
this.$repeatPattern = function $repeatPattern(pattern, desiredWidth) {
const patternWidth = defaultFont.measureString(pattern);
const repeatsNeeded = Math.ceil(desiredWidth / patternWidth);
let tmp = new Array(repeatsNeeded).join(pattern);
while (defaultFont.measureString(tmp) > desiredWidth) {
tmp = tmp.substring(0, tmp.length - 1);
}
return tmp
}
this.__LineWithCallbacksPrototype = {
/**
* Set callback that will be called when line is redrawing.
* The arguments for the function are same as for select callback,
* but it can return dictionary whose
* key-value pairs will be setted as choice parameters
* @param {Function} updateCallback
* @returns Line object
*/
onUpdate: function onUpdate(updateCallback) {
this.updateCallback = updateCallback;
return this;
},
/**
* Set callback that will be called when line is selected.
* The arguments for the function depends on line type.
* Is callback return true than screen will not redrawed.
* It can be used to change to another mission screen.
* @param {*} selectCallback
* @returns Line object
*/
onSelected: function onSelected(selectCallback) {
this.selectCallback = selectCallback;
return this;
}
}
// TODO: Add lines hiding?
/**
* Create empty line object
*/
this.EmptyLine = function () {}
this.EmptyLine.prototype.update = function update() {
return { text: "" };
}
/**
* Create line object that can`t be selected
* @param {string} text
*/
this.LabelLine = function (text, alignment) {
this.choiceObject = {
text: ILib.$padTextAround(text, 32),
unselectable: true,
color: "whiteColor",
alignment: alignment? alignment : "CENTER"
}
}
this.LabelLine.prototype = Object.create(__LineWithCallbacksPrototype);
this.LabelLine.prototype.update = function update() {
if (this.updateCallback) {
let updated = this.updateCallback();
const updatedKeys = Object.keys(updated);
for (let i = 0; i < updatedKeys.length; i++) {
const key = updatedKeys[i];
const value = updated[key];
if (value !== undefined &&
key !== "unselectable") {
if (key === "text")
this.choiceObject[key] = ILib.$padTextOnSide(value, "right", 32);
else
this.choiceObject[key] = value;
}
}
}
return this.choiceObject;
};
/**
* Create line object that can be selected.
* When selected onSelected callback will be called without any arguments
* @param {string} text
*/
this.ButtonLine = function (text) {
this.choiceObject = {
text: ILib.$padTextOnSide(text, "right", 32),
color: "cyanColor",
alignment: "LEFT"
};
this.selectableColor = "cyanColor";
this.unselectableColor = "darkGrayColor";
}
this.ButtonLine.prototype = Object.create(__LineWithCallbacksPrototype);
this.ButtonLine.prototype.update = function update() {
if (this.updateCallback) {
let updated = this.updateCallback();
const updatedKeys = Object.keys(updated);
for (let i = 0; i < updatedKeys.length; i++) {
const key = updatedKeys[i];
const value = updated[key];
if (value !== undefined) {
if (key === "color")
this.selectableColor = value;
else if (key === "text")
this.choiceObject[key] = ILib.$padTextOnSide(value, "right", 32);
else
this.choiceObject[key] = value;
}
}
if (this.choiceObject.unselectable)
this.choiceObject.color = this.unselectableColor;
else
this.choiceObject.color = this.selectableColor;
}
return this.choiceObject;
}
this.ButtonLine.prototype.select = function select() {
if (this.selectCallback)
return this.selectCallback();
}
/**
* Create line object that toggles between true and false when selected.
* When selected onSelected callback will be called with new state as argument
* @param {string} text
* @param {Boolean} initialState
*/
this.ToggleLine = function (text, initialState) {
this.choiceObject = {
color: "cyanColor"
};
this.selectableColor = "cyanColor";
this.unselectableColor = "darkGrayColor";
this.text = text;
this.state = !!initialState;
}
this.ToggleLine.prototype = Object.create(__LineWithCallbacksPrototype);
this.ToggleLine.prototype.update = function update() {
if (this.updateCallback) {
let updated = this.updateCallback(this.state);
const updatedKeys = Object.keys(updated);
for (let i = 0; i < updatedKeys.length; i++) {
const key = updatedKeys[i];
const value = updated[key];
if (value !== undefined) {
if (key === "color")
this.selectableColor = value;
else if (key === "text")
this.text = value;
else if (key === "state")
this.state = !!value;
else
this.choiceObject[key] = value;
}
}
if (this.choiceObject.unselectable)
this.choiceObject.color = this.unselectableColor;
else
this.choiceObject.color = this.selectableColor;
}
this.choiceObject.text =
ILib.$padTextOnSide(this.text, "right", 24) +
ILib.$padTextAround(" < " + (this.state ? "On" : "Off") + " > ", 8);
return this.choiceObject;
}
this.ToggleLine.prototype.select = function select() {
this.state = !this.state;
if (this.selectCallback)
return this.selectCallback(this.state);
}
/**
* Create line object that cycles between the specified values when selected.
* When selected onSelected callback will be called with new index and value as arguments
* @param {string} text
* @param {Array|Object.<string, any>} items If it is an array the values are
* the same as the labels, otherwise it is treated as a dictionary of labels to values
* @param {any} [initialItem]
*/
this.CycleLine = function (text, items, initialItem) {
const itemsIsArray = Array.isArray(items);
const itemKeys = itemsIsArray ? items : Object.keys(items);
let itemValues;
if (itemsIsArray)
itemValues = items
else { // Object.values(items);
itemValues = Array(itemKeys.length)
for (let i = 0; i < itemKeys.length; i++)
itemValues[i] = items[itemKeys[i]]
}
let initialIndex = itemKeys.indexOf(initialItem);
if (initialIndex === -1)
initialIndex = 0;
this.choiceObject = {
color: "cyanColor",
};
this.items = itemKeys;
this.values = itemValues;
this.currentIndex = initialIndex;
this.text = text;
this.selectableColor = "cyanColor";
this.unselectableColor = "darkGrayColor";
}
this.CycleLine.prototype = Object.create(__LineWithCallbacksPrototype);
this.CycleLine.prototype.update = function update() {
if (this.updateCallback) {
let updated = this.updateCallback(this.currentIndex, this.values[this.currentIndex]);
const updatedKeys = Object.keys(updated);
for (let i = 0; i < updatedKeys.length; i++) {
const key = updatedKeys[i];
const value = updated[key];
if (value !== undefined) {
if (key === "text")
this.text = value;
else if (key === "currentIndex") {
this.currentIndex = value;
}
else
this.choiceObject[key] = value;
}
}
if (this.choiceObject.unselectable)
this.choiceObject.color = this.unselectableColor;
else
this.choiceObject.color = this.selectableColor;
}
const currentItem = this.items[this.currentIndex];
this.choiceObject.text =
ILib.$padTextOnSide(this.text, "right", 24) +
ILib.$padTextAround(" < " + currentItem + " > ", 8);
return this.choiceObject;
}
this.CycleLine.prototype.select = function select() {
this.currentIndex = (this.currentIndex + 1) % this.values.length;
if (this.selectCallback)
return this.selectCallback(this.currentIndex, this.values[this.currentIndex]);
}
}).call(this); |
Scripts/route-planner-main.js |
"use strict";
this.name = "RoutePlanner";
this.author = "Alnivel";
this.copyright = "2022 Alnivel";
this.version = "0.2";
this.licence = "CC BY-NC-SA 4.0";
/*************** Loading and saving variables ***************/
this.startUp = function () {
this.$storedRoutes = [];
const storedRoutes = JSON.parse(missionVariables.routePlanner_storedRoutes);
if (storedRoutes)
this.$storedRoutes = storedRoutes;
const storedUserWaypoints = JSON.parse(missionVariables.routePlanner_storedUserWaypoints);
if (storedUserWaypoints)
this.$userWaypoints = storedUserWaypoints;
const storedWaypointOrderMode = JSON.parse(missionVariables.routePlanner_waypointOrderMode);
if (storedWaypointOrderMode)
this.$currentWaypointOrderMode = storedWaypointOrderMode;
const savedAppearanceParams = JSON.parse(missionVariables.routePlanner_appearanceParams);
if (savedAppearanceParams)
this.$appearance = savedAppearanceParams;
const wpTypesList = Object.keys(this.$wpTypes)
for(let i = 0; i < wpTypesList.length; i++)
{
const typeKey = wpTypesList[i];
const savedIsUsed = missionVariables["routePlanner_wpType_isUsed" + typeKey];
this.$wpTypes[typeKey].isUsed = savedIsUsed === null ? true : Boolean(Number(savedIsUsed));
}
// log("RoutePlannerDebug",
// "Loaded:".concat(
// "\nStored routes", JSON.stringify(this.$storedRoutes),
// "\nUser waypoints: ", JSON.stringify(this.$userWaypoints),
// "\nOrder mode: ", this.$currentWaypointOrderMode,
// "\nAppearence: ", JSON.stringify(this.$appearance),
// "\nEnd"
// ));
this.$waypointsOnCurrentRoute = [];
}
this.playerWillSaveGame = function (message) {
const equipmentStatus = player.ship.equipmentStatus("EQ_ADVANCED_NAVIGATIONAL_ARRAY");
if (equipmentStatus === "EQUIPMENT_OK" || equipmentStatus === "EQUIPMENT_DAMAGED") {
missionVariables.routePlanner_storedRoutes = JSON.stringify(this.$storedRoutes);
missionVariables.routePlanner_storedUserWaypoints = JSON.stringify(this.$userWaypoints);
missionVariables.routePlanner_waypointOrderMode = JSON.stringify(this.$currentWaypointOrderMode)
missionVariables.routePlanner_appearanceParams = JSON.stringify(this.$appearance);
const wpTypesList = Object.keys(this.$wpTypes)
for(let i = 0; i < wpTypesList.length; i++)
{
const typeKey = wpTypesList[i];
missionVariables["routePlanner_wpType_isUsed" + typeKey] = Number(this.$wpTypes[typeKey].isUsed);
}
}
else {
missionVariables.anlivel_routePlanner_storedRoutes = null;
}
}
/*************** Parameters ***************/
this.$waypointOrderModes = ["Manual", "Auto", "None"];
this.$currentWaypointOrderMode = "Auto";
this.$routeEditingEnabled = false;
this.$appearance = {
userWaypointColor: "blueColor",
userWaypointShape: "MARKER_X",
routeLineColorByJumps: "cyanColor",
routeLineColorByTime: "redColor"
}
/*************** Waypoint types ***************/
/**
* @typedef {Object} WaypointType
* @property {Boolean} isUsed
* @property {Boolean} requiresMainStation
* @property {string} nameKey
* @property {Boolean} isTargetSystem
*/
/** @type {Object.<string, WaypointType>}*/
this.$wpTypes = {
cargoContract: { isUsed: true, requiresMainStation: true, nameKey: "[RoutePlanner_wpType_cargo]" },
parcelContract: { isUsed: true, requiresMainStation: true, nameKey: "[RoutePlanner_wpType_parcel]" },
passengerContract: { isUsed: true, requiresMainStation: true, nameKey: "[RoutePlanner_wpType_passenger]" },
userSpecified: { isUsed: true, requiresMainStation: false, nameKey: "[RoutePlanner_wpType_userSpecified]" }
}
/** @type {WaypointType}*/
this.$wpTypeTargetSystem = {
isUsed: true,
requiresMainStation: false,
nameKey: "[RoutePlanner_wpType_targetSystem]",
isTargetSystem: true
}
/*************** Interface ***************/
this.$startUpCompleted = false;
this.shipDockedWithStation = this.startUpComplete = function () {
this.$startUpCompleted = true;
const equipmentStatus = player.ship.equipmentStatus("EQ_ADVANCED_NAVIGATIONAL_ARRAY");
if (equipmentStatus === "EQUIPMENT_OK" || equipmentStatus === "EQUIPMENT_DAMAGED") {
this.$setInterfaceToStation();
}
}
this.equipmentAdded = function (equipmentKey) {
if (player.ship.docked && equipmentKey === "EQ_ADVANCED_NAVIGATIONAL_ARRAY") {
this.$setInterfaceToStation();
}
}
this.equipmentRemoved = function (equipmentKey) {
const station = player.ship.dockedStation;
if (station && equipmentKey === "EQ_ADVANCED_NAVIGATIONAL_ARRAY") {
station.setInterface("RoutePlanner_Interface", null);
}
}
this.$setInterfaceToStation = function $setInterfaceToStation(){
if (!this.$startUpCompleted)
return;
if (!this.$interface) {
let interfaceScript = worldScripts["RoutePlanner_Interface"];
interfaceScript.$configureInterface();
this.$interface = interfaceScript.$screens
// log("RoutePlannerDebug",
// "Interface initiated with:".concat(
// "\nUser waypoints: ", JSON.stringify(this.$userWaypoints),
// "\nOrder mode: ", this.$currentWaypointOrderMode,
// "\nAppearence: ", JSON.stringify(this.$appearance),
// "\nEnd"
// ));
}
const interfaceMain = this.$interface.main;
const station = player.ship.dockedStation;
station.setInterface("RoutePlanner_Interface", {
title: "Route planning",
category: "Ship Systems", // String.fromCharCode("0x1D") + "Ship Systems", // \u200b - zws
summary: "Edit the current route, change the planning mode or other planner options",
callback: interfaceMain.runScreen.bind(interfaceMain)
});
}
/*************** Updating route in flight ***************/
// Route for previous galaxy makes no sense in new
this.playerEnteredNewGalaxy = function () {
this.$storedRoutes = [];
this.$userWaypoints = [];
this.$storedUserWaypoints = [];
}
// When player launched without target system and have planed routes select first route automatically
this.shipWillLaunchFromStation = function () {
if (player.ship.targetSystem === system.ID && this.$storedRoutes.length !== 0) {
const autoRouteInfo = this.$removeFirstStoredRoute()
const autoRoute = autoRouteInfo.route;
const newTargetSystem = autoRoute[autoRoute.length - 1];
player.ship.targetSystem = newTargetSystem;
this.$waypointsOnCurrentRoute = [autoRouteInfo.endWaypointInfo];
player.consoleMessage(
expandMissionText("RoutePlanner_message_automatically-setted-destination", {
nextWaypointName: System.systemNameForID(newTargetSystem)
}), 5);
}
}
/// TODO
this.shipExitedWitchspace = function () {
if (this.$currentWaypointOrderMode !== "None" ) {
const targetSystem = player.ship.targetSystem;
if(targetSystem === system.ID)
{
// log("RP TEST", JSON.stringify(this.$waypointsOnCurrentRoute))
if (this.$storedRoutes.length !== 0) {
const nextRouteInfo = this.$storedRoutes[0];
const currentWpInfo = nextRouteInfo.startWaypointInfo;
const wpRequiresMainStation = currentWpInfo.type.requiresMainStation;
const wpType = currentWpInfo.type.nameKey;
const nextWpInfo = nextRouteInfo.endWaypointInfo;
let message = wpRequiresMainStation?
"RoutePlanner_message_reached-waypoint_requires-main-station" :
"RoutePlanner_message_reached-waypoint";
player.consoleMessage(
expandMissionText(message, {
nextWaypointName: System.systemNameForID(nextWpInfo.ID),
waypointType: expandDescription(wpType)
}), 8);
}
else if(this.$waypointsOnCurrentRoute.length == 1)
{
const lastWp = this.$waypointsOnCurrentRoute[0];
let message = lastWp.type.requiresMainStation?
"RoutePlanner_message_reached-last-waypoint_requires-main-station" :
"RoutePlanner_message_reached-last-waypoint";
player.consoleMessage(
expandMissionText(message, {
waypointType: expandDescription(lastWp.type.nameKey)
}), 8);
}
}
else if(this.$waypointsOnCurrentRoute.length > 1){
const wp = this.$waypointsOnCurrentRoute[0];
if(wp.ID === targetSystem) {
let message = wp.type.requiresMainStation?
"RoutePlanner_message_reached-midroute-waypoint_requires-main-station" :
"RoutePlanner_message_reached-midroute-waypoint";
player.consoleMessage(
expandMissionText(message, {
waypointType: expandDescription(wp.type.nameKey)
}), 8);
this.$waypointsOnCurrentRoute.shift();
}
}
}
const userWpIndex = this.$userWaypoints.indexOf(system.ID);
if (userWpIndex !== -1) {
this.$userWaypoints.splice(userWpIndex, 1);
this.$redrawUserWaypoints(
this.$userWaypoints,
this.$appearance.userWaypointColor,
this.$appearance.userWaypointShape
);
}
}
/*************** Operation with waypoints ***************/
/**
* @typedef {Object} WaypointInfo
* @property {Number} ID
* @property {WaypointType} type
* @property {Number|null} eta
*/
/** @type {WaypointInfo[]} */
this.$userWaypoints = [];
this.$collectWaypoints = function $collectWaypoints() {
const playerShip = player.ship;
const targetSystem = playerShip.targetSystem;
const seenSystems = {/* wpDestination0: outIndex0, ... */};
const currentRouteSystems = {/* wpDestination0: routeWpIndex, ... */};
const plannedWps = [];
let plannedWpsIndex = 0;
const currentRouteWps = []; // waypoints that already on current route
let currentRouteWpsIndex = 0;
let targetSystemWp = { // waypoints that on current targetSystem
ID: targetSystem,
type: this.$wpTypeTargetSystem,
eta: undefined
};
// do this to remove waypoints that are already on the current route
const currentRouteInfo = system.info.routeToSystem(
System.infoForSystem(galaxyNumber, targetSystem),
playerShip.routeMode);
if (currentRouteInfo) {
let i = currentRouteInfo.route.length;
while (i--) {
currentRouteSystems[currentRouteInfo.route[i]] = i;
}
}
const wpCollections = [
{collection: playerShip.contracts, type: this.$wpTypes.cargoContract},
{collection: playerShip.parcels, type: this.$wpTypes.parcelContract},
{collection: playerShip.passengers, type: this.$wpTypes.passengerContract}
];
const userWp = this.$userWaypoints.map(function (systemId) {
return { destination: systemId }
});
wpCollections.push({collection: userWp, type: this.$wpTypes.userSpecified});
let wpCollectionIndex = wpCollections.length;
while (wpCollectionIndex--) {
const wpCollection = wpCollections[wpCollectionIndex].collection;
const wpType = wpCollections[wpCollectionIndex].type;
if(!wpType.isUsed)
continue;
let i = wpCollection.length;
while (i--) {
let wp = wpCollection[i];
let wpEta = wp.eta + clock.seconds; // Not sure if it can be called now eta, but whatever
let wpDestination = wp.destination;
let indexInCurrentRoute = currentRouteSystems[wpDestination];
let wpLiesOnCurrentRoute = indexInCurrentRoute !== undefined;
if (seenSystems[wpDestination] === undefined) {
let storedWaypoint = {
ID: wpDestination,
type: wpType,
eta: wpEta
};
if(wpLiesOnCurrentRoute)
{
if(targetSystemWp.ID === wpDestination)
{
if(targetSystemWp.type.isTargetSystem) // it target system type then just replace
{
targetSystemWp = storedWaypoint;
}
else if(targetSystemWp.type !== wpType)
{
seenWp.type = {
isUsed: targetSystemWp.isUsed || wpType.isUsed,
requiresMainStation: targetSystemWp.requiresMainStation || wpType.requiresMainStation,
nameKey: "[RoutePlanner_wpType_several]"
}
}
}
storedWaypoint.indexInCurrentRoute = indexInCurrentRoute;
seenSystems[wpDestination] = currentRouteWpsIndex;
currentRouteWps[currentRouteWpsIndex] = storedWaypoint;
currentRouteWpsIndex++;
}
else
{
seenSystems[wpDestination] = plannedWpsIndex;
plannedWps[plannedWpsIndex] = storedWaypoint;
plannedWpsIndex++;
}
}
else
{
let wpIndex = seenSystems[wpDestination];
let seenWp = wpLiesOnCurrentRoute? currentRouteWps[wpIndex] : plannedWps[wpIndex];
let seenWpType = seenWp.type;
if(seenWpType !== wpType)
{
seenWp.type = {
isUsed: seenWpType.isUsed || wpType.isUsed,
requiresMainStation: seenWpType.requiresMainStation || wpType.requiresMainStation,
nameKey: "[RoutePlanner_wpType_several]"
}
}
seenWp.eta = seenWp.eta < wpEta? seenWp.eta : wpEta;
}
}
}
currentRouteWps.sort(function (a, b) {
return a.indexInCurrentRoute < b.indexInCurrentRoute;
});
return {onRoute: currentRouteWps, notOnRoute: plannedWps, targetSystem: targetSystemWp};
}
this.$getUserWaypoints = function $getUserWaypoints() {
const that = this;
const targetSystem = player.ship.targetSystem;
const userWaypoints = this.$userWaypoints;
let startI;
let targetSystemWp; // waypoints that on current targetSystem
if(targetSystem && userWaypoints[0] === targetSystem) {
startI = 1;
targetSystemWp = {
ID: targetSystem,
type: this.$wpTypeTargetSystem,
eta: undefined
};
}
else {
startI = 0;
targetSystemWp = {
ID: userWaypoints[0],
type: this.$wpTypes.userSpecified
}
}
const plannedWps = [];
for(let i = startI; i < userWaypoints.length; i++) {
plannedWps.push({
ID: userWaypoints[i],
type: that.$wpTypes.userSpecified
});
}
return {onRoute: [targetSystemWp], notOnRoute: plannedWps, targetSystem: targetSystemWp};
}
this.$recalculateRoutes = function $recalculateRoutes() {
let newRoutesInfo = undefined;
const storedRoutes = this.$storedRoutes;
const currentWaypointOrderMode = this.$currentWaypointOrderMode;
const targetSystem = player.ship.targetSystem;
const routeMode = player.ship.routeMode;
if (currentWaypointOrderMode === "None" || routeMode === "OPTIMIZED_BY_NONE") {
this.$redrawRoutes([], lineColor);
this.$redrawUserWaypoints([], this.$appearance.userWaypointColor, this.$appearance.userWaypointShape);
return
}
let waypoints;
// There was a check to see if it was possible not to recalculate the path,
// but it became too complicated. Maybe should return it later
if (true) {
this.$prevRouteMode = routeMode;
const autoOrderMode = currentWaypointOrderMode === "Auto";
if(autoOrderMode)
{
waypoints = this.$collectWaypoints();
newRoutesInfo = this.$routeWithCheapestInsertion(waypoints.targetSystem, waypoints.notOnRoute, routeMode);
}
else
{
waypoints = this.$getUserWaypoints();
newRoutesInfo = this.$routeKeepingOrder(waypoints.targetSystem, waypoints.notOnRoute, routeMode);
}
}
this.$waypointsOnCurrentRoute = waypoints.onRoute;
const lineColor = routeMode === "OPTIMIZED_BY_JUMPS" ?
this.$appearance.routeLineColorByJumps :
this.$appearance.routeLineColorByTime;
this.$redrawRoutes(newRoutesInfo, lineColor);
this.$redrawUserWaypoints(this.$userWaypoints, this.$appearance.userWaypointColor, this.$appearance.userWaypointShape);
}
this.playerEnteredContract = function () { this.$recalculateRoutes() };
this.guiScreenChanged = function (to, from) {
if (this.$routeEditingEnabled) {
const fromChartScreen = from === "GUI_SCREEN_SHORT_RANGE_CHART" || from === "GUI_SCREEN_LONG_RANGE_CHART";
const toChartScreen = to === "GUI_SCREEN_SHORT_RANGE_CHART" || to === "GUI_SCREEN_LONG_RANGE_CHART";
const toSystemDataScreen = to === "GUI_SCREEN_SYSTEM_DATA";
if (fromChartScreen && toSystemDataScreen) {
const editScreen = this.$interface.editRouteSystemAction;
editScreen.runScreen.call(editScreen, player.ship.infoSystem, this.$currentWaypointOrderMode);
}
else if ((fromChartScreen && toChartScreen) || mission.screenID) { }
else {
this.$routeEditingEnabled = false;
}
}
// Start periodicaly check if route mode or cursor position changed if user on chart screen
if (guiScreen === "GUI_SCREEN_SHORT_RANGE_CHART" ||
guiScreen === "GUI_SCREEN_LONG_RANGE_CHART") {
this.$prevRouteMode = player.ship.routeMode;
this.$prevCursorCoordinates = player.ship.cursorCoordinates;
this.$cursorMoving = false;
this.$routeUndrawed = true;
this.$checkChartInterfaceChangesTimer.start();
}
else {
this.$checkChartInterfaceChangesTimer.stop();
if ((from === "GUI_SCREEN_SHORT_RANGE_CHART" ||
from === "GUI_SCREEN_LONG_RANGE_CHART") &&
this.$routeUndrawed === false) {
this.$eraseStoredRoutes();
}
}
}
this.$checkChartInterfaceChangesTimer = new Timer(this, function $checkChartInterfaceChangesTimer() {
const cursorCoordinates = player.ship.cursorCoordinates;
if (cursorCoordinates.x != this.$prevCursorCoordinates.x ||
cursorCoordinates.y != this.$prevCursorCoordinates.y)
this.$cursorMoving = true;
else
this.$cursorMoving = false;
if (player.ship.routeMode != this.$prevRouteMode) {
this.$recalculateRoutes();
}
else if (this.$cursorMoving && this.$routeUndrawed === false) {
this.$eraseStoredRoutes();
this.$routeUndrawed = true;
} else if (this.$cursorMoving == false && this.$routeUndrawed) {
this.$routeUndrawed = false;
this.$recalculateRoutes();
}
this.$prevCursorCoordinates = cursorCoordinates;
}, -1, 0.25);
/**
* Builds hopefully approximate optimal route from starting system thought waypoints
* @param {WaypointInfo} startSystem
* @param {WaypointInfo[]} waypoints
* @param {"OPTIMIZED_BY_NONE"|"OPTIMIZED_BY_JUMPS"|"OPTIMIZED_BY_TIME"} routeMode
* @returns {RouteInfo[]}
*/
this.$routeWithCheapestInsertion = function $routeWithCheapestInsertion(startSystem, waypoints, routeMode) {
if (routeMode === "OPTIMIZED_BY_NONE" || waypoints.length === 0) return [];
const measurement = routeMode === "OPTIMIZED_BY_JUMPS" ? "distance" : "time";
waypoints.sort((function (a, b) {
return this.$memoizedRoutes.routeCost(startSystem.ID, b.ID)[measurement] -
this.$memoizedRoutes.routeCost(startSystem.ID, a.ID)[measurement];
}).bind(this));
const waypointsCount = waypoints.length;
const bestRoute = [startSystem];
const noRoute = { route: { length: Infinity }, distance: Infinity, time: Infinity };
//bestRoute[0] = { ID: startSystem.ID };
for (let i = 0; i < waypointsCount; i++) {
const newWp = waypoints[i];
if (!newWp.ID) break;
const endPos = bestRoute.length - 1;
let bestInsertPos, bestCostDifference, bestNewCostStart, bestNewCostEnd = null;
if (true) { // check cost of inserting in end
const endRouteWp = bestRoute[endPos];
const cost = this.$memoizedRoutes.routeCost(endRouteWp.ID, newWp.ID);
let costDifference = cost[measurement];// * 2*(1 + i/waypointsCount);//* 2;
bestInsertPos = endPos + 1;
bestCostDifference = costDifference;
bestNewCostStart = cost;
}
for (let j = 0; j < endPos; j++) {
const startWp = bestRoute[j];
const endWp = bestRoute[j + 1];
const oldCost = this.$memoizedRoutes.routeCost(startWp.ID, endWp.ID);
const newCostStart = this.$memoizedRoutes.routeCost(startWp.ID, newWp.ID);
const newCostEnd = this.$memoizedRoutes.routeCost(newWp.ID, endWp.ID);
let costDifference = (newCostStart[measurement] + newCostEnd[measurement]) - oldCost[measurement];
if (costDifference < bestCostDifference) {
bestInsertPos = j + 1;
bestCostDifference = costDifference;
bestNewCostStart = newCostStart;
bestNewCostEnd = newCostEnd;
}
}
// Had plans to posibillity to add transitions between systems that cann`t be done with usual witchjump
// Maybe someday I`ll do it. Maybe
//
// if(bestNewCostStart.through)
// if(bestEndCostStart && bestEndCostStart.through)
// bestRoute.splice(bestInsertPos, 0, bestNewCostStart.through, newWp, bestEndCostStart.through);
// else
// bestRoute.splice(bestInsertPos, 0, bestNewCostStart.through, newWp);
// else
// if(bestEndCostStart && bestEndCostStart.through)
// bestRoute.splice(bestInsertPos, 0, newWp, bestEndCostStart.through);
// else
// bestRoute.splice(bestInsertPos, 0, newWp);
bestRoute.splice(bestInsertPos, 0, newWp);
}
const partialRoutesCount = bestRoute.length - 1;
const partialRoutes = new Array(partialRoutesCount);
let startWaypointInfo = bestRoute[0];
let aInfo, bInfo = System.infoForSystem(galaxyNumber, bestRoute[0].ID);
for (let i = 1; i < bestRoute.length; i++) {
aInfo = bInfo;
bInfo = System.infoForSystem(galaxyNumber, bestRoute[i].ID);
let routeInfo = aInfo.routeToSystem(bInfo, routeMode);
routeInfo.startWaypointInfo = bestRoute[i-1];
routeInfo.endWaypointInfo = bestRoute[i];
partialRoutes[i - 1] = routeInfo;
}
return partialRoutes;
}
/**
* Builds a route from the starting system through waypoints in their exact order
* @param {WaypointInfo} startSystem
* @param {WaypointInfo[]} waypoints
* @param {"OPTIMIZED_BY_NONE"|"OPTIMIZED_BY_JUMPS"|"OPTIMIZED_BY_TIME"} routeMode
* @returns {RouteInfo[]}
*/
this.$routeKeepingOrder = function $routeKeepingOrder(startSystem, waypoints, routeMode) {
if (routeMode === "OPTIMIZED_BY_NONE" || waypoints.length === 0) return [];
const partialRoutesCount = waypoints.length;
const partialRoutes = new Array(partialRoutesCount);
let previusWaypoint, currentWaypoint = startSystem;
let aInfo, bInfo = System.infoForSystem(galaxyNumber, startSystem.ID);
for (let i = 0; i < partialRoutesCount; i++) {
previusWaypoint = currentWaypoint;
currentWaypoint = waypoints[i];
aInfo = bInfo;
bInfo = System.infoForSystem(galaxyNumber, currentWaypoint.ID);
let routeInfo = aInfo.routeToSystem(bInfo, routeMode);
routeInfo.startWaypointInfo = previusWaypoint;
routeInfo.endWaypointInfo = currentWaypoint;
partialRoutes[i] = aInfo.routeToSystem(bInfo, routeMode);
}
return partialRoutes;
}
this.$memoizedRoutes = {
routeCost: function (from, to) {
if (to === null || to === undefined || from === to)
return { distance: 0, time: 0, through: null };
// routes are symmetric, right?
if (from <= to) {
a = from;
b = to
} else {
a = to;
b = from;
}
const a = from < to ? from : to;
const b = a === from ? to : from;
let aCosts = this.$costsMatrix[a];
if (aCosts === undefined) {
aCosts = this.$costsMatrix[a] = {};
}
let routeCost = aCosts[b];
if (routeCost === undefined) {
const aSystemInfo = System.infoForSystem(galaxyNumber, a);
const bSystemInfo = System.infoForSystem(galaxyNumber, b);
const routeInfoByJumps = aSystemInfo.routeToSystem(bSystemInfo, "OPTIMIZED_BY_JUMPS");
const routeInfoByTime = aSystemInfo.routeToSystem(bSystemInfo, "OPTIMIZED_BY_TIME");
if (routeInfoByJumps == null)
routeCost = aCosts[b] = null;
else {
routeCost = aCosts[b] = {
distance: routeInfoByJumps.distance,
timeWhenDistance: routeInfoByJumps.time,
time: routeInfoByTime.time,
distanceWhenTime: routeInfoByTime.distance,
through: null
};
}
}
return routeCost;
},
$costsMatrix: {
/*
systemNumberFrom: {
systemNumberTo: {
distance: 0
timeWhenDistance: 0, // time when optimized by distance
time: 0
distanceWhenTime: 0, // distance when optimized by time
through: null
}
}
*/
}
}
/*************** Chart drawing operations ***************/
/**
* Remove stored routes from chart but keep them stored
*/
this.$eraseStoredRoutes = function () {
for (let i = 0; i < this.$storedRoutes.length; i++) {
this.$eraseRoute(this.$storedRoutes[i]);
}
}
/**
* Remove stored routes from chart, draw and store new routes.
* If need remove current routes and do not draw new pass [] as newRoutes
* @param {RouteInfo[]} newRoutes
*/
this.$redrawRoutes = function (newRoutes, lineColor) {
if (newRoutes) {
for (let i = 0; i < this.$storedRoutes.length; i++) {
this.$eraseRoute(this.$storedRoutes[i]);
}
} else {
newRoutes = this.$storedRoutes;
}
for (let i = 0; i < newRoutes.length; i++) {
/*let a = i;
new Timer(this, function() {
this.$drawRoute(newRoutes[a]);
}, i);
*/
this.$drawRoute(newRoutes[i], lineColor);
}
this.$storedRoutes = newRoutes;
}
/**
* Remove first stored route, erase it from chart and return
* @returns {RouteInfo} Removed route
*/
this.$removeFirstStoredRoute = function () {
const route = this.$storedRoutes.shift();
this.$eraseRoute(route);
return route;
}
this.$drawRoute = function (routeInfo, lineColor) {
for (var i = 0; i < routeInfo.route.length - 1; i++) {
if (routeInfo.route[i] < routeInfo.route[i + 1]) {
SystemInfo.setInterstellarProperty(galaxyNumber, routeInfo.route[i], routeInfo.route[i + 1], 2, "link_color", lineColor);
} else {
SystemInfo.setInterstellarProperty(galaxyNumber, routeInfo.route[i + 1], routeInfo.route[i], 2, "link_color", lineColor);
}
}
}
this.$eraseRoute = function (routeInfo) {
mission.unmarkSystem({
system: routeInfo.route[0],
name: this.name
});
for (var i = 0; i < routeInfo.route.length - 1; i++) {
if (routeInfo.route[i] < routeInfo.route[i + 1]) {
SystemInfo.setInterstellarProperty(galaxyNumber, routeInfo.route[i], routeInfo.route[i + 1], 2, "link_color", null);
} else {
SystemInfo.setInterstellarProperty(galaxyNumber, routeInfo.route[i + 1], routeInfo.route[i], 2, "link_color", null);
}
}
}
this.$storedUserWaypoints = [];
/**
* Erase stored user waypoints from chart, draw and store new.
* @param {WaypointInfo[]} newUserWaypoints
* @param {string} userWaypointColor
* @param {string} userWaypointShape
*/
this.$redrawUserWaypoints = function $redrawUserWaypoints (newUserWaypoints, userWaypointColor, userWaypointShape) {
const storedUserWaypoints = this.$storedUserWaypoints;
for(let i = 0; i < storedUserWaypoints.length; i++) {
mission.unmarkSystem({
system: storedUserWaypoints[i],
name: this.name
});
}
this.$storedUserWaypoints = (newUserWaypoints || []).slice();
for(let i = 0; i < storedUserWaypoints.length; i++) {
mission.markSystem({
system: newUserWaypoints[i],
name: this.name,
markerColor: userWaypointColor,
markerShape: userWaypointShape
});
}
} |