From daf6ecd40e4cbc4b61853eadccfc9acb5747afca Mon Sep 17 00:00:00 2001 From: j0code <42189560+j0code@users.noreply.github.com> Date: Sat, 23 Mar 2024 03:32:55 +0100 Subject: [PATCH] eslint go brrr let's hope nothing breaks lol changes along the way: - add a lot of new typecheck.ts functions - add TinyError - refactor blockdef/entitydef validate() - use validateProperty to combat duplicated code and eslint complexity - errors have detailed info now - add typechecks to soundList (main.ts) --- .eslintrc.json | 299 ++++ package-lock.json | 1561 +++++++++++++++++ package.json | 8 +- public/blocks.yson | 12 +- src/AttributeList.ts | 15 +- src/Cam.ts | 6 +- src/CreativeInventory.ts | 6 +- src/Graphics.ts | 2 +- src/Input.ts | 6 +- src/Inventory.ts | 59 +- src/Item.ts | 20 +- src/ItemStack.ts | 10 +- src/TinyError.ts | 17 + src/block/Block.ts | 62 +- src/block/ContainerBlock.ts | 4 +- src/defs/Base.ts | 14 +- src/defs/BlockDef.ts | 90 +- src/defs/EntityDef.ts | 37 +- src/defs/ItemDef.ts | 7 +- src/defs/PlayerDef.ts | 34 +- src/dim/Dim2.ts | 13 +- src/dim/Dim3.ts | 13 +- src/entity/Entity.ts | 56 +- src/entity/ItemEntity.ts | 30 +- src/entity/Player.ts | 41 +- src/enums/MenuState.ts | 3 +- src/gui/Button.ts | 26 +- src/gui/state/{game_menu.ts => gameMenu.ts} | 66 +- src/gui/state/ingame.ts | 205 ++- .../state/{ingame_menu.ts => ingameMenu.ts} | 20 +- src/https.d.ts | 5 +- src/main.ts | 171 +- src/sound/AudioFile.ts | 10 +- src/sound/Sound.ts | 19 +- src/texture/Subtexture.ts | 4 +- src/texture/Texture.ts | 7 +- src/util/BoundingBox.ts | 4 +- src/util/Container.ts | 75 +- src/util/DebugScreen.ts | 32 +- src/util/EventEmitter.ts | 23 +- src/util/Hotbar.ts | 24 +- src/util/TextRenderer.ts | 63 +- src/util/interfaces.ts | 12 +- src/util/typecheck.ts | 142 +- src/util/util.ts | 6 +- src/world/World.ts | 124 +- src/world/WorldGenerator.ts | 70 +- 47 files changed, 2847 insertions(+), 686 deletions(-) create mode 100644 .eslintrc.json create mode 100644 src/TinyError.ts rename src/gui/state/{game_menu.ts => gameMenu.ts} (74%) rename src/gui/state/{ingame_menu.ts => ingameMenu.ts} (74%) diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..ff80370 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,299 @@ +{ + "env": { + "node": true, + "es2022": true + }, + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended" + ], + "overrides": [ + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": "latest", + "sourceType": "module" + }, + "plugins": [ + "@typescript-eslint" + ], + "rules": { + "constructor-super": "warn", + "for-direction": "warn", + "getter-return": "warn", + "no-async-promise-executor": "error", + "no-await-in-loop": "error", + "no-class-assign": "error", + "no-compare-neg-zero": "off", + "no-cond-assign": ["error", "always"], + "no-const-assign": "error", + "no-constant-binary-expression": "error", + "no-constant-condition": "error", + "no-constructor-return": "error", + "no-control-regex": "warn", + "no-debugger": "error", + "no-dupe-args": "error", + "no-dupe-class-members": "error", + "no-dupe-else-if": "error", + "no-dupe-keys": "error", + "no-duplicate-case": "off", + "no-duplicate-imports": "error", + "no-empty-character-class": "error", + "no-empty-pattern": "error", + "no-ex-assign": "warn", + "no-fallthrough": "off", + "no-func-assign": "warn", + "no-import-assign": "error", + "no-inner-declarations": ["error", "both"], + "no-invalid-regexp": "error", + "no-loss-of-precision": "error", + "no-misleading-character-class": "error", + "no-new-native-nonconstructor": "error", + "no-new-symbol": "error", + "no-obj-calls": "error", + "no-promise-executor-return": "warn", + "no-prototype-builtins": "off", + + "accessor-pairs": "off", + "arrow-body-style": ["warn", "as-needed"], + "block-scoped-var": "error", + "camelcase": "error", + "capitalized-comments": "off", + "class-methods-use-this": "off", + "complexity": ["warn", 24], + "consistent-return": "off", + "consistent-this": ["warn", "that"], + "curly": ["off", "multi-or-nest"], + "default-case": "off", + "default-case-last": "error", + "default-param-last": "off", + "dot-notation": "error", + "eqeqeq": "off", + "func-name-matching": "off", + "func-names": ["warn", "never"], + "grouped-accessor-pairs": "off", + "guard-for-in": "off", + "id-denylist": "off", + "id-length": ["warn", {"min": 1, "max": 32}], + "id-match": "off", + "init-declarations": "off", + "logical-assignment-operators": ["warn", "always"], + "max-classes-per-file": "off", + "max-lines-per-function": "off", + "max-nested-callbacks": "off", + "max-params": "off", + "max-statements": "off", + "multiline-comment-style": "off", + "new-cap": "off", + "no-alert": "error", + "no-array-constructor": "error", + "no-bitwise": "off", + "no-caller": "error", + "no-case-declarations": "error", + "no-confusing-arrow": ["error", {"allowParens": true, "onlyOneSimpleParam": true}], + "no-console": ["error", {"allow": ["table", "group", "groupEnd", "dir", "warn", "debug"]}], + "no-continue": "off", + "no-delete-var": "error", + "no-div-regex": "warn", + "no-else-return": "error", + "no-empty": "warn", + "no-empty-function": "off", + "no-empty-static-block": "warn", + "no-eq-null": "off", + "no-eval": "error", + "no-extend-native": "error", + "no-extra-bind": "warn", + "no-extra-boolean-cast": "error", + "no-extra-label": "error", + "no-extra-semi": "error", + "no-floating-decimal": "off", + "no-global-assign": "error", + "no-implicit-coercion": "off", + "no-implicit-globals": "warn", + "no-implied-eval": "error", + "no-inline-comments": "off", + "no-invalid-this": "error", + "no-iterator": "error", + "no-label-var": "error", + "no-labels": "off", + "no-lone-blocks": "warn", + "no-lonely-if": "warn", + "no-loop-func": "error", + "no-magic-numbers": "off", + "no-mixed-operators": ["warn", { "groups": [ + ["&", "|", "^", "~", "<<", ">>", ">>>", "?:"], + ["==", "!=", "===", "!==", ">", ">=", "<", "<="], + ["&&", "||", "?:"], + ["in", "instanceof"] + ] }], + "no-multi-assign": "off", + "no-multi-str": "error", + "no-negated-condition": "error", + "no-nested-ternary": "off", + "no-new": "off", + "no-new-func": "error", + "no-new-object": "warn", + "no-new-wrappers": "error", + "no-nonoctal-decimal-escape": "error", + "no-octal": "error", + "no-octal-escape": "error", + "no-param-reassign": "off", + "no-plusplus": "off", + "no-proto": "error", + "no-redeclare": "error", + "no-regex-spaces": "warn", + "no-restricted-exports": "off", + "no-restricted-globals": "off", + "no-restricted-imports": "off", + "no-restricted-properties": "off", + "no-restricted-syntax": "off", + "no-return-assign": "error", + "no-return-await": "warn", + "no-script-url": "error", + "no-sequences": "warn", + "no-shadow": "warn", + "no-shadow-restricted-names": "error", + "no-ternary": "off", + "no-throw-literal": "off", + "no-undef-init": "warn", + "no-undefined": "off", + "no-underscore-dangle": "warn", + "no-unneeded-ternary": "warn", + "no-unused-expressions": "warn", + "no-unused-labels": "error", + "no-useless-call": "error", + "no-useless-catch": "error", + "no-useless-computed-key": "error", + "no-useless-concat": "error", + "no-useless-constructor": "warn", + "no-useless-escape": "error", + "no-useless-rename": "error", + "no-var": "error", + "no-void": "off", + "no-warning-comments": "off", + "no-with": "error", + "object-shorthand": ["warn", "always"], + "one-var": "off", + "one-var-declaration-per-line": "off", + "operator-assignment": ["warn", "always"], + "prefer-arrow-callback": "error", + "prefer-const": "error", + "prefer-destructuring": "warn", + "prefer-exponentiation-operator": "warn", + "prefer-named-capture-group": "off", + "prefer-numeric-literals": "warn", + "prefer-object-has-own": "error", + "prefer-object-spread": "warn", + "prefer-promise-reject-errors": "warn", + "prefer-regex-literals": "off", + "prefer-rest-params": "error", + "prefer-spread": "error", + "prefer-template": "warn", + "quote-props": ["warn", "as-needed"], + "radix": "warn", + "require-await": "warn", + "require-unicode-regexp": "off", + "require-yield": "warn", + "sort-imports": "warn", + "sort-keys": "off", + "sort-vars": "warn", + "spaced-comment": ["warn", "always"], + "strict": "warn", + "symbol-description": "error", + "vars-on-top": "warn", + "yoda": ["warn", "never"], + + "array-bracket-newline": ["error", {"multiline": true}], + "array-bracket-spacing": ["warn", "never"], + "array-element-newline": ["warn", "consistent"], + "arrow-parens": ["warn", "as-needed"], + "arrow-spacing": ["error", {"before": true, "after": true}], + "block-spacing": ["error", "always"], + "brace-style": ["error", "1tbs", {"allowSingleLine": true}], + "comma-dangle": ["warn", "never"], + "comma-spacing": ["error", {"before": false, "after": true}], + "comma-style": ["error", "last"], + "computed-property-spacing": ["error", "never"], + "dot-location": ["error", "property"], + "eol-last": ["error", "always"], + "func-call-spacing": ["error", "never"], + "function-call-argument-newline": ["error", "consistent"], + "function-paren-newline": ["error", "multiline"], + "generator-star-spacing": ["warn", {"before": false, "after": true}], + "implicit-arrow-linebreak": ["error", "beside"], + "indent": ["error", "tab"], + "jsx-quotes": ["warn", "prefer-double"], + "key-spacing": ["warn", { + "beforeColon": false, "afterColon": true, + "mode": "minimum", "align": "value" + }], + "keyword-spacing": ["error", {"before": true, "after": true}], + "line-comment-position": "off", + "linebreak-style": ["error", "unix"], + "lines-around-comment": ["warn", { + "beforeBlockComment": true, + "beforeLineComment": true, + "allowBlockStart": true, + "allowBlockEnd": false, + "allowObjectStart": true, + "allowObjectEnd": false, + "allowArrayStart": true, + "allowArrayEnd": false, + "allowClassStart": false, + "allowClassEnd": false + }], + "lines-between-class-members": ["error", "always", {"exceptAfterSingleLine": true}], + "max-len": ["warn", { + "code": 160, "tabWidth": 2, "comments": 160, "ignoreUrls": true + }], + "max-statements-per-line": ["error", {"max": 1}], + "multiline-ternary": ["warn", "never"], + "new-parens": ["error", "always"], + "newline-per-chained-call": ["error", {"ignoreChainWithDepth": 3}], + "no-extra-parens": "off", + "no-mixed-spaces-and-tabs": ["error", "smart-tabs"], + "no-trailing-spaces": "error", + "no-whitespace-before-property": "error", + "nonblock-statement-body-position": ["error", "beside"], + "object-curly-newline": ["error", {"multiline": true}], + "object-curly-spacing": ["warn", "always"], + "object-property-newline": ["error", {"allowAllPropertiesOnSameLine": true}], + "operator-linebreak": ["error", "after"], + "padded-blocks": ["warn", {"blocks": "never", "classes": "always", "switches": "never"}, {"allowSingleLineBlocks": true}], + "padding-line-between-statements": ["warn", + { "blankLine": "always", "prev": "*", "next": ["return", "throw", "break"] }, + { "blankLine": "always", "prev": "*", "next": "case" }, + { "blankLine": "never", "prev": "case", "next": "*" }, + { "blankLine": "always", "prev": "*", "next": "default" }, + { "blankLine": "never", "prev": "default", "next": "*" }, + { "blankLine": "never", "prev": ["case", "default"], "next": ["case", "default"] }, + { "blankLine": "always", "prev": ["block", "block-like", "class", "function", "iife", "switch", "try", "while"], "next": ["class", "function", "if", "for", "switch", "while"] }, + { "blankLine": "always", "prev": ["class", "function", "if", "for", "switch", "while"], "next": "*" }, + { "blankLine": "any", "prev": "if", "next": "if" }, + { "blankLine": "always", "prev": ["let", "const"], "next": "*" }, + { "blankLine": "any", "prev": ["let", "const"], "next": ["let", "const"] }, + { "blankLine": "any", "prev": "let", "next": ["if", "for", "while"] }, + { "blankLine": "always", "prev": "import", "next": "*" }, + { "blankLine": "never", "prev": "import", "next": "import" } + ], + "quotes": ["error", "double", {"avoidEscape": true, "allowTemplateLiterals": true}], + "rest-spread-spacing": ["error", "never"], + "semi": ["error", "never"], + "semi-spacing": ["error", {"before": false, "after": true}], + "semi-style": ["error", "first"], + "space-before-blocks": ["error", "always"], + "space-before-function-paren": ["error", {"anonymous": "always", "named": "never", "asyncArrow": "always" }], + "space-in-parens": ["error", "never"], + "space-infix-ops": ["off"], + "space-unary-ops": ["error", {"words": true, "nonwords": false}], + "switch-colon-spacing": ["error", {"before": false, "after": true}], + "template-curly-spacing": ["error", "never"], + "template-tag-spacing": ["error", "never"], + "unicode-bom": "off", + "wrap-iife": ["error", "outside", {"functionPrototypeMethods": true}], + "wrap-regex": "warn", + "yield-star-spacing": ["warn", "after"], + + "@typescript-eslint/no-empty-function": "off" + } +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index a7559ea..41e4bee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,13 +11,1526 @@ "dependencies": { "@j0code/yson": "^1.0.4", "typescript": "^5.4.2" + }, + "devDependencies": { + "@typescript-eslint/eslint-plugin": "^7.3.1", + "eslint": "^8.57.0" + } + }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", + "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", + "dev": true + }, "node_modules/@j0code/yson": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@j0code/yson/-/yson-1.0.4.tgz", "integrity": "sha512-6JE1vxGOwi4OMuPqNzvfupgu9aedDA1/KA0QdjhdOv9qvfxcQV3locoxmiHzF7zHDGGTR4FXf8mmRfqLKAJBoQ==" }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "node_modules/@types/semver": { + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", + "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", + "dev": true + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.3.1.tgz", + "integrity": "sha512-STEDMVQGww5lhCuNXVSQfbfuNII5E08QWkvAw5Qwf+bj2WT+JkG1uc+5/vXA3AOYMDHVOSpL+9rcbEUiHIm2dw==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "7.3.1", + "@typescript-eslint/type-utils": "7.3.1", + "@typescript-eslint/utils": "7.3.1", + "@typescript-eslint/visitor-keys": "7.3.1", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.3.1.tgz", + "integrity": "sha512-Rq49+pq7viTRCH48XAbTA+wdLRrB/3sRq4Lpk0oGDm0VmnjBrAOVXH/Laalmwsv2VpekiEfVFwJYVk6/e8uvQw==", + "dev": true, + "peer": true, + "dependencies": { + "@typescript-eslint/scope-manager": "7.3.1", + "@typescript-eslint/types": "7.3.1", + "@typescript-eslint/typescript-estree": "7.3.1", + "@typescript-eslint/visitor-keys": "7.3.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.3.1.tgz", + "integrity": "sha512-fVS6fPxldsKY2nFvyT7IP78UO1/I2huG+AYu5AMjCT9wtl6JFiDnsv4uad4jQ0GTFzcUV5HShVeN96/17bTBag==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.3.1", + "@typescript-eslint/visitor-keys": "7.3.1" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.3.1.tgz", + "integrity": "sha512-iFhaysxFsMDQlzJn+vr3OrxN8NmdQkHks4WaqD4QBnt5hsq234wcYdyQ9uquzJJIDAj5W4wQne3yEsYA6OmXGw==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "7.3.1", + "@typescript-eslint/utils": "7.3.1", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.3.1.tgz", + "integrity": "sha512-2tUf3uWggBDl4S4183nivWQ2HqceOZh1U4hhu4p1tPiIJoRRXrab7Y+Y0p+dozYwZVvLPRI6r5wKe9kToF9FIw==", + "dev": true, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.3.1.tgz", + "integrity": "sha512-tLpuqM46LVkduWP7JO7yVoWshpJuJzxDOPYIVWUUZbW+4dBpgGeUdl/fQkhuV0A8eGnphYw3pp8d2EnvPOfxmQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.3.1", + "@typescript-eslint/visitor-keys": "7.3.1", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.3.1.tgz", + "integrity": "sha512-jIERm/6bYQ9HkynYlNZvXpzmXWZGhMbrOvq3jJzOSOlKXsVjrrolzWBjDW6/TvT5Q3WqaN4EkmcfdQwi9tDjBQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "7.3.1", + "@typescript-eslint/types": "7.3.1", + "@typescript-eslint/typescript-estree": "7.3.1", + "semver": "^7.5.4" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.3.1.tgz", + "integrity": "sha512-9RMXwQF8knsZvfv9tdi+4D/j7dMG28X/wMJ8Jj6eOHyHWwDW4ngQJcqEczSsqIKKjFiLFr40Mnr7a5ulDD3vmw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.3.1", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, + "node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ignore": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/typescript": { "version": "5.4.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.2.tgz", @@ -29,6 +1542,54 @@ "engines": { "node": ">=14.17" } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/package.json b/package.json index 3b13f55..f734c75 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,9 @@ "description": "tiny mc - now even tinier!", "main": "index.js", "scripts": { - "build": "tsc" + "stylefix": "npx eslint ./src/ --fix", + "style": "npx eslint ./src/", + "build": "npm run style && tsc" }, "repository": { "type": "git", @@ -19,5 +21,9 @@ "dependencies": { "@j0code/yson": "^1.0.4", "typescript": "^5.4.2" + }, + "devDependencies": { + "@typescript-eslint/eslint-plugin": "^7.3.1", + "eslint": "^8.57.0" } } diff --git a/public/blocks.yson b/public/blocks.yson index 8c05a7d..6a85240 100644 --- a/public/blocks.yson +++ b/public/blocks.yson @@ -1,10 +1,10 @@ { tiny: { - dirt: { sound_material: "gravel" }, - stone: { sound_material: "stone" }, - water: { type: "fluid", sound_material: "water" }, - grass_block: { sound_material: "grass" }, - chest: { type: "container", sound_material: "wood" }, - torch: { full: false, lightLevel: 15, sound_material: "wood" } + dirt: { soundMaterial: "gravel" }, + stone: { soundMaterial: "stone" }, + water: { type: "fluid", soundMaterial: "water" }, + grass_block: { soundMaterial: "grass" }, + chest: { type: "container", soundMaterial: "wood" }, + torch: { full: false, lightLevel: 15, soundMaterial: "wood" } } } diff --git a/src/AttributeList.ts b/src/AttributeList.ts index e974d07..a92d456 100644 --- a/src/AttributeList.ts +++ b/src/AttributeList.ts @@ -1,5 +1,5 @@ -import EntityDef from "./defs/EntityDef"; -import { type ArrayElement, type HasData } from "./util/interfaces"; +import { type ArrayElement, type HasData } from "./util/interfaces" +import EntityDef from "./defs/EntityDef" const AttributeNames = [ "generic.movement_speed", @@ -16,10 +16,13 @@ export type Attribute = { base: number } -export function isAttribute(data: any): data is Attribute { +export function isAttribute(data: unknown): data is Attribute { if (typeof data != "object" || data == null) return false + + // @ts-expect-error ts is being stupid here (AttributeNames.includes() should accept any string) if (!("name" in data) || typeof data.name != "string" || !AttributeNames.includes(data.name)) return false if (!("base" in data) || typeof data.base != "number") return false + return true } @@ -44,12 +47,14 @@ export default class AttributeList implements HasData { set(name: AttributeName, base: number) { let allowed = false - for (let attr of this.def.attributes) { + for (const attr of this.def.attributes) { if (attr.name == name) { allowed = true + break } } + if (!allowed) return // TODO: error this.list.set(name, base) @@ -59,4 +64,4 @@ export default class AttributeList implements HasData { return Array.from(this.list).map(e => ({ name: e[0], base: e[1] })) } -} \ No newline at end of file +} diff --git a/src/Cam.ts b/src/Cam.ts index a55849f..2d3ad65 100644 --- a/src/Cam.ts +++ b/src/Cam.ts @@ -1,5 +1,5 @@ -import Dim3 from "./dim/Dim3.js"; -import Entity from "./entity/Entity.js"; +import Dim3 from "./dim/Dim3.js" +import Entity from "./entity/Entity.js" export default class Cam { @@ -10,8 +10,10 @@ export default class Cam { if (!entity) { this.entity = null this.pos = new Dim3(0, 0, 0) + return } + this.entity = entity this.pos = entity.position } diff --git a/src/CreativeInventory.ts b/src/CreativeInventory.ts index d646e1e..1227d14 100644 --- a/src/CreativeInventory.ts +++ b/src/CreativeInventory.ts @@ -1,8 +1,8 @@ +import BlockDef from "./defs/BlockDef.js" import Inventory from "./Inventory.js" import Item from "./Item.js" -import ItemStack from "./ItemStack.js" -import BlockDef from "./defs/BlockDef.js" import ItemDef from "./defs/ItemDef.js" +import ItemStack from "./ItemStack.js" export default class CreativeInventory extends Inventory { @@ -19,7 +19,9 @@ export default class CreativeInventory extends Inventory { get(index: number) { if (index >= this.defs.length) return new ItemStack("tiny:air") + const def = this.defs[index] + return new ItemStack(new Item(def), def.maxItemStack) } diff --git a/src/Graphics.ts b/src/Graphics.ts index 7ec361e..2091012 100644 --- a/src/Graphics.ts +++ b/src/Graphics.ts @@ -24,7 +24,7 @@ export default class Graphics { save() { this.ctx.save() } restore() { this.ctx.restore() } - + reset() { this.ctx.reset() this.ctx.imageSmoothingEnabled = false diff --git a/src/Input.ts b/src/Input.ts index d09dd45..23304d4 100644 --- a/src/Input.ts +++ b/src/Input.ts @@ -21,8 +21,10 @@ export default class Input extends EventEmitter { } const before = this.keys.has(e.code) + this.keys.add(e.code) if (!before) this.emit("keydown", e.code) + this.emit("keypress", e.code) e.preventDefault() }) @@ -54,8 +56,8 @@ export default class Input extends EventEmitter { e.preventDefault() }) - window.addEventListener("wheel", e => { - //e.preventDefault() + window.addEventListener("wheel", () => { + // e.preventDefault() }) } diff --git a/src/Inventory.ts b/src/Inventory.ts index 0ff302e..d0c4a50 100644 --- a/src/Inventory.ts +++ b/src/Inventory.ts @@ -1,6 +1,6 @@ -import Item from "./Item.js" -import ItemStack, { type ItemStackData } from "./ItemStack.js" import { type Flatten, type HasData } from "./util/interfaces.js" +import ItemStack, { type ItemStackData } from "./ItemStack.js" +import Item from "./Item.js" export default class Inventory implements HasData { @@ -14,31 +14,24 @@ export default class Inventory implements HasData { this.slots = [] // fill with air (default) - for (let i = 0; i < this.size; i++) { - this.slots[i] = new ItemStack(new Item("tiny:air"), 1) - } + for (let i = 0; i < this.size; i++) this.slots[i] = new ItemStack(new Item("tiny:air"), 1) + // fill slots with data data.forEach(stack => { this.slots[stack.slot] = new ItemStack(stack.item.id, stack.amount) }) - } set(index: number, stack: ItemStack) { - if (index >= 0 && index < this.size) { - this.slots[index] = stack - } else { - throw new Error(`IndexOutOfBounds: Inventory slot ${index} (0..${this.size})`) - } + if (index >= 0 && index < this.size) this.slots[index] = stack + else throw new Error(`IndexOutOfBounds: Inventory slot ${index} (0..${this.size})`) } get(index: number) { - if (index >= 0 && index < this.size) { - return this.slots[index] - } else { - throw new Error(`IndexOutOfBounds: Inventory slot ${index} (0..${this.size})`) - } + if (index >= 0 && index < this.size) return this.slots[index] + + throw new Error(`IndexOutOfBounds: Inventory slot ${index} (0..${this.size})`) } each(cb: (value: ItemStack, index: number) => void) { @@ -46,60 +39,66 @@ export default class Inventory implements HasData { } addItems(stack: ItemStack): ItemStack | null { - let amount = stack.amount + let { amount } = stack + + for (const i in this.slots) { + const slot = this.slots[i] - for (let i in this.slots) { - let slot = this.slots[i] if (amount <= 0) break if (slot.item.id == "tiny:air") { this.slots[i] = new ItemStack(stack.item.id, amount) + return null } if (slot.match(stack)) { - let available = slot.item.maxItemStack - slot.amount - let d = Math.min(available, amount) + const available = slot.item.maxItemStack - slot.amount + const d = Math.min(available, amount) + slot.amount += d amount -= d } } if (amount > 0) return new ItemStack(stack.item.id, amount) + return null } find(item: Item): number { - for (let i in this.slots) { - if (this.slots[i].match(item)) return Number(i) - } + for (const i in this.slots) if (this.slots[i].match(item)) return Number(i) + return -1 } emptySlots(): number { let count = 0 - for (let slot of this.slots) { + for (const slot of this.slots) { if (slot.item.id == "tiny:air") count++ } + return count } firstEmptySlot(): number { - for (let i in this.slots) { - if (this.slots[i].item.id == "tiny:air") return Number(i) - } + for (const i in this.slots) if (this.slots[i].item.id == "tiny:air") return Number(i) + return -1 } getData() { const arr: (ReturnType & {slot: number})[] = [] + this.slots.forEach((slot, index) => { if (slot.item.id == "tiny:air") return - arr.push({...slot.getData(), slot: index}) + + arr.push({ ...slot.getData(), slot: index }) }) + return arr } } -export type InventoryData = Flatten<(ItemStackData & { slot: number })[]> \ No newline at end of file +export type InventoryData = Flatten<(ItemStackData & { slot: number })[]> diff --git a/src/Item.ts b/src/Item.ts index 12a055f..cbbb588 100644 --- a/src/Item.ts +++ b/src/Item.ts @@ -1,14 +1,15 @@ -import ItemDef from "./defs/ItemDef.js" +import { type BaseData, type Flatten, type HasData, type NamespacedId } from "./util/interfaces.js" +import { blockdefs, itemdefs } from "./main.js" import BlockDef from "./defs/BlockDef.js" -import { itemdefs, blockdefs } from "./main.js" -import Block from "./block/Block.js" -import { type HasData, type BaseData, type Flatten, type NamespacedId } from "./util/interfaces.js" +import ItemDef from "./defs/ItemDef.js" import { createBlock } from "./gui/state/ingame.js" export default class Item implements HasData { + // eslint-disable-next-line static fromYSON(data: any) { if (!(data instanceof Object) || typeof data.id != "string") throw new Error("Could not parse Block:", data) + return new Item(data.id) } @@ -17,11 +18,11 @@ export default class Item implements HasData { constructor(def: ItemDef | BlockDef | NamespacedId) { if (def instanceof ItemDef || def instanceof BlockDef) this.def = def else { - let itemdef = itemdefs.get(def) || blockdefs.get(def) + const itemdef = itemdefs.get(def) || blockdefs.get(def) + if (itemdef) this.def = itemdef else { - console.trace() - throw "Item definition not found: " + def + throw new Error(`Item definition not found: ${def}`) } } } @@ -48,13 +49,12 @@ export default class Item implements HasData { getBlock() { if (!(this.def instanceof BlockDef)) throw new Error("Item is not a Block!") + return createBlock(this.def.id) } getData(): ItemData { - return { - id: this.id - } + return { id: this.id } } } diff --git a/src/ItemStack.ts b/src/ItemStack.ts index 2b40c81..2b0efcf 100644 --- a/src/ItemStack.ts +++ b/src/ItemStack.ts @@ -1,6 +1,6 @@ -import Graphics from "./Graphics.js" +import { type HasData, type NamespacedId } from "./util/interfaces.js" import Item, { type ItemData } from "./Item.js" -import { type NamespacedId, type HasData } from "./util/interfaces.js" +import Graphics from "./Graphics.js" export default class ItemStack implements HasData { @@ -24,11 +24,13 @@ export default class ItemStack implements HasData { match(item: Item | ItemStack) { item = item instanceof Item ? item : item.item + return this.item.match(item) } draw(g: Graphics, size: number, hideAmount: boolean = false) { if (this.item.id == "tiny:air") return + this.item.texture?.draw(g, size, size, true) if (this.amount != 1 && !hideAmount) { @@ -36,14 +38,14 @@ export default class ItemStack implements HasData { g.ctx.translate(size * 0.95, size * 0.95) g.ctx.textAlign = "center" g.ctx.textBaseline = "middle" - g.drawText(this.amount + "", { color: "white", font: { size: size/2 } }) + g.drawText(`${this.amount}`, { color: "white", font: { size: size/2 } }) g.restore() } } getData(): ItemStackData { return { - item: this.item.getData(), + item: this.item.getData(), amount: this.amount } } diff --git a/src/TinyError.ts b/src/TinyError.ts new file mode 100644 index 0000000..0ffb45c --- /dev/null +++ b/src/TinyError.ts @@ -0,0 +1,17 @@ +export default class TinyError extends Error { + + constructor(readonly plainMessage: string, cause?: Error | string) { + super(undefined, { cause }) + } + + get message(): string { + return this.toString() + } + + toString() { + const causeMsg = this.cause instanceof Error ? this.cause?.message : this.cause + + return `${this.cause ? `${this.plainMessage}\nCause: ${causeMsg}` : this.plainMessage}` + } + +} diff --git a/src/block/Block.ts b/src/block/Block.ts index 325ae61..3346223 100644 --- a/src/block/Block.ts +++ b/src/block/Block.ts @@ -1,16 +1,18 @@ +import { type BaseData, type Flatten, type HasData, type NamespacedId } from "../util/interfaces.js" +import { blockdefs, debug } from "../main.js" import BlockDef from "../defs/BlockDef.js" import BoundingBox from "../util/BoundingBox.js" import Dim2 from "../dim/Dim2.js" import Dim3 from "../dim/Dim3.js" -import { blockdefs, debug } from "../main.js" import Graphics from "../Graphics.js" -import { type HasData, type BaseData, type Flatten, type NamespacedId } from "../util/interfaces.js" import World from "../world/World.js" export default class Block implements HasData { + // eslint-disable-next-line static fromYSON(data: any) { if (!(data instanceof Object) || typeof data.id != "string") throw new Error("Could not parse Block:", data) + return new Block(data.id) } @@ -21,11 +23,11 @@ export default class Block implements HasData { constructor(def: BlockDef | NamespacedId) { if (def instanceof BlockDef) this.def = def else { - let blockdef = blockdefs.get(def) + const blockdef = blockdefs.get(def) + if (blockdef) this.def = blockdef else { - console.trace() - throw "Block definition not found: " + def + throw new Error(`Block definition not found: ${def}`) } } @@ -77,13 +79,14 @@ export default class Block implements HasData { } playSound(sound: keyof BlockDef["sounds"]) { - //console.log("playSound", sound, this.def.sounds[sound]) + // console.log("playSound", sound, this.def.sounds[sound]) this.def.sounds[sound]?.play() } getBoundingBox(x: number, y: number) { x = Math.floor(x) y = Math.floor(y) + return new BoundingBox(new Dim3(x, y), new Dim2(1, 1)) } @@ -94,9 +97,8 @@ export default class Block implements HasData { draw(g: Graphics, x: number, y: number, z: number) { if (this.def.id == "tiny:air") { - if (debug.showDebugScreen && debug.showAirLightLevel) { - overlayLightLevel(g, x, y, this.lightLevel) - } + if (debug.showDebugScreen && debug.showAirLightLevel) overlayLightLevel(g, x, y, this.lightLevel) + return } @@ -106,9 +108,12 @@ export default class Block implements HasData { let light = this.lightLevel / 15 const FLUID_TRANSLUCENCY = 0.35 + if (this.type == "fluid") g.globalAlpha = 1 - (1 - FLUID_TRANSLUCENCY) * light if (z < 0) light *= 0.75 - //console.log(light) + + + // console.log(light) g.brightness(light) this.texture?.draw(g) @@ -118,7 +123,9 @@ export default class Block implements HasData { getData(x: number, y: number, z: number): BlockData { return { id: this.id, - x, y, z + x, + y, + z } } @@ -137,18 +144,23 @@ function updateSkyLight(world: World, x: number, y: number, z: number, skyLightB // from above const leftBlock = world.getBlock(x-1, y, z) const rightBlock = world.getBlock(x+1, y, z) + if (!leftBlock?.isSolid() || !leftBlock.full) derivLight.push(deriveSkyLight(world, x-1, y+1, z)) + derivLight.push(deriveSkyLight(world, x, y+1, z)) if (!rightBlock?.isSolid() || !rightBlock.full) derivLight.push(deriveSkyLight(world, x+1, y+1, z)) + + // from right/left derivLight.push(deriveSkyLight(world, x-1, y, z)/2) derivLight.push(deriveSkyLight(world, x+1, y, z)/2) - + skyLight = Math.floor(Math.max(...derivLight, 0)) if (skyLight != skyLightBefore) { // right/left world.scheduleBlockUpdate(x-1, y, z) world.scheduleBlockUpdate(x+1, y, z) + // below world.scheduleBlockUpdate(x-1, y-1, z) world.scheduleBlockUpdate(x, y-1, z) @@ -168,6 +180,7 @@ function updateBlockLight(world: World, x: number, y: number, z: number, blockLi derivLight.push(deriveBlockLight(world, x, y-1, z)) derivLight.push(deriveBlockLight(world, x, y+1, z)) } + derivLight.push(deriveBlockLight(world, x, y, z-1)) derivLight.push(deriveBlockLight(world, x, y, z+1)) @@ -179,6 +192,7 @@ function updateBlockLight(world: World, x: number, y: number, z: number, blockLi world.scheduleBlockUpdate(x, y-1, z) world.scheduleBlockUpdate(x, y+1, z) } + world.scheduleBlockUpdate(x, y, z-1) world.scheduleBlockUpdate(x, y, z+1) } @@ -188,24 +202,20 @@ function updateBlockLight(world: World, x: number, y: number, z: number, blockLi function deriveSkyLight(world: World, x: number, y: number, z: number) { const other = world.getBlock(x, y, z) - if (!other) { - return 15 - } else if (other.id == "tiny:air" || !other.full) { - return other.skyLight - } else if (other.isSolid()) { - return 0 // Math.floor(other.skyLight * 0.5) - } else { - return other.skyLight - 1 - } + + if (!other) return 15 + else if (other.id == "tiny:air" || !other.full) return other.skyLight + else if (other.isSolid()) return 0 // Math.floor(other.skyLight * 0.5) + + return other.skyLight - 1 } function deriveBlockLight(world: World, x: number, y: number, z: number) { const other = world.getBlock(x, y, z) - if (!other || (other.isSolid() && other.full)) { - return 0 - } else { - return other.blockLight - 1 - } + + if (!other || (other.isSolid() && other.full)) return 0 + + return other.blockLight - 1 } function overlayLightLevel(g: Graphics, x: number, y: number, level: number) { diff --git a/src/block/ContainerBlock.ts b/src/block/ContainerBlock.ts index d169722..188b636 100644 --- a/src/block/ContainerBlock.ts +++ b/src/block/ContainerBlock.ts @@ -1,7 +1,7 @@ +import Block, { type BlockData } from "./Block.js" +import { type Flatten, type HasInventory, type NamespacedId } from "../util/interfaces.js" import Inventory, { type InventoryData } from "../Inventory.js" import BlockDef from "../defs/BlockDef.js" -import { type NamespacedId, type Flatten, type HasInventory } from "../util/interfaces.js" -import Block, { type BlockData } from "./Block.js" export default class ContainerBlock extends Block implements HasInventory { diff --git a/src/defs/Base.ts b/src/defs/Base.ts index 1ef72ed..2be2802 100644 --- a/src/defs/Base.ts +++ b/src/defs/Base.ts @@ -1,17 +1,18 @@ +import { type NamespacedId } from "../util/interfaces.js" import Texture from "../texture/Texture.js" import { getTexture } from "../main.js" -import { type NamespacedId } from "../util/interfaces.js"; export default class Base { - readonly namespace: string; - readonly idname: string; - readonly texture: Texture | null; + + readonly namespace: string + readonly idname: string + readonly texture: Texture | null constructor(namespace: string, idname: string) { this.namespace = namespace this.idname = idname - - if (["tiny:air","tiny:player","tiny:item"].includes(this.id)) this.texture = null + + if (["tiny:air", "tiny:player", "tiny:item"].includes(this.id)) this.texture = null else this.texture = getTexture(this.assetsPath) } @@ -22,4 +23,5 @@ export default class Base { get assetsPath() { return "" } + } diff --git a/src/defs/BlockDef.ts b/src/defs/BlockDef.ts index aa7b6bd..9dd93ae 100644 --- a/src/defs/BlockDef.ts +++ b/src/defs/BlockDef.ts @@ -1,15 +1,16 @@ -import { getSound } from "../main.js" -import Sound from "../sound/Sound.js" -import { type Flatten } from "../util/interfaces.js" -import { isInteger } from "../util/typecheck.js" +import { equalsAny, isIntInRange, isObject, isPosInt, validateProperty } from "../util/typecheck.js" import Base from "./Base.js" +import { type Flatten } from "../util/interfaces.js" +import Sound from "../sound/Sound.js" +import TinyError from "../TinyError.js" +import { getSound } from "../main.js" export default class BlockDef extends Base { readonly type: "block" | "fluid" | "container" readonly maxItemStack: number readonly full: boolean - readonly sound_material: string + readonly soundMaterial: string readonly lightLevel: number readonly inventorySlots: number | null readonly inventoryColumns: number | null @@ -19,32 +20,34 @@ export default class BlockDef extends Base { place: Sound } - constructor(namespace: string, idname: string, data: any) { + constructor(namespace: string, idname: string, data: unknown) { super(namespace, idname) - if (!validate(data)) throw new Error(`Invalid blockdef for ${namespace}:${idname}: ${JSON.stringify(data)}`) + try { + if (!validate(data)) throw new Error(`Invalid blockdef for ${namespace}:${idname}: ${JSON.stringify(data)}`) + } catch (e) { + // @ts-expect-error e should be an Error + throw new TinyError(`Invalid blockdef for ${namespace}:${idname}`, e) + } this.type = data.type this.maxItemStack = data.maxItemStack - this.sound_material = data.sound_material + this.soundMaterial = data.soundMaterial this.sounds = { - break: getSound(`block.${this.sound_material}.break`), - place: getSound(`block.${this.sound_material}.place`) + break: getSound(`block.${this.soundMaterial}.break`), + place: getSound(`block.${this.soundMaterial}.place`) } as const - if (data.type == "container" || data.type == "block") { - this.full = data.full - } else { - this.full = true - } + if (data.type == "container" || data.type == "block") this.full = data.full + else this.full = true + if (data.type == "container") { this.inventorySlots = data.inventorySlots this.inventoryColumns = data.inventoryColumns } else this.inventorySlots = this.inventoryColumns = null - if (data.type == "block") { - this.lightLevel = data.lightLevel - } else this.lightLevel = 0 + if (data.type == "block") this.lightLevel = data.lightLevel + else this.lightLevel = 0 } get assetsPath() { @@ -67,68 +70,43 @@ export default class BlockDef extends Base { type BlockDefData = Flatten<{ maxItemStack: number, - sound_material: string + soundMaterial: string } & ({ type: "fluid" } | { full: boolean } & ({ type: "block", - lightLevel: number + lightLevel: number } | { type: "container", inventorySlots: number inventoryColumns: number }))> -function validate(data: any): data is BlockDefData { - if (typeof data != "object") return false +function validate(data: unknown): data is BlockDefData { + if (!isObject(data)) throw new Error(`expected an object but got${data}`) - if ("type" in data) { - if (!["block", "fluid", "container"].includes(data.type)) return false - } else { - data.type = "block" // default - } + const allowedTypes = ["block", "fluid", "container"] - if ("maxItemStack" in data) { - if (!isInteger(data.maxItemStack) || data.maxItemStack <= 0) return false - } else { - data.maxItemStack = 128 // default + if (!validateProperty(data, "type", x => equalsAny(allowedTypes, x), "block")) { + return false } - if ("sound_material" in data) { - if (typeof data.sound_material != "string") return false - } else { - data.sound_material = "grass" // default - } + validateProperty(data, "maxItemStack", isPosInt, 128) + validateProperty(data, "soundMaterial", "string", "grass") if (data.type == "container" || data.type == "block") { - if ("full" in data) { - if (typeof data.full != "boolean") return false - } else { - data.full = true // default - } + validateProperty(data, "full", "boolean", true) } if (data.type == "block") { - if ("lightLevel" in data) { - if (!isInteger(data.lightLevel) || data.lightLevel < 0 || data.lightLevel > 15) return false - } else { - data.lightLevel = 0 // default - } + validateProperty(data, "lightLevel", isIntInRange(0, 16), 0) } if (data.type == "container") { - if ("inventorySlots" in data) { - if (!isInteger(data.inventorySlots) || data.inventorySlots <= 0) return false - } else { - data.inventorySlots = 27 // default - } - if ("inventoryColumns" in data) { - if (!isInteger(data.inventoryColumns) || data.inventoryColumns <= 0) return false - } else { - data.inventoryColumns = 9 // default - } + validateProperty(data, "inventorySlots", isPosInt, 27) + validateProperty(data, "inventoryColumns", isPosInt, 9) } return true diff --git a/src/defs/EntityDef.ts b/src/defs/EntityDef.ts index 951b5c0..0f34786 100644 --- a/src/defs/EntityDef.ts +++ b/src/defs/EntityDef.ts @@ -1,5 +1,7 @@ import { Attribute, isAttribute } from "../AttributeList.js" +import { isInRangeIncl, isObject, validateArray, validateProperty } from "../util/typecheck.js" import Base from "./Base.js" +import TinyError from "../TinyError.js" export default class EntityDef extends Base { @@ -7,9 +9,14 @@ export default class EntityDef extends Base { readonly attributes: Attribute[] readonly eyeHeight: number - constructor(namespace: string, idname: string, data: any) { + constructor(namespace: string, idname: string, data: unknown) { super(namespace, idname) - if (!validate(data)) throw new Error(`Invalid entitydef for ${namespace}:${idname}: ${JSON.stringify(data)}`) + try { + if (!validate(data)) throw new Error(`Invalid entitydef for ${namespace}:${idname}: ${JSON.stringify(data)}`) + } catch (e) { + // @ts-expect-error e should be an Error + throw new TinyError(`Invalid entitydef for ${namespace}:${idname}`, e) + } this.hasFriction = data.hasFriction this.attributes = data.attributes @@ -28,28 +35,12 @@ export type EntityDefData = { eyeHeight: number } -function validate(data: any): data is EntityDefData { - if (typeof data != "object" || data == null) return false - if ("hasFriction" in data) { - if (typeof data.hasFriction != "boolean") return false - } else { - data.hasFriction = false - } - - if ("attributes" in data) { - if (!(data.attributes instanceof Array)) return false - for (let attr of data.attributes) { - if (!isAttribute(attr)) return false - } - } else { - data.attributes = [] - } +function validate(data: unknown): data is EntityDefData { + if (!isObject(data)) throw new Error(`expected an object but got${data}`) - if ("eyeHeight" in data) { - if (typeof data.eyeHeight != "number" || data.eyeHeight < 0 || data.eyeHeight > 1) return false - } else { - data.eyeHeight = 0.5 - } + validateProperty(data, "hasFriction", "boolean", false) + validateProperty(data, "attributes", validateArray(isAttribute), []) + validateProperty(data, "eyeHeight", isInRangeIncl(0, 1), 0.5) return true } diff --git a/src/defs/ItemDef.ts b/src/defs/ItemDef.ts index 32e0fb9..09f3ab6 100644 --- a/src/defs/ItemDef.ts +++ b/src/defs/ItemDef.ts @@ -1,11 +1,16 @@ +import { isInteger, isObject, safeValidateProperty } from "../util/typecheck.js" import Base from "./Base.js" export default class ItemDef extends Base { readonly maxItemStack: number - constructor(namespace: string, idname: string, data: any) { + constructor(namespace: string, idname: string, data: unknown) { super(namespace, idname) + if (!isObject(data) || !safeValidateProperty(data, "maxItemStack", isInteger, 128)) { + throw new Error(`Invalid itemdef for ${namespace}:${idname}: ${JSON.stringify(data)}`) + } + this.maxItemStack = data.maxItemStack as number || 128 } diff --git a/src/defs/PlayerDef.ts b/src/defs/PlayerDef.ts index 6b4e16d..b4ff33a 100644 --- a/src/defs/PlayerDef.ts +++ b/src/defs/PlayerDef.ts @@ -4,22 +4,24 @@ export default class PlayerDef extends EntityDef { constructor() { super("tiny", "player", { - attributes: [{ - name: "generic.movement_speed", - base: 1 - }, { - name: "generic.jump_strength", - base: 0.35 - }, { - name: "generic.scale", - base: 1 - }, { - name: "player.block_interaction_range", - base: 4.5 - }, { - name: "player.entity_interaction_range", - base: 5 - }], + attributes: [ + { + name: "generic.movement_speed", + base: 1 + }, { + name: "generic.jump_strength", + base: 0.35 + }, { + name: "generic.scale", + base: 1 + }, { + name: "player.block_interaction_range", + base: 4.5 + }, { + name: "player.entity_interaction_range", + base: 5 + } + ], eyeHeight: 0.6 } satisfies Partial) } diff --git a/src/dim/Dim2.ts b/src/dim/Dim2.ts index 3564605..4dad3f5 100644 --- a/src/dim/Dim2.ts +++ b/src/dim/Dim2.ts @@ -6,7 +6,7 @@ export default class Dim2 implements Dim { static polar(theta: number, scale: number = 1) { return new Dim2(Math.cos(theta) * scale, Math.sin(theta) * scale) } - + x: number y: number @@ -18,18 +18,21 @@ export default class Dim2 implements Dim { add(dim: Dim2 | Dim3): Dim2 { this.x += dim.x this.y += dim.y + return this } sub(dim: Dim2 | Dim3): Dim2 { this.x -= dim.x this.y -= dim.y + return this } mult(dim: Dim2): Dim2 { this.x = this.x * dim.x - this.y * dim.y this.y = this.x * dim.y + this.x * dim.x + return this } @@ -41,19 +44,22 @@ export default class Dim2 implements Dim { this.x = dim this.y = y || 0 } + return this } scale(x: number): Dim2 { this.x *= x this.y *= x + return this } rotate(theta: number): Dim2 { - const {x, y} = this const vec = Dim2.polar(theta) + this.mult(vec) + return this } @@ -75,7 +81,9 @@ export default class Dim2 implements Dim { normalize(): Dim2 { const mag = this.mag() + if (mag == 0) return this + return this.scale(1/mag) } @@ -86,6 +94,7 @@ export default class Dim2 implements Dim { floor(): Dim2 { this.x = Math.floor(this.x) this.y = Math.floor(this.y) + return this } diff --git a/src/dim/Dim3.ts b/src/dim/Dim3.ts index b972614..4cd7477 100644 --- a/src/dim/Dim3.ts +++ b/src/dim/Dim3.ts @@ -2,7 +2,7 @@ import Dim from "./Dim.js" import Dim2 from "./Dim2.js" export default class Dim3 implements Dim { - + x: number y: number z: number @@ -17,16 +17,19 @@ export default class Dim3 implements Dim { this.x += dim.x this.y += dim.y if (dim instanceof Dim3) this.z += dim.z + return this } - + sub(dim: Dim2 | Dim3): Dim3 { this.x -= dim.x this.y -= dim.y if (dim instanceof Dim3) this.z -= dim.z + return this } + // eslint-disable-next-line @typescript-eslint/no-unused-vars mult(dim: Dim3): Dim3 { return this } @@ -41,6 +44,7 @@ export default class Dim3 implements Dim { this.y = y || 0 this.z = z || 0 } + return this } @@ -48,11 +52,13 @@ export default class Dim3 implements Dim { this.x *= x this.y *= x this.z *= x + return this } dot(dim: Dim2 | Dim3): number { if (dim instanceof Dim2) return this.x * dim.x + this.y * dim.y + return this.x * dim.x + this.y * dim.y + this.z * dim.z } @@ -70,7 +76,9 @@ export default class Dim3 implements Dim { normalize(): Dim3 { const mag = this.mag() + if (mag == 0) return this + return this.scale(1/mag) } @@ -82,6 +90,7 @@ export default class Dim3 implements Dim { this.x = Math.floor(this.x) this.y = Math.floor(this.y) this.z = Math.floor(this.z) + return this } diff --git a/src/entity/Entity.ts b/src/entity/Entity.ts index 6d1ea18..2629a73 100644 --- a/src/entity/Entity.ts +++ b/src/entity/Entity.ts @@ -1,13 +1,13 @@ +import { type BaseData, type Flatten, type HasData, type NamespacedId } from "../util/interfaces.js" +import { entitydefs, getSound } from "../main.js" +import AttributeList from "../AttributeList.js" import BoundingBox from "../util/BoundingBox.js" import Dim2 from "../dim/Dim2.js" import Dim3 from "../dim/Dim3.js" import EntityDef from "../defs/EntityDef.js" -import { entitydefs, getSound } from "../main.js" -import World from "../world/World.js" import Graphics from "../Graphics.js" -import { type Flatten, type BaseData, type HasData, type NamespacedId } from "../util/interfaces.js" -import AttributeList from "../AttributeList.js" import Sound from "../sound/Sound.js" +import World from "../world/World.js" let sounds: { splash: Sound, @@ -55,13 +55,14 @@ export default class Entity implements HasData { constructor(def: EntityDef | NamespacedId, spawnTime: number, data: Partial = {}) { if (def instanceof EntityDef) this.def = def else { - let entitydef = entitydefs.get(def) + const entitydef = entitydefs.get(def) + if (entitydef) this.def = entitydef else { - console.trace() - throw "Entity definition not found: " + def + throw new Error(`Entity definition not found: ${def}`) } } + this.position = new Dim3(...(data.position || [0, 0, 0])) this.rotation = data.rotation || 0 this.motion = new Dim3(...(data.motion || [0, 0, 0])) @@ -106,12 +107,16 @@ export default class Entity implements HasData { getBoundingBox() { const size = this.size.copy().scale(this.attributes.get("generic.scale", 1)!) const pos = this.position.copy() + pos.x -= size.x/2 + return new BoundingBox(pos, size) } + // eslint-disable-next-line complexity tick(world: World) { - const {x: pX, y: pY, z: pZ} = this.position // p -> present + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { x: pX, y: pY, z: pZ } = this.position // p -> present const box = this.getBoundingBox() let onGround = false let inFluid = false @@ -120,17 +125,21 @@ export default class Entity implements HasData { loop: for (let y = Math.floor(box.pos.y); y <= Math.ceil(box.corner.y); y++) { for (let x = Math.floor(box.pos.x); x <= Math.ceil(box.corner.x); x++) { const block = world.getBlock(x, y, pZ) + if (block && block.type == "fluid") { if (box.intersect(block?.getBoundingBox(x, y))) { inFluid = true + break loop } } } } - //console.log("inFluid:", inFluid) + + // console.log("inFluid:", inFluid) const motion = this.motion.copy() + Entity.applyGravity(motion, inFluid) const fY = pY + motion.y // f -> future @@ -139,11 +148,13 @@ export default class Entity implements HasData { for (let x = Math.floor(box.pos.x); x <= Math.ceil(box.corner.x); x++) { box.pos.y = y const blockBelow = world.getBlock(x, y, pZ) + if (blockBelow && blockBelow.isSolid()) { if (box.intersect(blockBelow?.getBoundingBox(x, y))) { onGround = true this.motion.y = 0 this.position.y = y + 1 + break loop } } @@ -153,9 +164,8 @@ export default class Entity implements HasData { if (onGround && this.motion.y < 0) this.motion.y = 0 // apply gravity - if (!onGround && !this.noGravity) { - Entity.applyGravity(this.motion, inFluid) - } + if (!onGround && !this.noGravity) Entity.applyGravity(this.motion, inFluid) + // ground friction if ((onGround || inFluid) && this.def.hasFriction) { @@ -170,12 +180,12 @@ export default class Entity implements HasData { if (inFluid) { if (this.motion.sqMag() > 0 && this.swimTicks == 0) sounds.swim.play() + this.swimTicks = (this.swimTicks + 1) % 22 } - if (onGround || inFluid) { - this.airTime = 0 - } else { + if (onGround || inFluid) this.airTime = 0 + else { this.airTime++ } @@ -194,7 +204,8 @@ export default class Entity implements HasData { g.save() g.translate(box.pos.x, box.pos.y) - //g.translate(-box.size.x/2, 0) // to center (x) + + // g.translate(-box.size.x/2, 0) // to center (x) g.brightness(light / 15) this.texture?.draw(g, box.size.x, box.size.y) @@ -206,14 +217,15 @@ export default class Entity implements HasData { world.removeEntity(this) } - getData(world?: World): EntityData { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + getData(w?: World): EntityData { return { - id: this.id, - motion: this.motion.asArray(), - position: this.position.asArray(), // TODO: "pos" alias for compatibility - rotation: this.rotation, + id: this.id, + motion: this.motion.asArray(), + position: this.position.asArray(), // TODO: "pos" alias for compatibility + rotation: this.rotation, noGravity: this.noGravity, - onGround: this.onGround, + onGround: this.onGround, spawnTime: this.spawnTime } } diff --git a/src/entity/ItemEntity.ts b/src/entity/ItemEntity.ts index 00b29a1..0c6b641 100644 --- a/src/entity/ItemEntity.ts +++ b/src/entity/ItemEntity.ts @@ -1,9 +1,9 @@ -import Entity, { type EntityData } from "./Entity.js"; -import EntityDef from "../defs/EntityDef.js"; -import ItemStack, { type ItemStackData } from "../ItemStack.js"; -import World from "../world/World.js"; +import Entity, { type EntityData } from "./Entity.js" +import ItemStack, { type ItemStackData } from "../ItemStack.js" +import EntityDef from "../defs/EntityDef.js" +import { type Flatten } from "../util/interfaces.js" import Player from "./Player.js" -import { type Flatten } from "../util/interfaces.js"; +import World from "../world/World.js" export default class ItemEntity extends Entity { @@ -23,22 +23,26 @@ export default class ItemEntity extends Entity { tick(world: World) { if (this.inFluid) this.motion.y = Entity.TERMINAL_FLUID_VELOCITY + super.tick(world) if (this.motion.sqMag() > 0) this.rotation = this.motion.angle() else if (isNaN(this.rotation)) this.rotation = 0 - + const pickupDelay = Math.max(this.spawnTime + ItemEntity.PICKUP_TIME - world.tickTime, 0) const boundingBox = this.getBoundingBox() // player collision if (pickupDelay <= 0) { const players = world.getAllEntities("tiny:player") - for (let p of players) { + + for (const p of players) { if (boundingBox.intersect(p.getBoundingBox())) { const leftover = p.hotbar.addItems(this.itemstack) + if (leftover) this.itemstack.amount = leftover.amount else { world.removeEntity(this) + return } } @@ -46,17 +50,19 @@ export default class ItemEntity extends Entity { } // combine item entities - const items = world.getAllEntities(entity => { - return entity.id == "tiny:item" && (entity as ItemEntity).itemstack.match(this.itemstack) - }) - for (let item of items) { + const items = world.getAllEntities(entity => entity.id == "tiny:item" && (entity as ItemEntity).itemstack.match(this.itemstack)) + + for (const item of items) { if (item == this) continue + const stack = item.itemstack + if (boundingBox.intersect(item.getBoundingBox())) { if (stack.amount + this.itemstack.amount < this.itemstack.item.maxItemStack) { stack.amount += this.itemstack.amount item.motion.add(this.motion) world.removeEntity(this) + return } } @@ -66,7 +72,7 @@ export default class ItemEntity extends Entity { getData(world: World) { return { ...super.getData(), - item: this.itemstack.getData(), + item: this.itemstack.getData(), pickupDelay: Math.max(this.spawnTime + ItemEntity.PICKUP_TIME - world.tickTime, 0) } } diff --git a/src/entity/Player.ts b/src/entity/Player.ts index 90c5572..4a08f3c 100644 --- a/src/entity/Player.ts +++ b/src/entity/Player.ts @@ -1,16 +1,16 @@ -import Block from "../block/Block.js" import Entity, { EntityData } from "./Entity.js" import Inventory, { InventoryData } from "../Inventory.js" +import ItemStack, { ItemStackData } from "../ItemStack.js" +import { getMousePos, world } from "../gui/state/ingame.js" +import Block from "../block/Block.js" +import Dim2 from "../dim/Dim2.js" +import { type Flatten } from "../util/interfaces.js" import Item from "../Item.js" import { ItemEntityData } from "./ItemEntity.js" -import ItemStack, { ItemStackData } from "../ItemStack.js" -import { getTexture } from "../main.js" import PlayerDef from "../defs/PlayerDef.js" import Texture from "../texture/Texture.js" import World from "../world/World.js" -import { type Flatten } from "../util/interfaces.js" -import { world, getMousePos } from "../gui/state/ingame.js" -import Dim2 from "../dim/Dim2.js" +import { getTexture } from "../main.js" const playerDef = new PlayerDef() @@ -21,7 +21,7 @@ export default class Player extends Entity { #texture: Texture #selectedItemSlot: number readonly hotbar: Inventory - + constructor(skin: string, name: string, spawnTime: number, data: Partial = {}) { super(playerDef, spawnTime, data) this.name = name @@ -41,9 +41,7 @@ export default class Player extends Entity { } set selectedItemSlot(x: number) { - if (x >= 0 && x < this.hotbar.size) { - this.#selectedItemSlot = Math.floor(x) - } + if (x >= 0 && x < this.hotbar.size) this.#selectedItemSlot = Math.floor(x) } get selectedItem() { @@ -51,13 +49,16 @@ export default class Player extends Entity { } addItems(stack: ItemStack) { - let leftover = this.hotbar.addItems(stack) + const leftover = this.hotbar.addItems(stack) + if (leftover) world.spawn("tiny:item", { item: new ItemStack(this.selectedItem.item.id), position: this.position.asArray() }) } pickBlock(block: Block) { - let blockItem = new Item(block.id) + const blockItem = new Item(block.id) + if (this.selectedItem.item.id == block.id || block.id == "tiny:air") return + let index = this.hotbar.find(blockItem) if (index >= 0) this.#selectedItemSlot = index else { // TODO: creative mode only @@ -65,10 +66,7 @@ export default class Player extends Entity { if (index >= 0) { this.#selectedItemSlot = index this.hotbar.set(index, new ItemStack(blockItem)) - } - else { - this.hotbar.set(this.#selectedItemSlot, new ItemStack(blockItem)) - } + } else this.hotbar.set(this.#selectedItemSlot, new ItemStack(blockItem)) } } @@ -78,19 +76,20 @@ export default class Player extends Entity { this.rotation = 0 } - tick(world: World) { - super.tick(world) + tick(w: World) { + super.tick(w) const eyes = this.position.copy().add(new Dim2(0, this.eyeHeight)) + this.rotation = getMousePos().sub(eyes).angle() } getData(): PlayerData { return { ...super.getData(), - selectedItem: this.selectedItem.getData(), + selectedItem: this.selectedItem.getData(), selectedItemSlot: this.selectedItemSlot, - playerName: this.name, - hotbar: this.hotbar.getData() + playerName: this.name, + hotbar: this.hotbar.getData() } } diff --git a/src/enums/MenuState.ts b/src/enums/MenuState.ts index 549437c..92a2189 100644 --- a/src/enums/MenuState.ts +++ b/src/enums/MenuState.ts @@ -1,3 +1,4 @@ +// eslint-disable-next-line no-shadow enum MenuState { MENU, @@ -9,4 +10,4 @@ enum MenuState { } -export default MenuState; \ No newline at end of file +export default MenuState diff --git a/src/gui/Button.ts b/src/gui/Button.ts index 2d5dd2c..eb544e0 100644 --- a/src/gui/Button.ts +++ b/src/gui/Button.ts @@ -1,18 +1,17 @@ -import Graphics from "../Graphics.js" +import { game, getSound, getTexture, input } from "../main.js" +import BoundingBox from "../util/BoundingBox.js" import Dim2 from "../dim/Dim2.js" import Dim3 from "../dim/Dim3.js" -import { game, getSound, getTexture, input } from "../main.js" +import EventEmitter from "../util/EventEmitter.js" +import Graphics from "../Graphics.js" import Sound from "../sound/Sound.js" import Subtexture from "../texture/Subtexture.js" -import Texture from "../texture/Texture.js" -import BoundingBox from "../util/BoundingBox.js" -import EventEmitter from "../util/EventEmitter.js" import TextRenderer from "../util/TextRenderer.js" +import Texture from "../texture/Texture.js" let widgetsTex: Texture let buttonTex: Subtexture let hoverButtonTex: Subtexture -let clickButtonTex: Subtexture let clickSound: Sound export class Button extends EventEmitter { @@ -28,13 +27,12 @@ export class Button extends EventEmitter { widgetsTex = getTexture("tiny/textures/gui/widgets.png") buttonTex = widgetsTex.getSubtexture(0, 66, 200, 20) hoverButtonTex = widgetsTex.getSubtexture(0, 86, 200, 20) - clickButtonTex = widgetsTex.getSubtexture(0, 86, 200, 20) clickSound = getSound("gui.click") } draw(g: Graphics) { const touching = this.isHovered() - const tex = touching ? (false ? clickButtonTex : hoverButtonTex) : buttonTex + const tex = touching ? hoverButtonTex : buttonTex g.save() g.ctx.translate(this.x - this.w/2, this.y + this.h/2) @@ -42,16 +40,19 @@ export class Button extends EventEmitter { g.ctx.textAlign = "center" g.ctx.textBaseline = "middle" TextRenderer.drawText(g.ctx, this.text, this.w/2, this.h/2, { - font: { size: 40 }, + font: { size: 40 }, color: "white" }) - //g.ctx.fillText(this.text, 400, 100) + + // g.ctx.fillText(this.text, 400, 100) g.restore() } isHovered() { - const mouse = input.mouse + const { mouse } = input + mouse.x -= game.width/2 + return this.boundingBox.touch(mouse) } @@ -61,4 +62,5 @@ export class Button extends EventEmitter { this.emit("click") } } -} \ No newline at end of file + +} diff --git a/src/gui/state/game_menu.ts b/src/gui/state/gameMenu.ts similarity index 74% rename from src/gui/state/game_menu.ts rename to src/gui/state/gameMenu.ts index 7820067..2a37e7b 100644 --- a/src/gui/state/game_menu.ts +++ b/src/gui/state/gameMenu.ts @@ -1,14 +1,15 @@ +import * as ingameState from "./ingame.js" +import { game, getTexture, menuState, saveNewWorld, setMenuState } from "../../main.js" +import { Button } from "../Button.js" import type Graphics from "../../Graphics.js" import MenuState from "../../enums/MenuState.js" -import { game, getTexture, menuState, saveNewWorld, setMenuState } from "../../main.js" import TextRenderer from "../../util/TextRenderer.js" -import { Button } from "../Button.js" -import * as ingame_state from "../../gui/state/ingame.js" -import YSON from "https://j0code.github.io/browserjs-yson/main.mjs" -import World from "../../world/World.js" import Texture from "../../texture/Texture.js" +import World from "../../world/World.js" + + +// import YSON from "https://j0code.github.io/browserjs-yson/main.mjs" -let widgetsTex: Texture let logoTex: Texture const singleplayerButton = new Button(0, 200, 800, 80, "Singleplayer") const optionsButton = new Button(0, 300, 800, 80, "Options") @@ -18,27 +19,33 @@ const createWorldButton = new Button(0, 100, 800, 80, "Create World") singleplayerButton.on("click", () => { worldButtons = [] - const worldSaves = JSON.parse(localStorage.getItem("worlds") || "[]"); + const worldSaves = JSON.parse(localStorage.getItem("worlds") || "[]") + for (let i = 0; i < worldSaves.length; i++) { const worldSave = worldSaves[i] const button = new Button(0, 200 + i * 100, 800, 80, worldSave.name) + button.on("click", () => { const world = World.load(worldSave.name, worldSave.stringBlocks, worldSave.blockData, worldSave.dims, worldSave.entities) - if(!world) { - alert("Failed to load world!") + + if (!world) { + console.warn("Failed to load world!", worldSave) + return } - ingame_state.loadWorld(world, worldSave.players[0]) + + ingameState.loadWorld(world, worldSave.players[0]) setMenuState(MenuState.INGAME) - world.spawn(ingame_state.player) + world.spawn(ingameState.player) }) worldButtons.push(button) } - setMenuState(MenuState.WORLDSELECTION); + + setMenuState(MenuState.WORLDSELECTION) }) optionsButton.on("click", () => { - console.log("button 2 clicked") + console.debug("button 2 clicked") }) quitButton.on("click", () => { @@ -50,14 +57,14 @@ quitButton.on("click", () => { }) createWorldButton.on("click", () => { - console.log("creating new world") - const world = ingame_state.createWorld(prompt("World name") || "New World") + // eslint-disable-next-line no-alert + const world = ingameState.createWorld(prompt("World name") || "New World") + saveNewWorld(world) setMenuState(MenuState.INGAME) }) export function loadTexture() { - widgetsTex = getTexture("tiny/textures/gui/widgets.png") logoTex = getTexture("tiny/textures/gui/title/logo.png") } @@ -70,7 +77,8 @@ export function draw(g: Graphics) { // graphics.ctx.fillRect(0, 0, game.width, game.height) // graphics.ctx.imageSmoothingEnabled = false - const ctx = g.ctx + const { ctx } = g + ctx.reset() ctx.imageSmoothingEnabled = false @@ -80,8 +88,8 @@ export function draw(g: Graphics) { ctx.translate(game.width/2, 0) drawLogo(g) - if(menuState == MenuState.MENU) drawMainMenu(g) - else if(menuState == MenuState.WORLDSELECTION) drawWorldSelection(g) + if (menuState == MenuState.MENU) drawMainMenu(g) + else if (menuState == MenuState.WORLDSELECTION) drawWorldSelection(g) } function drawLogo(g: Graphics) { @@ -103,27 +111,25 @@ function drawMainMenu(g: Graphics) { function drawWorldSelection(g: Graphics) { createWorldButton.draw(g) - if(worldButtons.length == 0) { + if (worldButtons.length == 0) { TextRenderer.drawText(g.ctx, "No worlds found", 0, 400, { - font: { size: 40 }, + font: { size: 40 }, color: "white" }) + return } - for (let button of worldButtons) { - button.draw(g) - } + + for (const button of worldButtons) button.draw(g) } export function onClick(button: number) { - if(menuState == MenuState.MENU) { + if (menuState == MenuState.MENU) { singleplayerButton.click(button) optionsButton.click(button) quitButton.click(button) - } else if(menuState == MenuState.WORLDSELECTION) { + } else if (menuState == MenuState.WORLDSELECTION) { createWorldButton.click(button) - for (let btn of worldButtons) { - btn.click(button) - } + for (const btn of worldButtons) btn.click(button) } -} \ No newline at end of file +} diff --git a/src/gui/state/ingame.ts b/src/gui/state/ingame.ts index 610241f..31eabdd 100644 --- a/src/gui/state/ingame.ts +++ b/src/gui/state/ingame.ts @@ -1,18 +1,18 @@ -import Cam from "../../Cam.js" -import CreativeInventory from "../../CreativeInventory.js" -import type Graphics from "../../Graphics.js" -import ItemStack from "../../ItemStack.js" import Block, { BlockData } from "../../block/Block.js" -import ContainerBlock from "../../block/ContainerBlock.js" -import Dim2 from "../../dim/Dim2.js" import Entity, { EntityData } from "../../entity/Entity.js" import ItemEntity, { ItemEntityData, isItemEntityData } from "../../entity/ItemEntity.js" import Player, { PlayerData } from "../../entity/Player.js" -import MenuState from "../../enums/MenuState.js" import { blockdefs, cursors, debug, game, input, menuState } from "../../main.js" +import Cam from "../../Cam.js" import Container from "../../util/Container.js" +import ContainerBlock from "../../block/ContainerBlock.js" +import CreativeInventory from "../../CreativeInventory.js" import DebugScreen from "../../util/DebugScreen.js" +import Dim2 from "../../dim/Dim2.js" +import type Graphics from "../../Graphics.js" import Hotbar from "../../util/Hotbar.js" +import ItemStack from "../../ItemStack.js" +import MenuState from "../../enums/MenuState.js" import { NamespacedId } from "../../util/interfaces.js" import World from "../../world/World.js" import WorldGenerator from "../../world/WorldGenerator.js" @@ -32,11 +32,12 @@ export function createWorld(name: string): World { cam = new Cam(player) WorldGenerator.flat(world) world.spawn(player) + return world } export function loadWorld(newWorld: World, playerData: PlayerData) { - //console.log(playerData) + // console.log(playerData) world = newWorld player = new Player("jens", "TinyJens", playerData.spawnTime, playerData) cam = new Cam(player) @@ -65,9 +66,8 @@ export function draw(g: Graphics) { world.draw(g) // hitboxes - if (debug.showDebugScreen && debug.showHitboxes) { - world.drawBoundingBoxes(g) - } + if (debug.showDebugScreen && debug.showHitboxes) world.drawBoundingBoxes(g) + // origin / axis if (debug.showDebugScreen && debug.showOrigin) { @@ -84,7 +84,7 @@ export function draw(g: Graphics) { const reachable = isBlockReachable(mouseBlock) // block highlight - if(menuState == MenuState.INGAME) { + if (menuState == MenuState.INGAME) { g.save() g.translate(mouseBlock.x, mouseBlock.y) @@ -99,6 +99,7 @@ export function draw(g: Graphics) { // distance and player range (debug) if (debug.showDebugScreen && debug.showRange) { const range = (player.attributes.get("player.block_interaction_range", 0)!) * blockSize + g.lineWidth = 2 g.strokeStyle = "white" g.fillStyle = "white" @@ -113,7 +114,7 @@ export function draw(g: Graphics) { blockpos.scale(blockSize) playerpos.scale(blockSize) - const {x, y} = playerpos + const { x, y } = playerpos g.ctx.beginPath() g.ctx.ellipse(x, -y, range, range, 0, 0, 2 * Math.PI) @@ -130,26 +131,23 @@ export function draw(g: Graphics) { g.restore() // hotbar - { - Hotbar.drawHotbar(g) - } + Hotbar.drawHotbar(g) // container - { - Container.drawContainer(g) - } + Container.drawContainer(g) // cursor - if(menuState == MenuState.INGAME) { - const {x, y} = getMousePos() + if (menuState == MenuState.INGAME) { + const { x, y } = getMousePos() const size = blockSize/2 + g.save() g.translate(gameOffset.x, gameOffset.y) // move game by offset g.translate(-cam.x, -cam.y) // move game into view g.translate(x, y) const floatingStack = Container.floatingStack() - const {block: frontBlock, z: frontZ} = getFirstBlock(world, x, y) + const { block: frontBlock, z: frontZ } = getFirstBlock(world, x, y) const z = input.keyPressed("ShiftLeft") ? -1 : 0 const targetBlock = world.getBlock(x, y, z) const inaccessible = z < frontZ && frontBlock?.full @@ -163,7 +161,7 @@ export function draw(g: Graphics) { player.selectedItem.item.texture?.draw(g, size, size, true) } else if (targetBlock?.type == "container") { g.ctx.translate(-size/2, -size) - cursors.open_container.draw(g, size, size, true) + cursors.openContainer.draw(g, size, size, true) } else { g.strokeStyle = reachable ? "white" : "#707070" g.lineWidth = 2 @@ -171,24 +169,20 @@ export function draw(g: Graphics) { g.ctx.moveTo(0, -size/3) g.ctx.lineTo(0, size/3) g.ctx.moveTo(-size/3, 0) - g.ctx.lineTo( size/3, 0) + g.ctx.lineTo(size/3, 0) g.ctx.stroke() } - + g.restore() } // debug screen - if (debug.showDebugScreen) { - DebugScreen.draw(g, world, player) - } - + if (debug.showDebugScreen) DebugScreen.draw(g, world, player) } export function onKey(key: string) { - if (Container.showingInventory()) { - if (Container.onKey(key)) return - } + if (Container.showingInventory()) if (Container.onKey(key)) return + if (key == "Digit1") player.selectedItemSlot = 0 if (key == "Digit2") player.selectedItemSlot = 1 @@ -199,40 +193,38 @@ export function onKey(key: string) { inv: if (key == "KeyE") { // open inventory under mouse if (Container.showingInventory()) { Container.setInventory() + break inv } + openInventory() } if (key == "KeyC") { // temp creative inv const items = Array.from(blockdefs.values()) + items.pop() const inv = new CreativeInventory(items) + Container.setInventory(inv) } - if (key == "F3") { - debug.showDebugScreen = !debug.showDebugScreen - } + if (key == "F3") debug.showDebugScreen = !debug.showDebugScreen + if (input.keyPressed("F3") && debug.showDebugScreen) { + if (key == "KeyN") debug.showRange = !debug.showRange - if (key == "KeyN") { - debug.showRange = !debug.showRange - } if (key == "KeyM") { debug.showHitboxes = !debug.showHitboxes debug.showOrigin = debug.showHitboxes } - if (key == "KeyK") { - debug.showAirLightLevel = !debug.showAirLightLevel - } - + if (key == "KeyK") debug.showAirLightLevel = !debug.showAirLightLevel } - /*if (key == "KeyZ") { + /* if (key == "KeyZ") { const worldSave = world.save() console.log("entities:", worldSave.entities) console.log("players:", worldSave.players) @@ -243,75 +235,82 @@ export function onKey(key: string) { } export function whileKey(key: string) { - if (input.keyPressed("KeyQ")) { + if (key == "KeyQ") { const stack = player.selectedItem const index = player.selectedItemSlot + if (stack.item.id == "tiny:air") return const entityData = { position: player.eyes.asArray(), - motion: Dim2.polar(player.rotationAngle, 0.6).asArray() + motion: Dim2.polar(player.rotationAngle, 0.6).asArray() } let dropStack = stack - if (!input.keyPressed("ControlLeft")) { - dropStack = new ItemStack(stack.item.id) - } + if (!input.keyPressed("ControlLeft")) dropStack = new ItemStack(stack.item.id) + - if (input.keyPressed("ControlLeft") || stack.amount <= 1) { - player.hotbar.set(index, new ItemStack("tiny:air")) - } else stack.amount-- + if (input.keyPressed("ControlLeft") || stack.amount <= 1) player.hotbar.set(index, new ItemStack("tiny:air")) + else stack.amount-- world.spawn("tiny:item", { ...entityData, item: dropStack }) } } export function onClick(button: number) { - if (Container.showingInventory()) { - if (Container.onClick(button)) return - } + if (Container.showingInventory()) if (Container.onClick(button)) return + const mouseBlock = getMouseBlock() - const {x, y} = mouseBlock + const { x, y } = mouseBlock let z = input.keyPressed("ShiftLeft") ? -1 : 0 - const {block: frontBlock, z: frontZ} = getFirstBlock(world, x, y) + const { block: frontBlock, z: frontZ } = getFirstBlock(world, x, y) const reachable = isBlockReachable(mouseBlock) switch (button) { - case 0: - if (!reachable) break - if (z < frontZ && frontBlock?.full) break // inaccessible - world.clearBlock(x, y, z, false) - break - case 1: - if (!frontBlock || frontBlock.id == "tiny:air") break - player.pickBlock(frontBlock) - break - case 2: - if (!reachable) break - const stack = player.selectedItem - if (z != 0 && stack.item.getBlock().mainLayerOnly()) z = 0 - if (z < frontZ && frontBlock?.full) break // inaccessible - - const currentBlock = world.getBlock(x, y, z) - if (currentBlock && !currentBlock.isSolid() && stack.item.isBlock()) { - world.setBlock(x, y, z, stack.item.getBlock(), {}, false) - } else { - openInventory() - } - break + case 0: + if (!reachable) break + if (z < frontZ && frontBlock?.full) break // inaccessible + + world.clearBlock(x, y, z, false) + + break + case 1: + if (!frontBlock || frontBlock.id == "tiny:air") break + + player.pickBlock(frontBlock) + + break + case 2: { + if (!reachable) break + + const stack = player.selectedItem + + if (z != 0 && stack.item.getBlock().mainLayerOnly()) z = 0 + if (z < frontZ && frontBlock?.full) break // inaccessible + + const currentBlock = world.getBlock(x, y, z) + + if (currentBlock && !currentBlock.isSolid() && stack.item.isBlock()) world.setBlock(x, y, z, stack.item.getBlock(), {}, false) + else openInventory() + + break + } } } export function onMouseMove() { const mouse = getMousePos() const items = world.getAllEntities("tiny:item") - for (let item of items) { + + for (const item of items) { if (item.getBoundingBox().touch(mouse)) { const reachable = item.position.distanceTo(player.position) <= player.attributes.get("player.entity_interaction_range")! + if (!reachable) continue const leftover = player.hotbar.addItems(item.itemstack) + world.removeEntity(item) if (leftover) world.spawn("tiny:item", { item: leftover.getData(), position: player.position.asArray() }) } @@ -324,56 +323,68 @@ export function getMouseBlock() { export function getMousePos() { return new Dim2( - (input.mouseX - game.width/2 + cam.x*blockSize) / blockSize - gameOffset.x, + (input.mouseX - game.width/2 + cam.x*blockSize) / blockSize - gameOffset.x, -(input.mouseY - game.height/2 - cam.y*blockSize) / blockSize - gameOffset.y ) } function isBlockReachable(pos: Dim2) { - return pos.copy().add(new Dim2(0.5, 0.5)).distanceTo(player.position.copy().add(new Dim2(0, player.eyeHeight))) <= player.attributes.get("player.block_interaction_range", 0)! + return pos.copy().add(new Dim2(0.5, 0.5)) + .distanceTo(player.position.copy() + .add(new Dim2(0, player.eyeHeight))) <= player.attributes.get("player.block_interaction_range", 0)! } function openInventory() { - const {x, y} = getMouseBlock() + const { x, y } = getMouseBlock() const block = world.getBlock(x, y, 0) + if (!block?.hasInventory()) return + Container.setInventory((block as ContainerBlock).inventory) } -export function getFirstBlock(world: World, x: number, y: number, startZ: number = world.maxZ, predicate?: (block: Block) => boolean) { - startZ = Math.min(startZ, world.maxZ) - for (let z = startZ; z >= world.minZ; z--) { - const block = world.getBlock(x, y, z) +export function getFirstBlock(w: World, x: number, y: number, startZ: number = w.maxZ, predicate?: (block: Block) => boolean) { + startZ = Math.min(startZ, w.maxZ) + for (let z = startZ; z >= w.minZ; z--) { + const block = w.getBlock(x, y, z) + if (!block || !block.isSolid()) continue if (predicate && !predicate(block)) continue + return { block, z } } - return { block: undefined, z: world.minZ } + + return { block: undefined, z: w.minZ } } -export function getFirstFluid(world: World, x: number, y: number, startZ: number = world.maxZ) { - startZ = Math.min(startZ, world.maxZ) - for (let z = startZ; z >= world.minZ; z--) { - const block = world.getBlock(x, y, z) +export function getFirstFluid(w: World, x: number, y: number, startZ: number = w.maxZ) { + startZ = Math.min(startZ, w.maxZ) + for (let z = startZ; z >= w.minZ; z--) { + const block = w.getBlock(x, y, z) + if (!block || block.id == "tiny:air") continue + return { block, z } } - return { block: undefined, z: world.minZ } + + return { block: undefined, z: w.minZ } } export function createBlock(id: NamespacedId, data: Partial = {}) { const blockdef = blockdefs.get(id) + if (!blockdef) { - console.trace() - throw "Block definition not found: " + id + throw new Error(`Block definition not found: ${id}`) } if (blockdef.type == "container") return new ContainerBlock(blockdef, data) - else return new Block(blockdef) + + return new Block(blockdef) } export function createEntity(id: NamespacedId, spawnTime: number, data: Partial = {}) { data.id = id if (isItemEntityData(data, id)) return new ItemEntity(null, spawnTime, data) - else return new Entity(id, spawnTime, data) -} \ No newline at end of file + + return new Entity(id, spawnTime, data) +} diff --git a/src/gui/state/ingame_menu.ts b/src/gui/state/ingameMenu.ts similarity index 74% rename from src/gui/state/ingame_menu.ts rename to src/gui/state/ingameMenu.ts index 4e0b72a..40f6367 100644 --- a/src/gui/state/ingame_menu.ts +++ b/src/gui/state/ingameMenu.ts @@ -1,8 +1,8 @@ -import Graphics from "../../Graphics"; -import MenuState from "../../enums/MenuState.js"; -import { game, setMenuState } from "../../main.js"; -import TextRenderer from "../../util/TextRenderer.js"; -import { Button } from "../Button.js"; +import { game, setMenuState } from "../../main.js" +import { Button } from "../Button.js" +import Graphics from "../../Graphics.js" +import MenuState from "../../enums/MenuState.js" +import TextRenderer from "../../util/TextRenderer.js" const continueButton = new Button(0, 300, 800, 80, "Continue") const optionsButton = new Button(0, 400, 800, 80, "Options") @@ -18,19 +18,21 @@ quitButton.on("click", () => { export function draw(g: Graphics) { // Reset - const ctx = g.ctx + const { ctx } = g + ctx.reset() ctx.imageSmoothingEnabled = false ctx.fillStyle = "#78A7FF" ctx.fillRect(0, 0, game.width, game.height) - + ctx.translate(game.width/2, 0) + // ctx.textAlign = "center" TextRenderer.drawText(ctx, "Saved game!", 0, 90, { - font: { size: 50 }, + font: { size: 50 }, color: "white" }) continueButton.draw(g) @@ -42,4 +44,4 @@ export function onClick(button: number) { continueButton.click(button) optionsButton.click(button) quitButton.click(button) -} \ No newline at end of file +} diff --git a/src/https.d.ts b/src/https.d.ts index d2f0324..b96cef6 100644 --- a/src/https.d.ts +++ b/src/https.d.ts @@ -1,2 +1,3 @@ -declare module 'https://*' -// needed so ts won't panik \ No newline at end of file +declare module "https://*" + +// needed so ts won't panik diff --git a/src/main.ts b/src/main.ts index 251fc22..d9b07de 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,24 +1,26 @@ -import YSON from "https://j0code.github.io/browserjs-yson/main.mjs" +import * as gameMenuState from "./gui/state/gameMenu.js" +import * as ingameMenuState from "./gui/state/ingameMenu.js" +import * as ingameState from "./gui/state/ingame.js" +import { type NamespacedId, SoundDef } from "./util/interfaces.js" +import { isObject, validateArray, validateProperty, validateRecord } from "./util/typecheck.js" +import { $ } from "./util/util.js" +import AudioFile from "./sound/AudioFile.js" import BlockDef from "./defs/BlockDef.js" -import Texture from "./texture/Texture.js" -import ItemDef from "./defs/ItemDef.js" +import { Button } from "./gui/Button.js" +import Container from "./util/Container.js" +import Entity from "./entity/Entity.js" +import EntityDef from "./defs/EntityDef.js" +import Graphics from "./Graphics.js" import Hotbar from "./util/Hotbar.js" import Input from "./Input.js" -import EntityDef from "./defs/EntityDef.js" -import { $ } from "./util/util.js" -import Container from "./util/Container.js" +import ItemDef from "./defs/ItemDef.js" import MenuState from "./enums/MenuState.js" -import * as game_menu_state from "./gui/state/game_menu.js" -import * as ingame_menu_state from "./gui/state/ingame_menu.js" -import * as ingame_state from "./gui/state/ingame.js" -import { Button } from "./gui/Button.js" -import { type NamespacedId } from "./util/interfaces.js" -import AudioFile from "./sound/AudioFile.js" import Sound from "./sound/Sound.js" -import Graphics from "./Graphics.js" +import Texture from "./texture/Texture.js" import World from "./world/World.js" -import Entity from "./entity/Entity.js" +import YSON from "https://j0code.github.io/browserjs-yson/main.mjs" +// eslint-disable-next-line no-console console.log("Never Gonna Give You Up") // game info @@ -28,21 +30,28 @@ export const GAME_VERSION_BRANCH = "main" // TODO: determine from git // canvas export const game = $("#game") -const g = new Graphics(game, ingame_state.blockSize) +const g = new Graphics(game, ingameState.blockSize) // assets const textures: Map = new Map() -export const cursors = { - open_container: getTexture("tiny/textures/gui/cursors/open_container.png") -} satisfies Record -export const soundList = await fetch("./assets/tiny/sounds.json").then(res => res.json()) + +export const cursors = { openContainer: getTexture("tiny/textures/gui/cursors/open_container.png") } satisfies Record +export const soundList: Record = await fetch("./assets/tiny/sounds.json").then(res => res.json()) +validateRecord( // should capture Record + "string", + (value): value is object => isObject(value) && validateProperty( + value, + "sounds", + validateArray((e): e is unknown => typeof e == "string" || (isObject(e) && validateProperty(e, "name", "string"))) + ) +)(soundList) const audioFiles: Map = new Map() const sounds: Map = new Map() // defs -export const blockdefs = await loadDefs("blocks.yson", BlockDef) -export const itemdefs = await loadDefs("items.yson", ItemDef) -export const entitydefs = await loadDefs("entities.yson", EntityDef) +export const blockdefs = await loadDefs("blocks.yson", BlockDef) +export const itemdefs = await loadDefs("items.yson", ItemDef) +export const entitydefs = await loadDefs("entities.yson", EntityDef) blockdefs.set("tiny:air", new BlockDef("tiny", "air", {})) // other @@ -54,7 +63,7 @@ export function setMenuState(state: MenuState) { menuState = state } -game_menu_state.loadTexture() +gameMenuState.loadTexture() Button.loadAssets() Hotbar.loadTexture() Container.loadAssets() @@ -66,25 +75,33 @@ export const perf = { get mspt(): number { if (perf.tick.length <= 0) return 0 + let sum = 0 - perf.tick.forEach(v => sum += v) + + perf.tick.forEach(v => (sum += v)) + return sum / perf.tick.length }, get mspf(): number { if (perf.draw.length <= 0) return 0 + let sum = 0 - perf.draw.forEach(v => sum += v) + + perf.draw.forEach(v => (sum += v)) + return sum / perf.draw.length }, get tps(): number { if (perf.mspt <= 0) return 0 + return Math.min(1000 / perf.mspt, tickTarget) }, get fps(): number { if (perf.mspf <= 0) return 0 + return Math.min(1000 / perf.mspf, drawTarget) } } @@ -92,141 +109,143 @@ export const tickTarget = 20 export const drawTarget = 20 const perfTick = perfRun("tick", tick, 1000/tickTarget) const perfDraw = perfRun("draw", draw, 1000/drawTarget) + setInterval(perfTick, 1000/tickTarget) setInterval(() => requestAnimationFrame(perfDraw), 1000/drawTarget) -function perfRun(name: "tick" | "draw", fn: Function, target: number) { +function perfRun(name: "tick" | "draw", fn: () => void, target: number) { return () => { const before = performance.now() + fn() const timeElapsed = performance.now() - before + if (timeElapsed >= 1000) console.warn(`game lagging: last ${name} took ${timeElapsed}ms`) else if (timeElapsed >= target) console.debug(`%cgame lagging: last ${name} took ${timeElapsed}ms`, "color: skyblue") + perf[name].push(timeElapsed) if (perf[name].length > 50) perf[name].shift() } } -async function loadDefs(path: string, cls: any): Promise> { - let data = await YSON.load(path) - let defs = new Map() - let namespaces = Object.keys(data) +async function loadDefs(path: string, cls: { new(ns: string, id: string, data: unknown): T }): Promise> { + const data = await YSON.load(path) + const defs = new Map() + const namespaces = Object.keys(data) - for (let ns of namespaces) { - let ids = Object.keys(data[ns]) + for (const ns of namespaces) { + const ids = Object.keys(data[ns]) - for (let id of ids) { - defs.set(`${ns}:${id}`, new cls(ns, id, data[ns][id])) - } + for (const id of ids) defs.set(`${ns}:${id}`, new cls(ns, id, data[ns][id])) } + return defs } export function getTexture(path: string) { let texture = textures.get(path) - if (texture) { - if (texture.state != Texture.FAILED) return texture - } + if (texture) if (texture.state != Texture.FAILED) return texture + texture = new Texture(path) textures.set(path, texture) + return texture } export function getAudioFile(path: string) { let audio = audioFiles.get(path) - if (audio) { - if (audio.state != AudioFile.FAILED) return audio - } + if (audio) if (audio.state != AudioFile.FAILED) return audio + audio = new AudioFile(path) audioFiles.set(path, audio) + return audio } export function getSound(key: string) { let sound = sounds.get(key) if (sound) return sound + sound = new Sound(key) sounds.set(key, sound) + return sound } function tick() { - if (menuState == MenuState.INGAME) ingame_state.tick() + if (menuState == MenuState.INGAME) ingameState.tick() } function draw() { game.width = innerWidth game.height = innerHeight - game.style.width = innerWidth + "px" - game.style.height = innerHeight + "px" + game.style.width = `${innerWidth}px` + game.style.height = `${innerHeight}px` game.style.cursor = (menuState == MenuState.INGAME) ? "none" : "initial" - if (menuState == MenuState.MENU || menuState == MenuState.WORLDSELECTION) game_menu_state.draw(g) - else if (menuState == MenuState.INGAME) ingame_state.draw(g) + if (menuState == MenuState.MENU || menuState == MenuState.WORLDSELECTION) gameMenuState.draw(g) + else if (menuState == MenuState.INGAME) ingameState.draw(g) else if (menuState == MenuState.INGAME_MENU) { - // ingame_state.draw(g) - ingame_menu_state.draw(g) - } - else alert("unknown menu state") + // ingameState.draw(g) + ingameMenuState.draw(g) + } else throw new Error("unknown menu state") } export function saveGame() { if (menuState != MenuState.INGAME && menuState != MenuState.INGAME_MENU) return - if (!ingame_state.world) return - const currentWorlds = JSON.parse(localStorage.getItem("worlds") || "[]"); - let currentWorldIndex = currentWorlds.findIndex((worldSave: { name: string; data: any; }) => worldSave.name == ingame_state.world.name); + if (!ingameState.world) return + + const currentWorlds = JSON.parse(localStorage.getItem("worlds") || "[]") + const currentWorldIndex = currentWorlds.findIndex((worldSave: { name: string, data: unknown }) => worldSave.name == ingameState.world.name) + currentWorlds[currentWorldIndex] = { ...currentWorlds[currentWorldIndex], - ...ingame_state.world.save() + ...ingameState.world.save() } localStorage.setItem("worlds", JSON.stringify(currentWorlds)) - console.log("Game saved"); } export function saveNewWorld(world: World) { - const currentWorlds = JSON.parse(localStorage.getItem("worlds") || "[]"); - currentWorlds.push({ - ...world.save() - }) + const currentWorlds = JSON.parse(localStorage.getItem("worlds") || "[]") + + currentWorlds.push({ ...world.save() }) localStorage.setItem("worlds", JSON.stringify(currentWorlds)) } window.addEventListener("beforeunload", () => { saveGame() -}); +}) input.on("keydown", (key: string) => { if (key == "F11") { if (document.fullscreenElement) document.exitFullscreen() else game.requestFullscreen() + return - } else if(key == "Escape" && (menuState == MenuState.INGAME || menuState == MenuState.INGAME_MENU)) { + } else if (key == "Escape" && (menuState == MenuState.INGAME || menuState == MenuState.INGAME_MENU)) { menuState = menuState == MenuState.INGAME ? MenuState.INGAME_MENU : MenuState.INGAME - if(menuState == MenuState.INGAME_MENU) { - saveGame() - } - } else if(key == "Escape" && menuState == MenuState.WORLDSELECTION) { - setMenuState(MenuState.MENU) - } + if (menuState == MenuState.INGAME_MENU) saveGame() + } else if (key == "Escape" && menuState == MenuState.WORLDSELECTION) setMenuState(MenuState.MENU) + - /*if (menuState == MenuState.MENU) game_menu_state.onKey(key) - else*/ if (menuState == MenuState.INGAME) ingame_state.onKey(key) + /* if (menuState == MenuState.MENU) gameMenuState.onKey(key) + else*/ if (menuState == MenuState.INGAME) ingameState.onKey(key) }) input.on("keypress", (key: string) => { - /*if (menuState == MenuState.MENU) game_menu_state.whileKey(key) - else*/ if (menuState == MenuState.INGAME) ingame_state.whileKey(key) + /* if (menuState == MenuState.MENU) gameMenuState.whileKey(key) + else*/ if (menuState == MenuState.INGAME) ingameState.whileKey(key) }) input.on("click", (button: number) => { - if (menuState == MenuState.MENU || menuState == MenuState.WORLDSELECTION) game_menu_state.onClick(button) - else if (menuState == MenuState.INGAME) ingame_state.onClick(button) - else if (menuState == MenuState.INGAME_MENU) ingame_menu_state.onClick(button) + if (menuState == MenuState.MENU || menuState == MenuState.WORLDSELECTION) gameMenuState.onClick(button) + else if (menuState == MenuState.INGAME) ingameState.onClick(button) + else if (menuState == MenuState.INGAME_MENU) ingameMenuState.onClick(button) }) input.on("mousemove", () => { - /*if (menuState == MenuState.MENU) game_menu_state.onMouseMove() - else*/ if (menuState == MenuState.INGAME) ingame_state.onMouseMove() + /* if (menuState == MenuState.MENU) gameMenuState.onMouseMove() + else*/ if (menuState == MenuState.INGAME) ingameState.onMouseMove() }) diff --git a/src/sound/AudioFile.ts b/src/sound/AudioFile.ts index 6029460..c2149bd 100644 --- a/src/sound/AudioFile.ts +++ b/src/sound/AudioFile.ts @@ -1,3 +1,5 @@ +import TinyError from "../TinyError.js" + export default class AudioFile { static LOADING = 0 @@ -12,12 +14,14 @@ export default class AudioFile { constructor(path: string) { this.path = path const audio = new Audio(`./assets/tiny/sounds/${path}`) + audio.addEventListener("canplaythrough", () => { this.#state = AudioFile.LOADED }) audio.addEventListener("error", e => { this.#state = AudioFile.FAILED - console.error(`Audio "${path}" failed to load:`, e) + + throw new TinyError(`Audio "${path}" failed to load:`, e.error) }) this.audio = audio @@ -35,7 +39,9 @@ export default class AudioFile { play() { if (!this.ready()) return + const node = (this.audio.cloneNode(true) as HTMLAudioElement) + node.play() this.nodes.add(node) node.addEventListener("ended", () => this.nodes.delete(node)) @@ -43,4 +49,4 @@ export default class AudioFile { node.addEventListener("pause", () => this.nodes.delete(node)) } -} \ No newline at end of file +} diff --git a/src/sound/Sound.ts b/src/sound/Sound.ts index df872b4..8d09b2c 100644 --- a/src/sound/Sound.ts +++ b/src/sound/Sound.ts @@ -9,30 +9,33 @@ export default class Sound { constructor(key: string) { this.key = key this.audios = [] - + let def = soundList[key] if (!def) { console.warn(`Sound definition ${key} missing!`) - def = {} + def = { sounds: [] } } - let sounds: any[] = def.sounds || [] - if (!(sounds instanceof Array) || sounds.length == 0) { - console.warn(`Sounds array for ${key} empty or invalid:`, sounds) - sounds = [] + const { sounds } = def + + if (sounds.length == 0) { + console.warn(`Sounds array for ${key} empty:`, def.sounds) } for (let i = 0; i < sounds.length; i++) { const sound = sounds[i] const name = typeof sound == "string" ? sound : sound.name - this.audios.push(getAudioFile(name + ".ogg")) + + this.audios.push(getAudioFile(`${name}.ogg`)) } } play() { if (this.audios.length == 0) return + const r = Math.floor(Math.random() * this.audios.length) + this.audios[r].play() } -} \ No newline at end of file +} diff --git a/src/texture/Subtexture.ts b/src/texture/Subtexture.ts index aa83b6e..2151199 100644 --- a/src/texture/Subtexture.ts +++ b/src/texture/Subtexture.ts @@ -1,6 +1,6 @@ +import Dim2 from "../dim/Dim2.js" import Graphics from "../Graphics.js" -import Dim2 from "../dim/Dim2.js"; -import Texture from "./Texture.js"; +import Texture from "./Texture.js" export default class Subtexture { diff --git a/src/texture/Texture.ts b/src/texture/Texture.ts index 3744778..282f130 100644 --- a/src/texture/Texture.ts +++ b/src/texture/Texture.ts @@ -1,5 +1,5 @@ import Graphics from "../Graphics.js" -import Subtexture from "./Subtexture.js"; +import Subtexture from "./Subtexture.js" export default class Texture { @@ -14,14 +14,15 @@ export default class Texture { constructor(path: string) { this.path = path const img = new Image() + img.addEventListener("load", () => { this.#state = Texture.LOADED }) img.addEventListener("error", e => { this.#state = Texture.FAILED - console.error(`Texture "${path}" failed to load:`, e) + console.warn(`Texture "${path}" failed to load:`, e) }) - img.src = "./assets/" + path + img.src = `./assets/${path}` this.image = img this.#state = Texture.LOADING diff --git a/src/util/BoundingBox.ts b/src/util/BoundingBox.ts index 7f7764a..b822b70 100644 --- a/src/util/BoundingBox.ts +++ b/src/util/BoundingBox.ts @@ -1,6 +1,6 @@ -import Graphics from "../Graphics.js" import Dim2 from "../dim/Dim2.js" import Dim3 from "../dim/Dim3.js" +import Graphics from "../Graphics.js" export default class BoundingBox { @@ -22,6 +22,8 @@ export default class BoundingBox { touch(point: Dim2 | Dim3) { const t = point.x >= this.pos.x && point.x <= this.corner.x && point.y >= this.pos.y && point.y <= this.corner.y + + // if (t) console.log(this, point, point.x >= this.pos.x, point.x <= this.corner.x, point.y >= this.pos.y, point.y <= this.corner.y) return t } diff --git a/src/util/Container.ts b/src/util/Container.ts index 9106f03..1eeee29 100644 --- a/src/util/Container.ts +++ b/src/util/Container.ts @@ -1,11 +1,11 @@ +import { game, getTexture, input } from "../main.js" import CreativeInventory from "../CreativeInventory.js" +import Dim2 from "../dim/Dim2.js" import Graphics from "../Graphics.js" import Inventory from "../Inventory.js" import ItemStack from "../ItemStack.js" -import Dim2 from "../dim/Dim2.js" -import { player } from "../gui/state/ingame.js" -import { game, getTexture, input } from "../main.js" import Texture from "../texture/Texture.js" +import { player } from "../gui/state/ingame.js" let slot: Texture let inventory: Inventory | undefined @@ -34,19 +34,25 @@ export default class Container { static floatingStack() { if ((floatingStackIndex != undefined && inventory)) { const stack = inventory.get(floatingStackIndex) + if (stack.item.id != "tiny:air") return stack - else { // shouldn't happen, but just in case - floatingStackIndex = undefined - } + + + // shouldn't happen, but just in case + floatingStackIndex = undefined } + return undefined } static drawContainer(g: Graphics) { if (!slot.ready || !inventory) return - const ctx = g.ctx + + const { ctx } = g + ctx.save() const offset = new Dim2((inventory.columns * slotSize)/2, (Math.ceil(inventory.size / inventory.columns) * slotSize)/2) + ctx.translate(-offset.x, -offset.y) // slot @@ -57,6 +63,7 @@ export default class Container { // item drawSlots(g, inventory, (inv, i) => { const stack = inv.get(i) + if (stack.item.id != "tiny:air" && i != floatingStackIndex) { ctx.save() ctx.translate(inset, inset) @@ -67,8 +74,10 @@ export default class Container { // hover const mouseSlot = this.getMouseSlot() + if (mouseSlot) { const { slotPos } = mouseSlot + ctx.save() ctx.fillStyle = "white" ctx.globalAlpha = 0.3 @@ -82,51 +91,53 @@ export default class Container { static getMouseSlot() { if (!slot.ready || !inventory) return + const offset = new Dim2((inventory.columns * slotSize)/2, (Math.ceil(inventory.size / inventory.columns) * slotSize)/2) - const mouse = input.mouse + const { mouse } = input + mouse.add(new Dim2(-game.width/2, -game.height/2)) mouse.add(offset) const slotPos = new Dim2(mouse.x / slotSize, mouse.y / slotSize).floor() const rows = Math.ceil(inventory.size / inventory.columns) if (slotPos.x < 0 || slotPos.x >= inventory.columns || slotPos.y < 0 || slotPos.y >= rows) return null + const slotIndex = slotPos.y * inventory.columns + slotPos.x - if (slotIndex >= 0 && slotIndex < inventory.size) { - return { slotPos, slotIndex } - } + if (slotIndex >= 0 && slotIndex < inventory.size) return { slotPos, slotIndex } + return null } - + static onClick(button: number): boolean { // true -> an action was performed if (!inventory) return false + const mouseSlot = this.getMouseSlot() + if (!mouseSlot) return false if (button == 0) { if (input.keyPressed("ShiftLeft")) { const stack = inventory.get(mouseSlot.slotIndex) + if (stack.item.id == "tiny:air") return false + const leftOver = player.hotbar.addItems(stack) - if (leftOver) { - stack.amount = leftOver.amount - } else { - inventory.set(mouseSlot.slotIndex, new ItemStack("tiny:air")) - } + + if (leftOver) stack.amount = leftOver.amount + else inventory.set(mouseSlot.slotIndex, new ItemStack("tiny:air")) } else { const stack = inventory.get(mouseSlot.slotIndex) const previous = Container.floatingStack() - + if (floatingStackIndex != undefined) { inventory.set(mouseSlot.slotIndex, previous || new ItemStack("tiny:air")) inventory.set(floatingStackIndex, stack || new ItemStack("tiny:air")) } if (floatingStackIndex == undefined && stack.item.id != "tiny:air") floatingStackIndex = mouseSlot.slotIndex - else if (stack.item.id == "tiny:air" || floatingStackIndex == mouseSlot.slotIndex) { - floatingStackIndex = undefined - } + else if (stack.item.id == "tiny:air" || floatingStackIndex == mouseSlot.slotIndex) floatingStackIndex = undefined } } @@ -135,16 +146,21 @@ export default class Container { static onKey(key: string): boolean { // true -> an action was performed if (!inventory) return false + const mouseSlot = this.getMouseSlot() + if (!mouseSlot || mouseSlot.slotIndex == floatingStackIndex) return false if (!key.startsWith("Digit")) return false + const hotbarIndex = +key.substring(5) + if (!hotbarIndex || hotbarIndex < 1 || hotbarIndex > player.hotbar.size) return false // swap item stacks const invStack = inventory.get(mouseSlot.slotIndex) const hotbarStack = player.hotbar.get(hotbarIndex-1) + inventory.set(mouseSlot.slotIndex, hotbarStack) player.hotbar.set(hotbarIndex-1, invStack) @@ -153,23 +169,22 @@ export default class Container { } -function drawSlots(g: Graphics, inventory: Inventory, cb: (inv: Inventory, i: number) => void) { - const ctx = g.ctx +function drawSlots(g: Graphics, _inventory: Inventory, cb: (inv: Inventory, i: number) => void) { + const { ctx } = g + ctx.save() let column = 0 - for (let i = 0; i < inventory.size; i++) { - cb(inventory, i) + for (let i = 0; i < _inventory.size; i++) { + cb(_inventory, i) // translate column++ - if (column < inventory.columns) { - ctx.translate(slotSize, 0) - } else { - ctx.translate(-(inventory.columns - 1) * slotSize, slotSize) + if (column < _inventory.columns) ctx.translate(slotSize, 0) + else { + ctx.translate(-(_inventory.columns - 1) * slotSize, slotSize) column = 0 } - } ctx.restore() diff --git a/src/util/DebugScreen.ts b/src/util/DebugScreen.ts index 5326734..04f1b4c 100644 --- a/src/util/DebugScreen.ts +++ b/src/util/DebugScreen.ts @@ -1,13 +1,14 @@ +import { GAME_NAME, GAME_VERSION, GAME_VERSION_BRANCH, drawTarget, game, input, perf, tickTarget } from "../main.js" +import { getFirstBlock, getFirstFluid, getMouseBlock } from "../gui/state/ingame.js" import Graphics from "../Graphics.js" import Player from "../entity/Player.js" -import { getFirstBlock, getFirstFluid, getMouseBlock } from "../gui/state/ingame.js" -import { GAME_NAME, GAME_VERSION, GAME_VERSION_BRANCH, drawTarget, game, input, perf, tickTarget } from "../main.js" import World from "../world/World.js" export default class DebugScreen { static draw(g: Graphics, world: World, player: Player) { - const ctx = g.ctx + const { ctx } = g + ctx.save() ctx.fillStyle = "lime" ctx.textBaseline = "top" @@ -19,10 +20,9 @@ export default class DebugScreen { } function gameInfo(g: Graphics, world: World, player: Player) { - const ctx = g.ctx + const { ctx } = g const mouseBlock = getMouseBlock() const lookingAt = getFirstBlock(world, mouseBlock.x, mouseBlock.y, undefined, block => block.type != "fluid") - lookingAt.block = lookingAt.block const lookingAtFluid = getFirstFluid(world, mouseBlock.x, mouseBlock.y) ctx.save() @@ -41,7 +41,7 @@ function gameInfo(g: Graphics, world: World, player: Player) { lines.push(``) lines.push(`pos: ${Math.floor(player.position.x * 1000) / 1000}, ${Math.floor(player.position.y * 1000) / 1000}`) - lines.push(`motion: ${player.motion.x.toFixed(3)}, ${player.motion.y.toFixed(3) } (${player.motion.mag().toFixed(2)}/s)`) + lines.push(`motion: ${player.motion.x.toFixed(3)}, ${player.motion.y.toFixed(3)} (${player.motion.mag().toFixed(2)}/s)`) lines.push(`rotation: ${player.rotationAngle.toFixed(3)} (${(player.rotationAngle * 180 / Math.PI).toFixed(1)}°)`) lines.push(`mouse: ${mouseBlock.x}, ${mouseBlock.y}`) @@ -60,6 +60,7 @@ function gameInfo(g: Graphics, world: World, player: Player) { } const offset = g.drawText(lines[0], { drawBg: true, padding: 3, font: { size: 20 }, shadow: false }) + for (let i = 1; i < lines.length; i++) { ctx.translate(0, offset) g.drawText(lines[i], { drawBg: true, padding: 3, font: { size: 20 }, shadow: false }) @@ -72,7 +73,7 @@ function gameInfo(g: Graphics, world: World, player: Player) { // - performance.measureUserAgentSpecificMemory() // - performance.memory function envInfo(g: Graphics) { // some of this might break - const ctx = g.ctx + const { ctx } = g ctx.save() ctx.translate(game.width/2, -game.height/2) // uncenter (-> top right corner) @@ -81,17 +82,19 @@ function envInfo(g: Graphics) { // some of this might break const lines = [] - // @ts-expect-error - const browser = navigator.userAgent.split(" ").at(-1).split("/") || {} // navigator.userAgentData?.brands.find(b => b.brand != "Not A(Brand") || - // @ts-expect-error + // @ts-expect-error ik it's using non-standard functionality + const browser = navigator.userAgent.split(" ").at(-1).split("/") || {} // navigator.userAgentData?.brands.find(b => b.brand != "Not A(Brand") || + // @ts-expect-error ik it's using non-standard functionality const memTotal = (navigator.deviceMemory*1000) || (performance.memory?.jsHeapSizeLimit/1_000_000) || 0 - // @ts-expect-error + + // @ts-expect-error ik it's using non-standard functionality const memUsage = performance.memory?.usedJSHeapSize / 1_000_000 - // @ts-expect-error + + // @ts-expect-error ik it's using non-standard functionality const memAllocated = performance.memory?.totalJSHeapSize / 1_000_000 - lines.push(`${browser[0] || "Unknown"} ${browser[1]?.split(".")[0] || "??"}`) // browser.brand ||; browser.version || - // @ts-expect-error + lines.push(`${browser[0] || "Unknown"} ${browser[1]?.split(".")[0] || "??"}`) // browser.brand ||; browser.version || + // @ts-expect-error ik it's using non-standard functionality lines.push(navigator.platform || navigator.userAgentData?.platform || navigator.oscpu || "Unknown") if (memUsage) lines.push(`Mem: ${(memUsage / memTotal * 100).toFixed(1)}% ${memUsage.toFixed(1)}/${memTotal}MiB`) @@ -101,6 +104,7 @@ function envInfo(g: Graphics) { // some of this might break lines.push(`Display: ${game.width}x${game.height}`) const offset = g.drawText(lines[0], { drawBg: true, padding: 3, font: { size: 20 }, shadow: false }) + for (let i = 1; i < lines.length; i++) { ctx.translate(0, offset) g.drawText(lines[i], { drawBg: true, padding: 3, font: { size: 20 }, shadow: false }) diff --git a/src/util/EventEmitter.ts b/src/util/EventEmitter.ts index ce423d4..ae6145c 100644 --- a/src/util/EventEmitter.ts +++ b/src/util/EventEmitter.ts @@ -1,22 +1,25 @@ +type Listener = (...args: T[]) => void + export default class EventEmitter { - - private listeners: Map + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private listeners: Map[]> constructor() { this.listeners = new Map() } - on(event: string, listener: Function) { - let arr = this.listeners.get(event) || [] + on(event: string, listener: Listener) { + const arr = this.listeners.get(event) || [] + arr.push(listener) this.listeners.set(event, arr) } - emit(event: string, ...args: any[]) { - let arr = this.listeners.get(event) || [] - for (let listener of arr) { - listener(...args) - } + emit(event: string, ...args: T[]) { + const arr = this.listeners.get(event) || [] + + for (const listener of arr) listener(...args) } -} \ No newline at end of file +} diff --git a/src/util/Hotbar.ts b/src/util/Hotbar.ts index 6c5b45c..955739f 100644 --- a/src/util/Hotbar.ts +++ b/src/util/Hotbar.ts @@ -1,8 +1,8 @@ +import { game, getTexture } from "../main.js" import Graphics from "../Graphics.js" -import { player } from "../gui/state/ingame.js"; -import { game, getTexture } from "../main.js"; -import Subtexture from "../texture/Subtexture.js"; -import Texture from "../texture/Texture.js"; +import Subtexture from "../texture/Subtexture.js" +import Texture from "../texture/Texture.js" +import { player } from "../gui/state/ingame.js" let widgets: Texture let leftSlot: Subtexture @@ -24,8 +24,8 @@ export default class Hotbar { } static drawHotbar(g: Graphics) { - const ctx = g.ctx - let hotbar = player.hotbar + const { ctx } = g + const { hotbar } = player ctx.save() ctx.translate(-204, game.height/2 - 100) @@ -38,7 +38,7 @@ export default class Hotbar { ctx.scale(21, 22) leftSlot.draw(g, 1, 1, true) ctx.restore() - + ctx.save() ctx.translate(21, 0) ctx.scale(20, 22) @@ -46,6 +46,7 @@ export default class Hotbar { middleSlot.draw(g, 1, 1, true) ctx.translate(1, 0) } + ctx.restore() ctx.save() @@ -66,12 +67,13 @@ export default class Hotbar { ctx.restore() for (let i = 0; i < hotbar.size; i++) { - let stack = hotbar.get(i) + const stack = hotbar.get(i) + if (stack.item.id == "tiny:air") continue if (!stack.item.texture) continue - let cx = 22/2 * scale - itemSize/2 + i * 20 * scale - let cy = 22/2 * scale - itemSize/2 + const cx = 22/2 * scale - itemSize/2 + i * 20 * scale + const cy = 22/2 * scale - itemSize/2 ctx.save() ctx.translate(cx, cy) @@ -87,7 +89,7 @@ export default class Hotbar { ctx.scale(24, 24) selector.draw(g, 1, 1, true) ctx.restore() - + ctx.restore() } diff --git a/src/util/TextRenderer.ts b/src/util/TextRenderer.ts index dd1e781..54b5a07 100644 --- a/src/util/TextRenderer.ts +++ b/src/util/TextRenderer.ts @@ -14,11 +14,12 @@ export default class TextRenderer { strikethrough = false } = options || {} const { padding = drawBg ? 1 : 0 } = options || {} - font.family = font.family || "tinymc" - font.size = font.size || 16 - font.weight = font.weight || "normal" - font.style = font.style || "normal" - font.variant = font.variant || "" + + font.family ||= "tinymc" + font.size ||= 16 + font.weight ||= "normal" + font.style ||= "normal" + font.variant ||= "" if (text.trim() == "") return font.size + 2 * padding ctx.save() @@ -28,17 +29,19 @@ export default class TextRenderer { if (drawBg) { if (!["left", "right"].includes(ctx.textAlign)) ctx.textAlign = "left" // required or bg is off if (!["top", "bottom"].includes(ctx.textAlign)) ctx.textBaseline = "top" // required or bg is off + ctx.fillStyle = "black" ctx.globalAlpha = opacity * bgOpacity - let x = 0, y = 0 - let width = textWidth + 2 * padding - let height = font.size + 2 * padding - if (ctx.textAlign == "right") { - x -= textWidth + 2 * padding - } + let bgX = 0 + const bgY = 0 + const width = textWidth + 2 * padding + const height = font.size + 2 * padding + + if (ctx.textAlign == "right") bgX -= textWidth + 2 * padding - ctx.fillRect(x, y, width, height) + + ctx.fillRect(bgX, bgY, width, height) } ctx.globalAlpha = opacity @@ -83,7 +86,17 @@ export type TextRenderingOptions = { strikethrough?: boolean } -function drawDecoratedText(ctx: CanvasRenderingContext2D, text: string, x: number, y: number, unit: number, textWidth: number, underline: boolean, overline: boolean, strikethrough: boolean) { +function drawDecoratedText( + ctx: CanvasRenderingContext2D, + text: string, + x: number, + y: number, + unit: number, + textWidth: number, + underline: boolean, + overline: boolean, + strikethrough: boolean +) { ctx.fillText(text, x, y) if (underline) drawLine(ctx, x, y, textWidth, unit) @@ -92,21 +105,15 @@ function drawDecoratedText(ctx: CanvasRenderingContext2D, text: string, x: numbe } function drawLine(ctx: CanvasRenderingContext2D, x: number, y: number, width: number, height: number) { - if (ctx.textAlign == "right") { - x -= width - } else if (ctx.textAlign == "center") { - x -= width / 2 - } + if (ctx.textAlign == "right") x -= width + else if (ctx.textAlign == "center") x -= width / 2 + + + if (ctx.textBaseline == "top") y += height * 9 + else if (ctx.textBaseline == "middle") y += height * 4 + else if (ctx.textBaseline == "alphabetic") y += height + else if (ctx.textBaseline == "bottom" || ctx.textBaseline == "ideographic") y -= height - if (ctx.textBaseline == "top") { - y += height * 9 - } else if (ctx.textBaseline == "middle") { - y += height * 4 - } else if (ctx.textBaseline == "alphabetic") { - y += height - } else if (ctx.textBaseline == "bottom" || ctx.textBaseline == "ideographic") { - y -= height - } ctx.fillRect(x, y, width, height) -} \ No newline at end of file +} diff --git a/src/util/interfaces.ts b/src/util/interfaces.ts index 84f46f6..81c04c7 100644 --- a/src/util/interfaces.ts +++ b/src/util/interfaces.ts @@ -2,7 +2,7 @@ import Inventory from "../Inventory" export type ArrayElement = A extends readonly (infer T)[] ? T : never -export type Flatten = T extends Array ? (Flatten>)[] : { +export type Flatten = T extends Array ? (Flatten>)[] : { [K in keyof T]: T[K] } @@ -15,9 +15,15 @@ export type BaseData = { } export interface HasData { - getData: (...args: any) => {} + getData: (...args: never[]) => object } export type DataOf = ReturnType -export type NamespacedId = `${string}:${string}` \ No newline at end of file +export type NamespacedId = `${string}:${string}` + +export type SoundDef = { + sounds: Array +} diff --git a/src/util/typecheck.ts b/src/util/typecheck.ts index 7129ba9..ae7e969 100644 --- a/src/util/typecheck.ts +++ b/src/util/typecheck.ts @@ -1,10 +1,140 @@ -import { type NamespacedId } from "./interfaces"; - -export function isInteger(x: unknown): x is number { - return typeof x == "number" || Number.isInteger(x) -} +import { type NamespacedId } from "./interfaces" +import YSON from "https://j0code.github.io/browserjs-yson/main.mjs" export const namespacedIdRegex = /^([a-z_]+):([a-z_]+)$/ export function isNamespacedId(s: string): s is NamespacedId { return namespacedIdRegex.test(s) -} \ No newline at end of file +} + +export function equalsAny(a: unknown[], b: unknown) { + return a.includes(b) +} + +export function isInteger(x: unknown): x is number { + return typeof x == "number" && Number.isInteger(x) +} + +export function isPosInt(x: unknown): x is number { + return typeof x == "number" && Number.isInteger(x) && x > 0 +} + +/** x in [min, max) */ +export function isInRange(min: number, max: number): (x: unknown) => x is number { + return (x: unknown): x is number => typeof x == "number" && x >= min && x < max +} + +/** x in [min, max] */ +export function isInRangeIncl(min: number, max: number): (x: unknown) => x is number { + return (x: unknown): x is number => typeof x == "number" && x >= min && x <= max +} + +/** x in [min, max) */ +export function isIntInRange(min: number, max: number): (x: unknown) => x is number { + return (x: unknown): x is number => isInteger(x) && x >= min && x < max +} + +export function isObject(x: unknown): x is object { + return typeof x == "object" && x != null +} + +type PropCheck = ((x: unknown, path: string) => boolean) | "string" | "number" | "boolean" +export function safeValidateProperty(obj: object, prop: P, check?: PropCheck, defaultValue?: T, path: string = "$"): obj is Record { + const rec = obj as Record + + if (prop in rec) { + if (check) { + if (typeof check == "function") { + if (!check(rec[prop], `${path}.${prop}`)) return false + } else if (typeof rec[prop] != check) { + return false + } + } + } else rec[prop] = defaultValue + + return true +} + +export function validateProperty(obj: object, prop: P, check?: PropCheck, defaultValue?: T, path: string = "$"): obj is Record { + const rec = obj as Record + + if (prop in rec) { + if (check) { + if (typeof check == "function") { + if (!check(rec[prop], `${path}.${prop}`)) throw new Error(`${path}.${prop} is invalid: ${YSON.stringify(rec[prop])}`) + } else if (typeof rec[prop] != check) { + throw new Error(`Expected a ${check} at ${path}.${prop} but got ${YSON.stringify(rec[prop])}`) + } + } + } else rec[prop] = defaultValue + + return true +} + +type TypeCheck = ((x: unknown, path: string) => x is T) | "string" | "number" | "boolean" +export function validateArray(check?: TypeCheck): (arr: unknown, path?: string) => arr is Array { + return (arr, path = "$"): arr is Array => { + if (!(arr instanceof Array)) throw new Error(`Expected an array at ${path} but got ${YSON.stringify(arr)}`) + if (!check) return true + + for (let i = 0; i < arr.length; i++) { + if (typeof check == "function") { + if (!check(arr[i], `${path}[${i}]`)) throw new Error(`${path}[${i}] is invalid ${YSON.stringify(arr[i])}`) + } else if (typeof arr[i] != check) { + throw new Error(`Expected a ${check} at ${path}[${i}] but got ${YSON.stringify(arr[i])}`) + } + } + + return true + } +} + +export function safeValidateArray(check?: TypeCheck): (arr: unknown, path?: string) => arr is Array { + return (arr, path = "$"): arr is Array => { + if (!(arr instanceof Array)) return false + if (!check) return true + + for (let i = 0; i < arr.length; i++) { + if (typeof check == "function") { + if (!check(arr[i], `${path}[${i}]`)) return false + } else if (typeof arr[i] != check) { + return false + } + } + + return true + } +} + +type KeyCheck = ((x: unknown, path: string) => x is T) | "string" | "number" | "symbol" +export function validateRecord( + keyCheck: KeyCheck, + valueCheck: TypeCheck +): (rec: unknown, path?: string) => rec is Record { + return (rec, path = "$"): rec is Record => { + if (!isObject(rec)) throw new Error(`Expected a Record at ${path} but got ${YSON.stringify(rec)}`) + if (!keyCheck && !valueCheck) return true + + for (const k in Object.values(rec)) { + // @ts-expect-error k can be used to index rec (see for loop) + const v = rec[k] + + if (keyCheck) { + if (typeof keyCheck == "function") { + if (!keyCheck(k, `${path}`)) throw new Error(`key ${k} is ${path} is invalid`) + } else if (typeof k != keyCheck) { + throw new Error(`Expected a ${keyCheck} key at ${path} but got ${YSON.stringify(k)}`) + } + } + + if (valueCheck) { + if (typeof valueCheck == "function") { + if (!valueCheck(v, `${path}.${k}`)) return false + } else if (typeof v != valueCheck) { + throw new Error(`Expected a ${valueCheck} at ${path}.${k} but got ${YSON.stringify(v)}`) + } + } + } + + return true + } +} diff --git a/src/util/util.ts b/src/util/util.ts index 18d3e81..bcc76fa 100644 --- a/src/util/util.ts +++ b/src/util/util.ts @@ -1,7 +1,7 @@ export function $(q: string): T { const elem = document.querySelector(q) - if (!elem) { - throw new Error(`Element not found: ${q}`) - } + + if (!elem) throw new Error(`Element not found: ${q}`) + return elem } diff --git a/src/world/World.ts b/src/world/World.ts index bcc0dd1..6dcc5f2 100644 --- a/src/world/World.ts +++ b/src/world/World.ts @@ -1,55 +1,67 @@ -import Graphics from "../Graphics.js" import Block, { BlockData } from "../block/Block.js" import Entity, { EntityData } from "../entity/Entity.js" -import YSON from "https://j0code.github.io/browserjs-yson/main.mjs" import Player, { type PlayerData } from "../entity/Player.js" -import { type NamespacedId } from "../util/interfaces.js" -import { isNamespacedId } from "../util/typecheck.js" import { createBlock, createEntity, getFirstBlock } from "../gui/state/ingame.js" import Dim2 from "../dim/Dim2.js" +import Graphics from "../Graphics.js" +import { type NamespacedId } from "../util/interfaces.js" +import { isNamespacedId } from "../util/typecheck.js" + +// import YSON from "https://j0code.github.io/browserjs-yson/main.mjs" export default class World { + // eslint-disable-next-line static fromYSON(data: any) { if (!(data instanceof Object) || !data.blocks || !(data.blocks instanceof Map)) throw new Error("Could not parse World:", data) if (data.entities && !(data.entites instanceof Set)) throw new Error("Could not parse World:", data) - let dims = data.dims + + const { dims } = data + if (dims instanceof Array && dims.length >= 6) { - for (let i in dims) { + for (const i in dims) { if (isNaN(dims[i])) return // fill in default + dims[i] = Number(dims[i]) } } else return // fill in default dimensions - let world = new World("", dims) - for (let b of data.blocks) { - world.setBlock(b.x, b.y, b.z, b) - } + + const world = new World("", dims) + + for (const b of data.blocks) world.setBlock(b.x, b.y, b.z, b) + world.entities = data.entities || new Set() + return world } static load(name: string, stringBlocks: string, blockData: BlockData[], dims: number[], entities: EntityData[]) { const semi = stringBlocks.indexOf(";") + if (!semi) return // invalid save const blockIdStrings: string[] = stringBlocks.substring(0, semi).split(",") const blockIds: NamespacedId[] = blockIdStrings.map(v => { if (isNamespacedId(v)) return v - else return "tiny:air" + + return "tiny:air" }) const blocks = Array.from(stringBlocks.substring(semi + 1)).map(v => v.charCodeAt(0)) const blockDataMap: Map<`${number},${number},${number}`, BlockData> = new Map() + blockData.forEach(data => blockDataMap.set(`${data.x},${data.y},${data.z}`, data)) const world = new World(name, dims) + world.entities = new Set(entities.map(data => createEntity(data.id, data.spawnTime, data))) - + let i = 0 for (let z = world.minZ; z <= world.maxZ; z++) { for (let y = world.minY; y <= world.maxY; y++) { for (let x = world.minX; x <= world.maxX; x++) { const data = blockDataMap.get(`${x},${y},${z}`) + world.setBlock(x, y, z, createBlock(blockIds[blocks[i]] || "tiny:air", data)) // TODO: if blockID unknown, insert placeholder block i++ } @@ -88,6 +100,7 @@ export default class World { } } + this.updateQueue = [] } @@ -100,13 +113,17 @@ export default class World { y = Math.floor(y) z = Math.floor(z) if (x < this.minX || x > this.maxX || y < this.minY || y > this.maxY || z < this.minZ || z > this.maxZ) return null + return [x, y, z] } getBlock(x: number, y: number, z: number) { const pos = this.validBlockPosition(x, y, z) + if (!pos) return + [x, y, z] = pos + return this.blocks.get(`${x},${y},${z}`) } @@ -116,14 +133,18 @@ export default class World { setBlock(x: number, y: number, z: number, block: Block | NamespacedId, data?: Partial, silent = true) { const pos = this.validBlockPosition(x, y, z) + if (!pos) return + [x, y, z] = pos - if (typeof block == "string") { - block = createBlock(block, data) - } + if (typeof block == "string") block = createBlock(block, data) + + + if (x < this.minX || x > this.maxX || y < this.minY || y > this.maxY || z < this.minZ || z > this.maxZ) { + throw new Error(`Tried to set block outside the world: ${x},${y},${z}; ${block.id}`) + } - if (x < this.minX || x > this.maxX || y < this.minY || y > this.maxY || z < this.minZ || z > this.maxZ) throw new Error(`Tried to set block outside the world: ${x},${y},${z}; ${block.id}`) this.blocks.set(`${x},${y},${z}`, block) if (!silent) block.playSound("place") @@ -138,14 +159,18 @@ export default class World { clearBlock(x: number, y: number, z: number, silent = true) { const pos = this.validBlockPosition(x, y, z) + if (!pos) return + [x, y, z] = pos const oldBlock = this.getBlock(x, y, z) + if (!oldBlock || oldBlock.id == "tiny:air") return if (!silent) oldBlock?.playSound("break") const block = new Block("tiny:air") + this.blocks.set(`${x},${y},${z}`, block) this.scheduleBlockUpdate(x, y, z) @@ -161,15 +186,13 @@ export default class World { let entities = Array.from(this.entities.values()) if (typeof filter == "string") entities = entities.filter(entity => entity.id == filter) else if (typeof filter == "function") entities = entities.filter(filter) + return entities as E[] } spawn(entity: Entity | NamespacedId, data?: Partial) { - if (typeof entity == "string") { - this.entities.add(createEntity(entity, this.tickCount, data)) - } else { - this.entities.add(entity) - } + if (typeof entity == "string") this.entities.add(createEntity(entity, this.tickCount, data)) + else this.entities.add(entity) } removeEntity(entity: Entity) { @@ -177,16 +200,17 @@ export default class World { } tick() { - for (let entity of this.entities.values()) { - entity.tick(this) - } + for (const entity of this.entities.values()) entity.tick(this) + // block updates let entry: {x: number, y: number, z: number} | undefined if (this.updateQueue.length > 0) { + // eslint-disable-next-line no-cond-assign while (entry = this.updateQueue.shift()) { - const {x, y, z} = entry + const { x, y, z } = entry const block = this.getBlock(x, y, z) + block?.update(this, x, y, z) } } @@ -196,39 +220,39 @@ export default class World { } draw(g: Graphics) { - for (let z = this.minZ; z <= this.maxZ; z++) { if (z == 0) { // draw entities behind blocks (e.g. water) - for (let entity of this.getAllEntities()) { - entity.draw(g, this) - } + for (const entity of this.getAllEntities()) entity.draw(g, this) } + for (let y = this.minY; y <= this.maxY; y++) { for (let x = this.minX; x <= this.maxX; x++) { const frontBlock = getFirstBlock(this, x, y, undefined, block => block.full) + if (z >= frontBlock.z) this.getBlock(x, y, z)?.draw(g, x, y, z) } } } - } drawBoundingBoxes(g: Graphics) { - for (let z = this.minZ; z <= this.maxZ; z++) { for (let y = this.minY; y <= this.maxY; y++) { for (let x = this.minX; x <= this.maxX; x++) { const block = this.getBlock(x, y, z) + if (!block || block.id == "tiny:air") continue + block.getBoundingBox(x, y).draw(g, "blue") } } + if (z == 0) { - for (let entity of this.getAllEntities()) { + for (const entity of this.getAllEntities()) { entity.getBoundingBox().draw(g, "red") // eye ray - const eyes = entity.eyes + const { eyes } = entity const dir = Dim2.polar(entity.rotationAngle, 100) const endpoint = eyes.copy().add(dir) @@ -243,15 +267,17 @@ export default class World { } } } - } scheduleBlockUpdate(x: number, y: number, z: number) { const pos = this.validBlockPosition(x, y, z) + if (!pos) return + [x, y, z] = pos if (this.updateQueue.find(e => e.x == x && e.y == y && e.z == z)) return - this.updateQueue.push({x, y, z}) + + this.updateQueue.push({ x, y, z }) } save() { @@ -265,19 +291,22 @@ export default class World { for (let y = this.minY; y <= this.maxY; y++) { for (let x = this.minX; x <= this.maxX; x++) { const block = this.getBlock(x, y, z) + if (!block) { blocks[i] = 0 i++ continue } if (block.type == "container") blockData.push(block.getData(x, y, z)) + const idIndex = blockIds.indexOf(block.id) - if (idIndex >= 0) { - blocks[i] = idIndex - } else { + + if (idIndex >= 0) blocks[i] = idIndex + else { blockIds.push(block.id) blocks[i] = blockIds.length -1 } + i++ } } @@ -286,30 +315,27 @@ export default class World { const entities: EntityData[] = [] const players: PlayerData[] = [] - Array.from(this.entities).forEach(entity =>{ - if (entity instanceof Player) { - players.push(entity.getData()) - } else { - entities.push(entity.getData(this)) - } + Array.from(this.entities).forEach(entity => { + if (entity instanceof Player) players.push(entity.getData()) + else entities.push(entity.getData(this)) }) return { blockIds, blocks, - stringBlocks: blockIds.join(",") + ";" + String.fromCharCode(...Array.from(blocks)), + stringBlocks: `${blockIds.join(",")};${String.fromCharCode(...Array.from(blocks))}`, blockData, - dims: [this.minX, this.maxX, this.minY, this.maxY, this.minZ, this.maxZ], + dims: [this.minX, this.maxX, this.minY, this.maxY, this.minZ, this.maxZ], entities, players, - name: this.name + name: this.name } } toYSON() { return { - dims: [this.minX, this.maxX, this.minY, this.maxY, this.minZ, this.maxZ], - blocks: this.blocks, + dims: [this.minX, this.maxX, this.minY, this.maxY, this.minZ, this.maxZ], + blocks: this.blocks, entites: this.entities } } diff --git a/src/world/WorldGenerator.ts b/src/world/WorldGenerator.ts index 3b0bc62..42a42e4 100644 --- a/src/world/WorldGenerator.ts +++ b/src/world/WorldGenerator.ts @@ -1,38 +1,34 @@ -import Block from "../block/Block.js"; -import { blockdefs } from "../main.js"; -import { type NamespacedId } from "../util/interfaces.js"; -import World from "./World.js"; -import YSON from "https://j0code.github.io/browserjs-yson/main.mjs" +import Block from "../block/Block.js" +import { NamespacedId } from "../util/interfaces.js" +import World from "./World.js" +import { blockdefs } from "../main.js" + +// import YSON from "https://j0code.github.io/browserjs-yson/main.mjs" export default class WorldGenerator { - static flat(world: World) { - for (let y = world.minY; y <= world.maxY && y <= 0; y++) { - for (let x = world.minX; x <= world.maxX; x++) { + static flat(world: World) { + for (let y = world.minY; y <= world.maxY && y <= 0; y++) { + for (let x = world.minX; x <= world.maxX; x++) { let blockId: NamespacedId - if (y < -3) blockId = "tiny:stone" - else if(y < -1) blockId = "tiny:dirt" - else if(y == -1) blockId = "tiny:grass_block" + if (y < -3) blockId = "tiny:stone" + else if (y < -1) blockId = "tiny:dirt" + else if (y == -1) blockId = "tiny:grass_block" else continue - for (let z = world.minZ; z <= 0; z++) { - world.setBlock(x, y, z, new Block(blockId)) - } - } - } - - for (let x = world.minX; x <= world.maxX; x++) { - for (let z = world.minZ; z <= world.maxZ; z++) { - world.scheduleBlockUpdate(x, world.maxY, z) + for (let z = world.minZ; z <= 0; z++) world.setBlock(x, y, z, new Block(blockId)) } } + for (let x = world.minX; x <= world.maxX; x++) for (let z = world.minZ; z <= world.maxZ; z++) world.scheduleBlockUpdate(x, world.maxY, z) + + // for testing of world.save() and World.load() - /*(async () => { + /* (async () => { let ysonSave = YSON.stringify(world.toYSON().blocks) let save = world.save() let saveLength = save.stringBlocks.length - + console.log("blocks:", world.toYSON().blocks) console.log("save:", save) console.log("space effi:", saveLength - ysonSave.length, ysonSave.length / saveLength + "x") @@ -43,7 +39,7 @@ export default class WorldGenerator { console.error("oh no") return } - + for (let z = worldCopy.minZ; z <= worldCopy.maxZ; z++) { for (let y = worldCopy.minY; y <= worldCopy.maxY; y++) { for (let x = worldCopy.minX; x <= worldCopy.maxX; x++) { @@ -54,23 +50,23 @@ export default class WorldGenerator { } } })()*/ - } + } - static random(world: World) { - let blockArray = Array.from(blockdefs.values()) - console.log(blockArray) + static random(world: World) { + const blockArray = Array.from(blockdefs.values()) - for (let z = world.minZ; z <= world.maxZ; z++) { - for (let y = world.minY; y <= world.maxY; y++) { - for (let x = world.minX; x <= world.maxX; x++) { + // console.log(blockArray) - let r = Math.floor(Math.random() * (blockArray.length + 5)) - 5 - if (r < 0) r = 0 - world.setBlock(x, y, z, new Block(blockArray[r])) + for (let z = world.minZ; z <= world.maxZ; z++) { + for (let y = world.minY; y <= world.maxY; y++) { + for (let x = world.minX; x <= world.maxX; x++) { + let r = Math.floor(Math.random() * (blockArray.length + 5)) - 5 + if (r < 0) r = 0 - } - } - } - } + world.setBlock(x, y, z, new Block(blockArray[r])) + } + } + } + } }