Add and remove structure with outline

This commit is contained in:
ForestOfLight
2025-03-10 23:16:42 -07:00
Unverified
parent f52f82b867
commit 67e3f6fecc
12 changed files with 369 additions and 153 deletions
+82
View File
@@ -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]);
}
}
+77
View File
@@ -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;
}
}
+39
View File
@@ -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();
+42
View File
@@ -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}'` });
}
+8
View File
@@ -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'
});
+69
View File
@@ -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
View File
@@ -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';
+13
View File
@@ -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)] });
};