msagent.js/web: Implement graceful exit
This implementation isn't perfect (sometimes an animation can get stuck playing), but it does work for the most part. (All animation playing logic is async/promise now, which is fine because we only ever used the promise versions)
This commit is contained in:
parent
74743a1824
commit
95708da8cc
4 changed files with 92 additions and 33 deletions
|
@ -10,44 +10,85 @@ function randint(min: number, max: number) {
|
|||
return Math.floor(Math.random() * (max - min) + min);
|
||||
}
|
||||
|
||||
enum AnimState {
|
||||
Idle = 0,
|
||||
Cancel = 1,
|
||||
Playing = 2
|
||||
}
|
||||
|
||||
// an externally resolveable promise
|
||||
class ExternallyResolveablePromise<T> {
|
||||
private rescb: (t: T) => void;
|
||||
private rejectcb: (e: Error) => void;
|
||||
public promise: Promise<T>;
|
||||
|
||||
constructor() {
|
||||
let rescb;
|
||||
let rejectcb;
|
||||
|
||||
this.promise = new Promise<T>((res, rej) => {
|
||||
rescb = res;
|
||||
rejectcb = rej;
|
||||
});
|
||||
|
||||
this.rescb = rescb!;
|
||||
this.rejectcb = rejectcb!;
|
||||
}
|
||||
|
||||
resolve(t: T) {
|
||||
this.rescb(t);
|
||||
}
|
||||
|
||||
reject(e: Error) {
|
||||
this.rejectcb(e);
|
||||
}
|
||||
|
||||
raw() {
|
||||
return this.promise;
|
||||
}
|
||||
}
|
||||
|
||||
// animation state (used during animation playback)
|
||||
class AgentAnimationState {
|
||||
char: Agent;
|
||||
anim: AcsAnimation;
|
||||
private cancelled: boolean = false;
|
||||
|
||||
finishCallback: () => void;
|
||||
private animState: AnimState;
|
||||
private finishPromise = new ExternallyResolveablePromise<void>();
|
||||
|
||||
frameIndex = 0;
|
||||
|
||||
interval = 0;
|
||||
|
||||
constructor(char: Agent, anim: AcsAnimation, finishCallback: () => void) {
|
||||
constructor(char: Agent, anim: AcsAnimation) {
|
||||
this.char = char;
|
||||
this.anim = anim;
|
||||
this.finishCallback = finishCallback;
|
||||
this.animState = AnimState.Idle;
|
||||
}
|
||||
|
||||
// start playing the animation
|
||||
play() {
|
||||
async play() {
|
||||
this.animState = AnimState.Playing;
|
||||
this.nextFrame();
|
||||
await this.finishPromise.raw();
|
||||
|
||||
this.animState = AnimState.Idle;
|
||||
clearInterval(this.interval);
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.cancelled = true;
|
||||
clearTimeout(this.interval);
|
||||
async cancel() {
|
||||
this.animState = AnimState.Cancel;
|
||||
await this.finishPromise.raw();
|
||||
}
|
||||
|
||||
nextFrame() {
|
||||
requestAnimationFrame(() => {
|
||||
if (this.cancelled) return;
|
||||
|
||||
// Handle animation branching, if it is required
|
||||
let bi = this.anim.frameInfo[this.frameIndex].branchInfo;
|
||||
if (bi.length != 0) {
|
||||
let biCopy = [...bi];
|
||||
|
||||
// This happens more often then you'd think, but this basically handles
|
||||
// a branch that will always be taken.
|
||||
// a branch that will always be taken.
|
||||
//
|
||||
// This is often used for looping from my understanding?
|
||||
if (bi.length == 1 && bi[0].branchFrameProbability == 100) {
|
||||
|
@ -61,7 +102,7 @@ class AgentAnimationState {
|
|||
// Handles the off chance that there is a branch info list that sums less than 100%.
|
||||
// (Office Logo 'Idle3', Victor has a couple, ...)
|
||||
//
|
||||
// I'm not entirely sure the correct action in this case but I just
|
||||
// I'm not entirely sure the correct action in this case but I just
|
||||
// have this do nothing.
|
||||
if (totalProbability != 100) {
|
||||
let nothingBranchItem = new AcsBranchInfo();
|
||||
|
@ -85,10 +126,26 @@ class AgentAnimationState {
|
|||
}
|
||||
}
|
||||
|
||||
this.char.drawAnimationFrame(this.anim.frameInfo[this.frameIndex++]);
|
||||
// Actually draw the requested frame
|
||||
let fi = this.anim.frameInfo[this.frameIndex];
|
||||
|
||||
this.char.drawAnimationFrame(fi);
|
||||
|
||||
// If we've requested to cancel the animation and we're in a branch loop,
|
||||
// then this is how we get out of it.
|
||||
if (this.animState == AnimState.Cancel) {
|
||||
if (fi.branchExitFrameIndex != -2) {
|
||||
this.frameIndex = fi.branchExitFrameIndex;
|
||||
} else {
|
||||
this.finishPromise.resolve();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
this.frameIndex += 1;
|
||||
}
|
||||
|
||||
if (this.frameIndex >= this.anim.frameInfo.length) {
|
||||
this.finishCallback();
|
||||
this.finishPromise.resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -366,30 +423,32 @@ export class Agent {
|
|||
}
|
||||
|
||||
// add promise versions later.
|
||||
playAnimation(index: number, finishCallback: () => void) {
|
||||
async playAnimation(index: number): Promise<void> {
|
||||
if (this.animState != null) {
|
||||
this.animState.cancel();
|
||||
this.animState = null;
|
||||
await this.animState.cancel();
|
||||
}
|
||||
|
||||
let animInfo = this.data.animInfo[index];
|
||||
|
||||
// Create and start the animation state
|
||||
this.animState = new AgentAnimationState(this, animInfo.animationData, () => {
|
||||
this.animState = null;
|
||||
finishCallback();
|
||||
});
|
||||
this.animState.play();
|
||||
this.animState = new AgentAnimationState(this, animInfo.animationData);
|
||||
await this.animState.play();
|
||||
this.animState = null;
|
||||
}
|
||||
|
||||
playAnimationByName(name: string, finishCallback: () => void) {
|
||||
async playAnimationByName(name: string): Promise<void> {
|
||||
let index = this.data.animInfo.findIndex((n) => n.name == name);
|
||||
if (index !== -1) this.playAnimation(index, finishCallback);
|
||||
if (index !== -1) return this.playAnimation(index);
|
||||
//throw new Error(`Unknown animation \"${name}\"`);
|
||||
return;
|
||||
}
|
||||
|
||||
playAnimationByNamePromise(name: string): Promise<void> {
|
||||
return new Promise((res, rej) => {
|
||||
this.playAnimationByName(name, () => res());
|
||||
});
|
||||
async exitAnimation(): Promise<void> {
|
||||
if (this.animState != null) {
|
||||
await this.animState.cancel();
|
||||
this.animState = null;
|
||||
}
|
||||
// (After this we need to play the restpose animation or whatever exit is applicable)
|
||||
}
|
||||
|
||||
setUsername(username: string, color: string) {
|
||||
|
|
|
@ -397,7 +397,7 @@ export class MSAgentClient {
|
|||
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.agent.playAnimationByName(animMsg.data.anim);
|
||||
await user.doAnim('rest');
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ input.addEventListener('change', async () => {
|
|||
agent.addToDom(mount);
|
||||
|
||||
agent.show();
|
||||
await agent.playAnimationByNamePromise("Show");
|
||||
await agent.playAnimationByName("Show");
|
||||
console.log('Agent created');
|
||||
});
|
||||
|
||||
|
@ -40,7 +40,7 @@ form.addEventListener('submit', async (e) => {
|
|||
agent.addToDom(document.body);
|
||||
|
||||
agent.show();
|
||||
await agent.playAnimationByNamePromise("Show");
|
||||
await agent.playAnimationByName("Show");
|
||||
|
||||
console.log(`Loaded agent from ${url}`);
|
||||
});
|
||||
|
|
|
@ -20,7 +20,7 @@ export class User {
|
|||
async doAnim(action: string) {
|
||||
// @ts-ignore
|
||||
for (let anim of this.animations[action]) {
|
||||
await this.agent.playAnimationByNamePromise(anim);
|
||||
await this.agent.playAnimationByName(anim);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue