mirror of
https://github.com/iDisaster/GTAConnected.git
synced 2026-03-08 01:15:23 +00:00
Add custom chat UI with modern glossy design and welcome/leave messages
- Create client.js with glassmorphism-style chat UI matching mod menu theme - Add animated player join/leave notifications (slide-in from right) - Implement custom chat rendering with timestamps, scrolling, and message types - Support for multiple chat types: normal, action, whisper, shout, OOC, local, system - Add smooth fade animation when chat is idle - Update server.js with network events for custom UI communication - Add OnPlayerJoined/OnPlayerQuit handlers for welcome/leave broadcasts
This commit is contained in:
579
resources/chat/client.js
Normal file
579
resources/chat/client.js
Normal file
@@ -0,0 +1,579 @@
|
||||
// ============================================================================
|
||||
// CUSTOM CHAT UI - Client Side
|
||||
// Modern glossy UI matching MD Revolution menu style
|
||||
// ============================================================================
|
||||
|
||||
// ============================================================================
|
||||
// UI THEME - Dark Glassmorphism (matching mod menu)
|
||||
// ============================================================================
|
||||
const UI = {
|
||||
// Background colors
|
||||
bgDark: { r: 13, g: 17, b: 23 }, // Deep dark background
|
||||
bgPanel: { r: 22, g: 27, b: 34 }, // Panel background
|
||||
bgHover: { r: 33, g: 38, b: 45 }, // Hover state
|
||||
bgSelected: { r: 45, g: 50, b: 58 }, // Selected item
|
||||
bgMessage: { r: 18, g: 22, b: 28 }, // Message background
|
||||
|
||||
// Accent colors - clean cyan
|
||||
accent: { r: 0, g: 212, b: 255 }, // Primary accent (cyan)
|
||||
accentDim: { r: 0, g: 150, b: 180 }, // Dimmed accent
|
||||
accentGlow: { r: 0, g: 180, b: 220 }, // Glow effect
|
||||
|
||||
// Text colors
|
||||
textPrimary: { r: 230, g: 237, b: 243 }, // Primary text (almost white)
|
||||
textSecondary: { r: 139, g: 148, b: 158 }, // Secondary text (gray)
|
||||
textMuted: { r: 88, g: 96, b: 105 }, // Muted text
|
||||
|
||||
// Status colors
|
||||
success: { r: 63, g: 185, b: 80 }, // Green for joins
|
||||
error: { r: 248, g: 81, b: 73 }, // Red for leaves
|
||||
warning: { r: 210, g: 153, b: 34 }, // Yellow/gold for warnings
|
||||
info: { r: 88, g: 166, b: 255 }, // Blue for info
|
||||
|
||||
// Chat specific colors
|
||||
playerName: { r: 0, g: 212, b: 255 }, // Player name color (cyan)
|
||||
chatWhite: { r: 255, g: 255, b: 255 }, // Normal chat
|
||||
chatAction: { r: 200, g: 100, b: 255 }, // /me actions (purple)
|
||||
chatWhisper: { r: 255, g: 255, b: 100 }, // Private messages (yellow)
|
||||
chatShout: { r: 255, g: 100, b: 100 }, // Shout (red)
|
||||
chatOOC: { r: 150, g: 150, b: 150 }, // OOC (gray)
|
||||
chatSystem: { r: 100, g: 200, b: 255 }, // System messages (light blue)
|
||||
chatLocal: { r: 200, g: 255, b: 200 }, // Local chat (light green)
|
||||
|
||||
// Border colors
|
||||
border: { r: 48, g: 54, b: 61 }, // Subtle border
|
||||
borderFocus: { r: 0, g: 212, b: 255 } // Focused border (accent)
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// CHAT STATE
|
||||
// ============================================================================
|
||||
let chatMessages = [];
|
||||
const maxMessages = 50;
|
||||
let chatVisible = true;
|
||||
let chatInputActive = false;
|
||||
let chatInputText = "";
|
||||
let chatScrollOffset = 0;
|
||||
let chatFont = null;
|
||||
|
||||
// Animation state
|
||||
let animTime = 0;
|
||||
let chatFadeAlpha = 1.0;
|
||||
let lastMessageTime = 0;
|
||||
let chatFadeDelay = 10000; // 10 seconds before fade
|
||||
let chatFadeSpeed = 0.02;
|
||||
|
||||
// Welcome/Leave notification queue
|
||||
let notifications = [];
|
||||
let notificationDuration = 5000; // 5 seconds
|
||||
|
||||
// Chat dimensions
|
||||
const chat = {
|
||||
x: 25,
|
||||
y: 200,
|
||||
width: 550,
|
||||
messageHeight: 22,
|
||||
maxVisibleMessages: 12,
|
||||
padding: 12,
|
||||
inputHeight: 36,
|
||||
borderRadius: 4
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// RESOURCE START - Load font
|
||||
// ============================================================================
|
||||
addEventHandler("OnResourceStart", function(event, resource) {
|
||||
if (resource == thisResource) {
|
||||
try {
|
||||
chatFont = lucasFont;
|
||||
console.log("[Chat] Custom chat UI initialized");
|
||||
} catch(e) {
|
||||
console.log("[Chat] Font load error: " + e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// NETWORK EVENTS - Receive messages from server
|
||||
// ============================================================================
|
||||
addNetworkHandler("chatMessage", function(messageData) {
|
||||
addChatMessage(messageData.text, messageData.type || "normal", messageData.playerName || null);
|
||||
});
|
||||
|
||||
addNetworkHandler("playerJoined", function(data) {
|
||||
addNotification(data.name + " joined the server", "join");
|
||||
addChatMessage(data.name + " has joined the server", "system");
|
||||
});
|
||||
|
||||
addNetworkHandler("playerLeft", function(data) {
|
||||
let reason = data.reason || "Disconnected";
|
||||
addNotification(data.name + " left the server", "leave");
|
||||
addChatMessage(data.name + " has left the server (" + reason + ")", "system");
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// CHAT FUNCTIONS
|
||||
// ============================================================================
|
||||
|
||||
function addChatMessage(text, type, playerName) {
|
||||
let message = {
|
||||
text: text,
|
||||
type: type || "normal",
|
||||
playerName: playerName,
|
||||
timestamp: Date.now(),
|
||||
alpha: 1.0
|
||||
};
|
||||
|
||||
chatMessages.push(message);
|
||||
|
||||
// Limit messages
|
||||
if (chatMessages.length > maxMessages) {
|
||||
chatMessages.shift();
|
||||
}
|
||||
|
||||
// Reset fade timer
|
||||
lastMessageTime = Date.now();
|
||||
chatFadeAlpha = 1.0;
|
||||
|
||||
// Auto-scroll to bottom
|
||||
let totalMessages = chatMessages.length;
|
||||
if (totalMessages > chat.maxVisibleMessages) {
|
||||
chatScrollOffset = totalMessages - chat.maxVisibleMessages;
|
||||
}
|
||||
}
|
||||
|
||||
function addNotification(text, type) {
|
||||
notifications.push({
|
||||
text: text,
|
||||
type: type, // "join" or "leave"
|
||||
timestamp: Date.now(),
|
||||
alpha: 1.0,
|
||||
y: 0 // Will be animated
|
||||
});
|
||||
}
|
||||
|
||||
function getMessageColor(type) {
|
||||
switch(type) {
|
||||
case "action": return UI.chatAction;
|
||||
case "whisper": return UI.chatWhisper;
|
||||
case "shout": return UI.chatShout;
|
||||
case "ooc": return UI.chatOOC;
|
||||
case "system": return UI.chatSystem;
|
||||
case "local": return UI.chatLocal;
|
||||
case "join": return UI.success;
|
||||
case "leave": return UI.error;
|
||||
default: return UI.chatWhite;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// DRAWING FUNCTIONS
|
||||
// ============================================================================
|
||||
|
||||
function drawRect(x, y, w, h, colour) {
|
||||
try {
|
||||
let pos = new Vec2(x, y);
|
||||
let size = new Vec2(w, h);
|
||||
graphics.drawRectangle(null, pos, size, colour, colour, colour, colour);
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
function drawGradientRect(x, y, w, h, colourLeft, colourRight) {
|
||||
try {
|
||||
let pos = new Vec2(x, y);
|
||||
let size = new Vec2(w, h);
|
||||
graphics.drawRectangle(null, pos, size, colourLeft, colourRight, colourLeft, colourRight);
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
function drawGradientRectV(x, y, w, h, colourTop, colourBottom) {
|
||||
try {
|
||||
let pos = new Vec2(x, y);
|
||||
let size = new Vec2(w, h);
|
||||
graphics.drawRectangle(null, pos, size, colourTop, colourTop, colourBottom, colourBottom);
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
function drawText(text, x, y, colour, size) {
|
||||
if (chatFont != null) {
|
||||
try {
|
||||
let pos = new Vec2(x, y);
|
||||
chatFont.render(text, pos, chat.width - 20, 0.0, 0.0, size || 11, colour, false, false, false, true);
|
||||
} catch(e) {}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// RENDER CHAT UI
|
||||
// ============================================================================
|
||||
addEventHandler("OnDrawnHUD", function(event) {
|
||||
animTime += 0.016; // ~60fps
|
||||
|
||||
let screenWidth = 1920;
|
||||
let screenHeight = 1080;
|
||||
try {
|
||||
screenWidth = game.width || 1920;
|
||||
screenHeight = game.height || 1080;
|
||||
} catch(e) {}
|
||||
|
||||
// Update fade
|
||||
if (Date.now() - lastMessageTime > chatFadeDelay && !chatInputActive) {
|
||||
chatFadeAlpha = Math.max(0.3, chatFadeAlpha - chatFadeSpeed);
|
||||
}
|
||||
|
||||
// Draw notifications first (top right)
|
||||
drawNotifications(screenWidth, screenHeight);
|
||||
|
||||
// Skip chat drawing if hidden
|
||||
if (!chatVisible && !chatInputActive) return;
|
||||
|
||||
let alpha = Math.floor(255 * chatFadeAlpha);
|
||||
if (chatInputActive) alpha = 255;
|
||||
|
||||
// Calculate chat area
|
||||
let chatAreaHeight = chat.maxVisibleMessages * chat.messageHeight + chat.padding * 2;
|
||||
let totalHeight = chatAreaHeight + (chatInputActive ? chat.inputHeight + 8 : 0);
|
||||
|
||||
// ========================================
|
||||
// MAIN CHAT PANEL - Glossy background
|
||||
// ========================================
|
||||
|
||||
// Outer glow effect (subtle)
|
||||
let glowAlpha = Math.floor(30 * chatFadeAlpha);
|
||||
let glowCol = toColour(UI.accent.r, UI.accent.g, UI.accent.b, glowAlpha);
|
||||
drawRect(chat.x - 2, chat.y - 2, chat.width + 4, chatAreaHeight + 4, glowCol);
|
||||
|
||||
// Main panel background with transparency
|
||||
let bgAlpha = Math.floor(200 * chatFadeAlpha);
|
||||
let panelBg = toColour(UI.bgDark.r, UI.bgDark.g, UI.bgDark.b, bgAlpha);
|
||||
drawRect(chat.x, chat.y, chat.width, chatAreaHeight, panelBg);
|
||||
|
||||
// Glassmorphism overlay (subtle gradient)
|
||||
let glossTop = toColour(255, 255, 255, Math.floor(8 * chatFadeAlpha));
|
||||
let glossBottom = toColour(255, 255, 255, 0);
|
||||
drawGradientRectV(chat.x, chat.y, chat.width, 30, glossTop, glossBottom);
|
||||
|
||||
// Top accent line
|
||||
let accentAlpha = Math.floor(255 * chatFadeAlpha);
|
||||
let accentCol = toColour(UI.accent.r, UI.accent.g, UI.accent.b, accentAlpha);
|
||||
drawRect(chat.x, chat.y, chat.width, 2, accentCol);
|
||||
|
||||
// Side accent line (left)
|
||||
let sideAccent = toColour(UI.accent.r, UI.accent.g, UI.accent.b, Math.floor(150 * chatFadeAlpha));
|
||||
drawRect(chat.x, chat.y, 2, chatAreaHeight, sideAccent);
|
||||
|
||||
// ========================================
|
||||
// CHAT HEADER
|
||||
// ========================================
|
||||
let headerHeight = 28;
|
||||
let headerBg = toColour(UI.bgPanel.r, UI.bgPanel.g, UI.bgPanel.b, bgAlpha);
|
||||
drawRect(chat.x, chat.y + 2, chat.width, headerHeight, headerBg);
|
||||
|
||||
// Header text
|
||||
let headerTextCol = toColour(UI.accent.r, UI.accent.g, UI.accent.b, alpha);
|
||||
drawText("CHAT", chat.x + chat.padding, chat.y + 8, headerTextCol, 11);
|
||||
|
||||
// Online count (placeholder - can be updated via network)
|
||||
let onlineCol = toColour(UI.textSecondary.r, UI.textSecondary.g, UI.textSecondary.b, alpha);
|
||||
drawText("Press T to chat", chat.x + chat.width - 120, chat.y + 9, onlineCol, 9);
|
||||
|
||||
// Header bottom border
|
||||
let headerBorder = toColour(UI.border.r, UI.border.g, UI.border.b, Math.floor(100 * chatFadeAlpha));
|
||||
drawRect(chat.x + chat.padding, chat.y + headerHeight + 2, chat.width - chat.padding * 2, 1, headerBorder);
|
||||
|
||||
// ========================================
|
||||
// CHAT MESSAGES
|
||||
// ========================================
|
||||
let messageStartY = chat.y + headerHeight + chat.padding;
|
||||
let visibleCount = Math.min(chatMessages.length, chat.maxVisibleMessages);
|
||||
let startIndex = Math.max(0, chatMessages.length - chat.maxVisibleMessages - chatScrollOffset);
|
||||
|
||||
for (let i = 0; i < visibleCount && startIndex + i < chatMessages.length; i++) {
|
||||
let msg = chatMessages[startIndex + i];
|
||||
let msgY = messageStartY + (i * chat.messageHeight);
|
||||
|
||||
// Message background (alternating subtle)
|
||||
if (i % 2 === 0) {
|
||||
let msgBgAlpha = Math.floor(30 * chatFadeAlpha);
|
||||
let msgBg = toColour(UI.bgMessage.r, UI.bgMessage.g, UI.bgMessage.b, msgBgAlpha);
|
||||
drawRect(chat.x + 4, msgY - 2, chat.width - 8, chat.messageHeight, msgBg);
|
||||
}
|
||||
|
||||
// Get message color based on type
|
||||
let msgColor = getMessageColor(msg.type);
|
||||
let textCol = toColour(msgColor.r, msgColor.g, msgColor.b, alpha);
|
||||
|
||||
// Draw timestamp
|
||||
let time = new Date(msg.timestamp);
|
||||
let timeStr = "[" + padZero(time.getHours()) + ":" + padZero(time.getMinutes()) + "]";
|
||||
let timeCol = toColour(UI.textMuted.r, UI.textMuted.g, UI.textMuted.b, Math.floor(alpha * 0.7));
|
||||
drawText(timeStr, chat.x + chat.padding, msgY, timeCol, 9);
|
||||
|
||||
// Draw message text
|
||||
let textX = chat.x + chat.padding + 55;
|
||||
|
||||
if (msg.playerName && msg.type === "normal") {
|
||||
// Player name in accent color
|
||||
let nameCol = toColour(UI.playerName.r, UI.playerName.g, UI.playerName.b, alpha);
|
||||
drawText(msg.playerName + ":", textX, msgY, nameCol, 10);
|
||||
// Message text
|
||||
drawText(msg.text, textX + (msg.playerName.length * 7) + 12, msgY, textCol, 10);
|
||||
} else {
|
||||
drawText(msg.text, textX, msgY, textCol, 10);
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// SCROLLBAR (if needed)
|
||||
// ========================================
|
||||
if (chatMessages.length > chat.maxVisibleMessages) {
|
||||
let scrollbarX = chat.x + chat.width - 6;
|
||||
let scrollbarY = messageStartY;
|
||||
let scrollbarHeight = chat.maxVisibleMessages * chat.messageHeight;
|
||||
|
||||
// Scrollbar track
|
||||
let trackCol = toColour(UI.bgHover.r, UI.bgHover.g, UI.bgHover.b, Math.floor(100 * chatFadeAlpha));
|
||||
drawRect(scrollbarX, scrollbarY, 4, scrollbarHeight, trackCol);
|
||||
|
||||
// Scrollbar thumb
|
||||
let thumbHeight = Math.max(20, (chat.maxVisibleMessages / chatMessages.length) * scrollbarHeight);
|
||||
let thumbY = scrollbarY + ((startIndex / (chatMessages.length - chat.maxVisibleMessages)) * (scrollbarHeight - thumbHeight));
|
||||
let thumbCol = toColour(UI.accent.r, UI.accent.g, UI.accent.b, Math.floor(200 * chatFadeAlpha));
|
||||
drawRect(scrollbarX, thumbY, 4, thumbHeight, thumbCol);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// INPUT BOX (when active)
|
||||
// ========================================
|
||||
if (chatInputActive) {
|
||||
let inputY = chat.y + chatAreaHeight + 6;
|
||||
|
||||
// Input background
|
||||
let inputBg = toColour(UI.bgPanel.r, UI.bgPanel.g, UI.bgPanel.b, 240);
|
||||
drawRect(chat.x, inputY, chat.width, chat.inputHeight, inputBg);
|
||||
|
||||
// Input border glow
|
||||
let pulseAlpha = Math.floor(180 + Math.sin(animTime * 4) * 50);
|
||||
let inputBorder = toColour(UI.accent.r, UI.accent.g, UI.accent.b, pulseAlpha);
|
||||
drawRect(chat.x, inputY, chat.width, 2, inputBorder);
|
||||
drawRect(chat.x, inputY + chat.inputHeight - 2, chat.width, 2, inputBorder);
|
||||
drawRect(chat.x, inputY, 2, chat.inputHeight, inputBorder);
|
||||
drawRect(chat.x + chat.width - 2, inputY, 2, chat.inputHeight, inputBorder);
|
||||
|
||||
// Input label
|
||||
let labelCol = toColour(UI.textMuted.r, UI.textMuted.g, UI.textMuted.b, 255);
|
||||
drawText("Say:", chat.x + chat.padding, inputY + 10, labelCol, 10);
|
||||
|
||||
// Input text with cursor
|
||||
let cursorBlink = Math.floor(animTime * 2) % 2 === 0;
|
||||
let inputTextCol = toColour(UI.textPrimary.r, UI.textPrimary.g, UI.textPrimary.b, 255);
|
||||
let displayText = chatInputText + (cursorBlink ? "|" : "");
|
||||
drawText(displayText, chat.x + chat.padding + 40, inputY + 10, inputTextCol, 10);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// BOTTOM PANEL FADE
|
||||
// ========================================
|
||||
let fadeHeight = 20;
|
||||
let fadeTop = toColour(UI.bgDark.r, UI.bgDark.g, UI.bgDark.b, 0);
|
||||
let fadeBottom = toColour(UI.bgDark.r, UI.bgDark.g, UI.bgDark.b, bgAlpha);
|
||||
drawGradientRectV(chat.x, chat.y + chatAreaHeight - fadeHeight, chat.width, fadeHeight, fadeTop, fadeBottom);
|
||||
|
||||
// Bottom border
|
||||
drawRect(chat.x, chat.y + chatAreaHeight - 1, chat.width, 1, accentCol);
|
||||
});
|
||||
|
||||
// ========================================
|
||||
// DRAW NOTIFICATIONS (Join/Leave)
|
||||
// ========================================
|
||||
function drawNotifications(screenWidth, screenHeight) {
|
||||
let notifX = screenWidth - 350;
|
||||
let notifY = 100;
|
||||
let notifHeight = 45;
|
||||
let notifSpacing = 8;
|
||||
|
||||
// Update and draw each notification
|
||||
for (let i = notifications.length - 1; i >= 0; i--) {
|
||||
let notif = notifications[i];
|
||||
let age = Date.now() - notif.timestamp;
|
||||
|
||||
// Remove old notifications
|
||||
if (age > notificationDuration) {
|
||||
notifications.splice(i, 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Calculate alpha (fade in/out)
|
||||
let fadeIn = Math.min(1, age / 200);
|
||||
let fadeOut = Math.min(1, (notificationDuration - age) / 500);
|
||||
notif.alpha = fadeIn * fadeOut;
|
||||
|
||||
// Calculate slide animation
|
||||
let slideIn = Math.min(1, age / 300);
|
||||
let slideX = notifX + (1 - slideIn) * 100;
|
||||
|
||||
let alpha = Math.floor(255 * notif.alpha);
|
||||
let displayIndex = notifications.length - 1 - i;
|
||||
let yPos = notifY + displayIndex * (notifHeight + notifSpacing);
|
||||
|
||||
// Notification background
|
||||
let bgAlpha = Math.floor(220 * notif.alpha);
|
||||
let bgCol = toColour(UI.bgDark.r, UI.bgDark.g, UI.bgDark.b, bgAlpha);
|
||||
drawRect(slideX, yPos, 320, notifHeight, bgCol);
|
||||
|
||||
// Glossy top effect
|
||||
let glossTop = toColour(255, 255, 255, Math.floor(15 * notif.alpha));
|
||||
let glossBottom = toColour(255, 255, 255, 0);
|
||||
drawGradientRectV(slideX, yPos, 320, 15, glossTop, glossBottom);
|
||||
|
||||
// Left accent bar based on type
|
||||
let accentColor = notif.type === "join" ? UI.success : UI.error;
|
||||
let accentBarCol = toColour(accentColor.r, accentColor.g, accentColor.b, alpha);
|
||||
drawRect(slideX, yPos, 4, notifHeight, accentBarCol);
|
||||
|
||||
// Icon (+ for join, - for leave)
|
||||
let iconText = notif.type === "join" ? "+" : "-";
|
||||
let iconCol = toColour(accentColor.r, accentColor.g, accentColor.b, alpha);
|
||||
drawText(iconText, slideX + 15, yPos + 12, iconCol, 18);
|
||||
|
||||
// Notification text
|
||||
let textCol = toColour(UI.textPrimary.r, UI.textPrimary.g, UI.textPrimary.b, alpha);
|
||||
drawText(notif.text, slideX + 40, yPos + 14, textCol, 11);
|
||||
|
||||
// Subtitle
|
||||
let subtitleText = notif.type === "join" ? "Welcome to the server!" : "Goodbye!";
|
||||
let subtitleCol = toColour(UI.textSecondary.r, UI.textSecondary.g, UI.textSecondary.b, Math.floor(alpha * 0.7));
|
||||
drawText(subtitleText, slideX + 40, yPos + 28, subtitleCol, 9);
|
||||
|
||||
// Outer glow
|
||||
let glowAlpha = Math.floor(40 * notif.alpha);
|
||||
let glowCol = toColour(accentColor.r, accentColor.g, accentColor.b, glowAlpha);
|
||||
drawRect(slideX - 2, yPos - 2, 324, notifHeight + 4, glowCol);
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// HELPER FUNCTIONS
|
||||
// ========================================
|
||||
function padZero(num) {
|
||||
return num < 10 ? "0" + num : num.toString();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// INPUT HANDLING
|
||||
// ============================================================================
|
||||
addEventHandler("OnKeyUp", function(event, key, scancode, mods) {
|
||||
// T key to open chat
|
||||
if (key === SDLK_t && !chatInputActive) {
|
||||
chatInputActive = true;
|
||||
chatInputText = "";
|
||||
gui.showCursor(true, true);
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
// Escape to close chat
|
||||
if (key === SDLK_ESCAPE && chatInputActive) {
|
||||
chatInputActive = false;
|
||||
chatInputText = "";
|
||||
gui.showCursor(false, false);
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
// Enter to send message
|
||||
if (key === SDLK_RETURN && chatInputActive) {
|
||||
if (chatInputText.length > 0) {
|
||||
// Send message to server
|
||||
triggerNetworkEvent("chatSendMessage", chatInputText);
|
||||
}
|
||||
chatInputActive = false;
|
||||
chatInputText = "";
|
||||
gui.showCursor(false, false);
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
// Page up/down for scrolling
|
||||
if (key === SDLK_PAGEUP && chatMessages.length > chat.maxVisibleMessages) {
|
||||
chatScrollOffset = Math.min(chatScrollOffset + 3, chatMessages.length - chat.maxVisibleMessages);
|
||||
chatFadeAlpha = 1.0;
|
||||
lastMessageTime = Date.now();
|
||||
}
|
||||
if (key === SDLK_PAGEDOWN) {
|
||||
chatScrollOffset = Math.max(0, chatScrollOffset - 3);
|
||||
chatFadeAlpha = 1.0;
|
||||
lastMessageTime = Date.now();
|
||||
}
|
||||
});
|
||||
|
||||
// Handle text input
|
||||
addEventHandler("OnCharacter", function(event, character) {
|
||||
if (chatInputActive) {
|
||||
chatInputText += character;
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
addEventHandler("OnKeyDown", function(event, key, scancode, mods) {
|
||||
if (!chatInputActive) return;
|
||||
|
||||
// Backspace
|
||||
if (key === SDLK_BACKSPACE && chatInputText.length > 0) {
|
||||
chatInputText = chatInputText.slice(0, -1);
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// MOUSE WHEEL SCROLLING
|
||||
// ============================================================================
|
||||
addEventHandler("OnMouseWheel", function(event, x, y) {
|
||||
if (chatMessages.length <= chat.maxVisibleMessages) return;
|
||||
|
||||
// Check if mouse is over chat area
|
||||
let mouseX = gui.cursorPosition.x;
|
||||
let mouseY = gui.cursorPosition.y;
|
||||
|
||||
if (mouseX >= chat.x && mouseX <= chat.x + chat.width &&
|
||||
mouseY >= chat.y && mouseY <= chat.y + chat.maxVisibleMessages * chat.messageHeight + 50) {
|
||||
|
||||
if (y > 0) {
|
||||
chatScrollOffset = Math.min(chatScrollOffset + 2, chatMessages.length - chat.maxVisibleMessages);
|
||||
} else {
|
||||
chatScrollOffset = Math.max(0, chatScrollOffset - 2);
|
||||
}
|
||||
|
||||
chatFadeAlpha = 1.0;
|
||||
lastMessageTime = Date.now();
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// PROCESS - Animation updates
|
||||
// ============================================================================
|
||||
addEventHandler("OnProcess", function(event) {
|
||||
// Block game input while typing
|
||||
if (chatInputActive) {
|
||||
// Disable native chat
|
||||
try {
|
||||
chatInputEnabled = false;
|
||||
} catch(e) {}
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// DISABLE DEFAULT CHAT
|
||||
// ============================================================================
|
||||
addEventHandler("OnResourceReady", function(event, resource) {
|
||||
if (resource == thisResource) {
|
||||
try {
|
||||
// Disable default chat window
|
||||
setChatWindowEnabled(false);
|
||||
} catch(e) {
|
||||
console.log("[Chat] Could not disable default chat: " + e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log("[Chat] Client script loaded - Custom glossy UI enabled");
|
||||
6
resources/chat/meta.xml
Normal file
6
resources/chat/meta.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<meta>
|
||||
<info author="GTAConnected Server" type="script" version="2.0.0" description="Custom chat system with modern glossy UI and welcome/leave messages" />
|
||||
<script src="server.js" type="server" language="javascript" />
|
||||
<script src="client.js" type="client" language="javascript" />
|
||||
</meta>
|
||||
392
resources/chat/server.js
Normal file
392
resources/chat/server.js
Normal file
@@ -0,0 +1,392 @@
|
||||
// ============================================================================
|
||||
// CHAT RESOURCE - Server Side
|
||||
// Enhanced chat system with custom UI, welcome/leave messages, and features
|
||||
// ============================================================================
|
||||
|
||||
// Chat colors using toColour for integer format
|
||||
const COLOUR_WHITE = toColour(255, 255, 255, 255);
|
||||
const COLOUR_ACTION = toColour(200, 100, 255, 255);
|
||||
const COLOUR_WHISPER = toColour(255, 255, 100, 255);
|
||||
const COLOUR_SHOUT = toColour(255, 100, 100, 255);
|
||||
const COLOUR_OOC = toColour(150, 150, 150, 255);
|
||||
const COLOUR_ADMIN = toColour(255, 100, 100, 255);
|
||||
const COLOUR_SYSTEM = toColour(100, 200, 255, 255);
|
||||
const COLOUR_ORANGE = toColour(255, 200, 100, 255);
|
||||
const COLOUR_ERROR = toColour(255, 100, 100, 255);
|
||||
const COLOUR_GRAY = toColour(200, 200, 200, 255);
|
||||
const COLOUR_LOCAL = toColour(200, 255, 200, 255);
|
||||
const COLOUR_JOIN = toColour(63, 185, 80, 255);
|
||||
const COLOUR_LEAVE = toColour(248, 81, 73, 255);
|
||||
|
||||
// Chat history (for logging purposes)
|
||||
let chatHistory = [];
|
||||
const maxHistory = 100;
|
||||
|
||||
// Store last PM sender for reply function
|
||||
let lastPMSender = {};
|
||||
|
||||
// ============================================================================
|
||||
// EVENTS
|
||||
// ============================================================================
|
||||
|
||||
addEventHandler("OnResourceStart", function(event, resource) {
|
||||
if (resource == thisResource) {
|
||||
console.log("[Chat] Resource started - Custom chat system active");
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// PLAYER JOIN/LEAVE EVENTS
|
||||
// ============================================================================
|
||||
|
||||
addEventHandler("OnPlayerJoined", function(event, client) {
|
||||
// Broadcast join notification to all clients
|
||||
let joinData = {
|
||||
name: client.name
|
||||
};
|
||||
|
||||
// Send to all connected clients
|
||||
let clients = getClients();
|
||||
for (let i = 0; i < clients.length; i++) {
|
||||
triggerNetworkEvent("playerJoined", clients[i], joinData);
|
||||
}
|
||||
|
||||
// Log to console
|
||||
console.log("[Chat] " + client.name + " joined the server");
|
||||
|
||||
// Also send via native message for fallback
|
||||
message(client.name + " has joined the server", COLOUR_JOIN);
|
||||
});
|
||||
|
||||
addEventHandler("OnPlayerQuit", function(event, client, reason) {
|
||||
// Get disconnect reason text
|
||||
let reasonText = getDisconnectReason(reason);
|
||||
|
||||
// Broadcast leave notification to all remaining clients
|
||||
let leaveData = {
|
||||
name: client.name,
|
||||
reason: reasonText
|
||||
};
|
||||
|
||||
let clients = getClients();
|
||||
for (let i = 0; i < clients.length; i++) {
|
||||
if (clients[i].index !== client.index) {
|
||||
triggerNetworkEvent("playerLeft", clients[i], leaveData);
|
||||
}
|
||||
}
|
||||
|
||||
// Log to console
|
||||
console.log("[Chat] " + client.name + " left the server (" + reasonText + ")");
|
||||
|
||||
// Also send via native message for fallback
|
||||
message(client.name + " has left the server (" + reasonText + ")", COLOUR_LEAVE);
|
||||
|
||||
// Clear PM data
|
||||
delete lastPMSender[client.index];
|
||||
});
|
||||
|
||||
function getDisconnectReason(reason) {
|
||||
switch(reason) {
|
||||
case 0: return "Disconnected";
|
||||
case 1: return "Timed Out";
|
||||
case 2: return "Kicked";
|
||||
case 3: return "Banned";
|
||||
case 4: return "Connection Lost";
|
||||
default: return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CHAT MESSAGE HANDLING
|
||||
// ============================================================================
|
||||
|
||||
addEventHandler("OnPlayerChat", function(event, client, messageText) {
|
||||
// Broadcast to all clients via custom network event
|
||||
let chatData = {
|
||||
text: messageText,
|
||||
type: "normal",
|
||||
playerName: client.name
|
||||
};
|
||||
|
||||
let clients = getClients();
|
||||
for (let i = 0; i < clients.length; i++) {
|
||||
triggerNetworkEvent("chatMessage", clients[i], chatData);
|
||||
}
|
||||
|
||||
// Log to history
|
||||
logChat(client.name, messageText, "chat");
|
||||
|
||||
// Prevent default chat handling (we use custom UI)
|
||||
return false;
|
||||
});
|
||||
|
||||
// Network event handler for chat messages from custom UI
|
||||
addNetworkHandler("chatSendMessage", function(client, messageText) {
|
||||
if (!messageText || messageText.length === 0) return;
|
||||
|
||||
// Check if it's a command
|
||||
if (messageText.charAt(0) === '/') {
|
||||
// Parse command
|
||||
let parts = messageText.substring(1).split(" ");
|
||||
let command = parts[0].toLowerCase();
|
||||
let params = parts.slice(1).join(" ");
|
||||
|
||||
handleCommand(client, command, params);
|
||||
return;
|
||||
}
|
||||
|
||||
// Regular chat message - broadcast to all clients
|
||||
let chatData = {
|
||||
text: messageText,
|
||||
type: "normal",
|
||||
playerName: client.name
|
||||
};
|
||||
|
||||
let clients = getClients();
|
||||
for (let i = 0; i < clients.length; i++) {
|
||||
triggerNetworkEvent("chatMessage", clients[i], chatData);
|
||||
}
|
||||
|
||||
// Log to history
|
||||
logChat(client.name, messageText, "chat");
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// COMMAND HANDLING
|
||||
// ============================================================================
|
||||
|
||||
addEventHandler("OnPlayerCommand", function(event, client, command, params) {
|
||||
handleCommand(client, command.toLowerCase(), params);
|
||||
});
|
||||
|
||||
function handleCommand(client, cmd, params) {
|
||||
switch(cmd) {
|
||||
case "pm":
|
||||
case "msg":
|
||||
case "whisper":
|
||||
case "w":
|
||||
if (params && params.length > 0) {
|
||||
let parts = params.split(" ");
|
||||
let targetName = parts[0];
|
||||
let messageText = parts.slice(1).join(" ");
|
||||
|
||||
if (messageText.length > 0) {
|
||||
sendPrivateMessage(client, targetName, messageText);
|
||||
} else {
|
||||
sendSystemMessage(client, "[USAGE] /pm <player> <message>");
|
||||
}
|
||||
} else {
|
||||
sendSystemMessage(client, "[USAGE] /pm <player> <message>");
|
||||
}
|
||||
break;
|
||||
|
||||
case "me":
|
||||
case "action":
|
||||
if (params && params.length > 0) {
|
||||
broadcastChatMessage("* " + client.name + " " + params, "action");
|
||||
logChat(client.name, params, "action");
|
||||
} else {
|
||||
sendSystemMessage(client, "[USAGE] /me <action>");
|
||||
}
|
||||
break;
|
||||
|
||||
case "do":
|
||||
if (params && params.length > 0) {
|
||||
broadcastChatMessage("* " + params + " (" + client.name + ")", "action");
|
||||
logChat(client.name, params, "do");
|
||||
} else {
|
||||
sendSystemMessage(client, "[USAGE] /do <description>");
|
||||
}
|
||||
break;
|
||||
|
||||
case "shout":
|
||||
case "s":
|
||||
if (params && params.length > 0) {
|
||||
broadcastChatMessage(client.name + " shouts: " + params.toUpperCase() + "!", "shout");
|
||||
logChat(client.name, params, "shout");
|
||||
} else {
|
||||
sendSystemMessage(client, "[USAGE] /shout <message>");
|
||||
}
|
||||
break;
|
||||
|
||||
case "ooc":
|
||||
case "b":
|
||||
if (params && params.length > 0) {
|
||||
broadcastChatMessage("(( " + client.name + ": " + params + " ))", "ooc");
|
||||
logChat(client.name, params, "ooc");
|
||||
} else {
|
||||
sendSystemMessage(client, "[USAGE] /ooc <message>");
|
||||
}
|
||||
break;
|
||||
|
||||
case "local":
|
||||
case "l":
|
||||
if (params && params.length > 0) {
|
||||
sendLocalMessage(client, params);
|
||||
} else {
|
||||
sendSystemMessage(client, "[USAGE] /local <message>");
|
||||
}
|
||||
break;
|
||||
|
||||
case "reply":
|
||||
case "r":
|
||||
if (params && params.length > 0) {
|
||||
replyToLastPM(client, params);
|
||||
} else {
|
||||
sendSystemMessage(client, "[USAGE] /r <message>");
|
||||
}
|
||||
break;
|
||||
|
||||
case "clear":
|
||||
case "cls":
|
||||
// Clear chat for this player
|
||||
sendSystemMessage(client, "Chat cleared");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// MESSAGE FUNCTIONS
|
||||
// ============================================================================
|
||||
|
||||
function broadcastChatMessage(text, type, senderName) {
|
||||
let chatData = {
|
||||
text: text,
|
||||
type: type || "normal",
|
||||
playerName: senderName || null
|
||||
};
|
||||
|
||||
let clients = getClients();
|
||||
for (let i = 0; i < clients.length; i++) {
|
||||
triggerNetworkEvent("chatMessage", clients[i], chatData);
|
||||
}
|
||||
}
|
||||
|
||||
function sendChatMessage(client, text, type, playerName) {
|
||||
let chatData = {
|
||||
text: text,
|
||||
type: type || "normal",
|
||||
playerName: playerName || null
|
||||
};
|
||||
|
||||
triggerNetworkEvent("chatMessage", client, chatData);
|
||||
}
|
||||
|
||||
function sendSystemMessage(client, text) {
|
||||
sendChatMessage(client, text, "system");
|
||||
// Also send via native for fallback
|
||||
messageClient(text, client, COLOUR_SYSTEM);
|
||||
}
|
||||
|
||||
function sendPrivateMessage(sender, targetName, messageText) {
|
||||
let target = findPlayer(targetName);
|
||||
|
||||
if (target) {
|
||||
if (target.index === sender.index) {
|
||||
sendSystemMessage(sender, "[PM] You cannot message yourself!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Send to target via custom UI
|
||||
sendChatMessage(target, "[PM from " + sender.name + "]: " + messageText, "whisper");
|
||||
|
||||
// Confirm to sender via custom UI
|
||||
sendChatMessage(sender, "[PM to " + target.name + "]: " + messageText, "whisper");
|
||||
|
||||
// Store for reply function
|
||||
lastPMSender[target.index] = sender.index;
|
||||
|
||||
logChat(sender.name, "-> " + target.name + ": " + messageText, "pm");
|
||||
} else {
|
||||
sendSystemMessage(sender, "[PM] Player not found: " + targetName);
|
||||
}
|
||||
}
|
||||
|
||||
function replyToLastPM(client, messageText) {
|
||||
if (lastPMSender[client.index] !== undefined) {
|
||||
let targetIndex = lastPMSender[client.index];
|
||||
let clients = getClients();
|
||||
let target = null;
|
||||
|
||||
for (let i = 0; i < clients.length; i++) {
|
||||
if (clients[i].index === targetIndex) {
|
||||
target = clients[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (target) {
|
||||
sendPrivateMessage(client, target.name, messageText);
|
||||
} else {
|
||||
sendSystemMessage(client, "[PM] The player you're trying to reply to is no longer online");
|
||||
}
|
||||
} else {
|
||||
sendSystemMessage(client, "[PM] No one has messaged you yet");
|
||||
}
|
||||
}
|
||||
|
||||
function sendLocalMessage(sender, messageText) {
|
||||
if (!sender.player) {
|
||||
sendSystemMessage(sender, "[LOCAL] You need to spawn first!");
|
||||
return;
|
||||
}
|
||||
|
||||
let senderPos = sender.player.position;
|
||||
let localRange = 30.0; // 30 units range for local chat
|
||||
let clients = getClients();
|
||||
|
||||
// Send to nearby players
|
||||
for (let i = 0; i < clients.length; i++) {
|
||||
let client = clients[i];
|
||||
if (client.player) {
|
||||
let clientPos = client.player.position;
|
||||
let distance = getDistance(senderPos, clientPos);
|
||||
|
||||
if (distance <= localRange) {
|
||||
sendChatMessage(client, "(Local) " + sender.name + ": " + messageText, "local");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logChat(sender.name, messageText, "local");
|
||||
}
|
||||
|
||||
function findPlayer(name) {
|
||||
let clients = getClients();
|
||||
let nameLower = name.toLowerCase();
|
||||
|
||||
for (let i = 0; i < clients.length; i++) {
|
||||
if (clients[i].name.toLowerCase() === nameLower ||
|
||||
clients[i].name.toLowerCase().indexOf(nameLower) !== -1) {
|
||||
return clients[i];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getDistance(pos1, pos2) {
|
||||
let dx = pos1.x - pos2.x;
|
||||
let dy = pos1.y - pos2.y;
|
||||
let dz = pos1.z - pos2.z;
|
||||
return Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||||
}
|
||||
|
||||
function logChat(playerName, messageText, type) {
|
||||
let entry = {
|
||||
time: new Date().toISOString(),
|
||||
player: playerName,
|
||||
message: messageText,
|
||||
type: type
|
||||
};
|
||||
|
||||
chatHistory.push(entry);
|
||||
|
||||
// Keep history limited
|
||||
if (chatHistory.length > maxHistory) {
|
||||
chatHistory.shift();
|
||||
}
|
||||
|
||||
console.log("[Chat][" + type + "] " + playerName + ": " + messageText);
|
||||
}
|
||||
|
||||
console.log("[Chat] Server script loaded - Welcome/Leave messages enabled!");
|
||||
Reference in New Issue
Block a user