UNPKG

@minecraft/creator-tools

Version:

Minecraft Creator Tools command line and libraries.

570 lines (478 loc) 17 kB
// Copyright (c) Mojang AB. All rights reserved. import { BoolPropertyItemVariant, CursorTargetMode, IDisposable, ImageResourceType, IObservable, IRootPropertyPane, makeObservable, NumberPropertyItemVariant, Ray, } from "@minecraft/server-editor"; import { IPlayerUISession, registerEditorExtension, IModalTool, ActionTypes, MouseProps, MouseActionType, MouseInputType, KeyboardKey, InputModifier, EditorInputContext, IPropertyPane, } from "@minecraft/server-editor"; import { Vector3 } from "@minecraft/server"; import { MinecraftBlockTypes } from "@minecraft/vanilla-data"; enum PortalType { Nether = 0, End = 1, } enum PortalOrientation { X = 0, Z = 1, } type ExtensionStorage = { tool?: IModalTool; }; type PortalGeneratorSession = IPlayerUISession<ExtensionStorage>; type PaneSettingsType = { portalType: IObservable<number>; shouldReplaceFloor: IObservable<boolean>; }; interface IPortalGenerator { set parentPane(value: IPropertyPane); subPane(uiSession: PortalGeneratorSession): IPropertyPane | undefined; activatePane(uiSession: PortalGeneratorSession): void; deactivatePane(): void; generatePortal(uiSession: PortalGeneratorSession): void; } class PortalGenerator implements IDisposable { private _netherPortal: NetherPortal; private _endPortal: EndPortal; private _activePortal?: IPortalGenerator; private _pane?: IRootPropertyPane; private _settings: PaneSettingsType = { portalType: makeObservable<number>(PortalType.Nether), shouldReplaceFloor: makeObservable(true), }; constructor() { this._netherPortal = new NetherPortal(); this._endPortal = new EndPortal(); } public toolPane(uiSession: PortalGeneratorSession): IRootPropertyPane | undefined { if (!this._pane) { uiSession.log.error("Tool pane not initialized"); return undefined; } return this._pane; } initialize(uiSession: PortalGeneratorSession, storage: ExtensionStorage) { // Create Action const toolToggleAction = uiSession.actionManager.createAction({ actionType: ActionTypes.NoArgsAction, onExecute: () => { uiSession.toolRail.setSelectedToolId(tool.id); }, }); // Add the extension to the tool rail and give it an icon const tool = uiSession.toolRail.addTool("editorSample:portalTool", { title: "sample.portalgenerator.title", icon: "pack://textures/portal-generator.png", tooltip: "sample.portalgenerator.tooltip", action: toolToggleAction, }); // Register a global shortcut (CTRL + SHIFT + P) to select the tool uiSession.inputManager.registerKeyBinding( EditorInputContext.GlobalToolMode, toolToggleAction, { key: KeyboardKey.KEY_P, modifier: InputModifier.Control | InputModifier.Shift }, { uniqueId: "editorSamples:portalGenerator:toggleTool", label: "sample.portalgenerator.keyBinding.toggleTool", } ); // Create an action that will be executed on left mouse click const executeMouseAction = uiSession.actionManager.createAction({ actionType: ActionTypes.MouseRayCastAction, onExecute: (_mouseRay: Ray, mouseProps: MouseProps) => { if ( mouseProps.mouseAction === MouseActionType.LeftButton && mouseProps.inputType === MouseInputType.ButtonDown && this._activePortal ) { this._activePortal.generatePortal(uiSession); } }, }); tool.registerMouseButtonBinding(executeMouseAction); storage.tool = tool; // Build the UI components (and the sub pane with the options) this.buildPane(uiSession); if (this._pane) { tool.bindPropertyPane(this._pane); this.activatePortalGenerator(uiSession, this._settings.portalType.value); } } teardown(): void {} buildPane(uiSession: PortalGeneratorSession) { const pane = uiSession.createPropertyPane({ title: "sample.portalgenerator.pane.title", }); pane.addBool(this._settings.shouldReplaceFloor, { title: "sample.portalgenerator.pane.replacefloor", onChange: (current: boolean) => { const targetMode = current ? CursorTargetMode.Block : CursorTargetMode.Face; uiSession.extensionContext.cursor.setProperties({ targetMode }); }, variant: BoolPropertyItemVariant.ToggleSwitch, }); pane.addDropdown(this._settings.portalType, { title: "sample.portalgenerator.pane.portaltype", entries: [ { label: "sample.portalgenerator.pane.portaltype.nether", value: PortalType.Nether, imageData: { path: "portal", type: ImageResourceType.Block }, }, { label: "sample.portalgenerator.pane.portaltype.end", value: PortalType.End, imageData: { path: "enderEyeIcon", type: ImageResourceType.Icon }, }, ], onChange: (newValue: number) => { this.activatePortalGenerator(uiSession, newValue); }, }); this._pane = pane; this._endPortal.parentPane = pane; this._netherPortal.parentPane = pane; } activatePortalGenerator(uiSession: PortalGeneratorSession, portalType: PortalType): void { this._pane?.hide(); if (this._activePortal) { this._activePortal.deactivatePane(); } if (portalType === PortalType.Nether) { this._activePortal = this._netherPortal; } else { this._activePortal = this._endPortal; } this._activePortal.activatePane(uiSession); this._pane?.show(); } } class NetherPortal implements IPortalGenerator { private _pane?: IPropertyPane; private _parentPane?: IPropertyPane; // Settings private _sizeX: IObservable<number> = makeObservable(4); private _sizeY: IObservable<number> = makeObservable(5); private _percentComplete: IObservable<number> = makeObservable(100); private _orientation: IObservable<number> = makeObservable(PortalOrientation.X); private _hasCorners: IObservable<boolean> = makeObservable(true); constructor() {} public subPane(uiSession: PortalGeneratorSession): IPropertyPane | undefined { if (!this._pane) { uiSession.log.error("Sub pane not initialized"); return undefined; } return this._pane; } public set parentPane(value: IPropertyPane) { this._parentPane = value; } activatePane(uiSession: PortalGeneratorSession): void { if (this._pane) { this.deactivatePane(); } this._pane = this.buildSubPane(uiSession); this._pane?.show(); } deactivatePane(): void { if (this._pane) { this._pane.hide(); this._parentPane?.removeSubPane(this._pane); } this._pane = undefined; } buildSubPane(uiSession: PortalGeneratorSession): IPropertyPane | undefined { const windowPane = this._parentPane; if (!windowPane) { uiSession.log.error("Failed to find window binding"); return undefined; } const subPane = windowPane.createSubPane({ title: "sample.portalgenerator.pane.nether.pane.title", }); subPane.addDropdown(this._orientation, { title: "sample.portalgenerator.pane.nether.pane.orientation", entries: [ { label: "sample.portalgenerator.pane.nether.pane.orientation.x", value: PortalOrientation.X, }, { label: "sample.portalgenerator.pane.nether.pane.orientation.y", value: PortalOrientation.Z, }, ], }); subPane.addNumber(this._sizeX, { title: "sample.portalgenerator.pane.nether.pane.width", min: 4, max: 23, isInteger: true, }); subPane.addNumber(this._sizeY, { title: "sample.portalgenerator.pane.nether.pane.height", min: 5, max: 23, isInteger: true, }); subPane.addBool(this._hasCorners, { title: "sample.portalgenerator.pane.nether.pane.corners", tooltip: "sample.portalgenerator.pane.nether.pane.corners.tooltip", }); subPane.addNumber(this._percentComplete, { title: "sample.portalgenerator.pane.nether.pane.percentage", min: 0, max: 100, variant: NumberPropertyItemVariant.InputFieldAndSlider, }); return subPane; } generatePortal(uiSession: PortalGeneratorSession): void { const me = uiSession.extensionContext.player; const location = uiSession.extensionContext.cursor.getPosition(); const targetBlock = me.dimension.getBlock(location); if (targetBlock === undefined) { uiSession.log.warning("No block selected"); return; } if (this._percentComplete.value === 0) { return; } if (me.dimension.id.endsWith("the_end")) { uiSession.log.warning("You cannot create a nether portal in the end"); return; } uiSession.extensionContext.transactionManager.openTransaction("Transaction group portal generator"); let from: Vector3 = location; let to: Vector3 = { x: 0, y: 0, z: 0 }; if (this._orientation.value === PortalOrientation.X) { to = { x: location.x + this._sizeX.value, y: location.y + this._sizeY.value, z: location.z, }; } else if (this._orientation.value === PortalOrientation.Z) { to = { x: location.x, y: location.y + this._sizeY.value, z: location.z + this._sizeX.value, }; } else { uiSession.log.error("Failed to get valid orientation"); uiSession.extensionContext.transactionManager.discardOpenTransaction(); return; } const yEnd = this._sizeY.value - 1; const xEnd = this._sizeX.value - 1; uiSession.extensionContext.transactionManager.trackBlockChangeArea(from, to); for (let y = 0; y < this._sizeY.value; ++y) { for (let x = 0; x < this._sizeX.value; ++x) { let block = MinecraftBlockTypes.Air; // Percent complete is randomized percentage if (this._percentComplete.value !== 100) { const randVal = getRandomInt(100); if (this._percentComplete.value - randVal < 0) { continue; } } // Set as obsidian for bottom, top, and edges of portal if ( !this._hasCorners.value && ((y === 0 && x === 0) || (y === 0 && x === xEnd) || (y === yEnd && x === xEnd) || (y === yEnd && x === 0)) ) { continue; // no corners } else if (y === 0 || y === yEnd || x === 0 || x === xEnd) { block = MinecraftBlockTypes.Obsidian; } else { continue; } const loc: Vector3 = this._orientation.value === PortalOrientation.X ? { x: location.x + x, y: location.y + y, z: location.z } : { x: location.x, y: location.y + y, z: location.z + x }; me.dimension.getBlock(loc)?.setType(block); } } let ori = "x"; if (this._orientation.value === PortalOrientation.Z) { ori = "z"; from = { x: location.x, y: location.y + 1, z: location.z + 1 }; to = { x: location.x, y: location.y + this._sizeY.value - 2, z: location.z + this._sizeX.value - 2, }; } else { from = { x: location.x + 1, y: location.y + 1, z: location.z }; to = { x: location.x + this._sizeX.value - 2, y: location.y + this._sizeY.value - 2, z: location.z, }; } if (this._percentComplete.value === 100) { // We must fill the portals as it must have the axis set while setting the type // or the engine will destroy the block and the scripting API wont allow both in one operation me.dimension.runCommand( `FILL ${from.x} ${from.y} ${from.z} ${to.x} ${to.y} ${to.z} portal ["portal_axis":"${ori}"]` ); } uiSession.extensionContext.transactionManager.commitOpenTransaction(); } } class EndPortal implements IPortalGenerator { private _pane?: IPropertyPane; private _parentPane?: IPropertyPane; // Settings private _filledEyeCount: IObservable<number> = makeObservable(12); constructor() {} public subPane(uiSession: PortalGeneratorSession): IPropertyPane | undefined { if (!this._pane) { uiSession.log.error("Sub pane not initialized"); return undefined; } return this._pane; } public set parentPane(value: IPropertyPane) { this._parentPane = value; } activatePane(uiSession: PortalGeneratorSession): void { if (this._pane) { this.deactivatePane(); } this._pane = this.buildSubPane(uiSession); this._pane?.show(); } deactivatePane(): void { if (this._pane) { this._pane.hide(); this._parentPane?.removeSubPane(this._pane); } this._pane = undefined; } buildSubPane(uiSession: PortalGeneratorSession): IPropertyPane | undefined { const windowPane = this._parentPane; if (!windowPane) { uiSession.log.error("Failed to find window pane"); return undefined; } const subPane = windowPane.createSubPane({ title: "sample.portalgenerator.pane.end.pane.title", }); subPane.addNumber(this._filledEyeCount, { title: "sample.portalgenerator.pane.end.pane.filledcount", min: 0, max: 12, variant: NumberPropertyItemVariant.InputFieldAndSlider, isInteger: true, }); return subPane; } generatePortal(uiSession: PortalGeneratorSession): void { const me = uiSession.extensionContext.player; const location = uiSession.extensionContext.cursor.getPosition(); const targetBlock = me.dimension.getBlock(location); if (targetBlock === undefined) { uiSession.log.error("No block selected"); return; } uiSession.extensionContext.transactionManager.openTransaction("Transaction group portal generator"); const from: Vector3 = { x: location.x, y: location.y, z: location.z }; const to: Vector3 = { x: location.x + 4, y: location.y, z: location.z + 4 }; let eyesToUse: boolean[] = [false, false, false, false, false, false, false, false, false, false, false, false]; if (this._filledEyeCount.value === 12) { eyesToUse = [true, true, true, true, true, true, true, true, true, true, true, true]; } else if (this._filledEyeCount.value !== 0) { const possibleEyeLocs = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]; for (let i = 0; i < this._filledEyeCount.value; ++i) { const rand = Math.floor(Math.random() * possibleEyeLocs.length); eyesToUse[possibleEyeLocs[rand]] = true; possibleEyeLocs.splice(rand, 1); } } let i = 0; uiSession.extensionContext.transactionManager.trackBlockChangeArea(from, to); for (let z = 0; z < 5; ++z) { for (let x = 0; x < 5; ++x) { let rot = 0; let blockType = MinecraftBlockTypes.Air; if (x === 0 && z !== 0 && z !== 4) { // west edge blockType = MinecraftBlockTypes.EndPortalFrame; rot = 3; } else if (x === 4 && z !== 0 && z !== 4) { // east edge blockType = MinecraftBlockTypes.EndPortalFrame; rot = 1; } else if (z === 0 && x !== 0 && x !== 4) { // south edge blockType = MinecraftBlockTypes.EndPortalFrame; rot = 0; } else if (z === 4 && x !== 0 && x !== 4) { // north edge blockType = MinecraftBlockTypes.EndPortalFrame; rot = 2; } else if (this._filledEyeCount.value === 12 && x >= 1 && z >= 1 && x <= 3 && z <= 3) { // center blockType = MinecraftBlockTypes.EndPortal; } else { continue; } const block = me.dimension.getBlock({ x: location.x + x, y: location.y, z: location.z + z }); if (block) { block.setType(blockType); if (blockType === MinecraftBlockTypes.EndPortalFrame) { const perm = block.permutation.withState("direction", rot).withState("end_portal_eye_bit", eyesToUse[i]); block.setPermutation(perm); i += 1; } } else { uiSession.log.error("Failed to get block"); } } } uiSession.extensionContext.transactionManager.commitOpenTransaction(); } } function getRandomInt(upper: number) { return Math.ceil(Math.random() * (upper + 1)); } /** * Register Portal Generator extension */ export function registerPortalGeneratorExtension() { registerEditorExtension<ExtensionStorage>( "portal-generator-sample", (uiSession) => { uiSession.log.debug(`Initializing [${uiSession.extensionContext.extensionInfo.name}] extension`); uiSession.scratchStorage = {}; const generator = new PortalGenerator(); generator.initialize(uiSession, uiSession.scratchStorage); return [generator]; }, (uiSession) => { uiSession.log.debug(`Shutting down [${uiSession.extensionContext.extensionInfo.name}] extension`); }, { description: '"Portal Generator" Sample Extension', notes: "by Andrew", } ); }