@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,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Defines the Comet class for the night sky simulation.
|
|
3
|
+
*/
|
|
4
|
+
import { BoundedBody } from '../base/bounded-body';
|
|
5
|
+
import { STAR_CONFIG } from './star';
|
|
6
|
+
// --- Comet Configuration ---
|
|
7
|
+
export const COMET_CONFIG = {
|
|
8
|
+
COUNT: 1,
|
|
9
|
+
DORMANT_LIFE_MIN: 1600,
|
|
10
|
+
DORMANT_LIFE_VARIANCE: 6400,
|
|
11
|
+
MIN_RADIUS: 1,
|
|
12
|
+
RADIUS_VARIANCE: 1.5,
|
|
13
|
+
SPEED_MIN: 3,
|
|
14
|
+
SPEED_VARIANCE: 4,
|
|
15
|
+
MAX_LIFE_MIN: 150,
|
|
16
|
+
MAX_LIFE_VARIANCE: 200,
|
|
17
|
+
};
|
|
18
|
+
export class Comet extends BoundedBody {
|
|
19
|
+
constructor(width, height) {
|
|
20
|
+
super(0, 0, 0); // Temporary initial values
|
|
21
|
+
this.radius = 0;
|
|
22
|
+
this.color = '';
|
|
23
|
+
this.life = 0;
|
|
24
|
+
this.maxLife = 0;
|
|
25
|
+
this.dormant = true;
|
|
26
|
+
this.dormantLife = Math.random() * COMET_CONFIG.DORMANT_LIFE_VARIANCE + COMET_CONFIG.DORMANT_LIFE_MIN; // Start dormant
|
|
27
|
+
this.reset(width, height);
|
|
28
|
+
}
|
|
29
|
+
reset(width, height) {
|
|
30
|
+
this.x = Math.random() * width;
|
|
31
|
+
this.y = Math.random() > 0.5 ? -50 : height + 50;
|
|
32
|
+
this.radius = Math.random() * COMET_CONFIG.RADIUS_VARIANCE + COMET_CONFIG.MIN_RADIUS;
|
|
33
|
+
this.boundingRadius = this.radius;
|
|
34
|
+
const angle = Math.random() * Math.PI * 0.4 - Math.PI * 0.2;
|
|
35
|
+
const speed = Math.random() * COMET_CONFIG.SPEED_VARIANCE + COMET_CONFIG.SPEED_MIN;
|
|
36
|
+
this.vx = Math.sin(angle) * speed;
|
|
37
|
+
this.vy = Math.cos(angle) * speed * (this.y > height ? -1 : 1);
|
|
38
|
+
this.color = STAR_CONFIG.RARE_STAR_COLORS[Math.floor(Math.random() * STAR_CONFIG.RARE_STAR_COLORS.length)];
|
|
39
|
+
this.maxLife = (Math.random() * COMET_CONFIG.MAX_LIFE_VARIANCE + COMET_CONFIG.MAX_LIFE_MIN);
|
|
40
|
+
this.life = this.maxLife;
|
|
41
|
+
this.dormant = false;
|
|
42
|
+
this.dormantLife = 0;
|
|
43
|
+
}
|
|
44
|
+
update(ctx, width, height) {
|
|
45
|
+
if (this.dormant) {
|
|
46
|
+
this.dormantLife--;
|
|
47
|
+
if (this.dormantLife <= 0) {
|
|
48
|
+
this.reset(width, height);
|
|
49
|
+
}
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
this.x += this.vx;
|
|
53
|
+
this.y += this.vy;
|
|
54
|
+
this.life--;
|
|
55
|
+
if (this.life <= 0) {
|
|
56
|
+
this.dormant = true;
|
|
57
|
+
this.dormantLife = Math.random() * COMET_CONFIG.DORMANT_LIFE_VARIANCE + COMET_CONFIG.DORMANT_LIFE_MIN;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
draw(ctx) {
|
|
61
|
+
if (this.dormant)
|
|
62
|
+
return;
|
|
63
|
+
const tailLength = this.radius * 30 * (this.life / this.maxLife);
|
|
64
|
+
ctx.beginPath();
|
|
65
|
+
ctx.globalAlpha = this.life / this.maxLife * 0.8;
|
|
66
|
+
const gradient = ctx.createLinearGradient(this.x, this.y, this.x - this.vx * tailLength, this.y - this.vy * tailLength);
|
|
67
|
+
gradient.addColorStop(0, this.color);
|
|
68
|
+
gradient.addColorStop(1, "rgba(0,0,0,0)");
|
|
69
|
+
ctx.strokeStyle = gradient;
|
|
70
|
+
ctx.lineWidth = this.radius;
|
|
71
|
+
ctx.lineCap = 'round';
|
|
72
|
+
ctx.moveTo(this.x, this.y);
|
|
73
|
+
ctx.lineTo(this.x - this.vx * (tailLength / 10), this.y - this.vy * (tailLength / 10));
|
|
74
|
+
ctx.stroke();
|
|
75
|
+
ctx.beginPath();
|
|
76
|
+
ctx.fillStyle = this.color;
|
|
77
|
+
ctx.arc(this.x, this.y, this.radius / 2, 0, Math.PI * 2);
|
|
78
|
+
ctx.fill();
|
|
79
|
+
ctx.globalAlpha = 1;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Defines the DockingPoint class for the night sky simulation.
|
|
3
|
+
*/
|
|
4
|
+
import { CelestialBody } from '../base/celestial-body';
|
|
5
|
+
import { Star } from './star';
|
|
6
|
+
export declare class DockingPoint {
|
|
7
|
+
id: number;
|
|
8
|
+
parent: CelestialBody | Star;
|
|
9
|
+
relativeX: number;
|
|
10
|
+
relativeY: number;
|
|
11
|
+
constructor(parent: CelestialBody | Star, relativeX: number, relativeY: number);
|
|
12
|
+
/**
|
|
13
|
+
* Gets the absolute world coordinates of the docking point.
|
|
14
|
+
* @returns {{x: number, y: number}} The absolute position.
|
|
15
|
+
*/
|
|
16
|
+
getAbsolutePosition(): {
|
|
17
|
+
x: number;
|
|
18
|
+
y: number;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Defines the DockingPoint class for the night sky simulation.
|
|
3
|
+
*/
|
|
4
|
+
let dockingPointCounter = 0;
|
|
5
|
+
export class DockingPoint {
|
|
6
|
+
constructor(parent, relativeX, relativeY) {
|
|
7
|
+
this.id = ++dockingPointCounter;
|
|
8
|
+
this.parent = parent;
|
|
9
|
+
this.relativeX = relativeX;
|
|
10
|
+
this.relativeY = relativeY;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Gets the absolute world coordinates of the docking point.
|
|
14
|
+
* @returns {{x: number, y: number}} The absolute position.
|
|
15
|
+
*/
|
|
16
|
+
getAbsolutePosition() {
|
|
17
|
+
return {
|
|
18
|
+
x: this.parent.x + this.relativeX,
|
|
19
|
+
y: this.parent.y + this.relativeY
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Defines the Fleet class for managing groups of starships.
|
|
3
|
+
*/
|
|
4
|
+
import { Starship } from './starship';
|
|
5
|
+
import type { Hoverable } from '../base/hoverable';
|
|
6
|
+
import type { Clan } from './clans';
|
|
7
|
+
import { Formation } from './formations';
|
|
8
|
+
import { BoundedBody } from '../base/bounded-body';
|
|
9
|
+
type FleetState = 'PATROLLING' | 'REFORMING';
|
|
10
|
+
export declare const FLEET_CONFIG: {
|
|
11
|
+
HOVER_GLOW_COLOR: string;
|
|
12
|
+
HOVER_GLOW_WIDTH: number;
|
|
13
|
+
RING_COLOR: string;
|
|
14
|
+
RING_WIDTH: number;
|
|
15
|
+
HOVER_TIMEOUT: number;
|
|
16
|
+
ICON_SIZE: number;
|
|
17
|
+
ICON_MARGIN: number;
|
|
18
|
+
};
|
|
19
|
+
export declare class Fleet extends BoundedBody implements Hoverable {
|
|
20
|
+
id: number;
|
|
21
|
+
name: string;
|
|
22
|
+
members: Starship[];
|
|
23
|
+
size: number;
|
|
24
|
+
isActive: boolean;
|
|
25
|
+
clan: Clan;
|
|
26
|
+
state: FleetState;
|
|
27
|
+
isHovered: boolean;
|
|
28
|
+
hoverTimer: number;
|
|
29
|
+
formation: Formation | null;
|
|
30
|
+
radius: number;
|
|
31
|
+
hoverRadius: number;
|
|
32
|
+
speedMultiplier: number;
|
|
33
|
+
rotation: number;
|
|
34
|
+
targetRotation: number;
|
|
35
|
+
constructor(ships: Starship[], clan: Clan, canvasWidth: number, canvasHeight: number);
|
|
36
|
+
disband(): void;
|
|
37
|
+
private beginReforming;
|
|
38
|
+
private getBoundingBox;
|
|
39
|
+
private updateShipPositions;
|
|
40
|
+
update(ctx: CanvasRenderingContext2D, width: number, height: number): void;
|
|
41
|
+
private drawHoverLabel;
|
|
42
|
+
private drawDebugInfo;
|
|
43
|
+
draw(ctx: CanvasRenderingContext2D): void;
|
|
44
|
+
}
|
|
45
|
+
export {};
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Defines the Fleet class for managing groups of starships.
|
|
3
|
+
*/
|
|
4
|
+
import { STARSHIP_CONFIG } from './starship';
|
|
5
|
+
import { formationRegistry } from './formations';
|
|
6
|
+
import { DEBUG_CONFIG } from '../config/simulation';
|
|
7
|
+
import { BoundedBody } from '../base/bounded-body';
|
|
8
|
+
let fleetCounter = 0;
|
|
9
|
+
const FLEET_NAMES = ["Alpha", "Beta", "Gamma", "Delta", "Epsilon", "Zeta", "Eta", "Theta"];
|
|
10
|
+
// --- Fleet Configuration ---
|
|
11
|
+
export const FLEET_CONFIG = {
|
|
12
|
+
HOVER_GLOW_COLOR: 'rgba(255, 255, 255, 0.5)',
|
|
13
|
+
HOVER_GLOW_WIDTH: 2,
|
|
14
|
+
RING_COLOR: 'rgba(255, 255, 255, 0.2)',
|
|
15
|
+
RING_WIDTH: 0.5,
|
|
16
|
+
HOVER_TIMEOUT: 120, // 2 seconds in frames
|
|
17
|
+
ICON_SIZE: 12,
|
|
18
|
+
ICON_MARGIN: 8,
|
|
19
|
+
};
|
|
20
|
+
export class Fleet extends BoundedBody {
|
|
21
|
+
constructor(ships, clan, canvasWidth, canvasHeight) {
|
|
22
|
+
var _a;
|
|
23
|
+
const formation = formationRegistry.getFormationForFleet(ships);
|
|
24
|
+
const formationOffsets = formation ? formation.getPosition(ships) : [];
|
|
25
|
+
const maxOffset = formationOffsets.reduce((max, offset) => {
|
|
26
|
+
const dist = Math.sqrt(offset.x * offset.x + offset.y * offset.y);
|
|
27
|
+
return Math.max(max, dist);
|
|
28
|
+
}, 0);
|
|
29
|
+
const radius = maxOffset + STARSHIP_CONFIG.FORMATION_PADDING;
|
|
30
|
+
const safePadding = radius * 1.2;
|
|
31
|
+
const x = Math.random() * (canvasWidth - 2 * safePadding) + safePadding;
|
|
32
|
+
const y = Math.random() * (canvasHeight - 2 * safePadding) + safePadding;
|
|
33
|
+
super(x, y, radius);
|
|
34
|
+
this.isHovered = false;
|
|
35
|
+
this.boundingRadius = radius;
|
|
36
|
+
this.id = ++fleetCounter;
|
|
37
|
+
this.name = `${FLEET_NAMES[this.id % FLEET_NAMES.length]} Fleet #${this.id}`;
|
|
38
|
+
this.members = ships;
|
|
39
|
+
this.size = ships.length;
|
|
40
|
+
this.isActive = true;
|
|
41
|
+
this.clan = clan;
|
|
42
|
+
this.state = 'PATROLLING';
|
|
43
|
+
this.hoverTimer = 0;
|
|
44
|
+
this.formation = formation;
|
|
45
|
+
if (!this.formation || formationOffsets.length === 0) {
|
|
46
|
+
console.warn(`Fleet ${this.name} (${this.id}) created but got no valid formation or positions. Fleet will be inactive.`);
|
|
47
|
+
this.isActive = false;
|
|
48
|
+
}
|
|
49
|
+
this.radius = radius;
|
|
50
|
+
this.hoverRadius = this.radius * 1.2; // 20% larger hover area
|
|
51
|
+
this.speedMultiplier = Math.random() * 0.2 + 0.8; // Speed between 80% and 100% of normal
|
|
52
|
+
const baseSpeed = STARSHIP_CONFIG.MAX_SPEED * this.speedMultiplier;
|
|
53
|
+
const angle = Math.random() * 2 * Math.PI;
|
|
54
|
+
this.vx = Math.cos(angle) * baseSpeed;
|
|
55
|
+
this.vy = Math.sin(angle) * baseSpeed;
|
|
56
|
+
this.rotation = Math.atan2(this.vy, this.vx); // Initial rotation
|
|
57
|
+
this.targetRotation = this.rotation;
|
|
58
|
+
const rotatedOffsets = ((_a = this.formation) === null || _a === void 0 ? void 0 : _a.getPosition(this.members).map(offset => {
|
|
59
|
+
const rotatedX = offset.x * Math.cos(this.rotation) - offset.y * Math.sin(this.rotation);
|
|
60
|
+
const rotatedY = offset.x * Math.sin(this.rotation) + offset.y * Math.cos(this.rotation);
|
|
61
|
+
return { x: rotatedX, y: rotatedY };
|
|
62
|
+
})) || [];
|
|
63
|
+
this.members.forEach((ship, i) => {
|
|
64
|
+
ship.fleet = this;
|
|
65
|
+
ship.state = 'IN_FORMATION';
|
|
66
|
+
if (i >= rotatedOffsets.length)
|
|
67
|
+
return;
|
|
68
|
+
const offset = rotatedOffsets[i];
|
|
69
|
+
ship.x = this.x + offset.x;
|
|
70
|
+
ship.y = this.y + offset.y;
|
|
71
|
+
ship.rotation = this.rotation + Math.PI / 2;
|
|
72
|
+
});
|
|
73
|
+
// Register with the clan
|
|
74
|
+
this.clan.fleets.push(this);
|
|
75
|
+
this.clan.starships.push(...this.members);
|
|
76
|
+
this.clan.fleetsCount = this.clan.fleets.length;
|
|
77
|
+
this.clan.starshipsCount = this.clan.starships.length;
|
|
78
|
+
}
|
|
79
|
+
disband() {
|
|
80
|
+
this.isActive = false;
|
|
81
|
+
// Unregister from the clan
|
|
82
|
+
const fleetIndex = this.clan.fleets.indexOf(this);
|
|
83
|
+
if (fleetIndex > -1) {
|
|
84
|
+
this.clan.fleets.splice(fleetIndex, 1);
|
|
85
|
+
}
|
|
86
|
+
this.clan.fleetsCount = this.clan.fleets.length;
|
|
87
|
+
this.members.forEach(ship => {
|
|
88
|
+
ship.fleet = null;
|
|
89
|
+
ship.state = 'IDLE';
|
|
90
|
+
const shipIndex = this.clan.starships.indexOf(ship);
|
|
91
|
+
if (shipIndex > -1) {
|
|
92
|
+
this.clan.starships.splice(shipIndex, 1);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
this.clan.starshipsCount = this.clan.starships.length;
|
|
96
|
+
}
|
|
97
|
+
beginReforming() {
|
|
98
|
+
if (!this.formation)
|
|
99
|
+
return;
|
|
100
|
+
this.state = 'REFORMING';
|
|
101
|
+
this.targetRotation = Math.atan2(this.vy, this.vx);
|
|
102
|
+
const formationOffsets = this.formation.getPosition(this.members);
|
|
103
|
+
const sortedShips = [];
|
|
104
|
+
if (this.formation.type === 'Strict') {
|
|
105
|
+
const strictFormation = this.formation;
|
|
106
|
+
const availableShips = [...this.members];
|
|
107
|
+
strictFormation.composition.forEach(role => {
|
|
108
|
+
for (let i = 0; i < role.count; i++) {
|
|
109
|
+
const shipIndex = availableShips.findIndex(s => s.shipClass.name === role.shipClassName);
|
|
110
|
+
if (shipIndex !== -1) {
|
|
111
|
+
sortedShips.push(availableShips.splice(shipIndex, 1)[0]);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
sortedShips.push(...availableShips);
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
const unassignedShips = [...this.members];
|
|
119
|
+
const unassignedSlots = [...formationOffsets];
|
|
120
|
+
while (unassignedShips.length > 0 && unassignedSlots.length > 0) {
|
|
121
|
+
let bestShipIndex = -1;
|
|
122
|
+
let bestSlotIndex = -1;
|
|
123
|
+
let closestDistSq = Infinity;
|
|
124
|
+
for (let i = 0; i < unassignedShips.length; i++) {
|
|
125
|
+
for (let j = 0; j < unassignedSlots.length; j++) {
|
|
126
|
+
const ship = unassignedShips[i];
|
|
127
|
+
const slot = unassignedSlots[j];
|
|
128
|
+
const distSq = (ship.x - (this.x + slot.x)) ** 2 + (ship.y - (this.y + slot.y)) ** 2;
|
|
129
|
+
if (distSq < closestDistSq) {
|
|
130
|
+
closestDistSq = distSq;
|
|
131
|
+
bestShipIndex = i;
|
|
132
|
+
bestSlotIndex = j;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (bestShipIndex !== -1 && bestSlotIndex !== -1) {
|
|
137
|
+
sortedShips.push(unassignedShips.splice(bestShipIndex, 1)[0]);
|
|
138
|
+
unassignedSlots.splice(bestSlotIndex, 1);
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
sortedShips.push(...unassignedShips);
|
|
145
|
+
}
|
|
146
|
+
this.members = sortedShips;
|
|
147
|
+
}
|
|
148
|
+
getBoundingBox() {
|
|
149
|
+
if (this.members.length === 0) {
|
|
150
|
+
return { minX: this.x, minY: this.y, maxX: this.x, maxY: this.y };
|
|
151
|
+
}
|
|
152
|
+
return this.members.reduce((acc, ship) => ({
|
|
153
|
+
minX: Math.min(acc.minX, ship.x - ship.boundingRadius),
|
|
154
|
+
minY: Math.min(acc.minY, ship.y - ship.boundingRadius),
|
|
155
|
+
maxX: Math.max(acc.maxX, ship.x + ship.boundingRadius),
|
|
156
|
+
maxY: Math.max(acc.maxY, ship.y + ship.boundingRadius),
|
|
157
|
+
}), {
|
|
158
|
+
minX: Infinity,
|
|
159
|
+
minY: Infinity,
|
|
160
|
+
maxX: -Infinity,
|
|
161
|
+
maxY: -Infinity,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
updateShipPositions() {
|
|
165
|
+
if (!this.formation || this.members.length === 0)
|
|
166
|
+
return true;
|
|
167
|
+
let allShipsInPosition = true;
|
|
168
|
+
const formationOffsets = this.formation.getPosition(this.members);
|
|
169
|
+
this.members.forEach((ship, i) => {
|
|
170
|
+
if (i >= formationOffsets.length)
|
|
171
|
+
return;
|
|
172
|
+
let offset = formationOffsets[i];
|
|
173
|
+
const rotatedX = offset.x * Math.cos(this.rotation) - offset.y * Math.sin(this.rotation);
|
|
174
|
+
const rotatedY = offset.x * Math.sin(this.rotation) + offset.y * Math.cos(this.rotation);
|
|
175
|
+
const targetX = this.x + rotatedX;
|
|
176
|
+
const targetY = this.y + rotatedY;
|
|
177
|
+
ship.accelerateToPosition(targetX, targetY);
|
|
178
|
+
const dx = targetX - ship.x;
|
|
179
|
+
const dy = targetY - ship.y;
|
|
180
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
181
|
+
if (dist > 1) {
|
|
182
|
+
allShipsInPosition = false;
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
return allShipsInPosition;
|
|
186
|
+
}
|
|
187
|
+
update(ctx, width, height) {
|
|
188
|
+
if (!this.isActive || !this.formation)
|
|
189
|
+
return;
|
|
190
|
+
this.x += this.vx;
|
|
191
|
+
this.y += this.vy;
|
|
192
|
+
const padding = this.radius * 0.2;
|
|
193
|
+
let bounced = false;
|
|
194
|
+
const { minX, minY, maxX, maxY } = this.getBoundingBox();
|
|
195
|
+
let changed = false;
|
|
196
|
+
if (minX < padding && this.vx < 0) {
|
|
197
|
+
this.vx *= -1;
|
|
198
|
+
changed = true;
|
|
199
|
+
}
|
|
200
|
+
if (maxX > width - padding && this.vx > 0) {
|
|
201
|
+
this.vx *= -1;
|
|
202
|
+
changed = true;
|
|
203
|
+
}
|
|
204
|
+
if (minY < padding && this.vy < 0) {
|
|
205
|
+
this.vy *= -1;
|
|
206
|
+
changed = true;
|
|
207
|
+
}
|
|
208
|
+
if (maxY > height - padding && this.vy > 0) {
|
|
209
|
+
this.vy *= -1;
|
|
210
|
+
changed = true;
|
|
211
|
+
}
|
|
212
|
+
if (changed) {
|
|
213
|
+
bounced = true;
|
|
214
|
+
}
|
|
215
|
+
if (bounced && this.state !== 'REFORMING') {
|
|
216
|
+
this.beginReforming();
|
|
217
|
+
}
|
|
218
|
+
if (this.state === 'REFORMING') {
|
|
219
|
+
let angleDiff = this.targetRotation - this.rotation;
|
|
220
|
+
while (angleDiff > Math.PI)
|
|
221
|
+
angleDiff -= 2 * Math.PI;
|
|
222
|
+
while (angleDiff < -Math.PI)
|
|
223
|
+
angleDiff += 2 * Math.PI;
|
|
224
|
+
this.rotation += angleDiff * 0.05;
|
|
225
|
+
const areAllShipsInPosition = this.updateShipPositions();
|
|
226
|
+
const isRotationComplete = Math.abs(angleDiff) < 0.01;
|
|
227
|
+
if (isRotationComplete && areAllShipsInPosition) {
|
|
228
|
+
this.state = 'PATROLLING';
|
|
229
|
+
this.rotation = this.targetRotation;
|
|
230
|
+
const baseSpeed = STARSHIP_CONFIG.MAX_SPEED * this.speedMultiplier * (Math.random() * 0.2 + 0.9);
|
|
231
|
+
this.vx = Math.cos(this.rotation) * baseSpeed;
|
|
232
|
+
this.vy = Math.sin(this.rotation) * baseSpeed;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
else if (this.state === 'PATROLLING') {
|
|
236
|
+
this.rotation = Math.atan2(this.vy, this.vx);
|
|
237
|
+
const currentOffsets = this.formation.getPosition(this.members);
|
|
238
|
+
this.members.forEach((ship, i) => {
|
|
239
|
+
if (i >= currentOffsets.length)
|
|
240
|
+
return;
|
|
241
|
+
const offset = currentOffsets[i];
|
|
242
|
+
const rotatedX = offset.x * Math.cos(this.rotation) - offset.y * Math.sin(this.rotation);
|
|
243
|
+
const rotatedY = offset.x * Math.sin(this.rotation) + offset.y * Math.cos(this.rotation);
|
|
244
|
+
ship.x = this.x + rotatedX;
|
|
245
|
+
ship.y = this.y + rotatedY;
|
|
246
|
+
ship.vx = this.vx;
|
|
247
|
+
ship.vy = this.vy;
|
|
248
|
+
ship.rotation = this.rotation + Math.PI / 2;
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
drawHoverLabel(ctx) {
|
|
253
|
+
const iconY = this.y - this.radius - FLEET_CONFIG.ICON_MARGIN - FLEET_CONFIG.ICON_SIZE;
|
|
254
|
+
ctx.save();
|
|
255
|
+
ctx.translate(this.x, iconY);
|
|
256
|
+
ctx.fillStyle = this.clan.color;
|
|
257
|
+
ctx.strokeStyle = this.clan.color;
|
|
258
|
+
ctx.lineWidth = 1.5;
|
|
259
|
+
this.clan.icon(ctx, FLEET_CONFIG.ICON_SIZE);
|
|
260
|
+
ctx.restore();
|
|
261
|
+
}
|
|
262
|
+
drawDebugInfo(ctx) {
|
|
263
|
+
const startX = this.x + this.radius + 15;
|
|
264
|
+
let startY = this.y - (this.members.length / 2 * 12);
|
|
265
|
+
const boxPadding = 10;
|
|
266
|
+
const lineHeight = 14;
|
|
267
|
+
const infoLines = [
|
|
268
|
+
`Fleet: ${this.name} (ID: ${this.id})`,
|
|
269
|
+
`Clan: ${this.clan.name}`,
|
|
270
|
+
`Ships: ${this.members.length}`,
|
|
271
|
+
`Formation: ${this.formation ? this.formation.name : 'None'}`,
|
|
272
|
+
`State: ${this.state}`,
|
|
273
|
+
'--- Roster ---',
|
|
274
|
+
...this.members.map(ship => `> ${ship.name} (${ship.shipClass.name})`),
|
|
275
|
+
];
|
|
276
|
+
const boxHeight = infoLines.length * lineHeight + boxPadding * 2;
|
|
277
|
+
const boxWidth = Math.max(...infoLines.map(line => ctx.measureText(line).width)) + boxPadding * 2;
|
|
278
|
+
const boxY = this.y - boxHeight / 2;
|
|
279
|
+
// Draw background box
|
|
280
|
+
ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
|
|
281
|
+
ctx.strokeStyle = this.clan.color;
|
|
282
|
+
ctx.lineWidth = 1;
|
|
283
|
+
ctx.beginPath();
|
|
284
|
+
ctx.roundRect(startX, boxY, boxWidth, boxHeight, 5);
|
|
285
|
+
ctx.fill();
|
|
286
|
+
ctx.stroke();
|
|
287
|
+
// Draw text
|
|
288
|
+
ctx.fillStyle = 'white';
|
|
289
|
+
ctx.font = '10px "Roboto Mono", monospace';
|
|
290
|
+
ctx.textAlign = 'left';
|
|
291
|
+
ctx.textBaseline = 'top';
|
|
292
|
+
infoLines.forEach((line, index) => {
|
|
293
|
+
if (line.startsWith('>')) {
|
|
294
|
+
ctx.fillStyle = '#aaa';
|
|
295
|
+
}
|
|
296
|
+
else if (line.startsWith('---')) {
|
|
297
|
+
ctx.fillStyle = '#ccc';
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
ctx.fillStyle = 'white';
|
|
301
|
+
}
|
|
302
|
+
ctx.fillText(line, startX + boxPadding, boxY + boxPadding + index * lineHeight);
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
draw(ctx) {
|
|
306
|
+
var _a;
|
|
307
|
+
if (!this.isActive)
|
|
308
|
+
return;
|
|
309
|
+
this.isHovered = this.hoverTimer > 0;
|
|
310
|
+
if (this.isHovered) {
|
|
311
|
+
ctx.strokeStyle = FLEET_CONFIG.HOVER_GLOW_COLOR;
|
|
312
|
+
ctx.lineWidth = FLEET_CONFIG.HOVER_GLOW_WIDTH;
|
|
313
|
+
ctx.shadowColor = FLEET_CONFIG.HOVER_GLOW_COLOR;
|
|
314
|
+
ctx.shadowBlur = 10;
|
|
315
|
+
ctx.beginPath();
|
|
316
|
+
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
|
|
317
|
+
ctx.stroke();
|
|
318
|
+
ctx.shadowBlur = 0;
|
|
319
|
+
this.drawHoverLabel(ctx);
|
|
320
|
+
if (DEBUG_CONFIG.SHOW_DEBUG_INFO && DEBUG_CONFIG.SHOW_ENTITY_HOVER_INFO) {
|
|
321
|
+
this.drawDebugInfo(ctx);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
ctx.beginPath();
|
|
326
|
+
ctx.strokeStyle = FLEET_CONFIG.RING_COLOR;
|
|
327
|
+
ctx.lineWidth = FLEET_CONFIG.RING_WIDTH;
|
|
328
|
+
ctx.setLineDash([2, 3]);
|
|
329
|
+
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
|
|
330
|
+
ctx.stroke();
|
|
331
|
+
ctx.setLineDash([]);
|
|
332
|
+
}
|
|
333
|
+
if (DEBUG_CONFIG.SHOW_DEBUG_INFO && DEBUG_CONFIG.SHOW_FORMATION_TARGET_DOTS && this.state === 'REFORMING') {
|
|
334
|
+
const formationOffsets = ((_a = this.formation) === null || _a === void 0 ? void 0 : _a.getPosition(this.members)) || [];
|
|
335
|
+
this.members.forEach((ship, i) => {
|
|
336
|
+
if (i >= formationOffsets.length)
|
|
337
|
+
return;
|
|
338
|
+
// Ideal position (RED)
|
|
339
|
+
const offset = formationOffsets[i];
|
|
340
|
+
const rotatedX = offset.x * Math.cos(this.rotation) - offset.y * Math.sin(this.rotation);
|
|
341
|
+
const rotatedY = offset.x * Math.sin(this.rotation) + offset.y * Math.cos(this.rotation);
|
|
342
|
+
const idealX = this.x + rotatedX;
|
|
343
|
+
const idealY = this.y + rotatedY;
|
|
344
|
+
// Ship's immediate target (YELLOW) - This is a simplification.
|
|
345
|
+
// In the current logic, the idealX/Y IS the target.
|
|
346
|
+
const targetX = idealX;
|
|
347
|
+
const targetY = idealY;
|
|
348
|
+
// Compare ideal vs target to decide color
|
|
349
|
+
const dx = idealX - targetX;
|
|
350
|
+
const dy = idealY - targetY;
|
|
351
|
+
const distSq = dx * dx + dy * dy;
|
|
352
|
+
if (distSq < 0.1) {
|
|
353
|
+
// Positions match, draw GREEN
|
|
354
|
+
ctx.fillStyle = 'lime';
|
|
355
|
+
ctx.beginPath();
|
|
356
|
+
ctx.arc(idealX, idealY, 2, 0, Math.PI * 2);
|
|
357
|
+
ctx.fill();
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
// Positions do not match, draw RED and YELLOW
|
|
361
|
+
ctx.fillStyle = 'red';
|
|
362
|
+
ctx.beginPath();
|
|
363
|
+
ctx.arc(idealX, idealY, 2, 0, Math.PI * 2);
|
|
364
|
+
ctx.fill();
|
|
365
|
+
ctx.fillStyle = 'yellow';
|
|
366
|
+
ctx.beginPath();
|
|
367
|
+
ctx.arc(targetX, targetY, 2, 0, Math.PI * 2);
|
|
368
|
+
ctx.fill();
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
this.members.forEach(ship => ship.draw(ctx));
|
|
373
|
+
}
|
|
374
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Defines all fleet formations and a registry to manage them.
|
|
3
|
+
*/
|
|
4
|
+
import type { Starship } from './starship';
|
|
5
|
+
type FormationCategory = 'Military' | 'Mixed';
|
|
6
|
+
/**
|
|
7
|
+
* Common properties for all formations.
|
|
8
|
+
*/
|
|
9
|
+
interface IFormation {
|
|
10
|
+
name: string;
|
|
11
|
+
category: FormationCategory;
|
|
12
|
+
description: string;
|
|
13
|
+
getPosition: (ships: Starship[]) => {
|
|
14
|
+
x: number;
|
|
15
|
+
y: number;
|
|
16
|
+
}[];
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Strict formations are role-dependent and class-specific.
|
|
20
|
+
* They require a specific composition of ship classes to be effective.
|
|
21
|
+
*/
|
|
22
|
+
export interface StrictFormation extends IFormation {
|
|
23
|
+
type: 'Strict';
|
|
24
|
+
composition: {
|
|
25
|
+
shipClassName: string;
|
|
26
|
+
count: number;
|
|
27
|
+
}[];
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Free formations are geometry-based and adaptable to any ship composition.
|
|
31
|
+
* They prioritize spatial arrangement over specific ship roles.
|
|
32
|
+
*/
|
|
33
|
+
export interface FreeFormation extends IFormation {
|
|
34
|
+
type: 'Free';
|
|
35
|
+
}
|
|
36
|
+
export type Formation = StrictFormation | FreeFormation;
|
|
37
|
+
declare class FormationRegistry {
|
|
38
|
+
private formations;
|
|
39
|
+
private strictFormations;
|
|
40
|
+
private freeFormations;
|
|
41
|
+
constructor();
|
|
42
|
+
/**
|
|
43
|
+
* Finds a suitable formation for a given fleet based on its composition.
|
|
44
|
+
* @param fleetShips The list of starships in the fleet.
|
|
45
|
+
* @returns A formation, or null if no suitable formation is found.
|
|
46
|
+
*/
|
|
47
|
+
getFormationForFleet(fleetShips: Starship[]): Formation | null;
|
|
48
|
+
getAllFormations(): Formation[];
|
|
49
|
+
}
|
|
50
|
+
export declare const formationRegistry: FormationRegistry;
|
|
51
|
+
export {};
|