mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2024-11-17 21:23:54 +01:00
CORPORATION: Rewrite validation code for strings of price and quantity (#1753)
This commit is contained in:
parent
9bba6a0a41
commit
246d668951
@ -198,125 +198,89 @@ export function acceptInvestmentOffer(corporation: Corporation): void {
|
|||||||
corporation.investorShares += investShares;
|
corporation.investorShares += investShares;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function convertPriceString(price: string): string {
|
||||||
|
/**
|
||||||
|
* Replace invalid characters. Only accepts:
|
||||||
|
* - Digit characters
|
||||||
|
* - 4 most basic algebraic operations (+ - * /)
|
||||||
|
* - Parentheses
|
||||||
|
* - Dot character
|
||||||
|
* - Any characters in this list: [e, E, M, P]
|
||||||
|
*/
|
||||||
|
const sanitizedPrice = price.replace(/[^\d+\-*/().eEMP]/g, "");
|
||||||
|
|
||||||
|
// Replace MP with test numbers.
|
||||||
|
for (const testNumber of [-1.2e123, -123456, 123456, 1.2e123]) {
|
||||||
|
const temp = sanitizedPrice.replace(/MP/g, testNumber.toString());
|
||||||
|
let evaluatedTemp: unknown;
|
||||||
|
try {
|
||||||
|
evaluatedTemp = eval?.(temp);
|
||||||
|
if (typeof evaluatedTemp !== "number" || !Number.isFinite(evaluatedTemp)) {
|
||||||
|
throw new Error(
|
||||||
|
`Evaluated value is not a valid number: ${evaluatedTemp}. Price: ${price}. sanitizedPrice: ${sanitizedPrice}. testNumber: ${testNumber}.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Invalid value or expression for sell price field: ${error}`, { cause: error });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use sanitized price.
|
||||||
|
return sanitizedPrice;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertAmountString(amount: string): string {
|
||||||
|
/**
|
||||||
|
* Replace invalid characters. Only accepts:
|
||||||
|
* - Digit characters
|
||||||
|
* - 4 most basic algebraic operations (+ - * /)
|
||||||
|
* - Parentheses
|
||||||
|
* - Dot character
|
||||||
|
* - Any characters in this list: [e, E, M, A, X, P, R, O, D, I, N, V]
|
||||||
|
*/
|
||||||
|
const sanitizedAmount = amount.replace(/[^\d+\-*/().eEMAXPRODINV]/g, "");
|
||||||
|
|
||||||
|
for (const testNumber of [-1.2e123, -123456, 123456, 1.2e123]) {
|
||||||
|
let temp = sanitizedAmount.replace(/MAX/g, testNumber.toString());
|
||||||
|
temp = temp.replace(/PROD/g, testNumber.toString());
|
||||||
|
temp = temp.replace(/INV/g, testNumber.toString());
|
||||||
|
let evaluatedTemp: unknown;
|
||||||
|
try {
|
||||||
|
evaluatedTemp = eval?.(temp);
|
||||||
|
if (typeof evaluatedTemp !== "number" || !Number.isFinite(evaluatedTemp)) {
|
||||||
|
throw new Error(
|
||||||
|
`Evaluated value is not a valid number: ${evaluatedTemp}. Amount: ${amount}. sanitizedAmount: ${sanitizedAmount}. testNumber: ${testNumber}.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Invalid value or expression for sell quantity field: ${error}`, { cause: error });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use sanitized amount.
|
||||||
|
return sanitizedAmount;
|
||||||
|
}
|
||||||
|
|
||||||
export function sellMaterial(material: Material, amount: string, price: string): void {
|
export function sellMaterial(material: Material, amount: string, price: string): void {
|
||||||
if (price === "") price = "0";
|
const convertedPrice = convertPriceString(price.toUpperCase());
|
||||||
if (amount === "") amount = "0";
|
const convertedAmount = convertAmountString(amount.toUpperCase());
|
||||||
let cost = price.replace(/\s+/g, "");
|
|
||||||
cost = cost.replace(/[^-()\d/*+.MPe]/g, ""); //Sanitize cost
|
|
||||||
let temp = cost.replace(/MP/, "1.234e5");
|
|
||||||
try {
|
|
||||||
if (temp.includes("MP")) throw "Only one reference to MP is allowed in sell price.";
|
|
||||||
temp = eval?.(temp);
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error("Invalid value or expression for sell price field", { cause: error });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (temp == null || isNaN(parseFloat(temp))) {
|
material.desiredSellPrice = convertedPrice;
|
||||||
throw new Error("Invalid value or expression for sell price field");
|
material.desiredSellAmount = convertedAmount;
|
||||||
}
|
|
||||||
|
|
||||||
if (cost.includes("MP")) {
|
|
||||||
material.desiredSellPrice = cost; //Dynamically evaluated
|
|
||||||
} else {
|
|
||||||
material.desiredSellPrice = temp;
|
|
||||||
}
|
|
||||||
|
|
||||||
//Parse quantity
|
|
||||||
amount = amount.toUpperCase();
|
|
||||||
if (amount.includes("MAX") || amount.includes("PROD") || amount.includes("INV")) {
|
|
||||||
let q = amount.replace(/\s+/g, "");
|
|
||||||
q = q.replace(/[^-()\d/*+.MAXPRODINV]/g, "");
|
|
||||||
let tempQty = q.replace(/MAX/g, material.maxSellPerCycle.toString());
|
|
||||||
tempQty = tempQty.replace(/PROD/g, material.productionAmount.toString());
|
|
||||||
tempQty = tempQty.replace(/INV/g, material.productionAmount.toString());
|
|
||||||
try {
|
|
||||||
tempQty = eval?.(tempQty);
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error("Invalid value or expression for sell quantity field", { cause: error });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tempQty == null || isNaN(parseFloat(tempQty))) {
|
|
||||||
throw new Error("Invalid value or expression for sell quantity field");
|
|
||||||
}
|
|
||||||
material.desiredSellAmount = q; //Use sanitized input
|
|
||||||
} else if (isNaN(parseFloat(amount)) || parseFloat(amount) < 0) {
|
|
||||||
throw new Error("Invalid value for sell quantity field! Must be numeric or 'PROD' or 'MAX'");
|
|
||||||
} else {
|
|
||||||
let q = parseFloat(amount);
|
|
||||||
if (isNaN(q)) {
|
|
||||||
q = 0;
|
|
||||||
}
|
|
||||||
material.desiredSellAmount = q;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sellProduct(product: Product, city: CityName, amt: string, price: string, all: boolean): void {
|
export function sellProduct(product: Product, city: CityName, amt: string, price: string, all: boolean): void {
|
||||||
//Parse price
|
const convertedPrice = convertPriceString(price.toUpperCase());
|
||||||
// initliaze newPrice with oldPrice as default
|
const convertedAmount = convertAmountString(amt.toUpperCase());
|
||||||
let newPrice = product.cityData[city].desiredSellPrice;
|
|
||||||
if (price.includes("MP")) {
|
|
||||||
//Dynamically evaluated quantity. First test to make sure its valid
|
|
||||||
//Sanitize input, then replace dynamic variables with arbitrary numbers
|
|
||||||
price = price.replace(/\s+/g, "");
|
|
||||||
price = price.replace(/[^-()\d/*+.MPe]/g, "");
|
|
||||||
let temp = price.replace(/MP/, "1.234e5");
|
|
||||||
try {
|
|
||||||
if (temp.includes("MP")) throw "Only one reference to MP is allowed in sell price.";
|
|
||||||
temp = eval?.(temp);
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error("Invalid value or expression for sell price field.", { cause: error });
|
|
||||||
}
|
|
||||||
if (temp == null || isNaN(parseFloat(temp))) {
|
|
||||||
throw new Error("Invalid value or expression for sell price field.");
|
|
||||||
}
|
|
||||||
newPrice = price; //Use sanitized price
|
|
||||||
} else {
|
|
||||||
const cost = parseFloat(price);
|
|
||||||
if (isNaN(cost)) {
|
|
||||||
throw new Error("Invalid value for sell price field");
|
|
||||||
}
|
|
||||||
newPrice = cost;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse quantity
|
|
||||||
amt = amt.toUpperCase();
|
|
||||||
//initialize newAmount with old as default
|
|
||||||
let newAmount = product.cityData[city].desiredSellAmount;
|
|
||||||
if (amt.includes("MAX") || amt.includes("PROD") || amt.includes("INV")) {
|
|
||||||
//Dynamically evaluated quantity. First test to make sure its valid
|
|
||||||
let qty = amt.replace(/\s+/g, "");
|
|
||||||
qty = qty.replace(/[^-()\d/*+.MAXPRODINV]/g, "");
|
|
||||||
let temp = qty.replace(/MAX/g, product.maxSellAmount.toString());
|
|
||||||
temp = temp.replace(/PROD/g, product.cityData[city].productionAmount.toString());
|
|
||||||
temp = temp.replace(/INV/g, product.cityData[city].stored.toString());
|
|
||||||
try {
|
|
||||||
temp = eval?.(temp);
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error("Invalid value or expression for sell quantity field", { cause: error });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (temp == null || isNaN(parseFloat(temp))) {
|
|
||||||
throw new Error("Invalid value or expression for sell quantity field");
|
|
||||||
}
|
|
||||||
newAmount = qty; //Use sanitized input
|
|
||||||
} else if (isNaN(parseFloat(amt)) || parseFloat(amt) < 0) {
|
|
||||||
throw new Error("Invalid value for sell quantity field! Must be numeric or 'PROD' or 'MAX'");
|
|
||||||
} else {
|
|
||||||
let qty = parseFloat(amt);
|
|
||||||
if (isNaN(qty)) {
|
|
||||||
qty = 0;
|
|
||||||
}
|
|
||||||
newAmount = qty;
|
|
||||||
}
|
|
||||||
//apply new price and amount to all or just current
|
|
||||||
if (all) {
|
if (all) {
|
||||||
for (const cityName of Object.values(CityName)) {
|
for (const cityName of Object.values(CityName)) {
|
||||||
product.cityData[cityName].desiredSellAmount = newAmount;
|
product.cityData[cityName].desiredSellAmount = convertedAmount;
|
||||||
product.cityData[cityName].desiredSellPrice = newPrice;
|
product.cityData[cityName].desiredSellPrice = convertedPrice;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
product.cityData[city].desiredSellAmount = newAmount;
|
product.cityData[city].desiredSellAmount = convertedAmount;
|
||||||
product.cityData[city].desiredSellPrice = newPrice;
|
product.cityData[city].desiredSellPrice = convertedPrice;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -577,23 +541,21 @@ Attempted export amount: ${amount}`);
|
|||||||
sanitizedAmt = sanitizedAmt.replace(/[^-()\d/*+.MAXEPRODINV]/g, "");
|
sanitizedAmt = sanitizedAmt.replace(/[^-()\d/*+.MAXEPRODINV]/g, "");
|
||||||
for (const testReplacement of ["(1.23)", "(-1.23)"]) {
|
for (const testReplacement of ["(1.23)", "(-1.23)"]) {
|
||||||
const replaced = sanitizedAmt.replace(/(MAX|IPROD|EPROD|IINV|EINV)/g, testReplacement);
|
const replaced = sanitizedAmt.replace(/(MAX|IPROD|EPROD|IINV|EINV)/g, testReplacement);
|
||||||
let evaluated, error;
|
let evaluated: unknown;
|
||||||
try {
|
try {
|
||||||
evaluated = eval?.(replaced);
|
evaluated = eval?.(replaced);
|
||||||
} catch (e) {
|
if (typeof evaluated !== "number" || !Number.isFinite(evaluated)) {
|
||||||
error = e;
|
throw new Error(`Evaluated value is not a valid number: ${evaluated}`);
|
||||||
}
|
}
|
||||||
if (!error && isNaN(evaluated)) error = "evaluated value is NaN";
|
} catch (error) {
|
||||||
if (error) {
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Error while trying to set the exported amount of ${material.name}.
|
`Error while trying to set the exported amount of ${material.name}.
|
||||||
Error occurred while testing keyword replacement with ${testReplacement}.
|
Error occurred while testing keyword replacement with ${testReplacement}.
|
||||||
Your input: ${amount}
|
Your input: ${amount}
|
||||||
Sanitized input: ${sanitizedAmt}
|
Sanitized input: ${sanitizedAmt}
|
||||||
Input after replacement: ${replaced}
|
Input after replacement: ${replaced}
|
||||||
Evaluated value: ${evaluated}` +
|
Evaluated value: ${evaluated}
|
||||||
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
Error encountered: ${error}`,
|
||||||
`Error encountered: ${error}`,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@ import { PartialRecord, getRecordEntries, getRecordKeys, getRecordValues } from
|
|||||||
import { Material } from "./Material";
|
import { Material } from "./Material";
|
||||||
import { getKeyList } from "../utils/helpers/getKeyList";
|
import { getKeyList } from "../utils/helpers/getKeyList";
|
||||||
import { calculateMarkupMultiplier } from "./helpers";
|
import { calculateMarkupMultiplier } from "./helpers";
|
||||||
|
import { exceptionAlert } from "../utils/helpers/exceptionAlert";
|
||||||
import { throwIfReachable } from "../utils/helpers/throwIfReachable";
|
import { throwIfReachable } from "../utils/helpers/throwIfReachable";
|
||||||
|
|
||||||
interface DivisionParams {
|
interface DivisionParams {
|
||||||
@ -538,25 +539,30 @@ export class Division {
|
|||||||
// The amount gets re-multiplied later, so this is the correct
|
// The amount gets re-multiplied later, so this is the correct
|
||||||
// amount to calculate with for "MAX".
|
// amount to calculate with for "MAX".
|
||||||
const adjustedQty = mat.stored / (corpConstants.secondsPerMarketCycle * marketCycles);
|
const adjustedQty = mat.stored / (corpConstants.secondsPerMarketCycle * marketCycles);
|
||||||
if (typeof mat.desiredSellAmount === "string") {
|
/**
|
||||||
//Dynamically evaluated
|
* desiredSellAmount is usually a string, but it also can be a number in old versions. eval requires a
|
||||||
let tmp = mat.desiredSellAmount.replace(/MAX/g, adjustedQty.toString());
|
* string, so we convert it to a string here, replace placeholders, and then pass it to eval.
|
||||||
tmp = tmp.replace(/PROD/g, mat.productionAmount.toString());
|
*/
|
||||||
|
let temp = String(mat.desiredSellAmount);
|
||||||
|
temp = temp.replace(/MAX/g, adjustedQty.toString());
|
||||||
|
temp = temp.replace(/PROD/g, mat.productionAmount.toString());
|
||||||
|
temp = temp.replace(/INV/g, mat.stored.toString());
|
||||||
try {
|
try {
|
||||||
sellAmt = eval?.(tmp);
|
// Typecasting here is fine. We will validate the result immediately after this line.
|
||||||
} catch (e) {
|
sellAmt = eval?.(temp) as number;
|
||||||
dialogBoxCreate(
|
if (typeof sellAmt !== "number" || !Number.isFinite(sellAmt)) {
|
||||||
`Error evaluating your sell amount for material ${mat.name} in ${this.name}'s ${city} office. The sell amount is being set to zero`,
|
throw new Error(`Evaluated value is not a valid number: ${sellAmt}`);
|
||||||
);
|
|
||||||
sellAmt = 0;
|
|
||||||
}
|
}
|
||||||
} else {
|
} catch (error) {
|
||||||
sellAmt = mat.desiredSellAmount;
|
dialogBoxCreate(
|
||||||
|
`Error evaluating your sell amount for material ${mat.name} in ${this.name}'s ${city} office. Error: ${error}.`,
|
||||||
|
);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine the cost that the material will be sold at
|
// Determine the cost that the material will be sold at
|
||||||
const markupLimit = mat.getMarkupLimit();
|
const markupLimit = mat.getMarkupLimit();
|
||||||
let sCost;
|
let sCost: number;
|
||||||
if (mat.marketTa2) {
|
if (mat.marketTa2) {
|
||||||
// Reverse engineer the 'maxSell' formula
|
// Reverse engineer the 'maxSell' formula
|
||||||
// 1. Set 'maxSell' = sellAmt
|
// 1. Set 'maxSell' = sellAmt
|
||||||
@ -590,11 +596,29 @@ export class Division {
|
|||||||
sCost = optimalPrice;
|
sCost = optimalPrice;
|
||||||
} else if (mat.marketTa1) {
|
} else if (mat.marketTa1) {
|
||||||
sCost = mat.marketPrice + markupLimit;
|
sCost = mat.marketPrice + markupLimit;
|
||||||
} else if (typeof mat.desiredSellPrice === "string") {
|
|
||||||
sCost = mat.desiredSellPrice.replace(/MP/g, mat.marketPrice.toString());
|
|
||||||
sCost = eval?.(sCost);
|
|
||||||
} else {
|
} else {
|
||||||
sCost = mat.desiredSellPrice;
|
// If the player does not set the price, desiredSellPrice will be an empty string.
|
||||||
|
if (mat.desiredSellPrice === "") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* desiredSellPrice is usually a string, but it also can be a number in old versions. eval requires a
|
||||||
|
* string, so we convert it to a string here, replace the placeholder MP, and then pass it to eval later.
|
||||||
|
*/
|
||||||
|
const temp = String(mat.desiredSellPrice).replace(/MP/g, mat.marketPrice.toString());
|
||||||
|
try {
|
||||||
|
// Typecasting here is fine. We will validate the result immediately after this line.
|
||||||
|
sCost = eval?.(temp) as number;
|
||||||
|
if (typeof sCost !== "number" || !Number.isFinite(sCost)) {
|
||||||
|
throw new Error(`Evaluated value is not a valid number: ${sCost}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
dialogBoxCreate(
|
||||||
|
`Error evaluating your sell price for material ${mat.name} in ${this.name}'s ${city} office. ` +
|
||||||
|
`The sell amount is being set to zero. Error: ${error}`,
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
mat.uiMarketPrice = sCost;
|
mat.uiMarketPrice = sCost;
|
||||||
|
|
||||||
@ -658,19 +682,17 @@ export class Division {
|
|||||||
amtStr = amtStr.replace(/IINV/g, `(${tempMaterial.stored})`);
|
amtStr = amtStr.replace(/IINV/g, `(${tempMaterial.stored})`);
|
||||||
let amt = 0;
|
let amt = 0;
|
||||||
try {
|
try {
|
||||||
amt = eval?.(amtStr);
|
// Typecasting here is fine. We will validate the result immediately after this line.
|
||||||
|
amt = eval?.(amtStr) as number;
|
||||||
|
if (typeof amt !== "number" || !Number.isFinite(amt)) {
|
||||||
|
throw new Error(`Evaluated value is not a valid number: ${amt}`);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
dialogBoxCreate(
|
dialogBoxCreate(
|
||||||
`Calculating export for ${mat.name} in ${this.name}'s ${city} division failed with error: ${e}`,
|
`Calculating export for ${mat.name} in ${this.name}'s ${city} division failed with error: ${e}`,
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (isNaN(amt)) {
|
|
||||||
dialogBoxCreate(
|
|
||||||
`Error calculating export amount for ${mat.name} in ${this.name}'s ${city} division.`,
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
amt = amt * corpConstants.secondsPerMarketCycle * marketCycles;
|
amt = amt * corpConstants.secondsPerMarketCycle * marketCycles;
|
||||||
|
|
||||||
if (mat.stored < amt) {
|
if (mat.stored < amt) {
|
||||||
@ -869,35 +891,43 @@ export class Division {
|
|||||||
const marketFactor = this.getMarketFactor(product); //Competition + demand
|
const marketFactor = this.getMarketFactor(product); //Competition + demand
|
||||||
|
|
||||||
// Parse player sell-amount input (needed for TA.II and selling)
|
// Parse player sell-amount input (needed for TA.II and selling)
|
||||||
let sellAmt: number | string;
|
let sellAmt: number;
|
||||||
// The amount gets re-multiplied later, so this is the correct
|
// The amount gets re-multiplied later, so this is the correct
|
||||||
// amount to calculate with for "MAX".
|
// amount to calculate with for "MAX".
|
||||||
const adjustedQty = product.cityData[city].stored / (corpConstants.secondsPerMarketCycle * marketCycles);
|
const adjustedQty = product.cityData[city].stored / (corpConstants.secondsPerMarketCycle * marketCycles);
|
||||||
const desiredSellAmount = product.cityData[city].desiredSellAmount;
|
/**
|
||||||
if (typeof desiredSellAmount === "string") {
|
* desiredSellAmount is usually a string, but it also can be a number in old versions. eval requires a
|
||||||
//Sell amount is dynamically evaluated
|
* string, so we convert it to a string here, replace placeholders, and then pass it to eval.
|
||||||
let tmp: number | string = desiredSellAmount.replace(/MAX/g, adjustedQty.toString());
|
*/
|
||||||
tmp = tmp.replace(/PROD/g, product.cityData[city].productionAmount.toString());
|
const desiredSellAmount = String(product.cityData[city].desiredSellAmount);
|
||||||
|
let temp = desiredSellAmount.replace(/MAX/g, adjustedQty.toString());
|
||||||
|
temp = temp.replace(/PROD/g, product.cityData[city].productionAmount.toString());
|
||||||
|
temp = temp.replace(/INV/g, product.cityData[city].stored.toString());
|
||||||
try {
|
try {
|
||||||
tmp = eval?.(tmp);
|
// Typecasting here is fine. We will validate the result immediately after this line.
|
||||||
if (typeof tmp !== "number") throw "";
|
sellAmt = eval?.(temp) as number;
|
||||||
} catch (e) {
|
if (typeof sellAmt !== "number" || !Number.isFinite(sellAmt)) {
|
||||||
dialogBoxCreate(
|
throw new Error(`Evaluated value is not a valid number: ${sellAmt}`);
|
||||||
`Error evaluating your sell price expression for ${product.name} in ${this.name}'s ${city} office. Sell price is being set to MAX`,
|
}
|
||||||
);
|
} catch (error) {
|
||||||
tmp = product.maxSellAmount;
|
dialogBoxCreate(
|
||||||
|
`Error evaluating your sell amount for ${product.name} in ${this.name}'s ${city} office. Error: ${error}.`,
|
||||||
|
);
|
||||||
|
// break the case "SALE"
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
sellAmt = tmp;
|
|
||||||
} else if (desiredSellAmount && desiredSellAmount > 0) {
|
|
||||||
sellAmt = desiredSellAmount;
|
|
||||||
} else sellAmt = adjustedQty;
|
|
||||||
|
|
||||||
if (sellAmt < 0) sellAmt = 0;
|
if (sellAmt < 0) {
|
||||||
|
sellAmt = 0;
|
||||||
|
}
|
||||||
|
if (product.markup === 0) {
|
||||||
|
exceptionAlert(new Error("product.markup is 0"));
|
||||||
|
product.markup = 1;
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate Sale Cost (sCost), which could be dynamically evaluated
|
// Calculate Sale Cost (sCost), which could be dynamically evaluated
|
||||||
const markupLimit = Math.max(product.cityData[city].effectiveRating, 0.001) / product.markup;
|
const markupLimit = Math.max(product.cityData[city].effectiveRating, 0.001) / product.markup;
|
||||||
let sCost: number;
|
let sCost: number;
|
||||||
const sellPrice = product.cityData[city].desiredSellPrice;
|
|
||||||
if (product.marketTa2) {
|
if (product.marketTa2) {
|
||||||
// Reverse engineer the 'maxSell' formula
|
// Reverse engineer the 'maxSell' formula
|
||||||
// 1. Set 'maxSell' = sellAmt
|
// 1. Set 'maxSell' = sellAmt
|
||||||
@ -930,16 +960,29 @@ export class Division {
|
|||||||
sCost = optimalPrice;
|
sCost = optimalPrice;
|
||||||
} else if (product.marketTa1) {
|
} else if (product.marketTa1) {
|
||||||
sCost = product.cityData[city].productionCost + markupLimit;
|
sCost = product.cityData[city].productionCost + markupLimit;
|
||||||
} else if (typeof sellPrice === "string") {
|
|
||||||
let sCostString = sellPrice;
|
|
||||||
if (product.markup === 0) {
|
|
||||||
console.error(`mku is zero, reverting to 1 to avoid Infinity`);
|
|
||||||
product.markup = 1;
|
|
||||||
}
|
|
||||||
sCostString = sCostString.replace(/MP/g, product.cityData[city].productionCost.toString());
|
|
||||||
sCost = eval?.(sCostString);
|
|
||||||
} else {
|
} else {
|
||||||
sCost = sellPrice;
|
// If the player does not set the price, desiredSellPrice will be an empty string.
|
||||||
|
if (product.cityData[city].desiredSellPrice === "") {
|
||||||
|
// break the case "SALE"
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const temp = String(product.cityData[city].desiredSellPrice).replace(
|
||||||
|
/MP/g,
|
||||||
|
product.cityData[city].productionCost.toString(),
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
// Typecasting here is fine. We will validate the result immediately after this line.
|
||||||
|
sCost = eval?.(temp) as number;
|
||||||
|
if (typeof sCost !== "number" || !Number.isFinite(sCost)) {
|
||||||
|
throw new Error(`Evaluated value is not a valid number: ${sCost}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
dialogBoxCreate(
|
||||||
|
`Error evaluating your sell price for product ${product.name} in ${this.name}'s ${city} office. Error: ${error}.`,
|
||||||
|
);
|
||||||
|
// break the case "SALE"
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
product.uiMarketPrice[city] = sCost;
|
product.uiMarketPrice[city] = sCost;
|
||||||
|
|
||||||
|
@ -31,10 +31,16 @@ export function SellMaterialModal(props: IProps): React.ReactElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onAmtChange(event: React.ChangeEvent<HTMLInputElement>): void {
|
function onAmtChange(event: React.ChangeEvent<HTMLInputElement>): void {
|
||||||
|
if (event.target.value === "") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
setAmt(event.target.value);
|
setAmt(event.target.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onPriceChange(event: React.ChangeEvent<HTMLInputElement>): void {
|
function onPriceChange(event: React.ChangeEvent<HTMLInputElement>): void {
|
||||||
|
if (event.target.value === "") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
setPrice(event.target.value);
|
setPrice(event.target.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,8 +21,8 @@ interface IProps {
|
|||||||
// Create a popup that let the player manage sales of a material
|
// Create a popup that let the player manage sales of a material
|
||||||
export function SellProductModal(props: IProps): React.ReactElement {
|
export function SellProductModal(props: IProps): React.ReactElement {
|
||||||
const [checked, setChecked] = useState(true);
|
const [checked, setChecked] = useState(true);
|
||||||
const [iQty, setQty] = useState<string>(String(props.product.cityData[props.city].desiredSellAmount));
|
const [amt, setAmt] = useState<string>(String(props.product.cityData[props.city].desiredSellAmount));
|
||||||
const [px, setPx] = useState<string>(String(props.product.cityData[props.city].desiredSellPrice));
|
const [price, setPrice] = useState<string>(String(props.product.cityData[props.city].desiredSellPrice));
|
||||||
|
|
||||||
function onCheckedChange(event: React.ChangeEvent<HTMLInputElement>): void {
|
function onCheckedChange(event: React.ChangeEvent<HTMLInputElement>): void {
|
||||||
setChecked(event.target.checked);
|
setChecked(event.target.checked);
|
||||||
@ -30,7 +30,7 @@ export function SellProductModal(props: IProps): React.ReactElement {
|
|||||||
|
|
||||||
function sellProduct(): void {
|
function sellProduct(): void {
|
||||||
try {
|
try {
|
||||||
actions.sellProduct(props.product, props.city, iQty, px, checked);
|
actions.sellProduct(props.product, props.city, amt, price, checked);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
dialogBoxCreate(String(error));
|
dialogBoxCreate(String(error));
|
||||||
}
|
}
|
||||||
@ -39,11 +39,17 @@ export function SellProductModal(props: IProps): React.ReactElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onAmtChange(event: React.ChangeEvent<HTMLInputElement>): void {
|
function onAmtChange(event: React.ChangeEvent<HTMLInputElement>): void {
|
||||||
setQty(event.target.value);
|
if (event.target.value === "") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setAmt(event.target.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onPriceChange(event: React.ChangeEvent<HTMLInputElement>): void {
|
function onPriceChange(event: React.ChangeEvent<HTMLInputElement>): void {
|
||||||
setPx(event.target.value);
|
if (event.target.value === "") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setPrice(event.target.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onKeyDown(event: React.KeyboardEvent<HTMLInputElement>): void {
|
function onKeyDown(event: React.KeyboardEvent<HTMLInputElement>): void {
|
||||||
@ -75,14 +81,14 @@ export function SellProductModal(props: IProps): React.ReactElement {
|
|||||||
</Typography>
|
</Typography>
|
||||||
<br />
|
<br />
|
||||||
<TextField
|
<TextField
|
||||||
value={iQty}
|
value={amt}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Sell amount"
|
placeholder="Sell amount"
|
||||||
onChange={onAmtChange}
|
onChange={onAmtChange}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
/>
|
/>
|
||||||
<TextField value={px} type="text" placeholder="Sell price" onChange={onPriceChange} onKeyDown={onKeyDown} />
|
<TextField value={price} type="text" placeholder="Sell price" onChange={onPriceChange} onKeyDown={onKeyDown} />
|
||||||
<Button onClick={sellProduct} style={{ marginLeft: ".5rem", marginRight: ".5rem" }}>
|
<Button onClick={sellProduct} style={{ marginLeft: ".5rem", marginRight: ".5rem" }}>
|
||||||
Confirm
|
Confirm
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -11,6 +11,8 @@ import { PlayerObject } from "../../src/PersonObjects/Player/PlayerObject";
|
|||||||
import {
|
import {
|
||||||
acceptInvestmentOffer,
|
acceptInvestmentOffer,
|
||||||
buyBackShares,
|
buyBackShares,
|
||||||
|
convertAmountString,
|
||||||
|
convertPriceString,
|
||||||
goPublic,
|
goPublic,
|
||||||
issueNewShares,
|
issueNewShares,
|
||||||
sellShares,
|
sellShares,
|
||||||
@ -84,7 +86,10 @@ describe("Corporation", () => {
|
|||||||
it("should be preserved by seed funding", () => {
|
it("should be preserved by seed funding", () => {
|
||||||
const seedFunded = true;
|
const seedFunded = true;
|
||||||
Player.startCorporation("TestCorp", seedFunded);
|
Player.startCorporation("TestCorp", seedFunded);
|
||||||
expectSharesToAddUp(Player.corporation!);
|
if (!Player.corporation) {
|
||||||
|
throw new Error("Player.startCorporation failed to create a corporation.");
|
||||||
|
}
|
||||||
|
expectSharesToAddUp(Player.corporation);
|
||||||
});
|
});
|
||||||
it("should be preserved by acceptInvestmentOffer", () => {
|
it("should be preserved by acceptInvestmentOffer", () => {
|
||||||
acceptInvestmentOffer(corporation);
|
acceptInvestmentOffer(corporation);
|
||||||
@ -137,4 +142,45 @@ describe("Corporation", () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("convertPriceString", () => {
|
||||||
|
it("should pass normally", () => {
|
||||||
|
expect(convertPriceString("MP")).toStrictEqual("MP");
|
||||||
|
expect(convertPriceString("MP+1")).toStrictEqual("MP+1");
|
||||||
|
expect(convertPriceString("MP+MP")).toStrictEqual("MP+MP");
|
||||||
|
expect(convertPriceString("123")).toStrictEqual("123");
|
||||||
|
expect(convertPriceString("123+456")).toStrictEqual("123+456");
|
||||||
|
expect(convertPriceString("1e10")).toStrictEqual("1e10");
|
||||||
|
expect(convertPriceString("1E10")).toStrictEqual("1E10");
|
||||||
|
});
|
||||||
|
it("should throw errors", () => {
|
||||||
|
expect(() => convertPriceString("")).toThrow();
|
||||||
|
expect(() => convertPriceString("null")).toThrow();
|
||||||
|
expect(() => convertPriceString("undefined")).toThrow();
|
||||||
|
expect(() => convertPriceString("Infinity")).toThrow();
|
||||||
|
expect(() => convertPriceString("abc")).toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("convertAmountString", () => {
|
||||||
|
it("should pass normally", () => {
|
||||||
|
expect(convertAmountString("MAX")).toStrictEqual("MAX");
|
||||||
|
expect(convertAmountString("PROD")).toStrictEqual("PROD");
|
||||||
|
expect(convertAmountString("INV")).toStrictEqual("INV");
|
||||||
|
expect(convertAmountString("MAX+1")).toStrictEqual("MAX+1");
|
||||||
|
expect(convertAmountString("MAX+MAX")).toStrictEqual("MAX+MAX");
|
||||||
|
expect(convertAmountString("MAX+PROD+INV")).toStrictEqual("MAX+PROD+INV");
|
||||||
|
expect(convertAmountString("123")).toStrictEqual("123");
|
||||||
|
expect(convertAmountString("123+456")).toStrictEqual("123+456");
|
||||||
|
expect(convertAmountString("1e10")).toStrictEqual("1e10");
|
||||||
|
expect(convertAmountString("1E10")).toStrictEqual("1E10");
|
||||||
|
});
|
||||||
|
it("should throw errors", () => {
|
||||||
|
expect(() => convertAmountString("")).toThrow();
|
||||||
|
expect(() => convertAmountString("null")).toThrow();
|
||||||
|
expect(() => convertAmountString("undefined")).toThrow();
|
||||||
|
expect(() => convertAmountString("Infinity")).toThrow();
|
||||||
|
expect(() => convertAmountString("abc")).toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user