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:
@@ -39,3 +39,9 @@ export const ChemicalComposition = {
|
||||
export const PlayerTag = {
|
||||
_tag: [] as number[],
|
||||
};
|
||||
|
||||
/** Harvestable resource — mineral veins, geysers, etc. */
|
||||
export const Resource = {
|
||||
quantity: [] as number[], // remaining items to collect
|
||||
interactRange: [] as number[], // max interaction distance in pixels
|
||||
};
|
||||
|
||||
114
src/player/interaction.ts
Normal file
114
src/player/interaction.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* Resource Interaction System — Phase 4.3
|
||||
*
|
||||
* Handles player collecting elements from world resources
|
||||
* (mineral veins, geysers). Press E near a resource to collect.
|
||||
*/
|
||||
|
||||
import { query } from 'bitecs';
|
||||
import type { World } from '../ecs/world';
|
||||
import { Position, Resource, PlayerTag } from '../ecs/components';
|
||||
import { removeGameEntity } from '../ecs/factory';
|
||||
import type { Inventory } from './inventory';
|
||||
|
||||
/** Metadata for a resource entity (string data that can't go in bitECS arrays) */
|
||||
export interface ResourceInfo {
|
||||
itemId: string; // element symbol or compound id
|
||||
tileX: number;
|
||||
tileY: number;
|
||||
}
|
||||
|
||||
/** Result of an interaction attempt */
|
||||
export interface InteractionResult {
|
||||
type: 'collected' | 'depleted' | 'inventory_full' | 'nothing_nearby';
|
||||
itemId?: string;
|
||||
remaining?: number;
|
||||
}
|
||||
|
||||
/** Elements that mineral veins can yield */
|
||||
export const MINERAL_ELEMENTS = ['Fe', 'Cu', 'Zn', 'Au', 'Sn'] as const;
|
||||
|
||||
/** Elements that geysers can yield */
|
||||
export const GEYSER_ELEMENTS = ['S', 'H'] as const;
|
||||
|
||||
/**
|
||||
* Deterministic element picker based on tile position and seed.
|
||||
* Same (x, y, seed) always gives the same element.
|
||||
*/
|
||||
export function pickResourceElement(
|
||||
tileX: number,
|
||||
tileY: number,
|
||||
seed: number,
|
||||
options: readonly string[],
|
||||
): string {
|
||||
// Multiplicative hash for spatial distribution
|
||||
const hash = ((tileX * 73856093) ^ (tileY * 19349663) ^ (seed * 83492791)) >>> 0;
|
||||
return options[hash % options.length];
|
||||
}
|
||||
|
||||
/**
|
||||
* Interaction system — handles E-key resource collection.
|
||||
*
|
||||
* Finds closest resource in range, adds to inventory, decrements quantity.
|
||||
* Returns null if E not pressed, or InteractionResult describing what happened.
|
||||
*/
|
||||
export function interactionSystem(
|
||||
world: World,
|
||||
justPressedInteract: boolean,
|
||||
inventory: Inventory,
|
||||
resourceData: Map<number, ResourceInfo>,
|
||||
): InteractionResult | null {
|
||||
if (!justPressedInteract) return null;
|
||||
|
||||
// Find player position
|
||||
const players = query(world, [Position, PlayerTag]);
|
||||
if (players.length === 0) return null;
|
||||
const playerEid = players[0];
|
||||
const px = Position.x[playerEid];
|
||||
const py = Position.y[playerEid];
|
||||
|
||||
// Find closest resource in range
|
||||
let closestEid: number | null = null;
|
||||
let closestDist = Infinity;
|
||||
|
||||
for (const eid of query(world, [Position, Resource])) {
|
||||
const dx = Position.x[eid] - px;
|
||||
const dy = Position.y[eid] - py;
|
||||
const dist = Math.sqrt(dx * dx + dy * dy);
|
||||
const range = Resource.interactRange[eid];
|
||||
|
||||
if (dist <= range && dist < closestDist) {
|
||||
closestEid = eid;
|
||||
closestDist = dist;
|
||||
}
|
||||
}
|
||||
|
||||
if (closestEid === null) {
|
||||
return { type: 'nothing_nearby' };
|
||||
}
|
||||
|
||||
const info = resourceData.get(closestEid);
|
||||
if (!info) return { type: 'nothing_nearby' };
|
||||
|
||||
// Try to add to inventory
|
||||
const added = inventory.addItem(info.itemId, 1);
|
||||
if (added === 0) {
|
||||
return { type: 'inventory_full', itemId: info.itemId };
|
||||
}
|
||||
|
||||
// Decrement resource quantity
|
||||
Resource.quantity[closestEid] -= 1;
|
||||
|
||||
if (Resource.quantity[closestEid] <= 0) {
|
||||
// Resource depleted — remove entity
|
||||
removeGameEntity(world, closestEid);
|
||||
resourceData.delete(closestEid);
|
||||
return { type: 'depleted', itemId: info.itemId };
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'collected',
|
||||
itemId: info.itemId,
|
||||
remaining: Resource.quantity[closestEid],
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
106
src/world/resources.ts
Normal file
106
src/world/resources.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* Resource Spawner — creates ECS entities for harvestable world objects
|
||||
*
|
||||
* Scans the generated tile grid for mineral veins and geysers,
|
||||
* creates an entity at each with a randomly assigned element.
|
||||
*/
|
||||
|
||||
import { addEntity, addComponent } from 'bitecs';
|
||||
import type { World } from '../ecs/world';
|
||||
import { Position, Resource, SpriteRef } from '../ecs/components';
|
||||
import type { TileGrid, BiomeData } from './types';
|
||||
import {
|
||||
pickResourceElement,
|
||||
MINERAL_ELEMENTS,
|
||||
GEYSER_ELEMENTS,
|
||||
type ResourceInfo,
|
||||
} from '../player/interaction';
|
||||
|
||||
/** Resource spawn configuration per tile type */
|
||||
interface ResourceTileConfig {
|
||||
tileId: number;
|
||||
elements: readonly string[];
|
||||
minQuantity: number;
|
||||
maxQuantity: number;
|
||||
interactRange: number;
|
||||
spriteColor: number;
|
||||
spriteRadius: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Spawn resource entities for all resource tiles in the grid.
|
||||
* @returns Map of entity ID → ResourceInfo for string data
|
||||
*/
|
||||
export function spawnResources(
|
||||
world: World,
|
||||
grid: TileGrid,
|
||||
biome: BiomeData,
|
||||
seed: number,
|
||||
): Map<number, ResourceInfo> {
|
||||
const resourceData = new Map<number, ResourceInfo>();
|
||||
|
||||
// Find tile IDs for resource types
|
||||
const mineralTile = biome.tiles.find(t => t.name === 'mineral-vein');
|
||||
const geyserTile = biome.tiles.find(t => t.name === 'geyser');
|
||||
|
||||
const configs: ResourceTileConfig[] = [];
|
||||
|
||||
if (mineralTile) {
|
||||
configs.push({
|
||||
tileId: mineralTile.id,
|
||||
elements: MINERAL_ELEMENTS,
|
||||
minQuantity: 3,
|
||||
maxQuantity: 5,
|
||||
interactRange: 40,
|
||||
spriteColor: 0xffd700, // gold
|
||||
spriteRadius: 4,
|
||||
});
|
||||
}
|
||||
|
||||
if (geyserTile) {
|
||||
configs.push({
|
||||
tileId: geyserTile.id,
|
||||
elements: GEYSER_ELEMENTS,
|
||||
minQuantity: 2,
|
||||
maxQuantity: 4,
|
||||
interactRange: 48,
|
||||
spriteColor: 0xff6600, // orange
|
||||
spriteRadius: 5,
|
||||
});
|
||||
}
|
||||
|
||||
const tileSize = biome.tileSize;
|
||||
|
||||
for (let y = 0; y < grid.length; y++) {
|
||||
for (let x = 0; x < grid[y].length; x++) {
|
||||
const tileId = grid[y][x];
|
||||
const config = configs.find(c => c.tileId === tileId);
|
||||
if (!config) continue;
|
||||
|
||||
// Pick element deterministically
|
||||
const itemId = pickResourceElement(x, y, seed, config.elements);
|
||||
|
||||
// Quantity from deterministic hash
|
||||
const qHash = ((x * 48611) ^ (y * 29423) ^ (seed * 61379)) >>> 0;
|
||||
const range = config.maxQuantity - config.minQuantity + 1;
|
||||
const quantity = config.minQuantity + (qHash % range);
|
||||
|
||||
// Create entity at tile center
|
||||
const eid = addEntity(world);
|
||||
addComponent(world, eid, Position);
|
||||
addComponent(world, eid, Resource);
|
||||
addComponent(world, eid, SpriteRef);
|
||||
|
||||
Position.x[eid] = x * tileSize + tileSize / 2;
|
||||
Position.y[eid] = y * tileSize + tileSize / 2;
|
||||
Resource.quantity[eid] = quantity;
|
||||
Resource.interactRange[eid] = config.interactRange;
|
||||
SpriteRef.color[eid] = config.spriteColor;
|
||||
SpriteRef.radius[eid] = config.spriteRadius;
|
||||
|
||||
resourceData.set(eid, { itemId, tileX: x, tileY: y });
|
||||
}
|
||||
}
|
||||
|
||||
return resourceData;
|
||||
}
|
||||
191
tests/interaction.test.ts
Normal file
191
tests/interaction.test.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
/**
|
||||
* Resource Interaction Tests — Phase 4.3
|
||||
*
|
||||
* Tests: resource element picking, proximity detection,
|
||||
* collection into inventory, resource depletion.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { createWorld, addEntity, addComponent, query } from 'bitecs';
|
||||
import { Position, Resource, PlayerTag } from '../src/ecs/components';
|
||||
import { Inventory } from '../src/player/inventory';
|
||||
import {
|
||||
pickResourceElement,
|
||||
interactionSystem,
|
||||
type ResourceInfo,
|
||||
type InteractionResult,
|
||||
MINERAL_ELEMENTS,
|
||||
GEYSER_ELEMENTS,
|
||||
} from '../src/player/interaction';
|
||||
|
||||
// === Helpers ===
|
||||
|
||||
function createTestWorld() {
|
||||
const world = createWorld();
|
||||
const resourceData = new Map<number, ResourceInfo>();
|
||||
const inventory = new Inventory();
|
||||
return { world, resourceData, inventory };
|
||||
}
|
||||
|
||||
function addPlayer(world: ReturnType<typeof createWorld>, x: number, y: number): number {
|
||||
const eid = addEntity(world);
|
||||
addComponent(world, eid, Position);
|
||||
addComponent(world, eid, PlayerTag);
|
||||
Position.x[eid] = x;
|
||||
Position.y[eid] = y;
|
||||
return eid;
|
||||
}
|
||||
|
||||
function addResource(
|
||||
world: ReturnType<typeof createWorld>,
|
||||
resourceData: Map<number, ResourceInfo>,
|
||||
x: number,
|
||||
y: number,
|
||||
itemId: string,
|
||||
quantity: number,
|
||||
range = 40,
|
||||
): number {
|
||||
const eid = addEntity(world);
|
||||
addComponent(world, eid, Position);
|
||||
addComponent(world, eid, Resource);
|
||||
Position.x[eid] = x;
|
||||
Position.y[eid] = y;
|
||||
Resource.quantity[eid] = quantity;
|
||||
Resource.interactRange[eid] = range;
|
||||
resourceData.set(eid, { itemId, tileX: Math.floor(x / 32), tileY: Math.floor(y / 32) });
|
||||
return eid;
|
||||
}
|
||||
|
||||
// === pickResourceElement ===
|
||||
|
||||
describe('pickResourceElement', () => {
|
||||
it('always returns element from provided list', () => {
|
||||
for (let x = 0; x < 20; x++) {
|
||||
for (let y = 0; y < 20; y++) {
|
||||
const el = pickResourceElement(x, y, 12345, MINERAL_ELEMENTS);
|
||||
expect(MINERAL_ELEMENTS).toContain(el);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('is deterministic (same inputs → same output)', () => {
|
||||
const a = pickResourceElement(5, 10, 42, MINERAL_ELEMENTS);
|
||||
const b = pickResourceElement(5, 10, 42, MINERAL_ELEMENTS);
|
||||
expect(a).toBe(b);
|
||||
});
|
||||
|
||||
it('varies with different positions', () => {
|
||||
const results = new Set<string>();
|
||||
for (let i = 0; i < 100; i++) {
|
||||
results.add(pickResourceElement(i, i * 7, 42, MINERAL_ELEMENTS));
|
||||
}
|
||||
// Should use more than one element (statistical certainty)
|
||||
expect(results.size).toBeGreaterThan(1);
|
||||
});
|
||||
|
||||
it('works with geyser elements', () => {
|
||||
const el = pickResourceElement(3, 7, 42, GEYSER_ELEMENTS);
|
||||
expect(GEYSER_ELEMENTS).toContain(el);
|
||||
});
|
||||
});
|
||||
|
||||
// === Interaction System ===
|
||||
|
||||
describe('interactionSystem — collection', () => {
|
||||
it('collects element when in range and pressing E', () => {
|
||||
const { world, resourceData, inventory } = createTestWorld();
|
||||
addPlayer(world, 100, 100);
|
||||
addResource(world, resourceData, 120, 100, 'Fe', 5);
|
||||
|
||||
const result = interactionSystem(world, true, inventory, resourceData);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.type).toBe('collected');
|
||||
expect(result!.itemId).toBe('Fe');
|
||||
expect(inventory.getCount('Fe')).toBe(1);
|
||||
});
|
||||
|
||||
it('does nothing when E is not pressed', () => {
|
||||
const { world, resourceData, inventory } = createTestWorld();
|
||||
addPlayer(world, 100, 100);
|
||||
addResource(world, resourceData, 120, 100, 'Fe', 5);
|
||||
|
||||
const result = interactionSystem(world, false, inventory, resourceData);
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(inventory.getCount('Fe')).toBe(0);
|
||||
});
|
||||
|
||||
it('does nothing when no resources in range', () => {
|
||||
const { world, resourceData, inventory } = createTestWorld();
|
||||
addPlayer(world, 100, 100);
|
||||
addResource(world, resourceData, 500, 500, 'Fe', 5); // far away
|
||||
|
||||
const result = interactionSystem(world, true, inventory, resourceData);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.type).toBe('nothing_nearby');
|
||||
});
|
||||
|
||||
it('picks closest resource when multiple in range', () => {
|
||||
const { world, resourceData, inventory } = createTestWorld();
|
||||
addPlayer(world, 100, 100);
|
||||
addResource(world, resourceData, 130, 100, 'Cu', 3); // 30px away
|
||||
addResource(world, resourceData, 115, 100, 'Fe', 5); // 15px away (closer)
|
||||
|
||||
const result = interactionSystem(world, true, inventory, resourceData);
|
||||
|
||||
expect(result!.itemId).toBe('Fe'); // picked closer one
|
||||
});
|
||||
|
||||
it('decrements resource quantity on collection', () => {
|
||||
const { world, resourceData, inventory } = createTestWorld();
|
||||
addPlayer(world, 100, 100);
|
||||
const resEid = addResource(world, resourceData, 120, 100, 'Fe', 3);
|
||||
|
||||
interactionSystem(world, true, inventory, resourceData);
|
||||
|
||||
expect(Resource.quantity[resEid]).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('interactionSystem — depletion', () => {
|
||||
it('depletes resource when quantity reaches 0', () => {
|
||||
const { world, resourceData, inventory } = createTestWorld();
|
||||
addPlayer(world, 100, 100);
|
||||
addResource(world, resourceData, 120, 100, 'Fe', 1);
|
||||
|
||||
const result = interactionSystem(world, true, inventory, resourceData);
|
||||
|
||||
expect(result!.type).toBe('depleted');
|
||||
expect(result!.itemId).toBe('Fe');
|
||||
expect(resourceData.size).toBe(0);
|
||||
});
|
||||
|
||||
it('removes entity from world on depletion', () => {
|
||||
const { world, resourceData, inventory } = createTestWorld();
|
||||
addPlayer(world, 100, 100);
|
||||
addResource(world, resourceData, 120, 100, 'Fe', 1);
|
||||
|
||||
interactionSystem(world, true, inventory, resourceData);
|
||||
|
||||
const remaining = query(world, [Resource]);
|
||||
expect(remaining.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('interactionSystem — inventory full', () => {
|
||||
it('reports inventory_full when cannot add', () => {
|
||||
const { world, resourceData } = createTestWorld();
|
||||
const inventory = new Inventory(1, 1); // very small
|
||||
inventory.addItem('H', 1); // fills it up
|
||||
|
||||
addPlayer(world, 100, 100);
|
||||
addResource(world, resourceData, 120, 100, 'Fe', 5);
|
||||
|
||||
const result = interactionSystem(world, true, inventory, resourceData);
|
||||
|
||||
expect(result!.type).toBe('inventory_full');
|
||||
expect(Resource.quantity[query(world, [Resource])[0]]).toBe(5); // not consumed
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user