Starystars67 011769d0b6 New concept MainMenu is now loosely ready for testing.
This is a functional mainmenu which can be tested in-game.
2023-05-08 22:31:07 +01:00

2336 lines
75 KiB
JavaScript

angular.module('beamng.stuff', ['ngAnimate', 'toastr'])
angular.module('beamng.gameUI', ['ngAnimate', 'toastr'])
angular.module('beamng.color', [])
angular.module('beamng.gamepadNav', [])
angular.module('beamng.controls', [])
angular.module('BeamNG.ui', ['beamng.core', 'beamng.components', 'beamng.data', 'ngMaterial', 'ngAnimate', 'ui.router', 'beamng.stuff', 'beamng.gameUI', 'beamng.apps', 'beamng.color', 'pascalprecht.translate', 'beamng.gamepadNav', 'beamng.controls', 'fc.paging','ngSanitize','jkAngularRatingStars','ngFitText'])
.config(['$compileProvider', '$logProvider', '$stateProvider', '$urlRouterProvider', '$mdThemingProvider', '$translateProvider', 'toastrConfig', '$provide',
function($compileProvider, $logProvider, $stateProvider, $urlRouterProvider, $mdThemingProvider, $translateProvider, toastrConfig, $provide) {
$translateProvider.useStaticFilesLoader({
prefix: '/locales/',
suffix: '.json'
})
$translateProvider.useSanitizeValueStrategy('escaped')
$translateProvider.preferredLanguage('en-US') // this is the default language to load
// this is the fallback in case individual translations are missing:
if (beamng.shipping) {
$translateProvider.fallbackLanguage('en-US')
} else {
$translateProvider.fallbackLanguage(['en-US', 'not-shipping.internal'])
}
//$translateProvider.use('de-DE')
//DISABLE_TRANSLATIONS = true
$logProvider.debugEnabled(false)
// ..... User Interface states
$stateProvider
.state('play', {
url: '/play',
templateUrl: '/ui/modules/play/play.html',
controller: 'PlayController as playCtrl',
menuActionMapEnabled: false, // defaults to true
uiAppsShown: true, // defaults to false
})
.state('menu', {
url: '/menu',
templateUrl: '/ui/modules/menu/menu.html',
controller: 'MenuController as menuCtrl',
uiLayout: 'menu',
uiAppsShown: true, // defaults to false
})
// so the trick is: we load the real menu in the background in another view while the startscreen is covering it up
// This ensures that everything is properly loaded before we switch to it.
// Ideally, no dom changes are required then :)
// more docs to read: https://github.com/angular-ui/ui-router/wiki/Multiple-Named-Views#view-names---relative-vs-absolute-names
// this is a three stage process right now:
// 1) load menu.start which just loads the startscreen
// 2) the controller will navigate after some tiny bit to 'menu.start_loadmainmenu'
// 3) the controller will navigate after 3 seconds to 'menu.mainmenu'
// this process ensures that:
// a) BeamNG Logo is not perceived as 'lagging' as the mainmenu is loading at the same time.
// b) Mainmenu can load safely with the start screen fully done with everything.
.state('menu.start', {
loaderVisible: true,
views: {
'loader@': { // target the loader view in parent menu state
templateUrl: '/ui/modules/startScreen/startScreen.html',
controller: 'startScreenController as startScreen',
},
'@menu': { }
},
})
.state('menu.start_loadmainmenu', {
loaderVisible: true,
views: {
'loader@': { // target the loader view in parent menu state
templateUrl: '/ui/modules/startScreen/startScreen.html',
controller: 'startScreenController as startScreen',
},
'@menu': { // target the unnamed view in parent menu state
templateUrl: '/ui/modules/menu/menu.html',
controller: 'MenuController as menuCtrl',
}
},
transitionAnimation: 'moduleBlendOnLeave',
})
.state('menu.mainmenu', {
views: {
'loader':{}, // empty the loader view
'@menu': { // targe the unnamed default view in the menu parent state
templateUrl: `/ui/modules/mainmenu/drive/mainmenu.html`,
controller: 'MainMenuController as mmCtrl',
}
}
})
.state('menu.onlineFeatures', {
url: '/onlineFeatures',
templateUrl: `/ui/modules/onlineFeatures/online.html`,
controller: 'OnlineFeaturesController',
backState: 'menu.mainmenu',
})
.state('menu.bigmap', {
url: '/bigmap',
templateUrl: '/ui/modules/bigmap/bigmap.html',
controller: 'BigMapController',
backState: 'BACK_TO_MENU',
params: {
missionId: null
},
careerUiLayout: 'careerBigMap',
uiLayout: 'blank',
uiAppsShown: true, // defaults to false
})
.state('menu.levels', {
url: '/levels',
templateUrl: '/ui/modules/levelselect/levelselect.html',
controller: 'LevelSelectController as lsCtrl',
backState: 'menu.mainmenu',
})
.state('menu.levelDetails', {
url: '/levels-details/:levelName',
templateUrl: '/ui/modules/levelselect/levelselect-details.html',
controller: 'LevelSelectDetailsController as levelsDetails',
backState: 'menu.levels',
})
.state('menu.busRoutes', {
url: '/bus',
templateUrl: '/ui/modules/busRoute/busRoute.html',
controller: 'BusRoutesController as busCtrl',
backState: 'menu.mainmenu',
})
.state('menu.busRoutesLevelSelect', {
url: '/bus/level',
templateUrl: '/ui/modules/busRoute/levelSelect.html',
controller: 'BusRoutesLevelController',
backState: 'menu.busRoutes',
})
// .state('menu.busRoutesVehicleSelect', {
// url: '/bus/vehicle/:garage/:mode/:event',
// templateUrl: '/ui/modules/vehicleselect/vehicleselect.html',
// controller: 'VehicleSelectController as vehicles',
// backState: 'menu.busRoutes',
// })
.state('menu.busRoutesRouteSelect', {
url: '/bus/route',
templateUrl: '/ui/modules/busRoute/routeSelect.html',
controller: 'BusRoutesRouteController',
backState: 'menu.busRoutes',
})
.state('menu.environment', {
url: '/environment',
templateUrl: '/ui/modules/environment/environment.html',
controller: 'EnvironmentController as environment',
backState: 'BACK_TO_MENU',
})
// Track Builder
// .state('menu.trackBuilder', {
// url: '/trackBuilder',
// templateUrl: '/ui/modules/trackBuilder/trackBuilder.html',
// controller: 'TrackBuilderController as trackBuilder'
// })
.state('menu.scenarios', {
url: '/scenarios',
templateUrl: '/ui/modules/scenarioselect/scenarioselect.html',
controller: 'ScenarioSelectController',
backState: 'menu.mainmenu',
})
.state('menu.campaigns', {
url: '/campaigns',
templateUrl: '/ui/modules/campaignselect/campaignselect.html',
controller: 'CampaignSelectController as campaignSelect',
backState: 'menu.mainmenu',
})
.state('menu.appedit', {
url: '/appedit/:mode',
templateUrl: '/ui/modules/appedit/appedit.html',
controller: 'AppEditController as ctrl',
backState: 'BACK_TO_MENU',
uiAppsShown: true, // defaults to false
})
.state('menu.appselect', {
url: '/appselect',
templateUrl: '/ui/modules/appselect/appselect.html',
controller: 'AppSelectController as apps',
backState: 'menu.appedit',
})
.state('menu.vehicles', {
url: '/vehicleselect/:garage/:mode/:event',
templateUrl: '/ui/modules/vehicleselect/vehicleselect.html',
controller: 'VehicleSelectController as vehicles',
backState($scope, $state, $stateParams) {
if ($scope.gameState === 'garage')
return 'garage';
if ($stateParams && $stateParams.hasOwnProperty('mode')) {
switch ($stateParams.mode) {
case 'busRoutes': return 'menu.busRoutes';
case 'lightRunner': return 'menu.lightrunnerOverview';
}
}
return 'BACK_TO_MENU';
},
})
.state('menu.vehiclesdetails', {
url: '/vehicle-details/:model/:config/:mode/:event/{showAuxiliary:bool}',
templateUrl: '/ui/modules/vehicleselect/vehicleselect-details.html',
controller: 'VehicleDetailsController as vehicle',
backState: 'menu.vehicles',
})
.state('menu.options', {
url: '/options',
templateUrl: '/ui/modules/options/options.html',
controller: 'OptionsController',
controllerAs: 'options',
backState: 'BACK_TO_MENU',
abstract: true
})
.state('menu.options.help', {
url: '/help',
templateUrl: '/ui/modules/options/help.partial.html',
controller: 'SettingsHelpCtrl as opt',
backState: 'BACK_TO_MENU',
})
.state('menu.options.performance', {
url: '/performance',
templateUrl: '/ui/modules/options/performance.partial.html',
controller: 'SettingsPerformanceCtrl as opt',
backState: 'BACK_TO_MENU',
})
.state('menu.options.display', {
url: '/display',
templateUrl: '/ui/modules/options/display.partial.html',
controller: 'SettingsGraphicsCtrl as opt',
backState: 'BACK_TO_MENU',
})
.state('menu.options.graphics', {
url: '/graphics',
templateUrl: '/ui/modules/options/graphics.partial.html',
controller: 'SettingsGraphicsCtrl as opt',
backState: 'BACK_TO_MENU',
})
.state('menu.options.audio', {
url: '/audio',
templateUrl: '/ui/modules/options/audio.partial.html',
controller: 'SettingsAudioCtrl as opt',
backState: 'BACK_TO_MENU',
})
.state('menu.options.gameplay', {
url: '/gameplay',
templateUrl: '/ui/modules/options/gameplay.partial.html',
controller: 'SettingsGameplayCtrl as opt',
backState: 'BACK_TO_MENU',
})
.state('menu.options.camera', {
url: '/camera',
templateUrl: '/ui/modules/options/camera.partial.html',
controller: 'SettingsCameraCtrl as opt',
backState: 'BACK_TO_MENU',
})
.state('menu.options.userInterface', {
url: '/userInterface',
templateUrl: '/ui/modules/options/userinterface.partial.html',
controller: 'SettingsUserInterfaceCtrl as opt',
backState: 'BACK_TO_MENU',
})
.state('menu.options.language', {
url: '/language',
templateUrl: '/ui/modules/options/language.partial.html',
controller: 'SettingsLanguageCtrl as opt',
backState: 'BACK_TO_MENU',
})
.state('menu.options.stats', {
url: '/stat:category',
templateUrl: '/ui/modules/stat/stats.html',
controller: 'StatsController as statCtrl',
backState: 'menu.mainmenu',
})
.state('menu.options.other', {
url: '/other',
templateUrl: '/ui/modules/options/other.partial.html',
controller: 'SettingsOtherCtrl as opt',
backState: 'BACK_TO_MENU',
})
.state('menu.options.licenses', {
url: '/licenses',
templateUrl: '/ui/modules/options/licenses.partial.html',
controller: 'SettingsLicensesCtrl as opt',
backState: 'BACK_TO_MENU',
})
.state('menu.options.controls', {
url: '/controls',
templateUrl: '/ui/modules/options/controls.html',
controller: 'ControlsController as controls',
backState: 'BACK_TO_MENU',
})
.state('menu.options.controls.bindings', {
views: {
'': {
url: '/bindings',
templateUrl: '/ui/modules/options/controls-bindings.html',
controller: 'ControlsBindingsCtrl as controlsBindings'
}
},
backState: 'BACK_TO_MENU',
})
.state('menu.options.controls.bindings.edit', {
views: {
'edit@menu.options': {
url: '/edit',
templateUrl: '/ui/modules/options/controls-edit.html',
controller: 'ControlsEditCtrl as controlsEdit'
}
},
params: {action: '', oldBinding: {}},
backState: 'BACK_TO_MENU',
})
.state('menu.options.controls.filters', {
views: {
'': {
url: '/filters',
templateUrl: '/ui/modules/options/controls-filters.html',
controller: 'ControlsFiltersCtrl as controlsFilters'
},
},
backState: 'BACK_TO_MENU',
})
.state('menu.options.controls.ffb', {
views: {
'': {
url: '/ffb',
templateUrl: '/ui/modules/options/controls-ffb.html',
controller: 'ControlsFfbCtrl as controlsFfb'
}
},
backState: 'BACK_TO_MENU',
})
.state('menu.options.controls.ffb.edit', {
views: {
'edit@menu.options': {
url: '/edit',
templateUrl: '/ui/modules/options/controls-edit.html',
controller: 'ControlsEditCtrl as controlsEdit'
}
},
params: {action: '', oldBinding: {}},
backState: 'BACK_TO_MENU',
})
.state('menu.options.controls.hardware', {
views: {
'': {
url: '/hardware',
templateUrl: '/ui/modules/options/controls-hardware.html',
controller: 'ControlsHardwareCtrl as controlsHw'
}
},
backState: 'BACK_TO_MENU',
})
.state('menu.vehicleconfig', {
url: '/vehicle-config',
templateUrl: '/ui/modules/vehicleconfig/vehicleconfig.html',
controller: 'VehicleconfigCtrl',
redirectTo: 'menu.vehicleconfig.parts',
backState: 'BACK_TO_MENU',
})
.state('menu.vehicleconfig.parts', {
url: '/vehicle-config/parts',
templateUrl: '/ui/modules/vehicleconfig/partial.parts.html',
controller: 'Vehicleconfig_parts as vehConf_parts',
backState: 'BACK_TO_MENU',
uiAppsShown: true, // defaults to false
})
.state('menu.vehicleconfig.tuning', {
url: '/vehicle-config/tuning',
templateUrl: '/ui/modules/vehicleconfig/partial.tuning.html',
controller: 'Vehicleconfig_tuning as vehConf_tuning',
backState: 'BACK_TO_MENU',
uiAppsShown: true, // defaults to false
})
.state('menu.vehicleconfig.color', {
url: '/vehicle-config/color',
templateUrl: '/ui/modules/vehicleconfig/partial.color.html',
controller: 'Vehicleconfig_color as vehConf_color',
backState: 'BACK_TO_MENU',
})
.state('menu.vehicleconfig.save', {
url: '/vehicle-config/save',
templateUrl: '/ui/modules/vehicleconfig/partial.save.html',
controller: 'Vehicleconfig_save as vehConf_save',
backState: 'BACK_TO_MENU',
})
.state('menu.vehicleconfig.debug', {
url: '/vehicle-config/debug',
templateUrl: '/ui/modules/vehicleconfig/debug.partial.html',
controller: 'Vehicleconfig_debug as vehConf_debug',
backState: 'BACK_TO_MENU',
uiAppsShown: true, // defaults to false
})
.state('menu.mods', {
url: '/mods',
template: '<ui-view class="filler" style="position: relative;"></ui-view>',
abstract: true,
controller: 'ModManagerController as modCtrl',
backState: 'BACK_TO_MENU',
})
.state('menu.mods.local', {
url: '/local',
views: {
'': {
controller: 'LocalModController as modLoclCtrl',
templateUrl: '/ui/modules/modmanager/mods.html'
},
'content@menu.mods.local': {
templateUrl: '/ui/modules/modmanager/local.html'
},
'filter@menu.mods.local': {
templateUrl: '/ui/modules/modmanager/filter.html'
}
},
backState: 'BACK_TO_MENU',
})
.state('menu.mods.downloaded', {
url: '/downloaded',
views: {
'': {
controller: 'DownloadModController as modDwlCtrl',
templateUrl: '/ui/modules/modmanager/mods.html'
},
'content@menu.mods.downloaded': {
templateUrl: '/ui/modules/modmanager/downloaded.html',
}
},
backState: 'BACK_TO_MENU',
})
.state('menu.mods.scheduled', {
url: '/scheduled',
views: {
'': {
controller: 'ScheduledModController as modSchCtrl',
templateUrl: '/ui/modules/modmanager/mods.html'
},
'content@menu.mods.scheduled': {
templateUrl: '/ui/modules/modmanager/scheduled.html',
},
'filter@menu.mods.scheduled': {
templateUrl: '/ui/modules/modmanager/scheduled_conflict.html'
}
},
backState: 'BACK_TO_MENU',
})
.state('menu.mods.repository', {
url: '/repository?query',
views: {
'': {
controller: 'RepositoryController as repo',
templateUrl: '/ui/modules/modmanager/mods.html'
},
'content@menu.mods.repository': {
templateUrl: '/ui/modules/repository/repository.html'
},
'filter@menu.mods.repository': {
templateUrl: '/ui/modules/repository/filter.html'
}
},
backState: 'BACK_TO_MENU',
})
.state('menu.mods.automation', {
url: '/automation?query',
views: {
'': {
controller: 'AutomationController as automation',
templateUrl: '/ui/modules/modmanager/mods.html'
},
'content@menu.mods.automation': {
templateUrl: '/ui/modules/automation/automation.html'
},
'filter@menu.mods.automation': {
templateUrl: '/ui/modules/automation/filter.html'
}
},
backState: 'BACK_TO_MENU',
})
.state('menu.mods.automationDetails', {
url: '/automation/detail/{modId:[0-9A-Z]+}?page&param',
views: {
'': {
controller: 'AutomationDetailsController as automationDetailCtrl',
templateUrl: '/ui/modules/modmanager/mods.html'
},
'content@menu.mods.automationDetails': {
templateUrl: '/ui/modules/automation/automation-details.html'
},
'filter@menu.mods.automationDetails': {
templateUrl: '/ui/modules/automation/info.html'
}
},
backState: 'BACK_TO_MENU',
})
.state('menu.mods.details', {
url: '/detail/{modId:[0-9A-Z]+}?page&param',
views: {
'': {
controller: 'RepositoryDetailsController as repoDetailCtrl',
templateUrl: '/ui/modules/modmanager/mods.html'
},
'content@menu.mods.details': {
templateUrl: '/ui/modules/repository/repository-details.html'
},
'filter@menu.mods.details': {
templateUrl: '/ui/modules/repository/info.html'
}
},
backState: 'menu.mods.repository',
})
.state('menu.modsDetails', {
url: '/modmanager/details:modFilePath',
templateUrl: '/ui/modules/modmanager/info.html',
controller: 'ModManagerControllerDetails as managerDetailCtrl',
backState: 'menu.mods.repository',
})
.state('menu.gameContext', {
url: '/gameContext',
templateUrl: '/ui/modules/gameContext/gameContext.html',
controller: 'GameContextController',
backState: 'BACK_TO_MENU',
})
.state('scenario-start', {
url: '/scenariocontrol/start',
params: {
data: {}
},
templateUrl: '/ui/modules/scenariocontrol/start.html',
controller: 'ScenarioStartController as scenarioStart',
backState: 'BACK_TO_MENU',
uiLayout: 'blank',
uiAppsShown: true
})
.state('scenario-end', {
url: '/scenariocontrol/end',
params: {
missionData: {},
stats: {},
rewards: {},
portrait: {}
},
templateUrl: '/ui/modules/scenariocontrol/end.html',
controller: 'ScenarioEndController',
backState: 'BACK_TO_MENU',
careerUiLayout: 'careerMissionEnd',
uiAppsShown: true
})
.state('quickrace-end', {
url: '/quickraceEnd',
params: {
stats: {},
mockScenario: {}
},
templateUrl: '/ui/modules/scenariocontrol/quickraceEnd.html',
controller: 'ScenarioEndController',
backState: 'BACK_TO_MENU',
})
.state('chapter-end', {
url: '/cchapterEnd',
params: {
stats: {}
},
templateUrl: '/ui/modules/scenariocontrol/end.html',
controller: 'ScenarioEndController',
backState: 'BACK_TO_MENU',
})
// Transition to this state is handled by some unknown dark force (Torque?).
// Until this chanages, keep the url hash to "loading".
.state('loading', {
url: '/loading',
templateUrl: '/ui/modules/loading/loading.html',
controller: 'LoadingController as loading',
transitionAnimation: 'moduleBlendOnLeave',
backState: 'BLOCK',
})
.state('comic', {
url: '/comic',
params: {
comiclist: {}
},
templateUrl: '/ui/modules/comic/comic.html',
controller: 'ComicController',
backState: null,
})
.state('menu.photomode', {
url: '/photo-mode',
templateUrl: '/ui/modules/photomode/photomode.html',
controller: 'PhotoModeController as photo',
backState: 'BACK_TO_MENU',
})
.state('menu.replay', {
url: '/replay',
templateUrl: '/ui/modules/replay/replay.html',
controller: 'ReplayController as replay',
backState: 'BACK_TO_MENU',
})
.state('blank', {
uiAppsShown: true
})
.state('iconViewer', {
url: '/iconViewer',
templateUrl: '/ui/modules/iconView/icons.html',
controller: 'iconViewerCtrl as iconCtrl',
backState: 'BACK_TO_MENU',
})
.state('fadeScreen', {
url: '/fadeScreen',
templateUrl: '/ui/modules/fadeScreen/fadeScreen.html',
params: {
fadeIn: 1,
pause: 0,
fadeOut: 1,
data: {}
},
controller: 'fadeScreen',
backState: 'BLOCK',
})
.state('mapview', {
url: '/mapview',
templateUrl: '/ui/modules/mapview/mapview.html',
controller: 'MapViewCtrl as mapview',
params: {
data: {}
},
backState: 'BACK_TO_MENU',
// params: {
// baseImg: '',
// points: [],
// onClick: ''
// }
})
//Dragrace states WIP
.state('menu.dragRaceOverview', {
url: '/dragrace/overview',
templateUrl: '/ui/modules/dragrace/overview.html',
controller: 'DragRaceController',
params: {
results: {},
cinematicEnabled: true
},
backState: 'BACK_TO_MENU',
})
// LightRunner States
.state('menu.lightrunnerOverview', {
url: '/lightrunner/overview',
templateUrl: '/ui/modules/lightrunner/overview.html',
controller: 'LightRunnerController',
backState: 'BACK_TO_MENU',
})
.state('menu.lightrunnerTrackSelect', {
url: '/lightrunner/track',
templateUrl: '/ui/modules/lightrunner/trackSelect.html',
controller: 'LightRunnerTrackController',
backState: 'BACK_TO_MENU',
})
//Quickrace states WIP
.state('menu.quickraceOverview', {
url: '/quickrace/overview',
templateUrl: '/ui/modules/quickrace/overview.html',
controller: 'QuickraceController',
backState: 'BACK_TO_MENU',
})
.state('menu.quickraceLevelselect', {
url: '/quickrace/level',
templateUrl: '/ui/modules/quickrace/levelSelect.html',
controller: 'QuickraceLevelController',
backState: 'menu.quickraceOverview',
})
.state('menu.quickraceTrackselect', {
url: '/quickrace/track',
templateUrl: '/ui/modules/quickrace/trackSelect.html',
controller: 'QuickraceTrackController',
backState: 'menu.quickraceLevelselect',
})
.state('campaign', {
url: '/campaign',
template: '<ui-view class="container"></ui-view>',
backState: 'BACK_TO_MENU',
})
.state('campaign.quickraceOverview', {
url: '/quickrace/overview',
params: {
level: {},
track: {},
vehicles: {},
},
templateUrl: '/ui/modules/quickrace/overview.html',
controller: 'QuickraceController',
backState: 'BACK_TO_MENU',
})
.state('campaign.quickraceLevelselect', {
url: '/quickrace/level',
templateUrl: '/ui/modules/quickrace/levelSelect.html',
controller: 'QuickraceLevelController',
backState: 'BACK_TO_MENU',
})
.state('campaign.quickraceTrackselect', {
url: '/quickrace/track',
templateUrl: '/ui/modules/quickrace/trackSelect.html',
controller: 'QuickraceTrackController',
backState: 'BACK_TO_MENU',
})
.state('campaign.vehicles', {
url: '/vehicleselect/:garage/:mode',
templateUrl: '/ui/modules/vehicleselect/vehicleselect.html',
controller: 'VehicleSelectController as vehicles',
backState: 'BACK_TO_MENU',
})
.state('campaign.vehicleDetails', {
url: '/vehicle-details/:model/:config/:mode',
templateUrl: '/ui/modules/vehicleselect/vehicleselect-details.html',
controller: 'VehicleDetailsController as vehicle',
backState: 'BACK_TO_MENU',
})
.state('garage', {
url: '/garage',
templateUrl: '/ui/modules/garage/garage.html',
controller: 'GarageController as garageCtrl',
// menuActionMapEnabled: false,
uiAppsShown: true,
// uiLayout: 'garage',
backState: "menu.mainmenu",
})
.state('menu.partInventory', {
url: '/partInventory',
params: {
},
templateUrl: '/ui/modules/partInventory/partInventory.html',
controller: 'PartInventoryController',
uiAppsShown: true,
backState: "menu.mainmenu",
})
.state('menu.career', {
url: '/career',
templateUrl: '/ui/modules/career/career.html',
controller: 'CareerController',
backState: 'BACK_TO_MENU',
})
.state('menu.careermission', {
url: '/career-mission',
templateUrl: '/ui/modules/careermission/mission.html',
controller: 'GameContextController',
params: {
isCareer: true
},
backState: 'BACK_TO_MENU',
uiLayout: 'blank',
uiAppsShown: true, // defaults to false,
})
.state('menu.careerPause', {
url: '/careerPause',
templateUrl: '/ui/modules/careerPause/careerPause.html',
controller: 'CareerPauseController',
backState: 'BACK_TO_MENU',
careerUiLayout: 'careerPause',
uiAppsShown: true,
})
.state('menu.careerQuests', {
url: '/career-quest',
templateUrl: '/ui/modules/careerQuests/questsOverview.html',
controller: 'QuestOverviewController',
params: {
questId: undefined
},
backState: 'BACK_TO_MENU',
})
.state('menu.careerVehicleSelect', {
url: '/careerVehicleSelect',
templateUrl: '/ui/modules/careerVehicleSelect/careerVehicleSelect.html',
params: {
data: {}
},
//uiAppsShown: false,
controller: 'CareerVehicleSelectController',
backState: 'BACK_TO_MENU',
})
.state('menu.threeElementSelect', {
url: '/threeElementSelect',
templateUrl: '/ui/modules/threeElementSelect/threeElementSelect.html',
params: {
data: {}
},
//uiAppsShown: false,
controller: 'ThreeElementSelectController',
backState: 'BACK_TO_MENU',
})
.state('menu.careerLogBook', {
url: '/careerLogBook',
templateUrl: '/ui/modules/careerLogBook/logBook.html',
params: {
entryId: undefined
},
//uiAppsShown: false,
controller: 'CareerLogBookController',
backState: 'BACK_TO_MENU',
})
// default entry that is loaded on startup:
$urlRouterProvider.otherwise('menu.start')
$compileProvider.debugInfoEnabled(false)
// whitelist for local:// prefix
$compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|ftp|mailto|local):/)
$compileProvider.imgSrcSanitizationWhitelist(/^\s*(https?|ftp|mailto|local):/)
let theme = $mdThemingProvider.theme('default')
theme.dark()
$mdThemingProvider.definePalette('customPrimary', {
'50': '#ffffff',
'100': '#e9e9e9',
'200': '#d3d3d3',
'300': '#bebebe',
'400': '#a9a9a9',
'500': '#959595',
'600': '#818181',
'700': '#6d6d6d',
'800': '#5b5b5b',
'900': '#484848',
'A100': '#373737',
'A200': '#262626', // ### hue-3
'A400': '#171717', // ### hue-2
'A700': '#000000',
'contrastDefaultColor':'dark',
'contrastLightColors': ['600', '700', '800', '900', 'A100', 'A200', 'A400', 'A700']
})
theme.primaryPalette('customPrimary', {
'default': '800', // by default use shade A800 from the custom palette for primary intentions
'hue-1': 'A100', // use shade A900 for the <code>md-hue-1</code> class
'hue-2': 'A400', // use shade A400 for the <code>md-hue-2</code> class
'hue-3': '50', // use shade A100 for the <code>md-hue-3</code> class
})
$mdThemingProvider.definePalette('customAccent', {
'50': '#662800',
'100': '#803200',
'200': '#993c00',
'300': '#b34600',
'400': '#cc5000',
'500': '#e65a00',
'600': '#ff741a',
'700': '#ff8333',
'800': '#ff934d',
'900': '#ffa266',
'A100': '#ff741a',
'A200': '#ff6400',
'A400': '#e65a00',
'A700': '#ffb280',
'contrastDefaultColor':'dark',
'contrastLightColors': ['500', '600', '700', '800', '900', 'A100', 'A200', 'A400', 'A700']
})
theme.accentPalette('customAccent', {
'default': '500',
'hue-1': '300',
'hue-2': '800',
'hue-3': 'A100',
})
$mdThemingProvider.definePalette('customWarn', {
'50': '#ff7b82',
'100': '#ff626a',
'200': '#ff4852',
'300': '#ff2f3a',
'400': '#ff1522',
'500': '#fb000d',
'600': '#e1000c',
'700': '#c8000a',
'800': '#ae0009',
'900': '#950008',
'A100': '#ff959a',
'A200': '#ffaeb3',
'A400': '#ffc8cb',
'A700': '#7b0006',
'contrastDefaultColor':'dark',
'contrastLightColors': ['300','400','500', '600', '700', '800', '900', 'A200', 'A400', 'A700']
})
theme.warnPalette('customWarn', {
'default': '400',
'hue-1': '500',
'hue-2': '100',
'hue-3': 'A700',
})
$mdThemingProvider.definePalette('customBackground', {
'50': 'rgba(35,35,35,0.35)',
'100': 'rgba(35,35,35,0.4)',
//workaround for now, no clue why checkboxes are using '200'
//200': 'rgba(35,35,35,0.45)',
'200': '#FFF',
'300': 'rgba(35,35,35,0.5)',
'400': 'rgba(35,35,35,0.55)',
'500': 'rgba(35,35,35,0.6)',
'600': 'rgba(35,35,35,0.65)',
'700': 'rgba(35,35,35,0.7)',
'800': 'rgba(35,35,35,0.75)',
'900': 'rgba(35,35,35,0.8)',
'A100':'rgba(35,35,35,0.85)',
'A200':'rgba(35,35,35,0.9)',
'A400':'rgba(35,35,35,0.95)',
'A700':'rgba(35,35,35,1)',
'contrastDefaultColor':'light',
})
theme.backgroundPalette('customBackground', {
'default': 'A400',
'hue-1': '300',
'hue-2': '600',
'hue-3': '900',
})
// debug with this:
//console.log("UI theme setup: ", $mdThemingProvider)
angular.extend(toastrConfig, {
autoDismiss: false,
containerId: 'toast-container',
maxOpened: 10,
newestOnTop: true,
positionClass: 'toast-top-right',
preventOpenDuplicates: true,
allowHtml: true,
})
}])
.run(['$animate', '$http', '$rootScope', '$templateCache', '$window', '$translate', 'UIAppStorage', 'Settings', 'SettingsAuxData', 'bngWSApi', '$state',
function ($animate, $http, $rootScope, $templateCache, $window, $translate, UIAppStorage, Settings, SettingsAuxData, bngWSApi, $state) {
// apply language settings
$rootScope.$on('SettingsChanged', function(evt, data) {
if(data.values.uiLanguage) {
let lang = data.values.uiLanguage
if(lang == '') lang = 'en-US'
$http.get(`/locales/${lang}.json`,).then(function(res) {
vueI18n.global.locale = lang
vueI18n.global.setLocaleMessage(lang, res.data)
})
}
})
$http.get('/ui/modules/vehicleconfig/vehicle-config-tree.html').then(function (tmpl) {
$templateCache.put('vehicle-config-tree', tmpl.data)
})
$http.get('/ui/assets/sprites/svg-symbols.svg')
.success(svgSprite => {
var iconsSprite = angular.element(svgSprite)
angular.element(document.head).append(iconsSprite)
})
window.globalAngularRootScope = $rootScope
/* --- VUE3 START --- */
// i18n vue3 basics
window.bngVue && window.bngVue.start({
i18n: vueI18n
})
i18NLanguageFinished = true
/* --- VUE3 END --- */
$rootScope.$on('$translateChangeSuccess', (event, data) => {
i18nLanguageUsed = data.language
})
$rootScope.$on('$translateChangeStart', () => {
i18NLanguageFinished = false
})
$rootScope.$on('$translateChangeEnd', () => {
i18NLanguageFinished = true
})
/*
$rootScope.$on('$translateChangeError', () => { console.log('translateChangeError')})
$rootScope.$on('$translateLoadingStart', () => { console.log('translateLoadingStart')})
$rootScope.$on('$translateLoadingSuccess', () => { console.log('translateLoadingSuccess')})
$rootScope.$on('$translateLoadingError', () => { console.log('translateLoadingError')})
$rootScope.$on('$translateLoadingEnd', () => { console.log('translateLoadingEnd')})
$rootScope.$on('$translatePartialLoaderStructureChanged', () => { console.log('translatePartialLoaderStructureChanged')})
*/
//$animate.enabled(false)
bngApi.engineLua('ui_apps.requestUIAppsData()')
// ..... Define all objects attached directly to the window object here
/**
* HookManager is maybe the most important object to share status between
* the game and the user interface. Common usage from the game modules looks
* like HookManager.trigger(<event name>, <data>). In order to catch these events
* inside a controller, one has to set up a listener like
*
* @example
* $scope.$on('EventName', function (event, data) {
* // do all kinds of stuff with data...
* })
*
* Angular's event system manages the listener's removal on the $destroy event of
* the current scope.
**/
// listen to the window resize event. Maybe this can also be handled from the CEF side.
angular.element($window).bind('resize', function () {
var size = {width: window.innerWidth, height: window.innerHeight}
$rootScope.$broadcast('windowResize', size)
})
// This should not be a function attached to the window object, but rather a HookManager event.
// Until this is done, we just mock up the process.
// $window.updateProgress = function(val, txt) {
// $rootScope.$broadcast('UpdateProgress', {value: Math.floor(100 * val), text: txt })
// }
// Update game state each time a route change is triggered.
// Maybe an overkill, but why not be sure?
//$rootScope.$on('$stateChangeSuccess', function (event, toState, toStateParams) {
//})
// // settings storage for simple consumers in JS
// $rootScope.Settings = null
// $rootScope.$on('SettingsChanged', function (event, data) {
// $rootScope.Settings = data
// })
// use this to imitate settings lag
// let lag = false;
$rootScope.$on('SettingsChanged', function (event, data) {
// if (lag)
// Settings.loaded = true;
// lag = true;
Settings.loaded = true; // flag Settings as loaded
Settings.options = data.options;
Settings.values = data.values;
})
bngApi.engineLua('settings.notifyUI()')
bngApi.engineLua('core_gamestate.requestMainMenuState()')
bngApi.engineLua('core_gamestate.requestGameState()')
// bngApi.engineLua('print("requesting gamestate here and now")')
// settings storage end
// navigate to start pages
if (beamng.shipping && beamng.ingame && beamng.buildtype === 'RELEASE') {
$state.go('menu.start')
} else {
$state.go('menu.mainmenu')
}
}])
//------------Trying filter for date translation --------- put on separate file!!!
.filter('formattedDate', function(dateFilter, $translate) {
var format = null, translated = false
function returnFilter(inputDate) {
if(format){
return dateFilter(inputDate, format)
}else{
return '-'
}
}
function formattedDateFilter(inputDate){
if( format === null ) {
if( !translated ){
translated = true
$translate('general.time_format').then(function (result) {
format = result
},function (translationId) {
format = translationId
})
}
}
else return returnFilter(inputDate)
}
formattedDateFilter.$stateful = true
return formattedDateFilter
})
angular.module('beamng.stuff')
.service('translateService', ['$translate', function($translate){
contextTranslate = function(val, translateContext) {
if(typeof val == "string") {
return $translate.instant(val)
} else {
if (val && val.txt && val.context) {
let context = val.context
if(translateContext) {
let newContext = {}
for (let key in context) {
if (context.hasOwnProperty(key)) {
newContext[key] = contextTranslate(context[key], true);
}
}
context = newContext
}
return $translate.instant(val.txt, context)
}
}
return val
}
multiContextTranslate = function(val) {
if(val.txt) {
return contextTranslate(val)
}
let description = ""
for (var i = 0; i < val.length; i++) {
description = description + contextTranslate(val[i])
}
return description
}
return {
contextTranslate: contextTranslate,
multiContextTranslate: multiContextTranslate
}
}])
.filter('contextTranslate', ['translateService', function($translateService) {
function contextTranslateFilter(input){
return $translateService.contextTranslate(input, true)
}
contextTranslateFilter.$stateful = true
return contextTranslateFilter
}])
.service('gamepadNav', ['$rootScope', function ($rootScope) {
'use strict'
// TODO: hook this up to lua settings
// TODO: think about using a list of actions, so when one module unregisters it's action the old action gets used.
// this would have the benefit for example of dropdowns beeing opened, and while open their actions would be used
// todo: actually test the list approach
let useCrossfire = true
let useGamepadNavigation = false
let noop = () => {}
let actions = {
up: [{module: 'root', func: noop}],
down: [{module: 'root', func: noop}],
right: [{module: 'root', func: noop}],
left: [{module: 'root', func: noop}],
confirm: [{module: 'root', func: noop}],
back: [{module: 'root', func: noop}],
}
let prefix = {
up: 'menu_item_up',
down: 'menu_item_down',
right: 'menu_item_right',
left: 'menu_item_left',
confirm: 'menu_item_select',
back: 'menu_item_back',
'radial-x': 'menu_item_radial_x',
'radial-y': 'menu_item_radial_y',
'tab-right': 'menu_tab_right',
'tab-left': 'menu_tab_left',
}
function assignNavFunc (module, data) {
for (var name in data) {
if (actions[name] !== undefined) {
if (nonAssignable.indexOf(name) === -1) {
actions[name].push({module: module, func: data[name]})
// console.debug('Registered new function to "' + name+ '"')
} else {
// console.error('"' + name + '" is an unchangable action')
}
} else {
// console.error('"' + name + '" is not a valid action')
}
}
}
function unregisterActions (module, data) {
for (var name in data) {
if (actions[name] !== undefined) {
var helper = actions[name].map((elem) => elem.module)
if (helper.indexOf(module) !== -1) {
if (nonAssignable.indexOf(name) === -1) {
actions[name].splice(helper.indexOf(module), 1)
// console.debug('Succesfully unregistered "' + name+ '"')
} else {
// console.error('"' + name + '" is an unchangable action')
}
} else {
// console.error('Could not unregister "' + name + '" because there was no registered action from this modul')
}
} else {
// console.warn('Could not unregister "' + name + '" because it is not a valid action')
}
}
}
$rootScope.$on('MenuItemNavigation', function (event, action, val) {
//console.log('MenuItemNavigation - Got action: ' + action)
//console.log('Enabled Librarys', useCrossfire, useGamepadNavigation)
if(!beamng.ingame) return
if (action == 'toggleMenues') {
$rootScope.$broadcast('MenuToggle', val)
return
}
if(action == 'back') {
$rootScope.$broadcast('MenuToggle')
return
}
if (["left", "right", "up", "down"].indexOf(action) != -1) {
bngApi.engineLua('extensions.hook("onMenuItemNavigation")')
}
if (useCrossfire) {
if (action == 'confirm') {
const active = document.activeElement;
if (isNavigatable(active)) {
if (typeof active.click === "function") {
active.click()
} else {
let click = new CustomEvent("click")
active.dispatchEvent(click)
}
}
} else if(action == 'back') {
$rootScope.$broadcast('MenuToggle')
} else if (["left", "right", "up", "down"].indexOf(action) != -1) {
bngApi.engineLua('extensions.hook("onMenuItemNavigation")')
const targets = collectRects(action);
navigate(targets, action);
//console.log(`navigation ${action} handled by Crossfire`)
} else if (action == 'tab-left') {
$rootScope.$broadcast('$tabLeft')
} else if (action == 'tab-right') {
$rootScope.$broadcast('$tabRight')
}
}
if (useGamepadNavigation && actions[action]) {
//console.log(actions[action])
// console.log(actions[action][0])
$rootScope.$evalAsync(actions[action][0].func)
}
})
return {
crossfireEnabled: () => useCrossfire,
gamepadNavEnabled: () => useGamepadNavigation,
spatialNavEnabled: () => useCrossfire,
// TODO: make this intuitive (omiting the value shouldn't do something unexpected)
enableCrossfire: (val) => useCrossfire = val,
enableGamepadNav: (val) => useGamepadNavigation = val,
enableSpatialNav: (val) => { log.error("SpatialNavigation is deprecated. Please use Crossfire."); useCrossfire = val },
registerActions: assignNavFunc,
unregisterActions: unregisterActions,
provideScope: (scope) => scope = scope,
prefix: (val) => prefix[val] || val,
}
}])
/**
* @ngdoc controller
* @name beamng.stuff.controller:AppCtrl
* @description This is the top-level controller used throughout the game
**/
.controller('AppCtrl', ['$document', '$log', '$rootScope', '$scope', '$sce', '$compile', '$state', '$stateParams', '$translate', '$window', 'ControlsUtils', 'Utils', 'Settings', 'toastr', '$timeout', 'gamepadNav', '$injector', '$location', 'translateService', 'UiAppsService', 'MessageToasterService', 'InputCapturer',
function($document, $log, $rootScope, $scope, $sce, $compile, $state, $stateParams, $translate, $window, ControlsUtils, Utils, Settings, toastr, $timeout, gamepadNav, $injector, $location, translateService, UiAppsService,messageToasterService, InputCapturer) {
var vm = this
vm.uiSheetActive = false
// hack to fix backspace navigating between different menus.
// https://stackoverflow.com/questions/29006000/prevent-backspace-from-navigating-back-in-angularjs
$document.on('keydown', function(e){
if(e.which === 8 && ( e.target.nodeName !== "INPUT" && e.target.nodeName !== "TEXTAREA" && e.target.nodeName !== "SELECT" ) ){ // you can add others here inside brackets.
e.preventDefault()
}
})
// // Attempted fix to prevent keyboard events getting through to game when a textbox is being edited
// // Unfortunately causes other issues, but this may be on the right lines. Commented for now as it causes
// // more issues, and probably doesn't address some instances where the problem occurs. Related ticket is GE-4138
// $document.on('mouseup', function(e){
// $timeout(() => {
// vm.uiSheetActive = ["INPUT", "TEXTAREA"].includes(document.activeElement.tagName) ? true : false;
// }, 10);
// })
// Handle "Messages" of the category 'career' with MessageToasterService
messageToasterService.handledCategories = ['career']
messageToasterService.active = true
setTimeout(() => {
gamepadNav.provideScope($scope)
})
bngApi.engineLua('extensions.hook("onUIInitialised")')
$scope.$on('requestUIInitialised', () => {
bngApi.engineLua('core_gamestate.onUIInitialised()')
})
vm.shipping = beamng.shipping
vm.uitest = false
vm.uitestshow = false
// on CEF devtools toggle
$scope.$on('onCEFDevToolsVisibility', (event, enabled) => {
$scope.$applyAsync(function () {
vm.uitest = enabled
bngVue.debug(enabled)
})
})
// figure out if CEF devtools are already open
bngApi.engineLua("getCefDevConsoleOpen()", (enabled)=> {
$scope.$applyAsync(function () {
vm.uitest = enabled
bngVue.debug(enabled)
})
})
// *** DEBUG START
vm.currentStateName = '';
vm.stickyPlayState = null;
$scope.$state = $state
vm.states = $state.get().filter(state => !state.abstract) // filter abstract states
//console.log("vm.states = ", vm.states)
vm.emitMenuNav = function(action, val) {
$rootScope.$broadcast('MenuItemNavigation', action, val)
}
vm.switchState = function(stateName) {
if(stateName !== undefined) {
$state.go(stateName)
}
}
vm.prevState = function() {
if(vm.currentStateName === '') vm.currentStateName = vm.states[0].name
let nextStateIdx = -1
for(let i in vm.states) {
if(vm.states[i].name == vm.currentStateName) {
nextStateIdx = parseInt(i) - 1
if(nextStateIdx < 0) nextStateIdx = vm.states.length - 1
break
}
}
if(nextStateIdx != -1) {
console.log("Switching to new state: " + vm.states[nextStateIdx].name)
$state.go(vm.states[nextStateIdx].name)
}
}
vm.nextState = function() {
if(vm.currentStateName === '') vm.currentStateName = vm.states[0].name
let nextStateIdx = -1
for(let i in vm.states) {
if(vm.states[i].name == vm.currentStateName) {
nextStateIdx = parseInt(i) + 1
if(nextStateIdx >= vm.states.length) nextStateIdx = 0
break
}
}
if(nextStateIdx != -1) {
console.log("Switching to new state: " + vm.states[nextStateIdx].name)
$state.go(vm.states[nextStateIdx].name)
}
}
vm.reloadUI = function() {
window.location.reload()
}
// shortcut for debugging
window.openState = function(name) {
console.log("Switching to new state: " + name)
//bngApi.engineLua("ActionMap.enableInputCommands(false)")
$state.go(name)
}
// *** DEBUG END
vm.replayPaused = false
vm.replayActive = false
vm.physicsPaused = false
vm.physicsMaybePaused = false
vm.showPauseIcon = false
vm.showCrosshair = false
// vm.uiLayoutPrevious = false
vm.playmodeState = null
function updatePauseState() {
vm.physicsPaused= !vm.replayActive && vm.physicsMaybePaused
vm.showPauseIcon = vm.physicsPaused || vm.replayPaused
//console.log("updatePauseState", $state.current.name, vm.showPauseIcon)
}
// quite a hack, but the alternative would have been to manage a list and wait for each state to be actiavated
// the problem only occured because changeState was called almost simultaniously and before on state could be transitioned to the other ocnditinal was already executed next.
// TODO change this to use the $state.transition promise
var transitioningTo
// Screens that tasklist UI app will be visible
const captureInput = InputCapturer();
$scope.$on('$stateChangeSuccess', function (event, toState, toParams, fromState, fromParams) {
//console.log(`switching stage from ${fromState.name} to ${toState.name}`)
//console.trace()
vm.currentStateName = toState.name
// make sure Vue sees all state changes that affect location hash
if (toState.url && location.hash!=toState.url) location.hash = toState.url
// update activated action maps for UI bindings
let menuActionMapEnabled = typeof $state.current.menuActionMapEnabled === "boolean" ? $state.current.menuActionMapEnabled : true; // true by default
// bngApi.engineLua(`extensions.core_input_bindings.setMenuActionMapEnabled(${menuActionMapEnabled})`)
captureInput(menuActionMapEnabled);
bngApi.engineLua(`extensions.hook("onUiChangedState", "${toState.name}", "${fromState.name}")`)
// bngApi.engineLua("career_career.isCareerActive()", data => {
// if (data) {
// console.log(toState)
// if (toState.name == 'play') {
// console.log(menuActionMapEnabled)
// bngApi.engineLua(`bullettime.pause(${menuActionMapEnabled})`);
// }
// if (fromState.name == 'play') {
// console.log(menuActionMapEnabled)
// bngApi.engineLua(`bullettime.pause(${menuActionMapEnabled})`);
// }
// }
// });
// update ui apps layout
bngApi.engineLua("career_career.isCareerActive()", isCareerActive => {
$scope.$evalAsync(() => {
if (isCareerActive && $state.current.careerUiLayout) {
$scope.$emit('appContainer:loadLayoutByType', $state.current.careerUiLayout)
isUILayoutOverriden = true
} else {
if (vm.currentStateName === 'blank') {
console.log('Current state name is blank. Ignoring layout update because this may be already handled ' +
'ChangeState for Vue screens.')
} else if ($state.current.uiLayout === 'blank') {
console.log('Current layout name is blank. Clearing layout.')
$scope.$emit('appContainer:clear')
// $scope.$emit('appContainer:clear')
} else if ($state.current.uiLayout === undefined) {
// no particular ui layout defined, ensure we are in the default/previous one (whichever that may have been)
// console.log(`No layout defined - using previous (${vm.uiLayoutPrevious})`)
if (!(vm.playmodeState && (vm.playmodeState.state || vm.playmodeState.appLayout)) ||
// ignore menu.appselect from emitting layout to prevent issue menu.appedit
// selected layout will always be current game layout
vm.currentStateName === "menu.appselect")
return
if (typeof vm.playmodeState.appLayout == "string") {
$scope.$emit('appContainer:loadLayoutByType', vm.playmodeState.appLayout)
} else if (typeof vm.playmodeState.appLayout == "object") {
$scope.$emit('appContainer:loadLayoutByObject', vm.playmodeState.appLayout)
}
else {
$scope.$emit('appContainer:loadLayoutByReqData', {type: vm.playmodeState.state});
}
// if (vm.uiLayoutPrevious) {
// $scope.$emit('appContainer:loadLayoutByReqData', vm.uiLayoutPrevious)
// vm.uiLayoutPrevious = null
// }
} else {
// console.log(`Layout defined (${$state.current.uiLayout})`)
// this state requires a particular ui layout, set
// vm.uiLayoutPrevious = UiAppsService.getLayout()
$scope.$emit('appContainer:loadLayoutByType', $state.current.uiLayout)
}
}
// update ui apps visibility
$scope.$emit("ShowApps", !!$state.current.uiAppsShown);
$state.previous = fromState;
$state.previousArgs = fromParams;
if (
fromState.name !== "menu" && fromState.name.indexOf("menu.") !== 0 &&
(toState.name === "menu" || toState.name.indexOf("menu.") === 0)
) {
$state.gamestate = fromState;
$state.gamestateArgs = fromParams;
}
transitioningTo = undefined
updatePauseState()
})
})
})
$scope.$on('GameStateUpdate', function (event, data) {
vm.playmodeState = data;
})
$scope.$on('setNavigationStickyPlayState', function(event, stateName) {
vm.stickyPlayState = stateName
})
$scope.$on('$stateChangeCancel', function ( event, toState, toParams, fromState, fromParams) {
//console.warn('$stateChangeCancel', JSON.stringify({toState: toState, toParams: toParams, fromState: fromState, fromParams: fromParams}, null, ' '))
})
$scope.$on('$stateChangeError', function ( event, toState, toParams, fromState, fromParams, error) {
console.error('$stateChangeError', toState, toParams, fromState, fromParams, error)
})
vm.changeAngularStateFromVue = function(state) {
vm.switchState(state)
}
const vueTasklistScreens = ['menu.refueling']
$scope.$on('$stateNotFound', function (event, unfoundState, fromState, fromParams) {
if (vueTasklistScreens.includes(unfoundState.to)) {
bngApi.engineLua("career_career.isCareerActive()", isCareerActive => {
if (isCareerActive) {
$scope.$emit('appContainer:loadLayoutByType', 'tasklist')
}
})
}
// angular doesn't recognise the state, so try Vue (making sure it doesn't pingpong back to here)
bngVue.gotoGameState(unfoundState.to, {tryAngularJS: false})
unfoundState.to = 'blank'
})
$scope.$on('$stateChangeStart', function (event, toState, toParams, fromState, fromParams) {
if(toState.name == 'loading' && !beamng.ingame) {
// in external UI: do not ever allow transitioning into the loading state
event.preventDefault()
return
}
//hack to prevent undesired sound effect stacking
if(toState.name == 'menu.bigmap') {
dontPlayPauseSound = true
}
newPageSilenceEventCounter = 2
newPageTimestamp = Date.now()
//console.log('stateChangeStart', toState, toParams, fromState, fromParams)
//console.trace()
transitioningTo = toState.name
vm.transitionAnimation = toState.transitionAnimation || fromState.transitionAnimation // prefer the animation of the target state, otherwise use the state we came from
})
$scope.$on('ChangeState', function (event, target, ifCurrent) {
let targetName = typeof target === "string" ? target : target.state;
let current = $state.current.name;
// console.log('received ForceStateChange w/', { targetName, ifCurrent, current, transitioningTo});
// set current name if we're during a transition
if (typeof transitioningTo !== "undefined" && transitioningTo !== current) {
current = transitioningTo;
}
// fix ifCurrent
if (typeof ifCurrent !== "undefined" && !Array.isArray(ifCurrent)) {
ifCurrent = [ifCurrent];
}
// console.log(current, ifCurrent, ifCurrent.includes(current));
// decide if we're going to change state
if (!ifCurrent || ifCurrent.includes(current)) {
// console.log(`switching to state: ${targetName}`);
let params = target.params || {};
let stateTransitioning = $state.go(targetName, params, { reload: true });
// console.log("switched:", stateTransitioning);
}
})
$scope.$on('onCrosshairVisibilityChanged', function (event, visible) {
$scope.$applyAsync(function () {
vm.showCrosshair = visible
})
})
vm.showApps = true
vm.uiVisible = true
vm.mainmenu = true
vm.gameState = null
vm.ingame = beamng.ingame
vm.settings = Settings
vm.uiReady = false
// downloader start
var dlinfo = {}
function cancelHelper (id, ctr) {
if (dlinfo[id] && dlinfo[id][0] == 1) {
// hacky fix to remove downloading toastr when it gets stuck.
// This seems to be an issue with ngAnimate, more info here:
// https://github.com/Foxandxss/angular-toastr/issues/136
dlinfo[id][1].el[0].style.display = "none"
toastr.clear(dlinfo[id][1])
delete dlinfo[id]
} else {
if (ctr < 4) {
// setTimeout(cancelHelper.bind(undefined, id, ctr + 1),200)
}
}
}
$window.downloadStateChanged = function(data) {
if(data.filename == '') return
//console.log('downloadStateChanged', data)
if(data.state == 'working' && !dlinfo[data.id]) {
// the 0% is imporatne here, so the toaster library doesn't think the success msg later one to be a duplicate, so please leave it there
var t = toastr.info(data.filename + ': 0%', 'Downloading mod', {
positionClass: 'toast-top-right',
timeOut: 0,
extendedTimeOut: 0,
// progressBar: true,
closeButton: true,
onTap: function () {
$state.go('menu.mods.downloaded')
}
})
dlinfo[data.id] = [1, t]
console.warn(dlinfo[data.id])
} else if(data.state == 'working' && dlinfo[data.id][0] == 1) {
$scope.$evalAsync(function () {
if(dlinfo[data.id]) {
dlinfo[data.id][1].scope.message = $sce.trustAsHtml(data.filename + ": " + Math.floor(data.dlnow / data.dltotal * 100) + "%")
}
})
} else if(data.state == 'finished') {
cancelHelper(data.id)
var t = toastr.success(data.filename, 'Downloaded mod', {
positionClass: 'toast-top-right',
timeOut: 20000,
closeButton: true,
onTap: function () {
var help = data.filename
if (help.slice(-4) === '.zip') {
help = help.slice(0, -4)
}
$state.go('menu.mods.downloaded', {
// modFilePath: encodeURIComponent(help)
})
}
})
}
}
// downloader end
// let Lua know the UI is up and running
let ngLoaded = false, made2ndSettingsRequest = false;
function checkReadiness(canForce=false) {
if (vm.uiReady) // may happen when SettingsChanged event fired twice at once and this function not yet evaluated
return;
if (Settings.loaded) {
lsnSettingsChange();
if (ngLoaded) { // if this is false - either UI is still loading or broken due to mods
bngApi.engineLua("uiReady()");
vm.uiReady = true;
}
} else if (!made2ndSettingsRequest) {
// just to make sure, we'll send another request for settings but only once - we have a listener here anyway
// to imitate lag, find line above with $rootScope.$on('SettingsChanged'
// console.log("Settings are lagging behind...");
bngApi.engineLua("settings.notifyUI()");
made2ndSettingsRequest = true;
} else if (ngLoaded && canForce) {
console.warn("Settings not being loaded after subsequent requests. This error should never happen. Forcing UI to show.");
bngApi.engineLua("uiReady()");
vm.uiReady = true;
}
}
let lsnSettingsChange = $scope.$on("SettingsChanged", () => {
setTimeout(() => checkReadiness(true), 50);
});
angular.element(document).ready(() => {
ngLoaded = true;
checkReadiness(true);
});
$scope.$on("isUIReady", function (event) { // is this ever firing?
if (vm.uiReady) {
bngApi.engineLua("uiReady()");
} else { // not necessary, just for additional safety
checkReadiness();
}
})
$scope.$on('modmanagerError', function (event, data) {
$scope.$emit('app:waiting', false)
toastr.error(data, 'Error in Modmanager', {
positionClass: 'toast-top-right',
preventDuplicates: true,
progressBar: true,
timeOut: 10000,
extendedTimeOut: 1000,
closeButton: true
})
})
var toasts = {}
$scope.$on('toastrMsg', function (ev, data) {
toasts[data.title] = toastr[data.type]($translate.instant(data.msg, data.context), $translate.instant(data.title, data.context), data.config)
})
$scope.$on('toastrClose', function (ev, name) {
toastr.clear(toasts[name])
// delete toasts[name]
})
vm.sections = { // defines in which state after which icons should be a divider
freeroam: ['menu.vehicleconfig.parts', 'menu.environment', 'menu.photomode', 'menu.options.graphics'],
scenario: ['menu.photomode'],
}
$scope.$on('ShowEntertainingBackground', (ev, mainmenu) => {
//console.log("ShowEntertainingBackground")
$scope.$evalAsync(() => {
vm.mainmenu = mainmenu
})
})
vm.openRepo = function() {
var onlineState = Settings.values.onlineFeatures
if (onlineState === 'enable') {
$state.go('menu.mods.repository')
}
else {
window.location.href = 'http-external://www.beamng.com/resources/'
}
}
// The "GameStateUpdate" is triggered by calling "getGameState()" in the Lua engine.
// $rootScope takes care of this each time there is a state change.
$scope.$on('GameStateUpdate', function (event, data) {
//console.log(`got game state: ${data.state}`, data)
$scope.$evalAsync(() => {
vm.gameState = data.menuItems
})
})
$scope.$on('ShowApps', function (event, data) {
// console.log('got', (data ? 'show' : 'hide'), 'apps')
vm.showApps = data
})
$scope.$on("MenuFocusShow", function (event, enabled) {
//if (!enabled) uncollectRects($scope)
})
// Method used to show mods on repository when 'view ingame' on https://www.beamng.com/resources/ is clicked.
$scope.$on('ShowMod', function (event, data) {
var startTimeout
if (data) {
(function checkStart() {
// check if startScreen is still active every 100ms
if($state.current.name === 'startScreen') {
startTimeout = setTimeout(checkStart, 100)
} else {
// if no startScreen then we can continue to show mod
window.location.href = `local://local/ui/entrypoints/main/index.html#/menu/mods/detail/${data}`
clearTimeout(startTimeout)
}
})()
}
})
$scope.$on('MenuToggle', (event, data) => {
//console.log('toggleMenu', data, $state.current)
//console.trace()
if(!beamng.ingame) return
// *** navigation back logic here
let backState = $state.current.backState;
// this hack allows to catch Esc or (B) gamepad button
// currently used in garage mode and bigmap
if (typeof $state.preventStateChange === "function" && $state.preventStateChange()) {
backState = "BLOCK";
} else if (typeof backState === "function")
backState = backState(vm, $state, $stateParams);
if(backState) {
let targetState = backState
if(targetState === 'BLOCK') {
if(!$state.current.tryCounter) {
$state.current.tryCounter = 0
}
$state.current.tryCounter++
if($state.current.tryCounter < 6) {
return
}
$state.current.tryCounter = null
targetState = 'BACK_TO_MENU'
}
if (targetState === 'BACK_TO_MENU') {
targetState = selectTopMenu()
} else if (targetState == 'play' && vm.stickyPlayState) {
targetState = vm.stickyPlayState
}
$state.go(targetState, getPrevArgs($state, targetState))
return
}
//console.log(`received MenuToggle in gamestate: ${vm.gameState}. currently in state: ${$state.current.name}`)
let showMenu = false
$scope.$evalAsync(function () {
if (typeof(data) == 'boolean') {
showMenu = data
} else {
showMenu = $state.current.name !== (vm.gameState === 'garage' ? 'menu.mainmenu' : 'menu')
}
let targetState
if (showMenu) {
targetState = selectTopMenu()
} else {
// figure out where to go 'back' to. Normally the play state, but in scenarios it might be different
targetState = vm.stickyPlayState || 'play'
}
$state.go(targetState, getPrevArgs($state, targetState))
})
bngApi.engineLua(`extensions.hook("onMenuToggled", ${showMenu})`)
})
function getPrevArgs($state, targetState) {
// exiting menus
if ($state.gamestate && $state.gamestateArgs &&
$state.gamestate.name === targetState) {
return $state.gamestateArgs;
}
// generic going back
if ($state.previous && $state.previousArgs &&
$state.previous.name === targetState) {
return $state.previousArgs;
}
return null;
}
function selectTopMenu() {
return vm.mainmenu || vm.gameState === 'garage' ? 'menu.mainmenu' : 'menu'
}
$scope.$on('MenuHide', function (event, data) {
if(!beamng.ingame) return
// TODO FIXME
//console.log(">>>> MENUHIDE", data)
//console.trace()
// TODO: FIXME
let showMenu = false
if (typeof(data) == 'boolean') {
showMenu = data
}
if (showMenu) {
$state.go(vm.mainmenu ? 'menu.mainmenu' : 'menu')
} else {
$state.go('play')
}
})
$scope.$on('onCefVisibilityChanged', function (event, cefVisible) {
$scope.$evalAsync(function () {
vm.uiVisible = cefVisible
})
})
$scope.$on('hide_ui', function (event, visible) {
let cmd = (visible === undefined) ? `extensions.ui_visibility.toggleCef()` : `extensions.ui_visibility.set(${visible})`
console.error('The hide_ui function is deprecated and will stop working in the future. Please use ' + cmd)
bngApi.engineLua(cmd)
})
vm.quit = function () {
if (vm.mainmenu) {
bngApi.engineScript('quit();') //It should work but doesn't, `Platform::postQuitMessage` is executed but nothing happens, maybe CEF catch that message
bngApi.engineLua("TorqueScript.eval('quit();')")
} else {
bngApi.engineLua("returnToMainMenu()")
}
}
$scope.$on('CloseMenu', () => {
var newTarget = vm.mainmenu ? 'menu.mainmenu' : 'menu'
console.log("target", newTarget)
$state.go(newTarget)
})
$scope.$on('quit', vm.quit)
$scope.$on('SettingsChanged', (ev, data) => {
//console.log('SettingsChanged, updating languages... ', data.values.uiLanguage)
if(data.values.uiLanguage && data.values.uiLanguage !== '' && i18NLanguageFinished && i18nLanguageUsed !== data.values.uiLanguage) {
$rootScope.$eval(function() {
$translate.use(data.values.uiLanguage)
})
}
})
// **************************************************************************
// The "MenuOpenModule" event is used to quickly open a state from keyboard.
// The various arguments are defined in the lua/t3d/input_actions.json file
// and map actions to HookManager calls (and stuff for other modules).
//
// NOTE: Remember that transitioning to a state is not enough - menu must be open too!!
$scope.$on('MenuOpenModule', function (event, data) {
//console.log('received MenuOpenModule w/', data)
switch (data) {
case 'help':
$state.go('menu.options.help')
break
case 'vehicleselect':
$state.go('menu.vehicles')
break
case 'vehicleconfig':
$state.go('menu.vehicleconfig.parts')
break
case 'vehicledebug':
$state.go('menu.vehicleconfig.debug')
break
case 'options':
$state.go('menu.options.display')
break
case 'appedit':
$state.go('menu.appedit')
break
default:
$state.go(data)
break
}
})
$scope.$on('InputBindingsChanged', function (event, data) {
$scope.pauseControlText = ""
$scope.pauseControlIcon = ""
for (var i = 0; i < data.bindings.length; i++) {
for (var j = 0; j < data.bindings[i].contents.bindings.length; j++) {
var binding = data.bindings[i].contents.bindings[j]
if (binding.action != "pause") continue
$scope.pauseControlText = binding.control
$scope.pauseControlIcon = ControlsUtils.deviceIcon(data.bindings[i].contents.devicetype)
break
}
}
})
$scope.$on('physicsStateChanged', function (event, state) {
$scope.$evalAsync(function () {
// Clicking Options in menu will trigger pause even if game is already paused
// Need to check if previous state is already paused
if (vm.physicsMaybePaused === false && state === false && typeof dontPlayPauseSound === "undefined") {
bngApi.engineLua(`Engine.Audio.playOnce('AudioGui', 'event:>UI>Generic>Pause')`)
}
if(typeof dontPlayPauseSound !== "undefined") {
delete dontPlayPauseSound
}
vm.physicsMaybePaused = !state
updatePauseState()
})
})
$scope.$on('replayStateChanged', function (event, core_replay) {
$scope.$evalAsync(function () {
vm.replayActive = core_replay.state === 'playing'
vm.replayPaused = vm.replayActive && core_replay.paused
updatePauseState()
})
})
vm.unpause = function () {
bngApi.engineLua('bullettime.pause(false)')
}
$scope.$on('requestPhysicsState', function (event) {
$scope.$broadcast('physicsStateChanged', !vm.physicsPaused)
})
vm.isWaiting = false
$scope.$on('app:waiting', function (event, value, callback) {
vm.isWaiting = value
Utils.waitForCefAndAngular(() => {
if (callback !== undefined && typeof callback === 'function') {
callback(vm.isWaiting)
}
bngApi.engineLua('extensions.hook("onUiWaitingState", ' + String(value) + ')')
})
})
$scope.$on('onLevelsChanged', function(event, data){
levelsData = data
})
bngApi.engineLua('extensions.core_levels.requestData()')
/*
$rootScope.$watch(function() {
console.log("### DIGEST ###")
/// if you want to find out where the digest is triggered:
//console.trace()
})
*/
}])
.service('BlurGame', [function () {
// todo: find a solution if i should actually overflow at some point
let highestId = 0
let list = {}
function updateLua () {
if(!beamng.ingame) return
//console.log('---------update blur to lua' + JSON.stringify(list))
bngApi.engineLua(`extensions.ui_gameBlur.replaceGroup("uiBlur", ${bngApi.serializeToLua(list)})`)
}
return {
register: function (coord) {
if (coord === undefined) {
throw new Error('Cannot register bng-blur with coordinates: ' + coord)
}
if (list.isEmpty()) {
bngApi.engineLua('extensions.load("ui_gameBlur")')
}
highestId += 1
list[highestId] = coord
updateLua()
return highestId
},
unregister: function (i) {
if (i in list) {
delete list[i]
updateLua()
}
if (list.isEmpty()) {
highestId = 0
bngApi.engineLua('extensions.unload("ui_gameBlur")')
}
},
update: function (i, coord) {
if (!(i in list)) {
console.error("Trying to update bng-blur with an ID that is not registered: " + i + " (of " + Object.keys(list) + ")")
throw new Error('Trying to update bng-blur with an ID that is not registered: ' + i + " (of " + Object.keys(list) + ")")
}
list[i] = coord
updateLua()
},
}
}])
.directive('bngBlur', ['BlurGame', 'RateLimiter', function (BlurGame, RateLimiter) {
return {
restrict: 'A',
link: function (scope, elem, attrs) {
let id
let blurAmount = 1
let blurUpdateWrapper = RateLimiter.debounce(updateBlur, 50)
const resizeObserver = new ResizeObserver(blurUpdateWrapper);
resizeObserver.observe(elem[0]);
scope.$watch(attrs.bngBlur, val => {
switch (typeof val) {
case "undefined":
val = 1;
break;
case "boolean":
val = val ? 1 : 0;
break;
case "number":
if (val < 0 || val > 1) {
console.error(`Attempted to use bng-blur with a number out of range 0..1: ${val}\nSee stack:\n${new Error().stack}`);
val = 1;
}
// all fine
break;
default:
console.error(`Attempted to use bng-blur with a non-number, non-boolean value: ${val}\nSee stack:\n${new Error().stack}`)
val = 0;
break;
}
blurAmount = val;
blurUpdateWrapper();
});
function calcBlur () {
let rect = elem[0].getBoundingClientRect();
if (
// valid size (at least 1px)
rect.width > 0 && rect.height > 0 &&
// on screen (for at least 1px)
rect.bottom > 0 && rect.top < screen.height &&
rect.right > 0 && rect.left < screen.width
) {
return [
rect.left / screen.width, // x
rect.top / screen.height, // y
rect.width / screen.width, // width
rect.height / screen.height, // height
blurAmount
];
}
return null;
}
function updateBlur () {
if (blurAmount > 0 && isVisibleFast(elem[0])) {
const blur = calcBlur();
if (!id && blur)
id = BlurGame.register(blur);
else if (!blur)
return BlurGame.unregister(id);
else
BlurGame.update(id, blur);
} else {
if (id) {
BlurGame.unregister(id);
id = null;
}
}
}
scope.$on('$destroy', () => {
resizeObserver.disconnect();
blurAmount = 0
blurUpdateWrapper()
})
scope.$on('windowResize', () => {
blurUpdateWrapper()
})
}
}
}])
// this directive translates the view frustum a bit to make up for lost space behind side menu and alike
// use it like this: <md-content bng-frustum-mover="left">
.directive('bngFrustumMover', ['RateLimiter', function (RateLimiter) {
return {
restrict: 'A',
link: function (scope, elem, attrs) {
function updateGE() {
let screenWidth = window.screen.width
let sideBarWidth = elem[0].getBoundingClientRect().width
let percentHidden = (sideBarWidth / screenWidth)
if(attrs.bngFrustumMover === 'left') {
percentHidden *= -1
} else if(attrs.bngFrustumMover === 'right') {
//all good
} else {
console.error("only left/rigth supported right now")
}
if(Math.abs(percentHidden) < 0.0001) {``
//console.log("complete overlap")
percentHidden = 0
}
//console.log("adjusting frustom side offset:", screenWidth, sideBarWidth, percentHidden)
bngApi.engineLua(`scenetree.OnlyGui:setFrustumCameraCenterOffset(Point2F(${percentHidden}, 0))`)
}
angular.element(elem).ready(function () {
updateGE()
})
// update on resize
const resizeObserver = new ResizeObserver(entries => {
updateGE()
})
resizeObserver.observe(elem[0])
// rest on destroy
scope.$on('$destroy', () => {
//console.log("resetting frustom side offset")
bngApi.engineLua(`scenetree.OnlyGui:setFrustumCameraCenterOffset(Point2F(0, 0))`)
})
scope.$on('windowResize', () => {
updateGE()
})
}
}
}])
.service("InputCapturer", ["$state", function ($state) {
// captures input on UI
// usage:
// const captureInput = InputCapturer(); // create an instance
// captureInput(true|false); // change the value for that instance
// options can be specified:
// const captureInput = InputCapturer({
// // do something on Esc/B press
// backAction() {
// if (something) return true; // return true to prevent default action (e.g. BACK_TO_MENU)
// else return false; // if returned false or nothing - default action is allowed
// }
// });
let isEnabled = false, counter = 0;
function enabler(enable) {
counter += enable ? 1 : -1;
if (counter < 0)
counter = 0;
// console.log(counter);
if ((isEnabled && enable) || (!isEnabled && !enable) || (isEnabled && !enable && counter > 0))
return;
isEnabled = enable;
bngApi.engineLua(`extensions.core_input_bindings.setMenuActionMapEnabled(${enable})`);
}
const backActions = [];
$state.preventStateChange = () => {
if (backActions.length > 0)
return backActions[0]();
return false;
};
return opts => {
if (typeof opts !== "object")
opts = {};
// return a function that tracks previous state and passes it further only when change happened
let current;
return enable => {
if (typeof current !== "undefined" && current === enable)
return;
current = enable;
enabler(enable);
if (typeof opts.backAction === "function") {
if (enable)
backActions.unshift(opts.backAction);
else
backActions.shift();
}
};
};
}])