From 1367418fb2b91ab281f3b5a5145526ec57b92a8d Mon Sep 17 00:00:00 2001 From: Colin McLeod Date: Wed, 10 Jun 2015 00:17:55 -0700 Subject: [PATCH] Total range, priority management, other tweaks --- app/js/app.js | 11 +- app/js/controllers/controller-outfit.js | 68 +++- app/js/directives/directive-power-bands.js | 119 +++++++ app/js/shipyard/factory-ship.js | 355 ++++++++++++++------- app/js/shipyard/module-shipyard.js | 75 +++-- app/js/shipyard/service-components.js | 15 +- app/views/page-outfit.html | 119 +++---- 7 files changed, 543 insertions(+), 219 deletions(-) create mode 100644 app/js/directives/directive-power-bands.js diff --git a/app/js/app.js b/app/js/app.js index ced40f8a..d85ecb32 100755 --- a/app/js/app.js +++ b/app/js/app.js @@ -1,5 +1,5 @@ angular.module('app', ['ui.router', 'ct.ui.router.extras.sticky', 'ui.sortable', 'shipyard', 'ngLodash', 'app.templates']) -.run(['$rootScope', '$location', '$window', '$document','$state','commonArray','shipPurpose','shipSize','hardPointClass','internalGroupMap','hardpointsGroupMap', 'Persist', function ($rootScope, $location, $window, $doc, $state, CArr, shipPurpose, sz, hpc, igMap, hgMap, Persist) { +.run(['$rootScope', '$location', '$window', '$document','$state','commonArray','shipPurpose','shipSize','hardPointClass','GroupMap', 'Persist', function ($rootScope, $location, $window, $doc, $state, CArr, shipPurpose, sz, hpc, GroupMap, Persist) { // App is running as a standalone web app on tablet/mobile var isStandAlone = $window.navigator.standalone || ($window.external && $window.external.msIsSiteMode && $window.external.msIsSiteMode()); @@ -31,10 +31,15 @@ angular.module('app', ['ui.router', 'ct.ui.router.extras.sticky', 'ui.sortable', $rootScope.SP = shipPurpose; $rootScope.SZ = sz; $rootScope.HPC = hpc; - $rootScope.igMap = igMap; - $rootScope.hgMap = hgMap; + $rootScope.GMAP = GroupMap; + $rootScope.STATUS = ['','DISABLED', 'OFF', 'ON']; + $rootScope.STATUS_CLASS = ['','disabled', 'warning', 'secondary-disabled']; $rootScope.title = 'Coriolis'; + $rootScope.cName = function (c) { + return c.c? c.c.name? c.c.name : GroupMap[c.c.grp] : null; + } + // Formatters $rootScope.fCrd = d3.format(',.0f'); $rootScope.fPwr = d3.format(',.2f'); diff --git a/app/js/controllers/controller-outfit.js b/app/js/controllers/controller-outfit.js index 1d9e5ee8..cbb9ab04 100755 --- a/app/js/controllers/controller-outfit.js +++ b/app/js/controllers/controller-outfit.js @@ -1,6 +1,7 @@ -angular.module('app').controller('OutfitController', ['$window','$rootScope','$scope', '$state', '$stateParams', 'ShipsDB', 'Ship', 'Components', 'Serializer', 'Persist', function ($window, $rootScope, $scope, $state, $p, Ships, Ship, Components, Serializer, Persist) { +angular.module('app').controller('OutfitController', ['$window','$rootScope','$scope', '$state', '$stateParams', 'ShipsDB', 'Ship', 'Components', 'Serializer', 'Persist', 'lodash', function ($window, $rootScope, $scope, $state, $p, Ships, Ship, Components, Serializer, Persist, _) { var data = Ships[$p.shipId]; // Retrieve the basic ship properties, slots and defaults var ship = new Ship($p.shipId, data.properties, data.slots); // Create a new Ship instance + var win = angular.element($window); // Angularized window object for event triggering // Update the ship instance with the code (if provided) or the 'factory' defaults. if ($p.code) { @@ -22,11 +23,18 @@ angular.module('app').controller('OutfitController', ['$window','$rootScope','$s $scope.ft = ship.common[6]; // Fuel Tank $scope.hps = ship.hardpoints; $scope.internal = ship.internal; + $scope.costList = ship.costList; + $scope.powerList = ship.powerList; + $scope.priorityBands = ship.priorityBands; $scope.availCS = Components.forShip(ship.id); $scope.selectedSlot = null; $scope.savedCode = Persist.getBuild(ship.id, $scope.buildName); $scope.canSave = Persist.isEnabled(); $scope.fuel = 0; + $scope.pwrDesc = false; + $scope.pwrPredicate = null; + $scope.costDesc = true; + $scope.costPredicate = 'c.cost'; $scope.jrSeries = { xMin: 0, @@ -149,39 +157,71 @@ angular.module('app').controller('OutfitController', ['$window','$rootScope','$s * @param {object} item The component being toggled */ $scope.toggleCost = function(item) { - item.incCost = !item.incCost; - ship.updateTotals(); + ship.setCostIncluded(item, !item.incCost); + }; + + /** + * [sortCost description] + * @param {[type]} key [description] + * @return {[type]} [description] + */ + $scope.sortCost = function (key) { + $scope.costDesc = ($scope.costPredicate == key)? !$scope.costDesc : $scope.costDesc; + $scope.costPredicate = key; + }; + + $scope.sortPwr = function (key) { + $scope.pwrDesc = ($scope.pwrPredicate == key)? !$scope.pwrDesc : $scope.pwrDesc; + $scope.pwrPredicate = key; }; /** * Toggle the power on/off for the selected component * @param {object} item The component being toggled */ - $scope.togglePwr = function(item) { - // Update serialize code - // updateState(); - item.enabled = !item.enabled; - ship.updateTotals(); + $scope.togglePwr = function(c) { + ship.setSlotEnabled(c, !c.enabled); + $scope.code = Serializer.fromShip(ship); + updateState(); + }; + + $scope.incPriority = function (c) { + if (ship.changePriority(c, c.priority + 1)) { + $scope.code = Serializer.fromShip(ship); + updateState(); + } + }; + + $scope.decPriority = function (c) { + if (ship.changePriority(c, c.priority - 1)) { + $scope.code = Serializer.fromShip(ship); + updateState(); + } }; $scope.fuelChange = function (fuel) { $scope.fuel = fuel; - angular.element($window).triggerHandler('render'); + win.triggerHandler('render'); + }; + + $scope.statusRetracted = function (slot) { + return ship.getSlotStatus(slot, false); + }; + + $scope.statusDeployed = function (slot) { + return ship.getSlotStatus(slot, true); }; // Utilify functions + function updateState() { $state.go('outfit', {shipId: ship.id, code: $scope.code, bn: $scope.buildName}, {location:'replace', notify:false}); $scope.jrSeries.xMax = ship.cargoCapacity; $scope.jrSeries.yMax = ship.jumpRangeWithMass(ship.unladenMass); $scope.jrSeries.mass = ship.unladenMass; + win.triggerHandler('pwrchange'); } - // Hide any open menu/slot/etc if escape key is pressed - $scope.$on('escape', function () { - $scope.selectedSlot = null; - $scope.$apply(); - }); // Hide any open menu/slot/etc if the background is clicked $scope.$on('close', function () { $scope.selectedSlot = null; diff --git a/app/js/directives/directive-power-bands.js b/app/js/directives/directive-power-bands.js new file mode 100644 index 00000000..a5ac1e21 --- /dev/null +++ b/app/js/directives/directive-power-bands.js @@ -0,0 +1,119 @@ +angular.module('app').directive('powerBands', ['$window', function ($window) { + return { + restrict: 'A', + scope:{ + bands: '=', + available: '=' + }, + link: function(scope, element) { + var color = d3.scale.ordinal().range([ '#7b6888', '#6b486b', '#3182bd', '#a05d56', '#d0743c']), + margin = {top: 20, right: 130, bottom: 20, left: 40}, + barHeight = 20, + innerHeight = (barHeight * 2) + 3, + height = innerHeight + margin.top + margin.bottom + 1, + wattScale = d3.scale.linear(), + pctScale = d3.scale.linear().domain([0, 1]), + wattFmt = d3.format('.2f'); + pctFmt = d3.format('.1%'); + wattAxis = d3.svg.axis().scale(wattScale).outerTickSize(0).orient('top').tickFormat(d3.format('.2r')), + pctAxis = d3.svg.axis().scale(pctScale).outerTickSize(0).orient('bottom').tickFormat(d3.format('%')), + // Create chart + svg = d3.select(element[0]).append('svg'), + vis = svg.append('g').attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'), + deployed = vis.append('g'), + retracted = vis.append('g'); + + // Create Y Axis SVG Elements + vis.append('g').attr('class', 'watt axis'); + vis.append('g').attr('class', 'pct axis'); + vis.append("text").attr('x', -35).attr('y', 15).attr('class','primary').text('RET'); + vis.append("text").attr('x', -35).attr('y', barHeight + 17).attr('class','primary').text('DEP'); + + var retLbl = vis.append("text").attr('y', 15) + var depLbl = vis.append("text").attr('y', barHeight + 17).attr('class','primary'); + + // Watch for changes to data and events + scope.$watchCollection('available', render); + angular.element($window).bind('orientationchange resize pwrchange', render); + + function render() { + var bands = scope.bands, + available = scope.available, + width = element[0].offsetWidth, + w = width - margin.left - margin.right, + maxBand = bands[bands.length - 1]; + maxPwr = Math.max(available, maxBand.deployedSum); + + // Update chart size + svg.attr('width', width).attr('height', height); + + // Remove existing elements + retracted.selectAll('rect').remove(); + retracted.selectAll('text').remove(); + deployed.selectAll('rect').remove(); + deployed.selectAll('text').remove(); + + // Update X & Y Axis + wattScale.range([0, w]).domain([0, maxPwr]); + pctScale.range([0, w]).domain([0, maxPwr / available]); + + vis.selectAll('.watt.axis').call(wattAxis); + vis.selectAll('.pct.axis').attr('transform', 'translate(0,' + innerHeight + ')').call(pctAxis); + + retLbl + .attr('x', wattScale(maxBand.retractedSum) + 5 ) + .attr('class',maxBand.retractedSum > available? 'warning': 'primary') + .text(wattFmt(maxBand.retractedSum) + ' (' + pctFmt(maxBand.retractedSum / available) + ')'); + depLbl + .attr('x', wattScale(maxBand.deployedSum) + 5 ) + .attr('class',maxBand.deployedSum > available? 'warning': 'primary') + .text(wattFmt(maxBand.deployedSum) + ' (' + pctFmt(maxBand.deployedSum / available) + ')'); + + retracted.selectAll("rect").data(bands).enter().append("rect") + .attr("height", barHeight) + .attr("width", function(d) { return d.retracted? (wattScale(d.retracted) - 1) : 0; }) + .attr("x", function(d) { return wattScale(d.retractedSum) - wattScale(d.retracted); }) + .attr('y', 1) + .attr('class',function(d){ return (d.retractedSum > available)? 'warning' :'primary'; }); + + retracted.selectAll("text").data(bands).enter().append("text") + .attr('x', function(d) { return wattScale(d.retractedSum) - (wattScale(d.retracted) / 2); }) + .attr('y', 15) + .style('text-anchor', 'middle') + .attr('class','primary-bg') + .text(function(d,i) { return bandText(d.retracted, i, available); }); + + deployed.selectAll("rect").data(bands).enter().append("rect") + .attr("height", barHeight) + .attr("width", function(d) { return (d.deployed || d.retracted)? (wattScale(d.deployed + d.retracted) - 1) : 0; }) + .attr("x", function(d) { return wattScale(d.deployedSum) - wattScale(d.retracted) - wattScale(d.deployed); }) + .attr('y', barHeight + 2) + .attr('class',function(d){ return (d.deployedSum > available)? 'warning' :'primary'; }); + + deployed.selectAll("text").data(bands).enter().append("text") + .attr('x', function(d) { return wattScale(d.deployedSum) - ((wattScale(d.retracted) + wattScale(d.deployed)) / 2); }) + .attr('y', barHeight + 17) + .style('text-anchor', 'middle') + .attr('class','primary-bg') + .text(function(d,i) { return bandText(d.deployed + d.retracted, i, available); }); + + } + + function bandText(val, index, available) { + if (val > 0) { + if( wattScale(val) > 100) { + return (index + 1) + ' (' + wattFmt(val) + ' MW)'; + } + if( wattScale(val) > 10) { + return index + 1; + } + } + return ''; + } + + scope.$on('$destroy', function() { + angular.element($window).unbind('orientationchange resize pwrchange', render); + }); + } + }; +}]); \ No newline at end of file diff --git a/app/js/shipyard/factory-ship.js b/app/js/shipyard/factory-ship.js index fe46f2a9..130f77e5 100755 --- a/app/js/shipyard/factory-ship.js +++ b/app/js/shipyard/factory-ship.js @@ -9,8 +9,7 @@ angular.module('shipyard').factory('Ship', ['Components', 'calcShieldStrength', */ function Ship(id, properties, slots) { this.id = id; - this.incCost = true; - this.cargoScoop = { enabled: true, c: Components.cargoScoop() }; + this.cargoScoop = { enabled: true, c: Components.cargoScoop(), type: 'SYS' }; this.bulkheads = { incCost: true, maxClass: 8 }; for (var p in properties) { this[p] = properties[p]; } // Copy all base properties from shipData @@ -22,6 +21,37 @@ angular.module('shipyard').factory('Ship', ['Components', 'calcShieldStrength', group.push({id: null, c: null, enabled: true, incCost: true, maxClass: slotGroup[i]}); } } + this.c = { incCost: true, c: { name: this.name, cost: this.cost } }; // Make a 'Ship' component similar to other components + + this.costList = _.union(this.internal, this.common, this.hardpoints); + this.costList.push(this.bulkheads); // Add The bulkheads + this.costList.unshift(this.c); // Add the ship itself to the list + + this.powerList = _.union(this.internal, this.hardpoints); + this.powerList.unshift(this.cargoScoop); + this.powerList.unshift(this.common[1]); // Add Thrusters + this.powerList.unshift(this.common[5]); // Add Sensors + this.powerList.unshift(this.common[4]); // Add Power Distributor + this.powerList.unshift(this.common[3]); // Add Life Support + this.powerList.unshift(this.common[2]); // Add FSD + this.powerList.unshift(this.common[0]); // Add Power Plant + this.priorityBands = [ + {deployed: 0, retracted: 0}, + {deployed: 0, retracted: 0}, + {deployed: 0, retracted: 0}, + {deployed: 0, retracted: 0}, + {deployed: 0, retracted: 0} + ]; + + // Reset cumulative and aggragate stats + this.fuelCapacity = 0; + this.cargoCapacity = 0; + this.ladenMass = 0; + this.armourAdded = 0; + this.shieldMultiplier = 1; + this.totalCost = this.cost; + this.unladenMass = this.mass; + this.armourTotal = this.armour; } /** @@ -32,142 +62,68 @@ angular.module('shipyard').factory('Ship', ['Components', 'calcShieldStrength', var internal = this.internal; var common = this.common; var hps = this.hardpoints; + var bands = this.priorityBands; var i,l; - this.bulkheads.id = comps.bulkheads || 0; - this.bulkheads.c = Components.bulkheads(this.id, this.bulkheads.id); + this.useBulkhead(comps.bulkheads || 0, true); + this.cargoScoop.priority = 0; // TODO set from comps + bands[this.cargoScoop.priority].retracted += this.cargoScoop.c.power; for(i = 0, l = comps.common.length; i < l; i++) { - common[i].id = comps.common[i]; - common[i].c = Components.common(i, comps.common[i]); + common[i].enabled = true; // TODO set enabled from comps + common[i].priority = 0; // TODO set from comps + common[i].type = 'SYS'; + this.use(common[i], comps.common[i], Components.common(i, comps.common[i]), true); } + common[1].type = 'ENG'; // Thrusters + common[2].type = 'ENG'; // FSD + for(i = 0, l = comps.hardpoints.length; i < l; i++) { + hps[i].enabled = true; // TODO set enabled from comps + hps[i].priority = 0; // TODO set from comps + hps[i].type = hps[i].maxClass? 'WEP' : 'SYS'; if (comps.hardpoints[i] !== 0) { - hps[i].id = comps.hardpoints[i]; - hps[i].c = Components.hardpoints(comps.hardpoints[i]); + this.use(hps[i], comps.hardpoints[i], Components.hardpoints(comps.hardpoints[i]), true); } else { hps[i].c = hps[i].id = null; } } for(i = 0, l = comps.internal.length; i < l; i++) { + internal[i].enabled = true; // TODO set enabled from comps + internal[i].priority = 0; // TODO set from comps + internal[i].type = 'SYS'; if (comps.internal[i] !== 0) { - internal[i].id = comps.internal[i]; - internal[i].c = Components.internal(comps.internal[i]); + this.use(internal[i], comps.internal[i], Components.internal(comps.internal[i]), true); } else { - internal[i].id = internal[i].c = null; + internal[i].id = internal[i].c = null; } } - this.updateTotals(); + + // Update aggragated stats + this.updatePower(); + this.updateJumpStats(); + this.updateShieldStrength(); }; - /** - * Updates the ship totals based on the components for every slot. - */ - Ship.prototype.updateTotals = function() { - var c = _.reduce(this.common, optsSum, {cost: 0, power: 0, mass: 0}); - var i = _.reduce(this.internal, optsSum, {cost: 0, power: 0, mass: 0, fuel: 0, cargo: 0, armouradd: 0}); - var h = _.reduce(this.hardpoints, hpSum, {cost: 0, active: 0, passive: 0, mass: 0, shieldmul: 1}); - var fsd = this.common[2].c; // Frame Shift Drive; - var sgSI = this.findInternalByGroup('sg'); // Find Shield Generator slot Index if any - - this.totalCost = c.cost + i.cost + h.cost + (this.incCost? this.cost : 0) + (this.bulkheads.incCost? this.bulkheads.c.cost : 0); - this.unladenMass = c.mass + i.mass + h.mass + this.mass + this.bulkheads.c.mass; - this.powerAvailable = this.common[0].c.pGen; // Power Plant - this.fuelCapacity = this.common[6].c.capacity + i.fuel; // Fuel Tank + Internal Fuel Tanks - this.maxMass = this.common[1].c.maxmass; // Thrusters Max Mass - this.cargoCapacity = i.cargo; - this.ladenMass = this.unladenMass + this.cargoCapacity + this.fuelCapacity; - this.powerRetracted = c.power + i.power + h.passive + (this.cargoScoop.enabled? this.cargoScoop.c.power : 0); - this.powerDeployed = this.powerRetracted + h.active; - this.armourAdded = i.armouradd; - this.shieldMultiplier = h.shieldmul; - this.shieldStrength = sgSI != -1? calcShieldStrength(this.mass, this.shields, this.internal[sgSI].c, this.shieldMultiplier) : 0; - this.armourTotal = this.armourAdded + this.armour; - - // Jump Range and total range calculations - var fuelRemaining = this.fuelCapacity % fsd.maxfuel; // Fuel left after making N max jumps - var jumps = this.fuelCapacity / fsd.maxfuel; - this.unladenRange = calcJumpRange(this.unladenMass + fsd.maxfuel, fsd, this.fuelCapacity); // Include fuel weight for jump - this.fullTankRange = calcJumpRange(this.unladenMass + this.fuelCapacity, fsd, this.fuelCapacity); // Full Tanke - this.ladenRange = calcJumpRange(this.ladenMass, fsd, this.fuelCapacity); - this.maxJumpCount = Math.ceil(jumps); // Number of full fuel jumps + final jump to empty tank - - // Going backwards, start with the last jump using the remaining fuel - this.unladenTotalRange = fuelRemaining > 0? calcJumpRange(this.unladenMass + fuelRemaining, fsd, fuelRemaining): 0; - this.ladenTotalRange = fuelRemaining > 0? calcJumpRange(this.unladenMass + this.cargoCapacity + fuelRemaining, fsd, fuelRemaining): 0; - - // For each max fuel jump, calculate the max jump range based on fuel left in the tank - for (var j = 0, l = Math.floor(jumps); j < l; j++) { - fuelRemaining += fsd.maxfuel; - this.unladenTotalRange += calcJumpRange(this.unladenMass + fuelRemaining, fsd); - this.ladenTotalRange += calcJumpRange(this.unladenMass + this.cargoCapacity + fuelRemaining, fsd); - } - - // TODO: armor bonus / damage reduction for bulkheads - // TODO: Damage / DPS total (for all weapons) - }; - - /** - * Utilify function for summing the components properties - * - * @private - * @param {object} sum Sum of cost, power, mass, capacity - * @param {object} slot Slot object - * @return {object} The mutated sum object - */ - function optsSum(sum, slot) { - var c = slot.c; - if (c) { // The slot has a component installed - sum.cost += (slot.incCost && c.cost)? c.cost : 0; - sum.power += (slot.enabled && c.power)? c.power : 0; - sum.mass += c.mass || 0; - if (c.grp == 'ft') { // Internal Fuel Tank - sum.fuel += c.capacity; - } - else if (c.grp == 'cr') { // Internal Cargo Rack - sum.cargo += c.capacity; - } - sum.armouradd += c.armouradd || 0; - } - return sum; - } - - /** - * Utilify function for summing the hardpoint properties - * - * @private - * @param {object} sum Sum of cost, power, etc - * @param {object} slot Slot object - * @return {object} The mutated sum object - */ - function hpSum(sum, slot) { - var c = slot.c; - if (c) { // The slot has a component installed - sum.cost += (slot.incCost && c.cost)? c.cost : 0; - sum[c.passive? 'passive': 'active'] += slot.enabled? c.power : 0; - sum.mass += c.mass || 0; - sum.shieldmul += c.shieldmul || 0; - } - return sum; - } - - Ship.prototype.useBulkhead = function(index) { + Ship.prototype.useBulkhead = function(index, preventUpdate) { + var oldBulkhead = this.bulkheads.c; this.bulkheads.id = index; this.bulkheads.c = Components.bulkheads(this.id, index); - this.updateTotals(); // Update mass, range, shield strength, armor + this.updateStats(this.bulkheads, this.bulkheads.c, oldBulkhead, preventUpdate); }; /** * Update a slot with a the component if the id is different from the current id for this slot. * Has logic handling components that you may only have 1 of (Shield Generator or Refinery). * - * @param {object} slot The component slot - * @param {string} id Unique ID for the selected component - * @param {object} component Properties for the selected component + * @param {object} slot The component slot + * @param {string} id Unique ID for the selected component + * @param {object} component Properties for the selected component + * @param {boolean} preventUpdate If true, do not update aggregated stats */ - Ship.prototype.use = function(slot, id, component) { + Ship.prototype.use = function(slot, id, component, preventUpdate) { if (slot.id != id) { // Selecting a different component var slotIndex = this.internal.indexOf(slot); // Slot is an internal slot, is not being emptied, and the selected component group/type must be of unique @@ -177,17 +133,20 @@ angular.module('shipyard').factory('Ship', ['Components', 'calcShieldStrength', // If another slot has an installed component with of the same type if (similarSlotIndex != -1 && similarSlotIndex != slotIndex) { // Empty the slot - this.internal[similarSlotIndex].id = null; - this.internal[similarSlotIndex].c = null; + var similarSlot = this.internal[similarSlotIndex]; + this.updateStats(similarSlot, null, similarSlot.c, true); // Update stats but don't trigger a global update + similarSlot.id = null; + similarSlot.c = null; } } - // Update slot with selected component (or empty) + var oldComponent = slot.c; slot.id = id; slot.c = component; - this.updateTotals(); + this.updateStats(slot, component, oldComponent, preventUpdate); } }; + /** * Calculate jump range using the installed FSD and the * specified mass which can be more or less than ships actual mass @@ -205,11 +164,179 @@ angular.module('shipyard').factory('Ship', ['Components', 'calcShieldStrength', * @param {string} group Component group/type * @return {number} The index of the slot in ship.internal */ - Ship.prototype.findInternalByGroup = function(group) { + Ship.prototype.findInternalByGroup = function (group) { return _.findIndex(this.internal, function (slot) { return slot.c && slot.c.grp == group; }); }; + Ship.prototype.changePriority = function (slot, newPriority) { + if(newPriority >= 0 && newPriority < this.priorityBands.length) { + var oldPriority = slot.priority; + slot.priority = newPriority; + + if (slot.enabled) { + var usage = (slot.c.passive || this.hardpoints.indexOf(slot) == -1)? 'retracted' : 'deployed'; + this.priorityBands[oldPriority][usage] -= slot.c.power; + this.priorityBands[newPriority][usage] += slot.c.power; + this.updatePower(); + return true; + } + } + return false; + } + + Ship.prototype.setCostIncluded = function (item, included) { + if (item.incCost != included && item.c) { + this.totalCost += included? item.c.cost : -item.c.cost; + } + item.incCost = included; + } + + Ship.prototype.setSlotEnabled = function (slot, enabled) { + if (slot.enabled != enabled && slot.c) { // Enabled state is changing + var usage = (slot.c.passive || this.hardpoints.indexOf(slot) == -1)? 'retracted' : 'deployed'; + this.priorityBands[slot.priority][usage] += enabled? slot.c.power : -slot.c.power; + this.updatePower(); + } + slot.enabled = enabled; + } + + Ship.prototype.getSlotStatus = function (slot, deployed) { + if(!slot.c) { // Empty Slot + return 0; // No Status (Not possible) + } + else if (!slot.enabled) { + return 1; // Disabled + } + else if (deployed) { + return this.priorityBands[slot.priority].deployedSum > this.powerAvailable? 2 : 3; // Offline : Online + } + else if (this.hardpoints.indexOf(slot) != -1 && !slot.c.passive) { // Active hardpoints have no retracted status + return 0; // No Status (Not possible) + } + return this.priorityBands[slot.priority].retractedSum > this.powerAvailable? 2 : 3; // Offline : Online + } + + /** + * Updates the ship's cumulative and aggregated stats based on the component change. + */ + Ship.prototype.updateStats = function(slot, n, old, preventUpdate) { + var isHardPoint = this.hardpoints.indexOf(slot) != -1; + var powerChange = slot == this.common[0]; + + if (old) { // Old component now being removed + switch (old.grp) { + case 'ft': + this.fuelCapacity -= old.capacity; + break; + case 'cr': + this.cargoCapacity -= old.capacity; + break; + case 'hr': + this.armourAdded -= old.armouradd; + break; + case 'sb': + this.shieldMultiplier -= old.shieldmul; + break; + } + + if (slot.incCost && old.cost) { + this.totalCost -= old.cost; + } + + if(old.power) { + this.priorityBands[slot.priority][(isHardPoint && !old.passive)? 'deployed' : 'retracted'] -= old.power; + powerChange = true; + } + this.unladenMass -= old.mass || 0; + } + + if (n) { + switch (n.grp) { + case 'ft': + this.fuelCapacity += n.capacity; + break; + case 'cr': + this.cargoCapacity += n.capacity; + break; + case 't': + this.maxMass = n.maxmass; + break; + case 'hr': + this.armourAdded += n.armouradd; + break; + case 'sb': + this.shieldMultiplier += n.shieldmul; + break; + } + + if (slot.incCost && n.cost) { + this.totalCost += n.cost; + } + + if (n.power) { + this.priorityBands[slot.priority][(isHardPoint && !n.passive)? 'deployed' : 'retracted'] += n.power; + powerChange = true; + } + this.unladenMass += n.mass || 0; + } + + this.ladenMass = this.unladenMass + this.cargoCapacity + this.fuelCapacity; + this.armourTotal = this.armourAdded + this.armour; + + if(!preventUpdate) { + if (powerChange) { + this.updatePower(); + } + this.updateJumpStats(); + this.updateShieldStrength(); + } + }; + + Ship.prototype.updatePower = function() { + var bands = this.priorityBands; + var prevRetracted = 0, prevDeployed = 0; + + for(var i = 0, l = bands.length; i < l; i++) { + var band = bands[i]; + prevRetracted = band.retractedSum = prevRetracted + band.retracted; + prevDeployed = band.deployedSum = prevDeployed + band.deployed + band.retracted; + } + + this.powerAvailable = this.common[0].c.pGen; + this.powerRetracted = prevRetracted; + this.powerDeployed = prevDeployed; + }; + + Ship.prototype.updateShieldStrength = function() { + var sgSI = this.findInternalByGroup('sg'); // Find Shield Generator slot Index if any + this.shieldStrength = sgSI != -1? calcShieldStrength(this.mass, this.shields, this.internal[sgSI].c, this.shieldMultiplier) : 0; + }; + + /** + * Jump Range and total range calculations + */ + Ship.prototype.updateJumpStats = function() { + var fsd = this.common[2].c; // Frame Shift Drive; + var fuelRemaining = this.fuelCapacity % fsd.maxfuel; // Fuel left after making N max jumps + var jumps = this.fuelCapacity / fsd.maxfuel; + this.unladenRange = calcJumpRange(this.unladenMass + fsd.maxfuel, fsd, this.fuelCapacity); // Include fuel weight for jump + this.fullTankRange = calcJumpRange(this.unladenMass + this.fuelCapacity, fsd, this.fuelCapacity); // Full Tanke + this.ladenRange = calcJumpRange(this.ladenMass, fsd, this.fuelCapacity); + this.maxJumpCount = Math.ceil(jumps); // Number of full fuel jumps + final jump to empty tank + + // Going backwards, start with the last jump using the remaining fuel + this.unladenTotalRange = fuelRemaining > 0? calcJumpRange(this.unladenMass + fuelRemaining, fsd, fuelRemaining): 0; + this.ladenTotalRange = fuelRemaining > 0? calcJumpRange(this.unladenMass + this.cargoCapacity + fuelRemaining, fsd, fuelRemaining): 0; + + // For each max fuel jump, calculate the max jump range based on fuel left in the tank + for (var j = 0, l = Math.floor(jumps); j < l; j++) { + fuelRemaining += fsd.maxfuel; + this.unladenTotalRange += calcJumpRange(this.unladenMass + fuelRemaining, fsd); + this.ladenTotalRange += calcJumpRange(this.unladenMass + this.cargoCapacity + fuelRemaining, fsd); + } + }; + return Ship; }]); diff --git a/app/js/shipyard/module-shipyard.js b/app/js/shipyard/module-shipyard.js index c7f45530..8702f923 100755 --- a/app/js/shipyard/module-shipyard.js +++ b/app/js/shipyard/module-shipyard.js @@ -19,41 +19,51 @@ angular.module('shipyard', ['ngLodash']) 'Sensors', 'Fuel Tank' ]) - .value('internalGroupMap', { - fs:'Fuel Scoop', + // Map to lookup group labels/names for component grp + .value('GroupMap', { + // Common + pp:'Power Plant', + t:'Thrusters', + fsd:'Frame Shift Drive', + ls:'Life Support', + pd:'Power Distributor', + s:'Sensors', ft:'Fuel Tank', + + // Internal + fs:'Fuel Scoop', sc:'Scanners', - am:'Auto Field-Maintenance Unit', + am:'Auto Field-Maint. Unit', cr:'Cargo Racks', fi:'FSD Interdictor', hb:'Hatch Breaker Limpet Ctrl', hr:'Hull Reinforcement Package', rf:'Refinery', - sb:'Shield Cell Bank', + scb:'Shield Cell Bank', sg:'Shield Generator', dc:'Docking Computer', fx:'Fuel Transfer Limpet Ctrl', pc:'Prospector Limpet Ctrl', - cc:'Collector Limpet Ctrl' - }) - .value('hardpointsGroupMap', { - 'bl': "Beam Laser", - 'ul': "Burst Laser", - 'c': "Cannon", - 'cs': "Cargo Scanner", - 'cm': "Countermeasure", - 'fc': "Fragment Cannon", - 'fs': "Frame Shift Wake Scanner", - 'kw': "Kill Warrant Scanner", - 'nl': "Mine Launcher", - 'ml': "Mining Laser", - 'mr': "Missile Rack", - 'pa': "Plasma Accelerator", - 'mc': "Multi-cannon", - 'pl': "Pulse Laser", - 'rg': "Rail Gun", - 'sb': "Shield Booster", - 'tp': "Torpedo Pylon" + cc:'Collector Limpet Ctrl', + + // Hard Points + bl: "Beam Laser", + ul: "Burst Laser", + c: "Cannon", + cs: "Cargo Scanner", + cm: "Countermeasure", + fc: "Fragment Cannon", + ws: "Frame Shift Wake Scanner", + kw: "Kill Warrant Scanner", + nl: "Mine Launcher", + ml: "Mining Laser", + mr: "Missile Rack", + pa: "Plasma Accelerator", + mc: "Multi-cannon", + pl: "Pulse Laser", + rg: "Rail Gun", + sb: "Shield Booster", + tp: "Torpedo Pylon" }) .value('shipPurpose', { mp: 'Multi Purpose', @@ -93,7 +103,7 @@ angular.module('shipyard', ['ngLodash']) title: 'Speed', props: ['speed', 'boost'], lbls: ['Thrusters', 'Boost'], - unit: 'M/s', + unit: 'm/s', fmt: 'fRound' }, { // 2 @@ -105,13 +115,13 @@ angular.module('shipyard', ['ngLodash']) { // 3 title: 'Shields', props: ['shieldStrength'], - unit: 'Mj', + unit: 'MJ', fmt: 'fRound' }, { // 4 title: 'Jump Range', - props: ['unladenRange', 'ladenRange'], - lbls: ['Unladen', 'Laden'], + props: ['unladenRange', 'fullTankRange', 'ladenRange'], + lbls: ['Max', 'Full Tank', 'Laden'], unit: 'LY', fmt: 'fRound' }, @@ -146,7 +156,14 @@ angular.module('shipyard', ['ngLodash']) props: ['totalCost'], unit: 'CR', fmt: 'fCrd' - } + }, + { // 10 + title: 'Total Range', + props: ['unladenTotalRange', 'ladenTotalRange'], + lbls: ['Unladen', 'Laden'], + unit: 'LY', + fmt: 'fRound' + }, ]) /** * Calculate the maximum single jump range based on mass and a specific FSD diff --git a/app/js/shipyard/service-components.js b/app/js/shipyard/service-components.js index 17d7fd60..ac9c1e95 100755 --- a/app/js/shipyard/service-components.js +++ b/app/js/shipyard/service-components.js @@ -32,13 +32,26 @@ angular.module('shipyard').service('Components', ['lodash', 'ComponentsDB', 'Shi return null; }; + /** + * Looks up the bulkhead component for a specific ship and bulkhead + * @param {string} shipId Unique ship Id/Key + * @param {number} bulkheadsId Id/Index for the specified bulkhead + * @return {object} The bulkhead component object + */ this.bulkheads = function(shipId, bulkheadsId) { return C.bulkheads[shipId][bulkheadsId]; }; + /** + * Creates a new ComponentSet that contains all available components + * that the specified ship is eligible to use. + * + * @param {string} shipId Unique ship Id/Key + * @return {ComponentSet} The set of components the ship can install + */ this.forShip = function (shipId) { var ship = Ships[shipId]; - return new ComponentSet(C, ship.properties.mass, ship.slots.common, ship.slots.internal[0], ship.slots.hardpoints[0]); + return new ComponentSet(C, ship.properties.mass + 5, ship.slots.common, ship.slots.internal[0], ship.slots.hardpoints[0]); }; }]); \ No newline at end of file diff --git a/app/views/page-outfit.html b/app/views/page-outfit.html index 0ce52ba5..203b527c 100755 --- a/app/views/page-outfit.html +++ b/app/views/page-outfit.html @@ -53,7 +53,7 @@ {{fRound(ship.speed)}} m/s {{fRound(ship.boost)}} m/s {{ship.armourTotal}} ({{ship.armour}} + {{ship.armourAdded}}) - {{fRound(ship.shieldStrength)}} Mj ({{fRPct(ship.shieldMultiplier)}}) + {{fRound(ship.shieldStrength)}} MJ ({{fRPct(ship.shieldMultiplier)}}) {{fRound(ship.unladenMass)}} T {{fRound(ship.ladenMass)}} T {{fRound(ship.cargoCapacity)}} T @@ -137,9 +137,9 @@
{{pd.id}} Power Distributor
{{pd.c.mass}} T
-
Wep: {{pd.c.weaponcapacity}} Mj / {{pd.c.weaponrecharge}} MW
-
Sys: {{pd.c.systemcapacity}} Mj / {{pd.c.systemrecharge}} MW
-
Eng: {{pd.c.enginecapacity}} Mj / {{pd.c.enginerecharge}} MW
+
WEP: {{pd.c.weaponcapacity}} MJ / {{pd.c.weaponrecharge}} MW
+
SYS: {{pd.c.systemcapacity}} MJ / {{pd.c.systemrecharge}} MW
+
ENG: {{pd.c.enginecapacity}} MJ / {{pd.c.enginerecharge}} MW
@@ -149,7 +149,7 @@
{{ss.id}} Sensors
{{ss.c.mass}} T
-
Range: {{ss.c.range}} KM
+
Range: {{ss.c.range}} km
@@ -166,7 +166,7 @@

Internal Compartments

-
+
@@ -176,7 +176,7 @@

HardPoints

-
+
@@ -186,7 +186,7 @@

Utility Mounts

-
+
@@ -194,68 +194,71 @@
-

Power Use

-
-
Generated
-
-
{{pp.c.class}}{{pp.c.rating}} Power Plant
{{fPwr(pp.c.pGen)}}
-
-
Standard
-
-
{{c.c.class}}{{c.c.rating}} {{CArr[$index]}}
{{fPwr(c.c.power)}}
-
-
-
1H Cargo Scoop
{{fPwr(ship.cargoScoop.c.power)}}
-
-
Hardpoints
-
-
{{c.c.class}}{{c.c.rating}} {{c.c.name || hgMap[c.c.grp]}}
{{fPwr(c.c.power)}}
-
-
Internal
-
-
{{c.c.class}}{{c.c.rating}} {{c.c.name || igMap[c.c.grp]}}
{{fPwr(c.c.power)}}
-
-
- - - +
RetractedDeployed
+ + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + +
ComponentTypePriPwrRetDep
{{fPwr(ship.powerRetracted)}} MW ({{fPct(ship.powerRetracted/ship.powerAvailable)}}){{fPwr(ship.powerDeployed)}} MW ({{fPct(ship.powerDeployed/ship.powerAvailable)}}){{pp.c.class}}{{pp.c.rating}}Power PlantSYS1{{fPwr(pp.c.pGen)}}100%

{{c.c.class}}{{c.c.rating}} {{c.priority + 1}} {{fPwr(c.c.power)}}{{fRPct(c.c.power/ship.powerAvailable)}}DISABLED{{STATUS[statusRetracted(c)]}}{{STATUS[statusDeployed(c)]}}
+
-

Costs

-
-
-
{{ship.name}}
{{fCrd(ship.cost)}}
-
-
-
{{ship.bulkheads.c.name}}
{{fCrd(ship.bulkheads.c.cost)}}
-
-
-
{{c.c.class}}{{c.c.rating}} {{CArr[$index]}}
{{fCrd(c.c.cost)}}
-
-
-
{{c.c.class}}{{c.c.rating}} {{c.c.name || hgMap[c.c.grp]}}
{{fCrd(c.c.cost)}}
-
-
-
{{c.c.class}}{{c.c.rating}} {{c.c.name || igMap[c.c.grp]}}
{{fCrd(c.c.cost)}}
-
-
- - +
InsuranceTotal
+ + + + + + - - - + + + +
ComponentCredits
{{fCrd(ship.totalCost * insurance.current.pct)}} CR{{fCrd(ship.totalCost)}} CR
{{c.c.class}}{{c.c.rating}}{{fCrd(c.c.cost)}} CR
+ + + + + + + + + +
Total{{fCrd(ship.totalCost)}} CR
Insurance{{fCrd(ship.totalCost * insurance.current.pct)}} CR