@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,542 @@
1
+
2
+
3
+ import { SIMULATION_CONFIG, DEBUG_CONFIG } from './config/simulation';
4
+ import { Star } from './entities/star';
5
+ import { Comet, COMET_CONFIG } from './entities/comet';
6
+ import { Starship } from './entities/starship';
7
+ import { Fleet } from './entities/fleet';
8
+ import { HoverController } from './controllers/hover-controller';
9
+ import { BlackHle } from './entities/black-hole';
10
+ import { LayerController, type Layer } from './controllers/layer-controller';
11
+ import { StarshipController } from './controllers/starship-controller';
12
+ import { StarController } from './controllers/star-controller';
13
+ import { PhysicsController } from './controllers/physics-controller';
14
+ import { EffectsController } from './controllers/effects-controller';
15
+ import { DebugController } from './controllers/debug-controller';
16
+ import { Nebula, NEBULA_CONFIG } from './entities/nebula';
17
+ import { Quadtree } from './lib/quadtree';
18
+ import { DebugPanelController } from './ui/debug-panel-controller';
19
+ import { formationRegistry } from './entities/formations';
20
+ import { SectorGrid } from './entities/sector-grid';
21
+
22
+ export interface SimulationConfig {
23
+ starsCount?: number;
24
+ interactive?: boolean;
25
+ debug?: boolean;
26
+ showCatalog?: boolean;
27
+ showGrid?: boolean;
28
+ showGithubLink?: boolean;
29
+ }
30
+
31
+ export interface SimulationStats {
32
+ fps: number;
33
+ avgFps: number;
34
+ starCount: number;
35
+ starshipCount: number;
36
+ frameCount: number;
37
+ }
38
+
39
+ export class WhereStarsDrift {
40
+ private canvas: HTMLCanvasElement;
41
+ private ctx: CanvasRenderingContext2D;
42
+ private config: Required<SimulationConfig>;
43
+
44
+ private width: number;
45
+ private height: number;
46
+
47
+ private layerController!: LayerController;
48
+ private starController!: StarController;
49
+ private starshipController!: StarshipController;
50
+ private physicsController!: PhysicsController;
51
+ private effectsController!: EffectsController;
52
+ private debugController!: DebugController;
53
+ private debugPanelController!: DebugPanelController;
54
+ private hoverController!: HoverController;
55
+ private allLayers!: Layer[];
56
+ private allBodies!: (Star | Comet | Starship | Fleet | BlackHle | Nebula)[];
57
+ private quadtree!: Quadtree;
58
+
59
+ private mouse = {
60
+ x: 0,
61
+ y: 0,
62
+ lastX: 0,
63
+ lastY: 0,
64
+ dx: 0,
65
+ dy: 0,
66
+ };
67
+ private isMouseInside = false;
68
+
69
+ private animationFrameId: number = 0;
70
+ private lastTime: number = 0;
71
+ private pulseTime: number = 0;
72
+
73
+ // GitHub link interaction
74
+ private githubLinkBounds: { x: number; y: number; width: number; height: number } | null = null;
75
+ private isMouseOverGithubLink = false;
76
+
77
+ // Stats
78
+ private frameCount: number = 0;
79
+ private fps: number = 0;
80
+ private fpsFrameCount: number = 0;
81
+ private lastFpsUpdateTime: number = 0;
82
+ private fpsHistory: number[] = [];
83
+ private avgFps: number = 0;
84
+ private readonly avgFpsWindow = 60;
85
+
86
+ constructor(canvas: HTMLCanvasElement, config: SimulationConfig = {}) {
87
+ this.canvas = canvas;
88
+ const context = canvas.getContext('2d');
89
+ if (!context) {
90
+ throw new Error('Could not get 2D rendering context from canvas.');
91
+ }
92
+ this.ctx = context;
93
+
94
+ this.config = {
95
+ starsCount: config.starsCount ?? SIMULATION_CONFIG.STARS_COUNT,
96
+ interactive: config.interactive ?? true,
97
+ debug: config.debug ?? false,
98
+ showCatalog: config.showCatalog ?? false,
99
+ showGrid: config.showGrid ?? true,
100
+ showGithubLink: config.showGithubLink ?? true,
101
+ };
102
+
103
+ // Override DEBUG_CONFIG based on instance config
104
+ DEBUG_CONFIG.SHOW_DEBUG_INFO = this.config.debug;
105
+ DEBUG_CONFIG.SHOW_CATALOG_PANEL = this.config.showCatalog;
106
+
107
+ this.width = canvas.width;
108
+ this.height = canvas.height;
109
+
110
+ this.mouse.x = this.width / 2;
111
+ this.mouse.y = this.height / 2;
112
+ this.mouse.lastX = this.width / 2;
113
+ this.mouse.lastY = this.height / 2;
114
+
115
+ this._initializeControllers();
116
+
117
+ // Bind methods to the instance to maintain `this` context
118
+ this.animate = this.animate.bind(this);
119
+ this._handleMouseMove = this._handleMouseMove.bind(this);
120
+ this._handleMouseEnter = this._handleMouseEnter.bind(this);
121
+ this._handleMouseLeave = this._handleMouseLeave.bind(this);
122
+ this._handleMouseDown = this._handleMouseDown.bind(this);
123
+ this._handleMouseUp = this._handleMouseUp.bind(this);
124
+ this._handleWheel = this._handleWheel.bind(this);
125
+ this._handleResize = this._handleResize.bind(this);
126
+ }
127
+
128
+ private _initializeControllers() {
129
+ this.layerController = new LayerController();
130
+ this.debugController = new DebugController();
131
+ this.starController = new StarController(this.layerController, this.config.starsCount, this.width, this.height);
132
+ this.starshipController = new StarshipController(this.layerController, this.width, this.height, this.debugController);
133
+ this.physicsController = new PhysicsController(this.debugController);
134
+ this.effectsController = new EffectsController(this.width, this.height);
135
+
136
+ this.quadtree = new Quadtree({ x: 0, y: 0, width: this.width, height: this.height });
137
+
138
+ for (let i = 0; i < NEBULA_CONFIG.COUNT; i++) {
139
+ this.layerController.addBody('backgroundNebulas', new Nebula(Math.random() * this.width, Math.random() * this.height, this.width));
140
+ }
141
+ for (let i = 0; i < COMET_CONFIG.COUNT; i++) {
142
+ this.layerController.addBody('dynamicObjects', new Comet(this.width, this.height));
143
+ }
144
+
145
+ this.layerController.addBody('sectorGrid', new SectorGrid(this.width, this.height));
146
+
147
+ this.allLayers = Object.values(this.layerController.getLayers());
148
+ this.allBodies = this.allLayers.flatMap(l => l.bodies) as any[];
149
+ this.hoverController = new HoverController(this.allBodies);
150
+ this.debugPanelController = new DebugPanelController(this.ctx, formationRegistry.getAllFormations());
151
+ }
152
+
153
+ public start() {
154
+ if (this.animationFrameId !== 0) return; // Already running
155
+ this._setupEventListeners();
156
+ this.lastTime = performance.now();
157
+ this.animationFrameId = requestAnimationFrame(this.animate);
158
+ }
159
+
160
+ public stop() {
161
+ if (this.animationFrameId) {
162
+ cancelAnimationFrame(this.animationFrameId);
163
+ this.animationFrameId = 0;
164
+ }
165
+ }
166
+
167
+ public destroy() {
168
+ this.stop();
169
+ this._removeEventListeners();
170
+ }
171
+
172
+ public getStats(): SimulationStats {
173
+ return {
174
+ fps: this.fps,
175
+ avgFps: this.avgFps,
176
+ starCount: this.starController.getAllStars().length,
177
+ starshipCount: this.starshipController.getAllStarships().length,
178
+ frameCount: this.frameCount,
179
+ };
180
+ }
181
+
182
+ public setInteractive(enabled: boolean) {
183
+ if (this.config.interactive === enabled) return;
184
+ this.config.interactive = enabled;
185
+ if (this.animationFrameId !== 0) {
186
+ this._removeEventListeners();
187
+ this._setupEventListeners();
188
+ }
189
+ }
190
+
191
+ public setDebug(enabled: boolean) {
192
+ this.config.debug = enabled;
193
+ DEBUG_CONFIG.SHOW_DEBUG_INFO = enabled;
194
+ }
195
+
196
+ public setShowCatalog(visible: boolean) {
197
+ this.config.showCatalog = visible;
198
+ DEBUG_CONFIG.SHOW_CATALOG_PANEL = visible;
199
+ }
200
+
201
+ public setShowGrid(visible: boolean) {
202
+ this.config.showGrid = visible;
203
+ }
204
+
205
+ public setShowGithubLink(visible: boolean) {
206
+ this.config.showGithubLink = visible;
207
+ if (!visible) {
208
+ this.isMouseOverGithubLink = false;
209
+ this.canvas.style.cursor = 'default';
210
+ }
211
+ }
212
+
213
+ private _setupEventListeners() {
214
+ this.canvas.addEventListener('mousemove', this._handleMouseMove);
215
+ this.canvas.addEventListener('mouseenter', this._handleMouseEnter);
216
+ this.canvas.addEventListener('mouseleave', this._handleMouseLeave);
217
+ this.canvas.addEventListener('mousedown', this._handleMouseDown);
218
+ window.addEventListener('mouseup', this._handleMouseUp);
219
+ this.canvas.addEventListener('wheel', this._handleWheel);
220
+ window.addEventListener('resize', this._handleResize);
221
+ }
222
+
223
+ private _removeEventListeners() {
224
+ this.canvas.removeEventListener('mousemove', this._handleMouseMove);
225
+ this.canvas.removeEventListener('mouseenter', this._handleMouseEnter);
226
+ this.canvas.removeEventListener('mouseleave', this._handleMouseLeave);
227
+ this.canvas.removeEventListener('mousedown', this._handleMouseDown);
228
+ window.removeEventListener('mouseup', this._handleMouseUp);
229
+ this.canvas.removeEventListener('wheel', this._handleWheel);
230
+ window.removeEventListener('resize', this._handleResize);
231
+ }
232
+
233
+ private _handleMouseMove(event: MouseEvent) {
234
+ const rect = this.canvas.getBoundingClientRect();
235
+ const mouseX = event.clientX - rect.left;
236
+ const mouseY = event.clientY - rect.top;
237
+
238
+ if (this.debugPanelController && this.debugPanelController.isDragging()) {
239
+ this.debugPanelController.handleMouseMove(mouseX, mouseY);
240
+ return;
241
+ }
242
+
243
+ let isOverLink = false;
244
+ if (this.config.showGithubLink && this.githubLinkBounds) {
245
+ const bounds = this.githubLinkBounds;
246
+ if (mouseX >= bounds.x && mouseX <= bounds.x + bounds.width &&
247
+ mouseY >= bounds.y && mouseY <= bounds.y + bounds.height) {
248
+ isOverLink = true;
249
+ }
250
+ }
251
+
252
+ this.isMouseOverGithubLink = isOverLink;
253
+
254
+ if (this.isMouseOverGithubLink) {
255
+ this.canvas.style.cursor = 'pointer';
256
+ } else {
257
+ this.canvas.style.cursor = 'default';
258
+ }
259
+
260
+ if (this.isMouseInside) {
261
+ this.mouse.x = mouseX;
262
+ this.mouse.y = mouseY;
263
+ }
264
+ }
265
+
266
+ private _handleMouseEnter(event: MouseEvent) {
267
+ this.isMouseInside = true;
268
+ this.mouse.x = event.clientX - this.canvas.getBoundingClientRect().left;
269
+ this.mouse.y = event.clientY - this.canvas.getBoundingClientRect().top;
270
+ this.mouse.lastX = this.mouse.x;
271
+ this.mouse.lastY = this.mouse.y;
272
+ }
273
+
274
+ private _handleMouseLeave() {
275
+ this.isMouseInside = false;
276
+ this.mouse.lastX = -1;
277
+ this.mouse.lastY = -1;
278
+ this.mouse.dx = 0;
279
+ this.mouse.dy = 0;
280
+ this.isMouseOverGithubLink = false;
281
+ this.canvas.style.cursor = 'default';
282
+ }
283
+
284
+ private _handleMouseDown(event: MouseEvent) {
285
+ if (this.isMouseOverGithubLink) {
286
+ window.open('https://github.com/where-stars-drift', '_blank');
287
+ event.preventDefault();
288
+ return;
289
+ }
290
+
291
+ if (this.config.showCatalog) {
292
+ const rect = this.canvas.getBoundingClientRect();
293
+ const x = event.clientX - rect.left;
294
+ const y = event.clientY - rect.top;
295
+ if (this.debugPanelController.isMouseOver(x, y)) {
296
+ this.debugPanelController.handleMouseDown(x, y);
297
+ }
298
+ }
299
+ }
300
+
301
+ private _handleMouseUp() {
302
+ if (this.config.showCatalog) {
303
+ this.debugPanelController.handleMouseUp();
304
+ }
305
+ }
306
+
307
+ private _handleWheel(event: WheelEvent) {
308
+ if (this.config.showCatalog) {
309
+ const rect = this.canvas.getBoundingClientRect();
310
+ const x = event.clientX - rect.left;
311
+ const y = event.clientY - rect.top;
312
+ if (this.debugPanelController.isMouseOver(x, y)) {
313
+ event.preventDefault();
314
+ this.debugPanelController.handleScroll(event.deltaY);
315
+ }
316
+ }
317
+ }
318
+
319
+ private _handleResize() {
320
+ if (!this.canvas) return;
321
+ this.width = this.canvas.offsetWidth;
322
+ this.height = this.canvas.offsetHeight;
323
+ this.canvas.width = this.width;
324
+ this.canvas.height = this.height;
325
+
326
+ if (this.quadtree) {
327
+ this.quadtree = new Quadtree({ x: 0, y: 0, width: this.width, height: this.height });
328
+ }
329
+
330
+ const sectorGrid = this.layerController.getBodies('sectorGrid')[0] as SectorGrid;
331
+ if (sectorGrid) {
332
+ sectorGrid.resize(this.width, this.height);
333
+ }
334
+
335
+ if (this.debugPanelController) {
336
+ this.debugPanelController.handleResize();
337
+ }
338
+ }
339
+
340
+ private _drawGithubIcon(x: number, y: number, size: number) {
341
+ this.ctx.save();
342
+ this.ctx.translate(x, y);
343
+ // Scale to fit size, default path is for 24x24 box
344
+ this.ctx.scale(size / 24, size / 24);
345
+ const path = new Path2D('M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22');
346
+
347
+ const isHovered = this.isMouseOverGithubLink;
348
+ this.ctx.strokeStyle = isHovered ? 'rgba(255, 255, 255, 1.0)' : 'rgba(255, 255, 255, 0.8)';
349
+ this.ctx.lineWidth = 2;
350
+ this.ctx.stroke(path);
351
+ this.ctx.restore();
352
+ }
353
+
354
+ private _drawGithubLink() {
355
+ if (!this.config.showGithubLink) {
356
+ this.githubLinkBounds = null;
357
+ return;
358
+ }
359
+
360
+ const x = 16; // Corresponds to top-4, left-4
361
+ const y = 16;
362
+ const iconSize = 12;
363
+ const gap = 4;
364
+ const text = 'Where Stars Drift v1.0.0';
365
+
366
+ const isHovered = this.isMouseOverGithubLink;
367
+ this.ctx.fillStyle = isHovered ? 'rgba(255, 255, 255, 1.0)' : 'rgba(255, 255, 255, 0.8)';
368
+ this.ctx.font = '12px "Roboto Mono", monospace';
369
+ this.ctx.textBaseline = 'middle';
370
+
371
+ this._drawGithubIcon(x, y, iconSize);
372
+
373
+ const textX = x + iconSize + gap;
374
+ const textY = y + iconSize / 2;
375
+ this.ctx.fillText(text, textX, textY);
376
+
377
+ const textMetrics = this.ctx.measureText(text);
378
+ this.githubLinkBounds = {
379
+ x: x,
380
+ y: y,
381
+ width: iconSize + gap + textMetrics.width,
382
+ height: iconSize
383
+ };
384
+ }
385
+
386
+ private _checkMouseInteraction() {
387
+ if (!this.isMouseInside) return;
388
+
389
+ const starships = this.layerController.getBodies('dynamicObjects').filter(b => b instanceof Starship) as Starship[];
390
+ starships.forEach(ship => {
391
+ if (ship.state === 'FLEEING' || ship.state === 'IN_FORMATION') return;
392
+
393
+ const dx = ship.x - this.mouse.x;
394
+ const dy = ship.y - this.mouse.y;
395
+ const dist = Math.sqrt(dx * dx + dy * dy);
396
+
397
+ if (dist < 50) {
398
+ ship.state = 'FLEEING';
399
+ ship.stateTimer = 180;
400
+ }
401
+ });
402
+ }
403
+
404
+ private animate(currentTime: number) {
405
+ const deltaTime = (currentTime - this.lastTime) / 16.67; // Normalize to 60 FPS
406
+ this.lastTime = currentTime;
407
+ this.frameCount++;
408
+
409
+ // FPS Calculation
410
+ this.fpsFrameCount++;
411
+ if (currentTime - this.lastFpsUpdateTime > 1000) {
412
+ this.fps = this.fpsFrameCount / ((currentTime - this.lastFpsUpdateTime) / 1000);
413
+ this.lastFpsUpdateTime = currentTime;
414
+ this.fpsFrameCount = 0;
415
+
416
+ this.fpsHistory.push(this.fps);
417
+ if (this.fpsHistory.length > this.avgFpsWindow) {
418
+ this.fpsHistory.shift();
419
+ }
420
+ this.avgFps = this.fpsHistory.reduce((a, b) => a + b, 0) / this.fpsHistory.length;
421
+ }
422
+
423
+ this.ctx.clearRect(0, 0, this.width, this.height);
424
+ this.debugController.clear();
425
+
426
+ this.pulseTime += 0.02;
427
+
428
+ if (this.config.interactive && this.isMouseInside) {
429
+ this.mouse.dx = this.mouse.x - this.mouse.lastX;
430
+ this.mouse.dy = this.mouse.y - this.mouse.lastY;
431
+ } else {
432
+ this.mouse.dx = 0;
433
+ this.mouse.dy = 0;
434
+ }
435
+
436
+ this.mouse.lastX = this.mouse.x;
437
+ this.mouse.lastY = this.mouse.y;
438
+
439
+ const allStars = this.starController.getAllStars();
440
+ const allStarships = this.starshipController.getAllStarships();
441
+ const blackHoles = this.layerController.getBodies('blackHoles') as BlackHle[];
442
+ const allFleets = this.layerController.getBodies('dynamicObjects').filter(b => b instanceof Fleet) as Fleet[];
443
+ const allClans = this.starshipController.getAllClans();
444
+ const allFormations = formationRegistry.getAllFormations();
445
+
446
+ this.quadtree.clear();
447
+ for (const body of this.allBodies) {
448
+ this.quadtree.insert(body);
449
+ }
450
+
451
+ if (this.config.debug && DEBUG_CONFIG.SHOW_QUADTREE) {
452
+ this.quadtree.draw(this.ctx);
453
+ }
454
+
455
+ if (this.config.interactive) {
456
+ const newSupernovas = this.physicsController.update(blackHoles, allStars, this.quadtree, this.width, this.height);
457
+ this.effectsController.addSupernovas(newSupernovas);
458
+ }
459
+
460
+ this.allLayers.forEach(layer => {
461
+ layer.bodies.forEach(body => {
462
+ if (body instanceof Star) {
463
+ body.update(this.ctx, this.width, this.height, this.mouse, layer.isInteractive, this.isMouseInside, this.pulseTime);
464
+ } else if (body instanceof BlackHle) {
465
+ body.update(this.ctx, this.width, this.height, this.frameCount);
466
+ } else if (body instanceof Starship) {
467
+ if (body.state !== 'IN_FORMATION') {
468
+ body.update(this.ctx, this.width, this.height, this.mouse, deltaTime);
469
+ }
470
+ } else if (body instanceof Fleet) {
471
+ body.update(this.ctx, this.width, this.height);
472
+ } else if (body instanceof Comet) {
473
+ body.update(this.ctx, this.width, this.height);
474
+ } else {
475
+ body.update(this.ctx, this.width, this.height);
476
+ }
477
+ });
478
+ });
479
+
480
+ if (this.config.interactive) {
481
+ this.hoverController.update(this.mouse, this.isMouseInside, this.config.debug);
482
+ }
483
+
484
+ this.layerController.getBodies('backgroundNebulas').forEach(body => body.draw(this.ctx));
485
+ if (this.config.showGrid) {
486
+ this.layerController.getBodies('sectorGrid').forEach(body => body.draw(this.ctx));
487
+ }
488
+ this.layerController.getBodies('staticStars').forEach(body => (body as Star).draw(this.ctx, this.pulseTime));
489
+ this.layerController.getBodies('interactiveStars').forEach(body => (body as Star).draw(this.ctx, this.pulseTime));
490
+
491
+ this.effectsController.updateAndDrawSupernovas(this.ctx);
492
+ this.effectsController.drawStarToHoleStreams(this.ctx, allStars, blackHoles);
493
+ this.layerController.getBodies('blackHoles').forEach(body => (body as BlackHle).draw(this.ctx, this.pulseTime, this.frameCount));
494
+
495
+ this.layerController.getBodies('dynamicObjects').forEach(body => {
496
+ if (body instanceof Starship) {
497
+ const isInFleet = (this.layerController.getBodies('dynamicObjects').filter(b => b instanceof Fleet) as Fleet[]).some(f => f.members.includes(body));
498
+ if (!isInFleet) {
499
+ body.draw(this.ctx);
500
+ }
501
+ } else {
502
+ body.draw(this.ctx);
503
+ }
504
+ });
505
+
506
+ this.effectsController.updateAndDrawMeteors(this.ctx);
507
+
508
+ if (this.config.interactive) {
509
+ this.starshipController.manageStarshipTargets(allStars);
510
+ this.starshipController.manageFleets(this.width, this.height);
511
+ this._checkMouseInteraction();
512
+ }
513
+
514
+ // Isolate all UI drawing to prevent transform leaks
515
+ this.ctx.save();
516
+
517
+ this._drawGithubLink();
518
+
519
+ if (this.config.debug) {
520
+ this.debugController.draw(this.ctx);
521
+ if (DEBUG_CONFIG.SHOW_CATALOG_PANEL) {
522
+ this.debugPanelController.draw({
523
+ starships: allStarships,
524
+ formations: allFormations,
525
+ clans: allClans,
526
+ fleets: allFleets,
527
+ stars: allStars,
528
+ blackHoles: blackHoles,
529
+ }, this.pulseTime, this.frameCount);
530
+ }
531
+ this.ctx.fillStyle = 'rgba(255, 255, 255, 0.7)';
532
+ this.ctx.font = '12px "Roboto Mono", monospace';
533
+ this.ctx.textAlign = 'right';
534
+ const stats = this.getStats();
535
+ this.ctx.fillText(`FPS: ${stats.fps.toFixed(0)} (Avg: ${stats.avgFps.toFixed(0)})`, this.width - 10, this.height - 10);
536
+ }
537
+
538
+ this.ctx.restore();
539
+
540
+ this.animationFrameId = requestAnimationFrame(this.animate);
541
+ }
542
+ }