# Eliza App in Hyperfy
First, you need a server running Hyperfy's fork of Eliza:
https://github.com/hyperfy-xyz/eliza
You can see the diffs here:
https://github.com/hyperfy-xyz/eliza/tree/hyperfy
Then you need a VRM Avatar. You find tons free to download here:
https://hub.vroid.com/en or test with ours https://data.hyperfy.xyz/core/hyperbot-v2.vrm
Drag and drop the VRM into your environment, place it down, right click and then paste this into "script":
```js=
const BUBBLE_TIME = 5
const EMOTE_TIME = 2
const LOOK_TIME = 5
const UP = new Vector3(0, 1, 0)
const v1 = new Vector3()
const v2 = new Vector3()
const v3 = new Vector3()
const q1 = new Quaternion()
const q2 = new Quaternion()
const m1 = new Matrix4()
const config = app.config
const vrm = app.get('avatar')
// SERVER
if (world.isServer) {
// send initial state
const state = {
ready: true,
}
app.state = state
app.send('state', state)
// spawn controller
const ctrl = app.create('controller')
ctrl.position.copy(app.position)
world.add(ctrl)
ctrl.quaternion.copy(app.quaternion)
ctrl.add(vrm)
// read emotes
const emoteUrls = {}
if (config.emote1Name && config.emote1?.url) {
emoteUrls[config.emote1Name] = config.emote1.url
}
if (config.emote2Name && config.emote2?.url) {
emoteUrls[config.emote2Name] = config.emote2.url
}
if (config.emote3Name && config.emote3?.url) {
emoteUrls[config.emote3Name] = config.emote3.url
}
if (config.emote4Name && config.emote4?.url) {
emoteUrls[config.emote4Name] = config.emote4.url
}
// observe environment
let changed = true
let notifying = false
const info = {
world: {
id: null, // todo
name: null, // todo
url: null, // todo
context: config.context || 'You are in a virtual world powered by Hyperfy'
},
you: {
id: app.instanceId,
name: config.name,
},
emotes: Object.keys(emoteUrls),
triggers: [],
events: [],
}
world.on('enter', player => {
info.events.push({
type: 'player-enter',
playerId: player.id
})
changed = true
})
world.on('leave', player => {
info.events.push({
type: 'player-leave',
playerId: player.id
})
changed = true
})
world.on('chat', msg => {
if (msg.fromId === app.instanceId) return
info.events.push({
type: 'chat',
...msg
})
if (info.events.length > 16) {
info.events.shift()
}
changed = true
})
// DEBUG
// app.send('say', 'Test!')
// app.send('emote', emoteUrls.wave)
async function notify() {
if (!config.url) return
changed = false
notifying = true
console.log('notifying...', info)
let data
try {
const resp = await fetch(config.url, {
headers: {
'Content-Type': 'application/json',
},
method: 'POST',
body: JSON.stringify(info)
})
data = await resp.json()
} catch (err) {
console.error('notify failed')
}
notifying = false
if (!data) return
console.log(data)
if (data.say) {
app.send('say', data.say)
const msg = {
id: uuid(),
from: config.name + ' (agent)',
fromId: app.instanceId,
body: data.say,
createdAt: world.getTimestamp()
}
world.chat(msg, true)
info.events.push({
type: 'chat',
...msg
})
}
if (data.emote) {
const url = emoteUrls[data.emote]
app.send('emote', url)
}
if (data.look) {
app.send('look', data.look)
}
}
app.on('fixedUpdate', delta => {
if (changed && !notifying) {
notify()
}
// const v1 = new Vector3(0,0,1)
// app.on('fixedUpdate', delta => {
// ctrl.move(v1.set(0,0,1).multiplyScalar(1 * delta))
// })
})
}
// CLIENT
if (world.isClient) {
const config = app.config
const idleEmoteUrl = config.emote0?.url
world.attach(vrm)
let state = app.state
if (state.ready) {
init()
} else {
world.remove(vrm)
app.on('state', _state => {
state = _state
init()
})
}
// setup bubble
const bubble = app.create('ui')
bubble.width = 300
bubble.height = 512
bubble.size = 0.005
bubble.pivot = 'bottom-center'
bubble.billboard = 'full'
bubble.justifyContent = 'flex-end'
bubble.alignItems = 'center'
bubble.position.y = 2
const bubbleBox = app.create('uiview')
bubbleBox.backgroundColor = 'rgba(0, 0, 0, 0.95)'
bubbleBox.borderRadius = 20
bubbleBox.padding = 20
bubble.add(bubbleBox)
const bubbleText = app.create('uitext')
bubbleText.color = 'white'
bubbleText.fontWeight = 100
bubbleText.lineHeight = 1.4
bubbleText.fontSize = 16
bubbleText.value = '...'
bubbleBox.add(bubbleText)
function init() {
world.add(vrm)
vrm.setEmote(idleEmoteUrl)
}
const data = {}
app.on('say', value => {
data.say = { timer: 0 }
vrm.add(bubble)
bubbleText.value = value
})
app.on('emote', url => {
data.emote = { timer: 0 }
vrm.setEmote(url)
})
app.on('look', playerId => {
data.look = { playerId, timer: 0 }
})
app.on('update', delta => {
if (data.say) {
data.say.timer += delta
if (data.say.timer > BUBBLE_TIME) {
data.say = null
vrm.remove(bubble)
}
}
if (data.emote) {
data.emote.timer += delta
if (data.emote.timer > EMOTE_TIME) {
data.emote = null
vrm.setEmote(idleEmoteUrl)
}
}
if (data.look) {
const player = world.getPlayer(data.look.playerId)
if (player) {
const direction = v1.copy(player.position).sub(vrm.position)
direction.y = 0
const angle = Math.atan2(direction.x, direction.z) + Math.PI
vrm.quaternion.setFromAxisAngle(UP, angle)
}
data.look.timer += delta
if (data.look.timer > LOOK_TIME) {
data.look = null
}
}
})
}
// CONFIG
app.configure(() => {
return [
{
key: 'name',
type: 'text',
label: 'Name',
},
{
key: 'context',
type: 'textarea',
label: 'Context',
},
{
key: 'url',
type: 'text',
label: 'URL',
},
{
key: 'emotes',
type: 'section',
label: 'Emotes'
},
{
key: 'emote0',
type: 'file',
label: 'Idle',
accept: '.glb',
placeholder: 'glb',
},
{
key: 'emote',
type: 'switch',
label: 'Custom',
options: [
{ label: '1', value: '1' },
{ label: '2', value: '2' },
{ label: '3', value: '3' },
{ label: '4', value: '4' },
]
},
...customEmoteFields('1'),
...customEmoteFields('2'),
...customEmoteFields('3'),
...customEmoteFields('4'),
]
function customEmoteFields(n) {
return [
{
key: `emote${n}Name`,
type: 'text',
label: 'Name',
when: [{ key: 'emote', op: 'eq', value: n }],
},
{
key: `emote${n}`,
type: 'file',
label: 'Emote',
accept: '.glb',
placeholder: 'glb',
when: [{ key: 'emote', op: 'eq', value: n }],
},
]
}
})
```
The app currently has the following features:
- Eliza is aware of players who entered the environment and their names
- Eliza listens to every message in the chat
- Eliza responds to every message with text and an emote
This is the Hyperfy handler in Eliza:
```js
export const hyperfyHandlerTemplate = `
{{actionExamples}}
(Action examples are for reference only. Do not use the information from them in your response.)
# Knowledge
{{knowledge}}
# About {{agentName}}:
{{bio}}
{{lore}}
{{providers}}
{{attachments}}
# Capabilities
Note that {{agentName}} is capable of reading/seeing/hearing various forms of media, including images, videos, audio, plaintext and PDFs. Recent attachments have been included above under the "Attachments" section.
{{messageDirections}}
{{recentMessages}}
{{actions}}
# Context
You are currently an embodied avatar in someones Hyperfy virtual world.
This is the context for the environment and a list of recent events:
{{hyperfy}}
# Task: Generate a response based on the context above which describes what is happening in the world around you.
# Instructions: Write the next message for {{agentName}}.
Response format should be formatted in a JSON block like this:
\`\`\`json
{ "lookAt": "string" player id or null, "emote": "{{emotes}}" or null, "say": "string" or null, "actions": (array of strings) or null }
\`\`\`
`;
```
The response returned from Eliza includes:
- `lookAt`: a direction for your agent
- `emote`: an emote for your agent
- `say`: text response for your agent
- `actions`: wild card for custom logic