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