diff --git a/src/components/editor/AsteroidListEditor.svelte b/src/components/editor/AsteroidListEditor.svelte
index 54cf81f..e4e8989 100644
--- a/src/components/editor/AsteroidListEditor.svelte
+++ b/src/components/editor/AsteroidListEditor.svelte
@@ -1,12 +1,25 @@
@@ -170,8 +178,12 @@
{:else if activeTab === 'starfield'}
+ {:else if activeTab === 'targets'}
+ {ensureTargets()}
+
{:else if activeTab === 'asteroids'}
-
+ {ensureTargets()}
+
{:else if activeTab === 'planets'}
{/if}
diff --git a/src/components/editor/TargetListEditor.svelte b/src/components/editor/TargetListEditor.svelte
new file mode 100644
index 0000000..4b90f85
--- /dev/null
+++ b/src/components/editor/TargetListEditor.svelte
@@ -0,0 +1,185 @@
+
+
+
+ Targets are positions that asteroids can orbit or move toward.
+
+
+
+ {#if editingTarget !== null}
+
+ {/if}
+
+ {#if targets.length === 0}
+ No targets configured. Add targets for asteroids to orbit or move toward.
+ {:else}
+
+
+
+
+ | Name |
+ ID |
+ Position |
+ Actions |
+
+
+
+ {#each targets as target, index}
+
+ | {target.name} |
+ {target.id} |
+ {formatPosition(target.position)} |
+
+
+
+ |
+
+ {/each}
+
+
+
+ {/if}
+
+
+
diff --git a/src/environment/asteroids/rockFactory.ts b/src/environment/asteroids/rockFactory.ts
index c1718f3..866db85 100644
--- a/src/environment/asteroids/rockFactory.ts
+++ b/src/environment/asteroids/rockFactory.ts
@@ -41,6 +41,8 @@ interface RockConfig {
angularVelocity: Vector3;
scoreObservable: Observable
;
useOrbitConstraint: boolean;
+ targetPosition?: Vector3;
+ targetMode?: 'orbit' | 'moveToward';
}
export class RockFactory {
@@ -166,7 +168,9 @@ export class RockFactory {
angularVelocity: Vector3,
scoreObservable: Observable,
useOrbitConstraint: boolean = true,
- hidden: boolean = false
+ hidden: boolean = false,
+ targetPosition?: Vector3,
+ targetMode?: 'orbit' | 'moveToward'
): Rock {
if (!this._asteroidMesh) {
throw new Error('[RockFactory] Asteroid mesh not loaded. Call initMesh() first.');
@@ -188,11 +192,13 @@ export class RockFactory {
linearVelocity,
angularVelocity,
scoreObservable,
- useOrbitConstraint
+ useOrbitConstraint,
+ targetPosition,
+ targetMode
};
this._createdRocks.set(rock.id, { mesh: rock, config });
- log.debug(`[RockFactory] Created rock mesh ${rock.id} (hidden: ${hidden})`);
+ log.debug(`[RockFactory] Created rock mesh ${rock.id} (hidden: ${hidden}, target: ${targetMode || 'none'})`);
return new Rock(rock);
}
@@ -215,8 +221,11 @@ export class RockFactory {
body.setMotionType(PhysicsMotionType.DYNAMIC);
body.setCollisionCallbackEnabled(true);
- // Apply orbit constraint if enabled
- if (config.useOrbitConstraint && this._orbitCenter) {
+ // Handle target-based physics
+ if (config.targetPosition && config.targetMode) {
+ this.applyTargetPhysics(body, config);
+ } else if (config.useOrbitConstraint && this._orbitCenter) {
+ // Legacy: orbit around origin if no specific target
const constraint = new DistanceConstraint(
Vector3.Distance(config.position, this._orbitCenter.body.transformNode.position),
DefaultScene.MainScene
@@ -230,7 +239,10 @@ export class RockFactory {
physicsPlugin.setActivationControl(body, PhysicsActivationControl.ALWAYS_ACTIVE);
}
- body.setLinearVelocity(config.linearVelocity);
+ // Apply velocity (may be modified by applyTargetPhysics for moveToward mode)
+ if (!(config.targetPosition && config.targetMode === 'moveToward')) {
+ body.setLinearVelocity(config.linearVelocity);
+ }
body.setAngularVelocity(config.angularVelocity);
// Setup collision handler
@@ -239,6 +251,45 @@ export class RockFactory {
log.debug(`[RockFactory] Physics initialized for ${rock.id}`);
}
+ /**
+ * Apply target-based physics (orbit or moveToward)
+ */
+ private static applyTargetPhysics(body: PhysicsBody, config: RockConfig): void {
+ if (!config.targetPosition) return;
+
+ if (config.targetMode === 'orbit') {
+ // Create distance constraint to target position
+ // We need a static body at the target position for the constraint
+ const targetNode = new TransformNode(`target-${body.transformNode.id}`, DefaultScene.MainScene);
+ targetNode.position = config.targetPosition;
+ const targetBody = new PhysicsAggregate(
+ targetNode, PhysicsShapeType.SPHERE,
+ { radius: 0.1, mass: 0 },
+ DefaultScene.MainScene
+ );
+ targetBody.body.setMotionType(PhysicsMotionType.STATIC);
+
+ const distance = Vector3.Distance(config.position, config.targetPosition);
+ const constraint = new DistanceConstraint(distance, DefaultScene.MainScene);
+ body.addConstraint(targetBody.body, constraint);
+
+ // Apply original velocity for orbiting
+ body.setLinearVelocity(config.linearVelocity);
+ } else if (config.targetMode === 'moveToward') {
+ // Calculate speed as sum of absolute velocity components
+ const speed = Math.abs(config.linearVelocity.x) +
+ Math.abs(config.linearVelocity.y) +
+ Math.abs(config.linearVelocity.z);
+
+ // Direction toward target
+ const direction = config.targetPosition.subtract(config.position).normalize();
+
+ // Final velocity = direction * speed
+ const velocity = direction.scale(speed);
+ body.setLinearVelocity(velocity);
+ }
+ }
+
private static setupCollisionHandler(body: PhysicsBody, scoreObservable: Observable): void {
body.getCollisionObservable().add((eventData) => {
if (eventData.type !== 'COLLISION_STARTED') return;
diff --git a/src/levels/config/levelConfig.ts b/src/levels/config/levelConfig.ts
index b200b69..cc70023 100644
--- a/src/levels/config/levelConfig.ts
+++ b/src/levels/config/levelConfig.ts
@@ -111,16 +111,27 @@ interface PlanetConfig {
rotation?: Vector3Array;
}
+/**
+ * Target position for asteroids to orbit or move toward
+ */
+export interface TargetConfig {
+ id: string;
+ name: string; // Display name for editor
+ position: Vector3Array;
+}
+
/**
* Individual asteroid configuration
*/
-interface AsteroidConfig {
+export interface AsteroidConfig {
id: string;
position: Vector3Array;
scale: number; // Uniform scale applied to all axes
linearVelocity: Vector3Array;
angularVelocity?: Vector3Array;
mass?: number;
+ targetId?: string; // Reference to target from targets array
+ targetMode?: 'orbit' | 'moveToward'; // How asteroid interacts with target
}
/**
@@ -155,6 +166,7 @@ export interface LevelConfig {
startBase?: StartBaseConfig;
sun: SunConfig;
starfield?: StarfieldConfig;
+ targets?: TargetConfig[];
planets: PlanetConfig[];
asteroids: AsteroidConfig[];
diff --git a/src/levels/config/levelDeserializer.ts b/src/levels/config/levelDeserializer.ts
index d8c13e7..bc16b10 100644
--- a/src/levels/config/levelDeserializer.ts
+++ b/src/levels/config/levelDeserializer.ts
@@ -239,6 +239,15 @@ export class LevelDeserializer {
const asteroidConfig = this.config.asteroids[i];
const useOrbitConstraints = this.config.useOrbitConstraints !== false;
+ // Resolve target position if specified
+ let targetPosition: Vector3 | undefined;
+ if (asteroidConfig.targetId && this.config.targets) {
+ const target = this.config.targets.find(t => t.id === asteroidConfig.targetId);
+ if (target) {
+ targetPosition = this.arrayToVector3(target.position);
+ }
+ }
+
// Create mesh only (no physics)
RockFactory.createRockMesh(
i,
@@ -248,7 +257,9 @@ export class LevelDeserializer {
this.arrayToVector3(asteroidConfig.angularVelocity),
scoreObservable,
useOrbitConstraints,
- hidden
+ hidden,
+ targetPosition,
+ asteroidConfig.targetMode
);
const mesh = this.scene.getMeshByName(asteroidConfig.id);