Add and remove structure with outline
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
import { MolangVariableMap, system, world } from "@minecraft/server";
|
||||
import { Vector } from "../lib/Vector";
|
||||
|
||||
const drawFrequency = 8;
|
||||
const drawParticle = "minecraft:villager_happy";
|
||||
|
||||
export class Outliner {
|
||||
dimension;
|
||||
min = new Vector();
|
||||
max = new Vector();
|
||||
#shouldDraw = true;
|
||||
|
||||
#drawParticles = [];
|
||||
#runner = null;
|
||||
|
||||
constructor(dimension, min, max) {
|
||||
this.dimension = world.getDimension(dimension);
|
||||
this.min = new Vector(min.x, min.y, min.z);
|
||||
this.max = new Vector(max.x, max.y, max.z);
|
||||
}
|
||||
|
||||
startDraw() {
|
||||
this.#shouldDraw = true;
|
||||
this.#runner = system.runInterval(() => this.draw(), drawFrequency);
|
||||
}
|
||||
|
||||
stopDraw() {
|
||||
system.clearRun(this.#runner);
|
||||
this.shouldDraw = false;
|
||||
}
|
||||
|
||||
draw() {
|
||||
if (!this.#shouldDraw) return;
|
||||
this.#drawParticles.length = 0;
|
||||
this.#drawParticles.push(...this.getCubiodParticleLocations());
|
||||
|
||||
for (const [particleType, location] of this.#drawParticles) {
|
||||
try {
|
||||
this.dimension.spawnParticle(particleType, location);
|
||||
} catch {
|
||||
/* pass */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getCubiodParticleLocations() {
|
||||
const vertices = [
|
||||
new Vector(this.min.x, this.min.y, this.min.z),
|
||||
new Vector(this.max.x, this.min.y, this.min.z),
|
||||
new Vector(this.min.x, this.max.y, this.min.z),
|
||||
new Vector(this.max.x, this.max.y, this.min.z),
|
||||
new Vector(this.min.x, this.min.y, this.max.z),
|
||||
new Vector(this.max.x, this.min.y, this.max.z),
|
||||
new Vector(this.min.x, this.max.y, this.max.z),
|
||||
new Vector(this.max.x, this.max.y, this.max.z)
|
||||
];
|
||||
const edges = [
|
||||
[0, 1],
|
||||
[0, 2],
|
||||
[0, 4],
|
||||
[1, 3],
|
||||
[1, 5],
|
||||
[2, 3],
|
||||
[2, 6],
|
||||
[3, 7],
|
||||
[4, 5],
|
||||
[4, 6],
|
||||
[5, 7],
|
||||
[6, 7]
|
||||
];
|
||||
const edgePoints = [];
|
||||
for (const edge of edges) {
|
||||
const [startVertex, endVertex] = [vertices[edge[0]], vertices[edge[1]]];
|
||||
const resolution = Math.min(Math.floor(endVertex.subtract(startVertex).length), 16);
|
||||
for (let i = 1; i < resolution; i++) {
|
||||
const t = i / resolution;
|
||||
edgePoints.push(startVertex.lerp(endVertex, t));
|
||||
}
|
||||
}
|
||||
return vertices.concat(edgePoints).map((v) => [drawParticle, v]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { MinecraftDimensionTypes, world } from "@minecraft/server";
|
||||
import { Outliner } from "./Outliner";
|
||||
|
||||
export class Structure {
|
||||
#structure;
|
||||
#options = {
|
||||
isPlaced: false,
|
||||
dimensionId: MinecraftDimensionTypes.overworld,
|
||||
location: { x: 0, y: 0, z: 0 },
|
||||
rotation: 0,
|
||||
mirror: false
|
||||
};
|
||||
|
||||
constructor(structureName) {
|
||||
this.name = structureName;
|
||||
this.#structure = world.structureManager.get(structureName);
|
||||
this.#options = this.loadOptions();
|
||||
}
|
||||
|
||||
loadOptions() {
|
||||
try {
|
||||
return JSON.parse(world.getDynamicProperty(`structOptions:${this.name}`));
|
||||
} catch (e) {
|
||||
world.setDynamicProperty(`structOptions:${this.name}`, JSON.stringify(this.#options));
|
||||
}
|
||||
return this.#options;
|
||||
}
|
||||
|
||||
updateOptions() {
|
||||
world.setDynamicProperty(`structOptions:${this.name}`, JSON.stringify(this.#options));
|
||||
}
|
||||
|
||||
getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
getStructure() {
|
||||
return this.#structure;
|
||||
}
|
||||
|
||||
getOutlineLimits() {
|
||||
if (!this.#options.isPlaced)
|
||||
throw new Error(`[StrucTool] Structure '${this.name}' is not placed.`);
|
||||
return {
|
||||
min: {
|
||||
x: this.#options.location.x,
|
||||
y: this.#options.location.y,
|
||||
z: this.#options.location.z
|
||||
},
|
||||
max: {
|
||||
x: this.#options.location.x + this.#structure.size.x,
|
||||
y: this.#options.location.y + this.#structure.size.y,
|
||||
z: this.#options.location.z + this.#structure.size.z
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
place(dimensionId, location) {
|
||||
this.#options = {
|
||||
isPlaced: true,
|
||||
dimensionId,
|
||||
location: { x: Math.floor(location.x), y: Math.floor(location.y), z: Math.floor(location.z) },
|
||||
};
|
||||
this.updateOptions();
|
||||
this.outliner = new Outliner(dimensionId, this.getOutlineLimits().min, this.getOutlineLimits().max);
|
||||
this.outliner.startDraw();
|
||||
}
|
||||
|
||||
remove() {
|
||||
if (!this.#options.isPlaced)
|
||||
throw new Error(`[StrucTool] Structure '${this.name}' is not placed.`);
|
||||
this.#options.isPlaced = false;
|
||||
this.updateOptions();
|
||||
this.outliner.stopDraw();
|
||||
delete this.outliner;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { world } from '@minecraft/server';
|
||||
import { Structure } from './Structure';
|
||||
|
||||
class StructureCollection {
|
||||
#structures;
|
||||
|
||||
constructor() {
|
||||
this.#structures = {};
|
||||
}
|
||||
|
||||
add(name) {
|
||||
const struct = world.structureManager.get(name);
|
||||
this.#structures[name] = new Structure(name, struct);
|
||||
}
|
||||
|
||||
get(name) {
|
||||
const structure = this.#structures[name];
|
||||
if (!structure) {
|
||||
throw new Error(`Structure ${name} not found.`);
|
||||
}
|
||||
return structure;
|
||||
}
|
||||
|
||||
remove(name) {
|
||||
const struct = this.get(name);
|
||||
struct.remove();
|
||||
delete this.#structures[name];
|
||||
}
|
||||
|
||||
place(name, dimensionId, location) {
|
||||
const struct = this.get(name);
|
||||
if (!struct) {
|
||||
throw new Error(`Structure ${name} not found.`);
|
||||
}
|
||||
struct.place(dimensionId, location);
|
||||
}
|
||||
}
|
||||
|
||||
export const structureCollection = new StructureCollection();
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Command } from '../lib/canopy/CanopyExtension';
|
||||
import { extension } from '../config';
|
||||
import { structureCollection } from '../classes/StructureCollection';
|
||||
|
||||
const structCmd = new Command({
|
||||
name: 'struct',
|
||||
description: { text: 'Manages current StrucTool structures.' },
|
||||
usage: 'struct',
|
||||
callback: structCommand,
|
||||
args: [
|
||||
{ type: 'string', name: 'option' },
|
||||
{ type: 'string', name: 'name' }
|
||||
]
|
||||
});
|
||||
extension.addCommand(structCmd);
|
||||
|
||||
function structCommand(sender, args) {
|
||||
const { option, name, structure } = args;
|
||||
switch (option) {
|
||||
case 'add':
|
||||
addStructure(sender, name, structure);
|
||||
break;
|
||||
case 'remove':
|
||||
removeStructure(sender, name);
|
||||
break;
|
||||
case 'place':
|
||||
structureCollection.place(name, sender.dimension.id, sender.location);
|
||||
break;
|
||||
default:
|
||||
structCmd.sendUsage(sender);
|
||||
}
|
||||
}
|
||||
|
||||
function addStructure(sender, name, filename) {
|
||||
structureCollection.add(name, filename);
|
||||
sender.sendMessage({ text: `Added structure '${name}'` });
|
||||
}
|
||||
|
||||
function removeStructure(sender, name) {
|
||||
structureCollection.remove(name);
|
||||
sender.sendMessage({ text: `Removed structure '${name}'` });
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { CanopyExtension } from './lib/canopy/CanopyExtension';
|
||||
|
||||
export const extension = new CanopyExtension({
|
||||
author: 'ForestOfLight',
|
||||
name: 'StrucTool',
|
||||
description: 'Survival building extension for §l§aCanopy§r!',
|
||||
version: '1.0.0'
|
||||
});
|
||||
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* Part of ItemStack Database by @gameza_src
|
||||
* Unknown author
|
||||
*/
|
||||
const isVec3Symbol = Symbol("isVec3");
|
||||
export function Vector(x = 0, y = 0, z = 0) {
|
||||
if (new.target) {
|
||||
this.x = Number(x);
|
||||
this.y = Number(y);
|
||||
this.z = Number(z);
|
||||
} else {return { x: Number(x), y: Number(y), z: Number(z), __proto__: Vector.prototype };}
|
||||
}
|
||||
Vector.magnitude = function magnitude(vec) { return Math.sqrt(vec.x * vec.x + vec.y * vec.y + vec.z * vec.z); }
|
||||
Vector.normalize = function normalize(vec) { const l = Vector.magnitude(vec); return { x: vec.x / l, y: vec.y / l, z: vec.z / l, __proto__: Vector.prototype }; }
|
||||
Vector.cross = function crossProduct(a, b) { return { x: a.y * b.z - a.z * b.y, y: a.x * b.z - a.z * b.x, z: a.x * b.y - a.y * b.x, __proto__: Vector.prototype }; }
|
||||
Vector.dot = function dot(a, b) { return a.x * b.x + a.y * b.y + a.z * b.z; }
|
||||
Vector.angleBetween = function angleBetween(a, b) { return Math.acos(Vector.dot(a, b) / (Vector.magnitude(a) * Vector.magnitude(b))); }
|
||||
Vector.subtract = function subtract(a, b) { return { x: a.x - b.x, y: a.y - b.y, z: a.z - b.z, __proto__: Vector.prototype } };
|
||||
Vector.add = function add(a, b) { return { x: a.x + b.x, y: a.y + b.y, z: a.z + b.z, __proto__: Vector.prototype } };
|
||||
Vector.multiply = function multiply(vec, num) {
|
||||
if (typeof num == "number") return { x: vec.x * num, y: vec.y * num, z: vec.z * num, __proto__: Vector.prototype };
|
||||
return { x: vec.x * num.x, y: vec.y * num.y, z: vec.z * num.z, __proto__: Vector.prototype };
|
||||
}
|
||||
Vector.isVec3 = function isVec3(vec) { return vec[isVec3Symbol] === true; }
|
||||
Vector.floor = function floor(vec) { return { x: Math.floor(vec.x), y: Math.floor(vec.y), z: Math.floor(vec.z), __proto__: Vector.prototype }; }
|
||||
Vector.projection = function projection(a, b) { return Vector.multiply(b, Vector.dot(a, b) / ((b.x * b.x + b.y * b.y + b.z * b.z) ** 2)); }
|
||||
Vector.rejection = function rejection(a, b) { return Vector.subtract(a, Vector.projection(a, b)); }
|
||||
Vector.reflect = function reflect(v, n) { return Vector.subtract(v, Vector.multiply(n, 2 * Vector.dot(v, n))); }
|
||||
Vector.lerp = function lerp(a, b, t) { return Vector.multiply(a, 1 - t).add(Vector.multiply(b, t)); }
|
||||
Vector.distance = function distance(a, b) { return Vector.magnitude(Vector.subtract(a, b)); }
|
||||
Vector.from = function from(object) {
|
||||
if (Vector.isVec3(object)) return object;
|
||||
if (Array.isArray(object)) return new Vector(object[0], object[1], object[2]);
|
||||
const { x = 0, y = 0, z = 0 } = object ?? {};
|
||||
return { x: Number(x), y: Number(y), z: Number(z), __proto__: Vector.prototype };
|
||||
}
|
||||
Vector.sort = function sort(vec1, vec2) {
|
||||
const [x1, x2] = vec1.x < vec2.x ? [vec1.x, vec2.x] : [vec2.x, vec1.x];
|
||||
const [y1, y2] = vec1.y < vec2.y ? [vec1.y, vec2.y] : [vec2.y, vec1.y];
|
||||
const [z1, z2] = vec1.z < vec2.z ? [vec1.z, vec2.z] : [vec2.z, vec1.z];
|
||||
return [{ x: x1, y: y1, z: z1, __proto__: Vector.prototype }, { x: x2, y: y2, z: z2, __proto__: Vector.prototype }];
|
||||
}
|
||||
Vector.up = { x: 0, y: 1, z: 0, __proto__: Vector.prototype };
|
||||
Vector.down = { x: 0, y: -1, z: 0, __proto__: Vector.prototype };
|
||||
Vector.right = { x: 1, y: 0, z: 0, __proto__: Vector.prototype };
|
||||
Vector.left = { x: -1, y: 0, z: 0, __proto__: Vector.prototype };
|
||||
Vector.forward = { x: 0, y: 0, z: 1, __proto__: Vector.prototype };
|
||||
Vector.backward = { x: 0, y: 0, z: -1, __proto__: Vector.prototype };
|
||||
Vector.zero = { x: 0, y: 0, z: 0, __proto__: Vector.prototype };
|
||||
Vector.prototype = {
|
||||
distance(vec) { return Vector.distance(this, vec); },
|
||||
lerp(vec, t) { return Vector.lerp(this, vec, t); },
|
||||
projection(vec) { return Vector.projection(this, vec); },
|
||||
reflect(vec) { return Vector.reflect(this, vec); },
|
||||
rejection(vec) { return Vector.rejection(this, vec); },
|
||||
cross(vec) { return Vector.cross(this, vec); },
|
||||
dot(vec) { return Vector.dot(this, vec); },
|
||||
floor() { return Vector.floor(this); },
|
||||
add(vec) { return Vector.add(this, vec); },
|
||||
subtract(vec) { return Vector.subtract(this, vec); },
|
||||
multiply(num) { return Vector.multiply(this, num); },
|
||||
get length() { return Vector.magnitude(this); },
|
||||
get normalized() { return Vector.normalize(this); },
|
||||
x: 0,
|
||||
y: 0,
|
||||
z: 0,
|
||||
[isVec3Symbol]: true,
|
||||
toString() { return `<${this.x}, ${this.y}, ${this.z}>`; }
|
||||
}
|
||||
+3
-96
@@ -1,97 +1,4 @@
|
||||
import { CanopyExtension, Command, Rule } from './lib/canopy/CanopyExtension';
|
||||
import { world } from '@minecraft/server';
|
||||
// Rules
|
||||
|
||||
/**
|
||||
* Create a new CanopyExtension instance to define your extension.
|
||||
*/
|
||||
const extension = new CanopyExtension({
|
||||
author: 'YourName',
|
||||
name: 'ExampleExtension',
|
||||
description: 'Example extension for §l§aCanopy§r!',
|
||||
version: '1.0.0'
|
||||
});
|
||||
|
||||
// --------------------------------------------
|
||||
|
||||
/**
|
||||
* Adding a new Rule:
|
||||
*/
|
||||
const exampleRule = new Rule({
|
||||
identifier: 'exampleRule', // The name of the rule
|
||||
description: { text: 'An example rule that prints a message in chat when you hit a button.' }, // Shows up in the help command. Must be a RawMessage type (translatable!).
|
||||
// Optional:
|
||||
contingentRules: [], // Rules that will be enabled when this rule is enabled
|
||||
independentRules: [], // Rules that will be disabled when this rule is enabled
|
||||
onEnableCallback: () => world.afterEvents.buttonPush.subscribe(onButtonPush), // Function to run when the rule is enabled (also runs when the extension is loaded, if the rule is already enabled)
|
||||
onDisableCallback: () => world.afterEvents.buttonPush.unsubscribe(onButtonPush) // Function to run when the rule is disabled
|
||||
});
|
||||
extension.addRule(exampleRule);
|
||||
|
||||
// use the rule to control your code flow
|
||||
function onButtonPush(event) {
|
||||
if (!exampleRule.getValue())
|
||||
return;
|
||||
if (event.source === undefined) // Always check for undefined entities and players. Simulated players always show up as undefined in events.
|
||||
return;
|
||||
event.source.sendMessage('§aYou pushed a button!');
|
||||
}
|
||||
|
||||
// --------------------------------------------
|
||||
|
||||
/**
|
||||
* Making a second new rule which we can use to enable the example command:
|
||||
* (This is not required to make a new command, but it is helpful to allow admins to disable your commands.)
|
||||
*/
|
||||
const commandExampleRule = new Rule({
|
||||
identifier: 'commandExample',
|
||||
description: { text: 'Enables the example command.' } // Shows up in the help command. RawMessage type.
|
||||
});
|
||||
extension.addRule(commandExampleRule);
|
||||
|
||||
/**
|
||||
* Adding a new Command:
|
||||
*/
|
||||
const exampleCommand = new Command({
|
||||
name: 'example', // The name of the command
|
||||
description: { text: 'An example command that prints your message in chat.' }, // Shows up in the help command. RawMessage type.
|
||||
usage: 'example [message]', // The usage of the command that shows up in the help command & when used incorrectly
|
||||
callback: exampleCommandCallback, // The function to run when the command is executed
|
||||
// Optional:
|
||||
args: [
|
||||
{ type: 'string|number', name: 'message' } // The arguments that the command takes. 'string|number' means it can be either a string or a number
|
||||
],
|
||||
contingentRules: ['commandExample'], // Rules that must be true for the command to be enabled
|
||||
adminOnly: false, // Whether the command can only be run by admins (users with the 'CanopyAdmin' tag)
|
||||
helpEntries: [ // Additional help entries that show up in the help command
|
||||
{ usage: `example`, description: { text: `Run the example command with the default message.` } } // Description is a RawMessage type.
|
||||
],
|
||||
helpHidden: false // Whether the command should be hidden from the help command.
|
||||
});
|
||||
extension.addCommand(exampleCommand);
|
||||
|
||||
/**
|
||||
* Adding a command alias:
|
||||
* This is essentially just adding a new command that runs the same function as the original command and hiding its help.
|
||||
*/
|
||||
const exampleCommandAlias = new Command({
|
||||
name: 'ex',
|
||||
description: { text: 'An alias for the example command.' },
|
||||
usage: 'ex [message]',
|
||||
callback: exampleCommandCallback,
|
||||
args: [
|
||||
{ type: 'string|number', name: 'message' }
|
||||
],
|
||||
contingentRules: ['commandExample'],
|
||||
helpHidden: true
|
||||
});
|
||||
extension.addCommand(exampleCommandAlias);
|
||||
|
||||
// now you can define the function that will be called when the command is executed
|
||||
function exampleCommandCallback(sender, args) {
|
||||
let { message } = args;
|
||||
if (message === null)
|
||||
message = 'Hello, world!';
|
||||
if (!isNaN(parseFloat(message)) && isFinite(message))
|
||||
message = message.toString();
|
||||
sender.sendMessage(`§aYou ran the example command with the message: §7${message}`);
|
||||
}
|
||||
// Commands
|
||||
import './commands/struct.js';
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { FormCancelationReason } from '@minecraft/server-ui';
|
||||
|
||||
export async function forceShow(player, form, timeout = Infinity) {
|
||||
const startTick = system.currentTick;
|
||||
while ((system.currentTick - startTick) < timeout) {
|
||||
const response = await form.show(player);
|
||||
if (startTick + 1 === system.currentTick && response.cancelationReason === FormCancelationReason.UserBusy)
|
||||
player.sendMessage({ translate: 'commands.canopy.menu.busy' });
|
||||
if (response.cancelationReason !== FormCancelationReason.UserBusy)
|
||||
return response;
|
||||
}
|
||||
throw new Error({ translate: 'commands.canopy.menu.timeout', with: [String(timeout)] });
|
||||
};
|
||||
Reference in New Issue
Block a user