Add variable discount support

This commit is contained in:
Colin McLeod
2016-02-24 12:11:46 -08:00
parent 33fd30a377
commit 36bffe1758
5 changed files with 195 additions and 59 deletions

View File

@@ -303,9 +303,9 @@ export default class CostSection extends TranslatedComponent {
<thead>
<tr className='main'>
<th colSpan='2' className='sortable le' onClick={this._sortCostBy.bind(this,'m')}>
{translate('component')}
{shipDiscount < 1 && <u className='cap optional-hide' style={{ marginLeft: '0.5em' }}>{`[${translate('ship')} -${formats.pct1(1 - shipDiscount)}]`}</u>}
{moduleDiscount < 1 && <u className='cap optional-hide' style={{ marginLeft: '0.5em' }}>{`[${translate('modules')} -${formats.pct1(1 - moduleDiscount)}]`}</u>}
{translate('module')}
{shipDiscount && <u className='cap optional-hide' style={{ marginLeft: '0.5em' }}>{`[${translate('ship')} -${formats.pct(shipDiscount)}]`}</u>}
{moduleDiscount && <u className='cap optional-hide' style={{ marginLeft: '0.5em' }}>{`[${translate('modules')} -${formats.pct(moduleDiscount)}]`}</u>}
</th>
<th className='sortable le' onClick={this._sortCostBy.bind(this, 'cr')} >{translate('credits')}</th>
</tr>
@@ -363,7 +363,7 @@ export default class CostSection extends TranslatedComponent {
<th colSpan='2' className='sortable le' onClick={this._sortRetrofitBy.bind(this, 'buyName')}>{translate('buy')}</th>
<th colSpan='2' className='sortable le' onClick={this._sortRetrofitBy.bind(this, 'cr')}>
{translate('net cost')}
{moduleDiscount < 1 && <u className='cap optional-hide' style={{ marginLeft: '0.5em' }}>{`[${translate('modules')} -${formats.pct1(1 - moduleDiscount)}]`}</u>}
{moduleDiscount < 1 && <u className='cap optional-hide' style={{ marginLeft: '0.5em' }}>{`[${translate('modules')} -${formats.pct(moduleDiscount)}]`}</u>}
</th>
</tr>
</thead>
@@ -472,7 +472,7 @@ export default class CostSection extends TranslatedComponent {
<th colSpan='2' className='sortable le' onClick={this._sortAmmoBy.bind(this, 'm')} >{translate('module')}</th>
<th colSpan='1' className='sortable le' onClick={this._sortAmmoBy.bind(this, 'max')} >{translate('qty')}</th>
<th colSpan='1' className='sortable le' onClick={this._sortAmmoBy.bind(this, 'cost')} >{translate('unit cost')}</th>
<th className='sortable le' onClick={this._sortAmmoBy.bind(this, 'total')}>{translate('total cost')}</th>
<th className='sortable le' onClick={this._sortAmmoBy.bind(this, 'total')}>{translate('subtotal')}</th>
</tr>
</thead>
<tbody>

View File

@@ -1,7 +1,7 @@
import React from 'react';
import TranslatedComponent from './TranslatedComponent';
import { Languages } from '../i18n/Language';
import { Insurance, Discounts } from '../shipyard/Constants';
import { Insurance } from '../shipyard/Constants';
import Link from './Link';
import ActiveLink from './ActiveLink';
import cn from 'classnames';
@@ -18,6 +18,36 @@ import { outfitURL } from '../utils/UrlGenerators';
const SIZE_MIN = 0.65;
const SIZE_RANGE = 0.55;
/**
* Normalize percentages to 'clean' values
* @param {Number} val Percentage value
* @return {Number} Normalized value
*/
function normalizePercent(val) {
if (val === '' || isNaN(val)) {
return 0;
}
val = Math.round(val * 100) / 100;
return val >= 100 ? 100 : val;
}
/**
* Rounds the value to the nearest quarter (0, 0.25, 0.5, 0.75)
* @param {Number} val Value
* @return {Number} Rounded value
*/
function nearestQtrPct(val) {
return Math.round(val * 4) / 4;
}
/**
* Select all text in a field
* @param {SyntheticEvent} e Event
*/
function selectAll(e) {
e.target.select();
}
/**
* Coriolis App Header section / menus
*/
@@ -34,15 +64,22 @@ export default class Header extends TranslatedComponent {
this._setLanguage = this._setLanguage.bind(this);
this._setInsurance = this._setInsurance.bind(this);
this._setShipDiscount = this._setShipDiscount.bind(this);
this._changeShipDiscount = this._changeShipDiscount.bind(this);
this._kpShipDiscount = this._kpShipDiscount.bind(this);
this._setModuleDiscount = this._setModuleDiscount.bind(this);
this._changeModuleDiscount = this._changeModuleDiscount.bind(this);
this._kpModuleDiscount = this._kpModuleDiscount.bind(this);
this._openShips = this._openMenu.bind(this, 's');
this._openBuilds = this._openMenu.bind(this, 'b');
this._openComp = this._openMenu.bind(this, 'comp');
this._openSettings = this._openMenu.bind(this, 'settings');
this.languageOptions = [];
this.insuranceOptions = [];
this.discountOptions = [];
this.state = {
shipDiscount: normalizePercent(Persist.getShipDiscount() * 100),
moduleDiscount: normalizePercent(Persist.getModuleDiscount() * 100),
};
let translate = context.language.translate;
@@ -53,10 +90,6 @@ export default class Header extends TranslatedComponent {
for (let name in Insurance) {
this.insuranceOptions.push(<option key={name} value={name}>{translate(name)}</option>);
}
for (let name in Discounts) {
this.discountOptions.push(<option key={name} value={Discounts[name]}>{name}</option>);
}
}
/**
@@ -69,18 +102,90 @@ export default class Header extends TranslatedComponent {
/**
* Update the Module discount
* @param {SyntheticEvent} e Event
*/
_setModuleDiscount(e) {
Persist.setModuleDiscount(e.target.value * 1);
_setModuleDiscount() {
let moduleDiscount = normalizePercent(this.state.moduleDiscount);
this.setState({ moduleDiscount });
Persist.setModuleDiscount(moduleDiscount / 100); // Decimal value is stored
}
/**
* Update the Ship discount
*/
_setShipDiscount() {
let shipDiscount = normalizePercent(this.state.shipDiscount);
this.setState({ shipDiscount });
Persist.setShipDiscount(shipDiscount / 100); // Decimal value is stored
}
/**
* Input handler for the module discount field
* @param {SyntheticEvent} e Event
*/
_changeModuleDiscount(e) {
let moduleDiscount = e.target.value;
if (e.target.value === '' || e.target.value === '-' || e.target.value === '.') {
this.setState({ moduleDiscount });
} else if (!isNaN(moduleDiscount) && Math.round(moduleDiscount) < 100) {
this.setState({ moduleDiscount });
}
}
/**
* Input handler for the ship discount field
* @param {SyntheticEvent} e Event
*/
_setShipDiscount(e) {
Persist.setShipDiscount(e.target.value * 1);
_changeShipDiscount(e) {
let shipDiscount = e.target.value;
if (e.target.value === '' || e.target.value === '-' || e.target.value === '.') {
this.setState({ shipDiscount });
} else if (!isNaN(shipDiscount) && Math.round(shipDiscount) < 100) {
this.setState({ shipDiscount });
}
}
/**
* Key down/press handler for ship discount field
* @param {SyntheticEvent} e Event
*/
_kpShipDiscount(e) {
let sd = this.state.shipDiscount * 1;
switch (e.keyCode) {
case 38:
e.preventDefault();
this.setState({ shipDiscount: e.shiftKey ? nearestQtrPct(sd + 0.25) : normalizePercent(sd + 1) });
break;
case 40:
e.preventDefault();
this.setState({ shipDiscount: e.shiftKey ? nearestQtrPct(sd - 0.25) : normalizePercent(sd - 1) });
break;
case 13:
e.preventDefault();
e.target.blur();
}
}
/**
* Key down/press handler for module discount field
* @param {SyntheticEvent} e Event
*/
_kpModuleDiscount(e) {
let md = this.state.moduleDiscount * 1;
switch (e.keyCode) {
case 38:
e.preventDefault();
this.setState({ moduleDiscount: e.shiftKey ? nearestQtrPct(md + 0.25) : normalizePercent(md + 1) });
break;
case 40:
e.preventDefault();
this.setState({ moduleDiscount: e.shiftKey ? nearestQtrPct(md - 0.25) : normalizePercent(md - 1) });
break;
case 13:
e.preventDefault();
e.target.blur();
}
}
/**
@@ -274,14 +379,12 @@ export default class Header extends TranslatedComponent {
</select>
<br/>
{translate('ship')} {translate('discount')}
<select className='cap' value={Persist.getShipDiscount()} onChange={this._setShipDiscount}>
{this.discountOptions}
</select>
<input type='text' size='10' value={this.state.shipDiscount} onChange={this._changeShipDiscount} onFocus={selectAll} onBlur={this._setShipDiscount} onKeyDown={this._kpShipDiscount}/>
<u className='primary-disabled'>%</u>
<br/>
{translate('module')} {translate('discount')}
<select className='cap' value={Persist.getModuleDiscount()} onChange={this._setModuleDiscount} >
{this.discountOptions}
</select>
<input type='text' size='10' value={this.state.moduleDiscount} onChange={this._changeModuleDiscount} onFocus={selectAll} onBlur={this._setModuleDiscount} onKeyDown={this._kpModuleDiscount}/>
<u className='primary-disabled'>%</u>
</div>
<hr />
<ul>
@@ -317,7 +420,7 @@ export default class Header extends TranslatedComponent {
let update = () => this.forceUpdate();
Persist.addListener('language', update);
Persist.addListener('insurance', update);
Persist.addListener('discounts', update);
// Persist.addListener('discounts', update);
Persist.addListener('deletedAll', update);
Persist.addListener('builds', update);
Persist.addListener('tooltips', update);
@@ -336,6 +439,19 @@ export default class Header extends TranslatedComponent {
this.insuranceOptions.push(<option key={name} value={name}>{translate(name)}</option>);
}
}
if (nextProps.currentMenu == 'settings') { // Settings menu is about to be opened
this.setState({
shipDiscount: normalizePercent(Persist.getShipDiscount() * 100),
moduleDiscount: normalizePercent(Persist.getModuleDiscount() * 100),
});
} else if (this.props.currentMenu == 'settings') { // Settings menu is about to be closed
if (this.state.shipDiscount != (Persist.getShipDiscount() * 100)) {
this._setShipDiscount();
}
if (this.state.moduleDiscount != (Persist.getModuleDiscount() * 100)) {
this._setModuleDiscount();
}
}
}
/**

View File

@@ -96,7 +96,6 @@ export default class Ship {
this.standard[3], // Add Life Support
this.hardpoints
);
this.shipCostMultiplier = 1;
this.moduleCostMultiplier = 1;
this.priorityBands = [
{ deployed: 0, retracted: 0, },
@@ -345,11 +344,13 @@ export default class Ship {
/**
* Recalculate all item costs and total based on discounts.
* @param {Number} shipCostMultiplier Ship cost multiplier discount (e.g. 0.9 === 10% discount)
* @param {Number} moduleCostMultiplier Module cost multiplier discount (e.g. 0.75 === 25% discount)
* @param {Number} shipDiscount Ship cost discount (e.g. 0.1 === 10% discount)
* @param {Number} moduleDiscount Module cost discount (e.g. 0.75 === 25% discount)
* @return {this} The current ship instance for chaining
*/
applyDiscounts(shipCostMultiplier, moduleCostMultiplier) {
applyDiscounts(shipDiscount, moduleDiscount) {
let shipCostMultiplier = 1 - shipDiscount;
let moduleCostMultiplier = 1 - moduleDiscount;
let total = 0;
let costList = this.costList;
@@ -362,7 +363,6 @@ export default class Ship {
}
}
}
this.shipCostMultiplier = shipCostMultiplier;
this.moduleCostMultiplier = moduleCostMultiplier;
this.totalCost = total;
return this;

View File

@@ -1,26 +1,19 @@
import { EventEmitter } from 'fbemitter';
import { Insurance } from '../shipyard/Constants';
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_INSURANCE = 'insurance';
const LS_KEY_DISCOUNTS = 'discounts';
const LS_KEY_SHIP_DISCOUNT = 'shipDiscount';
const LS_KEY_MOD_DISCOUNT = 'moduleDiscount';
const LS_KEY_STATE = 'state';
const LS_KEY_SIZE_RATIO = 'sizeRatio';
const LS_KEY_TOOLTIPS = 'tooltips';
let LS;
// Safe check to determine if localStorage is enabled
try {
localStorage.setItem('test', 'test');
localStorage.removeItem('test');
LS = localStorage;
} catch(e) {
LS = null;
}
/**
* Safe localstorage put
* @param {String} key key
@@ -48,7 +41,11 @@ function _getString(key) {
*/
function _get(key) {
let str = _getString(key);
return str ? JSON.parse(str) : null;
try {
return str ? JSON.parse(str) : null;
} catch (e) {
return null;
}
}
/**
@@ -74,18 +71,30 @@ export class Persist extends EventEmitter {
*/
constructor() {
super();
// Eventually check use session over localstorage - 'remember me' logic
// Safe check to determine if localStorage is enabled
try {
localStorage.setItem('test', 'test');
localStorage.removeItem('test');
LS = localStorage;
} catch(e) {
LS = null;
}
let tips = _get(LS_KEY_TOOLTIPS);
let insurance = _getString(LS_KEY_INSURANCE);
let shipDiscount = _get(LS_KEY_SHIP_DISCOUNT);
let moduleDiscount = _get(LS_KEY_MOD_DISCOUNT);
let buildJson = _get(LS_KEY_BUILDS);
let comparisonJson = _get(LS_KEY_COMPARISONS);
let tips = _get(LS_KEY_TOOLTIPS);
let discounts = _get(LS_KEY_DISCOUNTS);
this.onStorageChange = this.onStorageChange.bind(this);
this.langCode = _getString(LS_KEY_LANG) || 'en';
this.insurance = _getString(LS_KEY_INSURANCE) || 'standard';
this.discounts = discounts && !isNaN(discounts[0]) && !isNaN(discounts[1]) ? discounts : [1, 1];
this.builds = buildJson ? buildJson : {};
this.comparisons = comparisonJson ? comparisonJson : {};
this.buildCount = Object.keys(this.builds).length;
this.insurance = insurance && Insurance[insurance.toLowerCase()] !== undefined ? insurance : 'standard';
this.shipDiscount = !isNaN(shipDiscount) && shipDiscount < 1 ? shipDiscount * 1 : 0;
this.moduleDiscount = !isNaN(moduleDiscount) && moduleDiscount < 1 ? moduleDiscount * 1 : 0;
this.builds = buildJson && typeof buildJson == 'object' ? buildJson : {};
this.comparisons = comparisonJson && typeof comparisonJson == 'object' ? comparisonJson : {};
this.costTab = _getString(LS_KEY_COST_TAB);
this.state = _get(LS_KEY_STATE);
this.sizeRatio = _get(LS_KEY_SIZE_RATIO) || 1;
@@ -122,9 +131,13 @@ export class Persist extends EventEmitter {
this.insurance = newValue;
this.emit('insurance', newValue);
break;
case LS_KEY_DISCOUNTS:
this.discounts = JSON.parse(newValue);
this.emit('discounts', this.discounts);
case LS_KEY_SHIP_DISCOUNT:
this.shipDiscount = JSON.parse(newValue);
this.emit('discounts');
break;
case LS_KEY_MOD_DISCOUNT:
this.moduleDiscount = JSON.parse(newValue);
this.emit('discounts');
break;
case LS_KEY_TOOLTIPS:
this.tooltipsEnabled = !!newValue && newValue.toLowerCase() == 'true';
@@ -361,7 +374,8 @@ export class Persist extends EventEmitter {
data[LS_KEY_BUILDS] = this.getBuilds();
data[LS_KEY_COMPARISONS] = this.getComparisons();
data[LS_KEY_INSURANCE] = this.getInsurance();
data[LS_KEY_DISCOUNTS] = this.discounts;
data[LS_KEY_SHIP_DISCOUNT] = this.shipDiscount;
data[LS_KEY_MOD_DISCOUNT] = this.moduleDiscount;
return data;
}
@@ -370,7 +384,7 @@ export class Persist extends EventEmitter {
* @return {String} The name of the saved insurance type of null
*/
getInsurance() {
return this.insurance;
return this.insurance.toLowerCase();
}
/**
@@ -388,8 +402,8 @@ export class Persist extends EventEmitter {
* @param {number} shipDiscount Discount value/amount
*/
setShipDiscount(shipDiscount) {
this.discounts[0] = shipDiscount;
_put(LS_KEY_DISCOUNTS, this.discounts);
this.shipDiscount = shipDiscount;
_put(LS_KEY_SHIP_DISCOUNT, this.shipDiscount);
this.emit('discounts');
}
@@ -398,7 +412,7 @@ export class Persist extends EventEmitter {
* @return {number} val Discount value/amount
*/
getShipDiscount() {
return this.discounts[0];
return this.shipDiscount;
};
/**
@@ -406,8 +420,8 @@ export class Persist extends EventEmitter {
* @param {number} moduleDiscount Discount value/amount
*/
setModuleDiscount(moduleDiscount) {
this.discounts[1] = moduleDiscount;
_put(LS_KEY_DISCOUNTS, this.discounts);
this.moduleDiscount = moduleDiscount;
_put(LS_KEY_MOD_DISCOUNT, this.moduleDiscount);
this.emit('discounts');
}
@@ -416,7 +430,7 @@ export class Persist extends EventEmitter {
* @return {number} val Discount value/amount
*/
getModuleDiscount() {
return this.discounts[1];
return this.moduleDiscount;
}
/**

View File

@@ -95,6 +95,12 @@ header {
background-color: @warning-disabled;
}
input {
border: none;
text-align: right;
font-size: 1em;
font-family: @fStandard;
}
.smallTablet({
max-height: 400px;