├── docs ├── static │ ├── .nojekyll │ └── img │ │ ├── tile.png │ │ ├── plain.png │ │ ├── favicon.ico │ │ └── textured.png ├── babel.config.js ├── .gitignore ├── sidebars.js ├── src │ ├── pages │ │ ├── styles.module.css │ │ └── index.js │ └── css │ │ └── custom.css ├── package.json ├── README.md ├── docs │ ├── guides │ │ ├── avatar-actions.md │ │ ├── create-room.md │ │ ├── use-base-avatar-and-furniture.md │ │ ├── adding-objects.md │ │ └── adding-windows.md │ └── install.md └── docusaurus.config.js ├── src ├── interfaces │ ├── IHitDetection.ts │ ├── IAnimationTicker.ts │ ├── ITextureable.ts │ ├── IRoomObject.ts │ ├── IRoomGeometry.ts │ ├── IRoomObjectContainer.ts │ ├── ITileMap.ts │ ├── IConfiguration.ts │ ├── IFurnitureLoader.ts │ ├── IAvatarLoader.ts │ ├── IFurnitureData.ts │ ├── IRoomVisualization.ts │ └── IRoomContext.ts ├── types │ ├── types.ts │ ├── TileType.ts │ └── RoomPosition.ts ├── objects │ ├── avatar │ │ ├── util │ │ │ ├── index.ts │ │ │ ├── getAvatarDirection.ts │ │ │ ├── parseLookString.ts │ │ │ ├── tests │ │ │ │ ├── getAvatarDirection.test.ts │ │ │ │ └── parseLookString.test.ts │ │ │ ├── getDrawOrderForActions.ts │ │ │ ├── getEffectSprite.ts │ │ │ ├── createLookServer.ts │ │ │ ├── getLibrariesForLook.ts │ │ │ ├── getAvatarDrawDefinition.ts │ │ │ └── getAssetFromPartMeta.ts │ │ ├── data │ │ │ ├── interfaces │ │ │ │ ├── IFigureMapData.ts │ │ │ │ ├── IAvatarOffsetsData.ts │ │ │ │ ├── IAvatarPartSetsData.ts │ │ │ │ ├── IAvatarEffectMap.ts │ │ │ │ ├── IManifestLibrary.ts │ │ │ │ ├── IFigureData.ts │ │ │ │ ├── IAvatarAnimationData.ts │ │ │ │ ├── IAvatarManifestData.ts │ │ │ │ ├── IAvatarEffectBundle.ts │ │ │ │ ├── IAvatarActionsData.ts │ │ │ │ ├── IAvatarGeometryData.ts │ │ │ │ └── IAvatarEffectData.ts │ │ │ ├── AvatarData.ts │ │ │ ├── AvatarOffsetsData.ts │ │ │ ├── ManifestLibrary.ts │ │ │ ├── AvatarEffectMap.ts │ │ │ ├── AvatarPartSetsData.ts │ │ │ ├── FigureMapData.ts │ │ │ └── AvatarManifestData.ts │ │ ├── structure │ │ │ └── interface │ │ │ │ ├── IAvatarDrawablePart.ts │ │ │ │ └── IAvatarEffectPart.ts │ │ ├── enum │ │ │ ├── AvatarFigurePartType.ts │ │ │ └── AvatarAction.ts │ │ ├── AvatarEffectBundle.ts │ │ └── types │ │ │ └── index.ts │ ├── furniture │ │ ├── FurnitureExtraData.ts │ │ ├── data │ │ │ ├── FurnitureIndexJson.ts │ │ │ ├── interfaces │ │ │ │ ├── IFurnitureIndexData.ts │ │ │ │ ├── IFurnitureAssetsData.ts │ │ │ │ └── IFurnitureVisualizationData.ts │ │ │ ├── FurnitureAssetsJson.ts │ │ │ ├── FurnitureJson.ts │ │ │ ├── JsonFurnitureAssetsData.ts │ │ │ ├── FurnitureIndexData.ts │ │ │ ├── FurnitureVisualizationJson.ts │ │ │ └── FurnitureAssetsData.ts │ │ ├── FurnitureFetchInfo.tsx │ │ ├── util │ │ │ ├── index.ts │ │ │ ├── IFurnitureEventHandlers.ts │ │ │ ├── getDirectionForFurniture.ts │ │ │ ├── DrawDefinition.ts │ │ │ ├── getDirectionForFurniture.test.ts │ │ │ ├── getFurnitureFetch.ts │ │ │ └── visualization │ │ │ │ ├── parseColors.ts │ │ │ │ ├── parseLayers.ts │ │ │ │ ├── parseDirections.ts │ │ │ │ ├── parseAnimations.ts │ │ │ │ └── VisualizationXml.ts │ │ ├── IFurnitureVisualization.ts │ │ ├── IFurnitureAssetBundle.ts │ │ ├── FurnitureRoomVisualization.ts │ │ ├── IFurniture.ts │ │ ├── visualization │ │ │ ├── FurnitureVisualization.ts │ │ │ └── BasicFurnitureVisualization.ts │ │ ├── filter │ │ │ └── HighlightFilter.ts │ │ ├── XmlFurnitureAssetBundle.ts │ │ ├── FurnitureSprite.ts │ │ └── IFurnitureVisualizationView.ts │ ├── interfaces │ │ ├── IScreenPositioned.ts │ │ └── IMoveable.ts │ ├── room │ │ ├── ILandscapeContainer.ts │ │ ├── parts │ │ │ ├── IRoomPart.ts │ │ │ ├── RoomPartData.ts │ │ │ ├── WallRight.ts │ │ │ └── WallOuterCorner.ts │ │ ├── ITileColorable.ts │ │ ├── IWallColorable.ts │ │ ├── IRoomRectangle.ts │ │ ├── util │ │ │ ├── getTilePosition.ts │ │ │ ├── getMaskId.ts │ │ │ ├── getPosition.ts │ │ │ ├── getTilePositionForTile.ts │ │ │ ├── getTileColors.ts │ │ │ ├── createPlaneMatrix.ts │ │ │ ├── getTileMapBounds.ts │ │ │ └── LegacyWallGeometry.ts │ │ ├── ParsedTileMap.ts │ │ ├── RoomObjectContainer.ts │ │ ├── matrixes.ts │ │ └── RoomObjectContainer.test.ts │ ├── events │ │ ├── interfaces │ │ │ ├── IEventManagerNode.ts │ │ │ ├── IEventTarget.ts │ │ │ ├── IEventManager.ts │ │ │ ├── IEventGroup.ts │ │ │ ├── IEventHittable.ts │ │ │ ├── IEventHandler.ts │ │ │ └── IEventManagerEvent.ts │ │ ├── EventEmitter.ts │ │ ├── EventManagerNode.ts │ │ └── EventManagerContainer.ts │ ├── hitdetection │ │ └── HitTexture.test.ts │ ├── animation │ │ └── AnimationTicker.ts │ └── Shroom.ts ├── util │ ├── mock.ts │ ├── isTile.ts │ ├── getIntFromHex.ts │ ├── getZOrder.ts │ ├── notNullOrUndefined.ts │ ├── isSetEqual.ts │ ├── applyTextureProperties.ts │ ├── associateBy.ts │ ├── getNumberFromAttribute.ts │ ├── traverseDOMTree.ts │ ├── loadImageFromBlob.ts │ ├── loadImageFromUrl.ts │ ├── tilemap │ │ ├── getColumnWalls.test.ts │ │ ├── padTileMap.ts │ │ ├── getRowWalls.ts │ │ └── getColumnWalls.ts │ ├── loadRoomTexture.ts │ ├── parseTileMapString.ts │ └── isPointInside.ts ├── assets │ ├── IAssetBundle.ts │ └── LegacyAssetBundle.ts ├── tools │ └── dump │ │ ├── types.ts │ │ ├── Logger.ts │ │ ├── parseExternalVariables.ts │ │ ├── extractSwfs.ts │ │ ├── dumpFigure.ts │ │ ├── downloadFileWithMessage.ts │ │ ├── ProgressBar.ts │ │ ├── getExternalVariableUrls.ts │ │ ├── createOffsetFile.ts │ │ ├── downloadEffects.ts │ │ ├── downloadAllFiles.ts │ │ ├── downloadFigures.ts │ │ ├── detectEdges.ts │ │ ├── downloadMultipleFiles.ts │ │ ├── dumpFurniture.ts │ │ └── downloadFile.ts └── data │ └── XmlData.ts ├── example ├── .gitignore ├── src │ ├── index.ejs │ ├── behaviors │ │ ├── FurniInfoBehavior.ts │ │ └── MultiStateBehavior.ts │ └── index.ts ├── tsconfig.json ├── babel.config.js ├── package.json └── webpack.config.js ├── storybook ├── .gitignore ├── .storybook │ ├── preview.js │ └── main.js ├── stories │ ├── assets │ │ ├── tile.png │ │ └── tile2.png │ ├── types.d.ts │ ├── furniture │ │ ├── FurnitureVisualizations.stories.ts │ │ ├── FurnitureExamples.stories.ts │ │ ├── FurnitureIssues.stories.ts │ │ └── renderFurnitureExample.ts │ ├── avatar │ │ ├── AvatarEffect.stories.ts │ │ └── renderAvatarDirections.ts │ └── common │ │ └── createShroom.tsx └── package.json ├── .vscode └── settings.json ├── e2e ├── .gitignore ├── src │ ├── TestMap.tsx │ ├── index.tsx │ ├── TestRenderer.ts │ ├── index.ejs │ ├── tests │ │ └── room │ │ │ ├── models │ │ │ ├── util │ │ │ │ └── renderModel.ts │ │ │ ├── renderModelC.ts │ │ │ ├── renderModelD.ts │ │ │ ├── renderModelE.ts │ │ │ ├── renderModelF.ts │ │ │ ├── renderModelH.ts │ │ │ ├── renderModelA.ts │ │ │ ├── renderModelB.ts │ │ │ ├── renderModelG.ts │ │ │ ├── renderModelL.ts │ │ │ ├── renderModelN.ts │ │ │ ├── renderModelI.ts │ │ │ ├── renderModelJ.ts │ │ │ ├── renderModelP.ts │ │ │ ├── renderModelQ.ts │ │ │ ├── renderModelR.ts │ │ │ ├── renderModelK.ts │ │ │ ├── renderModelO.ts │ │ │ └── renderModelM.ts │ │ │ └── renderHiddenWalls.ts │ └── CaretDown.tsx ├── tsconfig.json ├── babel.config.js ├── package.json └── webpack.config.js ├── .prettierrc ├── .gitignore ├── jest.config.js ├── README.md ├── tsconfig.json ├── ci └── discord │ ├── tsconfig.json │ ├── package.json │ └── src │ ├── template.json │ └── index.ts ├── .github └── workflows │ ├── node.js.yml │ ├── npm-publish.yml │ └── documentation.yml ├── .eslintrc.js └── webpack.config.js /docs/static/.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/interfaces/IHitDetection.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | ./public/figure 2 | ./public/hof_furni 3 | ./public/resources -------------------------------------------------------------------------------- /storybook/.gitignore: -------------------------------------------------------------------------------- 1 | ./public/figure 2 | ./public/hof_furni 3 | ./public/resources -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } -------------------------------------------------------------------------------- /docs/static/img/tile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jankuss/shroom/HEAD/docs/static/img/tile.png -------------------------------------------------------------------------------- /src/types/types.ts: -------------------------------------------------------------------------------- 1 | declare module "*.png" { 2 | const a: any; 3 | export default a; 4 | } 5 | -------------------------------------------------------------------------------- /docs/static/img/plain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jankuss/shroom/HEAD/docs/static/img/plain.png -------------------------------------------------------------------------------- /e2e/.gitignore: -------------------------------------------------------------------------------- 1 | ./public/figure 2 | ./public/hof_furni 3 | ./public/resources 4 | 5 | ./node_modules -------------------------------------------------------------------------------- /src/objects/avatar/util/index.ts: -------------------------------------------------------------------------------- 1 | export { createLookServer, LookServer } from "./createLookServer"; 2 | -------------------------------------------------------------------------------- /src/util/mock.ts: -------------------------------------------------------------------------------- 1 | export function mock(value: Partial): T { 2 | return value as T; 3 | } 4 | -------------------------------------------------------------------------------- /docs/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jankuss/shroom/HEAD/docs/static/img/favicon.ico -------------------------------------------------------------------------------- /docs/static/img/textured.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jankuss/shroom/HEAD/docs/static/img/textured.png -------------------------------------------------------------------------------- /src/util/isTile.ts: -------------------------------------------------------------------------------- 1 | export const isTile = (type: number | "x"): type is number => 2 | !isNaN(Number(type)); 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": false 6 | } -------------------------------------------------------------------------------- /storybook/.storybook/preview.js: -------------------------------------------------------------------------------- 1 | 2 | export const parameters = { 3 | actions: { argTypesRegex: "^on[A-Z].*" }, 4 | } -------------------------------------------------------------------------------- /storybook/stories/assets/tile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jankuss/shroom/HEAD/storybook/stories/assets/tile.png -------------------------------------------------------------------------------- /storybook/stories/assets/tile2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jankuss/shroom/HEAD/storybook/stories/assets/tile2.png -------------------------------------------------------------------------------- /storybook/stories/types.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.png" { 2 | const url: string; 3 | export default url; 4 | } 5 | -------------------------------------------------------------------------------- /docs/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')], 3 | }; 4 | -------------------------------------------------------------------------------- /src/util/getIntFromHex.ts: -------------------------------------------------------------------------------- 1 | export function getIntFromHex(str: string) { 2 | return parseInt(str.replace(/^#/, ""), 16); 3 | } 4 | -------------------------------------------------------------------------------- /src/util/getZOrder.ts: -------------------------------------------------------------------------------- 1 | export function getZOrder(x: number, y: number, z: number) { 2 | return x * 1000 + y * 1000 + z; 3 | } 4 | -------------------------------------------------------------------------------- /src/objects/furniture/FurnitureExtraData.ts: -------------------------------------------------------------------------------- 1 | export interface FurnitureExtraData { 2 | visualization?: string; 3 | logic?: string; 4 | } 5 | -------------------------------------------------------------------------------- /e2e/src/TestMap.tsx: -------------------------------------------------------------------------------- 1 | import { TestRenderer } from "./TestRenderer"; 2 | 3 | export type TestMap = { [key: string]: TestRenderer | TestMap }; 4 | -------------------------------------------------------------------------------- /src/objects/furniture/data/FurnitureIndexJson.ts: -------------------------------------------------------------------------------- 1 | export interface FurnitureIndexJson { 2 | logic?: string; 3 | visualization?: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/objects/interfaces/IScreenPositioned.ts: -------------------------------------------------------------------------------- 1 | export interface IScreenPositioned { 2 | screenPosition: { x: number; y: number } | undefined; 3 | } 4 | -------------------------------------------------------------------------------- /src/util/notNullOrUndefined.ts: -------------------------------------------------------------------------------- 1 | export function notNullOrUndefined(value: T | null | undefined): value is T { 2 | return value != null; 3 | } 4 | -------------------------------------------------------------------------------- /src/assets/IAssetBundle.ts: -------------------------------------------------------------------------------- 1 | export interface IAssetBundle { 2 | getBlob(name: string): Promise; 3 | getString(name: string): Promise; 4 | } 5 | -------------------------------------------------------------------------------- /src/objects/furniture/data/interfaces/IFurnitureIndexData.ts: -------------------------------------------------------------------------------- 1 | export interface IFurnitureIndexData { 2 | logic?: string; 3 | visualization?: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/objects/interfaces/IMoveable.ts: -------------------------------------------------------------------------------- 1 | export interface IMoveable { 2 | move(roomX: number, roomY: number, roomZ: number): void; 3 | clearMovement(): void; 4 | } 5 | -------------------------------------------------------------------------------- /src/objects/room/ILandscapeContainer.ts: -------------------------------------------------------------------------------- 1 | export interface ILandscapeContainer { 2 | getMaskLevel(roomX: number, roomY: number): { roomX: number; roomY: number }; 3 | } 4 | -------------------------------------------------------------------------------- /src/objects/room/parts/IRoomPart.ts: -------------------------------------------------------------------------------- 1 | import { RoomPartData } from "./RoomPartData"; 2 | 3 | export interface IRoomPart { 4 | update(data: RoomPartData): void; 5 | } 6 | -------------------------------------------------------------------------------- /src/interfaces/IAnimationTicker.ts: -------------------------------------------------------------------------------- 1 | export interface IAnimationTicker { 2 | subscribe(cb: (frame: number, accurateFrame: number) => void): () => void; 3 | current(): number; 4 | } 5 | -------------------------------------------------------------------------------- /src/interfaces/ITextureable.ts: -------------------------------------------------------------------------------- 1 | import * as PIXI from "pixi.js"; 2 | 3 | export interface ITexturable { 4 | texture: PIXI.Texture | undefined; 5 | color: string | undefined; 6 | } 7 | -------------------------------------------------------------------------------- /src/objects/events/interfaces/IEventManagerNode.ts: -------------------------------------------------------------------------------- 1 | import { Rectangle } from "../../room/IRoomRectangle"; 2 | 3 | export interface IEventManagerNode { 4 | destroy(): void; 5 | } 6 | -------------------------------------------------------------------------------- /src/interfaces/IRoomObject.ts: -------------------------------------------------------------------------------- 1 | import { IRoomContext } from "./IRoomContext"; 2 | 3 | export interface IRoomObject { 4 | setParent(room: IRoomContext): void; 5 | destroy(): void; 6 | } 7 | -------------------------------------------------------------------------------- /src/objects/furniture/FurnitureFetchInfo.tsx: -------------------------------------------------------------------------------- 1 | import { FurnitureId } from "../../interfaces/IFurnitureData"; 2 | 3 | export type FurnitureFetchInfo = { id?: FurnitureId; type?: string }; 4 | -------------------------------------------------------------------------------- /src/interfaces/IRoomGeometry.ts: -------------------------------------------------------------------------------- 1 | export interface IRoomGeometry { 2 | getPosition( 3 | roomX: number, 4 | roomY: number, 5 | roomZ: number 6 | ): { x: number; y: number }; 7 | } 8 | -------------------------------------------------------------------------------- /src/objects/avatar/data/interfaces/IFigureMapData.ts: -------------------------------------------------------------------------------- 1 | export interface IFigureMapData { 2 | getLibraryOfPart(id: string, type: string): string | undefined; 3 | getLibraries(): string[]; 4 | } 5 | -------------------------------------------------------------------------------- /src/objects/room/ITileColorable.ts: -------------------------------------------------------------------------------- 1 | export interface ITileColorable { 2 | tileLeftColor: number; 3 | tileRightColor: number; 4 | tileTopColor: number; 5 | tileTexture: PIXI.Texture; 6 | } 7 | -------------------------------------------------------------------------------- /src/objects/room/IWallColorable.ts: -------------------------------------------------------------------------------- 1 | export interface IWallColorable { 2 | wallLeftColor: number; 3 | wallRightColor: number; 4 | wallTopColor: number; 5 | wallTexture: PIXI.Texture; 6 | } 7 | -------------------------------------------------------------------------------- /src/tools/dump/types.ts: -------------------------------------------------------------------------------- 1 | declare module "bin-pack" { 2 | const x: any; 3 | export = x; 4 | } 5 | 6 | declare module "detect-edges" { 7 | const x: any; 8 | export = x; 9 | } 10 | -------------------------------------------------------------------------------- /e2e/src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as ReactDOM from "react-dom"; 2 | import * as React from "react"; 3 | 4 | import { App } from "./App"; 5 | 6 | ReactDOM.render(, document.getElementById("root")); 7 | -------------------------------------------------------------------------------- /example/src/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 | 7 |
8 | 9 | 10 | -------------------------------------------------------------------------------- /src/objects/avatar/data/interfaces/IAvatarOffsetsData.ts: -------------------------------------------------------------------------------- 1 | export interface IAvatarOffsetsData { 2 | getOffsets( 3 | fileName: string 4 | ): { offsetX: number; offsetY: number } | undefined; 5 | } 6 | -------------------------------------------------------------------------------- /src/util/isSetEqual.ts: -------------------------------------------------------------------------------- 1 | export function isSetEqual(as: Set, bs: Set) { 2 | if (as.size !== bs.size) return false; 3 | for (const a of as) if (!bs.has(a)) return false; 4 | return true; 5 | } 6 | -------------------------------------------------------------------------------- /src/tools/dump/Logger.ts: -------------------------------------------------------------------------------- 1 | export interface Logger { 2 | info(...args: string[]): void; 3 | debug(...args: string[]): void; 4 | error(...args: string[]): void; 5 | log(...args: string[]): void; 6 | } 7 | -------------------------------------------------------------------------------- /src/util/applyTextureProperties.ts: -------------------------------------------------------------------------------- 1 | import * as PIXI from "pixi.js"; 2 | 3 | export function applyTextureProperties(texture: PIXI.Texture) { 4 | texture.baseTexture.scaleMode = PIXI.SCALE_MODES.NEAREST; 5 | } 6 | -------------------------------------------------------------------------------- /src/objects/avatar/structure/interface/IAvatarDrawablePart.ts: -------------------------------------------------------------------------------- 1 | import { AvatarDrawPart } from "../../types"; 2 | 3 | export interface IAvatarDrawablePart { 4 | getDrawDefinition(): AvatarDrawPart | undefined; 5 | } 6 | -------------------------------------------------------------------------------- /src/objects/furniture/data/FurnitureAssetsJson.ts: -------------------------------------------------------------------------------- 1 | import { FurnitureAsset } from "./interfaces/IFurnitureAssetsData"; 2 | 3 | export interface FurnitureAssetsJson { 4 | [key: string]: FurnitureAsset | undefined; 5 | } 6 | -------------------------------------------------------------------------------- /src/objects/events/interfaces/IEventTarget.ts: -------------------------------------------------------------------------------- 1 | import { IEventHandler } from "./IEventHandler"; 2 | import { IEventHittable } from "./IEventHittable"; 3 | 4 | export interface IEventTarget extends IEventHittable, IEventHandler {} 5 | -------------------------------------------------------------------------------- /src/objects/room/IRoomRectangle.ts: -------------------------------------------------------------------------------- 1 | export interface IRoomRectangle { 2 | rectangle: Rectangle; 3 | } 4 | 5 | export interface Rectangle { 6 | x: number; 7 | y: number; 8 | width: number; 9 | height: number; 10 | } 11 | -------------------------------------------------------------------------------- /src/interfaces/IRoomObjectContainer.ts: -------------------------------------------------------------------------------- 1 | import { IRoomObject } from "./IRoomObject"; 2 | 3 | export interface IRoomObjectContainer { 4 | addRoomObject(roomObject: IRoomObject): void; 5 | removeRoomObject(roomObject: IRoomObject): void; 6 | } 7 | -------------------------------------------------------------------------------- /src/interfaces/ITileMap.ts: -------------------------------------------------------------------------------- 1 | import { ParsedTileType } from "../util/parseTileMap"; 2 | 3 | export interface ITileMap { 4 | getTileAtPosition(roomX: number, roomY: number): ParsedTileType | undefined; 5 | getParsedTileTypes(): ParsedTileType[][]; 6 | } 7 | -------------------------------------------------------------------------------- /src/objects/avatar/data/interfaces/IAvatarPartSetsData.ts: -------------------------------------------------------------------------------- 1 | export interface IAvatarPartSetsData { 2 | getActivePartSet(id: string): Set; 3 | getPartInfo( 4 | id: string 5 | ): { removeSetType?: string; flippedSetType?: string } | undefined; 6 | } 7 | -------------------------------------------------------------------------------- /src/interfaces/IConfiguration.ts: -------------------------------------------------------------------------------- 1 | export interface IConfiguration { 2 | placeholder?: PIXI.Texture; 3 | tileColor?: { floorColor?: string; leftFade?: number; rightFade?: number }; 4 | avatarMovementDuration?: number; 5 | furnitureMovementDuration?: number; 6 | } 7 | -------------------------------------------------------------------------------- /src/util/associateBy.ts: -------------------------------------------------------------------------------- 1 | export function associateBy(arr: T[], getKey: (value: T) => string) { 2 | const map = new Map(); 3 | arr.forEach((value) => { 4 | const key = getKey(value); 5 | map.set(key, value); 6 | }); 7 | 8 | return map; 9 | } 10 | -------------------------------------------------------------------------------- /src/objects/room/util/getTilePosition.ts: -------------------------------------------------------------------------------- 1 | import * as PIXI from "pixi.js"; 2 | 3 | export function getTilePosition(roomX: number, roomY: number) { 4 | const xEven = roomX % 2 === 0; 5 | const yEven = roomY % 2 === 0; 6 | 7 | return new PIXI.Point(xEven ? 0 : 32, yEven ? 32 : 0); 8 | } 9 | -------------------------------------------------------------------------------- /src/util/getNumberFromAttribute.ts: -------------------------------------------------------------------------------- 1 | export function getNumberFromAttribute( 2 | value: string | null 3 | ): number | undefined { 4 | if (value == null) return; 5 | 6 | const numberValue = Number(value); 7 | if (isNaN(numberValue)) return; 8 | 9 | return numberValue; 10 | } 11 | -------------------------------------------------------------------------------- /src/util/traverseDOMTree.ts: -------------------------------------------------------------------------------- 1 | export function traverseDOMTree( 2 | node: Node, 3 | options: { enter: (node: Node) => void; exit: (node: Node) => void } 4 | ) { 5 | options.enter(node); 6 | node.childNodes.forEach((node) => traverseDOMTree(node, options)); 7 | options.exit(node); 8 | } 9 | -------------------------------------------------------------------------------- /src/objects/room/util/getMaskId.ts: -------------------------------------------------------------------------------- 1 | export function getMaskId(direction: number, roomX: number, roomY: number) { 2 | switch (direction) { 3 | case 2: 4 | case 6: 5 | return `x_${roomX}`; 6 | 7 | case 0: 8 | case 4: 9 | return `y_${roomY}`; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/objects/events/interfaces/IEventManager.ts: -------------------------------------------------------------------------------- 1 | import { IEventManagerNode } from "./IEventManagerNode"; 2 | import { IEventTarget } from "./IEventTarget"; 3 | 4 | export interface IEventManager { 5 | register(target: IEventTarget): IEventManagerNode; 6 | remove(target: IEventTarget): void; 7 | } 8 | -------------------------------------------------------------------------------- /src/objects/avatar/data/interfaces/IAvatarEffectMap.ts: -------------------------------------------------------------------------------- 1 | export interface IAvatarEffectMap { 2 | getEffectInfo(id: string): AvatarEffect | undefined; 3 | getEffects(): AvatarEffect[]; 4 | } 5 | 6 | export interface AvatarEffect { 7 | id: string; 8 | lib: string; 9 | type: string; 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /public/hof_furni 3 | /resources 4 | /public 5 | /dist 6 | /example/public/hof_furni 7 | /example/public/figure 8 | /example/public/resources 9 | /example/public/images 10 | /storybook/public/resources 11 | /storybook/public/images 12 | /e2e/public/resources 13 | yarn.lock 14 | /.idea 15 | -------------------------------------------------------------------------------- /e2e/src/TestRenderer.ts: -------------------------------------------------------------------------------- 1 | import * as PIXI from "pixi.js"; 2 | 3 | import { Shroom } from "@jankuss/shroom"; 4 | 5 | export type TestRendererCleanup = () => void; 6 | 7 | export type TestRenderer = (options: { 8 | shroom: Shroom; 9 | application: PIXI.Application; 10 | }) => TestRendererCleanup | undefined | void; 11 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: ["/src"], 3 | testMatch: [ 4 | "**/__tests__/**/*.+(ts|tsx|js)", 5 | "**/?(*.)+(spec|test).+(ts|tsx|js)" 6 | ], 7 | transform: { 8 | "^.+\\.(ts|tsx)$": "ts-jest" 9 | }, 10 | testEnvironmentOptions: { "resources": "usable" } 11 | }; -------------------------------------------------------------------------------- /src/objects/furniture/util/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | FurniDrawDefinition as DrawDefinition, 3 | FurniDrawPart as DrawPart, 4 | } from "./DrawDefinition"; 5 | export * from "./visualization/parseVisualization"; 6 | 7 | export function getCharFromLayerIndex(index: number) { 8 | return String.fromCharCode(97 + index); 9 | } 10 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /src/objects/avatar/data/interfaces/IManifestLibrary.ts: -------------------------------------------------------------------------------- 1 | import { HitTexture } from "../../../hitdetection/HitTexture"; 2 | import { IAvatarManifestData } from "./IAvatarManifestData"; 3 | 4 | export interface IManifestLibrary { 5 | getManifest(): Promise; 6 | getTexture(name: string): Promise; 7 | } 8 | -------------------------------------------------------------------------------- /e2e/src/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | 15 | 16 |
17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /src/util/loadImageFromBlob.ts: -------------------------------------------------------------------------------- 1 | export async function loadImageFromBlob(blob: Blob) { 2 | const reader = new FileReader(); 3 | const url = await new Promise((resolve) => { 4 | reader.readAsDataURL(blob); 5 | reader.onloadend = () => { 6 | resolve(reader.result as string); 7 | }; 8 | }); 9 | 10 | return url; 11 | } 12 | -------------------------------------------------------------------------------- /storybook/.storybook/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | typescript: { 3 | check: false, 4 | checkOptions: {}, 5 | }, 6 | "stories": [ 7 | "../stories/**/*.stories.mdx", 8 | "../stories/**/*.stories.@(js|jsx|ts|tsx)" 9 | ], 10 | "addons": [ 11 | "@storybook/addon-links", 12 | "@storybook/addon-essentials" 13 | ] 14 | } -------------------------------------------------------------------------------- /src/objects/furniture/data/interfaces/IFurnitureAssetsData.ts: -------------------------------------------------------------------------------- 1 | export interface IFurnitureAssetsData { 2 | getAsset(name: string): FurnitureAsset | undefined; 3 | getAssets(): FurnitureAsset[]; 4 | } 5 | 6 | export interface FurnitureAsset { 7 | name: string; 8 | x: number; 9 | y: number; 10 | flipH: boolean; 11 | source?: string; 12 | valid: boolean; 13 | } 14 | -------------------------------------------------------------------------------- /docs/sidebars.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | someSidebar: { 3 | "Getting Started": ['install'], 4 | "Entry Guide": ["guides/create-room", "guides/applying-room-textures", "guides/adding-objects", "guides/adding-windows", "guides/avatar-movement","guides/avatar-actions"], 5 | "Advanced Usage": [ "guides/implementing-furniture-logic", "guides/use-base-avatar-and-furniture"] 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /src/objects/avatar/structure/interface/IAvatarEffectPart.ts: -------------------------------------------------------------------------------- 1 | import { IAvatarEffectData } from "../../data/interfaces/IAvatarEffectData"; 2 | 3 | export interface IAvatarEffectPart { 4 | setDirection(direction: number): void; 5 | setDirectionOffset(offset: number): void; 6 | setEffectFrame(effect: IAvatarEffectData, frame: number): void; 7 | setEffectFrameDefaultIfNotSet(): void; 8 | } 9 | -------------------------------------------------------------------------------- /e2e/src/tests/room/models/util/renderModel.ts: -------------------------------------------------------------------------------- 1 | import { Room } from "@jankuss/shroom"; 2 | import { TestRenderer } from "../../../../TestRenderer"; 3 | 4 | export function renderModel(tilemap: string): TestRenderer { 5 | return ({ shroom, application }) => { 6 | const room = Room.create(shroom, { 7 | tilemap: tilemap, 8 | }); 9 | 10 | application.stage.addChild(room); 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /src/objects/avatar/util/getAvatarDirection.ts: -------------------------------------------------------------------------------- 1 | export function getAvatarDirection(direction: number): number { 2 | if (direction < -8) { 3 | return 0; 4 | } 5 | 6 | if (direction > 15) { 7 | return 0; 8 | } 9 | 10 | if (direction < 0) { 11 | return direction + 8; 12 | } 13 | 14 | if (direction > 7) { 15 | return direction - 8; 16 | } 17 | 18 | return direction; 19 | } 20 | -------------------------------------------------------------------------------- /src/objects/events/interfaces/IEventGroup.ts: -------------------------------------------------------------------------------- 1 | export interface IEventGroup { 2 | getEventGroupIdentifier(): EventGroupIdentifier; 3 | } 4 | 5 | export type EventGroupIdentifier = 6 | | typeof FURNITURE 7 | | typeof AVATAR 8 | | typeof TILE_CURSOR; 9 | 10 | export const FURNITURE = Symbol("FURNITURE"); 11 | export const AVATAR = Symbol("AVATAR"); 12 | export const TILE_CURSOR = Symbol("TILE_CURSOR"); 13 | -------------------------------------------------------------------------------- /src/objects/events/interfaces/IEventHittable.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from "rxjs"; 2 | import { Rectangle } from "../../room/IRoomRectangle"; 3 | import { IEventGroup } from "./IEventGroup"; 4 | 5 | export interface IEventHittable { 6 | getGroup(): IEventGroup; 7 | getRectangleObservable(): Observable; 8 | getEventZOrder(): number; 9 | hits(x: number, y: number): void; 10 | } 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Shroom - Room Rendering Engine for Retros 2 | 3 | Shroom is a room rendering engine for retros. 4 | 5 | ## Documentation 6 | 7 | The documentation can be found [here](https://jankuss.github.io/shroom/docs/). 8 | 9 | ## Official Discord Server 10 | 11 | If you need support or you just want to talk about `shroom`, feel free to join us on our [Official Discord Server](https://discord.gg/PjeS9JHeaE). 12 | -------------------------------------------------------------------------------- /e2e/src/tests/room/models/renderModelC.ts: -------------------------------------------------------------------------------- 1 | import { renderModel } from "./util/renderModel"; 2 | 3 | export const renderModelC = renderModel(`xxxxxxxxxxxx 4 | xxxxxxxxxxxx 5 | xxxxxxxxxxxx 6 | xxxxxxxxxxxx 7 | xxxxxxxxxxxx 8 | xxxxx000000x 9 | xxxxx000000x 10 | xxxx0000000x 11 | xxxxx000000x 12 | xxxxx000000x 13 | xxxxx000000x 14 | xxxxxxxxxxxx 15 | xxxxxxxxxxxx 16 | xxxxxxxxxxxx 17 | xxxxxxxxxxxx 18 | xxxxxxxxxxxx`); 19 | -------------------------------------------------------------------------------- /e2e/src/tests/room/models/renderModelD.ts: -------------------------------------------------------------------------------- 1 | import { renderModel } from "./util/renderModel"; 2 | 3 | export const renderModelD = renderModel(`xxxxxxxxxxxx 4 | xxxxx000000x 5 | xxxxx000000x 6 | xxxxx000000x 7 | xxxxx000000x 8 | xxxxx000000x 9 | xxxxx000000x 10 | xxxx0000000x 11 | xxxxx000000x 12 | xxxxx000000x 13 | xxxxx000000x 14 | xxxxx000000x 15 | xxxxx000000x 16 | xxxxx000000x 17 | xxxxx000000x 18 | xxxxxxxxxxxx`); 19 | -------------------------------------------------------------------------------- /e2e/src/tests/room/models/renderModelE.ts: -------------------------------------------------------------------------------- 1 | import { renderModel } from "./util/renderModel"; 2 | 3 | export const renderModelE = renderModel(`xxxxxxxxxxxx 4 | xxxxxxxxxxxx 5 | xxxxxxxxxxxx 6 | xx0000000000 7 | xx0000000000 8 | x00000000000 9 | xx0000000000 10 | xx0000000000 11 | xx0000000000 12 | xx0000000000 13 | xx0000000000 14 | xxxxxxxxxxxx 15 | xxxxxxxxxxxx 16 | xxxxxxxxxxxx 17 | xxxxxxxxxxxx 18 | xxxxxxxxxxxx`); 19 | -------------------------------------------------------------------------------- /e2e/src/tests/room/models/renderModelF.ts: -------------------------------------------------------------------------------- 1 | import { renderModel } from "./util/renderModel"; 2 | 3 | export const renderModelF = renderModel(`xxxxxxxxxxxx 4 | xxxxxxx0000x 5 | xxxxxxx0000x 6 | xxx00000000x 7 | xxx00000000x 8 | xx000000000x 9 | xxx00000000x 10 | x0000000000x 11 | x0000000000x 12 | x0000000000x 13 | x0000000000x 14 | xxxxxxxxxxxx 15 | xxxxxxxxxxxx 16 | xxxxxxxxxxxx 17 | xxxxxxxxxxxx 18 | xxxxxxxxxxxx`); 19 | -------------------------------------------------------------------------------- /e2e/src/tests/room/models/renderModelH.ts: -------------------------------------------------------------------------------- 1 | import { renderModel } from "./util/renderModel"; 2 | 3 | export const renderModelH = renderModel(`xxxxxxxxxxxx 4 | xxxxxxxxxxxx 5 | xxxxx111111x 6 | xxxxx111111x 7 | xxxx1111111x 8 | xxxxx111111x 9 | xxxxx111111x 10 | xxxxx000000x 11 | xxxxx000000x 12 | xxx00000000x 13 | xxx00000000x 14 | xxx00000000x 15 | xxx00000000x 16 | xxxxxxxxxxxx 17 | xxxxxxxxxxxx 18 | xxxxxxxxxxxx`); 19 | -------------------------------------------------------------------------------- /src/interfaces/IFurnitureLoader.ts: -------------------------------------------------------------------------------- 1 | import { LoadFurniResult } from "../objects/furniture/util/loadFurni"; 2 | import { FurnitureId } from "./IFurnitureData"; 3 | 4 | export interface IFurnitureLoader { 5 | loadFurni(type: FurnitureFetch): Promise; 6 | } 7 | 8 | export type FurnitureFetch = 9 | | { kind: "id"; id: FurnitureId; placementType: "wall" | "floor" } 10 | | { kind: "type"; type: string }; 11 | -------------------------------------------------------------------------------- /src/objects/avatar/data/interfaces/IFigureData.ts: -------------------------------------------------------------------------------- 1 | export interface IFigureData { 2 | getColor(setType: string, colorId: string): string | undefined; 3 | getParts(setType: string, id: string): FigureDataPart[] | undefined; 4 | getHiddenLayers(setType: string, id: string): string[]; 5 | } 6 | 7 | export type FigureDataPart = { 8 | id: string; 9 | colorable: boolean; 10 | type: string; 11 | index: number; 12 | }; 13 | -------------------------------------------------------------------------------- /e2e/src/tests/room/models/renderModelA.ts: -------------------------------------------------------------------------------- 1 | import { renderModel } from "./util/renderModel"; 2 | 3 | export const renderModelA = renderModel(` 4 | xxxxxxxxxxxx 5 | xxxx00000000 6 | xxxx00000000 7 | xxxx00000000 8 | xxxx00000000 9 | xxx000000000 10 | xxxx00000000 11 | xxxx00000000 12 | xxxx00000000 13 | xxxx00000000 14 | xxxx00000000 15 | xxxx00000000 16 | xxxx00000000 17 | xxxx00000000 18 | xxxxxxxxxxxx 19 | xxxxxxxxxxxx 20 | `); 21 | -------------------------------------------------------------------------------- /e2e/src/tests/room/models/renderModelB.ts: -------------------------------------------------------------------------------- 1 | import { renderModel } from "./util/renderModel"; 2 | 3 | export const renderModelB = renderModel(` 4 | xxxxxxxxxxxx 5 | xxxxx0000000 6 | xxxxx0000000 7 | xxxxx0000000 8 | xxxxx0000000 9 | 000000000000 10 | x00000000000 11 | x00000000000 12 | x00000000000 13 | x00000000000 14 | x00000000000 15 | xxxxxxxxxxxx 16 | xxxxxxxxxxxx 17 | xxxxxxxxxxxx 18 | xxxxxxxxxxxx 19 | xxxxxxxxxxxx 20 | `); 21 | -------------------------------------------------------------------------------- /e2e/src/tests/room/models/renderModelG.ts: -------------------------------------------------------------------------------- 1 | import { renderModel } from "./util/renderModel"; 2 | 3 | export const renderModelG = renderModel(`xxxxxxxxxxxx 4 | xxxxxxxxxxxx 5 | xxxxxxx00000 6 | xxxxxxx00000 7 | xxxxxxx00000 8 | xx1111000000 9 | xx1111000000 10 | x11111000000 11 | xx1111000000 12 | xx1111000000 13 | xxxxxxx00000 14 | xxxxxxx00000 15 | xxxxxxx00000 16 | xxxxxxxxxxxx 17 | xxxxxxxxxxxx 18 | xxxxxxxxxxxx 19 | xxxxxxxxxxxx`); 20 | -------------------------------------------------------------------------------- /src/objects/furniture/data/FurnitureJson.ts: -------------------------------------------------------------------------------- 1 | import { FurnitureAssetsJson } from "./FurnitureAssetsJson"; 2 | import { FurnitureIndexJson } from "./FurnitureIndexJson"; 3 | import { FurnitureVisualizationJson } from "./FurnitureVisualizationJson"; 4 | 5 | export interface FurnitureJson { 6 | visualization: FurnitureVisualizationJson; 7 | assets: FurnitureAssetsJson; 8 | index: FurnitureIndexJson; 9 | spritesheet: any; 10 | } 11 | -------------------------------------------------------------------------------- /src/objects/room/parts/RoomPartData.ts: -------------------------------------------------------------------------------- 1 | export interface RoomPartData { 2 | wallHeight: number; 3 | borderWidth: number; 4 | tileHeight: number; 5 | wallLeftColor: number; 6 | wallRightColor: number; 7 | wallTopColor: number; 8 | wallTexture: PIXI.Texture; 9 | tileLeftColor: number; 10 | tileRightColor: number; 11 | tileTopColor: number; 12 | tileTexture: PIXI.Texture; 13 | masks: Map; 14 | } 15 | -------------------------------------------------------------------------------- /src/data/XmlData.ts: -------------------------------------------------------------------------------- 1 | export class XmlData { 2 | protected document: Document; 3 | 4 | constructor(xml: string) { 5 | this.document = new DOMParser().parseFromString(xml, "text/xml"); 6 | } 7 | 8 | protected querySelectorAll(query: string) { 9 | return Array.from(this.document.querySelectorAll(query)); 10 | } 11 | 12 | protected querySelector(query: string) { 13 | return this.document.querySelector(query); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/objects/avatar/data/AvatarData.ts: -------------------------------------------------------------------------------- 1 | export class AvatarData { 2 | protected document: Document; 3 | 4 | constructor(xml: string) { 5 | this.document = new DOMParser().parseFromString(xml, "text/xml"); 6 | } 7 | 8 | protected querySelectorAll(query: string) { 9 | return Array.from(this.document.querySelectorAll(query)); 10 | } 11 | 12 | protected querySelector(query: string) { 13 | return this.document.querySelector(query); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/util/loadImageFromUrl.ts: -------------------------------------------------------------------------------- 1 | export async function loadImageFromUrl(imageUrl: string) { 2 | const image = new Image(); 3 | 4 | image.src = imageUrl; 5 | 6 | await new Promise<{ 7 | width: number; 8 | height: number; 9 | }>((resolve, reject) => { 10 | image.onload = () => { 11 | resolve({ width: image.width, height: image.height }); 12 | }; 13 | 14 | image.onerror = (value) => reject(value); 15 | }); 16 | 17 | return image; 18 | } 19 | -------------------------------------------------------------------------------- /src/util/tilemap/getColumnWalls.test.ts: -------------------------------------------------------------------------------- 1 | import { parseTileMapString } from "../parseTileMapString"; 2 | import { getColumnWalls } from "./getColumnWalls"; 3 | 4 | test("parses single wall", () => { 5 | const tilemap = parseTileMapString(` 6 | xxx 7 | x00 8 | x00 9 | `); 10 | 11 | expect(getColumnWalls(tilemap)).toEqual([ 12 | { 13 | startX: 1, 14 | endX: 2, 15 | y: 0, 16 | height: 0, 17 | }, 18 | ]); 19 | }); 20 | -------------------------------------------------------------------------------- /src/objects/room/util/getPosition.ts: -------------------------------------------------------------------------------- 1 | export function getPosition( 2 | roomX: number, 3 | roomY: number, 4 | roomZ: number, 5 | wallOffsets: { x: number; y: number } 6 | ) { 7 | roomX = roomX + wallOffsets.x; 8 | roomY = roomY + wallOffsets.y; 9 | 10 | const base = 32; 11 | 12 | const xPos = roomX * base - roomY * base; 13 | const yPos = roomX * (base / 2) + roomY * (base / 2); 14 | 15 | return { 16 | x: xPos, 17 | y: yPos - roomZ * 32, 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /src/objects/room/util/getTilePositionForTile.ts: -------------------------------------------------------------------------------- 1 | import { getTilePosition } from "./getTilePosition"; 2 | 3 | export function getTilePositionForTile(roomX: number, roomY: number) { 4 | return { 5 | top: getTilePosition(roomX, roomY), 6 | left: getTilePosition(roomX, roomY + 1), 7 | right: getTilePosition(roomX + 1, roomY), 8 | }; 9 | } 10 | 11 | export interface TilePositionForTile { 12 | left: PIXI.Point; 13 | right: PIXI.Point; 14 | top: PIXI.Point; 15 | } 16 | -------------------------------------------------------------------------------- /src/objects/room/parts/WallRight.ts: -------------------------------------------------------------------------------- 1 | import { WallLeft, WallProps } from "./WallLeft"; 2 | 3 | export class WallRight extends WallLeft { 4 | constructor(props: WallProps) { 5 | super(props); 6 | } 7 | 8 | _update() { 9 | this._offsets = { x: this._wallWidth, y: 0 }; 10 | this.scale.x = -1; 11 | 12 | const left = this._wallLeftColor; 13 | this._wallLeftColor = this._wallRightColor; 14 | this._wallRightColor = left; 15 | 16 | super._update(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/objects/avatar/data/interfaces/IAvatarAnimationData.ts: -------------------------------------------------------------------------------- 1 | export interface IAvatarAnimationData { 2 | getAnimationFrames(id: string, type: string): AvatarAnimationFrame[]; 3 | getAnimationFramesCount(id: string): number; 4 | getAnimationFrame( 5 | id: string, 6 | type: string, 7 | frame: number 8 | ): AvatarAnimationFrame | undefined; 9 | } 10 | 11 | export type AvatarAnimationFrame = { 12 | number: number; 13 | assetpartdefinition: string; 14 | repeats: number; 15 | }; 16 | -------------------------------------------------------------------------------- /src/objects/avatar/util/parseLookString.ts: -------------------------------------------------------------------------------- 1 | export type ParsedLook = Map; 2 | 3 | export function parseLookString(look: string): ParsedLook { 4 | return new Map( 5 | look.split(".").map((str) => { 6 | const partData = str.split("-"); 7 | 8 | return [ 9 | partData[0], 10 | { 11 | setId: Number(partData[1]), 12 | colorId: Number(partData[2]), 13 | }, 14 | ] as const; 15 | }) 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/objects/events/interfaces/IEventHandler.ts: -------------------------------------------------------------------------------- 1 | import { IEventManagerEvent } from "./IEventManagerEvent"; 2 | 3 | export interface IEventHandler { 4 | triggerClick(event: IEventManagerEvent): void; 5 | triggerPointerDown(event: IEventManagerEvent): void; 6 | triggerPointerUp(event: IEventManagerEvent): void; 7 | triggerPointerOver(event: IEventManagerEvent): void; 8 | triggerPointerOut(event: IEventManagerEvent): void; 9 | triggerPointerTargetChanged(event: IEventManagerEvent): void; 10 | } 11 | -------------------------------------------------------------------------------- /src/types/TileType.ts: -------------------------------------------------------------------------------- 1 | export type TileType = TileTypeNumber | "x"; 2 | 3 | export type TileTypeNumber = 4 | | "0" 5 | | "1" 6 | | "2" 7 | | "3" 8 | | "4" 9 | | "5" 10 | | "6" 11 | | "7" 12 | | "8" 13 | | "9" 14 | | "a" 15 | | "b" 16 | | "c" 17 | | "d" 18 | | "e" 19 | | "f" 20 | | "g" 21 | | "h" 22 | | "i" 23 | | "j" 24 | | "k" 25 | | "l" 26 | | "m" 27 | | "n" 28 | | "o" 29 | | "p" 30 | | "q" 31 | | "r" 32 | | "s" 33 | | "t" 34 | | "u" 35 | | "v"; 36 | -------------------------------------------------------------------------------- /src/objects/avatar/data/interfaces/IAvatarManifestData.ts: -------------------------------------------------------------------------------- 1 | export interface IAvatarManifestData { 2 | getAssets(): ManifestAsset[]; 3 | getAliases(): ManifestAlias[]; 4 | getAssetByName(name: string): ManifestAsset | undefined; 5 | } 6 | 7 | export interface ManifestAsset { 8 | name: string; 9 | x: number; 10 | y: number; 11 | flipH: boolean; 12 | flipV: boolean; 13 | } 14 | 15 | export interface ManifestAlias { 16 | name: string; 17 | link: string; 18 | fliph: boolean; 19 | flipv: boolean; 20 | } 21 | -------------------------------------------------------------------------------- /src/objects/avatar/data/interfaces/IAvatarEffectBundle.ts: -------------------------------------------------------------------------------- 1 | import { HitTexture } from "../../../hitdetection/HitTexture"; 2 | import { IAvatarEffectData } from "./IAvatarEffectData"; 3 | import { IAvatarManifestData } from "./IAvatarManifestData"; 4 | import { IManifestLibrary } from "./IManifestLibrary"; 5 | 6 | export interface IAvatarEffectBundle extends IManifestLibrary { 7 | getData(): Promise; 8 | getTexture(name: string): Promise; 9 | getManifest(): Promise; 10 | } 11 | -------------------------------------------------------------------------------- /src/tools/dump/parseExternalVariables.ts: -------------------------------------------------------------------------------- 1 | export function parseExternalVariables(externalVars: string) { 2 | const lines = externalVars.split("\n"); 3 | const map = new Map( 4 | lines.map(line => line.split("=")).map(item => [item[0], item[1]]) 5 | ); 6 | 7 | map.forEach((replaceValue, key) => { 8 | map.forEach((value, okey) => { 9 | if (value) { 10 | map.set(okey, value.replace("${" + key + "}", replaceValue)); 11 | } 12 | }); 13 | }); 14 | 15 | return map; 16 | } 17 | -------------------------------------------------------------------------------- /src/objects/furniture/util/IFurnitureEventHandlers.ts: -------------------------------------------------------------------------------- 1 | import { IEventManagerEvent } from "../../events/interfaces/IEventManagerEvent"; 2 | 3 | export interface IFurnitureEventHandlers { 4 | onClick?: (event: IEventManagerEvent) => void; 5 | onDoubleClick?: (event: IEventManagerEvent) => void; 6 | onPointerDown?: (event: IEventManagerEvent) => void; 7 | onPointerUp?: (event: IEventManagerEvent) => void; 8 | onPointerOver?: (event: IEventManagerEvent) => void; 9 | onPointerOut?: (event: IEventManagerEvent) => void; 10 | } 11 | -------------------------------------------------------------------------------- /src/objects/furniture/IFurnitureVisualization.ts: -------------------------------------------------------------------------------- 1 | import { BaseFurniture } from "./BaseFurniture"; 2 | import { IFurnitureVisualizationView } from "./IFurnitureVisualizationView"; 3 | 4 | export interface IFurnitureVisualization { 5 | isAnimated(animation?: string): boolean; 6 | setView(view: IFurnitureVisualizationView): void; 7 | updateFrame(frame: number): void; 8 | updateDirection(direction: number): void; 9 | updateAnimation(animation: string | undefined): void; 10 | update(furniture: BaseFurniture): void; 11 | destroy(): void; 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2019", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "allowSyntheticDefaultImports": true, 8 | "strict": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "module": "commonjs", 11 | "moduleResolution": "node", 12 | "resolveJsonModule": true, 13 | "jsx": "react", 14 | "esModuleInterop": true, 15 | "outDir": "./dist", 16 | "declaration": true, 17 | }, 18 | "include": ["./src"] 19 | } 20 | -------------------------------------------------------------------------------- /e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2019", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "allowSyntheticDefaultImports": true, 8 | "strict": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "module": "commonjs", 11 | "moduleResolution": "node", 12 | "resolveJsonModule": true, 13 | "jsx": "react", 14 | "esModuleInterop": true, 15 | "outDir": "./dist", 16 | "declaration": true, 17 | }, 18 | "include": ["./src"] 19 | } 20 | -------------------------------------------------------------------------------- /ci/discord/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2019", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "allowSyntheticDefaultImports": true, 8 | "strict": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "module": "commonjs", 11 | "moduleResolution": "node", 12 | "resolveJsonModule": true, 13 | "jsx": "react", 14 | "esModuleInterop": true, 15 | "outDir": "./dist", 16 | "declaration": true, 17 | }, 18 | "include": ["./src"] 19 | } 20 | -------------------------------------------------------------------------------- /example/src/behaviors/FurniInfoBehavior.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IFurnitureBehavior, 3 | IFurniture, 4 | IFurnitureData, 5 | } from "@jankuss/shroom"; 6 | 7 | export class FurniInfoBehavior implements IFurnitureBehavior { 8 | private parent: IFurniture | undefined; 9 | 10 | constructor(private furnitureData: IFurnitureData) {} 11 | 12 | setParent(furniture: IFurniture): void { 13 | this.parent = furniture; 14 | this.parent.onClick = async (e) => { 15 | const info = await this.furnitureData.getInfoForFurniture(furniture); 16 | }; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2019", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "allowSyntheticDefaultImports": true, 8 | "strict": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "module": "commonjs", 11 | "moduleResolution": "node", 12 | "resolveJsonModule": true, 13 | "jsx": "react", 14 | "esModuleInterop": true, 15 | "outDir": "./dist", 16 | "declaration": true, 17 | }, 18 | "include": ["./src"] 19 | } 20 | -------------------------------------------------------------------------------- /src/util/loadRoomTexture.ts: -------------------------------------------------------------------------------- 1 | import * as PIXI from "pixi.js"; 2 | import { applyTextureProperties } from "./applyTextureProperties"; 3 | 4 | export async function loadRoomTexture(url: string): Promise { 5 | const image = new Image(); 6 | 7 | image.crossOrigin = "anonymous"; 8 | image.src = url; 9 | 10 | await new Promise((resolve) => { 11 | image.onload = () => { 12 | resolve(); 13 | }; 14 | }); 15 | 16 | const texture = PIXI.Texture.from(image); 17 | applyTextureProperties(texture); 18 | 19 | return texture; 20 | } 21 | -------------------------------------------------------------------------------- /src/util/parseTileMapString.ts: -------------------------------------------------------------------------------- 1 | import { TileType } from "../types/TileType"; 2 | 3 | function toTileType(str: string) { 4 | return str as TileType; 5 | } 6 | 7 | export function parseTileMapString(str: string): TileType[][] { 8 | // Thanks @Fusion for this code to sanitize the tilemap string into a readable format. 9 | str = str.replace(/\r/g, "\n"); 10 | str = str.replace(/ /g, ""); 11 | 12 | return str 13 | .split("\n") 14 | .map((row) => row.trim()) 15 | .filter((row) => row.length > 0) 16 | .map((row) => row.split("").map(toTileType)); 17 | } 18 | -------------------------------------------------------------------------------- /ci/discord/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shroom-discord-messenger", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "ts-node src/index.ts" 9 | }, 10 | "author": "", 11 | "dependencies": { 12 | "keep-a-changelog": "^0.10.2", 13 | "node-fetch": "^2.6.1", 14 | "ts-node": "^9.1.1", 15 | "typescript": "^4.1.3" 16 | }, 17 | "devDependencies": { 18 | "@types/node": "^14.14.13", 19 | "@types/node-fetch": "^2.5.7" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/objects/avatar/data/AvatarOffsetsData.ts: -------------------------------------------------------------------------------- 1 | import { IAvatarOffsetsData } from "./interfaces/IAvatarOffsetsData"; 2 | 3 | export class AvatarOffsetsData implements IAvatarOffsetsData { 4 | constructor(private _json: any) {} 5 | 6 | static async fromUrl(url: string) { 7 | const response = await fetch(url); 8 | const json = await response.json(); 9 | 10 | return new AvatarOffsetsData(json); 11 | } 12 | 13 | getOffsets( 14 | fileName: string 15 | ): { offsetX: number; offsetY: number } | undefined { 16 | return this._json[fileName]; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/util/isPointInside.ts: -------------------------------------------------------------------------------- 1 | export function isPointInside( 2 | point: [number, number], 3 | vs: [number, number][] 4 | ): boolean { 5 | const x = point[0]; 6 | const y = point[1]; 7 | 8 | let inside = false; 9 | for (let i = 0, j = vs.length - 1; i < vs.length; j = i++) { 10 | const xi = vs[i][0]; 11 | const yi = vs[i][1]; 12 | const xj = vs[j][0]; 13 | const yj = vs[j][1]; 14 | 15 | const intersect = 16 | yi > y != yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi; 17 | if (intersect) inside = !inside; 18 | } 19 | 20 | return inside; 21 | } 22 | -------------------------------------------------------------------------------- /src/interfaces/IAvatarLoader.ts: -------------------------------------------------------------------------------- 1 | import { AvatarDrawDefinition } from "../objects/avatar/structure/AvatarDrawDefinition"; 2 | import { LookOptions } from "../objects/avatar/util/createLookServer"; 3 | import { HitTexture } from "../objects/hitdetection/HitTexture"; 4 | 5 | export interface IAvatarLoader { 6 | getAvatarDrawDefinition( 7 | options: LookOptions & { initial?: boolean } 8 | ): Promise; 9 | } 10 | 11 | export type AvatarLoaderResult = { 12 | getTexture(id: string): HitTexture; 13 | getDrawDefinition(options: LookOptions): AvatarDrawDefinition; 14 | }; 15 | -------------------------------------------------------------------------------- /src/objects/events/interfaces/IEventManagerEvent.ts: -------------------------------------------------------------------------------- 1 | import { InteractionEvent } from "pixi.js"; 2 | import { EventGroupIdentifier } from "./IEventGroup"; 3 | 4 | export interface IEventManagerEvent { 5 | tag?: string; 6 | mouseEvent: MouseEvent | TouchEvent | PointerEvent; 7 | interactionEvent: InteractionEvent; 8 | stopPropagation(): void; 9 | skip(...identifiers: EventGroupIdentifierParam[]): void; 10 | skipExcept(...identifiers: EventGroupIdentifierParam[]): void; 11 | } 12 | 13 | export type EventGroupIdentifierParam = 14 | | EventGroupIdentifierParam[] 15 | | EventGroupIdentifier; 16 | -------------------------------------------------------------------------------- /src/objects/room/ParsedTileMap.ts: -------------------------------------------------------------------------------- 1 | import { TileType } from "../../types/TileType"; 2 | import { parseTileMap } from "../../util/parseTileMap"; 3 | 4 | export class ParsedTileMap { 5 | private _data: ReturnType; 6 | 7 | public get largestDiff() { 8 | return this._data.largestDiff; 9 | } 10 | 11 | public get parsedTileTypes() { 12 | return this._data.tilemap; 13 | } 14 | 15 | public get wallOffsets() { 16 | return this._data.wallOffsets; 17 | } 18 | 19 | constructor(private tilemap: TileType[][]) { 20 | this._data = parseTileMap(tilemap); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /e2e/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | babelrcRoots: ["."], 3 | presets: [ 4 | "@babel/preset-typescript", 5 | "@babel/preset-react", 6 | [ 7 | "@babel/preset-env", 8 | { 9 | modules: false, 10 | targets: { 11 | chrome: "72", 12 | }, 13 | }, 14 | ], 15 | ], 16 | plugins: [ 17 | "@babel/plugin-proposal-optional-chaining", 18 | "@babel/plugin-proposal-nullish-coalescing-operator", 19 | "@babel/plugin-proposal-numeric-separator", 20 | "@babel/plugin-proposal-class-properties" 21 | ], 22 | }; -------------------------------------------------------------------------------- /example/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | babelrcRoots: ["."], 3 | presets: [ 4 | "@babel/preset-typescript", 5 | "@babel/preset-react", 6 | [ 7 | "@babel/preset-env", 8 | { 9 | modules: false, 10 | targets: { 11 | chrome: "72", 12 | }, 13 | }, 14 | ], 15 | ], 16 | plugins: [ 17 | "@babel/plugin-proposal-optional-chaining", 18 | "@babel/plugin-proposal-nullish-coalescing-operator", 19 | "@babel/plugin-proposal-numeric-separator", 20 | "@babel/plugin-proposal-class-properties" 21 | ], 22 | }; -------------------------------------------------------------------------------- /src/objects/furniture/IFurnitureAssetBundle.ts: -------------------------------------------------------------------------------- 1 | import { HitTexture } from "../hitdetection/HitTexture"; 2 | import { IFurnitureAssetsData } from "./data/interfaces/IFurnitureAssetsData"; 3 | import { IFurnitureIndexData } from "./data/interfaces/IFurnitureIndexData"; 4 | import { IFurnitureVisualizationData } from "./data/interfaces/IFurnitureVisualizationData"; 5 | 6 | export interface IFurnitureAssetBundle { 7 | getAssets(): Promise; 8 | getVisualization(): Promise; 9 | getTexture(name: string): Promise; 10 | getIndex(): Promise; 11 | } 12 | -------------------------------------------------------------------------------- /src/objects/furniture/util/getDirectionForFurniture.ts: -------------------------------------------------------------------------------- 1 | export function getDirectionForFurniture( 2 | direction: number, 3 | validDirections: number[] 4 | ) { 5 | if (validDirections.length < 1) return 0; 6 | 7 | let fallbackDirection = validDirections[0]; 8 | for (let i = 0; i < validDirections.length; i++) { 9 | const validDirection = validDirections[i]; 10 | if (validDirection === direction) return direction; 11 | 12 | if (validDirection > direction) { 13 | return fallbackDirection; 14 | } 15 | 16 | fallbackDirection = validDirection; 17 | } 18 | 19 | return fallbackDirection; 20 | } 21 | -------------------------------------------------------------------------------- /src/objects/furniture/util/DrawDefinition.ts: -------------------------------------------------------------------------------- 1 | import { FurnitureAsset } from "../data/interfaces/IFurnitureAssetsData"; 2 | import { FurnitureLayer } from "../data/interfaces/IFurnitureVisualizationData"; 3 | 4 | export type BaseFurniDrawPart = { 5 | layerIndex: number; 6 | z?: number; 7 | shadow: boolean; 8 | frameRepeat: number; 9 | layer: FurnitureLayer | undefined; 10 | tint?: string; 11 | mask?: boolean; 12 | loopCount?: number; 13 | }; 14 | 15 | export type FurniDrawPart = { 16 | assets: FurnitureAsset[]; 17 | } & BaseFurniDrawPart; 18 | 19 | export interface FurniDrawDefinition { 20 | parts: FurniDrawPart[]; 21 | } 22 | -------------------------------------------------------------------------------- /src/objects/avatar/data/interfaces/IAvatarActionsData.ts: -------------------------------------------------------------------------------- 1 | import { AvatarAction } from "../../enum/AvatarAction"; 2 | 3 | export interface IAvatarActionsData { 4 | getAction(id: AvatarAction): AvatarActionInfo | undefined; 5 | getActions(): AvatarActionInfo[]; 6 | getHandItemId(actionId: string, id: string): number | undefined; 7 | } 8 | 9 | export interface AvatarActionInfo { 10 | id: string; 11 | state: string; 12 | precedence: number; 13 | geometrytype: string; 14 | activepartset: string | null; 15 | assetpartdefinition: string; 16 | prevents: string[]; 17 | animation: boolean; 18 | main: boolean; 19 | isdefault: boolean; 20 | } 21 | -------------------------------------------------------------------------------- /e2e/src/CaretDown.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export function CaretDown() { 4 | return ( 5 | 12 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/objects/furniture/data/JsonFurnitureAssetsData.ts: -------------------------------------------------------------------------------- 1 | import { notNullOrUndefined } from "../../../util/notNullOrUndefined"; 2 | import { FurnitureAssetsJson } from "./FurnitureAssetsJson"; 3 | import { 4 | FurnitureAsset, 5 | IFurnitureAssetsData, 6 | } from "./interfaces/IFurnitureAssetsData"; 7 | 8 | export class JsonFurnitureAssetsData implements IFurnitureAssetsData { 9 | constructor(private _assets: FurnitureAssetsJson) {} 10 | 11 | getAsset(name: string): FurnitureAsset | undefined { 12 | return this._assets[name]; 13 | } 14 | 15 | getAssets(): FurnitureAsset[] { 16 | return Object.values(this._assets).filter(notNullOrUndefined); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/objects/avatar/util/tests/getAvatarDirection.test.ts: -------------------------------------------------------------------------------- 1 | import { getAvatarDirection } from "../getAvatarDirection"; 2 | 3 | test.each([ 4 | [0, 0], 5 | [1, 1], 6 | [2, 2], 7 | [3, 3], 8 | [4, 4], 9 | [5, 5], 10 | [6, 6], 11 | [7, 7], 12 | [8, 0], 13 | [9, 1], 14 | [10, 2], 15 | [11, 3], 16 | [12, 4], 17 | [13, 5], 18 | [14, 6], 19 | [15, 7], 20 | [16, 0], 21 | [17, 0], 22 | [-1, 7], 23 | [-2, 6], 24 | [-3, 5], 25 | [-4, 4], 26 | [-5, 3], 27 | [-6, 2], 28 | [-7, 1], 29 | [-8, 0], 30 | [-9, 0], 31 | ])(`getAvatarDirection handles direction %s`, (input, output) => 32 | expect(getAvatarDirection(input)).toEqual(output) 33 | ); 34 | -------------------------------------------------------------------------------- /src/objects/avatar/enum/AvatarFigurePartType.ts: -------------------------------------------------------------------------------- 1 | export enum AvatarFigurePartType { 2 | Body = "bd", 3 | Shoes = "sh", 4 | Legs = "lg", 5 | Chest = "ch", 6 | WaistAccessory = "wa", 7 | ChestAccessory = "ca", 8 | Head = "hd", 9 | Hair = "hr", 10 | FaceAccessory = "fa", 11 | EyeAccessory = "ea", 12 | HeadAccessory = "ha", 13 | HeadAccessoryExtra = "he", 14 | CoatChest = "cc", 15 | ChestPrint = "cp", 16 | LeftHandItem = "li", 17 | LeftHand = "lh", 18 | LeftSleeve = "ls", 19 | RightHand = "rh", 20 | RightSleeve = "rs", 21 | Face = "fc", 22 | Eyes = "ey", 23 | HairBig = "hrb", 24 | RightHandItem = "ri", 25 | LeftCoatSleeve = "lc", 26 | RightCoatSleeve = "rc", 27 | } 28 | -------------------------------------------------------------------------------- /src/objects/furniture/util/getDirectionForFurniture.test.ts: -------------------------------------------------------------------------------- 1 | import { getDirectionForFurniture } from "./getDirectionForFurniture"; 2 | 3 | test("gets direction 1", () => { 4 | expect(getDirectionForFurniture(0, [0, 2, 4, 6])).toEqual(0); 5 | }); 6 | 7 | test("gets direction 2", () => { 8 | expect(getDirectionForFurniture(4, [0, 2, 4, 6])).toEqual(4); 9 | }); 10 | 11 | test("gets direction 3", () => { 12 | expect(getDirectionForFurniture(3, [0, 2, 4, 6])).toEqual(2); 13 | }); 14 | 15 | test("gets direction 4", () => { 16 | expect(getDirectionForFurniture(4, [0, 2])).toEqual(2); 17 | }); 18 | 19 | test("gets direction 5", () => { 20 | expect(getDirectionForFurniture(0, [2, 4])).toEqual(2); 21 | }); 22 | -------------------------------------------------------------------------------- /e2e/src/tests/room/models/renderModelL.ts: -------------------------------------------------------------------------------- 1 | import { renderModel } from "./util/renderModel"; 2 | 3 | export const renderModelL = renderModel(`xxxxxxxxxxxxxxxxxxxxx 4 | x00000000000000000000 5 | x00000000000000000000 6 | x00000000000000000000 7 | x00000000000000000000 8 | x00000000000000000000 9 | x00000000000000000000 10 | x00000000000000000000 11 | x00000000000000000000 12 | x00000000xxxx00000000 13 | x00000000xxxx00000000 14 | x00000000xxxx00000000 15 | x00000000xxxx00000000 16 | x00000000xxxx00000000 17 | x00000000xxxx00000000 18 | x00000000xxxx00000000 19 | 000000000xxxx00000000 20 | x00000000xxxx00000000 21 | x00000000xxxx00000000 22 | x00000000xxxx00000000 23 | x00000000xxxx00000000 24 | xxxxxxxxxxxxxxxxxxxxx`); 25 | -------------------------------------------------------------------------------- /e2e/src/tests/room/models/renderModelN.ts: -------------------------------------------------------------------------------- 1 | import { renderModel } from "./util/renderModel"; 2 | 3 | export const renderModelN = renderModel(`xxxxxxxxxxxxxxxxxxxxx 4 | x00000000000000000000 5 | x00000000000000000000 6 | x00000000000000000000 7 | x00000000000000000000 8 | x00000000000000000000 9 | x00000000000000000000 10 | x000000xxxxxxxx000000 11 | x000000x000000x000000 12 | x000000x000000x000000 13 | x000000x000000x000000 14 | x000000x000000x000000 15 | x000000x000000x000000 16 | x000000x000000x000000 17 | x000000xxxxxxxx000000 18 | x00000000000000000000 19 | 000000000000000000000 20 | x00000000000000000000 21 | x00000000000000000000 22 | x00000000000000000000 23 | x00000000000000000000 24 | xxxxxxxxxxxxxxxxxxxxx`); 25 | -------------------------------------------------------------------------------- /src/util/tilemap/padTileMap.ts: -------------------------------------------------------------------------------- 1 | import { TileType } from "../../types/TileType"; 2 | 3 | export function padTileMap(tilemap: TileType[][]) { 4 | const firstRow = tilemap[0]; 5 | if (firstRow == null) throw new Error("Invalid row"); 6 | 7 | let offsetY = 0; 8 | let offsetX = 0; 9 | 10 | if (firstRow.some((type) => type !== "x")) { 11 | tilemap = [firstRow.map(() => "x" as const), ...tilemap]; 12 | offsetY += 1; 13 | } 14 | 15 | const nonPrefixedRows = tilemap.filter((row) => row[0] !== "x"); 16 | if (nonPrefixedRows.length > 1) { 17 | tilemap = tilemap.map((row): TileType[] => ["x", ...row]); 18 | offsetX += 1; 19 | } 20 | 21 | return { 22 | tilemap, 23 | offsetX, 24 | offsetY, 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /docs/src/pages/styles.module.css: -------------------------------------------------------------------------------- 1 | /* stylelint-disable docusaurus/copyright-header */ 2 | 3 | /** 4 | * CSS files with the .module.css suffix will be treated as CSS modules 5 | * and scoped locally. 6 | */ 7 | 8 | .heroBanner { 9 | padding: 4rem 0; 10 | text-align: center; 11 | position: relative; 12 | overflow: hidden; 13 | } 14 | 15 | @media screen and (max-width: 966px) { 16 | .heroBanner { 17 | padding: 2rem; 18 | } 19 | } 20 | 21 | .buttons { 22 | display: flex; 23 | align-items: center; 24 | justify-content: center; 25 | } 26 | 27 | .features { 28 | display: flex; 29 | align-items: center; 30 | padding: 2rem 0; 31 | width: 100%; 32 | } 33 | 34 | .featureImage { 35 | height: 200px; 36 | width: 200px; 37 | } 38 | -------------------------------------------------------------------------------- /src/objects/furniture/util/getFurnitureFetch.ts: -------------------------------------------------------------------------------- 1 | import { FurnitureFetch } from "../../../interfaces/IFurnitureLoader"; 2 | import { FurnitureFetchInfo } from "../FurnitureFetchInfo"; 3 | 4 | export function getFurnitureFetch( 5 | data: FurnitureFetchInfo, 6 | placementType: "wall" | "floor" 7 | ): FurnitureFetch { 8 | if (data.id != null && data.type != null) 9 | throw new Error( 10 | "Both `id` and `type` specified. Please supply only one of the two." 11 | ); 12 | 13 | if (data.id != null) { 14 | return { kind: "id", id: data.id, placementType }; 15 | } 16 | 17 | if (data.type != null) { 18 | return { kind: "type", type: data.type }; 19 | } 20 | 21 | throw new Error("No `id` or `type` provided for the furniture."); 22 | } 23 | -------------------------------------------------------------------------------- /e2e/src/tests/room/models/renderModelI.ts: -------------------------------------------------------------------------------- 1 | import { renderModel } from "./util/renderModel"; 2 | 3 | export const renderModelI = renderModel(`xxxxxxxxxxxxxxxxx 4 | x0000000000000000 5 | x0000000000000000 6 | x0000000000000000 7 | x0000000000000000 8 | x0000000000000000 9 | x0000000000000000 10 | x0000000000000000 11 | x0000000000000000 12 | x0000000000000000 13 | 00000000000000000 14 | x0000000000000000 15 | x0000000000000000 16 | x0000000000000000 17 | x0000000000000000 18 | x0000000000000000 19 | x0000000000000000 20 | x0000000000000000 21 | x0000000000000000 22 | x0000000000000000 23 | x0000000000000000 24 | x0000000000000000 25 | x0000000000000000 26 | x0000000000000000 27 | x0000000000000000 28 | x0000000000000000 29 | x0000000000000000 30 | xxxxxxxxxxxxxxxxx`); 31 | -------------------------------------------------------------------------------- /e2e/src/tests/room/models/renderModelJ.ts: -------------------------------------------------------------------------------- 1 | import { renderModel } from "./util/renderModel"; 2 | 3 | export const renderModelJ = renderModel(`xxxxxxxxxxxxxxxxxxxxx 4 | xxxxxxxxxxx0000000000 5 | xxxxxxxxxxx0000000000 6 | xxxxxxxxxxx0000000000 7 | xxxxxxxxxxx0000000000 8 | xxxxxxxxxxx0000000000 9 | xxxxxxxxxxx0000000000 10 | x00000000000000000000 11 | x00000000000000000000 12 | x00000000000000000000 13 | 000000000000000000000 14 | x00000000000000000000 15 | x00000000000000000000 16 | x00000000000000000000 17 | x00000000000000000000 18 | x00000000000000000000 19 | x00000000000000000000 20 | x0000000000xxxxxxxxxx 21 | x0000000000xxxxxxxxxx 22 | x0000000000xxxxxxxxxx 23 | x0000000000xxxxxxxxxx 24 | x0000000000xxxxxxxxxx 25 | x0000000000xxxxxxxxxx 26 | xxxxxxxxxxxxxxxxxxxxx`); 27 | -------------------------------------------------------------------------------- /e2e/src/tests/room/models/renderModelP.ts: -------------------------------------------------------------------------------- 1 | import { renderModel } from "./util/renderModel"; 2 | 3 | export const renderModelP = renderModel(`xxxxxxxxxxxxxxxxxxx 4 | xxxxxxx222222222222 5 | xxxxxxx222222222222 6 | xxxxxxx222222222222 7 | xxxxxxx222222222222 8 | xxxxxxx222222222222 9 | xxxxxxx222222222222 10 | xxxxxxx22222222xxxx 11 | xxxxxxx11111111xxxx 12 | x222221111111111111 13 | x222221111111111111 14 | x222221111111111111 15 | x222221111111111111 16 | x222221111111111111 17 | x222221111111111111 18 | x222221111111111111 19 | x222221111111111111 20 | x2222xx11111111xxxx 21 | x2222xx00000000xxxx 22 | x2222xx000000000000 23 | x2222xx000000000000 24 | x2222xx000000000000 25 | x2222xx000000000000 26 | 22222xx000000000000 27 | x2222xx000000000000 28 | xxxxxxxxxxxxxxxxxxx`); 29 | -------------------------------------------------------------------------------- /e2e/src/tests/room/models/renderModelQ.ts: -------------------------------------------------------------------------------- 1 | import { renderModel } from "./util/renderModel"; 2 | 3 | export const renderModelQ = renderModel(`xxxxxxxxxxxxxxxxxxx 4 | xxxxxxxxxxx22222222 5 | xxxxxxxxxxx22222222 6 | xxxxxxxxxxx22222222 7 | xxxxxxxxxx222222222 8 | xxxxxxxxxxx22222222 9 | xxxxxxxxxxx22222222 10 | x222222222222222222 11 | x222222222222222222 12 | x222222222222222222 13 | x222222222222222222 14 | x222222222222222222 15 | x222222222222222222 16 | x2222xxxxxxxxxxxxxx 17 | x2222xxxxxxxxxxxxxx 18 | x2222211111xx000000 19 | x222221111110000000 20 | x222221111110000000 21 | x2222211111xx000000 22 | xx22xxx1111xxxxxxxx 23 | xx11xxx1111xxxxxxxx 24 | x1111xx1111xx000000 25 | x1111xx111110000000 26 | x1111xx111110000000 27 | x1111xx1111xx000000 28 | xxxxxxxxxxxxxxxxxxx`); 29 | -------------------------------------------------------------------------------- /src/objects/furniture/FurnitureRoomVisualization.ts: -------------------------------------------------------------------------------- 1 | import { MaskNode } from "../../interfaces/IRoomVisualization"; 2 | import { IFurnitureRoomVisualization } from "./BaseFurniture"; 3 | 4 | export class FurnitureRoomVisualization implements IFurnitureRoomVisualization { 5 | constructor(private _container: PIXI.Container) {} 6 | 7 | public get container() { 8 | return this._container; 9 | } 10 | 11 | static fromContainer(container: PIXI.Container) { 12 | return new FurnitureRoomVisualization(container); 13 | } 14 | 15 | addMask(): MaskNode { 16 | return { 17 | remove: () => { 18 | // Do nothing 19 | }, 20 | update: () => { 21 | // Do nothing 22 | }, 23 | sprite: null as any, 24 | }; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/objects/avatar/data/interfaces/IAvatarGeometryData.ts: -------------------------------------------------------------------------------- 1 | export interface AvatarGeometry { 2 | id: string; 3 | width: number; 4 | height: number; 5 | dx: number; 6 | dy: number; 7 | } 8 | 9 | export interface Bodypart { 10 | id: string; 11 | z: number; 12 | items: BodypartItem[]; 13 | } 14 | 15 | export interface BodypartItem { 16 | id: string; 17 | z: number; 18 | radius: number; 19 | } 20 | 21 | export interface IAvatarGeometryData { 22 | getGeometry(geometry: string): AvatarGeometry | undefined; 23 | getBodyParts(avaterSet: string): string[]; 24 | getBodyPart(geometry: string, bodyPartId: string): Bodypart | undefined; 25 | getBodyPartItem( 26 | geometry: string, 27 | bodyPartId: string, 28 | itemId: string 29 | ): BodypartItem | undefined; 30 | } 31 | -------------------------------------------------------------------------------- /e2e/src/tests/room/renderHiddenWalls.ts: -------------------------------------------------------------------------------- 1 | import { Room } from "@jankuss/shroom"; 2 | import { TestRenderer } from "../../TestRenderer"; 3 | 4 | export const renderHiddenWalls: TestRenderer = ({ shroom, application }) => { 5 | const room = Room.create(shroom, { 6 | tilemap: ` 7 | xxxxxxxxxxxx 8 | xxxxxxxxxxxx 9 | xxxxxxxxxxxx 10 | xxxxxxxxxxxx 11 | xxxxxxxxxxxx 12 | xxxxx000000x 13 | xxxxx000000x 14 | xxxx0000000x 15 | xxxxx000000x 16 | xxxxx000000x 17 | xxxxx000000x 18 | xxxxxxxxxxxx 19 | xxxxxxxxxxxx 20 | xxxxxxxxxxxx 21 | xxxxxxxxxxxx 22 | xxxxxxxxxxxx 23 | `, 24 | }); 25 | 26 | room.hideWalls = true; 27 | 28 | application.stage.addChild(room); 29 | }; 30 | -------------------------------------------------------------------------------- /src/objects/avatar/util/getDrawOrderForActions.ts: -------------------------------------------------------------------------------- 1 | import { AvatarActionInfo } from "../data/interfaces/IAvatarActionsData"; 2 | 3 | export function getDrawOrderForActions( 4 | activeActions: AvatarActionInfo[], 5 | options: { hasItem: boolean } 6 | ) { 7 | const activePartSets = new Set(); 8 | activeActions.forEach((info) => { 9 | if (info.activepartset != null) { 10 | activePartSets.add(info.activepartset); 11 | } 12 | }); 13 | 14 | if (options.hasItem) { 15 | activePartSets.add("itemRight"); 16 | } 17 | 18 | if (activePartSets.has("handLeft")) { 19 | return "lh-up"; 20 | } 21 | 22 | if ( 23 | activePartSets.has("handRightAndHead") || 24 | activePartSets.has("handRight") 25 | ) { 26 | return "rh-up"; 27 | } 28 | 29 | return "std"; 30 | } 31 | -------------------------------------------------------------------------------- /src/objects/avatar/enum/AvatarAction.ts: -------------------------------------------------------------------------------- 1 | export enum AvatarAction { 2 | Move = "Move", 3 | Wave = "Wave", 4 | Talk = "Talk", 5 | Swim = "Swim", 6 | Float = "Float", 7 | Sign = "Sign", 8 | Respect = "Respect", 9 | Blow = "Blow", 10 | Laugh = "Laugh", 11 | SnowWarRun = "SnowWarRun", 12 | SnowWarDieFront = "SnowWarDieFront", 13 | SnowWarDieBack = "SnowWarDieBack", 14 | SnowWarPick = "SnowWarPick", 15 | SnowWarThrow = "SnowWarThrow", 16 | Lay = "Lay", 17 | Sit = "Sit", 18 | Idle = "Idle", 19 | Dance = "Dance", 20 | UseItem = "UseItem", 21 | CarryItem = "CarryItem", 22 | Gesture = "Gesture", 23 | GestureSmile = "GestureSmile", 24 | GestureSad = "GestureSad", 25 | GestureAngry = "GestureAngry", 26 | GestureSurprised = "GestureSurprised", 27 | Sleep = "Sleep", 28 | Default = "Default", 29 | } 30 | -------------------------------------------------------------------------------- /src/objects/furniture/util/visualization/parseColors.ts: -------------------------------------------------------------------------------- 1 | import { VisualizationXmlVisualization } from "./VisualizationXml"; 2 | 3 | export function parseColors( 4 | visualization: VisualizationXmlVisualization, 5 | set: (id: string, colorLayersMap: Map) => void 6 | ) { 7 | const colors = visualization.colors && visualization.colors[0].color; 8 | 9 | if (colors != null) { 10 | colors.forEach(color => { 11 | const id = color["$"].id; 12 | const colorLayersMap = new Map(); 13 | const colorLayers = color.colorLayer; 14 | 15 | colorLayers.forEach(layer => { 16 | const layerId = layer["$"].id; 17 | const color = layer["$"].color; 18 | 19 | colorLayersMap.set(layerId, color); 20 | }); 21 | 22 | set(id, colorLayersMap); 23 | }); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/interfaces/IFurnitureData.ts: -------------------------------------------------------------------------------- 1 | import { IFurniture } from "../objects/furniture/IFurniture"; 2 | import { furnitureDataTransformers } from "../util/furnitureDataTransformers"; 3 | 4 | export interface IFurnitureData { 5 | getRevisionForType(type: string): Promise; 6 | getInfo(type: string): Promise; 7 | getTypeById( 8 | id: FurnitureId, 9 | type: "wall" | "floor" 10 | ): Promise; 11 | getInfoForFurniture( 12 | furniture: IFurniture 13 | ): Promise; 14 | getInfos(): Promise<[string, FurnitureInfo][]>; 15 | } 16 | 17 | export type FurnitureId = string | number; 18 | 19 | export type FurnitureInfo = { 20 | [key in keyof typeof furnitureDataTransformers]: ReturnType< 21 | typeof furnitureDataTransformers[key] 22 | >; 23 | }; 24 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [master] 9 | pull_request: 10 | branches: [master] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | node-version: [10.x, 12.x, 14.x] 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Use Node.js ${{ matrix.node-version }} 23 | uses: actions/setup-node@v1 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | - run: npm ci 27 | - run: npm run build 28 | - run: npm test 29 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shroom", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "start": "docusaurus start", 8 | "build": "docusaurus build", 9 | "swizzle": "docusaurus swizzle", 10 | "deploy": "docusaurus deploy", 11 | "serve": "docusaurus serve" 12 | }, 13 | "dependencies": { 14 | "@docusaurus/core": "2.0.0-alpha.66", 15 | "@docusaurus/preset-classic": "2.0.0-alpha.66", 16 | "@mdx-js/react": "^1.5.8", 17 | "clsx": "^1.1.1", 18 | "react": "^16.8.4", 19 | "react-dom": "^16.8.4" 20 | }, 21 | "browserslist": { 22 | "production": [ 23 | ">0.2%", 24 | "not dead", 25 | "not op_mini all" 26 | ], 27 | "development": [ 28 | "last 1 chrome version", 29 | "last 1 firefox version", 30 | "last 1 safari version" 31 | ] 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/interfaces/IRoomVisualization.ts: -------------------------------------------------------------------------------- 1 | import { IRoomPart } from "../objects/room/parts/IRoomPart"; 2 | import { RoomLandscapeMaskSprite } from "../objects/room/RoomLandscapeMaskSprite"; 3 | 4 | export interface IRoomVisualization { 5 | container: PIXI.Container; 6 | behindWallContainer: PIXI.Container; 7 | landscapeContainer: PIXI.Container; 8 | floorContainer: PIXI.Container; 9 | wallContainer: PIXI.Container; 10 | 11 | addPart(part: IRoomPart): PartNode; 12 | addMask(id: string, element: PIXI.Sprite): MaskNode; 13 | } 14 | 15 | export type MaskNode = { 16 | sprite: PIXI.Sprite; 17 | update: () => void; 18 | remove: () => void; 19 | }; 20 | 21 | export type PartNode = { 22 | remove: () => void; 23 | }; 24 | 25 | export type RoomVisualizationMeta = { 26 | masks: Map; 27 | wallHeight: number; 28 | wallHeightWithZ: number; 29 | }; 30 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 | This website is built using [Docusaurus 2](https://v2.docusaurus.io/), a modern static website generator. 4 | 5 | ## Installation 6 | 7 | ```console 8 | yarn install 9 | ``` 10 | 11 | ## Local Development 12 | 13 | ```console 14 | yarn start 15 | ``` 16 | 17 | This command starts a local development server and open up a browser window. Most changes are reflected live without having to restart the server. 18 | 19 | ## Build 20 | 21 | ```console 22 | yarn build 23 | ``` 24 | 25 | This command generates static content into the `build` directory and can be served using any static contents hosting service. 26 | 27 | ## Deployment 28 | 29 | ```console 30 | GIT_USER= USE_SSH=true yarn deploy 31 | ``` 32 | 33 | If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. 34 | -------------------------------------------------------------------------------- /e2e/src/tests/room/models/renderModelR.ts: -------------------------------------------------------------------------------- 1 | import { renderModel } from "./util/renderModel"; 2 | 3 | export const renderModelR = renderModel(`xxxxxxxxxxxxxxxxxxxxxxxxx 4 | xxxxxxxxxxx33333333333333 5 | xxxxxxxxxxx33333333333333 6 | xxxxxxxxxxx33333333333333 7 | xxxxxxxxxx333333333333333 8 | xxxxxxxxxxx33333333333333 9 | xxxxxxxxxxx33333333333333 10 | xxxxxxx333333333333333333 11 | xxxxxxx333333333333333333 12 | xxxxxxx333333333333333333 13 | xxxxxxx333333333333333333 14 | xxxxxxx333333333333333333 15 | xxxxxxx333333333333333333 16 | x4444433333xxxxxxxxxxxxxx 17 | x4444433333xxxxxxxxxxxxxx 18 | x44444333333222xx000000xx 19 | x44444333333222xx000000xx 20 | xxx44xxxxxxxx22xx000000xx 21 | xxx33xxxxxxxx11xx000000xx 22 | xxx33322222211110000000xx 23 | xxx33322222211110000000xx 24 | xxxxxxxxxxxxxxxxx000000xx 25 | xxxxxxxxxxxxxxxxx000000xx 26 | xxxxxxxxxxxxxxxxx000000xx 27 | xxxxxxxxxxxxxxxxx000000xx 28 | xxxxxxxxxxxxxxxxxxxxxxxxx`); 29 | -------------------------------------------------------------------------------- /src/objects/furniture/IFurniture.ts: -------------------------------------------------------------------------------- 1 | import { FurnitureId } from "../../interfaces/IFurnitureData"; 2 | import { FurnitureExtraData } from "./FurnitureExtraData"; 3 | import { IFurnitureVisualization } from "./IFurnitureVisualization"; 4 | import { IFurnitureEventHandlers } from "./util/IFurnitureEventHandlers"; 5 | 6 | export interface IFurniture extends IFurnitureEventHandlers { 7 | id: FurnitureId | undefined; 8 | type: string | undefined; 9 | roomX: number; 10 | roomY: number; 11 | roomZ: number; 12 | direction: number; 13 | animation: string | undefined; 14 | highlight: boolean | undefined; 15 | placementType: "wall" | "floor"; 16 | alpha: number; 17 | extradata: Promise; 18 | validDirections: Promise; 19 | visualization: IFurnitureVisualization; 20 | } 21 | 22 | export type IFurnitureBehavior = { 23 | setParent(furniture: T): void; 24 | }; 25 | -------------------------------------------------------------------------------- /example/src/behaviors/MultiStateBehavior.ts: -------------------------------------------------------------------------------- 1 | import { IFurniture, IFurnitureBehavior } from "@jankuss/shroom"; 2 | 3 | export class MultiStateBehavior implements IFurnitureBehavior { 4 | private furniture: IFurniture | undefined; 5 | private currentState: number = 0; 6 | 7 | constructor(private options: { initialState: number; count: number }) { 8 | this.currentState = this.options.initialState; 9 | this._updateState(); 10 | } 11 | 12 | private _updateState() { 13 | const furniture = this.furniture; 14 | 15 | if (furniture != null) { 16 | furniture.animation = this.currentState.toString(); 17 | } 18 | } 19 | 20 | setParent(furniture: IFurniture): void { 21 | this.furniture = furniture; 22 | 23 | this._updateState(); 24 | 25 | furniture.onDoubleClick = () => { 26 | this.currentState = (this.currentState + 1) % this.options.count; 27 | this._updateState(); 28 | }; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /docs/src/css/custom.css: -------------------------------------------------------------------------------- 1 | /* stylelint-disable docusaurus/copyright-header */ 2 | /** 3 | * Any CSS included here will be global. The classic template 4 | * bundles Infima by default. Infima is a CSS framework designed to 5 | * work well for content-centric websites. 6 | */ 7 | 8 | /* You can override the default Infima variables here. */ 9 | :root { 10 | --ifm-color-primary: #25c2a0; 11 | --ifm-color-primary-dark: rgb(33, 175, 144); 12 | --ifm-color-primary-darker: rgb(31, 165, 136); 13 | --ifm-color-primary-darkest: rgb(26, 136, 112); 14 | --ifm-color-primary-light: rgb(70, 203, 174); 15 | --ifm-color-primary-lighter: rgb(102, 212, 189); 16 | --ifm-color-primary-lightest: rgb(146, 224, 208); 17 | --ifm-code-font-size: 95%; 18 | } 19 | 20 | .docusaurus-highlight-code-line { 21 | background-color: rgb(72, 77, 91); 22 | display: block; 23 | margin: 0 calc(-1 * var(--ifm-pre-padding)); 24 | padding: 0 var(--ifm-pre-padding); 25 | } 26 | -------------------------------------------------------------------------------- /src/objects/avatar/util/tests/parseLookString.test.ts: -------------------------------------------------------------------------------- 1 | import { ParsedLook, parseLookString } from "../parseLookString"; 2 | 3 | test("parseLookString parses plain look string", () => { 4 | const expected: ParsedLook = new Map(); 5 | expected.set("hd", { setId: 99999, colorId: 99999 }); 6 | 7 | expect(parseLookString("hd-99999-99999")).toEqual(expected); 8 | }); 9 | 10 | test("parseLookString parses look string", () => { 11 | const expected: ParsedLook = new Map(); 12 | expected.set("hd", { setId: 180, colorId: 1 }); 13 | expected.set("ch", { setId: 255, colorId: 66 }); 14 | expected.set("lg", { setId: 280, colorId: 110 }); 15 | expected.set("sh", { setId: 305, colorId: 62 }); 16 | expected.set("ha", { setId: 1012, colorId: 110 }); 17 | expected.set("hr", { setId: 828, colorId: 61 }); 18 | 19 | expect( 20 | parseLookString( 21 | "hd-180-1.ch-255-66.lg-280-110.sh-305-62.ha-1012-110.hr-828-61" 22 | ) 23 | ).toEqual(expected); 24 | }); 25 | -------------------------------------------------------------------------------- /e2e/src/tests/room/models/renderModelK.ts: -------------------------------------------------------------------------------- 1 | import { renderModel } from "./util/renderModel"; 2 | 3 | export const renderModelK = renderModel(`xxxxxxxxxxxxxxxxxxxxxxxxx 4 | xxxxxxxxxxxxxxxxx00000000 5 | xxxxxxxxxxxxxxxxx00000000 6 | xxxxxxxxxxxxxxxxx00000000 7 | xxxxxxxxxxxxxxxxx00000000 8 | xxxxxxxxx0000000000000000 9 | xxxxxxxxx0000000000000000 10 | xxxxxxxxx0000000000000000 11 | xxxxxxxxx0000000000000000 12 | x000000000000000000000000 13 | x000000000000000000000000 14 | x000000000000000000000000 15 | x000000000000000000000000 16 | 0000000000000000000000000 17 | x000000000000000000000000 18 | x000000000000000000000000 19 | x000000000000000000000000 20 | xxxxxxxxx0000000000000000 21 | xxxxxxxxx0000000000000000 22 | xxxxxxxxx0000000000000000 23 | xxxxxxxxx0000000000000000 24 | xxxxxxxxx0000000000000000 25 | xxxxxxxxx0000000000000000 26 | xxxxxxxxx0000000000000000 27 | xxxxxxxxx0000000000000000 28 | xxxxxxxxx0000000000000000 29 | xxxxxxxxx0000000000000000 30 | xxxxxxxxxxxxxxxxxxxxxxxxx`); 31 | -------------------------------------------------------------------------------- /e2e/src/tests/room/models/renderModelO.ts: -------------------------------------------------------------------------------- 1 | import { renderModel } from "./util/renderModel"; 2 | 3 | export const renderModelO = renderModel(`xxxxxxxxxxxxxxxxxxxxxxxxx 4 | xxxxxxxxxxxxx11111111xxxx 5 | xxxxxxxxxxxxx11111111xxxx 6 | xxxxxxxxxxxxx11111111xxxx 7 | xxxxxxxxxxxxx11111111xxxx 8 | xxxxxxxxxxxxx11111111xxxx 9 | xxxxxxxxxxxxx11111111xxxx 10 | xxxxxxxxxxxxx11111111xxxx 11 | xxxxxxxxxxxxx00000000xxxx 12 | xxxxxxxxx0000000000000000 13 | xxxxxxxxx0000000000000000 14 | xxxxxxxxx0000000000000000 15 | xxxxxxxxx0000000000000000 16 | xxxxxxxxx0000000000000000 17 | xxxxxxxxx0000000000000000 18 | x111111100000000000000000 19 | x111111100000000000000000 20 | x111111100000000000000000 21 | 1111111100000000000000000 22 | x111111100000000000000000 23 | x111111100000000000000000 24 | x111111100000000000000000 25 | x111111100000000000000000 26 | xxxxxxxxx0000000000000000 27 | xxxxxxxxx0000000000000000 28 | xxxxxxxxx0000000000000000 29 | xxxxxxxxx0000000000000000 30 | xxxxxxxxxxxxxxxxxxxxxxxxx`); 31 | -------------------------------------------------------------------------------- /src/types/RoomPosition.ts: -------------------------------------------------------------------------------- 1 | export type RoomPosition = { 2 | /** 3 | * The x position in the room. 4 | * The y-Axis is marked in the following graphic: 5 | * 6 | * ``` 7 | * | 8 | * | 9 | * | 10 | * / \ 11 | * / \ <- x-Axis 12 | * / \ 13 | * ``` 14 | */ 15 | roomX: number; 16 | /** 17 | * The y position in the room. 18 | * The y-Axis is marked in the following graphic: 19 | * 20 | * ``` 21 | * | 22 | * | 23 | * | 24 | * / \ 25 | * y-Axis -> / \ 26 | * / \ 27 | * ``` 28 | */ 29 | roomY: number; 30 | /** 31 | * The z position in the room. 32 | * The z-Axis is marked in the following graphic: 33 | * 34 | * ``` 35 | * | 36 | * z-Axis -> | 37 | * | 38 | * / \ 39 | * / \ 40 | * / \ 41 | * ``` 42 | */ 43 | roomZ: number; 44 | }; 45 | -------------------------------------------------------------------------------- /storybook/stories/furniture/FurnitureVisualizations.stories.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BasicFurnitureVisualization, 3 | FurnitureGuildCustomizedVisualization, 4 | } from "../../../dist"; 5 | import { renderFurnitureExample } from "./renderFurnitureExample"; 6 | 7 | export default { 8 | title: "Furniture / Visualizations", 9 | }; 10 | 11 | export function StaticVisualization() { 12 | return renderFurnitureExample( 13 | "rare_dragonlamp*0", 14 | { directions: [2, 4], spacing: 2, animations: ["1"] }, 15 | (furniture) => { 16 | furniture.visualization = new BasicFurnitureVisualization(); 17 | } 18 | ); 19 | } 20 | 21 | export function GuildVisualization() { 22 | return renderFurnitureExample( 23 | "gld_gate", 24 | { directions: [2, 4], spacing: 2 }, 25 | (furniture) => { 26 | furniture.visualization = new FurnitureGuildCustomizedVisualization({ 27 | primaryColor: "#ff00ff", 28 | secondaryColor: "#00ff00", 29 | }); 30 | } 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /docs/docs/guides/avatar-actions.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: avatar-actions 3 | title: Avatar actions 4 | --- 5 | 6 | Avatars can have multiple different actions. Here is how you add and remove actions from avatars. 7 | 8 | ```ts 9 | /** 10 | * ... 11 | */ 12 | 13 | const room = Room.create(shroom, { 14 | tilemap: ` 15 | xxxx 16 | x000 17 | x000 18 | x000 19 | `, 20 | }); 21 | 22 | const avatar = new Avatar({ 23 | roomX: 1, 24 | roomY: 1, 25 | roomZ: 0, 26 | direction: 2, 27 | look: "hd-180-1.hr-100-61.ch-210-66.lg-280-110.sh-305-62", 28 | }); 29 | 30 | avatar.addAction(AvatarAction.GestureSmile); 31 | avatar.addAction(AvatarAction.Respect); 32 | avatar.addAction(AvatarAction.Sit); 33 | avatar.addAction(AvatarAction.CarryItem); 34 | avatar.item = 1; 35 | 36 | setTimeout(() => { 37 | // Remove the sitting action after some time passed 38 | avatar.removeAction(AvatarAction.Sit); 39 | }, 5000); 40 | 41 | room.addRoomObject(avatar); 42 | 43 | application.stage.addChild(room); 44 | ``` 45 | -------------------------------------------------------------------------------- /src/objects/hitdetection/HitTexture.test.ts: -------------------------------------------------------------------------------- 1 | import { HitTexture } from "./HitTexture"; 2 | 3 | // To view this image, just copy it and use it as a url in your browser 4 | const testImage = 5 | "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACAQMAAABIeJ9nAAAAAXNSR0IB2cksfwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAZQTFRF/wAAAAAAQaMSAwAAAAJ0Uk5T/wDltzBKAAAADElEQVR4nGNwYGgAAAFEAME6ehxWAAAAAElFTkSuQmCC"; 6 | 7 | test("detects first pixel", async () => { 8 | const texture = await HitTexture.fromUrl(testImage); 9 | expect(texture.hits(0, 0, { x: 0, y: 0 })).toBe(true); 10 | }); 11 | 12 | test("doesn't detect second pixel", async () => { 13 | const texture = await HitTexture.fromUrl(testImage); 14 | expect(texture.hits(1, 0, { x: 0, y: 0 })).toBe(false); 15 | }); 16 | 17 | test("detects multiple pixels", async () => { 18 | const texture = await HitTexture.fromUrl(testImage); 19 | expect(texture.hits(0, 1, { x: 0, y: 0 })).toBe(false); 20 | expect(texture.hits(1, 1, { x: 0, y: 0 })).toBe(true); 21 | }); 22 | -------------------------------------------------------------------------------- /ci/discord/src/template.json: -------------------------------------------------------------------------------- 1 | { 2 | "content": "A new version of shroom has been released.", 3 | "embeds": [ 4 | { 5 | "title": "shroom v0.1.0-alpha.15", 6 | "description": "The following changes have been made. You can view the full CHANGELOG [here](https://github.com/jankuss/shroom/blob/v0.1.0-dev.1/CHANGELOG.md). ```code```", 7 | "url": "https://www.npmjs.com/package/shroom/v/0.4.2", 8 | "fields": [ 9 | { 10 | "name": "Download with npm", 11 | "value": "Download shroom with [npm](https://www.npmjs.com/package/shroom/v/0.4.2)." 12 | }, 13 | { 14 | "name": "Report Issues", 15 | "value": "Please report any bugs or issues with this version on our [Github Issues](https://github.com/jankuss/shroom/issues)." 16 | }, 17 | { 18 | "name": "Need help?", 19 | "value": "Ask questions in the #support channel." 20 | } 21 | ] 22 | } 23 | ] 24 | } -------------------------------------------------------------------------------- /src/tools/dump/extractSwfs.ts: -------------------------------------------------------------------------------- 1 | import Bluebird from "bluebird"; 2 | import { basename } from "path"; 3 | import { ProgressBar } from "./ProgressBar"; 4 | import { dumpSwf, OnAfterCallback } from "./dumpSwf"; 5 | import { Logger } from "./Logger"; 6 | 7 | export async function extractSwfs( 8 | logger: Logger, 9 | name: string, 10 | swfs: string[], 11 | onAfter: OnAfterCallback 12 | ) { 13 | const dumpFurnitureProgress = new ProgressBar( 14 | logger, 15 | swfs.length, 16 | (current, count, extra) => { 17 | if (extra != null) { 18 | return `Extracting ${name}: ${current} / ${count} (${extra})`; 19 | } else { 20 | return `Extracting ${name}: ${current} / ${count}`; 21 | } 22 | } 23 | ); 24 | 25 | await Bluebird.map( 26 | swfs, 27 | async (path) => { 28 | await dumpSwf(path, onAfter); 29 | dumpFurnitureProgress.increment(basename(path)); 30 | }, 31 | { 32 | concurrency: 4, 33 | } 34 | ); 35 | 36 | dumpFurnitureProgress.done(); 37 | } 38 | -------------------------------------------------------------------------------- /docs/docs/guides/create-room.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: create-room 3 | title: Create a room 4 | --- 5 | 6 | import useBaseUrl from '@docusaurus/useBaseUrl'; 7 | 8 | The Room class is the most essential part of shroom. 9 | 10 | The simplest way to create a room is by using the following code. 11 | This will create a 4x3 room. 12 | 13 | ```ts 14 | import * as PIXI from "pixi.js"; 15 | 16 | import { Room, FloorFurniture, Avatar, Shroom } from "@jankuss/shroom"; 17 | 18 | const view = document.querySelector("#root") as HTMLCanvasElement; 19 | const application = new PIXI.Application({ view }); 20 | 21 | const shroom = Shroom.create({ application, resourcePath: "./resources" }); 22 | const room = Room.create(shroom, { 23 | tilemap: ` 24 | xxxxx 25 | x0000 26 | x0000 27 | x0000 28 | `, 29 | }); 30 | 31 | room.x = 100; 32 | room.y = 200; 33 | 34 | application.stage.addChild(room); 35 | ``` 36 | 37 | ## Result 38 | 39 | Your room should now look like this. 40 | 41 | Docusaurus with Keytar 42 | -------------------------------------------------------------------------------- /src/objects/furniture/util/visualization/parseLayers.ts: -------------------------------------------------------------------------------- 1 | import { VisualizationXmlVisualization } from "./VisualizationXml"; 2 | 3 | export type Layer = { 4 | zIndex: number | undefined; 5 | tag: string | undefined; 6 | ink: string | undefined; 7 | alpha: number | undefined; 8 | ignoreMouse: string | undefined; 9 | }; 10 | 11 | export function parseLayers( 12 | visualization: VisualizationXmlVisualization, 13 | set: (id: string, layer: Layer) => void 14 | ) { 15 | if (visualization.layers != null) { 16 | const layers = visualization.layers[0].layer; 17 | 18 | const layersLength = layers == null ? 0 : layers.length; 19 | for (let i = 0; i < layersLength; i++) { 20 | const layer = layers[i]["$"]; 21 | set(layer.id, { 22 | zIndex: layer != null && layer.z != null ? Number(layer.z) : 0, 23 | tag: layer.tag, 24 | ink: layer.ink, 25 | alpha: layer.alpha != null ? Number(layer.alpha) : undefined, 26 | ignoreMouse: layer.ignoreMouse, 27 | }); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /docs/docs/guides/use-base-avatar-and-furniture.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: use-base-avatar-and-furniture 3 | title: Using `BaseAvatar` and `BaseFurniture` 4 | --- 5 | 6 | For some use cases, you want to display avatar or furniture without a room. 7 | You can do this with `BaseAvatar` and `BaseFurniture`. The only thing you need is a `Shroom` instance. 8 | 9 | ```ts 10 | /** 11 | * ... 12 | */ 13 | 14 | const avatar = BaseAvatar.fromShroom(shroom, { 15 | look: { 16 | actions: new Set(), 17 | direction: 2, 18 | look: "hd-180-1.hr-100-61.ch-210-66.lg-280-110.sh-305-62", 19 | }, 20 | position: { x: 0, y: 100 }, 21 | zIndex: 0, 22 | onLoad: () => { 23 | // This is called when the avatar has been loaded completly. 24 | console.log("Loaded"); 25 | }, 26 | }); 27 | 28 | const furniture = BaseFurniture.fromShroom(shroom, application.stage, { 29 | direction: 2, 30 | type: { kind: "type", type: "club_sofa" }, 31 | animation: "0", 32 | }); 33 | 34 | furniture.x = 100; 35 | furniture.y = 50; 36 | 37 | application.stage.addChild(avatar); 38 | ``` 39 | -------------------------------------------------------------------------------- /src/assets/LegacyAssetBundle.ts: -------------------------------------------------------------------------------- 1 | import { IAssetBundle } from "./IAssetBundle"; 2 | 3 | export class LegacyAssetBundle implements IAssetBundle { 4 | private _blobs: Map> = new Map(); 5 | private _strings: Map> = new Map(); 6 | 7 | constructor(private _folderUrl: string) {} 8 | 9 | async getBlob(name: string): Promise { 10 | const current = this._blobs.get(name); 11 | if (current != null) return current; 12 | 13 | const imageUrl = `${this._folderUrl}/${name}`; 14 | 15 | const blob = fetch(imageUrl).then((response) => response.blob()); 16 | this._blobs.set(name, blob); 17 | 18 | return blob; 19 | } 20 | 21 | async getString(name: string): Promise { 22 | const current = this._strings.get(name); 23 | if (current != null) return current; 24 | 25 | const imageUrl = `${this._folderUrl}/${name}`; 26 | 27 | const string = fetch(imageUrl).then((response) => response.text()); 28 | this._strings.set(name, string); 29 | 30 | return string; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/objects/room/util/getTileColors.ts: -------------------------------------------------------------------------------- 1 | export function getTileColors(color: string) { 2 | const tileTint = fromHex(color); 3 | const borderLeftTint = fromHex(adjust(color, -20)); 4 | const borderRightTint = fromHex(adjust(color, -40)); 5 | 6 | return { 7 | tileTint, 8 | borderLeftTint, 9 | borderRightTint, 10 | }; 11 | } 12 | 13 | export function getWallColors(color: string) { 14 | const leftTint = fromHex(color); 15 | const topTint = fromHex(adjust(color, -103)); 16 | const rightTint = fromHex(adjust(color, -52)); 17 | 18 | return { 19 | topTint, 20 | leftTint, 21 | rightTint, 22 | }; 23 | } 24 | 25 | function fromHex(color: string) { 26 | return parseInt(color.replace("#", "0x"), 16); 27 | } 28 | 29 | function adjust(color: string, amount: number) { 30 | return ( 31 | "#" + 32 | color 33 | .replace(/^#/, "") 34 | .replace(/../g, (color) => 35 | ( 36 | "0" + 37 | Math.min(255, Math.max(0, parseInt(color, 16) + amount)).toString(16) 38 | ).substr(-2) 39 | ) 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /storybook/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "storybook", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "storybook": "start-storybook -s ./public -p 6006", 9 | "dump": "shroom dump --url https://www.habbo.com/gamedata/external_variables/326b0a1abf9e2571d541ac05e6eb3173b83bddea --location ./public/resources", 10 | "build-storybook": "build-storybook" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "ISC", 15 | "devDependencies": { 16 | "@babel/core": "^7.12.9", 17 | "@storybook/addon-actions": "^6.2.0-alpha.4", 18 | "@storybook/addon-essentials": "^6.2.0-alpha.4", 19 | "@storybook/addon-links": "^6.2.0-alpha.4", 20 | "@storybook/react": "^6.2.0-alpha.4", 21 | "babel-loader": "^8.2.2" 22 | }, 23 | "dependencies": { 24 | "@jankuss/shroom": "file:../", 25 | "pixi.js": "^5.3.3", 26 | "react": "^17.0.1", 27 | "react-dom": "^17.0.1" 28 | }, 29 | "nohoist": [ 30 | "**/@storybook/**" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /storybook/stories/avatar/AvatarEffect.stories.ts: -------------------------------------------------------------------------------- 1 | import { Room, Avatar } from "@jankuss/shroom"; 2 | import { renderAvatarDirections } from "./renderAvatarDirections"; 3 | 4 | export default { 5 | title: "Avatar / Effects", 6 | }; 7 | 8 | function renderEffect(effect: string) { 9 | return renderAvatarDirections( 10 | "hd-180-1.hr-828-61.ha-1012-110.he-1604-62.ea-1404-62.fa-1204-62.ch-255-66.lg-280-110.sh-305-62", 11 | undefined, 12 | (avatar) => { 13 | avatar.effect = effect; 14 | } 15 | ); 16 | } 17 | 18 | export function Dance1() { 19 | return renderEffect("dance.1"); 20 | } 21 | 22 | export function Dance2() { 23 | return renderEffect("dance.2"); 24 | } 25 | 26 | export function Dance3() { 27 | return renderEffect("dance.3"); 28 | } 29 | 30 | export function Dance4() { 31 | return renderEffect("dance.4"); 32 | } 33 | 34 | export function Spotlight() { 35 | return renderEffect("1"); 36 | } 37 | 38 | export function Hoverboard() { 39 | return renderEffect("2"); 40 | } 41 | 42 | export function UFO() { 43 | return renderEffect("3"); 44 | } 45 | -------------------------------------------------------------------------------- /src/tools/dump/dumpFigure.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from "fs"; 2 | import * as path from "path"; 3 | 4 | import { ShroomAssetBundle } from "../../assets/ShroomAssetBundle"; 5 | 6 | export async function dumpFigure( 7 | baseName: string, 8 | dumpLocation: string, 9 | imagePaths: string[], 10 | xmlPaths: string[] 11 | ) { 12 | const imageFiles = await Promise.all( 13 | imagePaths.map((path) => 14 | fs.readFile(path).then((buffer) => ({ path, buffer })) 15 | ) 16 | ); 17 | const binFiles = await Promise.all( 18 | xmlPaths.map((path) => 19 | fs.readFile(path).then((buffer) => ({ path, buffer })) 20 | ) 21 | ); 22 | 23 | const file = new ShroomAssetBundle(); 24 | 25 | imageFiles.forEach(({ path: filePath, buffer }) => { 26 | const baseName = path.basename(filePath); 27 | file.addFile(baseName, buffer); 28 | }); 29 | 30 | binFiles.forEach(({ path: filePath, buffer }) => { 31 | const baseName = path.basename(filePath); 32 | file.addFile(baseName, buffer); 33 | }); 34 | 35 | await fs.writeFile(`${dumpLocation}.shroom`, file.toBuffer()); 36 | } 37 | -------------------------------------------------------------------------------- /src/objects/furniture/data/FurnitureIndexData.ts: -------------------------------------------------------------------------------- 1 | import { FurnitureIndexJson } from "./FurnitureIndexJson"; 2 | 3 | export class FurnitureIndexData { 4 | private _visualization: string | undefined; 5 | private _logic: string | undefined; 6 | 7 | public get visualization() { 8 | return this._visualization; 9 | } 10 | 11 | public get logic() { 12 | return this._logic; 13 | } 14 | 15 | constructor(xml: string) { 16 | const document = new DOMParser().parseFromString(xml, "text/xml"); 17 | const object = document.querySelector("object"); 18 | 19 | this._visualization = object?.getAttribute("visualization") ?? undefined; 20 | this._logic = object?.getAttribute("logic") ?? undefined; 21 | } 22 | 23 | static async fromUrl(url: string) { 24 | const response = await fetch(url); 25 | const text = await response.text(); 26 | 27 | return new FurnitureIndexData(text); 28 | } 29 | 30 | toJson(): FurnitureIndexJson { 31 | return this.toObject(); 32 | } 33 | 34 | toObject() { 35 | return { visualization: this.visualization, logic: this.logic }; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /storybook/stories/furniture/FurnitureExamples.stories.ts: -------------------------------------------------------------------------------- 1 | import { renderFurnitureExample } from "./renderFurnitureExample"; 2 | 3 | export default { 4 | title: "Furniture / Examples", 5 | }; 6 | 7 | export function ClubSofa() { 8 | return renderFurnitureExample("club_sofa", { 9 | directions: [0, 2, 4, 6], 10 | spacing: 3, 11 | }); 12 | } 13 | 14 | export function DragonLamp0() { 15 | return renderFurnitureExample("rare_dragonlamp*0", { 16 | directions: [2, 4], 17 | spacing: 3, 18 | animations: ["0", "1"], 19 | }); 20 | } 21 | 22 | export function DragonLamp1() { 23 | return renderFurnitureExample("rare_dragonlamp*1", { 24 | directions: [2, 4], 25 | spacing: 3, 26 | animations: ["0", "1"], 27 | }); 28 | } 29 | 30 | export function GuildGate() { 31 | return renderFurnitureExample("gld_gate", { 32 | directions: [2, 4], 33 | spacing: 3, 34 | animations: ["0", "1"], 35 | }); 36 | } 37 | 38 | export function RareDaffodilRug() { 39 | return renderFurnitureExample("rare_daffodil_rug", { 40 | directions: [0, 2, 4, 6], 41 | spacing: 3, 42 | }); 43 | } 44 | -------------------------------------------------------------------------------- /src/objects/avatar/util/getEffectSprite.ts: -------------------------------------------------------------------------------- 1 | import { IAvatarOffsetsData } from "../data/interfaces/IAvatarOffsetsData"; 2 | import { getBasicFlippedMetaData } from "./getFlippedMetaData"; 3 | import { getSpriteId } from "../structure/AvatarEffectPart"; 4 | 5 | export function getEffectSprite( 6 | member: string, 7 | direction: number, 8 | frame: number, 9 | offsetsData: IAvatarOffsetsData, 10 | hasDirection: boolean, 11 | handleFlipped: boolean 12 | ) { 13 | let id = getSpriteId(member, direction, frame); 14 | let offsets = offsetsData.getOffsets(id); 15 | let flip = false; 16 | 17 | if (handleFlipped && offsets == null) { 18 | const flippedMeta = getBasicFlippedMetaData(direction); 19 | 20 | id = getSpriteId(member, flippedMeta.direction, frame); 21 | offsets = offsetsData.getOffsets(id); 22 | flip = flippedMeta.flip; 23 | } 24 | 25 | if (!hasDirection) { 26 | id = getSpriteId(member, 0, frame); 27 | 28 | if (offsets == null) { 29 | offsets = offsetsData.getOffsets(id); 30 | } 31 | } 32 | 33 | return { 34 | id, 35 | offsets, 36 | flip, 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /e2e/src/tests/room/models/renderModelM.ts: -------------------------------------------------------------------------------- 1 | import { renderModel } from "./util/renderModel"; 2 | 3 | export const renderModelM = renderModel(`xxxxxxxxxxxxxxxxxxxxxxxxxxxxx 4 | xxxxxxxxxxx00000000xxxxxxxxxx 5 | xxxxxxxxxxx00000000xxxxxxxxxx 6 | xxxxxxxxxxx00000000xxxxxxxxxx 7 | xxxxxxxxxxx00000000xxxxxxxxxx 8 | xxxxxxxxxxx00000000xxxxxxxxxx 9 | xxxxxxxxxxx00000000xxxxxxxxxx 10 | xxxxxxxxxxx00000000xxxxxxxxxx 11 | xxxxxxxxxxx00000000xxxxxxxxxx 12 | xxxxxxxxxxx00000000xxxxxxxxxx 13 | xxxxxxxxxxx00000000xxxxxxxxxx 14 | x0000000000000000000000000000 15 | x0000000000000000000000000000 16 | x0000000000000000000000000000 17 | x0000000000000000000000000000 18 | 00000000000000000000000000000 19 | x0000000000000000000000000000 20 | x0000000000000000000000000000 21 | x0000000000000000000000000000 22 | xxxxxxxxxxx00000000xxxxxxxxxx 23 | xxxxxxxxxxx00000000xxxxxxxxxx 24 | xxxxxxxxxxx00000000xxxxxxxxxx 25 | xxxxxxxxxxx00000000xxxxxxxxxx 26 | xxxxxxxxxxx00000000xxxxxxxxxx 27 | xxxxxxxxxxx00000000xxxxxxxxxx 28 | xxxxxxxxxxx00000000xxxxxxxxxx 29 | xxxxxxxxxxx00000000xxxxxxxxxx 30 | xxxxxxxxxxx00000000xxxxxxxxxx 31 | xxxxxxxxxxx00000000xxxxxxxxxx 32 | xxxxxxxxxxxxxxxxxxxxxxxxxxxxx`); 33 | -------------------------------------------------------------------------------- /src/interfaces/IRoomContext.ts: -------------------------------------------------------------------------------- 1 | import { IEventManager } from "../objects/events/interfaces/IEventManager"; 2 | import { ILandscapeContainer } from "../objects/room/ILandscapeContainer"; 3 | import { Room } from "../objects/room/Room"; 4 | import { IAnimationTicker } from "./IAnimationTicker"; 5 | import { IAvatarLoader } from "./IAvatarLoader"; 6 | import { IConfiguration } from "./IConfiguration"; 7 | import { IFurnitureLoader } from "./IFurnitureLoader"; 8 | import { IRoomGeometry } from "./IRoomGeometry"; 9 | import { IRoomObjectContainer } from "./IRoomObjectContainer"; 10 | import { IRoomVisualization } from "./IRoomVisualization"; 11 | import { ITileMap } from "./ITileMap"; 12 | 13 | export interface IRoomContext { 14 | geometry: IRoomGeometry; 15 | furnitureLoader: IFurnitureLoader; 16 | avatarLoader: IAvatarLoader; 17 | animationTicker: IAnimationTicker; 18 | visualization: IRoomVisualization; 19 | roomObjectContainer: IRoomObjectContainer; 20 | configuration: IConfiguration; 21 | tilemap: ITileMap; 22 | landscapeContainer: ILandscapeContainer; 23 | application: PIXI.Application; 24 | room: Room; 25 | eventManager: IEventManager; 26 | } 27 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/eslint-recommended", 9 | "plugin:@typescript-eslint/recommended" 10 | ], 11 | parser: '@typescript-eslint/parser', 12 | parserOptions: { 13 | ecmaVersion: 12, 14 | sourceType: 'module', 15 | }, 16 | plugins: [ 17 | '@typescript-eslint', 18 | ], 19 | rules: { 20 | "semi": 2, 21 | "comma-dangle": 0, 22 | "no-undef": 0, 23 | "prefer-const": 2, 24 | "@typescript-eslint/explicit-module-boundary-types": 0, 25 | "@typescript-eslint/no-explicit-any": 0, 26 | "@typescript-eslint/member-ordering": [2], 27 | "@typescript-eslint/naming-convention": [ 28 | 2, 29 | { 30 | "selector": "classProperty", 31 | "format": ["UPPER_CASE"], 32 | "modifiers": ['private', 'static', 'readonly'] 33 | }, 34 | { 35 | "selector": ["classMethod", "classProperty"], 36 | "format": ["camelCase"], 37 | "modifiers": ["private"], 38 | "leadingUnderscore": "require" 39 | } 40 | ], 41 | }, 42 | }; 43 | -------------------------------------------------------------------------------- /src/objects/furniture/visualization/FurnitureVisualization.ts: -------------------------------------------------------------------------------- 1 | import { IFurnitureVisualization } from "../IFurnitureVisualization"; 2 | import { IFurnitureVisualizationView } from "../IFurnitureVisualizationView"; 3 | 4 | export abstract class FurnitureVisualization 5 | implements IFurnitureVisualization { 6 | protected _view: IFurnitureVisualizationView | undefined; 7 | protected _previousView: IFurnitureVisualizationView | undefined; 8 | 9 | protected get view() { 10 | if (this._view == null) throw new Error("No view mounted"); 11 | 12 | return this._view; 13 | } 14 | 15 | protected get previousView() { 16 | return this._previousView; 17 | } 18 | 19 | protected get mounted() { 20 | return this._view != null; 21 | } 22 | 23 | setView(view: IFurnitureVisualizationView): void { 24 | this._previousView = this._view; 25 | this._view = view; 26 | } 27 | 28 | isAnimated(animation = "0"): boolean { 29 | return false; 30 | } 31 | 32 | abstract update(): void; 33 | abstract destroy(): void; 34 | 35 | abstract updateFrame(frame: number): void; 36 | abstract updateDirection(direction: number): void; 37 | abstract updateAnimation(animation: string): void; 38 | } 39 | -------------------------------------------------------------------------------- /src/objects/furniture/util/visualization/parseDirections.ts: -------------------------------------------------------------------------------- 1 | import { Layer } from "./parseLayers"; 2 | import { VisualizationXmlVisualization } from "./VisualizationXml"; 3 | 4 | export function parseDirections( 5 | visualization: VisualizationXmlVisualization, 6 | set: (direction: number, layerMap: Map) => void 7 | ) { 8 | const directions = visualization.directions[0].direction; 9 | const validDirections: number[] = []; 10 | 11 | for (let i = 0; i < directions.length; i++) { 12 | const layerMap = new Map(); 13 | 14 | const directionNumber = Number(directions[i]["$"].id); 15 | const directionLayers = directions[i].layer || []; 16 | 17 | validDirections.push(directionNumber); 18 | 19 | for (let j = 0; j < directionLayers.length; j++) { 20 | const layer = directionLayers[j]["$"]; 21 | 22 | layerMap.set(layer.id, { 23 | zIndex: layer != null && layer.z != null ? Number(layer.z) : undefined, 24 | tag: undefined, 25 | ink: undefined, 26 | alpha: undefined, 27 | ignoreMouse: undefined, 28 | }); 29 | } 30 | 31 | set(directionNumber, layerMap); 32 | } 33 | 34 | return { 35 | validDirections, 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /storybook/stories/furniture/FurnitureIssues.stories.ts: -------------------------------------------------------------------------------- 1 | import * as PIXI from "pixi.js"; 2 | import { Room, FloorFurniture } from "@jankuss/shroom"; 3 | 4 | import { createShroom } from "../common/createShroom"; 5 | 6 | export default { 7 | title: "Furniture / Issues", 8 | }; 9 | 10 | export function DestroyFurnitureWhileMoving() { 11 | return createShroom(({ application, shroom }) => { 12 | const container = new PIXI.Container(); 13 | application.stage.addChild(container); 14 | 15 | const room = Room.create(shroom, { 16 | tilemap: ` 17 | xxxxxxxxxxx 18 | x0000000000 19 | x0000000000 20 | x0000000000 21 | x0000000000 22 | x0000000000 23 | x0000000000 24 | x0000000000 25 | x0000000000 26 | `, 27 | }); 28 | 29 | const furniture = new FloorFurniture({ 30 | roomX: 1, 31 | roomY: 1, 32 | roomZ: 0, 33 | animation: "0", 34 | direction: 2, 35 | id: 8434, 36 | }); 37 | 38 | room.addRoomObject(furniture); 39 | 40 | setTimeout(() => { 41 | furniture.move(3, 2, 0); 42 | furniture.destroy(); 43 | }, 2000); 44 | 45 | application.stage.addChild(room); 46 | }); 47 | } 48 | -------------------------------------------------------------------------------- /docs/docs/guides/adding-objects.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: adding-objects 3 | title: Adding objects 4 | --- 5 | 6 | The room is so empty right now. Let's add a character and a sofa for him to sit on. 7 | We do this by using the `FloorFurniture` and `Avatar` classes. 8 | 9 | ```ts 10 | import * as PIXI from "pixi.js"; 11 | 12 | import { Room, FloorFurniture, Avatar, Shroom } from "@jankuss/shroom"; 13 | 14 | const view = document.querySelector("#root") as HTMLCanvasElement; 15 | const application = new PIXI.Application({ view }); 16 | 17 | const shroom = Shroom.create({ application, resourcePath: "./resources" }); 18 | const room = Room.create(shroom, { 19 | tilemap: ` 20 | xxxxx 21 | x0000 22 | x0000 23 | x0000 24 | `, 25 | }); 26 | 27 | const furni = new FloorFurniture({ 28 | roomX: 0, 29 | roomY: 0, 30 | roomZ: 0, 31 | direction: 4, 32 | type: "club_sofa", 33 | }); 34 | 35 | const avatar = new Avatar({ 36 | look: "hd-180-1.hr-100-61.ch-210-66.lg-280-110.sh-305-62", 37 | direction: 4, 38 | roomX: 0, 39 | roomY: 0, 40 | roomZ: 0, 41 | }); 42 | 43 | avatar.action = "sit"; 44 | 45 | room.addRoomObject(furni); 46 | room.addRoomObject(avatar); 47 | 48 | room.x = 100; 49 | room.y = 200; 50 | 51 | application.stage.addChild(room); 52 | ``` 53 | -------------------------------------------------------------------------------- /storybook/stories/avatar/renderAvatarDirections.ts: -------------------------------------------------------------------------------- 1 | import { Room, Avatar } from "@jankuss/shroom"; 2 | import { createShroom } from "../common/createShroom"; 3 | 4 | const directions = [0, 1, 2, 3, 4, 5, 6, 7]; 5 | 6 | export function renderAvatarDirections( 7 | look: string, 8 | dirs = directions, 9 | callback: (avatar: Avatar) => void 10 | ) { 11 | return createShroom(({ application, shroom }) => { 12 | const room = Room.create(shroom, { 13 | tilemap: ` 14 | xxxxxxxxxxxxxxxx 15 | x000000000000000 16 | x000000000000000 17 | x000000000000000 18 | `, 19 | }); 20 | 21 | const avatars: Avatar[] = []; 22 | 23 | for (let y = 1; y <= 1; y++) { 24 | for (let x = 0; x < dirs.length; x++) { 25 | const direction = dirs[x]; 26 | 27 | const avatar = new Avatar({ 28 | look: look, 29 | direction: direction, 30 | roomX: x * 2 + 1, 31 | roomY: y * 2 - 1, 32 | roomZ: 0, 33 | }); 34 | 35 | callback(avatar); 36 | 37 | room.addRoomObject(avatar); 38 | avatars.push(avatar); 39 | } 40 | } 41 | 42 | application.stage.addChild(room); 43 | }); 44 | } 45 | -------------------------------------------------------------------------------- /src/objects/avatar/data/ManifestLibrary.ts: -------------------------------------------------------------------------------- 1 | import { IAssetBundle } from "../../../assets/IAssetBundle"; 2 | import { HitTexture } from "../../hitdetection/HitTexture"; 3 | import { AvatarManifestData } from "./AvatarManifestData"; 4 | import { IAvatarManifestData } from "./interfaces/IAvatarManifestData"; 5 | import { IManifestLibrary } from "./interfaces/IManifestLibrary"; 6 | 7 | export class ManifestLibrary implements IManifestLibrary { 8 | private _manifest: Promise; 9 | private _map: Map> = new Map(); 10 | 11 | constructor(private _bundle: IAssetBundle) { 12 | this._manifest = _bundle.getString("manifest.bin").then((manifest) => { 13 | return new AvatarManifestData(manifest); 14 | }); 15 | } 16 | 17 | async getManifest(): Promise { 18 | return this._manifest; 19 | } 20 | 21 | async getTexture(name: string): Promise { 22 | const current = this._map.get(name); 23 | if (current != null) return current; 24 | 25 | const value = this._bundle 26 | .getBlob(`${name}.png`) 27 | .then((blob) => HitTexture.fromBlob(blob)) 28 | .catch(() => undefined); 29 | 30 | this._map.set(name, value); 31 | 32 | return value; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/objects/furniture/data/FurnitureVisualizationJson.ts: -------------------------------------------------------------------------------- 1 | import { 2 | FurnitureAnimationLayer, 3 | FurnitureDirectionLayer, 4 | FurnitureLayer, 5 | } from "./interfaces/IFurnitureVisualizationData"; 6 | 7 | export interface FurnitureVisualizationJson { 8 | [size: string]: { 9 | layerCount: number; 10 | layers: FurnitureLayersJson; 11 | directions: FurnitureDirectionsJson; 12 | colors: FurnitureColorsJson; 13 | animations: FurnitureAnimationsJson; 14 | }; 15 | } 16 | 17 | export type FurnitureLayersJson = { 18 | [id: string]: FurnitureLayer | undefined; 19 | }; 20 | 21 | export type FurnitureDirectionsJson = { 22 | [id: string]: 23 | | { 24 | layers: { 25 | [id: string]: FurnitureDirectionLayer | undefined; 26 | }; 27 | } 28 | | undefined; 29 | }; 30 | 31 | export type FurnitureColorsJson = { 32 | [id: string]: 33 | | { 34 | layers: { 35 | [id: string]: { color: string } | undefined; 36 | }; 37 | } 38 | | undefined; 39 | }; 40 | 41 | export type FurnitureAnimationsJson = { 42 | [id: string]: 43 | | { 44 | layers: { 45 | [id: string]: FurnitureAnimationLayer | undefined; 46 | }; 47 | transitionTo?: number; 48 | } 49 | | undefined; 50 | }; 51 | -------------------------------------------------------------------------------- /src/objects/room/RoomObjectContainer.ts: -------------------------------------------------------------------------------- 1 | import { IRoomContext } from "../../interfaces/IRoomContext"; 2 | import { IRoomObject } from "../../interfaces/IRoomObject"; 3 | import { IRoomObjectContainer } from "../../interfaces/IRoomObjectContainer"; 4 | 5 | export class RoomObjectContainer implements IRoomObjectContainer { 6 | private _roomObjects: Set = new Set(); 7 | private _context: IRoomContext | undefined; 8 | 9 | public get roomObjects(): ReadonlySet { 10 | return this._roomObjects; 11 | } 12 | 13 | public get context() { 14 | return this._context; 15 | } 16 | 17 | public set context(value) { 18 | this._context = value; 19 | } 20 | 21 | addRoomObject(object: IRoomObject) { 22 | if (this._context == null) 23 | throw new Error("Context wasn't supplied to RoomObjectContainer"); 24 | 25 | if (this._roomObjects.has(object)) { 26 | // The object already exists in this room. 27 | return; 28 | } 29 | 30 | object.setParent(this._context); 31 | 32 | this._roomObjects.add(object); 33 | } 34 | 35 | removeRoomObject(object: IRoomObject) { 36 | if (!this._roomObjects.has(object)) { 37 | return; 38 | } 39 | 40 | this._roomObjects.delete(object); 41 | 42 | object.destroy(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/objects/room/util/createPlaneMatrix.ts: -------------------------------------------------------------------------------- 1 | import * as PIXI from "pixi.js"; 2 | 3 | type PlanePoints = { 4 | a: { x: number; y: number }; 5 | b: { x: number; y: number }; 6 | c: { x: number; y: number }; 7 | d: { x: number; y: number }; 8 | }; 9 | 10 | export function createPlaneMatrix( 11 | points: PlanePoints, 12 | { 13 | width, 14 | height, 15 | x, 16 | y, 17 | }: { width: number; height: number; x: number; y: number } 18 | ) { 19 | let diffDxCx = points.d.x - points.c.x; 20 | let diffDyCy = points.d.y - points.c.y; 21 | let diffBxCx = points.b.x - points.c.x; 22 | let diffByCy = points.b.y - points.c.y; 23 | 24 | if (Math.abs(diffBxCx - width) <= 1) { 25 | diffBxCx = width; 26 | } 27 | if (Math.abs(diffByCy - width) <= 1) { 28 | diffByCy = width; 29 | } 30 | if (Math.abs(diffDxCx - height) <= 1) { 31 | diffDxCx = height; 32 | } 33 | if (Math.abs(diffDyCy - height) <= 1) { 34 | diffDyCy = height; 35 | } 36 | 37 | const a = diffBxCx / width; 38 | const b = diffByCy / width; 39 | const c = diffDxCx / height; 40 | const d = diffDyCy / height; 41 | 42 | const baseX = x + points.c.x; 43 | const baseY = y + points.c.y; 44 | 45 | const matrix: PIXI.Matrix = new PIXI.Matrix(a, b, c, d, baseX, baseY); 46 | 47 | return matrix; 48 | } 49 | -------------------------------------------------------------------------------- /docs/docusaurus.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | url: 'https://jankuss.github.io', // Your website URL 3 | baseUrl: '/shroom/', 4 | projectName: 'shroom', 5 | organizationName: 'jankuss', 6 | title: 'Shroom', 7 | tagline: 'Room Rendering Engine for Retros', 8 | onBrokenLinks: 'throw', 9 | favicon: 'img/favicon.ico', 10 | themeConfig: { 11 | navbar: { 12 | title: 'Shroom', 13 | logo: { 14 | alt: 'My Site Logo', 15 | src: 'img/logo.svg', 16 | }, 17 | items: [ 18 | { 19 | to: 'docs/', 20 | activeBasePath: 'docs', 21 | label: 'Docs', 22 | position: 'left', 23 | }, 24 | { 25 | href: 'https://github.com/jankuss/shroom', 26 | label: 'GitHub', 27 | position: 'right', 28 | }, 29 | ], 30 | }, 31 | }, 32 | presets: [ 33 | [ 34 | '@docusaurus/preset-classic', 35 | { 36 | docs: { 37 | sidebarPath: require.resolve('./sidebars.js'), 38 | // Please change this to your repo. 39 | editUrl: 40 | 'https://github.com/jankuss/shroom/edit/master/docs/', 41 | }, 42 | theme: { 43 | customCss: require.resolve('./src/css/custom.css'), 44 | }, 45 | }, 46 | ], 47 | ], 48 | }; 49 | -------------------------------------------------------------------------------- /src/tools/dump/downloadFileWithMessage.ts: -------------------------------------------------------------------------------- 1 | import { 2 | downloadFile, 3 | DownloadFileResult, 4 | DownloadRequest, 5 | } from "./downloadFile"; 6 | import { Logger } from "./Logger"; 7 | 8 | export async function downloadFileWithMessage( 9 | request: DownloadRequest, 10 | logger: Logger 11 | ) { 12 | const downloadedFile = await downloadFile(request); 13 | const message = getDownloadMessage(request, downloadedFile); 14 | 15 | switch (downloadedFile.type) { 16 | case "SUCCESS": 17 | logger.debug(message); 18 | break; 19 | case "FAILED_TO_WRITE": 20 | logger.error(message); 21 | break; 22 | case "HTTP_ERROR": 23 | logger.error(message); 24 | break; 25 | } 26 | 27 | return downloadedFile; 28 | } 29 | 30 | export function getDownloadMessage( 31 | request: DownloadRequest, 32 | result: DownloadFileResult 33 | ) { 34 | switch (result.type) { 35 | case "SUCCESS": 36 | return `Downloaded - ${request.savePath}`; 37 | case "HTTP_ERROR": 38 | return `Error: Failed to download file. Status Code: ${result.code} - ${request.url}`; 39 | case "FAILED_TO_WRITE": 40 | return `Error: Failed to write - ${request.savePath}`; 41 | case "RETRY_FAILED": 42 | return `Error: Failed to download file after retrying. - ${request.url}`; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/objects/room/matrixes.ts: -------------------------------------------------------------------------------- 1 | import { createPlaneMatrix } from "./util/createPlaneMatrix"; 2 | 3 | export function getFloorMatrix(x: number, y: number) { 4 | return createPlaneMatrix( 5 | { 6 | c: { x: 0, y: 16 }, 7 | d: { x: 32, y: 0 }, 8 | a: { x: 64, y: 16 }, 9 | b: { x: 32, y: 32 }, 10 | }, 11 | { width: 32, height: 32, x, y } 12 | ); 13 | } 14 | 15 | export function getLeftMatrix( 16 | x: number, 17 | y: number, 18 | dim: { width: number; height: number } 19 | ) { 20 | return createPlaneMatrix( 21 | { 22 | b: { x: 0, y: 16 }, 23 | c: { x: dim.width, y: 16 + dim.width / 2 }, 24 | d: { x: dim.width, y: 16 + dim.width / 2 + dim.height }, 25 | a: { x: 0, y: 16 + dim.height }, 26 | }, 27 | { width: dim.width, height: dim.height, x, y } 28 | ); 29 | } 30 | 31 | export function getRightMatrix( 32 | x: number, 33 | y: number, 34 | dim: { width: number; height: number } 35 | ) { 36 | return createPlaneMatrix( 37 | { 38 | b: { x: 32, y: 32 }, 39 | c: { x: 32 + dim.width, y: 32 - dim.width / 2 }, 40 | d: { x: 32 + dim.width, y: 32 + dim.height - dim.width / 2 }, 41 | a: { x: 32, y: 32 + dim.height }, 42 | }, 43 | { 44 | width: dim.width, 45 | height: dim.height, 46 | x: x, 47 | y: y, 48 | } 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /src/objects/events/EventEmitter.ts: -------------------------------------------------------------------------------- 1 | export class EventEmitter> { 2 | private _map = new Map>>(); 3 | 4 | addEventListener( 5 | name: K, 6 | callback: EventCallback 7 | ) { 8 | const key = name.toString(); 9 | const currentEventCallbackSet = 10 | this._map.get(key) ?? new Set>(); 11 | currentEventCallbackSet.add(callback); 12 | 13 | this._map.set(key, currentEventCallbackSet); 14 | } 15 | 16 | removeEventListener( 17 | name: K, 18 | callback: EventCallback 19 | ) { 20 | const key = name.toString(); 21 | const currentEventCallbackSet = this._map.get(key); 22 | 23 | if (currentEventCallbackSet != null) { 24 | currentEventCallbackSet.delete(callback); 25 | } 26 | } 27 | 28 | trigger(name: K, value: TMap[K]) { 29 | const key = name.toString(); 30 | const currentEventCallbackSet = this._map.get(key); 31 | 32 | currentEventCallbackSet?.forEach((callback) => callback(value)); 33 | } 34 | } 35 | 36 | type EventCallback> = ( 37 | event: TMap[K] 38 | ) => void; 39 | 40 | window.addEventListener; 41 | 42 | type BaseTypeMap = { 43 | [k in keyof T]: unknown; 44 | }; 45 | -------------------------------------------------------------------------------- /src/objects/room/util/getTileMapBounds.ts: -------------------------------------------------------------------------------- 1 | import { getPosition } from "./getPosition"; 2 | import { ParsedTileType } from "../../../util/parseTileMap"; 3 | 4 | export function getTileMapBounds( 5 | tilemap: ParsedTileType[][], 6 | wallOffsets: { x: number; y: number } 7 | ) { 8 | let minX: number | undefined; 9 | let minY: number | undefined; 10 | 11 | let maxX: number | undefined; 12 | let maxY: number | undefined; 13 | 14 | tilemap.forEach((row, y) => { 15 | row.forEach((column, x) => { 16 | if (column.type !== "tile") return; 17 | const position = getPosition(x, y, column.z, wallOffsets); 18 | const localMaxX = position.x + 64; 19 | const localMaxY = position.y + 32; 20 | 21 | if (minX == null || position.x < minX) { 22 | minX = position.x; 23 | } 24 | 25 | if (minY == null || position.y < minY) { 26 | minY = position.y; 27 | } 28 | 29 | if (maxX == null || localMaxX > maxX) { 30 | maxX = localMaxX; 31 | } 32 | 33 | if (maxY == null || localMaxY > maxY) { 34 | maxY = localMaxY; 35 | } 36 | }); 37 | }); 38 | 39 | if (minX == null || minY == null || maxX == null || maxY == null) { 40 | throw new Error("Couldnt figure out dimensions"); 41 | } 42 | 43 | return { 44 | minX, 45 | minY: minY - 32, 46 | maxX, 47 | maxY: maxY - 32, 48 | }; 49 | } 50 | -------------------------------------------------------------------------------- /src/objects/avatar/util/createLookServer.ts: -------------------------------------------------------------------------------- 1 | import { getAvatarDrawDefinition } from "./getAvatarDrawDefinition"; 2 | import { parseLookString } from "./parseLookString"; 3 | import { AvatarAction } from "../enum/AvatarAction"; 4 | import { IAvatarEffectData } from "../data/interfaces/IAvatarEffectData"; 5 | import { AvatarDependencies } from "../types"; 6 | import { AvatarDrawDefinition } from "../structure/AvatarDrawDefinition"; 7 | 8 | export interface LookOptions { 9 | look: string; 10 | actions: Set; 11 | direction: number; 12 | headDirection?: number; 13 | item?: string | number; 14 | effect?: string; 15 | initial?: boolean; 16 | skipCaching?: boolean; 17 | } 18 | 19 | export interface LookServer { 20 | (options: LookOptions, effect?: IAvatarEffectData): 21 | | AvatarDrawDefinition 22 | | undefined; 23 | } 24 | 25 | export async function createLookServer( 26 | dependencies: AvatarDependencies 27 | ): Promise { 28 | return ( 29 | { look, actions, direction, headDirection, item }: LookOptions, 30 | effect?: IAvatarEffectData 31 | ) => { 32 | return getAvatarDrawDefinition( 33 | { 34 | parsedLook: parseLookString(look), 35 | actions, 36 | direction, 37 | headDirection, 38 | frame: 0, 39 | item: item, 40 | effect: effect, 41 | }, 42 | dependencies 43 | ); 44 | }; 45 | } 46 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shroom-example", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "devDependencies": { 7 | "@babel/core": "^7.12.3", 8 | "@babel/plugin-proposal-class-properties": "^7.12.1", 9 | "@babel/plugin-proposal-nullish-coalescing-operator": "^7.12.1", 10 | "@babel/plugin-proposal-numeric-separator": "^7.12.1", 11 | "@babel/plugin-proposal-optional-chaining": "^7.12.1", 12 | "@babel/preset-env": "^7.12.1", 13 | "@babel/preset-react": "^7.12.1", 14 | "@babel/preset-typescript": "^7.12.1", 15 | "babel-loader": "^8.2.1", 16 | "copy-webpack-plugin": "^6.3.2", 17 | "file-loader": "^6.2.0", 18 | "fork-ts-checker-webpack-plugin": "^6.0.3", 19 | "html-webpack-plugin": "^4.5.0", 20 | "stream-browserify": "^3.0.0", 21 | "typescript": "^4.0.5", 22 | "webpack": "^5.6.0", 23 | "webpack-cli": "^4.2.0", 24 | "webpack-dev-server": "^3.11.0" 25 | }, 26 | "dependencies": { 27 | "@jankuss/shroom": "^0.1.8", 28 | "easystarjs": "^0.4.4", 29 | "events": "^3.2.0", 30 | "pixi.js": "^5.3.3", 31 | "stream": "0.0.2", 32 | "timers": "^0.1.1" 33 | }, 34 | "scripts": { 35 | "dev": "webpack serve", 36 | "build": "webpack", 37 | "dump": "shroom dump --url https://www.habbo.com/gamedata/external_variables/326b0a1abf9e2571d541ac05e6eb3173b83bddea --location ./public/resources" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/objects/avatar/data/AvatarEffectMap.ts: -------------------------------------------------------------------------------- 1 | import { traverseDOMTree } from "../../../util/traverseDOMTree"; 2 | import { AvatarEffect, IAvatarEffectMap } from "./interfaces/IAvatarEffectMap"; 3 | 4 | export class AvatarEffectMap implements IAvatarEffectMap { 5 | private _effects: Map = new Map(); 6 | private _effectsArray: AvatarEffect[] = []; 7 | 8 | constructor(string: string) { 9 | const document = new DOMParser().parseFromString(string, "text/xml"); 10 | 11 | document.querySelectorAll("effect").forEach((element) => { 12 | const effect = this._getEffectFromElement(element); 13 | this._effects.set(effect.id, effect); 14 | this._effectsArray.push(effect); 15 | }); 16 | } 17 | 18 | getEffects(): AvatarEffect[] { 19 | return this._effectsArray; 20 | } 21 | 22 | getEffectInfo(id: string): AvatarEffect | undefined { 23 | return this._effects.get(id); 24 | } 25 | 26 | private _getEffectFromElement(element: Element) { 27 | const id = element.getAttribute("id"); 28 | const lib = element.getAttribute("lib"); 29 | const type = element.getAttribute("type"); 30 | 31 | if (lib == null) throw new Error("Invalid lib for effect"); 32 | if (type == null) throw new Error("Invalid type for effect"); 33 | if (id == null) throw new Error("Invalid id"); 34 | 35 | return { 36 | id, 37 | lib, 38 | type, 39 | }; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/util/tilemap/getRowWalls.ts: -------------------------------------------------------------------------------- 1 | import { TileType } from "../../types/TileType"; 2 | import { getTileInfo } from "../getTileInfo"; 3 | 4 | export type RowWall = { 5 | startY: number; 6 | endY: number; 7 | x: number; 8 | height: number; 9 | }; 10 | 11 | export function getRowWalls(tilemap: TileType[][]) { 12 | let lastY = tilemap.length - 1; 13 | 14 | let wallEndY: number | undefined; 15 | let wallStartY: number | undefined; 16 | let height: number | undefined; 17 | 18 | const walls: RowWall[] = []; 19 | 20 | for (let x = 0; x < tilemap[0].length; x++) { 21 | for (let y = lastY; y >= 0; y--) { 22 | const current = getTileInfo(tilemap, x, y); 23 | 24 | if (current.rowEdge && !current.rowDoor) { 25 | if (wallEndY == null) { 26 | wallEndY = y; 27 | } 28 | 29 | wallStartY = y; 30 | lastY = y - 1; 31 | 32 | if (height == null || (current.height ?? 0) < height) { 33 | height = current.height; 34 | } 35 | } else { 36 | if (wallEndY != null && wallStartY != null) { 37 | walls.push({ 38 | startY: wallStartY, 39 | endY: wallEndY, 40 | x: x - 1, 41 | height: height ?? 0, 42 | }); 43 | wallEndY = undefined; 44 | wallStartY = undefined; 45 | height = undefined; 46 | } 47 | } 48 | } 49 | } 50 | 51 | return walls; 52 | } 53 | -------------------------------------------------------------------------------- /src/util/tilemap/getColumnWalls.ts: -------------------------------------------------------------------------------- 1 | import { TileType } from "../../types/TileType"; 2 | import { getTileInfo } from "../getTileInfo"; 3 | 4 | export type ColumnWall = { 5 | startX: number; 6 | endX: number; 7 | y: number; 8 | height: number; 9 | }; 10 | 11 | export function getColumnWalls(tilemap: TileType[][]) { 12 | let lastX = tilemap[0].length - 1; 13 | 14 | let wallEndX: number | undefined; 15 | let wallStartX: number | undefined; 16 | let height: number | undefined; 17 | 18 | const walls: ColumnWall[] = []; 19 | 20 | for (let y = 0; y < tilemap.length; y++) { 21 | for (let x = lastX; x >= 0; x--) { 22 | const current = getTileInfo(tilemap, x, y); 23 | 24 | if (current.colEdge && !current.rowDoor) { 25 | if (wallEndX == null) { 26 | wallEndX = x; 27 | } 28 | 29 | wallStartX = x; 30 | lastX = x - 1; 31 | if (height == null || (current.height ?? 0) < height) { 32 | height = current.height; 33 | } 34 | } else { 35 | if (wallEndX != null && wallStartX != null) { 36 | walls.push({ 37 | startX: wallStartX, 38 | endX: wallEndX, 39 | y: y - 1, 40 | height: height ?? 0, 41 | }); 42 | wallEndX = undefined; 43 | wallStartX = undefined; 44 | height = undefined; 45 | } 46 | } 47 | } 48 | } 49 | 50 | return walls; 51 | } 52 | -------------------------------------------------------------------------------- /src/objects/avatar/util/getLibrariesForLook.ts: -------------------------------------------------------------------------------- 1 | import { IFigureData } from "../data/interfaces/IFigureData"; 2 | import { IFigureMapData } from "../data/interfaces/IFigureMapData"; 3 | import { ParsedLook } from "./parseLookString"; 4 | 5 | export function getLibrariesForLook( 6 | look: ParsedLook, 7 | { 8 | figureMap, 9 | figureData, 10 | }: { figureMap: IFigureMapData; figureData: IFigureData } 11 | ): Set { 12 | const libraries = new Set(); 13 | 14 | const figureParts = Array.from(look).flatMap(([setType, { setId }]) => { 15 | return ( 16 | figureData 17 | .getParts(setType, setId.toString()) 18 | ?.map((part) => ({ ...part, setId, setType })) ?? [] 19 | ); 20 | }); 21 | 22 | for (const part of figureParts) { 23 | let libraryId = figureMap.getLibraryOfPart(part.id, part.type); 24 | if (libraryId != null) { 25 | const checkParts = 26 | figureData.getParts(part.setType, part.setId.toString()) ?? []; 27 | 28 | for (const checkPart of checkParts) { 29 | libraryId = figureMap.getLibraryOfPart(checkPart.id, checkPart.type); 30 | if (libraryId != null) break; 31 | } 32 | } 33 | 34 | if (libraryId != null) { 35 | libraries.add(libraryId); 36 | } 37 | } 38 | 39 | // Add base libraries 40 | libraries.add("hh_human_face"); 41 | libraries.add("hh_human_item"); 42 | libraries.add("hh_human_body"); 43 | 44 | return libraries; 45 | } 46 | -------------------------------------------------------------------------------- /src/objects/furniture/filter/HighlightFilter.ts: -------------------------------------------------------------------------------- 1 | import * as PIXI from "pixi.js"; 2 | 3 | export class HighlightFilter extends PIXI.Filter { 4 | constructor(private _backgroundColor: number, private _borderColor: number) { 5 | super(vertex, fragment); 6 | this.uniforms.backgroundColor = new Float32Array(4); 7 | this.uniforms.borderColor = new Float32Array(4); 8 | 9 | this.uniforms.backgroundColor = [ 10 | ...PIXI.utils.hex2rgb(this._backgroundColor), 11 | 1.0, 12 | ]; 13 | this.uniforms.borderColor = [...PIXI.utils.hex2rgb(this._borderColor), 1.0]; 14 | } 15 | } 16 | 17 | const vertex = ` 18 | attribute vec2 aVertexPosition; 19 | attribute vec2 aTextureCoord; 20 | 21 | uniform mat3 projectionMatrix; 22 | 23 | varying vec2 vTextureCoord; 24 | 25 | void main(void) 26 | { 27 | gl_Position = vec4((projectionMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); 28 | vTextureCoord = aTextureCoord; 29 | } 30 | `; 31 | 32 | const fragment = ` 33 | varying vec2 vTextureCoord; 34 | uniform sampler2D uSampler; 35 | uniform vec4 backgroundColor; 36 | uniform vec4 borderColor; 37 | 38 | void main(void) { 39 | vec4 currentColor = texture2D(uSampler, vTextureCoord); 40 | 41 | if (currentColor.a > 0.0) { 42 | if (currentColor.r == 0.0 && currentColor.g == 0.0 && currentColor.b == 0.0) { 43 | gl_FragColor = borderColor; 44 | } else { 45 | gl_FragColor = backgroundColor; 46 | } 47 | } 48 | } 49 | `; 50 | -------------------------------------------------------------------------------- /src/objects/animation/AnimationTicker.ts: -------------------------------------------------------------------------------- 1 | import { IAnimationTicker } from "../../interfaces/IAnimationTicker"; 2 | 3 | const ANIM_FPS = 24; 4 | const TARGET_FPS = 60; 5 | 6 | export class AnimationTicker implements IAnimationTicker { 7 | private _frame = 0; 8 | 9 | private _idCounter = 0; 10 | private _subscriptions = new Map< 11 | number, 12 | (frame: number, accurateFrame: number) => void 13 | >(); 14 | 15 | constructor(application: PIXI.Application) { 16 | application.ticker.maxFPS = TARGET_FPS; 17 | application.ticker.minFPS = ANIM_FPS; 18 | application.ticker.add(() => this._increment()); 19 | } 20 | 21 | static create(application: PIXI.Application) { 22 | return new AnimationTicker(application); 23 | } 24 | 25 | subscribe(cb: (frame: number, accurateFrame: number) => void): () => void { 26 | const id = this._idCounter++; 27 | 28 | this._subscriptions.set(id, cb); 29 | return () => this._subscriptions.delete(id); 30 | } 31 | 32 | current(): number { 33 | return this._getNormalizedFrame(this._frame).rounded; 34 | } 35 | 36 | private _getNormalizedFrame(frame: number) { 37 | const factor = ANIM_FPS / TARGET_FPS; 38 | const calculatedFrame = frame * factor; 39 | 40 | return { rounded: Math.floor(calculatedFrame), pure: calculatedFrame }; 41 | } 42 | 43 | private _increment() { 44 | this._frame += 1; 45 | const data = this._getNormalizedFrame(this._frame); 46 | 47 | this._subscriptions.forEach((cb) => cb(data.rounded, data.pure)); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/objects/room/RoomObjectContainer.test.ts: -------------------------------------------------------------------------------- 1 | import { RoomObject } from "../RoomObject"; 2 | import { RoomObjectContainer } from "./RoomObjectContainer"; 3 | 4 | test("adds & removes room objects", () => { 5 | class TestObject extends RoomObject { 6 | destroyed(): void { 7 | // Do nothing 8 | } 9 | registered(): void { 10 | // Do nothing 11 | } 12 | } 13 | 14 | const roomObjectContainer = new RoomObjectContainer(); 15 | 16 | roomObjectContainer.context = { 17 | roomObjectContainer: roomObjectContainer, 18 | } as any; 19 | 20 | const object = new TestObject(); 21 | roomObjectContainer.addRoomObject(object); 22 | expect(roomObjectContainer.roomObjects.has(object)).toBe(true); 23 | roomObjectContainer.removeRoomObject(object); 24 | expect(roomObjectContainer.roomObjects.has(object)).toBe(false); 25 | }); 26 | 27 | test("destroy on RoomObject removes room object from RoomObjectContainer", () => { 28 | class TestObject extends RoomObject { 29 | destroyed(): void { 30 | // Do nothing 31 | } 32 | registered(): void { 33 | // Do nothing 34 | } 35 | } 36 | 37 | const roomObjectContainer = new RoomObjectContainer(); 38 | 39 | roomObjectContainer.context = { 40 | roomObjectContainer: roomObjectContainer, 41 | } as any; 42 | 43 | const object = new TestObject(); 44 | roomObjectContainer.addRoomObject(object); 45 | expect(roomObjectContainer.roomObjects.has(object)).toBe(true); 46 | object.destroy(); 47 | expect(roomObjectContainer.roomObjects.has(object)).toBe(false); 48 | }); 49 | -------------------------------------------------------------------------------- /src/objects/room/parts/WallOuterCorner.ts: -------------------------------------------------------------------------------- 1 | import * as PIXI from "pixi.js"; 2 | 3 | import { IRoomPart } from "./IRoomPart"; 4 | import { RoomPartData } from "./RoomPartData"; 5 | 6 | export class WallOuterCorner extends PIXI.Container implements IRoomPart { 7 | private _borderWidth = 0; 8 | private _wallHeight = 0; 9 | private _roomZ = 0; 10 | private _wallTopColor = 0; 11 | 12 | constructor() { 13 | super(); 14 | } 15 | 16 | public get roomZ() { 17 | return this._roomZ; 18 | } 19 | 20 | public set roomZ(value) { 21 | this._roomZ = value; 22 | this._update(); 23 | } 24 | 25 | public get wallY() { 26 | return -this._wallHeight; 27 | } 28 | 29 | update(data: RoomPartData): void { 30 | this._borderWidth = data.borderWidth; 31 | this._wallHeight = data.wallHeight; 32 | this._wallTopColor = data.wallTopColor; 33 | this._update(); 34 | } 35 | 36 | private _createTopSprite() { 37 | const border = new PIXI.TilingSprite( 38 | PIXI.Texture.WHITE, 39 | this._borderWidth, 40 | this._borderWidth 41 | ); 42 | border.transform.setFromMatrix(new PIXI.Matrix(1, 0.5, 1, -0.5)); 43 | border.tint = this._wallTopColor; 44 | border.x = -this._borderWidth; 45 | border.y = 46 | -this._wallHeight + 47 | this.roomZ * 32 - 48 | 32 / 2 + 49 | this._borderWidth / 2 + 50 | (32 - this._borderWidth); 51 | return border; 52 | } 53 | 54 | private _update() { 55 | this.removeChildren(); 56 | 57 | this.addChild(this._createTopSprite()); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /storybook/stories/furniture/renderFurnitureExample.ts: -------------------------------------------------------------------------------- 1 | import { Room, FloorFurniture } from "@jankuss/shroom"; 2 | import { createShroom } from "../common/createShroom"; 3 | 4 | export function renderFurnitureExample( 5 | type: string, 6 | { 7 | animations = ["0"], 8 | spacing = 2, 9 | directions, 10 | }: { 11 | directions: number[]; 12 | animations?: string[]; 13 | spacing: number; 14 | }, 15 | cb: (furniture: FloorFurniture) => void = () => { 16 | /* Do nothing */ 17 | } 18 | ) { 19 | return createShroom(({ shroom, application }) => { 20 | const room = Room.create(shroom, { 21 | tilemap: ` 22 | xxxxxxxxxxxxxxxx 23 | x000000000000000 24 | x000000000000000 25 | x000000000000000 26 | x000000000000000 27 | x000000000000000 28 | `, 29 | }); 30 | 31 | let y = 0; 32 | 33 | for (const animation of animations) { 34 | let x = 0; 35 | 36 | for (const direction of directions) { 37 | const furniture = new FloorFurniture({ 38 | roomX: 1 + x * spacing, 39 | roomY: 1 + y * spacing, 40 | roomZ: 0, 41 | direction, 42 | type, 43 | animation: animation, 44 | }); 45 | 46 | room.addRoomObject(furniture); 47 | cb(furniture); 48 | x++; 49 | } 50 | 51 | y++; 52 | } 53 | 54 | application.stage.addChild(room); 55 | }); 56 | } 57 | -------------------------------------------------------------------------------- /example/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as PIXI from "pixi.js"; 2 | 3 | import { 4 | Room, 5 | Avatar, 6 | FloorFurniture, 7 | RoomCamera, 8 | Shroom, 9 | loadRoomTexture, 10 | } from "@jankuss/shroom"; 11 | 12 | const view = document.querySelector("#root") as HTMLCanvasElement | undefined; 13 | const container = document.querySelector("#container") as 14 | | HTMLDivElement 15 | | undefined; 16 | if (view == null || container == null) throw new Error("Invalid view"); 17 | 18 | const application = new PIXI.Application({ 19 | view, 20 | antialias: false, 21 | resolution: window.devicePixelRatio, 22 | autoDensity: true, 23 | width: 1200, 24 | height: 900, 25 | backgroundColor: 0x000000, 26 | }); 27 | 28 | PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; 29 | 30 | const shroom = Shroom.create({ 31 | application, 32 | resourcePath: "./resources", 33 | configuration: { placeholder: PIXI.Texture.from("./image.png") }, 34 | }); 35 | const room = Room.create(shroom, { 36 | tilemap: ` 37 | xxxxx 38 | x0000 39 | x0000 40 | x0000 41 | `, 42 | }); 43 | 44 | const avatar = new Avatar({ 45 | look: "hd-180-1.hr-100-61.ch-210-66.lg-280-110.sh-305-62", 46 | direction: 4, 47 | roomX: 0, 48 | roomY: 0, 49 | roomZ: 0, 50 | }); 51 | 52 | room.x = 200; 53 | room.y = 200; 54 | 55 | room.wallTexture = loadRoomTexture("./images/tile.png"); 56 | room.floorTexture = loadRoomTexture("./images/tile.png"); 57 | room.wallColor = "#dbbe6e"; 58 | room.floorColor = "#eeeeee"; 59 | 60 | room.addRoomObject(avatar); 61 | application.stage.addChild(RoomCamera.forScreen(room)); 62 | -------------------------------------------------------------------------------- /src/tools/dump/ProgressBar.ts: -------------------------------------------------------------------------------- 1 | import * as readline from "readline"; 2 | import { Logger } from "./Logger"; 3 | 4 | export class ProgressBar implements Logger { 5 | private _count = 0; 6 | private _currentItem: string | undefined; 7 | 8 | constructor( 9 | private _logger: Logger, 10 | private _elementCount: number, 11 | private _getString: ( 12 | current: number, 13 | count: number, 14 | extra?: string 15 | ) => string 16 | ) { 17 | this._update(); 18 | } 19 | 20 | info(...args: string[]): void { 21 | process.stdout.write("\n"); 22 | this._logger.info(...args); 23 | } 24 | 25 | debug(...args: string[]): void { 26 | process.stdout.write("\n"); 27 | this._logger.debug(...args); 28 | } 29 | 30 | error(...args: string[]): void { 31 | process.stdout.write("\n"); 32 | this._logger.error(...args); 33 | } 34 | 35 | log(...args: string[]): void { 36 | process.stdout.write("\n"); 37 | this._logger.log(...args); 38 | } 39 | 40 | public increment(item: string) { 41 | this._currentItem = item; 42 | this._count++; 43 | this._update(); 44 | } 45 | 46 | public done() { 47 | this._currentItem = undefined; 48 | this._update(); 49 | process.stdout.write("\n"); 50 | } 51 | 52 | private _update() { 53 | readline.clearLine(process.stdout, 0); 54 | readline.cursorTo(process.stdout, 0); 55 | const baseText = this._getString( 56 | this._count, 57 | this._elementCount, 58 | this._currentItem 59 | ); 60 | 61 | process.stdout.write(baseText); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /storybook/stories/common/createShroom.tsx: -------------------------------------------------------------------------------- 1 | import * as PIXI from "pixi.js"; 2 | import { Shroom } from "@jankuss/shroom"; 3 | import { useRef } from "react"; 4 | import React from "react"; 5 | 6 | type CleanupFn = () => void; 7 | type CallbackOptions = { 8 | application: PIXI.Application; 9 | shroom: Shroom; 10 | container: HTMLDivElement; 11 | }; 12 | 13 | export function createShroom( 14 | cb: (options: CallbackOptions) => CleanupFn | void 15 | ) { 16 | const App = () => { 17 | const containerRef = useRef(null); 18 | const canvasRef = useRef(null); 19 | 20 | React.useEffect(() => { 21 | const element = canvasRef.current; 22 | const container = containerRef.current; 23 | if (element == null) return; 24 | if (container == null) return; 25 | 26 | const application = new PIXI.Application({ 27 | view: element, 28 | width: 1400, 29 | height: 850, 30 | }); 31 | const shroom = Shroom.create({ 32 | resourcePath: "./resources", 33 | application: application, 34 | configuration: { 35 | placeholder: PIXI.Texture.from("./images/placeholder.png"), 36 | }, 37 | }); 38 | 39 | const cleanup = cb({ application, shroom, container }); 40 | 41 | return () => { 42 | cleanup && cleanup(); 43 | 44 | application.destroy(); 45 | }; 46 | }, []); 47 | 48 | return ( 49 |
50 | 51 |
52 | ); 53 | }; 54 | 55 | return ; 56 | } 57 | -------------------------------------------------------------------------------- /src/tools/dump/getExternalVariableUrls.ts: -------------------------------------------------------------------------------- 1 | import { parseExternalVariables } from "./parseExternalVariables"; 2 | 3 | import fetch from "node-fetch"; 4 | 5 | export async function getExternalVariableUrls( 6 | externalVariablesUrl: string 7 | ): Promise { 8 | const externalVariablesString = await fetch( 9 | externalVariablesUrl 10 | ).then((res) => res.text()); 11 | const parsed = await parseExternalVariables(externalVariablesString); 12 | const figureMapUrl = parsed.get( 13 | "flash.dynamic.avatar.download.configuration" 14 | ); 15 | 16 | const figureDataUrl = parsed.get("external.figurepartlist.txt"); 17 | 18 | const hofFurniUrl = parsed.get("dynamic.download.url"); 19 | 20 | const furniDataUrl = parsed.get("furnidata.load.url"); 21 | 22 | if (figureMapUrl == null) throw new Error("Invalid figure map url"); 23 | if (hofFurniUrl == null) throw new Error("Invalid hof_furni url"); 24 | if (figureDataUrl == null) throw new Error("Invalid figure data url"); 25 | if (furniDataUrl == null) throw new Error("Invalid furni data url"); 26 | 27 | const gordonUrl = figureMapUrl.split("/").slice(0, -1).join("/"); 28 | 29 | const effectMapUrl = `${gordonUrl}/effectmap.xml`; 30 | 31 | return { 32 | figureMapUrl, 33 | hofFurniUrl, 34 | figureDataUrl, 35 | furniDataUrl, 36 | gordonUrl, 37 | effectMapUrl, 38 | }; 39 | } 40 | 41 | export interface ExternalVariables { 42 | figureMapUrl: string; 43 | hofFurniUrl: string; 44 | figureDataUrl: string; 45 | furniDataUrl: string; 46 | gordonUrl: string; 47 | effectMapUrl: string; 48 | } 49 | -------------------------------------------------------------------------------- /src/objects/avatar/data/AvatarPartSetsData.ts: -------------------------------------------------------------------------------- 1 | import { AvatarData } from "./AvatarData"; 2 | import { IAvatarPartSetsData } from "./interfaces/IAvatarPartSetsData"; 3 | import { partsetsXml } from "./static/partsets.xml"; 4 | 5 | export class AvatarPartSetsData 6 | extends AvatarData 7 | implements IAvatarPartSetsData { 8 | constructor(xml: string) { 9 | super(xml); 10 | } 11 | 12 | static async fromUrl(url: string) { 13 | const response = await fetch(url); 14 | const text = await response.text(); 15 | 16 | return new AvatarPartSetsData(text); 17 | } 18 | 19 | static default() { 20 | return new AvatarPartSetsData(atob(partsetsXml)); 21 | } 22 | 23 | getPartInfo( 24 | id: string 25 | ): 26 | | { 27 | removeSetType?: string | undefined; 28 | flippedSetType?: string | undefined; 29 | } 30 | | undefined { 31 | const element = this.querySelector(`partSet part[set-type="${id}"]`); 32 | 33 | if (element == null) return; 34 | 35 | return { 36 | flippedSetType: element.getAttribute("flipped-set-type") ?? undefined, 37 | removeSetType: element.getAttribute("remove-set-type") ?? undefined, 38 | }; 39 | } 40 | 41 | getActivePartSet(id: string) { 42 | const partSet = this.querySelectorAll( 43 | `activePartSet[id="${id}"] activePart` 44 | ); 45 | 46 | return new Set( 47 | partSet.map((value) => { 48 | const setType = value.getAttribute("set-type"); 49 | if (setType == null) throw new Error("Invalid set type"); 50 | 51 | return setType; 52 | }) 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/tools/dump/createOffsetFile.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { IFigureMapData } from "../../objects/avatar/data/interfaces/IFigureMapData"; 3 | import { promises as fs } from "fs"; 4 | import { AvatarManifestData } from "../../objects/avatar/data/AvatarManifestData"; 5 | import { ProgressBar } from "./ProgressBar"; 6 | import { Logger } from "./Logger"; 7 | 8 | export async function createOffsetFile( 9 | downloadPath: string, 10 | figureMap: IFigureMapData, 11 | logger: Logger 12 | ) { 13 | const assets = figureMap.getLibraries(); 14 | const object: { [key: string]: { offsetX: number; offsetY: number } } = {}; 15 | const progress = new ProgressBar( 16 | logger, 17 | assets.length, 18 | (current, count, data) => { 19 | if (data != null) { 20 | return `Figure Offsets: ${current} / ${count} (${data})`; 21 | } else { 22 | return `Figure Offsets: ${current} / ${count}`; 23 | } 24 | } 25 | ); 26 | 27 | for (const asset of assets) { 28 | const manifestPath = path.join( 29 | downloadPath, 30 | "figure", 31 | asset, 32 | "manifest.bin" 33 | ); 34 | const manifestFile = await fs.readFile(manifestPath, "utf-8"); 35 | const manifest = new AvatarManifestData(manifestFile); 36 | 37 | manifest.getAssets().forEach((asset) => { 38 | object[asset.name] = { offsetX: asset.x, offsetY: asset.y }; 39 | }); 40 | 41 | progress.increment(asset); 42 | } 43 | 44 | progress.done(); 45 | 46 | await fs.writeFile( 47 | path.join(downloadPath, "offsets.json"), 48 | JSON.stringify(object) 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /e2e/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shroom-example", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "devDependencies": { 7 | "@babel/core": "^7.12.3", 8 | "@babel/plugin-proposal-class-properties": "^7.12.1", 9 | "@babel/plugin-proposal-nullish-coalescing-operator": "^7.12.1", 10 | "@babel/plugin-proposal-numeric-separator": "^7.12.1", 11 | "@babel/plugin-proposal-optional-chaining": "^7.12.1", 12 | "@babel/preset-env": "^7.12.1", 13 | "@babel/preset-react": "^7.12.1", 14 | "@babel/preset-typescript": "^7.12.1", 15 | "@types/react": "^17.0.0", 16 | "@types/react-dom": "^17.0.0", 17 | "@types/styled-components": "^5.1.7", 18 | "babel-loader": "^8.2.1", 19 | "copy-webpack-plugin": "^6.3.2", 20 | "file-loader": "^6.2.0", 21 | "fork-ts-checker-webpack-plugin": "^6.0.3", 22 | "html-webpack-plugin": "^4.5.0", 23 | "stream-browserify": "^3.0.0", 24 | "typescript": "^4.0.5", 25 | "webpack": "^5.6.0", 26 | "webpack-cli": "^4.2.0", 27 | "webpack-dev-server": "^3.11.0" 28 | }, 29 | "dependencies": { 30 | "@jankuss/shroom": "..", 31 | "easystarjs": "^0.4.4", 32 | "events": "^3.2.0", 33 | "pixi.js": "^5.3.3", 34 | "react": "^17.0.1", 35 | "react-dom": "^17.0.1", 36 | "stream": "0.0.2", 37 | "styled-components": "^5.2.1", 38 | "timers": "^0.1.1" 39 | }, 40 | "scripts": { 41 | "dev": "webpack serve", 42 | "build": "webpack", 43 | "dump": "shroom dump --url https://www.habbo.com/gamedata/external_variables/326b0a1abf9e2571d541ac05e6eb3173b83bddea --location ./public/resources" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/objects/avatar/AvatarEffectBundle.ts: -------------------------------------------------------------------------------- 1 | import { IAssetBundle } from "../../assets/IAssetBundle"; 2 | import { HitTexture } from "../hitdetection/HitTexture"; 3 | import { AvatarEffectData } from "./data/AvatarEffectData"; 4 | import { AvatarManifestData } from "./data/AvatarManifestData"; 5 | import { IAvatarEffectBundle } from "./data/interfaces/IAvatarEffectBundle"; 6 | import { IAvatarEffectData } from "./data/interfaces/IAvatarEffectData"; 7 | import { IAvatarManifestData } from "./data/interfaces/IAvatarManifestData"; 8 | 9 | export class AvatarEffectBundle implements IAvatarEffectBundle { 10 | private _data: Promise; 11 | private _textures: Map> = new Map(); 12 | private _manifest: Promise; 13 | 14 | constructor(private _bundle: IAssetBundle) { 15 | this._data = _bundle 16 | .getString(`animation.bin`) 17 | .then((xml) => new AvatarEffectData(xml)); 18 | 19 | this._manifest = _bundle 20 | .getString(`manifest.bin`) 21 | .then((xml) => new AvatarManifestData(xml)); 22 | } 23 | 24 | async getData(): Promise { 25 | return this._data; 26 | } 27 | 28 | async getTexture(name: string): Promise { 29 | const current = this._textures.get(name); 30 | if (current != null) return current; 31 | 32 | const blob = await this._bundle.getBlob(`${name}.png`); 33 | 34 | const texture = HitTexture.fromBlob(blob); 35 | this._textures.set(name, texture); 36 | 37 | return texture; 38 | } 39 | 40 | async getManifest(): Promise { 41 | return this._manifest; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/objects/furniture/data/interfaces/IFurnitureVisualizationData.ts: -------------------------------------------------------------------------------- 1 | export interface IFurnitureVisualizationData { 2 | getLayerCount(size: number): number; 3 | getLayer(size: number, layerId: number): FurnitureLayer | undefined; 4 | getDirections(size: number): number[]; 5 | getDirectionLayer( 6 | size: number, 7 | direction: number, 8 | layerId: number 9 | ): FurnitureDirectionLayer | undefined; 10 | getAnimationLayer( 11 | size: number, 12 | animationId: number, 13 | id: number 14 | ): FurnitureAnimationLayer | undefined; 15 | getFrameCountWithoutRepeat( 16 | size: number, 17 | animationId: number 18 | ): number | undefined; 19 | getFrameCount(size: number, animationId: number): number | undefined; 20 | getColor(size: number, colorId: number, layerId: number): string | undefined; 21 | getAnimation( 22 | size: number, 23 | animationId: number 24 | ): FurnitureAnimation | undefined; 25 | getTransitionForAnimation( 26 | size: number, 27 | animationId: number 28 | ): FurnitureAnimation | undefined; 29 | } 30 | 31 | export interface FurnitureAnimation { 32 | id: number; 33 | transitionTo?: number; 34 | } 35 | 36 | export interface FurnitureLayer { 37 | id: number; 38 | z: number; 39 | tag?: string; 40 | ignoreMouse?: boolean; 41 | alpha?: number; 42 | ink?: string; 43 | } 44 | 45 | export interface FurnitureDirectionLayer { 46 | x?: number; 47 | y?: number; 48 | z?: number; 49 | } 50 | 51 | export interface FurnitureAnimationLayer { 52 | id: number; 53 | frames: number[]; 54 | frameRepeat?: number; 55 | random?: boolean; 56 | loopCount?: number; 57 | } 58 | -------------------------------------------------------------------------------- /src/objects/avatar/types/index.ts: -------------------------------------------------------------------------------- 1 | import { IAvatarActionsData } from "../data/interfaces/IAvatarActionsData"; 2 | import { IAvatarAnimationData } from "../data/interfaces/IAvatarAnimationData"; 3 | import { IAvatarGeometryData } from "../data/interfaces/IAvatarGeometryData"; 4 | import { IAvatarOffsetsData } from "../data/interfaces/IAvatarOffsetsData"; 5 | import { IAvatarPartSetsData } from "../data/interfaces/IAvatarPartSetsData"; 6 | import { IFigureData } from "../data/interfaces/IFigureData"; 7 | import { IFigureMapData } from "../data/interfaces/IFigureMapData"; 8 | 9 | export type AvatarAsset = { 10 | fileId: string; 11 | x: number; 12 | y: number; 13 | library: string; 14 | mirror: boolean; 15 | substractWidth?: boolean; 16 | }; 17 | 18 | export type AvatarDrawPart = DefaultAvatarDrawPart | AvatarEffectDrawPart; 19 | 20 | export type DefaultAvatarDrawPart = { 21 | kind: "AVATAR_DRAW_PART"; 22 | type: string; 23 | index: number; 24 | mode: "colored" | "just-image"; 25 | color: string | undefined; 26 | assets: AvatarAsset[]; 27 | z: number; 28 | }; 29 | 30 | export type AvatarEffectDrawPart = { 31 | kind: "EFFECT_DRAW_PART"; 32 | assets: AvatarAsset[]; 33 | z: number; 34 | ink?: number; 35 | addition: boolean; 36 | }; 37 | 38 | export interface AvatarDependencies extends AvatarExternalDependencies { 39 | offsetsData: IAvatarOffsetsData; 40 | } 41 | 42 | export interface AvatarExternalDependencies { 43 | figureData: IFigureData; 44 | figureMap: IFigureMapData; 45 | animationData: IAvatarAnimationData; 46 | partSetsData: IAvatarPartSetsData; 47 | geometry: IAvatarGeometryData; 48 | actionsData: IAvatarActionsData; 49 | } 50 | -------------------------------------------------------------------------------- /src/tools/dump/downloadEffects.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { AvatarEffectMap } from "../../objects/avatar/data/AvatarEffectMap"; 3 | import { 4 | downloadFile, 5 | DownloadFileResult, 6 | DownloadRequest, 7 | } from "./downloadFile"; 8 | import { downloadMultipleFiles } from "./downloadMultipleFiles"; 9 | import { Logger } from "./Logger"; 10 | 11 | export async function downloadEffects( 12 | { 13 | downloadPath, 14 | gordonUrl, 15 | effectMapDownload, 16 | }: { 17 | downloadPath: string; 18 | gordonUrl: string; 19 | effectMapDownload: DownloadFileResult; 20 | }, 21 | logger: Logger 22 | ) { 23 | if (effectMapDownload.type !== "SUCCESS") { 24 | logger.info( 25 | "Skipping downloading furniture, since we couldn't download the furniture data." 26 | ); 27 | return; 28 | } 29 | 30 | const effectMap = new AvatarEffectMap(await effectMapDownload.text()); 31 | const libs = effectMap.getEffects().map((effect) => effect.lib); 32 | 33 | await downloadMultipleFiles( 34 | { data: libs, name: "Effects", concurrency: 30, logger }, 35 | async (library, view) => { 36 | const request: DownloadRequest = { 37 | url: `${gordonUrl}/${library}.swf`, 38 | savePath: path.join(downloadPath, `effects`, `${library}.swf`), 39 | }; 40 | 41 | const result = await downloadFile(request); 42 | switch (result.type) { 43 | case "SUCCESS": 44 | view.reportSuccess(library); 45 | break; 46 | 47 | case "HTTP_ERROR": 48 | case "FAILED_TO_WRITE": 49 | view.reportError(library, request, result); 50 | break; 51 | } 52 | } 53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /src/objects/furniture/XmlFurnitureAssetBundle.ts: -------------------------------------------------------------------------------- 1 | import { IAssetBundle } from "../../assets/IAssetBundle"; 2 | import { HitTexture } from "../hitdetection/HitTexture"; 3 | import { FurnitureAssetsData } from "./data/FurnitureAssetsData"; 4 | import { FurnitureIndexData } from "./data/FurnitureIndexData"; 5 | import { FurnitureVisualizationData } from "./data/FurnitureVisualizationData"; 6 | import { IFurnitureAssetsData } from "./data/interfaces/IFurnitureAssetsData"; 7 | import { IFurnitureIndexData } from "./data/interfaces/IFurnitureIndexData"; 8 | import { IFurnitureVisualizationData } from "./data/interfaces/IFurnitureVisualizationData"; 9 | import { IFurnitureAssetBundle } from "./IFurnitureAssetBundle"; 10 | 11 | export class XmlFurnitureAssetBundle implements IFurnitureAssetBundle { 12 | constructor(private _type: string, private _assetBundle: IAssetBundle) {} 13 | 14 | async getAssets(): Promise { 15 | const data = await this._assetBundle.getString(`${this._type}_assets.bin`); 16 | return new FurnitureAssetsData(data); 17 | } 18 | 19 | async getVisualization(): Promise { 20 | const data = await this._assetBundle.getString( 21 | `${this._type}_visualization.bin` 22 | ); 23 | return new FurnitureVisualizationData(data); 24 | } 25 | 26 | async getTexture(name: string): Promise { 27 | const blob = await this._assetBundle.getBlob(`${name}.png`); 28 | 29 | return HitTexture.fromBlob(blob); 30 | } 31 | 32 | async getIndex(): Promise { 33 | const data = await this._assetBundle.getString(`index.bin`); 34 | return new FurnitureIndexData(data); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /e2e/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const webpack = require("webpack"); 3 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 4 | const ForkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin"); 5 | const CopyPlugin = require('copy-webpack-plugin'); 6 | 7 | module.exports = { 8 | entry: "./src/index.tsx", 9 | output: { 10 | filename: "[name].[fullhash].js", 11 | path: path.resolve(__dirname, "dist/webpack"), 12 | }, 13 | devtool: "source-map", 14 | resolve: { 15 | extensions: [".tsx", ".ts", ".js", ".mjs"], 16 | fallback: { "buffer": false, "timers": false } 17 | }, 18 | optimization: { splitChunks: { chunks: 'all' }, runtimeChunk: true, minimizer: [] }, 19 | module: { 20 | rules: [ 21 | { 22 | test: /\.(js|jsx|ts|tsx)$/, 23 | use: [ 24 | { 25 | loader: "babel-loader", 26 | options: { 27 | /* Use `babel.config.js` in root folder */ 28 | rootMode: "upward", 29 | }, 30 | }, 31 | ], 32 | exclude: /node_modules/, 33 | }, 34 | { 35 | test: /\.(png|svg|bmp)$/, 36 | loader: "file-loader", 37 | }, 38 | ], 39 | }, 40 | plugins: [ 41 | new ForkTsCheckerWebpackPlugin({}), 42 | new HtmlWebpackPlugin({ 43 | template: "./src/index.ejs", 44 | }), 45 | new webpack.HotModuleReplacementPlugin(), 46 | new webpack.EnvironmentPlugin({ 47 | NODE_ENV: "development", 48 | }), 49 | ], 50 | devServer: { 51 | contentBase: [path.join(__dirname, 'public')] 52 | } 53 | }; -------------------------------------------------------------------------------- /src/objects/avatar/util/getAvatarDrawDefinition.ts: -------------------------------------------------------------------------------- 1 | import { ParsedLook } from "./parseLookString"; 2 | import { AvatarAction } from "../enum/AvatarAction"; 3 | import { IAvatarEffectData } from "../data/interfaces/IAvatarEffectData"; 4 | import { AvatarFigurePartType } from "../enum/AvatarFigurePartType"; 5 | import { AvatarDrawDefinition } from "../structure/AvatarDrawDefinition"; 6 | import { AvatarDependencies } from "../types"; 7 | 8 | export const basePartSet = new Set([ 9 | AvatarFigurePartType.LeftHand, 10 | AvatarFigurePartType.RightHand, 11 | AvatarFigurePartType.Body, 12 | AvatarFigurePartType.Head, 13 | ]); 14 | 15 | /** 16 | * Returns a definition of how the avatar should be drawn. 17 | * @param options Look options 18 | * @param deps External figure data, draw order and offsets 19 | */ 20 | export function getAvatarDrawDefinition( 21 | { 22 | parsedLook, 23 | actions: initialActions, 24 | direction, 25 | headDirection, 26 | item: itemId, 27 | effect, 28 | }: Options, 29 | deps: AvatarDependencies 30 | ): AvatarDrawDefinition | undefined { 31 | const actions = new Set(initialActions).add(AvatarAction.Default); 32 | const def = new AvatarDrawDefinition( 33 | { 34 | actions, 35 | direction, 36 | frame: 0, 37 | look: parsedLook, 38 | item: itemId, 39 | headDirection, 40 | effect, 41 | }, 42 | deps 43 | ); 44 | 45 | return def; 46 | } 47 | 48 | interface Options { 49 | parsedLook: ParsedLook; 50 | actions: Set; 51 | direction: number; 52 | headDirection?: number; 53 | frame: number; 54 | item?: string | number; 55 | effect?: IAvatarEffectData; 56 | } 57 | -------------------------------------------------------------------------------- /example/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const webpack = require("webpack"); 3 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 4 | const ForkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin"); 5 | const CopyPlugin = require('copy-webpack-plugin'); 6 | 7 | module.exports = { 8 | entry: "./src/index.ts", 9 | output: { 10 | filename: "[name].[fullhash].js", 11 | path: path.resolve(__dirname, "dist/webpack"), 12 | }, 13 | devtool: "source-map", 14 | resolve: { 15 | extensions: [".tsx", ".ts", ".js", ".mjs"], 16 | fallback: { "buffer": false, "timers": false } 17 | }, 18 | optimization: { splitChunks: { chunks: 'all' }, runtimeChunk: true, minimizer: [] }, 19 | module: { 20 | rules: [ 21 | { 22 | test: /\.(js|jsx|ts|tsx)$/, 23 | use: [ 24 | { 25 | loader: "babel-loader", 26 | options: { 27 | /* Use `babel.config.js` in root folder */ 28 | rootMode: "upward", 29 | }, 30 | }, 31 | ], 32 | exclude: /node_modules/, 33 | }, 34 | { 35 | test: /\.(png|svg|bmp)$/, 36 | loader: "file-loader", 37 | }, 38 | ], 39 | }, 40 | plugins: [ 41 | new ForkTsCheckerWebpackPlugin({}), 42 | new HtmlWebpackPlugin({ 43 | template: "./src/index.ejs", 44 | }), 45 | new webpack.HotModuleReplacementPlugin(), 46 | new webpack.EnvironmentPlugin({ 47 | NODE_ENV: "development", 48 | }), 49 | ], 50 | devServer: { 51 | contentBase: [path.join(__dirname, 'public')] 52 | } 53 | }; -------------------------------------------------------------------------------- /src/tools/dump/downloadAllFiles.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { downloadEffects } from "./downloadEffects"; 3 | import { downloadFigures } from "./downloadFigures"; 4 | import { downloadFileWithMessage } from "./downloadFileWithMessage"; 5 | import { downloadFurnitures } from "./downloadFurnitures"; 6 | import { ExternalVariables } from "./getExternalVariableUrls"; 7 | import { Logger } from "./Logger"; 8 | 9 | export async function downloadAllFiles( 10 | downloadPath: string, 11 | { 12 | figureDataUrl, 13 | figureMapUrl, 14 | furniDataUrl, 15 | hofFurniUrl, 16 | effectMapUrl, 17 | gordonUrl, 18 | }: ExternalVariables, 19 | logger: Logger 20 | ) { 21 | await downloadFileWithMessage( 22 | { 23 | url: figureDataUrl, 24 | savePath: path.join(downloadPath, "figuredata.xml"), 25 | }, 26 | logger 27 | ); 28 | 29 | const figureMap = await downloadFileWithMessage( 30 | { 31 | url: figureMapUrl, 32 | savePath: path.join(downloadPath, "figuremap.xml"), 33 | }, 34 | logger 35 | ); 36 | 37 | const furniData = await downloadFileWithMessage( 38 | { url: furniDataUrl, savePath: path.join(downloadPath, "furnidata.xml") }, 39 | logger 40 | ); 41 | 42 | const effectMap = await downloadFileWithMessage( 43 | { url: effectMapUrl, savePath: path.join(downloadPath, "effectmap.xml") }, 44 | logger 45 | ); 46 | 47 | await downloadFigures({ gordonUrl, file: figureMap, downloadPath }, logger); 48 | await downloadFurnitures( 49 | { downloadPath, file: furniData, hofFurniUrl }, 50 | logger 51 | ); 52 | await downloadEffects( 53 | { gordonUrl, downloadPath, effectMapDownload: effectMap }, 54 | logger 55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const webpack = require("webpack"); 3 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 4 | const ForkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin"); 5 | const CopyPlugin = require('copy-webpack-plugin'); 6 | 7 | module.exports = { 8 | entry: "./src/index.ts", 9 | output: { 10 | filename: "[name].[fullhash].js", 11 | path: path.resolve(__dirname, "dist/webpack"), 12 | }, 13 | devtool: "source-map", 14 | resolve: { 15 | extensions: [".tsx", ".ts", ".js", ".mjs"], 16 | fallback: { 17 | "buffer": false, 18 | "timers": false, 19 | "events": require.resolve("events"), 20 | "stream": require.resolve("stream-browserify") 21 | } 22 | }, 23 | module: { 24 | rules: [ 25 | { 26 | test: /\.(js|jsx|ts|tsx)$/, 27 | use: [ 28 | { 29 | loader: "babel-loader", 30 | options: { 31 | /* Use `babel.config.js` in root folder */ 32 | rootMode: "upward", 33 | }, 34 | }, 35 | ], 36 | exclude: /node_modules/, 37 | }, 38 | { 39 | test: /\.(png|svg|bmp)$/, 40 | loader: "file-loader", 41 | }, 42 | ], 43 | }, 44 | plugins: [ 45 | new ForkTsCheckerWebpackPlugin({}), 46 | new HtmlWebpackPlugin({ 47 | template: "./src/index.ejs", 48 | }), 49 | new webpack.HotModuleReplacementPlugin(), 50 | new webpack.EnvironmentPlugin({ 51 | NODE_ENV: "development", 52 | }), 53 | ], 54 | devServer: { 55 | contentBase: [path.join(__dirname, 'public')] 56 | } 57 | }; -------------------------------------------------------------------------------- /src/tools/dump/downloadFigures.ts: -------------------------------------------------------------------------------- 1 | import { FigureMapData } from "../../objects/avatar/data/FigureMapData"; 2 | import * as path from "path"; 3 | import Bluebird from "bluebird"; 4 | import * as readline from "readline"; 5 | 6 | import { 7 | downloadFile, 8 | DownloadFileResult, 9 | DownloadRequest, 10 | } from "./downloadFile"; 11 | import { 12 | downloadFileWithMessage, 13 | getDownloadMessage, 14 | } from "./downloadFileWithMessage"; 15 | import { Logger } from "./Logger"; 16 | import { downloadMultipleFiles } from "./downloadMultipleFiles"; 17 | 18 | export async function downloadFigures( 19 | { 20 | file, 21 | downloadPath, 22 | gordonUrl, 23 | }: { 24 | downloadPath: string; 25 | file: DownloadFileResult; 26 | gordonUrl: string; 27 | }, 28 | logger: Logger 29 | ) { 30 | if (file.type !== "SUCCESS") return; 31 | 32 | const text = await file.text(); 33 | const figureMap = new FigureMapData(text); 34 | const libraries = figureMap.getLibraries(); 35 | 36 | return downloadMultipleFiles( 37 | { 38 | data: libraries, 39 | concurrency: 30, 40 | logger, 41 | name: "Figure Libraries", 42 | }, 43 | async (library, view) => { 44 | const request: DownloadRequest = { 45 | url: `${gordonUrl}/${library}.swf`, 46 | savePath: path.join(downloadPath, `figure`, `${library}.swf`), 47 | }; 48 | 49 | const result = await downloadFile(request); 50 | switch (result.type) { 51 | case "SUCCESS": 52 | view.reportSuccess(library); 53 | break; 54 | 55 | case "HTTP_ERROR": 56 | case "FAILED_TO_WRITE": 57 | view.reportError(library, request, result); 58 | break; 59 | } 60 | } 61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /src/objects/avatar/data/interfaces/IAvatarEffectData.ts: -------------------------------------------------------------------------------- 1 | export interface IAvatarEffectData { 2 | getFrameCount(): number; 3 | getFrameBodyParts(frame: number): AvatarEffectFrameBodypart[]; 4 | getFrameBodyPart( 5 | bodyPartId: string, 6 | frame: number 7 | ): AvatarEffectFrameBodypart | undefined; 8 | getFrameBodyPartByBase( 9 | bodyPartId: string, 10 | frame: number 11 | ): AvatarEffectFrameBodypart | undefined; 12 | 13 | getFrameEffectParts(frame: number): AvatarEffectFrameFXPart[]; 14 | getFrameEffectPart( 15 | id: string, 16 | frame: number 17 | ): AvatarEffectFrameFXPart | undefined; 18 | 19 | getSprites(): AvatarEffectSprite[]; 20 | getSpriteDirection( 21 | id: string, 22 | direction: number 23 | ): AvatarEffectSpriteDirection | undefined; 24 | getDirection(): AvatarEffectDirection | undefined; 25 | getAddtions(): AvatarEffectFXAddition[]; 26 | } 27 | 28 | export interface AvatarEffectFXAddition { 29 | id: string; 30 | align?: string; 31 | base?: string; 32 | } 33 | 34 | export interface AvatarEffectFrameFXPart { 35 | id: string; 36 | action?: string; 37 | frame?: number; 38 | dx?: number; 39 | dy?: number; 40 | dd?: number; 41 | } 42 | 43 | export interface AvatarEffectFrameBodypart { 44 | id: string; 45 | action?: string; 46 | frame?: number; 47 | dx?: number; 48 | dy?: number; 49 | dd?: number; 50 | } 51 | 52 | export interface AvatarEffectSprite { 53 | id: string; 54 | ink?: number; 55 | member?: string; 56 | staticY?: number; 57 | directions: boolean; 58 | } 59 | 60 | export interface AvatarEffectSpriteDirection { 61 | id: number; 62 | dz?: number; 63 | dx?: number; 64 | dy?: number; 65 | } 66 | 67 | export interface AvatarEffectDirection { 68 | offset: number; 69 | } 70 | -------------------------------------------------------------------------------- /src/tools/dump/detectEdges.ts: -------------------------------------------------------------------------------- 1 | import { Canvas } from "canvas"; 2 | 3 | const checkOpacityLevel = (tolerance: number) => ( 4 | pixels: Uint8ClampedArray 5 | ) => { 6 | let transparent = true; 7 | for (let i = 3, l = pixels.length; i < l && transparent; i += 4) { 8 | transparent = transparent && pixels[i] === 255 * tolerance; 9 | } 10 | return transparent; 11 | }; 12 | 13 | const defaultOptions = { 14 | tolerance: 0, 15 | }; 16 | 17 | export const detectEdges = ( 18 | canvas: Canvas, 19 | options?: typeof defaultOptions 20 | ) => { 21 | const { tolerance } = { 22 | ...defaultOptions, 23 | ...options, 24 | }; 25 | 26 | const isTransparent = checkOpacityLevel(tolerance); 27 | 28 | const context = canvas.getContext("2d"); 29 | const { width, height } = canvas; 30 | let pixels; 31 | 32 | let top = -1; 33 | do { 34 | ++top; 35 | pixels = context.getImageData(0, top, width, 1).data; 36 | } while (isTransparent(pixels)); 37 | 38 | if (top === height) { 39 | throw new Error("Can't detect edges."); 40 | } 41 | 42 | // Left 43 | let left = -1; 44 | do { 45 | ++left; 46 | pixels = context.getImageData(left, top, 1, height - top).data; 47 | } while (isTransparent(pixels)); 48 | 49 | // Bottom 50 | let bottom = -1; 51 | do { 52 | ++bottom; 53 | pixels = context.getImageData(left, height - bottom - 1, width - left, 1) 54 | .data; 55 | } while (isTransparent(pixels)); 56 | 57 | // Right 58 | let right = -1; 59 | do { 60 | ++right; 61 | pixels = context.getImageData( 62 | width - right - 1, 63 | top, 64 | 1, 65 | height - (top + bottom) 66 | ).data; 67 | } while (isTransparent(pixels)); 68 | 69 | return { 70 | top, 71 | right, 72 | bottom, 73 | left, 74 | }; 75 | }; 76 | -------------------------------------------------------------------------------- /src/objects/furniture/FurnitureSprite.ts: -------------------------------------------------------------------------------- 1 | import { HitSprite } from "../hitdetection/HitSprite"; 2 | 3 | export class FurnitureSprite extends HitSprite { 4 | private _baseX = 0; 5 | private _baseY = 0; 6 | private _baseZIndex = 0; 7 | 8 | private _offsetX = 0; 9 | private _offsetY = 0; 10 | private _offsetZIndex = 0; 11 | 12 | private _assetName: string | undefined; 13 | 14 | public get offsetX() { 15 | return this._offsetX; 16 | } 17 | 18 | public set offsetX(value) { 19 | this._offsetX = value; 20 | this._update(); 21 | } 22 | 23 | public get offsetY() { 24 | return this._offsetY; 25 | } 26 | 27 | public set offsetY(value) { 28 | this._offsetY = value; 29 | this._update(); 30 | } 31 | 32 | public get offsetZIndex() { 33 | return this._offsetZIndex; 34 | } 35 | 36 | public set offsetZIndex(value) { 37 | this._offsetZIndex = value; 38 | this._update(); 39 | } 40 | 41 | public get baseX() { 42 | return this._baseX; 43 | } 44 | 45 | public set baseX(value) { 46 | this._baseX = value; 47 | this._update(); 48 | } 49 | 50 | public get baseY() { 51 | return this._baseY; 52 | } 53 | 54 | public set baseY(value) { 55 | this._baseY = value; 56 | this._update(); 57 | } 58 | 59 | public get baseZIndex() { 60 | return this._baseZIndex; 61 | } 62 | 63 | public set baseZIndex(value) { 64 | this._baseZIndex = value; 65 | this._update(); 66 | } 67 | 68 | public get assetName() { 69 | return this._assetName; 70 | } 71 | 72 | public set assetName(value) { 73 | this._assetName = value; 74 | } 75 | 76 | private _update() { 77 | this.x = this.baseX + this.offsetX; 78 | this.y = this.baseY + this.offsetY; 79 | this.zIndex = this.baseZIndex + this.offsetZIndex; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /docs/docs/guides/adding-windows.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: adding-windows 3 | title: Adding windows & landscapes 4 | --- 5 | 6 | ## Wall Furniture 7 | 8 | To create windows, we will use a `WallFurniture`. A wall furniture is just like a `FloorFurniture`, but for walls. 9 | 10 | ```ts 11 | import * as PIXI from "pixi.js"; 12 | 13 | import { Room, FloorFurniture, Avatar, Shroom } from "@jankuss/shroom"; 14 | 15 | const view = document.querySelector("#root") as HTMLCanvasElement; 16 | const application = new PIXI.Application({ view }); 17 | 18 | const shroom = Shroom.create({ application, resourcePath: "./resources" }); 19 | const room = Room.create(shroom, { 20 | tilemap: ` 21 | xxxxx 22 | x0000 23 | x0000 24 | x0000 25 | `, 26 | }); 27 | 28 | const furni1 = new WallFurniture({ 29 | roomX: 0, 30 | roomY: 0, 31 | roomZ: 0, 32 | direction: 4, 33 | type: "window_skyscraper", 34 | }); 35 | 36 | const furni2 = new WallFurniture({ 37 | roomX: 0, 38 | roomY: 0, 39 | roomZ: 0, 40 | direction: 4, 41 | type: "window_skyscraper", 42 | }); 43 | 44 | room.addRoomObject(furni1); 45 | room.addRoomObject(furni2); 46 | 47 | room.x = 100; 48 | room.y = 200; 49 | 50 | application.stage.addChild(room); 51 | ``` 52 | 53 | ## Landscapes 54 | 55 | The speciality of windows in this case, is that they can display a custom background called a `Landscape`. Landscapes basically are a texture applied to walls which only windows can display. 56 | 57 | To create a landscape, use the following code in addition to the previous example. 58 | 59 | ```ts 60 | /* ...*/ 61 | 62 | const landscape = new Landscape(); 63 | landscape.leftTexture = loadRoomTexture("./images/left.png"); 64 | landscape.rightTexture = loadRoomTexture("./images/right.png"); 65 | landscape.color = "#ff0000"; 66 | 67 | room.addRoomObject(landscape); 68 | ``` 69 | -------------------------------------------------------------------------------- /src/objects/avatar/util/getAssetFromPartMeta.ts: -------------------------------------------------------------------------------- 1 | import { IAvatarOffsetsData } from "../data/interfaces/IAvatarOffsetsData"; 2 | 3 | export function getAssetFromPartMeta( 4 | assetPartDefinition: string, 5 | assetInfoFrame: { flipped: boolean; swapped: boolean; asset: string }, 6 | offsetsData: IAvatarOffsetsData, 7 | { offsetX, offsetY }: { offsetX: number; offsetY: number } 8 | ) { 9 | const offsets = offsetsData.getOffsets(assetInfoFrame.asset); 10 | 11 | if (offsets == null) return; 12 | 13 | const { x: offsetsX, y: offsetsY } = applyOffsets({ 14 | offsets, 15 | customOffsets: { offsetX, offsetY }, 16 | flipped: assetInfoFrame.flipped, 17 | lay: assetPartDefinition === "lay", 18 | }); 19 | 20 | if (isNaN(offsetsX)) throw new Error("Invalid x offset"); 21 | if (isNaN(offsetsY)) throw new Error("Invalid y offset"); 22 | 23 | return { 24 | fileId: assetInfoFrame.asset, 25 | library: "", 26 | mirror: assetInfoFrame.flipped, 27 | x: offsetsX, 28 | y: offsetsY, 29 | }; 30 | } 31 | 32 | export function applyOffsets({ 33 | offsets, 34 | customOffsets: { offsetX, offsetY }, 35 | flipped, 36 | lay, 37 | }: { 38 | flipped: boolean; 39 | offsets: { offsetX: number; offsetY: number }; 40 | customOffsets: { offsetX: number; offsetY: number }; 41 | lay: boolean; 42 | }) { 43 | let offsetsX = 0; 44 | let offsetsY = 0; 45 | 46 | offsetsY = -offsets.offsetY + offsetY; 47 | 48 | if (flipped) { 49 | offsetsX = 64 + offsets.offsetX - offsetX; 50 | } else { 51 | offsetsX = -offsets.offsetX - offsetX; 52 | } 53 | 54 | if (lay) { 55 | if (flipped) { 56 | offsetsX -= 52; 57 | } else { 58 | offsetsX += 52; 59 | } 60 | } 61 | 62 | offsetsY = offsetsY + 16; 63 | 64 | return { 65 | x: offsetsX, 66 | y: offsetsY, 67 | }; 68 | } 69 | -------------------------------------------------------------------------------- /src/objects/furniture/util/visualization/parseAnimations.ts: -------------------------------------------------------------------------------- 1 | import { VisualizationXmlVisualization } from "./VisualizationXml"; 2 | 3 | export function parseAnimations( 4 | visualization: VisualizationXmlVisualization, 5 | set: (id: string, animation: AnimationData) => void 6 | ) { 7 | const animations = visualization.animations 8 | ? visualization.animations[0].animation 9 | : undefined; 10 | 11 | if (animations != null) { 12 | animations.forEach((animation) => { 13 | const animationId = animation["$"].id; 14 | 15 | const animationLayers = animation.animationLayer; 16 | 17 | const layerToFrames = new Map< 18 | string, 19 | { frames: string[]; frameRepeat?: number } 20 | >(); 21 | 22 | let frameCount = 1; 23 | 24 | animationLayers.forEach((layer) => { 25 | const layerId = layer["$"].id; 26 | 27 | if (layer.frameSequence != null) { 28 | const frameSequenceFrames = layer.frameSequence[0].frame; 29 | 30 | const frames = frameSequenceFrames.map((frame) => frame["$"].id); 31 | const frameRepeatString = layer["$"].frameRepeat; 32 | 33 | layerToFrames.set(layerId, { 34 | frames, 35 | frameRepeat: 36 | frameRepeatString != null ? Number(frameRepeatString) : undefined, 37 | }); 38 | 39 | if (frames.length > frameCount) { 40 | frameCount = frames.length; 41 | } 42 | } 43 | }); 44 | 45 | set(animationId, { 46 | frameCount: frameCount, 47 | layerToFrames, 48 | }); 49 | }); 50 | } 51 | } 52 | 53 | export type FramesData = { 54 | frames: string[]; 55 | frameRepeat?: number; 56 | }; 57 | 58 | export type AnimationData = { 59 | frameCount: number; 60 | layerToFrames: Map; 61 | }; 62 | -------------------------------------------------------------------------------- /src/objects/avatar/data/FigureMapData.ts: -------------------------------------------------------------------------------- 1 | import { AvatarData } from "./AvatarData"; 2 | import { IFigureMapData } from "./interfaces/IFigureMapData"; 3 | 4 | function _getLibraryForPartKey(id: string, type: string) { 5 | return `${id}_${type}`; 6 | } 7 | 8 | export class FigureMapData extends AvatarData implements IFigureMapData { 9 | private _libraryForPartMap = new Map(); 10 | private _allLibraries: string[] = []; 11 | 12 | constructor(xml: string) { 13 | super(xml); 14 | this._cacheData(); 15 | } 16 | 17 | static async fromUrl(url: string) { 18 | const response = await fetch(url); 19 | const text = await response.text(); 20 | 21 | return new FigureMapData(text); 22 | } 23 | 24 | getLibraryOfPart(id: string, type: string): string | undefined { 25 | const typeProcessed = type === "hrb" ? "hr" : type; 26 | 27 | return this._libraryForPartMap.get( 28 | _getLibraryForPartKey(id, typeProcessed) 29 | ); 30 | } 31 | 32 | getLibraries(): string[] { 33 | return this._allLibraries; 34 | } 35 | 36 | private _cacheData() { 37 | const allLibraries = this.querySelectorAll(`lib`); 38 | 39 | allLibraries.forEach((element) => { 40 | const libraryId = element.getAttribute("id"); 41 | if (libraryId == null) return; 42 | 43 | this._allLibraries.push(libraryId); 44 | 45 | const parts = Array.from(element.querySelectorAll("part")); 46 | 47 | parts.forEach((part) => { 48 | const partId = part.getAttribute("id"); 49 | const partType = part.getAttribute("type"); 50 | 51 | if (partId == null) return; 52 | if (partType == null) return; 53 | 54 | this._libraryForPartMap.set( 55 | _getLibraryForPartKey(partId, partType), 56 | libraryId 57 | ); 58 | }); 59 | }); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/tools/dump/downloadMultipleFiles.ts: -------------------------------------------------------------------------------- 1 | import { DownloadFileResult, DownloadRequest } from "./downloadFile"; 2 | import { Logger } from "./Logger"; 3 | import { getDownloadMessage } from "./downloadFileWithMessage"; 4 | import Bluebird from "bluebird"; 5 | import { ProgressBar } from "./ProgressBar"; 6 | 7 | export async function downloadMultipleFiles( 8 | { 9 | data, 10 | name, 11 | concurrency, 12 | logger, 13 | }: { 14 | data: T[]; 15 | concurrency: number; 16 | logger: Logger; 17 | name: string; 18 | }, 19 | map: (data: T, view: IView) => Promise 20 | ) { 21 | const downloadProgress = new ProgressBar( 22 | logger, 23 | data.length, 24 | (current, count, extra) => { 25 | if (extra != null) { 26 | return `${name}: ${current} / ${count} downloaded (${extra})`; 27 | } else { 28 | return `${name}: ${current} / ${count} downloaded`; 29 | } 30 | } 31 | ); 32 | 33 | const reportSuccess = (lastLibrary: string) => { 34 | downloadProgress.increment(lastLibrary); 35 | }; 36 | 37 | const reportError = ( 38 | errorLibrary: string, 39 | request: DownloadRequest, 40 | result: DownloadFileResult 41 | ) => { 42 | downloadProgress.error( 43 | `${errorLibrary} - ${getDownloadMessage(request, result)}` 44 | ); 45 | }; 46 | 47 | const reportDone = () => { 48 | downloadProgress.done(); 49 | }; 50 | 51 | await Bluebird.map( 52 | data, 53 | async (data) => { 54 | await map(data, { reportError, reportSuccess }); 55 | }, 56 | { concurrency: concurrency } 57 | ); 58 | 59 | reportDone(); 60 | } 61 | 62 | interface IView { 63 | reportSuccess(value: string): void; 64 | reportError( 65 | value: string, 66 | request: DownloadRequest, 67 | response: DownloadFileResult 68 | ): void; 69 | } 70 | -------------------------------------------------------------------------------- /src/objects/furniture/visualization/BasicFurnitureVisualization.ts: -------------------------------------------------------------------------------- 1 | import { FurnitureSprite } from "../FurnitureSprite"; 2 | import { IFurnitureVisualizationView } from "../IFurnitureVisualizationView"; 3 | import { FurnitureVisualization } from "./FurnitureVisualization"; 4 | 5 | export class StaticFurnitureVisualization extends FurnitureVisualization { 6 | private _sprites: FurnitureSprite[] = []; 7 | private _refreshFurniture = false; 8 | private _currentDirection: number | undefined; 9 | private _animationId: string | undefined; 10 | 11 | setView(view: IFurnitureVisualizationView): void { 12 | super.setView(view); 13 | this._update(); 14 | } 15 | 16 | updateDirection(direction: number): void { 17 | if (this._currentDirection === direction) return; 18 | 19 | this._currentDirection = direction; 20 | this._update(); 21 | } 22 | 23 | updateAnimation(animation: string): void { 24 | if (this._animationId === animation) return; 25 | 26 | this._animationId = animation; 27 | this._update(); 28 | } 29 | 30 | updateFrame(): void { 31 | if (!this.mounted) return; 32 | 33 | if (this._refreshFurniture) { 34 | this._refreshFurniture = false; 35 | this._update(); 36 | } 37 | } 38 | 39 | update() { 40 | this._update(); 41 | } 42 | 43 | destroy(): void { 44 | // Do nothing 45 | } 46 | 47 | private _update() { 48 | if (this._currentDirection == null) return; 49 | 50 | this.view.setDisplayAnimation(this._animationId); 51 | this.view.setDisplayDirection(this._currentDirection); 52 | this.view.updateDisplay(); 53 | this.view.getLayers().forEach((layer) => layer.setCurrentFrameIndex(0)); 54 | } 55 | } 56 | 57 | /** 58 | * @deprecated Use `StaticFurnitureVisualization` 59 | */ 60 | export const BasicFurnitureVisualization = StaticFurnitureVisualization; 61 | -------------------------------------------------------------------------------- /src/objects/room/util/LegacyWallGeometry.ts: -------------------------------------------------------------------------------- 1 | import { ParsedTileType } from "../../../util/parseTileMap"; 2 | 3 | export class LegacyWallGeometry { 4 | private static readonly RIGHT_WALL: string = "l"; 5 | private static readonly LEFT_WALL: string = "r"; 6 | 7 | private _width: number; 8 | private _height: number; 9 | private _scale: number; 10 | 11 | constructor(private _heightmap: ParsedTileType[][]) { 12 | this._width = _heightmap[0].length; 13 | this._height = _heightmap.length; 14 | this._scale = 64; 15 | } 16 | 17 | public getLocation( 18 | roomX: number, 19 | roomY: number, 20 | offsetX: number, 21 | offsetY: number, 22 | wall: string 23 | ): { x: number; y: number; z: number } { 24 | let rX: number = roomX; 25 | let rY: number = roomY; 26 | let rZ: number = this.getHeight(roomX, roomY); 27 | if (wall == LegacyWallGeometry.LEFT_WALL) { 28 | rX = rX + (offsetX / (this._scale / 2) - 0.5); 29 | rY = rY + 0.5; 30 | rZ = rZ - (offsetY - offsetX / 2) / (this._scale / 2); 31 | } else { 32 | rY = rY + ((this._scale / 2 - offsetX) / (this._scale / 2) - 0.5); 33 | rX = rX + 0.5; 34 | rZ = rZ - (offsetY - (this._scale / 2 - offsetX) / 2) / (this._scale / 2); 35 | } 36 | return { 37 | x: rX, 38 | y: rY, 39 | z: rZ, 40 | }; 41 | } 42 | 43 | public getHeight(x: number, y: number): number { 44 | if (x < 0 || x >= this._width || y < 0 || y >= this._height) return 0; 45 | 46 | const row = this._heightmap[y]; 47 | 48 | if (row == null) return 0; 49 | const cell = row[x]; 50 | 51 | switch (cell.type) { 52 | case "wall": 53 | return cell.height; 54 | case "stairs": 55 | return cell.z; 56 | case "tile": 57 | return cell.z; 58 | } 59 | 60 | return 0; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/objects/events/EventManagerNode.ts: -------------------------------------------------------------------------------- 1 | import RBush from "rbush"; 2 | import { Subscription } from "rxjs"; 3 | import { Rectangle } from "../room/IRoomRectangle"; 4 | import { IEventManagerNode } from "./interfaces/IEventManagerNode"; 5 | import { IEventTarget } from "./interfaces/IEventTarget"; 6 | 7 | export class EventManagerNode implements IEventManagerNode { 8 | private _rectangle: Rectangle | undefined; 9 | private _subscription: Subscription; 10 | 11 | public get minX() { 12 | if (this._rectangle == null) throw new Error("Rectangle wasn't set"); 13 | 14 | return this._rectangle.x; 15 | } 16 | 17 | public get maxX() { 18 | if (this._rectangle == null) throw new Error("Rectangle wasn't set"); 19 | 20 | return this._rectangle.x + this._rectangle.width; 21 | } 22 | 23 | public get minY() { 24 | if (this._rectangle == null) throw new Error("Rectangle wasn't set"); 25 | 26 | return this._rectangle.y; 27 | } 28 | 29 | public get maxY() { 30 | if (this._rectangle == null) throw new Error("Rectangle wasn't set"); 31 | 32 | return this._rectangle.y + this._rectangle.height; 33 | } 34 | 35 | constructor( 36 | public readonly target: IEventTarget, 37 | private _bush: RBush 38 | ) { 39 | this._subscription = target.getRectangleObservable().subscribe((value) => { 40 | this._updateRectangle(value); 41 | }); 42 | } 43 | 44 | destroy(): void { 45 | if (this._rectangle != null) { 46 | this._bush.remove(this); 47 | } 48 | this._subscription.unsubscribe(); 49 | } 50 | 51 | private _updateRectangle(rectangle: Rectangle | undefined): void { 52 | if (this._rectangle != null) { 53 | this._bush.remove(this); 54 | } 55 | 56 | this._rectangle = rectangle; 57 | 58 | if (rectangle != null) { 59 | this._bush.insert(this); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/objects/furniture/util/visualization/VisualizationXml.ts: -------------------------------------------------------------------------------- 1 | export type VisualizationXml = { 2 | visualizationData: { 3 | graphics: { 4 | visualization: VisualizationXmlVisualization[]; 5 | }[]; 6 | }; 7 | }; 8 | 9 | export type VisualizationXmlVisualization = { 10 | $: { size: string; layerCount: string }; 11 | layers: 12 | | { 13 | layer: { 14 | $: { 15 | id: string; 16 | z: string | undefined; 17 | ink: string | undefined; 18 | tag: string | undefined; 19 | ignoreMouse: string | undefined; 20 | alpha: string | undefined; 21 | }; 22 | }[]; 23 | }[] 24 | | undefined; 25 | 26 | directions: { 27 | direction: { 28 | $: { id: string }; 29 | layer: 30 | | { 31 | $: { 32 | id: string; 33 | z: string | undefined; 34 | ink: string | undefined; 35 | tag: string | undefined; 36 | }; 37 | }[] 38 | | undefined; 39 | }[]; 40 | }[]; 41 | 42 | colors: 43 | | { 44 | color: { 45 | $: { 46 | id: string; 47 | }; 48 | colorLayer: { 49 | $: { 50 | id: string; 51 | color: string; 52 | }; 53 | }[]; 54 | }[]; 55 | }[] 56 | | undefined; 57 | 58 | animations: 59 | | { 60 | animation: { 61 | $: { id: string }; 62 | animationLayer: { 63 | $: { id: string; frameRepeat: string | undefined }; 64 | frameSequence: { 65 | frame: { 66 | $: { 67 | id: string; 68 | }; 69 | }[]; 70 | }[]; 71 | }[]; 72 | }[]; 73 | }[] 74 | | undefined; 75 | }; 76 | -------------------------------------------------------------------------------- /src/objects/furniture/IFurnitureVisualizationView.ts: -------------------------------------------------------------------------------- 1 | import { IFurnitureVisualizationData } from "./data/interfaces/IFurnitureVisualizationData"; 2 | 3 | /** 4 | * This is a intermediary interface injected into the visualization 5 | * of a furniture (e.g. `AnimatedFurnitureVisualization`). This interface exposes 6 | * all the necessary methods to manipulate the way the furniture displays on the screen. 7 | */ 8 | export interface IFurnitureVisualizationView { 9 | /** 10 | * Sets the displaying animation of the furniture. 11 | * @param animation 12 | */ 13 | setDisplayAnimation(animation?: string): void; 14 | 15 | /** 16 | * Sets the displaying direction of the furniture. 17 | * @param direction 18 | */ 19 | setDisplayDirection(direction: number): void; 20 | 21 | /** 22 | * Updates the furniture display with the previously set animation and direction. 23 | */ 24 | updateDisplay(): void; 25 | 26 | /** 27 | * Gets the display layers of the furniture. 28 | * This can only be called after `updateDisplay` has been called. 29 | */ 30 | getLayers(): IFurnitureVisualizationLayer[]; 31 | 32 | /** 33 | * Gets the visualization data of the furniture. 34 | */ 35 | getVisualizationData(): IFurnitureVisualizationData; 36 | } 37 | 38 | export interface IFurnitureVisualizationLayer { 39 | tag?: string; 40 | /** 41 | * How often frames are repeated 42 | */ 43 | frameRepeat: number; 44 | /** 45 | * The layer index specified as a number. 46 | * a = 0 47 | * b = 1 48 | * c = 2 49 | * 50 | * etc. 51 | */ 52 | layerIndex: number; 53 | /** 54 | * The amount of frames for this layer 55 | */ 56 | assetCount: number; 57 | /** 58 | * Sets the active frame to display for this layer 59 | * @param frame 60 | */ 61 | setCurrentFrameIndex(frame: number): void; 62 | setColor(color: number): void; 63 | } 64 | -------------------------------------------------------------------------------- /src/tools/dump/dumpFurniture.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { promises as fs } from "fs"; 3 | import { createSpritesheet } from "./createSpritesheet"; 4 | import { FurnitureVisualizationData } from "../../objects/furniture/data/FurnitureVisualizationData"; 5 | import { FurnitureIndexData } from "../../objects/furniture/data/FurnitureIndexData"; 6 | import { FurnitureAssetsData } from "../../objects/furniture/data/FurnitureAssetsData"; 7 | import { ShroomAssetBundle } from "../../assets/ShroomAssetBundle"; 8 | 9 | export async function dumpFurniture( 10 | baseName: string, 11 | dumpLocation: string, 12 | imagePaths: string[] 13 | ) { 14 | const { json, image } = await createSpritesheet(imagePaths, { 15 | outputFormat: "png", 16 | }); 17 | 18 | const visualizationData = await fs.readFile( 19 | path.join(dumpLocation, `${baseName}_visualization.bin`), 20 | "utf-8" 21 | ); 22 | 23 | const indexData = await fs.readFile( 24 | path.join(dumpLocation, `index.bin`), 25 | "utf-8" 26 | ); 27 | const assetsData = await fs.readFile( 28 | path.join(dumpLocation, `${baseName}_assets.bin`), 29 | "utf-8" 30 | ); 31 | 32 | const visualization = new FurnitureVisualizationData(visualizationData); 33 | const index = new FurnitureIndexData(indexData); 34 | const assets = new FurnitureAssetsData(assetsData); 35 | 36 | const data = { 37 | spritesheet: json, 38 | visualization: visualization.toJson(), 39 | index: index.toJson(), 40 | assets: assets.toJson(), 41 | }; 42 | 43 | const jsonString = JSON.stringify(data); 44 | const encoder = new TextEncoder(); 45 | 46 | const furnitureFile = new ShroomAssetBundle(); 47 | furnitureFile.addFile("index.json", encoder.encode(jsonString)); 48 | furnitureFile.addFile("spritesheet.png", image); 49 | 50 | await fs.writeFile(`${dumpLocation}.shroom`, furnitureFile.toBuffer()); 51 | } 52 | -------------------------------------------------------------------------------- /docs/docs/install.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: install 3 | title: Installation 4 | slug: / 5 | --- 6 | 7 | ### 1. Install the shroom package 8 | 9 | To install shroom in your project, use the following command. 10 | 11 | ``` 12 | npm install @jankuss/shroom pixi.js 13 | ``` 14 | 15 | If you are using `yarn`, you can use 16 | 17 | ``` 18 | yarn add @jankuss/shroom pixi.js 19 | ``` 20 | 21 | ### 2. Dump assets into your project 22 | 23 | Run the following commands to dump the required assets into your project directory. This will take some time. 24 | The `--url` option specifies the url to the external variables to use. The `--location` option specifies the location where the assets should get dumped into. 25 | You can adjust both as needed. 26 | 27 | ``` 28 | npm install -g @jankuss/shroom 29 | shroom dump --url https://www.habbo.com/gamedata/external_variables/326b0a1abf9e2571d541ac05e6eb3173b83bddea --location ./public/resources 30 | ``` 31 | 32 | You will need to serve the created `resources` folder with a http server, so shroom can access the required assets. 33 | 34 | ### 4. Create the Shroom instance 35 | 36 | Lastly, in your code, import and initialize the Shroom instance. 37 | 38 | ```ts 39 | import * as PIXI from "pixi.js"; 40 | import { Shroom } from "@jankuss/shroom"; 41 | 42 | const view = document.querySelector("#root") as HTMLCanvasElement; 43 | const application = new PIXI.Application({ view }); 44 | 45 | // Assuming the resources are available under http://localhost:8080/resources 46 | const shroom = Shroom.create({ application, resourcePath: "./resources" }); 47 | ``` 48 | 49 | Now, you are fully ready to use shroom. 50 | Check out the [Guides](guides/create-room.md) section to learn how to use shroom. 51 | 52 | Also, take a look at the [example project](https://github.com/jankuss/shroom/tree/master/example) in the shroom repository for a basic project depending on shroom. 53 | You can use it as a boilerplate for your own. 54 | -------------------------------------------------------------------------------- /src/objects/furniture/data/FurnitureAssetsData.ts: -------------------------------------------------------------------------------- 1 | import { XmlData } from "../../../data/XmlData"; 2 | import { FurnitureAssetsJson } from "./FurnitureAssetsJson"; 3 | import { 4 | FurnitureAsset, 5 | IFurnitureAssetsData, 6 | } from "./interfaces/IFurnitureAssetsData"; 7 | 8 | export class FurnitureAssetsData 9 | extends XmlData 10 | implements IFurnitureAssetsData { 11 | private _assets = new Map(); 12 | 13 | constructor(xml: string) { 14 | super(xml); 15 | 16 | this.querySelectorAll("asset").forEach((element) => { 17 | const name = element.getAttribute("name"); 18 | const x = Number(element.getAttribute("x")); 19 | const y = Number(element.getAttribute("y")); 20 | const source = element.getAttribute("source"); 21 | const flipH = element.getAttribute("flipH") === "1"; 22 | 23 | if (name == null) throw new Error("Invalid name"); 24 | if (isNaN(x)) throw new Error("x is not a number"); 25 | if (isNaN(y)) throw new Error("y is not a number"); 26 | 27 | this._assets.set(name, { 28 | x, 29 | y, 30 | source: source ?? undefined, 31 | flipH, 32 | name, 33 | valid: true, 34 | }); 35 | }); 36 | } 37 | 38 | static async fromUrl(url: string) { 39 | const response = await fetch(url); 40 | const text = await response.text(); 41 | 42 | return new FurnitureAssetsData(text); 43 | } 44 | 45 | toJson(): FurnitureAssetsJson { 46 | const assets = this.getAssets(); 47 | const assetsObject: { [key: string]: FurnitureAsset } = {}; 48 | 49 | assets.forEach((asset) => { 50 | assetsObject[asset.name] = asset; 51 | }); 52 | 53 | return assetsObject; 54 | } 55 | 56 | getAsset(name: string): FurnitureAsset | undefined { 57 | return this._assets.get(name); 58 | } 59 | 60 | getAssets() { 61 | return Array.from(this._assets.values()); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/tools/dump/downloadFile.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from "fs"; 2 | import fetch, { Response } from "node-fetch"; 3 | import * as path from "path"; 4 | 5 | function makeAbsolute(url: string) { 6 | if (url.slice(0, 2) === "//") { 7 | return `http:${url}`; 8 | } 9 | 10 | return url; 11 | } 12 | 13 | export async function fetchRetry(url: string) { 14 | let response: Response | undefined; 15 | let count = 0; 16 | 17 | do { 18 | try { 19 | response = await fetch(url); 20 | } catch (e) { 21 | // Ignore network error 22 | } 23 | 24 | await new Promise((resolve) => setTimeout(resolve, count * 5000)); 25 | 26 | count++; 27 | } while ((response == null || response.status >= 500) && count < 20); 28 | 29 | return response; 30 | } 31 | 32 | export async function downloadFile({ 33 | url, 34 | savePath, 35 | }: DownloadRequest): Promise { 36 | url = makeAbsolute(url); 37 | const response = await fetchRetry(url); 38 | 39 | if (response == null) { 40 | return { 41 | type: "RETRY_FAILED", 42 | }; 43 | } 44 | 45 | if (response.status >= 200 && response.status < 300) { 46 | try { 47 | await fs.mkdir(path.dirname(savePath), { recursive: true }); 48 | const buffer = await response.buffer(); 49 | await fs.writeFile(savePath, buffer); 50 | 51 | return { 52 | type: "SUCCESS", 53 | text: async () => buffer.toString("utf-8"), 54 | }; 55 | } catch (e) { 56 | return { 57 | type: "FAILED_TO_WRITE", 58 | }; 59 | } 60 | } 61 | 62 | return { 63 | type: "HTTP_ERROR", 64 | code: response.status, 65 | }; 66 | } 67 | 68 | export type DownloadRequest = { url: string; savePath: string }; 69 | 70 | export type DownloadFileResult = 71 | | { type: "SUCCESS"; text: () => Promise } 72 | | { type: "FAILED_TO_WRITE" } 73 | | { type: "HTTP_ERROR"; code: number } 74 | | { type: "RETRY_FAILED" }; 75 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages 3 | 4 | name: Node.js Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: actions/setup-node@v1 16 | with: 17 | node-version: 12 18 | - run: npm ci 19 | - run: npm test 20 | 21 | publish-npm: 22 | needs: build 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v2 26 | - uses: actions/setup-node@v1 27 | with: 28 | node-version: 12 29 | registry-url: https://registry.npmjs.org/ 30 | - run: npm ci 31 | - run: npm publish --access public 32 | env: 33 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 34 | notify-discord: 35 | if: "!github.event.release.prerelease" 36 | needs: publish-npm 37 | runs-on: ubuntu-latest 38 | defaults: 39 | run: 40 | working-directory: ./ci/discord 41 | steps: 42 | - uses: actions/checkout@v2 43 | - uses: actions/setup-node@v1 44 | with: 45 | node-version: 12 46 | - run: npm install 47 | - run: npm start 48 | env: 49 | DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} 50 | GIT_VERSION_TAG: ${{ github.event.release.tag_name }} 51 | 52 | publish-gpr: 53 | needs: build 54 | runs-on: ubuntu-latest 55 | steps: 56 | - uses: actions/checkout@v2 57 | - uses: actions/setup-node@v1 58 | with: 59 | node-version: 12 60 | registry-url: https://npm.pkg.github.com/ 61 | - run: npm ci 62 | - run: npm publish 63 | env: 64 | NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} 65 | -------------------------------------------------------------------------------- /.github/workflows/documentation.yml: -------------------------------------------------------------------------------- 1 | name: documentation 2 | 3 | on: 4 | pull_request: 5 | branches: [master] 6 | push: 7 | branches: [master] 8 | 9 | jobs: 10 | checks: 11 | if: github.event_name != 'push' 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v1 15 | - uses: actions/setup-node@v1 16 | with: 17 | node-version: "12.x" 18 | - name: Test Build 19 | run: | 20 | cd docs 21 | if [ -e yarn.lock ]; then 22 | yarn install --frozen-lockfile 23 | elif [ -e package-lock.json ]; then 24 | npm ci 25 | else 26 | npm i 27 | fi 28 | npm run build 29 | gh-release: 30 | if: github.event_name != 'pull_request' 31 | runs-on: ubuntu-latest 32 | steps: 33 | - uses: actions/checkout@v1 34 | - uses: actions/setup-node@v1 35 | with: 36 | node-version: "12.x" 37 | - name: Add key to allow access to repository 38 | env: 39 | SSH_AUTH_SOCK: /tmp/ssh_agent.sock 40 | run: | 41 | mkdir -p ~/.ssh 42 | ssh-keyscan github.com >> ~/.ssh/known_hosts 43 | echo "${{ secrets.GH_PAGES_DEPLOY }}" > ~/.ssh/id_rsa 44 | chmod 600 ~/.ssh/id_rsa 45 | cat <> ~/.ssh/config 46 | Host github.com 47 | HostName github.com 48 | IdentityFile ~/.ssh/id_rsa 49 | EOT 50 | - name: Release to GitHub Pages 51 | env: 52 | USE_SSH: true 53 | GIT_USER: git 54 | run: | 55 | cd docs 56 | git config --global user.email "actions@gihub.com" 57 | git config --global user.name "gh-actions" 58 | if [ -e yarn.lock ]; then 59 | yarn install --frozen-lockfile 60 | elif [ -e package-lock.json ]; then 61 | npm ci 62 | else 63 | npm i 64 | fi 65 | npx docusaurus deploy 66 | -------------------------------------------------------------------------------- /src/objects/events/EventManagerContainer.ts: -------------------------------------------------------------------------------- 1 | import * as PIXI from "pixi.js"; 2 | import { EventManager } from "./EventManager"; 3 | 4 | export class EventManagerContainer { 5 | private _box: PIXI.TilingSprite | undefined; 6 | 7 | constructor( 8 | private _application: PIXI.Application, 9 | private _eventManager: EventManager 10 | ) { 11 | this._updateRectangle(); 12 | 13 | _application.ticker.add(this._updateRectangle); 14 | 15 | const interactionManager: PIXI.InteractionManager = this._application 16 | .renderer.plugins.interaction; 17 | 18 | interactionManager.addListener( 19 | "pointermove", 20 | (event: PIXI.InteractionEvent) => { 21 | const position = event.data.getLocalPosition(this._application.stage); 22 | 23 | this._eventManager.move(event, position.x, position.y); 24 | }, 25 | true 26 | ); 27 | 28 | interactionManager.addListener( 29 | "pointerup", 30 | (event: PIXI.InteractionEvent) => { 31 | const position = event.data.getLocalPosition(this._application.stage); 32 | 33 | this._eventManager.pointerUp(event, position.x, position.y); 34 | }, 35 | true 36 | ); 37 | 38 | interactionManager.addListener( 39 | "pointerdown", 40 | (event: PIXI.InteractionEvent) => { 41 | const position = event.data.getLocalPosition(this._application.stage); 42 | 43 | this._eventManager.pointerDown(event, position.x, position.y); 44 | }, 45 | true 46 | ); 47 | } 48 | 49 | destroy() { 50 | this._application.ticker.remove(this._updateRectangle); 51 | } 52 | 53 | private _updateRectangle = () => { 54 | //this._box?.destroy(); 55 | 56 | const renderer = this._application.renderer; 57 | const width = renderer.width / renderer.resolution; 58 | const height = renderer.height / renderer.resolution; 59 | 60 | this._box = new PIXI.TilingSprite(PIXI.Texture.WHITE, width, height); 61 | this._box.alpha = 0.3; 62 | 63 | //this._application.stage.addChild(this._box); 64 | }; 65 | } 66 | -------------------------------------------------------------------------------- /src/objects/Shroom.ts: -------------------------------------------------------------------------------- 1 | import { AnimationTicker } from "./animation/AnimationTicker"; 2 | import { AvatarLoader } from "./avatar/AvatarLoader"; 3 | import { FurnitureLoader } from "./furniture/FurnitureLoader"; 4 | import { FurnitureData } from "./furniture/FurnitureData"; 5 | import { Dependencies } from "./room/Room"; 6 | 7 | export class Shroom { 8 | constructor(public readonly dependencies: Dependencies) {} 9 | 10 | /** 11 | * Create a shroom instance 12 | */ 13 | static create( 14 | options: { 15 | resourcePath?: string; 16 | application: PIXI.Application; 17 | } & Partial 18 | ) { 19 | return this.createShared(options).for(options.application); 20 | } 21 | 22 | /** 23 | * Create a shared shroom instance. This is useful if you have multiple 24 | * `PIXI.Application` which all share the same shroom dependencies. 25 | */ 26 | static createShared({ 27 | resourcePath, 28 | configuration, 29 | animationTicker, 30 | avatarLoader, 31 | furnitureData, 32 | furnitureLoader, 33 | }: { 34 | resourcePath?: string; 35 | } & Partial) { 36 | const _furnitureData = furnitureData ?? FurnitureData.create(resourcePath); 37 | const _avatarLoader = 38 | avatarLoader ?? AvatarLoader.createForAssetBundle(resourcePath); 39 | const _furnitureLoader = 40 | furnitureLoader ?? 41 | FurnitureLoader.createForJson(_furnitureData, resourcePath); 42 | const _configuration = configuration ?? {}; 43 | 44 | return { 45 | for: (application: PIXI.Application) => { 46 | const _animationTicker = 47 | animationTicker ?? AnimationTicker.create(application); 48 | 49 | const realDependencies: Dependencies = { 50 | animationTicker: _animationTicker, 51 | avatarLoader: _avatarLoader, 52 | furnitureLoader: _furnitureLoader, 53 | configuration: _configuration, 54 | furnitureData: _furnitureData, 55 | application, 56 | }; 57 | 58 | return new Shroom(realDependencies); 59 | }, 60 | }; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/objects/avatar/data/AvatarManifestData.ts: -------------------------------------------------------------------------------- 1 | import { AvatarData } from "./AvatarData"; 2 | import { 3 | IAvatarManifestData, 4 | ManifestAlias, 5 | ManifestAsset, 6 | } from "./interfaces/IAvatarManifestData"; 7 | 8 | export class AvatarManifestData 9 | extends AvatarData 10 | implements IAvatarManifestData { 11 | private _assets: ManifestAsset[] = []; 12 | private _assetbyName: Map = new Map(); 13 | 14 | private _aliases: ManifestAlias[] = []; 15 | 16 | constructor(xml: string) { 17 | super(xml); 18 | this._cacheData(); 19 | } 20 | 21 | getAliases(): ManifestAlias[] { 22 | return this._aliases; 23 | } 24 | 25 | getAssets(): ManifestAsset[] { 26 | return this._assets; 27 | } 28 | 29 | getAssetByName(name: string): ManifestAsset | undefined { 30 | return this._assetbyName.get(name); 31 | } 32 | 33 | private _cacheData() { 34 | const assets = this.querySelectorAll(`assets asset`); 35 | const aliases = this.querySelectorAll(`aliases alias`); 36 | 37 | for (const asset of assets) { 38 | const offsetParam = asset.querySelector(`param[key="offset"]`); 39 | const value = offsetParam?.getAttribute("value"); 40 | const name = asset.getAttribute("name"); 41 | 42 | if (value != null && name != null) { 43 | const offsets = value.split(","); 44 | const x = Number(offsets[0]); 45 | const y = Number(offsets[1]); 46 | 47 | const asset: ManifestAsset = { name, x, y, flipH: false, flipV: false }; 48 | this._assets.push(asset); 49 | this._assetbyName.set(name, asset); 50 | } 51 | } 52 | 53 | for (const alias of aliases) { 54 | const name = alias.getAttribute("name"); 55 | const link = alias.getAttribute("link"); 56 | const fliph = alias.getAttribute("fliph") === "1"; 57 | const flipv = alias.getAttribute("flipv") === "1"; 58 | 59 | if (name != null && link != null) { 60 | const alias: ManifestAlias = { 61 | name, 62 | link, 63 | fliph, 64 | flipv, 65 | }; 66 | 67 | this._aliases.push(alias); 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /docs/src/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | import Layout from '@theme/Layout'; 4 | import Link from '@docusaurus/Link'; 5 | import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; 6 | import useBaseUrl from '@docusaurus/useBaseUrl'; 7 | import styles from './styles.module.css'; 8 | 9 | const features = []; 10 | 11 | function Feature({imageUrl, title, description}) { 12 | const imgUrl = useBaseUrl(imageUrl); 13 | return ( 14 |
15 | {imgUrl && ( 16 |
17 | {title} 18 |
19 | )} 20 |

{title}

21 |

{description}

22 |
23 | ); 24 | } 25 | 26 | function Home() { 27 | const context = useDocusaurusContext(); 28 | const {siteConfig = {}} = context; 29 | return ( 30 | 33 |
34 |
35 |

{siteConfig.title}

36 |

{siteConfig.tagline}

37 |
38 | 44 | Get Started 45 | 46 |
47 |
48 |
49 |
50 | {features && features.length > 0 && ( 51 |
52 |
53 |
54 | {features.map((props, idx) => ( 55 | 56 | ))} 57 |
58 |
59 |
60 | )} 61 |
62 |
63 | ); 64 | } 65 | 66 | export default Home; 67 | -------------------------------------------------------------------------------- /ci/discord/src/index.ts: -------------------------------------------------------------------------------- 1 | import fetch from "node-fetch"; 2 | const { parser } = require("keep-a-changelog"); 3 | import * as fs from "fs"; 4 | 5 | const { GIT_VERSION_TAG, DISCORD_WEBHOOK } = process.env; 6 | 7 | if (GIT_VERSION_TAG == null) throw new Error("Invalid git version tag"); 8 | if (DISCORD_WEBHOOK == null) throw new Error("Invalid webhook"); 9 | 10 | const versionWithoutV = GIT_VERSION_TAG.slice(1); 11 | 12 | function getLatestChangelogMarkdown(): string { 13 | const changelog = parser(fs.readFileSync("../../CHANGELOG.md", "utf-8")); 14 | 15 | const matchingRelease = changelog.releases.find((release: any) => { 16 | return release.version.version === versionWithoutV; 17 | }); 18 | 19 | return "```" + matchingRelease.toString() + "```"; 20 | } 21 | 22 | const packageName = "@jankuss/shroom"; 23 | 24 | const content = { 25 | content: "A new version of shroom has been released.", 26 | embeds: [ 27 | { 28 | title: `shroom ${GIT_VERSION_TAG}`, 29 | description: `The following changes have been made. You can view the full CHANGELOG [here](https://github.com/jankuss/shroom/blob/${GIT_VERSION_TAG}/CHANGELOG.md). ${getLatestChangelogMarkdown()}`, 30 | url: `https://www.npmjs.com/package/${packageName}/v/${versionWithoutV}`, 31 | fields: [ 32 | { 33 | name: "Download with npm", 34 | value: `Download the new version with [npm](https://www.npmjs.com/package/${packageName}/v/${versionWithoutV}).`, 35 | }, 36 | { 37 | name: "Report Issues", 38 | value: 39 | "Please report any bugs or issues with this version on our [Github Issues](https://github.com/jankuss/shroom/issues).", 40 | }, 41 | { 42 | name: "Need help?", 43 | value: "Ask questions in the #support channel.", 44 | }, 45 | ], 46 | author: { 47 | name: "jankuss", 48 | url: "https://github.com/jankuss", 49 | icon_url: "https://avatars0.githubusercontent.com/u/1659532", 50 | }, 51 | }, 52 | ], 53 | }; 54 | 55 | const body = JSON.stringify(content); 56 | 57 | fetch(DISCORD_WEBHOOK, { 58 | method: "POST", 59 | body, 60 | headers: { "Content-Type": "application/json" }, 61 | }).catch(console.error); 62 | --------------------------------------------------------------------------------