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} +
+

Edit Target: {editingTarget.name}

+ + + + + + + +
+ + +
+
+ {/if} + + {#if targets.length === 0} +

No targets configured. Add targets for asteroids to orbit or move toward.

+ {:else} +
+ + + + + + + + + + + {#each targets as target, index} + + + + + + + {/each} + +
NameIDPositionActions
{target.name}{target.id}{formatPosition(target.position)} + + +
+
+ {/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);