├── .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 | 
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 |
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 |
--------------------------------------------------------------------------------