├── .gitignore ├── .parcelrc ├── .terserrc ├── babel.config.js ├── docs ├── .nojekyll ├── assets │ ├── highlight.css │ ├── icons.css │ ├── icons.png │ ├── icons@2x.png │ ├── main.js │ ├── search.js │ ├── style.css │ ├── widgets.png │ └── widgets@2x.png ├── classes │ ├── feature.Feature.html │ ├── gui.default.html │ ├── lib_guiControl.guiControl.html │ ├── parcel.Space.html │ ├── parcel.default.html │ └── player.Player.html ├── enums │ └── lib_messages.SupportedMessageTypes.html ├── index.html ├── interfaces │ ├── lib_messages.ChangedMessage.html │ ├── lib_messages.ChatMessage.html │ ├── lib_messages.ClickMessage.html │ ├── lib_messages.JoinMessage.html │ ├── lib_messages.KeysMessage.html │ ├── lib_messages.MoveMessage.html │ ├── lib_messages.PatchMessage.html │ ├── lib_messages.PlayerAwayMessage.html │ ├── lib_messages.PlayerEnterMessage.html │ ├── lib_messages.PlayerLeaveMessage.html │ ├── lib_messages.PlayerNearbyMessage.html │ ├── lib_messages.StartMessage.html │ ├── lib_messages.StopMessage.html │ ├── lib_messages.TriggerMessage.html │ └── lib_types.IParcel.html ├── modules.html └── modules │ ├── feature.html │ ├── gui.html │ ├── helpers.html │ ├── index.html │ ├── lib_guiControl.html │ ├── lib_messages.html │ ├── lib_types.html │ ├── lib_validation_helpers.html │ ├── parcel.html │ ├── player.html │ └── types.html ├── jest.config.js ├── package-lock.json ├── package.json ├── readme.md ├── src ├── feature.ts ├── gui.ts ├── helpers.ts ├── index.ts ├── lib │ ├── guiControl.ts │ ├── messages.ts │ ├── types.ts │ └── validation-helpers.ts ├── parcel.ts ├── player.ts └── types.ts ├── test ├── feature.test.ts ├── parcel.json ├── parcel.test.ts ├── player.test.ts ├── test_lib.ts └── tsconfig.json ├── tsconfig.json ├── tsconfig.test.json └── typedoc.json /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | bundle.js* 3 | bundle.min.js 4 | node_modules 5 | dist 6 | .parcel-cache -------------------------------------------------------------------------------- /.parcelrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@parcel/config-default", 3 | "transformers": { 4 | "types:*.{ts,tsx}": ["@parcel/transformer-typescript-types"], 5 | "bundle-text:*": ["...", "@parcel/transformer-inline-string"], 6 | "worklet:*.{js,mjs,jsm,jsx,es6,cjs,ts,tsx}": [ 7 | "@parcel/transformer-worklet", 8 | "..." 9 | ], 10 | "*.{js,mjs,jsm,jsx,es6,cjs,ts,tsx}": [ 11 | "@parcel/transformer-js", 12 | "@parcel/transformer-react-refresh-wrap" 13 | ] 14 | } 15 | } -------------------------------------------------------------------------------- /.terserrc: -------------------------------------------------------------------------------- 1 | { 2 | keep_classnames: true, 3 | keep_fnames: true 4 | } -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', 5 | { 6 | targets: { node: 'current' }, 7 | }, 8 | ], 9 | '@babel/preset-typescript', 10 | ] 11 | } -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- 1 | TypeDoc added this file to prevent GitHub Pages from using Jekyll. You can turn off this behavior by setting the `githubPages` option to false. -------------------------------------------------------------------------------- /docs/assets/highlight.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --light-hl-0: #0000FF; 3 | --dark-hl-0: #569CD6; 4 | --light-hl-1: #000000; 5 | --dark-hl-1: #D4D4D4; 6 | --light-hl-2: #795E26; 7 | --dark-hl-2: #DCDCAA; 8 | --light-hl-3: #001080; 9 | --dark-hl-3: #9CDCFE; 10 | --light-hl-4: #098658; 11 | --dark-hl-4: #B5CEA8; 12 | --light-hl-5: #A31515; 13 | --dark-hl-5: #CE9178; 14 | --light-code-background: #F5F5F5; 15 | --dark-code-background: #1E1E1E; 16 | } 17 | 18 | @media (prefers-color-scheme: light) { :root { 19 | --hl-0: var(--light-hl-0); 20 | --hl-1: var(--light-hl-1); 21 | --hl-2: var(--light-hl-2); 22 | --hl-3: var(--light-hl-3); 23 | --hl-4: var(--light-hl-4); 24 | --hl-5: var(--light-hl-5); 25 | --code-background: var(--light-code-background); 26 | } } 27 | 28 | @media (prefers-color-scheme: dark) { :root { 29 | --hl-0: var(--dark-hl-0); 30 | --hl-1: var(--dark-hl-1); 31 | --hl-2: var(--dark-hl-2); 32 | --hl-3: var(--dark-hl-3); 33 | --hl-4: var(--dark-hl-4); 34 | --hl-5: var(--dark-hl-5); 35 | --code-background: var(--dark-code-background); 36 | } } 37 | 38 | body.light { 39 | --hl-0: var(--light-hl-0); 40 | --hl-1: var(--light-hl-1); 41 | --hl-2: var(--light-hl-2); 42 | --hl-3: var(--light-hl-3); 43 | --hl-4: var(--light-hl-4); 44 | --hl-5: var(--light-hl-5); 45 | --code-background: var(--light-code-background); 46 | } 47 | 48 | body.dark { 49 | --hl-0: var(--dark-hl-0); 50 | --hl-1: var(--dark-hl-1); 51 | --hl-2: var(--dark-hl-2); 52 | --hl-3: var(--dark-hl-3); 53 | --hl-4: var(--dark-hl-4); 54 | --hl-5: var(--dark-hl-5); 55 | --code-background: var(--dark-code-background); 56 | } 57 | 58 | .hl-0 { color: var(--hl-0); } 59 | .hl-1 { color: var(--hl-1); } 60 | .hl-2 { color: var(--hl-2); } 61 | .hl-3 { color: var(--hl-3); } 62 | .hl-4 { color: var(--hl-4); } 63 | .hl-5 { color: var(--hl-5); } 64 | pre, code { background: var(--code-background); } 65 | -------------------------------------------------------------------------------- /docs/assets/icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cryptovoxels/scripting-bundle/3f96a1c4bdca62d4ad98e2cf35b2316f63227ca7/docs/assets/icons.png -------------------------------------------------------------------------------- /docs/assets/icons@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cryptovoxels/scripting-bundle/3f96a1c4bdca62d4ad98e2cf35b2316f63227ca7/docs/assets/icons@2x.png -------------------------------------------------------------------------------- /docs/assets/widgets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cryptovoxels/scripting-bundle/3f96a1c4bdca62d4ad98e2cf35b2316f63227ca7/docs/assets/widgets.png -------------------------------------------------------------------------------- /docs/assets/widgets@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cryptovoxels/scripting-bundle/3f96a1c4bdca62d4ad98e2cf35b2316f63227ca7/docs/assets/widgets@2x.png -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | Voxels-Scripting-Engine
Options
All
  • Public
  • Public/Protected
  • All
Menu

Voxels-Scripting-Engine

2 | 3 |

Voxels.com scripting engine

4 |
5 |

Voxels.com is a voxel-based world 6 | built on top of the ethereum blockchain. This scripting engine is 7 | for adding interactivity to your Voxels parcels.

8 |

Scripts are written in world using the scripting field inside the feature editor.This script is then run in a webworker on the domain untrusted.cryptovoxels.com. Your scripts are run in the browser so there is no consistency between users, they each run their own version of the scripts.

9 |

For consistency between users, we have a hosted Scripting serice that parcel owners can decide to use or not.

10 |

If users want to host their own scripting server, we have https://github.com/cryptovoxels/Voxels-Scripting-Server.

11 | 12 | 13 |

Documentation

14 |
15 |

https://github.com/cryptovoxels/Voxels-Scripting-Server/docs/index.html

16 | 17 | 18 |

Contributing

19 |
20 |
    21 |
  1. Clone the repo.

    22 |
  2. 23 |
  3. Create a branch & Make your changes

    24 |
  4. 25 |
  5. Run npm run build and npm run format to (1) make sure the project still builds and (2) format your code.

    26 |
  6. 27 |
  7. Make a pull request on github.

    28 |
  8. 29 |
30 |

If you've changed code inside src/ please run npm run docs and commit the changes.

31 |

Note: 32 | If you have access to the cryptovoxels repo; you can test the build, by then running "npm run copy:voxels" after build ; 33 | This will copy the scripting bundle to the cryptovoxels repo, and you can test it on local

34 |

Generated using TypeDoc

-------------------------------------------------------------------------------- /docs/interfaces/lib_messages.JoinMessage.html: -------------------------------------------------------------------------------- 1 | JoinMessage | Voxels-Scripting-Engine
Options
All
  • Public
  • Public/Protected
  • All
Menu

Hierarchy

Index

Properties

event?: Record<string, unknown>
target: string
type: Join

Generated using TypeDoc

-------------------------------------------------------------------------------- /docs/interfaces/lib_messages.PatchMessage.html: -------------------------------------------------------------------------------- 1 | PatchMessage | Voxels-Scripting-Engine
Options
All
  • Public
  • Public/Protected
  • All
Menu

Hierarchy

Index

Properties

event: Partial<FeatureDescription>
target: string
type: Patch
uuid: string

Generated using TypeDoc

-------------------------------------------------------------------------------- /docs/interfaces/lib_messages.PlayerAwayMessage.html: -------------------------------------------------------------------------------- 1 | PlayerAwayMessage | Voxels-Scripting-Engine
Options
All
  • Public
  • Public/Protected
  • All
Menu

Hierarchy

Index

Properties

event?: Record<string, unknown>
target: string

Generated using TypeDoc

-------------------------------------------------------------------------------- /docs/interfaces/lib_messages.PlayerEnterMessage.html: -------------------------------------------------------------------------------- 1 | PlayerEnterMessage | Voxels-Scripting-Engine
Options
All
  • Public
  • Public/Protected
  • All
Menu

Hierarchy

Index

Properties

event?: Record<string, unknown>
target: string

Generated using TypeDoc

-------------------------------------------------------------------------------- /docs/interfaces/lib_messages.PlayerLeaveMessage.html: -------------------------------------------------------------------------------- 1 | PlayerLeaveMessage | Voxels-Scripting-Engine
Options
All
  • Public
  • Public/Protected
  • All
Menu

Hierarchy

Index

Properties

event?: Record<string, unknown>
target: string

Generated using TypeDoc

-------------------------------------------------------------------------------- /docs/interfaces/lib_messages.PlayerNearbyMessage.html: -------------------------------------------------------------------------------- 1 | PlayerNearbyMessage | Voxels-Scripting-Engine
Options
All
  • Public
  • Public/Protected
  • All
Menu

Hierarchy

Index

Properties

event?: Record<string, unknown>
target: string

Generated using TypeDoc

-------------------------------------------------------------------------------- /docs/interfaces/lib_messages.StopMessage.html: -------------------------------------------------------------------------------- 1 | StopMessage | Voxels-Scripting-Engine
Options
All
  • Public
  • Public/Protected
  • All
Menu

Hierarchy

Index

Properties

event?: Record<string, unknown>
target: string
type: Stop
uuid: string

Generated using TypeDoc

-------------------------------------------------------------------------------- /docs/interfaces/lib_messages.TriggerMessage.html: -------------------------------------------------------------------------------- 1 | TriggerMessage | Voxels-Scripting-Engine
Options
All
  • Public
  • Public/Protected
  • All
Menu

Hierarchy

Index

Properties

event: {}

Type declaration

    target: string
    type: Trigger
    uuid: string

    Generated using TypeDoc

    -------------------------------------------------------------------------------- /docs/interfaces/lib_types.IParcel.html: -------------------------------------------------------------------------------- 1 | IParcel | Voxels-Scripting-Engine
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Hierarchy

    • IParcel

    Index

    Properties

    Properties

    x1: number
    x2: number
    y1: number
    y2: number
    z1: number
    z2: number

    Generated using TypeDoc

    -------------------------------------------------------------------------------- /docs/modules.html: -------------------------------------------------------------------------------- 1 | Voxels-Scripting-Engine
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Voxels-Scripting-Engine

    Generated using TypeDoc

    -------------------------------------------------------------------------------- /docs/modules/feature.html: -------------------------------------------------------------------------------- 1 | feature | Voxels-Scripting-Engine
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Index

    Classes

    Generated using TypeDoc

    -------------------------------------------------------------------------------- /docs/modules/gui.html: -------------------------------------------------------------------------------- 1 | gui | Voxels-Scripting-Engine
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Index

    Classes

    Type aliases

    Type aliases

    GUIOptions: { billBoardMode: 1 | 2 | 0 }

    Type declaration

    • billBoardMode: 1 | 2 | 0

    Generated using TypeDoc

    -------------------------------------------------------------------------------- /docs/modules/helpers.html: -------------------------------------------------------------------------------- 1 | helpers | Voxels-Scripting-Engine
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Index

    Variables

    animations: { animation: number; name: string }[] = ...
    emojis: string[] = ...

    Generated using TypeDoc

    -------------------------------------------------------------------------------- /docs/modules/index.html: -------------------------------------------------------------------------------- 1 | index | Voxels-Scripting-Engine
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Index

    Variables

    Variables

    default: { Animation: typeof Animation; Color3: typeof Color3; Feature: typeof Feature; Matrix: typeof Matrix; Parcel: typeof default; Quaternion: typeof Quaternion; Space: typeof Space; Vector2: typeof Vector2; Vector3: typeof Vector3 } = ...

    Type declaration

    • Animation: typeof Animation
    • Color3: typeof Color3
    • Feature: typeof Feature
    • Matrix: typeof Matrix
    • Parcel: typeof default
    • Quaternion: typeof Quaternion
    • Space: typeof Space
    • Vector2: typeof Vector2
    • Vector3: typeof Vector3

    Generated using TypeDoc

    -------------------------------------------------------------------------------- /docs/modules/lib_guiControl.html: -------------------------------------------------------------------------------- 1 | lib/guiControl | Voxels-Scripting-Engine
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Index

    Classes

    Type aliases

    Type aliases

    guiControlOptions: { fontSizePx?: string; height?: string | number; id?: string; positionInGrid?: [number, number]; text?: string; type: "button" | "text" }

    Type declaration

    • Optional fontSizePx?: string
    • Optional height?: string | number
    • Optional id?: string
    • Optional positionInGrid?: [number, number]
    • Optional text?: string
    • type: "button" | "text"

    Generated using TypeDoc

    -------------------------------------------------------------------------------- /docs/modules/parcel.html: -------------------------------------------------------------------------------- 1 | parcel | Voxels-Scripting-Engine
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Index

    Generated using TypeDoc

    -------------------------------------------------------------------------------- /docs/modules/player.html: -------------------------------------------------------------------------------- 1 | player | Voxels-Scripting-Engine
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Index

    Classes

    Generated using TypeDoc

    -------------------------------------------------------------------------------- /docs/modules/types.html: -------------------------------------------------------------------------------- 1 | types | Voxels-Scripting-Engine
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Index

    Type aliases

    Type aliases

    ParcelOrSpaceId: string | number

    Generated using TypeDoc

    -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extensionsToTreatAsEsm: ['.ts'], 3 | globals: { 4 | 'jest': { 5 | useESM: true, 6 | }, 7 | }, 8 | moduleNameMapper: { 9 | '^(\\.{1,2}/.*)\\.js$': '$1', 10 | }, 11 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "voxels-scripting-bundler", 3 | "version": "0.9.2", 4 | "description": "", 5 | "engines": { 6 | "node": ">= 14" 7 | }, 8 | "iframe": "dist/iframe_scripting_host.js", 9 | "hosted": "dist/hosted_scripting_host.js", 10 | "targets": { 11 | "iframe": { 12 | "source": "src/index.ts", 13 | "outputFormat": "global", 14 | "engines": { 15 | "browsers": "chrome 80" 16 | } 17 | }, 18 | "hosted": { 19 | "source": "src/index.ts", 20 | "outputFormat": "global", 21 | "engines": { 22 | "browsers": "> 0.5%, last 2 versions, not dead" 23 | } 24 | } 25 | }, 26 | "scripts": { 27 | "build": "npm run build:iframe && npm run build:hosted", 28 | "build:iframe": "npx parcel build --target iframe --no-scope-hoist", 29 | "build:hosted": "npx parcel build --target hosted --no-optimize --no-scope-hoist", 30 | "copy:voxels": "cp ./dist/iframe_scripting_host.js ../cryptovoxels/dist/scripting-host.js", 31 | "copy:hosted": "cp ./dist/hosted_scripting_host.js ../grid/scripting-host.js", 32 | "format": "prettier --write \"src/**/*.ts\"", 33 | "docs": "typedoc --options typedoc.json --tsconfig tsconfig.json", 34 | "test": "cross-env NODE_OPTIONS='--experimental-vm-modules' jest", 35 | "coverage": "nyc -r lcov -e .ts -x \"*.test.ts\" npm run test" 36 | }, 37 | "author": "", 38 | "license": "ISC", 39 | "dependencies": { 40 | "@babylonjs/core": "^5.6.0", 41 | "jest": "^28.1.0", 42 | "lodash.throttle": "^4.1.1", 43 | "ndarray": "1.0.18", 44 | "node-fetch": "^2.6.7", 45 | "parcel": "^2.5.0", 46 | "tinyify": "^2.5.1", 47 | "uuid": "3.3.2", 48 | "ws": "7.4.6" 49 | }, 50 | "devDependencies": { 51 | "@babel/preset-env": "^7.17.10", 52 | "@babel/preset-typescript": "^7.16.7", 53 | "@parcel/validator-typescript": "^2.5.0", 54 | "@types/jest": "^27.5.1", 55 | "@types/node": "^17.0.33", 56 | "@types/node-fetch": "^2.6.1", 57 | "@types/uuid": "^8.3.4", 58 | "babel": "^6.23.0", 59 | "cross-env": "^7.0.3", 60 | "events": "^3.3.0", 61 | "nyc": "^15.1.0", 62 | "prettier": "^2.6.2", 63 | "tape": "4.9.1", 64 | "ts-node": "^10.7.0", 65 | "typedoc": "^0.22.15", 66 | "typescript": "^4.6.3" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Voxels.com scripting engine 2 | 3 | [Voxels.com](https://www.voxels.com/) is a voxel-based world 4 | built on top of the ethereum blockchain. This scripting engine is 5 | for adding interactivity to your Voxels parcels. 6 | 7 | Scripts are written in world using the scripting field inside the feature editor.This script is then run in a webworker on the domain `untrusted.cryptovoxels.com`. Your scripts are run in the browser so there is no consistency between users, they each run their own version of the scripts. 8 | 9 | For consistency between users, we have a hosted Scripting service that parcel owners can decide to use or not. 10 | 11 | If users want to host their own scripting server, we have https://github.com/cryptovoxels/Voxels-Scripting-Server. 12 | 13 | ## Documentation 14 | https://github.com/cryptovoxels/Voxels-Scripting-Server/docs/index.html 15 | 16 | ## Contributing 17 | 18 | 1. Clone the repo. 19 | 20 | 2. Create a branch & Make your changes 21 | 22 | 3. Run `npm run build` and `npm run format` to (1) make sure the project still builds and (2) format your code. 23 | 24 | 4. Make a pull request on github. 25 | 26 | If you've changed code inside `src/` please run `npm run docs` and commit the changes. 27 | 28 | Note: 29 | If you have access to the cryptovoxels repo; you can test the build, by then running "npm run copy:voxels" after build ; 30 | This will copy the scripting bundle to the cryptovoxels repo, and you can test it on local 31 | -------------------------------------------------------------------------------- /src/gui.ts: -------------------------------------------------------------------------------- 1 | import { Feature } from "./feature"; 2 | import { guiControl, guiControlOptions } from "./lib/guiControl"; 3 | import * as uuid from "uuid"; 4 | 5 | /* @internal */ 6 | export type GUIOptions = { billBoardMode: 1 | 2 | 0 }; 7 | /* @internal */ 8 | export default class FeatureBasicGUI { 9 | billBoardMode = 1; 10 | feature: Feature; 11 | uuid: string; 12 | id: string = undefined!; 13 | private _listOfControls: guiControl[]; 14 | showing: boolean = false; 15 | constructor(feature: Feature, options: GUIOptions = { billBoardMode: 2 }) { 16 | this.feature = feature; 17 | //@ts-expect-error 18 | this.uuid = uuid.default ? uuid.default.v4() : uuid.v4(); 19 | this._listOfControls = []; 20 | 21 | if (options) { 22 | if ( 23 | options.billBoardMode == 0 || // BILLBOARD_NONE 24 | options.billBoardMode == 1 || // BILLBOARD_X 25 | options.billBoardMode == 2 // BILLBOARD_Y 26 | ) { 27 | this.billBoardMode = options.billBoardMode; 28 | } else { 29 | this.billBoardMode = 2; 30 | } 31 | } 32 | } 33 | 34 | addButton( 35 | text: string | null = null, 36 | positionInGrid: [number, number] = [0, 0], 37 | id: string | null = null 38 | ) { 39 | if (!id) { 40 | id = "unknown" + this._listOfControls.length + 1; 41 | } 42 | if (!text) { 43 | text = "Text"; 44 | } 45 | const control = new guiControl(this, { 46 | type: "button", 47 | id, 48 | text, 49 | positionInGrid, 50 | }); 51 | if (this._replacesOldControl(control)) { 52 | return control; 53 | } 54 | if (this._listOfControls.length > 15) { 55 | this._listOfControls.pop(); 56 | } 57 | this._listOfControls.push(control); 58 | return control; 59 | } 60 | 61 | addText( 62 | text: string | null = null, 63 | positionInGrid: [number, number] = [0, 0], 64 | id?: string 65 | ) { 66 | if (!text) { 67 | text = "Text"; 68 | } 69 | const control = new guiControl(this, { 70 | type: "text", 71 | id, 72 | text, 73 | positionInGrid, 74 | }); 75 | if (this._replacesOldControl(control)) { 76 | return control; 77 | } 78 | if (this._listOfControls.length > 15) { 79 | this._listOfControls.pop(); 80 | } 81 | this._listOfControls.push(control); 82 | return control; 83 | } 84 | 85 | private _replacesOldControl(control: guiControl) { 86 | // Replace a control if the position is the same as another. 87 | let controlToReplace = this.getControlByPosition(control.positionInGrid); 88 | if (controlToReplace) { 89 | let i = this._listOfControls.indexOf(controlToReplace); 90 | this._listOfControls[i] = control; 91 | return true; 92 | } 93 | return false; 94 | } 95 | 96 | get defaultControl(): guiControlOptions { 97 | return { 98 | type: "text", 99 | text: "Text", 100 | id: undefined, 101 | positionInGrid: [0, 0], 102 | }; 103 | } 104 | 105 | getControlById(id: string) { 106 | return this._listOfControls.find((control) => control.id == id); 107 | } 108 | 109 | getControlByUuid(uuid: string) { 110 | return this._listOfControls.find((control) => control.uuid == uuid); 111 | } 112 | 113 | getControlByPosition(positionInGrid: [number, number]) { 114 | return this._listOfControls.find( 115 | (control) => 116 | control.positionInGrid[0] == positionInGrid[0] && 117 | control.positionInGrid[1] == positionInGrid[1] 118 | ); 119 | } 120 | 121 | serialize() { 122 | const listOfControls = Array.from(this._listOfControls).map( 123 | (control) => control.summary 124 | ); 125 | return { 126 | uuid: this.uuid, 127 | listOfControls: listOfControls, 128 | billBoardMode: this.billBoardMode, 129 | }; 130 | } 131 | 132 | get listOfControls(): guiControl[] { 133 | return this._listOfControls; 134 | } 135 | 136 | show() { 137 | if (!this._listOfControls || !this._listOfControls.length) { 138 | this._listOfControls[0] = new guiControl(this, this.defaultControl); 139 | } 140 | if (this._listOfControls.length > 15) { 141 | this._listOfControls = this._listOfControls.slice(0, 15); 142 | } 143 | 144 | this.feature.parcel.broadcast({ 145 | type: "create-feature-gui", 146 | uuid: this.feature.uuid, 147 | gui: this.serialize(), 148 | }); 149 | this.showing = true; 150 | } 151 | 152 | destroy() { 153 | this.feature.parcel.broadcast({ 154 | type: "destroy-feature-gui", 155 | uuid: this.feature.uuid, 156 | }); 157 | this._listOfControls = []; 158 | this.showing = false; 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/helpers.ts: -------------------------------------------------------------------------------- 1 | export const emojis = [ 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 | "🙏", 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 | ]; 62 | 63 | export const animations = [ 64 | { name: "Idle", animation: 0 }, 65 | { name: "Dance", animation: 3 }, 66 | { name: "Wave", animation: 1 }, 67 | { name: "Sitting", animation: 5 }, 68 | { name: "Spin", animation: 6 }, 69 | { name: "Savage", animation: 7 }, 70 | { name: "Kick", animation: 8 }, 71 | { name: "Uprock", animation: 9 }, 72 | { name: "Floss", animation: 10 }, 73 | { name: "Backflip", animation: 11 }, 74 | ]; 75 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /* global parcel,self,postMessage */ 2 | 3 | import { 4 | Vector3, 5 | Quaternion, 6 | Vector2, 7 | Color3, 8 | Matrix, 9 | } from "@babylonjs/core/Maths/math"; 10 | 11 | import { Animation } from "@babylonjs/core/Animations/animation"; 12 | import { emojis as emojiList } from "./helpers"; 13 | import { Feature } from "./feature"; 14 | 15 | import Parcel from "./parcel"; 16 | import { Space } from "./parcel"; 17 | 18 | function getGlobal(): typeof globalThis | (Window & typeof globalThis) | null { 19 | if (typeof global !== "undefined") { 20 | return global; 21 | } else if (typeof self !== "undefined") { 22 | return self; 23 | } else { 24 | return null; 25 | } 26 | } 27 | 28 | // Register of the singletons we still have bound to window 29 | declare global { 30 | function fetchJson(url: string): Promise; 31 | var emojis: string[]; 32 | var animations: any[]; 33 | var global: typeof global; 34 | } 35 | 36 | // the grid is usually `global` and the iframe when the script is not hosted is usually `self`; 37 | // Even though `window` will always be null, getGlobal() doesn't return it just in case, because we don't want 38 | // to override the setInterval of the window (could affect render). 39 | const G: typeof globalThis | (Window & typeof globalThis) | null = getGlobal(); 40 | if (G) { 41 | G.setInterval = (function (setInterval: Function) { 42 | return function (func: TimerHandler, time: number, ...args: any[]) { 43 | let t = time; 44 | if (isNaN(parseInt(time.toString(), 10))) { 45 | console.error("[Scripting] setInterval interval is invalid"); 46 | return; 47 | } 48 | if (parseInt(time.toString(), 10) < 30) { 49 | t = 30; 50 | console.log("[Scripting] setInterval minimum is 30ms"); 51 | } 52 | //@ts-ignore 53 | return setInterval.call(G, func, t, ...args); 54 | }; 55 | })(G.setInterval) as any; 56 | 57 | G.emojis = emojiList; 58 | G.animations = []; 59 | } 60 | 61 | const scriptingEngine = { 62 | Parcel, 63 | Space, 64 | Feature, 65 | Animation, 66 | Vector3, 67 | Quaternion, 68 | Vector2, 69 | Color3, 70 | Matrix, 71 | }; 72 | export default scriptingEngine; 73 | 74 | if (typeof self !== "undefined") { 75 | Object.assign(self, { 76 | Parcel, 77 | Space, 78 | Feature, 79 | Animation, 80 | Vector3, 81 | Quaternion, 82 | Vector2, 83 | Color3, 84 | Matrix, 85 | }); // eslint-disable-line 86 | } 87 | -------------------------------------------------------------------------------- /src/lib/guiControl.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from "events"; 2 | import FeatureBasicGUI from "../gui"; 3 | import * as uuid from "uuid"; 4 | /* @internal */ 5 | export type guiControlOptions = { 6 | type: "button" | "text"; 7 | id?: string; 8 | text?: string; 9 | fontSizePx?: string; 10 | height?: string | number; 11 | positionInGrid?: [number, number]; 12 | }; 13 | /* @internal */ 14 | export class guiControl extends EventEmitter { 15 | id?: string; 16 | private _uuid: string; 17 | gui: FeatureBasicGUI; 18 | type: "button" | "text"; 19 | positionInGrid: [number, number]; 20 | text?: string; 21 | constructor(gui: FeatureBasicGUI, options: guiControlOptions) { 22 | super(); 23 | if (!options) { 24 | options = { 25 | type: "text", 26 | id: undefined, 27 | text: "Text", 28 | positionInGrid: [0, 0], 29 | }; 30 | } 31 | this.gui = gui; 32 | //@ts-expect-error 33 | this._uuid = uuid.default ? uuid.default.v4() : uuid.v4(); 34 | this.type = options.type || "text"; 35 | this.id = options.id; 36 | this.text = options.text || "Text"; 37 | this.positionInGrid = options.positionInGrid || [0, 0]; 38 | } 39 | 40 | get uuid() { 41 | return this._uuid; 42 | } 43 | 44 | get summary() { 45 | return { 46 | uuid: this._uuid, 47 | type: this.type, 48 | id: this.id, 49 | text: this.text, 50 | positionInGrid: this.positionInGrid, 51 | }; 52 | } 53 | 54 | remove() { 55 | if (this.gui && this.gui.listOfControls.length > 0) { 56 | this.gui.listOfControls.splice(this.gui.listOfControls.indexOf(this), 1); 57 | } 58 | this.gui.show(); 59 | } 60 | 61 | update() { 62 | if (this.gui && this.gui.showing) { 63 | this.gui.feature.parcel.broadcast({ 64 | type: "update-feature-gui", 65 | uuid: this.gui.feature.uuid, 66 | control: this.summary, 67 | }); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/lib/messages.ts: -------------------------------------------------------------------------------- 1 | import { FeatureDescription, PlayerDescription, VidScreenKeys } from "./types"; 2 | 3 | export enum SupportedMessageTypes { 4 | Join = "join", 5 | PlayerEnter = "playerenter", 6 | PlayerLeave = "playerleave", 7 | PlayerNearby = "playernearby", 8 | PlayerAway = "playeraway", 9 | CryptoHash = "cryptohash", 10 | Move = "move", 11 | Click = "click", 12 | Keys = "keys", 13 | Start = "start", 14 | Stop = "stop", 15 | Chat = "chat", 16 | Changed = "changed", 17 | Trigger = "trigger", 18 | Patch = "patch", 19 | } 20 | 21 | export type BasicMessage = { 22 | target: string; 23 | type: SupportedMessageTypes; 24 | player: PlayerDescription; 25 | event?: Record; 26 | }; 27 | 28 | export interface JoinMessage extends BasicMessage { 29 | type: SupportedMessageTypes.Join; 30 | } 31 | 32 | export interface PlayerEnterMessage extends BasicMessage { 33 | type: SupportedMessageTypes.PlayerEnter; 34 | } 35 | export interface PlayerLeaveMessage extends BasicMessage { 36 | type: SupportedMessageTypes.PlayerLeave; 37 | } 38 | export interface PlayerNearbyMessage extends BasicMessage { 39 | type: SupportedMessageTypes.PlayerNearby; 40 | } 41 | export interface PlayerAwayMessage extends BasicMessage { 42 | type: SupportedMessageTypes.PlayerAway; 43 | } 44 | export interface CryptoHashMessage extends BasicMessage { 45 | type: SupportedMessageTypes.CryptoHash; 46 | event: { 47 | hash: string; 48 | quantity: number; 49 | to: string; 50 | chain_id: number; 51 | erc20Address: string; 52 | }; 53 | } 54 | 55 | export interface MoveMessage extends BasicMessage { 56 | type: SupportedMessageTypes.Move; 57 | position: number[]; 58 | rotation: number[]; 59 | } 60 | export interface ClickMessage extends BasicMessage { 61 | type: SupportedMessageTypes.Click; 62 | uuid: string; 63 | event: { point?: number[]; normal?: number[]; guiTarget?: string }; 64 | } 65 | export interface KeysMessage extends BasicMessage { 66 | type: SupportedMessageTypes.Keys; 67 | uuid: string; 68 | event: { keys: VidScreenKeys }; 69 | } 70 | export interface StartMessage extends BasicMessage { 71 | type: SupportedMessageTypes.Start; 72 | uuid: string; 73 | } 74 | export interface StopMessage extends BasicMessage { 75 | type: SupportedMessageTypes.Stop; 76 | uuid: string; 77 | } 78 | export interface ChatMessage extends BasicMessage { 79 | type: SupportedMessageTypes.Chat; 80 | uuid: string; 81 | event: { text: string }; 82 | } 83 | export interface ChangedMessage extends BasicMessage { 84 | type: SupportedMessageTypes.Changed; 85 | uuid: string; 86 | event: { value?: string; text?: string }; 87 | } 88 | export interface TriggerMessage extends BasicMessage { 89 | type: SupportedMessageTypes.Trigger; 90 | uuid: string; 91 | event: {}; 92 | } 93 | export interface PatchMessage extends BasicMessage { 94 | type: SupportedMessageTypes.Patch; 95 | uuid: string; 96 | event: Partial; 97 | } 98 | 99 | export type Message = 100 | | JoinMessage 101 | | PlayerEnterMessage 102 | | PlayerLeaveMessage 103 | | PlayerNearbyMessage 104 | | PlayerAwayMessage 105 | | CryptoHashMessage 106 | | MoveMessage 107 | | ClickMessage 108 | | KeysMessage 109 | | StartMessage 110 | | StopMessage 111 | | ChatMessage 112 | | ChangedMessage 113 | | TriggerMessage 114 | | PatchMessage; 115 | -------------------------------------------------------------------------------- /src/lib/types.ts: -------------------------------------------------------------------------------- 1 | export type ParcelOrSpaceId = string | number; 2 | 3 | export interface IParcel { 4 | x1: number; 5 | y1: number; 6 | z1: number; 7 | x2: number; 8 | y2: number; 9 | z2: number; 10 | } 11 | 12 | export type ParcelDescription = { 13 | id: ParcelOrSpaceId; 14 | contributors?: string[]; 15 | features?: FeatureDescription[]; 16 | voxels?: string; 17 | palette?: string[]; 18 | tileset?: string; 19 | }; 20 | 21 | export type ParcelContent = { 22 | features?: FeatureDescription[]; 23 | voxels?: string; 24 | palette?: string[]; 25 | tileset?: string; 26 | }; 27 | 28 | export type ParcelBroadcastMessage = { 29 | type: string; 30 | uuid?: string; 31 | content?: FeatureDescription; 32 | parcel?: ParcelDescription; 33 | emote?: string; 34 | animations?: any; 35 | coordinates?: string; 36 | reason?: string; 37 | control?: guiControlType; 38 | gui?: guiBatchInfo; 39 | cryptoData?: Record; 40 | }; 41 | 42 | export type guiBatchInfo = { 43 | uuid: string; 44 | listOfControls: guiControlType[]; 45 | billBoardMode: number; 46 | }; 47 | 48 | export type guiControlType = { 49 | uuid: string; 50 | type: "button" | "text"; 51 | id?: string; 52 | text?: string; 53 | fontSizePx?: string; 54 | height?: string | number; 55 | positionInGrid?: [number, number]; 56 | }; 57 | 58 | export type FeatureDescription = { 59 | uuid: string; 60 | id?: string; 61 | url?: string; 62 | type: string; 63 | position: number[]; 64 | scale: number[]; 65 | rotation: number[]; 66 | } & Record; 67 | 68 | export type CollectibleType = { 69 | wearable_id: number; 70 | collection_id?: number; 71 | bone: string; 72 | } & Record; 73 | 74 | export type PlayerDescription = { 75 | _token: string; 76 | uuid: string; 77 | name?: string; 78 | wallet?: string; 79 | collectibles?: CollectibleType[]; 80 | } & Record; 81 | 82 | export type VidScreenKeys = { 83 | up: boolean; 84 | down: boolean; 85 | left: boolean; 86 | right: boolean; 87 | a: boolean; 88 | b: boolean; 89 | }; 90 | 91 | export type Snapshot = { 92 | id: number; 93 | is_snapshot: boolean; 94 | parcel_id: number; 95 | content: ParcelContent; 96 | snapshot_name: string; 97 | name: string; 98 | updated_at: any; 99 | created_at: any; 100 | content_hash: string; 101 | }; 102 | 103 | export type AnimationTarget = "rotation" | "position" | "scale"; 104 | -------------------------------------------------------------------------------- /src/lib/validation-helpers.ts: -------------------------------------------------------------------------------- 1 | // We're not using typescript but we can at least pretend ;) 2 | const types = { 3 | url: "string", 4 | uuid: "string", 5 | id: "string", 6 | script: "string", 7 | /* objects */ 8 | position: "object", 9 | rotation: "object", 10 | scale: "object", 11 | tryPosition: "object", 12 | tryRotation: "object", 13 | tryScale: "object", 14 | tryable: "boolean", 15 | /* back to normal stuff */ 16 | collidable: "boolean", 17 | isTrigger: "boolean", 18 | proximityToTrigger: "number", 19 | blendMode: "string", 20 | opacity: "number", 21 | inverted: "boolean", 22 | triggerIsAudible: "boolean", 23 | link: "string", 24 | fontSize: "string", 25 | color: "string", 26 | background: "string", 27 | text: "string", 28 | updateDaily: "boolean", 29 | stretch: "boolean", 30 | /* media */ 31 | autoplay: "boolean", 32 | loop: "boolean", 33 | rolloffFactor: "number", 34 | volume: "number", 35 | screenRatio: "string", 36 | /* nftimage */ 37 | hasGui: "boolean", 38 | hasGuiResizable: "boolean", 39 | hasFrame: "boolean", 40 | emissiveColorIntensity: "number", 41 | // transparent:'string', // transparency can be both string or boolean, kinda stupid but ok 42 | sprite: "boolean", 43 | streaming: "boolean", 44 | 45 | /* particle emitters */ 46 | emitRate: "number", 47 | minSize: "number", 48 | maxSize: "number", 49 | color1: "string", 50 | color2: "string", 51 | colorDead: "string", 52 | opacityDead: "string,", 53 | placeholder: "string", 54 | /* slider */ 55 | minimum: "number", 56 | maximum: "number", 57 | default: "number", 58 | }; 59 | 60 | const arrayProperties = { 61 | position: ["number", "number", "number"], 62 | rotation: ["number", "number", "number"], 63 | scale: ["number", "number", "number"], 64 | tryPosition: ["number", "number", "number"], 65 | tryRotation: ["number", "number", "number"], 66 | tryScale: ["number", "number", "number"], 67 | specularColor: ["number", "number", "number"], 68 | }; 69 | 70 | export function _isValidArray( 71 | array: any[], 72 | expectedLength: number = 3, 73 | type: string | null = null 74 | ) { 75 | if (Array.isArray(array)) { 76 | if (array.length == expectedLength) { 77 | if (type) { 78 | // same length and type define, check each value 79 | let pass = true; 80 | array.forEach((v) => { 81 | if (typeof v !== type) { 82 | pass = false; 83 | } 84 | }); 85 | return pass; 86 | } 87 | // same length and no type defined, we pass the validation 88 | return true; 89 | } 90 | // is array and not same length 91 | return false; 92 | } 93 | // is not array 94 | return false; 95 | } 96 | 97 | export function _isValidProperty(value: any, type: string | null = null) { 98 | if (!type) { 99 | return true; 100 | } 101 | if (typeof value == type) { 102 | return true; 103 | } 104 | return false; 105 | } 106 | 107 | export function _validateObject(object: Record) { 108 | let resultDict = {} as Record; 109 | 110 | Object.entries(object).forEach(([dictKey, value]) => { 111 | let currentProperty = Object.keys(types).find( 112 | (key) => key == dictKey 113 | ) as keyof typeof types; 114 | // We found a property with same name 115 | if (currentProperty) { 116 | // If the type of value is appropriate add in to the resultDict 117 | if (_isValidProperty(value, types[currentProperty])) { 118 | switch (typeof value) { 119 | case "object": 120 | // We have an object, check if we're dealing with position,scale,rotation 121 | let is3DProperty = Object.keys(arrayProperties).find( 122 | (key) => key == dictKey 123 | ) as keyof typeof arrayProperties; 124 | if (is3DProperty) { 125 | // we have position,scale or rotation or another array 126 | let isValid = _isValidArray( 127 | object[currentProperty], 128 | arrayProperties[is3DProperty].length, 129 | "number" 130 | ); 131 | if (isValid) { 132 | resultDict[currentProperty] = value; 133 | } else { 134 | console.error( 135 | `[Scripting]${currentProperty} should be of type array of length 3` 136 | ); 137 | } 138 | } else { 139 | // it's just an object; 140 | resultDict[dictKey] = value; 141 | } 142 | 143 | break; 144 | default: 145 | // just add 146 | resultDict[dictKey] = value; 147 | } 148 | } else { 149 | let is3DProperty = Object.keys(arrayProperties).find( 150 | (key) => key == currentProperty 151 | ); 152 | if (is3DProperty) { 153 | console.error( 154 | `[Scripting] ${currentProperty} should be of type array of length 3` 155 | ); 156 | } else { 157 | console.error( 158 | `[Scripting] ${currentProperty} should be of type ${types[currentProperty]}` 159 | ); 160 | } 161 | } 162 | } else { 163 | // We found no type for that key, 164 | // key was probably mispecified or it's on purpose, do nothing 165 | // just add 166 | resultDict[dictKey] = value; 167 | } 168 | }); 169 | return resultDict; 170 | } 171 | -------------------------------------------------------------------------------- /src/player.ts: -------------------------------------------------------------------------------- 1 | import { Vector3 } from "@babylonjs/core/Maths/math"; 2 | import { MoveMessage } from "./lib/messages"; 3 | 4 | import { CollectibleType, PlayerDescription } from "./lib/types"; 5 | import fetch from "node-fetch"; 6 | import { EventEmitter } from "events"; 7 | import Parcel from "./parcel"; 8 | //@ts-ignore 9 | import throttle from "lodash.throttle"; 10 | /* @internal */ 11 | export class Player extends EventEmitter { 12 | collectibles: CollectibleType[]; 13 | private _token: string; 14 | private _iswithinParcel: boolean; 15 | readonly uuid: string; 16 | parcel: Parcel; 17 | name: string | undefined; 18 | wallet: string | undefined; 19 | position: Vector3; 20 | rotation: Vector3; 21 | constructor(description: PlayerDescription, parcel: Parcel) { 22 | super(); 23 | Object.assign(this, description); 24 | this.parcel = parcel; 25 | this._token = description && description._token; 26 | this.uuid = description && description.uuid; 27 | this.name = description && description.name; 28 | this.wallet = description && description.wallet; 29 | this._iswithinParcel = false; 30 | this.position = Vector3.Zero(); 31 | this.rotation = Vector3.Zero(); 32 | this.collectibles = (description && description.collectibles) || []; 33 | } 34 | 35 | emote = throttle( 36 | (emoji: string) => { 37 | if (!this.isWithinParcel) { 38 | // don't allow this if user is outside parcel 39 | console.error("[Scripting] User outside parcel, can't emote!"); 40 | return; 41 | } 42 | 43 | if (!emojis.includes(emoji)) { 44 | return; 45 | } 46 | this.parcel.broadcast({ 47 | type: "player-emote", 48 | uuid: this.uuid, 49 | emote: emoji, 50 | }); 51 | }, 52 | 500, 53 | { leading: true, trailing: false } 54 | ); 55 | /** 56 | * Animate the avatar 57 | * @deprecated 58 | */ 59 | animate = throttle((animation: string) => {}, 10000, { 60 | leading: true, 61 | trailing: false, 62 | }); 63 | 64 | get isWithinParcel() { 65 | return this._iswithinParcel; 66 | } 67 | set isWithinParcel(within: boolean) { 68 | this._iswithinParcel = !!within; 69 | } 70 | 71 | get token() { 72 | return this._token; 73 | } 74 | 75 | _set(playerInfo: Partial | null = null) { 76 | if (!playerInfo) { 77 | return; 78 | } 79 | if (playerInfo.name) { 80 | this.name = playerInfo.name; 81 | } 82 | if (playerInfo.wallet) { 83 | this.wallet = playerInfo.wallet; 84 | } 85 | if (typeof playerInfo._iswithinParcel !== "undefined") { 86 | this.isWithinParcel = !!playerInfo._iswithinParcel; 87 | } 88 | if (playerInfo.collectibles) { 89 | this.collectibles = playerInfo.collectibles; 90 | } 91 | } 92 | 93 | isLoggedIn() { 94 | return !!this.wallet && !!this.wallet.match(/^(0x)?[0-9a-f]{40}$/i); 95 | } 96 | /** 97 | * Teleports the avatar to a coordinate 98 | * @param coords string of coordinates, eg: NE@47W,250N 99 | */ 100 | teleportTo = throttle( 101 | (coords: string) => { 102 | if (!this.isWithinParcel) { 103 | // don't allow this if user is outside parcel 104 | return; 105 | } 106 | if (!coords || coords == "") { 107 | return; 108 | } 109 | this.parcel.broadcast({ 110 | type: "player-teleport", 111 | uuid: this.uuid, 112 | coordinates: coords, 113 | }); 114 | }, 115 | 1000, 116 | { trailing: false, leading: true } 117 | ) as (coords: string) => void; 118 | 119 | hasWearable(tokenId: number, collectionId: number = 1) { 120 | return !!this.collectibles.find((c) => { 121 | let collection_id = c.collection_id || 1; 122 | return c.wearable_id == tokenId && collectionId == collection_id; 123 | }); 124 | } 125 | /** 126 | * @deprecated Use hasNFT() in the future. 127 | * @param {string} contract the contract address 128 | * @param {string|number} tokenId the token id 129 | * @param {(boolean)=>void} successCallback A callback called on success and has a boolean (whether player has NFT or not) as argument 130 | * @param {(string)=>void} failCallback Callback called on fail. With a string as arugment (the reason) 131 | * @returns 132 | */ 133 | hasEthereumNFT = ( 134 | contract: string, 135 | tokenId: string | number, 136 | successCallback: ((bool: boolean) => void) | null = null, 137 | failCallback: ((reason: string) => void) | null = null 138 | ) => { 139 | if (!this.isLoggedIn()) { 140 | failCallback && failCallback("User is not logged in"); 141 | return false; 142 | } 143 | if (typeof tokenId !== "number" && typeof tokenId !== "string") { 144 | console.error("[Scripting] token id is invalid"); 145 | failCallback && failCallback("Invalid token id"); 146 | return false; 147 | } 148 | if (typeof tokenId == "number") { 149 | tokenId = tokenId.toString(); 150 | } 151 | 152 | if ( 153 | typeof contract !== "string" || 154 | (typeof contract == "string" && contract.substring(0, 2) !== "0x") 155 | ) { 156 | console.error("[Scripting] contract address is invalid"); 157 | return false; 158 | } 159 | let url = `https://www.voxels.com/api/avatar/owns/eth/${contract}/${tokenId}?wallet=${this.wallet}`; 160 | let promise; 161 | if (typeof global == "undefined" || !global.fetchJson) { 162 | /* fetch doesn't work nicely on the grid. So we use 'fetchJson' when on scripthost, and fetch() when local */ 163 | promise = fetch(url).then((r) => r.json()); 164 | } else { 165 | promise = fetchJson(url); 166 | } 167 | 168 | promise 169 | .then((r: { success: boolean; ownsToken?: boolean }) => { 170 | if (!r) { 171 | failCallback && failCallback("no data by opensea, try again later"); 172 | return false; 173 | } 174 | let ownsAsset = !!r.ownsToken; 175 | 176 | if (successCallback) { 177 | successCallback(ownsAsset); 178 | } else { 179 | console.error('[Scripting] No callback given to "hasEthereumNFT"'); 180 | console.log(`[Scripting] hasNFT = ${ownsAsset}`); 181 | } 182 | return ownsAsset; 183 | }) 184 | .catch((e) => { 185 | failCallback && failCallback(e.toString() || "error fetching the data"); 186 | console.error("[Scripting]", e); 187 | }); 188 | }; 189 | 190 | /** 191 | * Will Fetch whether the player has the given NFT and return the value using the callbacks provided 192 | * @param {string} chain the chain identifier: 'eth' or 'matic' 193 | * @param {string} contract the contract address 194 | * @param {string|number} tokenId the token id 195 | * @param {(boolean)=>void} successCallback A callback called on success and has a boolean (whether player has NFT or not) as argument 196 | * @param {(string)=>void} failCallback Callback called on fail. With a string as arugment (the reason) 197 | * @returns 198 | */ 199 | hasNFT = ( 200 | chain: string, 201 | contract: string, 202 | tokenId: string | number, 203 | successCallback: ((bool: boolean) => void) | null = null, 204 | failCallback: ((reason: string) => void) | null = null 205 | ) => { 206 | if (!this.isLoggedIn()) { 207 | failCallback && failCallback("User is not logged in"); 208 | return false; 209 | } 210 | if (typeof tokenId !== "number" && typeof tokenId !== "string") { 211 | console.error("[Scripting] token id is invalid"); 212 | failCallback && failCallback("Invalid token id"); 213 | return false; 214 | } 215 | if (typeof tokenId == "number") { 216 | tokenId = tokenId.toString(); 217 | } 218 | if (chain !== "eth" && chain !== "matic") { 219 | console.error("[Scripting] chain unsupported"); 220 | failCallback && failCallback("Invalid chain"); 221 | } 222 | 223 | if ( 224 | typeof contract !== "string" || 225 | (typeof contract == "string" && contract.substring(0, 2) !== "0x") 226 | ) { 227 | console.error("[Scripting] contract address is invalid"); 228 | return false; 229 | } 230 | let url = `https://www.voxels.com/api/avatar/owns/${chain}/${contract}/${tokenId}?wallet=${this.wallet}`; 231 | let promise; 232 | if (typeof global == "undefined" || !global.fetchJson) { 233 | /* fetch doesn't work nicely on the grid. So we use 'fetchJson' when on scripthost, and fetch() when local */ 234 | promise = fetch(url).then((r) => r.json()); 235 | } else { 236 | promise = fetchJson(url); 237 | } 238 | 239 | promise 240 | .then((r: { success: boolean; ownsToken?: boolean }) => { 241 | if (!r) { 242 | failCallback && failCallback("Could not reach API, try again later"); 243 | return false; 244 | } 245 | let ownsAsset = !!r.ownsToken; 246 | 247 | if (successCallback) { 248 | successCallback(ownsAsset); 249 | } else { 250 | console.error('[Scripting] No callback given to "hasNFT"'); 251 | console.log(`[Scripting] hasNFT = ${ownsAsset}`); 252 | } 253 | return ownsAsset; 254 | }) 255 | .catch((e) => { 256 | failCallback && failCallback(e.toString() || "error fetching the data"); 257 | console.error("[Scripting]", e); 258 | }); 259 | }; 260 | 261 | get isAnonymous() { 262 | return !this.isLoggedIn(); 263 | } 264 | 265 | onMove = (msg: MoveMessage) => { 266 | if (!msg) { 267 | return; 268 | } 269 | this.position.set(msg.position[0], msg.position[1], msg.position[2]); 270 | this.rotation.set(msg.rotation[0], msg.rotation[1], msg.rotation[2]); 271 | this.emit("move", msg); 272 | }; 273 | 274 | kick(reason: string | undefined = undefined) { 275 | if (!this.isWithinParcel) { 276 | // don't allow this if user is outside parcel 277 | return; 278 | } 279 | if (this.wallet == this.parcel.owner) { 280 | console.log("[Scripting] Cannot kick the owner"); 281 | return; 282 | } 283 | if (this.uuid) { 284 | this.parcel.broadcast({ 285 | type: "player-kick", 286 | uuid: this.uuid, 287 | reason: reason, 288 | }); 289 | } 290 | } 291 | 292 | ///@private 293 | private _askForCrypto( 294 | quantity: number = 0.01, 295 | to: string = this.parcel.owner, 296 | chain_id: number = 1, 297 | erc20Address?: string 298 | ) { 299 | this.parcel.broadcast({ 300 | type: "player-askcrypto", 301 | uuid: this.uuid, 302 | cryptoData: { quantity, to, chain_id, erc20Address }, 303 | }); 304 | } 305 | 306 | /** 307 | * Asks the given player for crypto. Throttled to 1.5s 308 | * This function is also ONLY available when the player has recently clicked. 309 | * @param {number} quantity the quantity to send, default 0.01 310 | * @param {string} to The receiver of the crypto; leave empty for parcel owner 311 | * @param {string} erc20Address Optional, Address of erc20 to send. 312 | * @param {number} chain_id Optional,The network id if any erc20 address is given. (1=mainnet,137= polygon) 313 | * 314 | * ```ts 315 | * feature.on('click',e=>{ 316 | * e.player.askForCrypto(1,137) 317 | * }) 318 | * ``` 319 | */ 320 | askForCrypto = throttle( 321 | ( 322 | quantity: number = 0.01, 323 | chain_id: number = 1, 324 | to: string = this.parcel.owner, 325 | erc20Address?: string 326 | ) => { 327 | this._askForCrypto(quantity, to, chain_id, erc20Address); 328 | }, 329 | 1500, 330 | { trailing: false, leading: true } 331 | ) as ( 332 | quantity: number, 333 | chain_id?: number, 334 | to?: string, 335 | erc20Address?: string 336 | ) => void; 337 | } 338 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export type ParcelOrSpaceId = string | number; 2 | -------------------------------------------------------------------------------- /test/feature.test.ts: -------------------------------------------------------------------------------- 1 | import { Animation, Vector3 } from "@babylonjs/core"; 2 | import assert from "assert"; 3 | import Parcel from "../src/parcel"; 4 | import {fn} from 'jest-mock' 5 | import * as p from './parcel.json' 6 | import { overrideParcel } from "./test_lib"; 7 | //@ts-ignore 8 | const json = p.default.parcel 9 | 10 | describe('Test Feature', function() { 11 | let parcel:Parcel & {receiveMsg: (obj: any) => any}; 12 | const playerDetails = {wallet:'ded',uuid:'wdwdwd',_token:'ded'} 13 | beforeAll(()=>{ 14 | parcel = overrideParcel(new Parcel(2)) 15 | 16 | parcel.broadcast=fn() 17 | global.postMessage=()=>{} 18 | }) 19 | 20 | it('Parse features', function() { 21 | parcel.parse(json) 22 | assert.equal(9, parcel.getFeatures().length) 23 | 24 | const f= parcel.getFeatureByUuid('faf05014-9a08-4aeb-89c6-02da0bb8e237') 25 | assert.ok(f) 26 | assert.deepEqual(f.position.asArray(),[ 27 | -3.75, 28 | 2.25, 29 | -3.75 30 | ]) 31 | assert.deepEqual(f.rotation.asArray(),[ 32 | 0, 33 | 4.71238898038469, 34 | 0 35 | ]) 36 | }); 37 | 38 | it('Feature- set new position', () => { 39 | const f= parcel.getFeatureByUuid('faf05014-9a08-4aeb-89c6-02da0bb8e237') 40 | assert.ok(f) 41 | f.position.set(2,2,2) 42 | //broadcast is not called yet 43 | assert.deepEqual(f.position.asArray(),[ 44 | 2, 45 | 2, 46 | 2 47 | ]) 48 | f.position.x = 5 49 | assert.deepEqual(f.position.asArray(),[ 50 | 5, 51 | 2, 52 | 2 53 | ]) 54 | // Test clone 55 | assert.deepEqual(f.position.clone().asArray(),[ 56 | 5, 57 | 2, 58 | 2 59 | ]) 60 | 61 | f.set({position:[1,1,1]}) 62 | expect(parcel.broadcast).toHaveBeenCalled() 63 | assert.deepEqual(f.position.asArray(),[1,1,1]) 64 | }) 65 | it('Feature- create Animation', () => { 66 | const f= parcel.getFeatureByUuid('faf05014-9a08-4aeb-89c6-02da0bb8e237') 67 | assert.ok(f) 68 | const newAnimation =f.createAnimation('position') 69 | expect(newAnimation).toBeInstanceOf(Animation) 70 | expect(newAnimation.name).toEqual(`scripting/animation/faf05014-9a08-4aeb-89c6-02da0bb8e237`) 71 | 72 | newAnimation.setKeys([{ 73 | frame: 30, // standard is 30 fps (means it take 1 second) 74 | value: f.position.add( new Vector3(0,10,0) ) 75 | }]) 76 | f.startAnimations([newAnimation]) 77 | expect(parcel.broadcast).toHaveBeenCalled() 78 | }) 79 | it('Feature- create basic gui', () => { 80 | const f= parcel.getFeatureByUuid('faf05014-9a08-4aeb-89c6-02da0bb8e237') 81 | assert.ok(f) 82 | const newGUI =f.createBasicGui('myId') 83 | assert.ok(newGUI) 84 | assert.ok(newGUI.uuid) 85 | expect(newGUI.id).toEqual('myId') 86 | const button = newGUI.addButton('Click me',[1,1],'lol') 87 | expect(newGUI.listOfControls.length).toEqual(1) 88 | expect(button.id).toEqual('lol') 89 | assert.deepEqual(button.positionInGrid,[1,1]) 90 | expect(button.text).toEqual('Click me') 91 | newGUI.show() 92 | expect(parcel.broadcast).toHaveBeenCalled() 93 | 94 | f.removeGui() 95 | expect(f.gui).toBeUndefined() 96 | }) 97 | 98 | }); 99 | -------------------------------------------------------------------------------- /test/parcel.json: -------------------------------------------------------------------------------- 1 | { 2 | "success": true, 3 | "parcel": { 4 | "id": 2, 5 | "features": [ 6 | { 7 | "id": "boop", 8 | "type": "richtext", 9 | "scale": [ 10 | 2, 11 | 2, 12 | 1 13 | ], 14 | "text": "# Welcome!\n\nCryptovoxels is a virtual world, where the city is owned by different people on the Ethereum blockchain.\n\nIf you have some Ether, you can buy a parcel on OpenSea and then build on it. Each different building you see in the world is build by a different person.\n\nYou will see other people walking around, press ENTER to chat with them.\n\nHave fun!", 15 | "position": [ 16 | -3.75, 17 | 2.25, 18 | -3.75 19 | ], 20 | "rotation": [ 21 | 0, 22 | 4.71238898038469, 23 | 0 24 | ], 25 | "uuid": "faf05014-9a08-4aeb-89c6-02da0bb8e237" 26 | }, 27 | { 28 | "type": "image", 29 | "scale": [ 30 | 1, 31 | 1, 32 | 1 33 | ], 34 | "url": "https://i.imgur.com/Efany1C.jpg", 35 | "position": [ 36 | -6.25, 37 | 2.25, 38 | 1.25 39 | ], 40 | "rotation": [ 41 | 0, 42 | 4.71238898038469, 43 | 0 44 | ], 45 | "uuid": "8f4e097d-d560-409c-a3b5-4638f1897469", 46 | "color": false 47 | }, 48 | { 49 | "type": "sign", 50 | "scale": [ 51 | 0.5, 52 | 0.5, 53 | 0.5 54 | ], 55 | "text": "Kense", 56 | "position": [ 57 | -6.25, 58 | 1.5, 59 | 1.25 60 | ], 61 | "rotation": [ 62 | 0, 63 | 4.71238898038469, 64 | 0 65 | ], 66 | "uuid": "7bd4c0cf-2fb3-403f-8fcc-2a4715d19190" 67 | }, 68 | { 69 | "type": "image", 70 | "scale": [ 71 | 1, 72 | 1, 73 | 1 74 | ], 75 | "url": "https://i.imgur.com/eqaMxTo.jpg", 76 | "position": [ 77 | -6.25, 78 | 2.25, 79 | 3.25 80 | ], 81 | "rotation": [ 82 | 0, 83 | 4.71238898038469, 84 | 0 85 | ], 86 | "id": "boompity", 87 | "uuid": "88c9d244-674f-40cd-99e2-537f4d1dc89d", 88 | "color": true 89 | }, 90 | { 91 | "type": "sign", 92 | "scale": [ 93 | 0.5, 94 | 0.5, 95 | 0.5 96 | ], 97 | "text": "Smokey", 98 | "position": [ 99 | -6.25, 100 | 1.5, 101 | 3.25 102 | ], 103 | "rotation": [ 104 | 0, 105 | 4.71238898038469, 106 | 0 107 | ], 108 | "uuid": "e4c4c2a6-302c-4c5d-9f19-776b818aa7ad" 109 | }, 110 | { 111 | "type": "image", 112 | "scale": [ 113 | 1, 114 | 1, 115 | 1 116 | ], 117 | "url": "https://i.imgur.com/bqNOxx9.jpg", 118 | "position": [ 119 | -6.25, 120 | 2.25, 121 | 5.25 122 | ], 123 | "rotation": [ 124 | 0, 125 | 4.71238898038469, 126 | 0 127 | ], 128 | "uuid": "aa5d9388-7169-4f93-bd35-7ea64aabbb96", 129 | "inverted": false, 130 | "color": true 131 | }, 132 | { 133 | "type": "sign", 134 | "scale": [ 135 | 0.6749999999999999, 136 | 0.5, 137 | 0.5 138 | ], 139 | "text": "Volca Rig", 140 | "position": [ 141 | -6.25, 142 | 1.5, 143 | 5.25 144 | ], 145 | "rotation": [ 146 | 0, 147 | 4.71238898038469, 148 | 0 149 | ], 150 | "uuid": "33a70c24-7f24-4318-89ee-22e33e7aead3" 151 | }, 152 | { 153 | "type": "polytext", 154 | "scale": [ 155 | 0.5, 156 | 0.5, 157 | 0.5 158 | ], 159 | "text": "Hi!", 160 | "position": [ 161 | 7, 162 | 0.75, 163 | 5 164 | ], 165 | "rotation": [ 166 | 0, 167 | 1.5707963267948966, 168 | 0 169 | ], 170 | "uuid": "88548223-a75f-4249-81ff-ae0dcf5542e8" 171 | }, 172 | { 173 | "type": "polytext", 174 | "scale": [ 175 | 0.5, 176 | 0.5, 177 | 0.5 178 | ], 179 | "text": ":D", 180 | "position": [ 181 | 1.75, 182 | 0.75, 183 | 5 184 | ], 185 | "rotation": [ 186 | 0, 187 | 1.5707963267948966, 188 | 0 189 | ], 190 | "uuid": "5851a3e3-277b-428f-97e5-4525f2ea64ff" 191 | } 192 | ], 193 | "voxels": "eJzt221qwzAMgOF5vWiP4qP0qCOELmnwMllBjiS/j390HxWpKS+Bziu1BFxfSswy63E24tLvVzsJeBSz/fG7BTyiXyCujP2+n3F8lOz2UR+19Shx1+yzPmvrEfll7Ndit4BH9AvERb9AXPQLxBW33+/da1i/k+92efbyCdH6KdEcs5ve2c/VM6vBbM9s3H4BXK2ffoH70C9g4VVHXIV+AQv0O1+/Y95zjEC/Hf02dtHaWXO3bmZfdVna67Ywe8fs+j6+l+q6QlfuZffV2/uay+9Z4v7dlo5zyNdnt3f7ynXhgff7b5x+l7+b6nd7paOR7cMX+qVfxEW/9Auco1/prAb9whb9Smc16Be20vRbP9YB/SKnPP1u5umX8x6zo9//0C/8ytVvafYrO7/R/p2soztm5Sd8kFeufv+6/+5fs3a/Hv/D2/p0HrN5Z13Ve9pvnPOTvbPce2eXp18+f8Z80vR7in6RE/1KZzXoF7boVzqrQb+wRb/SWQ36hS36lc5q0C9s0a90VoN+YStiv7Ky++o/zm5fbe1Hmd3/jNnss/r1A5MuiM4=", 194 | "owner": "0x2D891ED45C4C3EAB978513DF4B92a35Cf131d2e2", 195 | "address": "72 Block Fork", 196 | "colors": 174, 197 | "x1": -20, 198 | "y1": 0, 199 | "z1": 2, 200 | "x2": -2, 201 | "y2": 8, 202 | "z2": 17 203 | } 204 | } -------------------------------------------------------------------------------- /test/parcel.test.ts: -------------------------------------------------------------------------------- 1 | import { Vector3 } from "@babylonjs/core"; 2 | import assert from "assert"; 3 | import Parcel from "../src/parcel"; 4 | 5 | import * as p from "./parcel.json"; 6 | import { overrideParcel } from "./test_lib"; 7 | //@ts-ignore 8 | const json = p.default.parcel; 9 | 10 | describe("Test Parcel", function () { 11 | let parcel: Parcel & { receiveMsg: (obj: any) => any }; 12 | const playerDetails = { wallet: "ded", uuid: "wdwdwd", _token: "ded" }; 13 | beforeAll(() => { 14 | parcel = overrideParcel(new Parcel(2)); 15 | }); 16 | it("Parcel Object has id 2", function () { 17 | expect(parcel.id).toEqual(2); 18 | }); 19 | 20 | it("Parse parcel", function () { 21 | parcel.parse(json); 22 | expect(parcel.id).toEqual(2); 23 | expect(parcel.x1).toEqual(-20); 24 | expect(parcel.y1).toEqual(0); 25 | expect(parcel.z1).toEqual(2); 26 | expect(parcel.x2).toEqual(-2); 27 | expect(parcel.y2).toEqual(8); 28 | expect(parcel.z2).toEqual(17); 29 | 30 | expect(parcel.address).toEqual("72 Block Fork"); 31 | expect(parcel.owner).toEqual("0x2D891ED45C4C3EAB978513DF4B92a35Cf131d2e2"); 32 | 33 | assert.ok(parcel.getFeatureByUuid("faf05014-9a08-4aeb-89c6-02da0bb8e237")); 34 | assert.ok(parcel.getFeatureById("boop")); 35 | assert.equal(1, parcel.getFeaturesByType("richtext").length); 36 | assert.equal(9, parcel.getFeatures().length); 37 | }); 38 | 39 | it("createFeature / removeFeature", () => { 40 | parcel.broadcast = () => {}; 41 | 42 | assert.equal(9, parcel.getFeatures().length); 43 | 44 | const f = parcel.createFeature("image"); 45 | let uuid = f.uuid; 46 | assert.ok(f.uuid); 47 | assert.deepEqual(Vector3.Zero().asArray(), f.position.asArray()); 48 | assert.deepEqual(Vector3.Zero().asArray(), f.rotation.asArray()); 49 | assert.deepEqual([1, 1, 1], f.scale.asArray()); 50 | f.remove(); 51 | 52 | assert.equal(9, parcel.getFeatures().length); 53 | assert.equal(-1, parcel.getFeatures().indexOf(f)); 54 | assert.equal(undefined, parcel.getFeatureByUuid(uuid)); 55 | }); 56 | 57 | test("getFeatureByUuid", () => { 58 | assert.ok(parcel.getFeatureByUuid("8f4e097d-d560-409c-a3b5-4638f1897469")); 59 | assert.ok(!parcel.getFeatureByUuid("boopboop")); 60 | }); 61 | 62 | test("getFeatureById", () => { 63 | assert.ok(parcel.getFeatureById("boompity")); 64 | assert.ok(!parcel.getFeatureById("zingzong")); 65 | }); 66 | 67 | test("getFeatures", () => { 68 | assert.equal(9, parcel.getFeatures().length); 69 | }); 70 | 71 | test("getFeaturesByType", () => { 72 | assert.equal(3, parcel.getFeaturesByType("image").length); 73 | assert.equal(1, parcel.getFeaturesByType("richtext").length); 74 | }); 75 | 76 | test("Player joins and leave", () => { 77 | assert.equal(0, parcel.getPlayers().length); 78 | 79 | parcel.receiveMsg({ type: "join", player: playerDetails }); 80 | assert.equal(1, parcel.getPlayers().length); 81 | 82 | parcel.receiveMsg({ type: "playeraway", player: playerDetails }); 83 | 84 | assert.equal(0, parcel.getPlayers().length); 85 | }); 86 | 87 | test("Parcel events", (done) => { 88 | const getplayer = () => { 89 | return parcel.getPlayerByUuid(playerDetails.uuid); 90 | }; 91 | parcel.on("join", () => { 92 | assert.equal(1, parcel.getPlayers().length); 93 | }); 94 | parcel.on("playernearby", () => { 95 | let p = getplayer(); 96 | assert.ok(p); 97 | expect(p.isWithinParcel).toBeFalsy(); 98 | }); 99 | parcel.on("playerenter", () => { 100 | let p = getplayer(); 101 | assert.ok(p); 102 | expect(p.isWithinParcel).toBeTruthy(); 103 | }); 104 | parcel.on("playerleave", () => { 105 | let p = getplayer(); 106 | assert.ok(p); 107 | expect(p.isWithinParcel).toBeFalsy(); 108 | }); 109 | // player re-enters the parcel 110 | parcel.on("playerenter", () => { 111 | let p = getplayer(); 112 | assert.ok(p); 113 | expect(p.isWithinParcel).toBeTruthy(); 114 | }); 115 | parcel.on("playeraway", () => { 116 | let p = getplayer(); 117 | assert.ok(p); 118 | expect(p.isWithinParcel).toBeFalsy(); 119 | setTimeout(() => { 120 | let p = getplayer(); 121 | expect(p).toBeUndefined(); 122 | done(); 123 | }, 100); 124 | }); 125 | 126 | parcel.receiveMsg({ type: "join", player: playerDetails }); 127 | parcel.receiveMsg({ type: "playernearby", player: playerDetails }); 128 | parcel.receiveMsg({ type: "playerenter", player: playerDetails }); 129 | parcel.receiveMsg({ type: "playerleave", player: playerDetails }); 130 | parcel.receiveMsg({ type: "playerenter", player: playerDetails }); 131 | parcel.receiveMsg({ type: "playeraway", player: playerDetails }); 132 | }); 133 | }); 134 | -------------------------------------------------------------------------------- /test/player.test.ts: -------------------------------------------------------------------------------- 1 | import { Vector3 } from "@babylonjs/core"; 2 | import assert from "assert"; 3 | import { Player } from "../src/player"; 4 | import Parcel from "../src/parcel"; 5 | import { fail } from "assert"; 6 | import * as p from "./parcel.json"; 7 | import { overrideParcel } from "./test_lib"; 8 | import { MoveMessage } from "../src/lib/messages"; 9 | //@ts-ignore 10 | const json = p.default.parcel; 11 | const playerDetail = { wallet: "ded", uuid: "wdwdwd", _token: "lol" }; 12 | describe("Test Player", function () { 13 | let parcel: Parcel & { receiveMsg: (obj: any) => any }; 14 | let player: Player; 15 | 16 | beforeAll(() => { 17 | parcel = overrideParcel(new Parcel(2)); 18 | player = new Player(playerDetail, parcel); 19 | }); 20 | 21 | it("Player Object has correct info", function () { 22 | expect(player.uuid).toEqual("wdwdwd"); 23 | expect(player.wallet).toEqual("ded"); 24 | expect(player.token).toEqual("lol"); 25 | expect(player.collectibles.length).toEqual(0); 26 | expect(player.name).toEqual(undefined); 27 | 28 | assert.deepEqual(Vector3.Zero().asArray(), player.position.asArray()); 29 | assert.deepEqual(Vector3.Zero().asArray(), player.rotation.asArray()); 30 | 31 | expect(player.isWithinParcel).toEqual(false); 32 | }); 33 | 34 | it("Player Object has joined", function () { 35 | parcel.receiveMsg({ type: "join", player: playerDetail }); 36 | 37 | assert.equal(1, parcel.getPlayers().length); 38 | const p = parcel.getPlayerByUuid(playerDetail.uuid); 39 | assert.ok(p); 40 | expect(p.uuid).toEqual("wdwdwd"); 41 | expect(p.wallet).toEqual("ded"); 42 | expect(p.token).toEqual("lol"); 43 | expect(p.collectibles.length).toEqual(0); 44 | expect(p.name).toEqual(undefined); 45 | expect(p.isWithinParcel).toEqual(false); 46 | }); 47 | 48 | it("Player Object has playerentered", function () { 49 | parcel.receiveMsg({ type: "playerenter", player: playerDetail }); 50 | assert.equal(1, parcel.getPlayers().length); 51 | const p = parcel.getPlayerByUuid(playerDetail.uuid); 52 | assert.ok(p); 53 | expect(p.isWithinParcel).toEqual(true); 54 | }); 55 | 56 | it("Player Object has left", function () { 57 | parcel.receiveMsg({ type: "playerleave", player: playerDetail }); 58 | assert.equal(1, parcel.getPlayers().length); 59 | const p = parcel.getPlayerByUuid(playerDetail.uuid); 60 | assert.ok(p); 61 | expect(p.isWithinParcel).toEqual(false); 62 | }); 63 | 64 | it("Player has chatted", (done) => { 65 | const p = parcel.getPlayerByUuid(playerDetail.uuid); 66 | assert.ok(p); 67 | p.on("chat", (event: { text: string }) => { 68 | expect("I am chatting").toEqual(event.text); 69 | done(); 70 | }); 71 | 72 | parcel.receiveMsg({ 73 | type: "chat", 74 | event: { text: "I am chatting" }, 75 | uuid: playerDetail.uuid, 76 | player: playerDetail, 77 | }); 78 | }); 79 | 80 | it("Player has moved", (done) => { 81 | const p = parcel.getPlayerByUuid(playerDetail.uuid); 82 | const moveMsg = { 83 | position: [1, 1, 1], 84 | rotation: [1, 0, 1], 85 | }; 86 | assert.ok(p); 87 | p.on("move", (event: MoveMessage) => { 88 | assert.deepEqual([1, 1, 1], event.position); 89 | assert.deepEqual([1, 0, 1], event.rotation); 90 | done(); 91 | }); 92 | 93 | parcel.receiveMsg({ 94 | type: "move", 95 | ...moveMsg, 96 | uuid: playerDetail.uuid, 97 | player: playerDetail, 98 | }); 99 | }); 100 | 101 | it("Player hasEthereumNFT", (done) => { 102 | const p = new Player(playerDetail, parcel); 103 | const success = (result: boolean) => { 104 | expect(result).toBeTruthy(); 105 | done(); 106 | }; 107 | const shouldFail = (reason: string) => { 108 | fail(reason); 109 | }; 110 | p.wallet = "0x0fA074262d6AF761FB57751d610dc92Bac82AEf9"; 111 | // yup we know it's deprecated 112 | p.hasEthereumNFT( 113 | "0x610e6a9a978fc37642bbf73345dcc5def29ade7a", 114 | 53, 115 | success, 116 | shouldFail 117 | ); 118 | }, 30000); 119 | 120 | it("Player hasNFT", (done) => { 121 | const p = new Player(playerDetail, parcel); 122 | const success = (result: boolean) => { 123 | expect(result).toBeTruthy(); 124 | done(); 125 | }; 126 | const shouldFail = (reason: string) => { 127 | fail(reason); 128 | }; 129 | p.wallet = "0x0fA074262d6AF761FB57751d610dc92Bac82AEf9"; 130 | p.hasNFT( 131 | "eth", 132 | "0x610e6a9a978fc37642bbf73345dcc5def29ade7a", 133 | 53, 134 | success, 135 | shouldFail 136 | ); 137 | }, 30000); 138 | }); 139 | -------------------------------------------------------------------------------- /test/test_lib.ts: -------------------------------------------------------------------------------- 1 | //@ts-ignore-file 2 | import { Message, SupportedMessageTypes } from "../src/lib/messages"; 3 | import Parcel from "../src/parcel"; 4 | import { Player } from "../src/player"; 5 | 6 | export const overrideParcel = (parcel:any)=>{ 7 | 8 | parcel.receiveMsg=(obj:Message)=> { 9 | const ws = { 10 | readyState: 1, 11 | player: undefined, 12 | } as unknown as any; 13 | 14 | const data = obj as Message; 15 | if ( 16 | data && 17 | (data.target == "metamask-contentscript" || 18 | data.target == "metamask-inpage" || 19 | data.target == "inpage") 20 | ) { 21 | // ignore metamask messages 22 | return; 23 | } 24 | 25 | if (!Object.values(SupportedMessageTypes).includes(data.type)) { 26 | // invalid message type, ignore 27 | return; 28 | } 29 | 30 | //@ts-ignore 31 | if (!data.player && !data.player._token) { 32 | // no player record in the dataPacket, ignore 33 | return; 34 | } 35 | let oldPlayer = parcel.players.get(data.player._token.toLowerCase()); 36 | 37 | if (oldPlayer) { 38 | // we have an old player (perfect) 39 | ws.player = oldPlayer; 40 | // update player info 41 | ws.player._set(data.player); 42 | // console.log('[Scripting] Welcome back ', oldPlayer.name || oldPlayer.wallet || oldPlayer.uuid) 43 | } else { 44 | // player is non-existant 45 | ws.player = new Player(data.player, parcel); 46 | } 47 | 48 | if (!ws.player) { 49 | console.log("[Scripting] Player non-existant"); 50 | } 51 | 52 | // Message is not "Join", redirect to onMessage. 53 | if (data.type !== SupportedMessageTypes.Join) { 54 | parcel.onMessage(ws, data); 55 | return; 56 | } else { 57 | // Throw join event. 58 | parcel.join(ws.player); 59 | } 60 | 61 | } 62 | return parcel as Parcel &{receiveMsg:(obj:any)=>any} 63 | } -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.test.json" 3 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src"], 3 | "compilerOptions": { 4 | "sourceMap": false, 5 | "lib": ["es2017", "dom"], 6 | "target": "es5", 7 | "typeRoots": ["./node_modules/*"], 8 | "types": ["node"], 9 | "experimentalDecorators": true, 10 | "strict": true, 11 | "stripInternal": true, 12 | "skipLibCheck":true, 13 | "downlevelIteration":true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["./test"], 4 | "compilerOptions": { 5 | "resolveJsonModule":true, 6 | "esModuleInterop": true, 7 | "types": ["@types/jest","jest","node"], 8 | "noEmit": true 9 | } 10 | } -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "entryPoints":"src", 3 | "entryPointStrategy":"expand", 4 | "excludePrivate":true, 5 | "name": "Voxels-Scripting-Engine", 6 | "out": "docs", 7 | "excludeInternal":false, 8 | "exclude":"node_modules/**/*", 9 | "preserveWatchOutput": true 10 | } --------------------------------------------------------------------------------