diff --git a/public/levels/deep-space-patrol.json b/public/levels/deep-space-patrol.json new file mode 100644 index 0000000..461d577 --- /dev/null +++ b/public/levels/deep-space-patrol.json @@ -0,0 +1,441 @@ +{ + "version": "1.0", + "difficulty": "captain", + "timestamp": "2025-11-11T23:44:24.810Z", + "metadata": { + "author": "System", + "description": "Patrol a dangerous sector with heavy asteroid activity. Watch your fuel!", + "estimatedTime": "8-12 minutes", + "type": "default" + }, + "ship": { + "position": [ + 0, + 1, + 0 + ], + "rotation": [ + 0, + 0, + 0 + ], + "linearVelocity": [ + 0, + 0, + 0 + ], + "angularVelocity": [ + 0, + 0, + 0 + ] + }, + "startBase": { + "position": [ + 0, + 0, + 0 + ], + "baseGlbPath": "base.glb" + }, + "sun": { + "position": [ + 0, + 0, + 400 + ], + "diameter": 50, + "intensity": 1000000 + }, + "planets": [], + "asteroids": [ + { + "id": "asteroid-0", + "position": [ + 166.3103777512652, + 225.37995981448225, + -23.260546252581893 + ], + "scale": 15.57675246095847, + "linearVelocity": [ + -15.247760404104447, + -20.571728072500164, + 2.1325863179651163 + ], + "angularVelocity": [ + 0.3175968011295134, + 0.41914092045057316, + 0.3490716681663488 + ] + }, + { + "id": "asteroid-1", + "position": [ + -249.03794410207183, + -44.17921063467387, + 62.65614175181777 + ], + "scale": 17.559290714405755, + "linearVelocity": [ + 28.21843468423197, + 5.1192464223731795, + -7.099553644182771 + ], + "angularVelocity": [ + -0.7119913444012966, + 0.7778624030643284, + 0.692030459502786 + ] + }, + { + "id": "asteroid-2", + "position": [ + -75.13363399135183, + 316.85667650092637, + -310.96487929135486 + ], + "scale": 31.622342902375273, + "linearVelocity": [ + 4.725067636041931, + -19.86386230079167, + 19.556222812999994 + ], + "angularVelocity": [ + 0.5519570317343065, + 0.6149078850079825, + -0.891202682633375 + ] + }, + { + "id": "asteroid-3", + "position": [ + 37.69090411493977, + 258.7939790070069, + -296.8568452906189 + ], + "scale": 26.5394417569339, + "linearVelocity": [ + -2.5867033769995693, + -17.692240919295507, + 20.373101209167153 + ], + "angularVelocity": [ + -0.7785494324348137, + -0.20192027466159823, + -0.9607478385112791 + ] + }, + { + "id": "asteroid-4", + "position": [ + -221.43907625779156, + 174.13268502069522, + -28.083248212763042 + ], + "scale": 25.9507959425748, + "linearVelocity": [ + 24.966800076249847, + -19.5203538897708, + 3.166328434298123 + ], + "angularVelocity": [ + 0.12619252306439144, + -0.3248913262817572, + 0.81227377216443 + ] + }, + { + "id": "asteroid-5", + "position": [ + -259.9477876700893, + -254.62227443891425, + 11.496494773295924 + ], + "scale": 22.751714697755023, + "linearVelocity": [ + 12.927816475558101, + 12.712698502381889, + -0.5717477954842545 + ], + "angularVelocity": [ + -0.35481099529697646, + -0.6316024099485169, + -0.8870507592544725 + ] + }, + { + "id": "asteroid-6", + "position": [ + 161.9658398928775, + -27.636039784069954, + 264.68815118855474 + ], + "scale": 31.487052543025985, + "linearVelocity": [ + -14.659086384593829, + 2.5917698521181003, + -23.956202590729646 + ], + "angularVelocity": [ + 0.3478963338932801, + 0.02204602907153763, + 0.10920873627447891 + ] + }, + { + "id": "asteroid-7", + "position": [ + -284.20649039890117, + -272.4658003612411, + -164.57014594288472 + ], + "scale": 38.73582873730346, + "linearVelocity": [ + 22.435184300258108, + 21.58731710282488, + 12.99112328281743 + ], + "angularVelocity": [ + 0.06533995393293157, + -0.8892051337798605, + -0.3186200100290164 + ] + }, + { + "id": "asteroid-8", + "position": [ + -161.48900437690028, + -244.51884642800135, + -63.50169475507713 + ], + "scale": 9.793580529897, + "linearVelocity": [ + 19.05949281156754, + 28.97698643108488, + 7.494690424135613 + ], + "angularVelocity": [ + 0.026878678094339747, + 0.6390878354060479, + -0.4576976077246311 + ] + }, + { + "id": "asteroid-9", + "position": [ + -179.37511378444728, + 275.18039672598127, + 127.15174115989014 + ], + "scale": 12.201546220635752, + "linearVelocity": [ + 11.535052657633953, + -17.631683945442504, + -8.17673086775957 + ], + "angularVelocity": [ + -0.9276174339399885, + -0.7940475172876842, + -0.519891218823211 + ] + }, + { + "id": "asteroid-10", + "position": [ + -161.85346730070253, + -351.9583610917342, + -211.00495094996367 + ], + "scale": 28.520265023931636, + "linearVelocity": [ + 10.316203045109077, + 22.496831116545703, + 13.449016285075643 + ], + "angularVelocity": [ + -0.12498943826987396, + 0.19710933568597477, + 0.6942780482525945 + ] + }, + { + "id": "asteroid-11", + "position": [ + 238.15124667512623, + -336.0404556200413, + -137.62425812333427 + ], + "scale": 36.57155193341867, + "linearVelocity": [ + -13.074585061842177, + 18.503636524303484, + 7.555618937662253 + ], + "angularVelocity": [ + 0.8666046519504111, + 0.17700461662165567, + 0.7935034314479288 + ] + }, + { + "id": "asteroid-12", + "position": [ + 196.63832555316407, + -52.5552884713219, + 360.704951820807 + ], + "scale": 37.89225310776497, + "linearVelocity": [ + -14.659857963374991, + 3.9926749781282407, + -26.89141776153598 + ], + "angularVelocity": [ + -0.9987270161689143, + -0.1748693297130952, + 0.8516444011722366 + ] + }, + { + "id": "asteroid-13", + "position": [ + -83.92561949303223, + -253.3043114432265, + -1.616443680779213 + ], + "scale": 32.821087460334866, + "linearVelocity": [ + 10.323698427408104, + 31.281997511469118, + 0.19883889074705718 + ], + "angularVelocity": [ + 0.632854416028195, + 0.029291754806906045, + 0.40775129810052313 + ] + }, + { + "id": "asteroid-14", + "position": [ + 84.70991492450895, + -283.3171370603572, + -5.226290049567284 + ], + "scale": 24.384062404096834, + "linearVelocity": [ + -9.840583709194428, + 33.02856093875565, + 0.6071278051350835 + ], + "angularVelocity": [ + 0.9698449632732298, + 0.15375578935014778, + -0.3922269025131073 + ] + }, + { + "id": "asteroid-15", + "position": [ + -260.77067504513934, + 201.3472149603708, + 140.6455294494111 + ], + "scale": 7.723773988992447, + "linearVelocity": [ + 20.882499051701014, + -16.0437922158873, + -11.262885041981486 + ], + "angularVelocity": [ + 0.3799403198861504, + -0.4285539548267927, + 0.4547601376586794 + ] + }, + { + "id": "asteroid-16", + "position": [ + 88.29502639093482, + 39.73694881770429, + -429.03785854398495 + ], + "scale": 9.258837908114785, + "linearVelocity": [ + -4.590007145936655, + -2.0137359843781644, + 22.303485452000903 + ], + "angularVelocity": [ + -0.6590987578049781, + 0.866939902569952, + -0.3034435245285141 + ] + }, + { + "id": "asteroid-17", + "position": [ + 211.63076567269354, + -86.40698323508542, + -182.38745904183477 + ], + "scale": 10.822556168662619, + "linearVelocity": [ + -15.182311988861233, + 6.270544290959188, + 13.084408106860023 + ], + "angularVelocity": [ + -0.916175355667403, + 0.16026156077498221, + 0.7296019227737922 + ] + }, + { + "id": "asteroid-18", + "position": [ + 109.29137029559803, + -225.1245616977874, + 37.07836905330716 + ], + "scale": 6.601394757786682, + "linearVelocity": [ + -9.672107524407286, + 20.01165388205001, + -3.2813750193018394 + ], + "angularVelocity": [ + -0.6708090024867568, + -0.17052400155852965, + -0.7102991593042125 + ] + }, + { + "id": "asteroid-19", + "position": [ + 102.93248991332028, + 101.20067906285311, + -277.6954524087843 + ], + "scale": 5.224189195445002, + "linearVelocity": [ + -7.252212644577497, + -7.059740149167784, + 19.565313857622314 + ], + "angularVelocity": [ + -0.43523205758445815, + -0.7478682237907837, + -0.6354712781926715 + ] + } + ], + "difficultyConfig": { + "rockCount": 20, + "forceMultiplier": 1.2, + "rockSizeMin": 5, + "rockSizeMax": 40, + "distanceMin": 230, + "distanceMax": 450 + } +} \ No newline at end of file diff --git a/public/levels/directory.json b/public/levels/directory.json index 837f7fd..9fa1be9 100644 --- a/public/levels/directory.json +++ b/public/levels/directory.json @@ -1,30 +1,120 @@ -{"missions": [ - { - "id": 1, - "name": "Recruit", - "Description": "Simple level to get the hang of things", - "missionbrief": [ - "Destroy the asteroids", - "return to base after they're destroyed to complete the mission", - "return to base if you need more fuel, ammo, or hull repairs", - "don't get too far from base, if you run out of fuel, you'll be stranded", - "don't run into things, it damages your hull" - ], - "leveldata": "/levels/1.json", - "defaultlocked": false - } , - { - "id": 2, - "name": "Fuel Management", - "Description": "Don't run out of fuel", - "missionbrief": [ - "Astroids are further away and there a more of them", - "you'll need to keep an eye on your fuel levels", - "return to base after you've destroyed them all" - ], - "leveldata": null, - "defaultlocked": true - } -] - -} \ No newline at end of file +{ + "version": "1.0.5", + "levels": [ + { + "id": "rookie-training", + "name": "Rookie Training", + "description": "Simple level to get the hang of things", + "version": "1.0", + "levelPath": "rookie-training.json", + "difficulty": "recruit", + "estimatedTime": "3-5 minutes", + "missionBrief": [ + "Destroy the asteroids", + "Return to base after they're destroyed to complete the mission", + "Return to base if you need more fuel, ammo, or hull repairs", + "Don't get too far from base, if you run out of fuel, you'll be stranded", + "Don't run into things, it damages your hull" + ], + "unlockRequirements": [], + "tags": ["tutorial", "easy"], + "defaultLocked": false + }, + { + "id": "rescue-mission", + "name": "Rescue Mission", + "description": "Rescue operation in moderate asteroid field", + "version": "1.0", + "levelPath": "rescue-mission.json", + "difficulty": "pilot", + "estimatedTime": "5-8 minutes", + "missionBrief": [ + "More asteroids and increased difficulty", + "Manage your fuel and ammunition carefully", + "Complete the mission and return to base", + "Use your radar to track asteroids", + "Watch your shield strength" + ], + "unlockRequirements": ["rookie-training"], + "tags": ["medium"], + "defaultLocked": true + }, + { + "id": "deep-space-patrol", + "name": "Deep Space Patrol", + "description": "Extended patrol mission in dangerous territory", + "version": "1.0", + "levelPath": "deep-space-patrol.json", + "difficulty": "captain", + "estimatedTime": "8-12 minutes", + "missionBrief": [ + "Large asteroid field requiring careful navigation", + "Fuel management is critical at this distance", + "Return to base for resupply as needed", + "Asteroids are faster and more dangerous", + "Plan your route carefully" + ], + "unlockRequirements": ["rescue-mission"], + "tags": ["hard"], + "defaultLocked": true + }, + { + "id": "enemy-territory", + "name": "Enemy Territory", + "description": "Hazardous mission in hostile space", + "version": "1.0", + "levelPath": "enemy-territory.json", + "difficulty": "commander", + "estimatedTime": "10-15 minutes", + "missionBrief": [ + "Heavily defended asteroid field", + "Maximum asteroid count and speed", + "Expert fuel and ammunition management required", + "Multiple base trips may be necessary", + "Test your skills to the limit" + ], + "unlockRequirements": ["deep-space-patrol"], + "tags": ["very-hard", "combat"], + "defaultLocked": true + }, + { + "id": "the-gauntlet", + "name": "The Gauntlet", + "description": "Survive the ultimate asteroid gauntlet", + "version": "1.0", + "levelPath": "the-gauntlet.json", + "difficulty": "commander", + "estimatedTime": "12-18 minutes", + "missionBrief": [ + "Dense asteroid field with extreme hazards", + "High-speed rocks from all directions", + "This is a test of endurance and skill", + "Only the best pilots succeed", + "May the stars guide you" + ], + "unlockRequirements": ["enemy-territory"], + "tags": ["very-hard", "endurance"], + "defaultLocked": true + }, + { + "id": "final-challenge", + "name": "Final Challenge", + "description": "The ultimate test for master pilots", + "version": "1.0", + "levelPath": "final-challenge.json", + "difficulty": "commander", + "estimatedTime": "15-20 minutes", + "missionBrief": [ + "The pinnacle of difficulty", + "Everything you've learned will be tested", + "Maximum asteroid count and velocity", + "Precision flying and resource management essential", + "Complete this to prove your mastery", + "Good luck, Commander" + ], + "unlockRequirements": ["the-gauntlet"], + "tags": ["extreme", "final-boss"], + "defaultLocked": true + } + ] +} diff --git a/public/levels/enemy-territory.json b/public/levels/enemy-territory.json new file mode 100644 index 0000000..829770f --- /dev/null +++ b/public/levels/enemy-territory.json @@ -0,0 +1,1011 @@ +{ + "version": "1.0", + "difficulty": "commander", + "timestamp": "2025-11-11T23:44:24.811Z", + "metadata": { + "author": "System", + "description": "Navigate through hostile space with high-speed asteroids and limited resources.", + "estimatedTime": "10-15 minutes", + "type": "default" + }, + "ship": { + "position": [ + 0, + 1, + 0 + ], + "rotation": [ + 0, + 0, + 0 + ], + "linearVelocity": [ + 0, + 0, + 0 + ], + "angularVelocity": [ + 0, + 0, + 0 + ] + }, + "startBase": { + "position": [ + 0, + 0, + 0 + ], + "baseGlbPath": "base.glb" + }, + "sun": { + "position": [ + 0, + 0, + 400 + ], + "diameter": 50, + "intensity": 1000000 + }, + "planets": [], + "asteroids": [ + { + "id": "asteroid-0", + "position": [ + -55.77595863866099, + -60.516937558451644, + -101.10676378079069 + ], + "scale": 6.78524105501735, + "linearVelocity": [ + 9.957817407488125, + 10.982768322162109, + 18.05083618398419 + ], + "angularVelocity": [ + -0.5965819832055597, + 0.49764293312500696, + -0.23489500768751137 + ] + }, + { + "id": "asteroid-1", + "position": [ + 51.01366656736156, + 61.89083382225282, + -140.23316787747007 + ], + "scale": 7.375114439254016, + "linearVelocity": [ + -8.803899321799005, + -10.508493246288012, + 24.20133220457591 + ], + "angularVelocity": [ + 0.808562143760696, + 0.892530315002241, + 0.3166286328627703 + ] + }, + { + "id": "asteroid-2", + "position": [ + -89.27497643869671, + -96.70426975205562, + 75.94393244559606 + ], + "scale": 6.561114427119078, + "linearVelocity": [ + 14.389417639263389, + 15.748058399847189, + -12.240708479806575 + ], + "angularVelocity": [ + -0.6727262347775067, + -0.45885844690986977, + 0.552666584138167 + ] + }, + { + "id": "asteroid-3", + "position": [ + -25.37856560403051, + -100.51083367976476, + -6.739363736000233 + ], + "scale": 4.9465969460962835, + "linearVelocity": [ + 8.87451897700385, + 35.496876928280116, + 2.3566584613658033 + ], + "angularVelocity": [ + 0.41251734660501693, + -0.8931037557961039, + 0.4850034783280015 + ] + }, + { + "id": "asteroid-4", + "position": [ + -26.989345064641896, + -85.27489498304212, + -188.91293387811623 + ], + "scale": 6.124075184348532, + "linearVelocity": [ + 4.500266822056823, + 14.385678738728325, + 31.499786547353967 + ], + "angularVelocity": [ + 0.2273175647575063, + -0.558081045771968, + 0.6888867594728239 + ] + }, + { + "id": "asteroid-5", + "position": [ + -115.02553443028832, + 79.71278214944606, + -108.48885721755033 + ], + "scale": 7.105706173930612, + "linearVelocity": [ + 21.213862611510503, + -14.516795375552855, + 20.008320094245736 + ], + "angularVelocity": [ + 0.24455593269979614, + 0.5225958731494473, + -0.9505917169340652 + ] + }, + { + "id": "asteroid-6", + "position": [ + -58.73254228240268, + 97.33604894977107, + 44.80612177977446 + ], + "scale": 6.192900812956161, + "linearVelocity": [ + 13.77476533702421, + -22.59405801639833, + -10.508549250445865 + ], + "angularVelocity": [ + -0.7251515649183213, + 0.3639719415853704, + 0.3068612022321786 + ] + }, + { + "id": "asteroid-7", + "position": [ + -70.14667472965473, + -65.94017376177376, + 22.986547856821872 + ], + "scale": 3.4112336692142384, + "linearVelocity": [ + 24.747687153438573, + 23.616436340536257, + -8.10963452352621 + ], + "angularVelocity": [ + 0.7613217987150223, + 0.11150995152395415, + 0.14568352473357216 + ] + }, + { + "id": "asteroid-8", + "position": [ + 79.2290471781825, + -143.9244337160095, + -15.954683414183584 + ], + "scale": 6.46375274913997, + "linearVelocity": [ + -10.55858200565614, + 19.313579709881175, + 2.1262256609508765 + ], + "angularVelocity": [ + 0.6522444033491315, + -0.03232919151178004, + 0.4219260501045876 + ] + }, + { + "id": "asteroid-9", + "position": [ + 116.13499636175685, + 95.47973669791692, + 220.8617737797589 + ], + "scale": 2.501874975790654, + "linearVelocity": [ + -16.085623431031888, + -13.086197218723925, + -30.591117532431028 + ], + "angularVelocity": [ + 0.6031234836811801, + 0.5684191890733206, + -0.9904258206536913 + ] + }, + { + "id": "asteroid-10", + "position": [ + 63.57764356124894, + 53.77881563585659, + -58.567256433949154 + ], + "scale": 2.23322286614361, + "linearVelocity": [ + -13.633297129650531, + -11.317646194632621, + 12.558892785374262 + ], + "angularVelocity": [ + -0.4944078136639898, + -0.9584237700533875, + 0.47027253530042534 + ] + }, + { + "id": "asteroid-11", + "position": [ + 102.27037541868664, + 15.560392453883118, + -81.51315729604673 + ], + "scale": 7.75860618723746, + "linearVelocity": [ + -18.19446375780478, + -2.5903741109488996, + 14.501640187938293 + ], + "angularVelocity": [ + 0.9605188929395894, + -0.6334777108349012, + 0.525904915560369 + ] + }, + { + "id": "asteroid-12", + "position": [ + 115.21102333931695, + -78.2543653246074, + 46.9710092970659 + ], + "scale": 6.649739604058236, + "linearVelocity": [ + -23.054955749256447, + 15.859644611547127, + -9.399400417201324 + ], + "angularVelocity": [ + -0.8894416579885833, + -0.275711818504079, + -0.20094485130940143 + ] + }, + { + "id": "asteroid-13", + "position": [ + 85.45260567087442, + -171.56840934311765, + -43.995359799249194 + ], + "scale": 5.027088485841676, + "linearVelocity": [ + -16.665751938392873, + 33.65587602550644, + 8.580379113056557 + ], + "angularVelocity": [ + 0.5791200871773885, + 0.9948458194569811, + 0.6752235620302218 + ] + }, + { + "id": "asteroid-14", + "position": [ + -201.6451256292486, + -116.83464574331083, + -45.56327068442538 + ], + "scale": 6.847256970480096, + "linearVelocity": [ + 32.40905988593899, + 18.938767196162928, + 7.32307693331418 + ], + "angularVelocity": [ + 0.4489158407801095, + -0.30785248430337786, + 0.06966719871324889 + ] + }, + { + "id": "asteroid-15", + "position": [ + 85.37928578884734, + -32.329837349443885, + 17.588472801747493 + ], + "scale": 5.67030120172992, + "linearVelocity": [ + -30.49724091488736, + 11.905324223651178, + -6.282553050237677 + ], + "angularVelocity": [ + 0.43098798216538814, + 0.6791524480822151, + -0.20345735159289857 + ] + }, + { + "id": "asteroid-16", + "position": [ + 41.62347705955282, + 26.826253110998344, + -130.2721310353636 + ], + "scale": 2.141873334996801, + "linearVelocity": [ + -7.933668230199513, + -4.922628725098157, + 24.830598986166386 + ], + "angularVelocity": [ + 0.33962081485859796, + -0.30979450103903305, + 0.8301360843526173 + ] + }, + { + "id": "asteroid-17", + "position": [ + -141.98260412068652, + -199.72384481237484, + 11.591066713457069 + ], + "scale": 3.3776179066595953, + "linearVelocity": [ + 16.48437146006362, + 23.30430857548461, + -1.345738448074897 + ], + "angularVelocity": [ + -0.5290118589487647, + 0.32440659674099415, + -0.3326984041883576 + ] + }, + { + "id": "asteroid-18", + "position": [ + 37.5577466655546, + -109.58099721777445, + 52.26126229755586 + ], + "scale": 5.650170406789071, + "linearVelocity": [ + -7.664970032722214, + 22.56791488079716, + -10.665735965208647 + ], + "angularVelocity": [ + -0.7569816537054126, + 0.9991120604875028, + -0.6590140356101544 + ] + }, + { + "id": "asteroid-19", + "position": [ + -182.03520027071275, + 4.919397511425062, + -62.68190578694546 + ], + "scale": 6.0476532751015535, + "linearVelocity": [ + 33.12760279196319, + -0.7132699815706369, + 11.407141443331453 + ], + "angularVelocity": [ + -0.4716081293253884, + -0.041093359645206995, + -0.15360347434194122 + ] + }, + { + "id": "asteroid-20", + "position": [ + -45.93737485154738, + 70.62468789022887, + -38.38854626942504 + ], + "scale": 4.83109249387863, + "linearVelocity": [ + 13.341474419928998, + -20.220920230746255, + 11.149087420143319 + ], + "angularVelocity": [ + 0.5085973514099549, + 0.9174908198903449, + 0.4323552877124266 + ] + }, + { + "id": "asteroid-21", + "position": [ + -101.01374941001455, + -7.891582023150901, + 84.52275167436615 + ], + "scale": 6.494665864860442, + "linearVelocity": [ + 16.967425712184347, + 1.4935319035553953, + -14.19740895076638 + ], + "angularVelocity": [ + -0.39896802586636504, + -0.265442324858677, + -0.8527977464366554 + ] + }, + { + "id": "asteroid-22", + "position": [ + 128.87031443740625, + 33.53313840278379, + -99.86787765355972 + ], + "scale": 2.5863911862892928, + "linearVelocity": [ + -21.735237593888876, + -5.487031640656312, + 16.843693276249866 + ], + "angularVelocity": [ + -0.6122643121320746, + -0.05713552881377337, + 0.9023811170464904 + ] + }, + { + "id": "asteroid-23", + "position": [ + -62.55555474511086, + 231.536066116945, + 138.13143289771665 + ], + "scale": 2.6506038643628553, + "linearVelocity": [ + 6.852421096849546, + -25.253236255057306, + -15.131106242812898 + ], + "angularVelocity": [ + 0.47920660141212146, + 0.9382700413655973, + 0.6663190275659825 + ] + }, + { + "id": "asteroid-24", + "position": [ + 84.02473043661874, + 188.40365123130843, + 33.46175822498513 + ], + "scale": 7.511520298147304, + "linearVelocity": [ + -14.701639630115286, + -32.78964337587202, + -5.854737150092474 + ], + "angularVelocity": [ + -0.8314227729505954, + -0.0915063059645953, + 0.7822650726703664 + ] + }, + { + "id": "asteroid-25", + "position": [ + 93.62899884304426, + 30.64398267119048, + -27.13598292639072 + ], + "scale": 4.2809113321902394, + "linearVelocity": [ + -32.387597811753444, + -10.254273784370728, + 9.386721123867305 + ], + "angularVelocity": [ + -0.28643370726918205, + 0.3824954378342258, + -0.43187555381326925 + ] + }, + { + "id": "asteroid-26", + "position": [ + 8.305881808224829, + 148.22439744797802, + 105.94547474365258 + ], + "scale": 7.742627574483438, + "linearVelocity": [ + -1.6628439505666388, + -29.474438033751763, + -21.210365838933765 + ], + "angularVelocity": [ + 0.9547910262285431, + 0.35016764314831983, + -0.4237510867880898 + ] + }, + { + "id": "asteroid-27", + "position": [ + 157.09767645144422, + 108.13840082559065, + 42.0355485743682 + ], + "scale": 3.2424644031887904, + "linearVelocity": [ + -25.967071630662144, + -17.709176809451968, + -6.948161968532809 + ], + "angularVelocity": [ + 0.7036903589388861, + -0.06330036051990673, + -0.17475872827856298 + ] + }, + { + "id": "asteroid-28", + "position": [ + 43.91984051242634, + 3.246196407818413, + -236.95247999695144 + ], + "scale": 5.288565113501959, + "linearVelocity": [ + -4.802193030920653, + -0.24559899603125127, + 25.908371588434598 + ], + "angularVelocity": [ + 0.33623325704835594, + 0.8036587426025465, + 0.22854371423897568 + ] + }, + { + "id": "asteroid-29", + "position": [ + -103.00319864544981, + -22.148739180345533, + -70.13018614417182 + ], + "scale": 3.9335775528761565, + "linearVelocity": [ + 28.654511179258094, + 6.439759292448371, + 19.509551444016196 + ], + "angularVelocity": [ + -0.7856420204712635, + -0.46529446366424, + -0.8620752555779472 + ] + }, + { + "id": "asteroid-30", + "position": [ + 63.25704678708654, + -186.0084908966402, + 5.320347449668389 + ], + "scale": 4.72275715193363, + "linearVelocity": [ + -11.036851265134036, + 32.62853712236117, + -0.9282741838781579 + ], + "angularVelocity": [ + -0.8154893741817122, + -0.8473592397801126, + 0.02410829719988472 + ] + }, + { + "id": "asteroid-31", + "position": [ + 121.59110902309624, + 25.96647392161014, + 23.13855160633822 + ], + "scale": 7.334708642788891, + "linearVelocity": [ + -20.84177070876138, + -4.279470177228415, + -3.96614843788077 + ], + "angularVelocity": [ + -0.29452403825860785, + 0.11607308006521144, + 0.016418776053258366 + ] + }, + { + "id": "asteroid-32", + "position": [ + 79.40322114752394, + 151.25146721457682, + 124.98690191326033 + ], + "scale": 5.057867707702108, + "linearVelocity": [ + -9.994536500426532, + -18.912252571324764, + -15.732184855907006 + ], + "angularVelocity": [ + -0.10387762232765763, + -0.24983303420117142, + 0.505433755007731 + ] + }, + { + "id": "asteroid-33", + "position": [ + 75.05841348453647, + 98.86489755496987, + -251.33834436884416 + ], + "scale": 3.112375640080296, + "linearVelocity": [ + -7.504278546403557, + -9.784452096353522, + 25.12860128497498 + ], + "angularVelocity": [ + -0.6970085140531799, + 0.6944302481856859, + -0.35779221719403775 + ] + }, + { + "id": "asteroid-34", + "position": [ + 62.12618790923734, + -3.538937829080571, + -68.15533351103707 + ], + "scale": 3.9560817084240965, + "linearVelocity": [ + -19.23558851230661, + 1.4053516447957435, + 21.10234016374278 + ], + "angularVelocity": [ + -0.5473913868969675, + 0.5730823351977983, + 0.34838756558874984 + ] + }, + { + "id": "asteroid-35", + "position": [ + 116.1756732408044, + 55.691601789262165, + -83.00693976257432 + ], + "scale": 5.790391786535567, + "linearVelocity": [ + -22.115687740050898, + -10.411322383021089, + 15.801548713569543 + ], + "angularVelocity": [ + -0.4347303882306277, + 0.9261595614190203, + -0.020272311109515773 + ] + }, + { + "id": "asteroid-36", + "position": [ + 202.65222744292402, + -69.19813058340729, + -97.67436596402304 + ], + "scale": 2.919604775164769, + "linearVelocity": [ + -28.55002937763528, + 9.889645505997791, + 13.760549552805454 + ], + "angularVelocity": [ + 0.624846304863, + -0.6980451694656935, + 0.262148924939285 + ] + }, + { + "id": "asteroid-37", + "position": [ + 21.052291531932077, + 253.14294794744347, + 8.435171460322845 + ], + "scale": 3.235352100231171, + "linearVelocity": [ + -2.802520592417765, + -33.56574284484931, + -1.1229058690486347 + ], + "angularVelocity": [ + 0.7128661478854434, + 0.27389162999399863, + -0.49617905141764984 + ] + }, + { + "id": "asteroid-38", + "position": [ + 106.13496487476972, + 37.70879019675215, + 87.53590556500609 + ], + "scale": 5.303624643451116, + "linearVelocity": [ + -23.64338936973513, + -8.17751455364214, + -19.500128930629124 + ], + "angularVelocity": [ + -0.8869435462374473, + -0.2811150680625407, + 0.9565202371000203 + ] + }, + { + "id": "asteroid-39", + "position": [ + -102.52343593182007, + 135.73338350636757, + -166.9420870067364 + ], + "scale": 3.81912041262033, + "linearVelocity": [ + 14.061297192085606, + -18.478956835178383, + 22.896445850974125 + ], + "angularVelocity": [ + 0.33027822875958757, + 0.23102989400352536, + -0.3494466798669773 + ] + }, + { + "id": "asteroid-40", + "position": [ + 162.11131191959453, + -189.4249913585423, + 113.89363719819275 + ], + "scale": 2.431958227282983, + "linearVelocity": [ + -21.994376578171963, + 25.83582182045546, + -15.452466066261982 + ], + "angularVelocity": [ + 0.3043681065855095, + -0.19744749997776978, + 0.32696972189941675 + ] + }, + { + "id": "asteroid-41", + "position": [ + 129.11775008401733, + -52.86681458051011, + 64.15884326963163 + ], + "scale": 3.0433763109851872, + "linearVelocity": [ + -24.665472195546855, + 10.290222811609501, + -12.256317692461373 + ], + "angularVelocity": [ + -0.9503568009196837, + -0.31292037634038916, + 0.004492000893457693 + ] + }, + { + "id": "asteroid-42", + "position": [ + 185.05967289629763, + -81.48079487150534, + -1.8481013399568662 + ], + "scale": 3.020662613857349, + "linearVelocity": [ + -26.03407271399907, + 11.603343816545628, + 0.2599896774606195 + ], + "angularVelocity": [ + 0.908750453881332, + -0.2909648784959349, + 0.1454952322511609 + ] + }, + { + "id": "asteroid-43", + "position": [ + 45.60892236115607, + -154.08833936812889, + 176.98372346031434 + ], + "scale": 6.9260118746472825, + "linearVelocity": [ + -4.9210202803514, + 16.733411441586135, + -19.095835800366732 + ], + "angularVelocity": [ + 0.37980155760189005, + -0.5598500116742731, + -0.48253708056293254 + ] + }, + { + "id": "asteroid-44", + "position": [ + 14.104425185299734, + -22.854499660131754, + 152.97105629804378 + ], + "scale": 5.755154864775539, + "linearVelocity": [ + -2.585963326604952, + 4.373582084004659, + -28.046342649330466 + ], + "angularVelocity": [ + -0.7069096681256339, + -0.851126848962172, + 0.7727105211435448 + ] + }, + { + "id": "asteroid-45", + "position": [ + -85.92473911003765, + 230.09822167036307, + -37.212595160012135 + ], + "scale": 6.9599025866429445, + "linearVelocity": [ + 10.854424842547834, + -28.940785324907925, + 4.700873363644876 + ], + "angularVelocity": [ + 0.6396974549193719, + -0.744270463323927, + -0.4830902335806382 + ] + }, + { + "id": "asteroid-46", + "position": [ + 57.07524853608118, + 48.604842189265014, + 73.81865497533155 + ], + "scale": 4.079208065005364, + "linearVelocity": [ + -17.51913027538697, + -14.612208504457865, + -22.658484482097897 + ], + "angularVelocity": [ + 0.2943588150897254, + 0.6259462420689661, + -0.8410519641661378 + ] + }, + { + "id": "asteroid-47", + "position": [ + -22.974608684732146, + -34.886924563110306, + -135.18860401872973 + ], + "scale": 3.1168349533124733, + "linearVelocity": [ + 4.72635807604397, + 7.382691825604627, + 27.81112658591873 + ], + "angularVelocity": [ + 0.0817987574839929, + 0.6101801942102383, + 0.01853150710343643 + ] + }, + { + "id": "asteroid-48", + "position": [ + 89.52661935896718, + -60.95315410138144, + -179.05527858955801 + ], + "scale": 5.653201692959181, + "linearVelocity": [ + -15.187454150600228, + 10.509842705297656, + 30.37525434863282 + ], + "angularVelocity": [ + -0.864004053155587, + -0.40368710501753124, + -0.3158807655172895 + ] + }, + { + "id": "asteroid-49", + "position": [ + -50.82991621619616, + 27.639926942831284, + -87.79511819204637 + ], + "scale": 5.977674242247547, + "linearVelocity": [ + 14.856977108781319, + -7.7865323067980805, + 25.661463924002415 + ], + "angularVelocity": [ + 0.4391256607855465, + -0.6970641496209811, + -0.7867959684450998 + ] + } + ], + "difficultyConfig": { + "rockCount": 50, + "forceMultiplier": 1.3, + "rockSizeMin": 2, + "rockSizeMax": 8, + "distanceMin": 90, + "distanceMax": 280 + } +} \ No newline at end of file diff --git a/public/levels/final-challenge.json b/public/levels/final-challenge.json new file mode 100644 index 0000000..c772331 --- /dev/null +++ b/public/levels/final-challenge.json @@ -0,0 +1,1011 @@ +{ + "version": "1.0", + "difficulty": "commander", + "timestamp": "2025-11-11T23:44:24.811Z", + "metadata": { + "author": "System", + "description": "The ultimate challenge - survive the most chaotic asteroid field in known space.", + "estimatedTime": "15-20 minutes", + "type": "default" + }, + "ship": { + "position": [ + 0, + 1, + 0 + ], + "rotation": [ + 0, + 0, + 0 + ], + "linearVelocity": [ + 0, + 0, + 0 + ], + "angularVelocity": [ + 0, + 0, + 0 + ] + }, + "startBase": { + "position": [ + 0, + 0, + 0 + ], + "baseGlbPath": "base.glb" + }, + "sun": { + "position": [ + 0, + 0, + 400 + ], + "diameter": 50, + "intensity": 1000000 + }, + "planets": [], + "asteroids": [ + { + "id": "asteroid-0", + "position": [ + 197.09643531484056, + -89.14825393594855, + 11.90585373695789 + ], + "scale": 4.399137466451847, + "linearVelocity": [ + -27.345365975177263, + 12.507263218454119, + -1.6518306237450822 + ], + "angularVelocity": [ + -0.390204516191091, + -0.4067233664615002, + 0.31701008736288694 + ] + }, + { + "id": "asteroid-1", + "position": [ + -22.05086996253851, + 100.71375746574074, + -35.986743979220996 + ], + "scale": 2.7285804676767182, + "linearVelocity": [ + 5.679764320686774, + -25.683823037262613, + 9.269304332124353 + ], + "angularVelocity": [ + -0.042647589257244434, + -0.7204062422706432, + 0.12072201649700487 + ] + }, + { + "id": "asteroid-2", + "position": [ + 123.09367193100991, + 207.77898985632189, + -48.587319633658524 + ], + "scale": 3.6786822845012197, + "linearVelocity": [ + -18.89527797242572, + -31.741245759428086, + 7.458311187009071 + ], + "angularVelocity": [ + 0.9705973464285913, + 0.14656317258354257, + 0.9992336194001412 + ] + }, + { + "id": "asteroid-3", + "position": [ + 89.30898974878112, + 35.33109524911992, + -145.06733223913844 + ], + "scale": 6.888469487451312, + "linearVelocity": [ + -18.942753943646355, + -7.281747243466502, + 30.769296434737694 + ], + "angularVelocity": [ + -0.7629112776527025, + -0.5164361592877427, + -0.6441752778255809 + ] + }, + { + "id": "asteroid-4", + "position": [ + -45.122077578065706, + -140.1439494162342, + 124.92788820472445 + ], + "scale": 4.989179719913447, + "linearVelocity": [ + 8.346728185021815, + 26.108952512205615, + -23.10928887903233 + ], + "angularVelocity": [ + -0.7600978095375162, + 0.39730655658985103, + -0.37169385327458615 + ] + }, + { + "id": "asteroid-5", + "position": [ + 87.16194160068348, + 49.092397548337736, + -23.21867870490947 + ], + "scale": 5.356999152235866, + "linearVelocity": [ + -28.270340919201853, + -15.598418866596635, + 7.530809326028778 + ], + "angularVelocity": [ + 0.21358672453298722, + -0.9654860082110677, + -0.23635578293983261 + ] + }, + { + "id": "asteroid-6", + "position": [ + -60.18157877867517, + 105.61586849032237, + 59.2489634834698 + ], + "scale": 6.992509606849942, + "linearVelocity": [ + 16.429273031785396, + -28.559614117233657, + -16.17467367381745 + ], + "angularVelocity": [ + 0.8004364545225435, + -0.5509051691452647, + -0.23307281998303386 + ] + }, + { + "id": "asteroid-7", + "position": [ + 36.490759178202616, + -113.748633937046, + 66.2611424572859 + ], + "scale": 2.8629615192038846, + "linearVelocity": [ + -6.231308699936364, + 19.59493792567753, + -11.315019000987139 + ], + "angularVelocity": [ + -0.5652546515607755, + 0.08872760169536509, + -0.3817041033367037 + ] + }, + { + "id": "asteroid-8", + "position": [ + 151.01209178579262, + -64.05827189612683, + 92.46272300524083 + ], + "scale": 3.865000338605417, + "linearVelocity": [ + -24.211832685550245, + 10.430820309372425, + -14.8245875716284 + ], + "angularVelocity": [ + -0.752357515326953, + -0.6879306535034426, + 0.4624804290426705 + ] + }, + { + "id": "asteroid-9", + "position": [ + 141.36045416327275, + -91.48148253402032, + -85.116669110041 + ], + "scale": 6.115244214669208, + "linearVelocity": [ + -21.651413501756007, + 14.164886717799872, + 13.036858219662173 + ], + "angularVelocity": [ + 0.6891512982303367, + -0.812045637158409, + 0.6109149586806297 + ] + }, + { + "id": "asteroid-10", + "position": [ + 139.26026649006366, + -122.77943749993788, + 143.35992517501452 + ], + "scale": 7.581077495569217, + "linearVelocity": [ + -19.75186450305236, + 17.55615395104285, + -20.333336195551883 + ], + "angularVelocity": [ + 0.17865152400950324, + -0.7250158783763934, + 0.35870649668097077 + ] + }, + { + "id": "asteroid-11", + "position": [ + -133.22175412558724, + 33.51358825038045, + 49.482594310773 + ], + "scale": 7.010783583602642, + "linearVelocity": [ + 29.559837238905327, + -7.214260036150041, + -10.979418816287 + ], + "angularVelocity": [ + 0.006970641689681756, + 0.8085035823297635, + 0.261296695954941 + ] + }, + { + "id": "asteroid-12", + "position": [ + 52.88615939894847, + -40.55511139942254, + -126.39639295872398 + ], + "scale": 2.547979860881804, + "linearVelocity": [ + -14.07056955914136, + 11.055899920293106, + 33.628254714704276 + ], + "angularVelocity": [ + -0.25997501815568524, + 0.20162897126321955, + -0.6193621234887217 + ] + }, + { + "id": "asteroid-13", + "position": [ + 33.71316860172135, + 26.19223115813049, + -219.82948777649838 + ], + "scale": 3.5905563547959125, + "linearVelocity": [ + -3.0578229277195907, + -2.2849641618026393, + 19.93878581550797 + ], + "angularVelocity": [ + 0.8208004537206359, + 0.943626667623497, + 0.636331499279438 + ] + }, + { + "id": "asteroid-14", + "position": [ + 70.99037607148803, + 65.01706917911979, + 141.7156035818434 + ], + "scale": 7.900929561366385, + "linearVelocity": [ + -11.738878460803742, + -10.585781286097086, + -23.433912292159985 + ], + "angularVelocity": [ + 0.5542302859386417, + 0.7347191385249978, + -0.7600598276619333 + ] + }, + { + "id": "asteroid-15", + "position": [ + -63.24684515880312, + -121.32993266780319, + -236.30572977273826 + ], + "scale": 6.031745164618868, + "linearVelocity": [ + 5.4348582536848165, + 10.511921069944156, + 20.30596376188773 + ], + "angularVelocity": [ + -0.7997802484968162, + 0.42836919054372613, + -0.314396626586837 + ] + }, + { + "id": "asteroid-16", + "position": [ + -176.72603145601283, + 13.819761297012825, + -77.33003398577715 + ], + "scale": 3.6280218432879696, + "linearVelocity": [ + 23.539665910780506, + -1.7075746877886944, + 10.300254862835958 + ], + "angularVelocity": [ + -0.34409993460756905, + -0.20940818931646854, + -0.11173980734000555 + ] + }, + { + "id": "asteroid-17", + "position": [ + -15.581622764575508, + 97.68495726755624, + -131.15566381186713 + ], + "scale": 5.302935812905752, + "linearVelocity": [ + 2.0973576383778574, + -13.01423713724918, + 17.654151783068915 + ], + "angularVelocity": [ + 0.9043715876220539, + -0.48302780015394475, + 0.3182089564692956 + ] + }, + { + "id": "asteroid-18", + "position": [ + -127.31657799128861, + -163.71590384627933, + -8.658802013966405 + ], + "scale": 2.0655323763955127, + "linearVelocity": [ + 12.561981065792901, + 16.25207100283125, + 0.8543404847037116 + ], + "angularVelocity": [ + -0.4242424307159647, + -0.30548784444591126, + -0.1417785915052585 + ] + }, + { + "id": "asteroid-19", + "position": [ + 266.21201720238054, + 67.13263299906843, + -47.61795653071815 + ], + "scale": 4.148416375051257, + "linearVelocity": [ + -22.747929144440555, + -5.651061381103751, + 4.068974468347706 + ], + "angularVelocity": [ + 0.24657630590088297, + -0.2876210241185655, + 0.036951840019366244 + ] + }, + { + "id": "asteroid-20", + "position": [ + -130.4487444673596, + -133.54730547477757, + 122.08916981149021 + ], + "scale": 2.6766966053282504, + "linearVelocity": [ + 20.809900912759115, + 21.46372589817787, + -19.476335603474116 + ], + "angularVelocity": [ + -0.09448320109110542, + 0.5627809609464198, + 0.03899317893652299 + ] + }, + { + "id": "asteroid-21", + "position": [ + -167.01997402006253, + -81.60408402264582, + -131.50830148148964 + ], + "scale": 7.735792020015795, + "linearVelocity": [ + 23.728388441797488, + 11.735493338855232, + 18.683274735146135 + ], + "angularVelocity": [ + 0.39684294087209393, + 0.8913492946951482, + -0.7066277514054979 + ] + }, + { + "id": "asteroid-22", + "position": [ + 67.20559549760922, + -85.04449403639762, + -3.4657156416780683 + ], + "scale": 2.8083607565734185, + "linearVelocity": [ + -19.622893810106003, + 25.123532600504888, + 1.0119301749969825 + ], + "angularVelocity": [ + -0.21704223291194769, + -0.054572642271028204, + -0.8574342587539809 + ] + }, + { + "id": "asteroid-23", + "position": [ + -178.83660268420658, + -135.11846468332007, + -81.20130277734171 + ], + "scale": 5.617601889463837, + "linearVelocity": [ + 18.73095906653341, + 14.256753661803959, + 8.504848871219885 + ], + "angularVelocity": [ + 0.05846198435432193, + 0.7847536016343937, + -0.20024060705388136 + ] + }, + { + "id": "asteroid-24", + "position": [ + 133.298646185844, + 6.263088866828296, + 45.54416728413243 + ], + "scale": 3.8834755951862325, + "linearVelocity": [ + -31.549662103249243, + -1.2456891357793825, + -10.779577510374088 + ], + "angularVelocity": [ + 0.9406991286269459, + -0.7530273627887878, + -0.5146367905397993 + ] + }, + { + "id": "asteroid-25", + "position": [ + -52.870700373715756, + -30.457334160674296, + 66.80240799141129 + ], + "scale": 3.084903437984574, + "linearVelocity": [ + 14.57918241565675, + 8.674411532228188, + -18.420873660226565 + ], + "angularVelocity": [ + -0.1475500952638753, + 0.5420012182952676, + -0.9280062603862036 + ] + }, + { + "id": "asteroid-26", + "position": [ + 53.921586498644615, + 25.614880519848644, + -239.3792792049877 + ], + "scale": 4.129362492162142, + "linearVelocity": [ + -7.549536119561917, + -3.446317915144604, + 33.515380981569926 + ], + "angularVelocity": [ + 0.32899241666939494, + -0.5581685678578219, + -0.8299997976742683 + ] + }, + { + "id": "asteroid-27", + "position": [ + -84.0007854765908, + 39.40037065419949, + -184.5194755685237 + ], + "scale": 7.857691580383484, + "linearVelocity": [ + 9.223555040971405, + -4.216483575875807, + 20.260828864650872 + ], + "angularVelocity": [ + 0.5671327337952365, + 0.4966047689819506, + -0.12838194464568087 + ] + }, + { + "id": "asteroid-28", + "position": [ + -6.723062963768454, + 229.19109350652636, + -44.96094920552127 + ], + "scale": 4.146481028556643, + "linearVelocity": [ + 0.5815223495389018, + -19.737762617261815, + 3.8889709884315193 + ], + "angularVelocity": [ + -0.2819905204070907, + -0.666056758318089, + -0.7329241452147119 + ] + }, + { + "id": "asteroid-29", + "position": [ + -5.420085800167328, + 137.5929570067395, + 62.88367105901004 + ], + "scale": 3.957056081473905, + "linearVelocity": [ + 1.1742702147772721, + -29.593081525833952, + -13.623847415509067 + ], + "angularVelocity": [ + -0.75675132185515, + 0.39791799169962827, + -0.8993476320237579 + ] + }, + { + "id": "asteroid-30", + "position": [ + -96.53732432627598, + 4.005667372278689, + 218.0224902983474 + ], + "scale": 3.9772797190583407, + "linearVelocity": [ + 13.62751711903197, + -0.42428960876736294, + -30.776751268069084 + ], + "angularVelocity": [ + 0.47062905276332545, + 0.7359299323477955, + -0.013621065478302885 + ] + }, + { + "id": "asteroid-31", + "position": [ + 17.977161874173163, + 219.01565887599202, + -90.09466451286532 + ], + "scale": 6.461731818630775, + "linearVelocity": [ + -2.405499833479485, + -29.172381869440102, + 12.055445792833495 + ], + "angularVelocity": [ + -0.14778153893752277, + 0.8797235508363888, + -0.7617384158633715 + ] + }, + { + "id": "asteroid-32", + "position": [ + 212.37762042656277, + 32.08082234373045, + 98.96990689063155 + ], + "scale": 7.046638242818798, + "linearVelocity": [ + -32.019339098900744, + -4.6859334241421084, + -14.921303868801674 + ], + "angularVelocity": [ + 0.9711089067193552, + -0.6817796163798913, + 0.08772358279687165 + ] + }, + { + "id": "asteroid-33", + "position": [ + 202.45421857255099, + 17.766114210441344, + 52.999854458966894 + ], + "scale": 5.485004604300619, + "linearVelocity": [ + -21.495610030845626, + -1.780144940131811, + -5.62726828403066 + ], + "angularVelocity": [ + -0.3892121331474394, + -0.9607600126799083, + 0.40336226722458335 + ] + }, + { + "id": "asteroid-34", + "position": [ + 133.82532739865124, + -22.28674761837511, + -19.47715065985845 + ], + "scale": 4.805533514672975, + "linearVelocity": [ + -32.7422308754581, + 5.697427248491427, + 4.7653562752097836 + ], + "angularVelocity": [ + 0.9404459512722712, + -0.7973796489697049, + 0.28790265818839433 + ] + }, + { + "id": "asteroid-35", + "position": [ + -5.606809251802911, + -254.66675749226488, + -10.635198414965616 + ], + "scale": 4.868158964660745, + "linearVelocity": [ + 0.456334291128285, + 20.808538922165738, + 0.8655913750126973 + ], + "angularVelocity": [ + 0.3906433220412131, + 0.26410603775720576, + -0.364521441421076 + ] + }, + { + "id": "asteroid-36", + "position": [ + -126.70392892461646, + -112.98432594456347, + 41.83710351722766 + ], + "scale": 2.1597079657543365, + "linearVelocity": [ + 17.694142399324, + 15.917855994457849, + -5.842531273434433 + ], + "angularVelocity": [ + -0.9195088621706802, + 0.45467971429947207, + -0.5804904645288449 + ] + }, + { + "id": "asteroid-37", + "position": [ + 98.09603569379702, + 176.04097735099725, + -184.53396602588938 + ], + "scale": 7.6848963588573564, + "linearVelocity": [ + -7.910543808918427, + -14.11544625527125, + 14.880968554507746 + ], + "angularVelocity": [ + 0.17838675227087597, + -0.7807486854848582, + 0.18557145914210338 + ] + }, + { + "id": "asteroid-38", + "position": [ + -96.78393551125991, + 163.15357034985922, + 86.38366562881744 + ], + "scale": 6.461251439335374, + "linearVelocity": [ + 12.28975291189166, + -20.59047612451102, + -10.969112803623526 + ], + "angularVelocity": [ + -0.7009499648616151, + 0.31191584050991095, + 0.9135097165353177 + ] + }, + { + "id": "asteroid-39", + "position": [ + -113.75646033563181, + 94.04064946819827, + 228.25927168787246 + ], + "scale": 3.212742691765523, + "linearVelocity": [ + 15.627088724695533, + -12.781291541183881, + -31.356706075210653 + ], + "angularVelocity": [ + -0.3893464616882625, + 0.6858221147860539, + 0.6701153033899852 + ] + }, + { + "id": "asteroid-40", + "position": [ + -103.67465463239579, + 70.90053787393109, + 55.242289075228385 + ], + "scale": 5.840877663423122, + "linearVelocity": [ + 24.96798090210368, + -16.83415585874284, + -13.304007845595459 + ], + "angularVelocity": [ + -0.9648573178051563, + -0.25784351426379004, + -0.7350801272196414 + ] + }, + { + "id": "asteroid-41", + "position": [ + -38.28172667257399, + 74.60845401559355, + 64.00896537255215 + ], + "scale": 4.507908854908897, + "linearVelocity": [ + 10.127371481088943, + -19.473002467744898, + -16.933472619781597 + ], + "angularVelocity": [ + -0.2741982079231837, + -0.8091480289784934, + 0.27597687379539204 + ] + }, + { + "id": "asteroid-42", + "position": [ + 41.26891323067563, + 169.3466702949089, + 45.830622243471495 + ], + "scale": 6.312092614518194, + "linearVelocity": [ + -7.595773776137795, + -30.98514410541094, + -8.435381775992047 + ], + "angularVelocity": [ + 0.42162983420692557, + -0.2753023281598619, + 0.5429463799014655 + ] + }, + { + "id": "asteroid-43", + "position": [ + 103.08410072400935, + 27.070699067602238, + -50.2373689392114 + ], + "scale": 4.692463721627536, + "linearVelocity": [ + -18.766756455062037, + -4.746245605079522, + 9.145857229229264 + ], + "angularVelocity": [ + -0.21887611594005874, + -0.9457095897804053, + -0.8349682240847964 + ] + }, + { + "id": "asteroid-44", + "position": [ + -211.704823336666, + 28.633868714092078, + 54.36879913294464 + ], + "scale": 7.361304400691506, + "linearVelocity": [ + 23.109356849973484, + -3.0164685111766647, + -5.9348103688204485 + ], + "angularVelocity": [ + -0.4102834272306395, + -0.0005085326930096556, + 0.27144136108856687 + ] + }, + { + "id": "asteroid-45", + "position": [ + 40.59067938395703, + -112.63595913252314, + -231.26039013718872 + ], + "scale": 3.5759363198998693, + "linearVelocity": [ + -5.297570050449684, + 14.83085434614602, + 30.182252064750937 + ], + "angularVelocity": [ + -0.6950879443133422, + 0.4441824268524672, + -0.07409360133911669 + ] + }, + { + "id": "asteroid-46", + "position": [ + 50.72754186291391, + 80.55142505916288, + -61.67160515601282 + ], + "scale": 2.385886346276281, + "linearVelocity": [ + -10.42476777557777, + -16.348222326624164, + 12.673828427089251 + ], + "angularVelocity": [ + -0.7133111908426297, + -0.5586764394873507, + 0.46198693656014367 + ] + }, + { + "id": "asteroid-47", + "position": [ + -135.89028513460119, + -128.16310832550596, + -43.60556463891677 + ], + "scale": 7.533076777196445, + "linearVelocity": [ + 18.02879362210448, + 17.136287713897335, + 5.7852239022949234 + ], + "angularVelocity": [ + -0.6758791113463056, + -0.5832680879062346, + -0.9813816265316921 + ] + }, + { + "id": "asteroid-48", + "position": [ + -124.41539897273083, + 78.21449226859862, + -166.25175407703486 + ], + "scale": 7.706723236551336, + "linearVelocity": [ + 15.617651835165512, + -9.692602899938148, + 20.869297800747134 + ], + "angularVelocity": [ + -0.39628629090540235, + -0.059621252061398344, + 0.4553357505365425 + ] + }, + { + "id": "asteroid-49", + "position": [ + -66.79829949459466, + 74.5913805566566, + 48.555623126422674 + ], + "scale": 2.63422135535551, + "linearVelocity": [ + 16.76157960260831, + -18.466155465021334, + -12.18397696267165 + ], + "angularVelocity": [ + 0.796215193833989, + 0.31056861419965776, + 0.19715448756571075 + ] + } + ], + "difficultyConfig": { + "rockCount": 50, + "forceMultiplier": 1.3, + "rockSizeMin": 2, + "rockSizeMax": 8, + "distanceMin": 90, + "distanceMax": 280 + } +} \ No newline at end of file diff --git a/public/levels/rescue-mission.json b/public/levels/rescue-mission.json new file mode 100644 index 0000000..ab97ef5 --- /dev/null +++ b/public/levels/rescue-mission.json @@ -0,0 +1,251 @@ +{ + "version": "1.0", + "difficulty": "pilot", + "timestamp": "2025-11-11T23:44:24.810Z", + "metadata": { + "author": "System", + "description": "Clear a path through moderate asteroid density to reach the stranded station.", + "estimatedTime": "5-8 minutes", + "type": "default" + }, + "ship": { + "position": [ + 0, + 1, + 0 + ], + "rotation": [ + 0, + 0, + 0 + ], + "linearVelocity": [ + 0, + 0, + 0 + ], + "angularVelocity": [ + 0, + 0, + 0 + ] + }, + "startBase": { + "position": [ + 0, + 0, + 0 + ], + "baseGlbPath": "base.glb" + }, + "sun": { + "position": [ + 0, + 0, + 400 + ], + "diameter": 50, + "intensity": 1000000 + }, + "planets": [], + "asteroids": [ + { + "id": "asteroid-0", + "position": [ + 242.60734209985543, + -114.56996058926651, + 5.575229357062 + ], + "scale": 15.53109319217301, + "linearVelocity": [ + -17.167175139332553, + 8.177863609194048, + -0.39450965492725215 + ], + "angularVelocity": [ + -0.834980024785148, + 0.9648009938830251, + 0.8185653748494373 + ] + }, + { + "id": "asteroid-1", + "position": [ + 145.90971366777896, + 42.273817290099984, + -244.80503221456152 + ], + "scale": 17.57678371034564, + "linearVelocity": [ + -14.737555578618144, + -4.168846343154079, + 24.72643991613985 + ], + "angularVelocity": [ + 0.575649251710729, + -0.8551046445434349, + -0.9477761112717422 + ] + }, + { + "id": "asteroid-2", + "position": [ + 195.05992969157123, + -111.0584087077698, + -22.40662780090249 + ], + "scale": 14.234090261353138, + "linearVelocity": [ + -16.81570103491442, + 9.660316715266058, + 1.9316276535952197 + ], + "angularVelocity": [ + 0.8587973467645904, + 0.25620436829463733, + -0.7705721105608303 + ] + }, + { + "id": "asteroid-3", + "position": [ + -0.9357515100775112, + 85.76554222686204, + 249.4670613777975 + ], + "scale": 17.34408913479813, + "linearVelocity": [ + 0.07109432360434195, + -6.440116659897093, + -18.953420645560346 + ], + "angularVelocity": [ + 0.19650221972006143, + 0.4226089665809898, + -0.9419176203015098 + ] + }, + { + "id": "asteroid-4", + "position": [ + -254.14456477364413, + 54.65967750105119, + 82.65652287437858 + ], + "scale": 14.980803819380306, + "linearVelocity": [ + 22.372081486064396, + -4.723605553550473, + -7.2761676675924445 + ], + "angularVelocity": [ + -0.22039903827783025, + 0.03062354927084643, + 0.3628209366655213 + ] + }, + { + "id": "asteroid-5", + "position": [ + -257.7249224576784, + -112.97325792551102, + -92.25372143357285 + ], + "scale": 17.10484995348801, + "linearVelocity": [ + 17.764361846647077, + 7.855903788127005, + 6.358828139777149 + ], + "angularVelocity": [ + -0.27982741337355455, + 0.2465507084870353, + -0.8489416083688623 + ] + }, + { + "id": "asteroid-6", + "position": [ + -61.74000302102928, + 103.75532261403117, + -224.6843746923246 + ], + "scale": 14.438006716048399, + "linearVelocity": [ + 4.573571795825104, + -7.611901885044768, + 16.644154013167135 + ], + "angularVelocity": [ + -0.41949593751738457, + -0.5881266007071146, + 0.2671577602439994 + ] + }, + { + "id": "asteroid-7", + "position": [ + 16.846663100767792, + 72.36836836065181, + 271.36235273889974 + ], + "scale": 18.93457175982751, + "linearVelocity": [ + -1.2776861733199087, + -5.412726361379603, + -20.580688530433683 + ], + "angularVelocity": [ + -0.5793176374486806, + 0.8207961833131412, + -0.034658037798875885 + ] + }, + { + "id": "asteroid-8", + "position": [ + 129.11110725214024, + 91.10691458736655, + 205.0668479159754 + ], + "scale": 15.43421226033438, + "linearVelocity": [ + -10.330594112594069, + -7.209743461671342, + -16.4080567261488 + ], + "angularVelocity": [ + -0.572098306083443, + 0.6581860817605101, + -0.7141435682550208 + ] + }, + { + "id": "asteroid-9", + "position": [ + -30.953057070289603, + 225.21952155696817, + 139.05608152400566 + ], + "scale": 14.151176153817078, + "linearVelocity": [ + 1.9861965590557589, + -14.387724003424648, + -8.922954201633985 + ], + "angularVelocity": [ + 0.7016416714654072, + -0.8069811132136699, + -0.16093262088047533 + ] + } + ], + "difficultyConfig": { + "rockCount": 10, + "forceMultiplier": 1, + "rockSizeMin": 8, + "rockSizeMax": 20, + "distanceMin": 225, + "distanceMax": 300 + } +} \ No newline at end of file diff --git a/public/levels/rookie-training.json b/public/levels/rookie-training.json new file mode 100644 index 0000000..01fc7a0 --- /dev/null +++ b/public/levels/rookie-training.json @@ -0,0 +1,157 @@ +{ + "version": "1.0", + "difficulty": "recruit", + "timestamp": "2025-11-11T23:44:24.807Z", + "metadata": { + "author": "System", + "description": "Learn the basics of ship control and asteroid destruction in a calm sector of space.", + "estimatedTime": "3-5 minutes", + "type": "default" + }, + "ship": { + "position": [ + 0, + 1, + 0 + ], + "rotation": [ + 0, + 0, + 0 + ], + "linearVelocity": [ + 0, + 0, + 0 + ], + "angularVelocity": [ + 0, + 0, + 0 + ] + }, + "startBase": { + "position": [ + 0, + 0, + 0 + ], + "baseGlbPath": "base.glb" + }, + "sun": { + "position": [ + 0, + 0, + 400 + ], + "diameter": 50, + "intensity": 1000000 + }, + "planets": [], + "asteroids": [ + { + "id": "asteroid-0", + "position": [ + -58.36428561508043, + -34.06637347343719, + -338.05090628315398 + ], + "scale": 10.625768191199857, + "linearVelocity": [ + 40.109444660495561, + 20.469032555006988, + 50.76122675780117 + ], + "angularVelocity": [ + -0.9227568265038757, + 0.8864368307279991, + 0.2263811569998082 + ] + }, + { + "id": "asteroid-1", + "position": [ + -190.9219320499405, + -113.5286869174331, + 109.66679494883233 + ], + "scale": 13.813320127774826, + "linearVelocity": [ + 50.192029331119596, + 6.113911187593468, + -5.854367692424185 + ], + "angularVelocity": [ + -0.13959509905525813, + -0.43731874553360095, + -0.7521225869488948 + ] + }, + { + "id": "asteroid-2", + "position": [ + -83.56705788144077, + 64.21712690769468, + 308.30902558008037 + ], + "scale": 11.275762178690638, + "linearVelocity": [ + 5.1248287379998665, + -3.8768500043399237, + -52.774747702523037 + ], + "angularVelocity": [ + 0.2651717231493369, + -0.7099071240513899, + 0.1663263008032705 + ] + }, + { + "id": "asteroid-3", + "position": [ + 217.1757715338133, + -61.659461955067925, + -44.39701192104106 + ], + "scale": 10.559754454640935, + "linearVelocity": [ + -13.51279993073768, + 3.8987073336340097, + 2.7624073135533793 + ], + "angularVelocity": [ + -0.11401525507789279, + -0.8495070152885558, + -0.14027260599293268 + ] + }, + { + "id": "asteroid-4", + "position": [ + -146.69138484095544, + -213.7144044815136, + -152.58815494515483 + ], + "scale": 14.914115364458254, + "linearVelocity": [ + 30.82941775132167, + -90.089462061108069, + 12.30494231618762 + ], + "angularVelocity": [ + -0.5051583421856027, + -0.2763521676136351, + -0.9366828390177142 + ] + } + ], + "difficultyConfig": { + "rockCount": 5, + "forceMultiplier": 0.8, + "rockSizeMin": 10, + "rockSizeMax": 15, + "distanceMin": 220, + "distanceMax": 250 + }, + "useOrbitConstraints": true +} \ No newline at end of file diff --git a/public/levels/the-gauntlet.json b/public/levels/the-gauntlet.json new file mode 100644 index 0000000..069d397 --- /dev/null +++ b/public/levels/the-gauntlet.json @@ -0,0 +1,1011 @@ +{ + "version": "1.0", + "difficulty": "commander", + "timestamp": "2025-11-11T23:44:24.811Z", + "metadata": { + "author": "System", + "description": "Face maximum asteroid density in this ultimate test of piloting skill.", + "estimatedTime": "12-18 minutes", + "type": "default" + }, + "ship": { + "position": [ + 0, + 1, + 0 + ], + "rotation": [ + 0, + 0, + 0 + ], + "linearVelocity": [ + 0, + 0, + 0 + ], + "angularVelocity": [ + 0, + 0, + 0 + ] + }, + "startBase": { + "position": [ + 0, + 0, + 0 + ], + "baseGlbPath": "base.glb" + }, + "sun": { + "position": [ + 0, + 0, + 400 + ], + "diameter": 50, + "intensity": 1000000 + }, + "planets": [], + "asteroids": [ + { + "id": "asteroid-0", + "position": [ + -17.69249542924826, + 84.01947133429799, + -123.59996053766258 + ], + "scale": 2.4822349460013755, + "linearVelocity": [ + 2.845566885340348, + -13.3524103149862, + 19.87916041249419 + ], + "angularVelocity": [ + 0.8143534066313953, + -0.9359364839346735, + -0.15178405063615052 + ] + }, + { + "id": "asteroid-1", + "position": [ + 63.984810857938065, + -138.19039359552377, + -202.0837448202854 + ], + "scale": 5.228607391030508, + "linearVelocity": [ + -6.032111857562759, + 13.122052130942931, + 19.05126759001586 + ], + "angularVelocity": [ + 0.2456343615499761, + 0.1546807786434896, + -0.06339276268092364 + ] + }, + { + "id": "asteroid-2", + "position": [ + 58.01904127898412, + 12.138119094999812, + -89.25164583877536 + ], + "scale": 2.122806567381292, + "linearVelocity": [ + -12.904402645940184, + -2.477303491265637, + 19.8510549179584 + ], + "angularVelocity": [ + -0.315477510780648, + 0.02605341030097108, + -0.04781611527650309 + ] + }, + { + "id": "asteroid-3", + "position": [ + -3.732596323717469, + 76.9212787972044, + 147.8376940162343 + ], + "scale": 6.78767682745117, + "linearVelocity": [ + 0.7265875337501956, + -14.778842911542565, + -28.778098721264893 + ], + "angularVelocity": [ + -0.4582822008501273, + -0.2731815124326724, + -0.2799393493128326 + ] + }, + { + "id": "asteroid-4", + "position": [ + -70.25248112139637, + 87.4446903013746, + -95.37109606717652 + ], + "scale": 7.803845112745136, + "linearVelocity": [ + 9.61425791082154, + -11.83020918709425, + 13.051813974272738 + ], + "angularVelocity": [ + 0.055110327764350764, + -0.6960566876366734, + -0.8174703803698806 + ] + }, + { + "id": "asteroid-5", + "position": [ + 11.97154351290052, + 98.06453089645528, + 4.68809691635772 + ], + "scale": 7.176428304060899, + "linearVelocity": [ + -4.171955178580714, + -33.82595334457933, + -1.6337517536323216 + ], + "angularVelocity": [ + 0.23365116005857356, + 0.5640501772631668, + 0.380708669667587 + ] + }, + { + "id": "asteroid-6", + "position": [ + 192.37623868871566, + 7.9067941678296645, + -21.647149999117644 + ], + "scale": 4.21988390215245, + "linearVelocity": [ + -34.82926406114048, + -1.2504587860070007, + 3.9191654261956965 + ], + "angularVelocity": [ + -0.43528131155696936, + -0.48368793148659917, + 0.30910126660666526 + ] + }, + { + "id": "asteroid-7", + "position": [ + -139.1698118565668, + 119.36628762878482, + 20.619259081159424 + ], + "scale": 5.992358222092905, + "linearVelocity": [ + 24.112121849216546, + -20.50776897712679, + -3.572427675032611 + ], + "angularVelocity": [ + 0.9670925025584953, + -0.5364352737758646, + -0.6370220343457231 + ] + }, + { + "id": "asteroid-8", + "position": [ + -173.62607217490572, + -86.82244180359253, + 101.53714690010123 + ], + "scale": 4.975600066657989, + "linearVelocity": [ + 24.74684826064222, + 12.517294286328381, + -14.47204521579052 + ], + "angularVelocity": [ + -0.5500147661394417, + -0.47775772035399955, + 0.03624248493864268 + ] + }, + { + "id": "asteroid-9", + "position": [ + -63.293770039525235, + -47.57236815138559, + 98.44978727432182 + ], + "scale": 5.510187826488146, + "linearVelocity": [ + 9.905149561744375, + 7.601325862042018, + -15.406885490706511 + ], + "angularVelocity": [ + 0.5442896673614208, + 0.43278299885548366, + 0.7177786018517671 + ] + }, + { + "id": "asteroid-10", + "position": [ + -71.2443552209747, + -34.37394745718009, + -68.45790898149781 + ], + "scale": 7.44414316374618, + "linearVelocity": [ + 22.576899560179744, + 11.209787165744586, + 21.693891823168332 + ], + "angularVelocity": [ + 0.13705844930855315, + -0.13957278878608026, + 0.26051553985494236 + ] + }, + { + "id": "asteroid-11", + "position": [ + -56.20618420585617, + -15.20992678120928, + -139.64412229223393 + ], + "scale": 6.434561181339902, + "linearVelocity": [ + 8.652854911371863, + 2.495493094639771, + 21.49799611009127 + ], + "angularVelocity": [ + 0.8922275295480073, + 0.6441672013569177, + -0.973381248154531 + ] + }, + { + "id": "asteroid-12", + "position": [ + 158.00061717993069, + 173.40530582047103, + 38.190864206116224 + ], + "scale": 5.89053699178403, + "linearVelocity": [ + -16.584321359082637, + -18.096290044750486, + -4.008652474148492 + ], + "angularVelocity": [ + -0.7389906029437507, + 0.9265514774815395, + 0.10079559226610701 + ] + }, + { + "id": "asteroid-13", + "position": [ + -30.737732165559482, + -141.9978338257131, + 15.420078915248526 + ], + "scale": 5.690047523897615, + "linearVelocity": [ + 6.065184245710226, + 28.21640205005727, + -3.042697463841066 + ], + "angularVelocity": [ + 0.5323319786110039, + 0.8524845445090516, + 0.10932007875597138 + ] + }, + { + "id": "asteroid-14", + "position": [ + 24.94305070017472, + -43.22339758140768, + 151.6095306638 + ], + "scale": 6.439033819547129, + "linearVelocity": [ + -4.14729642136399, + 7.353051586775, + -25.208210155394 + ], + "angularVelocity": [ + 0.948963427047024, + 0.8769281695424223, + 0.7749395170021645 + ] + }, + { + "id": "asteroid-15", + "position": [ + -61.34653750296117, + 57.83255237146782, + 58.733804843372 + ], + "scale": 7.411803898691092, + "linearVelocity": [ + 22.97698052737865, + -21.286294260656696, + -21.998397059000684 + ], + "angularVelocity": [ + 0.6675307209862567, + 0.7227585311395863, + 0.4420673115212401 + ] + }, + { + "id": "asteroid-16", + "position": [ + -37.25553642113452, + -120.70527387944571, + -29.057404814029024 + ], + "scale": 3.957316655611956, + "linearVelocity": [ + 7.64786110383856, + 24.983804278459164, + 5.964938836033984 + ], + "angularVelocity": [ + -0.04990733621183585, + 0.3213807487954159, + -0.8249357279008502 + ] + }, + { + "id": "asteroid-17", + "position": [ + 71.36053456154347, + 8.83029677144011, + -108.83855840217205 + ], + "scale": 5.567689704136111, + "linearVelocity": [ + -11.387757399705226, + -1.249563509420639, + 17.368523182077134 + ], + "angularVelocity": [ + -0.5490182185036208, + 0.589669916681197, + -0.26754100712868967 + ] + }, + { + "id": "asteroid-18", + "position": [ + 151.50847074392675, + -32.3088798925633, + -223.19448362007165 + ], + "scale": 6.147415631174571, + "linearVelocity": [ + -21.168241669017547, + 4.653801967823788, + 31.18396446919878 + ], + "angularVelocity": [ + 0.2864696744974964, + -0.860383920390162, + 0.6688593336459294 + ] + }, + { + "id": "asteroid-19", + "position": [ + 88.38850855034818, + -32.730221403976486, + 33.93741577999266 + ], + "scale": 2.1495196617499763, + "linearVelocity": [ + -20.73873035762759, + 7.914173211800297, + -7.9627875437569084 + ], + "angularVelocity": [ + 0.22754232478381997, + -0.9600606731676464, + 0.6534324396607265 + ] + }, + { + "id": "asteroid-20", + "position": [ + 56.37728488635036, + -29.080058276579333, + -77.87984657632342 + ], + "scale": 2.4777429301381644, + "linearVelocity": [ + -16.094136490163834, + 8.587014513225695, + 22.232515864484874 + ], + "angularVelocity": [ + 0.23648856922553918, + -0.24451219338047414, + -0.4916351438552491 + ] + }, + { + "id": "asteroid-21", + "position": [ + -83.83737035611433, + 197.27711918145468, + -71.82003184115236 + ], + "scale": 5.306332890246562, + "linearVelocity": [ + 10.686368295690732, + -25.018551687400784, + 9.154572811655648 + ], + "angularVelocity": [ + -0.27317436709975684, + -0.9075865316791374, + -0.463661891894926 + ] + }, + { + "id": "asteroid-22", + "position": [ + -184.93009099932914, + -61.22619022984281, + -25.31363639383512 + ], + "scale": 3.82591176887556, + "linearVelocity": [ + 22.593389394352748, + 7.602335232696162, + 3.0926326859107585 + ], + "angularVelocity": [ + -0.13800594943626754, + 0.9820620198254972, + 0.1337952639500295 + ] + }, + { + "id": "asteroid-23", + "position": [ + -18.61656598851856, + -84.19062453265941, + -58.98293196690726 + ], + "scale": 7.723567853426068, + "linearVelocity": [ + 6.8848169020073025, + 31.505372797364377, + 21.813189778724862 + ], + "angularVelocity": [ + 0.2806886814321876, + -0.4707224077534278, + -0.44679626206260403 + ] + }, + { + "id": "asteroid-24", + "position": [ + 9.982756900828752, + 25.34550777883981, + 174.81546294680126 + ], + "scale": 2.455463837118164, + "linearVelocity": [ + -1.6261348854825026, + -3.9657461257691686, + -28.4764545148813 + ], + "angularVelocity": [ + -0.9878231501802839, + 0.8081769020759859, + 0.24139320114361373 + ] + }, + { + "id": "asteroid-25", + "position": [ + 85.97584014127001, + -18.36845444516715, + 21.612173734719885 + ], + "scale": 5.200751604722696, + "linearVelocity": [ + -22.65310271608842, + 5.103242809576686, + -5.694422883522937 + ], + "angularVelocity": [ + 0.04200987896597841, + -0.8139341666224849, + -0.8442424523018222 + ] + }, + { + "id": "asteroid-26", + "position": [ + -18.147362404732576, + -33.93349782485802, + 151.56104370220478 + ], + "scale": 4.243443960009571, + "linearVelocity": [ + 3.265456212855867, + 6.285971755279474, + -27.272059748760906 + ], + "angularVelocity": [ + -0.9702775053582617, + -0.0842554503357511, + -0.48239393091993543 + ] + }, + { + "id": "asteroid-27", + "position": [ + 29.463279081142502, + 48.53435203022517, + 85.93203549166978 + ], + "scale": 3.9736714501265165, + "linearVelocity": [ + -6.591719688806077, + -10.634699664940493, + -19.22528340072518 + ], + "angularVelocity": [ + 0.1477134041440542, + -0.23021176712254787, + -0.95912886657212 + ] + }, + { + "id": "asteroid-28", + "position": [ + 94.67472208601781, + -51.36902431340063, + -53.53649012698429 + ], + "scale": 5.342814120388171, + "linearVelocity": [ + -30.25486417960931, + 16.735382823527623, + 17.10846572090565 + ], + "angularVelocity": [ + -0.3292193104731016, + 0.2629761827218311, + -0.6373224611402581 + ] + }, + { + "id": "asteroid-29", + "position": [ + -77.35700658518722, + -60.2156008204566, + 223.39272030439184 + ], + "scale": 7.113926085337681, + "linearVelocity": [ + 9.08840529211956, + 7.192007744563502, + -26.24563269779095 + ], + "angularVelocity": [ + 0.976229994021721, + 0.7425192253135258, + -0.05865398467785443 + ] + }, + { + "id": "asteroid-30", + "position": [ + -160.62129847807205, + 24.303991026780103, + 203.6372247402815 + ], + "scale": 3.994871375933545, + "linearVelocity": [ + 22.590016365454648, + -3.277507675902893, + -28.639839691787444 + ], + "angularVelocity": [ + 0.7986524886593211, + 0.07017578002987568, + -0.0729810964221147 + ] + }, + { + "id": "asteroid-31", + "position": [ + -9.08559224648419, + 181.30147536657145, + -65.27599666295716 + ], + "scale": 3.7137853466561155, + "linearVelocity": [ + 1.6203577064663106, + -32.15562367005, + 11.641559666187014 + ], + "angularVelocity": [ + -0.913824435854449, + 0.7434377869132911, + -0.3756865004293357 + ] + }, + { + "id": "asteroid-32", + "position": [ + 189.70316470878717, + -120.09762092122276, + -48.846624927282996 + ], + "scale": 7.290759721105586, + "linearVelocity": [ + -17.654927556800274, + 11.270079378733827, + 4.545963299079479 + ], + "angularVelocity": [ + 0.6799807993971965, + -0.32327184736549164, + -0.6290207664318799 + ] + }, + { + "id": "asteroid-33", + "position": [ + 112.00956904768304, + 45.47325562421672, + -34.48590875224766 + ], + "scale": 4.539281693950024, + "linearVelocity": [ + -31.818327298474443, + -12.633426014501019, + 9.796340984020938 + ], + "angularVelocity": [ + -0.2769563462314655, + 0.8569603881562071, + 0.7552183434271313 + ] + }, + { + "id": "asteroid-34", + "position": [ + -12.92900057099983, + -104.55940293102228, + 193.01515476341243 + ], + "scale": 2.4522034888431365, + "linearVelocity": [ + 1.2259576345202725, + 10.009385892437988, + -18.30214186014367 + ], + "angularVelocity": [ + 0.6319179106392157, + 0.14889390927145296, + 0.5096284177561161 + ] + }, + { + "id": "asteroid-35", + "position": [ + 30.9222953424653, + -59.73614452377679, + -134.86589893545482 + ], + "scale": 7.648205531976861, + "linearVelocity": [ + -5.166396423389132, + 10.147597271253181, + 22.532955273227252 + ], + "angularVelocity": [ + -0.57447229513755, + 0.6167301423531786, + 0.7608483078864228 + ] + }, + { + "id": "asteroid-36", + "position": [ + 21.299271624744744, + -3.076528760779656, + -187.03355737593918 + ], + "scale": 3.3849895586031855, + "linearVelocity": [ + -2.3142869908978425, + 0.4429380330608524, + 20.3222596679603 + ], + "angularVelocity": [ + 0.4434594058024679, + 0.47601891376021976, + -0.9614706032298672 + ] + }, + { + "id": "asteroid-37", + "position": [ + -52.06555713074758, + -88.13082194012654, + 31.362300959863596 + ], + "scale": 2.9814249862342095, + "linearVelocity": [ + 13.065846441362154, + 22.367370999941603, + -7.870366341424741 + ], + "angularVelocity": [ + -0.606088525759358, + 0.9760190676080001, + 0.21614631003206553 + ] + }, + { + "id": "asteroid-38", + "position": [ + 111.74847001971337, + -46.593396030727135, + 10.30299345327331 + ], + "scale": 3.598715227421736, + "linearVelocity": [ + -34.39046566898009, + 14.646813974062974, + -3.170734620169892 + ], + "angularVelocity": [ + 0.7823563333916792, + -0.09667218106455566, + -0.9863725693621945 + ] + }, + { + "id": "asteroid-39", + "position": [ + 41.12549753059419, + -110.62600144734095, + 105.70294096797092 + ], + "scale": 7.5286295816877296, + "linearVelocity": [ + -6.739925253367709, + 18.294025635257128, + -17.32331434179661 + ], + "angularVelocity": [ + 0.5358451540291913, + -0.7670842268163174, + 0.25075136997588343 + ] + }, + { + "id": "asteroid-40", + "position": [ + -7.827104533179446, + -87.80034531298853, + 118.49863381522742 + ], + "scale": 4.367977717291674, + "linearVelocity": [ + 1.1285331197920545, + 12.80347417232854, + -17.085453802706855 + ], + "angularVelocity": [ + 0.671389974972818, + -0.8615181772964595, + 0.9359448435887807 + ] + }, + { + "id": "asteroid-41", + "position": [ + -46.80007443244874, + 57.97210772086458, + -68.82780636383549 + ], + "scale": 2.6345865049108226, + "linearVelocity": [ + 9.802870739378262, + -11.933532467853123, + 14.416859315758417 + ], + "angularVelocity": [ + 0.11529862932130674, + 0.6289048901581462, + 0.9755210004518591 + ] + }, + { + "id": "asteroid-42", + "position": [ + -8.773048719467749, + 137.24607560181138, + 29.92007746932852 + ], + "scale": 7.585496577115003, + "linearVelocity": [ + 1.8364536523717736, + -28.52025688687242, + -6.263140363721625 + ], + "angularVelocity": [ + 0.2568883436338276, + -0.027537736570946603, + -0.9997001024251122 + ] + }, + { + "id": "asteroid-43", + "position": [ + -208.06420264890895, + 168.58332269368364, + 49.3390009623257 + ], + "scale": 3.3155579748642157, + "linearVelocity": [ + 16.422079076340587, + -13.227006578327453, + -3.8942257487617167 + ], + "angularVelocity": [ + -0.4077385864933505, + 0.10523125455460924, + 0.0787742851613662 + ] + }, + { + "id": "asteroid-44", + "position": [ + 163.5341103753907, + -93.24071573475678, + -66.1228090099442 + ], + "scale": 7.530524133877911, + "linearVelocity": [ + -22.923549091767324, + 13.21028174874324, + 9.268827493821192 + ], + "angularVelocity": [ + -0.2853973502864995, + -0.38391699859347783, + -0.34145325244527625 + ] + }, + { + "id": "asteroid-45", + "position": [ + 58.44320381412548, + -6.663309996939115, + -139.77007327892716 + ], + "scale": 7.917462811120764, + "linearVelocity": [ + -13.35627744708985, + 1.7513292872804096, + 31.942257708025082 + ], + "angularVelocity": [ + -0.3035227337109796, + 0.8110502901756704, + 0.5622879202093074 + ] + }, + { + "id": "asteroid-46", + "position": [ + -84.86130981337153, + 125.7707536063935, + 58.50666852856773 + ], + "scale": 7.254098545673697, + "linearVelocity": [ + 14.05511634361984, + -20.665100056500204, + -9.690140711415802 + ], + "angularVelocity": [ + 0.5789240503925863, + 0.17764258688605983, + -0.5170086906715832 + ] + }, + { + "id": "asteroid-47", + "position": [ + 109.12027526940211, + -16.2972369891638, + 246.64273224066662 + ], + "scale": 4.76488074279656, + "linearVelocity": [ + -14.89890865321594, + 2.3617054962364326, + -33.67575391980061 + ], + "angularVelocity": [ + 0.8551827640601197, + -0.11165941443379568, + -0.2282173651835886 + ] + }, + { + "id": "asteroid-48", + "position": [ + -161.30786116434385, + -67.30060287410612, + 77.8187790616285 + ], + "scale": 6.009585306352164, + "linearVelocity": [ + 22.19472054425371, + 9.39763742977407, + -10.707265237421959 + ], + "angularVelocity": [ + 0.8522694373709983, + -0.8360825443501181, + 0.9106879359934164 + ] + }, + { + "id": "asteroid-49", + "position": [ + 114.48412297912911, + -8.05214097126244, + 5.591726826980028 + ], + "scale": 5.07128964153552, + "linearVelocity": [ + -36.747157200067484, + 2.9055596410411777, + -1.7948345970063413 + ], + "angularVelocity": [ + -0.14332341199825072, + 0.572781384427409, + 0.6469944623807775 + ] + } + ], + "difficultyConfig": { + "rockCount": 50, + "forceMultiplier": 1.3, + "rockSizeMin": 2, + "rockSizeMax": 8, + "distanceMin": 90, + "distanceMax": 280 + } +} \ No newline at end of file diff --git a/public/styles.css b/public/styles.css index 1400c18..9e9ee85 100644 --- a/public/styles.css +++ b/public/styles.css @@ -160,6 +160,7 @@ body { } .editor-link { + display: none; background: rgba(76, 175, 80, 0.8); } @@ -169,6 +170,7 @@ body { } .settings-link { + display: none; background: rgba(33, 150, 243, 0.8); } @@ -512,6 +514,11 @@ body { outline-offset: 2px; } +/* TODO: Re-enable copy button functionality for copying default levels to custom levels */ +.level-button-secondary { + display: none !important; +} + /* Test Level Button */ .test-level-button { background: var(--gradient-danger); diff --git a/scripts/generateDefaultLevels.cjs b/scripts/generateDefaultLevels.cjs new file mode 100644 index 0000000..2317c99 --- /dev/null +++ b/scripts/generateDefaultLevels.cjs @@ -0,0 +1,215 @@ +#!/usr/bin/env node + +/** + * Script to generate default level JSON files + * Run with: node scripts/generateDefaultLevels.js + */ + +const fs = require('fs'); +const path = require('path'); + +// Helper function to generate random asteroid data +function generateAsteroid(id, config, shipPos = [0, 1, 0]) { + const { distanceMin, distanceMax, rockSizeMin, rockSizeMax, forceMultiplier } = config; + + // Random spherical distribution + const theta = Math.random() * Math.PI * 2; // Azimuth angle + const phi = Math.acos(2 * Math.random() - 1); // Polar angle + const distance = distanceMin + Math.random() * (distanceMax - distanceMin); + + const position = [ + shipPos[0] + distance * Math.sin(phi) * Math.cos(theta), + shipPos[1] + distance * Math.sin(phi) * Math.sin(theta), + shipPos[2] + distance * Math.cos(phi) + ]; + + const scale = rockSizeMin + Math.random() * (rockSizeMax - rockSizeMin); + + // Random velocity toward ship + const speedMin = 15 * forceMultiplier; + const speedMax = 30 * forceMultiplier; + const speed = speedMin + Math.random() * (speedMax - speedMin); + + const dirToShip = [ + shipPos[0] - position[0], + shipPos[1] - position[1], + shipPos[2] - position[2] + ]; + const length = Math.sqrt(dirToShip[0]**2 + dirToShip[1]**2 + dirToShip[2]**2); + const normalized = dirToShip.map(v => v / length); + + const linearVelocity = normalized.map(v => v * speed); + + const angularVelocity = [ + (Math.random() - 0.5) * 2, + (Math.random() - 0.5) * 2, + (Math.random() - 0.5) * 2 + ]; + + return { + id: `asteroid-${id}`, + position, + scale, + linearVelocity, + angularVelocity + }; +} + +// Level configurations matching LevelGenerator difficulty configs +const levels = [ + { + filename: 'rookie-training.json', + difficulty: 'recruit', + difficultyConfig: { + rockCount: 5, + forceMultiplier: 0.8, + rockSizeMin: 10, + rockSizeMax: 15, + distanceMin: 220, + distanceMax: 250 + }, + metadata: { + author: 'System', + description: 'Learn the basics of ship control and asteroid destruction in a calm sector of space.', + estimatedTime: '3-5 minutes', + type: 'default' + } + }, + { + filename: 'rescue-mission.json', + difficulty: 'pilot', + difficultyConfig: { + rockCount: 10, + forceMultiplier: 1.0, + rockSizeMin: 8, + rockSizeMax: 20, + distanceMin: 225, + distanceMax: 300 + }, + metadata: { + author: 'System', + description: 'Clear a path through moderate asteroid density to reach the stranded station.', + estimatedTime: '5-8 minutes', + type: 'default' + } + }, + { + filename: 'deep-space-patrol.json', + difficulty: 'captain', + difficultyConfig: { + rockCount: 20, + forceMultiplier: 1.2, + rockSizeMin: 5, + rockSizeMax: 40, + distanceMin: 230, + distanceMax: 450 + }, + metadata: { + author: 'System', + description: 'Patrol a dangerous sector with heavy asteroid activity. Watch your fuel!', + estimatedTime: '8-12 minutes', + type: 'default' + } + }, + { + filename: 'enemy-territory.json', + difficulty: 'commander', + difficultyConfig: { + rockCount: 50, + forceMultiplier: 1.3, + rockSizeMin: 2, + rockSizeMax: 8, + distanceMin: 90, + distanceMax: 280 + }, + metadata: { + author: 'System', + description: 'Navigate through hostile space with high-speed asteroids and limited resources.', + estimatedTime: '10-15 minutes', + type: 'default' + } + }, + { + filename: 'the-gauntlet.json', + difficulty: 'commander', + difficultyConfig: { + rockCount: 50, + forceMultiplier: 1.3, + rockSizeMin: 2, + rockSizeMax: 8, + distanceMin: 90, + distanceMax: 280 + }, + metadata: { + author: 'System', + description: 'Face maximum asteroid density in this ultimate test of piloting skill.', + estimatedTime: '12-18 minutes', + type: 'default' + } + }, + { + filename: 'final-challenge.json', + difficulty: 'commander', + difficultyConfig: { + rockCount: 50, + forceMultiplier: 1.3, + rockSizeMin: 2, + rockSizeMax: 8, + distanceMin: 90, + distanceMax: 280 + }, + metadata: { + author: 'System', + description: 'The ultimate challenge - survive the most chaotic asteroid field in known space.', + estimatedTime: '15-20 minutes', + type: 'default' + } + } +]; + +// Output directory +const outputDir = path.join(__dirname, '../public/levels'); + +// Ensure directory exists +if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }); +} + +// Generate each level +for (const level of levels) { + const asteroids = []; + for (let i = 0; i < level.difficultyConfig.rockCount; i++) { + asteroids.push(generateAsteroid(i, level.difficultyConfig)); + } + + const levelConfig = { + version: '1.0', + difficulty: level.difficulty, + timestamp: new Date().toISOString(), + metadata: level.metadata, + ship: { + position: [0, 1, 0], + rotation: [0, 0, 0], + linearVelocity: [0, 0, 0], + angularVelocity: [0, 0, 0] + }, + startBase: { + position: [0, 0, 0], + baseGlbPath: 'base.glb' + }, + sun: { + position: [0, 0, 400], + diameter: 50, + intensity: 1000000 + }, + planets: [], + asteroids, + difficultyConfig: level.difficultyConfig + }; + + const outputPath = path.join(outputDir, level.filename); + fs.writeFileSync(outputPath, JSON.stringify(levelConfig, null, 2)); + console.log(`Generated: ${level.filename} (${level.difficultyConfig.rockCount} asteroids)`); +} + +console.log(`\nSuccessfully generated ${levels.length} default level files!`); diff --git a/src/core/router.ts b/src/core/router.ts index aab3725..067d00d 100644 --- a/src/core/router.ts +++ b/src/core/router.ts @@ -77,15 +77,20 @@ export const router = new Router(); * Helper to show/hide views */ export function showView(viewId: string): void { + console.log('[Router] showView() called with viewId:', viewId); + // Hide all views const views = document.querySelectorAll('[data-view]'); + console.log('[Router] Found views:', views.length); views.forEach(view => { (view as HTMLElement).style.display = 'none'; }); // Show requested view const targetView = document.querySelector(`[data-view="${viewId}"]`); + console.log('[Router] Target view found:', !!targetView); if (targetView) { (targetView as HTMLElement).style.display = 'block'; + console.log('[Router] View display set to block'); } } diff --git a/src/environment/asteroids/rockFactory.ts b/src/environment/asteroids/rockFactory.ts index 2bc1fb7..b9b7822 100644 --- a/src/environment/asteroids/rockFactory.ts +++ b/src/environment/asteroids/rockFactory.ts @@ -77,7 +77,8 @@ export class RockFactory { } public static async createRock(i: number, position: Vector3, scale: number, - linearVelocitry: Vector3, angularVelocity: Vector3, score: Observable): Promise { + linearVelocitry: Vector3, angularVelocity: Vector3, score: Observable, + useOrbitConstraint: boolean = true): Promise { const rock = new InstancedMesh("asteroid-" +i, this._asteroidMesh as Mesh); debugLog(rock.id); @@ -100,13 +101,32 @@ export class RockFactory { // Don't pass radius - let Babylon compute from scaled mesh bounds }, DefaultScene.MainScene); const body = agg.body; - const constraint = new DistanceConstraint(Vector3.Distance(position, this._orbitCenter.body.transformNode.position), DefaultScene.MainScene); - body.addConstraint(this._orbitCenter.body, constraint); + + // Only apply orbit constraint if enabled for this level + if (useOrbitConstraint) { + debugLog(`[RockFactory] Applying orbit constraint for ${rock.name}`); + const constraint = new DistanceConstraint(Vector3.Distance(position, this._orbitCenter.body.transformNode.position), DefaultScene.MainScene); + body.addConstraint(this._orbitCenter.body, constraint); + } else { + debugLog(`[RockFactory] Orbit constraint disabled for ${rock.name} - asteroid will move freely`); + } + body.setLinearDamping(0) body.setMotionType(PhysicsMotionType.DYNAMIC); body.setCollisionCallbackEnabled(true); + + debugLog(`[RockFactory] Setting velocities for ${rock.name}:`); + debugLog(`[RockFactory] Linear velocity input: ${linearVelocitry.toString()}`); + debugLog(`[RockFactory] Angular velocity input: ${angularVelocity.toString()}`); + body.setLinearVelocity(linearVelocitry); body.setAngularVelocity(angularVelocity); + + // Verify velocities were set + const setLinear = body.getLinearVelocity(); + const setAngular = body.getAngularVelocity(); + debugLog(`[RockFactory] Linear velocity after set: ${setLinear.toString()}`); + debugLog(`[RockFactory] Angular velocity after set: ${setAngular.toString()}`); body.getCollisionObservable().add((eventData) => { if (eventData.type == 'COLLISION_STARTED') { if ( eventData.collidedAgainst.transformNode.id == 'ammo') { diff --git a/src/levels/config/levelConfig.ts b/src/levels/config/levelConfig.ts index 19755f3..73446e3 100644 --- a/src/levels/config/levelConfig.ts +++ b/src/levels/config/levelConfig.ts @@ -147,6 +147,9 @@ export interface LevelConfig { // Optional: include original difficulty config for reference difficultyConfig?: DifficultyConfig; + // Physics configuration + useOrbitConstraints?: boolean; // Default: true - constrains asteroids to orbit at fixed distance + // New fields for full scene serialization materials?: MaterialConfig[]; sceneHierarchy?: SceneNodeConfig[]; diff --git a/src/levels/config/levelDeserializer.ts b/src/levels/config/levelDeserializer.ts index d3213e1..68bfcbe 100644 --- a/src/levels/config/levelDeserializer.ts +++ b/src/levels/config/levelDeserializer.ts @@ -21,6 +21,7 @@ import { FireProceduralTexture } from "@babylonjs/procedural-textures"; import { createSphereLightmap } from "../../environment/celestial/sphereLightmap"; import debugLog from '../../core/debug'; import StarBase from "../../environment/stations/starBase"; +import {LevelRegistry} from "../storage/levelRegistry"; /** * Deserializes a LevelConfig JSON object and creates all entities in the scene @@ -172,6 +173,16 @@ export class LevelDeserializer { for (let i = 0; i < this.config.asteroids.length; i++) { const asteroidConfig = this.config.asteroids[i]; + debugLog(`[LevelDeserializer] Creating asteroid ${i} (${asteroidConfig.id}):`); + debugLog(`[LevelDeserializer] Position: [${asteroidConfig.position.join(', ')}]`); + debugLog(`[LevelDeserializer] Scale: ${asteroidConfig.scale}`); + debugLog(`[LevelDeserializer] Linear velocity: [${asteroidConfig.linearVelocity.join(', ')}]`); + debugLog(`[LevelDeserializer] Angular velocity: [${asteroidConfig.angularVelocity.join(', ')}]`); + + // Use orbit constraints by default (true if not specified) + const useOrbitConstraints = this.config.useOrbitConstraints !== false; + debugLog(`[LevelDeserializer] Use orbit constraints: ${useOrbitConstraints}`); + // Use RockFactory to create the asteroid const rock = await RockFactory.createRock( i, @@ -179,7 +190,8 @@ export class LevelDeserializer { asteroidConfig.scale, this.arrayToVector3(asteroidConfig.linearVelocity), this.arrayToVector3(asteroidConfig.angularVelocity), - scoreObservable + scoreObservable, + useOrbitConstraints ); // Get the actual mesh from the Rock object @@ -246,4 +258,26 @@ export class LevelDeserializer { reader.readAsText(file); }); } + + /** + * Static helper to load from Level Registry by ID + * This is the preferred method for loading both default and custom levels + */ + public static async fromRegistry(levelId: string): Promise { + const registry = LevelRegistry.getInstance(); + + // Ensure registry is initialized + if (!registry.isInitialized()) { + await registry.initialize(); + } + + // Get level config from registry (loads if not already loaded) + const config = await registry.getLevel(levelId); + + if (!config) { + throw new Error(`Level not found in registry: ${levelId}`); + } + + return new LevelDeserializer(config); + } } diff --git a/src/levels/migration/legacyMigration.ts b/src/levels/migration/legacyMigration.ts new file mode 100644 index 0000000..e6e9c8b --- /dev/null +++ b/src/levels/migration/legacyMigration.ts @@ -0,0 +1,330 @@ +import {LevelConfig} from "../config/levelConfig"; + +const LEGACY_STORAGE_KEY = 'space-game-levels'; +const ARCHIVE_STORAGE_KEY = 'space-game-levels-archive'; +const CUSTOM_LEVELS_KEY = 'space-game-custom-levels'; +const MIGRATION_STATUS_KEY = 'space-game-migration-status'; + +/** + * Migration status information + */ +export interface MigrationStatus { + migrated: boolean; + migratedAt?: Date; + version: string; + customLevelsMigrated: number; + defaultLevelsRemoved: number; +} + +/** + * Result of migration operation + */ +export interface MigrationResult { + success: boolean; + customLevelsMigrated: number; + defaultLevelsFound: number; + error?: string; + legacyDataArchived: boolean; +} + +/** + * Handles migration from legacy localStorage format to new hybrid system + */ +export class LegacyMigration { + private static readonly MIGRATION_VERSION = '2.0'; + + /** + * Check if migration is needed + */ + public static needsMigration(): boolean { + // Check if migration was already completed + const status = this.getMigrationStatus(); + if (status && status.migrated) { + return false; + } + + // Check if legacy data exists + const legacyData = localStorage.getItem(LEGACY_STORAGE_KEY); + return legacyData !== null && legacyData.length > 0; + } + + /** + * Get current migration status + */ + public static getMigrationStatus(): MigrationStatus | null { + const stored = localStorage.getItem(MIGRATION_STATUS_KEY); + if (!stored) { + return null; + } + + try { + const status: MigrationStatus = JSON.parse(stored); + if (status.migratedAt && typeof status.migratedAt === 'string') { + status.migratedAt = new Date(status.migratedAt); + } + return status; + } catch (error) { + console.error('Failed to parse migration status:', error); + return null; + } + } + + /** + * Perform the migration + */ + public static migrate(): MigrationResult { + const result: MigrationResult = { + success: false, + customLevelsMigrated: 0, + defaultLevelsFound: 0, + legacyDataArchived: false + }; + + try { + // Load legacy data + const legacyData = localStorage.getItem(LEGACY_STORAGE_KEY); + if (!legacyData) { + result.error = 'No legacy data found'; + return result; + } + + const legacyLevels: [string, LevelConfig][] = JSON.parse(legacyData); + + // Separate custom from default levels + const customLevels: [string, LevelConfig][] = []; + + for (const [name, config] of legacyLevels) { + if (config.metadata?.type === 'default') { + result.defaultLevelsFound++; + // Skip default levels - they'll be loaded from JSON files now + } else { + customLevels.push([name, config]); + result.customLevelsMigrated++; + } + } + + // Save custom levels to new storage location + if (customLevels.length > 0) { + localStorage.setItem(CUSTOM_LEVELS_KEY, JSON.stringify(customLevels)); + } + + // Archive legacy data (don't delete immediately) + this.archiveLegacyData(legacyData); + result.legacyDataArchived = true; + + // Clear legacy storage key + localStorage.removeItem(LEGACY_STORAGE_KEY); + + // Record migration status + const status: MigrationStatus = { + migrated: true, + migratedAt: new Date(), + version: this.MIGRATION_VERSION, + customLevelsMigrated: result.customLevelsMigrated, + defaultLevelsRemoved: result.defaultLevelsFound + }; + localStorage.setItem(MIGRATION_STATUS_KEY, JSON.stringify(status)); + + result.success = true; + + console.log('Migration completed:', result); + } catch (error) { + result.error = error instanceof Error ? error.message : 'Unknown error'; + console.error('Migration failed:', error); + } + + return result; + } + + /** + * Archive legacy data for potential recovery + */ + private static archiveLegacyData(legacyData: string): void { + const archive = { + data: legacyData, + archivedAt: new Date().toISOString(), + migrationVersion: this.MIGRATION_VERSION + }; + + localStorage.setItem(ARCHIVE_STORAGE_KEY, JSON.stringify(archive)); + } + + /** + * Get archived legacy data (for export/recovery) + */ + public static getArchivedData(): string | null { + const stored = localStorage.getItem(ARCHIVE_STORAGE_KEY); + if (!stored) { + return null; + } + + try { + const archive = JSON.parse(stored); + return archive.data || null; + } catch (error) { + console.error('Failed to parse archived data:', error); + return null; + } + } + + /** + * Export legacy data as JSON file + */ + public static exportLegacyData(): string | null { + const archivedData = this.getArchivedData(); + if (!archivedData) { + return null; + } + + try { + const levels: [string, LevelConfig][] = JSON.parse(archivedData); + const exportData = { + exportedAt: new Date().toISOString(), + migrationVersion: this.MIGRATION_VERSION, + levels: Object.fromEntries(levels) + }; + + return JSON.stringify(exportData, null, 2); + } catch (error) { + console.error('Failed to export legacy data:', error); + return null; + } + } + + /** + * Download legacy data as JSON file + */ + public static downloadLegacyData(): void { + const jsonString = this.exportLegacyData(); + if (!jsonString) { + console.warn('No legacy data to download'); + return; + } + + const blob = new Blob([jsonString], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `legacy-levels-backup-${Date.now()}.json`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + } + + /** + * Clear archived data (after user confirms backup) + */ + public static clearArchive(): void { + localStorage.removeItem(ARCHIVE_STORAGE_KEY); + } + + /** + * Reset migration status (for testing/debugging) + */ + public static resetMigration(): void { + localStorage.removeItem(MIGRATION_STATUS_KEY); + console.log('Migration status reset'); + } + + /** + * Full reset - clear all migration data (dangerous!) + */ + public static fullReset(): void { + localStorage.removeItem(LEGACY_STORAGE_KEY); + localStorage.removeItem(ARCHIVE_STORAGE_KEY); + localStorage.removeItem(CUSTOM_LEVELS_KEY); + localStorage.removeItem(MIGRATION_STATUS_KEY); + console.log('Full migration reset completed'); + } + + /** + * Create and show migration modal UI + */ + public static showMigrationModal(onComplete: (result: MigrationResult) => void): void { + const modal = document.createElement('div'); + modal.style.cssText = ` + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.8); + display: flex; + align-items: center; + justify-content: center; + z-index: 10000; + font-family: 'Courier New', monospace; + `; + + const content = document.createElement('div'); + content.style.cssText = ` + background: #1a1a1a; + border: 2px solid #00ff00; + padding: 30px; + max-width: 600px; + color: #00ff00; + box-shadow: 0 0 20px rgba(0, 255, 0, 0.3); + `; + + content.innerHTML = ` +

Level System Updated

+

The level storage system has been upgraded!

+

Changes:

+
    +
  • Default levels now load from game files (always available)
  • +
  • Your custom levels remain in browser storage
  • +
  • Version tracking and update notifications enabled
  • +
  • Level statistics and performance tracking added
  • +
+

Your data will be migrated automatically.

+

A backup of your old level data will be saved.

+
+ + +
+ + `; + + modal.appendChild(content); + document.body.appendChild(modal); + + const exportBtn = content.querySelector('#export-backup') as HTMLButtonElement; + const migrateBtn = content.querySelector('#migrate-now') as HTMLButtonElement; + const statusText = content.querySelector('#migration-status') as HTMLParagraphElement; + + exportBtn.addEventListener('click', () => { + this.downloadLegacyData(); + statusText.textContent = 'Backup downloaded! You can now proceed with migration.'; + statusText.style.display = 'block'; + }); + + migrateBtn.addEventListener('click', () => { + statusText.textContent = 'Migrating...'; + statusText.style.display = 'block'; + statusText.style.color = '#ffff00'; + + // Give UI time to update + setTimeout(() => { + const result = this.migrate(); + + if (result.success) { + statusText.textContent = `Migration complete! ${result.customLevelsMigrated} custom levels migrated.`; + statusText.style.color = '#00ff00'; + + setTimeout(() => { + document.body.removeChild(modal); + onComplete(result); + }, 2000); + } else { + statusText.textContent = `Migration failed: ${result.error}`; + statusText.style.color = '#ff0000'; + } + }, 100); + }); + } +} diff --git a/src/levels/stats/levelStats.ts b/src/levels/stats/levelStats.ts new file mode 100644 index 0000000..0653f1d --- /dev/null +++ b/src/levels/stats/levelStats.ts @@ -0,0 +1,381 @@ +/** + * Completion record for a single play-through + */ +export interface LevelCompletion { + timestamp: Date; + completionTimeSeconds: number; + score?: number; + survived: boolean; // false if player died/quit +} + +/** + * Aggregated statistics for a level + */ +export interface LevelStatistics { + levelId: string; + firstPlayed?: Date; + lastPlayed?: Date; + completions: LevelCompletion[]; + totalAttempts: number; // Including incomplete attempts + totalCompletions: number; // Only successful completions + bestTimeSeconds?: number; + averageTimeSeconds?: number; + bestScore?: number; + averageScore?: number; + completionRate: number; // percentage (0-100) + difficultyRating?: number; // 1-5 stars, user-submitted +} + +const STATS_STORAGE_KEY = 'space-game-level-stats'; + +/** + * Manages level performance statistics and ratings + */ +export class LevelStatsManager { + private static instance: LevelStatsManager | null = null; + + private statsMap: Map = new Map(); + + private constructor() { + this.loadStats(); + } + + public static getInstance(): LevelStatsManager { + if (!LevelStatsManager.instance) { + LevelStatsManager.instance = new LevelStatsManager(); + } + return LevelStatsManager.instance; + } + + /** + * Load stats from localStorage + */ + private loadStats(): void { + const stored = localStorage.getItem(STATS_STORAGE_KEY); + if (!stored) { + return; + } + + try { + const statsArray: [string, LevelStatistics][] = JSON.parse(stored); + + for (const [id, stats] of statsArray) { + // Parse date strings back to Date objects + if (stats.firstPlayed && typeof stats.firstPlayed === 'string') { + stats.firstPlayed = new Date(stats.firstPlayed); + } + if (stats.lastPlayed && typeof stats.lastPlayed === 'string') { + stats.lastPlayed = new Date(stats.lastPlayed); + } + + // Parse completion timestamps + stats.completions = stats.completions.map(c => ({ + ...c, + timestamp: typeof c.timestamp === 'string' ? new Date(c.timestamp) : c.timestamp + })); + + this.statsMap.set(id, stats); + } + } catch (error) { + console.error('Failed to load level stats:', error); + } + } + + /** + * Save stats to localStorage + */ + private saveStats(): void { + const statsArray = Array.from(this.statsMap.entries()); + localStorage.setItem(STATS_STORAGE_KEY, JSON.stringify(statsArray)); + } + + /** + * Get statistics for a level + */ + public getStats(levelId: string): LevelStatistics | undefined { + return this.statsMap.get(levelId); + } + + /** + * Initialize stats for a level if not exists + */ + private ensureStatsExist(levelId: string): LevelStatistics { + let stats = this.statsMap.get(levelId); + if (!stats) { + stats = { + levelId, + completions: [], + totalAttempts: 0, + totalCompletions: 0, + completionRate: 0 + }; + this.statsMap.set(levelId, stats); + } + return stats; + } + + /** + * Record that a level was started (attempt) + */ + public recordAttempt(levelId: string): void { + const stats = this.ensureStatsExist(levelId); + stats.totalAttempts++; + + const now = new Date(); + if (!stats.firstPlayed) { + stats.firstPlayed = now; + } + stats.lastPlayed = now; + + this.recalculateStats(stats); + this.saveStats(); + } + + /** + * Record a level completion + */ + public recordCompletion( + levelId: string, + completionTimeSeconds: number, + score?: number, + survived: boolean = true + ): void { + const stats = this.ensureStatsExist(levelId); + + const completion: LevelCompletion = { + timestamp: new Date(), + completionTimeSeconds, + score, + survived + }; + + stats.completions.push(completion); + + if (survived) { + stats.totalCompletions++; + } + + const now = new Date(); + if (!stats.firstPlayed) { + stats.firstPlayed = now; + } + stats.lastPlayed = now; + + this.recalculateStats(stats); + this.saveStats(); + } + + /** + * Set difficulty rating for a level (1-5 stars) + */ + public setDifficultyRating(levelId: string, rating: number): void { + if (rating < 1 || rating > 5) { + console.warn('Rating must be between 1 and 5'); + return; + } + + const stats = this.ensureStatsExist(levelId); + stats.difficultyRating = rating; + this.saveStats(); + } + + /** + * Recalculate aggregated statistics + */ + private recalculateStats(stats: LevelStatistics): void { + const successfulCompletions = stats.completions.filter(c => c.survived); + + // Completion rate + stats.completionRate = stats.totalAttempts > 0 + ? (stats.totalCompletions / stats.totalAttempts) * 100 + : 0; + + // Time statistics + if (successfulCompletions.length > 0) { + const times = successfulCompletions.map(c => c.completionTimeSeconds); + stats.bestTimeSeconds = Math.min(...times); + stats.averageTimeSeconds = times.reduce((a, b) => a + b, 0) / times.length; + } else { + stats.bestTimeSeconds = undefined; + stats.averageTimeSeconds = undefined; + } + + // Score statistics + const completionsWithScore = successfulCompletions.filter(c => c.score !== undefined); + if (completionsWithScore.length > 0) { + const scores = completionsWithScore.map(c => c.score!); + stats.bestScore = Math.max(...scores); + stats.averageScore = scores.reduce((a, b) => a + b, 0) / scores.length; + } else { + stats.bestScore = undefined; + stats.averageScore = undefined; + } + } + + /** + * Get all stats + */ + public getAllStats(): Map { + return new Map(this.statsMap); + } + + /** + * Get stats for multiple levels + */ + public getStatsForLevels(levelIds: string[]): Map { + const result = new Map(); + for (const id of levelIds) { + const stats = this.statsMap.get(id); + if (stats) { + result.set(id, stats); + } + } + return result; + } + + /** + * Get top N fastest completions for a level + */ + public getTopCompletions(levelId: string, limit: number = 10): LevelCompletion[] { + const stats = this.statsMap.get(levelId); + if (!stats) { + return []; + } + + return stats.completions + .filter(c => c.survived) + .sort((a, b) => a.completionTimeSeconds - b.completionTimeSeconds) + .slice(0, limit); + } + + /** + * Get recent completions for a level + */ + public getRecentCompletions(levelId: string, limit: number = 10): LevelCompletion[] { + const stats = this.statsMap.get(levelId); + if (!stats) { + return []; + } + + return [...stats.completions] + .sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()) + .slice(0, limit); + } + + /** + * Delete stats for a level + */ + public deleteStats(levelId: string): boolean { + const deleted = this.statsMap.delete(levelId); + if (deleted) { + this.saveStats(); + } + return deleted; + } + + /** + * Clear all stats (for testing/reset) + */ + public clearAll(): void { + this.statsMap.clear(); + localStorage.removeItem(STATS_STORAGE_KEY); + } + + /** + * Export stats as JSON + */ + public exportStats(): string { + const statsArray = Array.from(this.statsMap.entries()); + return JSON.stringify(statsArray, null, 2); + } + + /** + * Import stats from JSON + */ + public importStats(jsonString: string): number { + try { + const statsArray: [string, LevelStatistics][] = JSON.parse(jsonString); + let importCount = 0; + + for (const [id, stats] of statsArray) { + // Parse dates + if (stats.firstPlayed && typeof stats.firstPlayed === 'string') { + stats.firstPlayed = new Date(stats.firstPlayed); + } + if (stats.lastPlayed && typeof stats.lastPlayed === 'string') { + stats.lastPlayed = new Date(stats.lastPlayed); + } + stats.completions = stats.completions.map(c => ({ + ...c, + timestamp: typeof c.timestamp === 'string' ? new Date(c.timestamp) : c.timestamp + })); + + this.statsMap.set(id, stats); + importCount++; + } + + this.saveStats(); + return importCount; + } catch (error) { + console.error('Failed to import stats:', error); + throw new Error('Invalid stats JSON format'); + } + } + + /** + * Get summary statistics across all levels + */ + public getGlobalSummary(): { + totalLevelsPlayed: number; + totalAttempts: number; + totalCompletions: number; + averageCompletionRate: number; + totalPlayTimeSeconds: number; + } { + let totalLevelsPlayed = 0; + let totalAttempts = 0; + let totalCompletions = 0; + let totalPlayTimeSeconds = 0; + let totalCompletionRates = 0; + + for (const stats of this.statsMap.values()) { + if (stats.totalAttempts > 0) { + totalLevelsPlayed++; + totalAttempts += stats.totalAttempts; + totalCompletions += stats.totalCompletions; + totalCompletionRates += stats.completionRate; + + // Sum all completion times + for (const completion of stats.completions) { + if (completion.survived) { + totalPlayTimeSeconds += completion.completionTimeSeconds; + } + } + } + } + + return { + totalLevelsPlayed, + totalAttempts, + totalCompletions, + averageCompletionRate: totalLevelsPlayed > 0 ? totalCompletionRates / totalLevelsPlayed : 0, + totalPlayTimeSeconds + }; + } + + /** + * Format time in MM:SS format + */ + public static formatTime(seconds: number): string { + const mins = Math.floor(seconds / 60); + const secs = Math.floor(seconds % 60); + return `${mins}:${secs.toString().padStart(2, '0')}`; + } + + /** + * Format completion rate as percentage + */ + public static formatCompletionRate(rate: number): string { + return `${rate.toFixed(1)}%`; + } +} diff --git a/src/levels/storage/ILevelStorageProvider.ts b/src/levels/storage/ILevelStorageProvider.ts new file mode 100644 index 0000000..c1f63e4 --- /dev/null +++ b/src/levels/storage/ILevelStorageProvider.ts @@ -0,0 +1,241 @@ +import {LevelConfig} from "../config/levelConfig"; + +/** + * Sync status for a level + */ +export enum SyncStatus { + NotSynced = 'not_synced', + Syncing = 'syncing', + Synced = 'synced', + Conflict = 'conflict', + Error = 'error' +} + +/** + * Metadata for synced levels + */ +export interface SyncMetadata { + lastSyncedAt?: Date; + syncStatus: SyncStatus; + cloudVersion?: string; + localVersion?: string; + syncError?: string; +} + +/** + * Interface for level storage providers (localStorage, cloud, etc.) + */ +export interface ILevelStorageProvider { + /** + * Get a level by ID + */ + getLevel(levelId: string): Promise; + + /** + * Save a level + */ + saveLevel(levelId: string, config: LevelConfig): Promise; + + /** + * Delete a level + */ + deleteLevel(levelId: string): Promise; + + /** + * List all level IDs + */ + listLevels(): Promise; + + /** + * Check if provider is available/connected + */ + isAvailable(): Promise; + + /** + * Get sync metadata for a level (if supported) + */ + getSyncMetadata?(levelId: string): Promise; +} + +/** + * LocalStorage implementation of level storage provider + */ +export class LocalStorageProvider implements ILevelStorageProvider { + private storageKey: string; + + constructor(storageKey: string = 'space-game-custom-levels') { + this.storageKey = storageKey; + } + + async getLevel(levelId: string): Promise { + const stored = localStorage.getItem(this.storageKey); + if (!stored) { + return null; + } + + try { + const levelsArray: [string, LevelConfig][] = JSON.parse(stored); + const found = levelsArray.find(([id]) => id === levelId); + return found ? found[1] : null; + } catch (error) { + console.error('Failed to get level from localStorage:', error); + return null; + } + } + + async saveLevel(levelId: string, config: LevelConfig): Promise { + const stored = localStorage.getItem(this.storageKey); + let levelsArray: [string, LevelConfig][] = []; + + if (stored) { + try { + levelsArray = JSON.parse(stored); + } catch (error) { + console.error('Failed to parse localStorage data:', error); + } + } + + // Update or add level + const existingIndex = levelsArray.findIndex(([id]) => id === levelId); + if (existingIndex >= 0) { + levelsArray[existingIndex] = [levelId, config]; + } else { + levelsArray.push([levelId, config]); + } + + localStorage.setItem(this.storageKey, JSON.stringify(levelsArray)); + } + + async deleteLevel(levelId: string): Promise { + const stored = localStorage.getItem(this.storageKey); + if (!stored) { + return false; + } + + try { + const levelsArray: [string, LevelConfig][] = JSON.parse(stored); + const newArray = levelsArray.filter(([id]) => id !== levelId); + + if (newArray.length === levelsArray.length) { + return false; // Level not found + } + + localStorage.setItem(this.storageKey, JSON.stringify(newArray)); + return true; + } catch (error) { + console.error('Failed to delete level from localStorage:', error); + return false; + } + } + + async listLevels(): Promise { + const stored = localStorage.getItem(this.storageKey); + if (!stored) { + return []; + } + + try { + const levelsArray: [string, LevelConfig][] = JSON.parse(stored); + return levelsArray.map(([id]) => id); + } catch (error) { + console.error('Failed to list levels from localStorage:', error); + return []; + } + } + + async isAvailable(): Promise { + try { + const testKey = '_storage_test_'; + localStorage.setItem(testKey, 'test'); + localStorage.removeItem(testKey); + return true; + } catch { + return false; + } + } +} + +/** + * Cloud storage provider (stub for future implementation) + * + * Future implementation could use: + * - Firebase Firestore + * - AWS S3 + DynamoDB + * - Custom backend API + * - IPFS for decentralized storage + */ +export class CloudStorageProvider implements ILevelStorageProvider { + private apiEndpoint: string; + private authToken?: string; + + constructor(apiEndpoint: string, authToken?: string) { + this.apiEndpoint = apiEndpoint; + this.authToken = authToken; + } + + async getLevel(_levelId: string): Promise { + // TODO: Implement cloud fetch + throw new Error('Cloud storage not yet implemented'); + } + + async saveLevel(_levelId: string, _config: LevelConfig): Promise { + // TODO: Implement cloud save + throw new Error('Cloud storage not yet implemented'); + } + + async deleteLevel(_levelId: string): Promise { + // TODO: Implement cloud delete + throw new Error('Cloud storage not yet implemented'); + } + + async listLevels(): Promise { + // TODO: Implement cloud list + throw new Error('Cloud storage not yet implemented'); + } + + async isAvailable(): Promise { + // TODO: Implement cloud connectivity check + return false; + } + + async getSyncMetadata(_levelId: string): Promise { + // TODO: Implement sync metadata fetch + throw new Error('Cloud storage not yet implemented'); + } + + /** + * Authenticate with cloud service + */ + async authenticate(token: string): Promise { + this.authToken = token; + // TODO: Implement authentication + return false; + } + + /** + * Sync local level to cloud + */ + async syncToCloud(_levelId: string, _config: LevelConfig): Promise { + // TODO: Implement sync to cloud + throw new Error('Cloud storage not yet implemented'); + } + + /** + * Sync cloud level to local + */ + async syncFromCloud(_levelId: string): Promise { + // TODO: Implement sync from cloud + throw new Error('Cloud storage not yet implemented'); + } + + /** + * Resolve sync conflicts + */ + async resolveConflict( + _levelId: string, + _strategy: 'use_local' | 'use_cloud' | 'merge' + ): Promise { + // TODO: Implement conflict resolution + throw new Error('Cloud storage not yet implemented'); + } +} diff --git a/src/levels/storage/levelRegistry.ts b/src/levels/storage/levelRegistry.ts new file mode 100644 index 0000000..129177d --- /dev/null +++ b/src/levels/storage/levelRegistry.ts @@ -0,0 +1,501 @@ +import {LevelConfig} from "../config/levelConfig"; + +/** + * Level directory entry from directory.json manifest + */ +export interface LevelDirectoryEntry { + id: string; + name: string; + description: string; + version: string; + levelPath: string; + missionBrief?: string[]; + estimatedTime?: string; + difficulty?: string; + unlockRequirements?: string[]; + tags?: string[]; + defaultLocked?: boolean; +} + +/** + * Directory manifest structure + */ +export interface LevelDirectory { + version: string; + levels: LevelDirectoryEntry[]; +} + +/** + * Registry entry combining directory info with loaded config + */ +export interface LevelRegistryEntry { + directoryEntry: LevelDirectoryEntry; + config: LevelConfig | null; // null if not yet loaded + isDefault: boolean; + loadedAt?: Date; +} + +const CUSTOM_LEVELS_KEY = 'space-game-custom-levels'; +const CACHE_NAME = 'space-game-levels-v1'; +const CACHED_VERSION_KEY = 'space-game-levels-cached-version'; + +/** + * Singleton registry for managing both default and custom levels + */ +export class LevelRegistry { + private static instance: LevelRegistry | null = null; + + private defaultLevels: Map = new Map(); + private customLevels: Map = new Map(); + private directoryManifest: LevelDirectory | null = null; + private initialized: boolean = false; + + private constructor() {} + + public static getInstance(): LevelRegistry { + if (!LevelRegistry.instance) { + LevelRegistry.instance = new LevelRegistry(); + } + return LevelRegistry.instance; + } + + /** + * Initialize the registry by loading directory and levels + */ + public async initialize(): Promise { + console.log('[LevelRegistry] initialize() called, initialized =', this.initialized); + + if (this.initialized) { + console.log('[LevelRegistry] Already initialized, skipping'); + return; + } + + try { + console.log('[LevelRegistry] Loading directory manifest...'); + // Load directory manifest + await this.loadDirectory(); + console.log('[LevelRegistry] Directory loaded, entries:', this.directoryManifest?.levels.length); + + console.log('[LevelRegistry] Loading custom levels from localStorage...'); + // Load custom levels from localStorage + this.loadCustomLevels(); + console.log('[LevelRegistry] Custom levels loaded:', this.customLevels.size); + + this.initialized = true; + console.log('[LevelRegistry] Initialization complete!'); + } catch (error) { + console.error('[LevelRegistry] Failed to initialize level registry:', error); + throw error; + } + } + + /** + * Load the directory.json manifest + */ + private async loadDirectory(): Promise { + try { + console.log('[LevelRegistry] Attempting to fetch /levels/directory.json'); + + // First, fetch from network to get the latest version + console.log('[LevelRegistry] Fetching from network to check version...'); + const response = await fetch('/levels/directory.json'); + console.log('[LevelRegistry] Fetch response status:', response.status, response.ok); + + if (!response.ok) { + // If network fails, try to use cached version as fallback + console.warn('[LevelRegistry] Network fetch failed, trying cache...'); + const cached = await this.getCachedResource('/levels/directory.json'); + if (cached) { + console.log('[LevelRegistry] Using cached directory as fallback'); + this.directoryManifest = cached; + this.populateDefaultLevelEntries(); + return; + } + throw new Error(`Failed to fetch directory: ${response.status}`); + } + + const networkManifest = await response.json(); + console.log('[LevelRegistry] Directory JSON parsed:', networkManifest); + + // Check if version changed + const cachedVersion = localStorage.getItem(CACHED_VERSION_KEY); + const currentVersion = networkManifest.version; + + if (cachedVersion && cachedVersion !== currentVersion) { + console.log('[LevelRegistry] Version changed from', cachedVersion, 'to', currentVersion, '- invalidating cache'); + await this.invalidateCache(); + } else { + console.log('[LevelRegistry] Version unchanged or first load:', currentVersion); + } + + // Update cached version + localStorage.setItem(CACHED_VERSION_KEY, currentVersion); + + // Store the manifest + this.directoryManifest = networkManifest; + + // Cache the directory + await this.cacheResource('/levels/directory.json', this.directoryManifest); + + this.populateDefaultLevelEntries(); + } catch (error) { + console.error('[LevelRegistry] Failed to load directory:', error); + throw new Error('Unable to load level directory. Please check your connection.'); + } + } + + /** + * Populate default level registry entries from directory + */ + private populateDefaultLevelEntries(): void { + if (!this.directoryManifest) { + return; + } + + this.defaultLevels.clear(); + + for (const entry of this.directoryManifest.levels) { + this.defaultLevels.set(entry.id, { + directoryEntry: entry, + config: null, // Lazy load + isDefault: true + }); + } + } + + /** + * Load custom levels from localStorage + */ + private loadCustomLevels(): void { + this.customLevels.clear(); + + const stored = localStorage.getItem(CUSTOM_LEVELS_KEY); + if (!stored) { + return; + } + + try { + const levelsArray: [string, LevelConfig][] = JSON.parse(stored); + + for (const [id, config] of levelsArray) { + this.customLevels.set(id, { + directoryEntry: { + id, + name: config.metadata?.description || id, + description: config.metadata?.description || '', + version: config.version || '1.0', + levelPath: '', // Not applicable for custom + difficulty: config.difficulty, + missionBrief: [], + defaultLocked: false + }, + config, + isDefault: false, + loadedAt: new Date() + }); + } + } catch (error) { + console.error('Failed to load custom levels from localStorage:', error); + } + } + + /** + * Get a level config by ID (loads if not yet loaded) + */ + public async getLevel(levelId: string): Promise { + // Check default levels first + const defaultEntry = this.defaultLevels.get(levelId); + if (defaultEntry) { + if (!defaultEntry.config) { + await this.loadDefaultLevel(levelId); + } + return defaultEntry.config; + } + + // Check custom levels + const customEntry = this.customLevels.get(levelId); + return customEntry?.config || null; + } + + /** + * Load a default level's config from JSON + */ + private async loadDefaultLevel(levelId: string): Promise { + const entry = this.defaultLevels.get(levelId); + if (!entry || entry.config) { + return; // Already loaded or doesn't exist + } + + try { + const levelPath = `/levels/${entry.directoryEntry.levelPath}`; + + // Try cache first + const cached = await this.getCachedResource(levelPath); + if (cached) { + entry.config = cached; + entry.loadedAt = new Date(); + return; + } + + // Fetch from network + const response = await fetch(levelPath); + if (!response.ok) { + throw new Error(`Failed to fetch level: ${response.status}`); + } + + const config: LevelConfig = await response.json(); + + // Cache the level + await this.cacheResource(levelPath, config); + + entry.config = config; + entry.loadedAt = new Date(); + } catch (error) { + console.error(`Failed to load default level ${levelId}:`, error); + throw error; + } + } + + /** + * Get all level registry entries (default + custom) + */ + public getAllLevels(): Map { + const all = new Map(); + + // Add defaults + for (const [id, entry] of this.defaultLevels) { + all.set(id, entry); + } + + // Add customs + for (const [id, entry] of this.customLevels) { + all.set(id, entry); + } + + return all; + } + + /** + * Get only default levels + */ + public getDefaultLevels(): Map { + return new Map(this.defaultLevels); + } + + /** + * Get only custom levels + */ + public getCustomLevels(): Map { + return new Map(this.customLevels); + } + + /** + * Save a custom level + */ + public saveCustomLevel(levelId: string, config: LevelConfig): void { + // Ensure metadata exists + if (!config.metadata) { + config.metadata = { + author: 'Player', + description: levelId + }; + } + + // Remove 'default' type if present + if (config.metadata.type === 'default') { + delete config.metadata.type; + } + + // Add/update in memory + this.customLevels.set(levelId, { + directoryEntry: { + id: levelId, + name: config.metadata.description || levelId, + description: config.metadata.description || '', + version: config.version || '1.0', + levelPath: '', + difficulty: config.difficulty, + missionBrief: [], + defaultLocked: false + }, + config, + isDefault: false, + loadedAt: new Date() + }); + + // Persist to localStorage + this.saveCustomLevelsToStorage(); + } + + /** + * Delete a custom level + */ + public deleteCustomLevel(levelId: string): boolean { + const deleted = this.customLevels.delete(levelId); + if (deleted) { + this.saveCustomLevelsToStorage(); + } + return deleted; + } + + /** + * Copy a default level to custom levels with a new ID + */ + public async copyDefaultToCustom(defaultLevelId: string, newCustomId: string): Promise { + const config = await this.getLevel(defaultLevelId); + if (!config) { + return false; + } + + // Deep clone the config + const clonedConfig: LevelConfig = JSON.parse(JSON.stringify(config)); + + // Update metadata + clonedConfig.metadata = { + ...clonedConfig.metadata, + type: undefined, + author: 'Player', + description: `Copy of ${defaultLevelId}`, + originalDefault: defaultLevelId + }; + + this.saveCustomLevel(newCustomId, clonedConfig); + return true; + } + + /** + * Persist custom levels to localStorage + */ + private saveCustomLevelsToStorage(): void { + const levelsArray: [string, LevelConfig][] = []; + + for (const [id, entry] of this.customLevels) { + if (entry.config) { + levelsArray.push([id, entry.config]); + } + } + + localStorage.setItem(CUSTOM_LEVELS_KEY, JSON.stringify(levelsArray)); + } + + /** + * Get a resource from cache + */ + private async getCachedResource(path: string): Promise { + if (!('caches' in window)) { + return null; + } + + try { + const cache = await caches.open(CACHE_NAME); + const response = await cache.match(path); + + if (response) { + return await response.json(); + } + } catch (error) { + console.warn('Cache read failed:', error); + } + + return null; + } + + /** + * Cache a resource + */ + private async cacheResource(path: string, data: any): Promise { + if (!('caches' in window)) { + return; + } + + try { + const cache = await caches.open(CACHE_NAME); + const response = new Response(JSON.stringify(data), { + headers: {'Content-Type': 'application/json'} + }); + await cache.put(path, response); + } catch (error) { + console.warn('Cache write failed:', error); + } + } + + /** + * Invalidate the entire cache (called when version changes) + */ + private async invalidateCache(): Promise { + console.log('[LevelRegistry] Invalidating cache...'); + if ('caches' in window) { + await caches.delete(CACHE_NAME); + } + + // Clear loaded configs + for (const entry of this.defaultLevels.values()) { + entry.config = null; + entry.loadedAt = undefined; + } + console.log('[LevelRegistry] Cache invalidated'); + } + + /** + * Force refresh all default levels from network + */ + public async refreshDefaultLevels(): Promise { + // Clear cache + await this.invalidateCache(); + + // Clear cached version to force re-check + localStorage.removeItem(CACHED_VERSION_KEY); + + // Reload directory + await this.loadDirectory(); + } + + /** + * Export custom levels as JSON for backup/sharing + */ + public exportCustomLevels(): string { + const levelsArray: [string, LevelConfig][] = []; + + for (const [id, entry] of this.customLevels) { + if (entry.config) { + levelsArray.push([id, entry.config]); + } + } + + return JSON.stringify(levelsArray, null, 2); + } + + /** + * Import custom levels from JSON + */ + public importCustomLevels(jsonString: string): number { + try { + const levelsArray: [string, LevelConfig][] = JSON.parse(jsonString); + let importCount = 0; + + for (const [id, config] of levelsArray) { + this.saveCustomLevel(id, config); + importCount++; + } + + return importCount; + } catch (error) { + console.error('Failed to import custom levels:', error); + throw new Error('Invalid custom levels JSON format'); + } + } + + /** + * Get directory manifest + */ + public getDirectory(): LevelDirectory | null { + return this.directoryManifest; + } + + /** + * Check if registry is initialized + */ + public isInitialized(): boolean { + return this.initialized; + } +} diff --git a/src/levels/ui/levelSelector.ts b/src/levels/ui/levelSelector.ts index 98f46a2..f451341 100644 --- a/src/levels/ui/levelSelector.ts +++ b/src/levels/ui/levelSelector.ts @@ -1,59 +1,85 @@ -import { getSavedLevels } from "../generation/levelEditor"; -import { LevelConfig } from "../config/levelConfig"; -import { ProgressionManager } from "../../game/progression"; -import { GameConfig } from "../../core/gameConfig"; -import { AuthService } from "../../services/authService"; +import {LevelConfig} from "../config/levelConfig"; +import {ProgressionManager} from "../../game/progression"; +import {GameConfig} from "../../core/gameConfig"; +import {AuthService} from "../../services/authService"; import debugLog from '../../core/debug'; +import {LevelRegistry} from "../storage/levelRegistry"; +import {LevelVersionManager} from "../versioning/levelVersionManager"; +import {LevelStatsManager} from "../stats/levelStats"; const SELECTED_LEVEL_KEY = 'space-game-selected-level'; -// Default level order for the carousel +// Default level IDs in display order (matches directory.json) const DEFAULT_LEVEL_ORDER = [ - 'Rookie Training', - 'Rescue Mission', - 'Deep Space Patrol', - 'Enemy Territory', - 'The Gauntlet', - 'Final Challenge' + 'rookie-training', + 'rescue-mission', + 'deep-space-patrol', + 'enemy-territory', + 'the-gauntlet', + 'final-challenge' ]; /** - * Populate the level selection screen with saved levels + * Populate the level selection screen with levels from registry * Shows all 6 default levels in a 3x2 carousel with locked/unlocked states */ export async function populateLevelSelector(): Promise { + console.log('[LevelSelector] populateLevelSelector() called'); const container = document.getElementById('levelCardsContainer'); if (!container) { - console.warn('Level cards container not found'); + console.warn('[LevelSelector] Level cards container not found'); return false; } + console.log('[LevelSelector] Container found:', container); - const savedLevels = getSavedLevels(); - const gameConfig = GameConfig.getInstance(); - const progressionEnabled = gameConfig.progressionEnabled; - const progression = ProgressionManager.getInstance(); + const registry = LevelRegistry.getInstance(); + const versionManager = LevelVersionManager.getInstance(); + const statsManager = LevelStatsManager.getInstance(); - if (savedLevels.size === 0) { + // Initialize registry + try { + console.log('[LevelSelector] Initializing registry...'); + await registry.initialize(); + console.log('[LevelSelector] Registry initialized'); + } catch (error) { + console.error('[LevelSelector] Registry initialization error:', error); container.innerHTML = `
-

No Levels Found

-

Something went wrong - default levels should be auto-generated!

- Go to Level Editor +

Failed to Load Levels

+

Could not load level directory. Check your connection and try again.

+
`; return false; } - // Separate default and custom levels - const defaultLevels = new Map(); - const customLevels = new Map(); + const gameConfig = GameConfig.getInstance(); + const progressionEnabled = gameConfig.progressionEnabled; + const progression = ProgressionManager.getInstance(); - for (const [name, config] of savedLevels.entries()) { - if (config.metadata?.type === 'default') { - defaultLevels.set(name, config); - } else { - customLevels.set(name, config); - } + // Update version manager with directory + const directory = registry.getDirectory(); + if (directory) { + versionManager.updateManifestVersions(directory); + } + + const defaultLevels = registry.getDefaultLevels(); + const customLevels = registry.getCustomLevels(); + + console.log('[LevelSelector] Default levels:', defaultLevels.size); + console.log('[LevelSelector] Custom levels:', customLevels.size); + console.log('[LevelSelector] Default level IDs:', Array.from(defaultLevels.keys())); + + if (defaultLevels.size === 0 && customLevels.size === 0) { + console.warn('[LevelSelector] No levels found!'); + container.innerHTML = ` +
+

No Levels Found

+

No levels available. Please check your installation.

+ Create Custom Level +
+ `; + return false; } let html = ''; @@ -79,28 +105,26 @@ export async function populateLevelSelector(): Promise { `; } - // Check if user is authenticated (ASYNC!) + // Check if user is authenticated const authService = AuthService.getInstance(); const isAuthenticated = await authService.isAuthenticated(); - const isTutorial = (levelName: string) => levelName === DEFAULT_LEVEL_ORDER[0]; + const isTutorial = (levelId: string) => levelId === DEFAULT_LEVEL_ORDER[0]; debugLog('[LevelSelector] Authenticated:', isAuthenticated); debugLog('[LevelSelector] Progression enabled:', progressionEnabled); - debugLog('[LevelSelector] Tutorial level name:', DEFAULT_LEVEL_ORDER[0]); debugLog('[LevelSelector] Default levels count:', defaultLevels.size); - debugLog('[LevelSelector] Default level names:', Array.from(defaultLevels.keys())); - // Show all 6 default levels in order (3x2 grid) + // Show all default levels in order (3x2 grid) if (defaultLevels.size > 0) { - for (const levelName of DEFAULT_LEVEL_ORDER) { - const config = defaultLevels.get(levelName); + for (const levelId of DEFAULT_LEVEL_ORDER) { + const entry = defaultLevels.get(levelId); - if (!config) { + if (!entry) { // Level doesn't exist - show empty slot html += `
-

${levelName}

+

Missing Level

🔒
Level not found
@@ -111,57 +135,72 @@ export async function populateLevelSelector(): Promise { continue; } - const description = config.metadata?.description || `${config.asteroids.length} asteroids • ${config.planets.length} planets`; - const estimatedTime = config.metadata?.estimatedTime || ''; + const dirEntry = entry.directoryEntry; + const levelName = dirEntry.name; + const description = dirEntry.description; + const estimatedTime = dirEntry.estimatedTime || ''; + const difficulty = dirEntry.difficulty || 'unknown'; + + // Check for version updates + const hasUpdate = versionManager.hasUpdate(levelId); + + // Get stats + const stats = statsManager.getStats(levelId); + const completionRate = stats?.completionRate || 0; + const bestTime = stats?.bestTimeSeconds; + + // Check progression const isCompleted = progressionEnabled && progression.isLevelComplete(levelName); - // Check if level is unlocked: - // - Tutorial is always unlocked - // - If authenticated: check progression unlock status - // - If not authenticated: only Tutorial is unlocked + // Check if level is unlocked let isUnlocked = false; - const isTut = isTutorial(levelName); + const isTut = isTutorial(levelId); if (isTut) { isUnlocked = true; // Tutorial always unlocked - debugLog(`[LevelSelector] ${levelName}: Tutorial - always unlocked`); } else if (!isAuthenticated) { isUnlocked = false; // Non-tutorial levels require authentication - debugLog(`[LevelSelector] ${levelName}: Not authenticated - locked`); } else { isUnlocked = !progressionEnabled || progression.isLevelUnlocked(levelName); - debugLog(`[LevelSelector] ${levelName}: Authenticated - unlocked:`, isUnlocked); } const isCurrentNext = progressionEnabled && progression.getNextLevel() === levelName; // Determine card state let cardClasses = 'level-card'; - let statusIcon = ''; + let statusIcons = ''; let buttonText = 'Play Level'; let buttonDisabled = ''; let lockReason = ''; + let metaTags = ''; + + // Version update badge + if (hasUpdate) { + statusIcons += '
UPDATED
'; + } if (isCompleted) { cardClasses += ' level-card-completed'; - statusIcon = '
'; + statusIcons += '
'; buttonText = 'Replay'; } else if (isCurrentNext && isUnlocked) { cardClasses += ' level-card-current'; - statusIcon = '
START HERE
'; + statusIcons += '
START HERE
'; } else if (!isUnlocked) { cardClasses += ' level-card-locked'; - statusIcon = '
🔒
'; + statusIcons += '
🔒
'; // Determine why it's locked - if (!isAuthenticated && !isTutorial(levelName)) { + if (!isAuthenticated && !isTutorial(levelId)) { buttonText = 'Sign In Required'; lockReason = '
Sign in to unlock
'; } else if (progressionEnabled) { - const levelIndex = DEFAULT_LEVEL_ORDER.indexOf(levelName); + const levelIndex = DEFAULT_LEVEL_ORDER.indexOf(levelId); if (levelIndex > 0) { - const previousLevel = DEFAULT_LEVEL_ORDER[levelIndex - 1]; - lockReason = `
Complete "${previousLevel}" to unlock
`; + const prevId = DEFAULT_LEVEL_ORDER[levelIndex - 1]; + const prevEntry = defaultLevels.get(prevId); + const prevName = prevEntry?.directoryEntry.name || 'previous level'; + lockReason = `
Complete "${prevName}" to unlock
`; } buttonText = 'Locked'; } else { @@ -170,18 +209,35 @@ export async function populateLevelSelector(): Promise { buttonDisabled = ' disabled'; } + // Show stats if available + if (stats && stats.totalAttempts > 0) { + metaTags = '
'; + if (bestTime) { + metaTags += `⏱️ ${LevelStatsManager.formatTime(bestTime)}`; + } + if (stats.totalCompletions > 0) { + metaTags += `✓ ${stats.totalCompletions}`; + } + metaTags += `${LevelStatsManager.formatCompletionRate(completionRate)}`; + metaTags += '
'; + } + html += `

${levelName}

- ${statusIcon} +
${statusIcons}
- Difficulty: ${config.difficulty}${estimatedTime ? ` • ${estimatedTime}` : ''} + Difficulty: ${difficulty}${estimatedTime ? ` • ${estimatedTime}` : ''}

${description}

+ ${metaTags} ${lockReason} - +
+ + ${entry.isDefault && isUnlocked ? `` : ''} +
`; } @@ -195,68 +251,165 @@ export async function populateLevelSelector(): Promise {
`; - for (const [name, config] of customLevels.entries()) { - const description = config.metadata?.description || `${config.asteroids.length} asteroids • ${config.planets.length} planets`; + for (const [levelId, entry] of customLevels.entries()) { + const config = entry.config; + if (!config) continue; + + const description = config.metadata?.description || `${config.asteroids.length} asteroids`; const author = config.metadata?.author ? ` by ${config.metadata.author}` : ''; + const difficulty = config.difficulty || 'custom'; + + // Get stats + const stats = statsManager.getStats(levelId); + const bestTime = stats?.bestTimeSeconds; + let metaTags = ''; + + if (stats && stats.totalAttempts > 0) { + metaTags = '
'; + if (bestTime) { + metaTags += `⏱️ ${LevelStatsManager.formatTime(bestTime)}`; + } + if (stats.totalCompletions > 0) { + metaTags += `✓ ${stats.totalCompletions}`; + } + metaTags += '
'; + } html += `
-

${name}

+

${levelId}

+
CUSTOM
- Custom${author} • ${config.difficulty} + ${difficulty}${author}

${description}

- + ${metaTags} +
+ + +
`; } } + console.log('[LevelSelector] Setting container innerHTML, html length:', html.length); container.innerHTML = html; + console.log('[LevelSelector] Container innerHTML set, now attaching event listeners'); // Attach event listeners to all level buttons - const buttons = container.querySelectorAll('.level-button:not([disabled])'); - buttons.forEach(button => { + const playButtons = container.querySelectorAll('.level-button:not([disabled])'); + playButtons.forEach(button => { button.addEventListener('click', (e) => { const target = e.target as HTMLButtonElement; - const levelName = target.getAttribute('data-level'); - if (levelName) { - selectLevel(levelName); + const levelId = target.getAttribute('data-level-id'); + if (levelId) { + selectLevel(levelId); } }); }); + // Attach copy button listeners + const copyButtons = container.querySelectorAll('[data-copy-level]'); + copyButtons.forEach(button => { + button.addEventListener('click', async (e) => { + const target = e.target as HTMLButtonElement; + const levelId = target.getAttribute('data-copy-level'); + if (levelId) { + await copyLevelToCustom(levelId); + } + }); + }); + + // Attach delete button listeners + const deleteButtons = container.querySelectorAll('[data-delete-level]'); + deleteButtons.forEach(button => { + button.addEventListener('click', (e) => { + const target = e.target as HTMLButtonElement; + const levelId = target.getAttribute('data-delete-level'); + if (levelId) { + deleteCustomLevel(levelId); + } + }); + }); + + console.log('[LevelSelector] Event listeners attached, returning true'); + + // Make the level selector visible by adding 'ready' class + const levelSelectDiv = document.getElementById('levelSelect'); + if (levelSelectDiv) { + levelSelectDiv.classList.add('ready'); + console.log('[LevelSelector] Added "ready" class to #levelSelect'); + } + return true; } +/** + * Copy a default level to custom levels + */ +async function copyLevelToCustom(levelId: string): Promise { + const registry = LevelRegistry.getInstance(); + const customName = prompt(`Enter a name for your copy of this level:`, `${levelId}-copy`); + + if (!customName || customName.trim() === '') { + return; + } + + const success = await registry.copyDefaultToCustom(levelId, customName); + + if (success) { + alert(`Level copied as "${customName}"!`); + await populateLevelSelector(); // Refresh UI + } else { + alert('Failed to copy level. Please try again.'); + } +} + +/** + * Delete a custom level + */ +function deleteCustomLevel(levelId: string): void { + if (!confirm(`Are you sure you want to delete "${levelId}"?`)) { + return; + } + + const registry = LevelRegistry.getInstance(); + const success = registry.deleteCustomLevel(levelId); + + if (success) { + populateLevelSelector(); // Refresh UI + } +} + /** * Select a level and dispatch event to start it */ -export function selectLevel(levelName: string): void { - debugLog(`[LevelSelector] Level selected: ${levelName}`); +export async function selectLevel(levelId: string): Promise { + debugLog(`[LevelSelector] Level selected: ${levelId}`); - const savedLevels = getSavedLevels(); - const config = savedLevels.get(levelName); + const registry = LevelRegistry.getInstance(); + const config = await registry.getLevel(levelId); if (!config) { - console.error(`Level not found: ${levelName}`); + console.error(`Level not found: ${levelId}`); return; } // Save selected level - localStorage.setItem(SELECTED_LEVEL_KEY, levelName); + localStorage.setItem(SELECTED_LEVEL_KEY, levelId); // Dispatch custom event that Main class will listen for const event = new CustomEvent('levelSelected', { - detail: { levelName, config } + detail: {levelId, config} }); window.dispatchEvent(event); } /** - * Get the last selected level name + * Get the last selected level ID */ export function getSelectedLevel(): string | null { return localStorage.getItem(SELECTED_LEVEL_KEY); diff --git a/src/levels/versioning/levelVersionManager.ts b/src/levels/versioning/levelVersionManager.ts new file mode 100644 index 0000000..fff1892 --- /dev/null +++ b/src/levels/versioning/levelVersionManager.ts @@ -0,0 +1,262 @@ +import {LevelDirectory, LevelDirectoryEntry} from "../storage/levelRegistry"; + +/** + * Tracked version information for a level + */ +export interface LevelVersionInfo { + levelId: string; + loadedVersion: string; + loadedAt: Date; + manifestVersion?: string; // Latest version from directory +} + +/** + * Version comparison result + */ +export interface VersionComparison { + levelId: string; + currentVersion: string; + latestVersion: string; + isOutdated: boolean; + changelog?: string; +} + +const VERSION_STORAGE_KEY = 'space-game-level-versions'; + +/** + * Manages level version tracking and update detection + */ +export class LevelVersionManager { + private static instance: LevelVersionManager | null = null; + + private versionMap: Map = new Map(); + + private constructor() { + this.loadVersions(); + } + + public static getInstance(): LevelVersionManager { + if (!LevelVersionManager.instance) { + LevelVersionManager.instance = new LevelVersionManager(); + } + return LevelVersionManager.instance; + } + + /** + * Load version tracking from localStorage + */ + private loadVersions(): void { + const stored = localStorage.getItem(VERSION_STORAGE_KEY); + if (!stored) { + return; + } + + try { + const versionsArray: [string, LevelVersionInfo][] = JSON.parse(stored); + + for (const [id, info] of versionsArray) { + // Parse date string back to Date object + if (info.loadedAt && typeof info.loadedAt === 'string') { + info.loadedAt = new Date(info.loadedAt); + } + this.versionMap.set(id, info); + } + } catch (error) { + console.error('Failed to load level versions:', error); + } + } + + /** + * Save version tracking to localStorage + */ + private saveVersions(): void { + const versionsArray = Array.from(this.versionMap.entries()); + localStorage.setItem(VERSION_STORAGE_KEY, JSON.stringify(versionsArray)); + } + + /** + * Record that a level was loaded with a specific version + */ + public recordLevelLoaded(levelId: string, version: string): void { + const info: LevelVersionInfo = { + levelId, + loadedVersion: version, + loadedAt: new Date() + }; + + this.versionMap.set(levelId, info); + this.saveVersions(); + } + + /** + * Update manifest versions from directory + */ + public updateManifestVersions(directory: LevelDirectory): void { + for (const entry of directory.levels) { + const existing = this.versionMap.get(entry.id); + if (existing) { + existing.manifestVersion = entry.version; + } else { + // First time seeing this level + this.versionMap.set(entry.id, { + levelId: entry.id, + loadedVersion: '', // Not yet loaded + loadedAt: new Date(), + manifestVersion: entry.version + }); + } + } + + this.saveVersions(); + } + + /** + * Check if a level has an update available + */ + public hasUpdate(levelId: string): boolean { + const info = this.versionMap.get(levelId); + if (!info || !info.manifestVersion || !info.loadedVersion) { + return false; + } + + return this.compareVersions(info.loadedVersion, info.manifestVersion) < 0; + } + + /** + * Get version comparison for a level + */ + public getVersionComparison(levelId: string): VersionComparison | null { + const info = this.versionMap.get(levelId); + if (!info || !info.manifestVersion) { + return null; + } + + const currentVersion = info.loadedVersion || '0.0'; + const latestVersion = info.manifestVersion; + const isOutdated = this.compareVersions(currentVersion, latestVersion) < 0; + + return { + levelId, + currentVersion, + latestVersion, + isOutdated + }; + } + + /** + * Get all levels with available updates + */ + public getUpdatableLevels(): VersionComparison[] { + const updatable: VersionComparison[] = []; + + for (const [levelId, info] of this.versionMap) { + if (info.manifestVersion && info.loadedVersion) { + const comparison = this.getVersionComparison(levelId); + if (comparison && comparison.isOutdated) { + updatable.push(comparison); + } + } + } + + return updatable; + } + + /** + * Get version info for a level + */ + public getVersionInfo(levelId: string): LevelVersionInfo | undefined { + return this.versionMap.get(levelId); + } + + /** + * Mark a level as updated (user accepted the new version) + */ + public markAsUpdated(levelId: string, newVersion: string): void { + const info = this.versionMap.get(levelId); + if (info) { + info.loadedVersion = newVersion; + info.loadedAt = new Date(); + this.saveVersions(); + } + } + + /** + * Compare two semantic version strings + * Returns: -1 if v1 < v2, 0 if equal, 1 if v1 > v2 + */ + private compareVersions(v1: string, v2: string): number { + const parts1 = v1.split('.').map(Number); + const parts2 = v2.split('.').map(Number); + + const maxLength = Math.max(parts1.length, parts2.length); + + for (let i = 0; i < maxLength; i++) { + const part1 = parts1[i] || 0; + const part2 = parts2[i] || 0; + + if (part1 < part2) return -1; + if (part1 > part2) return 1; + } + + return 0; + } + + /** + * Clear all version tracking (for testing/reset) + */ + public clearAll(): void { + this.versionMap.clear(); + localStorage.removeItem(VERSION_STORAGE_KEY); + } + + /** + * Get summary of version statuses + */ + public getVersionSummary(): { + total: number; + tracked: number; + updatable: number; + upToDate: number; + } { + let tracked = 0; + let updatable = 0; + let upToDate = 0; + + for (const info of this.versionMap.values()) { + if (info.loadedVersion) { + tracked++; + + if (info.manifestVersion) { + if (this.compareVersions(info.loadedVersion, info.manifestVersion) < 0) { + updatable++; + } else { + upToDate++; + } + } + } + } + + return { + total: this.versionMap.size, + tracked, + updatable, + upToDate + }; + } + + /** + * Build changelog text for version updates + */ + public static buildChangelog(directoryEntry: LevelDirectoryEntry): string { + // In the future, this could fetch from a changelog file or API + // For now, generate a simple message + return `Level updated to version ${directoryEntry.version}. Check for improvements and changes!`; + } + + /** + * Check if this is the first time loading any levels + */ + public isFirstRun(): boolean { + return this.versionMap.size === 0; + } +} diff --git a/src/main.ts b/src/main.ts index d316719..6fbe07f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -25,7 +25,8 @@ import {ControllerDebug} from "./utils/controllerDebug"; import {router, showView} from "./core/router"; import {populateLevelSelector} from "./levels/ui/levelSelector"; import {LevelConfig} from "./levels/config/levelConfig"; -import {generateDefaultLevels} from "./levels/generation/levelEditor"; +import {LegacyMigration} from "./levels/migration/legacyMigration"; +import {LevelRegistry} from "./levels/storage/levelRegistry"; import debugLog from './core/debug'; import {ReplaySelectionScreen} from "./replay/ReplaySelectionScreen"; import {ReplayManager} from "./replay/ReplayManager"; @@ -674,8 +675,43 @@ router.on('/settings', () => { } }); -// Generate default levels if localStorage is empty -generateDefaultLevels(); +// Initialize registry and start router +// This must happen BEFORE router.start() so levels are available +async function initializeApp() { + // Check for legacy data migration + if (LegacyMigration.needsMigration()) { + debugLog('[Main] Legacy data detected - showing migration modal'); + return new Promise((resolve) => { + LegacyMigration.showMigrationModal(async (result) => { + debugLog('[Main] Migration completed:', result); + // Initialize the new registry system + try { + await LevelRegistry.getInstance().initialize(); + debugLog('[Main] LevelRegistry initialized after migration'); + router.start(); + resolve(); + } catch (error) { + console.error('[Main] Failed to initialize LevelRegistry after migration:', error); + router.start(); // Start anyway to show error state + resolve(); + } + }); + }); + } else { + // Initialize the new registry system + try { + await LevelRegistry.getInstance().initialize(); + debugLog('[Main] LevelRegistry initialized'); + router.start(); + } catch (error) { + console.error('[Main] Failed to initialize LevelRegistry:', error); + router.start(); // Start anyway to show error state + } + } +} + +// Start the app +initializeApp(); // Suppress non-critical BabylonJS shader loading errors during development // Note: After Vite config fix to pre-bundle shaders, these errors should no longer occur @@ -694,8 +730,7 @@ window.addEventListener('unhandledrejection', (event) => { } }); -// Start the router after all routes are registered -router.start(); +// DO NOT start router here - it will be started after registry initialization below if (DEBUG_CONTROLLERS) { debugLog('🔍 DEBUG MODE: Running minimal controller test'); diff --git a/src/ui/hud/statusScreen.ts b/src/ui/hud/statusScreen.ts index 70cc25b..73248d3 100644 --- a/src/ui/hud/statusScreen.ts +++ b/src/ui/hud/statusScreen.ts @@ -165,7 +165,7 @@ export class StatusScreen { buttonBar.addControl(this._resumeButton); // Create Next Level button (only shown when game has ended and there's a next level) - this._nextLevelButton = Button.CreateSimpleButton("nextLevelButton", "NEXT LEVEL"); + /*this._nextLevelButton = Button.CreateSimpleButton("nextLevelButton", "NEXT LEVEL"); this._nextLevelButton.width = "300px"; this._nextLevelButton.height = "60px"; this._nextLevelButton.color = "white"; @@ -196,10 +196,10 @@ export class StatusScreen { this._onReplayCallback(); } }); - buttonBar.addControl(this._replayButton); + buttonBar.addControl(this._replayButton);*/ // Create Exit VR button - this._exitButton = Button.CreateSimpleButton("exitButton", "EXIT VR"); + this._exitButton = Button.CreateSimpleButton("exitButton", "EXIT"); this._exitButton.width = "300px"; this._exitButton.height = "60px"; this._exitButton.color = "white"; diff --git a/themes/default/base2.blend b/themes/default/base2.blend new file mode 100644 index 0000000..31df4bd Binary files /dev/null and b/themes/default/base2.blend differ