Chat Systeem
Real-time chat, berichten, en communicatie.
Overzicht
┌─────────────────────────────────────────────────────────────────┐
│ Chat Types │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Global Chat Club Chat Whisper (DM) │
│ ─────────── ───────── ───────────── │
│ • Alle spelers • Club leden • 1-op-1 │
│ • Moderated • Privé • Vrienden only │
│ • Rate limited • Announcements • Offline berichten │
│ │
│ Local Chat (Scene-based) │
│ ──────────────────────── │
│ • Alleen zichtbaar in dezelfde scene │
│ • Bubbels boven karakter │
│ │
└─────────────────────────────────────────────────────────────────┘
Chat UI
Layout
┌─────────────────────────────────────────────────────────────────┐
│ [Globaal] [Club] [Whisper] [−] [×] │
├─────────────────────────────────────────────────────────────────┤
│ │
│ [12:34] Milena: Hallo iedereen! │
│ [12:35] Buurman: Hey Milena! Hoe gaat het? │
│ [12:35] Oma Wilma: Goedemorgen lieverd │
│ [12:36] → Milena joined the treehouse │
│ [12:37] Milena: Super! Wie wil er samen tuinieren? │
│ │
├─────────────────────────────────────────────────────────────────┤
│ [Typ een bericht... ] [📎] [😀] [→]│
└─────────────────────────────────────────────────────────────────┘
Scene Structuur
ChatPanel (Control)
├── Header (HBoxContainer)
│ ├── TabContainer
│ │ ├── GlobalTab
│ │ ├── ClubTab
│ │ └── WhisperTab
│ ├── MinimizeButton
│ └── CloseButton
├── MessageList (ScrollContainer)
│ └── MessageContainer (VBoxContainer)
│ └── [ChatMessage instances]
├── InputContainer (HBoxContainer)
│ ├── MessageInput (LineEdit)
│ ├── AttachButton
│ ├── EmojiButton
│ └── SendButton
└── EmojiPicker (hidden)
Chat Manager
# chat_manager.gd
extends Node
signal message_received(message: Dictionary)
signal whisper_received(from_player: String, message: String)
const MAX_MESSAGE_LENGTH = 200
const RATE_LIMIT_MESSAGES = 5
const RATE_LIMIT_WINDOW = 10.0 # seconds
var _message_timestamps: Array[float] = []
var _current_channel: String = "global"
func _ready() -> void:
SpacetimeDB.ChatMessage.on_insert.connect(_on_message_inserted)
func send_message(content: String, channel: String = "global") -> bool:
# Validate
if content.is_empty():
return false
if content.length() > MAX_MESSAGE_LENGTH:
NotificationManager.toast("Bericht te lang!", "warning")
return false
# Rate limit check
if not _check_rate_limit():
NotificationManager.toast("Wacht even voor je volgende bericht", "warning")
return false
# Filter content
var filtered_content = _filter_content(content)
# Send to server
SpacetimeDB.call_reducer("send_chat_message", [
filtered_content,
channel
])
return true
func _check_rate_limit() -> bool:
var current_time = Time.get_unix_time_from_system()
# Remove old timestamps
_message_timestamps = _message_timestamps.filter(func(t):
return current_time - t < RATE_LIMIT_WINDOW
)
if _message_timestamps.size() >= RATE_LIMIT_MESSAGES:
return false
_message_timestamps.append(current_time)
return true
func _filter_content(content: String) -> String:
# Basic profanity filter
var filtered = content
for word in BadWordsList.get_words():
filtered = filtered.replacen(word, "***")
return filtered
Message Display
# chat_panel.gd
extends Control
@onready var message_container: VBoxContainer = $MessageList/MessageContainer
@onready var message_input: LineEdit = $InputContainer/MessageInput
@onready var scroll_container: ScrollContainer = $MessageList
const CHAT_MESSAGE_SCENE = preload("res://scenes/ui/chat_message.tscn")
const MAX_VISIBLE_MESSAGES = 100
var _messages: Array[Dictionary] = []
func _on_message_received(message: Dictionary) -> void:
# Filter by current channel
if message.channel != _current_channel:
return
_add_message(message)
func _add_message(message: Dictionary) -> void:
_messages.append(message)
# Create message UI
var msg_node = CHAT_MESSAGE_SCENE.instantiate()
msg_node.setup(message)
message_container.add_child(msg_node)
# Trim old messages
while message_container.get_child_count() > MAX_VISIBLE_MESSAGES:
var oldest = message_container.get_child(0)
oldest.queue_free()
# Scroll to bottom
await get_tree().process_frame
scroll_container.scroll_vertical = scroll_container.get_v_scroll_bar().max_value
Message Component
# chat_message.gd
extends HBoxContainer
@onready var timestamp_label: Label = $Timestamp
@onready var name_label: Label = $PlayerName
@onready var content_label: RichTextLabel = $Content
func setup(message: Dictionary) -> void:
# Timestamp
var time = Time.get_datetime_dict_from_unix_time(message.timestamp)
timestamp_label.text = "[%02d:%02d]" % [time.hour, time.minute]
# Player name
name_label.text = message.sender_name + ":"
# Color based on player role
match message.sender_role:
"admin":
name_label.modulate = Color("#FF5555")
"moderator":
name_label.modulate = Color("#55FF55")
"vip":
name_label.modulate = Color("#FFAA00")
_:
name_label.modulate = Color.WHITE
# Content (with emoji support)
content_label.text = _parse_emojis(message.content)
func _parse_emojis(text: String) -> String:
# Replace emoji codes with actual emojis
var result = text
for code in EmojiDatabase.CODES:
result = result.replace(":" + code + ":", EmojiDatabase.CODES[code])
return result
Whisper System
func send_whisper(target_player_id: int, content: String) -> void:
if content.is_empty():
return
# Check if friends
if not FriendManager.is_friend(target_player_id):
NotificationManager.toast("Je kunt alleen naar vrienden fluisteren", "warning")
return
var filtered = _filter_content(content)
SpacetimeDB.call_reducer("send_whisper", [
target_player_id,
filtered
])
# Add to local whisper history
_add_whisper_to_history(target_player_id, filtered, true)
func _on_whisper_received(row: WhisperMessage) -> void:
if row.target_id != GameState.player_id:
return
var sender_name = OnlinePlayersManager.get_player_name(row.sender_id)
# Notification
NotificationManager.toast(
"Whisper van %s" % sender_name,
"info"
)
# Add to history
_add_whisper_to_history(row.sender_id, row.content, false)
whisper_received.emit(sender_name, row.content)
Chat Bubbles
Above Character
# chat_bubble.gd
extends Control
@onready var label: Label = $Panel/Label
@onready var panel: NinePatchRect = $Panel
const DISPLAY_TIME: float = 5.0
const FADE_TIME: float = 0.5
func show_message(message: String) -> void:
label.text = message
# Auto-size panel
panel.custom_minimum_size.x = min(label.get_minimum_size().x + 20, 200)
visible = true
modulate.a = 1.0
# Hide after delay
await get_tree().create_timer(DISPLAY_TIME).timeout
_fade_out()
func _fade_out() -> void:
var tween = create_tween()
tween.tween_property(self, "modulate:a", 0.0, FADE_TIME)
tween.tween_callback(func(): visible = false)
Local Scene Chat
# scene_chat_manager.gd
func _on_local_message(row: ChatMessage) -> void:
if row.channel != "local":
return
# Find player node
var player_node = _find_player_node(row.sender_id)
if not player_node:
return
# Show bubble
var bubble = player_node.get_node("ChatBubble")
bubble.show_message(row.content)
Emoji Picker
# emoji_picker.gd
extends Control
signal emoji_selected(emoji: String)
const CATEGORIES = ["smileys", "animals", "food", "activities", "objects"]
@onready var category_tabs: HBoxContainer = $CategoryTabs
@onready var emoji_grid: GridContainer = $EmojiGrid
var current_category: String = "smileys"
func _ready() -> void:
_setup_categories()
_load_emojis()
func _load_emojis() -> void:
for child in emoji_grid.get_children():
child.queue_free()
var emojis = EmojiDatabase.get_by_category(current_category)
for emoji in emojis:
var btn = Button.new()
btn.text = emoji
btn.custom_minimum_size = Vector2(40, 40)
btn.pressed.connect(func(): _on_emoji_pressed(emoji))
emoji_grid.add_child(btn)
func _on_emoji_pressed(emoji: String) -> void:
emoji_selected.emit(emoji)
hide()
Quick Chat
Preset Messages
# Voor snelle communicatie
const QUICK_MESSAGES = [
"Hallo!",
"Tot ziens!",
"Dank je wel!",
"Graag gedaan!",
"Zullen we samen spelen?",
"Leuke tuin!",
"Mooi huisdier!",
"Gefeliciteerd!"
]
func show_quick_chat() -> void:
var menu = preload("res://scenes/ui/quick_chat_menu.tscn").instantiate()
menu.set_messages(QUICK_MESSAGES)
menu.message_selected.connect(_on_quick_message_selected)
add_child(menu)
func _on_quick_message_selected(message: String) -> void:
send_message(message, "local")
Moderation
# Report system
func report_message(message_id: int, reason: String) -> void:
SpacetimeDB.call_reducer("report_chat_message", [
message_id,
reason
])
NotificationManager.toast("Bericht gerapporteerd", "info")
# Mute player
func mute_player(player_id: int) -> void:
_muted_players.append(player_id)
SaveData.set_value("muted_players", _muted_players)
func is_muted(player_id: int) -> bool:
return player_id in _muted_players