diff --git a/.gitignore b/.gitignore index 72a9468b4..7c0c61522 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ node_modules/ package-lock.json .yarn/install-state.gz +tools/vt-diff/.cache +tools/vt-diff/out \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f8d1ac31c..bc798fd6f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -113,6 +113,8 @@ If there is need to scale the template use closest neighbor instead of other sam JSON, JS and TS files should be formatted using 1 tab with size 4 for indentation. +An addon may override this for its own JS/TS as long as its ESLint config is configured to match, so the linter and the editor agree. + ### Addons (Yarn Monorepo Packages) Addons are managed as Yarn workspace packages. To start working in addons you will need to have the following: @@ -126,11 +128,13 @@ right of the IDE. **Creating a new addon:** -1. Copy the template from `templates/addon/` to `addons///` -2. Update `addons//package.json` to include the new addon in workspaces -3. Update `addons///package.json` using the graves example as reference: -4. Update UUIDs, pack name, and descriptions in BP/RP manifest.json and config.json -5. Update module versions in config.json and package.json as needed +Addons are built on the [bedrock-core](https://bedrock-core.drav.dev/) stack. Scaffold them with the repo wrapper around its CLI: + +1. Run `yarn create-addon [--author ] [description...]` from the repo root (example: `yarn create-addon gameplay_changes graves When you die, a grave saves all your drops.`) +2. It scaffolds `addons/files///` with creator `bt` and namespace `bt__` (example: `bt_gc_graves`), credits you as the author (your git user.name, or `--author`) in `config.json`, the manifests and the bedrock-core addon list (`, Bedrock Tweaks`), adapts the package to the monorepo (`@bedrock-tweaks/`, private, no addon-local yarn files) and registers it in the category workspaces +3. Run `yarn install` at the root, then build the addon from its directory +4. Add the pack entry to `addons/packs.json` with id `` under its category (the `bt__` namespace is only for commands, tags and identifiers inside the addon) +5. Use the latest stable Minecraft module versions and `min_engine_version`; experimental/beta APIs are not accepted **Development workflow:** @@ -138,7 +142,7 @@ After installing the monorepo with `yarn install`, run `yarn regolith-install` o Then, **open each addon as a standalone VSCode instance** and run commands from that directory: -- **Watch mode** (live recompilation): `yarn run dev` (runs `regolith watch`) +- **Watch mode** (live recompilation): `yarn run watch` (runs `regolith watch`) - **Build once**: `yarn run build` (runs `regolith run build`) - **Lint addon**: `yarn run lint` (runs `eslint .`) @@ -152,7 +156,7 @@ From the root directory, use these commands to manage the entire monorepo: **Before submitting PR:** - Lint the addon from its directory: `yarn run lint` -- Bump versions in addon `package.json` and `addons/packs.json` +- Bump the addon version in its `package.json` and BP/RP `manifest.json` (addons do not set versions in `addons/packs.json` — the version is inferred from the pack itself) - Test the addon in-game on at least 1 device **Monorepo structure:** @@ -163,34 +167,11 @@ From the root directory, use these commands to manage the entire monorepo: Regarding regolith filters, currently it is only accepted filters which run on node. -Resource Pack JSON UI modifications for addons are not accepted at this moment. - #### Technical Details -- Addons should not have functions +- Addons should not have functions, prefer custom commands - All settings and interactions should be in-game or in server forms -- Addons should have a basic `/bt: config` (TBD specifics discuss in discord) base command which should open a config server form -- Addon could have extra commands for quick access if necessary for commodity (for example tpa) but prefer server forms, easier for normal users -- The code in the template is an example it could be removed and changed as long as it follows the structure -- Prefer interfaces to types. -- Prefer functional programming over object-oriented programming. -- Prefer `const` and `let` over `var`. - -#### Keys to change - -When making an addon from the template you should look for these keys and replace them - -```md - - - - - - - - - -``` +- Addon could have extra commands for quick access if necessary for commodity (for example tpa) but prefer ui, easier for normal users If you notice any files not following the Style Guide feel free to open a PR. @@ -333,7 +314,9 @@ export interface PacksJSON { section: Section; // Global pack version, this will be the header.min_engine_version in the manifest.json // example: [1, 21, 0] - version: number[]; + // * Not set for Addons: the server adds it when generating, as the min of the + // minimum engine versions of all addons + version?: number[]; categories: Category[]; combinations: Combination[]; deepMergeFiles: DeepMergeFile[]; @@ -364,22 +347,23 @@ export interface Pack { name: string; description: string; message?: Message; - version?: string; // * only Addons and CT + version?: string; // * only CT — addons do not carry a version here: it is inferred from the pack itself and added by the server, which builds the dependency tree and downloads all selected packs priority?: number; // Higher number, higher priority disabled?: boolean; } /** - * Pack Version is a string as follows: " - " for addons - * and just "" for crafting tweaks - * minecraft_version is the minimum version of the game the pack is compatible with + * Pack Version is a string as follows: "" for crafting tweaks * pack_version is the version of the pack for that minecraft update, each mc update it resets * example: - * (version update) "1.21.50 - 1.0.0" - * (bug fix) "1.21.50 - 1.0.1" - * (pack major revamp) "1.21.50 - 2.0.0" - * (pack improvements) "1.21.50 - 2.1.0" - * (version update) "1.22.0 - 1.0.0" + * (version update) "1.0.0" + * (bug fix) "1.0.1" + * (pack major revamp) "2.0.0" + * (pack improvements) "2.1.0" + * + * Addons never author versions in packs.json: each addon is its own pack, its + * version and minimum engine version come from the pack manifest, and the + * server adds pack versions and the root version when generating. */ export interface Combination { diff --git a/addons/files/gameplay_changes/graves/.editorconfig b/addons/files/gameplay_changes/graves/.editorconfig new file mode 100644 index 000000000..9ab222395 --- /dev/null +++ b/addons/files/gameplay_changes/graves/.editorconfig @@ -0,0 +1,18 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{ts,tsx,js,jsx,mjs,cjs}] +indent_style = space +indent_size = 2 + +[*.{json,jsonc,mcstructure}] +indent_style = tab +indent_size = 4 + +[*.md] +trim_trailing_whitespace = false diff --git a/addons/files/gameplay_changes/graves/.gitignore b/addons/files/gameplay_changes/graves/.gitignore index 3f195ca9a..1b86caab8 100644 --- a/addons/files/gameplay_changes/graves/.gitignore +++ b/addons/files/gameplay_changes/graves/.gitignore @@ -1,2 +1,71 @@ +# Dependencies +node_modules +**/node_modules +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions + +# Build outputs /build -/.regolith \ No newline at end of file +/.regolith +dist +**/dist +*.tsbuildinfo +**/*.tsbuildinfo + +# Testing +coverage +**/coverage +.nyc_output + +# Environment +.env +.env.local +.env.*.local + +# IDE +.vscode/* +!.vscode/launch.json +!.vscode/settings.json +!.vscode/extensions.json +!.vscode/mcp.json +.idea +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db +desktop.ini + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +# Temporary files +*.tmp +*.temp +.cache +**/.regolith/ + +TODO +.claude +# Filter-generated artifacts (rebuilt by regolith) +**/*.generated.json +**/*.generated.d.ts + +# Render pack (downloaded by the bedrock-core CLI, not committed) +core-ui-*.mcpack + +# Minecraft schema types (regenerated by the generator filter on build) +/packs/data/generated/ diff --git a/templates/addon/.mcignore b/addons/files/gameplay_changes/graves/.mcignore similarity index 100% rename from templates/addon/.mcignore rename to addons/files/gameplay_changes/graves/.mcignore diff --git a/addons/files/gameplay_changes/graves/.vscode/extensions.json b/addons/files/gameplay_changes/graves/.vscode/extensions.json index 25477bef4..8cbb4a4ff 100644 --- a/addons/files/gameplay_changes/graves/.vscode/extensions.json +++ b/addons/files/gameplay_changes/graves/.vscode/extensions.json @@ -1,6 +1,5 @@ { "recommendations": [ - "aaron-bond.better-comments", "blockceptionltd.blockceptionvscodeminecraftbedrockdevelopmentextension", "dbaeumer.vscode-eslint", "mojang-studios.minecraft-debugger", diff --git a/addons/files/gameplay_changes/graves/.vscode/launch.json b/addons/files/gameplay_changes/graves/.vscode/launch.json index c903aaa6c..bfd91823e 100644 --- a/addons/files/gameplay_changes/graves/.vscode/launch.json +++ b/addons/files/gameplay_changes/graves/.vscode/launch.json @@ -1,16 +1,16 @@ { - "version": "0.3.0", + "version": "0.2.0", "configurations": [ { "type": "minecraft-js", "request": "attach", - "sourceMapRoot": "${env:LOCALAPPDATA}/Packages/Microsoft.MinecraftUWP_8wekyb3d8bbwe/LocalState/games/com.mojang/development_behavior_packs/bedrock-tweaks/graves/scripts/", - "generatedSourceRoot": "${env:LOCALAPPDATA}/Packages/Microsoft.MinecraftUWP_8wekyb3d8bbwe/LocalState/games/com.mojang/development_behavior_packs/bedrock-tweaks/graves/scripts/", + "sourceMapRoot": "${env:APPDATA}/Minecraft Bedrock/Users/Shared/games/com.mojang/development_behavior_packs/gc_graves/scripts/", + "generatedSourceRoot": "${env:APPDATA}/Minecraft Bedrock/Users/Shared/games/com.mojang/development_behavior_packs/gc_graves/scripts/", "localRoot": "${workspaceFolder}/packs/BP/scripts/", "name": "Debug with Minecraft", "mode": "listen", "port": 19144, - "targetModuleUuid": "da5802ff-6c19-46d8-a27a-1f717580968b" + "targetModuleUuid": "8ec2e94f-b226-4d04-b966-69f89812700e" } ] } \ No newline at end of file diff --git a/addons/files/gameplay_changes/graves/.vscode/settings.json b/addons/files/gameplay_changes/graves/.vscode/settings.json index 03f36e123..c690145ac 100644 --- a/addons/files/gameplay_changes/graves/.vscode/settings.json +++ b/addons/files/gameplay_changes/graves/.vscode/settings.json @@ -4,17 +4,17 @@ "source.fixAll.eslint": "explicit" }, "editor.detectIndentation": false, - "editor.insertSpaces": false, + "editor.insertSpaces": true, "editor.tabSize": 2, "[typescript]": { "editor.defaultFormatter": "dbaeumer.vscode-eslint", - "editor.insertSpaces": false, - "editor.tabSize": 4 + "editor.insertSpaces": true, + "editor.tabSize": 2 }, "[javascript]": { "editor.defaultFormatter": "dbaeumer.vscode-eslint", - "editor.insertSpaces": false, - "editor.tabSize": 4 + "editor.insertSpaces": true, + "editor.tabSize": 2 }, "[json]": { "editor.defaultFormatter": "vscode.json-language-features", @@ -26,9 +26,10 @@ "editor.insertSpaces": false, "editor.tabSize": 4 }, - "typescript.tsdk": "node_modules/typescript/lib", - "typescript.enablePromptUseWorkspaceTsdk": true, + "js/ts.tsdk.path": "node_modules/typescript/lib", + "js/ts.tsdk.promptToUseWorkspaceVersion": true, "eslint.validate": [ + "javascript", "typescript", "json", "jsonc" @@ -37,5 +38,10 @@ "*.json": "jsonc" }, "eslint.enable": true, - "eslint.format.enable": true + "eslint.format.enable": true, + "eslint.workingDirectories": [ + { + "mode": "auto" + } + ] } \ No newline at end of file diff --git a/addons/files/gameplay_changes/graves/README.md b/addons/files/gameplay_changes/graves/README.md new file mode 100644 index 000000000..98465ea91 --- /dev/null +++ b/addons/files/gameplay_changes/graves/README.md @@ -0,0 +1,39 @@ +# Graves + +When you die, a grave keeps everything — inventory, armor, offhand and XP. It +never burns, never explodes, floats on lava, and gets rescued from the void. + +- **Interact** — opens the grave like a chest; it disappears once emptied. +- **Sneak + interact** — everything straight back into its original slots. +- **Hit it twice** — scatters the contents on the ground. + +Only the owner can open a grave, unless grave robbing is enabled or the opener +holds a grave key (consumed on use). + +Your own graves show as waypoints on the locator bar (owner only, both a +server-wide and a per-player toggle). + +## Commands + +| Command | Who | What | +| --- | --- | --- | +| `/bt_gc_graves:graves` | anyone | your grave list | +| `/bt_gc_graves:config` | anyone | settings | +| `/bt_gc_graves:guide` | anyone | in-game guide | +| `/bt_gc_graves:gravekey [player] [amount]` | operator | hand out grave keys | +| `/bt_gc_graves:gravesadmin ` | operator | panel, purge, enable/disable | + +## Development + +```bash +yarn run watch # live recompilation +yarn run build +yarn run lint +``` + +- Run a build once after cloning so the generated i18n/guides modules exist. +- Install the `core-ui-*.mcpack` in your test world to see the custom UI. +- The grave entity is unkillable by design; the only removal path is + `entity.remove()` — never `/kill`, never `runCommand('kill ...')`. +- The grave index (world dynamic properties) is the source of truth; grave + entities in unloaded chunks do not exist to `getEntities()`. diff --git a/addons/files/gameplay_changes/graves/config.json b/addons/files/gameplay_changes/graves/config.json index 53a8b723c..cf3a75568 100644 --- a/addons/files/gameplay_changes/graves/config.json +++ b/addons/files/gameplay_changes/graves/config.json @@ -1,40 +1,86 @@ { "$schema": "https://raw.githubusercontent.com/Bedrock-OSS/regolith-schemas/main/config/v1.4.json", - "name": "bedrock-tweaks/graves", "author": "DrAv0011", + "description": "When you die, it places a grave to save all your drops instead of dropping them.", + "name": "gc_graves", "packs": { "behaviorPack": "./packs/BP", "resourcePack": "./packs/RP" }, "regolith": { "dataPath": "./packs/data", - "formatVersion": "1.4.0", "filterDefinitions": { "bundler": { + "url": "github.com/bedrock-core/regolith-filters", + "version": "1.1.1" + }, + "generator": { + "url": "github.com/bedrock-core/regolith-filters", + "version": "1.1.0" + }, + "guides": { + "url": "github.com/bedrock-core/regolith-filters", + "version": "1.1.1" + }, + "i18n": { "url": "github.com/bedrock-core/regolith-filters", "version": "1.0.1" } }, + "formatVersion": "1.4.0", "profiles": { "build": { "export": { "build": "standard", - "readOnly": false, + "readOnly": true, "target": "local" }, "filters": [ { - "filter": "bundler" + "filter": "generator", + "settings": { + "include": ["**/*.ts"], + "exclude": ["BP/scripts/**", "data/**", "**/*.d.ts"], + "pretty": false + } + }, + { + "filter": "guides", + "settings": { + "namespace": "bt_gc_graves" + } + }, + { "filter": "i18n" }, + { + "filter": "bundler", + "settings": { + "debug": false + } } ] }, "default": { "export": { + "build": "standard", "readOnly": false, - "target": "development", - "build": "standard" + "target": "development" }, "filters": [ + { + "filter": "generator", + "settings": { + "include": ["**/*.ts"], + "exclude": ["BP/scripts/**", "data/**", "**/*.d.ts"], + "pretty": true + } + }, + { + "filter": "guides", + "settings": { + "namespace": "bt_gc_graves" + } + }, + { "filter": "i18n" }, { "filter": "bundler", "settings": { diff --git a/addons/files/gameplay_changes/graves/eslint.config.mjs b/addons/files/gameplay_changes/graves/eslint.config.mjs index c772a561f..eed74cf97 100644 --- a/addons/files/gameplay_changes/graves/eslint.config.mjs +++ b/addons/files/gameplay_changes/graves/eslint.config.mjs @@ -1,397 +1,146 @@ -import minecraftLinting from "eslint-plugin-minecraft-linting"; -import stylistic from "@stylistic/eslint-plugin"; -import { defineConfig } from "eslint/config"; -import tseslint from "typescript-eslint"; -import json from "@eslint/json"; -import { fileURLToPath } from "url"; -import { dirname } from "path"; +import js from '@eslint/js'; +import json from '@eslint/json'; +import stylistic from '@stylistic/eslint-plugin'; +import { defineConfig } from 'eslint/config'; +import minecraftLinting from 'eslint-plugin-minecraft-linting'; +import { dirname } from 'path'; +import { fileURLToPath } from 'url'; +import tseslint from 'typescript-eslint'; -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); +const __dirname = dirname(fileURLToPath(import.meta.url)); export default defineConfig([ - { - ignores: [ - ".*/**", // Any directory starting with dot (.yarn, .vscode, .regolith, etc.) - ".*", // Any file starting with dot - "node_modules/**", - "**/*.*js", // Generated JS files - "filters/**", // The filters directory - "build/**", // Build output - "*.json", // Root level JSON files (config.json, package.json, tsconfig.json) - "*.md", // Root level markdown files - "*.mjs", // Root level mjs files (like this config) - "*.js", // Root level js files - ], - }, - - { - plugins: { - json, - }, - }, - - // lint JSON files - { - files: ["**/*.json"], - language: "json/jsonc", - rules: { - "json/no-duplicate-keys": "error", - "json/no-empty-keys": "off", - "json/no-unnormalized-keys": "error", - "json/no-unsafe-values": "error", - "json/sort-keys": "off", - "json/top-level-interop": "off", - } - }, - - { - files: ["packs/BP/scripts/**/*.ts"], - plugins: { - "@stylistic": stylistic, - "@typescript-eslint": tseslint.plugin, - "@minecraft": minecraftLinting - }, - - languageOptions: { - parser: tseslint.parser, - ecmaVersion: "latest", - sourceType: "module", - parserOptions: { - project: ["tsconfig.json"], - tsconfigRootDir: __dirname, - }, - }, - - rules: { - "no-unused-expressions": "off", - "@typescript-eslint/no-unused-expressions": "off", - "@typescript-eslint/explicit-member-accessibility": ["error", - { - accessibility: "no-public", - } - ], - - "@typescript-eslint/no-restricted-types": [ - "error", - { - "types": { - "Object": { - "message": "Avoid using the `Object` type. Did you mean `object`?" - }, - "Function": { - "message": "Avoid using the `Function` type. Prefer a specific function type, like `() => void`." - }, - "Boolean": { - "message": "Avoid using the `Boolean` type. Did you mean `boolean`?" - }, - "Number": { - "message": "Avoid using the `Number` type. Did you mean `number`?" - }, - "String": { - "message": "Avoid using the `String` type. Did you mean `string`?" - }, - "Symbol": { - "message": "Avoid using the `Symbol` type. Did you mean `symbol`?" - } - } - } - ], - "@typescript-eslint/no-wrapper-object-types": "error", - "@typescript-eslint/no-unsafe-function-type": "error", - "@typescript-eslint/no-empty-object-type": ["error", - { - allowInterfaces: 'always' - } - ], - "@typescript-eslint/ban-ts-comment": ["error", - { - 'ts-expect-error': 'allow-with-description' - } - ], - - "@typescript-eslint/consistent-type-assertions": "error", - "@typescript-eslint/dot-notation": "error", - - "@typescript-eslint/member-ordering": ["error", - { - default: [ - "private-field", - "protected-field", - "public-field", - "constructor", - "public-method", - "protected-method", - "private-method", - ], - } - ], - - "@typescript-eslint/naming-convention": ["error", - { - selector: "default", - format: ["camelCase"], - }, - { - selector: "default", - modifiers: ["unused"], - - filter: { - regex: "^(_|_.*_?)$", - match: true, - }, - - format: null, - }, - { - selector: "variable", - format: ["camelCase"], - }, - { - selector: ["parameter"], - format: ["camelCase", "snake_case"], - }, - { - selector: "variable", - modifiers: ["const"], - format: ["camelCase", "UPPER_CASE", "PascalCase"], - }, - { - selector: ["enum", "enumMember", "function"], - format: ["camelCase", "UPPER_CASE", "PascalCase"], - }, - { - selector: ["property", "parameterProperty", "accessor"], - modifiers: ["private"], - format: ["camelCase", "snake_case"], - leadingUnderscore: "require", - }, - { - selector: ["property", "parameterProperty", "accessor"], - modifiers: ["private", "readonly"], - format: ["UPPER_CASE", "camelCase"], - leadingUnderscore: "allow", - }, - { - selector: ["property"], - modifiers: ["readonly"], - format: ["camelCase", "UPPER_CASE"], - }, - { - selector: ["objectLiteralProperty", "typeProperty"], - format: ["camelCase", "snake_case", "UPPER_CASE"], - }, - { - selector: "typeLike", - format: ["PascalCase"], - } - ], - - "@typescript-eslint/explicit-function-return-type": ["error", - { - allowExpressions: true, - } - ], - - "@typescript-eslint/no-unnecessary-type-assertion": "error", - "@typescript-eslint/default-param-last": "warn", - "@typescript-eslint/no-explicit-any": "warn", - "@typescript-eslint/no-unnecessary-boolean-literal-compare": "warn", - "@typescript-eslint/prefer-enum-initializers": "warn", - - "@typescript-eslint/unbound-method": ["error", - { - ignoreStatic: true, - } - ], - - eqeqeq: ["error", "always", - { - null: "ignore", - } - ], - - "object-shorthand": "error", - - "@typescript-eslint/no-unused-vars": ["error", - { - argsIgnorePattern: "^(_|_.*_?)$", - } - ], - - "arrow-body-style": "warn", - curly: ["warn", "multi-line", "consistent"], - "@typescript-eslint/array-type": "warn", - "@typescript-eslint/prefer-optional-chain": "warn", - "@typescript-eslint/prefer-reduce-type-parameter": "warn", - "@stylistic/array-bracket-newline": "off", - "@stylistic/array-bracket-spacing": ["warn"], - "@stylistic/array-element-newline": "off", - "@stylistic/arrow-parens": ["warn", "as-needed"], - "@stylistic/arrow-spacing": ["warn"], - "@stylistic/block-spacing": ["warn", "always"], - - "@stylistic/brace-style": ["warn", "1tbs", - { - allowSingleLine: true, - } - ], - - "@stylistic/comma-dangle": ["warn", - { - arrays: "always-multiline", - objects: "always-multiline", - imports: "never", - exports: "never", - functions: "always-multiline", - enums: "never", - generics: "never", - tuples: "always-multiline", - } - ], - - "@stylistic/comma-spacing": ["warn", - { - before: false, - after: true, - } - ], - - "@stylistic/computed-property-spacing": ["warn"], - "@stylistic/dot-location": ["warn", "property"], - "@stylistic/eol-last": ["warn"], - "@stylistic/function-call-argument-newline": ["warn", "consistent"], - "@stylistic/function-paren-newline": ["warn", "consistent"], - "@stylistic/generator-star-spacing": "off", - "@stylistic/implicit-arrow-linebreak": ["warn", "beside"], - "@stylistic/indent": ["warn", "tab"], - "@stylistic/jsx-quotes": "off", - - "@stylistic/key-spacing": ["warn", - { - beforeColon: false, - afterColon: true, - mode: "strict", - } - ], - - "@stylistic/keyword-spacing": ["warn"], - "@stylistic/linebreak-style": ["warn", "windows"], - - "@stylistic/lines-around-comment": ["warn", - { - afterBlockComment: false, - beforeLineComment: false, - afterLineComment: false, - allowBlockStart: true, - allowClassStart: true, - allowObjectStart: true, - allowArrayStart: true, - } - ], - - "@stylistic/lines-between-class-members": ["warn", "always", - { - exceptAfterSingleLine: true, - exceptAfterOverload: false, - } - ], - - "@stylistic/max-len": "off", - "@stylistic/max-statements-per-line": "off", - "@stylistic/multiline-ternary": "off", - "@stylistic/new-parens": ["warn"], - "@stylistic/newline-per-chained-call": "off", - "@stylistic/no-confusing-arrow": "off", - "@stylistic/no-extra-parens": ["warn"], - "@stylistic/no-extra-semi": ["warn"], - "@stylistic/no-floating-decimal": ["warn"], - "@stylistic/no-mixed-operators": "off", - "@stylistic/no-mixed-spaces-and-tabs": ["warn"], - "@stylistic/no-multi-spaces": ["warn"], - - "@stylistic/no-multiple-empty-lines": ["warn", - { - max: 1, - } - ], - - "@stylistic/no-tabs": ["off"], - "@stylistic/no-trailing-spaces": ["warn"], - "@stylistic/no-whitespace-before-property": ["warn"], - "@stylistic/nonblock-statement-body-position": "off", - - "@stylistic/object-curly-newline": ["warn", - { - multiline: true, - } - ], - - "@stylistic/object-curly-spacing": ["warn", "always"], - "@stylistic/object-property-newline": "off", - "@stylistic/one-var-declaration-per-line": "off", - "@stylistic/operator-linebreak": "off", - "@stylistic/padded-blocks": ["warn", "never"], - - "@stylistic/padding-line-between-statements": ["warn", - { - blankLine: "always", - prev: "*", - next: "return", - } - ], - - "@stylistic/quote-props": ["warn", "as-needed"], - - "@stylistic/quotes": ["warn", "single", - { - avoidEscape: true, - allowTemplateLiterals: 'always', - } - ], - - "@stylistic/rest-spread-spacing": ["warn", "never"], - "@stylistic/semi": ["warn", "always"], - "@stylistic/semi-spacing": ["warn"], - "@stylistic/semi-style": ["warn", "last"], - "@stylistic/space-before-blocks": ["warn"], - "@stylistic/space-before-function-paren": ["warn", { - "anonymous": "never", - "named": "never", - "asyncArrow": "never", - "catch": "always" - }], - "@stylistic/space-in-parens": ["warn", "never"], - "@stylistic/space-infix-ops": "warn", - "@stylistic/space-unary-ops": "off", - "@stylistic/spaced-comment": ["warn", "always"], - "@stylistic/switch-colon-spacing": ["warn"], - "@stylistic/template-curly-spacing": ["warn"], - "@stylistic/template-tag-spacing": "off", - "@stylistic/wrap-iife": "off", - "@stylistic/wrap-regex": "off", - "@stylistic/yield-star-spacing": "off", - "@stylistic/member-delimiter-style": ["warn"], - "@stylistic/type-annotation-spacing": "warn", - "@stylistic/jsx-child-element-spacing": "off", - "@stylistic/jsx-closing-bracket-location": "off", - "@stylistic/jsx-closing-tag-location": "off", - "@stylistic/jsx-curly-brace-presence": "off", - "@stylistic/jsx-curly-newline": "off", - "@stylistic/jsx-curly-spacing": "off", - "@stylistic/jsx-equals-spacing": "off", - "@stylistic/jsx-first-prop-new-line": "off", - "@stylistic/jsx-indent": "off", - "@stylistic/jsx-indent-props": "off", - "@stylistic/jsx-max-props-per-line": "off", - "@stylistic/jsx-newline": "off", - "@stylistic/jsx-one-expression-per-line": "off", - "@stylistic/jsx-props-no-multi-spaces": "off", - "@stylistic/jsx-self-closing-comp": "off", - "@stylistic/jsx-sort-props": "off", - "@stylistic/jsx-tag-spacing": "off", - "@stylistic/jsx-wrap-multilines": "off", - "@minecraft/avoid-unnecessary-command": "error", - }, - } + { + ignores: [ + 'node_modules/**', + '**/*.generated.*', // Filter-generated files (i18n bundle + declarations) + 'packs/data/generated/**', // Minecraft schema types (generator filter) + '.*/**', + '**/*.*js', + 'filters/**', + 'build/**', + ], + }, + + { + files: ['**/*.json', '**/*.jsonc'], + plugins: { json }, + language: 'json/jsonc', + extends: ['json/recommended'], + }, + + // Stylistic configuration factory (only for JS/TS files) + { + files: ['**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx'], + ...stylistic.configs.customize({ + indent: 2, + quotes: 'single', + semi: true, + jsx: true, + braceStyle: '1tbs', + }), + }, + + // Global padding rules (only for JS/TS files) + { + files: ['**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx'], + rules: { + '@stylistic/padding-line-between-statements': [ + 'error', + { blankLine: 'always', prev: ['const', 'let', 'var'], next: '*' }, + { blankLine: 'any', prev: ['const', 'let', 'var'], next: ['const', 'let', 'var'] }, + { blankLine: 'any', prev: ['case', 'default'], next: 'break' }, + { blankLine: 'any', prev: 'case', next: 'case' }, + { blankLine: 'always', prev: '*', next: 'return' }, + { blankLine: 'always', prev: 'block', next: '*' }, + { blankLine: 'always', prev: '*', next: 'block' }, + { blankLine: 'always', prev: 'block-like', next: '*' }, + { blankLine: 'always', prev: '*', next: 'block-like' }, + { blankLine: 'always', prev: ['import'], next: ['const', 'let', 'var'] }, + ], + }, + }, + + // TypeScript files + { + files: ['**/*.ts', '**/*.tsx'], + ignores: ['**/*.d.ts'], + extends: [ + js.configs.recommended, + ...tseslint.configs.recommended, + ], + plugins: { + '@minecraft': minecraftLinting, + }, + languageOptions: { + parser: tseslint.parser, + ecmaVersion: 'latest', + sourceType: 'module', + parserOptions: { + projectService: true, + tsconfigRootDir: __dirname, + }, + }, + rules: { + // TypeScript + '@typescript-eslint/no-unsafe-function-type': 'error', + '@typescript-eslint/no-explicit-any': 'error', + '@typescript-eslint/no-empty-object-type': ['error', { allowInterfaces: 'always' }], + '@typescript-eslint/no-inferrable-types': 'off', + '@typescript-eslint/explicit-function-return-type': ['warn', { allowExpressions: true }], + '@typescript-eslint/no-unsafe-type-assertion': 'error', + '@typescript-eslint/no-unnecessary-type-assertion': 'error', + '@typescript-eslint/no-wrapper-object-types': 'error', + '@typescript-eslint/ban-ts-comment': ['error', { 'ts-expect-error': 'allow-with-description' }], + '@typescript-eslint/consistent-type-assertions': 'error', + '@typescript-eslint/dot-notation': 'error', + '@typescript-eslint/member-ordering': [ + 'error', + { + default: [ + 'private-field', + 'protected-field', + 'public-field', + 'constructor', + 'public-method', + 'protected-method', + 'private-method', + ], + }, + ], + '@typescript-eslint/naming-convention': [ + 'error', + { selector: 'default', format: ['camelCase'] }, + { selector: 'default', modifiers: ['unused'], filter: { regex: '^(_|_.*_?)$', match: true }, format: null }, + { selector: 'variable', format: ['camelCase'] }, + { selector: 'variable', modifiers: ['const'], format: ['camelCase', 'UPPER_CASE', 'PascalCase'] }, + { selector: ['enum', 'enumMember', 'function'], format: ['camelCase', 'UPPER_CASE', 'PascalCase'] }, + { selector: ['property', 'parameterProperty', 'accessor'], modifiers: ['private'], format: ['camelCase'], leadingUnderscore: 'require' }, + { selector: ['property', 'parameterProperty', 'accessor'], modifiers: ['private', 'readonly'], format: ['UPPER_CASE', 'camelCase'], leadingUnderscore: 'allow' }, + { selector: ['property'], modifiers: ['readonly'], format: ['camelCase', 'UPPER_CASE'] }, + { selector: ['objectLiteralProperty', 'typeProperty'], format: ['camelCase', 'snake_case', 'UPPER_CASE', 'PascalCase'], leadingUnderscore: 'allowDouble' }, + // Namespaced Minecraft JSON keys ("minecraft:physics", "bt:gc.graves") in generator templates + { selector: ['objectLiteralProperty', 'typeProperty'], modifiers: ['requiresQuotes'], format: null }, + // Config group metadata keys ($label, $description) + { selector: ['objectLiteralProperty', 'typeProperty'], filter: { regex: '^\\$[a-z][a-zA-Z0-9]*$', match: true }, format: null }, + // i18n plural leaves: camelCase base + CLDR category suffix (stock_one, keyGiven_other) + { selector: ['objectLiteralProperty', 'typeProperty'], filter: { regex: '^[a-z][a-zA-Z0-9]*_(zero|one|two|few|many|other)$', match: true }, format: null }, + { selector: 'typeLike', format: ['PascalCase'] }, + ], + '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^(_|_.*_?)$' }], + '@typescript-eslint/prefer-optional-chain': 'warn', + '@typescript-eslint/unbound-method': ['error', { ignoreStatic: true }], + // @stylistic + '@stylistic/jsx-curly-brace-presence': ['warn', 'always'], + // eslint + 'arrow-body-style': ['error', 'as-needed'], + 'curly': 'warn', + 'no-console': ['warn', { allow: ['info', 'warn', 'error'] }], + 'prefer-const': 'warn', + 'eqeqeq': ['error', 'always', { null: 'ignore' }], + 'object-shorthand': 'error', + // Minecraft + '@minecraft/avoid-unnecessary-command': 'error', + }, + }, ]); diff --git a/addons/files/gameplay_changes/graves/package.json b/addons/files/gameplay_changes/graves/package.json index 7393208ee..8993f0db2 100644 --- a/addons/files/gameplay_changes/graves/package.json +++ b/addons/files/gameplay_changes/graves/package.json @@ -1,31 +1,29 @@ { "name": "@bedrock-tweaks/graves", - "version": "1.21.0-1.0.0", - "description": "Graves addon - place graves when you die to retrieve your items", + "version": "1.0.0", + "description": "When you die, it places a grave to save all your drops instead of dropping them.", "private": true, "scripts": { "regolith-install": "regolith install-all", "build": "regolith run build", - "dev": "regolith watch", - "lint": "npx eslint ." + "watch": "regolith watch", + "lint": "eslint ." }, "dependencies": { - "@minecraft/common": "1.2.0", - "@minecraft/math": "2.2.11", - "@minecraft/server": "2.2.0", - "@minecraft/server-ui": "^2.0.0", - "@minecraft/vanilla-data": "1.21.114", - "@stylistic/eslint-config": "^1.1.0" + "@bedrock-core/server": "^0.1.0", + "@bedrock-core/ui": "^0.11.0", + "@minecraft/common": "1.3.0", + "@minecraft/server": "2.9.0", + "@minecraft/server-ui": "2.1.0", + "@minecraft/vanilla-data": "1.26.44" }, "devDependencies": { - "@eslint/js": "^9.38.0", - "@eslint/json": "^0.13.2", - "@stylistic/eslint-plugin": "^5.4.0", - "@typescript-eslint/eslint-plugin": "^8.46.1", - "@typescript-eslint/parser": "^8.46.1", - "eslint": "^9.38.0", - "eslint-plugin-minecraft-linting": "^2.0.10", - "typescript": "^5.9.3", - "typescript-eslint": "^8.46.1" + "@eslint/js": "^10.0.1", + "@eslint/json": "^2.0.0", + "@stylistic/eslint-plugin": "^5.10.0", + "eslint": "^10.4.1", + "eslint-plugin-minecraft-linting": "^1.2.7", + "typescript": "^6.0.3", + "typescript-eslint": "^8.60.1" } } diff --git a/addons/files/gameplay_changes/graves/packs/BP/entities/grave.entity.ts b/addons/files/gameplay_changes/graves/packs/BP/entities/grave.entity.ts new file mode 100644 index 000000000..cbdb7f80a --- /dev/null +++ b/addons/files/gameplay_changes/graves/packs/BP/entities/grave.entity.ts @@ -0,0 +1,59 @@ +/** + * The grave (§3a/§3b): ONE custom entity — an engine-persisted 41-slot + * container (36 inventory + 5 equipment), frozen in place (no gravity, no + * collision, no push), immune to everything. The damage sensor lets a + * player's swing land (so entityHitEntity fires and weapons behave normally) + * while dealing zero damage — the ONLY removal path is entity.remove() from + * script. /kill does not work on graves; that is intended. + */ +export default { + 'format_version': '1.21.0', + 'minecraft:entity': { + description: { + identifier: 'bt:gc_graves.grave', + is_spawnable: false, + is_summonable: true, + is_experimental: false, + properties: { + // Drives the RP shake animation; set from script on the first hit. + 'bt:shaking': { type: 'bool', default: false, client_sync: true }, + }, + }, + components: { + 'minecraft:inventory': { + container_type: 'container', + inventory_size: 41, + // `private: true` also blocks the container screen from ever opening + // (observed in-game; the docs only mention death drops), and + // `restrict_to_owner` is an engine gate that would break the grave-key + // and robbing paths. Both stay off: access control is OUR before-event + // (open/gate.ts), and a grave cannot die, so nothing ever drops. + private: false, + restrict_to_owner: false, + can_be_siphoned_from: false, + }, + 'minecraft:physics': { has_gravity: false, has_collision: false }, + 'minecraft:pushable': { is_pushable: false, is_pushable_by_piston: false }, + 'minecraft:knockback_resistance': { value: 1.0 }, + 'minecraft:damage_sensor': { + triggers: [ + { + // Players: the hit lands (event fires, weapon durability applies), grave takes 0 damage. + cause: 'entity_attack', + deals_damage: 'no_but_side_effects_apply', + on_damage: { filters: { test: 'is_family', subject: 'other', value: 'player' } }, + }, + // Everything else — creeper, TNT, lava, wither, void — nothing. + { cause: 'all', deals_damage: 'no' }, + ], + }, + 'minecraft:health': { value: 1024, max: 1024 }, + 'minecraft:collision_box': { width: 0.8, height: 0.9 }, + 'minecraft:fire_immune': {}, + 'minecraft:persistent': {}, + 'minecraft:type_family': { family: ['grave', 'inanimate'] }, + 'minecraft:nameable': { allow_name_tag_renaming: false, always_show: true }, + 'minecraft:conditional_bandwidth_optimization': {}, + }, + }, +} satisfies Entity; diff --git a/addons/files/gameplay_changes/graves/packs/BP/entities/grave.json b/addons/files/gameplay_changes/graves/packs/BP/entities/grave.json deleted file mode 100644 index bc5425815..000000000 --- a/addons/files/gameplay_changes/graves/packs/BP/entities/grave.json +++ /dev/null @@ -1,79 +0,0 @@ -{ - "format_version": "1.21.50", - "minecraft:entity": { - "description": { - "identifier": "bt:g.grave", - "is_spawnable": false, - "is_summonable": true - }, - "components": { - "minecraft:type_family": { - "family": [ - "bedrock_tweaks", - "graves", - "grave" - ] - }, - "minecraft:physics": { - "has_collision": true, - "has_gravity": false - }, - "minecraft:knockback_resistance": { - "value": 1 - }, - "minecraft:collision_box": { - "width": 0.8, - "height": 0.8 - }, - "minecraft:health": { - "value": 1, - "min": 1, - "max": 1 - }, - "minecraft:push_through": { - "value": 1 - }, - "minecraft:fire_immune": {}, - "minecraft:water_movement": { - "drag_factor": 0 - }, - "minecraft:pushable": { - "is_pushable": false, - "is_pushable_by_piston": true - }, - "minecraft:inventory": { - "additional_slots_per_strength": 0, - "can_be_siphoned_from": false, - "container_type": "container", - "inventory_size": 45, - "private": true, - "restrict_to_owner": true - }, - "minecraft:damage_sensor": { - "triggers": [ - { - "cause": "all", - "deals_damage": "no" - } - ] - }, - "minecraft:interact": { - "interactions": [ - { - "on_interact": { - "filters": [ - { - "test": "has_equipment", - "subject": "other", - "domain": "hand", - "value": "bt:g.grave_key" - } - ] - }, - "interact_text": "bt.graves.force_open_grave" - } - ] - } - } - } -} \ No newline at end of file diff --git a/addons/files/gameplay_changes/graves/packs/BP/functions/graves/config.mcfunction b/addons/files/gameplay_changes/graves/packs/BP/functions/graves/config.mcfunction deleted file mode 100644 index 138e15e22..000000000 --- a/addons/files/gameplay_changes/graves/packs/BP/functions/graves/config.mcfunction +++ /dev/null @@ -1 +0,0 @@ -execute as @s run scriptevent bt:g.config \ No newline at end of file diff --git a/addons/files/gameplay_changes/graves/packs/BP/functions/graves/uninstall.mcfunction b/addons/files/gameplay_changes/graves/packs/BP/functions/graves/uninstall.mcfunction deleted file mode 100644 index 3eb4b501c..000000000 --- a/addons/files/gameplay_changes/graves/packs/BP/functions/graves/uninstall.mcfunction +++ /dev/null @@ -1 +0,0 @@ -execute as @s run scriptevent bt:g.uninstall \ No newline at end of file diff --git a/addons/files/gameplay_changes/graves/packs/BP/items/grave_key.item.ts b/addons/files/gameplay_changes/graves/packs/BP/items/grave_key.item.ts new file mode 100644 index 000000000..47f78b337 --- /dev/null +++ b/addons/files/gameplay_changes/graves/packs/BP/items/grave_key.item.ts @@ -0,0 +1,21 @@ +/** + * The grave key (§3a gate 2): held in either hand it opens any grave, and one + * is consumed on success (creative players keep theirs). Given out with + * /:gravekey. The display name resolves client-side from the addon's own + * i18n key, so it follows each player's language. + */ +export default { + 'format_version': '1.21.0', + 'minecraft:item': { + description: { + identifier: 'bt:gc_graves.grave_key', + menu_category: { category: 'items' }, + }, + components: { + 'minecraft:icon': 'bt.gc_graves.grave_key', + 'minecraft:glint': true, + 'minecraft:max_stack_size': 16, + 'minecraft:display_name': { value: 'bt_gc_graves.item.graveKey' }, + }, + }, +} satisfies Item; diff --git a/addons/files/gameplay_changes/graves/packs/BP/items/grave_key.json b/addons/files/gameplay_changes/graves/packs/BP/items/grave_key.json deleted file mode 100644 index a5608d1f2..000000000 --- a/addons/files/gameplay_changes/graves/packs/BP/items/grave_key.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "format_version": "1.20.50", - "minecraft:item": { - "description": { - "identifier": "bt:g.grave_key" - }, - "components": { - "minecraft:icon": "bt:grave_key" - } - } -} \ No newline at end of file diff --git a/addons/files/gameplay_changes/graves/packs/BP/manifest.json b/addons/files/gameplay_changes/graves/packs/BP/manifest.json index d768bee1d..774253d99 100644 --- a/addons/files/gameplay_changes/graves/packs/BP/manifest.json +++ b/addons/files/gameplay_changes/graves/packs/BP/manifest.json @@ -1,59 +1,47 @@ { "format_version": 2, "header": { - "name": "Graves BP - Gameplay Changes - Bedrock Tweaks", - "description": "When you die it places a grave to save all your drops", - "uuid": "c7bec17f-9394-4cad-8f14-4aae4374d51f", + "name": "pack.name", + "description": "pack.description", + "uuid": "b3dcb4e4-69cb-4088-8ff3-2313367c42b8", "pack_scope": "world", - "version": [ - 1, - 0, - 0 - ], - "min_engine_version": [ - 1, - 21, - 50 - ] + "version": [1, 0, 0], + "min_engine_version": [1, 26, 40] }, "modules": [ { "type": "data", - "uuid": "36f57ff3-733b-41e0-9ba5-6e204969bd0c", - "version": [ - 1, - 0, - 0 - ] + "uuid": "3f2ff7a8-2482-49e7-9a76-5622b61c5318", + "version": [1, 0, 0] }, { "type": "script", "language": "javascript", - "uuid": "da5802ff-6c19-46d8-a27a-1f717580968b", + "uuid": "8ec2e94f-b226-4d04-b966-69f89812700e", "entry": "scripts/main.js", - "version": [ - 1, - 0, - 0 - ] + "version": [1, 0, 0] } ], "dependencies": [ { - "uuid": "e9412201-d5f3-4d0f-bb25-cc3bb051c9b9", - "version": [ - 1, - 0, - 0 - ] + "uuid": "49af505d-e4b5-43b7-9e6e-847a41228ece", + "version": [1, 0, 0] + }, + { + "uuid": "761ecd37-ad1c-4a64-862a-d6cc38767426", + "version": [1, 11, 0] + }, + { + "module_name": "@minecraft/server", + "version": "2.9.0" + }, + { + "module_name": "@minecraft/server-ui", + "version": "2.1.0" } ], "metadata": { "product_type": "addon", - "authors": [ - "Vanilla Tweaks", - "Bedrock Tweaks", - "DrAv0011" - ] + "authors": ["Bedrock Tweaks", "DrAv0011"] } -} \ No newline at end of file +} diff --git a/addons/files/gameplay_changes/graves/packs/BP/scripts/Actions/Grave.ts b/addons/files/gameplay_changes/graves/packs/BP/scripts/Actions/Grave.ts deleted file mode 100644 index 55acaad30..000000000 --- a/addons/files/gameplay_changes/graves/packs/BP/scripts/Actions/Grave.ts +++ /dev/null @@ -1,408 +0,0 @@ -import { - Block, - Container, - Dimension, - DimensionType, - DimensionTypes, - Entity, - EntityComponentTypes, - EntityEquippableComponent, - EntityInventoryComponent, - EquipmentSlot, - ItemStack, - Player, - system, - TicksPerSecond, - Vector3, - world -} from '@minecraft/server'; -import { Grave, GraveDynamicProperties, GravesList, GravesListDynamicProperties, GravesSettings } from '../Models'; -import { getProperties, setProperties } from '../Util'; -import { getSettings } from './settings'; -import { clampNumber, Vector3Utils } from '@minecraft/math'; -import { MinecraftBlockTypes, MinecraftDimensionTypes } from '@minecraft/vanilla-data'; -import { GravesEntityTypes } from '../Models'; - -/** - * Spawns a grave for a player, transferring their items and experience to the grave. - * - * @param {Player} player - The player whose items and experience will be stored in the grave. - */ -export const spawnGrave = (player: Player): void => { - if (!isInventoryEmpty(player)) { - const gravesSettings: GravesSettings = getSettings(); - - // TODO if bottom of the world and empty below place on top of cobble slab - // TODO After spawning tp grave 1 block up to be able to do above - const graveLocation: Vector3 = calculateGraveLocation(player.location, player.dimension); - - // Spawn grave entity at the determined location - const graveEntity: Entity = player.dimension.spawnEntity(GravesEntityTypes.Grave, graveLocation); - - graveEntity.nameTag = player.nameTag; - - // Transfer items to the grave and set its properties - const itemCount: number = transferItemsToGrave(player, graveEntity); - - setGraveProperties(player, graveEntity, itemCount); - - // Notify player of grave location if enabled in settings - if (gravesSettings.graveLocating) { - player.sendMessage({ - rawtext: [{ - translate: 'bt.graves.location', - with: [graveEntity.location.x.toString(), graveEntity.location.y.toString(), graveEntity.location.z.toString(), graveEntity.dimension.id], - }], - }); - } - } -}; - -/** - * Opens a grave for a player, transferring items and experience to the player, - * and removing the grave from the world. - * - * @param {Player} player - The player attempting to open the grave. - * @param {Entity} grave - The grave entity to be opened. - */ -export const openGrave = (player: Player, grave: Entity): void => { - const gravesSettings: GravesSettings = getSettings(); - const graveProperties: Grave = getProperties(grave, GraveDynamicProperties); - - if (player.id === graveProperties.ownerId || gravesSettings.graveRobbing) { - transferItemsToPlayer(player, grave); - - if (gravesSettings.xpCollection) { - player.addExperience(graveProperties.playerExperience); - } - - removeGraveFromList(grave); - - grave.remove(); - } -}; - -/** - * Forcibly opens a grave, retrieving all items and removing the grave. - * - * @param {Entity} grave - The grave entity to be force-opened. - */ -export const forceOpenGrave = (grave: Entity): void => { - getAllItems(grave); - removeGraveFromList(grave); - - grave.remove(); -}; - -/** - * Checks all graves in all dimensions and removes those that have exceeded the despawn time. - */ -export const tickGrave = (): void => { - const gravesSettings: GravesSettings = getSettings(); - - if (gravesSettings.despawnTime > 0) { - DimensionTypes.getAll().forEach((dimensionType: DimensionType): void => { - const dimension: Dimension = world.getDimension(dimensionType.typeId); - const currentDimensionGraves: Entity[] = dimension.getEntities({ type: GravesEntityTypes.Grave }); - - currentDimensionGraves.forEach((grave: Entity): void => { - const graveProperties: Grave = getProperties(grave, GraveDynamicProperties); - - if ((system.currentTick - graveProperties.spawnTime) / TicksPerSecond > gravesSettings.despawnTime) { - removeGraveFromList(grave); - - grave.remove(); - } - }); - }); - } -}; - -/** - * Transfers all items from a player's inventory and equipment slots to a grave. - * - * @param {Player} player - The player whose items will be transferred. - * @param {Entity} grave - The grave entity that will store the items. - * @returns {number} - The total number of items transferred. - */ -const transferItemsToGrave = (player: Player, grave: Entity): number => { - let itemCount: number = 0; - - const playerContainer: Container = (player.getComponent(EntityComponentTypes.Inventory) as EntityInventoryComponent) - ?.container; - const graveContainer: Container = (grave.getComponent(EntityComponentTypes.Inventory) as EntityInventoryComponent) - ?.container; - const playerArmor: EntityEquippableComponent = player.getComponent(EntityComponentTypes.Equippable) as EntityEquippableComponent; - - const playerContainerSize: number = playerContainer?.size; - if (playerContainer && graveContainer && playerContainerSize !== undefined) { - // Transfer items from inventory - for (let i: number = 0; i < playerContainerSize; i++) { - const itemStack: ItemStack | undefined = playerContainer.getItem(i); - if (itemStack) { - itemCount += itemStack.amount; - } - - playerContainer.moveItem(i, i, graveContainer); - } - - // Transfer items from equipment slots - let j: number = playerContainerSize; - for (const value of Object.values(EquipmentSlot)) { - j++; - - const itemStack: ItemStack | undefined = playerArmor.getEquipmentSlot(value).getItem(); - - if (itemStack) { - itemCount += itemStack.amount; - } - - graveContainer.setItem(j, itemStack); - playerArmor.setEquipment(value, undefined); - } - - // Clear remaining inventory - playerContainer.clearAll(); - } - - return itemCount; -}; - -/** - * Transfers items from a grave to a player's inventory or drops items - * that cannot be stored in the player's inventory. - * - * @param {Player} player - The player receiving the items. - * @param {Entity} grave - The grave containing items to be transferred. - */ -const transferItemsToPlayer = (player: Player, grave: Entity): void => { - const playerContainer: Container = (player.getComponent(EntityComponentTypes.Inventory) as EntityInventoryComponent) - ?.container; - const graveContainer: Container = (grave.getComponent(EntityComponentTypes.Inventory) as EntityInventoryComponent) - ?.container; - const playerArmor: EntityEquippableComponent = player.getComponent(EntityComponentTypes.Equippable) as EntityEquippableComponent; - - const playerContainerSize: number = playerContainer?.size; - - if (playerContainer && graveContainer && playerContainerSize !== undefined) { - const itemsToSpawn: ItemStack[] = []; - - // Transfer items from grave to player inventory - for (let i: number = 0; i < playerContainerSize; i++) { - if (!playerContainer.getSlot(i).hasItem()) { - graveContainer.moveItem(i, i, playerContainer); - } else { - const slotItem: ItemStack | undefined = graveContainer.getItem(i); - - if (slotItem) { - itemsToSpawn.push(playerContainer.getSlot(i).getItem() as ItemStack); - graveContainer.moveItem(i, i, playerContainer); - } - } - } - - // Transfer items from grave to player equipment slots - let j = playerContainerSize; - for (const value of Object.values(EquipmentSlot)) { - j++; - - const slotItem: ItemStack | undefined = graveContainer.getItem(j); - - if (!playerArmor.getEquipmentSlot(value).hasItem()) { - playerArmor.getEquipmentSlot(value).setItem(slotItem); - } else { - if (slotItem) { - itemsToSpawn.push(playerArmor.getEquipmentSlot(value).getItem() as ItemStack); - playerArmor.getEquipmentSlot(value).setItem(slotItem); - } - } - } - - spawnItemsInWorld(grave, itemsToSpawn); - } -}; - -/** - * Retrieves all items from a grave and spawns them in the world. - * - * @param {Entity} grave - The grave containing items. - */ -const getAllItems = (grave: Entity): void => { - const graveContainer: Container = (grave.getComponent(EntityComponentTypes.Inventory) as EntityInventoryComponent) - ?.container; - - const containerSize: number = graveContainer?.size; - - const itemsToSpawn: ItemStack[] = []; - - for (let i: number = 0; i < containerSize; i++) { - const item: ItemStack | undefined = graveContainer.getSlot(i).getItem(); - - if (item) { - itemsToSpawn.push(item); - } - } - - spawnItemsInWorld(grave, itemsToSpawn); -}; - -/** - * Spawns items in the world at the grave's location. - * - * @param {Entity} grave - The grave entity where items should spawn. - * @param {ItemStack[]} itemsToSpawn - List of items to be spawned in the world. - */ -const spawnItemsInWorld = (grave: Entity, itemsToSpawn: ItemStack[]): void => { - itemsToSpawn.forEach((item: ItemStack): void => { - grave.dimension.spawnItem(item, grave.location); - }); -}; - -/** - * Checks if the player's inventory, armor, and offhand slots are empty. - * - * @param {Player} player - The player whose inventory will be checked. - * @returns {boolean} - Returns true if all slots are empty; false otherwise. - */ -const isInventoryEmpty = (player: Player): boolean => { - const playerContainer: Container | undefined = player.getComponent(EntityComponentTypes.Inventory)?.container; - const playerArmor: EntityEquippableComponent | undefined = player.getComponent(EntityComponentTypes.Equippable); - - if (!playerContainer || !playerArmor) { - return true; - } - - let emptySlotsCount: number = playerContainer.emptySlotsCount || 0; - playerArmor.getEquipment(EquipmentSlot.Head) === undefined && emptySlotsCount++; - playerArmor.getEquipment(EquipmentSlot.Chest) === undefined && emptySlotsCount++; - playerArmor.getEquipment(EquipmentSlot.Legs) === undefined && emptySlotsCount++; - playerArmor.getEquipment(EquipmentSlot.Feet) === undefined && emptySlotsCount++; - playerArmor.getEquipment(EquipmentSlot.Offhand) === undefined && emptySlotsCount++; - - // Total slots include inventory, armor, and offhand - const totalSlotCount: number = playerContainer.size + 4 + 1; - - return emptySlotsCount === totalSlotCount; -}; - -/** - * Sets the properties of a grave, such as owner, experience, and item count. - * - * @param {Player} player - The player whose grave is being created. - * @param {Entity} grave - The grave entity to set properties on. - * @param {number} itemCount - The total number of items in the grave. - */ -const setGraveProperties = (player: Player, grave: Entity, itemCount: number): void => { - const graveProperties: Grave = { - id: grave.id, - ownerId: player.id, - ownerName: player.nameTag, - playerExperience: player.getTotalXp(), - spawnTime: system.currentTick, - location: grave.location, - dimension: grave.dimension.id as MinecraftDimensionTypes, - itemCount, - }; - - setProperties(grave, GraveDynamicProperties, graveProperties); - - // Clear player experience (min bound) - player.addLevels(-(2 ** 24)); - - saveGraveToList(graveProperties); -}; - -/** - * Calculates a valid grave spawning location. - * - * @param {Vector3} initialLocation - The starting location for the grave. - * @param {Dimension} dimension - The spawning dimension of the grave. - * - * @returns {Vector3} - A valid grave location. - */ -const calculateGraveLocation = (initialLocation: Vector3, dimension: Dimension): Vector3 => { - // Define spawning limits - const minLimit: Vector3 = Vector3Utils.floor({ - x: initialLocation.x - 1, - y: dimension.heightRange.min, - z: initialLocation.z - 1, - }); - const maxLimit: Vector3 = Vector3Utils.floor({ - x: initialLocation.x + 1, - y: dimension.heightRange.max, - z: initialLocation.z + 1, - }); - - if (!isValidSpawnBlock(initialLocation, dimension)) { - for (let y: number = minLimit.y; y <= maxLimit.y; y++) { - for (let x: number = minLimit.x; x <= maxLimit.x; x++) { - for (let z: number = minLimit.z; z <= maxLimit.z; z++) { - if (isValidSpawnBlock({ x, y, z }, dimension)) { - return { x, y, z }; - } - } - } - } - } - - // If initialLocation is valid or no other valid location found, return the initial location clamped to world limits - return Vector3Utils.floor({ ...initialLocation, y: clampNumber(initialLocation.y, dimension.heightRange.min, dimension.heightRange.max) }); -}; - -/** - * Determines whether the block at a given location within a dimension is valid for spawning a grave. - * - * A valid spawn block is one that matches any of the types specified in the `validBlocks` array. - * This should include any "pass-through" blocks (like Air, Water, TallGrass, Flowers, Kelp, Bubbles, etc.). - * - * @param {Vector3} location - The location of the block to validate. - * @param {Dimension} dimension - The dimension in which to check the block. - * - * @returns {boolean} - If the block at the specified location is valid for spawning a grave - * - * - * Note: using {"@minecraft/server": "1.16.0"} we cannot check if a block is "pass-through" - * Needs to be updated when it will possible, so it can also be compatible with Addon blocks - */ -const isValidSpawnBlock = (location: Vector3, dimension: Dimension): boolean => { - const validBlocks: MinecraftBlockTypes[] = [ - MinecraftBlockTypes.Air, - MinecraftBlockTypes.Water, - // TODO add "pass-through" blocks like tall grass, flowers, kelp... - // TODO check flowing_water - ]; - - const block: Block | undefined = dimension.getBlock(location); - - if (!block) { - return false; - } - - return validBlocks.some((validBlockType: MinecraftBlockTypes): boolean => block.matches(validBlockType)); -}; - -/** - * Saves a grave's properties to the global graves list. - * - * @param {Grave} grave - The grave properties to be added to the list. - */ -const saveGraveToList = (grave: Grave): void => { - const gravesList: Grave[] = JSON.parse(getProperties(world, GravesListDynamicProperties).list); - - gravesList.push(grave); - - setProperties(world, GravesListDynamicProperties, { list: JSON.stringify(gravesList) }); -}; - -/** - * Removes a grave entity from the global list of graves. - * - * @param {Entity} grave - The grave entity to be removed. - */ -const removeGraveFromList = (grave: Entity): void => { - const gravesList: Grave[] = JSON.parse(getProperties(world, GravesListDynamicProperties).list); - - const filteredGraves: Grave[] = gravesList.filter((g: Grave): boolean => g.id !== grave.id); - - setProperties(world, GravesListDynamicProperties, { list: JSON.stringify(filteredGraves) }); -}; diff --git a/addons/files/gameplay_changes/graves/packs/BP/scripts/Actions/giveGraveKey.ts b/addons/files/gameplay_changes/graves/packs/BP/scripts/Actions/giveGraveKey.ts deleted file mode 100644 index 38c570bbe..000000000 --- a/addons/files/gameplay_changes/graves/packs/BP/scripts/Actions/giveGraveKey.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Container, EntityComponentTypes, EntityInventoryComponent, ItemStack, Player } from '@minecraft/server'; -import { GravesItemTypes } from '../Models'; - -/** - * Adds a Grave Key item to the player's inventory. - * - * @param {Player} player - The player to which the Grave Key will be given. - */ -export const giveGraveKey = (player: Player): void => { - const playerInventory: EntityInventoryComponent | undefined = player.getComponent(EntityComponentTypes.Inventory); - const playerContainer: Container | undefined = playerInventory?.container; - - playerContainer?.addItem(new ItemStack(GravesItemTypes.GraveKey)); -}; diff --git a/addons/files/gameplay_changes/graves/packs/BP/scripts/Actions/index.ts b/addons/files/gameplay_changes/graves/packs/BP/scripts/Actions/index.ts deleted file mode 100644 index ca6ce91fc..000000000 --- a/addons/files/gameplay_changes/graves/packs/BP/scripts/Actions/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export { giveGraveKey } from './giveGraveKey'; -export { forceOpenGrave, openGrave, spawnGrave, tickGrave } from './Grave'; -export { listAllGraves } from './listGraves'; -export { getSettings, initializeSettings, setSettings } from './settings'; -export { uninstall } from './uninstall'; - diff --git a/addons/files/gameplay_changes/graves/packs/BP/scripts/Actions/listGraves.ts b/addons/files/gameplay_changes/graves/packs/BP/scripts/Actions/listGraves.ts deleted file mode 100644 index b84e90309..000000000 --- a/addons/files/gameplay_changes/graves/packs/BP/scripts/Actions/listGraves.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { - Player, - system, - TicksPerSecond, - world -} from '@minecraft/server'; -import { Grave, GravesList, GravesListDynamicProperties } from '../Models'; -import { getProperties } from '../Util'; - -/** - * Lists all graves associated with the player. - * - * Retrieves the global graves list, organizes them by owner, sorts each owner's graves by spawn time, - * and sends the information to the specified player. If no graves are found, notifies the player accordingly. - * - * @param {Player} player - The player to whom the grave list will be sent. - */ -// TODO move to UI in UI update -export const listAllGraves = (player: Player): void => { - const gravesList: Grave[] = JSON.parse(getProperties(world, GravesListDynamicProperties).list); - - // Groups graves by owner - const gravesByOwner: { [owner: string]: Grave[] } = - gravesList.reduce((accumulator: { [owner: string]: Grave[] }, grave: Grave): { [owner: string]: Grave[] } => { - if (!accumulator[grave.ownerId]) { - accumulator[grave.ownerId] = []; - } - accumulator[grave.ownerId].push(grave); - - return accumulator; - }, {}); - - // Sort each owner's graves by spawnTime (oldest to newest) - const sortedGraves: Grave[][] = Object.values(gravesByOwner).map((group: Grave[]): Grave[] => group.sort((a: Grave, b: Grave): number => a.spawnTime - b.spawnTime)); - - if (sortedGraves.length) { - player.sendMessage({ translate: 'bt.graves.list_graves.title' }); - sortedGraves.forEach((playerGraves: Grave[]): void => { - player.sendMessage({ translate: 'bt.graves.list_graves.owner', with: [playerGraves[0].ownerName ?? ''] }); - - playerGraves.forEach((grave: Grave): void => { - const { hours, minutes }: { hours: number; minutes: number } = getTimeDifference(grave.spawnTime, system.currentTick); - - player.sendMessage({ - translate: 'bt.graves.list_graves.grave', - with: [ - grave.location.x.toFixed(0), - grave.location.y.toFixed(0), - grave.location.z.toFixed(0), - grave.dimension, - hours + '', - minutes + '', - grave.itemCount + '', - grave.playerExperience + '', - ], - }); - }); - }); - } else { - player.sendMessage({ translate: 'bt.graves.list_graves.empty' }); - } -}; - -/** - * Calculates the time difference between two times. - * - * Converts the difference in ticks to hours and minutes. - * - * @param {number} time1 - The first time in ticks. - * @param {number} time2 - The second time in ticks. - * @returns {{ hours: number; minutes: number }} - The difference represented in hours and minutes. - */ -const getTimeDifference = (time1: number, time2: number): { hours: number; minutes: number } => { - // Calculate the difference in ticks - const tickDifference: number = Math.abs(time1 - time2); - - // Convert the tick difference to seconds - const timeDifferenceInSeconds: number = tickDifference / TicksPerSecond; - - // Convert the time difference to hours and minutes - const hours: number = Math.floor(timeDifferenceInSeconds / 3600); // 3600 seconds in an hour - const minutes: number = Math.floor(timeDifferenceInSeconds % 3600 / 60); // Remaining seconds to minutes - - return { hours, minutes }; -}; diff --git a/addons/files/gameplay_changes/graves/packs/BP/scripts/Actions/settings.ts b/addons/files/gameplay_changes/graves/packs/BP/scripts/Actions/settings.ts deleted file mode 100644 index 74998d4e6..000000000 --- a/addons/files/gameplay_changes/graves/packs/BP/scripts/Actions/settings.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { world } from '@minecraft/server'; -import { GravesListDynamicProperties, GravesSettings, GravesSettingsDynamicProperties } from '../Models'; -import { getProperties, setProperties } from '../Util'; - -/** - * Initializes the general addon settings for graves if they are not already initialized. - * Sets default values for them and ensures required properties are set. - */ -export const initializeSettings = (): void => { - if (!getProperties(world, GravesSettingsDynamicProperties).initialized) { - setProperties( - world, - GravesSettingsDynamicProperties, - { - initialized: true, - graveLocating: true, - xpCollection: true, - graveRobbing: false, - despawnTime: 0, - keepInventory: world.gameRules.keepInventory, - }, - ); - - // Technically not a setting, but needed to be initialized - setProperties(world, GravesListDynamicProperties, { list: JSON.stringify([]) }); - - world.gameRules.keepInventory = true; - } -}; - -/** - * Retrieves the current addon settings from the world properties. - * - * @returns {GravesSettings} - The current graves settings - */ -export const getSettings = (): GravesSettings => getProperties(world, GravesSettingsDynamicProperties); - -/** - * Updates the addon settings in the world properties. - * - * @param {GravesSettings} graveSettings - The updated settings to be saved. - */ -export const setSettings = (graveSettings: GravesSettings): void => { - setProperties(world, GravesSettingsDynamicProperties, graveSettings); -}; diff --git a/addons/files/gameplay_changes/graves/packs/BP/scripts/Actions/uninstall.ts b/addons/files/gameplay_changes/graves/packs/BP/scripts/Actions/uninstall.ts deleted file mode 100644 index 85dca476c..000000000 --- a/addons/files/gameplay_changes/graves/packs/BP/scripts/Actions/uninstall.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { world } from '@minecraft/server'; -import { getSettings } from './settings'; -import { GravesSettings } from '../Models'; - -/** - * Uninstalls the grave addon by restoring game rules and clearing dynamic properties. - */ -export const uninstall = (): void => { - const settings: GravesSettings = getSettings(); - - world.gameRules.keepInventory = settings.keepInventory; - - world.clearDynamicProperties(); - world.sendMessage({ translate: 'bt.graves.uninstall' }); -}; diff --git a/addons/files/gameplay_changes/graves/packs/BP/scripts/Events/EntityDie.ts b/addons/files/gameplay_changes/graves/packs/BP/scripts/Events/EntityDie.ts deleted file mode 100644 index 6254642c2..000000000 --- a/addons/files/gameplay_changes/graves/packs/BP/scripts/Events/EntityDie.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { - EntityDieAfterEvent, - Player, - world -} from '@minecraft/server'; -import { MinecraftEntityTypes } from '@minecraft/vanilla-data'; -import { spawnGrave } from '../Actions'; - -world.afterEvents.entityDie.subscribe(({ deadEntity }: EntityDieAfterEvent): void => { - if (deadEntity.matches({ type: MinecraftEntityTypes.Player })) { - spawnGrave(deadEntity as Player); - } -}); diff --git a/addons/files/gameplay_changes/graves/packs/BP/scripts/Events/EntityHit.ts b/addons/files/gameplay_changes/graves/packs/BP/scripts/Events/EntityHit.ts deleted file mode 100644 index 8ea89476f..000000000 --- a/addons/files/gameplay_changes/graves/packs/BP/scripts/Events/EntityHit.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { - EntityHitEntityAfterEvent, - Player, - world -} from '@minecraft/server'; -import { MinecraftEntityTypes } from '@minecraft/vanilla-data'; -import { openGrave } from '../Actions'; -import { GravesEntityTypes } from '../Models'; - -world.afterEvents.entityHitEntity.subscribe(({ damagingEntity, hitEntity }: EntityHitEntityAfterEvent): void => { - if ( - damagingEntity.matches({ type: MinecraftEntityTypes.Player }) && - hitEntity.matches({ type: GravesEntityTypes.Grave }) - ) { - openGrave(damagingEntity as Player, hitEntity); - } -}); diff --git a/addons/files/gameplay_changes/graves/packs/BP/scripts/Events/GameRuleChange.ts b/addons/files/gameplay_changes/graves/packs/BP/scripts/Events/GameRuleChange.ts deleted file mode 100644 index dbca4379c..000000000 --- a/addons/files/gameplay_changes/graves/packs/BP/scripts/Events/GameRuleChange.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { - GameRule, - GameRuleChangeAfterEvent, - world -} from '@minecraft/server'; - -world.afterEvents.gameRuleChange.subscribe(({ rule, value }: GameRuleChangeAfterEvent): void => { - if (rule === GameRule.KeepInventory && value === false) { - world.sendMessage({ translate: 'bt.graves.keep_inventory' }); - - world.gameRules.keepInventory = true; - } -}); diff --git a/addons/files/gameplay_changes/graves/packs/BP/scripts/Events/PlayerInteractWithEntity.ts b/addons/files/gameplay_changes/graves/packs/BP/scripts/Events/PlayerInteractWithEntity.ts deleted file mode 100644 index bd272cad1..000000000 --- a/addons/files/gameplay_changes/graves/packs/BP/scripts/Events/PlayerInteractWithEntity.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { - PlayerInteractWithEntityAfterEvent, - world -} from '@minecraft/server'; -import { forceOpenGrave } from '../Actions'; -import { GravesEntityTypes, GravesItemTypes } from '../Models'; - -world.afterEvents.playerInteractWithEntity.subscribe(({ target, itemStack }: PlayerInteractWithEntityAfterEvent): void => { - if (target.matches({ type: GravesEntityTypes.Grave }) && itemStack?.matches(GravesItemTypes.GraveKey)) { - forceOpenGrave(target); - } -}); diff --git a/addons/files/gameplay_changes/graves/packs/BP/scripts/Events/ScriptEventReceive.ts b/addons/files/gameplay_changes/graves/packs/BP/scripts/Events/ScriptEventReceive.ts deleted file mode 100644 index dd9c778fb..000000000 --- a/addons/files/gameplay_changes/graves/packs/BP/scripts/Events/ScriptEventReceive.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { - Player, - ScriptEventCommandMessageAfterEvent, - system -} from '@minecraft/server'; -import { uninstall } from '../Actions'; -import { GravesScriptEvents } from '../Models'; -import { openConfigInterface } from '../UI'; -import { MinecraftEntityTypes } from '@minecraft/vanilla-data'; - -system.afterEvents.scriptEventReceive.subscribe(({ id, sourceEntity }: ScriptEventCommandMessageAfterEvent): void => { - switch (id) { - case GravesScriptEvents.config: - if (sourceEntity?.matches({ type: MinecraftEntityTypes.Player })) { - openConfigInterface(sourceEntity as Player); - } - break; - - case GravesScriptEvents.uninstall: - uninstall(); - break; - - default: - break; - } -}); diff --git a/addons/files/gameplay_changes/graves/packs/BP/scripts/Events/WorldInitialize.ts b/addons/files/gameplay_changes/graves/packs/BP/scripts/Events/WorldInitialize.ts deleted file mode 100644 index 424ef0a3a..000000000 --- a/addons/files/gameplay_changes/graves/packs/BP/scripts/Events/WorldInitialize.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { world } from '@minecraft/server'; -import { initializeSettings } from '../Actions'; - -world.afterEvents.worldLoad.subscribe((): void => { - initializeSettings(); -}); diff --git a/addons/files/gameplay_changes/graves/packs/BP/scripts/Events/index.ts b/addons/files/gameplay_changes/graves/packs/BP/scripts/Events/index.ts deleted file mode 100644 index aac30dd90..000000000 --- a/addons/files/gameplay_changes/graves/packs/BP/scripts/Events/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -import './EntityDie'; -import './EntityHit'; -import './GameRuleChange'; -import './PlayerInteractWithEntity'; -import './ScriptEventReceive'; -import './WorldInitialize'; diff --git a/addons/files/gameplay_changes/graves/packs/BP/scripts/Models/DynamicProperties.ts b/addons/files/gameplay_changes/graves/packs/BP/scripts/Models/DynamicProperties.ts deleted file mode 100644 index 8be516ec6..000000000 --- a/addons/files/gameplay_changes/graves/packs/BP/scripts/Models/DynamicProperties.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { Vector3 } from '@minecraft/server'; -import { MinecraftDimensionTypes } from '@minecraft/vanilla-data'; - -/** - * Definitions to use multiple Dynamic Properties as a Typed Object - * Enum keys and object properties should match for proper conversion. - */ -export enum GravesSettingsDynamicProperties { - initialized = 'bt:g.settings_initialized', - graveRobbing = 'bt:g.settings_grave_robbing', - xpCollection = 'bt:g.settings_xp_collection', - graveLocating = 'bt:g.settings_grave_locating', - despawnTime = 'bt:g.settings_despawn_time', - keepInventory = 'bt:g.keep_inventory' -} - -export interface GravesSettings { - // Indicates whether the graves system settings have been initialized. - initialized: boolean; - // Controls whether grave robbing is allowed. - graveRobbing: boolean; - // Controls whether XP can be collected from graves. - xpCollection: boolean; - // Controls whether there will be a chat message with the coordinates of their death - graveLocating: boolean; - // Specifies the time (in seconds) before graves automatically despawn and their contents are deleted - despawnTime: number; - // Stores the value of the keepInventory gamerule before installing the addon to be restored on uninstall - keepInventory: boolean; -} - -export enum GraveDynamicProperties { - id = 'bt:g.id', - ownerId = 'bt:g.owner_id', - ownerName = 'bt.g.owner_name', - location = 'bt:g.location', - dimension = 'bt:g.dimension', - spawnTime = 'bt:g.spawn_time', - itemCount = 'bt:g.item_count', - playerExperience = 'bt:g.player_experience' -} - -export interface Grave { - // The ID of the Grave - id: string; - // The ID of the player who owns the grave. - ownerId: string; - // The nametag of the player who owns the grave. - // Note: player nametags might change, so we check owner with id, but we also save the name to be able to display it in - // the graves list as you cannot get offline player data. If player changes nametag while there are graves on the world, - // and they do not spawn a new grave, it will show the old nametag but because is associated with id, the player will - // still be able to open the grave - ownerName: string; - // The location of the grave in the world - location: Vector3; - // The dimension where the grave is located - dimension: MinecraftDimensionTypes; - // The time in ticks since the start of the world indicating when the grave was created - spawnTime: number; - // The total number of items stored in the grave - itemCount: number; - // The amount of experience points stored in the grave - playerExperience: number; -} - -export enum GravesListDynamicProperties { - list = 'bt:g.graves_list' -} - -export interface GravesList { - // Stringified Grave[] - list: string; -} diff --git a/addons/files/gameplay_changes/graves/packs/BP/scripts/Models/EntityTypes.ts b/addons/files/gameplay_changes/graves/packs/BP/scripts/Models/EntityTypes.ts deleted file mode 100644 index d62bd58dd..000000000 --- a/addons/files/gameplay_changes/graves/packs/BP/scripts/Models/EntityTypes.ts +++ /dev/null @@ -1,3 +0,0 @@ -export enum GravesEntityTypes { - Grave = 'bt:g.grave' -} diff --git a/addons/files/gameplay_changes/graves/packs/BP/scripts/Models/ItemTypes.ts b/addons/files/gameplay_changes/graves/packs/BP/scripts/Models/ItemTypes.ts deleted file mode 100644 index 658bf282d..000000000 --- a/addons/files/gameplay_changes/graves/packs/BP/scripts/Models/ItemTypes.ts +++ /dev/null @@ -1,3 +0,0 @@ -export enum GravesItemTypes { - GraveKey = 'bt:g.grave_key' -} diff --git a/addons/files/gameplay_changes/graves/packs/BP/scripts/Models/ScriptEvents.ts b/addons/files/gameplay_changes/graves/packs/BP/scripts/Models/ScriptEvents.ts deleted file mode 100644 index 1d4b1a3b5..000000000 --- a/addons/files/gameplay_changes/graves/packs/BP/scripts/Models/ScriptEvents.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum GravesScriptEvents { - config = 'bt:g.config', - uninstall = 'bt:g.uninstall' -} diff --git a/addons/files/gameplay_changes/graves/packs/BP/scripts/Models/index.ts b/addons/files/gameplay_changes/graves/packs/BP/scripts/Models/index.ts deleted file mode 100644 index db1d71cbb..000000000 --- a/addons/files/gameplay_changes/graves/packs/BP/scripts/Models/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { Grave, GraveDynamicProperties, GravesList, GravesListDynamicProperties, GravesSettings, GravesSettingsDynamicProperties } from './DynamicProperties'; -export { GravesEntityTypes } from './EntityTypes'; -export { GravesItemTypes } from './ItemTypes'; -export { GravesScriptEvents } from './ScriptEvents'; diff --git a/addons/files/gameplay_changes/graves/packs/BP/scripts/UI/AdminPanel.tsx b/addons/files/gameplay_changes/graves/packs/BP/scripts/UI/AdminPanel.tsx new file mode 100644 index 000000000..b7a473a85 --- /dev/null +++ b/addons/files/gameplay_changes/graves/packs/BP/scripts/UI/AdminPanel.tsx @@ -0,0 +1,45 @@ +/** + * Every grave in the world, straight from the index — no chunk needs to be + * loaded (§6b). Rows navigate into the same detail screen the list uses. + * Same core screen shell as the list: Card parent, scroll below the header. + */ +import { Panel, Scroll, Text, useExit, useTranslation, type JSX } from '@bedrock-core/ui'; +import { Card, Header, MenuRow, theme } from '@bedrock-core/ui/ore-styled'; +import type { ScreenProps } from '@bedrock-core/ui/navigation'; +import { i18n } from './i18n'; +import { SettingsButton } from './SettingsButton'; +import { allRecords } from '../index/store'; +import { agoStr, dimName, posStr } from '../util'; +import type { GravesRoutes } from './GravesApp'; + +const { spacing } = theme.tokens; + +export function AdminPanel({ navigation }: ScreenProps): JSX.Element { + const bound = useTranslation(i18n); + const exit = useExit(); + const { t, key } = bound; + + const records = [...allRecords()].sort((a, b) => b.diedAt - a.diedAt); + + return ( + +
$.admin.title)} onClose={exit} /> + + + + + {records.length === 0 + ? {t($ => $.admin.empty)} + : records.map(record => ( + $.list.row, { count: record.items, xp: record.xp })} — ${agoStr(bound, record.diedAt)}`} + onPress={() => navigation.navigate('Detail', { graveId: record.id })} + /> + ))} + + + + + ); +} diff --git a/addons/files/gameplay_changes/graves/packs/BP/scripts/UI/Config.ts b/addons/files/gameplay_changes/graves/packs/BP/scripts/UI/Config.ts deleted file mode 100644 index 09ababd80..000000000 --- a/addons/files/gameplay_changes/graves/packs/BP/scripts/UI/Config.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Player } from '@minecraft/server'; -import { ActionFormData, ActionFormResponse } from '@minecraft/server-ui'; -import { giveGraveKey, listAllGraves } from '../Actions'; -import { openSettingsInterface } from './Settings'; - -export const openConfigInterface = (player: Player): void => { - const form: ActionFormData = new ActionFormData() - .title({ translate: 'bt.graves.config.title' }) - .button({ translate: 'bt.graves.config.change_settings' }) - .button({ translate: 'bt.graves.config.list_all_graves' }) - .button({ translate: 'bt.graves.config.receive_grave_key' }); - - form.show(player).then((response: ActionFormResponse): void => { - switch (response.selection) { - case 0: - openSettingsInterface(player); - - break; - case 1: - listAllGraves(player); - - break; - case 2: - giveGraveKey(player); - - break; - default: - break; - } - }); -}; diff --git a/addons/files/gameplay_changes/graves/packs/BP/scripts/UI/GraveDetail.tsx b/addons/files/gameplay_changes/graves/packs/BP/scripts/UI/GraveDetail.tsx new file mode 100644 index 000000000..fd79576a2 --- /dev/null +++ b/addons/files/gameplay_changes/graves/packs/BP/scripts/UI/GraveDetail.tsx @@ -0,0 +1,107 @@ +/** + * One grave: text summary only — no item icons by design (§4: ItemRenderer + * aux ids are unreliable in multi-addon worlds). Operators get teleport and + * purge actions. Core screen shell: Card parent, Header flush on top, info + * scrolls below it, actions pinned under the scroll. + */ +import { Fragment, Panel, Scroll, Text, usePlayer, useExit, useTranslation, type JSX } from '@bedrock-core/ui'; +import { Button, Card, Divider, Header, theme } from '@bedrock-core/ui/ore-styled'; +import { isOperator } from '@bedrock-core/server'; +import type { ScreenProps } from '@bedrock-core/ui/navigation'; +import { i18n } from './i18n'; +import { byEntityId, markPurge } from '../index/store'; +import { graveEntity, removeGrave } from '../lifecycle'; +import { agoStr, causeText, dimensionOf, dimName, posStr } from '../util'; +import type { GravesRoutes } from './GravesApp'; + +const { fontColor, spacing } = theme.tokens; + +export function GraveDetail({ navigation, route }: ScreenProps): JSX.Element { + const player = usePlayer(); + const bound = useTranslation(i18n); + const exit = useExit(); + const { t, key } = bound; + + const record = byEntityId(route.params.graveId); + + if (!record) { + // Looted or purged while this screen was open. + return ( + +
$.detail.title)} onBack={() => navigation.goBack()} onClose={exit} /> + + {t($ => $.list.empty)} + + + ); + } + + const admin = isOperator(player); + const cause = causeText(bound, record.cause, record.killer); + + const teleport = (): void => { + const dimension = dimensionOf(record.dim); + + if (!dimension) { + // The record outlived its dimension: nothing to teleport to, and the + // screen stays open rather than dropping the operator somewhere else. + console.warn(`[graves] cannot teleport to grave ${record.id}: dimension '${record.dim}' is gone`); + + return; + } + + player.teleport({ x: record.x + 0.5, y: record.y, z: record.z + 0.5 }, { dimension }); + exit(); + }; + + const purge = (): void => { + const entity = graveEntity(record.id); + + if (entity) { + removeGrave(entity); + } else { + markPurge(record.id); + } + + navigation.goBack(); + }; + + return ( + +
$.detail.title)} breadcrumbs={[record.ownerName]} onBack={() => navigation.goBack()} onClose={exit} /> + + + + + {`${fontColor.muted}${t($ => $.detail.owner)}: §r${record.ownerName}`} + {`${fontColor.muted}${t($ => $.detail.dimension)}: §r${dimName(bound, record.dim)}`} + {`${fontColor.muted}${t($ => $.detail.position)}: §r${posStr(record.x, record.y, record.z)}`} + + {t($ => $.detail.items, { count: record.items })} + {t($ => $.detail.xp, { amount: record.xp })} + {`${t($ => $.detail.when)}: ${agoStr(bound, record.diedAt)}`} + {cause ? {cause} : undefined} + {record.floating ? {`§6${t($ => $.list.floating)}`} : undefined} + {record.purge ? {`§c${t($ => $.detail.pendingPurge)}`} : undefined} + + + + {/* No Close button: the header already carries back and close, and a + third way out only crowds the screen. Operators still get their + actions, and the divider comes with them rather than floating over + an empty footer. */} + {admin + ? ( + + + + + + + + ) + : undefined} + + + ); +} diff --git a/addons/files/gameplay_changes/graves/packs/BP/scripts/UI/GraveList.tsx b/addons/files/gameplay_changes/graves/packs/BP/scripts/UI/GraveList.tsx new file mode 100644 index 000000000..cbaf643bf --- /dev/null +++ b/addons/files/gameplay_changes/graves/packs/BP/scripts/UI/GraveList.tsx @@ -0,0 +1,77 @@ +/** + * The player's own graves — one row per grave, newest first. Replaces the + * Java pack's paginated tellraw list; the scroll region below the header + * handles overflow, matching the core `ui(core)` screen shell. + * + * Each row carries its own locator-bar checkbox. This screen is the ONLY place + * that control exists, because it is the only screen that shows graves the + * viewer owns — the admin panel lists everyone's, and a waypoint onto someone + * else's death pile is not something this addon offers from anywhere. + */ +import { Panel, Scroll, Text, usePlayer, useExit, useState, useTranslation, type JSX } from '@bedrock-core/ui'; +import { Card, Checkbox, Header, MenuRow, theme } from '@bedrock-core/ui/ore-styled'; +import type { ScreenProps } from '@bedrock-core/ui/navigation'; +import { i18n } from './i18n'; +import { SettingsButton } from './SettingsButton'; +import { updateRecord, visibleRecordsOf } from '../index/store'; +import { agoStr, dimName, posStr } from '../util'; +import type { GraveRecord } from '../types'; +import type { GravesRoutes } from './GravesApp'; + +const { fontColor, spacing } = theme.tokens; + +export function GraveList({ navigation }: ScreenProps): JSX.Element { + const player = usePlayer(); + const bound = useTranslation(i18n); + const exit = useExit(); + const { t, key } = bound; + + // Re-read on every toggle: the checkbox writes to the index, and the index — + // not this component — is what the locator bar and the next open both read. + const [revision, setRevision] = useState(0); + const records = [...visibleRecordsOf(player.id)].sort((a, b) => b.diedAt - a.diedAt); + + const toggleWaypoint = (record: GraveRecord, show: boolean): void => { + // `undefined` rather than a false flag: the index is JSON, so the key + // simply stops being written, and "absent means shown" stays the one rule. + updateRecord({ ...record, noWaypoint: show ? undefined : true }); + setRevision(revision + 1); + }; + + return ( + +
$.list.title)} onClose={exit} /> + + + {records.length === 0 + ? {t($ => $.list.empty)} + : ( + + {`${fontColor.muted}${t($ => $.list.waypointHint)}`} + + + + {records.map(record => ( + + + $.list.row, { count: record.items, xp: record.xp })} + subtitle={`${dimName(bound, record.dim)} ${posStr(record.x, record.y, record.z)} · ${agoStr(bound, record.diedAt)}${record.floating ? ` · ${t($ => $.list.floating)}` : ''}`} + onPress={() => navigation.navigate('Detail', { graveId: record.id })} + /> + + toggleWaypoint(record, show)} + /> + + ))} + + + + + )} + + + ); +} diff --git a/addons/files/gameplay_changes/graves/packs/BP/scripts/UI/GravesApp.tsx b/addons/files/gameplay_changes/graves/packs/BP/scripts/UI/GravesApp.tsx new file mode 100644 index 000000000..0da0dcedc --- /dev/null +++ b/addons/files/gameplay_changes/graves/packs/BP/scripts/UI/GravesApp.tsx @@ -0,0 +1,39 @@ +/** + * The addon's own screen stack: grave list → detail, plus the operator panel. + * The shared config/guide/list screens come from `ui(core)` — nothing here + * touches those. Note our list command is `:graves`, because `ui(core)` + * already claims `:list` for the addon list. + */ +import { render, type JSX } from '@bedrock-core/ui'; +import { createStackNavigator, NavigationContainer } from '@bedrock-core/ui/navigation'; +import type { Player } from '@minecraft/server'; +import { GraveList } from './GraveList'; +import { GraveDetail } from './GraveDetail'; +import { AdminPanel } from './AdminPanel'; + +export type GravesRoutes = { + List: undefined; + Admin: undefined; + Detail: { graveId: string }; +}; + +const Stack = createStackNavigator({ + screens: { + List: GraveList, + Admin: AdminPanel, + Detail: GraveDetail, + }, + initialRouteName: 'List', +}); + +function GravesApp({ initialRoute }: { initialRoute: 'List' | 'Admin' }): JSX.Element { + return ( + + + + ); +} + +export const openGraveList = (player: Player): void => render(, player); + +export const openAdminPanel = (player: Player): void => render(, player); diff --git a/addons/files/gameplay_changes/graves/packs/BP/scripts/UI/Settings.ts b/addons/files/gameplay_changes/graves/packs/BP/scripts/UI/Settings.ts deleted file mode 100644 index 37aec0ec6..000000000 --- a/addons/files/gameplay_changes/graves/packs/BP/scripts/UI/Settings.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Player } from '@minecraft/server'; -import { ModalFormData, ModalFormResponse } from '@minecraft/server-ui'; -import { getSettings, setSettings } from '../Actions'; -import { GravesSettings } from '../Models'; -import { openConfigInterface } from './Config'; - -export const openSettingsInterface = (player: Player): void => { - const gravesSettings: GravesSettings = getSettings(); - - const form: ModalFormData = new ModalFormData() - .title({ translate: 'bt.graves.settings.title' }) - .toggle({ translate: 'bt.graves.settings.grave_robbing', with: ['\n'] }, { defaultValue: gravesSettings.graveRobbing }) - .toggle({ translate: 'bt.graves.settings.xp_collection', with: ['\n'] }, { defaultValue: gravesSettings.xpCollection }) - .toggle({ translate: 'bt.graves.settings.grave_locating', with: ['\n'] }, { defaultValue: gravesSettings.graveLocating }) - .slider({ translate: 'bt.graves.settings.despawn_time.hours', with: ['\n'] }, 0, 24, { defaultValue: Math.floor(gravesSettings.despawnTime / 3600), valueStep: 1 }) - .slider({ translate: 'bt.graves.settings.despawn_time.minutes', with: ['\n'] }, 0, 59, { defaultValue: Math.floor(gravesSettings.despawnTime % 3600 / 60), valueStep: 1 }) - .slider({ translate: 'bt.graves.settings.despawn_time.seconds', with: ['\n'] }, 0, 59, { defaultValue: gravesSettings.despawnTime % 60, valueStep: 1 }); - - form.show(player).then((response: ModalFormResponse): void => { - if (response.formValues) { - // Toggle = boolean - // Slider = number - // Togggle, Toggle, Toggle, Slider, Slider, Slider - const formValues: [boolean, boolean, boolean, number, number, number] = response.formValues as [boolean, boolean, boolean, number, number, number]; - - gravesSettings.graveRobbing = formValues[0]; - gravesSettings.xpCollection = formValues[1]; - gravesSettings.graveLocating = formValues[2]; - gravesSettings.despawnTime = formValues[3] * 3600 + formValues[4] * 60 + formValues[5]; - } - - if (!response.canceled) { - setSettings(gravesSettings); - - openConfigInterface(player); - } - }); -}; diff --git a/addons/files/gameplay_changes/graves/packs/BP/scripts/UI/SettingsButton.tsx b/addons/files/gameplay_changes/graves/packs/BP/scripts/UI/SettingsButton.tsx new file mode 100644 index 000000000..fe675414a --- /dev/null +++ b/addons/files/gameplay_changes/graves/packs/BP/scripts/UI/SettingsButton.tsx @@ -0,0 +1,37 @@ +/** + * Gear button seated in the header's empty back slot — an absolute overlay + * with the same metrics as the ore back control (header margins 1 + padding, + * iconSize square). Opens this addon's settings through the shared `ui(core)` + * config UI. The main list and the admin panel wear it; detail keeps back. + */ +import { usePlayer, type JSX } from '@bedrock-core/ui'; +import { openUi } from '@bedrock-core/ui/config'; +import { Button, theme } from '@bedrock-core/ui/ore-styled'; +import { core } from '../registration'; + +/** Vanilla dark gear glyph — reads on the light ore header like the §0 title. */ +const GEAR = 'textures/ui/settings_glyph_color_2x'; + +export function SettingsButton(): JSX.Element { + const player = usePlayer(); + const h = theme.components.header; + + return ( +