mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-07-25 12:05:19 +02:00
238 lines
7.7 KiB
TypeScript
238 lines
7.7 KiB
TypeScript
/*
|
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
|
* SPDX-License-Identifier: AGPL-3.0-only
|
|
*/
|
|
|
|
import { ref, shallowRef } from 'vue';
|
|
import { cm } from 'misskey-world/src/utility.js';
|
|
import { EngineControllerBase } from '../EngineControllerBase.js';
|
|
import type { ShallowRef } from 'vue';
|
|
import type { RoomState_InstalledFurniture } from 'misskey-world/src/room/furniture.js';
|
|
import type { RoomEngine } from 'misskey-world-engine/src/room/engine.js';
|
|
import type { RoomAttachments, RoomState } from 'misskey-world/src/room/type.js';
|
|
import type { PlayerProfile, PlayerState } from 'misskey-world-engine/src/PlayerContainer.js';
|
|
import * as sound from '@/utility/sound.js';
|
|
import { deepEqual } from '@/utility/deep-equal.js';
|
|
import { deepClone } from '@/utility/clone.js';
|
|
|
|
export type RoomControllerOptions = {
|
|
workerMode?: boolean;
|
|
graphicsQuality: number;
|
|
fps: number | null;
|
|
resolution: number;
|
|
antialias: boolean;
|
|
useVirtualJoystick?: boolean;
|
|
};
|
|
|
|
// 抽象化レイヤー
|
|
export class RoomController extends EngineControllerBase<RoomEngine> {
|
|
public isSitting = ref(false);
|
|
public isEditMode = ref(false);
|
|
public isRoomLightOn = ref(true);
|
|
public grabbing = ref<{ forInstall: boolean } | null>(null);
|
|
public gridSnapping = ref({ enabled: true, scale: cm(4) });
|
|
public selected = shallowRef<{
|
|
furnitureId: string;
|
|
funitureState: RoomState_InstalledFurniture;
|
|
interacions: {
|
|
id: string;
|
|
label: string;
|
|
isPrimary: boolean;
|
|
}[];
|
|
} | null>(null);
|
|
public roomState: ShallowRef<RoomState>;
|
|
public myPlayerState = shallowRef<PlayerState>({
|
|
position: [0, 0, 0],
|
|
rotation: [0, 0, 0],
|
|
});
|
|
|
|
constructor(roomState: RoomState, options: RoomControllerOptions) {
|
|
super(options);
|
|
this.roomState = shallowRef(deepClone(roomState));
|
|
}
|
|
|
|
public async init(canvas: HTMLCanvasElement, attachments: RoomAttachments) {
|
|
const { engineEvents } = await this._init_(canvas, {
|
|
createWorker: (offscreen) => new Promise((resolve) => {
|
|
import('frontend-misskey-world-engine/src/room/worker?worker').then(({ default: RoomWorker }) => {
|
|
const worker = new RoomWorker();
|
|
worker.postMessage({ type: 'init', canvas: offscreen, options: this.options, roomState: this.roomState.value, roomAttachments: attachments }, [offscreen]);
|
|
resolve(worker);
|
|
});
|
|
}),
|
|
createEngine: () => new Promise((resolve) => {
|
|
import('frontend-misskey-world-engine/src/room/nonWorker.js').then(({ createRoomEngine }) => {
|
|
const engine = createRoomEngine({ canvas, options: this.options, roomState: this.roomState.value, roomAttachments: attachments });
|
|
resolve(engine);
|
|
});
|
|
}),
|
|
});
|
|
|
|
engineEvents.on('changeGrabbingState', ({ grabbing }) => {
|
|
this.grabbing.value = grabbing;
|
|
});
|
|
|
|
engineEvents.on('changeEditMode', ({ isEditMode }) => {
|
|
this.isEditMode.value = isEditMode;
|
|
});
|
|
|
|
engineEvents.on('changeSittingState', ({ isSitting }) => {
|
|
this.isSitting.value = isSitting;
|
|
});
|
|
|
|
engineEvents.on('changeGridSnapping', ({ gridSnapping }) => {
|
|
this.gridSnapping.value = gridSnapping;
|
|
});
|
|
|
|
engineEvents.on('changeSelectedState', ({ selected }) => {
|
|
this.selected.value = deepClone(selected);
|
|
});
|
|
|
|
engineEvents.on('changeRoomState', ({ roomState }) => {
|
|
if (deepEqual(this.roomState.value, roomState)) return; // vueのリアクティビティが反応して無限ループになることがあるため
|
|
this.roomState.value = deepClone(roomState);
|
|
if (this.selected.value != null) {
|
|
const newSelected = roomState.installedFurnitures.find(o => o.id === this.selected.value.furnitureId);
|
|
if (newSelected) {
|
|
this.selected.value = {
|
|
furnitureId: newSelected.id,
|
|
|
|
// そのまま入れると「オブジェクト(newSelected)の内容」は変わってるけど「オブジェクトの参照」そのものは変化していないから、
|
|
// その状態で代入しようがtriggerRef呼ぼうがVueは「子に対しては」更新があったと見做してくれない(親から当該refをwatchする場合は発火する)っぽい(バグか仕様かは不明)
|
|
// そのため新しい参照にするためにdeepClone
|
|
funitureState: deepClone(newSelected),
|
|
};
|
|
} else {
|
|
this.selected.value = null;
|
|
}
|
|
}
|
|
});
|
|
|
|
engineEvents.on('changeMyPlayerState', (playerState) => {
|
|
this.myPlayerState.value = playerState;
|
|
});
|
|
|
|
engineEvents.on('playSfxUrl', ({ url, options }) => {
|
|
sound.playUrl(url, options);
|
|
});
|
|
}
|
|
|
|
public async reset(canvas: HTMLCanvasElement, attachments: RoomAttachments, roomState?: RoomState | null, options?: RoomControllerOptions | null) {
|
|
this._reset_();
|
|
if (roomState != null) this.roomState.value = deepClone(roomState);
|
|
if (options != null) this.options = options;
|
|
this.isSitting.value = false;
|
|
this.isEditMode.value = false;
|
|
this.isRoomLightOn.value = true;
|
|
this.grabbing.value = null;
|
|
this.selected.value = null;
|
|
this.myPlayerState.value = {
|
|
position: [0, 0, 0],
|
|
rotation: [0, 0, 0],
|
|
};
|
|
await this.init(canvas, attachments);
|
|
}
|
|
|
|
public setCameraMoveVector(vec: { x: number; y: number }, dash: boolean) {
|
|
this.call('cameraMove', [vec, dash]);
|
|
}
|
|
|
|
public setCameraRotateVector(vec: { x: number; y: number }) {
|
|
this.call('cameraRotate', [vec]);
|
|
}
|
|
|
|
public setCameraJoystickMoveVector(vec: { x: number; y: number }) {
|
|
this.call('cameraJoystickMove', [vec]);
|
|
}
|
|
|
|
public enterEditMode() {
|
|
this.call('enterEditMode');
|
|
}
|
|
|
|
public exitEditMode() {
|
|
this.call('exitEditMode');
|
|
}
|
|
|
|
public setGridSnapping(gridSnapping: { enabled: boolean; scale: number }) {
|
|
this.set('gridSnapping', gridSnapping);
|
|
}
|
|
|
|
public updateFurnitureOption(furnitureId: string, key: string, value: any, attachments?: RoomAttachments) {
|
|
this.call('updateFurnitureOption', deepClone([furnitureId, key, value, attachments])); // 場合によってはvueによって(postMessage不能な)proxy化された値が来ることも考えられるのでdeepClone
|
|
}
|
|
|
|
// エラーになる
|
|
//public changeEnvType(type: RoomState['env']['type']) {
|
|
// this.call('changeEnvType', [type]);
|
|
//}
|
|
|
|
public updateEnvOptions(options: RoomState['env']['options']) {
|
|
this.call('updateEnvOptions', [options]);
|
|
}
|
|
|
|
public updateRoomLightColor(color: [number, number, number]) {
|
|
this.call('updateRoomLightColor', [color]);
|
|
}
|
|
|
|
public beginSelectedInstalledFunitureGrabbing() {
|
|
this.call('beginSelectedInstalledFunitureGrabbing');
|
|
}
|
|
|
|
public duplicateSelectedFuniture() {
|
|
this.call('duplicateSelectedFuniture');
|
|
}
|
|
|
|
public removeSelectedFuniture() {
|
|
this.call('removeSelectedFuniture');
|
|
}
|
|
|
|
public addFuniture(type: string, options: any, attachments?: RoomAttachments) {
|
|
this.call('addFuniture', deepClone([type, options, attachments])); // 場合によってはvueによって(postMessage不能な)proxy化された値が来ることも考えられるのでdeepClone
|
|
}
|
|
|
|
public endGrabbing() {
|
|
this.call('endGrabbing');
|
|
}
|
|
|
|
public cancelGrabbing() {
|
|
this.call('endGrabbing', [true]);
|
|
}
|
|
|
|
public changeGrabbingDistance(delta: number) {
|
|
this.call('changeGrabbingDistance', [delta]);
|
|
}
|
|
|
|
public changeGrabbingRotation(delta: number) {
|
|
this.call('changeGrabbingRotation', [delta]);
|
|
}
|
|
|
|
public toggleRoomLight() {
|
|
if (this.isRoomLightOn.value) {
|
|
this.call('turnOffRoomLight');
|
|
} else {
|
|
this.call('turnOnRoomLight');
|
|
}
|
|
this.isRoomLightOn.value = !this.isRoomLightOn.value;
|
|
}
|
|
|
|
public interact(id: string) {
|
|
this.call('interact', [this.selected.value!.furnitureId, id]);
|
|
}
|
|
|
|
public standUp() {
|
|
this.call('standUp');
|
|
}
|
|
|
|
public updatePlayerProfiles(profiles: Record<string, PlayerProfile>) {
|
|
this.call('updatePlayerProfiles', [profiles]);
|
|
}
|
|
|
|
public updatePlayerStates(states: Record<string, PlayerState>) {
|
|
this.call('updatePlayerStates', [states]);
|
|
}
|
|
|
|
public clearPlayers() {
|
|
this.call('clearPlayers');
|
|
}
|
|
}
|