diff --git a/.gitignore b/.gitignore index 1bbc61e9..0dee7465 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ nginx.pid .idea /bin env +*.swp diff --git a/ChangeLog.md b/ChangeLog.md index cbed8ff4..f357241d 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,3 +1,48 @@ +#2.3.0 + * Make scan time visible on scanners where available + * Update power distributor able-to-boost calculation to take fractional MJ values in to account + * Revert to floating header due to issues on iOS + * Fix issue where new module added to a slot did not reset its enabled status + * Show integrity value for relevant modules + * Reset old modification values when a new roll is applied + * Fix issue with miner role where refinery would not be present in ships with class 5 slots but no class 4 + * Ensure that boost value is set correctly when modifications to power distributor enable/disable boost + * Ensure that hull reinforcement modifications take the inherent resistance in to account when calculating modification percentages + * Add tooltip for blueprints providing details of the features they alter, the components required for the blueprint and the engineer(s) who cam craft them + * Use opponent's saved pips if available + * Ignore rounds per shot for EPS and HPS calculations; it's already factored in to the numbers + * Ensure that clip size modification imports result in whole numbers + * Rework of separate offence/defence/movement sections to a unified interface + * Use cargo hatch information on import if available + * Additional information of power distributor pips, boost, cargo and fuel loads added to build + * Additional information of opponent and engagement range added to build + * Reworking of offence, defence and movement information in to separate tabs as part of the outfitting screen: + * Power and costs section provides the existing 'Power' and 'Costs' sections + * Profiles section provides a number of graphs that show how various components of the build (top speed, sustained DPS against opponent's shields and armour etc) are affected by mass, range, etc. + * Offence section provides details of your build's damage distribution and per-weapon effectiveness. It also gives summary information for how long it will take for your build to wear down your opponent's shields and armour + * Defence section provides details of your build's defences against your selected opponent. It provides details of the effectiveness of your resistances of both shields and armour, and effective strength of each as a result. It also provides key metrics around shield longevity and recovery times, as well as module protection + * Fix power band marker to show safe power limit at 40% rather than 50% + * Restyle blueprint list to improve consistency with similar menus + * Use coriolis-data 2.3.0: + * Add Dolphin + * Add turreted mining lasers + * Add long range / wide angle / fast scan scanner blueprints + * Fix EDDB IDs for class 5 and 7 fighter hangars for correct shopping list + * Fix cost for rocket-propelled FSD disruptor + * Add module names for blueprints + * Fix erroneous value for grade 5 kinetic shield booster + * Add missing integrity values for some modules + * Update module reinforcement package integrity + * Update specs of Beluga as per 2.3 + * Update specs of Asp Scout as per 2.3 + * Update specs of Diamondback Explorer as per 2.3 + * Add ED ID for Rocket Propelled FSD Disruptor + * Fix ED name for target lock breaker special + * Update scan range and angle information for sensors + * Tidy up shield cell bank information to allow for accurate calculations with modifications + * Update mine launcher stats + * Add appropriate engineers to per-module blueprint information + #2.2.19 * Power management panel now displays modules in descending order of power usage by default * Shot speed can no longer be modified directly. Its value is derived from the range modifier for Long Range and Focused modifications diff --git a/__tests__/fixtures/anaconda-test-detailed-export-v4.json b/__tests__/fixtures/anaconda-test-detailed-export-v4.json index 404ab0f1..32d622cf 100644 --- a/__tests__/fixtures/anaconda-test-detailed-export-v4.json +++ b/__tests__/fixtures/anaconda-test-detailed-export-v4.json @@ -320,7 +320,6 @@ "shieldExplRes": 0.5, "shieldKinRes": 0.4, "shieldThermRes": -0.2, - "timeToDrain": 7.04, "crew": 3 } } diff --git a/__tests__/fixtures/expected-builds.json b/__tests__/fixtures/expected-builds.json index 6cfc0f68..2e5498a6 100644 --- a/__tests__/fixtures/expected-builds.json +++ b/__tests__/fixtures/expected-builds.json @@ -36,7 +36,7 @@ "Test": "A4putkFklkdzsuf52c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04---0303326b.Iw18ZVA=.Aw18ZVA=." }, "diamondback_explorer": { - "Explorer": "A0p0tdFfldddsdf5---0202--320p432i2f.AwRj4zTI.AwiMIypI." + "Explorer": "A0p0tdFfldddsdf5---0202--320p432i2f-.AwRj4zTYg===.AwiMIyoo." }, "vulture": { "Bounty Hunter": "A3patcFalddksff31e1e0404-0l4a-5d27662j.AwRj4z2I.MwBhBYy6oJmAjLIA." diff --git a/d3.min.js b/d3.min.js index 10e71019..5b9a0d80 100644 --- a/d3.min.js +++ b/d3.min.js @@ -1,4 +1,4 @@ -!function(t,n){"object"==typeof exports&&"undefined"!=typeof module?n(exports):"function"==typeof define&&define.amd?define(["exports"],n):n(t.d3=t.d3||{})}(this,function(t){"use strict";function n(t){return function(n,e){return so(t(n),e)}}function e(t,n,e){var i=Math.abs(n-t)/Math.max(0,e),r=Math.pow(10,Math.floor(Math.log(i)/Math.LN10)),o=i/r;return o>=po?r*=10:o>=yo?r*=5:o>=vo&&(r*=2),n=0&&(e=t.slice(i+1),t=t.slice(0,i)),t&&!n.hasOwnProperty(t))throw new Error("unknown type: "+t);return{type:t,name:e}})}function p(t,n){for(var e,i=0,r=t.length;i=0&&(n=t.slice(e+1),t=t.slice(0,e)),{type:t,name:n}})}function w(t){return function(){var n=this.__on;if(n){for(var e,i=0,r=-1,o=n.length;in?1:t>=n?0:NaN}function A(t){return function(){this.removeAttribute(t)}}function E(t){return function(){this.removeAttributeNS(t.space,t.local)}}function U(t,n){return function(){this.setAttribute(t,n)}}function L(t,n){return function(){this.setAttributeNS(t.space,t.local,n)}}function P(t,n){return function(){var e=n.apply(this,arguments);null==e?this.removeAttribute(t):this.setAttribute(t,e)}}function D(t,n){return function(){var e=n.apply(this,arguments);null==e?this.removeAttributeNS(t.space,t.local):this.setAttributeNS(t.space,t.local,e)}}function R(t){return function(){this.style.removeProperty(t)}}function F(t,n,e){return function(){this.style.setProperty(t,n,e)}}function Y(t,n,e){return function(){var i=n.apply(this,arguments);null==i?this.style.removeProperty(t):this.style.setProperty(t,i,e)}}function q(t){return function(){delete this[t]}}function H(t,n){return function(){this[t]=n}}function z(t,n){return function(){var e=n.apply(this,arguments);null==e?delete this[t]:this[t]=e}}function O(t){return t.trim().split(/^|\s+/)}function j(t){return t.classList||new X(t)}function X(t){this._node=t,this._names=O(t.getAttribute("class")||"")}function I(t,n){for(var e=j(t),i=-1,r=n.length;++i>8&15|n>>4&240,n>>4&15|240&n,(15&n)<<4|15&n,1)):(n=Au.exec(t))?pt(parseInt(n[1],16)):(n=Eu.exec(t))?new gt(n[1],n[2],n[3],1):(n=Uu.exec(t))?new gt(255*n[1]/100,255*n[2]/100,255*n[3]/100,1):(n=Lu.exec(t))?dt(n[1],n[2],n[3],n[4]):(n=Pu.exec(t))?dt(255*n[1]/100,255*n[2]/100,255*n[3]/100,n[4]):(n=Du.exec(t))?xt(n[1],n[2]/100,n[3]/100,1):(n=Ru.exec(t))?xt(n[1],n[2]/100,n[3]/100,n[4]):Fu.hasOwnProperty(t)?pt(Fu[t]):"transparent"===t?new gt(NaN,NaN,NaN,0):null}function pt(t){return new gt(t>>16&255,t>>8&255,255&t,1)}function dt(t,n,e,i){return i<=0&&(t=n=e=NaN),new gt(t,n,e,i)}function yt(t){return t instanceof ft||(t=_t(t)),t?(t=t.rgb(),new gt(t.r,t.g,t.b,t.opacity)):new gt}function vt(t,n,e,i){return 1===arguments.length?yt(t):new gt(t,n,e,null==i?1:i)}function gt(t,n,e,i){this.r=+t,this.g=+n,this.b=+e,this.opacity=+i}function xt(t,n,e,i){return i<=0?t=n=e=NaN:e<=0||e>=1?t=n=NaN:n<=0&&(t=NaN),new bt(t,n,e,i)}function mt(t){if(t instanceof bt)return new bt(t.h,t.s,t.l,t.opacity);if(t instanceof ft||(t=_t(t)),!t)return new bt;if(t instanceof bt)return t;t=t.rgb();var n=t.r/255,e=t.g/255,i=t.b/255,r=Math.min(n,e,i),o=Math.max(n,e,i),u=NaN,a=o-r,s=(o+r)/2;return a?(u=n===o?(e-i)/a+6*(e0&&s<1?0:u,new bt(u,a,s,t.opacity)}function wt(t,n,e,i){return 1===arguments.length?mt(t):new bt(t,n,e,null==i?1:i)}function bt(t,n,e,i){this.h=+t,this.s=+n,this.l=+e,this.opacity=+i}function Mt(t,n,e){return 255*(t<60?n+(e-n)*t/60:t<180?e:t<240?n+(e-n)*(240-t)/60:n)}function Nt(t){if(t instanceof Tt)return new Tt(t.l,t.a,t.b,t.opacity);if(t instanceof Pt){var n=t.h*Yu;return new Tt(t.l,Math.cos(n)*t.c,Math.sin(n)*t.c,t.opacity)}t instanceof gt||(t=yt(t));var e=Et(t.r),i=Et(t.g),r=Et(t.b),o=Ct((.4124564*e+.3575761*i+.1804375*r)/zu),u=Ct((.2126729*e+.7151522*i+.072175*r)/Ou),a=Ct((.0193339*e+.119192*i+.9503041*r)/ju);return new Tt(116*u-16,500*(o-u),200*(u-a),t.opacity)}function kt(t,n,e,i){return 1===arguments.length?Nt(t):new Tt(t,n,e,null==i?1:i)}function Tt(t,n,e,i){this.l=+t,this.a=+n,this.b=+e,this.opacity=+i}function Ct(t){return t>Bu?Math.pow(t,1/3):t/$u+Xu}function St(t){return t>Iu?t*t*t:$u*(t-Xu)}function At(t){return 255*(t<=.0031308?12.92*t:1.055*Math.pow(t,1/2.4)-.055)}function Et(t){return(t/=255)<=.04045?t/12.92:Math.pow((t+.055)/1.055,2.4)}function Ut(t){if(t instanceof Pt)return new Pt(t.h,t.c,t.l,t.opacity);t instanceof Tt||(t=Nt(t));var n=Math.atan2(t.b,t.a)*qu;return new Pt(n<0?n+360:n,Math.sqrt(t.a*t.a+t.b*t.b),t.l,t.opacity)}function Lt(t,n,e,i){return 1===arguments.length?Ut(t):new Pt(t,n,e,null==i?1:i)}function Pt(t,n,e,i){this.h=+t,this.c=+n,this.l=+e,this.opacity=+i}function Dt(t){if(t instanceof Ft)return new Ft(t.h,t.s,t.l,t.opacity);t instanceof gt||(t=yt(t));var n=t.r/255,e=t.g/255,i=t.b/255,r=(ta*i+Qu*n-Ku*e)/(ta+Qu-Ku),o=i-r,u=(Gu*(e-r)-Wu*o)/Ju,a=Math.sqrt(u*u+o*o)/(Gu*r*(1-r)),s=a?Math.atan2(u,o)*qu-120:NaN;return new Ft(s<0?s+360:s,a,r,t.opacity)}function Rt(t,n,e,i){return 1===arguments.length?Dt(t):new Ft(t,n,e,null==i?1:i)}function Ft(t,n,e,i){this.h=+t,this.s=+n,this.l=+e,this.opacity=+i}function Yt(t,n){return function(e){return t+e*n}}function qt(t,n,e){return t=Math.pow(t,e),n=Math.pow(n,e)-t,e=1/e,function(i){return Math.pow(t+i*n,e)}}function Ht(t,n){var e=n-t;return e?Yt(t,e>180||e<-180?e-360*Math.round(e/360):e):oa(isNaN(t)?n:t)}function zt(t){return 1===(t=+t)?Ot:function(n,e){return e-n?qt(n,e,t):oa(isNaN(n)?e:n)}}function Ot(t,n){var e=n-t;return e?Yt(t,e):oa(isNaN(t)?n:t)}function jt(t){return function(){return t}}function Xt(t){return function(n){return t(n)+""}}function It(t){return"none"===t?va:(na||(na=document.createElement("DIV"),ea=document.documentElement,ia=document.defaultView),na.style.transform=t,t=ia.getComputedStyle(ea.appendChild(na),null).getPropertyValue("transform"),ea.removeChild(na),t=t.slice(7,-1).split(","),ga(+t[0],+t[1],+t[2],+t[3],+t[4],+t[5]))}function $t(t){return null==t?va:(ra||(ra=document.createElementNS("http://www.w3.org/2000/svg","g")),ra.setAttribute("transform",t),(t=ra.transform.baseVal.consolidate())?(t=t.matrix,ga(t.a,t.b,t.c,t.d,t.e,t.f)):va)}function Bt(t,n,e,i){function r(t){return t.length?t.pop()+" ":""}function o(t,i,r,o,u,a){if(t!==r||i!==o){var s=u.push("translate(",null,n,null,e);a.push({i:s-4,x:ha(t,r)},{i:s-2,x:ha(i,o)})}else(r||o)&&u.push("translate("+r+n+o+e)}function u(t,n,e,o){t!==n?(t-n>180?n+=360:n-t>180&&(t+=360),o.push({i:e.push(r(e)+"rotate(",null,i)-2,x:ha(t,n)})):n&&e.push(r(e)+"rotate("+n+i)}function a(t,n,e,o){t!==n?o.push({i:e.push(r(e)+"skewX(",null,i)-2,x:ha(t,n)}):n&&e.push(r(e)+"skewX("+n+i)}function s(t,n,e,i,o,u){if(t!==e||n!==i){var a=o.push(r(o)+"scale(",null,",",null,")");u.push({i:a-4,x:ha(t,e)},{i:a-2,x:ha(n,i)})}else 1===e&&1===i||o.push(r(o)+"scale("+e+","+i+")")}return function(n,e){var i=[],r=[];return n=t(n),e=t(e),o(n.translateX,n.translateY,e.translateX,e.translateY,i,r),u(n.rotate,e.rotate,i,r),a(n.skewX,e.skewX,i,r),s(n.scaleX,n.scaleY,e.scaleX,e.scaleY,i,r),n=e=null,function(t){for(var n,e=-1,o=r.length;++e=0&&n._call.call(null,t),n=n._next;--Na}function Kt(){Aa=(Sa=Ua.now())+Ea,Na=ka=0;try{Qt()}finally{Na=0,nn(),Aa=0}}function tn(){var t=Ua.now(),n=t-Sa;n>Ca&&(Ea-=n,Sa=t)}function nn(){for(var t,n,e=wa,i=1/0;e;)e._call?(i>e._time&&(i=e._time),t=e,e=e._next):(n=e._next,e._next=null,e=t?t._next=n:wa=n);ba=t,en(i)}function en(t){if(!Na){ka&&(ka=clearTimeout(ka));var n=t-Aa;n>24?(t<1/0&&(ka=setTimeout(Kt,n)),Ta&&(Ta=clearInterval(Ta))):(Ta||(Sa=Aa,Ta=setInterval(tn,Ca)),Na=1,La(Kt))}}function rn(t,n){var e=t.__transition;if(!e||!(e=e[n])||e.state>Fa)throw new Error("too late");return e}function on(t,n){var e=t.__transition;if(!e||!(e=e[n])||e.state>qa)throw new Error("too late");return e}function un(t,n){var e=t.__transition;if(!e||!(e=e[n]))throw new Error("too late");return e}function an(t,n,e){function i(t){e.state=Ya,e.timer.restart(r,e.delay,e.time),e.delay<=t&&r(t-e.delay)}function r(i){var h,c,l,f;if(e.state!==Ya)return u();for(h in s)if(f=s[h],f.name===e.name){if(f.state===Ha)return Pa(r);f.state===za?(f.state=ja,f.timer.stop(),f.on.call("interrupt",t,t.__data__,f.index,f.group),delete s[h]):+h=0&&(t=t.slice(0,n)),!t||"start"===t})}function kn(t,n,e){var i,r,o=Nn(n)?rn:on;return function(){var u=o(this,t),a=u.on;a!==i&&(r=(i=a).copy()).on(n,e),u.on=r}}function Tn(t){return function(){var n=this.parentNode;for(var e in this.__transition)if(+e!==t)return;n&&n.removeChild(this)}}function Cn(t,n){var e,i,r;return function(){var o=au(this).getComputedStyle(this,null),u=o.getPropertyValue(t),a=(this.style.removeProperty(t),o.getPropertyValue(t));return u===a?null:u===e&&a===i?r:r=n(e=u,i=a)}}function Sn(t){return function(){this.style.removeProperty(t)}}function An(t,n,e){var i,r;return function(){var o=au(this).getComputedStyle(this,null).getPropertyValue(t);return o===e?null:o===i?r:r=n(i=o,e)}}function En(t,n,e){var i,r,o;return function(){var u=au(this).getComputedStyle(this,null),a=u.getPropertyValue(t),s=e(this);return null==s&&(this.style.removeProperty(t),s=u.getPropertyValue(t)),a===s?null:a===i&&s===r?o:o=n(i=a,r=s)}}function Un(t,n,e){function i(){var i=this,r=n.apply(i,arguments);return r&&function(n){i.style.setProperty(t,r(n),e)}}return i._value=n,i}function Ln(t){return function(){this.textContent=t}}function Pn(t){return function(){var n=t(this);this.textContent=null==n?"":n}}function Dn(t,n,e,i){this._groups=t,this._parents=n,this._name=e,this._id=i}function Rn(t){return ht().transition(t)}function Fn(){return++ls}function Yn(t){return((t*=2)<=1?t*t*t:(t-=2)*t*t+2)/2}function qn(t,n){for(var e;!(e=t.__transition)||!(e=e[n]);)if(!(t=t.parentNode))return gs.time=Zt(),gs;return e}function Hn(t){return{type:t}}function zn(){this._x0=this._y0=this._x1=this._y1=null,this._=""}function On(){return new zn}function jn(){}function Xn(t,n){var e=new jn;if(t instanceof jn)t.each(function(t,n){e.set(n,t)});else if(Array.isArray(t)){var i,r=-1,o=t.length;if(null==n)for(;++r=(o=(d+v)/2))?d=o:v=o,(c=e>=(u=(y+g)/2))?y=u:g=u,r=_,!(_=_[l=c<<1|h]))return r[l]=p,t;if(a=+t._x.call(null,_.data),s=+t._y.call(null,_.data),n===a&&e===s)return p.next=_,r?r[l]=p:t._root=p,t;do r=r?r[l]=new Array(4):t._root=new Array(4),(h=n>=(o=(d+v)/2))?d=o:v=o,(c=e>=(u=(y+g)/2))?y=u:g=u;while((l=c<<1|h)===(f=(s>=u)<<1|a>=o));return r[f]=_,r[l]=p,t}function Jn(t){var n,e,i,r,o=t.length,u=new Array(o),a=new Array(o),s=1/0,h=1/0,c=-(1/0),l=-(1/0);for(e=0;ec&&(c=i),rl&&(l=r));for(c",r=n[3]||"-",o=n[4]||"",u=!!n[5],a=n[6]&&+n[6],s=!!n[7],h=n[8]&&+n[8].slice(1),c=n[9]||"";"n"===c?(s=!0,c="g"):Qs[c]||(c=""),(u||"0"===e&&"="===i)&&(u=!0,e="0",i="="),this.fill=e,this.align=i,this.sign=r,this.symbol=o,this.zero=u,this.width=a,this.comma=s,this.precision=h,this.type=c}function re(t){return t}function oe(t){return nh=oh(t),eh=nh.format,ih=nh.formatPrefix,nh}function ue(){this.reset()}function ae(t,n,e){var i=t.s=n+e,r=i-n,o=i-r;t.t=n-o+(e-r)}function se(t){return t>1?0:t<-1?fh:Math.acos(t)}function he(t){return t>1?_h:t<-1?-_h:Math.asin(t)}function ce(){}function le(t){var n=t[0],e=t[1],i=mh(e);return[i*mh(n),i*Mh(n),Mh(e)]}function fe(t,n){return[t[1]*n[2]-t[2]*n[1],t[2]*n[0]-t[0]*n[2],t[0]*n[1]-t[1]*n[0]]}function _e(t){var n=Nh(t[0]*t[0]+t[1]*t[1]+t[2]*t[2]);t[0]/=n,t[1]/=n,t[2]/=n}function pe(t,n){return[t>fh?t-dh:t<-fh?t+dh:t,n]}function de(t,n,e,i){this.x=t,this.z=n,this.o=e,this.e=i,this.v=!1,this.n=this.p=null}function ye(t){if(n=t.length){for(var n,e,i=0,r=t[0];++i1}function we(t,n){return((t=t.x)[0]<0?t[1]-_h-lh:_h-t[1])-((n=n.x)[0]<0?n[1]-_h-lh:_h-n[1])}function be(t){var n,e=NaN,i=NaN,r=NaN;return{lineStart:function(){t.lineStart(),n=1},point:function(o,u){var a=o>0?fh:-fh,s=vh(o-e);vh(s-fh)0?_h:-_h),t.point(r,i),t.lineEnd(),t.lineStart(),t.point(a,i),t.point(o,i),n=0):r!==a&&s>=fh&&(vh(e-r)lh?gh((Mh(n)*(o=mh(i))*Mh(e)-Mh(i)*(r=mh(n))*Mh(t))/(r*o*u)):(n+i)/2}function Ne(t,n,e,i){var r;if(null==t)r=e*_h,i.point(-fh,r),i.point(0,r),i.point(fh,r),i.point(fh,0),i.point(fh,-r),i.point(0,-r),i.point(-fh,-r),i.point(-fh,0),i.point(-fh,r);else if(vh(t[0]-n[0])>lh){var o=t[0]=0;)n+=e[i].value;else n=1;t.value=n}function Ue(t,n){if(t===n)return t;var e=t.ancestors(),i=n.ancestors(),r=null;for(t=e.pop(),n=i.pop();t===n;)r=t,t=e.pop(),n=i.pop();return r}function Le(t,n){var e,i,r,o,u,a=new Ye(t),s=+t.value&&(a.value=t.value),h=[a];for(null==n&&(n=De);e=h.pop();)if(s&&(e.value=+e.data.value),(r=n(e.data))&&(u=r.length))for(e.children=new Array(u),o=u-1;o>=0;--o)h.push(i=e.children[o]=new Ye(r[o])),i.parent=e,i.depth=e.depth+1;return a.eachBefore(Fe)}function Pe(){return Le(this).eachBefore(Re)}function De(t){return t.children}function Re(t){t.data=t.data.data}function Fe(t){var n=0;do t.height=n;while((t=t.parent)&&t.height<++n)}function Ye(t){this.data=t,this.depth=this.height=0,this.parent=null}function qe(t,n){this._=t,this.parent=null,this.children=null,this.A=null,this.a=this,this.z=0,this.m=0,this.c=0,this.s=0,this.t=null,this.i=n}function He(t,n,e,i,r,o){for(var u,a,s,h,c,l,f,_,p,d,y,v=[],g=n.children,x=0,m=0,w=g.length,b=n.value;xf&&(f=a),y=c*c*d,_=Math.max(f/y,y/l),_>p){c-=a;break}p=_}v.push(u={value:c,dice:s=0;)if((e=t._tasks[i])&&(t._tasks[i]=null,e.abort))try{e.abort()}catch(n){}t._active=NaN,Ie(t)}function Ie(t){if(!t._active&&t._call){var n=t._data;t._data=void 0,t._call(t._error,n)}}function $e(t){return function(n,e){t(null==n?e:null)}}function Be(t){var n=t.responseType;return n&&"text"!==n?t.response:t.responseText}function Ve(t,n){return function(e){return t(e.responseText,n)}}function Ze(t){function n(n){var o=n+"",u=e.get(o);if(!u){if(r!==ic)return r;e.set(o,u=i.push(n))}return t[(u-1)%t.length]}var e=Xn(),i=[],r=ic;return t=null==t?[]:ec.call(t),n.domain=function(t){if(!arguments.length)return i.slice();i=[],e=Xn();for(var r,o,u=-1,a=t.length;++u=e?1:i(t)}}}function Qe(t){return function(n,e){var i=t(n=+n,e=+e);return function(t){return t<=0?n:t>=1?e:i(t)}}}function Ke(t,n,e,i){var r=t[0],o=t[1],u=n[0],a=n[1];return o2?ti:Ke,o=u=null,i}function i(n){return(o||(o=r(a,s,c?Ge(t):t,h)))(+n)}var r,o,u,a=uc,s=uc,h=pa,c=!1;return i.invert=function(t){return(u||(u=r(s,a,Je,c?Qe(n):n)))(+t)},i.domain=function(t){return arguments.length?(a=nc.call(t,oc),e()):a.slice()},i.range=function(t){return arguments.length?(s=ec.call(t),e()):s.slice()},i.rangeRound=function(t){return s=ec.call(t),h=da,e()},i.clamp=function(t){return arguments.length?(c=!!t,e()):c},i.interpolate=function(t){return arguments.length?(h=t,e()):h},e()}function ii(t){var n=t.domain;return t.ticks=function(t){var e=n();return go(e[0],e[e.length-1],null==t?10:t)},t.tickFormat=function(t,e){return ac(n(),t,e)},t.nice=function(i){var r=n(),o=r.length-1,u=null==i?10:i,a=r[0],s=r[o],h=e(a,s,u);return h&&(h=e(Math.floor(a/h)*h,Math.ceil(s/h)*h,u),r[0]=Math.floor(a/h)*h,r[o]=Math.ceil(s/h)*h,n(r)),t},t}function ri(){var t=ei(Je,ha);return t.copy=function(){return ni(t,ri())},ii(t)}function oi(t,n,e,i){function r(n){return t(n=new Date((+n))),n}return r.floor=r,r.ceil=function(e){return t(e=new Date(e-1)),n(e,1),t(e),e},r.round=function(t){var n=r(t),e=r.ceil(t);return t-n0))return u;do u.push(new Date((+e)));while(n(e,o),t(e),e=n)for(;t(n),!e(n);)n.setTime(n-1)},function(t,i){if(t>=t)for(;--i>=0;)for(;n(t,1),!e(t););})},e&&(r.count=function(n,i){return sc.setTime(+n),hc.setTime(+i),t(sc),t(hc),Math.floor(e(sc,hc))},r.every=function(t){return t=Math.floor(t),isFinite(t)&&t>0?t>1?r.filter(i?function(n){return i(n)%t===0}:function(n){return r.count(0,n)%t===0}):r:null}),r}function ui(t){return oi(function(n){n.setDate(n.getDate()-(n.getDay()+7-t)%7),n.setHours(0,0,0,0)},function(t,n){t.setDate(t.getDate()+7*n)},function(t,n){return(n-t-(n.getTimezoneOffset()-t.getTimezoneOffset())*fc)/dc})}function ai(t){return oi(function(n){n.setUTCDate(n.getUTCDate()-(n.getUTCDay()+7-t)%7),n.setUTCHours(0,0,0,0)},function(t,n){t.setUTCDate(t.getUTCDate()+7*n)},function(t,n){return(n-t)/dc})}function si(t){if(0<=t.y&&t.y<100){var n=new Date((-1),t.m,t.d,t.H,t.M,t.S,t.L);return n.setFullYear(t.y),n}return new Date(t.y,t.m,t.d,t.H,t.M,t.S,t.L)}function hi(t){if(0<=t.y&&t.y<100){var n=new Date(Date.UTC(-1,t.m,t.d,t.H,t.M,t.S,t.L));return n.setUTCFullYear(t.y),n}return new Date(Date.UTC(t.y,t.m,t.d,t.H,t.M,t.S,t.L))}function ci(t){return{y:t,m:0,d:1,H:0,M:0,S:0,L:0}}function li(t){function n(t,n){return function(e){var i,r,o,u=[],a=-1,s=0,h=t.length;for(e instanceof Date||(e=new Date((+e)));++a=s)return-1;if(r=n.charCodeAt(u++),37===r){if(r=n.charAt(u++),o=j[r in Ac?n.charAt(u++):r],!o||(i=o(t,e,i))<0)return-1}else if(r!=e.charCodeAt(i++))return-1}return i}function r(t,n,e){var i=E.exec(n.slice(e));return i?(t.p=U[i[0].toLowerCase()],e+i[0].length):-1}function o(t,n,e){var i=D.exec(n.slice(e));return i?(t.w=R[i[0].toLowerCase()],e+i[0].length):-1}function u(t,n,e){var i=L.exec(n.slice(e));return i?(t.w=P[i[0].toLowerCase()],e+i[0].length):-1}function a(t,n,e){var i=q.exec(n.slice(e));return i?(t.m=H[i[0].toLowerCase()],e+i[0].length):-1}function s(t,n,e){var i=F.exec(n.slice(e));return i?(t.m=Y[i[0].toLowerCase()],e+i[0].length):-1}function h(t,n,e){return i(t,b,n,e)}function c(t,n,e){return i(t,M,n,e)}function l(t,n,e){return i(t,N,n,e)}function f(t){return C[t.getDay()]}function _(t){return T[t.getDay()]}function p(t){return A[t.getMonth()]}function d(t){return S[t.getMonth()]}function y(t){return k[+(t.getHours()>=12)]}function v(t){return C[t.getUTCDay()]}function g(t){return T[t.getUTCDay()]}function x(t){return A[t.getUTCMonth()]}function m(t){return S[t.getUTCMonth()]}function w(t){return k[+(t.getUTCHours()>=12)]}var b=t.dateTime,M=t.date,N=t.time,k=t.periods,T=t.days,C=t.shortDays,S=t.months,A=t.shortMonths,E=pi(k),U=di(k),L=pi(T),P=di(T),D=pi(C),R=di(C),F=pi(S),Y=di(S),q=pi(A),H=di(A),z={a:f,A:_,b:p,B:d,c:null,d:Ei,e:Ei,H:Ui,I:Li,j:Pi,L:Di,m:Ri,M:Fi,p:y,S:Yi,U:qi,w:Hi,W:zi,x:null,X:null,y:Oi,Y:ji,Z:Xi,"%":rr},O={a:v,A:g,b:x,B:m,c:null,d:Ii,e:Ii,H:$i,I:Bi,j:Vi,L:Zi,m:Wi,M:Ji,p:w,S:Gi,U:Qi,w:Ki,W:tr,x:null,X:null,y:nr,Y:er,Z:ir,"%":rr},j={a:o,A:u,b:a,B:s,c:h,d:Mi,e:Mi,H:ki,I:ki,j:Ni,L:Si,m:bi,M:Ti,p:r,S:Ci,U:vi,w:yi,W:gi,x:c,X:l,y:mi,Y:xi,Z:wi,"%":Ai};return z.x=n(M,z),z.X=n(N,z),z.c=n(b,z),O.x=n(M,O),O.X=n(N,O),O.c=n(b,O),{format:function(t){var e=n(t+="",z);return e.toString=function(){return t},e},parse:function(t){var n=e(t+="",si);return n.toString=function(){return t},n},utcFormat:function(t){var e=n(t+="",O);return e.toString=function(){return t},e},utcParse:function(t){var n=e(t,hi);return n.toString=function(){return t},n}}}function fi(t,n,e){var i=t<0?"-":"",r=(i?-t:t)+"",o=r.length;return i+(o68?1900:2e3),e+i[0].length):-1}function wi(t,n,e){var i=/^(Z)|([+-]\d\d)(?:\:?(\d\d))?/.exec(n.slice(e,e+6));return i?(t.Z=i[1]?0:-(i[2]+(i[3]||"00")),e+i[0].length):-1}function bi(t,n,e){var i=Ec.exec(n.slice(e,e+2));return i?(t.m=i[0]-1,e+i[0].length):-1}function Mi(t,n,e){var i=Ec.exec(n.slice(e,e+2));return i?(t.d=+i[0],e+i[0].length):-1}function Ni(t,n,e){var i=Ec.exec(n.slice(e,e+3));return i?(t.m=0,t.d=+i[0],e+i[0].length):-1}function ki(t,n,e){var i=Ec.exec(n.slice(e,e+2));return i?(t.H=+i[0],e+i[0].length):-1}function Ti(t,n,e){var i=Ec.exec(n.slice(e,e+2));return i?(t.M=+i[0],e+i[0].length):-1}function Ci(t,n,e){var i=Ec.exec(n.slice(e,e+2));return i?(t.S=+i[0],e+i[0].length):-1}function Si(t,n,e){var i=Ec.exec(n.slice(e,e+3));return i?(t.L=+i[0],e+i[0].length):-1}function Ai(t,n,e){var i=Uc.exec(n.slice(e,e+1));return i?e+i[0].length:-1}function Ei(t,n){return fi(t.getDate(),n,2)}function Ui(t,n){return fi(t.getHours(),n,2)}function Li(t,n){return fi(t.getHours()%12||12,n,2)}function Pi(t,n){return fi(1+yc.count(xc(t),t),n,3)}function Di(t,n){return fi(t.getMilliseconds(),n,3)}function Ri(t,n){return fi(t.getMonth()+1,n,2)}function Fi(t,n){return fi(t.getMinutes(),n,2)}function Yi(t,n){return fi(t.getSeconds(),n,2)}function qi(t,n){return fi(vc.count(xc(t),t),n,2)}function Hi(t){return t.getDay()}function zi(t,n){return fi(gc.count(xc(t),t),n,2)}function Oi(t,n){return fi(t.getFullYear()%100,n,2)}function ji(t,n){return fi(t.getFullYear()%1e4,n,4)}function Xi(t){var n=t.getTimezoneOffset();return(n>0?"-":(n*=-1,"+"))+fi(n/60|0,"0",2)+fi(n%60,"0",2)}function Ii(t,n){return fi(t.getUTCDate(),n,2)}function $i(t,n){return fi(t.getUTCHours(),n,2)}function Bi(t,n){return fi(t.getUTCHours()%12||12,n,2)}function Vi(t,n){return fi(1+mc.count(Mc(t),t),n,3)}function Zi(t,n){return fi(t.getUTCMilliseconds(),n,3)}function Wi(t,n){return fi(t.getUTCMonth()+1,n,2)}function Ji(t,n){return fi(t.getUTCMinutes(),n,2)}function Gi(t,n){return fi(t.getUTCSeconds(),n,2)}function Qi(t,n){return fi(wc.count(Mc(t),t),n,2)}function Ki(t){return t.getUTCDay()}function tr(t,n){return fi(bc.count(Mc(t),t),n,2)}function nr(t,n){return fi(t.getUTCFullYear()%100,n,2)}function er(t,n){return fi(t.getUTCFullYear()%1e4,n,4)}function ir(){return"+0000"}function rr(){return"%"}function or(t){return Nc=li(t),kc=Nc.format,Tc=Nc.parse,Cc=Nc.utcFormat,Sc=Nc.utcParse,Nc}function ur(t){return t.toISOString()}function ar(t){var n=new Date(t);return isNaN(n)?null:n}function sr(t){this._context=t}function hr(t){return t[0]}function cr(t){return t[1]}function lr(t){this._curve=t}function fr(t){function n(n){return new lr(t(n))}return n._curve=t,n}function _r(t,n,e){t._context.bezierCurveTo((2*t._x0+t._x1)/3,(2*t._y0+t._y1)/3,(t._x0+2*t._x1)/3,(t._y0+2*t._y1)/3,(t._x0+4*t._x1+n)/6,(t._y0+4*t._y1+e)/6)}function pr(t){this._context=t}function dr(t,n){this._basis=new pr(t),this._beta=n}function yr(t,n,e){t._context.bezierCurveTo(t._x1+t._k*(t._x2-t._x0),t._y1+t._k*(t._y2-t._y0),t._x2+t._k*(t._x1-n),t._y2+t._k*(t._y1-e),t._x2,t._y2)}function vr(t,n){this._context=t,this._k=(1-n)/6}function gr(t,n){this._context=t,this._k=(1-n)/6}function xr(t,n){this._context=t,this._k=(1-n)/6}function mr(t,n,e){var i=t._x1,r=t._y1,o=t._x2,u=t._y2;if(t._l01_a>Fc){var a=2*t._l01_2a+3*t._l01_a*t._l12_a+t._l12_2a,s=3*t._l01_a*(t._l01_a+t._l12_a);i=(i*a-t._x0*t._l12_2a+t._x2*t._l01_2a)/s,r=(r*a-t._y0*t._l12_2a+t._y2*t._l01_2a)/s}if(t._l23_a>Fc){var h=2*t._l23_2a+3*t._l23_a*t._l12_a+t._l12_2a,c=3*t._l23_a*(t._l23_a+t._l12_a);o=(o*h+t._x1*t._l23_2a-n*t._l12_2a)/c,u=(u*h+t._y1*t._l23_2a-e*t._l12_2a)/c}t._context.bezierCurveTo(i,r,o,u,t._x2,t._y2)}function wr(t,n){this._context=t,this._alpha=n}function br(t,n){this._context=t,this._alpha=n}function Mr(t,n){this._context=t,this._alpha=n}function Nr(t){return t<0?-1:1}function kr(t,n,e){var i=t._x1-t._x0,r=n-t._x1,o=(t._y1-t._y0)/(i||r<0&&-0),u=(e-t._y1)/(r||i<0&&-0),a=(o*r+u*i)/(i+r);return(Nr(o)+Nr(u))*Math.min(Math.abs(o),Math.abs(u),.5*Math.abs(a))||0}function Tr(t,n){var e=t._x1-t._x0;return e?(3*(t._y1-t._y0)/e-n)/2:n}function Cr(t,n,e){var i=t._x0,r=t._y0,o=t._x1,u=t._y1,a=(o-i)/3;t._context.bezierCurveTo(i+a,r+a*n,o-a,u-a*e,o,u)}function Sr(t){this._context=t}function Ar(t){this._context=new Er(t)}function Er(t){this._context=t}function Ur(){this._=null}function Lr(t){t.U=t.C=t.L=t.R=t.P=t.N=null}function Pr(t,n){var e=n,i=n.R,r=e.U;r?r.L===e?r.L=i:r.R=i:t._=i,i.U=r,e.U=i,e.R=i.L,e.R&&(e.R.U=e),i.L=e}function Dr(t,n){var e=n,i=n.L,r=e.U;r?r.L===e?r.L=i:r.R=i:t._=i,i.U=r,e.U=i,e.L=i.R,e.L&&(e.L.U=e),i.R=e}function Rr(t){for(;t.L;)t=t.L;return t}function Fr(t,n,e,i){var r=[null,null],o=Ic.push(r)-1;return r.left=t,r.right=n,e&&qr(r,t,n,e),i&&qr(r,n,t,i),jc[t.index].halfedges.push(o),jc[n.index].halfedges.push(o),r}function Yr(t,n,e){var i=[n,e];return i.left=t,i}function qr(t,n,e,i){t[0]||t[1]?t.left===e?t[1]=i:t[0]=i:(t[0]=i,t.left=n,t.right=e)}function Hr(t,n,e,i,r){var o,u=t[0],a=t[1],s=u[0],h=u[1],c=a[0],l=a[1],f=0,_=1,p=c-s,d=l-h;if(o=n-s,p||!(o>0)){if(o/=p,p<0){if(o0){if(o>_)return;o>f&&(f=o)}if(o=i-s,p||!(o<0)){if(o/=p,p<0){if(o>_)return;o>f&&(f=o)}else if(p>0){if(o0)){if(o/=d,d<0){if(o0){if(o>_)return;o>f&&(f=o)}if(o=r-h,d||!(o<0)){if(o/=d,d<0){if(o>_)return;o>f&&(f=o)}else if(d>0){if(o0||_<1)||(f>0&&(t[0]=[s+f*p,h+f*d]),_<1&&(t[1]=[s+_*p,h+_*d]),!0)}}}}}function zr(t,n,e,i,r){var o=t[1];if(o)return!0;var u,a,s=t[0],h=t.left,c=t.right,l=h[0],f=h[1],_=c[0],p=c[1],d=(l+_)/2,y=(f+p)/2;if(p===f){if(d=i)return;if(l>_){if(s){if(s[1]>=r)return}else s=[d,e];o=[d,r]}else{if(s){if(s[1]1)if(l>_){if(s){if(s[1]>=r)return}else s=[(e-a)/u,e];o=[(r-a)/u,r]}else{if(s){if(s[1]=i)return}else s=[n,u*n+a];o=[i,u*i+a]}else{if(s){if(s[0]Vc||Math.abs(r[0][1]-r[1][1])>Vc)||delete Ic[o]}function jr(t){return jc[t.index]={site:t,halfedges:[]}}function Xr(t,n){var e=t.site,i=n.left,r=n.right;return e===r&&(r=i,i=e),r?Math.atan2(r[1]-i[1],r[0]-i[0]):(e===i?(i=n[1],r=n[0]):(i=n[0],r=n[1]),Math.atan2(i[0]-r[0],r[1]-i[1]))}function Ir(t,n){return n[+(n.left!==t.site)]}function $r(t,n){return n[+(n.left===t.site)]}function Br(){for(var t,n,e,i,r=0,o=jc.length;rVc||Math.abs(d-f)>Vc)&&(s.splice(a,0,Ic.push(Yr(u,_,Math.abs(p-t)Vc?[t,Math.abs(l-t)Vc?[Math.abs(f-i)Vc?[e,Math.abs(l-e)Vc?[Math.abs(f-n)=-Zc)){var _=s*s+h*h,p=c*c+l*l,d=(l*_-h*p)/f,y=(s*p-c*_)/f,v=$c.pop()||new Zr;v.arc=t,v.site=r,v.x=d+u,v.y=(v.cy=y+a)+Math.sqrt(d*d+y*y),t.circle=v;for(var g=null,x=Xc._;x;)if(v.yVc)a=a.L;else{if(r=o-io(a,u),!(r>Vc)){i>-Vc?(n=a.P,e=a):r>-Vc?(n=a,e=a.N):n=e=a;break}if(!a.R){n=a;break}a=a.R}jr(t);var s=Qr(t);if(Oc.insert(n,s),n||e){if(n===e)return Jr(n),e=Qr(n.site),Oc.insert(s,e),s.edge=e.edge=Fr(n.site,s.site),Wr(n),void Wr(e);if(!e)return void(s.edge=Fr(n.site,s.site));Jr(n),Jr(e);var h=n.site,c=h[0],l=h[1],f=t[0]-c,_=t[1]-l,p=e.site,d=p[0]-c,y=p[1]-l,v=2*(f*y-_*d),g=f*f+_*_,x=d*d+y*y,m=[(y*g-_*x)/v+c,(f*x-d*g)/v+l];qr(e.edge,h,p,m),s.edge=Fr(h,t,null,m),e.edge=Fr(t,p,null,m),Wr(n),Wr(e)}}function eo(t,n){var e=t.site,i=e[0],r=e[1],o=r-n;if(!o)return i;var u=t.P;if(!u)return-(1/0);e=u.site;var a=e[0],s=e[1],h=s-n;if(!h)return a;var c=a-i,l=1/o-1/h,f=c/h;return l?(-f+Math.sqrt(f*f-2*l*(c*c/(-2*h)-s+h/2+r-o/2)))/l+i:(i+a)/2}function io(t,n){var e=t.N;if(e)return eo(e,n);var i=t.site;return i[1]===n?i[0]:1/0}function ro(t,n,e){return(t[0]-e[0])*(n[1]-t[1])-(t[0]-n[0])*(e[1]-t[1])}function oo(t,n){return n[1]-t[1]||n[0]-t[0]}function uo(t,n){var e,i,r,o=t.sort(oo).pop();for(Ic=[],jc=new Array(t.length),Oc=new Ur,Xc=new Ur;;)if(r=zc,o&&(!r||o[1]n?1:t>=n?0:NaN},ho=function(t){return 1===t.length&&(t=n(t)),{left:function(n,e,i,r){for(null==i&&(i=0),null==r&&(r=n.length);i>>1;t(n[o],e)<0?i=o+1:r=o}return i},right:function(n,e,i,r){for(null==i&&(i=0),null==r&&(r=n.length);i>>1;t(n[o],e)>0?r=o:i=o+1}return i}}},co=ho(so),lo=co.right,fo=Array.prototype,_o=(fo.slice,fo.map,function(t,n,e){t=+t,n=+n,e=(r=arguments.length)<2?(n=t,t=0,1):r<3?1:+e;for(var i=-1,r=0|Math.max(0,Math.ceil((n-t)/e)),o=new Array(r);++i=0;)for(i=t[r],n=i.length;--n>=0;)e[--u]=i[n];return e},mo=Array.prototype.slice,wo=function(t){return t},bo=1,Mo=2,No=3,ko=4,To=1e-6,Co={value:function(){}};f.prototype=l.prototype={constructor:f,on:function(t,n){var e,i=this._,r=_(t+"",i),o=-1,u=r.length;{if(!(arguments.length<2)){if(null!=n&&"function"!=typeof n)throw new Error("invalid callback: "+n);for(;++o0)for(var e,i,r=new Array(e),o=0;o=0&&"xmlns"!==(n=t.slice(0,e))&&(t=t.slice(e+1)),Ao.hasOwnProperty(n)?{space:Ao[n],local:t}:t},Uo=function(t){var n=Eo(t);return(n.local?v:y)(n)},Lo=function(t){return function(){return this.matches(t)}};if("undefined"!=typeof document){var Po=document.documentElement;if(!Po.matches){var Do=Po.webkitMatchesSelector||Po.msMatchesSelector||Po.mozMatchesSelector||Po.oMatchesSelector;Lo=function(t){return function(){return Do.call(this,t)}}}}var Ro=Lo,Fo={},Yo=null;if("undefined"!=typeof document){var qo=document.documentElement;"onmouseenter"in qo||(Fo={mouseenter:"mouseover",mouseleave:"mouseout"})}var Ho=function(t,n,e){var i,r,o=m(t+""),u=o.length;{if(!(arguments.length<2)){for(a=n?b:w,null==e&&(e=!1),i=0;i=w&&(w=m+1);!(x=y[w])&&++w=0;)(i=r[o])&&(u&&u!==i.nextSibling&&u.parentNode.insertBefore(i,u),u=i);return this},Ko=function(t){function n(n,e){return n&&e?t(n.__data__,e.__data__):!n-!e}t||(t=S);for(var e=this._groups,i=e.length,r=new Array(i),o=0;o1?this.each((null==n?R:"function"==typeof n?Y:F)(t,n,null==e?"":e)):au(i=this.node()).getComputedStyle(i,null).getPropertyValue(t)},hu=function(t,n){return arguments.length>1?this.each((null==n?q:"function"==typeof n?z:H)(t,n)):this.node()[t]};X.prototype={add:function(t){var n=this._names.indexOf(t);n<0&&(this._names.push(t),this._node.setAttribute("class",this._names.join(" ")))},remove:function(t){var n=this._names.indexOf(t);n>=0&&(this._names.splice(n,1),this._node.setAttribute("class",this._names.join(" ")))},contains:function(t){return this._names.indexOf(t)>=0}};var cu=function(t,n){var e=O(t+"");if(arguments.length<2){for(var i=j(this.node()),r=-1,o=e.length;++r=240?t-240:t+120,r,i),Mt(t,r,i),Mt(t<120?t+240:t-120,r,i),this.opacity)},displayable:function(){return(0<=this.s&&this.s<=1||isNaN(this.s))&&0<=this.l&&this.l<=1&&0<=this.opacity&&this.opacity<=1}}));var Yu=Math.PI/180,qu=180/Math.PI,Hu=18,zu=.95047,Ou=1,ju=1.08883,Xu=4/29,Iu=6/29,$u=3*Iu*Iu,Bu=Iu*Iu*Iu;bu(Tt,kt,lt(ft,{brighter:function(t){return new Tt(this.l+Hu*(null==t?1:t),this.a,this.b,this.opacity)},darker:function(t){return new Tt(this.l-Hu*(null==t?1:t),this.a,this.b,this.opacity)},rgb:function(){var t=(this.l+16)/116,n=isNaN(this.a)?t:t+this.a/500,e=isNaN(this.b)?t:t-this.b/200;return t=Ou*St(t),n=zu*St(n),e=ju*St(e),new gt(At(3.2404542*n-1.5371385*t-.4985314*e),At(-.969266*n+1.8760108*t+.041556*e),At(.0556434*n-.2040259*t+1.0572252*e),this.opacity)}})),bu(Pt,Lt,lt(ft,{brighter:function(t){return new Pt(this.h,this.c,this.l+Hu*(null==t?1:t),this.opacity)},darker:function(t){return new Pt(this.h,this.c,this.l-Hu*(null==t?1:t),this.opacity)},rgb:function(){return Nt(this).rgb()}}));var Vu=-.14861,Zu=1.78277,Wu=-.29227,Ju=-.90649,Gu=1.97294,Qu=Gu*Ju,Ku=Gu*Zu,ta=Zu*Wu-Ju*Vu;bu(Ft,Rt,lt(ft,{brighter:function(t){return t=null==t?Nu:Math.pow(Nu,t),new Ft(this.h,this.s,this.l*t,this.opacity)},darker:function(t){return t=null==t?Mu:Math.pow(Mu,t),new Ft(this.h,this.s,this.l*t,this.opacity)},rgb:function(){var t=isNaN(this.h)?0:(this.h+120)*Yu,n=+this.l,e=isNaN(this.s)?0:this.s*n*(1-n),i=Math.cos(t),r=Math.sin(t);return new gt(255*(n+e*(Vu*i+Zu*r)),255*(n+e*(Wu*i+Ju*r)),255*(n+e*(Gu*i)),this.opacity)}}));var na,ea,ia,ra,oa=function(t){return function(){return t}},ua=function Wc(t){function n(t,n){var i=e((t=vt(t)).r,(n=vt(n)).r),r=e(t.g,n.g),o=e(t.b,n.b),u=Ot(t.opacity,n.opacity);return function(n){return t.r=i(n),t.g=r(n),t.b=o(n),t.opacity=u(n),t+""}}var e=zt(t);return n.gamma=Wc,n}(1),aa=function(t,n){var e,i=n?n.length:0,r=t?Math.min(i,t.length):0,o=new Array(i),u=new Array(i);for(e=0;eo&&(r=n.slice(o,r),a[u]?a[u]+=r:a[++u]=r),(e=e[0])===(i=i[0])?a[u]?a[u]+=i:a[++u]=i:(a[++u]=null,s.push({i:u,x:ha(e,i)})),o=fa.lastIndex;return oqa&&e.statebs)if(Math.abs(c*a-s*h)>bs&&r){var f=e-o,_=i-u,p=a*a+s*s,d=f*f+_*_,y=Math.sqrt(p),v=Math.sqrt(l),g=r*Math.tan((ms-Math.acos((p+l-d)/(2*y*v)))/2),x=g/v,m=g/y;Math.abs(x-1)>bs&&(this._+="L"+(t+x*h)+","+(n+x*c)),this._+="A"+r+","+r+",0,0,"+ +(c*f>h*_)+","+(this._x1=t+m*a)+","+(this._y1=n+m*s)}else this._+="L"+(this._x1=t)+","+(this._y1=n);else;},arc:function(t,n,e,i,r,o){t=+t,n=+n,e=+e;var u=e*Math.cos(i),a=e*Math.sin(i),s=t+u,h=n+a,c=1^o,l=o?i-r:r-i;if(e<0)throw new Error("negative radius: "+e);null===this._x1?this._+="M"+s+","+h:(Math.abs(this._x1-s)>bs||Math.abs(this._y1-h)>bs)&&(this._+="L"+s+","+h),e&&(l>Ms?this._+="A"+e+","+e+",0,1,"+c+","+(t-u)+","+(n-a)+"A"+e+","+e+",0,1,"+c+","+(this._x1=s)+","+(this._y1=h):(l<0&&(l=l%ws+ws),this._+="A"+e+","+e+",0,"+ +(l>=ms)+","+c+","+(this._x1=t+e*Math.cos(r))+","+(this._y1=n+e*Math.sin(r))))},rect:function(t,n,e,i){this._+="M"+(this._x0=this._x1=+t)+","+(this._y0=this._y1=+n)+"h"+ +e+"v"+ +i+"h"+-e+"Z"},toString:function(){return this._}};var Ns="$";jn.prototype=Xn.prototype={constructor:jn,has:function(t){return Ns+t in this},get:function(t){return this[Ns+t]},set:function(t,n){return this[Ns+t]=n,this},remove:function(t){var n=Ns+t;return n in this&&delete this[n]},clear:function(){for(var t in this)t[0]===Ns&&delete this[t]},keys:function(){var t=[];for(var n in this)n[0]===Ns&&t.push(n.slice(1));return t},values:function(){var t=[];for(var n in this)n[0]===Ns&&t.push(this[n]);return t},entries:function(){var t=[];for(var n in this)n[0]===Ns&&t.push({key:n.slice(1),value:this[n]});return t},size:function(){var t=0;for(var n in this)n[0]===Ns&&++t;return t},empty:function(){for(var t in this)if(t[0]===Ns)return!1;return!0},each:function(t){for(var n in this)n[0]===Ns&&t(this[n],n.slice(1),this)}};var ks=Xn.prototype;In.prototype=$n.prototype={constructor:In,has:ks.has,add:function(t){return t+="",this[Ns+t]=t,this},remove:ks.remove,clear:ks.clear,values:ks.keys,size:ks.size,empty:ks.empty,each:ks.each};var Ts=function(t){function n(t,n){var i,r,o=e(t,function(t,e){return i?i(t,e-1):(r=t,void(i=n?Vn(t,n):Bn(t)))});return o.columns=r,o}function e(t,n){function e(){if(c>=h)return u;if(r)return r=!1,o;var n,e=c;if(34===t.charCodeAt(e)){for(var i=e;i++t||t>r||i>n||n>o))return this;var u,a,s=r-e,h=this._root;switch(a=(n<(i+o)/2)<<1|t<(e+r)/2){case 0:do u=new Array(4),u[a]=h,h=u;while(s*=2,r=e+s,o=i+s,t>r||n>o);break;case 1:do u=new Array(4),u[a]=h,h=u;while(s*=2,e=r-s,o=i+s,e>t||n>o);break;case 2:do u=new Array(4),u[a]=h,h=u;while(s*=2,r=e+s,i=o-s,t>r||i>n);break;case 3:do u=new Array(4),u[a]=h,h=u;while(s*=2,e=r-s,i=o-s,e>t||i>n)}this._root&&this._root.length&&(this._root=h)}return this._x0=e,this._y0=i,this._x1=r,this._y1=o,this},Ps=function(){var t=[];return this.visit(function(n){if(!n.length)do t.push(n.data);while(n=n.next)}),t},Ds=function(t){return arguments.length?this.cover(+t[0][0],+t[0][1]).cover(+t[1][0],+t[1][1]):isNaN(this._x0)?void 0:[[this._x0,this._y0],[this._x1,this._y1]]},Rs=function(t,n,e,i,r){this.node=t,this.x0=n,this.y0=e,this.x1=i,this.y1=r},Fs=function(t,n,e){var i,r,o,u,a,s,h,c=this._x0,l=this._y0,f=this._x1,_=this._y1,p=[],d=this._root;for(d&&p.push(new Rs(d,c,l,f,_)),null==e?e=1/0:(c=t-e,l=n-e,f=t+e,_=n+e,e*=e);s=p.pop();)if(!(!(d=s.node)||(r=s.x0)>f||(o=s.y0)>_||(u=s.x1)=v)<<1|t>=y)&&(s=p[p.length-1],p[p.length-1]=p[p.length-1-h],p[p.length-1-h]=s)}else{var g=t-+this._x.call(null,d.data),x=n-+this._y.call(null,d.data),m=g*g+x*x;if(m=(a=(p+y)/2))?p=a:y=a,(c=u>=(s=(d+v)/2))?d=s:v=s,n=_,!(_=_[l=c<<1|h]))return this;if(!_.length)break;(n[l+1&3]||n[l+2&3]||n[l+3&3])&&(e=n,f=l)}for(;_.data!==t;)if(i=_,!(_=_.next))return this;return(r=_.next)&&delete _.next,i?(r?i.next=r:delete i.next,this):n?(r?n[l]=r:delete n[l],(_=n[0]||n[1]||n[2]||n[3])&&_===(n[3]||n[2]||n[1]||n[0])&&!_.length&&(e?e[f]=_:this._root=_),this):(this._root=r,this)},qs=function(){return this._root},Hs=function(){var t=0;return this.visit(function(n){if(!n.length)do++t;while(n=n.next)}),t},zs=function(t){var n,e,i,r,o,u,a=[],s=this._root;for(s&&a.push(new Rs(s,this._x0,this._y0,this._x1,this._y1));n=a.pop();)if(!t(s=n.node,i=n.x0,r=n.y0,o=n.x1,u=n.y1)&&s.length){var h=(i+o)/2,c=(r+u)/2;(e=s[3])&&a.push(new Rs(e,h,c,o,u)),(e=s[2])&&a.push(new Rs(e,i,c,h,u)),(e=s[1])&&a.push(new Rs(e,h,r,o,c)),(e=s[0])&&a.push(new Rs(e,i,r,h,c))}return this},Os=function(t){var n,e=[],i=[];for(this._root&&e.push(new Rs(this._root,this._x0,this._y0,this._x1,this._y1));n=e.pop();){var r=n.node;if(r.length){var o,u=n.x0,a=n.y0,s=n.x1,h=n.y1,c=(u+s)/2,l=(a+h)/2;(o=r[0])&&e.push(new Rs(o,u,a,c,l)),(o=r[1])&&e.push(new Rs(o,c,a,s,l)),(o=r[2])&&e.push(new Rs(o,u,l,c,h)),(o=r[3])&&e.push(new Rs(o,c,l,s,h))}i.push(n)}for(;n=i.pop();)t(n.node,n.x0,n.y0,n.x1,n.y1);return this},js=function(t){return arguments.length?(this._x=t,this):this._x},Xs=function(t){return arguments.length?(this._y=t,this):this._y},Is=te.prototype=ne.prototype;Is.copy=function(){var t,n,e=new ne(this._x,this._y,this._x0,this._y0,this._x1,this._y1),i=this._root;if(!i)return e;if(!i.length)return e._root=ee(i),e;for(t=[{source:i,target:e._root=new Array(4)}];i=t.pop();)for(var r=0;r<4;++r)(n=i.source[r])&&(n.length?t.push({source:n,target:i.target[r]=new Array(4)}):i.target[r]=ee(n));return e},Is.add=Us,Is.addAll=Jn,Is.cover=Ls,Is.data=Ps,Is.extent=Ds,Is.find=Fs,Is.remove=Ys,Is.removeAll=Gn,Is.root=qs,Is.size=Hs,Is.visit=zs,Is.visitAfter=Os,Is.x=js,Is.y=Xs;var $s,Bs=(Math.PI*(3-Math.sqrt(5)),function(t,n){if((e=(t=n?t.toExponential(n-1):t.toExponential()).indexOf("e"))<0)return null;var e,i=t.slice(0,e);return[i.length>1?i[0]+i.slice(2):i,+t.slice(e+1)]}),Vs=function(t){return t=Bs(Math.abs(t)),t?t[1]:NaN},Zs=function(t,n){return function(e,i){for(var r=e.length,o=[],u=0,a=t[0],s=0;r>0&&a>0&&(s+a+1>i&&(a=Math.max(1,i-s)),o.push(e.substring(r-=a,r+a)),!((s+=a+1)>i));)a=t[u=(u+1)%t.length];return o.reverse().join(n)}},Ws=function(t,n){t=t.toPrecision(n);t:for(var e,i=t.length,r=1,o=-1;r0&&(o=0)}return o>0?t.slice(0,o)+t.slice(e+1):t},Js=function(t,n){var e=Bs(t,n);if(!e)return t+"";var i=e[0],r=e[1],o=r-($s=3*Math.max(-8,Math.min(8,Math.floor(r/3))))+1,u=i.length;return o===u?i:o>u?i+new Array(o-u+1).join("0"):o>0?i.slice(0,o)+"."+i.slice(o):"0."+new Array(1-o).join("0")+Bs(t,Math.max(0,n+o-1))[0]},Gs=function(t,n){var e=Bs(t,n);if(!e)return t+"";var i=e[0],r=e[1];return r<0?"0."+new Array((-r)).join("0")+i:i.length>r+1?i.slice(0,r+1)+"."+i.slice(r+1):i+new Array(r-i.length+2).join("0")},Qs={"":Ws,"%":function(t,n){return(100*t).toFixed(n)},b:function(t){return Math.round(t).toString(2)},c:function(t){return t+""},d:function(t){return Math.round(t).toString(10)},e:function(t,n){return t.toExponential(n)},f:function(t,n){return t.toFixed(n)},g:function(t,n){return t.toPrecision(n)},o:function(t){return Math.round(t).toString(8)},p:function(t,n){return Gs(100*t,n)},r:Gs,s:Js,X:function(t){return Math.round(t).toString(16).toUpperCase()},x:function(t){return Math.round(t).toString(16)}},Ks=/^(?:(.)?([<>=^]))?([+\-\( ])?([$#])?(0)?(\d+)?(,)?(\.\d+)?([a-z%])?$/i,th=function(t){return new ie(t)};ie.prototype.toString=function(){return this.fill+this.align+this.sign+this.symbol+(this.zero?"0":"")+(null==this.width?"":Math.max(1,0|this.width))+(this.comma?",":"")+(null==this.precision?"":"."+Math.max(0,0|this.precision))+this.type};var nh,eh,ih,rh=["y","z","a","f","p","n","µ","m","","k","M","G","T","P","E","Z","Y"],oh=function(t){function n(t){function n(t){var n,r,s,g=p,x=d;if("c"===_)x=y(t)+x,t="";else{t=+t;var m=(t<0||1/t<0)&&(t*=-1,!0);if(t=y(t,f),m)for(n=-1,r=t.length,m=!1;++ns||s>57){x=(46===s?o+t.slice(n+1):t.slice(n))+x,t=t.slice(0,n);break}}l&&!h&&(t=i(t,1/0));var w=g.length+t.length+x.length,b=w>1)+g+t+x+b.slice(w)}return b+g+t+x}t=th(t);var e=t.fill,u=t.align,a=t.sign,s=t.symbol,h=t.zero,c=t.width,l=t.comma,f=t.precision,_=t.type,p="$"===s?r[0]:"#"===s&&/[boxX]/.test(_)?"0"+_.toLowerCase():"",d="$"===s?r[1]:/[%p]/.test(_)?"%":"",y=Qs[_],v=!_||/[defgprs%]/.test(_);return f=null==f?_?6:12:/[gprs]/.test(_)?Math.max(1,Math.min(21,f)):Math.max(0,Math.min(20,f)),n.toString=function(){return t+""},n}function e(t,e){var i=n((t=th(t),t.type="f",t)),r=3*Math.max(-8,Math.min(8,Math.floor(Vs(e)/3))),o=Math.pow(10,-r),u=rh[8+r/3];return function(t){return i(o*t)+u}}var i=t.grouping&&t.thousands?Zs(t.grouping,t.thousands):re,r=t.currency,o=t.decimal;return{format:n,formatPrefix:e}};oe({decimal:".",thousands:",",grouping:[3],currency:["$",""]});var uh=function(t){return Math.max(0,-Vs(Math.abs(t)))},ah=function(t,n){return Math.max(0,3*Math.max(-8,Math.min(8,Math.floor(Vs(n)/3)))-Vs(Math.abs(t)))},sh=function(t,n){return t=Math.abs(t),n=Math.abs(n)-t,Math.max(0,Vs(n)-Vs(t))+1},hh=function(){return new ue};ue.prototype={constructor:ue,reset:function(){this.s=this.t=0},add:function(t){ae(ch,t,this.t),ae(this,ch.s,this.s),this.s?this.t+=ch.t:this.s=ch.t},valueOf:function(){return this.s}};var ch=new ue,lh=1e-6,fh=Math.PI,_h=fh/2,ph=fh/4,dh=2*fh,yh=fh/180,vh=Math.abs,gh=Math.atan,xh=Math.atan2,mh=Math.cos,wh=(Math.ceil,Math.exp),bh=Math.log,Mh=(Math.pow,Math.sin),Nh=(Math.sign||function(t){return t>0?1:t<0?-1:0},Math.sqrt),kh=Math.tan;hh(),hh(),hh();pe.invert=pe;var Th=function(){var t,n=[];return{point:function(n,e){t.push([n,e])},lineStart:function(){n.push(t=[])},lineEnd:ce,rejoin:function(){n.length>1&&n.push(n.pop().concat(n.shift()))},result:function(){var e=n;return n=[],t=null,e}}},Ch=function(t,n){return vh(t[0]-n[0])=0;--o)r.point((c=h[o])[0],c[1]);else i(f.x,f.p.x,-1,r);f=f.p}f=f.o,h=f.z,_=!_}while(!f.v);r.lineEnd()}}},Ah=(hh(),hh(),hh(),1/0),Eh=-Ah;ve.prototype={_radius:4.5,pointRadius:function(t){return this._radius=t,this},polygonStart:function(){this._line=0},polygonEnd:function(){this._line=NaN},lineStart:function(){this._point=0},lineEnd:function(){0===this._line&&this._context.closePath(),this._point=NaN},point:function(t,n){switch(this._point){case 0:this._context.moveTo(t,n),this._point=1;break;case 1:this._context.lineTo(t,n);break;default:this._context.moveTo(t+this._radius,n),this._context.arc(t,n,this._radius,0,dh)}},result:ce};hh();ge.prototype={_circle:xe(4.5),pointRadius:function(t){return this._circle=xe(t),this},polygonStart:function(){this._line=0},polygonEnd:function(){this._line=NaN},lineStart:function(){this._point=0},lineEnd:function(){0===this._line&&this._string.push("Z"),this._point=NaN},point:function(t,n){switch(this._point){case 0:this._string.push("M",t,",",n),this._point=1;break;case 1:this._string.push("L",t,",",n);break;default:this._string.push("M",t,",",n,this._circle)}},result:function(){if(this._string.length){var t=this._string.join("");return this._string=[],t}}};var Uh=hh(),Lh=function(t,n){var e=n[0],i=n[1],r=[Mh(e),-mh(e),0],o=0,u=0;Uh.reset();for(var a=0,s=t.length;a=0?1:-1,N=M*b,k=N>fh,T=p*m;if(Uh.add(xh(T*M*Mh(N),d*w+T*mh(N))),o+=k?b+M*dh:b,k^f>=e^g>=e){var C=fe(le(l),le(v));_e(C);var S=fe(r,C);_e(S);var A=(k^b>=0?-1:1)*he(S[2]);(i>A||i===A&&(C[0]||C[1]))&&(u+=k^b>=0?1:-1)}}return(o<-lh||o0){for(m||(o.polygonStart(),m=!0),o.lineStart(),t=0;t1&&2&r&&u.push(u.pop().concat(u.shift())),p.push(u.filter(me))}var _,p,d,y=n(o),v=r.invert(i[0],i[1]),g=Th(),x=n(g),m=!1,w={point:u,lineStart:s,lineEnd:h,polygonStart:function(){w.point=c,w.lineStart=l,w.lineEnd=f,p=[],_=[]},polygonEnd:function(){w.point=u,w.lineStart=s,w.lineEnd=h,p=xo(p);var t=Lh(_,v);p.length?(m||(o.polygonStart(),m=!0),Sh(p,we,t,e,o)):t&&(m||(o.polygonStart(),m=!0),o.lineStart(),e(null,null,1,o),o.lineEnd()),m&&(o.polygonEnd(),m=!1),p=_=null},sphere:function(){o.polygonStart(),o.lineStart(),e(null,null,1,o),o.lineEnd(),o.polygonEnd()}};return w}};Ph(function(){return!0},be,Ne,[-fh,-_h]);Te.prototype={constructor:Te,point:function(t,n){this.stream.point(t,n)},sphere:function(){this.stream.sphere()},lineStart:function(){this.stream.lineStart()},lineEnd:function(){this.stream.lineEnd()},polygonStart:function(){this.stream.polygonStart()},polygonEnd:function(){this.stream.polygonEnd()}};var Dh=(mh(30*yh),ke({point:function(t,n){this.stream.point(t*yh,n*yh)}}),Ce(function(t){return Nh(2/(1+t))}));Dh.invert=Se(function(t){return 2*he(t/2)});var Rh=Ce(function(t){return(t=se(t))&&t/Mh(t)});Rh.invert=Se(function(t){return t}),Ae.invert=function(t,n){return[-n,2*gh(wh(t))-_h]};var Fh=function(){return this.eachAfter(Ee)},Yh=function(t){var n,e,i,r,o=this,u=[o];do for(n=u.reverse(),u=[];o=n.pop();)if(t(o),e=o.children)for(i=0,r=e.length;i=0;--e)r.push(n[e]);return this},Hh=function(t){for(var n,e,i,r=this,o=[r],u=[];r=o.pop();)if(u.push(r),n=r.children)for(e=0,i=n.length;e=0;)e+=i[r].value;n.value=e})},Oh=function(t){return this.eachBefore(function(n){n.children&&n.children.sort(t)})},jh=function(t){for(var n=this,e=Ue(n,t),i=[n];n!==e;)n=n.parent,i.push(n);for(var r=i.length;t!==e;)i.splice(r,0,t),t=t.parent;return i},Xh=function(){for(var t=this,n=[t];t=t.parent;)n.push(t);return n},Ih=function(){var t=[];return this.each(function(n){t.push(n)}),t},$h=function(){var t=[];return this.eachBefore(function(n){n.children||t.push(n)}),t},Bh=function(){var t=this,n=[];return t.each(function(e){e!==t&&n.push({source:e.parent,target:e})}),n};Ye.prototype=Le.prototype={constructor:Ye,count:Fh,each:Yh,eachAfter:Hh,eachBefore:qh,sum:zh,sort:Oh,path:jh,ancestors:Xh,descendants:Ih,leaves:$h,links:Bh,copy:Pe};var Vh=function(t,n,e,i,r){for(var o,u=t.children,a=-1,s=u.length,h=t.value&&(i-n)/t.value;++a1?t:1)},n})(Wh);!function ul(t){function n(n,e,i,r,o){if((u=n._squarify)&&u.ratio===t)for(var u,a,s,h,c,l=-1,f=u.length,_=n.value;++l1?t:1)},n}(Wh);var Jh=([].slice,{}),Gh=function(t,n){function e(t){var n,e=h.status;if(!e&&Be(h)||e>=200&&e<300||304===e){if(o)try{n=o.call(i,h)}catch(r){return void a.call("error",i,r)}else n=h;a.call("load",i,n)}else a.call("error",i,t)}var i,r,o,u,a=l("beforesend","progress","load","error"),s=Xn(),h=new XMLHttpRequest,c=null,f=null,_=0;if("undefined"==typeof XDomainRequest||"withCredentials"in h||!/^(http(s)?:)?\/\//.test(t)||(h=new XDomainRequest),"onload"in h?h.onload=h.onerror=h.ontimeout=e:h.onreadystatechange=function(t){h.readyState>3&&e(t)},h.onprogress=function(t){a.call("progress",i,t)},i={header:function(t,n){return t=(t+"").toLowerCase(),arguments.length<2?s.get(t):(null==n?s.remove(t):s.set(t,n+""),i)},mimeType:function(t){return arguments.length?(r=null==t?null:t+"",i):r},responseType:function(t){return arguments.length?(u=t,i):u},timeout:function(t){return arguments.length?(_=+t,i):_},user:function(t){return arguments.length<1?c:(c=null==t?null:t+"",i)},password:function(t){return arguments.length<1?f:(f=null==t?null:t+"",i)},response:function(t){return o=t,i},get:function(t,n){return i.send("GET",t,n)},post:function(t,n){return i.send("POST",t,n)},send:function(n,e,o){return h.open(n,t,!0,c,f),null==r||s.has("accept")||s.set("accept",r+",*/*"),h.setRequestHeader&&s.each(function(t,n){h.setRequestHeader(n,t)}),null!=r&&h.overrideMimeType&&h.overrideMimeType(r),null!=u&&(h.responseType=u),_>0&&(h.timeout=_),null==o&&"function"==typeof e&&(o=e,e=null),null!=o&&1===o.length&&(o=$e(o)),null!=o&&i.on("error",o).on("load",function(t){o(null,t)}),a.call("beforesend",i,h),h.send(null==e?null:e),i},abort:function(){return h.abort(),i},on:function(){var t=a.on.apply(a,arguments);return t===a?i:t}},null!=n){if("function"!=typeof n)throw new Error("invalid callback: "+n);return i.get(n)}return i},Qh=function(t,n){return function(e,i){var r=Gh(e).mimeType(t).response(n);if(null!=i){if("function"!=typeof i)throw new Error("invalid callback: "+i);return r.get(i)}return r}};Qh("text/html",function(t){return document.createRange().createContextualFragment(t.responseText)}),Qh("application/json",function(t){return JSON.parse(t.responseText)}),Qh("text/plain",function(t){return t.responseText}),Qh("application/xml",function(t){var n=t.responseXML;if(!n)throw new Error("parse error");return n});var Kh=function(t,n){return function(e,i,r){arguments.length<3&&(r=i,i=null);var o=Gh(e).mimeType(t);return o.row=function(t){return arguments.length?o.response(Ve(n,i=t)):i},o.row(i),r?o.get(r):o}};Kh("text/csv",Ss),Kh("text/tab-separated-values",Es);var tc=Array.prototype,nc=tc.map,ec=tc.slice,ic={name:"implicit"},rc=function(t){return function(){return t}},oc=function(t){return+t},uc=[0,1],ac=function(t,n,i){var r,o=t[0],u=t[t.length-1],a=e(o,u,null==n?10:n);switch(i=th(null==i?",f":i),i.type){case"s":var s=Math.max(Math.abs(o),Math.abs(u));return null!=i.precision||isNaN(r=ah(a,s))||(i.precision=r),ih(i,s);case"":case"e":case"g":case"p":case"r":null!=i.precision||isNaN(r=sh(a,Math.max(Math.abs(o),Math.abs(u))))||(i.precision=r-("e"===i.type));break;case"f":case"%":null!=i.precision||isNaN(r=uh(a))||(i.precision=r-2*("%"===i.type))}return eh(i)},sc=new Date,hc=new Date,cc=oi(function(){},function(t,n){t.setTime(+t+n)},function(t,n){return n-t});cc.every=function(t){return t=Math.floor(t),isFinite(t)&&t>0?t>1?oi(function(n){n.setTime(Math.floor(n/t)*t)},function(n,e){n.setTime(+n+e*t)},function(n,e){return(e-n)/t}):cc:null};var lc=1e3,fc=6e4,_c=36e5,pc=864e5,dc=6048e5,yc=(oi(function(t){t.setTime(Math.floor(t/lc)*lc)},function(t,n){t.setTime(+t+n*lc)},function(t,n){return(n-t)/lc},function(t){return t.getUTCSeconds()}),oi(function(t){t.setTime(Math.floor(t/fc)*fc)},function(t,n){t.setTime(+t+n*fc)},function(t,n){return(n-t)/fc},function(t){return t.getMinutes()}),oi(function(t){var n=t.getTimezoneOffset()*fc%_c;n<0&&(n+=_c),t.setTime(Math.floor((+t-n)/_c)*_c+n)},function(t,n){t.setTime(+t+n*_c)},function(t,n){return(n-t)/_c},function(t){return t.getHours()}),oi(function(t){t.setHours(0,0,0,0)},function(t,n){t.setDate(t.getDate()+n)},function(t,n){return(n-t-(n.getTimezoneOffset()-t.getTimezoneOffset())*fc)/pc},function(t){return t.getDate()-1})),vc=ui(0),gc=ui(1),xc=(ui(2),ui(3),ui(4),ui(5),ui(6),oi(function(t){t.setDate(1),t.setHours(0,0,0,0)},function(t,n){t.setMonth(t.getMonth()+n)},function(t,n){return n.getMonth()-t.getMonth()+12*(n.getFullYear()-t.getFullYear())},function(t){return t.getMonth()}),oi(function(t){t.setMonth(0,1),t.setHours(0,0,0,0)},function(t,n){t.setFullYear(t.getFullYear()+n)},function(t,n){return n.getFullYear()-t.getFullYear()},function(t){return t.getFullYear()}));xc.every=function(t){return isFinite(t=Math.floor(t))&&t>0?oi(function(n){n.setFullYear(Math.floor(n.getFullYear()/t)*t),n.setMonth(0,1),n.setHours(0,0,0,0)},function(n,e){n.setFullYear(n.getFullYear()+e*t)}):null};var mc=(oi(function(t){t.setUTCSeconds(0,0)},function(t,n){t.setTime(+t+n*fc)},function(t,n){return(n-t)/fc},function(t){return t.getUTCMinutes()}),oi(function(t){t.setUTCMinutes(0,0,0)},function(t,n){t.setTime(+t+n*_c)},function(t,n){return(n-t)/_c},function(t){return t.getUTCHours()}),oi(function(t){t.setUTCHours(0,0,0,0)},function(t,n){t.setUTCDate(t.getUTCDate()+n)},function(t,n){return(n-t)/pc},function(t){return t.getUTCDate()-1})),wc=ai(0),bc=ai(1),Mc=(ai(2),ai(3),ai(4),ai(5),ai(6),oi(function(t){t.setUTCDate(1),t.setUTCHours(0,0,0,0)},function(t,n){t.setUTCMonth(t.getUTCMonth()+n)},function(t,n){return n.getUTCMonth()-t.getUTCMonth()+12*(n.getUTCFullYear()-t.getUTCFullYear())},function(t){return t.getUTCMonth()}),oi(function(t){t.setUTCMonth(0,1),t.setUTCHours(0,0,0,0)},function(t,n){t.setUTCFullYear(t.getUTCFullYear()+n)},function(t,n){return n.getUTCFullYear()-t.getUTCFullYear()},function(t){return t.getUTCFullYear()}));Mc.every=function(t){return isFinite(t=Math.floor(t))&&t>0?oi(function(n){n.setUTCFullYear(Math.floor(n.getUTCFullYear()/t)*t),n.setUTCMonth(0,1),n.setUTCHours(0,0,0,0)},function(n,e){n.setUTCFullYear(n.getUTCFullYear()+e*t)}):null};var Nc,kc,Tc,Cc,Sc,Ac={"-":"",_:" ",0:"0"},Ec=/^\s*\d+/,Uc=/^%/,Lc=/[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g;or({dateTime:"%x, %X",date:"%-m/%-d/%Y",time:"%-I:%M:%S %p",periods:["AM","PM"],days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],shortDays:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],shortMonths:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]});var Pc="%Y-%m-%dT%H:%M:%S.%LZ",Dc=(Date.prototype.toISOString?ur:Cc(Pc),+new Date("2000-01-01T00:00:00.000Z")?ar:Sc(Pc),function(t){return t.match(/.{6}/g).map(function(t){return"#"+t})});Dc("1f77b4ff7f0e2ca02cd627289467bd8c564be377c27f7f7fbcbd2217becf"),Dc("393b795254a36b6ecf9c9ede6379398ca252b5cf6bcedb9c8c6d31bd9e39e7ba52e7cb94843c39ad494ad6616be7969c7b4173a55194ce6dbdde9ed6"),Dc("3182bd6baed69ecae1c6dbefe6550dfd8d3cfdae6bfdd0a231a35474c476a1d99bc7e9c0756bb19e9ac8bcbddcdadaeb636363969696bdbdbdd9d9d9"),Dc("1f77b4aec7e8ff7f0effbb782ca02c98df8ad62728ff98969467bdc5b0d58c564bc49c94e377c2f7b6d27f7f7fc7c7c7bcbd22dbdb8d17becf9edae5"),Ma(Rt(300,.5,0),Rt(-240,.5,1));var Rc=(Ma(Rt(-100,.75,.35),Rt(80,1.5,.8)),Ma(Rt(260,.75,.35),Rt(80,1.5,.8)),Rt(),function(t){return function(){return t}}),Fc=1e-12;Math.PI;sr.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._point=0},lineEnd:function(){(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;default:this._context.lineTo(t,n)}}};var Yc=function(t){return new sr(t)},qc=function(){function t(t){ -var a,s,h,c=t.length,l=!1;for(null==r&&(u=o(h=On())),a=0;a<=c;++a)!(a0)for(var i,r=t[0],o=n[0],u=t[e]-r,a=n[e]-o,s=-1;++s<=e;)i=s/e,this._basis.point(this._beta*t[s]+(1-this._beta)*(r+i*u),this._beta*n[s]+(1-this._beta)*(o+i*a));this._x=this._y=null,this._basis.lineEnd()},point:function(t,n){this._x.push(+t),this._y.push(+n)}},function al(t){function n(n){return 1===t?new pr(n):new dr(n,t)}return n.beta=function(t){return al(+t)},n}(.85),vr.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x2,this._y2);break;case 3:yr(this,this._x1,this._y1)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2,this._x1=t,this._y1=n;break;case 2:this._point=3;default:yr(this,t,n)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}},function sl(t){function n(n){return new vr(n,t)}return n.tension=function(t){return sl(+t)},n}(0),gr.prototype={areaStart:Hc,areaEnd:Hc,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._x5=this._y0=this._y1=this._y2=this._y3=this._y4=this._y5=NaN,this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x3,this._y3),this._context.closePath();break;case 2:this._context.lineTo(this._x3,this._y3),this._context.closePath();break;case 3:this.point(this._x3,this._y3),this.point(this._x4,this._y4),this.point(this._x5,this._y5)}},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._x3=t,this._y3=n;break;case 1:this._point=2,this._context.moveTo(this._x4=t,this._y4=n);break;case 2:this._point=3,this._x5=t,this._y5=n;break;default:yr(this,t,n)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}},function hl(t){function n(n){return new gr(n,t)}return n.tension=function(t){return hl(+t)},n}(0),xr.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3,this._line?this._context.lineTo(this._x2,this._y2):this._context.moveTo(this._x2,this._y2);break;case 3:this._point=4;default:yr(this,t,n)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}},function cl(t){function n(n){return new xr(n,t)}return n.tension=function(t){return cl(+t)},n}(0),wr.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x2,this._y2);break;case 3:this.point(this._x2,this._y2)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){if(t=+t,n=+n,this._point){var e=this._x2-t,i=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(e*e+i*i,this._alpha))}switch(this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;break;case 2:this._point=3;default:mr(this,t,n)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}},function ll(t){function n(n){return t?new wr(n,t):new vr(n,0)}return n.alpha=function(t){return ll(+t)},n}(.5),br.prototype={areaStart:Hc,areaEnd:Hc,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._x5=this._y0=this._y1=this._y2=this._y3=this._y4=this._y5=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x3,this._y3),this._context.closePath();break;case 2:this._context.lineTo(this._x3,this._y3),this._context.closePath();break;case 3:this.point(this._x3,this._y3),this.point(this._x4,this._y4),this.point(this._x5,this._y5)}},point:function(t,n){if(t=+t,n=+n,this._point){var e=this._x2-t,i=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(e*e+i*i,this._alpha))}switch(this._point){case 0:this._point=1,this._x3=t,this._y3=n;break;case 1:this._point=2,this._context.moveTo(this._x4=t,this._y4=n);break;case 2:this._point=3,this._x5=t,this._y5=n;break;default:mr(this,t,n)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}},function fl(t){function n(n){return t?new br(n,t):new gr(n,0)}return n.alpha=function(t){return fl(+t)},n}(.5),Mr.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){if(t=+t,n=+n,this._point){var e=this._x2-t,i=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(e*e+i*i,this._alpha))}switch(this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3,this._line?this._context.lineTo(this._x2,this._y2):this._context.moveTo(this._x2,this._y2);break;case 3:this._point=4;default:mr(this,t,n)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}},function _l(t){function n(n){return t?new Mr(n,t):new xr(n,0)}return n.alpha=function(t){return _l(+t)},n}(.5),Sr.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=this._t0=NaN,this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x1,this._y1);break;case 3:Cr(this,this._t0,Tr(this,this._t0))}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){var e=NaN;if(t=+t,n=+n,t!==this._x1||n!==this._y1){switch(this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;break;case 2:this._point=3,Cr(this,Tr(this,e=kr(this,t,n)),e);break;default:Cr(this,this._t0,e=kr(this,t,n))}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=n,this._t0=e}}},(Ar.prototype=Object.create(Sr.prototype)).point=function(t,n){Sr.prototype.point.call(this,n,t)},Er.prototype={moveTo:function(t,n){this._context.moveTo(n,t)},closePath:function(){this._context.closePath()},lineTo:function(t,n){this._context.lineTo(n,t)},bezierCurveTo:function(t,n,e,i,r,o){this._context.bezierCurveTo(n,t,i,e,o,r)}};Array.prototype.slice;Ur.prototype={constructor:Ur,insert:function(t,n){var e,i,r;if(t){if(n.P=t,n.N=t.N,t.N&&(t.N.P=n),t.N=n,t.R){for(t=t.R;t.L;)t=t.L;t.L=n}else t.R=n;e=t}else this._?(t=Rr(this._),n.P=null,n.N=t,t.P=t.L=n,e=t):(n.P=n.N=null,this._=n,e=null);for(n.L=n.R=null,n.U=e,n.C=!0,t=n;e&&e.C;)i=e.U,e===i.L?(r=i.R,r&&r.C?(e.C=r.C=!1,i.C=!0,t=i):(t===e.R&&(Pr(this,e),t=e,e=t.U),e.C=!1,i.C=!0,Dr(this,i))):(r=i.L,r&&r.C?(e.C=r.C=!1,i.C=!0,t=i):(t===e.L&&(Dr(this,e),t=e,e=t.U),e.C=!1,i.C=!0,Pr(this,i))),e=t.U;this._.C=!1},remove:function(t){t.N&&(t.N.P=t.P),t.P&&(t.P.N=t.N),t.N=t.P=null;var n,e,i,r=t.U,o=t.L,u=t.R;if(e=o?u?Rr(u):o:u,r?r.L===t?r.L=e:r.R=e:this._=e,o&&u?(i=e.C,e.C=t.C,e.L=o,o.U=e,e!==u?(r=e.U,e.U=t.U,t=e.R,r.L=t,e.R=u,u.U=e):(e.U=r,r=e,t=e.R)):(i=t.C,t=e),t&&(t.U=r),!i){if(t&&t.C)return void(t.C=!1);do{if(t===this._)break;if(t===r.L){if(n=r.R,n.C&&(n.C=!1,r.C=!0,Pr(this,r),n=r.R),n.L&&n.L.C||n.R&&n.R.C){n.R&&n.R.C||(n.L.C=!1,n.C=!0,Dr(this,n),n=r.R),n.C=r.C,r.C=n.R.C=!1,Pr(this,r),t=this._;break}}else if(n=r.L,n.C&&(n.C=!1,r.C=!0,Dr(this,r),n=r.L),n.L&&n.L.C||n.R&&n.R.C){n.L&&n.L.C||(n.R.C=!1,n.C=!0,Pr(this,n),n=r.L),n.C=r.C,r.C=n.L.C=!1,Dr(this,r),t=this._;break}n.C=!0,t=r,r=r.U}while(!t.C);t&&(t.C=!1)}}};var zc,Oc,jc,Xc,Ic,$c=[],Bc=[],Vc=1e-6,Zc=1e-12;uo.prototype={constructor:uo,polygons:function(){var t=this.edges;return this.cells.map(function(n){var e=n.halfedges.map(function(e){return Ir(n,t[e])});return e.data=n.site.data,e})},triangles:function(){var t=[],n=this.edges;return this.cells.forEach(function(e,i){if(o=(r=e.halfedges).length)for(var r,o,u,a=e.site,s=-1,h=n[r[o-1]],c=h.left===a?h.right:h.left;++s=a)return null;var s=t-r.site[0],h=n-r.site[1],c=s*s+h*h;do r=o.cells[i=u],u=null,r.halfedges.forEach(function(e){var i=o.edges[e],a=i.left;if(a!==r.site&&a||(a=i.right)){var s=t-a[0],h=n-a[1],l=s*s+h*h;l=fo?r*=10:o>=_o?r*=5:o>=po&&(r*=2),n=0&&(e=t.slice(i+1),t=t.slice(0,i)),t&&!n.hasOwnProperty(t))throw new Error("unknown type: "+t);return{type:t,name:e}})}function p(t,n){for(var e,i=0,r=t.length;i=0&&(n=t.slice(e+1),t=t.slice(0,e)),{type:t,name:n}})}function w(t){return function(){var n=this.__on;if(n){for(var e,i=0,r=-1,o=n.length;in?1:t>=n?0:NaN}function A(t){return function(){this.removeAttribute(t)}}function E(t){return function(){this.removeAttributeNS(t.space,t.local)}}function U(t,n){return function(){this.setAttribute(t,n)}}function L(t,n){return function(){this.setAttributeNS(t.space,t.local,n)}}function P(t,n){return function(){var e=n.apply(this,arguments);null==e?this.removeAttribute(t):this.setAttribute(t,e)}}function D(t,n){return function(){var e=n.apply(this,arguments);null==e?this.removeAttributeNS(t.space,t.local):this.setAttributeNS(t.space,t.local,e)}}function R(t){return function(){this.style.removeProperty(t)}}function F(t,n,e){return function(){this.style.setProperty(t,n,e)}}function Y(t,n,e){return function(){var i=n.apply(this,arguments);null==i?this.style.removeProperty(t):this.style.setProperty(t,i,e)}}function q(t){return function(){delete this[t]}}function H(t,n){return function(){this[t]=n}}function z(t,n){return function(){var e=n.apply(this,arguments);null==e?delete this[t]:this[t]=e}}function O(t){return t.trim().split(/^|\s+/)}function j(t){return t.classList||new X(t)}function X(t){this._node=t,this._names=O(t.getAttribute("class")||"")}function I(t,n){for(var e=j(t),i=-1,r=n.length;++i>8&15|n>>4&240,n>>4&15|240&n,(15&n)<<4|15&n,1)):(n=Nu.exec(t))?pt(parseInt(n[1],16)):(n=ku.exec(t))?new gt(n[1],n[2],n[3],1):(n=Tu.exec(t))?new gt(255*n[1]/100,255*n[2]/100,255*n[3]/100,1):(n=Cu.exec(t))?dt(n[1],n[2],n[3],n[4]):(n=Su.exec(t))?dt(255*n[1]/100,255*n[2]/100,255*n[3]/100,n[4]):(n=Au.exec(t))?xt(n[1],n[2]/100,n[3]/100,1):(n=Eu.exec(t))?xt(n[1],n[2]/100,n[3]/100,n[4]):Uu.hasOwnProperty(t)?pt(Uu[t]):"transparent"===t?new gt(NaN,NaN,NaN,0):null}function pt(t){return new gt(t>>16&255,t>>8&255,255&t,1)}function dt(t,n,e,i){return i<=0&&(t=n=e=NaN),new gt(t,n,e,i)}function yt(t){return t instanceof ft||(t=_t(t)),t?(t=t.rgb(),new gt(t.r,t.g,t.b,t.opacity)):new gt}function vt(t,n,e,i){return 1===arguments.length?yt(t):new gt(t,n,e,null==i?1:i)}function gt(t,n,e,i){this.r=+t,this.g=+n,this.b=+e,this.opacity=+i}function xt(t,n,e,i){return i<=0?t=n=e=NaN:e<=0||e>=1?t=n=NaN:n<=0&&(t=NaN),new bt(t,n,e,i)}function mt(t){if(t instanceof bt)return new bt(t.h,t.s,t.l,t.opacity);if(t instanceof ft||(t=_t(t)),!t)return new bt;if(t instanceof bt)return t;t=t.rgb();var n=t.r/255,e=t.g/255,i=t.b/255,r=Math.min(n,e,i),o=Math.max(n,e,i),u=NaN,a=o-r,s=(o+r)/2;return a?(u=n===o?(e-i)/a+6*(e0&&s<1?0:u,new bt(u,a,s,t.opacity)}function wt(t,n,e,i){return 1===arguments.length?mt(t):new bt(t,n,e,null==i?1:i)}function bt(t,n,e,i){this.h=+t,this.s=+n,this.l=+e,this.opacity=+i}function Mt(t,n,e){return 255*(t<60?n+(e-n)*t/60:t<180?e:t<240?n+(e-n)*(240-t)/60:n)}function Nt(t){if(t instanceof Tt)return new Tt(t.l,t.a,t.b,t.opacity);if(t instanceof Pt){var n=t.h*Lu;return new Tt(t.l,Math.cos(n)*t.c,Math.sin(n)*t.c,t.opacity)}t instanceof gt||(t=yt(t));var e=Et(t.r),i=Et(t.g),r=Et(t.b),o=Ct((.4124564*e+.3575761*i+.1804375*r)/Du),u=Ct((.2126729*e+.7151522*i+.072175*r)/Ru);return new Tt(116*u-16,500*(o-u),200*(u-Ct((.0193339*e+.119192*i+.9503041*r)/Fu)),t.opacity)}function kt(t,n,e,i){return 1===arguments.length?Nt(t):new Tt(t,n,e,null==i?1:i)}function Tt(t,n,e,i){this.l=+t,this.a=+n,this.b=+e,this.opacity=+i}function Ct(t){return t>zu?Math.pow(t,1/3):t/Hu+Yu}function St(t){return t>qu?t*t*t:Hu*(t-Yu)}function At(t){return 255*(t<=.0031308?12.92*t:1.055*Math.pow(t,1/2.4)-.055)}function Et(t){return(t/=255)<=.04045?t/12.92:Math.pow((t+.055)/1.055,2.4)}function Ut(t){if(t instanceof Pt)return new Pt(t.h,t.c,t.l,t.opacity);t instanceof Tt||(t=Nt(t));var n=Math.atan2(t.b,t.a)*Pu;return new Pt(n<0?n+360:n,Math.sqrt(t.a*t.a+t.b*t.b),t.l,t.opacity)}function Lt(t,n,e,i){return 1===arguments.length?Ut(t):new Pt(t,n,e,null==i?1:i)}function Pt(t,n,e,i){this.h=+t,this.c=+n,this.l=+e,this.opacity=+i}function Dt(t){if(t instanceof Ft)return new Ft(t.h,t.s,t.l,t.opacity);t instanceof gt||(t=yt(t));var n=t.r/255,e=t.g/255,i=t.b/255,r=(Zu*i+Bu*n-Vu*e)/(Zu+Bu-Vu),o=i-r,u=($u*(e-r)-Xu*o)/Iu,a=Math.sqrt(u*u+o*o)/($u*r*(1-r)),s=a?Math.atan2(u,o)*Pu-120:NaN;return new Ft(s<0?s+360:s,a,r,t.opacity)}function Rt(t,n,e,i){return 1===arguments.length?Dt(t):new Ft(t,n,e,null==i?1:i)}function Ft(t,n,e,i){this.h=+t,this.s=+n,this.l=+e,this.opacity=+i}function Yt(t,n){return function(e){return t+e*n}}function qt(t,n,e){return t=Math.pow(t,e),n=Math.pow(n,e)-t,e=1/e,function(i){return Math.pow(t+i*n,e)}}function Ht(t,n){var e=n-t;return e?Yt(t,e>180||e<-180?e-360*Math.round(e/360):e):Ku(isNaN(t)?n:t)}function zt(t){return 1==(t=+t)?Ot:function(n,e){return e-n?qt(n,e,t):Ku(isNaN(n)?e:n)}}function Ot(t,n){var e=n-t;return e?Yt(t,e):Ku(isNaN(t)?n:t)}function jt(t){return function(){return t}}function Xt(t){return function(n){return t(n)+""}}function It(t){return"none"===t?la:(Wu||(Wu=document.createElement("DIV"),Ju=document.documentElement,Gu=document.defaultView),Wu.style.transform=t,t=Gu.getComputedStyle(Ju.appendChild(Wu),null).getPropertyValue("transform"),Ju.removeChild(Wu),t=t.slice(7,-1).split(","),fa(+t[0],+t[1],+t[2],+t[3],+t[4],+t[5]))}function $t(t){return null==t?la:(Qu||(Qu=document.createElementNS("http://www.w3.org/2000/svg","g")),Qu.setAttribute("transform",t),(t=Qu.transform.baseVal.consolidate())?(t=t.matrix,fa(t.a,t.b,t.c,t.d,t.e,t.f)):la)}function Bt(t,n,e,i){function r(t){return t.length?t.pop()+" ":""}function o(t,i,r,o,u,a){if(t!==r||i!==o){var s=u.push("translate(",null,n,null,e);a.push({i:s-4,x:ia(t,r)},{i:s-2,x:ia(i,o)})}else(r||o)&&u.push("translate("+r+n+o+e)}function u(t,n,e,o){t!==n?(t-n>180?n+=360:n-t>180&&(t+=360),o.push({i:e.push(r(e)+"rotate(",null,i)-2,x:ia(t,n)})):n&&e.push(r(e)+"rotate("+n+i)}function a(t,n,e,o){t!==n?o.push({i:e.push(r(e)+"skewX(",null,i)-2,x:ia(t,n)}):n&&e.push(r(e)+"skewX("+n+i)}function s(t,n,e,i,o,u){if(t!==e||n!==i){var a=o.push(r(o)+"scale(",null,",",null,")");u.push({i:a-4,x:ia(t,e)},{i:a-2,x:ia(n,i)})}else 1===e&&1===i||o.push(r(o)+"scale("+e+","+i+")")}return function(n,e){var i=[],r=[];return n=t(n),e=t(e),o(n.translateX,n.translateY,e.translateX,e.translateY,i,r),u(n.rotate,e.rotate,i,r),a(n.skewX,e.skewX,i,r),s(n.scaleX,n.scaleY,e.scaleX,e.scaleY,i,r),n=e=null,function(t){for(var n,e=-1,o=r.length;++e=0&&n._call.call(null,t),n=n._next;--ga}function Kt(){Ma=(ba=ka.now())+Na,ga=xa=0;try{Qt()}finally{ga=0,nn(),Ma=0}}function tn(){var t=ka.now(),n=t-ba;n>wa&&(Na-=n,ba=t)}function nn(){for(var t,n,e=da,i=1/0;e;)e._call?(i>e._time&&(i=e._time),t=e,e=e._next):(n=e._next,e._next=null,e=t?t._next=n:da=n);ya=t,en(i)}function en(t){if(!ga){xa&&(xa=clearTimeout(xa));var n=t-Ma;n>24?(t<1/0&&(xa=setTimeout(Kt,n)),ma&&(ma=clearInterval(ma))):(ma||(ba=Ma,ma=setInterval(tn,wa)),ga=1,Ta(Kt))}}function rn(t,n){var e=t.__transition;if(!e||!(e=e[n])||e.state>Ea)throw new Error("too late");return e}function on(t,n){var e=t.__transition;if(!e||!(e=e[n])||e.state>La)throw new Error("too late");return e}function un(t,n){var e=t.__transition;if(!e||!(e=e[n]))throw new Error("too late");return e}function an(t,n,e){function i(t){e.state=Ua,e.timer.restart(r,e.delay,e.time),e.delay<=t&&r(t-e.delay)}function r(i){var h,c,l,f;if(e.state!==Ua)return u();for(h in s)if(f=s[h],f.name===e.name){if(f.state===Pa)return Ca(r);f.state===Da?(f.state=Fa,f.timer.stop(),f.on.call("interrupt",t,t.__data__,f.index,f.group),delete s[h]):+h=0&&(t=t.slice(0,n)),!t||"start"===t})}function kn(t,n,e){var i,r,o=Nn(n)?rn:on;return function(){var u=o(this,t),a=u.on;a!==i&&(r=(i=a).copy()).on(n,e),u.on=r}}function Tn(t){return function(){var n=this.parentNode;for(var e in this.__transition)if(+e!==t)return;n&&n.removeChild(this)}}function Cn(t,n){var e,i,r;return function(){var o=ru(this).getComputedStyle(this,null),u=o.getPropertyValue(t),a=(this.style.removeProperty(t),o.getPropertyValue(t));return u===a?null:u===e&&a===i?r:r=n(e=u,i=a)}}function Sn(t){return function(){this.style.removeProperty(t)}}function An(t,n,e){var i,r;return function(){var o=ru(this).getComputedStyle(this,null).getPropertyValue(t);return o===e?null:o===i?r:r=n(i=o,e)}}function En(t,n,e){var i,r,o;return function(){var u=ru(this).getComputedStyle(this,null),a=u.getPropertyValue(t),s=e(this);return null==s&&(this.style.removeProperty(t),s=u.getPropertyValue(t)),a===s?null:a===i&&s===r?o:o=n(i=a,r=s)}}function Un(t,n,e){function i(){var i=this,r=n.apply(i,arguments);return r&&function(n){i.style.setProperty(t,r(n),e)}}return i._value=n,i}function Ln(t){return function(){this.textContent=t}}function Pn(t){return function(){var n=t(this);this.textContent=null==n?"":n}}function Dn(t,n,e,i){this._groups=t,this._parents=n,this._name=e,this._id=i}function Rn(t){return ht().transition(t)}function Fn(){return++os}function Yn(t){return((t*=2)<=1?t*t*t:(t-=2)*t*t+2)/2}function qn(t,n){for(var e;!(e=t.__transition)||!(e=e[n]);)if(!(t=t.parentNode))return ss.time=Zt(),ss;return e}function Hn(t){return{type:t}}function zn(){this._x0=this._y0=this._x1=this._y1=null,this._=""}function On(){return new zn}function jn(){}function Xn(t,n){var e=new jn;if(t instanceof jn)t.each(function(t,n){e.set(n,t)});else if(Array.isArray(t)){var i,r=-1,o=t.length;if(null==n)for(;++r=(o=(d+v)/2))?d=o:v=o,(c=e>=(u=(y+g)/2))?y=u:g=u,r=_,!(_=_[l=c<<1|h]))return r[l]=p,t;if(a=+t._x.call(null,_.data),s=+t._y.call(null,_.data),n===a&&e===s)return p.next=_,r?r[l]=p:t._root=p,t;do{r=r?r[l]=new Array(4):t._root=new Array(4),(h=n>=(o=(d+v)/2))?d=o:v=o,(c=e>=(u=(y+g)/2))?y=u:g=u}while((l=c<<1|h)==(f=(s>=u)<<1|a>=o));return r[f]=_,r[l]=p,t}function Jn(t){var n,e,i,r,o=t.length,u=new Array(o),a=new Array(o),s=1/0,h=1/0,c=-(1/0),l=-(1/0);for(e=0;ec&&(c=i),rl&&(l=r));for(c",r=n[3]||"-",o=n[4]||"",u=!!n[5],a=n[6]&&+n[6],s=!!n[7],h=n[8]&&+n[8].slice(1),c=n[9]||"";"n"===c?(s=!0,c="g"):zs[c]||(c=""),(u||"0"===e&&"="===i)&&(u=!0,e="0",i="="),this.fill=e,this.align=i,this.sign=r,this.symbol=o,this.zero=u,this.width=a,this.comma=s,this.precision=h,this.type=c}function re(t){return t}function oe(){this.reset()}function ue(t,n,e){var i=t.s=n+e,r=i-n,o=i-r;t.t=n-o+(e-r)}function ae(t){return t>1?0:t<-1?th:Math.acos(t)}function se(t){return t>1?nh:t<-1?-nh:Math.asin(t)}function he(){}function ce(t){var n=t[0],e=t[1],i=sh(e);return[i*sh(n),i*lh(n),lh(e)]}function le(t,n){return[t[1]*n[2]-t[2]*n[1],t[2]*n[0]-t[0]*n[2],t[0]*n[1]-t[1]*n[0]]}function fe(t){var n=fh(t[0]*t[0]+t[1]*t[1]+t[2]*t[2]);t[0]/=n,t[1]/=n,t[2]/=n}function _e(t,n){return[t>th?t-ih:t<-th?t+ih:t,n]}function pe(t,n,e,i){this.x=t,this.z=n,this.o=e,this.e=i,this.v=!1,this.n=this.p=null}function de(t){if(n=t.length){for(var n,e,i=0,r=t[0];++i1}function me(t,n){return((t=t.x)[0]<0?t[1]-nh-Ks:nh-t[1])-((n=n.x)[0]<0?n[1]-nh-Ks:nh-n[1])}function we(t){var n,e=NaN,i=NaN,r=NaN;return{lineStart:function(){t.lineStart(),n=1},point:function(o,u){var a=o>0?th:-th,s=oh(o-e);oh(s-th)0?nh:-nh),t.point(r,i),t.lineEnd(),t.lineStart(),t.point(a,i),t.point(o,i),n=0):r!==a&&s>=th&&(oh(e-r)Ks?uh((lh(n)*(o=sh(i))*lh(e)-lh(i)*(r=sh(n))*lh(t))/(r*o*u)):(n+i)/2}function Me(t,n,e,i){var r;if(null==t)r=e*nh,i.point(-th,r),i.point(0,r),i.point(th,r),i.point(th,0),i.point(th,-r),i.point(0,-r),i.point(-th,-r),i.point(-th,0),i.point(-th,r);else if(oh(t[0]-n[0])>Ks){var o=t[0]=0;)n+=e[i].value;else n=1;t.value=n}function Ee(t,n){if(t===n)return t;var e=t.ancestors(),i=n.ancestors(),r=null;for(t=e.pop(),n=i.pop();t===n;)r=t,t=e.pop(),n=i.pop();return r}function Ue(t,n){var e,i,r,o,u,a=new Fe(t),s=+t.value&&(a.value=t.value),h=[a];for(null==n&&(n=Pe);e=h.pop();)if(s&&(e.value=+e.data.value),(r=n(e.data))&&(u=r.length))for(e.children=new Array(u),o=u-1;o>=0;--o)h.push(i=e.children[o]=new Fe(r[o])),i.parent=e,i.depth=e.depth+1;return a.eachBefore(Re)}function Le(){return Ue(this).eachBefore(De)}function Pe(t){return t.children}function De(t){t.data=t.data.data}function Re(t){var n=0;do{t.height=n}while((t=t.parent)&&t.height<++n)}function Fe(t){this.data=t,this.depth=this.height=0,this.parent=null}function Ye(t,n){this._=t,this.parent=null,this.children=null,this.A=null,this.a=this,this.z=0,this.m=0,this.c=0,this.s=0,this.t=null,this.i=n}function qe(t,n,e,i,r,o){for(var u,a,s,h,c,l,f,_,p,d,y,v=[],g=n.children,x=0,m=0,w=g.length,b=n.value;xf&&(f=a),y=c*c*d,(_=Math.max(f/y,y/l))>p){c-=a;break}p=_}v.push(u={value:c,dice:s=0;)if((e=t._tasks[i])&&(t._tasks[i]=null,e.abort))try{e.abort()}catch(t){}t._active=NaN,Xe(t)}function Xe(t){if(!t._active&&t._call){var n=t._data;t._data=void 0,t._call(t._error,n)}}function Ie(t){return function(n,e){t(null==n?e:null)}}function $e(t){var n=t.responseType;return n&&"text"!==n?t.response:t.responseText}function Be(t,n){return function(e){return t(e.responseText,n)}}function Ve(t){function n(n){var o=n+"",u=e.get(o);if(!u){if(r!==Xh)return r;e.set(o,u=i.push(n))}return t[(u-1)%t.length]}var e=Xn(),i=[],r=Xh;return t=null==t?[]:jh.call(t),n.domain=function(t){if(!arguments.length)return i.slice();i=[],e=Xn();for(var r,o,u=-1,a=t.length;++u=e?1:i(t)}}}function Ge(t){return function(n,e){var i=t(n=+n,e=+e);return function(t){return t<=0?n:t>=1?e:i(t)}}}function Qe(t,n,e,i){var r=t[0],o=t[1],u=n[0],a=n[1];return o2?Ke:Qe,o=u=null,i}function i(n){return(o||(o=r(a,s,c?Je(t):t,h)))(+n)}var r,o,u,a=Bh,s=Bh,h=sa,c=!1;return i.invert=function(t){return(u||(u=r(s,a,We,c?Ge(n):n)))(+t)},i.domain=function(t){return arguments.length?(a=Oh.call(t,$h),e()):a.slice()},i.range=function(t){return arguments.length?(s=jh.call(t),e()):s.slice()},i.rangeRound=function(t){return s=jh.call(t),h=ha,e()},i.clamp=function(t){return arguments.length?(c=!!t,e()):c},i.interpolate=function(t){return arguments.length?(h=t,e()):h},e()}function ei(t){var n=t.domain;return t.ticks=function(t){var e=n();return yo(e[0],e[e.length-1],null==t?10:t)},t.tickFormat=function(t,e){return Vh(n(),t,e)},t.nice=function(i){var r=n(),o=r.length-1,u=null==i?10:i,a=r[0],s=r[o],h=e(a,s,u);return h&&(h=e(Math.floor(a/h)*h,Math.ceil(s/h)*h,u),r[0]=Math.floor(a/h)*h,r[o]=Math.ceil(s/h)*h,n(r)),t},t}function ii(){var t=ni(We,ia);return t.copy=function(){return ti(t,ii())},ei(t)}function ri(t,n,e,i){function r(n){return t(n=new Date(+n)),n}return r.floor=r,r.ceil=function(e){return t(e=new Date(e-1)),n(e,1),t(e),e},r.round=function(t){var n=r(t),e=r.ceil(t);return t-n0))return u;do{u.push(new Date(+e))}while(n(e,o),t(e),e=n)for(;t(n),!e(n);)n.setTime(n-1)},function(t,i){if(t>=t)for(;--i>=0;)for(;n(t,1),!e(t););})},e&&(r.count=function(n,i){return Zh.setTime(+n),Wh.setTime(+i),t(Zh),t(Wh),Math.floor(e(Zh,Wh))},r.every=function(t){return t=Math.floor(t),isFinite(t)&&t>0?t>1?r.filter(i?function(n){return i(n)%t==0}:function(n){return r.count(0,n)%t==0}):r:null}),r}function oi(t){return ri(function(n){n.setDate(n.getDate()-(n.getDay()+7-t)%7),n.setHours(0,0,0,0)},function(t,n){t.setDate(t.getDate()+7*n)},function(t,n){return(n-t-(n.getTimezoneOffset()-t.getTimezoneOffset())*Gh)/Qh})}function ui(t){return ri(function(n){n.setUTCDate(n.getUTCDate()-(n.getUTCDay()+7-t)%7),n.setUTCHours(0,0,0,0)},function(t,n){t.setUTCDate(t.getUTCDate()+7*n)},function(t,n){return(n-t)/Qh})}function ai(t){if(0<=t.y&&t.y<100){var n=new Date(-1,t.m,t.d,t.H,t.M,t.S,t.L);return n.setFullYear(t.y),n}return new Date(t.y,t.m,t.d,t.H,t.M,t.S,t.L)}function si(t){if(0<=t.y&&t.y<100){var n=new Date(Date.UTC(-1,t.m,t.d,t.H,t.M,t.S,t.L));return n.setUTCFullYear(t.y),n}return new Date(Date.UTC(t.y,t.m,t.d,t.H,t.M,t.S,t.L))}function hi(t){return{y:t,m:0,d:1,H:0,M:0,S:0,L:0}}function ci(t){function n(t,n){return function(e){var i,r,o,u=[],a=-1,s=0,h=t.length;for(e instanceof Date||(e=new Date(+e));++a=s)return-1;if(37===(r=n.charCodeAt(u++))){if(r=n.charAt(u++),!(o=j[r in fc?n.charAt(u++):r])||(i=o(t,e,i))<0)return-1}else if(r!=e.charCodeAt(i++))return-1}return i}function r(t,n,e){var i=E.exec(n.slice(e));return i?(t.p=U[i[0].toLowerCase()],e+i[0].length):-1}function o(t,n,e){var i=D.exec(n.slice(e));return i?(t.w=R[i[0].toLowerCase()],e+i[0].length):-1}function u(t,n,e){var i=L.exec(n.slice(e));return i?(t.w=P[i[0].toLowerCase()],e+i[0].length):-1}function a(t,n,e){var i=q.exec(n.slice(e));return i?(t.m=H[i[0].toLowerCase()],e+i[0].length):-1}function s(t,n,e){var i=F.exec(n.slice(e));return i?(t.m=Y[i[0].toLowerCase()],e+i[0].length):-1}function h(t,n,e){return i(t,b,n,e)}function c(t,n,e){return i(t,M,n,e)}function l(t,n,e){return i(t,N,n,e)}function f(t){return C[t.getDay()]}function _(t){return T[t.getDay()]}function p(t){return A[t.getMonth()]}function d(t){return S[t.getMonth()]}function y(t){return k[+(t.getHours()>=12)]}function v(t){return C[t.getUTCDay()]}function g(t){return T[t.getUTCDay()]}function x(t){return A[t.getUTCMonth()]}function m(t){return S[t.getUTCMonth()]}function w(t){return k[+(t.getUTCHours()>=12)]}var b=t.dateTime,M=t.date,N=t.time,k=t.periods,T=t.days,C=t.shortDays,S=t.months,A=t.shortMonths,E=_i(k),U=pi(k),L=_i(T),P=pi(T),D=_i(C),R=pi(C),F=_i(S),Y=pi(S),q=_i(A),H=pi(A),z={a:f,A:_,b:p,B:d,c:null,d:Ai,e:Ai,H:Ei,I:Ui,j:Li,L:Pi,m:Di,M:Ri,p:y,S:Fi,U:Yi,w:qi,W:Hi,x:null,X:null,y:zi,Y:Oi,Z:ji,"%":ir},O={a:v,A:g,b:x,B:m,c:null,d:Xi,e:Xi,H:Ii,I:$i,j:Bi,L:Vi,m:Zi,M:Wi,p:w,S:Ji,U:Gi,w:Qi,W:Ki,x:null,X:null,y:tr,Y:nr,Z:er,"%":ir},j={a:o,A:u,b:a,B:s,c:h,d:bi,e:bi,H:Ni,I:Ni,j:Mi,L:Ci,m:wi,M:ki,p:r,S:Ti,U:yi,w:di,W:vi,x:c,X:l,y:xi,Y:gi,Z:mi,"%":Si};return z.x=n(M,z),z.X=n(N,z),z.c=n(b,z),O.x=n(M,O),O.X=n(N,O),O.c=n(b,O),{format:function(t){var e=n(t+="",z);return e.toString=function(){return t},e},parse:function(t){var n=e(t+="",ai);return n.toString=function(){return t},n},utcFormat:function(t){var e=n(t+="",O);return e.toString=function(){return t},e},utcParse:function(t){var n=e(t,si);return n.toString=function(){return t},n}}}function li(t,n,e){var i=t<0?"-":"",r=(i?-t:t)+"",o=r.length;return i+(o68?1900:2e3),e+i[0].length):-1}function mi(t,n,e){var i=/^(Z)|([+-]\d\d)(?:\:?(\d\d))?/.exec(n.slice(e,e+6));return i?(t.Z=i[1]?0:-(i[2]+(i[3]||"00")),e+i[0].length):-1}function wi(t,n,e){var i=_c.exec(n.slice(e,e+2));return i?(t.m=i[0]-1,e+i[0].length):-1}function bi(t,n,e){var i=_c.exec(n.slice(e,e+2));return i?(t.d=+i[0],e+i[0].length):-1}function Mi(t,n,e){var i=_c.exec(n.slice(e,e+3));return i?(t.m=0,t.d=+i[0],e+i[0].length):-1}function Ni(t,n,e){var i=_c.exec(n.slice(e,e+2));return i?(t.H=+i[0],e+i[0].length):-1}function ki(t,n,e){var i=_c.exec(n.slice(e,e+2));return i?(t.M=+i[0],e+i[0].length):-1}function Ti(t,n,e){var i=_c.exec(n.slice(e,e+2));return i?(t.S=+i[0],e+i[0].length):-1}function Ci(t,n,e){var i=_c.exec(n.slice(e,e+3));return i?(t.L=+i[0],e+i[0].length):-1}function Si(t,n,e){var i=pc.exec(n.slice(e,e+1));return i?e+i[0].length:-1}function Ai(t,n){return li(t.getDate(),n,2)}function Ei(t,n){return li(t.getHours(),n,2)}function Ui(t,n){return li(t.getHours()%12||12,n,2)}function Li(t,n){return li(1+Kh.count(ec(t),t),n,3)}function Pi(t,n){return li(t.getMilliseconds(),n,3)}function Di(t,n){return li(t.getMonth()+1,n,2)}function Ri(t,n){return li(t.getMinutes(),n,2)}function Fi(t,n){return li(t.getSeconds(),n,2)}function Yi(t,n){return li(tc.count(ec(t),t),n,2)}function qi(t){return t.getDay()}function Hi(t,n){return li(nc.count(ec(t),t),n,2)}function zi(t,n){return li(t.getFullYear()%100,n,2)}function Oi(t,n){return li(t.getFullYear()%1e4,n,4)}function ji(t){var n=t.getTimezoneOffset();return(n>0?"-":(n*=-1,"+"))+li(n/60|0,"0",2)+li(n%60,"0",2)}function Xi(t,n){return li(t.getUTCDate(),n,2)}function Ii(t,n){return li(t.getUTCHours(),n,2)}function $i(t,n){return li(t.getUTCHours()%12||12,n,2)}function Bi(t,n){return li(1+ic.count(uc(t),t),n,3)}function Vi(t,n){return li(t.getUTCMilliseconds(),n,3)}function Zi(t,n){return li(t.getUTCMonth()+1,n,2)}function Wi(t,n){return li(t.getUTCMinutes(),n,2)}function Ji(t,n){return li(t.getUTCSeconds(),n,2)}function Gi(t,n){return li(rc.count(uc(t),t),n,2)}function Qi(t){return t.getUTCDay()}function Ki(t,n){return li(oc.count(uc(t),t),n,2)}function tr(t,n){return li(t.getUTCFullYear()%100,n,2)}function nr(t,n){return li(t.getUTCFullYear()%1e4,n,4)}function er(){return"+0000"}function ir(){return"%"}function rr(t){return t.toISOString()}function or(t){var n=new Date(t);return isNaN(n)?null:n}function ur(t){this._context=t}function ar(t){return t[0]}function sr(t){return t[1]}function hr(t){this._curve=t}function cr(t){function n(n){return new hr(t(n))}return n._curve=t,n}function lr(t,n,e){t._context.bezierCurveTo((2*t._x0+t._x1)/3,(2*t._y0+t._y1)/3,(t._x0+2*t._x1)/3,(t._y0+2*t._y1)/3,(t._x0+4*t._x1+n)/6,(t._y0+4*t._y1+e)/6)}function fr(t){this._context=t}function _r(t,n){this._basis=new fr(t),this._beta=n}function pr(t,n,e){t._context.bezierCurveTo(t._x1+t._k*(t._x2-t._x0),t._y1+t._k*(t._y2-t._y0),t._x2+t._k*(t._x1-n),t._y2+t._k*(t._y1-e),t._x2,t._y2)}function dr(t,n){this._context=t,this._k=(1-n)/6}function yr(t,n){this._context=t,this._k=(1-n)/6}function vr(t,n){this._context=t,this._k=(1-n)/6}function gr(t,n,e){var i=t._x1,r=t._y1,o=t._x2,u=t._y2;if(t._l01_a>gc){var a=2*t._l01_2a+3*t._l01_a*t._l12_a+t._l12_2a,s=3*t._l01_a*(t._l01_a+t._l12_a);i=(i*a-t._x0*t._l12_2a+t._x2*t._l01_2a)/s,r=(r*a-t._y0*t._l12_2a+t._y2*t._l01_2a)/s}if(t._l23_a>gc){var h=2*t._l23_2a+3*t._l23_a*t._l12_a+t._l12_2a,c=3*t._l23_a*(t._l23_a+t._l12_a);o=(o*h+t._x1*t._l23_2a-n*t._l12_2a)/c,u=(u*h+t._y1*t._l23_2a-e*t._l12_2a)/c}t._context.bezierCurveTo(i,r,o,u,t._x2,t._y2)}function xr(t,n){this._context=t,this._alpha=n}function mr(t,n){this._context=t,this._alpha=n}function wr(t,n){this._context=t,this._alpha=n}function br(t){return t<0?-1:1}function Mr(t,n,e){var i=t._x1-t._x0,r=n-t._x1,o=(t._y1-t._y0)/(i||r<0&&-0),u=(e-t._y1)/(r||i<0&&-0),a=(o*r+u*i)/(i+r);return(br(o)+br(u))*Math.min(Math.abs(o),Math.abs(u),.5*Math.abs(a))||0}function Nr(t,n){var e=t._x1-t._x0;return e?(3*(t._y1-t._y0)/e-n)/2:n}function kr(t,n,e){var i=t._x0,r=t._y0,o=t._x1,u=t._y1,a=(o-i)/3;t._context.bezierCurveTo(i+a,r+a*n,o-a,u-a*e,o,u)}function Tr(t){this._context=t}function Cr(t){this._context=new Sr(t)}function Sr(t){this._context=t}function Ar(){this._=null}function Er(t){t.U=t.C=t.L=t.R=t.P=t.N=null}function Ur(t,n){var e=n,i=n.R,r=e.U;r?r.L===e?r.L=i:r.R=i:t._=i,i.U=r,e.U=i,e.R=i.L,e.R&&(e.R.U=e),i.L=e}function Lr(t,n){var e=n,i=n.L,r=e.U;r?r.L===e?r.L=i:r.R=i:t._=i,i.U=r,e.U=i,e.L=i.R,e.L&&(e.L.U=e),i.R=e}function Pr(t){for(;t.L;)t=t.L;return t}function Dr(t,n,e,i){var r=[null,null],o=Tc.push(r)-1;return r.left=t,r.right=n,e&&Fr(r,t,n,e),i&&Fr(r,n,t,i),Nc[t.index].halfedges.push(o),Nc[n.index].halfedges.push(o),r}function Rr(t,n,e){var i=[n,e];return i.left=t,i}function Fr(t,n,e,i){t[0]||t[1]?t.left===e?t[1]=i:t[0]=i:(t[0]=i,t.left=n,t.right=e)}function Yr(t,n,e,i,r){var o,u=t[0],a=t[1],s=u[0],h=u[1],c=a[0],l=a[1],f=0,_=1,p=c-s,d=l-h;if(o=n-s,p||!(o>0)){if(o/=p,p<0){if(o0){if(o>_)return;o>f&&(f=o)}if(o=i-s,p||!(o<0)){if(o/=p,p<0){if(o>_)return;o>f&&(f=o)}else if(p>0){if(o0)){if(o/=d,d<0){if(o0){if(o>_)return;o>f&&(f=o)}if(o=r-h,d||!(o<0)){if(o/=d,d<0){if(o>_)return;o>f&&(f=o)}else if(d>0){if(o0||_<1)||(f>0&&(t[0]=[s+f*p,h+f*d]),_<1&&(t[1]=[s+_*p,h+_*d]),!0)}}}}}function qr(t,n,e,i,r){var o=t[1];if(o)return!0;var u,a,s=t[0],h=t.left,c=t.right,l=h[0],f=h[1],_=c[0],p=c[1],d=(l+_)/2,y=(f+p)/2;if(p===f){if(d=i)return;if(l>_){if(s){if(s[1]>=r)return}else s=[d,e];o=[d,r]}else{if(s){if(s[1]1)if(l>_){if(s){if(s[1]>=r)return}else s=[(e-a)/u,e];o=[(r-a)/u,r]}else{if(s){if(s[1]=i)return}else s=[n,u*n+a];o=[i,u*i+a]}else{if(s){if(s[0]Ac||Math.abs(r[0][1]-r[1][1])>Ac)||delete Tc[o]}function zr(t){return Nc[t.index]={site:t,halfedges:[]}}function Or(t,n){var e=t.site,i=n.left,r=n.right;return e===r&&(r=i,i=e),r?Math.atan2(r[1]-i[1],r[0]-i[0]):(e===i?(i=n[1],r=n[0]):(i=n[0],r=n[1]),Math.atan2(i[0]-r[0],r[1]-i[1]))}function jr(t,n){return n[+(n.left!==t.site)]}function Xr(t,n){return n[+(n.left===t.site)]}function Ir(){for(var t,n,e,i,r=0,o=Nc.length;rAc||Math.abs(d-f)>Ac)&&(s.splice(a,0,Tc.push(Rr(u,_,Math.abs(p-t)Ac?[t,Math.abs(l-t)Ac?[Math.abs(f-i)Ac?[e,Math.abs(l-e)Ac?[Math.abs(f-n)=-Ec)){var _=s*s+h*h,p=c*c+l*l,d=(l*_-h*p)/f,y=(s*p-c*_)/f,v=Cc.pop()||new Br;v.arc=t,v.site=r,v.x=d+u,v.y=(v.cy=y+a)+Math.sqrt(d*d+y*y),t.circle=v;for(var g=null,x=kc._;x;)if(v.yAc)a=a.L;else{if(!((r=o-no(a,u))>Ac)){i>-Ac?(n=a.P,e=a):r>-Ac?(n=a,e=a.N):n=e=a;break}if(!a.R){n=a;break}a=a.R}zr(t);var s=Jr(t);if(Mc.insert(n,s),n||e){if(n===e)return Zr(n),e=Jr(n.site),Mc.insert(s,e),s.edge=e.edge=Dr(n.site,s.site),Vr(n),void Vr(e);if(!e)return void(s.edge=Dr(n.site,s.site));Zr(n),Zr(e);var h=n.site,c=h[0],l=h[1],f=t[0]-c,_=t[1]-l,p=e.site,d=p[0]-c,y=p[1]-l,v=2*(f*y-_*d),g=f*f+_*_,x=d*d+y*y,m=[(y*g-_*x)/v+c,(f*x-d*g)/v+l];Fr(e.edge,h,p,m),s.edge=Dr(h,t,null,m),e.edge=Dr(t,p,null,m),Vr(n),Vr(e)}}function to(t,n){var e=t.site,i=e[0],r=e[1],o=r-n;if(!o)return i;var u=t.P;if(!u)return-(1/0);e=u.site;var a=e[0],s=e[1],h=s-n;if(!h)return a;var c=a-i,l=1/o-1/h,f=c/h;return l?(-f+Math.sqrt(f*f-2*l*(c*c/(-2*h)-s+h/2+r-o/2)))/l+i:(i+a)/2}function no(t,n){var e=t.N;if(e)return to(e,n);var i=t.site;return i[1]===n?i[0]:1/0}function eo(t,n,e){return(t[0]-e[0])*(n[1]-t[1])-(t[0]-n[0])*(e[1]-t[1])}function io(t,n){return n[1]-t[1]||n[0]-t[0]}function ro(t,n){var e,i,r,o=t.sort(io).pop();for(Tc=[],Nc=new Array(t.length),Mc=new Ar,kc=new Ar;;)if(r=bc,o&&(!r||o[1]n?1:t>=n?0:NaN},ao=function(t){return 1===t.length&&(t=n(t)),{left:function(n,e,i,r){for(null==i&&(i=0),null==r&&(r=n.length);i>>1;t(n[o],e)<0?i=o+1:r=o}return i},right:function(n,e,i,r){for(null==i&&(i=0),null==r&&(r=n.length);i>>1;t(n[o],e)>0?r=o:i=o+1}return i}}},so=ao(uo),ho=so.right,co=Array.prototype,lo=(co.slice,co.map,function(t,n,e){t=+t,n=+n,e=(r=arguments.length)<2?(n=t,t=0,1):r<3?1:+e;for(var i=-1,r=0|Math.max(0,Math.ceil((n-t)/e)),o=new Array(r);++i=0;)for(i=t[r],n=i.length;--n>=0;)e[--u]=i[n];return e},go=Array.prototype.slice,xo=function(t){return t},mo=1,wo=2,bo=3,Mo=4,No=1e-6,ko={value:function(){}};f.prototype=l.prototype={constructor:f,on:function(t,n){var e,i=this._,r=_(t+"",i),o=-1,u=r.length;{if(!(arguments.length<2)){if(null!=n&&"function"!=typeof n)throw new Error("invalid callback: "+n);for(;++o0)for(var e,i,r=new Array(e),o=0;o=0&&"xmlns"!==(n=t.slice(0,e))&&(t=t.slice(e+1)),Co.hasOwnProperty(n)?{space:Co[n],local:t}:t},Ao=function(t){var n=So(t);return(n.local?v:y)(n)},Eo=function(t){return function(){return this.matches(t)}};if("undefined"!=typeof document){var Uo=document.documentElement;if(!Uo.matches){var Lo=Uo.webkitMatchesSelector||Uo.msMatchesSelector||Uo.mozMatchesSelector||Uo.oMatchesSelector;Eo=function(t){return function(){return Lo.call(this,t)}}}}var Po=Eo,Do={},Ro=null;if("undefined"!=typeof document){"onmouseenter"in document.documentElement||(Do={mouseenter:"mouseover",mouseleave:"mouseout"})}var Fo=function(t,n,e){var i,r,o=m(t+""),u=o.length;{if(!(arguments.length<2)){for(a=n?b:w,null==e&&(e=!1),i=0;i=m&&(m=x+1);!(g=y[m])&&++m=0;)(i=r[o])&&(u&&u!==i.nextSibling&&u.parentNode.insertBefore(i,u),u=i);return this},Jo=function(t){function n(n,e){return n&&e?t(n.__data__,e.__data__):!n-!e}t||(t=S);for(var e=this._groups,i=e.length,r=new Array(i),o=0;o1?this.each((null==n?R:"function"==typeof n?Y:F)(t,n,null==e?"":e)):ru(i=this.node()).getComputedStyle(i,null).getPropertyValue(t)},uu=function(t,n){return arguments.length>1?this.each((null==n?q:"function"==typeof n?z:H)(t,n)):this.node()[t]};X.prototype={add:function(t){this._names.indexOf(t)<0&&(this._names.push(t),this._node.setAttribute("class",this._names.join(" ")))},remove:function(t){var n=this._names.indexOf(t);n>=0&&(this._names.splice(n,1),this._node.setAttribute("class",this._names.join(" ")))},contains:function(t){return this._names.indexOf(t)>=0}};var au=function(t,n){var e=O(t+"");if(arguments.length<2){for(var i=j(this.node()),r=-1,o=e.length;++r=240?t-240:t+120,r,i),Mt(t,r,i),Mt(t<120?t+240:t-120,r,i),this.opacity)},displayable:function(){return(0<=this.s&&this.s<=1||isNaN(this.s))&&0<=this.l&&this.l<=1&&0<=this.opacity&&this.opacity<=1}}));var Lu=Math.PI/180,Pu=180/Math.PI,Du=.95047,Ru=1,Fu=1.08883,Yu=4/29,qu=6/29,Hu=3*qu*qu,zu=qu*qu*qu;xu(Tt,kt,lt(ft,{brighter:function(t){return new Tt(this.l+18*(null==t?1:t),this.a,this.b,this.opacity)},darker:function(t){return new Tt(this.l-18*(null==t?1:t),this.a,this.b,this.opacity)},rgb:function(){var t=(this.l+16)/116,n=isNaN(this.a)?t:t+this.a/500,e=isNaN(this.b)?t:t-this.b/200;return t=Ru*St(t),n=Du*St(n),e=Fu*St(e),new gt(At(3.2404542*n-1.5371385*t-.4985314*e),At(-.969266*n+1.8760108*t+.041556*e),At(.0556434*n-.2040259*t+1.0572252*e),this.opacity)}})),xu(Pt,Lt,lt(ft,{brighter:function(t){return new Pt(this.h,this.c,this.l+18*(null==t?1:t),this.opacity)},darker:function(t){return new Pt(this.h,this.c,this.l-18*(null==t?1:t),this.opacity)},rgb:function(){return Nt(this).rgb()}}));var Ou=-.14861,ju=1.78277,Xu=-.29227,Iu=-.90649,$u=1.97294,Bu=$u*Iu,Vu=$u*ju,Zu=ju*Xu-Iu*Ou;xu(Ft,Rt,lt(ft,{brighter:function(t){return t=null==t?1/.7:Math.pow(1/.7,t),new Ft(this.h,this.s,this.l*t,this.opacity)},darker:function(t){return t=null==t?.7:Math.pow(.7,t),new Ft(this.h,this.s,this.l*t,this.opacity)},rgb:function(){var t=isNaN(this.h)?0:(this.h+120)*Lu,n=+this.l,e=isNaN(this.s)?0:this.s*n*(1-n),i=Math.cos(t),r=Math.sin(t);return new gt(255*(n+e*(Ou*i+ju*r)),255*(n+e*(Xu*i+Iu*r)),255*(n+e*($u*i)),this.opacity)}}));var Wu,Ju,Gu,Qu,Ku=function(t){return function(){return t}},ta=function t(n){function e(t,n){var e=i((t=vt(t)).r,(n=vt(n)).r),r=i(t.g,n.g),o=i(t.b,n.b),u=Ot(t.opacity,n.opacity);return function(n){return t.r=e(n),t.g=r(n),t.b=o(n),t.opacity=u(n),t+""}}var i=zt(n);return e.gamma=t,e}(1),na=function(t,n){var e,i=n?n.length:0,r=t?Math.min(i,t.length):0,o=new Array(i),u=new Array(i);for(e=0;eo&&(r=n.slice(o,r),a[u]?a[u]+=r:a[++u]=r),(e=e[0])===(i=i[0])?a[u]?a[u]+=i:a[++u]=i:(a[++u]=null,s.push({i:u,x:ia(e,i)})),o=ua.lastIndex;return oLa&&e.state1e-6)if(Math.abs(c*a-s*h)>1e-6&&r){var f=e-o,_=i-u,p=a*a+s*s,d=f*f+_*_,y=Math.sqrt(p),v=Math.sqrt(l),g=r*Math.tan((cs-Math.acos((p+l-d)/(2*y*v)))/2),x=g/v,m=g/y;Math.abs(x-1)>1e-6&&(this._+="L"+(t+x*h)+","+(n+x*c)),this._+="A"+r+","+r+",0,0,"+ +(c*f>h*_)+","+(this._x1=t+m*a)+","+(this._y1=n+m*s)}else this._+="L"+(this._x1=t)+","+(this._y1=n);else;},arc:function(t,n,e,i,r,o){t=+t,n=+n,e=+e;var u=e*Math.cos(i),a=e*Math.sin(i),s=t+u,h=n+a,c=1^o,l=o?i-r:r-i;if(e<0)throw new Error("negative radius: "+e);null===this._x1?this._+="M"+s+","+h:(Math.abs(this._x1-s)>1e-6||Math.abs(this._y1-h)>1e-6)&&(this._+="L"+s+","+h),e&&(l>fs?this._+="A"+e+","+e+",0,1,"+c+","+(t-u)+","+(n-a)+"A"+e+","+e+",0,1,"+c+","+(this._x1=s)+","+(this._y1=h):(l<0&&(l=l%ls+ls),this._+="A"+e+","+e+",0,"+ +(l>=cs)+","+c+","+(this._x1=t+e*Math.cos(r))+","+(this._y1=n+e*Math.sin(r))))},rect:function(t,n,e,i){this._+="M"+(this._x0=this._x1=+t)+","+(this._y0=this._y1=+n)+"h"+ +e+"v"+ +i+"h"+-e+"Z"},toString:function(){return this._}};jn.prototype=Xn.prototype={constructor:jn,has:function(t){return"$"+t in this},get:function(t){return this["$"+t]},set:function(t,n){return this["$"+t]=n,this},remove:function(t){var n="$"+t;return n in this&&delete this[n]},clear:function(){for(var t in this)"$"===t[0]&&delete this[t]},keys:function(){var t=[];for(var n in this)"$"===n[0]&&t.push(n.slice(1));return t},values:function(){var t=[];for(var n in this)"$"===n[0]&&t.push(this[n]);return t},entries:function(){var t=[];for(var n in this)"$"===n[0]&&t.push({key:n.slice(1),value:this[n]});return t},size:function(){var t=0;for(var n in this)"$"===n[0]&&++t;return t},empty:function(){for(var t in this)if("$"===t[0])return!1;return!0},each:function(t){for(var n in this)"$"===n[0]&&t(this[n],n.slice(1),this)}};var _s=Xn.prototype;In.prototype=$n.prototype={constructor:In,has:_s.has,add:function(t){return t+="",this["$"+t]=t,this},remove:_s.remove,clear:_s.clear,values:_s.keys,size:_s.size,empty:_s.empty,each:_s.each};var ps=function(t){function n(t,n){var i,r,o=e(t,function(t,e){if(i)return i(t,e-1);r=t,i=n?Vn(t,n):Bn(t)});return o.columns=r,o}function e(t,n){function e(){if(c>=h)return u;if(r)return r=!1,o;var n,e=c;if(34===t.charCodeAt(e)){for(var i=e;i++t||t>r||i>n||n>o))return this;var u,a,s=r-e,h=this._root;switch(a=(n<(i+o)/2)<<1|t<(e+r)/2){case 0:do{u=new Array(4),u[a]=h,h=u}while(s*=2,r=e+s,o=i+s,t>r||n>o);break;case 1:do{u=new Array(4),u[a]=h,h=u}while(s*=2,e=r-s,o=i+s,e>t||n>o);break;case 2:do{u=new Array(4),u[a]=h,h=u}while(s*=2,r=e+s,i=o-s,t>r||i>n);break;case 3:do{u=new Array(4),u[a]=h,h=u}while(s*=2,e=r-s,i=o-s,e>t||i>n)}this._root&&this._root.length&&(this._root=h)}return this._x0=e,this._y0=i,this._x1=r,this._y1=o,this},ws=function(){var t=[];return this.visit(function(n){if(!n.length)do{t.push(n.data)}while(n=n.next)}),t},bs=function(t){return arguments.length?this.cover(+t[0][0],+t[0][1]).cover(+t[1][0],+t[1][1]):isNaN(this._x0)?void 0:[[this._x0,this._y0],[this._x1,this._y1]]},Ms=function(t,n,e,i,r){this.node=t,this.x0=n,this.y0=e,this.x1=i,this.y1=r},Ns=function(t,n,e){var i,r,o,u,a,s,h,c=this._x0,l=this._y0,f=this._x1,_=this._y1,p=[],d=this._root;for(d&&p.push(new Ms(d,c,l,f,_)),null==e?e=1/0:(c=t-e,l=n-e,f=t+e,_=n+e,e*=e);s=p.pop();)if(!(!(d=s.node)||(r=s.x0)>f||(o=s.y0)>_||(u=s.x1)=v)<<1|t>=y)&&(s=p[p.length-1],p[p.length-1]=p[p.length-1-h],p[p.length-1-h]=s)}else{var g=t-+this._x.call(null,d.data),x=n-+this._y.call(null,d.data),m=g*g+x*x;if(m=(a=(p+y)/2))?p=a:y=a,(c=u>=(s=(d+v)/2))?d=s:v=s,n=_,!(_=_[l=c<<1|h]))return this;if(!_.length)break;(n[l+1&3]||n[l+2&3]||n[l+3&3])&&(e=n,f=l)}for(;_.data!==t;)if(i=_,!(_=_.next))return this;return(r=_.next)&&delete _.next,i?(r?i.next=r:delete i.next,this):n?(r?n[l]=r:delete n[l],(_=n[0]||n[1]||n[2]||n[3])&&_===(n[3]||n[2]||n[1]||n[0])&&!_.length&&(e?e[f]=_:this._root=_),this):(this._root=r,this)},Ts=function(){return this._root},Cs=function(){var t=0;return this.visit(function(n){if(!n.length)do{++t}while(n=n.next)}),t},Ss=function(t){var n,e,i,r,o,u,a=[],s=this._root;for(s&&a.push(new Ms(s,this._x0,this._y0,this._x1,this._y1));n=a.pop();)if(!t(s=n.node,i=n.x0,r=n.y0,o=n.x1,u=n.y1)&&s.length){var h=(i+o)/2,c=(r+u)/2;(e=s[3])&&a.push(new Ms(e,h,c,o,u)),(e=s[2])&&a.push(new Ms(e,i,c,h,u)),(e=s[1])&&a.push(new Ms(e,h,r,o,c)),(e=s[0])&&a.push(new Ms(e,i,r,h,c))}return this},As=function(t){var n,e=[],i=[];for(this._root&&e.push(new Ms(this._root,this._x0,this._y0,this._x1,this._y1));n=e.pop();){var r=n.node;if(r.length){var o,u=n.x0,a=n.y0,s=n.x1,h=n.y1,c=(u+s)/2,l=(a+h)/2;(o=r[0])&&e.push(new Ms(o,u,a,c,l)),(o=r[1])&&e.push(new Ms(o,c,a,s,l)),(o=r[2])&&e.push(new Ms(o,u,l,c,h)),(o=r[3])&&e.push(new Ms(o,c,l,s,h))}i.push(n)}for(;n=i.pop();)t(n.node,n.x0,n.y0,n.x1,n.y1);return this},Es=function(t){return arguments.length?(this._x=t,this):this._x},Us=function(t){return arguments.length?(this._y=t,this):this._y},Ls=te.prototype=ne.prototype;Ls.copy=function(){var t,n,e=new ne(this._x,this._y,this._x0,this._y0,this._x1,this._y1),i=this._root;if(!i)return e;if(!i.length)return e._root=ee(i),e;for(t=[{source:i,target:e._root=new Array(4)}];i=t.pop();)for(var r=0;r<4;++r)(n=i.source[r])&&(n.length?t.push({source:n,target:i.target[r]=new Array(4)}):i.target[r]=ee(n));return e},Ls.add=xs,Ls.addAll=Jn,Ls.cover=ms,Ls.data=ws,Ls.extent=bs,Ls.find=Ns,Ls.remove=ks,Ls.removeAll=Gn,Ls.root=Ts,Ls.size=Cs,Ls.visit=Ss,Ls.visitAfter=As,Ls.x=Es,Ls.y=Us;var Ps,Ds=(Math.PI,Math.sqrt(5),function(t,n){if((e=(t=n?t.toExponential(n-1):t.toExponential()).indexOf("e"))<0)return null;var e,i=t.slice(0,e);return[i.length>1?i[0]+i.slice(2):i,+t.slice(e+1)]}),Rs=function(t){return t=Ds(Math.abs(t)),t?t[1]:NaN},Fs=function(t,n){return function(e,i){for(var r=e.length,o=[],u=0,a=t[0],s=0;r>0&&a>0&&(s+a+1>i&&(a=Math.max(1,i-s)),o.push(e.substring(r-=a,r+a)),!((s+=a+1)>i));)a=t[u=(u+1)%t.length];return o.reverse().join(n)}},Ys=function(t,n){t=t.toPrecision(n);t:for(var e,i=t.length,r=1,o=-1;r0&&(o=0)}return o>0?t.slice(0,o)+t.slice(e+1):t},qs=function(t,n){var e=Ds(t,n);if(!e)return t+"";var i=e[0],r=e[1],o=r-(Ps=3*Math.max(-8,Math.min(8,Math.floor(r/3))))+1,u=i.length;return o===u?i:o>u?i+new Array(o-u+1).join("0"):o>0?i.slice(0,o)+"."+i.slice(o):"0."+new Array(1-o).join("0")+Ds(t,Math.max(0,n+o-1))[0]},Hs=function(t,n){var e=Ds(t,n);if(!e)return t+"";var i=e[0],r=e[1];return r<0?"0."+new Array(-r).join("0")+i:i.length>r+1?i.slice(0,r+1)+"."+i.slice(r+1):i+new Array(r-i.length+2).join("0")},zs={"":Ys,"%":function(t,n){return(100*t).toFixed(n)},b:function(t){return Math.round(t).toString(2)},c:function(t){return t+""},d:function(t){return Math.round(t).toString(10)},e:function(t,n){return t.toExponential(n)},f:function(t,n){return t.toFixed(n)},g:function(t,n){return t.toPrecision(n)},o:function(t){return Math.round(t).toString(8)},p:function(t,n){return Hs(100*t,n)},r:Hs,s:qs,X:function(t){return Math.round(t).toString(16).toUpperCase()},x:function(t){return Math.round(t).toString(16)}},Os=/^(?:(.)?([<>=^]))?([+\-\( ])?([$#])?(0)?(\d+)?(,)?(\.\d+)?([a-z%])?$/i,js=function(t){return new ie(t)};ie.prototype.toString=function(){return this.fill+this.align+this.sign+this.symbol+(this.zero?"0":"")+(null==this.width?"":Math.max(1,0|this.width))+(this.comma?",":"")+(null==this.precision?"":"."+Math.max(0,0|this.precision))+this.type};var Xs,Is,$s,Bs=["y","z","a","f","p","n","µ","m","","k","M","G","T","P","E","Z","Y"],Vs=function(t){function n(t){function n(t){var n,r,s,g=p,x=d;if("c"===_)x=y(t)+x,t="";else{t=+t;var m=(t<0||1/t<0)&&(t*=-1,!0);if(t=y(t,f),m)for(n=-1,r=t.length,m=!1;++n(s=t.charCodeAt(n))||s>57){x=(46===s?o+t.slice(n+1):t.slice(n))+x,t=t.slice(0,n);break}}l&&!h&&(t=i(t,1/0));var w=g.length+t.length+x.length,b=w>1)+g+t+x+b.slice(w)}return b+g+t+x}t=js(t);var e=t.fill,u=t.align,a=t.sign,s=t.symbol,h=t.zero,c=t.width,l=t.comma,f=t.precision,_=t.type,p="$"===s?r[0]:"#"===s&&/[boxX]/.test(_)?"0"+_.toLowerCase():"",d="$"===s?r[1]:/[%p]/.test(_)?"%":"",y=zs[_],v=!_||/[defgprs%]/.test(_);return f=null==f?_?6:12:/[gprs]/.test(_)?Math.max(1,Math.min(21,f)):Math.max(0,Math.min(20,f)),n.toString=function(){return t+""},n}function e(t,e){var i=n((t=js(t),t.type="f",t)),r=3*Math.max(-8,Math.min(8,Math.floor(Rs(e)/3))),o=Math.pow(10,-r),u=Bs[8+r/3];return function(t){return i(o*t)+u}}var i=t.grouping&&t.thousands?Fs(t.grouping,t.thousands):re,r=t.currency,o=t.decimal;return{format:n,formatPrefix:e}};!function(t){Xs=Vs(t),Is=Xs.format,$s=Xs.formatPrefix,Xs}({decimal:".",thousands:",",grouping:[3],currency:["$",""]});var Zs=function(t){return Math.max(0,-Rs(Math.abs(t)))},Ws=function(t,n){return Math.max(0,3*Math.max(-8,Math.min(8,Math.floor(Rs(n)/3)))-Rs(Math.abs(t)))},Js=function(t,n){return t=Math.abs(t),n=Math.abs(n)-t,Math.max(0,Rs(n)-Rs(t))+1},Gs=function(){return new oe};oe.prototype={constructor:oe,reset:function(){this.s=this.t=0},add:function(t){ue(Qs,t,this.t),ue(this,Qs.s,this.s),this.s?this.t+=Qs.t:this.s=Qs.t},valueOf:function(){return this.s}};var Qs=new oe,Ks=1e-6,th=Math.PI,nh=th/2,eh=th/4,ih=2*th,rh=th/180,oh=Math.abs,uh=Math.atan,ah=Math.atan2,sh=Math.cos,hh=(Math.ceil,Math.exp),ch=Math.log,lh=(Math.pow,Math.sin),fh=(Math.sign,Math.sqrt),_h=Math.tan;Gs(),Gs(),Gs();_e.invert=_e;var ph=function(){var t,n=[];return{point:function(n,e){t.push([n,e])},lineStart:function(){n.push(t=[])},lineEnd:he,rejoin:function(){n.length>1&&n.push(n.pop().concat(n.shift()))},result:function(){var e=n;return n=[],t=null,e}}},dh=function(t,n){return oh(t[0]-n[0])=0;--o)r.point((c=h[o])[0],c[1]);else i(f.x,f.p.x,-1,r);f=f.p}f=f.o,h=f.z,_=!_}while(!f.v);r.lineEnd()}}},vh=(Gs(),Gs(),Gs(),1/0),gh=-vh;ye.prototype={_radius:4.5,pointRadius:function(t){return this._radius=t,this},polygonStart:function(){this._line=0},polygonEnd:function(){this._line=NaN},lineStart:function(){this._point=0},lineEnd:function(){0===this._line&&this._context.closePath(),this._point=NaN},point:function(t,n){switch(this._point){case 0:this._context.moveTo(t,n),this._point=1;break;case 1:this._context.lineTo(t,n);break;default:this._context.moveTo(t+this._radius,n),this._context.arc(t,n,this._radius,0,ih)}},result:he};Gs();ve.prototype={_circle:ge(4.5),pointRadius:function(t){return this._circle=ge(t),this},polygonStart:function(){this._line=0},polygonEnd:function(){this._line=NaN},lineStart:function(){this._point=0},lineEnd:function(){0===this._line&&this._string.push("Z"),this._point=NaN},point:function(t,n){switch(this._point){case 0:this._string.push("M",t,",",n),this._point=1;break;case 1:this._string.push("L",t,",",n);break;default:this._string.push("M",t,",",n,this._circle)}},result:function(){if(this._string.length){var t=this._string.join("");return this._string=[],t}}};var xh=Gs(),mh=function(t,n){var e=n[0],i=n[1],r=[lh(e),-sh(e),0],o=0,u=0;xh.reset();for(var a=0,s=t.length;a=0?1:-1,N=M*b,k=N>th,T=p*m;if(xh.add(ah(T*M*lh(N),d*w+T*sh(N))),o+=k?b+M*ih:b,k^f>=e^g>=e){var C=le(ce(l),ce(v));fe(C);var S=le(r,C);fe(S);var A=(k^b>=0?-1:1)*se(S[2]);(i>A||i===A&&(C[0]||C[1]))&&(u+=k^b>=0?1:-1)}}return(o<-Ks||o0){for(m||(o.polygonStart(),m=!0),o.lineStart(),t=0;t1&&2&r&&u.push(u.pop().concat(u.shift())),p.push(u.filter(xe))}var _,p,d,y=n(o),v=r.invert(i[0],i[1]),g=ph(),x=n(g),m=!1,w={point:u,lineStart:s,lineEnd:h,polygonStart:function(){w.point=c,w.lineStart=l,w.lineEnd=f,p=[],_=[]},polygonEnd:function(){w.point=u,w.lineStart=s,w.lineEnd=h,p=vo(p);var t=mh(_,v);p.length?(m||(o.polygonStart(),m=!0),yh(p,me,t,e,o)):t&&(m||(o.polygonStart(),m=!0),o.lineStart(),e(null,null,1,o),o.lineEnd()),m&&(o.polygonEnd(),m=!1),p=_=null},sphere:function(){o.polygonStart(),o.lineStart(),e(null,null,1,o),o.lineEnd(),o.polygonEnd()}};return w}};wh(function(){return!0},we,Me,[-th,-nh]);ke.prototype={constructor:ke,point:function(t,n){this.stream.point(t,n)},sphere:function(){this.stream.sphere()},lineStart:function(){this.stream.lineStart()},lineEnd:function(){this.stream.lineEnd()},polygonStart:function(){this.stream.polygonStart()},polygonEnd:function(){this.stream.polygonEnd()}};sh(30*rh),Ne({point:function(t,n){this.stream.point(t*rh,n*rh)}});Te(function(t){return fh(2/(1+t))}).invert=Ce(function(t){return 2*se(t/2)}),Te(function(t){return(t=ae(t))&&t/lh(t)}).invert=Ce(function(t){return t}),Se.invert=function(t,n){return[-n,2*uh(hh(t))-nh]};var bh=function(){return this.eachAfter(Ae)},Mh=function(t){var n,e,i,r,o=this,u=[o];do{for(n=u.reverse(),u=[];o=n.pop();)if(t(o),e=o.children)for(i=0,r=e.length;i=0;--e)r.push(n[e]);return this},kh=function(t){for(var n,e,i,r=this,o=[r],u=[];r=o.pop();)if(u.push(r),n=r.children)for(e=0,i=n.length;e=0;)e+=i[r].value;n.value=e})},Ch=function(t){return this.eachBefore(function(n){n.children&&n.children.sort(t)})},Sh=function(t){for(var n=this,e=Ee(n,t),i=[n];n!==e;)n=n.parent,i.push(n);for(var r=i.length;t!==e;)i.splice(r,0,t),t=t.parent;return i},Ah=function(){for(var t=this,n=[t];t=t.parent;)n.push(t);return n},Eh=function(){var t=[];return this.each(function(n){t.push(n)}),t},Uh=function(){var t=[];return this.eachBefore(function(n){n.children||t.push(n)}),t},Lh=function(){var t=this,n=[];return t.each(function(e){e!==t&&n.push({source:e.parent,target:e})}),n};Fe.prototype=Ue.prototype={constructor:Fe,count:bh,each:Mh,eachAfter:kh,eachBefore:Nh,sum:Th,sort:Ch,path:Sh,ancestors:Ah,descendants:Eh,leaves:Uh,links:Lh,copy:Le};var Ph=function(t,n,e,i,r){for(var o,u=t.children,a=-1,s=u.length,h=t.value&&(i-n)/t.value;++a1?n:1)},e}(Rh);!function t(n){function e(t,e,i,r,o){if((u=t._squarify)&&u.ratio===n)for(var u,a,s,h,c,l=-1,f=u.length,_=t.value;++l1?n:1)},e}(Rh);var Fh=([].slice,{}),Yh=function(t,n){function e(t){var n,e=h.status;if(!e&&$e(h)||e>=200&&e<300||304===e){if(o)try{n=o.call(i,h)}catch(t){return void a.call("error",i,t)}else n=h;a.call("load",i,n)}else a.call("error",i,t)}var i,r,o,u,a=l("beforesend","progress","load","error"),s=Xn(),h=new XMLHttpRequest,c=null,f=null,_=0;if("undefined"==typeof XDomainRequest||"withCredentials"in h||!/^(http(s)?:)?\/\//.test(t)||(h=new XDomainRequest),"onload"in h?h.onload=h.onerror=h.ontimeout=e:h.onreadystatechange=function(t){h.readyState>3&&e(t)},h.onprogress=function(t){a.call("progress",i,t)},i={header:function(t,n){return t=(t+"").toLowerCase(),arguments.length<2?s.get(t):(null==n?s.remove(t):s.set(t,n+""),i)},mimeType:function(t){return arguments.length?(r=null==t?null:t+"",i):r},responseType:function(t){return arguments.length?(u=t,i):u},timeout:function(t){return arguments.length?(_=+t,i):_},user:function(t){return arguments.length<1?c:(c=null==t?null:t+"",i)},password:function(t){return arguments.length<1?f:(f=null==t?null:t+"",i)},response:function(t){return o=t,i},get:function(t,n){return i.send("GET",t,n)},post:function(t,n){return i.send("POST",t,n)},send:function(n,e,o){return h.open(n,t,!0,c,f),null==r||s.has("accept")||s.set("accept",r+",*/*"),h.setRequestHeader&&s.each(function(t,n){h.setRequestHeader(n,t)}),null!=r&&h.overrideMimeType&&h.overrideMimeType(r),null!=u&&(h.responseType=u),_>0&&(h.timeout=_),null==o&&"function"==typeof e&&(o=e,e=null),null!=o&&1===o.length&&(o=Ie(o)),null!=o&&i.on("error",o).on("load",function(t){o(null,t)}),a.call("beforesend",i,h),h.send(null==e?null:e),i},abort:function(){return h.abort(),i},on:function(){var t=a.on.apply(a,arguments);return t===a?i:t}},null!=n){if("function"!=typeof n)throw new Error("invalid callback: "+n);return i.get(n)}return i},qh=function(t,n){return function(e,i){var r=Yh(e).mimeType(t).response(n);if(null!=i){if("function"!=typeof i)throw new Error("invalid callback: "+i);return r.get(i)}return r}};qh("text/html",function(t){return document.createRange().createContextualFragment(t.responseText)}),qh("application/json",function(t){return JSON.parse(t.responseText)}),qh("text/plain",function(t){return t.responseText}),qh("application/xml",function(t){var n=t.responseXML;if(!n)throw new Error("parse error");return n});var Hh=function(t,n){return function(e,i,r){arguments.length<3&&(r=i,i=null);var o=Yh(e).mimeType(t);return o.row=function(t){return arguments.length?o.response(Be(n,i=t)):i},o.row(i),r?o.get(r):o}};Hh("text/csv",ys),Hh("text/tab-separated-values",gs);var zh=Array.prototype,Oh=zh.map,jh=zh.slice,Xh={name:"implicit"},Ih=function(t){return function(){return t}},$h=function(t){return+t},Bh=[0,1],Vh=function(t,n,i){var r,o=t[0],u=t[t.length-1],a=e(o,u,null==n?10:n);switch(i=js(null==i?",f":i),i.type){case"s":var s=Math.max(Math.abs(o),Math.abs(u));return null!=i.precision||isNaN(r=Ws(a,s))||(i.precision=r),$s(i,s);case"":case"e":case"g":case"p":case"r":null!=i.precision||isNaN(r=Js(a,Math.max(Math.abs(o),Math.abs(u))))||(i.precision=r-("e"===i.type));break;case"f":case"%":null!=i.precision||isNaN(r=Zs(a))||(i.precision=r-2*("%"===i.type))}return Is(i)},Zh=new Date,Wh=new Date,Jh=ri(function(){},function(t,n){t.setTime(+t+n)},function(t,n){return n-t});Jh.every=function(t){return t=Math.floor(t),isFinite(t)&&t>0?t>1?ri(function(n){n.setTime(Math.floor(n/t)*t)},function(n,e){n.setTime(+n+e*t)},function(n,e){return(e-n)/t}):Jh:null};var Gh=6e4,Qh=6048e5,Kh=(ri(function(t){t.setTime(1e3*Math.floor(t/1e3))},function(t,n){t.setTime(+t+1e3*n)},function(t,n){return(n-t)/1e3},function(t){return t.getUTCSeconds()}),ri(function(t){t.setTime(Math.floor(t/Gh)*Gh)},function(t,n){t.setTime(+t+n*Gh)},function(t,n){return(n-t)/Gh},function(t){return t.getMinutes()}),ri(function(t){var n=t.getTimezoneOffset()*Gh%36e5;n<0&&(n+=36e5),t.setTime(36e5*Math.floor((+t-n)/36e5)+n)},function(t,n){t.setTime(+t+36e5*n)},function(t,n){return(n-t)/36e5},function(t){return t.getHours()}),ri(function(t){t.setHours(0,0,0,0)},function(t,n){t.setDate(t.getDate()+n)},function(t,n){return(n-t-(n.getTimezoneOffset()-t.getTimezoneOffset())*Gh)/864e5},function(t){return t.getDate()-1})),tc=oi(0),nc=oi(1),ec=(oi(2),oi(3),oi(4),oi(5),oi(6),ri(function(t){t.setDate(1),t.setHours(0,0,0,0)},function(t,n){t.setMonth(t.getMonth()+n)},function(t,n){return n.getMonth()-t.getMonth()+12*(n.getFullYear()-t.getFullYear())},function(t){return t.getMonth()}),ri(function(t){t.setMonth(0,1),t.setHours(0,0,0,0)},function(t,n){t.setFullYear(t.getFullYear()+n)},function(t,n){return n.getFullYear()-t.getFullYear()},function(t){return t.getFullYear()}));ec.every=function(t){return isFinite(t=Math.floor(t))&&t>0?ri(function(n){n.setFullYear(Math.floor(n.getFullYear()/t)*t),n.setMonth(0,1),n.setHours(0,0,0,0)},function(n,e){n.setFullYear(n.getFullYear()+e*t)}):null};var ic=(ri(function(t){t.setUTCSeconds(0,0)},function(t,n){t.setTime(+t+n*Gh)},function(t,n){return(n-t)/Gh},function(t){return t.getUTCMinutes()}),ri(function(t){t.setUTCMinutes(0,0,0)},function(t,n){t.setTime(+t+36e5*n)},function(t,n){return(n-t)/36e5},function(t){return t.getUTCHours()}),ri(function(t){t.setUTCHours(0,0,0,0)},function(t,n){t.setUTCDate(t.getUTCDate()+n)},function(t,n){return(n-t)/864e5},function(t){return t.getUTCDate()-1})),rc=ui(0),oc=ui(1),uc=(ui(2),ui(3),ui(4),ui(5),ui(6),ri(function(t){t.setUTCDate(1),t.setUTCHours(0,0,0,0)},function(t,n){t.setUTCMonth(t.getUTCMonth()+n)},function(t,n){return n.getUTCMonth()-t.getUTCMonth()+12*(n.getUTCFullYear()-t.getUTCFullYear())},function(t){return t.getUTCMonth()}),ri(function(t){t.setUTCMonth(0,1),t.setUTCHours(0,0,0,0)},function(t,n){t.setUTCFullYear(t.getUTCFullYear()+n)},function(t,n){return n.getUTCFullYear()-t.getUTCFullYear()},function(t){return t.getUTCFullYear()}));uc.every=function(t){return isFinite(t=Math.floor(t))&&t>0?ri(function(n){n.setUTCFullYear(Math.floor(n.getUTCFullYear()/t)*t),n.setUTCMonth(0,1),n.setUTCHours(0,0,0,0)},function(n,e){n.setUTCFullYear(n.getUTCFullYear()+e*t)}):null};var ac,sc,hc,cc,lc,fc={"-":"",_:" ",0:"0"},_c=/^\s*\d+/,pc=/^%/,dc=/[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g;!function(t){ac=ci(t),sc=ac.format,hc=ac.parse,cc=ac.utcFormat,lc=ac.utcParse,ac}({dateTime:"%x, %X",date:"%-m/%-d/%Y",time:"%-I:%M:%S %p",periods:["AM","PM"],days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],shortDays:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],shortMonths:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]});var yc=(Date.prototype.toISOString||cc("%Y-%m-%dT%H:%M:%S.%LZ"),+new Date("2000-01-01T00:00:00.000Z")||lc("%Y-%m-%dT%H:%M:%S.%LZ"),function(t){return t.match(/.{6}/g).map(function(t){return"#"+t})});yc("1f77b4ff7f0e2ca02cd627289467bd8c564be377c27f7f7fbcbd2217becf"),yc("393b795254a36b6ecf9c9ede6379398ca252b5cf6bcedb9c8c6d31bd9e39e7ba52e7cb94843c39ad494ad6616be7969c7b4173a55194ce6dbdde9ed6"),yc("3182bd6baed69ecae1c6dbefe6550dfd8d3cfdae6bfdd0a231a35474c476a1d99bc7e9c0756bb19e9ac8bcbddcdadaeb636363969696bdbdbdd9d9d9"),yc("1f77b4aec7e8ff7f0effbb782ca02c98df8ad62728ff98969467bdc5b0d58c564bc49c94e377c2f7b6d27f7f7fc7c7c7bcbd22dbdb8d17becf9edae5"),va(Rt(300,.5,0),Rt(-240,.5,1));var vc=(va(Rt(-100,.75,.35),Rt(80,1.5,.8)),va(Rt(260,.75,.35),Rt(80,1.5,.8)),Rt(),function(t){return function(){return t}}),gc=1e-12;Math.PI;ur.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._point=0},lineEnd:function(){(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;default:this._context.lineTo(t,n)}}};var xc=function(t){return new ur(t)},mc=function(){function t(t){var a,s,h,c=t.length,l=!1;for(null==r&&(u=o(h=On())),a=0;a<=c;++a)!(a0)for(var i,r=t[0],o=n[0],u=t[e]-r,a=n[e]-o,s=-1;++s<=e;)i=s/e,this._basis.point(this._beta*t[s]+(1-this._beta)*(r+i*u),this._beta*n[s]+(1-this._beta)*(o+i*a));this._x=this._y=null,this._basis.lineEnd()},point:function(t,n){this._x.push(+t),this._y.push(+n)}},function t(n){function e(t){return 1===n?new fr(t):new _r(t,n)}return e.beta=function(n){return t(+n)},e}(.85),dr.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x2,this._y2);break;case 3:pr(this,this._x1,this._y1)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2,this._x1=t,this._y1=n;break;case 2:this._point=3;default:pr(this,t,n)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}},function t(n){function e(t){return new dr(t,n)}return e.tension=function(n){return t(+n)},e}(0),yr.prototype={areaStart:wc,areaEnd:wc,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._x5=this._y0=this._y1=this._y2=this._y3=this._y4=this._y5=NaN,this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x3,this._y3),this._context.closePath();break;case 2:this._context.lineTo(this._x3,this._y3),this._context.closePath();break;case 3:this.point(this._x3,this._y3),this.point(this._x4,this._y4),this.point(this._x5,this._y5)}},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._x3=t,this._y3=n;break;case 1:this._point=2,this._context.moveTo(this._x4=t,this._y4=n);break;case 2:this._point=3,this._x5=t,this._y5=n;break;default:pr(this,t,n)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}},function t(n){function e(t){return new yr(t,n)}return e.tension=function(n){return t(+n)},e}(0),vr.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3,this._line?this._context.lineTo(this._x2,this._y2):this._context.moveTo(this._x2,this._y2);break;case 3:this._point=4;default:pr(this,t,n)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}},function t(n){function e(t){return new vr(t,n)}return e.tension=function(n){return t(+n)},e}(0),xr.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x2,this._y2);break;case 3:this.point(this._x2,this._y2)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){if(t=+t,n=+n,this._point){var e=this._x2-t,i=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(e*e+i*i,this._alpha))}switch(this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;break;case 2:this._point=3;default:gr(this,t,n)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}},function t(n){function e(t){return n?new xr(t,n):new dr(t,0)}return e.alpha=function(n){return t(+n)},e}(.5),mr.prototype={areaStart:wc,areaEnd:wc,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._x5=this._y0=this._y1=this._y2=this._y3=this._y4=this._y5=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x3,this._y3),this._context.closePath();break;case 2:this._context.lineTo(this._x3,this._y3),this._context.closePath();break;case 3:this.point(this._x3,this._y3),this.point(this._x4,this._y4),this.point(this._x5,this._y5)}},point:function(t,n){if(t=+t,n=+n,this._point){var e=this._x2-t,i=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(e*e+i*i,this._alpha))}switch(this._point){case 0:this._point=1,this._x3=t,this._y3=n;break;case 1:this._point=2,this._context.moveTo(this._x4=t,this._y4=n);break;case 2:this._point=3,this._x5=t,this._y5=n;break;default:gr(this,t,n)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}},function t(n){function e(t){return n?new mr(t,n):new yr(t,0)}return e.alpha=function(n){return t(+n)},e}(.5),wr.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){if(t=+t,n=+n,this._point){var e=this._x2-t,i=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(e*e+i*i,this._alpha))}switch(this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3,this._line?this._context.lineTo(this._x2,this._y2):this._context.moveTo(this._x2,this._y2);break;case 3:this._point=4;default:gr(this,t,n)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}},function t(n){function e(t){return n?new wr(t,n):new vr(t,0)}return e.alpha=function(n){return t(+n)},e}(.5),Tr.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=this._t0=NaN,this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x1,this._y1);break;case 3:kr(this,this._t0,Nr(this,this._t0))}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){var e=NaN;if(t=+t,n=+n,t!==this._x1||n!==this._y1){switch(this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;break;case 2:this._point=3,kr(this,Nr(this,e=Mr(this,t,n)),e);break;default:kr(this,this._t0,e=Mr(this,t,n))}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=n,this._t0=e}}},(Cr.prototype=Object.create(Tr.prototype)).point=function(t,n){Tr.prototype.point.call(this,n,t)},Sr.prototype={moveTo:function(t,n){this._context.moveTo(n,t)},closePath:function(){this._context.closePath()},lineTo:function(t,n){this._context.lineTo(n,t)},bezierCurveTo:function(t,n,e,i,r,o){this._context.bezierCurveTo(n,t,i,e,o,r)}};Array.prototype.slice;Ar.prototype={constructor:Ar,insert:function(t,n){var e,i,r;if(t){if(n.P=t,n.N=t.N,t.N&&(t.N.P=n),t.N=n,t.R){for(t=t.R;t.L;)t=t.L;t.L=n}else t.R=n;e=t}else this._?(t=Pr(this._),n.P=null,n.N=t,t.P=t.L=n,e=t):(n.P=n.N=null,this._=n,e=null);for(n.L=n.R=null,n.U=e,n.C=!0,t=n;e&&e.C;)i=e.U,e===i.L?(r=i.R,r&&r.C?(e.C=r.C=!1,i.C=!0,t=i):(t===e.R&&(Ur(this,e),t=e,e=t.U),e.C=!1,i.C=!0,Lr(this,i))):(r=i.L,r&&r.C?(e.C=r.C=!1,i.C=!0,t=i):(t===e.L&&(Lr(this,e),t=e,e=t.U),e.C=!1,i.C=!0,Ur(this,i))),e=t.U;this._.C=!1},remove:function(t){t.N&&(t.N.P=t.P),t.P&&(t.P.N=t.N),t.N=t.P=null;var n,e,i,r=t.U,o=t.L,u=t.R;if(e=o?u?Pr(u):o:u,r?r.L===t?r.L=e:r.R=e:this._=e,o&&u?(i=e.C,e.C=t.C,e.L=o,o.U=e,e!==u?(r=e.U,e.U=t.U,t=e.R,r.L=t,e.R=u,u.U=e):(e.U=r,r=e,t=e.R)):(i=t.C,t=e),t&&(t.U=r),!i){if(t&&t.C)return void(t.C=!1);do{if(t===this._)break;if(t===r.L){if(n=r.R,n.C&&(n.C=!1,r.C=!0,Ur(this,r),n=r.R),n.L&&n.L.C||n.R&&n.R.C){n.R&&n.R.C||(n.L.C=!1,n.C=!0,Lr(this,n),n=r.R),n.C=r.C,r.C=n.R.C=!1,Ur(this,r),t=this._;break}}else if(n=r.L,n.C&&(n.C=!1,r.C=!0,Lr(this,r),n=r.L),n.L&&n.L.C||n.R&&n.R.C){n.L&&n.L.C||(n.R.C=!1,n.C=!0,Ur(this,n),n=r.L),n.C=r.C,r.C=n.L.C=!1,Lr(this,r),t=this._;break}n.C=!0,t=r,r=r.U}while(!t.C);t&&(t.C=!1)}}};var bc,Mc,Nc,kc,Tc,Cc=[],Sc=[],Ac=1e-6,Ec=1e-12;ro.prototype={constructor:ro,polygons:function(){var t=this.edges;return this.cells.map(function(n){var e=n.halfedges.map(function(e){return jr(n,t[e])});return e.data=n.site.data,e})},triangles:function(){var t=[],n=this.edges;return this.cells.forEach(function(e,i){if(o=(r=e.halfedges).length)for(var r,o,u,a=e.site,s=-1,h=n[r[o-1]],c=h.left===a?h.right:h.left;++s=a)return null;var s=t-r.site[0],h=n-r.site[1],c=s*s+h*h;do{r=o.cells[i=u],u=null,r.halfedges.forEach(function(e){var i=o.edges[e],a=i.left;if(a!==r.site&&a||(a=i.right)){var s=t-a[0],h=n-a[1],l=s*s+h*h;l{this.state.language.translate(term)}, event.currentTarget.getBoundingClientRect(), diff --git a/src/app/components/AvailableModulesMenu.jsx b/src/app/components/AvailableModulesMenu.jsx index a1e93235..1d3df29c 100644 --- a/src/app/components/AvailableModulesMenu.jsx +++ b/src/app/components/AvailableModulesMenu.jsx @@ -1,4 +1,5 @@ import React from 'react'; +import * as ModuleUtils from '../shipyard/ModuleUtils'; import { findDOMNode } from 'react-dom'; import TranslatedComponent from './TranslatedComponent'; import { stopCtxPropagation } from '../utils/UtilityFunctions'; @@ -41,6 +42,8 @@ const GRPCAT = { 'mr': 'ordnance', 'tp': 'ordnance', 'nl': 'ordnance', + 'sc': 'scanners', + 'ss': 'scanners', // Utilities 'cs': 'scanners', 'kw': 'scanners', @@ -60,7 +63,6 @@ const CATEGORIES = { 'limpet controllers': ['cc', 'fx', 'hb', 'pc'], 'passenger cabins': ['pce', 'pci', 'pcm', 'pcq'], 'rf': ['rf'], - 'sc': ['sc'], 'shields': ['sg', 'bsg', 'psg', 'scb'], 'structural reinforcement': ['hr', 'mrp'], 'dc': ['dc'], @@ -72,7 +74,7 @@ const CATEGORIES = { 'sb': ['sb'], 'hs': ['hs'], 'defence': ['ch', 'po', 'ec'], - 'scanners': ['cs', 'kw', 'ws'], + 'scanners': ['sc', 'ss', 'cs', 'kw', 'ws'], // Overloaded with internal scanners }; /** @@ -212,7 +214,14 @@ export default class AvailableModulesMenu extends TranslatedComponent { for (let i = 0; i < sortedModules.length; i++) { let m = sortedModules[i]; let mount = null; - let disabled = m.maxmass && (mass + (m.mass ? m.mass : 0)) > m.maxmass; + let disabled = false; + if (ModuleUtils.isShieldGenerator(m.grp)) { + // Shield generators care about maximum hull mass + disabled = mass > m.maxmass; + } else if (m.maxmass) { + // Thrusters care about total mass + disabled = mass + m.mass > m.maxmass; + } let active = mountedModule && mountedModule.id === m.id; let classes = cn(m.name ? 'lc' : 'c', { warning: !disabled && warningFunc && warningFunc(m), diff --git a/src/app/components/BarChart.jsx b/src/app/components/BarChart.jsx index 8ab59851..801e9f88 100644 --- a/src/app/components/BarChart.jsx +++ b/src/app/components/BarChart.jsx @@ -44,7 +44,7 @@ export default class BarChart extends TranslatedComponent { unit: '' }; - static PropTypes = { + static propTypes = { colors: React.PropTypes.array, data: React.PropTypes.array.isRequired, desc: React.PropTypes.bool, diff --git a/src/app/components/Boost.jsx b/src/app/components/Boost.jsx new file mode 100644 index 00000000..b6803138 --- /dev/null +++ b/src/app/components/Boost.jsx @@ -0,0 +1,89 @@ +import React from 'react'; +import TranslatedComponent from './TranslatedComponent'; +import { Ships } from 'coriolis-data/dist'; +import { nameComparator } from '../utils/SlotFunctions'; +import { Pip } from './SvgIcons'; +import LineChart from '../components/LineChart'; +import Slider from '../components/Slider'; +import * as ModuleUtils from '../shipyard/ModuleUtils'; +import Module from '../shipyard/Module'; + +/** + * Boost displays a boost button that toggles bosot + * Requires an onChange() function of the form onChange(boost) which is triggered whenever the boost changes. + */ +export default class Boost extends TranslatedComponent { + static propTypes = { + marker: React.PropTypes.string.isRequired, + ship: React.PropTypes.object.isRequired, + boost: React.PropTypes.bool.isRequired, + onChange: React.PropTypes.func.isRequired + }; + + /** + * Constructor + * @param {Object} props React Component properties + * @param {Object} context React Component context + */ + constructor(props, context) { + super(props); + const { ship, boost } = props; + + this._keyDown = this._keyDown.bind(this); + this._toggleBoost = this._toggleBoost.bind(this); + } + + /** + * Add listeners after mounting + */ + componentDidMount() { + document.addEventListener('keydown', this._keyDown); + } + + /** + * Remove listeners before unmounting + */ + componentWillUnmount() { + document.removeEventListener('keydown', this._keyDown); + } + + /** + * Handle Key Down + * @param {Event} e Keyboard Event + */ + _keyDown(e) { + if (e.ctrlKey || e.metaKey) { // CTRL/CMD + switch (e.keyCode) { + case 66: // b == boost + if (this.props.ship.canBoost()) { + e.preventDefault(); + this._toggleBoost(); + } + break; + } + } + } + + /** + * Toggle the boost feature + */ + _toggleBoost() { + this.props.onChange(!this.props.boost); + } + + /** + * Render boost + * @return {React.Component} contents + */ + render() { + const { formats, translate, units } = this.context.language; + const { ship, boost } = this.props; + + // TODO disable if ship cannot boost + return ( + + + + ); + } +} diff --git a/src/app/components/Cargo.jsx b/src/app/components/Cargo.jsx new file mode 100644 index 00000000..96bbac83 --- /dev/null +++ b/src/app/components/Cargo.jsx @@ -0,0 +1,75 @@ +import React from 'react'; +import TranslatedComponent from './TranslatedComponent'; +import { Ships } from 'coriolis-data/dist'; +import Slider from '../components/Slider'; + +/** + * Cargo slider + * Requires an onChange() function of the form onChange(cargo), providing the cargo in tonnes, which is triggered on cargo level change + */ +export default class Cargo extends TranslatedComponent { + static propTypes = { + cargo: React.PropTypes.number.isRequired, + cargoCapacity: React.PropTypes.number.isRequired, + onChange: React.PropTypes.func.isRequired + }; + + /** + * Constructor + * @param {Object} props React Component properties + * @param {Object} context React Component context + */ + constructor(props, context) { + super(props); + + this._cargoChange = this._cargoChange.bind(this); + } + + /** + * Update cargo level + * @param {number} cargoLevel percentage level from 0 to 1 + */ + _cargoChange(cargoLevel) { + const { cargo, cargoCapacity } = this.props; + if (cargoCapacity > 0) { + // We round the cargo to whole number of tonnes + const newCargo = Math.round(cargoLevel * cargoCapacity); + if (newCargo != cargo) { + this.props.onChange(newCargo); + } + } + } + + /** + * Render cargo slider + * @return {React.Component} contents + */ + render() { + const { language, onWindowResize, sizeRatio, tooltip, termtip } = this.context; + const { formats, translate, units } = language; + const { cargo, cargoCapacity } = this.props; + + return ( + +

{translate('cargo carried')}: {formats.int(cargo)}{units.T}

+ + + + + + +
+ +
+
+ ); + } +} diff --git a/src/app/components/CostSection.jsx b/src/app/components/CostSection.jsx index 2df44855..302dd5c8 100644 --- a/src/app/components/CostSection.jsx +++ b/src/app/components/CostSection.jsx @@ -12,7 +12,7 @@ import TranslatedComponent from './TranslatedComponent'; */ export default class CostSection extends TranslatedComponent { - static PropTypes = { + static propTypes = { ship: React.PropTypes.object.isRequired, code: React.PropTypes.string.isRequired, buildName: React.PropTypes.string @@ -507,7 +507,7 @@ export default class CostSection extends TranslatedComponent { scoop = true; break; case 'scb': - q = slotGroup[i].m.getCells(); + q = slotGroup[i].m.getAmmo() + 1; break; case 'am': q = slotGroup[i].m.getAmmo(); diff --git a/src/app/components/DamageDealt.jsx b/src/app/components/DamageDealt.jsx deleted file mode 100644 index bb4bd484..00000000 --- a/src/app/components/DamageDealt.jsx +++ /dev/null @@ -1,592 +0,0 @@ -import React from 'react'; -import TranslatedComponent from './TranslatedComponent'; -import { Ships } from 'coriolis-data/dist'; -import ShipSelector from './ShipSelector'; -import { nameComparator } from '../utils/SlotFunctions'; -import { CollapseSection, ExpandSection, MountFixed, MountGimballed, MountTurret } from './SvgIcons'; -import LineChart from '../components/LineChart'; -import Slider from '../components/Slider'; -import * as ModuleUtils from '../shipyard/ModuleUtils'; -import Module from '../shipyard/Module'; - -const DAMAGE_DEALT_COLORS = ['#FFFFFF', '#FF0000', '#00FF00', '#7777FF', '#FFFF00', '#FF00FF', '#00FFFF', '#777777']; - -/** - * Generates an internationalization friendly weapon comparator that will - * sort by specified property (if provided) then by name/group, class, rating - * @param {function} translate Translation function - * @param {function} propComparator Optional property comparator - * @param {boolean} desc Use descending order - * @return {function} Comparator function for names - */ -export function weaponComparator(translate, propComparator, desc) { - return (a, b) => { - if (!desc) { // Flip A and B if ascending order - let t = a; - a = b; - b = t; - } - - // If a property comparator is provided use it first - let diff = propComparator ? propComparator(a, b) : nameComparator(translate, a, b); - - if (diff) { - return diff; - } - - // Property matches so sort by name / group, then class, rating - if (a.name === b.name && a.grp === b.grp) { - if(a.class == b.class) { - return a.rating > b.rating ? 1 : -1; - } - return a.class - b.class; - } - - return nameComparator(translate, a, b); - }; -} - -/** - * Damage against a selected ship - */ -export default class DamageDealt extends TranslatedComponent { - static PropTypes = { - ship: React.PropTypes.object.isRequired, - chartWidth: React.PropTypes.number.isRequired, - code: React.PropTypes.string.isRequired - }; - - static DEFAULT_AGAINST = Ships['anaconda']; - - /** - * Constructor - * @param {Object} props React Component properties - * @param {Object} context React Component context - */ - constructor(props, context) { - super(props); - - this._sort = this._sort.bind(this); - this._onShipChange = this._onShipChange.bind(this); - this._onCollapseExpand = this._onCollapseExpand.bind(this); - - const ship = this.props.ship; - const against = DamageDealt.DEFAULT_AGAINST; - const maxRange = this._calcMaxRange(ship); - const range = 1000 / maxRange; - const maxDps = this._calcMaxSDps(ship, against); - const weaponNames = this._weaponNames(ship, context); - - this.state = { - predicate: 'n', - desc: true, - against, - expanded: false, - range, - maxRange, - maxDps, - weaponNames, - calcHullDpsFunc: this._calcDps.bind(this, context, ship, weaponNames, against, true), - calcShieldsDpsFunc: this._calcDps.bind(this, context, ship, weaponNames, against, false) - }; - } - - /** - * Set the initial weapons state - */ - componentWillMount() { - const data = this._calcWeaponsDps(this.props.ship, this.state.against, this.state.range * this.state.maxRange, true); - this.setState({ weapons: data.weapons, totals: data.totals }); - } - - /** - * Set the updated weapons state if our ship changes - * @param {Object} nextProps Incoming/Next properties - * @param {Object} nextContext Incoming/Next conext - * @return {boolean} Returns true if the component should be rerendered - */ - componentWillReceiveProps(nextProps, nextContext) { - if (nextProps.code != this.props.code) { - const data = this._calcWeaponsDps(nextProps.ship, this.state.against, this.state.range * this.state.maxRange, this.props.hull); - const weaponNames = this._weaponNames(nextProps.ship, nextContext); - const maxRange = this._calcMaxRange(nextProps.ship); - const maxDps = this._calcMaxSDps(nextProps.ship, this.state.against); - this.setState({ weapons: data.weapons, - totals: data.totals, - weaponNames, - maxRange, - maxDps, - calcHullDpsFunc: this._calcDps.bind(this, nextContext, nextProps.ship, weaponNames, this.state.against, true), - calcShieldsDpsFunc: this._calcDps.bind(this, nextContext, nextProps.ship, weaponNames, this.state.against, false) }); - } - return true; - } - - /** - * Calculate the maximum sustained single-weapon DPS for this ship against another ship - * @param {Object} ship The ship - * @param {Object} against The target - * @return {number} The maximum sustained single-weapon DPS - */ - _calcMaxSDps(ship, against) { - let maxSDps = 0; - for (let i = 0; i < ship.hardpoints.length; i++) { - if (ship.hardpoints[i].m && ship.hardpoints[i].enabled) { - const m = ship.hardpoints[i].m; - const thisSDps = m.getClip() ? (m.getClip() * m.getDps() / m.getRoF()) / ((m.getClip() / m.getRoF()) + m.getReload()) : m.getDps(); - if (thisSDps > maxSDps) { - maxSDps = thisSDps; - } - } - } - return maxSDps; - } - - /** - * Calculate the per-weapon DPS for this ship against another ship at a given range - * @param {Object} context The context - * @param {Object} ship The ship - * @param {Object} weaponNames The names of the weapons for which to calculate DPS - * @param {Object} against The target - * @param {bool} hull true if to calculate against hull, false if to calculate against shields - * @param {Object} range The engagement range - * @return {array} The array of weapon DPS - */ - _calcDps(context, ship, weaponNames, against, hull, range) { - let results = {}; - let weaponNum = 0; - for (let i = 0; i < ship.hardpoints.length; i++) { - if (ship.hardpoints[i].m && ship.hardpoints[i].enabled) { - const m = ship.hardpoints[i].m; - results[weaponNames[weaponNum++]] = this._calcWeaponDps(context, m, against, hull, range); - } - } - return results; - } - - /** - * Calculate the maximum range of a ship's weapons - * @param {Object} ship The ship - * @returns {int} The maximum range, in metres - */ - _calcMaxRange(ship) { - let maxRange = 1000; - for (let i = 0; i < ship.hardpoints.length; i++) { - if (ship.hardpoints[i].maxClass > 0 && ship.hardpoints[i].m && ship.hardpoints[i].enabled) { - const thisRange = ship.hardpoints[i].m.getRange(); - if (thisRange > maxRange) { - maxRange = thisRange; - } - } - } - - return maxRange; - } - - /** - * Obtain the weapon names for this ship - * @param {Object} ship The ship - * @param {Object} context The context - * @return {array} The weapon names - */ - _weaponNames(ship, context) { - const translate = context.language.translate; - let names = []; - let num = 1; - for (let i = 0; i < ship.hardpoints.length; i++) { - if (ship.hardpoints[i].maxClass > 0 && ship.hardpoints[i].m && ship.hardpoints[i].enabled) { - const m = ship.hardpoints[i].m; - let name = '' + num++ + ': ' + m.class + m.rating + (m.missile ? '/' + m.missile : '') + ' ' + translate(m.name || m.grp); - let engineering; - if (m.blueprint && m.blueprint.name) { - engineering = translate(m.blueprint.name) + ' ' + translate('grade') + ' ' + m.blueprint.grade; - if (m.blueprint.special && m.blueprint.special.id) { - engineering += ', ' + translate(m.blueprint.special.name); - } - } - if (engineering) { - name = name + ' (' + engineering + ')'; - } - names.push(name); - } - } - return names; - } - - - /** - * Calculate a specific weapon DPS for this ship against another ship at a given range - * @param {Object} context The context - * @param {Object} m The weapon - * @param {Object} against The target - * @param {bool} hull true if to calculate against hull, false if to calculate against shields - * @param {Object} range The engagement range - * @return {number} The weapon DPS - */ - _calcWeaponDps(context, m, against, hull, range) { - const translate = context.language.translate; - let dropoff = 1; - if (m.getFalloff()) { - // Calculate the dropoff % due to range - if (range > m.getRange()) { - // Weapon is out of range - dropoff = 0; - } else { - const falloff = m.getFalloff(); - if (range > falloff) { - const dropoffRange = m.getRange() - falloff; - // Assuming straight-line falloff - dropoff = 1 - (range - falloff) / dropoffRange; - } - } - } - const classRating = `${m.class}${m.rating}${m.missile ? '/' + m.missile : ''}`; - let engineering; - if (m.blueprint && m.blueprint.name) { - engineering = translate(m.blueprint.name) + ' ' + translate('grade') + ' ' + m.blueprint.grade; - if (m.blueprint.special && m.blueprint.special.id) { - engineering += ', ' + translate(m.blueprint.special.name); - } - } - const effectivenessShields = dropoff; - const effectiveDpsShields = m.getDps() * effectivenessShields; - const effectiveSDpsShields = (m.getClip() ? (m.getClip() * m.getDps() / m.getRoF()) / ((m.getClip() / m.getRoF()) + m.getReload()) * effectivenessShields : effectiveDpsShields); - const effectivenessHull = (m.getPiercing() >= against.properties.hardness ? 1 : m.getPiercing() / against.properties.hardness) * dropoff; - const effectiveDpsHull = m.getDps() * effectivenessHull; - const effectiveSDpsHull = (m.getClip() ? (m.getClip() * m.getDps() / m.getRoF()) / ((m.getClip() / m.getRoF()) + m.getReload()) * effectivenessHull : effectiveDpsHull); - - return hull ? effectiveSDpsHull : effectiveSDpsShields; - } - - /** - * Calculate the damage dealt by a ship - * @param {Object} ship The ship which will deal the damage - * @param {Object} against The ship against which damage will be dealt - * @param {Object} range The engagement range - * @return {object} Returns the per-weapon damage - */ - _calcWeaponsDps(ship, against, range) { - const translate = this.context.language.translate; - - // Tidy up the range so that it's to 4 decimal places - range = Math.round(10000 * range) / 10000; - - // Track totals - let totals = {}; - totals.effectivenessShields = 0; - totals.effectiveDpsShields = 0; - totals.effectiveSDpsShields = 0; - totals.effectivenessHull = 0; - totals.effectiveDpsHull = 0; - totals.effectiveSDpsHull = 0; - let totalDps = 0; - - let weapons = []; - for (let i = 0; i < ship.hardpoints.length; i++) { - if (ship.hardpoints[i].maxClass > 0 && ship.hardpoints[i].m && ship.hardpoints[i].enabled) { - const m = ship.hardpoints[i].m; - if (m.getDamage() && m.grp !== 'po') { - let dropoff = 1; - if (m.getFalloff()) { - // Calculate the dropoff % due to range - if (range > m.getRange()) { - // Weapon is out of range - dropoff = 0; - } else { - const falloff = m.getFalloff(); - if (range > falloff) { - const dropoffRange = m.getRange() - falloff; - // Assuming straight-line falloff - dropoff = 1 - (range - falloff) / dropoffRange; - } - } - } - const classRating = `${m.class}${m.rating}${m.missile ? '/' + m.missile : ''}`; - let engineering; - if (m.blueprint && m.blueprint.name) { - engineering = translate(m.blueprint.name) + ' ' + translate('grade') + ' ' + m.blueprint.grade; - if (m.blueprint.special && m.blueprint.special.id >= 0) { - engineering += ', ' + translate(m.blueprint.special.name); - } - } - - // Alter effectiveness as per standard shields (all have the same resistances) - const sg = ModuleUtils.findModule('sg', '3v'); - let effectivenessShields = 0; - if (m.getDamageDist().E) { - effectivenessShields += m.getDamageDist().E * (1 - sg.getExplosiveResistance()); - } - if (m.getDamageDist().K) { - effectivenessShields += m.getDamageDist().K * (1 - sg.getKineticResistance()); - } - if (m.getDamageDist().T) { - effectivenessShields += m.getDamageDist().T * (1 - sg.getThermalResistance()); - } - if (m.getDamageDist().A) { - effectivenessShields += m.getDamageDist().A; - } - effectivenessShields *= dropoff; - const effectiveDpsShields = m.getDps() * effectivenessShields; - const effectiveSDpsShields = (m.getClip() ? (m.getClip() * m.getDps() / m.getRoF()) / ((m.getClip() / m.getRoF()) + m.getReload()) * effectivenessShields : effectiveDpsShields); - - // Alter effectiveness as per standard hull - const bulkheads = new Module({ template: against.bulkheads }); - let effectivenessHull = 0; - if (m.getDamageDist().E) { - effectivenessHull += m.getDamageDist().E * (1 - bulkheads.getExplosiveResistance()); - } - if (m.getDamageDist().K) { - effectivenessHull += m.getDamageDist().K * (1 - bulkheads.getKineticResistance()); - } - if (m.getDamageDist().T) { - effectivenessHull += m.getDamageDist().T * (1 - bulkheads.getThermalResistance()); - } - if (m.getDamageDist().A) { - effectivenessHull += m.getDamageDist().A; - } - effectivenessHull *= Math.min(m.getPiercing() / against.properties.hardness, 1) * dropoff; - const effectiveDpsHull = m.getDps() * effectivenessHull; - const effectiveSDpsHull = (m.getClip() ? (m.getClip() * m.getDps() / m.getRoF()) / ((m.getClip() / m.getRoF()) + m.getReload()) * effectivenessHull : effectiveDpsHull); - totals.effectiveDpsShields += effectiveDpsShields; - totals.effectiveSDpsShields += effectiveSDpsShields; - totals.effectiveDpsHull += effectiveDpsHull; - totals.effectiveSDpsHull += effectiveSDpsHull; - totalDps += m.getDps(); - - weapons.push({ id: i, - mount: m.mount, - name: m.name || m.grp, - classRating, - engineering, - effectiveDpsShields, - effectiveSDpsShields, - effectivenessShields, - effectiveDpsHull, - effectiveSDpsHull, - effectivenessHull }); - } - } - } - totals.effectivenessShields = totalDps == 0 ? 0 : totals.effectiveDpsShields / totalDps; - totals.effectivenessHull = totalDps == 0 ? 0 : totals.effectiveDpsHull / totalDps; - - return { weapons, totals }; - } - - /** - * Triggered when the collapse or expand icons are clicked - */ - _onCollapseExpand() { - this.setState({ expanded: !this.state.expanded }); - } - - /** - * Triggered when the ship we compare against changes - * @param {string} s the new ship ID - */ - _onShipChange(s) { - const against = Ships[s]; - const data = this._calcWeaponsDps(this.props.ship, against, this.state.range * this.state.maxRange); - this.setState({ against, - weapons: data.weapons, - totals: data.totals, - calcHullDpsFunc: this._calcDps.bind(this, this.context, this.props.ship, this.state.weaponNames, against, true), - calcShieldsDpsFunc: this._calcDps.bind(this, this.context, this.props.ship, this.state.weaponNames, against, false) }); - } - - /** - * Set the sort order and sort - * @param {string} predicate Sort predicate - */ - _sortOrder(predicate) { - let desc = this.state.desc; - - if (predicate == this.state.predicate) { - desc = !desc; - } else { - desc = true; - } - - this._sort(this.props.ship, predicate, desc); - this.setState({ predicate, desc }); - } - - /** - * Sorts the weapon list - * @param {Ship} ship Ship instance - * @param {string} predicate Sort predicate - * @param {Boolean} desc Sort order descending - */ - _sort(ship, predicate, desc) { - let comp = weaponComparator.bind(null, this.context.language.translate); - - switch (predicate) { - case 'n': comp = comp(null, desc); break; - case 'edpss': comp = comp((a, b) => a.effectiveDpsShields - b.effectiveDpsShields, desc); break; - case 'esdpss': comp = comp((a, b) => a.effectiveSDpsShields - b.effectiveSDpsShields, desc); break; - case 'es': comp = comp((a, b) => a.effectivenessShields - b.effectivenessShields, desc); break; - case 'edpsh': comp = comp((a, b) => a.effectiveDpsHull - b.effectiveDpsHull, desc); break; - case 'esdpsh': comp = comp((a, b) => a.effectiveSDpsHull - b.effectiveSDpsHull, desc); break; - case 'eh': comp = comp((a, b) => a.effectivenessHull - b.effectivenessHull, desc); break; - } - - this.state.weapons.sort(comp); - } - - /** - * Render individual rows for hardpoints - * @param {Function} translate Translate function - * @param {Object} formats Localised formats map - * @return {array} The individual rows - * - */ - _renderRows(translate, formats) { - const { termtip, tooltip } = this.context; - - let rows = []; - - if (this.state.weapons) { - for (let i = 0; i < this.state.weapons.length; i++) { - const weapon = this.state.weapons[i]; - - rows.push( - - {weapon.mount == 'F' ? : null} - {weapon.mount == 'G' ? : null} - {weapon.mount == 'T' ? : null} - {weapon.classRating} {translate(weapon.name)} - {weapon.engineering ? ' (' + weapon.engineering + ')' : null } - - {formats.f1(weapon.effectiveDpsShields)} - {formats.f1(weapon.effectiveSDpsShields)} - {formats.pct(weapon.effectivenessShields)} - {formats.f1(weapon.effectiveDpsHull)} - {formats.f1(weapon.effectiveSDpsHull)} - {formats.pct(weapon.effectivenessHull)} - ); - } - } - - return rows; - } - - /** - * Update current range - * @param {number} range Range 0-1 - */ - _rangeChange(range) { - const data = this._calcWeaponsDps(this.props.ship, this.state.against, this.state.range * this.state.maxRange); - this.setState({ range, - weapons: data.weapons, - totals: data.totals }); - } - - /** - * Render damage dealt - * @return {React.Component} contents - */ - render() { - const { language, onWindowResize, sizeRatio, tooltip, termtip } = this.context; - const { formats, translate, units } = language; - const { against, expanded, maxRange, range, totals } = this.state; - const { ship } = this.props; - - const sortOrder = this._sortOrder; - const onCollapseExpand = this._onCollapseExpand; - - const code = ship.getHardpointsString() + '.' + ship.getModificationsString() + '.' + ship.getPowerEnabledString() + '.' + against.properties.name; - - return ( - -

{translate('damage dealt to')} {expanded ? : }

- {expanded ? - - - - - - - - - - - - - - - - - - - {this._renderRows(translate, formats)} - - - - - - - - - - - - -
{translate('weapon')}{translate('standard shields')}{translate('standard armour')}
{translate('effective dps')}{translate('effective sdps')}{translate('effectiveness')}{translate('effective dps')}{translate('effective sdps')}{translate('effectiveness')}
{translate('total')}{formats.f1(totals.effectiveDpsShields)}{formats.f1(totals.effectiveSDpsShields)}{formats.pct(totals.effectivenessShields)}{formats.f1(totals.effectiveDpsHull)}{formats.f1(totals.effectiveSDpsHull)}{formats.pct(totals.effectivenessHull)}
- - - - - - - - -
{translate('engagement range')} - - - {formats.f2(range * maxRange / 1000)}{units.km} -
-
-

{translate('sustained dps against standard shields')}

- -
-
-

{translate('sustained dps against standard armour')}

- -
-
: null } -
- ); - } -} diff --git a/src/app/components/DamageReceived.jsx b/src/app/components/DamageReceived.jsx deleted file mode 100644 index 294e4fbc..00000000 --- a/src/app/components/DamageReceived.jsx +++ /dev/null @@ -1,327 +0,0 @@ -import React from 'react'; -import TranslatedComponent from './TranslatedComponent'; -import { Modules } from 'coriolis-data/dist'; -import { nameComparator } from '../utils/SlotFunctions'; -import { CollapseSection, ExpandSection, MountFixed, MountGimballed, MountTurret } from './SvgIcons'; -import Module from '../shipyard/Module'; -import Slider from '../components/Slider'; - -/** - * Generates an internationalization friendly weapon comparator that will - * sort by specified property (if provided) then by name/group, class, rating - * @param {function} translate Translation function - * @param {function} propComparator Optional property comparator - * @param {boolean} desc Use descending order - * @return {function} Comparator function for names - */ -export function weaponComparator(translate, propComparator, desc) { - return (a, b) => { - if (!desc) { // Flip A and B if ascending order - let t = a; - a = b; - b = t; - } - - // If a property comparator is provided use it first - let diff = propComparator ? propComparator(a, b) : nameComparator(translate, a, b); - - if (diff) { - return diff; - } - - // Property matches so sort by name / group, then class, rating - if (a.name === b.name && a.grp === b.grp) { - if(a.class == b.class) { - return a.rating > b.rating ? 1 : -1; - } - return a.class - b.class; - } - - return nameComparator(translate, a, b); - }; -} - -/** - * Damage received by a selected ship - */ -export default class DamageReceived extends TranslatedComponent { - static PropTypes = { - ship: React.PropTypes.object.isRequired, - code: React.PropTypes.string.isRequired - }; - - /** - * Constructor - * @param {Object} props React Component properties - */ - constructor(props) { - super(props); - - this._sort = this._sort.bind(this); - this._onCollapseExpand = this._onCollapseExpand.bind(this); - - this.state = { - predicate: 'n', - desc: true, - expanded: false, - range: 0.1667, - maxRange: 6000 - }; - } - - /** - * Set the initial weapons state - */ - componentWillMount() { - this.setState({ weapons: this._calcWeapons(this.props.ship, this.state.range * this.state.maxRange) }); - } - - /** - * Set the updated weapons state - * @param {Object} nextProps Incoming/Next properties - * @param {Object} nextContext Incoming/Next conext - * @return {boolean} Returns true if the component should be rerendered - */ - componentWillReceiveProps(nextProps, nextContext) { - if (nextProps.code != this.props.code) { - this.setState({ weapons: this._calcWeapons(nextProps.ship, this.state.range * this.state.maxRange) }); - } - return true; - } - - /** - * Calculate the damage received by a ship - * @param {Object} ship The ship which will receive the damage - * @param {Object} range The engagement range - * @return {boolean} Returns the per-weapon damage - */ - _calcWeapons(ship, range) { - // Tidy up the range so that it's to 4 decimal places - range = Math.round(10000 * range) / 10000; - - let weapons = []; - for (let grp in Modules.hardpoints) { - if (Modules.hardpoints[grp][0].damage && Modules.hardpoints[grp][0].damagedist) { - for (let mId in Modules.hardpoints[grp]) { - const m = new Module(Modules.hardpoints[grp][mId]); - let dropoff = 1; - if (m.getFalloff()) { - // Calculate the dropoff % due to range - if (range > m.getRange()) { - // Weapon is out of range - dropoff = 0; - } else { - const falloff = m.getFalloff(); - if (range > falloff) { - const dropoffRange = m.getRange() - falloff; - // Assuming straight-line falloff - dropoff = 1 - (range - falloff) / dropoffRange; - } - } - } - const classRating = `${m.class}${m.rating}${m.missile ? '/' + m.missile : ''}`; - - // Base DPS - const baseDps = m.getDps() * dropoff; - const baseSDps = m.getClip() ? ((m.getClip() * baseDps / m.getRoF()) / ((m.getClip() / m.getRoF()) + m.getReload())) * dropoff : baseDps; - - // Effective DPS taking in to account shield resistance - let effectivenessShields = 0; - if (m.getDamageDist().E) { - effectivenessShields += m.getDamageDist().E * (1 - ship.shieldExplRes); - } - if (m.getDamageDist().K) { - effectivenessShields += m.getDamageDist().K * (1 - ship.shieldKinRes); - } - if (m.getDamageDist().T) { - effectivenessShields += m.getDamageDist().T * (1 - ship.shieldThermRes); - } - if (m.getDamageDist().A) { - effectivenessShields += m.getDamageDist().A; - } - effectivenessShields *= dropoff; - const effectiveDpsShields = baseDps * effectivenessShields; - const effectiveSDpsShields = baseSDps * effectivenessShields; - - // Effective DPS taking in to account hull hardness and resistance - let effectivenessHull = 0; - if (m.getDamageDist().E) { - effectivenessHull += m.getDamageDist().E * (1 - ship.hullExplRes); - } - if (m.getDamageDist().K) { - effectivenessHull += m.getDamageDist().K * (1 - ship.hullKinRes); - } - if (m.getDamageDist().T) { - effectivenessHull += m.getDamageDist().T * (1 - ship.hullThermRes); - } - if (m.getDamageDist().A) { - effectivenessHull += m.getDamageDist().A; - } - effectivenessHull *= Math.min(m.getPiercing() / ship.hardness, 1) * dropoff; - const effectiveDpsHull = baseDps * effectivenessHull; - const effectiveSDpsHull = baseSDps * effectivenessHull; - - weapons.push({ id: m.id, - classRating, - name: m.name || m.grp, - mount: m.mount, - effectiveDpsShields, - effectiveSDpsShields, - effectivenessShields, - effectiveDpsHull, - effectiveSDpsHull, - effectivenessHull }); - } - } - } - - return weapons; - } - - /** - * Triggered when the collapse or expand icons are clicked - */ - _onCollapseExpand() { - this.setState({ expanded: !this.state.expanded }); - } - - /** - * Set the sort order and sort - * @param {string} predicate Sort predicate - */ - _sortOrder(predicate) { - let desc = this.state.desc; - - if (predicate == this.state.predicate) { - desc = !desc; - } else { - desc = true; - } - - this._sort(this.props.ship, predicate, desc); - this.setState({ predicate, desc }); - } - - /** - * Sorts the weapon list - * @param {Ship} ship Ship instance - * @param {string} predicate Sort predicate - * @param {Boolean} desc Sort order descending - */ - _sort(ship, predicate, desc) { - let comp = weaponComparator.bind(null, this.context.language.translate); - - switch (predicate) { - case 'n': comp = comp(null, desc); break; - case 'edpss': comp = comp((a, b) => a.effectiveDpsShields - b.effectiveDpsShields, desc); break; - case 'esdpss': comp = comp((a, b) => a.effectiveSDpsShields - b.effectiveSDpsShields, desc); break; - case 'es': comp = comp((a, b) => a.effectivenessShields - b.effectivenessShields, desc); break; - case 'edpsh': comp = comp((a, b) => a.effectiveDpsHull - b.effectiveDpsHull, desc); break; - case 'esdpsh': comp = comp((a, b) => a.effectiveSDpsHull - b.effectiveSDpsHull, desc); break; - case 'eh': comp = comp((a, b) => a.effectivenessHull - b.effectivenessHull, desc); break; - } - - this.state.weapons.sort(comp); - } - - /** - * Render individual rows for weapons - * @param {Function} translate Translate function - * @param {Object} formats Localised formats map - * @return {array} The individual rows - * - */ - _renderRows(translate, formats) { - const { termtip, tooltip } = this.context; - - let rows = []; - - for (let i = 0; i < this.state.weapons.length; i++) { - const weapon = this.state.weapons[i]; - rows.push( - - {weapon.mount == 'F' ? : null} - {weapon.mount == 'G' ? : null} - {weapon.mount == 'T' ? : null} - {weapon.classRating} {translate(weapon.name)} - - {formats.round1(weapon.effectiveDpsShields)} - {formats.round1(weapon.effectiveSDpsShields)} - {formats.pct(weapon.effectivenessShields)} - {formats.round1(weapon.effectiveDpsHull)} - {formats.round1(weapon.effectiveSDpsHull)} - {formats.pct(weapon.effectivenessHull)} - ); - } - return rows; - } - - /** - * Update current range - * @param {number} range Range 0-1 - */ - _rangeChange(range) { - this.setState({ range, weapons: this._calcWeapons(this.props.ship, this.state.range * this.state.maxRange) }); - } - - /** - * Render damage received - * @return {React.Component} contents - */ - render() { - const { language, onWindowResize, sizeRatio, tooltip, termtip } = this.context; - const { formats, translate, units } = language; - const { expanded, maxRange, range } = this.state; - - const sortOrder = this._sortOrder; - const onCollapseExpand = this._onCollapseExpand; - - return ( - -

{translate('damage received from')} {expanded ? : }

- {expanded ? - - - - - - - - - - - - - - - - - - {this._renderRows(translate, formats)} - -
{translate('weapon')}{translate('against shields')}{translate('against hull')}
{translate('DPS')}{translate('SDPS')}{translate('effectiveness')}{translate('DPS')}{translate('SDPS')}{translate('effectiveness')}
- - - - - - - - -
{translate('engagement range')} - - - {formats.f2(range * maxRange / 1000)}{units.km} -
: null } -
- ); - } -} diff --git a/src/app/components/Defence.jsx b/src/app/components/Defence.jsx new file mode 100644 index 00000000..545e6eac --- /dev/null +++ b/src/app/components/Defence.jsx @@ -0,0 +1,261 @@ +import React from 'react'; +import TranslatedComponent from './TranslatedComponent'; +import * as Calc from '../shipyard/Calculations'; +import PieChart from './PieChart'; +import VerticalBarChart from './VerticalBarChart'; + +/** + * Defence information + * Shield information consists of four panels: + * - textual information (time to lose shields etc.) + * - breakdown of shield sources (pie chart) + * - comparison of shield resistances (bar chart) + * - effective shield (bar chart) + */ +export default class Defence extends TranslatedComponent { + static propTypes = { + marker: React.PropTypes.string.isRequired, + ship: React.PropTypes.object.isRequired, + opponent: React.PropTypes.object.isRequired, + engagementrange: React.PropTypes.number.isRequired, + sys: React.PropTypes.number.isRequired, + opponentWep: React.PropTypes.number.isRequired + }; + + /** + * Constructor + * @param {Object} props React Component properties + */ + constructor(props) { + super(props); + + const { shield, armour, shielddamage, armourdamage } = Calc.defenceMetrics(props.ship, props.opponent, props.sys, props.opponentWep, props.engagementrange); + this.state = { shield, armour, shielddamage, armourdamage }; + } + + /** + * Update the state if our properties change + * @param {Object} nextProps Incoming/Next properties + * @return {boolean} Returns true if the component should be rerendered + */ + componentWillReceiveProps(nextProps) { + if (this.props.marker != nextProps.marker || this.props.sys != nextProps.sys) { + const { shield, armour, shielddamage, armourdamage } = Calc.defenceMetrics(nextProps.ship, nextProps.opponent, nextProps.sys, nextProps.opponentWep, nextProps.engagementrange); + this.setState({ shield, armour, shielddamage, armourdamage }); + } + return true; + } + + /** + * Render defence + * @return {React.Component} contents + */ + render() { + const { ship, sys, opponentWep } = this.props; + const { language, tooltip, termtip } = this.context; + const { formats, translate, units } = language; + const { shield, armour, shielddamage, armourdamage } = this.state; + + const pd = ship.standard[4].m; + + const shieldSourcesData = []; + const effectiveShieldData = []; + const shieldDamageTakenData = []; + const shieldSourcesTt = []; + const shieldDamageTakenAbsoluteTt = []; + const shieldDamageTakenExplosiveTt = []; + const shieldDamageTakenKineticTt = []; + const shieldDamageTakenThermalTt = []; + const effectiveShieldAbsoluteTt = []; + const effectiveShieldExplosiveTt = []; + const effectiveShieldKineticTt = []; + const effectiveShieldThermalTt = []; + let maxEffectiveShield = 0; + if (shield.total) { + shieldSourcesData.push({ value: Math.round(shield.generator), label: translate('generator') }); + shieldSourcesData.push({ value: Math.round(shield.boosters), label: translate('boosters') }); + shieldSourcesData.push({ value: Math.round(shield.cells), label: translate('cells') }); + + if (shield.generator > 0) { + shieldSourcesTt.push(
{translate('generator') + ' ' + formats.int(shield.generator)}{units.MJ}
); + effectiveShieldAbsoluteTt.push(
{translate('generator') + ' ' + formats.int(shield.generator)}{units.MJ}
); + effectiveShieldExplosiveTt.push(
{translate('generator') + ' ' + formats.int(shield.generator)}{units.MJ}
); + effectiveShieldKineticTt.push(
{translate('generator') + ' ' + formats.int(shield.generator)}{units.MJ}
); + effectiveShieldThermalTt.push(
{translate('generator') + ' ' + formats.int(shield.generator)}{units.MJ}
); + if (shield.boosters > 0) { + shieldSourcesTt.push(
{translate('boosters') + ' ' + formats.int(shield.boosters)}{units.MJ}
); + effectiveShieldAbsoluteTt.push(
{translate('boosters') + ' ' + formats.int(shield.boosters)}{units.MJ}
); + effectiveShieldExplosiveTt.push(
{translate('boosters') + ' ' + formats.int(shield.boosters)}{units.MJ}
); + effectiveShieldKineticTt.push(
{translate('boosters') + ' ' + formats.int(shield.boosters)}{units.MJ}
); + effectiveShieldThermalTt.push(
{translate('boosters') + ' ' + formats.int(shield.boosters)}{units.MJ}
); + } + + if (shield.cells > 0) { + shieldSourcesTt.push(
{translate('cells') + ' ' + formats.int(shield.cells)}{units.MJ}
); + effectiveShieldAbsoluteTt.push(
{translate('cells') + ' ' + formats.int(shield.cells)}{units.MJ}
); + effectiveShieldExplosiveTt.push(
{translate('cells') + ' ' + formats.int(shield.cells)}{units.MJ}
); + effectiveShieldKineticTt.push(
{translate('cells') + ' ' + formats.int(shield.cells)}{units.MJ}
); + effectiveShieldThermalTt.push(
{translate('cells') + ' ' + formats.int(shield.cells)}{units.MJ}
); + } + + // Add effective shield from resistances + const rawMj = shield.generator + shield.boosters + shield.cells; + const explosiveMj = rawMj / (shield.explosive.generator * shield.explosive.boosters) - rawMj; + if (explosiveMj != 0) effectiveShieldExplosiveTt.push(
{translate('resistance') + ' ' + formats.int(explosiveMj)}{units.MJ}
); + const kineticMj = rawMj / (shield.kinetic.generator * shield.kinetic.boosters) - rawMj; + if (kineticMj != 0) effectiveShieldKineticTt.push(
{translate('resistance') + ' ' + formats.int(kineticMj)}{units.MJ}
); + const thermalMj = rawMj / (shield.thermal.generator * shield.thermal.boosters) - rawMj; + if (thermalMj != 0) effectiveShieldThermalTt.push(
{translate('resistance') + ' ' + formats.int(thermalMj)}{units.MJ}
); + + // Add effective shield from power distributor SYS pips + if (shield.absolute.sys != 1) { + effectiveShieldAbsoluteTt.push(
{translate('power distributor') + ' ' + formats.int(rawMj / shield.absolute.sys - rawMj)}{units.MJ}
); + effectiveShieldExplosiveTt.push(
{translate('power distributor') + ' ' + formats.int(rawMj / shield.explosive.sys - rawMj)}{units.MJ}
); + effectiveShieldKineticTt.push(
{translate('power distributor') + ' ' + formats.int(rawMj / shield.kinetic.sys - rawMj)}{units.MJ}
); + effectiveShieldThermalTt.push(
{translate('power distributor') + ' ' + formats.int(rawMj / shield.thermal.sys - rawMj)}{units.MJ}
); + } + } + + shieldDamageTakenAbsoluteTt.push(
{translate('generator') + ' ' + formats.pct1(shield.absolute.generator)}
); + shieldDamageTakenAbsoluteTt.push(
{translate('boosters') + ' ' + formats.pct1(shield.absolute.boosters)}
); + shieldDamageTakenAbsoluteTt.push(
{translate('power distributor') + ' ' + formats.pct1(shield.absolute.sys)}
); + + shieldDamageTakenExplosiveTt.push(
{translate('generator') + ' ' + formats.pct1(shield.explosive.generator)}
); + shieldDamageTakenExplosiveTt.push(
{translate('boosters') + ' ' + formats.pct1(shield.explosive.boosters)}
); + shieldDamageTakenExplosiveTt.push(
{translate('power distributor') + ' ' + formats.pct1(shield.explosive.sys)}
); + + shieldDamageTakenKineticTt.push(
{translate('generator') + ' ' + formats.pct1(shield.kinetic.generator)}
); + shieldDamageTakenKineticTt.push(
{translate('boosters') + ' ' + formats.pct1(shield.kinetic.boosters)}
); + shieldDamageTakenKineticTt.push(
{translate('power distributor') + ' ' + formats.pct1(shield.kinetic.sys)}
); + + shieldDamageTakenThermalTt.push(
{translate('generator') + ' ' + formats.pct1(shield.thermal.generator)}
); + shieldDamageTakenThermalTt.push(
{translate('boosters') + ' ' + formats.pct1(shield.thermal.boosters)}
); + shieldDamageTakenThermalTt.push(
{translate('power distributor') + ' ' + formats.pct1(shield.thermal.sys)}
); + + const effectiveAbsoluteShield = shield.total / shield.absolute.total; + effectiveShieldData.push({ value: Math.round(effectiveAbsoluteShield), label: translate('absolute'), tooltip: effectiveShieldAbsoluteTt }); + const effectiveExplosiveShield = shield.total / shield.explosive.total; + effectiveShieldData.push({ value: Math.round(effectiveExplosiveShield), label: translate('explosive'), tooltip: effectiveShieldExplosiveTt }); + const effectiveKineticShield = shield.total / shield.kinetic.total; + effectiveShieldData.push({ value: Math.round(effectiveKineticShield), label: translate('kinetic'), tooltip: effectiveShieldKineticTt }); + const effectiveThermalShield = shield.total / shield.thermal.total; + effectiveShieldData.push({ value: Math.round(effectiveThermalShield), label: translate('thermal'), tooltip: effectiveShieldThermalTt }); + + shieldDamageTakenData.push({ value: Math.round(shield.absolute.total * 100), label: translate('absolute'), tooltip: shieldDamageTakenAbsoluteTt }); + shieldDamageTakenData.push({ value: Math.round(shield.explosive.total * 100), label: translate('explosive'), tooltip: shieldDamageTakenExplosiveTt }); + shieldDamageTakenData.push({ value: Math.round(shield.kinetic.total * 100), label: translate('kinetic'), tooltip: shieldDamageTakenKineticTt }); + shieldDamageTakenData.push({ value: Math.round(shield.thermal.total * 100), label: translate('thermal'), tooltip: shieldDamageTakenThermalTt }); + + maxEffectiveShield = Math.max(shield.total / shield.absolute.max, shield.total / shield.explosive.max, shield.total / shield.kinetic.max, shield.total / shield.thermal.max); + } + + const armourSourcesData = []; + armourSourcesData.push({ value: Math.round(armour.bulkheads), label: translate('bulkheads') }); + armourSourcesData.push({ value: Math.round(armour.reinforcement), label: translate('reinforcement') }); + + const armourSourcesTt = []; + const effectiveArmourAbsoluteTt = []; + const effectiveArmourExplosiveTt = []; + const effectiveArmourKineticTt = []; + const effectiveArmourThermalTt = []; + if (armour.bulkheads > 0) { + armourSourcesTt.push(
{translate('bulkheads') + ' ' + formats.int(armour.bulkheads)}
); + effectiveArmourAbsoluteTt.push(
{translate('bulkheads') + ' ' + formats.int(armour.bulkheads)}
); + effectiveArmourExplosiveTt.push(
{translate('bulkheads') + ' ' + formats.int(armour.bulkheads)}
); + effectiveArmourKineticTt.push(
{translate('bulkheads') + ' ' + formats.int(armour.bulkheads)}
); + effectiveArmourThermalTt.push(
{translate('bulkheads') + ' ' + formats.int(armour.bulkheads)}
); + if (armour.reinforcement > 0) { + armourSourcesTt.push(
{translate('reinforcement') + ' ' + formats.int(armour.reinforcement)}
); + effectiveArmourAbsoluteTt.push(
{translate('reinforcement') + ' ' + formats.int(armour.reinforcement)}
); + effectiveArmourExplosiveTt.push(
{translate('reinforcement') + ' ' + formats.int(armour.reinforcement)}
); + effectiveArmourKineticTt.push(
{translate('reinforcement') + ' ' + formats.int(armour.reinforcement)}
); + effectiveArmourThermalTt.push(
{translate('reinforcement') + ' ' + formats.int(armour.reinforcement)}
); + } + } + + const rawArmour = armour.bulkheads + armour.reinforcement; + + const armourDamageTakenTt = []; + armourDamageTakenTt.push(
{translate('bulkheads') + ' ' + formats.pct1(armour.absolute.bulkheads)}
); + armourDamageTakenTt.push(
{translate('reinforcement') + ' ' + formats.pct1(armour.absolute.reinforcement)}
); + + const armourDamageTakenExplosiveTt = []; + armourDamageTakenExplosiveTt.push(
{translate('bulkheads') + ' ' + formats.pct1(armour.explosive.bulkheads)}
); + armourDamageTakenExplosiveTt.push(
{translate('reinforcement') + ' ' + formats.pct1(armour.explosive.reinforcement)}
); + if (armour.explosive.bulkheads * armour.explosive.reinforcement != 1) effectiveArmourExplosiveTt.push(
{translate('resistance') + ' ' + formats.int(rawArmour / (armour.explosive.bulkheads * armour.explosive.reinforcement) - rawArmour)}
); + + const armourDamageTakenKineticTt = []; + armourDamageTakenKineticTt.push(
{translate('bulkheads') + ' ' + formats.pct1(armour.kinetic.bulkheads)}
); + armourDamageTakenKineticTt.push(
{translate('reinforcement') + ' ' + formats.pct1(armour.kinetic.reinforcement)}
); + if (armour.kinetic.bulkheads * armour.kinetic.reinforcement != 1) effectiveArmourKineticTt.push(
{translate('resistance') + ' ' + formats.int(rawArmour / (armour.kinetic.bulkheads * armour.kinetic.reinforcement) - rawArmour)}
); + + const armourDamageTakenThermalTt = []; + armourDamageTakenThermalTt.push(
{translate('bulkheads') + ' ' + formats.pct1(armour.thermal.bulkheads)}
); + armourDamageTakenThermalTt.push(
{translate('reinforcement') + ' ' + formats.pct1(armour.thermal.reinforcement)}
); + if (armour.thermal.bulkheads * armour.thermal.reinforcement != 1) effectiveArmourThermalTt.push(
{translate('resistance') + ' ' + formats.int(rawArmour / (armour.thermal.bulkheads * armour.thermal.reinforcement) - rawArmour)}
); + + const effectiveArmourData = []; + const effectiveAbsoluteArmour = armour.total / armour.absolute.total; + effectiveArmourData.push({ value: Math.round(effectiveAbsoluteArmour), label: translate('absolute'), tooltip: effectiveArmourAbsoluteTt }); + const effectiveExplosiveArmour = armour.total / armour.explosive.total; + effectiveArmourData.push({ value: Math.round(effectiveExplosiveArmour), label: translate('explosive'), tooltip: effectiveArmourExplosiveTt }); + const effectiveKineticArmour = armour.total / armour.kinetic.total; + effectiveArmourData.push({ value: Math.round(effectiveKineticArmour), label: translate('kinetic'), tooltip: effectiveArmourKineticTt }); + const effectiveThermalArmour = armour.total / armour.thermal.total; + effectiveArmourData.push({ value: Math.round(effectiveThermalArmour), label: translate('thermal'), tooltip: effectiveArmourThermalTt }); + + const armourDamageTakenData = []; + armourDamageTakenData.push({ value: Math.round(armour.absolute.total * 100), label: translate('absolute'), tooltip: armourDamageTakenTt }); + armourDamageTakenData.push({ value: Math.round(armour.explosive.total * 100), label: translate('explosive'), tooltip: armourDamageTakenExplosiveTt }); + armourDamageTakenData.push({ value: Math.round(armour.kinetic.total * 100), label: translate('kinetic'), tooltip: armourDamageTakenKineticTt }); + armourDamageTakenData.push({ value: Math.round(armour.thermal.total * 100), label: translate('thermal'), tooltip: armourDamageTakenThermalTt }); + + return ( + + {shield.total ? +
+

{translate('shield metrics')}

+
+

{shieldSourcesTt}

)} onMouseOut={tooltip.bind(null, null)} className='summary'>{translate('raw shield strength')}
{formats.int(shield.total)}{units.MJ} +

{translate('PHRASE_TIME_TO_LOSE_SHIELDS')}
{shielddamage.totalsdps == 0 ? translate('ever') : formats.time(Calc.timeToDeplete(shield.total, shielddamage.totalsdps, shielddamage.totalseps, pd.getWeaponsCapacity(), pd.getWeaponsRechargeRate() * opponentWep / 4))}

+

{translate('PHRASE_TIME_TO_RECOVER_SHIELDS')}
{shield.recover === Math.Inf ? translate('never') : formats.time(shield.recover)}

+

{translate('PHRASE_TIME_TO_RECHARGE_SHIELDS')}
{shield.recharge === Math.Inf ? translate('never') : formats.time(shield.recharge)}

+ +
+

{translate('shield sources')}

+ +
+
+

{translate('damage taken')}(%)

+ +
+
+

{translate('effective shield')}(MJ)

+ +
+
: null } + +
+

{translate('armour metrics')}

+

{armourSourcesTt}

)} onMouseOut={tooltip.bind(null, null)} className='summary'>{translate('raw armour strength')}
{formats.int(armour.total)} +

{translate('PHRASE_TIME_TO_LOSE_ARMOUR')}
{armourdamage.totalsdps == 0 ? translate('ever') : formats.time(Calc.timeToDeplete(armour.total, armourdamage.totalsdps, armourdamage.totalseps, pd.getWeaponsCapacity(), pd.getWeaponsRechargeRate() * opponentWep / 4))}

+

{translate('raw module armour')}
{formats.int(armour.modulearmour)}

+

{translate('PHRASE_MODULE_PROTECTION_EXTERNAL')}
{formats.pct1(armour.moduleprotection / 2)}

+

{translate('PHRASE_MODULE_PROTECTION_INTERNAL')}
{formats.pct1(armour.moduleprotection)}

+
+ +
+

{translate('armour sources')}

+ +
+
+

{translate('damage taken')}(%)

+ +
+
+

{translate('effective armour')}

+ +
+
); + } +} diff --git a/src/app/components/DefenceSummary.jsx b/src/app/components/DefenceSummary.jsx deleted file mode 100644 index 21896b90..00000000 --- a/src/app/components/DefenceSummary.jsx +++ /dev/null @@ -1,106 +0,0 @@ -import React from 'react'; -import cn from 'classnames'; -import TranslatedComponent from './TranslatedComponent'; -import { DamageKinetic, DamageThermal, DamageExplosive } from './SvgIcons'; - -/** - * Defence summary - */ -export default class DefenceSummary extends TranslatedComponent { - static PropTypes = { - ship: React.PropTypes.object.isRequired - }; - - /** - * Constructor - * @param {Object} props React Component properties - */ - constructor(props) { - super(props); - } - - /** - * Render defence summary - * @return {React.Component} contents - */ - render() { - let ship = this.props.ship; - let { language, tooltip, termtip } = this.context; - let { formats, translate, units } = language; - let hide = tooltip.bind(null, null); - - const shieldGenerator = ship.findShieldGenerator(); - - // Damage values are 1 - resistance values - return ( - -

{translate('defence summary')}

- - - {ship.shield ? - - - : null } - {ship.shield ? - - - - - - : null } - {ship.shield ? - - - - - - : null } - - { ship.shield && ship.shieldCells ? - - - : null } - - - - - - - - - - - - {ship.modulearmour > 0 ? - - - : null } - - {ship.moduleprotection > 0 ? - - - - : null } - -

{translate('shields')}: {formats.int(ship.shield)} {units.MJ}

{translate('recovery')}{formats.time(ship.calcShieldRecovery())}{translate('recharge')}{formats.time(ship.calcShieldRecharge())}
{translate('damage from')} -   - {formats.pct1(1 - ship.shieldExplRes)} - -   - {formats.pct1(1 - ship.shieldKinRes)} - -   - {formats.pct1(1 - ship.shieldThermRes)} -

{translate('shield cells')}: {formats.int(ship.shieldCells)} {units.MJ}

{translate('armour')}: {formats.int(ship.armour)}

{translate('damage from')} -   - {formats.pct1(1 - ship.hullExplRes)} -   - {formats.pct1(1 - ship.hullKinRes)} - -   - {formats.pct1(1 - ship.hullThermRes)} -

{translate('module armour')}: {formats.int(ship.modulearmour)}

{translate('internal protection')} {formats.pct1(ship.moduleprotection)}{translate('external protection')} {formats.pct1(ship.moduleprotection / 2)}
-
- ); - } -} diff --git a/src/app/components/EngagementRange.jsx b/src/app/components/EngagementRange.jsx new file mode 100644 index 00000000..2a3bdfd1 --- /dev/null +++ b/src/app/components/EngagementRange.jsx @@ -0,0 +1,101 @@ +import React from 'react'; +import TranslatedComponent from './TranslatedComponent'; +import { Ships } from 'coriolis-data/dist'; +import Slider from '../components/Slider'; + +/** + * Engagement range slider + * Requires an onChange() function of the form onChange(range), providing the range in metres, which is triggered on range change + */ +export default class EngagementRange extends TranslatedComponent { + static propTypes = { + ship: React.PropTypes.object.isRequired, + engagementRange: React.PropTypes.number.isRequired, + onChange: React.PropTypes.func.isRequired + }; + + /** + * Constructor + * @param {Object} props React Component properties + * @param {Object} context React Component context + */ + constructor(props, context) { + super(props); + + const { ship } = props; + + const maxRange = this._calcMaxRange(ship); + + this.state = { + maxRange + }; + } + + /** + * Calculate the maximum range of a ship's weapons + * @param {Object} ship The ship + * @returns {int} The maximum range, in metres + */ + _calcMaxRange(ship) { + let maxRange = 1000; + for (let i = 0; i < ship.hardpoints.length; i++) { + if (ship.hardpoints[i].maxClass > 0 && ship.hardpoints[i].m && ship.hardpoints[i].enabled) { + const thisRange = ship.hardpoints[i].m.getRange(); + if (thisRange > maxRange) { + maxRange = thisRange; + } + } + } + + return maxRange; + } + + /** + * Update range + * @param {number} rangeLevel percentage level from 0 to 1 + */ + _rangeChange(rangeLevel) { + const { maxRange } = this.state; + + // We round the range to an integer value + const range = Math.round(rangeLevel * maxRange); + + if (range !== this.props.engagementRange) { + this.props.onChange(range); + } + } + + /** + * Render range slider + * @return {React.Component} contents + */ + render() { + const { language, onWindowResize, sizeRatio, tooltip, termtip } = this.context; + const { formats, translate, units } = language; + const { engagementRange } = this.props; + const { maxRange } = this.state; + + return ( + +

{translate('engagement range')}: {formats.int(engagementRange)}{translate('m')}

+ + + + + + +
+ +
+
+ ); + } +} diff --git a/src/app/components/EngineProfile.jsx b/src/app/components/EngineProfile.jsx index 6e4af044..8b90165f 100644 --- a/src/app/components/EngineProfile.jsx +++ b/src/app/components/EngineProfile.jsx @@ -1,7 +1,6 @@ import React from 'react'; import TranslatedComponent from './TranslatedComponent'; import { Ships } from 'coriolis-data/dist'; -import ShipSelector from './ShipSelector'; import { nameComparator } from '../utils/SlotFunctions'; import LineChart from '../components/LineChart'; import Slider from '../components/Slider'; @@ -13,10 +12,13 @@ import * as Calc from '../shipyard/Calculations'; * Engine profile for a given ship */ export default class EngineProfile extends TranslatedComponent { - static PropTypes = { + static propTypes = { ship: React.PropTypes.object.isRequired, - chartWidth: React.PropTypes.number.isRequired, - code: React.PropTypes.string.isRequired + cargo: React.PropTypes.number.isRequired, + fuel: React.PropTypes.number.isRequired, + eng: React.PropTypes.number.isRequired, + boost: React.PropTypes.bool.isRequired, + marker: React.PropTypes.string.isRequired }; /** @@ -30,8 +32,7 @@ export default class EngineProfile extends TranslatedComponent { const ship = this.props.ship; this.state = { - cargo: ship.cargoCapacity, - calcMaxSpeedFunc: this._calcMaxSpeed.bind(this, ship) + calcMaxSpeedFunc: this.calcMaxSpeed.bind(this, ship, this.props.eng, this.props.boost) }; } @@ -42,36 +43,23 @@ export default class EngineProfile extends TranslatedComponent { * @return {boolean} Returns true if the component should be rerendered */ componentWillReceiveProps(nextProps, nextContext) { - if (nextProps.code != this.props.code) { - this.setState({ cargo: nextProps.ship.cargoCapacity, calcMaxSpeedFunc: this._calcMaxSpeed.bind(this, nextProps.ship) }); + if (nextProps.marker != this.props.marker) { + this.setState({ calcMaxSpeedFunc: this.calcMaxSpeed.bind(this, nextProps.ship, nextProps.eng, nextProps.boost) }); } return true; } /** - * Calculate the maximum speed for this ship across its applicable mass + * Calculate the top speed for this ship given thrusters, mass and pips to ENG * @param {Object} ship The ship + * @param {Object} eng The number of pips to ENG + * @param {Object} boost If boost is enabled * @param {Object} mass The mass at which to calculate the top speed * @return {number} The maximum speed */ - _calcMaxSpeed(ship, mass) { - // Obtain the thrusters for this ship - const thrusters = ship.standard[1].m; - + calcMaxSpeed(ship, eng, boost, mass) { // Obtain the top speed - return Calc.speed(mass, ship.speed, thrusters, ship.engpip)[4]; - } - - /** - * Update cargo level - * @param {number} cargoLevel Cargo level 0 - 1 - */ - _cargoChange(cargoLevel) { - let ship = this.props.ship; - let cargo = Math.round(ship.cargoCapacity * cargoLevel); - this.setState({ - cargo - }); + return Calc.calcSpeed(mass, ship.speed, ship.standard[1].m, ship.pipSpeed, eng, ship.boost / ship.speed, boost); } /** @@ -81,70 +69,37 @@ export default class EngineProfile extends TranslatedComponent { render() { const { language, onWindowResize, sizeRatio, tooltip, termtip } = this.context; const { formats, translate, units } = language; - const { ship } = this.props; - const { cargo } = this.state; + const { ship, cargo, eng, fuel, boost } = this.props; // Calculate bounds for our line chart const thrusters = ship.standard[1].m; - const minMass = thrusters.getMinMass(); + const minMass = ship.calcLowestPossibleMass({ th: thrusters }); const maxMass = thrusters.getMaxMass(); - const minSpeed = Calc.speed(maxMass, ship.speed, thrusters, ship.engpip)[4]; - const maxSpeed = Calc.speed(minMass, ship.speed, thrusters, ship.engpip)[4]; - let mass = ship.unladenMass + ship.fuelCapacity + cargo; - let mark; - if (mass < minMass) { - mark = minMass; - } else if (mass > maxMass) { - mark = maxMass; - } else { - mark = mass; - } - - const cargoPercent = cargo / ship.cargoCapacity; + const mass = ship.unladenMass + fuel + cargo; + const minSpeed = Calc.calcSpeed(maxMass, ship.speed, thrusters, ship.pipSpeed, 0, ship.boost / ship.speed, false); + const maxSpeed = Calc.calcSpeed(minMass, ship.speed, thrusters, ship.pipSpeed, 4, ship.boost / ship.speed, true); + // Add a mark at our current mass + const mark = Math.min(mass, maxMass); - const code = ship.toString() + '.' + ship.getModificationsString() + '.' + ship.getPowerEnabledString(); + const code = `${ship.toString()}:${cargo}:${fuel}:${eng}:${boost}`; - // This graph has a precipitous fall-off so we use lots of points to make it look a little smoother + // This graph can have a precipitous fall-off so we use lots of points to make it look a little smoother return ( - -

{translate('engine profile')}

- - {ship.cargoCapacity ? - -

{translate('cargo carried')}: {formats.int(cargo)}{units.T}

- - - - - - -
- -
-
: '' } -
+ ); } } diff --git a/src/app/components/FSDProfile.jsx b/src/app/components/FSDProfile.jsx index 7633a34a..c3e7bb83 100644 --- a/src/app/components/FSDProfile.jsx +++ b/src/app/components/FSDProfile.jsx @@ -1,7 +1,6 @@ import React from 'react'; import TranslatedComponent from './TranslatedComponent'; import { Ships } from 'coriolis-data/dist'; -import ShipSelector from './ShipSelector'; import { nameComparator } from '../utils/SlotFunctions'; import LineChart from '../components/LineChart'; import Slider from '../components/Slider'; @@ -13,10 +12,11 @@ import * as Calc from '../shipyard/Calculations'; * FSD profile for a given ship */ export default class FSDProfile extends TranslatedComponent { - static PropTypes = { + static propTypes = { ship: React.PropTypes.object.isRequired, - chartWidth: React.PropTypes.number.isRequired, - code: React.PropTypes.string.isRequired + cargo: React.PropTypes.number.isRequired, + fuel: React.PropTypes.number.isRequired, + marker: React.PropTypes.string.isRequired }; /** @@ -30,8 +30,7 @@ export default class FSDProfile extends TranslatedComponent { const ship = this.props.ship; this.state = { - cargo: ship.cargoCapacity, - calcMaxRangeFunc: this._calcMaxRange.bind(this, ship) + calcMaxRangeFunc: this._calcMaxRange.bind(this, ship, this.props.fuel) }; } @@ -42,8 +41,8 @@ export default class FSDProfile extends TranslatedComponent { * @return {boolean} Returns true if the component should be rerendered */ componentWillReceiveProps(nextProps, nextContext) { - if (nextProps.code != this.props.code) { - this.setState({ cargo: nextProps.ship.cargoCapacity, calcMaxRangeFunc: this._calcMaxRange.bind(this, nextProps.ship) }); + if (nextProps.marker != this.props.marker) { + this.setState({ calcMaxRangeFunc: this._calcMaxRange.bind(this, nextProps.ship, nextProps.fuel) }); } return true; } @@ -51,101 +50,54 @@ export default class FSDProfile extends TranslatedComponent { /** * Calculate the maximum range for this ship across its applicable mass * @param {Object} ship The ship + * @param {Object} fuel The fuel on the ship * @param {Object} mass The mass at which to calculate the maximum range * @return {number} The maximum range */ - _calcMaxRange(ship, mass) { - // Obtain the FSD for this ship - const fsd = ship.standard[2].m; - + _calcMaxRange(ship, fuel, mass) { // Obtain the maximum range - return Calc.jumpRange(mass, fsd, fsd.getMaxFuelPerJump()); - } - - /** - * Update cargo level - * @param {number} cargoLevel Cargo level 0 - 1 - */ - _cargoChange(cargoLevel) { - let ship = this.props.ship; - let cargo = Math.round(ship.cargoCapacity * cargoLevel); - this.setState({ - cargo - }); + return Calc.jumpRange(mass, ship.standard[2].m, Math.min(fuel, ship.standard[2].m.getMaxFuelPerJump())); } /** - * Render engine profile + * Render FSD profile * @return {React.Component} contents */ render() { const { language, onWindowResize, sizeRatio, tooltip, termtip } = this.context; const { formats, translate, units } = language; - const { ship } = this.props; - const { cargo } = this.state; + const { ship, cargo, fuel } = this.props; // Calculate bounds for our line chart - use thruster info for X const thrusters = ship.standard[1].m; const fsd = ship.standard[2].m; - const minMass = thrusters.getMinMass(); + const minMass = ship.calcLowestPossibleMass({ th: thrusters }); const maxMass = thrusters.getMaxMass(); + const mass = ship.unladenMass + fuel + cargo; const minRange = 0; const maxRange = Calc.jumpRange(minMass + fsd.getMaxFuelPerJump(), fsd, fsd.getMaxFuelPerJump()); - let mass = ship.unladenMass + fsd.getMaxFuelPerJump() + cargo; - let mark; - if (mass < minMass) { - mark = minMass; - } else if (mass > maxMass) { - mark = maxMass; - } else { - mark = mass; - } - - const cargoPercent = cargo / ship.cargoCapacity; + // Add a mark at our current mass + const mark = Math.min(mass, maxMass); - const code = ship.name + ship.toString() + '.' + ship.getModificationsString() + '.' + ship.getPowerEnabledString(); + const code = ship.name + ship.toString() + '.' + fuel; return ( - -

{translate('fsd profile')}

- - {ship.cargoCapacity ? - -

{translate('cargo carried')}: {formats.int(cargo)}{units.T}

- - - - - - -
- -
-
: '' } -
+ ); } } diff --git a/src/app/components/Fuel.jsx b/src/app/components/Fuel.jsx new file mode 100644 index 00000000..8c8ea8d7 --- /dev/null +++ b/src/app/components/Fuel.jsx @@ -0,0 +1,74 @@ +import React from 'react'; +import TranslatedComponent from './TranslatedComponent'; +import { Ships } from 'coriolis-data/dist'; +import Slider from '../components/Slider'; + +/** + * Fuel slider + * Requires an onChange() function of the form onChange(fuel), providing the fuel in tonnes, which is triggered on fuel level change + */ +export default class Fuel extends TranslatedComponent { + static propTypes = { + fuel: React.PropTypes.number.isRequired, + fuelCapacity: React.PropTypes.number.isRequired, + onChange: React.PropTypes.func.isRequired + }; + + /** + * Constructor + * @param {Object} props React Component properties + * @param {Object} context React Component context + */ + constructor(props, context) { + super(props); + + this._fuelChange = this._fuelChange.bind(this); + } + + /** + * Update fuel level + * @param {number} fuelLevel percentage level from 0 to 1 + */ + _fuelChange(fuelLevel) { + const { fuel, fuelCapacity } = this.props; + + const newFuel = fuelLevel * fuelCapacity; + // Only send an update if the fuel has changed significantly + if (Math.round(fuel * 10) != Math.round(newFuel * 10)) { + this.props.onChange(Math.round(newFuel * 10) / 10); + } + } + + /** + * Render fuel slider + * @return {React.Component} contents + */ + render() { + const { language, onWindowResize, sizeRatio, tooltip, termtip } = this.context; + const { formats, translate, units } = language; + const { fuel, fuelCapacity } = this.props; + + return ( + +

{translate('fuel carried')}: {formats.f1(fuel)}{units.T}

+ + + + + + +
+ +
+
+ ); + } +} diff --git a/src/app/components/HardpointSlot.jsx b/src/app/components/HardpointSlot.jsx index ce8416a9..b10d22df 100644 --- a/src/app/components/HardpointSlot.jsx +++ b/src/app/components/HardpointSlot.jsx @@ -4,6 +4,7 @@ import Persist from '../stores/Persist'; import { DamageAbsolute, DamageKinetic, DamageThermal, DamageExplosive, MountFixed, MountGimballed, MountTurret, ListModifications, Modified } from './SvgIcons'; import { Modifications } from 'coriolis-data/dist'; import { stopCtxPropagation } from '../utils/UtilityFunctions'; +import { blueprintTooltip } from '../utils/BlueprintFunctions'; /** @@ -51,6 +52,12 @@ export default class HardpointSlot extends Slot { if (m.blueprint.special && m.blueprint.special.id >= 0) { modTT += ', ' + translate(m.blueprint.special.name); } + modTT = ( +
+
{modTT}
+ {blueprintTooltip(translate, m.blueprint.grades[m.blueprint.grade], null, m.grp, m)} +
+ ); } return
@@ -74,18 +81,20 @@ export default class HardpointSlot extends Slot { { m.getHps() ?
{translate('HPS')}: {formats.round1(m.getHps())} { m.getClip() ? ({formats.round1((m.getClip() * m.getHps() / m.getRoF()) / ((m.getClip() / m.getRoF()) + m.getReload())) }) : null }
: null } { m.getDps() && m.getEps() ?
{translate('DPE')}: {formats.f1(m.getDps() / m.getEps())}
: null } { m.getRoF() ?
{translate('ROF')}: {formats.f1(m.getRoF())}{u.ps}
: null } - { m.getRange() ?
{translate('range')} {formats.f1(m.getRange() / 1000)}{u.km}
: null } + { m.getRange() ?
{translate('range', m.grp)} {formats.f1(m.getRange() / 1000)}{u.km}
: null } + { m.getScanTime() ?
{translate('scantime')} {formats.f1(m.getScanTime())}{u.s}
: null } { m.getFalloff() ?
{translate('falloff')} {formats.round(m.getFalloff() / 1000)}{u.km}
: null } { m.getShieldBoost() ?
+{formats.pct1(m.getShieldBoost())}
: null } { m.getAmmo() ?
{translate('ammunition')}: {formats.int(m.getClip())}/{formats.int(m.getAmmo())}
: null } + { m.getReload() ?
{translate('reload')}: {formats.round(m.getReload())}{u.s}
: null } { m.getShotSpeed() ?
{translate('shotspeed')}: {formats.int(m.getShotSpeed())}{u.mps}
: null } { m.getPiercing() ?
{translate('piercing')}: {formats.int(m.getPiercing())}
: null } { m.getJitter() ?
{translate('jitter')}: {formats.f2(m.getJitter())}°
: null } { showModuleResistances && m.getExplosiveResistance() ?
{translate('explres')}: {formats.pct(m.getExplosiveResistance())}
: null } { showModuleResistances && m.getKineticResistance() ?
{translate('kinres')}: {formats.pct(m.getKineticResistance())}
: null } { showModuleResistances && m.getThermalResistance() ?
{translate('thermres')}: {formats.pct(m.getThermalResistance())}
: null } + { m.getIntegrity() ?
{translate('integrity')}: {formats.int(m.getIntegrity())}
: null } { m && validMods.length > 0 ?
: null } -
; } else { diff --git a/src/app/components/HardpointsSlotSection.jsx b/src/app/components/HardpointSlotSection.jsx similarity index 98% rename from src/app/components/HardpointsSlotSection.jsx rename to src/app/components/HardpointSlotSection.jsx index 51bb79d4..5d697476 100644 --- a/src/app/components/HardpointsSlotSection.jsx +++ b/src/app/components/HardpointSlotSection.jsx @@ -8,7 +8,7 @@ import { stopCtxPropagation } from '../utils/UtilityFunctions'; /** * Hardpoint slot section */ -export default class HardpointsSlotSection extends SlotSection { +export default class HardpointSlotSection extends SlotSection { /** * Constructor diff --git a/src/app/components/Header.jsx b/src/app/components/Header.jsx index ff880f1b..c36660da 100644 --- a/src/app/components/Header.jsx +++ b/src/app/components/Header.jsx @@ -356,7 +356,7 @@ export default class Header extends TranslatedComponent { let comps = Object.keys(Persist.getComparisons()).sort(); for (let name of comps) { - comparisons.push({name}); + comparisons.push({name}); } } else { comparisons = {translate('none created')}; diff --git a/src/app/components/InternalSlot.jsx b/src/app/components/InternalSlot.jsx index 979c9318..32916038 100644 --- a/src/app/components/InternalSlot.jsx +++ b/src/app/components/InternalSlot.jsx @@ -4,6 +4,7 @@ import Persist from '../stores/Persist'; import { ListModifications, Modified } from './SvgIcons'; import { Modifications } from 'coriolis-data/dist'; import { stopCtxPropagation } from '../utils/UtilityFunctions'; +import { blueprintTooltip } from '../utils/BlueprintFunctions'; /** * Internal Slot @@ -30,6 +31,12 @@ export default class InternalSlot extends Slot { let modTT = translate('modified'); if (m && m.blueprint && m.blueprint.name) { modTT = translate(m.blueprint.name) + ' ' + translate('grade') + ' ' + m.blueprint.grade; + modTT = ( +
+
{modTT}
+ {blueprintTooltip(translate, m.blueprint.grades[m.blueprint.grade], null, m.grp, m)} +
+ ); } let mass = m.getMass() || m.cargo || m.fuel || 0; @@ -45,31 +52,32 @@ export default class InternalSlot extends Slot { { m.bays ?
{translate('bays')}: {m.bays}
: null } { m.rebuildsperbay ?
{translate('rebuildsperbay')}: {m.rebuildsperbay}
: null } { m.rate ?
{translate('rate')}: {m.rate}{u.kgs}   {translate('refuel time')}: {formats.time(this.props.fuel * 1000 / m.rate)}
: null } - { m.getAmmo() ?
{translate('ammunition')}: {formats.gen(m.getAmmo())}
: null } - { m.cells ?
{translate('cells')}: {m.cells}
: null } - { m.getShieldReinforcement() ?
{translate('shieldreinforcement')}: {formats.int(m.getShieldReinforcement())} MJ   {translate('total')}: {formats.int(m.cells * m.getShieldReinforcement())}{u.MJ}
: null } + { m.getAmmo() && m.grp !== 'scb' ?
{translate('ammunition')}: {formats.gen(m.getAmmo())}
: null } + { m.getSpinup() ?
{translate('spinup')}: {formats.f1(m.getSpinup())}{u.s}
: null } + { m.getDuration() ?
{translate('duration')}: {formats.f1(m.getDuration())}{u.s}
: null } + { m.grp === 'scb' ?
{translate('cells')}: {formats.int(m.getAmmo() + 1)}
: null } + { m.getShieldReinforcement() ?
{translate('shieldreinforcement')}: {formats.f1(m.getDuration() * m.getShieldReinforcement())}{u.MJ}
: null } + { m.getShieldReinforcement() ?
{translate('total')}: {formats.int((m.getAmmo() + 1) * (m.getDuration() * m.getShieldReinforcement()))}{u.MJ}
: null } { m.repair ?
{translate('repair')}: {m.repair}
: null } { m.getFacingLimit() ?
{translate('facinglimit')} {formats.f1(m.getFacingLimit())}°
: null } { m.getRange() ?
{translate('range')} {formats.f2(m.getRange())}{u.km}
: null } { m.getRangeT() ?
{translate('ranget')} {formats.f1(m.getRangeT())}{u.s}
: null } - { m.getSpinup() ?
{translate('spinup')}: {formats.f1(m.getSpinup())}{u.s}
: null } { m.getTime() ?
{translate('time')}: {formats.time(m.getTime())}
: null } { m.maximum ?
{translate('max')}: {(m.maximum)}
: null } { m.rangeLS ?
{translate('range')}: {m.rangeLS}{u.Ls}
: null } { m.rangeLS === null ?
∞{u.Ls}
: null } { m.rangeRating ?
{translate('range')}: {m.rangeRating}
: null } - { m.getHullReinforcement() ?
+{formats.int(m.getHullReinforcement() + ship.baseArmour * m.getModValue('hullboost') / 10000)} {translate('armour')}
: null } - { m.getProtection() ?
{formats.rPct(m.getProtection())} {translate('protection')}
: null } - { m.getIntegrity() && m.grp === 'mrp' ?
{formats.int(m.getIntegrity())} {translate('integrity')}
: null } + { m.maximum ?
{translate('max')}: {(m.maximum)}
: null } { m.passengers ?
{translate('passengers')}: {m.passengers}
: null } + { m.getRegenerationRate() ?
{translate('regen')}: {formats.round1(m.getRegenerationRate())}{u.ps}
: null } + { m.getBrokenRegenerationRate() ?
{translate('brokenregen')}: {formats.round1(m.getBrokenRegenerationRate())}{u.ps}
: null } { showModuleResistances && m.getExplosiveResistance() ?
{translate('explres')}: {formats.pct(m.getExplosiveResistance())}
: null } { showModuleResistances && m.getKineticResistance() ?
{translate('kinres')}: {formats.pct(m.getKineticResistance())}
: null } { showModuleResistances && m.getThermalResistance() ?
{translate('thermres')}: {formats.pct(m.getThermalResistance())}
: null } - { m.getRegenerationRate() ?
{translate('regen')}: {formats.round1(m.getRegenerationRate())}{u.ps}
: null } - { m.getBrokenRegenerationRate() ?
{translate('brokenregen')}: {formats.round1(m.getBrokenRegenerationRate())}{u.ps}
: null } - + { m.getHullReinforcement() ?
{translate('armour')}: {formats.int(m.getHullReinforcement() + ship.baseArmour * m.getModValue('hullboost') / 10000)}
: null } + { m.getProtection() ?
{translate('protection')}: {formats.rPct(m.getProtection())}
: null } + { m.getIntegrity() ?
{translate('integrity')}: {formats.int(m.getIntegrity())}
: null } { m && validMods.length > 0 ?
: null } - ; } else { diff --git a/src/app/components/JumpRange.jsx b/src/app/components/JumpRange.jsx index d197ff04..d1421ec1 100644 --- a/src/app/components/JumpRange.jsx +++ b/src/app/components/JumpRange.jsx @@ -1,7 +1,6 @@ import React from 'react'; import TranslatedComponent from './TranslatedComponent'; import { Ships } from 'coriolis-data/dist'; -import ShipSelector from './ShipSelector'; import { nameComparator } from '../utils/SlotFunctions'; import LineChart from '../components/LineChart'; import Slider from '../components/Slider'; @@ -13,9 +12,8 @@ import * as Calc from '../shipyard/Calculations'; * Jump range for a given ship */ export default class JumpRange extends TranslatedComponent { - static PropTypes = { + static propTypes = { ship: React.PropTypes.object.isRequired, - chartWidth: React.PropTypes.number.isRequired, code: React.PropTypes.string.isRequired }; @@ -91,7 +89,6 @@ export default class JumpRange extends TranslatedComponent {

{translate('jump range')}

0.60), + flip = (xPos / width > 0.50), tipWidth = 0, tipHeightPx = tips.selectAll('rect').node().getBoundingClientRect().height; @@ -110,19 +117,21 @@ export default class LineChart extends TranslatedComponent { /** * Update dimensions based on properties and scale - * @param {Object} props React Component properties + * @param {Object} props React Component properties * @param {number} scale size ratio / scale + * @returns {Object} calculated dimensions */ _updateDimensions(props, scale) { - let { width, xMax, xMin, yMin, yMax } = props; - let innerWidth = width - MARGIN.left - MARGIN.right; - let outerHeight = Math.round(width * 0.5 * scale); - let innerHeight = outerHeight - MARGIN.top - MARGIN.bottom; + const { xMax, xMin, yMin, yMax } = props; + const { width, height } = this.state.dimensions; + const innerWidth = width - MARGIN.left - MARGIN.right; + const outerHeight = Math.round(width * props.aspect); + const innerHeight = outerHeight - MARGIN.top - MARGIN.bottom; this.state.xScale.range([0, innerWidth]).domain([xMin, xMax || 1]).clamp(true); this.state.xAxisScale.range([0, innerWidth]).domain([xMin, xMax]).clamp(true); this.state.yScale.range([innerHeight, 0]).domain([yMin, yMax + (yMax - yMin) * 0.1]); // 10% higher than maximum value for tooltip visibility - this.setState({ innerWidth, outerHeight, innerHeight }); + return { innerWidth, outerHeight, innerHeight }; } /** @@ -183,7 +192,7 @@ export default class LineChart extends TranslatedComponent { for (let i = 0, l = series ? series.length : 1; i < l; i++) { const yAccessor = series ? function(d) { return state.yScale(d[1][this]); }.bind(series[i]) : (d) => state.yScale(d[1]); seriesLines.push(d3.line().x((d, i) => this.state.xScale(d[0])).y(yAccessor)); - detailElems.push(); + detailElems.push(); markerElems.push(); } @@ -196,7 +205,6 @@ export default class LineChart extends TranslatedComponent { * Update dimensions and series data based on props and context. */ componentWillMount() { - this._updateDimensions(this.props, this.context.sizeRatio); this._updateSeries(this.props, this.state); } @@ -206,14 +214,7 @@ export default class LineChart extends TranslatedComponent { * @param {Object} nextContext Incoming/Next conext */ componentWillReceiveProps(nextProps, nextContext) { - let { func, xMin, xMax, yMin, yMax, width } = nextProps; - let props = this.props; - - let domainChanged = xMax != props.xMax || xMin != props.xMin || yMax != props.yMax || yMin != props.yMin || func != props.func; - - if (width != props.width || domainChanged || this.context.sizeRatio != nextContext.sizeRatio) { - this._updateDimensions(nextProps, nextContext.sizeRatio); - } + const props = this.props; if (props.code != nextProps.code) { this._updateSeries(nextProps, this.state); @@ -225,53 +226,57 @@ export default class LineChart extends TranslatedComponent { * @return {React.Component} Chart SVG */ render() { - if (!this.props.width) { - return null; - } - - let { xMin, xMax, xLabel, yLabel, xUnit, yUnit, xMark, colors } = this.props; - let { innerWidth, outerHeight, innerHeight, tipHeight, detailElems, markerElems, seriesData, seriesLines } = this.state; - let line = this.line; - let lines = seriesLines.map((line, i) => ).reverse(); + const { innerWidth, outerHeight, innerHeight } = this._updateDimensions(this.props, this.context.sizeRatio); + const { width, height } = this.state.dimensions; + const { xMin, xMax, xLabel, yLabel, xUnit, yUnit, xMark, colors } = this.props; + const { tipHeight, detailElems, markerElems, seriesData, seriesLines } = this.state; + const line = this.line; + const lines = seriesLines.map((line, i) => ).reverse(); const markX = xMark ? innerWidth * (xMark - xMin) / (xMax - xMin) : 0; - const xmark = xMark ? : ''; - - return - - {xmark} - {lines} - d3.select(elem).call(this.xAxis)} transform={`translate(0,${innerHeight})`}> - - {xLabel} - ({xUnit}) - - - d3.select(elem).call(this.yAxis)}> - - {yLabel} - { yUnit && ({yUnit}) } - - - this.tipContainer = d3.select(g)} style={{ display: 'none' }}> - - {detailElems} - - this.markersContainer = d3.select(g)} style={{ display: 'none' }}> - {markerElems} - - - - ; + const xmark = xMark ? : ''; + + return ( + { this.setState({ dimensions }); }}> +
+ + + {xmark} + {lines} + d3.select(elem).call(this.xAxis)} transform={`translate(0,${innerHeight})`}> + + {xLabel} + ({xUnit}) + + + d3.select(elem).call(this.yAxis)}> + + {yLabel} + { yUnit && ({yUnit}) } + + + this.tipContainer = d3.select(g)} style={{ display: 'none' }}> + + {detailElems} + + this.markersContainer = d3.select(g)} style={{ display: 'none' }}> + {markerElems} + + + + +
+
+ ); } } diff --git a/src/app/components/Modification.jsx b/src/app/components/Modification.jsx index 15cf5a44..4869a694 100644 --- a/src/app/components/Modification.jsx +++ b/src/app/components/Modification.jsx @@ -37,7 +37,7 @@ export default class Modification extends TranslatedComponent { const name = this.props.name; let scaledValue = Math.round(Number(value) * 100); - // Limit to +1000% / -100% + // Limit to +1000% / -99.99% if (scaledValue > 100000) { scaledValue = 100000; value = 1000; @@ -52,6 +52,12 @@ export default class Modification extends TranslatedComponent { ship.setModification(m, name, scaledValue, true); this.setState({ value }); + } + + /** + * Triggered when an update to slider value is finished i.e. when losing focus + */ + _updateFinished() { this.props.onChange(); } @@ -79,7 +85,7 @@ export default class Modification extends TranslatedComponent { } return ( -
+
{translate(name, m.grp)}{symbol}
diff --git a/src/app/components/ModificationsMenu.jsx b/src/app/components/ModificationsMenu.jsx index 0b25479b..1e657e79 100644 --- a/src/app/components/ModificationsMenu.jsx +++ b/src/app/components/ModificationsMenu.jsx @@ -5,6 +5,7 @@ import { isEmpty, stopCtxPropagation } from '../utils/UtilityFunctions'; import cn from 'classnames'; import { Modifications } from 'coriolis-data/dist'; import Modification from './Modification'; +import { getBlueprint, blueprintTooltip } from '../utils/BlueprintFunctions'; /** * Modifications menu @@ -14,6 +15,7 @@ export default class ModificationsMenu extends TranslatedComponent { static propTypes = { ship: React.PropTypes.object.isRequired, m: React.PropTypes.object.isRequired, + marker: React.PropTypes.string.isRequired, onChange: React.PropTypes.func.isRequired }; @@ -24,7 +26,6 @@ export default class ModificationsMenu extends TranslatedComponent { */ constructor(props, context) { super(props); - this.state = this._initState(props, context); this._toggleBlueprintsMenu = this._toggleBlueprintsMenu.bind(this); this._toggleSpecialsMenu = this._toggleSpecialsMenu.bind(this); @@ -33,31 +34,59 @@ export default class ModificationsMenu extends TranslatedComponent { this._rollBest = this._rollBest.bind(this); this._rollExtreme = this._rollExtreme.bind(this); this._reset = this._reset.bind(this); + + this.state = { + blueprintMenuOpened: false, + specialMenuOpened: false + }; } /** - * Initialise state - * @param {Object} props React Component properties - * @param {Object} context React Component context + * Render the blueprints + * @param {Object} props React component properties + * @param {Object} context React component context * @return {Object} list: Array of React Components */ - _initState(props, context) { - let { m } = props; - const { language } = context; + _renderBlueprints(props, context) { + const { m } = props; + const { language, tooltip, termtip } = context; const translate = language.translate; - // Set up the blueprints - let blueprints = []; + const blueprints = []; for (const blueprintName in Modifications.modules[m.grp].blueprints) { - for (const grade of Modifications.modules[m.grp].blueprints[blueprintName]) { - const close = this._blueprintSelected.bind(this, Modifications.blueprints[blueprintName].id, grade); + const blueprint = getBlueprint(blueprintName, m); + let blueprintGrades = []; + for (let grade in Modifications.modules[m.grp].blueprints[blueprintName].grades) { + // Grade is a string in the JSON so make it a number + grade = Number(grade); + const classes = cn('c', { + active: m.blueprint && blueprint.id === m.blueprint.id && grade === m.blueprint.grade + }); + const close = this._blueprintSelected.bind(this, blueprintName, grade); const key = blueprintName + ':' + grade; - blueprints.push(
{translate(Modifications.blueprints[blueprintName].name + ' grade ' + grade)}
); + const tooltipContent = blueprintTooltip(translate, blueprint.grades[grade], Modifications.modules[m.grp].blueprints[blueprintName].grades[grade].engineers, m.grp); + blueprintGrades.unshift(
  • {grade}
  • ); + } + if (blueprintGrades) { + blueprints.push(
    {translate(blueprint.name)}
    ); + blueprints.push(
      {blueprintGrades}
    ); } } + return blueprints; + } + + /** + * Render the specials + * @param {Object} props React component properties + * @param {Object} context React component context + * @return {Object} list: Array of React Components + */ + _renderSpecials(props, context) { + const { m } = props; + const { language, tooltip, termtip } = context; + const translate = language.translate; - // Set up the special effects - let specials = []; + const specials = []; if (Modifications.modules[m.grp].specials && Modifications.modules[m.grp].specials.length > 0) { const close = this._specialSelected.bind(this, null); specials.push(
    {translate('PHRASE_NO_SPECIAL')}
    ); @@ -66,24 +95,17 @@ export default class ModificationsMenu extends TranslatedComponent { specials.push(
    {translate(Modifications.specials[specialName].name)}
    ); } } - - // Set up the modifications - const modifications = this._setModifications(props); - - const blueprintMenuOpened = false; - const specialMenuOpened = false; - - return { blueprintMenuOpened, blueprints, modifications, specialMenuOpened, specials }; + return specials; } /** - * Initialise the modifications + * Render the modifications * @param {Object} props React Component properties * @return {Object} list: Array of React Components */ - _setModifications(props) { + _renderModifications(props) { const { m, onChange, ship } = props; - let modifications = []; + const modifications = []; for (const modName of Modifications.modules[m.grp].modifications) { if (!Modifications.modifications[modName].hidden) { const key = modName + (m.getModValue(modName) / 100 || 0); @@ -103,17 +125,17 @@ export default class ModificationsMenu extends TranslatedComponent { /** * Activated when a blueprint is selected - * @param {int} blueprintId The ID of the selected blueprint - * @param {int} grade The grade of the selected blueprint + * @param {int} fdname The Frontier name of the blueprint + * @param {int} grade The grade of the selected blueprint */ - _blueprintSelected(blueprintId, grade) { + _blueprintSelected(fdname, grade) { + this.context.tooltip(null); const { m } = this.props; - const blueprint = Object.assign({}, _.find(Modifications.blueprints, function(o) { return o.id === blueprintId; })); + const blueprint = getBlueprint(fdname, m); blueprint.grade = grade; m.blueprint = blueprint; - const blueprintMenuOpened = false; - this.setState({ blueprintMenuOpened }); + this.setState({ blueprintMenuOpened: false }); this.props.onChange(); } @@ -130,7 +152,8 @@ export default class ModificationsMenu extends TranslatedComponent { * @param {int} special The name of the selected special */ _specialSelected(special) { - const { m } = this.props; + this.context.tooltip(null); + const { m, ship } = this.props; if (m.blueprint) { if (special === null) { @@ -138,10 +161,12 @@ export default class ModificationsMenu extends TranslatedComponent { } else { m.blueprint.special = Modifications.specials[special]; } + ship.recalculateDps(); + ship.recalculateHps(); + ship.recalculateEps(); } - const specialMenuOpened = false; - this.setState({ specialMenuOpened, modifications: this._setModifications(this.props) }); + this.setState({ specialMenuOpened: false }); this.props.onChange(); } @@ -153,13 +178,6 @@ export default class ModificationsMenu extends TranslatedComponent { * @param {number} value The value of the roll */ _setRollResult(ship, m, featureName, value) { - if (Modifications.modifications[featureName].method !== 'overwrite') { - if (m.grp == 'sb' && featureName == 'shieldboost') { - // Shield boosters are a special case. Their boost is dependent on their base so we need to calculate the value here - value = ((1 + m.shieldboost) * (1 + value) - 1) / m.shieldboost - 1; - } - } - if (Modifications.modifications[featureName].type == 'percentage') { ship.setModification(m, featureName, value * 10000); } else if (Modifications.modifications[featureName].type == 'numeric') { @@ -174,12 +192,13 @@ export default class ModificationsMenu extends TranslatedComponent { */ _rollWorst() { const { m, ship } = this.props; + ship.clearModifications(m); const features = m.blueprint.grades[m.blueprint.grade].features; for (const featureName in features) { let value = features[featureName][0]; this._setRollResult(ship, m, featureName, value); } - this.setState({ modifications: this._setModifications(this.props) }); + this.props.onChange(); } @@ -188,12 +207,13 @@ export default class ModificationsMenu extends TranslatedComponent { */ _rollRandom() { const { m, ship } = this.props; + ship.clearModifications(m); const features = m.blueprint.grades[m.blueprint.grade].features; for (const featureName in features) { let value = features[featureName][0] + (Math.random() * (features[featureName][1] - features[featureName][0])); this._setRollResult(ship, m, featureName, value); } - this.setState({ modifications: this._setModifications(this.props) }); + this.props.onChange(); } @@ -207,7 +227,7 @@ export default class ModificationsMenu extends TranslatedComponent { let value = features[featureName][1]; this._setRollResult(ship, m, featureName, value); } - this.setState({ modifications: this._setModifications(this.props) }); + this.props.onChange(); } @@ -216,6 +236,7 @@ export default class ModificationsMenu extends TranslatedComponent { */ _rollExtreme() { const { m, ship } = this.props; + ship.clearModifications(m); const features = m.blueprint.grades[m.blueprint.grade].features; for (const featureName in features) { let value; @@ -237,7 +258,7 @@ export default class ModificationsMenu extends TranslatedComponent { this._setRollResult(ship, m, featureName, value); } - this.setState({ modifications: this._setModifications(this.props) }); + this.props.onChange(); } @@ -249,7 +270,6 @@ export default class ModificationsMenu extends TranslatedComponent { ship.clearModifications(m); ship.clearBlueprint(m); - this.setState({ modifications: this._setModifications(this.props) }); this.props.onChange(); } @@ -273,11 +293,11 @@ export default class ModificationsMenu extends TranslatedComponent { let blueprintLabel; let haveBlueprint = false; + let blueprintTt; if (m.blueprint && !isEmpty(m.blueprint)) { blueprintLabel = translate(m.blueprint.name) + ' ' + translate('grade') + ' ' + m.blueprint.grade; haveBlueprint = true; - } else { - blueprintLabel = translate('PHRASE_SELECT_BLUEPRINT'); + blueprintTt = blueprintTooltip(translate, m.blueprint.grades[m.blueprint.grade], Modifications.modules[m.grp].blueprints[m.blueprint.fdname].grades[m.blueprint.grade].engineers, m.grp); } let specialLabel; @@ -287,8 +307,10 @@ export default class ModificationsMenu extends TranslatedComponent { specialLabel = translate('PHRASE_SELECT_SPECIAL'); } + const specials = this._renderSpecials(this.props, this.context); + const showBlueprintsMenu = blueprintMenuOpened; - const showSpecial = haveBlueprint && this.state.specials.length > 0; + const showSpecial = haveBlueprint && specials.length && !blueprintMenuOpened; const showSpecialsMenu = specialMenuOpened; const showRolls = haveBlueprint && !blueprintMenuOpened && !specialMenuOpened; const showReset = !blueprintMenuOpened && !specialMenuOpened; @@ -300,10 +322,12 @@ export default class ModificationsMenu extends TranslatedComponent { onClick={(e) => e.stopPropagation() } onContextMenu={stopCtxPropagation} > -
    {blueprintLabel}
    - { showBlueprintsMenu ? this.state.blueprints : null } + { showBlueprintsMenu ? '' : haveBlueprint ? +
    {blueprintLabel}
    : +
    {translate('PHRASE_SELECT_BLUEPRINT')}
    } + { showBlueprintsMenu ? this._renderBlueprints(this.props, this.context) : null } { showSpecial ?
    {specialLabel}
    : null } - { showSpecialsMenu ? this.state.specials : null } + { showSpecialsMenu ? specials : null } { showRolls || showReset ? @@ -323,7 +347,7 @@ export default class ModificationsMenu extends TranslatedComponent {
    : null } { showMods ? - { this.state.modifications } + { this._renderModifications(this.props) } : null }
    ); diff --git a/src/app/components/Movement.jsx b/src/app/components/Movement.jsx new file mode 100644 index 00000000..e7432969 --- /dev/null +++ b/src/app/components/Movement.jsx @@ -0,0 +1,70 @@ +import React from 'react'; +import TranslatedComponent from './TranslatedComponent'; + +/** + * Movement + */ +export default class Movement extends TranslatedComponent { + static propTypes = { + marker: React.PropTypes.string.isRequired, + ship: React.PropTypes.object.isRequired, + boost: React.PropTypes.bool.isRequired, + eng: React.PropTypes.number.isRequired, + fuel: React.PropTypes.number.isRequired, + cargo: React.PropTypes.number.isRequired + }; + + /** + * Constructor + * @param {Object} props React Component properties + */ + constructor(props) { + super(props); + } + + /** + * Render movement + * @return {React.Component} contents + */ + render() { + const { ship, boost, eng, cargo, fuel } = this.props; + const { language } = this.context; + const { formats, translate, units } = language; + + return ( + + + // Axes + + + + // End Arrow + + // Axes arcs and arrows + + + + + + + + + + + + + + + + // Speed + {formats.int(ship.calcSpeed(eng, fuel, cargo, boost))}m/s + // Pitch + {formats.int(ship.calcPitch(eng, fuel, cargo, boost))}°/s + // Roll + {formats.int(ship.calcRoll(eng, fuel, cargo, boost))}°/s + // Yaw + {formats.int(ship.calcYaw(eng, fuel, cargo, boost))}°/s + + ); + } +} diff --git a/src/app/components/MovementSummary.jsx b/src/app/components/MovementSummary.jsx deleted file mode 100644 index adc3974e..00000000 --- a/src/app/components/MovementSummary.jsx +++ /dev/null @@ -1,90 +0,0 @@ -import React from 'react'; -import cn from 'classnames'; -import TranslatedComponent from './TranslatedComponent'; - -/** - * Movement summary - */ -export default class MovementSummary extends TranslatedComponent { - static PropTypes = { - ship: React.PropTypes.object.isRequired - }; - - /** - * Constructor - * @param {Object} props React Component properties - */ - constructor(props) { - super(props); - } - - /** - * Render movement summary - * @return {React.Component} contents - */ - render() { - let ship = this.props.ship; - let { language, tooltip, termtip } = this.context; - let { formats, translate, units } = language; - let boostMultiplier = ship.topBoost / ship.topSpeed; - - return ( - -

    {translate('movement summary')}

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     {translate('engine pips')}
     012344B
    {translate('speed')} ({units['m/s']}){formats.int(ship.speeds[0])}{formats.int(ship.speeds[1])}{formats.int(ship.speeds[2])}{formats.int(ship.speeds[3])}{formats.int(ship.speeds[4])}{formats.int(ship.speeds[4] * boostMultiplier)}
    {translate('pitch')} ({units['°/s']}){formats.int(ship.pitches[0])}{formats.int(ship.pitches[1])}{formats.int(ship.pitches[2])}{formats.int(ship.pitches[3])}{formats.int(ship.pitches[4])}{formats.int(ship.pitches[4] * boostMultiplier)}
    {translate('roll')} ({units['°/s']}){formats.int(ship.rolls[0])}{formats.int(ship.rolls[1])}{formats.int(ship.rolls[2])}{formats.int(ship.rolls[3])}{formats.int(ship.rolls[4])}{formats.int(ship.rolls[4] * boostMultiplier)}
    {translate('yaw')} ({units['°/s']}){formats.int(ship.yaws[0])}{formats.int(ship.yaws[1])}{formats.int(ship.yaws[2])}{formats.int(ship.yaws[3])}{formats.int(ship.yaws[4])}{formats.int(ship.yaws[4] * boostMultiplier)}
    -
    - ); - } -} diff --git a/src/app/components/Offence.jsx b/src/app/components/Offence.jsx new file mode 100644 index 00000000..18bb24b0 --- /dev/null +++ b/src/app/components/Offence.jsx @@ -0,0 +1,266 @@ +import React from 'react'; +import TranslatedComponent from './TranslatedComponent'; +import * as Calc from '../shipyard/Calculations'; +import PieChart from './PieChart'; +import { nameComparator } from '../utils/SlotFunctions'; +import { MountFixed, MountGimballed, MountTurret } from './SvgIcons'; +import VerticalBarChart from './VerticalBarChart'; + +/** + * Generates an internationalization friendly weapon comparator that will + * sort by specified property (if provided) then by name/group, class, rating + * @param {function} translate Translation function + * @param {function} propComparator Optional property comparator + * @param {boolean} desc Use descending order + * @return {function} Comparator function for names + */ +export function weaponComparator(translate, propComparator, desc) { + return (a, b) => { + if (!desc) { // Flip A and B if ascending order + let t = a; + a = b; + b = t; + } + + // If a property comparator is provided use it first + let diff = propComparator ? propComparator(a, b) : nameComparator(translate, a, b); + + if (diff) { + return diff; + } + + // Property matches so sort by name / group, then class, rating + if (a.name === b.name && a.grp === b.grp) { + if(a.class == b.class) { + return a.rating > b.rating ? 1 : -1; + } + return a.class - b.class; + } + + return nameComparator(translate, a, b); + }; +} + +/** + * Offence information + * Offence information consists of four panels: + * - textual information (time to drain cap, time to take down shields etc.) + * - breakdown of damage sources (pie chart) + * - comparison of shield resistances (table chart) + * - effective sustained DPS of weapons (bar chart) + */ +export default class Offence extends TranslatedComponent { + static propTypes = { + marker: React.PropTypes.string.isRequired, + ship: React.PropTypes.object.isRequired, + opponent: React.PropTypes.object.isRequired, + engagementrange: React.PropTypes.number.isRequired, + wep: React.PropTypes.number.isRequired, + opponentSys: React.PropTypes.number.isRequired + }; + + /** + * Constructor + * @param {Object} props React Component properties + */ + constructor(props) { + super(props); + + this._sort = this._sort.bind(this); + + const damage = Calc.offenceMetrics(props.ship, props.opponent, props.wep, props.opponentSys, props.engagementrange); + this.state = { + predicate: 'n', + desc: true, + damage + }; + } + + /** + * Update the state if our properties change + * @param {Object} nextProps Incoming/Next properties + * @return {boolean} Returns true if the component should be rerendered + */ + componentWillReceiveProps(nextProps) { + if (this.props.marker != nextProps.marker || this.props.eng != nextProps.eng) { + const damage = Calc.offenceMetrics(nextProps.ship, nextProps.opponent, nextProps.wep, nextProps.opponentSys, nextProps.engagementrange); + this.setState({ damage }); + } + return true; + } + + /** + * Set the sort order and sort + * @param {string} predicate Sort predicate + */ + _sortOrder(predicate) { + let desc = this.state.desc; + + if (predicate == this.state.predicate) { + desc = !desc; + } else { + desc = true; + } + + this._sort(predicate, desc); + this.setState({ predicate, desc }); + } + + /** + * Sorts the weapon list + * @param {string} predicate Sort predicate + * @param {Boolean} desc Sort order descending + */ + _sort(predicate, desc) { + let comp = weaponComparator.bind(null, this.context.language.translate); + + switch (predicate) { + case 'n': comp = comp(null, desc); break; + case 'esdpss': comp = comp((a, b) => a.sdps.shields.total - b.sdps.shields.total, desc); break; + case 'es': comp = comp((a, b) => a.effectiveness.shields.total - b.effectiveness.shields.total, desc); break; + case 'esdpsh': comp = comp((a, b) => a.sdps.armour.total - b.sdps.armour.total, desc); break; + case 'eh': comp = comp((a, b) => a.effectiveness.armour.total - b.effectiveness.armour.total, desc); break; + } + + this.state.damage.sort(comp); + } + + /** + * Render offence + * @return {React.Component} contents + */ + render() { + const { ship, opponent, wep, engagementrange } = this.props; + const { language, tooltip, termtip } = this.context; + const { formats, translate, units } = language; + const { damage } = this.state; + const sortOrder = this._sortOrder; + + const pd = ship.standard[4].m; + + const opponentShields = Calc.shieldMetrics(opponent, 4); + const opponentArmour = Calc.armourMetrics(opponent); + + const timeToDrain = Calc.timeToDrainWep(ship, wep); + + let absoluteShieldsSDps = 0; + let explosiveShieldsSDps = 0; + let kineticShieldsSDps = 0; + let thermalShieldsSDps = 0; + let absoluteArmourSDps = 0; + let explosiveArmourSDps = 0; + let kineticArmourSDps = 0; + let thermalArmourSDps = 0; + + let totalSEps = 0; + + const rows = []; + for (let i = 0; i < damage.length; i++) { + const weapon = damage[i]; + + totalSEps += weapon.seps; + absoluteShieldsSDps += weapon.sdps.shields.absolute; + explosiveShieldsSDps += weapon.sdps.shields.explosive; + kineticShieldsSDps += weapon.sdps.shields.kinetic; + thermalShieldsSDps += weapon.sdps.shields.thermal; + absoluteArmourSDps += weapon.sdps.armour.absolute; + explosiveArmourSDps += weapon.sdps.armour.explosive; + kineticArmourSDps += weapon.sdps.armour.kinetic; + thermalArmourSDps += weapon.sdps.armour.thermal; + + const effectivenessShieldsTooltipDetails = []; + effectivenessShieldsTooltipDetails.push(
    {translate('range') + ' ' + formats.pct1(weapon.effectiveness.shields.range)}
    ); + effectivenessShieldsTooltipDetails.push(
    {translate('resistance') + ' ' + formats.pct1(weapon.effectiveness.shields.resistance)}
    ); + effectivenessShieldsTooltipDetails.push(
    {translate('power distributor') + ' ' + formats.pct1(weapon.effectiveness.shields.sys)}
    ); + + const effectiveShieldsSDpsTooltipDetails = []; + if (weapon.sdps.shields.absolute) effectiveShieldsSDpsTooltipDetails.push(
    {translate('absolute') + ' ' + formats.f1(weapon.sdps.shields.absolute)}
    ); + if (weapon.sdps.shields.explosive) effectiveShieldsSDpsTooltipDetails.push(
    {translate('explosive') + ' ' + formats.f1(weapon.sdps.shields.explosive)}
    ); + if (weapon.sdps.shields.kinetic) effectiveShieldsSDpsTooltipDetails.push(
    {translate('kinetic') + ' ' + formats.f1(weapon.sdps.shields.kinetic)}
    ); + if (weapon.sdps.shields.thermal) effectiveShieldsSDpsTooltipDetails.push(
    {translate('thermal') + ' ' + formats.f1(weapon.sdps.shields.thermal)}
    ); + + const effectivenessArmourTooltipDetails = []; + effectivenessArmourTooltipDetails.push(
    {translate('range') + ' ' + formats.pct1(weapon.effectiveness.armour.range)}
    ); + effectivenessArmourTooltipDetails.push(
    {translate('resistance') + ' ' + formats.pct1(weapon.effectiveness.armour.resistance)}
    ); + effectivenessArmourTooltipDetails.push(
    {translate('hardness') + ' ' + formats.pct1(weapon.effectiveness.armour.hardness)}
    ); + const effectiveArmourSDpsTooltipDetails = []; + if (weapon.sdps.armour.absolute) effectiveArmourSDpsTooltipDetails.push(
    {translate('absolute') + ' ' + formats.f1(weapon.sdps.armour.absolute)}
    ); + if (weapon.sdps.armour.explosive) effectiveArmourSDpsTooltipDetails.push(
    {translate('explosive') + ' ' + formats.f1(weapon.sdps.armour.explosive)}
    ); + if (weapon.sdps.armour.kinetic) effectiveArmourSDpsTooltipDetails.push(
    {translate('kinetic') + ' ' + formats.f1(weapon.sdps.armour.kinetic)}
    ); + if (weapon.sdps.armour.thermal) effectiveArmourSDpsTooltipDetails.push(
    {translate('thermal') + ' ' + formats.f1(weapon.sdps.armour.thermal)}
    ); + + rows.push( + + + {weapon.mount == 'F' ? : null} + {weapon.mount == 'G' ? : null} + {weapon.mount == 'T' ? : null} + {weapon.classRating} {translate(weapon.name)} + {weapon.engineering ? ' (' + weapon.engineering + ')' : null } + + {formats.f1(weapon.sdps.shields.total)} + {formats.pct1(weapon.effectiveness.shields.total)} + {formats.f1(weapon.sdps.armour.total)} + {formats.pct1(weapon.effectiveness.armour.total)} + ); + } + + const totalShieldsSDps = absoluteShieldsSDps + explosiveShieldsSDps + kineticShieldsSDps + thermalShieldsSDps; + const totalArmourSDps = absoluteArmourSDps + explosiveArmourSDps + kineticArmourSDps + thermalArmourSDps; + + const shieldsSDpsData = []; + shieldsSDpsData.push({ value: Math.round(absoluteShieldsSDps), label: translate('absolute') }); + shieldsSDpsData.push({ value: Math.round(explosiveShieldsSDps), label: translate('explosive') }); + shieldsSDpsData.push({ value: Math.round(kineticShieldsSDps), label: translate('kinetic') }); + shieldsSDpsData.push({ value: Math.round(thermalShieldsSDps), label: translate('thermal') }); + + const armourSDpsData = []; + armourSDpsData.push({ value: Math.round(absoluteArmourSDps), label: translate('absolute') }); + armourSDpsData.push({ value: Math.round(explosiveArmourSDps), label: translate('explosive') }); + armourSDpsData.push({ value: Math.round(kineticArmourSDps), label: translate('kinetic') }); + armourSDpsData.push({ value: Math.round(thermalArmourSDps), label: translate('thermal') }); + + const timeToDepleteShields = Calc.timeToDeplete(opponentShields.total, totalShieldsSDps, totalSEps, pd.getWeaponsCapacity(), pd.getWeaponsRechargeRate() * (wep / 4)); + const timeToDepleteArmour = Calc.timeToDeplete(opponentArmour.total, totalArmourSDps, totalSEps, pd.getWeaponsCapacity(), pd.getWeaponsRechargeRate() * (wep / 4)); + + return ( + +
    + + + + + + + + + + + + + + + + {rows} + +
    {translate('weapon')}{translate('opponent\'s shields')}{translate('opponent\'s armour')}
    {'sdps'}{'eft'}{'sdps'}{'eft'}
    +
    +
    +

    {translate('offence metrics')}

    +

    {translate('PHRASE_TIME_TO_DRAIN_WEP')}
    {timeToDrain === Infinity ? translate('never') : formats.time(timeToDrain)}

    +

    {translate('PHRASE_EFFECTIVE_SDPS_SHIELDS')}
    {formats.f1(totalShieldsSDps)}

    +

    {translate('PHRASE_TIME_TO_REMOVE_SHIELDS')}
    {timeToDepleteShields === Infinity ? translate('never') : formats.time(timeToDepleteShields)}

    +

    {translate('PHRASE_EFFECTIVE_SDPS_ARMOUR')}
    {formats.f1(totalArmourSDps)}

    +

    {translate('PHRASE_TIME_TO_REMOVE_ARMOUR')}
    {timeToDepleteArmour === Infinity ? translate('never') : formats.time(timeToDepleteArmour)}

    +
    +
    +

    {translate('shield damage sources')}

    + +
    +
    +

    {translate('armour damage sources')}

    + +
    +
    ); + } +} diff --git a/src/app/components/OffenceSummary.jsx b/src/app/components/OffenceSummary.jsx deleted file mode 100644 index 1aac8b8c..00000000 --- a/src/app/components/OffenceSummary.jsx +++ /dev/null @@ -1,73 +0,0 @@ -import React from 'react'; -import cn from 'classnames'; -import TranslatedComponent from './TranslatedComponent'; -import { DamageAbsolute, DamageKinetic, DamageThermal, DamageExplosive } from './SvgIcons'; - -/** - * Offence summary - */ -export default class OffenceSummary extends TranslatedComponent { - static PropTypes = { - ship: React.PropTypes.object.isRequired - }; - - /** - * Constructor - * @param {Object} props React Component properties - */ - constructor(props) { - super(props); - } - - /** - * Render offence summary - * @return {React.Component} contents - */ - render() { - let ship = this.props.ship; - let { language, tooltip, termtip } = this.context; - let { formats, translate } = language; - - return ( - -

    {translate('offence summary')}

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

    {translate('dps')}: {formats.f1(ship.totalDps)}

    {translate('damage by')} {formats.f1(ship.totalAbsDps)} {formats.f1(ship.totalExplDps)} {formats.f1(ship.totalKinDps)} {formats.f1(ship.totalThermDps)}

    {translate('sdps')}: {formats.f1(ship.totalSDps)}

    {translate('damage by')} {formats.f1(ship.totalAbsSDps)} {formats.f1(ship.totalExplSDps)} {formats.f1(ship.totalKinSDps)} {formats.f1(ship.totalThermSDps)}

    {translate('dpe')}: {formats.f1(ship.totalDpe)}

    {translate('damage by')} {formats.f1(ship.totalAbsDpe)} {formats.f1(ship.totalExplDpe)} {formats.f1(ship.totalKinDpe)} {formats.f1(ship.totalThermDpe)}
    -
    - ); - } -} diff --git a/src/app/components/OutfittingSubpages.jsx b/src/app/components/OutfittingSubpages.jsx new file mode 100644 index 00000000..e849ecfe --- /dev/null +++ b/src/app/components/OutfittingSubpages.jsx @@ -0,0 +1,186 @@ +import React from 'react'; +import cn from 'classnames'; +import { Ships } from 'coriolis-data/dist'; +import Ship from '../shipyard/Ship'; +import Persist from '../stores/Persist'; +import TranslatedComponent from './TranslatedComponent'; +import PowerManagement from './PowerManagement'; +import CostSection from './CostSection'; +import EngineProfile from './EngineProfile'; +import FSDProfile from './FSDProfile'; +import Movement from './Movement'; +import Offence from './Offence'; +import Defence from './Defence'; +import WeaponDamageChart from './WeaponDamageChart'; + +/** + * Outfitting subpages + */ +export default class OutfittingSubpages extends TranslatedComponent { + + static propTypes = { + ship: React.PropTypes.object.isRequired, + code: React.PropTypes.string.isRequired, + onChange: React.PropTypes.func.isRequired, + buildName: React.PropTypes.string, + sys: React.PropTypes.number.isRequired, + eng: React.PropTypes.number.isRequired, + wep: React.PropTypes.number.isRequired, + cargo: React.PropTypes.number.isRequired, + fuel: React.PropTypes.number.isRequired, + boost: React.PropTypes.bool.isRequired, + engagementRange: React.PropTypes.number.isRequired, + opponent: React.PropTypes.object.isRequired, + opponentBuild: React.PropTypes.string, + opponentSys: React.PropTypes.number.isRequired, + opponentEng: React.PropTypes.number.isRequired, + opponentWep: React.PropTypes.number.isRequired, + }; + + /** + * Constructor + * @param {Object} props React Component properties + */ + constructor(props) { + super(props); + this._powerTab = this._powerTab.bind(this); + this._profilesTab = this._profilesTab.bind(this); + this._offenceTab = this._offenceTab.bind(this); + this._defenceTab = this._defenceTab.bind(this); + + this.state = { + tab: Persist.getOutfittingTab() || 'power', + }; + } + + /** + * Show selected tab + * @param {string} tab Tab name + */ + _showTab(tab) { + this.setState({ tab }); + } + + /** + * Render the power tab + * @return {React.Component} Tab contents + */ + _powerTab() { + let { ship, buildName, code, onChange } = this.props; + Persist.setOutfittingTab('power'); + + const powerMarker = `${ship.toString()}`; + const costMarker = `${ship.toString().split('.')[0]}`; + + return
    + + +
    ; + } + + /** + * Render the profiles tab + * @return {React.Component} Tab contents + */ + _profilesTab() { + const { ship, opponent, cargo, fuel, eng, boost, engagementRange, opponentSys } = this.props; + const { translate } = this.context.language; + let realBoost = boost && ship.canBoost(cargo, fuel); + Persist.setOutfittingTab('profiles'); + + const engineProfileMarker = `${ship.toString()}:${cargo}:${fuel}:${eng}:${realBoost}`; + const fsdProfileMarker = `${ship.toString()}:${cargo}:${fuel}`; + const movementMarker = `${ship.topSpeed}:${ship.pitch}:${ship.roll}:${ship.yaw}:${ship.canBoost(cargo, fuel)}`; + const damageMarker = `${ship.toString()}:${opponent.toString()}:${engagementRange}:${opponentSys}`; + + return
    +
    +

    {translate('engine profile')}

    + +
    + +
    +

    {translate('fsd profile')}

    + +
    + +
    +

    {translate('movement profile')}

    + +
    + +
    +

    {translate('damage to opponent\'s shields')}

    + +
    + +
    +

    {translate('damage to opponent\'s hull')}

    + +
    +
    ; + } + + /** + * Render the offence tab + * @return {React.Component} Tab contents + */ + _offenceTab() { + const { ship, sys, eng, wep, cargo, fuel, boost, engagementRange, opponent, opponentBuild, opponentSys } = this.props; + Persist.setOutfittingTab('offence'); + + const marker = `${ship.toString()}${opponent.toString()}${opponentBuild}${engagementRange}${opponentSys}`; + + return
    + +
    ; + } + + /** + * Render the defence tab + * @return {React.Component} Tab contents + */ + _defenceTab() { + const { ship, sys, eng, wep, cargo, fuel, boost, engagementRange, opponent, opponentBuild, opponentWep } = this.props; + Persist.setOutfittingTab('defence'); + + const marker = `${ship.toString()}${opponent.toString()}{opponentBuild}${engagementRange}${opponentWep}`; + + return
    + +
    ; + } + + /** + * Render the section + * @return {React.Component} Contents + */ + render() { + const tab = this.state.tab; + const translate = this.context.language.translate; + let tabSection; + + switch (tab) { + case 'power': tabSection = this._powerTab(); break; + case 'profiles': tabSection = this._profilesTab(); break; + case 'offence': tabSection = this._offenceTab(); break; + case 'defence': tabSection = this._defenceTab(); break; + } + + return ( +
    + + + + + + + + + +
    {translate('power and costs')}{translate('profiles')}{translate('offence')}{translate('defence')}
    + {tabSection} +
    + ); + } +} diff --git a/src/app/components/PieChart.jsx b/src/app/components/PieChart.jsx new file mode 100644 index 00000000..c5e5435e --- /dev/null +++ b/src/app/components/PieChart.jsx @@ -0,0 +1,96 @@ +import React, { Component } from 'react'; +import Measure from 'react-measure'; +import * as d3 from 'd3'; + +const CORIOLIS_COLOURS = ['#FF8C0D', '#1FB0FF', '#71A052', '#D5D54D']; +const LABEL_COLOUR = '#000000'; + +/** + * A pie chart + */ +export default class PieChart extends Component { + + static propTypes = { + data : React.PropTypes.array.isRequired + }; + + /** + * Constructor + * @param {Object} props React Component properties + * @param {Object} context React Component context + */ + constructor(props, context) { + super(props); + + this.pie = d3.pie().value((d) => d.value); + this.colors = CORIOLIS_COLOURS; + this.arc = d3.arc(); + this.arc.innerRadius(0); + + this.state = { + dimensions: { + width: 100, + height: 100 + } + }; + } + + + /** + * Generate a slice of the pie chart + * @param {Object} d the data for this slice + * @param {number} i the index of this slice + * @returns {Object} the SVG for the slice + */ + sliceGenerator(d, i) { + if (!d || d.value == 0) { + // Ignore 0 values + return null; + } + + const { width, height } = this.state.dimensions; + const { data } = this.props; + + // Push the labels further out from the centre of the slice + let [labelX, labelY] = this.arc.centroid(d); + const labelTranslate = `translate(${labelX * 1.5}, ${labelY * 1.5})`; + + // Put the keys in a line with equal spacing + const nonZeroItems = data.filter(d => d.value != 0).length; + const thisItemIndex = data.slice(0, i + 1).filter(d => d.value != 0).length - 1; + const keyX = -width / 2 + (width / nonZeroItems) * (thisItemIndex + 0.5); + const keyTranslate = `translate(${keyX}, ${width * 0.45})`; + + return ( + + + {d.value} + {d.data.label} + + ); + } + + /** + * Render the component + * @returns {object} Markup + */ + render() { + const { width, height } = this.state.dimensions; + const pie = this.pie(this.props.data), + translate = `translate(${width / 2}, ${width * 0.4})`; + + this.arc.outerRadius(width * 0.4); + + return ( + { this.setState({ dimensions }); }}> +
    + + + {pie.map((d, i) => this.sliceGenerator(d, i))} + + +
    +
    + ); + } +} diff --git a/src/app/components/Pips.jsx b/src/app/components/Pips.jsx new file mode 100644 index 00000000..08102c40 --- /dev/null +++ b/src/app/components/Pips.jsx @@ -0,0 +1,298 @@ +import React from 'react'; +import TranslatedComponent from './TranslatedComponent'; +import { Ships } from 'coriolis-data/dist'; +import { nameComparator } from '../utils/SlotFunctions'; +import { Pip } from './SvgIcons'; +import LineChart from '../components/LineChart'; +import Slider from '../components/Slider'; +import * as ModuleUtils from '../shipyard/ModuleUtils'; +import Module from '../shipyard/Module'; + +/** + * Pips displays SYS/ENG/WEP pips and allows users to change them with key presses by clicking on the relevant area. + * Requires an onChange() function of the form onChange(sys, eng, wep) which is triggered whenever the pips change. + */ +export default class Pips extends TranslatedComponent { + static propTypes = { + sys: React.PropTypes.number.isRequired, + eng: React.PropTypes.number.isRequired, + wep: React.PropTypes.number.isRequired, + onChange: React.PropTypes.func.isRequired + }; + + /** + * Constructor + * @param {Object} props React Component properties + * @param {Object} context React Component context + */ + constructor(props, context) { + super(props); + const { sys, eng, wep } = props; + + this._keyDown = this._keyDown.bind(this); + } + + /** + * Add listeners after mounting + */ + componentDidMount() { + document.addEventListener('keydown', this._keyDown); + } + + /** + * Remove listeners before unmounting + */ + componentWillUnmount() { + document.removeEventListener('keydown', this._keyDown); + } + + /** + * Handle Key Down + * @param {Event} e Keyboard Event + */ + _keyDown(e) { + if (e.ctrlKey || e.metaKey) { // CTRL/CMD + switch (e.keyCode) { + case 37: // Left arrow == increase SYS + e.preventDefault(); + this._incSys(); + break; + case 38: // Up arrow == increase ENG + e.preventDefault(); + this._incEng(); + break; + case 39: // Right arrow == increase WEP + e.preventDefault(); + this._incWep(); + break; + case 40: // Down arrow == reset + e.preventDefault(); + this._reset(); + break; + } + } + } + + /** + * Handle a click + * @param {string} which Which item was clicked + */ + onClick(which) { + if (which == 'SYS') { + this._incSys(); + } else if (which == 'ENG') { + this._incEng(); + } else if (which == 'WEP') { + this._incWep(); + } else if (which == 'RST') { + this._reset(); + } + } + + /** + * Reset the capacitor + */ + _reset() { + let { sys, eng, wep } = this.props; + if (sys != 2 || eng != 2 || wep != 2) { + sys = eng = wep = 2; + this.props.onChange(sys, eng, wep); + } + } + + /** + * Increment the SYS capacitor + */ + _incSys() { + let { sys, eng, wep } = this.props; + + const required = Math.min(1, 4 - sys); + if (required > 0) { + if (required == 0.5) { + // Take from whichever is larger + if (eng > wep) { + eng -= 0.5; + sys += 0.5; + } else { + wep -= 0.5; + sys += 0.5; + } + } else { + // Required is 1 - take from both if possible + if (eng == 0) { + wep -= 1; + sys += 1; + } else if (wep == 0) { + eng -= 1; + sys += 1; + } else { + eng -= 0.5; + wep -= 0.5; + sys += 1; + } + } + this.props.onChange(sys, eng, wep); + } + } + + /** + * Increment the ENG capacitor + */ + _incEng() { + let { sys, eng, wep } = this.props; + + const required = Math.min(1, 4 - eng); + if (required > 0) { + if (required == 0.5) { + // Take from whichever is larger + if (sys > wep) { + sys -= 0.5; + eng += 0.5; + } else { + wep -= 0.5; + eng += 0.5; + } + } else { + // Required is 1 - take from both if possible + if (sys == 0) { + wep -= 1; + eng += 1; + } else if (wep == 0) { + sys -= 1; + eng += 1; + } else { + sys -= 0.5; + wep -= 0.5; + eng += 1; + } + } + this.props.onChange(sys, eng, wep); + } + } + + /** + * Increment the WEP capacitor + */ + _incWep() { + let { sys, eng, wep } = this.props; + + const required = Math.min(1, 4 - wep); + if (required > 0) { + if (required == 0.5) { + // Take from whichever is larger + if (sys > eng) { + sys -= 0.5; + wep += 0.5; + } else { + eng -= 0.5; + wep += 0.5; + } + } else { + // Required is 1 - take from both if possible + if (sys == 0) { + eng -= 1; + wep += 1; + } else if (eng == 0) { + sys -= 1; + wep += 1; + } else { + sys -= 0.5; + eng -= 0.5; + wep += 1; + } + } + this.props.onChange(sys, eng, wep); + } + } + + /** + * Set up the rendering for pips + * @param {int} sys the SYS pips + * @param {int} eng the ENG pips + * @param {int} wep the WEP pips + * @returns {Object} Object containing the rendering for the pips + */ + _renderPips(sys, eng, wep) { + const pipsSvg = {}; + + // SYS + pipsSvg['SYS'] = []; + for (let i = 0; i < Math.floor(sys); i++) { + pipsSvg['SYS'].push(); + } + if (sys > Math.floor(sys)) { + pipsSvg['SYS'].push(); + } + for (let i = Math.floor(sys + 0.5); i < 4; i++) { + pipsSvg['SYS'].push(); + } + + // ENG + pipsSvg['ENG'] = []; + for (let i = 0; i < Math.floor(eng); i++) { + pipsSvg['ENG'].push(); + } + if (eng > Math.floor(eng)) { + pipsSvg['ENG'].push(); + } + for (let i = Math.floor(eng + 0.5); i < 4; i++) { + pipsSvg['ENG'].push(); + } + + // WEP + pipsSvg['WEP'] = []; + for (let i = 0; i < Math.floor(wep); i++) { + pipsSvg['WEP'].push(); + } + if (wep > Math.floor(wep)) { + pipsSvg['WEP'].push(); + } + for (let i = Math.floor(wep + 0.5); i < 4; i++) { + pipsSvg['WEP'].push(); + } + + return pipsSvg; + } + + /** + * Render pips + * @return {React.Component} contents + */ + render() { + const { formats, translate, units } = this.context.language; + const { sys, eng, wep } = this.props; + + const onSysClicked = this.onClick.bind(this, 'SYS'); + const onEngClicked = this.onClick.bind(this, 'ENG'); + const onWepClicked = this.onClick.bind(this, 'WEP'); + const onRstClicked = this.onClick.bind(this, 'RST'); + + const pipsSvg = this._renderPips(sys, eng, wep); + return ( + + + + + + + + + + + + + + + + + + + + + + +
      {pipsSvg['ENG']} 
     {pipsSvg['SYS']}{translate('ENG')}{pipsSvg['WEP']}
     {translate('SYS')}{translate('RST')}{translate('WEP')}
    +
    + ); + } +} diff --git a/src/app/components/PowerBands.jsx b/src/app/components/PowerBands.jsx index ad2ebcaf..fc9a1d7a 100644 --- a/src/app/components/PowerBands.jsx +++ b/src/app/components/PowerBands.jsx @@ -189,7 +189,7 @@ export default class PowerBands extends TranslatedComponent { let { f2, pct1 } = formats; // wattFmt, pctFmt let { available, bands } = props; let { innerWidth, ret, dep } = state; - let pwrWarningClass = cn('threshold', { exceeded: bands[0].retractedSum * 2 >= available }); + let pwrWarningClass = cn('threshold', { exceeded: bands[0].retractedSum > available * 0.4 }); let deployed = []; let retracted = []; let retSelected = Object.keys(ret).length > 0; @@ -268,7 +268,7 @@ export default class PowerBands extends TranslatedComponent { axis.call(this.pctAxis); axis.select('g:nth-child(6)').selectAll('line, text').attr('class', pwrWarningClass); }} className='pct axis' transform={`translate(0,${state.innerHeight})`}> - + {translate('ret')} {translate('dep')} {f2(Math.max(0, retSum))} ({pct1(Math.max(0, retSum / available))}) diff --git a/src/app/components/PowerManagement.jsx b/src/app/components/PowerManagement.jsx index e8a81fd0..39e78a19 100644 --- a/src/app/components/PowerManagement.jsx +++ b/src/app/components/PowerManagement.jsx @@ -17,7 +17,7 @@ const POWER = [ * Power Management Section */ export default class PowerManagement extends TranslatedComponent { - static PropTypes = { + static propTypes = { ship: React.PropTypes.object.isRequired, code: React.PropTypes.string.isRequired, onChange: React.PropTypes.func.isRequired diff --git a/src/app/components/ShipPicker.jsx b/src/app/components/ShipPicker.jsx new file mode 100644 index 00000000..93505b8d --- /dev/null +++ b/src/app/components/ShipPicker.jsx @@ -0,0 +1,126 @@ +import React from 'react'; +import TranslatedComponent from './TranslatedComponent'; +import Ship from '../shipyard/Ship'; +import { Ships } from 'coriolis-data/dist'; +import { Rocket } from './SvgIcons'; +import Persist from '../stores/Persist'; +import cn from 'classnames'; + +/** + * Ship picker + * Requires an onChange() function of the form onChange(ship), providing the ship, which is triggered on ship change + */ +export default class ShipPicker extends TranslatedComponent { + static propTypes = { + onChange: React.PropTypes.func.isRequired, + ship: React.PropTypes.string.isRequired, + build: React.PropTypes.string + }; + + static defaultProps = { + ship: 'eagle' + } + + /** + * Constructor + * @param {Object} props React Component properties + * @param {Object} context React Component context + */ + constructor(props, context) { + super(props); + + this.shipOrder = Object.keys(Ships).sort(); + this._toggleMenu = this._toggleMenu.bind(this); + this._closeMenu = this._closeMenu.bind(this); + + this.state = { menuOpen: false }; + } + + /** + * Update ship + * @param {object} ship the ship + * @param {string} build the build, if present + */ + _shipChange(ship, build) { + this._closeMenu(); + + // Ensure that the ship has changed + if (ship !== this.props.ship || build !== this.props.build) { + this.props.onChange(ship, build); + } + } + + /** + * Render the menu for the picker + * @returns {object} the picker menu + */ + _renderPickerMenu() { + const { ship, build } = this.props; + const _shipChange = this._shipChange; + const builds = Persist.getBuilds(); + const buildList = []; + for (let shipId of this.shipOrder) { + const shipBuilds = []; + // Add stock build + const stockSelected = (ship == shipId && !build); + shipBuilds.push(
  • Stock
  • ); + if (builds[shipId]) { + let buildNameOrder = Object.keys(builds[shipId]).sort(); + for (let buildName of buildNameOrder) { + const buildSelected = ship === shipId && build === buildName; + shipBuilds.push(
  • {buildName}
  • ); + } + } + buildList.push(
      {Ships[shipId].properties.name}{shipBuilds}
    ); + } + + return buildList; + } + + /** + * Toggle the menu state + */ + _toggleMenu() { + const { menuOpen } = this.state; + this.setState({ menuOpen: !menuOpen }); + } + + /** + * Close the menu + */ + _closeMenu() { + const { menuOpen } = this.state; + if (menuOpen) { + this._toggleMenu(); + } + } + + /** + * Render picker + * @return {React.Component} contents + */ + render() { + const { language, onWindowResize, sizeRatio, tooltip, termtip } = this.context; + const { formats, translate, units } = language; + const { ship, build } = this.props; + const { menuOpen } = this.state; + + const shipString = ship + ': ' + (build ? build : translate('stock')); + return ( +
    e.stopPropagation() }> +
    +
    + + {shipString} +
    + { menuOpen ? +
    e.stopPropagation() }> +
    + {this._renderPickerMenu()} +
    +
    : null } +
    +
    + ); + } +} diff --git a/src/app/components/ShipSelector.jsx b/src/app/components/ShipSelector.jsx deleted file mode 100644 index 34f11786..00000000 --- a/src/app/components/ShipSelector.jsx +++ /dev/null @@ -1,89 +0,0 @@ -import React from 'react'; -import cn from 'classnames'; -import { Ships } from 'coriolis-data/dist'; -import TranslatedComponent from './TranslatedComponent'; -import { Rocket } from './SvgIcons'; - -/** - * Selector for ships - */ -export default class ShipSelector extends TranslatedComponent { - static PropTypes = { - initial: React.PropTypes.object.isRequired, - onChange: React.PropTypes.func.isRequired - }; - - /** - * Constructor - * @param {Object} props React Component properties - */ - constructor(props) { - super(props); - - this.state = { ship : this.props.initial }; - } - - /** - * Generate the ships menu - * @return {React.Component} Menu - */ - _getShipsMenu() { - const _selectShip = this._selectShip; - - let shipList = []; - - for (let s in Ships) { - shipList.push(
    {Ships[s].properties.name}
    ); - } - - return shipList; - } - - /** - * Handle opening the menu - * @param {string} menu The ID of the opened menu - * @param {SyntheticEvent} event Event - */ - _openMenu(menu, event) { - event.stopPropagation(); - if (this.props.currentMenu == menu) { - menu = null; - } - - this.context.openMenu(menu); - } - - /** - * Handle selection of a ship - * @param {string} s The selected ship ID - */ - _selectShip(s) { - this.setState({ ship: Ships[s] }); - - this.context.openMenu(null); - this.props.onChange(s); - } - - /** - * Render ship selector - * @return {React.Component} contents - */ - render() { - const currentMenu = this.props.currentMenu; - const ship = this.state.ship; - - return ( -
    -
    -
    - {ship.properties.name} - {currentMenu == 'wds' ? -
    e.stopPropagation() }> - {this._getShipsMenu()} -
    : null } -
    -
    -
    - ); - } -} diff --git a/src/app/components/ShipSummaryTable.jsx b/src/app/components/ShipSummaryTable.jsx index 651f9a9f..881a3b25 100644 --- a/src/app/components/ShipSummaryTable.jsx +++ b/src/app/components/ShipSummaryTable.jsx @@ -2,6 +2,7 @@ import React from 'react'; import TranslatedComponent from './TranslatedComponent'; import cn from 'classnames'; import { Warning } from './SvgIcons'; +import * as Calc from '../shipyard/Calculations'; /** * Ship Summary Table / Stats @@ -9,7 +10,10 @@ import { Warning } from './SvgIcons'; export default class ShipSummaryTable extends TranslatedComponent { static propTypes = { - ship: React.PropTypes.object.isRequired + ship: React.PropTypes.object.isRequired, + cargo: React.PropTypes.number.isRequired, + fuel: React.PropTypes.number.isRequired, + marker: React.PropTypes.string.isRequired, }; /** @@ -17,77 +21,75 @@ export default class ShipSummaryTable extends TranslatedComponent { * @return {React.Component} Summary table */ render() { - let ship = this.props.ship; + const { ship, cargo, fuel } = this.props; let { language, tooltip, termtip } = this.context; let translate = language.translate; let u = language.units; let formats = language.formats; let { time, int, round, f1, f2 } = formats; - let sgClassNames = cn({ warning: ship.findInternalByGroup('sg') && !ship.shield, muted: !ship.findInternalByGroup('sg') }); - let sgRecover = '-'; - let sgRecharge = '-'; let hide = tooltip.bind(null, null); - if (ship.shield) { - sgRecover = time(ship.calcShieldRecovery()); - sgRecharge = time(ship.calcShieldRecharge()); - } + const shieldGenerator = ship.findInternalByGroup('sg'); + const sgClassNames = cn({ warning: shieldGenerator && !ship.shield, muted: !shieldGenerator }); + const sgTooltip = shieldGenerator ? 'TT_SUMMARY_SHIELDS' : 'TT_SUMMARY_SHIELDS_NONFUNCTIONAL'; + const timeToDrain = Calc.timeToDrainWep(ship, 4); + const canThrust = ship.canThrust(cargo, fuel); + const speedTooltip = canThrust ? 'TT_SUMMARY_SPEED' : 'TT_SUMMARY_SPEED_NONFUNCTIONAL'; + const canBoost = ship.canBoost(cargo, fuel); + const boostTooltip = canBoost ? 'TT_SUMMARY_BOOST' : canThrust ? 'TT_SUMMARY_BOOST_NONFUNCTIONAL' : 'TT_SUMMARY_SPEED_NONFUNCTIONAL'; return
    - - - - - - - - - - + + + + + + + + + {/* */} - - + + - + - - - - + - + + + - - - - - - + + + + + + + + + + + + + {/* */} + + + + + - - - - - - - - - - - - - diff --git a/src/app/components/Slot.jsx b/src/app/components/Slot.jsx index 9959a16f..03964d35 100644 --- a/src/app/components/Slot.jsx +++ b/src/app/components/Slot.jsx @@ -79,7 +79,7 @@ export default class Slot extends TranslatedComponent { let language = this.context.language; let translate = language.translate; let { ship, m, dropClass, dragOver, onOpen, onChange, selected, eligible, onSelect, warning, availableModules } = this.props; - let slotDetails, menu; + let slotDetails, modificationsMarker, menu; if (!selected) { // If not selected then sure that modifications flag is unset @@ -88,8 +88,10 @@ export default class Slot extends TranslatedComponent { if (m) { slotDetails = this._getSlotDetails(m, translate, language.formats, language.units); // Must be implemented by sub classes + modificationsMarker = JSON.stringify(m); } else { slotDetails =
    {translate(eligible ? 'emptyrestricted' : 'empty')}
    ; + modificationsMarker = ''; } if (selected) { @@ -99,6 +101,7 @@ export default class Slot extends TranslatedComponent { onChange={onChange} ship={ship} m={m} + marker={modificationsMarker} />; } else { menu = +
    {modTT}
    + {blueprintTooltip(translate, m.blueprint.grades[m.blueprint.grade], null, m.grp, m)} + + ); } if (!selected) { @@ -60,6 +68,8 @@ export default class StandardSlot extends TranslatedComponent { this._modificationsSelected = false; } + const modificationsMarker = JSON.stringify(m); + if (selected) { if (this._modificationsSelected) { menu = ; } else { menu = {translate('minimum mass')}: {formats.int(m.getMinMass())}{units.T} : null } { m.getOptMass() ?
    {translate('optimal mass')}: {formats.int(m.getOptMass())}{units.T}
    : null } { m.getMaxMass() ?
    {translate('max mass')}: {formats.int(m.getMaxMass())}{units.T}
    : null } - { m.getRange() ?
    {translate('range')}: {formats.f2(m.getRange())}{units.km}
    : null } + { m.getOptMul() ?
    {translate('optimal multiplier')}: {formats.rPct(m.getOptMul())}
    : null } + { m.getRange() ?
    {translate('range', m.grp)}: {formats.f2(m.getRange())}{units.km}
    : null } { m.time ?
    {translate('time')}: {formats.time(m.time)}
    : null } { m.getThermalEfficiency() ?
    {translate('efficiency')}: {formats.f2(m.getThermalEfficiency())}
    : null } { m.getPowerGeneration() > 0 ?
    {translate('pgen')}: {formats.f1(m.getPowerGeneration())}{units.MW}
    : null } @@ -104,7 +116,7 @@ export default class StandardSlot extends TranslatedComponent { { showModuleResistances && m.getExplosiveResistance() ?
    {translate('explres')}: {formats.pct(m.getExplosiveResistance())}
    : null } { showModuleResistances && m.getKineticResistance() ?
    {translate('kinres')}: {formats.pct(m.getKineticResistance())}
    : null } { showModuleResistances && m.getThermalResistance() ?
    {translate('thermres')}: {formats.pct(m.getThermalResistance())}
    : null } - + { m.getIntegrity() ?
    {translate('integrity')}: {formats.int(m.getIntegrity())}
    : null } { validMods.length > 0 ?
    : null } diff --git a/src/app/components/StandardSlotSection.jsx b/src/app/components/StandardSlotSection.jsx index 54cdf08e..f57edd90 100644 --- a/src/app/components/StandardSlotSection.jsx +++ b/src/app/components/StandardSlotSection.jsx @@ -95,7 +95,7 @@ export default class StandardSlotSection extends SlotSection { * @return {Array} Array of Slots */ _getSlots() { - let { ship, currentMenu } = this.props; + let { ship, currentMenu, cargo, fuel } = this.props; let slots = new Array(8); let open = this._openMenu; let select = this._selectModule; @@ -135,7 +135,7 @@ export default class StandardSlotSection extends SlotSection { selected={currentMenu == st[1]} onChange={this.props.onChange} ship={ship} - warning={m => m instanceof Module ? m.getMaxMass() < (ship.ladenMass - st[1].mass + m.mass) : m.maxmass < (ship.ladenMass - st[1].mass + m.mass)} + warning={m => m instanceof Module ? m.getMaxMass() < (ship.unladenMass + cargo + fuel - st[1].m.mass + m.mass) : m.maxmass < (ship.unladenMass + cargo + fuel - st[1].m.mass + m.mass)} />; @@ -170,7 +170,7 @@ export default class StandardSlotSection extends SlotSection { selected={currentMenu == st[4]} onChange={this.props.onChange} ship={ship} - warning={m => m instanceof Module ? m.getEnginesCapacity() < ship.boostEnergy : m.engcap < ship.boostEnergy} + warning={m => m instanceof Module ? m.getEnginesCapacity() <= ship.boostEnergy : m.engcap <= ship.boostEnergy} />; slots[6] = ; + } +} + /** * In-game Coriolis Station logo */ diff --git a/src/app/components/VerticalBarChart.jsx b/src/app/components/VerticalBarChart.jsx new file mode 100644 index 00000000..1f2ecb94 --- /dev/null +++ b/src/app/components/VerticalBarChart.jsx @@ -0,0 +1,107 @@ +import TranslatedComponent from './TranslatedComponent'; +import React, { PropTypes } from 'react'; +import Measure from 'react-measure'; +import { BarChart, Bar, XAxis, YAxis } from 'recharts'; + +const CORIOLIS_COLOURS = ['#FF8C0D', '#1FB0FF', '#71A052', '#D5D54D']; +const LABEL_COLOUR = '#000000'; +const AXIS_COLOUR = '#C06400'; + +const ASPECT = 1; + +const merge = function(one, two) { + return Object.assign({}, one, two); +}; + +/** + * A vertical bar chart + */ +export default class VerticalBarChart extends TranslatedComponent { + + static propTypes = { + data : PropTypes.array.isRequired, + yMax : PropTypes.number + }; + + /** + * Constructor + * @param {Object} props React Component properties + * @param {Object} context React Component context + */ + constructor(props, context) { + super(props); + + this._termtip = this._termtip.bind(this); + + this.state = { + dimensions: { + width: 300, + height: 300 + } + }; + } + + /** + * Render the bar chart + * @returns {Object} the markup + */ + render() { + const { width, height } = this.state.dimensions; + const { tooltip, termtip } = this.context; + + // Calculate maximum for Y + let dataMax = Math.max(...this.props.data.map(d => d.value)); + if (dataMax == -Infinity) dataMax = 0; + let yMax = this.props.yMax ? Math.round(this.props.yMax) : 0; + const localMax = Math.max(dataMax, yMax); + + return ( + this.setState({ dimensions }) }> +
    + + + + } fill={CORIOLIS_COLOURS[0]} isAnimationActive={false} onMouseOver={this._termtip} onMouseOut={tooltip.bind(null, null)}/> + +
    +
    + ); + } + + /** + * Generate a term tip + * @param {Object} d the data + * @param {number} i the index + * @param {Object} e the event + * @returns {Object} termtip markup + */ + _termtip(d, i, e) { + if (this.props.data[i].tooltip) { + return this.context.termtip(this.props.data[i].tooltip, e); + } else { + return null; + } + } +} + +/** + * A label that displays the value within the bar of the chart + */ +const ValueLabel = React.createClass({ + propTypes: { + x: PropTypes.number, + y: PropTypes.number, + payload: PropTypes.object, + value: PropTypes.number + }, + + render() { + const { x, y, payload, value } = this.props; + + const em = value < 1000 ? '1em' : value < 1000 ? '0.8em' : '0.7em'; + + return ( + {value} + ); + } +}); diff --git a/src/app/components/WeaponDamageChart.jsx b/src/app/components/WeaponDamageChart.jsx new file mode 100644 index 00000000..a2c407b0 --- /dev/null +++ b/src/app/components/WeaponDamageChart.jsx @@ -0,0 +1,203 @@ +import React from 'react'; +import TranslatedComponent from './TranslatedComponent'; +import { Ships } from 'coriolis-data/dist'; +import { nameComparator } from '../utils/SlotFunctions'; +import { CollapseSection, ExpandSection, MountFixed, MountGimballed, MountTurret } from './SvgIcons'; +import LineChart from '../components/LineChart'; +import Slider from '../components/Slider'; +import * as Calc from '../shipyard/Calculations'; +import Module from '../shipyard/Module'; + +const DAMAGE_DEALT_COLORS = ['#FFFFFF', '#FF0000', '#00FF00', '#7777FF', '#FFFF00', '#FF00FF', '#00FFFF', '#777777']; + +/** + * Weapon damage chart + */ +export default class WeaponDamageChart extends TranslatedComponent { + static propTypes = { + ship: React.PropTypes.object.isRequired, + opponent: React.PropTypes.object.isRequired, + hull: React.PropTypes.bool.isRequired, + engagementRange: React.PropTypes.number.isRequired, + opponentSys: React.PropTypes.number.isRequired, + marker: React.PropTypes.string.isRequired + }; + + /** + * Constructor + * @param {Object} props React Component properties + * @param {Object} context React Component context + */ + constructor(props, context) { + super(props); + } + + /** + * Set the initial weapons state + */ + componentWillMount() { + const weaponNames = this._weaponNames(this.props.ship, this.context); + const opponentShields = Calc.shieldMetrics(this.props.opponent, this.props.opponentSys); + const opponentArmour = Calc.armourMetrics(this.props.opponent); + const maxRange = this._calcMaxRange(this.props.ship); + const maxDps = this._calcMaxSDps(this.props.ship, this.props.opponent, opponentShields, opponentArmour); + + this.setState({ maxRange, maxDps, weaponNames, opponentShields, opponentArmour, calcSDpsFunc: this._calcSDps.bind(this, this.props.ship, weaponNames, this.props.opponent, opponentShields, opponentArmour, this.props.hull) }); + } + + /** + * Set the updated weapons state if our ship changes + * @param {Object} nextProps Incoming/Next properties + * @param {Object} nextContext Incoming/Next conext + * @return {boolean} Returns true if the component should be rerendered + */ + componentWillReceiveProps(nextProps, nextContext) { + if (nextProps.marker != this.props.marker) { + const weaponNames = this._weaponNames(nextProps.ship, nextContext); + const opponentShields = Calc.shieldMetrics(nextProps.opponent, nextProps.opponentSys); + const opponentArmour = Calc.armourMetrics(nextProps.opponent); + const maxRange = this._calcMaxRange(nextProps.ship); + const maxDps = this._calcMaxSDps(nextProps.ship, nextProps.opponent, opponentShields, opponentArmour); + this.setState({ weaponNames, + opponentShields, + opponentArmour, + maxRange, + maxDps, + calcSDpsFunc: this._calcSDps.bind(this, nextProps.ship, weaponNames, nextProps.opponent, opponentShields, opponentArmour, nextProps.hull) + }); + } + return true; + } + + /** + * Calculate the maximum range of a ship's weapons + * @param {Object} ship The ship + * @returns {int} The maximum range, in metres + */ + _calcMaxRange(ship) { + let maxRange = 1000; // Minimum + for (let i = 0; i < ship.hardpoints.length; i++) { + if (ship.hardpoints[i].maxClass > 0 && ship.hardpoints[i].m && ship.hardpoints[i].enabled) { + const thisRange = ship.hardpoints[i].m.getRange(); + if (thisRange > maxRange) { + maxRange = thisRange; + } + } + } + + return maxRange; + } + + /** + * Calculate the maximum sustained single-weapon DPS for this ship + * @param {Object} ship The ship + * @param {Object} opponent The opponent ship + * @param {Object} opponentShields The opponent's shields + * @param {Object} opponentArmour The opponent's armour + * @return {number} The maximum sustained single-weapon DPS + */ + _calcMaxSDps(ship, opponent, opponentShields, opponentArmour) { + // Additional information to allow effectiveness calculations + let maxSDps = 0; + for (let i = 0; i < ship.hardpoints.length; i++) { + if (ship.hardpoints[i].maxClass > 0 && ship.hardpoints[i].m && ship.hardpoints[i].enabled) { + const m = ship.hardpoints[i].m; + + const sustainedDps = Calc._weaponSustainedDps(m, opponent, opponentShields, opponentArmour, 0); + const thisSDps = sustainedDps.damage.armour.total > sustainedDps.damage.shields.total ? sustainedDps.damage.armour.total : sustainedDps.damage.shields.total; + if (thisSDps > maxSDps) { + maxSDps = thisSDps; + } + } + } + return maxSDps; + } + + /** + * Obtain the weapon names for this ship + * @param {Object} ship The ship + * @param {Object} context The context + * @return {array} The weapon names + */ + _weaponNames(ship, context) { + const translate = context.language.translate; + let names = []; + let num = 1; + for (let i = 0; i < ship.hardpoints.length; i++) { + if (ship.hardpoints[i].maxClass > 0 && ship.hardpoints[i].m && ship.hardpoints[i].enabled) { + const m = ship.hardpoints[i].m; + let name = '' + num++ + ': ' + m.class + m.rating + (m.missile ? '/' + m.missile : '') + ' ' + translate(m.name || m.grp); + let engineering; + if (m.blueprint && m.blueprint.name) { + engineering = translate(m.blueprint.name) + ' ' + translate('grade') + ' ' + m.blueprint.grade; + if (m.blueprint.special && m.blueprint.special.id) { + engineering += ', ' + translate(m.blueprint.special.name); + } + } + if (engineering) { + name = name + ' (' + engineering + ')'; + } + names.push(name); + } + } + return names; + } + + /** + * Calculate the per-weapon sustained DPS for this ship against another ship at a given range + * @param {Object} ship The ship + * @param {Object} weaponNames The names of the weapons for which to calculate DPS + * @param {Object} opponent The target + * @param {Object} opponentShields The opponent's shields + * @param {Object} opponentArmour The opponent's armour + * @param {bool} hull true if to calculate against hull, false if to calculate against shields + * @param {Object} engagementRange The engagement range + * @return {array} The array of weapon DPS + */ + _calcSDps(ship, weaponNames, opponent, opponentShields, opponentArmour, hull, engagementRange) { + let results = {}; + let weaponNum = 0; + for (let i = 0; i < ship.hardpoints.length; i++) { + if (ship.hardpoints[i].maxClass > 0 && ship.hardpoints[i].m && ship.hardpoints[i].enabled) { + const m = ship.hardpoints[i].m; + const sustainedDps = Calc._weaponSustainedDps(m, opponent, opponentShields, opponentArmour, engagementRange); + results[weaponNames[weaponNum++]] = hull ? sustainedDps.damage.armour.total : sustainedDps.damage.shields.total; + } + } + return results; + } + + /** + * Render damage dealt + * @return {React.Component} contents + */ + render() { + const { language, onWindowResize, sizeRatio, tooltip, termtip } = this.context; + const { formats, translate, units } = language; + const { maxRange } = this.state; + const { ship, opponent, engagementRange } = this.props; + + const sortOrder = this._sortOrder; + const onCollapseExpand = this._onCollapseExpand; + + const code = `${ship.toString()}:${opponent.toString()}`; + + return ( + + + + ); + } +} diff --git a/src/app/i18n/Language.jsx b/src/app/i18n/Language.jsx index f22c0d42..e8d81da0 100644 --- a/src/app/i18n/Language.jsx +++ b/src/app/i18n/Language.jsx @@ -58,21 +58,21 @@ export function getLanguage(langCode) { }, translate, units: { - CR: {translate('CR')}, // Credits - kg: {translate('kg')}, // Kilograms - kgs: {translate('kg/s')}, // Kilograms per second - km: {translate('km')}, // Kilometers - Ls: {translate('Ls')}, // Light Seconds - LY: {translate('LY')}, // Light Years - MJ: {translate('MJ')}, // Mega Joules - 'm/s': {translate('m/s')}, // Meters per second - '°/s': {translate('°/s')}, // Degrees per second - MW: {translate('MW')}, // Mega Watts (same as Mega Joules per second) + CR: {translate('CR')}, // Credits + kg: {translate('kg')}, // Kilograms + kgs: {translate('kg/s')}, // Kilograms per second + km: {translate('km')}, // Kilometers + Ls: {translate('Ls')}, // Light Seconds + LY: {translate('LY')}, // Light Years + MJ: {translate('MJ')}, // Mega Joules + 'm/s': {translate('m/s')}, // Meters per second + '°/s': {translate('°/s')}, // Degrees per second + MW: {translate('MW')}, // Mega Watts (same as Mega Joules per second) mps: {translate('m/s')}, // Metres per second ps: {translate('/s')}, // per second pm: {translate('/min')}, // per minute s: {translate('secs')}, // Seconds - T: {translate('T')}, // Metric Tons + T: {translate('T')}, // Metric Tons } }; } diff --git a/src/app/i18n/en.js b/src/app/i18n/en.js index c4dc2fe0..974d6be6 100644 --- a/src/app/i18n/en.js +++ b/src/app/i18n/en.js @@ -24,8 +24,8 @@ export const terms = { PHRASE_NO_BUILDS: 'No builds added to comparison!', PHRASE_NO_RETROCH: 'No Retrofitting changes', PHRASE_SELECT_BUILDS: 'Select builds to compare', - PHRASE_SG_RECHARGE: 'Time from 50% to 100% charge', - PHRASE_SG_RECOVER: 'Recovery (to 50%) after collapse', + PHRASE_SG_RECHARGE: 'Time from 50% to 100% charge, assuming full SYS capacitor to start with', + PHRASE_SG_RECOVER: 'Time from 0% to 50% charge, assuming full SYS capacitor to start with', PHRASE_UNLADEN: 'Ship mass excluding fuel and cargo', PHRASE_UPDATE_RDY: 'Update Available! Click to refresh', PHRASE_ENGAGEMENT_RANGE: 'The distance between your ship and its target', @@ -38,6 +38,59 @@ export const terms = { PHRASE_SELECT_SPECIAL: 'Click to select an experimental effect', PHRASE_NO_SPECIAL: 'No experimental effect', PHRASE_SHOPPING_LIST: 'Stations that sell this build', + PHRASE_TOTAL_EFFECTIVE_SHIELD: 'Total amount of damage that can be taken from each damage type, if using all shield cells', + PHRASE_TIME_TO_LOSE_SHIELDS: 'Shields will hold for', + PHRASE_TIME_TO_RECOVER_SHIELDS: 'Shields will recover in', + PHRASE_TIME_TO_RECHARGE_SHIELDS: 'Shields will recharge in', + PHRASE_SHIELD_SOURCES: 'Breakdown of the supply of shield energy', + PHRASE_EFFECTIVE_SHIELD: 'Effective shield strength against different damage types', + PHRASE_ARMOUR_SOURCES: 'Breakdown of the supply of armour', + PHRASE_EFFECTIVE_ARMOUR: 'Effective armour strength against different damage types', + PHRASE_DAMAGE_TAKEN: '% of raw damage taken for different damage types', + PHRASE_TIME_TO_LOSE_ARMOUR: 'Armour will hold for', + PHRASE_MODULE_PROTECTION_EXTERNAL: 'Protection for hardpoints', + PHRASE_MODULE_PROTECTION_INTERNAL: 'Protection for all other modules', + PHRASE_SHIELD_DAMAGE: 'Breakdown of sources for sustained DPS against shields', + PHRASE_ARMOUR_DAMAGE: 'Breakdown of sources for sustained DPS against armour', + + PHRASE_TIME_TO_REMOVE_SHIELDS: 'Will remove shields in', + TT_TIME_TO_REMOVE_SHIELDS: 'With sustained fire by all weapons', + PHRASE_TIME_TO_REMOVE_ARMOUR: 'Will remove armour in', + TT_TIME_TO_REMOVE_ARMOUR: 'With sustained fire by all weapons', + PHRASE_TIME_TO_DRAIN_WEP: 'Will drain WEP in', + TT_TIME_TO_DRAIN_WEP: 'Time to drain WEP capacitor with all weapons firing', + TT_TIME_TO_LOSE_SHIELDS: 'Against sustained fire from all opponent\'s weapons', + TT_TIME_TO_LOSE_ARMOUR: 'Against sustained fire from all opponent\'s weapons', + TT_MODULE_ARMOUR: 'Armour protecting against module damage', + TT_MODULE_PROTECTION_EXTERNAL: 'Percentage of damage diverted from hardpoints to module reinforcement packages', + TT_MODULE_PROTECTION_INTERNAL: 'Percentage of damage diverted from non-hardpoint modules to module reinforcement packages', + + TT_EFFECTIVE_SDPS_SHIELDS: 'Actual sustained DPS whilst WEP capacitor is not empty', + TT_EFFECTIVENESS_SHIELDS: 'Effectivness compared to hitting a 0-resistance target with 0 pips to SYS at 0m', + TT_EFFECTIVE_SDPS_ARMOUR: 'Actual sustained DPS whilst WEP capacitor is not empty', + TT_EFFECTIVENESS_ARMOUR: 'Effectivness compared to hitting a 0-resistance target at 0m', + + PHRASE_EFFECTIVE_SDPS_SHIELDS: 'SDPS against shields', + PHRASE_EFFECTIVE_SDPS_ARMOUR: 'SDPS against armour', + + TT_SUMMARY_SPEED: 'With full fuel tank and 4 pips to ENG', + TT_SUMMARY_SPEED_NONFUNCTIONAL: 'Thrusters powered off or over maximum mass', + TT_SUMMARY_BOOST: 'With full fuel tank and 4 pips to ENG', + TT_SUMMARY_BOOST_NONFUNCTIONAL: 'Power distributor not able to supply enough power to boost', + TT_SUMMARY_SHIELDS: 'Raw shield strength, including boosters', + TT_SUMMARY_SHIELDS_NONFUNCTIONAL: 'No shield generator or shield generator powered off', + TT_SUMMARY_INTEGRITY: 'Ship integrity, including bulkheads and hull reinforcement packages', + TT_SUMMARY_HULL_MASS: 'Mass of the hull prior to any modules being installed', + TT_SUMMARY_UNLADEN_MASS: 'Mass of the hull and modules prior to any fuel or cargo', + TT_SUMMARY_LADEN_MASS: 'Mass of the hull and modules with full fuel and cargo', + TT_SUMMARY_DPS: 'Damage per second with all weapons firing', + TT_SUMMARY_EPS: 'WEP capacitor consumed per second with all weapons firing', + TT_SUMMARY_TTD: 'Time to drain WEP capacitor with all weapons firing and 4 pips to WEP', + TT_SUMMARY_MAX_SINGLE_JUMP: 'Farthest possible jump range with no cargo and only enough fuel for the jump itself', + TT_SUMMARY_UNLADEN_SINGLE_JUMP: 'Farthest possible jump range with no cargo and a full fuel tank', + TT_SUMMARY_LADEN_SINGLE_JUMP: 'Farthest possible jump range with full cargo and a full fuel tank', + TT_SUMMARY_UNLADEN_TOTAL_JUMP: 'Farthest possible range with no cargo, a full fuel tank, and jumping as far as possible each time', + TT_SUMMARY_LADEN_TOTAL_JUMP: 'Farthest possible range with full cargo, a full fuel tank, and jumping as far as possible each time', HELP_MODIFICATIONS_MENU: 'Click on a number to enter a new value, or drag along the bar for small changes', @@ -88,9 +141,10 @@ export const terms = { rg: 'Rail Gun', s: 'Sensors', sb: 'Shield Booster', - sc: 'Scanner', + sc: 'Stellar Scanners', scb: 'Shield Cell Bank', sg: 'Shield Generator', + ss: 'Surface Scanners', t: 'thrusters', tp: 'Torpedo Pylon', ul: 'Burst Laser', @@ -103,6 +157,8 @@ export const terms = { 'damage received from': 'Damage received from', 'against shields': 'Against shields', 'against hull': 'Against hull', + 'total effective shield': 'Total effective shield', + // 'ammo' was overloaded for outfitting page and modul info, so changed to ammunition for outfitting page ammunition: 'Ammo', @@ -177,6 +233,8 @@ export const terms = { regen: 'Regeneration rate', reload: 'Reload', rof: 'Rate of fire', + angle: 'Scan angle', + scantime: 'Scan time', shield: 'Shield', shieldboost: 'Shield boost', shieldreinforcement: 'Shield reinforcement', @@ -196,6 +254,35 @@ export const terms = { minmul_sg: 'Minimum strength', optmul_sg: 'Optimal strength', maxmul_sg: 'Minimum strength', + minmass_psg: 'Minimum hull mass', + optmass_psg: 'Optimal hull mass', + maxmass_psg: 'Maximum hull mass', + minmul_psg: 'Minimum strength', + optmul_psg: 'Optimal strength', + maxmul_psg: 'Minimum strength', + minmass_bsg: 'Minimum hull mass', + optmass_bsg: 'Optimal hull mass', + maxmass_bsg: 'Maximum hull mass', + minmul_bsg: 'Minimum strength', + optmul_bsg: 'Optimal strength', + maxmul_bsg: 'Minimum strength', + + range_s: 'Typical emission range', + + // Damage types + absolute: 'Absolute', + explosive: 'Explosive', + kinetic: 'Kinetic', + thermal: 'Thermal', + + // Shield sources + generator: 'Generator', + boosters: 'Boosters', + cells: 'Cells', + + // Armour sources + bulkheads: 'Bulkheads', + reinforcement: 'Reinforcement', // Help text HELP_TEXT: ` @@ -227,7 +314,7 @@ Along the top of the screen are some of the key values for your build. This is Here, along with most places in Coriolis, acronyms will have tooltips explaining what they mean. Hover over the acronym to obtain more detail, or look in the glossary at the end of this help.

    -All values are the highest possible, assuming that you have maximum pips in the relevant capacitor (ENG for speed, WEP for time to drain, etc.).

    +All values are the highest possible, assuming that you an optimal setup for that particular value (maximum pips in ENG for speed, minimum fuel for jump range, etc.). This means that these values will not be affected by changes to pip settings. Details of the specific setup for each value are listed in the associated tootip.

    Modules

    The next set of panels laid out horizontally across the screen contain the modules you have put in your build. From left to right these are the core modules, the internal modules, the hardpoints and the utility mounts. These represent the available slots in your ship and cannot be altered. Each slot has a class, or size, and in general any module up to a given size can fit in a given slot (exceptions being bulkheads, life support and sensors in core modules and restricted internal slots, which can only take a subset of module depending on their restrictions).

    @@ -240,30 +327,39 @@ To move a module from one slot to another drag it. If you instead want to copy Clicking on the headings for each set of modules gives you the ability to either select an overall role for your ship (when clicking the core internal header) or a specific module with which you want to fill all applicable slots (when clicking the other headers).

    -

    Offence Summary

    -The offence summary panel provides information about the damage that you deal with your weapons.

    +

    Ship Controls

    +The ship controls allow you to set your pips, boost, and amount of fuel and cargo that your build carries. The changes made here will effect the information supplied in the subsequent panels, giving you a clearer view of what effect different changing these items will have.

    + +Ship control settings are saved as part of a build.

    -The first headline gives an overall damage per second rating: this is the optimum amount of damage the build will do per second according to weapon statistics. After that is a breakdown of the damage per second the build will do for each type of damage: absolute, explosive, kinetic, and thermal.

    +

    Opponent

    +The opponet selection allows you to choose your opponent. The opponent can be either a stock build of a ship or one of your own saved builds. You can also set the engagement range between you and your opponent. Your selection here will effect the information supplied in the subsequent panels, specifically the Offence and Defence panels.

    -The next headline gives an overall sustained damage per second rating: this is the optimum amount of damage the build will do per second over a longer period of time, taking in to account ammunition clip capacities and reload times. After that is a breakdown of the sustained damage per second the build will do for each type of damage: absolute, explosive, kinetic, and thermal.

    +Opponent settings are saved as part of a build.

    -The final headline gives an overall damage per energy rating: this is the amount of damage the build will do per unit of weapon capacitor energy expended. After that is a breakdown of the damage per energy the build will do for each type of damage: absolute, explosive, kinetic, and thermal.

    +

    Power and Costs Sub-panels

    +

    Power

    +The power management panel provides information about power usage and priorities. It allows you to enable and disable individual modules, as well as set power priorities for each module. Disabled modules will not be included in the build's statistics, with the exception of Shield Cell Banks as they are usually disabled when not in use and only enabled when required.

    -

    Defence Summary

    -The defence summary panel provides information about the strength of your defences and the damage that you receive from opponents.

    +

    Costs

    +The costs panel provides information about the costs for each of your modules, and the total cost and insurance for your build. By default Coriolis uses the standard costs, however discounts for your ship, modules and insurance can be altered in the 'Settings' at the top-right of the page.

    + +The retrofit costs provides information about the costs of changing the base build for your ship, or your saved build, to the current build.

    -The first headline gives your total shield strength (if you have shields), taking in to account your base shield plus boosters. After that are the details of how long it will take for your shields to recover from 0 to 50% (recovery time) and from 50% to 100% (recharge time). The next line provides a breakdown of the shield damage taken from different damage types. For example, if you damage from kinetic is 60% then it means that a weapon usually dealing 10 points of damage will only deal 6, the rest being resisted by the shield. Note that this does not include any resistance alterations due to pips in your SYS capacitor.

    +The reload costs provides information about the costs of reloading your current build.

    -The second headline gives your total shield cell strength (if you have shield cell banks). This is the sum of the recharge of all of equipped shield cell banks.

    +

    Profiles

    +Profiles provide graphs that show the general performance of modules in your build -The third headline gives your total armour strength, taking in to account your base armour plus hull reinforcement packages. The next line provides a breakdown of the hull damage taken from different damage types. For example, if you damage from kinetic is 120% then it means that a weapon usually dealing 10 points of damage will deal 12.

    +

    Engine Profile

    +The engine profile panel provides information about the capabilities of your current thrusters. The graph shows you how the maximum speed alters with the overall mass of your build. The vertical dashed line on the graph shows your current mass. Your engine profile can be altered by obtaining different thrusters or engineering your existing thrusters, and you can increase your maximum speed by adding pips to the ENG capacitor as well as reducing the amount of fuel and cargo you are carrying as well as reducing the overall weight of the build. You can also temporarily increase your speed by hitting the boost button.

    -The fourth headline gives your total module protection strength from module reinforcement packages. The next line provides a breakdown of the protection for both internal and external modules whilst all module reinforcement packages are functioning. For example, if external module protection is 20% then 10 points of damage will 2 points of damage to the module reinforcement packages and 8 points of damage to the module

    +

    FSD Profile

    +The FSD profile panel provides information about the capabilities of your current frame shift drive. The graph shows you how the maximum jump range alters with the overall mass of your build. The vertical dashed line on the graph shows your current maximum single jump range. Your FSD profile can be altered by obtaining a different FSD or engineering your existing FSD, and you can increase your maximum jump range by reducing the amount of fuel and cargo you are carrying as well as reducing the overall weight of the build,

    -

    Movement Summary

    -The movement summary panel provides information about the build's speed and agility.

    +

    Movement Profile

    +The movement profile panel provides information about the capabilities of your current thrusters with your current overall mass and ENG pips settings. The diagram shows your ability to move and rotate in the different axes: -Along the top of this panel are the number of pips you put in to your ENG capacitor, from 0 to 4 and also include 4 pips and boost (4b). Along the side of this panel are the names of the metrics. These are:
    Speed
    The fastest the ship can move, in metres per second
    Pitch
    The fastest the ship can raise or lower its nose, in degrees per second
    @@ -271,51 +367,84 @@ Along the top of this panel are the number of pips you put in to your ENG capaci
    Yaw
    The fastest the ship can turn its nose left or right, in degrees per second
    -

    Power Management

    -The power management panel provides information about power usage and priorities. It allows you to enable and disable individual modules, as well as set power priorities for each module.

    +Your movement profile can be altered by obtaining different thrusters or engineering your existing thrusters, and you can increase your movement values by adding pips to the ENG capacitor as well as reducing the amount of fuel and cargo you are carrying as well as reducing the overall weight of the build. You can also temporarily increase your movement profile by hitting the boost button.

    -

    Costs

    -The costs panel provides information about the costs for each of your modules, and the total cost and insurance for your build. By default Coriolis uses the standard costs, however discounts for your ship, modules and insurance can be altered in the 'Settings' at the top-right of the page.

    +

    Damage Profile

    +The damage profile provides two graphs showing how the the build's damage to the opponent's shields and hull change with engagement range. The vertical dashed line on the graph shows your current engagement range. This combines information about the build's weapons with the opponent's shields and hull to provide an accurate picture of sustained damage that can be inflicted on the opponent.

    -The retrofit costs provides information about the costs of changing the base build for your ship, or your saved build, to the current build.

    +

    Offence

    +

    Summary

    +The offence summary provides per-weapon information about sustained damage per second inflicted to shields and hull, along with a measure of effectiveness of that weapon. The effectiveness value has a tooltip that provides a breakdown of the effectiveness, and can include reductions or increases due to range, resistance, and either power distributor (for shields) or hardness (for hull). The final effectiveness value is calculated by multiplying these percentages together.

    -The reload costs provides information about the costs of reloading your current build.

    +

    Offence Metrics

    +The offence metrics panel provides information about your offence.

    + +Time to drain is a measure of how quickly your WEP capacitor will drain when firing all weapons. It is affected by the number of pips you have in your WEP capacitor, with more pips resulting in a higher WEP recharge rate and hence a longer time to drain.

    + +The next value is the time it will take you to remove your opponent's shields. This assumes that you have 100% time on target and that your engagement range stays constant. Note that if your time to remove shields is longer than your time to drain this assumes that you continue firing throughout, inflicting lower damage due to the reduced energy in your WEP capacitor.

    + +The next value is the time it will take you to remove your opponent's armour. This follows the same logic as the time to remove shields.

    + +

    Shield Damage Sources

    +The shield damage sources provides information about the sources of damage to your opponent by damage type. For each applicable type of damage (absolute explosive, kinetic, thermal) a sustained damage per second value is provided.

    + +

    Hull Damage Sources

    +The hull damage sources provides information about the sources of damage to your opponent by damage type. For each applicable type of damage (absolute explosive, kinetic, thermal) a sustained damage per second value is provided.

    + +

    Defence

    +

    Shield Metrics

    +The shield metrics provides information about your shield defence.

    + +Raw shield strength is the sum of the shield from your generator, boosters and shield cell banks. A tooltip provides a breakdown of these values.

    + +The time the shields will hold for is the time it will take your opponent' to remove your shields. This assumes that they have 100% time on target and that the engagement range stays constant. It also assumes that you fire all of your shield cell banks prior to your shields being lost.

    + +The time the shields will recover in is the time it will take your shields to go from collapsed (0%) to recovered (50%). This is affected by the number of pips you have in your SYS capacitor.

    + +The time the shields will recharge in is the time it will take your shields to go from recovered (50%) to full (100%). This is affected by the number of pips you have in your SYS capacitor.

    -

    Engine Profile

    -The engine profile panel provides information about the capabilities of your current thrusters. The graph shows you how the maximum speed (with 4 pips to engines) alters with the overall mass of your build. The slider can be altered to change the amount of cargo you have on-board. Your engine profile can be altered by obtaining different thrusters or engineering your existing thrusters.

    +Shield Sources +This chart provides information about the sources of your shields. For each applicable source of shields (generator, boosters, shield cell banks) a value is provided.

    -

    FSD Profile

    -The FSD profile panel provides information about the capabilities of your current frame shift drive. The graph shows you how the maximum jump range alters with the overall mass of your build. The slider can be altered to change the amount of cargo you have on-board. Your FSD profile can be altered by obtaining a different FSD or engineering your existing FSD.

    +Damage Taken +This graph shows how the initial damage from the weapons of each type are reduced before their damage is applied to the shields. For each type of damage (absolute, explosive, kinetic, thermal) a percentage of the initial damage is provided. A tooltip provides a breakdown of these values.

    -

    Jump Range

    -The jump range panel provides information about the build' jump range. The graph shows how the build's jump range changes with the amount of cargo on-board. The slider can be altered to change the amount of fuel you have on-board.

    +Effective Shield +This graph shows the effective shield for each damage type, found by dividing the raw shield value by the damage taken for that type.

    -

    Damage Dealt

    -The damage dealt panel provides information about the effectiveness of your build's weapons against opponents' shields and hull at different engagement distances.

    +

    Amour Metrics

    +The armour metrics provides information about your armour defence.

    -The ship against which you want to check damage dealt can be selected by clicking on the red ship icon or the red ship name at the top of this panel.

    +Raw armour strength is the sum of the armour from your bulkheads and hull reinforcement packages. A tooltip provides a breakdown of these values.

    -The main section of this panel is a table showing your weapons and their effectiveness. Effectiveness against shields takes in to account the weapon and its engagement range, and assumes standard shield resistances. Effectiveness against hull takes in to account the weapon and, its engagement range and the target's hardness, and assumes military grade armour resistances.

    +The time the armour will hold for is the time it will take your opponent' to take your armour to 0. This assumes that they have 100% time on target, the engagement range stays constant, and that all damage is dealt to the armour rather than modules.

    -Effective DPS and effective SDPS are the equivalent of DPS and SDPS for the weapon. Effectiveness is a percentage value that shows how effective the DPS of the weapon is compared in reality against the given target compared to the weapon's stated DPS. Effectiveness can never go above 100%.

    +Raw module armour is the sum of the protection from your module reinforcement packages.

    -Total effective DPS, SDPS and effectiveness against both shields and hull are provided at the bottom of the table.

    +Protection for hardpoints is the amount of protection that your module reinforcement packages provide to hardpoints. This percentage of damage to the hardpoints will be diverted to the module reinforcement packages.

    -At the bottom of this panel you can change your engagement range. The engagement range is the distance between your ship and your target. Many weapons suffer from what is known as damage falloff, where their effectiveness decreases the further the distance between your ship and your target. This allows you to model the effect of engaging at different ranges. +Protection for all other modules is the amount of protection that your module reinforcement packages provide to everything other than hardpoints. This percentage of damage to the modules will be diverted to the module reinforcement packages.

    -Note that this panel only shows enabled weapons, so if you want to see your overall effectiveness for a subset of your weapons you can disable the undesired weapons in the power management panel.

    +Armour Sources +This chart provides information about the sources of your armour. For each applicable source of shields (bulkheads, hull reinforcement packages) a value is provided.

    -At the bottom of this panel are two graphs showing how your sustained DPS changes with engagement range. This shows at a glance how effective each weapon is at different distances.

    +Damage Taken +This graph shows how the initial damage from the weapons of each type are reduced before their damage is applied to the armour. For each type of damage (absolute, explosive, kinetic, thermal) a percentage of the initial damage is provided. A tooltip provides a breakdown of these values.

    -

    Damage Received

    -The damage received panel provides information about the effectiveness of your build's defences against opponent's weapons at different engagement range. Features and functions are the same as the damage dealt panel, except that it does take in to account your build's resistances.

    +Effective Armour +This graph shows the effective armour for each damage type, found by dividing the raw armour value by the damage taken for that type.

    Keyboard Shortcuts

    +
    Ctrl-b
    toggle boost
    Ctrl-e
    open export dialogue (outfitting page only)
    Ctrl-h
    open help dialogue
    Ctrl-i
    open import dialogue
    Ctrl-o
    open shortlink dialogue
    +
    Ctrl-left-arrow
    increase SYS capacitor
    +
    Ctrl-up-arrow
    increase ENG capacitor
    +
    Ctrl-right-arrow
    increase WEP capacitor
    +
    Ctrl-down-arrow
    reset power distributor
    Esc
    close any open dialogue

    Glossary

    diff --git a/src/app/pages/OutfittingPage.jsx b/src/app/pages/OutfittingPage.jsx index 0e50fc63..358443f3 100644 --- a/src/app/pages/OutfittingPage.jsx +++ b/src/app/pages/OutfittingPage.jsx @@ -1,32 +1,31 @@ import React from 'react'; +// import Perf from 'react-addons-perf'; import { findDOMNode } from 'react-dom'; import { Ships } from 'coriolis-data/dist'; import cn from 'classnames'; import Page from './Page'; import Router from '../Router'; import Persist from '../stores/Persist'; +import * as Utils from '../utils/UtilityFunctions'; import Ship from '../shipyard/Ship'; import { toDetailedBuild } from '../shipyard/Serializer'; import { outfitURL } from '../utils/UrlGenerators'; import { FloppyDisk, Bin, Switch, Download, Reload, LinkIcon, ShoppingIcon } from '../components/SvgIcons'; +import LZString from 'lz-string'; import ShipSummaryTable from '../components/ShipSummaryTable'; import StandardSlotSection from '../components/StandardSlotSection'; -import HardpointsSlotSection from '../components/HardpointsSlotSection'; +import HardpointSlotSection from '../components/HardpointSlotSection'; import InternalSlotSection from '../components/InternalSlotSection'; import UtilitySlotSection from '../components/UtilitySlotSection'; -import OffenceSummary from '../components/OffenceSummary'; -import DefenceSummary from '../components/DefenceSummary'; -import MovementSummary from '../components/MovementSummary'; -import EngineProfile from '../components/EngineProfile'; -import FSDProfile from '../components/FSDProfile'; -import JumpRange from '../components/JumpRange'; -import DamageDealt from '../components/DamageDealt'; -import DamageReceived from '../components/DamageReceived'; -import PowerManagement from '../components/PowerManagement'; -import CostSection from '../components/CostSection'; +import Pips from '../components/Pips'; +import Boost from '../components/Boost'; +import Fuel from '../components/Fuel'; +import Cargo from '../components/Cargo'; +import ShipPicker from '../components/ShipPicker'; +import EngagementRange from '../components/EngagementRange'; +import OutfittingSubpages from '../components/OutfittingSubpages'; import ModalExport from '../components/ModalExport'; import ModalPermalink from '../components/ModalPermalink'; -import Slider from '../components/Slider'; /** * Document Title Generator @@ -50,17 +49,25 @@ export default class OutfittingPage extends Page { */ constructor(props, context) { super(props, context); - this.state = this._initState(context); + // window.Perf = Perf; + this.state = this._initState(props, context); this._keyDown = this._keyDown.bind(this); this._exportBuild = this._exportBuild.bind(this); + this._pipsUpdated = this._pipsUpdated.bind(this); + this._boostUpdated = this._boostUpdated.bind(this); + this._cargoUpdated = this._cargoUpdated.bind(this); + this._fuelUpdated = this._fuelUpdated.bind(this); + this._opponentUpdated = this._opponentUpdated.bind(this); + this._engagementRangeUpdated = this._engagementRangeUpdated.bind(this); } /** * [Re]Create initial state from context + * @param {Object} props React component properties * @param {context} context React component context * @return {Object} New state object */ - _initState(context) { + _initState(props, context) { let params = context.route.params; let shipId = params.ship; let code = params.code; @@ -82,6 +89,8 @@ export default class OutfittingPage extends Page { this._getTitle = getTitle.bind(this, data.properties.name); + // Obtain ship control from code + const { sys, eng, wep, boost, fuel, cargo, opponent, opponentBuild, opponentSys, opponentEng, opponentWep, engagementRange } = this._obtainControlFromCode(ship, code); return { error: null, title: this._getTitle(buildName), @@ -91,7 +100,19 @@ export default class OutfittingPage extends Page { shipId, ship, code, - savedCode + savedCode, + sys, + eng, + wep, + boost, + fuel, + cargo, + opponent, + opponentBuild, + opponentSys, + opponentEng, + opponentWep, + engagementRange }; } @@ -114,34 +135,212 @@ export default class OutfittingPage extends Page { } /** - * Save the current build + * Update the control part of the route */ - _saveBuild() { - let code = this.state.ship.toString(); - let { buildName, newBuildName, shipId } = this.state; + _updateRouteOnControlChange() { + const { ship, shipId, buildName } = this.state; + const code = this._fullCode(ship); + this._updateRoute(shipId, buildName, code); + this.setState({ code }); + } + + /** + * Provide a full code for this ship, including any additions due to the outfitting page + * @param {Object} ship the ship + * @param {number} fuel the fuel carried by the ship (if different from that in state) + * @param {number} cargo the cargo carried by the ship (if different from that in state) + * @returns {string} the code for this ship + */ + _fullCode(ship, fuel, cargo) { + return `${ship.toString()}.${LZString.compressToBase64(this._controlCode(fuel, cargo))}`; + } - if (buildName === newBuildName) { - Persist.saveBuild(shipId, buildName, code); - this._updateRoute(shipId, buildName, code); + /** + * Obtain the control information from the build code + * @param {Object} ship The ship + * @param {string} code The build code + * @returns {Object} The control information + */ + _obtainControlFromCode(ship, code) { + // Defaults + let sys = 2; + let eng = 2; + let wep = 2; + let boost = false; + let fuel = ship.fuelCapacity; + let cargo = ship.cargoCapacity; + let opponent = new Ship('eagle', Ships['eagle'].properties, Ships['eagle'].slots).buildWith(Ships['eagle'].defaults); + let opponentSys = 2; + let opponentEng = 2; + let opponentWep = 2; + let opponentBuild; + let engagementRange = 1000; + + // Obtain updates from code, if available + if (code) { + const parts = code.split('.'); + if (parts.length >= 5) { + // We have control information in the code + const control = LZString.decompressFromBase64(Utils.fromUrlSafe(parts[4])).split('/'); + sys = parseFloat(control[0]); + eng = parseFloat(control[1]); + wep = parseFloat(control[2]); + boost = control[3] == 1 ? true : false; + fuel = parseFloat(control[4]); + cargo = parseInt(control[5]); + if (control[6]) { + const shipId = control[6]; + opponent = new Ship(shipId, Ships[shipId].properties, Ships[shipId].slots); + if (control[7] && Persist.getBuild(shipId, control[7])) { + // Ship is a particular build + const opponentCode = Persist.getBuild(shipId, control[7]); + opponent.buildFrom(opponentCode); + opponentBuild = control[7]; + if (opponentBuild) { + // Obtain opponent's sys/eng/wep pips from their code + const opponentParts = opponentCode.split('.'); + if (opponentParts.length >= 5) { + const opponentControl = LZString.decompressFromBase64(Utils.fromUrlSafe(opponentParts[4])).split('/'); + opponentSys = parseFloat(opponentControl[0]); + opponentEng = parseFloat(opponentControl[1]); + opponentWep = parseFloat(opponentControl[2]); + } + } + } else { + // Ship is a stock build + opponent.buildWith(Ships[shipId].defaults); + } + } + engagementRange = parseInt(control[8]); + } + } + + return { sys, eng, wep, boost, fuel, cargo, opponent, opponentBuild, opponentSys, opponentEng, opponentWep, engagementRange }; + } + + /** + * Triggered when pips have been updated + * @param {number} sys SYS pips + * @param {number} eng ENG pips + * @param {number} wep WEP pips + */ + _pipsUpdated(sys, eng, wep) { + this.setState({ sys, eng, wep }, () => this._updateRouteOnControlChange()); + } + + /** + * Triggered when boost has been updated + * @param {boolean} boost true if boosting + */ + _boostUpdated(boost) { + this.setState({ boost }, () => this._updateRouteOnControlChange()); + } + + /** + * Triggered when fuel has been updated + * @param {number} fuel the amount of fuel, in T + */ + _fuelUpdated(fuel) { + this.setState({ fuel }, () => this._updateRouteOnControlChange()); + } + + /** + * Triggered when cargo has been updated + * @param {number} cargo the amount of cargo, in T + */ + _cargoUpdated(cargo) { + this.setState({ cargo }, () => this._updateRouteOnControlChange()); + } + + /** + * Triggered when engagement range has been updated + * @param {number} engagementRange the engagement range, in m + */ + _engagementRangeUpdated(engagementRange) { + this.setState({ engagementRange }, () => this._updateRouteOnControlChange()); + } + + /** + * Triggered when target ship has been updated + * @param {string} opponent the opponent's ship model + * @param {string} opponentBuild the name of the opponent's build + */ + _opponentUpdated(opponent, opponentBuild) { + const opponentShip = new Ship(opponent, Ships[opponent].properties, Ships[opponent].slots); + let opponentSys = this.state.opponentSys; + let opponentEng = this.state.opponentEng; + let opponentWep = this.state.opponentWep; + if (opponentBuild && Persist.getBuild(opponent, opponentBuild)) { + // Ship is a particular build + opponentShip.buildFrom(Persist.getBuild(opponent, opponentBuild)); + // Set pips for opponent + const opponentParts = Persist.getBuild(opponent, opponentBuild).split('.'); + if (opponentParts.length >= 5) { + const opponentControl = LZString.decompressFromBase64(Utils.fromUrlSafe(opponentParts[4])).split('/'); + opponentSys = parseFloat(opponentControl[0]); + opponentEng = parseFloat(opponentControl[1]); + opponentWep = parseFloat(opponentControl[2]); + } } else { - Persist.saveBuild(shipId, newBuildName, code); - this._updateRoute(shipId, newBuildName, code); + // Ship is a stock build + opponentShip.buildWith(Ships[opponent].defaults); + opponentSys = 2; + opponentEng = 2; + opponentWep = 2; } - this.setState({ buildName: newBuildName, code, savedCode: code, title: this._getTitle(newBuildName) }); + this.setState({ opponent: opponentShip, opponentBuild, opponentSys, opponentEng, opponentWep }, () => this._updateRouteOnControlChange()); + } + + /** + * Set the control code for this outfitting page + * @param {number} fuel the fuel carried by the ship (if different from that in state) + * @param {number} cargo the cargo carried by the ship (if different from that in state) + * @returns {string} The control code + */ + _controlCode(fuel, cargo) { + const { sys, eng, wep, boost, opponent, opponentBuild, engagementRange } = this.state; + const code = `${sys}/${eng}/${wep}/${boost ? 1 : 0}/${fuel || this.state.fuel}/${cargo || this.state.cargo}/${opponent.id}/${opponentBuild ? opponentBuild : ''}/${engagementRange}`; + return code; + } + + /** + * Save the current build + */ + _saveBuild() { + const { code, buildName, newBuildName, shipId } = this.state; + + Persist.saveBuild(shipId, newBuildName, code); + this._updateRoute(shipId, newBuildName, code); + + let opponent, opponentBuild, opponentSys, opponentEng, opponentWep; + if (shipId === this.state.opponent.id && buildName === this.state.opponentBuild) { + // This is a save of our current opponent build; update it + opponentBuild = newBuildName; + opponent = new Ship(shipId, Ships[shipId].properties, Ships[shipId].slots).buildFrom(code); + opponentSys = this.state.sys; + opponentEng = this.state.eng; + opponentWep = this.state.wep; + } else { + opponentBuild = this.state.opponentBuild; + opponent = this.state.opponent; + opponentSys = this.state.opponentSys; + opponentEng = this.state.opponentEng; + opponentWep = this.state.opponentWep; + } + this.setState({ buildName: newBuildName, code, savedCode: code, opponent, opponentBuild, opponentSys, opponentEng, opponentWep, title: this._getTitle(newBuildName) }); } /** * Rename the current build */ _renameBuild() { - let { buildName, newBuildName, shipId, ship } = this.state; + const { code, buildName, newBuildName, shipId, ship } = this.state; if (buildName != newBuildName && newBuildName.length) { - let code = ship.toString(); Persist.deleteBuild(shipId, buildName); Persist.saveBuild(shipId, newBuildName, code); this._updateRoute(shipId, newBuildName, code); - this.setState({ buildName: newBuildName, code, savedCode: code }); + this.setState({ buildName: newBuildName, code, savedCode: code, opponentBuild: newBuildName }); } } @@ -149,24 +348,50 @@ export default class OutfittingPage extends Page { * Reload build from last save */ _reloadBuild() { - this.state.ship.buildFrom(this.state.savedCode); - this._shipUpdated(); + this.setState({ code: this.state.savedCode }, () => this._codeUpdated()); } /** * Reset build to Stock/Factory defaults */ _resetBuild() { - this.state.ship.buildWith(Ships[this.state.shipId].defaults); - this._shipUpdated(); + const { ship, shipId, buildName } = this.state; + // Rebuild ship + ship.buildWith(Ships[shipId].defaults); + // Reset controls + const code = ship.toString(); + const { sys, eng, wep, boost, fuel, cargo, opponent, opponentBuild, engagementRange } = this._obtainControlFromCode(ship, code); + // Update state, and refresh the ship + this.setState({ + sys, + eng, + wep, + boost, + fuel, + cargo, + opponent, + opponentBuild, + engagementRange + }, () => this._updateRoute(shipId, buildName, code)); } /** * Delete the build */ _deleteBuild() { - Persist.deleteBuild(this.state.shipId, this.state.buildName); + const { shipId, buildName } = this.state; + Persist.deleteBuild(shipId, buildName); + + let opponentBuild; + if (shipId === this.state.opponent.id && buildName === this.state.opponentBuild) { + // Our current opponent has been deleted; revert to stock + opponentBuild = null; + } else { + opponentBuild = this.state.opponentBuild; + } Router.go(outfitURL(this.state.shipId)); + + this.setState({ opponentBuild }); } /** @@ -183,14 +408,43 @@ export default class OutfittingPage extends Page { } /** - * Trigger render on ship model change + * Called when the code for the ship has been updated, to synchronise the rest of the data */ - _shipUpdated() { - let { shipId, buildName, ship } = this.state; - let code = ship.toString(); + _codeUpdated() { + const { code, ship, shipId, buildName } = this.state; + + // Rebuild ship from the code + this.state.ship.buildFrom(code); + + // Obtain controls from the code + const { sys, eng, wep, boost, fuel, cargo, opponent, opponentBuild, engagementRange } = this._obtainControlFromCode(ship, code); + // Update state, and refresh the route when complete + this.setState({ + sys, + eng, + wep, + boost, + fuel, + cargo, + opponent, + opponentBuild, + engagementRange + }, () => this._updateRoute(shipId, buildName, code)); + } - this._updateRoute(shipId, buildName, code); - this.setState({ code }); + /** + * Called when the ship has been updated, to set the code and then update accordingly + */ + _shipUpdated() { + let { ship, shipId, buildName, cargo, fuel } = this.state; + if (cargo > ship.cargoCapacity) { + cargo = ship.cargoCapacity; + } + if (fuel > ship.fuelCapacity) { + fuel = ship.fuelCapacity; + } + const code = this._fullCode(ship, fuel, cargo); + this.setState({ code, cargo, fuel }, () => this._updateRoute(shipId, buildName, code)); } /** @@ -203,20 +457,6 @@ export default class OutfittingPage extends Page { Router.replace(outfitURL(shipId, code, buildName)); } - /** - * Update dimenions from rendered DOM - */ - _updateDimensions() { - let elem = findDOMNode(this.refs.chartThird); - - if (elem) { - this.setState({ - thirdChartWidth: findDOMNode(this.refs.chartThird).offsetWidth, - halfChartWidth: findDOMNode(this.refs.chartThird).offsetWidth * 3 / 2 - }); - } - } - /** * Update state based on context changes * @param {Object} nextProps Incoming/Next properties @@ -224,7 +464,7 @@ export default class OutfittingPage extends Page { */ componentWillReceiveProps(nextProps, nextContext) { if (this.context.route !== nextContext.route) { // Only reinit state if the route has changed - this.setState(this._initState(nextContext)); + this.setState(this._initState(nextProps, nextContext)); } } @@ -232,22 +472,14 @@ export default class OutfittingPage extends Page { * Add listeners when about to mount */ componentWillMount() { - this.resizeListener = this.context.onWindowResize(this._updateDimensions); document.addEventListener('keydown', this._keyDown); } - /** - * Trigger DOM updates on mount - */ - componentDidMount() { - this._updateDimensions(); - } - /** * Remove listeners on unmount */ componentWillUnmount() { - this.resizeListener.remove(); + document.removeEventListener('keydown', this._keyDown); } /** @@ -295,19 +527,30 @@ export default class OutfittingPage extends Page { let state = this.state, { language, termtip, tooltip, sizeRatio, onWindowResize } = this.context, { translate, units, formats } = language, - { ship, code, savedCode, buildName, newBuildName, halfChartWidth, thirdChartWidth } = state, + { ship, code, savedCode, buildName, newBuildName, sys, eng, wep, boost, fuel, cargo, opponent, opponentBuild, opponentSys, opponentEng, opponentWep, engagementRange } = state, hide = tooltip.bind(null, null), menu = this.props.currentMenu, shipUpdated = this._shipUpdated, canSave = (newBuildName || buildName) && code !== savedCode, canRename = buildName && newBuildName && buildName != newBuildName, - canReload = savedCode && canSave, - hStr = ship.getHardpointsString() + '.' + ship.getModificationsString(), - iStr = ship.getInternalString() + '.' + ship.getModificationsString(); + canReload = savedCode && canSave; // Code can be blank for a default loadout. Prefix it with the ship name to ensure that changes in default ships is picked up code = ship.name + (code || ''); + // Markers are used to propagate state changes without requiring a deep comparison of the ship, as that takes a long time + const _sStr = ship.getStandardString(); + const _iStr = ship.getInternalString(); + const _hStr = ship.getHardpointsString(); + const _pStr = `${ship.getPowerEnabledString()}${ship.getPowerPrioritiesString()}`; + const _mStr = ship.getModificationsString(); + + const standardSlotMarker = `${ship.name}${_sStr}${_pStr}${_mStr}${ship.ladenMass}${cargo}${fuel}`; + const internalSlotMarker = `${ship.name}${_iStr}${_pStr}${_mStr}`; + const hardpointsSlotMarker = `${ship.name}${_hStr}${_pStr}${_mStr}`; + const boostMarker = `${ship.canBoost(cargo, fuel)}`; + const shipSummaryMarker = `${ship.name}${_sStr}${_iStr}${_hStr}${_pStr}${_mStr}${ship.ladenMass}${ship.cargo}${ship.fuel}`; + return (
    @@ -341,46 +584,63 @@ export default class OutfittingPage extends Page {
    - - - - - - -
    - -
    -
    - + {/* Main tables */} + + + + + + + {/* Control of ship and opponent */} +
    +
    +

    {translate('ship control')}

    +
    +
    + +
    -
    - +
    +
    - - - - -
    - +
    +
    - -
    - +
    + { ship.cargoCapacity > 0 ? : null }
    - -
    - +
    +
    +

    {translate('opponent')}

    +
    +
    + +
    - -
    - +
    +
    -
    - -
    + {/* Tabbed subpages */} +
    - ); } } diff --git a/src/app/shipyard/Calculations.js b/src/app/shipyard/Calculations.js index 47b74b7f..42dc5cee 100644 --- a/src/app/shipyard/Calculations.js +++ b/src/app/shipyard/Calculations.js @@ -9,33 +9,33 @@ import Module from './Module'; * @return {number} Distance in Light Years */ export function jumpRange(mass, fsd, fuel) { - let fsdMaxFuelPerJump = fsd instanceof Module ? fsd.getMaxFuelPerJump() : fsd.maxfuel; - let fsdOptimalMass = fsd instanceof Module ? fsd.getOptMass() : fsd.optmass; + const fsdMaxFuelPerJump = fsd instanceof Module ? fsd.getMaxFuelPerJump() : fsd.maxfuel; + const fsdOptimalMass = fsd instanceof Module ? fsd.getOptMass() : fsd.optmass; return Math.pow(Math.min(fuel === undefined ? fsdMaxFuelPerJump : fuel, fsdMaxFuelPerJump) / fsd.fuelmul, 1 / fsd.fuelpower) * fsdOptimalMass / mass; } /** - * Calculate the fastest (total) range based on mass and a specific FSD, and all fuel available + * Calculate the total jump range based on mass and a specific FSD, and all fuel available * * @param {number} mass Mass of a ship: laden, unlanden, partially laden, etc * @param {object} fsd The FDS object/component with maxfuel, fuelmul, fuelpower, optmass * @param {number} fuel The total fuel available * @return {number} Distance in Light Years */ -export function fastestRange(mass, fsd, fuel) { - let fsdMaxFuelPerJump = fsd instanceof Module ? fsd.getMaxFuelPerJump() : fsd.maxfuel; - let fsdOptimalMass = fsd instanceof Module ? fsd.getOptMass() : fsd.optmass; - let fuelRemaining = fuel % fsdMaxFuelPerJump; // Fuel left after making N max jumps - let jumps = Math.floor(fuel / fsdMaxFuelPerJump); - mass += fuelRemaining; - // Going backwards, start with the last jump using the remaining fuel - let fastestRange = fuelRemaining > 0 ? Math.pow(fuelRemaining / fsd.fuelmul, 1 / fsd.fuelpower) * fsdOptimalMass / mass : 0; - // For each max fuel jump, calculate the max jump range based on fuel mass left in the tank - for (let j = 0; j < jumps; j++) { - mass += fsd.maxfuel; - fastestRange += Math.pow(fsdMaxFuelPerJump / fsd.fuelmul, 1 / fsd.fuelpower) * fsdOptimalMass / mass; - } - return fastestRange; +export function totalJumpRange(mass, fsd, fuel) { + const fsdMaxFuelPerJump = fsd instanceof Module ? fsd.getMaxFuelPerJump() : fsd.maxfuel; + const fsdOptimalMass = fsd instanceof Module ? fsd.getOptMass() : fsd.optmass; + + let fuelRemaining = fuel; + let totalRange = 0; + while (fuelRemaining > 0) { + const fuelForThisJump = Math.min(fuelRemaining, fsdMaxFuelPerJump); + totalRange += this.jumpRange(mass, fsd, fuelForThisJump); + // Mass is reduced + mass -= fuelForThisJump; + fuelRemaining -= fuelForThisJump; + } + return totalRange; }; /** @@ -173,3 +173,683 @@ function normValues(minMass, optMass, maxMass, minMul, optMul, maxMul, mass, bas res * (1 - (engpip * 1)), res]; } + +/** + * Calculate a single value + * @param {number} minMass the minimum mass of the thrusters + * @param {number} optMass the optimum mass of the thrusters + * @param {number} maxMass the maximum mass of the thrusters + * @param {number} minMul the minimum multiplier of the thrusters + * @param {number} optMul the optimum multiplier of the thrusters + * @param {number} maxMul the maximum multiplier of the thrusters + * @param {number} mass the mass of the ship + * @param {base} base the base value from which to calculate + * @param {number} engpip the multiplier per pip to engines + * @param {number} eng the pips to engines + * @returns {number} the resultant value + */ +function calcValue(minMass, optMass, maxMass, minMul, optMul, maxMul, mass, base, engpip, eng) { + const xnorm = Math.min(1, (maxMass - mass) / (maxMass - minMass)); + const exponent = Math.log((optMul - minMul) / (maxMul - minMul)) / Math.log(Math.min(1, (maxMass - optMass) / (maxMass - minMass))); + const ynorm = Math.pow(xnorm, exponent); + const mul = minMul + ynorm * (maxMul - minMul); + const res = base * mul; + + return res * (1 - (engpip * (4 - eng))); +} + +/** + * Calculate speed for a given setup + * @param {number} mass the mass of the ship + * @param {number} baseSpeed the base speed of the ship + * @param {ojbect} thrusters the thrusters of the ship + * @param {number} engpip the multiplier per pip to engines + * @param {number} eng the pips to engines + * @param {number} boostFactor the boost factor for ths ship + * @param {boolean} boost true if the boost is activated + * @returns {number} the resultant speed + */ +export function calcSpeed(mass, baseSpeed, thrusters, engpip, eng, boostFactor, boost) { + // thrusters might be a module or a template; handle either here + const minMass = thrusters instanceof Module ? thrusters.getMinMass() : thrusters.minmass; + const optMass = thrusters instanceof Module ? thrusters.getOptMass() : thrusters.optmass; + const maxMass = thrusters instanceof Module ? thrusters.getMaxMass() : thrusters.maxmass; + const minMul = thrusters instanceof Module ? thrusters.getMinMul('speed') : (thrusters.minmulspeed ? thrusters.minmulspeed : thrusters.minmul); + const optMul = thrusters instanceof Module ? thrusters.getOptMul('speed') : (thrusters.optmulspeed ? thrusters.minmulspeed : thrusters.minmul); + const maxMul = thrusters instanceof Module ? thrusters.getMaxMul('speed') : (thrusters.maxmulspeed ? thrusters.minmulspeed : thrusters.minmul); + + let result = calcValue(minMass, optMass, maxMass, minMul, optMul, maxMul, mass, baseSpeed, engpip, eng); + if (boost == true) { + result *= boostFactor; + } + + return result; +} + +/** + * Calculate pitch for a given setup + * @param {number} mass the mass of the ship + * @param {number} basePitch the base pitch of the ship + * @param {ojbect} thrusters the thrusters of the ship + * @param {number} engpip the multiplier per pip to engines + * @param {number} eng the pips to engines + * @param {number} boostFactor the boost factor for ths ship + * @param {boolean} boost true if the boost is activated + * @returns {number} the resultant pitch + */ +export function calcPitch(mass, basePitch, thrusters, engpip, eng, boostFactor, boost) { + // thrusters might be a module or a template; handle either here + let minMass = thrusters instanceof Module ? thrusters.getMinMass() : thrusters.minmass; + let optMass = thrusters instanceof Module ? thrusters.getOptMass() : thrusters.optmass; + let maxMass = thrusters instanceof Module ? thrusters.getMaxMass() : thrusters.maxmass; + let minMul = thrusters instanceof Module ? thrusters.getMinMul('rotation') : (thrusters.minmulrotation ? thrusters.minmulrotation : thrusters.minmul); + let optMul = thrusters instanceof Module ? thrusters.getOptMul('rotation') : (thrusters.optmulrotation ? thrusters.optmulrotation : thrusters.optmul); + let maxMul = thrusters instanceof Module ? thrusters.getMaxMul('rotation') : (thrusters.maxmulrotation ? thrusters.maxmulrotation : thrusters.maxmul); + + let result = calcValue(minMass, optMass, maxMass, minMul, optMul, maxMul, mass, basePitch, engpip, eng); + if (boost == true) { + result *= boostFactor; + } + + return result; +} + +/** + * Calculate roll for a given setup + * @param {number} mass the mass of the ship + * @param {number} baseRoll the base roll of the ship + * @param {ojbect} thrusters the thrusters of the ship + * @param {number} engpip the multiplier per pip to engines + * @param {number} eng the pips to engines + * @param {number} boostFactor the boost factor for ths ship + * @param {boolean} boost true if the boost is activated + * @returns {number} the resultant roll + */ +export function calcRoll(mass, baseRoll, thrusters, engpip, eng, boostFactor, boost) { + // thrusters might be a module or a template; handle either here + let minMass = thrusters instanceof Module ? thrusters.getMinMass() : thrusters.minmass; + let optMass = thrusters instanceof Module ? thrusters.getOptMass() : thrusters.optmass; + let maxMass = thrusters instanceof Module ? thrusters.getMaxMass() : thrusters.maxmass; + let minMul = thrusters instanceof Module ? thrusters.getMinMul('rotation') : (thrusters.minmulrotation ? thrusters.minmulrotation : thrusters.minmul); + let optMul = thrusters instanceof Module ? thrusters.getOptMul('rotation') : (thrusters.optmulrotation ? thrusters.optmulrotation : thrusters.optmul); + let maxMul = thrusters instanceof Module ? thrusters.getMaxMul('rotation') : (thrusters.maxmulrotation ? thrusters.maxmulrotation : thrusters.maxmul); + + let result = calcValue(minMass, optMass, maxMass, minMul, optMul, maxMul, mass, baseRoll, engpip, eng); + if (boost == true) { + result *= boostFactor; + } + + return result; +} + +/** + * Calculate yaw for a given setup + * @param {number} mass the mass of the ship + * @param {number} baseYaw the base yaw of the ship + * @param {ojbect} thrusters the thrusters of the ship + * @param {number} engpip the multiplier per pip to engines + * @param {number} eng the pips to engines + * @param {number} boostFactor the boost factor for ths ship + * @param {boolean} boost true if the boost is activated + * @returns {number} the resultant yaw + */ +export function calcYaw(mass, baseYaw, thrusters, engpip, eng, boostFactor, boost) { + // thrusters might be a module or a template; handle either here + let minMass = thrusters instanceof Module ? thrusters.getMinMass() : thrusters.minmass; + let optMass = thrusters instanceof Module ? thrusters.getOptMass() : thrusters.optmass; + let maxMass = thrusters instanceof Module ? thrusters.getMaxMass() : thrusters.maxmass; + let minMul = thrusters instanceof Module ? thrusters.getMinMul('rotation') : (thrusters.minmulrotation ? thrusters.minmulrotation : thrusters.minmul); + let optMul = thrusters instanceof Module ? thrusters.getOptMul('rotation') : (thrusters.optmulrotation ? thrusters.optmulrotation : thrusters.optmul); + let maxMul = thrusters instanceof Module ? thrusters.getMaxMul('rotation') : (thrusters.maxmulrotation ? thrusters.maxmulrotation : thrusters.maxmul); + + let result = calcValue(minMass, optMass, maxMass, minMul, optMul, maxMul, mass, baseYaw, engpip, eng); + if (boost == true) { + result *= boostFactor; + } + + return result; +} + +/** + * Calculate shield metrics + * @param {Object} ship The ship + * @param {int} sys The pips to SYS + * @returns {Object} Shield metrics + */ +export function shieldMetrics(ship, sys) { + const sysResistance = this.sysResistance(sys); + const maxSysResistance = this.sysResistance(4); + + let shield = {}; + + const shieldGeneratorSlot = ship.findInternalByGroup('sg'); + if (shieldGeneratorSlot && shieldGeneratorSlot.enabled && shieldGeneratorSlot.m) { + const shieldGenerator = shieldGeneratorSlot.m; + + // Boosters + let boost = 1; + let boosterExplDmg = 1; + let boosterKinDmg = 1; + let boosterThermDmg = 1; + for (let slot of ship.hardpoints) { + if (slot.enabled && slot.m && slot.m.grp == 'sb') { + boost += slot.m.getShieldBoost(); + boosterExplDmg = boosterExplDmg * (1 - slot.m.getExplosiveResistance()); + boosterKinDmg = boosterKinDmg * (1 - slot.m.getKineticResistance()); + boosterThermDmg = boosterThermDmg * (1 - slot.m.getThermalResistance()); + } + } + + // Calculate diminishing returns for boosters + // Diminishing returns not currently in-game + // boost = Math.min(boost, (1 - Math.pow(Math.E, -0.7 * boost)) * 2.5); + + // Remove base shield generator strength + boost -= 1; + // Apply diminishing returns + boosterExplDmg = boosterExplDmg > 0.7 ? boosterExplDmg : 0.7 - (0.7 - boosterExplDmg) / 2; + boosterKinDmg = boosterKinDmg > 0.7 ? boosterKinDmg : 0.7 - (0.7 - boosterKinDmg) / 2; + boosterThermDmg = boosterThermDmg > 0.7 ? boosterThermDmg : 0.7 - (0.7 - boosterThermDmg) / 2; + + const generatorStrength = this.shieldStrength(ship.hullMass, ship.baseShieldStrength, shieldGenerator, 1); + const boostersStrength = generatorStrength * boost; + + // Recover time is the time taken to go from 0 to 50%. It includes a 16-second wait before shields start to recover + const shieldToRecover = (generatorStrength + boostersStrength) / 2; + const powerDistributor = ship.standard[4].m; + const sysRechargeRate = this.sysRechargeRate(powerDistributor, sys); + + // Our initial regeneration comes from the SYS capacitor store, which is replenished as it goes + // 0.6 is a magic number from FD: each 0.6 MW of energy from the power distributor recharges 1 MJ/s of regeneration + let capacitorDrain = (shieldGenerator.getBrokenRegenerationRate() * 0.6) - sysRechargeRate; + let capacitorLifetime = powerDistributor.getSystemsCapacity() / capacitorDrain; + + let recover = 16; + if (capacitorDrain <= 0 || shieldToRecover < capacitorLifetime * shieldGenerator.getBrokenRegenerationRate()) { + // We can recover the entire shield from the capacitor store + recover += shieldToRecover / shieldGenerator.getBrokenRegenerationRate(); + } else { + // We can recover some of the shield from the capacitor store + recover += capacitorLifetime; + const remainingShieldToRecover = shieldToRecover - capacitorLifetime * shieldGenerator.getBrokenRegenerationRate(); + if (sys === 0) { + // No system pips so will never recover shields + recover = Math.Inf; + } else { + // Recover remaining shields at the rate of the power distributor's recharge + recover += remainingShieldToRecover / (sysRechargeRate / 0.6); + } + } + + // Recharge time is the time taken to go from 50% to 100% + const shieldToRecharge = (generatorStrength + boostersStrength) / 2; + + // Our initial regeneration comes from the SYS capacitor store, which is replenished as it goes + // 0.6 is a magic number from FD: each 0.6 MW of energy from the power distributor recharges 1 MJ/s of regeneration + capacitorDrain = (shieldGenerator.getRegenerationRate() * 0.6) - sysRechargeRate; + capacitorLifetime = powerDistributor.getSystemsCapacity() / capacitorDrain; + + let recharge = 0; + if (capacitorDrain <= 0 || shieldToRecharge < capacitorLifetime * shieldGenerator.getRegenerationRate()) { + // We can recharge the entire shield from the capacitor store + recharge += shieldToRecharge / shieldGenerator.getRegenerationRate(); + } else { + // We can recharge some of the shield from the capacitor store + recharge += capacitorLifetime; + const remainingShieldToRecharge = shieldToRecharge - capacitorLifetime * shieldGenerator.getRegenerationRate(); + if (sys === 0) { + // No system pips so will never recharge shields + recharge = Math.Inf; + } else { + // Recharge remaining shields at the rate of the power distributor's recharge + recharge += remainingShieldToRecharge / (sysRechargeRate / 0.6); + } + } + + shield = { + generator: generatorStrength, + boosters: boostersStrength, + cells: ship.shieldCells, + total: generatorStrength + boostersStrength + ship.shieldCells, + recover, + recharge, + }; + + // Shield resistances have three components: the shield generator, the shield boosters and the SYS pips. + // We re-cast these as damage percentages + shield.absolute = { + generator: 1, + boosters: 1, + sys: 1 - sysResistance, + total: 1 - sysResistance, + max: 1 - maxSysResistance + }; + + shield.explosive = { + generator: 1 - shieldGenerator.getExplosiveResistance(), + boosters: boosterExplDmg, + sys: (1 - sysResistance), + total: (1 - shieldGenerator.getExplosiveResistance()) * boosterExplDmg * (1 - sysResistance), + max: (1 - shieldGenerator.getExplosiveResistance()) * boosterExplDmg * (1 - maxSysResistance) + }; + + shield.kinetic = { + generator: 1 - shieldGenerator.getKineticResistance(), + boosters: boosterKinDmg, + sys: (1 - sysResistance), + total: (1 - shieldGenerator.getKineticResistance()) * boosterKinDmg * (1 - sysResistance), + max: (1 - shieldGenerator.getKineticResistance()) * boosterKinDmg * (1 - maxSysResistance) + }; + + shield.thermal = { + generator: 1 - shieldGenerator.getThermalResistance(), + boosters: boosterThermDmg, + sys: (1 - sysResistance), + total: (1 - shieldGenerator.getThermalResistance()) * boosterThermDmg * (1 - sysResistance), + max: (1 - shieldGenerator.getThermalResistance()) * boosterThermDmg * (1 - maxSysResistance) + }; + } + + return shield; +} + +/** + * Calculate armour metrics + * @param {Object} ship The ship + * @returns {Object} Armour metrics + */ +export function armourMetrics(ship) { + // Armour from bulkheads + const armourBulkheads = ship.baseArmour + (ship.baseArmour * ship.bulkheads.m.getHullBoost()); + let armourReinforcement = 0; + + let moduleArmour = 0; + let moduleProtection = 1; + + let hullExplDmg = 1; + let hullKinDmg = 1; + let hullThermDmg = 1; + + // Armour from HRPs and module armour from MRPs + for (let slot of ship.internal) { + if (slot.m && slot.m.grp == 'hr') { + armourReinforcement += slot.m.getHullReinforcement(); + // Hull boost for HRPs is applied against the ship's base armour + armourReinforcement += ship.baseArmour * slot.m.getModValue('hullboost') / 10000; + + hullExplDmg = hullExplDmg * (1 - slot.m.getExplosiveResistance()); + hullKinDmg = hullKinDmg * (1 - slot.m.getKineticResistance()); + hullThermDmg = hullThermDmg * (1 - slot.m.getThermalResistance()); + } + if (slot.m && slot.m.grp == 'mrp') { + moduleArmour += slot.m.getIntegrity(); + moduleProtection = moduleProtection * (1 - slot.m.getProtection()); + } + } + moduleProtection = 1 - moduleProtection; + + // Apply diminishing returns + hullExplDmg = hullExplDmg > 0.7 ? hullExplDmg : 0.7 - (0.7 - hullExplDmg) / 2; + hullKinDmg = hullKinDmg > 0.7 ? hullKinDmg : 0.7 - (0.7 - hullKinDmg) / 2; + hullThermDmg = hullThermDmg > 0.7 ? hullThermDmg : 0.7 - (0.7 - hullThermDmg) / 2; + + const armour = { + bulkheads: armourBulkheads, + reinforcement: armourReinforcement, + modulearmour: moduleArmour, + moduleprotection: moduleProtection, + total: armourBulkheads + armourReinforcement + }; + + // Armour resistances have two components: bulkheads and HRPs + // We re-cast these as damage percentages + armour.absolute = { + bulkheads: 1, + reinforcement: 1, + total: 1 + }; + + armour.explosive = { + bulkheads: 1 - ship.bulkheads.m.getExplosiveResistance(), + reinforcement: hullExplDmg, + total: (1 - ship.bulkheads.m.getExplosiveResistance()) * hullExplDmg + }; + + armour.kinetic = { + bulkheads: 1 - ship.bulkheads.m.getKineticResistance(), + reinforcement: hullKinDmg, + total: (1 - ship.bulkheads.m.getKineticResistance()) * hullKinDmg + }; + + armour.thermal = { + bulkheads: 1 - ship.bulkheads.m.getThermalResistance(), + reinforcement: hullThermDmg, + total: (1 - ship.bulkheads.m.getThermalResistance()) * hullThermDmg + }; + + return armour; +} + +/** + * Calculate defence metrics for a ship + * @param {Object} ship The ship + * @param {Object} opponent The opponent ship + * @param {int} sys The pips to SYS + * @param {int} opponentWep The pips to pponent's WEP + * @param {int} engagementrange The range between the ship and opponent + * @returns {Object} Defence metrics + */ +export function defenceMetrics(ship, opponent, sys, opponentWep, engagementrange) { + // Obtain the shield metrics + const shield = this.shieldMetrics(ship, sys); + + // Obtain the armour metrics + const armour = this.armourMetrics(ship); + + // Obtain the opponent's sustained DPS on us + const sustainedDps = this.sustainedDps(opponent, ship, sys, engagementrange); + + const shielddamage = shield.generator ? { + absolutesdps: sustainedDps.shieldsdps.absolute, + explosivesdps: sustainedDps.shieldsdps.explosive, + kineticsdps: sustainedDps.shieldsdps.kinetic, + thermalsdps: sustainedDps.shieldsdps.thermal, + totalsdps: sustainedDps.shieldsdps.absolute + sustainedDps.shieldsdps.explosive + sustainedDps.shieldsdps.kinetic + sustainedDps.shieldsdps.thermal, + totalseps: sustainedDps.eps + } : {}; + + const armourdamage = { + absolutesdps: sustainedDps.armoursdps.absolute, + explosivesdps: sustainedDps.armoursdps.explosive, + kineticsdps: sustainedDps.armoursdps.kinetic, + thermalsdps: sustainedDps.armoursdps.thermal, + totalsdps: sustainedDps.armoursdps.absolute + sustainedDps.armoursdps.explosive + sustainedDps.armoursdps.kinetic + sustainedDps.armoursdps.thermal, + totalseps: sustainedDps.eps + }; + + return { shield, armour, shielddamage, armourdamage }; +} + +/** + * Calculate offence metrics for a ship + * @param {Object} ship The ship + * @param {Object} opponent The opponent ship + * @param {int} wep The pips to WEP + * @param {int} opponentSys The pips to opponent's SYS + * @param {int} engagementrange The range between the ship and opponent + * @returns {array} Offence metrics + */ +export function offenceMetrics(ship, opponent, wep, opponentSys, engagementrange) { + // Per-weapon and total damage + const damage = []; + + // Obtain the opponent's shield and armour metrics + const opponentShields = this.shieldMetrics(opponent, opponentSys); + const opponentArmour = this.armourMetrics(opponent); + + // Per-weapon and total damage to shields + for (let i = 0; i < ship.hardpoints.length; i++) { + if (ship.hardpoints[i].maxClass > 0 && ship.hardpoints[i].m && ship.hardpoints[i].enabled) { + const m = ship.hardpoints[i].m; + + const classRating = `${m.class}${m.rating}${m.missile ? '/' + m.missile : ''}`; + let engineering; + if (m.blueprint && m.blueprint.name) { + engineering = m.blueprint.name + ' ' + 'grade' + ' ' + m.blueprint.grade; + if (m.blueprint.special && m.blueprint.special.id >= 0) { + engineering += ', ' + m.blueprint.special.name; + } + } + + const weaponSustainedDps = this._weaponSustainedDps(m, opponent, opponentShields, opponentArmour, engagementrange); + damage.push({ + id: i, + mount: m.mount, + name: m.name || m.grp, + classRating, + engineering, + sdps: weaponSustainedDps.damage, + seps: weaponSustainedDps.eps, + effectiveness: weaponSustainedDps.effectiveness + }); + } + } + + return damage; +} + +/** + * Calculate the resistance provided by SYS pips + * @param {integer} sys the value of the SYS pips + * @returns {integer} the resistance for the given pips + */ +export function sysResistance(sys) { + return Math.pow(sys, 0.85) * 0.6 / Math.pow(4, 0.85); +} + +/** + * Obtain the recharge rate of the SYS capacitor of a power distributor given pips + * @param {Object} pd The power distributor + * @param {number} sys The number of pips to SYS + * @returns {number} The recharge rate in MJ/s + */ +export function sysRechargeRate(pd, sys) { + return pd.getSystemsRechargeRate() * Math.pow(sys, 1.1) / Math.pow(4, 1.1); +} + +/** + * Calculate the sustained DPS for a ship against an opponent at a given range + * @param {Object} ship The ship + * @param {Object} opponent The opponent ship + * @param {number} sys Pips to opponent's SYS + * @param {int} engagementrange The range between the ship and opponent + * @returns {Object} Sustained DPS for shield and armour + */ +export function sustainedDps(ship, opponent, sys, engagementrange) { + // Obtain the opponent's shield and armour metrics + const opponentShields = this.shieldMetrics(opponent, sys); + const opponentArmour = this.armourMetrics(opponent); + + return this._sustainedDps(ship, opponent, opponentShields, opponentArmour, engagementrange); +} + +/** + * Calculate the sustained DPS for a ship against an opponent at a given range + * @param {Object} ship The ship + * @param {Object} opponent The opponent ship + * @param {Object} opponentShields The opponent's shield resistances + * @param {Object} opponentArmour The opponent's armour resistances + * @param {int} engagementrange The range between the ship and opponent + * @returns {Object} Sustained DPS for shield and armour + */ +export function _sustainedDps(ship, opponent, opponentShields, opponentArmour, engagementrange) { + const shieldsdps = { + absolute: 0, + explosive: 0, + kinetic: 0, + thermal: 0 + }; + + const armoursdps = { + absolute: 0, + explosive: 0, + kinetic: 0, + thermal: 0 + }; + + let eps = 0; + + for (let i = 0; i < ship.hardpoints.length; i++) { + if (ship.hardpoints[i].m && ship.hardpoints[i].enabled && ship.hardpoints[i].maxClass > 0) { + const m = ship.hardpoints[i].m; + const sustainedDps = this._weaponSustainedDps(m, opponent, opponentShields, opponentArmour, engagementrange); + shieldsdps.absolute += sustainedDps.damage.shields.absolute; + shieldsdps.explosive += sustainedDps.damage.shields.explosive; + shieldsdps.kinetic += sustainedDps.damage.shields.kinetic; + shieldsdps.thermal += sustainedDps.damage.shields.thermal; + armoursdps.absolute += sustainedDps.damage.armour.absolute; + armoursdps.explosive += sustainedDps.damage.armour.explosive; + armoursdps.kinetic += sustainedDps.damage.armour.kinetic; + armoursdps.thermal += sustainedDps.damage.armour.thermal; + eps += sustainedDps.eps; + } + } + + return { shieldsdps, armoursdps, eps }; +} + +/** + * Calculate the sustained DPS for a weapon at a given range + * @param {Object} m The weapon + * @param {Object} opponent The opponent ship + * @param {Object} opponentShields The opponent's shield resistances + * @param {Object} opponentArmour The opponent's armour resistances + * @param {int} engagementrange The range between the ship and opponent + * @returns {Object} Sustained DPS for shield and armour + */ +export function _weaponSustainedDps(m, opponent, opponentShields, opponentArmour, engagementrange) { + const opponentHasShields = opponentShields.generator ? true : false; + const weapon = { + eps: 0, + damage: { + shields: { + absolute: 0, + explosive: 0, + kinetic: 0, + thermal: 0, + total: 0 + }, + armour: { + absolute: 0, + explosive: 0, + kinetic: 0, + thermal: 0, + total: 0 + }, + }, + effectiveness: { + shields: { + range: 1, + sys: opponentHasShields ? opponentShields.absolute.sys : 1, + resistance: 1 + }, + armour: { + range: 1, + hardness: 1, + resistance: 1 + } + } + }; + + // EPS + weapon.eps = m.getClip() ? (m.getClip() * m.getEps() / m.getRoF()) / ((m.getClip() / m.getRoF()) + m.getReload()) : m.getEps(); + + // Initial sustained DPS + let sDps = m.getClip() ? (m.getClip() * m.getDps() / m.getRoF()) / ((m.getClip() / m.getRoF()) + m.getReload()) : m.getDps(); + + // Take fall-off in to account + const falloff = m.getFalloff(); + if (falloff && engagementrange > falloff) { + const dropoffRange = m.getRange() - falloff; + const dropoff = 1 - Math.min((engagementrange - falloff) / dropoffRange, 1); + weapon.effectiveness.shields.range = weapon.effectiveness.armour.range = dropoff; + sDps *= dropoff; + } + + // Piercing/hardness modifier (for armour only) + const armourMultiple = m.getPiercing() >= opponent.hardness ? 1 : m.getPiercing() / opponent.hardness; + weapon.effectiveness.armour.hardness = armourMultiple; + + // Break out the damage according to type + let shieldsResistance = 0; + let armourResistance = 0; + if (m.getDamageDist().A) { + weapon.damage.shields.absolute += sDps * m.getDamageDist().A * (opponentHasShields ? opponentShields.absolute.total : 1); + weapon.damage.armour.absolute += sDps * m.getDamageDist().A * armourMultiple * opponentArmour.absolute.total; + shieldsResistance += m.getDamageDist().A * (opponentHasShields ? opponentShields.absolute.generator * opponentShields.absolute.boosters : 1); + armourResistance += m.getDamageDist().A * opponentArmour.absolute.bulkheads * opponentArmour.absolute.reinforcement; + } + if (m.getDamageDist().E) { + weapon.damage.shields.explosive += sDps * m.getDamageDist().E * (opponentHasShields ? opponentShields.explosive.total : 1); + weapon.damage.armour.explosive += sDps * m.getDamageDist().E * armourMultiple * opponentArmour.explosive.total; + shieldsResistance += m.getDamageDist().E * (opponentHasShields ? opponentShields.explosive.generator * opponentShields.explosive.boosters : 1); + armourResistance += m.getDamageDist().E * opponentArmour.explosive.bulkheads * opponentArmour.explosive.reinforcement; + } + if (m.getDamageDist().K) { + weapon.damage.shields.kinetic += sDps * m.getDamageDist().K * (opponentHasShields ? opponentShields.kinetic.total : 1); + weapon.damage.armour.kinetic += sDps * m.getDamageDist().K * armourMultiple * opponentArmour.kinetic.total; + shieldsResistance += m.getDamageDist().K * (opponentHasShields ? opponentShields.kinetic.generator * opponentShields.kinetic.boosters : 1); + armourResistance += m.getDamageDist().K * opponentArmour.kinetic.bulkheads * opponentArmour.kinetic.reinforcement; + } + if (m.getDamageDist().T) { + weapon.damage.shields.thermal += sDps * m.getDamageDist().T * (opponentHasShields ? opponentShields.thermal.total : 1); + weapon.damage.armour.thermal += sDps * m.getDamageDist().T * armourMultiple * opponentArmour.thermal.total; + shieldsResistance += m.getDamageDist().T * (opponentHasShields ? opponentShields.thermal.generator * opponentShields.thermal.boosters : 1); + armourResistance += m.getDamageDist().T * opponentArmour.thermal.bulkheads * opponentArmour.thermal.reinforcement; + } + weapon.damage.shields.total = weapon.damage.shields.absolute + weapon.damage.shields.explosive + weapon.damage.shields.kinetic + weapon.damage.shields.thermal; + weapon.damage.armour.total = weapon.damage.armour.absolute + weapon.damage.armour.explosive + weapon.damage.armour.kinetic + weapon.damage.armour.thermal; + + weapon.effectiveness.shields.resistance *= shieldsResistance; + weapon.effectiveness.armour.resistance *= armourResistance; + + weapon.effectiveness.shields.total = weapon.effectiveness.shields.range * weapon.effectiveness.shields.sys * weapon.effectiveness.shields.resistance; + weapon.effectiveness.armour.total = weapon.effectiveness.armour.range * weapon.effectiveness.armour.resistance * weapon.effectiveness.armour.hardness; + return weapon; +} + +/** + * Calculate time to drain WEP capacitor + * @param {object} ship The ship + * @param {number} wep Pips to WEP + * @returns {number} The time to drain the WEP capacitor, in seconds + */ +export function timeToDrainWep(ship, wep) { + let totalSEps = 0; + + for (let slotNum in ship.hardpoints) { + const slot = ship.hardpoints[slotNum]; + if (slot.maxClass > 0 && slot.m && slot.enabled && slot.type === 'WEP' && slot.m.getDps()) { + totalSEps += slot.m.getClip() ? (slot.m.getClip() * slot.m.getEps() / slot.m.getRoF()) / ((slot.m.getClip() / slot.m.getRoF()) + slot.m.getReload()) : slot.m.getEps(); + } + } + + // Calculate the drain time + const drainPerSecond = totalSEps - ship.standard[4].m.getWeaponsRechargeRate() * wep / 4; + if (drainPerSecond <= 0) { + // Can fire forever + return Infinity; + } else { + const initialCharge = ship.standard[4].m.getWeaponsCapacity(); + return initialCharge / drainPerSecond; + } +} + +/** + * Calculate the time to deplete an amount of shields or armour + * @param {number} amount The amount to be depleted + * @param {number} dps The depletion per second + * @param {number} eps The energy drained per second + * @param {number} capacity The initial energy capacity + * @param {number} recharge The energy recharged per second + * @returns {number} The number of seconds to deplete to 0 + */ +export function timeToDeplete(amount, dps, eps, capacity, recharge) { + const drainPerSecond = eps - recharge; + if (drainPerSecond <= 0) { + // Simple result + return amount / dps; + } else { + // We are draining the capacitor, but can we deplete before we run out + const timeToDrain = capacity / drainPerSecond; + const depletedBeforeDrained = dps * timeToDrain; + if (depletedBeforeDrained >= amount) { + return amount / dps; + } else { + const restToDeplete = amount - depletedBeforeDrained; + // We delete the rest at the reduced rate + const reducedDps = dps * (recharge / eps); + return timeToDrain + (restToDeplete / reducedDps); + } + } +} diff --git a/src/app/shipyard/Constants.js b/src/app/shipyard/Constants.js index a273c578..4e2d8012 100755 --- a/src/app/shipyard/Constants.js +++ b/src/app/shipyard/Constants.js @@ -47,6 +47,7 @@ export const ModuleGroupToName = { pcm: 'First Class Passenger Cabin', pcq: 'Luxury Passenger Cabin', cc: 'Collector Limpet Controller', + ss: 'Surface Scanner', // Hard Points bl: 'Beam Laser', diff --git a/src/app/shipyard/Module.js b/src/app/shipyard/Module.js index 1f2dea54..db4cb8a1 100755 --- a/src/app/shipyard/Module.js +++ b/src/app/shipyard/Module.js @@ -48,7 +48,7 @@ export default class Module { // this special effect modifies our returned value const modification = Modifications.modifications[name]; if (modification.method === 'additive') { - result = result + modifierActions[name]; + result = result + modifierActions[name] * 100; } else if (modification.method === 'overwrite') { result = modifierActions[name]; } else { @@ -162,14 +162,6 @@ export default class Module { return result; } - /** - * Return true if this is a shield generator - * @return {Boolean} if this is a shield generator - */ - isShieldGenerator() { - return (this.grp === 'sg' || this.grp === 'psg' || this.grp === 'bsg'); - } - /** * Get the power generation of this module, taking in to account modifications * @return {Number} the power generation of this module @@ -344,6 +336,14 @@ export default class Module { return this._getModifiedValue('ranget'); } + /** + * Get the scan time for this module, taking in to account modifications + * @return {Number} the scan time of this module + */ + getScanTime() { + return this._getModifiedValue('scantime'); + } + /** * Get the capture arc for this module, taking in to account modifications * @return {Number} the capture arc of this module @@ -546,10 +546,10 @@ export default class Module { getEps() { // EPS is a synthetic value let distdraw = this.getDistDraw(); - let rpshot = this.roundspershot || 1; + // We don't use rpshot here as dist draw is per combined shot let rof = this.getRoF() || 1; - return distdraw * rpshot * rof; + return distdraw * rof; } /** @@ -559,10 +559,10 @@ export default class Module { getHps() { // HPS is a synthetic value let heat = this.getThermalLoad(); - let rpshot = this.roundspershot || 1; + // We don't use rpshot here as dist draw is per combined shot let rof = this.getRoF() || 1; - return heat * rpshot * rof; + return heat * rof; } /** @@ -672,14 +672,6 @@ export default class Module { return this._getModifiedValue('rebuildsperbay'); } - /** - * Get the cells for this module, taking in to account modifications - * @return {Number} the cells for this module - */ - getCells() { - return this._getModifiedValue('cells'); - } - /** * Get the jitter for this module, taking in to account modifications * @return {Number} the jitter for this module diff --git a/src/app/shipyard/ModuleSet.js b/src/app/shipyard/ModuleSet.js index 89fb0b55..1e1f3982 100755 --- a/src/app/shipyard/ModuleSet.js +++ b/src/app/shipyard/ModuleSet.js @@ -126,7 +126,7 @@ export default class ModuleSet { let pd = this.standard[4][0]; for (let p of this.standard[4]) { - if (p.mass < pd.mass && p.engcap >= boostEnergy) { + if (p.mass < pd.mass && p.engcap > boostEnergy) { pd = p; } } diff --git a/src/app/shipyard/Ship.js b/src/app/shipyard/Ship.js index e369f259..0de8b2ef 100755 --- a/src/app/shipyard/Ship.js +++ b/src/app/shipyard/Ship.js @@ -1,6 +1,7 @@ import * as Calc from './Calculations'; import * as ModuleUtils from './ModuleUtils'; import * as Utils from '../utils/UtilityFunctions'; +import { getBlueprint } from '../utils/BlueprintFunctions'; import Module from './Module'; import LZString from 'lz-string'; import * as _ from 'lodash'; @@ -122,31 +123,24 @@ export default class Ship { /** * Can the ship thrust/move + * @param {Number} cargo Amount of cargo in the ship + * @param {Number} fuel Amount of fuel in the ship * @return {[type]} True if thrusters operational */ - canThrust() { + canThrust(cargo, fuel) { return this.getSlotStatus(this.standard[1]) == 3 && // Thrusters are powered - this.ladenMass < this.standard[1].m.getMaxMass(); // Max mass not exceeded + this.unladenMass + cargo + fuel < this.standard[1].m.getMaxMass(); // Max mass not exceeded } /** * Can the ship boost + * @param {Number} cargo Amount of cargo in the ship + * @param {Number} fuel Amount of fuel in the ship * @return {[type]} True if boost capable */ - canBoost() { - return this.canThrust() && // Thrusters operational - this.boostEnergy <= this.standard[4].m.getEnginesCapacity(); // PD capacitor is sufficient for boost - } - - /** - * Calculate hypothetical jump range using the installed FSD and the - * specified mass which can be more or less than ships actual mass - * @param {Number} fuel Fuel available in tons - * @param {Number} cargo Cargo in tons - * @return {Number} Jump range in Light Years - */ - calcJumpRangeWith(fuel, cargo) { - return Calc.jumpRange(this.unladenMass + fuel + cargo, this.standard[2].m, fuel); + canBoost(cargo, fuel) { + return this.canThrust(cargo, fuel) && // Thrusters operational + this.standard[4].m.getEnginesCapacity() > this.boostEnergy; // PD capacitor is sufficient for boost } /** @@ -173,17 +167,6 @@ export default class Ship { return Calc.jumpRange(this.unladenMass + (massDelta || 0) + Math.min(fsdMaxFuelPerJump, fuel || this.fuelCapacity), fsd || this.standard[2].m, fuel); } - /** - * Calculate cumulative (total) jump range when making longest jumps using the installed FSD and the - * specified mass which can be more or less than ships actual mass - * @param {Number} fuel Fuel available in tons - * @param {Number} cargo Cargo in tons - * @return {Number} Total/Cumulative Jump range in Light Years - */ - calcFastestRangeWith(fuel, cargo) { - return Calc.fastestRange(this.unladenMass + fuel + cargo, this.standard[2].m, fuel); - } - /** * Calculate the hypothetical top speeds at cargo and fuel tonnage * @param {Number} fuel Fuel available in tons @@ -195,36 +178,51 @@ export default class Ship { } /** - * Calculate the recovery time after losing or turning on shields - * Thanks to CMDRs Al Gray, GIF, and Nomad Enigma for providing Shield recharge data and formulas - * - * @return {Number} Recovery time in seconds + * Calculate the speed for a given configuration + * @param {Number} eng Number of pips in ENG + * @param {Number} fuel Amount of fuel carried + * @param {Number} cargo Amount of cargo carried + * @param {boolean} boost true if boost is applied + * @return {Number} Speed */ - calcShieldRecovery() { - const shieldGenerator = this.findShieldGenerator(); - if (shieldGenerator) { - const brokenRegenRate = shieldGenerator.getBrokenRegenerationRate(); - // 50% of shield strength / broken recharge rate + 15 second delay before recharge starts - return ((this.shield / 2) / brokenRegenRate) + 15; - } - return 0; + calcSpeed(eng, fuel, cargo, boost) { + return Calc.calcSpeed(this.unladenMass + fuel + cargo, this.speed, this.standard[1].m, this.pipSpeed, eng, this.boost / this.speed, boost); } /** - * Calculate the recharge time for a shield going from 50% to 100% - * Thanks to CMDRs Al Gray, GIF, and Nomad Enigma for providing Shield recharge data and formulas - * - * @return {Number} 50 - 100% Recharge time in seconds + * Calculate the pitch for a given configuration + * @param {Number} eng Number of pips in ENG + * @param {Number} fuel Amount of fuel carried + * @param {Number} cargo Amount of cargo carried + * @param {boolean} boost true if boost is applied + * @return {Number} Pitch */ - calcShieldRecharge() { - const shieldGenerator = this.findShieldGenerator(); - if (shieldGenerator) { - const regenRate = shieldGenerator.getRegenerationRate(); + calcPitch(eng, fuel, cargo, boost) { + return Calc.calcPitch(this.unladenMass + fuel + cargo, this.pitch, this.standard[1].m, this.pipSpeed, eng, this.topBoost / this.topSpeed, boost); + } - // 50% of shield strength / recharge rate - return (this.shield / 2) / regenRate; - } - return 0; + /** + * Calculate the roll for a given configuration + * @param {Number} eng Number of pips in ENG + * @param {Number} fuel Amount of fuel carried + * @param {Number} cargo Amount of cargo carried + * @param {boolean} boost true if boost is applied + * @return {Number} Roll + */ + calcRoll(eng, fuel, cargo, boost) { + return Calc.calcRoll(this.unladenMass + fuel + cargo, this.roll, this.standard[1].m, this.pipSpeed, eng, this.topBoost / this.topSpeed, boost); + } + + /** + * Calculate the yaw for a given configuration + * @param {Number} eng Number of pips in ENG + * @param {Number} fuel Amount of fuel carried + * @param {Number} cargo Amount of cargo carried + * @param {boolean} boost true if boost is applied + * @return {Number} Yaw + */ + calcYaw(eng, fuel, cargo, boost) { + return Calc.calcYaw(this.unladenMass + fuel + cargo, this.yaw, this.standard[1].m, this.pipSpeed, eng, this.topBoost / this.topSpeed, boost); } /** @@ -427,7 +425,6 @@ export default class Ship { .recalculateDps() .recalculateEps() .recalculateHps() - .recalculateTtd() .updateMovement(); } @@ -496,15 +493,15 @@ export default class Ship { this.recalculateDps(); this.recalculateHps(); this.recalculateEps(); - this.recalculateTtd(); } else if (name === 'explres' || name === 'kinres' || name === 'thermres') { m.setModValue(name, value, sentfromui); // Could be for shields or armour this.recalculateArmour(); this.recalculateShield(); - } else if (name === 'wepcap' || name === 'weprate') { + } else if (name === 'engcap') { m.setModValue(name, value, sentfromui); - this.recalculateTtd(); + // Might have resulted in a change in boostability + this.updateMovement(); } else { // Generic m.setModValue(name, value, sentfromui); @@ -563,13 +560,18 @@ export default class Ship { this.bulkheads.m = null; this.useBulkhead(comps && comps.bulkheads ? comps.bulkheads : 0, true); this.bulkheads.m.mods = mods && mods[0] ? mods[0] : {}; - this.bulkheads.m.blueprint = blueprints && blueprints[0] ? blueprints[0] : {}; + if (blueprints && blueprints[0]) { + this.bulkheads.m.blueprint = getBlueprint(blueprints[0].fdname, this.bulkheads.m); + this.bulkheads.m.blueprint.grade = blueprints[0].grade; + this.bulkheads.m.blueprint.special = blueprints[0].special; + } else { + this.bulkheads.m.blueprint = {}; + } this.cargoHatch.priority = priorities ? priorities[0] * 1 : 0; this.cargoHatch.enabled = enabled ? enabled[0] * 1 : true; for (i = 0; i < cl; i++) { standard[i].cat = 0; - standard[i].enabled = enabled ? enabled[i + 1] * 1 : true; standard[i].priority = priorities && priorities[i + 1] ? priorities[i + 1] * 1 : 0; standard[i].type = 'SYS'; standard[i].m = null; // Resetting 'old' modul if there was one @@ -578,10 +580,17 @@ export default class Ship { let module = ModuleUtils.standard(i, comps.standard[i]); if (module != null) { module.mods = mods && mods[i + 1] ? mods[i + 1] : {}; - module.blueprint = blueprints && blueprints[i + 1] ? blueprints[i + 1] : {}; + if (blueprints && blueprints[i + 1]) { + module.blueprint = getBlueprint(blueprints[i + 1].fdname, module); + module.blueprint.grade = blueprints[i + 1].grade; + module.blueprint.special = blueprints[i + 1].special; + } else { + module.blueprint = {}; + } } this.use(standard[i], module, true); } + standard[i].enabled = enabled ? enabled[i + 1] * 1 : true; } standard[1].type = 'ENG'; // Thrusters @@ -590,7 +599,6 @@ export default class Ship { for (i = 0, l = hps.length; i < l; i++) { hps[i].cat = 1; - hps[i].enabled = enabled ? enabled[cl + i] * 1 : true; hps[i].priority = priorities && priorities[cl + i] ? priorities[cl + i] * 1 : 0; hps[i].type = hps[i].maxClass ? 'WEP' : 'SYS'; hps[i].m = null; // Resetting 'old' modul if there was one @@ -600,17 +608,23 @@ export default class Ship { let module = ModuleUtils.hardpoints(comps.hardpoints[i]); if (module != null) { module.mods = mods && mods[cl + i] ? mods[cl + i] : {}; - module.blueprint = blueprints && blueprints[cl + i] ? blueprints[cl + i] : {}; + if (blueprints && blueprints[cl + i]) { + module.blueprint = getBlueprint(blueprints[cl + i].fdname, module); + module.blueprint.grade = blueprints[cl + i].grade; + module.blueprint.special = blueprints[cl + i].special; + } else { + module.blueprint = {}; + } } this.use(hps[i], module, true); } + hps[i].enabled = enabled ? enabled[cl + i] * 1 : true; } cl += hps.length; // Increase accounts for hardpoints for (i = 0, l = internal.length; i < l; i++) { internal[i].cat = 2; - internal[i].enabled = enabled ? enabled[cl + i] * 1 : true; internal[i].priority = priorities && priorities[cl + i] ? priorities[cl + i] * 1 : 0; internal[i].type = 'SYS'; internal[i].m = null; // Resetting 'old' modul if there was one @@ -620,10 +634,17 @@ export default class Ship { let module = ModuleUtils.internal(comps.internal[i]); if (module != null) { module.mods = mods && mods[cl + i] ? mods[cl + i] : {}; - module.blueprint = blueprints && blueprints[cl + i] ? blueprints[cl + i] : {}; + if (blueprints && blueprints[cl + i]) { + module.blueprint = getBlueprint(blueprints[cl + i].fdname, module); + module.blueprint.grade = blueprints[cl + i].grade; + module.blueprint.special = blueprints[cl + i].special; + } else { + module.blueprint = {}; + } } this.use(internal[i], module, true); } + internal[i].enabled = enabled ? enabled[cl + i] * 1 : true; } // Update aggragated stats @@ -638,7 +659,6 @@ export default class Ship { .recalculateDps() .recalculateEps() .recalculateHps() - .recalculateTtd() .updateMovement(); } @@ -820,7 +840,6 @@ export default class Ship { if (slot.m.getEps()) { this.recalculateEps(); - this.recalculateTtd(); } } } @@ -896,7 +915,6 @@ export default class Ship { } if (epsChanged) { this.recalculateEps(); - this.recalculateTtd(); } if (hpsChanged) { this.recalculateHps(); @@ -904,9 +922,6 @@ export default class Ship { if (powerGeneratedChange) { this.updatePowerGenerated(); } - if (powerDistributorChange) { - this.recalculateTtd(); - } if (powerUsedChange) { this.updatePowerUsed(); } @@ -944,33 +959,6 @@ export default class Ship { return val; } - /** - * Calculate time to drain WEP capacitor - * @return {this} The ship instance (for chaining operations) - */ - recalculateTtd() { - let totalSEps = 0; - - for (let slotNum in this.hardpoints) { - const slot = this.hardpoints[slotNum]; - if (slot.m && slot.enabled && slot.type === 'WEP' && slot.m.getDps()) { - totalSEps += slot.m.getClip() ? (slot.m.getClip() * slot.m.getEps() / slot.m.getRoF()) / ((slot.m.getClip() / slot.m.getRoF()) + slot.m.getReload()) : slot.m.getEps(); - } - } - - // Calculate the drain time - const drainPerSecond = totalSEps - this.standard[4].m.getWeaponsRechargeRate(); - if (drainPerSecond <= 0) { - // Can fire forever - this.timeToDrain = Infinity; - } else { - const initialCharge = this.standard[4].m.getWeaponsCapacity(); - this.timeToDrain = initialCharge / drainPerSecond; - } - - return this; - } - /** * Calculate damage per second and related items for weapons * @return {this} The ship instance (for chaining operations) @@ -1201,7 +1189,7 @@ export default class Ship { updateMovement() { this.speeds = Calc.speed(this.unladenMass + this.fuelCapacity, this.speed, this.standard[1].m, this.pipSpeed); this.topSpeed = this.speeds[4]; - this.topBoost = this.canBoost() ? this.speeds[4] * this.boost / this.speed : 0; + this.topBoost = this.canBoost(0, 0) ? this.speeds[4] * this.boost / this.speed : 0; this.pitches = Calc.pitch(this.unladenMass + this.fuelCapacity, this.pitch, this.standard[1].m, this.pipSpeed); this.topPitch = this.pitches[4]; @@ -1220,54 +1208,13 @@ export default class Ship { * @return {this} The ship instance (for chaining operations) */ recalculateShield() { - let shield = 0; - let shieldBoost = 1; - let shieldExplRes = null; - let shieldKinRes = null; - let shieldThermRes = null; - let shieldExplDRStart = null; - let shieldExplDREnd = null; - let shieldKinDRStart = null; - let shieldKinDREnd = null; - let shieldThermDRStart = null; - let shieldThermDREnd = null; - - const sgSlot = this.findInternalByGroup('sg'); - if (sgSlot && sgSlot.enabled) { - // Shield from generator - shield = Calc.shieldStrength(this.hullMass, this.baseShieldStrength, sgSlot.m, 1); - shieldExplRes = 1 - sgSlot.m.getExplosiveResistance(); - shieldExplDRStart = shieldExplRes * 0.7; - shieldExplDREnd = shieldExplRes * 0; // Currently don't know where this is - shieldKinRes = 1 - sgSlot.m.getKineticResistance(); - shieldKinDRStart = shieldKinRes * 0.7; - shieldKinDREnd = shieldKinRes * 0; // Currently don't know where this is - shieldThermRes = 1 - sgSlot.m.getThermalResistance(); - shieldThermDRStart = shieldThermRes * 0.7; - shieldThermDREnd = shieldThermRes * 0; // Currently don't know where this is - - // Shield from boosters - for (let slot of this.hardpoints) { - if (slot.enabled && slot.m && slot.m.grp == 'sb') { - shieldBoost += slot.m.getShieldBoost(); - shieldExplRes *= (1 - slot.m.getExplosiveResistance()); - shieldKinRes *= (1 - slot.m.getKineticResistance()); - shieldThermRes *= (1 - slot.m.getThermalResistance()); - } - } - } - - // We apply diminishing returns to the boosted value - // (no we don't; FD pulled back on this idea. But leave this here in case they reinstate it) - // shieldBoost = Math.min(shieldBoost, (1 - Math.pow(Math.E, -0.7 * shieldBoost)) * 2.5); - - shield = shield * shieldBoost; - - this.shield = shield; - this.shieldExplRes = shieldExplRes ? 1 - this.diminishingReturns(shieldExplRes, shieldExplDREnd, shieldExplDRStart) : null; - this.shieldKinRes = shieldKinRes ? 1 - this.diminishingReturns(shieldKinRes, shieldKinDREnd, shieldKinDRStart) : null; - this.shieldThermRes = shieldThermRes ? 1 - this.diminishingReturns(shieldThermRes, shieldThermDREnd, shieldThermDRStart) : null; + // Obtain shield metrics with 0 pips to sys (parts affected by SYS aren't used here) + const metrics = Calc.shieldMetrics(this, 0); + this.shield = metrics.generator ? metrics.generator + metrics.boosters : 0; + this.shieldExplRes = this.shield > 0 ? 1 - metrics.explosive.total : null; + this.shieldKinRes = this.shield > 0 ? 1 - metrics.kinetic.total : null; + this.shieldThermRes = this.shield > 0 ? 1 - metrics.thermal.total : null; return this; } @@ -1280,7 +1227,9 @@ export default class Ship { for (let slot of this.internal) { if (slot.m && slot.m.grp == 'scb') { - shieldCells += slot.m.getShieldReinforcement() * slot.m.getCells(); + // There is currently a bug with Elite where you can have a clip > 1 thanks to engineering but it doesn't do anything, + // so we need to hard-code clip to 1 + shieldCells += slot.m.getShieldReinforcement() * slot.m.getDuration() * (slot.m.getAmmo() + 1); } } @@ -1301,13 +1250,13 @@ export default class Ship { let moduleprotection = 1; let hullExplRes = 1 - bulkhead.getExplosiveResistance(); const hullExplResDRStart = hullExplRes * 0.7; - const hullExplResDREnd = hullExplRes * 0; // Currently don't know where this is + const hullExplResDREnd = hullExplRes * 0; let hullKinRes = 1 - bulkhead.getKineticResistance(); const hullKinResDRStart = hullKinRes * 0.7; - const hullKinResDREnd = hullKinRes * 0; // Currently don't know where this is + const hullKinResDREnd = hullKinRes * 0; let hullThermRes = 1 - bulkhead.getThermalResistance(); const hullThermResDRStart = hullThermRes * 0.7; - const hullThermResDREnd = hullThermRes * 0; // Currently don't know where this is + const hullThermResDREnd = hullThermRes * 0; // Armour from HRPs and module armour from MRPs for (let slot of this.internal) { @@ -1347,9 +1296,9 @@ export default class Ship { this.unladenRange = this.calcUnladenRange(); // Includes fuel weight for jump this.fullTankRange = Calc.jumpRange(unladenMass + fuelCapacity, fsd); // Full Tank this.ladenRange = this.calcLadenRange(); // Includes full tank and caro - this.unladenFastestRange = Calc.fastestRange(unladenMass, fsd, fuelCapacity); - this.ladenFastestRange = Calc.fastestRange(unladenMass + this.cargoCapacity, fsd, fuelCapacity); - this.maxJumpCount = Math.ceil(fuelCapacity / fsd.maxfuel); + this.unladenFastestRange = Calc.totalJumpRange(unladenMass + this.fuelCapacity, fsd, fuelCapacity); + this.ladenFastestRange = Calc.totalJumpRange(unladenMass + this.fuelCapacity + this.cargoCapacity, fsd, fuelCapacity); + this.maxJumpCount = Math.ceil(fuelCapacity / fsd.getMaxFuelPerJump()); return this; } @@ -1641,6 +1590,7 @@ export default class Ship { } let oldModule = slot.m; slot.m = m; + slot.enabled = true; slot.discountedCost = (m && m.cost) ? m.cost * this.moduleCostMultiplier : 0; this.updateStats(slot, m, oldModule, preventUpdate); @@ -1682,6 +1632,25 @@ export default class Ship { return this; } + /** + * Calculate the lowest possible mass for this ship. + * @param {Object} m Module override set (standard type => Module) + * @return {number} The lowest possible mass for this ship + */ + calcLowestPossibleMass(m) { + m = m || {}; + + let mass = this.hullMass; + mass += m.pp ? m.pp.getMass() : ModuleUtils.standard(0, '2D').getMass(); + mass += m.th ? m.th.getMass() : ModuleUtils.standard(1, '2D').getMass(); + mass += m.fsd ? m.fsd.getMass() : ModuleUtils.standard(2, '2D').getMass(); + mass += m.ls ? m.ls.getMass() : ModuleUtils.standard(3, this.standard[3].maxClass + 'D').getMass() * 0.3; // Lightweight grade 4 mod reduces mass by up to 70% + mass += m.pd ? m.pd.getMass() : ModuleUtils.standard(4, '1D').getMass(); + mass += m.s ? m.s.getMass() : ModuleUtils.standard(5, this.standard[5].maxClass + 'D').getMass() * 0.2; // Lightweight grade 5 mod reduces mass by up to 80% + // Ignore fuel tank as it could be empty + return mass; + } + /** * Use the lightest standard ModuleUtils unless otherwise specified * @param {Object} m Module override set (standard type => module ID) diff --git a/src/app/shipyard/ShipRoles.js b/src/app/shipyard/ShipRoles.js index 8be5c12a..4ddf91fc 100644 --- a/src/app/shipyard/ShipRoles.js +++ b/src/app/shipyard/ShipRoles.js @@ -208,14 +208,14 @@ export function miner(ship, shielded) { // Cargo hatch should be enabled ship.setSlotEnabled(ship.cargoHatch, true); - // 4A or largest possible refinery + // Largest possible refinery const refineryOrder = [4, 5, 6, 7, 8, 3, 2, 1]; const refineryInternals = ship.internal.filter(a => usedSlots.indexOf(a) == -1) .filter(a => (!a.eligible) || a.eligible.rf) .sort((a,b) => refineryOrder.indexOf(a.maxClass) - refineryOrder.indexOf(b.maxClass)); for (let i = 0; i < refineryInternals.length; i++) { if (canMount(ship, refineryInternals[i], 'rf')) { - ship.use(refineryInternals[i], ModuleUtils.findInternal('rf', refineryInternals[i].maxClass, 'A')); + ship.use(refineryInternals[i], ModuleUtils.findInternal('rf', Math.min(refineryInternals[i].maxClass, 4), 'A')); usedSlots.push(refineryInternals[i]); break; } diff --git a/src/app/stores/Persist.js b/src/app/stores/Persist.js index 4ef740f6..e1ea7b82 100644 --- a/src/app/stores/Persist.js +++ b/src/app/stores/Persist.js @@ -5,6 +5,7 @@ const LS_KEY_BUILDS = 'builds'; const LS_KEY_COMPARISONS = 'comparisons'; const LS_KEY_LANG = 'NG_TRANSLATE_LANG_KEY'; const LS_KEY_COST_TAB = 'costTab'; +const LS_KEY_OUTFITTING_TAB = 'outfittingTab'; const LS_KEY_INSURANCE = 'insurance'; const LS_KEY_SHIP_DISCOUNT = 'shipDiscount'; const LS_KEY_MOD_DISCOUNT = 'moduleDiscount'; @@ -98,6 +99,7 @@ export class Persist extends EventEmitter { this.builds = buildJson && typeof buildJson == 'object' ? buildJson : {}; this.comparisons = comparisonJson && typeof comparisonJson == 'object' ? comparisonJson : {}; this.costTab = _getString(LS_KEY_COST_TAB); + this.outfittingTab = _getString(LS_KEY_OUTFITTING_TAB); this.state = _get(LS_KEY_STATE); this.sizeRatio = _get(LS_KEY_SIZE_RATIO) || 1; this.tooltipsEnabled = tips === null ? true : tips; @@ -472,6 +474,22 @@ export class Persist extends EventEmitter { return this.costTab; } + /** + * Persist selected outfitting tab + * @param {string} tabName Cost tab name + */ + setOutfittingTab(tabName) { + this.outfittingTab = tabName; + _put(LS_KEY_OUTFITTING_TAB, tabName); + } + /** + * Get the current outfitting tab + * @return {string} the current outfitting tab + */ + getOutfittingTab() { + return this.outfittingTab; + } + /** * Retrieve the last router state from local storage * @return {Object} state State object containing state name and params diff --git a/src/app/utils/BlueprintFunctions.js b/src/app/utils/BlueprintFunctions.js new file mode 100644 index 00000000..eaaffaec --- /dev/null +++ b/src/app/utils/BlueprintFunctions.js @@ -0,0 +1,229 @@ +import React from 'react'; +import { Modifications } from 'coriolis-data/dist'; + +/** + * Generate a tooltip with details of a blueprint's effects + * @param {Object} translate The translate object + * @param {Object} blueprint The blueprint at the required grade + * @param {Array} engineers The engineers supplying this blueprint + * @param {string} grp The group of the module + * @param {Object} m The module to compare with + * @returns {Object} The react components + */ +export function blueprintTooltip(translate, blueprint, engineers, grp, m) { + const effects = []; + for (const feature in blueprint.features) { + const featureIsBeneficial = isBeneficial(feature, blueprint.features[feature]); + const featureDef = Modifications.modifications[feature]; + if (!featureDef.hidden) { + let symbol = ''; + if (feature === 'jitter') { + symbol = '°'; + } else if (featureDef.type === 'percentage') { + symbol = '%'; + } + let lowerBound = blueprint.features[feature][0]; + let upperBound = blueprint.features[feature][1]; + if (featureDef.type === 'percentage') { + lowerBound = Math.round(lowerBound * 1000) / 10; + upperBound = Math.round(upperBound * 1000) / 10; + } + const lowerIsBeneficial = isValueBeneficial(feature, lowerBound); + const upperIsBeneficial = isValueBeneficial(feature, upperBound); + if (m) { + // We have a module - add in the current value + let current = m.getModValue(feature); + if (featureDef.type === 'percentage' || featureDef.name === 'burst' || featureDef.name === 'burstrof') { + current = Math.round(current / 10) / 10; + } else if (featureDef.type === 'numeric') { + current /= 100; + } + const currentIsBeneficial = isValueBeneficial(feature, current); + effects.push( +
    + + + + + + ); + } else { + // We do not have a module, no value + effects.push( + + + + + + ); + } + } + } + if (m) { + // Because we have a module add in any benefits that aren't part of the primary blueprint + for (const feature in m.mods) { + if (!blueprint.features[feature]) { + const featureDef = Modifications.modifications[feature]; + let symbol = ''; + if (feature === 'jitter') { + symbol = '°'; + } else if (featureDef.type === 'percentage') { + symbol = '%'; + } + let current = m.getModValue(feature); + if (featureDef.type === 'percentage' || featureDef.name === 'burst' || featureDef.name === 'burstrof') { + current = Math.round(current / 10) / 10; + } else if (featureDef.type === 'numeric') { + current /= 100; + } + const currentIsBeneficial = isValueBeneficial(feature, current); + effects.push( + + + + + + + ); + } + } + } + let components; + if (!m) { + components = []; + for (const component in blueprint.components) { + components.push( + + + + + ); + } + } + + let engineersList; + if (engineers) { + engineersList = []; + for (const engineer of engineers) { + engineersList.push( + + + + ); + } + } + + return ( +
    +
    {translate('speed')}{translate('boost')}{translate('DPS')}{translate('EPS')}{translate('TTD')}{translate('HPS')}{translate('hrd')}{translate('arm')}{translate('shld')}{translate('mass')}{translate('speed')}{translate('boost')}{translate('jump range')}{translate('shield')}{translate('integrity')}{translate('DPS')}{translate('EPS')}{translate('TTD')}{translate('HPS')}{translate('cargo')} {translate('fuel')}{translate('jump range')}{translate('fastest range')}{translate('mass')}{translate('hrd')} {translate('crew')}{translate('MLF')}{translate('MLF')}
    {translate('hull')}{translate('unladen')}{translate('laden')} {translate('max')}{translate('full tank')}{translate('unladen')} {translate('laden')}{translate('jumps')}{translate('total unladen')}{translate('total laden')}{translate('hull')} {translate('unladen')} {translate('laden')}
    { ship.canThrust() ? {int(ship.topSpeed)} {u['m/s']} : 0 }{ ship.canBoost() ? {int(ship.topBoost)} {u['m/s']} : 0 }{f1(ship.totalDps)}{f1(ship.totalEps)}{ship.timeToDrain === Infinity ? '∞' : time(ship.timeToDrain)}{f1(ship.totalHps)}{ canThrust ? {int(ship.calcSpeed(4, ship.fuelCapacity, 0, false))}{u['m/s']} : 0 }{ canBoost ? {int(ship.calcSpeed(4, ship.fuelCapacity, 0, true))}{u['m/s']} : 0 }{f2(Calc.jumpRange(ship.unladenMass + ship.standard[2].m.getMaxFuelPerJump(), ship.standard[2].m, ship.standard[2].m.getMaxFuelPerJump()))}{u.LY}{f2(Calc.jumpRange(ship.unladenMass + ship.fuelCapacity, ship.standard[2].m, ship.fuelCapacity))}{u.LY}{f2(Calc.jumpRange(ship.unladenMass + ship.fuelCapacity + ship.cargoCapacity, ship.standard[2].m, ship.fuelCapacity))}{u.LY}{f2(Calc.totalJumpRange(ship.unladenMass + ship.fuelCapacity, ship.standard[2].m, ship.fuelCapacity))}{u.LY}{f2(Calc.totalJumpRange(ship.unladenMass + ship.fuelCapacity + ship.cargoCapacity, ship.standard[2].m, ship.fuelCapacity))}{u.LY}{int(ship.shield)}{u.MJ}{int(ship.armour)}{f1(ship.totalDps)}{f1(ship.totalEps)}{timeToDrain === Infinity ? '∞' : time(timeToDrain)}{f1(ship.totalHps)}{round(ship.cargoCapacity)}{u.T}{round(ship.fuelCapacity)}{u.T}{ship.hullMass}{u.T}{int(ship.unladenMass)}{u.T}{int(ship.ladenMass)}{u.T} {int(ship.hardness)}{int(ship.armour)}{int(ship.shield)} {u.MJ}{ship.hullMass} {u.T}{int(ship.unladenMass)} {u.T}{int(ship.ladenMass)} {u.T}{round(ship.cargoCapacity)} {u.T}{round(ship.fuelCapacity)} {u.T}{f2(ship.unladenRange)} {u.LY}{f2(ship.fullTankRange)} {u.LY}{f2(ship.ladenRange)} {u.LY}{int(ship.maxJumpCount)}{f2(ship.unladenFastestRange)} {u.LY}{f2(ship.ladenFastestRange)} {u.LY} {ship.crew} {ship.masslock}
    {translate(feature, grp)}{lowerBound}{symbol}{current}{symbol}{upperBound}{symbol}
    {translate(feature, grp)}{lowerBound}{symbol}{upperBound}{symbol}
    {translate(feature, grp)} {current}{symbol} 
    {translate(component)}{blueprint.components[component]}
    {engineer}
    + + + + + {m ? : null } + + + + + {effects} + +
    {translate('feature')}{translate('worst')}{translate('current')}{translate('best')}
    + { components ? + + + + + + + + {components} + +
    {translate('component')}{translate('amount')}
    : null } + { engineersList ? + + + + + + + {engineersList} + +
    {translate('engineers')}
    : null } +
    + ); +} + +/** + * Is this blueprint feature beneficial? + * @param {string} feature The name of the feature + * @param {array} values The value of the feature + * @returns {boolean} True if this feature is beneficial + */ +export function isBeneficial(feature, values) { + const fact = (values[0] < 0 || (values[0] === 0 && values[1] < 0)); + if (Modifications.modifications[feature].higherbetter) { + return !fact; + } else { + return fact; + } +} + +/** + * Is this feature value beneficial? + * @param {string} feature The name of the feature + * @param {number} value The value of the feature + * @returns {boolean} True if this value is beneficial + */ +export function isValueBeneficial(feature, value) { + if (Modifications.modifications[feature].higherbetter) { + return value > 0; + } else { + return value < 0; + } +} + +/** + * Get a blueprint with a given name and an optional module + * @param {string} name The name of the blueprint + * @param {Object} module The module for which to obtain this blueprint + * @returns {Object} The matching blueprint + */ +export function getBlueprint(name, module) { + // Start with a copy of the blueprint + const blueprint = JSON.parse(JSON.stringify(Modifications.blueprints[name])); + if (module) { + if (module.grp === 'bh' || module.grp === 'hr' || module.grp === 'sg' || module.grp === 'psg' || module.grp === 'bsg') { + // Bulkheads, hull reinforcements and shield generators need to have their resistances altered by the base values + for (const grade in blueprint.grades) { + for (const feature in blueprint.grades[grade].features) { + if (feature === 'explres') { + blueprint.grades[grade].features[feature][0] *= (1 - module.explres); + blueprint.grades[grade].features[feature][1] *= (1 - module.explres); + } + if (feature === 'kinres') { + blueprint.grades[grade].features[feature][0] *= (1 - module.kinres); + blueprint.grades[grade].features[feature][1] *= (1 - module.kinres); + } + if (feature === 'thermres') { + blueprint.grades[grade].features[feature][0] *= (1 - module.thermres); + blueprint.grades[grade].features[feature][1] *= (1 - module.thermres); + } + } + } + } + if (module.grp === 'sb') { + // Shield boosters are treated internally as straight modifiers, so rather than (for example) + // being a 4% boost they are a 104% multiplier. We need to fix the values here so that they look + // accurate as per the information in Elite + for (const grade in blueprint.grades) { + for (const feature in blueprint.grades[grade].features) { + if (feature === 'shieldboost') { + blueprint.grades[grade].features[feature][0] = ((1 + blueprint.grades[grade].features[feature][0]) * (1 + module.shieldboost) - 1) / module.shieldboost - 1; + blueprint.grades[grade].features[feature][1] = ((1 + blueprint.grades[grade].features[feature][1]) * (1 + module.shieldboost) - 1) / module.shieldboost - 1; + } + } + } + } + } + return blueprint; +} diff --git a/src/app/utils/CompanionApiUtils.js b/src/app/utils/CompanionApiUtils.js index 461886cb..25124864 100644 --- a/src/app/utils/CompanionApiUtils.js +++ b/src/app/utils/CompanionApiUtils.js @@ -2,6 +2,8 @@ import React from 'react'; import { Modifications, Modules, Ships } from 'coriolis-data/dist'; import Module from '../shipyard/Module'; import Ship from '../shipyard/Ship'; +import { getBlueprint } from '../utils/BlueprintFunctions'; +import * as ModuleUtils from '../shipyard/ModuleUtils'; // mapping from fd's ship model names to coriolis' const SHIP_FD_NAME_TO_CORIOLIS_NAME = { @@ -129,9 +131,15 @@ export function shipFromJson(json) { let ship = new Ship(shipModel, shipTemplate.properties, shipTemplate.slots); ship.buildWith(null); - // Set the cargo hatch. We don't have any information on it so guess it's priority 5 and disabled - ship.cargoHatch.enabled = false; - ship.cargoHatch.priority = 4; + // Set the cargo hatch + if (json.modules.CargoHatch) { + ship.cargoHatch.enabled = json.modules.CargoHatch.module.on == true; + ship.cargoHatch.priority = json.modules.CargoHatch.module.priority; + } else { + // We don't have any information on it so guess it's priority 5 and disabled + ship.cargoHatch.enabled = false; + ship.cargoHatch.priority = 4; + } // Add the bulkheads const armourJson = json.modules.Armour.module; @@ -335,9 +343,9 @@ function _addModifications(module, modifiers, blueprint, grade) { } } - // Add the blueprint ID, grade and special + // Add the blueprint definition, grade and special if (blueprint) { - module.blueprint = Object.assign({}, Modifications.blueprints[blueprint]); + module.blueprint = getBlueprint(blueprint, module); if (grade) { module.blueprint.grade = Number(grade); } @@ -371,7 +379,7 @@ function _addModifications(module, modifiers, blueprint, grade) { // Shield generator resistance is actually a damage modifier, so needs to be inverted. // In addition, the modification is based off the inherent resistance of the module - if (module.isShieldGenerator()) { + if (ModuleUtils.isShieldGenerator(module.grp)) { if (module.getModValue('explres')) { module.setModValue('explres', ((1 - (1 - module.explres) * (1 + module.getModValue('explres') / 10000)) - module.explres) * 10000); } @@ -384,15 +392,16 @@ function _addModifications(module, modifiers, blueprint, grade) { } // Hull reinforcement package resistance is actually a damage modifier, so needs to be inverted. + // In addition, the modification is based off the inherent resistance of the module if (module.grp === 'hr') { if (module.getModValue('explres')) { - module.setModValue('explres', ((module.getModValue('explres') / 10000) * -1) * 10000); + module.setModValue('explres', ((1 - (1 - module.explres) * (1 + module.getModValue('explres') / 10000)) - module.explres) * 10000); } if (module.getModValue('kinres')) { - module.setModValue('kinres', ((module.getModValue('kinres') / 10000) * -1) * 10000); + module.setModValue('kinres', ((1 - (1 - module.kinres) * (1 + module.getModValue('kinres') / 10000)) - module.kinres) * 10000); } if (module.getModValue('thermres')) { - module.setModValue('thermres', ((module.getModValue('thermres') / 10000) * -1) * 10000); + module.setModValue('thermres', ((1 - (1 - module.thermres) * (1 + module.getModValue('thermres') / 10000)) - module.thermres) * 10000); } } @@ -425,4 +434,10 @@ function _addModifications(module, modifiers, blueprint, grade) { if (module.getModValue('rof')) { module.setModValue('rof', ((1 / (1 + module.getModValue('rof') / 10000)) - 1) * 10000); } + + // Clip size is rounded up so that the result is a whole number + if (module.getModValue('clip')) { + const individual = 1 / (module.clip || 1); + module.setModValue('clip', Math.ceil((module.getModValue('clip') / 10000) / individual) * individual * 10000); + } } diff --git a/src/app/utils/SlotFunctions.js b/src/app/utils/SlotFunctions.js index fc0c7ee3..c2f5f4ba 100644 --- a/src/app/utils/SlotFunctions.js +++ b/src/app/utils/SlotFunctions.js @@ -1,9 +1,9 @@ import React from 'react'; import cn from 'classnames'; -import { isShieldGenerator } from '../shipyard/ModuleUtils'; import Module from '../shipyard/Module'; import { Infinite } from '../components/SvgIcons'; import Persist from '../stores/Persist'; +import * as ModuleUtils from '../shipyard/ModuleUtils'; /** * Determine if a slot on a ship can mount a module of a particular class and group @@ -155,12 +155,12 @@ export function diffDetails(language, m, mm) { if (mPowerUsage != mmPowerUsage) propDiffs.push(
    {translate('power')}: {diff(formats.round, mPowerUsage, mmPowerUsage)}{units.MJ}
    ); } - let mDps = m.damage * (m.rpshot || 1) * (m.rof || 1) || 0; + let mDps = m.damage * (m.rpshot || 1) * (m.rof || 1); let mmDps = mm ? mm.getDps() || 0 : 0; - if (mDps != mmDps) propDiffs.push(
    {translate('dps')}: {diff(formats.round, mDps, mmDps)}
    ); + if (mDps && mDps != mmDps) propDiffs.push(
    {translate('dps')}: {diff(formats.round, mDps, mmDps)}
    ); - let mAffectsShield = isShieldGenerator(m.grp) || m.grp == 'sb'; - let mmAffectsShield = isShieldGenerator(mm ? mm.grp : null) || mm && mm.grp == 'sb'; + let mAffectsShield = ModuleUtils.isShieldGenerator(m.grp) || m.grp == 'sb'; + let mmAffectsShield = mm ? ModuleUtils.isShieldGenerator(m.grp) || mm.grp == 'sb' : false; if (mAffectsShield || mmAffectsShield) { let shield = this.calcShieldStrengthWith(); // Get shield strength regardless of slot active / inactive let newShield = 0; @@ -187,12 +187,12 @@ export function diffDetails(language, m, mm) { if (mProtection != mmProtection) { propDiffs.push(
    {translate('protection')}: {diff(formats.pct, mProtection, mmProtection)}
    ); } + } - let mIntegrity = m.integrity; - let mmIntegrity = mm ? mm.getIntegrity() || 0 : 0; - if (mIntegrity != mmIntegrity) { - propDiffs.push(
    {translate('integrity')}: {diff(formats.round, mIntegrity, mmIntegrity)}
    ); - } + if (m.grp === 'hr') { + let mHullReinforcement = m.hullreinforcement; + let mmHullReinforcement = mm ? mm.getHullReinforcement() || 0 : 0; + if (mHullReinforcement && mHullReinforcement != mmHullReinforcement) propDiffs.push(
    {translate('hullreinforcement')}: {diff(formats.round, mHullReinforcement, mmHullReinforcement)}
    ); } if (m.grp == 'pd') { @@ -243,5 +243,11 @@ export function diffDetails(language, m, mm) { } } - return propDiffs ?
    {propDiffs}
    : null; + let mIntegrity = m.integrity || 0; + let mmIntegrity = mm ? mm.getIntegrity() || 0 : 0; + if (mIntegrity != mmIntegrity) { + propDiffs.push(
    {translate('integrity')}: {diff(formats.round, mIntegrity, mmIntegrity)}
    ); + } + + return propDiffs.length > 0 ?
    {propDiffs}
    : null; } diff --git a/src/less/app.less b/src/less/app.less index ccff47d7..69c72d71 100755 --- a/src/less/app.less +++ b/src/less/app.less @@ -16,9 +16,14 @@ @import 'tooltip'; @import 'buttons'; @import 'error'; -@import 'shipselector'; @import 'sortable'; @import 'loader'; +@import 'pips'; +@import 'boost'; +@import 'movement'; +@import 'shippicker'; +@import 'defence'; +@import 'offence'; html, body { height: 100%; @@ -42,7 +47,6 @@ div, a, li { #coriolis { width: 100%; height: 100%; - padding-top: 48px; overflow-y: scroll; -webkit-overflow-scrolling: touch; box-sizing: border-box; diff --git a/src/less/boost.less b/src/less/boost.less new file mode 100755 index 00000000..75db11be --- /dev/null +++ b/src/less/boost.less @@ -0,0 +1,14 @@ +#boost { + button { + font-size: 1.2em; + background: @primary-bg; + color: @primary; + border: 1px solid @primary; + &.selected { + // Shown when button is selected + background: @primary; + color: @primary-bg; + } + } +} + diff --git a/src/less/charts.less b/src/less/charts.less index 556e15d8..22006184 100755 --- a/src/less/charts.less +++ b/src/less/charts.less @@ -44,6 +44,9 @@ svg { .label, .text-tip { text-transform: capitalize; + } + + .x { fill: @fg; } diff --git a/src/less/defence.less b/src/less/defence.less new file mode 100755 index 00000000..2c5c730e --- /dev/null +++ b/src/less/defence.less @@ -0,0 +1,14 @@ +#defence { + table { + background-color: @bgBlack; + color: @primary; + margin: 0 auto; + } + + .icon { + stroke: @primary; + stroke-width: 20; + fill: transparent; + } +} + diff --git a/src/less/header.less b/src/less/header.less index 07a24a5c..e2fadac1 100755 --- a/src/less/header.less +++ b/src/less/header.less @@ -20,10 +20,7 @@ header { line-height: 3em; font-family: @fTitle; vertical-align: middle; - position: absolute; - top: 0px; - left: 0px; - width: 100%; + position: relative; z-index: 2; box-sizing: border-box; .user-select-none(); diff --git a/src/less/movement.less b/src/less/movement.less new file mode 100644 index 00000000..796468f5 --- /dev/null +++ b/src/less/movement.less @@ -0,0 +1,14 @@ + +#movement { + svg { + width: 75%; + height: 75%; + stroke: @primary-disabled; + fill: @primary-disabled; + + text { + stroke: @primary; + font-size: 2em; + } + } +} diff --git a/src/less/offence.less b/src/less/offence.less new file mode 100755 index 00000000..39679f27 --- /dev/null +++ b/src/less/offence.less @@ -0,0 +1,14 @@ +#offence { + table { + background-color: @bgBlack; + color: @fg; + margin: 0 auto; + } + + .icon { + stroke: @fg; + stroke-width: 20; + fill: transparent; + } +} + diff --git a/src/less/outfit.less b/src/less/outfit.less index 8d5e7f0e..092fd499 100755 --- a/src/less/outfit.less +++ b/src/less/outfit.less @@ -190,9 +190,47 @@ }); } + &.quarter { + width: 25%; + + .tablet({ + td { + line-height: 2em; + } + }); + + .smallTablet({ + width: 50% !important; + }); + } + &.third { width: 33%; + .smallTablet({ + width: 50% !important; + }); + } + + &.twothirds { + width: 67%; + + .smallTablet({ + width: 100% !important; + }); + } + + &.threequarters { + width: 75%; + + .smallTablet({ + width: 100% !important; + }); + } + + &.full { + width: 100%; + .smallTablet({ width: 100% !important; }); diff --git a/src/less/pips.less b/src/less/pips.less new file mode 100755 index 00000000..2d3dccbd --- /dev/null +++ b/src/less/pips.less @@ -0,0 +1,33 @@ +// The pips table - keep the background black +#pips { + + table { + background-color: @bgBlack; + color: @primary; + margin: 0 auto; + } + + // A clickable entity in the pips table + .clickable { + cursor: pointer; + } + + // A full pip + .full { + stroke: @primary; + fill: @primary; + } + + // A half pip + .half { + stroke: @primary-disabled; + fill: @primary-disabled; + } + + // An empty pip + .empty { + stroke: @primary-bg; + fill: @primary-bg; + } +} + diff --git a/src/less/select.less b/src/less/select.less index 3fd98506..96f27a55 100755 --- a/src/less/select.less +++ b/src/less/select.less @@ -29,7 +29,7 @@ select { padding: 0.5em 0; width: 100%; margin: 0; - max-height: 400px; + max-height: 500px; overflow-y: auto; overflow-x: hidden; z-index: 0; diff --git a/src/less/shipselector.less b/src/less/shippicker.less similarity index 74% rename from src/less/shipselector.less rename to src/less/shippicker.less index 5764bdc5..bd5c4c17 100755 --- a/src/less/shipselector.less +++ b/src/less/shippicker.less @@ -1,18 +1,16 @@ -.shipselector { +.shippicker { background-color: @bgBlack; margin: 0; - padding: 0 0 0 1em; height: 3em; - line-height: 3em; font-family: @fTitle; vertical-align: middle; position: relative; + display: block; .user-select-none(); .menu { position: relative; - z-index: 1; cursor: default; &.r { @@ -28,10 +26,29 @@ } .menu-header { + height: 100%; + z-index: 2; padding : 0 1em; cursor: pointer; color: @warning; text-transform: uppercase; + + &.disabled { + color: @warning-disabled; + cursor: default; + } + + &.selected { + background-color: @bgBlack; + } + + .menu-item-label { + margin-left: 1em; + + .smallTablet({ + display: none; + }); + } } .menu-list { @@ -44,7 +61,7 @@ background-color: @bgBlack; font-size: 0.9em; overflow-y: auto; - z-index: 0; + z-index: 1; -webkit-overflow-scrolling: touch; max-height: 500px; @@ -75,7 +92,6 @@ border-bottom: 1px solid @bg; }); - .tablet({ li, a { padding: 0.3em 0; @@ -83,37 +99,6 @@ }); } - .dbl { - -webkit-column-count: 2; /* Chrome, Safari, Opera */ - -moz-column-count: 2; /* Firefox */ - column-count: 2; - ul { - min-width: 10em; - } - - .smallTablet({ - -webkit-column-count: 3; /* Chrome, Safari, Opera */ - -moz-column-count: 3; /* Firefox */ - column-count: 3; - - ul { - min-width: 20em; - } - }); - - .largePhone({ - -webkit-column-count: 2; /* Chrome, Safari, Opera */ - -moz-column-count: 2; /* Firefox */ - column-count: 2; - }); - - .smallPhone({ - -webkit-column-count: 1; /* Chrome, Safari, Opera */ - -moz-column-count: 1; /* Firefox */ - column-count: 1; - }); - } - .quad { -webkit-column-count: 4; /* Chrome, Safari, Opera */ -moz-column-count: 4; /* Firefox */ @@ -151,6 +136,7 @@ margin: 0 0 0.5em; padding: 0; line-height: 1.3em; + color: @fg; } li { @@ -158,20 +144,10 @@ list-style: none; margin-left: 1em; line-height: 1.1em; - } - - a { - vertical-align: middle; color: @warning; - text-decoration: none; + cursor: pointer; - &:visited { - color: @warning; - } - .no-touch &:hover { - color: teal; - } - &.active { + &.selected { color: @primary; } } @@ -182,6 +158,7 @@ } .no-wrap { + overflow-x: auto; white-space: nowrap; } diff --git a/src/schemas/ship-loadout/4.json b/src/schemas/ship-loadout/4.json index 474d40d5..7dcc1987 100644 --- a/src/schemas/ship-loadout/4.json +++ b/src/schemas/ship-loadout/4.json @@ -230,16 +230,16 @@ }, "armour": { "description": "Sum of base armour + any hull reinforcements", - "type": "integer", + "type": "number", "minimum": 1 }, "armourAdded":{ "description": "Armour added through Hull reinforcement", - "type": "integer", + "type": "number", "minimum": 0 }, "baseShieldStrength": { - "type": "integer", + "type": "number", "minimum": 1 }, "baseArmour": {