@where-stars-drift/core 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +143 -0
  3. package/dist/src/base/bounded-body.d.ts +8 -0
  4. package/dist/src/base/bounded-body.js +10 -0
  5. package/dist/src/base/celestial-body.d.ts +12 -0
  6. package/dist/src/base/celestial-body.js +11 -0
  7. package/dist/src/base/hoverable.d.ts +10 -0
  8. package/dist/src/base/hoverable.js +4 -0
  9. package/dist/src/base/massive-body.d.ts +8 -0
  10. package/dist/src/base/massive-body.js +10 -0
  11. package/dist/src/config/effects.d.ts +14 -0
  12. package/dist/src/config/effects.js +14 -0
  13. package/dist/src/config/grid.d.ts +10 -0
  14. package/dist/src/config/grid.js +10 -0
  15. package/dist/src/config/panel.d.ts +26 -0
  16. package/dist/src/config/panel.js +26 -0
  17. package/dist/src/config/simulation.d.ts +21 -0
  18. package/dist/src/config/simulation.js +23 -0
  19. package/dist/src/controllers/clan-controller.d.ts +7 -0
  20. package/dist/src/controllers/clan-controller.js +12 -0
  21. package/dist/src/controllers/debug-controller.d.ts +45 -0
  22. package/dist/src/controllers/debug-controller.js +82 -0
  23. package/dist/src/controllers/effects-controller.d.ts +17 -0
  24. package/dist/src/controllers/effects-controller.js +71 -0
  25. package/dist/src/controllers/hover-controller.d.ts +11 -0
  26. package/dist/src/controllers/hover-controller.js +107 -0
  27. package/dist/src/controllers/layer-controller.d.ts +16 -0
  28. package/dist/src/controllers/layer-controller.js +64 -0
  29. package/dist/src/controllers/physics-controller.d.ts +14 -0
  30. package/dist/src/controllers/physics-controller.js +152 -0
  31. package/dist/src/controllers/star-controller.d.ts +12 -0
  32. package/dist/src/controllers/star-controller.js +38 -0
  33. package/dist/src/controllers/starship-controller.d.ts +17 -0
  34. package/dist/src/controllers/starship-controller.js +58 -0
  35. package/dist/src/draw-debug-line.d.ts +9 -0
  36. package/dist/src/draw-debug-line.js +29 -0
  37. package/dist/src/entities/black-hole-factory.d.ts +15 -0
  38. package/dist/src/entities/black-hole-factory.js +23 -0
  39. package/dist/src/entities/black-hole-shapes.d.ts +9 -0
  40. package/dist/src/entities/black-hole-shapes.js +224 -0
  41. package/dist/src/entities/black-hole.d.ts +69 -0
  42. package/dist/src/entities/black-hole.js +210 -0
  43. package/dist/src/entities/clan-manager.d.ts +12 -0
  44. package/dist/src/entities/clan-manager.js +22 -0
  45. package/dist/src/entities/clans.d.ts +15 -0
  46. package/dist/src/entities/clans.js +76 -0
  47. package/dist/src/entities/comet.d.ts +27 -0
  48. package/dist/src/entities/comet.js +81 -0
  49. package/dist/src/entities/docking-point.d.ts +20 -0
  50. package/dist/src/entities/docking-point.js +22 -0
  51. package/dist/src/entities/fleet.d.ts +45 -0
  52. package/dist/src/entities/fleet.js +374 -0
  53. package/dist/src/entities/formations.d.ts +51 -0
  54. package/dist/src/entities/formations.js +340 -0
  55. package/dist/src/entities/meteor.d.ts +26 -0
  56. package/dist/src/entities/meteor.js +48 -0
  57. package/dist/src/entities/nebula.d.ts +18 -0
  58. package/dist/src/entities/nebula.js +43 -0
  59. package/dist/src/entities/orbit.d.ts +23 -0
  60. package/dist/src/entities/orbit.js +43 -0
  61. package/dist/src/entities/pulsar.d.ts +18 -0
  62. package/dist/src/entities/pulsar.js +41 -0
  63. package/dist/src/entities/ring.d.ts +13 -0
  64. package/dist/src/entities/ring.js +26 -0
  65. package/dist/src/entities/ringed-planet.d.ts +21 -0
  66. package/dist/src/entities/ringed-planet.js +68 -0
  67. package/dist/src/entities/sector-grid.d.ts +16 -0
  68. package/dist/src/entities/sector-grid.js +70 -0
  69. package/dist/src/entities/star-factory.d.ts +29 -0
  70. package/dist/src/entities/star-factory.js +47 -0
  71. package/dist/src/entities/star.d.ts +48 -0
  72. package/dist/src/entities/star.js +167 -0
  73. package/dist/src/entities/starship-classes.d.ts +0 -0
  74. package/dist/src/entities/starship-classes.js +2 -0
  75. package/dist/src/entities/starship.d.ts +91 -0
  76. package/dist/src/entities/starship.js +760 -0
  77. package/dist/src/entities/supernova.d.ts +26 -0
  78. package/dist/src/entities/supernova.js +54 -0
  79. package/dist/src/index.d.ts +20 -0
  80. package/dist/src/index.js +19 -0
  81. package/dist/src/lib/energy-stream.d.ts +5 -0
  82. package/dist/src/lib/energy-stream.js +98 -0
  83. package/dist/src/lib/quadtree.d.ts +31 -0
  84. package/dist/src/lib/quadtree.js +124 -0
  85. package/dist/src/lib/simplified-stream.d.ts +6 -0
  86. package/dist/src/lib/simplified-stream.js +19 -0
  87. package/dist/src/types.d.ts +14 -0
  88. package/dist/src/types.js +1 -0
  89. package/dist/src/ui/black-holes-panel.d.ts +2 -0
  90. package/dist/src/ui/black-holes-panel.js +76 -0
  91. package/dist/src/ui/clans-panel.d.ts +2 -0
  92. package/dist/src/ui/clans-panel.js +20 -0
  93. package/dist/src/ui/debug-panel-controller.d.ts +41 -0
  94. package/dist/src/ui/debug-panel-controller.js +285 -0
  95. package/dist/src/ui/fleets-panel.d.ts +2 -0
  96. package/dist/src/ui/fleets-panel.js +127 -0
  97. package/dist/src/ui/formations-panel.d.ts +3 -0
  98. package/dist/src/ui/formations-panel.js +129 -0
  99. package/dist/src/ui/panel-config.d.ts +26 -0
  100. package/dist/src/ui/panel-config.js +26 -0
  101. package/dist/src/ui/ships-panel.d.ts +12 -0
  102. package/dist/src/ui/ships-panel.js +61 -0
  103. package/dist/src/ui/stars-panel.d.ts +2 -0
  104. package/dist/src/ui/stars-panel.js +120 -0
  105. package/dist/src/where-stars-drift.d.ts +71 -0
  106. package/dist/src/where-stars-drift.js +440 -0
  107. package/dist/tsconfig.tsbuildinfo +1 -0
  108. package/package.json +35 -0
  109. package/src/base/bounded-body.ts +14 -0
  110. package/src/base/celestial-body.ts +20 -0
  111. package/src/base/hoverable.ts +11 -0
  112. package/src/base/massive-body.ts +14 -0
  113. package/src/config/effects.ts +15 -0
  114. package/src/config/grid.ts +11 -0
  115. package/src/config/panel.ts +26 -0
  116. package/src/config/simulation.ts +25 -0
  117. package/src/controllers/clan-controller.ts +19 -0
  118. package/src/controllers/debug-controller.ts +112 -0
  119. package/src/controllers/effects-controller.ts +86 -0
  120. package/src/controllers/hover-controller.ts +128 -0
  121. package/src/controllers/layer-controller.ts +78 -0
  122. package/src/controllers/physics-controller.ts +173 -0
  123. package/src/controllers/star-controller.ts +51 -0
  124. package/src/controllers/starship-controller.ts +76 -0
  125. package/src/draw-debug-line.ts +37 -0
  126. package/src/entities/black-hole-factory.ts +28 -0
  127. package/src/entities/black-hole-shapes.ts +276 -0
  128. package/src/entities/black-hole.ts +246 -0
  129. package/src/entities/clan-manager.ts +33 -0
  130. package/src/entities/clans.ts +98 -0
  131. package/src/entities/comet.ts +102 -0
  132. package/src/entities/docking-point.ts +34 -0
  133. package/src/entities/fleet.ts +446 -0
  134. package/src/entities/formations.ts +423 -0
  135. package/src/entities/meteor.ts +59 -0
  136. package/src/entities/nebula.ts +50 -0
  137. package/src/entities/orbit.ts +53 -0
  138. package/src/entities/pulsar.ts +64 -0
  139. package/src/entities/ring.ts +42 -0
  140. package/src/entities/ringed-planet.ts +85 -0
  141. package/src/entities/sector-grid.ts +81 -0
  142. package/src/entities/star-factory.ts +59 -0
  143. package/src/entities/star.ts +222 -0
  144. package/src/entities/starship-classes.ts +1 -0
  145. package/src/entities/starship.ts +906 -0
  146. package/src/entities/supernova.ts +75 -0
  147. package/src/index.ts +24 -0
  148. package/src/lib/energy-stream.ts +127 -0
  149. package/src/lib/quadtree.ts +159 -0
  150. package/src/lib/simplified-stream.ts +28 -0
  151. package/src/types.ts +16 -0
  152. package/src/ui/black-holes-panel.ts +91 -0
  153. package/src/ui/clans-panel.ts +27 -0
  154. package/src/ui/debug-panel-controller.ts +339 -0
  155. package/src/ui/fleets-panel.ts +153 -0
  156. package/src/ui/formations-panel.ts +155 -0
  157. package/src/ui/panel-config.ts +26 -0
  158. package/src/ui/ships-panel.ts +85 -0
  159. package/src/ui/stars-panel.ts +146 -0
  160. package/src/where-stars-drift.ts +542 -0
@@ -0,0 +1,173 @@
1
+
2
+ import { Star } from '../entities/star';
3
+ import { BlackHole, BlackHle, BLACK_HOLE_CONFIG } from '../entities/black-hole';
4
+ import { Supernova } from '../entities/supernova';
5
+ import { Quadtree } from '../lib/quadtree';
6
+ import { DEBUG_CONFIG } from '../config/simulation';
7
+ import { DebugController } from './debug-controller';
8
+ import { BoundedBody } from '../base/bounded-body';
9
+
10
+ const GRAVITATIONAL_CONSTANT = 0.007;
11
+
12
+ class SearchArea extends BoundedBody {
13
+ constructor(x: number, y: number, radius: number) {
14
+ super(x, y, radius);
15
+ }
16
+ update() {}
17
+ draw() {}
18
+ }
19
+
20
+ export class PhysicsController {
21
+ private debugController: DebugController;
22
+
23
+ constructor(debugController: DebugController) {
24
+ this.debugController = debugController;
25
+ }
26
+
27
+ public update(blackHoles: BlackHle[], allStars: Star[], quadtree: Quadtree, width: number, height: number): Supernova[] {
28
+ const newSupernovas: Supernova[] = [];
29
+
30
+ this.applyGravity(blackHoles, quadtree);
31
+ this.handleStarConsumption(blackHoles, allStars, quadtree, newSupernovas, width, height);
32
+ this.handleBlackHoleCollisions(blackHoles, newSupernovas, width, height);
33
+
34
+ return newSupernovas;
35
+ }
36
+
37
+ private findSafeRespawnLocation(blackHoles: BlackHole[], width: number, height: number): { x: number, y: number } {
38
+ const SAFE_DISTANCE = 200;
39
+ let attempts = 0;
40
+ const MAX_ATTEMPTS = 10;
41
+
42
+ while (attempts < MAX_ATTEMPTS) {
43
+ const x = Math.random() * width;
44
+ const y = Math.random() * height;
45
+ let isSafe = true;
46
+
47
+ for (const hole of blackHoles) {
48
+ const dx = x - hole.x;
49
+ const dy = y - hole.y;
50
+ const distance = Math.sqrt(dx * dx + dy * dy);
51
+ if (distance < SAFE_DISTANCE) {
52
+ isSafe = false;
53
+ break;
54
+ }
55
+ }
56
+
57
+ if (isSafe) {
58
+ return { x, y };
59
+ }
60
+ attempts++;
61
+ }
62
+ return { x: Math.random() * width, y: Math.random() * height };
63
+ };
64
+
65
+ private applyGravity(blackHoles: BlackHole[], quadtree: Quadtree) {
66
+ for (const blackHole of blackHoles) {
67
+ const searchArea = new SearchArea(blackHole.x, blackHole.y, BLACK_HOLE_CONFIG.STAR_ATTRACTION_RADIUS);
68
+ const nearbyStars = quadtree.query(searchArea).filter(b => b instanceof Star) as Star[];
69
+
70
+ for (const star of nearbyStars) {
71
+ const dx = blackHole.x - star.x;
72
+ const dy = blackHole.y - star.y;
73
+ const distSq = dx * dx + dy * dy;
74
+
75
+ if (distSq < BLACK_HOLE_CONFIG.STAR_ATTRACTION_RADIUS * BLACK_HOLE_CONFIG.STAR_ATTRACTION_RADIUS) {
76
+ const dist = Math.sqrt(distSq);
77
+ const force = (GRAVITATIONAL_CONSTANT * blackHole.mass * star.mass) / (distSq + 1);
78
+ const acceleration = force / star.mass;
79
+
80
+ star.vx += (dx / dist) * acceleration;
81
+ star.vy += (dy / dist) * acceleration;
82
+ }
83
+ }
84
+ }
85
+ }
86
+
87
+ private handleStarConsumption(blackHoles: BlackHole[], allStars: Star[], quadtree: Quadtree, supernovas: Supernova[], width: number, height: number) {
88
+ for (const blackHole of blackHoles) {
89
+ const consumptionRadius = blackHole.radius + 5; // A little buffer
90
+ const searchArea = new SearchArea(blackHole.x, blackHole.y, consumptionRadius);
91
+ const nearbyStars = quadtree.query(searchArea).filter(b => b instanceof Star) as Star[];
92
+
93
+ for (const star of nearbyStars) {
94
+ if (star.isEatenBy(blackHole)) {
95
+ blackHole.absorb(star);
96
+ const { x: respawnX, y: respawnY } = this.findSafeRespawnLocation(blackHoles, width, height);
97
+ star.respawn(respawnX, respawnY);
98
+ supernovas.push(new Supernova(blackHole.x, blackHole.y, blackHole.radius, blackHole));
99
+ supernovas.push(new Supernova(respawnX, respawnY, 5));
100
+ }
101
+ }
102
+ }
103
+ }
104
+
105
+ private handleBlackHoleCollisions(blackHoles: BlackHle[], supernovas: Supernova[], width: number, height: number) {
106
+ if (blackHoles.length < 2) return;
107
+
108
+ // Reset attraction targets at the start of each frame
109
+ for (const bh of blackHoles) {
110
+ bh.attractionTargets = [];
111
+ }
112
+
113
+ for (let i = 0; i < blackHoles.length; i++) {
114
+ for (let j = i + 1; j < blackHoles.length; j++) {
115
+ const bh1 = blackHoles[i];
116
+ const bh2 = blackHoles[j];
117
+
118
+ const dx = bh2.x - bh1.x;
119
+ const dy = bh2.y - bh1.y;
120
+ const distSq = dx * dx + dy * dy;
121
+ const dist = Math.sqrt(distSq);
122
+
123
+ if (dist < BLACK_HOLE_CONFIG.ENERGY_STREAM_DISTANCE) {
124
+ // Register targets for stream rendering
125
+ bh1.attractionTargets.push(bh2);
126
+ bh2.attractionTargets.push(bh1);
127
+
128
+ // Register line for debug display
129
+ if (DEBUG_CONFIG.SHOW_DEBUG_INFO && DEBUG_CONFIG.SHOW_INTER_HOLE_DISTANCE) {
130
+ this.debugController.registerLine(bh1, bh2, { drawLine: false, showDistance: true });
131
+ }
132
+ }
133
+
134
+ if (dist < BLACK_HOLE_CONFIG.ATTRACTION_RADIUS) {
135
+ const force = (GRAVITATIONAL_CONSTANT * bh1.mass * bh2.mass) / (distSq + 1);
136
+ const acceleration1 = force / bh1.mass;
137
+ const acceleration2 = force / bh2.mass;
138
+
139
+ bh1.vx += (dx / dist) * acceleration1;
140
+ bh1.vy += (dy / dist) * acceleration1;
141
+ bh2.vx -= (dx / dist) * acceleration2;
142
+ bh2.vy -= (dy / dist) * acceleration2;
143
+ }
144
+
145
+ if (dist < bh1.radius + bh2.radius) {
146
+ const absorber = bh1.mass > bh2.mass ? bh1 : bh2;
147
+ const absorbed = bh1.mass > bh2.mass ? bh2 : bh1;
148
+
149
+ absorber.absorb(absorbed);
150
+ supernovas.push(new Supernova(absorber.x, absorber.y, absorber.radius * 1.5, absorber));
151
+
152
+ const side = Math.floor(Math.random() * 4);
153
+ let x, y;
154
+ const buffer = 50;
155
+
156
+ switch (side) {
157
+ case 0: x = Math.random() * width; y = -buffer; break;
158
+ case 1: x = width + buffer; y = Math.random() * height; break;
159
+ case 2: x = Math.random() * width; y = height + buffer; break;
160
+ default: x = -buffer; y = Math.random() * height; break;
161
+ }
162
+
163
+ const angleToCenter = Math.atan2(height / 2 - y, width / 2 - x);
164
+ const speed = 0.1;
165
+ const vx = Math.cos(angleToCenter) * speed;
166
+ const vy = Math.sin(angleToCenter) * speed;
167
+
168
+ absorbed.reset(x, y, vx, vy);
169
+ }
170
+ }
171
+ }
172
+ }
173
+ }
@@ -0,0 +1,51 @@
1
+
2
+ import { Star } from '../entities/star';
3
+ import { LayerController } from './layer-controller';
4
+ import { StarFactory } from '../entities/star-factory';
5
+ import { BlackHoleFactory } from '../entities/black-hole-factory';
6
+
7
+ export class StarController {
8
+ private layerController: LayerController;
9
+ private allStars: Star[] = [];
10
+ private starFactory: StarFactory;
11
+ private blackHoleFactory: BlackHoleFactory;
12
+
13
+ constructor(layerController: LayerController, starsCount: number, width: number, height: number) {
14
+ this.layerController = layerController;
15
+ this.starFactory = new StarFactory();
16
+ this.blackHoleFactory = new BlackHoleFactory();
17
+ this.createStars(starsCount, width, height);
18
+ this.createBlackHoles(width, height);
19
+ }
20
+
21
+ private createStars(starsCount: number, width: number, height: number) {
22
+ const staticStarsCount = Math.floor(starsCount * 0.7);
23
+ const interactiveStarsCount = starsCount - staticStarsCount;
24
+
25
+ for (let i = 0; i < staticStarsCount; i++) {
26
+ const star = this.starFactory.createRandomStar(Math.random() * width, Math.random() * height);
27
+ this.layerController.addBody('staticStars', star);
28
+ this.allStars.push(star);
29
+ }
30
+ for (let i = 0; i < interactiveStarsCount; i++) {
31
+ const star = this.starFactory.createRandomStar(Math.random() * width, Math.random() * height);
32
+ this.layerController.addBody('interactiveStars', star);
33
+ this.allStars.push(star);
34
+ }
35
+ }
36
+
37
+ private createBlackHoles(width: number, height: number) {
38
+ const numBlackHoles = 3; // Hardcoded for now
39
+ const padding = 100;
40
+
41
+ for (let i = 0; i < numBlackHoles; i++) {
42
+ const x = Math.random() * (width - padding * 2) + padding;
43
+ const y = Math.random() * (height - padding * 2) + padding;
44
+ this.layerController.addBody('blackHoles', this.blackHoleFactory.createRandomBlackHole(x, y));
45
+ }
46
+ }
47
+
48
+ public getAllStars(): Star[] {
49
+ return this.allStars;
50
+ }
51
+ }
@@ -0,0 +1,76 @@
1
+
2
+ import { Starship, STARSHIP_CONFIG } from '../entities/starship';
3
+ import { Fleet } from '../entities/fleet';
4
+ import { LayerController } from './layer-controller';
5
+ import { ClanController } from './clan-controller';
6
+ import { Star } from '../entities/star';
7
+ import { DebugController } from './debug-controller';
8
+ import type { Clan } from '../entities/clans';
9
+
10
+ export class StarshipController {
11
+ private layerController: LayerController;
12
+ private clanController: ClanController;
13
+ private debugController: DebugController;
14
+ private allStarships: Starship[] = [];
15
+
16
+ constructor(layerController: LayerController, width: number, height: number, debugController: DebugController) {
17
+ this.layerController = layerController;
18
+ this.clanController = new ClanController();
19
+ this.debugController = debugController;
20
+ this.createStarships(width, height);
21
+ }
22
+
23
+ private createStarships(width: number, height: number) {
24
+ for (let i = 0; i < STARSHIP_CONFIG.COUNT; i++) {
25
+ const starship = new Starship(Math.random() * width, Math.random() * height, this.debugController);
26
+ this.layerController.addBody('dynamicObjects', starship);
27
+ this.allStarships.push(starship);
28
+ }
29
+ }
30
+
31
+ public manageFleets(width: number, height: number) {
32
+ const desiredFleetSizes = [7, 3, 5, 4, 3, 3];
33
+ const fleets = this.layerController.getBodies('dynamicObjects').filter(b => b instanceof Fleet) as Fleet[];
34
+ const starships = this.allStarships;
35
+
36
+ // Disband fleets that have lost too many members
37
+ fleets.forEach(fleet => {
38
+ if (fleet.isActive && fleet.members.length < fleet.size * 0.5) {
39
+ fleet.disband();
40
+ }
41
+ });
42
+
43
+ desiredFleetSizes.forEach(size => {
44
+ const fleetExists = fleets.some(f => f.size === size && f.isActive);
45
+ if (!fleetExists) {
46
+ const availableShips = starships.filter(s => s.isAvailable());
47
+ if (availableShips.length >= size) {
48
+ const clan = this.clanController.getNextClan();
49
+ const fleetMembers = availableShips.slice(0, size);
50
+ this.layerController.addBody('dynamicObjects', new Fleet(fleetMembers, clan, width, height));
51
+ }
52
+ }
53
+ });
54
+ }
55
+
56
+ public manageStarshipTargets(allStars: Star[]) {
57
+ const idleShips = this.allStarships.filter(ship => ship.state === 'IDLE' && ship.stateTimer <= 0);
58
+
59
+ if (idleShips.length === 0) return;
60
+
61
+ const starsWithDocks = allStars.filter(s => s.dockingPoints.length > 0);
62
+ if (starsWithDocks.length === 0) return;
63
+
64
+ for (const ship of idleShips) {
65
+ ship.findNewTarget(starsWithDocks);
66
+ }
67
+ }
68
+
69
+ public getAllStarships(): Starship[] {
70
+ return this.allStarships;
71
+ }
72
+
73
+ public getAllClans(): Clan[] {
74
+ return this.clanController.getClans();
75
+ }
76
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * @fileoverview Defines a utility function for drawing a debug line with distance.
3
+ */
4
+
5
+ interface Point {
6
+ x: number;
7
+ y: number;
8
+ }
9
+
10
+ export function drawDebugLineWithDistance(ctx: CanvasRenderingContext2D, source: Point, target: Point, drawLine: boolean, showDistance: boolean) {
11
+ const dx = target.x - source.x;
12
+ const dy = target.y - source.y;
13
+ const dist = Math.sqrt(dx * dx + dy * dy);
14
+
15
+ // Draw the dashed line if requested
16
+ if (drawLine) {
17
+ ctx.beginPath();
18
+ ctx.setLineDash([2, 4]);
19
+ ctx.moveTo(source.x, source.y);
20
+ ctx.lineTo(target.x, target.y);
21
+ ctx.strokeStyle = 'rgba(255, 255, 255, 0.2)';
22
+ ctx.lineWidth = 0.5;
23
+ ctx.stroke();
24
+ ctx.setLineDash([]);
25
+ }
26
+
27
+ // Draw the distance text if requested
28
+ if (showDistance) {
29
+ const midX = source.x + dx / 2;
30
+ const midY = source.y + dy / 2;
31
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.4)';
32
+ ctx.font = '10px "Roboto Mono", monospace';
33
+ ctx.textAlign = 'center';
34
+ ctx.textBaseline = 'bottom';
35
+ ctx.fillText(dist.toFixed(0), midX, midY - 2);
36
+ }
37
+ }
@@ -0,0 +1,28 @@
1
+
2
+ import { BlackHole, BlackHoleType } from './black-hole';
3
+
4
+ const BLACK_HOLE_TYPES: BlackHoleType[] = ['ACCRETION_DISK', 'VORTEX', 'LENSING', 'WARPED_DISK', 'STANDARD'];
5
+
6
+ export class BlackHoleFactory {
7
+ /**
8
+ * Creates a black hole of a random type.
9
+ */
10
+ createRandomBlackHole(x: number, y: number): BlackHole {
11
+ const randomType = BLACK_HOLE_TYPES[Math.floor(Math.random() * BLACK_HOLE_TYPES.length)];
12
+ return new BlackHole(x, y, randomType);
13
+ }
14
+
15
+ /**
16
+ * Creates a black hole of a specific type.
17
+ */
18
+ createBlackHole(x: number, y: number, type: BlackHoleType): BlackHole {
19
+ return new BlackHole(x, y, type);
20
+ }
21
+
22
+ /**
23
+ * Gets an array of all possible black hole types.
24
+ */
25
+ getAllBlackHoleTypes(): BlackHoleType[] {
26
+ return [...BLACK_HOLE_TYPES];
27
+ }
28
+ }
@@ -0,0 +1,276 @@
1
+ /**
2
+ * @fileoverview Defines all black hole shape rendering functions.
3
+ */
4
+
5
+ import type { BlackHle } from './black-hole';
6
+ import { BLACK_HOLE_CONFIG } from './black-hole';
7
+
8
+ export const drawStandardShape = (ctx: CanvasRenderingContext2D, hole: BlackHle) => {
9
+ // A simple white glow from behind
10
+ const ringRadius = hole.radius * 1.5;
11
+
12
+ ctx.save();
13
+ ctx.shadowColor = `rgba(255, 255, 255, 0.7)`;
14
+ ctx.shadowBlur = 20;
15
+
16
+ // Draw the black hole itself, the shadow will create the glow
17
+ ctx.beginPath();
18
+ ctx.fillStyle = hole.color;
19
+ ctx.arc(hole.x, hole.y, hole.radius, 0, Math.PI * 2);
20
+ ctx.fill();
21
+
22
+ ctx.restore();
23
+ };
24
+
25
+ export const drawWarpedDiskShape = (ctx: CanvasRenderingContext2D, hole: BlackHle) => {
26
+ const config = BLACK_HOLE_CONFIG.WARPED_DISK;
27
+ const diskRadius = hole.radius * 2.5;
28
+
29
+ // 1. Draw outer, faint, warped lines
30
+ ctx.save();
31
+ ctx.translate(hole.x, hole.y);
32
+ ctx.globalCompositeOperation = 'lighter';
33
+
34
+ for (let i = 0; i < config.WARPED_LINE_COUNT; i++) {
35
+ const progress = i / config.WARPED_LINE_COUNT;
36
+ const startRadius = diskRadius * (1.5 + progress * 2);
37
+ const endRadius = diskRadius * (1.1 - progress * 0.2);
38
+ const startAngle = hole.warpAngle + progress * Math.PI * 0.4;
39
+ const endAngle = hole.warpAngle + Math.PI * 0.2 + progress * Math.PI * 0.8;
40
+
41
+ const startX = Math.cos(startAngle) * startRadius;
42
+ const startY = Math.sin(startAngle) * startRadius;
43
+ const endX = Math.cos(endAngle) * endRadius;
44
+ const endY = Math.sin(endAngle) * endRadius;
45
+
46
+ const cp1x = Math.cos(startAngle + 0.5) * startRadius * 0.8;
47
+ const cp1y = Math.sin(startAngle + 0.5) * startRadius * 0.8;
48
+ const cp2x = Math.cos(endAngle - 0.5) * endRadius * 1.2;
49
+ const cp2y = Math.sin(endAngle - 0.5) * endRadius * 1.2;
50
+
51
+ ctx.beginPath();
52
+ ctx.moveTo(startX, startY);
53
+ ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, endX, endY);
54
+ ctx.strokeStyle = `rgba(255, 200, 150, ${0.05 + progress * 0.1})`;
55
+ ctx.lineWidth = 0.5 + progress * 1.0;
56
+ ctx.stroke();
57
+ }
58
+ ctx.restore();
59
+
60
+ // 2. Draw main accretion disk
61
+ ctx.save();
62
+ ctx.translate(hole.x, hole.y);
63
+
64
+ const gradient = ctx.createRadialGradient(0, 0, hole.radius, 0, 0, diskRadius);
65
+ gradient.addColorStop(0.3, config.DISK_COLOR_INNER);
66
+ gradient.addColorStop(0.6, config.DISK_COLOR_OUTER);
67
+ gradient.addColorStop(1, 'rgba(255, 150, 0, 0)');
68
+
69
+ ctx.strokeStyle = gradient;
70
+ ctx.lineWidth = hole.radius * 1.5;
71
+ ctx.shadowColor = config.DISK_COLOR_INNER;
72
+ ctx.shadowBlur = 30;
73
+
74
+ ctx.beginPath();
75
+ ctx.arc(0, 0, diskRadius, 0, Math.PI * 2);
76
+ ctx.stroke();
77
+
78
+ ctx.restore();
79
+
80
+ // 3. Draw the black hole itself
81
+ ctx.beginPath();
82
+ ctx.fillStyle = hole.color;
83
+ ctx.arc(hole.x, hole.y, hole.radius, 0, Math.PI * 2);
84
+ ctx.fill();
85
+
86
+ // 4. Draw the diagonal "cut" which creates the 3D effect
87
+ ctx.save();
88
+ ctx.translate(hole.x, hole.y);
89
+ ctx.fillStyle = 'rgba(0,0,0,1)';
90
+ ctx.shadowColor = 'black';
91
+ ctx.shadowBlur = 5;
92
+ ctx.fillRect(-diskRadius * 1.5, -hole.radius * 0.1, diskRadius * 3, hole.radius * 0.2);
93
+ ctx.restore();
94
+
95
+ ctx.shadowBlur = 0;
96
+ };
97
+
98
+ export const drawLensingShape = (ctx: CanvasRenderingContext2D, hole: BlackHle) => {
99
+ // Gravitational Lensing Effect
100
+ const ringRadius = hole.radius * 1.5;
101
+
102
+ ctx.save();
103
+ ctx.shadowColor = `rgba(255, 255, 255, 0.5)`;
104
+ ctx.shadowBlur = 50;
105
+
106
+ ctx.beginPath();
107
+ ctx.strokeStyle = `rgba(255, 255, 255, 0.9)`;
108
+ ctx.lineWidth = hole.radius * 0.4;
109
+ ctx.arc(hole.x, hole.y, ringRadius, 0, Math.PI * 2);
110
+ ctx.stroke();
111
+
112
+ ctx.restore();
113
+
114
+ // Draw the black hole itself
115
+ ctx.beginPath();
116
+ ctx.fillStyle = hole.color;
117
+ ctx.arc(hole.x, hole.y, hole.radius, 0, Math.PI * 2);
118
+ ctx.fill();
119
+ };
120
+
121
+ export const drawAccretionDiskShape = (ctx: CanvasRenderingContext2D, hole: BlackHle) => {
122
+ const diskRadius = hole.radius * 2.5;
123
+ const glowRadius = diskRadius * 0.6;
124
+ const config = BLACK_HOLE_CONFIG.ACCRETION_DISK;
125
+
126
+ // --- Draw Relativistic Jets ---
127
+ ctx.save();
128
+ ctx.translate(hole.x, hole.y);
129
+ ctx.rotate(hole.accretionDiskAngle);
130
+ ctx.globalCompositeOperation = 'lighter';
131
+
132
+ // Animate the nozzle length only every 5 frames for performance
133
+ if (hole.frameCount % 5 === 0) {
134
+ const maxNozzleLength = diskRadius * 1.1;
135
+ // nozzleLengthMultiplier will be between 0.5 (50%) and 1.0 (100%)
136
+ const nozzleLengthMultiplier = Math.random() * 0.5 + 0.5;
137
+ hole.nozzleLength = maxNozzleLength * nozzleLengthMultiplier;
138
+ }
139
+
140
+ // Always draw the jets using the cached nozzleLength
141
+ if (hole.nozzleLength > 0) {
142
+ ctx.shadowColor = 'white';
143
+ ctx.shadowBlur = 15;
144
+ const nozzleBaseWidth = hole.radius * 2; // Base is 2x the hole's radius
145
+ const jetColor = `rgba(200, 220, 255, ${0.1 + (hole.nozzleLength / (diskRadius * 1.1)) * 0.3})`;
146
+ ctx.fillStyle = jetColor;
147
+
148
+ // Jet 1 (positive x direction)
149
+ ctx.beginPath();
150
+ ctx.moveTo(hole.nozzleLength, 0); // Tip of the jet
151
+ ctx.lineTo(0, nozzleBaseWidth / 2); // Base centered at the hole
152
+ ctx.lineTo(0, -nozzleBaseWidth / 2); // Base centered at the hole
153
+ ctx.closePath();
154
+ ctx.fill();
155
+
156
+ // Jet 2 (negative x direction)
157
+ ctx.beginPath();
158
+ ctx.moveTo(-hole.nozzleLength, 0); // Tip of the jet
159
+ ctx.lineTo(0, nozzleBaseWidth / 2); // Base centered at the hole
160
+ ctx.lineTo(0, -nozzleBaseWidth / 2); // Base centered at the hole
161
+ ctx.closePath();
162
+ ctx.fill();
163
+ }
164
+
165
+ ctx.restore();
166
+
167
+
168
+ // 1. Draw the outer glow
169
+ ctx.save();
170
+ ctx.translate(hole.x, hole.y);
171
+ const glowGradient = ctx.createRadialGradient(0, 0, hole.radius, 0, 0, glowRadius);
172
+ glowGradient.addColorStop(0.2, config.GLOW_COLOR);
173
+ glowGradient.addColorStop(1, 'rgba(180, 210, 255, 0)');
174
+ ctx.fillStyle = glowGradient;
175
+ ctx.shadowColor = config.GLOW_COLOR;
176
+ ctx.shadowBlur = 30;
177
+ ctx.beginPath();
178
+ ctx.arc(0, 0, glowRadius, 0, Math.PI * 2);
179
+ ctx.fill();
180
+ ctx.restore();
181
+
182
+ // 2. Draw the black hole event horizon
183
+ ctx.beginPath();
184
+ ctx.fillStyle = hole.color;
185
+ ctx.arc(hole.x, hole.y, hole.radius, 0, Math.PI * 2);
186
+ ctx.fill();
187
+
188
+ // 3. Draw the flat white line crossing the front
189
+ ctx.save();
190
+ ctx.translate(hole.x, hole.y);
191
+ ctx.rotate(hole.accretionDiskAngle);
192
+
193
+ const lineGradient = ctx.createLinearGradient(-diskRadius * 1.1, 0, diskRadius * 1.1, 0);
194
+ lineGradient.addColorStop(0, 'rgba(255, 255, 255, 0)');
195
+ lineGradient.addColorStop(0.25, 'rgba(255, 255, 255, 0.95)');
196
+ lineGradient.addColorStop(0.75, 'rgba(255, 255, 255, 0.95)');
197
+ lineGradient.addColorStop(1, 'rgba(255, 255, 255, 0)');
198
+
199
+ ctx.fillStyle = lineGradient;
200
+ ctx.shadowColor = 'white';
201
+ ctx.shadowBlur = 15;
202
+ const lineHeight = hole.radius * 0.35;
203
+ ctx.fillRect(-diskRadius * 1.1, -lineHeight / 2, diskRadius * 2.2, lineHeight);
204
+
205
+ ctx.restore();
206
+
207
+ ctx.shadowBlur = 0;
208
+ };
209
+
210
+
211
+ export const drawVortexShape = (ctx: CanvasRenderingContext2D, hole: BlackHle) => {
212
+ const config = BLACK_HOLE_CONFIG.VORTEX;
213
+ const outerRadius = hole.radius * 5;
214
+
215
+ // 1. Draw the vast, faint outer glow
216
+ ctx.save();
217
+ ctx.translate(hole.x, hole.y);
218
+ const outerGradient = ctx.createRadialGradient(0, 0, hole.radius, 0, 0, outerRadius);
219
+ outerGradient.addColorStop(0.3, config.VORTEX_COLOR_MID);
220
+ outerGradient.addColorStop(1, config.VORTEX_COLOR_OUTER);
221
+ ctx.fillStyle = outerGradient;
222
+ ctx.beginPath();
223
+ ctx.arc(0, 0, outerRadius, 0, Math.PI * 2);
224
+ ctx.fill();
225
+ ctx.restore();
226
+
227
+ // 2. Draw the swirling, fiery arms
228
+ ctx.save();
229
+ ctx.translate(hole.x, hole.y);
230
+ ctx.globalCompositeOperation = 'lighter';
231
+
232
+ for (let i = 0; i < config.PARTICLE_COUNT; i++) {
233
+ const progress = i / config.PARTICLE_COUNT;
234
+ const angle = hole.vortexAngle + progress * Math.PI * 2;
235
+
236
+ const startRadius = hole.radius * (1.2 + progress * 1.5);
237
+ const endRadius = hole.radius * (1.0 + progress * 1.0);
238
+
239
+ const flameColor = i % 2 === 0 ? `rgba(255, 220, 150, 0.15)` : `rgba(255, 180, 50, 0.1)`;
240
+
241
+ ctx.beginPath();
242
+ ctx.arc(0, 0, startRadius, angle, angle + Math.PI * 0.8);
243
+ ctx.arc(0, 0, endRadius, angle + Math.PI * 0.8, angle, true);
244
+ ctx.closePath();
245
+
246
+ ctx.fillStyle = flameColor;
247
+ ctx.shadowColor = 'rgba(255, 150, 0, 0.5)';
248
+ ctx.shadowBlur = 45;
249
+ ctx.fill();
250
+ }
251
+ ctx.restore();
252
+
253
+ // 3. Draw the bright inner ring
254
+ ctx.save();
255
+ ctx.translate(hole.x, hole.y);
256
+ const innerRingRadius = hole.radius * 1.2;
257
+ const innerGradient = ctx.createRadialGradient(0, 0, 0, 0, 0, innerRingRadius);
258
+ innerGradient.addColorStop(0, 'rgba(255, 255, 255, 0.9)');
259
+ innerGradient.addColorStop(0.7, config.VORTEX_COLOR_INNER);
260
+ innerGradient.addColorStop(1, 'rgba(255, 200, 0, 0)');
261
+
262
+ ctx.fillStyle = innerGradient;
263
+ ctx.shadowColor = 'rgba(255, 255, 255, 0.7)';
264
+ ctx.shadowBlur = 20;
265
+ ctx.beginPath();
266
+ ctx.arc(0, 0, innerRingRadius, 0, Math.PI * 2);
267
+ ctx.fill();
268
+ ctx.restore();
269
+
270
+ // 4. Draw the central black hole
271
+ ctx.beginPath();
272
+ ctx.fillStyle = hole.color;
273
+ ctx.arc(hole.x, hole.y, hole.radius, 0, Math.PI * 2);
274
+ ctx.fill();
275
+ ctx.shadowBlur = 0;
276
+ };