Total range, priority management, other tweaks

This commit is contained in:
Colin McLeod
2015-06-10 00:17:55 -07:00
parent 4fb1ad11ca
commit 1367418fb2
7 changed files with 543 additions and 219 deletions

View File

@@ -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');

View File

@@ -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;

View File

@@ -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);
});
}
};
}]);

View File

@@ -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,131 +62,56 @@ 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;
}
}
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);
};
/**
@@ -166,8 +121,9 @@ angular.module('shipyard').factory('Ship', ['Components', 'calcShieldStrength',
* @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
@@ -211,5 +170,173 @@ angular.module('shipyard').factory('Ship', ['Components', 'calcShieldStrength',
});
};
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;
}]);

View File

@@ -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

View File

@@ -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]);
};
}]);

View File

@@ -53,7 +53,7 @@
<td>{{fRound(ship.speed)}} <u>m/s</u></td>
<td>{{fRound(ship.boost)}} <u>m/s</u></td>
<td>{{ship.armourTotal}} <span ng-if="ship.armourAdded">({{ship.armour}} + {{ship.armourAdded}})</span></td>
<td>{{fRound(ship.shieldStrength)}} <u>Mj</u> <span ng-if="ship.shieldMultiplier > 1">({{fRPct(ship.shieldMultiplier)}})</span></td>
<td>{{fRound(ship.shieldStrength)}} <u>MJ</u> <span ng-if="ship.shieldMultiplier > 1">({{fRPct(ship.shieldMultiplier)}})</span></td>
<td>{{fRound(ship.unladenMass)}} <u>T</u></td>
<td>{{fRound(ship.ladenMass)}} <u>T</u></td>
<td>{{fRound(ship.cargoCapacity)}} <u>T</u></td>
@@ -137,9 +137,9 @@
<div class="l">{{pd.id}} Power Distributor</div>
<div class="r">{{pd.c.mass}} <u>T</u></div>
<div class="cb"></div>
<div class="l">Wep: {{pd.c.weaponcapacity}} <u>Mj</u> / {{pd.c.weaponrecharge}} <u>MW</u></div>
<div class="l">Sys: {{pd.c.systemcapacity}} <u>Mj</u> / {{pd.c.systemrecharge}} <u>MW</u></div>
<div class="l">Eng: {{pd.c.enginecapacity}} <u>Mj</u> / {{pd.c.enginerecharge}} <u>MW</u></div>
<div class="l">WEP: {{pd.c.weaponcapacity}} <u>MJ</u> / {{pd.c.weaponrecharge}} <u>MW</u></div>
<div class="l">SYS: {{pd.c.systemcapacity}} <u>MJ</u> / {{pd.c.systemrecharge}} <u>MW</u></div>
<div class="l">ENG: {{pd.c.enginecapacity}} <u>MJ</u> / {{pd.c.enginerecharge}} <u>MW</u></div>
</div>
<div component-select class="select" s="pd" opts="availCS.common[4]" ng-if="selectedSlot==pd" ng-click="select('c',pd,$event)"></div>
</div>
@@ -149,7 +149,7 @@
<div class="l">{{ss.id}} Sensors</div>
<div class="r">{{ss.c.mass}} <u>T</u></div>
<div class="cb"></div>
<div class="l">Range: {{ss.c.range}} <u>KM</u></div>
<div class="l">Range: {{ss.c.range}} <u>km</u></div>
</div>
<div component-select class="select" s="ss" opts="availCS.common[5]" ng-if="selectedSlot==ss" ng-click="select('c',ss,$event)"></div>
</div>
@@ -166,7 +166,7 @@
<div id="internal" class="group">
<h1>Internal Compartments</h1>
<div class="slot" ng-repeat="i in ship.internal" ng-click="selectSlot($event, i)" ng-class="{selected: selectedSlot==i}">
<div slot-internal class="details" slot="i" lbl="igMap[i.c.grp]" fuel="ship.fuelCapacity"></div>
<div slot-internal class="details" slot="i" lbl="GMAP[i.c.grp]" fuel="ship.fuelCapacity"></div>
<div class="select" ng-if="selectedSlot==i" ng-click="select('i',i,$event)">
<div component-select s="i" groups="availCS.getInts(i.maxClass)"></div>
</div>
@@ -176,7 +176,7 @@
<div id="hardpoints" class="group">
<h1>HardPoints</h1>
<div class="slot" ng-repeat="h in ship.hardpoints | filter:{maxClass: '!0'}" ng-click="selectSlot($event, h)" ng-class="{selected: selectedSlot==h}">
<div slot-hardpoint class="details" hp="h" size="HPC[h.maxClass]" lbl="hgMap[h.c.grp]"></div>
<div slot-hardpoint class="details" hp="h" size="HPC[h.maxClass]" lbl="GMAP[h.c.grp]"></div>
<div class="select" ng-class="{hardpoint: h.maxClass > 0}" ng-if="selectedSlot==h" ng-click="select('h',h,$event)">
<div component-select s="h" groups="availCS.getHps(h.maxClass)"></div>
</div>
@@ -186,7 +186,7 @@
<div id="utility" class="group">
<h1>Utility Mounts</h1>
<div class="slot" ng-repeat="h in ship.hardpoints | filter:{maxClass: '0'}" ng-click="selectSlot($event, h)" ng-class="{selected: selectedSlot==h}">
<div slot-hardpoint class="details" hp="h" size="HPC[h.maxClass]" lbl="hgMap[h.c.grp]"></div>
<div slot-hardpoint class="details" hp="h" size="HPC[h.maxClass]" lbl="GMAP[h.c.grp]"></div>
<div class="select" ng-class="{hardpoint: h.maxClass > 0}" ng-if="selectedSlot==h" ng-click="select('h',h,$event)">
<div component-select s="h" groups="availCS.getHps(h.maxClass)"></div>
</div>
@@ -194,68 +194,71 @@
</div>
<div class="group dbl">
<h1>Power Use</h1>
<div class="items">
<div class="primary-disabled">Generated</div>
<div ng-if="pp.c.pGen" class="item common enabled untoggleable">
<div class="lbl">{{pp.c.class}}{{pp.c.rating}} Power Plant</div><div class="val">{{fPwr(pp.c.pGen)}}</div>
</div>
<div class="primary-disabled">Standard</div>
<div ng-repeat="c in ship.common" ng-if="c.c.power" class="item common" ng-class="{enabled:c.enabled, consumer:c.c.power}" ng-click="togglePwr(c)">
<div class="lbl">{{c.c.class}}{{c.c.rating}} {{CArr[$index]}}</div><div class="val">{{fPwr(c.c.power)}}</div>
</div>
<div class="item common consumer" ng-class="{enabled:ship.cargoScoop.enabled}" ng-click="togglePwr(ship.cargoScoop)">
<div class="lbl">1H Cargo Scoop</div><div class="val">{{fPwr(ship.cargoScoop.c.power)}}</div>
</div>
<div class="primary-disabled">Hardpoints</div>
<div ng-repeat="c in ship.hardpoints" ng-if="c.c.power" class="item hardpoints" ng-class="{enabled:c.enabled, consumer:c.c.power}" ng-click="togglePwr(c)">
<div class="lbl">{{c.c.class}}{{c.c.rating}} {{c.c.name || hgMap[c.c.grp]}}</div><div class="val">{{fPwr(c.c.power)}}</div>
</div>
<div class="primary-disabled">Internal</div>
<div ng-repeat="c in ship.internal" ng-if="c.c.power" class="item internal" ng-class="{enabled:c.enabled, consumer:c.c.power}" ng-click="togglePwr(c)">
<div class="lbl">{{c.c.class}}{{c.c.rating}} {{c.c.name || igMap[c.c.grp]}}</div><div class="val">{{fPwr(c.c.power)}}</div>
</div>
</div>
<table>
<thead><tr><td>Retracted</td><td>Deployed</td></tr></thead>
<table style="width:100%">
<thead>
<tr class="main">
<th colspan="2" class="sortable le" ng-click="sortPwr(cName)">Component</th>
<th style="width:3em;" class="sortable" ng-click="sortPwr('type')">Type</th>
<th style="width:4em;" class="sortable" ng-click="sortPwr('priority')">Pri</th>
<th colspan="2" class="sortable" ng-click="sortPwr('c.power')">Pwr</th>
<th style="width:3em;" class="sortable" ng-click="sortPwr(statusRetracted)">Ret</th>
<th style="width:3em;" class="sortable" ng-click="sortPwr(statusDeployed)">Dep</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{fPwr(ship.powerRetracted)}} <u>MW</u> ({{fPct(ship.powerRetracted/ship.powerAvailable)}})</td>
<td>{{fPwr(ship.powerDeployed)}} <u>MW</u> ({{fPct(ship.powerDeployed/ship.powerAvailable)}})</td>
<td ng-click="togglePwr(c)">{{pp.c.class}}{{pp.c.rating}}</td>
<td class="le shorten">Power Plant</td>
<td><u>SYS</u></td>
<td>1</td>
<td class="ri">{{fPwr(pp.c.pGen)}}</td>
<td><u>100%</u></td>
<td></td>
<td></td>
</tr>
<tr><td style="line-height:0;" colspan="8"><hr style="margin: 0 0 3px;background: #ff8c0d;border: 0;height: 1px;" /></td></tr>
<tr ng-repeat="c in powerList | orderBy:pwrPredicate:pwrDesc" ng-if="c.c.power" ng-class="{disabled:!c.enabled}">
<td style="width:1em;" ng-click="togglePwr(c)">{{c.c.class}}{{c.c.rating}}</td>
<td class="le shorten" ng-click="togglePwr(c)" ng-bind="cName(c)"></td>
<td ng-click="togglePwr(c)"><u ng-bind="c.type"></u></td>
<td><span ng-click="decPriority(c)"></span> {{c.priority + 1}} <span ng-click="incPriority(c)"></span></td>
<td class="ri" style="width:3.25em;">{{fPwr(c.c.power)}}</td>
<td class="ri" style="width:2em;"><u>{{fRPct(c.c.power/ship.powerAvailable)}}</u></td>
<td ng-if="!c.enabled" class="disabled" colspan="2">DISABLED</td>
<td ng-if="c.enabled" ng-class="STATUS_CLASS[statusRetracted(c)]">{{STATUS[statusRetracted(c)]}}</td>
<td ng-if="c.enabled" ng-class="STATUS_CLASS[statusDeployed(c)]">{{STATUS[statusDeployed(c)]}}</td>
</tr>
</tbody>
</table>
<div style="margin-top: 1em" power-bands bands="priorityBands" available="ship.powerAvailable"></div>
</div>
<div class="group dbl">
<h1>Costs</h1>
<div class="items">
<div class="item" ng-class="{enabled:ship.incCost}" ng-click="toggleCost(ship)">
<div class="lbl">{{ship.name}}</div><div class="val">{{fCrd(ship.cost)}}</div>
</div>
<div class="item" ng-class="{enabled:ship.bulkheads.incCost}" ng-click="toggleCost(ship.bulkheads)" ng-if="ship.bulkheads.c.cost">
<div class="lbl">{{ship.bulkheads.c.name}}</div><div class="val">{{fCrd(ship.bulkheads.c.cost)}}</div>
</div>
<div ng-repeat="c in ship.common" ng-if="c.c.cost" class="item common" ng-class="{enabled:c.incCost}" ng-click="toggleCost(c)">
<div class="lbl">{{c.c.class}}{{c.c.rating}} {{CArr[$index]}}</div><div class="val">{{fCrd(c.c.cost)}}</div>
</div>
<div ng-repeat="c in ship.hardpoints" ng-if="c.c.cost" class="item hardpoints" ng-class="{enabled:c.incCost}" ng-click="toggleCost(c)">
<div class="lbl">{{c.c.class}}{{c.c.rating}} {{c.c.name || hgMap[c.c.grp]}}</div><div class="val">{{fCrd(c.c.cost)}}</div>
</div>
<div ng-repeat="c in ship.internal" ng-if="c.c.cost" class="item internal" ng-class="{enabled:c.incCost}" ng-click="toggleCost(c)">
<div class="lbl">{{c.c.class}}{{c.c.rating}} {{c.c.name || igMap[c.c.grp]}}</div><div class="val">{{fCrd(c.c.cost)}}</div>
</div>
</div>
<table>
<thead><tr><td>Insurance</td><td>Total</td></tr></thead>
<table style="width:100%">
<thead>
<tr class="main">
<th colspan="2" class="sortable le" ng-click="sortCost(cName)">Component</th>
<th class="sortable le" ng-click="sortCost('c.cost')">Credits</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{fCrd(ship.totalCost * insurance.current.pct)}} <u>CR</u></td>
<td>{{fCrd(ship.totalCost)}} <u>CR</u></td>
<tr class="toggleable" ng-repeat="c in costList | orderBy:costPredicate:costDesc" ng-if="c.c.cost > 0" ng-class="{disabled:!c.incCost}" ng-click="toggleCost(c)">
<td style="width:1em;">{{c.c.class}}{{c.c.rating}}</td>
<td class="le shorten" ng-bind="cName(c)"></td>
<td class="ri">{{fCrd(c.c.cost)}} <u>CR</u></td>
</tr>
</tbody>
</table>
<table class="total">
<tr class="ri">
<td class="lbl">Total</td>
<td>{{fCrd(ship.totalCost)}} <u>CR</u></td>
</tr>
<tr class="ri">
<td class="lbl">Insurance</td>
<td>{{fCrd(ship.totalCost * insurance.current.pct)}} <u>CR</u></td>
</tr>
</table>
</div>
<div class="group dbl">