Chat Room

Simple message passing with player presence. The simplest multiplayer pattern.

When to Use

  • Chat applications
  • Game lobbies
  • Party game waiting rooms
  • Social spaces

The Pattern

import { connect } from '@watchtower-sdk/core'

const room = await connect('chat-room', {
  name: 'Alice'  // Display name
})

const messages = []

// Send a chat message
function sendMessage(text) {
  room.broadcast({
    type: 'chat',
    text,
    name: room.players.find(p => p.id === room.playerId)?.name
  })
  
  // Add to local messages too
  messages.push({
    from: room.playerId,
    name: 'You',
    text,
    time: Date.now()
  })
  renderMessages()
}

// Receive messages
room.on('message', (from, data) => {
  if (data.type === 'chat') {
    messages.push({
      from,
      name: data.name || from,
      text: data.text,
      time: Date.now()
    })
    renderMessages()
  }
})

// Player events
room.on('join', (player) => {
  messages.push({
    type: 'system',
    text: `${player.name || player.id} joined`,
    time: Date.now()
  })
  renderMessages()
  updatePlayerList()
})

room.on('leave', (player) => {
  messages.push({
    type: 'system',
    text: `${player.name || player.id} left`,
    time: Date.now()
  })
  renderMessages()
  updatePlayerList()
})

function updatePlayerList() {
  // room.players contains everyone in the room
  const list = room.players.map(p => p.name || p.id)
  renderPlayerList(list)
}

Key Points

  • Use player names — pass name in connect options
  • System messages — announce joins and leaves
  • Local echo — show your own messages immediately
  • Player list — use room.players for presence

Message Types

Expand beyond basic chat with different message types:

// Chat
room.broadcast({ type: 'chat', text: 'Hello!' })

// Emote
room.broadcast({ type: 'emote', emote: 'wave' })

// Typing indicator
room.broadcast({ type: 'typing', isTyping: true })

// Status update
room.broadcast({ type: 'status', status: 'away' })

// Receive
room.on('message', (from, data) => {
  switch (data.type) {
    case 'chat':
      addChatMessage(from, data.text)
      break
    case 'emote':
      showEmote(from, data.emote)
      break
    case 'typing':
      setTypingIndicator(from, data.isTyping)
      break
    case 'status':
      updateUserStatus(from, data.status)
      break
  }
})

Private Messages

// Send to specific player
function sendPrivateMessage(toPlayerId, text) {
  room.send(toPlayerId, {
    type: 'dm',
    text,
    from: room.playerId
  })
}

// Receive
room.on('message', (from, data) => {
  if (data.type === 'dm') {
    addPrivateMessage(from, data.text)
  }
})

Full Example

<!DOCTYPE html>
<html>
<head>
  <title>Chat Room</title>
  <style>
    body { font-family: sans-serif; margin: 20px; }
    #messages { height: 300px; overflow-y: auto; border: 1px solid #ccc; padding: 10px; }
    .system { color: #888; font-style: italic; }
    #players { margin-top: 10px; color: #666; }
  </style>
</head>
<body>
  <h1>Chat Room</h1>
  <div id="messages"></div>
  <div id="players"></div>
  <form id="form">
    <input id="input" placeholder="Type a message..." autocomplete="off" />
    <button>Send</button>
  </form>

  <script type="module">
    import { connect } from 'https://unpkg.com/@watchtower-sdk/core'
    
    const name = prompt('Your name?') || 'Anonymous'
    const room = await connect(location.hash.slice(1) || undefined, { name })
    
    // Update URL with room code
    location.hash = room.code
    document.title = 'Chat: ' + room.code
    
    const messagesEl = document.getElementById('messages')
    const playersEl = document.getElementById('players')
    const form = document.getElementById('form')
    const input = document.getElementById('input')
    
    function addMessage(html) {
      messagesEl.innerHTML += html + '<br>'
      messagesEl.scrollTop = messagesEl.scrollHeight
    }
    
    function updatePlayers() {
      playersEl.textContent = 'Online: ' + room.players.map(p => p.name || p.id).join(', ')
    }
    
    // Initial player list
    updatePlayers()
    addMessage('<span class="system">Connected to room ' + room.code + '</span>')
    
    // Send message
    form.onsubmit = (e) => {
      e.preventDefault()
      if (!input.value.trim()) return
      
      room.broadcast({ type: 'chat', text: input.value, name })
      addMessage('<b>You:</b> ' + input.value)
      input.value = ''
    }
    
    // Receive messages
    room.on('message', (from, data) => {
      if (data.type === 'chat') {
        addMessage('<b>' + (data.name || from) + ':</b> ' + data.text)
      }
    })
    
    room.on('join', (player) => {
      addMessage('<span class="system">' + (player.name || player.id) + ' joined</span>')
      updatePlayers()
    })
    
    room.on('leave', (player) => {
      addMessage('<span class="system">' + (player.name || player.id) + ' left</span>')
      updatePlayers()
    })
  </script>
</body>
</html>

Next Steps

  • Add emoji reactions
  • Add typing indicators
  • Add user avatars in metadata