diff --git a/css/companymanagement.scss b/css/companymanagement.scss index 7a8944a2b..e93dd331b 100644 --- a/css/companymanagement.scss +++ b/css/companymanagement.scss @@ -10,7 +10,8 @@ #cmpy-mgmt-container p, #cmpy-mgmt-container a, -#cmpy-mgmt-container div { +#cmpy-mgmt-container div, +#cmpy-mgmt-container br { font-size: $defaultFontSize * 0.8125; } diff --git a/src/Constants.ts b/src/Constants.ts index 33022df26..1d48c94a2 100644 --- a/src/Constants.ts +++ b/src/Constants.ts @@ -297,9 +297,11 @@ export let CONSTANTS: IMap = { ** Significantly changed the effects of the different employee positions. See updated descriptions ** Reduced the amount of money you gain from private investors ** Training employees is now 3x more effective + ** Bug Fix: An industry's products are now properly separated between different cities * Rebalanced BitNode-3 to make it slightly harder * Bug Fix: Bladeburner's Hyperbolic Regeneration Chamber should no longer instantly refill all stamina + * Bug Fix: The cost of purchasing Augmentations for Duplicate Sleeves no longer scales with how many Augs you've purchased for yourself ` } diff --git a/src/Corporation/Corporation.jsx b/src/Corporation/Corporation.jsx index d3e66643d..a8879b351 100644 --- a/src/Corporation/Corporation.jsx +++ b/src/Corporation/Corporation.jsx @@ -19,6 +19,7 @@ import { CONSTANTS } from "../Constants"; import { Factions } from "../Faction/Factions"; import { showLiterature } from "../Literature"; import { Locations } from "../Locations"; +import { createCityMap } from "../Locations/Cities"; import { Player } from "../Player"; import { numeralWrapper } from "../ui/numeralFormat"; @@ -590,7 +591,7 @@ Industry.prototype.processMaterials = function(marketCycles=1, company) { //At the start of the export state, set the imports of everything to 0 if (this.state === "EXPORT") { - for (var i = 0; i < Cities.length; ++i) { + for (let i = 0; i < Cities.length; ++i) { var city = Cities[i], office = this.offices[city]; if (!(this.warehouses[city] instanceof Warehouse)) { continue; @@ -605,7 +606,7 @@ Industry.prototype.processMaterials = function(marketCycles=1, company) { } } - for (var i = 0; i < Cities.length; ++i) { + for (let i = 0; i < Cities.length; ++i) { var city = Cities[i], office = this.offices[city]; if (this.warehouses[city] instanceof Warehouse) { @@ -665,19 +666,17 @@ Industry.prototype.processMaterials = function(marketCycles=1, company) { prod = maxProd; } prod *= (SecsPerMarketCycle * marketCycles); //Convert production from per second to per market cycle - //Calculate net change in warehouse storage making - //the produced materials will cost + + // Calculate net change in warehouse storage making the produced materials will cost var totalMatSize = 0; - for (var tmp = 0; tmp < this.prodMats.length; ++tmp) { + for (let tmp = 0; tmp < this.prodMats.length; ++tmp) { totalMatSize += (MaterialSizes[this.prodMats[tmp]]); } - for (var reqMatName in this.reqMats) { - if (this.reqMats.hasOwnProperty(reqMatName)) { - var normQty = this.reqMats[reqMatName]; - totalMatSize -= (MaterialSizes[reqMatName] * normQty); - } + for (const reqMatName in this.reqMats) { + var normQty = this.reqMats[reqMatName]; + totalMatSize -= (MaterialSizes[reqMatName] * normQty); } - //If not enough space in warehouse, limit the amount of produced materials + // If not enough space in warehouse, limit the amount of produced materials if (totalMatSize > 0) { var maxAmt = Math.floor((warehouse.size - warehouse.sizeUsed) / totalMatSize); prod = Math.min(maxAmt, prod); @@ -685,10 +684,10 @@ Industry.prototype.processMaterials = function(marketCycles=1, company) { if (prod < 0) {prod = 0;} - //Keep track of production for smart supply (/s) + // Keep track of production for smart supply (/s) warehouse.smartSupplyStore += (prod / (SecsPerMarketCycle * marketCycles)); - //Make sure we have enough resource to make our materials + // Make sure we have enough resource to make our materials var producableFrac = 1; for (var reqMatName in this.reqMats) { if (this.reqMats.hasOwnProperty(reqMatName)) { @@ -700,17 +699,15 @@ Industry.prototype.processMaterials = function(marketCycles=1, company) { } if (producableFrac <= 0) {producableFrac = 0; prod = 0;} - //Make our materials if they are producable + // Make our materials if they are producable if (producableFrac > 0 && prod > 0) { - for (var reqMatName in this.reqMats) { - if (this.reqMats.hasOwnProperty(reqMatName)) { - var reqMatQtyNeeded = (this.reqMats[reqMatName] * prod * producableFrac); - warehouse.materials[reqMatName].qty -= reqMatQtyNeeded; - warehouse.materials[reqMatName].prd = 0; - warehouse.materials[reqMatName].prd -= reqMatQtyNeeded / (SecsPerMarketCycle * marketCycles); - } + for (const reqMatName in this.reqMats) { + var reqMatQtyNeeded = (this.reqMats[reqMatName] * prod * producableFrac); + warehouse.materials[reqMatName].qty -= reqMatQtyNeeded; + warehouse.materials[reqMatName].prd = 0; + warehouse.materials[reqMatName].prd -= reqMatQtyNeeded / (SecsPerMarketCycle * marketCycles); } - for (var j = 0; j < this.prodMats.length; ++j) { + for (let j = 0; j < this.prodMats.length; ++j) { warehouse.materials[this.prodMats[j]].qty += (prod * producableFrac); warehouse.materials[this.prodMats[j]].qlt = (office.employeeProd[EmployeePositions.Engineer] / 90 + @@ -718,7 +715,7 @@ Industry.prototype.processMaterials = function(marketCycles=1, company) { Math.pow(warehouse.materials["AICores"].qty, this.aiFac) / 10e3); } } else { - for (var reqMatName in this.reqMats) { + for (const reqMatName in this.reqMats) { if (this.reqMats.hasOwnProperty(reqMatName)) { warehouse.materials[reqMatName].prd = 0; } @@ -726,18 +723,16 @@ Industry.prototype.processMaterials = function(marketCycles=1, company) { } //Per second - var fooProd = prod * producableFrac / (SecsPerMarketCycle * marketCycles); - for (var fooI = 0; fooI < this.prodMats.length; ++fooI) { + const fooProd = prod * producableFrac / (SecsPerMarketCycle * marketCycles); + for (let fooI = 0; fooI < this.prodMats.length; ++fooI) { warehouse.materials[this.prodMats[fooI]].prd = fooProd; } } else { //If this doesn't produce any materials, then it only creates //Products. Creating products will consume materials. The //Production of all consumed materials must be set to 0 - for (var reqMatName in this.reqMats) { - if (this.reqMats.hasOwnProperty(reqMatName)) { - warehouse.materials[reqMatName].prd = 0; - } + for (const reqMatName in this.reqMats) { + warehouse.materials[reqMatName].prd = 0; } } break; @@ -751,12 +746,39 @@ Industry.prototype.processMaterials = function(marketCycles=1, company) { mat.sll = 0; continue; } - var mat = warehouse.materials[matName]; - // Calculate sale cost + // Sale multipliers + const businessFactor = this.getBusinessFactor(office); //Business employee productivity + const advertisingFactor = this.getAdvertisingFactors()[0]; //Awareness + popularity + const marketFactor = this.getMarketFactor(mat); //Competition + demand + + // Determine the cost that the material will be sold at const markupLimit = mat.getMarkupLimit(); var sCost; - if (mat.marketTa1) { + if (mat.marketTa2) { + const prod = mat.prd; + + // Reverse engineer the 'maxSell' formula + // 1. Set 'maxSell' = prod + // 2. Substitute formula for 'markup' + // 3. Solve for 'sCost' + const numerator = markupLimit; + const sqrtNumerator = prod; + const sqrtDenominator = ((mat.qlt + .001) + * marketFactor + * businessFactor + * company.getSalesMultiplier() + * advertisingFactor + * this.getSalesMultiplier()); + const denominator = Math.sqrt(sqrtNumerator / sqrtDenominator); + const optimalPrice = (numerator / denominator) + mat.bCost; + + // We'll store this "Optimal Price" in a property so that we don't have + // to re-calculate it for the UI + mat.marketTa2Price = optimalPrice; + + sCost = optimalPrice; + } else if (mat.marketTa1) { sCost = mat.bCost + markupLimit; } else if (isString(mat.sCost)) { sCost = mat.sCost.replace(/MP/g, mat.bCost); @@ -780,9 +802,7 @@ Industry.prototype.processMaterials = function(marketCycles=1, company) { markup = mat.bCost / sCost; } } - var businessFactor = this.getBusinessFactor(office); //Business employee productivity - var advertisingFactor = this.getAdvertisingFactors()[0]; //Awareness + popularity - var marketFactor = this.getMarketFactor(mat); //Competition + demand + var maxSell = (mat.qlt + .001) * marketFactor * markup @@ -905,8 +925,8 @@ Industry.prototype.processMaterials = function(marketCycles=1, company) { //Produce Scientific Research based on R&D employees //Scientific Research can be produced without a warehouse if (office instanceof OfficeSpace) { - this.sciResearch.qty += (.005 - * Math.pow(office.employeeProd[EmployeePositions.RandD], 0.55) + this.sciResearch.qty += (.004 + * Math.pow(office.employeeProd[EmployeePositions.RandD], 0.5) * company.getScientificResearchMultiplier() * this.getScientificResearchMultiplier()); } @@ -962,9 +982,9 @@ Industry.prototype.processProducts = function(marketCycles=1, corporation) { //Processes FINISHED products Industry.prototype.processProduct = function(marketCycles=1, product, corporation) { - var totalProfit = 0; - for (var i = 0; i < Cities.length; ++i) { - var city = Cities[i], office = this.offices[city], warehouse = this.warehouses[city]; + let totalProfit = 0; + for (let i = 0; i < Cities.length; ++i) { + let city = Cities[i], office = this.offices[city], warehouse = this.warehouses[city]; if (warehouse instanceof Warehouse) { switch(this.state) { @@ -1040,27 +1060,60 @@ Industry.prototype.processProduct = function(marketCycles=1, product, corporatio } } - //Since its a product, its production cost is increased for labor + // Since its a product, its production cost is increased for labor product.pCost *= ProductProductionCostRatio; - //Calculate Sale Cost (sCost), which could be dynamically evaluated + // Sale multipliers + const businessFactor = this.getBusinessFactor(office); //Business employee productivity + const advertisingFactor = this.getAdvertisingFactors()[0]; //Awareness + popularity + const marketFactor = this.getMarketFactor(product); //Competition + demand + + // Calculate Sale Cost (sCost), which could be dynamically evaluated + const markupLimit = product.rat / product.mku; var sCost; - if (isString(product.sCost)) { + if (product.marketTa2) { + const prod = product.data[city][1]; + + // Reverse engineer the 'maxSell' formula + // 1. Set 'maxSell' = prod + // 2. Substitute formula for 'markup' + // 3. Solve for 'sCost'roduct.pCost = sCost + const numerator = markupLimit; + const sqrtNumerator = prod; + const sqrtDenominator = (0.5 + * Math.pow(product.rat, 0.65) + * marketFactor + * corporation.getSalesMultiplier() + * businessFactor + * advertisingFactor + * this.getSalesMultiplier()); + const denominator = Math.sqrt(sqrtNumerator / sqrtDenominator); + let optimalPrice; + if (sqrtDenominator === 0 || denominator === 0) { + optimalPrice = 0; + } else { + optimalPrice = (numerator / denominator) + product.pCost; + } + + // Store this "optimal Price" in a property so we don't have to re-calculate for UI + product.marketTa2Price[city] = optimalPrice; + sCost = optimalPrice; + } else if (product.marketTa1) { + sCost = product.pCost + markupLimit; + } else if (isString(product.sCost)) { sCost = product.sCost.replace(/MP/g, product.pCost + product.rat / product.mku); sCost = eval(sCost); } else { sCost = product.sCost; } - var markup = 1, markupLimit = product.rat / product.mku; + var markup = 1; if (sCost > product.pCost) { if ((sCost - product.pCost) > markupLimit) { markup = markupLimit / (sCost - product.pCost); } } - var businessFactor = this.getBusinessFactor(office); //Business employee productivity - var advertisingFactor = this.getAdvertisingFactors()[0]; //Awareness + popularity - var marketFactor = this.getMarketFactor(product); //Competition + demand + var maxSell = 0.5 * Math.pow(product.rat, 0.65) * marketFactor @@ -1085,8 +1138,9 @@ Industry.prototype.processProduct = function(marketCycles=1, product, corporatio } else if (product.sllman[city][0] && product.sllman[city][1] > 0) { //Sell amount is manually limited sellAmt = Math.min(maxSell, product.sllman[city][1]); + } else if (product.sllman[city][0] === false){ + sellAmt = 0; } else { - //Backwards compatibility, -1 = 0 sellAmt = maxSell; } if (sellAmt < 0) { sellAmt = 0; } @@ -1114,8 +1168,7 @@ Industry.prototype.processProduct = function(marketCycles=1, product, corporatio return totalProfit; } -Industry.prototype.discontinueProduct = function(product, parentRefs) { - var company = parentRefs.company, industry = parentRefs.industry; +Industry.prototype.discontinueProduct = function(product) { for (var productName in this.products) { if (this.products.hasOwnProperty(productName)) { if (product === this.products[productName]) { @@ -1182,7 +1235,7 @@ Industry.prototype.getOfficeProductivity = function(office, params) { // Returns a multiplier based on the office' 'Business' employees that affects sales Industry.prototype.getBusinessFactor = function(office) { const businessProd = 1 + office.employeeProd[EmployeePositions.Business]; - + return calculateEffectWithFactors(businessProd, 0.26, 10e3); } diff --git a/src/Corporation/Material.ts b/src/Corporation/Material.ts index 30c82c139..7ad8f4ae6 100644 --- a/src/Corporation/Material.ts +++ b/src/Corporation/Material.ts @@ -66,6 +66,7 @@ export class Material { // Flags that signal whether automatic sale pricing through Market TA is enabled marketTa1: boolean = false; marketTa2: boolean = false; + marketTa2Price: number = 0; constructor(params: IConstructorParams = {}) { if (params.name) { this.name = params.name; } diff --git a/src/Corporation/MaterialSizes.ts b/src/Corporation/MaterialSizes.ts index 6f744cc12..2ec694f25 100644 --- a/src/Corporation/MaterialSizes.ts +++ b/src/Corporation/MaterialSizes.ts @@ -2,15 +2,17 @@ import { IMap } from "../types"; // Map of material (by name) to their sizes (how much space it takes in warehouse) export const MaterialSizes: IMap = { - Water: 0.05, - Energy: 0.01, - Food: 0.03, - Plants: 0.05, - Metal: 0.1, - Hardware: 0.06, - Chemicals: 0.05, - Drugs: 0.02, - Robots: 0.5, - AICores: 0.1, - RealEstate: 0, + Water: 0.05, + Energy: 0.01, + Food: 0.03, + Plants: 0.05, + Metal: 0.1, + Hardware: 0.06, + Chemicals: 0.05, + Drugs: 0.02, + Robots: 0.5, + AICores: 0.1, + RealEstate: 0, + "Real Estate": 0, + "AI Cores": 0, } diff --git a/src/Corporation/Product.ts b/src/Corporation/Product.ts index 2a7d6afb1..befcc7396 100644 --- a/src/Corporation/Product.ts +++ b/src/Corporation/Product.ts @@ -4,8 +4,10 @@ import { ProductRatingWeights, IProductRatingWeight } from "./ProductRatingWeights"; import { Cities } from "../Locations/Cities"; +import { createCityMap } from "../Locations/createCityMap"; import { IMap } from "../types"; + import { Generic_fromJSON, Generic_toJSON, Reviver } from "../../utils/JSONReviver"; @@ -89,14 +91,7 @@ export class Product { // Data refers to the production, sale, and quantity of the products // These values are specific to a city // For each city, the data is [qty, prod, sell] - data: IMap = { - [Cities.Aevum]: [0, 0, 0], - [Cities.Chongqing]: [0, 0, 0], - [Cities.Sector12]: [0, 0, 0], - [Cities.NewTokyo]: [0, 0, 0], - [Cities.Ishima]: [0, 0, 0], - [Cities.Volhaven]: [0, 0, 0], - } + data: IMap = createCityMap([0, 0, 0]); // Location of this Product // Only applies for location-based products like restaurants/hospitals @@ -113,23 +108,13 @@ export class Product { // Data to keep track of whether production/sale of this Product is // manually limited. These values are specific to a city // [Whether production/sale is limited, limit amount] - prdman: IMap = { - [Cities.Aevum]: [false, 0], - [Cities.Chongqing]: [false, 0], - [Cities.Sector12]: [false, 0], - [Cities.NewTokyo]: [false, 0], - [Cities.Ishima]: [false, 0], - [Cities.Volhaven]: [false, 0], - } + prdman: IMap = createCityMap([false, 0]); + sllman: IMap = createCityMap([false, 0]); - sllman: IMap = { - [Cities.Aevum]: [false, 0], - [Cities.Chongqing]: [false, 0], - [Cities.Sector12]: [false, 0], - [Cities.NewTokyo]: [false, 0], - [Cities.Ishima]: [false, 0], - [Cities.Volhaven]: [false, 0], - } + // Flags that signal whether automatic sale pricing through Market TA is enabled + marketTa1: boolean = false; + marketTa2: boolean = false; + marketTa2Price: IMap = createCityMap(0); constructor(params: IConstructorParams={}) { this.name = params.name ? params.name : ""; diff --git a/src/Corporation/Warehouse.ts b/src/Corporation/Warehouse.ts index ecbaa826a..15bdf03b6 100644 --- a/src/Corporation/Warehouse.ts +++ b/src/Corporation/Warehouse.ts @@ -46,6 +46,10 @@ export class Warehouse { // Whether Smart Supply is enabled for this Industry (the Industry that this Warehouse is for) smartSupplyEnabled: boolean = false; + // Flag that indicates whether Smart Supply accounts for imports when calculating + // the amount fo purchase + smartSupplyConsiderExports: boolean = false; + // Stores the amount of product to be produced. Used for Smart Supply unlock. // The production tracked by smart supply is always based on the previous cycle, // so it will always trail the "true" production by 1 cycle diff --git a/src/Corporation/data/ResearchMetadata.ts b/src/Corporation/data/ResearchMetadata.ts index 57f6f7d5e..73e6ec5d5 100644 --- a/src/Corporation/data/ResearchMetadata.ts +++ b/src/Corporation/data/ResearchMetadata.ts @@ -94,11 +94,13 @@ export const researchMetadata: IConstructorParams[] = [ }, { name: "Market-TA.II", - cost: 40e3, + cost: 50e3, desc: "Develop double-advanced AI software that uses technical analysis to " + "help you understand and exploit the market. This research " + "allows you to know how many sales of a Material/Product you lose or gain " + - "from having too high or too low or a sale price.", + "from having too high or too low or a sale price. It also lets you automatically " + + "set the sale price of your Materials/Products at the optimal price such that " + + "the amount sold matches the amount produced.", }, { name: "Overclock", diff --git a/src/Corporation/ui/CorporationUIEventHandler.js b/src/Corporation/ui/CorporationUIEventHandler.js index 6feec0808..a43d78819 100644 --- a/src/Corporation/ui/CorporationUIEventHandler.js +++ b/src/Corporation/ui/CorporationUIEventHandler.js @@ -17,10 +17,14 @@ import { Industries, IndustryDescriptions, IndustryResearchTrees } from "../IndustryData"; +import { MaterialSizes } from "../MaterialSizes"; + import { Product } from "../Product"; import { Player } from "../../Player"; +import { Cities } from "../../Locations/Cities"; + import { numeralWrapper } from "../../ui/numeralFormat"; import { dialogBoxCreate } from "../../../utils/DialogBox"; @@ -81,7 +85,7 @@ export class CorporationEventHandler { var totalAmount = Number(money) + (stockShares * stockPrice); var repGain = totalAmount / BribeToRepRatio; - repGainText.innerText = "You will gain " + numeralWrapper.formatNumber(repGain, "0,0") + + repGainText.innerText = "You will gain " + numeralWrapper.format(repGain, "0,0") + " reputation with " + factionSelector.options[factionSelector.selectedIndex].value + " with this bribe"; @@ -104,7 +108,7 @@ export class CorporationEventHandler { var totalAmount = money + (stockShares * stockPrice); var repGain = totalAmount / BribeToRepRatio; console.log("repGain: " + repGain); - repGainText.innerText = "You will gain " + numeralWrapper.formatNumber(repGain, "0,0") + + repGainText.innerText = "You will gain " + numeralWrapper.format(repGain, "0,0") + " reputation with " + factionSelector.options[factionSelector.selectedIndex].value + " with this bribe"; @@ -131,7 +135,7 @@ export class CorporationEventHandler { } else { var totalAmount = money + (stockShares * stockPrice); var repGain = totalAmount / BribeToRepRatio; - dialogBoxCreate("You gained " + formatNumber(repGain, 0) + + dialogBoxCreate("You gained " + numeralWrapper.format(repGain, "0,0") + " reputation with " + fac.name + " by bribing them."); fac.playerReputation += repGain; this.corp.funds = this.corp.funds.minus(money); @@ -170,7 +174,6 @@ export class CorporationEventHandler { type:"number", placeholder:"Shares to buyback", margin:"5px", inputListener: ()=> { var numShares = Math.round(input.value); - //TODO add conditional for if player doesn't have enough money if (isNaN(numShares) || numShares <= 0) { costIndicator.innerText = "ERROR: Invalid value entered for number of shares to buyback" } else if (numShares > this.corp.issuedShares) { @@ -228,7 +231,7 @@ export class CorporationEventHandler { } // Create a popup that lets the player discontinue a product - createDiscontinueProductPopup(product) { + createDiscontinueProductPopup(product, industry) { const popupId = "cmpy-mgmt-discontinue-product-popup"; const txt = createElement("p", { innerText:"Are you sure you want to do this? Discontinuing a product " + @@ -237,9 +240,9 @@ export class CorporationEventHandler { "removed and left unsold", }); const confirmBtn = createElement("button", { - class:"a-link-button",innerText:"Discontinue", + class:"popup-box-button",innerText:"Discontinue", clickListener: () => { - industry.discontinueProduct(product, parentRefs); + industry.discontinueProduct(product); removeElementById(popupId); this.rerender(); return false; @@ -247,7 +250,7 @@ export class CorporationEventHandler { }); const cancelBtn = createPopupCloseButton(popupId, { innerText: "Cancel" }); - createPopup(popupId, [txt, confirmBtn, cancelBtn]); + createPopup(popupId, [txt, cancelBtn, confirmBtn]); } // Create a popup that lets the player manage exports @@ -669,8 +672,8 @@ export class CorporationEventHandler { productNameInput.focus(); } - // Create a popup that lets the player use the Market TA research - createMarketTaPopup(mat, industry) { + // Create a popup that lets the player use the Market TA research for Materials + createMaterialMarketTaPopup(mat, industry) { const corp = this.corp; const popupId = "cmpy-mgmt-marketta-popup"; @@ -694,19 +697,21 @@ export class CorporationEventHandler { "be sold at the price identified by Market-TA.I (i.e. the price shown above)" }) const useTa1AutoSaleCheckbox = createElement("input", { + checked: mat.marketTa1, id: useTa1AutoSaleId, + margin: "3px", type: "checkbox", - value: mat.marketTa1, changeListener: (e) => { - mat.marketTa1 = e.target.value; + mat.marketTa1 = e.target.checked; } }); - useTa1AutoSaleDiv.appendChild(useTa1AutoSaleCheckbox); + useTa1AutoSaleDiv.appendChild(useTa1AutoSaleLabel); useTa1AutoSaleDiv.appendChild(useTa1AutoSaleCheckbox); const closeBtn = createPopupCloseButton(popupId, { class: "std-button", display: "block", + innerText: "Close", }); if (industry.hasResearch("Market-TA.II")) { @@ -741,11 +746,36 @@ export class CorporationEventHandler { } ta2Text.innerHTML = `
Market-TA.II
` + `If you sell at ${numeralWrapper.formatMoney(sCost)}, ` + - `then you will sell ${formatNumber(markup, 5)}x as much compared ` + + `then you will sell ${numeralWrapper.format(markup, "0.00000")}x as much compared ` + `to if you sold at market price.`; } updateTa2Text(); - createPopup(popupId, [ta1, ta2Text, ta2Input, closeBtn]); + + // Enable using Market-TA2 for automatically setting sale price + const useTa2AutoSaleId = "cmpy-mgmt-marketa2-checkbox"; + const useTa2AutoSaleDiv = createElement("div", { display: "block" }); + const useTa2AutoSaleLabel = createElement("label", { + color: "white", + for: useTa2AutoSaleId, + innerText: "Use Market-TA.II for Auto-Sale Price", + tooltip: "If this is enabled, then this Material will automatically " + + "be sold at the optimal price such that the amount sold matches the " + + "amount produced. (i.e. the highest possible price, while still ensuring " + + " that all produced materials will be sold)" + }) + const useTa2AutoSaleCheckbox = createElement("input", { + checked: mat.marketTa2, + id: useTa2AutoSaleId, + margin: "3px", + type: "checkbox", + changeListener: (e) => { + mat.marketTa2 = e.target.checked; + } + }); + useTa2AutoSaleDiv.appendChild(useTa2AutoSaleLabel); + useTa2AutoSaleDiv.appendChild(useTa2AutoSaleCheckbox); + + createPopup(popupId, [ta1, useTa1AutoSaleDiv, ta2Text, ta2Input, useTa2AutoSaleDiv, closeBtn]); } else { // Market-TA.I only createPopup(popupId, [ta1, useTa1AutoSaleDiv, closeBtn]); @@ -775,6 +805,7 @@ export class CorporationEventHandler { display:"inline-block", innerText: "Confirm", clickListener: () => { + if (citySelector.length <= 0) { return false; } let city = citySelector.options[citySelector.selectedIndex].value; if (this.corp.funds.lt(OfficeInitialCost)) { dialogBoxCreate("You don't have enough company funds to open a new office!"); @@ -921,8 +952,110 @@ export class CorporationEventHandler { return false; } + // Create a popup that lets the player use the Market TA research for Products + createProductMarketTaPopup(product, industry) { + const corp = this.corp; + + const popupId = "cmpy-mgmt-marketta-popup"; + const markupLimit = product.rat / product.mku; + const ta1 = createElement("p", { + innerHTML: "Market-TA.I
" + + "The maximum sale price you can mark this up to is " + + numeralWrapper.formatMoney(product.pCost + markupLimit) + + ". This means that if you set the sale price higher than this, " + + "you will begin to experience a loss in number of sales", + }); + + // Enable using Market-TA1 for automatically setting sale price + const useTa1AutoSaleId = "cmpy-mgmt-marketa1-checkbox"; + const useTa1AutoSaleDiv = createElement("div", { display: "block" }); + const useTa1AutoSaleLabel = createElement("label", { + color: "white", + for: useTa1AutoSaleId, + innerText: "Use Market-TA.I for Auto-Sale Price", + tooltip: "If this is enabled, then this Product will automatically " + + "be sold at the price identified by Market-TA.I (i.e. the price shown above)" + }) + const useTa1AutoSaleCheckbox = createElement("input", { + checked: product.marketTa1, + id: useTa1AutoSaleId, + margin: "3px", + type: "checkbox", + changeListener: (e) => { + product.marketTa1 = e.target.checked; + } + }); + useTa1AutoSaleDiv.appendChild(useTa1AutoSaleLabel); + useTa1AutoSaleDiv.appendChild(useTa1AutoSaleCheckbox); + + const closeBtn = createPopupCloseButton(popupId, { + class: "std-button", + display: "block", + innerText: "Close", + }); + + if (industry.hasResearch("Market-TA.II")) { + let updateTa2Text; + const ta2Text = createElement("p"); + const ta2Input = createElement("input", { + marginTop: "4px", + onkeyup: (e) => { + e.preventDefault(); + updateTa2Text(); + }, + type: "number", + value: product.pCost, + }); + + // Function that updates the text in ta2Text element + updateTa2Text = function() { + const sCost = parseFloat(ta2Input.value); + let markup = 1; + if (sCost > product.pCost) { + if ((sCost - product.pCost) > markupLimit) { + markup = markupLimit / (sCost - product.pCost); + } + } + ta2Text.innerHTML = `
Market-TA.II
` + + `If you sell at ${numeralWrapper.formatMoney(sCost)}, ` + + `then you will sell ${numeralWrapper.format(markup, "0.00000")}x as much compared ` + + `to if you sold at market price.`; + } + updateTa2Text(); + + // Enable using Market-TA2 for automatically setting sale price + const useTa2AutoSaleId = "cmpy-mgmt-marketa2-checkbox"; + const useTa2AutoSaleDiv = createElement("div", { display: "block" }); + const useTa2AutoSaleLabel = createElement("label", { + color: "white", + for: useTa2AutoSaleId, + innerText: "Use Market-TA.II for Auto-Sale Price", + tooltip: "If this is enabled, then this Product will automatically " + + "be sold at the optimal price such that the amount sold matches the " + + "amount produced. (i.e. the highest possible price, while still ensuring " + + " that all produced materials will be sold)" + }) + const useTa2AutoSaleCheckbox = createElement("input", { + checked: product.marketTa2, + id: useTa2AutoSaleId, + margin: "3px", + type: "checkbox", + changeListener: (e) => { + product.marketTa2 = e.target.checked; + } + }); + useTa2AutoSaleDiv.appendChild(useTa2AutoSaleLabel); + useTa2AutoSaleDiv.appendChild(useTa2AutoSaleCheckbox); + + createPopup(popupId, [ta1, useTa1AutoSaleDiv, ta2Text, ta2Input, useTa2AutoSaleDiv, closeBtn]); + } else { + // Market-TA.I only + createPopup(popupId, [ta1, useTa1AutoSaleDiv, closeBtn]); + } + } + // Create a popup that lets the player purchase a Material - createPurchaseMaterialPopup(mat, industry) { + createPurchaseMaterialPopup(mat, industry, warehouse) { const corp = this.corp; const purchasePopupId = "cmpy-mgmt-material-purchase-popup"; @@ -980,15 +1113,20 @@ export class CorporationEventHandler { let bulkPurchaseCostTxt = createElement("p"); function updateBulkPurchaseText(amount) { - const cost = parseFloat(amount) * mat.bCost; - if (isNaN(cost)) { - dialogBoxCreate(`Bulk Purchase Cost calculated to be NaN. This is either due to ` + - `invalid input, or it is a bug (in which case you should report to dev)`); - return; - } + const parsedAmt = parseFloat(amount); + const cost = parsedAmt * mat.bCost; - bulkPurchaseCostTxt.innerText = `Purchasing ${numeralWrapper.format(amt, "0,0.00")} of ` + - `${mat.name} will cost ${numeralWrapper.formatMoney(cost)}`; + const matSize = MaterialSizes[mat.name]; + const maxAmount = ((warehouse.size - warehouse.sizeUsed) / matSize); + + if (parsedAmt * matSize > maxAmount) { + bulkPurchaseCostTxt.innerText = "Not enough warehouse space to purchase this amount"; + } else if (isNaN(cost)) { + bulkPurchaseCostTxt.innerText = "Invalid put for Bulk Purchase amount"; + } else { + bulkPurchaseCostTxt.innerText = `Purchasing ${numeralWrapper.format(parsedAmt, "0,0.00")} of ` + + `${mat.name} will cost ${numeralWrapper.formatMoney(cost)}`; + } } let bulkPurchaseConfirmBtn; @@ -998,7 +1136,7 @@ export class CorporationEventHandler { type: "number", onkeyup: (e) => { e.preventDefault(); - updateBulkPurchaseText(); + updateBulkPurchaseText(e.target.value); if (e.keyCode === KEY.ENTER) {bulkPurchaseConfirmBtn.click();} } }); @@ -1007,7 +1145,15 @@ export class CorporationEventHandler { class: "std-button", innerText: "Confirm Bulk Purchase", clickListener: () => { - const amount = parseFloat(input.value); + const amount = parseFloat(bulkPurchaseInput.value); + + const matSize = MaterialSizes[mat.name]; + const maxAmount = ((warehouse.size - warehouse.sizeUsed) / matSize); + if (amount * matSize > maxAmount) { + dialogBoxCreate(`You do not have enough warehouse size to fit this purchase`); + return false; + } + if (isNaN(amount)) { dialogBoxCreate("Invalid input amount"); } else { @@ -1065,9 +1211,18 @@ export class CorporationEventHandler { if (e.keyCode === KEY.ENTER) {confirmBtn.click();} } }); + + let inputButtonInitValue = mat.sCost ? mat.sCost : null; + if (mat.marketTa2) { + inputButtonInitValue += " (Market-TA.II)"; + } else if (mat.marketTa1) { + inputButtonInitValue += " (Market-TA.I)"; + } + const inputPx = createElement("input", { type: "text", marginTop: "4px", - value: mat.sCost ? mat.sCost : null, placeholder: "Sell price", + value: inputButtonInitValue, + placeholder: "Sell price", onkeyup: (e) => { e.preventDefault(); if (e.keyCode === KEY.ENTER) {confirmBtn.click();} @@ -1179,16 +1334,41 @@ export class CorporationEventHandler { if (e.keyCode === KEY.ENTER) {confirmBtn.click();} } }); + + let inputButtonInitValue = product.sCost ? product.sCost : null; + if (product.marketTa2) { + inputButtonInitValue += " (Market-TA.II)"; + } else if (product.marketTa1) { + inputButtonInitValue += " (Market-TA.I)"; + } + const inputPx = createElement("input", { margin: "5px 0px 5px 0px", placeholder: "Sell price", type: "text", - value: product.sCost ? product.sCost : null, + value: inputButtonInitValue, onkeyup: (e) => { e.preventDefault(); if (e.keyCode === KEY.ENTER) {confirmBtn.click();} } }); + const checkboxDiv = createElement("div", { + border: "1px solid white", + display: "inline-block", + }) + const checkboxLabel = createElement("label", { + for: popupId + "-checkbox", + innerText: "Use same 'Sell Amount' for all cities", + }); + const checkbox = createElement("input", { + checked: true, + id: popupId + "-checkbox", + margin: "2px", + type: "checkbox", + }); + checkboxDiv.appendChild(checkboxLabel); + checkboxDiv.appendChild(checkbox); + confirmBtn = createElement("button", { class: "std-button", innerText: "Confirm", @@ -1220,7 +1400,10 @@ export class CorporationEventHandler { product.sCost = cost; } - //Parse quantity + // Array of all cities. Used later + const cities = Object.values(Cities); + + // Parse quantity if (inputQty.value.includes("MAX") || inputQty.value.includes("PROD")) { //Dynamically evaluated quantity. First test to make sure its valid var qty = inputQty.value.replace(/\s+/g, ''); @@ -1238,8 +1421,16 @@ export class CorporationEventHandler { dialogBoxCreate("Invalid value or expression for sell price field"); return false; } - product.sllman[city][0] = true; - product.sllman[city][1] = qty; //Use sanitized input + if (checkbox.checked) { + for (let i = 0; i < cities.length; ++i) { + const tempCity = cities[i]; + product.sllman[tempCity][0] = true; + product.sllman[tempCity][1] = qty; //Use sanitized input + } + } else { + product.sllman[city][0] = true; + product.sllman[city][1] = qty; //Use sanitized input + } } else if (isNaN(inputQty.value)) { dialogBoxCreate("Invalid value for sell quantity field! Must be numeric"); return false; @@ -1247,10 +1438,25 @@ export class CorporationEventHandler { var qty = parseFloat(inputQty.value); if (isNaN(qty)) {qty = 0;} if (qty === 0) { - product.sllman[city][0] = false; + if (checkbox.checked) { + for (let i = 0; i < cities.length; ++i) { + const tempCity = cities[i]; + product.sllman[tempCity][0] = false; + } + } else { + product.sllman[city][0] = false; + } } else { - product.sllman[city][0] = true; - product.sllman[city][1] = qty; + if (checkbox.checked) { + for (let i = 0; i < cities.length; ++i) { + const tempCity = cities[i]; + product.sllman[tempCity][0] = true; + product.sllman[tempCity][1] = qty; + } + } else { + product.sllman[city][0] = true; + product.sllman[city][1] = qty; + } } } @@ -1259,9 +1465,12 @@ export class CorporationEventHandler { return false; } }); - const cancelBtn = createPopupCloseButton(popupId, { innerText: "Cancel" }); + const cancelBtn = createPopupCloseButton(popupId, { class: "std-button" }); - createPopup(popupId, [txt, inputQty, inputPx, confirmBtn, cancelBtn]); + const linebreak1 = createElement("br"); + + createPopup(popupId, [txt, inputQty, inputPx, confirmBtn, cancelBtn, linebreak1, + checkboxDiv]); inputQty.focus(); } diff --git a/src/Corporation/ui/IndustryOffice.jsx b/src/Corporation/ui/IndustryOffice.jsx index 91d234a80..54ce1dfa7 100644 --- a/src/Corporation/ui/IndustryOffice.jsx +++ b/src/Corporation/ui/IndustryOffice.jsx @@ -305,7 +305,7 @@ export class IndustryOffice extends BaseReactComponent {

Total Employee Salary: {numeralWrapper.formatMoney(totalSalary)}

{ vechain && -

+

Material Production: {numeralWrapper.format(division.getOfficeProductivity(office), "0.000")} The base amount of material this office can produce. Does not include @@ -314,9 +314,12 @@ export class IndustryOffice extends BaseReactComponent {

} + { + vechain &&
+ } { vechain && -

+

Product Production: {numeralWrapper.format(division.getOfficeProductivity(office, {forProduct:true}), "0.000")} The base amount of any given Product this office can produce. Does not include @@ -325,15 +328,21 @@ export class IndustryOffice extends BaseReactComponent {

} + { + vechain &&
+ } { vechain && -

+

Business Multiplier: x{numeralWrapper.format(division.getBusinessFactor(office), "0.000")} The effect this office's 'Business' employees has on boosting sales

} + { + vechain &&
+ }

{EmployeePositions.Operations} ({this.state.numOperations}) diff --git a/src/Corporation/ui/IndustryWarehouse.jsx b/src/Corporation/ui/IndustryWarehouse.jsx index e13bb3cf7..8b69638f1 100644 --- a/src/Corporation/ui/IndustryWarehouse.jsx +++ b/src/Corporation/ui/IndustryWarehouse.jsx @@ -3,13 +3,13 @@ import React from "react"; import { BaseReactComponent } from "./BaseReactComponent"; -import { Material } from "../Material"; -import { Product } from "../Product"; - -import { Warehouse, +import { OfficeSpace, WarehouseInitialCost, WarehouseUpgradeBaseCost, ProductProductionCostRatio } from "../Corporation"; +import { Material } from "../Material"; +import { Product } from "../Product"; +import { Warehouse } from "../Warehouse"; import { numeralWrapper } from "../../ui/numeralFormat"; @@ -45,7 +45,12 @@ function ProductComponent(props) { sellButtonText = "Sell (0.000/0.000)"; } - if (product.sCost) { + if (product.marketTa2) { + sellButtonText += (" @ " + numeralWrapper.formatMoney(product.marketTa2Price[city])); + } else if (product.marketTa1) { + const markupLimit = product.rat / product.mku; + sellButtonText += (" @ " + numeralWrapper.formatMoney(product.pCost + markupLimit)); + } else if (product.sCost) { if (isString(product.sCost)) { sellButtonText += (" @ " + product.sCost); } else { @@ -55,14 +60,17 @@ function ProductComponent(props) { const sellButtonOnClick = eventHandler.createSellProductPopup.bind(eventHandler, product, city); // Limit Production button - const limitProductionButtonText = "Limit Production"; + let limitProductionButtonText = "Limit Production"; if (product.prdman[city][0]) { limitProductionButtonText += " (" + numeralWrapper.format(product.prdman[city][1], nf) + ")"; } const limitProductionButtonOnClick = eventHandler.createLimitProductProdutionPopup.bind(eventHandler, product, city); // Discontinue Button - const discontinueButtonOnClick = eventHandler.createDiscontinueProductPopup.bind(eventHandler, product); + const discontinueButtonOnClick = eventHandler.createDiscontinueProductPopup.bind(eventHandler, product, division); + + // Market TA button + const marketTaButtonOnClick = eventHandler.createProductMarketTaPopup.bind(eventHandler, product, division); // Unfinished Product if (!product.fin) { @@ -83,6 +91,12 @@ function ProductComponent(props) { + { + division.hasResearch("Market-TA.I") && + + } ) @@ -139,7 +153,7 @@ function ProductComponent(props) {


- Est. Market Price: {numeralWrapper.formatMoney(product.pCost + product.rat / product.mku)} + Est. Market Price: {numeralWrapper.formatMoney(product.pCost)} An estimate of how much consumers are willing to pay for this product. Setting the sale price above this may result in less sales. Setting the sale price below this may result @@ -157,6 +171,12 @@ function ProductComponent(props) { + { + division.hasResearch("Market-TA.I") && + + } ) @@ -167,9 +187,14 @@ function MaterialComponent(props) { const corp = props.corp; const division = props.division; const warehouse = props.warehouse; + const city = props.city; const mat = props.mat; const eventHandler = props.eventHandler; const markupLimit = mat.getMarkupLimit(); + const office = division.offices[city]; + if (!(office instanceof OfficeSpace)) { + throw new Error(`Could not get OfficeSpace object for this city (${city})`); + } // Numeraljs formatter const nf = "0.000"; @@ -195,7 +220,7 @@ function MaterialComponent(props) { // Purchase material button const purchaseButtonText = `Buy (${numeralWrapper.format(mat.buy, nf)})`; const purchaseButtonClass = tutorial ? "std-button flashing-button tooltip" : "std-button"; - const purchaseButtonOnClick = eventHandler.createPurchaseMaterialPopup.bind(eventHandler, mat, division); + const purchaseButtonOnClick = eventHandler.createPurchaseMaterialPopup.bind(eventHandler, mat, division, warehouse); // Export material button const exportButtonOnClick = eventHandler.createExportMaterialPopup.bind(eventHandler, mat); @@ -209,10 +234,12 @@ function MaterialComponent(props) { sellButtonText = `Sell (${numeralWrapper.format(mat.sll, nf)}/${numeralWrapper.format(mat.sllman[1], nf)})`; } - if (mat.sCost) { - if (mat.marketTa1) { - sellButtonText += " @ " + numeralWrapper.formatMoney(mat.bCost + markupLimit); - } else if (isString(mat.sCost)) { + if (mat.marketTa2) { + sellButtonText += " @ " + numeralWrapper.formatMoney(mat.marketTa2Price); + } else if (mat.marketTa1) { + sellButtonText += " @ " + numeralWrapper.formatMoney(mat.bCost + markupLimit); + } else if (mat.sCost) { + if (isString(mat.sCost)) { var sCost = mat.sCost.replace(/MP/g, mat.bCost); sellButtonText += " @ " + numeralWrapper.formatMoney(eval(sCost)); } else { @@ -225,7 +252,7 @@ function MaterialComponent(props) { const sellButtonOnClick = eventHandler.createSellMaterialPopup.bind(eventHandler, mat); // Market TA button - const marketTaButtonOnClick = eventHandler.createMarketTaPopup.bind(eventHandler, mat, division); + const marketTaButtonOnClick = eventHandler.createMaterialMarketTaPopup.bind(eventHandler, mat, division); return (

@@ -411,6 +438,7 @@ export class IndustryWarehouse extends BaseReactComponent { // Only create UI for materials that are relevant for the industry if (isRelevantMaterial(matName)) { mats.push(MaterialComponent({ + city: this.props.currentCity, corp: corp, division: division, eventHandler: this.eventHandler(), diff --git a/src/Locations/createCityMap.ts b/src/Locations/createCityMap.ts new file mode 100644 index 000000000..15d49b928 --- /dev/null +++ b/src/Locations/createCityMap.ts @@ -0,0 +1,18 @@ +/** + * Utility function that creates a "city map", which is an object where + * each city is a key (property). + * + * This map uses the official name of the city, NOT its key in the 'Cities' object + */ +import { Cities } from "./Cities"; +import { IMap } from "../types"; + +export function createCityMap(initValue: T): IMap { + const map: IMap = {}; + const cities = Object.values(Cities); + for (let i = 0; i < cities.length; ++i) { + map[cities[i]] = initValue; + } + + return map; +} diff --git a/src/PersonObjects/Sleeve/SleeveAugmentationsUI.ts b/src/PersonObjects/Sleeve/SleeveAugmentationsUI.ts index bfc4fe43b..471d95695 100644 --- a/src/PersonObjects/Sleeve/SleeveAugmentationsUI.ts +++ b/src/PersonObjects/Sleeve/SleeveAugmentationsUI.ts @@ -105,13 +105,13 @@ export function createSleevePurchaseAugsPopup(sleeve: Sleeve, p: IPlayer) { innerHTML: [ `

${aug.name}


`, - `Cost: ${numeralWrapper.formatMoney(aug.baseCost)}

`, + `Cost: ${numeralWrapper.formatMoney(aug.startingCost)}

`, `${aug.info}` ].join(" "), padding: "2px", clickListener: () => { - if (p.canAfford(aug.baseCost)) { - p.loseMoney(aug.baseCost); + if (p.canAfford(aug.startingCost)) { + p.loseMoney(aug.startingCost); sleeve.installAugmentation(aug); dialogBoxCreate(`Installed ${aug.name} on Duplicate Sleeve!`, false) removeElementById(popupId); diff --git a/tsconfig.json b/tsconfig.json index 831898523..3c82af0b1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "baseUrl" : ".", "jsx": "react", - "lib" : ["es2016", "dom"], + "lib" : ["es2016", "dom", "es2017.object"], "module": "commonjs", "target": "es6", "sourceMap": true,