@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.
- package/LICENSE +20 -0
- package/README.md +143 -0
- package/dist/src/base/bounded-body.d.ts +8 -0
- package/dist/src/base/bounded-body.js +10 -0
- package/dist/src/base/celestial-body.d.ts +12 -0
- package/dist/src/base/celestial-body.js +11 -0
- package/dist/src/base/hoverable.d.ts +10 -0
- package/dist/src/base/hoverable.js +4 -0
- package/dist/src/base/massive-body.d.ts +8 -0
- package/dist/src/base/massive-body.js +10 -0
- package/dist/src/config/effects.d.ts +14 -0
- package/dist/src/config/effects.js +14 -0
- package/dist/src/config/grid.d.ts +10 -0
- package/dist/src/config/grid.js +10 -0
- package/dist/src/config/panel.d.ts +26 -0
- package/dist/src/config/panel.js +26 -0
- package/dist/src/config/simulation.d.ts +21 -0
- package/dist/src/config/simulation.js +23 -0
- package/dist/src/controllers/clan-controller.d.ts +7 -0
- package/dist/src/controllers/clan-controller.js +12 -0
- package/dist/src/controllers/debug-controller.d.ts +45 -0
- package/dist/src/controllers/debug-controller.js +82 -0
- package/dist/src/controllers/effects-controller.d.ts +17 -0
- package/dist/src/controllers/effects-controller.js +71 -0
- package/dist/src/controllers/hover-controller.d.ts +11 -0
- package/dist/src/controllers/hover-controller.js +107 -0
- package/dist/src/controllers/layer-controller.d.ts +16 -0
- package/dist/src/controllers/layer-controller.js +64 -0
- package/dist/src/controllers/physics-controller.d.ts +14 -0
- package/dist/src/controllers/physics-controller.js +152 -0
- package/dist/src/controllers/star-controller.d.ts +12 -0
- package/dist/src/controllers/star-controller.js +38 -0
- package/dist/src/controllers/starship-controller.d.ts +17 -0
- package/dist/src/controllers/starship-controller.js +58 -0
- package/dist/src/draw-debug-line.d.ts +9 -0
- package/dist/src/draw-debug-line.js +29 -0
- package/dist/src/entities/black-hole-factory.d.ts +15 -0
- package/dist/src/entities/black-hole-factory.js +23 -0
- package/dist/src/entities/black-hole-shapes.d.ts +9 -0
- package/dist/src/entities/black-hole-shapes.js +224 -0
- package/dist/src/entities/black-hole.d.ts +69 -0
- package/dist/src/entities/black-hole.js +210 -0
- package/dist/src/entities/clan-manager.d.ts +12 -0
- package/dist/src/entities/clan-manager.js +22 -0
- package/dist/src/entities/clans.d.ts +15 -0
- package/dist/src/entities/clans.js +76 -0
- package/dist/src/entities/comet.d.ts +27 -0
- package/dist/src/entities/comet.js +81 -0
- package/dist/src/entities/docking-point.d.ts +20 -0
- package/dist/src/entities/docking-point.js +22 -0
- package/dist/src/entities/fleet.d.ts +45 -0
- package/dist/src/entities/fleet.js +374 -0
- package/dist/src/entities/formations.d.ts +51 -0
- package/dist/src/entities/formations.js +340 -0
- package/dist/src/entities/meteor.d.ts +26 -0
- package/dist/src/entities/meteor.js +48 -0
- package/dist/src/entities/nebula.d.ts +18 -0
- package/dist/src/entities/nebula.js +43 -0
- package/dist/src/entities/orbit.d.ts +23 -0
- package/dist/src/entities/orbit.js +43 -0
- package/dist/src/entities/pulsar.d.ts +18 -0
- package/dist/src/entities/pulsar.js +41 -0
- package/dist/src/entities/ring.d.ts +13 -0
- package/dist/src/entities/ring.js +26 -0
- package/dist/src/entities/ringed-planet.d.ts +21 -0
- package/dist/src/entities/ringed-planet.js +68 -0
- package/dist/src/entities/sector-grid.d.ts +16 -0
- package/dist/src/entities/sector-grid.js +70 -0
- package/dist/src/entities/star-factory.d.ts +29 -0
- package/dist/src/entities/star-factory.js +47 -0
- package/dist/src/entities/star.d.ts +48 -0
- package/dist/src/entities/star.js +167 -0
- package/dist/src/entities/starship-classes.d.ts +0 -0
- package/dist/src/entities/starship-classes.js +2 -0
- package/dist/src/entities/starship.d.ts +91 -0
- package/dist/src/entities/starship.js +760 -0
- package/dist/src/entities/supernova.d.ts +26 -0
- package/dist/src/entities/supernova.js +54 -0
- package/dist/src/index.d.ts +20 -0
- package/dist/src/index.js +19 -0
- package/dist/src/lib/energy-stream.d.ts +5 -0
- package/dist/src/lib/energy-stream.js +98 -0
- package/dist/src/lib/quadtree.d.ts +31 -0
- package/dist/src/lib/quadtree.js +124 -0
- package/dist/src/lib/simplified-stream.d.ts +6 -0
- package/dist/src/lib/simplified-stream.js +19 -0
- package/dist/src/types.d.ts +14 -0
- package/dist/src/types.js +1 -0
- package/dist/src/ui/black-holes-panel.d.ts +2 -0
- package/dist/src/ui/black-holes-panel.js +76 -0
- package/dist/src/ui/clans-panel.d.ts +2 -0
- package/dist/src/ui/clans-panel.js +20 -0
- package/dist/src/ui/debug-panel-controller.d.ts +41 -0
- package/dist/src/ui/debug-panel-controller.js +285 -0
- package/dist/src/ui/fleets-panel.d.ts +2 -0
- package/dist/src/ui/fleets-panel.js +127 -0
- package/dist/src/ui/formations-panel.d.ts +3 -0
- package/dist/src/ui/formations-panel.js +129 -0
- package/dist/src/ui/panel-config.d.ts +26 -0
- package/dist/src/ui/panel-config.js +26 -0
- package/dist/src/ui/ships-panel.d.ts +12 -0
- package/dist/src/ui/ships-panel.js +61 -0
- package/dist/src/ui/stars-panel.d.ts +2 -0
- package/dist/src/ui/stars-panel.js +120 -0
- package/dist/src/where-stars-drift.d.ts +71 -0
- package/dist/src/where-stars-drift.js +440 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +35 -0
- package/src/base/bounded-body.ts +14 -0
- package/src/base/celestial-body.ts +20 -0
- package/src/base/hoverable.ts +11 -0
- package/src/base/massive-body.ts +14 -0
- package/src/config/effects.ts +15 -0
- package/src/config/grid.ts +11 -0
- package/src/config/panel.ts +26 -0
- package/src/config/simulation.ts +25 -0
- package/src/controllers/clan-controller.ts +19 -0
- package/src/controllers/debug-controller.ts +112 -0
- package/src/controllers/effects-controller.ts +86 -0
- package/src/controllers/hover-controller.ts +128 -0
- package/src/controllers/layer-controller.ts +78 -0
- package/src/controllers/physics-controller.ts +173 -0
- package/src/controllers/star-controller.ts +51 -0
- package/src/controllers/starship-controller.ts +76 -0
- package/src/draw-debug-line.ts +37 -0
- package/src/entities/black-hole-factory.ts +28 -0
- package/src/entities/black-hole-shapes.ts +276 -0
- package/src/entities/black-hole.ts +246 -0
- package/src/entities/clan-manager.ts +33 -0
- package/src/entities/clans.ts +98 -0
- package/src/entities/comet.ts +102 -0
- package/src/entities/docking-point.ts +34 -0
- package/src/entities/fleet.ts +446 -0
- package/src/entities/formations.ts +423 -0
- package/src/entities/meteor.ts +59 -0
- package/src/entities/nebula.ts +50 -0
- package/src/entities/orbit.ts +53 -0
- package/src/entities/pulsar.ts +64 -0
- package/src/entities/ring.ts +42 -0
- package/src/entities/ringed-planet.ts +85 -0
- package/src/entities/sector-grid.ts +81 -0
- package/src/entities/star-factory.ts +59 -0
- package/src/entities/star.ts +222 -0
- package/src/entities/starship-classes.ts +1 -0
- package/src/entities/starship.ts +906 -0
- package/src/entities/supernova.ts +75 -0
- package/src/index.ts +24 -0
- package/src/lib/energy-stream.ts +127 -0
- package/src/lib/quadtree.ts +159 -0
- package/src/lib/simplified-stream.ts +28 -0
- package/src/types.ts +16 -0
- package/src/ui/black-holes-panel.ts +91 -0
- package/src/ui/clans-panel.ts +27 -0
- package/src/ui/debug-panel-controller.ts +339 -0
- package/src/ui/fleets-panel.ts +153 -0
- package/src/ui/formations-panel.ts +155 -0
- package/src/ui/panel-config.ts +26 -0
- package/src/ui/ships-panel.ts +85 -0
- package/src/ui/stars-panel.ts +146 -0
- 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
|
+
}
|