├── .gitignore ├── README.md ├── docs ├── 22-10-30.png ├── ExoplanetPopulations-20170616.png ├── SmallPlanetsComeInTwoSizes-20170619.png ├── demo-client-js-1.png ├── demo-galaxy-svg-1.png ├── demo-planet-r3f-1.png └── original-experilous-planet-generator.js ├── example └── galaxy.js ├── jest.config.ts ├── package.json ├── project.json ├── src ├── _generators │ ├── Planet │ │ ├── Planet3DGenerator.js │ │ ├── Planet3DGenerator.spec_.js │ │ ├── PlanetConstants.js │ │ ├── PlanetGenerator.js │ │ └── helpers │ │ │ └── functions.js │ └── generators │ │ ├── CelestialObject.ts │ │ ├── Generator.js │ │ ├── PlanetarySubsystem.js │ │ ├── PlanetarySystem.js │ │ ├── Region.js │ │ └── SystemTS.tsx ├── galaxy-shape │ ├── BasicShape.ts │ ├── Cluster.ts │ ├── Grid.ts │ ├── ShapeStar.ts │ ├── Sphere.ts │ ├── Spiral.ts │ └── index.ts ├── galaxy │ ├── basic-generator.ts │ ├── galaxy-generator.ts │ ├── index.ts │ ├── physic │ │ ├── index.ts │ │ ├── orbit-generator.ts │ │ ├── orbit-physic.ts │ │ ├── planet-physic.ts │ │ ├── star-physic.ts │ │ └── system-physic.ts │ ├── planet │ │ ├── index.ts │ │ ├── planet-generator.model.ts │ │ ├── planet-generator.spec.ts │ │ ├── planet-generator.ts │ │ ├── planet-surface-generator.ts │ │ ├── strategy │ │ │ ├── index.ts │ │ │ └── planet-surface-strategy.ts │ │ └── surface │ │ │ ├── builders │ │ │ ├── icosahedron-builder.ts │ │ │ ├── planet-mesh-builder.ts │ │ │ ├── planet-partition-builder.ts │ │ │ └── planet-topology-builder.ts │ │ │ ├── index.ts │ │ │ ├── planet-surface-generator.ts │ │ │ ├── surface-strategy │ │ │ ├── biome-surface-modificator.ts │ │ │ ├── planet-weather-modificator.ts │ │ │ ├── surface-modificator.ts │ │ │ └── terrain-surface-modificator.ts │ │ │ ├── surface.types.ts │ │ │ └── utils.ts │ ├── star │ │ ├── index.ts │ │ ├── star-generator.spec.ts │ │ ├── star-generator.ts │ │ └── types.ts │ └── system │ │ ├── debris-belt-generator.ts │ │ ├── empty-zone.ts │ │ ├── index.ts │ │ ├── system-generator.spec.ts │ │ ├── system-generator.ts │ │ ├── system-orbits-generator.ts │ │ └── types.ts ├── global.types.ts ├── index.ts ├── interfaces │ └── index.ts ├── resources │ ├── 1002-geometry.json │ ├── 1002-planet-object.json │ ├── 1002-topology.json │ ├── 4412-geometry.json │ ├── 4412-planet-object.json │ ├── 4412-topology.json │ ├── GALAXIES_NAMES.ts │ └── STARS_NAMES.ts └── utils │ ├── MarkovNames │ ├── MarkovModel.ts │ ├── MarkovModelBuilder.ts │ ├── README.md │ └── index.ts │ ├── Names.ts │ ├── RandomObject.ts │ ├── StarName.ts │ ├── SteppedAction.js │ ├── XorShift128.ts │ ├── alphabet.spec.ts │ ├── alphabet.ts │ ├── assignDeep.ts │ ├── index.ts │ ├── math.spec.ts │ ├── math.ts │ ├── random.ts │ ├── star-name.spec.ts │ ├── weighted-random.ts │ ├── world-path.spec.ts │ └── world-path.ts ├── tsconfig.cjs.json ├── tsconfig.esm.json ├── tsconfig.json └── tsconfig.types.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @xenocide/world-generator 2 | 3 | _"Dear World, You are the best for Us!"_ - Artifexian 4 | TypeScript world generator for 4X like games in 3D space 5 | 6 | > In heavy development mess xD 7 | > 8 | > For [CodeSandbox example](https://codesandbox.io/s/1c8gs), use git source with tag `prealpha_0.0.2-rc.1`, package.json example: 9 | > 10 | > ```json 11 | > "dependencies": { 12 | > "xenocide-world-generator": "https://github.com/duchu-net/xenocide-world-generator@prealpha_0.0.2-rc.1", 13 | > } 14 | > ``` 15 | > 16 | > ~~For now generator is developed as part of a private project (galaxy simulator with [nx](https://nx.dev/)), 17 | > so currently without plans to release as a separate bundle (but can be used as git submodule).~~ 18 | 19 | # @xenocide/world-generator available on NPM and with TS from 0.0.4! 20 | 21 | ![Image](./docs/22-10-30.png) 22 | 23 | ## Install 24 | 25 | ```bash 26 | npm i @xenocide/world-generator@0.0.4 27 | ``` 28 | 29 | ### Usage 30 | 31 | ```ts 32 | const { GalaxyGenerator, GalaxyClass } = require('@xenocide/world-generator'); 33 | // or 34 | import { GalaxyGenerator, GalaxyClass } from '@xenocide/world-generator'; 35 | ``` 36 | 37 | Supports ESM and CommonJS modules. 38 | 39 | ### Examples 40 | 41 | New examples coming soon... 42 | 43 | 44 | 45 | 46 | 47 | 52 | 53 | 54 | 55 | 60 | 61 | 62 | 63 | 68 | 69 | 70 | 71 | 74 | 75 | 76 |
Galaxy generator with React and SVG (live demo). 48 | 49 | 50 | 51 |
Planet generator with React and @react-three/fiber (live demo). 56 | 57 | 58 | 59 |
[*alpha JS version] Full featured generator demo with React, Redux and react-three-fiber (live demo, video). 64 | 65 | 66 | 67 |
[*alpha JS version] Name generation with Markov Chains. 72 | live demo 73 |
77 | 78 | ### Code 79 | 80 | ```js 81 | import { GalaxyGenerator, GalaxyClass, PlanetGenerator } from '@xenocide/world-generator'; 82 | 83 | // Spiral shape 84 | const world = new GalaxyGenerator( 85 | { id: 'demo', classification: GalaxyClass.Spiral }, 86 | { seed: 123, spiral: { size: 500 } } 87 | ); 88 | // Grid shape 89 | const world = new GalaxyGenerator( 90 | { id: 'demo', classification: GalaxyClass.Grid }, 91 | { seed: 123, grid: { size: 15, spacing: 5 } } 92 | ); 93 | 94 | const systems = []; 95 | for (const system of world.generateSystems()) { 96 | systems.push(system.path); 97 | 98 | for (const star of system.generateStars()) { 99 | // console.log('*** Star generated:', star.path, star.toModel()); 100 | } 101 | 102 | /** don't generate too much for tests */ 103 | if (systems.length <= 3) { 104 | for (const planet of system.generatePlanets()) { 105 | // console.log('*** Planet/Belt/Etc. generated:', planet.path, planet.toModel()); 106 | if (planet instanceof PlanetGenerator) { 107 | for (const region of planet.generateSurface()) { 108 | // console.log('**** Regions generated:', region.path); 109 | } 110 | } 111 | } 112 | } 113 | } 114 | 115 | // Get Plain Object 116 | const model = world.toModel(); 117 | ``` 118 | 119 | ## Inspired by 120 | 121 | - [mainly] [Procedural Planet Generation](https://experilous.com/1/blog/post/procedural-planet-generation) by _Andy Gainey_ 122 | - [Star-Citizen-WebGL-Map](https://github.com/Leeft/Star-Citizen-WebGL-Map) by _Lianna Eeftinck_ 123 | - [Artifexian (YouTube)](https://www.youtube.com/user/Artifexian) by _Edgar Grunewald_ 124 | - [Procedural Generation For Dummies](http://martindevans.me/game-development/2016/01/14/Procedural-Generation-For-Dummies-Galaxies/) by _Martin Evans_ 125 | - [X game series](https://www.egosoft.com/games/x4/info_en.php) by Egosoft 126 | 127 | ### Other links 128 | 129 | - [4X games (Wikipedia)](https://en.wikipedia.org/wiki/4X) 130 | - [Stellar classification (Wikipedia)](https://en.wikipedia.org/wiki/Stellar_classification) 131 | - [Holdridge life zones (Wikipedia)](https://en.wikipedia.org/wiki/Holdridge_life_zones) 132 | - [Planet IX](https://planetix.com/) 133 | - [Prosperous Universe](https://prosperousuniverse.com/) 134 | - [Astro Empires](https://www.astroempires.com/) 135 | 136 | ## todo 137 | 138 | - [x] own tsconfig build, not from nx 139 | -------------------------------------------------------------------------------- /docs/22-10-30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duchu-net/xenocide-world-generator/497bedaa03e9a247836050b03b1a13a58e8cb119/docs/22-10-30.png -------------------------------------------------------------------------------- /docs/ExoplanetPopulations-20170616.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duchu-net/xenocide-world-generator/497bedaa03e9a247836050b03b1a13a58e8cb119/docs/ExoplanetPopulations-20170616.png -------------------------------------------------------------------------------- /docs/SmallPlanetsComeInTwoSizes-20170619.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duchu-net/xenocide-world-generator/497bedaa03e9a247836050b03b1a13a58e8cb119/docs/SmallPlanetsComeInTwoSizes-20170619.png -------------------------------------------------------------------------------- /docs/demo-client-js-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duchu-net/xenocide-world-generator/497bedaa03e9a247836050b03b1a13a58e8cb119/docs/demo-client-js-1.png -------------------------------------------------------------------------------- /docs/demo-galaxy-svg-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duchu-net/xenocide-world-generator/497bedaa03e9a247836050b03b1a13a58e8cb119/docs/demo-galaxy-svg-1.png -------------------------------------------------------------------------------- /docs/demo-planet-r3f-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duchu-net/xenocide-world-generator/497bedaa03e9a247836050b03b1a13a58e8cb119/docs/demo-planet-r3f-1.png -------------------------------------------------------------------------------- /example/galaxy.js: -------------------------------------------------------------------------------- 1 | const { Galaxy } = require('../lib/library') 2 | 3 | // const galaxy = new Galaxy() // Random shape 4 | const galaxy = new Galaxy({ classification: 'grid', buildData: { gridOptions: [100, 30] } }) // Grid shape with [size, spacing] 5 | // const galaxy = new Galaxy({ classification: 'spiral' }) // Spiral shape 6 | 7 | console.log('*** Galaxy generated:', galaxy.name, galaxy.code) 8 | for (let system of galaxy.generateSystems()) { 9 | // await system.build() 10 | console.log('** System generated:', system.name, system.code) 11 | for (let star of system.generateStars()) { 12 | console.log('* Star generated:', star.designation, star.code) 13 | } 14 | for (let planet of system.generatePlanets()) { 15 | console.log('* Planet generated:', planet.designation, planet.code) 16 | } 17 | } 18 | console.log(galaxy.statistics) 19 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | displayName: 'world-generator', 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | }; 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@xenocide/world-generator", 3 | "version": "0.0.4", 4 | "license": "ISC", 5 | "author": "Patryk Androsiuk (http://duchu.net)", 6 | "homepage": "https://github.com/duchu-net/xenocide-world-generator#readme", 7 | "repository": "git://github.com/duchu-net/xenocide-world-generator.git", 8 | "bugs": { 9 | "url": "https://github.com/duchu-net/xenocide-world-generator/issues" 10 | }, 11 | "main": "./dist/cjs/index.js", 12 | "module": "./dist/esm/index.js", 13 | "types": "./dist/types/index.d.ts", 14 | "files": [ 15 | "dist/" 16 | ], 17 | "scripts": { 18 | "build": "tsc -build tsconfig.cjs.json tsconfig.esm.json tsconfig.types.json", 19 | "prepublishOnly": "npm test && npm run build", 20 | "test": "jest" 21 | }, 22 | "nx": { 23 | "targets": { 24 | "test": { 25 | "dependsOn": [ 26 | "build" 27 | ] 28 | } 29 | } 30 | }, 31 | "dependencies": { 32 | "three": "^0.168.0" 33 | }, 34 | "devDependencies": { 35 | "@types/jest": "^29.5.13", 36 | "@types/three": "^0.168.0", 37 | "jest": "^29.7.0", 38 | "ts-jest": "^29.2.5", 39 | "typescript": "^5.6.2" 40 | } 41 | } -------------------------------------------------------------------------------- /project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "world-generator", 3 | "projectType": "library" 4 | } 5 | -------------------------------------------------------------------------------- /src/_generators/Planet/Planet3DGenerator.spec_.js: -------------------------------------------------------------------------------- 1 | // import expect from 'expect' 2 | import assert from 'assert' 3 | import Planet3DGenerator from './Planet3DGenerator' 4 | import SteppedAction from '../../utils/SteppedAction' 5 | var fs = require('fs'); 6 | 7 | 8 | 9 | describe('Planet3DGenerator: model', () => { 10 | describe('Planet asynchronous', () => { 11 | it ('should generate 10 planets', (done) => { 12 | const action = new SteppedAction() 13 | action.progressUpdater = (p) => { 14 | console.log(p.getCurrentActionName(), (p.getProgress() * 100).toFixed(2) + '%') 15 | } 16 | 17 | // const planet1 = new Planet3DGenerator() 18 | // const planet2 = new Planet3DGenerator() 19 | // const planet3 = new Planet3DGenerator() 20 | // const planet4 = new Planet3DGenerator() 21 | // const planet5 = new Planet3DGenerator() 22 | // const planet6 = new Planet3DGenerator() 23 | // const planet7 = new Planet3DGenerator() 24 | // const planet8 = new Planet3DGenerator() 25 | // const planet9 = new Planet3DGenerator() 26 | const planet10 = new Planet3DGenerator({ 27 | generationSettings: { 28 | subdivisions: 21 29 | } 30 | }) 31 | 32 | action 33 | // .executeSubaction((action) => { 34 | // planet1.generatePlanetAsynchronous(action) 35 | // }, 1) 36 | // .getResult((result) => { 37 | // console.log('getResult1') 38 | // expect.objectContaining(result) 39 | // }) 40 | // 41 | // .executeSubaction((action) => { 42 | // planet2.generatePlanetAsynchronous(action) 43 | // }, 1) 44 | // .getResult((result) => { 45 | // console.log('getResult2') 46 | // expect.objectContaining(result) 47 | // }) 48 | // 49 | // .executeSubaction((action) => { 50 | // planet3.generatePlanetAsynchronous(action) 51 | // }, 1) 52 | // .getResult((result) => { 53 | // console.log('getResult3') 54 | // expect.objectContaining(result) 55 | // }) 56 | // 57 | // .executeSubaction((action) => { 58 | // planet4.generatePlanetAsynchronous(action) 59 | // }, 1) 60 | // .getResult((result) => { 61 | // console.log('getResult4') 62 | // expect.objectContaining(result) 63 | // }) 64 | // 65 | // // 5 ------------------------------------------------------------------- 66 | // .executeSubaction((action) => { 67 | // planet5.generatePlanetAsynchronous(action) 68 | // }, 1) 69 | // .getResult((result) => { 70 | // console.log('getResult5') 71 | // expect.objectContaining(result) 72 | // }) 73 | // 74 | // .executeSubaction((action) => { 75 | // planet6.generatePlanetAsynchronous(action) 76 | // }, 1) 77 | // .getResult((result) => { 78 | // console.log('getResult6') 79 | // expect.objectContaining(result) 80 | // }) 81 | // 82 | // // 7 ------------------------------------------------------------------- 83 | // .executeSubaction((action) => { 84 | // planet7.generatePlanetAsynchronous(action) 85 | // }, 1) 86 | // .getResult((result) => { 87 | // console.log('getResult7') 88 | // expect.objectContaining(result) 89 | // }) 90 | // 91 | // .executeSubaction((action) => { 92 | // planet8.generatePlanetAsynchronous(action) 93 | // }, 1) 94 | // .getResult((result) => { 95 | // console.log('getResult8') 96 | // expect.objectContaining(result) 97 | // }) 98 | // 99 | // .executeSubaction((action) => { 100 | // planet9.generatePlanetAsynchronous(action) 101 | // }, 1) 102 | // .getResult((result) => { 103 | // console.log('getResult9') 104 | // expect.objectContaining(result) 105 | // }) 106 | 107 | // 10 ------------------------------------------------------------------ 108 | .executeSubaction((action) => { 109 | planet10.generatePlanetAsynchronous(action) 110 | }, 1) 111 | .getResult((result) => { 112 | // console.log('getResult10', Object.keys(result), Object.keys(result.renderData.surface)) 113 | const length = result.topology.tiles.length 114 | // var json = JSON.stringify({ 115 | // // mesh: result.mesh, 116 | // topology: result.topology, 117 | // partition: result.partition 118 | // }, 0, 2); 119 | // fs.writeFile(`tiles-${length}.json`, json, 'utf8', () => console.log('save done!')); 120 | var jsonTopology = JSON.stringify(result.topology); 121 | fs.writeFile(`${length}-topology.json`, jsonTopology, 'utf8', () => console.log('save done!')); 122 | 123 | var jsonGeometry = JSON.stringify(result.renderData.surface.geometry); 124 | fs.writeFile(`${length}-geometry.json`, jsonGeometry, 'utf8', () => console.log('save done!')) 125 | var jsonPlanetObject = JSON.stringify(result.renderData.surface.renderObject); 126 | fs.writeFile(`${length}-planet-object.json`, jsonPlanetObject, 'utf8', () => console.log('save done!')) 127 | // var jsonMapObject = JSON.stringify(result.renderData.surface.renderObject); 128 | // fs.writeFile(`${length}map-object.json`, jsonMapObject, 'utf8', () => console.log('save done!')) 129 | // expect.objectContaining(result) 130 | assert.ok(typeof result == 'object') 131 | }) 132 | 133 | // FINALIZE ------------------------------------------------------------ 134 | .finalize((action) => { 135 | console.warn('FINISH!!! :D'); 136 | done() 137 | }, 0) 138 | .execute() 139 | 140 | // Promise.resolve() 141 | // .then(() => planet.generatePlanetAsynchronous(action)) 142 | // .then((result) => { 143 | // // console.log('res', result) 144 | // console.log('res') 145 | // }) 146 | }).timeout(60000) 147 | }) 148 | 149 | }) 150 | -------------------------------------------------------------------------------- /src/_generators/Planet/PlanetConstants.js: -------------------------------------------------------------------------------- 1 | // const defaultsForMatrices = { 2 | // detail_level: { min: 12, max: 22 }, 3 | // distortion_level: { min: 40, max: 60 }, 4 | // plate_count: { min: 3, max: 20 }, 5 | // oceanic_rate: { min: 10, max: 90 }, 6 | // heat_level: { min: -50, max: 50 }, 7 | // moisture_level: { min: 50, max: 50 }, 8 | // } 9 | 10 | module.exports = { 11 | TYPES_DISTRIBUTION: { 12 | ocean: 3, 13 | earth: 9, 14 | jungle: 3, 15 | desert: 3, 16 | arctic: 4, 17 | gas_methan: 3, 18 | }, 19 | DEFAULT_MATRICE: { 20 | detail_level: { min: 12, max: 22 }, 21 | distortion_level: { min: 40, max: 60 }, 22 | plate_count: { min: 3, max: 20 }, 23 | oceanic_rate: { min: 10, max: 90 }, 24 | heat_level: { min: -50, max: 50 }, 25 | moisture_level: { min: 50, max: 50 }, 26 | }, 27 | PLANETS_MATRICES: [ 28 | // JUNGLE -------------------------------------------------------------------- 29 | { 30 | // ...defaultsForMatrices, 31 | type: 'jungle', 32 | weight: 3, 33 | inner: false, 34 | habit: true, 35 | outer: false, 36 | moon: true, 37 | detail_level: {min: 12, max: 22}, //4-100 38 | distortion_level: {min: 40, max: 60}, //0-100: 0-hexagon tile, 100-different tile 39 | plate_count: {min: 3, max: 20}, //2-1000: continents, or islands 40 | oceanic_rate: {min: 10, max: 30}, //0-100: water faction 41 | heat_level: {min: -50, max: 50}, //-100-100: glaciel size, -100 small 42 | moisture_level: {min: 50, max: 100}, //-100-100: desert, or jungle 43 | }, 44 | // LAVA ---------------------------------------------------------------------- 45 | // { 46 | // type: 'lava', weight: 4, 47 | // inner: true, habit: true, outer: false, moon: true 48 | // }, 49 | // DESERT -------------------------------------------------------------------- 50 | { 51 | // ...defaultsForMatrices, 52 | type: 'desert', 53 | weight: 3, 54 | inner: true, 55 | habit: true, 56 | outer: false, 57 | moon: true, 58 | oceanic_rate: {min: 0, max: 5}, 59 | heat_level: {min: 50, max: 100}, 60 | moisture_level: {min: 0, max: 50}, 61 | }, 62 | // BARREN -------------------------------------------------------------------- 63 | // { 64 | // type: 'barren', weight: 6, 65 | // inner: true, habit: true, outer: true, moon: true 66 | // }, 67 | // ARCTIC -------------------------------------------------------------------- 68 | { 69 | // ...defaultsForMatrices, 70 | type: 'arctic', 71 | weight: 4, 72 | inner: false, 73 | habit: false, 74 | outer: true, 75 | moon: true, 76 | oceanic_rate: {min: 10, max: 100}, 77 | heat_level: {min: 0, max: 20}, 78 | moisture_level: {min: 0, max: 20}, 79 | }, 80 | // OCEAN --------------------------------------------------------------------- 81 | { 82 | // ...defaultsForMatrices, 83 | type: 'ocean', 84 | weight: 3, 85 | inner: false, 86 | habit: true, 87 | outer: false, 88 | moon: true, 89 | oceanic_rate: {min: 80, max: 100}, 90 | heat_level: {min: -20, max: 20}, 91 | moisture_level: {min: 50, max: 100}, 92 | }, 93 | // EARTH --------------------------------------------------------------------- 94 | { 95 | // ...defaultsForMatrices, 96 | type: 'earth', 97 | weight: 9, 98 | inner: false, 99 | habit: true, 100 | outer: false, 101 | moon: true, 102 | oceanic_rate: {min: 10, max: 80}, 103 | heat_level: {min: -20, max: 20}, 104 | moisture_level: {min: 20, max: 70}, 105 | }, 106 | // GAS_METHAN ---------------------------------------------------------------- 107 | { 108 | // ...defaultsForMatrices, 109 | type: 'gas_methan', 110 | weight: 3, 111 | inner: false, 112 | habit: true, 113 | outer: true, 114 | moon: false, 115 | oceanic_rate: {min: 0, max: 0}, 116 | heat_level: {min: 50, max: 100}, 117 | moisture_level: {min: -100, max: -20}, 118 | plate_count: {min: 4, max: 9}, 119 | }, 120 | // GAS HELIUM ---------------------------------------------------------------- 121 | // { 122 | // type: 'gas_hellium', weight: 3, 123 | // inner: false, habit: true, outer: true, moon: false 124 | // }, 125 | // GAS_HYDROGEN -------------------------------------------------------------- 126 | // { 127 | // type: 'gas_hydrogen', weight: 3, 128 | // inner: false, habit: true, outer: true, moon: false 129 | // }, 130 | ] 131 | } 132 | -------------------------------------------------------------------------------- /src/_generators/Planet/helpers/functions.js: -------------------------------------------------------------------------------- 1 | // import config from '../scmap/config'; 2 | const config = {}; 3 | 4 | import * as THREE from 'three-math'; 5 | 6 | function hasLocalStorage() { 7 | // try { 8 | // return 'localStorage' in window && window.localStorage !== null; 9 | // } catch(e) { 10 | return false; 11 | // } 12 | } 13 | 14 | function hasSessionStorage() { 15 | // try { 16 | // return 'sessionStorage' in window && window.sessionStorage !== null; 17 | // } catch(e) { 18 | return false; 19 | // } 20 | } 21 | 22 | function humanSort( a, b ) { 23 | let aa = a.name.split(/(\d+)/); 24 | let bb = b.name.split(/(\d+)/); 25 | 26 | for ( let x = 0; x < Math.max( aa.length, bb.length ); x++ ) 27 | { 28 | if ( aa[x] != bb[x] ) 29 | { 30 | let cmp1 = ( isNaN( parseInt( aa[x], 10 ) ) ) ? aa[x] : parseInt( aa[x], 10 ); 31 | let cmp2 = ( isNaN( parseInt( bb[x], 10 ) ) ) ? bb[x] : parseInt( bb[x], 10 ); 32 | 33 | if ( cmp1 === undefined || cmp2 === undefined ) { 34 | return aa.length - bb.length; 35 | } else { 36 | return ( cmp1 < cmp2 ) ? -1 : 1; 37 | } 38 | } 39 | } 40 | 41 | return 0; 42 | } 43 | 44 | function travelTimeForAU( distanceAU ) { 45 | return ( config.approximateTraveltimePerAU * distanceAU ); 46 | } 47 | 48 | function hashString(s){ 49 | var hash = 0; 50 | var length = s.length; 51 | if (length === 0) 52 | return hash; 53 | for (var i = 0; i < length; ++i) 54 | { 55 | var character = s.charCodeAt(1); 56 | hash = ((hash << 5) - hash) + character; 57 | hash |= 0; 58 | } 59 | return hash; 60 | } 61 | 62 | function adjustRange(value, oldMin, oldMax, newMin, newMax){ 63 | return (value - oldMin) / (oldMax - oldMin) * (newMax - newMin) + newMin; 64 | } 65 | 66 | function accumulateArray(array, state, accumulator){ 67 | for (var i = 0; i < array.length; ++i){ 68 | state = accumulator(state, array[i]); 69 | } 70 | return state; 71 | } 72 | 73 | function slerp(p0, p1, t){ 74 | var omega = Math.acos(p0.dot(p1)); 75 | return p0.clone().multiplyScalar(Math.sin((1 - t) * omega)).add(p1.clone().multiplyScalar(Math.sin(t * omega))).divideScalar(Math.sin(omega)); 76 | } 77 | 78 | function calculateTriangleArea(pa, pb, pc){ 79 | var vab = new THREE.Vector3().subVectors(pb, pa); 80 | var vac = new THREE.Vector3().subVectors(pc, pa); 81 | var faceNormal = new THREE.Vector3().crossVectors(vab, vac); 82 | var vabNormal = new THREE.Vector3().crossVectors(faceNormal, vab).normalize(); 83 | var plane = new THREE.Plane().setFromNormalAndCoplanarPoint(vabNormal, pa); 84 | var height = plane.distanceToPoint(pc); 85 | var width = vab.length(); 86 | var area = width * height * 0.5; 87 | return area; 88 | } 89 | 90 | function randomUnitVector(random){ 91 | var theta = random.real(0, Math.PI * 2); 92 | var phi = Math.acos(random.realInclusive(-1, 1)); 93 | var sinPhi = Math.sin(phi); 94 | return new THREE.Vector3( 95 | Math.cos(theta) * sinPhi, 96 | Math.sin(theta) * sinPhi, 97 | Math.cos(phi)); 98 | } 99 | 100 | function intersectRayWithSphere(ray, sphere){ 101 | var v1 = sphere.center.clone().sub(ray.origin); 102 | var v2 = v1.clone().projectOnVector(ray.direction); 103 | var d = v1.distanceTo(v2); 104 | return (d <= sphere.radius); 105 | } 106 | 107 | export { hasLocalStorage, hasSessionStorage, humanSort, travelTimeForAU, hashString, adjustRange, accumulateArray, slerp, calculateTriangleArea, randomUnitVector, intersectRayWithSphere }; 108 | -------------------------------------------------------------------------------- /src/_generators/generators/CelestialObject.ts: -------------------------------------------------------------------------------- 1 | import { RandomObject, Seed } from '../../utils'; 2 | 3 | export interface CelestialModel { 4 | id?: string; 5 | seed?: number; 6 | type?: string; 7 | designation?: string; 8 | extends?: {}; 9 | parent?: {}; 10 | system?: {}; 11 | } 12 | 13 | export abstract class CelestialObject { 14 | static id = 0; 15 | 16 | private readonly id?: string; 17 | private seed?: Seed; 18 | private random?: RandomObject; 19 | 20 | name?: string | null = null; 21 | type?: string | null = null; // ONE OF [STAR,PLANET,SATELLITE,ASTEROID_BELT,MANMADE] 22 | parent = null; 23 | parent_id = null; 24 | code?: string | null = null; // eg. TAU.PLANETS.TAUI, TAU.STARS.TAU, TAU.STARS.TAUA 25 | designation?: string | null = null; 26 | orbit_period = null; 27 | rotation_period = null; 28 | habitable = null; 29 | age = null; 30 | appearance = 'DEFAULT'; // wygląd 31 | size = null; 32 | // @ts-ignore 33 | system: { name: string }; 34 | 35 | constructor(props: CelestialModel = {}, type: string) { 36 | Object.assign(this, props, props.extends || {}); // todo remove 37 | this.id = props.id; 38 | 39 | this.setSeed(props.seed); 40 | this.setId(props.id); 41 | this.setType(props.type || type); 42 | this.setParent(props.parent); 43 | this.setSystem(props.system); 44 | // this.setName(props.name) 45 | this.setDesignation(props.designation); 46 | } 47 | 48 | setSeed(seed?: Seed) { 49 | this.seed = seed || Date.now(); 50 | this.random = new RandomObject(this.seed); 51 | } 52 | 53 | setType(type: string) { 54 | this.type = type; 55 | } 56 | 57 | setId(id?: string) { 58 | // todo remove setter? 59 | // @ts-ignore 60 | this.id = id != null ? id : CelestialObject.getCurrentId(); 61 | } 62 | 63 | setSystem(system: any) { 64 | this.system = system; 65 | this.makeCode(); 66 | } 67 | 68 | setName(name: string) { 69 | this.name = name; 70 | this.makeCode(); 71 | } 72 | 73 | setDesignation(designation?: string) { 74 | this.designation = designation; 75 | this.makeCode(); 76 | } 77 | 78 | setParent(parent: any) { 79 | if (parent) { 80 | this.parent = parent; 81 | this.parent_id = parent.id || null; 82 | } 83 | } 84 | 85 | makeCode() { 86 | // console.log(this.system.name); 87 | const star_system_name = this.system && this.system.name ? this.escapeRegExp(this.system.name) : ''; 88 | const type = this.type; 89 | const designation = this.designation ? this.escapeRegExp(this.designation) : ''; 90 | const name = this.name ? this.escapeRegExp(this.name) : ''; 91 | this.code = `${star_system_name}.${type}.${designation}${name}`.toUpperCase().replace(/ /g, ''); 92 | } 93 | escapeRegExp(str: string) { 94 | return str.replace(/[.*+?^${}()|[\]\\]/g, ''); // $& means the whole matched string 95 | } 96 | 97 | static getCurrentId() { 98 | return CelestialObject.id++; 99 | } 100 | 101 | abstract toModel(): CelestialModel; 102 | toJSON() { 103 | return this.toModel(); 104 | } 105 | } 106 | 107 | export default CelestialObject; 108 | -------------------------------------------------------------------------------- /src/_generators/generators/Generator.js: -------------------------------------------------------------------------------- 1 | 2 | class Generator { 3 | static defaultProps = { 4 | seed: null, 5 | } 6 | constructor(props = {}) { 7 | this.props = { ...this.constructor.defaultProps, ...props } 8 | this.model = { 9 | seed: null, 10 | } 11 | } 12 | 13 | getSeed() { 14 | const originalSeed = this.props.seed || this.model.seed 15 | let seed = null 16 | if (typeof (originalSeed) === "number") 17 | seed = originalSeed 18 | else if (typeof (originalSeed) === "string") 19 | seed = this.hashString(originalSeed) 20 | else 21 | seed = Date.now() 22 | return seed 23 | // const random = new XorShift128(seed) 24 | } 25 | 26 | hashString(s) { 27 | let hash = 0 28 | const length = s.length 29 | if (length === 0) 30 | return hash 31 | for (let i = 0; i < length; ++i) { 32 | const character = s.charCodeAt(1) 33 | hash = ((hash << 5) - hash) + character 34 | // hash = hash & hash 35 | hash |= 0 36 | } 37 | return hash 38 | } 39 | } 40 | 41 | export default Generator 42 | -------------------------------------------------------------------------------- /src/_generators/generators/PlanetarySubsystem.js: -------------------------------------------------------------------------------- 1 | import Star from './Star' 2 | 3 | class PlanetarySubsystem { 4 | _average_separation = null 5 | _frost_line = null 6 | _inner_limit = null 7 | _outer_limit = null 8 | _barycentre = null 9 | _planets = [] 10 | _mass = null 11 | _children = [] 12 | _subtype = null 13 | _luminosity = null 14 | _planetable = null 15 | // SETTERS & GETTERS ========================================================= 16 | set average_separation(average_separation) { this._average_separation = average_separation } 17 | get average_separation() { return this._average_separation } 18 | set frost_line(frost_line) { this._frost_line = frost_line } 19 | get frost_line() { return this._frost_line } 20 | set inner_limit(inner_limit) { this._inner_limit = inner_limit } 21 | get inner_limit() { return this._inner_limit } 22 | set outer_limit(outer_limit) { this._outer_limit = outer_limit } 23 | get outer_limit() { return this._outer_limit } 24 | set barycentre(barycentre) { this._barycentre = barycentre } 25 | get barycentre() { return this._barycentre } 26 | set planets(planets) { this._planets = planets } 27 | get planets() { return this._planets } 28 | set mass(mass) { this._mass = mass } 29 | get mass() { return this._mass } 30 | set children(children) { this._children = children } 31 | get children() { return this._children } 32 | set subtype(subtype) { this._subtype = subtype } 33 | get subtype() { return this._subtype } 34 | set luminosity(luminosity) { this._luminosity = luminosity } 35 | get luminosity() { return this._luminosity } 36 | set planetable(planetable) { this._planetable = planetable } 37 | get planetable() { return this._planetable } 38 | // END SETTERS & GETTERS ===================================================== 39 | 40 | constructor(stars) { 41 | // if (stars) 42 | // this.system = PlanetarySubsystem.buildTree(stars, 0, stars.length) 43 | } 44 | 45 | generateName(random) { 46 | return 'abc123' 47 | } 48 | 49 | // FOR BINARY STAR: 0.15 - 6 AU (FOR HABITABLE GET EXTREME LOW VALUE) 50 | generateAverageSeparation(random) { 51 | // console.log('generateAverageSeparation'); 52 | // // if (this.subtype == null) this.checkSubtype() 53 | // switch (this.subtype) { 54 | // case ('BINARY_STAR'): return 1 55 | // case ('SUBSYSTEM'): return 2 56 | // default: throw new TypeError('subtype must be one of [...]') 57 | // } 58 | } 59 | 60 | 61 | 62 | recalculate() { 63 | this.subtype = PlanetarySubsystem.calcSubtype(this.children) 64 | this.planetable = PlanetarySubsystem.calcPlanetable(this.subtype, this.children) 65 | // console.log('subtype', this.subtype, this.children.length); 66 | const { children: [ch1, ch2] } = this 67 | this.mass = ch2 ? PlanetarySubsystem.calcMass(ch1.mass, ch2.mass) : ch1.mass 68 | this.inner_limit = ch2 ? PlanetarySubsystem.calcInnerLimit(this.mass) : ch1.inner_limit 69 | this.outer_limit = ch2 ? PlanetarySubsystem.calcOuterLimit(this.mass) : ch1.outer_limit 70 | this.luminosity = ch2 ? PlanetarySubsystem.calcLuminosity(ch1.luminosity, ch2.luminosity) : ch1.luminosity 71 | this.frost_line = ch2 ? PlanetarySubsystem.calcFrostLine(this.luminosity) : ch1.frost_line 72 | } 73 | 74 | static calcPlanetable(subtype, children) { 75 | switch (true) { 76 | case (subtype === 'SINGLE_STAR'): 77 | case (subtype === 'BINARY_STAR'): 78 | case (subtype === 'SUBSYSTEM' && children.every(c => c.subtype === 'SINGLE_STAR' || c.subtype === 'BINARY_STAR')): 79 | return true 80 | default: return false 81 | } 82 | } 83 | 84 | // checkSubtype() { 85 | // const {children} = this 86 | // if (children.length === 1) this.subtype = 'SINGLE_STAR' 87 | // else if (children[0] instanceof Star && children[1] instanceof Star) this.subtype = 'BINARY_STAR' 88 | // else if (children[0] instanceof Star || children[1] instanceof Star) this.subtype = 'SUBSYSTEM' 89 | // } 90 | 91 | Name(name) { 92 | this.name = name 93 | return this 94 | } 95 | Children(children) { 96 | this.children = children 97 | // this.checkSubtype() 98 | this.recalculate() 99 | return this 100 | } 101 | CalculateOverall() { 102 | this.mass = this.children.reduce((prev, curr) => { 103 | return prev + curr.mass 104 | }, 0) 105 | return this 106 | } 107 | 108 | Generate(random = this.random) { 109 | const genList = [ 110 | // ['seed', this.generateSeed], 111 | ['name', this.generateName], 112 | ['average_separation', this.generateAverageSeparation], 113 | // ['position', this.generatePosition], 114 | // ['subsystems', this.generateSubsystem], 115 | ] 116 | for (const [key, fun] of genList) { 117 | if (this[key] == null) this[key] = fun(random) 118 | // console.log('subsystem', key, this[key]); 119 | } 120 | 121 | return this 122 | } 123 | 124 | 125 | // STATICS =================================================================== 126 | static calcSubtype(children) { 127 | // console.log('children', children); 128 | switch (true) { 129 | case (children.length === 1 && children[0].subtype !== 'SINGLE_STAR'): return 'SINGLE_STAR' 130 | case (children.length === 2 && children.every(c => c.body_type === 'STAR')): return 'BINARY_STAR' 131 | default: return 'SUBSYSTEM' 132 | } 133 | } 134 | static calcMass(massA, massB) { 135 | return massA + massB 136 | } 137 | static calcPrimaryBarycentre(average_separation = 1, massA = 2, massB = 1) { 138 | return average_separation * (massB / (massA + massB)) 139 | } 140 | static calcSecondaryBarycentre(average_separation = 1, primary_barycentre = .3) { 141 | return average_separation - primary_barycentre 142 | } 143 | static calcFrostLine(luminosityA, luminosityB = 0) { 144 | // console.log('calcFrostLine', luminosityA, luminosityB); 145 | return 4.85 * Math.sqrt(luminosityA + luminosityB) 146 | } 147 | static calcInnerLimit(massA, massB = 0) { 148 | return Star.calcInnerLimit(massA + massB) 149 | } 150 | static calcOuterLimit(massA, massB = 0) { 151 | return Star.calcOuterLimit(massA + massB) 152 | } 153 | // TODO NOT SURE WITH THE FORMULA... 154 | static calcLuminosity(luminosityA, luminosityB = 0) { 155 | return luminosityA + luminosityB 156 | } 157 | 158 | static buildTree(array, start, end) { 159 | if (!Array.isArray(array)) throw new TypeError('array must be an Array type') 160 | if (start == null) start = 0 161 | if (end == null) end = array.length 162 | 163 | if (end-start > 1) { 164 | const mid = (start+end)>>1; 165 | // if (array.length === 1) return array[0] 166 | const left = PlanetarySubsystem.buildTree(array, start, mid); 167 | const right = PlanetarySubsystem.buildTree(array, mid, end); 168 | 169 | let leftSingle = null 170 | let rightSingle = null 171 | if (left.body_type === 'STAR' && right.body_type !== 'STAR') 172 | leftSingle = new PlanetarySubsystem() 173 | .Children([left]) 174 | if (left.body_type !== 'STAR' && right.body_type === 'STAR') 175 | rightSingle = new PlanetarySubsystem() 176 | .Children([right]) 177 | 178 | return new PlanetarySubsystem() 179 | .Children([leftSingle || left, rightSingle || right]) 180 | // .CalculateOverall() 181 | } else { 182 | return array[start] 183 | // return new PlanetarySubsystem() 184 | // .Children([array[start]]) 185 | } 186 | } 187 | // END STATICS =============================================================== 188 | } 189 | 190 | export default PlanetarySubsystem 191 | -------------------------------------------------------------------------------- /src/_generators/generators/PlanetarySystem.js: -------------------------------------------------------------------------------- 1 | import Random from '../utils/RandomObject' 2 | import { Vector3 } from 'three-math' 3 | import { GREEK_LETTERS_NAMES, GREEK_LETTERS } from '../utils/alphabet' 4 | import Names from './Names' 5 | // import StarSystem from './System' 6 | import PlanetarySubsystem from './PlanetarySubsystem' 7 | import { 8 | STAR_COUNT_DISTIBUTION_IN_SYSTEMS, 9 | } from '../CONSTANTS' 10 | import Star from './Star' 11 | 12 | 13 | class PlanetarySystem { 14 | _luminosity = null 15 | 16 | // GETTERS & SETTERS ========================================================= 17 | get luminosity() { return this._luminosity } 18 | set luminosity(luminosity) { this._luminosity = luminosity } 19 | get star_mass() { return this._star_mass } 20 | set star_mass(star_mass) { this._star_mass = star_mass } 21 | get subsystem() { return this._subsystem } 22 | set subsystem(subsystem) { this._subsystem = subsystem } 23 | get random() { 24 | if (this._random == null) this._random = new Random(this.seed) 25 | return this._random 26 | } 27 | // END GETTERS & SETTERS ===================================================== 28 | 29 | constructor(props = {}, random) { 30 | this.seed = props.seed 31 | this.name = props.name 32 | this.position = props.position //|| new Vector3() 33 | } 34 | 35 | async build() { 36 | await this.generateName() 37 | await this.generatePosition() 38 | await this.generateStars() 39 | return this 40 | } 41 | 42 | async generateName(force = false) { 43 | const name = await PlanetarySystem.generateName(this.random) 44 | if (!this.name || force) this.name = name 45 | return name 46 | } 47 | static async generateName(random) { 48 | return Names.Generate(random) 49 | } 50 | 51 | async generatePosition(force = false) { 52 | const position = await PlanetarySystem.generatePosition(this.random) 53 | if (!this.position || force) this.position = position 54 | return position 55 | } 56 | static async generatePosition(random) { 57 | return new Vector3() 58 | } 59 | 60 | async generateStars(force = false) { 61 | let stars = [] 62 | for (let star of PlanetarySystem.generateStars(this.random)) { 63 | stars.push(star) 64 | } 65 | stars = stars.sort((a, b) => a.mass < b.mass) 66 | if (!this.stars || force) this.stars = stars 67 | return this.stars 68 | } 69 | static * generateStars(random) { 70 | try { 71 | const count = random.weighted(STAR_COUNT_DISTIBUTION_IN_SYSTEMS) 72 | if (count <= 0) return 73 | 74 | for (let i=0; i ) 90 | for (let i=0; i 1) name.push(GREEK_LETTERS[i]) 93 | // const suffix = this.stars.length > 1 ? GREEK_LETTERS_NAMES[i] : '' 94 | name.push(this.name) 95 | this.stars[i].Name(name.join(' ')) 96 | } 97 | if (this.subsystem) { 98 | this.star_mass = this.subsystem.mass 99 | this.luminosity = this.subsystem.luminosity 100 | } 101 | } 102 | 103 | Name(name) { 104 | this.name = name 105 | return this 106 | } 107 | Position(position) { 108 | this.position = position 109 | return this 110 | } 111 | 112 | async Generate(random = this.random) { 113 | const genList = [ 114 | // ['seed', this.generateSeed], 115 | ['name', this.generateName], 116 | ['position', this.generatePosition], 117 | ['stars', (random) => [...this.generateStars(random)].sort((a, b) => a.mass < b.mass).map((s, i) => s.MassOrder(i))], 118 | // [''], 119 | ['subsystem', random => this.generateSubsystem(random, this.stars)], 120 | ] 121 | for (const [key, fun] of genList) { 122 | if (this[key] == null) this[key] = fun(random) 123 | // console.log(key, this[key]); 124 | } 125 | this.recalculate() 126 | return this 127 | } 128 | } 129 | 130 | export default PlanetarySystem 131 | -------------------------------------------------------------------------------- /src/_generators/generators/Region.js: -------------------------------------------------------------------------------- 1 | import RandomObject from '../../utils/RandomObject' 2 | import SteppedAction from '../../utils/SteppedAction' 3 | // import Generator from './Generator' 4 | // import { 5 | // SPECTRAL_CLASSIFICATION, 6 | // SUN_TEMPERATURE, 7 | // SUN_AGE, 8 | // } from '../CONSTANTS' 9 | 10 | 11 | class Region { 12 | type = 'REGION' 13 | subtype = null 14 | // seq_id = null 15 | 16 | tile = null // Tile sequential id 17 | neighbors = [] 18 | planet = null // Planet Object 19 | 20 | physics = { 21 | biome: null, 22 | elevation: null, 23 | moisture: null, 24 | temperature: null, 25 | } 26 | 27 | constructor({ physics, ...props } = {}) { 28 | Object.assign(this, props) 29 | if (physics != null) this.setPhysics(physics) 30 | } 31 | 32 | setPhysics(physics) { 33 | for (const key of Object.keys(this.physics)) { 34 | if (physics[key] !== undefined) this.physics[key] = physics[key] 35 | } 36 | } 37 | 38 | toObject() { 39 | return { 40 | type: this.type, 41 | subtype: this.subtype, 42 | tile: this.tile, 43 | neighbors: [...this.neighbors], 44 | physics: {...this.physics}, 45 | } 46 | } 47 | } 48 | 49 | export default Region 50 | -------------------------------------------------------------------------------- /src/_generators/generators/SystemTS.tsx: -------------------------------------------------------------------------------- 1 | // todo remove it! 2 | // @ts-nocheck 3 | 4 | import { Vector3 } from 'three'; 5 | 6 | import { STAR_COUNT_DISTIBUTION_IN_SYSTEMS, PLANETS_COUNT_IN_SINGLE_STAR_SYSTEM } from '../CONSTANTS'; 7 | import Star from './Star'; 8 | import StarSubsystem from './StarSubsystem'; 9 | import Planet from './Planet'; 10 | import { RandomObject } from '../utils/RandomObject'; 11 | import { decimalToRoman } from '../utils'; 12 | // import Names from './Names' 13 | import Names from './StarName'; 14 | import PlanetOrbitGenerator from './Planet/PlanetOrbitGenerator'; 15 | 16 | import { SystemType } from '../interfaces'; 17 | import { Seed } from '../utils'; 18 | 19 | interface StarGenModel { 20 | mass?: number; 21 | } 22 | interface PlanetGenModel {} 23 | 24 | export interface SystemGenModel { 25 | type?: SystemType; 26 | seed?: Seed; 27 | position?: Vector3; 28 | stars?: StarGenModel[]; 29 | planets?: PlanetGenModel[]; 30 | 31 | code?: string; 32 | name?: string; 33 | habitable?: boolean; 34 | habitable_zone_inner?: boolean; 35 | habitable_zone_outer?: boolean; 36 | frost_line?: boolean; 37 | description?: string; 38 | celestial_objects?: any[]; 39 | } 40 | 41 | export class System { 42 | type?: SystemGenModel; // SINGLE_STAR, BINARY 43 | code?: string; 44 | name?: string; 45 | position: Vector3; 46 | random?: RandomObject; 47 | 48 | seed?: Seed; 49 | seeds: { planets?: Seed; stars?: Seed } = {}; 50 | habitable = null; // fill after planet generation 51 | habitable_zone_inner = null; 52 | habitable_zone_outer = null; 53 | frost_line = null; 54 | description = null; 55 | celestial_objects = []; 56 | 57 | stars: Star[]; 58 | planets: any[]; 59 | 60 | starGenerationData?: any; // todo more generic 61 | planetGenerationData?: any; // todo more generic 62 | 63 | constructor(public model: SystemGenModel = { position: new Vector3(0, 0, 0) }) { 64 | Object.assign(this, model); // todo remove 65 | 66 | const { seed, position, name, stars, planets } = model; 67 | this.setSeed(seed); 68 | this.setName(name); 69 | this.setPosition(position); 70 | // @ts-ignore 71 | this.stars = stars || []; 72 | this.planets = planets || []; 73 | // this.seeds = {} 74 | // stars: null, 75 | // planets: null, 76 | // return this 77 | // console.log(this); 78 | } 79 | 80 | setSeed(seed: number) { 81 | if (this.seed == null) { 82 | this.seed = seed || Date.now(); 83 | } 84 | this.random = new RandomObject(this.seed); 85 | this.generateSeeds(); 86 | } 87 | generateSeeds() { 88 | if (this.seeds && this.seeds.planets) return this.seeds; 89 | this.seeds = { 90 | stars: this.random?.next(), 91 | planets: this.random?.next(), 92 | }; 93 | } 94 | 95 | setPosition(position: Vector3) { 96 | this.position = position || new Vector3(); 97 | } 98 | setName(name?: string) { 99 | // this.name = name || Names.GenerateSystemName(this.random) 100 | this.name = name || Names.Generate(this.random); 101 | this.code = this.escapeRegExp(this.name as string) 102 | .toUpperCase() 103 | .replace(/ /g, ''); 104 | } 105 | escapeRegExp(str: string) { 106 | return str.replace(/[.*+?^${}()|[\]\\]/g, ''); // $& means the whole matched string 107 | } 108 | 109 | // async build() { 110 | // // await this.generateStars() 111 | // // for (let star of this.generateStars()) {} 112 | // // this.stars.sort((s1, s2) => s1.mass < s2.mass) 113 | 114 | // return this 115 | // } 116 | *generateStars() { 117 | try { 118 | const random = new RandomObject(this.seeds.stars); 119 | for (let star of System.GenerateStars(random, this)) { 120 | this.stars.push(star); 121 | this.celestial_objects.push(star); 122 | yield star; 123 | } 124 | // @ts-ignore 125 | this.stars.sort((s1, s2) => s1.mass < s2.mass); 126 | this.fillStarInfo(); 127 | } catch (e) { 128 | console.warn(e); 129 | } 130 | } 131 | getStars() { 132 | return this.celestial_objects.filter((o) => o.type == 'STAR'); 133 | } 134 | getPlanets() { 135 | return this.celestial_objects.filter((o) => o.type == 'PLANET'); 136 | } 137 | fillPlanetInfo() { 138 | const planets = this.getPlanets(); 139 | this.planetGenerationData = { 140 | planets_count: planets.length, 141 | }; 142 | Object.assign(this, this.planetGenerationData); 143 | } 144 | fillStarInfo() { 145 | const stars = this.getStars(); 146 | // this.stars_count = stars.length 147 | let type = null; 148 | switch (stars.length) { 149 | case 1: 150 | type = 'SINGLE_STAR'; 151 | break; 152 | case 2: 153 | type = 'BINARY_STAR'; 154 | break; 155 | default: 156 | type = 'MULTIPLE_STAR'; 157 | } 158 | const star = stars[0]; 159 | this.starGenerationData = { 160 | type: type, 161 | stars_count: stars.length, 162 | inner_limit: star.inner_limit, 163 | outer_limit: star.outer_limit, 164 | frost_line: star.frost_line, 165 | habitable_zone: star.habitable_zone, 166 | habitable_zone_inner: star.habitable_zone_inner, 167 | habitable_zone_outer: star.habitable_zone_outer, 168 | star_color: star.color, 169 | }; 170 | Object.assign(this, this.starGenerationData); 171 | } 172 | flushStarGenerationData() { 173 | return this.starGenerationData; 174 | } 175 | flushPlanetGenerationData() { 176 | return this.planetGenerationData; 177 | } 178 | 179 | *generateProtoPlanets() { 180 | const random = new RandomObject(this.seeds.planets); 181 | const planet_count = random.weighted(PLANETS_COUNT_IN_SINGLE_STAR_SYSTEM); 182 | const used_seeds = []; 183 | const zones = []; 184 | const zonesNames = ['inner', 'habitable', 'outer']; 185 | let maxInInner = 4; 186 | let maxInHabitable = 3; 187 | for (let i = 0; i < planet_count; i++) { 188 | const tempZones = []; 189 | if (maxInInner != 0) tempZones.push('inner'); 190 | if (maxInHabitable != 0) tempZones.push('habitable'); 191 | tempZones.push('outer'); 192 | const choice = this.habitable && i == 0 ? 'habitable' : random.choice(tempZones); 193 | // if (this.habitable && i==0) 194 | if (choice == 'inner') maxInInner--; 195 | if (choice == 'habitable') maxInHabitable--; 196 | zones.push(choice); 197 | } 198 | zones.sort((a, b) => zonesNames.indexOf(a) - zonesNames.indexOf(b)); 199 | const habitableIndex = zones.indexOf('habitable'); 200 | // console.log('zones', zones); 201 | const planetOrbits = new PlanetOrbitGenerator(this); 202 | for (const orbit of planetOrbits.generateOrbits()) { 203 | let planetSeed = random.next(); 204 | while (used_seeds.find((o) => o == planetSeed)) planetSeed = random.next(); 205 | used_seeds.push(planetSeed); 206 | 207 | const designation = `${this.name} ${decimalToRoman(orbit.from_star)}`; 208 | yield { 209 | ...orbit, 210 | // type: undefined, 211 | // subtype: orbit.type, 212 | seed: planetSeed, 213 | orbit: orbit, 214 | // zone: zones[i], 215 | // subtype: this.habitable && i === habitableIndex ? 'earth' : null, 216 | designation: designation, 217 | }; 218 | } 219 | // for (let i=0; i o == planetSeed)) planetSeed = random.next() 223 | // used_seeds.push(planetSeed) 224 | // 225 | // const designation = `${this.name} ${decimalToRoman(i+1)}` 226 | // yield { 227 | // seed: planetSeed, 228 | // zone: zones[i], 229 | // subtype: this.habitable && i === habitableIndex ? 'earth' : null, 230 | // designation: designation, 231 | // } 232 | // } 233 | } 234 | *generatePlanets() { 235 | try { 236 | for (let protoPlanet of this.generateProtoPlanets()) { 237 | // CREATE PLANET 238 | const planet = new Planet({ 239 | ...protoPlanet, 240 | system: this, 241 | }); 242 | this.planets.push(planet); 243 | this.celestial_objects.push(planet); 244 | // console.log('_planet',planet); 245 | yield planet; 246 | } 247 | this.fillPlanetInfo(); 248 | } catch (e) { 249 | console.warn(e); 250 | } 251 | } 252 | 253 | Position(position) { 254 | this.position = position; 255 | return this; 256 | } 257 | Subsystem(subsystem) { 258 | // this._subsystem = subsystem; // todo maybe? xD 259 | return this; 260 | } 261 | 262 | static *GenerateStars(random, system) { 263 | try { 264 | const count = random.weighted(STAR_COUNT_DISTIBUTION_IN_SYSTEMS); 265 | // console.log('count', count, STAR_COUNT_DISTIBUTION_IN_SYSTEMS); 266 | if (count <= 0) return; 267 | 268 | for (let i = 0; i < count; i++) { 269 | const buildData = { 270 | // parent: system, 271 | system_sequence: undefined, // todo 272 | system: system, 273 | }; 274 | if (count > 1) buildData.system_sequence = i; 275 | yield Star.Generate(random, buildData); 276 | } 277 | } catch (err) { 278 | console.error('ERR>', err); 279 | } 280 | } 281 | // static async GenerateSubsystem(random, stars) { 282 | // return StarSubsystem.Generate(stars, random) 283 | // } 284 | // static async GeneratePlanets(random) { 285 | // 286 | // } 287 | } 288 | 289 | export default System; 290 | -------------------------------------------------------------------------------- /src/galaxy-shape/BasicShape.ts: -------------------------------------------------------------------------------- 1 | import { RandomObject } from '../utils'; 2 | import { ShapeStar } from './ShapeStar'; 3 | 4 | export abstract class BasicShape { 5 | abstract Generate(random?: RandomObject): IterableIterator; 6 | } 7 | -------------------------------------------------------------------------------- /src/galaxy-shape/Cluster.ts: -------------------------------------------------------------------------------- 1 | import { Vector3 } from 'three'; 2 | import { RandomObject } from '../utils'; 3 | import { BasicShape } from './BasicShape'; 4 | 5 | export class Cluster implements BasicShape { 6 | constructor( 7 | public readonly basis: BasicShape, 8 | public readonly countMean = 0.0000025, 9 | public readonly countDeviation = 0.000001, 10 | public readonly deviationX = 0.0000025, 11 | public readonly deviationY = 0.0000025, 12 | public readonly deviationZ = 0.0000025 13 | ) {} 14 | 15 | *Generate(random: RandomObject) { 16 | const { 17 | basis, 18 | // size, 19 | countDeviation, 20 | countMean, 21 | deviationX, 22 | deviationY, 23 | deviationZ, 24 | } = this; 25 | 26 | try { 27 | const count = Math.max(0, random.NormallyDistributedSingle(countDeviation, countMean)); 28 | if (count <= 0) return; 29 | 30 | for (let i = 0; i < count; i++) { 31 | const center = new Vector3( 32 | random.NormallyDistributedSingle(deviationX, 0), 33 | random.NormallyDistributedSingle(deviationY, 0), 34 | random.NormallyDistributedSingle(deviationZ, 0) 35 | ); 36 | 37 | for (const star of basis.Generate(random)) yield star.Offset(center); 38 | } 39 | } catch (err) { 40 | console.error('!', err); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/galaxy-shape/Grid.ts: -------------------------------------------------------------------------------- 1 | import { Vector3 } from 'three'; 2 | 3 | import { RandomObject } from '../utils'; 4 | import { BasicShape } from './BasicShape'; 5 | 6 | import { ShapeStar } from './ShapeStar'; 7 | 8 | export class Grid implements BasicShape { 9 | constructor(public readonly size: number = 5, public readonly spacing: number = 1) {} 10 | 11 | *Generate(random?: RandomObject) { 12 | const { size, spacing } = this; 13 | const count = parseInt((size / spacing).toFixed()); 14 | 15 | for (let i = 0; i < count; i++) { 16 | for (let j = 0; j < count; j++) { 17 | for (let k = 0; k < count; k++) { 18 | yield new ShapeStar({ 19 | position: new Vector3(i * spacing, j * spacing, k * spacing), 20 | // .add(new Vector3(-size/2, -size/2, -size/2)), 21 | // temperature: null, 22 | galaxy_size: size, 23 | }).Offset(new Vector3(-size / 2, -size / 2, -size / 2)); 24 | } 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/galaxy-shape/ShapeStar.ts: -------------------------------------------------------------------------------- 1 | import { Vector3 } from 'three'; 2 | 3 | interface ShapeStarModel { 4 | position: Vector3; 5 | temperature?: number; 6 | galaxy_size?: number; 7 | } 8 | 9 | export class ShapeStar { 10 | position: Vector3; 11 | temperature?: number; 12 | galaxy_size?: number; 13 | 14 | constructor(model: ShapeStarModel) { 15 | this.position = model.position; 16 | this.temperature = model.temperature; 17 | this.galaxy_size = model.galaxy_size; 18 | } 19 | 20 | Offset(offset: Vector3) { 21 | this.position.add(offset); 22 | return this; 23 | } 24 | 25 | swirl(axis: Vector3, amount: number) { 26 | var d = this.position.length(); 27 | var angle = Math.pow(d, 0.1) * amount; 28 | this.position.applyAxisAngle(axis, angle); 29 | return this; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/galaxy-shape/Sphere.ts: -------------------------------------------------------------------------------- 1 | import { Vector3 } from 'three'; 2 | 3 | import { RandomObject } from '../utils'; 4 | 5 | import { BasicShape } from './BasicShape'; 6 | import { ShapeStar } from './ShapeStar'; 7 | 8 | export class Sphere implements BasicShape { 9 | constructor( 10 | public readonly size: number = 750, 11 | public readonly densityMean: number = 0.000001, 12 | public readonly densityDeviation: number = 0.0000001, 13 | public readonly deviationX: number = 0.35, 14 | public readonly deviationY: number = 0.125, 15 | public readonly deviationZ: number = 0.35 16 | ) {} 17 | 18 | *Generate(random: RandomObject) { 19 | const { size, densityDeviation, densityMean, deviationX, deviationY, deviationZ } = this; 20 | const density = Math.max(0, random.NormallyDistributedSingle(densityDeviation, densityMean)); 21 | const countMax = Math.max(0, parseInt((size * size * size * density).toFixed())); 22 | 23 | if (countMax <= 0) return; 24 | var count = random.Next(countMax); 25 | 26 | for (let i = 0; i < count; i++) { 27 | var pos = new Vector3( 28 | random.NormallyDistributedSingle(deviationX * size, 0), 29 | random.NormallyDistributedSingle(deviationY * size, 0), 30 | random.NormallyDistributedSingle(deviationZ * size, 0) 31 | ); 32 | var d = pos.length() / size; 33 | var m = d * 2000 + (1 - d) * 15000; 34 | var t = random.NormallyDistributedSingle4(4000, m, 1000, 40000); 35 | 36 | yield new ShapeStar({ 37 | // name: StarName.Generate(random), 38 | position: pos, 39 | temperature: t, 40 | }); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/galaxy-shape/Spiral.ts: -------------------------------------------------------------------------------- 1 | import { Vector3 } from 'three'; 2 | 3 | import { RandomObject } from '../utils'; 4 | 5 | import { BasicShape } from './BasicShape'; 6 | import { Cluster } from './Cluster'; 7 | import { Sphere } from './Sphere'; 8 | 9 | export type SpiralShapeOptions = { 10 | /** Min. 120 */ 11 | size: number; 12 | swirl: number; 13 | /** Min space between stars */ 14 | spacing: number; 15 | minimumArms: number; 16 | maximumArms: number; 17 | clusterCountDeviation: number; 18 | clusterCenterDeviation: number; 19 | minArmClusterScale: number; 20 | armClusterScaleDeviation: number; 21 | maxArmClusterScale: number; 22 | centerClusterScale: number; 23 | centerClusterDensityMean: number; 24 | centerClusterDensityDeviation: number; 25 | centerClusterSizeDeviation: number; 26 | centerClusterCountMean: number; 27 | centerClusterCountDeviation: number; 28 | centerClusterPositionDeviation: number; 29 | centralVoidSizeMean: number; 30 | centralVoidSizeDeviation: number; 31 | }; 32 | 33 | const defaultOptions: SpiralShapeOptions = { 34 | size: 750, 35 | swirl: Math.PI * 4, 36 | spacing: 5, 37 | minimumArms: 3, 38 | maximumArms: 7, 39 | clusterCountDeviation: 0.35, 40 | clusterCenterDeviation: 0.2, 41 | minArmClusterScale: 0.02, 42 | armClusterScaleDeviation: 0.02, 43 | maxArmClusterScale: 0.1, 44 | centerClusterScale: 0.19, 45 | centerClusterDensityMean: 0.00005, 46 | centerClusterDensityDeviation: 0.000005, 47 | centerClusterSizeDeviation: 0.00125, 48 | centerClusterCountMean: 20, 49 | centerClusterCountDeviation: 3, 50 | centerClusterPositionDeviation: 0.075, 51 | centralVoidSizeMean: 25, 52 | centralVoidSizeDeviation: 7, 53 | }; 54 | 55 | export class Spiral implements BasicShape { 56 | public readonly options: SpiralShapeOptions; 57 | constructor(options: Partial = defaultOptions) { 58 | this.options = { ...defaultOptions, ...options }; 59 | } 60 | 61 | // * GenerateShape(random) { 62 | // return this.Generate(random) 63 | // } 64 | *Generate(random: RandomObject) { 65 | const { centralVoidSizeDeviation, centralVoidSizeMean } = this.options; 66 | 67 | try { 68 | let centralVoidSize = random.NormallyDistributedSingle(centralVoidSizeDeviation, centralVoidSizeMean); 69 | if (centralVoidSize < 0) centralVoidSize = 0; 70 | const centralVoidSizeSqr = centralVoidSize * centralVoidSize; 71 | 72 | for (const star of this.GenerateArms(random)) { 73 | if (star.position.lengthSq() > centralVoidSizeSqr) yield star; 74 | } 75 | 76 | for (const star of this.GenerateCenter(random)) { 77 | if (star.position.lengthSq() > centralVoidSizeSqr) yield star; 78 | } 79 | 80 | for (const star of this.GenerateBackgroundStars(random)) { 81 | if (star.position.lengthSq() > centralVoidSizeSqr) yield star; 82 | } 83 | } catch (err) { 84 | console.error('ERR>', err); 85 | } 86 | } 87 | 88 | GenerateBackgroundStars(random: RandomObject) { 89 | const { size } = this.options; 90 | return new Sphere(size, 0.000001, 0.0000001, 0.35, 0.125, 0.35).Generate(random); 91 | } 92 | 93 | *GenerateArms(random: RandomObject) { 94 | const { 95 | size, 96 | swirl, 97 | spacing, 98 | minimumArms, 99 | maximumArms, 100 | clusterCountDeviation, 101 | clusterCenterDeviation, 102 | armClusterScaleDeviation, 103 | minArmClusterScale, 104 | maxArmClusterScale, 105 | } = this.options; 106 | 107 | try { 108 | const arms = random.integer(minimumArms, maximumArms); 109 | const armAngle = (Math.PI * 2) / arms; 110 | 111 | const maxClusters = size / spacing / arms; 112 | for (let arm = 0; arm < arms; arm++) { 113 | const clusters = parseInt( 114 | random.NormallyDistributedSingle(maxClusters * clusterCountDeviation, maxClusters).toFixed() 115 | ); 116 | for (let i = 0; i < clusters; i++) { 117 | //Angle from center of this arm 118 | const angle = random.NormallyDistributedSingle(0.5 * armAngle * clusterCenterDeviation, 0) + armAngle * arm; 119 | //Distance along this arm 120 | const dist = Math.abs(random.NormallyDistributedSingle(size * 0.4, 0)); 121 | //Center of the cluster 122 | const center = new Vector3(0, 0, dist); 123 | center.applyAxisAngle(new Vector3(0, 1, 0), angle); 124 | //const center = Vector3.Transform(new Vector3(0, 0, dist), Quaternion.CreateFromAxisAngle(new Vector3(0, 1, 0), angle)); 125 | 126 | //size of the cluster 127 | const clsScaleDev = armClusterScaleDeviation * size; 128 | const clsScaleMin = minArmClusterScale * size; 129 | const clsScaleMax = maxArmClusterScale * size; 130 | const cSize = random.NormallyDistributedSingle4( 131 | clsScaleDev, 132 | clsScaleMin * 0.5 + clsScaleMax * 0.5, 133 | clsScaleMin, 134 | clsScaleMax 135 | ); 136 | 137 | const densityMean = 0.00025; 138 | const stars = new Sphere(cSize, densityMean, undefined, 1, 1, 1).Generate(random); 139 | for (const star of stars) { 140 | yield star.Offset(center).swirl(new Vector3(0, 1, 0), swirl); 141 | } 142 | } 143 | } 144 | } catch (err) { 145 | console.error('ERR>', err); 146 | } 147 | } 148 | 149 | *GenerateCenter(random: RandomObject) { 150 | const { 151 | size, 152 | swirl, 153 | centerClusterDensityDeviation, 154 | centerClusterDensityMean, 155 | centerClusterCountMean, 156 | centerClusterScale, 157 | centerClusterCountDeviation, 158 | centerClusterPositionDeviation, 159 | } = this.options; 160 | 161 | try { 162 | //Add a single central cluster 163 | const sphere = new Sphere( 164 | size * centerClusterScale, //size: 165 | centerClusterDensityMean, //densityMean: 166 | centerClusterDensityDeviation, //densityDeviation: 167 | centerClusterScale, //deviationX: 168 | centerClusterScale, //deviationY: 169 | centerClusterScale //deviationZ: 170 | ); 171 | 172 | const cluster = new Cluster( 173 | sphere, 174 | centerClusterCountMean, 175 | centerClusterCountDeviation, 176 | size * centerClusterPositionDeviation, 177 | size * centerClusterPositionDeviation, 178 | size * centerClusterPositionDeviation 179 | ); 180 | 181 | for (const star of cluster.Generate(random)) { 182 | yield star.swirl(new Vector3(0, 1, 0), swirl * 5); 183 | } 184 | } catch (err) { 185 | console.error('!', err); 186 | } 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/galaxy-shape/index.ts: -------------------------------------------------------------------------------- 1 | export * from './BasicShape'; 2 | export * from './Cluster'; 3 | export * from './Grid'; 4 | export * from './ShapeStar'; 5 | export * from './Sphere'; 6 | export * from './Spiral'; 7 | -------------------------------------------------------------------------------- /src/galaxy/basic-generator.ts: -------------------------------------------------------------------------------- 1 | import { RandomObject } from '../utils'; 2 | 3 | /** 4 | * Basic class to handle model logic 5 | */ 6 | export class ModelHandler { 7 | readonly schemaName: string = 'noname-model'; 8 | constructor(public readonly model: ObjectModel) {} 9 | 10 | /** 11 | * update stored model with value 12 | * @param fieldName key name in model 13 | * @param value value to insert into Model[fieldName] 14 | * @return inserted value 15 | */ 16 | updateModel(fieldName: T, value: ObjectModel[T]) { 17 | this.model[fieldName] = value; 18 | return this.model[fieldName]; 19 | } 20 | 21 | toModel(model: Partial = {}): ObjectModel { 22 | return { schemaName: this.schemaName, ...this.model, ...model }; 23 | } 24 | 25 | /** 26 | * @returns plain Model for JSON conversion 27 | */ 28 | toJSON(): ObjectModel { 29 | return this.toModel(); 30 | } 31 | } 32 | 33 | /** 34 | * Pure generator class with options 35 | */ 36 | export class PureGenerator extends ModelHandler { 37 | override readonly schemaName: string = 'noname-pure-generator'; 38 | constructor(model: ObjectModel, public readonly options: Options) { 39 | super(model); 40 | } 41 | 42 | override toModel(model: Partial = {}): ObjectModel { 43 | return super.toModel({ options: this.options, ...model }); 44 | } 45 | } 46 | 47 | /** 48 | * Pure generator extended with random 49 | */ 50 | export interface RandomGeneratorOptions { 51 | seed?: number; 52 | random?: RandomObject; 53 | } 54 | export class RandomGenerator extends PureGenerator< 55 | ObjectModel, 56 | Options 57 | > { 58 | override readonly schemaName: string = 'unnamed-basic-model-generator'; 59 | protected readonly random: RandomObject; 60 | constructor(model: ObjectModel, options: Options) { 61 | super(model, options); 62 | 63 | if (!options.seed) this.options.seed = Date.now(); // todo we get same seeds for many items created asynchronously 64 | this.random = options.random || new RandomObject(this.options.seed); 65 | } 66 | 67 | override toModel(model: Partial = {}): ObjectModel { 68 | const { random, ...options } = this.options; // exclude RandomObject 69 | return super.toModel({ options, ...model }); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/galaxy/galaxy-generator.ts: -------------------------------------------------------------------------------- 1 | import { Vector3 } from 'three'; 2 | 3 | import { BasicShape, Grid, Spiral } from '../galaxy-shape'; 4 | import { GalaxyClass, GalaxyClassShape, Position } from '../interfaces'; 5 | import { capitalize, codename } from '../utils'; 6 | import { Names } from '../utils/Names'; 7 | import { StarName } from '../utils/StarName'; 8 | 9 | import { RandomGenerator, RandomGeneratorOptions } from './basic-generator'; 10 | import { StarPhysics } from './physic'; 11 | import { SystemGenerator, SystemModel } from './system'; 12 | 13 | export interface GalaxyModel { 14 | id?: string; 15 | path?: string; 16 | systemsSeed?: number; // todo 17 | name?: string; 18 | position?: Position; 19 | classification?: GalaxyClass; 20 | systems?: SystemModel[]; 21 | 22 | options?: {}; 23 | } 24 | 25 | export interface GalaxyOptions extends RandomGeneratorOptions { 26 | grid: { size: number; spacing: number }; // todo 27 | spiral: { size: number }; // todo 28 | } 29 | 30 | const defaultOptions: GalaxyOptions = { 31 | grid: { size: 100, spacing: 30 }, 32 | spiral: { size: 400 }, 33 | }; 34 | 35 | export class GalaxyGenerator extends RandomGenerator { 36 | override schemaName = 'GalaxyModel'; 37 | private readonly systems: SystemGenerator[] = []; 38 | 39 | constructor(model: GalaxyModel, options: Partial = defaultOptions) { 40 | super(model, { ...defaultOptions, ...model.options, ...options }); 41 | 42 | if (!model.name) this.model.name = capitalize(Names.GenerateGalaxyName(this.random)); // todo name generator should be static inside Galaxy? 43 | if (!model.id) this.model.id = codename(this.model.name); 44 | if (!model.path) this.model.path = codename(this.model.name); 45 | if (!model.position) this.model.position = new Vector3(); 46 | 47 | // todo check that 48 | this.systems = model.systems?.map((system) => new SystemGenerator(system)) || []; 49 | 50 | this.setClassification(); 51 | } 52 | 53 | setClassification(classification?: GalaxyClass) { 54 | if (!this.model.classification) { 55 | const classificationT = this.random?.choice(Object.values(GalaxyClassShape)); 56 | this.model.classification = classification || classificationT; 57 | } 58 | } 59 | 60 | getShape(): BasicShape { 61 | switch (this.model.classification) { 62 | case 'spiral': 63 | return new Spiral(this.options.spiral); 64 | case 'grid': 65 | default: 66 | return new Grid(this.options.grid.size, this.options.grid.spacing); 67 | } 68 | } 69 | 70 | *generateSystems() { 71 | const shape = this.getShape(); 72 | for (const system of shape.Generate(this.random)) { 73 | // CHECK UNIQUE SEED 74 | let systemSeed = this.random.next(); 75 | while (this.systems.find((system) => system.options.seed == systemSeed)) systemSeed = this.random.next(); 76 | let systemName = StarName.Generate(this.random); 77 | while ( 78 | this.systems.find((system) => { 79 | return system.model.name?.toLowerCase() == systemName.toLowerCase(); 80 | }) 81 | ) 82 | systemName = StarName.Generate(this.random); 83 | 84 | const systemGenerator = new SystemGenerator( 85 | { 86 | name: systemName, 87 | parentPath: this.model.path, 88 | position: system.position, 89 | temperature: system.temperature, // todo not needed? 90 | }, 91 | { 92 | seed: systemSeed, 93 | spectralClass: StarPhysics.getSpectralByTemperature(system.temperature as number)?.class, 94 | } 95 | ); 96 | this.systems.push(systemGenerator); 97 | yield systemGenerator; 98 | } 99 | // this.fillStatistics(); 100 | } 101 | 102 | override toModel(): Required { 103 | // @ts-ignore 104 | return { ...this.model, options: this.options, systems: this.systems.map((system) => system.toModel()) }; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/galaxy/index.ts: -------------------------------------------------------------------------------- 1 | export * from './physic'; 2 | export * from './galaxy-generator'; 3 | export * from './star'; 4 | export * from './system'; 5 | export * from './planet'; 6 | -------------------------------------------------------------------------------- /src/galaxy/physic/index.ts: -------------------------------------------------------------------------------- 1 | export * from './orbit-physic'; 2 | export * from './planet-physic'; 3 | export * from './star-physic'; 4 | export * from './system-physic'; 5 | -------------------------------------------------------------------------------- /src/galaxy/physic/orbit-generator.ts: -------------------------------------------------------------------------------- 1 | import { RandomObject } from '../../utils'; 2 | import { RandomGenerator, RandomGeneratorOptions } from '../basic-generator'; 3 | 4 | import { OrbitPhysicModel, SystemZone } from './orbit-physic'; 5 | import { StarPhysicModel } from './star-physic'; 6 | 7 | enum SystemBodyType { 8 | EMPTY = 'EMPTY', 9 | PLANET = 'PLANET', 10 | ASTEROID_BELT = 'ASTEROID_BELT', 11 | } 12 | // enum PlanetType { 13 | // lava = 'lava', 14 | // barren = 'barren', 15 | // desert = 'desert', 16 | // earth = 'earth', 17 | // ocean = 'ocean', 18 | // ice = 'ice', 19 | // ice_giant = 'ice_giant', 20 | // gas_giant = 'gas_giant', 21 | // EMPTY = 'EMPTY', 22 | // } 23 | 24 | export interface OrbitModel extends OrbitPhysicModel { 25 | schemaName?: 'orbit-model'; 26 | /** zone in system */ 27 | zone?: SystemZone; 28 | /** orbiting body type */ 29 | bodyType?: SystemBodyType; 30 | // /** planet type */ 31 | // subtype?: PlanetType; 32 | } 33 | 34 | interface OrbitOptions extends RandomGeneratorOptions { 35 | maxInclinationDeg: number; 36 | } 37 | 38 | const defaultOptions = { 39 | maxInclinationDeg: 15, 40 | }; 41 | 42 | const denormalize = (normalized: number, min: number, max: number) => normalized * (max - min) + min; 43 | const normalize = (value: number, min: number, max: number) => (value - min) / (max - min); 44 | 45 | export class OrbitGenerator extends RandomGenerator { 46 | override schemaName = 'orbit-model'; 47 | 48 | protected tags: string[] = []; 49 | protected lock: boolean = false; 50 | 51 | constructor(model: OrbitModel, options: Partial = {}) { 52 | super(model, { ...defaultOptions, ...options }); 53 | // this.distance = this.cutDecimals(props.distance, 2); 54 | this.generateOrbit(); 55 | } 56 | 57 | generateOrbit() { 58 | const { maxInclinationDeg } = this.options; 59 | const pow = 3; 60 | const temp1 = 15; 61 | // todo near orbit has orbit inclination closer to 0 62 | // const temp1 = Math.pow(this.model.distance, pow); 63 | 64 | const random = this.random.integer(-temp1, temp1); 65 | let inclination = normalize(Math.pow(random, pow), Math.pow(-temp1, pow), Math.pow(temp1, pow)); 66 | inclination = denormalize(inclination, -maxInclinationDeg, maxInclinationDeg); 67 | 68 | this.updateModel('inclination', inclination); 69 | this.updateModel('longitude', this.random.integer(-180, 180)); 70 | this.updateModel('anomaly', this.random.integer(-180, 180)); 71 | } 72 | 73 | setTags(tags: string[]) { 74 | this.tags = tags; 75 | } 76 | hasTag(tagName: string) { 77 | return this.tags.includes(tagName); 78 | } 79 | lockTag(tags: OrbitGenerator['tags'] | OrbitGenerator['tags'][0]) { 80 | if (!Array.isArray(tags)) tags = [tags]; 81 | this.lock = true; 82 | this.tags = tags; 83 | } 84 | markAsEmpty() { 85 | this.lock = true; 86 | this.tags = []; 87 | } 88 | 89 | generateType(random: RandomObject) { 90 | const tags = this.tags; 91 | if (tags.length == 0 || (tags.length == 1 && tags[0] == 'EMPTY')) { 92 | this.updateModel('bodyType', SystemBodyType.EMPTY); 93 | // this.updateModel('subtype', PlanetType.EMPTY); 94 | return; 95 | } 96 | const weighted: [number, string][] = []; 97 | for (const tag of tags) { 98 | const orbitObject = ORBIT_OBJECT_TYPES.find((ot) => ot.type == tag); 99 | if (!orbitObject) continue; 100 | weighted.push([orbitObject.probability, tag]); 101 | } 102 | const bodyType = random.weighted(weighted); 103 | // const subtype = random.weighted(weighted); 104 | // this.updateModel('subtype', subtype); 105 | // const type = ['EMPTY', 'ASTEROID_BELT'].includes(subtype) ? subtype : 'PLANET'; 106 | this.updateModel('bodyType', bodyType); 107 | } 108 | 109 | // cutDecimals(number: number, position = 2) { 110 | // const factor = Math.pow(10, position); 111 | // return Math.floor(number * factor) / factor; 112 | // } 113 | 114 | // static MOONS_TOPOLOGIES = [ 115 | // // { probability: 1, name: 'EMPTY' }, 116 | // { probability: .5, name: 'terrestial_moons', modificators: [Orbit.TerrestialMoons] }, // ['barren', 'ice'] 117 | // { probability: .5, name: 'terrestial_one_moon' }, 118 | // { probability: .5, name: 'giant_moons', modificators: [Orbit.GiantMoons] }, 119 | // { probability: .5, name: 'giant_habitable_moon', modificators: [Orbit.GiantMoons, Orbit.HabitableMoon] }, 120 | // ] 121 | // generateMoons(random, options) { 122 | // // if (this.type == 'gas_giant' && this.zone == 'habitable') { 123 | // // this.moons = [ 124 | // // { type: 'earth' } 125 | // // ] 126 | // // } 127 | // } 128 | // static HabitableMoon() {} 129 | // static TerrestialMoons() {} 130 | // static GiantMoons() {} 131 | } 132 | 133 | export const ORBIT_OBJECT_TYPES: { 134 | type: OrbitModel['bodyType']; 135 | probability: number; 136 | when: (star: StarPhysicModel, orbit: OrbitPhysicModel) => boolean; 137 | }[] = [ 138 | { type: SystemBodyType.EMPTY, probability: 0.05, when: (star: StarPhysicModel, orbit: OrbitPhysicModel) => true }, 139 | // { 140 | // type: PlanetType.lava, 141 | // probability: 0.2, 142 | // when: (star: StarPhysicModel, orbit: OrbitPhysicModel) => orbit.distance < star.habitable_zone_inner * 0.7, 143 | // }, 144 | // { 145 | // type: PlanetType.barren, 146 | // probability: 0.1, 147 | // when: (star: StarPhysicModel, orbit: OrbitPhysicModel) => orbit.distance > star.habitable_zone_inner * 0.18, 148 | // }, 149 | // { 150 | // type: PlanetType.desert, 151 | // probability: 0.2, 152 | // when: (star: StarPhysicModel, orbit: OrbitPhysicModel) => 153 | // orbit.distance > star.habitable_zone_inner * 0.7 && orbit.distance < star.frost_line, 154 | // }, 155 | { 156 | type: SystemBodyType.ASTEROID_BELT, 157 | probability: 0.2, 158 | when: (star: StarPhysicModel, orbit: OrbitPhysicModel) => orbit.distance > star.frost_line * 0.1, 159 | }, 160 | { 161 | type: SystemBodyType.PLANET, 162 | probability: 1, 163 | when: (star: StarPhysicModel, orbit: OrbitPhysicModel) => true, 164 | }, 165 | // { 166 | // type: PlanetType.earth, 167 | // probability: 1, 168 | // when: (star: StarPhysicModel, orbit: OrbitPhysicModel) => 169 | // orbit.distance > star.habitable_zone_inner && orbit.distance < star.habitable_zone_outer, 170 | // }, 171 | // { 172 | // type: PlanetType.ocean, 173 | // probability: 0.3, 174 | // when: (star: StarPhysicModel, orbit: OrbitPhysicModel) => 175 | // orbit.distance > star.habitable_zone_inner && orbit.distance < star.frost_line, 176 | // }, 177 | // { 178 | // type: PlanetType.ice, 179 | // probability: 0.3, 180 | // when: (star: StarPhysicModel, orbit: OrbitPhysicModel) => orbit.distance > star.frost_line, 181 | // }, 182 | // { 183 | // type: PlanetType.gas_giant, 184 | // probability: 0.5, 185 | // when: (star: StarPhysicModel, orbit: OrbitPhysicModel) => 186 | // orbit.distance > star.frost_line && orbit.distance < star.frost_line + (star.outer_limit - star.frost_line) * 0.5, 187 | // }, 188 | // { 189 | // type: PlanetType.ice_giant, 190 | // probability: 0.6, 191 | // when: (star: StarPhysicModel, orbit: OrbitPhysicModel) => 192 | // orbit.distance > star.frost_line && orbit.distance > star.frost_line + (star.outer_limit - star.frost_line) * 0.1, 193 | // }, 194 | ]; 195 | -------------------------------------------------------------------------------- /src/galaxy/physic/orbit-physic.ts: -------------------------------------------------------------------------------- 1 | import { StarPhysicModel } from './star-physic'; 2 | 3 | export interface OrbitPhysicModel { 4 | /** 5 | * (r radius, Au) average distance from center of mass, 6 | * here we have perfect round orbit, so: distance = semiMajorAxis = semiMinorAxis 7 | */ 8 | distance: number; 9 | /** (i inclination, DEG) inclination (nachylenie orbity) */ 10 | inclination?: number; 11 | /** (Ω Omega, 0-360 DEG) longitude of the ascending node (długość węzła wstępującego) */ 12 | longitude?: number; 13 | /** (θ theta, 0-360 DEG) true anomaly (anomalia prawdziwa) */ 14 | anomaly?: number; 15 | /** (P, EARTH YEAR) orbital period (okres orbitalny/rok ziemski) */ 16 | orbitalPeriod?: number; 17 | /** (x, EARTH DEYS) orbital period in days (okres orbitalny/dzień ziemski) */ 18 | orbitalPeriodInDays?: number; 19 | /** sequential order from center */ 20 | order: number; 21 | 22 | // todo proposal: eliptic orbits 23 | // semiMajorAxis?: number; // (a) półoś wielka 24 | // semiMinorAxis?: number; // (b) półoś mała 25 | // eccentricity?: number; // (e, 0-1) ekscentryczność/mimośród 26 | // argumentOfPeriapsis?: number; // (ω, omega, 0-360 DEG) argument perycentrum 27 | // todo: not needed? position is calculated with Simulation Clock 28 | // orbitalVelocity?: number; // (Vo, EARTH SPEED) prędkość orbitalna 29 | } 30 | 31 | export enum SystemZone { 32 | Habitable = 'habitable', 33 | Inner = 'inner', 34 | Outer = 'outer', 35 | } 36 | 37 | export class OrbitPhysic { 38 | private constructor() {} 39 | 40 | static readonly EARTH_YEAR_IN_DAYS = 365; 41 | 42 | /** 43 | * @param centerMass center of mass mass 44 | * @param distance average distance from center of mass 45 | * @returns orbital period in EARTH_YEARS 46 | */ 47 | static calcOrbitalPeriod(centerMass: number, distance: number) { 48 | return Math.sqrt(Math.pow(distance, 3) / centerMass); 49 | } 50 | 51 | /** 52 | * @param orbitalPeriod orbital period in EARTH_YEAR 53 | * @returns orbital period in EARTH_DAYS 54 | */ 55 | static convertOrbitalPeriodToDays(orbitalPeriod: number) { 56 | return Math.floor(orbitalPeriod * this.EARTH_YEAR_IN_DAYS); 57 | } 58 | 59 | static calcZone(distance: number, physic: StarPhysicModel) { 60 | switch (true) { 61 | case distance > physic.habitable_zone_inner && distance < physic.habitable_zone_outer: 62 | return SystemZone.Habitable; 63 | case distance < physic.frost_line: 64 | return SystemZone.Inner; 65 | case distance > physic.frost_line: 66 | default: 67 | return SystemZone.Outer; 68 | } 69 | } 70 | 71 | static sortByDistance(mx: OrbitPhysicModel, my: OrbitPhysicModel) { 72 | return mx.distance - my.distance; 73 | } 74 | 75 | // todo 76 | // createInclination() { 77 | // if (this.inclination != null) return this.inclination; 78 | // if (this.subtype != null) { 79 | // const direction = Math.random() > 0.5 ? 1 : -1; 80 | // this.inclination = Math.random() > 0.33 ? 0 : direction * Math.floor(Math.random() * 15); 81 | // } else throw { subtype: 'create planet subtype first' }; 82 | // return this.inclination; 83 | // } 84 | } 85 | -------------------------------------------------------------------------------- /src/galaxy/physic/planet-physic.ts: -------------------------------------------------------------------------------- 1 | import { OrbitPhysicModel } from './orbit-physic'; 2 | import { StarPhysicModel } from './star-physic'; 3 | 4 | export interface PlanetPhysicModel { 5 | /** (kg) planet mass */ // todo in EARTH MASS? 6 | mass: number; 7 | /** (g/cm3) planet density */ 8 | density: number; 9 | /** (km) planet radius */ 10 | radius: number; 11 | /** (EARTH DAY) full rotation, solar day length */ 12 | rotationPeriod: number; 13 | /** (axial tilt, 0-180 DEG) angle between planet rotational axis and its orbital axis */ 14 | obliquity: number; 15 | } 16 | 17 | const SECONDS_IN_DAY = 86400; 18 | const G = 6.6743e-11; // gravitational constant 19 | const EARTH_MASS_IN_KG = 5.972e24; // Earth mass in kg 20 | const AU_IN_M = 1.496e11; // Astronomical unit in meters 21 | 22 | const EARTH_RADIUS = 6371; 23 | const JUPITER_MASS_IN_EARTH_MASS = 317.83; 24 | const JUPITER_RADIUS_IN_EARTH_RADIUS = 11.209; 25 | const JUPITER_RADIUS = EARTH_RADIUS * JUPITER_RADIUS_IN_EARTH_RADIUS; 26 | 27 | type MinMax = [min: number, max: number]; 28 | const HABITABLE_WORLD_DENSITY: MinMax = [3, 8]; // h/cm3 29 | const TERRAN_MASS_RANGE: MinMax = [0.1, 10]; 30 | const PLANET_MASS: MinMax = [0.1, 13 * JUPITER_MASS_IN_EARTH_MASS]; 31 | const PLANET_RADIUS: MinMax = [0.5, 3 * JUPITER_RADIUS_IN_EARTH_RADIUS]; 32 | 33 | export interface PlanetClassifier { 34 | class: string; 35 | subClass: string; 36 | mass: MinMax; 37 | radius: MinMax; 38 | cmf?: MinMax; 39 | gravity?: MinMax; 40 | probability: number; 41 | color: string[]; 42 | when: (star: StarPhysicModel, orbit: OrbitPhysicModel) => boolean; 43 | } 44 | 45 | const PLANET_CLASSIFICATION: PlanetClassifier[] = [ 46 | { 47 | class: 'lava', 48 | subClass: 'terrestial', 49 | mass: [0.1, 2], 50 | radius: [0.5, 1.2], 51 | // gravity: [0.4, 1.6], 52 | // cmf: [0.3, 0.4], 53 | probability: 0.2, 54 | color: ['#FF5722'], 55 | when: (star: StarPhysicModel, orbit: OrbitPhysicModel) => orbit.distance < star.habitable_zone_inner * 0.7, 56 | }, 57 | { 58 | /* rocky planet without surface water */ 59 | class: 'rocky', 60 | subClass: 'terrestial', 61 | mass: [0.1, 10], 62 | radius: [0.5, 1.5], 63 | gravity: [0.4, 1.6], 64 | cmf: [0.3, 0.4], 65 | probability: 0.2, 66 | color: ['#9E9E9E'], 67 | when: () => true, 68 | }, 69 | { 70 | /* earth like planet, with ocean and landmasses */ 71 | class: 'terran', 72 | subClass: 'terrestial', 73 | mass: [0.1, 10], 74 | radius: [0.5, 1.5], 75 | gravity: [0.4, 1.6], 76 | cmf: [0.3, 0.4], 77 | probability: 1, 78 | color: ['#4CAF50'], 79 | // color: ['green'], 80 | when: (star: StarPhysicModel, orbit: OrbitPhysicModel) => 81 | orbit.distance > star.habitable_zone_inner && orbit.distance < star.habitable_zone_outer, 82 | }, 83 | { 84 | /* ocean planet without core, or with very small = no resources available, no advanced life */ 85 | class: 'coreless-watery', 86 | subClass: 'liquid', 87 | mass: [0.1, 10], 88 | radius: [0.5, 1.5], 89 | probability: 0.1, 90 | color: ['#2196F3'], 91 | when: (star: StarPhysicModel, orbit: OrbitPhysicModel) => 92 | orbit.distance > star.habitable_zone_inner && orbit.distance < star.frost_line, 93 | }, 94 | { 95 | /* ocean planet with core and islands */ 96 | class: 'watery', 97 | subClass: 'terrestial', 98 | mass: [0.1, 10], 99 | radius: [0.5, 1.5], 100 | probability: 0.2, 101 | color: ['#00BCD4'], 102 | when: (star: StarPhysicModel, orbit: OrbitPhysicModel) => 103 | orbit.distance > star.habitable_zone_inner && orbit.distance < star.frost_line, 104 | }, 105 | { 106 | class: 'icy', 107 | subClass: 'terrestial', 108 | mass: [0.1, 10], 109 | radius: [0.5, 1.5], 110 | probability: 0.2, 111 | color: ['#03A9F4'], 112 | when: (star: StarPhysicModel, orbit: OrbitPhysicModel) => orbit.distance > star.frost_line, 113 | }, 114 | { 115 | /* hot ice planet - enought big mass (and graviti) keeps ice under big pressure, don't allow melt even in 700K */ 116 | class: 'hot-icy', 117 | subClass: 'terrestial', 118 | mass: [3, 10], 119 | radius: [0.5, 1.5], 120 | probability: 0.05, 121 | color: ['#E91E63'], 122 | when: (star: StarPhysicModel, orbit: OrbitPhysicModel) => orbit.distance < star.habitable_zone_inner * 0.5, 123 | }, 124 | { 125 | /* iron reach and big core, form close to star, where asteroid has heavy elements */ 126 | class: 'super_mercury', 127 | subClass: 'terrestial', 128 | mass: [1, 10], 129 | radius: [0.5, 1.5], 130 | probability: 0.05, 131 | color: ['#FFC107'], 132 | when: (star: StarPhysicModel, orbit: OrbitPhysicModel) => orbit.distance < star.habitable_zone_inner, 133 | }, 134 | 135 | { 136 | class: 'puffy_giant', 137 | subClass: 'gas', 138 | mass: [1 * JUPITER_MASS_IN_EARTH_MASS, 2 * JUPITER_MASS_IN_EARTH_MASS], 139 | radius: [1 * JUPITER_RADIUS_IN_EARTH_RADIUS, 3 * JUPITER_RADIUS_IN_EARTH_RADIUS], 140 | probability: 0.1, 141 | color: ['#FF9800'], 142 | when: (star: StarPhysicModel, orbit: OrbitPhysicModel) => orbit.distance < star.frost_line * 0.5, 143 | }, 144 | { 145 | class: 'jupiter', // jupiter like 146 | subClass: 'gas', 147 | mass: [10, 2 * JUPITER_MASS_IN_EARTH_MASS], 148 | radius: [0.9 * JUPITER_RADIUS_IN_EARTH_RADIUS, 1.5 * JUPITER_RADIUS_IN_EARTH_RADIUS], 149 | probability: 0.3, 150 | color: ['#FF5722'], 151 | when: (star: StarPhysicModel, orbit: OrbitPhysicModel) => 152 | orbit.distance > star.frost_line && orbit.distance < star.outer_limit * 0.7, 153 | // orbit.distance < star.frost_line + (star.outer_limit - star.frost_line) * 0.4, 154 | }, 155 | { 156 | /* jupiter migrated from behind frost_line (todo: earth like planet with 2Me can be created after migration) */ 157 | class: 'hot_jupiter', 158 | subClass: 'gas', 159 | mass: [1 * JUPITER_MASS_IN_EARTH_MASS, 2 * JUPITER_MASS_IN_EARTH_MASS], 160 | radius: [0.9 * JUPITER_RADIUS_IN_EARTH_RADIUS, 1.5 * JUPITER_RADIUS_IN_EARTH_RADIUS], 161 | probability: 0.05, 162 | color: ['#F44336'], 163 | when: (star: StarPhysicModel, orbit: OrbitPhysicModel) => orbit.distance > 0.04 && orbit.distance < 0.5, 164 | }, 165 | { 166 | class: 'super_jupiter', 167 | subClass: 'gas', 168 | mass: [2 * JUPITER_MASS_IN_EARTH_MASS, 13 * JUPITER_MASS_IN_EARTH_MASS], 169 | radius: [0.8 * JUPITER_RADIUS_IN_EARTH_RADIUS, 1.2 * JUPITER_RADIUS_IN_EARTH_RADIUS], 170 | probability: 0.3, 171 | color: ['#9C27B0'], 172 | when: (star: StarPhysicModel, orbit: OrbitPhysicModel) => 173 | orbit.distance > star.frost_line + 1 && orbit.distance < star.frost_line + 2, 174 | }, 175 | { 176 | class: 'gas_dwarf', 177 | subClass: 'gas', 178 | mass: [1, 20], 179 | radius: [2, 0.8 * JUPITER_RADIUS_IN_EARTH_RADIUS], 180 | probability: 0.2, 181 | color: ['#FFEB3B'], 182 | when: (star: StarPhysicModel, orbit: OrbitPhysicModel) => orbit.distance > star.outer_limit * 0.5, 183 | }, 184 | 185 | { 186 | class: 'ice_giant', // neptune like, todo hot_neptune 187 | subClass: 'ice', 188 | mass: [10, 50], 189 | radius: [3, 0.6 * JUPITER_RADIUS_IN_EARTH_RADIUS], 190 | probability: 0.2, 191 | color: ['#673AB7'], 192 | when: (star: StarPhysicModel, orbit: OrbitPhysicModel) => orbit.distance > star.frost_line * 1.2, 193 | }, 194 | ]; 195 | 196 | export type PlanetClass = typeof PLANET_CLASSIFICATION[number]['class']; 197 | export type PlanetSubClass = typeof PLANET_CLASSIFICATION[number]['subClass']; 198 | 199 | export class PlanetPhysic { 200 | private constructor() {} 201 | 202 | /* (km) */ 203 | static EARTH_RADIUS = EARTH_RADIUS; 204 | // static EARTH_MASS = 205 | // static JUPITER_MASS = 206 | static PLANET_MASS = PLANET_MASS; 207 | static PLANET_RADIUS = PLANET_RADIUS; 208 | 209 | static JUPITER_RADIUS = JUPITER_RADIUS; 210 | static JUPITER_MASS_IN_EARTH_MASS = JUPITER_MASS_IN_EARTH_MASS; 211 | static JUPITER_RADIUS_IN_EARTH_RADIUS = JUPITER_RADIUS_IN_EARTH_RADIUS; 212 | 213 | static readonly PLANET_CLASSIFICATION = PLANET_CLASSIFICATION; 214 | 215 | // todo needs check 216 | /** 217 | * @param radius planet radius 218 | * @returns rotation period in EARTH DAYS // todo in hours? 219 | */ 220 | static calcRotationalPeriod(mass: number, radius: number, distance: number): number { 221 | // Convert mass from Earth masses to kg 222 | const massInKg = mass * EARTH_MASS_IN_KG; 223 | // Convert radius from Earth radii to meters 224 | const radiusInMeters = radius * (EARTH_RADIUS * 1000); 225 | // Convert distance from AU to meters 226 | const distanceInMeters = distance * AU_IN_M; 227 | 228 | const period = 2 * Math.PI * Math.sqrt(Math.pow(distanceInMeters, 3) / (G * massInKg)); 229 | // Adjust for the planet's radius 230 | const circumference = 2 * Math.PI * radiusInMeters; 231 | const adjustedPeriod = period * (circumference / distanceInMeters); 232 | // console.log(period / SECONDS_IN_DAY, adjustedPeriod / SECONDS_IN_DAY); 233 | 234 | return adjustedPeriod / SECONDS_IN_DAY /* todo - probably return completle fictional values xD */ / 100; 235 | } 236 | 237 | static calcDensity(mass: number, cmf = 0.35) { 238 | if (mass > 0.6) return (5.51 * Math.pow(mass, 0.189)) / Math.pow(1.07 - 0.21 * cmf, 3); 239 | if ((5.51 * Math.pow(mass, 0.189)) / Math.pow(1.07 - 0.21 * cmf, 3) > 3.5 + 4.37 * cmf) 240 | return (5.51 * Math.pow(mass, 0.189)) / Math.pow(1.07 - 0.21 * cmf, 3); 241 | return 3.5 + 4.37 * cmf; 242 | } 243 | 244 | static calcRadius(mass: number, density: number) { 245 | return Math.pow(mass / (density / 5.51), 1 / 3); 246 | } 247 | 248 | static calcGravity(mass: number, radius: number) { 249 | return Math.pow(mass / radius, 2); 250 | } 251 | 252 | static getClass(planetClass: PlanetClass) { 253 | return this.PLANET_CLASSIFICATION.find((matrice) => matrice.class === planetClass) as PlanetClassifier; 254 | } 255 | static getClassColor(planetClass: string) { 256 | return this.getClass(planetClass)?.color[0] || 'white'; 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /src/galaxy/physic/system-physic.ts: -------------------------------------------------------------------------------- 1 | export interface SystemPhysicModel { 2 | // mass: number; 3 | color: string; 4 | starsCount: number; 5 | planetsCount: number; 6 | asteroidsCount: number; 7 | // radius: number; 8 | // volume: number; 9 | // density: number; 10 | // subtype: string; 11 | // stellar_class: SystemStellarClass; 12 | // // habitable: boolean; 13 | // evolution: boolean; 14 | // luminosity: number; 15 | // inner_limit: number; 16 | // outer_limit: number; 17 | // frost_line: number; 18 | // temperature: number; 19 | // surface_area: number; 20 | // circumference: number; 21 | // main_sequence_lifetime: number; 22 | // habitable_zone: number; 23 | // habitable_zone_inner: number; 24 | // habitable_zone_outer: number; 25 | } 26 | 27 | export enum SystemType { 28 | SINGLE_STAR = 'SINGLE_STAR', 29 | BINARY_STAR = 'BINARY_STAR', 30 | MULTIPLE_STAR = 'MULTIPLE_STAR', 31 | } 32 | 33 | // SINGLE STAR 34 | export const PLANETS_COUNT_IN_SINGLE_STAR_SYSTEM = { 35 | 0: 0.1, 36 | 1: 0.1, 37 | 2: 0.2, 38 | 3: 0.2, 39 | 4: 0.3, 40 | 5: 0.3, 41 | 6: 0.4, 42 | 7: 0.4, 43 | 8: 0.5, 44 | 9: 0.5, 45 | 10: 0.3, 46 | 11: 0.3, 47 | 12: 0.1, 48 | 13: 0.1, 49 | 14: 0.1, 50 | 15: 0.01, 51 | 16: 0.01, 52 | 17: 0.001, 53 | }; 54 | // @todo binary and multiple systems 55 | // const PLANETARY_SYSTEMS_TYPES = { 56 | // SINGLE_STAR: 'SINGLE_STAR', 57 | // BINARY_P_TYPE_STAR: 'BINARY_P_TYPE_STAR', 58 | // // MULTIPLE_BINARY_S_TYPE_STAR: 'MULTIPLE_BINARY_S_TYPE_STAR', 59 | // MULTIPLE_S_TYPE_STAR: 'MULTIPLE_S_TYPE_STAR', 60 | // }; 61 | // // 2 STARS RELATIVELY CLOSE 62 | // const PLANETS_COUNT_IN_BINARY_STAR_P_TYPE_SYSTEM = PLANETS_COUNT_IN_SINGLE_STAR_SYSTEM; 63 | // // 2 STARS FAR AWAY 64 | // const PLANETS_COUNT_IN_BINARY_STAR_S_TYPE_SYSTEM = { 65 | // 0: 1, 66 | // 1: 0.5, 67 | // 2: 0.1, 68 | // 3: 0.01, 69 | // 4: 0.001, 70 | // 5: 0.0001, 71 | // }; 72 | // const STAR_COUNT_DISTIBUTION_IN_BINARY_SUBSYSTEMS = { 73 | // 1: 0.2, 74 | // 2: 1, // WE WANT MORE CHANCE FOR BINARY P TYPE STARS 75 | // }; 76 | 77 | export const STAR_COUNT_DISTIBUTION_IN_SYSTEMS = { 78 | 1: 1, 79 | 2: 0.2, 80 | // 3: 0.05, 81 | // 4: 0.01, 82 | // 5: 0.005 83 | } as const; 84 | 85 | export class SystemPhysics { 86 | private constructor() {} 87 | } 88 | -------------------------------------------------------------------------------- /src/galaxy/planet/index.ts: -------------------------------------------------------------------------------- 1 | export * from './planet-generator.model'; 2 | export * from './planet-generator'; 3 | export * from './surface'; 4 | -------------------------------------------------------------------------------- /src/galaxy/planet/planet-generator.model.ts: -------------------------------------------------------------------------------- 1 | import { Vector3 } from 'three'; 2 | 3 | import { PlanetPath, SystemPath } from '../../global.types'; 4 | import { Seed } from '../../utils'; 5 | import { PlanetPhysicModel } from '../physic'; 6 | import { SystemOrbitModel } from '../system'; 7 | 8 | export enum RegionBiome { 9 | Ocean = 'ocean', 10 | } 11 | 12 | export interface RegionModel { 13 | id: string; 14 | path: string; 15 | biome?: RegionBiome; 16 | color?: string; 17 | corners: { x: number; y: number; z: number }[]; 18 | neighbors: string[]; 19 | effects?: {}[]; 20 | } 21 | 22 | export interface PlanetModel { 23 | id?: string; 24 | name?: string; 25 | path?: PlanetPath; 26 | parentPath?: SystemPath; 27 | // type?: string; 28 | radius?: number; 29 | physic?: PlanetPhysicModel; 30 | orbit?: SystemOrbitModel; 31 | // orbit?: SystemOrbitModel; // OrbitModel; 32 | regions?: RegionModel[]; 33 | options?: { 34 | seed?: Seed; 35 | surfaceSeed?: Seed; 36 | }; // todo generator options??? 37 | 38 | type?: 39 | | 'lava' 40 | | 'rocky' 41 | | 'terran' 42 | | 'coreless-watery' 43 | | 'watery' 44 | | 'icy' 45 | | 'hot_icy' 46 | | 'super_mercury' 47 | | 'puffy_giant' 48 | | 'jupiter' 49 | | 'hot_jupiter' 50 | | 'super_jupiter' 51 | | 'gas_dwarf' 52 | | 'ice_giant'; 53 | subType?: 'terrestial' | 'liquid' | 'ice'; 54 | schemaName?: 'PlanetModel'; 55 | } 56 | -------------------------------------------------------------------------------- /src/galaxy/planet/planet-generator.spec.ts: -------------------------------------------------------------------------------- 1 | import { PlanetGenerator } from './planet-generator'; 2 | 3 | describe('world-generator planet-generator.ts', () => { 4 | it('should generate regions', () => { 5 | const planet = new PlanetGenerator({}); 6 | expect(planet instanceof PlanetGenerator).toBeTruthy(); 7 | 8 | for (const region of planet.generateSurface()) { 9 | } 10 | expect(planet.regions.length).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/galaxy/planet/planet-generator.ts: -------------------------------------------------------------------------------- 1 | import { codename, decimalToRoman, Seed } from '../../utils'; 2 | import { RandomGenerator, RandomGeneratorOptions } from '../basic-generator'; 3 | import { OrbitPhysicModel, PlanetClassifier, PlanetPhysic, PlanetPhysicModel, StarPhysicModel } from '../physic'; 4 | import { OrbitModel } from '../physic/orbit-generator'; 5 | import { StarModel } from '../star'; 6 | 7 | import { PlanetSurfaceGenerator } from './surface/planet-surface-generator'; 8 | import { PlanetModel, RegionModel } from './planet-generator.model'; 9 | 10 | export interface PlanetOptions extends RandomGeneratorOptions { 11 | seed: Seed; 12 | surfaceSeed: Seed; 13 | // random?: RandomObject; 14 | star?: StarModel; 15 | planetType?: string; 16 | } 17 | const defaultOptions: PlanetOptions = { 18 | seed: 0, 19 | surfaceSeed: 0, 20 | // position: new Vector3(0, 0, 0), 21 | }; 22 | 23 | // export interface PlanetGeneratorModel { 24 | // model?: PlanetModel; 25 | // options?: PlanetOptions; 26 | // } 27 | 28 | export class PlanetGenerator extends RandomGenerator { 29 | override schemaName = 'PlanetModel'; 30 | public regions: RegionModel[]; 31 | private meta: PlanetClassifier; 32 | public physic: PlanetPhysicModel = { 33 | mass: 0, 34 | density: 0, 35 | radius: 0, 36 | rotationPeriod: 0, 37 | obliquity: 0, 38 | }; 39 | 40 | constructor(model: PlanetModel, options: Partial = defaultOptions) { 41 | super(model, { ...defaultOptions, ...model.options, ...options }); 42 | 43 | if (!this.options.surfaceSeed) this.options.surfaceSeed = this.random.seed(); 44 | 45 | if (!model.id) this.model.id = codename(this.model.name); 46 | if (!model.path) this.model.path = `${this.model.parentPath!}/p:${this.model.id!}`; 47 | this.regions = (model.regions as RegionModel[]) || []; 48 | 49 | const type = model.type || options.planetType; 50 | if (type) { 51 | this.meta = PlanetPhysic.getClass(type); 52 | } else { 53 | const availableClasses = PlanetPhysic.PLANET_CLASSIFICATION.filter((planetTopology) => 54 | planetTopology.when(this.options.star?.physic as StarPhysicModel, this.model.orbit as OrbitPhysicModel) 55 | ); 56 | this.meta = this.random.weighted(availableClasses.map((top) => [top.probability, top])) as PlanetClassifier; 57 | } 58 | 59 | this.model.type = this.meta.class as PlanetModel['type']; 60 | this.model.subType = this.meta.subClass as PlanetModel['subType']; 61 | this.model.radius = this.model.radius || this.random.real(this.meta.radius[0], this.meta.radius[1]); 62 | 63 | // this.generateTopology(); 64 | this.initializePhysic(); 65 | } 66 | 67 | initializePhysic() { 68 | const { model, physic, options } = this; 69 | // Object.assign(physic, options.orbit); 70 | physic.radius = model.radius || physic.radius; 71 | 72 | // physic.mass = model.mass || physic.mass; 73 | physic.mass = 1; 74 | physic.rotationPeriod = PlanetPhysic.calcRotationalPeriod(physic.mass, physic.radius, model.orbit?.distance || 1); 75 | } 76 | 77 | get subtype(): string { 78 | // @ts-ignore 79 | return this.model.subtype; 80 | } 81 | 82 | *generateSurface() { 83 | try { 84 | const surface = new PlanetSurfaceGenerator({}, { strategyName: this.model.type, seed: this.options.surfaceSeed }); 85 | surface.generateSurface(); 86 | this.regions = surface.planet.topology.tiles.map((tile) => ({ 87 | id: tile.id.toString(), 88 | path: `${this.model.path}/r:${tile.id.toString()}`, 89 | biome: tile.biome as RegionModel['biome'], 90 | color: tile.color ? `#${tile.color.getHexString()}` : this.meta.color[0], 91 | corners: tile.corners.map((corner) => corner.position), 92 | neighbors: tile.tiles.map((tile) => tile.id.toString()), 93 | })); 94 | // for (const region of surface.generateSurface()) { 95 | // yield region; 96 | // } 97 | 98 | for (let index = 0; index < this.regions.length; index++) { 99 | yield this.regions[index]; 100 | } 101 | } catch (error) { 102 | console.warn('*generateSurface()', error); 103 | } 104 | } 105 | 106 | static getSequentialName(systemName: string, planetIndex: number) { 107 | return `${systemName} ${decimalToRoman(planetIndex + 1)}`; 108 | } 109 | 110 | override toModel(): PlanetModel { 111 | const { star, ...options } = this.options; 112 | return super.toModel({ 113 | ...this.model, 114 | regions: this.regions, 115 | physic: { ...this.physic }, 116 | options: { ...options }, 117 | }); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/galaxy/planet/planet-surface-generator.ts: -------------------------------------------------------------------------------- 1 | import { RandomGenerator, RandomGeneratorOptions } from '../basic-generator'; 2 | 3 | import { PlanetGenerator } from './planet-generator'; 4 | import { SurfaceStrategies, SurfaceStrategy } from './strategy'; 5 | 6 | export interface PlanetSurfaceModel { 7 | options?: {}; 8 | } 9 | 10 | export interface PlanetSurfaceOptions extends RandomGeneratorOptions { 11 | // prefer_habitable: boolean; 12 | } 13 | 14 | const defaultOptions: PlanetSurfaceOptions = { 15 | // prefer_habitable: true, 16 | }; 17 | 18 | export class PlanetSurfaceGenerator extends RandomGenerator { 19 | public readonly strategy: SurfaceStrategy; 20 | 21 | constructor(public readonly planet: PlanetGenerator, options: Partial = defaultOptions) { 22 | super(planet, { ...defaultOptions, ...planet.options, ...options }); 23 | 24 | this.strategy = SurfaceStrategies[planet.subtype] || SurfaceStrategies['barren']; 25 | } 26 | 27 | *generateSurface() { 28 | // @ts-ignore 29 | this.strategy.doAlgorithm(this.planet.regions); 30 | for (let i = 0; i < this.planet.regions.length; i++) { 31 | yield this.planet.regions[i]; 32 | } 33 | } 34 | 35 | override toModel() { 36 | return { 37 | ...this.planet, 38 | // stars: this.stars.map((star) => star.toModel()), 39 | // // @ts-ignore 40 | // orbits: this.orbits.map((orbit) => orbit.toModel?.()), 41 | // options: this.options, 42 | }; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/galaxy/planet/strategy/index.ts: -------------------------------------------------------------------------------- 1 | export * from './planet-surface-strategy' -------------------------------------------------------------------------------- /src/galaxy/planet/strategy/planet-surface-strategy.ts: -------------------------------------------------------------------------------- 1 | interface RegionModel { 2 | biome: string; 3 | } 4 | 5 | export abstract class SurfaceStrategy { 6 | abstract doAlgorithm(regions: RegionModel[]): RegionModel[]; 7 | } 8 | 9 | class BarrenSurfaceStrategy implements SurfaceStrategy { 10 | public doAlgorithm(regions: RegionModel[]): RegionModel[] { 11 | // @ts-ignore 12 | regions.forEach((region) => (region.biome = 'barren')); 13 | return regions; 14 | } 15 | } 16 | 17 | class LavaSurfaceStrategy implements SurfaceStrategy { 18 | public doAlgorithm(regions: RegionModel[]): RegionModel[] { 19 | // @ts-ignore 20 | regions.forEach((region) => (region.biome = 'lava')); 21 | return regions; 22 | } 23 | } 24 | 25 | class EarthSurfaceStrategy implements SurfaceStrategy { 26 | public doAlgorithm(regions: RegionModel[]): RegionModel[] { 27 | // @ts-ignore 28 | regions.forEach((region) => (region.biome = 'forest')); 29 | return regions; 30 | } 31 | } 32 | 33 | export const SurfaceStrategies: { [key: string]: SurfaceStrategy } = { 34 | lava: new LavaSurfaceStrategy(), 35 | earth: new EarthSurfaceStrategy(), 36 | barren: new BarrenSurfaceStrategy(), 37 | }; 38 | -------------------------------------------------------------------------------- /src/galaxy/planet/surface/builders/planet-partition-builder.ts: -------------------------------------------------------------------------------- 1 | import { Sphere, Vector3 } from 'three'; 2 | 3 | import { SpatialPartition, Tile } from '../utils'; 4 | 5 | import { IcosahedronBuilder } from './icosahedron-builder'; 6 | 7 | export class PlanetPartitionBuilder { 8 | private constructor() {} 9 | 10 | static generatePlanetPartition(tiles: Tile[]) { 11 | const icosahedron = IcosahedronBuilder.generateIcosahedron(); 12 | 13 | icosahedron.faces.forEach((face) => { 14 | const p0 = icosahedron.nodes[face.n[0]].p.clone().multiplyScalar(1000); 15 | const p1 = icosahedron.nodes[face.n[1]].p.clone().multiplyScalar(1000); 16 | const p2 = icosahedron.nodes[face.n[2]].p.clone().multiplyScalar(1000); 17 | const center = p0.clone().add(p1).add(p2).divideScalar(3); 18 | const radius = Math.max(center.distanceTo(p0), center.distanceTo(p2), center.distanceTo(p2)); 19 | // @ts-ignore 20 | face.boundingSphere = new Sphere(center, radius); 21 | // @ts-ignore 22 | face.children = []; 23 | }); 24 | 25 | const unparentedTiles: Tile[] = []; 26 | let maxDistanceFromOrigin = 0; 27 | tiles.forEach((tile) => { 28 | maxDistanceFromOrigin = Math.max( 29 | maxDistanceFromOrigin, 30 | (tile.boundingSphere as Sphere).center.length() + (tile.boundingSphere as Sphere).radius 31 | ); 32 | 33 | let parentFound = false; 34 | for (let j = 0; j < icosahedron.faces.length; ++j) { 35 | const face = icosahedron.faces[j]; 36 | const distance = 37 | // @ts-ignore 38 | (tile.boundingSphere as Sphere).center.distanceTo(face.boundingSphere.center) + 39 | (tile.boundingSphere as Sphere).radius; 40 | // @ts-ignore 41 | if (distance < face.boundingSphere.radius) { 42 | // @ts-ignore 43 | face.children.push(tile); 44 | parentFound = true; 45 | break; 46 | } 47 | } 48 | if (!parentFound) { 49 | unparentedTiles.push(tile); 50 | } 51 | }); 52 | 53 | const rootPartition = new SpatialPartition( 54 | new Sphere(new Vector3(0, 0, 0), maxDistanceFromOrigin), 55 | [], 56 | unparentedTiles 57 | ); 58 | icosahedron.faces.forEach((face) => { 59 | // @ts-ignore 60 | rootPartition.partitions.push(new SpatialPartition(face.boundingSphere, [], face.children)); 61 | // @ts-ignore 62 | delete face.boundingSphere; 63 | // @ts-ignore 64 | delete face.children; 65 | }); 66 | 67 | return rootPartition; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/galaxy/planet/surface/builders/planet-topology-builder.ts: -------------------------------------------------------------------------------- 1 | import { Plane, Sphere, Vector3 } from 'three'; 2 | 3 | import { Mesh, Topology } from '../surface.types'; 4 | import { Border, Corner, Tile } from '../utils'; 5 | 6 | function calculateTriangleArea(pa: Vector3, pb: Vector3, pc: Vector3) { 7 | const vab = new Vector3().subVectors(pb, pa); 8 | const vac = new Vector3().subVectors(pc, pa); 9 | const faceNormal = new Vector3().crossVectors(vab, vac); 10 | const vabNormal = new Vector3().crossVectors(faceNormal, vab).normalize(); 11 | const plane = new Plane().setFromNormalAndCoplanarPoint(vabNormal, pa); 12 | const height = plane.distanceToPoint(pc); 13 | const width = vab.length(); 14 | const area = width * height * 0.5; 15 | return area; 16 | } 17 | 18 | export class PlanetTopologyBuilder { 19 | maxDistanceToCorner?: number; 20 | 21 | generatePlanetTopology(mesh: Mesh): Topology { 22 | const corners: Corner[] = mesh.faces.map( 23 | (face, index) => 24 | new Corner(index, face.centroid.clone().multiplyScalar(1000), face.e.length, face.e.length, face.n.length) 25 | ); 26 | const borders: Border[] = mesh.edges.map((edge, index) => new Border(index, 2, 4, 2)); 27 | 28 | const tiles: Tile[] = mesh.nodes.map( 29 | (node, index) => new Tile(index, node.p.clone().multiplyScalar(1000), node.f.length, node.e.length, node.e.length) 30 | ); 31 | 32 | corners.forEach((corner, index) => { 33 | const face = mesh.faces[index]; 34 | for (let j = 0; j < face.e.length; ++j) { 35 | corner.borders[j] = borders[face.e[j]]; 36 | } 37 | for (let j = 0; j < face.n.length; ++j) { 38 | corner.tiles[j] = tiles[face.n[j]]; 39 | } 40 | }); 41 | 42 | borders.forEach((border, index) => { 43 | const edge = mesh.edges[index]; 44 | const averageCorner = new Vector3(0, 0, 0); 45 | let n = 0; 46 | for (let j = 0; j < edge.f.length; ++j) { 47 | const corner = corners[edge.f[j]]; 48 | averageCorner.add(corner.position); 49 | border.corners[j] = corner; 50 | for (let k = 0; k < corner.borders.length; ++k) { 51 | if (corner.borders[k] !== border) border.borders[n++] = corner.borders[k]; 52 | } 53 | } 54 | border.midpoint = averageCorner.multiplyScalar(1 / border.corners.length); 55 | for (let j = 0; j < edge.n.length; ++j) { 56 | border.tiles[j] = tiles[edge.n[j]]; 57 | } 58 | }); 59 | 60 | corners.forEach((corner, index) => { 61 | for (let j = 0; j < corner.borders.length; ++j) { 62 | corner.corners[j] = corner.borders[j].oppositeCorner(corner); 63 | } 64 | }); 65 | 66 | tiles.forEach((tile, index) => { 67 | const node = mesh.nodes[index]; 68 | for (let j = 0; j < node.f.length; ++j) { 69 | tile.corners[j] = corners[node.f[j]]; 70 | } 71 | for (let j = 0; j < node.e.length; ++j) { 72 | const border = borders[node.e[j]]; 73 | if (border.tiles[0] === tile) { 74 | for (let k = 0; k < tile.corners.length; ++k) { 75 | const corner0 = tile.corners[k]; 76 | const corner1 = tile.corners[(k + 1) % tile.corners.length]; 77 | if (border.corners[1] === corner0 && border.corners[0] === corner1) { 78 | border.corners[0] = corner0; 79 | border.corners[1] = corner1; 80 | } else if (border.corners[0] !== corner0 || border.corners[1] !== corner1) { 81 | continue; 82 | } 83 | tile.borders[k] = border; 84 | tile.tiles[k] = border.oppositeTile(tile); 85 | break; 86 | } 87 | } else { 88 | for (let k = 0; k < tile.corners.length; ++k) { 89 | const corner0 = tile.corners[k]; 90 | const corner1 = tile.corners[(k + 1) % tile.corners.length]; 91 | if (border.corners[0] === corner0 && border.corners[1] === corner1) { 92 | border.corners[1] = corner0; 93 | border.corners[0] = corner1; 94 | } else if (border.corners[1] !== corner0 || border.corners[0] !== corner1) { 95 | continue; 96 | } 97 | tile.borders[k] = border; 98 | tile.tiles[k] = border.oppositeTile(tile); 99 | break; 100 | } 101 | } 102 | } 103 | tile.averagePosition = new Vector3(0, 0, 0); 104 | for (let j = 0; j < tile.corners.length; ++j) { 105 | tile.averagePosition.add(tile.corners[j].position); 106 | } 107 | tile.averagePosition.multiplyScalar(1 / tile.corners.length); 108 | let maxDistanceToCorner = 0; 109 | for (let j = 0; j < tile.corners.length; ++j) { 110 | maxDistanceToCorner = Math.max(maxDistanceToCorner, tile.corners[j].position.distanceTo(tile.averagePosition)); 111 | } 112 | let area = 0; 113 | for (let j = 0; j < tile.borders.length; ++j) { 114 | area += calculateTriangleArea( 115 | tile.position, 116 | tile.borders[j].corners[0].position, 117 | tile.borders[j].corners[1].position 118 | ); 119 | } 120 | tile.area = area; 121 | tile.normal = tile.position.clone().normalize(); 122 | this.maxDistanceToCorner = maxDistanceToCorner; 123 | tile.boundingSphere = new Sphere(tile.averagePosition, maxDistanceToCorner); 124 | }); 125 | 126 | corners.forEach((corner) => { 127 | corner.area = 0; 128 | for (let j = 0; j < corner.tiles.length; ++j) { 129 | corner.area += (corner.tiles[j].area as number) / corner.tiles[j].corners.length; 130 | } 131 | }); 132 | 133 | return { corners, borders, tiles }; 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/galaxy/planet/surface/index.ts: -------------------------------------------------------------------------------- 1 | export * from './planet-surface-generator'; 2 | export * from './surface.types'; 3 | export { Tile } from './utils'; 4 | -------------------------------------------------------------------------------- /src/galaxy/planet/surface/planet-surface-generator.ts: -------------------------------------------------------------------------------- 1 | import { RandomGeneratorOptions, RandomGenerator } from '../../basic-generator'; 2 | 3 | import { PlanetMeshBuilder } from './builders/planet-mesh-builder'; 4 | import { PlanetTopologyBuilder } from './builders/planet-topology-builder'; 5 | // import { PlanetPartitionBuilder } from './builders/planet-partition-builder'; 6 | 7 | import { BiomeSurfaceModificator } from './surface-strategy/biome-surface-modificator'; 8 | import { TerrainSurfaceModificator } from './surface-strategy/terrain-surface-modificator'; 9 | 10 | import { PlanetSurface } from './surface.types'; 11 | 12 | function adjustRange(value: number, oldMin: number, oldMax: number, newMin: number, newMax: number) { 13 | return ((value - oldMin) / (oldMax - oldMin)) * (newMax - newMin) + newMin; 14 | } 15 | 16 | export interface PlanetSurfaceModelGen {} 17 | 18 | export interface PlanetSurfaceOptions extends RandomGeneratorOptions { 19 | subdivisions: number; // [2,20] detail_level 20 | distortionLevel: number; // [.1,1] - 0 occur visual bugs: empty space // todo: CONST?? --- | 21 | plateCount: number; // [0,100] 22 | oceanicRate: number; // [0,1] 23 | heatLevel: number; // [0,2] 24 | moistureLevel: number; // [0,2] 25 | // seed: number; 26 | strategyName: string; 27 | byStrategy: boolean; 28 | } 29 | 30 | const defaultOptions: PlanetSurfaceOptions = { 31 | subdivisions: 10, 32 | distortionLevel: 1, 33 | plateCount: 7, 34 | oceanicRate: 70 / 100, 35 | heatLevel: 1 / 100 + 1, 36 | moistureLevel: 1 / 100 + 1, 37 | seed: 19191919, 38 | strategyName: 'terran', 39 | byStrategy: false, 40 | }; 41 | 42 | export const surfaceStrategy = [ 43 | { 44 | name: 'lava', 45 | modyficators: [ 46 | [TerrainSurfaceModificator, { plateCount: 20, oceanicRate: 0.7, subdivisions: 9 }], 47 | [BiomeSurfaceModificator, { strategy: 'terrestial-lava' }], 48 | ] as const, 49 | }, 50 | { 51 | name: 'terran', 52 | modyficators: [ 53 | [TerrainSurfaceModificator, { plateCount: 20, subdivisions: 9 }], 54 | [BiomeSurfaceModificator, { strategy: 'terrestial-earth' }], 55 | ] as const, 56 | }, 57 | { 58 | name: 'watery', 59 | modyficators: [ 60 | [TerrainSurfaceModificator, { plateCount: 7, subdivisions: 9, oceanicRate: 1, moistureLevel: 1 }], 61 | [BiomeSurfaceModificator, { strategy: 'terrestial-earth' }], 62 | ] as const, 63 | }, 64 | // { 65 | // name: 'terrestial-desert', 66 | // modyficators: [ 67 | // [TerrainSurfaceModificator, { plateCount: 20, subdivisions: 9, oceanicRate: 0, moistureLevel: 0, heatLevel: 1 }], 68 | // [BiomeSurfaceModificator, { strategy: 'terrestial-earth' }], 69 | // ] as const, 70 | // }, 71 | 72 | { 73 | name: 'puffy_giant', 74 | modyficators: [[BiomeSurfaceModificator, { strategy: 'gas-giant' }]] as const, 75 | }, 76 | { 77 | name: 'jupiter', 78 | modyficators: [[BiomeSurfaceModificator, { strategy: 'gas-giant' }]] as const, 79 | }, 80 | { 81 | name: 'hot_jupiter', 82 | modyficators: [[BiomeSurfaceModificator, { strategy: 'gas-giant' }]] as const, 83 | }, 84 | { 85 | name: 'super_jupiter', 86 | modyficators: [[BiomeSurfaceModificator, { strategy: 'gas-giant' }]] as const, 87 | }, 88 | { 89 | name: 'gas_dwarf', 90 | modyficators: [[BiomeSurfaceModificator, { strategy: 'gas-giant' }]] as const, 91 | }, 92 | ]; 93 | 94 | export class PlanetSurfaceGenerator extends RandomGenerator { 95 | // @ts-ignore 96 | planet: PlanetSurface = {}; 97 | 98 | constructor(model: PlanetSurfaceModelGen, options?: Partial) { 99 | super(model, { ...defaultOptions, ...options }); 100 | } 101 | 102 | generateSurface() { 103 | console.time('all generators'); 104 | const { 105 | distortionLevel, 106 | plateCount, 107 | oceanicRate, 108 | heatLevel, 109 | moistureLevel, 110 | subdivisions, 111 | strategyName, 112 | byStrategy, 113 | } = this.options; 114 | 115 | let distortionRate = null; 116 | if (distortionLevel < 0.25) distortionRate = adjustRange(distortionLevel, 0.0, 0.25, 0.0, 0.04); 117 | else if (distortionLevel < 0.5) distortionRate = adjustRange(distortionLevel, 0.25, 0.5, 0.04, 0.05); 118 | else if (distortionLevel < 0.75) distortionRate = adjustRange(distortionLevel, 0.5, 0.75, 0.05, 0.075); 119 | else distortionRate = adjustRange(distortionLevel, 0.75, 1.0, 0.075, 0.15); 120 | 121 | console.time('mesh'); 122 | const meshGenerator = new PlanetMeshBuilder(); 123 | this.planet.mesh = meshGenerator.generatePlanetMesh(subdivisions, distortionRate, this.random); 124 | console.timeEnd('mesh'); 125 | 126 | console.time('topology'); 127 | const topologyGenerator = new PlanetTopologyBuilder(); 128 | this.planet.topology = topologyGenerator.generatePlanetTopology(this.planet.mesh); 129 | console.timeEnd('topology'); 130 | 131 | // todo can be deleted? 132 | // console.time('partition'); 133 | // this.planet.partition = PlanetPartitionBuilder.generatePlanetPartition(this.planet.topology.tiles); 134 | // console.timeEnd('partition'); 135 | 136 | // console.time('terrain'); 137 | // const terrainGenerator = new TerrainSurfaceModificator(); 138 | // terrainGenerator.generate(this.planet, this.random, { 139 | // plateCount, 140 | // oceanicRate, 141 | // heatLevel, 142 | // moistureLevel, 143 | // }); 144 | // console.timeEnd('terrain'); 145 | 146 | // console.time('biomes'); 147 | // // 'Generating Biomes' 148 | // const biomeGenerator = new BiomeSurfaceModificator(); 149 | // biomeGenerator.generate(this.planet, this.random); 150 | // console.timeEnd('biomes'); 151 | 152 | // const strategyName = 'terrestial-earth'; 153 | // const strategyName = 'gas-giant'; 154 | // const strategyName = 'terrestial-lava'; 155 | // strategy 156 | const strategy = surfaceStrategy.find((strategy) => strategy.name === strategyName); 157 | strategy?.modyficators.forEach(([Generator, options]) => { 158 | const generator = new Generator(options); 159 | generator.generate(this.planet, this.random, byStrategy ? options : { ...options, ...this.options }); 160 | // generator.generate(this.planet, this.random, {...options, ...this.options}); 161 | }); 162 | 163 | // end strategy 164 | 165 | console.timeEnd('all generators'); 166 | return this.planet; 167 | } 168 | 169 | // toModel() { 170 | // return { ...this.model }; 171 | // } 172 | } 173 | -------------------------------------------------------------------------------- /src/galaxy/planet/surface/surface-strategy/biome-surface-modificator.ts: -------------------------------------------------------------------------------- 1 | import { Color, Spherical } from 'three'; 2 | 3 | import { RandomObject, sphericalPhiToPolarAngle } from '../../../../utils'; 4 | import { PlanetSurface } from '../surface.types'; 5 | import { Tile } from '../utils'; 6 | 7 | import { SurfaceModificator } from './surface-modificator'; 8 | 9 | interface BiomeStrategy { 10 | name: string; 11 | generateBiomes: (tiles: Tile[], planetRadius: number, random: RandomObject) => void; 12 | } 13 | class EarthBiomeStrategy implements BiomeStrategy { 14 | name = 'terrestial-earth'; 15 | generateBiomes(tiles: Tile[], planetRadius: number, random: RandomObject) { 16 | for (let i = 0; i < tiles.length; ++i) { 17 | const tile = tiles[i]; 18 | const elevation = Math.max(0, tile.elevation as number); 19 | const temperature = tile.temperature as number; 20 | const moisture = tile.moisture as number; 21 | 22 | const colorDeviance = new Color(random.unit(), random.unit(), random.unit()); 23 | 24 | let normalizedElevation = Math.min(-(tile.elevation as number), 1); 25 | if (elevation <= 0) { 26 | if (temperature > 0) { 27 | tile.biome = 'ocean'; 28 | tile.color = new Color(0x0066ff).lerp(new Color(0x0044bb), normalizedElevation).lerp(colorDeviance, 0.05); 29 | } else { 30 | tile.biome = 'oceanGlacier'; 31 | tile.color = new Color(0xddeeff).lerp(colorDeviance, 0.1); 32 | } 33 | } else if (elevation < 0.6) { 34 | normalizedElevation = (tile.elevation as number) / 0.6; 35 | if (temperature > 0.75) { 36 | if (moisture < 0.25) { 37 | tile.biome = 'desert'; 38 | tile.color = new Color(0xdddd77).lerp(new Color(0xbbbb55), normalizedElevation).lerp(colorDeviance, 0.1); 39 | } else { 40 | tile.biome = 'rainForest'; 41 | tile.color = new Color(0x44dd00).lerp(new Color(0x229900), normalizedElevation).lerp(colorDeviance, 0.2); 42 | } 43 | } else if (temperature > 0.5) { 44 | if (moisture < 0.25) { 45 | tile.biome = 'rocky'; 46 | tile.color = new Color(0xaa9977).lerp(new Color(0x887755), normalizedElevation).lerp(colorDeviance, 0.15); 47 | } else if (moisture < 0.5) { 48 | tile.biome = 'plains'; 49 | tile.color = new Color(0x99bb44).lerp(new Color(0x667722), normalizedElevation).lerp(colorDeviance, 0.1); 50 | } else { 51 | tile.biome = 'swamp'; 52 | tile.color = new Color(0x77aa44).lerp(new Color(0x446622), normalizedElevation).lerp(colorDeviance, 0.25); 53 | } 54 | } else if (temperature > 0) { 55 | if (moisture < 0.25) { 56 | tile.biome = 'plains'; 57 | tile.color = new Color(0x99bb44).lerp(new Color(0x667722), normalizedElevation).lerp(colorDeviance, 0.1); 58 | } else if (moisture < 0.5) { 59 | tile.biome = 'grassland'; 60 | tile.color = new Color(0x77cc44).lerp(new Color(0x448822), normalizedElevation).lerp(colorDeviance, 0.15); 61 | } else { 62 | tile.biome = 'deciduousForest'; 63 | tile.color = new Color(0x33aa22).lerp(new Color(0x116600), normalizedElevation).lerp(colorDeviance, 0.1); 64 | } 65 | } else { 66 | if (moisture < 0.25) { 67 | tile.biome = 'tundra'; 68 | tile.color = new Color(0x9999aa).lerp(new Color(0x777788), normalizedElevation).lerp(colorDeviance, 0.15); 69 | } else { 70 | tile.biome = 'landGlacier'; 71 | tile.color = new Color(0xddeeff).lerp(colorDeviance, 0.1); 72 | } 73 | } 74 | } else if (elevation < 0.8) { 75 | normalizedElevation = ((tile.elevation as number) - 0.6) / 0.2; 76 | if (temperature > 0) { 77 | if (moisture < 0.25) { 78 | tile.biome = 'tundra'; 79 | tile.color = new Color(0x777788).lerp(new Color(0x666677), normalizedElevation).lerp(colorDeviance, 0.1); 80 | } else { 81 | tile.biome = 'coniferForest'; 82 | tile.color = new Color(0x338822).lerp(new Color(0x116600), normalizedElevation).lerp(colorDeviance, 0.1); 83 | } 84 | } else { 85 | tile.biome = 'tundra'; 86 | tile.color = new Color('lightgreen'); 87 | } 88 | } else { 89 | normalizedElevation = Math.min(((tile.elevation as number) - 0.8) / 0.5, 1); 90 | if (temperature > 0 || moisture < 0.25) { 91 | tile.biome = 'mountain'; 92 | tile.color = new Color(0x444433).lerp(new Color(0x333322), normalizedElevation).lerp(colorDeviance, 0.05); 93 | } else { 94 | tile.biome = 'snowyMountain'; 95 | tile.color = new Color(0xdddddd).lerp(new Color(0xffffff), normalizedElevation).lerp(colorDeviance, 0.1); 96 | } 97 | } 98 | 99 | tile.color.multiplyScalar(0.5); 100 | } 101 | } 102 | } 103 | 104 | function adjustRange(value: number, oldMin: number, oldMax: number, newMin: number, newMax: number) { 105 | return ((value - oldMin) / (oldMax - oldMin)) * (newMax - newMin) + newMin; 106 | } 107 | function add(accumulator: number, factor: number) { 108 | return accumulator + factor; 109 | } 110 | 111 | class GasGiantBiomeStrategy implements BiomeStrategy { 112 | name = 'gas-giant'; 113 | generateBiomes(tiles: Tile[], planetRadius: number, random: RandomObject) { 114 | for (let i = 0; i < tiles.length; ++i) { 115 | const tile = tiles[i]; 116 | 117 | const colorDeviance = new Color(random.unit(), random.unit(), random.unit()); 118 | const colors = [ 119 | [0.1, 0xc99039], // todo random factor 120 | [0.1, 0xb53b2f], 121 | [0.15, 0xebf3f6], 122 | [0.2, 0xd8ca9d], 123 | [0.3, 0xa59186], 124 | ]; 125 | 126 | const spherical = new Spherical().setFromVector3(tile.position); 127 | let angle = sphericalPhiToPolarAngle(spherical.phi); 128 | angle = angle > 90 ? 180 - angle : angle; 129 | /** 130 | * todo can be write with Polar from @shared (but for now generator should't have dependencies): 131 | * const polar = new Polar().setFromVector3(tile.position); 132 | * // convert [-90,90] to [0,90] from equator, with [0,180] angle 133 | * const angle = 90 - Math.abs(polar.latitude); 134 | */ 135 | const normalizedAngle = adjustRange(angle, 0, 90, 0, colors.map((col) => col[0]).reduce(add, 0)); 136 | 137 | let sum = 0; 138 | let index = 0; 139 | colors.reverse(); 140 | while (index < colors.length) { 141 | const newSum = sum + colors[index][0]; 142 | if (sum < normalizedAngle && normalizedAngle < newSum) { 143 | tile.biome = `gas_${colors.length - index}`; 144 | tile.color = new Color(colors[index][1]).lerp(colorDeviance, 0.1); //.multiplyScalar(colors[index][2] || 1); 145 | break; 146 | } 147 | sum = newSum; 148 | index += 1; 149 | } 150 | } 151 | } 152 | } 153 | 154 | const carnelian = `#A81B18`; 155 | const roseEbony = '#5A4548'; 156 | class LavaBiomeStrategy implements BiomeStrategy { 157 | name = 'terran-lava'; 158 | generateBiomes(tiles: Tile[], planetRadius: number, random: RandomObject) { 159 | for (let i = 0; i < tiles.length; ++i) { 160 | const tile = tiles[i]; 161 | const colorDeviance = new Color(random.unit(), random.unit(), random.unit()); 162 | const elevation = Math.max(0, tile.elevation as number); 163 | 164 | if (elevation <= 0) { 165 | tile.biome = 'lava'; 166 | tile.color = new Color(carnelian).lerp(colorDeviance, 0.03); 167 | } else { 168 | tile.biome = 'rocks'; 169 | tile.color = new Color(roseEbony).lerp(new Color('silver').lerp(colorDeviance.multiplyScalar(0.2), 0.3), 0.05); 170 | // tile.color = new Color('black').lerp(new Color('silver').lerp(colorDeviance, 0.1), 0.025); 171 | // tile.color = new Color('saddlebrown').lerp(new Color('silver').lerp(colorDeviance, 0.7), 0.1); 172 | // tile.color = new Color('saddlebrown').lerp(colorDeviance, 0.05); 173 | // tile.color = new Color('black').lerp(colorDeviance, 0.01); 174 | } 175 | } 176 | } 177 | } 178 | 179 | interface PlanetBiomeGeneratorOptions { 180 | strategy: string; 181 | } 182 | const defaultOptions: PlanetBiomeGeneratorOptions = { 183 | strategy: 'terrestial-earth', 184 | }; 185 | export class BiomeSurfaceModificator extends SurfaceModificator { 186 | public strategy: BiomeStrategy; 187 | 188 | constructor(options?: Partial) { 189 | super('biome-generator', { ...defaultOptions, ...options }); 190 | 191 | let strategy: BiomeStrategy; 192 | switch (this.options.strategy) { 193 | case 'terrestial-earth': 194 | strategy = new EarthBiomeStrategy(); 195 | break; 196 | case 'terrestial-lava': 197 | strategy = new LavaBiomeStrategy(); 198 | break; 199 | case 'gas-giant': 200 | strategy = new GasGiantBiomeStrategy(); 201 | break; 202 | default: 203 | strategy = new GasGiantBiomeStrategy(); 204 | } 205 | this.strategy = strategy; 206 | } 207 | 208 | // generatePlanetBiomes(tiles: Tile[], planetRadius: number, random: RandomObject) { 209 | override generate(planet: PlanetSurface, random: RandomObject, options?: Partial) { 210 | this.strategy.generateBiomes(planet.topology.tiles, 1000, random); 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /src/galaxy/planet/surface/surface-strategy/surface-modificator.ts: -------------------------------------------------------------------------------- 1 | import { RandomObject } from '../../../../utils'; 2 | 3 | import { PlanetSurface } from '../surface.types'; 4 | 5 | export class SurfaceModificator { 6 | constructor(public name: string, public options: SurfaceModificatorOptions) {} 7 | 8 | generate(planet: PlanetSurface, random: RandomObject, options?: Partial) { 9 | throw new Error('must be ovveride'); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/galaxy/planet/surface/surface.types.ts: -------------------------------------------------------------------------------- 1 | import { Vector3 } from 'three'; 2 | 3 | import { Border, Corner, SpatialPartition, Tile } from './utils'; 4 | 5 | export interface MeshEdge { 6 | n: number[]; 7 | f: number[]; 8 | subdivided_n: number[]; 9 | subdivided_e: number[]; 10 | } 11 | export interface MeshFace { 12 | n: number[]; 13 | e: number[]; 14 | centroid: Vector3; 15 | } 16 | export interface MeshNode { 17 | p: Vector3; 18 | e: number[]; 19 | f: number[]; 20 | } 21 | export interface Mesh { 22 | nodes: MeshNode[]; 23 | edges: MeshEdge[]; 24 | faces: MeshFace[]; 25 | } 26 | 27 | export interface Topology { 28 | corners: Corner[]; 29 | borders: Border[]; 30 | tiles: Tile[]; 31 | } 32 | 33 | export interface PlanetSurface { 34 | mesh: Mesh; 35 | topology: Topology; 36 | plates: any[]; 37 | partition: SpatialPartition; 38 | } 39 | -------------------------------------------------------------------------------- /src/galaxy/planet/surface/utils.ts: -------------------------------------------------------------------------------- 1 | import { Color, Plane, Sphere, Vector3 } from 'three'; 2 | 3 | export class Corner { 4 | corners: Corner[]; 5 | borders: Border[]; 6 | tiles: Tile[]; 7 | 8 | area?: number; 9 | elevation?: number; 10 | distanceToPlateBoundary?: number; 11 | distanceToPlateRoot?: number; 12 | betweenPlates?: boolean; 13 | shear?: number; 14 | pressure?: number; 15 | 16 | // weather 17 | airHeat?: number; 18 | newAirHeat?: number; 19 | maxHeat?: number; 20 | heat?: number; 21 | airCurrent?: Vector3; 22 | airCurrentSpeed?: number; 23 | airCurrentOutflows?: number[]; 24 | heatAbsorption?: number; 25 | temperature?: number; 26 | airMoisture?: number; 27 | newAirMoisture?: number; 28 | precipitation?: number; 29 | precipitationRate?: number; 30 | maxPrecipitation?: number; 31 | moisture?: number; 32 | 33 | constructor( 34 | public id: number, 35 | public position: Vector3, 36 | cornerCount: number, 37 | borderCount: number, 38 | tileCount: number 39 | ) { 40 | this.corners = new Array(cornerCount); 41 | this.borders = new Array(borderCount); 42 | this.tiles = new Array(tileCount); 43 | } 44 | 45 | vectorTo(corner: Corner) { 46 | return corner.position.clone().sub(this.position); 47 | } 48 | 49 | toString() { 50 | return ( 51 | 'Corner ' + 52 | this.id.toFixed(0) + 53 | ' < ' + 54 | this.position.x.toFixed(0) + 55 | ', ' + 56 | this.position.y.toFixed(0) + 57 | ', ' + 58 | this.position.z.toFixed(0) + 59 | ' >' 60 | ); 61 | } 62 | 63 | toJSON() { 64 | return { 65 | id: this.id, 66 | position: this.position, 67 | corners: this.corners.map((corner) => corner.id), 68 | borders: this.borders.map((border) => border.id), 69 | tiles: this.tiles.map((tile) => tile.id), 70 | }; 71 | } 72 | } 73 | 74 | export class Border { 75 | corners: Corner[]; 76 | borders: Border[]; 77 | tiles: Tile[]; 78 | 79 | midpoint?: Vector3; 80 | betweenPlates?: boolean; 81 | 82 | constructor(public id: number, cornerCount: number, borderCount: number, tileCount: number) { 83 | this.id = id; 84 | this.corners = new Array(cornerCount); 85 | this.borders = new Array(borderCount); 86 | this.tiles = new Array(tileCount); 87 | } 88 | 89 | oppositeCorner(corner: Corner) { 90 | return this.corners[0] === corner ? this.corners[1] : this.corners[0]; 91 | } 92 | 93 | oppositeTile(tile: Tile) { 94 | return this.tiles[0] === tile ? this.tiles[1] : this.tiles[0]; 95 | } 96 | 97 | length() { 98 | return this.corners[0].position.distanceTo(this.corners[1].position); 99 | } 100 | 101 | isLandBoundary() { 102 | return (this.tiles[0].elevation as number) > 0 !== (this.tiles[1].elevation as number) > 0; 103 | } 104 | 105 | toString() { 106 | return 'Border ' + this.id.toFixed(0); 107 | } 108 | 109 | toJSON() { 110 | return { 111 | id: this.id, 112 | corners: this.corners.map((corner) => corner.id), 113 | borders: this.borders.map((border) => border.id), 114 | tiles: this.tiles.map((tile) => tile.id), 115 | }; 116 | } 117 | } 118 | 119 | export class Tile { 120 | corners: Corner[]; 121 | borders: Border[]; 122 | tiles: Tile[]; 123 | 124 | area?: number; 125 | normal?: Vector3; 126 | averagePosition?: Vector3; 127 | boundingSphere?: Sphere; 128 | elevation?: number; 129 | maxDistanceToCorner?: number; 130 | plate?: Plate; 131 | biome?: string; 132 | color?: Color; 133 | moisture?: number; 134 | temperature?: number; 135 | 136 | constructor( 137 | public id: number, 138 | public position: Vector3, 139 | cornerCount: number, 140 | borderCount: number, 141 | tileCount: number 142 | ) { 143 | this.corners = new Array(cornerCount); 144 | this.borders = new Array(borderCount); 145 | this.tiles = new Array(tileCount); 146 | } 147 | 148 | intersectRay(ray: any) { 149 | if (!this.intersectRayWithSphere(ray, this.boundingSphere as Sphere)) return false; 150 | 151 | const surface = new Plane().setFromNormalAndCoplanarPoint(this.normal as Vector3, this.averagePosition as Vector3); 152 | if (surface.distanceToPoint(ray.origin) <= 0) return false; 153 | 154 | const denominator = surface.normal.dot(ray.direction); 155 | if (denominator === 0) return false; 156 | 157 | const t = -(ray.origin.dot(surface.normal) + surface.constant) / denominator; 158 | const point = ray.direction.clone().multiplyScalar(t).add(ray.origin); 159 | 160 | const origin = new Vector3(0, 0, 0); 161 | for (let i = 0; i < this.corners.length; ++i) { 162 | const j = (i + 1) % this.corners.length; 163 | const side = new Plane().setFromCoplanarPoints(this.corners[j].position, this.corners[i].position, origin); 164 | 165 | if (side.distanceToPoint(point) < 0) return false; 166 | } 167 | return true; 168 | } 169 | 170 | intersectRayWithSphere(ray: any, sphere: Sphere) { 171 | const v1 = sphere.center.clone().sub(ray.origin); 172 | const v2 = v1.clone().projectOnVector(ray.direction); 173 | const d = v1.distanceTo(v2); 174 | return d <= sphere.radius; 175 | } 176 | 177 | toString() { 178 | return ( 179 | 'Tile ' + 180 | this.id.toFixed(0) + 181 | ' (' + 182 | this.tiles.length.toFixed(0) + 183 | ' Neighbors) < ' + 184 | this.position.x.toFixed(0) + 185 | ', ' + 186 | this.position.y.toFixed(0) + 187 | ', ' + 188 | this.position.z.toFixed(0) + 189 | ' >' 190 | ); 191 | } 192 | 193 | // fromJSON() {} 194 | toJSON() { 195 | return { 196 | id: this.id, 197 | position: this.position, 198 | tiles: this.tiles.map((tile) => tile.id), 199 | corners: this.corners.map((corner) => corner.id), 200 | borders: this.borders.map((border) => border.id), 201 | 202 | normal: this.normal, 203 | averagePosition: this.averagePosition, 204 | maxDistanceToCorner: this.maxDistanceToCorner, 205 | plate: this.plate ? this.plate.id : undefined, 206 | area: this.area, 207 | biome: this.biome, 208 | color: this.color, 209 | moisture: this.moisture, 210 | elevation: this.elevation, 211 | temperature: this.temperature, 212 | }; 213 | } 214 | } 215 | 216 | export class SpatialPartition { 217 | constructor(public boundingSphere: Sphere, public partitions: SpatialPartition[], public tiles: Tile[]) {} 218 | 219 | // intersectRay(ray){ 220 | // if (intersectRayWithSphere(ray, this.boundingSphere)){ 221 | // for (let i = 0; i < this.partitions.length; ++i){ 222 | // const intersection = this.partitions[i].intersectRay(ray); 223 | // if (intersection !== false){ 224 | // return intersection; 225 | // } 226 | // } 227 | // 228 | // for (let i = 0; i < this.tiles.length; ++i){ 229 | // if (this.tiles[i].intersectRay(ray)){ 230 | // return this.tiles[i]; 231 | // } 232 | // } 233 | // } 234 | // return false; 235 | // } 236 | toJSON() { 237 | return { 238 | boundingSphere: this.boundingSphere, 239 | tiles: this.tiles.map((tile) => tile.id), 240 | // tiles: this.tiles, 241 | partitions: this.partitions, 242 | }; 243 | } 244 | } 245 | 246 | export class Plate { 247 | tiles: Tile[] = []; 248 | boundaryCorners: Corner[] = []; 249 | boundaryBorders: Border[] = []; 250 | constructor( 251 | public id: number, 252 | public color: Color, 253 | public driftAxis: Vector3, 254 | public driftRate: number, 255 | public spinRate: number, 256 | public elevation: number, 257 | public oceanic: boolean, 258 | public root: Corner 259 | ) {} 260 | 261 | calculateMovement(position: Vector3) { 262 | var movement = this.driftAxis 263 | .clone() 264 | .cross(position) 265 | .setLength(this.driftRate * position.clone().projectOnVector(this.driftAxis).distanceTo(position)); 266 | movement.add( 267 | this.root.position 268 | .clone() 269 | .cross(position) 270 | .setLength(this.spinRate * position.clone().projectOnVector(this.root.position).distanceTo(position)) 271 | ); 272 | return movement; 273 | } 274 | 275 | // toJSON() { 276 | // return { 277 | // id: this.id, 278 | // color: this.color, 279 | // driftAxis: this.driftAxis, 280 | // driftRate: this.driftRate, 281 | // spinRate: this.spinRate, 282 | // elevation: this.elevation, 283 | // oceanic: this.oceanic, 284 | // root: this.root.id, // corner 285 | // tiles: this.tiles.map((tile) => tile.id), 286 | // boundaryCorners: this.boundaryCorners.map((corner) => corner.id), 287 | // boundaryBorders: this.boundaryBorders.map((border) => border.id), 288 | // }; 289 | // } 290 | } 291 | -------------------------------------------------------------------------------- /src/galaxy/star/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types'; 2 | export * from './star-generator'; 3 | -------------------------------------------------------------------------------- /src/galaxy/star/star-generator.spec.ts: -------------------------------------------------------------------------------- 1 | import { StarGenerator } from './star-generator'; 2 | 3 | describe('world-generator star-generator.ts', () => { 4 | it('should generate star Sun like physic', () => { 5 | const star = new StarGenerator({ mass: 1 }); 6 | expect(star instanceof StarGenerator).toBeTruthy(); 7 | 8 | expect(star.physic?.stellar_class).toEqual('G'); 9 | expect(star.physic?.temperature).toEqual(1); 10 | expect(star.physic?.luminosity).toEqual(1); 11 | expect(star.physic?.evolution).toEqual(true); 12 | expect(star.physic?.radius).toEqual(1); 13 | }); 14 | 15 | it('should generate same star for same seeds', () => { 16 | const star1 = new StarGenerator({}, { seed: 1234567890 }); 17 | const star2 = new StarGenerator({}, { seed: 1234567890 }); 18 | 19 | expect(star1.physic).not.toBeNull(); 20 | expect(star2.physic).not.toBeNull(); 21 | expect(star1).toEqual(star2); 22 | expect(star1.toModel()).toEqual(star2.toModel()); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/galaxy/star/star-generator.ts: -------------------------------------------------------------------------------- 1 | import { codename, GREEK_LETTERS_NAMES, numberToGreekChar } from '../../utils'; 2 | import { RandomGenerator, RandomGeneratorOptions } from '../basic-generator'; 3 | import { StarPhysicModel, StarPhysics, StarStellarClass, StarStellarClassData } from '../physic'; 4 | 5 | import { StarModel } from './types'; 6 | 7 | type Constant = () => T; 8 | type ConstantValue = (x: T) => Constant; 9 | const constant: ConstantValue = (x) => () => x; 10 | 11 | interface StarOptions extends RandomGeneratorOptions { 12 | // name?: string; 13 | // temperature?: number; 14 | } 15 | 16 | const defaultOptions: StarOptions = { 17 | // seed: 999, 18 | }; 19 | 20 | export class StarGenerator extends RandomGenerator { 21 | public physic: StarPhysicModel; 22 | private meta: StarStellarClassData; 23 | 24 | constructor(model: StarModel, options: Partial = defaultOptions) { 25 | super(model, { ...defaultOptions, ...model.options, ...options }); 26 | 27 | if (model.name && !model.id) this.name(model.name); 28 | 29 | const { spectralClass } = model; 30 | let { mass } = model; 31 | if (!mass) { 32 | const meta = spectralClass 33 | ? StarPhysics.getSpectralByClass(spectralClass) 34 | : this.random.choice(StarPhysics.SPECTRAL_CLASSIFICATION); 35 | mass = this.random.real(meta.min_sol_mass, meta.max_sol_mass); 36 | } 37 | this.mass(mass); 38 | } 39 | 40 | // initialize() {} 41 | // /** mass accessor function */ 42 | // _mass = constant(1); 43 | // // mass(value?: number | Constant) { 44 | // mass(): () => number; 45 | // mass(value: number | (() => number)): this; 46 | // mass(value?: number | (() => number)) { 47 | // return value !== undefined 48 | // ? ((this._mass = typeof value === 'function' ? value : constant(value)), this.initialize(), this) 49 | // : this._mass; 50 | // } 51 | 52 | /** Get Star mass */ 53 | mass(): number; 54 | /** Set Star mass */ 55 | mass(mass: number): this; 56 | mass(value?: number) { 57 | return value !== undefined ? (this.initializePhysic(value), this) : this.model.mass; 58 | // return value !== undefined ? ((this.model.mass = value), this.initializePhysic(value), this) : this.model.mass; 59 | } 60 | 61 | /** Get Star name */ 62 | name(): string; 63 | /** Set Star name */ 64 | name(starName: string): this; 65 | /** Set Star name based on System name and Star sequence */ 66 | name(systemName: string, sequenceIndex: number): this; 67 | name(value?: string, sequenceIndex?: number) { 68 | return value !== undefined ? (this.initializeNaming(value, sequenceIndex), this) : this.model.name; 69 | } 70 | 71 | private initializeNaming(initialName: string, sequenceIndex?: number) { 72 | let id = initialName; 73 | let name = initialName; 74 | if (sequenceIndex !== undefined) { 75 | id = StarGenerator.getSequentialName(initialName, sequenceIndex, true); 76 | name = StarGenerator.getSequentialName(initialName, sequenceIndex); 77 | } 78 | this.model.id = codename(id); 79 | this.model.name = name; 80 | this.model.path = `${this.model.parentPath!}/s:${this.model.id}`; 81 | } 82 | 83 | private initializePhysic(mass: number) { 84 | this.meta = StarPhysics.getSpectralByMass(mass); 85 | this.model.mass = mass; 86 | this.model.spectralClass = this.meta.class; 87 | 88 | const model = { mass } as StarPhysicModel; 89 | 90 | model.subtype = this.meta.class; 91 | model.stellar_class = this.meta.class; 92 | model.evolution = this.meta.organisms_evolution; 93 | 94 | model.radius = StarPhysics.calcRadius(model.mass); 95 | model.volume = StarPhysics.calcVolume(model.radius); 96 | model.density = StarPhysics.calcDensity(model.mass, model.radius); 97 | model.luminosity = StarPhysics.calcLuminosity(model.mass); 98 | model.inner_limit = StarPhysics.calcInnerLimit(model.mass); 99 | model.outer_limit = StarPhysics.calcOuterLimit(model.mass); 100 | model.frost_line = StarPhysics.calcFrostLine(model.luminosity); 101 | model.temperature = StarPhysics.calcTemperature(model.luminosity, model.radius); 102 | model.color = StarPhysics.calcColor(model.temperature); 103 | model.surface_area = StarPhysics.calcSurfaceArea(model.radius); 104 | model.circumference = StarPhysics.calcCircumference(model.radius); 105 | model.main_sequence_lifetime = StarPhysics.calcMainSequenceLifetime(model.mass, model.luminosity); 106 | model.habitable_zone = StarPhysics.calcHabitableZone(model.luminosity); 107 | model.habitable_zone_inner = StarPhysics.calcHabitableZoneStart(model.luminosity); 108 | model.habitable_zone_outer = StarPhysics.calcHabitableZoneEnd(model.luminosity); 109 | 110 | this.physic = model; 111 | } 112 | 113 | override toModel() { 114 | return { ...this.model, physic: this.physic, meta: this.meta }; 115 | } 116 | 117 | static getSequentialName(systemName: string, starIndex: number, standarize = false) { 118 | return `${systemName} ${standarize ? GREEK_LETTERS_NAMES[starIndex] : numberToGreekChar(starIndex)}`; 119 | } 120 | static sortByMass(stars: StarGenerator[]) { 121 | return stars.sort((a, b) => b.mass() - a.mass()); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/galaxy/star/types.ts: -------------------------------------------------------------------------------- 1 | import { StarPath, SystemPath } from '../../global.types'; 2 | import { StarPhysicModel,StarStellarClass } from '../physic/star-physic'; 3 | 4 | export interface StarModel { 5 | id?: string; 6 | path?: StarPath; 7 | parentPath?: SystemPath; 8 | mass?: number; 9 | spectralClass?: StarStellarClass; 10 | name?: string; 11 | physic?: StarPhysicModel; 12 | options?: {}; 13 | } 14 | -------------------------------------------------------------------------------- /src/galaxy/system/debris-belt-generator.ts: -------------------------------------------------------------------------------- 1 | import { codename, decimalToRoman } from '../../utils'; 2 | import { RandomGenerator, RandomGeneratorOptions } from '../basic-generator'; 3 | import { SystemOrbitModel } from './system-orbits-generator'; 4 | 5 | export interface DebrisBeltOptions extends RandomGeneratorOptions { 6 | // surfaceSeed?: Seed; 7 | // random?: RandomObject; 8 | } 9 | const defaultOptions: DebrisBeltOptions = { 10 | // position: new Vector3(0, 0, 0), 11 | }; 12 | 13 | export interface DebrisBeltModel { 14 | id?: string; 15 | name?: string; 16 | path?: string; 17 | parentPath?: string; 18 | type?: string; // todo eg. icy, iron, etc.? 19 | subType?: string; // todo dust - planet ring, rocky - big 20 | physic?: { 21 | mass?: number; 22 | }; 23 | orbit?: SystemOrbitModel; // OrbitModel; 24 | options?: {}; // todo generator options??? 25 | schemaName?: 'DebrisBeltModel'; 26 | } 27 | 28 | // export interface DebrisBeltGeneratorModel { 29 | // model?: DebrisBeltModel; 30 | // options?: DebrisBeltOptions; 31 | // } 32 | 33 | export class DebrisBeltGenerator extends RandomGenerator { 34 | override schemaName = 'DebrisBeltModel'; 35 | 36 | constructor(model: DebrisBeltModel, options: Partial = defaultOptions) { 37 | super(model, { ...defaultOptions, ...model.options, ...options }); 38 | 39 | if (!model.id) this.model.id = codename(this.model.name); 40 | if (!model.path) this.model.path = `${this.model.parentPath}/b:${this.model.id}`; 41 | } 42 | 43 | get subtype(): string { 44 | // @ts-ignore 45 | return this.model.orbit.subtype; 46 | } 47 | 48 | static getSequentialName(beltIndex: number) { 49 | return `Belt ${decimalToRoman(beltIndex + 1)}`; 50 | } 51 | 52 | override toModel(): DebrisBeltModel { 53 | return super.toModel({ options: this.options }); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/galaxy/system/empty-zone.ts: -------------------------------------------------------------------------------- 1 | import { decimalToRoman } from '../../utils'; 2 | 3 | import { ModelHandler } from '../basic-generator'; 4 | 5 | import { SystemOrbitModel } from './system-orbits-generator'; 6 | 7 | export interface EmptyZoneModel { 8 | id?: string; 9 | name?: string; 10 | path?: string; 11 | orbit?: SystemOrbitModel; 12 | type?: string; // not used 13 | subType?: string; // not used 14 | schemaName?: 'EmptyZoneModel'; 15 | } 16 | 17 | export class EmptyZone extends ModelHandler { 18 | override schemaName = 'EmptyZoneModel'; 19 | 20 | static getSequentialName(beltIndex: number) { 21 | return `empty ${decimalToRoman(beltIndex + 1)}`; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/galaxy/system/index.ts: -------------------------------------------------------------------------------- 1 | export * from './debris-belt-generator'; 2 | export * from './types'; 3 | export * from './system-generator'; 4 | export * from './system-orbits-generator'; 5 | -------------------------------------------------------------------------------- /src/galaxy/system/system-generator.spec.ts: -------------------------------------------------------------------------------- 1 | import { SystemGenerator } from './system-generator'; 2 | 3 | describe('world-generator system-generator.ts', () => { 4 | it('should generate stars and planets', () => { 5 | const system = new SystemGenerator({}); 6 | expect(system instanceof SystemGenerator).toBeTruthy(); 7 | 8 | for (const star of system.generateStars()) { 9 | } 10 | expect(system.stars.length).toBeTruthy(); 11 | for (const planet of system.generatePlanets()) { 12 | } 13 | expect(system.orbits.length).toBeTruthy(); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/galaxy/system/system-generator.ts: -------------------------------------------------------------------------------- 1 | import { Vector3 } from 'three'; 2 | 3 | import { codename, RandomObject } from '../../utils'; 4 | import { RandomGenerator, RandomGeneratorOptions } from '../basic-generator'; 5 | import { STAR_COUNT_DISTIBUTION_IN_SYSTEMS, StarStellarClass, SystemPhysicModel } from '../physic'; 6 | import { OrbitGenerator } from '../physic/orbit-generator'; 7 | import { PlanetGenerator, PlanetModel } from '../planet'; 8 | import { StarGenerator, StarModel } from '../star'; 9 | 10 | import { DebrisBeltGenerator, DebrisBeltModel } from './debris-belt-generator'; 11 | import { EmptyZone, EmptyZoneModel } from './empty-zone'; 12 | import { SystemOrbitsGenerator } from './system-orbits-generator'; 13 | import { SystemModel } from './types'; 14 | 15 | type OnOrbitGenerator = PlanetGenerator | DebrisBeltGenerator | EmptyZone; 16 | 17 | interface SystemOptions extends RandomGeneratorOptions { 18 | starsSeed: number; 19 | planetsSeed: number; 20 | // name?: string; 21 | // position: Vector3; 22 | // temperature?: number; 23 | // starsSeed?: number; 24 | // planetsSeed?: number; 25 | prefer_habitable: boolean; 26 | planetsCount?: number; // todo 27 | spectralClass?: StarStellarClass; 28 | } 29 | 30 | const defaultOptions: SystemOptions = { 31 | // position: new Vector3(0, 0, 0), 32 | seed: 0, 33 | starsSeed: 0, 34 | planetsSeed: 0, 35 | prefer_habitable: true, 36 | }; 37 | 38 | // enum GenerationStep { 39 | // INIT = 'init', 40 | // BASIC = 'basic', 41 | // STARS = 'stars', 42 | // PLANETS = 'planets', 43 | // FINISHED = 'finished', 44 | // } 45 | 46 | export class SystemGenerator extends RandomGenerator { 47 | override schemaName = 'SystemModel'; 48 | public readonly stars: StarGenerator[] = []; 49 | // public readonly orbits: OrbitGenerator[] = []; 50 | public readonly orbits: Required['orbits'] = []; 51 | public readonly belts: DebrisBeltGenerator[] = []; 52 | public readonly planets: PlanetGenerator[] = []; 53 | public physic: SystemPhysicModel; 54 | 55 | constructor(model: SystemModel, options: Partial = {}) { 56 | super(model, { ...defaultOptions, ...model.options, ...options }); 57 | 58 | if (!this.options.starsSeed) this.options.starsSeed = this.random.seed(); 59 | if (!this.options.planetsSeed) this.options.planetsSeed = this.random.seed(); 60 | this.physic = { ...(model.physic as SystemPhysicModel) }; 61 | 62 | if (!model.id) this.model.id = codename(this.model.name); 63 | if (!model.path) this.model.path = `${this.model.parentPath}/${this.model.id}`; 64 | if (!model.position) this.model.position = new Vector3(); 65 | } 66 | 67 | get name(): string { 68 | return this.model.name || 'Example System 1'; // todo 69 | } 70 | get position(): Vector3 { 71 | return this.model.position as Vector3; 72 | } 73 | 74 | *generateStars(): IterableIterator { 75 | try { 76 | const random = new RandomObject(this.options.starsSeed); 77 | const { spectralClass } = this.options; 78 | // console.log({spectralClass, temperature: this.model.temperature}) 79 | 80 | const count = random.weighted(STAR_COUNT_DISTIBUTION_IN_SYSTEMS); 81 | // if (count <= 0) return; 82 | for (let i = 0; i < count; i++) { 83 | const star = new StarGenerator( 84 | { 85 | // todo when spectralClass is provided, next star should be smaller 86 | spectralClass: spectralClass && i === 0 ? spectralClass : undefined, 87 | parentPath: this.model.path, 88 | }, 89 | { random } 90 | ); 91 | this.stars.push(star); 92 | yield star; 93 | } 94 | 95 | StarGenerator.sortByMass(this.stars); 96 | const isSingleStar = this.stars.length === 1; 97 | this.stars.forEach((star, index) => (isSingleStar ? star.name(this.name) : star.name(this.name, index))); 98 | 99 | if (this.stars[0]) { 100 | this.model.starColor = this.stars[0].physic?.color; 101 | this.model.starRadius = this.stars[0].physic?.radius; 102 | // this.model.habitable = this.stars[0].physic?.habitable; 103 | this.physic.color = this.stars[0].physic?.color; 104 | } 105 | this.physic.starsCount = this.stars.length; 106 | // this.fillStarInfo(); // todo 107 | } catch (e) { 108 | console.warn('*generateStars()', e); 109 | } 110 | } 111 | 112 | private getStarsModels() { 113 | if (this.stars.length) return this.stars.map((it) => it.toModel()); 114 | return this.model.stars || []; 115 | } 116 | *generatePlanets(): IterableIterator { 117 | try { 118 | const random = new RandomObject(this.options.planetsSeed); 119 | let nameIndex = 0; 120 | for (const orbitGenerator of this.generateOrbits()) { 121 | // this.orbits.push(orbitGenerator); 122 | const orbit = orbitGenerator.toModel(); 123 | 124 | let bodyGenerator: OnOrbitGenerator; 125 | if (orbit.bodyType === 'PLANET') { 126 | bodyGenerator = new PlanetGenerator( 127 | { 128 | name: PlanetGenerator.getSequentialName(this.name, nameIndex++), 129 | parentPath: this.model.path, 130 | orbit, 131 | }, 132 | { star: this.getStarsModels()[0], seed: random.seed() } 133 | ); 134 | this.orbits.push({ bodyType: orbit.bodyType, planetPath: bodyGenerator.model.path! }); 135 | this.planets.push(bodyGenerator); 136 | } else if (orbit.bodyType === 'ASTEROID_BELT') { 137 | bodyGenerator = new DebrisBeltGenerator( 138 | { 139 | name: DebrisBeltGenerator.getSequentialName(nameIndex++), 140 | parentPath: this.model.path, 141 | orbit, 142 | }, 143 | { seed: random.seed() } 144 | ); 145 | this.orbits.push({ bodyType: orbit.bodyType, beltPath: bodyGenerator.model.path! }); 146 | this.belts.push(bodyGenerator); 147 | } else { 148 | bodyGenerator = new EmptyZone({ 149 | name: EmptyZone.getSequentialName(nameIndex++), 150 | orbit, 151 | }); 152 | this.orbits.push({ bodyType: 'EMPTY' }); 153 | } 154 | 155 | // this.orbits.push({bodyType: orbit.bodyType, }); // todo whole orbit logic should be separated, but accessible :? 156 | yield bodyGenerator; 157 | } 158 | this.physic.planetsCount = this.planets.length; 159 | this.physic.asteroidsCount = this.belts.length; 160 | // this.fillPlanetInfo(); // todo 161 | } catch (error) { 162 | console.warn('*generatePlanets()', error); 163 | } 164 | } 165 | 166 | private *generateOrbits(): IterableIterator { 167 | const planetOrbits = new SystemOrbitsGenerator( 168 | {}, 169 | // todo - when we generate from from existed system, we should use its model 170 | { star: this.getStarsModels()[0], seed: this.options.planetsSeed } 171 | ); 172 | for (const orbit of planetOrbits.generateOrbits()) yield orbit; 173 | } 174 | 175 | override toModel(): SystemModel { 176 | return super.toModel({ 177 | ...this.model, 178 | // orbits: this.orbits.map((orbit) => orbit.toModel?.()), 179 | orbits: this.orbits, 180 | stars: this.stars.map((star) => star.toModel()), 181 | belts: this.belts.map((belt) => belt.toModel()), 182 | planets: this.planets.map((planet) => planet.toModel()), 183 | physic: { ...this.physic }, 184 | options: { ...this.options }, 185 | }); 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/galaxy/system/system-orbits-generator.ts: -------------------------------------------------------------------------------- 1 | import { RandomObject, Seed } from '../../utils'; 2 | import { RandomGenerator } from '../basic-generator'; 3 | import { OrbitPhysic, OrbitPhysicModel, StarPhysicModel } from '../physic'; 4 | import { ORBIT_OBJECT_TYPES,OrbitGenerator, OrbitModel } from '../physic/orbit-generator'; 5 | import { StarGenerator, StarModel } from '../star'; 6 | 7 | interface SystemOrbitOptions { 8 | seed?: Seed; 9 | random?: RandomObject; 10 | prefer_habitable?: boolean; 11 | star: StarModel; // todo StarPhysic be enought 12 | } 13 | export interface SystemOrbitModel extends Partial { 14 | options?: Partial; 15 | order?: number; 16 | } 17 | 18 | const defaultOptions: Partial = { 19 | prefer_habitable: true, 20 | }; 21 | 22 | export class SystemOrbitsGenerator extends RandomGenerator { 23 | public orbits: OrbitGenerator[] = []; 24 | public topology?: string; 25 | public beetwen_orbits_factor = [1.4, 2]; 26 | public modyficators: ( 27 | | typeof SystemOrbitsGenerator.ClassicSystem 28 | | typeof SystemOrbitsGenerator.HabitableMoonSystem 29 | )[] = []; 30 | 31 | constructor(model: SystemOrbitModel, options: SystemOrbitOptions) { 32 | super(model, { ...defaultOptions, ...model.options, ...options }); 33 | } 34 | 35 | *generateOrbits(): IterableIterator { 36 | // console.log('*generateOrbits()'); 37 | if (!this.orbits.length) this.build(); 38 | for (const orbit of this.orbits) yield orbit; 39 | } 40 | 41 | build() { 42 | this.generateTopology(); 43 | this.generateProtoOrbits(); 44 | this.fillOrbitZone(); 45 | this.fillOrbitPeriod(); 46 | // console.log('build()', this); 47 | const opts = { 48 | prefer_habitable: this.options.prefer_habitable, 49 | }; 50 | for (const modyficator of this.modyficators) modyficator(this.random, opts)(this); 51 | for (const orbit of this.orbits) orbit.generateType(this.random); 52 | // this.fillInfo(); // todo 53 | return true; 54 | } 55 | 56 | generateTopology() { 57 | const topology = this.random.weighted(TOPOLOGIES.map((top) => [top.probability, top])); 58 | this.topology = topology.name; 59 | this.modyficators = topology.modyficators; 60 | } 61 | 62 | generateProtoOrbits() { 63 | if (!this.options.star?.physic) throw new Error('no star available'); 64 | 65 | const { physic } = this.options.star; 66 | let firstOrbitdistance = null; 67 | 68 | const createOrbit = (distance: number) => 69 | this.orbits.push(new OrbitGenerator({ distance, order: this.orbits.length }, { seed: this.random.seed() })); 70 | 71 | // Get fist orbit distance 72 | if (this.options.prefer_habitable) { 73 | // Make sure at least one habitable will be generated 74 | firstOrbitdistance = this.random.real(physic.habitable_zone_inner, physic.habitable_zone_outer); 75 | } else { 76 | firstOrbitdistance = this.random.real(physic.inner_limit, physic.outer_limit); 77 | } 78 | createOrbit(firstOrbitdistance); 79 | // Fill orbits down 80 | let lastDistance = firstOrbitdistance; 81 | while (true) { 82 | const nextOrbit = lastDistance / this.random.real(this.beetwen_orbits_factor[0], this.beetwen_orbits_factor[1]); 83 | if (nextOrbit < physic.inner_limit) break; 84 | createOrbit(nextOrbit); 85 | lastDistance = nextOrbit; 86 | } 87 | // Fill orbits up 88 | lastDistance = firstOrbitdistance; 89 | while (true) { 90 | const nextOrbit = lastDistance * this.random.real(this.beetwen_orbits_factor[0], this.beetwen_orbits_factor[1]); 91 | if (nextOrbit > physic.outer_limit) break; 92 | createOrbit(nextOrbit); 93 | lastDistance = nextOrbit; 94 | } 95 | // Sort by distance 96 | this.orbits.sort((ox, oy) => OrbitPhysic.sortByDistance(ox.model, oy.model)); 97 | // Fill from sun order 98 | this.orbits.forEach((orbit, index) => orbit.updateModel('order', index + 1)); 99 | } 100 | 101 | fillOrbitZone() { 102 | if (!this.options.star?.physic) throw new Error('no star available'); 103 | const { physic } = this.options.star; 104 | 105 | for (const orbit of this.orbits) { 106 | orbit.updateModel('zone', OrbitPhysic.calcZone(orbit.model.distance, physic)); 107 | } 108 | } 109 | 110 | fillOrbitPeriod() { 111 | for (const orbit of this.orbits) { 112 | const mass = this.options.star.physic?.mass || this.options.star.mass as number; 113 | const orbitalPeriod = OrbitPhysic.calcOrbitalPeriod(mass, orbit.model.distance); 114 | orbit.updateModel('orbitalPeriod', orbitalPeriod); 115 | orbit.updateModel('orbitalPeriodInDays', OrbitPhysic.convertOrbitalPeriodToDays(orbitalPeriod)); 116 | } 117 | } 118 | 119 | // static _generationStrategies = [ 120 | // [1, PlanetOrbitGenerator.ClassicSystem], 121 | // [.1, PlanetOrbitGenerator.HabitableMoonSystem], 122 | // [.05, PlanetOrbitGenerator.HotJupiterSystem] 123 | // ] 124 | // todo fix that after planet rework - not working properly 125 | static ClassicSystem(random: RandomObject, { prefer_habitable }: { prefer_habitable?: boolean }) { 126 | return (systemOrbit: SystemOrbitsGenerator) => { 127 | // systemOrbit.topology = 'classic' 128 | for (const orbit of systemOrbit.orbits) { 129 | let tags = []; 130 | for (const orbitObject of ORBIT_OBJECT_TYPES) { 131 | if (orbitObject.when?.(systemOrbit.options.star?.physic as StarPhysicModel, orbit.model)) 132 | tags.push(orbitObject.type as string); 133 | } 134 | if (prefer_habitable && tags.includes('earth')) { 135 | tags = ['earth']; 136 | } 137 | orbit.setTags(tags); 138 | } 139 | }; 140 | } 141 | // Jupiter like planet (gas giant) transfer to habitable zone from outer zone, 142 | // space between is cleared by giant. 143 | static HabitableMoonSystem(random: RandomObject) { 144 | return (planetOrbit: SystemOrbitsGenerator) => { 145 | // planetOrbit.topology = 'habitable_moon' 146 | let findedHabitable = false; 147 | let findedGasGiant = false; 148 | for (const orbit of planetOrbit.orbits) { 149 | const isGiant = orbit.hasTag('gas_giant'); 150 | if (orbit.model.zone == 'habitable' && !findedHabitable) { 151 | orbit.lockTag('gas_giant'); 152 | // orbit.generateMoons(random, { min_one: ['earth'] }) 153 | // orbit.lock = true 154 | // orbit.tags = ['gas_giant'] 155 | findedHabitable = true; 156 | } else if (findedHabitable && !findedGasGiant) { 157 | // orbit.tags = ['EMPTY'] 158 | orbit.markAsEmpty(); 159 | } 160 | if (isGiant) findedGasGiant = true; 161 | } 162 | }; 163 | } 164 | // static HotJupiterSystem(random) { 165 | // return (planetOrbit) => { 166 | // // planetOrbit.topology = 'hot_jupiter' 167 | // let findedGasGiant = false; 168 | // for (const [index, orbit] of planetOrbit.orbits.entries()) { 169 | // const isGiant = orbit.tags.some((tgs) => tgs == 'gas_giant'); 170 | // if (index == 0) { 171 | // orbit.lockTag('gas_giant'); 172 | // // orbit.lock = true 173 | // // orbit.tags = ['gas_giant'] 174 | // } else if (!findedGasGiant) { 175 | // orbit.markAsEmpty(); 176 | // // orbit.tags = ['EMPTY'] 177 | // } 178 | // if (isGiant) findedGasGiant = true; 179 | // } 180 | // }; 181 | // } 182 | } 183 | 184 | const TOPOLOGIES = [ 185 | { probability: 1, name: 'classic', modyficators: [SystemOrbitsGenerator.ClassicSystem] }, 186 | { 187 | probability: 0.1, 188 | name: 'habitable_moon', 189 | modyficators: [SystemOrbitsGenerator.ClassicSystem, SystemOrbitsGenerator.HabitableMoonSystem], 190 | }, 191 | // { 192 | // probability: 0.01, 193 | // name: 'hot_jupiter', 194 | // modyficators: [SystemOrbitsGenerator.ClassicSystem, SystemOrbitsGenerator.HotJupiterSystem], 195 | // }, 196 | // { 197 | // probability: 0.05, 198 | // name: 'hot_jupiter_habitable_moon', 199 | // modyficators: [ 200 | // SystemOrbitsGenerator.ClassicSystem, 201 | // SystemOrbitsGenerator.HotJupiterSystem, 202 | // SystemOrbitsGenerator.HabitableMoonSystem, 203 | // ], 204 | // }, 205 | ] as const; 206 | -------------------------------------------------------------------------------- /src/galaxy/system/types.ts: -------------------------------------------------------------------------------- 1 | import { Vector3 } from 'three'; 2 | 3 | import { GalaxyPath, SystemPath } from '../../global.types'; 4 | import { SystemPhysicModel } from '../physic'; 5 | import { OrbitModel } from '../physic/orbit-generator'; 6 | import { PlanetModel } from '../planet'; 7 | import { StarModel } from '../star/types'; 8 | 9 | import { DebrisBeltModel } from './debris-belt-generator'; 10 | import { EmptyZoneModel } from './empty-zone'; 11 | 12 | export interface SystemModel { 13 | id?: string; 14 | path?: SystemPath; 15 | parentPath?: GalaxyPath; 16 | name?: `${string}`; 17 | starColor?: string; 18 | habitable?: boolean; 19 | starRadius?: number; 20 | position?: Vector3; 21 | temperature?: number; 22 | 23 | stars?: StarModel[]; 24 | // orbits?: (PlanetModel | DebrisBeltModel | EmptyZoneModel)[]; 25 | // orbits?: OrbitModel[]; 26 | orbits?: ( 27 | | { bodyType: 'PLANET'; planetPath: string } 28 | | { bodyType: 'ASTEROID_BELT'; beltPath: string } 29 | | { bodyType: 'EMPTY' } 30 | )[]; 31 | belts?: DebrisBeltModel[]; 32 | planets?: PlanetModel[]; 33 | physic?: SystemPhysicModel; 34 | options?: { 35 | seed?: number; 36 | starsSeed?: number; 37 | planetsSeed?: number; 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /src/global.types.ts: -------------------------------------------------------------------------------- 1 | export type GalaxyPath = `${string}`; 2 | 3 | type SystemName = `${string}`; 4 | export type SystemPath = `${GalaxyPath}/${SystemName}`; 5 | 6 | type StarName = `${string}`; 7 | export type StarPath = `${SystemPath}/s:${StarName}`; 8 | 9 | export type BeltPath = `${SystemPath}/b:${SystemName}_${string}`; 10 | 11 | export type PlanetPath = `${SystemPath}/p:${SystemName}_${string}`; 12 | 13 | type RegionName = `${number}`; 14 | export type RegionPath = `${PlanetPath}/r:${RegionName}`; 15 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './global.types'; 2 | 3 | export * from './galaxy'; 4 | export * from './galaxy-shape'; 5 | 6 | export * from './interfaces'; 7 | export * from './utils'; 8 | -------------------------------------------------------------------------------- /src/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export interface Position { 2 | x: number; 3 | y: number; 4 | z: number; 5 | } 6 | 7 | export enum GalaxyClass { 8 | Spiral = 'spiral', 9 | Grid = 'grid', 10 | } 11 | 12 | export enum GalaxyAge { 13 | Young = 'young', 14 | Mature = 'mature', 15 | Acient = 'ancient', 16 | } 17 | 18 | export enum GalaxyClassShape { 19 | Elliptical = 'elliptical', 20 | Spiral2 = 'spiral-2', 21 | Spiral3 = 'spiral-3', 22 | Spiral4 = 'spiral-4', 23 | Cluster = 'cluster', 24 | Disc = 'disc', 25 | Box = 'box', 26 | Irregular = 'irregular', 27 | Ring = 'ring', 28 | } 29 | 30 | export const CONSTELATIONS_NAMES = [ 31 | 'centauri', 32 | 'andromeda', 33 | 'antlia', 34 | 'aquarius', 35 | 'aquila', 36 | 'ara', 37 | 'aries', 38 | 'auriga', 39 | 'bootes', 40 | 'caelum', 41 | 'camelopardalis', 42 | 'cancer', 43 | 'canes venatici', 44 | 'canis major', 45 | 'canis minor', 46 | 'capricornus', 47 | 'carina', 48 | 'cassiopeia', 49 | 'centaurus', 50 | 'cepheus', 51 | 'cetus', 52 | 'chamaeleon', 53 | 'circinus', 54 | 'columba', 55 | 'coma berenices', 56 | 'corona australis', 57 | 'corona borealis', 58 | 'corvus', 59 | 'crater', 60 | 'crux', 61 | 'cygnus', 62 | 'dephinus', 63 | 'dorado', 64 | 'draco', 65 | 'equuleus', 66 | 'eridanus', 67 | 'fornax', 68 | 'gemini', 69 | 'grus', 70 | 'hercules', 71 | 'horologium', 72 | 'hydra', 73 | 'hydrus', 74 | 'indus', 75 | 'lacerta', 76 | 'leo', 77 | 'leo minor', 78 | 'lepus', 79 | 'libra', 80 | 'lupus', 81 | 'lynx', 82 | 'lyra', 83 | 'mensa', 84 | 'microscopium', 85 | 'monoceros', 86 | 'musca', 87 | 'norma', 88 | 'octans', 89 | 'ophiuchus', 90 | 'orion', 91 | 'pavo', 92 | 'pegasus', 93 | 'perseus', 94 | 'phoenix', 95 | 'pictor', 96 | 'pisces', 97 | 'piscis austrinus', 98 | 'puppis', 99 | 'pyxis', 100 | 'reticulum', 101 | 'sagitta', 102 | 'sagittarius', 103 | 'scorpius', 104 | 'sculptor', 105 | 'scutum', 106 | 'serpens', 107 | 'sextans', 108 | 'taurus', 109 | 'telescopium', 110 | 'triangulum', 111 | 'triangulum australe', 112 | 'tucana', 113 | 'ursa major', 114 | 'ursa minor', 115 | 'vela', 116 | 'virgo', 117 | 'volans', 118 | 'vulpecula', 119 | // OLD 120 | 'anguilla', 121 | 'antinous', 122 | 'apes', 123 | 'apis', 124 | 'aranea', 125 | 'argo navis', 126 | 'asselli', 127 | 'praesepe', 128 | 'asterion', 129 | 'chara', 130 | 'battery', 131 | 'volta', 132 | 'bufo', 133 | 'cancer minor', 134 | 'capra', 135 | 'haedi', 136 | 'cerberus', 137 | 'corona firmiana', 138 | // https://en.wikipedia.org/wiki/Former_constellations 139 | ]; 140 | -------------------------------------------------------------------------------- /src/resources/GALAXIES_NAMES.ts: -------------------------------------------------------------------------------- 1 | export const REAL_GALAXIES_NAMES = [ 2 | 'Andromeda', 3 | 'Black Eye', 4 | 'Cartwheel', 5 | 'Cigar', 6 | 'Comet', 7 | 'Cosmos Redshift', 8 | "Hoag's Object", 9 | 'Magellanic Cloud', 10 | 'Pinwheel', 11 | 'Sombrero', 12 | 'Sunflower', 13 | 'Tadpole', 14 | 'Whirlpool', 15 | 'Milky Way', 16 | 'Triangulum', 17 | 'Centaurus A', 18 | 'Bode', 19 | 'Messier', 20 | 'Canis Major', 21 | 'Centaurus A', 22 | 'Circinus', 23 | 'Markarian', 24 | 'IC', 25 | 'NGC', 26 | 'Cygnus A', 27 | 'Malin', 28 | 'Lacertae', 29 | 'Virgo Stellar Stream', 30 | 'Baby Boom', 31 | 'Boötes Dwarf', 32 | 'WISE', 33 | 'Segue', 34 | 'Mice', 35 | 'Sagittarius', 36 | 'Omega Centauri', 37 | 'Mayall', 38 | ]; 39 | 40 | export const STARGATE_GALAXIES_NAMES = []; 41 | 42 | export const DUNE_GALAXIES_NAMES = []; 43 | 44 | export const getGalaxiesNames = () => [...REAL_GALAXIES_NAMES, ...DUNE_GALAXIES_NAMES, ...STARGATE_GALAXIES_NAMES]; 45 | -------------------------------------------------------------------------------- /src/utils/MarkovNames/MarkovModel.ts: -------------------------------------------------------------------------------- 1 | import { RandomObject } from '../RandomObject'; 2 | 3 | export class MarkovModel { 4 | constructor( 5 | public order: number, 6 | public startingStrings = new Map(), 7 | public productions = new Map>() 8 | ) {} 9 | 10 | Generate(random: RandomObject) { 11 | let builder = ''; 12 | let lastSelected = MarkovModel.WeightedRandom(random, this.startingStrings); 13 | 14 | do { 15 | //Extend string 16 | builder += lastSelected; 17 | if (builder.length < this.order) break; 18 | 19 | //Key to use to find next production 20 | const key = builder.substring(builder.length - this.order); 21 | 22 | //Find production rules for this key 23 | const prod = []; 24 | if (!this.productions.has(key)) break; 25 | 26 | //Produce next expansion 27 | lastSelected = MarkovModel.WeightedRandom(random, this.productions.get(key)); 28 | // @ts-ignore 29 | } while (lastSelected != ''); 30 | return builder; 31 | } 32 | 33 | static WeightedRandom(random: RandomObject, _items: Map = new Map()) { 34 | let num = random.unit(); 35 | const items = Array.from(_items.entries()); 36 | for (let i = 0; i < items.length; i++) { 37 | num -= items[i][1]; 38 | if (num <= 0) return items[i][0]; 39 | } 40 | throw new Error('InvalidOperationException'); 41 | } 42 | } 43 | 44 | export default MarkovModel; 45 | -------------------------------------------------------------------------------- /src/utils/MarkovNames/MarkovModelBuilder.ts: -------------------------------------------------------------------------------- 1 | import MarkovModel from './MarkovModel'; 2 | 3 | export class MarkovModelBuilder { 4 | private _startingStrings: Map; 5 | private _productions: Map>; 6 | 7 | constructor(private _order: number) { 8 | this._startingStrings = new Map(); 9 | this._productions = new Map(); 10 | } 11 | 12 | TeachArray(examples: string[]) { 13 | examples.forEach((e) => this.Teach(e)); 14 | // for (const example of examples) this.Teach(example); 15 | return this; 16 | } 17 | 18 | Teach(example: string) { 19 | example = example.toLowerCase(); 20 | 21 | //if the example is shorter than the order, just add a production that this example instantly leads to null 22 | if (example.length <= this._order) { 23 | MarkovModelBuilder.AddOrUpdateMap(this._startingStrings, example, 1, (a) => a + 1); 24 | this.Increment(example, ''); 25 | return this; 26 | } 27 | 28 | //Chomp string into "order" length parts, and the single letter which follows it 29 | for (let i = 0; i < example.length - this._order + 1; i++) { 30 | const key = example.substring(i, i + this._order).trim(); 31 | if (i == 0) MarkovModelBuilder.AddOrUpdateMap(this._startingStrings, key, 1, (a) => a + 1); 32 | const sub = 33 | i + this._order == example.length ? '' : example.substring(i + this._order, i + this._order + 1).trim(); 34 | this.Increment(key, sub); 35 | } 36 | 37 | return this; 38 | } 39 | 40 | toModel() { 41 | const normalized = this.Normalize(this._startingStrings); 42 | const startingStrings = new Map(normalized); 43 | const productions = new Map([...this._productions.entries()].map((a) => [a[0], new Map(this.Normalize(a[1]))])); 44 | 45 | return new MarkovModel(this._order, startingStrings, productions); 46 | } 47 | 48 | Normalize(stringCounts: Map = new Map()) { 49 | /* fix: somehow Map.entries and Map.values not working with SB */ 50 | const values = Array.from(stringCounts, ([key, value]) => value); 51 | const entries = Array.from(stringCounts, ([key, value]) => [key, value] as const); 52 | const total = values.reduce((a, b) => a + b, 0); 53 | return entries.map(([key, value]) => [key, value / total] as const); 54 | } 55 | 56 | Increment(key: string, value: string) { 57 | if (!this._productions.has(key)) this._productions.set(key, new Map()); 58 | MarkovModelBuilder.AddOrUpdateMap(this._productions.get(key) as Map, value, 1, (a) => a + 1); 59 | } 60 | 61 | static AddOrUpdateMap(map: Map, key: string, value: number, update: (value: number) => number) { 62 | if (!map.has(key)) map.set(key, value); 63 | else map.set(key, update(map.get(key) as number)); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/utils/MarkovNames/README.md: -------------------------------------------------------------------------------- 1 | based on [MarvellousMarkovModels (C#)](https://github.com/martindevans/MarvellousMarkovModels) by _Martin Evans_ 2 | -------------------------------------------------------------------------------- /src/utils/MarkovNames/index.ts: -------------------------------------------------------------------------------- 1 | export * from './MarkovModelBuilder'; 2 | export * from './MarkovModel'; 3 | -------------------------------------------------------------------------------- /src/utils/Names.ts: -------------------------------------------------------------------------------- 1 | import { getGalaxiesNames } from '../resources/GALAXIES_NAMES'; 2 | import { getStarsNames } from '../resources/STARS_NAMES'; 3 | 4 | import { MarkovModel,MarkovModelBuilder } from './MarkovNames'; 5 | // import PLANETS_NAMES from '../../resources/PLANETS_NAMES' 6 | import RandomObject from './RandomObject'; 7 | 8 | export class Names { 9 | private constructor() {} 10 | 11 | static names = [ 12 | 'Trevor', 13 | 'Yeltsin', 14 | 'Barnard', 15 | 'Genovese', 16 | 'Martin', 17 | 'Simon', 18 | 'Rob', 19 | 'Ed', 20 | 'Walter', 21 | 'Mohammed', 22 | 'Emil', 23 | 'Shannon', 24 | 'Nicole', 25 | 'Yury', 26 | 'Coleman', 27 | 'Deanne', 28 | 'Rosenda', 29 | 'Geoffrey', 30 | 'Taryn', 31 | 'Noreen', 32 | 'Rita', 33 | 'Jeanetta', 34 | 'Stanton', 35 | 'Alesha', 36 | 'Mark', 37 | 'Georgiann', 38 | 'Modesta', 39 | 'Lee', 40 | 'Cyndi', 41 | 'Raylene', 42 | 'Ellamae', 43 | 'Sharmaine', 44 | 'Candra', 45 | 'Karine', 46 | 'Trena', 47 | 'Tarah', 48 | 'Dorie', 49 | 'Kyoko', 50 | 'Wei', 51 | 'Cristopher', 52 | 'Yoshie', 53 | 'Meany', 54 | 'Zola', 55 | 'Corene', 56 | 'Suzie', 57 | 'Sherwood', 58 | 'Monnie', 59 | 'Savannah', 60 | 'Amalia', 61 | 'Lon', 62 | 'Luetta', 63 | 'Concetta', 64 | 'Dani', 65 | 'Sharen', 66 | 'Mora', 67 | 'Wilton', 68 | 'Hunter', 69 | 'Nobuko', 70 | 'Maryellen', 71 | 'Johnetta', 72 | 'Eleanora', 73 | 'Arline', 74 | 'Rae', 75 | 'Caprice', 76 | ]; 77 | static specialLocations = [ 78 | 'Epsilon Eridani', 79 | 'San Martin', 80 | 'Seaford Nine', 81 | 'Proctor Three', 82 | 'Smoking Frog', 83 | 'First of the Sun', 84 | 'Xendi Sabu', 85 | 'Bela Tegeuse', 86 | ]; 87 | static greekLetters = [ 88 | 'Alpha', 89 | 'Beta', 90 | 'Gamma', 91 | 'Delta', 92 | 'Epsilon', 93 | 'Zeta', 94 | 'Eta', 95 | 'Theta', 96 | 'Iota', 97 | 'Kappa', 98 | 'Lambda', 99 | 'Mu', 100 | 'Nu', 101 | 'Xi', 102 | 'Omnicron', 103 | 'Pi', 104 | 'Rho', 105 | 'Sigma', 106 | 'Tau', 107 | 'Upsilon', 108 | 'Phi', 109 | 'Chi', 110 | 'Psi', 111 | 'Omega', 112 | ]; 113 | static decorators = ['Major', 'Majoris', 'Minor', 'Minoris', 'Prime', 'Secundis', 'System']; 114 | 115 | static PlainMarkovT(destination: MarkovModel, names: string[]) { 116 | return (random: RandomObject) => { 117 | if (!destination) { 118 | const model = new MarkovModelBuilder(2); 119 | model.TeachArray(names); 120 | destination = model.toModel(); 121 | } 122 | // console.log(destination, names); 123 | return destination.Generate(random); 124 | }; 125 | } 126 | static Named(names: string[]) { 127 | return (random: RandomObject) => { 128 | return random.choice(names).toLowerCase(); //+ "'s Star" 129 | }; 130 | } 131 | 132 | static _system_markov: MarkovModel; 133 | static get systemNamingStrategies() { 134 | return [ 135 | [1, this.PlainMarkovT(this._system_markov, [...getStarsNames(), ...this.names])], 136 | [0.1, this.Named([...getGalaxiesNames(), ...this.names])], 137 | ]; 138 | } 139 | static GenerateSystemName(random: RandomObject, count = 1) { 140 | return random.weighted(this.systemNamingStrategies)(random); 141 | } 142 | 143 | static _galaxy_markov: MarkovModel; 144 | static getGalaxyNamingStrategies() { 145 | return [ 146 | [1, this.PlainMarkovT(this._galaxy_markov, [...getStarsNames(), ...this.names])], 147 | [0.1, this.Named(getGalaxiesNames())], 148 | ]; 149 | } 150 | 151 | static GenerateGalaxyName(random: RandomObject, count = 1): string { 152 | return random.weighted(this.getGalaxyNamingStrategies())(random); 153 | } 154 | 155 | static _markov: MarkovModel; 156 | static get namingStrategies() { 157 | return [ 158 | [1, this.PlainMarkovT(this._markov, this.names)], 159 | [0.1, this.Named(this.names)], 160 | ]; 161 | } 162 | 163 | // static PlainMarkov(random) { 164 | // if (!this._markov) { 165 | // const m = new MarkovModelBuilder(2) 166 | // m.TeachArray(this.names) 167 | // this._markov = m.toModel() 168 | // } 169 | // return this._markov.Generate(random) 170 | // } 171 | // static NamedStar(random) { 172 | // return random.choice(this.names).toLowerCase() //+ "'s Star" 173 | // } 174 | 175 | static Generate(random: RandomObject, count = 1) { 176 | return random.weighted(this.namingStrategies)(random); 177 | 178 | // var choices = [] 179 | // while (choices.length < count) { 180 | // var newChoice = this.Generate(random) 181 | // // if (choices.Add(newChoice)) 182 | // choices.push(newChoice) 183 | // yield newChoice 184 | // } 185 | } 186 | } 187 | 188 | export default this; 189 | -------------------------------------------------------------------------------- /src/utils/RandomObject.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import XorShift128 from './XorShift128'; 3 | 4 | export class RandomObject { 5 | _random: XorShift128; 6 | get random() { 7 | return this._random; 8 | } 9 | set random(rand: XorShift128) { 10 | this._random = rand; 11 | } 12 | 13 | constructor(random) { 14 | if (random == null) { 15 | this.random = new XorShift128(); 16 | return this; 17 | } 18 | switch (typeof random) { 19 | case 'object': 20 | this.random = random; 21 | break; 22 | case 'number': 23 | case 'string': 24 | this.random = new XorShift128(random); 25 | break; 26 | default: 27 | this.random = new XorShift128(); 28 | } 29 | // this._random = random || new XorShift128(254158958941485) 30 | } 31 | 32 | choice(list) { 33 | if (!Array.isArray(list)) throw new TypeError('list must by an array'); 34 | // console.log('@',this.next(list.length), list[this.next(list.length)]); 35 | return list[this.next(list.length - 1)]; 36 | } 37 | 38 | weighted(list: [] | {}) { 39 | // weighted(list: T[] | {}): T[1] { 40 | if (typeof list !== 'object') throw new TypeError('list must be array or object'); 41 | if (!Array.isArray(list)) { 42 | list = Object.entries(list).map((e) => [e[1], e[0]]); 43 | } 44 | 45 | const sum = list.reduce((o, c) => (o += c[0]), 0); 46 | let num = this.random.real(0, sum); 47 | // console.log(sum, num) 48 | for (let i = 0; i < list.length; i++) { 49 | num -= list[i][0]; 50 | if (num < 0) return list[i][1]; 51 | } 52 | } 53 | 54 | Next(max?: number) { 55 | return this.next(max); 56 | } 57 | next(max?: number) { 58 | if (max != null) return this.random.integer(0, max); 59 | return this.random.next(); 60 | } 61 | unit() { 62 | return this.random.unit(); 63 | } 64 | integer(min: number, max: number) { 65 | return this.random.integer(min, max); 66 | } 67 | integerExclusive(min: number, max: number) { 68 | return this.random.integerExclusive(min, max); 69 | } 70 | real(min: number, max: number) { 71 | return this.random.real(min, max); 72 | } 73 | realInclusive(min: number, max: number) { 74 | return this.random.realInclusive(min, max); 75 | } 76 | seed() { 77 | return this.next(); 78 | } 79 | 80 | NormallyDistributedSingle(standardDeviation: number, mean: number) { 81 | // ***************************************************** 82 | // Intentionally duplicated to avoid IEnumerable overhead 83 | // ***************************************************** 84 | // var u1 = random.NextDouble() //these are uniform(0,1) random doubles 85 | // var u2 = random.NextDouble() 86 | const u1 = this.random.unit(); //these are uniform(0,1) random doubles 87 | const u2 = this.random.unit(); 88 | // console.log('@@', u1, u2); 89 | 90 | const x1 = Math.sqrt(-2.0 * Math.log(u1)); 91 | const x2 = 2.0 * Math.PI * u2; 92 | const z1 = x1 * Math.sin(x2); //random normal(0,1) 93 | return z1 * standardDeviation + mean; 94 | } 95 | 96 | NormallyDistributedSingle4(standardDeviation: number, mean: number, min: number, max: number) { 97 | const nMax = (max - mean) / standardDeviation; 98 | const nMin = (min - mean) / standardDeviation; 99 | const nRange = nMax - nMin; 100 | const nMaxSq = nMax * nMax; 101 | const nMinSq = nMin * nMin; 102 | let subFrom = nMinSq; 103 | if (nMin < 0 && 0 < nMax) subFrom = 0; 104 | else if (nMax < 0) subFrom = nMaxSq; 105 | 106 | let sigma = 0.0; 107 | let u; 108 | let z; 109 | do { 110 | z = nRange * this.unit() + nMin; // uniform[normMin, normMax] 111 | sigma = Math.exp((subFrom - z * z) / 2); 112 | u = this.unit(); 113 | } while (u > sigma); 114 | 115 | return z * standardDeviation + mean; 116 | } 117 | 118 | static randomSeed() { 119 | return Math.floor(new Date().getTime() / Math.floor(Math.random() * 100 + 1)); 120 | } 121 | } 122 | 123 | export default RandomObject; 124 | -------------------------------------------------------------------------------- /src/utils/StarName.ts: -------------------------------------------------------------------------------- 1 | import { getStarsNames } from '../resources/STARS_NAMES'; 2 | 3 | import { MarkovModel } from './MarkovNames/MarkovModel'; 4 | import { capitalize } from './alphabet'; 5 | import { MarkovModelBuilder } from './MarkovNames'; 6 | import { RandomObject } from './RandomObject'; 7 | 8 | export class StarName { 9 | private constructor() {} 10 | static instance?: MarkovModel; 11 | static getInstance() { 12 | if (StarName.instance) return StarName.instance; 13 | // console.log(STARS_NAMES); 14 | StarName.instance = new MarkovModelBuilder(3).TeachArray(getStarsNames()).toModel(); 15 | return StarName.instance; 16 | } 17 | 18 | static _prefixStrategies = [ 19 | // [1.0, StarName.Greek], 20 | [1.0, StarName.Decorator], 21 | [0.01, StarName.RomanNumeral], 22 | [1.0, StarName.Letter], 23 | [1.0, StarName.Integer], 24 | [0.3, StarName.Decimal], 25 | [0.0, () => 'Al'], 26 | [0.0, () => 'San'], 27 | ]; 28 | static _suffixStrategies = [ 29 | // [1.0, StarName.Greek], 30 | [1.0, StarName.Decorator], 31 | [1.0, StarName.RomanNumeral], 32 | [1.0, StarName.Letter], 33 | [1.0, StarName.Integer], 34 | [0.3, StarName.Decimal], 35 | ]; 36 | static _namingStrategies = [ 37 | [1, StarName.PlainMarkov], 38 | [1, StarName.WithDecoration(1, StarName.WithDecoration(0.001, StarName.PlainMarkov))], 39 | [0.05, (random: RandomObject) => StarName.Letter(random) + '-' + StarName.Integer(random)], 40 | [0.01, StarName.NamedStar], 41 | [0.01, (random: RandomObject) => random.choice(StarName.specialLocations)], 42 | ]; 43 | static specialLocations = [ 44 | 'Epsilon Eridani', 45 | 'San Martin', 46 | 'Seaford Nine', 47 | 'Proctor Three', 48 | 'Smoking Frog', 49 | 'First of the Sun', 50 | 'Xendi Sabu', 51 | 'Bela Tegeuse', 52 | ]; 53 | static greekLetters = [ 54 | 'Alpha', 55 | 'Beta', 56 | 'Gamma', 57 | 'Delta', 58 | 'Epsilon', 59 | 'Zeta', 60 | 'Eta', 61 | 'Theta', 62 | 'Iota', 63 | 'Kappa', 64 | 'Lambda', 65 | 'Mu', 66 | 'Nu', 67 | 'Xi', 68 | 'Omnicron', 69 | 'Pi', 70 | 'Rho', 71 | 'Sigma', 72 | 'Tau', 73 | 'Upsilon', 74 | 'Phi', 75 | 'Chi', 76 | 'Psi', 77 | 'Omega', 78 | ]; 79 | static decorators = ['Major', 'Majoris', 'Minor', 'Minoris', 'Prime', 'Secundis']; 80 | 81 | static Greek(random: RandomObject) { 82 | // console.log('Greek'); 83 | return random.choice(StarName.greekLetters); 84 | } 85 | static Decorator(random: RandomObject) { 86 | // console.log('Decorator'); 87 | return random.choice(StarName.decorators); 88 | } 89 | static RomanNumeral(random: RandomObject) { 90 | // console.log('RomanNumeral'); 91 | const integer = random.NormallyDistributedSingle4(10, 15, 1, 200); 92 | const bigInteger = random.NormallyDistributedSingle4(400, 100, 200, 3000); 93 | return StarName.ToRoman(random.unit() > 0.8 ? integer : bigInteger); 94 | } 95 | static Integer(random: RandomObject) { 96 | // console.log('Integer'); 97 | const number = random.NormallyDistributedSingle4(100, 5, 1, 1000); 98 | return Math.trunc(Math.abs(number)); 99 | } 100 | static Decimal(random: RandomObject) { 101 | // console.log('Decimal'); 102 | const number = random.NormallyDistributedSingle4(100, 5, 1, 1000); 103 | return Math.abs(parseInt(number.toFixed(2))); 104 | } 105 | static Letter(random: RandomObject) { 106 | // console.log('Letter'); 107 | return String.fromCharCode(random.integer(65, 90)); 108 | } 109 | 110 | // static markovNameModel = new MarkovModelBuilder(3).TeachArray(STARS_NAMES).toModel(); 111 | static PlainMarkov(random: RandomObject) { 112 | // console.log('PlainMarkov'); 113 | const str = StarName.getInstance().Generate(random); // todo capitalize? 114 | return capitalize(str); 115 | } 116 | static NamedStar(random: RandomObject) { 117 | // console.log('NamedStar'); 118 | return random.choice(getStarsNames()); 119 | } 120 | 121 | static WithDecoration(probability: number, func: (random: RandomObject) => string) { 122 | return (random: RandomObject) => { 123 | // console.log('WithDecoration'); 124 | const result = func(random); 125 | if (random.unit() > probability) return result; 126 | 127 | const prefix = random.weighted(StarName._prefixStrategies)(random) + ' '; 128 | const suffix = ' ' + random.weighted(StarName._suffixStrategies)(random); 129 | 130 | switch ( 131 | random.weighted([ 132 | [0.4, 'neither'], 133 | [1.0, 'prefix'], 134 | [1.0, 'suffix'], 135 | [0.2, 'both'], 136 | ]) 137 | ) { 138 | case 'prefix': 139 | return prefix + result; 140 | case 'suffix': 141 | return result + suffix; 142 | case 'both': 143 | return prefix + result + suffix; 144 | default: 145 | return result; 146 | } 147 | }; 148 | } 149 | 150 | static ToRoman(number: number): string { 151 | if (number < 1) return ''; 152 | if (number >= 1000) return 'M' + StarName.ToRoman(number - 1000); 153 | if (number >= 900) return 'CM' + StarName.ToRoman(number - 900); 154 | if (number >= 500) return 'D' + StarName.ToRoman(number - 500); 155 | if (number >= 400) return 'CD' + StarName.ToRoman(number - 400); 156 | if (number >= 100) return 'C' + StarName.ToRoman(number - 100); 157 | if (number >= 90) return 'XC' + StarName.ToRoman(number - 90); 158 | if (number >= 50) return 'L' + StarName.ToRoman(number - 50); 159 | if (number >= 40) return 'XL' + StarName.ToRoman(number - 40); 160 | if (number >= 10) return 'X' + StarName.ToRoman(number - 10); 161 | if (number >= 9) return 'IX' + StarName.ToRoman(number - 9); 162 | if (number >= 5) return 'V' + StarName.ToRoman(number - 5); 163 | if (number >= 4) return 'IV' + StarName.ToRoman(number - 4); 164 | if (number >= 1) return 'I' + StarName.ToRoman(number - 1); 165 | throw new RangeError(); 166 | } 167 | 168 | static Generate(random: RandomObject) { 169 | return random.weighted(StarName._namingStrategies)(random).trim(); 170 | } 171 | static async GenerateCount(random: RandomObject, count = 1) { 172 | const names = []; 173 | while (names.length < count) { 174 | const name = await StarName.Generate(random); 175 | if (names.indexOf(name) === -1) names.push(name); 176 | } 177 | // console.log('>>', names, '<<'); 178 | return names; 179 | } 180 | } 181 | 182 | export default StarName; 183 | -------------------------------------------------------------------------------- /src/utils/SteppedAction.js: -------------------------------------------------------------------------------- 1 | // import { accumulateArray } from './arrays'; 2 | function accumulateArray(array, state, accumulator) { 3 | for (let i = 0; i < array.length; ++i) { 4 | state = accumulator(state, array[i]) 5 | } 6 | return state 7 | } 8 | 9 | class SteppedAction{ 10 | constructor(progressUpdater, unbrokenInterval, sleepInterval){ 11 | this.callStack = null; 12 | this.subactions = []; 13 | this.finalizers = []; 14 | this.unbrokenInterval = (typeof (unbrokenInterval) === "number" && unbrokenInterval >= 0) ? unbrokenInterval : 16; 15 | this.sleepInterval = (typeof (sleepInterval) === "number" && sleepInterval >= 0) ? sleepInterval : 0; 16 | this.loopAction = false; 17 | this.started = false; 18 | this.canceled = false; 19 | this.completed = false; 20 | this.intervalIteration = 0; //number of times an unbroken interval has been completed 21 | this.stepIteration = 0; //number of times any of the stepper functions have been called 22 | this.intervalStepIteration = null; //number of times any of the stepper functions have been called during the current interval 23 | this.intervalStartTime = null; //begin time of the current interval 24 | this.intervalEndTime = null; //end time of the current interval 25 | this.progressUpdater = (typeof (progressUpdater) === "function") ? progressUpdater : null; 26 | } 27 | 28 | 29 | 30 | execute(){ 31 | if (!this.canceled && !this.completed && this.callStack === null && this.started === false){ 32 | this.started = true; 33 | if (this.subactions.length > 0){ 34 | this.beginSubactions(0, 1); 35 | if (this.progressUpdater !== null) 36 | this.progressUpdater(this); 37 | setTimeout(this.step.bind(this), this.sleepInterval); 38 | } else{ 39 | this.completed = true; 40 | } 41 | } 42 | return this; 43 | } 44 | 45 | step(){ 46 | this.intervalStartTime = Date.now(); 47 | this.intervalEndTime = this.intervalStartTime + this.unbrokenInterval; 48 | this.intervalStepIteration = 0; 49 | while (Date.now() < this.intervalEndTime && !this.canceled && !this.completed){ 50 | var action = this.callStack.actions[this.callStack.index]; 51 | 52 | this.callStack.loop = false; 53 | action.action(this); 54 | this.intervalStepIteration += 1; 55 | this.stepIteration += 1; 56 | 57 | if (this.subactions.length > 0){ 58 | this.beginSubactions(this.getProgress(), (this.callStack.loop) ? 0 : (1 - this.callStack.loopProgress) * action.proportion / this.callStack.proportionSum * this.callStack.parentProgressRange); 59 | } else{ 60 | while (this.callStack !== null && this.callStack.loop === false && this.callStack.index === this.callStack.actions.length - 1){ 61 | for (var i = 0; i < this.callStack.finalizers.length; ++i){ 62 | this.callStack.finalizers[i](this); 63 | } 64 | this.callStack = this.callStack.parent; 65 | } 66 | if (this.callStack !== null){ 67 | if (this.callStack.loop === false){ 68 | this.callStack.loopProgress = 0; 69 | this.callStack.index += 1; 70 | } 71 | } else{ 72 | this.completed = true; 73 | } 74 | } 75 | } 76 | this.intervalStartTime = null; 77 | this.intervalEndTime = null; 78 | this.intervalStepIteration = null; 79 | 80 | if (this.progressUpdater !== null) 81 | this.progressUpdater(this); 82 | 83 | this.intervalIteration += 1; 84 | if (this.canceled){ 85 | while (this.callStack !== null){ 86 | for (var i = 0; i < this.callStack.finalizers.length; ++i){ 87 | this.callStack.finalizers[i](this); 88 | } 89 | this.callStack = this.callStack.parent; 90 | } 91 | } else if (!this.completed){ 92 | setTimeout(this.step.bind(this), this.sleepInterval); 93 | } 94 | } 95 | 96 | beginSubactions(parentProgress, parentProgressRange){ 97 | this.callStack = { 98 | actions: this.subactions, 99 | finalizers: this.finalizers, 100 | proportionSum: accumulateArray(this.subactions, 0, function (sum, subaction) { 101 | return sum + subaction.proportion; 102 | }), 103 | index: 0, 104 | loop: false, 105 | loopProgress: 0, 106 | parent: this.callStack, 107 | parentProgress: parentProgress, 108 | parentProgressRange: parentProgressRange, 109 | }; 110 | this.subactions = []; 111 | this.finalizers = []; 112 | } 113 | 114 | cancel(){ 115 | this.canceled = true; 116 | } 117 | 118 | provideResult(resultProvider){ 119 | this.callStack.resultProvider = resultProvider; 120 | } 121 | 122 | loop(progress){ 123 | this.callStack.loop = true; 124 | if (typeof (progress) === "number" && progress >= 0 && progress < 1){ 125 | this.callStack.loopProgress = progress; 126 | } 127 | } 128 | 129 | executeSubaction(subaction, proportion, name){ 130 | proportion = (typeof (proportion) === "number" && proportion >= 0) ? proportion : 1; 131 | this.subactions.push({action: subaction, proportion: proportion, name: name}); 132 | return this; 133 | } 134 | 135 | getResult(recipient){ 136 | this.subactions.push({ 137 | action: function (action){ 138 | var resultProvider = action.callStack.resultProvider; 139 | var resultProviderType = typeof (resultProvider); 140 | if (resultProviderType === "function") 141 | recipient(resultProvider()); 142 | else if (resultProviderType !== "undefined") 143 | recipient(resultProvider); 144 | else 145 | recipient(); 146 | }, 147 | proportion: 0, 148 | }); 149 | return this; 150 | } 151 | 152 | finalize(finalizer) { 153 | this.finalizers.push(finalizer); 154 | return this; 155 | } 156 | 157 | getTimeRemainingInInterval() { 158 | if (this.intervalEndTime !== null) { 159 | return Math.max(0, this.intervalEndTime - Date.now()); 160 | } else { 161 | return 0; 162 | } 163 | } 164 | 165 | getProgress(){ 166 | if (this.callStack !== null){ 167 | if (this.callStack.proportionSum === 0) 168 | return this.callStack.parentProgress; 169 | 170 | var currentProportionSum = 0; 171 | for (var i = 0; i < this.callStack.index; ++i){ 172 | currentProportionSum += this.callStack.actions[i].proportion; 173 | } 174 | currentProportionSum += this.callStack.loopProgress * this.callStack.actions[this.callStack.index].proportion; 175 | return this.callStack.parentProgress + currentProportionSum / this.callStack.proportionSum * this.callStack.parentProgressRange; 176 | } else{ 177 | return this.completed ? 1 : 0; 178 | } 179 | } 180 | 181 | getCurrentActionName(){ 182 | var callStack = this.callStack; 183 | while (callStack !== null){ 184 | var action = callStack.actions[callStack.index]; 185 | if (typeof (action.name) === "string") 186 | return action.name; 187 | callStack = callStack.parent; 188 | } 189 | 190 | return ""; 191 | } 192 | } 193 | 194 | export default SteppedAction; 195 | -------------------------------------------------------------------------------- /src/utils/XorShift128.ts: -------------------------------------------------------------------------------- 1 | // export type Seed = string | number 2 | export type Seed = number; 3 | 4 | export class XorShift128 { 5 | private x: Seed; 6 | private y: Seed; 7 | private z: Seed; 8 | private w: Seed; 9 | 10 | constructor(x?: Seed, y?: Seed, z?: Seed, w?: Seed) { 11 | this.x = x ? x >>> 0 : 123456789; 12 | this.y = y ? y >>> 0 : 362436069; 13 | this.z = z ? z >>> 0 : 521288629; 14 | this.w = w ? w >>> 0 : 88675123; 15 | } 16 | 17 | next() { 18 | var t = this.x ^ ((this.x << 11) & 0x7fffffff); 19 | this.x = this.y; 20 | this.y = this.z; 21 | this.z = this.w; 22 | this.w = this.w ^ (this.w >> 19) ^ (t ^ (t >> 8)); 23 | return this.w; 24 | } 25 | 26 | unit() { 27 | return this.next() / 0x80000000; 28 | } 29 | 30 | unitInclusive() { 31 | return this.next() / 0x7fffffff; 32 | } 33 | 34 | integer(min: number, max: number) { 35 | return this.integerExclusive(min, max + 1); 36 | } 37 | 38 | integerExclusive(min: number, max: number) { 39 | min = Math.floor(min); 40 | max = Math.floor(max); 41 | return Math.floor(this.unit() * (max - min)) + min; 42 | } 43 | 44 | real(min: number, max: number) { 45 | return this.unit() * (max - min) + min; 46 | } 47 | 48 | realInclusive(min: number, max: number) { 49 | return this.unitInclusive() * (max - min) + min; 50 | } 51 | 52 | reseed(x: Seed, y: Seed, z: Seed, w: Seed) { 53 | this.x = x ? x >>> 0 : 123456789; 54 | this.y = y ? y >>> 0 : 362436069; 55 | this.z = z ? z >>> 0 : 521288629; 56 | this.w = w ? w >>> 0 : 88675123; 57 | } 58 | } 59 | 60 | export default XorShift128; 61 | -------------------------------------------------------------------------------- /src/utils/alphabet.spec.ts: -------------------------------------------------------------------------------- 1 | import { decimalToRoman, greekLetterNameToLetter, romanToDecimal } from './alphabet'; 2 | 3 | const values = [ 4 | [1, 'I'], 5 | [2, 'II'], 6 | [3, 'III'], 7 | [4, 'IV'], 8 | [5, 'V'], 9 | [6, 'VI'], 10 | [7, 'VII'], 11 | [8, 'VIII'], 12 | [9, 'IX'], 13 | [10, 'X'], 14 | [11, 'XI'], 15 | [40, 'XL'], 16 | [41, 'XLI'], 17 | [50, 'L'], 18 | [51, 'LI'], 19 | [99, 'XCIX'], 20 | [100, 'C'], 21 | [499, 'CDXCIX'], 22 | [500, 'D'], 23 | [999, 'CMXCIX'], 24 | [1000, 'M'], 25 | [2023, 'MMXXIII'], 26 | [4587, 'MMMMDLXXXVII'], 27 | [4999, 'MMMMCMXCIX'], 28 | ] as const; 29 | 30 | describe('word-generator util alphabet.ts', () => { 31 | it('decimalToRoman should work', () => { 32 | values.map(([decimal, roman]) => expect(decimalToRoman(decimal)).toEqual(roman)); 33 | }); 34 | 35 | it('romanToDecimal should work', () => { 36 | values.map(([decimal, roman]) => expect(romanToDecimal(roman)).toEqual(decimal)); 37 | }); 38 | 39 | it('greekLetterNameToLetter should return "ω" - greek omega letter', () => { 40 | expect(greekLetterNameToLetter('omega')).toEqual('ω'); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /src/utils/alphabet.ts: -------------------------------------------------------------------------------- 1 | // @formatter:off 2 | export const ISO_LATIN = ['a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z'] 3 | export const GREEK_LETTERS_NAMES = ['alpha','beta','gamma','delta','epsilon','zeta','eta','theta','iota','kappa','lambda','mu','nu','xi','omicron','pi','rho','sigma','tau','upsilon','phi','chi','psi','omega'] 4 | export const GREEK_LETTERS = ['α','β','γ','δ','ε','ζ','η','θ','ι','κ','λ','μ','ν','ξ','ο','π','ρ','σ','τ','υ','φ','χ','ψ','ω'] 5 | // @formatter:on 6 | 7 | export function greekLetterNameToLetter(name: string) { 8 | return GREEK_LETTERS[GREEK_LETTERS_NAMES.indexOf(name)] 9 | } 10 | 11 | export const numberToGreekChar = (num: number) => { 12 | return GREEK_LETTERS[num % GREEK_LETTERS.length]; 13 | }; 14 | 15 | function romanCharToInt(char: string) { 16 | switch (char) { 17 | case 'I': 18 | return 1; 19 | case 'V': 20 | return 5; 21 | case 'X': 22 | return 10; 23 | case 'L': 24 | return 50; 25 | case 'C': 26 | return 100; 27 | case 'D': 28 | return 500; 29 | case 'M': 30 | return 1000; 31 | default: 32 | return -1; 33 | } 34 | } 35 | 36 | export const romanToDecimal = (str?: string): number => { 37 | if (str == null) return -1; 38 | let num = romanCharToInt(str.charAt(0)); 39 | let prev: number; 40 | let curr: number; 41 | 42 | for (let i = 1; i < str.length; i++) { 43 | curr = romanCharToInt(str.charAt(i)); 44 | prev = romanCharToInt(str.charAt(i - 1)); 45 | num = curr <= prev ? num + curr : num - prev * 2 + curr; 46 | } 47 | 48 | return num; 49 | }; 50 | 51 | // max is 4999, for >=5000 should use extended roman numeral (V̅, V̅I̅, V̅I̅I̅, V̅I̅I̅I, I̅X̅, X̅) 52 | export const decimalToRoman = (num: number): string => { 53 | // if (num < 1 || num > 4999) return ''; 54 | if (num < 1) return ''; 55 | if (num >= 1000) return `M${decimalToRoman(num - 1000)}`; 56 | if (num >= 900) return `CM${decimalToRoman(num - 900)}`; 57 | if (num >= 500) return `D${decimalToRoman(num - 500)}`; 58 | if (num >= 400) return `CD${decimalToRoman(num - 400)}`; 59 | if (num >= 100) return `C${decimalToRoman(num - 100)}`; 60 | if (num >= 90) return `XC${decimalToRoman(num - 90)}`; 61 | if (num >= 50) return `L${decimalToRoman(num - 50)}`; 62 | if (num >= 40) return `XL${decimalToRoman(num - 40)}`; 63 | if (num >= 10) return `X${decimalToRoman(num - 10)}`; 64 | if (num >= 9) return `IX${decimalToRoman(num - 9)}`; 65 | if (num >= 5) return `V${decimalToRoman(num - 5)}`; 66 | if (num >= 4) return `IV${decimalToRoman(num - 4)}`; 67 | if (num >= 1) return `I${decimalToRoman(num - 1)}`; 68 | throw new RangeError(); 69 | }; 70 | 71 | export const codename = (str?: string) => { 72 | return str?.toLowerCase().replace(/\s/g, '_').replace(/'/g, '').replace(/`/g, ''); 73 | }; 74 | 75 | export const capitalize = (str: string) => { 76 | return str && str[0].toLocaleUpperCase() + str.substring(1) 77 | } -------------------------------------------------------------------------------- /src/utils/assignDeep.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | /*! 3 | * @description Recursive object extending 4 | * @author Viacheslav Lotsmanov 5 | * @license MIT 6 | * 7 | * The MIT License (MIT) 8 | * 9 | * Copyright (c) 2013-2015 Viacheslav Lotsmanov 10 | * 11 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 12 | * this software and associated documentation files (the "Software"), to deal in 13 | * the Software without restriction, including without limitation the rights to 14 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 15 | * the Software, and to permit persons to whom the Software is furnished to do so, 16 | * subject to the following conditions: 17 | * 18 | * The above copyright notice and this permission notice shall be included in all 19 | * copies or substantial portions of the Software. 20 | * 21 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 23 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 24 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 25 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 26 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 27 | */ 28 | 29 | function isSpecificValue(val) { 30 | return ( 31 | val instanceof Buffer 32 | || val instanceof Date 33 | || val instanceof RegExp 34 | ) ? true : false; 35 | } 36 | 37 | function cloneSpecificValue(val) { 38 | if (val instanceof Buffer) { 39 | var x = new Buffer(val.length); 40 | val.copy(x); 41 | return x; 42 | } else if (val instanceof Date) { 43 | return new Date(val.getTime()); 44 | } else if (val instanceof RegExp) { 45 | return new RegExp(val); 46 | } else { 47 | throw new Error('Unexpected situation'); 48 | } 49 | } 50 | 51 | /** 52 | * Recursive cloning array. 53 | */ 54 | function deepCloneArray(arr) { 55 | var clone = []; 56 | arr.forEach(function (item, index) { 57 | if (typeof item === 'object' && item !== null) { 58 | if (Array.isArray(item)) { 59 | clone[index] = deepCloneArray(item); 60 | } else if (isSpecificValue(item)) { 61 | clone[index] = cloneSpecificValue(item); 62 | } else { 63 | clone[index] = assignDeep({}, item); 64 | } 65 | } else { 66 | clone[index] = item; 67 | } 68 | }); 69 | return clone; 70 | } 71 | 72 | /** 73 | * Extening object that entered in first argument. 74 | * 75 | * Returns extended object or false if have no target object or incorrect type. 76 | * 77 | * If you wish to clone source object (without modify it), just use empty new 78 | * object as first argument, like this: 79 | * assignDeep({}, yourObj_1, [yourObj_N]); 80 | */ 81 | export function assignDeep(/*obj_1, [obj_2], [obj_N]*/) { 82 | if (arguments.length < 1 || typeof arguments[0] !== 'object') { 83 | return false; 84 | } 85 | 86 | if (arguments.length < 2) { 87 | return arguments[0]; 88 | } 89 | 90 | var target = arguments[0]; 91 | 92 | // convert arguments to array and cut off target object 93 | var args = Array.prototype.slice.call(arguments, 1); 94 | 95 | var val, src, clone; 96 | 97 | args.forEach(function (obj) { 98 | // skip argument if isn't an object, is null, or is an array 99 | if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) { 100 | return; 101 | } 102 | 103 | Object.keys(obj).forEach(function (key) { 104 | src = target[key]; // source value 105 | val = obj[key]; // new value 106 | 107 | // recursion prevention 108 | if (val === target) { 109 | return; 110 | 111 | /** 112 | * if new value isn't object then just overwrite by new value 113 | * instead of extending. 114 | */ 115 | } else if (typeof val !== 'object' || val === null) { 116 | target[key] = val; 117 | return; 118 | 119 | // just clone arrays (and recursive clone objects inside) 120 | } else if (Array.isArray(val)) { 121 | target[key] = deepCloneArray(val); 122 | return; 123 | 124 | // custom cloning and overwrite for specific objects 125 | } else if (isSpecificValue(val)) { 126 | target[key] = cloneSpecificValue(val); 127 | return; 128 | 129 | // overwrite by new value if source isn't object or array 130 | } else if (typeof src !== 'object' || src === null || Array.isArray(src)) { 131 | target[key] = assignDeep({}, val); 132 | return; 133 | 134 | // source value and new value is objects both, extending... 135 | } else { 136 | target[key] = assignDeep(src, val); 137 | return; 138 | } 139 | }); 140 | }); 141 | 142 | return target; 143 | } 144 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './MarkovNames'; 2 | export * from './assignDeep'; 3 | 4 | export * from './alphabet'; 5 | export * from './math' 6 | export * from './random'; 7 | export * from './RandomObject'; 8 | export * from './weighted-random'; 9 | export * from './world-path'; 10 | export * from './XorShift128'; 11 | -------------------------------------------------------------------------------- /src/utils/math.spec.ts: -------------------------------------------------------------------------------- 1 | import { truncDecimals } from './math'; 2 | 3 | describe('word-generator util math.ts', () => { 4 | it('truncDecimals should work', () => { 5 | expect(truncDecimals(4.27, 2)).toEqual(4.27) 6 | expect(truncDecimals(4.21, 2)).toEqual(4.21) 7 | expect(truncDecimals(4.29, 2)).toEqual(4.29) 8 | expect(truncDecimals(0.000000199, 2)).toEqual(0); 9 | expect(truncDecimals(0.99999999, 3)).toEqual(0.999); 10 | expect(truncDecimals(15.99999999, 1)).toEqual(15.9); 11 | expect(truncDecimals(4.9999, 3)).toEqual(4.999); 12 | expect(truncDecimals(-15.7784514, 1)).toEqual(-15.7); 13 | expect(truncDecimals(-15.7784514, 2)).toEqual(-15.77); 14 | expect(truncDecimals(-15.7784514, 3)).toEqual(-15.778); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/utils/math.ts: -------------------------------------------------------------------------------- 1 | function toFixed(x: number) { 2 | if (Math.abs(x) < 1.0) { 3 | let e = parseInt(x.toString().split('e-')[1]); 4 | if (e) { 5 | x *= Math.pow(10, e - 1); 6 | // @ts-ignore 7 | x = '0.' + new Array(e).join('0') + x.toString().substring(2); 8 | } 9 | } else { 10 | let e = parseInt(x.toString().split('+')[1]); 11 | if (e > 20) { 12 | e -= 20; 13 | x /= Math.pow(10, e); 14 | // @ts-ignore 15 | x += new Array(e + 1).join('0'); 16 | } 17 | } 18 | return String(x); 19 | } 20 | 21 | export const toFixedTrunc = (value: number, decimals: number): string => { 22 | const str = toFixed(value).split('.'); 23 | if (decimals <= 0) return str[0]; 24 | 25 | let decimal = str[1] || ''; 26 | if (decimal.length > decimals) return `${str[0]}.${decimal.substr(0, decimals)}`; 27 | while (decimal.length < decimals) decimal += '0'; 28 | return `${str[0]}.${decimal}`; 29 | }; 30 | 31 | export const truncDecimals = (value: number, decimals: number): number => { 32 | return parseFloat(toFixedTrunc(value, decimals)); 33 | }; 34 | 35 | /** 36 | * Spherical coordinate system to polar angle 37 | * @param phi phi (ϕ) in methematics meaning, angle with respect to polar axis 38 | * @returns 39 | */ 40 | export const sphericalPhiToPolarAngle = (phi: number) => phi * (180 / Math.PI); -------------------------------------------------------------------------------- /src/utils/random.ts: -------------------------------------------------------------------------------- 1 | export function randomInt(from = 0, to = 1000) { 2 | return Math.floor(Math.random() * to) + from 3 | } 4 | -------------------------------------------------------------------------------- /src/utils/star-name.spec.ts: -------------------------------------------------------------------------------- 1 | import { StarName } from './StarName'; 2 | 3 | describe('StarName - star-name.ts', () => { 4 | test('Greek returns a greek letter', () => { 5 | const result = StarName.Greek({ choice: jest.fn() } as any); 6 | expect(StarName.greekLetters).toContain(result); 7 | }); 8 | 9 | test('Decorator returns a decorator', () => { 10 | const result = StarName.Decorator({ choice: jest.fn() } as any); 11 | expect(StarName.decorators).toContain(result); 12 | }); 13 | 14 | test('RomanNumeral returns a roman numeral', () => { 15 | const result = StarName.RomanNumeral({ 16 | NormallyDistributedSingle4: jest.fn(() => 1), 17 | unit: jest.fn(() => 0.9), 18 | } as any); 19 | expect(result).toBe('I'); 20 | }); 21 | 22 | test('Integer returns an integer', () => { 23 | const result = StarName.Integer({ 24 | NormallyDistributedSingle4: jest.fn(() => 1), 25 | } as any); 26 | expect(Number.isInteger(result)).toBeTruthy(); 27 | }); 28 | 29 | test('Decimal returns a decimal', () => { 30 | const result = StarName.Decimal({ 31 | NormallyDistributedSingle4: jest.fn(() => 1), 32 | } as any); 33 | expect(result).toBeGreaterThanOrEqual(0); 34 | expect(result).toBeLessThanOrEqual(100); 35 | }); 36 | 37 | test('Letter returns a letter', () => { 38 | const result = StarName.Letter({ 39 | integer: jest.fn(() => 1), 40 | } as any); 41 | expect(typeof result).toBe('string'); 42 | expect(result.length).toBe(1); 43 | }); 44 | 45 | test('PlainMarkov returns a star name generated from MarkovModel', () => { 46 | StarName.instance = { 47 | Generate: jest.fn(() => 'star'), 48 | } as any; 49 | const result = StarName.PlainMarkov({} as any); 50 | expect(result).toBe('Star'); 51 | }); 52 | 53 | test('NamedStar returns a named star', () => { 54 | const names = ['Sirius', 'Betelgeuse', 'Vega']; 55 | const result = StarName.NamedStar({ 56 | choice: jest.fn(() => 'Sirius'), 57 | } as any); 58 | expect(names).toContain(result); 59 | }); 60 | 61 | test('WithDecoration returns a decorated star name', () => { 62 | const func = (random: any) => 'name'; 63 | const result = StarName.WithDecoration(0.5, func)({} as any); 64 | expect(result).toBe('name'); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /src/utils/weighted-random.ts: -------------------------------------------------------------------------------- 1 | function getTotal(weights: number[]) { 2 | return weights.reduce((prev, curr) => prev + curr, 0); 3 | } 4 | 5 | export function randomWeighted(args: any[], options: { rand: any; parse: any; total: any; normal: any }) { 6 | if (typeof options.rand !== 'function') { 7 | options.rand = Math.random; 8 | } 9 | if (typeof options.parse !== 'function') { 10 | options.parse = (x: number) => x; 11 | } 12 | // if (set.length !== weights.length) { 13 | // throw new TypeError('Different number of options & weights.') 14 | // } 15 | 16 | const weights = Object.values(args); 17 | const set = Object.keys(args); 18 | 19 | const total = options.total || (options.normal ? 1 : getTotal(weights)); 20 | let key = options.rand() * total; 21 | 22 | // if (typeof args === 'object') { 23 | // 24 | // } 25 | 26 | for (let index = 0; index < weights.length; index++) { 27 | key -= weights[index]; 28 | if (key < 0) return options.parse(set[index]); 29 | } 30 | 31 | throw new RangeError('All weights do not add up to >= 1 as expected.'); 32 | } 33 | 34 | export default randomWeighted; 35 | -------------------------------------------------------------------------------- /src/utils/world-path.spec.ts: -------------------------------------------------------------------------------- 1 | import { getPathTarget, parseWorldPath } from './world-path'; 2 | 3 | /** 4 | * nx run world-generator:test --testFile=libs/world-generator/src/utils/world-path.spec.ts 5 | */ 6 | describe('WorldPath - world-path.ts', () => { 7 | test('Should resolve world path target', () => { 8 | expect(getPathTarget('')).toBeFalsy(); 9 | expect(getPathTarget('galaxy1')).toBe('galaxy'); 10 | expect(getPathTarget('galaxy1/system1')).toBe('system'); 11 | expect(getPathTarget('galaxy1/system1/s:star1')).toBe('star'); 12 | expect(getPathTarget('galaxy1/system1/b:belt1')).toBe('belt'); 13 | expect(getPathTarget('galaxy1/system1/p:planet1/m:moon1')).toBe('moon'); 14 | expect(getPathTarget('galaxy1/system1/p:planet1/r:region1/c:construction1')).toBe('construction'); 15 | }); 16 | 17 | test('Path not contains designation', () => { 18 | const result = parseWorldPath(''); 19 | expect(result.galaxy).toBeFalsy(); 20 | expect(result.system).toBeFalsy(); 21 | expect(result.planet).toBeFalsy(); 22 | expect(result.target).toBeFalsy(); 23 | }); 24 | 25 | test('Path contains galaxy, system and star', () => { 26 | const result = parseWorldPath('galaxy1/system1/s:star1'); 27 | expect(result).toMatchObject({ 28 | galaxy: 'galaxy1', 29 | system: 'system1', 30 | star: 'star1', 31 | target: 'star', 32 | }); 33 | }); 34 | 35 | test('Path contains galaxy, system and belt', () => { 36 | const result = parseWorldPath('galaxy1/system1/b:belt1'); 37 | expect(result).toMatchObject({ 38 | galaxy: 'galaxy1', 39 | system: 'system1', 40 | belt: 'belt1', 41 | target: 'belt', 42 | }); 43 | expect(result.belt).toBeTruthy(); 44 | expect(result.planet).toBeFalsy(); 45 | }); 46 | 47 | test('Path contains galaxy, system, planet, region and construction', () => { 48 | const result = parseWorldPath('galaxy1/system1/p:planet1/r:region1/c:construction1'); 49 | expect(result).toMatchObject({ 50 | galaxy: 'galaxy1', 51 | system: 'system1', 52 | planet: 'planet1', 53 | region: 'region1', 54 | construction: 'construction1', 55 | target: 'construction', 56 | }); 57 | }); 58 | 59 | test('Path contains galaxy, system, planet and moon', () => { 60 | const result = parseWorldPath('galaxy1/system1/p:planet1/m:moon1'); 61 | expect(result).toMatchObject({ 62 | galaxy: 'galaxy1', 63 | system: 'system1', 64 | planet: 'planet1', 65 | moon: 'moon1', 66 | target: 'moon', 67 | }); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /src/utils/world-path.ts: -------------------------------------------------------------------------------- 1 | export const pathPrefix = { 2 | 's:': 'star', 3 | 'p:': 'planet', 4 | 'r:': 'region', 5 | 'm:': 'moon', 6 | 'b:': 'belt', 7 | 'c:': 'construction', 8 | 'a:': 'administration', 9 | 'q:': 'queue', 10 | 'o:': 'orbital', 11 | } as const; 12 | 13 | type Pos = { [K in typeof pathPrefix[keyof typeof pathPrefix]]: string }; 14 | 15 | export const getPathTarget = (path: string): 'galaxy' | 'system' | typeof pathPrefix[keyof typeof pathPrefix] | '' => { 16 | const groups = path.split('/'); 17 | if (!groups[0]) return ''; 18 | if (groups.length === 1) return 'galaxy'; 19 | if (groups.length === 2) return 'system'; 20 | const lastItem = groups.at(-1) ?? ''; 21 | const prefix = lastItem.split(':').at(0); 22 | // @ts-ignore 23 | return pathPrefix[`${prefix}:`] ?? ''; 24 | }; 25 | 26 | export interface WorldPath extends Pos { 27 | path: string; 28 | systemPath: string; 29 | planetPath: string; 30 | starPath: string; 31 | galaxy: string; 32 | system: string; 33 | target: '' | 'galaxy' | 'system' | typeof pathPrefix[keyof typeof pathPrefix]; 34 | groups: WorldPath['target'][]; 35 | } 36 | 37 | export const parseWorldPath = (path = '') => { 38 | const [galaxy, system, ...nodes] = path.split('/'); 39 | const position: WorldPath = { 40 | path, 41 | galaxy, 42 | system, 43 | systemPath: galaxy && system ? `${galaxy}/${system}` : '', 44 | star: '', 45 | planet: '', 46 | region: '', 47 | moon: '', 48 | belt: '', 49 | construction: '', 50 | target: system ? 'system' : galaxy ? 'galaxy' : '', 51 | // @ts-ignore 52 | groups: [galaxy && 'galaxy', system && 'system'].filter(Boolean), 53 | }; 54 | nodes.forEach((node, index) => { 55 | // @ts-ignore 56 | const pre = pathPrefix[node.substring(0, 2)]; // todo: allow more letters in pathPrefix 57 | if (pre) { 58 | position.groups.push(pre); 59 | // @ts-ignore 60 | position[pre] = node.substring(2, node.length); 61 | } 62 | if (index === nodes.length - 1) position.target = pre; 63 | }); 64 | 65 | position.starPath = position.target === 'star' ? path : ''; 66 | position.planetPath = position.target === 'planet' ? path : ''; 67 | 68 | return position; 69 | }; 70 | -------------------------------------------------------------------------------- /tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "CommonJS", 5 | "outDir": "./dist/cjs", 6 | "declaration": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist/esm", 5 | "module": "ESNext", 6 | "declaration": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.types.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist/types", 5 | "declaration": true, 6 | "emitDeclarationOnly": true 7 | } 8 | } 9 | --------------------------------------------------------------------------------