init(🎉): new habbo renderer

This commit is contained in:
Walid 2023-07-22 13:34:09 +01:00
commit 29675c7c0b
Signed by: Walidoux
GPG Key ID: CCF21881FE8BEBAF
65 changed files with 12819 additions and 0 deletions

10
.editorconfig Executable file
View File

@ -0,0 +1,10 @@
# editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
node_modules
.parcel-cache
dist
.github

8
.parcelrc Normal file
View File

@ -0,0 +1,8 @@
{
"extends": "@parcel/config-default",
"transformers": {
"*.ts": [
"@parcel/transformer-typescript-tsc"
]
}
}

10
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,10 @@
{
"typescript.enablePromptUseWorkspaceTsdk": true,
"typescript.tsdk": "node_modules/typescript/lib",
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll": true
}
}

47
package.json Normal file
View File

@ -0,0 +1,47 @@
{
"name": "renderer",
"version": "0.0.0",
"main": "dist/index.js",
"types": "dist/types.d.ts",
"source": "src/index.ts",
"license": "MIT",
"scripts": {
"dev": "parcel public/index.html --no-cache --open",
"build": "parcelb",
"lint:typescript": "eslint \"**/*.ts\" --ignore-path \".gitignore\" && tsc --noemit",
"lint:prettier": "prettier \".\" --check --ignore-path \".gitignore\""
},
"dependencies": {
"@pixi/color": "7.2.4",
"@pixi/layers": "2.1.0",
"@pixi/utils": "^7.2.4",
"gsap": "^3.12.2",
"pixi.js": "^7.2.4"
},
"devDependencies": {
"@parcel/config-default": "^2.9.3",
"@parcel/transformer-typescript-tsc": "^2.9.3",
"@types/node": "^20.4.3",
"@walidoux/eslint-config": "1.0.3",
"@walidoux/prettier-config": "1.0.3",
"parcel": "^2.9.3",
"process": "^0.11.10",
"punycode": "1.4.1",
"querystring-es3": "^0.2.1",
"timers-browserify": "^2.0.12",
"ts-node": "^10.9.1",
"ts-node-dev": "^2.0.0",
"eslint": "8.45.0",
"prettier": "2.8.8",
"typescript": "^5.1.6"
},
"peerDependencies": {
"url": "0.11.1"
},
"prettier": "@walidoux/prettier-config",
"eslintConfig": {
"extends": [
"@walidoux/eslint-config"
]
}
}

4645
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

31
public/index.html Normal file
View File

@ -0,0 +1,31 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8" />
<title>Scuti Renderer</title>
<style>
body {
margin: 0;
padding: 0;
overflow: hidden;
image-rendering: pixelated;
}
div#stats {
position: fixed;
top: 0;
right: 0;
z-index: 500;
width: max(200px, 10vw, 10vh);
height: max(100px, 6vh, 6vw);
opacity: 0.8;
user-select: none;
}
</style>
</head>
<body>
<div id="app"></div>
<script type="module" src="script4.js"></script>
</body>
</html>

61
public/script.js Normal file
View File

@ -0,0 +1,61 @@
import { Scuti } from '../src/Scuti';
import { Room } from '../src/objects/rooms/Room';
import { FloorMaterial } from '../src/objects/rooms/materials/FloorMaterial';
import { WallMaterial } from '../src/objects/rooms/materials/WallMaterial';
import { Avatar } from '../src/objects/avatars/Avatar';
import { AvatarAction } from '../src/objects/avatars/actions/AvatarAction';
(async () => {
const renderer = new Scuti({
canvas: document.getElementById('app'),
width: window.innerWidth,
height: window.innerHeight,
resources: './resources'
});
await renderer.loadResources();
const tileMap =
'x0000000000000000x\n' +
'x0000000000000000x\n' +
'x0000000000000000x\n' +
'00000000000000000x\n' +
'x0000000000000000x\n' +
'x0000000000000000x\n' +
'x0000000000000000x\n' +
'x0000000000000000x\n' +
'x0000000000000000x\n' +
'x0000000000000000x\n';
const room = new Room(renderer, {
tileMap: tileMap,
/*floorMaterial: new FloorMaterial(renderer, 110),
wallMaterial: new WallMaterial(renderer, 1501)*/
//floorMaterial: new FloorMaterial(renderer, 307),
floorMaterial: new FloorMaterial(renderer, 110),
wallMaterial: new WallMaterial(renderer, 1601)
});
avatar(room, 2, 1, 0, 0);
avatar(room, 4, 1, 0, 1);
avatar(room, 6, 1, 0, 2);
avatar(room, 8, 1, 0, 3);
avatar(room, 10, 1, 0, 4);
avatar(room, 12, 1, 0, 5);
avatar(room, 14, 1, 0, 6);
avatar(room, 16, 1, 0, 7);
})();
function avatar(room, x, y, z, direction) {
const avatar = new Avatar({
figure: 'hr-100-61.hd-180-7.ch-210-66.lg-270-82.sh-290-80',
position: {
x: x,
y: y,
z: z
},
bodyDirection: direction,
headDirection: direction,
actions: [AvatarAction.Talk, AvatarAction.Wave, AvatarAction.Walk, AvatarAction.CarryItem],
handItem: 55
});
room.objects.add(avatar);
}

427
public/script2.js Normal file
View File

@ -0,0 +1,427 @@
import { Scuti } from '../src/Scuti';
import { Room } from '../src/objects/rooms/Room';
import { FloorMaterial } from '../src/objects/rooms/materials/FloorMaterial';
import { WallMaterial } from '../src/objects/rooms/materials/WallMaterial';
import { FloorFurniture } from '../src/objects/furnitures/FloorFurniture';
import { WallFurniture } from '../src/objects/furnitures/WallFurniture';
import { Avatar } from '../src/objects/avatars/Avatar';
import { AvatarAction } from '../src/objects/avatars/actions/AvatarAction';
(async () => {
const renderer = new Scuti({
canvas: document.getElementById('app'),
width: window.innerWidth,
height: window.innerHeight,
resources: './resources'
});
await renderer.loadResources();
const tileMap =
'xxxxxxxxxxxxxxxxxxxx\n' +
'x222221111111111111x\n' +
'x222221111111111111x\n' +
'2222221111111111111x\n' +
'x222221111111111111x\n' +
'x222221111111111111x\n' +
'x222221111111111111x\n' +
'xxxxxxxx1111xxxxxxxx\n' +
'xxxxxxxx0000xxxxxxxx\n' +
'x000000x0000x000000x\n' +
'x000000x0000x000000x\n' +
'x00000000000x000000x\n' +
'x00000000000x000000x\n' +
'x000000000000000000x\n' +
'x000000000000000000x\n' +
'xxxxxxxx00000000000x\n' +
'x000000x00000000000x\n' +
'x000000x0000xxxxxxxx\n' +
'x00000000000x000000x\n' +
'x00000000000x000000x\n' +
'x00000000000x000000x\n' +
'x00000000000x000000x\n' +
'xxxxxxxx0000x000000x\n' +
'x000000x0000x000000x\n' +
'x000000x0000x000000x\n' +
'x000000000000000000x\n' +
'x000000000000000000x\n' +
'x000000000000000000x\n' +
'x000000000000000000x\n' +
'xxxxxxxxxxxxxxxxxxxx\n';
const tileMap1 =
'xxxxxx\n' +
'x4444432110011111x\n' +
'x444443211001xx00x\n' +
'0000000011001xx00x\n' +
'x0000000000000000x\n' +
'x0001000000000000x\n' +
'x0000000000000000x\n' +
'x000000000xx00000x\n' +
'x000000000x000000x\n' +
'00000000000001000x\n' +
'x0000432100011110x\n' +
'x0000000000001000x\n' +
'x0000000000000000x\n' +
'x0000000000000000x\n' +
'x0000000000000000x\n' +
'x0000000100000000x\n' +
'x0000001110000000x\n' +
'x0000011211000000x\n' +
'x0000012221000000x\n' +
'x000111232111100x\n' +
'x0001112321111000x\n' +
'x0000012321000000x\n' +
'x0000012221000000x\n' +
'x0000011211000000x\n' +
'x00000011100xxxxxx\n' +
'x00000001000xxxxxx\n' +
'x00000000000xxxxxx\n' +
'x00000000000xxxxxx\n';
const tileMap2 =
'x0000000000000000x\n' +
'x0000000000000000x\n' +
'x0000000000000000x\n' +
'00000000000000000x\n' +
'x0000000000000000x\n' +
'x0000000000000000x\n' +
'x0000000000000000x\n' +
'x0000000000000000x\n' +
'x0000000000000000x\n' +
'x0000000000000000x\n';
const tileMap3 =
'xxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n' +
'x222222222222222222222222222x\n' +
'x222222222222222222222222222x\n' +
'2222222222222222222222222222x\n' +
'x222222222222222222222222222x\n' +
'x2222xxxxxx222222xxxxxxx2222x\n' +
'x2222xxxxxx111111xxxxxxx2222x\n' +
'x2222xx111111111111111xx2222x\n' +
'x2222xx111111111111111xx2222x\n' +
'x2222xx11xxx1111xxxx11xx2222x\n' +
'x2222xx11xxx0000xxxx11xx2222x\n' +
'x22222111x00000000xx11xx2222x\n' +
'x22222111x00000000xx11xx2222x\n' +
'x22222111x00000000xx11xx2222x\n' +
'x22222111x00000000xx11xx2222x\n' +
'x22222111x00000000xx11xx2222x\n' +
'x22222111x00000000xx11xx2222x\n' +
'x2222xx11xxxxxxxxxxx11xx2222x\n' +
'x2222xx11xxxxxxxxxxx11xx2222x\n' +
'x2222xx111111111111111xx2222x\n' +
'x2222xx111111111111111xx2222x\n' +
'x2222xxxxxxxxxxxxxxxxxxx2222x\n' +
'x2222xxxxxxxxxxxxxxxxxxx2222x\n' +
'x222222222222222222222222222x\n' +
'x222222222222222222222222222x\n' +
'x222222222222222222222222222x\n' +
'x222222222222222222222222222x\n' +
'xxxxxxxxxxxxxxxxxxxxxxxxxxxxx';
const tileMap4 =
'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n' +
'xeeeeeeeeeeeeeeeedcba9888888888888\n' +
'xeeeeeeeeeeeeeeeexxxxxx88888888888\n' +
'xeeeeeeeeeeeeeeeexxxxxx88888888888\n' +
'xeeeeeeeeeeeeeeeexxxxxx88888888888\n' +
'xeeeeeeeeeeeeeeeexxxxxx88888888888\n' +
'xdxxxxxxxxxxxxxxxxxxxxx88888888888\n' +
'xcxxxxxxxxxxxxxxxxxxxxx88888888888\n' +
'xbxxxxxxxxxxxxxxxxxxxxx88888888888\n' +
'xaxxxxxxxxxxxxxxxxxxxxx88888888888\n' +
'aaaaaaaaaaaaaaaaaxxxxxxxxxxxxxxxxx\n' +
'xaaaaaaaaaaaaaaaaxxxxxxxxxxxxxxxxx\n' +
'xaaaaaaaaaaaaaaaaxxxxxxxxxxxxxxxxx\n' +
'xaaaaaaaaaaaaaaaaxxxx6666666666666\n' +
'xaaaaaaaaaaaaaaaaxxxx6666666666666\n' +
'xaaaaaaaaaaaaaaaaxxxx6666666666666\n' +
'xaaaaaaaaaaaaaaaaxxxx6666666666666\n' +
'xaaaaaaaaaaaaaaaaxxxx6666666666666\n' +
'xaaaaaaaaaaaaaaaa98766666666666666\n' +
'xaaaaaaaaaaaaaaaaxxxxxxxxxxxx5xxxx\n' +
'xaaaaaaaaaaaaaaaaxxxxxxxxxxxx4xxxx\n' +
'xaaaaaaaaaaaaaaaaxxxxxxxxxxxx3xxxx\n' +
'xaaaaaaaaaaaaaaaaxxx3333333333xxxx\n' +
'xaaaaaaaaaaaaaaaaxxx3333333333xxxx\n' +
'xaaaaaaaaaaaaaaaaxxx3333333333xxxx\n' +
'xaaaaaaaaaaaaaaaaxxx3333333333xxxx\n' +
'xaaaaaaaaaaaaaaaaxxx3333333333xxxx\n' +
'xaaaaaaaaaaaaaaaaxxx3333333333xxxx\n' +
'xaaaaaaaaaaaaaaaaxxx3333333333xxxx\n' +
'xaaaaaaaaaaaaaaaaxxx3333333333xxxx\n' +
'xaaaaaaaaaaaaaaaaxxx3333333333xxxx\n' +
'xaaaaaaaaaaaaaaaaxxx3333333333xxxx\n' +
'xxxxxxxxxxxxxxxx9xxx3333333333xxxx\n' +
'xxxxxxxxxxxxxxxx8xxx3333333333xxxx\n' +
'xxxxxxxxxxxxxxxx7xxx3333333333xxxx\n' +
'xxx777777777xxxx6xxx3333333333xxxx\n' +
'xxx777777777xxxx5xxxxxxxxxxxxxxxxx\n' +
'xxx777777777xxxx4xxxxxxxxxxxxxxxxx\n' +
'xxx777777777xxxx3xxxxxxxxxxxxxxxxx\n' +
'xxx777777777xxxx2xxxxxxxxxxxxxxxxx\n' +
'xfffffffffxxxxxx1xxxxxxxxxxxxxxxxx\n' +
'xfffffffffxxxxxx111111111111111111\n' +
'xfffffffffxxxxxx111111111111111111\n' +
'xfffffffffxxxxxx111111111111111111\n' +
'xfffffffffxxxxxx111111111111111111\n' +
'xfffffffffxxxxxx111111111111111111\n' +
'xfffffffffxxxxxx111111111111111111\n' +
'xxxxxxxxxxxxxxxx111111111111111111\n' +
'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';
const tileMap5 =
'xxxxxxxxxxxxxxxxxxxxxxxxxxx\n' +
'x2222xx1111111111xx11111111\n' +
'x2222xx1111111111xx11111111\n' +
'222222111111111111111111111\n' +
'x22222111111111111111111111\n' +
'x22222111111111111111111111\n' +
'x22222111111111111111111111\n' +
'x2222xx1111111111xx11111111\n' +
'x2222xx1111111111xx11111111\n' +
'x2222xx1111111111xxxx1111xx\n' +
'x2222xx1111111111xxxx0000xx\n' +
'xxxxxxx1111111111xx00000000\n' +
'xxxxxxx1111111111xx00000000\n' +
'x22222111111111111000000000\n' +
'x22222111111111111000000000\n' +
'x22222111111111111000000000\n' +
'x22222111111111111000000000\n' +
'x2222xx1111111111xx00000000\n' +
'x2222xx1111111111xx00000000\n' +
'x2222xxxx1111xxxxxxxxxxxxxx\n' +
'x2222xxxx0000xxxxxxxxxxxxxx\n' +
'x2222x0000000000xxxxxxxxxxx\n' +
'x2222x0000000000xxxxxxxxxxx\n' +
'x2222x0000000000xxxxxxxxxxx\n' +
'x2222x0000000000xxxxxxxxxxx\n' +
'x2222x0000000000xxxxxxxxxxx\n' +
'x2222x0000000000xxxxxxxxxxx';
const room = new Room(renderer, {
tileMap: tileMap5,
/*floorMaterial: new FloorMaterial(renderer, 110),
wallMaterial: new WallMaterial(renderer, 1501)*/
//floorMaterial: new FloorMaterial(renderer, 307),
floorMaterial: new FloorMaterial(renderer, 110),
wallMaterial: new WallMaterial(renderer, 1601)
});
/*setTimeout(() => {
room.wallMaterial = new WallMaterial(renderer, 1701);
room.floorMaterial = new FloorMaterial(renderer, 301);
room.wallThickness = 8;
room.floorThickness = 8;
room.wallHeight = 6;
}, 5000);*/
const furniture = new FloorFurniture({
id: 1619,
position: {
x: 7,
y: 5,
z: 0
},
direction: 4,
state: 1
});
const furniture2 = new FloorFurniture({
id: 3895,
position: {
x: 7,
y: 5,
z: 0
},
direction: 4,
state: 1
});
const wallFurniture = new WallFurniture({
position: {
x: 1,
y: 0,
offsetX: 8,
offsetY: 36
},
state: 0,
id: 4625,
direction: 2
});
const wallFurniture2 = new WallFurniture({
position: {
x: 1,
y: 3,
offsetX: 8,
offsetY: 36
},
state: 0,
id: 4625,
direction: 2
});
const wallFurniture3 = new WallFurniture({
position: {
x: 4,
y: 0,
offsetX: 14,
offsetY: 41
},
id: 4066,
direction: 4,
state: 3
});
const avatar = new Avatar({
figure: 'hr-100-61.hd-180-7.ch-210-66.lg-270-82.sh-290-80',
position: {
x: 4,
y: 4,
z: 0
},
bodyDirection: 2,
headDirection: 2,
actions: [
//AvatarAction.Idle,
//AvatarAction.Walk,
AvatarAction.Talk,
AvatarAction.Wave,
AvatarAction.Walk,
AvatarAction.CarryItem
],
handItem: 55
});
let hd = [180, 185, 190, 195, 200, 205];
let hr = [100, 105, 110, 115, 125, 135, 145, 155, 165, 170];
let ch = [210, 215, 220, 225, 230, 235, 240, 245, 250, 255];
let sh = [290, 295, 300, 305, 725, 730, 735, 740, 905, 906, 907, 908];
let ha = [
1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008, 1009, 1010, 1011, 1012, 1013, 1014, 1015, 1016, 1017, 1018, 1019,
1020, 1021, 1022, 1023, 1024, 1025, 1026, 1027
];
let lg = [270, 275, 280, 285, 281, 695, 696, 716, 700, 705, 710, 715, 720, 827];
let wa = [2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012];
let ea = [1401, 1402, 1403, 1404, 1405, 1406];
let color = [
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30
];
let color2 = [
31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59,
60, 61
];
let color3 = [
62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90,
91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110
];
let actions = [
AvatarAction.Default,
AvatarAction.Walk,
AvatarAction.GestureSmile,
AvatarAction.Wave,
AvatarAction.GestureAngry,
AvatarAction.GestureSurprised,
AvatarAction.Respect,
AvatarAction.CarryItem,
AvatarAction.UseItem
];
for (let y = 0; y < 2; y++) {
for (let x = 0; x < 2; x++) {
let figure =
'hr-' +
hr[Math.floor(Math.random() * hr.length)] +
'-' +
color2[Math.floor(Math.random() * color2.length)] +
'.hd-' +
hd[Math.floor(Math.random() * hd.length)] +
'-' +
color[Math.floor(Math.random() * color.length)] +
'.ch-' +
ch[Math.floor(Math.random() * ch.length)] +
'-' +
color3[Math.floor(Math.random() * color3.length)] +
'.lg-' +
lg[Math.floor(Math.random() * lg.length)] +
'-' +
color3[Math.floor(Math.random() * color3.length)] +
'.sh-' +
sh[Math.floor(Math.random() * sh.length)] +
'-' +
color3[Math.floor(Math.random() * color3.length)] +
'.ha-' +
ha[Math.floor(Math.random() * ha.length)] +
'-' +
color3[Math.floor(Math.random() * color3.length)] +
'.wa-' +
wa[Math.floor(Math.random() * wa.length)] +
'-' +
color3[Math.floor(Math.random() * color3.length)] +
'.ea-' +
ea[Math.floor(Math.random() * ea.length)] +
'-' +
color3[Math.floor(Math.random() * color3.length)];
let randomAvatar = new Avatar({
position: {
x: x + 1,
y: y,
z: 0
},
bodyDirection: 2,
headDirection: 2,
figure: figure,
actions: [actions[Math.floor(Math.random() * actions.length)]]
});
//room.objects.add(randomAvatar);
}
}
room.objects.add(avatar);
avatar.onPointerDown = (event) => {
console.log('down', event);
};
avatar.onPointerUp = (event) => {
console.log('up', event);
};
avatar.onPointerMove = (event) => {
console.log('move', event);
};
avatar.onPointerOut = (event) => {
console.log('out', event);
};
avatar.onPointerOver = (event) => {
console.log('over', event);
};
avatar.onDoubleClick = (event) => {
console.log('doubleclick', event);
};
//room.objects.add(furniture2);
room.visualization.onTileClick = (position) => {
console.log('click', position);
/*if(furniture.direction === 4) {
furniture.direction = 2
} else {
furniture.direction = 4;
}*/
avatar.roomPosition = {
x: position.x,
y: position.y,
z: position.z
};
wallFurniture.pos = {
x: 1,
y: Math.floor(Math.random() * (10 - 1 + 1) + 1),
offsetX: 8,
offsetY: 36
};
};
room.visualization.onTileOver = (position) => {
console.log('over', position);
};
room.visualization.onTileOut = (position) => {
console.log('out', position);
};
room.objects.add(furniture);
furniture.onDoubleClick = (event) => {
console.log('dblclick furni', event);
};
//room.addRoomObject(wallFurniture);
//room.addRoomObject(wallFurniture2);
//room.addRoomObject(wallFurniture3);
})();

443
public/script3.js Normal file
View File

@ -0,0 +1,443 @@
import { Scuti } from '../src/Scuti';
import { Room } from '../src/objects/rooms/Room';
import { FloorMaterial } from '../src/objects/rooms/materials/FloorMaterial';
import { WallMaterial } from '../src/objects/rooms/materials/WallMaterial';
import { FloorFurniture } from '../src/objects/furnitures/FloorFurniture';
import { WallFurniture } from '../src/objects/furnitures/WallFurniture';
import { Avatar } from '../src/objects/avatars/Avatar';
import { AvatarAction } from '../src/objects/avatars/actions/AvatarAction';
(async () => {
const renderer = new Scuti({
canvas: document.getElementById('app'),
width: window.innerWidth,
height: window.innerHeight,
resources: 'http://localhost:8081/'
});
await renderer.loadResources('http://localhost:8081/');
const tileMap =
'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n' +
'xeeeeeeeeeeeeeeeedcba9888888888888\n' +
'xeeeeeeeeeeeeeeeexxxxxx88888888888\n' +
'xeeeeeeeeeeeeeeeexxxxxx88888888888\n' +
'xeeeeeeeeeeeeeeeexxxxxx88888888888\n' +
'xeeeeeeeeeeeeeeeexxxxxx88888888888\n' +
'xdxxxxxxxxxxxxxxxxxxxxx88888888888\n' +
'xcaaaxxxxxxxxxxxxxxxxxx88888888888\n' +
'xbaaaxxxxxxxxxxxxxxxxxx88888888888\n' +
'xaaaaxxxxxxxxxxxxxxxxxx88888888888\n' +
'aaaaaaaaaaaaaaaaaxxxxxxxxxxxxxxxxx\n' +
'xaaaaaaaaaaaaaaaaxxxxxxxxxxxxxxxxx\n' +
'xaaaaaaaaaaaaaaaaxxxxxxxxxxxxxxxxx\n' +
'xaaaaaaaaaaaaaaaaxxxx6666666666666\n' +
'xaaaaaaaaaaaaaaaaxxxx6666666666666\n' +
'xaaaaaaaaaaaaaaaaxxxx6666666666666\n' +
'xaaaaaaaaaaaaaaaaxxxx6666666666666\n' +
'xaaaaaaaaaaaaaaaaxxxx6666666666666\n' +
'xaaaaaaaaaaaaaaaa98766666666666666\n' +
'xaaaaaaaaaaaaaaaaxxxxxxxxxxxx5xxxx\n' +
'xaaaaaaaaaaaaaaaaxxxxxxxxxxxx4xxxx\n' +
'xaaaaaaaaaaaaaaaaxxxxxxxxxxxx3xxxx\n' +
'xaaaaaaaaaaaaaaaaxxx3333333333xxxx\n' +
'xaaaaaaaaaaaaaaaaxxx3333333333xxxx\n' +
'xaaaaaaaaaaaaaaaaxxx3333333333xxxx\n' +
'xaaaaaaaaaaaaaaaaxxx3333333333xxxx\n' +
'xaaaaaaaaaaaaaaaaxxx3333333333xxxx\n' +
'xaaaaaaaaaaaaaaaaxxx3333333333xxxx\n' +
'xaaaaaaaaaaaaaaaaxxx3333333333xxxx\n' +
'xaaaaaaaaaaaaaaaaxxx3333333333xxxx\n' +
'xaaaaaaaaaaaaaaaaxxx3333333333xxxx\n' +
'xaaaaaaaaaaaaaaaaxxx3333333333xxxx\n' +
'xxxxxxxxxxxxxxxx9xxx3333333333xxxx\n' +
'xxxxxxxxxxxxxxxx8xxx3333333333xxxx\n' +
'xxxxxxxxxxxxxxxx7xxx3333333333xxxx\n' +
'xxx777777777xxxx6xxx3333333333xxxx\n' +
'xxx777777777xxxx5xxxxxxxxxxxxxxxxx\n' +
'xxx777777777xxxx4xxxxxxxxxxxxxxxxx\n' +
'xxx777777777xxxx3xxxxxxxxxxxxxxxxx\n' +
'xxx777777777xxxx2xxxxxxxxxxxxxxxxx\n' +
'xfffffffffxxxxxx1xxxxxxxxxxxxxxxxx\n' +
'xfffffffffxxxxxx111111111111111111\n' +
'xfffffffffxxxxxx111111111111111111\n' +
'xfffffffffxxxxxx111111111111111111\n' +
'xfffffffffxxxxxx111111111111111111\n' +
'xfffffffffxxxxxx111111111111111111\n' +
'xfffffffffxxxxxx111111111111111111\n' +
'xxxxxxxxxxxxxxxx111111111111111111\n' +
'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';
const tileMapUpdated =
'x6543210000000000x\n' +
'x0000000000000000x\n' +
'x0000000000000000x\n' +
'00000000000000000x\n' +
'x0000000000000000x\n' +
'x0000000000000000x\n' +
'x0000000000000000x\n' +
'x0000000000000000x\n' +
'x0000000000000000x\n' +
'x0000000000000000x\n';
const room = new Room(renderer, {
tileMap:
'xxxxxxxxxxxxxxxxxxxxxxxx\n' +
'xxxxxxxxxxxxxxxxxxxxxxxx\n' +
'xxxxxxxxxxxxxxxxxxxxxxxx\n' +
'xxxxxxxxxxxxxxxxxxxxxxxx\n' +
'xxxxxxxxxxxxxxxxxxxxxxxx\n' +
'xxxxxxxxxxxxxxxxxxxxxxxx\n' +
'xxxxxxxxxxxxxxxxxxxxxxxx\n' +
'xxxxx00xxxxxxxxxxxxxxxxx\n' +
'xxxxx00xxxxxxxxxxxxxxxxx\n' +
'xxxxx00xxxxx00xxxxxxxxxx\n' +
'xxxxxxxxxxx00000000xxxxx\n' +
'xxxxxxxxxxx00000000xxxxx\n' +
'xxxxxxxxxxx00000000xxxxx\n' +
'xxxxxxxxxxx00000000xxxxx\n' +
'xxxxxxxxxxx00000000xxxxx\n' +
'xxxxxxxxxxx00000000xxxxx\n' +
'xxxxxxxxxxx000000000000x\n' +
'xxxxxxxxxxx000000000000x\n' +
'xxxxxxxxxxx000000000000x\n' +
'xxxxxxxxxx0000000000000x\n' +
'xxxxxxxxxxx0000000000000\n' +
'xxxxxxxxxxx0000000000000\n' +
'x66666xxxxx000000000000x\n' +
'x66666xxxxx000000000000x\n' +
'x6666xxxxxx000000000000x\n' +
'x6666xxxxxx000000000000x\n' +
'x6666xxxxxx00000000xxxxx\n' +
'x6666xxxxxx00000000xxxxx\n' +
'x6666xxxxxx00000000xxxxx\n' +
'x6666xxxxxx00000000xxxxx\n' +
'x6666xxxxxx00000000xxxxx\n' +
'x6666xxxxxx00000000xxxxx\n' +
'xxxxxxxxxxxx00xxxxxxxxxx',
/*floorMaterial: new FloorMaterial(renderer, 110),
wallMaterial: new WallMaterial(renderer, 1501)*/
//floorMaterial: new FloorMaterial(renderer, 307),
floorMaterial: new FloorMaterial(renderer, 110),
wallMaterial: new WallMaterial(renderer, 1601)
});
setTimeout(() => {
//room.tileMap = tileMapUpdated;
//room.camera._centerCamera();
}, 2000);
const avatar = new Avatar({
//figure: "hr-100-61.hd-180-7.ch-210-66.lg-270-82.sh-290-80",
// police figure: "hr-892-46.hd-209-8.ch-225-81.lg-270-64.sh-300-64.ca-1804-64.wa-2012",
figure: 'hd-209-14.ch-3688-1408.lg-280-1408.sh-290-1408.ha-1008.ea-3578.ca-1806-82.cc-3360-1408',
//figure: "hd-180-1.ch-255-66.lg-280-110.sh-305-62.ha-1012-110.hr-828-61",
position: {
x: 4,
y: 4,
z: 0
},
bodyDirection: 6,
headDirection: 6,
actions: [
//AvatarAction.Idle,
//AvatarAction.Walk,
AvatarAction.Talk,
AvatarAction.Wave,
//AvatarAction.Walk,
AvatarAction.CarryItem,
AvatarAction.Sit
],
handItem: 55
});
setTimeout(() => {
avatar.addAction(AvatarAction.Walk);
}, 5000);
setTimeout(() => {
avatar.removeAction(AvatarAction.Walk);
}, 7000);
setTimeout(() => {
avatar.addAction(AvatarAction.Walk);
}, 9000);
room.objects.add(avatar);
room.tiles.onPointerDown = (position) => {
console.log('click', position);
avatar.roomPosition = position.position;
};
room.tiles.onDoubleClick = (position) => {
console.log('dblclick', position);
};
room.tiles.onPointerOver = (event) => {};
dice(room, 5, 5, 2);
dice(room, 5, 6, 1);
dice(room, 6, 5, 2);
dice(room, 7, 5, 2);
dice(room, 7, 6, 1);
const wallFurniture = new WallFurniture({
id: 4054,
position: {
x: 0,
y: 0,
offsetX: 0,
offsetY: 0
},
direction: 4,
state: 0
});
const wallFurniture2 = new WallFurniture({
position: {
x: 1,
y: 0,
offsetX: 8,
offsetY: 36
},
state: 0,
id: 4625,
direction: 2
});
const furniture = new FloorFurniture({
id: 1619,
position: {
x: 8,
y: 5,
z: 0
},
direction: 2,
state: 1
});
setTimeout(() => {}, 2000);
document.body.addEventListener('keyup', function (event) {
event.preventDefault();
if (event.keyCode === 13) {
room.camera.centerCamera(furniture);
}
});
const furniture2 = new FloorFurniture({
id: 8916,
position: {
x: 8,
y: 10,
z: 0
},
direction: 2,
state: 1
});
const furniture3 = new FloorFurniture({
id: 8916,
position: {
x: 10,
y: 10,
z: 0
},
direction: 2,
state: 0
});
furniture.onPointerDown = () => {
if (furniture.selected) {
furniture.selected = false;
} else {
furniture.selected = true;
}
};
const guildGate = new FloorFurniture({
id: 4389,
position: {
x: 11,
y: 5,
z: 0
},
direction: 2,
state: 1
});
const background1 = new FloorFurniture({
id: 3996,
position: {
x: 20,
y: 19,
z: 0
},
direction: 1,
state: 0
});
const background2 = new FloorFurniture({
id: 3996,
position: {
x: 12,
y: 11,
z: 1
},
direction: 1,
state: 0
});
const background3 = new FloorFurniture({
id: 3996,
position: {
x: 12,
y: 11,
z: 0
},
direction: 1,
state: 0
});
const background4 = new FloorFurniture({
id: 3996,
position: {
x: 11,
y: 10,
z: 0
},
direction: 1,
state: 0
});
const background5 = new FloorFurniture({
id: 3996,
position: {
x: 8,
y: 5,
z: 0
},
direction: 1,
state: 1
});
setTimeout(() => {
console.log(furniture.position);
/*furniture.move({
x: 10,
y: 6,
z: 0
}, 0);*/
//
furniture.rotate(4);
//furniture.direction = 4;
furniture.state = 0;
guildGate.state = 101;
guildGate.visualization.secondaryColor = 0xffff00;
guildGate.visualization.primaryColor = 0x00ffff;
}, 5000);
const wallFurniture3 = new WallFurniture({
position: {
x: 4,
y: 0,
offsetX: 14,
offsetY: 41
},
id: 4066,
direction: 4,
state: 3
});
background1.onLoad = () => {
background1.move(
{
x: 20,
y: 19,
z: 0
},
0
);
background1.visualization.offsetX = -720;
background1.visualization.offsetY = 190;
background1.visualization.offsetZ = 8700;
background1.visualization.imageUrl = '/images/room_ads/wl15/wl15_a.png';
};
room.objects.add(background1);
background2.onLoad = () => {
background2.move(
{
x: 12,
y: 11,
z: 1
},
0
);
background2.visualization.offsetX = -253;
background2.visualization.offsetY = 446;
background2.visualization.offsetZ = 8700;
background2.visualization.imageUrl = '/images/room_ads/wl15/wl15_d.png';
};
room.objects.add(background2);
background3.onLoad = () => {
background3.move(
{
x: 12,
y: 11,
z: 0
},
0
);
background3.visualization.offsetX = -704;
background3.visualization.offsetY = 155;
background3.visualization.offsetZ = 8700;
background3.visualization.imageUrl = '/images/room_ads/wl15/wl15_c.png';
};
room.objects.add(background3);
background4.onLoad = () => {
background4.move(
{
x: 11,
y: 10,
z: 0
},
0
);
background4.visualization.offsetX = -253;
background4.visualization.offsetY = 187;
background4.visualization.offsetZ = 9995;
background4.visualization.imageUrl = '/images/room_ads/wl15/wl15_b.png';
};
room.objects.add(background4);
//background.visualization.imageUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/9/95/Vue.js_Logo_2.svg/2367px-Vue.js_Logo_2.svg.png";
room.objects.add(wallFurniture);
setInterval(() => {
wallFurniture3.state += 1;
wallFurniture3.destroy();
}, 3000);
room.objects.add(wallFurniture3);
room.objects.add(furniture);
room.objects.add(furniture2);
room.objects.add(furniture3);
room.objects.add(guildGate);
/*const furniture = new FloorFurniture({
id: 1619,
position: {
x: 7,
y: 5,
z: 0
},
direction: 4,
state: 1
});*/
})();
function dice(room, x, y, z) {
let furni5 = new FloorFurniture({
position: {
x: x,
y: y,
z: z
},
//direction: randomRotation[Math.floor(Math.random() * randomRotation.length)],
direction: 0,
//id: furniId[Math.floor(Math.random() * furniId.length)],
id: 284,
state: 1
});
room.objects.add(furni5);
let timeout = undefined;
furni5.onDoubleClick = (event) => {
console.log(event);
//if(furni5.infos.logic === "furniture_dice") {
console.log('clicked furni5', event);
if (event.tag === 'activate') {
clearTimeout(timeout);
furni5.state = -1;
timeout = setTimeout(() => {
furni5.state = Math.floor(Math.random() * 6) + 1;
}, 1000);
/*setTimeout(() => {
furni5.state = 0
}, 2000);*/
} else {
clearTimeout(timeout);
furni5.state = 0;
}
//x@}
};
}

166
public/script4.js Normal file
View File

@ -0,0 +1,166 @@
import { Scuti } from '../src/Scuti';
import { Room } from '../src/objects/rooms/Room';
import { FloorMaterial } from '../src/objects/rooms/materials/FloorMaterial';
import { WallMaterial } from '../src/objects/rooms/materials/WallMaterial';
import { FloorFurniture } from '../src/objects/furnitures/FloorFurniture';
import {WiredSelectionFilter} from "../src/objects/filters/WiredSelectionFilter";
import {WallFurniture} from "../src";
(async () => {
const renderer = new Scuti({
canvas: document.getElementById('app'),
width: window.innerWidth,
height: window.innerHeight,
resources: 'https://kozennnn.github.io/scuti-resources/'
});
await renderer.loadResources('https://kozennnn.github.io/scuti-resources/');
const tileMap = 'x1110001\n' + 'x0000000\n' + '00000000\n' + 'x0000000\n' + 'x0000000\n';
const room = new Room(renderer, {
tileMap: tileMap,
floorMaterial: new FloorMaterial(renderer, 110),
wallMaterial: new WallMaterial(renderer, 2301)
});
const furniture = new FloorFurniture({
//id: 4950,
//id: 1619,
id: 4967,
position: {
x: 5,
y: 4,
z: 0
},
direction: 2,
state: 1
});
room.objects.add(furniture);
furniture.onPointerDown = () => {
console.log('clicked');
};
const furniture3 = new FloorFurniture({
id: 8916,
position: {
x: 10,
y: 10,
z: 0
},
direction: 2,
state: 1
});
const furniture2 = new FloorFurniture({
id: 8916,
position: {
x: 8,
y: 10,
z: 0
},
direction: 2,
state: 1
});
const wallFurniture = new WallFurniture({
id: 4625,
position: {
x: -1,
y: 2,
offsetX: 2,
offsetY: -25
},
direction: 2,
state: 2
});
const wallFurniture2 = new WallFurniture({
id: 4032,
position: {
x: 3,
y: -1,
offsetX: 4,
offsetY: -30
},
direction: 4,
state: 1
});
room.objects.add(furniture3);
room.objects.add(furniture2);
room.objects.add(wallFurniture);
room.objects.add(wallFurniture2);
setTimeout(() => wallFurniture.move({
x: -1,
y: 3,
offsetX: 2,
offsetY: -25
}), 3000);
setTimeout(() => wallFurniture.move({
x: -1,
y: 5,
offsetX: 2,
offsetY: -25
}), 5000);
//setTimeout(() => room.objects.add(furniture), 6000);
furniture3.onLoadComplete = () => {
console.log('loaded!');
};
room.tiles.onPointerDown = (event) => {
furniture.move(event.position);
//room.tileMap = tileMap;
};
//dice(room, 5, 5, 2);
document.onkeydown = (e) => {
e = e || window.event;
if (e.keyCode == '38') {
if (room.camera.zoomLevel <= 1) {
room.camera.zoomLevel = room.camera.zoomLevel * 2;
} else {
room.camera.zoomLevel += 1;
}
} else if (e.keyCode == '40') {
if (room.camera.zoomLevel <= 1) {
room.camera.zoomLevel = room.camera.zoomLevel / 2;
} else {
room.camera.zoomLevel -= 1;
}
} else if (e.keyCode == '37') {
furniture.rotate(4);
} else if (e.keyCode == '39') {
const filter = new WiredSelectionFilter(0xffffff, 0x999999);
furniture.addFilter(filter);
}
};
})();
function dice(room, x, y, z) {
let furni5 = new FloorFurniture({
position: {
x: x,
y: y,
z: z
},
//direction: randomRotation[Math.floor(Math.random() * randomRotation.length)],
direction: 0,
//id: furniId[Math.floor(Math.random() * furniId.length)],
id: 284,
state: 1
});
room.objects.add(furni5);
let timeout = undefined;
furni5.onDoubleClick = (event) => {
console.log(event);
//if(furni5.infos.logic === "furniture_dice") {
console.log('clicked furni5', event);
if (event.tag === 'activate') {
clearTimeout(timeout);
furni5.state = -1;
timeout = setTimeout(() => {
furni5.state = Math.floor(Math.random() * 6) + 1;
}, 1000);
/*setTimeout(() => {
furni5.state = 0
}, 2000);*/
} else {
clearTimeout(timeout);
furni5.state = 0;
}
//x@}
};
}

126
src/Scuti.ts Normal file
View File

@ -0,0 +1,126 @@
import { Application, BaseTexture, Container, SCALE_MODES, settings } from 'pixi.js'
import { PixiPlugin } from 'gsap/PixiPlugin'
import { gsap } from 'gsap'
import { Stage } from '@pixi/layers'
import { Logger } from './utilities/Logger'
import type { IRendererConfiguration } from './types/Configuration'
import { AssetLoader } from './utilities/AssetLoader'
/**
* Convenience class to create a new Scuti renderer.
*
* This class automatically load all the needed resources and initialise the PixiJS application.
* @example
* import { Scuti } from 'scuti-renderer';
*
* // Create the renderer
* const renderer = new Scuti({
* canvas: document.getElementById("app"),
* width: window.innerWidth,
* height: window.innerHeight,
* resources: './resources'
* });
* await renderer.loadResources();
*
* @class
* @memberof Scuti
*/
export class Scuti {
/**
* The canvas that will be used to render the PixiJS canvas.
*
* @member {HTMLElement}
* @private
*/
private readonly _canvas: HTMLElement
/**
* The PixiJS application instance that will be used to render everything.
*
* @member {Application}
* @private
*/
private readonly _application: Application
/**
* The renderer logger instance.
*
* @member {Logger}
* @private
*/
private readonly _logger: Logger = new Logger('Scuti')
/**
* @param {IRendererConfiguration} [configuration] - The renderer configuration.
* @param {HTMLElement} [configuration.canvas] - The canvas that will be used to render everything.
* @param {number} [configuration.width] - The width of the render part.
* @param {number} [configuration.height] - The height of the render part.
* @param {string} [configuration.resources] - The URL of the resource server.
**/
constructor(configuration: IRendererConfiguration) {
this._logger.info('⚡ Scuti Renderer - v1.0.0')
/** Change the PixiJS settings and default settings */
settings.RESOLUTION = 1
Container.defaultSortableChildren = true
BaseTexture.defaultOptions.scaleMode = SCALE_MODES.NEAREST
/** Register the plugins */
gsap.registerPlugin(PixiPlugin)
/** Create the PixiJS application */
this._application = new Application({
width: configuration.width,
height: configuration.height,
resolution: 1,
antialias: false
})
/** Support for PIXI.js dev-tool */
if (process.env.NODE_ENV === 'development') (globalThis as any).__PIXI_APP__ = this._application
this._application.stage = new Stage()
this._canvas = configuration.canvas
/** Append it to the canvas */
this._canvas.append(this._application.view as unknown as Node)
}
/**
* It loads all the resources that are essentials for rendering rooms and objects.
* It's necessary to call this method just after the instanciation of this class.
*
* @member {Promise<void>}
* @public
*/
public async loadResources(domain: string = 'http://127.0.0.1:8081/'): Promise<void> {
AssetLoader.domain = domain
/** And now load them */
await Promise.all([
AssetLoader.load('room/materials', 'generic/room/room_data.json'),
AssetLoader.load('room/room', 'generic/room/room.json'),
AssetLoader.load('room/cursors', 'generic/tile_cursor/tile_cursor.json'),
AssetLoader.load('furnitures/floor/placeholder', 'generic/place_holder/place_holder_furniture.json'),
AssetLoader.load('furnitures/wall/placeholder', 'generic/place_holder/place_holder_wall_item.json'),
AssetLoader.load('furnitures/furnidata', 'gamedata/furnidata.json'),
AssetLoader.load('figures/figuredata', 'gamedata/figuredata.json'),
AssetLoader.load('figures/figuremap', 'gamedata/figuremap.json'),
AssetLoader.load('figures/draworder', 'gamedata/draworder.json'),
AssetLoader.load('figures/actions', 'generic/HabboAvatarActions.json'),
AssetLoader.load('figures/partsets', 'generic/HabboAvatarPartSets.json'),
AssetLoader.load('figures/animations', 'generic/HabboAvatarAnimations.json')
])
}
/**
* Reference to the PixiJS application instance.
*
* @member {Application}
* @readonly
* @public
*/
public get application(): Application {
return this._application
}
}

16
src/enums/Direction.ts Normal file
View File

@ -0,0 +1,16 @@
/**
* Direction enum regroup the 8 directions that an avatar can handle on Habbo.
*
* @enum
* @memberof Scuti
*/
export enum Direction {
NORTH /** Direction 0 */,
NORTH_EAST /** Direction 1 */,
EAST /** Direction 2 */,
SOUTH_EAST /** Direction 3 */,
SOUTH /** Direction 4 */,
SOUTH_WEST /** Direction 5 */,
WEST /** Direction 6 */,
NORTH_WEST /** Direction 7 */
}

11
src/enums/StairType.ts Normal file
View File

@ -0,0 +1,11 @@
/**
* StairType enum regroup the 3 differents type of stairs available on Habbo.
*
* @enum
* @memberof Scuti
*/
export enum StairType {
INNER_CORNER_STAIR,
OUTER_CORNER_STAIR,
STAIR
}

12
src/enums/WallType.ts Normal file
View File

@ -0,0 +1,12 @@
/**
* WallType enum regroup the 3 differents type of walls available on Habbo.
*
* @enum
* @memberof Scuti
*/
export enum WallType {
CORNER_WALL,
LEFT_WALL,
RIGHT_WALL,
DOOR_WALL
}

8
src/index.ts Normal file
View File

@ -0,0 +1,8 @@
export { Scuti } from './Scuti';
export { Room } from './objects/rooms/Room';
export { FloorMaterial } from './objects/rooms/materials/FloorMaterial';
export { FloorFurniture } from './objects/furnitures/FloorFurniture';
export { WallMaterial } from './objects/rooms/materials/WallMaterial';
export { WallFurniture } from './objects/furnitures/WallFurniture';
export { Avatar } from './objects/avatars/Avatar';
export { AvatarAction } from './objects/avatars/actions/AvatarAction';

View File

@ -0,0 +1,98 @@
import { gsap } from 'gsap';
import type { Direction } from '../../enums/Direction';
import { AvatarAction } from './actions/AvatarAction';
import { AvatarActionManager } from './actions/AvatarActionManager';
import { AvatarAnimationManager } from './animations/AvatarAnimationManager';
import type { AvatarBodyPart } from './visualizations/AvatarBodyPart';
import type { IAvatarConfig, IAvatarPosition } from '../../types/Avatar';
import { RoomObject } from '../rooms/objects/RoomObject';
import { AvatarVisualization } from './visualizations/AvatarVisualization';
import { AvatarFigure } from './AvatarFigure';
export class Avatar extends RoomObject {
private readonly _figure: AvatarFigure;
private _bodyDirection: Direction;
private _headDirection: Direction;
private _actions: AvatarAction[];
private readonly _actionManager: AvatarActionManager;
private readonly _animationManager: AvatarAnimationManager;
private readonly _bodyParts: AvatarBodyPart[] = [];
constructor(config: IAvatarConfig) {
super(config);
this._headDirection = config.headDirection;
this._bodyDirection = config.bodyDirection;
this._actions = config.actions;
this._figure = new AvatarFigure(this, config.figure);
this._visualization = new AvatarVisualization(this);
this._actionManager = new AvatarActionManager(AvatarAction.Default);
this._animationManager = new AvatarAnimationManager(config.actions);
}
public addAction(action: AvatarAction): void {
if (!Boolean(this._animationManager.getAnimation(action))) this._animationManager.registerAnimation(action);
if (!this._actions.includes(action)) this._actions.push(action);
}
public removeAction(action: AvatarAction): void {
this._actions = this._actions.filter((fAction) => {
return fAction !== action;
});
}
move(position: IAvatarPosition, duration: number = 0.5): void {
if (this._visualization === undefined) return;
gsap.to(this.position, {
x: 32 * position.x - 32 * position.y,
y: 16 * position.x + 16 * position.y - 32 * position.z,
duration,
ease: 'linear',
onUpdate: () => this._visualization.updatePosition()
});
}
public get actions(): AvatarAction[] {
return this._actions;
}
public set actions(actions: AvatarAction[]) {
actions.forEach((action) => this._animationManager.registerAnimation(action));
actions.forEach((action) => this.actions.push(action));
this._actions = actions;
}
public get headDirection(): Direction {
return this._headDirection;
}
public set headDirection(direction: Direction) {
this._headDirection = direction;
}
public get bodyDirection(): Direction {
return this._bodyDirection;
}
public set bodyDirection(direction: Direction) {
this._bodyDirection = direction;
}
public get actionManager(): AvatarActionManager {
return this._actionManager;
}
public get bodyParts(): AvatarBodyPart[] {
return this._bodyParts;
}
public get animationManager(): AvatarAnimationManager {
return this._animationManager;
}
public get figure(): AvatarFigure {
return this._figure;
}
}

View File

@ -0,0 +1,79 @@
import { Assets } from 'pixi.js';
import type { Figure, IAvatarPart, IFigureData, IFigureMap } from '../../types';
import type { Avatar } from './Avatar';
import { AvatarBodyPart } from './visualizations/AvatarBodyPart';
export class AvatarFigure {
private readonly _avatar: Avatar;
private _figure!: Figure;
constructor(avatar: Avatar, figure: string) {
this._avatar = avatar;
this._parseFigure(figure);
this._parseBodyParts();
}
private _parseFigure(figure: string): void {
// todo!(): Handle split errors for the string
this._figure = new Map(
figure.split('.').map((part) => {
const data = part.split('-');
return [
data[0],
{
setId: Number(data[1]),
colors: data.splice(2, 2).map((color) => Number(color))
}
] as const;
})
);
}
private _parseBodyParts(): void {
return this._figure.forEach((set, type) => {
const parts = this._getParts(type, set.setId);
return this._avatar.bodyParts.push(
new AvatarBodyPart(this._avatar, {
type,
setId: set.setId,
colors: set.colors,
parts,
actions: this._avatar.actions
})
);
});
}
private _getParts(type: string, setId: number): IAvatarPart[] {
const figureData = Assets.get<IFigureData>('figures/figuredata');
const figureMap = Assets.get<IFigureMap>('figures/figuremap');
let parts: IAvatarPart[] = [];
if (!Boolean(figureData.settype[type]?.set[setId])) return parts;
const hiddenLayers = figureData.settype[type].set[setId].hiddenLayers;
const set = figureData.settype[type].set[setId];
set?.parts.forEach((part) => {
const libId = figureMap.parts[part.type][String(part.id)];
const lib = figureMap.libs[libId];
//console.log(part.type, libId);
part.lib = lib;
parts.push(part);
});
if (hiddenLayers !== undefined) {
parts = parts.filter((part) => !hiddenLayers.includes(part.type));
}
return parts;
}
public get figure(): Figure {
return this._figure;
}
}

View File

@ -0,0 +1,30 @@
export enum AvatarAction {
Default = 'Default',
Walk = 'Move',
Sleep = 'Sleep',
GestureSurprised = 'GestureSurprised',
GestureAngry = 'GestureAngry',
GestureSad = 'GestureSad',
GestureSmile = 'GestureSmile',
Gesture = 'Gesture',
Talk = 'Talk',
CarryItem = 'CarryItem',
UseItem = 'UseItem',
Dance = 'Dance',
AvatarEffect = 'AvatarEffect',
Idle = 'Idle',
Laugh = 'Laugh',
Blow = 'Blow',
Sign = 'Sign',
Wave = 'Wave',
Respect = 'Respect',
RideJump = 'RideJump',
SnowboardSquat = 'SnowboardSquat',
SnowboardUp = 'SnowboardUp',
SnowboardOllie = 'SnowboardOllie',
Snowboard360 = 'Snowboard360',
Sit = 'Sit',
Swim = 'Swim',
Float = 'Float',
Lay = 'Lay'
}

View File

@ -0,0 +1,48 @@
import { Assets } from 'pixi.js';
import type { AvatarAction } from './AvatarAction';
import type { IActionDefinition, IAvatarPartSets } from '../../../types/Avatar';
export class AvatarActionManager {
private readonly _avatarActionsLib: IActionDefinition[] = Assets.get('figures/actions');
private readonly _avatarPartSetsLib: IAvatarPartSets = Assets.get('figures/partsets');
constructor(private readonly _defaultAction: AvatarAction) {}
public filterActions(actions: AvatarAction[], partType: string): AvatarAction[] {
return actions.filter((action: AvatarAction) => {
const actionDefinition: IActionDefinition = this._avatarActionsLib[action];
return (
actionDefinition !== undefined &&
this._avatarPartSetsLib.activePartSets[actionDefinition.activepartset].includes(partType)
);
});
}
public sortActions(actions: AvatarAction[]): AvatarAction[] {
if (actions.length === 0) return [this._defaultAction];
return actions.sort((a: AvatarAction, b: AvatarAction) => {
const actionDefinitionA: IActionDefinition = this._avatarActionsLib[a];
const actionDefinitionB: IActionDefinition = this._avatarActionsLib[b];
if (Number(actionDefinitionA.precedence) < Number(actionDefinitionB.precedence)) return -1;
if (Number(actionDefinitionA.precedence) > Number(actionDefinitionB.precedence)) return 1;
return 0;
});
}
public getActionDefinition(action: AvatarAction): IActionDefinition {
return this._avatarActionsLib[action];
}
public get defaultAction(): AvatarAction {
return this._defaultAction;
}
public get definitions(): IActionDefinition[] {
return this._avatarActionsLib;
}
public get partSets(): IAvatarPartSets {
return this._avatarPartSetsLib;
}
}

View File

@ -0,0 +1,15 @@
import type { IAnimationDefinition, IAnimationFrameData } from '../../../types/Avatar';
import type { AvatarAction } from '../actions/AvatarAction';
export class AvatarAnimation {
constructor(_action: AvatarAction, private readonly _definition: IAnimationDefinition) {}
public getFrame(frame: number, type: string): IAnimationFrameData {
// @ts-expect-error
return this._definition.frames[frame].bodyparts[type];
}
public getFrameCount(): number {
return this._definition.frames.length;
}
}

View File

@ -0,0 +1,33 @@
import { Assets } from 'pixi.js';
import type { IAvatarPartSets, IAnimationFrameData } from '../../../types/Avatar';
import type { AvatarAction } from '../actions/AvatarAction';
import { AvatarAnimation } from './AvatarAnimation';
export class AvatarAnimationManager {
private readonly _animations = new Map<AvatarAction, AvatarAnimation>();
private readonly _avatarAnimationsLib = Assets.get<IAvatarPartSets>('figures/animations');
constructor(actions: AvatarAction[]) {
actions.forEach((action) => this.registerAnimation(action));
}
public registerAnimation(action: AvatarAction): void {
if (this._avatarAnimationsLib[action] === undefined) return;
this._animations.set(action, new AvatarAnimation(action, this._avatarAnimationsLib[action]));
}
public getAnimation(action: AvatarAction): AvatarAnimation {
return this._animations.get(action);
}
public getLayerData(action: AvatarAction, frame: number, type: string): IAnimationFrameData {
const animation = this.getAnimation(action);
if (animation === undefined) return;
return animation.getFrame(frame, type);
}
public get animations(): IAvatarPartSets {
return this._avatarAnimationsLib;
}
}

View File

@ -0,0 +1,231 @@
import { Assets } from 'pixi.js';
import type { IAvatarPart, IBodyPartConfiguration } from '../../../types/Avatar';
import { AssetLoader } from '../../../utilities/AssetLoader';
import { ZOrder } from '../../../utilities/ZOrder';
import { AvatarAction } from '../actions/AvatarAction';
import type { Avatar } from '../Avatar';
import { AvatarLayer } from './AvatarLayer';
export class AvatarBodyPart {
private readonly _avatar: Avatar;
private readonly _type: string; // hr - hd - ch - lg - sh
private readonly _setId: number;
private readonly _colors: number[];
private readonly _parts: IAvatarPart[]; // lib.id, etc...
private _actions: AvatarAction[];
private readonly _frames: Map<number, Map<string, { action: AvatarAction; frame: number; repeat: number }>> =
new Map();
private areAllAssetsLoaded = false;
constructor(avatar: Avatar, config: IBodyPartConfiguration) {
this._avatar = avatar;
this._type = config.type;
this._setId = config.setId;
this._colors = config.colors;
this._parts = config.parts;
this._actions = config.actions;
const assets: Array<Promise<void>> = [];
this._parts.forEach((part: IAvatarPart) => {
if (part.lib != null)
assets.push(AssetLoader.load('figures/' + part.lib.id, 'figure/' + part.lib.id + '/' + part.lib.id + '.json'));
});
Promise.all(assets)
.then(() => (this.areAllAssetsLoaded = true))
.catch((error) => this._avatar.logger.error(error));
}
private _draw(): void {
if (!this.areAllAssetsLoaded) return;
this._parts.forEach((part) => this._createPart(part));
}
private _createPart(part: IAvatarPart): void {
if (!this._frames.has(part.id)) this._frames.set(part.id, new Map());
if (part.lib == null) return;
const spritesheet = Assets.get('figures/' + part.lib.id);
Object.keys(spritesheet.data.partsType).forEach((type) => {
// We register the part type if it's not already registered
if (!this._frames.get(part.id).has(type))
this._frames.get(part.id).set(type, {
action: AvatarAction.Default,
frame: 0,
repeat: 0
});
let direction = this._avatar.bodyDirection;
// We get the actions, check if it's valid and if the action is included in the active part set
const sortedActions = this._avatar.actionManager.filterActions(this._actions, type);
let finalAction: AvatarAction = this._avatar.actionManager.sortActions(sortedActions)[0];
// If this part type is in the head part set, we put the direction equal to the head direction
if (this._isHeadPart(type)) direction = this._avatar.headDirection;
// We get the animation gesture and frame
const frameData = this._avatar.animationManager.getLayerData(
finalAction,
this._frames.get(part.id).get(type).frame,
type
);
let gesture: string = this._avatar.actionManager.getActionDefinition(finalAction).assetpartdefinition;
let frame: number = 0;
let flip: boolean = false;
if (frameData !== undefined) {
this._frames.get(part.id).get(type).action = finalAction;
frame = frameData.frame;
gesture = frameData.assetpartdefinition;
}
if ([4, 5, 6].includes(direction)) {
flip = true;
}
// const zDirection = direction;
let tempDirection: number = direction;
if ([4, 5, 6].includes(tempDirection)) tempDirection = 6 - tempDirection;
// If the texture don't exist we reinitalise the gesture and the final action
if (
spritesheet.textures[
// Skipping type checking because we cannot convert
part.lib.id + '_h_' + gesture + '_' + type + '_' + part.id + '_' + tempDirection + '_' + frame
] === undefined
) {
gesture = 'std';
finalAction = AvatarAction.Default;
this._frames.get(part.id).get(type).action = finalAction;
}
if (
((gesture === 'wav' && (type === 'lh' || type === 'ls' || type === 'lc' || type === 'lcs')) ||
(gesture === 'drk' && (type === 'rh' || type === 'rs' || type === 'rcs')) ||
(gesture === 'blw' && type === 'rh') ||
(gesture === 'sig' && type === 'lh') ||
(gesture === 'respect' && type === 'lh')) &&
[4, 5, 6].includes(this._avatar.bodyDirection)
) {
flip = !flip;
}
if (
(gesture === 'crr' || gesture === 'respect' || gesture === 'sig') &&
[4, 5, 6].includes(this._avatar.bodyDirection)
) {
if (
this._avatar.actionManager.partSets.partSets[type] !== undefined &&
this._avatar.actionManager.partSets.partSets[type]['flipped-set-type'] !== undefined &&
[4, 5, 6, 7].includes(direction)
) {
type = this._avatar.actionManager.partSets.partSets[type]['flipped-set-type'];
}
}
const zOrder = ZOrder.avatar(this._avatar.position, this._getDrawOrder(type, gesture, direction));
// We create the layer
if (
spritesheet.textures[
// Skipping type checking because we cannot convert
part.lib.id + '_h_' + gesture + '_' + type + '_' + part.id + '_' + tempDirection + '_' + frame
] !== undefined
) {
const layer = new AvatarLayer(this._avatar, {
type,
part,
gesture,
tint:
part.colorable === 1 && type !== 'ey' ? this._getColor(this._type, this._colors[part.index]) : undefined,
z: zOrder,
flip,
direction,
frame
});
/*let tempType: string = type;
if(this._avatar.actionManager.partSets.partSets[type] !== undefined && this._avatar.actionManager.partSets.partSets[type]["flipped-set-type"] !== undefined && [4, 5, 6, 7].includes(direction)) {
tempType = this._avatar.actionManager.partSets.partSets[type]["flipped-set-type"];
if(spritesheet.data.frames[part.lib.id + "_h_std_" + tempType + "_" + part.id + "_" + tempDirection + "_0"] !== undefined) {
layer.x = spritesheet.data.frames[part.lib.id + "_h_std_" + tempType + "_" + part.id + "_" + tempDirection + "_0"].spriteSourceSize.x;
layer.y = spritesheet.data.frames[part.lib.id + "_h_std_" + tempType + "_" + part.id + "_" + tempDirection + "_0"].spriteSourceSize.y;
}
}*/
this._avatar.addChild(layer);
}
});
}
private _isHeadPart(type: string): boolean {
return this._avatar.actionManager.partSets.activePartSets.head.includes(type);
}
// types must be changed here
private _getColor(type: string, colorId: number): number {
const figureData: [] = Assets.get('figures/figuredata');
const paletteId = figureData.settype[type].paletteid;
const palette = figureData.palette[String(paletteId)];
if (palette[String(colorId)] === undefined) return Number('0xFFFFFF');
return Number('0x' + String(palette[String(colorId)].color));
}
private _getDrawOrder(type: string, action: string, direction: number): number {
const drawOrder: [] = Assets.get('figures/draworder');
const drawOrderList = Object.entries(
drawOrder[drawOrder[action] !== undefined ? action : 'std'][direction] ?? drawOrder.std[direction]
).find((entry) => {
return entry[1] === type;
});
return drawOrderList !== undefined ? Number(drawOrderList[0]) : 0;
}
public updateParts(): void {
this._frames.forEach((types, partId) => {
types.forEach((value, type) => {
const animation = this._avatar.animationManager.getAnimation(value.action);
const frameData = this._avatar.animationManager.getLayerData(value.action, value.frame, type);
if (frameData !== undefined) {
const currentFrame = this._frames.get(partId).get(type);
if (frameData.repeats !== undefined) {
if (currentFrame.repeat >= Number(frameData.repeats)) {
currentFrame.repeat = 0;
currentFrame.frame = currentFrame.frame >= animation.getFrameCount() - 1 ? 0 : currentFrame.frame + 1;
} else {
currentFrame.repeat += 1;
}
} else {
currentFrame.frame = currentFrame.frame >= animation.getFrameCount() - 1 ? 0 : currentFrame.frame + 1;
}
}
});
});
return this._draw();
}
public get actions(): AvatarAction[] {
return this._actions;
}
public set actions(actions: AvatarAction[]) {
this._actions = actions;
}
public get frames(): Map<number, Map<string, { action: AvatarAction; frame: number; repeat: number }>> {
return this._frames;
}
}

View File

@ -0,0 +1,110 @@
import { Assets, Container } from 'pixi.js';
import { Color } from '@pixi/color';
import type { Avatar } from '../Avatar';
import type { IAvatarLayerConfiguration, IAvatarPart } from '../../../types/Avatar';
import { HitSprite } from '../../interactions/HitSprite';
import type { Direction } from '../../../enums/Direction';
export class AvatarLayer extends Container {
private readonly _avatar: Avatar;
private readonly _type: string;
private readonly _part: IAvatarPart;
private readonly _gesture: string;
/**
* The layer tint
* @private
*/
private readonly _tint: number;
/**
* The layer z
* @private
*/
private readonly _z: number;
private readonly _direction: Direction;
/**
* Is the layer flipped
* @private
*/
private readonly _flip: boolean;
private readonly _frame: number;
private readonly _alpha: number;
constructor(avatar: Avatar, configuration: IAvatarLayerConfiguration) {
super();
this._avatar = avatar;
this._type = configuration.type;
this._part = configuration.part;
this._gesture = configuration.gesture;
this._tint = configuration.tint;
this._z = configuration.z;
this._flip = configuration.flip;
this._direction = configuration.direction;
this._frame = configuration.frame;
this._alpha = configuration.alpha;
this._draw();
}
private _draw(): void {
let tempDirection = this._direction;
if (this._flip && [4, 5, 6].includes(tempDirection)) this.scale.x = -1;
if (this._flip && [4, 5, 6].includes(tempDirection)) this.x = 64;
if ([4, 5, 6].includes(tempDirection) && this._flip) {
tempDirection = 6 - tempDirection;
}
// const avatarActions: IActionDefinition[] = Assets.get('figures/actions')
// if(this._type === "ls" || this._type === "lh" || this._type === "lc") console.log(2, this._part.lib.id + "_h_" + this._gesture + "_" + this._type + "_" + this._part.id + "_" + tempDirection + "_" + this._frame);
const sprite = new HitSprite(
Assets.get('figures/' + this._part.lib.id).textures[
this._part.lib.id +
'_h_' +
this._gesture +
'_' +
this._type +
'_' +
String(this._part.id) +
'_' +
String(tempDirection) +
'_' +
String(this._frame)
]
);
if (this._tint !== undefined) sprite.tint = new Color(this._tint).premultiply(1).toNumber();
if (this._avatar.room !== undefined) this.parentLayer = this._avatar.room.objects.layer;
if (this._z !== undefined) this.zOrder = this._z;
if (this._alpha !== undefined) sprite.alpha = this._alpha;
//sprite.animationSpeed = 0.167;
//sprite.play();
sprite.interactive = true;
sprite.on('pointerdown', (event) => {
return this._avatar.eventManager.handlePointerDown({ event });
});
sprite.on('pointerup', (event) => {
return this._avatar.eventManager.handlePointerUp({ event });
});
sprite.on('pointermove', (event) => {
return this._avatar.eventManager.handlePointerMove({ event });
});
sprite.on('pointerout', (event) => {
return this._avatar.eventManager.handlePointerOut({ event });
});
sprite.on('pointerover', (event) => {
return this._avatar.eventManager.handlePointerOver({ event });
});
this._avatar.addChild(sprite);
}
public get avatar(): Avatar {
return this._avatar;
}
}

View File

@ -0,0 +1,134 @@
import type { IAvatarPosition, Nullable } from '../../../types';
import { AssetLoader } from '../../../utilities/AssetLoader';
import { RoomObjectVisualization } from '../../rooms/objects/RoomObjectVisualization';
import { AvatarAction } from '../actions/AvatarAction';
import type { Avatar } from '../Avatar';
import { AvatarLayer } from './AvatarLayer';
export class AvatarVisualization extends RoomObjectVisualization {
private readonly _avatar: Avatar;
private readonly _handItem: Nullable<number>;
constructor(avatar: Avatar) {
super();
this._avatar = avatar;
this._loadAssets();
this._avatar.onRoomAdded = (room) => {
if (this.loaded) room.visualization.animationTicker.add(() => this.render());
};
}
private _loadAssets(): void {
const assets: Array<Promise<void>> = [];
if (this._avatar.onLoad !== undefined) this._avatar.onLoad();
if (this._hasHandItem())
assets.push(AssetLoader.load('figures/hh_human_item', 'figure/hh_human_item/hh_human_item.json'));
assets.push(AssetLoader.load('figures/hh_human_body', 'figure/hh_human_body/hh_human_body.json'));
Promise.all(assets)
.then(() => {
if (this._avatar.onLoadComplete !== undefined) this._avatar.onLoadComplete();
this.loaded = true;
if (this.placeholder !== undefined) this.placeholder.destroy();
if (this._avatar.room != null) this._avatar.room.visualization.animationTicker.add(() => this.render());
})
.catch(() => {
this.logger.error('Unable to load the assets');
});
}
render(): void {
this.destroy();
if (!this.loaded) return;
if (this._hasHandItem()) this._createHandItem();
this._createShadow();
this._avatar.bodyParts.forEach((bodyPart) => {
bodyPart.actions = this._avatar.actions;
bodyPart.updateParts();
});
const position = this._avatar.position as IAvatarPosition;
this._avatar.x = 32 * position.x - 32 * position.y;
this._avatar.y = 16 * position.x + 16 * position.y - 32 * position.z;
}
// todo!(): destroy avatar's bodyparts
destroy(): void {
// if (this.placeholder.parent !== null) this.placeholder.destroy();
}
// todo!(): add figure placeholder
renderPlaceholder(): void {
// const position = this._avatar.position as IAvatarPosition;
// this.placeholder = new Sprite(Assets.get('furnitures/floor/placeholder').textures['place_holder_furniture_64.png']);
// if (this._avatar.room != null) this._avatar.addChild(this.placeholder);
// this.placeholder.x = 32 + 32 * position.x - 32 * position.y - 32;
// this.placeholder.y = 16 * position.x + 16 * position.y - 32 * position.z - 50;
}
updatePosition(): void {}
private _createShadow(): void {
this._avatar.addChild(
new AvatarLayer(this._avatar, {
type: 'sd',
part: { id: 1, lib: { id: 'hh_human_body' } },
gesture: 'std',
tint: undefined,
z: 0,
flip: true,
direction: 0,
frame: 0,
alpha: 0.1
})
);
}
private _createHandItem(): void {
const item = this._handItem;
const handItemCarryId = this._avatar.actionManager.getActionDefinition(AvatarAction.CarryItem).params[String(item)];
const handItemUseId = this._avatar.actionManager.getActionDefinition(AvatarAction.UseItem).params[String(item)];
if (this._avatar.actions.includes(AvatarAction.UseItem) && handItemUseId !== undefined) {
this._avatar.addChild(
new AvatarLayer(this._avatar, {
type: 'ri',
part: { id: handItemUseId, lib: { id: 'hh_human_item' } },
gesture: 'drk',
tint: undefined,
z: 1000,
flip: false,
direction: this._avatar.bodyDirection,
frame: 0
})
);
} else if (this._avatar.actions.includes(AvatarAction.CarryItem) && handItemCarryId !== undefined) {
this._avatar.addChild(
new AvatarLayer(this._avatar, {
type: 'ri',
part: { id: handItemCarryId, lib: { id: 'hh_human_item' } },
gesture: 'crr',
tint: undefined,
z: 1000,
flip: false,
direction: this._avatar.bodyDirection,
frame: 0
})
);
}
}
private _hasHandItem(): boolean {
return this._handItem !== 0 || this._handItem !== undefined;
}
}

View File

@ -0,0 +1,52 @@
import { Color, Filter } from 'pixi.js';
/** The shader vertex */
const vertex = `
attribute vec2 aVertexPosition;
attribute vec2 aTextureCoord;
uniform mat3 projectionMatrix;
varying vec2 vTextureCoord;
void main(void)
{
gl_Position = vec4((projectionMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0);
vTextureCoord = aTextureCoord;
}
`;
/** The shader fragment */
const fragment = `
varying vec2 vTextureCoord;
uniform sampler2D uSampler;
uniform vec3 lineColor;
uniform vec3 backgroundColor;
void main(void) {
vec4 currentColor = texture2D(uSampler, vTextureCoord);
vec3 colorLine = lineColor * currentColor.a;
vec3 colorOverlay = backgroundColor * currentColor.a;
if(currentColor.r == 0.0 && currentColor.g == 0.0 && currentColor.b == 0.0 && currentColor.a > 0.0) {
gl_FragColor = vec4(colorLine.r, colorLine.g, colorLine.b, currentColor.a);
} else if(currentColor.a > 0.0) {
gl_FragColor = vec4(colorOverlay.r, colorOverlay.g, colorOverlay.b, currentColor.a);
}
}
`;
/**
* WiredSelectionFilter class that aim to reproduce the wired selection effect.
*
* @class
* @memberof Scuti
*/
export class WiredSelectionFilter extends Filter {
/**
* @param {number} [lineColor] - The color of the furniture border when selected.
* @param {number} [backgroundColor] - The main color of the furniture when selected.
**/
constructor(lineColor: number, backgroundColor: number) {
super(vertex, fragment);
/** Set the colors */
this.uniforms.lineColor = new Color(lineColor).toRgbArray();
this.uniforms.backgroundColor = new Color(backgroundColor).toRgbArray();
}
}

View File

@ -0,0 +1,65 @@
import { gsap } from 'gsap';
import type { IFloorFurnitureConfiguration, IFloorPosition } from '../../types/Furniture';
import { FurnitureData } from './visualizations/FurnitureData';
import { FurnitureVisualization } from './visualizations/FurnitureVisualization';
import { RoomObject } from '../rooms/objects/RoomObject';
/**
* FloorFurniture class that aim to reproduce the floor furnitures on Habbo.
*
* @class
* @memberof Scuti
*/
export class FloorFurniture extends RoomObject {
/**
* The furniture id that represent the one in furnidata.
*
* @member {number}
* @private
*/
private readonly _id: number;
/**
* @param {IFloorFurnitureConfiguration} [config] - The furniture configuration.
*/
constructor(config: IFloorFurnitureConfiguration) {
super(config);
this._id = config.id;
this._state = config.state ?? 0;
this._data = new FurnitureData(this);
this._visualization = new FurnitureVisualization(this);
}
/**
* Reference to the furniture id from the furni data.
*
* @member {number}
* @readonly
* @public
*/
public get id(): number {
return this._id;
}
/**
* Move the furniture at the given position and in time.
*
* @param {IFloorPosition} [position] - The position where we want to move the furniture.
* @param {number} [duration] - The time to move the furniture to the given position.
* @return {void}
* @public
*/
move(position: IFloorPosition, duration: number = 0.5): void {
if (this._visualization === undefined) return;
gsap.to(this.position, {
x: position.x,
y: position.y,
z: position.z,
duration,
ease: 'linear',
onUpdate: () => this._visualization.updatePosition()
});
}
}

View File

@ -0,0 +1,64 @@
import { gsap } from 'gsap';
import type { IWallFurniConfig, IWallPosition } from '../../types/Furniture';
import { RoomObject } from '../rooms/objects/RoomObject';
import { FurnitureData } from './visualizations/FurnitureData';
import { FurnitureVisualization } from './visualizations/FurnitureVisualization';
/**
* WallFurniture class that aim to reproduce the wall furnitures on Habbo.
*
* @class
* @memberof Scuti
*/
export class WallFurniture extends RoomObject {
/**
* The furniture's id that represent the one in furnidata.
*
* @member {number}
* @private
*/
private readonly _id: number;
/**
* @param {IWallFurniConfig} [config] - The furniture configuration.
*/
constructor(config: IWallFurniConfig) {
super(config);
this._id = config.id;
this._state = config.state ?? 0;
this._data = new FurnitureData(this);
this._visualization = new FurnitureVisualization(this);
}
/**
* Reference to the furniture id from the furni data.
*
* @member {number}
* @readonly
* @public
*/
public get id(): number {
return this._id;
}
/**
* Move the furniture at the given position and in time.
*
* @param {IWallPosition} [position] - The position where we want to move the furniture.
* @param {number} [duration] - The time to move the furniture to the given position.
* @return {void}
* @public
*/
move(position: IWallPosition, duration: number = 0.5): void {
if (this._visualization === undefined) return;
gsap.to(this.position, {
x: position.x,
y: position.y,
duration,
ease: 'linear',
onUpdate: () => this._visualization.updatePosition()
});
}
}

View File

@ -0,0 +1,97 @@
import { Assets } from 'pixi.js';
import type { ISharedFurniData } from '../../../types/Furniture';
import { FloorFurniture } from '../FloorFurniture';
import type { WallFurniture } from '../WallFurniture';
/**
* FurnitureData class that manage the data of a furniture.
*
* @class
* @memberof Scuti
*/
export class FurnitureData {
/**
* The furniture instance that we want to retrieve data.
*
* @member {FloorFurniture | WallFurniture}
* @private
*/
private readonly _furniture: FloorFurniture | WallFurniture;
/**
* The furniture data.
*
* @member {ISharedFurniData}
* @private
*/
private _data!: ISharedFurniData;
/**
* @param {FloorFurniture | WallFurniture} [furniture] - The furniture instance.
*/
constructor(furniture: FloorFurniture | WallFurniture) {
this._furniture = furniture;
this._load();
}
/**
* Load the furniture data.
*
* @return {void}
* @private
*/
private _load(): void {
const furniType = this._furniture instanceof FloorFurniture ? 'floorItems' : 'wallItems';
this._data = Assets.get('furnitures/furnidata')[furniType].find((item: ISharedFurniData) => {
return item.id === this._furniture.id;
});
}
/**
* Reference to the furniture id.
*
* @member {number}
* @readonly
* @public
*/
public get id(): number {
return this._data.id;
}
/**
* Reference to the furniture class name.
*
* @member {string}
* @readonly
* @public
*/
public get className(): string {
return this._data.className;
}
/**
* Reference to the furniture base name.
*
* @member {string}
* @readonly
* @public
*/
public get baseName(): string {
if (!Boolean(this._data.className.includes('*'))) return this._data.className;
return this._data.className.split('*')[0];
}
/**
* Reference to the furniture color id.
*
* @member {number}
* @readonly
* @public
*/
public get color(): number | null {
if (!Boolean(this._data.className.includes('*'))) return null;
return Number(this._data.className.split('*')[1]);
}
}

View File

@ -0,0 +1,202 @@
import type { BLEND_MODES } from 'pixi.js';
import { Assets } from 'pixi.js';
import { Color } from '@pixi/color';
import type { FloorFurniture } from '../FloorFurniture';
import type { IFurnitureLayerConfiguration } from '../../../types/Furniture';
import { HitSprite } from '../../interactions/HitSprite';
import type { WallFurniture } from '../WallFurniture';
import type { Direction } from '../../../enums/Direction';
import { WiredSelectionFilter } from '../../filters/WiredSelectionFilter';
/** The wired selection filter */
const WIRED_SELECTION_FILTER = new WiredSelectionFilter(0xffffff, 0x999999);
/**
* FurnitureLayer class.
*
* @class
* @memberof Scuti
*/
export class FurnitureLayer extends HitSprite {
/**
* The furniture instance.
*
* @member {FloorFurniture | WallFurniture}
* @private
*/
private readonly _furniture: FloorFurniture | WallFurniture;
/**
* The layer id.
*
* @member {number | string}
* @private
*/
private readonly _layer: number;
/**
* The layer alpha.
*
* @member {number}
* @private
*/
private readonly _alpha: number;
/**
* The layer tint.
*
* @member {number}
* @private
*/
private readonly _tint: number;
/**
* The layer z index.
*
* @member {number}
* @private
*/
private readonly _z: number;
/**
* The layer blend mode.
*
* @member {BLEND_MODES}
* @private
*/
private readonly _blendMode: BLEND_MODES;
/**
* Is the layer flipped.
*
* @member {boolean}
* @private
*/
private readonly _flip: boolean;
/**
* The layer frame id.
*
* @member {number}
* @private
*/
private readonly _frame: number;
/**
* Is the layer interactive.
*
* @member {boolean}
* @private
*/
private readonly _ignoreMouse: boolean;
/**
* The layer direction.
*
* @member {Direction}
* @private
*/
private readonly _direction: Direction;
/**
* The layer tag.
*
* @member {string}
* @private
*/
private readonly _tag: string;
/**
* @param {FloorFurniture | WallFurniture} [furniture] - The furniture instance.
* @param {IFurnitureLayerConfiguration} [config] - The layer configuration.
*/
constructor(furniture: FloorFurniture | WallFurniture, config: IFurnitureLayerConfiguration) {
super(undefined);
this._furniture = furniture;
this._layer = config.layer;
this._alpha = config.alpha;
this._tint = config.tint;
this._z = config.z;
this._blendMode = config.blendMode;
this._flip = config.flip;
this._frame = config.frame;
this._ignoreMouse = config.ignoreMouse;
this._direction = config.direction;
this._tag = config.tag;
this._draw();
}
/**
* Draw the part.
*
* @return {void}
* @private
*/
private _draw(): void {
this.filters = [];
this.texture = Assets.get('furnitures/' + this._furniture.data.baseName).textures[
this._furniture.data.baseName +
'_' +
this._furniture.data.baseName +
'_64_' +
String.fromCharCode(97 + Number(this._layer)) +
'_' +
String(this._direction) +
'_' +
String(this._frame)
];
/*console.log(
this._furniture.data.baseName +
'_' +
this._furniture.data.baseName +
'_64_' +
String.fromCharCode(97 + Number(this._layer)) +
'_' +
String(this._direction) +
'_' +
String(this._frame)
);*/
if (this._tint !== undefined) this.tint = new Color(this._tint).premultiply(1).toNumber();
if (this._blendMode !== undefined) this.blendMode = this._blendMode;
if (this._alpha !== undefined) this.alpha = this._alpha;
if (this._flip) this.scale.x = -1;
//if (this._furniture.room !== undefined) this.parentLayer = this._furniture.room.objects.layer;
if (this._z !== undefined) this.zIndex = this._z;
if (this._ignoreMouse !== null && !this._ignoreMouse) this.interactive = true;
if (this._furniture.selected) this.filters.push(WIRED_SELECTION_FILTER);
this.on('pointerdown', (event) => {
return this._furniture.eventManager.handlePointerDown({ event, tag: this._tag });
});
this.on('pointerup', (event) => {
return this._furniture.eventManager.handlePointerUp({ event, tag: this._tag });
});
this.on('pointermove', (event) => {
return this._furniture.eventManager.handlePointerMove({ event, tag: this._tag });
});
this.on('pointerout', (event) => {
return this._furniture.eventManager.handlePointerOut({ event, tag: this._tag });
});
this.on('pointerover', (event) => {
return this._furniture.eventManager.handlePointerOver({ event, tag: this._tag });
});
}
/**
* Reference to the furniture instance.
*
* @member {FloorFurniture | WallFurniture}
* @readonly
* @public
*/
public get furniture(): FloorFurniture | WallFurniture {
return this._furniture;
}
public get layer(): number {
return this._layer;
}
}

View File

@ -0,0 +1,283 @@
import type { Spritesheet } from 'pixi.js';
import { Assets, BLEND_MODES, Sprite } from 'pixi.js';
import { Direction } from '../../../enums/Direction';
import { ZOrder } from '../../../utilities/ZOrder';
import { AssetLoader } from '../../../utilities/AssetLoader';
import { FurnitureLayer } from './FurnitureLayer';
import { RoomObjectVisualization } from '../../rooms/objects/RoomObjectVisualization';
import type {
IFloorPosition,
IFurnitureLayerData,
IFurnitureProperty,
IFurnitureVisualization,
IWallPosition
} from '../../../types/Furniture';
import { WallFurniture } from '../WallFurniture';
import { FloorFurniture } from '../FloorFurniture';
export class FurnitureVisualization extends RoomObjectVisualization {
// updateLayerPosition and layerData private
private readonly _frames: Map<number, number> = new Map();
public _layers = new Map<number, FurnitureLayer>();
public _spritesheet!: Spritesheet;
public _properties!: IFurnitureProperty;
public _furniture: FloorFurniture | WallFurniture;
constructor(furniture: FloorFurniture | WallFurniture) {
super();
this._furniture = furniture;
this._loadAssets();
this._furniture.onRoomAdded = (room) => {
if (this.loaded) room.visualization.animationTicker.add(() => this._update());
};
}
private _loadAssets(): void {
const name = this._furniture.data.baseName;
if (this._furniture.onLoad !== undefined) this._furniture.onLoad();
AssetLoader.load('furnitures/' + name, 'furniture/' + name + '/' + name + '.json')
.then(() => {
if (this._furniture.onLoadComplete !== undefined) this._furniture.onLoadComplete();
this._spritesheet = Assets.get('furnitures/' + name);
// @ts-expect-error
this._properties = this._spritesheet.data.furniProperty;
this.loaded = true;
if (this.placeholder !== undefined) this.placeholder.destroy();
if (this._furniture.room != null) this._furniture.room.visualization.animationTicker.add(() => this._update());
})
.catch(() => {
this.logger.error(
'Unable to load the assets "' +
name +
'". It can be an invalid file, an invalid json format or just it don\t exist!'
);
});
}
private _update(): void {
const visualization = this._properties.visualization;
for (let i = 0; i < this._properties.visualization.layerCount; i++) {
const frame = visualization.animation[String(this._furniture.state)];
if (frame !== undefined && frame[i] !== undefined) {
const frameSequence = frame[i].frameSequence;
const currentFrame = this._frames.get(i) ?? 0;
if (frameSequence.length > 1) {
if (frameSequence.length - 1 > currentFrame) {
this._frames.set(i, currentFrame + 1);
} else this._frames.set(i, 0);
this._renderLayer(i, this._frames.get(i) ?? 0);
} else {
if (this._layers.get(i) == null) this._renderLayer(i, 0);
}
} else {
if (this._layers.get(i) == null) this._renderLayer(i, 0);
}
}
}
private _renderLayer(layer: number, frame: number): void {
const visualization = this._properties.visualization;
const furnitureLayer = this._layers.get(layer);
const frames = visualization.animation[this._furniture.state];
if (furnitureLayer != null) furnitureLayer.destroy();
if (this._properties.infos.visualization === 'furniture_animated' && frames === undefined) {
this._furniture.state = Number(Object.keys(visualization.animation)[0]);
return;
}
const layerData = this.layerData(layer, frame);
if (!visualization.directions.includes(this._furniture.direction)) {
this._furniture.rotate(visualization.directions[0], 0);
}
if (frames !== undefined && frames[layer] !== undefined && frames[layer].frameSequence.length > 1)
layerData.frame = frame;
if (frames !== undefined && frames[layer] !== undefined)
layerData.frame = frames[layer].frameSequence[layerData.frame] ?? 0;
const layerContainer = new FurnitureLayer(this._furniture, layerData);
layerContainer.zIndex = layerData.z;
this._layers.set(layer, layerContainer);
if (this._furniture.room != null) {
// @ts-expect-error
this._furniture.addChild(layerContainer);
this.updateLayerPosition(layer);
}
layerContainer.filters = this._furniture.filters; // TODO: Move this to global
}
destroy(): void {
if (this.placeholder.parent !== null) this.placeholder.destroy();
this._layers.forEach((layer) => layer.destroy());
this._layers.clear();
}
render(): void {
this.destroy();
this.directions = this._properties.visualization.directions;
for (let i = 0; i < this._properties.visualization.layerCount; i++) {
this._renderLayer(i, this._frames.get(i) ?? 0);
}
}
updatePosition(): void {
if (this._furniture instanceof FloorFurniture) {
const position = this._furniture.position as IFloorPosition;
return this._layers.forEach((layer) => {
layer.x = layer.texture.orig.x + 32 + 32 * position.x - 32 * position.y;
layer.y = layer.texture.orig.y + 16 * position.x + 16 * position.y - 32 * position.z;
});
} else {
const position = this._furniture.position as IWallPosition;
// TODO: Refactor wall items
return this._layers.forEach((layer) => {
if (this._furniture.direction === Direction.EAST) {
layer.x = layer.texture.orig.x + 32 + 32 * position.x - 32 * position.y + position.offsetX * 2 - 1;
layer.y = layer.texture.orig.y + 16 * position.x + 16 * position.y - 32 + position.offsetY * 2 + 31;
} else if (this._furniture.direction === Direction.SOUTH) {
layer.x = layer.texture.orig.x + 32 + 32 * position.x - 32 * position.y + position.offsetX * 2 - 32;
layer.y = layer.texture.orig.x + 16 * position.x + 16 * position.y - 32 + position.offsetY * 2 + 31;
}
});
}
}
updateLayerPosition(layer: number): void {
const furnitureLayer = this._layers.get(layer);
if (furnitureLayer == null) return;
if (this._furniture instanceof FloorFurniture) {
const position = this._furniture.position as IFloorPosition;
furnitureLayer.x = furnitureLayer.texture.orig.x + 32 + 32 * position.x - 32 * position.y;
furnitureLayer.y = furnitureLayer.texture.orig.y + 16 * position.x + 16 * position.y - 32 * position.z;
} else {
const position = this._furniture.position as IWallPosition;
if (this._furniture.direction === Direction.EAST) {
furnitureLayer.x =
furnitureLayer.texture.orig.x + 32 + 32 * position.x - 32 * position.y + position.offsetX * 2 - 1;
furnitureLayer.y =
furnitureLayer.texture.orig.y + 16 * position.x + 16 * position.y - 32 + position.offsetY * 2 + 31;
} else if (this._furniture.direction === Direction.SOUTH) {
furnitureLayer.x =
furnitureLayer.texture.orig.x + 32 + 32 * position.x - 32 * position.y + position.offsetX * 2 - 32;
furnitureLayer.y =
furnitureLayer.texture.orig.x + 16 * position.x + 16 * position.y - 32 + position.offsetY * 2 + 31;
}
}
}
renderPlaceholder(): void {
if (this._furniture instanceof FloorFurniture) {
const position = this._furniture.position as IFloorPosition;
this.placeholder = new Sprite(
Assets.get('furnitures/floor/placeholder').textures['place_holder_furniture_64.png']
);
if (this._furniture.room != null) this._furniture.addChild(this.placeholder);
this.placeholder.x = 32 + 32 * position.x - 32 * position.y - 32;
this.placeholder.y = 16 * position.x + 16 * position.y - 32 * position.z - 50;
} else {
const position = this._furniture.position as IWallPosition;
this.placeholder = new Sprite(
Assets.get('furnitures/wall/placeholder').textures['place_holder_wall_item_64.png']
);
if (this._furniture.room != null) this._furniture.addChild(this.placeholder);
if (this._furniture.direction === Direction.EAST) {
this.placeholder.x = 32 + 32 * position.x - 32 * position.y + position.offsetX * 2 - 1;
this.placeholder.y = 16 * position.x + 16 * position.y - 32 + position.offsetY * 2 + 31 - 50;
} else if (this._furniture.direction === Direction.SOUTH) {
this.placeholder.scale.x = -1;
this.placeholder.x = 32 + 32 * position.x - 32 * position.y + position.offsetX * 2 - 32;
this.placeholder.y = 16 * position.x + 16 * position.y - 32 + position.offsetY * 2 + 31 - 50;
}
}
}
layerData(layer: number, frame: number = 0): IFurnitureLayerData {
const spritesheet = Assets.get<Spritesheet>('furnitures/' + this._furniture.data.baseName);
// @ts-expect-error
const visualization = spritesheet.data.furniProperty.visualization as IFurnitureVisualization;
const layerData: IFurnitureLayerData = {
layer,
alpha: 1,
z: 0,
blendMode: BLEND_MODES.NORMAL,
flip: false,
frame: 0,
ignoreMouse: false,
direction: Direction.NORTH
};
if (!visualization.directions.includes(this._furniture.direction))
this._furniture.rotate(visualization.directions[0], 0);
layerData.direction = this._furniture.direction;
if (
this._furniture.data.color !== null &&
visualization.colors[this._furniture.data.color] !== undefined &&
visualization.colors[this._furniture.data.color][layer] !== undefined
)
layerData.tint = Number('0x' + String(visualization.colors[this._furniture.data.color][layer]));
const visualizationLayerData = visualization.layers[layer];
if (visualizationLayerData !== undefined) {
if (visualizationLayerData.z !== undefined) layerData.z = visualizationLayerData.z;
if (visualizationLayerData.alpha !== undefined) layerData.alpha = visualizationLayerData.alpha / 255;
if (visualizationLayerData.ink !== undefined) layerData.blendMode = BLEND_MODES[visualizationLayerData.ink];
if (visualization.layers[layer].ignoreMouse !== undefined)
layerData.ignoreMouse = visualizationLayerData.ignoreMouse;
if (visualization.layers[layer].tag !== undefined) layerData.tag = visualizationLayerData.tag;
}
const name = [
this._furniture.data.baseName,
this._furniture.data.baseName,
64,
String.fromCharCode(97 + Number(layer)),
this._furniture.direction,
frame
].join('_');
if (this._furniture instanceof FloorFurniture)
layerData.z = ZOrder.floorItem(this._furniture.position as IFloorPosition, layerData.z);
if (this._furniture instanceof WallFurniture)
layerData.z = ZOrder.wallItem(this._furniture.position as IWallPosition, layerData.z);
// @ts-expect-error
if (spritesheet.data.frames[name] !== undefined) layerData.flip = spritesheet.data.frames[name].flipH;
return layerData;
}
}

View File

@ -0,0 +1,416 @@
import type { IInteractionEvent } from '../../types/Interaction';
import type { Room } from '../rooms/Room';
/**
* InteractionManager class for interaction handling.
*
* @class
* @memberof Scuti
*/
export class EventManager {
/**
* A boolean indicating if the user have clicked at least one time, indicating that the second click is a double click.
*
* @member {boolean}
* @private
*/
private _isDoubleClicking = false;
/**
* The double click timeout that set the _isDoubleClicking boolean value to false after 350ms.
*
* @member {number}
* @private
*/
private _doubleClickTimeout!: number;
/**
* The pointer down event.
*
* @member {(event: IInteractionEvent) => void}
* @private
*/
private _onPointerDown!: (event: IInteractionEvent) => void;
/**
* The pointer up event.
*
* @member {(event: IInteractionEvent) => void}
* @private
*/
private _onPointerUp!: (event: IInteractionEvent) => void;
/**
* The pointer move event.
*
* @member {(event: IInteractionEvent) => void}
* @private
*/
private _onPointerMove!: (event: IInteractionEvent) => void;
/**
* The pointer out event.
*
* @member {(event: IInteractionEvent) => void}
* @private
*/
private _onPointerOut!: (event: IInteractionEvent) => void;
/**
* The pointer ouver event.
*
* @member {(event: IInteractionEvent) => void}
* @private
*/
private _onPointerOver!: (event: IInteractionEvent) => void;
/**
* The pointer double click event.
*
* @member {(event: IInteractionEvent) => void}
* @private
*/
private _onDoubleClick!: (event: IInteractionEvent) => void;
/**
* The assets starting load event.
*
* @member {() => void}
* @private
*/
private _onLoad!: () => void;
/**
* The assets ending load event.
*
* @member {() => void}
* @private
*/
private _onLoadComplete!: () => void;
/**
* The room add event.
*
* @member {(room: Room) => void}
* @private
*/
private _onRoomAdded!: (room: Room) => void;
/**
* The room remove event.
*
* @member {(room: Room) => void}
* @private
*/
private _onRoomRemoved!: (room: Room) => void;
/**
* Reference to the pointer down event.
*
* @member {(event: IInteractionEvent) => void}
* @readonly
* @public
*/
public get onPointerDown(): (event: IInteractionEvent) => void {
return this._onPointerDown;
}
/**
* Update the event function that will be executed.
*
* @param {(event: IInteractionEvent) => void} [value] - The event function that will be executed.
* @public
*/
public set onPointerDown(value: (event: IInteractionEvent) => void) {
this._onPointerDown = value;
}
/**
* Reference to the pointer up event.
*
* @member {(event: IInteractionEvent) => void}
* @readonly
* @public
*/
public get onPointerUp(): (event: IInteractionEvent) => void {
return this._onPointerUp;
}
/**
* Update the event function that will be executed.
*
* @param {(event: IInteractionEvent) => void} [value] - The event function that will be executed.
* @public
*/
public set onPointerUp(value: (event: IInteractionEvent) => void) {
this._onPointerUp = value;
}
/**
* Reference to the pointer move event.
*
* @member {(event: IInteractionEvent) => void}
* @readonly
* @public
*/
public get onPointerMove(): (event: IInteractionEvent) => void {
return this._onPointerMove;
}
/**
* Update the event function that will be executed.
*
* @param {(event: IInteractionEvent) => void} [value] - The event function that will be executed.
* @public
*/
public set onPointerMove(value: (event: IInteractionEvent) => void) {
this._onPointerMove = value;
}
/**
* Reference to the pointer out event.
*
* @member {(event: IInteractionEvent) => void}
* @readonly
* @public
*/
public get onPointerOut(): (event: IInteractionEvent) => void {
return this._onPointerOut;
}
/**
* Update the event function that will be executed.
*
* @param {(event: IInteractionEvent) => void} [value] - The event function that will be executed.
* @public
*/
public set onPointerOut(value: (event: IInteractionEvent) => void) {
this._onPointerOut = value;
}
/**
* Reference to the pointer over event.
*
* @member {(event: IInteractionEvent) => void}
* @readonly
* @public
*/
public get onPointerOver(): (event: IInteractionEvent) => void {
return this._onPointerOver;
}
/**
* Update the event function that will be executed.
*
* @param {(event: IInteractionEvent) => void} [value] - The event function that will be executed.
* @public
*/
public set onPointerOver(value: (event: IInteractionEvent) => void) {
this._onPointerOver = value;
}
/**
* Reference to the pointer double click event.
*
* @member {(event: IInteractionEvent) => void}
* @readonly
* @public
*/
public get onDoubleClick(): (event: IInteractionEvent) => void {
return this._onDoubleClick;
}
/**
* Update the event function that will be executed.
*
* @param {(event: IInteractionEvent) => void} [value] - The event function that will be executed.
* @public
*/
public set onDoubleClick(value: (event: IInteractionEvent) => void) {
this._onDoubleClick = value;
}
/**
* Reference to the assets starting load event.
*
* @member {() => void}
* @readonly
* @public
*/
public get onLoad(): () => void {
return this._onLoad;
}
/**
* Update the event function that will be executed.
*
* @param {() => void} [value] - The event function that will be executed.
* @public
*/
public set onLoad(value: () => void) {
this._onLoad = value;
}
/**
* Reference to the assets ending load event.
*
* @member {() => void}
* @readonly
* @public
*/
public get onLoadComplete(): () => void {
return this._onLoadComplete;
}
/**
* Update the event function that will be executed.
*
* @param {() => void} [value] - The event function that will be executed.
* @public
*/
public set onLoadComplete(value: () => void) {
this._onLoadComplete = value;
}
/**
* Reference to the room add event.
*
* @member {(room: Room) => void}
* @readonly
* @public
*/
public get onRoomAdded(): (room: Room) => void {
return this._onRoomAdded;
}
/**
* Update the event function that will be executed.
*
* @param {(room: Room) => void} [value] - The event function that will be executed.
* @public
*/
public set onRoomAdded(value: (room: Room) => void) {
this._onRoomAdded = value;
}
/**
* Reference to the room remove event.
*
* @member {(room: Room) => void}
* @readonly
* @public
*/
public get onRoomRemoved(): (room: Room) => void {
return this._onRoomRemoved;
}
/**
* Update the event function that will be executed.
*
* @param {(room: Room) => void} [value] - The event function that will be executed.
* @public
*/
public set onRoomRemoved(value: (room: Room) => void) {
this._onRoomRemoved = value;
}
/**
* Handle the pointer down event.
*
* @return {void}
* @public
*/
public handlePointerDown(event: IInteractionEvent): void {
if (!this._isDoubleClicking) {
if (this.onPointerDown !== undefined) this._onPointerDown(event);
this._isDoubleClicking = true;
this._doubleClickTimeout = window.setTimeout(() => {
return (this._isDoubleClicking = false);
}, 350);
} else {
if (this.onDoubleClick !== undefined) this._onDoubleClick(event);
this._isDoubleClicking = false;
window.clearTimeout(this._doubleClickTimeout);
}
}
/**
* Handle the pointer up event.
*
* @return {void}
* @public
*/
public handlePointerUp(event: IInteractionEvent): void {
if (this.onPointerUp !== undefined) this._onPointerUp(event);
}
/**
* Handle the pointer move event.
*
* @return {void}
* @public
*/
public handlePointerMove(event: IInteractionEvent): void {
if (this.onPointerMove !== undefined) this._onPointerMove(event);
}
/**
* Handle the pointer out event.
*
* @return {void}
* @public
*/
public handlePointerOut(event: IInteractionEvent): void {
if (this.onPointerOut !== undefined) this._onPointerOut(event);
}
/**
* Handle the pointer over event.
*
* @return {void}
* @public
*/
public handlePointerOver(event: IInteractionEvent): void {
if (this.onPointerOver !== undefined) this._onPointerOver(event);
}
/**
* Handle the assets load start event.
*
* @return {void}
* @public
*/
public handleLoad(): void {
if (this.onLoad !== undefined) this._onLoad();
}
/**
* Handle the assets load end event.
*
* @return {void}
* @public
*/
public handleLoadComplete(): void {
if (this.onLoadComplete !== undefined) this._onLoadComplete();
}
/**
* Handle the room add event.
*
* @return {void}
* @public
*/
public handleRoomAdded(room: Room): void {
if (this.onRoomAdded !== undefined) this._onRoomAdded(room);
}
/**
* Handle the room remove event.
*
* @return {void}
* @public
*/
public handleRoomRemoved(room: Room): void {
if (this.onRoomRemoved !== undefined) this._onRoomRemoved(room);
}
}

View File

@ -0,0 +1,71 @@
import type { IPointData } from 'pixi.js';
import { Sprite } from 'pixi.js';
import { HitTexture } from './HitTexture';
/**
* HitSprite class that manage the interactions with sprite transparency.
*
* @class
* @memberof Scuti
*/
export class HitSprite extends Sprite {
/**
* The sprite interactivity.
*
* @member {boolean}
* @public
*/
public interactive!: boolean;
/**
* The global sprite position in the canvas.
*
* @member {{ x: number, y: number }}
* @public
*/
public getGlobalPosition: () => { x: number; y: number };
/**
* The hit texture that contains the hit map data.
*
* @member {HitTexture}
* @private
*/
private _hitTexture!: HitTexture;
/**
* Return a boolean indicating if the pointer is on the sprite.
*
* @return {boolean}
* @public
*/
public containsPoint(point: IPointData): boolean {
/** The sprite is not interactive, so we stop here */
if (!this.interactive) return false;
if (this.texture.trim === undefined) return false;
const width = this.texture.orig.width;
const height = this.texture.orig.height;
const x1 = this.getGlobalPosition().x + this.texture.trim.x;
let y1 = 0;
let flag = false;
/** Check if the pointer is out of bound of the sprite */
if (point.x >= x1 && point.x < x1 + width) {
y1 = this.getGlobalPosition().y + this.texture.trim.y;
if (point.y >= y1 && point.y < y1 + height) flag = true;
}
/** Return false if the pointer is out of bound */
if (!flag) return false;
/** Create the hit texture */
if (this._hitTexture == null) this._hitTexture = new HitTexture(this);
/** Check the hit map of the hit texture if the pointer is on a transparent pixel or not */
return this._hitTexture.hit(point.x - x1, point.y - y1, this.scale.x === -1);
}
}

View File

@ -0,0 +1,290 @@
import type { BaseTexture, DisplayObject, ICanvas, IRenderer, Resource } from 'pixi.js';
import { BaseImageResource, Rectangle, RenderTexture, Sprite, Texture } from 'pixi.js';
import { CanvasRenderTarget } from '@pixi/utils';
import type { HitSprite } from './HitSprite';
import { FurnitureLayer } from '../furnitures/visualizations/FurnitureLayer';
import { AvatarLayer } from '../avatars/visualizations/AvatarLayer';
/**
* HitTexture class create an hit map from a texture to manage interactions.
*
* @class
* @memberof Scuti
*/
export class HitTexture {
/**
* The hit sprite of the hit texture.
*
* @member {HitSprite}
* @readonly
* @private
*/
private readonly _sprite: HitSprite;
/**
* The hit texture base texture.
*
* @member {Texture}
* @private
*/
private readonly _texture: Texture;
/**
* The hit map array.
*
* @member {Uint32Array}
* @private
*/
private _hitMap!: Uint32Array;
/**
* @param {HitSprite} [sprite] - The hit sprite that we want to retrieve the texture.
*/
constructor(sprite: HitSprite) {
this._sprite = sprite;
this._texture = this._generateTexture();
}
/**
* Return the cached hit map of the sprite.
*
* @return {Uint32Array} - A Uint32Array that is the cached hit map of the sprite.
* @private
*/
private _getHitMap(): Uint32Array {
if (!Boolean(this._hitMap)) this._hitMap = this._generateHitMap(this._texture.baseTexture);
return this._hitMap;
}
/**
* Generate the hit map from a specified base texture.
*
* @param {BaseTexture} baseTexture - The base texture that we want to have the hit map.
* @return {Uint32Array} - An Uint32Array that is the hit map of the specified baseTexture.
* @private
*/
private _generateHitMap(baseTexture: BaseTexture<Resource>): Uint32Array {
const { height: imageHeight, width: imageWidth } = baseTexture.resource;
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d') as CanvasRenderingContext2D;
canvas.width = imageWidth;
canvas.height = imageHeight;
// @ts-expect-error
context.drawImage(baseTexture.resource.source, 0, 0);
let w = canvas.width;
let h = canvas.height;
if (w > canvas.width) w = canvas.width;
if (h > canvas.height) h = canvas.height;
if (w === 0) return new Uint32Array();
const imageData = context.getImageData(0, 0, w, h);
const threshold = 255;
const hitmap = new Uint32Array(Math.ceil((w * h) / 32));
for (let i = 0; i < w * h; i++) {
const ind1 = i % 32;
const ind2 = (i / 32) | 0;
if (imageData.data[i * 4 + 3] >= threshold) {
hitmap[ind2] = hitmap[ind2] | (1 << ind1);
}
}
return hitmap;
}
/**
* Generate a sprite texture and not the texture of the entire spritesheet.
*
* @return {Texture} - A texture of the sprite (not all the spritesheet).
* @private
*/
private _generateTexture(): Texture {
const sprite = new Sprite(this._sprite.texture.clone());
let renderTexture = {} as RenderTexture;
sprite.x = this._sprite.getGlobalPosition().x + this._sprite.texture.trim.x;
sprite.y = this._sprite.getGlobalPosition().y + this._sprite.texture.trim.y;
sprite.texture.trim.x = 0;
sprite.texture.trim.y = 0;
if (this._sprite instanceof FurnitureLayer) {
this._sprite.furniture.room.engine.application.stage.addChild(sprite);
renderTexture = this._sprite.furniture.room.engine.application.renderer.generateTexture(sprite);
} else if (this._sprite.parent instanceof AvatarLayer) {
this._sprite.parent.avatar.room.engine.application.stage.addChild(sprite);
renderTexture = this._sprite.parent.avatar.room.engine.application.renderer.generateTexture(sprite);
}
const image = this._image(renderTexture);
const baseTexture = renderTexture.baseTexture;
renderTexture.baseTexture.resource = new BaseImageResource(image);
renderTexture.destroy();
sprite.destroy();
return new Texture(baseTexture);
}
/**
* Will return a boolean that indicate if we hit the sprite.
*
* @param {number} x - The x coordinate of the hit point
* @param {number} y - The y coordinate of the hit point
* @param {boolean} flip - If the sprite is flipped
* @return {boolean} - A boolean indicating if we hit the sprite.
* @public
*/
public hit(x: number, y: number, flip: boolean): boolean {
const hitmap = this._getHitMap();
const dx = flip
? this._texture.baseTexture.realWidth - Math.round(x * this._texture.baseTexture.resolution)
: Math.round(x * this._texture.baseTexture.resolution);
const dy = Math.round(y * this._texture.baseTexture.resolution);
const ind = dx + dy * this._texture.baseTexture.realWidth;
const ind1 = ind % 32;
const ind2 = (ind / 32) | 0;
return (hitmap[ind2] & (1 << ind1)) !== 0;
}
/**
* Will return a HTML Image of the target.
*
* @param {DisplayObject|RenderTexture} target - A displayObject or renderTexture
* to convert. If left empty will use the main renderer
* @param {string} [format] - Image format, e.g. "image/jpeg" or "image/webp".
* @param {number} [quality] - JPEG or Webp compression from 0 to 1. Default is 0.92.
* @return {HTMLImageElement} HTML Image of the target.
* @private
*/
private _image(target: DisplayObject | RenderTexture, format?: string, quality?: number): HTMLImageElement {
const image = new Image();
image.src = this._base64(target, format, quality);
return image;
}
/**
* Will return a a base64 encoded string of this target. It works by calling
* `Extract.getCanvas` and then running toDataURL on that.
*
* @param {DisplayObject|RenderTexture} target - A displayObject or renderTexture
* to convert. If left empty will use the main renderer
* @param {string} [format] - Image format, e.g. "image/jpeg" or "image/webp".
* @param {number} [quality] - JPEG or Webp compression from 0 to 1. Default is 0.92.
* @return {string} A base64 encoded string of the texture.
* @private
*/
private _base64(target: DisplayObject | RenderTexture, format?: string, quality?: number): string {
return this._canvas(target).toDataURL(format, quality);
}
/**
* Creates a Canvas element, renders this target to it and then returns it.
*
* @param {DisplayObject|RenderTexture} target - A displayObject or renderTexture
* to convert. If left empty will use the main renderer
* @return {HTMLCanvasElement} A Canvas element with the texture rendered on.
* @private
*/
private _canvas(target: DisplayObject | RenderTexture): ICanvas {
const TEMP_RECT = new Rectangle();
const BYTES_PER_PIXEL = 4;
let renderer = {} as IRenderer<ICanvas>;
if (this._sprite instanceof FurnitureLayer) {
renderer = this._sprite.furniture.room.engine.application.renderer;
} else if (this._sprite.parent instanceof AvatarLayer) {
renderer = this._sprite.parent.avatar.room.engine.application.renderer;
}
let resolution: number;
let frame: Rectangle;
let flipY: boolean;
let renderTexture: RenderTexture | undefined;
let generated = false;
if (Boolean(target)) {
if (target instanceof RenderTexture) {
renderTexture = target;
} else {
renderTexture = renderer.generateTexture(target);
generated = true;
}
}
if (Boolean(renderTexture)) {
resolution = renderTexture.baseTexture.resolution;
frame = renderTexture.frame;
flipY = false;
renderer.renderTexture.bind(renderTexture);
} else {
resolution = renderer.resolution;
flipY = true;
frame = TEMP_RECT;
frame.width = renderer.width;
frame.height = renderer.height;
renderer.renderTexture.bind(null);
}
const width = Math.floor(frame.width * resolution + 1e-4);
const height = Math.floor(frame.height * resolution + 1e-4);
const webglPixels = new Uint8Array(BYTES_PER_PIXEL * width * height);
let canvasBuffer = new CanvasRenderTarget(width, height, 1);
/** Read pixels to the array */
const gl = renderer.gl;
gl.readPixels(frame.x * resolution, frame.y * resolution, width, height, gl.RGBA, gl.UNSIGNED_BYTE, webglPixels);
/** Add the pixels to the canvas */
const canvasData = canvasBuffer.context.getImageData(0, 0, width, height);
this.arrayPostDivide(webglPixels, canvasData.data);
canvasBuffer.context.putImageData(canvasData, 0, 0);
/** Pulling pixels */
if (flipY) {
const target = new CanvasRenderTarget(canvasBuffer.width, canvasBuffer.height, 1);
target.context.scale(1, -1);
/** We can't render to itself because we should be empty before render. */
target.context.drawImage(canvasBuffer.canvas, 0, -height);
canvasBuffer.destroy();
canvasBuffer = target;
}
if (generated) renderTexture.destroy(true);
/** Send the canvas back... */
return canvasBuffer.canvas;
}
/**
* Takes premultiplied pixel data and produces regular pixel data
* @private
* @param pixels - array of pixel data
* @param out - output array
*/
private arrayPostDivide(
pixels: number[] | Uint8Array | Uint8ClampedArray,
out: number[] | Uint8Array | Uint8ClampedArray
): void {
for (let i = 0; i < pixels.length; i += 4) {
const alpha = (out[i + 3] = pixels[i + 3]);
if (alpha !== 0) {
out[i] = Math.round(Math.min((pixels[i] * 255) / alpha, 255));
out[i + 1] = Math.round(Math.min((pixels[i + 1] * 255) / alpha, 255));
out[i + 2] = Math.round(Math.min((pixels[i + 2] * 255) / alpha, 255));
} else {
out[i] = pixels[i];
out[i + 1] = pixels[i + 1];
out[i + 2] = pixels[i + 2];
}
}
}
}

333
src/objects/rooms/Room.ts Normal file
View File

@ -0,0 +1,333 @@
import { Container } from 'pixi.js';
import type { Scuti } from '../../Scuti';
import { RoomVisualization } from './RoomVisualization';
import { RoomTileMap } from './RoomTileMap';
import type { Material } from './materials/Material';
import { WallMaterial } from './materials/WallMaterial';
import { FloorMaterial } from './materials/FloorMaterial';
import { RoomCamera } from './RoomCamera';
import type { EventManager } from '../interactions/EventManager';
import type { IRoomConfig } from '../../types/Room';
import type { RoomPartLayer } from './layers/RoomPartLayer';
import type { RoomObjectLayer } from './layers/RoomObjectLayer';
/**
* Room class for rendering rooms like the ones on Habbo Hotel.
*
* @class
* @memberof Scuti
*/
export class Room extends Container {
/**
* The game engine instance that the room will be using to render objects.
*
* @member {Scuti}
* @private
*/
private readonly _engine: Scuti;
/**
* The room tile map where every informations about the room model is stored.
*
* @member {RoomTileMap}
* @private
*/
private _tileMap: RoomTileMap;
/**
* The wall material that will be applied in the room, it contains the color and the texture of the wall.
*
* @member {Material}
* @private
*/
private _wallMaterial: Material;
/**
* The floor material that will be applied in the room, it contains the color and the texture of the wall.
*
* @member {Material}
* @private
*/
private _floorMaterial: Material;
/**
* The wall thickness of the room.
*
* @member {number}
* @private
*/
private _wallThickness: number;
/**
* The floor thickness of the room.
*
* @member {number}
* @private
*/
private _floorThickness: number;
/**
* The wall height of the room, the height is added to the base height of the room.
*
* @member {number}
* @private
*/
private _wallHeight: number;
/**
* The room view instance, where all the objects like furnitures, avatars or the tiles, walls and stairs are stored.
*
* @member {RoomVisualization}
* @private
*/
private readonly _visualization: RoomVisualization;
/**
* The room camera, it manage the room dragging and centering the room when it's out of bounds.
*
* @member {RoomCamera}
* @private
*/
private readonly _camera: RoomCamera;
/**
* @param {Scuti} [engine] - The Scuti instance that will be used to render the room.
* @param {IRoomConfig} [config] - The room configuration.
* @param {string} [config.tilemap] - The room tile map that will be parsed.
* @param {Material} [config.floorMaterial] - The room floor material that will be applied.
* @param {number} [config.floorThickness] - The room floor thickness.
* @param {Material} [config.wallMaterial] - The room wall material that will be applied.
* @param {number} [config.wallHeight] - The room wall height.
* @param {number} [config.wallThickness] - The room wall thickness.
**/
constructor(engine: Scuti, config: IRoomConfig) {
super();
/** Store variables */
this._engine = engine;
this._wallMaterial = config.wallMaterial ?? new WallMaterial(this._engine);
this._floorMaterial = config.floorMaterial ?? new FloorMaterial(this._engine);
this._wallThickness = config.wallThickness ?? 8;
this._floorThickness = config.floorThickness ?? 8;
this._wallHeight = config.wallHeight ?? 0;
/** Initialise everything */
this._tileMap = new RoomTileMap(config.tileMap);
this._visualization = new RoomVisualization(this);
this._camera = new RoomCamera(this);
/** Add the room view and then the room camera to the PixiJS application */
this.addChild(this._visualization);
this._engine.application.stage.addChild(this._camera);
}
/**
* Reference to the game engine instance.
*
* @member {Scuti}
* @readonly
* @public
*/
public get engine(): Scuti {
return this._engine;
}
/**
* Reference to the room view instance.
*
* @member {RoomVisualization}
* @readonly
* @public
*/
public get visualization(): RoomVisualization {
return this._visualization;
}
/**
* Reference to the room tile map instance.
*
* @member {RoomTileMap}
* @readonly
* @public
*/
public get tileMap(): RoomTileMap {
return this._tileMap;
}
/**
* Update the room tileMap.
*
* @param {string} [tileMap] - The new room tileMap.
* @public
*/
public set tileMap(tileMap: RoomTileMap) {
this._tileMap = tileMap;
this._visualization.update();
}
/**
* Reference to the wall material instance.
*
* @member {Material}
* @readonly
* @public
*/
public get wallMaterial(): Material {
return this._wallMaterial;
}
/**
* Update the wall material and rerender the room.
*
* @param {Material} [material] - The room wall material that will be applied.
* @public
*/
public set wallMaterial(material: Material) {
this._wallMaterial = material;
this._visualization.update();
}
/**
* Reference to the floor material instance.
*
* @member {Material}
* @readonly
* @public
*/
public get floorMaterial(): Material {
return this._floorMaterial;
}
/**
* Update the floor material and rerender the room.
*
* @param {Material} [material] - The room floor material that will be applied.
* @public
*/
public set floorMaterial(material: Material) {
this._floorMaterial = material;
this._visualization.update();
}
/**
* Reference to the wall thickness.
*
* @member {number}
* @readonly
* @public
*/
public get wallThickness(): number {
return this._wallThickness;
}
/**
* Update the wall thickness and rerender the room.
*
* @param {number} [thickness] - The room wall thickness that will be applied.
* @public
*/
public set wallThickness(thickness: number) {
this._wallThickness = thickness;
this._visualization.update();
}
/**
* Reference to the floor thickness.
*
* @member {number}
* @readonly
* @public
*/
public get floorThickness(): number {
return this._floorThickness;
}
/**
* Update the floor thickness and rerender the room.
*
* @param {number} [thickness] - The room floor thickness that will be applied.
* @public
*/
public set floorThickness(thickness: number) {
this._floorThickness = thickness;
this._visualization.update();
}
/**
* Reference to the wall height.
*
* @member {number}
* @readonly
* @public
*/
public get wallHeight(): number {
return this._wallHeight;
}
/**
* Update the wall height and rerender the room.
*
* @param {number} [height] - The room wall height that will be applied.
* @public
*/
public set wallHeight(height: number) {
this._wallHeight = height;
this._visualization.update();
}
/**
* Reference to the tile event manager.
*
* @member {EventManager}
* @readonly
* @public
*/
public get tiles(): EventManager {
return this._visualization.partLayer.tiles;
}
/**
* Reference to the wall event manager.
*
* @member {EventManager}
* @readonly
* @public
*/
public get walls(): EventManager {
return this._visualization.partLayer.walls;
}
/**
* Reference to the object layer container.
*
* @member {RoomObjectLayer}
* @readonly
* @public
*/
public get objects(): RoomObjectLayer {
return this._visualization.objectLayer;
}
/**
* Reference to the part layer container.
*
* @member {RoomPartLayer}
* @readonly
* @public
*/
public get parts(): RoomPartLayer {
return this._visualization.partLayer;
}
/**
* Reference to the room camera.
*
* @member {RoomCamera}
* @readonly
* @public
*/
public get camera(): RoomCamera {
return this._camera;
}
}

View File

@ -0,0 +1,269 @@
import { Container, EventBoundary, FederatedPointerEvent } from 'pixi.js';
import { Expo, gsap } from 'gsap';
import type { Room } from './Room';
import type { Tile } from './parts/Tile';
import type { Stair } from './parts/Stair';
import type { RoomObject } from './objects/RoomObject';
/**
* RoomCamera class that manage things like the room dragging or detecting if the room is out of bounds.
*
* @class
* @memberof Scuti
*/
export class RoomCamera extends Container {
/**
* The room instance that will be managed by the camera.
*
* @member {Room}
* @private
*/
private readonly _room: Room;
/**
* A boolean indicating if the room is being dragged.
*
* @member {boolean}
* @private
*/
private _dragging!: boolean;
/**
* The current selected tile.
*
* @member {Tile | Stair}
* @private
*/
private _selectedTile!: Tile | Stair;
/**
* The current zoom level.
*
* @member {number}
* @private
*/
private _zoomLevel: number;
/**
* @param {Room} [room] - The room instance that will be managed by this camera.
*/
constructor(room: Room) {
super();
this._room = room;
this._zoomLevel = 1;
this.addChild(this._room);
/** Handle interactions */
this._room.engine.application.renderer.events.domElement.addEventListener('pointerdown', this._dragStart);
this._room.engine.application.renderer.events.domElement.addEventListener('pointerup', this._dragEnd);
this._room.engine.application.renderer.events.domElement.addEventListener('pointermove', this._dragMove);
/** Handle tile interactions */
this._room.engine.application.renderer.events.domElement.addEventListener('pointerdown', this._tilePointerDown);
this._room.engine.application.renderer.events.domElement.addEventListener('pointerup', this._tilePointerUp);
this._room.engine.application.renderer.events.domElement.addEventListener('pointermove', this._tilePointerMove);
window.addEventListener(
'wheel',
(event) => {
if (event.ctrlKey) event.preventDefault();
const delta = Math.sign(event.deltaY);
const zoomLevel = parseFloat((-delta / 8).toFixed(2));
if (this.zoomLevel + zoomLevel <= 0.8 || this.zoomLevel + zoomLevel >= 2.8) return;
this.zoomLevel += zoomLevel;
},
{ passive: false }
);
window.addEventListener('resize', () => {
this._room.engine.application.view.height = window.innerHeight;
this._room.engine.application.view.width = window.innerWidth;
});
this._updateBounds();
this.centerCamera();
}
/**
* Update the room container bounds.
*
* @return {void}
* @private
*/
private _updateBounds(): void {
this.pivot.x = this._room.visualization.getBounds().x;
this.pivot.y = this._room.visualization.getBounds().y;
}
/**
* Tween the room container at the center of the PixiJS view.
*
* @return {void}
* @public
*/
public centerCamera(object?: RoomObject): void {
if (object === null || object === undefined)
gsap.to(this, {
x: Math.floor(this._room.engine.application.view.width / 2 - this._room.visualization.width / 2),
y: Math.floor(this._room.engine.application.view.height / 2 - this._room.visualization.height / 2),
duration: 0.8,
ease: 'easeOut'
});
// TODO: Reimplement this part with the new room object
/*else {
const globalPos: Point = object.getGlobalPosition(new Point(object.width / 2, object.height / 2));
const diffX: number = globalPos.x - this._room.engine.application.view.width / 2;
const diffY: number = globalPos.y - this._room.engine.application.view.height / 2;
gsap.to(this._roomContainer, {
x: Math.floor(this._roomContainer.x - diffX),
y: Math.floor(this._roomContainer.y - diffY),
duration: 0.8,
ease: 'easeOut'
});
}*/
}
/**
* This method is called when the user start dragging the room.
*
* @return {void}
* @private
*/
private readonly _dragStart = (): void => {
// console.log(event.movementX);
this._dragging = true;
};
/**
* This method is called when the user stop dragging the room.
*
* @return {void}
* @private
*/
private readonly _dragEnd = (): void => {
this._dragging = false;
if (this._isOutOfBounds()) this.centerCamera();
};
/**
* This method is called when the user is moving the dragged room in the canvas.
*
* @param {PointerEvent} [event] - The mouse event.
* @return {void}
* @private
*/
private readonly _dragMove = (event: PointerEvent): void => {
if (!this._dragging) return;
this.x = Math.floor(this.x + event.movementX * (1 / this._zoomLevel));
this.y = Math.floor(this.y + event.movementY * (1 / this._zoomLevel));
};
/**
* Indicate if the room container is out of bounds of the PixiJS view.
*
* @return {boolean}
* @private
*/
private _isOutOfBounds(): boolean {
/** Out of bounds on the right */
if (this.x > this._room.engine.application.view.width) return true;
/** Out of bounds on the left */
if (this.x + this.width < 0) return true;
/** Out of bounds on the bottom */
if (this.y > this._room.engine.application.view.height) return true;
/** Out of bounds on the top */
if (this.y + this.height < 0) return true;
/** It is not out of bounds */
return false;
}
/**
* Manage pointer down event on the canvas for tile interaction.
*
* @return {void}
* @private
*/
private readonly _tilePointerDown = (event: PointerEvent): void => {
const tile = this._room.parts.getFromGlobal({ x: event.clientX, y: event.clientY });
if (tile != null) tile.emit('pointerdown', new FederatedPointerEvent(new EventBoundary()));
};
/**
* Manage pointer up event on the canvas for tile interaction.
*
* @return {void}
* @private
*/
private readonly _tilePointerUp = (event: PointerEvent): void => {
const tile = this._room.parts.getFromGlobal({ x: event.clientX, y: event.clientY });
if (tile != null) tile.emit('pointerup', new FederatedPointerEvent(new EventBoundary()));
};
/**
* Manage pointer move event on the canvas for tile interaction.
*
* @return {void}
* @private
*/
private readonly _tilePointerMove = (event: PointerEvent): void => {
const objectPart = this._room.parts.getFromGlobal({ x: event.clientX, y: event.clientY });
if (objectPart == null) return;
if (this._selectedTile === objectPart) {
objectPart.emit('pointermove', new FederatedPointerEvent(new EventBoundary()));
} else {
if (this._selectedTile != null)
this._selectedTile.emit('pointerout', new FederatedPointerEvent(new EventBoundary()));
if (objectPart != null) objectPart.emit('pointerover', new FederatedPointerEvent(new EventBoundary()));
this._selectedTile = objectPart;
}
};
/**
* Zoom the room container.
*
* @param {number} [zoomLevel] - The zoom ratio as a number.
* @public
*/
public set zoomLevel(zoomLevel: number) {
const origWidth = this.width / this._zoomLevel;
const origHeight = this.height / this._zoomLevel;
this._zoomLevel = zoomLevel;
// pivot's container property must be changed
// because by default things are centered and scaled from the top corner
// this.pivot.x = ... / this.pivot.y = ...
gsap.set(this.scale, { x: zoomLevel, y: zoomLevel, ease: Expo.easeOut });
const diffWidth = origWidth - this.width;
const diffHeight = origHeight - this.height;
const offsetX = this._room.engine.application.view.width / 2 - (this.x + origWidth / 2);
const offsetY = this._room.engine.application.view.height / 2 - (this.y + origHeight / 2);
gsap.to(this, {
x: this.x + Math.floor(diffWidth / 2 + offsetX),
y: this.y + Math.floor(diffHeight / 2 + offsetY),
duration: 0.8,
ease: 'easeOut'
});
}
/**
* Return the zoom level of the room container.
*
* @member {Application}
* @readonly
* @public
*/
public get zoomLevel(): number {
return this._zoomLevel;
}
}

View File

@ -0,0 +1,325 @@
import type { IPosition2D, ITileInfo, TileMap } from '../../types/Room';
import { WallType } from '../../enums/WallType';
import { StairType } from '../../enums/StairType';
import { Direction } from '../../enums/Direction';
/**
* RoomTileMap class that manage all the things about the room model.
*
* @class
* @memberof Scuti
*/
export class RoomTileMap {
/**
* The room tile map where every informations about the room model is stored.
*
* @member {TileMap}
* @private
*/
private readonly _tileMap: TileMap;
/**
* @param {string} [tileMap] - The room tile map string that need to be parsed.
*/
constructor(tileMap: string) {
/** Parse the tile map string to convert it into a matrix */
this._tileMap = this._parse(tileMap);
}
/**
* Parse the given tile map to convert it into a matrix.
*
* @param {string} [tileMap] - The tile map string that we want to convert into a matrix.
* @return {TileMap}
* @private
*/
private _parse(tileMap: string): TileMap {
tileMap = tileMap.replace(/ /g, '');
tileMap = tileMap.replace(/\n\n/g, '\n');
return tileMap.split(/\r?\n/).map((line) => {
return line.split('');
});
}
/**
* Reference to the room tile map matrix.
*
* @member {TileMap}
* @readonly
* @public
*/
public get tileMap(): TileMap {
return this._tileMap;
}
/**
* Convert the 2D position into it's tile type character.
*
* @param {IPosition2D} [position] - The tile position that we want to have the type.
* @return {string}
* @public
*/
public getTile(position: IPosition2D): string {
return position.x < 0 ||
position.y < 0 ||
this._tileMap[position.y] === undefined ||
this._tileMap[position.y][position.x] === undefined
? 'x'
: this._tileMap[position.y][position.x];
}
/**
* Convert the tile character into a number that is the height of the tile.
*
* @param {IPosition2D} [position] - The tile position that we want to have the height.
* @return {string}
* @public
*/
public getTileHeight(position: IPosition2D): number {
const tile = this.getTile(position);
return tile === 'x' ? 0 : isNaN(Number(tile)) ? tile.charCodeAt(0) - 96 + 9 : Number(tile);
}
/**
* Return informations about the tile (if it's a stair, a door, if there is walls, ...).
*
* @param {IPosition2D} [position] - The tile position that we want to have informations.
* @return {ITileInfo}
* @public
*/
public getTileInfo(position: IPosition2D): ITileInfo {
return {
tile: this.isTile(position),
door: this.isDoor(position),
height: this.getTileHeight(position),
stairType: this._getStairType(position),
wallType: this._getWallType(position)
};
}
/**
* Return walls informations about the given tile, like if it's a left wall, a corner wall, ...
*
* @param {IPosition2D} [position] - The tile position where we want to have the wall informations.
* @return {WallType}
* @private
*/
private _getWallType(position: IPosition2D): WallType | undefined {
const topLeftTile: IPosition2D = { x: position.x - 1, y: position.y - 1 };
const topTile: IPosition2D = { x: position.x, y: position.y - 1 };
const midLeftTile: IPosition2D = { x: position.x - 1, y: position.y };
if (this.isDoor(position)) return;
if (!this.isTile(topLeftTile) && !this.isTile(topTile) && !this.isTile(midLeftTile) && this.isTile(position))
return WallType.CORNER_WALL;
if (!this.isTile(midLeftTile) && this.isTile(position)) return WallType.LEFT_WALL;
if (!this.isTile(topTile) && this.isTile(position)) return WallType.RIGHT_WALL;
}
/**
* Return stairs informations about the given tile, like if it's a normal stair, a corner stair, ...
*
* @param {IPosition2D} [position] - The tile position where we want to have the stair informations.
* @return {{ type: StairType, direction: Direction }}
* @private
*/
private _getStairType(position: IPosition2D): { type: StairType; direction: Direction } | undefined {
const topLeftTile: IPosition2D = { x: position.x - 1, y: position.y - 1 };
const topTile: IPosition2D = { x: position.x, y: position.y - 1 };
const topRightTile: IPosition2D = { x: position.x + 1, y: position.y - 1 };
const midLeftTile: IPosition2D = { x: position.x - 1, y: position.y };
const midRightTile: IPosition2D = { x: position.x + 1, y: position.y };
const botLeftTile: IPosition2D = { x: position.x - 1, y: position.y + 1 };
const botTile: IPosition2D = { x: position.x, y: position.y + 1 };
const botRightTile: IPosition2D = { x: position.x + 1, y: position.y + 1 };
if (
this.isTile(position) &&
this.isTile(topRightTile) &&
this._getTileDifference(topRightTile, position) === 1 &&
this._getTileDifference(midRightTile, position) === 1 &&
this._getTileDifference(topTile, position) === 1
)
return { type: StairType.INNER_CORNER_STAIR, direction: Direction.NORTH_EAST };
if (
this.isTile(position) &&
this.isTile(botRightTile) &&
this._getTileDifference(botRightTile, position) === 1 &&
this._getTileDifference(midRightTile, position) === 1 &&
this._getTileDifference(botTile, position) === 1
)
return { type: StairType.INNER_CORNER_STAIR, direction: Direction.SOUTH_EAST };
if (
this.isTile(position) &&
this.isTile(botLeftTile) &&
this._getTileDifference(botLeftTile, position) === 1 &&
this._getTileDifference(midLeftTile, position) === 1 &&
this._getTileDifference(botTile, position) === 1
)
return { type: StairType.INNER_CORNER_STAIR, direction: Direction.SOUTH_WEST };
if (
this.isTile(position) &&
this.isTile(topLeftTile) &&
this._getTileDifference(topLeftTile, position) === 1 &&
this._getTileDifference(midLeftTile, position) === 1 &&
this._getTileDifference(topTile, position) === 1
)
return { type: StairType.INNER_CORNER_STAIR, direction: Direction.NORTH_WEST };
if (this.isTile(position) && this.isTile(topTile) && this._getTileDifference(topTile, position) === 1)
return { type: StairType.STAIR, direction: Direction.NORTH };
if (
this.isTile(position) &&
this.isTile(topRightTile) &&
this._getTileDifference(topRightTile, position) === 1 &&
this._getTileDifference(midRightTile, position) === 0 &&
this._getTileDifference(topTile, position) === 0
)
return { type: StairType.OUTER_CORNER_STAIR, direction: Direction.NORTH_EAST };
if (this.isTile(position) && this.isTile(midRightTile) && this._getTileDifference(midRightTile, position) === 1)
return { type: StairType.STAIR, direction: Direction.EAST };
if (
this.isTile(position) &&
this.isTile(botRightTile) &&
this._getTileDifference(botRightTile, position) === 1 &&
this._getTileDifference(midRightTile, position) === 0 &&
this._getTileDifference(botTile, position) === 0
)
return { type: StairType.OUTER_CORNER_STAIR, direction: Direction.SOUTH_EAST };
if (this.isTile(position) && this.isTile(botTile) && this._getTileDifference(botTile, position) === 1)
return { type: StairType.STAIR, direction: Direction.SOUTH };
if (
this.isTile(position) &&
this.isTile(botLeftTile) &&
this._getTileDifference(botLeftTile, position) === 1 &&
this._getTileDifference(midLeftTile, position) === 0 &&
this._getTileDifference(botTile, position) === 0
)
return { type: StairType.OUTER_CORNER_STAIR, direction: Direction.SOUTH_WEST };
if (this.isTile(position) && this.isTile(midLeftTile) && this._getTileDifference(midLeftTile, position) === 1)
return { type: StairType.STAIR, direction: Direction.WEST };
if (
this.isTile(position) &&
this.isTile(topLeftTile) &&
this._getTileDifference(topLeftTile, position) === 1 &&
this._getTileDifference(midLeftTile, position) === 0 &&
this._getTileDifference(topTile, position) === 0
)
return { type: StairType.OUTER_CORNER_STAIR, direction: Direction.NORTH_WEST };
}
/**
* Calculate the height differencte between two tile position.
*
* @param {IPosition2D} [position1] - The tile position that we want to compare the height.
* @param {IPosition2D} [position2] - The tile position that we want to compare the height.
* @return {number}
* @private
*/
private _getTileDifference(position1: IPosition2D, position2: IPosition2D): number {
return Number(this.getTileHeight(position1)) - Number(this.getTileHeight(position2));
}
/**
* Return a boolean that indicate if the tile position given refer to an existing tile.
*
* @param {IPosition2D} [position] - The tile position that we want to see if it exist.
* @return {boolean}
* @public
*/
public isTile(position: IPosition2D): boolean {
return this.getTile(position) !== 'x';
}
/**
* Return a boolean that indicate if the given tile is a door.
*
* @param {IPosition2D} [position] - The tile position that we want to see if it's a door.
* @return {boolean}
* @public
*/
public isDoor(position: IPosition2D): boolean {
const topLeftTile: IPosition2D = { x: position.x - 1, y: position.y - 1 };
const topTile: IPosition2D = { x: position.x, y: position.y - 1 };
const midLeftTile: IPosition2D = { x: position.x - 1, y: position.y };
const midTile: IPosition2D = { x: position.x, y: position.y };
const botLeftTile: IPosition2D = { x: position.x - 1, y: position.y + 1 };
const botTile: IPosition2D = { x: position.x, y: position.y + 1 };
return (
!this.isTile(topTile) &&
!this.isTile(topLeftTile) &&
!this.isTile(midLeftTile) &&
!this.isTile(botLeftTile) &&
!this.isTile(botTile) &&
this.isTile(midTile)
);
}
/**
* Return the max Z value of this tile map.
*
* @return {number}
* @public
*/
public get maxZ(): number {
let z = 0;
for (let y = 0; y < this._tileMap.length; y++) {
for (let x = 0; x < this._tileMap[y].length; x++) {
const height = this.getTileHeight({ x, y });
if (height > z) z = height;
}
}
return z;
}
/**
* Indicate if the given tile position have a left or a right wall.
*
* @param {IPosition2D} [position] - The given tile position that we wan't to check if it have walls.
* @return {{ x: boolean, y: boolean }}
* @public
*/
public hasWall(position: IPosition2D): { x: boolean; y: boolean } {
// TODO: Integrate it in _getWallType()
let wallX = false;
let wallY = false;
for (let i = position.y - 1; i >= 0; i--) {
const wall: WallType | undefined = this._getWallType({ x: position.x, y: i });
if (wall) {
if (wall === WallType.RIGHT_WALL || wall === (WallType.CORNER_WALL as WallType)) {
wallY = true;
}
}
for (let j = position.x - 1; j >= 0; j--) {
const wall2: WallType | undefined = this._getWallType({ x: j, y: i });
if (wall2) {
if (wall2 === WallType.LEFT_WALL || wall2 === (WallType.CORNER_WALL as WallType)) {
wallY = true;
}
}
}
}
for (let i = position.x - 1; i >= 0; i--) {
const wall: WallType | undefined = this._getWallType({ x: i, y: position.y });
if (wall) {
if (wall === WallType.LEFT_WALL || wall === (WallType.CORNER_WALL as WallType)) {
wallX = true;
}
}
for (let j = position.y - 1; j >= 0; j--) {
const wall2: WallType | undefined = this._getWallType({ x: i, y: j });
if (wall2) {
if (wall2 === WallType.RIGHT_WALL || wall2 === (WallType.CORNER_WALL as WallType)) {
wallX = true;
}
}
}
}
return { x: wallX, y: wallY };
}
}

View File

@ -0,0 +1,404 @@
import { Container, Ticker } from 'pixi.js';
import type { Room } from './Room';
import type { IPosition3D, ITileInfo } from '../../types/Room';
import { Tile } from './parts/Tile';
import { Wall } from './parts/Wall';
import { Stair } from './parts/Stair';
import { WallType } from '../../enums/WallType';
import type { StairType } from '../../enums/StairType';
import { Cursor } from './parts/Cursor';
import { RoomObjectLayer } from './layers/RoomObjectLayer';
import { RoomPartLayer } from './layers/RoomPartLayer';
/**
* RoomView class that manage all the rendering part of the room.
*
* @class
* @memberof Scuti
*/
export class RoomVisualization extends Container {
/**
* The room instance that will be managed by the camera.
*
* @member {Room}
* @private
*/
private readonly _room: Room;
/**
* The container that will contains all the objects like avatars or furnitures.
*
* @member {RoomObjectLayer}
* @private
*/
private readonly _objectLayer: RoomObjectLayer;
/**
* The container that will contains all the parts like tiles, walls and stairs.
*
* @member {RoomPartLayer}
* @private
*/
private readonly _partLayer: RoomPartLayer;
/**
* List containing all the walls instances.
*
* @member {Wall}
* @private
*/
private _walls: Wall[] = [];
/**
* List containing all the tiles and stairs instances.
*
* @member {Tile | Stair}
* @private
*/
private _tiles: Array<Tile | Stair> = [];
/**
* Infos related to the door tile.
*
* @member {ITileInfo}
* @private
*/
private _doorTile!: ITileInfo;
/**
* The room tile cursor instance.
*
* @member {Cursor}
* @private
*/
private _cursor!: Cursor;
/**
* The room animation ticker instance that will manage all the objects animations
*
* @member {Ticker}
* @private
*/
private readonly _animationTicker = new Ticker();
/**
* @param {Room} [room] - The room instance that we want to visualize.
*/
constructor(room: Room) {
super();
this._room = room;
this._objectLayer = new RoomObjectLayer(this._room);
this._partLayer = new RoomPartLayer(this._room);
/** Start the animation ticker */
this._animationTicker.maxFPS = 4;
this._animationTicker.start();
/** Render everything */
this._draw();
}
/**
* Draw the room visualization with all the tiles and walls.
*
* @return {void}
* @private
*/
private _draw(): void {
this._destroyParts();
this._destroyCursor();
for (let y = 0; y < this._room.tileMap.tileMap.length; y++) {
for (let x = 0; x < this._room.tileMap.tileMap[y].length; x++) {
const tileInfo = this._room.tileMap.getTileInfo({ x, y });
// todo: avoid duplicate tile doors
if (tileInfo.door && this._doorTile != null) tileInfo.door = false;
if (tileInfo.door && this._doorTile == null) this._doorTile = tileInfo;
this._createPart(tileInfo, { x, y, z: tileInfo.height });
}
}
}
/**
* Destroy all the parts (tiles, walls, stairs, ...).
*
* @return {void}
* @private
*/
private _destroyParts(): void {
[...this._tiles, ...this._walls].forEach((part) => part.destroy());
this._tiles = [];
this._walls = [];
}
/**
* Rerender all the room visualization.
*
* @return {void}
* @private
*/
public update(): void {
this._draw();
}
/**
* Create a room part and add it into the visualization.
*
* @param {ITileInfo} [tileInfo] - The tile informations where we want to create the part.
* @param {IPosition3D} [position] - And the position.
* @return {void}
* @private
*/
private _createPart(tileInfo: ITileInfo, position: IPosition3D): void {
if (tileInfo.wallType !== null || tileInfo.door) {
if (
tileInfo.wallType === WallType.CORNER_WALL &&
!this._room.tileMap.hasWall(position).x &&
!this._room.tileMap.hasWall(position).y
) {
this._createWall(position, WallType.CORNER_WALL);
this._createWall(position, WallType.LEFT_WALL);
this._createWall(position, WallType.RIGHT_WALL);
} else if (tileInfo.wallType === WallType.CORNER_WALL && !this._room.tileMap.hasWall(position).x) {
this._createWall(position, WallType.LEFT_WALL);
} else if (tileInfo.wallType === WallType.CORNER_WALL && !this._room.tileMap.hasWall(position).y) {
this._createWall(position, WallType.RIGHT_WALL);
}
if (tileInfo.wallType === WallType.LEFT_WALL && !this._room.tileMap.hasWall(position).y)
this._createWall(position, WallType.LEFT_WALL);
if (tileInfo.wallType === WallType.RIGHT_WALL && !this._room.tileMap.hasWall(position).y)
this._createWall(position, WallType.RIGHT_WALL);
if (tileInfo.door) this._createWall(position, WallType.DOOR_WALL);
}
if (tileInfo.stairType != null) {
position.direction = tileInfo.stairType.direction;
this._createStair(position, tileInfo.stairType.type);
} else if (tileInfo.door) {
this._createDoor(position);
} else if (tileInfo.tile) {
this._createTile(position, tileInfo);
}
}
/**
* Destroy the current cursor and draw a new one at the new position.
*
* @param {IPosition3D} [position] - The cursor position.
* @return {void}
* @private
*/
private _createCursor(position: IPosition3D): void {
if (this._cursor != null) {
this._cursor.visible = true;
return this._cursor.moveTo(position);
}
this._destroyCursor();
const cursor = new Cursor(this._room, { position });
this.addChild(cursor);
this._cursor = cursor;
}
/**
* Destroy the room cursor
*
* @return {void}
* @private
*/
private _destroyCursor(): void {
if (this._cursor != null) this._cursor.visible = false;
}
/**
* Create a tile.
*
* @param {IPosition3D} [position] - The tile position.
* @param {ITileInfo} [tileInfo]
* @return {void}
* @private
*/
private _createTile(position: IPosition3D, tileInfo: ITileInfo): void {
const tile = new Tile(
this._room,
{ position, material: this._room.floorMaterial, thickness: this._room.floorThickness },
tileInfo
);
/** Register interactions */
tile.onPointerDown = (event) => {
if (this._partLayer.tiles.onPointerDown != null) this._partLayer.tiles.onPointerDown(event);
};
tile.onPointerUp = (event) => {
if (this._partLayer.tiles.onPointerUp != null) this._partLayer.tiles.onPointerUp(event);
};
tile.onPointerMove = (event) => {
if (this._partLayer.tiles.onPointerMove != null) this._partLayer.tiles.onPointerMove(event);
};
tile.onPointerOut = (event) => {
if (this._partLayer.tiles.onPointerOut != null) this._partLayer.tiles.onPointerOut(event);
this._destroyCursor();
};
tile.onPointerOver = (event) => {
if (this._partLayer.tiles.onPointerOver != null) this._partLayer.tiles.onPointerOver(event);
this._createCursor(position);
};
tile.onDoubleClick = (event) => {
if (this._partLayer.tiles.onDoubleClick != null) this._partLayer.tiles.onDoubleClick(event);
};
this.addChild(tile);
this._tiles.push(tile);
}
/**
* Create a door.
*
* @param {IPosition3D} [position] - The door position.
* @return {void}
* @private
*/
private _createDoor(position: IPosition3D): void {
const tile = new Tile(this._room, { position, material: this._room.floorMaterial });
/** Register interactions */
tile.onPointerDown = (event) => {
if (this._partLayer.tiles.onPointerDown != null) this._partLayer.tiles.onPointerDown(event);
};
tile.onPointerUp = (event) => {
if (this._partLayer.tiles.onPointerUp != null) this._partLayer.tiles.onPointerUp(event);
};
tile.onPointerMove = (event) => {
if (this._partLayer.tiles.onPointerMove != null) this._partLayer.tiles.onPointerMove(event);
};
tile.onPointerOut = (event) => {
if (this._partLayer.tiles.onPointerOut != null) this._partLayer.tiles.onPointerOut(event);
this._destroyCursor();
};
tile.onPointerOver = (event) => {
if (this._partLayer.tiles.onPointerOver != null) this._partLayer.tiles.onPointerOver(event);
this._createCursor(position);
};
tile.onDoubleClick = (event) => {
if (this._partLayer.tiles.onDoubleClick != null) this._partLayer.tiles.onDoubleClick(event);
};
this.addChild(tile);
this._tiles.push(tile);
}
/**
* Create a wall.
*
* @param {IPosition3D} [position] - The wall position.
* @param {WallType} [type] - The wall type.
* @return {void}
* @private
*/
private _createWall(position: IPosition3D, type: WallType): void {
const wall = new Wall(this._room, {
position,
material: this._room.wallMaterial,
thickness: this._room.wallThickness,
height: this._room.wallHeight,
type
});
// todo!(): register event interactions for walls */
this.addChild(wall);
this._walls.push(wall);
}
/**
* Create stairs.
*
* @param {IPosition3D} [position] - The stairs position.
* @param {StairType} [type] - The stairs type.
* @return {void}
* @private
*/
private _createStair(position: IPosition3D, type: StairType): void {
const stair = new Stair(this._room, {
position,
material: this._room.floorMaterial,
thickness: this._room.floorThickness,
type
});
/** Register interactions */
stair.onPointerDown = (event) => {
if (this._partLayer.tiles.onPointerDown != null) this._partLayer.tiles.onPointerDown(event);
};
stair.onPointerUp = (event) => {
if (this._partLayer.tiles.onPointerUp != null) this._partLayer.tiles.onPointerUp(event);
};
stair.onPointerMove = (event) => {
if (this._partLayer.tiles.onPointerMove != null) this._partLayer.tiles.onPointerMove(event);
};
stair.onPointerOut = (event) => {
if (this._partLayer.tiles.onPointerOut != null) this._partLayer.tiles.onPointerOut(event);
this._destroyCursor();
};
stair.onPointerOver = (event) => {
if (this._partLayer.tiles.onPointerOver != null) this._partLayer.tiles.onPointerOver(event);
this._createCursor(position);
};
stair.onDoubleClick = (event) => {
if (this._partLayer.tiles.onDoubleClick != null) this._partLayer.tiles.onDoubleClick(event);
};
this.addChild(stair);
this._tiles.push(stair);
}
/**
* Reference to the room visualization room instance.
*
* @member {Room}
* @readonly
* @public
*/
public get room(): Room {
return this._room;
}
/**
* Reference to the object layer container.
*
* @member {RoomObjectLayer}
* @readonly
* @public
*/
public get objectLayer(): RoomObjectLayer {
return this._objectLayer;
}
/**
* Reference to the part layer container.
*
* @member {RoomObjectLayer}
* @readonly
* @public
*/
public get partLayer(): RoomPartLayer {
return this._partLayer;
}
/**
* Reference to the room animation ticker instance.
*
* @member {Ticker}
* @readonly
* @public
*/
public get animationTicker(): Ticker {
return this._animationTicker;
}
}

View File

@ -0,0 +1,72 @@
import type { RoomObject } from '../objects/RoomObject';
import type { Room } from '../Room';
/**
* RoomObjectLayer class that manage all the room objects.
*
* @class
* @memberof Scuti
*/
export class RoomObjectLayer {
/**
* The room instance that will be managed by the camera.
*
* @member {Room}
* @private
*/
private readonly _room: Room;
/**
* The object list.
*
* @member {RoomObject[]}
* @private
*/
private _objects: RoomObject[] = [];
/**
* @param {Room} [room] - The room instance that we want to visualize.
*/
constructor(room: Room) {
this._room = room;
}
/**
* Add the given room object into the object layer of the room.
*
* @param {RoomObject[]} [objects] - The room objects that we want to add.
* @return {void}
* @public
*/
public add(...objects: RoomObject[]): void {
return objects.forEach((object) => {
object.room = this._room;
this._objects.push(object);
// @ts-expect-error
this._room.visualization.addChild(object);
if (object.onRoomAdded !== undefined) object.onRoomAdded(this._room);
if (object.visualization.loaded) object.visualization.render();
else object.visualization.renderPlaceholder();
});
}
/**
* Remove the given room object into the object layer of the room.
*
* @param {RoomObject[]} [objects] - The room objects that we want to remove.
* @return {void}
* @public
*/
public remove(...objects: RoomObject[]): void {
return objects.forEach((object) => {
if (object.onRoomRemoved !== undefined) object.onRoomRemoved(this._room);
object.room = undefined;
// @ts-expect-error
this._room.visualization.removeChild(object);
this._objects = this._objects.filter((fObject) => fObject !== object);
object.destroy();
});
}
}

View File

@ -0,0 +1,104 @@
import { Point } from 'pixi.js';
import type { Room } from '../Room';
import type { RoomPart } from '../parts/RoomPart';
import { EventManager } from '../../interactions/EventManager';
import type { Tile } from '../parts/Tile';
import type { Stair } from '../parts/Stair';
import type { Dimension } from '../../../types/Dimension';
/**
* RoomPartLayer class that manage all the room parts.
*
* @class
* @memberof Scuti
*/
export class RoomPartLayer {
/**
* The room instance that will be managed by the camera.
*
* @member {Room}
* @private
*/
private readonly _room: Room;
/**
* The part list.
*
* @member {RoomPart[]}
* @private
*/
private readonly _parts: RoomPart[] = [];
/**
* The room tiles interaction manager.
*
* @member {EventManager}
* @private
*/
private readonly _tileInteractionManager = new EventManager();
/**
* The room walls interaction manager.
*
* @member {EventManager}
* @private
*/
private readonly _wallInteractionManager = new EventManager();
/**
* @param {Room} [room] - The room instance that we want to visualize.
*/
constructor(room: Room) {
this._room = room;
}
/**
* Add the given room part into the part layer of the room.
*
* @param {RoomPart} [part] - The room part that we want to add.
* @return {void}
* @public
*/
public add(part: RoomPart): void {
this._parts.push(part);
}
/**
* Return the part at the specified screen position.
*
* @param {IPosition2D} [position] - The screen position.
* @return {Tile | Stair}
* @public
*/
public getFromGlobal(position: Dimension.IPosition2D): Tile | Stair {
const container = this._room.visualization.children.find((container) => {
const point = new Point(position.x, position.y);
if (Boolean(container.hitArea?.contains(container.toLocal(point).x, container.toLocal(point).y)))
return container;
});
// @ts-expect-error
return container;
}
/**
* Return the tile event manager.
*
* @return {EventManager}
* @public
*/
public get tiles(): EventManager {
return this._tileInteractionManager;
}
/**
* Return the wall event manager.
*
* @return {EventManager}
* @public
*/
public get walls(): EventManager {
return this._wallInteractionManager;
}
}

View File

@ -0,0 +1,73 @@
import type { Spritesheet } from 'pixi.js';
import { Assets, Sprite, Texture } from 'pixi.js';
import { Material } from './Material';
import type { Scuti } from '../../../Scuti';
import type { RoomMaterial } from '../../../types/RoomMaterial';
import { Logger } from '../../../utilities/Logger';
export class FloorMaterial extends Material {
/**
* The game engine instance that the room will be using to render texture.
*
* @member {Scuti}
* @private
*/
private readonly _engine: Scuti;
/**
* The material id from materials.json.
*
* @member {number}
* @private
*/
private readonly _id: number | undefined;
/**
* @param {Scuti} [engine] - The scuti engine instance to use.
* @param {number} [id] - The id of the material (it can be found into materials.json).
**/
constructor(engine: Scuti, id?: number) {
super(0xffffff, Texture.WHITE);
this._engine = engine;
this._id = id;
this._load();
}
/**
* Load the material.
*
* @return {void}
* @private
*/
private _load(): void {
const materials = Assets.get<RoomMaterial>('room/materials');
const defaultMaterial = materials.floorData.floors[0];
let material = materials.floorData.floors.find((material) => {
if (this._id != null) return material.id === this._id.toString();
else return defaultMaterial;
});
if (material == null) {
const console = new Logger('FloorMaterial');
this._id != null && console.warn(`Unknown floor id: "${this._id}"`);
/** apply default (white) one rather than throwing an error */
material = defaultMaterial;
}
const { color, materialId } = material.visualizations[0].layers[0];
const materialTexture = materials.floorData.textures.find((texture) => {
return texture.id === materialId.toString();
});
const name = materialTexture?.bitmaps[0].assetName as string;
const texture = Assets.get<Spritesheet>('room/room').textures[`room_${name}.png`];
const sprite = new Sprite(texture);
this.color = color;
this.texture = new Texture(this._engine.application.renderer.generateTexture(sprite).baseTexture);
}
}

View File

@ -0,0 +1,76 @@
import type { Texture } from 'pixi.js';
/**
* Material class that regroup the color and texture to be applied on the wall or the floor.
*
* @class
* @memberof Scuti
*/
export class Material {
/**
* The material color.
*
* @member {number}
* @private
*/
private _color: number;
/**
* The material texture.
*
* @member {Texture}
* @private
*/
private _texture: Texture;
/**
* @param {number} [color] - The material color.
* @param {Texture} [texture] - The material texture.
**/
constructor(color: number, texture: Texture) {
this._color = color;
this._texture = texture;
}
/**
* Reference to the material color.
*
* @member {number}
* @readonly
* @public
*/
public get color(): number {
return this._color;
}
/**
* Update the material color.
*
* @param {number} [color] - The new material color.
* @public
*/
public set color(color: number) {
this._color = color;
}
/**
* Reference to the material texture.
*
* @member {Texture}
* @readonly
* @public
*/
public get texture(): Texture {
return this._texture;
}
/**
* Update the material texture.
*
* @param {Texture} [texture] - The new material texture.
* @public
*/
public set texture(texture: Texture) {
this._texture = texture;
}
}

View File

@ -0,0 +1,73 @@
import type { Spritesheet } from 'pixi.js';
import { Assets, Sprite, Texture } from 'pixi.js';
import { Material } from './Material';
import type { Scuti } from '../../../Scuti';
import type { RoomMaterial } from '../../../types/RoomMaterial';
import { Logger } from '../../../utilities/Logger';
export class WallMaterial extends Material {
/**
* The game engine instance that the room will be using to render texture.
*
* @member {Scuti}
* @private
*/
private readonly _engine: Scuti;
/**
* The material id from materials.json.
*
* @member {number}
* @private
*/
private readonly _id: number | undefined;
/**
* @param {Scuti} [engine] - The scuti engine instance to use.
* @param {number} [id] - The id of the material (it can be found into materials.json).
**/
constructor(engine: Scuti, id?: number) {
super(0xffffff, Texture.WHITE);
this._engine = engine;
this._id = id;
this._load();
}
/**
* Load the material.
*
* @return {void}
* @private
*/
private _load(): void {
const materials = Assets.get<RoomMaterial>('room/materials');
const defaultMaterial = materials.wallData.walls[0];
let material = materials.wallData.walls.find((material) => {
if (this._id != null) return material.id === this._id.toString();
else return defaultMaterial;
});
if (material == null) {
const console = new Logger('WallMaterial');
this._id != null && console.warn(`Unknown wall id: "${this._id}"`);
/** apply default (white) one rather than throwing an error */
material = defaultMaterial;
}
const { color, materialId } = material.visualizations[0].layers[0];
const materialTexture = materials.wallData.textures.find((texture) => {
return texture.id === materialId.toString();
});
const name = materialTexture?.bitmaps[0].assetName as string;
const texture = Assets.get<Spritesheet>('room/room').textures[`room_${name}.png`];
const sprite = new Sprite(texture);
this.color = color;
this.texture = new Texture(this._engine.application.renderer.generateTexture(sprite).baseTexture);
}
}

View File

@ -0,0 +1,547 @@
import type { Filter } from 'pixi.js';
import { Container } from 'pixi.js';
import gsap from 'gsap';
import { EventManager } from '../../interactions/EventManager';
import { Logger } from '../../../utilities/Logger';
import type { Room } from '../Room';
import type { Direction } from '../../../enums/Direction';
import type { RoomObjectVisualization } from './RoomObjectVisualization';
import type { IFloorPosition, IWallPosition } from '../../../types/Furniture';
import type { Dimension, IAvatarPosition, IRoomObjectConfig, IInteractionEvent } from '../../../types';
import type { FurnitureData } from '../../furnitures/visualizations/FurnitureData';
import { FloorFurniture } from '../../furnitures/FloorFurniture';
import { WallFurniture } from '../../furnitures/WallFurniture';
/**
* RoomObject class that is extended by the avatars or furnitures.
*
* @class
* @memberof Scuti
*/
export abstract class RoomObject extends Container {
/**
* The object's position in the room.
*
* @member {FloorPosition | IWallPosition | IAvatarPosition}
* @private
*/
private readonly _position: IFloorPosition | IWallPosition | IAvatarPosition;
private _isAnimating = false;
/**
* The object's direction in the room.
*
* @member {Direction}
* @private
*/
private _direction: Direction;
/**
* The object's state that represent it's current playing animation.
*
* @member {number}
* @private
*/
public _state!: number;
/**
* The object's visualization.
*
* @member {FurnitureData}
* @private
*/
public _visualization!: RoomObjectVisualization;
/**
* The object's data.
*
* @member {FurnitureData}
* @private
*/
public _data!: FurnitureData;
/**
* The room object's logger instance.
*
* @member {Logger}
* @private
*/
private readonly _logger = new Logger('RoomObject');
/**
* The object interaction manager to handle all the clicks and taps.
*
* @member {EventManager}
* @private
*/
private readonly _eventManager = new EventManager();
/**
* The room instance that will be managed by the camera.
*
* @member {Room}
* @private
*/
private _room!: Room;
/**
* The room object filters.
*
* @member {Filter[]}n
* @private
*/
private _filters: Filter[] = [];
protected constructor(config: IRoomObjectConfig) {
super();
this._position = config.position;
this._direction = config.direction;
}
/**
* Move the object at te given position and in time.
*
* @param {IFloorPosition | IWallPosition | IAvatarPosition} [position] - The position where we want to move the furniture.
* @param {number} [duration] - The time to move the furniture to the given position.
* @return {void}
* @public
*/
abstract move(position: IFloorPosition | IWallPosition | IAvatarPosition, duration: number): void;
/**
* Rotate the furniture at the given direction and in time.
*
* @param {Direction} [direction] - The new direction of the furniture.
* @param {number} [duration] - The time to rotate the furniture at the given direction.
* @return {void}
* @public
*/
public rotate(direction?: Direction, duration: number = 0.15): void {
if (this instanceof FloorFurniture || this instanceof WallFurniture) {
if (this._visualization === undefined || this._isAnimating) return;
const z = (this.position as Dimension.IPosition3D).z;
gsap.to(this.position, {
z: z + 0.5,
duration,
onStart: () => {
this._isAnimating = true;
},
onUpdate: () => this._visualization.updatePosition(),
onComplete: () => {
if (direction == null) {
const direction = this.visualization.directions.indexOf(this.direction);
const nextDirection = (direction + 1) % this.visualization.directions.length;
this._direction = this.visualization.directions[nextDirection];
} else this._direction = direction;
this._visualization.render();
gsap.to(this.position, {
z,
duration,
onComplete: () => {
this._visualization.render();
this._isAnimating = false;
},
onUpdate: () => this._visualization.updatePosition()
});
}
});
} else {
// todo!(): rotate entities (avatar, pet or bot)
}
}
/**
* Reference to the room object room instance.
*
* @member {Room | undefined}
* @readonly
* @public
*/
get room(): Room {
return this._room;
}
/**
* Update the current room instance.
*
* @param {Room} [room] - The new room instance.
* @public
*/
set room(room: Room) {
this._room = room;
}
/**
* Reference to the object event manager.
*
* @member {EventManager}
* @readonly
* @public
*/
get eventManager(): EventManager {
return this._eventManager;
}
/**
* Reference to the pointer down event.
*
* @member {(event: IInteractionEvent) => void}
* @readonly
* @public
*/
get onPointerDown(): (event: IInteractionEvent) => void {
return this._eventManager.onPointerDown;
}
/**
* Update the event function that will be executed.
*
* @param {(event: IInteractionEvent) => void} [value] - The event function that will be executed.
* @public
*/
set onPointerDown(value: (event: IInteractionEvent) => void) {
this._eventManager.onPointerDown = value;
}
/**
* Reference to the pointer up event.
*
* @member {(event: IInteractionEvent) => void}
* @readonly
* @public
*/
get onPointerUp(): (event: IInteractionEvent) => void {
return this._eventManager.onPointerUp;
}
/**
* Update the event function that will be executed.
*
* @param {(event: IInteractionEvent) => void} [value] - The event function that will be executed.
* @public
*/
set onPointerUp(value: (event: IInteractionEvent) => void) {
this._eventManager.onPointerUp = value;
}
/**
* Reference to the pointer move event.
*
* @member {(event: IInteractionEvent) => void}
* @readonly
* @public
*/
get onPointerMove(): (event: IInteractionEvent) => void {
return this._eventManager.onPointerMove;
}
/**
* Update the event function that will be executed.
*
* @param {(event: IInteractionEvent) => void} [value] - The event function that will be executed.
* @public
*/
set onPointerMove(value: (event: IInteractionEvent) => void) {
this._eventManager.onPointerMove = value;
}
/**
* Reference to the pointer out event.
*
* @member {(event: IInteractionEvent) => void}
* @readonly
* @public
*/
get onPointerOut(): (event: IInteractionEvent) => void {
return this._eventManager.onPointerOut;
}
/**
* Update the event function that will be executed.
*
* @param {(event: IInteractionEvent) => void} [value] - The event function that will be executed.
* @public
*/
set onPointerOut(value: (event: IInteractionEvent) => void) {
this._eventManager.onPointerOut = value;
}
/**
* Reference to the pointer over event.
*
* @member {(event: IInteractionEvent) => void}
* @readonly
* @public
*/
get onPointerOver(): (event: IInteractionEvent) => void {
return this._eventManager.onPointerOver;
}
/**
* Update the event function that will be executed.
*
* @param {(event: IInteractionEvent) => void} [value] - The event function that will be executed.
* @public
*/
set onPointerOver(value: (event: IInteractionEvent) => void) {
this._eventManager.onPointerOver = value;
}
/**
* Reference to the pointer double click event.
*
* @member {(event: IInteractionEvent) => void}
* @readonly
* @public
*/
get onDoubleClick(): (event: IInteractionEvent) => void {
return this._eventManager.onDoubleClick;
}
/**
* Update the event function that will be executed.
*
* @param {(event: IInteractionEvent) => void} [value] - The event function that will be executed.
* @public
*/
set onDoubleClick(value: (event: IInteractionEvent) => void) {
this._eventManager.onDoubleClick = value;
}
/**
* Reference to the assets starting load event.
*
* @member {() => void}
* @readonly
* @public
*/
public get onLoad(): () => void {
return this._eventManager.onLoad;
}
/**
* Update the event function that will be executed.
*
* @param {() => void} [value] - The event function that will be executed.
* @public
*/
public set onLoad(value: () => void) {
this._eventManager.onLoad = value;
}
/**
* Reference to the assets ending load event.
*
* @member {() => void}
* @readonly
* @public
*/
public get onLoadComplete(): () => void {
return this._eventManager.onLoadComplete;
}
/**
* Update the event function that will be executed.
*
* @param {() => void} [value] - The event function that will be executed.
* @public
*/
public set onLoadComplete(value: () => void) {
this._eventManager.onLoadComplete = value;
}
/**
* Reference to the room add event.
*
* @member {(room: Room) => void}
* @readonly
* @public
*/
get onRoomAdded(): (event: Room) => void {
return this._eventManager.onRoomAdded;
}
/**
* Update the event function that will be executed.
*
* @param {(room: Room) => void} [value] - The event function that will be executed.
* @public
*/
set onRoomAdded(value: (room: Room) => void) {
this._eventManager.onRoomAdded = value;
}
/**
* Reference to the room remove event.
*
* @member {(room: Room) => void}
* @readonly
* @public
*/
get onRoomRemoved(): (event: Room) => void {
return this._eventManager.onRoomRemoved;
}
/**
* Update the event function that will be executed.
*
* @param {(room: Room) => void} [value] - The event function that will be executed.
* @public
*/
set onRoomRemoved(value: (room: Room) => void) {
this._eventManager.onRoomRemoved = value;
}
/**
* Reference to the furniture state.
*
* @member {number}
* @readonly
* @public
*/
public get state(): number {
return this._state;
}
/**
* Update the furniture state (so the animation).
*
* @param {number} [state] - The new furniture state.
* @public
*/
public set state(state: number) {
this._state = state;
this._visualization.render();
}
/**
* Reference the filters list.
*
* @member {Filter[]}
* @readonly
* @public
*/
// @ts-expect-error
public get filters(): Filter[] {
return this._filters;
}
/**
* Update the filters list.
*
* @member {Filter[]}
* @readonly
* @public
*/
public set filters(filters: Filter[]) {
this._filters = filters;
}
/**
* Add a filter to the room object.
*
* @param {Filter} [filter] - The filter.
* @public
*/
public addFilter(filter: Filter): void {
if (this._filters.includes(filter)) return;
this._filters.push(filter);
this._visualization.render();
}
/**
* Remove filter from the room object.
*
* @param {Filter} [filter] - The filter.
* @public
*/
public removeFilter(filter: Filter): void {
this._filters = this._filters.filter((fFilter) => fFilter !== filter);
this._visualization.render();
}
/**
* Reference to the object's direction.
*
* @member {Direction}
* @public
*/
public get direction(): Direction {
return this._direction;
}
/**
* Update the object's direction
*
* @member {Direction}
* @public
*/
public set direction(direction: Direction) {
this._direction = direction;
}
/**
* Reference to the furniture position in the room.
*
* @member {IFloorPosition}
* @readonly
* @public
*/
// @ts-expect-error
public get position(): IFloorPosition | IWallPosition | IAvatarPosition {
return this._position;
}
/**
* Destroy the room object from the room.
*
* @return {void}
* @public
*/
destroy(): void {
if (this._visualization === undefined) return;
this._visualization.destroy();
if (this._room == null) return;
this._room.objects.remove(this);
}
/**
* Reference to the room object logger instance.
*
* @member {Logger}
* @readonly
* @public
*/
public get logger(): Logger {
return this._logger;
}
/**
* Reference to the visualization instance.
*
* @member {RoomObjectVisualization}
* @readonly
* @public
*/
public get visualization(): RoomObjectVisualization {
return this._visualization;
}
/**
* Reference to the furniture data.
*
* @member {FurnitureData}
* @readonly
* @public
*/
public get data(): FurnitureData {
return this._data;
}
}

View File

@ -0,0 +1,76 @@
import type { Sprite } from 'pixi.js';
import type { Direction } from '../../../enums/Direction';
import { Logger } from '../../../utilities/Logger';
export abstract class RoomObjectVisualization {
/**
* The room object visualization's logger instance.
*
* @member {Logger}
* @private
*/
private readonly _logger = new Logger('RoomObjectVisualization');
private _placeholder!: Sprite;
private _directions!: Direction[];
private _loaded = false;
/**
* Renders the visual layers of the room object.
*
* @public
*/
public abstract render(): void;
/**
* Renders the placeholder right before the layers of the room object when loading.
*
* @public
*/
public abstract renderPlaceholder(): void;
/**
* Updates the position of each visual layer of the room object.
*
* @public
*/
public abstract updatePosition(): void;
/**
* Removes all visual layers of the room object.
*
* @public
*/
public abstract destroy(): void;
get loaded(): boolean {
return this._loaded;
}
set loaded(load: boolean) {
this._loaded = load;
}
set placeholder(placeholder: Sprite) {
this._placeholder = placeholder;
}
get placeholder(): Sprite {
return this._placeholder;
}
set directions(directions: Direction[]) {
this._directions = directions;
}
get directions(): Direction[] {
return this._directions;
}
get logger(): Logger {
return this._logger;
}
}

View File

@ -0,0 +1,69 @@
import type { Spritesheet } from 'pixi.js';
import { Container, Assets, Sprite } from 'pixi.js';
import type { Room } from '../Room';
import type { ICursorConfiguration, IPosition3D } from '../../../types/Room';
import { ZOrder } from '../../../utilities/ZOrder';
/**
* Cursor class that show up when we move the cursor on a room tile.
*
* @class
* @memberof Scuti
*/
export class Cursor extends Container {
/**
* The cursor position.
*
* @member {IPosition3D}
* @private
*/
private _position: IPosition3D;
/**
* @param {Room} [_room] - The room instance where the cursor will be drawn.
* @param {ICursorConfiguration} [configuration] - The tile configuration.
**/
constructor(_room: Room, configuration: ICursorConfiguration) {
super();
this._position = configuration.position;
/** Draw the cursor */
this._draw();
// todo!(): create the blue circle 'cursor_64_b' cursor when needed
}
/**
* Draw the cursor.
*
* @return {void}
* @private
*/
private _draw(): void {
/** Creating the sprite */
const texture = Assets.get<Spritesheet>('room/cursors').textures['tile_cursor_64_a_0_0.png'];
const sprite = new Sprite(texture);
sprite.y = -20;
this.addChild(sprite);
/** Positionate the cursor and its zIndex */
this.moveTo(this._position);
this.zIndex = ZOrder.tileCursor(this._position);
}
/**
* Apply position of the cursor on the x, y axis relative to the local coordinates of the parent.
*
* @param {IPosition3D} [position] - The cursor position.
* @return {void}
* @public
*/
public moveTo(position: IPosition3D): void {
// this.zOrder = ZOrder.tileCursor(position);
this._position = position;
this.x = 32 * this._position.x - 32 * this._position.y;
this.y = 16 * this._position.x + 16 * this._position.y - 32 * this._position.z;
}
}

View File

@ -0,0 +1,228 @@
import { Container, Point } from 'pixi.js';
import type { Dimension } from '../../../types/Dimension';
import type { Room } from '../Room';
import type { Stair, Tile } from '.';
import { EventManager } from '../../interactions/EventManager';
import type { IInteractionEvent } from '../../../types/Interaction';
export abstract class RoomPart extends Container {
/**
* The part's position in the room.
*
* @member {IWallPosition | IPosition3D}
* @private
*/
abstract _position: Dimension.IPosition3D;
/**
* The part interaction manager to handle all the clicks and taps.
*
* @member {EventManager}
* @private
*/
private readonly _interactionManager = new EventManager();
/**
* The room instance where the ârt will be drawn.
*
* @member {Room}
* @private
*/
private _room: Room;
constructor(room: Room) {
super();
this._room = room;
}
/**
* Return the part at the specified screen position.
*
* @param {IPosition2D} [position] - The screen position.
* @return {Tile | Stair}
* @public
*/
public getFromGlobal(position: Dimension.IPosition2D): Tile | Stair {
const container = this._room.visualization.children.find((container) => {
const point = new Point(position.x, position.y);
if (Boolean(container.hitArea?.contains(container.toLocal(point).x, container.toLocal(point).y)))
return container;
});
// @ts-expect-error
return container;
}
public registerInteractions(position: Dimension.IPosition3D): void {
this.on('pointerdown', (event) => {
return this.interactionManager.handlePointerDown({
event,
position: { x: position.x, y: position.y, z: position.z }
});
});
this.on('pointerup', (event) => {
return this.interactionManager.handlePointerUp({
event,
position: { x: position.x, y: position.y, z: position.z }
});
});
this.on('pointermove', (event) => {
return this.interactionManager.handlePointerMove({
event,
position: { x: position.x, y: position.y, z: position.z }
});
});
this.on('pointerout', (event) => {
return this.interactionManager.handlePointerOut({
event,
position: { x: position.x, y: position.y, z: position.z }
});
});
this.on('pointerover', (event) => {
return this.interactionManager.handlePointerOver({
event,
position: { x: position.x, y: position.y, z: position.z }
});
});
}
public set room(room: Room) {
this._room = room;
}
public get room(): Room {
return this._room;
}
public get interactionManager(): EventManager {
return this._interactionManager;
}
/**
* Reference to the pointer down event.
*
* @member {(event: IInteractionEvent) => void}
* @readonly
* @public
*/
public get onPointerDown(): (event: IInteractionEvent) => void {
return this._interactionManager.onPointerDown;
}
/**
* Update the event function that will be executed.
*
* @param {(event: IInteractionEvent) => void} [value] - The event function that will be executed.
* @public
*/
public set onPointerDown(value: (event: IInteractionEvent) => void) {
this._interactionManager.onPointerDown = value;
}
/**
* Reference to the pointer up event.
*
* @member {(event: IInteractionEvent) => void}
* @readonly
* @public
*/
public get onPointerUp(): (event: IInteractionEvent) => void {
return this._interactionManager.onPointerUp;
}
/**
* Update the event function that will be executed.
*
* @param {(event: IInteractionEvent) => void} [value] - The event function that will be executed.
* @public
*/
public set onPointerUp(value: (event: IInteractionEvent) => void) {
this._interactionManager.onPointerUp = value;
}
/**
* Reference to the pointer move event.
*
* @member {(event: IInteractionEvent) => void}
* @readonly
* @public
*/
public get onPointerMove(): (event: IInteractionEvent) => void {
return this._interactionManager.onPointerMove;
}
/**
* Update the event function that will be executed.
*
* @param {(event: IInteractionEvent) => void} [value] - The event function that will be executed.
* @public
*/
public set onPointerMove(value: (event: IInteractionEvent) => void) {
this._interactionManager.onPointerMove = value;
}
/**
* Reference to the pointer out event.
*
* @member {(event: IInteractionEvent) => void}
* @readonly
* @public
*/
public get onPointerOut(): (event: IInteractionEvent) => void {
return this._interactionManager.onPointerOut;
}
/**
* Update the event function that will be executed.
*
* @param {(event: IInteractionEvent) => void} [value] - The event function that will be executed.
* @public
*/
public set onPointerOut(value: (event: IInteractionEvent) => void) {
this._interactionManager.onPointerOut = value;
}
/**
* Reference to the pointer over event.
*
* @member {(event: IInteractionEvent) => void}
* @readonly
* @public
*/
public get onPointerOver(): (event: IInteractionEvent) => void {
return this._interactionManager.onPointerOver;
}
/**
* Update the event function that will be executed.
*
* @param {(event: IInteractionEvent) => void} [value] - The event function that will be executed.
* @public
*/
public set onPointerOver(value: (event: IInteractionEvent) => void) {
this._interactionManager.onPointerOver = value;
}
/**
* Reference to the pointer double click event.
*
* @member {(event: IInteractionEvent) => void}
* @readonly
* @public
*/
public get onDoubleClick(): (event: IInteractionEvent) => void {
return this._interactionManager.onDoubleClick;
}
/**
* Update the event function that will be executed.
*
* @param {(event: IInteractionEvent) => void} [value] - The event function that will be executed.
* @public
*/
public set onDoubleClick(value: (event: IInteractionEvent) => void) {
this._interactionManager.onDoubleClick = value;
}
}

View File

@ -0,0 +1,579 @@
import { Container, Graphics, Matrix, Point, Polygon } from 'pixi.js';
import { Color } from '@pixi/color';
import type { Room } from '../Room';
import type { IPosition3D, IPosition2D, IStairConfiguration } from '../../../types/Room';
import type { Material } from '../materials/Material';
import { StairType } from '../../../enums/StairType';
import { Direction } from '../../../enums/Direction';
import { FloorMaterial } from '../materials/FloorMaterial';
import { ZOrder } from '../../../utilities/ZOrder';
import { RoomPart } from './RoomPart';
/**
* Stair class that show up when two tiles side by side have a height difference of one.
*
* @class
* @memberof Scuti
*/
export class Stair extends RoomPart {
/**
* The thickness of the stair part.
*
* @member {number}
* @private
*/
private readonly _thickness: number;
/**
* The stair material that will be applied to this part, it contains the color and the texture of the stair.
*
* @member {Material}
* @private
*/
private readonly _material: Material;
/**
* The stair position.
*
* @member {IPosition3D}
* @private
*/
readonly _position: IPosition3D;
/**
* The stair type.
*
* @member {StairType}
* @private
*/
private readonly _type: StairType;
/**
* @param {Room} [room] - The room instance where the stair will be drawn.
* @param {IStairConfiguration} [configuration] - The stair configuration.
* @param {Material} [configuration.material] - The stair material that will be applied.
* @param {number} [configuration.thickness] - The stair thickness.
* @param {IPosition3D} [configuration.position] - The stair position.
* @param {StairType} [configuration.type] - The stair type.
**/
constructor(room: Room, configuration: IStairConfiguration) {
super(room);
/** Store the configuration */
this.room = room;
this._position = configuration.position;
this._thickness = configuration.thickness ?? 8;
this._material = configuration.material ?? new FloorMaterial(this.room.engine, 111);
this._type = configuration.type;
/** Register interactions */
this.registerInteractions(this._position);
/** Draw the stair */
this._draw();
}
/**
* Select which stair should be drawn by it's type.
*
* @return {void}
* @private
*/
private _draw(): void {
if (this._type === StairType.STAIR) {
/** Straight stair */
switch (this._position.direction) {
/** Draw a north stair */
case Direction.NORTH:
return this._drawStair(
[
{ x: 0, y: 0 },
{ x: 8, y: -4 },
{ x: 40, y: 12 },
{ x: 32, y: 16 }
],
[
{ x: 8, y: -12 },
{ x: 0, y: 0 }
]
);
/** Draw an east stair */
case Direction.EAST:
return this._drawStair(
[
{ x: 0, y: 0 },
{ x: 32, y: -16 },
{ x: 40, y: -12 },
{ x: 8, y: 4 }
],
[
{ x: 8, y: -4 },
{ x: 0, y: 0 }
]
);
/** Draw a south stair */
case Direction.SOUTH:
return this._drawStair(
[
{ x: 0, y: 0 },
{ x: 8, y: -4 },
{ x: 40, y: 12 },
{ x: 32, y: 16 }
],
[
{ x: -8, y: -4 },
{ x: 24, y: -12 }
]
);
/** Draw a west stair */
case Direction.WEST:
return this._drawStair(
[
{ x: 0, y: 0 },
{ x: 32, y: -16 },
{ x: 40, y: -12 },
{ x: 8, y: 4 }
],
[
{ x: -8, y: -12 },
{ x: 24, y: 12 }
]
);
}
} else if (this._type === StairType.OUTER_CORNER_STAIR) {
/** Corner stair */
switch (this._position.direction) {
/** Draw a north east stair */
case Direction.NORTH_EAST:
return this._drawCornerStair(
[
{ x: 0, y: 0 },
{ x: 8, y: -4 },
{ x: 16, y: 0 },
{ x: 8, y: 4 }
],
[
{ x: 0, y: 0 },
{ x: 0, y: 0 },
{ x: 8, y: 4 },
{ x: 8, y: 4 }
],
[
{ x: 16, y: -8 },
{ x: 24, y: -12 },
{ x: 0, y: 0 },
{ x: 8, y: -4 }
]
);
/** Draw a south east stair */
case Direction.SOUTH_EAST:
return this._drawCornerStair(
[
{ x: 0, y: 0 },
{ x: 8, y: -4 },
{ x: 16, y: 0 },
{ x: 8, y: 4 }
],
[
{ x: 0, y: 0 },
{ x: 0, y: 0 },
{ x: 8, y: 4 },
{ x: 8, y: 4 }
],
[
{ x: 0, y: 0 },
{ x: -8, y: 4 },
{ x: 24, y: -12 },
{ x: 0, y: 0 }
]
);
/** Draw a south west stair */
case Direction.SOUTH_WEST:
return this._drawCornerStair(
[
{ x: 0, y: 0 },
{ x: 8, y: -4 },
{ x: 16, y: 0 },
{ x: 8, y: 4 }
],
[
{ x: 0, y: 0 },
{ x: 0, y: 0 },
{ x: 8, y: 4 },
{ x: 8, y: 4 }
],
[
{ x: -8, y: -4 },
{ x: 16, y: 16 },
{ x: 24, y: -12 },
{ x: -16, y: -8 }
]
);
/** Draw a north west stair */
case Direction.NORTH_WEST:
return this._drawCornerStair(
[
{ x: 0, y: 0 },
{ x: 8, y: -4 },
{ x: 16, y: 0 },
{ x: 8, y: 4 }
],
[
{ x: 0, y: 0 },
{ x: 0, y: 0 },
{ x: 8, y: 4 },
{ x: 8, y: 4 }
],
[
{ x: 8, y: -12 },
{ x: 48, y: 0 },
{ x: 0, y: 0 },
{ x: -8, y: -12 }
]
);
}
} else if (this._type === StairType.INNER_CORNER_STAIR) {
/** Inner corner stair */
switch (this._position.direction) {
/** Draw a north east inner stair */
case Direction.NORTH_EAST:
return this._drawCornerStair(
[
{ x: 0, y: 0 },
{ x: 8, y: -4 },
{ x: 16, y: 0 },
{ x: 8, y: 4 }
],
[
{ x: 0, y: 0 },
{ x: 0, y: 0 },
{ x: 8, y: 4 },
{ x: 8, y: 4 }
],
[
{ x: -8, y: 12 },
{ x: 16, y: 16 },
{ x: 24, y: -36 },
{ x: -16, y: 8 }
]
);
case Direction.SOUTH_EAST:
break;
/** Draw a south west inner stair */
case Direction.SOUTH_WEST:
return this._drawCornerStair(
[
{ x: 0, y: 0 },
{ x: 8, y: -4 },
{ x: 16, y: 0 },
{ x: 8, y: 4 }
],
[
{ x: 0, y: 0 },
{ x: 0, y: 0 },
{ x: 8, y: 4 },
{ x: 8, y: 4 }
],
[
{ x: 16, y: 8 },
{ x: 24, y: -12 },
{ x: 0, y: -24 },
{ x: 8, y: 12 }
]
);
/** Draw a north west inner stair */
case Direction.NORTH_WEST:
return this._drawCornerStair(
[
{ x: 0, y: 0 },
{ x: 8, y: -4 },
{ x: 16, y: 0 },
{ x: 8, y: 4 }
],
[
{ x: 0, y: 0 },
{ x: 0, y: 0 },
{ x: 8, y: 4 },
{ x: 8, y: 4 }
],
[
{ x: 0, y: 16 },
{ x: -8, y: 4 },
{ x: 24, y: -36 },
{ x: 0, y: 16 }
]
);
}
}
}
/**
* Draw the stair using the given points and offsets.
*
* @param {IPosition2D[]} [points] - The point list that will be used to draw the stair.
* @param {IPosition2D[]} [offsets] - The offset list that will be used to draw the stair.
* @return {void}
* @private
*/
private _drawStair(points: IPosition2D[], offsets: IPosition2D[]): void {
for (let i = 0; i < 4; i++) {
const step = new Container();
/** Top face */
const top = new Graphics()
.beginTextureFill({
texture: this._material.texture,
color: new Color(this._material.color).premultiply(1).toNumber(),
matrix: new Matrix(1, 0.5, 1, -0.5, this._position.y % 2 === 0 ? 32 : 64, this._position.y % 2 === 0 ? 16 : 0)
})
.moveTo(points[0].x, points[0].y)
.lineTo(points[1].x, points[1].y)
.lineTo(points[2].x, points[2].y)
.lineTo(points[3].x, points[3].y)
.lineTo(points[0].x, points[0].y)
.endFill();
/** Left face */
const left = new Graphics()
.beginTextureFill({
texture: this._material.texture,
color: new Color(this._material.color).premultiply(0.8).toNumber(),
matrix: new Matrix(1, 0.5, 0, 1, 0, 0)
})
.moveTo(points[0].x, points[0].y)
.lineTo(points[0].x, points[0].y + this._thickness)
.lineTo(points[3].x, points[3].y + this._thickness)
.lineTo(points[3].x, points[3].y)
.endFill();
/** Right face */
const right = new Graphics()
.beginTextureFill({
texture: this._material.texture,
color: new Color(this._material.color).premultiply(0.71).toNumber(),
matrix: new Matrix(1, -0.5, 0, 1, 0, 0)
})
.moveTo(points[3].x, points[3].y)
.lineTo(points[3].x, points[3].y + this._thickness)
.lineTo(points[2].x, points[2].y + this._thickness)
.lineTo(points[2].x, points[2].y)
.lineTo(points[3].x, points[3].y)
.endFill();
/** And we combine everything */
step.addChild(top);
step.addChild(left);
step.addChild(right);
/** Add the offsets to the step */
step.x = offsets[0].x * i;
step.y = offsets[0].y * i;
/** Add the step to the stair */
this.addChild(step);
}
/** Positionate the stair */
this.x = 32 * this._position.x - 32 * this._position.y + offsets[1].x;
this.y = 16 * this._position.x + 16 * this._position.y - 32 * this._position.z + offsets[1].y;
/** Set the hit area */
this.hitArea = new Polygon(
new Point(0 - offsets[1].x, 0 - offsets[1].y),
new Point(32 - offsets[1].x, -16 - offsets[1].y),
new Point(64 - offsets[1].x, 0 - offsets[1].y),
new Point(32 - offsets[1].x, 16 - offsets[1].y),
new Point(0 - offsets[1].x, 0 - offsets[1].y)
);
/** Set the zIndex */
this.zIndex = ZOrder.floor(this._position);
}
/**
* Draw the corner stair using the given points, points offsets and offsets.
*
* @param {IPosition2D[]} [points] - The point list that will be used to draw the stair.
* @param {IPosition2D[]} [pointsOffsets] - The offset point list that will be used to draw the stair.
* @param {IPosition2D[]} [offsets] - The offset list that will be used to draw the stair.
* @return {void}
* @private
*/
private _drawCornerStair(points: IPosition2D[], pointsOffsets: IPosition2D[], offsets: IPosition2D[]): void {
for (let i = 0; i < 3; i++) {
const step = new Container();
/** Top face */
const top = new Graphics()
.beginTextureFill({
texture: this._material.texture,
color: new Color(this._material.color).premultiply(1).toNumber(),
matrix: new Matrix(1, 0.5, 1, -0.5, this._position.y % 2 === 0 ? 32 : 64, this._position.y % 2 === 0 ? 16 : 0)
})
.moveTo(points[0].x + -pointsOffsets[2].x * (2 - i), points[0].y + pointsOffsets[2].y * (2 - i))
.lineTo(points[1].x + pointsOffsets[1].x * (2 - i), points[1].y + pointsOffsets[1].y * (2 - i))
.lineTo(points[2].x + pointsOffsets[0].x * (2 - i), points[2].y + pointsOffsets[0].y * (2 - i))
.lineTo(points[3].x + -pointsOffsets[3].x * (2 - i), points[3].y + pointsOffsets[3].y * (2 - i))
.lineTo(points[0].x + -pointsOffsets[2].x * (2 - i), points[0].y + pointsOffsets[2].y * (2 - i))
.endFill();
/** Left face */
const left = new Graphics()
.beginTextureFill({
texture: this._material.texture,
color: new Color(this._material.color).premultiply(0.8).toNumber(),
matrix: new Matrix(1, 0.5, 0, 1, 0, 0)
})
.moveTo(points[0].x + -pointsOffsets[2].x * (2 - i), points[0].y + pointsOffsets[2].y * (2 - i))
.lineTo(
points[0].x + -pointsOffsets[2].x * (2 - i),
points[0].y + pointsOffsets[2].y * (2 - i) + this._thickness
)
.lineTo(
points[3].x + -pointsOffsets[3].x * (2 - i),
points[3].y + pointsOffsets[3].y * (2 - i) + this._thickness
)
.lineTo(points[3].x + -pointsOffsets[3].x * (2 - i), points[3].y + pointsOffsets[3].y * (2 - i))
.endFill();
/** Right face */
const right = new Graphics()
.beginTextureFill({
texture: this._material.texture,
color: new Color(this._material.color).premultiply(0.71).toNumber(),
matrix: new Matrix(1, -0.5, 0, 1, 0, 0)
})
.moveTo(points[3].x + -pointsOffsets[3].x * (2 - i), points[3].y + pointsOffsets[3].y * (2 - i))
.lineTo(
points[3].x + -pointsOffsets[3].x * (2 - i),
points[3].y + pointsOffsets[3].y * (2 - i) + this._thickness
)
.lineTo(
points[2].x + -pointsOffsets[0].x * (2 - i),
points[2].y + pointsOffsets[0].y * (2 - i) + this._thickness
)
.lineTo(points[2].x + -pointsOffsets[0].x * (2 - i), points[2].y + pointsOffsets[0].y * (2 - i))
.lineTo(points[3].x + -pointsOffsets[3].x * (2 - i), points[3].y + pointsOffsets[3].y * (2 - i))
.endFill();
/** And we combine everything */
step.addChild(top);
step.addChild(left);
step.addChild(right);
/** Add the offsets to the step */
step.x = offsets[3].x * i + offsets[1].x;
step.y = offsets[3].y * i + offsets[1].y;
/** zIndex */
if (this._type === StairType.OUTER_CORNER_STAIR) step.zIndex = -i;
if (
this._type === StairType.INNER_CORNER_STAIR ||
(this._type === StairType.OUTER_CORNER_STAIR && this._position.direction === Direction.SOUTH_WEST)
)
step.zIndex = 4 - i;
/** Add the step to the stair */
this.addChild(step);
}
for (let i = 0; i < 4; i++) {
const step = new Container();
/** Top face */
const top = new Graphics()
.beginTextureFill({
texture: this._material.texture,
color: new Color(this._material.color).premultiply(1).toNumber(),
matrix: new Matrix(1, 0.5, 1, -0.5, 0, 0)
})
.moveTo(points[0].x + pointsOffsets[0].x * (3 - i), points[0].y + pointsOffsets[0].y * (3 - i))
.lineTo(points[1].x + pointsOffsets[1].x * (3 - i), points[1].y + pointsOffsets[1].y * (3 - i))
.lineTo(points[2].x + pointsOffsets[2].x * (3 - i), points[2].y + pointsOffsets[2].y * (3 - i))
.lineTo(points[3].x + pointsOffsets[3].x * (3 - i), points[3].y + pointsOffsets[3].y * (3 - i))
.lineTo(points[0].x + pointsOffsets[0].x * (3 - i), points[0].y + pointsOffsets[0].y * (3 - i))
.endFill();
/** Left face */
const left = new Graphics()
.beginTextureFill({
texture: this._material.texture,
color: new Color(this._material.color).premultiply(0.8).toNumber(),
matrix: new Matrix(1, 0.5, 0, 1, 0, 0)
})
.moveTo(points[0].x + pointsOffsets[0].x * (3 - i), points[0].y + pointsOffsets[0].y * (3 - i))
.lineTo(
points[0].x + pointsOffsets[0].x * (3 - i),
points[0].y + pointsOffsets[0].y * (3 - i) + this._thickness
)
.lineTo(
points[3].x + pointsOffsets[3].x * (3 - i),
points[3].y + pointsOffsets[3].y * (3 - i) + this._thickness
)
.lineTo(points[3].x + pointsOffsets[3].x * (3 - i), points[3].y + pointsOffsets[3].y * (3 - i))
.endFill();
/** Right face */
const right = new Graphics()
.beginTextureFill({
texture: this._material.texture,
color: new Color(this._material.color).premultiply(0.71).toNumber(),
matrix: new Matrix(1, -0.5, 0, 1, 0, 0)
})
.moveTo(points[3].x + pointsOffsets[3].x * (3 - i), points[3].y + pointsOffsets[3].y * (3 - i))
.lineTo(
points[3].x + pointsOffsets[3].x * (3 - i),
points[3].y + pointsOffsets[3].y * (3 - i) + this._thickness
)
.lineTo(
points[2].x + pointsOffsets[2].x * (3 - i),
points[2].y + pointsOffsets[2].y * (3 - i) + this._thickness
)
.lineTo(points[2].x + pointsOffsets[2].x * (3 - i), points[2].y + pointsOffsets[2].y * (3 - i))
.lineTo(points[3].x + pointsOffsets[3].x * (3 - i), points[3].y + pointsOffsets[3].y * (3 - i))
.endFill();
/** And we combine everything */
step.addChild(top);
step.addChild(left);
step.addChild(right);
/** Add the offsets to the step */
step.x = offsets[0].x * i;
step.y = offsets[0].y * i;
/** zIndex */
if (this._type === StairType.INNER_CORNER_STAIR) step.zIndex = 3 - i;
/** Add the step to the stair */
this.addChild(step);
}
this.sortableChildren = true;
/** Positionate the stair */
this.x = 32 * this._position.x - 32 * this._position.y + offsets[2].x;
this.y = 16 * this._position.x + 16 * this._position.y - 32 * this._position.z + offsets[2].y;
/** Set the hit area */
this.hitArea = new Polygon(
new Point(0 - offsets[2].x, 0 - offsets[2].y),
new Point(32 - offsets[2].x, -16 - offsets[2].y),
new Point(64 - offsets[2].x, 0 - offsets[2].y),
new Point(32 - offsets[2].x, 16 - offsets[2].y),
new Point(0 - offsets[2].x, 0 - offsets[2].y)
);
/** Set the zIndex */
this.zIndex = ZOrder.floor(this._position);
}
}

View File

@ -0,0 +1,219 @@
import { Graphics, Matrix, Point, Polygon } from 'pixi.js';
import { Color } from '@pixi/color';
import type { Room } from '../Room';
import type { ITileConfiguration, ITileInfo } from '../../../types/Room';
import type { Material } from '../materials/Material';
import { FloorMaterial } from '../materials/FloorMaterial';
import { ZOrder } from '../../../utilities/ZOrder';
import { RoomPart } from './RoomPart';
import type { Dimension } from '../../../types/Dimension';
/**
* Tile class that show up during room rendering.
*
* @class
* @memberof Scuti
*/
export class Tile extends RoomPart {
/**
* The ITileInfo instance
*
* @member {ITileInfo}
* @private
*/
private readonly _tileInfo?: ITileInfo;
/**
* The thickness of the tile part.
*
* @member {number}
* @private
*/
private _thickness: number;
/**
* The tile material that will be applied to this part, it contains the color and the texture of the tile.
*
* @member {Material}
* @private
*/
private _material: Material;
/**
* The tile position.
*
* @member {IPosition3D}
* @private
*/
readonly _position: Dimension.IPosition3D;
/**
* @param {Room} [room] - The room instance where the tile will be drawn.
* @param {ITileConfiguration} [configuration] - The tile configuration.
* @param {Material} [configuration.material] - The time material that will be applied.
* @param {number} [configuration.thickness] - The tile thickness.
* @param {IPosition3D} [configuration.position] - The tile position.
**/
constructor(room: Room, configuration: ITileConfiguration, tileInfo?: ITileInfo) {
super(room);
/** Store the configuration */
this.room = room;
this._tileInfo = tileInfo;
this._position = configuration.position;
this._thickness = configuration.thickness ?? 8;
this._material = configuration.material ?? new FloorMaterial(this.room.engine, 111);
/** Register interactions */
this.registerInteractions(this._position);
// TODO: Make the method public and use it when adding it to a room, not when instancing the class
/** Draw the tile */
this._draw();
}
/**
* Draw the tile.
*
* @return {void}
* @private
*/
private _draw(): void {
/** Top face */
const top = new Graphics()
.beginTextureFill({
texture: this._material.texture,
color: new Color(this._material.color).premultiply(1).toNumber(),
//matrix: new Matrix(1, 0.5, 1, -0.5, (this._position.x % 2 === 0 || this._position.y % 2 === 0) && !(this._position.x % 2 === 0 && this._position.y % 2 === 0) ? 32 : 0, (this._position.x % 2 === 0 || this._position.y % 2 === 0) && !(this._position.x % 2 === 0 && this._position.y % 2 === 0) ? 16 : 0)
matrix: new Matrix(1, 0.5, 1, -0.5, this._position.y % 2 === 0 ? 32 : 64, this._position.y % 2 === 0 ? 16 : 0)
})
.moveTo(0, 0)
.lineTo(32, -16)
.lineTo(64, 0)
.lineTo(32, 16)
.lineTo(0, 0)
.endFill();
this.addChild(top);
let bottomTile;
let rightTile;
if (
this.room.tileMap.tileMap[this._position.y + 1] !== undefined &&
this.room.tileMap.tileMap[this._position.y + 1][this._position.x] !== undefined
) {
bottomTile = this.room.tileMap.getTileInfo({ x: this._position.x, y: this._position.y + 1 });
}
if (this.room.tileMap.tileMap[this._position.y][this._position.x + 1] !== undefined) {
rightTile = this.room.tileMap.getTileInfo({ x: this._position.x + 1, y: this._position.y });
}
if (
!Boolean(bottomTile) ||
Boolean(bottomTile?.stairType) ||
!Boolean(bottomTile?.tile) ||
(this._tileInfo != null && bottomTile?.height !== this._tileInfo.height)
) {
/** Left face */
const left = new Graphics()
.beginTextureFill({
texture: this._material.texture,
color: new Color(this._material.color).premultiply(0.8).toNumber(),
matrix: new Matrix(1, 0.5, 0, 1, 0, 0)
})
.moveTo(0, 0)
.lineTo(0, this._thickness)
.lineTo(32, 16 + this._thickness)
.lineTo(32, 16)
.endFill();
this.addChild(left);
}
if (
rightTile == null ||
rightTile.stairType != null ||
!rightTile.tile ||
(this._tileInfo != null && rightTile.height !== this._tileInfo.height)
) {
/** Right face */
const right = new Graphics()
.beginTextureFill({
texture: this._material.texture,
color: new Color(this._material.color).premultiply(0.71).toNumber(),
matrix: new Matrix(1, -0.5, 0, 1, 0, 0)
})
.moveTo(32, 16)
.lineTo(32, 16 + this._thickness)
.lineTo(64, this._thickness)
.lineTo(64, 0)
.lineTo(32, 16)
.endFill();
this.addChild(right);
}
/** Positionate the wall */
this.x = 32 * this._position.x - 32 * this._position.y;
this.y = 16 * this._position.x + 16 * this._position.y - 32 * this._position.z;
/** Set the hit area */
this.hitArea = new Polygon(
new Point(0, 0),
new Point(32, -16),
new Point(64, 0),
new Point(32, 16),
new Point(0, 0)
);
/** Set the zIndex */
this.zIndex = ZOrder.floor(this._position);
}
/**
* Reference to the tile thickness.
*
* @member {number}
* @readonly
* @public
*/
public get thickness(): number {
return this._thickness;
}
/**
* Update the tile thickness and redraw the tile.
*
* @param {number} [thickness] - The room tile thickness that will be applied.
* @public
*/
public set thickness(thickness: number) {
this._thickness = thickness;
/** Redraw the tile */
this._draw();
}
/**
* Reference to the tile material instance.
*
* @member {Material}
* @readonly
* @public
*/
public get material(): Material {
return this._material;
}
/**
* Update the tile material and redraw the tile.
*
* @param {Material} [material] - The room tile material that will be applied.
* @public
*/
public set material(material: Material) {
this._material = material;
/** Redraw the tile */
this._draw();
}
}

View File

@ -0,0 +1,314 @@
import { Graphics, Matrix, Texture } from 'pixi.js';
import { Color } from '@pixi/color';
import type { Room } from '../Room';
import type { IPosition3D, IPosition2D, IWallConfiguration } from '../../../types/Room';
import type { Material } from '../materials/Material';
import { WallType } from '../../../enums/WallType';
import { WallMaterial } from '../materials/WallMaterial';
import { ZOrder } from '../../../utilities/ZOrder';
import { RoomPart } from './RoomPart';
/**
* Wall class that show up on the sides of the tiles.
*
* @class
* @memberof Scuti
*/
export class Wall extends RoomPart {
/**
* The thickness of the wall part.
*
* @member {number}
* @private
*/
private readonly _thickness: number;
/**
* The wall material that will be applied to this part, it contains the color and the texture of the wall.
*
* @member {Material}
* @private
*/
private readonly _material: Material;
/**
* The wall position.
*
* @member {IPosition3D}
* @private
*/
readonly _position: IPosition3D;
/**
* The wall type.
*
* @member {WallType}
* @private
*/
private readonly _type: WallType;
/**
* @param {Room} [room] - The room instance where the wall will be drawn.
* @param {IWallConfiguration} [configuration] - The wall configuration.
* @param {Material} [configuration.material] - The wall material that will be applied.
* @param {number} [configuration.thickness] - The wall thickness.
* @param {number} [configuration.height] - The wall height.
* @param {IPosition3D} [configuration.position] - The wall position.
* @param {WallType} [configuration.type] - The wall type.
* @param {boolean} [configuration.door] - Is it a door wall?
**/
constructor(room: Room, configuration: IWallConfiguration) {
super(room);
/** Store the configuration */
this.room = room;
this._position = configuration.position;
this._thickness = configuration.thickness ?? 8;
this._material = configuration.material ?? new WallMaterial(this.room.engine);
this._height = configuration.height ?? 0;
this._type = configuration.type;
/** Draw the wall */
this._draw();
}
/**
* Select which wall should be drawn by it's type.
*
* @return {void}
* @private
*/
private _draw(): void {
if (this._type === WallType.LEFT_WALL) {
/** Draw a left wall */
this._drawWall([
{
x: -this._thickness,
y: -this._thickness / 2 + this._position.z * 32 - this.room.tileMap.maxZ * 32 - 115 - this._height * 64
},
{
x: -this._thickness + 32,
y: -this._thickness / 2 + this._position.z * 32 - this.room.tileMap.maxZ * 32 - 131 - this._height * 64
},
{
x: -this._thickness + 32 + this._thickness,
y:
-this._thickness / 2 +
this._position.z * 32 -
this.room.tileMap.maxZ * 32 -
131 +
this._thickness / 2 -
this._height * 64
},
{
x: -this._thickness + this._thickness,
y:
-this._thickness / 2 +
this._position.z * 32 -
this.room.tileMap.maxZ * 32 -
115 +
this._thickness / 2 -
this._height * 64
}
]);
} else if (this._type === WallType.RIGHT_WALL) {
/** Draw a right wall */
this._drawWall([
{
x: 32,
y: -16 + this._position.z * 32 - this.room.tileMap.maxZ * 32 - 115 - this._height * 64
},
{
x: 32 + this._thickness,
y: -16 + this._position.z * 32 - this.room.tileMap.maxZ * 32 - 115 - this._thickness / 2 - this._height * 64
},
{
x: 64 + this._thickness,
y: -16 + this._position.z * 32 - this.room.tileMap.maxZ * 32 - 99 - this._thickness / 2 - this._height * 64
},
{
x: 64,
y: -16 + this._position.z * 32 - this.room.tileMap.maxZ * 32 - 99 - this._height * 64
}
]);
} else if (this._type === WallType.CORNER_WALL) {
/** Draw a corner wall */
this._drawWall([
{
x: 32 - this._thickness,
y: -16 + this._position.z * 32 - this.room.tileMap.maxZ * 32 - 115 - this._thickness / 2 - this._height * 64
},
{
x: 32,
y:
-16 +
this._position.z * 32 -
this.room.tileMap.maxZ * 32 -
115 -
2 * (this._thickness / 2) -
this._height * 64
},
{
x: 32 + this._thickness,
y: -16 + this._position.z * 32 - this.room.tileMap.maxZ * 32 - 115 - this._thickness / 2 - this._height * 64
},
{
x: 32,
y: -16 + this._position.z * 32 - this.room.tileMap.maxZ * 32 - 115 - this._height * 64
}
]);
} else if (this._type === WallType.DOOR_WALL) {
/** Draw a door wall */
this._drawWall([
{
x: -this._thickness + 32,
y: -this._thickness / 2 + this._position.z * 32 - this.room.tileMap.maxZ * 32 - 99 - this._height * 64
},
{
x: -this._thickness + 64,
y: -this._thickness / 2 + this._position.z * 32 - this.room.tileMap.maxZ * 32 - 115 - this._height * 64
},
{
x: -this._thickness + 64 + this._thickness,
y:
-this._thickness / 2 +
this._position.z * 32 -
this.room.tileMap.maxZ * 32 -
115 +
this._thickness / 2 -
this._height * 64
},
{
x: -this._thickness + 32 + this._thickness,
y:
-this._thickness / 2 +
this._position.z * 32 -
this.room.tileMap.maxZ * 32 -
99 +
this._thickness / 2 -
this._height * 64
}
]);
}
}
/**
* Draw the wall using the given points.
*
* @param {IPosition2D[]} [points] - The point list that will be used to draw the wall.
* @return {void}
* @private
*/
private _drawWall(points: IPosition2D[]): void {
/** Top face */
const top = new Graphics()
.beginTextureFill({
texture: Texture.WHITE,
color: new Color(this._material.color).premultiply(0.61).toNumber()
})
.moveTo(points[0].x, points[0].y)
.lineTo(points[1].x, points[1].y)
.lineTo(points[2].x, points[2].y)
.lineTo(points[3].x, points[3].y)
.lineTo(points[0].x, points[0].y)
.endFill();
/** Left face */
const left = new Graphics()
.beginTextureFill({
texture: this._type === WallType.RIGHT_WALL ? this._material.texture : Texture.WHITE,
color: new Color(this._material.color).premultiply(1).toNumber(),
matrix: new Matrix(1, 0.5, 0, 1, points[0].x, points[0].y)
})
.moveTo(points[0].x, points[0].y)
.lineTo(
points[0].x,
points[0].y +
115 +
this.room.floorThickness +
this.room.tileMap.maxZ * 32 -
this._position.z * 32 +
this._height * 64
)
.lineTo(
points[3].x,
points[3].y +
115 +
this.room.floorThickness +
this.room.tileMap.maxZ * 32 -
this._position.z * 32 +
this._height * 64
)
.lineTo(points[3].x, points[3].y)
.endFill();
/** Right face */
const right = new Graphics()
.beginTextureFill({
texture:
this._type === WallType.LEFT_WALL || this._type === WallType.DOOR_WALL
? this._material.texture
: Texture.WHITE,
color: new Color(this._material.color).premultiply(0.8).toNumber(),
matrix: new Matrix(1, -0.5, 0, 1, points[0].x + this._thickness, points[0].y + 4)
})
.moveTo(points[3].x, points[3].y);
if (this._type === WallType.DOOR_WALL) {
right
.lineTo(
points[3].x,
points[3].y +
22 +
this.room.floorThickness +
this.room.tileMap.maxZ * 32 -
this._position.z * 32 +
this._height * 64
)
.lineTo(
points[2].x,
points[2].y +
22 +
this.room.floorThickness +
this.room.tileMap.maxZ * 32 -
this._position.z * 32 +
this._height * 64
);
} else {
right
.lineTo(
points[3].x,
points[3].y +
115 +
this.room.floorThickness +
this.room.tileMap.maxZ * 32 -
this._position.z * 32 +
this._height * 64
)
.lineTo(
points[2].x,
points[2].y +
115 +
this.room.floorThickness +
this.room.tileMap.maxZ * 32 -
this._position.z * 32 +
this._height * 64
);
}
right.lineTo(points[2].x, points[2].y).lineTo(points[3].x, points[3].y).endFill();
/** And we combine everything */
this.addChild(top);
this.addChild(left);
this.addChild(right);
/** Positionate the wall */
this.x = 32 * this._position.x - 32 * this._position.y;
this.y = 16 * this._position.x + 16 * this._position.y - 32 * this._position.z;
/** Set the zIndex */
this.zIndex = ZOrder.wall(this._position);
}
}

View File

@ -0,0 +1,4 @@
export * from './Cursor';
export * from './Stair';
export * from './Tile';
export * from './Wall';

93
src/types/Avatar.d.ts vendored Normal file
View File

@ -0,0 +1,93 @@
import type { Direction } from '../enums/Direction';
import type { AvatarAction } from '../objects/avatars/actions/AvatarAction';
import type { Dimension } from './Dimension';
import type { IRoomObjectConfig } from './Room';
export type IAvatarPosition = Dimension.IPosition3D;
export type Figure = Map<string, { setId: number; colors: number[] }>;
export interface IAvatarConfig extends IRoomObjectConfig {
figure: string;
bodyDirection: Direction;
headDirection: Direction;
actions: AvatarAction[];
handItem?: number;
}
export interface IAvatarPart {
colorable: number;
colorindex: number;
id: number;
index: number;
lib: { id: string; revision: string };
type: string;
}
export interface IActionDefinition {
state: string;
precedence: string;
main: string;
geometrytype: string;
activepartset: string;
assetpartdefinition: string;
prevents: string;
params: string[];
}
export interface IAnimationDefinition {
desc: string;
frames: Array<{
bodyparts: object;
}>;
}
export interface IAnimationFrameData {
assetpartdefinition: string;
repeats: number;
frame: number;
}
export interface IBodyPartConfiguration {
type: string;
setId: number;
colors: number[];
parts: IAvatarPart[];
actions: AvatarAction[];
}
export interface IAvatarPartSets {
partSets: object;
activePartSets: {
figure: string[];
head: string[];
speak: string[];
gesture: string[];
eye: string[];
handRight: string[];
handRightAndHead: string[];
handLeft: string[];
walk: string[];
sit: string[];
itemRight: string[];
swim: string[];
};
}
export interface IAvatarLayerConfiguration {
type: string;
part: IAvatarPart;
gesture: string;
tint?: number;
z: number;
flip: boolean;
direction: Direction;
frame: number;
alpha?: number;
}
export interface LayerFrame {
action: AvatarAction;
frame: number;
repeat: number;
}

14
src/types/Configuration.d.ts vendored Normal file
View File

@ -0,0 +1,14 @@
import type { Spritesheet } from 'pixi.js';
export interface IRendererConfiguration {
canvas: HTMLElement;
width: number;
height: number;
resources: string;
}
export interface ScutiSpritesheet extends Omit<Spritesheet, 'data'> {
data: {
partsType: { [key: string]: { gestures: string[] } };
};
}

15
src/types/Dimension.d.ts vendored Normal file
View File

@ -0,0 +1,15 @@
export namespace Dimension {
export type IPosition2D = Omit<IPosition3D, 'z'>;
export interface IPosition3D {
x: number;
y: number;
z: number;
}
export type IOffsets2D = Omit<IOffsets3D, 'offsetZ'>;
export interface IOffsets3D {
offsetX: number;
offsetY: number;
offsetZ: number;
}
}

52
src/types/Figure.d.ts vendored Normal file
View File

@ -0,0 +1,52 @@
export interface IFigureData {
palette: Record<string, Record<string, IFigureDataColor>>;
settype: Record<string, IFigureDataSetType>;
}
export interface IFigureDataPalette {
id: number;
color: IFigureDataColor[];
}
export interface IFigureMap {
libs: Array<{ id: string; revision: string }>;
parts: Record<string, Record<string, number>>;
}
export interface IFigureDataSetType {
type: string;
paletteid: string;
mand_f_0: boolean; // has been changed to boolean, can be either 1, 0
mand_f_1: boolean; // has been changed to boolean, can be either 1, 0
mand_m_0: boolean; // has been changed to boolean, can be either 1, 0
mand_m_1: boolean; // has been changed to boolean, can be either 1, 0
set: Record<string, IFigureDataSet>;
}
export interface IFigureDataColor {
id: number;
index: number;
club: number; // must be changed to something, either 0, 1, 2
selectable: boolean; // has been changed to boolean, can be either 1, 0
hexCode: string;
}
export interface IFigureDataSet {
id: number;
gender: 'M' | 'F' | 'U'; // has been changed
club: number; // must be changed to something, either 0, 1, 2
colorable: boolean; // has been changed to boolean, can be either 1, 0
selectable: boolean; // has been changed to boolean, can be either 1, 0
preselectable: boolean; // has been changed to boolean, can be either 1, 0
sellable?: boolean; // has been changed to boolean, can be either 1, 0, null
parts: IFigureDataPart[];
hiddenLayers: Array<{ partType: string }>; // !! can be empty
}
export interface IFigureDataPart {
id: number;
type: string; // must be changed (i guess)
colorable: boolean; // has been changed to boolean, can be either 1, 0
index: number;
colorindex: number;
}

100
src/types/Furniture.d.ts vendored Normal file
View File

@ -0,0 +1,100 @@
import type { BLEND_MODES } from 'pixi.js';
import type { Direction } from '../enums/Direction';
import type { Dimension } from './Dimension';
import type { IRoomObjectConfig } from './Room';
export type IFloorPosition = Dimension.IPosition3D;
export type IWallPosition = Dimension.IPosition2D & Dimension.IOffsets2D;
export interface IFloorFurnitureConfiguration {
id: number;
position: IFloorPosition;
direction: Direction;
state?: number;
}
export interface IWallFurniConfig extends IRoomObjectConfig {
id: number;
state?: number;
}
export interface IFurnitureData {
floorItems: IFloorItem[];
wallItems: IWallItem[];
}
export interface ISharedFurniData {
id: number;
className: string;
}
export interface IFloorItem extends ISharedFurniData {
name: string;
description: string;
furniLine: string;
offerId: number;
adUrl: string;
excludeDynamic: false;
specialType: number;
customParams: null;
}
export interface IWallItem extends ISharedFurniData {
name: string;
description: string;
furniLine: string;
offerId: number;
adUrl: string;
excludeDynamic: boolean;
specialType: number;
customParams: string;
dimensions: {
x: number;
y: number;
defaultDirection: number;
};
canStandOn: boolean;
canSitOn: boolean;
canLayOn: boolean;
}
export interface IFurnitureProperty {
dimensions: Dimension.IPosition2D; // not sure { x: .., y: .. }
infos: { logic: string; visualization: string };
visualization: IFurnitureVisualization;
}
export interface IFurnitureVisualization {
layerCount: number;
directions: Direction[];
colors: Record<string, Record<string, `#${string}`>>;
layers: Array<{ z: number; ignoreMouse: boolean; tag: string; alpha: number; ink: keyof typeof BLEND_MODES }>; // wrong types here
animation: Record<string, Record<string, { frameSequence: number[] }>>;
}
export interface IFurnitureLayerConfiguration {
layer: number | string;
alpha: number;
tint?: number | undefined;
z: number;
blendMode: BLEND_MODES;
flip: boolean;
frame: number;
ignoreMouse: boolean;
direction: Direction;
tag?: string;
}
export interface IFurnitureLayerData {
layer: number | string;
alpha: number;
tint?: number | undefined;
z: number;
blendMode: BLEND_MODES;
flip: boolean;
frame: number;
ignoreMouse: boolean;
direction: Direction;
tag?: string;
}

1
src/types/Global.d.ts vendored Normal file
View File

@ -0,0 +1 @@
export type Nullable<T> = T | undefined | null;

10
src/types/Interaction.d.ts vendored Normal file
View File

@ -0,0 +1,10 @@
import type { FederatedPointerEvent } from 'pixi.js';
import type { IAvatarPosition } from './Avatar';
import type { IFloorPosition, IWallPosition } from './Furniture';
export interface IInteractionEvent {
tag?: string;
event: FederatedPointerEvent;
position?: IWallPosition | IFloorPosition | IAvatarPosition;
}

108
src/types/Room.d.ts vendored Normal file
View File

@ -0,0 +1,108 @@
import type { StairType } from '../enums/StairType';
import type { Direction } from '../enums/Direction';
import type { WallType } from '../enums/WallType';
import type { Material } from '../objects/rooms/materials/Material';
import type { IFloorPosition, IWallPosition } from './Furniture';
import type { IAvatarPosition } from './Avatar';
export type TileMap = string[][];
export interface IRoomConfig {
tileMap: string;
floorMaterial?: Material;
floorThickness?: number;
wallMaterial?: Material;
wallHeight?: number;
wallThickness?: number;
}
export interface IRoomObjectConfig {
position: IWallPosition | IFloorPosition | IAvatarPosition;
direction: Direction;
}
export interface ITileConfiguration {
material?: Material;
thickness?: number;
position: IPosition3D;
}
export interface IStairConfiguration {
material?: Material;
thickness?: number;
type: StairType;
position: IPosition3D;
}
export interface IWallConfiguration {
material?: Material;
thickness?: number;
height?: number;
position: IPosition3D;
type: WallType;
door?: boolean;
}
export interface ICursorConfiguration {
position: IPosition3D;
}
export interface IPosition3D {
x: number;
y: number;
z: number;
direction?: number;
}
export interface IPosition2D {
x: number;
y: number;
}
export interface ITileInfo {
tile: boolean;
door: boolean;
height: number;
stairType?: { type: StairType; direction: Direction };
wallType?: WallType;
}
// missing types here
export interface RoomMaterial {
assets: { x: string; y: string; source?: string; flipH?: boolean };
floorData: {
floors: Array<{
id: string;
visualizations: Array<{ size: number; layers: Array<{ color: number; materialId: string }> }>;
}>;
};
materials: Array<{
id: string;
matrices: Array<{ columns: Array<{ width: number; cells: Array<{ textureId: string }> }> }>;
}>;
textures: Array<{ id: string; bitmaps: Array<{ assetName: string }> }>;
wallData: Array<{
materials: Array<{
id: string;
matrices: Array<{ columns: Array<{ width: number; cells: Array<{ textureId: string }> }> }>;
}>;
textures: Array<{ id: string; bitmaps: Array<{ assetName: string }> }>;
walls: Array<{
id: string;
visualizations: Array<{ size: number; layers: Array<{ color: number; materialId: string }> }>;
}>;
}>;
landscapeData: object;
// ...
visualizationType: string;
type: string;
logicType: string;
spritesheet: string;
name: string;
maskData: {
masks: Array<{
id: string;
visualizations: Array<{ size: number; layers: Array<{ color: number; materialId: string }> }>;
}>;
};
}

54
src/types/RoomMaterial.d.ts vendored Normal file
View File

@ -0,0 +1,54 @@
export interface RoomMaterial {
type: string;
name: string;
visualizationType: string;
logicType: string;
spritesheet: string;
assets: Record<string, Assets>;
wallData: WallData;
floorData: FloorData;
landscapeData: LandscapeData;
maskData: MaskData;
}
export interface Assets {
x: number;
y: number;
source?: string;
flipH?: boolean;
}
export interface FloorDataMaterial {
id: string;
matrices: PurpleMatrix[];
}
export interface PurpleMatrix {
columns: PurpleColumn[];
}
export interface FloorData {
floors: Floor[];
materials: FloorDataMaterial[];
textures: Texture[];
}
export interface WallData {
walls: Floor[];
materials: FloorDataMaterial[];
textures: Texture[];
}
export interface Floor {
id: string;
visualizations: FloorVisualization[];
}
export interface Texture {
id: string;
bitmaps: Bitmap[];
}
export interface Bitmap {
assetName: string;
}

8
src/types/index.d.ts vendored Normal file
View File

@ -0,0 +1,8 @@
export * from './Avatar';
export * from './Configuration';
export * from './Dimension';
export * from './Figure';
export * from './Furniture';
export * from './Global';
export * from './Interaction';
export * from './Room';

View File

@ -0,0 +1,26 @@
import { Assets, Cache } from 'pixi.js';
const domain = 'http://localhost:8081/';
interface LoadedKeys {
[key: string]: Promise<any>;
}
const loadedKeys: LoadedKeys = {};
const load = async (key: string, url: string, onUncached?: () => void): Promise<void> => {
if (loadedKeys[key] !== undefined) return await loadedKeys[key];
if (!Cache.has(key)) {
if (onUncached != null) onUncached();
Assets.add(key, AssetLoader.domain + url);
loadedKeys[key] = Assets.load(key);
await loadedKeys[key];
}
};
/**
* Loads assets from a certain domain
*
* @memberof Scuti
*/
export const AssetLoader = { domain, load };

107
src/utilities/Logger.ts Normal file
View File

@ -0,0 +1,107 @@
/**
* Material class that regroup the methods to make beautiful logs.
*
* @class
* @memberof Scuti
*/
export class Logger {
/**
* The logger name (the name shown on the left of the log).
*
* @member {string}
* @private
*/
private readonly _name: string;
/**
* @param {string} [name] - The logger name.
*/
constructor(name: string) {
this._name = name;
}
/**
* Send a normal log message into the console.
*
* @param {string} [message] - The message that will be logged into the console.
* @return {void}
* @public
*/
public log(message: string): void {
this._log('#078000', '#FFFFFF', message);
}
/**
* Send an error log message in the console.
*
* @param {string} [message] - The message that will be logged into the console.
* @return {void}
* @public
*/
public error(message: string): void {
this._log('#E86C5D', '#FFFFFF', message);
}
/**
* Send a warning log message in the console.
*
* @param {string} [message] - The message that will be logged into the console.
* @return {void}
* @public
*/
public warn(message: string): void {
this._log('#FFD100', '#000000', message);
}
/**
* Send an info log message in the console.
*
* @param {string} [message] - The message that will be logged into the console.
* @return {void}
* @public
*/
public info(message: string): void {
this._log('#EC3262', '#FFFFFF', message);
}
/**
* Send a stylized message in the console.
*
* @param {string} [backgroundColor] - The color of the background of the message.
* @param {string} [textColor] - The color of the message.
* @param {string} [message] - The message that will be logged into the console.
* @return {void}
* @public
*/
private _log(backgroundColor: string, textColor: string, message: string): void {
console.log(
`%c ${this.time} %c ${this._name} %c ${message} `,
`background: #FFFFFF; color: #000000;`,
`background: #000000; color: #FFFFFF`,
`background: ${backgroundColor}; color: ${textColor};`
);
}
/**
* Reference of the current time of the day formatted in a string.
*
* @member {string}
* @readonly
* @public
*/
public get time(): string {
const date = new Date();
return String(date.getHours()) + ':' + String(date.getMinutes()) + ':' + String(date.getSeconds());
}
/**
* Reference of the logger name.
*
* @member {string}
* @readonly
* @public
*/
public get name(): string {
return this._name;
}
}

102
src/utilities/ZOrder.ts Normal file
View File

@ -0,0 +1,102 @@
import type { IPosition2D, IPosition3D } from '../types/Room';
import type { IFloorPosition, IWallPosition } from '../types/Furniture';
/**
* Priority values
*/
const PRIORITY_WALL = 6;
const PRIORITY_FLOOR = 7;
const PRIORITY_TILE_CURSOR = 11;
const PRIORITY_ROOM_AVATAR = 11;
const PRIORITY_ROOM_ITEM = 11;
const PRIORITY_WALL_ITEM = 9;
const PRIORITY_MULTIPLIER = 10000000;
/**
* Comparable values
*/
const COMPARABLE_X_Y = 1000000;
const COMPARABLE_Z = 10000;
/**
* Return the zOrder of an avatar by it's position in the room.
*
* @param {IPosition3D} [position] - The avatar position in the room.
* @return {number}
* @private
*/
const avatar = function (position: IPosition3D, z: number): number {
return (
(Math.floor(position.x) + Math.floor(position.y)) * COMPARABLE_X_Y +
(position.z + 0.001 * COMPARABLE_Z) +
PRIORITY_MULTIPLIER * PRIORITY_ROOM_AVATAR +
z
);
};
/**
* Return the zOrder of the floor by it's position in the room.
*
* @param {IPosition2D} [position] - The floor position in the room.
* @return {number}
* @private
*/
const floor = function (position: IPosition2D): number {
return (position.x + position.y) * COMPARABLE_X_Y + PRIORITY_MULTIPLIER * PRIORITY_WALL;
};
/**
* Return the zOrder of the wall by it's position in the room.
*
* @param {IPosition2D} [position] - The wall position in the room.
* @return {number}
* @private
*/
const wall = function (position: IPosition2D): number {
return (position.x + position.y) * COMPARABLE_X_Y + PRIORITY_MULTIPLIER * PRIORITY_WALL;
};
/**
* Return the zOrder of the tile cursor by it's position in the room.
*
* @param {IPosition2D} [position] - The tile cursor position in the room.
* @return {number}
* @private
*/
const tileCursor = function (position: IPosition2D): number {
return (position.x + position.y) * COMPARABLE_X_Y + PRIORITY_MULTIPLIER * PRIORITY_TILE_CURSOR;
};
/**
* Return the zOrder of a floor item by it's position in the room.
*
* @param {IFloorPosition} [position] - The floor item position in the room.
* @param {number} [z] - The z value of the layer.
* @return {number}
* @private
*/
const floorItem = function (position: IFloorPosition, z: number): number {
const compareY = Math.trunc(z / 100) / 10;
return (
(position.x + position.y + compareY) * COMPARABLE_X_Y +
position.z * COMPARABLE_Z +
PRIORITY_MULTIPLIER * PRIORITY_ROOM_ITEM
);
};
/**
* Return the zOrder of a wall item by it's position in the room.
*
* @param {IPosition3D} [position] - The wall item position in the room.
* @param {number} [z] - The z value of the layer.
* @return {number}
* @private
*/
const wallItem = function (position: IWallPosition, z: number): number {
return (position.x + position.y) * COMPARABLE_X_Y + z * COMPARABLE_Z + PRIORITY_MULTIPLIER * PRIORITY_WALL_ITEM;
};
/**
* ZOrder variable that manage the z ordering of room objects.
*/
export const ZOrder = { avatar, tileCursor, floorItem, wallItem, floor, wall };

21
tsconfig.json Normal file
View File

@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ESNext",
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"moduleResolution": "node",
"declaration": true,
"declarationDir": "dist/types",
"noImplicitAny": true,
"allowJs": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"esModuleInterop": true,
"sourceMap": false,
"removeComments": true
},
"include": ["src", "public"]
}