feat: HUD overlay via UIScene — health bar, quick slots, inventory info (Phase 4.7)
UIScene renders as overlay on top of GameScene: - Health bar (top-left) with color transitions green→yellow→red - 4 quick slots (bottom-center) with active highlight, item symbols and counts - Inventory weight/slots info (bottom-right) - Controls hint (top-right) GameScene pushes state to Phaser registry each frame. Phase 4 (Player Systems) complete — 222 tests passing. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
25
PROGRESS.md
25
PROGRESS.md
@@ -1,7 +1,7 @@
|
|||||||
# Synthesis — Development Progress
|
# Synthesis — Development Progress
|
||||||
|
|
||||||
> **Last updated:** 2026-02-12
|
> **Last updated:** 2026-02-12
|
||||||
> **Current phase:** Phase 3 ✅ → Ready for Phase 4
|
> **Current phase:** Phase 4 ✅ → Ready for Phase 5
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -48,23 +48,27 @@
|
|||||||
- [x] 3.7 Minimap — canvas-based 160x160 overview, viewport indicator, border (`src/world/minimap.ts`)
|
- [x] 3.7 Minimap — canvas-based 160x160 overview, viewport indicator, border (`src/world/minimap.ts`)
|
||||||
- [x] Unit tests — 21 passing (`tests/world.test.ts`)
|
- [x] Unit tests — 21 passing (`tests/world.test.ts`)
|
||||||
|
|
||||||
|
### Phase 4: Player Systems ✅
|
||||||
|
- [x] 4.1 Player entity + WASD controller (`src/player/input.ts`, `src/player/collision.ts`, `src/player/spawn.ts`, `src/player/factory.ts`)
|
||||||
|
- [x] 4.2 Inventory — weight-based, element stacking, AMU mass from registries (`src/player/inventory.ts`)
|
||||||
|
- [x] 4.3 Element collection from world objects — resource entities, proximity interaction, E key (`src/player/interaction.ts`, `src/world/resources.ts`)
|
||||||
|
- [x] 4.4 Crafting — chemistry engine integration, inventory consume/produce, condition checking (`src/player/crafting.ts`)
|
||||||
|
- [x] 4.5 Projectile system — throw elements toward mouse, lifetime + tile collision (`src/player/projectile.ts`)
|
||||||
|
- [x] 4.6 Quick slots — 4 hotkeys, auto-assign on pickup, active slot for throw (`src/player/quickslots.ts`)
|
||||||
|
- [x] 4.7 HUD — UIScene overlay: health bar, quick slots, inventory info, controls hint (`src/scenes/UIScene.ts`)
|
||||||
|
- [x] Unit tests — 39 player + 28 inventory + 12 interaction + 11 crafting + 13 projectile + 15 quickslots + 8 UI = 126 tests (222 total)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## In Progress
|
## In Progress
|
||||||
|
|
||||||
_None — ready to begin Phase 4_
|
_None — ready to begin Phase 5_
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Up Next: Phase 4 — Player Systems
|
## Up Next: Phase 5 — Creatures & AI
|
||||||
|
|
||||||
- [ ] 4.1 Player entity + WASD controller
|
_(See IMPLEMENTATION-PLAN.md for details)_
|
||||||
- [ ] 4.2 Inventory (weight-based, element stacking)
|
|
||||||
- [ ] 4.3 Element collection from world objects
|
|
||||||
- [ ] 4.4 Crafting (chemistry engine integration)
|
|
||||||
- [ ] 4.5 Projectile system (throw elements/compounds)
|
|
||||||
- [ ] 4.6 Quick slots (1-2-3-4 hotkeys)
|
|
||||||
- [ ] 4.7 HUD (UIScene: health ring, inventory bar, element info)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -82,3 +86,4 @@ None
|
|||||||
| 2 | 2026-02-12 | Phase 1 | Chemistry engine: 20 elements, 25 compounds, 34 reactions, engine with O(1) lookup + educational failures, 35 tests passing |
|
| 2 | 2026-02-12 | Phase 1 | Chemistry engine: 20 elements, 25 compounds, 34 reactions, engine with O(1) lookup + educational failures, 35 tests passing |
|
||||||
| 3 | 2026-02-12 | Phase 2 | ECS foundation: world + time, 5 components, movement + bounce + health systems, Phaser bridge (polling sync), entity factory, GameScene with 20 bouncing circles at 60fps, 39 tests passing |
|
| 3 | 2026-02-12 | Phase 2 | ECS foundation: world + time, 5 components, movement + bounce + health systems, Phaser bridge (polling sync), entity factory, GameScene with 20 bouncing circles at 60fps, 39 tests passing |
|
||||||
| 4 | 2026-02-12 | Phase 3 | World generation: simplex noise (seeded), 80x80 tilemap with 8 tile types, Catalytic Wastes biome, camera WASD+zoom, minimap with viewport indicator, 21 tests passing (95 total) |
|
| 4 | 2026-02-12 | Phase 3 | World generation: simplex noise (seeded), 80x80 tilemap with 8 tile types, Catalytic Wastes biome, camera WASD+zoom, minimap with viewport indicator, 21 tests passing (95 total) |
|
||||||
|
| 5 | 2026-02-12 | Phase 4 | Player systems: WASD movement + tile collision, weight-based inventory, resource collection, crafting via chemistry engine, projectile throw, 4 quick slots, UIScene HUD overlay (health bar, slots, inventory), 126 new tests (222 total) |
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import Phaser from 'phaser';
|
import Phaser from 'phaser';
|
||||||
import { BootScene } from './scenes/BootScene';
|
import { BootScene } from './scenes/BootScene';
|
||||||
import { GameScene } from './scenes/GameScene';
|
import { GameScene } from './scenes/GameScene';
|
||||||
|
import { UIScene } from './scenes/UIScene';
|
||||||
|
|
||||||
export const GAME_WIDTH = 1280;
|
export const GAME_WIDTH = 1280;
|
||||||
export const GAME_HEIGHT = 720;
|
export const GAME_HEIGHT = 720;
|
||||||
@@ -11,7 +12,7 @@ export const gameConfig: Phaser.Types.Core.GameConfig = {
|
|||||||
height: GAME_HEIGHT,
|
height: GAME_HEIGHT,
|
||||||
backgroundColor: '#0a0a0a',
|
backgroundColor: '#0a0a0a',
|
||||||
parent: document.body,
|
parent: document.body,
|
||||||
scene: [BootScene, GameScene],
|
scene: [BootScene, GameScene, UIScene],
|
||||||
physics: {
|
physics: {
|
||||||
default: 'arcade',
|
default: 'arcade',
|
||||||
arcade: {
|
arcade: {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import Phaser from 'phaser';
|
import Phaser from 'phaser';
|
||||||
import { createGameWorld, updateTime, type GameWorld } from '../ecs/world';
|
import { createGameWorld, updateTime, type GameWorld } from '../ecs/world';
|
||||||
import { Position } from '../ecs/components';
|
import { Health, Position } from '../ecs/components';
|
||||||
import { movementSystem } from '../ecs/systems/movement';
|
import { movementSystem } from '../ecs/systems/movement';
|
||||||
import { healthSystem } from '../ecs/systems/health';
|
import { healthSystem } from '../ecs/systems/health';
|
||||||
import { removeGameEntity } from '../ecs/factory';
|
import { removeGameEntity } from '../ecs/factory';
|
||||||
@@ -154,6 +154,9 @@ export class GameScene extends Phaser.Scene {
|
|||||||
this.interactionText.setOrigin(0.5);
|
this.interactionText.setOrigin(0.5);
|
||||||
this.interactionText.setDepth(100);
|
this.interactionText.setDepth(100);
|
||||||
this.interactionText.setAlpha(0);
|
this.interactionText.setAlpha(0);
|
||||||
|
|
||||||
|
// 11. Launch UIScene overlay
|
||||||
|
this.scene.launch('UIScene');
|
||||||
}
|
}
|
||||||
|
|
||||||
update(_time: number, delta: number): void {
|
update(_time: number, delta: number): void {
|
||||||
@@ -243,15 +246,27 @@ export class GameScene extends Phaser.Scene {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 13. Stats
|
// 13. Push shared state to registry for UIScene
|
||||||
|
this.registry.set('health', Health.current[this.playerEid] ?? 100);
|
||||||
|
this.registry.set('healthMax', Health.max[this.playerEid] ?? 100);
|
||||||
|
this.registry.set('quickSlots', this.quickSlots.getAll());
|
||||||
|
this.registry.set('activeSlot', this.quickSlots.activeIndex);
|
||||||
|
this.registry.set('invWeight', this.inventory.getTotalWeight());
|
||||||
|
this.registry.set('invMaxWeight', this.inventory.maxWeight);
|
||||||
|
this.registry.set('invSlots', this.inventory.slotCount);
|
||||||
|
|
||||||
|
// Build counts map for UIScene
|
||||||
|
const counts = new Map<string, number>();
|
||||||
|
for (const item of this.inventory.getItems()) {
|
||||||
|
counts.set(item.id, item.count);
|
||||||
|
}
|
||||||
|
this.registry.set('invCounts', counts);
|
||||||
|
|
||||||
|
// 14. Debug stats overlay
|
||||||
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());
|
this.statsText.setText(`seed: ${this.worldSeed} | ${fps} fps | pos: ${px},${py}`);
|
||||||
const invSlots = this.inventory.slotCount;
|
|
||||||
this.statsText.setText(
|
|
||||||
`seed: ${this.worldSeed} | ${fps} fps | pos: ${px},${py} | inv: ${invSlots} items, ${invWeight} AMU | WASD/E/F/scroll`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Try to launch a projectile from active quick slot toward mouse */
|
/** Try to launch a projectile from active quick slot toward mouse */
|
||||||
|
|||||||
196
src/scenes/UIScene.ts
Normal file
196
src/scenes/UIScene.ts
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
/**
|
||||||
|
* UIScene — HUD overlay on top of GameScene
|
||||||
|
*
|
||||||
|
* Renders health bar, quick slots, inventory weight.
|
||||||
|
* Reads shared game state from Phaser registry.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Phaser from 'phaser';
|
||||||
|
import { SLOT_COUNT } from '../player/quickslots';
|
||||||
|
import { ElementRegistry } from '../chemistry/elements';
|
||||||
|
import { CompoundRegistry } from '../chemistry/compounds';
|
||||||
|
|
||||||
|
const SLOT_SIZE = 44;
|
||||||
|
const SLOT_GAP = 4;
|
||||||
|
const HEALTH_BAR_WIDTH = 160;
|
||||||
|
const HEALTH_BAR_HEIGHT = 14;
|
||||||
|
|
||||||
|
/** Get color for a chemical item as number (for rendering) */
|
||||||
|
function getItemDisplayColor(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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get display name for a chemical item */
|
||||||
|
function getItemDisplayName(itemId: string): string {
|
||||||
|
const el = ElementRegistry.getBySymbol(itemId);
|
||||||
|
if (el) return el.symbol;
|
||||||
|
const comp = CompoundRegistry.getById(itemId);
|
||||||
|
if (comp) return comp.formula;
|
||||||
|
return itemId;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UIScene extends Phaser.Scene {
|
||||||
|
// Health
|
||||||
|
private healthBarBg!: Phaser.GameObjects.Rectangle;
|
||||||
|
private healthBarFill!: Phaser.GameObjects.Rectangle;
|
||||||
|
private healthText!: Phaser.GameObjects.Text;
|
||||||
|
|
||||||
|
// Quick slots
|
||||||
|
private slotBoxes: Phaser.GameObjects.Rectangle[] = [];
|
||||||
|
private slotTexts: Phaser.GameObjects.Text[] = [];
|
||||||
|
private slotNumTexts: Phaser.GameObjects.Text[] = [];
|
||||||
|
private slotCountTexts: Phaser.GameObjects.Text[] = [];
|
||||||
|
|
||||||
|
// Inventory
|
||||||
|
private invText!: Phaser.GameObjects.Text;
|
||||||
|
|
||||||
|
// Controls hint
|
||||||
|
private controlsText!: Phaser.GameObjects.Text;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super({ key: 'UIScene' });
|
||||||
|
}
|
||||||
|
|
||||||
|
create(): void {
|
||||||
|
const w = this.cameras.main.width;
|
||||||
|
const h = this.cameras.main.height;
|
||||||
|
|
||||||
|
// === Health bar (top-left) ===
|
||||||
|
const hpX = 12;
|
||||||
|
const hpY = 12;
|
||||||
|
|
||||||
|
this.healthBarBg = this.add.rectangle(
|
||||||
|
hpX + HEALTH_BAR_WIDTH / 2, hpY + HEALTH_BAR_HEIGHT / 2,
|
||||||
|
HEALTH_BAR_WIDTH, HEALTH_BAR_HEIGHT, 0x330000,
|
||||||
|
);
|
||||||
|
this.healthBarBg.setStrokeStyle(1, 0x660000);
|
||||||
|
|
||||||
|
this.healthBarFill = this.add.rectangle(
|
||||||
|
hpX + HEALTH_BAR_WIDTH / 2, hpY + HEALTH_BAR_HEIGHT / 2,
|
||||||
|
HEALTH_BAR_WIDTH, HEALTH_BAR_HEIGHT, 0x00cc44,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.healthText = this.add.text(
|
||||||
|
hpX + HEALTH_BAR_WIDTH + 8, hpY, '', {
|
||||||
|
fontSize: '12px',
|
||||||
|
color: '#00ff88',
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// === Quick slots (bottom-center) ===
|
||||||
|
const totalW = SLOT_COUNT * SLOT_SIZE + (SLOT_COUNT - 1) * SLOT_GAP;
|
||||||
|
const startX = (w - totalW) / 2;
|
||||||
|
const startY = h - SLOT_SIZE - 12;
|
||||||
|
|
||||||
|
for (let i = 0; i < SLOT_COUNT; i++) {
|
||||||
|
const x = startX + i * (SLOT_SIZE + SLOT_GAP);
|
||||||
|
const cx = x + SLOT_SIZE / 2;
|
||||||
|
const cy = startY + SLOT_SIZE / 2;
|
||||||
|
|
||||||
|
// Slot background
|
||||||
|
const box = this.add.rectangle(cx, cy, SLOT_SIZE, SLOT_SIZE, 0x111111, 0.85);
|
||||||
|
box.setStrokeStyle(2, 0x444444);
|
||||||
|
this.slotBoxes.push(box);
|
||||||
|
|
||||||
|
// Slot number (top-left corner)
|
||||||
|
const numText = this.add.text(x + 3, startY + 2, `${i + 1}`, {
|
||||||
|
fontSize: '10px',
|
||||||
|
color: '#555555',
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
});
|
||||||
|
this.slotNumTexts.push(numText);
|
||||||
|
|
||||||
|
// Item name (center)
|
||||||
|
const itemText = this.add.text(cx, cy - 2, '', {
|
||||||
|
fontSize: '14px',
|
||||||
|
color: '#ffffff',
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
fontStyle: 'bold',
|
||||||
|
});
|
||||||
|
itemText.setOrigin(0.5);
|
||||||
|
this.slotTexts.push(itemText);
|
||||||
|
|
||||||
|
// Count (bottom-right)
|
||||||
|
const countText = this.add.text(x + SLOT_SIZE - 3, startY + SLOT_SIZE - 3, '', {
|
||||||
|
fontSize: '10px',
|
||||||
|
color: '#aaaaaa',
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
});
|
||||||
|
countText.setOrigin(1, 1);
|
||||||
|
this.slotCountTexts.push(countText);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Inventory info (bottom-right) ===
|
||||||
|
this.invText = this.add.text(w - 12, h - 14, '', {
|
||||||
|
fontSize: '11px',
|
||||||
|
color: '#888888',
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
backgroundColor: '#000000aa',
|
||||||
|
padding: { x: 4, y: 2 },
|
||||||
|
});
|
||||||
|
this.invText.setOrigin(1, 1);
|
||||||
|
|
||||||
|
// === Controls hint (top-right) ===
|
||||||
|
this.controlsText = this.add.text(w - 12, 12, 'WASD move | E collect | F throw | 1-4 slots | scroll zoom', {
|
||||||
|
fontSize: '10px',
|
||||||
|
color: '#444444',
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
});
|
||||||
|
this.controlsText.setOrigin(1, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
update(): void {
|
||||||
|
// === Read shared state from registry ===
|
||||||
|
const health = (this.registry.get('health') as number) ?? 100;
|
||||||
|
const healthMax = (this.registry.get('healthMax') as number) ?? 100;
|
||||||
|
const slots = (this.registry.get('quickSlots') as (string | null)[]) ?? [];
|
||||||
|
const activeSlot = (this.registry.get('activeSlot') as number) ?? 0;
|
||||||
|
const invWeight = (this.registry.get('invWeight') as number) ?? 0;
|
||||||
|
const invMaxWeight = (this.registry.get('invMaxWeight') as number) ?? 500;
|
||||||
|
const invSlots = (this.registry.get('invSlots') as number) ?? 0;
|
||||||
|
const invCounts = (this.registry.get('invCounts') as Map<string, number>) ?? new Map();
|
||||||
|
|
||||||
|
// === Update health bar ===
|
||||||
|
const ratio = healthMax > 0 ? health / healthMax : 0;
|
||||||
|
const fillWidth = HEALTH_BAR_WIDTH * ratio;
|
||||||
|
this.healthBarFill.width = fillWidth;
|
||||||
|
this.healthBarFill.x = this.healthBarBg.x - (HEALTH_BAR_WIDTH - fillWidth) / 2;
|
||||||
|
|
||||||
|
// Color: green → yellow → red
|
||||||
|
const hpColor = ratio > 0.5 ? 0x00cc44 : ratio > 0.25 ? 0xccaa00 : 0xcc2200;
|
||||||
|
this.healthBarFill.setFillStyle(hpColor);
|
||||||
|
this.healthText.setText(`${Math.round(health)}/${healthMax}`);
|
||||||
|
|
||||||
|
// === Update quick slots ===
|
||||||
|
for (let i = 0; i < SLOT_COUNT; i++) {
|
||||||
|
const itemId = i < slots.length ? slots[i] : null;
|
||||||
|
const isActive = i === activeSlot;
|
||||||
|
|
||||||
|
// Border color
|
||||||
|
this.slotBoxes[i].setStrokeStyle(2, isActive ? 0x00ff88 : 0x444444);
|
||||||
|
this.slotBoxes[i].setFillStyle(isActive ? 0x1a2a1a : 0x111111, 0.85);
|
||||||
|
|
||||||
|
if (itemId) {
|
||||||
|
const displayName = getItemDisplayName(itemId);
|
||||||
|
const displayColor = getItemDisplayColor(itemId);
|
||||||
|
this.slotTexts[i].setText(displayName);
|
||||||
|
this.slotTexts[i].setColor(`#${displayColor.toString(16).padStart(6, '0')}`);
|
||||||
|
|
||||||
|
const count = invCounts.get(itemId) ?? 0;
|
||||||
|
this.slotCountTexts[i].setText(count > 0 ? `${count}` : '0');
|
||||||
|
this.slotCountTexts[i].setColor(count > 0 ? '#aaaaaa' : '#660000');
|
||||||
|
} else {
|
||||||
|
this.slotTexts[i].setText('');
|
||||||
|
this.slotCountTexts[i].setText('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Update inventory ===
|
||||||
|
this.invText.setText(`${Math.round(invWeight)}/${invMaxWeight} AMU | ${invSlots} items`);
|
||||||
|
}
|
||||||
|
}
|
||||||
73
tests/ui.test.ts
Normal file
73
tests/ui.test.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
/**
|
||||||
|
* Tests for UIScene helper functions and HUD state contract
|
||||||
|
*/
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { ElementRegistry } from '../src/chemistry/elements';
|
||||||
|
import { CompoundRegistry } from '../src/chemistry/compounds';
|
||||||
|
import { QuickSlots, SLOT_COUNT } from '../src/player/quickslots';
|
||||||
|
import { Inventory } from '../src/player/inventory';
|
||||||
|
import { Health } from '../src/ecs/components';
|
||||||
|
|
||||||
|
/** Mirror of UIScene's getItemDisplayColor for testability */
|
||||||
|
function getItemDisplayColor(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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Mirror of UIScene's getItemDisplayName for testability */
|
||||||
|
function getItemDisplayName(itemId: string): string {
|
||||||
|
const el = ElementRegistry.getBySymbol(itemId);
|
||||||
|
if (el) return el.symbol;
|
||||||
|
const comp = CompoundRegistry.getById(itemId);
|
||||||
|
if (comp) return comp.formula;
|
||||||
|
return itemId;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('HUD display helpers', () => {
|
||||||
|
it('getItemDisplayColor returns correct color for element', () => {
|
||||||
|
const color = getItemDisplayColor('Fe');
|
||||||
|
expect(color).toBeGreaterThan(0);
|
||||||
|
expect(color).not.toBe(0xffffff); // Not the fallback
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getItemDisplayColor returns fallback for unknown item', () => {
|
||||||
|
expect(getItemDisplayColor('totally_unknown')).toBe(0xffffff);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getItemDisplayName returns symbol for element', () => {
|
||||||
|
expect(getItemDisplayName('Fe')).toBe('Fe');
|
||||||
|
expect(getItemDisplayName('Na')).toBe('Na');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getItemDisplayName returns formula for compound', () => {
|
||||||
|
const name = getItemDisplayName('H2O');
|
||||||
|
expect(name).toBe('H₂O');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getItemDisplayName returns raw id for unknown', () => {
|
||||||
|
expect(getItemDisplayName('xyz')).toBe('xyz');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('HUD registry state contract', () => {
|
||||||
|
it('QuickSlots.getAll returns array of length SLOT_COUNT', () => {
|
||||||
|
const qs = new QuickSlots();
|
||||||
|
expect(qs.getAll()).toHaveLength(SLOT_COUNT);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Inventory provides required state fields', () => {
|
||||||
|
const inv = new Inventory(500, 20);
|
||||||
|
expect(typeof inv.getTotalWeight()).toBe('number');
|
||||||
|
expect(typeof inv.maxWeight).toBe('number');
|
||||||
|
expect(typeof inv.slotCount).toBe('number');
|
||||||
|
expect(Array.isArray(inv.getItems())).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Health component arrays are accessible', () => {
|
||||||
|
expect(Array.isArray(Health.current)).toBe(true);
|
||||||
|
expect(Array.isArray(Health.max)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user