1 2const D=window.EVE_PI_DATA; 3const $=id=>document.getElementById(id); 4const products=new Map(D.products.map(p=>[p.id,p])); 5 6const PROJECT_SCHEMA="eve-pi-planner-project"; 7const PROJECT_VERSION=85; 8const METRIKA_COUNTER_ID=111613438; 9 10let current=null; 11let selected=[]; 12let clones=[{name:"Ðлон 1",limit:6,ccLevel:6}]; 13let authorizedClones=[],accountCsrf="",savedPlanCount=0; 14let planetSettings={}; 15let globalPlanetSettings={maxP0:99,p0p1:true,p1p2:true,p2p3:true,p3p4:true}; 16let productionSettings={separateP0:true,sharedP0Limit:1,separateFactories:false,factoryLimit:1}; 17let storageSettings={}; 18let planetTemplateSettings={}; 19let planetCounts=Object.fromEntries(D.meta.planetTypes.map(pt=>[pt,0])); 20let planetCountsDirty=false; 21let allocationMemo=new Map(); 22let targetColors={}; 23let marketSettings={enabled:false,hub:"Jita",tax:3.6,broker:0,customsTax:2,hauling:0}; 24let marketTrends=new Map(),marketPrices=new Map(),marketPriceDate=null,marketLoading=false,marketLoadToken=0; 25let profitabilityRankingCache=[],profitabilityRankingSignature="",profitabilityRankingVerifiedSignature=""; 26let profitabilityRankingPromise=null,profitabilityRankingPromiseSignature="",profitabilityRankingPromiseRevision=-1; 27let profitabilityRankingToken=0,profitabilityStateRevision=0,profitabilityRankingStage=""; 28let autosaveWarningShown=false; 29let autoProfitRunning=false,autoProfitToken=0; 30const trackedPlanSignatures=new Set(); 31let planetAutoApplyTimer=null; 32let selectedSystemInfo=null,systemLookupToken=0,systemMessageTimer=null; 33const MARKET_HUBS={ 34 Jita:{region:10000002,system:30000142}, 35 Amarr:{region:10000043,system:30002187}, 36 Dodixie:{region:10000032,system:30002659}, 37 Rens:{region:10000030,system:30002510} 38}; 39const PLANET_TYPE_BY_ID={11:"Temperate",12:"Ice",13:"Gas",2014:"Oceanic",2015:"Lava",2016:"Barren",2017:"Storm",2063:"Plasma"}; 40const SYSTEM_CACHE_TTL=24*60*60*1000; 41// ÐкÑÑалÑнÑе обÑÑÐ¼Ñ ÐµÐ´Ð¸Ð½Ð¸ÑÑ PI-маÑеÑиала в Tranquility (м³). 42const PRODUCT_VOLUME={0:.005,1:.19,2:.75,3:3,4:50}; 43const TRANSFER_EPSILON=1e-7; 44const STORAGE_CAPACITY={launchpad:10000,storage:12000}; 45const PLANET_STRUCTURE_TYPE_ID={ 46 Barren:{launchpad:2544,storage:2541},Gas:{launchpad:2543,storage:2536},Ice:{launchpad:2552,storage:2257},Lava:{launchpad:2555,storage:2558}, 47 Oceanic:{launchpad:2542,storage:2535},Plasma:{launchpad:2556,storage:2560},Storm:{launchpad:2557,storage:2561},Temperate:{launchpad:2256,storage:2562} 48}; 49function planetStructureTypeId(planetType,kind){ 50 const generated=window.PLANET_HUB_TEMPLATES?.structureTypeIds?.(planetType)?.[kind]; 51 return Number(generated)||(PLANET_STRUCTURE_TYPE_ID[planetType]||PLANET_STRUCTURE_TYPE_ID.Barren)[kind]; 52} 53// Fixed taxable values used by EVE Online customs offices (ISK per unit). 54const CUSTOMS_TAX_VALUE={0:5,1:400,2:7200,3:60000,4:1200000}; 55const TARGET_PALETTE=["#ff6b6b","#4dabf7","#ffd43b","#69db7c","#da77f2","#ff922b","#38d9a9","#748ffc","#f06595","#a9e34b","#22b8cf","#ffa8a8"]; 56 57let siteLanguage="ru"; 58try{siteLanguage=localStorage.getItem("planetHubLanguage")==="en"?"en":"ru"}catch(_error){} 59const originalText=new WeakMap(),originalAttributes=new WeakMap(); 60const EN_TRANSLATIONS=[ 61 ["PLANET HUB ÐланеÑаÑнÑй планиÑовÑик","PLANET HUB Planetary Industry Planner"], 62 ["ÐдеÑÑ Ð²Ñ Ð¼Ð¾Ð¶ÐµÑе бÑÑÑÑо опÑеделиÑÑ, ÑÑо можеÑе добÑваÑÑ Ð¸ пÑоизводиÑÑ","Quickly determine what you can extract and produce"], 63 ["иÑÑ Ð¾Ð´Ñ Ð¸Ð· имеÑÑÐ¸Ñ ÑÑ Ð¿Ð»Ð°Ð½ÐµÑ Ð² ÑиÑÑеме, колиÑеÑÑва клонов и планеÑ, коÑоÑÑе они могÑÑ Ð¾ÑвоиÑÑ.","from the planets in your system, the number of clones, and the planets they can colonize."], 64 ["ÐÐСТУÐÐЫРÐÐÐÐÐТЫ","AVAILABLE PLANETS"],["ÐолиÑеÑÑво Ð¿Ð»Ð°Ð½ÐµÑ Ð¿Ð¾ Ñипам","Planet counts by type"], 65 ["УкажиÑе ÑакÑиÑеÑкое колиÑеÑÑво планеÑ, коÑоÑÑми ÑаÑполагаеÑе.","Enter the actual number of planets available to you."], 66 ["ÐведиÑе ÑиÑÑемÑ","Enter a system"],["Ðазвание ÑолнеÑной ÑиÑÑемÑ","Solar system name"],["ÐайÑи","Find"], 67 ["ÐÐТÐÐЦÐÐÐ ÐÐ ÐÐÐÐÐÐСТÐÐ","PRODUCTION POTENTIAL"],["ÐÐÐÐЫ","CLONES"], 68 ["ÐоÑÑÑпнÑе клонÑ","Available clones"],["ÐÐ»Ñ ÐºÐ°Ð¶Ð´Ð¾Ð³Ð¾ клона ÑкажиÑе, ÑколÑко Ð¿Ð»Ð°Ð½ÐµÑ Ð¾Ð½ Ð¼Ð¾Ð¶ÐµÑ Ð¾ÑвоиÑÑ. ÐакÑимÑм â 6.","Set the colony limit for each clone. Maximum: 6."], 69 ["ÐобавиÑÑ ÐºÐ»Ð¾Ð½Ð°","Add clone"],["ÐÑимениÑÑ","Apply"],["Ðлонов:","Clones:"],["ÐÑего доÑÑÑпно планеÑ:","Total planets available:"], 70 ["УÑÐ¾Ð²ÐµÐ½Ñ ÐЦ:","CC level:"],["Ðлон не вÑбÑан","Clone not selected"], 71 ["УкажиÑе колиÑеÑÑво планеÑ","Enter planet counts"],["ÐоÑле Ð·Ð°Ð¿Ð¾Ð»Ð½ÐµÐ½Ð¸Ñ ÑоÑÐ¼Ñ Ð¿Ð¾ÑвÑÑÑÑ Ð½Ð°ÑÑÑойки конкÑеÑнÑÑ Ð¿Ð»Ð°Ð½ÐµÑ Ð¸ доÑÑÑпнÑе маÑеÑÐ¸Ð°Ð»Ñ P0âP4.","After completing the form, planet settings and available P0âP4 materials will appear."], 72 ["ÐоÑле каждого вÑбоÑа оÑÑалÑнÑе позиÑии пеÑеÑÑиÑÑваÑÑÑÑ Ñ ÑÑÑÑом клонов и наÑÑÑоек конкÑеÑнÑÑ Ð¿Ð»Ð°Ð½ÐµÑ.","Every selection recalculates the remaining options using clone and planet settings."], 73 ["ÐоказÑваÑÑ Ð½ÐµÐ´Ð¾ÑÑÑпнÑе","Show unavailable"],["ÐÐÐÐÐÐÐТÐÐЬÐЫРÐÐСТРÐÐÐÐ","ADVANCED SETTINGS"], 74 ["ÐÑделÑÐ½Ð°Ñ Ð´Ð¾Ð±ÑÑа P0 Ð´Ð»Ñ ÐºÐ°Ð¶Ð´Ð¾Ð³Ð¾ компоненÑа","Separate P0 extraction for each component"], 75 ["ÐклÑÑено: одинаковÑй P0 Ð´Ð»Ñ ÑазнÑÑ Ð²ÑбÑаннÑÑ ÐºÐ¾Ð¼Ð¿Ð¾Ð½ÐµÐ½Ñов добÑваеÑÑÑ Ð¾ÑделÑнÑми лиÐ
75½Ð¸Ñми на ÑазнÑÑ Ð¿Ð»Ð°Ð½ÐµÑÐ°Ñ . ÐÑклÑÑено: неÑколÑко P0-линий можно ÑазмеÑÑиÑÑ Ð²Ð¼ÐµÑÑе. ÐаданнÑй Ð»Ð¸Ð¼Ð¸Ñ Ð´Ð¾Ð±ÑÑÑиков P0 дейÑÑвÑÐµÑ Ð² Ð¾Ð±Ð¾Ð¸Ñ ÑÐµÐ¶Ð¸Ð¼Ð°Ñ Ð¸ ÑÑиÑÑваеÑÑÑ Ð°Ð²ÑоподбоÑом.","Enabled: the same P0 required by different selected components uses separate extraction lines on different planets. Disabled: multiple P0 lines may share a planet. The selected P0 extractor limit applies in both modes and is respected by auto-build."], 76 ["ÐакÑималÑное колиÑеÑÑво добÑваÑÑÐ¸Ñ P0-линий на одной планеÑе","Maximum number of P0 extraction lines on one planet"], 77 ["ÐобÑÑÑиков P0 на одной планеÑе","P0 extraction lines per planet"],["ÐеÑеÑабаÑÑваÑÑ P0âP1 ÑÑазÑ?","Process P0âP1 locally?"], 78 ["ÐпиÑание ÑазделÑной добÑÑи P0 и лимиÑа добÑÑÑиков","Separate P0 extraction and extractor-limit description"], 79 ["Флажок запÑеÑÐ°ÐµÑ Ð¾Ð±ÑединÑÑÑ Ð¾Ð´Ð½Ñ Ð´Ð¾Ð±ÑваÑÑÑÑ Ð»Ð¸Ð
79½Ð¸Ñ P0 Ð¼ÐµÐ¶Ð´Ñ ÑазнÑми вÑбÑаннÑми маÑеÑиалами. ЧиÑло ÑпÑава задаÑÑ Ð¼Ð°ÐºÑимÑм добÑваÑÑÐ¸Ñ P0-линий именно на одной планеÑе, а не во вÑÑм плане. ÐапÑимеÑ, пÑи лимиÑе 1 две оÑделÑнÑе линии бÑдÑÑ ÑазмеÑÐµÐ½Ñ Ð½Ð° двÑÑ Ð¿Ð»Ð°Ð½ÐµÑÐ°Ñ .","The checkbox prevents one P0 extraction line from being shared by different selected materials. The number on the right limits P0 extraction lines on each planet, not in the whole plan. For example, with a limit of 1, two separate lines are placed on two planets."], 80 ["ÐпиÑание пеÑеÑабоÑки P0âP1","P0âP1 processing description"], 81 ["ÐклÑÑено: добÑÑÑй P0 пеÑеÑабаÑÑваеÑÑÑ Ð² P1 на Ñой же планеÑе. ÐÑклÑÑено: планеÑа ÑолÑко добÑÐ²Ð°ÐµÑ P0, а пеÑеÑабоÑка в P1 на ней не ÑазмеÑаеÑÑÑ.","Enabled: extracted P0 is processed into P1 on the same planet. Disabled: the planet only extracts P0 and does not host P1 processing."], 82 ["ÐÑделÑнÑе ÑбоÑоÑнÑе планеÑÑ","Dedicated factory planets"], 83 ["ÐакÑимÑм загÑÑженноÑÑи пÑоизводÑÑва","Maximum production utilization"],["ÐÑенка ÑÑоимоÑÑи маÑеÑиалов","Material value estimate"], 84 ["ÐолиÑеÑÑво пÑоизводÑÑвеннÑÑ Ð»Ð¸Ð½Ð¸Ð¹ на одной планеÑе","Number of production lines per planet"], 85 ["ÐÑоизводÑÑвеннÑÑ Ð»Ð¸Ð½Ð¸Ð¹ на одной планеÑе","Production lines per planet"],["ÐпиÑание пÑоизводÑÑвеннÑÑ Ð»Ð¸Ð½Ð¸Ð¹","Production line limit description"], 86 ["ÐадаÑÑ Ð¼Ð°ÐºÑималÑное колиÑеÑÑво пÑоизводÑÑвеннÑÑ Ð·Ð°Ð²Ð¾Ð´Ð¾Ð² на одной планеÑе. УÑиÑÑваÑÑÑÑ Ð²Ñе ÑÑовни: P0âP1, P1âP2, P2âP3 и P3âP4. ÐапÑимеÑ, один завод Silicon и один завод Rocket Fuel ÑÑиÑаÑÑÑÑ ÐºÐ°Ðº 2 линии. ECU, коÑмопоÑÑÑ Ð¸ Ñ ÑанилиÑа в Ð»Ð¸Ð¼Ð¸Ñ Ð½Ðµ Ð²Ñ Ð¾Ð´ÑÑ.","Sets the maximum number of production facilities on one planet. Every tier counts: P0âP1, P1âP2, P2âP3, and P3âP4. For example, one Silicon facility and one Rocket Fuel facility count as 2 lines. ECUs, Launchpads, and Storage Facilities do not count toward this limit."], 87 ["Ðалог Ñ Ð¿Ñодаж, %","Sales tax, %"],["ÐÑокеÑÑÐºÐ°Ñ ÐºÐ¾Ð¼Ð¸ÑÑиÑ, %","Broker fee, %"],["СÑавка налога Ñаможни, %","Customs tax rate, %"], 88 ["ÐеÑевозка, ISK за м³","Hauling, ISK per m³"],["Ð¦ÐµÐ½Ñ ÐµÑÑ Ð½Ðµ загÑÑженÑ","Prices have not been loaded"],["ÐбновиÑÑ ÑенÑ","Refresh prices"], 89 ["ÐоÑÑÑоиÑÑ Ð¿Ð»Ð°Ð½","Build plan"],["ÐвÑÐ¾Ð¿Ð¾Ð´Ð±Ð¾Ñ Ð¸ÑполÑзÑÐµÑ Ð·Ð°Ð´Ð°Ð½Ð½Ñе налоги, пеÑевозкÑ, Ð»Ð¸Ð¼Ð¸Ñ Ð´Ð¾Ð±ÑÑÑиков P0 и Ð»Ð¸Ð¼Ð¸Ñ Ð¿ÑоизводÑÑвеннÑÑ Ð»Ð¸Ð½Ð¸Ð¹. ÐклÑÑÑннÑй Ñежим оÑделÑнÑÑ ÑбоÑоÑнÑÑ Ð¿Ð»Ð°Ð½ÐµÑ ÑÐ¾Ñ ÑанÑеÑÑÑ.","Auto-build uses your taxes, hauling cost, P0 extractor limit, and production-line limit. Enabled dedicated factory planets remain enabled."], 90 ["ÐÑзÑв или пÑедложение в Telegram","Feedback or suggestion on Telegram"],["ÐÑзÑв или пÑедложение","Feedback or suggestion"], 91 ["ÐÑкÑÑÑÑ Ð¸Ð½ÑÑÑÑкÑиÑ","Open user guide"],["ÐакÑÑÑÑ Ð¸Ð½ÑÑÑÑкÑиÑ","Close user guide"], 92 ["ÐнÑÑÑÑкÑÐ¸Ñ Ð¿Ð¾ иÑполÑзованиÑ","User guide"],["СÐÐ ÐÐÐÐ","HELP"],["Ðазад","Previous"],["Ðалее","Next"], 93 ["ÐÐ ÐÐÐÐ ÐСÐÐТРPI-ШÐÐÐÐÐÐ","PI TEMPLATE PREVIEW"],["Шаблон планеÑÑ","Planet template"], 94 ["ÐакÑÑÑÑ Ð¿ÑедпÑоÑмоÑÑ","Close preview"],["ФакÑиÑеÑкое ÑаÑположение поÑÑÑоек","Actual structure layout"], 95 ["ÐоÑенÑиалÑное ÑаÑположение в EVE по кооÑдинаÑам Ñаблона и ÑакÑиÑеÑкие линии ÑвÑзи.","Potential EVE layout from the template coordinates with its actual links."], 96 ["ЧТРÐÐÐÐÐТ ШÐÐÐÐÐ","WHAT THIS TEMPLATE DOES"],["СÐСТÐÐ","COMPOSITION"], 97 ["Ð ÐСУРСЫ ÐÐÐÐÐÐÐÐÐРЦÐÐТРÐ","COMMAND CENTER RESOURCES"],["ÐÐÐÐÐÐЧÐÐÐЯ","LEGEND"], 98 ["ÐоÑмоÑÑеÑÑ Ñаблон","Preview template"],["СкопиÑоваÑÑ Ñаблон","Copy template"],["ÐакÑÑÑÑ","Close"], 99 ["ÐажмиÑе на изобÑажение Ð´Ð»Ñ ÑвелиÑениÑ","Click the image to zoom"],["Шаги инÑÑÑÑкÑии","Guide steps"],["ÐнÑÑÑÑкÑÐ¸Ñ â Ñаг","Guide â step"],["Шаг","Step"], 100 ["ÐеÑебиÑаем пÑоизводÑÑвеннÑе конÑигÑÑаÑииâ¦","Testing production configurationsâ¦"],["ÐÑибÑлÑнÑй ваÑÐ¸Ð°Ð½Ñ Ð½Ðµ найден.","No profitable plan was found."],["ÐпÑималÑнÑй план поÑÑÑоен:","Optimal plan built:"], 101 ["ТÐÐУЩÐÐ ÐÐÐÐ","CURRENT PLAN"],["СнÑÑÑ Ð²Ñе","Clear all"],["Ðо клонам","By clone"],["ЦепоÑка","Chain"], 102 ["ÐÑÐ¾Ð³Ð¾Ð²Ð°Ñ ÑÑоимоÑÑÑ Ð¿Ð»Ð°Ð½Ð°","Final plan value"],["ÐагÑÑзка Ñенâ¦","Loading pricesâ¦"], 103 ["ÐÐÐÐÐÐÐÐРТÐÐУЩÐÐÐ ÐÐÐÐÐ","CURRENT PLAN ECONOMICS"],["ÐÑÐ¾Ð³Ð¾Ð²Ð°Ñ ÑÑоимоÑÑÑ Ð¿Ð¾Ð»ÑÑенного маÑеÑиала","Final value of produced material"], 104 ["ÐÐÐÐÐ¥ÐÐÐÐÐЯ СТÐÐÐÐЬÐÐЯ ÐÐÐЫЧРP0 ÐÐЯ Ð ÐÐÐТЫ 24/7","STABLE P0 EXTRACTION REQUIRED FOR 24/7 OPERATION"], 105 ["ÐÑли ÑакÑиÑеÑÐºÐ°Ñ Ð´Ð¾Ð±ÑÑа ниже, Ð·Ð°Ð²Ð¾Ð´Ñ Ð±ÑдÑÑ Ð¿ÑоÑÑаиваÑÑ Ð¸ ÑеалÑнÑй ÑезÑлÑÑÐ°Ñ Ð¾ÐºÐ°Ð¶ÐµÑÑÑ Ð¼ÐµÐ½ÑÑе пÑогноза.","If actual extraction is lower, factories will idle and actual output will be below the forecast."], 106 ["ÐÐÐ ÐÐÐЧРÐÐÐÐУ ÐÐÐÐÐÐÐ","TRANSFERS BETWEEN CLONES"],["Ð¿Ð»Ð°Ð½ÐµÑ Ð·Ð°Ð´ÐµÐ¹ÑÑвовано","planets used"],["Свободно Ð¿Ð»Ð°Ð½ÐµÑ Ñ ÐºÐ»Ð¾Ð½Ð°:","Unused planets for clone:"], 107 [" â ÑбоÑоÑнаÑ"," â assembly"],["пеÑедаÑÑ:","transfer:"],["ÐобÑÑа P0","P0 extraction"],["РеÑепÑÑÑÐ½Ð°Ñ ÑепоÑка поÑÑÑоена из PI-ÑÑ ÐµÐ¼ SDE.","The production chain is built from SDE PI schematics."], 108 ["ÐгÑаниÑÐµÐ½Ð¸Ñ ÐºÐ¾Ð½ÐºÑеÑнÑÑ Ð¿Ð»Ð°Ð½ÐµÑ Ð²Ð»Ð¸ÑÑÑ Ð½Ð° ÑазмеÑение во вкладке «Ðо клонам».","Planet constraints affect placement on the âBy cloneâ tab."], 109 ["ÐÑбÑано","Selected"],["ÐадейÑÑвовано планеÑ","Planets used"],["ÐинимÑм планеÑ","Minimum planets"],["ÐоÑÑÑпно клонам","Available to clones"],["Свободно планеÑ","Unused planets"], 110 ["Ðлан невÑполним","Plan is not feasible"],["Ðлон","Clone"],["ÐолиÑеÑÑво планеÑ:","Planet count:"], 111 ["Ð ÑÑной ÑоÑÑав","Manual composition"],["ÑÑÑки","day"],["дней","days"],["ÐÑпÑÑк:","Output:"], 112 ["ÐагÑÑжаем ÑÑноÑнÑе ÑенÑâ¦","Loading market pricesâ¦"], 113 ["ÐÐ»Ñ ÑаÑÑÑÑа Ñкономики не Ñ Ð²Ð°ÑÐ°ÐµÑ Ð°ÐºÑÑалÑнÑÑ ÑÑноÑнÑÑ Ð´Ð°Ð½Ð½ÑÑ .","Current market data is unavailable for this estimate."], 114 ["ÐклÑÑено: добÑÑÑй P0 ÑÑÐ°Ð·Ñ Ð¿ÐµÑеÑабаÑÑваеÑÑÑ Ð² P1 на Ñой же планеÑе. ÐÑклÑÑено: добÑваÑÑÐ°Ñ Ð¿Ð»Ð°Ð½ÐµÑа пÑÐ¾Ð¸Ð·Ð²Ð¾Ð´Ð¸Ñ ÑолÑко P0, а пеÑеÑабоÑка P0âP1 пеÑеноÑиÑÑÑ Ð½Ð° ÑбоÑоÑнÑÑ Ð¿Ð»Ð°Ð½ÐµÑÑ.","
114Enabled: extracted P0 is immediately processed into P1 on the same planet. Disabled: the extraction planet produces only P0, while P0âP1 processing moves to a factory planet."], 115 ["ÐклÑÑено: Ð·Ð°Ð²Ð¾Ð´Ñ P1âP2, P2âP3 и P3âP4 ÑазмеÑаÑÑÑÑ ÑолÑко на планеÑÐ°Ñ Ð±ÐµÐ· добÑÑи P0. ÐÑклÑÑено: ÑÑи Ð·Ð°Ð²Ð¾Ð´Ñ Ð¼Ð¾Ð³ÑÑ ÑовмеÑаÑÑÑÑ Ñ Ð´Ð¾Ð±ÑÑей, еÑли позволÑÑÑ Ð·Ð°Ð´Ð°Ð½Ð½Ñе огÑаниÑениÑ.","Enabled: P1âP2, P2âP3, and P3âP4 factories are placed only on planets without P0 extraction. Disabled: these factories may share extraction planets when the selected limits allow it."], 116 ["ÐклÑÑено: ÑвободнÑе планеÑÑ ÑнаÑала повÑоÑÑÑÑ Ð¿Ð¾Ð»Ð½ÑÑ Ð²ÑбÑаннÑÑ ÑепоÑкÑ, заÑем пÑоизводÑÑ ÐµÑ P3, P2, P1 и P0. ÐÑклÑÑено: вÑбÑÐ°Ð½Ð½Ð°Ñ ÑепоÑка ÑÑÑоиÑÑÑ ÑолÑко один Ñаз.","Enabled: free planets first repeat the complete selected chain, then produce its P3, P2, P1, and P0. Disabled: the selected chain is built only once."], 117 ["ÐклÑÑено: пÑогÑамма ÑÑÐ°Ð²Ð½Ð¸Ð²Ð°ÐµÑ Ð´Ð¾ÑÑÑпнÑе пÑоизводÑÑвеннÑе конÑигÑÑаÑии по оÑиенÑиÑовоÑной ÑиÑÑой пÑодаже за 30 дней, вклÑÑÐ°Ñ Ð¿Ð¾Ð»ÐµÐ·Ð½ÑÑ Ð¿ÑодÑкÑÐ¸Ñ Ð¾ÑÑавÑÐ¸Ñ ÑÑ Ð¿Ð»Ð°Ð½ÐµÑ Ð¿Ñи макÑималÑной загÑÑзке. ÐÑклÑÑено: ÑÑноÑнÑе запÑоÑÑ Ð½Ðµ вÑполнÑÑÑÑÑ.","Enabled: available production configurations are compared by estimated net sales over 30 days, including useful output from remaining planets at maximum utilization. Disabled: no market requests are made."], 118 ["ÐÑи межпланеÑной пеÑевозке ÑÑиÑÑваÑÑÑÑ Ð¾Ð±Ð° ÑбоÑа: ÑкÑпоÑÑ Ñ Ð¸ÑÑ Ð¾Ð´Ð½Ð¾Ð¹ планеÑÑ Ð¸ импоÑÑ Ð½Ð° ÑледÑÑÑÑÑ. ÐмпоÑÑ ÑаÑÑÑиÑÑваеÑÑÑ ÐºÐ°Ðº 50% ÑкÑпоÑÑного ÑбоÑа.","Interplanetary hauling includes both fees: export from the source planet and import to the destination. The import fee is calculated as 50% of the export fee."], 119 ["СÑавниваеÑÑÑ Ð¾ÑиенÑиÑовоÑÐ½Ð°Ñ ÑиÑÑÐ°Ñ Ð¿Ñодажа за 30 дней. ÐÑпÑÑк огÑаниÑиваеÑÑÑ ÑеалÑнÑм Ñзким меÑÑом вÑей ÑеÑепÑÑÑной ÑепоÑки и ÑÑедней ликвидноÑÑÑÑ ÑÑнка. РаÑÑÑÑÐ½Ð°Ñ Ñена â ÑÑедневзвеÑÐµÐ½Ð½Ð°Ñ Ñена ÑакÑиÑеÑÐºÐ¸Ñ Ñделок за поÑледние 30 завеÑÑÑннÑÑ Ð´Ð½ÐµÐ¹. УÑÑÐµÐ½Ñ Ð½Ð°Ð»Ð¾Ð³ Ñ Ð¿Ñодаж, бÑокеÑÑÐºÐ°Ñ ÐºÐ¾Ð¼Ð¸ÑÑиÑ, ÑкÑпоÑÑ Ð¸ импоÑÑ ÑеÑез ÑаможнÑ, а Ñакже пеÑевозка; добÑÑа P0 оÑÑаÑÑÑÑ Ð¾ÑиенÑиÑовоÑной без заданного дебиÑа ÑкÑÑÑакÑоÑов.","Estimated net sales over 30 days are compared. Output is limited by the actual bottleneck of the entire production chain and average market liquidity. The calculated price is the volume-weighted average of actual trades over the last 30 completed days. Sales tax, broker fee, customs export and import, and hauling are included; P0 extraction remains an estimate without specified extractor yields."], 120 ["ФакÑиÑеÑÐºÐ°Ñ Ð´Ð¾Ð±ÑÑа завиÑÐ¸Ñ Ð¾Ñ Ð·Ð°Ð»ÐµÐ¶ÐµÐ¹, пÑогÑÐ°Ð¼Ð¼Ñ ÑкÑÑÑакÑоÑа и ÑаÑÑоÑÑ ÐµÑ Ð¿ÐµÑезапÑÑка. ÐÑи менÑÑей добÑÑе Ð·Ð°Ð²Ð¾Ð´Ñ Ð±ÑдÑÑ Ð¿ÑоÑÑаиваÑÑ, а ÑеалÑÐ½Ð°Ñ Ð¿ÑибÑÐ»Ñ Ð¾ÐºÐ°Ð¶ÐµÑÑÑ Ð½Ð¸Ð¶Ðµ пÑогноза.","Actual extraction depends on deposits, the extractor program, and restart frequency. With lower extraction, factories will idle and actual profit will be below the forecast."], 121 ["ÐÑа конÑигÑÑаÑÐ¸Ñ Ð¿ÑоизводÑÑва невозможна.","This production configuration is not feasible."], 122 ["ÐÑбÑаннÑе маÑеÑÐ¸Ð°Ð»Ñ ÑÐ¾Ñ ÑÐ°Ð½ÐµÐ½Ñ â измениÑе наÑÑÑойки, колиÑеÑÑво Ð¿Ð»Ð°Ð½ÐµÑ Ð¸Ð»Ð¸ клонов.","The selected materials were kept â change the settings, planet counts, or clones."], 123 ["Ðе Ñ Ð²Ð°ÑÐ°ÐµÑ Ð¿Ð»Ð°Ð½ÐµÑаÑнÑÑ ÑлоÑов Ð´Ð»Ñ ÑазделÑной добÑÑи P0","Not enough colony slots for separate P0 extraction"], 124 ["Ðе Ñ Ð²Ð°ÑÐ°ÐµÑ Ð¿Ð»Ð°Ð½ÐµÑаÑнÑÑ ÑлоÑов Ñ ÐºÐ»Ð¾Ð½Ð¾Ð² Ñ ÑÑÑÑом огÑаниÑений добÑÑи P0","Not enough clone colony slots with the selected P0 extraction limits"], 125 ["ÐÐ»Ñ Ð¾ÑделÑного ÑÑапа","The separate stage"],["не Ñ Ð²Ð°ÑÐ°ÐµÑ Ñвободного планеÑаÑного ÑлоÑа Ñ ÐºÐ»Ð¾Ð½Ð¾Ð²","does not have a free clone colony slot"], 126 ["ÐолиÑеÑÑво Ð¿Ð»Ð°Ð½ÐµÑ Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¾. ÐжидаеÑÑÑ Ð°Ð²ÑомаÑиÑеÑкий пеÑеÑаÑÑÑÑâ¦","Planet counts changed. Waiting for automatic recalculationâ¦"], 127 ["ÐодгоÑавливаем пÑовеÑÐºÑ Ð¿ÑоизводÑÑвеннÑÑ ÐºÐ¾Ð½ÑигÑÑаÑийâ¦","Preparing production configuration checksâ¦"], 128 ["СÑавниваем вÑе доÑÑÑпнÑе пÑоизводÑÑвеннÑе конÑигÑÑаÑииâ¦","Comparing all available production configurationsâ¦"], 129 ["ÐÑовеÑÑем акÑÑалÑноÑÑÑ ÑезÑлÑÑаÑа повÑоÑнÑм ÑаÑÑÑÑомâ¦","Verifying the result with a second calculationâ¦"], 130 ["РезÑлÑÑаÑÑ Ð¸Ð·Ð¼ÐµÐ½Ð¸Ð»Ð¸ÑÑ â вÑполнÑем новÑй ÑаÑÑÑÑâ¦","Results changed â starting a new calculationâ¦"], 131 ["ÐодгоÑовка авÑоподбоÑа макÑималÑной пÑибÑлиâ¦","Preparing maximum-profit auto-buildâ¦"], 132 ["ÐÑибка авÑоподбоÑа:","Auto-build error:"],["ÐÑибка пеÑеÑÑÑÑа:","Recalculation error:"], 133 ["СнаÑала ÑкажиÑе колиÑеÑÑво планеÑ","Enter planet counts first"],["СнаÑала вклÑÑиÑе оÑÐµÐ½ÐºÑ ÑÑоимоÑÑи маÑеÑиалов","Enable material value estimation first"], 134 ["ÐождиÑеÑÑ Ð·Ð°Ð³ÑÑзки ÑÑноÑнÑÑ Ñен","Wait for market prices to load"],["Ðе Ñ Ð²Ð°ÑÐ°ÐµÑ Ð´Ð¾ÑÑÑпнÑÑ Ð¿Ð»Ð°Ð½ÐµÑаÑнÑÑ ÑлоÑов.","Not enough available colony slots."], 135 ["СиÑÑема найдена. ESI недоÑÑÑпен â иÑполÑÐ·Ð¾Ð²Ð°Ð½Ñ ÑезеÑвнÑе даннÑе.","System found. ESI is unavailable â fallback data was used."], 136 ["СиÑÑема найдена, ÑоÑÑав Ð¿Ð»Ð°Ð½ÐµÑ Ð·Ð°Ð¿Ð¾Ð»Ð½ÐµÐ½","System found and planet counts filled in"],["СиÑÑема не найдена","System not found"], 137 ["Ðе ÑдалоÑÑ Ð¿Ð¾Ð»ÑÑиÑÑ Ð´Ð°Ð½Ð½Ñе ÑиÑÑÐµÐ¼Ñ Ð¸Ð· ESI","Could not retrieve system data from ESI"],["ÐведиÑе название ÑиÑÑемÑ","Enter a system name"], 138 ["ESI не веÑнÑл ÑпиÑок планеÑ","ESI did not return a planet list"],["ÐеизвеÑÑнÑй Ñип планеÑÑ ESI:","Unknown ESI planet type:"], 139 ["СÑедневзвеÑÐµÐ½Ð½Ð°Ñ Ñена Ñделок за","Volume-weighted trade price over"],["ÐоÑледний завеÑÑÑннÑй Ð´ÐµÐ½Ñ Ð¾ÑноÑиÑелÑно ÑÑедней:","Last completed day versus average:"], 140 ["за единиÑÑ","per unit"],[" из 6"," of 6"], 141 ["ÐедоÑÑаÑоÑно ÑÑноÑнÑÑ Ð´Ð°Ð½Ð½ÑÑ Ð·Ð° 30 дней","Insufficient market data for 30 days"],["Цена загÑÑжаеÑÑÑ","Price is loading"], 142 ["Ð¦ÐµÐ½Ñ Ð°ÐºÑÑалÑÐ½Ñ Ð½Ð°","Prices updated on"],["ÑÑедневзвеÑÐµÐ½Ð½Ð°Ñ Ñена Ñделок за 30 дней","volume-weighted trade price over 30 days"], 143 ["ÐагÑÑзка ÑÑÑоÑнÑÑ Ð¸Ð½Ð´ÐµÐºÑов:","Loading daily market data:"],["Ðе ÑдалоÑÑ Ð¿Ð¾Ð»ÑÑиÑÑ ÑенÑ. ÐÑовеÑÑÑе подклÑÑение к инÑеÑнеÑÑ","Could not retrieve prices. Check your internet connection"], 144 ["ÐÑÑавÑÑе 0, еÑли комиÑÑÐ¸Ñ Ð½Ðµ взимаеÑÑÑ","Leave 0 if no fee is charged"], 145 ["ÐÐÐÐÐ¥ÐÐÐÐЫРÐÐÐÐÐÐÐЫРЦÐÐТРЫ","REQUIRED COMMAND CENTERS"],["СкопиÑоваÑÑ ÑпиÑок команднÑÑ ÑенÑÑов","Copy command center list"], 146 ["СпиÑок команднÑÑ ÑенÑÑов ÑкопиÑован","Command center list copied"],["ÐоманднÑй ÑенÑÑ:","Command Center:"],["ÑÑ.","units"], 147 ["ÐоÑмопоÑÑ","Launchpad"],["Ð¥ÑанилиÑе","Storage Facility"],["ÐагÑÑзиÑÑ","Loa
147d"],["ÐÑего","Total"],["ÐмеÑÑимоÑÑÑ","Capacity"], 148 ["ÐолиÑеÑÑво коÑмопоÑÑов","Launchpad count"],["ÐолиÑеÑÑво Ñ ÑанилиÑ","Storage Facility count"], 149 ["ÐÐµÑ Ð´Ð¾ÑÑÑпнÑÑ ÐºÐ¾Ð¼Ð¿Ð¾Ð½ÐµÐ½Ñов Ð´Ð»Ñ ÐºÐ¾Ð¿Ð¸ÑованиÑ","No available components to copy"],["ÐвÑомаÑиÑеÑкое копиÑование заблокиÑовано бÑаÑзеÑом. ÐажмиÑе Ctrl+C, заÑем Enter:","Automatic copying was blocked by the browser. Press Ctrl+C, then Enter:"], 150 ["Ðе ÑдалоÑÑ ÑкопиÑоваÑÑ","Could not copy"],["СкопиÑоваÑÑ Ð´Ð¾ÑÑÑпнÑе компоненÑÑ","Copy available components"],["ÐÐµÑ Ð´Ð¾ÑÑÑпнÑÑ ","None available"], 151 ["Ð½ÐµÑ Ð´Ð¾ÑÑÑпнÑÑ ÐºÐ¾Ð¼Ð¿Ð¾Ð½ÐµÐ½Ñов","has no available components"],[" ÑкопиÑованÑ"," copied"],[" ÑкопиÑован"," copied"],["СкопиÑоваÑÑ Ð¿Ð¾ÐºÐ°Ð·Ð°Ð½Ð½Ñе маÑеÑиалÑ","Copy displayed materials"],["ÐоказаннÑе маÑеÑиалÑ","Displayed materials"], 152 ["Ð ÑиÑÑеме оÑÑÑÑÑÑвÑÐµÑ Ð½ÐµÐ¾Ð±Ñ Ð¾Ð´Ð¸Ð¼Ñй P0","The required P0 is not available in the system"],["ÐÐµÑ P0:","Missing P0:"],["ÐÑжно болÑÑе доÑÑÑпнÑÑ Ð¿Ð»Ð°Ð½ÐµÑ","More available planets are required"], 153 ["ÐÐµÐ¾Ð±Ñ Ð¾Ð´Ð¸Ð¼Ð¾ планеÑ","Planets required"],["СÐÐЫРÐЫÐÐÐÐЫРÐÐСТУÐÐЫРÐÐÐ ÐÐÐТ","MOST PROFITABLE AVAILABLE OPTION"], 154 ["ÐагÑÑжаем ÑекÑÑие ÑенÑâ¦","Loading current pricesâ¦"],["P0 ÐÐЯ ÐÐÐÐ ÐРЫÐÐÐÐ Ð ÐÐÐТЫ 24/7","P0 FOR CONTINUOUS 24/7 OPERATION"], 155 ["ÐÑогноз доÑÑижим пÑи ÑÑабилÑной добÑÑе не ниже ÑказаннÑÑ Ð¾Ð±ÑÑмов.","The forecast is achievable with stable extraction at or above the listed volumes."], 156 ["РеÑÑÑÑ P0","P0 resource"],["Ð ÑаÑ","Per hour"],["Ð ÑÑÑки","Per day"],["Ðа 30 дней","Over 30 days"], 157 ["заводов конеÑного пÑодÑкÑа:","final-product factories:"],["вÑпÑÑк:","output:"],["занÑÑо","used"],["дополниÑелÑно:","additional:"], 158 ["завод. Ñели","target factories"],["план.","planets"],["30 дн.","30 days"],["30 дней","30 days"], 159 ["ÑÑÑÑÑ:","source:"],["ÐланеÑ:","Planets:"],["УдалиÑÑ","Remove"], 160 ["РаÑÑÑÑ Ð¿ÑоизводÑÑвенного плана","Calculating production plan"],["ÐодгоÑовка даннÑÑ â¦","Preparing dataâ¦"], 161 ["ÐÑовеÑка вÑбÑанной конÑигÑÑаÑииâ¦","Checking selected configurationâ¦"],["ÐонÑигÑÑаÑÐ¸Ñ Ð½ÐµÐ²Ð¾Ð·Ð¼Ð¾Ð¶Ð½Ð° â ÑоÑмиÑование пÑедÑпÑеждениÑâ¦","Configuration is not feasible â preparing warningâ¦"], 162 ["Ðбновление доÑÑÑпнÑÑ Ð¼Ð°ÑеÑиаловâ¦","Updating available materialsâ¦"],["ÐеÑеÑÑÑÑ ÑпиÑков P0âP4â¦","Recalculating P0âP4 listsâ¦"], 163 ["ФоÑмиÑование плана по клонамâ¦","Building clone planâ¦"],["ÐоÑово","Done"],["УкажиÑе Ñ Ð¾ÑÑ Ð±Ñ Ð¾Ð´Ð½Ñ Ð´Ð¾ÑÑÑпнÑÑ Ð¿Ð»Ð°Ð½ÐµÑÑ","Enter at least one available planet"], 164 ["ÐиагноÑÑиÑеÑкий Ñайл Ñоздан","Diagnostic file created"],["ÐеизвеÑÑнÑй ÑоÑÐ¼Ð°Ñ Ð¿ÑоекÑа","Unknown project format"],["Ð ÑÑаÑом пÑоекÑе Ð½ÐµÑ Ð´Ð°Ð½Ð½ÑÑ Ð¾ колиÑеÑÑве планеÑ","The old project contains no planet-count data"], 165 ["ÐÑÐ¾ÐµÐºÑ Ð·Ð°Ð³ÑÑжен","Project loaded"],["Ðе ÑдалоÑÑ Ð·Ð°Ð³ÑÑзиÑÑ Ð¿ÑоекÑ:","Could not load project:"],["ÐвÑоÑÐ¾Ñ Ñанение недоÑÑÑпно в ÑÑом бÑаÑзеÑе","Autosave is unavailable in this browser"], 166 ["ÐаÑеÑÐ¸Ð°Ð»Ñ P0âP4","P0âP4 MATERIALS"],["ÐополниÑелÑнÑе наÑÑÑойки планиÑованиÑ","Advanced planning settings"],["ÐÑÐ±Ð¾Ñ ÑзÑка","Language selection"], 167 ["ÐапÑÑÑиÑÑ Ð¸Ð½ÑеÑакÑивное обÑÑение","Start interactive tour"],["ÐнÑеÑакÑивное обÑÑение","Interactive tour"],["ÐÑопÑÑÑиÑÑ Ð¾Ð±ÑÑение","Skip tour"], 168 ["ÐпиÑание ÑазделÑной добÑÑи P0","Separate P0 extraction description"],["ÐпиÑание пеÑеÑабоÑки P0âP1","P0âP1 processing description"], 169 ["ÐпиÑание оÑделÑнÑÑ ÑбоÑоÑнÑÑ Ð¿Ð»Ð°Ð½ÐµÑ","Dedicated factory planets description"],["ÐпиÑание макÑималÑной загÑÑзки","Maximum utilization description"],["ÐпиÑание оÑенки ÑÑоимоÑÑи","Material value estimation description"], 170 ["Ð Ñнок","Market"],["ÐкÑпоÑÑ ÑÑиÑаеÑÑÑ Ð¿Ð¾ полной ÑÑавке, импоÑÑ Ð½Ð° дÑÑгÑÑ Ð¿Ð»Ð°Ð½ÐµÑÑ â по половине ÑÑавки","Export uses the full rate; import to another planet uses half the rate"], 171 ["ÐоблагодаÑиÑÑ","Say thanks"],["Ðе за ÑÑо :) ÐÑÑавÑÑе пожалÑйÑÑа оÑзÑв","You're welcome :) Please leave a review"], 172 ["Сделано ленивÑм Ð´Ð»Ñ Ð»ÐµÐ½Ð¸Ð²ÑÑ ","Made by the lazy for the lazy"],["ÐкÑпоÑÑ Ð´Ð»Ñ Ð¿ÑовеÑки","Export for verification"], 173 ["ÐоиÑкâ¦","Searchingâ¦"],["млÑд","bn"],["млн","M"],["ÑÑÑ.","K"], 174 ["ÐÑÑÑоÑÑ","Barren"],["Ðаз","Gas"],["ÐÑд","Ice"],["Ðава","Lava"],["Ðкеан","Oceanic"],["Ðлазма","Plasma"],["ÐÑÑÑ","Storm"],["УмеÑеннаÑ","Temperate"] 175].sort((a,b)=>b[0].length-a[0].length); 176function translateSiteText(value){ 177 if(siteLanguage!=="en")return value; 178 return EN_TRANSLATIONS.reduce((text,[ru,en])=>text.split(ru).join(en),String(value)); 179} 180const siteLocale=()=>siteLanguage==="en"?"en-US":"ru-RU"; 181function applySiteLanguage(root=document.body){ 182 if(!root)return; 183 if(document.documentElement?.getAttribute?.("lang")!==siteLanguage)document.documentElement?.setAttribute?.("lang",siteLanguage); 184 document.title=siteLanguage==="en"?"PLANET HUB â EVE Online Planetary Industry Planner":"PLANET HUB â ÐланеÑаÑнÑй планиÑовÑик EVE Online";
185 const visit=node=>{ 186 if(node.nodeType===3){if(!originalText.has(node))originalText.set(node,node.nodeValue);node.nodeValue=siteLanguage==="en"?translateSiteText(originalText.get(node)):originalText.get(node);return} 187 if(node.nodeType!==1&&node!==document.body)return; 188 if(node.nodeType===1){ 189 if(!originalAttributes.has(node))originalAttributes.set(node,Object.fromEntries(["placeholder","title","aria-label","data-tooltip"].filter(name=>node.hasAttribute?.(name)).map(name=>[name,node.getAttribute(name)]))); 190 for(const [name,value] of Object.entries(originalAttributes.get(node)||{}))node.setAttribute(name,siteLanguage==="en"?translateSiteText(value):value); 191 } 192 for(const child of node.childNodes||[])visit(child); 193 }; 194 visit(root); 195 ["ru","en"].forEach(lang=>{const button=$("lang"+lang[0].toUpperCase()+lang.slice(1)+"Btn");if(button){button.classList.toggle("active",siteLanguage===lang);button.setAttribute("aria-pressed",String(siteLanguage===lang))}}); 196} 197function setSiteLanguage(language){ 198 siteLanguage=language==="en"?"en":"ru"; 199 try{localStorage.setItem("planetHubLanguage",siteLanguage)}catch(_error){} 200 if($("cloneList"))renderClones(); 201 if($("potentialColumns"))renderPotential(); 202 if(current&&$("materialColumns"))mats(); 203 if(selected.length)plan(); 204 applySiteLanguage(); 205 if($("guidedTour")&&!$("guidedTour").classList.contains("hidden"))renderGuidedTour(); 206} 207 208const GUIDED_TOUR_STORAGE_KEY="planetHubGuidedTourV8"; 209const GUIDED_TOUR_RESUME_KEY="planetHubGuidedTourResumeV8"; 210let guidedTourIndex=0,guidedTourStepsCache=[],guidedTourReturnFocus=null,guidedTourAutomatic=false,guidedTourPhase="all"; 211let guidedTourAwaitTarget=null; 212const guidedTourCopy=(ru,en)=>siteLanguage==="en"?en:ru; 213function guidedTourElementVisible(element){return !!element&&!element.classList.contains("hidden")} 214function guidedTourSteps(phase="all",state=null){ 215 const setupSteps=[ 216 { 217 target:"header .headerContent", 218 title:guidedTourCopy("ÐобÑо пожаловаÑÑ Ð² PLANET HUB","Welcome to PLANET HUB"), 219 text:guidedTourCopy("ÐÑо коÑоÑкое обÑÑение иÑполÑзÑÐµÑ Ð½Ð°ÑÑоÑÑий инÑеÑÑÐµÐ¹Ñ ÑайÑа. Ðно покажеÑ, где задаÑÑ Ð¸ÑÑ Ð¾Ð´Ð½Ñе даннÑе, вÑбÑаÑÑ Ð¼Ð°ÑеÑÐ¸Ð°Ð»Ñ Ð¸ полÑÑиÑÑ Ð³Ð¾ÑовÑй пÑоизводÑÑвеннÑй план.","This short tour uses the live site interface. It shows where to enter source data, choose materials, and obtain a complete production plan.") 220 } 221 ]; 222 const cloneStep={ 223 target:".clonePanel",awaitsInput:true, 224 title:guidedTourCopy("ÐаÑÑÑойÑе клонов","Configure clones"), 225 text:guidedTourCopy("ТепеÑÑ Ð´Ð¾Ð±Ð°Ð²ÑÑе нÑжнÑÑ ÐºÐ»Ð¾Ð½Ð¾Ð² и ÑкажиÑе доÑÑÑпное ÑиÑло колоний Ð´Ð»Ñ ÐºÐ°Ð¶Ð´Ð¾Ð³Ð¾. ÐÑли доÑÑаÑоÑно одного клона, ниÑего добавлÑÑÑ Ð½Ðµ нÑжно â пÑовеÑÑÑе его Ð»Ð¸Ð¼Ð¸Ñ Ð¸ нажмиÑе «Ðалее».","Now add the required clones and set each colony limit. If one clone is enough, add nothingâcheck its limit and click Next."), 226 actionLabel:guidedTourCopy("ÐÐ»Ð¾Ð½Ñ Ð½Ð°ÑÑÑоенÑ","Clones configured") 227 }; 228 const plannerSteps=[cloneStep,{ 229 target:".plannerSettings", 230 awaitsInput:true, 231 title:guidedTourCopy("СнаÑала наÑÑÑойÑе пÑоизводÑÑво","Configure production first"), 232 text:guidedTourCopy("Ðо вÑбоÑа маÑеÑиала задайÑе оÑделÑнÑÑ Ð´Ð¾Ð±ÑÑÑ, пеÑеÑабоÑÐºÑ P0âP1, ÑиÑло добÑваÑÑÐ¸Ñ Ð¸ пÑоизводÑÑвеннÑÑ Ð»Ð¸Ð½Ð¸Ð¹, оÑделÑнÑе ÑбоÑоÑнÑе планеÑÑ Ð¸ макÑималÑнÑÑ Ð·Ð°Ð³ÑÑзкÑ. ÐодÑвеÑеннÑе наÑÑÑойки можно менÑÑÑ Ð¿ÑÑмо ÑейÑаÑ.","Before choosing a material, configure separate extraction, P0âP1 processing, extractor and production line limits, dedicated factory planets, and maximum utilization. You can change the highlighted settings now.") 233 },{ 234 target:"#marketEvaluationOption", 235 choice:"marketEvaluation", 236 title:guidedTourCopy("ÐÑжна оÑенка ÑÑоимоÑÑи?","Do you need market evaluation?"), 237 text:guidedTourCopy("ÐÑенка загÑÑÐ¶Ð°ÐµÑ ÑÑноÑнÑе даннÑе и позволÑÐµÑ Ð¿ÑогÑамме найÑи ÑамÑй вÑгоднÑй доÑÑÑпнÑй ваÑианÑ. Ðез оÑенки Ð²Ñ ÑÑÐ°Ð·Ñ Ð²ÑбеÑеÑе желаемÑй маÑеÑиал ÑамоÑÑоÑÑелÑно.","Market evaluation loads price data and lets the planner find the most profitable available option. Without it, you can immediately choose the desired material yourself."), 238 secondaryLabel:guidedTourCopy("Ðез оÑенки","Without evaluation"), 239 primaryLabel:guidedTourCopy("ÐклÑÑиÑÑ Ð¾ÑенкÑ","Enable evaluation") 240 }]; 241 const planStep={ 242 target:"#planPanel", 243 title:guidedTourCopy("ÐÑполÑзÑйÑе гоÑовÑй план","Use the finished plan"), 244 text:guidedTourCopy("Ðлан ÑÐ°Ð·Ð±Ð¸Ñ Ð¿Ð¾ клонам и планеÑам. РнÑм ÑÐºÐ°Ð·Ð°Ð½Ñ Ð¿ÑоизводÑÑво, загÑÑзка и пеÑедаÑи. Ðозле каждого PI-Ñаблона можно ÑкопиÑоваÑÑ JSON или оÑкÑÑÑÑ ÑоÑнÑÑ Ð²Ð¸Ð·ÑализаÑÐ¸Ñ Ð¿Ð¾ÑÑÑоек.","The plan is grouped by clone and planet, including production, loads, and transfers. Each PI template can be copied as JSON or opened as an exact structure preview.") 245 }; 246 const transferStep={ 247 target:"#assignments .transferSummary", 248 title:guidedTourCopy("ÐÑовеÑÑÑе пеÑедаÑи Ð¼ÐµÐ¶Ð´Ñ ÐºÐ»Ð¾Ð½Ð°Ð¼Ð¸","Check transfers between clones"), 249 text:guidedTourCopy("ÐдеÑÑ Ñказано, какой клон, ÐºÐ°ÐºÐ¾Ð¼Ñ ÐºÐ»Ð¾Ð½Ñ Ð¸ какие маÑеÑÐ¸Ð°Ð»Ñ Ð´Ð¾Ð»Ð¶ÐµÐ½ пеÑедаÑÑ. ÐÑли возле маÑеÑиала показана дÑобÑ, пеÑедаÑÑÑÑ ÑолÑко ÑÐºÐ°Ð·Ð°Ð½Ð½Ð°Ñ Ð´Ð¾Ð»Ñ; без дÑоби маÑеÑиал пеÑедаÑÑÑÑ Ñеликом.","This section shows which clone sends which materials to another clone. A displayed fraction means only that share is transferred; without a fraction, transfer the whole material.") 250 }; 251 const waitForPlanner={ 252 target:"#systemLookup",waitFor:"planner",awaitsInput:true,cardPlacement:"right", 253 title:guidedTourCopy("ÐведиÑе ÑиÑÑÐµÐ¼Ñ Ð¸Ð»Ð¸ задайÑе планеÑÑ","Enter a system or set planets"), 254 text:guidedTourCopy("ÐведиÑе название ÑиÑÑÐµÐ¼Ñ Ð¸ нажмиÑе «ÐайÑи» либо задайÑе колиÑеÑÑво Ð¿Ð»Ð°Ð½ÐµÑ Ð²ÑÑÑнÑÑ. Ðогда поÑвиÑÑÑ ÑпиÑок маÑеÑиалов P0âP4, обÑÑение Ñамо пеÑейдÑÑ Ðº наÑÑÑойке клонов.","Enter a system name and click Find, or set planet counts manually. When the P0âP4 material list appears, the tour automatically continues to clone setup."), 255 actionLabel:guidedTourCopy("ÐÐ¶Ð¸Ð´Ð°Ñ Ð²Ð²Ð¾Ð´â¦","Waiting for inputâ¦") 256 }; 257 const waitForPlan={ 258 target:"#materialColumns",waitFor:"plan",awaitsInput:true, 259 title:guidedTourCopy("ÐÑбеÑиÑе желаемÑй маÑеÑиал","Choose the desired material"), 260 text:guidedTourCopy("ÐбÑÑение вÑеменно закÑоеÑÑÑ. ÐÑбеÑиÑе один или неÑколÑко доÑÑÑпнÑÑ Ð¼Ð°ÑеÑиалов â как ÑолÑко пÑогÑамма поÑÑÑÐ¾Ð¸Ñ Ð¿Ð»Ð°Ð½, обÑÑение пÑодолжиÑÑÑ Ð²Ð¾Ð·Ð»Ðµ гоÑового ÑезÑлÑÑаÑа и PI-Ñаблонов.","The tour pauses temporarily. Choose one or more available materials; as soon as the planner builds a plan, the tour resumes at the finished result and PI templates."), 261 actionLabel:guidedTourCopy("ÐÑбÑаÑÑ Ð¼Ð°ÑеÑиал","Choose material") 262 }; 263 const marketPlanChoice={ 264 target:"#marketSettings",choice:"marketPlan", 265 title:guidedTourCopy("Ðак поÑÑÑоиÑÑ Ð¿Ð»Ð°Ð½?","How should the plan be built?"), 266 text:guidedTourCopy("Ðожно поÑÑÑиÑÑ Ð¿ÑогÑамме найÑи ÑамÑй вÑгоднÑй доÑÑÑпнÑй маÑеÑиал Ñ ÑÑÑÑом наÑÑÑоек и Ñен либо вÑбÑаÑÑ Ð¶ÐµÐ»Ð°ÐµÐ¼Ñй маÑеÑиал ÑамоÑÑоÑÑелÑно.","Let the planner find the most profitable available material using your settings and prices, or choose the desired material yourself."), 267 secondaryLabel:guidedTourCopy("ÐелаемÑй маÑеÑиал","Desired material"), 268 primaryLabel:guidedTourCopy("СамÑй вÑгоднÑй","Most profitable") 269 }; 270 const marketCostsStep={ 271 target:"#marketSettings .marketSettingsGrid", 272 awaitsInput:true, 273 title:guidedTourCopy("УкажиÑе ÑаÑÑ Ð¾Ð´Ñ","Set your costs"), 274 text:guidedTourCopy("ÐÑовеÑÑÑе ÑоÑговÑй Ñ Ð°Ð±, налог Ñ Ð¿Ñодаж, комиÑÑÐ¸Ñ Ð±ÑокеÑа, ÑÑÐ°Ð²ÐºÑ Ñаможни и ÑÑоимоÑÑÑ Ð¿ÐµÑевозки за м³. ÐÑи знаÑÐµÐ½Ð¸Ñ Ð½Ð°Ð¿ÑÑмÑÑ Ð²Ð»Ð¸ÑÑÑ Ð½Ð° ÑаÑÑÑÑ Ð¸Ñоговой пÑибÑли и вÑÐ±Ð¾Ñ Ñамого вÑгодного маÑеÑиала.","Check the trade hub, sales tax, broker fee, customs rate, and hauling cost per m³. These values directly affect net profit and the most profitable material selection.") 275 }; 276 const resultVisible=state?!!state.resultVisible:guidedTourElementVisible($("result")); 277 const planVisible=state?!!state.planVisible:guidedTourElementVisible($("planPanel")); 278 const transfersVisible=state?!!state.transfersVisible:!!document.querySelector?.("#assignments .transferSummary"); 279 const finishedPlanSteps=planVisible?[planStep,...(transfersVisible?[transferStep]:[])]:[]; 280 if(phase==="setup")return resultVisible?setupSteps:[...setupSteps,waitForPlanner]; 281 if(phase==="planner")return resultVisible?[...plannerSteps,...finishedPlanSteps]:[waitForPlanner]; 282 if(phase==="materialChoice")return planVisible?finishedPlanSteps:[waitForPlan]; 283 if(phase==="marketChoice")return planVisible?finishedPlanSteps:[marketCostsStep,marketPlanChoice]; 284 if(phase==="plan")return planVisible?finishedPlanSteps:[waitForPlan]; 285 if(!resultVisible)return[...setupSteps,waitForPlanner]; 286 return[...setupSteps,...plannerSteps,...finishedPlanSteps]; 287} 288function guidedTourTarget(step){ 289 const target=document.querySelector?.(step?.target||""); 290 return target&&typeof target.getBoundingClientRect==="function"?target:null; 291} 292function positionGuidedTour(){ 293 const tour=$("guidedTour"),spotlight=$("guidedTourSpotlight"),pointer=$("guidedTourPointer"),card=$("guidedTourCard"); 294 if(!tour||tour.classList.contains("hidden")||!spotlight||!pointer||!card)return; 295 const step=guidedTourStepsCache[guidedTourIndex],target=guidedTourTarget(step); 296 if(!target){pointer.classList.add("hidden");return} 297 const viewportWidth=Math.max(320,window.innerWidth||document.documentElement?.clientWidth||320);
298 const viewportHeight=Math.max(320,window.innerHeight||document.documentElement?.clientHeight||320); 299 const rect=target.getBoundingClientRect(),padding=9,edge=8; 300 const left=Math.max(edge,rect.left-padding),top=Math.max(edge,rect.top-padding); 301 const right=Math.min(viewportWidth-edge,rect.right+padding),bottom=Math.min(viewportHeight-edge,rect.bottom+padding); 302 Object.assign(spotlight.style,{left:`${left}px`,top:`${top}px`,width:`${Math.max(24,right-left)}px`,height:`${Math.max(24,bottom-top)}px`}); 303 const pointerWidth=38,pointerHeight=52,pointerX=Math.max(edge,Math.min(viewportWidth-pointerWidth-edge,left+(right-left-pointerWidth)/2)); 304 const pointerAbove=top>=pointerHeight+12; 305 pointer.classList.remove("hidden");pointer.classList.toggle("pointsUp",!pointerAbove); 306 Object.assign(pointer.style,{left:`${pointerX}px`,top:`${pointerAbove?top-pointerHeight-7:bottom+7}px`}); 307 card.style.visibility="hidden"; 308 card.style.left=`${edge}px`;card.style.top=`${edge}px`; 309 const cardRect=card.getBoundingClientRect(),cardWidth=Math.min(cardRect.width||430,viewportWidth-edge*2),cardHeight=cardRect.height||250; 310 const below=bottom+14,above=top-cardHeight-14; 311 let cardLeft=Math.max(edge,Math.min(viewportWidth-cardWidth-edge,rect.left+rect.width/2-cardWidth/2)); 312 let cardTop=below+cardHeight<=viewportHeight-edge?below:(above>=edge?above:Math.max(edge,viewportHeight-cardHeight-edge)); 313 if(step.cardPlacement==="right"){ 314 if(right+14+cardWidth<=viewportWidth-edge){cardLeft=right+14;cardTop=Math.max(edge,Math.min(viewportHeight-cardHeight-edge,top))} 315 else if(left-cardWidth-14>=edge){cardLeft=left-cardWidth-14;cardTop=Math.max(edge,Math.min(viewportHeight-cardHeight-edge,top))} 316 } 317 Object.assign(card.style,{left:`${cardLeft}px`,top:`${cardTop}px`,visibility:"visible"}); 318} 319function renderGuidedTour(){ 320 if(!guidedTourStepsCache.length)guidedTourStepsCache=guidedTourSteps(guidedTourPhase); 321 guidedTourIndex=Math.max(0,Math.min(guidedTourStepsCache.length-1,guidedTourIndex)); 322 const step=guidedTourStepsCache[guidedTourIndex];if(!step)return; 323 if(step.choice==="marketEvaluation"&&guidedTourMarketEvaluationEnabled()){ 324 switchGuidedTourPhase("marketChoice");return; 325 } 326 $("guidedTourTitle").textContent=step.title;$("guidedTourText").textContent=step.text; 327 $("guidedTourStepLabel").textContent=guidedTourCopy(`ÐÐУЧÐÐÐР· ШÐÐ ${guidedTourIndex+1} ÐÐ ${guidedTourStepsCache.length}`,`TOUR · STEP ${guidedTourIndex+1} OF ${guidedTourStepsCache.length}`); 328 $("guidedTourPrev").textContent=guidedTourCopy("Ðазад","Back"); 329 $("guidedTourSkip").textContent=guidedTourCopy("ÐÑопÑÑÑиÑÑ","Skip"); 330 $("guidedTourNext").textContent=step.actionLabel||(guidedTourIndex===guidedTourStepsCache.length-1?guidedTourCopy("ÐоÑово","Done"):guidedTourCopy("Ðалее","Next")); 331 $("guidedTourPrev").disabled=guidedTourIndex===0; 332 const hasChoices=!!step.choice; 333 const waitingForInput=!!step.waitFor;
334 $("guidedTourChoices").classList.toggle("hidden",!hasChoices); 335 $("guidedTourNext").classList.toggle("hidden",hasChoices||waitingForInput); 336 $("guidedTourSpotlight").classList.toggle("awaitingInput",!!step.awaitsInput); 337 if(waitingForInput)setGuidedTourResumePhase(step.waitFor); 338 if(hasChoices){ 339 $("guidedTourChoiceSecondary").textContent=step.secondaryLabel||guidedTourCopy("ÐеÑ","No"); 340 $("guidedTourChoicePrimary").textContent=step.primaryLabel||guidedTourCopy("Ðа","Yes"); 341 } 342 $("guidedTourProgressBar").style.width=`${((guidedTourIndex+1)/guidedTourStepsCache.length)*100}%`; 343 const target=guidedTourTarget(step); 344 guidedTourAwaitTarget?.classList.remove("guidedTourAwaitTarget"); 345 guidedTourAwaitTarget=step.awaitsInput?target:null; 346 guidedTourAwaitTarget?.classList.add("guidedTourAwaitTarget"); 347 target?.scrollIntoView?.({behavior:"smooth",block:"center",inline:"nearest"}); 348 const schedule=typeof requestAnimationFrame==="function"?requestAnimationFrame:callback=>setTimeout(callback,0); 349 schedule(positionGuidedTour);setTimeout(positionGuidedTour,260); 350} 351function closeGuidedTour(remember=false,goal="guided_tour_closed"){ 352 const tour=$("guidedTour");if(!tour)return; 353 tour.classList.add("hidden");tour.setAttribute("aria-hidden","true"); 354 document.body.classList.remove("guidedTourOpen"); 355 guidedTourAwaitTarget?.classList.remove("guidedTourAwaitTarget");guidedTourAwaitTarget=null; 356 if(remember)try{localStorage.setItem(GUIDED_TOUR_STORAGE_KEY,"completed");localStorage.removeItem(GUIDED_TOUR_RESUME_KEY)}catch(_error){} 357 metrikaGoal(goal,{steps:guidedTourStepsCache.length,automatic:guidedTourAutomatic?1:0}); 358 guidedTourReturnFocus?.focus?.();guidedTourReturnFocus=null;guidedTourAutomatic=false;guidedTourPhase="all"; 359} 360function guidedTourResumePhase(){try{return localStorage.getItem(GUIDED_TOUR_RESUME_KEY)||""}catch(_error){return""}} 361function setGuidedTourResumePhase(phase){try{if(phase)localStorage.setItem(GUIDED_TOUR_RESUME_KEY,phase);else localStorage.removeItem(GUIDED_TOUR_RESUME_KEY)}catch(_error){}} 362function openGuidedTour(automatic=false,phase="all"){ 363 const tour=$("guidedTour");if(!tour)return; 364 guidedTourReturnFocus=automatic?null:document.activeElement; 365 guidedTourAutomatic=automatic;guidedTourPhase=phase;guidedTourIndex=0;guidedTourStepsCache=guidedTourSteps(phase); 366 tour.classList.remove("hidden");tour.setAttribute("aria-hidden","false");document.body.classList.add("guidedTourOpen"); 367 metrikaGoal("guided_tour_started",{steps:guidedTourStepsCache.length,automatic:automatic?1:0}); 368 renderGuidedTour();$("guidedTourNext")?.focus?.(); 369} 370function guidedTourMarketEvaluationEnabled(){ 371 const control=$("marketEvaluation"); 372 return control&&typeof control.checked==="boolean"?control.checked:!!marketSettings.enabled; 373} 374function shouldSkipGuidedTourMarketQuestion(direction,phase,enabled,nextStep){ 375 return direction>0&&(phase==="planner"||phase==="all")&&!!enabled&&nextStep?.choice==="marketEvaluation"; 376} 377function moveGuidedTour(direction){ 378 const step=guidedTourStepsCache[guidedTourIndex]; 379 if(direction>0&&step?.waitFor)return; 380 if(direction<0&&step?.waitFor)setGuidedTourResumePhase(""); 381 if(shouldSkipGuidedTourMarketQuestion(direction,guidedTourPhase,guidedTourMarketEvaluationEnabled(),guidedTourStepsCache[guidedTourIndex+1])){ 382 switchGuidedTourPhase("marketChoice");return; 383 } 384 if(direction>0&&guidedTourIndex===guidedTourStepsCache.length-1){closeGuidedTour(true,"guided_tour_completed");return} 385 guidedTourIndex=Math.max(0,Math.min(guidedTourStepsCache.length-1,guidedTourIndex+direction));renderGuidedTour(); 386} 387function switchGuidedTourPhase(phase){ 388 guidedTourPhase=phase;guidedTourIndex=0;guidedTourStepsCache=guidedTourSteps(phase);renderGuidedTour(); 389} 390async function handleGuidedTourChoice(primary){ 391 const step=guidedTourStepsCache[guidedTourIndex]; 392 if(!step?.choice)return; 393 const buttons=[$("guidedTourChoiceSecondary"),$("guidedTourChoicePrimary")]; 394 buttons.forEach(button=>button.disabled=true); 395 try{ 396 if(step.choice==="marketEvaluation"){ 397 await setMarketEvaluation(primary); 398 switchGuidedTourPhase(primary?"marketChoice":"materialChoice"); 399 }else if(step.choice==="marketPlan"){ 400 if(!primary){switchGuidedTourPhase("materialChoice");return} 401 setGuidedTourResumePhase("plan"); 402 closeGuidedTour(false,"guided_tour_auto_profit"); 403 await buildMaximumProfitPlan(); 404 } 405 }finally{buttons.forEach(button=>button.disabled=false)} 406} 407function skipGuidedTour(){closeGuidedTour(true,"guided_tour_skipped")} 408function maybeResumeGuidedTour(phase){ 409 const tour=$("guidedTour"),active=!tour?.classList.contains("hidden"); 410 const currentStep=guidedTourStepsCache[guidedTourIndex],waitingHere=active&¤tStep?.waitFor===phase; 411 if(!waitingHere&&guidedTourResumePhase()!==phase)return; 412 const ready=phase==="planner"?guidedTourElementVisible($("result")):phase==="plan"?guidedTourElementVisible($("planPanel")):false; 413 if(!ready)return; 414 setGuidedTourResumePhase(""); 415 setTimeout(()=>{ 416 if(!$("guidedTour")?.classList.contains("hidden")&&guidedTourStepsCache[guidedTourIndex]?.waitFor===phase)switchGuidedTourPhase(phase); 417 else if($("guidedTour")?.classList.contains("hidden"))openGuidedTour(true,phase); 418 },320); 419} 420 421function planetIcon(pt){ 422 const safe=String(pt).replace(/[^A-Za-z]/g,""); 423 return `<span class="planetIcon planetIcon${safe}" aria-hidden="true"></span>`;
424} 425 426function clampPlanetCount(v){return Math.max(0,Math.min(99,Math.round(Number(v)||0)))} 427function clearSelectedSystem(){ 428 selectedSystemInfo=null; 429 const input=$("systemInput"); 430 if(input)input.value=""; 431} 432function renderPlanetCountForm(){ 433 $("planetCountForm").innerHTML=D.meta.planetTypes.map(pt=>`<label class="planetCountRow"> 434 <span class="planetTypeLabel">${planetIcon(pt)}${localizedPlanetType(pt)}</span> 435 <input type="number" min="0" max="99" step="1" data-pt="${pt}" value="${planetCounts[pt]||0}" aria-label="ÐолиÑеÑÑво планеÑ: ${localizedPlanetType(pt)}"> 436 </label>`).join(""); 437 document.querySelectorAll(".planetCountRow input").forEach(input=>input.oninput=e=>{ 438 clearSelectedSystem(); 439 const pt=e.target.dataset.pt; 440 planetCounts[pt]=clampPlanetCount(e.target.value); 441 planetCountsDirty=true; 442 allocationMemo=new Map(); 443 invalidateProfitabilityRanking(); 444 renderPotential(); 445 if(current)renderProfitabilityRanking(); 446 if(planetAutoApplyTimer){clearTimeout(planetAutoApplyTimer);planetAutoApplyTimer=null} 447 if(Object.values(planetCounts).some(n=>n>0)){ 448 planetAutoApplyTimer=setTimeout(()=>{ 449 planetAutoApplyTimer=null; 450 run(); 451 },650); 452 } 453 }); 454} 455function manualSystem(){ 456 return { 457 id:null, 458 name:"Ð ÑÑной ÑоÑÑав", 459 security:0, 460 planets:D.meta.planetTypes.map(pt=>planetCounts[pt]||0) 461 }; 462} 463function normalizeSystemName(value){return String(value||"").trim().toLocaleLowerCase("en-US")} 464function systemSuggestions(query,limit=10){ 465 const normalized=normalizeSystemName(query); 466 if(!normalized)return[]; 467 return D.systems 468 .filter(system=>normalizeSystemName(system.name).includes(normalized)) 469 .sort((a,b)=>{ 470 const aPrefix=normalizeSystemName(a.name).startsWith(normalized)?0:1; 471 const bPrefix=normalizeSystemName(b.name).startsWith(normalized)?0:1; 472 return aPrefix-bPrefix||a.name.length-b.name.length||a.name.localeCompare(b.name); 473 }) 474 .slice(0,limit); 475} 476function localSystemByName(name){ 477 const normalized=normalizeSystemName(name); 478 return D.systems.find(system=>normalizeSystemName(system.name)===normalized)||null; 479} 480function emptyPlanetCounts(){return Object.fromEntries(D.meta.planetTypes.map(type=>[type,0]))} 481function countsFromPlanetTypeIds(typeIds){ 482 const counts=emptyPlanetCounts(); 483 for(const typeId of typeIds){ 484 const type=PLANET_TYPE_BY_ID[Number(typeId)]; 485 if(!type)throw new Error(`ÐеизвеÑÑнÑй Ñип планеÑÑ ESI: ${typeId}`); 486 counts[type]++; 487 } 488 return counts; 489} 490function countsFromSystemRecord(system){ 491 return Object.fromEntries(D.meta.planetTypes.map((type,index)=>[type,clampPlanetCount(system?.planets?.[index])])) 492} 493function systemCacheKey(systemId){return `planetHubSystem:v2:${systemId}`} 494function readSystemCache(systemId){ 495 try{ 496 const cached=JSON.parse(localStorage.getItem(systemCacheKey(systemId))||"null"); 497 if(cached&&Date.now()-Number(cached.savedAt)<SYSTEM_CACHE_TTL&&cached.counts&&Array.isArray(cached.planets))return cached; 498 }catch(_){} 499 return null; 500} 501function writeSystemCache(system,counts,planets){ 502 try{localStorage.setItem(systemCacheKey(system.id),JSON.stringify({savedAt:Date.now(),name:system.name,counts,planets}))}catch(_){} 503} 504function romanToNumber(value){ 505 const values={I:1,V:5,X:10,L:50,C:100,D:500,M:1000}; 506 let total=0,previous=0; 507 for(const char of String(value||"").toUpperCase().split("").reverse()){ 508 const current=values[char]||0; 509 total+=current<previous?-current:current; 510 previous=Math.max(previous,current); 511 } 512 return total; 513} 514function planetOrderFromName(name,fallback){ 515 const match=String(name||"").match(/\s([IVXLCDM]+)(?=\s|\(|$)/i); 516 return match?romanToNumber(match[1]):fallback; 517} 518function planetRadiusKmById(id){ 519 const values=window.EVE_PLANET_RADII,target=Number(id); 520 if(!Array.isArray(values)||!Number.isInteger(target)||target<=0)return 0; 521 let low=0,high=Math.floor(values.length/2)-1; 522 while(low<=high){ 523 const middle=(low+high)>>1,index=middle*2,current=values[index]; 524 if(current===target)return Math.max(0,Number(values[index+1])||0); 525 if(current<target)low=middle+1;else high=middle-1; 526 } 527 return 0; 528} 529function normalizedPlanetDetails(planets){ 530 if(!Array.isArray(planets))return[]; 531 return planets.map((planet,index)=>({ 532 id:Number(planet.id??planet.planet_id)||null, 533 name:String(planet.name||"").trim(),
534 pt:PLANET_TYPE_BY_ID[Number(planet.type_id)]||String(planet.pt||""), 535 order:Math.max(1,Math.round(Number(planet.order)||planetOrderFromName(planet.name,index+1))), 536 radiusKm:Math.max(0,Number(planet.radiusKm??planet.radius_km??planet.radius)||planetRadiusKmById(planet.id??planet.planet_id)) 537 })).filter(planet=>planet.name&&D.meta.planetTypes.includes(planet.pt)).sort((a,b)=>a.order-b.order||a.name.localeCompare(b.name)); 538} 539async function resolveSystemByName(name){ 540 const local=localSystemByName(name); 541 if(local)return local; 542 const response=await fetch("https://esi.evetech.net/latest/universe/ids/?datasource=tranquility",{ 543 method:"POST",headers:{Accept:"application/json","Content-Type":"application/json"},body:JSON.stringify([String(name).trim()]) 544 }); 545 if(!response.ok)throw new Error(`ESI ${response.status}`); 546 const data=await response.json(); 547 const systems=data.systems||data.solar_systems||[]; 548 const match=systems.find(system=>normalizeSystemName(system.name)===normalizeSystemName(name)); 549 if(!match){const error=new Error("СиÑÑема не найдена");error.code="SYSTEM_NOT_FOUND";throw error} 550 return{id:Number(match.id),name:match.name,security:null,planets:null}; 551} 552async function fetchSystemPlanetCounts(system,force=false){ 553 if(!force){ 554 const cached=readSystemCache(system.id); 555 if(cached)return{counts:Object.fromEntries(D.meta.planetTypes.map(type=>[type,clampPlanetCount(cached.counts[type])])),planets:normalizedPlanetDetails(cached.planets),source:"cache"}; 556 } 557 const systemData=await fetchJson(`https://esi.evetech.net/latest/universe/systems/${system.id}/?datasource=tranquility&language=en`); 558 const planetRefs=Array.isArray(systemData.planets)?systemData.planets:[]; 559 if(!planetRefs.length)throw new Error("ESI не веÑнÑл ÑпиÑок планеÑ"); 560 const details=await Promise.all(planetRefs.map(planet=>fetchJson(`https://esi.evetech.net/latest/universe/planets/${planet.planet_id}/?datasource=tranquility`))); 561 const planets=normalizedPlanetDetails(details.map((planet,index)=>({...planet,id:planetRefs[index].planet_id,order:planetOrderFromName(planet.name,index+1)}))); 562 const counts=countsFromPlanetTypeIds(details.map(planet=>planet.type_id)); 563 writeSystemCache(system,counts,planets); 564 return{counts,planets,source:"esi"}; 565} 566function hideSystemSuggestions(){ 567 $("systemSuggestions").classList.add("hidden"); 568 $("systemInput").setAttribute("aria-expanded","false"); 569}
570function renderSystemSuggestions(query){ 571 const suggestions=systemSuggestions(query); 572 const box=$("systemSuggestions"); 573 if(!query.trim()||!suggestions.length){hideSystemSuggestions();return suggestions} 574 box.innerHTML=suggestions.map(system=>`<button type="button" class="systemSuggestion" role="option" data-system-id="${system.id}">${esc(system.name)}</button>`).join(""); 575 box.classList.remove("hidden"); 576 $("systemInput").setAttribute("aria-expanded","true"); 577 box.querySelectorAll(".systemSuggestion").forEach(button=>button.onmousedown=event=>{ 578 event.preventDefault(); 579 const system=D.systems.find(item=>item.id===Number(button.dataset.systemId)); 580 if(system)searchAndApplySystem(system); 581 }); 582 return suggestions; 583} 584function showSystemMessage(message,isError=false,markInput=false){ 585 const box=$("systemSearchMessage"),input=$("systemInput"); 586 if(systemMessageTimer)clearTimeout(systemMessageTimer); 587 box.className=`systemSearchMessage${isError?" error":""}`; 588 box.textContent=translateSiteText(message); 589 void box.offsetWidth; 590 box.classList.add("visible"); 591 input.classList.toggle("systemSearchError",!!markInput); 592 systemMessageTimer=setTimeout(()=>{ 593 box.classList.remove("visible"); 594 input.classList.remove("systemSearchError"); 595 systemMessageTimer=null; 596 },3000); 597} 598function setSystemSearchBusy(busy){ 599 $("systemFindBtn").disabled=busy; 600 $("systemFindBtn").textContent=translateSiteText(busy?"ÐоиÑкâ¦":"ÐайÑи"); 601 $("systemLookup").setAttribute("aria-busy",String(busy)); 602} 603async function searchAndApplySystem(systemCandidate=null){ 604 const query=String(systemCandidate?.name||$("systemInput").value).trim(); 605 if(!query){showSystemMessage("ÐведиÑе название ÑиÑÑемÑ",true,true);return false} 606 const token=++systemLookupToken; 607 hideSystemSuggestions();setSystemSearchBusy(true); 608 try{ 609 const system=systemCandidate||await resolveSystemByName(query); 610 let result; 611 try{result=await fetchSystemPlanetCounts(system)} 612 catch(error){ 613 if(!Array.isArray(system.planets))throw error; 614 result={counts:countsFromSystemRecord(system),planets:[],source:"sde"}; 615 } 616 if(token!==systemLookupToken)return false; 617 planetCounts={...result.counts}; 618 selectedSystemInfo={id:system.id,name:system.name,security:system.security??null,planets:D.meta.planetTypes.map(type=>planetCounts[type]||0),planetDetails:normalizedPlanetDetails(result.planets),source:result.source}; 619 renderPlanetCountForm(); 620 $("systemInput").value=system.name; 621 run(); 622 showSystemMessage(result.source==="sde"?"СиÑÑема найдена. ESI недоÑÑÑпен â иÑполÑÐ·Ð¾Ð²Ð°Ð½Ñ ÑезеÑвнÑе даннÑе.":"СиÑÑема найдена, ÑоÑÑав Ð¿Ð»Ð°Ð½ÐµÑ Ð·Ð°Ð¿Ð¾Ð»Ð½ÐµÐ½"); 623 return true; 624 }catch(error){ 625 if(token!==systemLookupToken)return false; 626 if(error.code==="SYSTEM_NOT_FOUND")showSystemMessage("СиÑÑема не найдена",true,true); 627 else showSystemMessage("Ðе ÑдалоÑÑ Ð¿Ð¾Ð»ÑÑиÑÑ Ð´Ð°Ð½Ð½Ñе ÑиÑÑÐµÐ¼Ñ Ð¸Ð· ESI",true,false); 628 return false; 629 }finally{ 630 if(token===systemLookupToken)setSystemSearchBusy(false); 631 } 632} 633function initSystemLookup(){ 634 const input=$("systemInput"); 635 input.oninput=()=>{ 636 input.classList.remove("systemSearchError"); 637 if(selectedSystemInfo&&normalizeSystemName(input.value)!==normalizeSystemName(selectedSystemInfo.name))selectedSystemInfo=null; 638 renderSystemSuggestions(input.value); 639 }; 640 input.onkeydown=event=>{ 641 if(event.key==="Enter"){event.preventDefault();searchAndApplySystem()} 642 else if(event.key==="Escape")hideSystemSuggestions(); 643 }; 644 input.onblur=()=>setTimeout(hideSystemSuggestions,150); 645 input.onfocus=()=>{if(input.value.trim())renderSystemSuggestions(input.value)}; 646 $("systemFindBtn").onclick=()=>searchAndApplySystem(); 647} 648initSystemLookup(); 649renderPlanetCountForm(); 650 651function toast(msg){ 652 const old=document.querySelector(".toast"); if(old)old.remove(); 653 const x=document.createElement("div");x.className="toast";x.textContent=translateSiteText(msg);document.body.appendChild(x); 654 setTimeout(()=>x.remove(),2600); 655} 656function metrikaGoal(goal,params={}){ 657 try{ 658 if(typeof window.ym==="function")window.ym(METRIKA_COUNTER_ID,"reachGoal",goal,params); 659 }catch(err){ 660 console.warn("PLANET HUB: ÑобÑÑие ÐеÑÑики не оÑпÑавлено",goal,err); 661 } 662} 663function planAnalyticsSignature(finalPlan){ 664 return JSON.stringify({ 665 selected:[...selected].sort((a,b)=>a-b), 666 planetCounts:D.meta.planetTypes.map(type=>planetCounts[type]||0), 667 clones:clones.map(clone=>clone.limit), 668 maxUtilization:!!$("maxUtilization").checked, 669 globalP0P1:!!globalPlanetSettings.p0p1, 670 productionSettings:{ 671 separateP0:!!productionSettings.separateP0, 672 sharedP0Limit:productionSettings.sharedP0Limit, 673 separateFactories:!!productionSettings.separateFactories, 674 factoryLimit:clampFactoryLimit(productionSettings.factoryLimit) 675 }, 676 colonies:finalPlan.map(colony=>({ 677 type:colony.pt, 678 p0:[...colony.p0].sort((a,b)=>a-b), 679 tasks:colony.tasks.map(task=>[task.productId,task.kind,Number(task.factoryCount)||1]) 680 })) 681 }); 682} 683function trackCreatedPlan(finalPlan){ 684 if(!finalPlan.length||!selected.length)return false;
685 const signature=planAnalyticsSignature(finalPlan); 686 if(trackedPlanSignatures.has(signature))return false; 687 trackedPlanSignatures.add(signature); 688 metrikaGoal("plan_created",{ 689 selected_count:selected.length, 690 used_planets:finalPlan.length, 691 free_planets:Math.max(0,cap()-finalPlan.length) 692 }); 693 return true; 694} 695function cap(){return clones.reduce((a,c)=>a+c.limit,0)} 696function cloneCommandCenterLevel(index){return Math.max(1,Math.min(6,Math.round(Number(clones[index]?.ccLevel)||6)))} 697function fix(v){v=Math.round(Number(v)||1);return Math.max(1,Math.min(6,v))} 698function esc(s){return String(s??"").replace(/[&<>"']/g,m=>({"&":"&","<":"<",">":">",'"':""","'":"'"}[m]))} 699function ensureTargetColor(id){ 700 if(!targetColors[id]){ 701 const used=new Set(Object.values(targetColors)); 702 targetColors[id]=TARGET_PALETTE.find(c=>!used.has(c))||TARGET_PALETTE[Object.keys(targetColors).length%TARGET_PALETTE.length]; 703 } 704 return targetColors[id]; 705} 706function targetsForProduct(productId){ 707 return selected.filter(targetId=>dependencyProducts([targetId]).some(p=>p.id===productId)); 708} 709function colorDots(targetIds){ 710 if(selected.length<2)return""; 711 return `<span class="targetDots" aria-hidden="true">${targetIds.map(id=>`<span class="targetDot" style="--target-color:${ensureTargetColor(id)}"></span>`).join("")}</span>`; 712} 713function clampPercent(v){return Math.max(0,Math.min(100,Number(v)||0))} 714function clampMoney(v){return Math.max(0,Number(v)||0)} 715function clampFactoryLimit(value){return Math.max(1,Math.min(10,Math.round(Number(value)||1)))} 716function migratedFactoryLimit(settings){ 717 if(settings?.factoryLimit!=null)return clampFactoryLimit(settings.factoryLimit); 718 const limits={1:1,2:1,3:1,4:1,...settings?.factoryLimits}; 719 let limit=Math.max(...[1,2,3,4].map(tier=>clampFactoryLimit(limits[tier]))); 720 const mixed=settings?.mixedStages||{}; 721 if(mixed[12])limit=Math.max(limit,clampFactoryLimit(limits[1])+clampFactoryLimit(limits[2])); 722 if(mixed[23])limit=Math.max(limit,clampFactoryLimit(limits[2])+clampFactoryLimit(limits[3])); 723 if(mixed[34])limit=Math.max(limit,clampFactoryLimit(limits[3])+clampFactoryLimit(limits[4])); 724 return clampFactoryLimit(limit); 725} 726function normalizedProductionSettings(settings={}){ 727 return{ 728 separateP0:settings.separateP0!==false, 729 sharedP0Limit:Math.max(1,Math.min(10,Math.round(Number(settings.sharedP0Limit)||1))), 730 separateFactories:!!settings.separateFactories, 731 factoryLimit:migratedFactoryLimit(settings) 732 }; 733} 734function localDate(value){return value?new Date(value+"T00:00:00Z").toLocaleDateString(siteLocale(),{timeZone:"UTC"}):"â"} 735function requiredUnitsPerTarget(productId){ 736 const required=new Map([[productId,1]]); 737 const chain=dependencyProducts([productId]).filter(p=>p.t>=1).sort((a,b)=>b.t-a.t); 738 for(const product of chain){ 739 const outputUnits=required.get(product.id)||0; 740 if(!outputUnits)continue; 741 const cycles=outputUnits/Math.max(1,Number(product.q)||1); 742 for(const [inputId,quantity] of product.i||[]){ 743 const input=products.get(inputId); 744 if(!input||input.t===0)continue; 745 required.set(inputId,(required.get(inputId)||0)+cycles*Math.max(0,Number(quantity)||0)); 746 } 747 } 748 return required; 749} 750function allocationOutputPerDay(allocation,productId){ 751 if(!allocation?.ok)return 0; 752 const factoryCounts=new Map(); 753 for(const placement of allocation.cover?.stagePlacements||[]){ 754 factoryCounts.set(placement.productId,(factoryCounts.get(placement.productId)||0)+1); 755 } 756 const required=requiredUnitsPerTarget(productId); 757 let targetPerHour=Infinity; 758 for(const [id,unitsPerTarget] of required){ 759 const product=products.get(id),count=factoryCounts.get(id)||0; 760 if(!product||!count||!(unitsPerTarget>0))return 0; 761 const capacity=count*(Number(product.q)||1)*3600/Math.max(1,Number(product.c)||3600); 762 targetPerHour=Math.min(targetPerHour,capacity/unitsPerTarget); 763 } 764 return Number.isFinite(targetPerHour)?targetPerHour*24:0; 765} 766function allocationOutputsPerDay(allocation,targetIds){ 767 const capacities=new Map(); 768 for(const placement of allocation?.cover?.stagePlacements||[]){ 769 const product=products.get(placement.productId); 770 if(!product)continue;
771 const hourly=(Number(product.q)||1)*3600/Math.max(1,Number(product.c)||3600); 772 capacities.set(product.id,(capacities.get(product.id)||0)+hourly); 773 } 774 const targets=[...new Set((targetIds||[]).map(Number))].filter(id=>products.get(id)?.t>0); 775 const requestedCopies=targetCopyMap(targets,allocation?.targetCopies||1); 776 const nominal=new Map(targets.map(id=>{ 777 const product=products.get(id); 778 const perFactory=(Number(product?.q)||1)*3600/Math.max(1,Number(product?.c)||3600); 779 return[id,Math.min(capacities.get(id)||0,(requestedCopies.get(id)||1)*perFactory)]; 780 })); 781 const requirements=new Map(targets.map(id=>[id,requiredUnitsPerTarget(id)])); 782 const sharedFactors=new Map(); 783 for(const id of new Set(targets.flatMap(targetId=>[...requirements.get(targetId).keys()]))){ 784 const demand=targets.reduce((sum,targetId)=>sum+(requirements.get(targetId).get(id)||0)*(nominal.get(targetId)||0),0); 785 const capacity=capacities.get(id)||0; 786 sharedFactors.set(id,demand>0?Math.min(1,capacity/demand):1); 787 } 788 return new Map(targets.map(targetId=>{ 789 const needed=requirements.get(targetId),scale=[...needed.keys()].reduce((value,id)=>Math.min(value,sharedFactors.get(id)??1),1); 790 return[targetId,(nominal.get(targetId)||0)*scale*24]; 791 })); 792} 793function allocationRateFactorsForTargets(allocation,targetIds){ 794 const outputs=allocationOutputsPerDay(allocation,targetIds),factoryCounts=new Map(); 795 for(const placement of allocation?.cover?.stagePlacements||[])factoryCounts.set(placement.productId,(factoryCounts.get(placement.productId)||0)+1); 796 const factors=new Map(); 797 for(const productId of new Set([...outputs.keys()].flatMap(targetId=>[...requiredUnitsPerTarget(targetId).keys()]))){ 798 const product=products.get(productId),count=factoryCounts.get(productId)||0; 799 const capacity=count*(Number(product?.q)||1)*3600/Math.max(1,Number(product?.c)||3600); 800 const requiredPerHour=[...outputs].reduce((sum,[targetId,perDay])=>sum+(requiredUnitsPerTarget(targetId).get(productId)||0)*perDay/24,0); 801 factors.set(productId,capacity>0?Math.min(1,requiredPerHour/capacity):0); 802 } 803 return factors; 804} 805function productionLineEconomics(product){ 806 const average30=marketPrices.get(product.id),trend=marketTrends.get(product.id); 807 if(average30==null)return null; 808 const averageDailyVolume=Number.isFinite(trend?.averageDailyVolume)?trend.averageDailyVolume:null; 809 return {average30,calculationPrice:average30,averageDailyVolume}; 810} 811function maximumPackedAllocation(productId){ 812 const limit=clampFactoryLimit(productionSettings.factoryLimit); 813 const maximumAttempts=Math.max(1,cap()*limit); 814 let best=null,copies=0; 815 for(let candidateCopies=1;candidateCopies<=maximumAttempts;candidateCopies++){ 816 const candidate=calculateAllocation([productId],new Map(),candidateCopies,true); 817 if(!candidate.ok)break; 818 best=candidate;copies=candidateCopies; 819 } 820 return best?{allocation:best,copies}:null; 821} 822function reserveCover(reserved,cover){ 823 cover.colonies.forEach(colony=>reserved.set(colony.colonyKey,colony)); 824} 825function productionTotalsForBuiltPlan(built,targetIds){ 826 const totals=new Map(); 827 for(const segment of built?.segments||[]){ 828 const segmentTargets=segment.targetProductId==null?targetIds:[segment.targetProductId]; 829 const outputs=allocationOutputsPerDay(segment.allocation,segmentTargets); 830 for(const [targetId,producedPerDay] of outputs){ 831 if(!(producedPerDay>0))continue; 832 const targetLines=(segment.allocation.cover?.stagePlacements||[]).filter(x=>x.productId===targetId).length; 833 const row=totals.get(targetId)||{producedPerDay:0,targetLines:0}; 834 row.producedPerDay+=producedPerDay;row.targetLines+=targetLines;totals.set(targetId,row); 835 } 836 } 837 return totals; 838} 839function netSaleBreakdown(product,quantity,price){ 840 const gross=Math.max(0,Number(quantity)||0)*Math.max(0,Number(price)||0); 841 const salesTax=gross*marketSettings.tax/100; 842 const broker=gross*marketSettings.broker/100; 843 const finalExport=customsExportFee(product,quantity); 844 const hauling=(PRODUCT_VOLUME[product.t]||0)*quantity*marketSettings.hauling; 845 return{gross,salesTax,broker,finalExport,hauling,net:gross-salesTax-broker-finalExport-hauling}; 846} 847function evaluateBuiltPlanEconomics(built,targetIds){ 848 const totals=productionTotalsForBuiltPlan(built,targetIds); 849 const p0Requirements=p0RequirementsForPlan(built.plan); 850 const lines=[]; 851 for(const [id,total] of totals){ 852 const output=products.get(id),economics=productionLineEconomics(output); 853 if(!output||!economics)continue; 854 const saleablePerDay=economics.averageDailyVolume==null?total.producedPerDay:Math.min(total.producedPerDay,economics.averageDailyVolume); 855 const sale=netSaleBreakdown(output,saleablePerDay*30,economics.calculationPrice); 856 lines.push({id,name:output.n,tier:output.t,count:total.targetLines,producedPerDay:total.producedPerDay,saleablePerDay,net30Days:sale.net,average30:economics.average30,calculationPrice:economics.calculationPrice,averageDailyVolume:economics.averageDailyVolume,...sale}); 857 } 858 if(!lines.length)return null; 859 const customsTransfer=interplanetaryCustomsFees(built.plan,30*24); 860 const sum=key=>lines.reduce((value,line)=>value+(line[key]||0),0); 861 const gross30Days=sum("gross"),salesTax30Days=sum("salesTax"),broker30Days=sum("broker"),finalExport30Days=sum("finalExport"),hauling30Days=sum("hauling"); 862 const total30Days=sum("net")-customsTransfer.total; 863 lines.sort((a,b)=>b.net30Days-a.net30Days); 864 return{total30Days,gross30Days,salesTax30Days,broker30Days,finalExport30Days,hauling30Days,customsTransfer30Days:customsTransfer.total,customsTransferExport30Days:customsTransfer.exportFee,customsTransferImport30Days:customsTransfer.importFee,customsTransferLines:customsTransfer.byProduct,p0Requirements,usedPlanets:built.plan.length,freePlanets:Math.max(0,cap()-built.plan.length),lines}; 865} 866function simulateProductionConfiguration(product){ 867 // The selected product must first have a feasible minimum chain. Maximum 868 // utilization is an expansion strategy and must never invalidate that base. 869 const first=calculateAllocation([product.id],new Map(),1,false); 870 if(!first.ok||!first.cover.count)return null;
871 const built=buildPlanetInstructionsFor([product.id],first,!!$("maxUtilization")?.checked); 872 const economics=evaluateBuiltPlanEconomics(built,[product.id]); 873 if(!economics)return null; 874 const mainLine=economics.lines.find(x=>x.id===product.id); 875 return{...economics,product,targetLines:mainLine?.count||0,mainLine}; 876} 877 878function autoProfitConfigurations(){ 879 const configurations=[]; 880 const separateFactoryOptions=productionSettings.separateFactories?[true]:[false,true]; 881 for(const separateP0 of [true,false])for(const p0p1 of [true,false])for(const separateFactories of separateFactoryOptions)for(const maxUtilization of [true,false]){ 882 configurations.push({separateP0,p0p1,separateFactories,maxUtilization}); 883 } 884 return configurations; 885} 886function applyAutoProfitConfiguration(configuration){ 887 productionSettings={...productionSettings,separateP0:configuration.separateP0,separateFactories:configuration.separateFactories}; 888 globalPlanetSettings={...globalPlanetSettings,p0p1:configuration.p0p1}; 889 $("maxUtilization").checked=configuration.maxUtilization; 890 allocationMemo=new Map(); 891} 892async function buildMaximumProfitPlan(){ 893 if(autoProfitRunning)return; 894 if(!current){toast("СнаÑала ÑкажиÑе колиÑеÑÑво планеÑ");return} 895 if(!marketSettings.enabled){toast("СнаÑала вклÑÑиÑе оÑÐµÐ½ÐºÑ ÑÑоимоÑÑи маÑеÑиалов");return} 896 if(marketLoading||!marketPrices.size){toast("ÐождиÑеÑÑ Ð·Ð°Ð³ÑÑзки ÑÑноÑнÑÑ Ñен");return} 897 const button=$("autoProfitBtn"),status=$("autoProfitStatus"),token=++autoProfitToken; 898 const saved={productionSettings:JSON.parse(JSON.stringify(productionSettings)),globalPlanetSettings:{...globalPlanetSettings},maxUtilization:!!$("maxUtilization").checked,selected:[...selected]}; 899 const configurations=autoProfitConfigurations(),candidates=D.products.filter(product=>product.t>0&&marketPrices.get(product.id)!=null); 900 const total=Math.max(1,configurations.length*candidates.length);let completed=0,best=null,applied=false; 901 autoProfitRunning=true;button.disabled=true;invalidateProfitabilityRanking(); 902 $("calculationProgress").classList.remove("hidden");setCalculationProgress(1,"ÐодгоÑовка авÑоподбоÑа макÑималÑной пÑибÑлиâ¦"); 903 try{ 904 for(const configuration of configurations){ 905 if(token!==autoProfitToken)return; 906 applyAutoProfitConfiguration(configuration); 907 for(const product of candidates){ 908 const result=simulateProductionConfiguration(product);completed++; 909 if(result&&(!best||result.total30Days>best.result.total30Days||(result.total30Days===best.result.total30Days&&result.usedPlanets<best.result.usedPlanets)))best={configuration:JSON.parse(JSON.stringify(configuration)),product,result}; 910 if(completed%6===0){ 911 const percent=Math.max(1,Math.min(99,Math.round(completed/total*100))); 912 setCalculationProgress(percent,`ÐеÑебиÑаем пÑоизводÑÑвеннÑе конÑигÑÑаÑии⦠${completed.toLocaleString(siteLocale())} / ${total.toLocaleString(siteLocale())}`); 913 await new Promise(resolve=>setTimeout(resolve,0)); 914 } 915 } 916 } 917 if(!best||!(best.result.total30Days>0)){ 918 status.textContent="ÐÑибÑлÑнÑй ваÑÐ¸Ð°Ð½Ñ Ð½Ðµ найден.";toast("ÐÑибÑлÑнÑй ваÑÐ¸Ð°Ð½Ñ Ð½Ðµ найден.");return; 919 } 920 applyAutoProfitConfiguration(best.configuration);selected=[best.product.id];ensureTargetColor(best.product.id);renderProductionControls(); 921 status.textContent=`ÐпÑималÑнÑй план поÑÑÑоен: ${best.product.n} â ${Math.round(best.result.total30Days).toLocaleString(siteLocale())} ISK / 30 дней`; 922 metrikaGoal("auto_profit_plan",{material_id:best.product.id,material_name:best.product.n,tier:best.product.t});autosave();applied=true; 923 }catch(error){console.error(error);toast("ÐÑибка авÑоподбоÑа: "+error.message)} 924 finally{ 925 if(!applied){productionSettings=saved.productionSettings;globalPlanetSettings=saved.globalPlanetSettings;$("maxUtilization").checked=saved.maxUtilization;selected=saved.selected;renderProductionControls()} 926 autoProfitRunning=false;button.disabled=false;$("calculationProgress").classList.add("hidden"); 927 }
928 if(applied){invalidateProfitabilityRanking();recalc()} 929} 930 931function p0RequirementsForPlan(plan){ 932 const perHour=new Map(),extractFallback=new Map(); 933 for(const planet of plan||[])for(const task of planet.tasks||[]){ 934 const product=products.get(task.productId); 935 const factories=Math.max(1,Number(task.factoryCount)||1); 936 const rateFactor=Number.isFinite(Number(task.rateFactor))?Number(task.rateFactor):1; 937 if(task.kind==="assembly"&&product?.t===1){ 938 const cycles=factories*rateFactor*3600/Math.max(1,Number(product.c)||3600); 939 for(const [inputId,quantity] of product.i||[])perHour.set(inputId,(perHour.get(inputId)||0)+cycles*Math.max(0,Number(quantity)||0)); 940 }else if(task.kind==="extract"&&product?.t===0){ 941 const p1=directP1ForP0(product.id); 942 const inputQuantity=p1?.i?.find(([id])=>id===product.id)?.[1]||0; 943 const quantity=factories*rateFactor*inputQuantity*3600/Math.max(1,Number(p1?.c)||3600); 944 extractFallback.set(product.id,(extractFallback.get(product.id)||0)+quantity); 945 } 946 } 947 for(const [id,quantity] of extractFallback)if(!perHour.has(id))perHour.set(id,quantity); 948 return [...perHour].map(([id,hour])=>({id,name:products.get(id)?.n||String(id),hour,day:hour*24,days30:hour*24*30})).sort((a,b)=>a.name.localeCompare(b.name)); 949} 950function profitabilitySignature(){ 951 if(!current||!marketSettings.enabled||marketLoading)return""; 952 return JSON.stringify({ 953 planets:planetCounts,clones:clones.map(c=>c.limit),globalPlanetSettings,productionSettings, 954 maxUtilization:!!$("maxUtilization")?.checked,hub:marketSettings.hub,tax:marketSettings.tax,broker:marketSettings.broker,customsTax:marketSettings.customsTax, 955 hauling:marketSettings.hauling,prices:[...marketPrices].sort((a,b)=>a[0]-b[0]),trends:[...marketTrends].sort((a,b)=>a[0]-b[0]),date:marketPriceDate 956 }); 957}
958function invalidateProfitabilityRanking(){ 959 profitabilityStateRevision++; 960 profitabilityRankingToken++; 961 profitabilityRankingCache=[]; 962 profitabilityRankingSignature=""; 963 profitabilityRankingVerifiedSignature=""; 964 profitabilityRankingStage=""; 965} 966function profitabilityRunIsCurrent(signature,revision,token){ 967 return token===profitabilityRankingToken&&revision===profitabilityStateRevision&&signature===profitabilitySignature(); 968} 969function profitabilityResultsMatch(first,second){ 970 if(!first||!second||first.length!==second.length)return false; 971 const right=new Map(second.map(item=>[item.product.id,item])); 972 return first.every(item=>{ 973 const other=right.get(item.product.id); 974 if(!other||item.usedPlanets!==other.usedPlanets||item.targetLines!==other.targetLines||item.lines.length!==other.lines.length)return false; 975 const tolerance=Math.max(1,Math.abs(item.total30Days)*1e-9); 976 return Math.abs(item.total30Days-other.total30Days)<=tolerance; 977 }); 978} 979async function calculateProfitabilityPass(candidates,signature,revision,token){ 980 const ranked=[]; 981 for(const product of candidates){ 982 if(!profitabilityRunIsCurrent(signature,revision,token))return null; 983 const result=simulateProductionConfiguration(product); 984 if(result)ranked.push(result); 985 await new Promise(resolve=>setTimeout(resolve,0)); 986 if(!profitabilityRunIsCurrent(signature,revision,token))return null; 987 } 988 ranked.sort((a,b)=>b.total30Days-a.total30Days||a.usedPlanets-b.usedPlanets||a.product.n.localeCompare(b.product.n)); 989 return ranked; 990} 991async function ensureProfitabilityRanking(){ 992 const signature=profitabilitySignature(); 993 if(!signature)return[]; 994 if(signature===profitabilityRankingVerifiedSignature&&signature===profitabilityRankingSignature&&profitabilityRankingCache.length)return profitabilityRankingCache; 995 if(profitabilityRankingPromise&&profitabilityRankingPromiseSignature===signature&&profitabilityRankingPromiseRevision===profitabilityStateRevision)return profitabilityRankingPromise; 996 const token=++profitabilityRankingToken; 997 const revision=profitabilityStateRevision; 998 const candidates=D.products.filter(product=>product.t>0&&marketPrices.get(product.id)!=null); 999 profitabilityRankingStage="СÑавниваем вÑе доÑÑÑпнÑе пÑоизводÑÑвеннÑе конÑигÑÑаÑииâ¦"; 1000 const run=(async()=>{ 1001 const first=await calculateProfitabilityPass(candidates,signature,revision,token); 1002 if(!first)return[]; 1003 profitabilityRankingStage="ÐÑовеÑÑем акÑÑалÑноÑÑÑ ÑезÑлÑÑаÑа повÑоÑнÑм ÑаÑÑÑÑомâ¦"; 1004 renderProfitabilityRanking(); 1005 const verified=await calculateProfitabilityPass(candidates,signature,revision,token); 1006 if(!verified||!profitabilityRunIsCurrent(signature,revision,token))return[]; 1007 if(!profitabilityResultsMatch(first,verified)){ 1008 profitabilityRankingStage="РезÑлÑÑаÑÑ Ð¸Ð·Ð¼ÐµÐ½Ð¸Ð»Ð¸ÑÑ â вÑполнÑем новÑй ÑаÑÑÑÑâ¦"; 1009 return[]; 1010 } 1011 profitabilityRankingCache=verified; 1012 profitabilityRankingSignature=signature; 1013 profitabilityRankingVerifiedSignature=signature; 1014 profitabilityRankingStage=""; 1015 return verified; 1016 })(); 1017 profitabilityRankingPromise=run; 1018 profitabilityRankingPromiseSignature=signature; 1019 profitabilityRankingPromiseRevision=revision; 1020 try{return await run} 1021 finally{ 1022 if(profitabilityRankingPromise===run){ 1023 profitabilityRankingPromise=null; 1024 profitabilityRankingPromiseSignature=""; 1025 profitabilityRankingPromiseRevision=-1; 1026 } 1027 } 1028} 1029function compactPrice(value){ 1030 if(value>=1e9)return `${(value/1e9).toLocaleString(siteLocale(),{maximumFractionDigits:1})} ${siteLanguage==="en"?"bn":"млÑд"}`; 1031 if(value>=1e6)return `${(value/1e6).toLocaleString(siteLocale(),{maximumFractionDigits:1})} ${siteLanguage==="en"?"M":"млн"}`; 1032 if(value>=1e3)return `${(value/1e3).toLocaleString(siteLocale(),{maximumFractionDigits:value>=1e5?0:1})} ${siteLanguage==="en"?"K":"ÑÑÑ."}`; 1033 return value.toLocaleString(siteLocale(),{maximumFractionDigits:2}); 1034} 1035function marketIndicator(id){ 1036 if(!marketSettings.enabled)return""; 1037 const price=marketPrices.get(id); 1038 const trend=marketTrends.get(id); 1039 if(price==null)return marketLoading?`<span class="priceTrend pending" title="Цена загÑÑжаеÑÑÑ">â¦</span>`:`<span class="priceTrend flat" title="ÐедоÑÑаÑоÑно ÑÑноÑнÑÑ Ð´Ð°Ð½Ð½ÑÑ Ð·Ð° 30 дней">â</span>`;
1040 const reference=Number.isFinite(trend?.change)?` · ÐоÑледний завеÑÑÑннÑй Ð´ÐµÐ½Ñ Ð¾ÑноÑиÑелÑно ÑÑедней: ${trend.change>=0?"+":""}${trend.change.toFixed(1)}%`:""; 1041 return `<span class="priceTrend marketPrice" title="СÑедневзвеÑÐµÐ½Ð½Ð°Ñ Ñена Ñделок за ${trend?.sampleDays||30} дней: ${price.toLocaleString(siteLocale(),{maximumFractionDigits:2})} ISK за единиÑÑ${reference}">${compactPrice(price)}</span>`; 1042} 1043function marketHistorySummary(days){ 1044 if(!days.length)return null; 1045 const totalVolume=days.reduce((sum,x)=>sum+(Number(x.volume)||0),0); 1046 const weightedValue=days.reduce((sum,x)=>sum+(Number(x.average)||0)*(Number(x.volume)||0),0); 1047 const average30=totalVolume>0?weightedValue/totalVolume:days.reduce((sum,x)=>sum+Number(x.average),0)/days.length; 1048 return{average30,averageDailyVolume:totalVolume/days.length}; 1049} 1050function marketCacheKey(kind){ 1051 const today=new Date().toISOString().slice(0,10); 1052 return `evePiMarket:v5:${kind}:${marketSettings.hub}:${today}`; 1053} 1054function restoreTrendCache(){ 1055 marketTrends=new Map();marketPriceDate=null; 1056 try{ 1057 const cached=JSON.parse(localStorage.getItem(marketCacheKey("trends"))||"null"); 1058 if(cached?.items){ 1059 marketTrends=new Map(cached.items.map(x=>[x.id,x])); 1060 marketPrices=new Map(cached.items.filter(x=>Number.isFinite(x.average30)).map(x=>[x.id,x.average30])); 1061 marketPriceDate=cached.date||null;return marketPrices.size>0; 1062 } 1063 }catch(_){} 1064 return false; 1065} 1066async function fetchJson(url){ 1067 const response=await fetch(url,{headers:{Accept:"application/json"}}); 1068 if(!response.ok)throw new Error(`ESI ${response.status}`); 1069 return response.json(); 1070} 1071async function mapConcurrent(items,limit,worker,onProgress){ 1072 let cursor=0,done=0; 1073 async function runner(){ 1074 while(cursor<items.length){ 1075 const index=cursor++,item=items[index]; 1076 try{await worker(item,index)}catch(err){console.warn("Market request failed",item.id||item,err)} 1077 done++;onProgress?.(done,items.length); 1078 } 1079 } 1080 await Promise.all(Array.from({length:Math.min(limit,items.length)},runner)); 1081} 1082function updateMarketStatus(text){$("marketStatus").textContent=translateSiteText(text)} 1083function refreshMarketViews(){ 1084 renderPotential(); 1085 if(current)mats(); 1086} 1087async function loadMarketTrends(force=false){ 1088 if(!marketSettings.enabled)return; 1089 const token=++marketLoadToken; 1090 if(!force&&restoreTrendCache()){ 1091 marketLoading=false;$("marketLoadTrack").classList.add("hidden"); 1092 updateMarketStatus(`Ð¦ÐµÐ½Ñ Ð°ÐºÑÑалÑÐ½Ñ Ð½Ð° ${localDate(marketPriceDate)} · ÑÑедневзвеÑÐµÐ½Ð½Ð°Ñ Ñена Ñделок за 30 дней`); 1093 refreshMarketViews();if(selected.length)plan();return; 1094 } 1095 marketTrends=new Map();marketPrices=new Map();marketPriceDate=null;marketLoading=true; 1096 $("marketLoadTrack").classList.remove("hidden");$("marketLoadBar").style.width="0%"; 1097 updateMarketStatus("ÐагÑÑзка ÑÑÑоÑнÑÑ Ð¸Ð½Ð´ÐµÐºÑов: 0%â¦");refreshMarketViews(); 1098 const hub=MARKET_HUBS[marketSettings.hub],loaded=[]; 1099 await mapConcurrent(D.products,6,async p=>{ 1100 const history=await fetchJson(`https://esi.evetech.net/latest/markets/${hub.region}/history/?datasource=tranquility&type_id=${p.id}`); 1101 const today=new Date().toISOString().slice(0,10); 1102 const days=history.filter(x=>x.average>0&&x.date<today).sort((a,b)=>a.date.localeCompare(b.date)).slice(-30); 1103 if(days.length>=2){ 1104 const current=days[days.length-1].average; 1105 const {average30,averageDailyVolume}=marketHistorySummary(days); 1106 const item={id:p.id,current,average30,change:average30?((current-average30)/average30)*100:0,date:days[days.length-1].date,sampleDays:days.length,averageDailyVolume}; 1107 marketTrends.set(p.id,item);marketPrices.set(p.id,average30);loaded.push(item); 1108 if(!marketPriceDate||item.date>marketPriceDate)marketPriceDate=item.date; 1109 } 1110 },(done,total)=>{ 1111 if(token!==marketLoadToken)return; 1112 const percent=Math.round(done/total*100);$("marketLoadBar").style.width=`${percent}%`; 1113 updateMarketStatus(`ÐагÑÑзка ÑÑÑоÑнÑÑ Ð¸Ð½Ð´ÐµÐºÑов: ${percent}%â¦`); 1114 if(done%10===0)refreshMarketViews(); 1115 }); 1116 if(token!==marketLoadToken)return; 1117 marketLoading=false; 1118 $("marketLoadTrack").classList.add("hidden"); 1119 if(loaded.length){ 1120 try{localStorage.setItem(marketCacheKey("trends"),JSON.stringify({date:marketPriceDate,items:loaded}))}catch(_){} 1121 updateMarketStatus(`Ð¦ÐµÐ½Ñ Ð°ÐºÑÑалÑÐ½Ñ Ð½Ð° ${localDate(marketPriceDate)} · ÑÑедневзвеÑÐµÐ½Ð½Ð°Ñ Ñена Ñделок за 30 дней`); 1122 }else updateMarketStatus("Ðе ÑдалоÑÑ Ð¿Ð¾Ð»ÑÑиÑÑ ÑенÑ. ÐÑовеÑÑÑе подклÑÑение к инÑеÑнеÑÑ"); 1123 refreshMarketViews();if(selected.length)plan(); 1124} 1125function renderMarketControls(){ 1126 $("marketEvaluation").checked=marketSettings.enabled; 1127 $("marketSettings").classList.toggle("hidden",!marketSettings.enabled); 1128 $("marketHub").value=marketSettings.hub; 1129 $("marketTax").value=marketSettings.tax; 1130 $("marketBroker").value=marketSettings.broker;
1131 $("marketBroker").disabled=false; 1132 $("marketBroker").title="ÐÑÑавÑÑе 0, еÑли комиÑÑÐ¸Ñ Ð½Ðµ взимаеÑÑÑ"; 1133 $("customsTax").value=marketSettings.customsTax; 1134 $("marketHauling").value=marketSettings.hauling; 1135} 1136function showP0ExtractorLimit(){ 1137 return true; 1138} 1139function renderProductionControls(){ 1140 $("separateP0").checked=productionSettings.separateP0; 1141 $("sharedP0Limit").value=String(productionSettings.sharedP0Limit||1); 1142 $("separateFactories").checked=productionSettings.separateFactories; 1143 $("globalP0P1").checked=globalPlanetSettings.p0p1; 1144 $("factoryLimit").value=String(clampFactoryLimit(productionSettings.factoryLimit)); 1145 renderMarketControls(); 1146} 1147 1148function localizedCloneName(name){ 1149 const match=/^(?:Ðлон|Clone)\s+(\d+)$/.exec(String(name||"")); 1150 return match?`${siteLanguage==="en"?"Clone":"Ðлон"} ${match[1]}`:String(name||""); 1151} 1152function renderClones(){ 1153 const cloneField=c=>authorizedClones.length?`<select class="cn" aria-label="Ðлон EVE"><option value="">${esc(localizedCloneName(c.name))}</option>${authorizedClones.map(item=>`<option value="${item.character_id}" ${Number(c.characterId)===Number(item.character_id)?"selected":""}>${esc(item.character_name)}</option>`).join("")}</select>`:`<span class="cloneAutoName">${esc(localizedCloneName(c.name))}</span>`; 1154 $("cloneList").innerHTML=clones.map((c,i)=>`<div class="cloneRow" data-i="${i}"> 1155 ${cloneField(c)} 1156 <div class="limit">ÐланеÑ: <input class="cl" type="number" min="1" max="6" value="${c.limit}"> из 6</div> 1157 <div class="limit">УÑÐ¾Ð²ÐµÐ½Ñ ÐЦ: <input class="cc" type="number" min="1" max="6" value="${cloneCommandCenterLevel(i)}"> из 6</div> 1158 <button class="remove">УдалиÑÑ</button> 1159 </div>`).join(""); 1160 document.querySelectorAll(".cloneRow").forEach(r=>{ 1161 let i=+r.dataset.i; 1162 const nameField=r.querySelector(".cn"); 1163 if(nameField?.tagName==="SELECT")nameField.onchange=e=>{const found=authorizedClones.find(item=>String(item.character_id)===e.target.value);clones[i].characterId=found?.character_id||null;clones[i].name=found?.character_name||`Ðлон ${i+1}`;if(found)clones[i].ccLevel=found.command_center_level||6;renderClones();allocationMemo=new Map();invalidateProfitabilityRanking();recalc();autosave()}; 1164 r.querySelector(".cl").onchange=e=>{clones[i].limit=fix(e.target.value);e.target.value=clones[i].limit;totals();invalidateProfitabilityRanking();recalc();autosave()}; 1165 r.querySelector(".cc").onchange=e=>{clones[i].ccLevel=Math.max(1,Math.min(6,Math.round(Number(e.target.value)||6)));e.target.value=clones[i].ccLevel;allocationMemo=new Map();invalidateProfitabilityRanking();recalc();autosave()}; 1166 r.querySelector(".remove").onclick=()=>{ 1167 clones.splice(i,1); 1168 if(!clones.length)clones=[{name:"Ðлон 1",limit:6,ccLevel:6}]; 1169 renderClones();invalidateProfitabilityRanking();recalc();autosave(); 1170 }; 1171 }); 1172 totals(); 1173} 1174function totals(){$("cloneCount").textContent=clones.length;$("planetCapacity").textContent=cap()} 1175$("addCloneBtn").onclick=()=>{clones.push({name:`Ðлон ${clones.length+1}`,limit:6,ccLevel:6});renderClones();invalidateProfitabilityRanking();recalc();autosave()}; 1176$("maxUtilization").onchange=()=>{invalidateProfitabilityRanking();recalc();autosave()}; 1177$("separateP0").onchange=e=>{ 1178 productionSettings.separateP0=e.target.checked; 1179 allocationMemo=new Map(); 1180 invalidateProfitabilityRanking();recalc();autosave(); 1181}; 1182$("sharedP0Limit").onchange=e=>{ 1183 productionSettings.sharedP0Limit=Math.max(1,Math.min(10,Math.round(Number(e.target.value)||1))); 1184 e.target.value=String(productionSettings.sharedP0Limit);allocationMemo=new Map();invalidateProfitabilityRanking();recalc();autosave(); 1185}; 1186$("separateFactories").onchange=e=>{ 1187 productionSettings.separateFactories=e.target.checked; 1188 allocationMemo=new Map(); 1189 if(selected.length){ 1190 const test=calculateAllocation(selected,new Map(),1,balancedPlanningEnabled()&&selected.length>1); 1191 if(!test.ok)alert(translateSiteText(`ÐÑа конÑигÑÑаÑÐ¸Ñ Ð¿ÑоизводÑÑва невозможна.\n\n${test.reason||"Ðе Ñ Ð²Ð°ÑÐ°ÐµÑ Ð´Ð¾ÑÑÑпнÑÑ Ð¿Ð»Ð°Ð½ÐµÑаÑнÑÑ ÑлоÑов."}\n\nÐÑбÑаннÑе маÑеÑÐ¸Ð°Ð»Ñ ÑÐ¾Ñ ÑÐ°Ð½ÐµÐ½Ñ â измениÑе наÑÑÑойки, колиÑеÑÑво Ð¿Ð»Ð°Ð½ÐµÑ Ð¸Ð»Ð¸ клонов.`)); 1192 } 1193 invalidateProfitabilityRanking();recalc();autosave(); 1194}; 1195$("globalP0P1").onchange=e=>{ 1196 globalPlanetSettings.p0p1=e.target.checked; 1197 invalidateProfitabilityRanking();recalc();autosave(); 1198}; 1199async function setMarketEvaluation(enabled){ 1200 marketSettings.enabled=!!enabled;
1200invalidateProfitabilityRanking();renderMarketControls();autosave(); 1201 if(marketSettings.enabled)metrikaGoal("market_enabled"); 1202 if(marketSettings.enabled)await loadMarketTrends(false);else{marketLoadToken++;marketTrends=new Map();marketPrices=new Map();refreshMarketViews();if(selected.length)plan()} 1203} 1204$("marketEvaluation").onchange=e=>{void setMarketEvaluation(e.target.checked)}; 1205$("marketHub").onchange=e=>{ 1206 marketSettings.hub=e.target.value;invalidateProfitabilityRanking();marketTrends=new Map();marketPrices=new Map();marketPriceDate=null;autosave();loadMarketTrends(false); 1207}; 1208$("marketTax").onchange=e=>{marketSettings.tax=clampPercent(e.target.value);e.target.value=marketSettings.tax;invalidateProfitabilityRanking();autosave();refreshMarketViews();if(selected.length)plan()}; 1209$("marketBroker").onchange=e=>{marketSettings.broker=clampPercent(e.target.value);e.target.value=marketSettings.broker;invalidateProfitabilityRanking();autosave();refreshMarketViews();if(selected.length)plan()}; 1210$("customsTax").onchange=e=>{marketSettings.customsTax=clampPercent(e.target.value);e.target.value=marketSettings.customsTax;invalidateProfitabilityRanking();autosave();refreshMarketViews();if(selected.length)plan()}; 1211$("marketHauling").onchange=e=>{marketSettings.hauling=clampMoney(e.target.value);e.target.value=marketSettings.hauling;invalidateProfitabilityRanking();autosave();refreshMarketViews();if(selected.length)plan()}; 1212$("refreshMarketBtn").onclick=()=>{invalidateProfitabilityRanking();loadMarketTrends(true)}; 1213$("autoProfitBtn").onclick=()=>buildMaximumProfitPlan(); 1214$("factoryLimit").onchange=e=>{ 1215 productionSettings.factoryLimit=clampFactoryLimit(e.target.value); 1216 e.target.value=productionSettings.factoryLimit; 1217 allocationMemo=new Map();invalidateProfitabilityRanking();recalc();autosave(); 1218}; 1219renderProductionControls(); 1220renderClones(); 1221 1222function sysCounts(s){let x={};s.planets.forEach((n,i)=>x[D.meta.planetTypes[i]]=n||0);return x} 1223function p0set(pt){return new Set(D.p0ByPlanet[pt]||[])} 1224function localizedPlanetType(pt){ 1225 const m={Barren:"ÐÑÑÑоÑÑ",Gas:"Ðаз",Ice:"ÐÑд",Lava:"Ðава",Oceanic:"Ðкеан",Plasma:"Ðлазма",Storm:"ÐÑÑÑ",Temperate:"УмеÑеннаÑ"}; 1226 return siteLanguage==="en"?pt:(m[pt]||pt); 1227} 1228function commandCenterName(pt){return `${pt} Command Center`} 1229function commandCenterIcon(pt){ 1230 const safe=String(pt).replace(/[^A-Za-z]/g,"").toLowerCase(); 1231 return `<img class="commandCenterIcon" src="assets/command-centers/${safe}.png" alt="" aria-hidden="true">`; 1232} 1233function commandCenterCounts(planets){ 1234 const counts=new Map(); 1235 (planets||[]).forEach(planet=>counts.set(planet.pt,(counts.get(planet.pt)||0)+1)); 1236 return D.meta.planetTypes 1237 .map(pt=>({pt,count:counts.get(pt)||0})) 1238 .filter(item=>item.count>0); 1239} 1240function commandCenterCopyText(rows){ 1241 return rows.map(({pt,count})=>`${commandCenterName(pt)} ${count}`).join("\n"); 1242} 1243function renderCommandCenterSummary(planets){ 1244 const box=$("commandCenterSummary"); 1245 const rows=commandCenterCounts(planets); 1246 if(!rows.length){box.innerHTML="";box.classList.add("hidden");return} 1247 box.classList.remove("hidden"); 1248 const centerLabel=siteLanguage==="en"?"Command Center":"ÐоманднÑй ÑенÑÑ"; 1249 const planetLabel=siteLanguage==="en"?"planet":"планеÑа"; 1250 const unitLabel=siteLanguage==="en"?"units":"ÑÑ."; 1251 box.innerHTML=`<div class="commandCenterSummaryHead"><span>ÐÐÐÐÐ¥ÐÐÐÐЫРÐÐÐÐÐÐÐЫРЦÐÐТРЫ</span> 1252 <button id="copyCommandCenters" class="copyCommandCenters" type="button" title="СкопиÑоваÑÑ ÑпиÑок команднÑÑ ÑенÑÑов" aria-label="СкопиÑоваÑÑ ÑпиÑок команднÑÑ ÑенÑÑов">â§</button> 1253 </div> 1254 <div class="commandCenterList">${rows.map(({pt,count})=>`<div class="commandCenterRow"> 1255 <span class="commandCenterType">${commandCenterIcon(pt)}${centerLabel}: ${esc(localizedPlanetType(pt))} ${planetLabel}</span> 1256 <b>${count} ${unitLabel}</b> 1257 </div>`).join("")}</div>`; 1258 $("copyCommandCenters").onclick=()=>copyText(commandCenterCopyText(rows),"СпиÑок команднÑÑ ÑенÑÑов ÑкопиÑован"); 1259} 1260function numberedPlanetLabels(system){ 1261 const exact=normalizedPlanetDetails(system?.planetDetails); 1262 if(exact.length){ 1263 const counts=sysCounts(system),requestedTotal=Object.values(counts).reduce((sum,count)=>sum+count,0),labels=[];
1264 D.meta.planetTypes.forEach((pt,typeIndex)=>{ 1265 const known=exact.filter(planet=>planet.pt===pt),requested=requestedTotal?counts[pt]:known.length; 1266 known.slice(0,requested).forEach((planet,index)=>labels.push({ 1267 pt:planet.pt, 1268 ordinal:planet.order, 1269 sortOrder:planet.order, 1270 key:planet.id?`planet:${planet.id}`:`planet-order:${planet.order}:${index}`, 1271 label:`${planet.name} - ${localizedPlanetType(planet.pt)}`, 1272 planetId:planet.id, 1273 planetName:planet.name, 1274 radiusKm:planet.radiusKm||0, 1275 radiusSource:planet.id&&planet.radiusKm>0?"automatic":"standard" 1276 })); 1277 for(let index=known.length;index<requested;index++)labels.push({ 1278 pt,ordinal:index+1,sortOrder:1000+typeIndex*100+index,key:`manual-extra:${pt}:${index+1}`, 1279 label:`${localizedPlanetType(pt)} ${index+1}`,planetId:null,planetName:null,radiusKm:0,radiusSource:"standard" 1280 }); 1281 }); 1282 return labels.sort((a,b)=>a.sortOrder-b.sortOrder||a.label.localeCompare(b.label)); 1283 } 1284 const labels=[],counts=sysCounts(system); 1285 Object.keys(counts).forEach(pt=>{ 1286 for(let i=1;i<=counts[pt];i++)labels.push({pt,ordinal:i,sortOrder:D.meta.planetTypes.indexOf(pt)*100+i,key:`${pt}:${i}`,label:`${localizedPlanetType(pt)} ${i}`,planetId:null,planetName:null,radiusKm:0,radiusSource:"standard"}); 1287 }); 1288 return labels; 1289} 1290 1291function effectivePlanetSetting(key){ 1292 return {...globalPlanetSettings}; 1293} 1294function ensurePlanetSettings(){ 1295 planetSettings={}; 1296} 1297 1298function roots(id,m=new Map()){ 1299 if(m.has(id))return new Set(m.get(id)); 1300 let p=products.get(id),r=new Set; 1301 if(!p)return r; 1302 if(p.t===0)r.add(id); else p.i.forEach(([q])=>roots(q,m).forEach(x=>r.add(x))); 1303 m.set(id,[...r]);return r; 1304} 1305function dependencyProducts(ids){ 1306 let seen=new Set(); 1307 function walk(id){if(seen.has(id))return;seen.add(id);let p=products.get(id);if(!p)return;p.i.forEach(([iid])=>walk(iid))} 1308 ids.forEach(walk); 1309 return [...seen].map(id=>products.get(id)).filter(Boolean).sort((a,b)=>a.t-b.t||a.n.localeCompare(b.n)); 1310} 1311function theoreticalProductsByTier(){ 1312 const availableP0=new Set; 1313 D.meta.planetTypes.forEach(pt=>{ 1314 if((planetCounts[pt]||0)>0)(D.p0ByPlanet[pt]||[]).forEach(id=>availableP0.add(id)); 1315 }); 1316 const possible=new Set(availableP0); 1317 for(let tier=1;tier<=4;tier++){ 1318 D.products.filter(p=>p.t===tier).forEach(p=>{ 1319 if(p.i.every(([id])=>possible.has(id)))possible.add(p.id); 1320 }); 1321 } 1322 return [0,1,2,3,4].map(t=>D.products.filter(p=>p.t===t&&possible.has(p.id)).sort((a,b)=>a.n.localeCompare(b.n))); 1323} 1324function materialIcon(p,extraClass=""){ 1325 return `<img class="materialIcon ${extraClass}" src="https://images.evetech.net/types/${p.id}/icon?size=64" alt="" width="24" height="24" loading="lazy">`; 1326} 1327function materialInline(p,extraClass=""){ 1328 if(!p)return""; 1329 return `<span class="inlineMaterial ${extraClass}">${materialIcon(p,"inlineMaterialIcon")}<span>${esc(p.n)}</span></span>`; 1330} 1331async function copyText(text,successMessage){ 1332 if(!text){toast("ÐÐµÑ Ð´Ð¾ÑÑÑпнÑÑ ÐºÐ¾Ð¼Ð¿Ð¾Ð½ÐµÐ½Ñов Ð´Ð»Ñ ÐºÐ¾Ð¿Ð¸ÑованиÑ");return} 1333 try{ 1334 const area=document.createElement("textarea"); 1335 area.value=text;area.setAttribute("readonly",""); 1336 area.style.position="fixed";area.style.left="-9999px";area.style.top="0"; 1337 document.body.appendChild(area);area.focus();area.select();area.setSelectionRange(0,area.value.length); 1338 let copied=false; 1339 try{copied=!!document.execCommand("copy")}catch(_){} 1340 area.remove(); 1341 if(!copied&&navigator.clipboard?.writeText){await navigator.clipboard.writeText(text);copied=true} 1342 if(!copied){ 1343 window.prompt(translateSiteText("ÐвÑомаÑиÑеÑкое копиÑование заблокиÑовано бÑаÑзеÑом. ÐажмиÑе Ctrl+C, заÑем Enter:"),text); 1344 return; 1345 } 1346 toast(successMessage); 1347 }catch(_){toast("Ðе ÑдалоÑÑ ÑкопиÑоваÑÑ")} 1348} 1349function renderPotential(){ 1350 const byTier=theoreticalProductsByTier(); 1351 if(marketSettings.enabled)byTier.forEach(items=>items.sort((a,b)=>(marketPrices.get(b.id)??-1)-(marketPrices.get(a.id)??-1)||a.n.localeCompare(b.n))); 1352 $("potentialColumns").innerHTML=byTier.map((items,t)=>`<section class="potentialTier"> 1353 <div class="potentialHead"><b>P${t}</b><button class="copyTier" type="button" data-tier="${t}" title="СкопиÑоваÑÑ Ð´Ð¾ÑÑÑпнÑе компоненÑÑ P${t}" aria-label="СкопиÑоваÑÑ Ð´Ð¾ÑÑÑпнÑе компоненÑÑ P${t}">â§</button></div> 1354 <div class="potentialList">${items.map(p=>`<span>${materialIcon(p,"potentialMaterialIcon")}<span class="potentialMaterialName">${esc(p.n)}</span>${marketIndicator(p.id)}</span>`).join("")||`<em>ÐÐµÑ Ð´Ð¾ÑÑÑпнÑÑ </em>`}</div> 1355 </section>`).join(""); 1356 document.querySelectorAll(".copyTier").forEach(btn=>btn.onclick=()=>{ 1357 const t=Number(btn.dataset.tier),items=byTier[t]||[],text=items.map(p=>p.n).join("\n"); 1358 if(!text){toast(siteLanguage==="en"?`P${t} has no available components`:`Ð P${t} Ð½ÐµÑ Ð´Ð¾ÑÑÑпнÑÑ ÐºÐ¾Ð¼Ð¿Ð¾Ð½ÐµÐ½Ñов`);return} 1359 copyText(text,`P${t} ÑкопиÑован`); 1360 }); 1361} 1362renderPotential(); 1363function directP1ForP0(id){return D.products.find(p=>p.t===1&&p.i.some(([iid])=>iid===id))} 1364function stageKey(tier){return tier===1?"p0p1":tier===2?"p1p2":tier===3?"p2p3":"p3p4"} 1365function supportsFactoryTier(planetType,tier){return tier<4||planetType==="Barren"||planetType==="Temperate"} 1366function balancedPlanningEnabled(){return !!$("maxUtilization")?.checked} 1367 1368 1369function physicalPlanets(){ 1370 if(!current)return[]; 1371 ensurePlanetSettings(); 1372 return numberedPlanetLabels(current.system).map(p=>({...p,setting:effectivePlanetSetting(p.key)})); 1373} 1374 1375/* 1376 ÐÐÐÐÐ: 1377 одна и Ñа же ÑизиÑеÑÐºÐ°Ñ Ð¿Ð»Ð°Ð½ÐµÑа Ð¼Ð¾Ð¶ÐµÑ Ð±ÑÑÑ Ð¾Ñвоена Ð ÐÐÐЫÐРклонами. 1378 ÐгÑаниÑение ÑникалÑноÑÑи дейÑÑвÑÐµÑ ÑолÑко внÑÑÑи одного клона: 1379 один клон не Ð¼Ð¾Ð¶ÐµÑ Ð·Ð°Ð½ÑÑÑ Ð¾Ð´Ð½Ñ Ð¸ ÑÑ Ð¶Ðµ ÑизиÑеÑкÑÑ Ð¿Ð»Ð°Ð½ÐµÑÑ Ð´Ð²Ð°Ð¶Ð´Ñ. 1380*/ 1381function colonyCandidates(){ 1382 const pp=physicalPlanets(); 1383 const out=[];
1384 clones.forEach((c,ci)=>{ 1385 pp.forEach(p=>out.push({ 1386 colonyKey:`c${ci}:${p.key}`, 1387 cloneIndex:ci, 1388 cloneName:c.name, 1389 planetKey:p.key, 1390 pt:p.pt, 1391 ordinal:p.ordinal, 1392 sortOrder:p.sortOrder, 1393 label:p.label, 1394 planetId:p.planetId??null, 1395 planetName:p.planetName||null, 1396 radiusKm:Math.max(0,Number(p.radiusKm)||0), 1397 radiusSource:p.radiusSource||((p.planetId&&p.radiusKm>0)?"automatic":"standard"), 1398 setting:{...p.setting} 1399 })); 1400 }); 1401 return out; 1402} 1403function usedCountByClone(used){ 1404 const counts=Array(clones.length).fill(0); 1405 used.forEach((_,key)=>{ 1406 const m=/^c(\d+):/.exec(key); 1407 if(m)counts[+m[1]]++; 1408 }); 1409 return counts; 1410} 1411function canOpenColony(candidate,used){ 1412 if(used.has(candidate.colonyKey))return true; 1413 const counts=usedCountByClone(used); 1414 return counts[candidate.cloneIndex] < clones[candidate.cloneIndex].limit; 1415} 1416 1417/* 1418 ÐинималÑнÑй Ð½Ð°Ð±Ð¾Ñ "оÑвоений планеÑ" Ð´Ð»Ñ Ð½ÑжнÑÑ P0. 1419 ÐлÑÑ ÑлоÑа вклÑÑÐ°ÐµÑ ÐºÐ»Ð¾Ð½Ð°, поÑÑомÑ, напÑимеÑ, B6-XE8 VI Ð¼Ð¾Ð¶ÐµÑ Ð¾Ð´Ð½Ð¾Ð²Ñеменно 1420 иÑполÑзоваÑÑÑÑ Ðлоном 1 и Ðлоном 2. 1421*/ 1422function targetCopyMap(ids,targetCopies=1){ 1423 if(targetCopies instanceof Map)return new Map(ids.map(id=>[id,Math.max(1,Math.round(Number(targetCopies.get(id))||1))])); 1424 if(targetCopies&&typeof targetCopies==="object")return new Map(ids.map(id=>[id,Math.max(1,Math.round(Number(targetCopies[id])||1))])); 1425 const copies=Math.max(1,Math.round(Number(targetCopies)||1)); 1426 return new Map(ids.map(id=>[id,copies])); 1427} 1428function factoryRequirements(ids,targetCopies=1){ 1429 const requirements=new Map(targetCopyMap(ids,targetCopies)); 1430 for(let tier=4;tier>=2;tier--){ 1431 const inputRates=new Map(); 1432 for(const product of D.products.filter(p=>p.t===tier&&requirements.has(p.id))){ 1433 const factoryCount=requirements.get(product.id)||0; 1434 const cyclesPerHour=3600/Math.max(1,Number(product.c)||3600); 1435 for(const [inputId,quantity] of product.i||[]){ 1436 const input=products.get(inputId); 1437 if(input?.t>=1)inputRates.set(inputId,(inputRates.get(inputId)||0)+factoryCount*cyclesPerHour*quantity); 1438 } 1439 } 1440 for(const [inputId,unitsPerHour] of inputRates){ 1441 const input=products.get(inputId); 1442 const outputPerHour=(Number(input.q)||1)*3600/Math.max(1,Number(input.c)||3600); 1443 requirements.set(inputId,(requirements.get(inputId)||0)+Math.ceil(unitsPerHour/outputPerHour-1e-9)); 1444 } 1445 } 1446 return requirements; 1447} 1448function minimumFactoryRequirements(ids){ 1449 return new Map(dependencyProducts(ids).filter(product=>product.t>=1).map(product=>[product.id,1])); 1450} 1451function targetFactoryRequirements(ids,targetCopies=1,balanced=true){ 1452 const copies=targetCopyMap(ids,targetCopies),byTarget=new Map(); 1453 for(const targetId of ids){ 1454 const count=copies.get(targetId)||1; 1455 byTarget.set(targetId,balanced?factoryRequirements([targetId],count):minimumFactoryRequirements([targetId])); 1456 } 1457 return byTarget; 1458} 1459function allocationFactoryRequirements(ids,targetCopies=1,balanced=true){ 1460 const requirements=balanced?factoryRequirements(ids,targetCopies):minimumFactoryRequirements(ids); 1461 if(!productionSettings.separateP0||ids.length<2)return requirements; 1462 1463 // Shared recipes may occur in several selected chains (for example a 1464 // separately selected Biofuels line and Biofuels required by Biocells). 1465 // In separate-P0 mode each target keeps its own P0 -> P1 line instead of 1466 // silently reusing the first target's extractor and basic factory. 1467 const p1Totals=new Map(); 1468 for(const targetRequirements of targetFactoryRequirements(ids,targetCopies,balanced).values()){ 1469 for(const [productId,count] of targetRequirements){ 1470 if(products.get(productId)?.t===1)p1Totals.set(productId,(p1Totals.get(productId)||0)+count); 1471 } 1472 } 1473 for(const product of D.products.filter(p=>p.t===1))requirements.delete(product.id); 1474 for(const [productId,count] of p1Totals)requirements.set(productId,count); 1475 return requirements; 1476} 1477function extractionDemands(ids,targetCopies=1,balanced=true){ 1478 const copies=targetCopyMap(ids,targetCopies),requirements=allocationFactoryRequirements(ids,targetCopies,balanced),demands=[]; 1479 for(const id of ids){ 1480 if(products.get(id)?.t!==0)continue; 1481 for(let instance=1;instance<=(copies.get(id)||1);instance++)demands.push({id,targetId:id,productId:null,instance,token:`${id}>P0@${id}:${instance}`}); 1482 } 1483 if(productionSettings.separateP0){ 1484 for(const [targetId,targetRequirements] of targetFactoryRequirements(ids,targetCopies,balanced)){ 1485 for(const product of D.products.filter(p=>p.t===1&&targetRequirements.has(p.id))){ 1486 const count=targetRequirements.get(product.id)||0,inputId=product.i?.[0]?.[0]; 1487 if(inputId==null)continue; 1488 for(let instance=1;instance<=count;instance++)demands.push({id:inputId,targetId,productId:product.id,instance,token:`${inputId}>${product.id}@${targetId}:${instance}`}); 1489 } 1490 } 1491 return demands; 1492 } 1493 for(const product of D.products.filter(p=>p.t===1&&requirements.has(p.id))){ 1494 const count=requirements.get(product.id)||0,inputId=product.i?.[0]?.[0]; 1495 if(inputId==null)continue; 1496 const owner=ids.find(targetId=>dependencyProducts([targetId]).some(p=>p.id===product.id))||ids[0]||null; 1497 for(let instance=1;instance<=count;instance++)demands.push({id:inputId,targetId:owner,productId:product.id,instance,token:`${inputId}>${product.id}@${owner}:${instance}`}); 1498 } 1499 return demands; 1500} 1501 1502function mergedUsed(reserved,used){return new Map([...(reserved||new Map()),...used])} 1503function extractionCapacity(candidate){ 1504 return Math.min( 1505 candidate.setting.maxP0, 1506 productionSettings.sharedP0Limit||1, 1507 globalPlanetSettings.p0p1?clampFactoryLimit(productionSettings.factoryLimit):99 1508 ); 1509} 1510function minExtractionCover(demands,reserved=new Map()){ 1511 if(!demands.length)return{ok:true,count:0,colonies:[],planets:[],p0Assignment:{},p0AssignmentDetails:{}}; 1512 1513 const pool=colonyCandidates().filter(x=>!reserved.has(x.colonyKey)); 1514 const byKey=new Map(pool.map(x=>[x.colonyKey,x])); 1515 const candidatesByP0=new Map(); 1516 const demandByToken=new Map(demands.map(d=>[d.token,d])); 1517 const missing=new Set(); 1518 for(const d of demands){ 1519 const c=pool.filter(x=>p0set(x.pt).has(d.id)); 1520 if(!c.length){missing.add(d.id);continue} 1521 candidatesByP0.set(d.token,c.map(x=>x.colonyKey)); 1522 } 1523 if(missing.size)return{ok:false,missing:[...missing]}; 1524 1525 const ordered=demands.map(d=>d.token).sort((a,b)=>candidatesByP0.get(a).length-candidatesByP0.get(b).length); 1526 const used=new Map(); // colonyKey -> Set(demand token) 1527 for(const token of ordered){ 1528 const demand=demandByToken.get(token); 1529 const valid=key=>{ 1530 const cand=byKey.get(key),currentSet=used.get(key)||new Set(); 1531 if(!canOpenColony(cand,mergedUsed(reserved,used))||currentSet.size>=extractionCapacity(cand))return false;
1532 if(productionSettings.separateP0&&[...used.entries()].some(([usedKey,tokens])=> 1533 byKey.get(usedKey).planetKey===cand.planetKey&&[...tokens].some(x=>{ 1534 const existing=demandByToken.get(x); 1535 return existing.id===demand.id&&existing.targetId!==demand.targetId; 1536 }) 1537 ))return false; 1538 return true; 1539 }; 1540 const allKeys=candidatesByP0.get(token); 1541 const key=allKeys.filter(k=>used.has(k)&&valid(k)).sort((a,b)=>used.get(b).size-used.get(a).size)[0] 1542 ||allKeys.find(k=>!used.has(k)&&valid(k)); 1543 if(!key)return{ok:false,capacity:true}; 1544 if(!used.has(key))used.set(key,new Set()); 1545 used.get(key).add(token); 1546 } 1547 return{ 1548 ok:true,count:used.size, 1549 colonies:[...used.keys()].map(k=>byKey.get(k)), 1550 planets:[...used.keys()].map(k=>byKey.get(k)), 1551 p0Assignment:Object.fromEntries([...used.entries()].map(([k,v])=>[k,[...v].map(token=>demandByToken.get(token).id)])), 1552 p0AssignmentDetails:Object.fromEntries([...used.entries()].map(([k,v])=>[k,[...v].map(token=>({...demandByToken.get(token)}))])) 1553 }; 1554} 1555 1556function requiredStages(ids,targetCopies=1,balanced=true){ 1557 const requirements=allocationFactoryRequirements(ids,targetCopies,balanced),stages=[]; 1558 for(const product of dependencyProducts(ids).filter(p=>p.t>=1)){ 1559 const count=requirements.get(product.id)||0; 1560 for(let instance=1;instance<=count;instance++)stages.push({product,key:stageKey(product.t),instance,count}); 1561 } 1562 return stages; 1563} 1564 1565/* 1566 ÐобавлÑем меÑÑа Ð´Ð»Ñ Ð¿ÐµÑеÑабоÑки. 1567 1568 ÐалоÑки p0p1/p1p2/p2p3/p3p4 ознаÑаÑÑ Ð²Ð¾Ð·Ð¼Ð¾Ð¶Ð½Ð¾ÑÑÑ ÑовмеÑÑиÑÑ Ð¾ÑеÑедной 1569 ÑÑап Ñ Ð¸Ð·Ð³Ð¾Ñовлением его непоÑÑедÑÑвеннÑÑ ÐºÐ¾Ð¼Ð¿Ð¾Ð½ÐµÐ½Ñов на одной колонии. 1570 СнÑÑÐ°Ñ Ð³Ð°Ð»Ð¾Ñка ÐРзапÑеÑÐ°ÐµÑ ÑÑап: он пеÑеноÑиÑÑÑ Ð½Ð° оÑделÑнÑÑ ÑбоÑоÑнÑÑ 1571 колониÑ. 1572*/ 1573function addProcessingPlanets(extraction,ids,reserved=new Map(),targetCopies=1,balanced=true){ 1574 const pool=colonyCandidates().filter(x=>!reserved.has(x.colonyKey)); 1575 const used=new Map(extraction.colonies.map(x=>[x.colonyKey,x])); 1576 const factoryKeys=new Set(); 1577 const stageTasks=requiredStages(ids,targetCopies,balanced); 1578 const stageHomesByProduct=new Map(),stagePlacements=[]; 1579 const extractionKeys=new Set(extraction.colonies.map(x=>x.colonyKey)); 1580 const factoryCounts=new Map(); 1581 const totalFactories=key=>Object.values(factoryCounts.get(key)||{}).reduce((sum,count)=>sum+count,0); 1582 const hasStageCapacity=key=>totalFactories(key)<clampFactoryLimit(productionSettings.factoryLimit); 1583 const addStage=(key,tier)=>{ 1584 const counts=factoryCounts.get(key)||{}; 1585 counts[tier]=(counts[tier]||0)+1; 1586 factoryCounts.set(key,counts); 1587 factoryKeys.add(key); 1588 }; 1589 1590 // P0 already has a home: the colony that extracts it. 1591 for(const [colonyKey,p0ids] of Object.entries(extraction.p0Assignment)){ 1592 p0ids.forEach(id=>{ 1593 if(!stageHomesByProduct.has(id))stageHomesByProduct.set(id,[]); 1594 stageHomesByProduct.get(id).push(colonyKey); 1595 }); 1596 } 1597 1598 for(const st of stageTasks){ 1599 const inputHomes=new Set(st.product.i.flatMap(([id])=>stageHomesByProduct.get(id)||[])); 1600 1601 // СнаÑала ÑазмеÑаем ÑÑап ÑÑдом Ñ ÐµÐ³Ð¾ непоÑÑедÑÑвеннÑми компоненÑами: 1602 // ÑÑо ÑменÑÑÐ°ÐµÑ Ð¼ÐµÐ¶Ð¿Ð»Ð°Ð½ÐµÑнÑе пеÑевозки и ÑаможеннÑе ÑбоÑÑ. P0 â P1 1603 // Ð¿Ð¾Ð¿Ð°Ð´Ð°ÐµÑ Ð½Ð° добÑваÑÑÑÑ Ð¿Ð»Ð°Ð½ÐµÑÑ ÑолÑко пÑи вклÑÑÑнной пеÑеÑабоÑке на меÑÑе. 1604 let home=[...used.values()].find(x=>{ 1605 const coLocationAllowed=st.product.t===1?globalPlanetSettings.p0p1:x.setting[st.key]; 1606 const assemblyAllowed=st.product.t===1||!productionSettings.separateFactories||!extractionKeys.has(x.colonyKey); 1607 return supportsFactoryTier(x.pt,st.product.t)&&coLocationAllowed&&assemblyAllowed&&inputHomes.has(x.colonyKey)&&hasStageCapacity(x.colonyKey); 1608 }); 1609 1610 // ÐнаÑе иÑполÑзÑем Ñже оÑкÑÑÑÑÑ ÑбоÑоÑнÑÑ Ð¿Ð»Ð°Ð½ÐµÑÑ Ñ Ð¾Ð±Ñим ÑвободнÑм 1611 // меÑÑом. ÐдинаковÑе и ÑазнÑе ÑÑовни заводов ÑаÑÑ Ð¾Ð´ÑÑÑ Ð¾Ð´Ð¸Ð½ и ÑÐ¾Ñ Ð¶Ðµ лимиÑ. 1612 if(!home)home=[...used.values()].find(x=>{ 1613 const extractionReuseAllowed=!extractionKeys.has(x.colonyKey)||(st.product.t>1&&!productionSettings.separateFactories); 1614 return supportsFactoryTier(x.pt,st.product.t)&&factoryKeys.has(x.colonyKey)&&extractionReuseAllowed&&!inputHomes.has(x.colonyKey)&&hasStageCapacity(x.c
1614olonyKey); 1615 }); 1616 1617 // Otherwise open a separate factory colony. Its checkbox controls future 1618 // co-location; it does not prohibit production on this colony. 1619 if(!home){ 1620 const free=pool.find(x=>supportsFactoryTier(x.pt,st.product.t)&&!used.has(x.colonyKey)&&canOpenColony(x,mergedUsed(reserved,used))); 1621 if(free){ 1622 used.set(free.colonyKey,free); 1623 factoryKeys.add(free.colonyKey); 1624 home=free; 1625 } 1626 } 1627 1628 if(!home){ 1629 const stageLabel=st.product.t===1?"P0 â P1":`P${st.product.t-1} â P${st.product.t}`; 1630 if(st.product.t===4)return{ok:false,reason:"ÐÐ»Ñ Ð¿ÑоизводÑÑва P4 нÑжна ÑÐ²Ð¾Ð±Ð¾Ð´Ð½Ð°Ñ Ð¿Ð»Ð°Ð½ÐµÑа Ñипа «ÐÑÑÑоÑÑ» или «УмеÑеннаÑ»"}; 1631 return{ok:false,reason:`ÐÐ»Ñ Ð¾ÑделÑного ÑÑапа ${stageLabel} не Ñ Ð²Ð°ÑÐ°ÐµÑ Ñвободного планеÑаÑного ÑлоÑа Ñ ÐºÐ»Ð¾Ð½Ð¾Ð²`}; 1632 } 1633 1634 if(!stageHomesByProduct.has(st.product.id))stageHomesByProduct.set(st.product.id,[]); 1635 stageHomesByProduct.get(st.product.id).push(home.colonyKey); 1636 stagePlacements.push({productId:st.product.id,tier:st.product.t,instance:st.instance,homeKey:home.colonyKey}); 1637 addStage(home.colonyKey,st.product.t); 1638 } 1639 const stageHomes=Object.fromEntries([...stageHomesByProduct].filter(([id])=>products.get(+id)?.t>=1).map(([id,homes])=>[id,homes])); 1640 1641 return{ 1642 ok:true, 1643 colonies:[...used.values()], 1644 planets:[...used.values()], 1645 count:used.size, 1646 p0Assignment:extraction.p0Assignment, 1647 p0AssignmentDetails:extraction.p0AssignmentDetails, 1648 stageHomes, 1649 stagePlacements, 1650 factoryCounts:Object.fromEntries(factoryCounts), 1651 factoryColonies:[...factoryKeys] 1652 }; 1653} 1654 1655function allocationIntegrity(result,ids,targetCopies,balanced){ 1656 const errors=[]; 1657 if(!result?.ok||!result.cover)return{ok:true,mode:balanced?"balanced":"minimum",checks:0}; 1658 const cover=result.cover,colonies=cover.colonies||[],colonyKeys=new Set(colonies.map(x=>x.colonyKey)); 1659 if(colonyKeys.size!==colonies.length)errors.push("duplicate colony"); 1660 if(cover.count!==colonies.length)errors.push("colony count mismatch"); 1661 if(colonies.length+(result.reservedCount||0)>cap())errors.push("global colony limit exceeded");
1662 const cloneCounts=Array(clones.length).fill(0); 1663 colonies.forEach(colony=>{ 1664 if(colony.cloneIndex<0||colony.cloneIndex>=clones.length)errors.push("unknown clone"); 1665 else cloneCounts[colony.cloneIndex]++; 1666 }); 1667 cloneCounts.forEach((count,index)=>{if(count>(clones[index]?.limit||0))errors.push(`clone ${index+1} colony limit exceeded`)}); 1668 1669 const expected=allocationFactoryRequirements(ids,targetCopies,balanced),actual=new Map(); 1670 for(const placement of cover.stagePlacements||[]){ 1671 actual.set(placement.productId,(actual.get(placement.productId)||0)+1); 1672 const home=colonies.find(x=>x.colonyKey===placement.homeKey); 1673 if(!home)errors.push(`factory ${placement.productId} has no colony`); 1674 if(placement.tier===4&&home&&!supportsFactoryTier(home.pt,4))errors.push(`P4 factory on ${home.pt}`); 1675 } 1676 for(const [productId,count] of expected)if(products.get(productId)?.t>=1&&(actual.get(productId)||0)!==count)errors.push(`factory count ${productId}: ${actual.get(productId)||0}/${count}`); 1677 for(const [productId,count] of actual)if(!expected.has(productId)&&count)errors.push(`unexpected factory ${productId}`); 1678 1679 const factoriesByColony=new Map(); 1680 for(const placement of cover.stagePlacements||[])factoriesByColony.set(placement.homeKey,(factoriesByColony.get(placement.homeKey)||0)+1); 1681 for(const [key,count] of factoriesByColony)if(count>clampFactoryLimit(productionSettings.factoryLimit))errors.push(`factory limit exceeded on ${key}`); 1682 1683 for(const [key,p0ids] of Object.entries(cover.p0Assignment||{})){ 1684 const colony=colonies.find(x=>x.colonyKey===key); 1685 if(!colony){errors.push(`P0 assignment has no colony ${key}`);continue} 1686 for(const id of p0ids)if(!p0set(colony.pt).has(id))errors.push(`P0 ${id} unavailable on ${colony.pt}`); 1687 } 1688 const expectedDemands=new Set(extractionDemands(ids,targetCopies,balanced).map(d=>d.token)); 1689 const actualDetails=Object.values(cover.p0AssignmentDetails||{}).flat(); 1690 const actualDemands=new Set(actualDetails.map(d=>d.token)); 1691 for(const token of expectedDemands)if(!actualDemands.has(token))errors.push(`missing P0 demand ${token}`); 1692 for(const token of actualDemands)if(!expectedDemands.has(token))errors.push(`unexpected P0 demand ${token}`); 1693 for(const [key,details] of Object.entries(cover.p0AssignmentDetails||{})){ 1694 const colony=colonies.find(x=>x.colonyKey===key); 1695 if(colony&&details.length>extractionCapacity(colony))errors.push(`P0 line limit exceeded on ${key}`); 1696 } 1697 if(productionSettings.separateP0){ 1698 const targetsByPhysicalPlanet=new Map(); 1699 for(const [key,details] of Object.entries(cover.p0AssignmentDetails||{})){ 1700 const colony=colonies.find(x=>x.colonyKey===key);if(!colony)continue; 1701 if(!targetsByPhysicalPlanet.has(colony.planetKey))targetsByPhysicalPlanet.set(colony.planetKey,new Map()); 1702 const resources=targetsByPhysicalPlanet.get(colony.planetKey); 1703 for(const detail of details){ 1704 if(!resources.has(detail.id))resources.set(detail.id,new Set()); 1705 resources.get(detail.id).add(detail.targetId); 1706 } 1707 } 1708 for(const [planetKey,resources] of targetsByPhysicalPlanet)for(const [p0id,targetIds] of resources){ 1709 if(targetIds.size>1)errors.push(`shared P0 ${p0id} between targets on ${planetKey}`); 1710 } 1711 } 1712 const outputs=allocationOutputsPerDay(result,ids); 1713 ids.forEach(id=>{if((products.get(id)?.t||0)>0&&!((outputs.get(id)||0)>0))errors.push(`no target output ${id}`)}); 1714 return{ok:errors.length===0,mode:balanced?"balanced":"minimum",checks:13,errors}; 1715} 1716 1717function calculateAllocation(ids,reserved=new Map(),targetCopies=1,balanced=true){ 1718 const requestedCopies=targetCopyMap(ids,targetCopies); 1719 let need=new Set; 1720 ids.forEach(id=>roots(id).forEach(x=>need.add(x))); 1721 const extraction=minExtractionCover(extractionDemands(ids,targetCopies,balanced),reserved); 1722 if(!extraction.ok)return{ok:false,cover:extraction,need,balanced,selfCheck:{ok:true,mode:balanced?"balanced":"minimum",checks:1},reason:extraction.missing?"Ð ÑиÑÑеме оÑÑÑÑÑÑвÑÐµÑ Ð½ÐµÐ¾Ð±Ñ Ð¾Ð´Ð¸Ð¼Ñй P0":productionSettings.separateP0?"Ðе Ñ Ð²Ð°ÑÐ°ÐµÑ Ð¿Ð»Ð°Ð½ÐµÑаÑнÑÑ ÑлоÑов Ð´Ð»Ñ ÑазделÑной добÑÑи P0":"Ðе Ñ Ð²Ð°ÑÐ°ÐµÑ Ð¿Ð»Ð°Ð½ÐµÑаÑнÑÑ ÑлоÑов Ñ ÐºÐ»Ð¾Ð½Ð¾Ð² Ñ ÑÑÑÑом огÑаниÑений добÑÑи P0"}; 1723 const processing=addProcessingPlanets(extraction,ids,reserved,targetCopies,balanced); 1724 if(!processing.ok)return{ok:false,cover:extraction,need,balanced,selfCheck:{ok:true,mode:balanced?"balanced":"minimum",checks:1},reason:processing.reason}; 1725 const result={ok:true,cover:processing,need,balanced,reservedCount:reserved.size,free:cap()-reserved.size-processing.count,targetIds:[...ids],targetCopies:Object.fromEntries(requestedCopies)}; 1726 result.selfCheck=allocationIntegrity(result,ids,targetCopies,balanced); 1727 if(!result.selfCheck.ok){ 1728 console.error("PI allocation self-check failed",result.selfCheck.errors); 1729 return{...result,ok:false,internalError:true,reason:"ÐнÑÑÑеннÑÑ Ð¿ÑовеÑка обнаÑÑжила оÑÐ¸Ð±ÐºÑ Ð¿Ð¾ÑÑÑÐ¾ÐµÐ½Ð¸Ñ Ð¿Ð»Ð°Ð½Ð°. ÐзмениÑе наÑÑÑойки и оÑпÑавÑÑе оÑладоÑнÑй Ñайл ÑазÑабоÑÑикÑ."}; 1730 } 1731 return result; 1732} 1733function allocation(ids){ 1734 // A single selected target always starts from its feasible minimum chain. 1735 // Maximum utilization is then applied by buildAllocationSegments: a full 1736 // balanced copy if possible, otherwise useful P3âP0 component chains. 1737 // Multiple simultaneous targets retain the stricter balanced reservation so 1738 // an added material cannot consume the output promised to earlier targets. 1739 const balanced=balancedPlanningEnabled()&&ids.length>1; 1740 const memoKey=`${balanced?"balanced-multi":"minimum-base"}|${[...ids].map(Number).sort((a,b)=>a-b).join(",")}`; 1741 if(allocationMemo.has(memoKey))return allocationMemo.get(memoKey); 1742 const result=calculateAllocation(ids,new Map(),1,balanced); 1743 if(result.balanced!==balanced){ 1744 console.error("PI base allocation mode mismatch",{expected:balanced,actual:result.balanced}); 1745 return{...result,ok:false,internalError:true,reason:"ÐнÑÑÑеннÑÑ Ð¿ÑовеÑка обнаÑÑжила оÑÐ¸Ð±ÐºÑ Ð±Ð°Ð·Ð¾Ð²Ð¾Ð³Ð¾ ÑаÑÑÑÑа пÑоизводÑÑва."}; 1746 } 1747 allocationMemo.set(memoKey,result); 1748 return result; 1749} 1750function avail(id){ 1751 if(selected.includes(id))return{ok:true}; 1752 let a=allocation([...selected,id]); 1753 if(a.ok)return{ok:true,a}; 1754 if(a.cover?.missing)return{ok:false,reason:"ÐÐµÑ P0: "+a.cover.missing.map(x=>products.get(x)?.n||x).join(", ")}; 1755 return{ok:false,reason:a.reason||(siteLanguage==="en"?`More available planets are required (limit ${cap()})`:`ÐÑжно болÑÑе доÑÑÑпнÑÑ Ð¿Ð»Ð°Ð½ÐµÑ (Ð»Ð¸Ð¼Ð¸Ñ ${cap()})`)}; 1756} 1757 1758function materialDisplaySet(tier,showUnavailable){ 1759 const all=D.products.filter(p=>p.t===tier); 1760 if(marketSettings.enabled)all.sort((a,b)=>(marketPrices.get(b.id)??-1)-(marketPrices.get(a.id)??-1)||a.n.localeCompare(b.n)); 1761 const available=all.filter(p=>avail(p.id).ok); 1762 return{all,available,displayed:showUnavailable?all:available}; 1763} 1764function mats(){ 1765 let show=$("showUnavailable").checked; 1766 const displayedByTier=[]; 1767 $("materialColumns").innerHTML=[0,1,2,3,4].map(t=>{ 1768 const {all:ps,available,displayed}=materialDisplaySet(t,show); 1769 displayedByTier[t]=displayed; 1770 let rows=ps.map(p=>{ 1771 let a=avail(p.id),sel=selected.includes(p.id); 1772 if(!show&&!a.ok)return""; 1773 let meta=sel?"ÐÑбÑано":a.ok?`ÐÐµÐ¾Ð±Ñ Ð¾Ð´Ð¸Ð¼Ð¾ Ð¿Ð»Ð°Ð½ÐµÑ ${allocation([...selected,p.id]).cover.count}`:"";
1774 return`<label class="mat ${a.ok?"":"disabled"} ${sel?"selected":""}" title="${esc(a.reason||"")}"> 1775 <input type="checkbox" data-id="${p.id}" ${sel?"checked":""} ${a.ok?"":"disabled"}> 1776 ${materialIcon(p,"materialListIcon")} 1777 <div><div class="matName"><span>${esc(p.n)}</span>${marketIndicator(p.id)}</div><div class="meta">${esc(meta)}</div>${a.ok?"":`<div class="reason">${esc(a.reason)}</div>`}</div> 1778 </label>`; 1779 }).join(""); 1780 let ok=available.length; 1781 return`<div class="matCol"><div class="matHead"><b>P${t}</b><div class="matHeadActions"><span>${ok}/${ps.length}</span><button class="copyMaterialsTier" type="button" data-tier="${t}" title="СкопиÑоваÑÑ Ð¿Ð¾ÐºÐ°Ð·Ð°Ð½Ð½Ñе маÑеÑÐ¸Ð°Ð»Ñ P${t}" aria-label="СкопиÑоваÑÑ Ð¿Ð¾ÐºÐ°Ð·Ð°Ð½Ð½Ñе маÑеÑÐ¸Ð°Ð»Ñ P${t}">â§</button></div></div><div class="matList">${rows}</div></div>`; 1782 }).join(""); 1783 document.querySelectorAll(".copyMaterialsTier").forEach(btn=>btn.onclick=()=>{ 1784 const t=Number(btn.dataset.tier),items=displayedByTier[t]||[],text=items.map(p=>p.n).join("\n"); 1785 copyText(text,`ÐоказаннÑе маÑеÑÐ¸Ð°Ð»Ñ P${t} ÑкопиÑованÑ`); 1786 }); 1787 renderProfitabilityRanking(); 1788 document.querySelectorAll(".mat input").forEach(cb=>cb.onchange=e=>{ 1789 let id=+e.target.dataset.id; 1790 if(e.target.checked){ 1791 if(avail(id).ok&&!selected.includes(id)){ 1792 ensureTargetColor(id);selected.push(id); 1793 const product=products.get(id); 1794 metrikaGoal("material_selected",{material_id:id,material_name:product?.n||String(id),tier:product?.t}); 1795 } 1796 } 1797 else selected=selected.filter(x=>x!==id); 1798 recalc();autosave(); 1799 }); 1800} 1801 1802function renderProfitabilityRanking(){ 1803 const box=$("profitabilityRanking"); 1804 if(!box)return; 1805 if(!marketSettings.enabled){box.classList.add("hidden");box.innerHTML="";return} 1806 box.classList.remove("hidden"); 1807 if(planetCountsDirty){ 1808 box.innerHTML=`<div class="profitabilityRankingHead">СÐÐЫРÐЫÐÐÐÐЫРÐÐСТУÐÐЫРÐÐÐ ÐÐÐТ</div><span class="profitabilityRankingEmpty">ÐолиÑеÑÑво Ð¿Ð»Ð°Ð½ÐµÑ Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¾. ÐжидаеÑÑÑ Ð°Ð²ÑомаÑиÑеÑкий пеÑеÑаÑÑÑÑâ¦</span>`; 1809 return; 1810 } 1811 const signature=profitabilitySignature(),ready=signature&&signature===profitabilityRankingSignature&&signature===profitabilityRankingVerifiedSignature&&profitabilityRankingCache.length; 1812 if(!ready){ 1813 box.innerHTML=`<div class="profitabilityRankingHead">СÐÐЫРÐЫÐÐÐÐЫРÐÐСТУÐÐЫРÐÐÐ ÐÐÐТ</div><span class="profitabilityRankingEmpty">${marketLoading?"ÐагÑÑжаем ÑекÑÑие ÑенÑâ¦":profitabilityRankingStage||"ÐодгоÑавливаем пÑовеÑÐºÑ Ð¿ÑоизводÑÑвеннÑÑ ÐºÐ¾Ð½ÑигÑÑаÑийâ¦"}</span>`; 1814 if(!marketLoading)ensureProfitabilityRanking().then(()=>renderProfitabilityRanking()); 1815 return; 1816 } 1817 const ranked=profitabilityRankingCache.slice(0,8),best=ranked[0]; 1818 const bestExtra=best.lines.filter(x=>x.id!==best.product.id).map(x=>x.name).join(", "); 1819 const p0Forecast=best.p0Requirements?.length?`<section class="p0Forecast"><div class="p0ForecastHead"><b>P0 ÐÐЯ ÐÐÐÐ ÐРЫÐÐÐÐ Ð ÐÐÐТЫ 24/7</b><span>ÐÑогноз доÑÑижим пÑи ÑÑабилÑной добÑÑе не ниже ÑказаннÑÑ Ð¾Ð±ÑÑмов.</span></div><div class="p0ForecastTable"><div class="p0ForecastRow p0ForecastLabels"><span>РеÑÑÑÑ P0</span><span>Ð ÑаÑ</span><span>Ð ÑÑÑки</span><span>Ðа 30 дней</span></div>${best.p0Requirements.map(row=>`<div class="p0ForecastRow">${materialIcon(products.get(row.id),"p0ForecastIcon")}<span>${esc(row.name)}</span><b>${Math.round(row.hour).toLocaleString(siteLocale())}</b><b>${Math.round(row.day).toLocaleString(siteLocale())}</b><b>${Math.round(row.days30).toLocaleString(siteLocale())}</b></div>`).join("")}</div><small>ФакÑиÑеÑÐºÐ°Ñ Ð´Ð¾Ð±ÑÑа завиÑÐ¸Ñ Ð¾Ñ Ð·Ð°Ð»ÐµÐ¶ÐµÐ¹, пÑогÑÐ°Ð¼Ð¼Ñ ÑкÑÑÑакÑоÑа и ÑаÑÑоÑÑ ÐµÑ Ð¿ÐµÑезапÑÑка. ÐÑи менÑÑей добÑÑе Ð·Ð°Ð²Ð¾Ð´Ñ Ð±ÑдÑÑ Ð¿ÑоÑÑаиваÑÑ, а ÑеалÑÐ½Ð°Ñ Ð¿ÑибÑÐ»Ñ Ð¾ÐºÐ°Ð¶ÐµÑÑÑ Ð½Ð¸Ð¶Ðµ пÑогноза.</small></section>`:""; 1820 box.innerHTML=`<div class="profitabilityRankingHead">СÐÐЫРÐЫÐÐÐÐЫРÐÐСТУÐÐЫРÐÐÐ ÐÐÐТ</div> 1821 <div class="profitabilityBest">${materialIcon(best.product,"profitabilityBestIcon")}<span><b>${esc(best.product.n)}</b><small>P${best.product.t} · заводов конеÑного пÑодÑкÑа: ${best.targetLines} · вÑпÑÑк: ${Math.round((best.mainLine?.producedPerDay||0)*30).toLocaleStr
1821ing(siteLocale())} ÑÑ. / 30 дней · занÑÑо ${best.usedPlanets} ${siteLanguage==="en"?"of":"из"} ${cap()} ${siteLanguage==="en"?"planets":"планеÑ"}${bestExtra?` · дополниÑелÑно: ${esc(bestExtra)}`:""}</small></span><strong>${Math.round(best.total30Days).toLocaleString(siteLocale())} ISK / 30 дней</strong></div> 1822 ${p0Forecast} 1823 <div class="profitabilityRankingList">${ranked.map((x,index)=>`<div class="profitabilityRank ${selected.includes(x.product.id)?"selectedRank":"positive"}"><b>${index+1}</b>${materialIcon(x.product,"profitabilityIcon")}<span>P${x.product.t} · ${esc(x.product.n)}<small>${x.targetLines} завод. Ñели · ${x.usedPlanets} план. · ${siteLanguage==="en"?"output":"вÑпÑÑк"}: ${Math.round((x.mainLine?.producedPerDay||0)*30).toLocaleString(siteLocale())} ${siteLanguage==="en"?"units / 30 days":"ÑÑ. / 30 дн."}</small></span><strong>${Math.round(x.total30Days).toLocaleString(siteLocale())} ISK / 30 дн.</strong></div>`).join("")}</div> 1824 <small>СÑавниваеÑÑÑ Ð¾ÑиенÑиÑовоÑÐ½Ð°Ñ ÑиÑÑÐ°Ñ Ð¿Ñодажа за 30 дней. ÐÑпÑÑк огÑаниÑиваеÑÑÑ ÑеалÑнÑм Ñзким меÑÑом вÑей ÑеÑепÑÑÑной ÑепоÑки и ÑÑедней ликвидноÑÑÑÑ ÑÑнка. РаÑÑÑÑÐ½Ð°Ñ Ñена â ÑÑедневзвеÑÐµÐ½Ð½Ð°Ñ Ñена ÑакÑиÑеÑÐºÐ¸Ñ Ñделок за поÑледние 30 завеÑÑÑннÑÑ Ð´Ð½ÐµÐ¹. УÑÑÐµÐ½Ñ Ð½Ð°Ð»Ð¾Ð³ Ñ Ð¿Ñодаж, бÑокеÑÑÐºÐ°Ñ ÐºÐ¾Ð¼Ð¸ÑÑиÑ, ÑкÑпоÑÑ Ð¸ импоÑÑ ÑеÑез ÑаможнÑ, а Ñакже пеÑевозка; добÑÑа P0 оÑÑаÑÑÑÑ Ð¾ÑиенÑиÑовоÑной без заданного дебиÑа ÑкÑÑÑакÑоÑов.</small>`; 1825} 1826 1827 1828function instructionsForAllocation(a,planIds,ownerTargetIds=null,rateFactors=new Map()){ 1829 const used=a.cover.colonies.map(x=>({ 1830 ...x, 1831 tasks:[], 1832 p0:[...(a.cover.p0Assignment[x.colonyKey]||[])], 1833 p0Details:[...(a.cover.p0AssignmentDetails?.[x.colonyKey]||[])].map(d=>({...d})) 1834 })); 1835 const usedByKey=new Map(used.map(x=>[x.colonyKey,x])); 1836 const homeCursor={}; 1837 const nextStageHome=productId=>{ 1838 const homes=a.cover.stageHomes?.[productId]||[]; 1839 const index=homeCursor[productId]||0;homeCursor[productId]=index+1; 1840 return homes[index]||homes[homes.length-1]; 1841 }; 1842 const taskOwnerIds=(productId,fallbackTargetId=null)=>{ 1843 if(productionSettings.separateP0&&fallbackTargetId!=null)return[fallbackTargetId]; 1844 const owners=targetOwnersFor(ownerTargetIds||[],productId); 1845 return owners.length?owners:(fallbackTargetId!=null?[fallbackTargetId]:null); 1846 }; 1847 1848 // ÐобÑÑа + P0->P1. 1849 for(const c of used){ 1850 const details=c.p0Details.length?c.p0Details:c.p0.map(id=>({id,targetId:null})); 1851 for(const detail of details){ 1852 const id=detail.id; 1853 const p0=products.get(id); 1854 if(detail.productId==null){ 1855 c.tasks.push({kind:"extract",productId:id,targetIds:taskOwnerIds(id,detail.targetId),rateFactor:1,text:`ÐобÑÑа P0: ${p0.n}`}); 1856 continue; 1857 } 1858 const p1=products.get(detail.productId)||directP1ForP0(id); 1859 if(p1){ 1860 const homeKey=nextStageHome(p1.id); 1861 const home=usedByKey.get(homeKey); 1862 const rateFactor=rateFactors.get(p1.id)??1; 1863 const task={kind:"assembly",productId:p1.id,targetIds:taskOwnerIds(p1.id,detail.targetId),rateFactor,text:`P0 â P1: ${p0.n} â ${p1.n}`}; 1864 if(globalPlanetSettings.p0p1){ 1865 c.tasks.push(task); 1866 }else{ 1867 c.tasks.push({kind:"extract",productId:id,targetIds:taskOwnerIds(id,detail.targetId),rateFactor,text:`ÐобÑÑа P0: ${p0.n}`}); 1868 if(homeKey===c.colonyKey){ 1869 c.tasks.push(task); 1870 }else if(home){ 1871 home.tasks.push({...task,text:`P0 â P1: ${p0.n} â ${p1.n} (ÑÑÑÑÑ: ${c.cloneName} / ${c.label})`}); 1872 } 1873 } 1874 } 1875 } 1876 } 1877 1878 // P1 -> P2 -> P3 -> P4. 1879 for(const placement of (a.cover.stagePlacements||[]).filter(x=>x.tier>=2)){ 1880 const p=products.get(placement.productId),home=usedByKey.get(placement.homeKey); 1881 if(home){ 1882 const inputs=p.i.map(([id])=>products.get(id)?.n||id).join(" + "); 1883 home.tasks.push({kind:"assembly",productId:p.id,targetIds:taskOwnerIds(p.id),rateFactor:rateFactors.get(p.id)??1,text:`P${p.t-1} â P${p.t}: ${inputs} = ${p.n}`}); 1884 } 1885 } 1886 1887 // ÐдинаковÑе Ð·Ð°Ð²Ð¾Ð´Ñ Ð¾Ð´Ð½Ð¾Ð³Ð¾ ÑеÑепÑа на одной планеÑе показÑваем одной ÑÑÑокой. 1888 for(const colony of used){ 1889 const grouped=new Map(); 1890 for(const task of colony.tasks){
1891 const key=`${task.kind}|${task.productId}|${task.text}|${(task.targetIds||[]).join(",")}`; 1892 if(!grouped.has(key))grouped.set(key,{...task,factoryCount:0}); 1893 grouped.get(key).factoryCount++; 1894 } 1895 colony.tasks=[...grouped.values()].map(task=>({...task,text:task.factoryCount>1?`${task.factoryCount}à ${task.text}`:task.text})); 1896 } 1897 1898 return used; 1899} 1900 1901function targetOwnersFor(ids,productId){ 1902 return ids.filter(targetId=>dependencyProducts([targetId]).some(p=>p.id===productId)); 1903} 1904function buildAllocationSegments(ids,a,maxUtilization){ 1905 let baseAllocation=a; 1906 if(maxUtilization&&ids.length===1){ 1907 const packed=maximumPackedAllocation(ids[0]); 1908 if(packed?.allocation?.ok)baseAllocation=packed.allocation; 1909 } 1910 const segments=[{allocation:baseAllocation,targetProductId:ids.length===1?ids[0]:null,ownerTargetIds:[...ids]}]; 1911 if(!maxUtilization)return segments; 1912 1913 const reserved=new Map();reserveCover(reserved,baseAllocation.cover); 1914 const byTier={0:[],1:[],2:[],3:[],4:[]}; 1915 dependencyProducts(ids).forEach(p=>byTier[p.t].push(p.id)); 1916 1917 // ÐолнÑе копии Ñелевой ÑепоÑки Ñже ÑбаланÑиÑÐ¾Ð²Ð°Ð½Ñ Ð² baseAllocation. 1918 // ÐÑли ÑлоÑов на ÑледÑÑÑÑÑ Ð¿Ð¾Ð»Ð½ÑÑ ÐºÐ¾Ð¿Ð¸Ñ Ð½Ðµ Ñ Ð²Ð°Ñило, заполнÑем оÑÑаÑок 1919 // полезнÑми пÑомежÑÑоÑнÑми ÑепоÑками: P3, заÑем P2, P1 и P0. 1920 // calculateAllocation Ð´Ð»Ñ ÐºÐ°Ð¶Ð´Ð¾Ð³Ð¾ Ñакого пÑодÑкÑа Ñнова ÑÑиÑÐ°ÐµÑ Ð²Ñе нижние ÑÑовни, 1921 // поÑÑÐ¾Ð¼Ñ Ð½ÐµÐ¾Ð±ÐµÑпеÑеннÑе «лиÑние» Ð·Ð°Ð²Ð¾Ð´Ñ Ð½Ðµ поÑвлÑÑÑÑÑ. 1922 const priorityGroups=[[...ids],byTier[3],byTier[2],byTier[1],byTier[0]]; 1923 for(const group of priorityGroups){ 1924 if(!group.length)continue; 1925 let placedInPass=true; 1926 while(placedInPass){ 1927 placedInPass=false; 1928 for(const productId of group){ 1929 const extra=calculateAllocation([productId],reserved,1,true); 1930 if(!extra.ok||!extra.cover.count)continue; 1931 reserveCover(reserved,extra.cover); 1932 segments.push({allocation:extra,targetProductId:productId,ownerTargetIds:targetOwnersFor(ids,productId)}); 1933 placedInPass=true; 1934 } 1935 } 1936 } 1937 return segments; 1938} 1939function buildPlanetInstructionsFor(ids,a,maxUtilization){ 1940 const segments=buildAllocationSegments(ids,a,maxUtilization),plan=[]; 1941 for(const segment of segments){ 1942 const segmentTargets=segment.targetProductId==null?ids:[segment.targetProductId]; 1943 const rateFactors=allocationRateFactorsForTargets(segment.allocation,segmentTargets); 1944 instructionsForAllocation(segment.allocation,[segment.targetProductId].filter(x=>x!=null),segment.ownerTargetIds,rateFactors).forEach(colony=>plan.push(colony)); 1945 } 1946 return{plan,segments}; 1947} 1948function buildPlanetInstructions(a){ 1949 return buildPlanetInstructionsFor(selected,a,!!$("maxUtilization").checked).plan; 1950} 1951 1952function assignToClones(colonies){ 1953 return clones.map((c,ci)=>({ 1954 cloneIndex:ci, 1955 name:c.name, 1956 limit:c.limit, 1957 planets:colonies.filter(x=>x.cloneIndex===ci).sort((a,b)=>(a.sortOrder??a.ordinal??0)-(b.sortOrder??b.ordinal??0)||String(a.label).localeCompare(String(b.label))) 1958 })); 1959} 1960function taskTargets(task){ 1961 return task.targetIds?.length?task.targetIds:targetsForProduct(task.productId); 1962} 1963function targetsOverlap(a,b){ 1964 const right=new Set(taskTargets(b)); 1965 return taskTargets(a).some(id=>right.has(id)); 1966} 1967function annotateCloneTransfers(planets){ 1968 const balances=new Map(),producerLines=new Map(),producedTotals=new Map(),processorClonesByInput=new Map(); 1969 const balanceKey=(cloneIndex,productId)=>`${cloneIndex}:${productId}`; 1970 const addBalance=(cloneIndex,productId,quantity)=>{ 1971 const key=balanceKey(cloneIndex,productId); 1972 balances.set(key,(balances.get(key)||0)+quantity); 1973 }; 1974 const registerProducer=(planet,task,outputPerHour,factories)=>{ 1975 const key=balanceKey(planet.cloneIndex,task.productId); 1976 const lineCount=task.kind==="assembly"?Math.max(1,Number(factories)||1):1; 1977 const outputPerLine=outputPerHour/lineCount; 1978 task.lineOutputPerHour=outputPerLine; 1979 task.lineTransfers=Array.from({length:lineCount},()=>[]); 1980 if(!producerLines.has(key))producerLines.set(key,[]); 1981 for(let lineIndex=0;lineIndex<lineCount;lineIndex++)producerLines.get(key).push({planet,task,lineIndex,remaining:outputPerLine}); 1982 producedTotals.set(key,(producedTotals.get(key)||0)+outputPerHour); 1983 };
1984 planets.forEach(planet=>planet.tasks.forEach(task=>{ 1985 task.transferTo=[]; 1986 task.lineTransfers=[]; 1987 const product=products.get(task.productId); 1988 if(!product)return; 1989 const factories=Math.max(1,Number(task.factoryCount)||1); 1990 const rateFactor=Number.isFinite(Number(task.rateFactor))?Number(task.rateFactor):1; 1991 if(task.kind==="extract"&&product.t===0){ 1992 const p1=directP1ForP0(product.id); 1993 const inputQuantity=p1?.i?.find(([id])=>id===product.id)?.[1]||0; 1994 const outputPerHour=factories*rateFactor*inputQuantity*3600/Math.max(1,Number(p1?.c)||3600); 1995 addBalance(planet.cloneIndex,product.id,outputPerHour); 1996 registerProducer(planet,task,outputPerHour,factories); 1997 return; 1998 } 1999 if(task.kind!=="assembly"||product.t===0)return; 2000 const cyclesPerHour=factories*rateFactor*3600/Math.max(1,Number(product.c)||3600); 2001 const outputPerHour=cyclesPerHour*Math.max(1,Number(product.q)||1); 2002 addBalance(planet.cloneIndex,product.id,outputPerHour); 2003 registerProducer(planet,task,outputPerHour,factories); 2004 for(const [inputId,quantity] of product.i||[]){ 2005 if(!processorClonesByInput.has(inputId))processorClonesByInput.set(inputId,new Set()); 2006 processorClonesByInput.get(inputId).add(planet.cloneIndex); 2007 const inputTier=products.get(inputId)?.t; 2008 if(inputTier>0||(inputTier===0&&!globalPlanetSettings.p0p1))addBalance(planet.cloneIndex,inputId,-cyclesPerHour*Math.max(0,Number(quantity)||0)); 2009 } 2010 })); 2011 2012 const productIds=[...new Set([...balances.keys()].map(key=>Number(key.split(":")[1])))]; 2013 const routes=[]; 2014 for(const productId of productIds){ 2015 const suppliers=clones.map((clone,cloneIndex)=>({clone,cloneIndex,quantity:balances.get(balanceKey(cloneIndex,productId))||0})).filter(x=>x.quantity>TRANSFER_EPSILON); 2016 const consumers=clones.map((clone,cloneIndex)=>({clone,cloneIndex,quantity:-(balances.get(balanceKey(cloneIndex,productId))||0)})).filter(x=>x.quantity>TRANSFER_EPSILON); 2017 const productRoutes=[]; 2018 const addRoute=(supplier,toIndex,toName,quantity)=>{ 2019 if(!(quantity>TRANSFER_EPSILON)||supplier.cloneIndex===toIndex)return; 2020 const route={productId,quantityPerHour:quantity,fromIndex:supplier.cloneIndex,fromName:supplier.clone.name,toIndex,toName}; 2021 routes.push(route);productRoutes.push(route); 2022 }; 2023 for(const consumer of consumers){ 2024 let needed=consumer.quantity; 2025 for(const supplier of suppliers){ 2026 if(needed<=TRANSFER_EPSILON)break; 2027 if(supplier.cloneIndex===consumer.cloneIndex||supplier.quantity<=TRANSFER_EPSILON)continue; 2028 const quantity=Math.min(needed,supplier.quantity); 2029 if(quantity<=TRANSFER_EPSILON)continue; 2030 addRoute(supplier,consumer.cloneIndex,consumer.clone.name,quantity); 2031 supplier.quantity-=quantity;needed-=quantity; 2032 } 2033 } 2034 2035 // ÐолезнÑй пÑомежÑÑоÑнÑй маÑеÑиал не оÑÑавлÑем Ñ ÐºÐ»Ð¾Ð½Ð°, коÑоÑÑй не Ð¼Ð¾Ð¶ÐµÑ 2036 // пеÑеÑабоÑаÑÑ ÐµÐ³Ð¾ далÑÑе. ÐоÑле закÑÑÑÐ¸Ñ ÑекÑÑÐ¸Ñ Ð´ÐµÑиÑиÑов веÑÑ Ð¾ÑÑаÑок 2037 // поÑÐ¾Ð²Ð½Ñ ÑаÑпÑеделÑеÑÑÑ Ð¼ÐµÐ¶Ð´Ñ ÐºÐ»Ð¾Ð½Ð°Ð¼Ð¸ Ñ Ð¿Ð¾Ð´Ñ Ð¾Ð´ÑÑими заводами ÑледÑÑÑего 2038 // ÑÑовнÑ. Уже назнаÑеннÑе пеÑедаÑи ÑÑиÑÑваÑÑÑÑ, поÑÑÐ¾Ð¼Ñ Ð´Ð¾Ð¿Ð¾Ð»Ð½Ð¸ÑелÑнÑй 2039 // обÑÑм ÑнаÑала вÑÑÐ°Ð²Ð½Ð¸Ð²Ð°ÐµÑ Ð´Ð¾Ð»Ð¸ полÑÑаÑелей (10 + 5 пÑевÑаÑаеÑÑÑ Ð² 10 + 10). 2040 const processorIndexes=[...(processorClonesByInput.get(productId)||[])].sort((a,b)=>a-b); 2041 for(const supplier of suppliers){ 2042 if(supplier.quantity<=TRANSFER_EPSILON)continue; 2043 // ÐÑли Ñам пÑоизводиÑÐµÐ»Ñ ÑпоÑобен иÑполÑзоваÑÑ Ð¼Ð°ÑеÑиал далÑÑе, излиÑек 2044 // Ñже Ð½Ð°Ñ Ð¾Ð´Ð¸ÑÑÑ Ñ Ð¿Ð¾Ð´Ñ Ð¾Ð´ÑÑего клона и беÑÑмÑÑÐ»ÐµÐ½Ð½Ð°Ñ Ð²ÑÑÑеÑÐ½Ð°Ñ Ð¿ÐµÑевозка не нÑжна. 2045 if(processorIndexes.includes(supplier.cloneIndex))continue; 2046 const recipients=processorIndexes.filter(index=>index!==supplier.cloneIndex); 2047 if(!recipients.length)continue; 2048 const assigned=new Map(recipients.map(index=>[index,productRoutes 2049 .filter(route=>route.fromIndex===supplier.cloneIndex&&route.toIndex===index) 2050 .reduce((sum,route)=>sum+route.quantityPerHour,0)])); 2051 let remaining=supplier.quantity; 2052 while(remaining>TRANSFER_EPSILON){ 2053 const ordered=recipients.map(index=>({index,quantity:assigned.get(index)||0})).sort((a,b)=>a.quantity-b.quantity||a.index-b.index); 2054 const minimum=ordered[0].quantity; 2055 const lowest=ordered.filter(item=>Math.abs(item.quantity-minimum)<=TRANSFER_EPSILON); 2056 const next=ordered.find(item=>item.quantity>minimum+TRANSFER_EPSILON)?.quantity; 2057 const neededToNext=next==null?remaining:(next-minimum)*lowest.length; 2058 const used=Math.min(remaining,neededToNext); 2059 const each=used/lowest.length; 2060 lowest.forEach(item=>assigned.set(item.index,(assigned.get(item.index)||0)+each)); 2061 remaining=Math.max(0,remaining-used); 2062 } 2063 for(const recipientIndex of recipients){ 2064 const previous=productRoutes 2065 .filter(route=>route.fromIndex===supplier.cloneIndex&&route.toIndex===recipientIndex) 2066 .reduce((sum,route)=>sum+route.quantityPerHour,0); 2067 const extra=(assigned.get(recipientIndex)||0)-previous; 2068 if(extra>
2068TRANSFER_EPSILON)addRoute(supplier,recipientIndex,clones[recipientIndex].name,extra); 2069 } 2070 supplier.quantity=0; 2071 } 2072 } 2073 2074 for(const route of routes){ 2075 let remaining=route.quantityPerHour; 2076 for(const producer of producerLines.get(balanceKey(route.fromIndex,route.productId))||[]){ 2077 if(remaining<=TRANSFER_EPSILON)break; 2078 if(producer.remaining<=TRANSFER_EPSILON)continue; 2079 const quantity=Math.min(remaining,producer.remaining); 2080 if(quantity<=TRANSFER_EPSILON)continue; 2081 producer.task.lineTransfers[producer.lineIndex].push({cloneIndex:route.toIndex,cloneName:route.toName,quantityPerHour:quantity}); 2082 producer.remaining=Math.max(0,producer.remaining-quantity);remaining=Math.max(0,remaining-quantity); 2083 } 2084 } 2085 2086 // Ð¡Ð¾Ñ ÑанÑем обÑий ÑпиÑок Ð´Ð»Ñ Ð´Ð¸Ð°Ð³Ð½Ð¾ÑÑики, но инÑеÑÑÐµÐ¹Ñ Ð¸ÑполÑзÑÐµÑ ÑоÑнÑе 2087 // пеÑедаÑи каждой оÑделÑной заводÑкой линии. 2088 planets.forEach(planet=>planet.tasks.forEach(task=>{ 2089 const groupedRecipients=new Map(); 2090 for(const transfers of task.lineTransfers||[])for(const transfer of transfers||[]){ 2091 if(!(transfer.quantityPerHour>TRANSFER_EPSILON))continue; 2092 const current=groupedRecipients.get(transfer.cloneIndex)||{cloneIndex:transfer.cloneIndex,cloneName:transfer.cloneName,quantityPerHour:0}; 2093 current.quantityPerHour+=transfer.quantityPerHour;groupedRecipients.set(transfer.cloneIndex,current); 2094 } 2095 task.transferTo=[...groupedRecipients.values()]; 2096 })); 2097 2098 const grouped=new Map; 2099 const outgoingTotals=new Map(),outgoingDestinations=new Map(); 2100 for(const route of routes){ 2101 const key=balanceKey(route.fromIndex,route.productId); 2102 outgoingTotals.set(key,(outgoingTotals.get(key)||0)+route.quantityPerHour); 2103 if(!outgoingDestinations.has(key))outgoingDestinations.set(key,new Set()); 2104 outgoingDestinations.get(key).add(route.toIndex); 2105 } 2106 for(const route of routes){ 2107 const key=`${route.fromIndex}>${route.toIndex}`; 2108 if(!grouped.has(key))grouped.set(key,{fromIndex:route.fromIndex,fromName:route.fromName,toIndex:route.toIndex,toName:route.toName,products:new Map}); 2109 const existing=grouped.get(key).products.get(route.productId)||{id:route.productId,name:products.get(route.productId)?.n||String(route.productId),quantityPerHour:0}; 2110 existing.quantityPerHour+=route.quantityPerHour; 2111 const outgoingKey=balanceKey(route.fromIndex,route.productId); 2112 existing.totalOutgoingPerHour=outgoingTotals.get(outgoingKey)||existing.quantityPerHour; 2113 existing.totalProducedPerHour=producedTotals.get(outgoingKey)||existing.quantityPerHour; 2114 existing.destinationCount=outgoingDestinations.get(outgoingKey)?.size||1; 2115 grouped.get(key).products.set(route.productId,existing); 2116 } 2117 for(const transfer of grouped.values())for(const item of transfer.products.values()){ 2118 item.sharePercent=item.totalOutgoingPerHour>0?item.quantityPerHour/item.totalOutgoingPerHour*100:100; 2119 } 2120 return [...grouped.values()].sort((a,b)=>a.fromIndex-b.fromIndex||a.toIndex-b.toIndex); 2121} 2122 2123function transferShareLabel(item){ 2124 if(!(item?.destinationCount>1))return""; 2125 return ` (${shareRatioText((Number(item.sharePercent)||0)/100)})`; 2126} 2127function shareRatioText(value){ 2128 const ratio=Math.max(0,Math.min(1,Number(value)||0)); 2129 if(ratio<=TRANSFER_EPSILON)return"0"; 2130 if(ratio>=1-TRANSFER_EPSILON)return"1/1"; 2131 let best=null; 2132 for(let denominator=2;denominator<=60;denominator++){ 2133 const numerator=Math.round(ratio*denominator); 2134 if(numerator<=0||numerator>=denominator)continue; 2135 const error=Math.abs(ratio-numerator/denominator); 2136 if(!best||error<best.error)best={numerator,denominator,error}; 2137 } 2138 if(best&&best.error<=0.0015){ 2139 const gcd=(a,b)=>b?gcd(b,a%b):a,divisor=gcd(best.numerator,best.denominator); 2140 return `${best.numerator/divisor}/${best.denominator/divisor}`; 2141 } 2142 const rounded=Math.round(ratio*1000)/10; 2143 return `${Number.isInteger(rounded)?rounded:rounded.toFixed(1)}%`; 2144} 2145function transferHintHtml(transfers,lineOutputPerHour=0){ 2146 const valid=(transfers||[]).filter(item=>item.quantityPerHour>TRANSFER_EPSILON); 2147 if(!valid.length)return""; 2148 const verb=siteLanguage==="en"?"transfer":"пеÑедаÑÑ",outputWord=siteLanguage==="en"?" of output":" вÑпÑÑка"; 2149 return ` <span class="transferHint">(${verb}: ${valid.map(item=>{ 2150 const share=lineOutputPerHour>TRANSFER_EPSILON?item.quantityPerHour/lineOutputPerHour:1; 2151 return `${esc(item.cloneName)}
2151${share<1-TRANSFER_EPSILON?` â ${shareRatioText(share)}${outputWord}`:""}`; 2152 }).join(", ")})</span>`; 2153} 2154function transferSummaryProductHtml(item){ 2155 const quantity=Math.max(0,Number(item?.quantityPerHour)||0),produced=Math.max(quantity,Number(item?.totalProducedPerHour)||0); 2156 const product=products.get(Number(item?.id)),label=product?materialInline(product,"transferMaterial"):`<span class="inlineMaterial transferMaterial"><span>${esc(item?.name||"")}</span></span>`; 2157 if(quantity>=produced-TRANSFER_EPSILON)return label; 2158 const transferWord=siteLanguage==="en"?"transfer":"пеÑедаÑÑ"; 2159 return `${label}<span class="transferFraction"> â ${transferWord} ${shareRatioText(quantity/produced)}</span>`; 2160} 2161function netSaleValue(product,quantity,price){ 2162 return netSaleBreakdown(product,quantity,price).net; 2163} 2164 2165function customsExportFee(product,quantity){ 2166 return Math.max(0,Number(quantity)||0)*(CUSTOMS_TAX_VALUE[product?.t]||0)*clampPercent(marketSettings.customsTax)/100; 2167} 2168 2169function customsImportFee(product,quantity){ 2170 return customsExportFee(product,quantity)*0.5; 2171} 2172 2173// Intermediate products pay export at the source and import at the destination 2174// when their net output leaves one colony to cover a deficit on another. 2175// Production and consumption inside one colony cancel before fees are calculated. 2176function interplanetaryCustomsFees(plan,hours){ 2177 if(!(marketSettings.customsTax>0)||!(hours>0)||!Array.isArray(plan))return{exportFee:0,importFee:0,total:0,byProduct:[]}; 2178 const balances=[]; 2179 for(const planet of plan){ 2180 const balance=new Map(); 2181 const add=(id,quantity)=>balance.set(id,(balance.get(id)||0)+quantity); 2182 for(const task of planet.tasks||[]){ 2183 const product=products.get(task.productId);if(!product)continue; 2184 const factories=Math.max(1,Number(task.factoryCount)||1); 2185 const rateFactor=Number.isFinite(Number(task.rateFactor))?Number(task.rateFactor):1; 2186 if(task.kind==="extract"&&product.t===0){ 2187 const p1=directP1ForP0(product.id); 2188 const quantity=p1?.i?.find(([id])=>id===product.id)?.[1]||0; 2189 add(product.id,factories*rateFactor*quantity*3600/Math.max(1,Number(p1?.c)||3600)); 2190 }else if(task.kind==="assembly"&&product.t>0){ 2191 const cycles=factories*rateFactor*3600/Math.max(1,Number(product.c)||3600); 2192 add(product.id,cycles*Math.max(1,Number(product.q)||1)); 2193 for(const [inputId,quantity] of product.i||[])add(inputId,-cycles*Math.max(0,Number(quantity)||0)); 2194 } 2195 } 2196 balances.push(balance); 2197 } 2198 const ids=new Set(balances.flatMap(balance=>[...balance.keys()])); 2199 let exportFee=0,importFee=0;const byProduct=[]; 2200 for(const id of ids){ 2201 let surplus=0,deficit=0; 2202 for(const balance of balances){const amount=balance.get(id)||0;if(amount>0)surplus+=amount;else deficit-=amount} 2203 const product=products.get(id),transferredPerHour=Math.min(surplus,deficit); 2204 const quantity=transferredPerHour*hours; 2205 const productExport=customsExportFee(product,quantity),productImport=customsImportFee(product,quantity); 2206 exportFee+=productExport;importFee+=productImport; 2207 if(quantity>0)byProduct.push({id,name:product?.n||String(id),tier:product?.t,quantity,exportFee:productExport,importFee:productImport,total:productExport+productImport}); 2208 } 2209 return{exportFee,importFee,total:exportFee+importFee,byProduct}; 2210} 2211function renderChain(){ 2212 let deps=dependencyProducts(selected),byTier={0:[],1:[],2:[],3:[],4:[]}; 2213 deps.forEach(p=>byTier[p.t].push(p)); 2214 $("chainContent").innerHTML=`<div class="notice">РеÑепÑÑÑÐ½Ð°Ñ ÑепоÑка поÑÑÑоена из PI-ÑÑ ÐµÐ¼ SDE. ÐгÑаниÑÐµÐ½Ð¸Ñ ÐºÐ¾Ð½ÐºÑеÑнÑÑ Ð¿Ð»Ð°Ð½ÐµÑ Ð²Ð»Ð¸ÑÑÑ Ð½Ð° ÑазмеÑение во вкладке «Ðо клонам».</div> 2215 <div class="chainGrid">${[0,1,2,3,4].map(t=>`<section class="chainTier"><h3>P${t}</h3>${byTier[t].map(p=>{ 2216 const isTarget=selected.includes(p.id),inputs=p.i.map(([id])=>products.get(id)).filter(Boolean); 2217 return`<div class="chainNode ${isTarget?"target":""}"><b>${materialInline(p,"chainMaterial")}</b>${inputs.length?`<small>${inputs.map(input=>materialInline(input,"chainInputMaterial")).join('<span class="taskOperator"> + </span>')}</small>`:""}</div>`; 2218 }).join("")||"<small>â</small>"}</section>`).join("")}</div>`; 2219} 2220 2221function clampStorageCount(value){ 2222 const number=Math.floor(Number(value)||0); 2223 return Math.max(0,Math.min(99,number)); 2224} 2225function normalizedStorageSettings(value){ 2226 const result={}; 2227 if(!value||typeof value!=="object")return result; 2228 for(const [key,item] of Object.entries(value)){ 2229 const launchpad=clampStorageCount(item?.launchpad),storage=clampStorageCount(item?.storage); 2230 if(launchpad||storage)result[key]={launchpad,storage}; 2231 } 2232 return result; 2233} 2234function storageSettingKey(planet,task,taskIndex,lineIndex){ 2235 return `${planet.colonyKey}|${task.kind}|${task.productId}|${taskIndex}|${lineIndex}`; 2236} 2237function storageLineSetting(key,defaults={launchpad:1,storage:0}){ 2238 const saved=storageSettings[key]||{}; 2239 return{ 2240 launchpad:Math.max(1,clampStorageCount(saved.launchpad==null?defaults.launchpad:saved.launchpad)), 2241 storage:clampStorageCount(saved.storage==null?defaults.storage:saved.storage) 2242 }; 2243} 2244function individualTaskText(task){ 2245 return String(task?.text||"").replace(/^\d+Ã\s*/,""); 2246} 2247function taskInstructionHtml(task){ 2248 const product=products.get(Number(task?.productId));
2249 if(!product)return esc(individualTaskText(task)); 2250 if(task.kind==="extract")return `${siteLanguage==="en"?"P0 extraction":"ÐобÑÑа P0"}: ${materialInline(product,"taskMaterial")}`; 2251 if(task.kind!=="assembly")return esc(individualTaskText(task)); 2252 const inputs=(product.i||[]).map(([id])=>products.get(id)).filter(Boolean); 2253 const sourceNote=/\s*(\(ÑÑÑÑÑ:[^)]+\))\s*$/.exec(individualTaskText(task))?.[1]||""; 2254 const separator=product.t===1?'<span class="taskOperator"> â </span>':'<span class="taskOperator"> + </span>'; 2255 const relation=product.t===1?"â":"="; 2256 return `P${Math.max(0,product.t-1)} â P${product.t}: ${inputs.map(input=>materialInline(input,"taskMaterial")).join(separator)} <span class="taskOperator">${relation}</span> ${materialInline(product,"taskMaterial taskOutputMaterial")}${sourceNote?` <span class="taskSourceNote">${esc(sourceNote)}</span>`:""}`; 2257} 2258function storageLoadPlan(task,setting={}){ 2259 const product=products.get(Number(task?.productId)); 2260 const inputs=(product?.i||[]).map(([id,quantity])=>({product:products.get(id),quantity:Math.max(0,Number(quantity)||0)})).filter(item=>item.product&&item.quantity>0); 2261 const launchpads=clampStorageCount(setting.launchpad),storages=clampStorageCount(setting.storage); 2262 const capacity=launchpads*STORAGE_CAPACITY.launchpad+storages*STORAGE_CAPACITY.storage; 2263 const recipeVolume=inputs.reduce((sum,item)=>sum+item.quantity*(PRODUCT_VOLUME[item.product.t]||0),0); 2264 const rowFor=(kind,index,rowCapacity)=>{ 2265 const scale=rowCapacity>0&&recipeVolume>0?rowCapacity/recipeVolume:0; 2266 return{ 2267 kind,index,capacity:rowCapacity, 2268 inputs:inputs.map(item=>({id:item.product.id,name:item.product.n,tier:item.product.t,product:item.product,volume:PRODUCT_VOLUME[item.product.t]||0,quantity:Math.floor(item.quantity*scale+1e-9)})) 2269 }; 2270 }; 2271 const rows=[ 2272 ...Array.from({length:launchpads},(_unused,index)=>rowFor("launchpad",index,STORAGE_CAPACITY.launchpad)), 2273 ...Array.from({length:storages},(_unused,index)=>rowFor("storage",index,STORAGE_CAPACITY.storage)) 2274 ]; 2275 const totals=new Map(inputs.map(item=>[item.product.id,{id:item.product.id,name:item.product.n,tier:item.product.t,product:item.product,volume:PRODUCT_VOLUME[item.product.t]||0,quantity:0}])); 2276 rows.forEach(row=>row.inputs.forEach(item=>{totals.get(item.id).quantity+=item.quantity})); 2277 return{ 2278 capacity, 2279 launchpads, 2280 storages, 2281 rows, 2282 inputs:[...totals.values()] 2283 }; 2284} 2285function storageStructureControl(key,kind,count,planetType,defaultLaunchpads=1){ 2286 const isLaunchpad=kind==="launchpad",name=isLaunchpad?"ÐоÑмопоÑÑ":"Ð¥ÑанилиÑе"; 2287 const typeId=planetStructureTypeId(planetType,kind); 2288 const countLabel=isLaunchpad?"ÐолиÑеÑÑво коÑмопоÑÑов":"ÐолиÑеÑÑво Ñ ÑанилиÑ"; 2289 return `<div class="storageControl ${count?"active":"inactive"}"> 2290 <button type="button" class="storageToggle ${count?"active":"inactive"} ${isLaunchpad?"requiredStructure":""}" data-storage-key="${esc(key)}" data-storage-kind="${kind}" data-storage-default-launchpads="${defaultLaunchpads}" aria-pressed="${count?"true":"false"}" ${isLaunchpad?'aria-disabled="true"':''} title="${isLaunchpad?"ÐоÑмопоÑÑ Ð¾Ð±ÑзаÑелен":name}"> 2291 <img src="https://images.evetech.net/types/${typeId}/icon?size=64" alt=""><span>${name}</span> 2292 </button> 2293 ${count?`<label title="${countLabel}"><input class="storageCountInput" type="number" min="1" max="99" step="1" value="${count}" data-storage-key="${esc(key)}" data-storage-kind="${kind}" data-storage-default-launchpads="${defaultLaunchpads}" aria-label="${countLabel}"></label>`:""} 2294 </div>`; 2295} 2296function templateStructureControl(setting,kind,planetType){ 2297 const isLaunchpad=kind==="launchpad",name=isLaunchpad?"ÐоÑмопоÑÑ":"Ð¥ÑанилиÑе"; 2298 const typeId=planetStructureTypeId(planetType,kind); 2299 const count=clampStorageCount(setting[kind]),countLabel=isLaunchpad?"ÐолиÑеÑÑво коÑмопоÑÑов":"ÐолиÑеÑÑво Ñ ÑанилиÑ"; 2300 return `<div class="storageControl piTemplateStructureControl ${count?"active":"inactive"}"> 2301 <button type="button" class="storageToggle piTemplateInfraToggle ${count?"active":"inactive"} ${isLaunchpad?"requiredStructure":""}" data-template-key="${esc(setting.key)}" data-template-field="${kind}" data-template-default-launchpads="${setting.launchpad}" data-template-default-storages="${setting.storage}" aria-pressed="${count?"true":"false"}" ${isLaunchpad?'aria-disabled="true"':''} title="${isLaunchpad?"ÐоÑмопоÑÑ Ð¾Ð±ÑзаÑелен":name}"> 2302 <img src="https://images.evetech.net/types/${typeId}/icon?size=64" alt=""><span>${name}</span> 2303 </button> 2304 ${count?`<label title="${countLabel}"><input class="storageCountInput piTemplateInfraInput" type="number" min="1" max="6" step="1" value="${count}" data-template-key="${esc(setting.key)}" data-template-field="${kind}" data-template-default-launchpads="${setting.launchpad}" data-template-default-storages="${setting.storage}" aria-label="${countLabel}"></label>`:""} 2305 </div>`; 2306} 2307function groupedStorageSetting(planet,task,taskIndex,factoryCount){ 2308 const keys=Array.from({length:Math.max(1,factoryCount)},(_unused,lineIndex)=>storageSettingKey(planet,task,taskIndex,lineIndex)); 2309 const primaryKey=keys[0]; 2310 if(keys.slice(1).some(key=>storageSettings[key])){ 2311 const combined=keys.reduce((total,key)=>{ 2312 const saved=storageSettings[key]||{}; 2313 total.launchpad+=clampStorageCount(saved.launchpad); 2314 total.storage+=clampStorageCount(saved.storage); 2315 return total; 2316 },{launchpad:0,storage:0}); 2317 combined.launchpad=Math.max(1,combined.launchpad); 2318 if(combined.launchpad||combined.storage)storageSettings[primaryKey]=combined;else delete storageSettings[primaryKey];
2319 keys.slice(1).forEach(key=>delete storageSettings[key]); 2320 } 2321 return{key:primaryKey,setting:storageLineSetting(primaryKey,{launchpad:Math.max(1,factoryCount),storage:0})}; 2322} 2323function storageLoadItemsHtml(items){ 2324 return items.map(item=>`${materialInline(item.product||products.get(item.id),"storageMaterial")} â <strong>${item.quantity.toLocaleString(siteLocale())}</strong>`).join('<span class="storageSeparator">; </span>'); 2325} 2326function storageControlsHtml(planet,task,taskIndex,factoryCount){ 2327 const {key,setting}=groupedStorageSetting(planet,task,taskIndex,factoryCount),load=storageLoadPlan(task,setting); 2328 const loadWord=siteLanguage==="en"?"Load":"ÐагÑÑзиÑÑ",totalWord=siteLanguage==="en"?"Total":"ÐÑего"; 2329 const summary=load.rows.length&&load.inputs.length 2330 ? `<div class="storageLoadSummary"><div class="storageLoadRows">${load.rows.map(row=>`<div class="storageLoadRow"><b>${loadWord} (${row.capacity.toLocaleString(siteLocale())} м³):</b> ${storageLoadItemsHtml(row.inputs)}</div>`).join("")}</div>${load.rows.length>1?`<div class="storageLoadTotal"><b>${totalWord}: (${load.capacity.toLocaleString(siteLocale())} м³):</b> ${storageLoadItemsHtml(load.inputs)}</div>`:""}</div>` 2331 : ""; 2332 return `<div class="storagePlanner" data-storage-line="${esc(key)}"> 2333 <div class="storageControls">${storageStructureControl(key,"launchpad",setting.launchpad,planet.pt,setting.launchpad)}${storageStructureControl(key,"storage",setting.storage,planet.pt,setting.launchpad)}</div> 2334 ${summary} 2335 </div>`; 2336} 2337function renderFactoryTaskLines(planet,task,taskIndex){ 2338 const count=task.kind==="assembly"?Math.max(1,Number(task.factoryCount)||1):1; 2339 const textHtml=`${count>1?`<span class="factoryMultiplier">${count}Ã</span> `:""}${taskInstructionHtml(task)}`; 2340 const planner=planet.p0.length||task.kind!=="assembly"||!(products.get(task.productId)?.i?.length)?"":storageControlsHtml(planet,task,taskIndex,count); 2341 return `<div class="taskLineBlock groupedTaskLine"><div class="task ${task.kind} ${selected.length>1?"colorTagged":""}">${colorDots(task.targetIds||targetsForProduct(task.productId))}<span class="taskText">${textHtml}</span></div>${planner}</div>`; 2342} 2343function bindStorageControls(planets){ 2344 const rerender=()=>{renderInstructions(null,planets);autosave()}; 2345 document.querySelectorAll("#assignments .storageToggle").forEach(button=>button.onclick=()=>{ 2346 const key=button.dataset.storageKey,kind=button.dataset.storageKind,defaultLaunchpads=Math.max(1,clampStorageCount(button.dataset.storageDefaultLaunchpads)),current=storageLineSetting(key,{launchpad:defaultLaunchpads,storage:0}); 2347 if(kind==="launchpad"){current.launchpad=Math.max(1,current.launchpad);storageSettings[key]=current;return} 2348 current[kind]=current[kind]>0?0:1; 2349 if(current.launchpad||current.storage)storageSettings[key]=current;else delete storageSettings[key]; 2350 rerender(); 2351 }); 2352 document.querySelectorAll("#assignments .storageCountInput").forEach(input=>input.onchange=()=>{ 2353 const key=input.dataset.storageKey,kind=input.dataset.storageKind,defaultLaunchpads=Math.max(1,clampStorageCount(input.dataset.storageDefaultLaunchpads)),current=storageLineSetting(key,{launchpad:defaultLaunchpads,storage:0}); 2354 current[kind]=Math.max(1,clampStorageCount(input.value)); 2355 storageSettings[key]=current;rerender(); 2356 }); 2357} 2358 2359function clampTemplateRadius(value){ 2360 const number=Math.round(Number(value)||0); 2361 return Math.max(0,Math.min(100000,number)); 2362} 2363function normalizedPlanetTemplateSettings(value){ 2364 const result={}; 2365 if(!value||typeof value!=="object")return result; 2366 for(const [key,item] of Object.entries(value)){ 2367 const launchpad=clampStorageCount(item?.launchpad),storage=clampStorageCount(item?.storage),radiusKm=clampTemplateRadius(item?.radiusKm); 2368 const normalized={}; 2369 if(Object.prototype.hasOwnProperty.call(item||{},"launchpad")&&launchpad>0)normalized.launchpad=launchpad; 2370 if(Object.prototype.hasOwnProperty.call(item||{},"storage"))normalized.storage=storage; 2371 if(radiusKm){normalized.radiusKm=radiusKm;normalized.radiusSource="manual"} 2372 if(Object.keys(normalized).length)result[key]=normalized; 2373 } 2374 return result; 2375} 2376function planetTemplateSettingKey(planet){return `${planet.colonyKey}|pi-template`} 2377function plannedMiningLineCount(planet){ 2378 const p1Factories=(planet.tasks||[]).filter(task=>task.kind==="assembly"&&products.get(Number(task.productId))?.t===1).reduce((sum,task)=>sum+Math.max(1,Number(task.factoryCount)||1),0);
2379 const extractionLines=Math.max((planet.p0Details||[]).length,(planet.p0||[]).length,(planet.tasks||[]).filter(task=>task.kind==="extract").reduce((sum,task)=>sum+Math.max(1,Number(task.factoryCount)||1),0)); 2380 return Math.max(1,p1Factories||extractionLines||1); 2381} 2382function planetTemplateSetting(planet){ 2383 const key=planetTemplateSettingKey(planet),saved=planetTemplateSettings[key]||{}; 2384 const automaticRadiusKm=planet.planetId&&Number(planet.radiusKm)>0?clampTemplateRadius(planet.radiusKm):0; 2385 const manualRadiusKm=automaticRadiusKm?0:clampTemplateRadius(saved.radiusKm); 2386 const radiusKm=automaticRadiusKm||manualRadiusKm,radiusSource=automaticRadiusKm?"automatic":manualRadiusKm?"manual":"standard"; 2387 if(planet.p0?.length){ 2388 const plannedLines=plannedMiningLineCount(planet); 2389 return{key,launchpad:saved.launchpad>0?Math.max(1,clampStorageCount(saved.launchpad)):1,storage:Object.prototype.hasOwnProperty.call(saved,"storage")?clampStorageCount(saved.storage):plannedLines,radiusKm,radiusSource,radiusLocked:radiusSource==="automatic",plannedLines}; 2390 } 2391 let launchpad=0,storage=0; 2392 (planet.tasks||[]).forEach((task,taskIndex)=>{ 2393 if(task.kind!=="assembly")return; 2394 const grouped=groupedStorageSetting(planet,task,taskIndex,Math.max(1,Number(task.factoryCount)||1)).setting; 2395 launchpad+=grouped.launchpad;storage+=grouped.storage; 2396 }); 2397 if(!launchpad&&!storage)launchpad=1; 2398 return{key,launchpad,storage,radiusKm,radiusSource,radiusLocked:radiusSource==="automatic"}; 2399} 2400function templateRecipe(product,weight=1){ 2401 return product?{id:product.id,name:product.n,tier:product.t,inputs:product.i,outputQuantity:product.q,weight:Math.max(1,Number(weight)||1)}:null; 2402} 2403function uniqueTemplateRecipes(items){ 2404 const byId=new Map(); 2405 items.filter(Boolean).forEach(item=>{ 2406 const current=byId.get(item.id); 2407 if(current)current.weight+=item.weight;else byId.set(item.id,{...item}); 2408 }); 2409 return[...byId.values()]; 2410} 2411function buildPlanetTemplate(planet){ 2412 const generator=window.PLANET_HUB_TEMPLATES; 2413 if(!generator)return{ok:false,error:"ÐодÑÐ»Ñ Ñаблонов не загÑÑжен."}; 2414 const setting=planetTemplateSetting(planet),exactRadiusKm=setting.radiusKm||0; 2415 const calculationRadiusKm=exactRadiusKm||generator.standardRadius(planet.pt); 2416 const assemblyTasks=(planet.tasks||[]).filter(task=>task.kind==="assembly"); 2417 const assemblyRecipes=uniqueTemplateRecipes(assemblyTasks.map(task=>templateRecipe(products.get(Number(task.productId)),task.factoryCount))); 2418 const p1Recipes=assemblyRecipes.filter(recipe=>recipe.tier===1); 2419 const higherRecipes=assemblyRecipes.filter(recipe=>recipe.tier>=2); 2420 const ccLevel=cloneCommandCenterLevel(planet.cloneIndex),common={planetType:planet.pt,planetLabel:planet.label,launchpads:setting.launchpad,storages:setting.storage,exactRadiusKm,calculationRadiusKm,radiusSource:setting.radiusSource,commandCenterLevel:ccLevel-1}; 2421 if(planet.p0?.length||p1Recipes.length){ 2422 const recipes=p1Recipes.length?p1Recipes:uniqueTemplateRecipes((planet.p0||[]).map(id=>templateRecipe(directP1ForP0(id),1))); 2423 const result=generator.generateMiningTemplate({...common,recipes,higherRecipes,includeEcu:!!planet.p0?.length,processP0P1:p1Recipes.length>0}); 2424 const requiredHigherFactories=higherRecipes.reduce((sum,recipe)=>sum+Math.max(1,Number(recipe.weight)||1),0); 2425 const requiredFactories=assemblyRecipes.reduce((sum,recipe)=>sum+Math.max(1,Number(recipe.weight)||1),0); 2426 if(result.ok&&Number(result.actualFactories||0)<requiredFactories)return{ok:false,error:`УÑÐ¾Ð²ÐµÐ½Ñ ÐºÐ¾Ð¼Ð°Ð½Ð´Ð½Ð¾Ð³Ð¾ ÑенÑÑа ${ccLevel}/6 недоÑÑаÑоÑен: ÑÑебÑеÑÑÑ Ð·Ð°Ð²Ð¾Ð´Ð¾Ð² â ${requiredFactories}, помеÑаеÑÑÑ â ${Number(result.actualFactories)||0}.`}; 2427 if(result.ok&&Number(result.higherFactoryCount||0)!==requiredHigherFactories)return{ok:false,error:`УÑÐ¾Ð²ÐµÐ½Ñ ÐºÐ¾Ð¼Ð°Ð½Ð´Ð½Ð¾Ð³Ð¾ ÑенÑÑа ${ccLevel}/6 не позволÑÐµÑ ÑазмеÑÑиÑÑ Ð²Ñе назнаÑеннÑе заводÑ.`}; 2428 return result; 2429 } 2430 const result=generator.generateFactoryTemplate({...common,recipes:higherRecipes}); 2431 const requiredFactories=higherRecipes.reduce((sum,recipe)=>sum+Math.max(1,Number(recipe.weight)||1),0); 2432 if(result.ok&&Number(result.actualFactories||0)<requiredFactories)return{ok:false,error:`УÑÐ¾Ð²ÐµÐ½Ñ ÐºÐ¾Ð¼Ð°Ð½Ð´Ð½Ð¾Ð³Ð¾ ÑенÑÑа ${ccLevel}/6 недоÑÑаÑоÑен: ÑÑебÑеÑÑÑ Ð·Ð°Ð²Ð¾Ð´Ð¾Ð² â ${requiredFactories}, помеÑаеÑÑÑ â ${Number(result.actualFactories)||0}.`}; 2433 return result; 2434} 2435function templateUsageText(result){ 2436 if(!result?.usage)return"";
2437 const cpu=Math.ceil(result.usage.cpu).toLocaleString(siteLocale()),pg=Math.ceil(result.usage.pg).toLocaleString(siteLocale()); 2438 return `CPU ${cpu} / ${result.usage.capacity.cpu.toLocaleString(siteLocale())} · Power Grid ${pg} / ${result.usage.capacity.pg.toLocaleString(siteLocale())}`; 2439} 2440const PI_PREVIEW_MIN_CENTER_GAP=100; 2441// One schematic lattice for every composition. Exported P/L/R are read-only: 2442// screen cells are deliberately independent of geographical marker compression. 2443function templatePreviewLattice(kinds){ 2444 const groups={launchpad:[],storage:[],factory:[],ecu:[],unknown:[]}; 2445 kinds.forEach((kind,index)=>groups[["basic","advanced","highTech"].includes(kind)?"factory":Object.hasOwn(groups,kind)?kind:"unknown"].push(index)); 2446 const cx=groups.launchpad.length&&groups.launchpad.length%2===0?.5:0; 2447 const occupied=new Set(),result=Array(kinds.length),key=c=>`${c.q}:${c.r}`; 2448 const xy=c=>({x:c.q+c.r/2-cx,y:c.r*Math.sqrt(3)/2}); 2449 const radius=c=>{const p=xy(c);return Math.hypot(p.x,p.y)}; 2450 const extent=Math.max(4,Math.ceil(Math.sqrt(kinds.length))*2+2),cells=[]; 2451 for(let r=-extent;r<=extent;r++)for(let q=-extent;q<=extent;q++)cells.push({q,r}); 2452 const mirrorOrbit=c=>[c,{q:2*cx-c.q-c.r,r:c.r},{q:c.q+c.r,r:-c.r},{q:2*cx-c.q,r:-c.r}].filter((v,i,a)=>a.findIndex(w=>key(w)===key(v))===i); 2453 function place(indexes,exterior=false,quartets=false){ 2454 let cursor=0; 2455 const boundary=exterior?Math.max(0,...result.filter(Boolean).map(radius))+.5:0; 2456 while(cursor<indexes.length){ 2457 const remaining=indexes.length-cursor,candidates=[]; 2458 for(const cell of cells){ 2459 if(radius(cell)+1e-8<boundary)continue; 2460 let orbit=mirrorOrbit(cell); 2461 // Odd groups put their unpaired marker on the horizontal axis; 2462 // the remaining markers always occupy complete reflected groups. 2463 if(remaining%2){if(cell.r!==0)continue;orbit=[cell]} 2464 if(orbit.length>remaining||orbit.some(c=>occupied.has(key(c))))continue; 2465 candidates.push({orbit,distance:Math.max(Math.abs(cell.r),Math.abs(cell.q-cx),Math.abs(cell.q+cell.r-cx)),width:Math.abs(xy(cell).x),preferred:quartets&&remaining%4===0&&orbit.length===4?0:1}); 2466 } 2467 // A group divisible by four must use complete mirrored quartets first. 2468 // If distance wins here, pairs consume the nearby cells and the last 2469 // factories are pushed onto the horizontal axis beside the ECU. 2470 candidates.sort((a,b)=>a.preferred-b.preferred||a.distance-b.distance||a.width-b.width||b.orbit.length-a.orbit.length||a.orbit[0].r-b.orbit[0].r||a.orbit[0].q-b.orbit[0].q); 2471 const chosen=candidates[0]; 2472 if(!chosen)throw new Error("PI preview lattice has no free symmetric cells"); 2473 chosen.orbit.forEach(cell=>{occupied.add(key(cell));result[indexes[cursor++]]=cell}); 2474 } 2475 } 2476 place(groups.launchpad); 2477 place(groups.storage); 2478 // Keep ECUs attached to the central logistics group. Putting them outside 2479 // the factory radius made an ECU drift farther away whenever more factories 2480 // were added and visually pulled the whole composition to one side. 2481 place(groups.ecu); 2482 place(groups.factory,false,true); 2483 place(groups.unknown); 2484 return result.map(xy); 2485} 2486function templatePreviewProjection(template,width=1000,height=720,padding=52,kinds=[],layoutCells=[]){ 2487 const pins=Array.isArray(template?.P)?template.P:[]; 2488 if(!pins.length)return[]; 2489 const fit=points=>{ 2490 const minX=Math.min(...points.map(point=>point.x)),maxX=Math.max(...points.map(point=>point.x)); 2491 const minY=Math.min(...points.map(point=>point.y)),maxY=Math.max(...points.map(point=>point.y)); 2492 const spanX=maxX-minX,spanY=maxY-minY; 2493 const scale=points.length===1?1:Math.min(Math.max(1,width-2*padding)/Math.max(spanX,1e-9),Math.max(1,height-2*padding)/Math.max(spanY,1e-9)); 2494 const centerX=(minX+maxX)/2,centerY=(minY+maxY)/2; 2495 return points.map(point=>({x:width/2+(point.x-centerX)*scale,y:height/2+(point.y-centerY)*scale})); 2496 }; 2497 if(!pins.every(pin=>Number.isFinite(Number(pin.La))&&Number.isFinite(Number(pin.Lo)))){ 2498 return fit(templatePreviewLattice(Array.from({length:pins.length},(_unused,index)=>kinds[index]||"unknown"))); 2499 } 2500 // Project the exact coordinates exported to EVE. PI template La/Lo offsets 2501 // are used directly by the client layout; applying geographic cos(latitude) 2502 // compression would distort the imported shape near the coordinate pole. 2503 const latitudes=pins.map(pin=>Number(pin.La)),longitudes=pins.map(pin=>Number(pin.Lo)); 2504 const centerLat=latitudes.reduce((sum,value)=>sum+value,0)/latitudes.length; 2505 const centerLon=Math.atan2(longitudes.reduce((sum,value)=>sum+Math.sin(value),0),longitudes.reduce((sum,value)=>sum+Math.cos(value),0)); 2506 const wrap=value=>Math.atan2(Math.sin(value),Math.cos(value)); 2507 const points=pins.map(pin=>({x:wrap(Number(pin.Lo)-centerLon),y:-(Number(pin.La)-centerLat)})); 2508 return fit(points); 2509} 2510function templatePreviewMarkerGeometry(points){ 2511 let minDistance=Infinity; 2512 for(let first=0;first<points.length;first++)for(let second=first+1;second<points.length;second++){ 2513 const distance=Math.hypot(points[first].x-points[second].x,points[first].y-points[second].y); 2514 if(distance<minDistance)minDistance=distance; 2515 } 2516 const gap=Number.isFinite(minDistance)?Math.min(8,Math.max(2,minDistance*.12)):8;
2517 const strokeOverflow=4; 2518 const outer=Number.isFinite(minDistance)?Math.min(40,Math.max(1,(minDistance-gap)/2-strokeOverflow)):40; 2519 return{minDistance,gap,strokeOverflow,outer,segments:outer*.84,core:outer*.66,icon:outer*.49}; 2520} 2521function templateStructureMeta(planetType,pin){ 2522 const types=window.PLANET_HUB_TEMPLATES?.structureTypeIds(planetType)||{}; 2523 const kind=pin.T===types.launchpad?"launchpad" 2524 : pin.T===types.storage?"storage" 2525 : pin.T===types.ecu?"ecu" 2526 : pin.T===types.basic?"basic" 2527 : pin.T===types.advanced?"advanced" 2528 : pin.T===types.highTech?"highTech":"unknown"; 2529 const gameNames={ 2530 launchpad:`${planetType} Launchpad`,storage:`${planetType} Storage Facility`,ecu:`${planetType} Extractor Control Unit`, 2531 basic:`${planetType} Basic Industry Facility`,advanced:`${planetType} Advanced Industry Facility`,highTech:`${planetType} High-Tech Production Plant`,unknown:`EVE Structure ${pin.T}` 2532 }; 2533 const product=products.get(Number(pin.S)); 2534 const detail=product?` â ${product.n}`:""; 2535 return{kind,gameName:gameNames[kind],label:`${gameNames[kind]}${detail}`,product}; 2536} 2537function templatePreviewMaterialRows(pins){ 2538 const resources=[...new Map(pins.filter(pin=>pin.kind==="ecu"&&pin.product).map(pin=>[pin.product.id,pin.product])).values()]; 2539 const outputs=[...new Map(pins.filter(pin=>["basic","advanced","highTech"].includes(pin.kind)&&pin.product).map(pin=>[pin.product.id,pin.product])).values()].sort((a,b)=>a.t-b.t||a.n.localeCompare(b.n)); 2540 const processedP0=new Set(outputs.filter(product=>product.t===1).flatMap(product=>(product.i||[]).map(([id])=>Number(id)))); 2541 const extractionOnly=resources.filter(product=>!processedP0.has(Number(product.id))).map(product=>({kind:"extract",stage:siteLanguage==="en"?"P0 extraction":"ÐобÑÑа P0",inputs:[],output:product})); 2542 const production=outputs.map(product=>({kind:"production",stage:`P${Math.max(0,product.t-1)} â P${product.t}`,inputs:(product.i||[]).map(([id])=>products.get(Number(id))).filter(Boolean),output:product})); 2543 return[...extractionOnly,...production]; 2544} 2545function templatePreviewMaterialRowsHtml(rows){ 2546 if(!rows.length)return `<div class="piTemplateMaterialEmpty">${siteLanguage==="en"?"No extraction or production is configured.":"ÐобÑÑа или пÑоизводÑÑво не наÑÑÑоенÑ."}</div>`; 2547 return rows.map(row=>{ 2548 const inputs=row.inputs.map((product,index)=>`${index?'<span class="piTemplateMaterialOperator"> + </span>':""}${materialInline(product,"piTemplateMaterial")}`).join(""); 2549 const relation=row.kind==="extract"?"":`<span class="piTemplateMaterialOperator"> ${row.output.t===1?"â":"="} </span>`; 2550 return `<div class="piTemplateMaterialRow"><span class="piTemplateMaterialStage">${esc(row.stage)}:</span><span class="piTemplateMaterialFlow">${inputs}${relation}${materialInline(row.output,"piTemplateMaterial piTemplateMaterialOutput")}</span></div>`; 2551 }).join(""); 2552} 2553function templatePreviewModel(planet,result=buildPlanetTemplate(planet)){ 2554 if(!result?.ok||!result.template)return{ok:false,error:result?.error||"Ðе ÑдалоÑÑ Ð¿Ð¾ÑÑÑоиÑÑ Ñаблон"}; 2555 const template=result.template,pinMeta=(template.P||[]).map(pin=>templateStructureMeta(planet.pt,pin)),points=templatePreviewProjection(template,1000,720,52,pinMeta.map(meta=>meta.kind),result.layoutCells),markerGeometry=templatePreviewMarkerGeometry(points); 2556 const pins=(template.P||[]).map((pin,index)=>({index,...pin,...points[index],...pinMeta[index],icon:`https://images.evetech.net/types/${Number(pin.T)}/icon?size=64`})); 2557 const links=(template.L||[]).map((link,index)=>({index,source:Number(link.S)-1,destination:Number(link.D)-1,level:Number(link.Lv)||0})).filter(link=>pins[link.source]&&pins[link.destination]); 2558 const count=kind=>pins.filter(pin=>pin.kind===kind).length; 2559 const counts={launchpads:count("launchpad"),storages:count("storage"),ecus:count("ecu"),basic:count("basic"),advanced:count("advanced"),highTech:count("highTech")}; 2560 counts.factories=counts.basic+counts.advanced+counts.highTech; 2561 counts.heads=pins.reduce((sum,pin)=>sum+(pin.kind==="ecu"?Number(pin.H)||0:0),0); 2562 return{ok:true,planet,result,template,pins,links,counts,markerGeometry,materialRows:templatePreviewMaterialRows(pins)}; 2563} 2564const PI_TEMPLATE_GLYPHS={ 2565 launchpad:"M10.2 19V6.2L12 4l1.8 2.2V19h-3.6ZM4 19l4.6-7.5V19H4Zm5.5 0v-5.6h1V19h-1Zm4 0v-5.6h1V19h-1Zm1.9 0v-7.5L20 19h-4.6Z", 2566 factory:"M19.14 12.94c.04-.31.06-.63.06-.94s-.02-.63-.07-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.31-.6-.22l-2.39.96c-.5-.38-1.04-.7-1.64-.94l-.36-2.55a.47.47 0 0 0-.46-.42h-3.84c-.24 0-.44.18-.47.42l-.36 2.55c-.6.24-1.15.57-1.65.95L5.2 5.34c-.22-.08-.47 0-.59.22L2.7 8.88c-.12.21-.08.47.12.61l2.03 1.58c-.05.31-.09.65-.09.94s.03.63.08.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.31.6.22l2.39-.96c.5.38 1.04.7 1.64.94l.36 2.54c.03.24.23.42.47.42h3.84c.24 0 .44-.18.47-.42l.36-2.54c.6-.24 1.15-.56 1.64-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.59ZM12 15.5a3.5 3.5 0 1 1 0-7 3.5 3.5 0 0 1 0 7Z", 2567 storage:"m12 2 9 5v10l-9 5-9-5V7l9-5Zm0 2.4L5.2 8 12 11.8 18.8 8 12 4.4ZM5 10.1v5.7l6 3.4v-5.7l-6-3.4Zm14 0-6 3.4v5.7l6-3.4v-5.7Z", 2568 ecu:"M12 21V7m0 0-5 5m5-5 5 5M5 4h14" 2569}; 2570function templatePreviewGlyphKind(kind){return["basic","advanced","highTech"].includes(kind)?"factory":kind} 2571function templatePreviewSvg(model){ 2572 const geometry=model.markerGeometry||templatePreviewMarkerGeometry(model.pins),format=value=>Number(value).toFixed(2); 2573 const linkSvg=model.links.map(link=>{ 2574 const source=model.pins[link.source],destination=model.pins[link.destination]; 2575 return `<line class="piTemplatePreviewLink" x1="${source.x.toFixed(2)}" y1="${source.y.toFixed(2)}" x2="${destination.x.toFixed(2)}" y2="${destination.y.toFixed(2)}"></line>`; 2576 }).join(""); 2577 const pinIcon=pin=>{ 2578 const size=geometry.icon*2,offset=-geometry.icon; 2579 if(pin.kind==="launchpad")return `<use class="piTemplateMapGlyph piTemplateLaunchpadGlyph" href="#piTemplateLaunchpadGlyph" x="${format(
2579offset)}" y="${format(offset)}" width="${format(size)}" height="${format(size)}"></use>`; 2580 if(["basic","advanced","highTech"].includes(pin.kind))return `<use class="piTemplateMapGlyph piTemplateFactoryGlyph" href="#piTemplateFactoryGlyph" x="${format(offset)}" y="${format(offset)}" width="${format(size)}" height="${format(size)}"></use>`; 2581 if(pin.kind==="storage")return `<use class="piTemplateMapGlyph piTemplateStorageGlyph" href="#piTemplateStorageGlyph" x="${format(offset)}" y="${format(offset)}" width="${format(size)}" height="${format(size)}"></use>`; 2582 if(pin.kind==="ecu")return `<use class="piTemplateMapGlyph piTemplateEcuGlyph" href="#piTemplateEcuGlyph" x="${format(offset)}" y="${format(offset)}" width="${format(size)}" height="${format(size)}"></use>`; 2583 return `<image href="${pin.icon}" x="${format(offset)}" y="${format(offset)}" width="${format(size)}" height="${format(size)}" preserveAspectRatio="xMidYMid meet"></image>`; 2584 }; 2585 const pinSvg=model.pins.map(pin=>`<g class="piTemplatePreviewPin kind-${pin.kind}" data-template-pin="${pin.index}" data-template-label="${esc(pin.label)}" transform="translate(${pin.x.toFixed(2)} ${pin.y.toFixed(2)})" tabindex="0" role="img" aria-label="${esc(pin.label)}"> 2586 <circle class="piTemplatePreviewPinOuter" r="${format(geometry.outer)}"></circle> 2587 <circle class="piTemplatePreviewPinSegments" r="${format(geometry.segments)}"></circle> 2588 <circle class="piTemplatePreviewPinCore" r="${format(geometry.core)}"></circle> 2589 ${pinIcon(pin)} 2590 </g>`).join(""); 2591 return `<svg class="piTemplatePreviewSvg" viewBox="0 0 1000 720" preserveAspectRatio="xMidYMid meet" role="img" aria-label="${siteLanguage==="en"?"Schematic PI template layout":"Ð¡Ñ ÐµÐ¼Ð°ÑиÑное ÑаÑположение PI-Ñаблона"}"> 2592 <defs> 2593 <symbol id="piTemplateLaunchpadGlyph" viewBox="0 0 24 24"><path d="${PI_TEMPLATE_GLYPHS.launchpad}"></path></symbol> 2594 <symbol id="piTemplateFactoryGlyph" viewBox="0 0 24 24"><path d="${PI_TEMPLATE_GLYPHS.factory}"></path></symbol> 2595 <symbol id="piTemplateStorageGlyph" viewBox="0 0 24 24"><path d="${PI_TEMPLATE_GLYPHS.storage}"></path></symbol> 2596 <symbol id="piTemplateEcuGlyph" viewBox="0 0 24 24"><path d="${PI_TEMPLATE_GLYPHS.ecu}"></path></symbol> 2597 </defs><g>${linkSvg}</g><g>${pinSvg}</g></svg>`; 2598} 2599function templatePreviewCompositionHtml(model){ 2600 const rows=[ 2601 [siteLanguage==="en"?"Launchpads":"ÐоÑмопоÑÑÑ",model.counts.launchpads], 2602 [siteLanguage==="en"?"Storage Facilities":"Ð¥ÑанилиÑа",model.counts.storages], 2603 [siteLanguage==="en"?"Factories":"ÐаводÑ",model.counts.factories], 2604 ["ECU",model.counts.ecus], 2605 [siteLanguage==="en"?"Extractor heads":"Ðоловки ÑкÑÑÑакÑоÑа",model.counts.heads], 2606 [siteLanguage==="en"?"Links":"Ðинии ÑвÑзи",model.links.length] 2607 ].filter(([_label,value],index)=>value>0||index<3); 2608 return rows.map(([label,value])=>`<div><span>${label}</span><b>${value}</b></div>`).join(""); 2609} 2610function templatePreviewUsageHtml(result){ 2611 if(!result?.usage)return""; 2612 const rows=[ 2613 ["CPU",result.usage.cpu,result.usage.capacity.cpu], 2614 ["Power Grid",result.usage.pg,result.usage.capacity.pg] 2615 ]; 2616 return rows.map(([label,used,capacity])=>{ 2617 const percent=Math.max(0,Math.min(100,Number(used)/Math.max(1,Number(capacity))*100)); 2618 return `<div class="piTemplatePreviewUsageRow"><div><span>${label}</span><b>${Math.ceil(used).toLocaleString(siteLocale())} / ${Number(capacity).toLocaleString(siteLocale())}</b></div><div class="piTemplatePreviewUsageTrack"><i style="width:${percent.toFixed(2)}%"></i></div></div>`; 2619 }).join(""); 2620} 2621function templatePreviewLegendHtml(model){ 2622 const groups=[ 2623 ["launchpad",siteLanguage==="en"?"Launchpad":"ÐоÑмопоÑÑ"],["storage",siteLanguage==="en"?"Storage Facility":"Ð¥ÑанилиÑе"], 2624 ["ecu","ECU"],["basic",siteLanguage==="en"?"Basic Industry Facility":"ÐазовÑй завод"], 2625 ["advanced",siteLanguage==="en"?"Advanced Industry Facility":"ÐÑодвинÑÑÑй завод"],["highTech",siteLanguage==="en"?"High-Tech Production Plant":"ÐÑÑокоÑÐµÑ Ð½Ð¾Ð»Ð¾Ð³Ð¸ÑнÑй завод"] 2626 ]; 2627 const rows=groups.map(([kind,label])=>{ 2628 const pin=model.pins.find(item=>item.kind===kind);if(!pin)return""; 2629 const glyphKind=templatePreviewGlyphKind(kind),glyphPath=PI_TEMPLATE_GLYPHS[glyphKind]; 2630 return `<div><span class="piTemplateLegendIcon kind-${kind}"><svg class="piTemplateLegendGlyph piTemplate${glyphKind[0].toUpperCase()+glyphKind.slice(1)}Glyph" viewBox="0 0 24 24" aria-hidden="true"><path d="${glyphPath}"></path></svg></span><span>${label}</span></div>`; 2631 }).join(""); 2632 return `${rows}<div><span class="piTemplateLegendLink"></span><span>${siteLanguage==="en"?"Link":"ÐÐ¸Ð½Ð¸Ñ ÑвÑзи"}</span></div>`; 2633} 2634let activeTemplatePreview=null;
2635function hideTemplateObjectTooltip(){ 2636 const tooltip=$("piTemplateObjectTooltip");if(tooltip)tooltip.classList.remove("visible"); 2637} 2638function showTemplateObjectTooltip(label,event,target){ 2639 const tooltip=$("piTemplateObjectTooltip"),surface=$("piTemplatePreviewSurface");if(!tooltip||!surface)return; 2640 tooltip.textContent=label;tooltip.classList.add("visible"); 2641 const bounds=surface.getBoundingClientRect?.()||{left:0,top:0,width:900,height:600}; 2642 const targetBounds=target?.getBoundingClientRect?.(); 2643 let x=event?.clientX!=null?event.clientX-bounds.left:(targetBounds?targetBounds.left+targetBounds.width/2-bounds.left:bounds.width/2); 2644 let y=event?.clientY!=null?event.clientY-bounds.top:(targetBounds?targetBounds.top-bounds.top:bounds.height/2); 2645 const width=tooltip.offsetWidth||260,height=tooltip.offsetHeight||40; 2646 x=Math.max(8,Math.min(bounds.width-width-8,x+14));y=Math.max(8,Math.min(bounds.height-height-8,y+14)); 2647 tooltip.style.left=`${x}px`;tooltip.style.top=`${y}px`; 2648} 2649function bindTemplatePreviewPins(){ 2650 const diagram=$("piTemplatePreviewDiagram");if(!diagram)return; 2651 diagram.querySelectorAll("[data-template-label]").forEach(pin=>{ 2652 const show=event=>showTemplateObjectTooltip(pin.dataset.templateLabel,event,pin); 2653 pin.onpointerenter=show;pin.onpointermove=show;pin.onpointerleave=hideTemplateObjectTooltip; 2654 pin.onfocus=show;pin.onblur=hideTemplateObjectTooltip; 2655 }); 2656} 2657function copyPlanetTemplateResult(planet,result){ 2658 if(!result?.ok){toast(result?.error||"Ðе ÑдалоÑÑ Ð¿Ð¾ÑÑÑоиÑÑ Ñаблон");return} 2659 copyText(result.text,`Шаблон ÑкопиÑован: ${result.launchpads} коÑмопоÑÑ., ${result.actualFactories} завод.`); 2660 metrikaGoal("pi_template_copied",{planet_type:planet.pt,mode:result.mode,launchpads:result.launchpads,storages:result.storages,factories:result.actualFactories}); 2661} 2662function openTemplatePreview(planet){ 2663 const result=planet&&buildPlanetTemplate(planet),model=planet&&templatePreviewModel(planet,result); 2664 if(!model?.ok){toast(model?.error||"Ðе ÑдалоÑÑ Ð¿Ð¾ÑÑÑоиÑÑ Ñаблон");return} 2665 const radiusSource=planetTemplateSetting(planet).radiusSource; 2666 activeTemplatePreview={planet,result,model,returnFocus:document.activeElement}; 2667 $("piTemplatePreviewTitle").textContent=planet.label; 2668 $("piTemplatePreviewSubtitle").textContent=`${localizedPlanetType(planet.pt)} · ${radiusSource==="automatic"?(siteLanguage==="en"?"exact radius":"ÑоÑнÑй ÑадиÑÑ"):radiusSource==="manual"?(siteLanguage==="en"?"entered radius":"ÑказаннÑй ÑадиÑÑ"):(siteLanguage==="en"?"standard radius":"ÑÑандаÑÑнÑй ÑадиÑÑ")}`; 2669 const surface=$("piTemplatePreviewSurface");surface.className=`piTemplatePreviewSurface planetScene${String(planet.pt).replace(/[^A-Za-z]/g,"")}`; 2670 $("piTemplatePreviewDiagram").innerHTML=templatePreviewSvg(model); 2671 $("piTemplatePreviewDescription").innerHTML=templatePreviewMaterialRowsHtml(model.materialRows); 2672 $("piTemplatePreviewComposition").innerHTML=templatePreviewCompositionHtml(model); 2673 $("piTemplatePreviewUsage").innerHTML=templatePreviewUsageHtml(result); 2674 $("piTemplatePreviewLegend").innerHTML=templatePreviewLegendHtml(model); 2675 bindTemplatePreviewPins();hideTemplateObjectTooltip(); 2676 $("piTemplatePreviewModal").classList.remove("hidden");document.body.classList.add("piTemplatePreviewOpen"); 2677 $("piTemplatePreviewClose")?.focus?.(); 2678 metrikaGoal("pi_template_previewed",{planet_type:planet.pt,pins:model.pins.length,links:model.links.length}); 2679} 2680function closeTemplatePreview(){ 2681 const modal=$("piTemplatePreviewModal");if(!modal)return; 2682 modal.classList.add("hidden");document.body.classList.remove("piTemplatePreviewOpen");hideTemplateObjectTooltip(); 2683 activeTemplatePreview?.returnFocus?.focus?.();activeTemplatePreview=null; 2684} 2685function planetTemplatePanelHtml(planet){ 2686 const setting=planetTemplateSetting(planet),result=buildPlanetTemplate(planet),source=setting.radiusSource; 2687 const mode=source==="automatic" 2688 ? (siteLanguage==="en"?"Exact template":"ТоÑнÑй Ñаблон") 2689 : source==="manual" 2690 ? (siteLanguage==="en"?"Template by entered radius":"Шаблон по ÑÐºÐ°Ð·Ð°Ð½Ð½Ð¾Ð¼Ñ ÑадиÑÑÑ") 2691 : (siteLanguage==="en"?"Standard template":"СÑандаÑÑнÑй Ñаблон"); 2692 const tooltip=source==="automatic" 2693 ? (siteLanguage==="en"?`The exact ${setting.radiusKm.toLocaleStr
2693ing(siteLocale())} km radius was loaded automatically for this planet and cannot be changed.`:`ТоÑнÑй ÑадиÑÑ ${setting.radiusKm.toLocaleString(siteLocale())} км полÑÑен авÑомаÑиÑеÑки Ð´Ð»Ñ ÑÑой планеÑÑ Ð¸ заÑиÑÑн Ð¾Ñ Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ð¹.`) 2694 : source==="manual" 2695 ? (siteLanguage==="en"?`The entered ${setting.radiusKm.toLocaleString(siteLocale())} km radius is used to calculate link lengths.`:`УказаннÑй вÑÑÑнÑÑ ÑадиÑÑ ${setting.radiusKm.toLocaleString(siteLocale())} км иÑполÑзÑеÑÑÑ Ð´Ð»Ñ ÑаÑÑÑÑа Ð´Ð»Ð¸Ð½Ñ ÑвÑзей.`) 2696 : (siteLanguage==="en"?`The radius is unknown. A safe standard estimate for the ${localizedPlanetType(planet.pt)} planet type is used.`:`РадиÑÑ Ð½Ðµ Ñказан. ÐÑполÑзÑеÑÑÑ Ð±ÐµÐ·Ð¾Ð¿Ð°ÑÐ½Ð°Ñ ÑÑандаÑÑÐ½Ð°Ñ Ð¾Ñенка Ð´Ð»Ñ Ñипа планеÑÑ Â«${localizedPlanetType(planet.pt)}».`); 2697 const radiusBadge=source==="automatic"?(siteLanguage==="en"?"ð Auto":"ð ÐвÑо"):source==="manual"?(siteLanguage==="en"?"Manual":"ÐÑÑÑнÑÑ"):(siteLanguage==="en"?"Standard":"СÑандаÑÑ"); 2698 const infra=result.ok 2699 ? (siteLanguage==="en" 2700 ? `<span>Launchpads - <b>${result.launchpads}</b>;</span><span>Storage Facilities - <b>${result.storages}</b>;</span><span>Factories - <b>${result.actualFactories}</b>;</span>` 2701 : `<span>ÐоÑмопоÑÑ - <b>${result.launchpads}</b>;</span><span>Ð¥ÑанилиÑе - <b>${result.storages}</b>;</span><span>ÐÐ°Ð²Ð¾Ð´Ñ - <b>${result.actualFactories}</b>;</span>`) 2702 : `<span>${esc(result.error)}</span>`; 2703 const warnings=result.ok&&result.warnings?.length?`<div class="piTemplateWarnings">${result.warnings.map(esc).join(" ")}</div>`:""; 2704 const extractionControls=planet.p0?.length?`<div class="piTemplateInfraInputs">${templateStructureControl(setting,"launchpad",planet.pt)}
2704${templateStructureControl(setting,"storage",planet.pt)}</div>`:""; 2705 return `<div class="piTemplatePanel ${result.ok?"ready":"invalid"}" data-template-planet="${esc(planet.colonyKey)}"> 2706 <div class="piTemplateHead"><b>${mode}</b><span class="settingHelp piTemplateHelp" tabindex="0" role="button" aria-label="ÐпиÑание Ñаблона" data-tooltip="${esc(tooltip)}">?</span></div> 2707 ${extractionControls} 2708 <div class="piTemplateRadius"><label>${siteLanguage==="en"?"Radius, km":"РадиÑÑ, км"} <input class="piTemplateRadiusInput ${setting.radiusLocked?"automaticRadius":""}" type="number" min="1" max="100000" step="10" value="${setting.radiusKm||""}" placeholder="${siteLanguage==="en"?"standard":"ÑÑандаÑÑ"}" data-template-key="${esc(setting.key)}" data-template-default-launchpads="${setting.launchpad}" data-template-default-storages="${setting.storage}" ${setting.radiusLocked?`disabled readonly aria-label="${siteLanguage==="en"?"Exact radius loaded automatically":"ТоÑнÑй ÑадиÑÑ Ð¿Ð¾Ð»ÑÑен авÑомаÑиÑеÑки"}"`:''}><span class="piTemplateRadiusSource ${source}">${radiusBadge}</span></label><small class="piTemplateInfraSummary">${infra}</small></div> 2709 ${result.ok?`<div class="piTemplateUsage">${esc(templateUsageText(result))}</div>`:""} 2710 ${warnings} 2711 <div class="piTemplateActions"> 2712 <button class="copyPiTemplate" type="button" data-colony-key="${esc(planet.colonyKey)}" ${result.ok?"":"disabled"}>â§ ${siteLanguage==="en"?"Copy template":"СкопиÑоваÑÑ Ñаблон"}</button> 2713 <button class="previewPiTemplate" type="button" data-colony-key="${esc(planet.colonyKey)}" aria-label="${siteLanguage==="en"?"Preview template":"ÐоÑмоÑÑеÑÑ Ñаблон"}" title="${siteLanguage==="en"?"Preview template":"ÐоÑмоÑÑеÑÑ Ñаблон"}" ${result.ok?"":"disabled"}><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M2.4 12s3.5-6 9.6-6 9.6 6 9.6 6-3.5 6-9.6 6-9.6-6-9.6-6Z"></path><circle cx="12" cy="12" r="2.8"></circle></svg></button> 2714 </div> 2715 </div>`; 2716} 2717function bindPlanetTemplateControls(planets){ 2718 const byKey=new Map((planets||[]).map(planet=>[planet.colonyKey,planet])); 2719 const rerender=()=>{renderInstructions(null,planets);autosave()}; 2720 document.querySelectorAll("#assignments .piTemplateInfraToggle").forEach(button=>button.onclick=()=>{ 2721 const key=button.dataset.templateKey,field=button.dataset.templateField,current=planetTemplateSettings[key]||{launchpad:Math.max(1,clampStorageCount(button.dataset.templateDefaultLaunchpads)),storage:clampStorageCount(button.dataset.templateDefaultStorages),radiusKm:0}; 2722 if(field==="launchpad"){current.launchpad=Math.max(1,clampStorageCount(current.launchpad));planetTemplateSettings[key]=current;return} 2723 const defaultCount=Math.max(1,clampStorageCount(button.dataset.templateDefaultStorages)); 2724 current[field]=clampStorageCount(current[field])>0?0:defaultCount;planetTemplateSettings[key]=current;rerender(); 2725 }); 2726 document.querySelectorAll("#assignments .piTemplateInfraInput").forEach(input=>input.onchange=()=>{ 2727 const key=input.dataset.templateKey,field=input.dataset.templateField,current=planetTemplateSettings[key]||{launchpad:Math.max(1,clampStorageCount(input.dataset.templateDefaultLaunchpads)),storage:clampStorageCount(input.dataset.templateDefaultStorages),radiusKm:0}; 2728 current[field]=Math.max(1,clampStorageCount(input.value));planetTemplateSettings[key]=current;rerender(); 2729 }); 2730 document.querySelectorAll("#assignments .piTemplateRadiusInput").forEach(input=>input.onchange=()=>{ 2731 if(input.disabled)return; 2732 const key=input.dataset.templateKey,current=planetTemplateSettings[key]||{launchpad:Math.max(1,clampStorageCount(input.dataset.templateDefaultLaunchpads)),storage:clampStorageCount(input.dataset.templateDefaultStorages),radiusKm:0}; 2733 current.radiusKm=clampTemplateRadius(input.value);current.radiusSource=current.radiusKm?"manual":"standard";planetTemplateSettings[key]=current;rerender(); 2734 }); 2735 document.querySelectorAll("#assignments .copyPiTemplate").forEach(button=>button.onclick=()=>{ 2736 const planet=byKey.get(button.dataset.colonyKey),result=planet&&buildPlanetTemplate(planet); 2737 copyPlanetTemplateResult(planet,result); 2738 }); 2739 document.querySelectorAll("#assignments .previewPiTemplate").forEach(button=>button.onclick=()=>openTemplatePreview(byKey.get(button.dataset.colonyKey))); 2740} 2741 2742function renderInstructions(a,preparedPlan=null){
2743 const planets=preparedPlan||buildPlanetInstructions(a); 2744 const transfers=annotateCloneTransfers(planets); 2745 const assignments=assignToClones(planets); 2746 2747 let html=""; 2748 assignments.forEach(c=>{ 2749 html+=`<section class="clonePlan"><div class="clonePlanHead"><b>${esc(c.name)}</b><span class="cloneUsage">${c.planets.length}/${c.limit} Ð¿Ð»Ð°Ð½ÐµÑ Ð·Ð°Ð´ÐµÐ¹ÑÑвовано</span></div>`; 2750 c.planets.forEach(p=>{ 2751 const assemblyLabel=p.p0.length?"":" â ÑбоÑоÑнаÑ"; 2752 html+=`<div class="planetPlan planetScene planetScene${esc(p.pt)}"><div class="planetTitle"><b>${planetIcon(p.pt)}${esc(p.label)}${assemblyLabel}</b><label class="planetBuildToggle"><input type="checkbox" class="planetBuiltCheck" autocomplete="off" aria-label="${esc((siteLanguage==="en"?"Built: ":"ÐоÑÑÑоено: ")+c.name+" â "+p.label)}">${siteLanguage==="en"?"Built":"ÐоÑÑÑоено"}</label></div> 2753 ${p.tasks.map((t,taskIndex)=>renderFactoryTaskLines(p,t,taskIndex)).join("")}${planetTemplatePanelHtml(p)}</div>`; 2754 }); 2755 const free=c.limit-c.planets.length; 2756 if(free>0)html+=`<div class="freePlanet">Свободно Ð¿Ð»Ð°Ð½ÐµÑ Ñ ÐºÐ»Ð¾Ð½Ð°: ${free}</div>`; 2757 html+="</section>"; 2758 }); 2759 2760 if(transfers.length){ 2761 const transferTitle=siteLanguage==="en"?"TRANSFERS BETWEEN CLONES":"ÐÐÐ ÐÐÐЧРÐÐÐÐУ ÐÐÐÐÐÐÐ"; 2762 const tooltip=siteLanguage==="en" 2763 ?"If no fraction is shown, transfer the entire material. âTransfer 1/2â means half of the total amount of that material produced by the sending clone. Every fraction is calculated from the original total, not from the remainder after another transfer." 2764 :"ÐÑли возле маÑеÑиала Ð½ÐµÑ Ð´Ñоби â пеÑедайÑе его Ñеликом. «ÐеÑедаÑÑ 1/2» ознаÑÐ°ÐµÑ Ð¿Ð¾Ð»Ð¾Ð²Ð¸Ð½Ñ Ð¾Ñ Ð²Ñего колиÑеÑÑва ÑÑого маÑеÑиала, пÑоизведÑнного клоном-оÑпÑавиÑелем. ÐÑе доли ÑÑиÑаÑÑÑÑ Ð¾Ñ Ð¸ÑÑ Ð¾Ð´Ð½Ð¾Ð³Ð¾ обÑего колиÑеÑÑва, а не Ð¾Ñ Ð¾ÑÑаÑка поÑле пÑедÑдÑÑей пеÑедаÑи."; 2765 const helpLabel=siteLanguage==="en"?"How transfer fractions work":"Ðак обознаÑаÑÑÑÑ Ð¿Ð¾Ð»Ð½Ð°Ñ Ð¸ ÑаÑÑиÑÐ½Ð°Ñ Ð¿ÐµÑедаÑи"; 2766 html+=`<section class="transferSummary"><div class="transferSummaryHead"><span>${transferTitle}</span><span class="settingHelp transferSummaryHelp" tabindex="0" role="button" aria-label="${esc(helpLabel)}" data-tooltip="${esc(tooltip)}">?</span></div>${transfers.map(t=>`<div class="transferRoute"><b>${esc(t.fromName)}
2766 â ${esc(t.toName)}</b><span class="transferProducts">${[...t.products.values()].map(transferSummaryProductHtml).join('<span class="transferSeparator">, </span>')}</span></div>`).join("")}</section>`; 2767 } 2768 const systemSuffix=String(selectedSystemInfo?.name||$("systemInput")?.value||"").trim(); 2769 const defaultPlanName=`${siteLanguage==="en"?"Plan":"Ðлан"} ${savedPlanCount+1}${systemSuffix?` ${systemSuffix}`:""}`; 2770 html+=`<section class="planSaveBar"><label><span>${siteLanguage==="en"?"Plan name":"Ðазвание плана"}</span><input id="planSaveName" maxlength="80" value="${esc(defaultPlanName)}"></label><button id="saveAccountPlan" type="button">${siteLanguage==="en"?"Save plan":"Ð¡Ð¾Ñ ÑаниÑÑ Ð¿Ð»Ð°Ð½"}</button><span id="planSaveStatus"></span></section>`; 2771 2772 $("assignments").innerHTML=`<p id="planetBuildProgress" class="planetBuildProgress" role="status" aria-live="polite"></p>`+html; 2773 bindPlanetBuildChecks(); 2774 bindStorageControls(planets); 2775 bindPlanetTemplateControls(planets); 2776 bindPlanSaveControl(); 2777} 2778 2779function bindPlanetBuildChecks(){ 2780 // Temporary DOM-only checklist: never part of saved/exported plan state. 2781 const boxes=[...document.querySelectorAll("#assignments .planetBuiltCheck")]; 2782 const update=()=>{ 2783 const count=boxes.filter(box=>box.checked).length; 2784 const status=$("planetBuildProgress"); 2785 if(status)status.textContent=siteLanguage==="en"?`Built ${count} of ${boxes.length}`:`ÐоÑÑÑоено ${count} из ${boxes.length}`; 2786 }; 2787 for(const box of boxes){ 2788 box.checked=false; 2789 const card=box.closest(".planetPlan"); 2790 if(card)card.classList.remove("planetBuilt"); 2791 box.onchange=()=>{if(card)card.classList.toggle("planetBuilt",box.checked);update();}; 2792 } 2793 update(); 2794} 2795 2796function bindPlanSaveControl(){ 2797 const button=$("saveAccountPlan");if(!button)return; 2798 button.onclick=async()=>{ 2799 if(!accountCsrf){location.href="/account";return} 2800 const name=$("planSaveName").value.trim(),status=$("planSaveStatus");if(!name){status.textContent="УкажиÑе название";return} 2801 button.disabled=true;status.textContent="Ð¡Ð¾Ñ ÑанÑемâ¦"; 2802 try{const response=await fetch("/api/plans",{method:"POST",credentials:"same-origin",headers:{"Content-Type":"application/json","X-CSRF-Token":accountCsrf},body:JSON.stringify({name,project:serializeProject(false)})});const data=await response.json().catch(()=>({}));if(!response.ok)throw new Error(data.error||"Ðе ÑдалоÑÑ ÑÐ¾Ñ ÑаниÑÑ Ð¿Ð»Ð°Ð½");savedPlanCount++;status.textContent=`Ð¡Ð¾Ñ Ñанено: ${data.plan.name}`;toast("Ðлан ÑÐ¾Ñ ÑанÑн в аккаÑнÑе")} 2803 catch(error){status.textContent=error.message}finally{button.disabled=false} 2804 }; 2805} 2806 2807const initialPlannerLocation={pathname:location.pathname,search:location.search}; 2808async function loadLocalAccountData(){ 2809 try{ 2810 const query=new URLSearchParams(initialPlannerLocation.search); 2811 const pathShare=initialPlannerLocation.pathname.match(/(?:^|\/)p\/([A-Za-z0-9_-]{20,64})\/?$/); 2812 const planId=query.get("plan"),shareToken=query.get("share")||(pathShare?.[1]||""); 2813 const planRequest=planId||shareToken?fetch(planId?`/api/plans/${encodeURIComponent(planId)}`:`/api/shared-plans/${encodeURIComponent(shareToken)}`,{credentials:"same-origin",cache:"no-store"}):null; 2814 const session=await fetch("/api/auth/session",{credentials:"same-origin",cache:"no-store"});if(!session.ok&&!planRequest)return; 2815 const data=session.ok?await session.json():{};accountCsrf=data.csrf||"";authorizedClones=Array.isArray(data.characters)?data.characters:[]; 2816 if(query.get("eveClones")==="1"){ 2817 let transfer=null;try{transfer=JSON.parse(localStorage.getItem("planetHubEvePlannerTransfer")||"null")}catch(_error){} 2818 const allowed=new Set(authorizedClones.map(item=>Number(item.character_id))); 2819 const imported=transfer?.version===1&&Date.now()-Number(transfer.savedAt)<600000&&Array.isArray(transfer.clones)?transfer.clones.filter(item=>allowed.has(Number(item.characterId))).map((item,index)=>({name:String(item.name||`Ðлон ${index+1}`),limit:fix(item.limit),ccLevel:Math.max(1,Math.min(6,Math.round(Number(item.ccLevel)||6))),characterId:Number(item.characterId)})):[]; 2820 if(imported.length){clones=imported;allocationMemo=new Map();invalidateProfitabilityRanking();autosave();toast(`ÐÐ»Ð¾Ð½Ñ EVE добавленÑ: ${imported.length}`);}
2821 try{localStorage.removeItem("planetHubEvePlannerTransfer")}catch(_error){} 2822 query.delete("eveClones");history.replaceState(null,"",location.pathname+(query.toString()?`?${query}`:"")); 2823 } 2824 renderClones(); 2825 const plansRequest=data.user?fetch("/api/plans",{credentials:"same-origin",cache:"no-store"}):null; 2826 if(planRequest){ 2827 const response=await planRequest; 2828 const payload=await response.json().catch(()=>({}));if(!response.ok)throw new Error(payload.error||"Ðе ÑдалоÑÑ Ð·Ð°Ð³ÑÑзиÑÑ Ð¿Ð»Ð°Ð½"); 2829 const project=planId?payload.plan?.project:payload.project; 2830 if(!project||typeof project!=="object")throw new Error("СеÑÐ²ÐµÑ Ð²ÐµÑнÑл пÑÑÑой план"); 2831 const applied=await applyProject(project,{shared:!!shareToken}); 2832 if(!applied)throw new Error("Ðе ÑдалоÑÑ Ð¿ÑимениÑÑ Ð·Ð°Ð³ÑÑженнÑй план"); 2833 history.replaceState(null,"",initialPlannerLocation.pathname); 2834 setTimeout(()=>document.getElementById("planPanel")?.scrollIntoView({behavior:"smooth",block:"start"}),450); 2835 } 2836 if(plansRequest){const plans=await plansRequest;if(plans.ok){savedPlanCount=(await plans.json()).plans?.length||0;const input=$("planSaveName"),systemSuffix=String(selectedSystemInfo?.name||$("systemInput")?.value||"").trim();if(input&&/^(Ðлан|Plan) 1(?:\s+.*)?$/.test(input.value))input.value=`${siteLanguage==="en"?"Plan":"Ðлан"} ${savedPlanCount+1}${systemSuffix?` ${systemSuffix}`:""}`;}} 2837 }catch(error){toast(error.message||"Ðе ÑдалоÑÑ Ð·Ð°Ð³ÑÑзиÑÑ Ð´Ð°Ð½Ð½Ñе аккаÑнÑа")} 2838} 2839 2840function plan(){ 2841 if(!selected.length){$("planPanel").classList.add("hidden");renderCommandCenterSummary([]);return} 2842 $("planPanel").classList.remove("hidden"); 2843 let a=allocation(selected); 2844 if(a.ok&&$("maxUtilization").checked&&selected.length===1){ 2845 const packed=maximumPackedAllocation(selected[0]); 2846 if(packed?.allocation?.ok)a=packed.allocation; 2847 } 2848 const built=a.ok?buildPlanetInstructionsFor(selected,a,!!$("maxUtilization").checked):{plan:[],segments:[]}; 2849 const finalPlan=built.plan; 2850 if(a.ok){ 2851 const resourceError=finalPlan.map(planet=>({planet,result:buildPlanetTemplate(planet)})).find(item=>!item.result.ok); 2852 if(resourceError)a={...a,ok:false,reason:`${resourceError.planet.cloneName} · ${resourceError.planet.label}: ${resourceError.result.error}`}; 2853 } 2854 const usedPlanetCount=finalPlan.length; 2855 const freePlanetCount=Math.max(0,cap()-usedPlanetCount); 2856 const currentEconomics=a.ok&&marketSettings.enabled&&!marketLoading?evaluateBuiltPlanEconomics(built,selected):null; 2857 $("planTitle").innerHTML=selected.map(id=>{ 2858 const product=products.get(id); 2859 return `${selected.length>1?colorDots([id]):""}${materialIcon(product,"planTargetIcon")}<span class="planTargetName">${esc(product.n)}</span>`; 2860 }).join(`<span class="planTargetSeparator"> + </span>`); 2861 $("planStats").classList.toggle("hasPlanValue",marketSettings.enabled); 2862 $("planStats").innerHTML=`<div class="stat"><span>ÐÑбÑано</span><b>${selected.length}</b></div> 2863 <div class="stat"><span>${$("maxUtilization").checked?"ÐадейÑÑвовано планеÑ":"ÐинимÑм планеÑ"}</span><b>${a.ok?usedPlanetCount:"â"}</b></div> 2864 <div class="stat"><span>ÐоÑÑÑпно клонам</span><b>${cap()}</b></div> 2865 <div class="stat"><span>Свободно планеÑ</span><b>${a.ok?freePlanetCount:"â"}</b></div> 2866 ${marketSettings.enabled?`<div class="stat planValueStat"><span>ÐÑÐ¾Ð³Ð¾Ð²Ð°Ñ ÑÑоимоÑÑÑ Ð¿Ð»Ð°Ð½Ð°</span><b>${marketLoading?"ÐагÑÑзка Ñенâ¦":currentEconomics?`${Math.round(currentEconomics.total30Days).toLocaleString(siteLocale())} ISK / 30 дней`:"â"}</b></div>`:""}`; 2867 renderCommandCenterSummary(a.ok?finalPlan:[]); 2868 if(!a.ok){$("assignments").innerHTML=`<div class="assign"><b style="color:var(--bad)">Ðлан невÑполним</b><div class="reason">${esc(a.reason||"")}</div></div>`;maybeResumeGuidedTour("plan");return} 2869 renderInstructions(a,finalPlan);renderChain();trackCreatedPlan(finalPlan);maybeResumeGuidedTour("plan"); 2870} 2871let recalcToken=0; 2872const calculationPause=()=>new Promise(resolve=>setTimeout(resolve,24)); 2873function setCalculationProgress(percent,stage){ 2874 $("calculationPercent").textContent=`${percent}%`; 2875 $("calculationBar").style.width=`${percent}%`; 2876 $("calculationStage").textContent=translateSiteText(stage); 2877} 2878function recalc(){ 2879 if(!current)return; 2880 const token=++recalcToken; 2881 $("calculationProgress").classList.remove("hidden"); 2882 setCalculationProgress(5,"ÐодгоÑовка даннÑÑ â¦"); 2883 (async()=>{ 2884 try{ 2885 await calculationPause();if(token!==recalcToken)return; 2886 allocationMemo=new Map(); 2887 setCalculationProgress(25,"ÐÑовеÑка вÑбÑанной конÑигÑÑаÑииâ¦"); 2888 const selectedPlan=selected.length?allocation(selected):null; 2889 if(selectedPlan&&!selectedPlan.ok){ 2890 setCalculationProgress(45,"ÐонÑигÑÑаÑÐ¸Ñ Ð½ÐµÐ²Ð¾Ð·Ð¼Ð¾Ð¶Ð½Ð° â ÑоÑмиÑование пÑедÑпÑеждениÑâ¦"); 2891 await calculationPause();if(token!==recalcToken)return; 2892 } 2893 setCalculationProgress(55,"Ðбновление доÑÑÑпнÑÑ Ð¼Ð°ÑеÑиаловâ¦"); 2894 renderPotential(); 2895 await calculationPause();if(token!==recalcToken)return; 2896 setCalculationProgress(72,"ÐеÑеÑÑÑÑ ÑпиÑков P0âP4â¦"); 2897 mats(); 2898 await calculationPause();if(token!==recalcToken)return; 2899 setCalculationProgress(90,"ФоÑмиÑование плана по клонамâ¦"); 2900 plan(); 2901 await calculationPause();if(token!==recalcToken)return; 2902 setCalculationProgress(100,"ÐоÑово"); 2903 await calculationPause();if(token!==recalcToken)return; 2904 }catch(err){ 2905 console.error(err);toast("ÐÑибка пеÑеÑÑÑÑа: "+err.message); 2906 }finally{ 2907 if(token===recalcToken)$("calculationProgress").classList.add("hidden"); 2908 } 2909 })(); 2910} 2911 2912function run(){ 2913 if(planetAutoApplyTimer){clearTimeout(planetAutoApplyTimer);planetAutoApplyTimer=null} 2914 document.querySelectorAll(".planetCountRow input").forEach(input=>{ 2915 planetCounts[input.dataset.pt]=clampPlanetCount(input.value); 2916 input.value=planetCounts[input.dataset.pt]; 2917 }); 2918 if(!Object.values(planetCounts).some(n=>n>0)){ 2919 alert(translateSiteText("УкажиÑе Ñ Ð¾ÑÑ Ð±Ñ Ð¾Ð´Ð½Ñ Ð´Ð¾ÑÑÑпнÑÑ Ð¿Ð»Ð°Ð½ÐµÑÑ"));return; 2920 } 2921 current={system:selectedSystemInfo?{...selectedSystemInfo,planets:D.meta.planetTypes.map(type=>planetCounts[type]||0)}:manualSystem()}; 2922 planetCountsDirty=false; 2923 trackedPlanSignatures.clear(); 2924 selected=[]; 2925 targetColors={}; 2926 planetSettings={}; 2927 storageSettings={}; 2928 planetTemplateSettings={}; 2929 ensurePlanetSettings(); 2930 $("empty").classList.add("hidden"); 2931 $("result").classList.remove("hidden"); 2932 metrikaGoal("planets_applied",{total_planets:physicalPlanets(),clone_capacity:cap()}); 2933 invalidateProfitabilityRanking();recalc();autosave();maybeResumeGuidedTour("planner"); 2934} 2935$("showUnavailable").onchange=()=>
2935{recalc();autosave()}; 2936$("clearSelectionBtn").onclick=()=>{selected=[];targetColors={};recalc();autosave()}; 2937 2938document.querySelectorAll(".planTab").forEach(btn=>btn.onclick=()=>{ 2939 document.querySelectorAll(".planTab").forEach(x=>x.classList.remove("active")); 2940 btn.classList.add("active"); 2941 let v=btn.dataset.view; 2942 $("instructionView").classList.toggle("hidden",v!=="instructions"); 2943 $("chainView").classList.toggle("hidden",v!=="chain"); 2944}); 2945 2946/* ---------- Save / load / diagnostic export ---------- */ 2947function serializeProject(includeDiagnostics=false){ 2948 const base={ 2949 schema:PROJECT_SCHEMA, 2950 version:PROJECT_VERSION, 2951 sdeBuild:D.meta.sdeBuild, 2952 savedAt:new Date().toISOString(), 2953 system:current?{id:current.system?.id??null,name:current.system?.name||"Ð ÑÑной ÑоÑÑав",security:current.system?.security??null,source:selectedSystemInfo?.source||"manual",planetDetails:normalizedPlanetDetails(current.system?.planetDetails)}:null, 2954 planetCounts:{...planetCounts}, 2955 clones:clones.map(c=>({...c})), 2956 settings:{ 2957 maxUtilization:$("maxUtilization").checked, 2958 showUnavailable:$("showUnavailable").checked 2959 }, 2960 globalPlanetSettings:JSON.parse(JSON.stringify(globalPlanetSettings)), 2961 productionSettings:JSON.parse(JSON.stringify(productionSettings)), 2962 marketSettings:JSON.parse(JSON.stringify(marketSettings)), 2963 planetSettings:JSON.parse(JSON.stringify(planetSettings)), 2964 storageSettings:JSON.parse(JSON.stringify(storageSettings)), 2965 planetTemplateSettings:JSON.parse(JSON.stringify(planetTemplateSettings)), 2966 selected:[...selected], 2967 targetColors:{...targetColors} 2968 }; 2969 if(includeDiagnostics&¤t){ 2970 let a=selected.length?allocation(selected):null; 2971 if(a?.ok&&$("maxUtilization").checked&&selected.length===1){ 2972 const packed=maximumPackedAllocation(selected[0]); 2973 if(packed?.allocation?.ok)a=packed.allocation; 2974 } 2975 const generatedBuilt=a?.ok?buildPlanetInstructionsFor(selected,a,!!$("maxUtilization").checked):null; 2976 const generatedPlan=generatedBuilt?.plan||null; 2977 const currentPlanEconomics=generatedBuilt&&marketSettings.enabled&&!marketLoading?evaluateBuiltPlanEconomics(generatedBuilt,selected):null; 2978 if(generatedPlan)annotateCloneTransfers(generatedPlan); 2979 base.diagnostics={ 2980 capacity:cap(), 2981 physicalPlanets:physicalPlanets(), 2982 selectedProducts:selected.map(id=>products.get(id)).filter(Boolean), 2983 dependencies:dependencyProducts(selected), 2984 requiredP0:selected.length?[...selected.reduce((acc,id)=>{roots(id).forEach(x=>acc.add(x));return acc},new Set())]:[], 2985 allocation:a, 2986 finalPlanStats:generatedPlan?{usedPlanets:generatedPlan.length,freePlanets:Math.max(0,cap()-generatedPlan.length)}:null, 2987 generatedPlan, 2988 currentPlanEconomics, 2989 marketSnapshot:marketSettings.enabled?{ 2990 hub:marketSettings.hub, 2991 priceDate:marketPriceDate, 2992 priceBasis:"30-day volume-weighted average of ESI market history", 2993 prices:[...marketPrices].map(([id,average30])=>({id,name:products.get(id)?.n||String(id),tier:products.get(id)?.t,average30,lastCompletedDayAverage:marketTrends.get(id)?.current??null,averageDailyVolume:marketTrends.get(id)?.averageDailyVolume??null,volume:PRODUCT_VOLUME[products.get(id)?.t]||0})), 2994 profitabilityRanking:profitabilityRankingCache.map((x,index)=>({ 2995 rank:index+1,id:x.product.id,name:x.product.n,tier:x.product.t,total30Days:x.total30Days, 2996 customsTransfer30Days:x.customsTransfer30Days,customsTransferExport30Days:x.customsTransferExport30Days,customsTransferImport30Days:x.customsTransferImport30Days,p0Requirements:x.p0Requirements,usedPlanets:x.usedPlanets,freePlanets:x.freePlanets,targetLines:x.targetLines,lines:x.lines 2997 })) 2998 }:null 2999 }; 3000 } 3001 return base; 3002} 3003function downloadJson(obj,filename){ 3004 const blob=new Blob([JSON.stringify(obj,null,2)],{type:"application/json"}); 3005 const url=URL.createObjectURL(blob),a=document.createElement("a"); 3006 a.href=url;a.download=filename;document.body.appendChild(a);a.click();a.remove(); 3007 setTimeout(()=>URL.revokeObjectURL(url),1000); 3008} 3009$("exportDebugBtn").onclick=()=>{ 3010 downloadJson(serializeProject(true),`eve_pi_debug_${current?.system?.name||"no-system"}.json`); 3011 toast("ÐиагноÑÑиÑеÑкий Ñайл Ñоздан"); 3012}; 3013// ÐмпоÑÑ Ð¿ÑоекÑов вÑеменно ÑкÑÑÑ Ð¸Ð· инÑеÑÑейÑа; авÑоÑÐ¾Ñ Ñанение в бÑаÑзеÑе оÑÑаÑÑÑÑ. 3014async function applyProject(obj,{shared=false}={}){ 3015 try{ 3016 if(obj.schema!==PROJECT_SCHEMA)throw new Error("ÐеизвеÑÑнÑй ÑоÑÐ¼Ð°Ñ Ð¿ÑоекÑа"); 3017 const sourceClones=Array.isArray(obj.clones)&&obj.clones.length?obj.clones:[]; 3018 const plannedIds=sourceClones.map(c=>Number(c.characterId)).filter(Number.isSafeInteger).sort((a,b)=>a-b); 3019 const authorizedIds=authorizedClones.map(c=>Number(c.character_id)).filter(Number.isSafeInteger).sort((a,b)=>a-b); 3020 const exactCloneMatch=plannedIds.length===sourceClones.length&&plannedIds.length===authorizedIds.length&&plannedIds.every((id,index)=>id===authorizedIds[index]); 3021 clones=sourceClones.length?sourceClones.map((c,index)=>({ 3022 name:shared&&!exactCloneMatch?`Ðлон ${index+1}`:(c.name||`Ðлон ${index+1}`),limit:fix(c.limit),ccLevel:Math.max(1,Math.min(6,Math.round(Number(c.ccLevel)||6))), 3023 ...(shared&&!exactCloneMatch?{}:Number.isSafeInteger(Number(c.characterId))?{characterId:Number(c.characterId)}:{}) 3024 })):[{name:"Ðлон 1",limit:6,ccLevel:6}]; 3025 $("maxUtilization").checked=!!obj.settings?.maxUtilization; 3026 $("showUnavailable").checked=obj.settings?.showUnavailable===true; 3027 renderClones(); 3028 if(obj.planetCounts||obj.system?.name){ 3029 if(obj.planetCounts){ 3030 planetCounts=Object.fromEntries(D.meta.planetTypes.map(pt=>[pt,clampPlanetCount(obj.planetCounts[pt])])); 3031 }else{ 3032 const legacy=D.systems.find(s=>s.name===obj.system.name); 3033 if(!legacy)throw new Error("Ð ÑÑаÑом пÑоекÑе Ð½ÐµÑ Ð´Ð°Ð½Ð½ÑÑ Ð¾ колиÑеÑÑве планеÑ"); 3034 planetCounts=Object.fromEntries(D.meta.planetTypes.map((pt,i)=>[pt,legacy.planets[i]||0])); 3035 } 3036 selectedSystemInfo=obj.system?.id?{id:Number(obj.system.id),name:String(obj.system.name||""),security:obj.system.security??null,planets:D.meta.planetTypes.map(type=>planetCounts[type]||0),planetDetails:normalizedPlanetDetails(obj.system.planetDetails),source:obj.system.source||"saved"}:null; 3037 renderPlanetCountForm(); 3038 $("systemInput").value=selectedSystemInfo?.name||""; 3039 current={system:selectedSystemInfo?{...selectedSystemInfo}:manualSystem()}; 3040 planetCountsDirty=false; 3041 globalPlanetSettings=obj.globalPlanetSettings&&typeof obj.globalPlanetSettings==="object" 3042 ? {...globalPlanetSettings,...obj.globalPlanetSettings} 3043 : {maxP0:99,p0p1:true,p1p2:true,p2p3:true,p3p4:true}; 3044 globalPlanetSettings.maxP0=99;globalPlanetSettings.p1p2=true;globalPlanetSettings.p2p3=true;globalPlanetSettings.p3p4=true; 3045 productionSettings=obj.productionSettings&&typeof obj.productionSettings==="object" 3046 ? normalizedProductionSettings(obj.productionSettings) 3047 : normalizedProductionSettings(); 3048 marketSettings=obj.marketSettings&&typeof obj.marketSettings==="object" 3049 ? {enabled:!!obj.marketSettings.enabled,hub:MARKET_HUBS[obj.marketSettings.hub]?obj.marketSettings.hub:"Jita",tax:clampPercent(obj.marketSettings.tax),broker:clampPercent(obj.marketSettings.broker),customsTax:obj.marketSettings.customsTax==null?2:clampPercent(obj.marketSettings.customsTax),hauling:clampMoney(obj.marketSettings.hauling)} 3050 : {enabled:false,hub:"Jita",tax:3.6,broker:0,customsTax:2,hauling:0}; 3051 planetSettings=obj.planetSettings&&typeof obj.planetSettings==="object"?obj.planetSettings:{}; 3052 storageSettings=normalizedStorageSettings(obj.storageSettings); 3053 planetTemplateSettings=normalizedPlanetTemplateSettings(obj.planetTemplateSettings); 3054 ensurePlanetSettings(); 3055 selected=(obj.selected||[]).map(Number).filter(id=>products.has(id)); 3056 targetColors=obj.targetColors&&typeof obj.targetColors==="object"?{...obj.targetColors}:{}; 3057 selected.forEach(ensureTargetColor); 3058 renderProductionControls(); 3059 $("empty").classList.add("hidden");$("result").classList.remove("hidden"); 3060 invalidateProfitabilityRanking();recalc(); 3061 if(marketSettings.enabled)loadMarketTrends(false); 3062 } 3063 autosave();
3063toast(shared&&!exactCloneMatch?"Ðлан загÑÑжен; ÐºÐ»Ð¾Ð½Ñ Ð·Ð°Ð¼ÐµÐ½ÐµÐ½Ñ Ð½Ð° Ðлон 1, Ðлон 2â¦":"ÐÑÐ¾ÐµÐºÑ Ð·Ð°Ð³ÑÑжен");return true; 3064 }catch(err){alert(translateSiteText("Ðе ÑдалоÑÑ Ð·Ð°Ð³ÑÑзиÑÑ Ð¿ÑоекÑ: "+err.message));return false} 3065} 3066$("projectFileInput").onchange=async e=>{ 3067 const file=e.target.files?.[0];if(!file)return; 3068 await applyProject(JSON.parse(await file.text())); 3069 e.target.value=""; 3070}; 3071 3072function autosave(){ 3073 try{ 3074 localStorage.setItem("eve_pi_autosave_v10",JSON.stringify(serializeProject(false))); 3075 autosaveWarningShown=false; 3076 }catch(err){ 3077 console.warn("PI Factory: авÑоÑÐ¾Ñ Ñанение недоÑÑÑпно",err); 3078 if(!autosaveWarningShown){ 3079 autosaveWarningShown=true; 3080 toast("ÐвÑоÑÐ¾Ñ Ñанение недоÑÑÑпно в ÑÑом бÑаÑзеÑе"); 3081 } 3082 } 3083} 3084 3085if($("langRuBtn")&&$("langEnBtn")){ 3086 $("langRuBtn").onclick=()=>setSiteLanguage("ru"); 3087 $("langEnBtn").onclick=()=>setSiteLanguage("en"); 3088 applySiteLanguage(); 3089 if(typeof MutationObserver!=="undefined"){ 3090 let translating=false; 3091 new MutationObserver(records=>{ 3092 if(translating)return; 3093 translating=true; 3094 for(const record of records)for(const node of record.addedNodes||[])if(node.nodeType===1||node.nodeType===3)applySiteLanguage(node.nodeType===3?node.parentNode:node); 3095 translating=false; 3096 }).observe(document.body,{childList:true,subtree:true}); 3097 } 3098} 3099 3100if($("guidedTourOpenBtn")&&$("guidedTour")){ 3101 $("guidedTourOpenBtn").onclick=()=>openGuidedTour(false); 3102 $("guidedTourPrev").onclick=()=>moveGuidedTour(-1); 3103 $("guidedTourNext").onclick=()=>moveGuidedTour(1); 3104 $("guidedTourChoiceSecondary").onclick=()=>handleGuidedTourChoice(false); 3105 $("guidedTourChoicePrimary").onclick=()=>handleGuidedTourChoice(true); 3106 $("guidedTourSkip").onclick=skipGuidedTour; 3107 $("guidedTourSkipTop").onclick=skipGuidedTour; 3108 if(typeof document.addEventListener==="function")document.addEventListener("keydown",event=>{ 3109 if($("guidedTour").classList.contains("hidden"))return; 3110 if(event.key==="Escape"){event.preventDefault?.();skipGuidedTour()} 3111 else if(event.key==="ArrowLeft"){event.preventDefault?.();moveGuidedTour(-1)} 3112 else if(event.key==="ArrowRight"){event.preventDefault?.();moveGuidedTour(1)} 3113 }); 3114 if(typeof window.addEventListener==="function"){ 3115 window.addEventListener("resize",positionGuidedTour); 3116 window.addEventListener("scroll",positionGuidedTour,{passive:true,capture:true}); 3117 let completed=false;try{completed=localStorage.getItem(GUIDED_TOUR_STORAGE_KEY)==="completed"}catch(_error){} 3118 document.body.classList.remove("guidedTourOpen"); 3119 $("guidedTour").classList.add("hidden"); 3120 try{if(!completed)localStorage.removeItem(GUIDED_TOUR_RESUME_KEY)}catch(_error){} 3121 } 3122} 3123 3124if($("piTemplatePreviewModal")){ 3125 $("piTemplatePreviewClose").onclick=closeTemplatePreview; 3126 $("piTemplatePreviewCancel").onclick=closeTemplatePreview; 3127 $("piTemplatePreviewCopy").onclick=()=>{ 3128 if(activeTemplatePreview)copyPlanetTemplateResult(activeTemplatePreview.planet,activeTemplatePreview.result); 3129 }; 3130 $("piTemplatePreviewModal").onclick=event=>{if(event.target===$("piTemplatePreviewModal"))closeTemplatePreview()}; 3131 if(typeof document.addEventListener==="function")document.addEventListener("keydown",event=>{ 3132 if(event.key==="Escape"&&!$("piTemplatePreviewModal").classList.contains("hidden"))closeTemplatePreview(); 3133 }); 3134} 3135loadLocalAccountData();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.