@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,446 @@
1
+
2
+ /**
3
+ * @fileoverview Defines the Fleet class for managing groups of starships.
4
+ */
5
+ import { Starship, STARSHIP_CONFIG } from './starship';
6
+ import type { Hoverable } from '../base/hoverable';
7
+ import type { Clan } from './clans';
8
+ import { formationRegistry, Formation, StrictFormation } from './formations';
9
+ import { DEBUG_CONFIG } from '../config/simulation';
10
+ import { BoundedBody } from '../base/bounded-body';
11
+
12
+ let fleetCounter = 0;
13
+ const FLEET_NAMES = ["Alpha", "Beta", "Gamma", "Delta", "Epsilon", "Zeta", "Eta", "Theta"];
14
+
15
+ type FleetState = 'PATROLLING' | 'REFORMING';
16
+
17
+ // --- Fleet Configuration ---
18
+ export const FLEET_CONFIG = {
19
+ HOVER_GLOW_COLOR: 'rgba(255, 255, 255, 0.5)',
20
+ HOVER_GLOW_WIDTH: 2,
21
+ RING_COLOR: 'rgba(255, 255, 255, 0.2)',
22
+ RING_WIDTH: 0.5,
23
+ HOVER_TIMEOUT: 120, // 2 seconds in frames
24
+ ICON_SIZE: 12,
25
+ ICON_MARGIN: 8,
26
+ };
27
+
28
+ export class Fleet extends BoundedBody implements Hoverable {
29
+ id: number;
30
+ name: string;
31
+ members: Starship[];
32
+ size: number;
33
+ isActive: boolean;
34
+ clan: Clan;
35
+ state: FleetState;
36
+ isHovered: boolean = false;
37
+ hoverTimer: number;
38
+ formation: Formation | null;
39
+
40
+ // Fleet movement properties
41
+ radius: number;
42
+ hoverRadius: number;
43
+ speedMultiplier: number;
44
+ rotation: number; // Current visual rotation
45
+ targetRotation: number; // Target visual rotation
46
+
47
+ constructor(ships: Starship[], clan: Clan, canvasWidth: number, canvasHeight: number) {
48
+ const formation = formationRegistry.getFormationForFleet(ships);
49
+ const formationOffsets = formation ? formation.getPosition(ships) : [];
50
+
51
+ const maxOffset = formationOffsets.reduce((max, offset) => {
52
+ const dist = Math.sqrt(offset.x * offset.x + offset.y * offset.y);
53
+ return Math.max(max, dist);
54
+ }, 0);
55
+
56
+ const radius = maxOffset + STARSHIP_CONFIG.FORMATION_PADDING;
57
+ const safePadding = radius * 1.2;
58
+ const x = Math.random() * (canvasWidth - 2 * safePadding) + safePadding;
59
+ const y = Math.random() * (canvasHeight - 2 * safePadding) + safePadding;
60
+
61
+ super(x, y, radius);
62
+ this.boundingRadius = radius;
63
+
64
+ this.id = ++fleetCounter;
65
+ this.name = `${FLEET_NAMES[this.id % FLEET_NAMES.length]} Fleet #${this.id}`;
66
+ this.members = ships;
67
+ this.size = ships.length;
68
+ this.isActive = true;
69
+ this.clan = clan;
70
+ this.state = 'PATROLLING';
71
+ this.hoverTimer = 0;
72
+ this.formation = formation;
73
+
74
+ if (!this.formation || formationOffsets.length === 0) {
75
+ console.warn(`Fleet ${this.name} (${this.id}) created but got no valid formation or positions. Fleet will be inactive.`);
76
+ this.isActive = false;
77
+ }
78
+
79
+ this.radius = radius;
80
+ this.hoverRadius = this.radius * 1.2; // 20% larger hover area
81
+
82
+ this.speedMultiplier = Math.random() * 0.2 + 0.8; // Speed between 80% and 100% of normal
83
+ const baseSpeed = STARSHIP_CONFIG.MAX_SPEED * this.speedMultiplier;
84
+ const angle = Math.random() * 2 * Math.PI;
85
+ this.vx = Math.cos(angle) * baseSpeed;
86
+ this.vy = Math.sin(angle) * baseSpeed;
87
+
88
+ this.rotation = Math.atan2(this.vy, this.vx); // Initial rotation
89
+ this.targetRotation = this.rotation;
90
+
91
+ const rotatedOffsets = this.formation?.getPosition(this.members).map(offset => {
92
+ const rotatedX = offset.x * Math.cos(this.rotation) - offset.y * Math.sin(this.rotation);
93
+ const rotatedY = offset.x * Math.sin(this.rotation) + offset.y * Math.cos(this.rotation);
94
+ return { x: rotatedX, y: rotatedY };
95
+ }) || [];
96
+
97
+ this.members.forEach((ship, i) => {
98
+ ship.fleet = this;
99
+ ship.state = 'IN_FORMATION';
100
+ if (i >= rotatedOffsets.length) return;
101
+ const offset = rotatedOffsets[i];
102
+ ship.x = this.x + offset.x;
103
+ ship.y = this.y + offset.y;
104
+ ship.rotation = this.rotation + Math.PI / 2;
105
+ });
106
+
107
+ // Register with the clan
108
+ this.clan.fleets.push(this);
109
+ this.clan.starships.push(...this.members);
110
+ this.clan.fleetsCount = this.clan.fleets.length;
111
+ this.clan.starshipsCount = this.clan.starships.length;
112
+ }
113
+
114
+ disband() {
115
+ this.isActive = false;
116
+
117
+ // Unregister from the clan
118
+ const fleetIndex = this.clan.fleets.indexOf(this);
119
+ if (fleetIndex > -1) {
120
+ this.clan.fleets.splice(fleetIndex, 1);
121
+ }
122
+ this.clan.fleetsCount = this.clan.fleets.length;
123
+
124
+ this.members.forEach(ship => {
125
+ ship.fleet = null;
126
+ ship.state = 'IDLE';
127
+
128
+ const shipIndex = this.clan.starships.indexOf(ship);
129
+ if (shipIndex > -1) {
130
+ this.clan.starships.splice(shipIndex, 1);
131
+ }
132
+ });
133
+ this.clan.starshipsCount = this.clan.starships.length;
134
+ }
135
+
136
+
137
+ private beginReforming() {
138
+ if (!this.formation) return;
139
+ this.state = 'REFORMING';
140
+ this.targetRotation = Math.atan2(this.vy, this.vx);
141
+
142
+ const formationOffsets = this.formation.getPosition(this.members);
143
+ const sortedShips: Starship[] = [];
144
+
145
+ if (this.formation.type === 'Strict') {
146
+ const strictFormation = this.formation as StrictFormation;
147
+ const availableShips = [...this.members];
148
+
149
+ strictFormation.composition.forEach(role => {
150
+ for (let i = 0; i < role.count; i++) {
151
+ const shipIndex = availableShips.findIndex(s => s.shipClass.name === role.shipClassName);
152
+ if (shipIndex !== -1) {
153
+ sortedShips.push(availableShips.splice(shipIndex, 1)[0]);
154
+ }
155
+ }
156
+ });
157
+ sortedShips.push(...availableShips);
158
+ } else {
159
+ const unassignedShips = [...this.members];
160
+ const unassignedSlots = [...formationOffsets];
161
+
162
+ while(unassignedShips.length > 0 && unassignedSlots.length > 0) {
163
+ let bestShipIndex = -1;
164
+ let bestSlotIndex = -1;
165
+ let closestDistSq = Infinity;
166
+
167
+ for (let i = 0; i < unassignedShips.length; i++) {
168
+ for (let j = 0; j < unassignedSlots.length; j++) {
169
+ const ship = unassignedShips[i];
170
+ const slot = unassignedSlots[j];
171
+ const distSq = (ship.x - (this.x + slot.x)) ** 2 + (ship.y - (this.y + slot.y)) ** 2;
172
+ if (distSq < closestDistSq) {
173
+ closestDistSq = distSq;
174
+ bestShipIndex = i;
175
+ bestSlotIndex = j;
176
+ }
177
+ }
178
+ }
179
+
180
+ if (bestShipIndex !== -1 && bestSlotIndex !== -1) {
181
+ sortedShips.push(unassignedShips.splice(bestShipIndex, 1)[0]);
182
+ unassignedSlots.splice(bestSlotIndex, 1);
183
+ } else {
184
+ break;
185
+ }
186
+ }
187
+ sortedShips.push(...unassignedShips);
188
+ }
189
+
190
+ this.members = sortedShips;
191
+ }
192
+
193
+
194
+ private getBoundingBox() {
195
+ if (this.members.length === 0) {
196
+ return { minX: this.x, minY: this.y, maxX: this.x, maxY: this.y };
197
+ }
198
+
199
+ return this.members.reduce((acc, ship) => ({
200
+ minX: Math.min(acc.minX, ship.x - ship.boundingRadius),
201
+ minY: Math.min(acc.minY, ship.y - ship.boundingRadius),
202
+ maxX: Math.max(acc.maxX, ship.x + ship.boundingRadius),
203
+ maxY: Math.max(acc.maxY, ship.y + ship.boundingRadius),
204
+ }), {
205
+ minX: Infinity,
206
+ minY: Infinity,
207
+ maxX: -Infinity,
208
+ maxY: -Infinity,
209
+ });
210
+ }
211
+
212
+ private updateShipPositions(): boolean {
213
+ if (!this.formation || this.members.length === 0) return true;
214
+
215
+ let allShipsInPosition = true;
216
+ const formationOffsets = this.formation.getPosition(this.members);
217
+
218
+ this.members.forEach((ship, i) => {
219
+ if (i >= formationOffsets.length) return;
220
+
221
+ let offset = formationOffsets[i];
222
+
223
+ const rotatedX = offset.x * Math.cos(this.rotation) - offset.y * Math.sin(this.rotation);
224
+ const rotatedY = offset.x * Math.sin(this.rotation) + offset.y * Math.cos(this.rotation);
225
+ const targetX = this.x + rotatedX;
226
+ const targetY = this.y + rotatedY;
227
+
228
+ ship.accelerateToPosition(targetX, targetY);
229
+
230
+ const dx = targetX - ship.x;
231
+ const dy = targetY - ship.y;
232
+ const dist = Math.sqrt(dx * dx + dy * dy);
233
+
234
+ if (dist > 1) {
235
+ allShipsInPosition = false;
236
+ }
237
+ });
238
+
239
+ return allShipsInPosition;
240
+ }
241
+
242
+ update(ctx: CanvasRenderingContext2D, width: number, height: number) {
243
+ if (!this.isActive || !this.formation) return;
244
+
245
+ this.x += this.vx;
246
+ this.y += this.vy;
247
+
248
+ const padding = this.radius * 0.2;
249
+ let bounced = false;
250
+
251
+ const { minX, minY, maxX, maxY } = this.getBoundingBox();
252
+ let changed = false;
253
+
254
+ if (minX < padding && this.vx < 0) {
255
+ this.vx *= -1;
256
+ changed = true;
257
+ }
258
+ if (maxX > width - padding && this.vx > 0) {
259
+ this.vx *= -1;
260
+ changed = true;
261
+ }
262
+ if (minY < padding && this.vy < 0) {
263
+ this.vy *= -1;
264
+ changed = true;
265
+ }
266
+ if (maxY > height - padding && this.vy > 0) {
267
+ this.vy *= -1;
268
+ changed = true;
269
+ }
270
+ if (changed) {
271
+ bounced = true;
272
+ }
273
+
274
+ if (bounced && this.state !== 'REFORMING') {
275
+ this.beginReforming();
276
+ }
277
+
278
+ if (this.state === 'REFORMING') {
279
+ let angleDiff = this.targetRotation - this.rotation;
280
+ while (angleDiff > Math.PI) angleDiff -= 2 * Math.PI;
281
+ while (angleDiff < -Math.PI) angleDiff += 2 * Math.PI;
282
+ this.rotation += angleDiff * 0.05;
283
+
284
+ const areAllShipsInPosition = this.updateShipPositions();
285
+ const isRotationComplete = Math.abs(angleDiff) < 0.01;
286
+
287
+ if (isRotationComplete && areAllShipsInPosition) {
288
+ this.state = 'PATROLLING';
289
+ this.rotation = this.targetRotation;
290
+ const baseSpeed = STARSHIP_CONFIG.MAX_SPEED * this.speedMultiplier * (Math.random() * 0.2 + 0.9);
291
+ this.vx = Math.cos(this.rotation) * baseSpeed;
292
+ this.vy = Math.sin(this.rotation) * baseSpeed;
293
+ }
294
+ } else if (this.state === 'PATROLLING') {
295
+ this.rotation = Math.atan2(this.vy, this.vx);
296
+
297
+ const currentOffsets = this.formation.getPosition(this.members);
298
+ this.members.forEach((ship, i) => {
299
+ if (i >= currentOffsets.length) return;
300
+ const offset = currentOffsets[i];
301
+
302
+ const rotatedX = offset.x * Math.cos(this.rotation) - offset.y * Math.sin(this.rotation);
303
+ const rotatedY = offset.x * Math.sin(this.rotation) + offset.y * Math.cos(this.rotation);
304
+
305
+ ship.x = this.x + rotatedX;
306
+ ship.y = this.y + rotatedY;
307
+ ship.vx = this.vx;
308
+ ship.vy = this.vy;
309
+ ship.rotation = this.rotation + Math.PI / 2;
310
+ });
311
+ }
312
+ }
313
+
314
+ private drawHoverLabel(ctx: CanvasRenderingContext2D) {
315
+ const iconY = this.y - this.radius - FLEET_CONFIG.ICON_MARGIN - FLEET_CONFIG.ICON_SIZE;
316
+ ctx.save();
317
+ ctx.translate(this.x, iconY);
318
+ ctx.fillStyle = this.clan.color;
319
+ ctx.strokeStyle = this.clan.color;
320
+ ctx.lineWidth = 1.5;
321
+ this.clan.icon(ctx, FLEET_CONFIG.ICON_SIZE);
322
+ ctx.restore();
323
+ }
324
+
325
+ private drawDebugInfo(ctx: CanvasRenderingContext2D) {
326
+ const startX = this.x + this.radius + 15;
327
+ let startY = this.y - (this.members.length / 2 * 12);
328
+
329
+ const boxPadding = 10;
330
+ const lineHeight = 14;
331
+ const infoLines = [
332
+ `Fleet: ${this.name} (ID: ${this.id})`,
333
+ `Clan: ${this.clan.name}`,
334
+ `Ships: ${this.members.length}`,
335
+ `Formation: ${this.formation ? this.formation.name : 'None'}`,
336
+ `State: ${this.state}`,
337
+ '--- Roster ---',
338
+ ...this.members.map(ship => `> ${ship.name} (${ship.shipClass.name})`),
339
+ ];
340
+ const boxHeight = infoLines.length * lineHeight + boxPadding * 2;
341
+ const boxWidth = Math.max(...infoLines.map(line => ctx.measureText(line).width)) + boxPadding * 2;
342
+
343
+ const boxY = this.y - boxHeight / 2;
344
+
345
+ // Draw background box
346
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
347
+ ctx.strokeStyle = this.clan.color;
348
+ ctx.lineWidth = 1;
349
+ ctx.beginPath();
350
+ ctx.roundRect(startX, boxY, boxWidth, boxHeight, 5);
351
+ ctx.fill();
352
+ ctx.stroke();
353
+
354
+ // Draw text
355
+ ctx.fillStyle = 'white';
356
+ ctx.font = '10px "Roboto Mono", monospace';
357
+ ctx.textAlign = 'left';
358
+ ctx.textBaseline = 'top';
359
+
360
+ infoLines.forEach((line, index) => {
361
+ if (line.startsWith('>')) {
362
+ ctx.fillStyle = '#aaa';
363
+ } else if (line.startsWith('---')) {
364
+ ctx.fillStyle = '#ccc';
365
+ } else {
366
+ ctx.fillStyle = 'white';
367
+ }
368
+ ctx.fillText(line, startX + boxPadding, boxY + boxPadding + index * lineHeight);
369
+ });
370
+ }
371
+
372
+ draw(ctx: CanvasRenderingContext2D) {
373
+ if (!this.isActive) return;
374
+
375
+ this.isHovered = this.hoverTimer > 0;
376
+ if (this.isHovered) {
377
+ ctx.strokeStyle = FLEET_CONFIG.HOVER_GLOW_COLOR;
378
+ ctx.lineWidth = FLEET_CONFIG.HOVER_GLOW_WIDTH;
379
+ ctx.shadowColor = FLEET_CONFIG.HOVER_GLOW_COLOR;
380
+ ctx.shadowBlur = 10;
381
+ ctx.beginPath();
382
+ ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
383
+ ctx.stroke();
384
+ ctx.shadowBlur = 0;
385
+
386
+ this.drawHoverLabel(ctx);
387
+
388
+ if (DEBUG_CONFIG.SHOW_DEBUG_INFO && DEBUG_CONFIG.SHOW_ENTITY_HOVER_INFO) {
389
+ this.drawDebugInfo(ctx);
390
+ }
391
+ } else {
392
+ ctx.beginPath();
393
+ ctx.strokeStyle = FLEET_CONFIG.RING_COLOR;
394
+ ctx.lineWidth = FLEET_CONFIG.RING_WIDTH;
395
+ ctx.setLineDash([2, 3]);
396
+ ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
397
+ ctx.stroke();
398
+ ctx.setLineDash([]);
399
+ }
400
+
401
+ if (DEBUG_CONFIG.SHOW_DEBUG_INFO && DEBUG_CONFIG.SHOW_FORMATION_TARGET_DOTS && this.state === 'REFORMING') {
402
+ const formationOffsets = this.formation?.getPosition(this.members) || [];
403
+ this.members.forEach((ship, i) => {
404
+ if (i >= formationOffsets.length) return;
405
+
406
+ // Ideal position (RED)
407
+ const offset = formationOffsets[i];
408
+ const rotatedX = offset.x * Math.cos(this.rotation) - offset.y * Math.sin(this.rotation);
409
+ const rotatedY = offset.x * Math.sin(this.rotation) + offset.y * Math.cos(this.rotation);
410
+ const idealX = this.x + rotatedX;
411
+ const idealY = this.y + rotatedY;
412
+
413
+ // Ship's immediate target (YELLOW) - This is a simplification.
414
+ // In the current logic, the idealX/Y IS the target.
415
+ const targetX = idealX;
416
+ const targetY = idealY;
417
+
418
+ // Compare ideal vs target to decide color
419
+ const dx = idealX - targetX;
420
+ const dy = idealY - targetY;
421
+ const distSq = dx * dx + dy * dy;
422
+
423
+ if (distSq < 0.1) {
424
+ // Positions match, draw GREEN
425
+ ctx.fillStyle = 'lime';
426
+ ctx.beginPath();
427
+ ctx.arc(idealX, idealY, 2, 0, Math.PI * 2);
428
+ ctx.fill();
429
+ } else {
430
+ // Positions do not match, draw RED and YELLOW
431
+ ctx.fillStyle = 'red';
432
+ ctx.beginPath();
433
+ ctx.arc(idealX, idealY, 2, 0, Math.PI * 2);
434
+ ctx.fill();
435
+
436
+ ctx.fillStyle = 'yellow';
437
+ ctx.beginPath();
438
+ ctx.arc(targetX, targetY, 2, 0, Math.PI * 2);
439
+ ctx.fill();
440
+ }
441
+ });
442
+ }
443
+
444
+ this.members.forEach(ship => ship.draw(ctx));
445
+ }
446
+ }