extended animation support, add /anim command (TODO: animation list, command help)

This commit is contained in:
Elijah R 2024-07-28 23:01:26 -04:00
parent 5550303284
commit 59efab9300
10 changed files with 155 additions and 18 deletions

View file

@ -378,6 +378,12 @@ export class Agent {
if (index !== -1) this.playAnimation(index, finishCallback); if (index !== -1) this.playAnimation(index, finishCallback);
} }
playAnimationByNamePromise(name: string): Promise<void> {
return new Promise((res, rej) => {
this.playAnimationByName(name, () => res());
});
}
setUsername(username: string, color: string) { setUsername(username: string, color: string) {
if (this.usernameBalloonState !== null) { if (this.usernameBalloonState !== null) {
this.usernameBalloonState.finish(); this.usernameBalloonState.finish();
@ -420,13 +426,10 @@ export class Agent {
this.y = randint(0, document.documentElement.clientHeight - this.data.characterInfo.charHeight); this.y = randint(0, document.documentElement.clientHeight - this.data.characterInfo.charHeight);
this.setLoc(); this.setLoc();
this.cnv.style.display = 'block'; this.cnv.style.display = 'block';
this.playAnimationByName('Show', () => {});
} }
hide(remove: boolean = false) { hide(remove: boolean = false) {
this.playAnimationByName('Hide', () => {
if (remove) this.remove(); if (remove) this.remove();
else this.cnv.style.display = 'none'; else this.cnv.style.display = 'none';
});
} }
} }

View file

@ -5,6 +5,7 @@ export enum MSAgentProtocolMessageType {
KeepAlive = 'nop', KeepAlive = 'nop',
Join = 'join', Join = 'join',
Talk = 'talk', Talk = 'talk',
PlayAnimation = 'anim',
SendImage = 'img', SendImage = 'img',
Admin = 'admin', Admin = 'admin',
// Server-to-client // Server-to-client
@ -20,6 +21,14 @@ export interface MSAgentProtocolMessage {
op: MSAgentProtocolMessageType; op: MSAgentProtocolMessageType;
} }
export interface AgentAnimationConfig {
join: string[];
chat: string[];
idle: string[];
rest: string[];
leave: string[];
}
// Client-to-server // Client-to-server
export interface MSAgentJoinMessage extends MSAgentProtocolMessage { export interface MSAgentJoinMessage extends MSAgentProtocolMessage {
@ -37,6 +46,14 @@ export interface MSAgentTalkMessage extends MSAgentProtocolMessage {
}; };
} }
export interface MSAgentPlayAnimationMessage extends MSAgentProtocolMessage {
op: MSAgentProtocolMessageType.PlayAnimation;
data: {
anim: string;
};
}
export interface MSAgentSendImageMessage extends MSAgentProtocolMessage { export interface MSAgentSendImageMessage extends MSAgentProtocolMessage {
op: MSAgentProtocolMessageType.SendImage; op: MSAgentProtocolMessageType.SendImage;
data: { data: {
@ -56,6 +73,7 @@ export interface MSAgentInitMessage extends MSAgentProtocolMessage {
username: string; username: string;
agent: string; agent: string;
admin: boolean; admin: boolean;
animations: AgentAnimationConfig;
}[]; }[];
}; };
} }
@ -65,6 +83,7 @@ export interface MSAgentAddUserMessage extends MSAgentProtocolMessage {
data: { data: {
username: string; username: string;
agent: string; agent: string;
animations: AgentAnimationConfig;
}; };
} }
@ -84,6 +103,14 @@ export interface MSAgentChatMessage extends MSAgentProtocolMessage {
}; };
} }
export interface MSAgentAnimationMessage extends MSAgentProtocolMessage {
op: MSAgentProtocolMessageType.PlayAnimation;
data: {
username: string;
anim: string;
};
}
export interface MSAgentImageMessage extends MSAgentProtocolMessage { export interface MSAgentImageMessage extends MSAgentProtocolMessage {
op: MSAgentProtocolMessageType.SendImage; op: MSAgentProtocolMessageType.SendImage;
data: { data: {

View file

@ -23,6 +23,7 @@ bannedWords = []
[chat.ratelimits] [chat.ratelimits]
chat = {seconds = 10, limit = 8} chat = {seconds = 10, limit = 8}
anim = {seconds = 10, limit = 8}
[motd] [motd]
version = 1 version = 1
@ -58,71 +59,89 @@ expirySeconds = 60
[[agents]] [[agents]]
friendlyName = "Clippy" friendlyName = "Clippy"
filename = "CLIPPIT.ACS" filename = "CLIPPIT.ACS"
animations = { join = ["Greeting"], chat = ["Explain"], idle = [], leave = ["GoodBye"], rest = ["RestPose"] }
[[agents]] [[agents]]
friendlyName = "Courtney" friendlyName = "Courtney"
filename = "courtney.acs" filename = "courtney.acs"
animations = { join = ["Show", "Greet"], chat = ["GetAttentionMinor"], idle = [], leave = ["Disappear"], rest = ["RestPose"] }
[[agents]] [[agents]]
friendlyName = "Dot" friendlyName = "Dot"
filename = "DOT.ACS" filename = "DOT.ACS"
animations = { join = ["Greeting", "Show"], chat = ["Alert", "Show"], idle = [], leave = ["Goodbye"], rest = ["RestPose"] }
[[agents]] [[agents]]
friendlyName = "Earl" friendlyName = "Earl"
filename = "earl.acs" filename = "earl.acs"
animations = { join = ["Show", "Greet"], chat = ["LookUp"], idle = [], leave = ["Disappear"], rest = ["RestPose"] }
[[agents]] [[agents]]
friendlyName = "F1" friendlyName = "F1"
filename = "F1.ACS" filename = "F1.ACS"
animations = { join = ["Show"], chat = ["Explain"], idle = [], leave = ["Hide"], rest = ["RestPose"] }
[[agents]] [[agents]]
friendlyName = "Genie" friendlyName = "Genie"
filename = "Genie.acs" filename = "Genie.acs"
animations = { join = ["Show"], chat = ["Suggest", "RestPose"], idle = [], leave = ["Hide"], rest = ["RestPose"] }
[[agents]] [[agents]]
friendlyName = "James" friendlyName = "James"
filename = "James.acs" filename = "James.acs"
animations = { join = ["Show"], chat = ["Suggest", "RestPose"], idle = [], leave = ["Hide"], rest = ["RestPose"] }
[[agents]] [[agents]]
friendlyName = "Links" friendlyName = "Links"
filename = "Links.ACS" filename = "Links.ACS"
animations = { join = ["Greeting"], chat = ["GetAttention"], idle = [], leave = ["GoodBye"], rest = ["RestPose"] }
[[agents]] [[agents]]
friendlyName = "Merlin" friendlyName = "Merlin"
filename = "merlin.acs" filename = "merlin.acs"
animations = { join = ["Show"], chat = ["Explain", "RestPose"], idle = [], leave = ["Hide"], rest = ["RestPose"] }
[[agents]] [[agents]]
friendlyName = "Mother Nature" friendlyName = "Mother Nature"
filename = "Mother_NATURE.ACS" filename = "Mother_NATURE.ACS"
animations = { join = ["Greeting"], chat = ["Explain"], idle = [], leave = ["Hide"], rest = ["RestPose"] }
[[agents]] [[agents]]
friendlyName = "Office Logo" friendlyName = "Office Logo"
filename = "Office_Logo.ACS" filename = "Office_Logo.ACS"
animations = { join = ["Greeting"], chat = ["Explain"], idle = [], leave = ["Goodbye"], rest = ["RestPose"] }
[[agents]] [[agents]]
friendlyName = "Peedy" friendlyName = "Peedy"
filename = "Peedy.acs" filename = "Peedy.acs"
animations = { join = ["Show", "Greet", "RestPose"], chat = ["Thinking", "RestPose"], idle = [], leave = ["Hide"], rest = ["RestPose"] }
#[[agents]] [[agents]]
#friendlyName = "Question" friendlyName = "Question"
#filename = "question_mark.acs" filename = "question_mark.acs"
animations = { join = ["Welcome"], chat = ["Shimmer"], idle = [], leave = ["Fade"], rest = ["Restpose"] }
[[agents]] [[agents]]
friendlyName = "Robby" friendlyName = "Robby"
filename = "Robby.acs" filename = "Robby.acs"
animations = { join = ["Show", "Greet", "RestPose"], chat = ["Thinking", "RestPose"], idle = [], leave = ["Hide"], rest = ["RestPose"] }
[[agents]] [[agents]]
friendlyName = "Rocky" friendlyName = "Rocky"
filename = "ROCKY.ACS" filename = "ROCKY.ACS"
animations = { join = ["Show"], chat = ["Alert"], idle = [], leave = ["Hide"], rest = ["RestPose"] }
[[agents]] [[agents]]
friendlyName = "Rover" friendlyName = "Rover"
filename = "rover.acs" filename = "rover.acs"
animations = { join = ["Show", "Greet", "RestPose"], chat = ["Thinking", "RestPose"], idle = [], leave = ["Hide"], rest = ["RestPose"] }
[[agents]] [[agents]]
friendlyName = "Victor" friendlyName = "Victor"
filename = "Victor.acs" filename = "Victor.acs"
animations = { join = ["Show", "Greet", "RestPose"], chat = ["Thinking", "RestPose"], idle = [], leave = ["Hide"], rest = ["RestPose"] }
[[agents]] [[agents]]
friendlyName = "Bonzi" friendlyName = "Bonzi"
filename = "Bonzi.acs" filename = "Bonzi.acs"
animations = { join = ["Show", "Wave", "RestPose"], chat = ["Giggle"], idle = [], leave = ["Hide"], rest = ["RestPose"] }

View file

@ -14,7 +14,8 @@ import {
MSAgentJoinMessage, MSAgentJoinMessage,
MSAgentProtocolMessage, MSAgentProtocolMessage,
MSAgentProtocolMessageType, MSAgentProtocolMessageType,
MSAgentTalkMessage MSAgentTalkMessage,
MSAgentPlayAnimationMessage
} from '@msagent-chat/protocol'; } from '@msagent-chat/protocol';
import { MSAgentChatRoom } from './room.js'; import { MSAgentChatRoom } from './room.js';
import * as htmlentities from 'html-entities'; import * as htmlentities from 'html-entities';
@ -27,6 +28,7 @@ export interface Client {
on(event: 'join', listener: () => void): this; on(event: 'join', listener: () => void): this;
on(event: 'close', listener: () => void): this; on(event: 'close', listener: () => void): this;
on(event: 'talk', listener: (msg: string) => void): this; on(event: 'talk', listener: (msg: string) => void): this;
on(event: 'animation', listener: (anim: string) => void): this;
on(event: 'image', listener: (id: string) => void): this; on(event: 'image', listener: (id: string) => void): this;
on(event: string, listener: Function): this; on(event: string, listener: Function): this;
@ -45,6 +47,7 @@ export class Client extends EventEmitter {
nopLevel: number; nopLevel: number;
chatRateLimit: RateLimiter; chatRateLimit: RateLimiter;
animRateLimit: RateLimiter;
constructor(socket: WebSocket, room: MSAgentChatRoom, ip: string) { constructor(socket: WebSocket, room: MSAgentChatRoom, ip: string) {
super(); super();
@ -58,6 +61,7 @@ export class Client extends EventEmitter {
this.nopLevel = 0; this.nopLevel = 0;
this.chatRateLimit = new RateLimiter(this.room.config.ratelimits.chat); this.chatRateLimit = new RateLimiter(this.room.config.ratelimits.chat);
this.animRateLimit = new RateLimiter(this.room.config.ratelimits.anim);
this.socket.on('message', (msg, isBinary) => { this.socket.on('message', (msg, isBinary) => {
if (isBinary) { if (isBinary) {
@ -162,6 +166,12 @@ export class Client extends EventEmitter {
this.emit('talk', talkMsg.data.msg); this.emit('talk', talkMsg.data.msg);
break; break;
} }
case MSAgentProtocolMessageType.PlayAnimation: {
let animMsg = msg as MSAgentPlayAnimationMessage;
if (!animMsg.data || !animMsg.data.anim || !this.animRateLimit.request()) return;
this.emit('animation', animMsg.data.anim);
break;
}
case MSAgentProtocolMessageType.SendImage: { case MSAgentProtocolMessageType.SendImage: {
let imgMsg = msg as MSAgentSendImageMessage; let imgMsg = msg as MSAgentSendImageMessage;
if (!imgMsg.data || !imgMsg.data.id || !this.chatRateLimit.request()) return; if (!imgMsg.data || !imgMsg.data.id || !this.chatRateLimit.request()) return;

View file

@ -1,3 +1,5 @@
import { AgentAnimationConfig } from "@msagent-chat/protocol";
export interface IConfig { export interface IConfig {
http: { http: {
host: string; host: string;
@ -31,6 +33,7 @@ export interface ChatConfig {
bannedWords: string[]; bannedWords: string[];
ratelimits: { ratelimits: {
chat: RateLimitConfig; chat: RateLimitConfig;
anim: RateLimitConfig;
}; };
} }
@ -42,6 +45,7 @@ export interface motdConfig {
export interface AgentConfig { export interface AgentConfig {
friendlyName: string; friendlyName: string;
filename: string; filename: string;
animations: AgentAnimationConfig;
} }
export interface RateLimitConfig { export interface RateLimitConfig {

View file

@ -1,10 +1,10 @@
import { import {
MSAgentAddUserMessage, MSAgentAddUserMessage,
MSAgentAnimationMessage,
MSAgentChatMessage, MSAgentChatMessage,
MSAgentImageMessage, MSAgentImageMessage,
MSAgentInitMessage, MSAgentInitMessage,
MSAgentPromoteMessage, MSAgentPromoteMessage,
MSAgentProtocolMessage,
MSAgentProtocolMessageType, MSAgentProtocolMessageType,
MSAgentRemoveUserMessage MSAgentRemoveUserMessage
} from '@msagent-chat/protocol'; } from '@msagent-chat/protocol';
@ -52,6 +52,7 @@ export class MSAgentChatRoom {
} }
}); });
client.on('join', () => { client.on('join', () => {
let agent = this.agents.find(a => a.filename === client.agent)!;
let initmsg: MSAgentInitMessage = { let initmsg: MSAgentInitMessage = {
op: MSAgentProtocolMessageType.Init, op: MSAgentProtocolMessageType.Init,
data: { data: {
@ -64,7 +65,8 @@ export class MSAgentChatRoom {
return { return {
username: c.username!, username: c.username!,
agent: c.agent!, agent: c.agent!,
admin: c.admin admin: c.admin,
animations: this.agents.find(a => a.filename === c.agent)!.animations
}; };
}) })
} }
@ -74,7 +76,8 @@ export class MSAgentChatRoom {
op: MSAgentProtocolMessageType.AddUser, op: MSAgentProtocolMessageType.AddUser,
data: { data: {
username: client.username!, username: client.username!,
agent: client.agent! agent: client.agent!,
animations: agent.animations
} }
}; };
for (const _client of this.getActiveClients().filter((c) => c !== client)) { for (const _client of this.getActiveClients().filter((c) => c !== client)) {
@ -104,6 +107,18 @@ export class MSAgentChatRoom {
} }
this.discord?.logMsg(client.username!, message); this.discord?.logMsg(client.username!, message);
}); });
client.on('animation', async anim => {
let msg: MSAgentAnimationMessage = {
op: MSAgentProtocolMessageType.PlayAnimation,
data: {
username: client.username!,
anim: anim
}
};
for (const _client of this.getActiveClients()) {
_client.send(msg);
}
});
client.on('image', async (id) => { client.on('image', async (id) => {
if (!this.img.has(id)) return; if (!this.img.has(id)) return;

View file

@ -9,11 +9,13 @@ import {
MSAgentAdminLoginResponse, MSAgentAdminLoginResponse,
MSAgentAdminMessage, MSAgentAdminMessage,
MSAgentAdminOperation, MSAgentAdminOperation,
MSAgentAnimationMessage,
MSAgentChatMessage, MSAgentChatMessage,
MSAgentErrorMessage, MSAgentErrorMessage,
MSAgentImageMessage, MSAgentImageMessage,
MSAgentInitMessage, MSAgentInitMessage,
MSAgentJoinMessage, MSAgentJoinMessage,
MSAgentPlayAnimationMessage,
MSAgentPromoteMessage, MSAgentPromoteMessage,
MSAgentProtocolMessage, MSAgentProtocolMessage,
MSAgentProtocolMessageType, MSAgentProtocolMessageType,
@ -174,6 +176,16 @@ export class MSAgentClient {
this.send(talkMsg); this.send(talkMsg);
} }
animation(anim: string) {
let msg: MSAgentPlayAnimationMessage = {
op: MSAgentProtocolMessageType.PlayAnimation,
data: {
anim
}
};
this.send(msg);
}
async sendImage(img: ArrayBuffer, type: string) { async sendImage(img: ArrayBuffer, type: string) {
// Upload image // Upload image
let res = await fetch(this.url + '/api/upload', { let res = await fetch(this.url + '/api/upload', {
@ -296,8 +308,9 @@ export class MSAgentClient {
let agent = await agentCreateCharacterFromUrl(this.url + '/api/agents/' + _user.agent); let agent = await agentCreateCharacterFromUrl(this.url + '/api/agents/' + _user.agent);
agent.setUsername(_user.username, _user.admin ? '#FF0000' : '#000000'); agent.setUsername(_user.username, _user.admin ? '#FF0000' : '#000000');
agent.addToDom(this.agentContainer); agent.addToDom(this.agentContainer);
let user = new User(_user.username, agent, _user.animations);
agent.show(); agent.show();
let user = new User(_user.username, agent); user.doAnim('join');
this.setContextMenu(user); this.setContextMenu(user);
this.users.push(user); this.users.push(user);
} }
@ -309,8 +322,9 @@ export class MSAgentClient {
let agent = await agentCreateCharacterFromUrl(this.url + '/api/agents/' + addUserMsg.data.agent); let agent = await agentCreateCharacterFromUrl(this.url + '/api/agents/' + addUserMsg.data.agent);
agent.setUsername(addUserMsg.data.username, '#000000'); agent.setUsername(addUserMsg.data.username, '#000000');
agent.addToDom(this.agentContainer); agent.addToDom(this.agentContainer);
let user = new User(addUserMsg.data.username, agent, addUserMsg.data.animations);
agent.show(); agent.show();
let user = new User(addUserMsg.data.username, agent); user.doAnim('join');
this.setContextMenu(user); this.setContextMenu(user);
this.users.push(user); this.users.push(user);
this.events.emit('adduser', user); this.events.emit('adduser', user);
@ -320,7 +334,9 @@ export class MSAgentClient {
let remUserMsg = msg as MSAgentRemoveUserMessage; let remUserMsg = msg as MSAgentRemoveUserMessage;
let user = this.users.find((u) => u.username === remUserMsg.data.username); let user = this.users.find((u) => u.username === remUserMsg.data.username);
if (!user) return; if (!user) return;
user.doAnim('leave').then(() => {
user.agent.hide(true); user.agent.hide(true);
});
if (this.playingAudio.has(user!.username)) { if (this.playingAudio.has(user!.username)) {
this.playingAudio.get(user!.username)?.pause(); this.playingAudio.get(user!.username)?.pause();
this.playingAudio.delete(user!.username); this.playingAudio.delete(user!.username);
@ -337,6 +353,7 @@ export class MSAgentClient {
this.events.emit('chat', user, chatMsg.data.message); this.events.emit('chat', user, chatMsg.data.message);
user?.agent.speak(chatMsg.data.message); user?.agent.speak(chatMsg.data.message);
user?.doAnim('chat');
let msgId = ++user!.msgId; let msgId = ++user!.msgId;
if (chatMsg.data.audio !== undefined) { if (chatMsg.data.audio !== undefined) {
@ -368,6 +385,14 @@ export class MSAgentClient {
} }
break; break;
} }
case MSAgentProtocolMessageType.PlayAnimation: {
let animMsg = msg as MSAgentAnimationMessage;
let user = this.users.find((u) => u.username === animMsg.data.username);
if (!user || user.muted) return;
await user.agent.playAnimationByNamePromise(animMsg.data.anim);
await user.doAnim('rest');
break;
}
case MSAgentProtocolMessageType.SendImage: { case MSAgentProtocolMessageType.SendImage: {
let imgMsg = msg as MSAgentImageMessage; let imgMsg = msg as MSAgentImageMessage;
let user = this.users.find((u) => u.username === imgMsg.data.username); let user = this.users.find((u) => u.username === imgMsg.data.username);

18
webapp/src/ts/commands.ts Normal file
View file

@ -0,0 +1,18 @@
import { MSAgentClient } from "./client.js";
export function InitCommands() {
}
export function RunCommand(command: string, room: MSAgentClient) {
let arr = command.split(' ');
let cmd = arr[0];
let args = arr.slice(1);
switch (cmd) {
case '/anim': {
let anim = args.join(' ');
room.animation(anim);
break;
}
}
}

View file

@ -2,6 +2,7 @@ import { MSWindow, MSWindowStartPosition } from './MSWindow.js';
import { agentInit } from '@msagent-chat/msagent.js'; import { agentInit } from '@msagent-chat/msagent.js';
import { MSAgentClient } from './client.js'; import { MSAgentClient } from './client.js';
import { Config } from '../../config.js'; import { Config } from '../../config.js';
import { RunCommand } from './commands.js';
const elements = { const elements = {
motdWindow: document.getElementById('motdWindow') as HTMLDivElement, motdWindow: document.getElementById('motdWindow') as HTMLDivElement,
@ -120,7 +121,12 @@ document.addEventListener('DOMContentLoaded', async () => {
function talk() { function talk() {
if (Room === null) return; if (Room === null) return;
Room.talk(elements.chatInput.value); let msg = elements.chatInput.value;
if (msg.startsWith('/')) {
RunCommand(msg, Room);
} else {
Room.talk(msg);
}
elements.chatInput.value = ''; elements.chatInput.value = '';
} }

View file

@ -1,4 +1,5 @@
import { Agent } from '@msagent-chat/msagent.js'; import { Agent } from '@msagent-chat/msagent.js';
import { AgentAnimationConfig } from '@msagent-chat/protocol';
export class User { export class User {
username: string; username: string;
@ -6,11 +7,20 @@ export class User {
muted: boolean; muted: boolean;
admin: boolean; admin: boolean;
msgId: number = 0; msgId: number = 0;
animations: AgentAnimationConfig;
constructor(username: string, agent: Agent) { constructor(username: string, agent: Agent, animations: AgentAnimationConfig) {
this.username = username; this.username = username;
this.agent = agent; this.agent = agent;
this.muted = false; this.muted = false;
this.admin = false; this.admin = false;
this.animations = animations;
}
async doAnim(action: string) {
// @ts-ignore
for (let anim of this.animations[action]) {
await this.agent.playAnimationByNamePromise(anim);
}
} }
} }