@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,760 @@
1
+ /**
2
+ * @fileoverview Defines all starship classes, shapes, and properties, and the Starship class itself.
3
+ */
4
+ import { BoundedBody } from '../base/bounded-body';
5
+ import { DEBUG_CONFIG } from '../config/simulation';
6
+ // --- Ship Shape Definitions ---
7
+ // A collection of reusable path-drawing functions for different ship shapes.
8
+ export const SHAPES = {
9
+ CAPITAL: [
10
+ // Variant 1: Based on Carrier v3 image
11
+ (ctx, r) => {
12
+ ctx.moveTo(0, -r * 1.5); // Nose tip
13
+ ctx.lineTo(r * 0.8, -r * 1.3);
14
+ ctx.lineTo(r * 0.9, r * 0.5);
15
+ ctx.lineTo(r * 1.2, r * 0.8);
16
+ ctx.lineTo(r * 1.1, r * 1.5);
17
+ ctx.lineTo(-r * 1.1, r * 1.5);
18
+ ctx.lineTo(-r * 1.2, r * 0.8);
19
+ ctx.lineTo(-r * 0.9, r * 0.5);
20
+ ctx.lineTo(-r * 0.8, -r * 1.3);
21
+ ctx.closePath();
22
+ },
23
+ // Variant 2: Based on Carrier v2 image (long, bulky freighter/dreadnought)
24
+ (ctx, r) => {
25
+ ctx.moveTo(0, -r * 1.6); // Top antenna area
26
+ ctx.lineTo(r * 0.6, -r * 1.5);
27
+ ctx.lineTo(r * 0.6, -r * 0.8);
28
+ ctx.lineTo(r * 0.9, -r * 0.6);
29
+ ctx.lineTo(r * 0.9, r * 0.8);
30
+ ctx.lineTo(r * 0.6, r * 1.0);
31
+ ctx.lineTo(r * 0.7, r * 1.5);
32
+ ctx.lineTo(-r * 0.7, r * 1.5);
33
+ ctx.lineTo(-r * 0.6, r * 1.0);
34
+ ctx.lineTo(-r * 0.9, r * 0.8);
35
+ ctx.lineTo(-r * 0.9, -r * 0.6);
36
+ ctx.lineTo(-r * 0.6, -r * 0.8);
37
+ ctx.lineTo(-r * 0.6, -r * 1.5);
38
+ ctx.closePath();
39
+ },
40
+ // Variant 3: Based on Carrier v1 image
41
+ (ctx, r) => {
42
+ ctx.moveTo(r * 0.3, -r * 1.6); // Right prong nose
43
+ ctx.lineTo(r * 0.8, -r * 1.5); // Right prong outer edge
44
+ ctx.lineTo(r * 0.9, r * 1.2);
45
+ ctx.lineTo(r * 0.7, r * 1.6); // Right engine
46
+ ctx.lineTo(-r * 0.7, r * 1.6); // Left engine
47
+ ctx.lineTo(-r * 0.9, r * 1.2);
48
+ ctx.lineTo(-r * 0.8, -r * 1.5); // Left prong outer edge
49
+ ctx.lineTo(-r * 0.3, -r * 1.6); // Left prong nose
50
+ ctx.lineTo(-r * 0.2, -r * 1.3);
51
+ ctx.lineTo(r * 0.2, -r * 1.3);
52
+ ctx.closePath();
53
+ },
54
+ ],
55
+ CRUISER: [
56
+ // Variant 1: Sleek cruiser with side pods (third from left)
57
+ (ctx, r) => {
58
+ ctx.moveTo(0, -r * 1.5);
59
+ ctx.lineTo(r * 0.3, -r * 1.2);
60
+ ctx.lineTo(r * 0.4, 0);
61
+ ctx.lineTo(r * 0.8, -0.2 * r);
62
+ ctx.lineTo(r * 0.8, 0.2 * r);
63
+ ctx.lineTo(r * 0.4, r * 0.4);
64
+ ctx.lineTo(r * 0.5, r * 1.4);
65
+ ctx.lineTo(-r * 0.5, r * 1.4);
66
+ ctx.lineTo(-r * 0.4, r * 0.4);
67
+ ctx.lineTo(-r * 0.8, 0.2 * r);
68
+ ctx.lineTo(-r * 0.8, -0.2 * r);
69
+ ctx.lineTo(-r * 0.4, 0);
70
+ ctx.lineTo(-r * 0.3, -r * 1.2);
71
+ ctx.closePath();
72
+ },
73
+ // Variant 2: Cruiser with forward wings (fourth from left)
74
+ (ctx, r) => {
75
+ ctx.moveTo(0, -r * 1.5);
76
+ ctx.lineTo(r * 1.2, r * 0.2);
77
+ ctx.lineTo(r * 0.6, r * 1.3);
78
+ ctx.lineTo(-r * 0.6, r * 1.3);
79
+ ctx.lineTo(-r * 1.2, r * 0.2);
80
+ ctx.closePath();
81
+ },
82
+ ],
83
+ ESCORT: [
84
+ // Variant 1: Compact escort with rear engines (fifth from left)
85
+ (ctx, r) => {
86
+ ctx.moveTo(0, -r * 1.3);
87
+ ctx.lineTo(r * 0.8, -r * 0.5);
88
+ ctx.lineTo(r * 0.8, r * 0.6);
89
+ ctx.lineTo(r * 0.5, r * 1.2);
90
+ ctx.lineTo(-r * 0.5, r * 1.2);
91
+ ctx.lineTo(-r * 0.8, r * 0.6);
92
+ ctx.lineTo(-r * 0.8, -r * 0.5);
93
+ ctx.closePath();
94
+ },
95
+ // Variant 2: Smallest ship with wings (far right)
96
+ (ctx, r) => {
97
+ ctx.moveTo(0, -r * 1.2);
98
+ ctx.lineTo(r * 1, r * 0.1);
99
+ ctx.lineTo(r * 0.4, r * 0.1);
100
+ ctx.lineTo(r * 0.6, r * 1);
101
+ ctx.lineTo(-r * 0.6, r * 1);
102
+ ctx.lineTo(-r * 0.4, r * 0.1);
103
+ ctx.lineTo(-r * 1, r * 0.1);
104
+ ctx.closePath();
105
+ },
106
+ // Variant 3: Inspired by middle-bottom sprites
107
+ (ctx, r) => {
108
+ ctx.moveTo(0, -r);
109
+ ctx.lineTo(r * 0.9, -r * 0.7);
110
+ ctx.lineTo(r * 0.9, r * 0.7);
111
+ ctx.lineTo(r * 0.4, r * 1.1);
112
+ ctx.lineTo(-r * 0.4, r * 1.1);
113
+ ctx.lineTo(-r * 0.9, r * 0.7);
114
+ ctx.lineTo(-r * 0.9, -r * 0.7);
115
+ ctx.closePath();
116
+ },
117
+ ],
118
+ FIGHTER: [
119
+ // Variant 1: Arrowhead fighter
120
+ (ctx, r) => {
121
+ ctx.moveTo(0, -r * 1.4); // Nose
122
+ ctx.lineTo(r * 0.7, r * 0.7); // Right wing tip
123
+ ctx.lineTo(r * 0.4, r * 0.9); // Right wing inner
124
+ ctx.lineTo(-r * 0.4, r * 0.9); // Left wing inner
125
+ ctx.lineTo(-r * 0.7, r * 0.7); // Left wing tip
126
+ ctx.closePath();
127
+ },
128
+ // Variant 2: Inspired by X-Wing style
129
+ (ctx, r) => {
130
+ ctx.moveTo(0, -r * 0.5); // Cockpit
131
+ ctx.lineTo(r * 1.2, -r * 0.2); // Right-front wing
132
+ ctx.lineTo(r * 1.2, r * 0.2);
133
+ ctx.lineTo(0, r * 1.2); // Rear
134
+ ctx.lineTo(-r * 1.2, r * 0.2);
135
+ ctx.lineTo(-r * 1.2, -r * 0.2);
136
+ ctx.closePath();
137
+ },
138
+ ],
139
+ SUPPORT: [
140
+ // Variant 1: Based on Hauler image
141
+ (ctx, r) => {
142
+ const h = r * 1.8;
143
+ const w = r * 0.4;
144
+ const wingW = r * 1.2;
145
+ const tailW = r * 0.8;
146
+ ctx.moveTo(0, -h); // Nose
147
+ ctx.lineTo(w, -h * 0.8);
148
+ ctx.lineTo(w, r * 0.1);
149
+ ctx.lineTo(wingW, r * 0.2); // Wing start
150
+ ctx.lineTo(wingW, r * 0.7); // Wing end
151
+ ctx.lineTo(w, r * 0.8);
152
+ ctx.lineTo(w, h * 0.8);
153
+ ctx.lineTo(tailW, h); // Tail fin outer
154
+ ctx.lineTo(tailW * 0.8, h);
155
+ ctx.lineTo(w * 0.5, h * 0.85);
156
+ ctx.lineTo(-w * 0.5, h * 0.85);
157
+ ctx.lineTo(-tailW * 0.8, h);
158
+ ctx.lineTo(-tailW, h); // Tail fin outer
159
+ ctx.lineTo(-w, h * 0.8);
160
+ ctx.lineTo(-w, r * 0.8);
161
+ ctx.lineTo(-wingW, r * 0.7); // Wing end
162
+ ctx.lineTo(-wingW, r * 0.2); // Wing start
163
+ ctx.lineTo(-w, r * 0.1);
164
+ ctx.lineTo(-w, -h * 0.8);
165
+ ctx.closePath();
166
+ },
167
+ // Variant 2: Based on Transport image
168
+ (ctx, r) => {
169
+ const h = r * 2.0; // Height (length)
170
+ const w = r * 0.6; // Width
171
+ ctx.moveTo(0, -h); // Nose
172
+ ctx.bezierCurveTo(w * 0.8, -h, w, -h * 0.8, w, -h * 0.6);
173
+ ctx.lineTo(w, h * 0.7);
174
+ ctx.lineTo(w * 0.8, h * 0.9);
175
+ ctx.lineTo(w * 0.8, h * 1.1); // right pylon
176
+ ctx.lineTo(w * 0.6, h * 1.1);
177
+ ctx.lineTo(w * 0.6, h * 0.9);
178
+ ctx.lineTo(-w * 0.6, h * 0.9);
179
+ ctx.lineTo(-w * 0.6, h * 1.1); // left pylon
180
+ ctx.lineTo(-w * 0.8, h * 1.1);
181
+ ctx.lineTo(-w * 0.8, h * 0.9);
182
+ ctx.lineTo(-w, h * 0.7);
183
+ ctx.lineTo(-w, -h * 0.6);
184
+ ctx.bezierCurveTo(-w, -h * 0.8, -w * 0.8, -h, 0, -h);
185
+ ctx.closePath();
186
+ },
187
+ // Variant 3: Based on Colony Ship v1 image
188
+ (ctx, r) => {
189
+ const h = r * 2.2;
190
+ const w = r * 0.3;
191
+ const wingW = r * 1.0;
192
+ const engineW = r * 0.6;
193
+ ctx.moveTo(0, -h); // Nose cone tip
194
+ ctx.lineTo(w, -h + r * 0.2);
195
+ ctx.lineTo(w, -h * 0.4);
196
+ ctx.lineTo(wingW, -h * 0.35); // Right solar panel
197
+ ctx.lineTo(wingW, -h * 0.25);
198
+ ctx.lineTo(w, -h * 0.2);
199
+ ctx.lineTo(w, h * 0.7); // Main body
200
+ ctx.lineTo(engineW, h * 0.8); // Right engine block
201
+ ctx.lineTo(engineW, h);
202
+ ctx.lineTo(-engineW, h);
203
+ ctx.lineTo(-engineW, h * 0.8);
204
+ ctx.lineTo(-w, h * 0.7);
205
+ ctx.lineTo(-w, -h * 0.2);
206
+ ctx.lineTo(-wingW, -h * 0.25); // Left solar panel
207
+ ctx.lineTo(-wingW, -h * 0.35);
208
+ ctx.lineTo(-w, -h * 0.4);
209
+ ctx.lineTo(-w, -h + r * 0.2);
210
+ ctx.closePath();
211
+ },
212
+ // Variant 4: Based on Colony Ship v2 image
213
+ (ctx, r) => {
214
+ const h = r * 2.5; // Very tall
215
+ const w = r * 1.5; // Wide base
216
+ ctx.moveTo(0, -h); // Top center
217
+ ctx.lineTo(r * 0.1, -h * 0.95);
218
+ ctx.lineTo(r * 0.1, -h * 0.6); // Upper hull side
219
+ ctx.lineTo(r * 0.4, -h * 0.5); // Side module start
220
+ ctx.lineTo(r * 0.4, -h * 0.2);
221
+ ctx.lineTo(r * 0.2, -h * 0.1); // Back to main hull
222
+ ctx.lineTo(r * 0.2, h * 0.1);
223
+ ctx.lineTo(w * 0.5, h * 0.2); // Lower structure start
224
+ ctx.lineTo(w * 0.5, h * 0.4);
225
+ ctx.lineTo(w * 0.8, h * 0.5);
226
+ ctx.lineTo(w * 0.8, h * 0.7);
227
+ ctx.lineTo(w * 0.4, h * 0.9);
228
+ ctx.lineTo(w * 0.3, h); // Base corner
229
+ ctx.lineTo(-w * 0.3, h);
230
+ ctx.lineTo(-w * 0.4, h * 0.9);
231
+ ctx.lineTo(-w * 0.8, h * 0.7);
232
+ ctx.lineTo(-w * 0.8, h * 0.5);
233
+ ctx.lineTo(-w * 0.5, h * 0.4);
234
+ ctx.lineTo(-w * 0.5, h * 0.2);
235
+ ctx.lineTo(-r * 0.2, h * 0.1);
236
+ ctx.lineTo(-r * 0.2, -h * 0.1);
237
+ ctx.lineTo(-r * 0.4, -h * 0.2);
238
+ ctx.lineTo(-r * 0.4, -h * 0.5);
239
+ ctx.lineTo(-r * 0.1, -h * 0.6);
240
+ ctx.lineTo(-r * 0.1, -h * 0.95);
241
+ ctx.closePath();
242
+ },
243
+ ],
244
+ MINER: [
245
+ // Based on Mining Vessel image
246
+ (ctx, r) => {
247
+ const h = r * 1.8; // Height (length)
248
+ const w = r * 0.8; // Width
249
+ ctx.moveTo(0, -h * 0.8); // Front middle top
250
+ ctx.lineTo(w * 0.2, -h); // Front right tooth tip
251
+ ctx.lineTo(w * 0.3, -h * 0.7);
252
+ ctx.lineTo(w * 0.5, -h * 0.9); // Front right 2nd tooth
253
+ ctx.lineTo(w * 0.6, -h * 0.6);
254
+ ctx.lineTo(w, -h * 0.5); // Right side wing/pod start
255
+ ctx.lineTo(w, h * 0.4);
256
+ ctx.lineTo(w * 1.2, h * 0.5); // Right side wing/pod end
257
+ ctx.lineTo(w * 1.2, h * 0.8);
258
+ ctx.lineTo(w * 0.8, h); // Right rear
259
+ ctx.lineTo(-w * 0.8, h); // Left rear
260
+ ctx.lineTo(-w * 1.2, h * 0.8);
261
+ ctx.lineTo(-w * 1.2, h * 0.5); // Left side wing/pod end
262
+ ctx.lineTo(-w, h * 0.4);
263
+ ctx.lineTo(-w, -h * 0.5); // Left side wing/pod start
264
+ ctx.lineTo(-w * 0.6, -h * 0.6);
265
+ ctx.lineTo(-w * 0.5, -h * 0.9); // Left front 2nd tooth
266
+ ctx.lineTo(-w * 0.3, -h * 0.7);
267
+ ctx.lineTo(-w * 0.2, -h); // Left front tooth tip
268
+ ctx.closePath();
269
+ }
270
+ ],
271
+ UTILITY: [
272
+ // Simple circle for shuttles/runabouts
273
+ (ctx, r) => {
274
+ ctx.arc(0, 0, r, 0, Math.PI * 2);
275
+ },
276
+ ],
277
+ };
278
+ // --- Ship Class Definitions ---
279
+ // A registry of all available ship classes in the simulation.
280
+ // The `shape` property here is a string key to look up variants in SHAPES.
281
+ export const SHIP_CLASS_TEMPLATES = {
282
+ // --- Military Classes ---
283
+ Dreadnought: { name: 'Dreadnought', category: 'Capital', shapeKey: 'CAPITAL', radius: 4.5, color: '#3E4A61' },
284
+ Battleship: { name: 'Battleship', category: 'Capital', shapeKey: 'CAPITAL', radius: 4.0, color: '#4E5A71' },
285
+ Carrier: { name: 'Carrier', category: 'Capital', shapeKey: 'CAPITAL', radius: 4.2, color: '#5E6A81' },
286
+ HeavyCruiser: { name: 'Heavy Cruiser', category: 'Cruiser', shapeKey: 'CRUISER', radius: 3.0, color: '#4A617A' },
287
+ LightCruiser: { name: 'Light Cruiser', category: 'Cruiser', shapeKey: 'CRUISER', radius: 2.5, color: '#5A718A' },
288
+ Battlecruiser: { name: 'Battlecruiser', category: 'Cruiser', shapeKey: 'CRUISER', radius: 3.5, color: '#6A819A' },
289
+ Destroyer: { name: 'Destroyer', category: 'Escort', shapeKey: 'ESCORT', radius: 2.0, color: '#516E5A' },
290
+ Frigate: { name: 'Frigate', category: 'Escort', shapeKey: 'ESCORT', radius: 1.8, color: '#617E6A' },
291
+ Corvette: { name: 'Corvette', category: 'Escort', shapeKey: 'ESCORT', radius: 1.5, color: '#718E7A' },
292
+ Monitor: { name: 'Monitor', category: 'Support', shapeKey: 'SUPPORT', radius: 3.2, color: '#7E7A61' },
293
+ TroopTransport: { name: 'Troop Transport', category: 'Support', shapeKey: 'SUPPORT', radius: 3.0, color: '#8E8A71' },
294
+ Fighter: { name: 'Fighter', category: 'Small Craft', shapeKey: 'FIGHTER', radius: 1.0, color: '#6A5A7A' },
295
+ Bomber: { name: 'Bomber', category: 'Small Craft', shapeKey: 'FIGHTER', radius: 1.2, color: '#7A6A8A' },
296
+ // --- Peaceful Classes ---
297
+ // Exploration & Science
298
+ Explorer: { name: 'Explorer', category: 'Exploration & Science', shapeKey: 'CRUISER', radius: 3.2, color: '#3E5E6B' },
299
+ ScienceFrigate: { name: 'Science Frigate', category: 'Exploration & Science', shapeKey: 'ESCORT', radius: 2.2, color: '#4E6E7B' },
300
+ Pathfinder: { name: 'Pathfinder', category: 'Exploration & Science', shapeKey: 'FIGHTER', radius: 1.3, color: '#5E7E8B' },
301
+ // Commerce & Logistics
302
+ Hauler: { name: 'Hauler', category: 'Commerce & Logistics', shapeKey: 'SUPPORT', radius: 5.0, color: '#555555' },
303
+ ColonyShip: { name: 'Colony Ship', category: 'Commerce & Logistics', shapeKey: 'SUPPORT', radius: 4.0, color: '#666666' },
304
+ Shuttle: { name: 'Shuttle', category: 'Commerce & Logistics', shapeKey: 'UTILITY', radius: 1.0, color: '#DDDDDD' },
305
+ // Resource Extraction
306
+ MiningVessel: { name: 'Mining Vessel', category: 'Resource Extraction', shapeKey: 'MINER', radius: 4.0, color: '#6F554A' },
307
+ GasMiner: { name: 'Gas Miner', category: 'Resource Extraction', shapeKey: 'MINER', radius: 4.5, color: '#7F655A' },
308
+ SalvageShip: { name: 'Salvage Ship', category: 'Resource Extraction', shapeKey: 'MINER', radius: 3.5, color: '#8F756A' },
309
+ // Diplomatic & Civil
310
+ DiplomaticCourier: { name: 'Diplomatic Courier', category: 'Diplomatic & Civil', shapeKey: 'ESCORT', radius: 2.0, color: '#FFFFFF' },
311
+ HospitalShip: { name: 'Hospital Ship', category: 'Diplomatic & Civil', shapeKey: 'SUPPORT', radius: 4.8, color: '#BDECB6' },
312
+ };
313
+ const ACCENT_COLOR_PALETTES = {
314
+ Military: ['#FF4136', '#FF6B6B', '#C44D58'],
315
+ Exploration: ['#39CCCC', '#3D9970', '#20B2AA'],
316
+ Commerce: ['#AAAAAA', '#F0E68C', '#DDDDDD'],
317
+ Extraction: ['#85144b', '#FF851B', '#BDBDBD'],
318
+ Civil: ['#FFFFFF', '#2ECC40', '#0074D9'],
319
+ };
320
+ /**
321
+ * Returns a random ship class with a specific shape variant selected.
322
+ * @returns {ShipClass} A random ship class instance.
323
+ */
324
+ export function getRandomShipClass() {
325
+ const classKeys = Object.keys(SHIP_CLASS_TEMPLATES);
326
+ const randomKey = classKeys[Math.floor(Math.random() * classKeys.length)];
327
+ const template = SHIP_CLASS_TEMPLATES[randomKey];
328
+ const shapeVariants = SHAPES[template.shapeKey];
329
+ const randomShape = shapeVariants[Math.floor(Math.random() * shapeVariants.length)];
330
+ let accentPalette;
331
+ switch (template.category) {
332
+ case 'Capital':
333
+ case 'Cruiser':
334
+ case 'Escort':
335
+ case 'Support': // Military support
336
+ case 'Small Craft':
337
+ accentPalette = ACCENT_COLOR_PALETTES.Military;
338
+ break;
339
+ case 'Exploration & Science':
340
+ accentPalette = ACCENT_COLOR_PALETTES.Exploration;
341
+ break;
342
+ case 'Commerce & Logistics':
343
+ accentPalette = ACCENT_COLOR_PALETTES.Commerce;
344
+ break;
345
+ case 'Resource Extraction':
346
+ accentPalette = ACCENT_COLOR_PALETTES.Extraction;
347
+ break;
348
+ case 'Diplomatic & Civil':
349
+ accentPalette = ACCENT_COLOR_PALETTES.Civil;
350
+ break;
351
+ default:
352
+ accentPalette = ['#FFFFFF'];
353
+ }
354
+ const randomAccent = accentPalette[Math.floor(Math.random() * accentPalette.length)];
355
+ return Object.assign(Object.assign({}, template), { shape: randomShape, accentColor: randomAccent });
356
+ }
357
+ // --- Starship Configuration ---
358
+ export const STARSHIP_CONFIG = {
359
+ COUNT: 30,
360
+ MIN_SPEED: 0.22,
361
+ MAX_SPEED: 0.3,
362
+ ACCELERATION: 0.01,
363
+ DOCKING_DISTANCE: 1,
364
+ DOCK_DURATION_MIN: 120, // 2 seconds in frames
365
+ DOCK_DURATION_VARIANCE: 300, // 2-7 seconds in frames
366
+ TRAIL_LENGTH: 20,
367
+ WARP_CHANCE: 0.05,
368
+ WARP_SPEED_MULTIPLIER: 10,
369
+ WARP_TRAIL_LENGTH_MULTIPLIER: 3,
370
+ FORMATION_PADDING: 10, // Extra padding for the fleet's bounding circle
371
+ FLEE_DISTANCE: 50,
372
+ FLEE_ACCELERATION: 0.05,
373
+ FLEE_DURATION: 180, // 3 seconds in frames
374
+ HOVER_RADIUS_MULTIPLIER: 5,
375
+ HOVER_GLOW_COLOR: 'rgba(255, 255, 255, 0.7)',
376
+ HOVER_GLOW_WIDTH: 1.5,
377
+ HOVER_TIMEOUT: 120, // 2 seconds in frames
378
+ REFORM_ACCELERATION: 0.05,
379
+ REFORM_SPEED_MULTIPLIER: 2.0,
380
+ };
381
+ let starshipCounter = 0;
382
+ export class Starship extends BoundedBody {
383
+ constructor(x, y, debugController) {
384
+ const shipClass = getRandomShipClass();
385
+ super(x, y, shipClass.radius);
386
+ this.isHovered = false;
387
+ this.id = ++starshipCounter;
388
+ this.name = `SS-${this.id}`;
389
+ this.shipClass = shipClass;
390
+ this.radius = shipClass.radius;
391
+ this.boundingRadius = shipClass.radius;
392
+ this.target = null;
393
+ this.rotation = 0;
394
+ this.state = 'IDLE';
395
+ this.stateTimer = this.getNewIdleTime();
396
+ this.currentMaxSpeed = this.getNewMaxSpeed();
397
+ this.hoverRadius = this.boundingRadius * STARSHIP_CONFIG.HOVER_RADIUS_MULTIPLIER;
398
+ this.hoverTimer = 0;
399
+ this.fleet = null;
400
+ this.debugController = debugController;
401
+ this.orbit = null;
402
+ this.orbitAngle = 0;
403
+ this.orbitSpeed = (Math.random() * 0.02 + 0.005) * (Math.random() > 0.5 ? 1 : -1);
404
+ this.warpTotalDistance = 0;
405
+ this.warpDistanceTraveled = 0;
406
+ }
407
+ getNewMaxSpeed() {
408
+ return Math.random() * (STARSHIP_CONFIG.MAX_SPEED - STARSHIP_CONFIG.MIN_SPEED) + STARSHIP_CONFIG.MIN_SPEED;
409
+ }
410
+ isAvailable() {
411
+ return this.state === 'IDLE';
412
+ }
413
+ getNewIdleTime() {
414
+ return Math.random() * STARSHIP_CONFIG.DOCK_DURATION_VARIANCE + STARSHIP_CONFIG.DOCK_DURATION_MIN;
415
+ }
416
+ findNewTarget(starsWithDocks) {
417
+ if (starsWithDocks.length === 0 || this.state === 'IN_FORMATION') {
418
+ this.target = null;
419
+ return;
420
+ }
421
+ this.currentMaxSpeed = this.getNewMaxSpeed();
422
+ const currentTargetParent = this.target ? this.target.parent : null;
423
+ const eligibleStars = starsWithDocks.filter(s => s !== currentTargetParent);
424
+ const targetStars = eligibleStars.length > 0 ? eligibleStars : starsWithDocks;
425
+ if (targetStars.length === 0) {
426
+ this.target = null;
427
+ return;
428
+ }
429
+ const randomStar = targetStars[Math.floor(Math.random() * targetStars.length)];
430
+ if (randomStar.dockingPoints.length > 0) {
431
+ this.target = randomStar.dockingPoints[Math.floor(Math.random() * randomStar.dockingPoints.length)];
432
+ }
433
+ if (Math.random() < STARSHIP_CONFIG.WARP_CHANCE && this.target) {
434
+ this.enterWarpState(this.target);
435
+ }
436
+ else {
437
+ this.state = 'TRAVELING';
438
+ }
439
+ }
440
+ enterWarpState(target) {
441
+ this.state = 'WARPING';
442
+ this.target = target;
443
+ const { x: targetX, y: targetY } = this.target.getAbsolutePosition();
444
+ const dx = targetX - this.x;
445
+ const dy = targetY - this.y;
446
+ this.warpTotalDistance = Math.sqrt(dx * dx + dy * dy);
447
+ this.warpDistanceTraveled = 0;
448
+ const angle = Math.atan2(dy, dx);
449
+ const targetSpeed = this.currentMaxSpeed * STARSHIP_CONFIG.WARP_SPEED_MULTIPLIER;
450
+ this.vx = Math.cos(angle) * targetSpeed;
451
+ this.vy = Math.sin(angle) * targetSpeed;
452
+ this.rotation = Math.atan2(this.vy, this.vx) + Math.PI / 2;
453
+ }
454
+ updateDocking() {
455
+ this.stateTimer--;
456
+ if (this.target) {
457
+ const { x: targetX, y: targetY } = this.target.getAbsolutePosition();
458
+ this.x = targetX;
459
+ this.y = targetY;
460
+ }
461
+ if (this.stateTimer <= 0) {
462
+ this.state = 'IDLE';
463
+ this.stateTimer = this.getNewIdleTime();
464
+ }
465
+ }
466
+ updateOrbiting(deltaTime) {
467
+ this.stateTimer--;
468
+ if (!this.orbit || !this.orbit.parent) {
469
+ this.state = 'IDLE';
470
+ this.stateTimer = this.getNewIdleTime();
471
+ return;
472
+ }
473
+ this.orbitAngle += this.orbitSpeed * deltaTime;
474
+ const parentStar = this.orbit.parent;
475
+ this.x = parentStar.x + Math.cos(this.orbitAngle) * this.orbit.radius;
476
+ this.y = parentStar.y + Math.sin(this.orbitAngle) * this.orbit.radius;
477
+ this.rotation = this.orbitAngle + Math.PI / 2;
478
+ if (this.stateTimer <= 0) {
479
+ this.state = 'IDLE';
480
+ this.stateTimer = this.getNewIdleTime();
481
+ this.orbit = null;
482
+ }
483
+ }
484
+ accelerateToPosition(targetX, targetY) {
485
+ const desiredVectorX = targetX - this.x;
486
+ const desiredVectorY = targetY - this.y;
487
+ const dist = Math.sqrt(desiredVectorX * desiredVectorX + desiredVectorY * desiredVectorY);
488
+ const maxSpeed = this.currentMaxSpeed * STARSHIP_CONFIG.REFORM_SPEED_MULTIPLIER;
489
+ const slowingRadius = 15;
490
+ let desiredSpeed = maxSpeed;
491
+ if (dist < slowingRadius) {
492
+ // A non-linear easing function for deceleration
493
+ const factor = (slowingRadius - dist) / slowingRadius; // 0 at edge, 1 at center
494
+ const easedFactor = 1 - (1 - factor) * (1 - factor); // Ease-in quad
495
+ desiredSpeed = maxSpeed * (1 - easedFactor);
496
+ desiredSpeed = Math.max(desiredSpeed, STARSHIP_CONFIG.MIN_SPEED * 2);
497
+ }
498
+ const desiredVx = (desiredVectorX / dist) * desiredSpeed;
499
+ const desiredVy = (desiredVectorY / dist) * desiredSpeed;
500
+ const steerX = desiredVx - this.vx;
501
+ const steerY = desiredVy - this.vy;
502
+ // Apply the steering force, capped at max acceleration
503
+ this.vx += steerX * STARSHIP_CONFIG.REFORM_ACCELERATION;
504
+ this.vy += steerY * STARSHIP_CONFIG.REFORM_ACCELERATION;
505
+ // Update rotation to match velocity
506
+ if (Math.abs(this.vx) > 0.01 || Math.abs(this.vy) > 0.01) {
507
+ this.rotation = Math.atan2(this.vy, this.vx) + Math.PI / 2;
508
+ }
509
+ this.x += this.vx;
510
+ this.y += this.vy;
511
+ }
512
+ updateTraveling(width, height) {
513
+ if (!this.target) {
514
+ this.state = 'IDLE';
515
+ this.stateTimer = this.getNewIdleTime();
516
+ return;
517
+ }
518
+ const { x: targetX, y: targetY } = this.target.getAbsolutePosition();
519
+ const dx = targetX - this.x;
520
+ const dy = targetY - this.y;
521
+ const dist = Math.sqrt(dx * dx + dy * dy);
522
+ if (dist < STARSHIP_CONFIG.DOCKING_DISTANCE) {
523
+ this.state = 'ORBITING';
524
+ this.stateTimer = this.getNewIdleTime();
525
+ const parentStar = this.target.parent;
526
+ const targetOrbit = parentStar.orbits.find(o => o.dockingPoints.includes(this.target));
527
+ if (targetOrbit) {
528
+ this.orbit = targetOrbit;
529
+ this.orbitAngle = Math.atan2(dy, dx);
530
+ this.orbitSpeed = (Math.random() * 0.02 + 0.005) * (Math.random() > 0.5 ? 1 : -1);
531
+ }
532
+ else {
533
+ this.state = 'DOCKING';
534
+ }
535
+ }
536
+ else {
537
+ const angle = Math.atan2(dy, dx);
538
+ this.vx += Math.cos(angle) * STARSHIP_CONFIG.ACCELERATION;
539
+ this.vy += Math.sin(angle) * STARSHIP_CONFIG.ACCELERATION;
540
+ const speed = Math.sqrt(this.vx * this.vx + this.vy * this.vy);
541
+ if (speed > this.currentMaxSpeed) {
542
+ this.vx = (this.vx / speed) * this.currentMaxSpeed;
543
+ this.vy = (this.vy / speed) * this.currentMaxSpeed;
544
+ }
545
+ this.rotation = Math.atan2(this.vy, this.vx) + Math.PI / 2;
546
+ }
547
+ this.x += this.vx;
548
+ this.y += this.vy;
549
+ }
550
+ updateWarping() {
551
+ if (!this.target) {
552
+ this.state = 'TRAVELING';
553
+ return;
554
+ }
555
+ const { x: targetX, y: targetY } = this.target.getAbsolutePosition();
556
+ const distToTarget = Math.sqrt((targetX - this.x) ** 2 +
557
+ (targetY - this.y) ** 2);
558
+ const travelSpeed = Math.sqrt(this.vx ** 2 + this.vy ** 2);
559
+ this.warpDistanceTraveled += travelSpeed;
560
+ if (this.warpDistanceTraveled >= this.warpTotalDistance || distToTarget < travelSpeed) {
561
+ this.state = 'TRAVELING';
562
+ this.warpTotalDistance = 0;
563
+ this.warpDistanceTraveled = 0;
564
+ const angle = Math.atan2(this.vy, this.vx);
565
+ this.vx = Math.cos(angle) * this.currentMaxSpeed;
566
+ this.vy = Math.sin(angle) * this.currentMaxSpeed;
567
+ return;
568
+ }
569
+ this.x += this.vx;
570
+ this.y += this.vy;
571
+ }
572
+ updateFleeing(mouse) {
573
+ this.stateTimer--;
574
+ if (this.stateTimer <= 0) {
575
+ this.state = 'IDLE';
576
+ this.stateTimer = this.getNewIdleTime();
577
+ return;
578
+ }
579
+ const dx = this.x - mouse.x;
580
+ const dy = this.y - mouse.y;
581
+ const angle = Math.atan2(dy, dx);
582
+ this.vx += Math.cos(angle) * STARSHIP_CONFIG.FLEE_ACCELERATION;
583
+ this.vy += Math.sin(angle) * STARSHIP_CONFIG.FLEE_ACCELERATION;
584
+ const speed = Math.sqrt(this.vx * this.vx + this.vy * this.vy);
585
+ if (speed > this.currentMaxSpeed * 2) { // Flee faster
586
+ this.vx = (this.vx / speed) * this.currentMaxSpeed * 2;
587
+ this.vy = (this.vy / speed) * this.currentMaxSpeed * 2;
588
+ }
589
+ this.rotation = Math.atan2(this.vy, this.vx) + Math.PI / 2;
590
+ this.x += this.vx;
591
+ this.y += this.vy;
592
+ }
593
+ update(ctx, width, height, mouse, deltaTime) {
594
+ if (DEBUG_CONFIG.SHOW_DEBUG_INFO && DEBUG_CONFIG.SHOW_STARSHIP_TARGET_LINE && this.target && this.debugController) {
595
+ this.debugController.registerDirectionalLine(this, this.target.getAbsolutePosition(), { drawLine: true, showDistance: true });
596
+ }
597
+ if (this.state === 'IN_FORMATION') {
598
+ // When in formation, movement is controlled by the Fleet class.
599
+ // But we still need to apply the final velocity to the position.
600
+ this.x += this.vx;
601
+ this.y += this.vy;
602
+ return;
603
+ }
604
+ if (this.state === 'IDLE') {
605
+ this.stateTimer--;
606
+ // Target finding is now handled by the controller
607
+ }
608
+ switch (this.state) {
609
+ case 'DOCKING':
610
+ this.updateDocking();
611
+ break;
612
+ case 'ORBITING':
613
+ this.updateOrbiting(deltaTime);
614
+ return;
615
+ case 'TRAVELING':
616
+ this.updateTraveling(width, height);
617
+ break;
618
+ case 'WARPING':
619
+ this.updateWarping();
620
+ break;
621
+ case 'FLEEING':
622
+ this.updateFleeing(mouse);
623
+ break;
624
+ }
625
+ if (['TRAVELING', 'WARPING', 'FLEEING'].includes(this.state)) {
626
+ if (this.x < -this.boundingRadius)
627
+ this.x = width + this.boundingRadius;
628
+ if (this.x > width + this.boundingRadius)
629
+ this.x = -this.boundingRadius;
630
+ if (this.y < -this.boundingRadius)
631
+ this.y = height + this.boundingRadius;
632
+ if (this.y > height + this.boundingRadius)
633
+ this.y = -this.boundingRadius;
634
+ }
635
+ }
636
+ drawWarpTrail(ctx) {
637
+ const trailLength = this.radius * STARSHIP_CONFIG.WARP_TRAIL_LENGTH_MULTIPLIER * 5;
638
+ const warpProgress = this.warpDistanceTraveled / this.warpTotalDistance;
639
+ let currentTrailLength = 0;
640
+ if (warpProgress < 0.1) {
641
+ currentTrailLength = trailLength * (warpProgress / 0.1);
642
+ }
643
+ else if (warpProgress > 0.9) {
644
+ currentTrailLength = trailLength * ((1 - warpProgress) / 0.1);
645
+ }
646
+ else {
647
+ return;
648
+ }
649
+ if (currentTrailLength <= 0)
650
+ return;
651
+ ctx.save();
652
+ ctx.translate(this.x, this.y);
653
+ ctx.rotate(this.rotation);
654
+ const shapeFn = this.shipClass.shape;
655
+ if (shapeFn) {
656
+ ctx.fillStyle = this.shipClass.color;
657
+ shapeFn(ctx, this.radius);
658
+ ctx.fill();
659
+ }
660
+ const gradient = ctx.createLinearGradient(0, this.radius, 0, -currentTrailLength);
661
+ const color = this.shipClass.color;
662
+ const r = parseInt(color.slice(1, 3), 16);
663
+ const g = parseInt(color.slice(3, 5), 16);
664
+ const b = parseInt(color.slice(5, 7), 16);
665
+ gradient.addColorStop(0, `rgba(${r},${g},${b}, 0.7)`);
666
+ gradient.addColorStop(1, `rgba(${r},${g},${b}, 0)`);
667
+ ctx.fillStyle = gradient;
668
+ ctx.beginPath();
669
+ ctx.moveTo(this.radius, this.radius);
670
+ ctx.lineTo(-this.radius, this.radius);
671
+ ctx.lineTo(0, -currentTrailLength);
672
+ ctx.closePath();
673
+ ctx.fill();
674
+ ctx.restore();
675
+ }
676
+ drawDebugInfo(ctx) {
677
+ const startX = this.x + this.hoverRadius + 15;
678
+ const boxPadding = 10;
679
+ const lineHeight = 14;
680
+ const infoLines = [
681
+ `Name: ${this.name}`,
682
+ `Class: ${this.shipClass.name} (${this.shipClass.category})`,
683
+ `State: ${this.state} (${Math.ceil(this.stateTimer)})`,
684
+ `Target: ${this.target ? `${this.target.parent.name} (DP: ${this.target.id})` : 'None'}`,
685
+ `Position: (${this.x.toFixed(0)}, ${this.y.toFixed(0)})`,
686
+ `Velocity: (${this.vx.toFixed(2)}, ${this.vy.toFixed(2)})`,
687
+ `Max Speed: ${this.currentMaxSpeed.toFixed(2)}`,
688
+ ];
689
+ const boxHeight = infoLines.length * lineHeight + boxPadding * 2;
690
+ const boxWidth = Math.max(...infoLines.map(line => ctx.measureText(line).width)) + boxPadding * 2;
691
+ const boxY = this.y - boxHeight / 2;
692
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
693
+ ctx.strokeStyle = this.shipClass.accentColor;
694
+ ctx.lineWidth = 1;
695
+ ctx.beginPath();
696
+ ctx.roundRect(startX, boxY, boxWidth, boxHeight, 5);
697
+ ctx.fill();
698
+ ctx.stroke();
699
+ ctx.fillStyle = 'white';
700
+ ctx.font = '10px "Roboto Mono", monospace';
701
+ ctx.textAlign = 'left';
702
+ ctx.textBaseline = 'top';
703
+ infoLines.forEach((line, index) => {
704
+ ctx.fillText(line, startX + boxPadding, boxY + boxPadding + index * lineHeight);
705
+ });
706
+ }
707
+ draw(ctx) {
708
+ if (this.state === 'WARPING') {
709
+ this.drawWarpTrail(ctx);
710
+ return; // Skip other drawing logic for warp
711
+ }
712
+ this.isHovered = this.hoverTimer > 0;
713
+ const isInFleet = this.state === 'IN_FORMATION';
714
+ ctx.save();
715
+ ctx.translate(this.x, this.y);
716
+ ctx.rotate(this.rotation);
717
+ const shapeFn = this.shipClass.shape;
718
+ if (this.isHovered && shapeFn) {
719
+ ctx.strokeStyle = STARSHIP_CONFIG.HOVER_GLOW_COLOR;
720
+ ctx.lineWidth = STARSHIP_CONFIG.HOVER_GLOW_WIDTH;
721
+ ctx.shadowColor = STARSHIP_CONFIG.HOVER_GLOW_COLOR;
722
+ ctx.shadowBlur = 10;
723
+ ctx.beginPath();
724
+ shapeFn(ctx, this.radius + 1);
725
+ ctx.stroke();
726
+ ctx.shadowBlur = 0;
727
+ if (!isInFleet && DEBUG_CONFIG.SHOW_DEBUG_INFO && DEBUG_CONFIG.SHOW_ENTITY_HOVER_INFO) {
728
+ // To draw the debug info panel correctly, we need to un-rotate the canvas
729
+ ctx.restore();
730
+ this.drawDebugInfo(ctx);
731
+ ctx.save(); // Re-save context to restore it at the end
732
+ ctx.translate(this.x, this.y);
733
+ ctx.rotate(this.rotation);
734
+ }
735
+ }
736
+ if (shapeFn) {
737
+ ctx.beginPath();
738
+ shapeFn(ctx, this.radius);
739
+ ctx.fillStyle = this.shipClass.color;
740
+ ctx.fill();
741
+ ctx.strokeStyle = this.shipClass.accentColor;
742
+ ctx.lineWidth = 0.5;
743
+ ctx.stroke();
744
+ if (this.fleet) {
745
+ ctx.strokeStyle = this.fleet.clan.color;
746
+ ctx.lineWidth = 0.25;
747
+ ctx.stroke();
748
+ }
749
+ }
750
+ ctx.restore();
751
+ if (DEBUG_CONFIG.SHOW_DEBUG_INFO && !this.fleet) {
752
+ ctx.save();
753
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.5)';
754
+ ctx.font = '8px "Roboto Mono", monospace';
755
+ ctx.textAlign = 'center';
756
+ ctx.fillText(this.state, this.x, this.y + this.radius + 8);
757
+ ctx.restore();
758
+ }
759
+ }
760
+ }