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

View file

@ -5,6 +5,7 @@ export enum MSAgentProtocolMessageType {
KeepAlive = 'nop',
Join = 'join',
Talk = 'talk',
PlayAnimation = 'anim',
SendImage = 'img',
Admin = 'admin',
// Server-to-client
@ -20,6 +21,14 @@ export interface MSAgentProtocolMessage {
op: MSAgentProtocolMessageType;
}
export interface AgentAnimationConfig {
join: string[];
chat: string[];
idle: string[];
rest: string[];
leave: string[];
}
// Client-to-server
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 {
op: MSAgentProtocolMessageType.SendImage;
data: {
@ -56,6 +73,7 @@ export interface MSAgentInitMessage extends MSAgentProtocolMessage {
username: string;
agent: string;
admin: boolean;
animations: AgentAnimationConfig;
}[];
};
}
@ -65,6 +83,7 @@ export interface MSAgentAddUserMessage extends MSAgentProtocolMessage {
data: {
username: 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 {
op: MSAgentProtocolMessageType.SendImage;
data: {

View file

@ -23,6 +23,7 @@ bannedWords = []
[chat.ratelimits]
chat = {seconds = 10, limit = 8}
anim = {seconds = 10, limit = 8}
[motd]
version = 1
@ -58,71 +59,89 @@ expirySeconds = 60
[[agents]]
friendlyName = "Clippy"
filename = "CLIPPIT.ACS"
animations = { join = ["Greeting"], chat = ["Explain"], idle = [], leave = ["GoodBye"], rest = ["RestPose"] }
[[agents]]
friendlyName = "Courtney"
filename = "courtney.acs"
animations = { join = ["Show", "Greet"], chat = ["GetAttentionMinor"], idle = [], leave = ["Disappear"], rest = ["RestPose"] }
[[agents]]
friendlyName = "Dot"
filename = "DOT.ACS"
animations = { join = ["Greeting", "Show"], chat = ["Alert", "Show"], idle = [], leave = ["Goodbye"], rest = ["RestPose"] }
[[agents]]
friendlyName = "Earl"
filename = "earl.acs"
animations = { join = ["Show", "Greet"], chat = ["LookUp"], idle = [], leave = ["Disappear"], rest = ["RestPose"] }
[[agents]]
friendlyName = "F1"
filename = "F1.ACS"
animations = { join = ["Show"], chat = ["Explain"], idle = [], leave = ["Hide"], rest = ["RestPose"] }
[[agents]]
friendlyName = "Genie"
filename = "Genie.acs"
animations = { join = ["Show"], chat = ["Suggest", "RestPose"], idle = [], leave = ["Hide"], rest = ["RestPose"] }
[[agents]]
friendlyName = "James"
filename = "James.acs"
animations = { join = ["Show"], chat = ["Suggest", "RestPose"], idle = [], leave = ["Hide"], rest = ["RestPose"] }
[[agents]]
friendlyName = "Links"
filename = "Links.ACS"
animations = { join = ["Greeting"], chat = ["GetAttention"], idle = [], leave = ["GoodBye"], rest = ["RestPose"] }
[[agents]]
friendlyName = "Merlin"
filename = "merlin.acs"
animations = { join = ["Show"], chat = ["Explain", "RestPose"], idle = [], leave = ["Hide"], rest = ["RestPose"] }
[[agents]]
friendlyName = "Mother Nature"
filename = "Mother_NATURE.ACS"
animations = { join = ["Greeting"], chat = ["Explain"], idle = [], leave = ["Hide"], rest = ["RestPose"] }
[[agents]]
friendlyName = "Office Logo"
filename = "Office_Logo.ACS"
animations = { join = ["Greeting"], chat = ["Explain"], idle = [], leave = ["Goodbye"], rest = ["RestPose"] }
[[agents]]
friendlyName = "Peedy"
filename = "Peedy.acs"
animations = { join = ["Show", "Greet", "RestPose"], chat = ["Thinking", "RestPose"], idle = [], leave = ["Hide"], rest = ["RestPose"] }
#[[agents]]
#friendlyName = "Question"
#filename = "question_mark.acs"
[[agents]]
friendlyName = "Question"
filename = "question_mark.acs"
animations = { join = ["Welcome"], chat = ["Shimmer"], idle = [], leave = ["Fade"], rest = ["Restpose"] }
[[agents]]
friendlyName = "Robby"
filename = "Robby.acs"
animations = { join = ["Show", "Greet", "RestPose"], chat = ["Thinking", "RestPose"], idle = [], leave = ["Hide"], rest = ["RestPose"] }
[[agents]]
friendlyName = "Rocky"
filename = "ROCKY.ACS"
animations = { join = ["Show"], chat = ["Alert"], idle = [], leave = ["Hide"], rest = ["RestPose"] }
[[agents]]
friendlyName = "Rover"
filename = "rover.acs"
animations = { join = ["Show", "Greet", "RestPose"], chat = ["Thinking", "RestPose"], idle = [], leave = ["Hide"], rest = ["RestPose"] }
[[agents]]
friendlyName = "Victor"
filename = "Victor.acs"
animations = { join = ["Show", "Greet", "RestPose"], chat = ["Thinking", "RestPose"], idle = [], leave = ["Hide"], rest = ["RestPose"] }
[[agents]]
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,
MSAgentProtocolMessage,
MSAgentProtocolMessageType,
MSAgentTalkMessage
MSAgentTalkMessage,
MSAgentPlayAnimationMessage
} from '@msagent-chat/protocol';
import { MSAgentChatRoom } from './room.js';
import * as htmlentities from 'html-entities';
@ -27,6 +28,7 @@ export interface Client {
on(event: 'join', listener: () => void): this;
on(event: 'close', listener: () => 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: string, listener: Function): this;
@ -45,6 +47,7 @@ export class Client extends EventEmitter {
nopLevel: number;
chatRateLimit: RateLimiter;
animRateLimit: RateLimiter;
constructor(socket: WebSocket, room: MSAgentChatRoom, ip: string) {
super();
@ -58,6 +61,7 @@ export class Client extends EventEmitter {
this.nopLevel = 0;
this.chatRateLimit = new RateLimiter(this.room.config.ratelimits.chat);
this.animRateLimit = new RateLimiter(this.room.config.ratelimits.anim);
this.socket.on('message', (msg, isBinary) => {
if (isBinary) {
@ -162,6 +166,12 @@ export class Client extends EventEmitter {
this.emit('talk', talkMsg.data.msg);
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: {
let imgMsg = msg as MSAgentSendImageMessage;
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 {
http: {
host: string;
@ -31,6 +33,7 @@ export interface ChatConfig {
bannedWords: string[];
ratelimits: {
chat: RateLimitConfig;
anim: RateLimitConfig;
};
}
@ -42,6 +45,7 @@ export interface motdConfig {
export interface AgentConfig {
friendlyName: string;
filename: string;
animations: AgentAnimationConfig;
}
export interface RateLimitConfig {

View file

@ -1,10 +1,10 @@
import {
MSAgentAddUserMessage,
MSAgentAnimationMessage,
MSAgentChatMessage,
MSAgentImageMessage,
MSAgentInitMessage,
MSAgentPromoteMessage,
MSAgentProtocolMessage,
MSAgentProtocolMessageType,
MSAgentRemoveUserMessage
} from '@msagent-chat/protocol';
@ -52,6 +52,7 @@ export class MSAgentChatRoom {
}
});
client.on('join', () => {
let agent = this.agents.find(a => a.filename === client.agent)!;
let initmsg: MSAgentInitMessage = {
op: MSAgentProtocolMessageType.Init,
data: {
@ -64,7 +65,8 @@ export class MSAgentChatRoom {
return {
username: c.username!,
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,
data: {
username: client.username!,
agent: client.agent!
agent: client.agent!,
animations: agent.animations
}
};
for (const _client of this.getActiveClients().filter((c) => c !== client)) {
@ -104,6 +107,18 @@ export class MSAgentChatRoom {
}
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) => {
if (!this.img.has(id)) return;

View file

@ -9,11 +9,13 @@ import {
MSAgentAdminLoginResponse,
MSAgentAdminMessage,
MSAgentAdminOperation,
MSAgentAnimationMessage,
MSAgentChatMessage,
MSAgentErrorMessage,
MSAgentImageMessage,
MSAgentInitMessage,
MSAgentJoinMessage,
MSAgentPlayAnimationMessage,
MSAgentPromoteMessage,
MSAgentProtocolMessage,
MSAgentProtocolMessageType,
@ -174,6 +176,16 @@ export class MSAgentClient {
this.send(talkMsg);
}
animation(anim: string) {
let msg: MSAgentPlayAnimationMessage = {
op: MSAgentProtocolMessageType.PlayAnimation,
data: {
anim
}
};
this.send(msg);
}
async sendImage(img: ArrayBuffer, type: string) {
// Upload image
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);
agent.setUsername(_user.username, _user.admin ? '#FF0000' : '#000000');
agent.addToDom(this.agentContainer);
let user = new User(_user.username, agent, _user.animations);
agent.show();
let user = new User(_user.username, agent);
user.doAnim('join');
this.setContextMenu(user);
this.users.push(user);
}
@ -309,8 +322,9 @@ export class MSAgentClient {
let agent = await agentCreateCharacterFromUrl(this.url + '/api/agents/' + addUserMsg.data.agent);
agent.setUsername(addUserMsg.data.username, '#000000');
agent.addToDom(this.agentContainer);
let user = new User(addUserMsg.data.username, agent, addUserMsg.data.animations);
agent.show();
let user = new User(addUserMsg.data.username, agent);
user.doAnim('join');
this.setContextMenu(user);
this.users.push(user);
this.events.emit('adduser', user);
@ -320,7 +334,9 @@ export class MSAgentClient {
let remUserMsg = msg as MSAgentRemoveUserMessage;
let user = this.users.find((u) => u.username === remUserMsg.data.username);
if (!user) return;
user.agent.hide(true);
user.doAnim('leave').then(() => {
user.agent.hide(true);
});
if (this.playingAudio.has(user!.username)) {
this.playingAudio.get(user!.username)?.pause();
this.playingAudio.delete(user!.username);
@ -337,6 +353,7 @@ export class MSAgentClient {
this.events.emit('chat', user, chatMsg.data.message);
user?.agent.speak(chatMsg.data.message);
user?.doAnim('chat');
let msgId = ++user!.msgId;
if (chatMsg.data.audio !== undefined) {
@ -368,6 +385,14 @@ export class MSAgentClient {
}
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: {
let imgMsg = msg as MSAgentImageMessage;
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 { MSAgentClient } from './client.js';
import { Config } from '../../config.js';
import { RunCommand } from './commands.js';
const elements = {
motdWindow: document.getElementById('motdWindow') as HTMLDivElement,
@ -120,7 +121,12 @@ document.addEventListener('DOMContentLoaded', async () => {
function talk() {
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 = '';
}

View file

@ -1,4 +1,5 @@
import { Agent } from '@msagent-chat/msagent.js';
import { AgentAnimationConfig } from '@msagent-chat/protocol';
export class User {
username: string;
@ -6,11 +7,20 @@ export class User {
muted: boolean;
admin: boolean;
msgId: number = 0;
animations: AgentAnimationConfig;
constructor(username: string, agent: Agent) {
constructor(username: string, agent: Agent, animations: AgentAnimationConfig) {
this.username = username;
this.agent = agent;
this.muted = 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);
}
}
}