feat: resource collection with E-key interaction (Phase 4.3)
Mineral veins yield metals (Fe, Cu, Zn, Au, Sn), geysers yield S/H. Resources spawn as ECS entities with gold/orange dot sprites on tiles. E-key collects nearest resource in range into inventory. Resources deplete after collection. Visual feedback text. 12 new tests (174 total). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -15,6 +15,9 @@ import { playerInputSystem } from '../player/input';
|
||||
import { tileCollisionSystem, buildWalkableSet } from '../player/collision';
|
||||
import { findSpawnPosition } from '../player/spawn';
|
||||
import { createPlayerEntity } from '../player/factory';
|
||||
import { Inventory } from '../player/inventory';
|
||||
import { interactionSystem, type ResourceInfo } from '../player/interaction';
|
||||
import { spawnResources } from '../world/resources';
|
||||
import type { InputState } from '../player/types';
|
||||
|
||||
export class GameScene extends Phaser.Scene {
|
||||
@@ -26,9 +29,11 @@ export class GameScene extends Phaser.Scene {
|
||||
|
||||
// Player state
|
||||
private playerEid!: number;
|
||||
private inventory!: Inventory;
|
||||
private walkableSet!: Set<number>;
|
||||
private worldGrid!: number[][];
|
||||
private tileSize!: number;
|
||||
private resourceData!: Map<number, ResourceInfo>;
|
||||
private keys!: {
|
||||
W: Phaser.Input.Keyboard.Key;
|
||||
A: Phaser.Input.Keyboard.Key;
|
||||
@@ -37,6 +42,11 @@ export class GameScene extends Phaser.Scene {
|
||||
E: Phaser.Input.Keyboard.Key;
|
||||
};
|
||||
|
||||
// Interaction feedback
|
||||
private interactionText!: Phaser.GameObjects.Text;
|
||||
private interactionTimer = 0;
|
||||
private wasEDown = false;
|
||||
|
||||
constructor() {
|
||||
super({ key: 'GameScene' });
|
||||
}
|
||||
@@ -59,18 +69,24 @@ export class GameScene extends Phaser.Scene {
|
||||
this.worldGrid = worldData.grid;
|
||||
this.tileSize = biome.tileSize;
|
||||
|
||||
// 5. Create player at spawn position
|
||||
// 5. Spawn resource entities (mineral veins, geysers)
|
||||
this.resourceData = spawnResources(
|
||||
this.gameWorld.world, worldData.grid, biome, this.worldSeed,
|
||||
);
|
||||
|
||||
// 6. Create player at spawn position + inventory
|
||||
const spawn = findSpawnPosition(worldData.grid, biome.tileSize, this.walkableSet);
|
||||
const spawnX = spawn?.x ?? (biome.mapWidth * biome.tileSize) / 2;
|
||||
const spawnY = spawn?.y ?? (biome.mapHeight * biome.tileSize) / 2;
|
||||
this.playerEid = createPlayerEntity(this.gameWorld.world, spawnX, spawnY);
|
||||
this.inventory = new Inventory(500, 20);
|
||||
|
||||
// 6. Camera — follow player, zoom via scroll wheel
|
||||
// 7. Camera — follow player, zoom via scroll wheel
|
||||
const worldPixelW = biome.mapWidth * biome.tileSize;
|
||||
const worldPixelH = biome.mapHeight * biome.tileSize;
|
||||
setupPlayerCamera(this, worldPixelW, worldPixelH);
|
||||
|
||||
// Sync bridge to create player sprite, then attach camera follow
|
||||
// Sync bridge to create sprites, then attach camera follow to player
|
||||
this.bridge.sync(this.gameWorld.world);
|
||||
const playerSprite = this.bridge.getSprite(this.playerEid);
|
||||
if (playerSprite) {
|
||||
@@ -78,7 +94,7 @@ export class GameScene extends Phaser.Scene {
|
||||
this.cameras.main.startFollow(playerSprite, true, 0.1, 0.1);
|
||||
}
|
||||
|
||||
// 7. Keyboard input
|
||||
// 8. Keyboard input
|
||||
const keyboard = this.input.keyboard;
|
||||
if (!keyboard) throw new Error('Keyboard plugin not available');
|
||||
this.keys = {
|
||||
@@ -89,10 +105,10 @@ export class GameScene extends Phaser.Scene {
|
||||
E: keyboard.addKey('E'),
|
||||
};
|
||||
|
||||
// 8. Minimap
|
||||
// 9. Minimap
|
||||
this.minimap = new Minimap(this, worldData);
|
||||
|
||||
// 9. UI stats overlay
|
||||
// 10. UI overlay
|
||||
this.statsText = this.add.text(10, 10, '', {
|
||||
fontSize: '12px',
|
||||
color: '#00ff88',
|
||||
@@ -102,6 +118,21 @@ export class GameScene extends Phaser.Scene {
|
||||
});
|
||||
this.statsText.setScrollFactor(0);
|
||||
this.statsText.setDepth(100);
|
||||
|
||||
// Interaction feedback text (center-bottom of screen)
|
||||
this.interactionText = this.add.text(
|
||||
this.cameras.main.width / 2, this.cameras.main.height - 40, '', {
|
||||
fontSize: '14px',
|
||||
color: '#ffdd44',
|
||||
fontFamily: 'monospace',
|
||||
backgroundColor: '#000000cc',
|
||||
padding: { x: 6, y: 3 },
|
||||
},
|
||||
);
|
||||
this.interactionText.setScrollFactor(0);
|
||||
this.interactionText.setOrigin(0.5);
|
||||
this.interactionText.setDepth(100);
|
||||
this.interactionText.setAlpha(0);
|
||||
}
|
||||
|
||||
update(_time: number, delta: number): void {
|
||||
@@ -130,24 +161,66 @@ export class GameScene extends Phaser.Scene {
|
||||
this.walkableSet,
|
||||
);
|
||||
|
||||
// 6. Health / death
|
||||
// 6. Resource interaction (E key, debounced — manual edge detection)
|
||||
const isEDown = this.keys.E.isDown;
|
||||
const justPressedE = isEDown && !this.wasEDown;
|
||||
this.wasEDown = isEDown;
|
||||
const interaction = interactionSystem(
|
||||
this.gameWorld.world, justPressedE, this.inventory, this.resourceData,
|
||||
);
|
||||
if (interaction) {
|
||||
this.showInteractionFeedback(interaction.type, interaction.itemId);
|
||||
}
|
||||
|
||||
// 7. Health / death
|
||||
const dead = healthSystem(this.gameWorld.world);
|
||||
for (const eid of dead) {
|
||||
removeGameEntity(this.gameWorld.world, eid);
|
||||
}
|
||||
|
||||
// 7. Render sync
|
||||
// 8. Render sync
|
||||
this.bridge.sync(this.gameWorld.world);
|
||||
|
||||
// 8. Minimap viewport
|
||||
// 9. Minimap viewport
|
||||
this.minimap.update(this.cameras.main);
|
||||
|
||||
// 9. Stats
|
||||
// 10. Fade interaction text
|
||||
if (this.interactionTimer > 0) {
|
||||
this.interactionTimer -= delta;
|
||||
if (this.interactionTimer <= 0) {
|
||||
this.interactionText.setAlpha(0);
|
||||
}
|
||||
}
|
||||
|
||||
// 11. 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} | WASD move, E interact, scroll zoom`,
|
||||
`seed: ${this.worldSeed} | ${fps} fps | pos: ${px},${py} | inv: ${invSlots} items, ${invWeight} AMU | WASD/E/scroll`,
|
||||
);
|
||||
}
|
||||
|
||||
private showInteractionFeedback(type: string, itemId?: string): void {
|
||||
let msg = '';
|
||||
switch (type) {
|
||||
case 'collected':
|
||||
msg = `+1 ${itemId ?? ''}`;
|
||||
break;
|
||||
case 'depleted':
|
||||
msg = `+1 ${itemId ?? ''} (depleted)`;
|
||||
break;
|
||||
case 'inventory_full':
|
||||
msg = `Inventory full! Can't pick up ${itemId ?? ''}`;
|
||||
break;
|
||||
case 'nothing_nearby':
|
||||
msg = 'Nothing to interact with nearby';
|
||||
break;
|
||||
}
|
||||
this.interactionText.setText(msg);
|
||||
this.interactionText.setAlpha(1);
|
||||
this.interactionTimer = 1500; // fade after 1.5s
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user