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:
| Position | Use Case |
|---|---|
top-left | Minimap, time |
top-center | Notifications |
top-right | Money, inventory quick-view |
center-left | Quest trackers |
center-center | Modal dialogs |
center-right | Vehicle HUD |
bottom-left | Health, needs |
bottom-center | Speedometer |
bottom-right | Radio, 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.tsx2. 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:
- HudShell - Renders all HUDs in a 3x3 grid, filtered by group visibility
- PageShell - Renders the currently active page fullscreen
The shells automatically:
- Discover features from module definitions
- Filter HUDs by group visibility
- Sort HUDs by priority within each position
- Render the active page when requested
Best Practices
- Use
"use web"directive - All component files need it - Keep components focused - Single responsibility
- Use services for state - Don't use local state for shared data
- Use useObservable for reactivity - Auto-subscription and cleanup
- Use HUD groups - For fullscreen overlays that need to hide HUDs
- Use path aliases - Consistent imports
- Name files
index.tsx- Standard naming convention