feat: projectile system for throwing elements (Phase 4.5)
F key launches first inventory item toward mouse cursor as projectile. Projectiles travel at 350px/s, expire after 2s, destroyed on hitting non-walkable tiles. Color matches element's periodic table color. 13 new tests (198 total). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -18,6 +18,11 @@ import { createPlayerEntity } from '../player/factory';
|
||||
import { Inventory } from '../player/inventory';
|
||||
import { interactionSystem, type ResourceInfo } from '../player/interaction';
|
||||
import { spawnResources } from '../world/resources';
|
||||
import {
|
||||
launchProjectile,
|
||||
projectileSystem,
|
||||
type ProjectileData,
|
||||
} from '../player/projectile';
|
||||
import type { InputState } from '../player/types';
|
||||
|
||||
export class GameScene extends Phaser.Scene {
|
||||
@@ -34,18 +39,21 @@ export class GameScene extends Phaser.Scene {
|
||||
private worldGrid!: number[][];
|
||||
private tileSize!: number;
|
||||
private resourceData!: Map<number, ResourceInfo>;
|
||||
private projectileData!: Map<number, ProjectileData>;
|
||||
private keys!: {
|
||||
W: Phaser.Input.Keyboard.Key;
|
||||
A: Phaser.Input.Keyboard.Key;
|
||||
S: Phaser.Input.Keyboard.Key;
|
||||
D: Phaser.Input.Keyboard.Key;
|
||||
E: Phaser.Input.Keyboard.Key;
|
||||
F: Phaser.Input.Keyboard.Key;
|
||||
};
|
||||
|
||||
// Interaction feedback
|
||||
private interactionText!: Phaser.GameObjects.Text;
|
||||
private interactionTimer = 0;
|
||||
private wasEDown = false;
|
||||
private wasFDown = false;
|
||||
|
||||
constructor() {
|
||||
super({ key: 'GameScene' });
|
||||
@@ -55,6 +63,7 @@ export class GameScene extends Phaser.Scene {
|
||||
// 1. Initialize ECS
|
||||
this.gameWorld = createGameWorld();
|
||||
this.bridge = new PhaserBridge(this);
|
||||
this.projectileData = new Map();
|
||||
|
||||
// 2. Generate world
|
||||
const biome = biomeDataArray[0] as BiomeData;
|
||||
@@ -103,6 +112,7 @@ export class GameScene extends Phaser.Scene {
|
||||
S: keyboard.addKey('S'),
|
||||
D: keyboard.addKey('D'),
|
||||
E: keyboard.addKey('E'),
|
||||
F: keyboard.addKey('F'),
|
||||
};
|
||||
|
||||
// 9. Minimap
|
||||
@@ -149,7 +159,7 @@ export class GameScene extends Phaser.Scene {
|
||||
// 3. Player input → velocity
|
||||
playerInputSystem(this.gameWorld.world, input);
|
||||
|
||||
// 4. Movement (all entities)
|
||||
// 4. Movement (all entities including projectiles)
|
||||
movementSystem(this.gameWorld.world, delta);
|
||||
|
||||
// 5. Tile collision (player only)
|
||||
@@ -161,7 +171,17 @@ export class GameScene extends Phaser.Scene {
|
||||
this.walkableSet,
|
||||
);
|
||||
|
||||
// 6. Resource interaction (E key, debounced — manual edge detection)
|
||||
// 6. Projectile system (lifetime + tile collision)
|
||||
projectileSystem(
|
||||
this.gameWorld.world,
|
||||
delta,
|
||||
this.worldGrid,
|
||||
this.tileSize,
|
||||
this.walkableSet,
|
||||
this.projectileData,
|
||||
);
|
||||
|
||||
// 7. Resource interaction (E key, debounced)
|
||||
const isEDown = this.keys.E.isDown;
|
||||
const justPressedE = isEDown && !this.wasEDown;
|
||||
this.wasEDown = isEDown;
|
||||
@@ -172,19 +192,27 @@ export class GameScene extends Phaser.Scene {
|
||||
this.showInteractionFeedback(interaction.type, interaction.itemId);
|
||||
}
|
||||
|
||||
// 7. Health / death
|
||||
// 8. Throw projectile (F key, debounced)
|
||||
const isFDown = this.keys.F.isDown;
|
||||
const justPressedF = isFDown && !this.wasFDown;
|
||||
this.wasFDown = isFDown;
|
||||
if (justPressedF) {
|
||||
this.tryLaunchProjectile();
|
||||
}
|
||||
|
||||
// 9. Health / death
|
||||
const dead = healthSystem(this.gameWorld.world);
|
||||
for (const eid of dead) {
|
||||
removeGameEntity(this.gameWorld.world, eid);
|
||||
}
|
||||
|
||||
// 8. Render sync
|
||||
// 10. Render sync
|
||||
this.bridge.sync(this.gameWorld.world);
|
||||
|
||||
// 9. Minimap viewport
|
||||
// 11. Minimap viewport
|
||||
this.minimap.update(this.cameras.main);
|
||||
|
||||
// 10. Fade interaction text
|
||||
// 12. Fade interaction text
|
||||
if (this.interactionTimer > 0) {
|
||||
this.interactionTimer -= delta;
|
||||
if (this.interactionTimer <= 0) {
|
||||
@@ -192,22 +220,53 @@ export class GameScene extends Phaser.Scene {
|
||||
}
|
||||
}
|
||||
|
||||
// 11. Stats
|
||||
// 13. Stats
|
||||
const fps = delta > 0 ? Math.round(1000 / delta) : 0;
|
||||
const px = Math.round(Position.x[this.playerEid]);
|
||||
const py = Math.round(Position.y[this.playerEid]);
|
||||
const invWeight = Math.round(this.inventory.getTotalWeight());
|
||||
const invSlots = this.inventory.slotCount;
|
||||
this.statsText.setText(
|
||||
`seed: ${this.worldSeed} | ${fps} fps | pos: ${px},${py} | inv: ${invSlots} items, ${invWeight} AMU | WASD/E/scroll`,
|
||||
`seed: ${this.worldSeed} | ${fps} fps | pos: ${px},${py} | inv: ${invSlots} items, ${invWeight} AMU | WASD/E/F/scroll`,
|
||||
);
|
||||
}
|
||||
|
||||
/** Try to launch a projectile from the first inventory item toward mouse */
|
||||
private tryLaunchProjectile(): void {
|
||||
const items = this.inventory.getItems();
|
||||
if (items.length === 0) {
|
||||
this.showInteractionFeedback('nothing_nearby');
|
||||
return;
|
||||
}
|
||||
|
||||
// Use first item in inventory (quick slots will replace this in 4.6)
|
||||
const item = items[0];
|
||||
const removed = this.inventory.removeItem(item.id, 1);
|
||||
if (removed === 0) return;
|
||||
|
||||
// Get mouse world position for direction
|
||||
const pointer = this.input.activePointer;
|
||||
const worldPoint = this.cameras.main.getWorldPoint(pointer.x, pointer.y);
|
||||
|
||||
const px = Position.x[this.playerEid];
|
||||
const py = Position.y[this.playerEid];
|
||||
|
||||
launchProjectile(
|
||||
this.gameWorld.world,
|
||||
this.projectileData,
|
||||
px, py,
|
||||
worldPoint.x, worldPoint.y,
|
||||
item.id,
|
||||
);
|
||||
|
||||
this.showInteractionFeedback('collected', `Threw ${item.id}`);
|
||||
}
|
||||
|
||||
private showInteractionFeedback(type: string, itemId?: string): void {
|
||||
let msg = '';
|
||||
switch (type) {
|
||||
case 'collected':
|
||||
msg = `+1 ${itemId ?? ''}`;
|
||||
msg = itemId?.startsWith('Threw') ? itemId : `+1 ${itemId ?? ''}`;
|
||||
break;
|
||||
case 'depleted':
|
||||
msg = `+1 ${itemId ?? ''} (depleted)`;
|
||||
@@ -216,11 +275,11 @@ export class GameScene extends Phaser.Scene {
|
||||
msg = `Inventory full! Can't pick up ${itemId ?? ''}`;
|
||||
break;
|
||||
case 'nothing_nearby':
|
||||
msg = 'Nothing to interact with nearby';
|
||||
msg = 'Nothing to throw / interact with';
|
||||
break;
|
||||
}
|
||||
this.interactionText.setText(msg);
|
||||
this.interactionText.setAlpha(1);
|
||||
this.interactionTimer = 1500; // fade after 1.5s
|
||||
this.interactionTimer = 1500;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user