@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,423 @@
1
+ /**
2
+ * @fileoverview Defines all fleet formations and a registry to manage them.
3
+ */
4
+ import type { Starship, ShipClass } from './starship';
5
+
6
+ // --- Base Formation Interfaces ---
7
+
8
+ type FormationCategory = 'Military' | 'Mixed';
9
+
10
+ /**
11
+ * Common properties for all formations.
12
+ */
13
+ interface IFormation {
14
+ name: string;
15
+ category: FormationCategory;
16
+ description: string;
17
+ // The function that calculates and returns target positions for each ship.
18
+ getPosition: (ships: Starship[]) => { x: number, y: number }[];
19
+ }
20
+
21
+ /**
22
+ * Strict formations are role-dependent and class-specific.
23
+ * They require a specific composition of ship classes to be effective.
24
+ */
25
+ export interface StrictFormation extends IFormation {
26
+ type: 'Strict';
27
+ // Defines the exact ship classes and counts required for this formation.
28
+ composition: { shipClassName: string; count: number }[];
29
+ }
30
+
31
+ /**
32
+ * Free formations are geometry-based and adaptable to any ship composition.
33
+ * They prioritize spatial arrangement over specific ship roles.
34
+ */
35
+ export interface FreeFormation extends IFormation {
36
+ type: 'Free';
37
+ }
38
+
39
+ export type Formation = StrictFormation | FreeFormation;
40
+
41
+ // --- Formation Geometry Functions ---
42
+
43
+ const getStaggeredColumnPosition = (ships: Starship[]): { x: number, y: number }[] => {
44
+ const offsets = [];
45
+ const numShips = ships.length;
46
+ const spacingY = 7.5; // Vertical spacing between ships
47
+ const spacingX = 5; // Horizontal zigzag spacing
48
+
49
+ // Calculate total height of the formation
50
+ const totalHeight = (numShips - 1) * spacingY;
51
+ const startY = -totalHeight / 2;
52
+
53
+ for (let i = 0; i < numShips; i++) {
54
+ const x = (i % 2 === 0 ? -1 : 1) * spacingX;
55
+ const y = startY + i * spacingY;
56
+ offsets.push({ x, y });
57
+ }
58
+ return offsets;
59
+ };
60
+
61
+ const getBoxPosition = (ships: Starship[]): { x: number, y: number }[] => {
62
+ const numShips = ships.length;
63
+ const sideLength = Math.ceil(Math.sqrt(numShips));
64
+ const spacing = 10;
65
+ const offsets = [];
66
+
67
+ for (let i = 0; i < numShips; i++) {
68
+ const row = Math.floor(i / sideLength);
69
+ const col = i % sideLength;
70
+ const x = (col - (sideLength - 1) / 2) * spacing;
71
+ const y = (row - (sideLength - 1) / 2) * spacing;
72
+ offsets.push({ x, y });
73
+ }
74
+ return offsets;
75
+ }
76
+
77
+ const getVPointPosition = (ships: Starship[]): { x: number, y: number }[] => {
78
+ // This is a complex strict formation. We'll sort ships by their role from the composition.
79
+ const dreadnought = ships.filter(s => s.shipClass.name === 'Dreadnought');
80
+ const battleships = ships.filter(s => s.shipClass.name === 'Battleship');
81
+ const heavyCruisers = ships.filter(s => s.shipClass.name === 'Heavy Cruiser');
82
+ const destroyers = ships.filter(s => s.shipClass.name === 'Destroyer');
83
+
84
+ const sortedShips = [...dreadnought, ...battleships, ...heavyCruisers, ...destroyers];
85
+ const offsets: { x: number; y: number }[] = [];
86
+
87
+ // 1 Dreadnought at the point
88
+ if (dreadnought.length > 0) offsets.push({ x: 0, y: -20 });
89
+
90
+ // 4 Battleships in a smaller V
91
+ for(let i=0; i<battleships.length; i++) {
92
+ const side = i % 2 === 0 ? -1 : 1;
93
+ const rank = Math.floor(i/2) + 1;
94
+ offsets.push({ x: side * rank * 7.5, y: -20 + rank * 7.5 });
95
+ }
96
+
97
+ // 8 Heavy Cruisers flanking
98
+ for(let i=0; i<heavyCruisers.length; i++) {
99
+ const side = i % 2 === 0 ? -1 : 1;
100
+ const rank = Math.floor(i/2) + 1;
101
+ offsets.push({ x: side * (15 + rank * 6), y: -12.5 + rank * 6 });
102
+ }
103
+
104
+ // 12 Destroyers screening
105
+ for(let i=0; i<destroyers.length; i++) {
106
+ const side = i % 2 === 0 ? -1 : 1;
107
+ const rank = Math.floor(i/2) + 1;
108
+ offsets.push({ x: side * (30 + rank * 4), y: -7.5 + rank * 4 });
109
+ }
110
+
111
+ // This logic needs to map offsets back to original ships, so we return offsets for the sorted list.
112
+ return offsets;
113
+ };
114
+
115
+ const getSpherePosition = (ships: Starship[]): { x: number, y: number }[] => {
116
+ const militaryShips = ships.filter(s => ['Capital', 'Cruiser', 'Escort', 'Small Craft'].includes(s.shipClass.category));
117
+ const civilianShips = ships.filter(s => !militaryShips.includes(s));
118
+
119
+ const offsets: { x: number, y: number }[] = [];
120
+ const radius = 8 + militaryShips.length * 1.25;
121
+
122
+ // Place military ships in a circle
123
+ militaryShips.forEach((ship, i) => {
124
+ const angle = (i / militaryShips.length) * 2 * Math.PI;
125
+ offsets.push({ x: Math.cos(angle) * radius, y: Math.sin(angle) * radius });
126
+ });
127
+
128
+ // Place civilian ships in the center
129
+ civilianShips.forEach((ship, i) => {
130
+ const angle = (i / civilianShips.length) * 2 * Math.PI;
131
+ offsets.push({ x: Math.cos(angle) * (radius * 0.25), y: Math.sin(angle) * (radius * 0.25) });
132
+ });
133
+
134
+ return offsets;
135
+ };
136
+
137
+ // --- Formation Definitions ---
138
+
139
+ const FORMATIONS: Formation[] = [
140
+ // --- Military Formations (Strict) ---
141
+ {
142
+ name: 'Dreadnought V-Point',
143
+ category: 'Military',
144
+ type: 'Strict',
145
+ description: 'A massive Dreadnought leads a wedge of 4 Battleships. 8 Heavy Cruisers flank the sides to provide broadside support, while a screen of 12 Destroyers prevents flanking.',
146
+ composition: [
147
+ { shipClassName: 'Dreadnought', count: 1 },
148
+ { shipClassName: 'Battleship', count: 4 },
149
+ { shipClassName: 'Heavy Cruiser', count: 8 },
150
+ { shipClassName: 'Destroyer', count: 12 },
151
+ ],
152
+ getPosition: getVPointPosition,
153
+ },
154
+ {
155
+ name: 'Carrier Strike Nest',
156
+ category: 'Military',
157
+ type: 'Strict',
158
+ description: 'Two Carriers are positioned at the center of a sphere formed by 6 Light Cruisers. 20 Corvettes orbit the carriers in a high-speed "swarm" to intercept incoming torpedoes.',
159
+ composition: [
160
+ { shipClassName: 'Carrier', count: 2 },
161
+ { shipClassName: 'Light Cruiser', count: 6 },
162
+ { shipClassName: 'Corvette', count: 20 },
163
+ ],
164
+ getPosition: getStaggeredColumnPosition,
165
+ },
166
+ {
167
+ name: 'Battlecruiser Intercept Row',
168
+ category: 'Military',
169
+ type: 'Strict',
170
+ description: 'A fast-moving line of 6 Battlecruisers designed for "hit and run." They are supported by 4 Scout ships that stay ahead to provide targeting data for long-range spinal mounts.',
171
+ composition: [
172
+ { shipClassName: 'Battlecruiser', count: 6 },
173
+ { shipClassName: 'Pathfinder', count: 4 }, // Assuming Scout = Pathfinder
174
+ ],
175
+ getPosition: getStaggeredColumnPosition,
176
+ },
177
+ {
178
+ name: 'Assault Picket Line',
179
+ category: 'Military',
180
+ type: 'Strict',
181
+ description: '15 Frigates and 10 Destroyers are spaced in a wide, flat plane. This formation is used to block an entire orbital corridor, relying on overlapping sensor ranges.',
182
+ composition: [
183
+ { shipClassName: 'Frigate', count: 15 },
184
+ { shipClassName: 'Destroyer', count: 10 },
185
+ ],
186
+ getPosition: getStaggeredColumnPosition,
187
+ },
188
+ {
189
+ name: 'Dreadnought Brawler Pair',
190
+ category: 'Military',
191
+ type: 'Strict',
192
+ description: 'Two Dreadnoughts fly hull-to-hull, linking their heavy shields. They are screened by a tight group of 4 Heavy Cruisers to protect their rear engines.',
193
+ composition: [
194
+ { shipClassName: 'Dreadnought', count: 2 },
195
+ { shipClassName: 'Heavy Cruiser', count: 4 },
196
+ ],
197
+ getPosition: getStaggeredColumnPosition,
198
+ },
199
+
200
+ // --- Military Formations (Free) ---
201
+ {
202
+ name: 'The Box',
203
+ category: 'Military',
204
+ type: 'Free',
205
+ description: 'Ships are arranged in a 3D cube. Any mix of heavy and light ships can fill the corners, creating a multi-directional fire zone.',
206
+ getPosition: getBoxPosition,
207
+ },
208
+ {
209
+ name: 'The Double Echelon',
210
+ category: 'Military',
211
+ type: 'Free',
212
+ description: 'Ships are arranged in two parallel diagonal lines. This allows any set of ships to retreat in sequence while the next line covers them.',
213
+ getPosition: getStaggeredColumnPosition,
214
+ },
215
+ {
216
+ name: 'The Orbital Ring',
217
+ category: 'Military',
218
+ type: 'Free',
219
+ description: 'Ships maintain a circular orbit around a fixed point (like a station). Can be held by as few as 3 ships or as many as 100.',
220
+ getPosition: getStaggeredColumnPosition,
221
+ },
222
+ {
223
+ name: 'The Staggered Column',
224
+ category: 'Military',
225
+ type: 'Free',
226
+ description: 'Ships follow each other in a zigzag vertical line. This hides the true number of ships from forward sensors.',
227
+ getPosition: getStaggeredColumnPosition,
228
+ },
229
+ {
230
+ name: 'The Dispersed Cloud',
231
+ category: 'Military',
232
+ type: 'Free',
233
+ description: 'Ships maintain maximum sensor range from one another. This "free" formation prevents a single nuclear or area-of-effect weapon from hitting more than one vessel.',
234
+ getPosition: getStaggeredColumnPosition,
235
+ },
236
+
237
+ // --- Mixed Formations (Strict) ---
238
+ {
239
+ name: 'Colony Vanguard',
240
+ category: 'Mixed',
241
+ type: 'Strict',
242
+ description: 'A Colony Ship at the center, guarded by 2 Battleships and 4 Frigates. 2 Surveyors lead the formation to scan for habitable zones.',
243
+ composition: [
244
+ { shipClassName: 'Colony Ship', count: 1 },
245
+ { shipClassName: 'Battleship', count: 2 },
246
+ { shipClassName: 'Frigate', count: 4 },
247
+ { shipClassName: 'Explorer', count: 2 }, // Assuming Surveyor = Explorer
248
+ ],
249
+ getPosition: getStaggeredColumnPosition,
250
+ },
251
+ {
252
+ name: 'The Refueling Phalanx',
253
+ category: 'Mixed',
254
+ type: 'Strict',
255
+ description: 'A Tanker is positioned directly behind a Dreadnought for protection. 4 Destroyers circle the pair to prevent "engine-sniping" by enemy fighters.',
256
+ composition: [
257
+ { shipClassName: 'Hauler', count: 1 }, // Assuming Tanker = Hauler
258
+ { shipClassName: 'Dreadnought', count: 1 },
259
+ { shipClassName: 'Destroyer', count: 4 },
260
+ ],
261
+ getPosition: getStaggeredColumnPosition,
262
+ },
263
+ {
264
+ name: 'Extraction Screen',
265
+ category: 'Mixed',
266
+ type: 'Strict',
267
+ description: '5 Asteroid Miners work a field while 2 Monitors provide stationary defense. 4 Corvettes "patrol" the gaps between the miners.',
268
+ composition: [
269
+ { shipClassName: 'Mining Vessel', count: 5 },
270
+ { shipClassName: 'Monitor', count: 2 },
271
+ { shipClassName: 'Corvette', count: 4 },
272
+ ],
273
+ getPosition: getStaggeredColumnPosition,
274
+ },
275
+ {
276
+ name: 'Diplomatic Escort',
277
+ category: 'Mixed',
278
+ type: 'Strict',
279
+ description: 'A Diplomatic Courier is surrounded by 4 Light Cruisers in a diamond formation. A Hospital Ship follows at the rear in case of an ambush.',
280
+ composition: [
281
+ { shipClassName: 'Diplomatic Courier', count: 1 },
282
+ { shipClassName: 'Light Cruiser', count: 4 },
283
+ { shipClassName: 'Hospital Ship', count: 1 },
284
+ ],
285
+ getPosition: getStaggeredColumnPosition,
286
+ },
287
+ {
288
+ name: 'Salvage Sweep',
289
+ category: 'Mixed',
290
+ type: 'Strict',
291
+ description: 'Two Salvage Ships lead, followed by a Tender to store parts. A Heavy Cruiser provides long-range overwatch from a higher orbital plane.',
292
+ composition: [
293
+ { shipClassName: 'Salvage Ship', count: 2 },
294
+ { shipClassName: 'Hauler', count: 1 }, // Assuming Tender = Hauler
295
+ { shipClassName: 'Heavy Cruiser', count: 1 },
296
+ ],
297
+ getPosition: getStaggeredColumnPosition,
298
+ },
299
+
300
+ // --- Mixed Formations (Free) ---
301
+ {
302
+ name: 'The Sphere (Bubble)',
303
+ category: 'Mixed',
304
+ type: 'Free',
305
+ description: 'Civilian ships (like Haulers) stay at the dead center while any available military ships form a protective globe around them.',
306
+ getPosition: getSpherePosition,
307
+ },
308
+ {
309
+ name: 'The Conical Tail',
310
+ category: 'Mixed',
311
+ type: 'Free',
312
+ description: 'A large military ship (like a Battleship) leads, with peaceful ships (like Freighters) following in its "sensor shadow" to remain undetected.',
313
+ getPosition: getStaggeredColumnPosition,
314
+ },
315
+ {
316
+ name: 'The Hub and Spoke',
317
+ category: 'Mixed',
318
+ type: 'Free',
319
+ description: 'A large ship or station acts as the "hub," with smaller ships (Shuttles, Pathfinders) flying back and forth along set "spokes."',
320
+ getPosition: getStaggeredColumnPosition,
321
+ },
322
+ {
323
+ name: 'The Anchor Point',
324
+ category: 'Mixed',
325
+ type: 'Free',
326
+ description: 'One heavy ship remains stationary while all other ships (peaceful or military) orbit it in an erratic, unpredictable pattern to confuse attackers.',
327
+ getPosition: getStaggeredColumnPosition,
328
+ },
329
+ {
330
+ name: 'The Lateral Wall',
331
+ category: 'Mixed',
332
+ type: 'Free',
333
+ description: 'All ships, regardless of class, align on a single flat horizontal plane. This maximizes the fleet\'s ability to scan a large area of space simultaneously.',
334
+ getPosition: getStaggeredColumnPosition,
335
+ },
336
+ ];
337
+
338
+ // --- Formation Registry ---
339
+
340
+ class FormationRegistry {
341
+ private formations: Formation[];
342
+ private strictFormations: StrictFormation[];
343
+ private freeFormations: FreeFormation[];
344
+
345
+ constructor() {
346
+ this.formations = FORMATIONS;
347
+ this.strictFormations = FORMATIONS.filter(f => f.type === 'Strict') as StrictFormation[];
348
+ this.freeFormations = FORMATIONS.filter(f => f.type === 'Free') as FreeFormation[];
349
+ }
350
+
351
+ /**
352
+ * Finds a suitable formation for a given fleet based on its composition.
353
+ * @param fleetShips The list of starships in the fleet.
354
+ * @returns A formation, or null if no suitable formation is found.
355
+ */
356
+ public getFormationForFleet(fleetShips: Starship[]): Formation | null {
357
+ // 1. Get the composition of the current fleet.
358
+ const fleetComposition = new Map<string, number>();
359
+ fleetShips.forEach(ship => {
360
+ const name = ship.shipClass.name;
361
+ fleetComposition.set(name, (fleetComposition.get(name) || 0) + 1);
362
+ });
363
+
364
+ // 2. Try to find a matching strict formation.
365
+ const matchingStrictFormation = this.strictFormations.find(formation => {
366
+ // Check if the fleet has the exact number of ships required.
367
+ const totalShipsInFormation = formation.composition.reduce((sum, role) => sum + role.count, 0);
368
+ if (fleetShips.length !== totalShipsInFormation) {
369
+ return false;
370
+ }
371
+
372
+ // Check if the fleet has the correct types and counts of ships.
373
+ const formationComposition = new Map<string, number>();
374
+ formation.composition.forEach(role => {
375
+ formationComposition.set(role.shipClassName, role.count);
376
+ });
377
+
378
+ if (formationComposition.size !== fleetComposition.size) {
379
+ return false;
380
+ }
381
+
382
+ for (const [shipName, count] of formationComposition) {
383
+ if (fleetComposition.get(shipName) !== count) {
384
+ return false;
385
+ }
386
+ }
387
+
388
+ return true;
389
+ });
390
+
391
+ if (matchingStrictFormation) {
392
+ return matchingStrictFormation;
393
+ }
394
+
395
+ // 3. If no strict formation matches, find a suitable free formation.
396
+ const hasCivilianShips = fleetShips.some(s =>
397
+ s.shipClass.category === 'Commerce & Logistics' ||
398
+ s.shipClass.category === 'Resource Extraction' ||
399
+ s.shipClass.category === 'Diplomatic & Civil'
400
+ );
401
+
402
+ const suitableFreeFormations = this.freeFormations.filter(f => {
403
+ return hasCivilianShips ? f.category === 'Mixed' : f.category === 'Military';
404
+ });
405
+
406
+ if (suitableFreeFormations.length > 0) {
407
+ return suitableFreeFormations[Math.floor(Math.random() * suitableFreeFormations.length)];
408
+ }
409
+
410
+ // 4. As a last resort, return any free formation.
411
+ if (this.freeFormations.length > 0) {
412
+ return this.freeFormations[Math.floor(Math.random() * this.freeFormations.length)];
413
+ }
414
+
415
+ return null;
416
+ }
417
+
418
+ public getAllFormations(): Formation[] {
419
+ return this.formations;
420
+ }
421
+ }
422
+
423
+ export const formationRegistry = new FormationRegistry();
@@ -0,0 +1,59 @@
1
+ /**
2
+ * @fileoverview Defines the Meteor class for the night sky simulation.
3
+ */
4
+
5
+ import { BoundedBody } from '../base/bounded-body';
6
+
7
+ // --- Meteor Shower Configuration ---
8
+ export const METEOR_CONFIG = {
9
+ SHOWER_COOLDOWN_MIN: 900, // 15 seconds in frames
10
+ SHOWER_COOLDOWN_VARIANCE: 900, // 15-30 seconds in frames
11
+ COUNT_MIN: 10,
12
+ COUNT_VARIANCE: 15,
13
+ SPEED_MIN: 3.2,
14
+ SPEED_VARIANCE: 6.4,
15
+ LIFESPAN_MIN: 20,
16
+ LIFESPAN_VARIANCE: 40,
17
+ MIN_RADIUS: 0.5,
18
+ RADIUS_VARIANCE: 1.2,
19
+ };
20
+
21
+ export class Meteor extends BoundedBody {
22
+ radius: number;
23
+ color: string;
24
+ life: number;
25
+ maxLife: number;
26
+
27
+ constructor(x: number, y: number, vx: number, vy: number, color: string) {
28
+ const radius = Math.random() * METEOR_CONFIG.RADIUS_VARIANCE + METEOR_CONFIG.MIN_RADIUS;
29
+ super(x, y, radius);
30
+ this.radius = radius;
31
+ this.vx = vx;
32
+ this.vy = vy;
33
+ this.color = color;
34
+ this.maxLife = Math.random() * METEOR_CONFIG.LIFESPAN_VARIANCE + METEOR_CONFIG.LIFESPAN_MIN;
35
+ this.life = this.maxLife;
36
+ }
37
+
38
+ update() {
39
+ this.x += this.vx;
40
+ this.y += this.vy;
41
+ this.life--;
42
+ }
43
+
44
+ isAlive(width: number, height: number): boolean {
45
+ return this.life > 0 && this.x > 0 && this.x < width && this.y > 0 && this.y < height;
46
+ }
47
+
48
+ draw(ctx: CanvasRenderingContext2D) {
49
+ const alpha = this.life / this.maxLife;
50
+ ctx.beginPath();
51
+ ctx.globalAlpha = alpha;
52
+ ctx.strokeStyle = this.color;
53
+ ctx.lineWidth = this.radius;
54
+ ctx.moveTo(this.x, this.y);
55
+ ctx.lineTo(this.x - this.vx * 2, this.y - this.vy * 2);
56
+ ctx.stroke();
57
+ ctx.globalAlpha = 1;
58
+ }
59
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * @fileoverview Defines the Nebula class for the night sky simulation.
3
+ */
4
+
5
+ import { BoundedBody } from '../base/bounded-body';
6
+
7
+ // --- Nebula Configuration ---
8
+ export const NEBULA_CONFIG = {
9
+ COUNT: 5,
10
+ COLORS: ['rgba(139, 0, 255, 0.15)', 'rgba(0, 191, 255, 0.15)', 'rgba(255, 105, 180, 0.15)'],
11
+ VELOCITY_RANGE: 0.1,
12
+ RADIUS_BASE_FACTOR: 1 / 6,
13
+ RADIUS_VARIANCE_FACTOR: 1 / 4,
14
+ };
15
+
16
+
17
+ export class Nebula extends BoundedBody {
18
+ radius: number;
19
+ color: string;
20
+
21
+ constructor(x: number, y: number, width: number) {
22
+ const radius = Math.random() * (width * NEBULA_CONFIG.RADIUS_VARIANCE_FACTOR) + (width * NEBULA_CONFIG.RADIUS_BASE_FACTOR);
23
+ super(x, y, radius);
24
+ this.radius = radius;
25
+ this.vx = Math.random() * NEBULA_CONFIG.VELOCITY_RANGE * 2 - NEBULA_CONFIG.VELOCITY_RANGE;
26
+ this.vy = Math.random() * NEBULA_CONFIG.VELOCITY_RANGE * 2 - NEBULA_CONFIG.VELOCITY_RANGE;
27
+ this.color = NEBULA_CONFIG.COLORS[Math.floor(Math.random() * NEBULA_CONFIG.COLORS.length)];
28
+ }
29
+
30
+ update(ctx: CanvasRenderingContext2D, width: number, height: number) {
31
+ this.x += this.vx;
32
+ this.y += this.vy;
33
+
34
+ if (this.x - this.radius > width) this.x = -this.radius;
35
+ if (this.x + this.radius < 0) this.x = width + this.radius;
36
+ if (this.y - this.radius > height) this.y = -this.radius;
37
+ if (this.y + this.radius < 0) this.y = height + this.radius;
38
+ }
39
+
40
+ draw(ctx: CanvasRenderingContext2D) {
41
+ const gradient = ctx.createRadialGradient(this.x, this.y, this.radius * 0.2, this.x, this.y, this.radius);
42
+ gradient.addColorStop(0, this.color);
43
+ gradient.addColorStop(1, 'rgba(0, 0, 0, 0)');
44
+
45
+ ctx.fillStyle = gradient;
46
+ ctx.beginPath();
47
+ ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
48
+ ctx.fill();
49
+ }
50
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * @fileoverview Defines the Orbit class for the night sky simulation.
3
+ */
4
+
5
+ import { Star } from './star';
6
+ import { DockingPoint } from './docking-point';
7
+
8
+ // --- Orbit Configuration ---
9
+ export const ORBIT_CONFIG = {
10
+ COUNT_MIN: 1,
11
+ COUNT_MAX: 3,
12
+ RADIUS_MULTIPLIER_MIN: 3,
13
+ RADIUS_MULTIPLIER_MAX: 7,
14
+ THICKNESS: 0.1,
15
+ ALPHA: 0.0,
16
+ DOCKING_POINTS_PER_ORBIT: 8,
17
+ };
18
+
19
+ export class Orbit {
20
+ parent: Star;
21
+ radius: number;
22
+ thickness: number;
23
+ color: string;
24
+ dockingPoints: DockingPoint[] = [];
25
+
26
+ constructor(parent: Star, radius: number, thickness: number, color: string) {
27
+ this.parent = parent;
28
+ this.radius = radius;
29
+ this.thickness = thickness;
30
+ this.color = color;
31
+
32
+ // Add docking points along the orbit
33
+ const pointsOnOrbit = ORBIT_CONFIG.DOCKING_POINTS_PER_ORBIT;
34
+ for (let j = 0; j < pointsOnOrbit; j++) {
35
+ const angle = (j / pointsOnOrbit) * Math.PI * 2;
36
+ const dx = Math.cos(angle) * this.radius;
37
+ const dy = Math.sin(angle) * this.radius;
38
+ this.dockingPoints.push(new DockingPoint(this.parent, dx, dy));
39
+ }
40
+ }
41
+
42
+ draw(ctx: CanvasRenderingContext2D) {
43
+ if (!this.parent || ORBIT_CONFIG.ALPHA === 0) return;
44
+ ctx.save();
45
+ ctx.globalAlpha = ORBIT_CONFIG.ALPHA;
46
+ ctx.beginPath();
47
+ ctx.strokeStyle = this.color;
48
+ ctx.lineWidth = this.thickness;
49
+ ctx.arc(this.parent.x, this.parent.y, this.radius, 0, 2 * Math.PI);
50
+ ctx.stroke();
51
+ ctx.restore();
52
+ }
53
+ }
@@ -0,0 +1,64 @@
1
+
2
+ /**
3
+ * @fileoverview Defines the Pulsar class, a specialized type of Star.
4
+ */
5
+
6
+ import { Star, STAR_CONFIG } from './star';
7
+
8
+ // --- Pulsar Configuration ---
9
+ export const PULSAR_CONFIG = {
10
+ PULSAR_SPEED_MIN: 12.0,
11
+ PULSAR_SPEED_VARIANCE: 12.0,
12
+ };
13
+
14
+ let pulsarCounter = 0;
15
+
16
+ export class Pulsar extends Star {
17
+ pulsarPhase: number;
18
+ pulsationSpeed: number;
19
+
20
+ constructor(x: number, y: number) {
21
+ super(x, y);
22
+ this.name = `P-${++pulsarCounter}`;
23
+ this.pulsarPhase = Math.random() * Math.PI * 2;
24
+ this.pulsationSpeed = Math.random() * PULSAR_CONFIG.PULSAR_SPEED_VARIANCE + PULSAR_CONFIG.PULSAR_SPEED_MIN;
25
+ this.hasAura = Math.random() < STAR_CONFIG.AURA_CHANCE;
26
+ }
27
+
28
+ update(
29
+ ctx: CanvasRenderingContext2D,
30
+ width: number,
31
+ height: number,
32
+ mouse: { dx: number; dy: number },
33
+ interactive: boolean,
34
+ isMouseInside: boolean,
35
+ pulseTime?: number
36
+ ) {
37
+ // Call the parent update method to handle movement only in the main simulation
38
+ if (width > 0 && height > 0) {
39
+ super.update(ctx, width, height, mouse, interactive, isMouseInside);
40
+ }
41
+
42
+ // Update radius for the live simulation instance
43
+ const effectivePulseTime = (pulseTime || 0) * this.pulsationSpeed;
44
+ const pulseFactor = (Math.sin(effectivePulseTime + this.pulsarPhase) + 1.5) / 2.5;
45
+ this.radius = this.baseRadius * pulseFactor;
46
+ this.boundingRadius = this.radius;
47
+ }
48
+
49
+ draw(ctx: CanvasRenderingContext2D, pulseTime: number) {
50
+ const effectivePulseTime = pulseTime * this.pulsationSpeed;
51
+
52
+ ctx.save();
53
+
54
+ // Calculate alpha for the "blinking" effect
55
+ const alphaPulseFactor = (Math.sin(effectivePulseTime + this.pulsarPhase) + 1) / 2; // varies between 0 and 1
56
+ ctx.globalAlpha = 0.5 + alphaPulseFactor * 0.5;
57
+
58
+ // Call the base star drawing method which handles auras and hover effects correctly.
59
+ // We wrap it in save/restore to contain the globalAlpha change.
60
+ super.drawBase(ctx, pulseTime);
61
+
62
+ ctx.restore();
63
+ }
64
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * @fileoverview Defines the Ring class, a visual ring around a celestial body.
3
+ */
4
+ import type { CelestialBody } from '../base/celestial-body';
5
+
6
+ export class Ring {
7
+ parent: CelestialBody;
8
+ radius: number;
9
+ thickness: number;
10
+ color: string;
11
+ isDotted: boolean;
12
+
13
+ constructor(parent: CelestialBody, radius: number, thickness: number, color: string, isDotted: boolean) {
14
+ this.parent = parent;
15
+ this.radius = radius;
16
+ this.thickness = thickness;
17
+ this.color = color;
18
+ this.isDotted = isDotted;
19
+ }
20
+
21
+ draw(ctx: CanvasRenderingContext2D) {
22
+ // When drawing rings individually in the catalog, the parent might not be at (0,0) relative to the canvas.
23
+ // We will draw relative to the current transform.
24
+ const drawX = this.parent ? this.parent.x : 0;
25
+ const drawY = this.parent ? this.parent.y : 0;
26
+
27
+ ctx.beginPath();
28
+ ctx.strokeStyle = this.color;
29
+ ctx.lineWidth = this.thickness;
30
+
31
+ if (this.isDotted) {
32
+ ctx.setLineDash([2, 3]);
33
+ }
34
+
35
+ ctx.arc(drawX, drawY, this.radius, 0, 2 * Math.PI);
36
+ ctx.stroke();
37
+
38
+ if (this.isDotted) {
39
+ ctx.setLineDash([]);
40
+ }
41
+ }
42
+ }