diff --git a/dist/engine.bundle.js b/dist/engine.bundle.js index 52ec6866c..e3262c675 100644 --- a/dist/engine.bundle.js +++ b/dist/engine.bundle.js @@ -1,4 +1,4 @@ -!function(e){function t(t){for(var a,o,s=t[0],l=t[1],c=t[2],m=0,p=[];m${e}`;r.getElementById("terminal-input").insertAdjacentHTML("beforebegin",a),function(){const e=r.getElementById("terminal-container");e.scrollTop=e.scrollHeight}()}t.post=function(e){i(e)},t.postError=function(e){i(`ERROR: ${e}`,{color:"#ff2929"})},t.hackProgressBarPost=function(e){i(e,{id:"hack-progress-bar"})},t.hackProgressPost=function(e){i(e,{id:"hack-progress"})},t.postElement=function(e){i(a.renderToStaticMarkup(e))},t.postContent=i},,,function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.getRamCost=t.RamCosts=t.RamCostConstants=void 0,t.RamCostConstants={ScriptBaseRamCost:1.6,ScriptDomRamCost:25,ScriptHackRamCost:.1,ScriptHackAnalyzeRamCost:1,ScriptGrowRamCost:.15,ScriptGrowthAnalyzeRamCost:1,ScriptWeakenRamCost:.15,ScriptScanRamCost:.2,ScriptPortProgramRamCost:.05,ScriptRunRamCost:1,ScriptExecRamCost:1.3,ScriptSpawnRamCost:2,ScriptScpRamCost:.6,ScriptKillRamCost:.5,ScriptHasRootAccessRamCost:.05,ScriptGetHostnameRamCost:.05,ScriptGetHackingLevelRamCost:.05,ScriptGetMultipliersRamCost:4,ScriptGetServerRamCost:.1,ScriptGetServerMaxRam:.05,ScriptGetServerUsedRam:.05,ScriptFileExistsRamCost:.1,ScriptIsRunningRamCost:.1,ScriptHacknetNodesRamCost:4,ScriptHNUpgLevelRamCost:.4,ScriptHNUpgRamRamCost:.6,ScriptHNUpgCoreRamCost:.8,ScriptGetStockRamCost:2,ScriptBuySellStockRamCost:2.5,ScriptGetPurchaseServerRamCost:.25,ScriptPurchaseServerRamCost:2.25,ScriptGetPurchasedServerLimit:.05,ScriptGetPurchasedServerMaxRam:.05,ScriptRoundRamCost:.05,ScriptReadWriteRamCost:1,ScriptArbScriptRamCost:1,ScriptGetScriptRamCost:.1,ScriptGetRunningScriptRamCost:.3,ScriptGetHackTimeRamCost:.05,ScriptGetFavorToDonate:.1,ScriptCodingContractBaseRamCost:10,ScriptSleeveBaseRamCost:4,ScriptSingularityFn1RamCost:2,ScriptSingularityFn2RamCost:3,ScriptSingularityFn3RamCost:5,ScriptGangApiBaseRamCost:4,ScriptBladeburnerApiBaseRamCost:4},t.RamCosts={hacknet:{numNodes:()=>0,purchaseNode:()=>0,getPurchaseNodeCost:()=>0,getNodeStats:()=>0,upgradeLevel:()=>0,upgradeRam:()=>0,upgradeCore:()=>0,upgradeCache:()=>0,getLevelUpgradeCost:()=>0,getRamUpgradeCost:()=>0,getCoreUpgradeCost:()=>0,getCacheUpgradeCost:()=>0,numHashes:()=>0,hashCost:()=>0,spendHashes:()=>0},sprintf:()=>0,vsprintf:()=>0,scan:()=>t.RamCostConstants.ScriptScanRamCost,hack:()=>t.RamCostConstants.ScriptHackRamCost,hackAnalyzeThreads:()=>t.RamCostConstants.ScriptHackAnalyzeRamCost,hackAnalyzePercent:()=>t.RamCostConstants.ScriptHackAnalyzeRamCost,hackChance:()=>t.RamCostConstants.ScriptHackAnalyzeRamCost,sleep:()=>0,grow:()=>t.RamCostConstants.ScriptGrowRamCost,growthAnalyze:()=>t.RamCostConstants.ScriptGrowthAnalyzeRamCost,weaken:()=>t.RamCostConstants.ScriptWeakenRamCost,print:()=>0,tprint:()=>0,clearLog:()=>0,disableLog:()=>0,enableLog:()=>0,isLogEnabled:()=>0,getScriptLogs:()=>0,nuke:()=>t.RamCostConstants.ScriptPortProgramRamCost,brutessh:()=>t.RamCostConstants.ScriptPortProgramRamCost,ftpcrack:()=>t.RamCostConstants.ScriptPortProgramRamCost,relaysmtp:()=>t.RamCostConstants.ScriptPortProgramRamCost,httpworm:()=>t.RamCostConstants.ScriptPortProgramRamCost,sqlinject:()=>t.RamCostConstants.ScriptPortProgramRamCost,run:()=>t.RamCostConstants.ScriptRunRamCost,exec:()=>t.RamCostConstants.ScriptExecRamCost,spawn:()=>t.RamCostConstants.ScriptSpawnRamCost,kill:()=>t.RamCostConstants.ScriptKillRamCost,killall:()=>t.RamCostConstants.ScriptKillRamCost,exit:()=>0,scp:()=>t.RamCostConstants.ScriptScpRamCost,ls:()=>t.RamCostConstants.ScriptScanRamCost,ps:()=>t.RamCostConstants.ScriptScanRamCost,hasRootAccess:()=>t.RamCostConstants.ScriptHasRootAccessRamCost,getIp:()=>t.RamCostConstants.ScriptGetHostnameRamCost,getHostname:()=>t.RamCostConstants.ScriptGetHostnameRamCost,getHackingLevel:()=>t.RamCostConstants.ScriptGetHackingLevelRamCost,getHackingMultipliers:()=>t.RamCostConstants.ScriptGetMultipliersRamCost,getHacknetMultipliers:()=>t.RamCostConstants.ScriptGetMultipliersRamCost,getBitNodeMultipliers:()=>t.RamCostConstants.ScriptGetMultipliersRamCost,getServer:()=>t.RamCostConstants.ScriptGetMultipliersRamCost/2,getServerMoneyAvailable:()=>t.RamCostConstants.ScriptGetServerRamCost,getServerSecurityLevel:()=>t.RamCostConstants.ScriptGetServerRamCost,getServerBaseSecurityLevel:()=>t.RamCostConstants.ScriptGetServerRamCost,getServerMinSecurityLevel:()=>t.RamCostConstants.ScriptGetServerRamCost,getServerRequiredHackingLevel:()=>t.RamCostConstants.ScriptGetServerRamCost,getServerMaxMoney:()=>t.RamCostConstants.ScriptGetServerRamCost,getServerGrowth:()=>t.RamCostConstants.ScriptGetServerRamCost,getServerNumPortsRequired:()=>t.RamCostConstants.ScriptGetServerRamCost,getServerRam:()=>t.RamCostConstants.ScriptGetServerRamCost,getServerMaxRam:()=>t.RamCostConstants.ScriptGetServerMaxRam,getServerUsedRam:()=>t.RamCostConstants.ScriptGetServerUsedRam,serverExists:()=>t.RamCostConstants.ScriptGetServerRamCost,fileExists:()=>t.RamCostConstants.ScriptFileExistsRamCost,isRunning:()=>t.RamCostConstants.ScriptIsRunningRamCost,getStockSymbols:()=>t.RamCostConstants.ScriptGetStockRamCost,getStockPrice:()=>t.RamCostConstants.ScriptGetStockRamCost,getStockAskPrice:()=>t.RamCostConstants.ScriptGetStockRamCost,getStockBidPrice:()=>t.RamCostConstants.ScriptGetStockRamCost,getStockPosition:()=>t.RamCostConstants.ScriptGetStockRamCost,getStockMaxShares:()=>t.RamCostConstants.ScriptGetStockRamCost,getStockPurchaseCost:()=>t.RamCostConstants.ScriptGetStockRamCost,getStockSaleGain:()=>t.RamCostConstants.ScriptGetStockRamCost,buyStock:()=>t.RamCostConstants.ScriptBuySellStockRamCost,sellStock:()=>t.RamCostConstants.ScriptBuySellStockRamCost,shortStock:()=>t.RamCostConstants.ScriptBuySellStockRamCost,sellShort:()=>t.RamCostConstants.ScriptBuySellStockRamCost,placeOrder:()=>t.RamCostConstants.ScriptBuySellStockRamCost,cancelOrder:()=>t.RamCostConstants.ScriptBuySellStockRamCost,getOrders:()=>t.RamCostConstants.ScriptBuySellStockRamCost,getStockVolatility:()=>t.RamCostConstants.ScriptBuySellStockRamCost,getStockForecast:()=>t.RamCostConstants.ScriptBuySellStockRamCost,purchase4SMarketData:()=>t.RamCostConstants.ScriptBuySellStockRamCost,purchase4SMarketDataTixApi:()=>t.RamCostConstants.ScriptBuySellStockRamCost,getPurchasedServerLimit:()=>t.RamCostConstants.ScriptGetPurchasedServerLimit,getPurchasedServerMaxRam:()=>t.RamCostConstants.ScriptGetPurchasedServerMaxRam,getPurchasedServerCost:()=>t.RamCostConstants.ScriptGetPurchaseServerRamCost,purchaseServer:()=>t.RamCostConstants.ScriptPurchaseServerRamCost,deleteServer:()=>t.RamCostConstants.ScriptPurchaseServerRamCost,getPurchasedServers:()=>t.RamCostConstants.ScriptPurchaseServerRamCost,write:()=>t.RamCostConstants.ScriptReadWriteRamCost,tryWrite:()=>t.RamCostConstants.ScriptReadWriteRamCost,read:()=>t.RamCostConstants.ScriptReadWriteRamCost,peek:()=>t.RamCostConstants.ScriptReadWriteRamCost,clear:()=>t.RamCostConstants.ScriptReadWriteRamCost,getPortHandle:()=>10*t.RamCostConstants.ScriptReadWriteRamCost,rm:()=>t.RamCostConstants.ScriptReadWriteRamCost,scriptRunning:()=>t.RamCostConstants.ScriptArbScriptRamCost,scriptKill:()=>t.RamCostConstants.ScriptArbScriptRamCost,getScriptName:()=>0,getScriptRam:()=>t.RamCostConstants.ScriptGetScriptRamCost,getHackTime:()=>t.RamCostConstants.ScriptGetHackTimeRamCost,getGrowTime:()=>t.RamCostConstants.ScriptGetHackTimeRamCost,getWeakenTime:()=>t.RamCostConstants.ScriptGetHackTimeRamCost,getScriptIncome:()=>t.RamCostConstants.ScriptGetScriptRamCost,getScriptExpGain:()=>t.RamCostConstants.ScriptGetScriptRamCost,getRunningScript:()=>t.RamCostConstants.ScriptGetRunningScriptRamCost,nFormat:()=>0,getTimeSinceLastAug:()=>t.RamCostConstants.ScriptGetHackTimeRamCost,prompt:()=>0,wget:()=>0,getFavorToDonate:()=>t.RamCostConstants.ScriptGetFavorToDonate,universityCourse:()=>t.RamCostConstants.ScriptSingularityFn1RamCost,gymWorkout:()=>t.RamCostConstants.ScriptSingularityFn1RamCost,travelToCity:()=>t.RamCostConstants.ScriptSingularityFn1RamCost,purchaseTor:()=>t.RamCostConstants.ScriptSingularityFn1RamCost,purchaseProgram:()=>t.RamCostConstants.ScriptSingularityFn1RamCost,getCurrentServer:()=>t.RamCostConstants.ScriptSingularityFn1RamCost,connect:()=>t.RamCostConstants.ScriptSingularityFn1RamCost,manualHack:()=>t.RamCostConstants.ScriptSingularityFn1RamCost,installBackdoor:()=>t.RamCostConstants.ScriptSingularityFn1RamCost,getStats:()=>t.RamCostConstants.ScriptSingularityFn1RamCost/4,getCharacterInformation:()=>t.RamCostConstants.ScriptSingularityFn1RamCost/4,getPlayer:()=>t.RamCostConstants.ScriptSingularityFn1RamCost/4,hospitalize:()=>t.RamCostConstants.ScriptSingularityFn1RamCost/4,isBusy:()=>t.RamCostConstants.ScriptSingularityFn1RamCost/4,stopAction:()=>t.RamCostConstants.ScriptSingularityFn1RamCost/2,upgradeHomeRam:()=>t.RamCostConstants.ScriptSingularityFn2RamCost,getUpgradeHomeRamCost:()=>t.RamCostConstants.ScriptSingularityFn2RamCost/2,workForCompany:()=>t.RamCostConstants.ScriptSingularityFn2RamCost,applyToCompany:()=>t.RamCostConstants.ScriptSingularityFn2RamCost,getCompanyRep:()=>t.RamCostConstants.ScriptSingularityFn2RamCost/3,getCompanyFavor:()=>t.RamCostConstants.ScriptSingularityFn2RamCost/3,getCompanyFavorGain:()=>t.RamCostConstants.ScriptSingularityFn2RamCost/4,checkFactionInvitations:()=>t.RamCostConstants.ScriptSingularityFn2RamCost,joinFaction:()=>t.RamCostConstants.ScriptSingularityFn2RamCost,workForFaction:()=>t.RamCostConstants.ScriptSingularityFn2RamCost,getFactionRep:()=>t.RamCostConstants.ScriptSingularityFn2RamCost/3,getFactionFavor:()=>t.RamCostConstants.ScriptSingularityFn2RamCost/3,getFactionFavorGain:()=>t.RamCostConstants.ScriptSingularityFn2RamCost/4,donateToFaction:()=>t.RamCostConstants.ScriptSingularityFn3RamCost,createProgram:()=>t.RamCostConstants.ScriptSingularityFn3RamCost,commitCrime:()=>t.RamCostConstants.ScriptSingularityFn3RamCost,getCrimeChance:()=>t.RamCostConstants.ScriptSingularityFn3RamCost,getCrimeStats:()=>t.RamCostConstants.ScriptSingularityFn3RamCost,getOwnedAugmentations:()=>t.RamCostConstants.ScriptSingularityFn3RamCost,getOwnedSourceFiles:()=>t.RamCostConstants.ScriptSingularityFn3RamCost,getAugmentationsFromFaction:()=>t.RamCostConstants.ScriptSingularityFn3RamCost,getAugmentationPrereq:()=>t.RamCostConstants.ScriptSingularityFn3RamCost,getAugmentationCost:()=>t.RamCostConstants.ScriptSingularityFn3RamCost,getAugmentationStats:()=>t.RamCostConstants.ScriptSingularityFn3RamCost,purchaseAugmentation:()=>t.RamCostConstants.ScriptSingularityFn3RamCost,softReset:()=>t.RamCostConstants.ScriptSingularityFn3RamCost,installAugmentations:()=>t.RamCostConstants.ScriptSingularityFn3RamCost,gang:{createGang:()=>t.RamCostConstants.ScriptGangApiBaseRamCost/4,inGang:()=>t.RamCostConstants.ScriptGangApiBaseRamCost/4,getMemberNames:()=>t.RamCostConstants.ScriptGangApiBaseRamCost/4,getGangInformation:()=>t.RamCostConstants.ScriptGangApiBaseRamCost/2,getOtherGangInformation:()=>t.RamCostConstants.ScriptGangApiBaseRamCost/2,getMemberInformation:()=>t.RamCostConstants.ScriptGangApiBaseRamCost/2,canRecruitMember:()=>t.RamCostConstants.ScriptGangApiBaseRamCost/4,recruitMember:()=>t.RamCostConstants.ScriptGangApiBaseRamCost/2,getTaskNames:()=>t.RamCostConstants.ScriptGangApiBaseRamCost/4,getTaskStats:()=>t.RamCostConstants.ScriptGangApiBaseRamCost/4,setMemberTask:()=>t.RamCostConstants.ScriptGangApiBaseRamCost/2,getEquipmentNames:()=>t.RamCostConstants.ScriptGangApiBaseRamCost/4,getEquipmentCost:()=>t.RamCostConstants.ScriptGangApiBaseRamCost/2,getEquipmentType:()=>t.RamCostConstants.ScriptGangApiBaseRamCost/2,getEquipmentStats:()=>t.RamCostConstants.ScriptGangApiBaseRamCost/2,purchaseEquipment:()=>t.RamCostConstants.ScriptGangApiBaseRamCost,ascendMember:()=>t.RamCostConstants.ScriptGangApiBaseRamCost,setTerritoryWarfare:()=>t.RamCostConstants.ScriptGangApiBaseRamCost/2,getChanceToWinClash:()=>t.RamCostConstants.ScriptGangApiBaseRamCost,getBonusTime:()=>0},bladeburner:{getContractNames:()=>t.RamCostConstants.ScriptBladeburnerApiBaseRamCost/10,getOperationNames:()=>t.RamCostConstants.ScriptBladeburnerApiBaseRamCost/10,getBlackOpNames:()=>t.RamCostConstants.ScriptBladeburnerApiBaseRamCost/10,getBlackOpRank:()=>t.RamCostConstants.ScriptBladeburnerApiBaseRamCost/2,getGeneralActionNames:()=>t.RamCostConstants.ScriptBladeburnerApiBaseRamCost/10,getSkillNames:()=>t.RamCostConstants.ScriptBladeburnerApiBaseRamCost/10,startAction:()=>t.RamCostConstants.ScriptBladeburnerApiBaseRamCost,stopBladeburnerAction:()=>t.RamCostConstants.ScriptBladeburnerApiBaseRamCost/2,getCurrentAction:()=>t.RamCostConstants.ScriptBladeburnerApiBaseRamCost/4,getActionTime:()=>t.RamCostConstants.ScriptBladeburnerApiBaseRamCost,getActionEstimatedSuccessChance:()=>t.RamCostConstants.ScriptBladeburnerApiBaseRamCost,getActionRepGain:()=>t.RamCostConstants.ScriptBladeburnerApiBaseRamCost,getActionCountRemaining:()=>t.RamCostConstants.ScriptBladeburnerApiBaseRamCost,getActionMaxLevel:()=>t.RamCostConstants.ScriptBladeburnerApiBaseRamCost,getActionCurrentLevel:()=>t.RamCostConstants.ScriptBladeburnerApiBaseRamCost,getActionAutolevel:()=>t.RamCostConstants.ScriptBladeburnerApiBaseRamCost,setActionAutolevel:()=>t.RamCostConstants.ScriptBladeburnerApiBaseRamCost,setActionLevel:()=>t.RamCostConstants.ScriptBladeburnerApiBaseRamCost,getRank:()=>t.RamCostConstants.ScriptBladeburnerApiBaseRamCost,getSkillPoints:()=>t.RamCostConstants.ScriptBladeburnerApiBaseRamCost,getSkillLevel:()=>t.RamCostConstants.ScriptBladeburnerApiBaseRamCost,getSkillUpgradeCost:()=>t.RamCostConstants.ScriptBladeburnerApiBaseRamCost,upgradeSkill:()=>t.RamCostConstants.ScriptBladeburnerApiBaseRamCost,getTeamSize:()=>t.RamCostConstants.ScriptBladeburnerApiBaseRamCost,setTeamSize:()=>t.RamCostConstants.ScriptBladeburnerApiBaseRamCost,getCityEstimatedPopulation:()=>t.RamCostConstants.ScriptBladeburnerApiBaseRamCost,getCityEstimatedCommunities:()=>t.RamCostConstants.ScriptBladeburnerApiBaseRamCost,getCityChaos:()=>t.RamCostConstants.ScriptBladeburnerApiBaseRamCost,getCity:()=>t.RamCostConstants.ScriptBladeburnerApiBaseRamCost,switchCity:()=>t.RamCostConstants.ScriptBladeburnerApiBaseRamCost,getStamina:()=>t.RamCostConstants.ScriptBladeburnerApiBaseRamCost,joinBladeburnerFaction:()=>t.RamCostConstants.ScriptBladeburnerApiBaseRamCost,joinBladeburnerDivision:()=>t.RamCostConstants.ScriptBladeburnerApiBaseRamCost,getBonusTime:()=>0},codingcontract:{attempt:()=>t.RamCostConstants.ScriptCodingContractBaseRamCost,getContractType:()=>t.RamCostConstants.ScriptCodingContractBaseRamCost/2,getData:()=>t.RamCostConstants.ScriptCodingContractBaseRamCost/2,getDescription:()=>t.RamCostConstants.ScriptCodingContractBaseRamCost/2,getNumTriesRemaining:()=>t.RamCostConstants.ScriptCodingContractBaseRamCost/5},sleeve:{getNumSleeves:()=>t.RamCostConstants.ScriptSleeveBaseRamCost,setToShockRecovery:()=>t.RamCostConstants.ScriptSleeveBaseRamCost,setToSynchronize:()=>t.RamCostConstants.ScriptSleeveBaseRamCost,setToCommitCrime:()=>t.RamCostConstants.ScriptSleeveBaseRamCost,setToUniversityCourse:()=>t.RamCostConstants.ScriptSleeveBaseRamCost,travel:()=>t.RamCostConstants.ScriptSleeveBaseRamCost,setToCompanyWork:()=>t.RamCostConstants.ScriptSleeveBaseRamCost,setToFactionWork:()=>t.RamCostConstants.ScriptSleeveBaseRamCost,setToGymWorkout:()=>t.RamCostConstants.ScriptSleeveBaseRamCost,getSleeveStats:()=>t.RamCostConstants.ScriptSleeveBaseRamCost,getTask:()=>t.RamCostConstants.ScriptSleeveBaseRamCost,getInformation:()=>t.RamCostConstants.ScriptSleeveBaseRamCost,getSleeveAugmentations:()=>t.RamCostConstants.ScriptSleeveBaseRamCost,getSleevePurchasableAugs:()=>t.RamCostConstants.ScriptSleeveBaseRamCost,purchaseSleeveAug:()=>t.RamCostConstants.ScriptSleeveBaseRamCost},heart:{break:()=>0}},t.getRamCost=function(...e){if(0===e.length)return console.warn("No arguments passed to getRamCost()"),0;let n=t.RamCosts[e[0]];for(let t=1;t
In this game you control a set of Nodes and use them to try and defeat an enemy. Your Nodes are colored blue, while the enemy's are red. There are also other nodes on the map colored gray that initially belong to neither you nor the enemy. The goal of the game is to capture all of the enemy's Database nodes within the time limit. If you fail to do this, you will lose.

Each Node has three stats: Attack, Defense, and HP. There are five different actions that a Node can take:

Attack - Targets an enemy Node and lowers its HP. The effectiveness is determined by the owner's Attack, the Player's hacking level, and the enemy's defense.

Scan - Targets an enemy Node and lowers its Defense. The effectiveness is determined by the owner's Attack, the Player's hacking level, and the enemy's defense.

Weaken - Targets an enemy Node and lowers its Attack. The effectiveness is determined by the owner's Attack, the Player's hacking level, and the enemy's defense.

Fortify - Raises the Node's Defense. The effectiveness is determined by your hacking level.

Overflow - Raises the Node's Attack but lowers its Defense. The effectiveness is determined by your hacking level.

Note that when determining the effectiveness of the above actions, the TOTAL Attack or Defense of the team is used, not just the Attack/Defense of the individual Node that is performing the action.

To capture a Node, you must lower its HP down to 0.

There are six different types of Nodes:

CPU Core - These are your main Nodes that are used to perform actions. Capable of performing every action

Firewall - Nodes with high defense. These Nodes can 'Fortify'

Database - A special type of Node. The player's objective is to conquer all of the enemy's Database Nodes within the time limit. These Nodes cannot perform any actions

Spam - Conquering one of these Nodes will slow the enemy's trace, giving the player additional time to complete the mission. These Nodes cannot perform any actions

Transfer - Conquering one of these nodes will increase the Attack of all of your CPU Cores by a small fixed percentage. These Nodes are capable of performing every action except the 'Attack' action

Shield - Nodes with high defense. These Nodes can 'Fortify'

To assign an action to a Node, you must first select one of your Nodes. This can be done by simply clicking on it. Double-clicking a node will select all of your Nodes of the same type (e.g. select all CPU Core Nodes or all Transfer Nodes). Note that only Nodes that can perform actions (CPU Core, Transfer, Shield, Firewall) can be selected. Selected Nodes will be denoted with a white highlight. After selecting a Node or multiple Nodes, select its action using the Action Buttons near the top of the screen. Every action also has a corresponding keyboard shortcut.

For certain actions such as attacking, scanning, and weakening, the Node performing the action must have a target. To target another node, simply click-and-drag from the 'source' Node to a target. A Node can only have one target, and you can target any Node that is adjacent to one of your Nodes (immediately above, below, or to the side. NOT diagonal). Furthermore, only CPU Cores and Transfer Nodes can target, since they are the only ones that can perform the related actions. To remove a target, you can simply click on the line that represents the connection between one of your Nodes and its target. Alternatively, you can select the 'source' Node and click the 'Drop Connection' button, or press 'd'.

Other Notes:

-Whenever a miscellenaous Node (not owned by the player or enemy) is conquered, the defense of all remaining miscellaneous Nodes that are not actively being targeted will increase by a fixed percentage.

-Whenever a Node is conquered, its stats are significantly reduced

-Miscellaneous Nodes slowly raise their defense over time

-Nodes slowly regenerate health over time.",MillisecondsPer20Hours:72e6,GameCyclesPer20Hours:36e4,MillisecondsPer10Hours:36e6,GameCyclesPer10Hours:18e4,MillisecondsPer8Hours:288e5,GameCyclesPer8Hours:144e3,MillisecondsPer4Hours:144e5,GameCyclesPer4Hours:72e3,MillisecondsPer2Hours:72e5,GameCyclesPer2Hours:36e3,MillisecondsPerHour:36e5,GameCyclesPerHour:18e3,MillisecondsPerHalfHour:18e5,GameCyclesPerHalfHour:9e3,MillisecondsPerQuarterHour:9e5,GameCyclesPerQuarterHour:4500,MillisecondsPerFiveMinutes:3e5,GameCyclesPerFiveMinutes:1500,FactionWorkHacking:"Faction Hacking Work",FactionWorkField:"Faction Field Work",FactionWorkSecurity:"Faction Security Work",WorkTypeCompany:"Working for Company",WorkTypeCompanyPartTime:"Working for Company part-time",WorkTypeFaction:"Working for Faction",WorkTypeCreateProgram:"Working on Create a Program",WorkTypeStudyClass:"Studying or Taking a class at university",WorkTypeCrime:"Committing a crime",ClassStudyComputerScience:"studying Computer Science",ClassDataStructures:"taking a Data Structures course",ClassNetworks:"taking a Networks course",ClassAlgorithms:"taking an Algorithms course",ClassManagement:"taking a Management course",ClassLeadership:"taking a Leadership course",ClassGymStrength:"training your strength at a gym",ClassGymDefense:"training your defense at a gym",ClassGymDexterity:"training your dexterity at a gym",ClassGymAgility:"training your agility at a gym",ClassDataStructuresBaseCost:40,ClassNetworksBaseCost:80,ClassAlgorithmsBaseCost:320,ClassManagementBaseCost:160,ClassLeadershipBaseCost:320,ClassGymBaseCost:120,ClassStudyComputerScienceBaseExp:.5,ClassDataStructuresBaseExp:1,ClassNetworksBaseExp:2,ClassAlgorithmsBaseExp:4,ClassManagementBaseExp:2,ClassLeadershipBaseExp:4,CrimeShoplift:"shoplift",CrimeRobStore:"rob a store",CrimeMug:"mug someone",CrimeLarceny:"commit larceny",CrimeDrugs:"deal drugs",CrimeBondForgery:"forge corporate bonds",CrimeTraffickArms:"traffick illegal arms",CrimeHomicide:"commit homicide",CrimeGrandTheftAuto:"commit grand theft auto",CrimeKidnap:"kidnap someone for ransom",CrimeAssassination:"assassinate a high-profile target",CrimeHeist:"pull off the ultimate heist",CodingContractBaseFactionRepGain:2500,CodingContractBaseCompanyRepGain:4e3,CodingContractBaseMoneyGain:75e6,TotalNumBitNodes:24,LatestUpdate:"\n v0.52.9 - 2021-08-27 Less lag! (hydroflame & community)\n -------------------------------------------\n\n ** Active Scripts page **\n\n * Now less laggy, has pagination.\n\n ** File diagnostic ** \n\n * Added a popup found under options that shows the files you own and how\n large they are. This help find bugs and leftover massive logs files.\n\n ** Corporation **\n\n * Added safeguard against a very specific bug that causes NaN money. I'm\n still not sure what the root cause is but it should prevent corp from\n breaking.\n\n ** Netscript ** \n\n * tprintf is a new function that doesn't print the filename.\n\n ** Misc. **\n\n * Infiltration kills you if you try to automate it. (@threehams)\n * Fix beautify button not working\n * Added bladeburner_analysis_mult to getPlayer() (@brusby)\n * Fixed joining bladeburner via netscript functions. (@omuretsu)\n * All bladeburner actions are click-to-copy\n * nerf noodle bar\n"}},function(e,t,n){"use strict";var a=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0}),t.dialogBoxCreate=t.MessagePopup=void 0;const r=n(79),i=n(36),o=a(n(0));function s(e){return o.default.createElement(o.default.Fragment,null,e.content)}t.MessagePopup=s,t.dialogBoxCreate=function(e,t=!1){const n="popup-"+Array.from(Array(16)).map(()=>`${i.getRandomInt(0,9)}`).join("");"string"==typeof e?t?r.createPopup(n,s,{content:o.default.createElement("pre",{dangerouslySetInnerHTML:{__html:e}})}):r.createPopup(n,s,{content:o.default.createElement("p",{dangerouslySetInnerHTML:{__html:e.replace(/(?:\r\n|\r|\n)/g,"
")}})}):r.createPopup(n,s,{content:e})}},function(e,t,n){"use strict";function a(e,t={}){const n=document.createElement(e);return void 0!==t.id&&(n.id=t.id),void 0!==t.class&&(n.className=t.class),void 0!==t.innerHTML&&(n.innerHTML=t.innerHTML),void 0!==t.innerText&&(n.innerText=t.innerText),void 0!==t.tabIndex&&(n.tabIndex=t.tabIndex),function(e,t){void 0!==t.text&&(e.text=t.text),void 0!==t.href&&(e.href=t.href),void 0!==t.target&&(e.target=t.target)}(n,t),function(e,t){void 0!==t.name&&(e.name=t.name),void 0!==t.value&&(e.value=t.value),void 0!==t.type&&(e.type=t.type),void 0!==t.checked&&(e.checked=t.checked),void 0!==t.pattern&&(e.pattern=t.pattern),void 0!==t.maxLength&&(e.maxLength=t.maxLength),void 0!==t.placeholder&&(e.placeholder=t.placeholder),void 0!==t.max&&(e.max=t.max),void 0!==t.min&&(e.min=t.min),void 0!==t.step&&(e.step=t.step)}(n,t),function(e,t){void 0!==t.for&&(e.htmlFor=t.for)}(n,t),function(e,t){void 0!==t.clickListener&&e.addEventListener("click",t.clickListener),void 0!==t.inputListener&&e.addEventListener("input",t.inputListener),void 0!==t.changeListener&&e.addEventListener("change",t.changeListener),void 0!==t.onkeyup&&e.addEventListener("keyup",t.onkeyup),void 0!==t.onkeydown&&e.addEventListener("keydown",t.onkeydown),void 0!==t.onfocus&&e.addEventListener("focus",t.onfocus)}(n,t),function(e,t){void 0!==t.display&&(e.style.display=t.display),void 0!==t.visibility&&(e.style.visibility=t.visibility),void 0!==t.margin&&(e.style.margin=t.margin),void 0!==t.marginLeft&&(e.style.marginLeft=t.marginLeft),void 0!==t.marginTop&&(e.style.marginTop=t.marginTop),void 0!==t.padding&&(e.style.padding=t.padding),void 0!==t.color&&(e.style.color=t.color),void 0!==t.border&&(e.style.border=t.border),void 0!==t.float&&(e.style.cssFloat=t.float),void 0!==t.fontSize&&(e.style.fontSize=t.fontSize),void 0!==t.whiteSpace&&(e.style.whiteSpace=t.whiteSpace),void 0!==t.width&&(e.style.width=t.width),void 0!==t.height&&(e.style.height=t.height),void 0!==t.top&&(e.style.top=t.top),void 0!==t.left&&(e.style.left=t.left),void 0!==t.backgroundColor&&(e.style.backgroundColor=t.backgroundColor),void 0!==t.position&&(e.style.position=t.position),void 0!==t.overflow&&(e.style.overflow=t.overflow)}(n,t),function(e,t){void 0!==t.tooltip&&""!==t.tooltip?(e.className+=" tooltip",e.appendChild(a("span",{class:"tooltiptext",innerHTML:t.tooltip}))):void 0!==t.tooltipleft?(e.className+=" tooltip",e.appendChild(a("span",{class:"tooltiptextleft",innerHTML:t.tooltipleft}))):void 0!==t.tooltipsmall?(e.className+=" tooltip",e.appendChild(a("span",{class:"tooltiptext smallfont",innerHTML:t.tooltipsmall}))):void 0!==t.tooltiplow&&(e.className+="tooltip",e.appendChild(a("span",{class:"tooltiptextlow",innerHTML:t.tooltiplow})))}(n,t),n}Object.defineProperty(t,"__esModule",{value:!0}),t.createElement=void 0,t.createElement=a},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.Augmentations=void 0,t.Augmentations={}},,function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.resetFaction=t.initFactions=t.factionExists=t.AddToFactions=t.loadFactions=t.Factions=void 0;const a=n(131),r=n(757),i=n(28);function o(e){const n=e.name;t.Factions[n]=e}function s(e){return t.Factions.hasOwnProperty(e)}function l(e){if(!(e instanceof a.Faction))throw new Error("Invalid argument 'newFactionObject' passed into resetFaction()");const n=e.name;s(n)&&(e.favor=t.Factions[n].favor,delete t.Factions[n]),o(e)}t.Factions={},t.loadFactions=function(e){t.Factions=JSON.parse(e,i.Reviver)},t.AddToFactions=o,t.factionExists=s,t.initFactions=function(){for(const e in r.FactionInfos)l(new a.Faction(e))},t.resetFaction=l},function(e,t,n){"use strict";n.r(t),function(e){n.d(t,"Engine",function(){return _e});var a=n(20),r=n(14),i=n(87),o=n(4),s=n(134),l=n(100),c=n(827),u=n(470),m=n(184),p=n(35),d=n(41),h=n(11),_=n(558),g=n(16),f=n(80),y=n(826),b=n(825),E=n(824),v=n(823),C=n(114),k=n(46),P=n(29),S=n(50),O=n(47),M=n(479),T=n(95),x=n(89),w=n(116),A=n(88),R=(n(24),n(1)),N=n(180),D=n(322),B=n(108),I=n(118),L=n(775),W=n(23),F=n(27),j=n(43),U=n(45),H=n(52),G=n(762),K=n(82),q=n(186),$=n(475),Y=n(552),z=n(149),V=n(761),J=n(19),X=n(760),Q=n(25),Z=n(133),ee=n(67),te=n(551),ne=n(759),ae=n(34),re=n(758),ie=n(79),oe=n(12),se=n(321),le=n(97),ce=n(550),ue=n(44),me=(n(942),n(941),n(0)),pe=n.n(me),de=n(22),he=n.n(de);e(document).keydown(function(e){if(!0!==F.Settings.DisableHotkeys){if(!(R.Player.isWorking||B.redPillFlag||x.c||u.a))if(e.keyCode==ue.KEY.T&&e.altKey)e.preventDefault(),_e.loadTerminalContent();else if(e.keyCode===ue.KEY.C&&e.altKey)e.preventDefault(),_e.loadCharacterContent();else if(e.keyCode===ue.KEY.E&&e.altKey)e.preventDefault(),_e.loadScriptEditorContent();else if(e.keyCode===ue.KEY.S&&e.altKey)e.preventDefault(),_e.loadActiveScriptsContent();else if(e.keyCode===ue.KEY.H&&e.altKey)e.preventDefault(),_e.loadHacknetNodesContent();else if(e.keyCode===ue.KEY.W&&e.altKey)e.preventDefault(),_e.loadLocationContent();else if(e.keyCode===ue.KEY.J&&e.altKey)e.preventDefault(),_e.loadJobContent();else if(e.keyCode===ue.KEY.R&&e.altKey)e.preventDefault(),_e.loadTravelContent();else if(e.keyCode===ue.KEY.P&&e.altKey)e.preventDefault(),_e.loadCreateProgramContent();else if(e.keyCode===ue.KEY.F&&e.altKey){if(J.routing.isOn(J.Page.Terminal)&&k.FconfSettings.ENABLE_BASH_HOTKEYS)return;e.preventDefault(),_e.loadFactionsContent()}else e.keyCode===ue.KEY.A&&e.altKey?(e.preventDefault(),_e.loadAugmentationsContent()):e.keyCode===ue.KEY.U&&e.altKey?(e.preventDefault(),_e.loadTutorialContent()):e.keyCode===ue.KEY.B&&e.altKey?(e.preventDefault(),_e.loadBladeburnerContent()):e.keyCode===ue.KEY.G&&e.altKey&&(e.preventDefault(),_e.loadGangContent());e.keyCode===ue.KEY.O&&e.altKey&&(e.preventDefault(),Object(se.b)())}});const _e={Clickables:{saveMainMenuButton:null,deleteMainMenuButton:null},Display:{terminalContent:null,characterContent:null,scriptEditorContent:null,activeScriptsContent:null,hacknetNodesContent:null,createProgramContent:null,factionsContent:null,factionContent:null,augmentationsContent:null,milestonesContent:null,tutorialContent:null,infiltrationContent:null,stockMarketContent:null,gangContent:null,bladeburnerContent:null,locationContent:null,workInProgressContent:null,redPillContent:null,cinematicTextContent:null,missionContent:null,characterInfo:null},indexedDb:void 0,_lastUpdate:(new Date).getTime(),_idleSpeed:200,loadTerminalContent:function(){_e.hideAllContent(),_e.Display.terminalContent.style.display="block",J.routing.navigateTo(J.Page.Terminal),ae.MainMenuLinks.Terminal.classList.add("active")},loadCharacterContent:function(){_e.hideAllContent(),_e.Display.characterContent.style.display="block",_e.updateCharacterInfo(),J.routing.navigateTo(J.Page.CharacterInfo),ae.MainMenuLinks.Stats.classList.add("active")},loadScriptEditorContent:function(e="",t=""){_e.hideAllContent(),_e.Display.scriptEditorContent.style.display="block",J.routing.navigateTo(J.Page.ScriptEditor),he.a.render(pe.a.createElement(L.Root,{filename:e,code:t,player:R.Player,engine:this}),_e.Display.scriptEditorContent),ae.MainMenuLinks.ScriptEditor.classList.add("active")},loadActiveScriptsContent:function(){_e.hideAllContent(),_e.Display.activeScriptsContent.style.display="block",J.routing.navigateTo(J.Page.ActiveScripts),he.a.render(pe.a.createElement(te.ActiveScriptsRoot,{p:R.Player,workerScripts:w.workerScripts}),_e.Display.activeScriptsContent),ae.MainMenuLinks.ActiveScripts.classList.add("active")},loadHacknetNodesContent:function(){_e.hideAllContent(),_e.Display.hacknetNodesContent.style.display="block",J.routing.navigateTo(J.Page.HacknetNodes),Object(P.q)(),ae.MainMenuLinks.HacknetNodes.classList.add("active")},loadCreateProgramContent:function(){_e.hideAllContent(),_e.Display.createProgramContent.style.display="block",Object(D.a)(),J.routing.navigateTo(J.Page.CreateProgram),ae.MainMenuLinks.CreateProgram.classList.add("active")},loadFactionsContent:function(){_e.hideAllContent(),_e.Display.factionsContent.style.display="block",J.routing.navigateTo(J.Page.Factions),he.a.render(pe.a.createElement(y.FactionList,{player:R.Player,engine:this}),_e.Display.factionsContent),ae.MainMenuLinks.Factions.classList.add("active")},loadFactionContent:function(){_e.hideAllContent(),_e.Display.factionContent.style.display="block",J.routing.navigateTo(J.Page.Faction)},loadAugmentationsContent:function(){_e.hideAllContent(),_e.Display.augmentationsContent.style.display="block",J.routing.navigateTo(J.Page.Augmentations),Object(i.c)(_e.Display.augmentationsContent),ae.MainMenuLinks.Augmentations.classList.add("active")},loadMilestonesContent:function(){_e.hideAllContent(),_e.Display.milestonesContent.style.display="block",J.routing.navigateTo(J.Page.Milestones),Object(G.displayMilestonesContent)(),ae.MainMenuLinks.Milestones.classList.add("active")},loadTutorialContent:function(){_e.hideAllContent(),_e.Display.tutorialContent.style.display="block",J.routing.navigateTo(J.Page.Tutorial),ae.MainMenuLinks.Tutorial.classList.add("active")},loadDevMenuContent:function(){_e.hideAllContent(),Object(_.b)(),J.routing.navigateTo(J.Page.DevMenu),ae.MainMenuLinks.DevMenu.classList.add("active")},loadLocationContent:function(e=!0){_e.hideAllContent(),_e.Display.locationContent.style.display="block",ae.MainMenuLinks.City.classList.add("active"),J.routing.navigateTo(J.Page.Location);const t=pe.a.createElement(M.LocationRoot,{initiallyInCity:e,engine:_e,p:R.Player});he.a.render(t,_e.Display.locationContent)},loadTravelContent:function(){_e.hideAllContent(),R.Player.gotoLocation(O.LocationName.TravelAgency),_e.Display.locationContent.style.display="block",ae.MainMenuLinks.Travel.classList.add("active"),J.routing.navigateTo(J.Page.Location);const e=pe.a.createElement(M.LocationRoot,{initiallyInCity:!1,engine:_e,p:R.Player});he.a.render(e,_e.Display.locationContent)},loadJobContent:function(){if(""==R.Player.companyName)return void Object(oe.dialogBoxCreate)("You do not currently have a job! You can visit various companies in the city and try to find a job.");_e.hideAllContent(),R.Player.gotoLocation(R.Player.companyName),_e.Display.locationContent.style.display="block",ae.MainMenuLinks.Job.classList.add("active"),J.routing.navigateTo(J.Page.Location);const e=pe.a.createElement(M.LocationRoot,{initiallyInCity:!1,engine:_e,p:R.Player});he.a.render(e,_e.Display.locationContent)},loadWorkInProgressContent:function(){_e.hideAllContent(),document.getElementById("mainmenu-container").style.visibility="hidden",_e.Display.workInProgressContent.style.display="block",J.routing.navigateTo(J.Page.WorkInProgress)},loadRedPillContent:function(){_e.hideAllContent(),document.getElementById("mainmenu-container").style.visibility="hidden",_e.Display.redPillContent.style.display="block",J.routing.navigateTo(J.Page.RedPill)},loadCinematicTextContent:function(){_e.hideAllContent(),document.getElementById("mainmenu-container").style.visibility="hidden",_e.Display.cinematicTextContent.style.display="block",J.routing.navigateTo(J.Page.CinematicText)},loadInfiltrationContent:function(e,t,n){_e.hideAllContent(),document.getElementById("mainmenu-container").style.visibility="hidden",_e.Display.infiltrationContent.style.display="block",J.routing.navigateTo(J.Page.Infiltration),Object(v.displayInfiltrationContent)(this,R.Player,e,t,n)},loadStockMarketContent:function(){_e.hideAllContent(),_e.Display.stockMarketContent.style.display="block",J.routing.navigateTo(J.Page.StockMarket),Object(H.displayStockMarketContent)()},loadGangContent:function(){_e.hideAllContent(),R.Player.inGang()?(_e.Display.gangContent.style.display="block",J.routing.navigateTo(J.Page.Gang),Object(b.displayGangContent)(this,R.Player.gang,R.Player)):(_e.loadTerminalContent(),J.routing.navigateTo(J.Page.Terminal))},loadMissionContent:function(){_e.hideAllContent(),document.getElementById("mainmenu-container").style.visibility="hidden",document.getElementById("character-overview-wrapper").style.visibility="hidden",_e.Display.missionContent.style.display="block",J.routing.navigateTo(J.Page.Mission)},loadCorporationContent:function(){R.Player.corporation instanceof d.c&&(_e.hideAllContent(),J.routing.navigateTo(J.Page.Corporation),R.Player.corporation.createUI())},loadBladeburnerContent:function(){R.Player.bladeburner instanceof l.Bladeburner&&(_e.hideAllContent(),J.routing.navigateTo(J.Page.Bladeburner),_e.Display.bladeburnerContent.style.display="block",he.a.render(pe.a.createElement(E.Root,{bladeburner:R.Player.bladeburner,player:R.Player,engine:this}),_e.Display.bladeburnerContent),ae.MainMenuLinks.Bladeburner.classList.add("active"))},loadSleevesContent:function(){try{_e.hideAllContent(),J.routing.navigateTo(J.Page.Sleeves),Object($.createSleevesPage)(R.Player)}catch(e){Object(le.exceptionAlert)(e)}},loadResleevingContent:function(){try{_e.hideAllContent(),J.routing.navigateTo(J.Page.Resleeves),Object(Y.createResleevesPage)(R.Player)}catch(e){Object(le.exceptionAlert)(e)}},hideAllContent:function(){_e.Display.terminalContent.style.display="none",_e.Display.characterContent.style.display="none",_e.Display.scriptEditorContent.style.display="none",he.a.unmountComponentAtNode(_e.Display.scriptEditorContent),_e.Display.activeScriptsContent.style.display="none",he.a.unmountComponentAtNode(_e.Display.activeScriptsContent),_e.Display.infiltrationContent.style.display="none",he.a.unmountComponentAtNode(_e.Display.infiltrationContent),Object(P.a)(),_e.Display.createProgramContent.style.display="none",_e.Display.factionsContent.style.display="none",he.a.unmountComponentAtNode(_e.Display.factionsContent),_e.Display.factionContent.style.display="none",he.a.unmountComponentAtNode(_e.Display.factionContent),_e.Display.augmentationsContent.style.display="none",he.a.unmountComponentAtNode(_e.Display.augmentationsContent),_e.Display.milestonesContent.style.display="none",_e.Display.tutorialContent.style.display="none",_e.Display.locationContent.style.display="none",he.a.unmountComponentAtNode(_e.Display.locationContent),_e.Display.gangContent.style.display="none",he.a.unmountComponentAtNode(_e.Display.gangContent),_e.Display.bladeburnerContent.style.display="none",he.a.unmountComponentAtNode(_e.Display.bladeburnerContent),_e.Display.workInProgressContent.style.display="none",_e.Display.redPillContent.style.display="none",_e.Display.cinematicTextContent.style.display="none",_e.Display.stockMarketContent.style.display="none",_e.Display.missionContent.style.display="none",document.getElementById("gang-container")&&(document.getElementById("gang-container").style.display="none"),R.Player.corporation instanceof d.c&&R.Player.corporation.clearUI(),Object(Y.clearResleevesPage)(),Object($.clearSleevesPage)(),_e.inactivateMainMenuLinks(),Object(_.a)()},inactivateMainMenuLinks:function(){ae.MainMenuLinks.Terminal.classList.remove("active"),ae.MainMenuLinks.ScriptEditor.classList.remove("active"),ae.MainMenuLinks.ActiveScripts.classList.remove("active"),ae.MainMenuLinks.CreateProgram.classList.remove("active"),ae.MainMenuLinks.Stats.classList.remove("active"),ae.MainMenuLinks.Factions.classList.remove("active"),ae.MainMenuLinks.Augmentations.classList.remove("active"),ae.MainMenuLinks.HacknetNodes.classList.remove("active"),ae.MainMenuLinks.Sleeves.classList.remove("active"),ae.MainMenuLinks.City.classList.remove("active"),ae.MainMenuLinks.Travel.classList.remove("active"),ae.MainMenuLinks.Job.classList.remove("active"),ae.MainMenuLinks.StockMarket.classList.remove("active"),ae.MainMenuLinks.Gang.classList.remove("active"),ae.MainMenuLinks.Bladeburner.classList.remove("active"),ae.MainMenuLinks.Corporation.classList.remove("active"),ae.MainMenuLinks.Gang.classList.remove("active"),ae.MainMenuLinks.Milestones.classList.remove("active"),ae.MainMenuLinks.Tutorial.classList.remove("active"),ae.MainMenuLinks.Options.classList.remove("active"),ae.MainMenuLinks.DevMenu.classList.remove("active")},displayCharacterOverviewInfo:function(){he.a.render(pe.a.createElement(c.a,null),document.getElementById("character-overview-text"));const e=document.getElementById("character-overview-save-button");F.Settings.AutosaveInterval?e.classList.remove("flashing-button"):e.classList.add("flashing-button")},updateCharacterInfo:function(){he.a.render(Object(V.CharacterInfo)(R.Player),_e.Display.characterInfo)},idleTimer:function(){const e=(new Date).getTime();let t=e-_e._lastUpdate;const n=t%_e._idleSpeed;(t=Math.floor(t/_e._idleSpeed))>0&&(_e._lastUpdate=e-n,R.Player.lastUpdate=e-n,_e.updateGame(t)),window.requestAnimationFrame(_e.idleTimer)},updateGame:function(e=1){const t=e*_e._idleSpeed;null==R.Player.totalPlaytime&&(R.Player.totalPlaytime=0),null==R.Player.playtimeSinceLastAug&&(R.Player.playtimeSinceLastAug=0),null==R.Player.playtimeSinceLastBitnode&&(R.Player.playtimeSinceLastBitnode=0),R.Player.totalPlaytime+=t,R.Player.playtimeSinceLastAug+=t,R.Player.playtimeSinceLastBitnode+=t,!0===K.a.actionStarted&&(_e._totalActionTime=K.a.actionTime,_e._actionTimeLeft=K.a.actionTime,_e._actionInProgress=!0,_e._actionProgressBarCount=1,_e._actionProgressStr="[ ]",_e._actionTimeStr="Time left: ",K.a.actionStarted=!1),R.Player.isWorking&&(R.Player.workType==h.CONSTANTS.WorkTypeFaction?R.Player.workForFaction(e):R.Player.workType==h.CONSTANTS.WorkTypeCreateProgram?R.Player.createProgramWork(e):R.Player.workType==h.CONSTANTS.WorkTypeStudyClass?R.Player.takeClass(e):R.Player.workType==h.CONSTANTS.WorkTypeCrime?R.Player.commitCrime(e):R.Player.workType==h.CONSTANTS.WorkTypeCompanyPartTime?R.Player.workPartTime(e):R.Player.work(e)),R.Player.hasWseAccount&&Object(H.processStockPrices)(e),R.Player.inGang()&&R.Player.gang.process(e,R.Player),x.c&&x.b&&x.b.process(e),R.Player.corporation instanceof d.c&&R.Player.corporation.storeCycles(e),R.Player.bladeburner instanceof l.Bladeburner&&R.Player.bladeburner.storeCycles(e);for(let t=0;t0?(t.innerHTML=e,t.setAttribute("class","notification-on")):(t.innerHTML="",t.setAttribute("class","notification-off")),_e.Counters.createProgramNotifications=10}if(_e.Counters.augmentationsNotifications<=0){e=R.Player.queuedAugmentations.length,t=document.getElementById("augmentations-notification");e>0?(t.innerHTML=e,t.setAttribute("class","notification-on")):(t.innerHTML="",t.setAttribute("class","notification-off")),_e.Counters.augmentationsNotifications=10}if(_e.Counters.checkFactionInvitations<=0){var n=R.Player.checkForFactionInvitations();if(n.length>0){!1===R.Player.firstFacInvRecvd&&(R.Player.firstFacInvRecvd=!0,document.getElementById("factions-tab").style.display="list-item",document.getElementById("character-menu-header").click(),document.getElementById("character-menu-header").click());var a=n[Math.floor(Math.random()*n.length)];Object(f.inviteToFaction)(a)}const e=R.Player.factionInvitations.length,t=document.getElementById("factions-notification");e>0?(t.innerHTML=e,t.setAttribute("class","notification-on")):(t.innerHTML="",t.setAttribute("class","notification-off")),_e.Counters.checkFactionInvitations=100}if(_e.Counters.passiveFactionGrowth<=0){var i=Math.floor(5-_e.Counters.passiveFactionGrowth);Object(f.processPassiveFactionRepGain)(i),_e.Counters.passiveFactionGrowth=5}if(_e.Counters.messages<=0&&(Object(T.b)(),r.Augmentations[o.AugmentationNames.TheRedPill].owned?_e.Counters.messages=4500:_e.Counters.messages=150),_e.Counters.mechanicProcess<=0){if(R.Player.corporation instanceof d.c&&R.Player.corporation.process(),R.Player.bladeburner instanceof l.Bladeburner)try{R.Player.bladeburner.process(R.Player)}catch(e){Object(le.exceptionAlert)("Exception caught in Bladeburner.process(): "+e)}_e.Counters.mechanicProcess=5}_e.Counters.contractGeneration<=0&&(Math.random()<=.25&&Object(m.generateRandomContract)(),_e.Counters.contractGeneration=3e3)},_totalActionTime:0,_actionTimeLeft:0,_actionTimeStr:"Time left: ",_actionProgressStr:"[ ]",_actionProgressBarCount:1,_actionInProgress:!1,updateHackProgress:function(e=1){var t=e*_e._idleSpeed;_e._actionTimeLeft-=t/1e3,_e._actionTimeLeft=Math.max(_e._actionTimeLeft,0);for(var n=Math.round(100*(1-_e._actionTimeLeft/_e._totalActionTime));2*_e._actionProgressBarCount<=n;)_e._actionProgressStr=Object(a.replaceAt)(_e._actionProgressStr,_e._actionProgressBarCount,"|"),_e._actionProgressBarCount+=1;_e._actionTimeStr="Time left: "+Math.max(0,Math.round(_e._actionTimeLeft)).toString()+"s",document.getElementById("hack-progress").innerHTML=_e._actionTimeStr,document.getElementById("hack-progress-bar").innerHTML=_e._actionProgressStr.replace(/ /g," "),n>=100&&(_e._actionInProgress=!1,K.a.finishAction())},closeMainMenuHeader:function(e){for(var t=0;te===1.0777-1?"7.77%":e===1.777-1?"77.7%":c.numeralWrapper.formatPercentage(e,t);let r=o.createElement(o.Fragment,null,"Effects:");return e.hacking_mult&&e.hacking_mult==e.strength_mult&&e.hacking_mult==e.defense_mult&&e.hacking_mult==e.dexterity_mult&&e.hacking_mult==e.agility_mult&&e.hacking_mult==e.charisma_mult?r=o.createElement(o.Fragment,null,r,o.createElement("br",null),"+",a(e.hacking_mult-1)," all skills"):(e.hacking_mult&&(r=o.createElement(o.Fragment,null,r,o.createElement("br",null),"+",a(e.hacking_mult-1)," hacking skill")),e.strength_mult&&e.strength_mult==e.defense_mult&&e.strength_mult==e.dexterity_mult&&e.strength_mult==e.agility_mult?r=o.createElement(o.Fragment,null,r,o.createElement("br",null),"+",a(e.strength_mult-1)," combat skills"):(e.strength_mult&&(r=o.createElement(o.Fragment,null,r,o.createElement("br",null),"+",a(e.strength_mult-1)," strength skill")),e.defense_mult&&(r=o.createElement(o.Fragment,null,r,o.createElement("br",null),"+",a(e.defense_mult-1)," defense skill")),e.dexterity_mult&&(r=o.createElement(o.Fragment,null,r,o.createElement("br",null),"+",a(e.dexterity_mult-1)," dexterity skill")),e.agility_mult&&(r=o.createElement(o.Fragment,null,r,o.createElement("br",null),"+",a(e.agility_mult-1)," agility skill"))),e.charisma_mult&&(r=o.createElement(o.Fragment,null,r,o.createElement("br",null),"+",a(e.charisma_mult-1)," Charisma skill"))),e.hacking_exp_mult&&e.hacking_exp_mult===e.strength_exp_mult&&e.hacking_exp_mult===e.defense_exp_mult&&e.hacking_exp_mult===e.dexterity_exp_mult&&e.hacking_exp_mult===e.agility_exp_mult&&e.hacking_exp_mult===e.charisma_exp_mult?r=o.createElement(o.Fragment,null,r,o.createElement("br",null),"+",a(e.hacking_exp_mult-1)," exp for all skills"):(e.hacking_exp_mult&&(r=o.createElement(o.Fragment,null,r,o.createElement("br",null),"+",a(e.hacking_exp_mult-1)," hacking exp")),e.strength_exp_mult&&e.strength_exp_mult===e.defense_exp_mult&&e.strength_exp_mult===e.dexterity_exp_mult&&e.strength_exp_mult===e.agility_exp_mult?r=o.createElement(o.Fragment,null,r,o.createElement("br",null),"+",a(e.strength_exp_mult-1)," combat exp"):(e.strength_exp_mult&&(r=o.createElement(o.Fragment,null,r,o.createElement("br",null),"+",a(e.strength_exp_mult-1)," strength exp")),e.defense_exp_mult&&(r=o.createElement(o.Fragment,null,r,o.createElement("br",null),"+",a(e.defense_exp_mult-1)," defense exp")),e.dexterity_exp_mult&&(r=o.createElement(o.Fragment,null,r,o.createElement("br",null),"+",a(e.dexterity_exp_mult-1)," dexterity exp")),e.agility_exp_mult&&(r=o.createElement(o.Fragment,null,r,o.createElement("br",null),"+",a(e.agility_exp_mult-1)," agility exp"))),e.charisma_exp_mult&&(r=o.createElement(o.Fragment,null,r,o.createElement("br",null),"+",a(e.charisma_exp_mult-1)," charisma exp"))),e.hacking_speed_mult&&(r=o.createElement(o.Fragment,null,r,o.createElement("br",null),"+",a(e.hacking_speed_mult-1)," faster hacking")),e.hacking_chance_mult&&(r=o.createElement(o.Fragment,null,r,o.createElement("br",null),"+",a(e.hacking_chance_mult-1)," hack() success chance")),e.hacking_money_mult&&(r=o.createElement(o.Fragment,null,r,o.createElement("br",null),"+",a(e.hacking_money_mult-1)," hack() power")),e.hacking_grow_mult&&(r=o.createElement(o.Fragment,null,r,o.createElement("br",null),"+",a(e.hacking_grow_mult-1)," grow() power")),e.faction_rep_mult&&e.faction_rep_mult===e.company_rep_mult?r=o.createElement(o.Fragment,null,r,o.createElement("br",null),"+",a(e.faction_rep_mult-1)," reputation from factions and companies"):(e.faction_rep_mult&&(r=o.createElement(o.Fragment,null,r,o.createElement("br",null),"+",a(e.faction_rep_mult-1)," reputation from factions")),e.company_rep_mult&&(r=o.createElement(o.Fragment,null,r,o.createElement("br",null),"+",a(e.company_rep_mult-1)," reputation from companies"))),e.crime_money_mult&&(r=o.createElement(o.Fragment,null,r,o.createElement("br",null),"+",a(e.crime_money_mult-1)," crime money")),e.crime_success_mult&&(r=o.createElement(o.Fragment,null,r,o.createElement("br",null),"+",a(e.crime_success_mult-1)," crime success rate")),e.work_money_mult&&(r=o.createElement(o.Fragment,null,r,o.createElement("br",null),"+",a(e.work_money_mult-1)," work money")),e.hacknet_node_money_mult&&(r=o.createElement(o.Fragment,null,r,o.createElement("br",null),"+",a(e.hacknet_node_money_mult-1)," hacknet production")),e.hacknet_node_purchase_cost_mult&&(r=o.createElement(o.Fragment,null,r,o.createElement("br",null),"-",a(-(e.hacknet_node_purchase_cost_mult-1))," hacknet nodes cost")),e.hacknet_node_level_cost_mult&&(r=o.createElement(o.Fragment,null,r,o.createElement("br",null),"-",a(-(e.hacknet_node_level_cost_mult-1))," hacknet nodes upgrade cost")),e.bladeburner_max_stamina_mult&&(r=o.createElement(o.Fragment,null,r,o.createElement("br",null),"+",a(e.bladeburner_max_stamina_mult-1)," Bladeburner Max Stamina")),e.bladeburner_stamina_gain_mult&&(r=o.createElement(o.Fragment,null,r,o.createElement("br",null),"+",a(e.bladeburner_stamina_gain_mult-1)," Bladeburner Stamina gain")),e.bladeburner_analysis_mult&&(r=o.createElement(o.Fragment,null,r,o.createElement("br",null),"+",a(e.bladeburner_analysis_mult-1)," Bladeburner Field Analysis effectiveness")),e.bladeburner_success_chance_mult&&(r=o.createElement(o.Fragment,null,r,o.createElement("br",null),"+",a(e.bladeburner_success_chance_mult-1)," Bladeburner Contracts and Operations success chance")),n&&(r=o.createElement(o.Fragment,null,r,o.createElement("br",null),"Start with ",o.createElement(u.Money,{money:n})," after installing Augmentations.")),t&&(r=o.createElement(o.Fragment,null,r,o.createElement("br",null),"Start with ",t.join(" and ")," after installing Augmentations.")),r}(this.mults,e.programs,e.startingMoney)}addToFactions(e){for(let t=0;t{let t=`${e%1e3}`;for(;t.length<3;)t="0"+t;return t})(),u=t?`${l}.${c}`:`${l}`;let m="";return a>0&&(m+=`${a} days `),i>0&&(m+=`${i} hours `),s>0&&(m+=`${s} minutes `),m+=`${u} seconds`},t.longestCommonStart=function(e){if(!r(e))return"";if(0===e.length)return"";const t=e.concat().sort(),n=t[0],a=t[t.length-1],i=n.length;let o=0;const s=(e,t)=>e.toUpperCase()===t.toUpperCase();for(;o=0;e--)if(1===n[e].nodeType)return!0;return!1},t.generateRandomString=function(e){let t="";const n="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";for(let a=0;a{switch(typeof e){case"number":return e;case"object":return s.getRandomInt(e.min,e.max);default:throw Error(`Do not know how to convert the type '${typeof e}' to a number`)}};for(const e of i.serverMetadata){const i={hostname:e.hostname,ip:u(),numOpenPortsRequired:e.numOpenPortsRequired,organizationName:e.organizationName};void 0!==e.maxRamExponent&&(i.maxRam=Math.pow(2,o(e.maxRamExponent)));for(const t of n)void 0!==e[t]&&(i[t]=o(e[t]));const s=new a.Server(i);for(const t of e.literature||[])s.messages.push(t);void 0!==e.specialName&&r.SpecialServerIps.addIp(e.specialName,s.ip),m(s),void 0!==e.networkLayer&&t[o(e.networkLayer)-1].push(s)}const l=(e,t)=>{e.serversOnNetwork.push(t.ip),t.serversOnNetwork.push(e.ip)},c=e=>e[Math.floor(Math.random()*e.length)],p=(e,t)=>{for(const n of e)l(n,t())};p(t[0],()=>e);for(let e=1;ec(t[e-1]))},t.prestigeAllServers=function(){for(const e in t.AllServers)delete t.AllServers[e];t.AllServers={}},t.loadAllServers=function(e){t.AllServers=JSON.parse(e,l.Reviver)}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.isBackdoorInstalled=t.getServerOnNetwork=t.getServer=t.GetServerByHostname=t.prestigeHomeComputer=t.processSingleServerGrowth=t.numCycleForGrowth=t.safetlyCreateUniqueServer=void 0;const a=n(23),r=n(509),i=n(548),o=n(21),s=n(11),l=n(32),c=n(140),u=n(1143),m=n(704);function p(e,t,n){let a=1+(s.CONSTANTS.ServerBaseGrowthRate-1)/e.hackDifficulty;a>s.CONSTANTS.ServerMaxGrowthRate&&(a=s.CONSTANTS.ServerMaxGrowthRate);const r=e.serverGrowth/100;return Math.log(t)/(Math.log(a)*n.hacking_grow_mult*r*o.BitNodeMultipliers.ServerGrowthRate)}function d(e){for(const t in a.AllServers)if(a.AllServers.hasOwnProperty(t)&&a.AllServers[t].hostname==e)return a.AllServers[t];return null}t.safetlyCreateUniqueServer=function(e){if(null!=e.ip&&a.ipExists(e.ip)&&(e.ip=a.createUniqueRandomIp()),null!=d(e.hostname)){let t=e.hostname;for(let n=0;n<200&&null!=d(t=`${e.hostname}-${n}`);++n);e.hostname=t}return new r.Server(e)},t.numCycleForGrowth=p,t.processSingleServerGrowth=function(e,t,n,a=1){let r=i.calculateServerGrowth(e,t,n,a);r<1&&(console.warn("serverGrowth calculated to be less than 1"),r=1);const o=e.moneyAvailable;if(e.moneyAvailable*=r,u.isValidNumber(e.moneyMax)&&isNaN(e.moneyAvailable)&&(e.moneyAvailable=e.moneyMax),u.isValidNumber(e.moneyMax)&&e.moneyAvailable>e.moneyMax&&(e.moneyAvailable=e.moneyMax),o!==e.moneyAvailable){let t=p(e,e.moneyAvailable/o,n);t=Math.max(0,t),e.fortify(2*s.CONSTANTS.ServerFortifyAmount*Math.ceil(t))}return e.moneyAvailable/o},t.prestigeHomeComputer=function(e){const t=e.programs.includes(l.Programs.BitFlume.name);e.programs.length=0,e.runningScripts=[],e.serversOnNetwork=[],e.isConnectedTo=!0,e.ramUsed=0,e.programs.push(l.Programs.NukeProgram.name),t&&e.programs.push(l.Programs.BitFlume.name),e.scripts.forEach(function(t){t.updateRamUsage(e.scripts)}),e.messages.length=0,e.messages.push(c.LiteratureNames.HackersStartingHandbook)},t.GetServerByHostname=d,t.getServer=function(e){return m.isValidIPAddress(e)?void 0!==a.AllServers[e]?a.AllServers[e]:null:d(e)},t.getServerOnNetwork=function(e,t){return t>e.serversOnNetwork.length?(console.error("Tried to get server on network that was out of range"),null):a.AllServers[e.serversOnNetwork[t]]},t.isBackdoorInstalled=function(e){return"backdoorInstalled"in e&&e.backdoorInstalled}},function(e,t,n){"use strict";var a=this&&this.__createBinding||(Object.create?function(e,t,n,a){void 0===a&&(a=n),Object.defineProperty(e,a,{enumerable:!0,get:function(){return t[n]}})}:function(e,t,n,a){void 0===a&&(a=n),e[a]=t[n]}),r=this&&this.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:!0,value:t})}:function(e,t){e.default=t}),i=this&&this.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var n in e)"default"!==n&&Object.prototype.hasOwnProperty.call(e,n)&&a(t,e,n);return r(t,e),t};Object.defineProperty(t,"__esModule",{value:!0}),t.Money=void 0;const o=i(n(0)),s=n(3);t.Money=function(e){if(void 0!==e.player){if("number"!=typeof e.money)throw new Error("if player if provided, money should be number, contact dev");if(!e.player.canAfford(e.money))return o.createElement("span",{className:"unbuyable samefont"},s.numeralWrapper.formatMoney(e.money))}return o.createElement("span",{className:"money-gold samefont"},"number"==typeof e.money?s.numeralWrapper.formatMoney(e.money):e.money)}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.EmployeePositions=void 0,t.EmployeePositions={Operations:"Operations",Engineer:"Engineer",Business:"Business",Management:"Management",RandD:"Research & Development",Training:"Training",Unassigned:"Unassigned"}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.Settings=void 0;const a=n(233),r={AutosaveInterval:60,CodeInstructionRunTime:50,DisableASCIIArt:!1,DisableHotkeys:!1,DisableTextEffects:!1,Locale:"en",MaxLogCapacity:50,MaxPortCapacity:50,SuppressBuyAugmentationConfirmation:!1,SuppressFactionInvites:!1,SuppressHospitalizationPopup:!1,SuppressMessages:!1,SuppressTravelConfirmation:!1,SuppressBladeburnerPopup:!1};t.Settings={AutosaveInterval:r.AutosaveInterval,CodeInstructionRunTime:25,DisableASCIIArt:r.DisableASCIIArt,DisableHotkeys:r.DisableHotkeys,DisableTextEffects:r.DisableTextEffects,Locale:"en",MaxLogCapacity:r.MaxLogCapacity,MaxPortCapacity:r.MaxPortCapacity,OwnedAugmentationsOrder:a.OwnedAugmentationsOrderSetting.AcquirementTime,PurchaseAugmentationsOrder:a.PurchaseAugmentationsOrderSetting.Default,SuppressBuyAugmentationConfirmation:r.SuppressBuyAugmentationConfirmation,SuppressFactionInvites:r.SuppressFactionInvites,SuppressHospitalizationPopup:r.SuppressHospitalizationPopup,SuppressMessages:r.SuppressMessages,SuppressTravelConfirmation:r.SuppressTravelConfirmation,SuppressBladeburnerPopup:r.SuppressBladeburnerPopup,MonacoTheme:"vs-dark",MonacoInsertSpaces:!1,init(){Object.assign(t.Settings,r)},load(e){Object.assign(t.Settings,JSON.parse(e))}}},function(e,t,n){"use strict";function a(e,t){var n;if(null==t)return console.log("Reviver WRONGLY called with key: "+e+", and value: "+t),0;if("object"==typeof t&&"string"==typeof t.ctor&&void 0!==t.data){if("AllServersMap"===t.ctor)return console.log("Converting AllServersMap for v0.43.1"),t.data;if("function"==typeof(n=a.constructors[t.ctor]||window[t.ctor])&&"function"==typeof n.fromJSON)return n.fromJSON(t)}return t}function r(e,t,n){var a,r;n||(n=Object.keys(t)),a={};for(let e=0;e0}function P(){if(m.ITutorial.isRunning){if(m.ITutorial.currStep!==m.iTutorialSteps.HacknetNodesIntroduction)return;Object(m.iTutorialNextStep)()}const e=p.Player.hacknetNodes.length;if(k()){const t=M();if(isNaN(t))throw new Error("Calculated cost of purchasing HacknetServer is NaN");return p.Player.canAfford(t)?(p.Player.loseMoney(t),p.Player.createHacknetServer(),j(),e):-1}{const t=O();if(isNaN(t))throw new Error("Calculated cost of purchasing HacknetNode is NaN");if(!p.Player.canAfford(t))return-1;const n="hacknet-node-"+e,r=new a.HacknetNode(n,p.Player.hacknet_node_money_mult);return p.Player.loseMoney(t),p.Player.hacknetNodes.push(r),e}}function S(){return k()&&p.Player.hacknetNodes.length>=o.HacknetServerConstants.MaxServers}function O(){return Object(r.calculateNodeCost)(p.Player.hacknetNodes.length+1,p.Player.hacknet_node_purchase_cost_mult)}function M(){return Object(i.calculateServerCost)(p.Player.hacknetNodes.length+1,p.Player.hacknet_node_purchase_cost_mult)}function T(e,t){if(null==t)throw new Error("getMaxNumberLevelUpgrades() called without maxLevel arg");if(p.Player.money.lt(e.calculateLevelUpgradeCost(1,p.Player.hacknet_node_level_cost_mult)))return 0;let n=1,a=t-1,r=t-e.level;if(p.Player.money.gt(e.calculateLevelUpgradeCost(r,p.Player.hacknet_node_level_cost_mult)))return r;for(;n<=a;){var i=(n+a)/2|0;if(i!==t&&p.Player.money.gt(e.calculateLevelUpgradeCost(i,p.Player.hacknet_node_level_cost_mult))&&p.Player.money.lt(e.calculateLevelUpgradeCost(i+1,p.Player.hacknet_node_level_cost_mult)))return Math.min(r,i);if(p.Player.money.lt(e.calculateLevelUpgradeCost(i,p.Player.hacknet_node_level_cost_mult)))a=i-1;else{if(!p.Player.money.gt(e.calculateLevelUpgradeCost(i,p.Player.hacknet_node_level_cost_mult)))return Math.min(r,i);n=i+1}}return 0}function x(e,t){if(null==t)throw new Error("getMaxNumberRamUpgrades() called without maxLevel arg");if(p.Player.money.lt(e.calculateRamUpgradeCost(1,p.Player.hacknet_node_ram_cost_mult)))return 0;let n;if(n=e instanceof s.HacknetServer?Math.round(Math.log2(t/e.maxRam)):Math.round(Math.log2(t/e.ram)),p.Player.money.gt(e.calculateRamUpgradeCost(n,p.Player.hacknet_node_ram_cost_mult)))return n;for(let t=n-1;t>=0;--t)if(p.Player.money.gt(e.calculateRamUpgradeCost(t,p.Player.hacknet_node_ram_cost_mult)))return t;return 0}function w(e,t){if(null==t)throw new Error("getMaxNumberCoreUpgrades() called without maxLevel arg");if(p.Player.money.lt(e.calculateCoreUpgradeCost(1,p.Player.hacknet_node_core_cost_mult)))return 0;let n=1,a=t-1;const r=t-e.cores;if(p.Player.money.gt(e.calculateCoreUpgradeCost(r,p.Player.hacknet_node_core_cost_mult)))return r;for(;n<=a;){let i=(n+a)/2|0;if(i!=t&&p.Player.money.gt(e.calculateCoreUpgradeCost(i,p.Player.hacknet_node_core_cost_mult))&&p.Player.money.lt(e.calculateCoreUpgradeCost(i+1,p.Player.hacknet_node_core_cost_mult)))return Math.min(r,i);if(p.Player.money.lt(e.calculateCoreUpgradeCost(i,p.Player.hacknet_node_core_cost_mult)))a=i-1;else{if(!p.Player.money.gt(e.calculateCoreUpgradeCost(i,p.Player.hacknet_node_core_cost_mult)))return Math.min(r,i);n=i+1}}return 0}function A(e,t){if(null==t)throw new Error("getMaxNumberCacheUpgrades() called without maxLevel arg");if(!p.Player.canAfford(e.calculateCacheUpgradeCost(1)))return 0;let n=1,a=t-1;const r=t-e.cache;if(p.Player.canAfford(e.calculateCacheUpgradeCost(r)))return r;for(;n<=a;){let i=(n+a)/2|0;if(i!=t&&p.Player.canAfford(e.calculateCacheUpgradeCost(i))&&!p.Player.canAfford(e.calculateCacheUpgradeCost(i+1)))return Math.min(r,i);if(p.Player.canAfford(e.calculateCacheUpgradeCost(i))){if(!p.Player.canAfford(e.calculateCacheUpgradeCost(i)))return Math.min(r,i);n=i+1}else a=i-1}return 0}function R(e,t=1){const n=Math.round(t),a=e.calculateLevelUpgradeCost(n,p.Player.hacknet_node_level_cost_mult);if(isNaN(a)||a<=0||n<0)return!1;const r=e instanceof s.HacknetServer;if(e.level>=(r?o.HacknetServerConstants.MaxLevel:o.HacknetNodeConstants.MaxLevel))return!1;if(e.level+n>(r?o.HacknetServerConstants.MaxLevel:o.HacknetNodeConstants.MaxLevel)){return R(e,Math.max(0,(r?o.HacknetServerConstants.MaxLevel:o.HacknetNodeConstants.MaxLevel)-e.level))}return!!p.Player.canAfford(a)&&(p.Player.loseMoney(a),e.upgradeLevel(n,p.Player.hacknet_node_money_mult),!0)}function N(e,t=1){const n=Math.round(t),a=e.calculateRamUpgradeCost(n,p.Player.hacknet_node_ram_cost_mult);if(isNaN(a)||a<=0||n<0)return!1;const r=e instanceof s.HacknetServer;if(e.ram>=(r?o.HacknetServerConstants.MaxRam:o.HacknetNodeConstants.MaxRam))return!1;if(r){if(e.maxRam*Math.pow(2,n)>o.HacknetServerConstants.MaxRam){return N(e,Math.max(0,Math.log2(Math.round(o.HacknetServerConstants.MaxRam/e.maxRam))))}}else if(e.ram*Math.pow(2,n)>o.HacknetNodeConstants.MaxRam){return N(e,Math.max(0,Math.log2(Math.round(o.HacknetNodeConstants.MaxRam/e.ram))))}return!!p.Player.canAfford(a)&&(p.Player.loseMoney(a),e.upgradeRam(n,p.Player.hacknet_node_money_mult),!0)}function D(e,t=1){const n=Math.round(t),a=e.calculateCoreUpgradeCost(n,p.Player.hacknet_node_core_cost_mult);if(isNaN(a)||a<=0||n<0)return!1;const r=e instanceof s.HacknetServer;if(e.cores>=(r?o.HacknetServerConstants.MaxCores:o.HacknetNodeConstants.MaxCores))return!1;if(e.cores+n>(r?o.HacknetServerConstants.MaxCores:o.HacknetNodeConstants.MaxCores)){return D(e,Math.max(0,(r?o.HacknetServerConstants.MaxCores:o.HacknetNodeConstants.MaxCores)-e.cores))}return!!p.Player.canAfford(a)&&(p.Player.loseMoney(a),e.upgradeCore(n,p.Player.hacknet_node_money_mult),!0)}function B(e,t=1){const n=Math.round(t),a=e.calculateCacheUpgradeCost(n);if(isNaN(a)||a<=0||n<0)return!1;if(!(e instanceof s.HacknetServer))return console.warn("purchaseCacheUpgrade() called for a non-HacknetNode"),!1;if(e.cache+n>o.HacknetServerConstants.MaxCache){return B(e,Math.max(0,o.HacknetServerConstants.MaxCache-e.cache))}return!!p.Player.canAfford(a)&&(p.Player.loseMoney(a),e.upgradeCache(n),!0)}function I(){g.routing.isOn(g.Page.HacknetNodes)&&E.a.render(y.a.createElement(v.a,null),C)}function L(){C instanceof HTMLElement&&E.a.unmountComponentAtNode(C),C.style.display="none"}function W(e){return 0===p.Player.hacknetNodes.length?0:k()?function(e){if(!(p.Player.hashManager instanceof l.HashManager))throw new Error("Player does not have a HashManager (should be in 'hashManager' prop)");let t=0;for(let n=0;n
Starting cost: "+r.numeralWrapper.format(t.IndustryStartingCosts.Energy,"$0.000a")+"
Recommended starting Industry: NO",Utilities:"Distribute water and provide wastewater services.

Starting cost: "+r.numeralWrapper.format(t.IndustryStartingCosts.Utilities,"$0.000a")+"
Recommended starting Industry: NO",Agriculture:"Cultivate crops and breed livestock to produce food.

Starting cost: "+r.numeralWrapper.format(t.IndustryStartingCosts.Agriculture,"$0.000a")+"
Recommended starting Industry: YES",Fishing:"Produce food through the breeding and processing of fish and fish products.

Starting cost: "+r.numeralWrapper.format(t.IndustryStartingCosts.Fishing,"$0.000a")+"
Recommended starting Industry: NO",Mining:"Extract and process metals from the earth.

Starting cost: "+r.numeralWrapper.format(t.IndustryStartingCosts.Mining,"$0.000a")+"
Recommended starting Industry: NO",Food:"Create your own restaurants all around the world.

Starting cost: "+r.numeralWrapper.format(t.IndustryStartingCosts.Food,"$0.000a")+"
Recommended starting Industry: YES",Tobacco:"Create and distribute tobacco and tobacco-related products.

Starting cost: "+r.numeralWrapper.format(t.IndustryStartingCosts.Tobacco,"$0.000a")+"
Recommended starting Industry: YES",Chemical:"Produce industrial chemicals.

Starting cost: "+r.numeralWrapper.format(t.IndustryStartingCosts.Chemical,"$0.000a")+"
Recommended starting Industry: NO",Pharmaceutical:"Discover, develop, and create new pharmaceutical drugs.

Starting cost: "+r.numeralWrapper.format(t.IndustryStartingCosts.Pharmaceutical,"$0.000a")+"
Recommended starting Industry: NO",Computer:"Develop and manufacture new computer hardware and networking infrastructures.

Starting cost: "+r.numeralWrapper.format(t.IndustryStartingCosts.Computer,"$0.000a")+"
Recommended starting Industry: NO",Robotics:"Develop and create robots.

Starting cost: "+r.numeralWrapper.format(t.IndustryStartingCosts.Robotics,"$0.000a")+"
Recommended starting Industry: NO",Software:"Develop computer software and create AI Cores.

Starting cost: "+r.numeralWrapper.format(t.IndustryStartingCosts.Software,"$0.000a")+"
Recommended starting Industry: YES",Healthcare:"Create and manage hospitals.

Starting cost: "+r.numeralWrapper.format(t.IndustryStartingCosts.Healthcare,"$0.000a")+"
Recommended starting Industry: NO",RealEstate:"Develop and manage real estate properties.

Starting cost: "+r.numeralWrapper.format(t.IndustryStartingCosts.RealEstate,"$0.000a")+"
Recommended starting Industry: NO"},t.IndustryResearchTrees={Energy:a.getBaseResearchTreeCopy(),Utilities:a.getBaseResearchTreeCopy(),Agriculture:a.getBaseResearchTreeCopy(),Fishing:a.getBaseResearchTreeCopy(),Mining:a.getBaseResearchTreeCopy(),Food:a.getProductIndustryResearchTreeCopy(),Tobacco:a.getProductIndustryResearchTreeCopy(),Chemical:a.getBaseResearchTreeCopy(),Pharmaceutical:a.getProductIndustryResearchTreeCopy(),Computer:a.getProductIndustryResearchTreeCopy(),Robotics:a.getProductIndustryResearchTreeCopy(),Software:a.getProductIndustryResearchTreeCopy(),Healthcare:a.getProductIndustryResearchTreeCopy(),RealEstate:a.getProductIndustryResearchTreeCopy()},t.resetIndustryResearchTrees=function(){t.IndustryResearchTrees.Energy=a.getBaseResearchTreeCopy(),t.IndustryResearchTrees.Utilities=a.getBaseResearchTreeCopy(),t.IndustryResearchTrees.Agriculture=a.getBaseResearchTreeCopy(),t.IndustryResearchTrees.Fishing=a.getBaseResearchTreeCopy(),t.IndustryResearchTrees.Mining=a.getBaseResearchTreeCopy(),t.IndustryResearchTrees.Food=a.getBaseResearchTreeCopy(),t.IndustryResearchTrees.Tobacco=a.getBaseResearchTreeCopy(),t.IndustryResearchTrees.Chemical=a.getBaseResearchTreeCopy(),t.IndustryResearchTrees.Pharmaceutical=a.getBaseResearchTreeCopy(),t.IndustryResearchTrees.Computer=a.getBaseResearchTreeCopy(),t.IndustryResearchTrees.Robotics=a.getBaseResearchTreeCopy(),t.IndustryResearchTrees.Software=a.getBaseResearchTreeCopy(),t.IndustryResearchTrees.Healthcare=a.getBaseResearchTreeCopy(),t.IndustryResearchTrees.RealEstate=a.getBaseResearchTreeCopy()}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.initializeMainMenuLinks=t.MainMenuLinks=void 0;const a=n(55),r=(()=>{const e=document.createElement("div");if(null===e)throw new Error("unable to create empty div element");return e})();t.MainMenuLinks={Terminal:r,ScriptEditor:r,ActiveScripts:r,CreateProgram:r,Stats:r,Factions:r,Augmentations:r,HacknetNodes:r,Sleeves:r,City:r,Travel:r,Job:r,StockMarket:r,Bladeburner:r,Corporation:r,Gang:r,Milestones:r,Tutorial:r,Options:r,DevMenu:r},t.initializeMainMenuLinks=function(){try{function e(e){const t=a.clearEventListeners(e);if(null==t)throw new Error(`clearEventListeners() failed for element with id: ${e}`);return t}t.MainMenuLinks.Terminal=e("terminal-menu-link"),t.MainMenuLinks.ScriptEditor=e("create-script-menu-link"),t.MainMenuLinks.ActiveScripts=e("active-scripts-menu-link"),t.MainMenuLinks.CreateProgram=e("create-program-menu-link"),t.MainMenuLinks.Stats=e("stats-menu-link"),t.MainMenuLinks.Factions=e("factions-menu-link"),t.MainMenuLinks.Augmentations=e("augmentations-menu-link"),t.MainMenuLinks.HacknetNodes=e("hacknet-nodes-menu-link"),t.MainMenuLinks.Sleeves=e("sleeves-menu-link"),t.MainMenuLinks.City=e("city-menu-link"),t.MainMenuLinks.Travel=e("travel-menu-link"),t.MainMenuLinks.Job=e("job-menu-link"),t.MainMenuLinks.StockMarket=e("stock-market-menu-link"),t.MainMenuLinks.Bladeburner=e("bladeburner-menu-link"),t.MainMenuLinks.Corporation=e("corporation-menu-link"),t.MainMenuLinks.Gang=e("gang-menu-link"),t.MainMenuLinks.Milestones=e("milestones-menu-link"),t.MainMenuLinks.Tutorial=e("tutorial-menu-link");const n=document.getElementById("options-menu-link");if(null===n)throw new Error('Could not find element with id: "options-menu-link"');return t.MainMenuLinks.Options=n,t.MainMenuLinks.DevMenu=e("dev-menu-link"),!0}catch(e){return console.error(`Failed to initialize Main Menu Links: ${e}`),!1}}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.companyExists=t.loadCompanies=t.initCompanies=t.Companies=void 0;const a=n(1310),r=n(146),i=n(28);t.Companies={},t.initCompanies=function(){const e=t.Companies;t.Companies={},a.companiesMetadata.forEach(e=>{!function(e){null!=t.Companies[e.name]&&console.warn(`Duplicate Company Position being defined: ${e.name}`),t.Companies[e.name]=new r.Company(e)}(e)});for(const n in t.Companies){const a=t.Companies[n];e[n]instanceof r.Company?(a.favor=e[n].favor,isNaN(a.favor)&&(a.favor=0)):a.favor=0}},t.loadCompanies=function(e){t.Companies=JSON.parse(e,i.Reviver)},t.companyExists=function(e){return t.Companies.hasOwnProperty(e)}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.getRandomInt=void 0,t.getRandomInt=function(e,t){const n=Math.min(e,t),a=Math.max(e,t);return Math.floor(Math.random()*(a-n+1))+n}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.HacknetServerConstants=t.HacknetNodeConstants=void 0,t.HacknetNodeConstants={MoneyGainPerLevel:1.6,BaseCost:1e3,LevelBaseCost:1,RamBaseCost:3e4,CoreBaseCost:5e5,PurchaseNextMult:1.85,UpgradeLevelMult:1.04,UpgradeRamMult:1.28,UpgradeCoreMult:1.48,MaxLevel:200,MaxRam:64,MaxCores:16},t.HacknetServerConstants={HashesPerLevel:.001,BaseCost:5e4,RamBaseCost:2e5,CoreBaseCost:1e6,CacheBaseCost:1e7,PurchaseMult:3.2,UpgradeLevelMult:1.1,UpgradeRamMult:1.4,UpgradeCoreMult:1.55,UpgradeCacheMult:1.85,MaxServers:20,MaxLevel:300,MaxRam:8192,MaxCores:128,MaxCache:15}},,function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.CityName=void 0,function(e){e.Aevum="Aevum",e.Chongqing="Chongqing",e.Ishima="Ishima",e.NewTokyo="New Tokyo",e.Sector12="Sector-12",e.Volhaven="Volhaven"}(t.CityName||(t.CityName={}))},,function(module,__webpack_exports__,__webpack_require__){"use strict";(function($){__webpack_require__.d(__webpack_exports__,"f",function(){return IssueNewSharesCooldown}),__webpack_require__.d(__webpack_exports__,"k",function(){return SellSharesCooldown}),__webpack_require__.d(__webpack_exports__,"m",function(){return WarehouseInitialCost}),__webpack_require__.d(__webpack_exports__,"n",function(){return WarehouseInitialSize}),__webpack_require__.d(__webpack_exports__,"o",function(){return WarehouseUpgradeBaseCost}),__webpack_require__.d(__webpack_exports__,"g",function(){return OfficeInitialCost}),__webpack_require__.d(__webpack_exports__,"h",function(){return OfficeInitialSize}),__webpack_require__.d(__webpack_exports__,"a",function(){return BribeThreshold}),__webpack_require__.d(__webpack_exports__,"b",function(){return BribeToRepRatio}),__webpack_require__.d(__webpack_exports__,"j",function(){return ProductProductionCostRatio}),__webpack_require__.d(__webpack_exports__,"d",function(){return DividendMaxPercentage}),__webpack_require__.d(__webpack_exports__,"c",function(){return Corporation}),__webpack_require__.d(__webpack_exports__,"e",function(){return Industry}),__webpack_require__.d(__webpack_exports__,"i",function(){return OfficeSpace});var _CorporationState__WEBPACK_IMPORTED_MODULE_0__=__webpack_require__(562),_CorporationState__WEBPACK_IMPORTED_MODULE_0___default=__webpack_require__.n(_CorporationState__WEBPACK_IMPORTED_MODULE_0__),_data_CorporationUnlockUpgrades__WEBPACK_IMPORTED_MODULE_1__=__webpack_require__(474),_data_CorporationUnlockUpgrades__WEBPACK_IMPORTED_MODULE_1___default=__webpack_require__.n(_data_CorporationUnlockUpgrades__WEBPACK_IMPORTED_MODULE_1__),_data_CorporationUpgrades__WEBPACK_IMPORTED_MODULE_2__=__webpack_require__(473),_data_CorporationUpgrades__WEBPACK_IMPORTED_MODULE_2___default=__webpack_require__.n(_data_CorporationUpgrades__WEBPACK_IMPORTED_MODULE_2__),_EmployeePositions__WEBPACK_IMPORTED_MODULE_3__=__webpack_require__(26),_EmployeePositions__WEBPACK_IMPORTED_MODULE_3___default=__webpack_require__.n(_EmployeePositions__WEBPACK_IMPORTED_MODULE_3__),_IndustryData__WEBPACK_IMPORTED_MODULE_4__=__webpack_require__(33),_IndustryData__WEBPACK_IMPORTED_MODULE_4___default=__webpack_require__.n(_IndustryData__WEBPACK_IMPORTED_MODULE_4__),_IndustryUpgrades__WEBPACK_IMPORTED_MODULE_5__=__webpack_require__(320),_IndustryUpgrades__WEBPACK_IMPORTED_MODULE_5___default=__webpack_require__.n(_IndustryUpgrades__WEBPACK_IMPORTED_MODULE_5__),_Material__WEBPACK_IMPORTED_MODULE_6__=__webpack_require__(307),_Material__WEBPACK_IMPORTED_MODULE_6___default=__webpack_require__.n(_Material__WEBPACK_IMPORTED_MODULE_6__),_MaterialSizes__WEBPACK_IMPORTED_MODULE_7__=__webpack_require__(135),_MaterialSizes__WEBPACK_IMPORTED_MODULE_7___default=__webpack_require__.n(_MaterialSizes__WEBPACK_IMPORTED_MODULE_7__),_Product__WEBPACK_IMPORTED_MODULE_8__=__webpack_require__(210),_Product__WEBPACK_IMPORTED_MODULE_8___default=__webpack_require__.n(_Product__WEBPACK_IMPORTED_MODULE_8__),_ResearchMap__WEBPACK_IMPORTED_MODULE_9__=__webpack_require__(434),_ResearchMap__WEBPACK_IMPORTED_MODULE_9___default=__webpack_require__.n(_ResearchMap__WEBPACK_IMPORTED_MODULE_9__),_Warehouse__WEBPACK_IMPORTED_MODULE_10__=__webpack_require__(119),_Warehouse__WEBPACK_IMPORTED_MODULE_10___default=__webpack_require__.n(_Warehouse__WEBPACK_IMPORTED_MODULE_10__);__webpack_require__.d(__webpack_exports__,"l",function(){return _Warehouse__WEBPACK_IMPORTED_MODULE_10__.Warehouse});var _BitNode_BitNodeMultipliers__WEBPACK_IMPORTED_MODULE_11__=__webpack_require__(21),_BitNode_BitNodeMultipliers__WEBPACK_IMPORTED_MODULE_11___default=__webpack_require__.n(_BitNode_BitNodeMultipliers__WEBPACK_IMPORTED_MODULE_11__),_Literature_LiteratureHelpers__WEBPACK_IMPORTED_MODULE_12__=__webpack_require__(472),_Literature_LiteratureHelpers__WEBPACK_IMPORTED_MODULE_12___default=__webpack_require__.n(_Literature_LiteratureHelpers__WEBPACK_IMPORTED_MODULE_12__),_Literature_data_LiteratureNames__WEBPACK_IMPORTED_MODULE_13__=__webpack_require__(140),_Literature_data_LiteratureNames__WEBPACK_IMPORTED_MODULE_13___default=__webpack_require__.n(_Literature_data_LiteratureNames__WEBPACK_IMPORTED_MODULE_13__),_Locations_data_CityNames__WEBPACK_IMPORTED_MODULE_14__=__webpack_require__(39),_Locations_data_CityNames__WEBPACK_IMPORTED_MODULE_14___default=__webpack_require__.n(_Locations_data_CityNames__WEBPACK_IMPORTED_MODULE_14__),_Player__WEBPACK_IMPORTED_MODULE_15__=__webpack_require__(1),_ui_numeralFormat__WEBPACK_IMPORTED_MODULE_16__=__webpack_require__(3),_ui_numeralFormat__WEBPACK_IMPORTED_MODULE_16___default=__webpack_require__.n(_ui_numeralFormat__WEBPACK_IMPORTED_MODULE_16__),_ui_navigationTracking__WEBPACK_IMPORTED_MODULE_17__=__webpack_require__(19),_ui_navigationTracking__WEBPACK_IMPORTED_MODULE_17___default=__webpack_require__.n(_ui_navigationTracking__WEBPACK_IMPORTED_MODULE_17__),_utils_calculateEffectWithFactors__WEBPACK_IMPORTED_MODULE_18__=__webpack_require__(873),_utils_calculateEffectWithFactors__WEBPACK_IMPORTED_MODULE_18___default=__webpack_require__.n(_utils_calculateEffectWithFactors__WEBPACK_IMPORTED_MODULE_18__),_utils_DialogBox__WEBPACK_IMPORTED_MODULE_19__=__webpack_require__(12),_utils_DialogBox__WEBPACK_IMPORTED_MODULE_19___default=__webpack_require__.n(_utils_DialogBox__WEBPACK_IMPORTED_MODULE_19__),_utils_JSONReviver__WEBPACK_IMPORTED_MODULE_20__=__webpack_require__(28),_utils_uiHelpers_appendLineBreaks__WEBPACK_IMPORTED_MODULE_21__=__webpack_require__(872),_utils_uiHelpers_appendLineBreaks__WEBPACK_IMPORTED_MODULE_21___default=__webpack_require__.n(_utils_uiHelpers_appendLineBreaks__WEBPACK_IMPORTED_MODULE_21__),_utils_uiHelpers_createElement__WEBPACK_IMPORTED_MODULE_22__=__webpack_require__(13),_utils_uiHelpers_createElement__WEBPACK_IMPORTED_MODULE_22___default=__webpack_require__.n(_utils_uiHelpers_createElement__WEBPACK_IMPORTED_MODULE_22__),_utils_uiHelpers_createPopup__WEBPACK_IMPORTED_MODULE_23__=__webpack_require__(56),_utils_uiHelpers_createPopup__WEBPACK_IMPORTED_MODULE_23___default=__webpack_require__.n(_utils_uiHelpers_createPopup__WEBPACK_IMPORTED_MODULE_23__),_utils_uiHelpers_createPopupCloseButton__WEBPACK_IMPORTED_MODULE_24__=__webpack_require__(69),_utils_uiHelpers_createPopupCloseButton__WEBPACK_IMPORTED_MODULE_24___default=__webpack_require__.n(_utils_uiHelpers_createPopupCloseButton__WEBPACK_IMPORTED_MODULE_24__),_utils_StringHelperFunctions__WEBPACK_IMPORTED_MODULE_25__=__webpack_require__(20),_utils_StringHelperFunctions__WEBPACK_IMPORTED_MODULE_25___default=__webpack_require__.n(_utils_StringHelperFunctions__WEBPACK_IMPORTED_MODULE_25__),_utils_helpers_getRandomInt__WEBPACK_IMPORTED_MODULE_26__=__webpack_require__(36),_utils_helpers_getRandomInt__WEBPACK_IMPORTED_MODULE_26___default=__webpack_require__.n(_utils_helpers_getRandomInt__WEBPACK_IMPORTED_MODULE_26__),_utils_helpers_isString__WEBPACK_IMPORTED_MODULE_27__=__webpack_require__(63),_utils_helpers_isString__WEBPACK_IMPORTED_MODULE_27___default=__webpack_require__.n(_utils_helpers_isString__WEBPACK_IMPORTED_MODULE_27__),_utils_helpers_keyCodes__WEBPACK_IMPORTED_MODULE_28__=__webpack_require__(44),_utils_helpers_keyCodes__WEBPACK_IMPORTED_MODULE_28___default=__webpack_require__.n(_utils_helpers_keyCodes__WEBPACK_IMPORTED_MODULE_28__),_utils_uiHelpers_removeElement__WEBPACK_IMPORTED_MODULE_29__=__webpack_require__(172),_utils_uiHelpers_removeElement__WEBPACK_IMPORTED_MODULE_29___default=__webpack_require__.n(_utils_uiHelpers_removeElement__WEBPACK_IMPORTED_MODULE_29__),_utils_uiHelpers_removeElementById__WEBPACK_IMPORTED_MODULE_30__=__webpack_require__(49),_utils_uiHelpers_removeElementById__WEBPACK_IMPORTED_MODULE_30___default=__webpack_require__.n(_utils_uiHelpers_removeElementById__WEBPACK_IMPORTED_MODULE_30__),_utils_YesNoBox__WEBPACK_IMPORTED_MODULE_31__=__webpack_require__(54),_utils_YesNoBox__WEBPACK_IMPORTED_MODULE_31___default=__webpack_require__.n(_utils_YesNoBox__WEBPACK_IMPORTED_MODULE_31__),react__WEBPACK_IMPORTED_MODULE_32__=__webpack_require__(0),react__WEBPACK_IMPORTED_MODULE_32___default=__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_32__),react_dom__WEBPACK_IMPORTED_MODULE_33__=__webpack_require__(22),react_dom__WEBPACK_IMPORTED_MODULE_33___default=__webpack_require__.n(react_dom__WEBPACK_IMPORTED_MODULE_33__),_ui_CorporationUIEventHandler__WEBPACK_IMPORTED_MODULE_34__=__webpack_require__(871),_ui_Root__WEBPACK_IMPORTED_MODULE_35__=__webpack_require__(869),_ui_Routing__WEBPACK_IMPORTED_MODULE_36__=__webpack_require__(471),_ui_Routing__WEBPACK_IMPORTED_MODULE_36___default=__webpack_require__.n(_ui_Routing__WEBPACK_IMPORTED_MODULE_36__),decimal_js__WEBPACK_IMPORTED_MODULE_37__=__webpack_require__(57);const INITIALSHARES=1e9,SHARESPERPRICEUPDATE=1e6,IssueNewSharesCooldown=216e3,SellSharesCooldown=18e3,CyclesPerMarketCycle=50,CyclesPerIndustryStateCycle=CyclesPerMarketCycle/_CorporationState__WEBPACK_IMPORTED_MODULE_0__.AllCorporationStates.length,SecsPerMarketCycle=CyclesPerMarketCycle/5,Cities=["Aevum","Chongqing","Sector-12","New Tokyo","Ishima","Volhaven"],WarehouseInitialCost=5e9,WarehouseInitialSize=100,WarehouseUpgradeBaseCost=1e9,OfficeInitialCost=4e9,OfficeInitialSize=3,OfficeUpgradeBaseCost=1e9,BribeThreshold=1e14,BribeToRepRatio=1e9,ProductProductionCostRatio=5,DividendMaxPercentage=50,EmployeeSalaryMultiplier=3,CyclesPerEmployeeRaise=400,EmployeeRaiseAmount=50,BaseMaxProducts=3;let researchTreeBoxOpened=!1,researchTreeBox=null;function Industry(e={}){this.offices={[_Locations_data_CityNames__WEBPACK_IMPORTED_MODULE_14__.CityName.Aevum]:0,[_Locations_data_CityNames__WEBPACK_IMPORTED_MODULE_14__.CityName.Chongqing]:0,[_Locations_data_CityNames__WEBPACK_IMPORTED_MODULE_14__.CityName.Sector12]:new OfficeSpace({loc:_Locations_data_CityNames__WEBPACK_IMPORTED_MODULE_14__.CityName.Sector12,size:OfficeInitialSize}),[_Locations_data_CityNames__WEBPACK_IMPORTED_MODULE_14__.CityName.NewTokyo]:0,[_Locations_data_CityNames__WEBPACK_IMPORTED_MODULE_14__.CityName.Ishima]:0,[_Locations_data_CityNames__WEBPACK_IMPORTED_MODULE_14__.CityName.Volhaven]:0},this.name=e.name?e.name:0,this.type=e.type?e.type:_IndustryData__WEBPACK_IMPORTED_MODULE_4__.Industries.Agriculture,this.sciResearch=new _Material__WEBPACK_IMPORTED_MODULE_6__.Material({name:"Scientific Research"}),this.researched={},this.reqMats={},this.prodMats=[],this.products={},this.makesProducts=!1,this.awareness=0,this.popularity=0,this.startingCost=0,this.reFac=0,this.sciFac=0,this.hwFac=0,this.robFac=0,this.aiFac=0,this.advFac=0,this.prodMult=0,this.lastCycleRevenue=new decimal_js__WEBPACK_IMPORTED_MODULE_37__.a(0),this.lastCycleExpenses=new decimal_js__WEBPACK_IMPORTED_MODULE_37__.a(0),this.thisCycleRevenue=new decimal_js__WEBPACK_IMPORTED_MODULE_37__.a(0),this.thisCycleExpenses=new decimal_js__WEBPACK_IMPORTED_MODULE_37__.a(0);var t=Object.keys(_IndustryUpgrades__WEBPACK_IMPORTED_MODULE_5__.IndustryUpgrades).length;this.upgrades=Array(t).fill(0),this.state="START",this.newInd=!0,this.warehouses={[_Locations_data_CityNames__WEBPACK_IMPORTED_MODULE_14__.CityName.Aevum]:0,[_Locations_data_CityNames__WEBPACK_IMPORTED_MODULE_14__.CityName.Chonqing]:0,[_Locations_data_CityNames__WEBPACK_IMPORTED_MODULE_14__.CityName.Sector12]:new _Warehouse__WEBPACK_IMPORTED_MODULE_10__.Warehouse({corp:e.corp,industry:this,loc:_Locations_data_CityNames__WEBPACK_IMPORTED_MODULE_14__.CityName.Sector12,size:WarehouseInitialSize}),[_Locations_data_CityNames__WEBPACK_IMPORTED_MODULE_14__.CityName.NewTokyo]:0,[_Locations_data_CityNames__WEBPACK_IMPORTED_MODULE_14__.CityName.Ishima]:0,[_Locations_data_CityNames__WEBPACK_IMPORTED_MODULE_14__.CityName.Volhaven]:0},this.init()}function Employee(e={}){if(!(this instanceof Employee))return new Employee(e);this.name=e.name?e.name:"Bobby",this.mor=e.morale?e.morale:Object(_utils_helpers_getRandomInt__WEBPACK_IMPORTED_MODULE_26__.getRandomInt)(50,100),this.hap=e.happiness?e.happiness:Object(_utils_helpers_getRandomInt__WEBPACK_IMPORTED_MODULE_26__.getRandomInt)(50,100),this.ene=e.energy?e.energy:Object(_utils_helpers_getRandomInt__WEBPACK_IMPORTED_MODULE_26__.getRandomInt)(50,100),this.int=e.intelligence?e.intelligence:Object(_utils_helpers_getRandomInt__WEBPACK_IMPORTED_MODULE_26__.getRandomInt)(10,50),this.cha=e.charisma?e.charisma:Object(_utils_helpers_getRandomInt__WEBPACK_IMPORTED_MODULE_26__.getRandomInt)(10,50),this.exp=e.experience?e.experience:Object(_utils_helpers_getRandomInt__WEBPACK_IMPORTED_MODULE_26__.getRandomInt)(10,50),this.cre=e.creativity?e.creativity:Object(_utils_helpers_getRandomInt__WEBPACK_IMPORTED_MODULE_26__.getRandomInt)(10,50),this.eff=e.efficiency?e.efficiency:Object(_utils_helpers_getRandomInt__WEBPACK_IMPORTED_MODULE_26__.getRandomInt)(10,50),this.sal=e.salary?e.salary:Object(_utils_helpers_getRandomInt__WEBPACK_IMPORTED_MODULE_26__.getRandomInt)(.1,5),this.pro=0,this.cyclesUntilRaise=CyclesPerEmployeeRaise,this.loc=e.loc?e.loc:"",this.pos=_EmployeePositions__WEBPACK_IMPORTED_MODULE_3__.EmployeePositions.Unassigned}$(document).mousedown(function(e){researchTreeBoxOpened&&null==$(e.target).closest("#corporation-research-popup-box-content").get(0)&&(Object(_utils_uiHelpers_removeElement__WEBPACK_IMPORTED_MODULE_29__.removeElement)(researchTreeBox),researchTreeBox=null,researchTreeBoxOpened=!1)}),Industry.prototype.init=function(){switch(this.startingCost=_IndustryData__WEBPACK_IMPORTED_MODULE_4__.IndustryStartingCosts[this.type],this.type){case _IndustryData__WEBPACK_IMPORTED_MODULE_4__.Industries.Energy:this.reFac=.65,this.sciFac=.7,this.robFac=.05,this.aiFac=.3,this.advFac=.08,this.reqMats={Hardware:.1,Metal:.2},this.prodMats=["Energy"];break;case _IndustryData__WEBPACK_IMPORTED_MODULE_4__.Industries.Utilities:case"Utilities":this.reFac=.5,this.sciFac=.6,this.robFac=.4,this.aiFac=.4,this.advFac=.08,this.reqMats={Hardware:.1,Metal:.1},this.prodMats=["Water"];break;case _IndustryData__WEBPACK_IMPORTED_MODULE_4__.Industries.Agriculture:this.reFac=.72,this.sciFac=.5,this.hwFac=.2,this.robFac=.3,this.aiFac=.3,this.advFac=.04,this.reqMats={Water:.5,Energy:.5},this.prodMats=["Plants","Food"];break;case _IndustryData__WEBPACK_IMPORTED_MODULE_4__.Industries.Fishing:this.reFac=.15,this.sciFac=.35,this.hwFac=.35,this.robFac=.5,this.aiFac=.2,this.advFac=.08,this.reqMats={Energy:.5},this.prodMats=["Food"];break;case _IndustryData__WEBPACK_IMPORTED_MODULE_4__.Industries.Mining:this.reFac=.3,this.sciFac=.26,this.hwFac=.4,this.robFac=.45,this.aiFac=.45,this.advFac=.06,this.reqMats={Energy:.8},this.prodMats=["Metal"];break;case _IndustryData__WEBPACK_IMPORTED_MODULE_4__.Industries.Food:this.sciFac=.12,this.hwFac=.15,this.robFac=.3,this.aiFac=.25,this.advFac=.25,this.reFac=.05,this.reqMats={Food:.5,Water:.5,Energy:.2},this.makesProducts=!0;break;case _IndustryData__WEBPACK_IMPORTED_MODULE_4__.Industries.Tobacco:this.reFac=.15,this.sciFac=.75,this.hwFac=.15,this.robFac=.2,this.aiFac=.15,this.advFac=.2,this.reqMats={Plants:1,Water:.2},this.makesProducts=!0;break;case _IndustryData__WEBPACK_IMPORTED_MODULE_4__.Industries.Chemical:this.reFac=.25,this.sciFac=.75,this.hwFac=.2,this.robFac=.25,this.aiFac=.2,this.advFac=.07,this.reqMats={Plants:1,Energy:.5,Water:.5},this.prodMats=["Chemicals"];break;case _IndustryData__WEBPACK_IMPORTED_MODULE_4__.Industries.Pharmaceutical:this.reFac=.05,this.sciFac=.8,this.hwFac=.15,this.robFac=.25,this.aiFac=.2,this.advFac=.16,this.reqMats={Chemicals:2,Energy:1,Water:.5},this.prodMats=["Drugs"],this.makesProducts=!0;break;case _IndustryData__WEBPACK_IMPORTED_MODULE_4__.Industries.Computer:case"Computer":this.reFac=.2,this.sciFac=.62,this.robFac=.36,this.aiFac=.19,this.advFac=.17,this.reqMats={Metal:2,Energy:1},this.prodMats=["Hardware"],this.makesProducts=!0;break;case _IndustryData__WEBPACK_IMPORTED_MODULE_4__.Industries.Robotics:this.reFac=.32,this.sciFac=.65,this.aiFac=.36,this.advFac=.18,this.hwFac=.19,this.reqMats={Hardware:5,Energy:3},this.prodMats=["Robots"],this.makesProducts=!0;break;case _IndustryData__WEBPACK_IMPORTED_MODULE_4__.Industries.Software:this.sciFac=.62,this.advFac=.16,this.hwFac=.25,this.reFac=.15,this.aiFac=.18,this.robFac=.05,this.reqMats={Hardware:.5,Energy:.5},this.prodMats=["AICores"],this.makesProducts=!0;break;case _IndustryData__WEBPACK_IMPORTED_MODULE_4__.Industries.Healthcare:this.reFac=.1,this.sciFac=.75,this.advFac=.11,this.hwFac=.1,this.robFac=.1,this.aiFac=.1,this.reqMats={Robots:10,AICores:5,Energy:5,Water:5},this.makesProducts=!0;break;case _IndustryData__WEBPACK_IMPORTED_MODULE_4__.Industries.RealEstate:this.robFac=.6,this.aiFac=.6,this.advFac=.25,this.sciFac=.05,this.hwFac=.05,this.reqMats={Metal:5,Energy:5,Water:2,Hardware:4},this.prodMats=["RealEstate"],this.makesProducts=!0;break;default:return void console.error(`Invalid Industry Type passed into Industry.init(): ${this.type}`)}},Industry.prototype.getProductDescriptionText=function(){if(this.makesProducts)switch(this.type){case _IndustryData__WEBPACK_IMPORTED_MODULE_4__.Industries.Food:return"create and manage restaurants";case _IndustryData__WEBPACK_IMPORTED_MODULE_4__.Industries.Tobacco:return"create tobacco and tobacco-related products";case _IndustryData__WEBPACK_IMPORTED_MODULE_4__.Industries.Pharmaceutical:return"develop new pharmaceutical drugs";case _IndustryData__WEBPACK_IMPORTED_MODULE_4__.Industries.Computer:case"Computer":return"create new computer hardware and networking infrastructures";case _IndustryData__WEBPACK_IMPORTED_MODULE_4__.Industries.Robotics:return"build specialized robots and robot-related products";case _IndustryData__WEBPACK_IMPORTED_MODULE_4__.Industries.Software:return"develop computer software";case _IndustryData__WEBPACK_IMPORTED_MODULE_4__.Industries.Healthcare:return"build and manage hospitals";case _IndustryData__WEBPACK_IMPORTED_MODULE_4__.Industries.RealEstate:return"develop and manage real estate properties";default:return console.error("Invalid industry type in Industry.getProductDescriptionText"),""}},Industry.prototype.getMaximumNumberProducts=function(){if(!this.makesProducts)return 0;let e=0;return this.hasResearch("uPgrade: Capacity.I")&&++e,this.hasResearch("uPgrade: Capacity.II")&&++e,BaseMaxProducts+e},Industry.prototype.hasMaximumNumberProducts=function(){return Object.keys(this.products).length>=this.getMaximumNumberProducts()},Industry.prototype.calculateProductionFactors=function(){for(var e=0,t=0;t0&&(e.breakdown+=t+": "+Object(_utils_StringHelperFunctions__WEBPACK_IMPORTED_MODULE_25__.formatNumber)(n.data[e.loc][0]*n.siz,0)+"
")}},Industry.prototype.process=function(e=1,t,n){if(this.state=t,"START"===t){(isNaN(this.thisCycleRevenue)||isNaN(this.thisCycleExpenses))&&(console.error("NaN in Corporation's computed revenue/expenses"),Object(_utils_DialogBox__WEBPACK_IMPORTED_MODULE_19__.dialogBoxCreate)("Something went wrong when compting Corporation's revenue/expenses. This is a bug. Please report to game developer"),this.thisCycleRevenue=new decimal_js__WEBPACK_IMPORTED_MODULE_37__.a(0),this.thisCycleExpenses=new decimal_js__WEBPACK_IMPORTED_MODULE_37__.a(0)),this.lastCycleRevenue=this.thisCycleRevenue.dividedBy(e*SecsPerMarketCycle),this.lastCycleExpenses=this.thisCycleExpenses.dividedBy(e*SecsPerMarketCycle),this.thisCycleRevenue=new decimal_js__WEBPACK_IMPORTED_MODULE_37__.a(0),this.thisCycleExpenses=new decimal_js__WEBPACK_IMPORTED_MODULE_37__.a(0),this.lastCycleRevenue.gt(0)&&(this.newInd=!1);var a=0;for(var r in this.offices)this.offices[r]instanceof OfficeSpace&&(a+=this.offices[r].process(e,{industry:this,corporation:n}));this.thisCycleExpenses=this.thisCycleExpenses.plus(a),this.processMaterialMarket(e),this.processProductMarket(e),this.popularity-=1e-4*e,this.popularity=Math.max(0,this.popularity);var i=n.getDreamSenseGain(),o=4*i;return void(i>0&&(this.popularity+=i*e,this.awareness+=o*e))}let s=this.processMaterials(e,n);Array.isArray(s)&&(this.thisCycleRevenue=this.thisCycleRevenue.plus(s[0]),this.thisCycleExpenses=this.thisCycleExpenses.plus(s[1])),s=this.processProducts(e,n),Array.isArray(s)&&(this.thisCycleRevenue=this.thisCycleRevenue.plus(s[0]),this.thisCycleExpenses=this.thisCycleExpenses.plus(s[1]))},Industry.prototype.processMaterialMarket=function(){for(var e=this.reqMats,t=this.prodMats,n=0;n0&&(r.qty+=a,expenses+=a*r.bCost)}(matName,this),this.updateWarehouseSizeUsed(warehouse));break;case"PRODUCTION":if(warehouse.smartSupplyStore=0,this.prodMats.length>0){var mat=warehouse.materials[this.prodMats[0]],maxProd=this.getOfficeProductivity(office)*this.prodMult*company.getProductionMultiplier()*this.getProductionMultiplier();let e;e=mat.prdman[0]?Math.min(maxProd,mat.prdman[1]):maxProd,e*=SecsPerMarketCycle*marketCycles;var totalMatSize=0;for(let e=0;e0){var maxAmt=Math.floor((warehouse.size-warehouse.sizeUsed)/totalMatSize);e=Math.min(maxAmt,e)}e<0&&(e=0),warehouse.smartSupplyStore+=e/(SecsPerMarketCycle*marketCycles);var producableFrac=1;for(var reqMatName in this.reqMats)if(this.reqMats.hasOwnProperty(reqMatName)){var req=this.reqMats[reqMatName]*e;warehouse.materials[reqMatName].qty0&&e>0){for(const t in this.reqMats){var reqMatQtyNeeded=this.reqMats[t]*e*producableFrac;warehouse.materials[t].qty-=reqMatQtyNeeded,warehouse.materials[t].prd=0,warehouse.materials[t].prd-=reqMatQtyNeeded/(SecsPerMarketCycle*marketCycles)}for(let t=0;tmat.bCost?sCost-mat.bCost>markupLimit&&(markup=Math.pow(markupLimit/(sCost-mat.bCost),2)):sCost=0?(mat.qty-=sellAmt,revenue+=sellAmt*sCost,mat.sll=sellAmt/(SecsPerMarketCycle*marketCycles)):mat.sll=0}break;case"EXPORT":for(var matName in warehouse.materials)if(warehouse.materials.hasOwnProperty(matName)){var mat=warehouse.materials[matName];mat.totalExp=0;for(var expI=0;expI=expWarehouse.size)return[0,0];var maxAmt=Math.floor((expWarehouse.size-expWarehouse.sizeUsed)/_MaterialSizes__WEBPACK_IMPORTED_MODULE_7__.MaterialSizes[matName]);amt=Math.min(maxAmt,amt),expWarehouse.materials[matName].imp+=amt/(SecsPerMarketCycle*marketCycles),expWarehouse.materials[matName].qty+=amt,expWarehouse.materials[matName].qlt=mat.qlt,mat.qty-=amt,mat.totalExp+=amt,expIndustry.updateWarehouseSizeUsed(expWarehouse);break}}}mat.totalExp/=SecsPerMarketCycle*marketCycles}break;case"START":break;default:console.error(`Invalid state: ${this.state}`)}this.updateWarehouseSizeUsed(warehouse)}office instanceof OfficeSpace&&(this.sciResearch.qty+=.004*Math.pow(office.employeeProd[_EmployeePositions__WEBPACK_IMPORTED_MODULE_3__.EmployeePositions.RandD],.5)*company.getScientificResearchMultiplier()*this.getScientificResearchMultiplier())}return[revenue,expenses]},Industry.prototype.processProducts=function(e=1,t){var n=0;if("PRODUCTION"===this.state)for(const t in this.products){const n=this.products[t];if(!n.fin){const t=n.createCity,a=this.offices[t],r=a.employeeProd[_EmployeePositions__WEBPACK_IMPORTED_MODULE_3__.EmployeePositions.Engineer],i=a.employeeProd[_EmployeePositions__WEBPACK_IMPORTED_MODULE_3__.EmployeePositions.Management],o=a.employeeProd[_EmployeePositions__WEBPACK_IMPORTED_MODULE_3__.EmployeePositions.Operations],s=r+i+o;if(s<=0)break;const l=1+i/(1.2*s),c=(Math.pow(r,.34)+Math.pow(o,.2))*l;n.createProduct(e,c),n.prog>=100&&n.finishProduct(a.employeeProd,this);break}}for(var a in this.products)if(this.products.hasOwnProperty(a)){var r=this.products[a];r instanceof _Product__WEBPACK_IMPORTED_MODULE_8__.Product&&r.fin&&(n+=this.processProduct(e,r,t))}return[n,0]},Industry.prototype.processProduct=function(marketCycles=1,product,corporation){let totalProfit=0;for(let i=0;i0){var maxAmt=Math.floor((warehouse.size-warehouse.sizeUsed)/netStorageSize);e=Math.min(maxAmt,e)}warehouse.smartSupplyStore+=e/(SecsPerMarketCycle*marketCycles);var producableFrac=1;for(var reqMatName in product.reqMats)if(product.reqMats.hasOwnProperty(reqMatName)){var req=product.reqMats[reqMatName]*e;warehouse.materials[reqMatName].qty0&&e>0){for(var reqMatName in product.reqMats)if(product.reqMats.hasOwnProperty(reqMatName)){var reqMatQtyNeeded=product.reqMats[reqMatName]*e*producableFrac;warehouse.materials[reqMatName].qty-=reqMatQtyNeeded,warehouse.materials[reqMatName].prd-=reqMatQtyNeeded/(SecsPerMarketCycle*marketCycles)}product.data[city][0]+=e*producableFrac}product.data[city][1]=e*producableFrac/(SecsPerMarketCycle*marketCycles);break}case"SALE":{for(var reqMatName in product.pCost=0,product.reqMats)product.reqMats.hasOwnProperty(reqMatName)&&(product.pCost+=product.reqMats[reqMatName]*warehouse.materials[reqMatName].bCost);product.pCost*=ProductProductionCostRatio;const businessFactor=this.getBusinessFactor(office),advertisingFactor=this.getAdvertisingFactors()[0],marketFactor=this.getMarketFactor(product),markupLimit=product.rat/product.mku;var sCost;if(product.marketTa2){const e=product.data[city][1],t=markupLimit,n=e,a=.5*Math.pow(product.rat,.65)*marketFactor*corporation.getSalesMultiplier()*businessFactor*advertisingFactor*this.getSalesMultiplier(),r=Math.sqrt(n/a);let i;0===a||0===r?0===n?i=0:(i=product.pCost+markupLimit,console.warn("In Corporation, found illegal 0s when trying to calculate MarketTA2 sale cost")):i=t/r+product.pCost,product.marketTa2Price[city]=i,sCost=i}else product.marketTa1?sCost=product.pCost+markupLimit:Object(_utils_helpers_isString__WEBPACK_IMPORTED_MODULE_27__.isString)(product.sCost)?(0===product.mku&&(console.error("mku is zero, reverting to 1 to avoid Infinity"),product.mku=1),sCost=product.sCost.replace(/MP/g,product.pCost+product.rat/product.mku),sCost=eval(sCost)):sCost=product.sCost;var markup=1;sCost>product.pCost&&sCost-product.pCost>markupLimit&&(markup=markupLimit/(sCost-product.pCost));var maxSell=.5*Math.pow(product.rat,.65)*marketFactor*corporation.getSalesMultiplier()*Math.pow(markup,2)*businessFactor*advertisingFactor*this.getSalesMultiplier(),sellAmt;if(product.sllman[city][0]&&Object(_utils_helpers_isString__WEBPACK_IMPORTED_MODULE_27__.isString)(product.sllman[city][1])){var tmp=product.sllman[city][1].replace(/MAX/g,maxSell);tmp=tmp.replace(/PROD/g,product.data[city][1]);try{tmp=eval(tmp)}catch(e){Object(_utils_DialogBox__WEBPACK_IMPORTED_MODULE_19__.dialogBoxCreate)("Error evaluating your sell price expression for "+product.name+" in "+this.name+"'s "+city+" office. Sell price is being set to MAX"),tmp=maxSell}sellAmt=Math.min(maxSell,tmp)}else sellAmt=product.sllman[city][0]&&product.sllman[city][1]>0?Math.min(maxSell,product.sllman[city][1]):!1===product.sllman[city][0]?0:maxSell;sellAmt<0&&(sellAmt=0),sellAmt=sellAmt*SecsPerMarketCycle*marketCycles,sellAmt=Math.min(product.data[city][0],sellAmt),sellAmt&&sCost?(product.data[city][0]-=sellAmt,totalProfit+=sellAmt*sCost,product.data[city][2]=sellAmt/(SecsPerMarketCycle*marketCycles)):product.data[city][2]=0;break}case"START":case"PURCHASE":case"EXPORT":break;default:console.error(`Invalid State: ${this.state}`)}}return totalProfit},Industry.prototype.discontinueProduct=function(e){for(var t in this.products)this.products.hasOwnProperty(t)&&e===this.products[t]&&delete this.products[t]},Industry.prototype.upgrade=function(e,t){for(var n=t.corporation,a=t.office,r=e[0];this.upgrades.length<=r;)this.upgrades.push(0);switch(++this.upgrades[r],r){case 0:for(let e=0;e{if(this.sciResearch.qty>=n.cost){this.sciResearch.qty-=n.cost,t.research(a[e]),this.researched[a[e]]=!0;const r=this.createResearchBox();return Object(_utils_DialogBox__WEBPACK_IMPORTED_MODULE_19__.dialogBoxCreate)(`Researched ${a[e]}. It may take a market cycle `+`(~${SecsPerMarketCycle} seconds) before the effects of `+"the Research apply."),r}Object(_utils_DialogBox__WEBPACK_IMPORTED_MODULE_19__.dialogBoxCreate)(`You do not have enough Scientific Research for ${n.name}`)}):console.warn(`Could not find Research Tree div for ${r}`)}const r=document.getElementById(`${e}-content`);null!=r&&(Object(_utils_uiHelpers_appendLineBreaks__WEBPACK_IMPORTED_MODULE_21__.appendLineBreaks)(r,2),r.appendChild(Object(_utils_uiHelpers_createElement__WEBPACK_IMPORTED_MODULE_22__.createElement)("pre",{display:"block",innerText:"Multipliers from research:\n"+` * Advertising Multiplier: x${t.getAdvertisingMultiplier()}\n`+` * Employee Charisma Multiplier: x${t.getEmployeeChaMultiplier()}\n`+` * Employee Creativity Multiplier: x${t.getEmployeeCreMultiplier()}\n`+` * Employee Efficiency Multiplier: x${t.getEmployeeEffMultiplier()}\n`+` * Employee Intelligence Multiplier: x${t.getEmployeeIntMultiplier()}\n`+` * Production Multiplier: x${t.getProductionMultiplier()}\n`+` * Sales Multiplier: x${t.getSalesMultiplier()}\n`+` * Scientific Research Multiplier: x${t.getScientificResearchMultiplier()}\n`+` * Storage Multiplier: x${t.getStorageMultiplier()}`})),r.appendChild(Object(_utils_uiHelpers_createPopupCloseButton__WEBPACK_IMPORTED_MODULE_24__.createPopupCloseButton)(researchTreeBox,{class:"std-button",display:"block",innerText:"Close"}))),researchTreeBoxOpened=!0},Industry.prototype.toJSON=function(){return Object(_utils_JSONReviver__WEBPACK_IMPORTED_MODULE_20__.Generic_toJSON)("Industry",this)},Industry.fromJSON=function(e){return Object(_utils_JSONReviver__WEBPACK_IMPORTED_MODULE_20__.Generic_fromJSON)(Industry,e.data)},_utils_JSONReviver__WEBPACK_IMPORTED_MODULE_20__.Reviver.constructors.Industry=Industry,Employee.prototype.process=function(e=1,t){var n=.003*e,a=n*Math.random();this.exp+=n,this.cyclesUntilRaise-=e,this.cyclesUntilRaise<=0&&(this.salary+=EmployeeRaiseAmount,this.cyclesUntilRaise+=CyclesPerEmployeeRaise);var r=n*Math.random();return this.pos===_EmployeePositions__WEBPACK_IMPORTED_MODULE_3__.EmployeePositions.Training&&(this.cha+=r,this.exp+=r,this.eff+=r),this.ene-=a,this.hap-=a,this.eneHappiness: "+Object(_utils_StringHelperFunctions__WEBPACK_IMPORTED_MODULE_25__.formatNumber)(this.hap,3)+"
Energy: "+Object(_utils_StringHelperFunctions__WEBPACK_IMPORTED_MODULE_25__.formatNumber)(this.ene,3)+"
Intelligence: "+Object(_utils_StringHelperFunctions__WEBPACK_IMPORTED_MODULE_25__.formatNumber)(i,3)+"
Charisma: "+Object(_utils_StringHelperFunctions__WEBPACK_IMPORTED_MODULE_25__.formatNumber)(r,3)+"
Experience: "+Object(_utils_StringHelperFunctions__WEBPACK_IMPORTED_MODULE_25__.formatNumber)(this.exp,3)+"
Creativity: "+Object(_utils_StringHelperFunctions__WEBPACK_IMPORTED_MODULE_25__.formatNumber)(a,3)+"
Efficiency: "+Object(_utils_StringHelperFunctions__WEBPACK_IMPORTED_MODULE_25__.formatNumber)(o,3)+"
Salary: "+_ui_numeralFormat__WEBPACK_IMPORTED_MODULE_16__.numeralWrapper.format(this.sal,"$0.000a")+"/ s
"}));var s=Object(_utils_uiHelpers_createElement__WEBPACK_IMPORTED_MODULE_22__.createElement)("select",{});for(var l in _EmployeePositions__WEBPACK_IMPORTED_MODULE_3__.EmployeePositions)_EmployeePositions__WEBPACK_IMPORTED_MODULE_3__.EmployeePositions.hasOwnProperty(l)&&s.add(Object(_utils_uiHelpers_createElement__WEBPACK_IMPORTED_MODULE_22__.createElement)("option",{text:_EmployeePositions__WEBPACK_IMPORTED_MODULE_3__.EmployeePositions[l],value:_EmployeePositions__WEBPACK_IMPORTED_MODULE_3__.EmployeePositions[l]}));s.addEventListener("change",()=>{this.pos=s.options[s.selectedIndex].value});for(var c=0;c=this.size},OfficeSpace.prototype.process=function(e=1,t){var n=t.industry;if(n.hasResearch("HRBuddy-Recruitment")&&!this.atCapacity()){const e=this.hireRandomEmployee();n.hasResearch("HRBuddy-Training")&&(e.pos=_EmployeePositions__WEBPACK_IMPORTED_MODULE_3__.EmployeePositions.Training)}this.maxEne=100,this.maxHap=100,this.maxMor=100,n.hasResearch("Go-Juice")&&(this.maxEne+=10),n.hasResearch("JoyWire")&&(this.maxHap+=10),n.hasResearch("Sti.mu")&&(this.maxMor+=10);var a=1;n.funds<0&&n.lastCycleRevenue<0?a=Math.pow(.99,e):n.funds>0&&n.lastCycleRevenue>0&&(a=Math.pow(1.01,e));const r=n.hasResearch("AutoBrew"),i=n.hasResearch("AutoPartyManager");var o=0;for(let t=0;tCharisma: "+Object(_utils_StringHelperFunctions__WEBPACK_IMPORTED_MODULE_25__.formatNumber)(t.cha,1)+"
Experience: "+Object(_utils_StringHelperFunctions__WEBPACK_IMPORTED_MODULE_25__.formatNumber)(t.exp,1)+"
Creativity: "+Object(_utils_StringHelperFunctions__WEBPACK_IMPORTED_MODULE_25__.formatNumber)(t.cre,1)+"
Efficiency: "+Object(_utils_StringHelperFunctions__WEBPACK_IMPORTED_MODULE_25__.formatNumber)(t.eff,1)+"
Salary: "+_ui_numeralFormat__WEBPACK_IMPORTED_MODULE_16__.numeralWrapper.format(t.sal,"$0.000a")+" s
",clickListener:()=>(n.hireEmployee(t,e),Object(_utils_uiHelpers_removeElementById__WEBPACK_IMPORTED_MODULE_30__.removeElementById)("cmpy-mgmt-hire-employee-popup"),!1)})},_=Object(_utils_uiHelpers_createElement__WEBPACK_IMPORTED_MODULE_22__.createElement)("a",{class:"a-link-button",innerText:"Cancel",float:"right",clickListener:()=>(Object(_utils_uiHelpers_removeElementById__WEBPACK_IMPORTED_MODULE_30__.removeElementById)("cmpy-mgmt-hire-employee-popup"),!1)}),g=[d,h(u,this),h(m,this),h(p,this),_];Object(_utils_uiHelpers_createPopup__WEBPACK_IMPORTED_MODULE_23__.createPopup)("cmpy-mgmt-hire-employee-popup",g)}},OfficeSpace.prototype.hireEmployee=function(e,t){var n=t.corporation,a=Object(_utils_YesNoBox__WEBPACK_IMPORTED_MODULE_31__.yesNoTxtInpBoxGetYesButton)(),r=Object(_utils_YesNoBox__WEBPACK_IMPORTED_MODULE_31__.yesNoTxtInpBoxGetNoButton)();a.innerHTML="Hire",r.innerHTML="Cancel",a.addEventListener("click",()=>{for(var t=Object(_utils_YesNoBox__WEBPACK_IMPORTED_MODULE_31__.yesNoTxtInpBoxGetInput)(),a=0;aObject(_utils_YesNoBox__WEBPACK_IMPORTED_MODULE_31__.yesNoTxtInpBoxClose)()),Object(_utils_YesNoBox__WEBPACK_IMPORTED_MODULE_31__.yesNoTxtInpBoxCreate)("Give your employee a nickname!")},OfficeSpace.prototype.hireRandomEmployee=function(){if(!this.atCapacity()&&null==document.getElementById("cmpy-mgmt-hire-employee-popup")){var e=Object(_utils_helpers_getRandomInt__WEBPACK_IMPORTED_MODULE_26__.getRandomInt)(76,100)/100,t=Object(_utils_helpers_getRandomInt__WEBPACK_IMPORTED_MODULE_26__.getRandomInt)(50,100),n=Object(_utils_helpers_getRandomInt__WEBPACK_IMPORTED_MODULE_26__.getRandomInt)(50,100),a=Object(_utils_helpers_getRandomInt__WEBPACK_IMPORTED_MODULE_26__.getRandomInt)(50,100),r=Object(_utils_helpers_getRandomInt__WEBPACK_IMPORTED_MODULE_26__.getRandomInt)(50,100),i=Object(_utils_helpers_getRandomInt__WEBPACK_IMPORTED_MODULE_26__.getRandomInt)(50,100),o=new Employee({intelligence:t*e,charisma:n*e,experience:a*e,creativity:r*e,efficiency:i*e,salary:EmployeeSalaryMultiplier*(t+n+a+r+i)*e}),s=Object(_utils_StringHelperFunctions__WEBPACK_IMPORTED_MODULE_25__.generateRandomString)(7);for(let e=0;e=CyclesPerIndustryStateCycle){const e=this.getState(),t=1,n=t*CyclesPerIndustryStateCycle;if(this.storedCycles-=n,this.divisions.forEach(n=>{n.process(t,e,this)}),this.shareSaleCooldown>0&&(this.shareSaleCooldown-=n),this.issueNewSharesCooldown>0&&(this.issueNewSharesCooldown-=n),"START"===e){this.revenue=new decimal_js__WEBPACK_IMPORTED_MODULE_37__.a(0),this.expenses=new decimal_js__WEBPACK_IMPORTED_MODULE_37__.a(0),this.divisions.forEach(e=>{e.lastCycleRevenue!==-1/0&&e.lastCycleRevenue!==1/0&&e.lastCycleExpenses!==-1/0&&e.lastCycleExpenses!==1/0&&(this.revenue=this.revenue.plus(e.lastCycleRevenue),this.expenses=this.expenses.plus(e.lastCycleExpenses))});const e=this.revenue.minus(this.expenses).times(t*SecsPerMarketCycle);if((isNaN(this.funds)||this.funds===1/0||this.funds===-1/0)&&(Object(_utils_DialogBox__WEBPACK_IMPORTED_MODULE_19__.dialogBoxCreate)("There was an error calculating your Corporations funds and they got reset to 0. This is a bug. Please report to game developer.

(Your funds have been set to $150b for the inconvenience)"),this.funds=new decimal_js__WEBPACK_IMPORTED_MODULE_37__.a(15e10)),this.dividendPercentage>0&&e>0)if(isNaN(this.dividendPercentage)||this.dividendPercentage<0||this.dividendPercentage>DividendMaxPercentage)console.error(`Invalid Corporation dividend percentage: ${this.dividendPercentage}`);else{const t=this.dividendPercentage/100*e,n=e-t,a=t/this.totalShares,r=this.numShares*a*(1-this.dividendTaxPercentage/100);_Player__WEBPACK_IMPORTED_MODULE_15__.Player.gainMoney(r),_Player__WEBPACK_IMPORTED_MODULE_15__.Player.recordMoneySource(r,"corporation"),this.addFunds(n)}else this.addFunds(e);this.updateSharePrice()}this.state.nextState(),_ui_navigationTracking__WEBPACK_IMPORTED_MODULE_17__.routing.isOn(_ui_navigationTracking__WEBPACK_IMPORTED_MODULE_17__.Page.Corporation)&&this.rerender()}},Corporation.prototype.determineValuation=function(){var e,t=this.revenue.minus(this.expenses).toNumber();return this.public?(this.dividendPercentage>0&&(t*=(100-this.dividendPercentage)/100),e=this.funds.toNumber()+85e3*t,e*=Math.pow(1.1,this.divisions.length),e=Math.max(e,0)):(e=1e10+Math.max(this.funds.toNumber(),0)/3,t>0?(e+=315e3*t,e*=Math.pow(1.1,this.divisions.length)):e=1e10*Math.pow(1.1,this.divisions.length),e-=e%1e6),e*_BitNode_BitNodeMultipliers__WEBPACK_IMPORTED_MODULE_11__.BitNodeMultipliers.CorporationValuation},Corporation.prototype.getInvestment=function(){var e,t=this.determineValuation();let n=4;switch(this.fundingRound){case 0:e=.1,n=4;break;case 1:e=.35,n=3;break;case 2:e=.25,n=3;break;case 3:e=.2,n=2.5;break;case 4:return}var a=t*e*n,r=Math.floor(INITIALSHARES*e),i=Object(_utils_YesNoBox__WEBPACK_IMPORTED_MODULE_31__.yesNoBoxGetYesButton)(),o=Object(_utils_YesNoBox__WEBPACK_IMPORTED_MODULE_31__.yesNoBoxGetNoButton)();i.innerHTML="Accept",o.innerHML="Reject",i.addEventListener("click",()=>(++this.fundingRound,this.addFunds(a),this.numShares-=r,this.rerender(),Object(_utils_YesNoBox__WEBPACK_IMPORTED_MODULE_31__.yesNoBoxClose)())),o.addEventListener("click",()=>Object(_utils_YesNoBox__WEBPACK_IMPORTED_MODULE_31__.yesNoBoxClose)()),Object(_utils_YesNoBox__WEBPACK_IMPORTED_MODULE_31__.yesNoBoxCreate)("An investment firm has offered you "+_ui_numeralFormat__WEBPACK_IMPORTED_MODULE_16__.numeralWrapper.format(a,"$0.000a")+" in funding in exchange for a "+_ui_numeralFormat__WEBPACK_IMPORTED_MODULE_16__.numeralWrapper.format(100*e,"0.000a")+"% stake in the company ("+_ui_numeralFormat__WEBPACK_IMPORTED_MODULE_16__.numeralWrapper.format(r,"0.000a")+" shares).

Do you accept or reject this offer?

Hint: Investment firms will offer more money if your corporation is turning a profit")},Corporation.prototype.goPublic=function(){var e,t=this.determineValuation()/this.totalShares,n=Object(_utils_uiHelpers_createElement__WEBPACK_IMPORTED_MODULE_22__.createElement)("p",{innerHTML:"Enter the number of shares you would like to issue for your IPO. These shares will be publicly sold and you will no longer own them. Your Corporation will receive "+_ui_numeralFormat__WEBPACK_IMPORTED_MODULE_16__.numeralWrapper.format(t,"$0.000a")+" per share (the IPO money will be deposited directly into your Corporation's funds).

You have a total of "+_ui_numeralFormat__WEBPACK_IMPORTED_MODULE_16__.numeralWrapper.format(this.numShares,"0.000a")+" of shares that you can issue."}),a=Object(_utils_uiHelpers_createElement__WEBPACK_IMPORTED_MODULE_22__.createElement)("input",{type:"number",placeholder:"Shares to issue",onkeyup:t=>{t.preventDefault(),t.keyCode===_utils_helpers_keyCodes__WEBPACK_IMPORTED_MODULE_28__.KEY.ENTER&&e.click()}}),r=Object(_utils_uiHelpers_createElement__WEBPACK_IMPORTED_MODULE_22__.createElement)("br",{});e=Object(_utils_uiHelpers_createElement__WEBPACK_IMPORTED_MODULE_22__.createElement)("a",{class:"a-link-button",innerText:"Go Public",clickListener:()=>{var e=Math.round(a.value),t=this.determineValuation()/this.totalShares;return isNaN(e)?(Object(_utils_DialogBox__WEBPACK_IMPORTED_MODULE_19__.dialogBoxCreate)("Invalid value for number of issued shares"),!1):e>this.numShares?(Object(_utils_DialogBox__WEBPACK_IMPORTED_MODULE_19__.dialogBoxCreate)("Error: You don't have that many shares to issue!"),!1):(this.public=!0,this.sharePrice=t,this.issuedShares=e,this.numShares-=e,this.addFunds(e*t),this.rerender(),Object(_utils_uiHelpers_removeElementById__WEBPACK_IMPORTED_MODULE_30__.removeElementById)("cmpy-mgmt-go-public-popup"),Object(_utils_DialogBox__WEBPACK_IMPORTED_MODULE_19__.dialogBoxCreate)(`You took your ${this.name} public and earned `+`${_ui_numeralFormat__WEBPACK_IMPORTED_MODULE_16__.numeralWrapper.formatMoney(e*t)} in your IPO`),!1)}});var i=Object(_utils_uiHelpers_createElement__WEBPACK_IMPORTED_MODULE_22__.createElement)("a",{class:"a-link-button",innerText:"Cancel",clickListener:()=>(Object(_utils_uiHelpers_removeElementById__WEBPACK_IMPORTED_MODULE_30__.removeElementById)("cmpy-mgmt-go-public-popup"),!1)});Object(_utils_uiHelpers_createPopup__WEBPACK_IMPORTED_MODULE_23__.createPopup)("cmpy-mgmt-go-public-popup",[n,r,a,e,i])},Corporation.prototype.getTargetSharePrice=function(){return this.determineValuation()/(2*(this.totalShares-this.numShares)+1)},Corporation.prototype.updateSharePrice=function(){const e=this.getTargetSharePrice();this.sharePrice<=e?this.sharePrice*=1+.01*Math.random():this.sharePrice*=1-.01*Math.random(),this.sharePrice<=.01&&(this.sharePrice=.01)},Corporation.prototype.immediatelyUpdateSharePrice=function(){this.sharePrice=this.getTargetSharePrice()},Corporation.prototype.calculateShareSale=function(e){let t=e,n=this.shareSalesUntilPriceUpdate,a=this.sharePrice,r=0,i=0;const o=Math.ceil(e/SHARESPERPRICEUPDATE);if(!(isNaN(o)||o>1e7)){for(let e=0;e3600?`${Math.floor(t/3600)} hour(s)`:t>60?`${Math.floor(t/60)} minute(s)`:`${Math.floor(t)} second(s)`},Corporation.prototype.unlock=function(e){const t=e[0],n=e[1];for(;this.unlockUpgrades.length<=t;)this.unlockUpgrades.push(0);this.funds.lt(n)?Object(_utils_DialogBox__WEBPACK_IMPORTED_MODULE_19__.dialogBoxCreate)("You don't have enough funds to unlock this!"):(this.unlockUpgrades[t]=1,this.funds=this.funds.minus(n),5===t?this.dividendTaxPercentage-=5:6===t&&(this.dividendTaxPercentage-=10))},Corporation.prototype.upgrade=function(e){for(var t=e[0],n=e[1],a=e[2],r=e[3];this.upgrades.length<=t;)this.upgrades.push(0);for(;this.upgradeMultipliers.length<=t;)this.upgradeMultipliers.push(1);var i=n*Math.pow(a,this.upgrades[t]);if(this.funds.lt(i))Object(_utils_DialogBox__WEBPACK_IMPORTED_MODULE_19__.dialogBoxCreate)("You don't have enough funds to purchase this!");else if(++this.upgrades[t],this.funds=this.funds.minus(i),this.upgradeMultipliers[t]=1+this.upgrades[t]*r,1===t)for(var o=0;oh.Start&&(_.currStep-=1);f()}(),!1}),Object(c.clearEventListeners)("interactive-tutorial-next").addEventListener("click",function(){return y(),!1}),f()}function f(){if(!_.isRunning)return;const e=Object(c.clearEventListeners)("terminal-menu-link"),t=Object(c.clearEventListeners)("stats-menu-link"),n=Object(c.clearEventListeners)("active-scripts-menu-link"),r=Object(c.clearEventListeners)("hacknet-nodes-menu-link"),i=Object(c.clearEventListeners)("city-menu-link"),o=Object(c.clearEventListeners)("tutorial-menu-link");e.removeAttribute("class"),t.removeAttribute("class"),n.removeAttribute("class"),r.removeAttribute("class"),i.removeAttribute("class"),o.removeAttribute("class");const s=document.getElementById("interactive-tutorial-next");switch(_.currStep){case h.Start:a.Engine.loadTerminalContent(),v("Welcome to Bitburner, a cyberpunk-themed incremental RPG! The game takes place in a dark, dystopian future... The year is 2077...

This tutorial will show you the basics of the game. You may skip the tutorial at any time."),s.style.display="inline-block";break;case h.GoToCharacterPage:a.Engine.loadTerminalContent(),v("Let's start by heading to the Stats page. Click the Stats tab on the main navigation menu (left-hand side of the screen)"),s.style.display="none",t.setAttribute("class","flashing-button"),t.addEventListener("click",function(){return a.Engine.loadCharacterContent(),y(),!1});break;case h.CharacterPage:a.Engine.loadCharacterContent(),v("The Stats page shows a lot of important information about your progress, such as your skills, money, and bonuses. "),s.style.display="inline-block";break;case h.CharacterGoToTerminalPage:a.Engine.loadCharacterContent(),v("Let's head to your computer's terminal by clicking the Terminal tab on the main navigation menu."),s.style.display="none",e.setAttribute("class","flashing-button"),e.addEventListener("click",function(){return a.Engine.loadTerminalContent(),y(),!1});break;case h.TerminalIntro:a.Engine.loadTerminalContent(),v("The Terminal is used to interface with your home computer as well as all of the other machines around the world."),s.style.display="inline-block";break;case h.TerminalHelp:a.Engine.loadTerminalContent(),v("Let's try it out. Start by entering the help command into the Terminal (Don't forget to press Enter after typing the command)"),s.style.display="none";break;case h.TerminalLs:a.Engine.loadTerminalContent(),v("The help command displays a list of all available Terminal commands, how to use them, and a description of what they do.

Let's try another command. Enter the ls command."),s.style.display="none";break;case h.TerminalScan:a.Engine.loadTerminalContent(),v(" ls is a basic command that shows files on the computer. Right now, it shows that you have a program called NUKE.exe on your computer. We'll get to what this does later.

Using your home computer's terminal, you can connect to other machines throughout the world. Let's do that now by first entering the scan command."),s.style.display="none";break;case h.TerminalScanAnalyze1:a.Engine.loadTerminalContent(),v("The scan command shows all available network connections. In other words, it displays a list of all servers that can be connected to from your current machine. A server is identified by its hostname.

That's great and all, but there's so many servers. Which one should you go to? The scan-analyze command gives some more detailed information about servers on the network. Try it now!"),s.style.display="none";break;case h.TerminalScanAnalyze2:a.Engine.loadTerminalContent(),v("You just ran scan-analyze with a depth of one. This command shows more detailed information about each server that you can connect to (servers that are a distance of one node away).

It is also possible to run scan-analyze with a higher depth. Let's try a depth of two with the following command: scan-analyze 2."),s.style.display="none";break;case h.TerminalConnect:a.Engine.loadTerminalContent(),v("Now you can see information about all servers that are up to two nodes away, as well as figure out how to navigate to those servers through the network. You can only connect to a server that is one node away. To connect to a machine, use the connect [hostname] command.

From the results of the scan-analyze command, we can see that the n00dles server is only one node away. Let's connect so it now using: connect n00dles"),s.style.display="none";break;case h.TerminalAnalyze:a.Engine.loadTerminalContent(),v("You are now connected to another machine! What can you do now? You can hack it!

In the year 2077, currency has become digital and decentralized. People and corporations store their money on servers and computers. Using your hacking abilities, you can hack servers to steal money and gain experience.

Before you try to hack a server, you should run diagnostics using the analyze command."),s.style.display="none";break;case h.TerminalNuke:a.Engine.loadTerminalContent(),v("When the analyze command finishes running it will show useful information about hacking the server.

For this server, the required hacking skill is only 1, which means you can hack it right now. However, in order to hack a server you must first gain root access. The NUKE.exe program that we saw earlier on your home computer is a virus that will grant you root access to a machine if there are enough open ports.

The analyze results shows that there do not need to be any open ports on this machine for the NUKE virus to work, so go ahead and run the virus using the run NUKE.exe command."),s.style.display="none";break;case h.TerminalManualHack:a.Engine.loadTerminalContent(),v("You now have root access! You can hack the server using the hack command. Try doing that now."),s.style.display="none";break;case h.TerminalHackingMechanics:a.Engine.loadTerminalContent(),v("You are now attempting to hack the server. Performing a hack takes time and only has a certain percentage chance of success. This time and success chance is determined by a variety of factors, including your hacking skill and the server's security level.

If your attempt to hack the server is successful, you will steal a certain percentage of the server's total money. This percentage is affected by your hacking skill and the server's security level.

The amount of money on a server is not limitless. So, if you constantly hack a server and deplete its money, then you will encounter diminishing returns in your hacking."),s.style.display="inline-block";break;case h.TerminalCreateScript:a.Engine.loadTerminalContent(),v("Hacking is the core mechanic of the game and is necessary for progressing. However, you don't want to be hacking manually the entire time. You can automate your hacking by writing scripts!

To create a new script or edit an existing one, you can use the nano command. Scripts must end with the .script extension. Let's make a script now by entering nano n00dles.script after the hack command finishes running (Sidenote: Pressing ctrl + c will end a command like hack early)"),s.style.display="none";break;case h.TerminalTypeScript:a.Engine.loadScriptEditorContent("n00dles.script",""),v("This is the script editor. You can use it to program your scripts. Scripts are written in a simplified version of javascript. Copy and paste the following code into the script editor:

while(true) {\n  hack('n00dles');\n}
For anyone with basic programming experience, this code should be straightforward. This script will continuously hack the n00dles server.

To save and close the script editor, press the button in the bottom left, or press ctrl + b."),s.style.display="none";break;case h.TerminalFree:a.Engine.loadTerminalContent(),v("Now we'll run the script. Scripts require a certain amount of RAM to run, and can be run on any machine which you have root access to. Different servers have different amounts of RAM. You can also purchase more RAM for your home server.

To check how much RAM is available on this machine, enter the free command."),s.style.display="none";break;case h.TerminalRunScript:a.Engine.loadTerminalContent(),v("We have 4GB of free RAM on this machine, which is enough to run our script. Let's run our script using run n00dles.script."),s.style.display="none";break;case h.TerminalGoToActiveScriptsPage:a.Engine.loadTerminalContent(),v("Your script is now running! It will continuously run in the background and will automatically stop if the code ever completes (the n00dles.script will never complete because it runs an infinite loop).

These scripts can passively earn you income and hacking experience. Your scripts will also earn money and experience while you are offline, although at a slightly slower rate.

Let's check out some statistics for our running scripts by clicking the Active Scripts link in the main navigation menu."),s.style.display="none",n.setAttribute("class","flashing-button"),n.addEventListener("click",function(){return a.Engine.loadActiveScriptsContent(),y(),!1});break;case h.ActiveScriptsPage:a.Engine.loadActiveScriptsContent(),v("This page displays information about all of your scripts that are running across every server. You can use this to gauge how well your scripts are doing. Let's go back to the Terminal"),s.style.display="none",e.setAttribute("class","flashing-button"),e.addEventListener("click",function(){return a.Engine.loadTerminalContent(),y(),!1});break;case h.ActiveScriptsToTerminal:a.Engine.loadTerminalContent(),v("One last thing about scripts, each active script contains logs that detail what it's doing. We can check these logs using the tail command. Do that now for the script we just ran by typing tail n00dles.script"),s.style.display="none";break;case h.TerminalTailScript:a.Engine.loadTerminalContent(),v("The log for this script won't show much right now (it might show nothing at all) because it just started running...but check back again in a few minutes!

This covers the basics of hacking. To learn more about writing scripts, select the Tutorial link in the main navigation menu to look at the documentation. If you are an experienced JavaScript developer, I would highly suggest you check out the section on NetscriptJS/Netscript 2.0, it's faster and more powerful.

For now, let's move on to something else!"),s.style.display="inline-block";break;case h.GoToHacknetNodesPage:a.Engine.loadTerminalContent(),v("Hacking is not the only way to earn money. One other way to passively earn money is by purchasing and upgrading Hacknet Nodes. Let's go to the Hacknet page through the main navigation menu now."),s.style.display="none",r.setAttribute("class","flashing-button"),r.addEventListener("click",function(){return a.Engine.loadHacknetNodesContent(),y(),!1});break;case h.HacknetNodesIntroduction:a.Engine.loadHacknetNodesContent(),v("here you can purchase new Hacknet Nodes and upgrade your existing ones. Let's purchase a new one now."),s.style.display="none";break;case h.HacknetNodesGoToWorldPage:a.Engine.loadHacknetNodesContent(),v("You just purchased a Hacknet Node! This Hacknet Node will passively earn you money over time, both online and offline. When you get enough money, you can upgrade your newly-purchased Hacknet Node below.

Let's go to the City page through the main navigation menu."),s.style.display="none",i.setAttribute("class","flashing-button"),i.addEventListener("click",function(){return a.Engine.loadLocationContent(),y(),!1});break;case h.WorldDescription:a.Engine.loadLocationContent(),v("This page lists all of the different locations you can currently travel to. Each location has something that you can do. There's a lot of content out in the world, make sure you explore and discover!

Lastly, click on the Tutorial link in the main navigation menu."),s.style.display="none",o.setAttribute("class","flashing-button"),o.addEventListener("click",function(){return a.Engine.loadTutorialContent(),y(),!1});break;case h.TutorialPageInfo:a.Engine.loadTutorialContent(),v("This page contains a lot of different documentation about the game's content and mechanics. I know it's a lot, but I highly suggest you read (or at least skim) through this before you start playing. That's the end of the tutorial. Hope you enjoy the game!"),s.style.display="inline-block",s.innerHTML="Finish Tutorial";break;case h.End:b();break;default:throw new Error("Invalid tutorial step")}!0===_.stepIsDone[_.currStep]&&(s.style.display="inline-block")}function y(){_.currStep===h.GoToCharacterPage&&document.getElementById("stats-menu-link").removeAttribute("class"),_.currStep===h.CharacterGoToTerminalPage&&document.getElementById("terminal-menu-link").removeAttribute("class"),_.currStep===h.TerminalGoToActiveScriptsPage&&document.getElementById("active-scripts-menu-link").removeAttribute("class"),_.currStep===h.ActiveScriptsPage&&document.getElementById("terminal-menu-link").removeAttribute("class"),_.currStep===h.GoToHacknetNodesPage&&document.getElementById("hacknet-nodes-menu-link").removeAttribute("class"),_.currStep===h.HacknetNodesGoToWorldPage&&document.getElementById("city-menu-link").removeAttribute("class"),_.currStep===h.WorldDescription&&document.getElementById("tutorial-menu-link").removeAttribute("class"),_.stepIsDone[_.currStep]=!0,_.currStep
Getting Started GuideDocumentation

The Beginner's Guide to Hacking was added to your home computer! It contains some tips/pointers for starting out with the game. To read it, go to Terminal and enter

cat "+s.LiteratureNames.HackersStartingHandbook}),n=Object(u.createElement)("a",{class:"a-link-button",float:"right",padding:"6px",innerText:"Got it!",clickListener:()=>{Object(p.removeElementById)(e)}});Object(m.createPopup)(e,[t,n]),r.Player.getHomeComputer().messages.push(s.LiteratureNames.HackersStartingHandbook)}let E=null;function v(e){E.innerHTML=e,E.parentElement.scrollTop=0}!function(){document.addEventListener("DOMContentLoaded",function e(){E=document.getElementById("interactive-tutorial-text"),document.removeEventListener("DOMContentLoaded",e)})}()},,function(e,t,n){"use strict";var a=this&&this.__createBinding||(Object.create?function(e,t,n,a){void 0===a&&(a=n),Object.defineProperty(e,a,{enumerable:!0,get:function(){return t[n]}})}:function(e,t,n,a){void 0===a&&(a=n),e[a]=t[n]}),r=this&&this.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:!0,value:t})}:function(e,t){e.default=t}),i=this&&this.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var n in e)"default"!==n&&Object.prototype.hasOwnProperty.call(e,n)&&a(t,e,n);return r(t,e),t};Object.defineProperty(t,"__esModule",{value:!0}),t.displayStockMarketContent=t.processStockPrices=t.stockMarketCycle=t.initSymbolToStockMap=t.initStockMarket=t.deleteStockMarket=t.loadStockMarket=t.cancelOrder=t.placeOrder=t.SymbolToStockMap=t.StockMarket=void 0;const o=n(182),s=n(1080),l=n(1079),c=n(199),u=n(1078),m=n(1077),p=n(115),d=n(103),h=n(299),_=n(1076),g=n(11),f=n(1),y=n(508),b=n(19),E=n(3),v=n(12),C=n(28),k=i(n(0)),P=i(n(22));function S(e,n,a,r,i,o=null){if(!(e instanceof c.Stock))return o?o.log("placeOrder",`Invalid stock: '${e}'`):v.dialogBoxCreate("ERROR: Invalid stock passed to placeOrder() function"),!1;if("number"!=typeof n||"number"!=typeof a)return o?o.log("placeOrder",`Invalid arguments: shares='${n}' price='${a}'`):v.dialogBoxCreate("ERROR: Invalid numeric value provided for either 'shares' or 'price' argument"),!1;const u=new s.Order(e.symbol,n,a,r,i);if(null==t.StockMarket.Orders){const e={};for(const n in t.StockMarket){const a=t.StockMarket[n];a instanceof c.Stock&&(e[a.symbol]=[])}t.StockMarket.Orders=e}t.StockMarket.Orders[e.symbol].push(u);const m={rerenderFn:D,stockMarket:t.StockMarket,symbolToStockMap:t.SymbolToStockMap};return l.processOrders(e,u.type,u.pos,m),D(),!0}function O(e,n=null){if(null==t.StockMarket.Orders)return!1;if(e.order&&e.order instanceof s.Order){const n=e.order,a=t.StockMarket.Orders[n.stockSymbol];for(let e=0;e=n.cap&&(i=.1,n.b=!1),isNaN(i)&&(i=.5);const o=Math.random(),s={rerenderFn:D,stockMarket:t.StockMarket,symbolToStockMap:t.SymbolToStockMap};o1?1:i<0?0:i},t.calculateHackingExpGain=function(e,t){null==e.baseDifficulty&&(e.baseDifficulty=e.hackDifficulty);let n=3;return(n+=e.baseDifficulty*t.hacking_exp_mult*.3)*a.BitNodeMultipliers.HackExpGain},t.calculatePercentMoneyHacked=function(e,t){const n=(100-e.hackDifficulty)/100*((t.hacking_skill-(e.requiredHackingSkill-1))/t.hacking_skill)*t.hacking_money_mult/240;return n<0?0:n>1?1:n*a.BitNodeMultipliers.ScriptHackMoney},t.calculateHackingTime=i,t.calculateGrowTime=function(e,t){return 3.2*i(e,t)},t.calculateWeakenTime=function(e,t){return 4*i(e,t)}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.CompanyPositions=void 0;const a=n(1153),r=n(301);t.CompanyPositions={},a.companyPositionMetadata.forEach(e=>{!function(e){null!=t.CompanyPositions[e.name]&&console.warn(`Duplicate Company Position being defined: ${e.name}`),t.CompanyPositions[e.name]=new r.CompanyPosition(e)}(e)})},function(e,t,n){"use strict";var a=this&&this.__createBinding||(Object.create?function(e,t,n,a){void 0===a&&(a=n),Object.defineProperty(e,a,{enumerable:!0,get:function(){return t[n]}})}:function(e,t,n,a){void 0===a&&(a=n),e[a]=t[n]}),r=this&&this.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:!0,value:t})}:function(e,t){e.default=t}),i=this&&this.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var n in e)"default"!==n&&Object.prototype.hasOwnProperty.call(e,n)&&a(t,e,n);return r(t,e),t};Object.defineProperty(t,"__esModule",{value:!0}),t.StdButton=void 0;const o=i(n(0));t.StdButton=function(e){const t=null!=e.tooltip&&""!==e.tooltip;let n,a=e.disabled?"std-button-disabled":"std-button";if(t&&(a+=" tooltip"),"string"==typeof e.addClasses&&(a+=` ${e.addClasses}`),t)if("string"==typeof e.tooltip){const t={__html:e.tooltip};n=o.createElement("span",{className:"tooltiptext",dangerouslySetInnerHTML:t})}else n=o.createElement("span",{className:"tooltiptext"},e.tooltip);return o.createElement("button",{className:a,id:e.id,onClick:e.onClick,style:e.style},e.text,t&&n)}},,,function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.isString=void 0,t.isString=function(e){return"string"==typeof e||e instanceof String}},,function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.PartTimeCompanyPositions=t.BusinessConsultantCompanyPositions=t.SoftwareConsultantCompanyPositions=t.MiscCompanyPositions=t.AgentCompanyPositions=t.SecurityCompanyPositions=t.BusinessCompanyPositions=t.NetworkEngineerCompanyPositions=t.SecurityEngineerCompanyPositions=t.ITCompanyPositions=t.SoftwareCompanyPositions=void 0,t.SoftwareCompanyPositions=["Software Engineering Intern","Junior Software Engineer","Senior Software Engineer","Lead Software Developer","Head of Software","Head of Engineering","Vice President of Technology","Chief Technology Officer"],t.ITCompanyPositions=["IT Intern","IT Analyst","IT Manager","Systems Administrator"],t.SecurityEngineerCompanyPositions=["Security Engineer"],t.NetworkEngineerCompanyPositions=["Network Engineer","Network Administrator"],t.BusinessCompanyPositions=["Business Intern","Business Analyst","Business Manager","Operations Manager","Chief Financial Officer","Chief Executive Officer"],t.SecurityCompanyPositions=["Police Officer","Police Chief","Security Guard","Security Officer","Security Supervisor","Head of Security"],t.AgentCompanyPositions=["Field Agent","Secret Agent","Special Operative"],t.MiscCompanyPositions=["Waiter","Employee"],t.SoftwareConsultantCompanyPositions=["Software Consultant","Senior Software Consultant"],t.BusinessConsultantCompanyPositions=["Business Consultant","Senior Business Consultant"],t.PartTimeCompanyPositions=["Part-time Waiter","Part-time Employee"]},,function(e,t,n){"use strict";var a=this&&this.__createBinding||(Object.create?function(e,t,n,a){void 0===a&&(a=n),Object.defineProperty(e,a,{enumerable:!0,get:function(){return t[n]}})}:function(e,t,n,a){void 0===a&&(a=n),e[a]=t[n]}),r=this&&this.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:!0,value:t})}:function(e,t){e.default=t}),i=this&&this.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var n in e)"default"!==n&&Object.prototype.hasOwnProperty.call(e,n)&&a(t,e,n);return r(t,e),t};Object.defineProperty(t,"__esModule",{value:!0}),t.Reputation=void 0;const o=i(n(0)),s=n(3);t.Reputation=function(e){return o.createElement("span",{className:"reputation samefont"},"number"==typeof e?s.numeralWrapper.formatReputation(e):e)}},,function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.createPopupCloseButton=void 0;const a=n(13),r=n(172);t.createPopupCloseButton=function(e,t){const n=a.createElement("button",{class:t.class?t.class:"popup-box-button",display:t.display?t.display:"inline-block",innerText:null==t.innerText?"Cancel":t.innerText});function i(e){27===e.keyCode&&n.click()}return n.addEventListener("click",()=>{if(e instanceof Element)r.removeElement(e);else try{const t=document.getElementById(e);t instanceof Element&&r.removeElement(t)}catch(e){console.error(`createPopupCloseButton() threw: ${e}`)}return document.removeEventListener("keydown",i),!1}),document.addEventListener("keydown",i),n}},,,function(e,t,n){"use strict";n.d(t,"a",function(){return i});var a=n(0);const r=n.n(a).a.Component;class i extends r{corp(){return this.props.corp}eventHandler(){return this.props.eventHandler}routing(){return this.props.routing}}},,function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.isScriptFilename=void 0,t.isScriptFilename=function(e){return e.endsWith(".js")||e.endsWith(".script")||e.endsWith(".ns")}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.substituteAliases=t.removeAlias=t.parseAliasDeclaration=t.printAliases=t.loadGlobalAliases=t.loadAliases=t.GlobalAliases=t.Aliases=void 0;const a=n(7);function r(e){return t.Aliases.hasOwnProperty(e)?t.Aliases[e]:null}function i(e){return t.GlobalAliases.hasOwnProperty(e)?t.GlobalAliases[e]:null}t.Aliases={},t.GlobalAliases={},t.loadAliases=function(e){t.Aliases=""===e?{}:JSON.parse(e)},t.loadGlobalAliases=function(e){t.GlobalAliases=""===e?{}:JSON.parse(e)},t.printAliases=function(){for(const e in t.Aliases)t.Aliases.hasOwnProperty(e)&&a.post("alias "+e+"="+t.Aliases[e]);for(const e in t.GlobalAliases)t.GlobalAliases.hasOwnProperty(e)&&a.post("global alias "+e+"="+t.GlobalAliases[e])},t.parseAliasDeclaration=function(e,n=!1){const a=e.match(/^([_|\w|!|%|,|@]+)="(.+)"$/);return null!=a&&3==a.length&&(n?function(e,n){e in t.Aliases&&delete t.Aliases[e],t.GlobalAliases[e]=n.trim()}(a[1],a[2]):function(e,n){e in t.GlobalAliases&&delete t.GlobalAliases[e],t.Aliases[e]=n.trim()}(a[1],a[2]),!0)},t.removeAlias=function(e){return t.Aliases.hasOwnProperty(e)?(delete t.Aliases[e],!0):!!t.GlobalAliases.hasOwnProperty(e)&&(delete t.GlobalAliases[e],!0)},t.substituteAliases=function(e){var t,n;const a=e.split(" ");if(a.length>0){if("unalias"===a[0]||"alias"===a[0])return a.join(" ");let e=!0,o=0;for(;e&&o<10;){o++,e=!1;const s=null===(t=r(a[0]))||void 0===t?void 0:t.split(" ");null!=s&&(e=!0,a.splice(0,1,...s));for(let t=0;t{t.delay=null,n()},e),t.delayResolve=n})}function s(e,t,n=null){var a="";null!=n&&(a=" (Line "+function(e,t){var n=t.scriptRef.codeCode();try{return((n=n.substring(0,e.start)).match(/\n/g)||[]).length+1}catch(e){return-1}}(n,e)+")");const r=i.AllServers[e.serverIp];if(null==r)throw new Error(`WorkerScript constructed with invalid server ip: ${this.serverIp}`);return"|"+r.hostname+"|"+e.name+"|"+t+a}function l(e,t,n){const a=e.scriptRef.threads;if(!n)return isNaN(a)||a<1?1:a;const r=0|n;if(isNaN(n)||r<1)throw s(e,`Invalid thread count passed to ${t}: ${n}. Threads must be a positive number.`);if(n>a)throw s(e,`Too many threads requested by ${t}. Requested: ${n}. Has: ${a}.`);return r}function c(e){if(!Object(r.isString)(e))return!1;return 4==e.split("|").length}},,function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.killWorkerScript=void 0;const a=n(257),r=n(116),i=n(154),o=n(203),s=n(23),l=n(204),c=n(306);function u(e,t=!0){const n=r.workerScripts.get(e);return n instanceof a.WorkerScript&&(m(n,t),!0)}function m(e,t=!0){e.env.stopFlag=!0,function(e){e instanceof a.WorkerScript&&e.delay&&(clearTimeout(e.delay),e.delayResolve&&e.delayResolve())}(e),function(e,t=!0){if(!(e instanceof a.WorkerScript))return console.error("Invalid argument passed into removeWorkerScript():"),void console.error(e);{const n=e.serverIp,a=e.name,o=s.AllServers[n];if(null==o)return void console.error(`Could not find server on which this script is running: ${n}`);o.ramUsed=c.roundToTwo(o.ramUsed-e.ramUsage),o.ramUsed<0&&(console.warn(`Server (${o.hostname}) RAM usage went negative (if it's due to floating pt imprecision, it's okay): ${o.ramUsed}`),o.ramUsed=0);for(let t=0;t0)for(let n=0;n["+(_Fconf_FconfSettings__WEBPACK_IMPORTED_MODULE_12__.FconfSettings.ENABLE_TIMESTAMPS?Object(_utils_helpers_getTimestamp__WEBPACK_IMPORTED_MODULE_37__.getTimestamp)()+" ":"")+_Player__WEBPACK_IMPORTED_MODULE_22__.Player.getCurrentServer().hostname+` ~${n}]> ${t}`),t.length>0&&(Terminal.resetTerminalInput(),Terminal.executeCommands(t))}if(e.keyCode===_utils_helpers_keyCodes__WEBPACK_IMPORTED_MODULE_35__.KEY.C&&e.ctrlKey&&(_engine__WEBPACK_IMPORTED_MODULE_11__.Engine._actionInProgress?(Object(_ui_postToTerminal__WEBPACK_IMPORTED_MODULE_40__.post)("Cancelling..."),_engine__WEBPACK_IMPORTED_MODULE_11__.Engine._actionInProgress=!1,Terminal.finishAction(!0)):_Fconf_FconfSettings__WEBPACK_IMPORTED_MODULE_12__.FconfSettings.ENABLE_BASH_HOTKEYS&&Terminal.resetTerminalInput()),e.keyCode===_utils_helpers_keyCodes__WEBPACK_IMPORTED_MODULE_35__.KEY.L&&e.ctrlKey&&(e.preventDefault(),Terminal.executeCommand("clear")),e.keyCode===_utils_helpers_keyCodes__WEBPACK_IMPORTED_MODULE_35__.KEY.UPARROW||_Fconf_FconfSettings__WEBPACK_IMPORTED_MODULE_12__.FconfSettings.ENABLE_BASH_HOTKEYS&&e.keyCode===_utils_helpers_keyCodes__WEBPACK_IMPORTED_MODULE_35__.KEY.P&&e.ctrlKey){if(_Fconf_FconfSettings__WEBPACK_IMPORTED_MODULE_12__.FconfSettings.ENABLE_BASH_HOTKEYS&&e.preventDefault(),null==t)return;var n=Terminal.commandHistoryIndex;if(0==(r=Terminal.commandHistory.length))return;(n<0||n>r)&&(Terminal.commandHistoryIndex=r),0!=n&&--Terminal.commandHistoryIndex;var a=Terminal.commandHistory[Terminal.commandHistoryIndex];t.value=a,Object(_utils_SetTimeoutRef__WEBPACK_IMPORTED_MODULE_32__.setTimeoutRef)(function(){t.selectionStart=t.selectionEnd=1e4},10)}if(e.keyCode===_utils_helpers_keyCodes__WEBPACK_IMPORTED_MODULE_35__.KEY.DOWNARROW||_Fconf_FconfSettings__WEBPACK_IMPORTED_MODULE_12__.FconfSettings.ENABLE_BASH_HOTKEYS&&e.keyCode===_utils_helpers_keyCodes__WEBPACK_IMPORTED_MODULE_35__.KEY.M&&e.ctrlKey){if(_Fconf_FconfSettings__WEBPACK_IMPORTED_MODULE_12__.FconfSettings.ENABLE_BASH_HOTKEYS&&e.preventDefault(),null==t)return;var r;n=Terminal.commandHistoryIndex;if(0==(r=Terminal.commandHistory.length))return;if((n<0||n>r)&&(Terminal.commandHistoryIndex=r),n==r||n==r-1)Terminal.commandHistoryIndex=r,t.value="";else{++Terminal.commandHistoryIndex;a=Terminal.commandHistory[Terminal.commandHistoryIndex];t.value=a}}if(e.keyCode===_utils_helpers_keyCodes__WEBPACK_IMPORTED_MODULE_35__.KEY.TAB){if(e.preventDefault(),null==t)return;let n=t.value;if(""==n)return;const a=n.lastIndexOf(";");-1!==a&&(n=n.slice(a+1));const r=(n=(n=n.trim()).replace(/\s\s+/g," ")).split(" ");let i=r.length-2;i<-1&&(i=0);const o=Object(_Terminal_determineAllPossibilitiesForTabCompletion__WEBPACK_IMPORTED_MODULE_1__.determineAllPossibilitiesForTabCompletion)(_Player__WEBPACK_IMPORTED_MODULE_22__.Player,n,i,Terminal.currDir);if(0==o.length)return;let s="",l="";if(0==r.length)return;1==r.length?l=r[0]:2==r.length?(l=r[0],s=r[1]):3==r.length?(l=r[0]+" "+r[1],s=r[2]):(s=r.pop(),l=r.join(" ")),Object(_Terminal_tabCompletion__WEBPACK_IMPORTED_MODULE_3__.tabCompletion)(l,s,o),t.focus()}_Fconf_FconfSettings__WEBPACK_IMPORTED_MODULE_12__.FconfSettings.ENABLE_BASH_HOTKEYS&&(e.keyCode===_utils_helpers_keyCodes__WEBPACK_IMPORTED_MODULE_35__.KEY.A&&e.ctrlKey&&(e.preventDefault(),Terminal.moveTextCursor("home")),e.keyCode===_utils_helpers_keyCodes__WEBPACK_IMPORTED_MODULE_35__.KEY.E&&e.ctrlKey&&(e.preventDefault(),Terminal.moveTextCursor("end")),e.keyCode===_utils_helpers_keyCodes__WEBPACK_IMPORTED_MODULE_35__.KEY.B&&e.ctrlKey&&(e.preventDefault(),Terminal.moveTextCursor("prevchar")),e.keyCode===_utils_helpers_keyCodes__WEBPACK_IMPORTED_MODULE_35__.KEY.B&&e.altKey&&(e.preventDefault(),Terminal.moveTextCursor("prevword")),e.keyCode===_utils_helpers_keyCodes__WEBPACK_IMPORTED_MODULE_35__.KEY.F&&e.ctrlKey&&(e.preventDefault(),Terminal.moveTextCursor("nextchar")),e.keyCode===_utils_helpers_keyCodes__WEBPACK_IMPORTED_MODULE_35__.KEY.F&&e.altKey&&(e.preventDefault(),Terminal.moveTextCursor("nextword")),e.keyCode!==_utils_helpers_keyCodes__WEBPACK_IMPORTED_MODULE_35__.KEY.H&&e.keyCode!==_utils_helpers_keyCodes__WEBPACK_IMPORTED_MODULE_35__.KEY.D||!e.ctrlKey||(Terminal.modifyInput("backspace"),e.preventDefault()))}});let terminalCtrlPressed=!1,shiftKeyPressed=!1;$(document).ready(function(){_ui_navigationTracking__WEBPACK_IMPORTED_MODULE_33__.routing.isOn(_ui_navigationTracking__WEBPACK_IMPORTED_MODULE_33__.Page.Terminal)&&$(".terminal-input").focus()}),$(document).keydown(function(e){if(_ui_navigationTracking__WEBPACK_IMPORTED_MODULE_33__.routing.isOn(_ui_navigationTracking__WEBPACK_IMPORTED_MODULE_33__.Page.Terminal))if(e.which==_utils_helpers_keyCodes__WEBPACK_IMPORTED_MODULE_35__.KEY.CTRL)terminalCtrlPressed=!0;else if(e.shiftKey)shiftKeyPressed=!0;else if(terminalCtrlPressed||shiftKeyPressed||Terminal.contractOpen);else{var t=document.getElementById("terminal-input-text-box");null!=t&&t.focus(),terminalCtrlPressed=!1,shiftKeyPressed=!1}}),$(document).keyup(function(e){_ui_navigationTracking__WEBPACK_IMPORTED_MODULE_33__.routing.isOn(_ui_navigationTracking__WEBPACK_IMPORTED_MODULE_33__.Page.Terminal)&&(e.which==_utils_helpers_keyCodes__WEBPACK_IMPORTED_MODULE_35__.KEY.CTRL&&(terminalCtrlPressed=!1),e.shiftKey&&(shiftKeyPressed=!1))});let Terminal={hackFlag:!1,backdoorFlag:!1,analyzeFlag:!1,actionStarted:!1,actionTime:0,commandHistory:[],commandHistoryIndex:0,contractOpen:!1,currDir:"/",resetTerminalInput:function(e=!1){let t="";e&&(t=getTerminalInput());const n=Terminal.currDir;_Fconf_FconfSettings__WEBPACK_IMPORTED_MODULE_12__.FconfSettings.WRAP_INPUT?(document.getElementById("terminal-input-td").innerHTML=`
[${_Player__WEBPACK_IMPORTED_MODULE_22__.Player.getCurrentServer().hostname} ~${n}]$
`+`",h.noCloneChecked=!!t.cloneNode(!0).lastChild.defaultValue,t.innerHTML="",h.option=!!t.lastChild}();var ht={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function mt(t,e){var n;return n=void 0!==t.getElementsByTagName?t.getElementsByTagName(e||"*"):void 0!==t.querySelectorAll?t.querySelectorAll(e||"*"):[],void 0===e||e&&C(t,e)?H.merge([t],n):n}function vt(t,e){for(var n=0,r=t.length;n",""]);var gt=/<|&#?\w+;/;function yt(t,e,n,r,i){for(var o,a,s,u,l,c,Q=e.createDocumentFragment(),T=[],f=0,d=t.length;f-1)i&&i.push(o);else if(l=at(o),a=mt(Q.appendChild(o),"script"),l&&vt(a),n)for(c=0;o=a[c++];)pt.test(o.type||"")&&n.push(o);return Q}var bt=/^key/,Lt=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,Ht=/^([^.]*)(?:\.(.+)|)/;function xt(){return!0}function _t(){return!1}function Et(t,e){return t===function(){try{return g.activeElement}catch(t){}}()==("focus"===e)}function wt(t,e,n,r,i,o){var a,s;if("object"==typeof e){for(s in"string"!=typeof n&&(r=r||n,n=void 0),e)wt(t,s,n,r,e[s],o);return t}if(null==r&&null==i?(i=n,r=n=void 0):null==i&&("string"==typeof n?(i=r,r=void 0):(i=r,r=n,n=void 0)),!1===i)i=_t;else if(!i)return t;return 1===o&&(a=i,(i=function(t){return H().off(t),a.apply(this,arguments)}).guid=a.guid||(a.guid=H.guid++)),t.each(function(){H.event.add(this,e,i,r,n)})}function Ot(t,e,n){n?($.set(t,e,!1),H.event.add(t,e,{namespace:!1,handler:function(t){var r,i,o=$.get(this,e);if(1&t.isTrigger&&this[e]){if(o.length)(H.event.special[e]||{}).delegateType&&t.stopPropagation();else if(o=s.call(arguments),$.set(this,e,o),r=n(this,e),this[e](),o!==(i=$.get(this,e))||r?$.set(this,e,!1):i={},o!==i)return t.stopImmediatePropagation(),t.preventDefault(),i.value}else o.length&&($.set(this,e,{value:H.event.trigger(H.extend(o[0],H.Event.prototype),o.slice(1),this)}),t.stopImmediatePropagation())}})):void 0===$.get(t,e)&&H.event.add(t,e,xt)}H.event={global:{},add:function(t,e,n,r,i){var o,a,s,u,l,c,Q,T,f,d,p,h=$.get(t);if(q(t))for(n.handler&&(n=(o=n).handler,i=o.selector),i&&H.find.matchesSelector(ot,i),n.guid||(n.guid=H.guid++),(u=h.events)||(u=h.events=Object.create(null)),(a=h.handle)||(a=h.handle=function(e){return void 0!==H&&H.event.triggered!==e.type?H.event.dispatch.apply(t,arguments):void 0}),l=(e=(e||"").match(j)||[""]).length;l--;)f=p=(s=Ht.exec(e[l])||[])[1],d=(s[2]||"").split(".").sort(),f&&(Q=H.event.special[f]||{},f=(i?Q.delegateType:Q.bindType)||f,Q=H.event.special[f]||{},c=H.extend({type:f,origType:p,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&H.expr.match.needsContext.test(i),namespace:d.join(".")},o),(T=u[f])||((T=u[f]=[]).delegateCount=0,Q.setup&&!1!==Q.setup.call(t,r,d,a)||t.addEventListener&&t.addEventListener(f,a)),Q.add&&(Q.add.call(t,c),c.handler.guid||(c.handler.guid=n.guid)),i?T.splice(T.delegateCount++,0,c):T.push(c),H.event.global[f]=!0)},remove:function(t,e,n,r,i){var o,a,s,u,l,c,Q,T,f,d,p,h=$.hasData(t)&&$.get(t);if(h&&(u=h.events)){for(l=(e=(e||"").match(j)||[""]).length;l--;)if(f=p=(s=Ht.exec(e[l])||[])[1],d=(s[2]||"").split(".").sort(),f){for(Q=H.event.special[f]||{},T=u[f=(r?Q.delegateType:Q.bindType)||f]||[],s=s[2]&&new RegExp("(^|\\.)"+d.join("\\.(?:.*\\.|)")+"(\\.|$)"),a=o=T.length;o--;)c=T[o],!i&&p!==c.origType||n&&n.guid!==c.guid||s&&!s.test(c.namespace)||r&&r!==c.selector&&("**"!==r||!c.selector)||(T.splice(o,1),c.selector&&T.delegateCount--,Q.remove&&Q.remove.call(t,c));a&&!T.length&&(Q.teardown&&!1!==Q.teardown.call(t,d,h.handle)||H.removeEvent(t,f,h.handle),delete u[f])}else for(f in u)H.event.remove(t,f+e[l],n,r,!0);H.isEmptyObject(u)&&$.remove(t,"handle events")}},dispatch:function(t){var e,n,r,i,o,a,s=new Array(arguments.length),u=H.event.fix(t),l=($.get(this,"events")||Object.create(null))[u.type]||[],c=H.event.special[u.type]||{};for(s[0]=u,e=1;e=1))for(;l!==this;l=l.parentNode||this)if(1===l.nodeType&&("click"!==t.type||!0!==l.disabled)){for(o=[],a={},n=0;n-1:H.find(i,this,null,[l]).length),a[i]&&o.push(r);o.length&&s.push({elem:l,handlers:o})}return l=this,u\s*$/g;function Vt(t,e){return C(t,"table")&&C(11!==e.nodeType?e:e.firstChild,"tr")&&H(t).children("tbody")[0]||t}function At(t){return t.type=(null!==t.getAttribute("type"))+"/"+t.type,t}function kt(t){return"true/"===(t.type||"").slice(0,5)?t.type=t.type.slice(5):t.removeAttribute("type"),t}function Dt(t,e){var n,r,i,o,a,s;if(1===e.nodeType){if($.hasData(t)&&(s=$.get(t).events))for(i in $.remove(e,"handle events"),s)for(n=0,r=s[i].length;n1&&"string"==typeof d&&!h.checkClone&&St.test(d))return t.each(function(i){var o=t.eq(i);p&&(e[0]=d.call(this,i,o.html())),jt(o,e,n,r)});if(T&&(o=(i=yt(e,t[0].ownerDocument,!1,t,r)).firstChild,1===i.childNodes.length&&(i=o),o||r)){for(s=(a=H.map(mt(i,"script"),At)).length;Q0&&vt(a,!u&&mt(t,"script")),s},cleanData:function(t){for(var e,n,r,i=H.event.special,o=0;void 0!==(n=t[o]);o++)if(q(n)){if(e=n[$.expando]){if(e.events)for(r in e.events)i[r]?H.event.remove(n,r):H.removeEvent(n,r,e.handle);n[$.expando]=void 0}n[Y.expando]&&(n[Y.expando]=void 0)}}}),H.fn.extend({detach:function(t){return Rt(this,t,!0)},remove:function(t){return Rt(this,t)},text:function(t){return z(this,function(t){return void 0===t?H.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=t)})},null,t,arguments.length)},append:function(){return jt(this,arguments,function(t){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||Vt(this,t).appendChild(t)})},prepend:function(){return jt(this,arguments,function(t){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var e=Vt(this,t);e.insertBefore(t,e.firstChild)}})},before:function(){return jt(this,arguments,function(t){this.parentNode&&this.parentNode.insertBefore(t,this)})},after:function(){return jt(this,arguments,function(t){this.parentNode&&this.parentNode.insertBefore(t,this.nextSibling)})},empty:function(){for(var t,e=0;null!=(t=this[e]);e++)1===t.nodeType&&(H.cleanData(mt(t,!1)),t.textContent="");return this},clone:function(t,e){return t=null!=t&&t,e=null==e?t:e,this.map(function(){return H.clone(this,t,e)})},html:function(t){return z(this,function(t){var e=this[0]||{},n=0,r=this.length;if(void 0===t&&1===e.nodeType)return e.innerHTML;if("string"==typeof t&&!Ct.test(t)&&!ht[(dt.exec(t)||["",""])[1].toLowerCase()]){t=H.htmlPrefilter(t);try{for(;n3,ot.removeChild(t)),s}}))}();var Wt=["Webkit","Moz","ms"],Ut=g.createElement("div").style,Gt={};function Xt(t){var e=H.cssProps[t]||Gt[t];return e||(t in Ut?t:Gt[t]=function(t){for(var e=t[0].toUpperCase()+t.slice(1),n=Wt.length;n--;)if((t=Wt[n]+e)in Ut)return t}(t)||t)}var qt=/^(none|table(?!-c[ea]).+)/,Kt=/^--/,$t={position:"absolute",visibility:"hidden",display:"block"},Yt={letterSpacing:"0",fontWeight:"400"};function Jt(t,e,n){var r=rt.exec(e);return r?Math.max(0,r[2]-(n||0))+(r[3]||"px"):e}function te(t,e,n,r,i,o){var a="width"===e?1:0,s=0,u=0;if(n===(r?"border":"content"))return 0;for(;a<4;a+=2)"margin"===n&&(u+=H.css(t,n+it[a],!0,i)),r?("content"===n&&(u-=H.css(t,"padding"+it[a],!0,i)),"margin"!==n&&(u-=H.css(t,"border"+it[a]+"Width",!0,i))):(u+=H.css(t,"padding"+it[a],!0,i),"padding"!==n?u+=H.css(t,"border"+it[a]+"Width",!0,i):s+=H.css(t,"border"+it[a]+"Width",!0,i));return!r&&o>=0&&(u+=Math.max(0,Math.ceil(t["offset"+e[0].toUpperCase()+e.slice(1)]-o-u-s-.5))||0),u}function ee(t,e,n){var r=It(t),i=(!h.boxSizingReliable()||n)&&"border-box"===H.css(t,"boxSizing",!1,r),o=i,a=Zt(t,e,r),s="offset"+e[0].toUpperCase()+e.slice(1);if(Nt.test(a)){if(!n)return a;a="auto"}return(!h.boxSizingReliable()&&i||!h.reliableTrDimensions()&&C(t,"tr")||"auto"===a||!parseFloat(a)&&"inline"===H.css(t,"display",!1,r))&&t.getClientRects().length&&(i="border-box"===H.css(t,"boxSizing",!1,r),(o=s in t)&&(a=t[s])),(a=parseFloat(a)||0)+te(t,e,n||(i?"border":"content"),o,r,a)+"px"}function ne(t,e,n,r,i){return new ne.prototype.init(t,e,n,r,i)}H.extend({cssHooks:{opacity:{get:function(t,e){if(e){var n=Zt(t,"opacity");return""===n?"1":n}}}},cssNumber:{animationIterationCount:!0,columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,gridArea:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnStart:!0,gridRow:!0,gridRowEnd:!0,gridRowStart:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{},style:function(t,e,n,r){if(t&&3!==t.nodeType&&8!==t.nodeType&&t.style){var i,o,a,s=X(e),u=Kt.test(e),l=t.style;if(u||(e=Xt(s)),a=H.cssHooks[e]||H.cssHooks[s],void 0===n)return a&&"get"in a&&void 0!==(i=a.get(t,!1,r))?i:l[e];"string"===(o=typeof n)&&(i=rt.exec(n))&&i[1]&&(n=lt(t,e,i),o="number"),null!=n&&n==n&&("number"!==o||u||(n+=i&&i[3]||(H.cssNumber[s]?"":"px")),h.clearCloneStyle||""!==n||0!==e.indexOf("background")||(l[e]="inherit"),a&&"set"in a&&void 0===(n=a.set(t,n,r))||(u?l.setProperty(e,n):l[e]=n))}},css:function(t,e,n,r){var i,o,a,s=X(e);return Kt.test(e)||(e=Xt(s)),(a=H.cssHooks[e]||H.cssHooks[s])&&"get"in a&&(i=a.get(t,!0,n)),void 0===i&&(i=Zt(t,e,r)),"normal"===i&&e in Yt&&(i=Yt[e]),""===n||n?(o=parseFloat(i),!0===n||isFinite(o)?o||0:i):i}}),H.each(["height","width"],function(t,e){H.cssHooks[e]={get:function(t,n,r){if(n)return!qt.test(H.css(t,"display"))||t.getClientRects().length&&t.getBoundingClientRect().width?ee(t,e,r):Bt(t,$t,function(){return ee(t,e,r)})},set:function(t,n,r){var i,o=It(t),a=!h.scrollboxSize()&&"absolute"===o.position,s=(a||r)&&"border-box"===H.css(t,"boxSizing",!1,o),u=r?te(t,e,r,s,o):0;return s&&a&&(u-=Math.ceil(t["offset"+e[0].toUpperCase()+e.slice(1)]-parseFloat(o[e])-te(t,e,"border",!1,o)-.5)),u&&(i=rt.exec(n))&&"px"!==(i[3]||"px")&&(t.style[e]=n,n=H.css(t,e)),Jt(0,n,u)}}}),H.cssHooks.marginLeft=zt(h.reliableMarginLeft,function(t,e){if(e)return(parseFloat(Zt(t,"marginLeft"))||t.getBoundingClientRect().left-Bt(t,{marginLeft:0},function(){return t.getBoundingClientRect().left}))+"px"}),H.each({margin:"",padding:"",border:"Width"},function(t,e){H.cssHooks[t+e]={expand:function(n){for(var r=0,i={},o="string"==typeof n?n.split(" "):[n];r<4;r++)i[t+it[r]+e]=o[r]||o[r-2]||o[0];return i}},"margin"!==t&&(H.cssHooks[t+e].set=Jt)}),H.fn.extend({css:function(t,e){return z(this,function(t,e,n){var r,i,o={},a=0;if(Array.isArray(e)){for(r=It(t),i=e.length;a1)}}),H.Tween=ne,ne.prototype={constructor:ne,init:function(t,e,n,r,i,o){this.elem=t,this.prop=n,this.easing=i||H.easing._default,this.options=e,this.start=this.now=this.cur(),this.end=r,this.unit=o||(H.cssNumber[n]?"":"px")},cur:function(){var t=ne.propHooks[this.prop];return t&&t.get?t.get(this):ne.propHooks._default.get(this)},run:function(t){var e,n=ne.propHooks[this.prop];return this.options.duration?this.pos=e=H.easing[this.easing](t,this.options.duration*t,0,1,this.options.duration):this.pos=e=t,this.now=(this.end-this.start)*e+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):ne.propHooks._default.set(this),this}},ne.prototype.init.prototype=ne.prototype,ne.propHooks={_default:{get:function(t){var e;return 1!==t.elem.nodeType||null!=t.elem[t.prop]&&null==t.elem.style[t.prop]?t.elem[t.prop]:(e=H.css(t.elem,t.prop,""))&&"auto"!==e?e:0},set:function(t){H.fx.step[t.prop]?H.fx.step[t.prop](t):1!==t.elem.nodeType||!H.cssHooks[t.prop]&&null==t.elem.style[Xt(t.prop)]?t.elem[t.prop]=t.now:H.style(t.elem,t.prop,t.now+t.unit)}}},ne.propHooks.scrollTop=ne.propHooks.scrollLeft={set:function(t){t.elem.nodeType&&t.elem.parentNode&&(t.elem[t.prop]=t.now)}},H.easing={linear:function(t){return t},swing:function(t){return.5-Math.cos(t*Math.PI)/2},_default:"swing"},H.fx=ne.prototype.init,H.fx.step={};var re,ie,oe=/^(?:toggle|show|hide)$/,ae=/queueHooks$/;function se(){ie&&(!1===g.hidden&&n.requestAnimationFrame?n.requestAnimationFrame(se):n.setTimeout(se,H.fx.interval),H.fx.tick())}function ue(){return n.setTimeout(function(){re=void 0}),re=Date.now()}function le(t,e){var n,r=0,i={height:t};for(e=e?1:0;r<4;r+=2-e)i["margin"+(n=it[r])]=i["padding"+n]=t;return e&&(i.opacity=i.width=t),i}function ce(t,e,n){for(var r,i=(Qe.tweeners[e]||[]).concat(Qe.tweeners["*"]),o=0,a=i.length;o1)},removeAttr:function(t){return this.each(function(){H.removeAttr(this,t)})}}),H.extend({attr:function(t,e,n){var r,i,o=t.nodeType;if(3!==o&&8!==o&&2!==o)return void 0===t.getAttribute?H.prop(t,e,n):(1===o&&H.isXMLDoc(t)||(i=H.attrHooks[e.toLowerCase()]||(H.expr.match.bool.test(e)?Te:void 0)),void 0!==n?null===n?void H.removeAttr(t,e):i&&"set"in i&&void 0!==(r=i.set(t,n,e))?r:(t.setAttribute(e,n+""),n):i&&"get"in i&&null!==(r=i.get(t,e))?r:null==(r=H.find.attr(t,e))?void 0:r)},attrHooks:{type:{set:function(t,e){if(!h.radioValue&&"radio"===e&&C(t,"input")){var n=t.value;return t.setAttribute("type",e),n&&(t.value=n),e}}}},removeAttr:function(t,e){var n,r=0,i=e&&e.match(j);if(i&&1===t.nodeType)for(;n=i[r++];)t.removeAttribute(n)}}),Te={set:function(t,e,n){return!1===e?H.removeAttr(t,n):t.setAttribute(n,n),n}},H.each(H.expr.match.bool.source.match(/\w+/g),function(t,e){var n=fe[e]||H.find.attr;fe[e]=function(t,e,r){var i,o,a=e.toLowerCase();return r||(o=fe[a],fe[a]=i,i=null!=n(t,e,r)?a:null,fe[a]=o),i}});var de=/^(?:input|select|textarea|button)$/i,pe=/^(?:a|area)$/i;function he(t){return(t.match(j)||[]).join(" ")}function me(t){return t.getAttribute&&t.getAttribute("class")||""}function ve(t){return Array.isArray(t)?t:"string"==typeof t&&t.match(j)||[]}H.fn.extend({prop:function(t,e){return z(this,H.prop,t,e,arguments.length>1)},removeProp:function(t){return this.each(function(){delete this[H.propFix[t]||t]})}}),H.extend({prop:function(t,e,n){var r,i,o=t.nodeType;if(3!==o&&8!==o&&2!==o)return 1===o&&H.isXMLDoc(t)||(e=H.propFix[e]||e,i=H.propHooks[e]),void 0!==n?i&&"set"in i&&void 0!==(r=i.set(t,n,e))?r:t[e]=n:i&&"get"in i&&null!==(r=i.get(t,e))?r:t[e]},propHooks:{tabIndex:{get:function(t){var e=H.find.attr(t,"tabindex");return e?parseInt(e,10):de.test(t.nodeName)||pe.test(t.nodeName)&&t.href?0:-1}}},propFix:{for:"htmlFor",class:"className"}}),h.optSelected||(H.propHooks.selected={get:function(t){var e=t.parentNode;return e&&e.parentNode&&e.parentNode.selectedIndex,null},set:function(t){var e=t.parentNode;e&&(e.selectedIndex,e.parentNode&&e.parentNode.selectedIndex)}}),H.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){H.propFix[this.toLowerCase()]=this}),H.fn.extend({addClass:function(t){var e,n,r,i,o,a,s,u=0;if(m(t))return this.each(function(e){H(this).addClass(t.call(this,e,me(this)))});if((e=ve(t)).length)for(;n=this[u++];)if(i=me(n),r=1===n.nodeType&&" "+he(i)+" "){for(a=0;o=e[a++];)r.indexOf(" "+o+" ")<0&&(r+=o+" ");i!==(s=he(r))&&n.setAttribute("class",s)}return this},removeClass:function(t){var e,n,r,i,o,a,s,u=0;if(m(t))return this.each(function(e){H(this).removeClass(t.call(this,e,me(this)))});if(!arguments.length)return this.attr("class","");if((e=ve(t)).length)for(;n=this[u++];)if(i=me(n),r=1===n.nodeType&&" "+he(i)+" "){for(a=0;o=e[a++];)for(;r.indexOf(" "+o+" ")>-1;)r=r.replace(" "+o+" "," ");i!==(s=he(r))&&n.setAttribute("class",s)}return this},toggleClass:function(t,e){var n=typeof t,r="string"===n||Array.isArray(t);return"boolean"==typeof e&&r?e?this.addClass(t):this.removeClass(t):m(t)?this.each(function(n){H(this).toggleClass(t.call(this,n,me(this),e),e)}):this.each(function(){var e,i,o,a;if(r)for(i=0,o=H(this),a=ve(t);e=a[i++];)o.hasClass(e)?o.removeClass(e):o.addClass(e);else void 0!==t&&"boolean"!==n||((e=me(this))&&$.set(this,"__className__",e),this.setAttribute&&this.setAttribute("class",e||!1===t?"":$.get(this,"__className__")||""))})},hasClass:function(t){var e,n,r=0;for(e=" "+t+" ";n=this[r++];)if(1===n.nodeType&&(" "+he(me(n))+" ").indexOf(e)>-1)return!0;return!1}});var ge=/\r/g;H.fn.extend({val:function(t){var e,n,r,i=this[0];return arguments.length?(r=m(t),this.each(function(n){var i;1===this.nodeType&&(null==(i=r?t.call(this,n,H(this).val()):t)?i="":"number"==typeof i?i+="":Array.isArray(i)&&(i=H.map(i,function(t){return null==t?"":t+""})),(e=H.valHooks[this.type]||H.valHooks[this.nodeName.toLowerCase()])&&"set"in e&&void 0!==e.set(this,i,"value")||(this.value=i))})):i?(e=H.valHooks[i.type]||H.valHooks[i.nodeName.toLowerCase()])&&"get"in e&&void 0!==(n=e.get(i,"value"))?n:"string"==typeof(n=i.value)?n.replace(ge,""):null==n?"":n:void 0}}),H.extend({valHooks:{option:{get:function(t){var e=H.find.attr(t,"value");return null!=e?e:he(H.text(t))}},select:{get:function(t){var e,n,r,i=t.options,o=t.selectedIndex,a="select-one"===t.type,s=a?null:[],u=a?o+1:i.length;for(r=o<0?u:a?o:0;r-1)&&(n=!0);return n||(t.selectedIndex=-1),o}}}}),H.each(["radio","checkbox"],function(){H.valHooks[this]={set:function(t,e){if(Array.isArray(e))return t.checked=H.inArray(H(t).val(),e)>-1}},h.checkOn||(H.valHooks[this].get=function(t){return null===t.getAttribute("value")?"on":t.value})}),h.focusin="onfocusin"in n;var ye=/^(?:focusinfocus|focusoutblur)$/,be=function(t){t.stopPropagation()};H.extend(H.event,{trigger:function(t,e,r,i){var o,a,s,u,l,c,Q,T,d=[r||g],p=f.call(t,"type")?t.type:t,h=f.call(t,"namespace")?t.namespace.split("."):[];if(a=T=s=r=r||g,3!==r.nodeType&&8!==r.nodeType&&!ye.test(p+H.event.triggered)&&(p.indexOf(".")>-1&&(p=(h=p.split(".")).shift(),h.sort()),l=p.indexOf(":")<0&&"on"+p,(t=t[H.expando]?t:new H.Event(p,"object"==typeof t&&t)).isTrigger=i?2:3,t.namespace=h.join("."),t.rnamespace=t.namespace?new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,t.result=void 0,t.target||(t.target=r),e=null==e?[t]:H.makeArray(e,[t]),Q=H.event.special[p]||{},i||!Q.trigger||!1!==Q.trigger.apply(r,e))){if(!i&&!Q.noBubble&&!v(r)){for(u=Q.delegateType||p,ye.test(u+p)||(a=a.parentNode);a;a=a.parentNode)d.push(a),s=a;s===(r.ownerDocument||g)&&d.push(s.defaultView||s.parentWindow||n)}for(o=0;(a=d[o++])&&!t.isPropagationStopped();)T=a,t.type=o>1?u:Q.bindType||p,(c=($.get(a,"events")||Object.create(null))[t.type]&&$.get(a,"handle"))&&c.apply(a,e),(c=l&&a[l])&&c.apply&&q(a)&&(t.result=c.apply(a,e),!1===t.result&&t.preventDefault());return t.type=p,i||t.isDefaultPrevented()||Q._default&&!1!==Q._default.apply(d.pop(),e)||!q(r)||l&&m(r[p])&&!v(r)&&((s=r[l])&&(r[l]=null),H.event.triggered=p,t.isPropagationStopped()&&T.addEventListener(p,be),r[p](),t.isPropagationStopped()&&T.removeEventListener(p,be),H.event.triggered=void 0,s&&(r[l]=s)),t.result}},simulate:function(t,e,n){var r=H.extend(new H.Event,n,{type:t,isSimulated:!0});H.event.trigger(r,null,e)}}),H.fn.extend({trigger:function(t,e){return this.each(function(){H.event.trigger(t,e,this)})},triggerHandler:function(t,e){var n=this[0];if(n)return H.event.trigger(t,e,n,!0)}}),h.focusin||H.each({focus:"focusin",blur:"focusout"},function(t,e){var n=function(t){H.event.simulate(e,t.target,H.event.fix(t))};H.event.special[e]={setup:function(){var r=this.ownerDocument||this.document||this,i=$.access(r,e);i||r.addEventListener(t,n,!0),$.access(r,e,(i||0)+1)},teardown:function(){var r=this.ownerDocument||this.document||this,i=$.access(r,e)-1;i?$.access(r,e,i):(r.removeEventListener(t,n,!0),$.remove(r,e))}}});var Le=n.location,He={guid:Date.now()},xe=/\?/;H.parseXML=function(t){var e;if(!t||"string"!=typeof t)return null;try{e=(new n.DOMParser).parseFromString(t,"text/xml")}catch(t){e=void 0}return e&&!e.getElementsByTagName("parsererror").length||H.error("Invalid XML: "+t),e};var _e=/\[\]$/,Ee=/\r?\n/g,we=/^(?:submit|button|image|reset|file)$/i,Oe=/^(?:input|select|textarea|keygen)/i;function Ce(t,e,n,r){var i;if(Array.isArray(e))H.each(e,function(e,i){n||_e.test(t)?r(t,i):Ce(t+"["+("object"==typeof i&&null!=i?e:"")+"]",i,n,r)});else if(n||"object"!==L(e))r(t,e);else for(i in e)Ce(t+"["+i+"]",e[i],n,r)}H.param=function(t,e){var n,r=[],i=function(t,e){var n=m(e)?e():e;r[r.length]=encodeURIComponent(t)+"="+encodeURIComponent(null==n?"":n)};if(null==t)return"";if(Array.isArray(t)||t.jquery&&!H.isPlainObject(t))H.each(t,function(){i(this.name,this.value)});else for(n in t)Ce(n,t[n],e,i);return r.join("&")},H.fn.extend({serialize:function(){return H.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var t=H.prop(this,"elements");return t?H.makeArray(t):this}).filter(function(){var t=this.type;return this.name&&!H(this).is(":disabled")&&Oe.test(this.nodeName)&&!we.test(t)&&(this.checked||!ft.test(t))}).map(function(t,e){var n=H(this).val();return null==n?null:Array.isArray(n)?H.map(n,function(t){return{name:e.name,value:t.replace(Ee,"\r\n")}}):{name:e.name,value:n.replace(Ee,"\r\n")}}).get()}});var Se=/%20/g,Me=/#.*$/,Ve=/([?&])_=[^&]*/,Ae=/^(.*?):[ \t]*([^\r\n]*)$/gm,ke=/^(?:GET|HEAD)$/,De=/^\/\//,Pe={},je={},Re="*/".concat("*"),Ne=g.createElement("a");function Ie(t){return function(e,n){"string"!=typeof e&&(n=e,e="*");var r,i=0,o=e.toLowerCase().match(j)||[];if(m(n))for(;r=o[i++];)"+"===r[0]?(r=r.slice(1)||"*",(t[r]=t[r]||[]).unshift(n)):(t[r]=t[r]||[]).push(n)}}function Be(t,e,n,r){var i={},o=t===je;function a(s){var u;return i[s]=!0,H.each(t[s]||[],function(t,s){var l=s(e,n,r);return"string"!=typeof l||o||i[l]?o?!(u=l):void 0:(e.dataTypes.unshift(l),a(l),!1)}),u}return a(e.dataTypes[0])||!i["*"]&&a("*")}function Fe(t,e){var n,r,i=H.ajaxSettings.flatOptions||{};for(n in e)void 0!==e[n]&&((i[n]?t:r||(r={}))[n]=e[n]);return r&&H.extend(!0,t,r),t}Ne.href=Le.href,H.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:Le.href,type:"GET",isLocal:/^(?:about|app|app-storage|.+-extension|file|res|widget):$/.test(Le.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Re,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":H.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(t,e){return e?Fe(Fe(t,H.ajaxSettings),e):Fe(H.ajaxSettings,t)},ajaxPrefilter:Ie(Pe),ajaxTransport:Ie(je),ajax:function(t,e){"object"==typeof t&&(e=t,t=void 0),e=e||{};var r,i,o,a,s,u,l,c,Q,T,f=H.ajaxSetup({},e),d=f.context||f,p=f.context&&(d.nodeType||d.jquery)?H(d):H.event,h=H.Deferred(),m=H.Callbacks("once memory"),v=f.statusCode||{},y={},b={},L="canceled",x={readyState:0,getResponseHeader:function(t){var e;if(l){if(!a)for(a={};e=Ae.exec(o);)a[e[1].toLowerCase()+" "]=(a[e[1].toLowerCase()+" "]||[]).concat(e[2]);e=a[t.toLowerCase()+" "]}return null==e?null:e.join(", ")},getAllResponseHeaders:function(){return l?o:null},setRequestHeader:function(t,e){return null==l&&(t=b[t.toLowerCase()]=b[t.toLowerCase()]||t,y[t]=e),this},overrideMimeType:function(t){return null==l&&(f.mimeType=t),this},statusCode:function(t){var e;if(t)if(l)x.always(t[x.status]);else for(e in t)v[e]=[v[e],t[e]];return this},abort:function(t){var e=t||L;return r&&r.abort(e),_(0,e),this}};if(h.promise(x),f.url=((t||f.url||Le.href)+"").replace(De,Le.protocol+"//"),f.type=e.method||e.type||f.method||f.type,f.dataTypes=(f.dataType||"*").toLowerCase().match(j)||[""],null==f.crossDomain){u=g.createElement("a");try{u.href=f.url,u.href=u.href,f.crossDomain=Ne.protocol+"//"+Ne.host!=u.protocol+"//"+u.host}catch(t){f.crossDomain=!0}}if(f.data&&f.processData&&"string"!=typeof f.data&&(f.data=H.param(f.data,f.traditional)),Be(Pe,f,e,x),l)return x;for(Q in(c=H.event&&f.global)&&0==H.active++&&H.event.trigger("ajaxStart"),f.type=f.type.toUpperCase(),f.hasContent=!ke.test(f.type),i=f.url.replace(Me,""),f.hasContent?f.data&&f.processData&&0===(f.contentType||"").indexOf("application/x-www-form-urlencoded")&&(f.data=f.data.replace(Se,"+")):(T=f.url.slice(i.length),f.data&&(f.processData||"string"==typeof f.data)&&(i+=(xe.test(i)?"&":"?")+f.data,delete f.data),!1===f.cache&&(i=i.replace(Ve,"$1"),T=(xe.test(i)?"&":"?")+"_="+He.guid+++T),f.url=i+T),f.ifModified&&(H.lastModified[i]&&x.setRequestHeader("If-Modified-Since",H.lastModified[i]),H.etag[i]&&x.setRequestHeader("If-None-Match",H.etag[i])),(f.data&&f.hasContent&&!1!==f.contentType||e.contentType)&&x.setRequestHeader("Content-Type",f.contentType),x.setRequestHeader("Accept",f.dataTypes[0]&&f.accepts[f.dataTypes[0]]?f.accepts[f.dataTypes[0]]+("*"!==f.dataTypes[0]?", "+Re+"; q=0.01":""):f.accepts["*"]),f.headers)x.setRequestHeader(Q,f.headers[Q]);if(f.beforeSend&&(!1===f.beforeSend.call(d,x,f)||l))return x.abort();if(L="abort",m.add(f.complete),x.done(f.success),x.fail(f.error),r=Be(je,f,e,x)){if(x.readyState=1,c&&p.trigger("ajaxSend",[x,f]),l)return x;f.async&&f.timeout>0&&(s=n.setTimeout(function(){x.abort("timeout")},f.timeout));try{l=!1,r.send(y,_)}catch(t){if(l)throw t;_(-1,t)}}else _(-1,"No Transport");function _(t,e,a,u){var Q,T,g,y,b,L=e;l||(l=!0,s&&n.clearTimeout(s),r=void 0,o=u||"",x.readyState=t>0?4:0,Q=t>=200&&t<300||304===t,a&&(y=function(t,e,n){for(var r,i,o,a,s=t.contents,u=t.dataTypes;"*"===u[0];)u.shift(),void 0===r&&(r=t.mimeType||e.getResponseHeader("Content-Type"));if(r)for(i in s)if(s[i]&&s[i].test(r)){u.unshift(i);break}if(u[0]in n)o=u[0];else{for(i in n){if(!u[0]||t.converters[i+" "+u[0]]){o=i;break}a||(a=i)}o=o||a}if(o)return o!==u[0]&&u.unshift(o),n[o]}(f,x,a)),!Q&&H.inArray("script",f.dataTypes)>-1&&(f.converters["text script"]=function(){}),y=function(t,e,n,r){var i,o,a,s,u,l={},c=t.dataTypes.slice();if(c[1])for(a in t.converters)l[a.toLowerCase()]=t.converters[a];for(o=c.shift();o;)if(t.responseFields[o]&&(n[t.responseFields[o]]=e),!u&&r&&t.dataFilter&&(e=t.dataFilter(e,t.dataType)),u=o,o=c.shift())if("*"===o)o=u;else if("*"!==u&&u!==o){if(!(a=l[u+" "+o]||l["* "+o]))for(i in l)if((s=i.split(" "))[1]===o&&(a=l[u+" "+s[0]]||l["* "+s[0]])){!0===a?a=l[i]:!0!==l[i]&&(o=s[0],c.unshift(s[1]));break}if(!0!==a)if(a&&t.throws)e=a(e);else try{e=a(e)}catch(t){return{state:"parsererror",error:a?t:"No conversion from "+u+" to "+o}}}return{state:"success",data:e}}(f,y,x,Q),Q?(f.ifModified&&((b=x.getResponseHeader("Last-Modified"))&&(H.lastModified[i]=b),(b=x.getResponseHeader("etag"))&&(H.etag[i]=b)),204===t||"HEAD"===f.type?L="nocontent":304===t?L="notmodified":(L=y.state,T=y.data,Q=!(g=y.error))):(g=L,!t&&L||(L="error",t<0&&(t=0))),x.status=t,x.statusText=(e||L)+"",Q?h.resolveWith(d,[T,L,x]):h.rejectWith(d,[x,L,g]),x.statusCode(v),v=void 0,c&&p.trigger(Q?"ajaxSuccess":"ajaxError",[x,f,Q?T:g]),m.fireWith(d,[x,L]),c&&(p.trigger("ajaxComplete",[x,f]),--H.active||H.event.trigger("ajaxStop")))}return x},getJSON:function(t,e,n){return H.get(t,e,n,"json")},getScript:function(t,e){return H.get(t,void 0,e,"script")}}),H.each(["get","post"],function(t,e){H[e]=function(t,n,r,i){return m(n)&&(i=i||r,r=n,n=void 0),H.ajax(H.extend({url:t,type:e,dataType:i,data:n,success:r},H.isPlainObject(t)&&t))}}),H.ajaxPrefilter(function(t){var e;for(e in t.headers)"content-type"===e.toLowerCase()&&(t.contentType=t.headers[e]||"")}),H._evalUrl=function(t,e,n){return H.ajax({url:t,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,converters:{"text script":function(){}},dataFilter:function(t){H.globalEval(t,e,n)}})},H.fn.extend({wrapAll:function(t){var e;return this[0]&&(m(t)&&(t=t.call(this[0])),e=H(t,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&e.insertBefore(this[0]),e.map(function(){for(var t=this;t.firstElementChild;)t=t.firstElementChild;return t}).append(this)),this},wrapInner:function(t){return m(t)?this.each(function(e){H(this).wrapInner(t.call(this,e))}):this.each(function(){var e=H(this),n=e.contents();n.length?n.wrapAll(t):e.append(t)})},wrap:function(t){var e=m(t);return this.each(function(n){H(this).wrapAll(e?t.call(this,n):t)})},unwrap:function(t){return this.parent(t).not("body").each(function(){H(this).replaceWith(this.childNodes)}),this}}),H.expr.pseudos.hidden=function(t){return!H.expr.pseudos.visible(t)},H.expr.pseudos.visible=function(t){return!!(t.offsetWidth||t.offsetHeight||t.getClientRects().length)},H.ajaxSettings.xhr=function(){try{return new n.XMLHttpRequest}catch(t){}};var Ze={0:200,1223:204},ze=H.ajaxSettings.xhr();h.cors=!!ze&&"withCredentials"in ze,h.ajax=ze=!!ze,H.ajaxTransport(function(t){var e,r;if(h.cors||ze&&!t.crossDomain)return{send:function(i,o){var a,s=t.xhr();if(s.open(t.type,t.url,t.async,t.username,t.password),t.xhrFields)for(a in t.xhrFields)s[a]=t.xhrFields[a];for(a in t.mimeType&&s.overrideMimeType&&s.overrideMimeType(t.mimeType),t.crossDomain||i["X-Requested-With"]||(i["X-Requested-With"]="XMLHttpRequest"),i)s.setRequestHeader(a,i[a]);e=function(t){return function(){e&&(e=r=s.onload=s.onerror=s.onabort=s.ontimeout=s.onreadystatechange=null,"abort"===t?s.abort():"error"===t?"number"!=typeof s.status?o(0,"error"):o(s.status,s.statusText):o(Ze[s.status]||s.status,s.statusText,"text"!==(s.responseType||"text")||"string"!=typeof s.responseText?{binary:s.response}:{text:s.responseText},s.getAllResponseHeaders()))}},s.onload=e(),r=s.onerror=s.ontimeout=e("error"),void 0!==s.onabort?s.onabort=r:s.onreadystatechange=function(){4===s.readyState&&n.setTimeout(function(){e&&r()})},e=e("abort");try{s.send(t.hasContent&&t.data||null)}catch(t){if(e)throw t}},abort:function(){e&&e()}}}),H.ajaxPrefilter(function(t){t.crossDomain&&(t.contents.script=!1)}),H.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(t){return H.globalEval(t),t}}}),H.ajaxPrefilter("script",function(t){void 0===t.cache&&(t.cache=!1),t.crossDomain&&(t.type="GET")}),H.ajaxTransport("script",function(t){var e,n;if(t.crossDomain||t.scriptAttrs)return{send:function(r,i){e=H(" - diff --git a/package-lock.json b/package-lock.json index cd30e8fba..1504c8dfc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48,6 +48,7 @@ "react-modal": "^3.12.1", "sprintf-js": "^1.1.1", "tapable": "^1.0.0", + "treant-js": "^1.0.1", "uuid": "^3.2.1", "w3c-blob": "0.0.1" }, @@ -21630,6 +21631,11 @@ "node": ">=6" } }, + "node_modules/treant-js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/treant-js/-/treant-js-1.0.1.tgz", + "integrity": "sha1-aRdSt+9maSCzQiP8ZlJUaTtG/VQ=" + }, "node_modules/trim": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/trim/-/trim-0.0.1.tgz", @@ -41190,6 +41196,11 @@ } } }, + "treant-js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/treant-js/-/treant-js-1.0.1.tgz", + "integrity": "sha1-aRdSt+9maSCzQiP8ZlJUaTtG/VQ=" + }, "trim": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/trim/-/trim-0.0.1.tgz", diff --git a/package.json b/package.json index 388c07fab..9e0550073 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "react-modal": "^3.12.1", "sprintf-js": "^1.1.1", "tapable": "^1.0.0", + "treant-js": "^1.0.1", "uuid": "^3.2.1", "w3c-blob": "0.0.1" }, diff --git a/src/Corporation/Actions.ts b/src/Corporation/Actions.ts new file mode 100644 index 000000000..d3bfbc80f --- /dev/null +++ b/src/Corporation/Actions.ts @@ -0,0 +1,228 @@ +import { ICorporation } from "./ICorporation"; +import { IIndustry } from "./IIndustry"; +import { IndustryStartingCosts } from "./IndustryData"; +import { Industry } from "./Industry"; +import { CorporationConstants } from "./data/Constants"; +import { OfficeSpace } from "./OfficeSpace"; +import { Material } from "./Material"; +import { Product } from "./Product"; +import { Warehouse } from "./Warehouse"; +import { CorporationUnlockUpgrade } from "./data/CorporationUnlockUpgrades"; +import { CorporationUpgrade } from "./data/CorporationUpgrades"; +import { Cities } from "../Locations/Cities"; + +export function NewIndustry(corporation: ICorporation, industry: string, name: string): void { + for (let i = 0; i < corporation.divisions.length; ++i) { + if (corporation.divisions[i].name === name) { + throw new Error("This division name is already in use!"); + return; + } + } + + const cost = IndustryStartingCosts[industry]; + if(cost === undefined) { + throw new Error("Invalid industry: ${industry}"); + } + if (corporation.funds.lt(cost)) { + throw new Error("Not enough money to create a new division in this industry"); + } else if (name === "") { + throw new Error("New division must have a name!"); + } else { + corporation.funds = corporation.funds.minus(cost); + corporation.divisions.push(new Industry({ + corp: corporation, + name: name, + type: industry, + })); + } +} + +export function NewCity(corporation: ICorporation, division: IIndustry, city: string): void { + if (corporation.funds.lt(CorporationConstants.OfficeInitialCost)) { + throw new Error("You don't have enough company funds to open a new office!"); + } else { + corporation.funds = corporation.funds.minus(CorporationConstants.OfficeInitialCost); + division.offices[city] = new OfficeSpace({ + loc: city, + size: CorporationConstants.OfficeInitialSize, + }); + } +} + +export function UnlockUpgrade(corporation: ICorporation, upgrade: CorporationUnlockUpgrade): void { + if (corporation.funds.lt(upgrade[1])) { + throw new Error("Insufficient funds"); + } + corporation.unlock(upgrade); +} + +export function LevelUpgrade(corporation: ICorporation, upgrade: CorporationUpgrade): void { + const baseCost = upgrade[1]; + const priceMult = upgrade[2]; + const level = corporation.upgrades[upgrade[0]]; + const cost = baseCost * Math.pow(priceMult, level); + if (corporation.funds.lt(cost)) { + throw new Error("Insufficient funds"); + } else { + corporation.upgrade(upgrade); + } +} + +export function IssueDividends(corporation: ICorporation, percent: number): void { + if (isNaN(percent) || percent < 0 || percent > CorporationConstants.DividendMaxPercentage) { + throw new Error(`Invalid value. Must be an integer between 0 and ${CorporationConstants.DividendMaxPercentage}`); + } + + corporation.dividendPercentage = percent*100; +} + +export function SellMaterial(mat: Material, amt: string, price: string): void { + if(price === '') price = '0'; + if(amt === '') amt = '0'; + let cost = price.replace(/\s+/g, ''); + cost = cost.replace(/[^-()\d/*+.MP]/g, ''); //Sanitize cost + let temp = cost.replace(/MP/g, mat.bCost+''); + try { + temp = eval(temp); + } catch(e) { + throw new Error("Invalid value or expression for sell price field: " + e); + } + + if (temp == null || isNaN(parseFloat(temp))) { + throw new Error("Invalid value or expression for sell price field"); + } + + if (cost.includes("MP")) { + mat.sCost = cost; //Dynamically evaluated + } else { + mat.sCost = temp; + } + + //Parse quantity + if (amt.includes("MAX") || amt.includes("PROD")) { + let q = amt.replace(/\s+/g, ''); + q = q.replace(/[^-()\d/*+.MAXPROD]/g, ''); + let tempQty = q.replace(/MAX/g, '1'); + tempQty = tempQty.replace(/PROD/g, '1'); + try { + tempQty = eval(tempQty); + } catch(e) { + throw new Error("Invalid value or expression for sell price field: " + e); + } + + if (tempQty == null || isNaN(parseFloat(tempQty))) { + throw new Error("Invalid value or expression for sell price field"); + } + + mat.sllman[0] = true; + mat.sllman[1] = q; //Use sanitized input + } else if (isNaN(parseFloat(amt))) { + throw new Error("Invalid value for sell quantity field! Must be numeric or 'MAX'"); + } else { + let q = parseFloat(amt); + if (isNaN(q)) {q = 0;} + if (q === 0) { + mat.sllman[0] = false; + mat.sllman[1] = 0; + } else { + mat.sllman[0] = true; + mat.sllman[1] = q; + } + } +} + +export function SellProduct(product: Product, city: string, amt: string, price: string, all: boolean): void { + //Parse price + 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/*+.MP]/g, ''); + let temp = price.replace(/MP/g, '1'); + try { + temp = eval(temp); + } catch(e) { + throw new Error("Invalid value or expression for sell quantity field: " + e); + } + if (temp == null || isNaN(parseFloat(temp))) { + throw new Error("Invalid value or expression for sell quantity field."); + } + product.sCost = price; //Use sanitized price + } else { + const cost = parseFloat(price); + if (isNaN(cost)) { + throw new Error("Invalid value for sell price field"); + } + product.sCost = cost; + } + + // Array of all cities. Used later + const cities = Object.keys(Cities); + + // Parse quantity + if (amt.includes("MAX") || amt.includes("PROD")) { + //Dynamically evaluated quantity. First test to make sure its valid + let qty = amt.replace(/\s+/g, ''); + qty = qty.replace(/[^-()\d/*+.MAXPROD]/g, ''); + let temp = qty.replace(/MAX/g, '1'); + temp = temp.replace(/PROD/g, '1'); + try { + temp = eval(temp); + } catch(e) { + throw new Error("Invalid value or expression for sell price field: " + e); + } + + if (temp == null || isNaN(parseFloat(temp))) { + throw new Error("Invalid value or expression for sell price field"); + } + if (all) { + 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(parseFloat(amt))) { + throw new Error("Invalid value for sell quantity field! Must be numeric"); + } else { + let qty = parseFloat(amt); + if (isNaN(qty)) {qty = 0;} + if (qty === 0) { + if (all) { + for (let i = 0; i < cities.length; ++i) { + const tempCity = cities[i]; + product.sllman[tempCity][0] = false; + product.sllman[tempCity][1] = ''; + } + } else { + product.sllman[city][0] = false; + product.sllman[city][1] = ''; + } + } else { + if (all) { + 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; + } + } + } +} + +export function SetSmartSupply(warehouse: Warehouse, smartSupply: boolean): void { + warehouse.smartSupplyEnabled = smartSupply; +} + +export function BuyMaterial(material: Material, amt: number): void { + if (isNaN(amt)) { + throw new Error(`Invalid amount '${amt}' to buy material '${material.name}'`); + } + material.buy = amt; +} \ No newline at end of file diff --git a/src/Corporation/Corporation.jsx b/src/Corporation/Corporation.jsx deleted file mode 100644 index 02f4c9a76..000000000 --- a/src/Corporation/Corporation.jsx +++ /dev/null @@ -1,2401 +0,0 @@ -import { AllCorporationStates, - CorporationState } from "./CorporationState"; -import { CorporationUnlockUpgrades } from "./data/CorporationUnlockUpgrades"; -import { CorporationUpgrades } from "./data/CorporationUpgrades"; -import { EmployeePositions } from "./EmployeePositions"; -import { Industries, - IndustryStartingCosts, - IndustryResearchTrees } from "./IndustryData"; -import { IndustryUpgrades } from "./IndustryUpgrades"; -import { Material } from "./Material"; -import { MaterialSizes } from "./MaterialSizes"; -import { Product } from "./Product"; -import { ResearchMap } from "./ResearchMap"; -import { Warehouse } from "./Warehouse"; - -import { BitNodeMultipliers } from "../BitNode/BitNodeMultipliers"; -import { showLiterature } from "../Literature/LiteratureHelpers"; -import { LiteratureNames } from "../Literature/data/LiteratureNames"; -import { CityName } from "../Locations/data/CityNames"; -import { Player } from "../Player"; - -import { numeralWrapper } from "../ui/numeralFormat"; -import { Page, routing } from "../ui/navigationTracking"; - -import { calculateEffectWithFactors } from "../utils/calculateEffectWithFactors"; - -import { dialogBoxCreate } from "../../utils/DialogBox"; -import { Reviver, - Generic_toJSON, - Generic_fromJSON } from "../../utils/JSONReviver"; -import { appendLineBreaks } from "../../utils/uiHelpers/appendLineBreaks"; -import { createElement } from "../../utils/uiHelpers/createElement"; -import { createPopup } from "../../utils/uiHelpers/createPopup"; -import { createPopupCloseButton } from "../../utils/uiHelpers/createPopupCloseButton"; -import { formatNumber, generateRandomString } from "../../utils/StringHelperFunctions"; -import { getRandomInt } from "../../utils/helpers/getRandomInt"; -import { isString } from "../../utils/helpers/isString"; -import { KEY } from "../../utils/helpers/keyCodes"; -import { removeElement } from "../../utils/uiHelpers/removeElement"; -import { removeElementById } from "../../utils/uiHelpers/removeElementById"; -import { yesNoBoxCreate, - yesNoTxtInpBoxCreate, - yesNoBoxGetYesButton, - yesNoBoxGetNoButton, - yesNoTxtInpBoxGetYesButton, - yesNoTxtInpBoxGetNoButton, - yesNoTxtInpBoxGetInput, - yesNoBoxClose, - yesNoTxtInpBoxClose } from "../../utils/YesNoBox"; - -// UI Related Imports -import React from "react"; -import ReactDOM from "react-dom"; -import { CorporationEventHandler } from "./ui/CorporationUIEventHandler"; -import { CorporationRoot } from "./ui/Root"; -import { CorporationRouting } from "./ui/Routing"; - -import Decimal from "decimal.js"; - -/* Constants */ -export const INITIALSHARES = 1e9; //Total number of shares you have at your company -export const SHARESPERPRICEUPDATE = 1e6; //When selling large number of shares, price is dynamically updated for every batch of this amount -export const IssueNewSharesCooldown = 216e3; // 12 Hour in terms of game cycles -export const SellSharesCooldown = 18e3; // 1 Hour in terms of game cycles - -export const CyclesPerMarketCycle = 50; -export const CyclesPerIndustryStateCycle = CyclesPerMarketCycle / AllCorporationStates.length; -export const SecsPerMarketCycle = CyclesPerMarketCycle / 5; - -export const Cities = ["Aevum", "Chongqing", "Sector-12", "New Tokyo", "Ishima", "Volhaven"]; - -export const WarehouseInitialCost = 5e9; //Initial purchase cost of warehouse -export const WarehouseInitialSize = 100; -export const WarehouseUpgradeBaseCost = 1e9; - -export const OfficeInitialCost = 4e9; -export const OfficeInitialSize = 3; -export const OfficeUpgradeBaseCost = 1e9; - -export const BribeThreshold = 100e12; //Money needed to be able to bribe for faction rep -export const BribeToRepRatio = 1e9; //Bribe Value divided by this = rep gain - -export const ProductProductionCostRatio = 5; //Ratio of material cost of a product to its production cost - -export const DividendMaxPercentage = 50; - -export const EmployeeSalaryMultiplier = 3; // Employee stats multiplied by this to determine initial salary -export const CyclesPerEmployeeRaise = 400; // All employees get a raise every X market cycles -export const EmployeeRaiseAmount = 50; // Employee salary increases by this (additive) - -export const BaseMaxProducts = 3; // Initial value for maximum number of products allowed - -// Delete Research Popup Box when clicking outside of it -let researchTreeBoxOpened = false; -let researchTreeBox = null; -$(document).mousedown(function(event) { - const contentId = "corporation-research-popup-box-content"; - if (researchTreeBoxOpened) { - if ( $(event.target).closest("#" + contentId).get(0) == null ) { - // Delete the box - removeElement(researchTreeBox); - researchTreeBox = null; - researchTreeBoxOpened = false; - } - } -}); - -function Industry(params={}) { - this.offices = { //Maps locations to offices. 0 if no office at that location - [CityName.Aevum]: 0, - [CityName.Chongqing]: 0, - [CityName.Sector12]: new OfficeSpace({ - loc:CityName.Sector12, - size:OfficeInitialSize, - }), - [CityName.NewTokyo]: 0, - [CityName.Ishima]: 0, - [CityName.Volhaven]: 0, - }; - - this.name = params.name ? params.name : 0; - this.type = params.type ? params.type : Industries.Agriculture; - - this.sciResearch = new Material({name: "Scientific Research"}); - this.researched = {}; // Object of acquired Research. Keys = research name - - //A map of the NAME of materials required to create produced materials to - //how many are needed to produce 1 unit of produced materials - this.reqMats = {}; - - //An array of the name of materials being produced - this.prodMats = []; - - this.products = {}; - this.makesProducts = false; - - this.awareness = 0; - this.popularity = 0; //Should always be less than awareness - this.startingCost = 0; - - /* The following are factors for how much production/other things are increased by - different factors. The production increase always has diminishing returns, - and they are all reprsented by exponentials of < 1 (e.g x ^ 0.5, x ^ 0.8) - The number for these represent the exponential. A lower number means more - diminishing returns */ - this.reFac = 0; //Real estate Factor - this.sciFac = 0; //Scientific Research Factor, affects quality - this.hwFac = 0; //Hardware factor - this.robFac = 0; //Robotics Factor - this.aiFac = 0; //AI Cores factor; - this.advFac = 0; //Advertising factor, affects sales - - this.prodMult = 0; //Production multiplier - - //Financials - this.lastCycleRevenue = new Decimal(0); - this.lastCycleExpenses = new Decimal(0); - this.thisCycleRevenue = new Decimal(0); - this.thisCycleExpenses = new Decimal(0); - - //Upgrades - var numUpgrades = Object.keys(IndustryUpgrades).length; - this.upgrades = Array(numUpgrades).fill(0); - - this.state = "START"; - this.newInd = true; - - this.warehouses = { //Maps locations to warehouses. 0 if no warehouse at that location - [CityName.Aevum]: 0, - [CityName.Chonqing]: 0, - [CityName.Sector12]: new Warehouse({ - corp: params.corp, - industry: this, - loc: CityName.Sector12, - size: WarehouseInitialSize, - }), - [CityName.NewTokyo]: 0, - [CityName.Ishima]: 0, - [CityName.Volhaven]: 0, - }; - - this.init(); -} - -Industry.prototype.init = function() { - //Set the unique properties of an industry (how much its affected by real estate/scientific research, etc.) - this.startingCost = IndustryStartingCosts[this.type]; - switch (this.type) { - case Industries.Energy: - this.reFac = 0.65; - this.sciFac = 0.7; - this.robFac = 0.05; - this.aiFac = 0.3; - this.advFac = 0.08; - this.reqMats = { - "Hardware": 0.1, - "Metal": 0.2, - }; - this.prodMats = ["Energy"]; - break; - case Industries.Utilities: - case "Utilities": - this.reFac = 0.5; - this.sciFac = 0.6; - this.robFac = 0.4; - this.aiFac = 0.4; - this.advFac = 0.08; - this.reqMats = { - "Hardware": 0.1, - "Metal": 0.1, - } - this.prodMats = ["Water"]; - break; - case Industries.Agriculture: - this.reFac = 0.72; - this.sciFac = 0.5; - this.hwFac = 0.2; - this.robFac = 0.3; - this.aiFac = 0.3; - this.advFac = 0.04; - this.reqMats = { - "Water": 0.5, - "Energy": 0.5, - } - this.prodMats = ["Plants", "Food"]; - break; - case Industries.Fishing: - this.reFac = 0.15; - this.sciFac = 0.35; - this.hwFac = 0.35; - this.robFac = 0.5; - this.aiFac = 0.2; - this.advFac = 0.08; - this.reqMats = { - "Energy": 0.5, - } - this.prodMats = ["Food"]; - break; - case Industries.Mining: - this.reFac = 0.3; - this.sciFac = 0.26; - this.hwFac = 0.4; - this.robFac = 0.45; - this.aiFac = 0.45; - this.advFac = 0.06; - this.reqMats = { - "Energy": 0.8, - } - this.prodMats = ["Metal"]; - break; - case Industries.Food: - //reFac is unique for this bc it diminishes greatly per city. Handle this separately in code? - this.sciFac = 0.12; - this.hwFac = 0.15; - this.robFac = 0.3; - this.aiFac = 0.25; - this.advFac = 0.25; - this.reFac = 0.05; - this.reqMats = { - "Food": 0.5, - "Water": 0.5, - "Energy": 0.2, - } - this.makesProducts = true; - break; - case Industries.Tobacco: - this.reFac = 0.15; - this.sciFac = 0.75; - this.hwFac = 0.15; - this.robFac = 0.2; - this.aiFac = 0.15; - this.advFac = 0.2; - this.reqMats = { - "Plants": 1, - "Water": 0.2, - } - this.makesProducts = true; - break; - case Industries.Chemical: - this.reFac = 0.25; - this.sciFac = 0.75; - this.hwFac = 0.2; - this.robFac = 0.25; - this.aiFac = 0.2; - this.advFac = 0.07; - this.reqMats = { - "Plants": 1, - "Energy": 0.5, - "Water": 0.5, - } - this.prodMats = ["Chemicals"]; - break; - case Industries.Pharmaceutical: - this.reFac = 0.05; - this.sciFac = 0.8; - this.hwFac = 0.15; - this.robFac = 0.25; - this.aiFac = 0.2; - this.advFac = 0.16; - this.reqMats = { - "Chemicals": 2, - "Energy": 1, - "Water": 0.5, - } - this.prodMats = ["Drugs"]; - this.makesProducts = true; - break; - case Industries.Computer: - case "Computer": - this.reFac = 0.2; - this.sciFac = 0.62; - this.robFac = 0.36; - this.aiFac = 0.19; - this.advFac = 0.17; - this.reqMats = { - "Metal": 2, - "Energy": 1, - } - this.prodMats = ["Hardware"]; - this.makesProducts = true; - break; - case Industries.Robotics: - this.reFac = 0.32; - this.sciFac = 0.65; - this.aiFac = 0.36; - this.advFac = 0.18; - this.hwFac = 0.19; - this.reqMats = { - "Hardware": 5, - "Energy": 3, - } - this.prodMats = ["Robots"]; - this.makesProducts = true; - break; - case Industries.Software: - this.sciFac = 0.62; - this.advFac = 0.16; - this.hwFac = 0.25; - this.reFac = 0.15; - this.aiFac = 0.18; - this.robFac = 0.05; - this.reqMats = { - "Hardware": 0.5, - "Energy": 0.5, - } - this.prodMats = ["AICores"]; - this.makesProducts = true; - break; - case Industries.Healthcare: - this.reFac = 0.1; - this.sciFac = 0.75; - this.advFac = 0.11; - this.hwFac = 0.1; - this.robFac = 0.1; - this.aiFac = 0.1; - this.reqMats = { - "Robots": 10, - "AICores": 5, - "Energy": 5, - "Water": 5, - } - this.makesProducts = true; - break; - case Industries.RealEstate: - this.robFac = 0.6; - this.aiFac = 0.6; - this.advFac = 0.25; - this.sciFac = 0.05; - this.hwFac = 0.05; - this.reqMats = { - "Metal": 5, - "Energy": 5, - "Water": 2, - "Hardware": 4, - } - this.prodMats = ["RealEstate"]; - this.makesProducts = true; - break; - default: - console.error(`Invalid Industry Type passed into Industry.init(): ${this.type}`); - return; - } -} - -Industry.prototype.getProductDescriptionText = function() { - if (!this.makesProducts) {return;} - switch (this.type) { - case Industries.Food: - return "create and manage restaurants"; - case Industries.Tobacco: - return "create tobacco and tobacco-related products"; - case Industries.Pharmaceutical: - return "develop new pharmaceutical drugs"; - case Industries.Computer: - case "Computer": - return "create new computer hardware and networking infrastructures"; - case Industries.Robotics: - return "build specialized robots and robot-related products"; - case Industries.Software: - return "develop computer software"; - case Industries.Healthcare: - return "build and manage hospitals"; - case Industries.RealEstate: - return "develop and manage real estate properties"; - default: - console.error("Invalid industry type in Industry.getProductDescriptionText"); - return ""; - } -} - -Industry.prototype.getMaximumNumberProducts = function() { - if (!this.makesProducts) { return 0; } - - // Calculate additional number of allowed Products from Research/Upgrades - let additional = 0; - if (this.hasResearch("uPgrade: Capacity.I")) { ++additional; } - if (this.hasResearch("uPgrade: Capacity.II")) { ++additional; } - - return BaseMaxProducts + additional; -} - -Industry.prototype.hasMaximumNumberProducts = function() { - return (Object.keys(this.products).length >= this.getMaximumNumberProducts()); -} - -//Calculates the values that factor into the production and properties of -//materials/products (such as quality, etc.) -Industry.prototype.calculateProductionFactors = function() { - var multSum = 0; - for (var i = 0; i < Cities.length; ++i) { - var city = Cities[i]; - var warehouse = this.warehouses[city]; - if (!(warehouse instanceof Warehouse)) { - continue; - } - - var materials = warehouse.materials; - - var cityMult = Math.pow(0.002 * materials.RealEstate.qty+1, this.reFac) * - Math.pow(0.002 * materials.Hardware.qty+1, this.hwFac) * - Math.pow(0.002 * materials.Robots.qty+1, this.robFac) * - Math.pow(0.002 * materials.AICores.qty+1, this.aiFac); - multSum += Math.pow(cityMult, 0.73); - } - - multSum < 1 ? this.prodMult = 1 : this.prodMult = multSum; -} - -Industry.prototype.updateWarehouseSizeUsed = function(warehouse) { - if (warehouse instanceof Warehouse) { - //This resets the size back to 0 and then accounts for materials - warehouse.updateMaterialSizeUsed(); - } - - for (var prodName in this.products) { - if (this.products.hasOwnProperty(prodName)) { - var prod = this.products[prodName]; - warehouse.sizeUsed += (prod.data[warehouse.loc][0] * prod.siz); - if (prod.data[warehouse.loc][0] > 0) { - warehouse.breakdown += (prodName + ": " + formatNumber(prod.data[warehouse.loc][0] * prod.siz, 0) + "
"); - } - } - } -} - -Industry.prototype.process = function(marketCycles=1, state, company) { - this.state = state; - - //At the start of a cycle, store and reset revenue/expenses - //Then calculate salaries and processs the markets - if (state === "START") { - if (isNaN(this.thisCycleRevenue) || isNaN(this.thisCycleExpenses)) { - console.error("NaN in Corporation's computed revenue/expenses"); - dialogBoxCreate("Something went wrong when compting Corporation's revenue/expenses. This is a bug. Please report to game developer"); - this.thisCycleRevenue = new Decimal(0); - this.thisCycleExpenses = new Decimal(0); - } - this.lastCycleRevenue = this.thisCycleRevenue.dividedBy(marketCycles * SecsPerMarketCycle); - this.lastCycleExpenses = this.thisCycleExpenses.dividedBy(marketCycles * SecsPerMarketCycle); - this.thisCycleRevenue = new Decimal(0); - this.thisCycleExpenses = new Decimal(0); - - // Once you start making revenue, the player should no longer be - // considered new, and therefore no longer needs the 'tutorial' UI elements - if (this.lastCycleRevenue.gt(0)) {this.newInd = false;} - - // Process offices (and the employees in them) - var employeeSalary = 0; - for (var officeLoc in this.offices) { - if (this.offices[officeLoc] instanceof OfficeSpace) { - employeeSalary += this.offices[officeLoc].process(marketCycles, {industry:this, corporation:company}); - } - } - this.thisCycleExpenses = this.thisCycleExpenses.plus(employeeSalary); - - // Process change in demand/competition of materials/products - this.processMaterialMarket(marketCycles); - this.processProductMarket(marketCycles); - - // Process loss of popularity - this.popularity -= (marketCycles * .0001); - this.popularity = Math.max(0, this.popularity); - - // Process Dreamsense gains - var popularityGain = company.getDreamSenseGain(), awarenessGain = popularityGain * 4; - if (popularityGain > 0) { - this.popularity += (popularityGain * marketCycles); - this.awareness += (awarenessGain * marketCycles); - } - - return; - } - - // Process production, purchase, and import/export of materials - let res = this.processMaterials(marketCycles, company); - if (Array.isArray(res)) { - this.thisCycleRevenue = this.thisCycleRevenue.plus(res[0]); - this.thisCycleExpenses = this.thisCycleExpenses.plus(res[1]); - } - - // Process creation, production & sale of products - res = this.processProducts(marketCycles, company); - if (Array.isArray(res)) { - this.thisCycleRevenue = this.thisCycleRevenue.plus(res[0]); - this.thisCycleExpenses = this.thisCycleExpenses.plus(res[1]); - } -} - -// Process change in demand and competition for this industry's materials -Industry.prototype.processMaterialMarket = function() { - //References to prodMats and reqMats - var reqMats = this.reqMats, prodMats = this.prodMats; - - //Only 'process the market' for materials that this industry deals with - for (var i = 0; i < Cities.length; ++i) { - //If this industry has a warehouse in this city, process the market - //for every material this industry requires or produces - if (this.warehouses[Cities[i]] instanceof Warehouse) { - var wh = this.warehouses[Cities[i]]; - for (var name in reqMats) { - if (reqMats.hasOwnProperty(name)) { - wh.materials[name].processMarket(); - } - } - - //Produced materials are stored in an array - for (var foo = 0; foo < prodMats.length; ++foo) { - wh.materials[prodMats[foo]].processMarket(); - } - - //Process these twice because these boost production - wh.materials["Hardware"].processMarket(); - wh.materials["Robots"].processMarket(); - wh.materials["AICores"].processMarket(); - wh.materials["RealEstate"].processMarket(); - } - } -} - -// Process change in demand and competition for this industry's products -Industry.prototype.processProductMarket = function(marketCycles=1) { - // Demand gradually decreases, and competition gradually increases - for (const name in this.products) { - if (this.products.hasOwnProperty(name)) { - const product = this.products[name]; - let change = getRandomInt(0, 3) * 0.0004; - if (change === 0) { continue; } - - if (this.type === Industries.Pharmaceutical || this.type === Industries.Software || - this.type === Industries.Robotics) { - change *= 3; - } - change *= marketCycles; - product.dmd -= change; - product.cmp += change; - product.cmp = Math.min(product.cmp, 99.99); - product.dmd = Math.max(product.dmd, 0.001); - } - } -} - -//Process production, purchase, and import/export of materials -Industry.prototype.processMaterials = function(marketCycles=1, company) { - var revenue = 0, expenses = 0; - this.calculateProductionFactors(); - - //At the start of the export state, set the imports of everything to 0 - if (this.state === "EXPORT") { - for (let i = 0; i < Cities.length; ++i) { - var city = Cities[i], office = this.offices[city]; - if (!(this.warehouses[city] instanceof Warehouse)) { - continue; - } - var warehouse = this.warehouses[city]; - for (var matName in warehouse.materials) { - if (warehouse.materials.hasOwnProperty(matName)) { - var mat = warehouse.materials[matName]; - mat.imp = 0; - } - } - } - } - - for (let i = 0; i < Cities.length; ++i) { - var city = Cities[i], office = this.offices[city]; - - if (this.warehouses[city] instanceof Warehouse) { - var warehouse = this.warehouses[city]; - - switch(this.state) { - - case "PURCHASE": - /* Process purchase of materials */ - for (var matName in warehouse.materials) { - if (warehouse.materials.hasOwnProperty(matName)) { - (function(matName, ind) { - var mat = warehouse.materials[matName]; - var buyAmt, maxAmt; - if (warehouse.smartSupplyEnabled && Object.keys(ind.reqMats).includes(matName)) { - //Smart supply tracker is stored as per second rate - mat.buy = ind.reqMats[matName] * warehouse.smartSupplyStore; - buyAmt = mat.buy * SecsPerMarketCycle * marketCycles; - } else { - buyAmt = (mat.buy * SecsPerMarketCycle * marketCycles); - } - - if (matName == "RealEstate") { - maxAmt = buyAmt; - } else { - maxAmt = Math.floor((warehouse.size - warehouse.sizeUsed) / MaterialSizes[matName]); - } - var buyAmt = Math.min(buyAmt, maxAmt); - if (buyAmt > 0) { - mat.qty += buyAmt; - expenses += (buyAmt * mat.bCost); - } - })(matName, this); - this.updateWarehouseSizeUsed(warehouse); - } - } //End process purchase of materials - break; - - case "PRODUCTION": - warehouse.smartSupplyStore = 0; //Reset smart supply amount - - /* Process production of materials */ - if (this.prodMats.length > 0) { - var mat = warehouse.materials[this.prodMats[0]]; - //Calculate the maximum production of this material based - //on the office's productivity - var maxProd = this.getOfficeProductivity(office) - * this.prodMult // Multiplier from materials - * company.getProductionMultiplier() - * this.getProductionMultiplier(); // Multiplier from Research - let prod; - - if (mat.prdman[0]) { - //Production is manually limited - prod = Math.min(maxProd, mat.prdman[1]); - } else { - 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 - var totalMatSize = 0; - for (let tmp = 0; tmp < this.prodMats.length; ++tmp) { - totalMatSize += (MaterialSizes[this.prodMats[tmp]]); - } - 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 (totalMatSize > 0) { - var maxAmt = Math.floor((warehouse.size - warehouse.sizeUsed) / totalMatSize); - prod = Math.min(maxAmt, prod); - } - - if (prod < 0) {prod = 0;} - - // Keep track of production for smart supply (/s) - warehouse.smartSupplyStore += (prod / (SecsPerMarketCycle * marketCycles)); - - // Make sure we have enough resource to make our materials - var producableFrac = 1; - for (var reqMatName in this.reqMats) { - if (this.reqMats.hasOwnProperty(reqMatName)) { - var req = this.reqMats[reqMatName] * prod; - if (warehouse.materials[reqMatName].qty < req) { - producableFrac = Math.min(producableFrac, warehouse.materials[reqMatName].qty / req); - } - } - } - if (producableFrac <= 0) {producableFrac = 0; prod = 0;} - - // Make our materials if they are producable - if (producableFrac > 0 && prod > 0) { - 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 (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 + - Math.pow(this.sciResearch.qty, this.sciFac) + - Math.pow(warehouse.materials["AICores"].qty, this.aiFac) / 10e3); - } - } else { - for (const reqMatName in this.reqMats) { - if (this.reqMats.hasOwnProperty(reqMatName)) { - warehouse.materials[reqMatName].prd = 0; - } - } - } - - //Per second - 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 (const reqMatName in this.reqMats) { - warehouse.materials[reqMatName].prd = 0; - } - } - break; - - case "SALE": - /* Process sale of materials */ - for (var matName in warehouse.materials) { - if (warehouse.materials.hasOwnProperty(matName)) { - var mat = warehouse.materials[matName]; - if (mat.sCost < 0 || mat.sllman[0] === false) { - mat.sll = 0; - continue; - } - - // 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.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); - let optimalPrice; - if (sqrtDenominator === 0 || denominator === 0) { - if (sqrtNumerator === 0) { - optimalPrice = 0; // No production - } else { - optimalPrice = mat.bCost + markupLimit; - console.warn(`In Corporation, found illegal 0s when trying to calculate MarketTA2 sale cost`); - } - } else { - 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); - sCost = eval(sCost); - } else { - sCost = mat.sCost; - } - - // Calculate how much of the material sells (per second) - let markup = 1; - if (sCost > mat.bCost) { - //Penalty if difference between sCost and bCost is greater than markup limit - if ((sCost - mat.bCost) > markupLimit) { - markup = Math.pow(markupLimit / (sCost - mat.bCost), 2); - } - } else if (sCost < mat.bCost) { - if (sCost <= 0) { - markup = 1e12; //Sell everything, essentially discard - } else { - //Lower prices than market increases sales - markup = mat.bCost / sCost; - } - } - - var maxSell = (mat.qlt + .001) - * marketFactor - * markup - * businessFactor - * company.getSalesMultiplier() - * advertisingFactor - * this.getSalesMultiplier(); - var sellAmt; - if (isString(mat.sllman[1])) { - //Dynamically evaluated - var tmp = mat.sllman[1].replace(/MAX/g, maxSell); - tmp = tmp.replace(/PROD/g, mat.prd); - try { - sellAmt = eval(tmp); - } catch(e) { - dialogBoxCreate("Error evaluating your sell amount for material " + mat.name + - " in " + this.name + "'s " + city + " office. The sell amount " + - "is being set to zero"); - sellAmt = 0; - } - sellAmt = Math.min(maxSell, sellAmt); - } else if (mat.sllman[1] === -1) { - //Backwards compatibility, -1 = MAX - sellAmt = maxSell; - } else { - //Player's input value is just a number - sellAmt = Math.min(maxSell, mat.sllman[1]); - } - - sellAmt = (sellAmt * SecsPerMarketCycle * marketCycles); - sellAmt = Math.min(mat.qty, sellAmt); - if (sellAmt < 0) { - console.warn(`sellAmt calculated to be negative for ${matName} in ${city}`); - mat.sll = 0; - continue; - } - if (sellAmt && sCost >= 0) { - mat.qty -= sellAmt; - revenue += (sellAmt * sCost); - mat.sll = sellAmt / (SecsPerMarketCycle * marketCycles); - } else { - mat.sll = 0; - } - } - } //End processing of sale of materials - break; - - case "EXPORT": - for (var matName in warehouse.materials) { - if (warehouse.materials.hasOwnProperty(matName)) { - var mat = warehouse.materials[matName]; - mat.totalExp = 0; //Reset export - for (var expI = 0; expI < mat.exp.length; ++expI) { - var exp = mat.exp[expI]; - var amt = exp.amt.replace(/MAX/g, mat.qty / (SecsPerMarketCycle * marketCycles)); - try { - amt = eval(amt); - } catch(e) { - dialogBoxCreate("Calculating export for " + mat.name + " in " + - this.name + "'s " + city + " division failed with " + - "error: " + e); - continue; - } - if (isNaN(amt)) { - dialogBoxCreate("Error calculating export amount for " + mat.name + " in " + - this.name + "'s " + city + " division."); - continue; - } - amt = amt * SecsPerMarketCycle * marketCycles; - - if (mat.qty < amt) { - amt = mat.qty; - } - if (amt === 0) { - break; //None left - } - for (var foo = 0; foo < company.divisions.length; ++foo) { - if (company.divisions[foo].name === exp.ind) { - var expIndustry = company.divisions[foo]; - var expWarehouse = expIndustry.warehouses[exp.city]; - if (!(expWarehouse instanceof Warehouse)) { - console.error(`Invalid export! ${expIndustry.name} ${exp.city}`); - break; - } - - // Make sure theres enough space in warehouse - if (expWarehouse.sizeUsed >= expWarehouse.size) { - // Warehouse at capacity. Exporting doesnt - // affect revenue so just return 0's - return [0, 0]; - } else { - var maxAmt = Math.floor((expWarehouse.size - expWarehouse.sizeUsed) / MaterialSizes[matName]); - amt = Math.min(maxAmt, amt); - } - expWarehouse.materials[matName].imp += (amt / (SecsPerMarketCycle * marketCycles)); - expWarehouse.materials[matName].qty += amt; - expWarehouse.materials[matName].qlt = mat.qlt; - mat.qty -= amt; - mat.totalExp += amt; - expIndustry.updateWarehouseSizeUsed(expWarehouse); - break; - } - } - } - //totalExp should be per second - mat.totalExp /= (SecsPerMarketCycle * marketCycles); - } - } - - break; - - case "START": - break; - default: - console.error(`Invalid state: ${this.state}`); - break; - } //End switch(this.state) - this.updateWarehouseSizeUsed(warehouse); - - } // End warehouse - - //Produce Scientific Research based on R&D employees - //Scientific Research can be produced without a warehouse - if (office instanceof OfficeSpace) { - this.sciResearch.qty += (.004 - * Math.pow(office.employeeProd[EmployeePositions.RandD], 0.5) - * company.getScientificResearchMultiplier() - * this.getScientificResearchMultiplier()); - } - } - return [revenue, expenses]; -} - -//Process production & sale of this industry's FINISHED products (including all of their stats) -Industry.prototype.processProducts = function(marketCycles=1, corporation) { - var revenue = 0, expenses = 0; - - //Create products - if (this.state === "PRODUCTION") { - for (const prodName in this.products) { - const prod = this.products[prodName]; - if (!prod.fin) { - const city = prod.createCity; - const office = this.offices[city]; - - // Designing/Creating a Product is based mostly off Engineers - const engrProd = office.employeeProd[EmployeePositions.Engineer]; - const mgmtProd = office.employeeProd[EmployeePositions.Management]; - const opProd = office.employeeProd[EmployeePositions.Operations]; - const total = engrProd + mgmtProd + opProd; - if (total <= 0) { break; } - - // Management is a multiplier for the production from Engineers - const mgmtFactor = 1 + (mgmtProd / (1.2 * total)); - - const progress = (Math.pow(engrProd, 0.34) + Math.pow(opProd, 0.2)) * mgmtFactor; - - prod.createProduct(marketCycles, progress); - if (prod.prog >= 100) { - prod.finishProduct(office.employeeProd, this); - } - break; - } - } - } - - //Produce Products - for (var prodName in this.products) { - if (this.products.hasOwnProperty(prodName)) { - var prod = this.products[prodName]; - if (prod instanceof Product && prod.fin) { - revenue += this.processProduct(marketCycles, prod, corporation); - } - } - } - return [revenue, expenses]; -} - -//Processes FINISHED products -Industry.prototype.processProduct = function(marketCycles=1, product, corporation) { - 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) { - - case "PRODUCTION": { - //Calculate the maximum production of this material based - //on the office's productivity - var maxProd = this.getOfficeProductivity(office, {forProduct:true}) - * corporation.getProductionMultiplier() - * this.prodMult // Multiplier from materials - * this.getProductionMultiplier() // Multiplier from research - * this.getProductProductionMultiplier(); // Multiplier from research - let prod; - - //Account for whether production is manually limited - if (product.prdman[city][0]) { - prod = Math.min(maxProd, product.prdman[city][1]); - } else { - prod = maxProd; - } - prod *= (SecsPerMarketCycle * marketCycles); - - //Calculate net change in warehouse storage making the Products will cost - var netStorageSize = product.siz; - for (var reqMatName in product.reqMats) { - if (product.reqMats.hasOwnProperty(reqMatName)) { - var normQty = product.reqMats[reqMatName]; - netStorageSize -= (MaterialSizes[reqMatName] * normQty); - } - } - - //If there's not enough space in warehouse, limit the amount of Product - if (netStorageSize > 0) { - var maxAmt = Math.floor((warehouse.size - warehouse.sizeUsed) / netStorageSize); - prod = Math.min(maxAmt, prod); - } - - warehouse.smartSupplyStore += (prod / (SecsPerMarketCycle * marketCycles)); - - //Make sure we have enough resources to make our Products - var producableFrac = 1; - for (var reqMatName in product.reqMats) { - if (product.reqMats.hasOwnProperty(reqMatName)) { - var req = product.reqMats[reqMatName] * prod; - if (warehouse.materials[reqMatName].qty < req) { - producableFrac = Math.min(producableFrac, warehouse.materials[reqMatName].qty / req); - } - } - } - - //Make our Products if they are producable - if (producableFrac > 0 && prod > 0) { - for (var reqMatName in product.reqMats) { - if (product.reqMats.hasOwnProperty(reqMatName)) { - var reqMatQtyNeeded = (product.reqMats[reqMatName] * prod * producableFrac); - warehouse.materials[reqMatName].qty -= reqMatQtyNeeded; - warehouse.materials[reqMatName].prd -= reqMatQtyNeeded / (SecsPerMarketCycle * marketCycles); - } - } - //Quantity - product.data[city][0] += (prod * producableFrac); - } - - //Keep track of production Per second - product.data[city][1] = prod * producableFrac / (SecsPerMarketCycle * marketCycles); - break; - } - case "SALE": { - //Process sale of Products - product.pCost = 0; //Estimated production cost - for (var reqMatName in product.reqMats) { - if (product.reqMats.hasOwnProperty(reqMatName)) { - product.pCost += (product.reqMats[reqMatName] * warehouse.materials[reqMatName].bCost); - } - } - - // Since its a product, its production cost is increased for labor - product.pCost *= ProductProductionCostRatio; - - // 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 (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) { - if (sqrtNumerator === 0) { - optimalPrice = 0; // No production - } else { - optimalPrice = product.pCost + markupLimit; - console.warn(`In Corporation, found illegal 0s when trying to calculate MarketTA2 sale cost`); - } - } 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)) { - if(product.mku === 0) { - console.error(`mku is zero, reverting to 1 to avoid Infinity`); - product.mku = 1; - } - sCost = product.sCost.replace(/MP/g, product.pCost + product.rat / product.mku); - sCost = eval(sCost); - - } else { - sCost = product.sCost; - } - - var markup = 1; - if (sCost > product.pCost) { - if ((sCost - product.pCost) > markupLimit) { - markup = markupLimit / (sCost - product.pCost); - } - } - - var maxSell = 0.5 - * Math.pow(product.rat, 0.65) - * marketFactor - * corporation.getSalesMultiplier() - * Math.pow(markup, 2) - * businessFactor - * advertisingFactor - * this.getSalesMultiplier(); - var sellAmt; - if (product.sllman[city][0] && isString(product.sllman[city][1])) { - //Sell amount is dynamically evaluated - var tmp = product.sllman[city][1].replace(/MAX/g, maxSell); - tmp = tmp.replace(/PROD/g, product.data[city][1]); - try { - tmp = eval(tmp); - } catch(e) { - dialogBoxCreate("Error evaluating your sell price expression for " + product.name + - " in " + this.name + "'s " + city + " office. Sell price is being set to MAX"); - tmp = maxSell; - } - sellAmt = Math.min(maxSell, tmp); - } 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 { - sellAmt = maxSell; - } - if (sellAmt < 0) { sellAmt = 0; } - sellAmt = sellAmt * SecsPerMarketCycle * marketCycles; - sellAmt = Math.min(product.data[city][0], sellAmt); //data[0] is qty - if (sellAmt && sCost) { - product.data[city][0] -= sellAmt; //data[0] is qty - totalProfit += (sellAmt * sCost); - product.data[city][2] = sellAmt / (SecsPerMarketCycle * marketCycles); //data[2] is sell property - } else { - product.data[city][2] = 0; //data[2] is sell property - } - break; - } - case "START": - case "PURCHASE": - case "EXPORT": - break; - default: - console.error(`Invalid State: ${this.state}`); - break; - } //End switch(this.state) - } - } - return totalProfit; -} - -Industry.prototype.discontinueProduct = function(product) { - for (var productName in this.products) { - if (this.products.hasOwnProperty(productName)) { - if (product === this.products[productName]) { - delete this.products[productName]; - } - } - } -} - -Industry.prototype.upgrade = function(upgrade, refs) { - var corporation = refs.corporation; - var office = refs.office; - var upgN = upgrade[0]; - while (this.upgrades.length <= upgN) {this.upgrades.push(0);} - ++this.upgrades[upgN]; - - switch (upgN) { - case 0: //Coffee, 5% energy per employee - for (let i = 0; i < office.employees.length; ++i) { - office.employees[i].ene = Math.min(office.employees[i].ene * 1.05, office.maxEne); - } - break; - case 1: //AdVert.Inc, - var advMult = corporation.getAdvertisingMultiplier() * this.getAdvertisingMultiplier(); - this.awareness += (3 * advMult); - this.popularity += (1 * advMult); - this.awareness *= (1.01 * advMult); - this.popularity *= ((1 + getRandomInt(1, 3) / 100) * advMult); - break; - default: - console.error(`Un-implemented function index: ${upgN}`); - break; - } -} - -// Returns how much of a material can be produced based of office productivity (employee stats) -Industry.prototype.getOfficeProductivity = function(office, params) { - const opProd = office.employeeProd[EmployeePositions.Operations]; - const engrProd = office.employeeProd[EmployeePositions.Engineer]; - const mgmtProd = office.employeeProd[EmployeePositions.Management] - const total = opProd + engrProd + mgmtProd; - - if (total <= 0) { return 0; } - - // Management is a multiplier for the production from Operations and Engineers - const mgmtFactor = 1 + (mgmtProd / (1.2 * total)); - - // For production, Operations is slightly more important than engineering - // Both Engineering and Operations have diminishing returns - const prod = (Math.pow(opProd, 0.4) + Math.pow(engrProd, 0.3)) * mgmtFactor; - - // Generic multiplier for the production. Used for game-balancing purposes - const balancingMult = 0.05; - - if (params && params.forProduct) { - // Products are harder to create and therefore have less production - return 0.5 * balancingMult * prod; - } else { - return balancingMult * prod; - } -} - -// 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); -} - -//Returns a set of multipliers based on the Industry's awareness, popularity, and advFac. This -//multiplier affects sales. The result is: -// [Total sales mult, total awareness mult, total pop mult, awareness/pop ratio mult] -Industry.prototype.getAdvertisingFactors = function() { - var awarenessFac = Math.pow(this.awareness + 1, this.advFac); - var popularityFac = Math.pow(this.popularity + 1, this.advFac); - var ratioFac = (this.awareness === 0 ? 0.01 : Math.max((this.popularity + .001) / this.awareness, 0.01)); - var totalFac = Math.pow(awarenessFac * popularityFac * ratioFac, 0.85); - return [totalFac, awarenessFac, popularityFac, ratioFac]; -} - -//Returns a multiplier based on a materials demand and competition that affects sales -Industry.prototype.getMarketFactor = function(mat) { - return Math.max(0.1, mat.dmd * (100 - mat.cmp) / 100); -} - -// Returns a boolean indicating whether this Industry has the specified Research -Industry.prototype.hasResearch = function(name) { - return (this.researched[name] === true); -} - -Industry.prototype.updateResearchTree = function() { - const researchTree = IndustryResearchTrees[this.type]; - - // Since ResearchTree data isnt saved, we'll update the Research Tree data - // based on the stored 'researched' property in the Industry object - if (Object.keys(researchTree.researched).length !== Object.keys(this.researched).length) { - for (let research in this.researched) { - researchTree.research(research); - } - } -} - -// Get multipliers from Research -Industry.prototype.getAdvertisingMultiplier = function() { - this.updateResearchTree(); - return IndustryResearchTrees[this.type].getAdvertisingMultiplier(); -} - -Industry.prototype.getEmployeeChaMultiplier = function() { - this.updateResearchTree(); - return IndustryResearchTrees[this.type].getEmployeeChaMultiplier(); -} - -Industry.prototype.getEmployeeCreMultiplier = function() { - this.updateResearchTree(); - return IndustryResearchTrees[this.type].getEmployeeCreMultiplier(); -} - -Industry.prototype.getEmployeeEffMultiplier = function() { - this.updateResearchTree(); - return IndustryResearchTrees[this.type].getEmployeeEffMultiplier(); -} - -Industry.prototype.getEmployeeIntMultiplier = function() { - this.updateResearchTree(); - return IndustryResearchTrees[this.type].getEmployeeIntMultiplier(); -} - -Industry.prototype.getProductionMultiplier = function() { - this.updateResearchTree(); - return IndustryResearchTrees[this.type].getProductionMultiplier(); -} - -Industry.prototype.getProductProductionMultiplier = function() { - this.updateResearchTree(); - return IndustryResearchTrees[this.type].getProductProductionMultiplier(); -} - -Industry.prototype.getSalesMultiplier = function() { - this.updateResearchTree(); - return IndustryResearchTrees[this.type].getSalesMultiplier(); -} - -Industry.prototype.getScientificResearchMultiplier = function() { - this.updateResearchTree(); - return IndustryResearchTrees[this.type].getScientificResearchMultiplier(); -} - -Industry.prototype.getStorageMultiplier = function() { - this.updateResearchTree(); - return IndustryResearchTrees[this.type].getStorageMultiplier(); -} - -// Create the Research Tree UI for this Industry -Industry.prototype.createResearchBox = function() { - const boxId = "corporation-research-popup-box"; - - if (researchTreeBoxOpened) { - // It's already opened, so delete it to refresh content - removeElementById(boxId); - researchTreeBox = null; - } - - const researchTree = IndustryResearchTrees[this.type]; - - // Create the popup first, so that the tree diagram can be added to it - // This is handled by Treant - researchTreeBox = createPopup(boxId, [], { backgroundColor: "black" }); - - // Get the tree's markup (i.e. config) for Treant - const markup = researchTree.createTreantMarkup(); - markup.chart.container = "#" + boxId + "-content"; - markup.chart.nodeAlign = "BOTTOM"; - markup.chart.rootOrientation = "WEST"; - markup.chart.siblingSeparation = 40; - markup.chart.connectors = { - type: "step", - style: { - "arrow-end": "block-wide-long", - "stroke": "white", - "stroke-width": 2, - }, - } - - // Construct the tree with Treant - // This is required for side effect. - // eslint-disable-next-line no-new - new Treant(markup); - - // Add Event Listeners for all Nodes - const allResearch = researchTree.getAllNodes(); - for (let i = 0; i < allResearch.length; ++i) { - // If this is already Researched, skip it - if (this.researched[allResearch[i]] === true) { - continue; - } - - // Get the Research object - const research = ResearchMap[allResearch[i]]; - - // Get the DOM Element to add a click listener to it - const sanitizedName = allResearch[i].replace(/\s/g, ''); - const div = document.getElementById(sanitizedName + "-corp-research-click-listener"); - if (div == null) { - console.warn(`Could not find Research Tree div for ${sanitizedName}`); - continue; - } - - div.addEventListener("click", () => { - if (this.sciResearch.qty >= research.cost) { - this.sciResearch.qty -= research.cost; - - // Get the Node from the Research Tree and set its 'researched' property - researchTree.research(allResearch[i]); - this.researched[allResearch[i]] = true; - - const researchBox = this.createResearchBox(); - dialogBoxCreate(`Researched ${allResearch[i]}. It may take a market cycle ` + - `(~${SecsPerMarketCycle} seconds) before the effects of ` + - `the Research apply.`); - - return researchBox; - } else { - dialogBoxCreate(`You do not have enough Scientific Research for ${research.name}`); - } - }); - } - - - const boxContent = document.getElementById(`${boxId}-content`); - if (boxContent != null) { - // Add information about multipliers from research at the bottom of the popup - appendLineBreaks(boxContent, 2); - boxContent.appendChild(createElement("pre", { - display: "block", - innerText: `Multipliers from research:\n` + - ` * Advertising Multiplier: x${researchTree.getAdvertisingMultiplier()}\n` + - ` * Employee Charisma Multiplier: x${researchTree.getEmployeeChaMultiplier()}\n` + - ` * Employee Creativity Multiplier: x${researchTree.getEmployeeCreMultiplier()}\n` + - ` * Employee Efficiency Multiplier: x${researchTree.getEmployeeEffMultiplier()}\n` + - ` * Employee Intelligence Multiplier: x${researchTree.getEmployeeIntMultiplier()}\n` + - ` * Production Multiplier: x${researchTree.getProductionMultiplier()}\n` + - ` * Sales Multiplier: x${researchTree.getSalesMultiplier()}\n` + - ` * Scientific Research Multiplier: x${researchTree.getScientificResearchMultiplier()}\n` + - ` * Storage Multiplier: x${researchTree.getStorageMultiplier()}`, - })); - - // Close button - boxContent.appendChild(createPopupCloseButton(researchTreeBox, { - class: "std-button", - display: "block", - innerText: "Close", - })); - } - - researchTreeBoxOpened = true; -} - -Industry.prototype.toJSON = function() { - return Generic_toJSON("Industry", this); -} - -Industry.fromJSON = function(value) { - return Generic_fromJSON(Industry, value.data); -} - -Reviver.constructors.Industry = Industry; - -function Employee(params={}) { - if (!(this instanceof Employee)) { - return new Employee(params); - } - this.name = params.name ? params.name : "Bobby"; - - //Morale, happiness, and energy are 0-100 - this.mor = params.morale ? params.morale : getRandomInt(50, 100); - this.hap = params.happiness ? params.happiness : getRandomInt(50, 100); - this.ene = params.energy ? params.energy : getRandomInt(50, 100); - - this.int = params.intelligence ? params.intelligence : getRandomInt(10, 50); - this.cha = params.charisma ? params.charisma : getRandomInt(10, 50); - this.exp = params.experience ? params.experience : getRandomInt(10, 50); - this.cre = params.creativity ? params.creativity : getRandomInt(10, 50); - this.eff = params.efficiency ? params.efficiency : getRandomInt(10, 50); - this.sal = params.salary ? params.salary : getRandomInt(0.1, 5); - this.pro = 0; //Productivity, This is calculated - - this.cyclesUntilRaise = CyclesPerEmployeeRaise; - - this.loc = params.loc ? params.loc : ""; - this.pos = EmployeePositions.Unassigned; -} - -//Returns the amount the employee needs to be paid -Employee.prototype.process = function(marketCycles=1, office) { - var gain = 0.003 * marketCycles, - det = gain * Math.random(); - this.exp += gain; - - // Employee salaries slowly go up over time - this.cyclesUntilRaise -= marketCycles; - if (this.cyclesUntilRaise <= 0) { - this.salary += EmployeeRaiseAmount; - this.cyclesUntilRaise += CyclesPerEmployeeRaise; - } - - //Training - var trainingEff = gain * Math.random(); - if (this.pos === EmployeePositions.Training) { - //To increase creativity and intelligence special upgrades are needed - this.cha += trainingEff; - this.exp += trainingEff; - this.eff += trainingEff; - } - - this.ene -= det; - this.hap -= det; - - if (this.ene < office.minEne) {this.ene = office.minEne;} - if (this.hap < office.minHap) {this.hap = office.minHap;} - var salary = this.sal * marketCycles * SecsPerMarketCycle; - return salary; -} - -Employee.prototype.calculateProductivity = function(corporation, industry) { - var effCre = this.cre * corporation.getEmployeeCreMultiplier() * industry.getEmployeeCreMultiplier(), - effCha = this.cha * corporation.getEmployeeChaMultiplier() * industry.getEmployeeChaMultiplier(), - effInt = this.int * corporation.getEmployeeIntMultiplier() * industry.getEmployeeIntMultiplier(), - effEff = this.eff * corporation.getEmployeeEffMultiplier() * industry.getEmployeeEffMultiplier(); - const prodBase = this.mor * this.hap * this.ene * 1e-6; - let prodMult; - switch(this.pos) { - //Calculate productivity based on position. This is multipled by prodBase - //to get final value - case EmployeePositions.Operations: - prodMult = (0.6 * effInt) + (0.1 * effCha) + (this.exp) + - (0.5 * effCre) + (effEff); - break; - case EmployeePositions.Engineer: - prodMult = (effInt) + (0.1 * effCha) + (1.5 * this.exp) + - (effEff); - break; - case EmployeePositions.Business: - prodMult = (0.4 * effInt) + (effCha) + (0.5 * this.exp); - break; - case EmployeePositions.Management: - prodMult = (2 * effCha) + (this.exp) + (0.2 * effCre) + - (0.7 * effEff); - break; - case EmployeePositions.RandD: - prodMult = (1.5 * effInt) + (0.8 * this.exp) + (effCre) + - (0.5 * effEff); - break; - case EmployeePositions.Unassigned: - case EmployeePositions.Training: - prodMult = 0; - break; - default: - console.error(`Invalid employee position: ${this.pos}`); - break; - } - return prodBase * prodMult; -} - -//Process benefits from having an office party thrown -Employee.prototype.throwParty = function(money) { - var mult = 1 + (money / 10e6); - this.mor *= mult; - this.mor = Math.min(100, this.mor); - this.hap *= mult; - this.hap = Math.min(100, this.hap); - return mult; -} - -//'panel' is the DOM element on which to create the UI -Employee.prototype.createUI = function(panel, corporation, industry) { - var effCre = this.cre * corporation.getEmployeeCreMultiplier() * industry.getEmployeeCreMultiplier(), - effCha = this.cha * corporation.getEmployeeChaMultiplier() * industry.getEmployeeChaMultiplier(), - effInt = this.int * corporation.getEmployeeIntMultiplier() * industry.getEmployeeIntMultiplier(), - effEff = this.eff * corporation.getEmployeeEffMultiplier() * industry.getEmployeeEffMultiplier(); - panel.style.color = "white"; - panel.appendChild(createElement("p", { - id:"cmpy-mgmt-employee-" + this.name + "-panel-text", - innerHTML:"Morale: " + formatNumber(this.mor, 3) + "
" + - "Happiness: " + formatNumber(this.hap, 3) + "
" + - "Energy: " + formatNumber(this.ene, 3) + "
" + - "Intelligence: " + formatNumber(effInt, 3) + "
" + - "Charisma: " + formatNumber(effCha, 3) + "
" + - "Experience: " + formatNumber(this.exp, 3) + "
" + - "Creativity: " + formatNumber(effCre, 3) + "
" + - "Efficiency: " + formatNumber(effEff, 3) + "
" + - "Salary: " + numeralWrapper.format(this.sal, "$0.000a") + "/ s
", - })); - - //Selector for employee position - var selector = createElement("select", {}); - for (var key in EmployeePositions) { - if (EmployeePositions.hasOwnProperty(key)) { - selector.add(createElement("option", { - text: EmployeePositions[key], - value: EmployeePositions[key], - })); - } - } - - selector.addEventListener("change", () => { - this.pos = selector.options[selector.selectedIndex].value; - }); - - //Set initial value of selector - for (var i = 0; i < selector.length; ++i) { - if (selector.options[i].value === this.pos) { - selector.selectedIndex = i; - break; - } - } - panel.appendChild(selector); -} - -Employee.prototype.toJSON = function() { - return Generic_toJSON("Employee", this); -} - -Employee.fromJSON = function(value) { - return Generic_fromJSON(Employee, value.data); -} - -Reviver.constructors.Employee = Employee; - -var OfficeSpaceTiers = { - Basic: "Basic", - Enhanced: "Enhanced", - Luxurious: "Luxurious", - Extravagant: "Extravagant", -} - -function OfficeSpace(params={}) { - this.loc = params.loc ? params.loc : ""; - this.cost = params.cost ? params.cost : 1; - this.size = params.size ? params.size : 1; - this.comf = params.comfort ? params.comfort : 1; - this.beau = params.beauty ? params.beauty : 1; - this.tier = OfficeSpaceTiers.Basic; - - // Min/max energy of employees - this.minEne = 0; - this.maxEne = 100; - - // Min/max Happiness of office - this.minHap = 0; - this.maxHap = 100; - - // Maximum Morale of office - this.maxMor = 100; - - this.employees = []; - this.employeeProd = { - [EmployeePositions.Operations]: 0, - [EmployeePositions.Engineer]: 0, - [EmployeePositions.Business]: 0, - [EmployeePositions.Management]: 0, - [EmployeePositions.RandD]: 0, - total: 0, - }; -} - -OfficeSpace.prototype.atCapacity = function() { - return (this.employees.length) >= this.size; -} - -OfficeSpace.prototype.process = function(marketCycles=1, parentRefs) { - var industry = parentRefs.industry; - - // HRBuddy AutoRecruitment and training - if (industry.hasResearch("HRBuddy-Recruitment") && !this.atCapacity()) { - const emp = this.hireRandomEmployee(); - if (industry.hasResearch("HRBuddy-Training")) { - emp.pos = EmployeePositions.Training; - } - } - - // Process Office properties - this.maxEne = 100; - this.maxHap = 100; - this.maxMor = 100; - if (industry.hasResearch("Go-Juice")) { - this.maxEne += 10; - } - if (industry.hasResearch("JoyWire")) { - this.maxHap += 10; - } - if (industry.hasResearch("Sti.mu")) { - this.maxMor += 10; - } - - // Calculate changes in Morale/Happiness/Energy for Employees - var perfMult=1; //Multiplier for employee morale/happiness/energy based on company performance - if (industry.funds < 0 && industry.lastCycleRevenue < 0) { - perfMult = Math.pow(0.99, marketCycles); - } else if (industry.funds > 0 && industry.lastCycleRevenue > 0) { - perfMult = Math.pow(1.01, marketCycles); - } - - const hasAutobrew = industry.hasResearch("AutoBrew"); - const hasAutoparty = industry.hasResearch("AutoPartyManager"); - - var salaryPaid = 0; - for (let i = 0; i < this.employees.length; ++i) { - const emp = this.employees[i]; - if (hasAutoparty) { - emp.mor = this.maxMor; - emp.hap = this.maxHap; - } else { - emp.mor *= perfMult; - emp.hap *= perfMult; - emp.mor = Math.min(emp.mor, this.maxMor); - emp.hap = Math.min(emp.hap, this.maxHap); - } - - if (hasAutobrew) { - emp.ene = this.maxEne; - } else { - emp.ene *= perfMult; - emp.ene = Math.min(emp.ene, this.maxEne); - } - - const salary = emp.process(marketCycles, this); - salaryPaid += salary; - } - - this.calculateEmployeeProductivity(parentRefs); - return salaryPaid; -} - -OfficeSpace.prototype.calculateEmployeeProductivity = function(parentRefs) { - var company = parentRefs.corporation, industry = parentRefs.industry; - - //Reset - for (const name in this.employeeProd) { - this.employeeProd[name] = 0; - } - - var total = 0; - for (let i = 0; i < this.employees.length; ++i) { - const employee = this.employees[i]; - const prod = employee.calculateProductivity(company, industry); - this.employeeProd[employee.pos] += prod; - total += prod; - } - this.employeeProd["total"] = total; -} - -//Takes care of UI as well -OfficeSpace.prototype.findEmployees = function(parentRefs) { - if (this.atCapacity()) { return; } - if (document.getElementById("cmpy-mgmt-hire-employee-popup") != null) {return;} - - //Generate three random employees (meh, decent, amazing) - var mult1 = getRandomInt(25, 50)/100, - mult2 = getRandomInt(51, 75)/100, - mult3 = getRandomInt(76, 100)/100; - var int = getRandomInt(50, 100), - cha = getRandomInt(50, 100), - exp = getRandomInt(50, 100), - cre = getRandomInt(50, 100), - eff = getRandomInt(50, 100), - sal = EmployeeSalaryMultiplier * (int + cha + exp + cre + eff); - - var emp1 = new Employee({ - intelligence: int * mult1, - charisma: cha * mult1, - experience: exp * mult1, - creativity: cre * mult1, - efficiency: eff * mult1, - salary: sal * mult1, - }); - - var emp2 = new Employee({ - intelligence: int * mult2, - charisma: cha * mult2, - experience: exp * mult2, - creativity: cre * mult2, - efficiency: eff * mult2, - salary: sal * mult2, - }); - - var emp3 = new Employee({ - intelligence: int * mult3, - charisma: cha * mult3, - experience: exp * mult3, - creativity: cre * mult3, - efficiency: eff * mult3, - salary: sal * mult3, - }); - - var text = createElement("h1", { - innerHTML: "Select one of the following candidates for hire:", - }); - - var createEmpDiv = function(employee, office) { - var div = createElement("div", { - class:"cmpy-mgmt-find-employee-option", - innerHTML: "Intelligence: " + formatNumber(employee.int, 1) + "
" + - "Charisma: " + formatNumber(employee.cha, 1) + "
" + - "Experience: " + formatNumber(employee.exp, 1) + "
" + - "Creativity: " + formatNumber(employee.cre, 1) + "
" + - "Efficiency: " + formatNumber(employee.eff, 1) + "
" + - "Salary: " + numeralWrapper.format(employee.sal, '$0.000a') + " \ s
", - clickListener:() => { - office.hireEmployee(employee, parentRefs); - removeElementById("cmpy-mgmt-hire-employee-popup"); - return false; - }, - }); - return div; - }; - - var cancelBtn = createElement("a", { - class:"a-link-button", - innerText:"Cancel", - float:"right", - clickListener:() => { - removeElementById("cmpy-mgmt-hire-employee-popup"); - return false; - }, - }); - - var elems = [text, - createEmpDiv(emp1, this), - createEmpDiv(emp2, this), - createEmpDiv(emp3, this), - cancelBtn]; - - createPopup("cmpy-mgmt-hire-employee-popup", elems); -} - -OfficeSpace.prototype.hireEmployee = function(employee, parentRefs) { - var company = parentRefs.corporation; - var yesBtn = yesNoTxtInpBoxGetYesButton(), - noBtn = yesNoTxtInpBoxGetNoButton(); - yesBtn.innerHTML = "Hire"; - noBtn.innerHTML = "Cancel"; - yesBtn.addEventListener("click", () => { - var name = yesNoTxtInpBoxGetInput(); - for (var i = 0; i < this.employees.length; ++i) { - if (this.employees[i].name === name) { - dialogBoxCreate("You already have an employee with this nickname! Please give every employee a unique nickname."); - return false; - } - } - employee.name = name; - this.employees.push(employee); - company.rerender(); - return yesNoTxtInpBoxClose(); - }); - noBtn.addEventListener("click", () => { - return yesNoTxtInpBoxClose(); - }); - yesNoTxtInpBoxCreate("Give your employee a nickname!"); -} - -OfficeSpace.prototype.hireRandomEmployee = function() { - if (this.atCapacity()) { return; } - if (document.getElementById("cmpy-mgmt-hire-employee-popup") != null) {return;} - - //Generate three random employees (meh, decent, amazing) - var mult = getRandomInt(76, 100)/100; - var int = getRandomInt(50, 100), - cha = getRandomInt(50, 100), - exp = getRandomInt(50, 100), - cre = getRandomInt(50, 100), - eff = getRandomInt(50, 100), - sal = EmployeeSalaryMultiplier * (int + cha + exp + cre + eff); - - var emp = new Employee({ - intelligence: int * mult, - charisma: cha * mult, - experience: exp * mult, - creativity: cre * mult, - efficiency: eff * mult, - salary: sal * mult, - }); - - var name = generateRandomString(7); - - for (let i = 0; i < this.employees.length; ++i) { - if (this.employees[i].name === name) { - return this.hireRandomEmployee(); - } - } - emp.name = name; - this.employees.push(emp); - - return emp; -} - -//Finds the first unassigned employee and assigns its to the specified job -OfficeSpace.prototype.assignEmployeeToJob = function(job) { - for (var i = 0; i < this.employees.length; ++i) { - if (this.employees[i].pos === EmployeePositions.Unassigned) { - this.employees[i].pos = job; - return true; - } - } - return false; -} - -//Finds the first employee with the given job and unassigns it -OfficeSpace.prototype.unassignEmployeeFromJob = function(job) { - for (var i = 0; i < this.employees.length; ++i) { - if (this.employees[i].pos === job) { - this.employees[i].pos = EmployeePositions.Unassigned; - return true; - } - } - return false; -} - -OfficeSpace.prototype.toJSON = function() { - return Generic_toJSON("OfficeSpace", this); -} - -OfficeSpace.fromJSON = function(value) { - return Generic_fromJSON(OfficeSpace, value.data); -} - -Reviver.constructors.OfficeSpace = OfficeSpace; - -function Corporation(params={}) { - this.name = params.name ? params.name : "The Corporation"; - - //A division/business sector is represented by the object: - this.divisions = []; - - //Financial stats - this.funds = new Decimal(150e9); - this.revenue = new Decimal(0); - this.expenses = new Decimal(0); - this.fundingRound = 0; - this.public = false; //Publicly traded - this.totalShares = INITIALSHARES; // Total existing shares - this.numShares = INITIALSHARES; // Total shares owned by player - this.shareSalesUntilPriceUpdate = SHARESPERPRICEUPDATE; - this.shareSaleCooldown = 0; // Game cycles until player can sell shares again - this.issueNewSharesCooldown = 0; // Game cycles until player can issue shares again - this.dividendPercentage = 0; - this.dividendTaxPercentage = 50; - this.issuedShares = 0; - this.sharePrice = 0; - this.storedCycles = 0; - - var numUnlockUpgrades = Object.keys(CorporationUnlockUpgrades).length, - numUpgrades = Object.keys(CorporationUpgrades).length; - - this.unlockUpgrades = Array(numUnlockUpgrades).fill(0); - this.upgrades = Array(numUpgrades).fill(0); - this.upgradeMultipliers = Array(numUpgrades).fill(1); - - this.state = new CorporationState(); -} - -Corporation.prototype.addFunds = function(amt) { - if(!isFinite(amt)) { - console.error('Trying to add invalid amount of funds. Report to a developper.'); - return; - } - this.funds = this.funds.plus(amt); -} - -Corporation.prototype.getState = function() { - return this.state.getState(); -} - -Corporation.prototype.storeCycles = function(numCycles=1) { - this.storedCycles += numCycles; -} - -Corporation.prototype.process = function() { - if (this.storedCycles >= CyclesPerIndustryStateCycle) { - const state = this.getState(); - const marketCycles = 1; - const gameCycles = (marketCycles * CyclesPerIndustryStateCycle); - this.storedCycles -= gameCycles; - - this.divisions.forEach((ind) => { - ind.process(marketCycles, state, this); - }); - - // Process cooldowns - if (this.shareSaleCooldown > 0) { - this.shareSaleCooldown -= gameCycles; - } - if (this.issueNewSharesCooldown > 0) { - this.issueNewSharesCooldown -= gameCycles; - } - - //At the start of a new cycle, calculate profits from previous cycle - if (state === "START") { - this.revenue = new Decimal(0); - this.expenses = new Decimal(0); - this.divisions.forEach((ind) => { - if (ind.lastCycleRevenue === -Infinity || ind.lastCycleRevenue === Infinity) { return; } - if (ind.lastCycleExpenses === -Infinity || ind.lastCycleExpenses === Infinity) { return; } - this.revenue = this.revenue.plus(ind.lastCycleRevenue); - this.expenses = this.expenses.plus(ind.lastCycleExpenses); - }); - var profit = this.revenue.minus(this.expenses); - const cycleProfit = profit.times(marketCycles * SecsPerMarketCycle); - if (isNaN(this.funds) || this.funds === Infinity || this.funds === -Infinity) { - dialogBoxCreate("There was an error calculating your Corporations funds and they got reset to 0. " + - "This is a bug. Please report to game developer.

" + - "(Your funds have been set to $150b for the inconvenience)"); - this.funds = new Decimal(150e9); - } - - // Process dividends - if (this.dividendPercentage > 0 && cycleProfit > 0) { - // Validate input again, just to be safe - if (isNaN(this.dividendPercentage) || this.dividendPercentage < 0 || this.dividendPercentage > DividendMaxPercentage) { - console.error(`Invalid Corporation dividend percentage: ${this.dividendPercentage}`); - } else { - const totalDividends = (this.dividendPercentage / 100) * cycleProfit; - const retainedEarnings = cycleProfit - totalDividends; - const dividendsPerShare = totalDividends / this.totalShares; - const profit = this.numShares * dividendsPerShare * (1 - (this.dividendTaxPercentage / 100)); - Player.gainMoney(profit); - Player.recordMoneySource(profit, "corporation"); - this.addFunds(retainedEarnings); - } - } else { - this.addFunds(cycleProfit); - } - - this.updateSharePrice(); - } - - this.state.nextState(); - - if (routing.isOn(Page.Corporation)) { this.rerender(); } - } -} - -Corporation.prototype.determineValuation = function() { - var val, profit = (this.revenue.minus(this.expenses)).toNumber(); - if (this.public) { - // Account for dividends - if (this.dividendPercentage > 0) { - profit *= ((100 - this.dividendPercentage) / 100); - } - - val = this.funds.toNumber() + (profit * 85e3); - val *= (Math.pow(1.1, this.divisions.length)); - val = Math.max(val, 0); - } else { - val = 10e9 + Math.max(this.funds.toNumber(), 0) / 3; //Base valuation - if (profit > 0) { - val += (profit * 315e3); - val *= (Math.pow(1.1, this.divisions.length)); - } else { - val = 10e9 * Math.pow(1.1, this.divisions.length); - } - val -= (val % 1e6); //Round down to nearest millionth - } - return val * BitNodeMultipliers.CorporationValuation; -} - -Corporation.prototype.getInvestment = function() { - var val = this.determineValuation(), percShares; - let roundMultiplier = 4; - switch (this.fundingRound) { - case 0: //Seed - percShares = 0.10; - roundMultiplier = 4; - break; - case 1: //Series A - percShares = 0.35; - roundMultiplier = 3; - break; - case 2: //Series B - percShares = 0.25; - roundMultiplier = 3; - break; - case 3: //Series C - percShares = 0.20; - roundMultiplier = 2.5; - break; - case 4: - return; - } - var funding = val * percShares * roundMultiplier, - investShares = Math.floor(INITIALSHARES * percShares), - yesBtn = yesNoBoxGetYesButton(), - noBtn = yesNoBoxGetNoButton(); - yesBtn.innerHTML = "Accept"; - noBtn.innerHML = "Reject"; - yesBtn.addEventListener("click", () => { - ++this.fundingRound; - this.addFunds(funding); - this.numShares -= investShares; - this.rerender(); - return yesNoBoxClose(); - }); - noBtn.addEventListener("click", () => { - return yesNoBoxClose(); - }); - yesNoBoxCreate("An investment firm has offered you " + numeralWrapper.format(funding, '$0.000a') + - " in funding in exchange for a " + numeralWrapper.format(percShares*100, "0.000a") + - "% stake in the company (" + numeralWrapper.format(investShares, '0.000a') + " shares).

" + - "Do you accept or reject this offer?

" + - "Hint: Investment firms will offer more money if your corporation is turning a profit"); -} - -Corporation.prototype.goPublic = function() { - var goPublicPopupId = "cmpy-mgmt-go-public-popup"; - var initialSharePrice = this.determineValuation() / (this.totalShares); - var txt = createElement("p", { - innerHTML: "Enter the number of shares you would like to issue " + - "for your IPO. These shares will be publicly sold " + - "and you will no longer own them. Your Corporation will receive " + - numeralWrapper.format(initialSharePrice, '$0.000a') + " per share " + - "(the IPO money will be deposited directly into your Corporation's funds).

" + - "You have a total of " + numeralWrapper.format(this.numShares, "0.000a") + " of shares that you can issue.", - }); - var yesBtn; - var input = createElement("input", { - type:"number", - placeholder: "Shares to issue", - onkeyup:(e) => { - e.preventDefault(); - if (e.keyCode === KEY.ENTER) {yesBtn.click();} - }, - }); - var br = createElement("br", {}); - yesBtn = createElement("a", { - class:"a-link-button", - innerText:"Go Public", - clickListener:() => { - var numShares = Math.round(input.value); - var initialSharePrice = this.determineValuation() / (this.totalShares); - if (isNaN(numShares)) { - dialogBoxCreate("Invalid value for number of issued shares"); - return false; - } - if (numShares > this.numShares) { - dialogBoxCreate("Error: You don't have that many shares to issue!"); - return false; - } - this.public = true; - this.sharePrice = initialSharePrice; - this.issuedShares = numShares; - this.numShares -= numShares; - this.addFunds(numShares * initialSharePrice); - this.rerender(); - removeElementById(goPublicPopupId); - dialogBoxCreate(`You took your ${this.name} public and earned ` + - `${numeralWrapper.formatMoney(numShares * initialSharePrice)} in your IPO`); - return false; - }, - }); - var noBtn = createElement("a", { - class:"a-link-button", - innerText:"Cancel", - clickListener:() => { - removeElementById(goPublicPopupId); - return false; - }, - }); - createPopup(goPublicPopupId, [txt, br, input, yesBtn, noBtn]); -} - -Corporation.prototype.getTargetSharePrice = function() { - // Note: totalShares - numShares is not the same as issuedShares because - // issuedShares does not account for private investors - return this.determineValuation() / (2 * (this.totalShares - this.numShares) + 1); -} - -Corporation.prototype.updateSharePrice = function() { - const targetPrice = this.getTargetSharePrice(); - if (this.sharePrice <= targetPrice) { - this.sharePrice *= (1 + (Math.random() * 0.01)); - } else { - this.sharePrice *= (1 - (Math.random() * 0.01)); - } - if (this.sharePrice <= 0.01) {this.sharePrice = 0.01;} -} - -Corporation.prototype.immediatelyUpdateSharePrice = function() { - this.sharePrice = this.getTargetSharePrice(); -} - -// Calculates how much money will be made and what the resulting stock price -// will be when the player sells his/her shares -// @return - [Player profit, final stock price, end shareSalesUntilPriceUpdate property] -Corporation.prototype.calculateShareSale = function(numShares) { - let sharesTracker = numShares; - let sharesUntilUpdate = this.shareSalesUntilPriceUpdate; - let sharePrice = this.sharePrice; - let sharesSold = 0; - let profit = 0; - - const maxIterations = Math.ceil(numShares / SHARESPERPRICEUPDATE); - if (isNaN(maxIterations) || maxIterations > 10e6) { - console.error(`Something went wrong or unexpected when calculating share sale. Maxiterations calculated to be ${maxIterations}`); - return; - } - - for (let i = 0; i < maxIterations; ++i) { - if (sharesTracker < sharesUntilUpdate) { - profit += (sharePrice * sharesTracker); - sharesUntilUpdate -= sharesTracker; - break; - } else { - profit += (sharePrice * sharesUntilUpdate); - sharesUntilUpdate = SHARESPERPRICEUPDATE; - sharesTracker -= sharesUntilUpdate; - sharesSold += sharesUntilUpdate; - - // Calculate what new share price would be - sharePrice = this.determineValuation() / (2 * (this.totalShares + sharesSold - this.numShares)); - } - } - - return [profit, sharePrice, sharesUntilUpdate]; -} - -Corporation.prototype.convertCooldownToString = function(cd) { - // The cooldown value is based on game cycles. Convert to a simple string - const seconds = cd / 5; - - const SecondsPerMinute = 60; - const SecondsPerHour = 3600; - - if (seconds > SecondsPerHour) { - return `${Math.floor(seconds / SecondsPerHour)} hour(s)`; - } else if (seconds > SecondsPerMinute) { - return `${Math.floor(seconds / SecondsPerMinute)} minute(s)`; - } else { - return `${Math.floor(seconds)} second(s)`; - } -} - -//One time upgrades that unlock new features -Corporation.prototype.unlock = function(upgrade) { - const upgN = upgrade[0], price = upgrade[1]; - while (this.unlockUpgrades.length <= upgN) { - this.unlockUpgrades.push(0); - } - if (this.funds.lt(price)) { - dialogBoxCreate("You don't have enough funds to unlock this!"); - return; - } - this.unlockUpgrades[upgN] = 1; - this.funds = this.funds.minus(price); - - // Apply effects for one-time upgrades - if (upgN === 5) { - this.dividendTaxPercentage -= 5; - } else if (upgN === 6) { - this.dividendTaxPercentage -= 10; - } -} - -//Levelable upgrades -Corporation.prototype.upgrade = function(upgrade) { - var upgN = upgrade[0], basePrice = upgrade[1], priceMult = upgrade[2], - upgradeAmt = upgrade[3]; //Amount by which the upgrade multiplier gets increased (additive) - while (this.upgrades.length <= upgN) {this.upgrades.push(0);} - while (this.upgradeMultipliers.length <= upgN) {this.upgradeMultipliers.push(1);} - var totalCost = basePrice * Math.pow(priceMult, this.upgrades[upgN]); - if (this.funds.lt(totalCost)) { - dialogBoxCreate("You don't have enough funds to purchase this!"); - return; - } - ++this.upgrades[upgN]; - this.funds = this.funds.minus(totalCost); - - //Increase upgrade multiplier - this.upgradeMultipliers[upgN] = 1 + (this.upgrades[upgN] * upgradeAmt); - - //If storage size is being updated, update values in Warehouse objects - if (upgN === 1) { - for (var i = 0; i < this.divisions.length; ++i) { - var industry = this.divisions[i]; - for (var city in industry.warehouses) { - if (industry.warehouses.hasOwnProperty(city) && industry.warehouses[city] instanceof Warehouse) { - industry.warehouses[city].updateSize(this, industry); - } - } - } - } -} - -Corporation.prototype.getProductionMultiplier = function() { - var mult = this.upgradeMultipliers[0]; - if (isNaN(mult) || mult < 1) {return 1;} else {return mult;} -} - -Corporation.prototype.getStorageMultiplier = function() { - var mult = this.upgradeMultipliers[1]; - if (isNaN(mult) || mult < 1) {return 1;} else {return mult;} -} - -Corporation.prototype.getDreamSenseGain = function() { - var gain = this.upgradeMultipliers[2] - 1; - return gain <= 0 ? 0 : gain; -} - -Corporation.prototype.getAdvertisingMultiplier = function() { - var mult = this.upgradeMultipliers[3]; - if (isNaN(mult) || mult < 1) {return 1;} else {return mult;} -} - -Corporation.prototype.getEmployeeCreMultiplier = function() { - var mult = this.upgradeMultipliers[4]; - if (isNaN(mult) || mult < 1) {return 1;} else {return mult;} -} - -Corporation.prototype.getEmployeeChaMultiplier = function() { - var mult = this.upgradeMultipliers[5]; - if (isNaN(mult) || mult < 1) {return 1;} else {return mult;} -} - -Corporation.prototype.getEmployeeIntMultiplier = function() { - var mult = this.upgradeMultipliers[6]; - if (isNaN(mult) || mult < 1) {return 1;} else {return mult;} -} - -Corporation.prototype.getEmployeeEffMultiplier = function() { - var mult = this.upgradeMultipliers[7]; - if (isNaN(mult) || mult < 1) {return 1;} else {return mult;} -} - -Corporation.prototype.getSalesMultiplier = function() { - var mult = this.upgradeMultipliers[8]; - if (isNaN(mult) || mult < 1) {return 1;} else {return mult;} -} - -Corporation.prototype.getScientificResearchMultiplier = function() { - var mult = this.upgradeMultipliers[9]; - if (isNaN(mult) || mult < 1) {return 1;} else {return mult;} -} - -// Adds the Corporation Handbook (Starter Guide) to the player's home computer. -// This is a lit file that gives introductory info to the player -// This occurs when the player clicks the "Getting Started Guide" button on the overview panel -Corporation.prototype.getStarterGuide = function() { - // Check if player already has Corporation Handbook - let homeComp = Player.getHomeComputer(), - hasHandbook = false, - handbookFn = LiteratureNames.CorporationManagementHandbook; - for (let i = 0; i < homeComp.messages.length; ++i) { - if (isString(homeComp.messages[i]) && homeComp.messages[i] === handbookFn) { - hasHandbook = true; - break; - } - } - - if (!hasHandbook) { homeComp.messages.push(handbookFn); } - showLiterature(handbookFn); - return false; -} - -let corpRouting; -let eventHandler; -let companyManagementDiv; -Corporation.prototype.createUI = function() { - companyManagementDiv = createElement("div", { - id:"cmpy-mgmt-container", - position:"fixed", - class:"generic-menupage-container", - }); - document.getElementById("entire-game-container").appendChild(companyManagementDiv); - - corpRouting = new CorporationRouting(this); - eventHandler = new CorporationEventHandler(this, corpRouting); - - this.rerender(); -} - -Corporation.prototype.rerender = function() { - if (companyManagementDiv == null || corpRouting == null || eventHandler == null) { - console.warn(`Corporation.rerender() called when companyManagementDiv, corpRouting, or eventHandler is null`); - return; - } - if (!routing.isOn(Page.Corporation)) { return; } - - ReactDOM.render(, companyManagementDiv); -} - -Corporation.prototype.clearUI = function() { - if (companyManagementDiv instanceof HTMLElement) { - ReactDOM.unmountComponentAtNode(companyManagementDiv); - removeElementById(companyManagementDiv.id); - } - - companyManagementDiv = null; - document.getElementById("character-overview-wrapper").style.visibility = "visible"; -} - -Corporation.prototype.toJSON = function() { - return Generic_toJSON("Corporation", this); -} - -Corporation.fromJSON = function(value) { - return Generic_fromJSON(Corporation, value.data); -} - -Reviver.constructors.Corporation = Corporation; - -export {Corporation, Industry, OfficeSpace, Warehouse}; diff --git a/src/Corporation/Corporation.tsx b/src/Corporation/Corporation.tsx new file mode 100644 index 000000000..139d9cdf5 --- /dev/null +++ b/src/Corporation/Corporation.tsx @@ -0,0 +1,434 @@ +import { CorporationState } from "./CorporationState"; +import { + CorporationUnlockUpgrade, + CorporationUnlockUpgrades } from "./data/CorporationUnlockUpgrades"; +import { CorporationUpgrade, CorporationUpgrades } from "./data/CorporationUpgrades"; +import { Warehouse } from "./Warehouse"; +import { CorporationConstants } from "./data/Constants"; +import { Industry } from "./Industry"; + +import { BitNodeMultipliers } from "../BitNode/BitNodeMultipliers"; +import { showLiterature } from "../Literature/LiteratureHelpers"; +import { LiteratureNames } from "../Literature/data/LiteratureNames"; +import { IPlayer } from "../PersonObjects/IPlayer"; + +import { Page, routing } from "../ui/navigationTracking"; + +import { dialogBoxCreate } from "../../utils/DialogBox"; +import { Reviver, + Generic_toJSON, + Generic_fromJSON } from "../../utils/JSONReviver"; +import { createElement } from "../../utils/uiHelpers/createElement"; +import { isString } from "../../utils/helpers/isString"; +import { removeElementById } from "../../utils/uiHelpers/removeElementById"; + +// UI Related Imports +import React from "react"; +import ReactDOM from "react-dom"; +import { CorporationRoot } from "./ui/Root"; +import { CorporationRouting } from "./ui/Routing"; + +import Decimal from "decimal.js"; + +interface IParams { + name?: string; +} + +let corpRouting: CorporationRouting; +let companyManagementDiv: HTMLDivElement | null = null; + +export class Corporation { + name = "The Corporation"; + + //A division/business sector is represented by the object: + divisions: Industry[] = []; + + //Financial stats + funds = new Decimal(150e9); + revenue = new Decimal(0); + expenses = new Decimal(0); + fundingRound = 0; + public = false; //Publicly traded + totalShares = CorporationConstants.INITIALSHARES; // Total existing shares + numShares = CorporationConstants.INITIALSHARES; // Total shares owned by player + shareSalesUntilPriceUpdate = CorporationConstants.SHARESPERPRICEUPDATE; + shareSaleCooldown = 0; // Game cycles until player can sell shares again + issueNewSharesCooldown = 0; // Game cycles until player can issue shares again + dividendPercentage = 0; + dividendTaxPercentage = 50; + issuedShares = 0; + sharePrice = 0; + storedCycles = 0; + + unlockUpgrades: number[]; + upgrades: number[]; + upgradeMultipliers: number[]; + + state = new CorporationState(); + + constructor(params: IParams = {}) { + this.name = params.name ? params.name : "The Corporation"; + const numUnlockUpgrades = Object.keys(CorporationUnlockUpgrades).length; + const numUpgrades = Object.keys(CorporationUpgrades).length; + this.unlockUpgrades = Array(numUnlockUpgrades).fill(0); + this.upgrades = Array(numUpgrades).fill(0); + this.upgradeMultipliers = Array(numUpgrades).fill(1); + } + + addFunds(amt: number): void { + if(!isFinite(amt)) { + console.error('Trying to add invalid amount of funds. Report to a developper.'); + return; + } + this.funds = this.funds.plus(amt); + } + + getState(): string { + return this.state.getState(); + } + + storeCycles(numCycles=1): void { + this.storedCycles += numCycles; + } + + process(player: IPlayer): void { + if (this.storedCycles >= CorporationConstants.CyclesPerIndustryStateCycle) { + const state = this.getState(); + const marketCycles = 1; + const gameCycles = (marketCycles * CorporationConstants.CyclesPerIndustryStateCycle); + this.storedCycles -= gameCycles; + + this.divisions.forEach((ind) => { + ind.process(marketCycles, state, this); + }); + + // Process cooldowns + if (this.shareSaleCooldown > 0) { + this.shareSaleCooldown -= gameCycles; + } + if (this.issueNewSharesCooldown > 0) { + this.issueNewSharesCooldown -= gameCycles; + } + + //At the start of a new cycle, calculate profits from previous cycle + if (state === "START") { + this.revenue = new Decimal(0); + this.expenses = new Decimal(0); + this.divisions.forEach((ind) => { + if (ind.lastCycleRevenue === -Infinity || ind.lastCycleRevenue === Infinity) { return; } + if (ind.lastCycleExpenses === -Infinity || ind.lastCycleExpenses === Infinity) { return; } + this.revenue = this.revenue.plus(ind.lastCycleRevenue); + this.expenses = this.expenses.plus(ind.lastCycleExpenses); + }); + const profit = this.revenue.minus(this.expenses); + const cycleProfit = profit.times(marketCycles * CorporationConstants.SecsPerMarketCycle); + if (isNaN(this.funds) || this.funds === Infinity || this.funds === -Infinity) { + dialogBoxCreate("There was an error calculating your Corporations funds and they got reset to 0. " + + "This is a bug. Please report to game developer.

" + + "(Your funds have been set to $150b for the inconvenience)"); + this.funds = new Decimal(150e9); + } + + // Process dividends + if (this.dividendPercentage > 0 && cycleProfit > 0) { + // Validate input again, just to be safe + if (isNaN(this.dividendPercentage) || this.dividendPercentage < 0 || this.dividendPercentage > CorporationConstants.DividendMaxPercentage*100) { + console.error(`Invalid Corporation dividend percentage: ${this.dividendPercentage}`); + } else { + const totalDividends = (this.dividendPercentage / 100) * cycleProfit; + const retainedEarnings = cycleProfit - totalDividends; + const dividendsPerShare = totalDividends / this.totalShares; + const profit = this.numShares * dividendsPerShare * (1 - (this.dividendTaxPercentage / 100)); + player.gainMoney(profit); + player.recordMoneySource(profit, "corporation"); + this.addFunds(retainedEarnings); + } + } else { + this.addFunds(cycleProfit); + } + + this.updateSharePrice(); + } + + this.state.nextState(); + + if (routing.isOn(Page.Corporation)) this.rerender(player); + } + } + + determineValuation(): number { + let val, profit = (this.revenue.minus(this.expenses)).toNumber(); + if (this.public) { + // Account for dividends + if (this.dividendPercentage > 0) { + profit *= ((100 - this.dividendPercentage) / 100); + } + + val = this.funds.toNumber() + (profit * 85e3); + val *= (Math.pow(1.1, this.divisions.length)); + val = Math.max(val, 0); + } else { + val = 10e9 + Math.max(this.funds.toNumber(), 0) / 3; //Base valuation + if (profit > 0) { + val += (profit * 315e3); + val *= (Math.pow(1.1, this.divisions.length)); + } else { + val = 10e9 * Math.pow(1.1, this.divisions.length); + } + val -= (val % 1e6); //Round down to nearest millionth + } + return val * BitNodeMultipliers.CorporationValuation; + } + + getTargetSharePrice(): number { + // Note: totalShares - numShares is not the same as issuedShares because + // issuedShares does not account for private investors + return this.determineValuation() / (2 * (this.totalShares - this.numShares) + 1); + } + + updateSharePrice(): void { + const targetPrice = this.getTargetSharePrice(); + if (this.sharePrice <= targetPrice) { + this.sharePrice *= (1 + (Math.random() * 0.01)); + } else { + this.sharePrice *= (1 - (Math.random() * 0.01)); + } + if (this.sharePrice <= 0.01) {this.sharePrice = 0.01;} + } + + immediatelyUpdateSharePrice(): void { + this.sharePrice = this.getTargetSharePrice(); + } + + // Calculates how much money will be made and what the resulting stock price + // will be when the player sells his/her shares + // @return - [Player profit, final stock price, end shareSalesUntilPriceUpdate property] + calculateShareSale(numShares: number): [number, number, number] { + let sharesTracker = numShares; + let sharesUntilUpdate = this.shareSalesUntilPriceUpdate; + let sharePrice = this.sharePrice; + let sharesSold = 0; + let profit = 0; + + const maxIterations = Math.ceil(numShares / CorporationConstants.SHARESPERPRICEUPDATE); + if (isNaN(maxIterations) || maxIterations > 10e6) { + console.error(`Something went wrong or unexpected when calculating share sale. Maxiterations calculated to be ${maxIterations}`); + return [0, 0, 0]; + } + + for (let i = 0; i < maxIterations; ++i) { + if (sharesTracker < sharesUntilUpdate) { + profit += (sharePrice * sharesTracker); + sharesUntilUpdate -= sharesTracker; + break; + } else { + profit += (sharePrice * sharesUntilUpdate); + sharesUntilUpdate = CorporationConstants.SHARESPERPRICEUPDATE; + sharesTracker -= sharesUntilUpdate; + sharesSold += sharesUntilUpdate; + + // Calculate what new share price would be + sharePrice = this.determineValuation() / (2 * (this.totalShares + sharesSold - this.numShares)); + } + } + + return [profit, sharePrice, sharesUntilUpdate]; + } + + convertCooldownToString(cd: number): string { + // The cooldown value is based on game cycles. Convert to a simple string + const seconds = cd / 5; + + const SecondsPerMinute = 60; + const SecondsPerHour = 3600; + + if (seconds > SecondsPerHour) { + return `${Math.floor(seconds / SecondsPerHour)} hour(s)`; + } else if (seconds > SecondsPerMinute) { + return `${Math.floor(seconds / SecondsPerMinute)} minute(s)`; + } else { + return `${Math.floor(seconds)} second(s)`; + } + } + + //One time upgrades that unlock new features + unlock(upgrade: CorporationUnlockUpgrade): void { + const upgN = upgrade[0], price = upgrade[1]; + while (this.unlockUpgrades.length <= upgN) { + this.unlockUpgrades.push(0); + } + if (this.funds.lt(price)) { + dialogBoxCreate("You don't have enough funds to unlock this!"); + return; + } + this.unlockUpgrades[upgN] = 1; + this.funds = this.funds.minus(price); + + // Apply effects for one-time upgrades + if (upgN === 5) { + this.dividendTaxPercentage -= 5; + } else if (upgN === 6) { + this.dividendTaxPercentage -= 10; + } + } + + //Levelable upgrades + upgrade(upgrade: CorporationUpgrade): void { + const upgN = upgrade[0], basePrice = upgrade[1], priceMult = upgrade[2], + upgradeAmt = upgrade[3]; //Amount by which the upgrade multiplier gets increased (additive) + while (this.upgrades.length <= upgN) {this.upgrades.push(0);} + while (this.upgradeMultipliers.length <= upgN) {this.upgradeMultipliers.push(1);} + const totalCost = basePrice * Math.pow(priceMult, this.upgrades[upgN]); + if (this.funds.lt(totalCost)) { + dialogBoxCreate("You don't have enough funds to purchase this!"); + return; + } + ++this.upgrades[upgN]; + this.funds = this.funds.minus(totalCost); + + //Increase upgrade multiplier + this.upgradeMultipliers[upgN] = 1 + (this.upgrades[upgN] * upgradeAmt); + + //If storage size is being updated, update values in Warehouse objects + if (upgN === 1) { + for (let i = 0; i < this.divisions.length; ++i) { + const industry = this.divisions[i]; + for (const city in industry.warehouses) { + const warehouse = industry.warehouses[city] + if(warehouse === 0) continue + if (industry.warehouses.hasOwnProperty(city) && warehouse instanceof Warehouse) { + warehouse.updateSize(this, industry); + } + } + } + } + } + + getProductionMultiplier(): number { + const mult = this.upgradeMultipliers[0]; + if (isNaN(mult) || mult < 1) {return 1;} else {return mult;} + } + + getStorageMultiplier(): number { + const mult = this.upgradeMultipliers[1]; + if (isNaN(mult) || mult < 1) {return 1;} else {return mult;} + } + + getDreamSenseGain(): number { + const gain = this.upgradeMultipliers[2] - 1; + return gain <= 0 ? 0 : gain; + } + + getAdvertisingMultiplier(): number { + const mult = this.upgradeMultipliers[3]; + if (isNaN(mult) || mult < 1) {return 1;} else {return mult;} + } + + getEmployeeCreMultiplier(): number { + const mult = this.upgradeMultipliers[4]; + if (isNaN(mult) || mult < 1) {return 1;} else {return mult;} + } + + getEmployeeChaMultiplier(): number { + const mult = this.upgradeMultipliers[5]; + if (isNaN(mult) || mult < 1) {return 1;} else {return mult;} + } + + getEmployeeIntMultiplier(): number { + const mult = this.upgradeMultipliers[6]; + if (isNaN(mult) || mult < 1) {return 1;} else {return mult;} + } + + getEmployeeEffMultiplier(): number { + const mult = this.upgradeMultipliers[7]; + if (isNaN(mult) || mult < 1) {return 1;} else {return mult;} + } + + getSalesMultiplier(): number { + const mult = this.upgradeMultipliers[8]; + if (isNaN(mult) || mult < 1) {return 1;} else {return mult;} + } + + getScientificResearchMultiplier(): number { + const mult = this.upgradeMultipliers[9]; + if (isNaN(mult) || mult < 1) {return 1;} else {return mult;} + } + + // Adds the Corporation Handbook (Starter Guide) to the player's home computer. + // This is a lit file that gives introductory info to the player + // This occurs when the player clicks the "Getting Started Guide" button on the overview panel + getStarterGuide(player: IPlayer): void { + // Check if player already has Corporation Handbook + const homeComp = player.getHomeComputer(); + let hasHandbook = false; + const handbookFn = LiteratureNames.CorporationManagementHandbook; + for (let i = 0; i < homeComp.messages.length; ++i) { + if (isString(homeComp.messages[i]) && homeComp.messages[i] === handbookFn) { + hasHandbook = true; + break; + } + } + + if (!hasHandbook) { homeComp.messages.push(handbookFn); } + showLiterature(handbookFn); + return; + } + + createUI(player: IPlayer): void { + companyManagementDiv = createElement("div", { + id:"cmpy-mgmt-container", + position:"fixed", + class:"generic-menupage-container", + }) as HTMLDivElement; + const game = document.getElementById("entire-game-container"); + if(game) + game.appendChild(companyManagementDiv); + + corpRouting = new CorporationRouting(this); + + this.rerender(player); + } + + rerender(player: IPlayer): void { + if (companyManagementDiv == null || corpRouting == null) { + console.warn(`Corporation.rerender() called when companyManagementDiv, corpRouting, or eventHandler is null`); + return; + } + if (!routing.isOn(Page.Corporation)) return; + + ReactDOM.render(, companyManagementDiv); + } + + clearUI(): void { + if (companyManagementDiv instanceof HTMLElement) { + ReactDOM.unmountComponentAtNode(companyManagementDiv); + removeElementById(companyManagementDiv.id); + } + + companyManagementDiv = null; + const character = document.getElementById("character-overview-wrapper"); + if(character) + character.style.visibility = "visible"; + } + + /** + * Serialize the current object to a JSON save state. + */ + toJSON(): any { + return Generic_toJSON("Corporation", this); + } + + /** + * Initiatizes a Corporation object from a JSON save state. + */ + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types + static fromJSON(value: any): Corporation { + return Generic_fromJSON(Corporation, value.data); + } +} + +Reviver.constructors.Corporation = Corporation; diff --git a/src/Corporation/Employee.ts b/src/Corporation/Employee.ts new file mode 100644 index 000000000..2750a6c0c --- /dev/null +++ b/src/Corporation/Employee.ts @@ -0,0 +1,197 @@ +import { CorporationConstants } from "./data/Constants"; +import { getRandomInt } from "../../utils/helpers/getRandomInt"; +import { Generic_fromJSON, Generic_toJSON, Reviver } from "../../utils/JSONReviver"; +import { createElement } from "../../utils/uiHelpers/createElement"; +import { EmployeePositions } from "./EmployeePositions"; +import { ICorporation } from "./ICorporation"; +import { numeralWrapper } from "../ui/numeralFormat"; +import { formatNumber } from "../../utils/StringHelperFunctions"; +import { OfficeSpace } from "./OfficeSpace"; +import { IIndustry } from "./IIndustry"; + +interface IParams { + name?: string; + morale?: number; + happiness?: number; + energy?: number; + intelligence?: number; + charisma?: number; + experience?: number; + creativity?: number; + efficiency?: number; + salary?: number; + loc?: string; +} + +export class Employee { + name: string; + mor: number; + hap: number; + ene: number; + int: number; + cha: number; + exp: number; + cre: number; + eff: number; + sal: number; + pro = 0; + cyclesUntilRaise = CorporationConstants.CyclesPerEmployeeRaise; + loc: string; + pos: string; + + constructor(params: IParams = {}) { + this.name = params.name ? params.name : "Bobby"; + + //Morale, happiness, and energy are 0-100 + this.mor = params.morale ? params.morale : getRandomInt(50, 100); + this.hap = params.happiness ? params.happiness : getRandomInt(50, 100); + this.ene = params.energy ? params.energy : getRandomInt(50, 100); + + this.int = params.intelligence ? params.intelligence : getRandomInt(10, 50); + this.cha = params.charisma ? params.charisma : getRandomInt(10, 50); + this.exp = params.experience ? params.experience : getRandomInt(10, 50); + this.cre = params.creativity ? params.creativity : getRandomInt(10, 50); + this.eff = params.efficiency ? params.efficiency : getRandomInt(10, 50); + this.sal = params.salary ? params.salary : getRandomInt(0.1, 5); + + this.loc = params.loc ? params.loc : ""; + this.pos = EmployeePositions.Unassigned; + } + + //Returns the amount the employee needs to be paid + process(marketCycles = 1, office: OfficeSpace): number { + const gain = 0.003 * marketCycles, + det = gain * Math.random(); + this.exp += gain; + + // Employee salaries slowly go up over time + this.cyclesUntilRaise -= marketCycles; + if (this.cyclesUntilRaise <= 0) { + this.sal += CorporationConstants.EmployeeRaiseAmount; + this.cyclesUntilRaise += CorporationConstants.CyclesPerEmployeeRaise; + } + + //Training + const trainingEff = gain * Math.random(); + if (this.pos === EmployeePositions.Training) { + //To increase creativity and intelligence special upgrades are needed + this.cha += trainingEff; + this.exp += trainingEff; + this.eff += trainingEff; + } + + this.ene -= det; + this.hap -= det; + + if (this.ene < office.minEne) {this.ene = office.minEne;} + if (this.hap < office.minHap) {this.hap = office.minHap;} + const salary = this.sal * marketCycles * CorporationConstants.SecsPerMarketCycle; + return salary; + } + + calculateProductivity(corporation: ICorporation, industry: IIndustry): number { + const effCre = this.cre * corporation.getEmployeeCreMultiplier() * industry.getEmployeeCreMultiplier(), + effCha = this.cha * corporation.getEmployeeChaMultiplier() * industry.getEmployeeChaMultiplier(), + effInt = this.int * corporation.getEmployeeIntMultiplier() * industry.getEmployeeIntMultiplier(), + effEff = this.eff * corporation.getEmployeeEffMultiplier() * industry.getEmployeeEffMultiplier(); + const prodBase = this.mor * this.hap * this.ene * 1e-6; + let prodMult = 0; + switch(this.pos) { + //Calculate productivity based on position. This is multipled by prodBase + //to get final value + case EmployeePositions.Operations: + prodMult = (0.6 * effInt) + (0.1 * effCha) + (this.exp) + + (0.5 * effCre) + (effEff); + break; + case EmployeePositions.Engineer: + prodMult = (effInt) + (0.1 * effCha) + (1.5 * this.exp) + + (effEff); + break; + case EmployeePositions.Business: + prodMult = (0.4 * effInt) + (effCha) + (0.5 * this.exp); + break; + case EmployeePositions.Management: + prodMult = (2 * effCha) + (this.exp) + (0.2 * effCre) + + (0.7 * effEff); + break; + case EmployeePositions.RandD: + prodMult = (1.5 * effInt) + (0.8 * this.exp) + (effCre) + + (0.5 * effEff); + break; + case EmployeePositions.Unassigned: + case EmployeePositions.Training: + prodMult = 0; + break; + default: + console.error(`Invalid employee position: ${this.pos}`); + break; + } + return prodBase * prodMult; + } + + //Process benefits from having an office party thrown + throwParty(money: number): number { + const mult = 1 + (money / 10e6); + this.mor *= mult; + this.mor = Math.min(100, this.mor); + this.hap *= mult; + this.hap = Math.min(100, this.hap); + return mult; + } + + //'panel' is the DOM element on which to create the UI + createUI(panel: HTMLElement, corporation: ICorporation, industry: IIndustry): void { + const effCre = this.cre * corporation.getEmployeeCreMultiplier() * industry.getEmployeeCreMultiplier(), + effCha = this.cha * corporation.getEmployeeChaMultiplier() * industry.getEmployeeChaMultiplier(), + effInt = this.int * corporation.getEmployeeIntMultiplier() * industry.getEmployeeIntMultiplier(), + effEff = this.eff * corporation.getEmployeeEffMultiplier() * industry.getEmployeeEffMultiplier(); + panel.style.color = "white"; + panel.appendChild(createElement("p", { + id:"cmpy-mgmt-employee-" + this.name + "-panel-text", + innerHTML:"Morale: " + formatNumber(this.mor, 3) + "
" + + "Happiness: " + formatNumber(this.hap, 3) + "
" + + "Energy: " + formatNumber(this.ene, 3) + "
" + + "Intelligence: " + formatNumber(effInt, 3) + "
" + + "Charisma: " + formatNumber(effCha, 3) + "
" + + "Experience: " + formatNumber(this.exp, 3) + "
" + + "Creativity: " + formatNumber(effCre, 3) + "
" + + "Efficiency: " + formatNumber(effEff, 3) + "
" + + "Salary: " + numeralWrapper.format(this.sal, "$0.000a") + "/ s
", + })); + + //Selector for employee position + const selector = createElement("select", {}) as HTMLSelectElement; + for (const key in EmployeePositions) { + if (EmployeePositions.hasOwnProperty(key)) { + selector.add(createElement("option", { + text: EmployeePositions[key], + value: EmployeePositions[key], + }) as HTMLOptionElement); + } + } + + selector.addEventListener("change", () => { + this.pos = selector.options[selector.selectedIndex].value; + }); + + //Set initial value of selector + for (let i = 0; i < selector.length; ++i) { + if (selector.options[i].value === this.pos) { + selector.selectedIndex = i; + break; + } + } + panel.appendChild(selector); + } + + toJSON(): any { + return Generic_toJSON("Employee", this); + } + + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types + static fromJSON(value: any): Employee { + return Generic_fromJSON(Employee, value.data); + } +} + +Reviver.constructors.Employee = Employee; \ No newline at end of file diff --git a/src/Corporation/Export.ts b/src/Corporation/Export.ts new file mode 100644 index 000000000..ff4ae9ec6 --- /dev/null +++ b/src/Corporation/Export.ts @@ -0,0 +1,5 @@ +export interface Export { + ind: string; + city: string; + amt: string; +} \ No newline at end of file diff --git a/src/Corporation/ICorporation.ts b/src/Corporation/ICorporation.ts new file mode 100644 index 000000000..775774458 --- /dev/null +++ b/src/Corporation/ICorporation.ts @@ -0,0 +1,61 @@ +import { Industry } from "./Industry"; +import { IPlayer } from "../PersonObjects/IPlayer"; +import { CorporationUnlockUpgrade } from "./data/CorporationUnlockUpgrades"; +import { CorporationUpgrade } from "./data/CorporationUpgrades"; +import { CorporationState } from "./CorporationState"; + +export interface ICorporation { + name: string; + + divisions: Industry[]; + + funds: any; + revenue: any; + expenses: any; + fundingRound: number; + public: boolean; + totalShares: number; + numShares: number; + shareSalesUntilPriceUpdate: number; + shareSaleCooldown: number; + issueNewSharesCooldown: number; + dividendPercentage: number; + dividendTaxPercentage: number; + issuedShares: number; + sharePrice: number; + storedCycles: number; + + unlockUpgrades: number[]; + upgrades: number[]; + upgradeMultipliers: number[]; + + state: CorporationState; + + addFunds(amt: number): void; + getState(): string; + storeCycles(numCycles: number): void; + process(player: IPlayer): void; + determineValuation(): number; + getTargetSharePrice(): number; + updateSharePrice(): void; + immediatelyUpdateSharePrice(): void; + calculateShareSale(numShares: number): [number, number, number]; + convertCooldownToString(cd: number): string; + unlock(upgrade: CorporationUnlockUpgrade): void; + upgrade(upgrade: CorporationUpgrade): void; + getProductionMultiplier(): number; + getStorageMultiplier(): number; + getDreamSenseGain(): number; + getAdvertisingMultiplier(): number; + getEmployeeCreMultiplier(): number; + getEmployeeChaMultiplier(): number; + getEmployeeIntMultiplier(): number; + getEmployeeEffMultiplier(): number; + getSalesMultiplier(): number; + getScientificResearchMultiplier(): number; + getStarterGuide(player: IPlayer): void; + createUI(player: IPlayer): void; + rerender(player: IPlayer): void; + clearUI(): void; + toJSON(): any; +} \ No newline at end of file diff --git a/src/Corporation/IIndustry.ts b/src/Corporation/IIndustry.ts new file mode 100644 index 000000000..298d92fd5 --- /dev/null +++ b/src/Corporation/IIndustry.ts @@ -0,0 +1,77 @@ +import { Material } from "./Material"; +import { Warehouse } from "./Warehouse"; +import { ICorporation } from "./ICorporation"; +import { OfficeSpace } from "./OfficeSpace"; +import { Product } from "./Product"; +import { IndustryUpgrade } from "./IndustryUpgrades"; + +export interface IIndustry { + name: string; + type: string; + sciResearch: Material; + researched: {[key: string]: boolean | undefined}; + reqMats: {[key: string]: number | undefined}; + + prodMats: string[]; + + products: {[key: string]: Product | undefined}; + makesProducts: boolean; + + awareness: number; + popularity: number; + startingCost: number; + + reFac: number; + sciFac: number; + hwFac: number; + robFac: number; + aiFac: number; + advFac: number; + + prodMult: number; + + // Decimal + lastCycleRevenue: any; + lastCycleExpenses: any; + thisCycleRevenue: any; + thisCycleExpenses: any; + + upgrades: number[]; + + state: string; + newInd: boolean; + warehouses: {[key: string]: Warehouse | 0}; + offices: {[key: string]: OfficeSpace | 0}; + + init(): void; + getProductDescriptionText(): string; + getMaximumNumberProducts(): number; + hasMaximumNumberProducts(): boolean; + calculateProductionFactors(): void; + updateWarehouseSizeUsed(warehouse: Warehouse): void; + process(marketCycles: number, state: string, corporation: ICorporation): void; + processMaterialMarket(): void; + processProductMarket(marketCycles: number): void; + processMaterials(marketCycles: number, corporation: ICorporation): [number, number]; + processProducts(marketCycles: number, corporation: ICorporation): [number, number]; + processProduct(marketCycles: number, product: Product, corporation: ICorporation): number; + discontinueProduct(product: Product): void; + upgrade(upgrade: IndustryUpgrade, refs: {corporation: ICorporation; office: OfficeSpace}): void; + getOfficeProductivity(office: OfficeSpace, params?: {forProduct?: boolean}): number; + getBusinessFactor(office: OfficeSpace): number; + getAdvertisingFactors(): [number, number, number, number]; + getMarketFactor(mat: {dmd: number; cmp: number}): number; + hasResearch(name: string): boolean; + updateResearchTree(): void; + getAdvertisingMultiplier(): number; + getEmployeeChaMultiplier(): number; + getEmployeeCreMultiplier(): number; + getEmployeeEffMultiplier(): number; + getEmployeeIntMultiplier(): number; + getProductionMultiplier(): number; + getProductProductionMultiplier(): number; + getSalesMultiplier(): number; + getScientificResearchMultiplier(): number; + getStorageMultiplier(): number; + toJSON(): any; +} \ No newline at end of file diff --git a/src/Corporation/Industry.ts b/src/Corporation/Industry.ts new file mode 100644 index 000000000..49c68203d --- /dev/null +++ b/src/Corporation/Industry.ts @@ -0,0 +1,1353 @@ +import { + Reviver, + Generic_toJSON, + Generic_fromJSON, +} from "../../utils/JSONReviver"; +import { CityName } from "../Locations/data/CityNames"; +import Decimal from 'decimal.js'; +import { Industries, + IndustryStartingCosts, + IndustryResearchTrees } from "./IndustryData"; +import { CorporationConstants } from "./data/Constants"; +import { EmployeePositions } from "./EmployeePositions"; +import { Material } from "./Material"; +import { getRandomInt } from "../../utils/helpers/getRandomInt"; +import { calculateEffectWithFactors } from "../utils/calculateEffectWithFactors"; +import { OfficeSpace } from "./OfficeSpace"; +import { Product } from "./Product"; +import { dialogBoxCreate } from "../../utils/DialogBox"; +import { isString } from "../../utils/helpers/isString"; +import { MaterialSizes } from "./MaterialSizes"; +import { Warehouse } from "./Warehouse"; +import { ICorporation } from "./ICorporation"; +import { IIndustry } from "./IIndustry"; +import { + IndustryUpgrade, + IndustryUpgrades } from "./IndustryUpgrades"; +import { formatNumber } from "../../utils/StringHelperFunctions"; + +interface IParams { + name?: string; + corp?: ICorporation; + type?: string; +} + +export class Industry implements IIndustry { + name = ""; + type = Industries.Agriculture; + sciResearch = new Material({name: "Scientific Research"}); + researched: {[key: string]: boolean | undefined} = {}; + reqMats: {[key: string]: number | undefined} = {}; + + //An array of the name of materials being produced + prodMats: string[] = []; + + products: {[key: string]: Product | undefined} = {}; + makesProducts = false; + + awareness = 0; + popularity = 0; //Should always be less than awareness + startingCost = 0; + + /* The following are factors for how much production/other things are increased by + different factors. The production increase always has diminishing returns, + and they are all reprsented by exponentials of < 1 (e.g x ^ 0.5, x ^ 0.8) + The number for these represent the exponential. A lower number means more + diminishing returns */ + reFac = 0; //Real estate Factor + sciFac = 0; //Scientific Research Factor, affects quality + hwFac = 0; //Hardware factor + robFac = 0; //Robotics Factor + aiFac = 0; //AI Cores factor; + advFac = 0; //Advertising factor, affects sales + + prodMult = 0; //Production multiplier + + //Financials + lastCycleRevenue: any; + lastCycleExpenses: any; + thisCycleRevenue: any; + thisCycleExpenses: any; + + //Upgrades + upgrades: number[] = Array(Object.keys(IndustryUpgrades).length).fill(0); + + state = "START"; + newInd = true; + + //Maps locations to warehouses. 0 if no warehouse at that location + warehouses: {[key: string]: Warehouse | 0}; + + //Maps locations to offices. 0 if no office at that location + offices: {[key: string]: OfficeSpace | 0} = { + [CityName.Aevum]: 0, + [CityName.Chongqing]: 0, + [CityName.Sector12]: new OfficeSpace({ + loc: CityName.Sector12, + size: CorporationConstants.OfficeInitialSize, + }), + [CityName.NewTokyo]: 0, + [CityName.Ishima]: 0, + [CityName.Volhaven]: 0, + }; + + constructor(params: IParams = {}) { + this.name = params.name ? params.name : ''; + this.type = params.type ? params.type : Industries.Agriculture; + + //Financials + this.lastCycleRevenue = new Decimal(0); + this.lastCycleExpenses = new Decimal(0); + this.thisCycleRevenue = new Decimal(0); + this.thisCycleExpenses = new Decimal(0); + + this.warehouses = { + [CityName.Aevum]: 0, + [CityName.Chongqing]: 0, + [CityName.Sector12]: new Warehouse({ + corp: params.corp, + industry: this, + loc: CityName.Sector12, + size: CorporationConstants.WarehouseInitialSize, + }), + [CityName.NewTokyo]: 0, + [CityName.Ishima]: 0, + [CityName.Volhaven]: 0, + }; + + this.init(); + } + + init(): void { + //Set the unique properties of an industry (how much its affected by real estate/scientific research, etc.) + const startingCost = IndustryStartingCosts[this.type]; + if(startingCost === undefined) + throw new Error(`Invalid industry: "${this.type}"`); + this.startingCost = startingCost; + switch (this.type) { + case Industries.Energy: + this.reFac = 0.65; + this.sciFac = 0.7; + this.robFac = 0.05; + this.aiFac = 0.3; + this.advFac = 0.08; + this.reqMats = { + "Hardware": 0.1, + "Metal": 0.2, + }; + this.prodMats = ["Energy"]; + break; + case Industries.Utilities: + case "Utilities": + this.reFac = 0.5; + this.sciFac = 0.6; + this.robFac = 0.4; + this.aiFac = 0.4; + this.advFac = 0.08; + this.reqMats = { + "Hardware": 0.1, + "Metal": 0.1, + } + this.prodMats = ["Water"]; + break; + case Industries.Agriculture: + this.reFac = 0.72; + this.sciFac = 0.5; + this.hwFac = 0.2; + this.robFac = 0.3; + this.aiFac = 0.3; + this.advFac = 0.04; + this.reqMats = { + "Water": 0.5, + "Energy": 0.5, + } + this.prodMats = ["Plants", "Food"]; + break; + case Industries.Fishing: + this.reFac = 0.15; + this.sciFac = 0.35; + this.hwFac = 0.35; + this.robFac = 0.5; + this.aiFac = 0.2; + this.advFac = 0.08; + this.reqMats = { + "Energy": 0.5, + } + this.prodMats = ["Food"]; + break; + case Industries.Mining: + this.reFac = 0.3; + this.sciFac = 0.26; + this.hwFac = 0.4; + this.robFac = 0.45; + this.aiFac = 0.45; + this.advFac = 0.06; + this.reqMats = { + "Energy": 0.8, + } + this.prodMats = ["Metal"]; + break; + case Industries.Food: + //reFac is unique for this bc it diminishes greatly per city. Handle this separately in code? + this.sciFac = 0.12; + this.hwFac = 0.15; + this.robFac = 0.3; + this.aiFac = 0.25; + this.advFac = 0.25; + this.reFac = 0.05; + this.reqMats = { + "Food": 0.5, + "Water": 0.5, + "Energy": 0.2, + } + this.makesProducts = true; + break; + case Industries.Tobacco: + this.reFac = 0.15; + this.sciFac = 0.75; + this.hwFac = 0.15; + this.robFac = 0.2; + this.aiFac = 0.15; + this.advFac = 0.2; + this.reqMats = { + "Plants": 1, + "Water": 0.2, + } + this.makesProducts = true; + break; + case Industries.Chemical: + this.reFac = 0.25; + this.sciFac = 0.75; + this.hwFac = 0.2; + this.robFac = 0.25; + this.aiFac = 0.2; + this.advFac = 0.07; + this.reqMats = { + "Plants": 1, + "Energy": 0.5, + "Water": 0.5, + } + this.prodMats = ["Chemicals"]; + break; + case Industries.Pharmaceutical: + this.reFac = 0.05; + this.sciFac = 0.8; + this.hwFac = 0.15; + this.robFac = 0.25; + this.aiFac = 0.2; + this.advFac = 0.16; + this.reqMats = { + "Chemicals": 2, + "Energy": 1, + "Water": 0.5, + } + this.prodMats = ["Drugs"]; + this.makesProducts = true; + break; + case Industries.Computer: + case "Computer": + this.reFac = 0.2; + this.sciFac = 0.62; + this.robFac = 0.36; + this.aiFac = 0.19; + this.advFac = 0.17; + this.reqMats = { + "Metal": 2, + "Energy": 1, + } + this.prodMats = ["Hardware"]; + this.makesProducts = true; + break; + case Industries.Robotics: + this.reFac = 0.32; + this.sciFac = 0.65; + this.aiFac = 0.36; + this.advFac = 0.18; + this.hwFac = 0.19; + this.reqMats = { + "Hardware": 5, + "Energy": 3, + } + this.prodMats = ["Robots"]; + this.makesProducts = true; + break; + case Industries.Software: + this.sciFac = 0.62; + this.advFac = 0.16; + this.hwFac = 0.25; + this.reFac = 0.15; + this.aiFac = 0.18; + this.robFac = 0.05; + this.reqMats = { + "Hardware": 0.5, + "Energy": 0.5, + } + this.prodMats = ["AICores"]; + this.makesProducts = true; + break; + case Industries.Healthcare: + this.reFac = 0.1; + this.sciFac = 0.75; + this.advFac = 0.11; + this.hwFac = 0.1; + this.robFac = 0.1; + this.aiFac = 0.1; + this.reqMats = { + "Robots": 10, + "AICores": 5, + "Energy": 5, + "Water": 5, + } + this.makesProducts = true; + break; + case Industries.RealEstate: + this.robFac = 0.6; + this.aiFac = 0.6; + this.advFac = 0.25; + this.sciFac = 0.05; + this.hwFac = 0.05; + this.reqMats = { + "Metal": 5, + "Energy": 5, + "Water": 2, + "Hardware": 4, + } + this.prodMats = ["RealEstate"]; + this.makesProducts = true; + break; + default: + console.error(`Invalid Industry Type passed into Industry.init(): ${this.type}`); + return; + } + } + + getProductDescriptionText(): string { + if (!this.makesProducts) return ''; + switch (this.type) { + case Industries.Food: + return "create and manage restaurants"; + case Industries.Tobacco: + return "create tobacco and tobacco-related products"; + case Industries.Pharmaceutical: + return "develop new pharmaceutical drugs"; + case Industries.Computer: + case "Computer": + return "create new computer hardware and networking infrastructures"; + case Industries.Robotics: + return "build specialized robots and robot-related products"; + case Industries.Software: + return "develop computer software"; + case Industries.Healthcare: + return "build and manage hospitals"; + case Industries.RealEstate: + return "develop and manage real estate properties"; + default: + console.error("Invalid industry type in Industry.getProductDescriptionText"); + return ""; + } + } + + getMaximumNumberProducts(): number { + if (!this.makesProducts) return 0; + + // Calculate additional number of allowed Products from Research/Upgrades + let additional = 0; + if (this.hasResearch("uPgrade: Capacity.I")) ++additional; + if (this.hasResearch("uPgrade: Capacity.II")) ++additional; + + return CorporationConstants.BaseMaxProducts + additional; + } + + hasMaximumNumberProducts(): boolean { + return (Object.keys(this.products).length >= this.getMaximumNumberProducts()); + } + + //Calculates the values that factor into the production and properties of + //materials/products (such as quality, etc.) + calculateProductionFactors(): void { + let multSum = 0; + for (let i = 0; i < CorporationConstants.Cities.length; ++i) { + const city = CorporationConstants.Cities[i]; + const warehouse = this.warehouses[city]; + if (!(warehouse instanceof Warehouse)) { + continue; + } + + const materials = warehouse.materials; + + const cityMult = Math.pow(0.002 * materials.RealEstate.qty+1, this.reFac) * + Math.pow(0.002 * materials.Hardware.qty+1, this.hwFac) * + Math.pow(0.002 * materials.Robots.qty+1, this.robFac) * + Math.pow(0.002 * materials.AICores.qty+1, this.aiFac); + multSum += Math.pow(cityMult, 0.73); + } + + multSum < 1 ? this.prodMult = 1 : this.prodMult = multSum; + } + + updateWarehouseSizeUsed(warehouse: Warehouse): void { + warehouse.updateMaterialSizeUsed(); + + for (const prodName in this.products) { + if (this.products.hasOwnProperty(prodName)) { + const prod = this.products[prodName]; + if(prod === undefined) continue; + warehouse.sizeUsed += (prod.data[warehouse.loc][0] * prod.siz); + if (prod.data[warehouse.loc][0] > 0) { + warehouse.breakdown += (prodName + ": " + formatNumber(prod.data[warehouse.loc][0] * prod.siz, 0) + "
"); + } + } + } + } + + process(marketCycles=1, state: string, corporation: ICorporation): void { + this.state = state; + + //At the start of a cycle, store and reset revenue/expenses + //Then calculate salaries and processs the markets + if (state === "START") { + if (isNaN(this.thisCycleRevenue) || isNaN(this.thisCycleExpenses)) { + console.error("NaN in Corporation's computed revenue/expenses"); + dialogBoxCreate("Something went wrong when compting Corporation's revenue/expenses. This is a bug. Please report to game developer"); + this.thisCycleRevenue = new Decimal(0); + this.thisCycleExpenses = new Decimal(0); + } + this.lastCycleRevenue = this.thisCycleRevenue.dividedBy(marketCycles * CorporationConstants.SecsPerMarketCycle); + this.lastCycleExpenses = this.thisCycleExpenses.dividedBy(marketCycles * CorporationConstants.SecsPerMarketCycle); + this.thisCycleRevenue = new Decimal(0); + this.thisCycleExpenses = new Decimal(0); + + // Once you start making revenue, the player should no longer be + // considered new, and therefore no longer needs the 'tutorial' UI elements + if (this.lastCycleRevenue.gt(0)) {this.newInd = false;} + + // Process offices (and the employees in them) + let employeeSalary = 0; + for (const officeLoc in this.offices) { + const office = this.offices[officeLoc]; + if(office === 0) continue; + if (office instanceof OfficeSpace) { + employeeSalary += office.process(marketCycles, corporation, this); + } + } + this.thisCycleExpenses = this.thisCycleExpenses.plus(employeeSalary); + + // Process change in demand/competition of materials/products + this.processMaterialMarket(); + this.processProductMarket(marketCycles); + + // Process loss of popularity + this.popularity -= (marketCycles * .0001); + this.popularity = Math.max(0, this.popularity); + + // Process Dreamsense gains + const popularityGain = corporation.getDreamSenseGain(), awarenessGain = popularityGain * 4; + if (popularityGain > 0) { + this.popularity += (popularityGain * marketCycles); + this.awareness += (awarenessGain * marketCycles); + } + + return; + } + + // Process production, purchase, and import/export of materials + let res = this.processMaterials(marketCycles, corporation); + if (Array.isArray(res)) { + this.thisCycleRevenue = this.thisCycleRevenue.plus(res[0]); + this.thisCycleExpenses = this.thisCycleExpenses.plus(res[1]); + } + + // Process creation, production & sale of products + res = this.processProducts(marketCycles, corporation); + if (Array.isArray(res)) { + this.thisCycleRevenue = this.thisCycleRevenue.plus(res[0]); + this.thisCycleExpenses = this.thisCycleExpenses.plus(res[1]); + } + } + + // Process change in demand and competition for this industry's materials + processMaterialMarket(): void { + //References to prodMats and reqMats + const reqMats = this.reqMats, prodMats = this.prodMats; + + //Only 'process the market' for materials that this industry deals with + for (let i = 0; i < CorporationConstants.Cities.length; ++i) { + //If this industry has a warehouse in this city, process the market + //for every material this industry requires or produces + if (this.warehouses[CorporationConstants.Cities[i]] instanceof Warehouse) { + const wh = this.warehouses[CorporationConstants.Cities[i]]; + if(wh === 0) continue; + for (const name in reqMats) { + if (reqMats.hasOwnProperty(name)) { + wh.materials[name].processMarket(); + } + } + + //Produced materials are stored in an array + for (let foo = 0; foo < prodMats.length; ++foo) { + wh.materials[prodMats[foo]].processMarket(); + } + + //Process these twice because these boost production + wh.materials["Hardware"].processMarket(); + wh.materials["Robots"].processMarket(); + wh.materials["AICores"].processMarket(); + wh.materials["RealEstate"].processMarket(); + } + } + } + + // Process change in demand and competition for this industry's products + processProductMarket(marketCycles=1): void { + // Demand gradually decreases, and competition gradually increases + for (const name in this.products) { + if (this.products.hasOwnProperty(name)) { + const product = this.products[name]; + if(product === undefined) continue; + let change = getRandomInt(0, 3) * 0.0004; + if (change === 0) continue; + + if (this.type === Industries.Pharmaceutical || this.type === Industries.Software || + this.type === Industries.Robotics) { + change *= 3; + } + change *= marketCycles; + product.dmd -= change; + product.cmp += change; + product.cmp = Math.min(product.cmp, 99.99); + product.dmd = Math.max(product.dmd, 0.001); + } + } + } + + //Process production, purchase, and import/export of materials + processMaterials(marketCycles=1, corporation: ICorporation): [number, number] { + let revenue = 0, expenses = 0; + this.calculateProductionFactors(); + + //At the start of the export state, set the imports of everything to 0 + if (this.state === "EXPORT") { + for (let i = 0; i < CorporationConstants.Cities.length; ++i) { + const city = CorporationConstants.Cities[i]; + if (!(this.warehouses[city] instanceof Warehouse)) { + continue; + } + const warehouse = this.warehouses[city]; + if(warehouse === 0) continue; + for (const matName in warehouse.materials) { + if (warehouse.materials.hasOwnProperty(matName)) { + const mat = warehouse.materials[matName]; + mat.imp = 0; + } + } + } + } + + for (let i = 0; i < CorporationConstants.Cities.length; ++i) { + const city = CorporationConstants.Cities[i]; + const office = this.offices[city]; + if(office === 0) continue; + + if (this.warehouses[city] instanceof Warehouse) { + const warehouse = this.warehouses[city]; + if(warehouse === 0) continue; + + switch(this.state) { + + case "PURCHASE": + /* Process purchase of materials */ + for (const matName in warehouse.materials) { + if (warehouse.materials.hasOwnProperty(matName)) { + (function(matName, ind) { + const mat = warehouse.materials[matName]; + let buyAmt, maxAmt; + if (warehouse.smartSupplyEnabled && Object.keys(ind.reqMats).includes(matName)) { + //Smart supply tracker is stored as per second rate + const reqMat = ind.reqMats[matName]; + if(reqMat === undefined) + throw new Error(`reqMat "${matName}" is undefined`); + mat.buy = reqMat * warehouse.smartSupplyStore; + buyAmt = mat.buy * CorporationConstants.SecsPerMarketCycle * marketCycles; + } else { + buyAmt = (mat.buy * CorporationConstants.SecsPerMarketCycle * marketCycles); + } + + if (matName == "RealEstate") { + maxAmt = buyAmt; + } else { + maxAmt = Math.floor((warehouse.size - warehouse.sizeUsed) / MaterialSizes[matName]); + } + buyAmt = Math.min(buyAmt, maxAmt); + if (buyAmt > 0) { + mat.qty += buyAmt; + expenses += (buyAmt * mat.bCost); + } + })(matName, this); + this.updateWarehouseSizeUsed(warehouse); + } + } //End process purchase of materials + break; + + case "PRODUCTION": + warehouse.smartSupplyStore = 0; //Reset smart supply amount + + /* Process production of materials */ + if (this.prodMats.length > 0) { + const mat = warehouse.materials[this.prodMats[0]]; + //Calculate the maximum production of this material based + //on the office's productivity + const maxProd = this.getOfficeProductivity(office) + * this.prodMult // Multiplier from materials + * corporation.getProductionMultiplier() + * this.getProductionMultiplier(); // Multiplier from Research + let prod; + + if (mat.prdman[0]) { + //Production is manually limited + prod = Math.min(maxProd, mat.prdman[1]); + } else { + prod = maxProd; + } + prod *= (CorporationConstants.SecsPerMarketCycle * marketCycles); //Convert production from per second to per market cycle + + // Calculate net change in warehouse storage making the produced materials will cost + let totalMatSize = 0; + for (let tmp = 0; tmp < this.prodMats.length; ++tmp) { + totalMatSize += (MaterialSizes[this.prodMats[tmp]]); + } + for (const reqMatName in this.reqMats) { + const normQty = this.reqMats[reqMatName]; + if(normQty === undefined) continue; + totalMatSize -= (MaterialSizes[reqMatName] * normQty); + } + // If not enough space in warehouse, limit the amount of produced materials + if (totalMatSize > 0) { + const maxAmt = Math.floor((warehouse.size - warehouse.sizeUsed) / totalMatSize); + prod = Math.min(maxAmt, prod); + } + + if (prod < 0) {prod = 0;} + + // Keep track of production for smart supply (/s) + warehouse.smartSupplyStore += (prod / (CorporationConstants.SecsPerMarketCycle * marketCycles)); + + // Make sure we have enough resource to make our materials + let producableFrac = 1; + for (const reqMatName in this.reqMats) { + if (this.reqMats.hasOwnProperty(reqMatName)) { + const reqMat = this.reqMats[reqMatName]; + if(reqMat === undefined) continue; + const req = reqMat * prod; + if (warehouse.materials[reqMatName].qty < req) { + producableFrac = Math.min(producableFrac, warehouse.materials[reqMatName].qty / req); + } + } + } + if (producableFrac <= 0) {producableFrac = 0; prod = 0;} + + // Make our materials if they are producable + if (producableFrac > 0 && prod > 0) { + for (const reqMatName in this.reqMats) { + const reqMat = this.reqMats[reqMatName]; + if(reqMat === undefined) continue; + const reqMatQtyNeeded = (reqMat * prod * producableFrac); + warehouse.materials[reqMatName].qty -= reqMatQtyNeeded; + warehouse.materials[reqMatName].prd = 0; + warehouse.materials[reqMatName].prd -= reqMatQtyNeeded / (CorporationConstants.SecsPerMarketCycle * marketCycles); + } + 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 + + Math.pow(this.sciResearch.qty, this.sciFac) + + Math.pow(warehouse.materials["AICores"].qty, this.aiFac) / 10e3); + } + } else { + for (const reqMatName in this.reqMats) { + if (this.reqMats.hasOwnProperty(reqMatName)) { + warehouse.materials[reqMatName].prd = 0; + } + } + } + + //Per second + const fooProd = prod * producableFrac / (CorporationConstants.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 (const reqMatName in this.reqMats) { + warehouse.materials[reqMatName].prd = 0; + } + } + break; + + case "SALE": + /* Process sale of materials */ + for (const matName in warehouse.materials) { + if (warehouse.materials.hasOwnProperty(matName)) { + const mat = warehouse.materials[matName]; + if (mat.sCost < 0 || mat.sllman[0] === false) { + mat.sll = 0; + continue; + } + + // 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(); + let sCost; + 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 + * corporation.getSalesMultiplier() + * advertisingFactor + * this.getSalesMultiplier()); + const denominator = Math.sqrt(sqrtNumerator / sqrtDenominator); + let optimalPrice; + if (sqrtDenominator === 0 || denominator === 0) { + if (sqrtNumerator === 0) { + optimalPrice = 0; // No production + } else { + optimalPrice = mat.bCost + markupLimit; + console.warn(`In Corporation, found illegal 0s when trying to calculate MarketTA2 sale cost`); + } + } else { + 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 as string).replace(/MP/g, mat.bCost+''); + sCost = eval(sCost); + } else { + sCost = mat.sCost; + } + + // Calculate how much of the material sells (per second) + let markup = 1; + if (sCost > mat.bCost) { + //Penalty if difference between sCost and bCost is greater than markup limit + if ((sCost - mat.bCost) > markupLimit) { + markup = Math.pow(markupLimit / (sCost - mat.bCost), 2); + } + } else if (sCost < mat.bCost) { + if (sCost <= 0) { + markup = 1e12; //Sell everything, essentially discard + } else { + //Lower prices than market increases sales + markup = mat.bCost / sCost; + } + } + + const maxSell = (mat.qlt + .001) + * marketFactor + * markup + * businessFactor + * corporation.getSalesMultiplier() + * advertisingFactor + * this.getSalesMultiplier(); + let sellAmt; + if (isString(mat.sllman[1])) { + //Dynamically evaluated + let tmp = (mat.sllman[1] as string).replace(/MAX/g, maxSell+''); + tmp = tmp.replace(/PROD/g, mat.prd+''); + try { + sellAmt = eval(tmp); + } catch(e) { + dialogBoxCreate("Error evaluating your sell amount for material " + mat.name + + " in " + this.name + "'s " + city + " office. The sell amount " + + "is being set to zero"); + sellAmt = 0; + } + sellAmt = Math.min(maxSell, sellAmt); + } else if (mat.sllman[1] === -1) { + //Backwards compatibility, -1 = MAX + sellAmt = maxSell; + } else { + //Player's input value is just a number + sellAmt = Math.min(maxSell, mat.sllman[1] as number); + } + + sellAmt = (sellAmt * CorporationConstants.SecsPerMarketCycle * marketCycles); + sellAmt = Math.min(mat.qty, sellAmt); + if (sellAmt < 0) { + console.warn(`sellAmt calculated to be negative for ${matName} in ${city}`); + mat.sll = 0; + continue; + } + if (sellAmt && sCost >= 0) { + mat.qty -= sellAmt; + revenue += (sellAmt * sCost); + mat.sll = sellAmt / (CorporationConstants.SecsPerMarketCycle * marketCycles); + } else { + mat.sll = 0; + } + } + } //End processing of sale of materials + break; + + case "EXPORT": + for (const matName in warehouse.materials) { + if (warehouse.materials.hasOwnProperty(matName)) { + const mat = warehouse.materials[matName]; + mat.totalExp = 0; //Reset export + for (let expI = 0; expI < mat.exp.length; ++expI) { + const exp = mat.exp[expI]; + const amtStr = exp.amt.replace(/MAX/g, (mat.qty / (CorporationConstants.SecsPerMarketCycle * marketCycles))+''); + let amt = 0; + try { + amt = eval(amtStr); + } catch(e) { + dialogBoxCreate("Calculating export for " + mat.name + " in " + + this.name + "'s " + city + " division failed with " + + "error: " + e); + continue; + } + if (isNaN(amt)) { + dialogBoxCreate("Error calculating export amount for " + mat.name + " in " + + this.name + "'s " + city + " division."); + continue; + } + amt = amt * CorporationConstants.SecsPerMarketCycle * marketCycles; + + if (mat.qty < amt) { + amt = mat.qty; + } + if (amt === 0) { + break; //None left + } + for (let foo = 0; foo < corporation.divisions.length; ++foo) { + if (corporation.divisions[foo].name === exp.ind) { + const expIndustry = corporation.divisions[foo]; + const expWarehouse = expIndustry.warehouses[exp.city]; + if (!(expWarehouse instanceof Warehouse)) { + console.error(`Invalid export! ${expIndustry.name} ${exp.city}`); + break; + } + + // Make sure theres enough space in warehouse + if (expWarehouse.sizeUsed >= expWarehouse.size) { + // Warehouse at capacity. Exporting doesnt + // affect revenue so just return 0's + return [0, 0]; + } else { + const maxAmt = Math.floor((expWarehouse.size - expWarehouse.sizeUsed) / MaterialSizes[matName]); + amt = Math.min(maxAmt, amt); + } + expWarehouse.materials[matName].imp += (amt / (CorporationConstants.SecsPerMarketCycle * marketCycles)); + expWarehouse.materials[matName].qty += amt; + expWarehouse.materials[matName].qlt = mat.qlt; + mat.qty -= amt; + mat.totalExp += amt; + expIndustry.updateWarehouseSizeUsed(expWarehouse); + break; + } + } + } + //totalExp should be per second + mat.totalExp /= (CorporationConstants.SecsPerMarketCycle * marketCycles); + } + } + + break; + + case "START": + break; + default: + console.error(`Invalid state: ${this.state}`); + break; + } //End switch(this.state) + this.updateWarehouseSizeUsed(warehouse); + + } // End warehouse + + //Produce Scientific Research based on R&D employees + //Scientific Research can be produced without a warehouse + if (office instanceof OfficeSpace) { + this.sciResearch.qty += (.004 + * Math.pow(office.employeeProd[EmployeePositions.RandD], 0.5) + * corporation.getScientificResearchMultiplier() + * this.getScientificResearchMultiplier()); + } + } + return [revenue, expenses]; + } + + //Process production & sale of this industry's FINISHED products (including all of their stats) + processProducts(marketCycles=1, corporation: ICorporation): [number, number] { + let revenue = 0; + const expenses = 0; + + //Create products + if (this.state === "PRODUCTION") { + for (const prodName in this.products) { + const prod = this.products[prodName]; + if(prod === undefined) continue; + if (!prod.fin) { + const city = prod.createCity; + const office = this.offices[city]; + if(office === 0) continue; + + // Designing/Creating a Product is based mostly off Engineers + const engrProd = office.employeeProd[EmployeePositions.Engineer]; + const mgmtProd = office.employeeProd[EmployeePositions.Management]; + const opProd = office.employeeProd[EmployeePositions.Operations]; + const total = engrProd + mgmtProd + opProd; + if (total <= 0) { break; } + + // Management is a multiplier for the production from Engineers + const mgmtFactor = 1 + (mgmtProd / (1.2 * total)); + + const progress = (Math.pow(engrProd, 0.34) + Math.pow(opProd, 0.2)) * mgmtFactor; + + prod.createProduct(marketCycles, progress); + if (prod.prog >= 100) { + prod.finishProduct(office.employeeProd, this); + } + break; + } + } + } + + //Produce Products + for (const prodName in this.products) { + if (this.products.hasOwnProperty(prodName)) { + const prod = this.products[prodName]; + if (prod instanceof Product && prod.fin) { + revenue += this.processProduct(marketCycles, prod, corporation); + } + } + } + return [revenue, expenses]; + } + + //Processes FINISHED products + processProduct(marketCycles=1, product: Product, corporation: ICorporation): number { + let totalProfit = 0; + for (let i = 0; i < CorporationConstants.Cities.length; ++i) { + const city = CorporationConstants.Cities[i]; + const office = this.offices[city]; + if(office === 0) continue; + const warehouse = this.warehouses[city]; + if (warehouse instanceof Warehouse) { + switch(this.state) { + + case "PRODUCTION": { + //Calculate the maximum production of this material based + //on the office's productivity + const maxProd = this.getOfficeProductivity(office, {forProduct:true}) + * corporation.getProductionMultiplier() + * this.prodMult // Multiplier from materials + * this.getProductionMultiplier() // Multiplier from research + * this.getProductProductionMultiplier(); // Multiplier from research + let prod; + + //Account for whether production is manually limited + if (product.prdman[city][0]) { + prod = Math.min(maxProd, product.prdman[city][1]); + } else { + prod = maxProd; + } + prod *= (CorporationConstants.SecsPerMarketCycle * marketCycles); + + //Calculate net change in warehouse storage making the Products will cost + let netStorageSize = product.siz; + for (const reqMatName in product.reqMats) { + if (product.reqMats.hasOwnProperty(reqMatName)) { + const normQty = product.reqMats[reqMatName]; + netStorageSize -= (MaterialSizes[reqMatName] * normQty); + } + } + + //If there's not enough space in warehouse, limit the amount of Product + if (netStorageSize > 0) { + const maxAmt = Math.floor((warehouse.size - warehouse.sizeUsed) / netStorageSize); + prod = Math.min(maxAmt, prod); + } + + warehouse.smartSupplyStore += (prod / (CorporationConstants.SecsPerMarketCycle * marketCycles)); + + //Make sure we have enough resources to make our Products + let producableFrac = 1; + for (const reqMatName in product.reqMats) { + if (product.reqMats.hasOwnProperty(reqMatName)) { + const req = product.reqMats[reqMatName] * prod; + if (warehouse.materials[reqMatName].qty < req) { + producableFrac = Math.min(producableFrac, warehouse.materials[reqMatName].qty / req); + } + } + } + + //Make our Products if they are producable + if (producableFrac > 0 && prod > 0) { + for (const reqMatName in product.reqMats) { + if (product.reqMats.hasOwnProperty(reqMatName)) { + const reqMatQtyNeeded = (product.reqMats[reqMatName] * prod * producableFrac); + warehouse.materials[reqMatName].qty -= reqMatQtyNeeded; + warehouse.materials[reqMatName].prd -= reqMatQtyNeeded / (CorporationConstants.SecsPerMarketCycle * marketCycles); + } + } + //Quantity + product.data[city][0] += (prod * producableFrac); + } + + //Keep track of production Per second + product.data[city][1] = prod * producableFrac / (CorporationConstants.SecsPerMarketCycle * marketCycles); + break; + } + case "SALE": { + //Process sale of Products + product.pCost = 0; //Estimated production cost + for (const reqMatName in product.reqMats) { + if (product.reqMats.hasOwnProperty(reqMatName)) { + product.pCost += (product.reqMats[reqMatName] * warehouse.materials[reqMatName].bCost); + } + } + + // Since its a product, its production cost is increased for labor + product.pCost *= CorporationConstants.ProductProductionCostRatio; + + // 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; + let 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) { + if (sqrtNumerator === 0) { + optimalPrice = 0; // No production + } else { + optimalPrice = product.pCost + markupLimit; + console.warn(`In Corporation, found illegal 0s when trying to calculate MarketTA2 sale cost`); + } + } 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)) { + const sCostString = product.sCost as string; + if(product.mku === 0) { + console.error(`mku is zero, reverting to 1 to avoid Infinity`); + product.mku = 1; + } + sCost = sCostString.replace(/MP/g, (product.pCost + product.rat / product.mku)+''); + sCost = eval(sCost); + + } else { + sCost = product.sCost; + } + + let markup = 1; + if (sCost > product.pCost) { + if ((sCost - product.pCost) > markupLimit) { + markup = markupLimit / (sCost - product.pCost); + } + } + + const maxSell = 0.5 + * Math.pow(product.rat, 0.65) + * marketFactor + * corporation.getSalesMultiplier() + * Math.pow(markup, 2) + * businessFactor + * advertisingFactor + * this.getSalesMultiplier(); + let sellAmt; + if (product.sllman[city][0] && isString(product.sllman[city][1])) { + //Sell amount is dynamically evaluated + let tmp = product.sllman[city][1].replace(/MAX/g, maxSell); + tmp = tmp.replace(/PROD/g, product.data[city][1]); + try { + tmp = eval(tmp); + } catch(e) { + dialogBoxCreate("Error evaluating your sell price expression for " + product.name + + " in " + this.name + "'s " + city + " office. Sell price is being set to MAX"); + tmp = maxSell; + } + sellAmt = Math.min(maxSell, tmp); + } 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 { + sellAmt = maxSell; + } + if (sellAmt < 0) { sellAmt = 0; } + sellAmt = sellAmt * CorporationConstants.SecsPerMarketCycle * marketCycles; + sellAmt = Math.min(product.data[city][0], sellAmt); //data[0] is qty + if (sellAmt && sCost) { + product.data[city][0] -= sellAmt; //data[0] is qty + totalProfit += (sellAmt * sCost); + product.data[city][2] = sellAmt / (CorporationConstants.SecsPerMarketCycle * marketCycles); //data[2] is sell property + } else { + product.data[city][2] = 0; //data[2] is sell property + } + break; + } + case "START": + case "PURCHASE": + case "EXPORT": + break; + default: + console.error(`Invalid State: ${this.state}`); + break; + } //End switch(this.state) + } + } + return totalProfit; + } + + discontinueProduct(product: Product): void { + for (const productName in this.products) { + if (this.products.hasOwnProperty(productName)) { + if (product === this.products[productName]) { + delete this.products[productName]; + } + } + } + } + + upgrade(upgrade: IndustryUpgrade, refs: {corporation: ICorporation; office: OfficeSpace}): void { + const corporation = refs.corporation; + const office = refs.office; + const upgN = upgrade[0]; + while (this.upgrades.length <= upgN) {this.upgrades.push(0);} + ++this.upgrades[upgN]; + + switch (upgN) { + case 0: { //Coffee, 5% energy per employee + for (let i = 0; i < office.employees.length; ++i) { + office.employees[i].ene = Math.min(office.employees[i].ene * 1.05, office.maxEne); + } + break; + } + case 1: { //AdVert.Inc, + const advMult = corporation.getAdvertisingMultiplier() * this.getAdvertisingMultiplier(); + this.awareness += (3 * advMult); + this.popularity += (1 * advMult); + this.awareness *= (1.01 * advMult); + this.popularity *= ((1 + getRandomInt(1, 3) / 100) * advMult); + break; + } + default: { + console.error(`Un-implemented function index: ${upgN}`); + break; + } + } + } + + // Returns how much of a material can be produced based of office productivity (employee stats) + getOfficeProductivity(office: OfficeSpace, params: {forProduct?: boolean} = {}): number { + const opProd = office.employeeProd[EmployeePositions.Operations]; + const engrProd = office.employeeProd[EmployeePositions.Engineer]; + const mgmtProd = office.employeeProd[EmployeePositions.Management] + const total = opProd + engrProd + mgmtProd; + + if (total <= 0) return 0; + + // Management is a multiplier for the production from Operations and Engineers + const mgmtFactor = 1 + (mgmtProd / (1.2 * total)); + + // For production, Operations is slightly more important than engineering + // Both Engineering and Operations have diminishing returns + const prod = (Math.pow(opProd, 0.4) + Math.pow(engrProd, 0.3)) * mgmtFactor; + + // Generic multiplier for the production. Used for game-balancing purposes + const balancingMult = 0.05; + + if (params && params.forProduct) { + // Products are harder to create and therefore have less production + return 0.5 * balancingMult * prod; + } else { + return balancingMult * prod; + } + } + + // Returns a multiplier based on the office' 'Business' employees that affects sales + getBusinessFactor(office: OfficeSpace): number { + const businessProd = 1 + office.employeeProd[EmployeePositions.Business]; + + return calculateEffectWithFactors(businessProd, 0.26, 10e3); + } + + //Returns a set of multipliers based on the Industry's awareness, popularity, and advFac. This + //multiplier affects sales. The result is: + // [Total sales mult, total awareness mult, total pop mult, awareness/pop ratio mult] + getAdvertisingFactors(): [number, number, number, number] { + const awarenessFac = Math.pow(this.awareness + 1, this.advFac); + const popularityFac = Math.pow(this.popularity + 1, this.advFac); + const ratioFac = (this.awareness === 0 ? 0.01 : Math.max((this.popularity + .001) / this.awareness, 0.01)); + const totalFac = Math.pow(awarenessFac * popularityFac * ratioFac, 0.85); + return [totalFac, awarenessFac, popularityFac, ratioFac]; + } + + //Returns a multiplier based on a materials demand and competition that affects sales + getMarketFactor(mat: {dmd: number; cmp: number}): number { + return Math.max(0.1, mat.dmd * (100 - mat.cmp) / 100); + } + + // Returns a boolean indicating whether this Industry has the specified Research + hasResearch(name: string): boolean { + return (this.researched[name] === true); + } + + updateResearchTree(): void { + const researchTree = IndustryResearchTrees[this.type]; + if(researchTree === undefined) + throw new Error(`Invalid industry "${this.type}"`); + + // Since ResearchTree data isnt saved, we'll update the Research Tree data + // based on the stored 'researched' property in the Industry object + if (Object.keys(researchTree.researched).length !== Object.keys(this.researched).length) { + for (const research in this.researched) { + researchTree.research(research); + } + } + } + + // Get multipliers from Research + getAdvertisingMultiplier(): number { + const researchTree = IndustryResearchTrees[this.type]; + if(researchTree === undefined) + throw new Error(`Invalid industry: "${this.type}"`); + this.updateResearchTree(); + return researchTree.getAdvertisingMultiplier(); + } + + getEmployeeChaMultiplier(): number { + const researchTree = IndustryResearchTrees[this.type]; + if(researchTree === undefined) + throw new Error(`Invalid industry: "${this.type}"`); + this.updateResearchTree(); + return researchTree.getEmployeeChaMultiplier(); + } + + getEmployeeCreMultiplier(): number { + const researchTree = IndustryResearchTrees[this.type]; + if(researchTree === undefined) + throw new Error(`Invalid industry: "${this.type}"`); + this.updateResearchTree(); + return researchTree.getEmployeeCreMultiplier(); + } + + getEmployeeEffMultiplier(): number { + const researchTree = IndustryResearchTrees[this.type]; + if(researchTree === undefined) + throw new Error(`Invalid industry: "${this.type}"`); + this.updateResearchTree(); + return researchTree.getEmployeeEffMultiplier(); + } + + getEmployeeIntMultiplier(): number { + const researchTree = IndustryResearchTrees[this.type]; + if(researchTree === undefined) + throw new Error(`Invalid industry: "${this.type}"`); + this.updateResearchTree(); + return researchTree.getEmployeeIntMultiplier(); + } + + getProductionMultiplier(): number { + const researchTree = IndustryResearchTrees[this.type]; + if(researchTree === undefined) + throw new Error(`Invalid industry: "${this.type}"`); + this.updateResearchTree(); + return researchTree.getProductionMultiplier(); + } + + getProductProductionMultiplier(): number { + const researchTree = IndustryResearchTrees[this.type]; + if(researchTree === undefined) + throw new Error(`Invalid industry: "${this.type}"`); + this.updateResearchTree(); + return researchTree.getProductProductionMultiplier(); + } + + getSalesMultiplier(): number { + const researchTree = IndustryResearchTrees[this.type]; + if(researchTree === undefined) + throw new Error(`Invalid industry: "${this.type}"`); + this.updateResearchTree(); + return researchTree.getSalesMultiplier(); + } + + getScientificResearchMultiplier(): number { + const researchTree = IndustryResearchTrees[this.type]; + if(researchTree === undefined) + throw new Error(`Invalid industry: "${this.type}"`); + this.updateResearchTree(); + return researchTree.getScientificResearchMultiplier(); + } + + getStorageMultiplier(): number { + const researchTree = IndustryResearchTrees[this.type]; + if(researchTree === undefined) + throw new Error(`Invalid industry: "${this.type}"`); + this.updateResearchTree(); + return researchTree.getStorageMultiplier(); + } + + /** + * Serialize the current object to a JSON save state. + */ + toJSON(): any { + return Generic_toJSON("Industry", this); + } + + /** + * Initiatizes a Industry object from a JSON save state. + */ + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types + static fromJSON(value: any): Industry { + return Generic_fromJSON(Industry, value.data); + } +} + +Reviver.constructors.Industry = Industry; diff --git a/src/Corporation/IndustryData.ts b/src/Corporation/IndustryData.ts deleted file mode 100644 index a7154026f..000000000 --- a/src/Corporation/IndustryData.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { ResearchTree } from "./ResearchTree"; -import { getBaseResearchTreeCopy, - getProductIndustryResearchTreeCopy } from "./data/BaseResearchTree"; - -import { numeralWrapper } from "../ui/numeralFormat"; - -interface IIndustryMap { - Energy: T; - Utilities: T; - Agriculture: T; - Fishing: T; - Mining: T; - Food: T; - Tobacco: T; - Chemical: T; - Pharmaceutical: T; - Computer: T; - Robotics: T; - Software: T; - Healthcare: T; - RealEstate: T; -} - -// Map of official names for each Industry -export const Industries: IIndustryMap = { - Energy: "Energy", - Utilities: "Water Utilities", - Agriculture: "Agriculture", - Fishing: "Fishing", - Mining: "Mining", - Food: "Food", - Tobacco: "Tobacco", - Chemical: "Chemical", - Pharmaceutical: "Pharmaceutical", - Computer: "Computer Hardware", - Robotics: "Robotics", - Software: "Software", - Healthcare: "Healthcare", - RealEstate: "RealEstate", -} - -// Map of how much money it takes to start each industry -export const IndustryStartingCosts: IIndustryMap = { - Energy: 225e9, - Utilities: 150e9, - Agriculture: 40e9, - Fishing: 80e9, - Mining: 300e9, - Food: 10e9, - Tobacco: 20e9, - Chemical: 70e9, - Pharmaceutical: 200e9, - Computer: 500e9, - Robotics: 1e12, - Software: 25e9, - Healthcare: 750e9, - RealEstate: 600e9, -} - -// Map of description for each industry -export const IndustryDescriptions: IIndustryMap = { - Energy: "Engage in the production and distribution of energy.

" + - "Starting cost: " + numeralWrapper.format(IndustryStartingCosts.Energy, "$0.000a") + "
" + - "Recommended starting Industry: NO", - Utilities: "Distribute water and provide wastewater services.

" + - "Starting cost: " + numeralWrapper.format(IndustryStartingCosts.Utilities, "$0.000a") + "
" + - "Recommended starting Industry: NO", - Agriculture: "Cultivate crops and breed livestock to produce food.

" + - "Starting cost: " + numeralWrapper.format(IndustryStartingCosts.Agriculture, "$0.000a") + "
" + - "Recommended starting Industry: YES", - Fishing: "Produce food through the breeding and processing of fish and fish products.

" + - "Starting cost: " + numeralWrapper.format(IndustryStartingCosts.Fishing, "$0.000a") + "
" + - "Recommended starting Industry: NO", - Mining: "Extract and process metals from the earth.

" + - "Starting cost: " + numeralWrapper.format(IndustryStartingCosts.Mining, "$0.000a") + "
" + - "Recommended starting Industry: NO", - Food: "Create your own restaurants all around the world.

" + - "Starting cost: " + numeralWrapper.format(IndustryStartingCosts.Food, "$0.000a") + "
" + - "Recommended starting Industry: YES", - Tobacco: "Create and distribute tobacco and tobacco-related products.

" + - "Starting cost: " + numeralWrapper.format(IndustryStartingCosts.Tobacco, "$0.000a") + "
" + - "Recommended starting Industry: YES", - Chemical: "Produce industrial chemicals.

" + - "Starting cost: " + numeralWrapper.format(IndustryStartingCosts.Chemical, "$0.000a") + "
" + - "Recommended starting Industry: NO", - Pharmaceutical: "Discover, develop, and create new pharmaceutical drugs.

" + - "Starting cost: " + numeralWrapper.format(IndustryStartingCosts.Pharmaceutical, "$0.000a") + "
" + - "Recommended starting Industry: NO", - Computer: "Develop and manufacture new computer hardware and networking infrastructures.

" + - "Starting cost: " + numeralWrapper.format(IndustryStartingCosts.Computer, "$0.000a") + "
" + - "Recommended starting Industry: NO", - Robotics: "Develop and create robots.

" + - "Starting cost: " + numeralWrapper.format(IndustryStartingCosts.Robotics, "$0.000a") + "
" + - "Recommended starting Industry: NO", - Software: "Develop computer software and create AI Cores.

" + - "Starting cost: " + numeralWrapper.format(IndustryStartingCosts.Software, "$0.000a") + "
" + - "Recommended starting Industry: YES", - Healthcare: "Create and manage hospitals.

" + - "Starting cost: " + numeralWrapper.format(IndustryStartingCosts.Healthcare, "$0.000a") + "
" + - "Recommended starting Industry: NO", - RealEstate: "Develop and manage real estate properties.

" + - "Starting cost: " + numeralWrapper.format(IndustryStartingCosts.RealEstate, "$0.000a") + "
" + - "Recommended starting Industry: NO", -} - -// Map of available Research for each Industry. This data is held in a -// ResearchTree object -export const IndustryResearchTrees: IIndustryMap = { - Energy: getBaseResearchTreeCopy(), - Utilities: getBaseResearchTreeCopy(), - Agriculture: getBaseResearchTreeCopy(), - Fishing: getBaseResearchTreeCopy(), - Mining: getBaseResearchTreeCopy(), - Food: getProductIndustryResearchTreeCopy(), - Tobacco: getProductIndustryResearchTreeCopy(), - Chemical: getBaseResearchTreeCopy(), - Pharmaceutical: getProductIndustryResearchTreeCopy(), - Computer: getProductIndustryResearchTreeCopy(), - Robotics: getProductIndustryResearchTreeCopy(), - Software: getProductIndustryResearchTreeCopy(), - Healthcare: getProductIndustryResearchTreeCopy(), - RealEstate: getProductIndustryResearchTreeCopy(), -} - -export function resetIndustryResearchTrees(): void { - IndustryResearchTrees.Energy = getBaseResearchTreeCopy(); - IndustryResearchTrees.Utilities = getBaseResearchTreeCopy(); - IndustryResearchTrees.Agriculture = getBaseResearchTreeCopy(); - IndustryResearchTrees.Fishing = getBaseResearchTreeCopy(); - IndustryResearchTrees.Mining = getBaseResearchTreeCopy(); - IndustryResearchTrees.Food = getBaseResearchTreeCopy(); - IndustryResearchTrees.Tobacco = getBaseResearchTreeCopy(); - IndustryResearchTrees.Chemical = getBaseResearchTreeCopy(); - IndustryResearchTrees.Pharmaceutical = getBaseResearchTreeCopy(); - IndustryResearchTrees.Computer = getBaseResearchTreeCopy(); - IndustryResearchTrees.Robotics = getBaseResearchTreeCopy(); - IndustryResearchTrees.Software = getBaseResearchTreeCopy(); - IndustryResearchTrees.Healthcare = getBaseResearchTreeCopy(); - IndustryResearchTrees.RealEstate = getBaseResearchTreeCopy(); -} diff --git a/src/Corporation/IndustryData.tsx b/src/Corporation/IndustryData.tsx new file mode 100644 index 000000000..04459d3ef --- /dev/null +++ b/src/Corporation/IndustryData.tsx @@ -0,0 +1,141 @@ +import React from 'react'; +import { ResearchTree } from "./ResearchTree"; +import { getBaseResearchTreeCopy, + getProductIndustryResearchTreeCopy } from "./data/BaseResearchTree"; +import { Money } from "../ui/React/Money"; + +interface IIndustryMap { + [key: string]: T | undefined; + Energy: T; + Utilities: T; + Agriculture: T; + Fishing: T; + Mining: T; + Food: T; + Tobacco: T; + Chemical: T; + Pharmaceutical: T; + Computer: T; + Robotics: T; + Software: T; + Healthcare: T; + RealEstate: T; +} + +// Map of official names for each Industry +export const Industries: IIndustryMap = { + Energy: "Energy", + Utilities: "Water Utilities", + Agriculture: "Agriculture", + Fishing: "Fishing", + Mining: "Mining", + Food: "Food", + Tobacco: "Tobacco", + Chemical: "Chemical", + Pharmaceutical: "Pharmaceutical", + Computer: "Computer Hardware", + Robotics: "Robotics", + Software: "Software", + Healthcare: "Healthcare", + RealEstate: "RealEstate", +} + +// Map of how much money it takes to start each industry +export const IndustryStartingCosts: IIndustryMap = { + Energy: 225e9, + Utilities: 150e9, + Agriculture: 40e9, + Fishing: 80e9, + Mining: 300e9, + Food: 10e9, + Tobacco: 20e9, + Chemical: 70e9, + Pharmaceutical: 200e9, + Computer: 500e9, + Robotics: 1e12, + Software: 25e9, + Healthcare: 750e9, + RealEstate: 600e9, +} + +// Map of description for each industry +export const IndustryDescriptions: IIndustryMap = { + Energy: (<>Engage in the production and distribution of energy.

+ Starting cost:
+ Recommended starting Industry: NO), + Utilities: (<>Distribute water and provide wastewater services.

+ Starting cost:
+ Recommended starting Industry: NO), + Agriculture: (<>Cultivate crops and breed livestock to produce food.

+ Starting cost:
+ Recommended starting Industry: YES), + Fishing: (<>Produce food through the breeding and processing of fish and fish products.

+ Starting cost:
+ Recommended starting Industry: NO), + Mining: (<>Extract and process metals from the earth.

+ Starting cost:
+ Recommended starting Industry: NO), + Food: (<>Create your own restaurants all around the world.

+ Starting cost:
+ Recommended starting Industry: YES), + Tobacco: (<>Create and distribute tobacco and tobacco-related products.

+ Starting cost:
+ Recommended starting Industry: YES), + Chemical: (<>Produce industrial chemicals.

+ Starting cost:
+ Recommended starting Industry: NO), + Pharmaceutical: (<>Discover, develop, and create new pharmaceutical drugs.

+ Starting cost:
+ Recommended starting Industry: NO), + Computer: (<>Develop and manufacture new computer hardware and networking infrastructures.

+ Starting cost:
+ Recommended starting Industry: NO), + Robotics: (<>Develop and create robots.

+ Starting cost:
+ Recommended starting Industry: NO), + Software: (<>Develop computer software and create AI Cores.

+ Starting cost:
+ Recommended starting Industry: YES), + Healthcare: (<>Create and manage hospitals.

+ Starting cost:
+ Recommended starting Industry: NO), + RealEstate: (<>Develop and manage real estate properties.

+ Starting cost:
+ Recommended starting Industry: NO), +} + +// Map of available Research for each Industry. This data is held in a +// ResearchTree object +export const IndustryResearchTrees: IIndustryMap = { + Energy: getBaseResearchTreeCopy(), + Utilities: getBaseResearchTreeCopy(), + Agriculture: getBaseResearchTreeCopy(), + Fishing: getBaseResearchTreeCopy(), + Mining: getBaseResearchTreeCopy(), + Food: getProductIndustryResearchTreeCopy(), + Tobacco: getProductIndustryResearchTreeCopy(), + Chemical: getBaseResearchTreeCopy(), + Pharmaceutical: getProductIndustryResearchTreeCopy(), + Computer: getProductIndustryResearchTreeCopy(), + Robotics: getProductIndustryResearchTreeCopy(), + Software: getProductIndustryResearchTreeCopy(), + Healthcare: getProductIndustryResearchTreeCopy(), + RealEstate: getProductIndustryResearchTreeCopy(), +} + +export function resetIndustryResearchTrees(): void { + IndustryResearchTrees.Energy = getBaseResearchTreeCopy(); + IndustryResearchTrees.Utilities = getBaseResearchTreeCopy(); + IndustryResearchTrees.Agriculture = getBaseResearchTreeCopy(); + IndustryResearchTrees.Fishing = getBaseResearchTreeCopy(); + IndustryResearchTrees.Mining = getBaseResearchTreeCopy(); + IndustryResearchTrees.Food = getBaseResearchTreeCopy(); + IndustryResearchTrees.Tobacco = getBaseResearchTreeCopy(); + IndustryResearchTrees.Chemical = getBaseResearchTreeCopy(); + IndustryResearchTrees.Pharmaceutical = getBaseResearchTreeCopy(); + IndustryResearchTrees.Computer = getBaseResearchTreeCopy(); + IndustryResearchTrees.Robotics = getBaseResearchTreeCopy(); + IndustryResearchTrees.Software = getBaseResearchTreeCopy(); + IndustryResearchTrees.Healthcare = getBaseResearchTreeCopy(); + IndustryResearchTrees.RealEstate = getBaseResearchTreeCopy(); +} diff --git a/src/Corporation/IndustryUpgrades.ts b/src/Corporation/IndustryUpgrades.ts index d83ada87f..234ca88e5 100644 --- a/src/Corporation/IndustryUpgrades.ts +++ b/src/Corporation/IndustryUpgrades.ts @@ -1,9 +1,11 @@ import { IMap } from "../types"; +export type IndustryUpgrade = [number, number, number, number, string, string]; + // Industry upgrades // The data structure is an array with the following format: // [index in array, base price, price mult, benefit mult (if applicable), name, desc] -export const IndustryUpgrades: IMap = { +export const IndustryUpgrades: IMap = { "0": [0, 500e3, 1, 1.05, "Coffee", "Provide your employees with coffee, increasing their energy by 5%."], "1": [1, 1e9, 1.06, 1.03, diff --git a/src/Corporation/Material.ts b/src/Corporation/Material.ts index 29a165dd7..9c5d1ca19 100644 --- a/src/Corporation/Material.ts +++ b/src/Corporation/Material.ts @@ -1,6 +1,7 @@ import { Generic_fromJSON, Generic_toJSON, Reviver } from "../../utils/JSONReviver"; +import { Export } from "./Export"; interface IConstructorParams { name?: string; @@ -43,7 +44,7 @@ export class Material { imp = 0; // Exports of this material to another warehouse/industry - exp: any[] = []; + exp: Export[] = []; // Total amount of this material exported in the last cycle totalExp = 0; @@ -52,12 +53,12 @@ export class Material { bCost = 0; // Cost / sec to sell this material - sCost = 0; + sCost: string | number = 0; // Flags to keep track of whether production and/or sale of this material is limited // [Whether production/sale is limited, limit amount] - prdman: any[] = [false, 0]; // Production - sllman: any[] = [false, 0]; // Sale + prdman: [boolean, number] = [false, 0]; // Production + sllman: [boolean, string | number] = [false, 0]; // Sale // Flags that signal whether automatic sale pricing through Market TA is enabled marketTa1 = false; diff --git a/src/Corporation/OfficeSpace.ts b/src/Corporation/OfficeSpace.ts new file mode 100644 index 000000000..057813df2 --- /dev/null +++ b/src/Corporation/OfficeSpace.ts @@ -0,0 +1,198 @@ +import { EmployeePositions } from "./EmployeePositions"; +import { CorporationConstants } from "./data/Constants"; +import { getRandomInt } from "../../utils/helpers/getRandomInt"; +import { generateRandomString } from "../../utils/StringHelperFunctions"; +import { Generic_fromJSON, Generic_toJSON, Reviver } from "../../utils/JSONReviver"; +import { Employee } from "./Employee"; +import { IIndustry } from "./IIndustry"; +import { ICorporation } from './ICorporation'; + +interface IParams { + loc?: string; + cost?: number; + size?: number; + comfort?: number; + beauty?: number; +} + +export class OfficeSpace { + loc: string; + cost: number; + size: number; + comf: number; + beau: number; + tier = "Basic"; + minEne = 0; + maxEne = 100; + minHap = 0; + maxHap = 100; + maxMor = 100; + employees: Employee[] = []; + employeeProd: {[key: string]: number} = { + [EmployeePositions.Operations]: 0, + [EmployeePositions.Engineer]: 0, + [EmployeePositions.Business]: 0, + [EmployeePositions.Management]: 0, + [EmployeePositions.RandD]: 0, + total: 0, + }; + + constructor(params: IParams = {}) { + this.loc = params.loc ? params.loc : ""; + this.cost = params.cost ? params.cost : 1; + this.size = params.size ? params.size : 1; + this.comf = params.comfort ? params.comfort : 1; + this.beau = params.beauty ? params.beauty : 1; + } + + + atCapacity(): boolean { + return (this.employees.length) >= this.size; + } + + process(marketCycles = 1, corporation: ICorporation, industry: IIndustry): number { + // HRBuddy AutoRecruitment and training + if (industry.hasResearch("HRBuddy-Recruitment") && !this.atCapacity()) { + const emp = this.hireRandomEmployee(); + if (industry.hasResearch("HRBuddy-Training") && emp !== undefined) { + emp.pos = EmployeePositions.Training; + } + } + + // Process Office properties + this.maxEne = 100; + this.maxHap = 100; + this.maxMor = 100; + if (industry.hasResearch("Go-Juice")) { + this.maxEne += 10; + } + if (industry.hasResearch("JoyWire")) { + this.maxHap += 10; + } + if (industry.hasResearch("Sti.mu")) { + this.maxMor += 10; + } + + // Calculate changes in Morale/Happiness/Energy for Employees + let perfMult=1; //Multiplier for employee morale/happiness/energy based on company performance + if (corporation.funds < 0 && industry.lastCycleRevenue < 0) { + perfMult = Math.pow(0.99, marketCycles); + } else if (corporation.funds > 0 && industry.lastCycleRevenue > 0) { + perfMult = Math.pow(1.01, marketCycles); + } + + const hasAutobrew = industry.hasResearch("AutoBrew"); + const hasAutoparty = industry.hasResearch("AutoPartyManager"); + + let salaryPaid = 0; + for (let i = 0; i < this.employees.length; ++i) { + const emp = this.employees[i]; + if (hasAutoparty) { + emp.mor = this.maxMor; + emp.hap = this.maxHap; + } else { + emp.mor *= perfMult; + emp.hap *= perfMult; + emp.mor = Math.min(emp.mor, this.maxMor); + emp.hap = Math.min(emp.hap, this.maxHap); + } + + if (hasAutobrew) { + emp.ene = this.maxEne; + } else { + emp.ene *= perfMult; + emp.ene = Math.min(emp.ene, this.maxEne); + } + + const salary = emp.process(marketCycles, this); + salaryPaid += salary; + } + + this.calculateEmployeeProductivity(corporation, industry); + return salaryPaid; + } + + calculateEmployeeProductivity(corporation: ICorporation, industry: IIndustry): void { + //Reset + for (const name in this.employeeProd) { + this.employeeProd[name] = 0; + } + + let total = 0; + for (let i = 0; i < this.employees.length; ++i) { + const employee = this.employees[i]; + const prod = employee.calculateProductivity(corporation, industry); + this.employeeProd[employee.pos] += prod; + total += prod; + } + this.employeeProd.total = total; + } + + hireRandomEmployee(): Employee | undefined { + if (this.atCapacity()) return; + if (document.getElementById("cmpy-mgmt-hire-employee-popup") != null) return; + + //Generate three random employees (meh, decent, amazing) + const mult = getRandomInt(76, 100)/100; + const int = getRandomInt(50, 100), + cha = getRandomInt(50, 100), + exp = getRandomInt(50, 100), + cre = getRandomInt(50, 100), + eff = getRandomInt(50, 100), + sal = CorporationConstants.EmployeeSalaryMultiplier * (int + cha + exp + cre + eff); + + const emp = new Employee({ + intelligence: int * mult, + charisma: cha * mult, + experience: exp * mult, + creativity: cre * mult, + efficiency: eff * mult, + salary: sal * mult, + }); + + const name = generateRandomString(7); + + for (let i = 0; i < this.employees.length; ++i) { + if (this.employees[i].name === name) { + return this.hireRandomEmployee(); + } + } + emp.name = name; + this.employees.push(emp); + + return emp; + } + + //Finds the first unassigned employee and assigns its to the specified job + assignEmployeeToJob(job: string): boolean { + for (let i = 0; i < this.employees.length; ++i) { + if (this.employees[i].pos === EmployeePositions.Unassigned) { + this.employees[i].pos = job; + return true; + } + } + return false; + } + + //Finds the first employee with the given job and unassigns it + unassignEmployeeFromJob(job: string): boolean { + for (let i = 0; i < this.employees.length; ++i) { + if (this.employees[i].pos === job) { + this.employees[i].pos = EmployeePositions.Unassigned; + return true; + } + } + return false; + } + + toJSON(): any { + return Generic_toJSON("OfficeSpace", this); + } + + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types + static fromJSON(value: any): OfficeSpace { + return Generic_fromJSON(OfficeSpace, value.data); + } +} + +Reviver.constructors.OfficeSpace = OfficeSpace; \ No newline at end of file diff --git a/src/Corporation/Product.ts b/src/Corporation/Product.ts index 6a688d1f1..3ac15ff6d 100644 --- a/src/Corporation/Product.ts +++ b/src/Corporation/Product.ts @@ -1,5 +1,6 @@ import { EmployeePositions } from "./EmployeePositions"; import { MaterialSizes } from "./MaterialSizes"; +import { IIndustry } from "./IIndustry"; import { ProductRatingWeights, IProductRatingWeight } from "./ProductRatingWeights"; @@ -30,17 +31,6 @@ interface IConstructorParams { req?: IMap; } -// Interface for an Industry object - Used for type checking method arguments -interface IIndustry { - awareness: number; - popularity: number; - reqMats: IMap; - sciFac: number; - sciResearch: any; - type: string; -} - - export class Product { // Product name @@ -60,7 +50,7 @@ export class Product { pCost = 0; // Sell cost - sCost = 0; + sCost: string | number = 0; // Variables for handling the creation process of this Product fin = false; // Whether this Product has finished being created @@ -137,7 +127,7 @@ export class Product { } // @param industry - Industry object. Reference to industry that makes this Product - finishProduct(employeeProd: IMap, industry: IIndustry): void { + finishProduct(employeeProd: {[key: string]: number}, industry: IIndustry): void { this.fin = true; //Calculate properties @@ -199,7 +189,9 @@ export class Product { //For now, just set it to be the same as the requirements to make materials for (const matName in industry.reqMats) { if (industry.reqMats.hasOwnProperty(matName)) { - this.reqMats[matName] = industry.reqMats[matName]; + const reqMat = industry.reqMats[matName]; + if(reqMat === undefined) continue; + this.reqMats[matName] = reqMat; } } @@ -207,18 +199,10 @@ export class Product { //For now, just set it to be the same size as the requirements to make materials this.siz = 0; for (const matName in industry.reqMats) { - this.siz += MaterialSizes[matName] * industry.reqMats[matName]; + const reqMat = industry.reqMats[matName]; + if(reqMat === undefined) continue; + this.siz += MaterialSizes[matName] * reqMat; } - - //Delete unneeded variables - // @ts-ignore - delete this.prog; - // @ts-ignore - delete this.createCity; - // @ts-ignore - delete this.designCost; - // @ts-ignore - delete this.advCost; } diff --git a/src/Corporation/Warehouse.ts b/src/Corporation/Warehouse.ts index ec912fa28..100573c05 100644 --- a/src/Corporation/Warehouse.ts +++ b/src/Corporation/Warehouse.ts @@ -1,4 +1,6 @@ import { Material } from "./Material"; +import { ICorporation } from "./ICorporation"; +import { IIndustry } from "./IIndustry"; import { MaterialSizes } from "./MaterialSizes"; import { IMap } from "../types"; import { numeralWrapper } from "../ui/numeralFormat"; @@ -7,13 +9,9 @@ import { Generic_fromJSON, Reviver } from "../../utils/JSONReviver"; import { exceptionAlert } from "../../utils/helpers/exceptionAlert"; -interface IParent { - getStorageMultiplier(): number; -} - interface IConstructorParams { - corp?: IParent; - industry?: IParent; + corp?: ICorporation; + industry?: IIndustry; loc?: string; size?: number; } @@ -92,7 +90,7 @@ export class Warehouse { } } - updateSize(corporation: IParent, industry: IParent): void { + updateSize(corporation: ICorporation, industry: IIndustry): void { try { this.size = (this.level * 100) * corporation.getStorageMultiplier() diff --git a/src/Corporation/data/Constants.ts b/src/Corporation/data/Constants.ts new file mode 100644 index 000000000..3b0692a9e --- /dev/null +++ b/src/Corporation/data/Constants.ts @@ -0,0 +1,60 @@ +const CyclesPerMarketCycle = 50; +const AllCorporationStates = ["START", "PURCHASE", "PRODUCTION", "SALE", "EXPORT"]; +export const CorporationConstants: { + INITIALSHARES: number; + SHARESPERPRICEUPDATE: number; + IssueNewSharesCooldown: number; + SellSharesCooldown: number; + CyclesPerMarketCycle: number; + CyclesPerIndustryStateCycle: number; + SecsPerMarketCycle: number; + Cities: string[]; + WarehouseInitialCost: number; + WarehouseInitialSize: number; + WarehouseUpgradeBaseCost: number; + OfficeInitialCost: number; + OfficeInitialSize: number; + OfficeUpgradeBaseCost: number; + BribeThreshold: number; + BribeToRepRatio: number; + ProductProductionCostRatio: number; + DividendMaxPercentage: number; + EmployeeSalaryMultiplier: number; + CyclesPerEmployeeRaise: number; + EmployeeRaiseAmount: number; + BaseMaxProducts: number; + AllCorporationStates: string[]; +} = { + INITIALSHARES: 1e9, //Total number of shares you have at your company + SHARESPERPRICEUPDATE: 1e6, //When selling large number of shares, price is dynamically updated for every batch of this amount + IssueNewSharesCooldown: 216e3, // 12 Hour in terms of game cycles + SellSharesCooldown: 18e3, // 1 Hour in terms of game cycles + + CyclesPerMarketCycle: CyclesPerMarketCycle, + CyclesPerIndustryStateCycle: CyclesPerMarketCycle / AllCorporationStates.length, + SecsPerMarketCycle: CyclesPerMarketCycle / 5, + + Cities: ["Aevum", "Chongqing", "Sector-12", "New Tokyo", "Ishima", "Volhaven"], + + WarehouseInitialCost: 5e9, //Initial purchase cost of warehouse + WarehouseInitialSize: 100, + WarehouseUpgradeBaseCost: 1e9, + + OfficeInitialCost: 4e9, + OfficeInitialSize: 3, + OfficeUpgradeBaseCost: 1e9, + + BribeThreshold: 100e12, //Money needed to be able to bribe for faction rep + BribeToRepRatio: 1e9, //Bribe Value divided by this = rep gain + + ProductProductionCostRatio: 5, //Ratio of material cost of a product to its production cost + + DividendMaxPercentage: .5, + + EmployeeSalaryMultiplier: 3, // Employee stats multiplied by this to determine initial salary + CyclesPerEmployeeRaise: 400, // All employees get a raise every X market cycles + EmployeeRaiseAmount: 50, // Employee salary increases by this (additive) + + BaseMaxProducts: 3, // Initial value for maximum number of products allowed + AllCorporationStates: AllCorporationStates, +}; \ No newline at end of file diff --git a/src/Corporation/data/CorporationUnlockUpgrades.ts b/src/Corporation/data/CorporationUnlockUpgrades.ts index 5cbed6abc..aae13fe5a 100644 --- a/src/Corporation/data/CorporationUnlockUpgrades.ts +++ b/src/Corporation/data/CorporationUnlockUpgrades.ts @@ -1,10 +1,12 @@ import { IMap } from "../../types"; +export type CorporationUnlockUpgrade = [number, number, string, string]; + // Corporation Unlock Upgrades // Upgrades for entire corporation, unlocks features, either you have it or you dont // The data structure is an array with the following format: // [index in Corporation feature upgrades array, price, name, description] -export const CorporationUnlockUpgrades: IMap = { +export const CorporationUnlockUpgrades: IMap = { //Lets you export goods "0": [0, 20e9, "Export", "Develop infrastructure to export your materials to your other facilities. " + diff --git a/src/Corporation/data/CorporationUpgrades.ts b/src/Corporation/data/CorporationUpgrades.ts index 519977ff8..57c97331c 100644 --- a/src/Corporation/data/CorporationUpgrades.ts +++ b/src/Corporation/data/CorporationUpgrades.ts @@ -1,10 +1,12 @@ import { IMap } from "../../types"; +export type CorporationUpgrade = [number, number, number, number, string, string]; + // Corporation Upgrades // Upgrades for entire corporation, levelable upgrades // The data structure is an array with the following format // [index in Corporation upgrades array, base price, price mult, benefit mult (additive), name, desc] -export const CorporationUpgrades: IMap = { +export const CorporationUpgrades: IMap = { //Smart factories, increases production "0": [0, 2e9, 1.06, 0.03, "Smart Factories", "Advanced AI automatically optimizes the operation and productivity " + diff --git a/src/Corporation/ui/BaseReactComponent.js b/src/Corporation/ui/BaseReactComponent.js deleted file mode 100644 index b3da9a6fc..000000000 --- a/src/Corporation/ui/BaseReactComponent.js +++ /dev/null @@ -1,20 +0,0 @@ -// Base class for React Components for Corporation UI -// Contains a few helper functions that let derived classes easily -// access Corporation properties -import React from "react"; - -const Component = React.Component; - -export class BaseReactComponent extends Component { - corp() { - return this.props.corp; - } - - eventHandler() { - return this.props.eventHandler; - } - - routing() { - return this.props.routing; - } -} diff --git a/src/Corporation/ui/BribeFactionPopup.tsx b/src/Corporation/ui/BribeFactionPopup.tsx new file mode 100644 index 000000000..3ded57c1c --- /dev/null +++ b/src/Corporation/ui/BribeFactionPopup.tsx @@ -0,0 +1,88 @@ +import React, { useState } from 'react'; +import { IPlayer } from "../../PersonObjects/IPlayer"; +import { Factions } from "../../Faction/Factions"; +import { CorporationConstants } from "../data/Constants"; +import { numeralWrapper } from "../../ui/numeralFormat"; +import { removePopup } from "../../ui/React/createPopup"; +import { dialogBoxCreate } from "../../../utils/DialogBox"; +import { ICorporation } from "../ICorporation"; + +interface IProps { + popupId: string; + corp: ICorporation; + player: IPlayer; +} + +export function BribeFactionPopup(props: IProps): React.ReactElement { + const [money, setMoney] = useState(0); + const [stock, setStock] = useState(0); + const [selectedFaction, setSelectedFaction] = useState(props.player.factions.length > 0 ? props.player.factions[0] : ""); + + function onMoneyChange(event: React.ChangeEvent): void { + setMoney(parseFloat(event.target.value)); + } + + function onStockChange(event: React.ChangeEvent): void { + setStock(parseFloat(event.target.value)); + } + + function changeFaction(event: React.ChangeEvent): void { + setSelectedFaction(event.target.value); + } + + function repGain(money: number, stock: number): number { + return (money + (stock * props.corp.sharePrice)) / CorporationConstants.BribeToRepRatio; + } + + function getRepText(money: number, stock: number): string { + if(money === 0 && stock === 0) return ""; + if (isNaN(money) || isNaN(stock) || money < 0 || stock < 0) { + return "ERROR: Invalid value(s) entered"; + } else if (props.corp.funds.lt(money)) { + return "ERROR: You do not have this much money to bribe with"; + } else if (stock > props.corp.numShares) { + return "ERROR: You do not have this many shares to bribe with"; + } else { + return "You will gain " + numeralWrapper.formatReputation(repGain(money, stock)) + + " reputation with " + + selectedFaction + + " with this bribe"; + } + } + + function bribe(money: number, stock: number): void { + const fac = Factions[selectedFaction]; + if (fac == null) { + dialogBoxCreate("ERROR: You must select a faction to bribe"); + } + if (isNaN(money) || isNaN(stock) || money < 0 || stock < 0) { + } else if (props.corp.funds.lt(money)) { + } else if (stock > props.corp.numShares) { + } else { + const rep = repGain(money, stock); + dialogBoxCreate("You gained " + numeralWrapper.formatReputation(rep) + + " reputation with " + fac.name + " by bribing them."); + fac.playerReputation += rep; + props.corp.funds = props.corp.funds.minus(money); + props.corp.numShares -= stock; + removePopup(props.popupId); + } + } + + return (<> +

You can use Corporation funds or stock shares to bribe Faction Leaders in exchange for faction reputation.

+ +

{getRepText(money ? money : 0, stock ? stock : 0)}

+ + + + ); +} diff --git a/src/Corporation/ui/BuybackSharesPopup.tsx b/src/Corporation/ui/BuybackSharesPopup.tsx new file mode 100644 index 000000000..eda57a330 --- /dev/null +++ b/src/Corporation/ui/BuybackSharesPopup.tsx @@ -0,0 +1,87 @@ +import React, { useState } from 'react'; +import { IPlayer } from "../../PersonObjects/IPlayer"; +import { removePopup } from "../../ui/React/createPopup"; +import { numeralWrapper } from "../../ui/numeralFormat"; +import { dialogBoxCreate } from "../../../utils/DialogBox"; +import { ICorporation } from "../ICorporation"; + +interface IProps { + player: IPlayer; + popupId: string; + corp: ICorporation; +} + +// Create a popup that lets the player buyback shares +// This is created when the player clicks the "Buyback Shares" button in the overview panel +export function BuybackSharesPopup(props: IProps): React.ReactElement { + const [shares, setShares] = useState(null); + + function changeShares(event: React.ChangeEvent): void { + if(event.target.value === "") setShares(null); + else setShares(Math.round(parseFloat(event.target.value))); + } + + const currentStockPrice = props.corp.sharePrice; + const buybackPrice = currentStockPrice * 1.1; + + function buy(): void { + if(shares === null) return; + const tempStockPrice = props.corp.sharePrice; + const buybackPrice = tempStockPrice * 1.1; + if (isNaN(shares) || shares <= 0) { + dialogBoxCreate("ERROR: Invalid value for number of shares"); + } else if (shares > props.corp.issuedShares) { + dialogBoxCreate("ERROR: There are not this many oustanding shares to buy back"); + } else if (shares * buybackPrice > props.player.money) { + dialogBoxCreate("ERROR: You do not have enough money to purchase this many shares (you need " + + numeralWrapper.format(shares * buybackPrice, "$0.000a") + ")"); + } else { + props.corp.numShares += shares; + if (isNaN(props.corp.issuedShares)) { + console.warn("Corporation issuedShares is NaN: " + props.corp.issuedShares); + console.warn("Converting to number now"); + const res = props.corp.issuedShares; + if (isNaN(res)) { + props.corp.issuedShares = 0; + } else { + props.corp.issuedShares = res; + } + } + props.corp.issuedShares -= shares; + props.player.loseMoney(shares * buybackPrice); + removePopup(props.popupId); + props.corp.rerender(props.player); + } + } + + function CostIndicator(): React.ReactElement { + if(shares === null) return (<>); + if (isNaN(shares) || shares <= 0) { + return (<>ERROR: Invalid value entered for number of shares to buyback); + } else if (shares > props.corp.issuedShares) { + return (<>There are not this many shares available to buy back. + There are only {numeralWrapper.formatBigNumber(props.corp.issuedShares)} outstanding shares.); + } else { + return (<>Purchase {shares} shares for a total of {numeralWrapper.formatMoney(shares * buybackPrice)}); + } + } + + function onKeyDown(event: React.KeyboardEvent): void { + if (event.keyCode === 13) buy(); + } + + return (<> +

+Enter the number of outstanding shares you would like to buy back. +These shares must be bought at a 10% premium. However, +repurchasing shares from the market tends to lead to an increase in stock price.

+To purchase these shares, you must use your own money (NOT your Corporation's funds).

+The current buyback price of your company's stock is {numeralWrapper.formatMoney(buybackPrice)}. +Your company currently has {numeralWrapper.formatBigNumber(props.corp.issuedShares)} outstanding stock shares. +

+ +
+ + + ); +} \ No newline at end of file diff --git a/src/Corporation/ui/CityTab.tsx b/src/Corporation/ui/CityTab.tsx new file mode 100644 index 000000000..8e6759417 --- /dev/null +++ b/src/Corporation/ui/CityTab.tsx @@ -0,0 +1,20 @@ +import React from "react"; + +interface IProps { + onClick: () => void; + name: string; + current: boolean; +} + +export function CityTab(props: IProps): React.ReactElement { + let className = "cmpy-mgmt-city-tab"; + if (props.current) { + className += " current"; + } + + return ( + + ) +} \ No newline at end of file diff --git a/src/Corporation/ui/CityTabs.jsx b/src/Corporation/ui/CityTabs.jsx deleted file mode 100644 index 56dfbf27b..000000000 --- a/src/Corporation/ui/CityTabs.jsx +++ /dev/null @@ -1,62 +0,0 @@ -// React Components for the Corporation UI's City navigation tabs -// These allow player to navigate between different cities for each industry -import React from "react"; -import { BaseReactComponent } from "./BaseReactComponent"; - -export class CityTabs extends BaseReactComponent { - constructor(props) { - // An object with [key = city name] and [value = click handler] - // needs to be passed into the constructor as the "onClicks" property. - // We'll make sure that that happens here - if (props.onClicks == null) { - throw new Error(`CityTabs component constructed without onClick handlers`); - } - if (props.city == null) { - throw new Error(`CityTabs component constructed without 'city' property`) - } - if (props.cityStateSetter == null) { - throw new Error(`CityTabs component constructed without 'cityStateSetter' property`) - } - - super(props); - } - - renderTab(props) { - let className = "cmpy-mgmt-city-tab"; - if (props.current) { - className += " current"; - } - - return ( - - ) - } - - render() { - const division = this.routing().currentDivision; - - const tabs = []; - - // Tabs for each city - for (const cityName in this.props.onClicks) { - tabs.push(this.renderTab({ - current: this.props.city === cityName, - key: cityName, - onClick: this.props.onClicks[cityName], - })); - } - - // Tab to "Expand into new City" - const newCityOnClick = this.eventHandler().createNewCityPopup.bind(this.eventHandler(), division, this.props.cityStateSetter); - - tabs.push(this.renderTab({ - current: false, - key: "Expand into new City", - onClick: newCityOnClick, - })); - - return tabs; - } -} diff --git a/src/Corporation/ui/CityTabs.tsx b/src/Corporation/ui/CityTabs.tsx new file mode 100644 index 000000000..fa2b5d3e7 --- /dev/null +++ b/src/Corporation/ui/CityTabs.tsx @@ -0,0 +1,45 @@ +// React Components for the Corporation UI's City navigation tabs +// These allow player to navigate between different cities for each industry +import React from "react"; +import { CityTab } from "./CityTab"; +import { ExpandNewCityPopup } from "./ExpandNewCityPopup"; +import { createPopup } from "../../ui/React/createPopup"; +import { ICorporation } from "../ICorporation"; +import { CorporationRouting } from "./Routing"; + +interface IProps { + routing: CorporationRouting; + onClicks: {[key: string]: () => void}; + city: string; // currentCity + cityStateSetter: (city: string) => void; + corp: ICorporation; +} + +export function CityTabs(props: IProps): React.ReactElement { + const division = props.routing.currentDivision; + + function openExpandNewCityModal(): void { + if(division === null) return; + const popupId = "cmpy-mgmt-expand-city-popup"; + createPopup(popupId, ExpandNewCityPopup, { + popupId: popupId, + corp: props.corp, + division: division, + cityStateSetter: props.cityStateSetter, + }); + } + + return <> + { + Object.keys(props.onClicks).map((cityName: string) => + , + ) + } + + ; +} \ No newline at end of file diff --git a/src/Corporation/ui/CorporationUIEventHandler.js b/src/Corporation/ui/CorporationUIEventHandler.js deleted file mode 100644 index 687457f55..000000000 --- a/src/Corporation/ui/CorporationUIEventHandler.js +++ /dev/null @@ -1,1753 +0,0 @@ -// Creates a class for handling UI events, such as clicks and keyboard events -import { CorporationRouting } from "./Routing"; -import { Corporation, - Industry, - OfficeSpace, - Warehouse, - DividendMaxPercentage, - IssueNewSharesCooldown, - OfficeInitialCost, - OfficeInitialSize, - SellSharesCooldown, - WarehouseInitialCost, - WarehouseInitialSize, - BribeToRepRatio } from "../Corporation"; - -import { Industries, - IndustryStartingCosts, - IndustryDescriptions, -} from "../IndustryData"; - -import { MaterialSizes } from "../MaterialSizes"; - -import { Product } from "../Product"; - -import { Player } from "../../Player"; - -import { Factions } from "../../Faction/Factions"; -import { Cities } from "../../Locations/Cities"; - -import { numeralWrapper } from "../../ui/numeralFormat"; - -import { dialogBoxCreate } from "../../../utils/DialogBox"; - -import { getRandomInt } from "../../../utils/helpers/getRandomInt"; -import { KEY } from "../../../utils/helpers/keyCodes"; - -import { clearSelector } from "../../../utils/uiHelpers/clearSelector"; -import { createElement } from "../../../utils/uiHelpers/createElement"; -import { createOptionElement } from "../../../utils/uiHelpers/createOptionElement"; -import { createPopup } from "../../../utils/uiHelpers/createPopup"; -import { createPopupCloseButton } from "../../../utils/uiHelpers/createPopupCloseButton"; -import { getSelectText, - getSelectValue } from "../../../utils/uiHelpers/getSelectData"; -import { removeElementById } from "../../../utils/uiHelpers/removeElementById"; - -export class CorporationEventHandler { - constructor(corp, routing) { - if (!(corp instanceof Corporation)) { - throw new Error(`CorporationEventHandler constructed without proper Corporation instance`); - } - if (!(routing instanceof CorporationRouting)) { - throw new Error(`CorporationEventHandler constructed without proper CorporationRouting instance`); - } - - this.corp = corp; - this.routing = routing; - } - - // Create a popup that lets the player bribe factions - // This is created when the player clicks the "Bribe Factions" button in the overview panel - createBribeFactionsPopup() { - const popupId = "cmpy-mgmt-bribe-factions-popup"; - const txt = createElement("p", { - innerText:"You can use Corporation funds or stock shares to bribe Faction Leaders in exchange for faction reputation", - }); - const factionSelector = createElement("select", { class: "dropdown", margin:"3px" }); - for (let i = 0; i < Player.factions.length; ++i) { - const facName = Player.factions[i]; - - const faction = Factions[facName]; - const info = faction.getInfo(); - if(!info.offersWork()) continue; - - factionSelector.add(createElement("option", { - text: facName, value: facName, - })); - } - var repGainText = createElement("p"); - var stockSharesInput; - var moneyInput = createElement("input", { - class: "text-input", - type:"number", placeholder:"Corporation funds", margin:"5px", - inputListener:()=>{ - var money = moneyInput.value == null || moneyInput.value == "" ? 0 : parseFloat(moneyInput.value); - var stockPrice = this.corp.sharePrice; - var stockShares = stockSharesInput.value == null || stockSharesInput.value == "" ? 0 : Math.round(parseFloat(stockSharesInput.value)); - if (isNaN(money) || isNaN(stockShares) || money < 0 || stockShares < 0) { - repGainText.innerText = "ERROR: Invalid value(s) entered"; - } else if (this.corp.funds.lt(money)) { - repGainText.innerText = "ERROR: You do not have this much money to bribe with"; - } else if (this.corp.stockShares > this.corp.numShares) { - repGainText.innerText = "ERROR: You do not have this many shares to bribe with"; - } else { - - var totalAmount = Number(money) + (stockShares * stockPrice); - var repGain = totalAmount / BribeToRepRatio; - repGainText.innerText = "You will gain " + numeralWrapper.format(repGain, "0,0") + - " reputation with " + - factionSelector.options[factionSelector.selectedIndex].value + - " with this bribe"; - } - }, - }); - stockSharesInput = createElement("input", { - class: "text-input", - type:"number", placeholder:"Stock Shares", margin: "5px", - inputListener:()=>{ - var money = moneyInput.value == null || moneyInput.value == "" ? 0 : parseFloat(moneyInput.value); - var stockPrice = this.corp.sharePrice; - var stockShares = stockSharesInput.value == null || stockSharesInput.value == "" ? 0 : Math.round(stockSharesInput.value); - if (isNaN(money) || isNaN(stockShares) || money < 0 || stockShares < 0) { - repGainText.innerText = "ERROR: Invalid value(s) entered"; - } else if (this.corp.funds.lt(money)) { - repGainText.innerText = "ERROR: You do not have this much money to bribe with"; - } else if (this.corp.stockShares > this.corp.numShares) { - repGainText.innerText = "ERROR: You do not have this many shares to bribe with"; - } else { - var totalAmount = money + (stockShares * stockPrice); - var repGain = totalAmount / BribeToRepRatio; - repGainText.innerText = "You will gain " + numeralWrapper.format(repGain, "0,0") + - " reputation with " + - factionSelector.options[factionSelector.selectedIndex].value + - " with this bribe"; - } - }, - }); - var confirmButton = createElement("button", { - class:"a-link-button", innerText:"Bribe", display:"inline-block", - clickListener:()=>{ - var money = moneyInput.value == null || moneyInput.value == "" ? 0 : parseFloat(moneyInput.value); - var stockPrice = this.corp.sharePrice; - var stockShares = stockSharesInput.value == null || stockSharesInput.value == ""? 0 : Math.round(parseFloat(stockSharesInput.value)); - var fac = Factions[factionSelector.options[factionSelector.selectedIndex].value]; - if (fac == null) { - dialogBoxCreate("ERROR: You must select a faction to bribe"); - return false; - } - if (isNaN(money) || isNaN(stockShares) || money < 0 || stockShares < 0) { - dialogBoxCreate("ERROR: Invalid value(s) entered"); - } else if (this.corp.funds.lt(money)) { - dialogBoxCreate("ERROR: You do not have this much money to bribe with"); - } else if (stockShares > this.corp.numShares) { - dialogBoxCreate("ERROR: You do not have this many shares to bribe with"); - } else { - var totalAmount = money + (stockShares * stockPrice); - var repGain = totalAmount / BribeToRepRatio; - 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); - this.corp.numShares -= stockShares; - removeElementById(popupId); - return false; - } - }, - }); - const cancelButton = createPopupCloseButton(popupId, { - class: "std-button", - display: "inline-block", - innerText: "Cancel", - }) - - createPopup(popupId, [txt, factionSelector, repGainText, - moneyInput, stockSharesInput, confirmButton, cancelButton]); - } - - // Create a popup that lets the player buyback shares - // This is created when the player clicks the "Buyback Shares" button in the overview panel - createBuybackSharesPopup() { - const popupId = "cmpy-mgmt-buyback-shares-popup"; - const currentStockPrice = this.corp.sharePrice; - const buybackPrice = currentStockPrice * 1.1; - const txt = createElement("p", { - innerHTML: "Enter the number of outstanding shares you would like to buy back. " + - "These shares must be bought at a 10% premium. However, " + - "repurchasing shares from the market tends to lead to an increase in stock price.

" + - "To purchase these shares, you must use your own money (NOT your Corporation's funds).

" + - "The current buyback price of your company's stock is " + - numeralWrapper.format(buybackPrice, "$0.000a") + - ". Your company currently has " + numeralWrapper.formatBigNumber(this.corp.issuedShares) + " outstanding stock shares", - }); - var costIndicator = createElement("p", {}); - var input = createElement("input", { - type:"number", placeholder:"Shares to buyback", margin:"5px", - inputListener: ()=> { - var numShares = Math.round(input.value); - if (isNaN(numShares) || numShares <= 0) { - costIndicator.innerText = "ERROR: Invalid value entered for number of shares to buyback" - } else if (numShares > this.corp.issuedShares) { - costIndicator.innerText = "There are not this many shares available to buy back. " + - "There are only " + numeralWrapper.formatBigNumber(this.corp.issuedShares) + - " outstanding shares."; - } else { - costIndicator.innerText = "Purchase " + numShares + " shares for a total of " + - numeralWrapper.format(numShares * buybackPrice, '$0.000a'); - } - }, - }); - var confirmBtn = createElement("button", { - class:"a-link-button", innerText:"Buy shares", display:"inline-block", - clickListener: () => { - var shares = Math.round(input.value); - const tempStockPrice = this.corp.sharePrice; - const buybackPrice = tempStockPrice * 1.1; - if (isNaN(shares) || shares <= 0) { - dialogBoxCreate("ERROR: Invalid value for number of shares"); - } else if (shares > this.corp.issuedShares) { - dialogBoxCreate("ERROR: There are not this many oustanding shares to buy back"); - } else if (shares * buybackPrice > Player.money) { - dialogBoxCreate("ERROR: You do not have enough money to purchase this many shares (you need " + - numeralWrapper.format(shares * buybackPrice, "$0.000a") + ")"); - } else { - this.corp.numShares += shares; - if (isNaN(this.corp.issuedShares)) { - console.warn("Corporation issuedShares is NaN: " + this.corp.issuedShares); - console.warn("Converting to number now"); - const res = parseInt(this.corp.issuedShares); - if (isNaN(res)) { - this.corp.issuedShares = 0; - } else { - this.corp.issuedShares = res; - } - } - this.corp.issuedShares -= shares; - Player.loseMoney(shares * buybackPrice); - removeElementById(popupId); - this.rerender(); - } - return false; - - }, - }); - var cancelBtn = createPopupCloseButton(popupId, { - class: "std-button", - display: "inline-block", - innerText: "Cancel", - }); - - createPopup(popupId, [txt, costIndicator, input, confirmBtn, cancelBtn]); - input.focus(); - } - - // Create a popup that lets the player discontinue a 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 " + - "removes it completely and permanently. You will no longer " + - "produce this product and all of its existing stock will be " + - "removed and left unsold", - }); - const confirmBtn = createElement("button", { - class:"popup-box-button",innerText:"Discontinue", - clickListener: () => { - industry.discontinueProduct(product); - removeElementById(popupId); - this.rerender(); - return false; - }, - }); - const cancelBtn = createPopupCloseButton(popupId, { innerText: "Cancel" }); - - createPopup(popupId, [txt, cancelBtn, confirmBtn]); - } - - // Create a popup that lets the player manage exports - createExportMaterialPopup(mat) { - const corp = this.corp; - - const popupId = "cmpy-mgmt-export-popup"; - const exportTxt = createElement("p", { - innerText:"Select the industry and city to export this material to, as well as " + - "how much of this material to export per second. You can set the export " + - "amount to 'MAX' to export all of the materials in this warehouse.", - }); - - //Select industry and city to export to - const citySelector = createElement("select", {class: "dropdown"}); - const industrySelector = createElement("select", { - class: "dropdown", - changeListener: () => { - const industryName = getSelectValue(industrySelector); - for (let i = 0; i < corp.divisions.length; ++i) { - if (corp.divisions[i].name == industryName) { - clearSelector(citySelector); - for (const cityName in corp.divisions[i].warehouses) { - if (corp.divisions[i].warehouses[cityName] instanceof Warehouse) { - citySelector.add(createElement("option", { - value:cityName, text:cityName, - })); - } - } - return; - } - } - }, - }); - - for (let i = 0; i < corp.divisions.length; ++i) { - industrySelector.add(createOptionElement(corp.divisions[i].name)); - } - - // Force change listener to initialize citySelector - industrySelector.dispatchEvent(new Event("change")); - - //Select amount to export - const exportAmount = createElement("input", { - class: "text-input", - placeholder:"Export amount / s", - }); - - const exportBtn = createElement("button", { - class: "std-button", display:"inline-block", innerText:"Export", - clickListener: () => { - const industryName = getSelectText(industrySelector); - const cityName = citySelector.options[citySelector.selectedIndex].text; - const amt = exportAmount.value; - - // Sanitize amt - let sanitizedAmt = amt.replace(/\s+/g, ''); - sanitizedAmt = sanitizedAmt.replace(/[^-()\d/*+.MAX]/g, ''); - let temp = sanitizedAmt.replace(/MAX/g, 1); - try { - temp = eval(temp); - } catch(e) { - dialogBoxCreate("Invalid expression entered for export amount: " + e); - return false; - } - - if (temp == null || isNaN(temp) || temp < 0) { - dialogBoxCreate("Invalid amount entered for export"); - return; - } - var exportObj = {ind:industryName, city:cityName, amt:sanitizedAmt}; - mat.exp.push(exportObj); - removeElementById(popupId); - return false; - }, - }); - - const cancelBtn = createPopupCloseButton(popupId, { innerText: "Cancel" }); - - const currExportsText = createElement("p", { - innerText:"Below is a list of all current exports of this material from this warehouse. " + - "Clicking on one of the exports below will REMOVE that export.", - }); - const currExports = []; - for (var i = 0; i < mat.exp.length; ++i) { - (function(i, mat, currExports){ - currExports.push(createElement("div", { - class:"cmpy-mgmt-existing-export", - innerHTML: "Industry: " + mat.exp[i].ind + "
" + - "City: " + mat.exp[i].city + "
" + - "Amount/s: " + mat.exp[i].amt, - clickListener:()=>{ - mat.exp.splice(i, 1); //Remove export object - removeElementById(popupId); - createExportMaterialPopup(mat); - }, - })); - })(i, mat, currExports); - } - createPopup(popupId, [exportTxt, industrySelector, citySelector, exportAmount, - exportBtn, cancelBtn, currExportsText].concat(currExports)); - } - - // Create a popup that lets the player issue & manage dividends - // This is created when the player clicks the "Issue Dividends" button in the overview panel - createIssueDividendsPopup() { - const popupId = "cmpy-mgmt-issue-dividends-popup"; - const descText = "Dividends are a distribution of a portion of the corporation's " + - "profits to the shareholders. This includes yourself, as well.

" + - "In order to issue dividends, simply allocate some percentage " + - "of your corporation's profits to dividends. This percentage must be an " + - `integer between 0 and ${DividendMaxPercentage}. (A percentage of 0 means no dividends will be ` + - "issued

" + - "Two important things to note:
" + - " * Issuing dividends will negatively affect your corporation's stock price
" + - " * Dividends are taxed. Taxes start at 50%, but can be decreased

" + - "Example: Assume your corporation makes $100m / sec in profit and you allocate " + - "40% of that towards dividends. That means your corporation will gain $60m / sec " + - "in funds and the remaining $40m / sec will be paid as dividends. Since your " + - "corporation starts with 1 billion shares, every shareholder will be paid $0.04 per share " + - "per second before taxes."; - const txt = createElement("p", { innerHTML: descText }); - - let allocateBtn; - const dividendPercentInput = createElement("input", { - margin: "5px", - placeholder: "Dividend %", - type: "number", - onkeyup: (e) => { - e.preventDefault(); - if (e.keyCode === KEY.ENTER) {allocateBtn.click();} - }, - }); - - allocateBtn = createElement("button", { - class: "std-button", - display: "inline-block", - innerText: "Allocate Dividend Percentage", - clickListener: () => { - const percentage = Math.round(parseInt(dividendPercentInput.value)); - if (isNaN(percentage) || percentage < 0 || percentage > DividendMaxPercentage) { - return dialogBoxCreate(`Invalid value. Must be an integer between 0 and ${DividendMaxPercentage}`); - } - - this.corp.dividendPercentage = percentage; - - removeElementById(popupId); - - this.rerender(); - return false; - }, - }); - - const cancelBtn = createPopupCloseButton(popupId, { - class: "std-button", - display: "inline-block", - innerText: "Cancel", - }); - - createPopup(popupId, [txt, dividendPercentInput, allocateBtn, cancelBtn]); - dividendPercentInput.focus(); - } - - // Create a popup that lets the player issue new shares - // This is created when the player clicks the "Issue New Shares" buttons in the overview panel - createIssueNewSharesPopup() { - const popupId = "cmpy-mgmt-issue-new-shares-popup"; - const maxNewSharesUnrounded = Math.round(this.corp.totalShares * 0.2); - const maxNewShares = maxNewSharesUnrounded - (maxNewSharesUnrounded % 1e6); - - const descText = createElement("p", { - innerHTML: "You can issue new equity shares (i.e. stocks) in order to raise " + - "capital for your corporation.

" + - ` * You can issue at most ${numeralWrapper.format(maxNewShares, "0.000a")} new shares
` + - ` * New shares are sold at a 10% discount
` + - ` * You can only issue new shares once every 12 hours
` + - ` * Issuing new shares causes dilution, resulting in a decrease in stock price and lower dividends per share
` + - ` * Number of new shares issued must be a multiple of 10 million

` + - `When you choose to issue new equity, private shareholders have first priority for up to 50% of the new shares. ` + - `If they choose to exercise this option, these newly issued shares become private, restricted shares, which means ` + - `you cannot buy them back.`, - }); - - let issueBtn, newSharesInput; - const dynamicText = createElement("p", { - display: "block", - }); - - function updateDynamicText(corp) { - const newSharePrice = Math.round(corp.sharePrice * 0.9); - let newShares = parseInt(newSharesInput.value); - if (isNaN(newShares)) { - dynamicText.innerText = "Invalid input"; - return; - } - - // Round to nearest ten-millionth - newShares /= 10e6; - newShares = Math.round(newShares) * 10e6; - - if (newShares < 10e6) { - dynamicText.innerText = "Must issue at least 10 million new shares"; - return; - } - - if (newShares > maxNewShares) { - dynamicText.innerText = "You cannot issue that many shares"; - return; - } - - dynamicText.innerText = `Issue ${numeralWrapper.format(newShares, "0.000a")} new shares ` + - `for ${numeralWrapper.formatMoney(newShares * newSharePrice)}?` - } - newSharesInput = createElement("input", { - margin: "5px", - placeholder: "# New Shares", - type: "number", - onkeyup: (e) => { - e.preventDefault(); - if (e.keyCode === KEY.ENTER) { - issueBtn.click(); - } else { - updateDynamicText(this.corp); - } - }, - }); - - issueBtn = createElement("button", { - class: "std-button", - display: "inline-block", - innerText: "Issue New Shares", - clickListener: () => { - const newSharePrice = Math.round(this.corp.sharePrice * 0.9); - let newShares = parseInt(newSharesInput.value); - if (isNaN(newShares)) { - dialogBoxCreate("Invalid input for number of new shares"); - return; - } - - // Round to nearest ten-millionth - newShares = Math.round(newShares / 10e6) * 10e6; - - if (newShares < 10e6 || newShares > maxNewShares) { - dialogBoxCreate("Invalid input for number of new shares"); - return; - } - - const profit = newShares * newSharePrice; - this.corp.issueNewSharesCooldown = IssueNewSharesCooldown; - this.corp.totalShares += newShares; - - // Determine how many are bought by private investors - // Private investors get up to 50% at most - // Round # of private shares to the nearest millionth - let privateShares = getRandomInt(0, Math.round(newShares / 2)); - privateShares = Math.round(privateShares / 1e6) * 1e6; - - this.corp.issuedShares += (newShares - privateShares); - this.corp.funds = this.corp.funds.plus(profit); - this.corp.immediatelyUpdateSharePrice(); - - removeElementById(popupId); - dialogBoxCreate(`Issued ${numeralWrapper.format(newShares, "0.000a")} and raised ` + - `${numeralWrapper.formatMoney(profit)}. ${numeralWrapper.format(privateShares, "0.000a")} ` + - `of these shares were bought by private investors.

` + - `Stock price decreased to ${numeralWrapper.formatMoney(this.corp.sharePrice)}`); - - this.rerender(); - return false; - }, - }); - - const cancelBtn = createPopupCloseButton(popupId, { - class: "std-button", - display: "inline-block", - innerText: "Cancel", - }); - - createPopup(popupId, [descText, dynamicText, newSharesInput, issueBtn, cancelBtn]); - newSharesInput.focus(); - } - - // Create a popup that lets the player limit the production of a product - createLimitProductProdutionPopup(product, city) { - const popupId = "cmpy-mgmt-limit-product-production-popup"; - const txt = createElement("p", { - innerText:"Enter a limit to the amount of this product you would " + - "like to product per second. Leave the box empty to set no limit.", - }); - let confirmBtn; - const input = createElement("input", { - margin: "5px", - placeholder:"Limit", - type:"number", - onkeyup: (e) => { - e.preventDefault(); - if (e.keyCode === KEY.ENTER) { confirmBtn.click(); } - }, - }); - confirmBtn = createElement("button", { - class: "std-button", - display: "inline-block", - innerText: "Limit production", - margin: "5px", - clickListener: () => { - if (input.value === "") { - product.prdman[city][0] = false; - removeElementById(popupId); - return false; - } - var qty = parseFloat(input.value); - if (isNaN(qty)) { - dialogBoxCreate("Invalid value entered"); - return false; - } - if (qty < 0) { - product.prdman[city][0] = false; - } else { - product.prdman[city][0] = true; - product.prdman[city][1] = qty; - } - removeElementById(popupId); - this.rerender(); - return false; - }, - }); - const cancelBtn = createPopupCloseButton(popupId, { innerText: "Cancel" }); - cancelBtn.style.margin = "6px"; - - createPopup(popupId, [txt, input, confirmBtn, cancelBtn]); - input.focus(); - } - - // Create a popup that lets the player create a product for their current industry - createMakeProductPopup(popupText, division) { - if (division.hasMaximumNumberProducts()) { return; } - - const popupId = "cmpy-mgmt-create-product-popup"; - const txt = createElement("p", { - innerHTML: popupText, - }); - const designCity = createElement("select", { - class: "dropdown", - margin: "5px", - }); - for (const cityName in division.offices) { - if (division.offices[cityName] instanceof OfficeSpace) { - designCity.add(createElement("option", { - value: cityName, - text: cityName, - })); - } - } - let productNamePlaceholder = "Product Name"; - if (division.type === Industries.Food) { - productNamePlaceholder = "Restaurant Name"; - } else if (division.type === Industries.Healthcare) { - productNamePlaceholder = "Hospital Name"; - } else if (division.type === Industries.RealEstate) { - productNamePlaceholder = "Property Name"; - } - var productNameInput = createElement("input", { - class: "text-input", - margin: "5px", - placeholder: productNamePlaceholder, - }); - var lineBreak1 = createElement("br"); - var designInvestInput = createElement("input", { - class: "text-input", - margin: "5px", - placeholder: "Design investment", - type: "number", - }); - let confirmBtn; - var marketingInvestInput = createElement("input", { - class: "text-input", - margin: "5px", - placeholder: "Marketing investment", - type: "number", - onkeyup: (e) => { - e.preventDefault(); - if (e.keyCode === KEY.ENTER) { confirmBtn.click(); } - }, - }); - confirmBtn = createElement("button", { - class: "std-button", - innerText: "Develop Product", - clickListener: () => { - if (designInvestInput.value == null || designInvestInput.value < 0) { designInvestInput.value = 0; } - if (marketingInvestInput.value == null || marketingInvestInput.value < 0) { marketingInvestInput.value = 0; } - var designInvest = parseFloat(designInvestInput.value), - marketingInvest = parseFloat(marketingInvestInput.value); - if (productNameInput.value == null || productNameInput.value === "") { - dialogBoxCreate("You must specify a name for your product!"); - } else if (isNaN(designInvest)) { - dialogBoxCreate("Invalid value for design investment"); - } else if (isNaN(marketingInvest)) { - dialogBoxCreate("Invalid value for marketing investment"); - } else if (this.corp.funds.lt(designInvest + marketingInvest)) { - dialogBoxCreate("You don't have enough company funds to make this large of an investment"); - } else { - const product = new Product({ - name:productNameInput.value.replace(/[<>]/g, ''), //Sanitize for HTMl elements - createCity:designCity.options[designCity.selectedIndex].value, - designCost: designInvest, - advCost: marketingInvest, - }); - if (division.products[product.name] instanceof Product) { - dialogBoxCreate(`You already have a product with this name!`); - return; - } - this.corp.funds = this.corp.funds.minus(designInvest + marketingInvest); - division.products[product.name] = product; - removeElementById(popupId); - } - this.rerender(); - return false; - }, - }) - const cancelBtn = createPopupCloseButton(popupId, { - class: "std-button", - innerText: "Cancel", - }); - - createPopup(popupId, [txt, designCity, productNameInput, lineBreak1, - designInvestInput, marketingInvestInput, confirmBtn, cancelBtn]); - productNameInput.focus(); - } - - // Create a popup that lets the player use the Market TA research for Materials - createMaterialMarketTaPopup(mat, industry) { - const popupId = "cmpy-mgmt-marketta-popup"; - const markupLimit = mat.getMarkupLimit(); - const ta1 = createElement("p", { - innerHTML: "Market-TA.I
" + - "The maximum sale price you can mark this up to is " + - numeralWrapper.formatMoney(mat.bCost + 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 Material will automatically " + - "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", - changeListener: (e) => { - mat.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: mat.bCost, - }); - - // Function that updates the text in ta2Text element - updateTa2Text = function() { - const sCost = parseFloat(ta2Input.value); - let markup = 1; - if (sCost > mat.bCost) { - //Penalty if difference between sCost and bCost is greater than markup limit - if ((sCost - mat.bCost) > markupLimit) { - markup = Math.pow(markupLimit / (sCost - mat.bCost), 2); - } - } else if (sCost < mat.bCost) { - if (sCost <= 0) { - markup = 1e12; //Sell everything, essentially discard - } else { - //Lower prices than market increases sales - markup = mat.bCost / sCost; - } - } - 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 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); - - const ta2OverridesTa1 = createElement("p", { - innerText: "Note that Market-TA.II overrides Market-TA.I. This means that if " + - "both are enabled, then Market-TA.II will take effect, not Market-TA.I", - }); - - createPopup(popupId, [ta1, useTa1AutoSaleDiv, ta2Text, ta2Input, useTa2AutoSaleDiv, ta2OverridesTa1, closeBtn]); - } else { - // Market-TA.I only - createPopup(popupId, [ta1, useTa1AutoSaleDiv, closeBtn]); - } - } - - // Create a popup that lets the player expand into a new city (for the current industry) - // The 'cityStateSetter' arg is a function that sets the UI's 'city' state property - createNewCityPopup(division, cityStateSetter) { - const popupId = "cmpy-mgmt-expand-city-popup"; - const text = createElement("p", { - innerText: "Would you like to expand into a new city by opening an office? " + - "This would cost " + numeralWrapper.format(OfficeInitialCost, '$0.000a'), - }); - const citySelector = createElement("select", { class: "dropdown", margin:"5px" }); - for (const cityName in division.offices) { - if (!(division.offices[cityName] instanceof OfficeSpace)) { - citySelector.add(createElement("option", { - text: cityName, - value: cityName, - })); - } - } - - const confirmBtn = createElement("button", { - class:"std-button", - 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!"); - } else { - this.corp.funds = this.corp.funds.minus(OfficeInitialCost); - dialogBoxCreate("Opened a new office in " + city + "!"); - division.offices[city] = new OfficeSpace({ - loc: city, - size: OfficeInitialSize, - }); - } - - cityStateSetter(city); - removeElementById(popupId); - this.rerender(); - return false; - }, - }); - const cancelBtn = createPopupCloseButton(popupId, { - class: "std-button", - innerText: "Cancel", - }); - - createPopup(popupId, [text, citySelector, confirmBtn, cancelBtn]); - } - - // Create a popup that lets the player create a new industry. - // This is created when the player clicks the "Expand into new Industry" header tab - createNewIndustryPopup() { - const popupId = "cmpy-mgmt-expand-industry-popup"; - if (document.getElementById(popupId) != null) { return; } - - var txt = createElement("p", { - innerHTML: "Create a new division to expand into a new industry:", - }); - var selector = createElement("select", { - class:"dropdown", - }); - var industryDescription = createElement("p", {}); - var yesBtn; - var nameInput = createElement("input", { - type:"text", - id:"cmpy-mgmt-expand-industry-name-input", - class: "text-input", - display:"block", - maxLength: 30, - pattern:"[a-zA-Z0-9-_]", - onkeyup:(e)=>{ - e.preventDefault(); - if (e.keyCode === KEY.ENTER) {yesBtn.click();} - }, - }); - var nameLabel = createElement("label", { - for:"cmpy-mgmt-expand-industry-name-input", - innerText:"Division name: ", - }); - yesBtn = createElement("span", { - class:"popup-box-button", - innerText:"Create Division", - clickListener: ()=>{ - const ind = selector.options[selector.selectedIndex].value; - const newDivisionName = nameInput.value; - - for (let i = 0; i < this.corp.divisions.length; ++i) { - if (this.corp.divisions[i].name === newDivisionName) { - dialogBoxCreate("This name is already in use!"); - return false; - } - } - if (this.corp.funds.lt(IndustryStartingCosts[ind])) { - dialogBoxCreate("Not enough money to create a new division in this industry"); - } else if (newDivisionName === "") { - dialogBoxCreate("New division must have a name!"); - } else { - this.corp.funds = this.corp.funds.minus(IndustryStartingCosts[ind]); - var newInd = new Industry({ - corp: this.corp, - name: newDivisionName, - type: ind, - }); - this.corp.divisions.push(newInd); - - // Set routing to the new division so that the UI automatically switches to it - this.routing.routeTo(newDivisionName); - - removeElementById("cmpy-mgmt-expand-industry-popup"); - this.rerender(); - } - return false; - }, - }); - - const noBtn = createPopupCloseButton(popupId, { - display: "inline-block", - innerText: "Cancel", - }); - - // Make an object to keep track of what industries you're already in - const ownedIndustries = {}; - for (let i = 0; i < this.corp.divisions.length; ++i) { - ownedIndustries[this.corp.divisions[i].type] = true; - } - - // Add industry types to selector - // Have Agriculture be first as recommended option - if (!ownedIndustries["Agriculture"]) { - selector.add(createElement("option", { - text:Industries["Agriculture"], value:"Agriculture", - })); - } - - for (var key in Industries) { - if (key !== "Agriculture" && Industries.hasOwnProperty(key) && !ownedIndustries[key]) { - var ind = Industries[key]; - selector.add(createElement("option", { - text: ind,value:key, - })); - } - } - - //Initial Industry Description - var ind = selector.options[selector.selectedIndex].value; - industryDescription.innerHTML = (IndustryDescriptions[ind] + "

"); - - //Change the industry description text based on selected option - selector.addEventListener("change", function() { - var ind = selector.options[selector.selectedIndex].value; - industryDescription.innerHTML = IndustryDescriptions[ind] + "

"; - }); - - //Add to DOM - const elems = []; - elems.push(txt); - elems.push(selector); - elems.push(industryDescription); - elems.push(nameLabel); - elems.push(nameInput); - elems.push(noBtn); - elems.push(yesBtn); - - createPopup(popupId, elems); - nameInput.focus(); - - return false; - } - - // Create a popup that lets the player use the Market TA research for Products - createProductMarketTaPopup(product, industry) { - 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); - - const ta2OverridesTa1 = createElement("p", { - innerText: "Note that Market-TA.II overrides Market-TA.I. This means that if " + - "both are enabled, then Market-TA.II will take effect, not Market-TA.I", - }); - - createPopup(popupId, [ta1, useTa1AutoSaleDiv, ta2Text, ta2Input, useTa2AutoSaleDiv, ta2OverridesTa1, closeBtn]); - } else { - // Market-TA.I only - createPopup(popupId, [ta1, useTa1AutoSaleDiv, closeBtn]); - } - } - - // Create a popup that lets the player purchase a Material - createPurchaseMaterialPopup(mat, industry, warehouse) { - const corp = this.corp; - - const purchasePopupId = "cmpy-mgmt-material-purchase-popup"; - const txt = createElement("p", { - innerHTML: "Enter the amount of " + mat.name + " you would like " + - "to purchase per second. This material's cost changes constantly", - }); - let confirmBtn; - let input = createElement("input", { - margin: "5px", - placeholder: "Purchase amount", - type: "number", - value: mat.buy ? mat.buy : null, - onkeyup: (e) => { - e.preventDefault(); - if (e.keyCode === KEY.ENTER) {confirmBtn.click();} - }, - }); - confirmBtn = createElement("button", { - innerText: "Confirm", class: "std-button", - clickListener: () => { - if (isNaN(input.value)) { - dialogBoxCreate("Invalid amount"); - } else { - mat.buy = parseFloat(input.value); - if (isNaN(mat.buy)) {mat.buy = 0;} - removeElementById(purchasePopupId); - this.rerender(); - return false; - } - }, - }); - const clearButton = createElement("button", { - innerText: "Clear Purchase", class: "std-button", - clickListener: () => { - mat.buy = 0; - removeElementById(purchasePopupId); - this.rerender(); - return false; - }, - }); - const cancelBtn = createPopupCloseButton(purchasePopupId, { - class: "std-button", - innerText: "Cancel", - }); - - const elems = [txt, input, confirmBtn, clearButton, cancelBtn]; - - if (industry.hasResearch("Bulk Purchasing")) { - const bulkPurchaseInfo = createElement("p", { - innerText: "Enter the amount of " + mat.name + " you would like " + - "to bulk purchase. This purchases the specified amount instantly " + - "(all at once).", - }); - - let bulkPurchaseCostTxt = createElement("p"); - function updateBulkPurchaseText(amount) { - const parsedAmt = parseFloat(amount); - const cost = parsedAmt * mat.bCost; - - 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; - const bulkPurchaseInput = createElement("input", { - margin: "5px", - placeholder: "Bulk Purchase amount", - type: "number", - onkeyup: (e) => { - e.preventDefault(); - updateBulkPurchaseText(e.target.value); - if (e.keyCode === KEY.ENTER) {bulkPurchaseConfirmBtn.click();} - }, - }); - - bulkPurchaseConfirmBtn = createElement("button", { - class: "std-button", - innerText: "Confirm Bulk Purchase", - clickListener: () => { - 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 { - const cost = amount * mat.bCost; - if (corp.funds.gt(cost)) { - corp.funds = corp.funds.minus(cost); - mat.qty += amount; - } else { - dialogBoxCreate(`You cannot afford this purchase.`); - return false; - } - - removeElementById(purchasePopupId); - return false; - } - }, - }) - - elems.push(bulkPurchaseInfo); - elems.push(bulkPurchaseCostTxt); - elems.push(bulkPurchaseInput); - elems.push(bulkPurchaseConfirmBtn); - } - - createPopup(purchasePopupId, elems); - input.focus(); - } - - // Create a popup that let the player manage sales of a material - createSellMaterialPopup(mat) { - const sellPopupId = "cmpy-mgmt-material-sell-popup"; - const txt = createElement("p", { - innerHTML: "Enter the maximum amount of " + mat.name + " you would like " + - "to sell per second, as well as the price at which you would " + - "like to sell at.

" + - "If the sell amount is set to 0, then the material will not be sold. If the sell price " + - "if set to 0, then the material will be discarded

" + - "Setting the sell amount to 'MAX' will result in you always selling the " + - "maximum possible amount of the material.

" + - "When setting the sell amount, you can use the 'PROD' variable to designate a dynamically " + - "changing amount that depends on your production. For example, if you set the sell amount " + - "to 'PROD-5' then you will always sell 5 less of the material than you produce.

" + - "When setting the sell price, you can use the 'MP' variable to designate a dynamically " + - "changing price that depends on the market price. For example, if you set the sell price " + - "to 'MP+10' then it will always be sold at $10 above the market price.", - }); - const br = createElement("br"); - let confirmBtn; - const inputQty = createElement("input", { - type: "text", marginTop: "4px", - value: mat.sllman[1] ? mat.sllman[1] : null, - placeholder: "Sell amount", - onkeyup: (e) => { - e.preventDefault(); - 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: inputButtonInitValue, - placeholder: "Sell price", - onkeyup: (e) => { - e.preventDefault(); - if (e.keyCode === KEY.ENTER) {confirmBtn.click();} - }, - }); - confirmBtn = createElement("button", { - class: "std-button", - innerText: "Confirm", - clickListener: () => { - //Parse price - let cost = inputPx.value.replace(/\s+/g, ''); - cost = cost.replace(/[^-()\d/*+.MP]/g, ''); //Sanitize cost - let temp = cost.replace(/MP/g, mat.bCost); - try { - temp = eval(temp); - } catch(e) { - dialogBoxCreate("Invalid value or expression for sell price field: " + e); - return false; - } - - if (temp == null || isNaN(temp)) { - dialogBoxCreate("Invalid value or expression for sell price field"); - return false; - } - - if (cost.includes("MP")) { - mat.sCost = cost; //Dynamically evaluated - } else { - mat.sCost = temp; - } - - //Parse quantity - if (inputQty.value.includes("MAX") || inputQty.value.includes("PROD")) { - let qty = inputQty.value.replace(/\s+/g, ''); - qty = qty.replace(/[^-()\d/*+.MAXPROD]/g, ''); - let tempQty = qty.replace(/MAX/g, 1); - tempQty = tempQty.replace(/PROD/g, 1); - try { - tempQty = eval(tempQty); - } catch(e) { - dialogBoxCreate("Invalid value or expression for sell price field: " + e); - return false; - } - - if (tempQty == null || isNaN(tempQty)) { - dialogBoxCreate("Invalid value or expression for sell price field"); - return false; - } - - mat.sllman[0] = true; - mat.sllman[1] = qty; //Use sanitized input - } else if (isNaN(inputQty.value)) { - dialogBoxCreate("Invalid value for sell quantity field! Must be numeric or 'MAX'"); - return false; - } else { - var qty = parseFloat(inputQty.value); - if (isNaN(qty)) {qty = 0;} - if (qty === 0) { - mat.sllman[0] = false; - mat.sllman[1] = 0; - } else { - mat.sllman[0] = true; - mat.sllman[1] = qty; - } - } - - removeElementById(sellPopupId); - this.rerender(); - return false; - }, - }); - const cancelBtn = createPopupCloseButton(sellPopupId, { - class: "std-button", - innerText: "Cancel", - }); - - createPopup(sellPopupId, [txt, br, inputQty, inputPx, confirmBtn, cancelBtn]); - inputQty.focus(); - } - - // Create a popup that lets the player manage sales of the product - createSellProductPopup(product, city) { - const popupId = "cmpy-mgmt-sell-product-popup"; - const txt = createElement("p", { - innerHTML:"Enter the maximum amount of " + product.name + " you would like " + - "to sell per second, as well as the price at which you would like to " + - "sell it at.

" + - "If the sell amount is set to 0, then the product will not be sold. If the " + - "sell price is set to 0, then the product will be discarded.

" + - "Setting the sell amount to 'MAX' will result in you always selling the " + - "maximum possible amount of the material.

" + - "When setting the sell amount, you can use the 'PROD' variable to designate a " + - "dynamically changing amount that depends on your production. For example, " + - "if you set the sell amount to 'PROD-1' then you will always sell 1 less of " + - "the material than you produce.

" + - "When setting the sell price, you can use the 'MP' variable to set a " + - "dynamically changing price that depends on the Product's estimated " + - "market price. For example, if you set it to 'MP*5' then it " + - "will always be sold at five times the estimated market price.", - }); - let confirmBtn; - const inputQty = createElement("input", { - margin: "5px 0px 5px 0px", - placeholder: "Sell amount", - type: "text", - value: product.sllman[city][1] ? product.sllman[city][1] : null, - onkeyup: (e) => { - e.preventDefault(); - 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: 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", - clickListener: () => { - //Parse price - if (inputPx.value.includes("MP")) { - //Dynamically evaluated quantity. First test to make sure its valid - //Sanitize input, then replace dynamic variables with arbitrary numbers - var price = inputPx.value.replace(/\s+/g, ''); - price = price.replace(/[^-()\d/*+.MP]/g, ''); - var temp = price.replace(/MP/g, 1); - try { - temp = eval(temp); - } catch(e) { - dialogBoxCreate("Invalid value or expression for sell quantity field: " + e); - return false; - } - if (temp == null || isNaN(temp)) { - dialogBoxCreate("Invalid value or expression for sell quantity field."); - return false; - } - product.sCost = price; //Use sanitized price - } else { - var cost = parseFloat(inputPx.value); - if (isNaN(cost)) { - dialogBoxCreate("Invalid value for sell price field"); - return false; - } - product.sCost = cost; - } - - // Array of all cities. Used later - const cities = Object.keys(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, ''); - qty = qty.replace(/[^-()\d/*+.MAXPROD]/g, ''); - var temp = qty.replace(/MAX/g, 1); - temp = temp.replace(/PROD/g, 1); - try { - temp = eval(temp); - } catch(e) { - dialogBoxCreate("Invalid value or expression for sell price field: " + e); - return false; - } - - if (temp == null || isNaN(temp)) { - dialogBoxCreate("Invalid value or expression for sell price field"); - return false; - } - 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; - } else { - var qty = parseFloat(inputQty.value); - if (isNaN(qty)) {qty = 0;} - if (qty === 0) { - 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 { - 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; - } - } - } - - removeElementById(popupId); - this.rerender(); - return false; - }, - }); - const cancelBtn = createPopupCloseButton(popupId, { class: "std-button" }); - - const linebreak1 = createElement("br"); - - createPopup(popupId, [txt, inputQty, inputPx, confirmBtn, cancelBtn, linebreak1, - checkboxDiv]); - inputQty.focus(); - } - - // Create a popup that lets the player sell Corporation shares - // This is created when the player clicks the "Sell Shares" button in the overview panel - createSellSharesPopup() { - const popupId = "cmpy-mgmt-sell-shares-popup"; - const currentStockPrice = this.corp.sharePrice; - const txt = createElement("p", { - innerHTML: "Enter the number of shares you would like to sell. The money from " + - "selling your shares will go directly to you (NOT your Corporation).

" + - "Selling your shares will cause your corporation's stock price to fall due to " + - "dilution. Furthermore, selling a large number of shares all at once will have an immediate effect " + - "in reducing your stock price.

" + - "The current price of your " + - "company's stock is " + numeralWrapper.format(currentStockPrice, "$0.000a"), - }); - const profitIndicator = createElement("p"); - const input = createElement("input", { - class: "text-input", - type:"number", placeholder:"Shares to sell", margin:"5px", - inputListener: ()=> { - var numShares = Math.round(input.value); - if (isNaN(numShares) || numShares <= 0) { - profitIndicator.innerText = "ERROR: Invalid value entered for number of shares to sell" - } else if (numShares > this.corp.numShares) { - profitIndicator.innerText = "You don't have this many shares to sell!"; - } else { - const stockSaleResults = this.corp.calculateShareSale(numShares); - const profit = stockSaleResults[0]; - profitIndicator.innerText = "Sell " + numShares + " shares for a total of " + - numeralWrapper.format(profit, '$0.000a'); - } - }, - }); - const confirmBtn = createElement("button", { - class:"a-link-button", innerText:"Sell shares", display:"inline-block", - clickListener:()=>{ - var shares = Math.round(input.value); - if (isNaN(shares) || shares <= 0) { - dialogBoxCreate("ERROR: Invalid value for number of shares"); - } else if (shares > this.corp.numShares) { - dialogBoxCreate("ERROR: You don't have this many shares to sell"); - } else { - const stockSaleResults = this.corp.calculateShareSale(shares); - const profit = stockSaleResults[0]; - const newSharePrice = stockSaleResults[1]; - const newSharesUntilUpdate = stockSaleResults[2]; - - this.corp.numShares -= shares; - if (isNaN(this.corp.issuedShares)) { - console.error(`Corporation issuedShares is NaN: ${this.corp.issuedShares}`); - var res = parseInt(this.corp.issuedShares); - if (isNaN(res)) { - this.corp.issuedShares = 0; - } else { - this.corp.issuedShares = res; - } - } - this.corp.issuedShares += shares; - this.corp.sharePrice = newSharePrice; - this.corp.shareSalesUntilPriceUpdate = newSharesUntilUpdate; - this.corp.shareSaleCooldown = SellSharesCooldown; - Player.gainMoney(profit); - Player.recordMoneySource(profit, "corporation"); - removeElementById(popupId); - dialogBoxCreate(`Sold ${numeralWrapper.formatMoney(shares, "0.000a")} shares for ` + - `${numeralWrapper.formatMoney(profit, "$0.000a")}. ` + - `The corporation's stock price fell to ${numeralWrapper.formatMoney(this.corp.sharePrice)} ` + - `as a result of dilution.`); - - this.rerender(); - return false; - } - - }, - }); - const cancelBtn = createPopupCloseButton(popupId, { - class: "std-button", - display: "inline-block", - innerText: "Cancel", - }); - - createPopup(popupId, [txt, profitIndicator, input, confirmBtn, cancelBtn]); - input.focus(); - } - - // Creates a popup that lets the player throw an office party - createThrowOfficePartyPopup(office) { - const popupId = "cmpy-mgmt-throw-office-party-popup"; - const txt = createElement("p", { - innerText:"Enter the amount of money you would like to spend PER EMPLOYEE " + - "on this office party", - }); - const totalCostTxt = createElement("p", { - innerText:"Throwing this party will cost a total of $0", - }); - let confirmBtn; - const input = createElement("input", { - type: "number", margin: "5px", placeholder: "$ / employee", - inputListener: () => { - if (isNaN(input.value) || input.value < 0) { - totalCostTxt.innerText = "Invalid value entered!" - } else { - const totalCost = input.value * office.employees.length; - totalCostTxt.innerText = "Throwing this party will cost a total of " + numeralWrapper.format(totalCost, '$0.000a'); - } - }, - onkeyup:(e)=>{ - e.preventDefault(); - if (e.keyCode === KEY.ENTER) {confirmBtn.click();} - }, - }); - confirmBtn = createElement("button", { - class: "std-button", - innerText: "Throw Party", - clickListener:()=>{ - if (isNaN(input.value) || input.value < 0) { - dialogBoxCreate("Invalid value entered"); - } else { - var totalCost = input.value * office.employees.length; - if (this.corp.funds.lt(totalCost)) { - dialogBoxCreate("You don't have enough company funds to throw this.corp party!"); - } else { - this.corp.funds = this.corp.funds.minus(totalCost); - var mult; - for (let fooit = 0; fooit < office.employees.length; ++fooit) { - mult = office.employees[fooit].throwParty(input.value); - } - dialogBoxCreate("You threw a party for the office! The morale and happiness " + - "of each employee increased by " + numeralWrapper.formatPercentage((mult-1))); - removeElementById(popupId); - } - } - return false; - }, - }); - const cancelBtn = createPopupCloseButton(popupId, { class: "std-button", innerText: "Cancel" }); - - createPopup(popupId, [txt, totalCostTxt, input, confirmBtn, cancelBtn]); - input.focus(); - } - - // Creates a popup that lets the player upgrade the current OfficeSpace's size - createUpgradeOfficeSizePopup(office) { - const popupId = "cmpy-mgmt-upgrade-office-size-popup"; - const initialPriceMult = Math.round(office.size / OfficeInitialSize); - const costMultiplier = 1.09; - const upgradeCost = OfficeInitialCost * Math.pow(costMultiplier, initialPriceMult); - - // Calculate cost to upgrade size by 15 employees - let mult = 0; - for (let i = 0; i < 5; ++i) { - mult += (Math.pow(costMultiplier, initialPriceMult + i)); - } - const upgradeCost15 = OfficeInitialCost * mult; - - //Calculate max upgrade size and cost - let maxMult = (this.corp.funds.dividedBy(OfficeInitialCost)).toNumber(); - let maxNum = 1; - mult = Math.pow(costMultiplier, initialPriceMult); - while(maxNum < 50) { //Hard cap of 50x (extra 150 employees) - if (mult >= maxMult) {break;} - let multIncrease = Math.pow(costMultiplier, initialPriceMult + maxNum); - if (mult + multIncrease > maxMult) { - break; - } else { - mult += multIncrease; - } - ++maxNum; - } - const upgradeCostMax = OfficeInitialCost * mult; - - const text = createElement("p", { - innerText:"Increase the size of your office space to fit additional employees!", - }); - const text2 = createElement("p", { innerText: "Upgrade size: " }); - - const confirmBtn = createElement("button", { - class: this.corp.funds.lt(upgradeCost) ? "a-link-button-inactive" : "a-link-button", - display:"inline-block", margin:"4px", innerText:"by 3", - tooltip:numeralWrapper.format(upgradeCost, "$0.000a"), - clickListener:()=>{ - if (this.corp.funds.lt(upgradeCost)) { - dialogBoxCreate("You don't have enough company funds to purchase this upgrade!"); - } else { - office.size += OfficeInitialSize; - this.corp.funds = this.corp.funds.minus(upgradeCost); - dialogBoxCreate("Office space increased! It can now hold " + office.size + " employees"); - this.rerender(); - } - removeElementById(popupId); - return false; - }, - }); - const confirmBtn15 = createElement("button", { - class: this.corp.funds.lt(upgradeCost15) ? "a-link-button-inactive" : "a-link-button", - display:"inline-block", margin:"4px", innerText:"by 15", - tooltip:numeralWrapper.format(upgradeCost15, "$0.000a"), - clickListener:()=>{ - if (this.corp.funds.lt(upgradeCost15)) { - dialogBoxCreate("You don't have enough company funds to purchase this upgrade!"); - } else { - office.size += (OfficeInitialSize * 5); - this.corp.funds = this.corp.funds.minus(upgradeCost15); - dialogBoxCreate("Office space increased! It can now hold " + office.size + " employees"); - this.rerender(); - } - removeElementById(popupId); - return false; - }, - }); - const confirmBtnMax = createElement("button", { - class:this.corp.funds.lt(upgradeCostMax) ? "a-link-button-inactive" : "a-link-button", - display:"inline-block", margin:"4px", innerText:"by MAX (" + maxNum*OfficeInitialSize + ")", - tooltip:numeralWrapper.format(upgradeCostMax, "$0.000a"), - clickListener:()=>{ - if (this.corp.funds.lt(upgradeCostMax)) { - dialogBoxCreate("You don't have enough company funds to purchase this upgrade!"); - } else { - office.size += (OfficeInitialSize * maxNum); - this.corp.funds = this.corp.funds.minus(upgradeCostMax); - dialogBoxCreate("Office space increased! It can now hold " + office.size + " employees"); - this.rerender(); - } - removeElementById(popupId); - return false; - }, - }); - const cancelBtn = createPopupCloseButton(popupId, { innerText: "Cancel" }); - cancelBtn.style.margin = "4px"; - - createPopup(popupId, [text, text2, confirmBtn, confirmBtn15, confirmBtnMax, cancelBtn]); - } - - // Purchases a new Warehouse - purchaseWarehouse(division, city) { - const corp = this.corp; - if (corp.funds.lt(WarehouseInitialCost)) { - dialogBoxCreate("You do not have enough funds to do this!"); - } else { - division.warehouses[city] = new Warehouse({ - corp: corp, - industry: division, - loc: city, - size: WarehouseInitialSize, - }); - corp.funds = corp.funds.minus(WarehouseInitialCost); - this.rerender(); - } - } - - rerender() { - this.corp.rerender(); - } -} diff --git a/src/Corporation/ui/DiscontinueProductPopup.tsx b/src/Corporation/ui/DiscontinueProductPopup.tsx new file mode 100644 index 000000000..41c803f01 --- /dev/null +++ b/src/Corporation/ui/DiscontinueProductPopup.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { removePopup } from "../../ui/React/createPopup"; +import { ICorporation } from "../ICorporation"; +import { Product } from "../Product"; +import { IIndustry } from "../IIndustry"; +import { IPlayer } from "../../PersonObjects/IPlayer"; + +interface IProps { + product: Product; + industry: IIndustry; + corp: ICorporation; + popupId: string; + player: IPlayer; +} + +// Create a popup that lets the player discontinue a product +export function DiscontinueProductPopup(props: IProps): React.ReactElement { + function discontinue(): void { + props.industry.discontinueProduct(props.product); + removePopup(props.popupId); + props.corp.rerender(props.player); + } + + return (<> +

+Are you sure you want to do this? Discontinuing a product +removes it completely and permanently. You will no longer +produce this product and all of its existing stock will be +removed and left unsold

+ + ); +} \ No newline at end of file diff --git a/src/Corporation/ui/ExpandNewCityPopup.tsx b/src/Corporation/ui/ExpandNewCityPopup.tsx new file mode 100644 index 000000000..f40807fcc --- /dev/null +++ b/src/Corporation/ui/ExpandNewCityPopup.tsx @@ -0,0 +1,50 @@ +import React, { useRef } from "react"; +import { IIndustry } from "../IIndustry"; +import { numeralWrapper } from "../../ui/numeralFormat"; +import { CorporationConstants } from "../data/Constants"; +import { removePopup } from "../../ui/React/createPopup"; +import { dialogBoxCreate } from "../../../utils/DialogBox"; +import { OfficeSpace } from "../OfficeSpace"; +import { ICorporation } from "../ICorporation"; +import { NewCity } from "../Actions"; + +interface IProps { + popupId: string; + corp: ICorporation; + division: IIndustry; + cityStateSetter: (city: string) => void; +} + +export function ExpandNewCityPopup(props: IProps): React.ReactElement { + const dropdown = useRef(null); + + function expand(): void { + if(dropdown.current === null) return; + try { + NewCity(props.corp, props.division, dropdown.current.value); + } catch(err) { + dialogBoxCreate(err+''); + return; + } + + dialogBoxCreate(`Opened a new office in ${dropdown.current.value}!`); + + props.cityStateSetter(dropdown.current.value); + removePopup(props.popupId); + } + return (<> +

+ Would you like to expand into a new city by opening an office? + This would cost {numeralWrapper.format(CorporationConstants.OfficeInitialCost, '$0.000a')} +

+ + + ); +} \ No newline at end of file diff --git a/src/Corporation/ui/ExportPopup.tsx b/src/Corporation/ui/ExportPopup.tsx new file mode 100644 index 000000000..7ff9f5a89 --- /dev/null +++ b/src/Corporation/ui/ExportPopup.tsx @@ -0,0 +1,116 @@ +import React, { useState } from 'react'; +import { dialogBoxCreate } from "../../../utils/DialogBox"; +import { removePopup } from "../../ui/React/createPopup"; +import { ICorporation } from "../ICorporation"; +import { Material } from "../Material"; +import { Export } from "../Export"; +import { IIndustry } from "../IIndustry"; + +interface IProps { + mat: Material; + corp: ICorporation; + popupId: string; +} + +// Create a popup that lets the player manage exports +export function ExportPopup(props: IProps): React.ReactElement { + if(props.corp.divisions.length === 0) + throw new Error('Export popup created with no divisions.'); + if(Object.keys(props.corp.divisions[0].warehouses).length === 0) + throw new Error('Export popup created in a division with no warehouses.'); + const [industry, setIndustry] = useState(props.corp.divisions[0].name); + const [city, setCity] = useState(Object.keys(props.corp.divisions[0].warehouses)[0]); + const [amt, setAmt] = useState(''); + const setRerender = useState(false)[1]; + + function rerender(): void { + setRerender(old => !old); + } + + function onCityChange(event: React.ChangeEvent): void { + setCity(event.target.value); + } + + function onIndustryChange(event: React.ChangeEvent): void { + setIndustry(event.target.value); + } + + function onAmtChange(event: React.ChangeEvent): void { + setAmt(event.target.value); + } + + function exportMaterial(): void { + const industryName = industry; + const cityName = city; + console.log(`${industryName}, ${cityName}, ${amt}`) + + // Sanitize amt + let sanitizedAmt = amt.replace(/\s+/g, ''); + sanitizedAmt = sanitizedAmt.replace(/[^-()\d/*+.MAX]/g, ''); + let temp = sanitizedAmt.replace(/MAX/g, '1'); + try { + temp = eval(temp); + } catch(e) { + dialogBoxCreate("Invalid expression entered for export amount: " + e); + return; + } + + const n = parseFloat(temp); + + if (n == null || isNaN(n) || n < 0) { + dialogBoxCreate("Invalid amount entered for export"); + return; + } + const exportObj = {ind:industryName, city:cityName, amt:sanitizedAmt}; + console.log(exportObj); + props.mat.exp.push(exportObj); + removePopup(props.popupId); + } + + function removeExport(exp: Export): void { + for (let i = 0; i < props.mat.exp.length; ++i) { + if(props.mat.exp[i].ind !== exp.ind || + props.mat.exp[i].city !== exp.city || + props.mat.exp[i].amt !== exp.amt) continue; + props.mat.exp.splice(i, 1); + break + } + rerender(); + } + + const currentDivision = props.corp.divisions.find((division: IIndustry) => division.name === industry); + + return (<> +

+Select the industry and city to export this material to, as well as +how much of this material to export per second. You can set the export +amount to 'MAX' to export all of the materials in this warehouse. +

+ + + + +

+Below is a list of all current exports of this material from this warehouse. +Clicking on one of the exports below will REMOVE that export. +

+ { + props.mat.exp.map((exp: Export, index: number) =>
removeExport(exp)}> + Industry: {exp.ind}
+ City: {exp.city}
+ Amount/s: {exp.amt} +
) + } + ); +} diff --git a/src/Corporation/ui/FindInvestorsPopup.tsx b/src/Corporation/ui/FindInvestorsPopup.tsx new file mode 100644 index 000000000..3c52331ba --- /dev/null +++ b/src/Corporation/ui/FindInvestorsPopup.tsx @@ -0,0 +1,59 @@ +import React from 'react'; +import { removePopup } from "../../ui/React/createPopup"; +import { numeralWrapper } from "../../ui/numeralFormat"; +import { CorporationConstants } from "../data/Constants"; +import { ICorporation } from "../ICorporation"; +import { IPlayer } from "../../PersonObjects/IPlayer"; + +interface IProps { + corp: ICorporation; + popupId: string; + player: IPlayer; +} + +// Create a popup that lets the player manage exports +export function FindInvestorsPopup(props: IProps): React.ReactElement { + const val = props.corp.determineValuation() + let percShares = 0; + let roundMultiplier = 4; + switch (props.corp.fundingRound) { + case 0: //Seed + percShares = 0.10; + roundMultiplier = 4; + break; + case 1: //Series A + percShares = 0.35; + roundMultiplier = 3; + break; + case 2: //Series B + percShares = 0.25; + roundMultiplier = 3; + break; + case 3: //Series C + percShares = 0.20; + roundMultiplier = 2.5; + break; + default: + return (<>); + } + const funding = val * percShares * roundMultiplier; + const investShares = Math.floor(CorporationConstants.INITIALSHARES * percShares); + + function findInvestors(): void { + props.corp.fundingRound++; + props.corp.addFunds(funding); + props.corp.numShares -= investShares; + props.corp.rerender(props.player); + removePopup(props.popupId); + } + return (<> +

+ An investment firm has offered you {numeralWrapper.formatMoney(funding)} in + funding in exchange for a {numeralWrapper.format(percShares*100, "0.000a")}% + stake in the company ({numeralWrapper.format(investShares, '0.000a')} shares).

+ Do you accept or reject this offer?

+ Hint: Investment firms will offer more money if your corporation is turning a profit +

+ + ); +} diff --git a/src/Corporation/ui/GoPublicPopup.tsx b/src/Corporation/ui/GoPublicPopup.tsx new file mode 100644 index 000000000..305c502d4 --- /dev/null +++ b/src/Corporation/ui/GoPublicPopup.tsx @@ -0,0 +1,63 @@ +import React, { useState } from 'react'; +import { dialogBoxCreate } from "../../../utils/DialogBox"; +import { removePopup } from "../../ui/React/createPopup"; +import { numeralWrapper } from "../../ui/numeralFormat"; +import { ICorporation } from "../ICorporation"; +import { IPlayer } from "../../PersonObjects/IPlayer"; + +interface IProps { + corp: ICorporation; + popupId: string; + player: IPlayer; +} + +// Create a popup that lets the player manage exports +export function GoPublicPopup(props: IProps): React.ReactElement { + const [shares, setShares] = useState(''); + const initialSharePrice = props.corp.determineValuation() / (props.corp.totalShares); + + function goPublic(): void { + const numShares = parseFloat(shares); + const initialSharePrice = props.corp.determineValuation() / (props.corp.totalShares); + if (isNaN(numShares)) { + dialogBoxCreate("Invalid value for number of issued shares"); + return; + } + if (numShares > props.corp.numShares) { + dialogBoxCreate("Error: You don't have that many shares to issue!"); + return; + } + props.corp.public = true; + props.corp.sharePrice = initialSharePrice; + props.corp.issuedShares = numShares; + props.corp.numShares -= numShares; + props.corp.addFunds(numShares * initialSharePrice); + props.corp.rerender(props.player); + dialogBoxCreate(`You took your ${props.corp.name} public and earned ` + + `${numeralWrapper.formatMoney(numShares * initialSharePrice)} in your IPO`); + removePopup(props.popupId); + } + + function onKeyDown(event: React.KeyboardEvent): void { + if (event.keyCode === 13) goPublic(); + } + + function onChange(event: React.ChangeEvent): void { + setShares(event.target.value); + } + + return (<> +

+ Enter the number of shares you would like to issue + for your IPO. These shares will be publicly sold + and you will no longer own them. Your Corporation will + receive {numeralWrapper.formatMoney(initialSharePrice)} per share + (the IPO money will be deposited directly into your Corporation's funds). +

+ You have a total of {numeralWrapper.format(props.corp.numShares, "0.000a")} of + shares that you can issue. +

+ + + ); +} \ No newline at end of file diff --git a/src/Corporation/ui/HeaderTab.tsx b/src/Corporation/ui/HeaderTab.tsx new file mode 100644 index 000000000..d31f34271 --- /dev/null +++ b/src/Corporation/ui/HeaderTab.tsx @@ -0,0 +1,20 @@ +import React from 'react'; + +interface IProps { + current: boolean; + text: string; + onClick: () => void; +} + +export function HeaderTab(props: IProps): React.ReactElement { + let className = "cmpy-mgmt-header-tab"; + if (props.current) { + className += " current"; + } + + return ( + + ) +} \ No newline at end of file diff --git a/src/Corporation/ui/HeaderTabs.jsx b/src/Corporation/ui/HeaderTabs.jsx deleted file mode 100644 index c2d895d7d..000000000 --- a/src/Corporation/ui/HeaderTabs.jsx +++ /dev/null @@ -1,78 +0,0 @@ -// React Components for the Corporation UI's navigation tabs -// These are the tabs at the top of the UI that let you switch to different -// divisions, see an overview of your corporation, or create a new industry -import React from "react"; -import { BaseReactComponent } from "./BaseReactComponent"; - -function HeaderTab(props) { - let className = "cmpy-mgmt-header-tab"; - if (props.current) { - className += " current"; - } - - return ( - - ) -} - -export class HeaderTabs extends BaseReactComponent { - renderTab(props) { - return ( - - ) - } - - render() { - const overviewOnClick = () => { - this.routing().routeToOverviewPage(); - this.corp().rerender(); - } - - const divisionOnClicks = {}; - for (const division of this.corp().divisions) { - const name = division.name; - const onClick = () => { - this.routing().routeTo(name); - this.corp().rerender(); - } - - divisionOnClicks[name] = onClick; - } - - return ( -
- { - this.renderTab({ - current: this.routing().isOnOverviewPage(), - key: "overview", - onClick: overviewOnClick, - text: this.corp().name, - }) - } - { - this.corp().divisions.map((division) => { - return this.renderTab({ - current: this.routing().isOn(division.name), - key: division.name, - onClick: divisionOnClicks[division.name], - text: division.name, - }); - }) - } - { - this.renderTab({ - onClick: this.eventHandler().createNewIndustryPopup.bind(this.eventHandler()), - text: "Expand into new Industry", - }) - } -
- ) - } -} diff --git a/src/Corporation/ui/HeaderTabs.tsx b/src/Corporation/ui/HeaderTabs.tsx new file mode 100644 index 000000000..67b368977 --- /dev/null +++ b/src/Corporation/ui/HeaderTabs.tsx @@ -0,0 +1,61 @@ +// React Components for the Corporation UI's navigation tabs +// These are the tabs at the top of the UI that let you switch to different +// divisions, see an overview of your corporation, or create a new industry +import React from "react"; +import { HeaderTab } from "./HeaderTab"; +import { IIndustry } from "../IIndustry"; +import { NewIndustryPopup } from "./NewIndustryPopup"; +import { createPopup } from "../../ui/React/createPopup"; +import { ICorporation } from "../ICorporation"; +import { CorporationRouting } from "./Routing"; +import { IPlayer } from "../../PersonObjects/IPlayer"; + +interface IProps { + corp: ICorporation; + routing: CorporationRouting; + player: IPlayer; +} + +export function HeaderTabs(props: IProps): React.ReactElement { + function overviewOnClick(): void { + props.routing.routeToOverviewPage(); + props.corp.rerender(props.player); + } + + function openNewIndustryPopup(): void { + const popupId = "cmpy-mgmt-expand-industry-popup"; + createPopup(popupId, NewIndustryPopup, { + corp: props.corp, + routing: props.routing, + popupId: popupId, + }); + } + + return ( +
+ + { + props.corp.divisions.map((division: IIndustry) => { + props.routing.routeTo(division.name); + props.corp.rerender(props.player); + }} + text={division.name} + />) + } + +
+ ) + +} diff --git a/src/Corporation/ui/HireEmployeePopup.tsx b/src/Corporation/ui/HireEmployeePopup.tsx new file mode 100644 index 000000000..e4571d220 --- /dev/null +++ b/src/Corporation/ui/HireEmployeePopup.tsx @@ -0,0 +1,137 @@ +import React, { useState } from 'react'; +import { createPopup, removePopup } from "../../ui/React/createPopup"; +import { numeralWrapper } from "../../ui/numeralFormat"; +import { CorporationConstants } from "../data/Constants"; +import { ICorporation } from "../ICorporation"; +import { OfficeSpace } from "../OfficeSpace"; +import { IPlayer } from "../../PersonObjects/IPlayer"; +import { getRandomInt } from "../../../utils/helpers/getRandomInt"; +import { formatNumber } from "../../../utils/StringHelperFunctions"; +import { Employee } from "../Employee"; +import { dialogBoxCreate } from "../../../utils/DialogBox"; + +interface INameEmployeeProps { + office: OfficeSpace; + corp: ICorporation; + popupId: string; + employee: Employee; + player: IPlayer; +} + +function NameEmployeePopup(props: INameEmployeeProps): React.ReactElement { + const [name, setName] = useState(''); + function nameEmployee(): void { + for (let i = 0; i < props.office.employees.length; ++i) { + if (props.office.employees[i].name === name) { + dialogBoxCreate("You already have an employee with this nickname!"); + return; + } + } + props.employee.name = name; + props.office.employees.push(props.employee); + props.corp.rerender(props.player); + removePopup(props.popupId); + } + + function onKeyDown(event: React.KeyboardEvent): void { + if (event.keyCode === 13) nameEmployee(); + } + + function onChange(event: React.ChangeEvent): void { + setName(event.target.value); + } + + return (<> +

Give your employee a nickname!

+ + + ); +} + +interface IHireEmployeeProps { + employee: Employee; + office: OfficeSpace; + popupId: string; + player: IPlayer; + corp: ICorporation; +} + +function HireEmployeeButton(props: IHireEmployeeProps): React.ReactElement { + function hire(): void { + const popupId = "cmpy-mgmt-name-employee-popup"; + createPopup(popupId, NameEmployeePopup, { + office: props.office, + corp: props.corp, + popupId: popupId, + player: props.player, + employee: props.employee, + }); + removePopup(props.popupId); + } + + return (
+ Intelligence: {formatNumber(props.employee.int, 1)}
+ Charisma: {formatNumber(props.employee.cha, 1)}
+ Experience: {formatNumber(props.employee.exp, 1)}
+ Creativity: {formatNumber(props.employee.cre, 1)}
+ Efficiency: {formatNumber(props.employee.eff, 1)}
+ Salary: {numeralWrapper.formatMoney(props.employee.sal)} \ s
+
); +} + +interface IProps { + office: OfficeSpace; + corp: ICorporation; + popupId: string; + player: IPlayer; +} + +// Create a popup that lets the player manage exports +export function HireEmployeePopup(props: IProps): React.ReactElement { + if (props.office.atCapacity()) return (<>); + + //Generate three random employees (meh, decent, amazing) + const mult1 = getRandomInt(25, 50)/100; + const mult2 = getRandomInt(51, 75)/100; + const mult3 = getRandomInt(76, 100)/100; + const int = getRandomInt(50, 100); + const cha = getRandomInt(50, 100); + const exp = getRandomInt(50, 100); + const cre = getRandomInt(50, 100); + const eff = getRandomInt(50, 100); + const sal = CorporationConstants.EmployeeSalaryMultiplier * (int + cha + exp + cre + eff); + + const emp1 = new Employee({ + intelligence: int * mult1, + charisma: cha * mult1, + experience: exp * mult1, + creativity: cre * mult1, + efficiency: eff * mult1, + salary: sal * mult1, + }); + + const emp2 = new Employee({ + intelligence: int * mult2, + charisma: cha * mult2, + experience: exp * mult2, + creativity: cre * mult2, + efficiency: eff * mult2, + salary: sal * mult2, + }); + + const emp3 = new Employee({ + intelligence: int * mult3, + charisma: cha * mult3, + experience: exp * mult3, + creativity: cre * mult3, + efficiency: eff * mult3, + salary: sal * mult3, + }); + + return (<> +

Select one of the following candidates for hire:

+ + + + ); +} diff --git a/src/Corporation/ui/Industry.jsx b/src/Corporation/ui/Industry.jsx deleted file mode 100644 index c6787e3ed..000000000 --- a/src/Corporation/ui/Industry.jsx +++ /dev/null @@ -1,34 +0,0 @@ -// React Component for managing the Corporation's Industry UI -// This Industry component does NOT include the city tabs at the top -import React from "react"; -import { BaseReactComponent } from "./BaseReactComponent"; - -import { IndustryOffice } from "./IndustryOffice"; -import { IndustryOverview } from "./IndustryOverview"; -import { IndustryWarehouse } from "./IndustryWarehouse"; - -export class Industry extends BaseReactComponent { - constructor(props) { - if (props.currentCity == null) { - throw new Error(`Industry component constructed without 'city' prop`); - } - - super(props); - } - - render() { - return ( -
-
- - -
- -
- -
-
- ) - - } -} diff --git a/src/Corporation/ui/Industry.tsx b/src/Corporation/ui/Industry.tsx new file mode 100644 index 000000000..c96596212 --- /dev/null +++ b/src/Corporation/ui/Industry.tsx @@ -0,0 +1,43 @@ +// React Component for managing the Corporation's Industry UI +// This Industry component does NOT include the city tabs at the top +import React from "react"; + +import { IndustryOffice } from "./IndustryOffice"; +import { IndustryOverview } from "./IndustryOverview"; +import { IndustryWarehouse } from "./IndustryWarehouse"; +import { ICorporation } from "../ICorporation"; +import { CorporationRouting } from "./Routing"; +import { IPlayer } from "../../PersonObjects/IPlayer"; + +interface IProps { + routing: CorporationRouting; + corp: ICorporation; + currentCity: string; + player: IPlayer; +} + +export function Industry(props: IProps): React.ReactElement { + return ( +
+
+ + +
+
+ +
+
+ ) +} diff --git a/src/Corporation/ui/IndustryOffice.jsx b/src/Corporation/ui/IndustryOffice.tsx similarity index 53% rename from src/Corporation/ui/IndustryOffice.jsx rename to src/Corporation/ui/IndustryOffice.tsx index d43cadf22..fa09e3a12 100644 --- a/src/Corporation/ui/IndustryOffice.jsx +++ b/src/Corporation/ui/IndustryOffice.tsx @@ -1,120 +1,145 @@ // React Component for displaying an Industry's OfficeSpace information // (bottom-left panel in the Industry UI) -import React from "react"; -import { BaseReactComponent } from "./BaseReactComponent"; +import React, { useState } from "react"; -import { OfficeSpace } from "../Corporation"; +import { OfficeSpace } from "../OfficeSpace"; +import { Employee } from "../Employee"; import { EmployeePositions } from "../EmployeePositions"; import { numeralWrapper } from "../../ui/numeralFormat"; import { getSelectText } from "../../../utils/uiHelpers/getSelectData"; +import { createPopup } from "../../ui/React/createPopup"; +import { UpgradeOfficeSizePopup } from "./UpgradeOfficeSizePopup"; +import { HireEmployeePopup } from "./HireEmployeePopup"; +import { ThrowPartyPopup } from "./ThrowPartyPopup"; +import { ICorporation } from "../ICorporation"; +import { IPlayer } from "../../PersonObjects/IPlayer"; +import { CorporationRouting } from "./Routing"; -export class IndustryOffice extends BaseReactComponent { - constructor(props) { - super(props); +interface IProps { + routing: CorporationRouting; + corp: ICorporation; + currentCity: string; + player: IPlayer; +} - this.state = { - city: "", - division: "", - employeeManualAssignMode: false, - employee: null, // Reference to employee being referenced if in Manual Mode - numEmployees: 0, - numOperations: 0, - numEngineers: 0, - numBusiness: 0, - numManagement: 0, - numResearch: 0, - numUnassigned: 0, - numTraining: 0, - } +export function IndustryOffice(props: IProps): React.ReactElement { + const [employeeManualAssignMode, setEmployeeManualAssignMode] = useState(false); + const [city, setCity] = useState(""); + const [divisionName, setDivisionName] = useState(""); + const [employee, setEmployee] = useState(null); + const [numEmployees, setNumEmployees] = useState(0); + const [numOperations, setNumOperations] = useState(0); + const [numEngineers, setNumEngineers] = useState(0); + const [numBusiness, setNumBusiness] = useState(0); + const [numManagement, setNumManagement] = useState(0); + const [numResearch, setNumResearch] = useState(0); + const [numUnassigned, setNumUnassigned] = useState(0); + const [numTraining, setNumTraining] = useState(0); - this.updateEmployeeCount(); // This function validates division and office refs + function resetEmployeeCount(): void { + setNumEmployees(0); + setNumOperations(0); + setNumEngineers(0); + setNumBusiness(0); + setNumManagement(0); + setNumResearch(0); + setNumUnassigned(0); + setNumTraining(0); } - resetEmployeeCount() { - this.state.numEmployees = 0; - this.state.numOperations = 0; - this.state.numEngineers = 0; - this.state.numBusiness = 0; - this.state.numManagement = 0; - this.state.numResearch = 0; - this.state.numUnassigned = 0; - this.state.numTraining = 0; - } - - updateEmployeeCount() { - const division = this.routing().currentDivision; + function updateEmployeeCount(): void { + const division = props.routing.currentDivision; if (division == null) { throw new Error(`Routing does not hold reference to the current Industry`); } - const office = division.offices[this.props.currentCity]; + const office = division.offices[props.currentCity]; if (!(office instanceof OfficeSpace)) { - throw new Error(`Current City (${this.props.currentCity}) for UI does not have an OfficeSpace object`); + throw new Error(`Current City (${props.currentCity}) for UI does not have an OfficeSpace object`); } // If we're in a new city, we have to reset the state - if (division.name !== this.state.division || this.props.currentCity !== this.state.city) { - this.resetEmployeeCount(); - this.state.division = division.name; - this.state.city = this.props.currentCity; + if (division.name !== divisionName || props.currentCity !== city) { + resetEmployeeCount(); + setDivisionName(division.name); + setCity(props.currentCity); } - // Calculate how many NEW emplyoees we need to account for + // Calculate how many NEW employees we need to account for const currentNumEmployees = office.employees.length; + let newOperations = numOperations; + let newEngineers = numEngineers; + let newBusiness = numBusiness; + let newManagement = numManagement; + let newResearch = numResearch; + let newUnassigned = numUnassigned; + let newTraining = numTraining; + // Record the number of employees in each position, for NEW employees only - for (let i = this.state.numEmployees; i < office.employees.length; ++i) { + for (let i = numEmployees; i < office.employees.length; ++i) { switch (office.employees[i].pos) { case EmployeePositions.Operations: - ++this.state.numOperations; + newOperations++; break; case EmployeePositions.Engineer: - ++this.state.numEngineers; + newEngineers++; break; case EmployeePositions.Business: - ++this.state.numBusiness; + newBusiness++; break; case EmployeePositions.Management: - ++this.state.numManagement; + newManagement++; break; case EmployeePositions.RandD: - ++this.state.numResearch; + newResearch++; break; case EmployeePositions.Unassigned: - ++this.state.numUnassigned; + newUnassigned++; break; case EmployeePositions.Training: - ++this.state.numTraining; + newTraining++; break; default: console.error("Unrecognized employee position: " + office.employees[i].pos); break; } } + if(newOperations !== numOperations) setNumOperations(newOperations); + if(newEngineers !== numEngineers) setNumEngineers(newEngineers); + if(newBusiness !== numBusiness) setNumBusiness(newBusiness); + if(newManagement !== numManagement) setNumManagement(newManagement); + if(newResearch !== numResearch) setNumResearch(newResearch); + if(newUnassigned !== numUnassigned) setNumUnassigned(newUnassigned); + if(newTraining !== numTraining) setNumTraining(newTraining); - this.state.numEmployees = currentNumEmployees; + if(currentNumEmployees !== numEmployees) setNumEmployees(currentNumEmployees); } - // Renders the "Employee Management" section of the Office UI - renderEmployeeManagement() { - this.updateEmployeeCount(); + updateEmployeeCount(); - if (this.state.employeeManualAssignMode) { - return this.renderManualEmployeeManagement(); + // Renders the "Employee Management" section of the Office UI + function renderEmployeeManagement(): React.ReactElement { + updateEmployeeCount(); + + if (employeeManualAssignMode) { + return renderManualEmployeeManagement(); } else { - return this.renderAutomaticEmployeeManagement(); + return renderAutomaticEmployeeManagement(); } } - renderAutomaticEmployeeManagement() { - const division = this.routing().currentDivision; // Validated in constructor - const office = division.offices[this.props.currentCity]; // Validated in constructor - const vechain = (this.corp().unlockUpgrades[4] === 1); // Has Vechain upgrade + function renderAutomaticEmployeeManagement(): React.ReactElement { + const division = props.routing.currentDivision; // Validated in constructor + if(division === null) return(<>); + const office = division.offices[props.currentCity]; // Validated in constructor + if(office === 0) return (<>); + const vechain = (props.corp.unlockUpgrades[4] === 1); // Has Vechain upgrade - const switchModeOnClick = () => { - this.state.employeeManualAssignMode = true; - this.corp().rerender(); + function switchModeOnClick(): void { + setEmployeeManualAssignMode(true); + props.corp.rerender(props.player); } // Calculate average morale, happiness, and energy. Also salary @@ -135,87 +160,91 @@ export class IndustryOffice extends BaseReactComponent { } // Helper functions for (re-)assigning employees to different positions - const assignEmployee = (to) => { - if (this.state.numUnassigned <= 0) { + function assignEmployee(to: string): void { + if(office === 0) return; + if(division === null) return; + if (numUnassigned <= 0) { console.warn("Cannot assign employee. No unassigned employees available"); return; } switch (to) { case EmployeePositions.Operations: - ++this.state.numOperations; + setNumOperations(n => n+1); break; case EmployeePositions.Engineer: - ++this.state.numEngineers; + setNumEngineers(n => n+1); break; case EmployeePositions.Business: - ++this.state.numBusiness; + setNumBusiness(n => n+1); break; case EmployeePositions.Management: - ++this.state.numManagement; + setNumManagement(n => n+1); break; case EmployeePositions.RandD: - ++this.state.numResearch; + setNumResearch(n => n+1); break; case EmployeePositions.Unassigned: - ++this.state.numUnassigned; + setNumUnassigned(n => n+1); break; case EmployeePositions.Training: - ++this.state.numTraining; + setNumTraining(n => n+1); break; default: console.error("Unrecognized employee position: " + to); break; } - --this.state.numUnassigned; + setNumUnassigned(n => n-1); office.assignEmployeeToJob(to); - office.calculateEmployeeProductivity({ corporation: this.corp(), industry:division }); - this.corp().rerender(); + office.calculateEmployeeProductivity(props.corp, division); + props.corp.rerender(props.player); } - const unassignEmployee = (from) => { - function logWarning(pos) { + function unassignEmployee(from: string): void { + if(office === 0) return; + if(division === null) return; + function logWarning(pos: string): void { console.warn(`Cannot unassign from ${pos} because there is nobody assigned to that position`); } switch (from) { case EmployeePositions.Operations: - if (this.state.numOperations <= 0) { return logWarning(EmployeePositions.Operations); } - --this.state.numOperations; + if (numOperations <= 0) { return logWarning(EmployeePositions.Operations); } + setNumOperations(n => n-1); break; case EmployeePositions.Engineer: - if (this.state.numEngineers <= 0) { return logWarning(EmployeePositions.Operations); } - --this.state.numEngineers; + if (numEngineers <= 0) { return logWarning(EmployeePositions.Operations); } + setNumEngineers(n => n-1); break; case EmployeePositions.Business: - if (this.state.numBusiness <= 0) { return logWarning(EmployeePositions.Operations); } - --this.state.numBusiness; + if (numBusiness <= 0) { return logWarning(EmployeePositions.Operations); } + setNumBusiness(n => n-1); break; case EmployeePositions.Management: - if (this.state.numManagement <= 0) { return logWarning(EmployeePositions.Operations); } - --this.state.numManagement; + if (numManagement <= 0) { return logWarning(EmployeePositions.Operations); } + setNumManagement(n => n-1); break; case EmployeePositions.RandD: - if (this.state.numResearch <= 0) { return logWarning(EmployeePositions.Operations); } - --this.state.numResearch; + if (numResearch <= 0) { return logWarning(EmployeePositions.Operations); } + setNumResearch(n => n-1); break; case EmployeePositions.Unassigned: console.warn(`Tried to unassign from the Unassigned position`); break; case EmployeePositions.Training: - if (this.state.numTraining <= 0) { return logWarning(EmployeePositions.Operations); } - --this.state.numTraining; + if (numTraining <= 0) { return logWarning(EmployeePositions.Operations); } + setNumTraining(n => n-1); break; default: console.error("Unrecognized employee position: " + from); break; } - ++this.state.numUnassigned; + setNumUnassigned(n => n+1); office.unassignEmployeeFromJob(from); - office.calculateEmployeeProductivity({ corporation: this.corp(), industry:division }); - this.corp().rerender(); + office.calculateEmployeeProductivity(props.corp, division); + props.corp.rerender(props.player); } const positionHeaderStyle = { @@ -223,67 +252,67 @@ export class IndustryOffice extends BaseReactComponent { margin: "5px 0px 5px 0px", width: "50%", } - const assignButtonClass = this.state.numUnassigned > 0 ? "std-button" : "a-link-button-inactive"; + const assignButtonClass = numUnassigned > 0 ? "std-button" : "a-link-button-inactive"; - const operationAssignButtonOnClick = () => { + function operationAssignButtonOnClick(): void { assignEmployee(EmployeePositions.Operations); - this.corp().rerender(); + props.corp.rerender(props.player); } - const operationUnassignButtonOnClick = () => { + function operationUnassignButtonOnClick(): void { unassignEmployee(EmployeePositions.Operations); - this.corp().rerender(); + props.corp.rerender(props.player); } - const operationUnassignButtonClass = this.state.numOperations > 0 ? "std-button" : "a-link-button-inactive"; + const operationUnassignButtonClass = numOperations > 0 ? "std-button" : "a-link-button-inactive"; - const engineerAssignButtonOnClick = () => { + function engineerAssignButtonOnClick(): void { assignEmployee(EmployeePositions.Engineer); - this.corp().rerender(); + props.corp.rerender(props.player); } - const engineerUnassignButtonOnClick = () => { + function engineerUnassignButtonOnClick(): void { unassignEmployee(EmployeePositions.Engineer); - this.corp().rerender(); + props.corp.rerender(props.player); } - const engineerUnassignButtonClass = this.state.numEngineers > 0 ? "std-button" : "a-link-button-inactive"; + const engineerUnassignButtonClass = numEngineers > 0 ? "std-button" : "a-link-button-inactive"; - const businessAssignButtonOnClick = () => { + function businessAssignButtonOnClick(): void { assignEmployee(EmployeePositions.Business); - this.corp().rerender(); + props.corp.rerender(props.player); } - const businessUnassignButtonOnClick = () => { + function businessUnassignButtonOnClick(): void { unassignEmployee(EmployeePositions.Business); - this.corp().rerender(); + props.corp.rerender(props.player); } - const businessUnassignButtonClass = this.state.numBusiness > 0 ? "std-button" : "a-link-button-inactive"; + const businessUnassignButtonClass = numBusiness > 0 ? "std-button" : "a-link-button-inactive"; - const managementAssignButtonOnClick = () => { + function managementAssignButtonOnClick(): void { assignEmployee(EmployeePositions.Management); - this.corp().rerender(); + props.corp.rerender(props.player); } - const managementUnassignButtonOnClick = () => { + function managementUnassignButtonOnClick(): void { unassignEmployee(EmployeePositions.Management); - this.corp().rerender(); + props.corp.rerender(props.player); } - const managementUnassignButtonClass = this.state.numManagement > 0 ? "std-button" : "a-link-button-inactive"; + const managementUnassignButtonClass = numManagement > 0 ? "std-button" : "a-link-button-inactive"; - const rndAssignButtonOnClick = () => { + function rndAssignButtonOnClick(): void { assignEmployee(EmployeePositions.RandD); - this.corp().rerender(); + props.corp.rerender(props.player); } - const rndUnassignButtonOnClick = () => { + function rndUnassignButtonOnClick(): void { unassignEmployee(EmployeePositions.RandD); - this.corp().rerender(); + props.corp.rerender(props.player); } - const rndUnassignButtonClass = this.state.numResearch > 0 ? "std-button" : "a-link-button-inactive"; + const rndUnassignButtonClass = numResearch > 0 ? "std-button" : "a-link-button-inactive"; - const trainingAssignButtonOnClick = () => { + function trainingAssignButtonOnClick(): void { assignEmployee(EmployeePositions.Training); - this.corp().rerender(); + props.corp.rerender(props.player); } - const trainingUnassignButtonOnClick = () => { + function trainingUnassignButtonOnClick(): void { unassignEmployee(EmployeePositions.Training); - this.corp().rerender(); + props.corp.rerender(props.player); } - const trainingUnassignButtonClass = this.state.numTraining > 0 ? "std-button" : "a-link-button-inactive"; + const trainingUnassignButtonClass = numTraining > 0 ? "std-button" : "a-link-button-inactive"; return (
@@ -295,7 +324,7 @@ export class IndustryOffice extends BaseReactComponent { -

Unassigned Employees: {this.state.numUnassigned}

+

Unassigned Employees: {numUnassigned}


Avg Employee Morale: {numeralWrapper.format(avgMorale, "0.000")}

@@ -344,7 +373,7 @@ export class IndustryOffice extends BaseReactComponent { }

- {EmployeePositions.Operations} ({this.state.numOperations}) + {EmployeePositions.Operations} ({numOperations}) Manages supply chain operations. Improves the amount of Materials and Products you produce. @@ -354,7 +383,7 @@ export class IndustryOffice extends BaseReactComponent {

- {EmployeePositions.Engineer} ({this.state.numEngineers}) + {EmployeePositions.Engineer} ({numEngineers}) Develops and maintains products and production systems. Increases the quality of everything you produce. Also increases the amount you produce (not as much @@ -366,7 +395,7 @@ export class IndustryOffice extends BaseReactComponent {

- {EmployeePositions.Business} ({this.state.numBusiness}) + {EmployeePositions.Business} ({numBusiness}) Handles sales and finances. Improves the amount of Materials and Products you can sell. @@ -376,7 +405,7 @@ export class IndustryOffice extends BaseReactComponent {

- {EmployeePositions.Management} ({this.state.numManagement}) + {EmployeePositions.Management} ({numManagement}) Leads and oversees employees and office operations. Improves the effectiveness of Engineer and Operations employees @@ -387,7 +416,7 @@ export class IndustryOffice extends BaseReactComponent {

- {EmployeePositions.RandD} ({this.state.numResearch}) + {EmployeePositions.RandD} ({numResearch}) Research new innovative ways to improve the company. Generates Scientific Research @@ -397,7 +426,7 @@ export class IndustryOffice extends BaseReactComponent {

- {EmployeePositions.Training} ({this.state.numTraining}) + {EmployeePositions.Training} ({numTraining}) Set employee to training, which will increase some of their stats. Employees in training do not affect any company operations. @@ -408,14 +437,16 @@ export class IndustryOffice extends BaseReactComponent { ) } - renderManualEmployeeManagement() { - const corp = this.corp(); - const division = this.routing().currentDivision; // Validated in constructor - const office = division.offices[this.props.currentCity]; // Validated in constructor + function renderManualEmployeeManagement(): React.ReactElement { + const corp = props.corp; + const division = props.routing.currentDivision; // Validated in constructor + if(division === null) return (<>); + const office = division.offices[props.currentCity]; // Validated in constructor + if(office === 0) return (<>); - const switchModeOnClick = () => { - this.state.employeeManualAssignMode = false; - this.corp().rerender(); + function switchModeOnClick(): void { + setEmployeeManualAssignMode(false); + props.corp.rerender(props.player); } const employeeInfoDivStyle = { @@ -430,21 +461,22 @@ export class IndustryOffice extends BaseReactComponent { employees.push() } - const employeeSelectorOnChange = (e) => { + function employeeSelectorOnChange(e: React.ChangeEvent): void { + if(office === 0) return; const name = getSelectText(e.target); for (let i = 0; i < office.employees.length; ++i) { if (name === office.employees[i].name) { - this.state.employee = office.employees[i]; + setEmployee(office.employees[i]); break; } } - corp.rerender(); + corp.rerender(props.player); } // Employee Positions Selector - const emp = this.state.employee; - let employeePositionSelectorInitialValue = null; + const emp = employee; + let employeePositionSelectorInitialValue = ""; const employeePositions = []; const positionNames = Object.values(EmployeePositions); for (let i = 0; i < positionNames.length; ++i) { @@ -454,11 +486,12 @@ export class IndustryOffice extends BaseReactComponent { } } - const employeePositionSelectorOnChange = (e) => { + function employeePositionSelectorOnChange(e: React.ChangeEvent): void { + if(employee === null) return; const pos = getSelectText(e.target); - this.state.employee.pos = pos; - this.resetEmployeeCount(); - corp.rerender(); + employee.pos = pos; + resetEmployeeCount(); + corp.rerender(props.player); } // Numeraljs formatter @@ -486,29 +519,29 @@ export class IndustryOffice extends BaseReactComponent { {employees} { - this.state.employee != null && + employee != null &&

- Morale: {numeralWrapper.format(this.state.employee.mor, nf)} + Morale: {numeralWrapper.format(employee.mor, nf)}
- Happiness: {numeralWrapper.format(this.state.employee.hap, nf)} + Happiness: {numeralWrapper.format(employee.hap, nf)}
- Energy: {numeralWrapper.format(this.state.employee.ene, nf)} + Energy: {numeralWrapper.format(employee.ene, nf)}
Intelligence: {numeralWrapper.format(effInt, nf)}
Charisma: {numeralWrapper.format(effCha, nf)}
- Experience: {numeralWrapper.format(this.state.employee.exp, nf)} + Experience: {numeralWrapper.format(employee.exp, nf)}
Creativity: {numeralWrapper.format(effCre, nf)}
Efficiency: {numeralWrapper.format(effEff, nf)}
- Salary: {numeralWrapper.formatMoney(this.state.employee.sal)} + Salary: {numeralWrapper.formatMoney(employee.sal)}

} { - this.state.employee != null && + employee != null && @@ -518,89 +551,110 @@ export class IndustryOffice extends BaseReactComponent { ) } - render() { - const corp = this.corp(); - const division = this.routing().currentDivision; // Validated in constructor - const office = division.offices[this.props.currentCity]; // Validated in constructor - - const buttonStyle = { - fontSize: "13px", - } - - // Hire Employee button - let hireEmployeeButtonClass = "tooltip"; - if (office.atCapacity()) { - hireEmployeeButtonClass += " a-link-button-inactive"; - } else { - hireEmployeeButtonClass += " std-button"; - if (office.employees.length === 0) { - hireEmployeeButtonClass += " flashing-button"; - } - } - - const hireEmployeeButtonOnClick = () => { - office.findEmployees({ corporation: corp, industry: division }); - } - - // Autohire employee button - let autohireEmployeeButtonClass = "tooltip"; - if (office.atCapacity()) { - autohireEmployeeButtonClass += " a-link-button-inactive"; - } else { - autohireEmployeeButtonClass += " std-button"; - } - const autohireEmployeeButtonOnClick = () => { - if (office.atCapacity()) { return; } - office.hireRandomEmployee(); - this.corp().rerender(); - } - - // Upgrade Office Size Button - const upgradeOfficeSizeOnClick = this.eventHandler().createUpgradeOfficeSizePopup.bind(this.eventHandler(), office); - - // Throw Office Party - const throwOfficePartyOnClick = this.eventHandler().createThrowOfficePartyPopup.bind(this.eventHandler(), office); - - return ( -
-

Office Space

-

Size: {office.employees.length} / {office.size} employees

- - -
- - { - !division.hasResearch("AutoPartyManager") && - - } -
- - {this.renderEmployeeManagement()} -
- ) + const division = props.routing.currentDivision; // Validated in constructor + if(division === null) return (<>); + const office = division.offices[props.currentCity]; // Validated in constructor + if(office === 0) return (<>); + const buttonStyle = { + fontSize: "13px", } + + // Hire Employee button + let hireEmployeeButtonClass = "tooltip"; + if (office.atCapacity()) { + hireEmployeeButtonClass += " a-link-button-inactive"; + } else { + hireEmployeeButtonClass += " std-button"; + if (office.employees.length === 0) { + hireEmployeeButtonClass += " flashing-button"; + } + } + + function openHireEmployeePopup(): void { + if(office === 0) return; + const popupId = "cmpy-mgmt-hire-employee-popup"; + createPopup(popupId, HireEmployeePopup, { + office: office, + corp: props.corp, + popupId: popupId, + player: props.player, + }); + } + + // Autohire employee button + let autohireEmployeeButtonClass = "tooltip"; + if (office.atCapacity()) { + autohireEmployeeButtonClass += " a-link-button-inactive"; + } else { + autohireEmployeeButtonClass += " std-button"; + } + function autohireEmployeeButtonOnClick(): void { + if(office === 0) return; + if (office.atCapacity()) return; + office.hireRandomEmployee(); + props.corp.rerender(props.player); + } + + function openUpgradeOfficeSizePopup(): void { + if(office === 0) return; + const popupId = "cmpy-mgmt-upgrade-office-size-popup"; + createPopup(popupId, UpgradeOfficeSizePopup, { + office: office, + corp: props.corp, + popupId: popupId, + player: props.player, + }); + } + + function openThrowPartyPopup(): void { + if(office === 0) return; + const popupId = "cmpy-mgmt-throw-office-party-popup"; + createPopup(popupId, ThrowPartyPopup, { + office: office, + corp: props.corp, + popupId: popupId, + }); + } + + return ( +
+

Office Space

+

Size: {office.employees.length} / {office.size} employees

+ + +
+ + { + !division.hasResearch("AutoPartyManager") && + + } +
+ + {renderEmployeeManagement()} +
+ ) } diff --git a/src/Corporation/ui/IndustryOverview.jsx b/src/Corporation/ui/IndustryOverview.tsx similarity index 69% rename from src/Corporation/ui/IndustryOverview.jsx rename to src/Corporation/ui/IndustryOverview.tsx index 09cdc6a0a..f288f5dd5 100644 --- a/src/Corporation/ui/IndustryOverview.jsx +++ b/src/Corporation/ui/IndustryOverview.tsx @@ -1,20 +1,34 @@ // React Component for displaying an Industry's overview information // (top-left panel in the Industry UI) import React from "react"; -import { BaseReactComponent } from "./BaseReactComponent"; -import { OfficeSpace } from "../Corporation"; +import { OfficeSpace } from "../OfficeSpace"; import { Industries } from "../IndustryData"; import { IndustryUpgrades } from "../IndustryUpgrades"; import { numeralWrapper } from "../../ui/numeralFormat"; import { dialogBoxCreate } from "../../../utils/DialogBox"; import { createProgressBarText } from "../../../utils/helpers/createProgressBarText"; +import { MakeProductPopup } from "./MakeProductPopup"; +import { ResearchPopup } from "./ResearchPopup"; +import { createPopup } from "../../ui/React/createPopup"; +import { Money } from "../../ui/React/Money"; +import { ICorporation } from "../ICorporation"; +import { IPlayer } from "../../PersonObjects/IPlayer"; +import { CorporationRouting } from "./Routing"; -export class IndustryOverview extends BaseReactComponent { - renderMakeProductButton() { - const division = this.routing().currentDivision; // Validated inside render() +interface IProps { + routing: CorporationRouting; + corp: ICorporation; + currentCity: string; + player: IPlayer; +} - var createProductButtonText, createProductPopupText; +export function IndustryOverview(props: IProps): React.ReactElement { + function renderMakeProductButton(): React.ReactElement { + const division = props.routing.currentDivision; // Validated inside render() + if(division === null) return (<>); + let createProductButtonText = ""; + let createProductPopupText = ""; switch(division.type) { case Industries.Food: createProductButtonText = "Build Restaurant"; @@ -52,7 +66,7 @@ export class IndustryOverview extends BaseReactComponent { default: createProductButtonText = "Create Product"; createProductPopupText = "Create a new product!"; - return ""; + return (<>); } createProductPopupText += "

To begin developing a product, " + "first choose the city in which it will be designed. The stats of your employees " + @@ -65,14 +79,24 @@ export class IndustryOverview extends BaseReactComponent { const hasMaxProducts = division.hasMaximumNumberProducts(); const className = hasMaxProducts ? "a-link-button-inactive tooltip" : "std-button"; - const onClick = this.eventHandler().createMakeProductPopup.bind(this.eventHandler(), createProductPopupText, division); const buttonStyle = { margin: "6px", display: "inline-block", } + function openMakeProductPopup(): void { + if(division === null) return; + const popupId = "cmpy-mgmt-create-product-popup"; + createPopup(popupId, MakeProductPopup, { + popupText: createProductPopupText, + division: division, + corp: props.corp, + popupId: popupId, + }); + } + return ( -

) } - renderUpgrades() { - const corp = this.corp(); - const division = this.routing().currentDivision; // Validated inside render() - const office = division.offices[this.props.currentCity]; + function renderUpgrades(): React.ReactElement[] { + const corp = props.corp; + const division = props.routing.currentDivision; // Validated inside render() + if(division === null) return ([<>]); + const office = division.offices[props.currentCity]; if (!(office instanceof OfficeSpace)) { - throw new Error(`Current City (${this.props.currentCity}) for UI does not have an OfficeSpace object`); + throw new Error(`Current City (${props.currentCity}) for UI does not have an OfficeSpace object`); } const upgrades = []; @@ -213,7 +240,9 @@ export class IndustryOverview extends BaseReactComponent { break; } - const onClick = () => { + function onClick(): void { + if(office === 0) return; + if(division === null) return; if (corp.funds.lt(cost)) { dialogBoxCreate("Insufficient funds"); } else { @@ -223,13 +252,14 @@ export class IndustryOverview extends BaseReactComponent { office: office, }); // corp.displayDivisionContent(division, city); - corp.rerender(); + corp.rerender(props.player); } } - upgrades.push(this.renderUpgrade({ + upgrades.push(renderUpgrade({ + key: index, onClick: onClick, - text: `${upgrade[4]} - ${numeralWrapper.formatMoney(cost)}`, + text: <>{upgrade[4]} - , tooltip: upgrade[5], })); } @@ -237,9 +267,16 @@ export class IndustryOverview extends BaseReactComponent { return upgrades; } - renderUpgrade(props) { + interface IRenderUpgradeProps { + key: string; + onClick: () => void; + text: JSX.Element; + tooltip: string; + } + + function renderUpgrade(props: IRenderUpgradeProps): React.ReactElement { return ( -
+
{props.text} { props.tooltip != null && @@ -249,27 +286,25 @@ export class IndustryOverview extends BaseReactComponent { ) } - render() { - const division = this.routing().currentDivision; - if (division == null) { - throw new Error(`Routing does not hold reference to the current Industry`); - } - - const makeProductButton = this.renderMakeProductButton(); - - return ( -
- {this.renderText()} -
- - Purchases & Upgrades
- {this.renderUpgrades()}
- - { - division.makesProducts && - makeProductButton - } -
- ) + const division = props.routing.currentDivision; + if (division == null) { + throw new Error(`Routing does not hold reference to the current Industry`); } + + const makeProductButton = renderMakeProductButton(); + + return ( +
+ {renderText()} +
+ + Purchases & Upgrades
+ {renderUpgrades()}
+ + { + division.makesProducts && + makeProductButton + } +
+ ) } diff --git a/src/Corporation/ui/IndustryWarehouse.jsx b/src/Corporation/ui/IndustryWarehouse.tsx similarity index 69% rename from src/Corporation/ui/IndustryWarehouse.jsx rename to src/Corporation/ui/IndustryWarehouse.tsx index d0f220a56..cce46cefb 100644 --- a/src/Corporation/ui/IndustryWarehouse.jsx +++ b/src/Corporation/ui/IndustryWarehouse.tsx @@ -1,27 +1,47 @@ // React Component for displaying an Industry's warehouse information // (right-side panel in the Industry UI) import React from "react"; -import { BaseReactComponent } from "./BaseReactComponent"; -import { OfficeSpace, - WarehouseInitialCost, - WarehouseUpgradeBaseCost, - ProductProductionCostRatio } from "../Corporation"; +import { CorporationConstants } from "../data/Constants"; +import { OfficeSpace } from "../OfficeSpace"; import { Material } from "../Material"; import { Product } from "../Product"; import { Warehouse } from "../Warehouse"; +import { DiscontinueProductPopup } from "./DiscontinueProductPopup"; +import { ExportPopup } from "./ExportPopup"; +import { LimitProductProductionPopup } from "./LimitProductProductionPopup"; +import { MaterialMarketTaPopup } from "./MaterialMarketTaPopup"; +import { SellMaterialPopup } from "./SellMaterialPopup"; +import { SellProductPopup } from "./SellProductPopup"; +import { PurchaseMaterialPopup } from "./PurchaseMaterialPopup"; +import { ProductMarketTaPopup } from "./ProductMarketTaPopup"; import { numeralWrapper } from "../../ui/numeralFormat"; +import { dialogBoxCreate } from "../../../utils/DialogBox"; +import { createPopup } from "../../ui/React/createPopup"; import { isString } from "../../../utils/helpers/isString"; +import { ICorporation } from "../ICorporation"; +import { IIndustry } from "../IIndustry"; +import { CorporationRouting } from "./Routing"; +import { IPlayer } from "../../PersonObjects/IPlayer"; +import { SetSmartSupply } from "../Actions"; + + +interface IProductProps { + corp: ICorporation; + division: IIndustry; + city: string; + product: Product; + player: IPlayer; +} // Creates the UI for a single Product type -function ProductComponent(props) { +function ProductComponent(props: IProductProps): React.ReactElement { const corp = props.corp; const division = props.division; const city = props.city; const product = props.product; - const eventHandler = props.eventHandler; // Numeraljs formatters const nf = "0.000"; @@ -53,23 +73,53 @@ function ProductComponent(props) { if (isString(product.sCost)) { sellButtonText += (" @ " + product.sCost); } else { - sellButtonText += (" @ " + numeralWrapper.format(product.sCost, "$0.000a")); + sellButtonText += (" @ " + numeralWrapper.formatMoney(product.sCost as number)); } } - const sellButtonOnClick = eventHandler.createSellProductPopup.bind(eventHandler, product, city); + + function openSellProductPopup(): void { + const popupId = "cmpy-mgmt-limit-product-production-popup"; + createPopup(popupId, SellProductPopup, { + product: product, + city: city, + popupId: popupId, + }); + } // Limit Production button 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, division); + function openLimitProductProdutionPopup(): void { + const popupId = "cmpy-mgmt-limit-product-production-popup"; + createPopup(popupId, LimitProductProductionPopup, { + product: product, + city: city, + popupId: popupId, + }); + } - // Market TA button - const marketTaButtonOnClick = eventHandler.createProductMarketTaPopup.bind(eventHandler, product, division); + function openDiscontinueProductPopup(): void { + const popupId = "cmpy-mgmt-discontinue-product-popup"; + createPopup(popupId, DiscontinueProductPopup, { + product: product, + industry: division, + corp: props.corp, + popupId: popupId, + player: props.player, + }); + } + + function openProductMarketTaPopup(): void { + const popupId = "cmpy-mgmt-marketta-popup"; + createPopup(popupId, ProductMarketTaPopup, { + product: product, + industry: division, + popupId: popupId, + }); + } // Unfinished Product if (!product.fin) { @@ -81,18 +131,18 @@ function ProductComponent(props) {
-
- - { division.hasResearch("Market-TA.I") && - } @@ -146,7 +196,7 @@ function ProductComponent(props) {


- Est. Production Cost: {numeralWrapper.formatMoney(product.pCost / ProductProductionCostRatio)} + Est. Production Cost: {numeralWrapper.formatMoney(product.pCost / CorporationConstants.ProductProductionCostRatio)} An estimate of the material cost it takes to create this Product. @@ -161,18 +211,18 @@ function ProductComponent(props) {

-
- - { division.hasResearch("Market-TA.I") && - } @@ -181,14 +231,21 @@ function ProductComponent(props) { ) } +interface IMaterialProps { + corp: ICorporation; + division: IIndustry; + warehouse: Warehouse; + city: string; + mat: Material; +} + // Creates the UI for a single Material type -function MaterialComponent(props) { +function MaterialComponent(props: IMaterialProps): React.ReactElement { 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)) { @@ -210,10 +267,26 @@ function MaterialComponent(props) { // Purchase material button const purchaseButtonText = `Buy (${numeralWrapper.format(mat.buy, nfB)})`; const purchaseButtonClass = tutorial ? "std-button flashing-button tooltip" : "std-button"; - const purchaseButtonOnClick = eventHandler.createPurchaseMaterialPopup.bind(eventHandler, mat, division, warehouse); - // Export material button - const exportButtonOnClick = eventHandler.createExportMaterialPopup.bind(eventHandler, mat); + function openPurchaseMaterialPopup(): void { + const popupId = "cmpy-mgmt-material-purchase-popup"; + createPopup(popupId, PurchaseMaterialPopup, { + mat: mat, + industry: division, + warehouse: warehouse, + corp: props.corp, + popupId: popupId, + }); + } + + function openExportPopup(): void { + const popupId = "cmpy-mgmt-export-popup"; + createPopup(popupId, ExportPopup, { + mat: mat, + corp: props.corp, + popupId: popupId, + }); + } // Sell material button let sellButtonText; @@ -221,7 +294,7 @@ function MaterialComponent(props) { if (isString(mat.sllman[1])) { sellButtonText = `Sell (${numeralWrapper.format(mat.sll, nfB)}/${mat.sllman[1]})` } else { - sellButtonText = `Sell (${numeralWrapper.format(mat.sll, nfB)}/${numeralWrapper.format(mat.sllman[1], nfB)})`; + sellButtonText = `Sell (${numeralWrapper.format(mat.sll, nfB)}/${numeralWrapper.format(mat.sllman[1] as number, nfB)})`; } if (mat.marketTa2) { @@ -230,19 +303,34 @@ function MaterialComponent(props) { sellButtonText += " @ " + numeralWrapper.formatMoney(mat.bCost + markupLimit); } else if (mat.sCost) { if (isString(mat.sCost)) { - var sCost = mat.sCost.replace(/MP/g, mat.bCost); + const sCost = (mat.sCost as string).replace(/MP/g, mat.bCost+''); sellButtonText += " @ " + numeralWrapper.formatMoney(eval(sCost)); } else { - sellButtonText += " @ " + numeralWrapper.formatMoney(mat.sCost); + sellButtonText += " @ " + numeralWrapper.formatMoney(mat.sCost as number); } } } else { sellButtonText = "Sell (0.000/0.000)"; } - const sellButtonOnClick = eventHandler.createSellMaterialPopup.bind(eventHandler, mat); - // Market TA button - const marketTaButtonOnClick = eventHandler.createMaterialMarketTaPopup.bind(eventHandler, mat, division); + function openSellMaterialPopup(): void { + const popupId = "cmpy-mgmt-material-sell-popup"; + createPopup(popupId, SellMaterialPopup, { + mat: mat, + corp: props.corp, + popupId: popupId, + }); + } + + function openMaterialMarketTaPopup(): void { + const popupId = "cmpy-mgmt-export-popup"; + createPopup(popupId, MaterialMarketTaPopup, { + mat: mat, + industry: division, + corp: props.corp, + popupId: popupId, + }); + } return (
@@ -286,7 +374,7 @@ function MaterialComponent(props) {
- }
- { division.hasResearch("Market-TA.I") && - } @@ -320,10 +408,17 @@ function MaterialComponent(props) { ) } -export class IndustryWarehouse extends BaseReactComponent { +interface IProps { + corp: ICorporation; + routing: CorporationRouting; + currentCity: string; + player: IPlayer; +} + +export function IndustryWarehouse(props: IProps): React.ReactElement { // Returns a boolean indicating whether the given material is relevant for the // current industry. - isRelevantMaterial(matName, division) { + function isRelevantMaterial(matName: string, division: IIndustry): boolean { // Materials that affect Production multiplier const prodMultiplierMats = ["Hardware", "Robots", "AICores", "RealEstate"]; @@ -334,10 +429,12 @@ export class IndustryWarehouse extends BaseReactComponent { return false; } - renderWarehouseUI() { - const corp = this.corp(); - const division = this.routing().currentDivision; // Validated in render() - const warehouse = division.warehouses[this.props.currentCity]; // Validated in render() + function renderWarehouseUI(): React.ReactElement { + const corp = props.corp; + const division = props.routing.currentDivision; // Validated in render() + if(division === null) return (<>); + const warehouse = division.warehouses[props.currentCity]; // Validated in render() + if(warehouse === 0) return (<>); // General Storage information at the top const sizeUsageStyle = { @@ -346,15 +443,16 @@ export class IndustryWarehouse extends BaseReactComponent { } // Upgrade Warehouse size button - const sizeUpgradeCost = WarehouseUpgradeBaseCost * Math.pow(1.07, warehouse.level + 1); + const sizeUpgradeCost = CorporationConstants.WarehouseUpgradeBaseCost * Math.pow(1.07, warehouse.level + 1); const canAffordUpgrade = (corp.funds.gt(sizeUpgradeCost)); const upgradeWarehouseClass = canAffordUpgrade ? "std-button" : "a-link-button-inactive"; - const upgradeWarehouseOnClick = () => { + function upgradeWarehouseOnClick(): void { + if(division === null) return; + if(warehouse === 0) return; ++warehouse.level; warehouse.updateSize(corp, division); corp.funds = corp.funds.minus(sizeUpgradeCost); - corp.rerender(); - return; + corp.rerender(props.player); } // Industry material Requirements @@ -416,9 +514,10 @@ export class IndustryWarehouse extends BaseReactComponent { // Smart Supply Checkbox const smartSupplyCheckboxId = "cmpy-mgmt-smart-supply-checkbox"; - const smartSupplyOnChange = (e) => { - warehouse.smartSupplyEnabled = e.target.checked; - corp.rerender(); + function smartSupplyOnChange(e: React.ChangeEvent): void { + if(warehouse === 0) return; + SetSmartSupply(warehouse, e.target.checked); + corp.rerender(props.player); } // Create React components for materials @@ -426,12 +525,11 @@ export class IndustryWarehouse extends BaseReactComponent { for (const matName in warehouse.materials) { if (warehouse.materials[matName] instanceof Material) { // Only create UI for materials that are relevant for the industry - if (this.isRelevantMaterial(matName, division)) { + if (isRelevantMaterial(matName, division)) { mats.push(); @@ -443,15 +541,16 @@ export class IndustryWarehouse extends BaseReactComponent { const products = []; if (division.makesProducts && Object.keys(division.products).length > 0) { for (const productName in division.products) { - if (division.products[productName] instanceof Product) { + const product = division.products[productName]; + if (product instanceof Product) { products.push(); + product={product} + />); } } } @@ -500,25 +599,36 @@ export class IndustryWarehouse extends BaseReactComponent { ) } - render() { - const division = this.routing().currentDivision; - if (division == null) { - throw new Error(`Routing does not hold reference to the current Industry`); - } - const warehouse = division.warehouses[this.props.currentCity]; + const division = props.routing.currentDivision; + if (division == null) { + throw new Error(`Routing does not hold reference to the current Industry`); + } + const warehouse = division.warehouses[props.currentCity]; - const newWarehouseOnClick = this.eventHandler().purchaseWarehouse.bind(this.eventHandler(), division, this.props.currentCity); - - if (warehouse instanceof Warehouse) { - return this.renderWarehouseUI(); + function purchaseWarehouse(division: IIndustry, city: string): void { + if (props.corp.funds.lt(CorporationConstants.WarehouseInitialCost)) { + dialogBoxCreate("You do not have enough funds to do this!"); } else { - return ( -
- -
- ) + division.warehouses[city] = new Warehouse({ + corp: props.corp, + industry: division, + loc: city, + size: CorporationConstants.WarehouseInitialSize, + }); + props.corp.funds = props.corp.funds.minus(CorporationConstants.WarehouseInitialCost); + props.corp.rerender(props.player); } } + + if (warehouse instanceof Warehouse) { + return renderWarehouseUI(); + } else { + return ( +
+ +
+ ) + } } diff --git a/src/Corporation/ui/IssueDividendsPopup.tsx b/src/Corporation/ui/IssueDividendsPopup.tsx new file mode 100644 index 000000000..881af18fa --- /dev/null +++ b/src/Corporation/ui/IssueDividendsPopup.tsx @@ -0,0 +1,58 @@ +import React, { useState } from 'react'; +import { removePopup } from "../../ui/React/createPopup"; +import { dialogBoxCreate } from "../../../utils/DialogBox"; +import { CorporationConstants } from "../data/Constants"; +import { ICorporation } from "../ICorporation"; +import { IssueDividends } from "../Actions"; + +interface IProps { + popupId: string; + corp: ICorporation; +} + +// Create a popup that lets the player issue & manage dividends +// This is created when the player clicks the "Issue Dividends" button in the overview panel +export function IssueDividendsPopup(props: IProps): React.ReactElement { + const [percent, setPercent] = useState(null); + + function issueDividends(): void { + if(percent === null) return; + try { + IssueDividends(props.corp, percent/100); + } catch(err) { + dialogBoxCreate(err+''); + } + + removePopup(props.popupId); + } + + function onKeyDown(event: React.KeyboardEvent): void { + if (event.keyCode === 13) issueDividends(); + } + + function onChange(event: React.ChangeEvent): void { + if(event.target.value === "") setPercent(null); + else setPercent(parseFloat(event.target.value)); + } + + return (<> +

+Dividends are a distribution of a portion of the corporation's +profits to the shareholders. This includes yourself, as well.

+In order to issue dividends, simply allocate some percentage +of your corporation's profits to dividends. This percentage must be an +integer between 0 and {CorporationConstants.DividendMaxPercentage}. (A percentage of 0 means no dividends will be +issued

+Two important things to note:
+ * Issuing dividends will negatively affect your corporation's stock price
+ * Dividends are taxed. Taxes start at 50%, but can be decreased

+Example: Assume your corporation makes $100m / sec in profit and you allocate +40% of that towards dividends. That means your corporation will gain $60m / sec +in funds and the remaining $40m / sec will be paid as dividends. Since your +corporation starts with 1 billion shares, every shareholder will be paid $0.04 per share +per second before taxes. +

+ + + ); +} diff --git a/src/Corporation/ui/IssueNewSharesPopup.tsx b/src/Corporation/ui/IssueNewSharesPopup.tsx new file mode 100644 index 000000000..9591a15c7 --- /dev/null +++ b/src/Corporation/ui/IssueNewSharesPopup.tsx @@ -0,0 +1,134 @@ +import React, { useState } from 'react'; +import { numeralWrapper } from "../../ui/numeralFormat"; +import { dialogBoxCreate } from "../../../utils/DialogBox"; +import { removePopup } from "../../ui/React/createPopup"; +import { getRandomInt } from "../../../utils/helpers/getRandomInt"; +import { CorporationConstants } from "../data/Constants"; +import { ICorporation } from "../ICorporation"; + +interface IEffectTextProps { + corp: ICorporation; + shares: number | null; +} + +function EffectText(props: IEffectTextProps): React.ReactElement { + if(props.shares === null) return (<>); + const newSharePrice = Math.round(props.corp.sharePrice * 0.9); + const maxNewSharesUnrounded = Math.round(props.corp.totalShares * 0.2); + const maxNewShares = maxNewSharesUnrounded - (maxNewSharesUnrounded % 1e6); + let newShares = props.shares; + if (isNaN(newShares)) { + return (

Invalid input

); + } + + // Round to nearest ten-millionth + newShares /= 10e6; + newShares = Math.round(newShares) * 10e6; + + if (newShares < 10e6) { + return (

Must issue at least 10 million new shares

); + } + + if (newShares > maxNewShares) { + return (

You cannot issue that many shares

); + } + + return (

+ Issue ${numeralWrapper.format(newShares, "0.000a")} new + shares for {numeralWrapper.formatMoney(newShares * newSharePrice)}? +

); +} + +interface IProps { + corp: ICorporation; + popupId: string; +} + +// Create a popup that lets the player issue new shares +// This is created when the player clicks the "Issue New Shares" buttons in the overview panel +export function IssueNewSharesPopup(props: IProps): React.ReactElement { + const [shares, setShares] = useState(null); + const maxNewSharesUnrounded = Math.round(props.corp.totalShares * 0.2); + const maxNewShares = maxNewSharesUnrounded - (maxNewSharesUnrounded % 1e6); + + function issueNewShares(): void { + if(shares === null) return; + const newSharePrice = Math.round(props.corp.sharePrice * 0.9); + let newShares = shares; + if (isNaN(newShares)) { + dialogBoxCreate("Invalid input for number of new shares"); + return; + } + + // Round to nearest ten-millionth + newShares = Math.round(newShares / 10e6) * 10e6; + + if (newShares < 10e6 || newShares > maxNewShares) { + dialogBoxCreate("Invalid input for number of new shares"); + return; + } + + const profit = newShares * newSharePrice; + props.corp.issueNewSharesCooldown = CorporationConstants.IssueNewSharesCooldown; + props.corp.totalShares += newShares; + + // Determine how many are bought by private investors + // Private investors get up to 50% at most + // Round # of private shares to the nearest millionth + let privateShares = getRandomInt(0, Math.round(newShares / 2)); + privateShares = Math.round(privateShares / 1e6) * 1e6; + + props.corp.issuedShares += (newShares - privateShares); + props.corp.funds = props.corp.funds.plus(profit); + props.corp.immediatelyUpdateSharePrice(); + + removePopup(props.popupId); + dialogBoxCreate(`Issued ${numeralWrapper.format(newShares, "0.000a")} and raised ` + + `${numeralWrapper.formatMoney(profit)}. ${numeralWrapper.format(privateShares, "0.000a")} ` + + `of these shares were bought by private investors.

` + + `Stock price decreased to ${numeralWrapper.formatMoney(props.corp.sharePrice)}`); + } + + function onKeyDown(event: React.KeyboardEvent): void { + if (event.keyCode === 13) issueNewShares(); + } + + function onChange(event: React.ChangeEvent): void { + if(event.target.value === "") setShares(null); + else setShares(parseFloat(event.target.value)); + } + + return (<> +

+You can issue new equity shares (i.e. stocks) in order to raise +capital for your corporation.

+ * You can issue at most {numeralWrapper.formatMoney(maxNewShares)} new shares
+ * New shares are sold at a 10% discount
+ * You can only issue new shares once every 12 hours
+ * Issuing new shares causes dilution, resulting in a decrease in stock price and lower dividends per share
+ * Number of new shares issued must be a multiple of 10 million

+When you choose to issue new equity, private shareholders have first priority for up to 50% of the new shares. +If they choose to exercise this option, these newly issued shares become private, restricted shares, which means +you cannot buy them back. +

+ + + + ); + + + // let issueBtn, newSharesInput; + // const dynamicText = createElement("p", { + // display: "block", + // }); + + // function updateDynamicText(corp) { + + // } + + + + + // createPopup(popupId, [descText, dynamicText, newSharesInput, issueBtn, cancelBtn]); + // newSharesInput.focus(); +} diff --git a/src/Corporation/ui/LevelableUpgrade.jsx b/src/Corporation/ui/LevelableUpgrade.jsx deleted file mode 100644 index 937652d8f..000000000 --- a/src/Corporation/ui/LevelableUpgrade.jsx +++ /dev/null @@ -1,36 +0,0 @@ -// React components for the levelable upgrade buttons on the overview panel -import React from "react"; -import { BaseReactComponent } from "./BaseReactComponent"; - -import { numeralWrapper } from "../../ui/numeralFormat"; -import { dialogBoxCreate } from "../../../utils/DialogBox"; - -export class LevelableUpgrade extends BaseReactComponent { - render() { - const data = this.props.upgradeData; - const level = this.props.upgradeLevel; - - const baseCost = data[1]; - const priceMult = data[2]; - const cost = baseCost * Math.pow(priceMult, level); - - const text = `${data[4]} - ${numeralWrapper.formatMoney(cost)}` - const tooltip = data[5]; - const onClick = () => { - const corp = this.corp(); - if (corp.funds.lt(cost)) { - dialogBoxCreate("Insufficient funds"); - } else { - corp.upgrade(data); - corp.rerender(); - } - } - - return ( -
- {text} - {tooltip} -
- ) - } -} diff --git a/src/Corporation/ui/LevelableUpgrade.tsx b/src/Corporation/ui/LevelableUpgrade.tsx new file mode 100644 index 000000000..aedff3800 --- /dev/null +++ b/src/Corporation/ui/LevelableUpgrade.tsx @@ -0,0 +1,43 @@ +// React components for the levelable upgrade buttons on the overview panel +import React from "react"; + +import { numeralWrapper } from "../../ui/numeralFormat"; +import { dialogBoxCreate } from "../../../utils/DialogBox"; +import { ICorporation } from "../ICorporation"; +import { IPlayer } from "../../PersonObjects/IPlayer"; +import { CorporationUpgrade } from "../data/CorporationUpgrades"; +import { LevelUpgrade } from "../Actions"; +import { Money } from "../../ui/React/Money"; + +interface IProps { + upgrade: CorporationUpgrade; + corp: ICorporation; + player: IPlayer; +} + +export function LevelableUpgrade(props: IProps): React.ReactElement { + const data = props.upgrade; + const level = props.corp.upgrades[data[0]]; + + const baseCost = data[1]; + const priceMult = data[2]; + const cost = baseCost * Math.pow(priceMult, level); + + const text = <>{data[4]} - + const tooltip = data[5]; + function onClick(): void { + try { + LevelUpgrade(props.corp, props.upgrade); + } catch(err) { + dialogBoxCreate(err+''); + } + props.corp.rerender(props.player); + } + + return ( +
+ {text} + {tooltip} +
+ ) +} diff --git a/src/Corporation/ui/LimitProductProductionPopup.tsx b/src/Corporation/ui/LimitProductProductionPopup.tsx new file mode 100644 index 000000000..bf9c51f70 --- /dev/null +++ b/src/Corporation/ui/LimitProductProductionPopup.tsx @@ -0,0 +1,53 @@ +import React, { useState } from 'react'; +import { dialogBoxCreate } from "../../../utils/DialogBox"; +import { removePopup } from "../../ui/React/createPopup"; +import { Product } from "../Product"; + +interface IProps { + product: Product; + city: string; + popupId: string; +} + +// Create a popup that lets the player limit the production of a product +export function LimitProductProductionPopup(props: IProps): React.ReactElement { + const [limit, setLimit] = useState(null); + + function limitProductProduction(): void { + if (limit === null) { + props.product.prdman[props.city][0] = false; + removePopup(props.popupId); + return; + } + const qty = limit; + if (isNaN(qty)) { + dialogBoxCreate("Invalid value entered"); + return; + } + if (qty < 0) { + props.product.prdman[props.city][0] = false; + } else { + props.product.prdman[props.city][0] = true; + props.product.prdman[props.city][1] = qty; + } + removePopup(props.popupId); + } + + function onKeyDown(event: React.KeyboardEvent): void { + if (event.keyCode === 13) limitProductProduction(); + } + + function onChange(event: React.ChangeEvent): void { + if(event.target.value === "") setLimit(null); + else setLimit(parseFloat(event.target.value)); + } + + return (<> +

+Enter a limit to the amount of this product you would +like to product per second. Leave the box empty to set no limit. +

+ + + ); +} \ No newline at end of file diff --git a/src/Corporation/ui/MainPanel.jsx b/src/Corporation/ui/MainPanel.jsx deleted file mode 100644 index 3a632639c..000000000 --- a/src/Corporation/ui/MainPanel.jsx +++ /dev/null @@ -1,105 +0,0 @@ -// React Component for the element that contains the actual info/data -// for the Corporation UI. This panel lies below the header tabs and will -// be filled with whatever is needed based on the routing/navigation -import React from "react"; -import { BaseReactComponent } from "./BaseReactComponent"; - -import { CityTabs } from "./CityTabs"; -import { Industry } from "./Industry"; -import { Overview } from "./Overview"; - -import { OfficeSpace } from "../Corporation"; - -import { CityName } from "../../Locations/data/CityNames"; - -export class MainPanel extends BaseReactComponent { - constructor(props) { - super(props); - - this.state = { - division: "", - city: CityName.Sector12, - } - } - - // We can pass this setter to child components - changeCityState(newCity) { - if (Object.values(CityName).includes(newCity)) { - this.state.city = newCity; - } else { - console.error(`Tried to change MainPanel's city state to an invalid city: ${newCity}`); - } - } - - // Determines what UI content to render based on routing - renderContent() { - if (this.routing().isOnOverviewPage()) { - // Corporation overview Content - return this.renderOverviewPage(); - } else { - // Division content - - // First, check if we're at a new division. If so, we need to reset the city to Sector-12 - // Otherwise, just switch the 'city' state - const currentDivision = this.routing().current(); - if (currentDivision !== this.state.division) { - this.state.division = currentDivision; - this.state.city = CityName.Sector12; - } - - return this.renderDivisionPage(); - } - } - - renderOverviewPage() { - return ( -
- -
- ) - } - - renderDivisionPage() { - // Note: Division is the same thing as Industry...I wasn't consistent with naming - const division = this.routing().currentDivision; - if (division == null) { - throw new Error(`Routing does not hold reference to the current Industry`); - } - - // City tabs - const onClicks = {}; - for (const cityName in division.offices) { - if (division.offices[cityName] instanceof OfficeSpace) { - onClicks[cityName] = () => { - this.state.city = cityName; - this.corp().rerender(); - } - } - } - - const cityTabs = ( - - ) - - // Rest of Industry UI - const industry = ( - - ) - - return ( -
- {cityTabs} - {industry} -
- ) - } - - render() { - return this.renderContent(); - } -} diff --git a/src/Corporation/ui/MainPanel.tsx b/src/Corporation/ui/MainPanel.tsx new file mode 100644 index 000000000..aa353291a --- /dev/null +++ b/src/Corporation/ui/MainPanel.tsx @@ -0,0 +1,96 @@ +// React Component for the element that contains the actual info/data +// for the Corporation UI. This panel lies below the header tabs and will +// be filled with whatever is needed based on the routing/navigation +import React, { useState } from "react"; + +import { CityTabs } from "./CityTabs"; +import { Industry } from "./Industry"; +import { Overview } from "./Overview"; + +import { OfficeSpace } from "../OfficeSpace"; + +import { CityName } from "../../Locations/data/CityNames"; +import { IPlayer } from "../../PersonObjects/IPlayer"; +import { ICorporation } from "../ICorporation"; +import { CorporationRouting } from "./Routing"; + +interface IProps { + corp: ICorporation; + routing: CorporationRouting; + player: IPlayer; +} + +export function MainPanel(props: IProps): React.ReactElement { + const [division, setDivision] = useState(""); + const [city, setCity] = useState(CityName.Sector12); + + // We can pass this setter to child components + function changeCityState(newCity: string): void { + if (Object.values(CityName).includes(newCity as CityName)) { + setCity(newCity); + } else { + console.error(`Tried to change MainPanel's city state to an invalid city: ${newCity}`); + } + } + + function renderOverviewPage(): React.ReactElement { + return ( +
+ +
+ ) + } + + function renderDivisionPage(): React.ReactElement { + // Note: Division is the same thing as Industry...I wasn't consistent with naming + const division = props.routing.currentDivision; + if (division == null) { + throw new Error(`Routing does not hold reference to the current Industry`); + } + + // City tabs + const onClicks: { [key: string]: () => void } = {}; + for (const cityName in division.offices) { + if (division.offices[cityName] instanceof OfficeSpace) { + onClicks[cityName] = () => { + setCity(cityName); + props.corp.rerender(props.player); + } + } + } + + const cityTabs = ( + + ) + + return ( +
+ {cityTabs} + +
+ ) + } + + if (props.routing.isOnOverviewPage()) { + // Corporation overview Content + return renderOverviewPage(); + } else { + // Division content + + // First, check if we're at a new division. If so, we need to reset the city to Sector-12 + // Otherwise, just switch the 'city' state + const currentDivision = props.routing.current(); + if (currentDivision !== division) { + setDivision(currentDivision); + setCity(CityName.Sector12); + } + + return renderDivisionPage(); + } +} diff --git a/src/Corporation/ui/MakeProductPopup.tsx b/src/Corporation/ui/MakeProductPopup.tsx new file mode 100644 index 000000000..a3806cec1 --- /dev/null +++ b/src/Corporation/ui/MakeProductPopup.tsx @@ -0,0 +1,102 @@ +import React, { useState } from 'react'; +import { dialogBoxCreate } from "../../../utils/DialogBox"; +import { removePopup } from "../../ui/React/createPopup"; +import { Industries } from "../IndustryData"; +import { Product } from "../Product"; +import { ICorporation } from "../ICorporation"; +import { IIndustry } from "../IIndustry"; + +interface IProps { + popupText: string; + division: IIndustry; + corp: ICorporation; + popupId: string; +} + +function productPlaceholder(tpe: string): string { + if (tpe === Industries.Food) { + return "Restaurant Name"; + } else if (tpe === Industries.Healthcare) { + return "Hospital Name"; + } else if (tpe === Industries.RealEstate) { + return "Property Name"; + } + return "Product Name"; +} + +// Create a popup that lets the player create a product for their current industry +export function MakeProductPopup(props: IProps): React.ReactElement { + const allCities = Object.keys(props.division.offices) + .filter((cityName: string) => props.division.offices[cityName] !== 0); + const [city, setCity] = useState(allCities.length > 0 ? allCities[0] : ''); + const [name, setName] = useState(''); + const [design, setDesign] = useState(null); + const [marketing, setMarketing] = useState(null); + if (props.division.hasMaximumNumberProducts()) return (<>); + + function makeProduct(): void { + let designInvest = design; + let marketingInvest = marketing; + if (designInvest == null || designInvest < 0) { designInvest = 0; } + if (marketingInvest == null || marketingInvest < 0) { marketingInvest = 0; } + if (name == null || name === "") { + dialogBoxCreate("You must specify a name for your product!"); + } else if (isNaN(designInvest)) { + dialogBoxCreate("Invalid value for design investment"); + } else if (isNaN(marketingInvest)) { + dialogBoxCreate("Invalid value for marketing investment"); + } else if (props.corp.funds.lt(designInvest + marketingInvest)) { + dialogBoxCreate("You don't have enough company funds to make this large of an investment"); + } else { + const product = new Product({ + name: name.replace(/[<>]/g, ''), //Sanitize for HTMl elements + createCity: city, + designCost: designInvest, + advCost: marketingInvest, + }); + if (props.division.products[product.name] instanceof Product) { + dialogBoxCreate(`You already have a product with this name!`); + return; + } + props.corp.funds = props.corp.funds.minus(designInvest + marketingInvest); + props.division.products[product.name] = product; + removePopup(props.popupId); + } + } + + function onCityChange(event: React.ChangeEvent): void { + setCity(event.target.value); + } + + function onProductNameChange(event: React.ChangeEvent): void { + setName(event.target.value); + } + + function onDesignChange(event: React.ChangeEvent): void { + if(event.target.value === "") setDesign(null); + else setDesign(parseFloat(event.target.value)); + } + + function onMarketingChange(event: React.ChangeEvent): void { + if(event.target.value === "") setMarketing(null); + else setMarketing(parseFloat(event.target.value)); + } + + function onKeyDown(event: React.KeyboardEvent): void { + if (event.keyCode === 13) makeProduct(); + } + + return (<> +

+ + +
+ + + + ); +} diff --git a/src/Corporation/ui/MaterialMarketTaPopup.tsx b/src/Corporation/ui/MaterialMarketTaPopup.tsx new file mode 100644 index 000000000..94af5f78c --- /dev/null +++ b/src/Corporation/ui/MaterialMarketTaPopup.tsx @@ -0,0 +1,116 @@ +import React, { useState } from 'react'; +import { numeralWrapper } from "../../ui/numeralFormat"; +import { IIndustry } from "../IIndustry"; +import { ICorporation } from "../ICorporation"; +import { Material } from "../Material"; + +interface IMarketTA2Props { + industry: IIndustry; + mat: Material; +} + +function MarketTA2(props: IMarketTA2Props): React.ReactElement { + if(!props.industry.hasResearch("Market-TA.II")) return (<>); + + const [newCost, setNewCost] = useState(props.mat.bCost); + const setRerender = useState(false)[1]; + function rerender(): void { + setRerender(old => !old); + } + const markupLimit = props.mat.getMarkupLimit(); + + function onChange(event: React.ChangeEvent): void { + if(event.target.value === "") setNewCost(0); + else setNewCost(parseFloat(event.target.value)); + } + + const sCost = newCost; + let markup = 1; + if (sCost > props.mat.bCost) { + //Penalty if difference between sCost and bCost is greater than markup limit + if ((sCost - props.mat.bCost) > markupLimit) { + markup = Math.pow(markupLimit / (sCost - props.mat.bCost), 2); + } + } else if (sCost < props.mat.bCost) { + if (sCost <= 0) { + markup = 1e12; //Sell everything, essentially discard + } else { + //Lower prices than market increases sales + markup = props.mat.bCost / sCost; + } + } + + function onMarketTA2(event: React.ChangeEvent): void { + props.mat.marketTa2 = event.target.checked; + rerender(); + } + + return (<> +

+
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. +

+ +
+ + +
+

+ Note that Market-TA.II overrides Market-TA.I. This means that if + both are enabled, then Market-TA.II will take effect, not Market-TA.I +

+ ); +} + +interface IProps { + mat: Material; + industry: IIndustry; + corp: ICorporation; + popupId: string; +} + +// Create a popup that lets the player use the Market TA research for Materials +export function MaterialMarketTaPopup(props: IProps): React.ReactElement { + const setRerender = useState(false)[1]; + function rerender(): void { + setRerender(old => !old); + } + const markupLimit = props.mat.getMarkupLimit(); + + function onMarketTA1(event: React.ChangeEvent): void { + props.mat.marketTa1 = event.target.checked; + rerender(); + } + + return (<> +

+ Market-TA.I
+ The maximum sale price you can mark this up to + is {numeralWrapper.formatMoney(props.mat.bCost + markupLimit)}. + This means that if you set the sale price higher than this, + you will begin to experience a loss in number of sales +

+
+ + +
+ + ); + +} diff --git a/src/Corporation/ui/NewIndustryPopup.tsx b/src/Corporation/ui/NewIndustryPopup.tsx new file mode 100644 index 000000000..8fa4f0aff --- /dev/null +++ b/src/Corporation/ui/NewIndustryPopup.tsx @@ -0,0 +1,68 @@ +import React, { useState } from 'react'; +import { dialogBoxCreate } from "../../../utils/DialogBox"; +import { removePopup } from "../../ui/React/createPopup"; +import { + Industries, + IndustryStartingCosts, + IndustryDescriptions } from "../IndustryData"; +import { Industry } from "../Industry"; +import { ICorporation } from "../ICorporation"; +import { IIndustry } from "../IIndustry"; +import { CorporationRouting } from "./Routing"; +import { NewIndustry } from "../Actions"; + +interface IProps { + corp: ICorporation; + popupId: string; + routing: CorporationRouting; +} +// Create a popup that lets the player create a new industry. +// This is created when the player clicks the "Expand into new Industry" header tab +export function NewIndustryPopup(props: IProps): React.ReactElement { + const allIndustries = Object.keys(Industries).sort(); + const possibleIndustries = allIndustries.filter((industryType: string) => props.corp.divisions.find((division: IIndustry) => division.type === industryType) === undefined).sort(); + const [industry, setIndustry] = useState(possibleIndustries.length > 0 ? possibleIndustries[0] : ''); + const [name, setName] = useState(''); + + function newIndustry(): void { + try { + NewIndustry(props.corp, industry, name); + } catch(err) { + dialogBoxCreate(err+''); + return; + } + + // Set routing to the new division so that the UI automatically switches to it + props.routing.routeTo(name); + + removePopup(props.popupId); + } + + function onNameChange(event: React.ChangeEvent): void { + setName(event.target.value); + } + + function onKeyDown(event: React.KeyboardEvent): void { + if (event.keyCode === 13) newIndustry(); + } + + function onIndustryChange(event: React.ChangeEvent): void { + setIndustry(event.target.value); + } + + return (<> +

Create a new division to expand into a new industry:

+ +

{IndustryDescriptions[industry]}

+

+ +

Division name:

+ + Create Division + ); + +} diff --git a/src/Corporation/ui/Overview.jsx b/src/Corporation/ui/Overview.jsx deleted file mode 100644 index bbeb05431..000000000 --- a/src/Corporation/ui/Overview.jsx +++ /dev/null @@ -1,327 +0,0 @@ -// React Component for displaying Corporation Overview info -import React from "react"; -import { BaseReactComponent } from "./BaseReactComponent"; -import { LevelableUpgrade } from "./LevelableUpgrade"; -import { UnlockUpgrade } from "./UnlockUpgrade"; - -import { BribeThreshold } from "../Corporation"; -import { CorporationUnlockUpgrades } from "../data/CorporationUnlockUpgrades"; -import { CorporationUpgrades } from "../data/CorporationUpgrades"; - -import { CONSTANTS } from "../../Constants"; -import { numeralWrapper } from "../../ui/numeralFormat"; -import { convertTimeMsToTimeElapsedString } from "../../../utils/StringHelperFunctions"; - -export class Overview extends BaseReactComponent { - // Generic Function for Creating a button - createButton(props) { - let className = props.class ? props.class : "std-button"; - const displayStyle = props.display ? props.display : "block"; - const hasTooltip = (props.tooltip != null); - if (hasTooltip) { - className += " tooltip"; - } - - return ( - - {props.text} - { - hasTooltip && - - {props.tooltip} - - } - - ) - - } - - // Returns a string with general information about Corporation - getOverviewText() { - // Formatted text for profit - var profit = this.corp().revenue.minus(this.corp().expenses).toNumber(), - profitStr = profit >= 0 ? numeralWrapper.formatMoney(profit) : "-" + numeralWrapper.format(-1 * profit, "$0.000a"); - - // Formatted text for dividend information, if applicable - let dividendStr = ""; - if (this.corp().dividendPercentage > 0 && profit > 0) { - const totalDividends = (this.corp().dividendPercentage / 100) * profit; - const retainedEarnings = profit - totalDividends; - const dividendsPerShare = totalDividends / this.corp().totalShares; - const playerEarnings = this.corp().numShares * dividendsPerShare; - - dividendStr = `Retained Profits (after dividends): ${numeralWrapper.format(retainedEarnings, "$0.000a")} / s

` + - `Dividend Percentage: ${numeralWrapper.format(this.corp().dividendPercentage / 100, "0%")}
` + - `Dividends per share: ${numeralWrapper.format(dividendsPerShare, "$0.000a")} / s
` + - `Your earnings as a shareholder (Pre-Tax): ${numeralWrapper.format(playerEarnings, "$0.000a")} / s
` + - `Dividend Tax Rate: ${this.corp().dividendTaxPercentage}%
` + - `Your earnings as a shareholder (Post-Tax): ${numeralWrapper.format(playerEarnings * (1 - (this.corp().dividendTaxPercentage / 100)), "$0.000a")} / s

`; - } - - let txt = "Total Funds: " + numeralWrapper.format(this.corp().funds.toNumber(), '$0.000a') + "
" + - "Total Revenue: " + numeralWrapper.format(this.corp().revenue.toNumber(), "$0.000a") + " / s
" + - "Total Expenses: " + numeralWrapper.format(this.corp().expenses.toNumber(), "$0.000a") + " / s
" + - "Total Profits: " + profitStr + " / s
" + - dividendStr + - "Publicly Traded: " + (this.corp().public ? "Yes" : "No") + "
" + - "Owned Stock Shares: " + numeralWrapper.format(this.corp().numShares, '0.000a') + "
" + - "Stock Price: " + (this.corp().public ? numeralWrapper.formatMoney(this.corp().sharePrice) : "N/A") + "
" + - "

Total Stock Shares: " + numeralWrapper.format(this.corp().totalShares, "0.000a") + - "" + - `Outstanding Shares: ${numeralWrapper.format(this.corp().issuedShares, "0.000a")}
` + - `Private Shares: ${numeralWrapper.format(this.corp().totalShares - this.corp().issuedShares - this.corp().numShares, "0.000a")}` + - "



"; - - const storedTime = this.corp().storedCycles * CONSTANTS.MilliPerCycle; - if (storedTime > 15000) { - txt += `Bonus time: ${convertTimeMsToTimeElapsedString(storedTime)}

`; - } - - let prodMult = this.corp().getProductionMultiplier(), - storageMult = this.corp().getStorageMultiplier(), - advMult = this.corp().getAdvertisingMultiplier(), - empCreMult = this.corp().getEmployeeCreMultiplier(), - empChaMult = this.corp().getEmployeeChaMultiplier(), - empIntMult = this.corp().getEmployeeIntMultiplier(), - empEffMult = this.corp().getEmployeeEffMultiplier(), - salesMult = this.corp().getSalesMultiplier(), - sciResMult = this.corp().getScientificResearchMultiplier(); - if (prodMult > 1) {txt += "Production Multiplier: " + numeralWrapper.format(prodMult, "0.000") + "
";} - if (storageMult > 1) {txt += "Storage Multiplier: " + numeralWrapper.format(storageMult, "0.000") + "
";} - if (advMult > 1) {txt += "Advertising Multiplier: " + numeralWrapper.format(advMult, "0.000") + "
";} - if (empCreMult > 1) {txt += "Empl. Creativity Multiplier: " + numeralWrapper.format(empCreMult, "0.000") + "
";} - if (empChaMult > 1) {txt += "Empl. Charisma Multiplier: " + numeralWrapper.format(empChaMult, "0.000") + "
";} - if (empIntMult > 1) {txt += "Empl. Intelligence Multiplier: " + numeralWrapper.format(empIntMult, "0.000") + "
";} - if (empEffMult > 1) {txt += "Empl. Efficiency Multiplier: " + numeralWrapper.format(empEffMult, "0.000") + "
";} - if (salesMult > 1) {txt += "Sales Multiplier: " + numeralWrapper.format(salesMult, "0.000") + "
";} - if (sciResMult > 1) {txt += "Scientific Research Multiplier: " + numeralWrapper.format(sciResMult, "0.000") + "
";} - - return txt; - } - - // Render the buttons that lie below the overview text. - // These are mainly for things such as managing finances/stock - renderButtons() { - // Create a "Getting Started Guide" button that lets player view the - // handbook and adds it to the players home computer - const getStarterGuideOnClick = this.corp().getStarterGuide.bind(this.corp()); - const getStarterGuideBtn = this.createButton({ - class: "a-link-button", - display: "inline-block", - onClick: getStarterGuideOnClick, - text: "Getting Started Guide", - tooltip: "Get a copy of and read 'The Complete Handbook for Creating a Successful Corporation.' " + - "This is a .lit file that guides you through the beginning of setting up a Corporation and " + - "provides some tips/pointers for helping you get started with managing it.", - }); - - // Create a "Bribe Factions" button if your Corporation is powerful enough. - // This occurs regardless of whether you're public or private - const canBribe = (this.corp().determineValuation() >= BribeThreshold); - const bribeFactionsOnClick = this.eventHandler().createBribeFactionsPopup.bind(this.eventHandler()); - const bribeFactionsClass = (canBribe ? "a-link-button" : "a-link-button-inactive"); - const bribeFactionsBtn = this.createButton({ - class: bribeFactionsClass, - display: "inline-block", - onClick: bribeFactionsOnClick, - text: "Bribe Factions", - tooltip: (canBribe - ? "Use your Corporations power and influence to bribe Faction leaders in exchange for reputation" - : "Your Corporation is not powerful enough to bribe Faction leaders"), - - }); - - const generalBtns = { - bribeFactions: bribeFactionsBtn, - getStarterGuide: getStarterGuideBtn, - }; - - if (this.corp().public) { - return this.renderPublicButtons(generalBtns); - } else { - return this.renderPrivateButtons(generalBtns); - } - } - - - // Render the buttons for when your Corporation is still private - renderPrivateButtons(generalBtns) { - const fundingAvailable = (this.corp().fundingRound < 4); - const findInvestorsClassName = fundingAvailable ? "std-button" : "a-link-button-inactive"; - const findInvestorsTooltip = fundingAvailable ? "Search for private investors who will give you startup funding in exchangefor equity (stock shares) in your company" : null; - - const findInvestorsOnClick = this.corp().getInvestment.bind(this.corp()); - const goPublicOnClick = this.corp().goPublic.bind(this.corp()); - - const findInvestorsBtn = this.createButton({ - class: findInvestorsClassName, - onClick: findInvestorsOnClick, - style: "inline-block", - text: "Find Investors", - tooltip: findInvestorsTooltip, - }); - const goPublicBtn = this.createButton({ - class: "std-button", - onClick: goPublicOnClick, - style: "inline-block", - text: "Go Public", - tooltip: "Become a publicly traded and owned entity. Going public " + - "involves issuing shares for an IPO. Once you are a public " + - "company, your shares will be traded on the stock market.", - }); - - return ( -
- {generalBtns.getStarterGuide} - {findInvestorsBtn} - {goPublicBtn} -
- {generalBtns.bribeFactions} -
- ) - - } - - // Render the buttons for when your Corporation has gone public - renderPublicButtons(generalBtns) { - const corp = this.corp(); - - const sellSharesOnClick = this.eventHandler().createSellSharesPopup.bind(this.eventHandler()); - const sellSharesOnCd = (corp.shareSaleCooldown > 0); - const sellSharesClass = sellSharesOnCd ? "a-link-button-inactive" : "std-button"; - const sellSharesTooltip = sellSharesOnCd - ? "Cannot sell shares for " + corp.convertCooldownToString(corp.shareSaleCooldown) - : "Sell your shares in the company. The money earned from selling your " + - "shares goes into your personal account, not the Corporation's. " + - "This is one of the only ways to profit from your business venture." - const sellSharesBtn = this.createButton({ - class: sellSharesClass, - display: "inline-block", - onClick: function(event) { - if(!event.isTrusted) return; - sellSharesOnClick(event); - }, - text: "Sell Shares", - tooltip: sellSharesTooltip, - }); - - const buybackSharesOnClick = this.eventHandler().createBuybackSharesPopup.bind(this.eventHandler()); - const buybackSharesBtn = this.createButton({ - class: "std-button", - display: "inline-block", - onClick: buybackSharesOnClick, - text: "Buyback shares", - tooltip: "Buy back shares you that previously issued or sold at market price.", - }); - - const issueNewSharesOnClick = this.eventHandler().createIssueNewSharesPopup.bind(this.eventHandler()); - const issueNewSharesOnCd = (corp.issueNewSharesCooldown > 0); - const issueNewSharesClass = issueNewSharesOnCd ? "a-link-button-inactive" : "std-button"; - const issueNewSharesTooltip = issueNewSharesOnCd - ? "Cannot issue new shares for " + corp.convertCooldownToString(corp.issueNewSharesCooldown) - : "Issue new equity shares to raise capital."; - const issueNewSharesBtn = this.createButton({ - class: issueNewSharesClass, - display: "inline-block", - onClick: issueNewSharesOnClick, - text: "Issue New Shares", - tooltip: issueNewSharesTooltip, - }); - - const issueDividendsOnClick = this.eventHandler().createIssueDividendsPopup.bind(this.eventHandler()); - const issueDividendsBtn = this.createButton({ - class: "std-button", - display: "inline-block", - onClick: issueDividendsOnClick, - text: "Issue Dividends", - tooltip: "Manage the dividends that are paid out to shareholders (including yourself)", - }); - - return ( -
- {generalBtns.getStarterGuide} - {sellSharesBtn} - {buybackSharesBtn} -
- {issueNewSharesBtn} - {issueDividendsBtn} -
- {generalBtns.bribeFactions} -
- ) - } - - // Render the UI for Corporation upgrades - renderUpgrades() { - // Don't show upgrades - if (this.corp().divisions.length <= 0) { return; } - - // Create an array of all Unlocks - const unlockUpgrades = []; - Object.values(CorporationUnlockUpgrades).forEach((unlockData) => { - if (this.corp().unlockUpgrades[unlockData[0]] === 0) { - unlockUpgrades.push(this.renderUnlockUpgrade(unlockData)); - } - }); - - // Create an array of properties of all unlocks - const levelableUpgradeProps = []; - for (let i = 0; i < this.corp().upgrades.length; ++i) { - const upgradeData = CorporationUpgrades[i]; - const level = this.corp().upgrades[i]; - - levelableUpgradeProps.push({ - upgradeData: upgradeData, - upgradeLevel: level, - }); - } - - - return ( -
-

Unlocks

- {unlockUpgrades} - -

Upgrades

- { - levelableUpgradeProps.map((data) => { - return this.renderLevelableUpgrade(data); - }) - } -
- ) - } - - renderUnlockUpgrade(data) { - return ( - - ) - } - - renderLevelableUpgrade(data) { - return ( - - ) - - } - - render() { - return ( -
-

- {this.renderButtons()} -
- {this.renderUpgrades()} -
- ) - } -} diff --git a/src/Corporation/ui/Overview.tsx b/src/Corporation/ui/Overview.tsx new file mode 100644 index 000000000..d6edd6a42 --- /dev/null +++ b/src/Corporation/ui/Overview.tsx @@ -0,0 +1,367 @@ +// React Component for displaying Corporation Overview info +import React from "react"; +import { LevelableUpgrade } from "./LevelableUpgrade"; +import { UnlockUpgrade } from "./UnlockUpgrade"; +import { BribeFactionPopup } from "./BribeFactionPopup"; +import { SellSharesPopup } from "./SellSharesPopup"; +import { BuybackSharesPopup } from "./BuybackSharesPopup"; +import { IssueDividendsPopup } from "./IssueDividendsPopup"; +import { IssueNewSharesPopup } from "./IssueNewSharesPopup"; +import { FindInvestorsPopup } from "./FindInvestorsPopup"; +import { GoPublicPopup } from "./GoPublicPopup"; + +import { CorporationConstants } from "../data/Constants"; +import { + CorporationUnlockUpgrade, + CorporationUnlockUpgrades } from "../data/CorporationUnlockUpgrades"; +import { + CorporationUpgrade, + CorporationUpgrades } from "../data/CorporationUpgrades"; + +import { CONSTANTS } from "../../Constants"; +import { numeralWrapper } from "../../ui/numeralFormat"; +import { convertTimeMsToTimeElapsedString } from "../../../utils/StringHelperFunctions"; +import { createPopup } from "../../ui/React/createPopup"; +import { Money } from "../../ui/React/Money"; +import { IPlayer } from "../../PersonObjects/IPlayer"; +import { ICorporation } from "../ICorporation"; + +interface IProps { + corp: ICorporation; + player: IPlayer; +} + +interface GeneralBtns { + bribeFactions: React.ReactElement; +} + +export function Overview(props: IProps): React.ReactElement { + // Generic Function for Creating a button + interface ICreateButtonProps { + text: string; + class?: string; + className?: string; + display?: string; + tooltip?: string; + onClick?: (event: React.MouseEvent) => void; + } + + function Button(props: ICreateButtonProps): React.ReactElement { + let className = props.className ? props.className : "std-button"; + const hasTooltip = props.tooltip != null; + if(hasTooltip) className += " tooltip"; + return ( + + {props.text} + { + hasTooltip && + + {props.tooltip} + + } + + ); + } + + function createButton(props: ICreateButtonProps): React.ReactElement { + let className = props.class ? props.class : "std-button"; + const displayStyle = props.display ? props.display : "block"; + const hasTooltip = (props.tooltip != null); + if (hasTooltip) { + className += " tooltip"; + } + + return ( + + {props.text} + { + hasTooltip && + + {props.tooltip} + + } + + ) + } + + function openBribeFactionPopup(): void { + const popupId = "corp-bribe-popup"; + createPopup(popupId, BribeFactionPopup, { + player: props.player, + popupId: popupId, + corp: props.corp, + }); + } + + const profit: number = props.corp.revenue.minus(props.corp.expenses).toNumber(); + + function DividendsStats() { + if(props.corp.dividendPercentage <= 0 || profit <= 0) return (<>); + const totalDividends = (props.corp.dividendPercentage / 100) * profit; + const retainedEarnings = profit - totalDividends; + const dividendsPerShare = totalDividends / props.corp.totalShares; + const playerEarnings = props.corp.numShares * dividendsPerShare; + return (<> + Retained Profits (after dividends): / s

+ Dividend Percentage: {numeralWrapper.format(props.corp.dividendPercentage / 100, "0%")}
+ Dividends per share: / s
+ Your earnings as a shareholder (Pre-Tax): / s
+ Dividend Tax Rate: {props.corp.dividendTaxPercentage}%
+ Your earnings as a shareholder (Post-Tax): / s

+ ); + } + + function Mult(props: {name: string, mult: number}): React.ReactElement { + if(props.mult <= 1) return (<>); + return (

{props.name}{numeralWrapper.format(props.mult, "0.000")}

); + } + + // Returns a string with general information about Corporation + function BonusTime(): React.ReactElement { + const storedTime = props.corp.storedCycles * CONSTANTS.MilliPerCycle; + if (storedTime <= 15000) return (<>); + return (

Bonus time: {convertTimeMsToTimeElapsedString(storedTime)}

); + } + + function BribeButton(): React.ReactElement { + const canBribe = (props.corp.determineValuation() >= CorporationConstants.BribeThreshold) || true; + const bribeFactionsClass = (canBribe ? "a-link-button" : "a-link-button-inactive"); + return
+
+ +
+ ) +} diff --git a/src/Corporation/ui/ProductMarketTaPopup.tsx b/src/Corporation/ui/ProductMarketTaPopup.tsx new file mode 100644 index 000000000..be00d9131 --- /dev/null +++ b/src/Corporation/ui/ProductMarketTaPopup.tsx @@ -0,0 +1,99 @@ +import React, { useState } from 'react'; +import { numeralWrapper } from "../../ui/numeralFormat"; +import { IIndustry } from "../IIndustry"; +import { Product } from "../Product"; + +interface IProps { + product: Product; + industry: IIndustry; + popupId: string; +} + +function MarketTA2(props: IProps): React.ReactElement { + const markupLimit = props.product.rat / props.product.mku; + const [value, setValue] = useState(props.product.pCost); + const setRerender = useState(false)[1]; + function rerender(): void { + setRerender(old => !old); + } + + function onChange(event: React.ChangeEvent): void { + setValue(parseFloat(event.target.value)); + } + + + function onCheckedChange(event: React.ChangeEvent): void { + props.product.marketTa2 = event.target.checked; + rerender(); + } + + const sCost = value; + let markup = 1; + if (sCost > props.product.pCost) { + if ((sCost - props.product.pCost) > markupLimit) { + markup = markupLimit / (sCost - props.product.pCost); + } + } + + return (<> +

+
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. +

+ +
+ + +
+

+ Note that Market-TA.II overrides Market-TA.I. This means that if + both are enabled, then Market-TA.II will take effect, not Market-TA.I +

+ ); +} + +// Create a popup that lets the player use the Market TA research for Products +export function ProductMarketTaPopup(props: IProps): React.ReactElement { + const markupLimit = props.product.rat / props.product.mku; + const setRerender = useState(false)[1]; + function rerender(): void { + setRerender(old => !old); + } + + function onChange(event: React.ChangeEvent): void { + props.product.marketTa1 = event.target.checked; + rerender(); + } + + return (<> +

+ Market-TA.I
+ The maximum sale price you can mark this up to + is {numeralWrapper.formatMoney(props.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. +

+
+ + +
+ {props.industry.hasResearch("Market-TA.II") && } + ); +} diff --git a/src/Corporation/ui/PurchaseMaterialPopup.tsx b/src/Corporation/ui/PurchaseMaterialPopup.tsx new file mode 100644 index 000000000..19166a030 --- /dev/null +++ b/src/Corporation/ui/PurchaseMaterialPopup.tsx @@ -0,0 +1,130 @@ +import React, { useState } from 'react'; +import { dialogBoxCreate } from "../../../utils/DialogBox"; +import { removePopup } from "../../ui/React/createPopup"; +import { MaterialSizes } from "../MaterialSizes"; +import { Warehouse } from "../Warehouse"; +import { Material } from "../Material"; +import { IIndustry } from "../IIndustry"; +import { ICorporation } from "../ICorporation"; +import { numeralWrapper } from "../../ui/numeralFormat"; +import { BuyMaterial } from "../Actions"; + +interface IBulkPurchaseTextProps { + warehouse: Warehouse; + mat: Material; + amount: string; +} + +function BulkPurchaseText(props: IBulkPurchaseTextProps): React.ReactElement { + const parsedAmt = parseFloat(props.amount); + const cost = parsedAmt * props.mat.bCost; + + const matSize = MaterialSizes[props.mat.name]; + const maxAmount = ((props.warehouse.size - props.warehouse.sizeUsed) / matSize); + + if (parsedAmt * matSize > maxAmount) { + return (<>Not enough warehouse space to purchase this amount); + } else if (isNaN(cost)) { + return (<>Invalid put for Bulk Purchase amount); + } else { + return (<>Purchasing {numeralWrapper.format(parsedAmt, "0,0.00")} of + {props.mat.name} will cost {numeralWrapper.formatMoney(cost)}); + } +} + +interface IProps { + mat: Material; + industry: IIndustry; + warehouse: Warehouse; + corp: ICorporation; + popupId: string; +} + +function BulkPurchase(props: IProps): React.ReactElement { + const [buyAmt, setBuyAmt] = useState(''); + + function bulkPurchase(): void { + const amount = parseFloat(buyAmt); + + const matSize = MaterialSizes[props.mat.name]; + const maxAmount = ((props.warehouse.size - props.warehouse.sizeUsed) / matSize); + if (amount * matSize > maxAmount) { + dialogBoxCreate(`You do not have enough warehouse size to fit this purchase`); + return; + } + + if (isNaN(amount)) { + dialogBoxCreate("Invalid input amount"); + } else { + const cost = amount * props.mat.bCost; + if (props.corp.funds.gt(cost)) { + props.corp.funds = props.corp.funds.minus(cost); + props.mat.qty += amount; + } else { + dialogBoxCreate(`You cannot afford this purchase.`); + return; + } + + removePopup(props.popupId); + } + } + + function onKeyDown(event: React.KeyboardEvent): void { + if (event.keyCode === 13) bulkPurchase(); + } + + function onChange(event: React.ChangeEvent): void { + setBuyAmt(event.target.value); + } + + return (<> +

+ Enter the amount of {props.mat.name} you would like + to bulk purchase. This purchases the specified amount instantly + (all at once). +

+ + + + ); +} + +// Create a popup that lets the player purchase a Material +export function PurchaseMaterialPopup(props: IProps): React.ReactElement { + const [buyAmt, setBuyAmt] = useState(props.mat.buy ? props.mat.buy : null); + + function purchaseMaterial(): void { + if(buyAmt === null) return; + try { + BuyMaterial(props.mat, buyAmt) + } catch(err) { + dialogBoxCreate(err+''); + } + + removePopup(props.popupId); + } + + function clearPurchase(): void { + props.mat.buy = 0; + removePopup(props.popupId); + } + + function onKeyDown(event: React.KeyboardEvent): void { + if (event.keyCode === 13) purchaseMaterial(); + } + + function onChange(event: React.ChangeEvent): void { + setBuyAmt(parseFloat(event.target.value)); + } + + return (<> +

+ Enter the amount of {props.mat.name} you would like + to purchase per second. This material's cost changes constantly. +

+ + + + {props.industry.hasResearch("Bulk Purchasing") && } + ); +} diff --git a/src/Corporation/ui/ResearchPopup.tsx b/src/Corporation/ui/ResearchPopup.tsx new file mode 100644 index 000000000..72010b249 --- /dev/null +++ b/src/Corporation/ui/ResearchPopup.tsx @@ -0,0 +1,101 @@ +import React, { useEffect } from 'react'; +import { dialogBoxCreate } from "../../../utils/DialogBox"; +import { createElement } from "../../../utils/uiHelpers/createElement"; +import { removePopup } from "../../ui/React/createPopup"; +import { IndustryResearchTrees } from "../IndustryData"; +import { CorporationConstants } from "../data/Constants"; +import { ResearchMap } from "../ResearchMap"; +import { Treant } from 'treant-js'; +import { IIndustry } from "../IIndustry"; + +interface IProps { + industry: IIndustry; + popupId: string; +} + +// Create the Research Tree UI for this Industry +export function ResearchPopup(props: IProps): React.ReactElement { + useEffect(() => { + const researchTree = IndustryResearchTrees[props.industry.type]; + if(researchTree === undefined) return; + + // Get the tree's markup (i.e. config) for Treant + const markup = researchTree.createTreantMarkup(); + markup.chart.container = "#" + props.popupId + "-content"; + markup.chart.nodeAlign = "BOTTOM"; + markup.chart.rootOrientation = "WEST"; + markup.chart.siblingSeparation = 40; + markup.chart.connectors = { + type: "step", + style: { + "arrow-end": "block-wide-long", + "stroke": "white", + "stroke-width": 2, + }, + } + + Treant(markup); + + // Add Event Listeners for all Nodes + const allResearch = researchTree.getAllNodes(); + for (let i = 0; i < allResearch.length; ++i) { + // If this is already Researched, skip it + if (props.industry.researched[allResearch[i]] === true) { + continue; + } + + // Get the Research object + const research = ResearchMap[allResearch[i]]; + + // Get the DOM Element to add a click listener to it + const sanitizedName = allResearch[i].replace(/\s/g, ''); + const div = document.getElementById(sanitizedName + "-corp-research-click-listener"); + if (div == null) { + console.warn(`Could not find Research Tree div for ${sanitizedName}`); + continue; + } + + div.addEventListener("click", () => { + if (props.industry.sciResearch.qty >= research.cost) { + props.industry.sciResearch.qty -= research.cost; + + // Get the Node from the Research Tree and set its 'researched' property + researchTree.research(allResearch[i]); + props.industry.researched[allResearch[i]] = true; + + dialogBoxCreate(`Researched ${allResearch[i]}. It may take a market cycle ` + + `(~${CorporationConstants.SecsPerMarketCycle} seconds) before the effects of ` + + `the Research apply.`); + removePopup(props.popupId); + } else { + dialogBoxCreate(`You do not have enough Scientific Research for ${research.name}`); + } + }); + } + + + const boxContent = document.getElementById(`${props.popupId}-content`); + if (boxContent != null) { + // Add information about multipliers from research at the bottom of the popup + //appendLineBreaks(boxContent, 2); + boxContent.appendChild(createElement("pre", { + display: "block", + innerText: `Multipliers from research:\n` + + ` * Advertising Multiplier: x${researchTree.getAdvertisingMultiplier()}\n` + + ` * Employee Charisma Multiplier: x${researchTree.getEmployeeChaMultiplier()}\n` + + ` * Employee Creativity Multiplier: x${researchTree.getEmployeeCreMultiplier()}\n` + + ` * Employee Efficiency Multiplier: x${researchTree.getEmployeeEffMultiplier()}\n` + + ` * Employee Intelligence Multiplier: x${researchTree.getEmployeeIntMultiplier()}\n` + + ` * Production Multiplier: x${researchTree.getProductionMultiplier()}\n` + + ` * Sales Multiplier: x${researchTree.getSalesMultiplier()}\n` + + ` * Scientific Research Multiplier: x${researchTree.getScientificResearchMultiplier()}\n` + + ` * Storage Multiplier: x${researchTree.getStorageMultiplier()}`, + })); + } + }); + + return
+
+
+ +} diff --git a/src/Corporation/ui/Root.jsx b/src/Corporation/ui/Root.jsx deleted file mode 100644 index f393de20e..000000000 --- a/src/Corporation/ui/Root.jsx +++ /dev/null @@ -1,17 +0,0 @@ -// Root React Component for the Corporation UI -import React from "react"; -import { BaseReactComponent } from "./BaseReactComponent"; - -import { HeaderTabs } from "./HeaderTabs"; -import { MainPanel } from "./MainPanel"; - -export class CorporationRoot extends BaseReactComponent { - render() { - return ( -
- - -
- ) - } -} diff --git a/src/Corporation/ui/Root.tsx b/src/Corporation/ui/Root.tsx new file mode 100644 index 000000000..f253e0157 --- /dev/null +++ b/src/Corporation/ui/Root.tsx @@ -0,0 +1,23 @@ +// Root React Component for the Corporation UI +import React from "react"; + +import { HeaderTabs } from "./HeaderTabs"; +import { MainPanel } from "./MainPanel"; +import { IPlayer } from "../../PersonObjects/IPlayer"; +import { ICorporation } from "../ICorporation"; +import { CorporationRouting } from "./Routing"; + +interface IProps { + corp: ICorporation; + routing: CorporationRouting; + player: IPlayer; +} + +export function CorporationRoot(props: IProps): React.ReactElement { + return ( +
+ + +
+ ) +} diff --git a/src/Corporation/ui/Routing.ts b/src/Corporation/ui/Routing.ts index 307f64f16..b39d8d164 100644 --- a/src/Corporation/ui/Routing.ts +++ b/src/Corporation/ui/Routing.ts @@ -1,33 +1,8 @@ -import { IMap } from "../../types"; +import { ICorporation } from "../ICorporation"; +import { IIndustry } from "../IIndustry"; export const overviewPage = "Overview"; -// Interfaces for whatever's required to sanitize routing with Corporation Data -interface IOfficeSpace { - loc: string; - cost: number; - size: number; - comf: number; - beau: number; - tier: any; - minEne: number; - maxEne: number; - minHap: number; - maxHap: number; - maxMor: number; - employees: any; - employeeProd: any; -} - -interface IDivision { - name: string; - offices: IMap; -} - -interface ICorporation { - divisions: IDivision[]; -} - /** * Keeps track of what content is currently being displayed for the Corporation UI */ @@ -39,7 +14,7 @@ export class CorporationRouting { // Stores a reference to the Division instance that the routing is currently on // This will be null if routing is on the overview page - currentDivision: IDivision | null = null; + currentDivision: IIndustry | null = null; constructor(corp: ICorporation) { this.corp = corp; diff --git a/src/Corporation/ui/SellMaterialPopup.tsx b/src/Corporation/ui/SellMaterialPopup.tsx new file mode 100644 index 000000000..b209a90e0 --- /dev/null +++ b/src/Corporation/ui/SellMaterialPopup.tsx @@ -0,0 +1,72 @@ +import React, { useState } from 'react'; +import { dialogBoxCreate } from "../../../utils/DialogBox"; +import { removePopup } from "../../ui/React/createPopup"; +import { ICorporation } from "../ICorporation"; +import { Material } from "../Material"; +import { SellMaterial } from "../Actions"; + +function initialPrice(mat: Material): string { + let val = mat.sCost ? mat.sCost+'' : ''; + if (mat.marketTa2) { + val += " (Market-TA.II)"; + } else if (mat.marketTa1) { + val += " (Market-TA.I)"; + } + return val; +} + +interface IProps { + mat: Material; + corp: ICorporation; + popupId: string; +} + +// Create a popup that let the player manage sales of a material +export function SellMaterialPopup(props: IProps): React.ReactElement { + const [amt, setAmt] = useState(props.mat.sllman[1] ? props.mat.sllman[1]+'' : ''); + const [price, setPrice] = useState(initialPrice(props.mat)); + + function sellMaterial(): void { + try { + SellMaterial(props.mat, amt, price); + } catch(err) { + dialogBoxCreate(err+''); + } + + removePopup(props.popupId); + } + + function onAmtChange(event: React.ChangeEvent): void { + setAmt(event.target.value); + } + + function onPriceChange(event: React.ChangeEvent): void { + setPrice(event.target.value); + } + + function onKeyDown(event: React.KeyboardEvent): void { + if (event.keyCode === 13) sellMaterial(); + } + + return (<> +

+Enter the maximum amount of {props.mat.name} you would like +to sell per second, as well as the price at which you would +like to sell at.

+If the sell amount is set to 0, then the material will not be sold. If the sell price +if set to 0, then the material will be discarded

+Setting the sell amount to 'MAX' will result in you always selling the +maximum possible amount of the material.

+When setting the sell amount, you can use the 'PROD' variable to designate a dynamically +changing amount that depends on your production. For example, if you set the sell amount +to 'PROD-5' then you will always sell 5 less of the material than you produce.

+When setting the sell price, you can use the 'MP' variable to designate a dynamically +changing price that depends on the market price. For example, if you set the sell price +to 'MP+10' then it will always be sold at $10 above the market price. +

+
+ + + + ); +} diff --git a/src/Corporation/ui/SellProductPopup.tsx b/src/Corporation/ui/SellProductPopup.tsx new file mode 100644 index 000000000..d04f13f60 --- /dev/null +++ b/src/Corporation/ui/SellProductPopup.tsx @@ -0,0 +1,83 @@ +import React, { useState } from 'react'; +import { dialogBoxCreate } from "../../../utils/DialogBox"; +import { removePopup } from "../../ui/React/createPopup"; +import { Cities } from "../../Locations/Cities"; +import { Product } from "../Product"; +import { SellProduct } from "../Actions"; + +function initialPrice(product: Product): string { + let val = product.sCost ? product.sCost+'' : ''; + if (product.marketTa2) { + val += " (Market-TA.II)"; + } else if (product.marketTa1) { + val += " (Market-TA.I)"; + } + return val; +} + +interface IProps { + product: Product; + city: string; + popupId: string; +} + +// Create a popup that let the player manage sales of a material +export function SellProductPopup(props: IProps): React.ReactElement { + const [checked, setChecked] = useState(true); + const [iQty, setQty] = useState(props.product.sllman[props.city][1] ? props.product.sllman[props.city][1] : ''); + const [px, setPx] = useState(initialPrice(props.product)); + + function onCheckedChange(event: React.ChangeEvent): void { + setChecked(event.target.checked); + } + + function sellProduct(): void { + try { + SellProduct(props.product, props.city, iQty, px, checked); + } catch(err) { + dialogBoxCreate(err+''); + } + + removePopup(props.popupId); + } + + function onAmtChange(event: React.ChangeEvent): void { + setQty(event.target.value); + } + + function onPriceChange(event: React.ChangeEvent): void { + setPx(event.target.value); + } + + function onKeyDown(event: React.KeyboardEvent): void { + if (event.keyCode === 13) sellProduct(); + } + + return (<> +

+Enter the maximum amount of {props.product.name} you would like +to sell per second, as well as the price at which you would like to +sell it at.

+If the sell amount is set to 0, then the product will not be sold. If the +sell price is set to 0, then the product will be discarded.

+Setting the sell amount to 'MAX' will result in you always selling the +maximum possible amount of the material.

+When setting the sell amount, you can use the 'PROD' variable to designate a +dynamically changing amount that depends on your production. For example, +if you set the sell amount to 'PROD-1' then you will always sell 1 less of +the material than you produce.

+When setting the sell price, you can use the 'MP' variable to set a +dynamically changing price that depends on the Product's estimated +market price. For example, if you set it to 'MP*5' then it +will always be sold at five times the estimated market price. +

+
+ + + +
+ + +
+ ); +} diff --git a/src/Corporation/ui/SellSharesPopup.tsx b/src/Corporation/ui/SellSharesPopup.tsx new file mode 100644 index 000000000..f8b898810 --- /dev/null +++ b/src/Corporation/ui/SellSharesPopup.tsx @@ -0,0 +1,97 @@ +import React, { useState } from 'react'; +import { numeralWrapper } from "../../ui/numeralFormat"; +import { dialogBoxCreate } from "../../../utils/DialogBox"; +import { IPlayer } from "../../PersonObjects/IPlayer"; +import { removePopup } from "../../ui/React/createPopup"; +import { CorporationConstants } from "../data/Constants"; +import { ICorporation } from "../ICorporation"; + +interface IProps { + corp: ICorporation; + player: IPlayer; + popupId: string; +} + +// Create a popup that lets the player sell Corporation shares +// This is created when the player clicks the "Sell Shares" button in the overview panel +export function SellSharesPopup(props: IProps): React.ReactElement { + const [shares, setShares] = useState(null); + + function changeShares(event: React.ChangeEvent): void { + if(event.target.value === "") setShares(null); + else setShares(Math.round(parseFloat(event.target.value))); + } + + function ProfitIndicator(props: {shares: number | null; corp: ICorporation}): React.ReactElement { + if(props.shares === null) return (<>); + if (isNaN(props.shares) || props.shares <= 0) { + return (<>ERROR: Invalid value entered for number of shares to sell); + } else if (props.shares > props.corp.numShares) { + return (<>You don't have this many shares to sell!); + } else { + const stockSaleResults = props.corp.calculateShareSale(props.shares); + const profit = stockSaleResults[0]; + return (<>Sell {props.shares} shares for a total of {numeralWrapper.formatMoney(profit)}); + } + } + + function sell(): void { + if(shares === null) return; + if (isNaN(shares) || shares <= 0) { + dialogBoxCreate("ERROR: Invalid value for number of shares"); + } else if (shares > props.corp.numShares) { + dialogBoxCreate("ERROR: You don't have this many shares to sell"); + } else { + const stockSaleResults = props.corp.calculateShareSale(shares); + const profit = stockSaleResults[0]; + const newSharePrice = stockSaleResults[1]; + const newSharesUntilUpdate = stockSaleResults[2]; + + props.corp.numShares -= shares; + if (isNaN(props.corp.issuedShares)) { + console.error(`Corporation issuedShares is NaN: ${props.corp.issuedShares}`); + const res = props.corp.issuedShares; + if (isNaN(res)) { + props.corp.issuedShares = 0; + } else { + props.corp.issuedShares = res; + } + } + props.corp.issuedShares += shares; + props.corp.sharePrice = newSharePrice; + props.corp.shareSalesUntilPriceUpdate = newSharesUntilUpdate; + props.corp.shareSaleCooldown = CorporationConstants.SellSharesCooldown; + props.player.gainMoney(profit); + props.player.recordMoneySource(profit, "corporation"); + removePopup(props.popupId); + dialogBoxCreate(`Sold ${numeralWrapper.formatMoney(shares)} shares for ` + + `${numeralWrapper.formatMoney(profit)}. ` + + `The corporation's stock price fell to ${numeralWrapper.formatMoney(props.corp.sharePrice)} ` + + `as a result of dilution.`); + + props.corp.rerender(props.player); + } + } + + + function onKeyDown(event: React.KeyboardEvent): void { + if (event.keyCode === 13) sell(); + } + + return (<> +

+Enter the number of shares you would like to sell. The money from +selling your shares will go directly to you (NOT your Corporation).

+Selling your shares will cause your corporation's stock price to fall due to +dilution. Furthermore, selling a large number of shares all at once will have an immediate effect +in reducing your stock price.

+The current price of your +company's stock is {numeralWrapper.formatMoney(props.corp.sharePrice)} +

+ +
+ + + ); + +} \ No newline at end of file diff --git a/src/Corporation/ui/ThrowPartyPopup.tsx b/src/Corporation/ui/ThrowPartyPopup.tsx new file mode 100644 index 000000000..6f6fc774d --- /dev/null +++ b/src/Corporation/ui/ThrowPartyPopup.tsx @@ -0,0 +1,58 @@ +import React, { useState } from 'react'; +import { removePopup } from "../../ui/React/createPopup"; +import { numeralWrapper } from "../../ui/numeralFormat"; +import { dialogBoxCreate } from "../../../utils/DialogBox"; +import { OfficeSpace } from "../OfficeSpace"; +import { ICorporation } from "../ICorporation"; + +interface IProps { + office: OfficeSpace; + corp: ICorporation; + popupId: string; +} + +export function ThrowPartyPopup(props: IProps): React.ReactElement { + const [cost, setCost] = useState(null); + + function changeCost(event: React.ChangeEvent): void { + setCost(parseFloat(event.target.value)); + } + + function throwParty(): void { + if (cost === null || isNaN(cost) || cost < 0) { + dialogBoxCreate("Invalid value entered"); + } else { + const totalCost = cost * props.office.employees.length; + if (props.corp.funds.lt(totalCost)) { + dialogBoxCreate("You don't have enough company funds to throw a party!"); + } else { + props.corp.funds = props.corp.funds.minus(totalCost); + let mult = 0; + for (let i = 0; i < props.office.employees.length; ++i) { + mult = props.office.employees[i].throwParty(cost); + } + dialogBoxCreate("You threw a party for the office! The morale and happiness " + + "of each employee increased by " + numeralWrapper.formatPercentage((mult-1))); + removePopup(props.popupId); + } + } + } + + function EffectText(props: {cost: number | null; office: OfficeSpace}): React.ReactElement { + let cost = props.cost; + if(cost !== null && (isNaN(cost) || cost < 0)) return

Invalid value entered!

+ if(cost === null) cost = 0; + return

Throwing this party will cost a total of {numeralWrapper.formatMoney(cost * props.office.employees.length)}

+ } + + function onKeyDown(event: React.KeyboardEvent): void { + if (event.keyCode === 13) throwParty(); + } + + return (<> +

Enter the amount of money you would like to spend PER EMPLOYEE on this office party

+ + + + ); +} \ No newline at end of file diff --git a/src/Corporation/ui/UnlockUpgrade.jsx b/src/Corporation/ui/UnlockUpgrade.jsx deleted file mode 100644 index e719c73c7..000000000 --- a/src/Corporation/ui/UnlockUpgrade.jsx +++ /dev/null @@ -1,30 +0,0 @@ -// React Components for the Unlock upgrade buttons on the overview page -import React from "react"; -import { BaseReactComponent } from "./BaseReactComponent"; - -import { numeralWrapper } from "../../ui/numeralFormat"; -import { dialogBoxCreate } from "../../../utils/DialogBox"; - -export class UnlockUpgrade extends BaseReactComponent { - render() { - const data = this.props.upgradeData; - const text = `${data[2]} - ${numeralWrapper.formatMoney(data[1])}`; - const tooltip = data[3]; - const onClick = () => { - const corp = this.corp(); - if (corp.funds.lt(data[1])) { - dialogBoxCreate("Insufficient funds"); - } else { - corp.unlock(data); - corp.rerender(); - } - } - - return ( -
- {text} - {tooltip} -
- ) - } -} diff --git a/src/Corporation/ui/UnlockUpgrade.tsx b/src/Corporation/ui/UnlockUpgrade.tsx new file mode 100644 index 000000000..0b08c4d4a --- /dev/null +++ b/src/Corporation/ui/UnlockUpgrade.tsx @@ -0,0 +1,37 @@ +// React Components for the Unlock upgrade buttons on the overview page +import React from "react"; + +import { numeralWrapper } from "../../ui/numeralFormat"; +import { dialogBoxCreate } from "../../../utils/DialogBox"; +import { CorporationUnlockUpgrade } from "../data/CorporationUnlockUpgrades"; +import { ICorporation } from "../ICorporation"; +import { IPlayer } from "../../PersonObjects/IPlayer"; +import { UnlockUpgrade as UU } from "../Actions"; +import { Money } from "../../ui/React/Money"; + +interface IProps { + upgradeData: CorporationUnlockUpgrade; + corp: ICorporation; + player: IPlayer; +} + +export function UnlockUpgrade(props: IProps): React.ReactElement { + const data = props.upgradeData; + const text = <>{data[2]} - ; + const tooltip = data[3]; + function onClick(): void { + try { + UU(props.corp, props.upgradeData); + } catch(err) { + dialogBoxCreate(err+''); + } + props.corp.rerender(props.player); + } + + return ( +
+ {text} + {tooltip} +
+ ) +} diff --git a/src/Corporation/ui/UpgradeOfficeSizePopup.tsx b/src/Corporation/ui/UpgradeOfficeSizePopup.tsx new file mode 100644 index 000000000..a7b948140 --- /dev/null +++ b/src/Corporation/ui/UpgradeOfficeSizePopup.tsx @@ -0,0 +1,80 @@ +import React from "react"; +import { removePopup } from "../../ui/React/createPopup"; +import { numeralWrapper } from "../../ui/numeralFormat"; +import { dialogBoxCreate } from "../../../utils/DialogBox"; +import { CorporationConstants } from "../data/Constants"; +import { OfficeSpace } from "../OfficeSpace"; +import { ICorporation } from "../ICorporation"; +import { IPlayer } from "../../PersonObjects/IPlayer"; + +interface IProps { + office: OfficeSpace; + corp: ICorporation; + popupId: string; + player: IPlayer; +} + +export function UpgradeOfficeSizePopup(props: IProps): React.ReactElement { + const initialPriceMult = Math.round(props.office.size / CorporationConstants.OfficeInitialSize); + const costMultiplier = 1.09; + const upgradeCost = CorporationConstants.OfficeInitialCost * Math.pow(costMultiplier, initialPriceMult); + + // Calculate cost to upgrade size by 15 employees + let mult = 0; + for (let i = 0; i < 5; ++i) { + mult += (Math.pow(costMultiplier, initialPriceMult + i)); + } + const upgradeCost15 = CorporationConstants.OfficeInitialCost * mult; + + //Calculate max upgrade size and cost + const maxMult = (props.corp.funds.dividedBy(CorporationConstants.OfficeInitialCost)).toNumber(); + let maxNum = 1; + mult = Math.pow(costMultiplier, initialPriceMult); + while(maxNum < 50) { //Hard cap of 50x (extra 150 employees) + if (mult >= maxMult) break; + const multIncrease = Math.pow(costMultiplier, initialPriceMult + maxNum); + if (mult + multIncrease > maxMult) { + break; + } else { + mult += multIncrease; + } + ++maxNum; + } + const upgradeCostMax = CorporationConstants.OfficeInitialCost * mult; + + function upgradeSize(cost: number, size: number): void { + if (props.corp.funds.lt(cost)) { + dialogBoxCreate("You don't have enough company funds to purchase this upgrade!"); + } else { + props.office.size += size; + props.corp.funds = props.corp.funds.minus(cost); + dialogBoxCreate("Office space increased! It can now hold " + props.office.size + " employees"); + props.corp.rerender(props.player); + } + removePopup(props.popupId); + } + + interface IUpgradeButton { + cost: number; + size: number; + corp: ICorporation; + } + + function UpgradeSizeButton(props: IUpgradeButton): React.ReactElement { + return (); + } + + return (<> +

Increase the size of your office space to fit additional employees!

+

Upgrade size:

+ + + + ); +} \ No newline at end of file diff --git a/src/DevMenu.jsx b/src/DevMenu.jsx index 74ebdf3a9..cbb745785 100644 --- a/src/DevMenu.jsx +++ b/src/DevMenu.jsx @@ -584,6 +584,20 @@ class DevMenuComponent extends Component { } } + finishCorporationProducts() { + if(!Player.corporation) return; + Player.corporation.divisions.forEach(div => { + Object.keys(div.products).forEach(prod => div.products[prod].prog = 99.9) + }); + } + + addCorporationResearch() { + if(!Player.corporation) return; + Player.corporation.divisions.forEach(div => { + div.sciResearch.qty += 1e10; + }); + } + specificContract() { generateContract({ problemType: this.state.codingcontract, @@ -1169,6 +1183,16 @@ class DevMenuComponent extends Component { /> + + + + + + + + + +
diff --git a/src/InteractiveTutorial.d.ts b/src/InteractiveTutorial.d.ts index fb971c533..d99d083d8 100644 --- a/src/InteractiveTutorial.d.ts +++ b/src/InteractiveTutorial.d.ts @@ -1,3 +1,3 @@ export declare function iTutorialNextStep(): void; -export declare const ITutorial: {isRunning: boolean, currStep: number}; +export declare const ITutorial: {isRunning: boolean; currStep: number}; export declare const iTutorialSteps: {[key: string]: number}; \ No newline at end of file diff --git a/src/NetscriptFunctions.js b/src/NetscriptFunctions.js index 75e88b260..df2417a78 100644 --- a/src/NetscriptFunctions.js +++ b/src/NetscriptFunctions.js @@ -20,6 +20,18 @@ import { CompanyPosition } from "./Company/CompanyPosition"; import { CompanyPositions } from "./Company/CompanyPositions"; import { CONSTANTS } from "./Constants"; import { DarkWebItems } from "./DarkWeb/DarkWebItems"; +import { + NewIndustry, + NewCity, + UnlockUpgrade, + LevelUpgrade, + IssueDividends, + SellMaterial, + SellProduct, + SetSmartSupply, + BuyMaterial } from "./Corporation/Actions"; +import { CorporationUnlockUpgrades } from "./Corporation/data/CorporationUnlockUpgrades"; +import { CorporationUpgrades } from "./Corporation/data/CorporationUpgrades"; import { calculateHackingChance, calculateHackingExpGain, @@ -555,6 +567,39 @@ function NetscriptFunctions(workerScript) { return Augmentations[name]; } + function getDivision(divisionName) { + const division = Player.corporation.divisions.find(div => div.name === divisionName); + if(division === undefined) + throw new Error(`No division named '${divisionName}'`); + return division; + } + + function getWarehouse(divisionName, cityName) { + const division = getDivision(divisionName); + if(!(cityName in division.warehouses)) + throw new Error(`Invalid city name '${cityName}'`); + const warehouse = division.warehouses[cityName]; + if(warehouse === 0) + throw new Error(`${division.name} has not expanded to '${cityName}'`); + return warehouse; + } + + function getMaterial(divisionName, cityName, materialName) { + const warehouse = getWarehouse(divisionName, cityName); + const material = warehouse.materials[materialName]; + if(material === undefined) + throw new Error(`Invalid material name: '${materialName}'`); + return material; + } + + function getProduct(divisionName, productName) { + const division = getDivision(divisionName); + const product = division.products[productName]; + if(product === undefined) + throw new Error(`Invalid product name: '${productName}'`); + return product; + } + const runAfterReset = function(cbScript=null) { //Run a script after reset if (cbScript && isString(cbScript)) { @@ -4097,6 +4142,46 @@ function NetscriptFunctions(workerScript) { }, }, // End Bladeburner + // corporation: { + // expandIndustry: function(industryName, divisionName) { + // NewIndustry(Player.corporation, industryName, divisionName); + // }, + // expandCity: function(divisionName, cityName) { + // const division = getDivision(divisionName); + // NewCity(Player.corporation, division, cityName); + // }, + // unlockUpgrade: function(upgradeName) { + // const upgrade = Object.values(CorporationUnlockUpgrades). + // find(upgrade => upgrade[2] === upgradeName); + // if(upgrade === undefined) throw new Error("No upgrade named '${upgradeName}'") + // UnlockUpgrade(Player.corporation, upgrade); + // }, + // levelUpgrade: function(upgradeName) { + // const upgrade = Object.values(CorporationUpgrades). + // find(upgrade => upgrade[4] === upgradeName); + // if(upgrade === undefined) throw new Error("No upgrade named '${upgradeName}'") + // LevelUpgrade(Player.corporation, upgrade); + // }, + // issueDividends: function(percent) { + // IssueDividends(Player.corporation, percent); + // }, + // sellMaterial: function(divisionName, cityName, materialName, amt, price) { + // const material = getMaterial(divisionName, cityName, materialName); + // SellMaterial(material, amt, price); + // }, + // sellProduct: function(divisionName, cityName, productName, amt, price, all) { + // const product = getProduct(divisionName, productName); + // SellProduct(product, cityName, amt, price, all); + // }, + // setSmartSupply: function(divisionName, cityName, enabled) { + // const warehouse = getWarehouse(divisionName, cityName); + // SetSmartSupply(warehouse, enabled); + // }, + // BuyMaterial: function(divisionName, cityName, materialName, amt) { + + // }, + // }, // End Corporation API + // Coding Contract API codingcontract: { attempt: function(answer, fn, ip=workerScript.serverIp, { returnReward } = {}) { diff --git a/src/PersonObjects/IPlayer.ts b/src/PersonObjects/IPlayer.ts index 76894ed75..bed14af84 100644 --- a/src/PersonObjects/IPlayer.ts +++ b/src/PersonObjects/IPlayer.ts @@ -19,7 +19,8 @@ import { LocationName } from "../Locations/data/LocationNames"; import { Server } from "../Server/Server"; import { IPlayerOwnedSourceFile } from "../SourceFile/PlayerOwnedSourceFile"; import { MoneySourceTracker } from "../utils/MoneySourceTracker"; -import { Exploit } from "../Exploits/Exploit"; +import { Exploit } from "../Exploits/Exploit"; +import { ICorporation } from "../Corporation/ICorporation"; export interface IPlayer { // Class members @@ -28,7 +29,7 @@ export interface IPlayer { bitNodeN: number; city: CityName; companyName: string; - corporation: any; + corporation: ICorporation; currentServer: string; factions: string[]; factionInvitations: string[]; diff --git a/src/ThirdParty/Treant.js b/src/ThirdParty/Treant.js deleted file mode 100644 index 2ac51885a..000000000 --- a/src/ThirdParty/Treant.js +++ /dev/null @@ -1,2171 +0,0 @@ -/* - * Treant-js - * - * (c) 2013 Fran Peručić - * Treant-js may be freely distributed under the MIT license. - * For all details and documentation: - * http://fperucic.github.io/treant-js - * - * Treant is an open-source JavaScipt library for visualization of tree diagrams. - * It implements the node positioning algorithm of John Q. Walker II "Positioning nodes for General Trees". - * - * References: - * Emilio Cortegoso Lobato: ECOTree.js v1.0 (October 26th, 2006) - * - * Contributors: - * Fran Peručić, https://github.com/fperucic - * Dave Goodchild, https://github.com/dlgoodchild - */ - -( function() { - // Polyfill for IE to use startsWith - if (!String.prototype.startsWith) { - String.prototype.startsWith = function(searchString, position){ - return this.substr(position || 0, searchString.length) === searchString; - }; - } - - var $ = null; - - var UTIL = { - - /** - * Directly updates, recursively/deeply, the first object with all properties in the second object - * @param {object} applyTo - * @param {object} applyFrom - * @return {object} - */ - inheritAttrs: function( applyTo, applyFrom ) { - for ( var attr in applyFrom ) { - if ( applyFrom.hasOwnProperty( attr ) ) { - if ( ( applyTo[attr] instanceof Object && applyFrom[attr] instanceof Object ) && ( typeof applyFrom[attr] !== 'function' ) ) { - this.inheritAttrs( applyTo[attr], applyFrom[attr] ); - } - else { - applyTo[attr] = applyFrom[attr]; - } - } - } - return applyTo; - }, - - /** - * Returns a new object by merging the two supplied objects - * @param {object} obj1 - * @param {object} obj2 - * @returns {object} - */ - createMerge: function( obj1, obj2 ) { - var newObj = {}; - if ( obj1 ) { - this.inheritAttrs( newObj, this.cloneObj( obj1 ) ); - } - if ( obj2 ) { - this.inheritAttrs( newObj, obj2 ); - } - return newObj; - }, - - /** - * Takes any number of arguments - * @returns {*} - */ - extend: function() { - if ( $ ) { - Array.prototype.unshift.apply( arguments, [true, {}] ); - return $.extend.apply( $, arguments ); - } - else { - return UTIL.createMerge.apply( this, arguments ); - } - }, - - /** - * @param {object} obj - * @returns {*} - */ - cloneObj: function ( obj ) { - if ( Object( obj ) !== obj ) { - return obj; - } - var res = new obj.constructor(); - for ( var key in obj ) { - if ( obj.hasOwnProperty(key) ) { - res[key] = this.cloneObj(obj[key]); - } - } - return res; - }, - - /** - * @param {Element} el - * @param {string} eventType - * @param {function} handler - */ - addEvent: function( el, eventType, handler ) { - if ( $ ) { - $( el ).on( eventType+'.treant', handler ); - } - else if ( el.addEventListener ) { // DOM Level 2 browsers - el.addEventListener( eventType, handler, false ); - } - else if ( el.attachEvent ) { // IE <= 8 - el.attachEvent( 'on' + eventType, handler ); - } - else { // ancient browsers - el['on' + eventType] = handler; - } - }, - - /** - * @param {string} selector - * @param {boolean} raw - * @param {Element} parentEl - * @returns {Element|jQuery} - */ - findEl: function( selector, raw, parentEl ) { - parentEl = parentEl || document; - - if ( $ ) { - var $element = $( selector, parentEl ); - return ( raw? $element.get( 0 ): $element ); - } - else { - // todo: getElementsByName() - // todo: getElementsByTagName() - // todo: getElementsByTagNameNS() - if ( selector.charAt( 0 ) === '#' ) { - return parentEl.getElementById( selector.substring( 1 ) ); - } - else if ( selector.charAt( 0 ) === '.' ) { - var oElements = parentEl.getElementsByClassName( selector.substring( 1 ) ); - return ( oElements.length? oElements[0]: null ); - } - - throw new Error( 'Unknown container element' ); - } - }, - - getOuterHeight: function( element ) { - var nRoundingCompensation = 1; - if ( typeof element.getBoundingClientRect === 'function' ) { - return element.getBoundingClientRect().height; - } - else if ( $ ) { - return Math.ceil( $( element ).outerHeight() ) + nRoundingCompensation; - } - else { - return Math.ceil( - element.clientHeight - + UTIL.getStyle( element, 'border-top-width', true ) - + UTIL.getStyle( element, 'border-bottom-width', true ) - + UTIL.getStyle( element, 'padding-top', true ) - + UTIL.getStyle( element, 'padding-bottom', true ) - + nRoundingCompensation, - ); - } - }, - - getOuterWidth: function( element ) { - var nRoundingCompensation = 1; - if ( typeof element.getBoundingClientRect === 'function' ) { - return element.getBoundingClientRect().width; - } - else if ( $ ) { - return Math.ceil( $( element ).outerWidth() ) + nRoundingCompensation; - } - else { - return Math.ceil( - element.clientWidth - + UTIL.getStyle( element, 'border-left-width', true ) - + UTIL.getStyle( element, 'border-right-width', true ) - + UTIL.getStyle( element, 'padding-left', true ) - + UTIL.getStyle( element, 'padding-right', true ) - + nRoundingCompensation, - ); - } - }, - - getStyle: function( element, strCssRule, asInt ) { - var strValue = ""; - if ( document.defaultView && document.defaultView.getComputedStyle ) { - strValue = document.defaultView.getComputedStyle( element, '' ).getPropertyValue( strCssRule ); - } - else if( element.currentStyle ) { - strCssRule = strCssRule.replace(/\-(\w)/g, - function (strMatch, p1){ - return p1.toUpperCase(); - }, - ); - strValue = element.currentStyle[strCssRule]; - } - //Number(elem.style.width.replace(/[^\d\.\-]/g, '')); - return ( asInt? parseFloat( strValue ): strValue ); - }, - - addClass: function( element, cssClass ) { - if ( $ ) { - $( element ).addClass( cssClass ); - } - else { - if ( !UTIL.hasClass( element, cssClass ) ) { - if ( element.classList ) { - element.classList.add( cssClass ); - } - else { - element.className += " "+cssClass; - } - } - } - }, - - hasClass: function(element, my_class) { - return (" " + element.className + " ").replace(/[\n\t]/g, " ").indexOf(" "+my_class+" ") > -1; - }, - - toggleClass: function ( element, cls, apply ) { - if ( $ ) { - $( element ).toggleClass( cls, apply ); - } - else { - if ( apply ) { - //element.className += " "+cls; - element.classList.add( cls ); - } - else { - element.classList.remove( cls ); - } - } - }, - - setDimensions: function( element, width, height ) { - if ( $ ) { - $( element ).width( width ).height( height ); - } - else { - element.style.width = width+'px'; - element.style.height = height+'px'; - } - }, - isjQueryAvailable: function() {return(typeof ($) !== 'undefined' && $);}, - }; - - /** - * ImageLoader is used for determining if all the images from the Tree are loaded. - * Node size (width, height) can be correctly determined only when all inner images are loaded - */ - var ImageLoader = function() { - this.reset(); - }; - - ImageLoader.prototype = { - - /** - * @returns {ImageLoader} - */ - reset: function() { - this.loading = []; - return this; - }, - - /** - * @param {TreeNode} node - * @returns {ImageLoader} - */ - processNode: function( node ) { - var aImages = node.nodeDOM.getElementsByTagName( 'img' ); - - var i = aImages.length; - while ( i-- ) { - this.create( node, aImages[i] ); - } - return this; - }, - - /** - * @returns {ImageLoader} - */ - removeAll: function( img_src ) { - var i = this.loading.length; - while ( i-- ) { - if ( this.loading[i] === img_src ) { - this.loading.splice( i, 1 ); - } - } - return this; - }, - - /** - * @param {TreeNode} node - * @param {Element} image - * @returns {*} - */ - create: function ( node, image ) { - var self = this, source = image.src; - - function imgTrigger() { - self.removeAll( source ); - node.width = node.nodeDOM.offsetWidth; - node.height = node.nodeDOM.offsetHeight; - } - - if ( image.src.indexOf( 'data:' ) !== 0 ) { - this.loading.push( source ); - - if ( image.complete ) { - return imgTrigger(); - } - - UTIL.addEvent( image, 'load', imgTrigger ); - UTIL.addEvent( image, 'error', imgTrigger ); // handle broken url-s - - // load event is not fired for cached images, force the load event - image.src += ( ( image.src.indexOf( '?' ) > 0)? '&': '?' ) + new Date().getTime(); - } - else { - imgTrigger(); - } - }, - - /** - * @returns {boolean} - */ - isNotLoading: function() { - return ( this.loading.length === 0 ); - }, - }; - - /** - * Class: TreeStore - * TreeStore is used for holding initialized Tree objects - * Its purpose is to avoid global variables and enable multiple Trees on the page. - */ - var TreeStore = { - - store: [], - - /** - * @param {object} jsonConfig - * @returns {Tree} - */ - createTree: function( jsonConfig ) { - var nNewTreeId = this.store.length; - this.store.push( new Tree( jsonConfig, nNewTreeId ) ); - return this.get( nNewTreeId ); - }, - - /** - * @param {number} treeId - * @returns {Tree} - */ - get: function ( treeId ) { - return this.store[treeId]; - }, - - /** - * @param {number} treeId - * @returns {TreeStore} - */ - destroy: function( treeId ) { - var tree = this.get( treeId ); - if ( tree ) { - tree._R.remove(); - var draw_area = tree.drawArea; - - while ( draw_area.firstChild ) { - draw_area.removeChild( draw_area.firstChild ); - } - - var classes = draw_area.className.split(' '), - classes_to_stay = []; - - for ( var i = 0; i < classes.length; i++ ) { - var cls = classes[i]; - if ( cls !== 'Treant' && cls !== 'Treant-loaded' ) { - classes_to_stay.push(cls); - } - } - draw_area.style.overflowY = ''; - draw_area.style.overflowX = ''; - draw_area.className = classes_to_stay.join(' '); - - this.store[treeId] = null; - } - return this; - }, - }; - - /** - * Tree constructor. - * @param {object} jsonConfig - * @param {number} treeId - * @constructor - */ - var Tree = function (jsonConfig, treeId ) { - - /** - * @param {object} jsonConfig - * @param {number} treeId - * @returns {Tree} - */ - this.reset = function( jsonConfig, treeId ) { - this.initJsonConfig = jsonConfig; - this.initTreeId = treeId; - - this.id = treeId; - - this.CONFIG = UTIL.extend( Tree.CONFIG, jsonConfig.chart ); - this.drawArea = UTIL.findEl( this.CONFIG.container, true ); - if ( !this.drawArea ) { - throw new Error( 'Failed to find element by selector "'+this.CONFIG.container+'"' ); - } - - UTIL.addClass( this.drawArea, 'Treant' ); - - // kill of any child elements that may be there - this.drawArea.innerHTML = ''; - - this.imageLoader = new ImageLoader(); - - this.nodeDB = new NodeDB( jsonConfig.nodeStructure, this ); - - // key store for storing reference to node connectors, - // key = nodeId where the connector ends - this.connectionStore = {}; - - this.loaded = false; - - this._R = new Raphael( this.drawArea, 100, 100 ); - - return this; - }; - - /** - * @returns {Tree} - */ - this.reload = function() { - this.reset( this.initJsonConfig, this.initTreeId ).redraw(); - return this; - }; - - this.reset( jsonConfig, treeId ); - }; - - Tree.prototype = { - - /** - * @returns {NodeDB} - */ - getNodeDb: function() { - return this.nodeDB; - }, - - /** - * @param {TreeNode} parentTreeNode - * @param {object} nodeDefinition - * @returns {TreeNode} - */ - addNode: function( parentTreeNode, nodeDefinition ) { - var dbEntry = this.nodeDB.get( parentTreeNode.id ); - - this.CONFIG.callback.onBeforeAddNode.apply( this, [parentTreeNode, nodeDefinition] ); - - var oNewNode = this.nodeDB.createNode( nodeDefinition, parentTreeNode.id, this ); - oNewNode.createGeometry( this ); - - oNewNode.parent().createSwitchGeometry( this ); - - this.positionTree(); - - this.CONFIG.callback.onAfterAddNode.apply( this, [oNewNode, parentTreeNode, nodeDefinition] ); - - return oNewNode; - }, - - /** - * @returns {Tree} - */ - redraw: function() { - this.positionTree(); - return this; - }, - - /** - * @param {function} callback - * @returns {Tree} - */ - positionTree: function( callback ) { - var self = this; - - if ( this.imageLoader.isNotLoading() ) { - var root = this.root(), - orient = this.CONFIG.rootOrientation; - - this.resetLevelData(); - - this.firstWalk( root, 0 ); - this.secondWalk( root, 0, 0, 0 ); - - this.positionNodes(); - - if ( this.CONFIG.animateOnInit ) { - setTimeout( - function() { - root.toggleCollapse(); - }, - this.CONFIG.animateOnInitDelay, - ); - } - - if ( !this.loaded ) { - UTIL.addClass( this.drawArea, 'Treant-loaded' ); // nodes are hidden until .loaded class is added - if ( Object.prototype.toString.call( callback ) === "[object Function]" ) { - callback( self ); - } - self.CONFIG.callback.onTreeLoaded.apply( self, [root] ); - this.loaded = true; - } - - } - else { - setTimeout( - function() { - self.positionTree( callback ); - }, 10, - ); - } - return this; - }, - - /** - * In a first post-order walk, every node of the tree is assigned a preliminary - * x-coordinate (held in field node->prelim). - * In addition, internal nodes are given modifiers, which will be used to move their - * children to the right (held in field node->modifier). - * @param {TreeNode} node - * @param {number} level - * @returns {Tree} - */ - firstWalk: function( node, level ) { - node.prelim = null; - node.modifier = null; - - this.setNeighbors( node, level ); - this.calcLevelDim( node, level ); - - var leftSibling = node.leftSibling(); - - if ( node.childrenCount() === 0 || level == this.CONFIG.maxDepth ) { - // set preliminary x-coordinate - if ( leftSibling ) { - node.prelim = leftSibling.prelim + leftSibling.size() + this.CONFIG.siblingSeparation; - } - else { - node.prelim = 0; - } - } - else { - //node is not a leaf, firstWalk for each child - for ( var i = 0, n = node.childrenCount(); i < n; i++ ) { - this.firstWalk(node.childAt(i), level + 1); - } - - var midPoint = node.childrenCenter() - node.size() / 2; - - if ( leftSibling ) { - node.prelim = leftSibling.prelim + leftSibling.size() + this.CONFIG.siblingSeparation; - node.modifier = node.prelim - midPoint; - this.apportion( node, level ); - } - else { - node.prelim = midPoint; - } - - // handle stacked children positioning - if ( node.stackParent ) { // handle the parent of stacked children - node.modifier += this.nodeDB.get( node.stackChildren[0] ).size()/2 + node.connStyle.stackIndent; - } - else if ( node.stackParentId ) { // handle stacked children - node.prelim = 0; - } - } - return this; - }, - - /* - * Clean up the positioning of small sibling subtrees. - * Subtrees of a node are formed independently and - * placed as close together as possible. By requiring - * that the subtrees be rigid at the time they are put - * together, we avoid the undesirable effects that can - * accrue from positioning nodes rather than subtrees. - */ - apportion: function (node, level) { - var firstChild = node.firstChild(), - firstChildLeftNeighbor = firstChild.leftNeighbor(), - compareDepth = 1, - depthToStop = this.CONFIG.maxDepth - level; - - while( firstChild && firstChildLeftNeighbor && compareDepth <= depthToStop ) { - // calculate the position of the firstChild, according to the position of firstChildLeftNeighbor - - var modifierSumRight = 0, - modifierSumLeft = 0, - leftAncestor = firstChildLeftNeighbor, - rightAncestor = firstChild; - - for ( var i = 0; i < compareDepth; i++ ) { - leftAncestor = leftAncestor.parent(); - rightAncestor = rightAncestor.parent(); - modifierSumLeft += leftAncestor.modifier; - modifierSumRight += rightAncestor.modifier; - - // all the stacked children are oriented towards right so use right variables - if ( rightAncestor.stackParent !== undefined ) { - modifierSumRight += rightAncestor.size() / 2; - } - } - - // find the gap between two trees and apply it to subTrees - // and mathing smaller gaps to smaller subtrees - - var totalGap = (firstChildLeftNeighbor.prelim + modifierSumLeft + firstChildLeftNeighbor.size() + this.CONFIG.subTeeSeparation) - (firstChild.prelim + modifierSumRight ); - - if ( totalGap > 0 ) { - var subtreeAux = node, - numSubtrees = 0; - - // count all the subtrees in the LeftSibling - while ( subtreeAux && subtreeAux.id !== leftAncestor.id ) { - subtreeAux = subtreeAux.leftSibling(); - numSubtrees++; - } - - if ( subtreeAux ) { - var subtreeMoveAux = node, - singleGap = totalGap / numSubtrees; - - while ( subtreeMoveAux.id !== leftAncestor.id ) { - subtreeMoveAux.prelim += totalGap; - subtreeMoveAux.modifier += totalGap; - - totalGap -= singleGap; - subtreeMoveAux = subtreeMoveAux.leftSibling(); - } - } - } - - compareDepth++; - - firstChild = ( firstChild.childrenCount() === 0 )? - node.leftMost(0, compareDepth): - firstChild = firstChild.firstChild(); - - if ( firstChild ) { - firstChildLeftNeighbor = firstChild.leftNeighbor(); - } - } - }, - - /* - * During a second pre-order walk, each node is given a - * final x-coordinate by summing its preliminary - * x-coordinate and the modifiers of all the node's - * ancestors. The y-coordinate depends on the height of - * the tree. (The roles of x and y are reversed for - * RootOrientations of EAST or WEST.) - */ - secondWalk: function( node, level, X, Y ) { - if ( level <= this.CONFIG.maxDepth ) { - var xTmp = node.prelim + X, - yTmp = Y, align = this.CONFIG.nodeAlign, - orient = this.CONFIG.rootOrientation, - levelHeight, nodesizeTmp; - - if (orient === 'NORTH' || orient === 'SOUTH') { - levelHeight = this.levelMaxDim[level].height; - nodesizeTmp = node.height; - if (node.pseudo) { - node.height = levelHeight; - } // assign a new size to pseudo nodes - } - else if (orient === 'WEST' || orient === 'EAST') { - levelHeight = this.levelMaxDim[level].width; - nodesizeTmp = node.width; - if (node.pseudo) { - node.width = levelHeight; - } // assign a new size to pseudo nodes - } - - node.X = xTmp; - - if (node.pseudo) { // pseudo nodes need to be properly aligned, otherwise position is not correct in some examples - if (orient === 'NORTH' || orient === 'WEST') { - node.Y = yTmp; // align "BOTTOM" - } - else if (orient === 'SOUTH' || orient === 'EAST') { - node.Y = (yTmp + (levelHeight - nodesizeTmp)); // align "TOP" - } - - } else { - node.Y = ( align === 'CENTER' ) ? (yTmp + (levelHeight - nodesizeTmp) / 2) : - ( align === 'TOP' ) ? (yTmp + (levelHeight - nodesizeTmp)) : - yTmp; - } - - if ( orient === 'WEST' || orient === 'EAST' ) { - var swapTmp = node.X; - node.X = node.Y; - node.Y = swapTmp; - } - - if (orient === 'SOUTH' ) { - node.Y = -node.Y - nodesizeTmp; - } - else if ( orient === 'EAST' ) { - node.X = -node.X - nodesizeTmp; - } - - if ( node.childrenCount() !== 0 ) { - if ( node.id === 0 && this.CONFIG.hideRootNode ) { - // ako je root node Hiden onda nemoj njegovu dijecu pomaknut po Y osi za Level separation, neka ona budu na vrhu - this.secondWalk(node.firstChild(), level + 1, X + node.modifier, Y); - } - else { - this.secondWalk(node.firstChild(), level + 1, X + node.modifier, Y + levelHeight + this.CONFIG.levelSeparation); - } - } - - if ( node.rightSibling() ) { - this.secondWalk( node.rightSibling(), level, X, Y ); - } - } - }, - - /** - * position all the nodes, center the tree in center of its container - * 0,0 coordinate is in the upper left corner - * @returns {Tree} - */ - positionNodes: function() { - var self = this, - treeSize = { - x: self.nodeDB.getMinMaxCoord('X', null, null), - y: self.nodeDB.getMinMaxCoord('Y', null, null), - }, - - treeWidth = treeSize.x.max - treeSize.x.min, - treeHeight = treeSize.y.max - treeSize.y.min, - - treeCenter = { - x: treeSize.x.max - treeWidth/2, - y: treeSize.y.max - treeHeight/2, - }; - - this.handleOverflow(treeWidth, treeHeight); - - var - containerCenter = { - x: self.drawArea.clientWidth/2, - y: self.drawArea.clientHeight/2, - }, - - deltaX = containerCenter.x - treeCenter.x, - deltaY = containerCenter.y - treeCenter.y, - - // all nodes must have positive X or Y coordinates, handle this with offsets - negOffsetX = ((treeSize.x.min + deltaX) <= 0) ? Math.abs(treeSize.x.min) : 0, - negOffsetY = ((treeSize.y.min + deltaY) <= 0) ? Math.abs(treeSize.y.min) : 0, - i, len, node; - - // position all the nodes - for ( i = 0, len = this.nodeDB.db.length; i < len; i++ ) { - - node = this.nodeDB.get(i); - - self.CONFIG.callback.onBeforePositionNode.apply( self, [node, i, containerCenter, treeCenter] ); - - if ( node.id === 0 && this.CONFIG.hideRootNode ) { - self.CONFIG.callback.onAfterPositionNode.apply( self, [node, i, containerCenter, treeCenter] ); - continue; - } - - // if the tree is smaller than the draw area, then center the tree within drawing area - node.X += negOffsetX + ((treeWidth < this.drawArea.clientWidth) ? deltaX : this.CONFIG.padding); - node.Y += negOffsetY + ((treeHeight < this.drawArea.clientHeight) ? deltaY : this.CONFIG.padding); - - var collapsedParent = node.collapsedParent(), - hidePoint = null; - - if (collapsedParent) { - // position the node behind the connector point of the parent, so future animations can be visible - hidePoint = collapsedParent.connectorPoint( true ); - node.hide(hidePoint); - - } - else if (node.positioned) { - // node is already positioned, - node.show(); - } - else { // inicijalno stvaranje nodeova, postavi lokaciju - node.nodeDOM.style.left = node.X + 'px'; - node.nodeDOM.style.top = node.Y + 'px'; - node.positioned = true; - } - - if (node.id !== 0 && !(node.parent().id === 0 && this.CONFIG.hideRootNode)) { - this.setConnectionToParent(node, hidePoint); // skip the root node - } - else if (!this.CONFIG.hideRootNode && node.drawLineThrough) { - // drawlinethrough is performed for for the root node also - node.drawLineThroughMe(); - } - - self.CONFIG.callback.onAfterPositionNode.apply( self, [node, i, containerCenter, treeCenter] ); - } - return this; - }, - - /** - * Create Raphael instance, (optionally set scroll bars if necessary) - * @param {number} treeWidth - * @param {number} treeHeight - * @returns {Tree} - */ - handleOverflow: function( treeWidth, treeHeight ) { - var viewWidth = (treeWidth < this.drawArea.clientWidth) ? this.drawArea.clientWidth : treeWidth + this.CONFIG.padding*2, - viewHeight = (treeHeight < this.drawArea.clientHeight) ? this.drawArea.clientHeight : treeHeight + this.CONFIG.padding*2; - - this._R.setSize( viewWidth, viewHeight ); - - if ( this.CONFIG.scrollbar === 'resize') { - UTIL.setDimensions( this.drawArea, viewWidth, viewHeight ); - } - else if ( !UTIL.isjQueryAvailable() || this.CONFIG.scrollbar === 'native' ) { - - if ( this.drawArea.clientWidth < treeWidth ) { // is overflow-x necessary - this.drawArea.style.overflowX = "auto"; - } - - if ( this.drawArea.clientHeight < treeHeight ) { // is overflow-y necessary - this.drawArea.style.overflowY = "auto"; - } - } - // Fancy scrollbar relies heavily on jQuery, so guarding with if ( $ ) - else if ( this.CONFIG.scrollbar === 'fancy') { - var jq_drawArea = $( this.drawArea ); - if (jq_drawArea.hasClass('ps-container')) { // znaci da je 'fancy' vec inicijaliziran, treba updateat - jq_drawArea.find('.Treant').css({ - width: viewWidth, - height: viewHeight, - }); - - jq_drawArea.perfectScrollbar('update'); - } - else { - var mainContainer = jq_drawArea.wrapInner('
'), - child = mainContainer.find('.Treant'); - - child.css({ - width: viewWidth, - height: viewHeight, - }); - - mainContainer.perfectScrollbar(); - } - } // else this.CONFIG.scrollbar == 'None' - - return this; - }, - /** - * @param {TreeNode} treeNode - * @param {boolean} hidePoint - * @returns {Tree} - */ - setConnectionToParent: function( treeNode, hidePoint ) { - var stacked = treeNode.stackParentId, - connLine, - parent = ( stacked? this.nodeDB.get( stacked ): treeNode.parent() ), - - pathString = hidePoint? - this.getPointPathString(hidePoint): - this.getPathString(parent, treeNode, stacked); - - if ( this.connectionStore[treeNode.id] ) { - // connector already exists, update the connector geometry - connLine = this.connectionStore[treeNode.id]; - this.animatePath( connLine, pathString ); - } - else { - connLine = this._R.path( pathString ); - this.connectionStore[treeNode.id] = connLine; - - // don't show connector arrows por pseudo nodes - if ( treeNode.pseudo ) { - delete parent.connStyle.style['arrow-end']; - } - if ( parent.pseudo ) { - delete parent.connStyle.style['arrow-start']; - } - - connLine.attr( parent.connStyle.style ); - - if ( treeNode.drawLineThrough || treeNode.pseudo ) { - treeNode.drawLineThroughMe( hidePoint ); - } - } - treeNode.connector = connLine; - return this; - }, - - /** - * Create the path which is represented as a point, used for hiding the connection - * A path with a leading "_" indicates the path will be hidden - * See: http://dmitrybaranovskiy.github.io/raphael/reference.html#Paper.path - * @param {object} hidePoint - * @returns {string} - */ - getPointPathString: function( hidePoint ) { - return ["_M", hidePoint.x, ",", hidePoint.y, 'L', hidePoint.x, ",", hidePoint.y, hidePoint.x, ",", hidePoint.y].join(' '); - }, - - /** - * This method relied on receiving a valid Raphael Paper.path. - * See: http://dmitrybaranovskiy.github.io/raphael/reference.html#Paper.path - * A pathString is typically in the format of "M10,20L30,40" - * @param path - * @param {string} pathString - * @returns {Tree} - */ - animatePath: function( path, pathString ) { - if (path.hidden && pathString.charAt(0) !== "_") { // path will be shown, so show it - path.show(); - path.hidden = false; - } - - // See: http://dmitrybaranovskiy.github.io/raphael/reference.html#Element.animate - path.animate( - { - path: pathString.charAt(0) === "_"? - pathString.substring(1): - pathString, // remove the "_" prefix if it exists - }, - this.CONFIG.animation.connectorsSpeed, - this.CONFIG.animation.connectorsAnimation, - function() { - if ( pathString.charAt(0) === "_" ) { // animation is hiding the path, hide it at the and of animation - path.hide(); - path.hidden = true; - } - }, - ); - return this; - }, - - /** - * - * @param {TreeNode} from_node - * @param {TreeNode} to_node - * @param {boolean} stacked - * @returns {string} - */ - getPathString: function( from_node, to_node, stacked ) { - var startPoint = from_node.connectorPoint( true ), - endPoint = to_node.connectorPoint( false ), - orientation = this.CONFIG.rootOrientation, - connType = from_node.connStyle.type, - P1 = {}, P2 = {}; - - if ( orientation === 'NORTH' || orientation === 'SOUTH' ) { - P1.y = P2.y = (startPoint.y + endPoint.y) / 2; - - P1.x = startPoint.x; - P2.x = endPoint.x; - } - else if ( orientation === 'EAST' || orientation === 'WEST' ) { - P1.x = P2.x = (startPoint.x + endPoint.x) / 2; - - P1.y = startPoint.y; - P2.y = endPoint.y; - } - - // sp, p1, pm, p2, ep == "x,y" - var sp = startPoint.x+','+startPoint.y, p1 = P1.x+','+P1.y, p2 = P2.x+','+P2.y, ep = endPoint.x+','+endPoint.y, - pm = (P1.x + P2.x)/2 +','+ (P1.y + P2.y)/2, pathString, stackPoint; - - if ( stacked ) { // STACKED CHILDREN - - stackPoint = (orientation === 'EAST' || orientation === 'WEST')? - endPoint.x+','+startPoint.y: - startPoint.x+','+endPoint.y; - - if ( connType === "step" || connType === "straight" ) { - pathString = ["M", sp, 'L', stackPoint, 'L', ep]; - } - else if ( connType === "curve" || connType === "bCurve" ) { - var helpPoint, // used for nicer curve lines - indent = from_node.connStyle.stackIndent; - - if ( orientation === 'NORTH' ) { - helpPoint = (endPoint.x - indent)+','+(endPoint.y - indent); - } - else if ( orientation === 'SOUTH' ) { - helpPoint = (endPoint.x - indent)+','+(endPoint.y + indent); - } - else if ( orientation === 'EAST' ) { - helpPoint = (endPoint.x + indent) +','+startPoint.y; - } - else if ( orientation === 'WEST' ) { - helpPoint = (endPoint.x - indent) +','+startPoint.y; - } - pathString = ["M", sp, 'L', helpPoint, 'S', stackPoint, ep]; - } - - } - else { // NORMAL CHILDREN - if ( connType === "step" ) { - pathString = ["M", sp, 'L', p1, 'L', p2, 'L', ep]; - } - else if ( connType === "curve" ) { - pathString = ["M", sp, 'C', p1, p2, ep ]; - } - else if ( connType === "bCurve" ) { - pathString = ["M", sp, 'Q', p1, pm, 'T', ep]; - } - else if (connType === "straight" ) { - pathString = ["M", sp, 'L', sp, ep]; - } - } - - return pathString.join(" "); - }, - - /** - * Algorithm works from left to right, so previous processed node will be left neighbour of the next node - * @param {TreeNode} node - * @param {number} level - * @returns {Tree} - */ - setNeighbors: function( node, level ) { - node.leftNeighborId = this.lastNodeOnLevel[level]; - if ( node.leftNeighborId ) { - node.leftNeighbor().rightNeighborId = node.id; - } - this.lastNodeOnLevel[level] = node.id; - return this; - }, - - /** - * Used for calculation of height and width of a level (level dimensions) - * @param {TreeNode} node - * @param {number} level - * @returns {Tree} - */ - calcLevelDim: function( node, level ) { // root node is on level 0 - this.levelMaxDim[level] = { - width: Math.max( this.levelMaxDim[level]? this.levelMaxDim[level].width: 0, node.width ), - height: Math.max( this.levelMaxDim[level]? this.levelMaxDim[level].height: 0, node.height ), - }; - return this; - }, - - /** - * @returns {Tree} - */ - resetLevelData: function() { - this.lastNodeOnLevel = []; - this.levelMaxDim = []; - return this; - }, - - /** - * @returns {TreeNode} - */ - root: function() { - return this.nodeDB.get( 0 ); - }, - }; - - /** - * NodeDB is used for storing the nodes. Each tree has its own NodeDB. - * @param {object} nodeStructure - * @param {Tree} tree - * @constructor - */ - var NodeDB = function ( nodeStructure, tree ) { - this.reset( nodeStructure, tree ); - }; - - NodeDB.prototype = { - - /** - * @param {object} nodeStructure - * @param {Tree} tree - * @returns {NodeDB} - */ - reset: function( nodeStructure, tree ) { - - this.db = []; - - var self = this; - - /** - * @param {object} node - * @param {number} parentId - */ - function iterateChildren( node, parentId ) { - var newNode = self.createNode( node, parentId, tree, null ); - - if ( node.children ) { - // pseudo node is used for descending children to the next level - if ( node.childrenDropLevel && node.childrenDropLevel > 0 ) { - while ( node.childrenDropLevel-- ) { - // pseudo node needs to inherit the connection style from its parent for continuous connectors - var connStyle = UTIL.cloneObj( newNode.connStyle ); - newNode = self.createNode( 'pseudo', newNode.id, tree, null ); - newNode.connStyle = connStyle; - newNode.children = []; - } - } - - var stack = ( node.stackChildren && !self.hasGrandChildren( node ) )? newNode.id: null; - - // children are positioned on separate levels, one beneath the other - if ( stack !== null ) { - newNode.stackChildren = []; - } - - for ( var i = 0, len = node.children.length; i < len ; i++ ) { - if ( stack !== null ) { - newNode = self.createNode( node.children[i], newNode.id, tree, stack ); - if ( ( i + 1 ) < len ) { - // last node cant have children - newNode.children = []; - } - } - else { - iterateChildren( node.children[i], newNode.id ); - } - } - } - } - - if ( tree.CONFIG.animateOnInit ) { - nodeStructure.collapsed = true; - } - - iterateChildren( nodeStructure, -1 ); // root node - - this.createGeometries( tree ); - - return this; - }, - - /** - * @param {Tree} tree - * @returns {NodeDB} - */ - createGeometries: function( tree ) { - var i = this.db.length; - - while ( i-- ) { - this.get( i ).createGeometry( tree ); - } - return this; - }, - - /** - * @param {number} nodeId - * @returns {TreeNode} - */ - get: function ( nodeId ) { - return this.db[nodeId]; // get TreeNode by ID - }, - - /** - * @param {function} callback - * @returns {NodeDB} - */ - walk: function( callback ) { - var i = this.db.length; - - while ( i-- ) { - callback.apply( this, [ this.get( i ) ] ); - } - return this; - }, - - /** - * - * @param {object} nodeStructure - * @param {number} parentId - * @param {Tree} tree - * @param {number} stackParentId - * @returns {TreeNode} - */ - createNode: function( nodeStructure, parentId, tree, stackParentId ) { - var node = new TreeNode( nodeStructure, this.db.length, parentId, tree, stackParentId ); - - this.db.push( node ); - - // skip root node (0) - if ( parentId >= 0 ) { - var parent = this.get( parentId ); - - // todo: refactor into separate private method - if ( nodeStructure.position ) { - if ( nodeStructure.position === 'left' ) { - parent.children.push( node.id ); - } - else if ( nodeStructure.position === 'right' ) { - parent.children.splice( 0, 0, node.id ); - } - else if ( nodeStructure.position === 'center' ) { - parent.children.splice( Math.floor( parent.children.length / 2 ), 0, node.id ); - } - else { - // edge case when there's only 1 child - var position = parseInt( nodeStructure.position ); - if ( parent.children.length === 1 && position > 0 ) { - parent.children.splice( 0, 0, node.id ); - } - else { - parent.children.splice( - Math.max( position, parent.children.length - 1 ), - 0, node.id, - ); - } - } - } - else { - parent.children.push( node.id ); - } - } - - if ( stackParentId ) { - this.get( stackParentId ).stackParent = true; - this.get( stackParentId ).stackChildren.push( node.id ); - } - - return node; - }, - - getMinMaxCoord: function( dim, parent, MinMax ) { // used for getting the dimensions of the tree, dim = 'X' || 'Y' - // looks for min and max (X and Y) within the set of nodes - parent = parent || this.get(0); - - MinMax = MinMax || { // start with root node dimensions - min: parent[dim], - max: parent[dim] + ( ( dim === 'X' )? parent.width: parent.height ), - }; - - var i = parent.childrenCount(); - - while ( i-- ) { - var node = parent.childAt( i ), - maxTest = node[dim] + ( ( dim === 'X' )? node.width: node.height ), - minTest = node[dim]; - - if ( maxTest > MinMax.max ) { - MinMax.max = maxTest; - } - if ( minTest < MinMax.min ) { - MinMax.min = minTest; - } - - this.getMinMaxCoord( dim, node, MinMax ); - } - return MinMax; - }, - - /** - * @param {object} nodeStructure - * @returns {boolean} - */ - hasGrandChildren: function( nodeStructure ) { - var i = nodeStructure.children.length; - while ( i-- ) { - if ( nodeStructure.children[i].children ) { - return true; - } - } - return false; - }, - }; - - /** - * TreeNode constructor. - * @param {object} nodeStructure - * @param {number} id - * @param {number} parentId - * @param {Tree} tree - * @param {number} stackParentId - * @constructor - */ - var TreeNode = function( nodeStructure, id, parentId, tree, stackParentId ) { - this.reset( nodeStructure, id, parentId, tree, stackParentId ); - }; - - TreeNode.prototype = { - - /** - * @param {object} nodeStructure - * @param {number} id - * @param {number} parentId - * @param {Tree} tree - * @param {number} stackParentId - * @returns {TreeNode} - */ - reset: function( nodeStructure, id, parentId, tree, stackParentId ) { - this.id = id; - this.parentId = parentId; - this.treeId = tree.id; - - this.prelim = 0; - this.modifier = 0; - this.leftNeighborId = null; - - this.stackParentId = stackParentId; - - // pseudo node is a node with width=height=0, it is invisible, but necessary for the correct positioning of the tree - this.pseudo = nodeStructure === 'pseudo' || nodeStructure['pseudo']; // todo: surely if nodeStructure is a scalar then the rest will error: - - this.meta = nodeStructure.meta || {}; - this.image = nodeStructure.image || null; - - this.link = UTIL.createMerge( tree.CONFIG.node.link, nodeStructure.link ); - - this.connStyle = UTIL.createMerge( tree.CONFIG.connectors, nodeStructure.connectors ); - this.connector = null; - - this.drawLineThrough = nodeStructure.drawLineThrough === false ? false : ( nodeStructure.drawLineThrough || tree.CONFIG.node.drawLineThrough ); - - this.collapsable = nodeStructure.collapsable === false ? false : ( nodeStructure.collapsable || tree.CONFIG.node.collapsable ); - this.collapsed = nodeStructure.collapsed; - - this.text = nodeStructure.text; - - // '.node' DIV - this.nodeInnerHTML = nodeStructure.innerHTML; - this.nodeHTMLclass = (tree.CONFIG.node.HTMLclass ? tree.CONFIG.node.HTMLclass : '') + // globally defined class for the nodex - (nodeStructure.HTMLclass ? (' ' + nodeStructure.HTMLclass) : ''); // + specific node class - - this.nodeHTMLid = nodeStructure.HTMLid; - - this.children = []; - - return this; - }, - - /** - * @returns {Tree} - */ - getTree: function() { - return TreeStore.get( this.treeId ); - }, - - /** - * @returns {object} - */ - getTreeConfig: function() { - return this.getTree().CONFIG; - }, - - /** - * @returns {NodeDB} - */ - getTreeNodeDb: function() { - return this.getTree().getNodeDb(); - }, - - /** - * @param {number} nodeId - * @returns {TreeNode} - */ - lookupNode: function( nodeId ) { - return this.getTreeNodeDb().get( nodeId ); - }, - - /** - * @returns {Tree} - */ - Tree: function() { - return TreeStore.get( this.treeId ); - }, - - /** - * @param {number} nodeId - * @returns {TreeNode} - */ - dbGet: function( nodeId ) { - return this.getTreeNodeDb().get( nodeId ); - }, - - /** - * Returns the width of the node - * @returns {float} - */ - size: function() { - var orientation = this.getTreeConfig().rootOrientation; - - if ( this.pseudo ) { - // prevents separating the subtrees - return ( -this.getTreeConfig().subTeeSeparation ); - } - - if ( orientation === 'NORTH' || orientation === 'SOUTH' ) { - return this.width; - } - else if ( orientation === 'WEST' || orientation === 'EAST' ) { - return this.height; - } - }, - - /** - * @returns {number} - */ - childrenCount: function () { - return ( ( this.collapsed || !this.children)? 0: this.children.length ); - }, - - /** - * @param {number} index - * @returns {TreeNode} - */ - childAt: function( index ) { - return this.dbGet( this.children[index] ); - }, - - /** - * @returns {TreeNode} - */ - firstChild: function() { - return this.childAt( 0 ); - }, - - /** - * @returns {TreeNode} - */ - lastChild: function() { - return this.childAt( this.children.length - 1 ); - }, - - /** - * @returns {TreeNode} - */ - parent: function() { - return this.lookupNode( this.parentId ); - }, - - /** - * @returns {TreeNode} - */ - leftNeighbor: function() { - if ( this.leftNeighborId ) { - return this.lookupNode( this.leftNeighborId ); - } - }, - - /** - * @returns {TreeNode} - */ - rightNeighbor: function() { - if ( this.rightNeighborId ) { - return this.lookupNode( this.rightNeighborId ); - } - }, - - /** - * @returns {TreeNode} - */ - leftSibling: function () { - var leftNeighbor = this.leftNeighbor(); - - if ( leftNeighbor && leftNeighbor.parentId === this.parentId ){ - return leftNeighbor; - } - }, - - /** - * @returns {TreeNode} - */ - rightSibling: function () { - var rightNeighbor = this.rightNeighbor(); - - if ( rightNeighbor && rightNeighbor.parentId === this.parentId ) { - return rightNeighbor; - } - }, - - /** - * @returns {number} - */ - childrenCenter: function () { - var first = this.firstChild(), - last = this.lastChild(); - - return ( first.prelim + ((last.prelim - first.prelim) + last.size()) / 2 ); - }, - - /** - * Find out if one of the node ancestors is collapsed - * @returns {*} - */ - collapsedParent: function() { - var parent = this.parent(); - if ( !parent ) { - return false; - } - if ( parent.collapsed ) { - return parent; - } - return parent.collapsedParent(); - }, - - /** - * Returns the leftmost child at specific level, (initial level = 0) - * @param level - * @param depth - * @returns {*} - */ - leftMost: function ( level, depth ) { - if ( level >= depth ) { - return this; - } - if ( this.childrenCount() === 0 ) { - return; - } - - for ( var i = 0, n = this.childrenCount(); i < n; i++ ) { - var leftmostDescendant = this.childAt( i ).leftMost( level + 1, depth ); - if ( leftmostDescendant ) { - return leftmostDescendant; - } - } - }, - - // returns start or the end point of the connector line, origin is upper-left - connectorPoint: function(startPoint) { - var orient = this.Tree().CONFIG.rootOrientation, point = {}; - - if ( this.stackParentId ) { // return different end point if node is a stacked child - if ( orient === 'NORTH' || orient === 'SOUTH' ) { - orient = 'WEST'; - } - else if ( orient === 'EAST' || orient === 'WEST' ) { - orient = 'NORTH'; - } - } - - // if pseudo, a virtual center is used - if ( orient === 'NORTH' ) { - point.x = (this.pseudo) ? this.X - this.Tree().CONFIG.subTeeSeparation/2 : this.X + this.width/2; - point.y = (startPoint) ? this.Y + this.height : this.Y; - } - else if (orient === 'SOUTH') { - point.x = (this.pseudo) ? this.X - this.Tree().CONFIG.subTeeSeparation/2 : this.X + this.width/2; - point.y = (startPoint) ? this.Y : this.Y + this.height; - } - else if (orient === 'EAST') { - point.x = (startPoint) ? this.X : this.X + this.width; - point.y = (this.pseudo) ? this.Y - this.Tree().CONFIG.subTeeSeparation/2 : this.Y + this.height/2; - } - else if (orient === 'WEST') { - point.x = (startPoint) ? this.X + this.width : this.X; - point.y = (this.pseudo) ? this.Y - this.Tree().CONFIG.subTeeSeparation/2 : this.Y + this.height/2; - } - return point; - }, - - /** - * @returns {string} - */ - pathStringThrough: function() { // get the geometry of a path going through the node - var startPoint = this.connectorPoint( true ), - endPoint = this.connectorPoint( false ); - - return ["M", startPoint.x+","+startPoint.y, 'L', endPoint.x+","+endPoint.y].join(" "); - }, - - /** - * @param {object} hidePoint - */ - drawLineThroughMe: function( hidePoint ) { // hidepoint se proslijedjuje ako je node sakriven zbog collapsed - var pathString = hidePoint? - this.Tree().getPointPathString( hidePoint ): - this.pathStringThrough(); - - this.lineThroughMe = this.lineThroughMe || this.Tree()._R.path(pathString); - - var line_style = UTIL.cloneObj( this.connStyle.style ); - - delete line_style['arrow-start']; - delete line_style['arrow-end']; - - this.lineThroughMe.attr( line_style ); - - if ( hidePoint ) { - this.lineThroughMe.hide(); - this.lineThroughMe.hidden = true; - } - }, - - addSwitchEvent: function( nodeSwitch ) { - var self = this; - UTIL.addEvent( nodeSwitch, 'click', - function( e ) { - e.preventDefault(); - if ( self.getTreeConfig().callback.onBeforeClickCollapseSwitch.apply( self, [ nodeSwitch, e ] ) === false ) { - return false; - } - - self.toggleCollapse(); - - self.getTreeConfig().callback.onAfterClickCollapseSwitch.apply( self, [ nodeSwitch, e ] ); - }, - ); - }, - - /** - * @returns {TreeNode} - */ - collapse: function() { - if ( !this.collapsed ) { - this.toggleCollapse(); - } - return this; - }, - - /** - * @returns {TreeNode} - */ - expand: function() { - if ( this.collapsed ) { - this.toggleCollapse(); - } - return this; - }, - - /** - * @returns {TreeNode} - */ - toggleCollapse: function() { - var oTree = this.getTree(); - - if ( !oTree.inAnimation ) { - oTree.inAnimation = true; - - this.collapsed = !this.collapsed; // toggle the collapse at each click - UTIL.toggleClass( this.nodeDOM, 'collapsed', this.collapsed ); - - oTree.positionTree(); - - var self = this; - - setTimeout( - function() { // set the flag after the animation - oTree.inAnimation = false; - oTree.CONFIG.callback.onToggleCollapseFinished.apply( oTree, [ self, self.collapsed ] ); - }, - ( oTree.CONFIG.animation.nodeSpeed > oTree.CONFIG.animation.connectorsSpeed )? - oTree.CONFIG.animation.nodeSpeed: - oTree.CONFIG.animation.connectorsSpeed, - ); - } - return this; - }, - - hide: function( collapse_to_point ) { - collapse_to_point = collapse_to_point || false; - - var bCurrentState = this.hidden; - this.hidden = true; - - this.nodeDOM.style.overflow = 'hidden'; - - var tree = this.getTree(), - config = this.getTreeConfig(), - oNewState = { - opacity: 0, - }; - - if ( collapse_to_point ) { - oNewState.left = collapse_to_point.x; - oNewState.top = collapse_to_point.y; - } - - // if parent was hidden in initial configuration, position the node behind the parent without animations - if ( !this.positioned || bCurrentState ) { - this.nodeDOM.style.visibility = 'hidden'; - if ( $ ) { - $( this.nodeDOM ).css( oNewState ); - } - else { - this.nodeDOM.style.left = oNewState.left + 'px'; - this.nodeDOM.style.top = oNewState.top + 'px'; - } - this.positioned = true; - } - else { - // todo: fix flashy bug when a node is manually hidden and tree.redraw is called. - if ( $ ) { - $( this.nodeDOM ).animate( - oNewState, config.animation.nodeSpeed, config.animation.nodeAnimation, - function () { - this.style.visibility = 'hidden'; - }, - ); - } - else { - this.nodeDOM.style.transition = 'all '+config.animation.nodeSpeed+'ms ease'; - this.nodeDOM.style.transitionProperty = 'opacity, left, top'; - this.nodeDOM.style.opacity = oNewState.opacity; - this.nodeDOM.style.left = oNewState.left + 'px'; - this.nodeDOM.style.top = oNewState.top + 'px'; - this.nodeDOM.style.visibility = 'hidden'; - } - } - - // animate the line through node if the line exists - if ( this.lineThroughMe ) { - var new_path = tree.getPointPathString( collapse_to_point ); - if ( bCurrentState ) { - // update without animations - this.lineThroughMe.attr( { path: new_path } ); - } - else { - // update with animations - tree.animatePath( this.lineThroughMe, tree.getPointPathString( collapse_to_point ) ); - } - } - - return this; - }, - - /** - * @returns {TreeNode} - */ - hideConnector: function() { - var oTree = this.Tree(); - var oPath = oTree.connectionStore[this.id]; - if ( oPath ) { - oPath.animate( - { 'opacity': 0 }, - oTree.CONFIG.animation.connectorsSpeed, - oTree.CONFIG.animation.connectorsAnimation, - ); - } - return this; - }, - - show: function() { - var bCurrentState = this.hidden; - this.hidden = false; - - this.nodeDOM.style.visibility = 'visible'; - - var oTree = this.Tree(); - - var oNewState = { - left: this.X, - top: this.Y, - opacity: 1, - }, - config = this.getTreeConfig(); - - // if the node was hidden, update opacity and position - if ( $ ) { - $( this.nodeDOM ).animate( - oNewState, - config.animation.nodeSpeed, config.animation.nodeAnimation, - function () { - // $.animate applies "overflow:hidden" to the node, remove it to avoid visual problems - this.style.overflow = ""; - }, - ); - } - else { - this.nodeDOM.style.transition = 'all '+config.animation.nodeSpeed+'ms ease'; - this.nodeDOM.style.transitionProperty = 'opacity, left, top'; - this.nodeDOM.style.left = oNewState.left + 'px'; - this.nodeDOM.style.top = oNewState.top + 'px'; - this.nodeDOM.style.opacity = oNewState.opacity; - this.nodeDOM.style.overflow = ''; - } - - if ( this.lineThroughMe ) { - this.getTree().animatePath( this.lineThroughMe, this.pathStringThrough() ); - } - - return this; - }, - - /** - * @returns {TreeNode} - */ - showConnector: function() { - var oTree = this.Tree(); - var oPath = oTree.connectionStore[this.id]; - if ( oPath ) { - oPath.animate( - { 'opacity': 1 }, - oTree.CONFIG.animation.connectorsSpeed, - oTree.CONFIG.animation.connectorsAnimation, - ); - } - return this; - }, - }; - - - /** - * Build a node from the 'text' and 'img' property and return with it. - * - * The node will contain all the fields that present under the 'text' property - * Each field will refer to a css class with name defined as node-{$property_name} - * - * Example: - * The definition: - * - * text: { - * desc: "some description", - * paragraph: "some text" - * } - * - * will generate the following elements: - * - *

some description

- *

some text

- * - * @Returns the configured node - */ - TreeNode.prototype.buildNodeFromText = function (node) { - // IMAGE - if (this.image) { - image = document.createElement('img'); - image.src = this.image; - node.appendChild(image); - } - - // TEXT - if (this.text) { - for (var key in this.text) { - // adding DATA Attributes to the node - if (key.startsWith("data-")) { - node.setAttribute(key, this.text[key]); - } else { - - var textElement = document.createElement(this.text[key].href ? 'a' : 'p'); - - // make an element if required - if (this.text[key].href) { - textElement.href = this.text[key].href; - if (this.text[key].target) { - textElement.target = this.text[key].target; - } - } - - textElement.className = "node-"+key; - textElement.appendChild(document.createTextNode( - this.text[key].val ? this.text[key].val : - this.text[key] instanceof Object ? "'val' param missing!" : this.text[key], - ), - ); - - node.appendChild(textElement); - } - } - } - return node; - }; - - /** - * Build a node from 'nodeInnerHTML' property that defines an existing HTML element, referenced by it's id, e.g: #someElement - * Change the text in the passed node to 'Wrong ID selector' if the referenced element does ot exist, - * return with a cloned and configured node otherwise - * - * @Returns node the configured node - */ - TreeNode.prototype.buildNodeFromHtml = function(node) { - // get some element by ID and clone its structure into a node - if (this.nodeInnerHTML.charAt(0) === "#") { - var elem = document.getElementById(this.nodeInnerHTML.substring(1)); - if (elem) { - node = elem.cloneNode(true); - node.id += "-clone"; - node.className += " node"; - } - else { - node.innerHTML = " Wrong ID selector "; - } - } - else { - // insert your custom HTML into a node - node.innerHTML = this.nodeInnerHTML; - } - return node; - }; - - /** - * @param {Tree} tree - */ - TreeNode.prototype.createGeometry = function( tree ) { - if ( this.id === 0 && tree.CONFIG.hideRootNode ) { - this.width = 0; - this.height = 0; - return; - } - - var drawArea = tree.drawArea, - image, - - /////////// CREATE NODE ////////////// - node = document.createElement( this.link.href? 'a': 'div' ); - - node.className = ( !this.pseudo )? TreeNode.CONFIG.nodeHTMLclass: 'pseudo'; - if ( this.nodeHTMLclass && !this.pseudo ) { - node.className += ' ' + this.nodeHTMLclass; - } - - if ( this.nodeHTMLid ) { - node.id = this.nodeHTMLid; - } - - if ( this.link.href ) { - node.href = this.link.href; - node.target = this.link.target; - } - - if ( $ ) { - $( node ).data( 'treenode', this ); - } - else { - node.data = { - 'treenode': this, - }; - } - - /////////// BUILD NODE CONTENT ////////////// - if ( !this.pseudo ) { - node = this.nodeInnerHTML? this.buildNodeFromHtml(node) : this.buildNodeFromText(node) - - // handle collapse switch - if ( this.collapsed || (this.collapsable && this.childrenCount() && !this.stackParentId) ) { - this.createSwitchGeometry( tree, node ); - } - } - - tree.CONFIG.callback.onCreateNode.apply( tree, [this, node] ); - - /////////// APPEND all ////////////// - drawArea.appendChild(node); - - this.width = node.offsetWidth; - this.height = node.offsetHeight; - - this.nodeDOM = node; - - tree.imageLoader.processNode(this); - }; - - /** - * @param {Tree} tree - * @param {Element} nodeEl - */ - TreeNode.prototype.createSwitchGeometry = function( tree, nodeEl ) { - nodeEl = nodeEl || this.nodeDOM; - - // safe guard and check to see if it has a collapse switch - var nodeSwitchEl = UTIL.findEl( '.collapse-switch', true, nodeEl ); - if ( !nodeSwitchEl ) { - nodeSwitchEl = document.createElement( 'a' ); - nodeSwitchEl.className = "collapse-switch"; - - nodeEl.appendChild( nodeSwitchEl ); - this.addSwitchEvent( nodeSwitchEl ); - if ( this.collapsed ) { - nodeEl.className += " collapsed"; - } - - tree.CONFIG.callback.onCreateNodeCollapseSwitch.apply( tree, [this, nodeEl, nodeSwitchEl] ); - } - return nodeSwitchEl; - }; - - - // ########################################### - // Expose global + default CONFIG params - // ########################################### - - - Tree.CONFIG = { - maxDepth: 100, - rootOrientation: 'NORTH', // NORTH || EAST || WEST || SOUTH - nodeAlign: 'CENTER', // CENTER || TOP || BOTTOM - levelSeparation: 30, - siblingSeparation: 30, - subTeeSeparation: 30, - - hideRootNode: false, - - animateOnInit: false, - animateOnInitDelay: 500, - - padding: 15, // the difference is seen only when the scrollbar is shown - scrollbar: 'native', // "native" || "fancy" || "None" (PS: "fancy" requires jquery and perfect-scrollbar) - - connectors: { - type: 'curve', // 'curve' || 'step' || 'straight' || 'bCurve' - style: { - stroke: 'black', - }, - stackIndent: 15, - }, - - node: { // each node inherits this, it can all be overridden in node config - - // HTMLclass: 'node', - // drawLineThrough: false, - // collapsable: false, - link: { - target: '_self', - }, - }, - - animation: { // each node inherits this, it can all be overridden in node config - nodeSpeed: 450, - nodeAnimation: 'linear', - connectorsSpeed: 450, - connectorsAnimation: 'linear', - }, - - callback: { - onCreateNode: function( treeNode, treeNodeDom ) {}, // this = Tree - onCreateNodeCollapseSwitch: function( treeNode, treeNodeDom, switchDom ) {}, // this = Tree - onAfterAddNode: function( newTreeNode, parentTreeNode, nodeStructure ) {}, // this = Tree - onBeforeAddNode: function( parentTreeNode, nodeStructure ) {}, // this = Tree - onAfterPositionNode: function( treeNode, nodeDbIndex, containerCenter, treeCenter) {}, // this = Tree - onBeforePositionNode: function( treeNode, nodeDbIndex, containerCenter, treeCenter) {}, // this = Tree - onToggleCollapseFinished: function ( treeNode, bIsCollapsed ) {}, // this = Tree - onAfterClickCollapseSwitch: function( nodeSwitch, event ) {}, // this = TreeNode - onBeforeClickCollapseSwitch: function( nodeSwitch, event ) {}, // this = TreeNode - onTreeLoaded: function( rootTreeNode ) {}, // this = Tree - }, - }; - - TreeNode.CONFIG = { - nodeHTMLclass: 'node', - }; - - // ############################################# - // Makes a JSON chart config out of Array config - // ############################################# - - var JSONconfig = { - make: function( configArray ) { - - var i = configArray.length, node; - - this.jsonStructure = { - chart: null, - nodeStructure: null, - }; - //fist loop: find config, find root; - while(i--) { - node = configArray[i]; - if (node.hasOwnProperty('container')) { - this.jsonStructure.chart = node; - continue; - } - - if (!node.hasOwnProperty('parent') && ! node.hasOwnProperty('container')) { - this.jsonStructure.nodeStructure = node; - node._json_id = 0; - } - } - - this.findChildren(configArray); - - return this.jsonStructure; - }, - - findChildren: function(nodes) { - var parents = [0]; // start with a a root node - - while(parents.length) { - var parentId = parents.pop(), - parent = this.findNode(this.jsonStructure.nodeStructure, parentId), - i = 0, len = nodes.length, - children = []; - - for(;i -