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:
@@ -45,3 +45,8 @@ export const Resource = {
|
|||||||
quantity: [] as number[], // remaining items to collect
|
quantity: [] as number[], // remaining items to collect
|
||||||
interactRange: [] as number[], // max interaction distance in pixels
|
interactRange: [] as number[], // max interaction distance in pixels
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Thrown element/compound projectile */
|
||||||
|
export const Projectile = {
|
||||||
|
lifetime: [] as number[], // remaining lifetime in ms (removed at 0)
|
||||||
|
};
|
||||||
|
|||||||
141
src/player/projectile.ts
Normal file
141
src/player/projectile.ts
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
/**
|
||||||
|
* Projectile System — Throw Elements & Compounds
|
||||||
|
*
|
||||||
|
* Player launches projectiles toward mouse cursor.
|
||||||
|
* Projectiles travel in a straight line, expire after a lifetime,
|
||||||
|
* and are destroyed on hitting non-walkable tiles.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { addEntity, addComponent, query } from 'bitecs';
|
||||||
|
import type { World } from '../ecs/world';
|
||||||
|
import { Position, Velocity, SpriteRef, Projectile } from '../ecs/components';
|
||||||
|
import { removeGameEntity } from '../ecs/factory';
|
||||||
|
import { isTileWalkable } from './collision';
|
||||||
|
import { ElementRegistry } from '../chemistry/elements';
|
||||||
|
import { CompoundRegistry } from '../chemistry/compounds';
|
||||||
|
import type { TileGrid } from '../world/types';
|
||||||
|
|
||||||
|
/** Extra data for a projectile entity (string data) */
|
||||||
|
export interface ProjectileData {
|
||||||
|
itemId: string; // element symbol or compound id
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Constants ===
|
||||||
|
|
||||||
|
/** Projectile flight speed in pixels per second */
|
||||||
|
export const PROJECTILE_SPEED = 350;
|
||||||
|
|
||||||
|
/** Projectile lifetime in milliseconds */
|
||||||
|
export const PROJECTILE_LIFETIME = 2000;
|
||||||
|
|
||||||
|
/** Projectile visual radius */
|
||||||
|
export const PROJECTILE_RADIUS = 5;
|
||||||
|
|
||||||
|
// === Direction ===
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate normalized direction from source to target.
|
||||||
|
* Returns (1, 0) if source == target (default: right).
|
||||||
|
*/
|
||||||
|
export function calculateDirection(
|
||||||
|
fromX: number, fromY: number,
|
||||||
|
toX: number, toY: number,
|
||||||
|
): { dx: number; dy: number } {
|
||||||
|
const dx = toX - fromX;
|
||||||
|
const dy = toY - fromY;
|
||||||
|
const len = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
|
||||||
|
if (len < 0.001) {
|
||||||
|
return { dx: 1, dy: 0 }; // default direction
|
||||||
|
}
|
||||||
|
|
||||||
|
return { dx: dx / len, dy: dy / len };
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Launch ===
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the display color for a chemical item.
|
||||||
|
* Uses element color from registry, or compound color, or default white.
|
||||||
|
*/
|
||||||
|
function getItemColor(itemId: string): number {
|
||||||
|
const el = ElementRegistry.getBySymbol(itemId);
|
||||||
|
if (el) return parseInt(el.color.replace('#', ''), 16);
|
||||||
|
|
||||||
|
const comp = CompoundRegistry.getById(itemId);
|
||||||
|
if (comp) return parseInt(comp.color.replace('#', ''), 16);
|
||||||
|
|
||||||
|
return 0xffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a projectile entity flying from (fromX, fromY) toward (toX, toY).
|
||||||
|
* @returns entity ID
|
||||||
|
*/
|
||||||
|
export function launchProjectile(
|
||||||
|
world: World,
|
||||||
|
projData: Map<number, ProjectileData>,
|
||||||
|
fromX: number,
|
||||||
|
fromY: number,
|
||||||
|
toX: number,
|
||||||
|
toY: number,
|
||||||
|
itemId: string,
|
||||||
|
): number {
|
||||||
|
const dir = calculateDirection(fromX, fromY, toX, toY);
|
||||||
|
|
||||||
|
const eid = addEntity(world);
|
||||||
|
addComponent(world, eid, Position);
|
||||||
|
addComponent(world, eid, Velocity);
|
||||||
|
addComponent(world, eid, SpriteRef);
|
||||||
|
addComponent(world, eid, Projectile);
|
||||||
|
|
||||||
|
Position.x[eid] = fromX;
|
||||||
|
Position.y[eid] = fromY;
|
||||||
|
Velocity.vx[eid] = dir.dx * PROJECTILE_SPEED;
|
||||||
|
Velocity.vy[eid] = dir.dy * PROJECTILE_SPEED;
|
||||||
|
SpriteRef.color[eid] = getItemColor(itemId);
|
||||||
|
SpriteRef.radius[eid] = PROJECTILE_RADIUS;
|
||||||
|
Projectile.lifetime[eid] = PROJECTILE_LIFETIME;
|
||||||
|
|
||||||
|
projData.set(eid, { itemId });
|
||||||
|
|
||||||
|
return eid;
|
||||||
|
}
|
||||||
|
|
||||||
|
// === System ===
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update projectiles: decrement lifetime, remove expired/collided.
|
||||||
|
* Runs AFTER movementSystem (projectiles moved by generic movement).
|
||||||
|
*/
|
||||||
|
export function projectileSystem(
|
||||||
|
world: World,
|
||||||
|
deltaMs: number,
|
||||||
|
grid: TileGrid,
|
||||||
|
tileSize: number,
|
||||||
|
walkable: Set<number>,
|
||||||
|
projData: Map<number, ProjectileData>,
|
||||||
|
): void {
|
||||||
|
const toRemove: number[] = [];
|
||||||
|
|
||||||
|
for (const eid of query(world, [Position, Projectile])) {
|
||||||
|
// Decrement lifetime
|
||||||
|
Projectile.lifetime[eid] -= deltaMs;
|
||||||
|
|
||||||
|
// Check expiry
|
||||||
|
if (Projectile.lifetime[eid] <= 0) {
|
||||||
|
toRemove.push(eid);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check tile collision (point check at projectile center)
|
||||||
|
if (!isTileWalkable(Position.x[eid], Position.y[eid], grid, tileSize, walkable)) {
|
||||||
|
toRemove.push(eid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const eid of toRemove) {
|
||||||
|
removeGameEntity(world, eid);
|
||||||
|
projData.delete(eid);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,11 @@ import { createPlayerEntity } from '../player/factory';
|
|||||||
import { Inventory } from '../player/inventory';
|
import { Inventory } from '../player/inventory';
|
||||||
import { interactionSystem, type ResourceInfo } from '../player/interaction';
|
import { interactionSystem, type ResourceInfo } from '../player/interaction';
|
||||||
import { spawnResources } from '../world/resources';
|
import { spawnResources } from '../world/resources';
|
||||||
|
import {
|
||||||
|
launchProjectile,
|
||||||
|
projectileSystem,
|
||||||
|
type ProjectileData,
|
||||||
|
} from '../player/projectile';
|
||||||
import type { InputState } from '../player/types';
|
import type { InputState } from '../player/types';
|
||||||
|
|
||||||
export class GameScene extends Phaser.Scene {
|
export class GameScene extends Phaser.Scene {
|
||||||
@@ -34,18 +39,21 @@ export class GameScene extends Phaser.Scene {
|
|||||||
private worldGrid!: number[][];
|
private worldGrid!: number[][];
|
||||||
private tileSize!: number;
|
private tileSize!: number;
|
||||||
private resourceData!: Map<number, ResourceInfo>;
|
private resourceData!: Map<number, ResourceInfo>;
|
||||||
|
private projectileData!: Map<number, ProjectileData>;
|
||||||
private keys!: {
|
private keys!: {
|
||||||
W: Phaser.Input.Keyboard.Key;
|
W: Phaser.Input.Keyboard.Key;
|
||||||
A: Phaser.Input.Keyboard.Key;
|
A: Phaser.Input.Keyboard.Key;
|
||||||
S: Phaser.Input.Keyboard.Key;
|
S: Phaser.Input.Keyboard.Key;
|
||||||
D: Phaser.Input.Keyboard.Key;
|
D: Phaser.Input.Keyboard.Key;
|
||||||
E: Phaser.Input.Keyboard.Key;
|
E: Phaser.Input.Keyboard.Key;
|
||||||
|
F: Phaser.Input.Keyboard.Key;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Interaction feedback
|
// Interaction feedback
|
||||||
private interactionText!: Phaser.GameObjects.Text;
|
private interactionText!: Phaser.GameObjects.Text;
|
||||||
private interactionTimer = 0;
|
private interactionTimer = 0;
|
||||||
private wasEDown = false;
|
private wasEDown = false;
|
||||||
|
private wasFDown = false;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super({ key: 'GameScene' });
|
super({ key: 'GameScene' });
|
||||||
@@ -55,6 +63,7 @@ export class GameScene extends Phaser.Scene {
|
|||||||
// 1. Initialize ECS
|
// 1. Initialize ECS
|
||||||
this.gameWorld = createGameWorld();
|
this.gameWorld = createGameWorld();
|
||||||
this.bridge = new PhaserBridge(this);
|
this.bridge = new PhaserBridge(this);
|
||||||
|
this.projectileData = new Map();
|
||||||
|
|
||||||
// 2. Generate world
|
// 2. Generate world
|
||||||
const biome = biomeDataArray[0] as BiomeData;
|
const biome = biomeDataArray[0] as BiomeData;
|
||||||
@@ -103,6 +112,7 @@ export class GameScene extends Phaser.Scene {
|
|||||||
S: keyboard.addKey('S'),
|
S: keyboard.addKey('S'),
|
||||||
D: keyboard.addKey('D'),
|
D: keyboard.addKey('D'),
|
||||||
E: keyboard.addKey('E'),
|
E: keyboard.addKey('E'),
|
||||||
|
F: keyboard.addKey('F'),
|
||||||
};
|
};
|
||||||
|
|
||||||
// 9. Minimap
|
// 9. Minimap
|
||||||
@@ -149,7 +159,7 @@ export class GameScene extends Phaser.Scene {
|
|||||||
// 3. Player input → velocity
|
// 3. Player input → velocity
|
||||||
playerInputSystem(this.gameWorld.world, input);
|
playerInputSystem(this.gameWorld.world, input);
|
||||||
|
|
||||||
// 4. Movement (all entities)
|
// 4. Movement (all entities including projectiles)
|
||||||
movementSystem(this.gameWorld.world, delta);
|
movementSystem(this.gameWorld.world, delta);
|
||||||
|
|
||||||
// 5. Tile collision (player only)
|
// 5. Tile collision (player only)
|
||||||
@@ -161,7 +171,17 @@ export class GameScene extends Phaser.Scene {
|
|||||||
this.walkableSet,
|
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 isEDown = this.keys.E.isDown;
|
||||||
const justPressedE = isEDown && !this.wasEDown;
|
const justPressedE = isEDown && !this.wasEDown;
|
||||||
this.wasEDown = isEDown;
|
this.wasEDown = isEDown;
|
||||||
@@ -172,19 +192,27 @@ export class GameScene extends Phaser.Scene {
|
|||||||
this.showInteractionFeedback(interaction.type, interaction.itemId);
|
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);
|
const dead = healthSystem(this.gameWorld.world);
|
||||||
for (const eid of dead) {
|
for (const eid of dead) {
|
||||||
removeGameEntity(this.gameWorld.world, eid);
|
removeGameEntity(this.gameWorld.world, eid);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 8. Render sync
|
// 10. Render sync
|
||||||
this.bridge.sync(this.gameWorld.world);
|
this.bridge.sync(this.gameWorld.world);
|
||||||
|
|
||||||
// 9. Minimap viewport
|
// 11. Minimap viewport
|
||||||
this.minimap.update(this.cameras.main);
|
this.minimap.update(this.cameras.main);
|
||||||
|
|
||||||
// 10. Fade interaction text
|
// 12. Fade interaction text
|
||||||
if (this.interactionTimer > 0) {
|
if (this.interactionTimer > 0) {
|
||||||
this.interactionTimer -= delta;
|
this.interactionTimer -= delta;
|
||||||
if (this.interactionTimer <= 0) {
|
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 fps = delta > 0 ? Math.round(1000 / delta) : 0;
|
||||||
const px = Math.round(Position.x[this.playerEid]);
|
const px = Math.round(Position.x[this.playerEid]);
|
||||||
const py = Math.round(Position.y[this.playerEid]);
|
const py = Math.round(Position.y[this.playerEid]);
|
||||||
const invWeight = Math.round(this.inventory.getTotalWeight());
|
const invWeight = Math.round(this.inventory.getTotalWeight());
|
||||||
const invSlots = this.inventory.slotCount;
|
const invSlots = this.inventory.slotCount;
|
||||||
this.statsText.setText(
|
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 {
|
private showInteractionFeedback(type: string, itemId?: string): void {
|
||||||
let msg = '';
|
let msg = '';
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'collected':
|
case 'collected':
|
||||||
msg = `+1 ${itemId ?? ''}`;
|
msg = itemId?.startsWith('Threw') ? itemId : `+1 ${itemId ?? ''}`;
|
||||||
break;
|
break;
|
||||||
case 'depleted':
|
case 'depleted':
|
||||||
msg = `+1 ${itemId ?? ''} (depleted)`;
|
msg = `+1 ${itemId ?? ''} (depleted)`;
|
||||||
@@ -216,11 +275,11 @@ export class GameScene extends Phaser.Scene {
|
|||||||
msg = `Inventory full! Can't pick up ${itemId ?? ''}`;
|
msg = `Inventory full! Can't pick up ${itemId ?? ''}`;
|
||||||
break;
|
break;
|
||||||
case 'nothing_nearby':
|
case 'nothing_nearby':
|
||||||
msg = 'Nothing to interact with nearby';
|
msg = 'Nothing to throw / interact with';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
this.interactionText.setText(msg);
|
this.interactionText.setText(msg);
|
||||||
this.interactionText.setAlpha(1);
|
this.interactionText.setAlpha(1);
|
||||||
this.interactionTimer = 1500; // fade after 1.5s
|
this.interactionTimer = 1500;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
188
tests/projectile.test.ts
Normal file
188
tests/projectile.test.ts
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
/**
|
||||||
|
* Projectile System Tests — Phase 4.5
|
||||||
|
*
|
||||||
|
* Tests: projectile creation, direction calculation,
|
||||||
|
* lifetime expiry, tile collision removal.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { createWorld, addEntity, addComponent, query } from 'bitecs';
|
||||||
|
import { Position, Velocity, Projectile, SpriteRef } from '../src/ecs/components';
|
||||||
|
import {
|
||||||
|
launchProjectile,
|
||||||
|
projectileSystem,
|
||||||
|
calculateDirection,
|
||||||
|
type ProjectileData,
|
||||||
|
PROJECTILE_SPEED,
|
||||||
|
PROJECTILE_LIFETIME,
|
||||||
|
} from '../src/player/projectile';
|
||||||
|
|
||||||
|
// === Direction Calculation ===
|
||||||
|
|
||||||
|
describe('calculateDirection', () => {
|
||||||
|
it('normalizes direction vector', () => {
|
||||||
|
const d = calculateDirection(0, 0, 100, 0);
|
||||||
|
expect(d.dx).toBeCloseTo(1);
|
||||||
|
expect(d.dy).toBeCloseTo(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles diagonal direction', () => {
|
||||||
|
const d = calculateDirection(0, 0, 100, 100);
|
||||||
|
const mag = Math.sqrt(d.dx * d.dx + d.dy * d.dy);
|
||||||
|
expect(mag).toBeCloseTo(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles negative direction', () => {
|
||||||
|
const d = calculateDirection(100, 100, 0, 0);
|
||||||
|
expect(d.dx).toBeCloseTo(-1 / Math.SQRT2);
|
||||||
|
expect(d.dy).toBeCloseTo(-1 / Math.SQRT2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns zero for same position', () => {
|
||||||
|
const d = calculateDirection(50, 50, 50, 50);
|
||||||
|
expect(d.dx).toBe(1); // default to right
|
||||||
|
expect(d.dy).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// === Launch Projectile ===
|
||||||
|
|
||||||
|
describe('launchProjectile', () => {
|
||||||
|
it('creates entity at source position', () => {
|
||||||
|
const world = createWorld();
|
||||||
|
const projData = new Map<number, ProjectileData>();
|
||||||
|
|
||||||
|
const eid = launchProjectile(world, projData, 100, 200, 200, 200, 'Na');
|
||||||
|
|
||||||
|
expect(Position.x[eid]).toBe(100);
|
||||||
|
expect(Position.y[eid]).toBe(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets velocity toward target', () => {
|
||||||
|
const world = createWorld();
|
||||||
|
const projData = new Map<number, ProjectileData>();
|
||||||
|
|
||||||
|
const eid = launchProjectile(world, projData, 0, 0, 100, 0, 'Na');
|
||||||
|
|
||||||
|
expect(Velocity.vx[eid]).toBeCloseTo(PROJECTILE_SPEED);
|
||||||
|
expect(Velocity.vy[eid]).toBeCloseTo(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('stores chemical data', () => {
|
||||||
|
const world = createWorld();
|
||||||
|
const projData = new Map<number, ProjectileData>();
|
||||||
|
|
||||||
|
const eid = launchProjectile(world, projData, 0, 0, 100, 0, 'Na');
|
||||||
|
|
||||||
|
expect(projData.get(eid)).toBeDefined();
|
||||||
|
expect(projData.get(eid)!.itemId).toBe('Na');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets lifetime', () => {
|
||||||
|
const world = createWorld();
|
||||||
|
const projData = new Map<number, ProjectileData>();
|
||||||
|
|
||||||
|
const eid = launchProjectile(world, projData, 0, 0, 100, 0, 'Na');
|
||||||
|
|
||||||
|
expect(Projectile.lifetime[eid]).toBe(PROJECTILE_LIFETIME);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets sprite with element color', () => {
|
||||||
|
const world = createWorld();
|
||||||
|
const projData = new Map<number, ProjectileData>();
|
||||||
|
|
||||||
|
const eid = launchProjectile(world, projData, 0, 0, 100, 0, 'Na');
|
||||||
|
|
||||||
|
expect(SpriteRef.radius[eid]).toBeGreaterThan(0);
|
||||||
|
expect(SpriteRef.color[eid]).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// === Projectile System ===
|
||||||
|
|
||||||
|
describe('projectileSystem', () => {
|
||||||
|
it('decrements lifetime', () => {
|
||||||
|
const world = createWorld();
|
||||||
|
const eid = addEntity(world);
|
||||||
|
addComponent(world, eid, Position);
|
||||||
|
addComponent(world, eid, Velocity);
|
||||||
|
addComponent(world, eid, Projectile);
|
||||||
|
Position.x[eid] = 48;
|
||||||
|
Position.y[eid] = 48;
|
||||||
|
Velocity.vx[eid] = 100;
|
||||||
|
Velocity.vy[eid] = 0;
|
||||||
|
Projectile.lifetime[eid] = 1000;
|
||||||
|
|
||||||
|
const grid = [[0, 0, 0], [0, 0, 0], [0, 0, 0]];
|
||||||
|
const walkable = new Set([0]);
|
||||||
|
const projData = new Map<number, ProjectileData>();
|
||||||
|
projData.set(eid, { itemId: 'Na' });
|
||||||
|
|
||||||
|
projectileSystem(world, 100, grid, 32, walkable, projData);
|
||||||
|
|
||||||
|
expect(Projectile.lifetime[eid]).toBe(900);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes expired projectiles', () => {
|
||||||
|
const world = createWorld();
|
||||||
|
const eid = addEntity(world);
|
||||||
|
addComponent(world, eid, Position);
|
||||||
|
addComponent(world, eid, Velocity);
|
||||||
|
addComponent(world, eid, Projectile);
|
||||||
|
Position.x[eid] = 48;
|
||||||
|
Position.y[eid] = 48;
|
||||||
|
Projectile.lifetime[eid] = 50;
|
||||||
|
|
||||||
|
const grid = [[0, 0, 0], [0, 0, 0], [0, 0, 0]];
|
||||||
|
const walkable = new Set([0]);
|
||||||
|
const projData = new Map<number, ProjectileData>();
|
||||||
|
projData.set(eid, { itemId: 'Na' });
|
||||||
|
|
||||||
|
projectileSystem(world, 100, grid, 32, walkable, projData);
|
||||||
|
|
||||||
|
expect(query(world, [Projectile]).length).toBe(0);
|
||||||
|
expect(projData.size).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes projectiles on non-walkable tile', () => {
|
||||||
|
const world = createWorld();
|
||||||
|
const eid = addEntity(world);
|
||||||
|
addComponent(world, eid, Position);
|
||||||
|
addComponent(world, eid, Velocity);
|
||||||
|
addComponent(world, eid, Projectile);
|
||||||
|
// Position on non-walkable tile (1,1) = tile 3
|
||||||
|
Position.x[eid] = 48;
|
||||||
|
Position.y[eid] = 48;
|
||||||
|
Projectile.lifetime[eid] = 5000;
|
||||||
|
|
||||||
|
const grid = [[0, 0, 0], [0, 3, 0], [0, 0, 0]];
|
||||||
|
const walkable = new Set([0]);
|
||||||
|
const projData = new Map<number, ProjectileData>();
|
||||||
|
projData.set(eid, { itemId: 'Na' });
|
||||||
|
|
||||||
|
projectileSystem(world, 16, grid, 32, walkable, projData);
|
||||||
|
|
||||||
|
expect(query(world, [Projectile]).length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps valid projectiles alive', () => {
|
||||||
|
const world = createWorld();
|
||||||
|
const eid = addEntity(world);
|
||||||
|
addComponent(world, eid, Position);
|
||||||
|
addComponent(world, eid, Velocity);
|
||||||
|
addComponent(world, eid, Projectile);
|
||||||
|
Position.x[eid] = 48;
|
||||||
|
Position.y[eid] = 48;
|
||||||
|
Projectile.lifetime[eid] = 5000;
|
||||||
|
|
||||||
|
const grid = [[0, 0, 0], [0, 0, 0], [0, 0, 0]];
|
||||||
|
const walkable = new Set([0]);
|
||||||
|
const projData = new Map<number, ProjectileData>();
|
||||||
|
projData.set(eid, { itemId: 'Na' });
|
||||||
|
|
||||||
|
projectileSystem(world, 16, grid, 32, walkable, projData);
|
||||||
|
|
||||||
|
expect(query(world, [Projectile]).length).toBe(1);
|
||||||
|
expect(projData.size).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user