Skip to main content

UI Features

Features are the building blocks of True Life's UI system. They encapsulate React components that render HUDs and pages, using services and observables for state management.

Feature Definition

Features are defined directly in module.ts:

// src/modules/my-feature/module.ts
import { registerModule } from "@core/module";
import { MyService } from "@modules/my-feature/my.service";
import MyHudPanel from "@modules/my-feature/huds/my-hud/index";
import MyPage from "@modules/my-feature/pages/my-page/index";
 
export default registerModule({
	name: "my-feature",
	services: [MyService],
 
	// HUD definitions
	huds: [
		{
			name: "my-hud",
			panel: MyHudPanel,
			hudPosition: "bottom-left",
			hudPriority: 0,
			hudGroup: "default", // Optional: for group visibility control
		},
	],
 
	// Page definitions
	pages: {
		"my-page": {
			panel: MyPage,
		},
	},
});

HUD Positions

HUDs are positioned in a 3x3 grid:

PositionUse Case
top-leftMinimap, time
top-centerNotifications
top-rightMoney, inventory quick-view
center-leftQuest trackers
center-centerModal dialogs
center-rightVehicle HUD
bottom-leftHealth, needs
bottom-centerSpeedometer
bottom-rightRadio, voice

HUD Priority

When multiple HUDs share a position, hudPriority determines stacking order:

// Renders first (bottom of stack)
hudPriority: -100,
 
// Renders last (top of stack)
hudPriority: 100,

HUD Groups

HUD groups allow controlling visibility of multiple HUDs together:

// In module.ts
huds: [
    {
        name: "status",
        panel: StatusPanel,
        hudPosition: "bottom-left",
        hudGroup: "default", // Belongs to "default" group
    },
    {
        name: "minimap",
        panel: MinimapPanel,
        hudPosition: "top-left",
        hudGroup: "default",
    },
    {
        name: "combat",
        panel: CombatPanel,
        hudPosition: "center-center",
        hudGroup: "combat", // Belongs to "combat" group
    },
],

Control group visibility:

import { setHudGroupVisible, isHudGroupVisible, getHudGroups } from "@ui/feature";
 
// Hide all HUDs in the "default" group
setHudGroupVisible("default", false);
 
// Show them again
setHudGroupVisible("default", true);
 
// Check visibility
if (isHudGroupVisible("default")) {
	// HUDs are visible
}
 
// Get all registered groups
const groups = getHudGroups(); // ["default", "combat", ...]

Creating a HUD Feature

1. Create Directory Structure

src/modules/my-feature/
├── module.ts
├── my.service.ts
└── huds/
    └── my-hud/
        └── index.tsx

2. Create Service with webOwned Observable

// src/modules/my-feature/my.service.ts
import { webOwned } from "@core/observable";
 
interface HudState {
	value: number;
	label: string;
}
 
export class MyService {
	public hudState = webOwned<HudState>({
		id: "my-feature:hud",
		initialValue: { value: 0, label: "" },
	});
 
	@ClientOnly
	updateHud(value: number, label: string): void {
		this.hudState.set({ value, label });
	}
}

3. Create Panel Component

// src/modules/my-feature/huds/my-hud/index.tsx
"use web";
 
import React from "react";
import { useService, useObservable } from "@core/react";
import { MyService } from "@modules/my-feature/my.service";
 
export default function MyHudPanel() {
	const myService = useService(MyService);
	const state = useObservable(myService.hudState);
 
	return (
		<div className="rounded-lg bg-black/80 p-4">
			<div className="text-sm text-gray-400">{state.label}</div>
			<div className="text-2xl font-bold text-white">{state.value}</div>
		</div>
	);
}

4. Register in Module

// src/modules/my-feature/module.ts
import { registerModule } from "@core/module";
import { MyService } from "@modules/my-feature/my.service";
import MyHudPanel from "@modules/my-feature/huds/my-hud/index";
 
export default registerModule({
	name: "my-feature",
	services: [MyService],
	huds: [
		{
			name: "my-hud",
			panel: MyHudPanel,
			hudPosition: "bottom-left",
			hudPriority: 0,
			hudGroup: "default",
		},
	],
});

Creating a Page Feature

Pages follow the same pattern but without positioning:

// src/modules/atm/module.ts
import { registerModule } from "@core/module";
import AtmPage from "@modules/features/atm/pages/atm-interface/index";
 
export default registerModule({
	name: "atm",
	pages: {
		"atm-interface": {
			panel: AtmPage,
		},
	},
});
// src/modules/atm/pages/atm-interface/index.tsx
"use web";
 
import React from "react";
 
export default function AtmPage() {
	return (
		<div className="flex h-screen items-center justify-center">
			<div className="w-96 rounded-xl bg-gray-900 p-8">
				<h1 className="mb-4 text-2xl font-bold text-white">ATM</h1>
				{/* ATM UI */}
			</div>
		</div>
	);
}

Page Open/Close Events

Control page visibility from the client:

// From client - open a page
sendHudEvent("page:open", { page: "atm-interface" });
 
// From client - close current page
sendHudEvent("page:close", {});

Feature Loading

Features are loaded and rendered by the shell components:

  1. HudShell - Renders all HUDs in a 3x3 grid, filtered by group visibility
  2. PageShell - Renders the currently active page fullscreen

The shells automatically:

  1. Discover features from module definitions
  2. Filter HUDs by group visibility
  3. Sort HUDs by priority within each position
  4. Render the active page when requested

Best Practices

  1. Use "use web" directive - All component files need it
  2. Keep components focused - Single responsibility
  3. Use services for state - Don't use local state for shared data
  4. Use useObservable for reactivity - Auto-subscription and cleanup
  5. Use HUD groups - For fullscreen overlays that need to hide HUDs
  6. Use path aliases - Consistent imports
  7. Name files index.tsx - Standard naming convention