Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unit DB Feature parity PR #57

Closed
wants to merge 11 commits into from
32 changes: 16 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
unitdb [![Build Status](https://travis-ci.org/spooky/unitdb.svg?branch=master)](https://travis-ci.org/spooky/unitdb)
Unitdb [![Build Status](https://travis-ci.org/spooky/unitdb.svg?branch=master)](https://travis-ci.org/spooky/unitdb)
======

Forged Alliance Forever unit database remake. Uses Angular.js + some hack scripts to extract the data from unit blueprint files.

You can see the app [here](http://spooky.github.io/unitdb).

images
Images
------
cb260 - http://cb260.deviantart.com/

fonts
Fonts
-----
Zeroes Three - http://www.larabiefonts.com/
Muli - http://www.newtypography.co.uk/


license
License
-------
http://www.wtfpl.net/


contributing
Contributing
------------
All contributions are welcome, though I can't guarantee to pull all of them in. If you do want to contribute,
please create a separate branch and a pull request for that. It'll be a bit easier for me to keep the repo tidy that way.
Expand All @@ -30,14 +30,14 @@ Running the Application on MAC OS X
-----------------------------------
Necessary packages that need to be installed beforehand:

> brew install pkgconfig
> brew install pixman
> brew install libjpeg
> brew install giflib
> brew install cairo
> brew install graphicsmagick
> npm install
> bower install - will need npm V4 - otherwise issue `Cannot find module 'internal/fs'`
> grunt serve

View the program in dist directory.
1) brew install pkgconfig
2) brew install pixman
3) brew install libjpeg
4) brew install giflib
5) brew install cairo
6) brew install graphicsmagick
7) npm install
8) bower install - will need npm V4 - otherwise issue `Cannot find module 'internal/fs'`
9) grunt serve

View the program in dist directory.
2 changes: 1 addition & 1 deletion app/data/index.json

Large diffs are not rendered by default.

136 changes: 118 additions & 18 deletions app/js/UnitDecorator.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,25 +95,25 @@ unitDb.UnitDecorator = function(blueprint) {
},
gdiBaseClassificationLookupAndOrder = {
'Construction - Buildpower': ['T1 Engineer','T2 Engineer','T2 Field Engineer','T2 Engineering Station','T1 Engineering Drone','T3 Engineer','T3 Engineering Station','T3 Support Armored Command Unit','Armored Command Unit','ACU Engineering Drone'],

'Land': ['T1 Bot/Tank','T1 Light Assault Bot','T1 Mobile Light Artillery','T1 Mobile Anti-Air','T1 Land Scout','T2 Heavy Tank','T2 Amphibious/Hover Tank','T2 Bot','T2 Mobile Missile Launcher','T2 Mobile Anti-Air','T2 Mobile Shield Generator','T2 Mobile Stealth Field System','T2 Mobile Bomb','T2 Crab Egg (Flak)','T3 Main Assault Bot/Tank','T3 Assault Bot','T3 Sniper Bot','T3 Mobile Heavy Artillery','T3 Mobile Missile Platform','T3 Mobile Anti-Air','T3 Mobile Shield Generator','T3 Shield Disruptor','T3 Crab Egg (Engineer)','T3 Crab Egg (Brick)','T3 Crab Egg (Artillery)'],

'Air': ['T1 Interceptor','T1 Attack Bomber','T1 Light Gunship','T1 Air Scout','T1 Light Air Transport','T2 Combat Fighter','T2 Fighter/Bomber','T2 Gunship','T2 Torpedo Bomber','T2 Guided Missile','T2 Air Transport','T3 Air-Superiority Fighter','T3 Strategic Bomber','T3 Heavy Gunship','T3 Anti-Air Gunship','T3 Torpedo Bomber','T3 Spy Plane','T3 Heavy Air Transport'],

'Naval': ['T1 Attack Submarine','T1 Frigate','T1 Anti-Air Boat','T2 Submarine','T2 Destroyer','T2 Cruiser','T2 Torpedo Boat','T2 Shield Boat','T2 Counter-Intelligence Boat','T3 Submarine Hunter','T3 Battleship','T3 Strategic Missile Submarine','T3 Aircraft Carrier','T3 Battlecruiser','T3 Missile Ship'],

'Experimental' : ['Direct Fire Experimental', 'Air Experimental', 'Naval Experimental', 'Indirect Fire Experimental', 'Other Experimental'],

'Structures - Weapons': ['T1 Point Defense','T1 Anti-Air Turret','T1 Torpedo Launcher','T2 Point Defense','T2 Anti-Air Flak Artillery','T2 Torpedo Launcher','T2 Artillery Installation','T2 Tactical Missile Launcher','T2 Tactical Missile Defense','T3 Heavy Point Defense','T3 Anti-Air SAM Launcher','T3 Torpedo Ambushing System','T3 Heavy Artillery Installation','T3 Rapid-Fire Artillery Installation','T3 Strategic Missile Launcher','T3 Strategic Missile Defense'],

'Structures - Support': ['T1 Wall Section','T2 Air Staging Facility','T2 Shield Generator','T2 Shield Generator: ED2','T2 Shield Generator: ED3','T2 Shield Generator: ED4','T2 Shield Generator: ED5','T3 Heavy Shield Generator'],

'Structures - Intelligence': ['T1 Radar System','T1 Sonar System','T2 Radar System','T2 Sonar System','T2 Stealth Field Generator','T3 Omni Sensor Array','T3 Sonar Platform','T3 Perimeter Monitoring System','T3 Quantum Optics Facility'],

'Structures - Economy': ['T1 Mass Extractor','T1 Power Generator','T1 Hydrocarbon Power Plant','T1 Energy Storage','T1 Mass Storage','T2 Mass Extractor','T2 Power Generator','T2 Mass Fabricator','T3 Mass Extractor','T3 Power Generator','T3 Mass Fabricator'],

'Structures - Factories': ['T1 Land Factory','T1 Air Factory','T1 Naval Factory','T2 Land Factory HQ','T2 Land Factory','T2 Air Factory HQ','T2 Air Factory','T2 Naval Factory HQ','T2 Naval Factory','T3 Land Factory HQ','T3 Land Factory','T3 Air Factory HQ','T3 Air Factory','T3 Naval Factory HQ','T3 Naval Factory','T3 Quantum Gateway'],

},
getTech = function(bp) {
var x = _.intersection(bp.Categories, _.keys(techLookup));
Expand All @@ -123,20 +123,118 @@ unitDb.UnitDecorator = function(blueprint) {
return (u.name ? u.name + ': ' : '') + (u.tech === 'EXP' ? '' : u.tech + ' ') + u.description;
},
weaponStats = function(weapon) {
var shots = weapon.ManualFire ? 1 : weapon.MuzzleSalvoSize, // number of projectiles
var shots = weapon.ManualFire ? 1 : 0, //weapon.MuzzleSalvoSize, // number of projectiles
rate = weapon.RateOfFire,
delay = weapon.RackSalvoChargeTime||0 + weapon.RackSalvoReloadTime||0,
cycle = 1 / rate + delay, // how long it takes between shots
damage = weapon.Damage;
//delay = weapon.RackSalvoChargeTime||0 + weapon.RackSalvoReloadTime||0,
cycle = 1 / rate, // + delay, how long it takes between shots
ppf = weapon.ProjectilesPerOnFire ? weapon.ProjectilesPerOnFire : 1,
salvoSize = typeof weapon.MuzzleSalvoSize !== 'undefined' ? weapon.MuzzleSalvoSize : 1,
salvoDelay = typeof weapon.MuzzleSalvoDelay !== 'undefined' ? weapon.MuzzleSalvoDelay : 1,
dotPulse = typeof weapon.DoTPulses !== 'undefined' ? weapon.DoTPulses : 1,
rackCount = weapon.RackBones ? weapon.RackBones.length : 1,
damage = weapon.Damage * ppf * dotPulse,
// this thing is needed to properly calculate split projectiles. Unfortunately the numbers hardcoded here are not available in the blueprint,
// but specified in the .lua files for corresponding projectiles.
projectileMultiplierLookup = {
'/projectiles/TIFFragmentationSensorShell01/TIFFragmentationSensorShell01_proj.bp': 5, // Lobo
'/projectiles/SIFThunthoArtilleryShell01/SIFThunthoArtilleryShell01_proj.bp': 6, // Zthuee
'/projectiles/AIFFragmentationSensorShell01/AIFFragmentationSensorShell01_proj.bp': 36, // Zthuee
};

salvoDelay = salvoDelay === 0 ? 0 : salvoDelay - 0.1;

var x = Math.pow(10, 1 || 0);
cycle = Math.round((cycle || 0) * x) / x;
salvoDelay = Math.round((salvoDelay || 0) * x) / x;
salvoSize = Math.round((salvoSize || 0) * x) / x;

if (weapon.MuzzleSalvoDelay === 0) {
shots += weapon.RackBones[0].MuzzleBones.length;
}

if ( ( (typeof weapon.ProjectileId !== 'undefined') || (typeof weapon.ProjectileLifetimeUsesMultiplier !== 'undefined') ) && typeof weapon.ForceSingleFire === 'undefined' ) {

//var muzzleDelay = salvoDelay === 0 ? 0 : salvoDelay - 0.1;

return { shots: shots, cycle: cycle, damage: damage };
if (salvoDelay === 0) {
shots = weapon.RackBones[0].MuzzleBones.length;
if (weapon.RackFireTogether)
shots = shots * rackCount;
} else {
shots = salvoSize;
if (weapon.RackFireTogether)
shots = shots * rackCount;
}

if (projectileMultiplierLookup[weapon.ProjectileId])
shots = projectileMultiplierLookup[weapon.ProjectileId];

if (weapon.RateOfFire !== 1) {
cycle = weapon.WeaponCategory === 'Kamikaze' ? 1 : cycle;
} else if (weapon.RackSalvoReloadTime !== 0) {
var reloadCharge = weapon.RackSalvoChargeTime !== 0 ? Math.floor(weapon.RackSalvoReloadTime/weapon.RackSalvoChargeTime) : 0;
var salvoReload = weapon.RackSalvoChargeTime > weapon.RackSalvoReloadTime ? weapon.RackSalvoChargeTime : reloadCharge + weapon.RackSalvoReloadTime;
cycle = salvoReload + rate + salvoDelay * shots;
} else {
cycle = weapon.RackSalvoChargeTime + weapon.RateOfFire;
}
}

return { shots: shots, cycle: cycle, damage: damage, salvoDelay: salvoDelay, salvoSize: salvoSize, dotPulse: dotPulse };
},
fireCycle = function(weapon) {
var stats = weaponStats(weapon);
return stats.shots + ' shot' + (stats.shots > 1 ? 's' : '') + ' / ' + ( stats.cycle === 1 ? '' : Math.round(stats.cycle * 10)/10 ) + ' sec';
switch (weapon.BeamLifetime) {
case 1:
return stats.shots + ' beam(s) every ' + stats.cycle + 's, ' + (9 * weapon.Damage) * stats.shots + ' damage total';
case 0:
return 'continuous beam: ' + (weapon.Damage * stats.shots);// + ' #' + (weapon.Damage * 2) + ' damage';
}

if (stats.salvoDelay !== 0) {
var projectiles = stats.shots;
var x = Math.pow(10, 1 || 0);
var reload = Math.round(((stats.cycle - ((projectiles - 1) * stats.salvoDelay)) || 0) * x) / x;
return projectiles + ' times 1 projectile every ' + stats.salvoDelay + ' seconds + ' +
reload + ' seconds reload ' +
'= ' + stats.cycle + ' seconds total, ' + weapon.Damage * projectiles + ' damage total';
}
return stats.shots + ' shot' + (stats.shots > 1 ? 's' : '') + ' every ' + ( stats.cycle === 1 ? '' : Math.round(stats.cycle * 10)/10 ) + ' sec for ' + (weapon.Damage * stats.shots) + ' total damage';
},
beamCycle = function(weapon) {
if (weapon.BeamCollisionDelay > 0.1) {
var shots = Math.round(weapon.BeamLifetime/(0.1 + weapon.BeamCollisionDelay));
var shotText = '';
if (shots > 1)
shotText = 'every ' + weapon.BeamCollisionDelay + 0.1 + ' seconds ';
return shots + ' times ' + weapon.Damage +
' damage ' + shotText +
'<b>' + weapon.Damage * shots + '</b> damage total';
} else if (weapon.BeamLifetime === 1) {
return '9 times every 0.1 seconds ' + weapon.Damage + ' damage = ' + (9 * weapon.Damage) + ' damage total, 0.8 seconds total';
} else {
if (weapon.DoTPulses) {
return weapon.DoTPulses + ' times ' + weapon.Damage + ' damage every ' +
(weapon.DoTTime/10) + ' seconds = ' + (weapon.damage * weapon.DoTPulses) + ' total ' +
(weapon.DoTTime/10 * weapon.DoTPulses - 0.1) + ' seconds total';
} else {
return weapon.Damage + ' damage';
}
}
},
getDps = function(weapon) {
return unitDb.dpsCalculator.dps(weapon);
var stats = weaponStats(weapon);

if (weapon.ForceSingleFire)
return null;

return unitDb.dpsCalculator.dps(weapon, stats);
},
isTML = function(weapon) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isTML = function(weapon) { return !!weapon.ForceSingleFire; }

if (weapon.ForceSingleFire)
return true;

return false;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there's a lot of dense code in this file - this should be extracted to smaller methods - right now it's really hard to follow.

},
gdiClassificationLookupFunc = function( bp ) {
var gdiClassById = gdiClassificationLookup[bp.Id];
Expand Down Expand Up @@ -179,15 +277,17 @@ unitDb.UnitDecorator = function(blueprint) {
strategicIcon: blueprint.StrategicIconName,
icon: blueprint.General.Icon || '',
order: blueprint.BuildIconSortPriority || 1000,
fireCycle: fireCycle
fireCycle: fireCycle,
beamCycle: beamCycle
};

self.fullName = fullName(self);

// additional stats for weapons
for(var i in blueprint.Weapon) {
_.extend(blueprint.Weapon[i], {
dps: getDps(blueprint.Weapon[i])
dps: getDps(blueprint.Weapon[i]),
isTML: isTML(blueprint.Weapon[i])
});
}

Expand Down
50 changes: 30 additions & 20 deletions app/js/dps.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,40 +9,50 @@ unitDb.DpsCalculator = {
rateInverse: function(w) {
return Math.round(10 / w.RateOfFire) / 10;
},
dps: function(w) {
dps: function(w,s) {
if (this.canCalculate(w))
return this._dps(w);
return this._dps(w,s);
else if (this.next && this.next.dps) {
return this.next.dps(w);
return this.next.dps(w,s);
}
}
};
unitDb.DefaultDpsCalculator = angular.extend({}, unitDb.DpsCalculator, {
canCalculate: function() {
return true;
},
_dps: function(w) {
var projectileMultiplier = 1,
// this thing is needed to properly calculate split projectiles. Unfortunately the numbers hardcoded here are not available in the blueprint,
// but specified in the .lua files for corresponding projectiles.
projectileMultiplierLookup = {
'/projectiles/TIFFragmentationSensorShell01/TIFFragmentationSensorShell01_proj.bp': 4, // Lobo
'/projectiles/SIFThunthoArtilleryShell01/SIFThunthoArtilleryShell01_proj.bp': 5 // Zthuee
};

if (w.ProjectileId)
projectileMultiplier = projectileMultiplierLookup[w.ProjectileId] || 1;

return (projectileMultiplier * w.Damage * w.MuzzleSalvoSize) / unitDb.DpsCalculator.rateInverse(w);
_dps: function(w,s) {
return (s.shots * w.Damage * s.dotPulse) / s.cycle;
}
});
unitDb.BeamDpsCalculator = angular.extend({}, unitDb.DpsCalculator, {
next: unitDb.DefaultDpsCalculator,
canCalculate: function(w) {
return w.BeamLifetime;
},
_dps: function(w) {
return w.Damage * w.BeamLifetime * (w.BeamCollisionDelay || 1) * 10 / unitDb.DpsCalculator.rateInverse(w);
_dps: function(w,s) {
var dps = 0;

switch (w.BeamLifetime) {
case 1:
dps = 9 * w.Damage * (1/s.cycle) * s.shots;
break;
case 0:
dps = w.Damage/(0.1 + w.BeamCollisionDelay);
break;
case w.BeamLifetime > 1:
dps = w.Damage * (w.BeamLifetime/(0.1 + w.BeamCollisionDelay))/s.cycle;
break;
default:
dps = Math.round(w.BeamLifetime/(0.1 + w.BeamCollisionDelay))*w.Damage/s.cycle;
dps = dps * s.shots;
break;
}

var x = Math.pow(10, 2 || 0);
dps = Math.round((dps || 0) * x) / x;

return dps;
}
});
unitDb.ContinousBeamDpsCalculator = angular.extend({}, unitDb.DpsCalculator, {
Expand All @@ -59,8 +69,8 @@ unitDb.DoTDpsCalculator = angular.extend({}, unitDb.DpsCalculator, {
canCalculate: function(w) {
return w.DoTPulses;
},
_dps: function(w) {
var initial = unitDb.DefaultDpsCalculator._dps(w);
_dps: function(w,s) {
var initial = unitDb.DefaultDpsCalculator._dps(w,s);
return (initial + w.Damage * w.DoTPulses * w.MuzzleSalvoSize) / unitDb.DpsCalculator.rateInverse(w);
}
});
Expand Down
35 changes: 32 additions & 3 deletions app/views/unit.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<span class="strategic icon-{{ item.faction }}_{{ item.strategicIcon}}" ></span>
</span>
<h1>{{ item.fullName }}</h1>
<small>{{ item.id }}</small>
<a href="https://github.com/FAForever/fa/blob/develop/units/{{ item.id }}/{{ item.id }}_unit.bp" target="_blank"><small>{{ item.id }}</small></a>
</header>
<section>
<table>
Expand Down Expand Up @@ -162,6 +162,11 @@ <h1>{{ item.fullName }}</h1>
<td colspan="2"><small>Muzzle velocity</small></td>
<td title="muzzle velocity"><small>{{ w.MuzzleVelocity }}</small></td>
</tr>
<tr ng-if="w.ProjectileLifetime">
<td></td>
<td colspan="2"><small>Lifetime</small></td>
<td title="muzzle lifetime"><small>{{ w.ProjectileLifetime }}</small></td>
</tr>
<tr ng-if="w.NukeInnerRingRadius && w.NukeOuterRingRadius">
<td></td>
<td colspan="2"><small>Ring radius</small></td>
Expand All @@ -173,7 +178,19 @@ <h1>{{ item.fullName }}</h1>
<td title="inner ring damage | outer ring damage"><small>{{ w.NukeInnerRingDamage | shorten }} | {{ w.NukeOuterRingDamage | shorten }}</small></td>
</tr>

<tr ng-if="w.ManualFire || w.MuzzleSalvoSize">
<tr ng-if="w.BeamLifetime">
<td></td>
<td colspan="2"><small>Beam cycle</small></td>
<td><small>{{ item.beamCycle(w) }}</small></td>
</tr>

<tr ng-if="w.RackFireTogether">
<td></td>
<td colspan="2"><small>RackFireTogether</small></td>
<td><small>{{ w.RackFireTogether }}</small></td>
</tr>

<tr ng-if="(w.ManualFire || w.MuzzleSalvoSize) && ! w.isTML">
<td></td>
<td colspan="2" title="{{ 'manual fire' | if:w.ManualFire }}"><small>Fire cycle{{ " ⦿"| if:w.ManualFire }}</small></td>
<td><small>{{ item.fireCycle(w) }}</small></td>
Expand All @@ -184,12 +201,24 @@ <h1>{{ item.fullName }}</h1>
<td colspan="2"><small>Turret pitch</small></td>
<td title="min ~ max @ speed"><small>{{ w.TurretPitch }}~{{ w.TurretPitchRange }}° @&nbsp;{{ w.TurretPitchSpeed }}°/s</small></td>
</tr>
<tr ng-if="w.TurretYawRange" ng-repeat-end>
<tr ng-if="w.TurretYawRange">
<td></td>
<td colspan="2"><small>Turret yaw</small></td>
<td title="min ~ max @ speed"><small>{{ w.TurretYaw }}~{{ w.TurretYawRange }}° @&nbsp;{{ w.TurretYawSpeed }}°/s</small></td>
</tr>

<tr ng-if="w.FiringTolerance">
<td></td>
<td colspan="2"><small>Firing Tolerance</small></td>
<td><small>{{ w.FiringTolerance }}</small></td>
</tr>

<tr ng-if="w.FiringRandomness" ng-repeat-end>
<td></td>
<td colspan="2"><small>Firing Randomness</small></td>
<td><small>{{ w.FiringRandomness }}</small></td>
</tr>

<!-- Upgrades -->
<tr ng-if="item.Enhancements" ng-repeat-start="u in item.Enhancements | where: {RemoveEnhancements:undefined} | sortBy: 'Slot'" class="sec">
<td ng-class="{ invisible: !$first }">Upgrades</td>
Expand Down
Loading