Lesson 3.1: Custom Trait Development
In this advanced lesson, you'll learn how to create your own traits - extending HoloScript's capabilities to meet your specific needs.
Learning Objectives
By the end of this lesson, you will:
- Understand the trait system architecture
- Create custom traits from scratch
- Implement trait lifecycle methods
- Publish traits for others to use
Trait Architecture
Traits in HoloScript follow a component-based architecture:
┌─────────────────────────────────────────────┐
│ composition │
├─────────────────────────────────────────────┤
│ @trait1 │ @trait2 │ @trait3 │ ... │
├───────────┼───────────┼───────────┼─────────┤
│ state │ state │ state │ │
│ methods │ methods │ methods │ │
│ events │ events │ events │ │
└───────────┴───────────┴───────────┴─────────┘Each trait:
- Has its own state (configuration and runtime data)
- Implements lifecycle methods (init, update, destroy)
- Can emit and listen to events
- Can access the host orb and other traits
Creating Your First Custom Trait
Let's create a @health trait for damageable objects:
Step 1: Define the Trait
Create src/traits/HealthTrait.ts:
typescript
import { TraitDefinition, TraitContext, TraitConfig, TraitEvent } from '@holoscript/core';
// Configuration schema
interface HealthConfig extends TraitConfig {
maxHealth: number;
currentHealth?: number;
regeneration?: number;
invulnerableTime?: number;
}
// Trait state
interface HealthState {
currentHealth: number;
maxHealth: number;
isDead: boolean;
isInvulnerable: boolean;
lastDamageTime: number;
}
// Events this trait emits
interface HealthEvents {
'health:damaged': { amount: number; source?: string; newHealth: number };
'health:healed': { amount: number; newHealth: number };
'health:died': { killer?: string };
'health:revived': { health: number };
}
export const HealthTrait: TraitDefinition<HealthConfig, HealthState, HealthEvents> = {
name: 'health',
version: '1.0.0',
// Default configuration
defaultConfig: {
maxHealth: 100,
currentHealth: undefined, // Defaults to maxHealth
regeneration: 0, // HP per second
invulnerableTime: 0.5, // Seconds after damage
},
// Initialize the trait
init(ctx: TraitContext<HealthConfig>): HealthState {
const config = ctx.config;
return {
currentHealth: config.currentHealth ?? config.maxHealth,
maxHealth: config.maxHealth,
isDead: false,
isInvulnerable: false,
lastDamageTime: 0,
};
},
// Update called every frame
update(ctx: TraitContext<HealthConfig>, state: HealthState, deltaTime: number): void {
// Handle invulnerability
if (state.isInvulnerable) {
const timeSinceDamage = ctx.time - state.lastDamageTime;
if (timeSinceDamage >= ctx.config.invulnerableTime) {
state.isInvulnerable = false;
}
}
// Handle regeneration
if (ctx.config.regeneration > 0 && !state.isDead) {
const regenAmount = ctx.config.regeneration * deltaTime;
if (state.currentHealth < state.maxHealth) {
state.currentHealth = Math.min(state.maxHealth, state.currentHealth + regenAmount);
}
}
},
// Public methods exposed to HoloScript
methods: {
damage(ctx, state, amount: number, source?: string): number {
if (state.isDead || state.isInvulnerable) {
return 0;
}
const actualDamage = Math.min(amount, state.currentHealth);
state.currentHealth -= actualDamage;
state.lastDamageTime = ctx.time;
state.isInvulnerable = true;
ctx.emit('health:damaged', {
amount: actualDamage,
source,
newHealth: state.currentHealth,
});
if (state.currentHealth <= 0) {
state.isDead = true;
ctx.emit('health:died', { killer: source });
}
return actualDamage;
},
heal(ctx, state, amount: number): number {
if (state.isDead) {
return 0;
}
const actualHeal = Math.min(amount, state.maxHealth - state.currentHealth);
state.currentHealth += actualHeal;
ctx.emit('health:healed', {
amount: actualHeal,
newHealth: state.currentHealth,
});
return actualHeal;
},
revive(ctx, state, healthPercent: number = 0.5): void {
if (!state.isDead) return;
state.isDead = false;
state.currentHealth = state.maxHealth * healthPercent;
state.isInvulnerable = true;
state.lastDamageTime = ctx.time;
ctx.emit('health:revived', { health: state.currentHealth });
},
getHealth(ctx, state): number {
return state.currentHealth;
},
getHealthPercent(ctx, state): number {
return state.currentHealth / state.maxHealth;
},
isDead(ctx, state): boolean {
return state.isDead;
},
},
// Cleanup when trait is removed
destroy(ctx: TraitContext<HealthConfig>, state: HealthState): void {
// Cleanup resources if needed
},
};
export default HealthTrait;Step 2: Register the Trait
In your project's holoscript.config.ts:
typescript
import { defineConfig } from '@holoscript/cli';
import { HealthTrait } from './src/traits/HealthTrait';
export default defineConfig({
traits: [HealthTrait],
});Step 3: Use Your Trait
hsplus
composition "Combat Demo" {
composition enemy {
@health {
maxHealth: 50
regeneration: 2
invulnerableTime: 0.3
}
@collidable
position: [0, 1, -3]
color: "#E74C3C"
// Listen to health events
on_health_damaged: (event) => {
console.log(`Took ${event.amount} damage! HP: ${event.newHealth}`)
// Flash red
this.color = "#FF0000"
setTimeout(() => { this.color = "#E74C3C" }, 100)
}
on_health_died: (event) => {
console.log("Enemy defeated!")
this.visible = false
}
}
// Weapon that deals damage
composition sword {
@grabbable
position: [1, 1, -2]
scale: [0.1, 0.5, 0.02]
color: "#C0C0C0"
geometry: "cube"
onCollision: (other) => {
if (other.hasTrait("health")) {
other.damage(10, "sword")
}
}
}
}Advanced Trait Patterns
Trait Dependencies
Require other traits:
typescript
export const ArmorTrait: TraitDefinition = {
name: 'armor',
requires: ['health'], // Must have @health trait
init(ctx) {
// Access health trait
const healthTrait = ctx.getTrait('health');
// Intercept damage
ctx.intercept('health:damage', (amount, source) => {
const reduction = ctx.config.damageReduction;
return amount * (1 - reduction);
});
},
};Trait Communication
Traits can communicate via the host orb:
typescript
// In HealthTrait
methods: {
damage(ctx, state, amount, source) {
// Check for shield trait
const shield = ctx.getTrait('shield');
if (shield && shield.isActive()) {
amount = shield.absorbDamage(amount);
}
// Continue with damage logic...
}
}Networked Traits
Make trait state sync across network:
typescript
export const HealthTrait: TraitDefinition = {
name: 'health',
// Define which state syncs
networked: {
syncState: ['currentHealth', 'isDead'],
ownerAuthority: true, // Only owner can modify
interpolate: true,
},
// Network-specific methods
onNetworkSync(ctx, state, remoteState) {
// Smooth interpolation
state.currentHealth = lerp(state.currentHealth, remoteState.currentHealth, 0.1);
},
};Publishing Your Trait
Package Structure
my-traits/
├── src/
│ ├── HealthTrait.ts
│ ├── ArmorTrait.ts
│ └── index.ts
├── package.json
└── README.mdpackage.json
json
{
"name": "@myteam/combat-traits",
"version": "1.0.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"holoscript": {
"traits": ["health", "armor", "shield"]
},
"peerDependencies": {
"@holoscript/core": "^2.0.0"
}
}Publishing
bash
# Build
npm run build
# Publish to HoloScript registry
holoscript publishUsing Published Traits
bash
# Install the package
holoscript add @myteam/combat-traitshsplus
// Automatically available after install
composition player {
@health { maxHealth: 100 }
@armor { damageReduction: 0.2 }
}Exercise: Create a "Collectible" Trait
Build a @collectible trait that:
- Makes objects collectible by the player
- Plays a collection sound
- Adds to an inventory system
- Has a respawn timer
Requirements
- Config:
respawnTime,value,soundClip - Events:
collected,respawned - Methods:
collect(),forceRespawn()
Summary
In this lesson, you learned:
- The trait architecture and lifecycle
- Creating traits with TypeScript
- Implementing state, methods, and events
- Trait dependencies and communication
- Publishing traits for reuse
Next Lesson
In Lesson 3.2: Plugin Architecture, we'll explore the broader plugin system for extending HoloScript.
Time to complete: ~60 minutes Difficulty: Advanced Prerequisites: Level 2 Complete, TypeScript knowledge