diff --git a/public/ui-config.json b/public/ui-config.json index 58dfdcc7..6b223dca 100644 --- a/public/ui-config.json +++ b/public/ui-config.json @@ -4,6 +4,7 @@ "camera.url": "https://nitro.nitrots.co/camera", "thumbnails.url": "https://nitro.nitrots.co/camera/thumbnail/%thumbnail%.png", "url.prefix": "http://localhost:3000", + "floorplan.tile.url": "${url.prefix}/floorplan-editor/tiles.json", "chat.viewer.height.percentage": 0.40, "widget.dimmer.colorwheel": false, "hotelview": { diff --git a/src/api/nitro/room/widgets/handlers/RoomWidgetChatInputHandler.ts b/src/api/nitro/room/widgets/handlers/RoomWidgetChatInputHandler.ts index 81f5411d..9e6f234b 100644 --- a/src/api/nitro/room/widgets/handlers/RoomWidgetChatInputHandler.ts +++ b/src/api/nitro/room/widgets/handlers/RoomWidgetChatInputHandler.ts @@ -1,6 +1,8 @@ import { AvatarExpressionEnum, HabboClubLevelEnum, NitroEvent, RoomControllerLevel, RoomSessionChatEvent, RoomSettingsComposer, RoomWidgetEnum, RoomZoomEvent, TextureUtils } from '@nitrots/nitro-renderer'; import { GetConfiguration, GetNitroInstance } from '../../..'; import { GetRoomEngine, GetSessionDataManager } from '../../../..'; +import { FloorplanEditorEvent } from '../../../../../events/floorplan-editor/FloorplanEditorEvent'; +import { dispatchUiEvent } from '../../../../../hooks'; import { SendMessageHook } from '../../../../../hooks/messages'; import { RoomWidgetFloodControlEvent, RoomWidgetUpdateEvent } from '../events'; import { RoomWidgetChatMessage, RoomWidgetChatSelectAvatarMessage, RoomWidgetChatTypingMessage, RoomWidgetMessage, RoomWidgetRequestWidgetMessage } from '../messages'; @@ -143,7 +145,8 @@ export class RoomWidgetChatInputHandler extends RoomWidgetHandler case ':bcfloor': if(this.container.roomSession.controllerLevel >= RoomControllerLevel.ROOM_OWNER) { - this.container.processWidgetMessage(new RoomWidgetRequestWidgetMessage(RoomWidgetRequestWidgetMessage.FLOOR_EDITOR)); + //this.container.processWidgetMessage(new RoomWidgetRequestWidgetMessage(RoomWidgetRequestWidgetMessage.FLOOR_EDITOR)); + dispatchUiEvent(new FloorplanEditorEvent(FloorplanEditorEvent.SHOW_FLOORPLAN_EDITOR)); } return null; diff --git a/src/assets/images/floorplaneditor/door-direction-0.png b/src/assets/images/floorplaneditor/door-direction-0.png new file mode 100644 index 00000000..8c272a0a Binary files /dev/null and b/src/assets/images/floorplaneditor/door-direction-0.png differ diff --git a/src/assets/images/floorplaneditor/door-direction-1.png b/src/assets/images/floorplaneditor/door-direction-1.png new file mode 100644 index 00000000..52e488f6 Binary files /dev/null and b/src/assets/images/floorplaneditor/door-direction-1.png differ diff --git a/src/assets/images/floorplaneditor/door-direction-2.png b/src/assets/images/floorplaneditor/door-direction-2.png new file mode 100644 index 00000000..da1a1cb5 Binary files /dev/null and b/src/assets/images/floorplaneditor/door-direction-2.png differ diff --git a/src/assets/images/floorplaneditor/door-direction-3.png b/src/assets/images/floorplaneditor/door-direction-3.png new file mode 100644 index 00000000..15712a91 Binary files /dev/null and b/src/assets/images/floorplaneditor/door-direction-3.png differ diff --git a/src/assets/images/floorplaneditor/door-direction-4.png b/src/assets/images/floorplaneditor/door-direction-4.png new file mode 100644 index 00000000..eb1b4b5e Binary files /dev/null and b/src/assets/images/floorplaneditor/door-direction-4.png differ diff --git a/src/assets/images/floorplaneditor/door-direction-5.png b/src/assets/images/floorplaneditor/door-direction-5.png new file mode 100644 index 00000000..46e6f4d9 Binary files /dev/null and b/src/assets/images/floorplaneditor/door-direction-5.png differ diff --git a/src/assets/images/floorplaneditor/door-direction-6.png b/src/assets/images/floorplaneditor/door-direction-6.png new file mode 100644 index 00000000..fda613ac Binary files /dev/null and b/src/assets/images/floorplaneditor/door-direction-6.png differ diff --git a/src/assets/images/floorplaneditor/door-direction-7.png b/src/assets/images/floorplaneditor/door-direction-7.png new file mode 100644 index 00000000..96fa8e48 Binary files /dev/null and b/src/assets/images/floorplaneditor/door-direction-7.png differ diff --git a/src/assets/images/floorplaneditor/icon-door.png b/src/assets/images/floorplaneditor/icon-door.png new file mode 100644 index 00000000..1b56bb2b Binary files /dev/null and b/src/assets/images/floorplaneditor/icon-door.png differ diff --git a/src/assets/images/floorplaneditor/icon-tile-down.png b/src/assets/images/floorplaneditor/icon-tile-down.png new file mode 100644 index 00000000..352c48df Binary files /dev/null and b/src/assets/images/floorplaneditor/icon-tile-down.png differ diff --git a/src/assets/images/floorplaneditor/icon-tile-set.png b/src/assets/images/floorplaneditor/icon-tile-set.png new file mode 100644 index 00000000..eac61532 Binary files /dev/null and b/src/assets/images/floorplaneditor/icon-tile-set.png differ diff --git a/src/assets/images/floorplaneditor/icon-tile-unset.png b/src/assets/images/floorplaneditor/icon-tile-unset.png new file mode 100644 index 00000000..3f5e2181 Binary files /dev/null and b/src/assets/images/floorplaneditor/icon-tile-unset.png differ diff --git a/src/assets/images/floorplaneditor/icon-tile-up.png b/src/assets/images/floorplaneditor/icon-tile-up.png new file mode 100644 index 00000000..27153e0c Binary files /dev/null and b/src/assets/images/floorplaneditor/icon-tile-up.png differ diff --git a/src/assets/images/floorplaneditor/preview_tile.png b/src/assets/images/floorplaneditor/preview_tile.png new file mode 100644 index 00000000..607f4501 Binary files /dev/null and b/src/assets/images/floorplaneditor/preview_tile.png differ diff --git a/src/assets/images/floorplaneditor/selected_height_icon.png b/src/assets/images/floorplaneditor/selected_height_icon.png new file mode 100644 index 00000000..f763fde5 Binary files /dev/null and b/src/assets/images/floorplaneditor/selected_height_icon.png differ diff --git a/src/events/chat-history/ChatHistoryEvent.ts b/src/events/chat-history/ChatHistoryEvent.ts index bf057561..e038f22e 100644 --- a/src/events/chat-history/ChatHistoryEvent.ts +++ b/src/events/chat-history/ChatHistoryEvent.ts @@ -5,5 +5,4 @@ export class ChatHistoryEvent extends NitroEvent public static SHOW_CHAT_HISTORY: string = 'CHE_SHOW_CHAT_HISTORY'; public static HIDE_CHAT_HISTORY: string = 'CHE_HIDE_CHAT_HISTORY'; public static TOGGLE_CHAT_HISTORY: string = 'CHE_TOGGLE_CHAT_HISTORY'; - public static CHAT_HISTORY_CHANGED: string = 'CHE_CHAT_HISTORY_CHANGED'; } diff --git a/src/events/floorplan-editor/FloorplanEditorEvent.ts b/src/events/floorplan-editor/FloorplanEditorEvent.ts new file mode 100644 index 00000000..62f7f3bf --- /dev/null +++ b/src/events/floorplan-editor/FloorplanEditorEvent.ts @@ -0,0 +1,8 @@ +import { NitroEvent } from '@nitrots/nitro-renderer'; + +export class FloorplanEditorEvent extends NitroEvent +{ + public static SHOW_FLOORPLAN_EDITOR: string = 'FPEE_SHOW_FLOORPLAN_EDITOR'; + public static HIDE_FLOORPLAN_EDITOR: string = 'FPEE_HIDE_FLOORPLAN_EDITOR'; + public static TOGGLE_FLOORPLAN_EDITOR: string = 'FPEE_TOGGLE_FLOORPLAN_EDITOR'; +} diff --git a/src/views/Styles.scss b/src/views/Styles.scss index 77dc121f..6369f546 100644 --- a/src/views/Styles.scss +++ b/src/views/Styles.scss @@ -22,3 +22,4 @@ @import './user-profile/UserProfileVew'; @import './chat-history/ChatHistoryView'; @import './help/HelpView'; +@import './floorplan-editor/FloorplanEditorView'; diff --git a/src/views/chat-history/ChatHistoryView.tsx b/src/views/chat-history/ChatHistoryView.tsx index 2a29cdd8..f80ddbbd 100644 --- a/src/views/chat-history/ChatHistoryView.tsx +++ b/src/views/chat-history/ChatHistoryView.tsx @@ -11,8 +11,6 @@ import { RoomHistoryState } from './common/RoomHistoryState'; import { ChatHistoryContextProvider } from './context/ChatHistoryContext'; import { ChatEntryType } from './context/ChatHistoryContext.types'; - - export const ChatHistoryView: FC<{}> = props => { const [ isVisible, setIsVisible ] = useState(false); @@ -52,18 +50,15 @@ export const ChatHistoryView: FC<{}> = props => case ChatHistoryEvent.TOGGLE_CHAT_HISTORY: setIsVisible(!isVisible); break; - case ChatHistoryEvent.CHAT_HISTORY_CHANGED: - break; } }, [isVisible]); useUiEvent(ChatHistoryEvent.HIDE_CHAT_HISTORY, onChatHistoryEvent); useUiEvent(ChatHistoryEvent.SHOW_CHAT_HISTORY, onChatHistoryEvent); useUiEvent(ChatHistoryEvent.TOGGLE_CHAT_HISTORY, onChatHistoryEvent); - useUiEvent(ChatHistoryEvent.CHAT_HISTORY_CHANGED, onChatHistoryEvent); const cache = useMemo(() => -{ + { return new CellMeasurerCache({ defaultHeight: 25, fixedWidth: true, diff --git a/src/views/floorplan-editor/FloorplanEditorView.scss b/src/views/floorplan-editor/FloorplanEditorView.scss new file mode 100644 index 00000000..138d1c09 --- /dev/null +++ b/src/views/floorplan-editor/FloorplanEditorView.scss @@ -0,0 +1,37 @@ +.nitro-floorplan-editor +{ + width: 760px; + height: 575px; + + .editor-area + { + width: 100%; + height: 300px; + overflow-x: scroll; + } + + .set-tile + { + background-image: url('../../assets/images/floorplaneditor/icon-tile-set.png'); + } + + .unset-tile + { + background-image: url('../../assets/images/floorplaneditor/icon-tile-unset.png'); + } + + .increase-height + { + background-image: url('../../assets/images/floorplaneditor/icon-tile-up.png'); + } + + .decrease-height + { + background-image: url('../../assets/images/floorplaneditor/icon-tile-down.png'); + } + + .set-door + { + background-image: url('../../assets/images/floorplaneditor/icon-door.png'); + } +} diff --git a/src/views/floorplan-editor/FloorplanEditorView.tsx b/src/views/floorplan-editor/FloorplanEditorView.tsx new file mode 100644 index 00000000..dd452c57 --- /dev/null +++ b/src/views/floorplan-editor/FloorplanEditorView.tsx @@ -0,0 +1,119 @@ +import { FloorHeightMapEvent, RoomVisualizationSettingsEvent, UpdateFloorPropertiesMessageComposer } from '@nitrots/nitro-renderer'; +import { FC, useCallback, useEffect, useState } from 'react'; +import { LocalizeText } from '../../api'; +import { FloorplanEditorEvent } from '../../events/floorplan-editor/FloorplanEditorEvent'; +import { CreateMessageHook, SendMessageHook, useUiEvent } from '../../hooks'; +import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../layout'; +import { FloorplanEditor } from './common/FloorplanEditor'; +import { convertNumbersForSaving, convertSettingToNumber } from './common/Utils'; +import { FloorplanEditorContextProvider } from './context/FloorplanEditorContext'; +import { IFloorplanSettings, initialFloorplanSettings } from './context/FloorplanEditorContext.types'; +import { FloorplanCanvasView } from './views/FloorplanCanvasView'; +import { FloorplanOptionsView } from './views/FloorplanOptionsView'; + +export const FloorplanEditorView: FC<{}> = props => +{ + const [isVisible, setIsVisible] = useState(false); + const [floorplanSettings, setFloorplanSettings ] = useState(initialFloorplanSettings); + + const onFloorplanEditorEvent = useCallback((event: FloorplanEditorEvent) => + { + switch(event.type) + { + case FloorplanEditorEvent.HIDE_FLOORPLAN_EDITOR: + setIsVisible(false); + break; + case FloorplanEditorEvent.SHOW_FLOORPLAN_EDITOR: + setIsVisible(true); + break; + case FloorplanEditorEvent.TOGGLE_FLOORPLAN_EDITOR: + setIsVisible(!isVisible); + break; + } + }, [isVisible]); + + useUiEvent(FloorplanEditorEvent.HIDE_FLOORPLAN_EDITOR, onFloorplanEditorEvent); + useUiEvent(FloorplanEditorEvent.SHOW_FLOORPLAN_EDITOR, onFloorplanEditorEvent); + useUiEvent(FloorplanEditorEvent.TOGGLE_FLOORPLAN_EDITOR, onFloorplanEditorEvent); + + useEffect(() => + { + FloorplanEditor.instance.initialize(); + }, []); + + const onFloorHeightMapEvent = useCallback((event: FloorHeightMapEvent) => + { + const parser = event.getParser(); + + if(!parser) return; + + const settings = Object.assign({}, floorplanSettings); + settings.tilemap = parser.model; + settings.wallHeight = parser.wallHeight + 1; + setFloorplanSettings(settings); + }, [floorplanSettings]); + + CreateMessageHook(FloorHeightMapEvent, onFloorHeightMapEvent); + + const onRoomVisualizationSettingsEvent = useCallback((event: RoomVisualizationSettingsEvent) => + { + const parser = event.getParser(); + + if(!parser) return; + + const settings = Object.assign({}, floorplanSettings); + settings.thicknessFloor = convertSettingToNumber(parser.thicknessFloor) + settings.thicknessWall = convertSettingToNumber(parser.thicknessWall); + + setFloorplanSettings(settings); + }, [floorplanSettings]); + + CreateMessageHook(RoomVisualizationSettingsEvent, onRoomVisualizationSettingsEvent); + + const saveFloorChanges = useCallback(() => + { + SendMessageHook(new UpdateFloorPropertiesMessageComposer( + FloorplanEditor.instance.getCurrentTilemapString(), + floorplanSettings.entryPoint[0], + floorplanSettings.entryPoint[1], + floorplanSettings.entryPointDir, + convertNumbersForSaving(floorplanSettings.thicknessWall), + convertNumbersForSaving(floorplanSettings.thicknessFloor), + floorplanSettings.wallHeight - 1 + )); + }, [floorplanSettings.entryPoint, floorplanSettings.entryPointDir, floorplanSettings.thicknessFloor, floorplanSettings.thicknessWall, floorplanSettings.wallHeight]); + + return ( + <> + + {isVisible && + + setIsVisible(false)} /> + +
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+ + + +
+
+
+
+ } +
+ + ); +} diff --git a/src/views/floorplan-editor/common/ActionSettings.ts b/src/views/floorplan-editor/common/ActionSettings.ts new file mode 100644 index 00000000..8ac38251 --- /dev/null +++ b/src/views/floorplan-editor/common/ActionSettings.ts @@ -0,0 +1,39 @@ +import { FloorAction, HEIGHT_SCHEME } from './Constants'; + +export class ActionSettings +{ + private _currentAction: number; + private _currentHeight: string; + + constructor() + { + this._currentAction = FloorAction.SET; + this._currentHeight = HEIGHT_SCHEME[1]; + } + + public get currentAction(): number + { + return this._currentAction; + } + + public set currentAction(value: number) + { + this._currentAction = value; + } + + public get currentHeight(): string + { + return this._currentHeight; + } + + public set currentHeight(value: string) + { + this._currentHeight = value; + } + + public clear(): void + { + this._currentAction = FloorAction.SET; + this._currentHeight = HEIGHT_SCHEME[1]; + } +} diff --git a/src/views/floorplan-editor/common/Constants.ts b/src/views/floorplan-editor/common/Constants.ts new file mode 100644 index 00000000..717f40d0 --- /dev/null +++ b/src/views/floorplan-editor/common/Constants.ts @@ -0,0 +1,13 @@ +export const TILE_SIZE = 32; +export const MAX_NUM_TILE_PER_AXIS = 64; + +export const HEIGHT_SCHEME: string = 'x0123456789abcdefghijklmnopq'; + +export class FloorAction +{ + public static readonly DOOR = 0; + public static readonly UP = 1; + public static readonly DOWN = 2; + public static readonly SET = 3; + public static readonly UNSET = 4; +} diff --git a/src/views/floorplan-editor/common/FloorplanEditor.ts b/src/views/floorplan-editor/common/FloorplanEditor.ts new file mode 100644 index 00000000..bdacb553 --- /dev/null +++ b/src/views/floorplan-editor/common/FloorplanEditor.ts @@ -0,0 +1,412 @@ +import { NitroPoint, NitroTilemap, PixiApplicationProxy, PixiInteractionEventProxy, POINT_STRUCT_SIZE } from '@nitrots/nitro-renderer'; +import { GetConfiguration } from '../../../api'; +import { ActionSettings } from './ActionSettings'; +import { FloorAction, HEIGHT_SCHEME, MAX_NUM_TILE_PER_AXIS, TILE_SIZE } from './Constants'; +import { Tile } from './Tile'; +import { getScreenPositionForTile, getTileFromScreenPosition } from './Utils'; + +export class FloorplanEditor extends PixiApplicationProxy +{ + private static _instance: FloorplanEditor = new FloorplanEditor(); + + private static readonly TILE_BLOCKED = 'r_blocked'; + private static readonly TILE_DOOR = 'r_door'; + + private _tilemap: Tile[][]; + private _width: number; + private _height: number; + private _isHolding: boolean; + private _doorLocation: NitroPoint; + private _lastUsedTile: NitroPoint; + private _tilemapRenderer: NitroTilemap; + private _actionSettings: ActionSettings; + private _isInitialized: boolean; + + private constructor() + { + const width = TILE_SIZE * MAX_NUM_TILE_PER_AXIS + 20; + const height = (TILE_SIZE * MAX_NUM_TILE_PER_AXIS) / 2 + 100; + + super({ + width: width, + height: height, + backgroundColor: 0x2b2b2b, + antialias: true, + autoDensity: true, + resolution: 1, + sharedLoader: true, + sharedTicker: true + }); + + this._tilemap = []; + this._doorLocation = new NitroPoint(0, 0); + this._width = 0; + this._height = 0; + this._isHolding = false; + this._lastUsedTile = new NitroPoint(-1,-1); + this._actionSettings = new ActionSettings(); + } + + public initialize(): void + { + if(!this._isInitialized) + { + this.loader.add('tiles', GetConfiguration('floorplan.tile.url')); + + this.loader.load((_, resources) => + { + this._tilemapRenderer = new NitroTilemap(resources['tiles'].spritesheet.baseTexture); + this.registerEventListeners(); + this.stage.addChild(this._tilemapRenderer); + }); + this._isInitialized = true; + } + } + + private registerEventListeners(): void + { + //this._tilemapRenderer.interactive = true; + + const tempPoint = new NitroPoint(); + // @ts-ignore + this._tilemapRenderer.containsPoint = (position) => + { + this._tilemapRenderer.worldTransform.applyInverse(position, tempPoint); + return this.tileHitDettection(tempPoint, false); + }; + + this._tilemapRenderer.on('pointerup', () => + { + this._isHolding = false; + }); + + this._tilemapRenderer.on('pointerout', () => + { + this._isHolding = false; + }); + + this._tilemapRenderer.on('pointerdown', (event: PixiInteractionEventProxy) => + { + if(!(event.data.originalEvent instanceof PointerEvent)) return; + + const pointerEvent = event.data.originalEvent; + if(pointerEvent.button === 2) return; + + + const location = event.data.global; + this.tileHitDettection(location, true); + }); + + this._tilemapRenderer.on('click', (event: PixiInteractionEventProxy) => + { + if(!(event.data.originalEvent instanceof PointerEvent)) return; + + const pointerEvent = event.data.originalEvent; + if(pointerEvent.button === 2) return; + + const location = event.data.global; + this.tileHitDettection(location, true, true); + }); + } + + private tileHitDettection(tempPoint: NitroPoint, setHolding: boolean, isClick: boolean = false): boolean + { + // @ts-ignore + const buffer = this._tilemapRenderer.pointsBuf; + const bufSize = POINT_STRUCT_SIZE; + + const len = buffer.length; + + if(setHolding) + { + this._isHolding = true; + } + + for(let j = 0; j < len; j += bufSize) + { + const bufIndex = j + bufSize; + const data = buffer.slice(j, bufIndex); + + const width = data[4]; + const height = data[5]; + + + const mousePositionX = Math.floor(tempPoint.x); + const mousePositionY = Math.floor(tempPoint.y); + + const tileStartX = data[2]; + const tileStartY = data[3]; + + + const centreX = tileStartX + (width / 2); + const centreY = tileStartY + (height / 2); + + const dx = Math.abs(mousePositionX - centreX - 2); + const dy = Math.abs(mousePositionY - centreY - 2); + + const solution = (dx / (width * 0.5) + dy / (height * 0.5) <= 1);//todo: improve this + if(solution) + { + if(this._isHolding) + { + const [realX, realY] = getTileFromScreenPosition(tileStartX, tileStartY); + + if(isClick) + { + this.onClick(realX, realY); + } + + else if(this._lastUsedTile.x !== realX || this._lastUsedTile.y !== realY) + { + this._lastUsedTile.x = realX; + this._lastUsedTile.y = realY; + this.onClick(realX, realY); + } + + } + return true; + } + + } + return false; + } + + + private onClick(x: number, y: number): void + { + const tile = this._tilemap[y][x]; + const heightIndex = HEIGHT_SCHEME.indexOf(tile.height); + + let futureHeightIndex = 0; + + switch(this._actionSettings.currentAction) + { + case FloorAction.DOOR: + + if(tile.height !== 'x') + { + this._doorLocation.x = x; + this._doorLocation.y = y; + this.renderTiles(); + } + return; + case FloorAction.UP: + futureHeightIndex = heightIndex + 1; + break; + case FloorAction.DOWN: + futureHeightIndex = heightIndex - 1; + break; + case FloorAction.SET: + futureHeightIndex = HEIGHT_SCHEME.indexOf(this._actionSettings.currentHeight); + break; + case FloorAction.UNSET: + futureHeightIndex = 0; + break; + } + + if(futureHeightIndex === -1) return; + + if(heightIndex === futureHeightIndex) return; + + if(futureHeightIndex > 0) + { + if((x + 1) > this._width) this._width = x + 1; + + if( (y + 1) > this._height) this._height = y + 1; + } + + const newHeight = HEIGHT_SCHEME[futureHeightIndex]; + + if(!newHeight) return; + + if(tile.isBlocked) return; + + this._tilemap[y][x].height = newHeight; + + this.renderTiles(); + } + + public renderTiles(): void + { + this.tilemapRenderer.clear(); + + for(let y = 0; y < this._tilemap.length; y++) + { + for(let x = 0; x < this.tilemap[y].length; x++) + { + const tile = this.tilemap[y][x]; + let assetName = tile.height; + + if(this._doorLocation.x === x && this._doorLocation.y === y) + assetName = FloorplanEditor.TILE_DOOR; + + if(tile.isBlocked) assetName = FloorplanEditor.TILE_BLOCKED; + + //if((tile.height === 'x') || tile.height === 'X') continue; + const [positionX, positionY ] = getScreenPositionForTile(x, y); + this._tilemapRenderer.tile(`${assetName}.png`, positionX, positionY); + } + } + } + + public setTilemap(map: string, blockedTiles: boolean[][]): void + { + this._tilemap = []; + const roomMapStringSplit = map.split('\r'); + + let width = 0; + let height = roomMapStringSplit.length; + + // find the map width, height + for(let y = 0; y < height; y++) + { + const originalRow = roomMapStringSplit[y]; + + if(originalRow.length === 0) + { + roomMapStringSplit.splice(y, 1); + height = roomMapStringSplit.length; + y--; + continue; + } + + if(originalRow.length > width) + { + width = originalRow.length; + } + } + // fill map with room heightmap tiles + for(let y = 0; y < height; y++) + { + this._tilemap[y] = []; + const rowString = roomMapStringSplit[y]; + + for(let x = 0; x < width; x++) + { + const blocked = (blockedTiles[y] && blockedTiles[y][x]) || false; + + const char = rowString[x]; + if(((!(char === 'x')) && (!(char === 'X')) && char)) + { + this._tilemap[y][x] = new Tile(char, blocked); + } + else + { + this._tilemap[y][x] = new Tile('x', blocked); + } + } + + for(let x = width; x < MAX_NUM_TILE_PER_AXIS; x++) + { + this.tilemap[y][x] = new Tile('x', false); + } + } + + // fill remaining map with empty tiles + for(let y = height; y < MAX_NUM_TILE_PER_AXIS; y++) + { + if(!this.tilemap[y]) this.tilemap[y] = []; + for(let x = 0; x < MAX_NUM_TILE_PER_AXIS; x++) + { + this.tilemap[y][x] = new Tile('x', false); + } + } + + this._width = width; + this._height = height; + } + + public getCurrentTilemapString(): string + { + const highestTile = this._tilemap[this._height - 1][this._width - 1]; + + if(highestTile.height === 'x') + { + this._width = -1; + this._height = -1; + + for(let y = MAX_NUM_TILE_PER_AXIS - 1; y >= 0; y--) + { + if(!this._tilemap[y]) continue; + + for(let x = MAX_NUM_TILE_PER_AXIS - 1; x >= 0; x--) + { + if(!this._tilemap[y][x]) continue; + + const tile = this._tilemap[y][x]; + + if(tile.height !== 'x') + { + if( (x + 1) > this._width) + this._width = x + 1; + + if( (y + 1) > this._height) + this._height = y + 1; + } + } + } + } + + + const rows = []; + + for(let y = 0; y < this._height; y++) + { + const row = []; + + for(let x = 0; x < this._width; x++) + { + const tile = this._tilemap[y][x]; + + row[x] = tile.height; + } + + rows[y] = row.join(''); + } + + return rows.join('\r'); + } + + public clear(): void + { + this._tilemapRenderer.interactive = false; + this._tilemap = []; + this._doorLocation.set(-1, -1); + this._width = 0; + this._height = 0; + this._isHolding = false; + this._lastUsedTile.set(-1, -1); + this._actionSettings.clear(); + this._tilemapRenderer.clear(); + } + + public get tilemapRenderer(): NitroTilemap + { + return this._tilemapRenderer; + } + + public get tilemap(): Tile[][] + { + return this._tilemap; + } + + public get doorLocation(): NitroPoint + { + return this._doorLocation; + } + + public set doorLocation(value: NitroPoint) + { + this._doorLocation = value; + } + + public static get instance(): FloorplanEditor + { + if(!FloorplanEditor._instance) + { + FloorplanEditor._instance = new FloorplanEditor(); + } + + return FloorplanEditor._instance; + } +} diff --git a/src/views/floorplan-editor/common/Tile.ts b/src/views/floorplan-editor/common/Tile.ts new file mode 100644 index 00000000..fd9c0596 --- /dev/null +++ b/src/views/floorplan-editor/common/Tile.ts @@ -0,0 +1,31 @@ +export class Tile +{ + private _height: string; + private _isBlocked: boolean; + + constructor(height: string, isBlocked: boolean) + { + this._height = height; + this._isBlocked = isBlocked; + } + + public get height(): string + { + return this._height; + } + + public set height(height: string) + { + this._height = height; + } + + public get isBlocked(): boolean + { + return this._isBlocked; + } + + public set isBlocked(val: boolean) + { + this._isBlocked = val; + } +} diff --git a/src/views/floorplan-editor/common/Utils.ts b/src/views/floorplan-editor/common/Utils.ts new file mode 100644 index 00000000..6ffa52de --- /dev/null +++ b/src/views/floorplan-editor/common/Utils.ts @@ -0,0 +1,53 @@ +import { TILE_SIZE } from './Constants'; + +export const getScreenPositionForTile = (x: number, y: number): [number , number] => +{ + let positionX = (x * TILE_SIZE / 2) - (y * TILE_SIZE / 2); + const positionY = (x * TILE_SIZE / 4) + (y * TILE_SIZE / 4); + + positionX = positionX + 1024; // center the map in the canvas + + return [positionX, positionY]; +} + +export const getTileFromScreenPosition = (x: number, y: number): [number, number] => +{ + const translatedX = x - 1024; // after centering translation + + const realX = ((translatedX /(TILE_SIZE / 2)) + (y / (TILE_SIZE / 4))) / 2; + const realY = ((y /(TILE_SIZE / 4)) - (translatedX / (TILE_SIZE / 2))) / 2; + + return [realX, realY]; +} + +export const convertNumbersForSaving = (value: number): number => +{ + value = parseInt(value.toString()); + switch(value) + { + case 0: + return -2; + case 1: + return -1; + case 3: + return 1; + default: + return 0; + + } +} + +export const convertSettingToNumber = (value: number): number => +{ + switch(value) + { + case 0.25: + return 0; + case 0.5: + return 1; + case 2: + return 3; + default: + return 2; + } +} diff --git a/src/views/floorplan-editor/context/FloorplanEditorContext.tsx b/src/views/floorplan-editor/context/FloorplanEditorContext.tsx new file mode 100644 index 00000000..613c6fe6 --- /dev/null +++ b/src/views/floorplan-editor/context/FloorplanEditorContext.tsx @@ -0,0 +1,14 @@ +import { createContext, FC, useContext } from 'react'; +import { FloorplanEditorContextProps, IFloorplanEditorContext } from './FloorplanEditorContext.types'; + +const FloorplanEditorContext = createContext({ + floorplanSettings: null, + setFloorplanSettings: null +}); + +export const FloorplanEditorContextProvider: FC = props => +{ + return { props.children } +} + +export const useFloorplanEditorContext = () => useContext(FloorplanEditorContext); diff --git a/src/views/floorplan-editor/context/FloorplanEditorContext.types.ts b/src/views/floorplan-editor/context/FloorplanEditorContext.types.ts new file mode 100644 index 00000000..c6d8eb66 --- /dev/null +++ b/src/views/floorplan-editor/context/FloorplanEditorContext.types.ts @@ -0,0 +1,32 @@ +import { ProviderProps } from 'react'; + +export interface IFloorplanEditorContext +{ + floorplanSettings: IFloorplanSettings; + setFloorplanSettings: React.Dispatch>; +} + +export interface IFloorplanSettings { + tilemap: string; + reservedTiles: boolean[][]; + entryPoint: [number, number]; + entryPointDir: number; + wallHeight: number; + thicknessWall: number; + thicknessFloor: number; +} + +export const initialFloorplanSettings: IFloorplanSettings = { + tilemap: '', + reservedTiles: [], + entryPoint: [0, 0], + entryPointDir: 2, + wallHeight: -1, + thicknessWall: 1, + thicknessFloor: 1 +} + +export interface FloorplanEditorContextProps extends ProviderProps +{ + +} diff --git a/src/views/floorplan-editor/views/FloorplanCanvasView.tsx b/src/views/floorplan-editor/views/FloorplanCanvasView.tsx new file mode 100644 index 00000000..a6f9969c --- /dev/null +++ b/src/views/floorplan-editor/views/FloorplanCanvasView.tsx @@ -0,0 +1,72 @@ +import { GetOccupiedTilesMessageComposer, GetRoomEntryTileMessageComposer, NitroPoint, RoomEntryTileMessageEvent, RoomOccupiedTilesMessageEvent } from '@nitrots/nitro-renderer'; +import { FC, useCallback, useEffect, useRef, useState } from 'react'; +import { CreateMessageHook, SendMessageHook } from '../../../hooks'; +import { FloorplanEditor } from '../common/FloorplanEditor'; +import { useFloorplanEditorContext } from '../context/FloorplanEditorContext'; + +export const FloorplanCanvasView: FC<{}> = props => +{ + const { floorplanSettings = null, setFloorplanSettings = null } = useFloorplanEditorContext(); + const [ occupiedTilesReceived , setOccupiedTilesReceived ] = useState(false); + const [ entryTileReceived, setEntryTileReceived ] = useState(false); + const elementRef = useRef(null); + + useEffect(() => + { + SendMessageHook(new GetRoomEntryTileMessageComposer()); + SendMessageHook(new GetOccupiedTilesMessageComposer()); + FloorplanEditor.instance.tilemapRenderer.interactive = true; + elementRef.current.appendChild(FloorplanEditor.instance.renderer.view); + + return ( () => + { + FloorplanEditor.instance.clear(); + }); + }, []); + + const onRoomOccupiedTilesMessageEvent = useCallback((event: RoomOccupiedTilesMessageEvent) => + { + const parser = event.getParser(); + + if(!parser) return; + + const settings = Object.assign({}, floorplanSettings); + settings.reservedTiles = parser.blockedTilesMap; + setFloorplanSettings(settings); + + FloorplanEditor.instance.setTilemap(floorplanSettings.tilemap, parser.blockedTilesMap); + + setOccupiedTilesReceived(true); + + elementRef.current.scrollTo(FloorplanEditor.instance.view.width / 3, 0); + }, [floorplanSettings, setFloorplanSettings]); + + CreateMessageHook(RoomOccupiedTilesMessageEvent, onRoomOccupiedTilesMessageEvent); + + const onRoomEntryTileMessageEvent = useCallback((event: RoomEntryTileMessageEvent) => + { + const parser = event.getParser(); + + if(!parser) return; + + const settings = Object.assign({}, floorplanSettings); + settings.entryPoint = [parser.x, parser.y]; + settings.entryPointDir = parser.direction; + setFloorplanSettings(settings); + + FloorplanEditor.instance.doorLocation = new NitroPoint(settings.entryPoint[0], settings.entryPoint[1]); + setEntryTileReceived(true); + }, [floorplanSettings, setFloorplanSettings]); + + CreateMessageHook(RoomEntryTileMessageEvent, onRoomEntryTileMessageEvent); + + useEffect(() => + { + if(entryTileReceived && occupiedTilesReceived) + FloorplanEditor.instance.renderTiles(); + }, [entryTileReceived, occupiedTilesReceived]) + + return ( +
+ ); +} diff --git a/src/views/floorplan-editor/views/FloorplanOptionsView.tsx b/src/views/floorplan-editor/views/FloorplanOptionsView.tsx new file mode 100644 index 00000000..1132bcf6 --- /dev/null +++ b/src/views/floorplan-editor/views/FloorplanOptionsView.tsx @@ -0,0 +1,20 @@ +import { FC } from 'react'; +import { NitroCardGridItemView, NitroCardGridView } from '../../../layout'; +import { useFloorplanEditorContext } from '../context/FloorplanEditorContext'; + +export const FloorplanOptionsView: FC<{}> = props => +{ + const { floorplanSettings = null, setFloorplanSettings = null } = useFloorplanEditorContext(); + + return ( + <> + + + + + + + + + ); +} diff --git a/src/views/main/MainView.tsx b/src/views/main/MainView.tsx index 2a7e1427..7a5eccdd 100644 --- a/src/views/main/MainView.tsx +++ b/src/views/main/MainView.tsx @@ -8,6 +8,7 @@ import { AvatarEditorView } from '../avatar-editor/AvatarEditorView'; import { CameraWidgetView } from '../camera/CameraWidgetView'; import { CatalogView } from '../catalog/CatalogView'; import { ChatHistoryView } from '../chat-history/ChatHistoryView'; +import { FloorplanEditorView } from '../floorplan-editor/FloorplanEditorView'; import { FriendsView } from '../friends/FriendsView'; import { GroupsView } from '../groups/GroupsView'; import { HelpView } from '../help/HelpView'; @@ -73,6 +74,7 @@ export const MainView: FC = props => +
); } diff --git a/src/views/navigator/views/room-info/NavigatorRoomInfoView.tsx b/src/views/navigator/views/room-info/NavigatorRoomInfoView.tsx index f46de29c..f0a9ab42 100644 --- a/src/views/navigator/views/room-info/NavigatorRoomInfoView.tsx +++ b/src/views/navigator/views/room-info/NavigatorRoomInfoView.tsx @@ -3,6 +3,7 @@ import classNames from 'classnames'; import { FC, useCallback, useEffect, useState } from 'react'; import { GetConfiguration, GetGroupInformation, GetSessionDataManager, LocalizeText } from '../../../../api'; import { NavigatorEvent } from '../../../../events'; +import { FloorplanEditorEvent } from '../../../../events/floorplan-editor/FloorplanEditorEvent'; import { RoomWidgetThumbnailEvent } from '../../../../events/room-widgets/thumbnail'; import { dispatchUiEvent } from '../../../../hooks/events'; import { SendMessageHook } from '../../../../hooks/messages'; @@ -98,6 +99,9 @@ export const NavigatorRoomInfoView: FC = props => setIsRoomMuted(value => !value); SendMessageHook(new RoomMuteComposer()); return; + case 'open_floorplan_editor': + dispatchUiEvent(new FloorplanEditorEvent(FloorplanEditorEvent.TOGGLE_FLOORPLAN_EDITOR)); + return; case 'close': onCloseClick(); return; @@ -155,7 +159,7 @@ export const NavigatorRoomInfoView: FC = props => { hasPermission('settings') && <> - + } { hasPermission('staff_pick') && }