ActionManager.ts 9.13 KB
import { DeepReadonly } from "simba-utils";
import { Disposable } from "simba-eventkit";
import { Action, TriggerType, ModifyVariableAction, OperandType, VariableOperator, EmitEventAction, ActionType, PlayAudioAction, Actions } from "./model/ActionModel";
import { EditorEvents } from "./EditorEvents";
import { GameRecord } from "./game-data/GameRecord";
import { Random } from "simba-random";
import { AudioManager } from "../common/gameplay/managers/AudioManager";

const runningExecutors = new Set<Executor>();

let _affectRecord = true;

interface Executor {
    execute(): Promise<void>;
    stop(): void;
}

abstract class ActionExecutor<T extends Action> implements Executor {
    private _timerHandle: any;
    protected _action: DeepReadonly<T>;
    nextExecutor?: ActionExecutor<any>;
    private _disposable?: Disposable;

    constructor(action: DeepReadonly<T>) {
        this._action = action;
    }

    protected sleep = (t: number) => {
        return new Promise<void>((resolve) => {
            this._timerHandle = setTimeout(() => {
                this._timerHandle = undefined;
                resolve();
            }, t * 1000);
        });
    }

    abstract onExec(): Promise<boolean>;

    execute = async (): Promise<void> => {
        runningExecutors.add(this);
        if (this._action.trigger === TriggerType.Click) { // wait for click events
            // await new Promise((resolve) => {
            //     this._disposable = ClickEvent.once(() => {
            //         resolve();
            //     })
            // });
        }
        if (this._action.trigger === TriggerType.Event) {
            if (!this._action.triggerEvent) return;
            return new Promise((resolve) => {
                this._disposable = EditorEvents.emitter.once(this._action.triggerEvent!, async () => {
                    let result = await this.executeInternal();
                    resolve();
                    return result;
                });
            });
        } else {
            return this.executeInternal();
        }
    }

    private executeInternal = async () => {
        if (this._action.delay) {
            await this.sleep(this._action.delay);
        }
        let finished = true;
        if (this.nextExecutor && this.nextExecutor._action.trigger === TriggerType.PreviousStart) {
            [finished] = await Promise.all([this.onExec(), this.nextExecutor.execute()]);
        } else {
            finished = await this.onExec();
            if (this.nextExecutor) {
                await this.nextExecutor.execute();
            }
        }
        if (finished) {
            runningExecutors.delete(this);
        }
    }

    abstract onStop(): void;

    stop = () => {
        this.onStop();
        if (this._disposable) this._disposable.dispose();
        if (this._timerHandle) clearTimeout(this._timerHandle);
    }
}

class ModifyVariableExecutor extends ActionExecutor<ModifyVariableAction> {
    onExec(): Promise<boolean> {
        if (!_affectRecord) return Promise.resolve(true);
        try {
            let action = this._action;
            let varValue = GameRecord.getVariableValue(action.target);
            let value: string | number = action.oprand.value;
            if (action.oprand.type === OperandType.Const) {
                if (typeof varValue === "number") {
                    value = Number.parseFloat(value);
                }
            } else if (action.oprand.type === OperandType.Random) {
                let arr = value.split("-");
                let min = Number.parseFloat(arr[0]);
                let max = Number.parseFloat(arr[1]);
                value = Random.range(min, max);
            } else if (action.oprand.type === OperandType.Variable) {
                value = GameRecord.getVariableValue(value);
            }
            switch (action.operator) {
                case VariableOperator.Assign:

                    break;
                case VariableOperator.Plus:
                    varValue += (value as any);
                    break;
                case VariableOperator.Minus:
                    (varValue as number) -= (value as number);
                    break;
                case VariableOperator.Multiply:
                    (varValue as number) *= (value as number);
                    break;
                case VariableOperator.Divide:
                    (varValue as number) /= (value as number);
                    break;
                case VariableOperator.Modulo:
                    (varValue as number) %= (value as number);
                    break;
            }
            GameRecord.setVariableValue(action.target, varValue);
        } catch (e) {
            console.error(e);
        }
        return Promise.resolve(true);
    }

    onStop(): void {
    }
}

class PlayAudioExecutor extends ActionExecutor<PlayAudioAction> {
    private audioId?: number;

    async onExec(): Promise<boolean> {
        let action = this._action;
        if (action.audioType === "effect") {
            if (action.stopPreviousSound) {
                AudioManager.stopAllEffect();
            }
            if (action.loopCount <= 0) {
                this.audioId = await AudioManager.playEffect(action.filePath, 0);
                return false;
            } else {
                AudioManager.playEffect(action.filePath, action.loopCount);
            }
        } else {
            if (!action.filePath) {
                AudioManager.stopMusic();
            } else {
                AudioManager.playMusic(action.filePath);
            }
            if (_affectRecord) {
                GameRecord.recordVariables.bgm = action.filePath;
            }
        }

        return true;
    }

    onStop(): void {
        if (this.audioId !== undefined) {
            AudioManager.stopEffect(this.audioId);
        }
    }
}

class EmitEventExecutor extends ActionExecutor<EmitEventAction> {
    async onExec(): Promise<boolean> {
        if (this._action.emitEvent) {
            await Promise.all(EditorEvents.emitter.emit(this._action.emitEvent, this._action.param));
        }
        return Promise.resolve(true);
    }

    onStop(): void {
    }

}

export namespace ActionManager {
    function getExecutor(action: Action) {
        if (action.type === ActionType.ModifyVariable) {
            return new ModifyVariableExecutor(action);
        } else if (action.type === ActionType.PlayAudio) {
            return new PlayAudioExecutor(action);
        } else if (action.type === ActionType.EmitEvent) {
            return new EmitEventExecutor(action);
        }
        return undefined;
    }

    let stopped = false;
    let prevActions: DeepReadonly<Actions>;
    export async function executeActions(actionData: DeepReadonly<Actions>, filter?: (action: DeepReadonly<Action>) => boolean, affectRecord = true) {
        // if (prevActions === actionData) {
        //     throw new Error("Action already executed.");
        // }
        // prevActions = actionData;
        _affectRecord = affectRecord;
        if (!stopped) {
            stop();
        }
        stopped = false;

        let firstActionExecutor: Executor | undefined;
        let triggerType: TriggerType = TriggerType.PreviousStart;
        let executors: Executor[] = [];
        let noWaitExecutors: Executor[] = [];
        if (actionData.actions) {
            let currExecutor: ActionExecutor<any> | undefined;
            for (let i = 0; i < actionData.actions.length; i++) {
                if (i > 0 && actionData.actions[i].trigger === TriggerType.Event) {
                    if (firstActionExecutor) {
                        executors.push(firstActionExecutor);
                        firstActionExecutor = undefined;
                    }
                }
                let action = actionData.actions[i];
                if (filter && !filter(action)) {
                    continue;
                }
                let executor = getExecutor(action);
                if (executor) {
                    if (!firstActionExecutor) {
                        firstActionExecutor = executor;
                        triggerType = action.trigger;
                        currExecutor = executor;
                    } else if (currExecutor) {
                        currExecutor.nextExecutor = executor;
                        currExecutor = executor;
                    }
                }
            }

            if (firstActionExecutor) {
                if (triggerType === TriggerType.PreviousStart || triggerType === TriggerType.PreviousEnd) {
                    executors.push(firstActionExecutor);
                } else {
                    noWaitExecutors.push(firstActionExecutor);
                }
            }
        }

        if (noWaitExecutors.length) {
            noWaitExecutors.forEach((v) => v.execute());
        }
        let promises = executors.map(v => v.execute());
        if (executors.length) {
            await Promise.all(promises);
        }

        if (stopped) return;
    }

    export function stop() {
        if (stopped) return;
        stopped = true;
        for (let executor of runningExecutors) {
            executor.stop();
        }
        runningExecutors.clear();
        //add line
        // prevActions = undefined;
    }
}