@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,339 @@
1
+
2
+ /**
3
+ * @fileoverview Defines the DebugPanelController for rendering the debug catalog panel.
4
+ */
5
+ import { DEBUG_CONFIG } from '../config/simulation';
6
+ import { type Starship, type ShipClass, SHAPES, SHIP_CLASS_TEMPLATES } from '../entities/starship';
7
+ import type { Formation } from '../entities/formations';
8
+ import type { Clan } from '../entities/clans';
9
+ import type { Fleet } from '../entities/fleet';
10
+ import { PANEL_CONFIG } from './panel-config';
11
+ import { drawStarshipCatalog, type ShipCatalog } from './ships-panel';
12
+ import { drawFormationCatalog, type FormationCatalog } from './formations-panel';
13
+ import { drawClanCatalog } from './clans-panel';
14
+ import { drawFleetCatalog } from './fleets-panel';
15
+ import { drawStarCatalog } from './stars-panel';
16
+ import { drawBlackHoleCatalog } from './black-holes-panel';
17
+ import type { CatalogData } from '../types';
18
+ import { Star } from '../entities/star';
19
+ import { StarFactory } from '../entities/star-factory';
20
+ import { BlackHole } from '../entities/black-hole';
21
+ import { BlackHoleFactory } from '../entities/black-hole-factory';
22
+
23
+ type Tab = 'Ships' | 'Formations' | 'Clans' | 'Fleets' | 'Stars' | 'Holes';
24
+
25
+ export class DebugPanelController {
26
+ private ctx: CanvasRenderingContext2D;
27
+ private activeTab: Tab | null = null;
28
+ private tabs: Tab[] = [];
29
+ private shipCatalog: ShipCatalog = { 'Military': {}, 'Non-Military': {} };
30
+ private formationCatalog: FormationCatalog = {};
31
+ private starCatalog: Star[] = [];
32
+ private blackHoleCatalog: BlackHole[] = [];
33
+ private panelY: number = 0;
34
+ private panelHeight: number = 0;
35
+ private scrollTop: number = 0;
36
+ private contentHeight: number = 0;
37
+ private starFactory: StarFactory;
38
+ private blackHoleFactory: BlackHoleFactory;
39
+
40
+ // State for scrollbar dragging
41
+ private isDraggingScrollbar: boolean = false;
42
+ private dragStartY: number = 0;
43
+ private dragStartScrollTop: number = 0;
44
+ private scrollbarThumb: { y: number, height: number } | null = null;
45
+ private scrollbarTrack: { x: number, y: number, width: number, height: number } | null = null;
46
+
47
+ // New state for formation ship visualization
48
+ private showFormationShips: boolean = false;
49
+
50
+ constructor(ctx: CanvasRenderingContext2D, allFormations: Formation[]) {
51
+ this.ctx = ctx;
52
+ this.starFactory = new StarFactory();
53
+ this.blackHoleFactory = new BlackHoleFactory();
54
+ this.initializeTabs();
55
+ this.cacheShipClasses();
56
+ this.cacheFormations(allFormations);
57
+ this.cacheStarVariations();
58
+ this.cacheBlackHoleTypes();
59
+ this.calculatePanelDimensions();
60
+ }
61
+
62
+ private initializeTabs() {
63
+ if (DEBUG_CONFIG.SHOW_STARSHIP_CATALOG) this.tabs.push('Ships');
64
+ if (DEBUG_CONFIG.SHOW_FORMATION_CATALOG) this.tabs.push('Formations');
65
+ if (DEBUG_CONFIG.SHOW_CLAN_CATALOG) this.tabs.push('Clans');
66
+ if (DEBUG_CONFIG.SHOW_FLEET_CATALOG) this.tabs.push('Fleets');
67
+ if (DEBUG_CONFIG.SHOW_STAR_CATALOG) this.tabs.push('Stars');
68
+ if (DEBUG_CONFIG.SHOW_BLACK_HOLE_CATALOG) this.tabs.push('Holes');
69
+
70
+ if (this.tabs.length > 0) {
71
+ this.activeTab = this.tabs[0];
72
+ }
73
+ }
74
+
75
+ private cacheShipClasses() {
76
+ const militaryCategories = ['Capital', 'Cruiser', 'Escort', 'Support', 'Small Craft'];
77
+
78
+ Object.values(SHIP_CLASS_TEMPLATES).forEach(template => {
79
+ const groupKey = militaryCategories.includes(template.category) ? 'Military' : 'Non-Military';
80
+
81
+ if (!this.shipCatalog[groupKey][template.category]) {
82
+ this.shipCatalog[groupKey][template.category] = [];
83
+ }
84
+
85
+ this.shipCatalog[groupKey][template.category].push({
86
+ template,
87
+ variants: SHAPES[template.shapeKey] || [],
88
+ });
89
+ });
90
+ }
91
+
92
+ private cacheFormations(allFormations: Formation[]) {
93
+ allFormations.forEach(formation => {
94
+ const categoryKey = formation.category;
95
+ const typeKey = formation.type;
96
+
97
+ if (!this.formationCatalog[categoryKey]) {
98
+ this.formationCatalog[categoryKey] = {};
99
+ }
100
+ if (!this.formationCatalog[categoryKey][typeKey]) {
101
+ this.formationCatalog[categoryKey][typeKey] = [];
102
+ }
103
+ this.formationCatalog[categoryKey][typeKey].push(formation);
104
+ });
105
+ }
106
+
107
+ private cacheStarVariations() {
108
+ // Standard Stars
109
+ const starWithAura = this.starFactory.createStar(0, 0);
110
+ starWithAura.hasAura = true;
111
+ this.starCatalog.push(starWithAura);
112
+ this.starCatalog.push(this.starFactory.createStar(0, 0, { radius: 1.5 }));
113
+ this.starCatalog.push(this.starFactory.createStar(0, 0, { radius: 2 }));
114
+
115
+ // Pulsars
116
+ const pulsarWithAura = this.starFactory.createPulsar(0, 0);
117
+ pulsarWithAura.hasAura = true;
118
+ this.starCatalog.push(pulsarWithAura);
119
+ this.starCatalog.push(this.starFactory.createPulsar(0, 0));
120
+ this.starCatalog.push(this.starFactory.createPulsar(0, 0));
121
+
122
+ // Ringed Planets
123
+ const planetWithAura = this.starFactory.createRingedPlanet(0, 0, { ringCount: 1 });
124
+ planetWithAura.hasAura = true;
125
+ this.starCatalog.push(planetWithAura);
126
+ this.starCatalog.push(this.starFactory.createRingedPlanet(0, 0, { ringCount: 2 }));
127
+ this.starCatalog.push(this.starFactory.createRingedPlanet(0, 0, { ringCount: 3 }));
128
+ }
129
+
130
+ private cacheBlackHoleTypes() {
131
+ const blackHoleTypes = this.blackHoleFactory.getAllBlackHoleTypes();
132
+ this.blackHoleCatalog = blackHoleTypes.map(type => this.blackHoleFactory.createBlackHole(0, 0, type));
133
+ }
134
+
135
+ private calculatePanelDimensions() {
136
+ this.panelHeight = this.ctx.canvas.height * PANEL_CONFIG.HEIGHT_PERCENT;
137
+ this.panelY = (this.ctx.canvas.height - this.panelHeight) / 2;
138
+ }
139
+
140
+ public isMouseOver(x: number, y: number): boolean {
141
+ return x >= PANEL_CONFIG.X && x <= PANEL_CONFIG.X + PANEL_CONFIG.WIDTH &&
142
+ y >= this.panelY && y <= this.panelY + this.panelHeight;
143
+ }
144
+
145
+ public handleScroll(deltaY: number) {
146
+ this.scrollTop += deltaY > 0 ? PANEL_CONFIG.SCROLL_SPEED : -PANEL_CONFIG.SCROLL_SPEED;
147
+ const contentAreaHeight = this.panelHeight - (PANEL_CONFIG.HEADER_HEIGHT + PANEL_CONFIG.TAB_HEIGHT);
148
+ const maxScroll = Math.max(0, this.contentHeight - contentAreaHeight);
149
+ this.scrollTop = Math.max(0, Math.min(this.scrollTop, maxScroll));
150
+ }
151
+
152
+ public isDragging(): boolean {
153
+ return this.isDraggingScrollbar;
154
+ }
155
+
156
+ public handleMouseDown(x: number, y: number) {
157
+ if (!this.activeTab || !this.isMouseOver(x, y)) return;
158
+
159
+ // Check for scrollbar thumb click
160
+ const contentAreaHeight = this.panelHeight - (PANEL_CONFIG.HEADER_HEIGHT + PANEL_CONFIG.TAB_HEIGHT);
161
+ if (this.scrollbarTrack && this.scrollbarThumb && this.contentHeight > contentAreaHeight) {
162
+ const track = this.scrollbarTrack;
163
+ const thumb = this.scrollbarThumb;
164
+ if (x >= track.x && x <= track.x + track.width && y >= thumb.y && y <= thumb.y + thumb.height) {
165
+ this.isDraggingScrollbar = true;
166
+ this.dragStartY = y;
167
+ this.dragStartScrollTop = this.scrollTop;
168
+ return;
169
+ }
170
+ }
171
+
172
+ // Handle tab clicks
173
+ const tabY = this.panelY + PANEL_CONFIG.HEADER_HEIGHT;
174
+ if (y >= tabY && y <= tabY + PANEL_CONFIG.TAB_HEIGHT) {
175
+ const tabWidth = PANEL_CONFIG.WIDTH / this.tabs.length;
176
+ for (let i = 0; i < this.tabs.length; i++) {
177
+ const tabX = PANEL_CONFIG.X + i * tabWidth;
178
+ if (x >= tabX && x < tabX + tabWidth) {
179
+ if (this.activeTab !== this.tabs[i]) {
180
+ this.activeTab = this.tabs[i];
181
+ this.scrollTop = 0; // Reset scroll on tab change
182
+ }
183
+ return;
184
+ }
185
+ }
186
+ }
187
+
188
+ // Handle toggle clicks within panels
189
+ const contentY = this.panelY + PANEL_CONFIG.HEADER_HEIGHT + PANEL_CONFIG.TAB_HEIGHT;
190
+ const contentClickY = y - (contentY - this.scrollTop);
191
+
192
+ if (this.activeTab === 'Formations') {
193
+ const button = PANEL_CONFIG.TOGGLE_BUTTON;
194
+ if (contentClickY >= button.y && contentClickY <= button.y + button.h &&
195
+ x >= PANEL_CONFIG.X + button.x && x <= PANEL_CONFIG.X + button.x + button.w) {
196
+ this.showFormationShips = !this.showFormationShips;
197
+ }
198
+ }
199
+ }
200
+
201
+ public handleMouseMove(x: number, y: number) {
202
+ if (!this.isDraggingScrollbar) return;
203
+
204
+ const contentAreaHeight = this.panelHeight - (PANEL_CONFIG.HEADER_HEIGHT + PANEL_CONFIG.TAB_HEIGHT);
205
+ const maxScrollTop = Math.max(0, this.contentHeight - contentAreaHeight);
206
+ if (maxScrollTop <= 0) return;
207
+
208
+ const thumbHeight = this.scrollbarThumb ? this.scrollbarThumb.height : 20;
209
+ const draggableAreaHeight = contentAreaHeight - thumbHeight;
210
+
211
+ if (draggableAreaHeight <= 0) return;
212
+
213
+ const scrollDelta = y - this.dragStartY;
214
+ const scrollTopDelta = (scrollDelta / draggableAreaHeight) * maxScrollTop;
215
+
216
+ const newScrollTop = this.dragStartScrollTop + scrollTopDelta;
217
+
218
+ this.scrollTop = Math.max(0, Math.min(newScrollTop, maxScrollTop));
219
+ }
220
+
221
+ public handleMouseUp() {
222
+ this.isDraggingScrollbar = false;
223
+ }
224
+
225
+ public handleResize() {
226
+ this.calculatePanelDimensions();
227
+ }
228
+
229
+ public draw(data: CatalogData, pulseTime: number, frameCount: number) {
230
+ if (!this.activeTab) return;
231
+
232
+ this.ctx.save();
233
+ this.drawPanelBase();
234
+ this.drawTabs();
235
+
236
+ const contentY = this.panelY + PANEL_CONFIG.HEADER_HEIGHT + PANEL_CONFIG.TAB_HEIGHT;
237
+ const contentAreaHeight = this.panelHeight - (PANEL_CONFIG.HEADER_HEIGHT + PANEL_CONFIG.TAB_HEIGHT);
238
+
239
+ this.ctx.save();
240
+ this.ctx.beginPath();
241
+ this.ctx.rect(PANEL_CONFIG.X, contentY, PANEL_CONFIG.WIDTH, contentAreaHeight);
242
+ this.ctx.clip();
243
+
244
+ this.ctx.translate(0, -this.scrollTop);
245
+
246
+ switch (this.activeTab) {
247
+ case 'Ships':
248
+ this.contentHeight = drawStarshipCatalog(this.ctx, contentY, this.shipCatalog);
249
+ break;
250
+ case 'Formations':
251
+ this.contentHeight = drawFormationCatalog(this.ctx, contentY, this.formationCatalog, this.showFormationShips);
252
+ break;
253
+ case 'Clans':
254
+ this.contentHeight = drawClanCatalog(this.ctx, contentY, data.clans);
255
+ break;
256
+ case 'Fleets':
257
+ this.contentHeight = drawFleetCatalog(this.ctx, contentY, data.fleets);
258
+ break;
259
+ case 'Stars':
260
+ this.contentHeight = drawStarCatalog(this.ctx, contentY, this.starCatalog, pulseTime);
261
+ break;
262
+ case 'Holes':
263
+ this.contentHeight = drawBlackHoleCatalog(this.ctx, contentY, this.blackHoleCatalog, pulseTime, frameCount);
264
+ break;
265
+ }
266
+
267
+ this.ctx.restore(); // Restore from translate and clip
268
+ this.drawScrollbar();
269
+ this.ctx.restore(); // Restore from initial save
270
+ }
271
+
272
+ private drawPanelBase() {
273
+ this.ctx.fillStyle = PANEL_CONFIG.BG_COLOR;
274
+ this.ctx.strokeStyle = PANEL_CONFIG.BORDER_COLOR;
275
+ this.ctx.lineWidth = 1;
276
+ this.ctx.fillRect(PANEL_CONFIG.X, this.panelY, PANEL_CONFIG.WIDTH, this.panelHeight);
277
+ this.ctx.strokeRect(PANEL_CONFIG.X, this.panelY, PANEL_CONFIG.WIDTH, this.panelHeight);
278
+
279
+ this.ctx.fillStyle = PANEL_CONFIG.TEXT_COLOR;
280
+ this.ctx.font = PANEL_CONFIG.TITLE_FONT;
281
+ this.ctx.textAlign = 'left';
282
+ this.ctx.textBaseline = 'middle';
283
+ this.ctx.fillText('Debug Catalog', PANEL_CONFIG.X + PANEL_CONFIG.PADDING, this.panelY + PANEL_CONFIG.HEADER_HEIGHT / 2);
284
+ }
285
+
286
+ private drawTabs() {
287
+ const tabY = this.panelY + PANEL_CONFIG.HEADER_HEIGHT;
288
+ const tabWidth = PANEL_CONFIG.WIDTH / this.tabs.length;
289
+ this.ctx.font = PANEL_CONFIG.TAB_FONT;
290
+ this.ctx.textAlign = 'center';
291
+
292
+ this.tabs.forEach((tab, i) => {
293
+ const tabX = PANEL_CONFIG.X + i * tabWidth;
294
+ if (tab === this.activeTab) {
295
+ this.ctx.fillStyle = 'rgba(255, 255, 255, 0.1)';
296
+ this.ctx.fillRect(tabX, tabY, tabWidth, PANEL_CONFIG.TAB_HEIGHT);
297
+ this.ctx.fillStyle = '#fff';
298
+ } else {
299
+ this.ctx.fillStyle = PANEL_CONFIG.ACCENT_COLOR;
300
+ }
301
+ this.ctx.fillText(tab, tabX + tabWidth / 2, tabY + PANEL_CONFIG.TAB_HEIGHT / 2);
302
+ });
303
+
304
+ this.ctx.beginPath();
305
+ this.ctx.moveTo(PANEL_CONFIG.X, tabY + PANEL_CONFIG.TAB_HEIGHT);
306
+ this.ctx.lineTo(PANEL_CONFIG.X + PANEL_CONFIG.WIDTH, tabY + PANEL_CONFIG.TAB_HEIGHT);
307
+ this.ctx.stroke();
308
+ }
309
+
310
+ private drawScrollbar() {
311
+ const contentY = this.panelY + PANEL_CONFIG.HEADER_HEIGHT + PANEL_CONFIG.TAB_HEIGHT;
312
+ const contentAreaHeight = this.panelHeight - (PANEL_CONFIG.HEADER_HEIGHT + PANEL_CONFIG.TAB_HEIGHT);
313
+
314
+ if (this.contentHeight <= contentAreaHeight) {
315
+ this.scrollbarThumb = null;
316
+ this.scrollbarTrack = null;
317
+ return; // No scrollbar needed
318
+ }
319
+
320
+ const scrollbarWidth = 8;
321
+ const scrollbarX = PANEL_CONFIG.X + PANEL_CONFIG.WIDTH - scrollbarWidth - 2;
322
+ this.scrollbarTrack = { x: scrollbarX, y: contentY, width: scrollbarWidth, height: contentAreaHeight };
323
+
324
+
325
+ // Draw scrollbar track
326
+ this.ctx.fillStyle = 'rgba(255, 255, 255, 0.1)';
327
+ this.ctx.fillRect(scrollbarX, contentY, scrollbarWidth, contentAreaHeight);
328
+
329
+ // Draw scrollbar thumb
330
+ const thumbHeight = Math.max(20, contentAreaHeight * (contentAreaHeight / this.contentHeight));
331
+ const maxScrollTop = this.contentHeight - contentAreaHeight;
332
+ const thumbY = contentY + (this.scrollTop / maxScrollTop) * (contentAreaHeight - thumbHeight);
333
+ this.scrollbarThumb = { y: thumbY, height: thumbHeight };
334
+
335
+
336
+ this.ctx.fillStyle = this.isDraggingScrollbar ? 'rgba(255, 255, 255, 0.6)' : 'rgba(255, 255, 255, 0.4)';
337
+ this.ctx.fillRect(scrollbarX, thumbY, scrollbarWidth, thumbHeight);
338
+ }
339
+ }
@@ -0,0 +1,153 @@
1
+
2
+ import type { Fleet } from '../entities/fleet';
3
+ import { PANEL_CONFIG } from './panel-config';
4
+ import { FLEET_CONFIG } from '../entities/fleet';
5
+
6
+ function drawFleetDetails(ctx: CanvasRenderingContext2D, fleet: Fleet, x: number, y: number): number {
7
+ let currentY = y;
8
+ const lineHeight = 14;
9
+
10
+ ctx.font = 'bold 12px "Roboto Mono", monospace';
11
+ ctx.fillStyle = '#fff';
12
+ ctx.textAlign = 'left';
13
+ ctx.textBaseline = 'top';
14
+ ctx.fillText(fleet.name, x, currentY);
15
+ currentY += lineHeight * 1.5;
16
+
17
+ const details = [
18
+ { label: 'Clan', value: fleet.clan?.name || 'Unknown' },
19
+ { label: 'State', value: fleet.state },
20
+ { label: 'Formation', value: fleet.formation?.name || 'None' },
21
+ { label: 'Ships', value: fleet.members.length.toString() },
22
+ ];
23
+
24
+ ctx.font = '10px "Roboto Mono", monospace';
25
+ details.forEach(detail => {
26
+ ctx.fillStyle = PANEL_CONFIG.ACCENT_COLOR;
27
+ ctx.fillText(`${detail.label}:`, x, currentY);
28
+ ctx.fillStyle = PANEL_CONFIG.TEXT_COLOR;
29
+ ctx.fillText(detail.value, x + 70, currentY);
30
+ currentY += lineHeight;
31
+ });
32
+
33
+ currentY += lineHeight * 0.5;
34
+ ctx.fillStyle = PANEL_CONFIG.ACCENT_COLOR;
35
+ ctx.fillText('Roster:', x, currentY);
36
+ currentY += lineHeight;
37
+
38
+ fleet.members.forEach(ship => {
39
+ if (ship.shipClass) {
40
+ ctx.fillStyle = ship.shipClass.color;
41
+ ctx.fillText(`\u2022 ${ship.shipClass.name} (${ship.name})`, x + 5, currentY);
42
+ } else {
43
+ ctx.fillStyle = '#ff00ff'; // Fallback color
44
+ ctx.fillText(`\u2022 Unknown Ship (${ship.name})`, x + 5, currentY);
45
+ }
46
+ currentY += lineHeight;
47
+ });
48
+
49
+ return currentY - y; // Return the height of this section
50
+ }
51
+
52
+ function drawFleetVisualization(ctx: CanvasRenderingContext2D, fleet: Fleet, centerX: number, centerY: number) {
53
+ if (!fleet.formation) return;
54
+
55
+ const positions = fleet.formation.getPosition(fleet.members);
56
+ const maxOffset = positions.reduce((max, offset) => {
57
+ const dist = Math.sqrt(offset.x * offset.x + offset.y * offset.y);
58
+ return Math.max(max, dist);
59
+ }, 0);
60
+
61
+ // Scale the formation to fit nicely in the panel
62
+ const vizRadius = Math.min(40, PANEL_CONFIG.WIDTH / 4);
63
+ const scaleFactor = vizRadius / (maxOffset + 5);
64
+
65
+ ctx.save();
66
+ ctx.translate(centerX, centerY);
67
+
68
+ // Draw bounding ring
69
+ ctx.beginPath();
70
+ ctx.strokeStyle = FLEET_CONFIG.RING_COLOR;
71
+ ctx.lineWidth = FLEET_CONFIG.RING_WIDTH;
72
+ ctx.setLineDash([2, 3]);
73
+ ctx.arc(0, 0, vizRadius, 0, Math.PI * 2);
74
+ ctx.stroke();
75
+ ctx.setLineDash([]);
76
+
77
+ // Draw ships
78
+ fleet.members.forEach((ship, i) => {
79
+ if (i >= positions.length || !ship.shipClass) return;
80
+
81
+ ctx.save();
82
+ const pos = positions[i];
83
+ // Here we use the fleet's current rotation to orient the ships
84
+ const rotatedX = pos.x * Math.cos(fleet.rotation) - pos.y * Math.sin(fleet.rotation);
85
+ const rotatedY = pos.x * Math.sin(fleet.rotation) + pos.y * Math.cos(fleet.rotation);
86
+
87
+ ctx.translate(rotatedX * scaleFactor, rotatedY * scaleFactor);
88
+ ctx.rotate(fleet.rotation + Math.PI / 2); // Ships face "up" relative to fleet direction
89
+
90
+ ctx.beginPath();
91
+ ship.shipClass.shape(ctx, ship.shipClass.radius * scaleFactor);
92
+ ctx.fillStyle = ship.shipClass.color;
93
+
94
+ if (fleet.clan) {
95
+ ctx.strokeStyle = fleet.clan.color;
96
+ } else {
97
+ ctx.strokeStyle = '#FFFFFF';
98
+ }
99
+
100
+ ctx.lineWidth = 0.5;
101
+ ctx.fill();
102
+ ctx.stroke();
103
+
104
+ ctx.restore();
105
+ });
106
+
107
+ ctx.restore();
108
+ }
109
+
110
+ export function drawFleetCatalog(ctx: CanvasRenderingContext2D, startY: number, fleets: Fleet[]): number {
111
+ let currentY = startY + PANEL_CONFIG.PADDING;
112
+
113
+ ctx.font = PANEL_CONFIG.GROUP_FONT;
114
+ ctx.fillStyle = PANEL_CONFIG.TEXT_COLOR;
115
+ ctx.textAlign = 'left';
116
+ ctx.textBaseline = 'top';
117
+ ctx.fillText('Active Fleets', PANEL_CONFIG.X + PANEL_CONFIG.PADDING, currentY);
118
+ currentY += PANEL_CONFIG.GROUP_HEADER_HEIGHT;
119
+
120
+ if (fleets.length === 0) {
121
+ ctx.font = '11px "Roboto Mono", monospace';
122
+ ctx.fillStyle = PANEL_CONFIG.ACCENT_COLOR;
123
+ ctx.fillText('No active fleets in simulation.', PANEL_CONFIG.X + PANEL_CONFIG.PADDING + 10, currentY);
124
+ currentY += 20;
125
+ return currentY - startY;
126
+ }
127
+
128
+ fleets.forEach((fleet, i) => {
129
+ const rowHeight = 150 + fleet.members.length * 12;
130
+ const vizCenterX = PANEL_CONFIG.X + PANEL_CONFIG.PADDING + 45;
131
+ const vizCenterY = currentY + 70;
132
+
133
+ drawFleetVisualization(ctx, fleet, vizCenterX, vizCenterY);
134
+
135
+ const detailsX = vizCenterX + 60;
136
+ drawFleetDetails(ctx, fleet, detailsX, currentY);
137
+
138
+ currentY += rowHeight;
139
+
140
+ // Draw separator
141
+ if (i < fleets.length - 1) {
142
+ ctx.beginPath();
143
+ ctx.moveTo(PANEL_CONFIG.X + PANEL_CONFIG.PADDING, currentY);
144
+ ctx.lineTo(PANEL_CONFIG.X + PANEL_CONFIG.WIDTH - PANEL_CONFIG.PADDING, currentY);
145
+ ctx.strokeStyle = PANEL_CONFIG.BORDER_COLOR;
146
+ ctx.lineWidth = 0.5;
147
+ ctx.stroke();
148
+ currentY += PANEL_CONFIG.PADDING;
149
+ }
150
+ });
151
+
152
+ return currentY - startY;
153
+ }
@@ -0,0 +1,155 @@
1
+
2
+ import type { Formation } from '../entities/formations';
3
+ import { type Starship, type ShipClass, SHAPES, SHIP_CLASS_TEMPLATES } from '../entities/starship';
4
+ import { FLEET_CONFIG } from '../entities/fleet';
5
+ import { PANEL_CONFIG } from './panel-config';
6
+
7
+ export type FormationCatalog = Record<string, Record<string, Formation[]>>;
8
+
9
+ const createMockShip = (shipClassName: string): Partial<Starship> => {
10
+ const template = Object.values(SHIP_CLASS_TEMPLATES).find(t => t.name === shipClassName);
11
+ if (!template) {
12
+ return { shipClass: { name: shipClassName, radius: 2, color: '#ff00ff', shape: SHAPES.UTILITY[0] } as any };
13
+ }
14
+
15
+ const shapeVariants = SHAPES[template.shapeKey];
16
+ const shape = shapeVariants[0]; // Use the first variant for consistency
17
+
18
+ return {
19
+ shipClass: { ...template, shape, accentColor: '#fff' } as ShipClass,
20
+ };
21
+ };
22
+
23
+ export function drawFormationCatalog(ctx: CanvasRenderingContext2D, startY: number, formationCatalog: FormationCatalog, showShips: boolean): number {
24
+ let currentY = startY + PANEL_CONFIG.PADDING;
25
+
26
+ // Draw the toggle button
27
+ const button = PANEL_CONFIG.TOGGLE_BUTTON;
28
+ ctx.strokeStyle = showShips ? '#fff' : PANEL_CONFIG.BORDER_COLOR;
29
+ ctx.fillStyle = showShips ? 'rgba(255, 255, 255, 0.1)' : 'transparent';
30
+ ctx.lineWidth = 1;
31
+ ctx.fillRect(button.x, currentY + button.y, button.w, button.h);
32
+ ctx.strokeRect(button.x, currentY + button.y, button.w, button.h);
33
+
34
+ ctx.fillStyle = PANEL_CONFIG.TEXT_COLOR;
35
+ ctx.font = '11px "Roboto Mono", monospace';
36
+ ctx.textAlign = 'left';
37
+ ctx.textBaseline = 'middle';
38
+ ctx.fillText(`Show Ships: ${showShips ? 'ON' : 'OFF'}`, button.x + 5, currentY + button.y + button.h / 2);
39
+
40
+ currentY += button.h + PANEL_CONFIG.PADDING * 2;
41
+
42
+
43
+ Object.keys(formationCatalog).forEach(category => {
44
+ ctx.font = PANEL_CONFIG.GROUP_FONT;
45
+ ctx.fillStyle = PANEL_CONFIG.TEXT_COLOR;
46
+ ctx.textAlign = 'left';
47
+ ctx.textBaseline = 'top';
48
+ ctx.fillText(category, PANEL_CONFIG.X + PANEL_CONFIG.PADDING, currentY);
49
+ currentY += PANEL_CONFIG.GROUP_HEADER_HEIGHT;
50
+
51
+ const types = formationCatalog[category];
52
+ Object.keys(types).forEach(type => {
53
+ ctx.font = PANEL_CONFIG.ROW_FONT;
54
+ ctx.fillStyle = PANEL_CONFIG.ACCENT_COLOR;
55
+ ctx.fillText(type, PANEL_CONFIG.X + PANEL_CONFIG.PADDING + 10, currentY);
56
+ currentY += PANEL_CONFIG.ROW_HEIGHT * 0.8;
57
+
58
+ const formationList = types[type];
59
+ formationList.forEach(formation => {
60
+ ctx.fillStyle = PANEL_CONFIG.TEXT_COLOR;
61
+ ctx.fillText(formation.name, PANEL_CONFIG.X + PANEL_CONFIG.PADDING + 20, currentY);
62
+ currentY += 15;
63
+
64
+ // Draw description
65
+ ctx.font = '11px "Roboto Mono", monospace';
66
+ ctx.fillStyle = PANEL_CONFIG.ACCENT_COLOR;
67
+ const words = formation.description.split(' ');
68
+ let line = '';
69
+ for (let n = 0; n < words.length; n++) {
70
+ const testLine = line + words[n] + ' ';
71
+ const metrics = ctx.measureText(testLine);
72
+ if (metrics.width > PANEL_CONFIG.WIDTH - 60 && n > 0) {
73
+ ctx.fillText(line, PANEL_CONFIG.X + PANEL_CONFIG.PADDING + 20, currentY);
74
+ line = words[n] + ' ';
75
+ currentY += 12;
76
+ } else {
77
+ line = testLine;
78
+ }
79
+ }
80
+ ctx.fillText(line, PANEL_CONFIG.X + PANEL_CONFIG.PADDING + 20, currentY);
81
+ currentY += 15;
82
+
83
+ // Draw formation visualization
84
+ let mockShips: Partial<Starship>[];
85
+ if (formation.type === 'Strict') {
86
+ mockShips = formation.composition
87
+ .map(c => Array.from({ length: c.count }, () => createMockShip(c.shipClassName)))
88
+ .flat();
89
+ } else {
90
+ // For free formations, just use a set number of generic ships
91
+ mockShips = Array.from({ length: 7 }, () => createMockShip(''));
92
+ }
93
+
94
+ const positions = formation.getPosition(mockShips as Starship[]);
95
+ const maxOffset = positions.reduce((max, offset) => {
96
+ const dist = Math.sqrt(offset.x * offset.x + offset.y * offset.y);
97
+ return Math.max(max, dist);
98
+ }, 0);
99
+ const formationRadius = maxOffset + 5; // Add some padding
100
+
101
+ ctx.save();
102
+ ctx.translate(PANEL_CONFIG.X + PANEL_CONFIG.WIDTH / 2, currentY + formationRadius + 10);
103
+
104
+ // Draw dashed circle
105
+ ctx.beginPath();
106
+ ctx.strokeStyle = FLEET_CONFIG.RING_COLOR;
107
+ ctx.lineWidth = FLEET_CONFIG.RING_WIDTH;
108
+ ctx.setLineDash([2, 3]);
109
+ ctx.arc(0, 0, formationRadius, 0, Math.PI * 2);
110
+ ctx.stroke();
111
+ ctx.setLineDash([]);
112
+
113
+ // Draw ships first (if enabled)
114
+ if (showShips && formation.type === 'Strict') {
115
+ mockShips.forEach((ship, i) => {
116
+ if (!ship.shipClass || i >= positions.length) return;
117
+
118
+ ctx.save();
119
+ ctx.translate(positions[i].x, positions[i].y);
120
+ // Ships face "up" by default, so no extra rotation needed unless the formation dictates it
121
+ ctx.rotate(Math.PI / 2);
122
+
123
+ ctx.beginPath();
124
+ ship.shipClass.shape(ctx, ship.shipClass.radius * PANEL_CONFIG.SHIP_RENDER_SCALE);
125
+ ctx.fillStyle = ship.shipClass.color;
126
+ ctx.strokeStyle = '#fff';
127
+ ctx.lineWidth = 0.5;
128
+ ctx.fill();
129
+ ctx.stroke();
130
+
131
+ ctx.restore();
132
+ });
133
+ }
134
+
135
+ // Draw dots on top
136
+ ctx.fillStyle = 'red';
137
+ positions.forEach(pos => {
138
+ ctx.beginPath();
139
+ ctx.arc(pos.x, pos.y, 1.5, 0, Math.PI * 2);
140
+ ctx.fill();
141
+ });
142
+
143
+ ctx.restore();
144
+
145
+ const totalFormationHeight = formationRadius * 2 + 20;
146
+ currentY += Math.max(30, totalFormationHeight); // Add spacing for next item
147
+
148
+ ctx.font = PANEL_CONFIG.ROW_FONT; // Reset font
149
+ });
150
+ currentY += PANEL_CONFIG.ROW_HEIGHT / 2;
151
+ });
152
+ });
153
+
154
+ return currentY - startY;
155
+ }
@@ -0,0 +1,26 @@
1
+ export const PANEL_CONFIG = {
2
+ X: 10,
3
+ WIDTH: 350,
4
+ HEIGHT_PERCENT: 0.5,
5
+ HEADER_HEIGHT: 40,
6
+ TAB_HEIGHT: 30,
7
+ ROW_HEIGHT: 25,
8
+ GROUP_HEADER_HEIGHT: 20,
9
+ PADDING: 10,
10
+ BG_COLOR: 'rgba(0, 0, 0, 0.75)',
11
+ BORDER_COLOR: '#555',
12
+ TEXT_COLOR: '#ddd',
13
+ ACCENT_COLOR: '#aaa',
14
+ TITLE_FONT: '14px "Roboto Mono", monospace',
15
+ TAB_FONT: '11px "Roboto Mono", monospace',
16
+ ROW_FONT: '12px "Roboto Mono", monospace',
17
+ GROUP_FONT: 'bold 12px "Roboto Mono", monospace',
18
+ SHIP_RENDER_SCALE: 1,
19
+ SCROLL_SPEED: 20,
20
+ TOGGLE_BUTTON: {
21
+ x: 15,
22
+ y: 5,
23
+ w: 120,
24
+ h: 22,
25
+ },
26
+ };