@minecraft/creator-tools
Version:
Minecraft Creator Tools command line and libraries.
1,636 lines (1,347 loc) • 52.7 kB
text/typescript
import Team from "./Team.js";
import {
world,
system,
PlayerSpawnAfterEvent,
PlayerLeaveAfterEvent,
LeverActionAfterEvent,
TitleDisplayOptions,
Player,
DisplaySlotId,
BlockPermutation,
ScriptEventCommandMessageAfterEvent,
ItemStack,
Block,
TicksPerSecond,
BlockComponentTypes,
BlockInventoryComponent,
EntityInventoryComponent,
ItemStopUseOnAfterEvent,
} from "@minecraft/server";
import ChallengePlayer, { ChallengePlayerRole, IPlayerData } from "./ChallengePlayer.js";
import Log from "./Log.js";
import {
AIRSPACE_GAP,
JOIN_TEAM_X,
JOIN_TEAM_Y,
JOIN_TEAM_Z,
MAX_SLOTS as MAX_SLOTS,
PAD_SIZE_X,
PAD_SIZE_Y,
PAD_SIZE_Z,
POST_INIT_TICK,
BLOCK_SCORESHEET as BLOCK_SCORESHEET,
TEAM_INIT_TICK,
TEAM_SIZE_X,
TEAM_SIZE_Z,
TOTAL_X,
TOTAL_Y,
TOTAL_Z,
ITEM_SCORESHEET,
TEAM_OPTIONS_X,
TEAM_OPTIONS_Y,
TEAM_OPTIONS_Z,
PAD_SURROUND_X,
PAD_SURROUND_Z,
PLAYER_DATA_STORAGE_SIZE,
STANDARD_TRACK_TIME,
MAX_PLAYERS_TEAM,
MAX_TIME_TO_SWITCH_TEAMS_TICK,
} from "./Constants.js";
import Utilities from "./Utilities.js";
import Track from "./Track.js";
import { MinecraftBlockTypes, MinecraftDimensionTypes } from "@minecraft/vanilla-data";
export enum ChallengePhase {
setup = 1,
pre = 2,
build = 3,
vote = 4,
post = 5,
}
export enum ChallengeBoardSize {
small = 4,
medium = 8,
large = 16,
xtralarge = 32,
}
export enum ChallengeFlavor {
regular = 0,
goodVibes = 1,
}
export default class Challenge {
nwbLocation = { x: 0, y: 0, z: 0 }; // nwb = north west bottom
teams: Team[] = [];
tracks: Track[] = [];
totalTrackTime = 0;
tickIndex = 0;
challengePlayers: { [name: string]: ChallengePlayer } = {};
#phase: ChallengePhase = ChallengePhase.pre;
#size: ChallengeBoardSize = ChallengeBoardSize.small;
#motdTitle: string = "Build Challenge";
#motdSubtitle: string = "A place to showcase your build skills";
#flavor: ChallengeFlavor = 0;
refreshTeamIter = 0;
clearTeamIter = 0;
activeTeamScore = -1;
get phase() {
return this.#phase;
}
set phase(newPhase: number) {
if (this.#phase !== newPhase) {
let oldPhase = this.#phase;
this.#phase = newPhase;
this.applyPhase();
// update pads to add or remove the vote pedestal
if (oldPhase === ChallengePhase.vote || newPhase === ChallengePhase.vote) {
this.refreshTeamIter = 0;
this.refreshTeam();
}
this.save();
}
}
get flavor() {
return this.#flavor;
}
set flavor(newFlavor: number) {
if (this.#flavor !== newFlavor) {
let oldPhase = this.#flavor;
this.#flavor = newFlavor;
this.applyFlavor();
this.save();
}
}
get size() {
return this.#size;
}
set size(newSize: ChallengeBoardSize) {
if (newSize > this.#size) {
this.save();
this.sendToAllPlayers("Extending team size: " + newSize);
this.clearTeamIter = this.#size;
system.run(this.clearTeamArea);
this.#size = newSize;
this.initTeams();
this.loadTeams();
this.save();
}
}
get motdTitle() {
return this.#motdTitle;
}
set motdTitle(newMotdTitle: string) {
if (newMotdTitle.length < 1 || newMotdTitle.length > 40 || newMotdTitle == this.#motdTitle) {
return;
}
this.#motdTitle = newMotdTitle;
this.save();
}
get motdSubtitle() {
return this.#motdSubtitle;
}
set motdSubtitle(newMotdSubtitle: string) {
if (newMotdSubtitle.length < 1 || newMotdSubtitle.length > 40 || newMotdSubtitle == this.#motdSubtitle) {
return;
}
this.#motdSubtitle = newMotdSubtitle;
this.save();
}
constructor() {
this.tick = this.tick.bind(this);
this.playerSpawned = this.playerSpawned.bind(this);
this.playerLeft = this.playerLeft.bind(this);
this.leverAction = this.leverAction.bind(this);
this.itemUseOn = this.itemUseOn.bind(this);
this.handleScriptEvent = this.handleScriptEvent.bind(this);
this.refreshTeam = this.refreshTeam.bind(this);
this.clearTeamArea = this.clearTeamArea.bind(this);
this.postInit = this.postInit.bind(this);
this.continueInit = this.continueInit.bind(this);
this.continueInit2 = this.continueInit2.bind(this);
this.applyPhase = this.applyPhase.bind(this);
this.applyFlavor = this.applyFlavor.bind(this);
this.updateMetaBonuses = this.updateMetaBonuses.bind(this);
this.refreshTeamScores = this.refreshTeamScores.bind(this);
this.clearPads = this.clearPads.bind(this);
}
save() {
world.setDynamicProperty("challenge:phase", this.#phase);
world.setDynamicProperty("challenge:size", this.#size);
world.setDynamicProperty("challenge:motdTitle", this.#motdTitle);
world.setDynamicProperty("challenge:motdSubtitle", this.#motdSubtitle);
world.setDynamicProperty("challenge:nwbX", this.nwbLocation.x);
world.setDynamicProperty("challenge:nwbY", this.nwbLocation.y);
world.setDynamicProperty("challenge:nwbZ", this.nwbLocation.z);
world.setDynamicProperty("challenge:flavor", this.#flavor);
let data = [];
for (let i = 0; i < this.teams.length; i++) {
data.push(this.teams[i].getSaveData());
}
let dataStr = JSON.stringify(data);
world.setDynamicProperty("challenge:teamData", dataStr);
let playerData: { [name: string]: IPlayerData } = {};
let charLength = 10;
for (let challPlayerName in this.challengePlayers) {
let challPlayer = this.challengePlayers[challPlayerName];
challPlayer.save();
let playerDataObj = challPlayer.getSaveData();
// only store a player if they are on a team or have voted.
if (playerDataObj.t >= 0 || playerDataObj.v >= 0 || playerDataObj.x >= 0) {
charLength += challPlayerName.length + 30; // 22 chars of overhead assuming + 2 chars per number
if (charLength < PLAYER_DATA_STORAGE_SIZE) {
playerData[challPlayerName] = playerDataObj;
}
}
}
if (charLength > PLAYER_DATA_STORAGE_SIZE - 50) {
Log.debug("WARNING: Max number of players historically exceeded");
}
const playerDataStr = JSON.stringify(playerData);
world.setDynamicProperty("challenge:playerState", playerDataStr);
}
public getTeamIndexFromSlot(slotIndex: number) {
// convert
// 0 1 2 3 4 5
// 6 7 8 9 10 11
// 12 13 14 15
// 16 17 18 19
// 20 21 22 23 24 25
// 26 27 28 29 30 31
//
// to
//
// 16 17 8 9 18 19
// 20 21 4 5 22 23
// 14 0 2 10
// 15 1 3 11
// 24 25 6 7 26 27
// 28 29 12 13 30 31
if (slotIndex === 13) {
return 0;
} else if (slotIndex === 17) {
return 1;
} else if (slotIndex === 14) {
return 2;
} else if (slotIndex === 18) {
return 3;
} else if (slotIndex === 12) {
return 14;
} else if (slotIndex === 15) {
return 10;
} else if (slotIndex === 16) {
return 15;
} else if (slotIndex === 19) {
return 11;
} else if (slotIndex === 28) {
return 12;
} else if (slotIndex === 29) {
return 13;
} else if (slotIndex >= 8 && slotIndex <= 9) {
// 4 & 5
return slotIndex - 4;
} else if (slotIndex >= 22 && slotIndex <= 23) {
// 6 & 7
return slotIndex - 16;
} else if (slotIndex >= 0 && slotIndex <= 1) {
// 16 & 17
return slotIndex + 16;
} else if (slotIndex >= 2 && slotIndex <= 3) {
// 8 & 9
return slotIndex + 6;
} else if (slotIndex >= 4 && slotIndex <= 7) {
// 18, 19, 20, 21
return slotIndex + 14;
} else if (slotIndex >= 10 && slotIndex <= 11) {
// 22, 23
return slotIndex + 12;
} else if (slotIndex >= 20 && slotIndex <= 21) {
// 24, 25
return slotIndex + 4;
} else if (slotIndex >= 24 && slotIndex <= 27) {
// 26, 27, 28, 29
return slotIndex + 2;
} else {
return slotIndex; // 30, 31
}
}
initTeams() {
let slot = 0;
for (let i = 0; i < TEAM_SIZE_X; i++) {
for (let j = 0; j < TEAM_SIZE_Z; j++) {
// leave a donut hole in the middle.
if (i < TEAM_SIZE_X / 2 - 1 || i > TEAM_SIZE_X / 2 || j < TEAM_SIZE_Z / 2 - 1 || j > TEAM_SIZE_Z / 2) {
let teamIndex = this.getTeamIndexFromSlot(slot);
if (teamIndex < this.#size) {
if (this.teams[teamIndex] === undefined) {
const team = new Team(this, teamIndex, i, j);
this.teams[teamIndex] = team;
}
}
slot++;
}
}
}
}
loadPlayerState() {
let playerStr = world.getDynamicProperty("challenge:playerState") as string;
if (playerStr) {
try {
let playerObjs = JSON.parse(playerStr);
for (let playerName in playerObjs) {
let playerObj = playerObjs[playerName] as IPlayerData;
if (playerObj.t) {
this.ensurePlayerFromData(playerName, playerObj);
}
}
} catch (e) {
Log.debug("Could not parse player data '" + playerStr + "'");
}
}
}
loadTeams() {
let teamDataStr = world.getDynamicProperty("challenge:teamData") as string;
if (teamDataStr) {
try {
let teamDataObj = JSON.parse(teamDataStr);
if (teamDataObj.length) {
for (let i = 0; i < Math.min(teamDataObj.length, this.teams.length); i++) {
this.teams[i].loadFromData(teamDataObj[i]);
}
}
} catch (e) {
Log.debug("Could not parse team data '" + teamDataStr + "'");
}
}
for (let i = 0; i < this.teams.length; i++) {
this.teams[i].init();
}
this.refreshTeamIter = 0;
this.refreshTeam();
}
init() {
if (this.teams.length > 0) {
Log.debug("Challenge initialized twice");
return;
}
let candNwbX = world.getDynamicProperty("challenge:nwbX") as number;
let candNwbY = world.getDynamicProperty("challenge:nwbY") as number;
let candNwbZ = world.getDynamicProperty("challenge:nwbZ") as number;
if (candNwbX !== undefined && candNwbY !== undefined && candNwbZ !== undefined) {
this.nwbLocation = { x: candNwbX, y: candNwbY, z: candNwbZ };
}
let val = world.getDynamicProperty("challenge:phase") as number;
if (val) {
this.#phase = val;
} else {
this.#phase = ChallengePhase.pre;
}
val = world.getDynamicProperty("challenge:flavor") as number;
if (val) {
this.#flavor = val;
} else {
this.#flavor = ChallengeFlavor.regular;
}
let motdTitle = world.getDynamicProperty("challenge:motdTitle") as string;
if (motdTitle) {
this.#motdTitle = motdTitle;
}
let motdSubtitle = world.getDynamicProperty("challenge:motdSubtitle") as string;
if (motdSubtitle) {
this.#motdSubtitle = motdSubtitle;
}
let sizeVal = world.getDynamicProperty("challenge:size") as number;
if (sizeVal) {
this.#size = sizeVal;
} else {
this.#size = ChallengeBoardSize.small;
}
system.run(this.continueInit);
}
continueInit() {
system.run(this.continueInit2);
this.initTeams();
this.loadTeams();
this.setupTracks();
}
continueInit2() {
system.run(this.tick);
system.run(this.applyPhase);
system.run(this.applyFlavor);
world.afterEvents.playerSpawn.subscribe(this.playerSpawned);
world.afterEvents.playerLeave.subscribe(this.playerLeft);
world.afterEvents.leverAction.subscribe(this.leverAction);
world.afterEvents.itemStopUseOn.subscribe(this.itemUseOn);
system.afterEvents.scriptEventReceive.subscribe(this.handleScriptEvent);
this.loadPlayerState();
this.ensureAllPlayers();
}
setupTracks() {
this.tracks = [];
let track = new Track(
{ x: this.nwbLocation.x, y: this.nwbLocation.y + 10, z: this.nwbLocation.z },
{ x: this.nwbLocation.x, y: this.nwbLocation.y + 10, z: this.nwbLocation.z + TOTAL_Z },
{ x: 20, y: -10, z: 0 }
);
this.tracks.push(track);
track = new Track(
{ x: this.nwbLocation.x + TOTAL_X / 2, y: this.nwbLocation.y + 10, z: this.nwbLocation.z - 20 },
{ x: this.nwbLocation.x + TOTAL_X / 2, y: this.nwbLocation.y + 10, z: this.nwbLocation.z + TOTAL_Z },
{ x: 0, y: -10, z: 20 }
);
this.tracks.push(track);
track = new Track(
{ x: this.nwbLocation.x + TOTAL_X, y: this.nwbLocation.y + 10, z: this.nwbLocation.z },
{ x: this.nwbLocation.x + TOTAL_X, y: this.nwbLocation.y + 10, z: this.nwbLocation.z + TOTAL_Z },
{ x: -20, y: -10, z: 10 }
);
this.tracks.push(track);
this.totalTrackTime = this.tracks.length * STANDARD_TRACK_TIME;
}
refreshTeamScores() {
if (world.scoreboard.getObjective("main")) {
world.scoreboard.removeObjective("main");
}
this.addScores();
}
handleScriptEvent(event: ScriptEventCommandMessageAfterEvent) {
if (!event.sourceEntity || event.sourceEntity.typeId !== "minecraft:player") {
return;
}
let id = event.id;
if (id.startsWith("bc:")) {
id = id.substring(3, id.length);
}
let mess = event.message;
if (mess.startsWith('"') && mess.endsWith('"')) {
mess = mess.substring(1, mess.length - 1);
}
this.processMessage("!" + id + " " + mess, event.sourceEntity as Player);
}
processMessage(message: string, player: Player) {
let mess = message.toLowerCase().trim();
let messageSender = this.ensurePlayer(player);
if (!messageSender) {
return;
}
let nextSpace = mess.indexOf(" ");
let content = "";
let contentSep: string[] = [];
if (nextSpace < 0) {
nextSpace = mess.length;
content = "";
} else {
content = mess.substring(nextSpace + 1, mess.length);
contentSep = content.split(" ");
}
let command = mess.substring(1, nextSpace);
if (nextSpace > 0) {
switch (command) {
case "setmotdtitle":
if (!messageSender.isAdmin) {
this.sendMessageToAdminsPlus(
"Cannot run setmotdtitle, " + messageSender.name + " is not an admin.",
player
);
return;
}
if (content.length > 39 || content.length < 2) {
this.sendMessageToAdminsPlus("Message must be less than 40 chars.", player);
return;
}
this.motdTitle = message.substring(message.indexOf(" ") + 1);
this.showMotd(player);
break;
case "setmotdsubtitle":
if (!messageSender.isAdmin) {
this.sendMessageToAdminsPlus(
"Cannot run setmotdsubtitle, " + messageSender.name + " is not an admin.",
player
);
return;
}
if (content.length > 39 || content.length < 2) {
this.sendMessageToAdminsPlus("Message must be less than 40 chars.", player);
return;
}
this.motdSubtitle = message.substring(message.indexOf(" ") + 1);
this.showMotd(player);
break;
case "setstart":
if (this.#phase !== ChallengePhase.setup) {
this.sendMessageToAdminsPlus("Cannot run setstart outside of setup phase.", player);
return;
}
if (!messageSender.isAdmin) {
this.sendMessageToAdminsPlus("Cannot run setstart, " + player.name + " is not an admin.", player);
return;
}
if (contentSep.length === 3) {
try {
let x = parseInt(contentSep[0]);
let y = parseInt(contentSep[1]);
let z = parseInt(contentSep[2]);
this.sendMessageToAdminsPlus("Setting new start location to " + x + " " + y + " " + z, player);
this.setStart(x, y, z);
} catch (e) {}
} else {
const blockRay = player.getBlockFromViewDirection();
if (blockRay && blockRay.block) {
const blockLoc = blockRay.block.location;
this.sendMessageToAdminsPlus(
"Setting new start location to " + blockLoc.x + " " + blockLoc.y + " " + blockLoc.z,
player
);
this.setStart(blockLoc.x, blockLoc.y, blockLoc.z);
}
}
break;
case "debug":
this.save();
this.sendMessageToAdminsPlus(
"State: Ph:" +
this.#phase +
" Size:" +
this.#size +
" " +
" NWB: " +
this.nwbLocation.x +
" " +
this.nwbLocation.y +
" " +
this.nwbLocation.z
);
this.sendMessageToAdminsPlus("Team:" + world.getDynamicProperty("challenge:teamData"));
this.sendMessageToAdminsPlus("Player:" + world.getDynamicProperty("challenge:playerState"));
break;
case "setphase":
if (!messageSender.isAdmin) {
this.sendMessageToAdminsPlus("Cannot run setphase, " + messageSender.name + " is not an admin.", player);
return;
}
if (contentSep.length === 1) {
switch (contentSep[0].toLowerCase()) {
case "pre":
this.phase = ChallengePhase.pre;
break;
case "post":
this.phase = ChallengePhase.post;
break;
case "build":
this.phase = ChallengePhase.build;
break;
case "vote":
this.phase = ChallengePhase.vote;
break;
case "setup":
this.phase = ChallengePhase.setup;
break;
}
}
break;
case "setflavor":
if (!messageSender.isAdmin) {
this.sendMessageToAdminsPlus("Cannot run setflavor, " + messageSender.name + " is not an admin.", player);
return;
}
if (contentSep.length === 1) {
switch (contentSep[0].toLowerCase()) {
case "goodvibes":
case "1":
this.#flavor = ChallengeFlavor.goodVibes;
break;
default:
this.#flavor = ChallengeFlavor.regular;
break;
}
}
if (this.#flavor === ChallengeFlavor.goodVibes) {
this.sendMessageToAdminsPlus("Current game flavor is set to goodvibes.", player);
} else {
this.sendMessageToAdminsPlus("Current game flavor is set to regular.", player);
}
break;
case "setrole":
if (contentSep.length === 2) {
let name = contentSep[0];
let challPlayer = this.getPlayer(name);
if (!challPlayer) {
this.sendMessageToAdminsPlus("Could not find player '" + name + "'", player);
return;
}
if (!messageSender.isAdmin) {
this.sendMessageToAdminsPlus("Cannot run setrole, " + messageSender.name + " is not an admin.", player);
return;
}
switch (contentSep[1].toLowerCase()) {
case "spectator":
if (challPlayer.role == ChallengePlayerRole.admin) {
this.sendMessageToAdminsPlus("Setting player '" + name + "' to adminSpectator.", player);
challPlayer.role = ChallengePlayerRole.adminSpectator;
} else {
this.sendMessageToAdminsPlus("Setting player '" + name + "' to spectator.", player);
challPlayer.role = ChallengePlayerRole.spectator;
}
break;
case "admin":
this.sendMessageToAdminsPlus("Setting player '" + name + "' to admin.", player);
challPlayer.role = ChallengePlayerRole.admin;
break;
case "judge":
this.sendMessageToAdminsPlus("Setting player '" + name + "' to judge.", player);
challPlayer.role = ChallengePlayerRole.judge;
break;
case "player":
this.sendMessageToAdminsPlus("Setting player '" + name + "' to player.", player);
challPlayer.lastTeamSwitchTick = this.tickIndex;
challPlayer.role = ChallengePlayerRole.player;
break;
}
}
break;
case "setallowteamchange":
if (contentSep.length === 2) {
let name = contentSep[0];
let challPlayer = this.getPlayer(name);
if (!challPlayer) {
this.sendMessageToAdminsPlus("Could not find player '" + name + "'", player);
return;
}
if (!messageSender.isAdmin) {
this.sendMessageToAdminsPlus(
"Cannot run setallowteamchange, " + messageSender.name + " is not an admin.",
player
);
return;
}
switch (contentSep[1].toLowerCase()) {
case "true":
case "t":
case "1":
this.sendMessageToAdminsPlus("Changing " + name + " to always allow team change.", player);
challPlayer.allowTeamChangeAlways = true;
break;
case "false":
case "f":
case "0":
this.sendMessageToAdminsPlus("Changing " + name + " to disallow team change.", player);
challPlayer.allowTeamChangeAlways = false;
break;
}
}
break;
case "setsize":
if (this.#phase !== ChallengePhase.setup) {
Log.debug("Cannot run setsize outside of setup phase.");
return;
}
if (!messageSender.isAdmin) {
this.sendMessageToAdminsPlus("Cannot run setsize, " + messageSender.name + " is not an admin.", player);
return;
}
if (contentSep.length === 1) {
switch (contentSep[0].toLowerCase()) {
case "s":
this.size = ChallengeBoardSize.small;
break;
case "m":
this.size = ChallengeBoardSize.medium;
break;
case "l":
this.size = ChallengeBoardSize.large;
break;
case "xl":
this.size = ChallengeBoardSize.xtralarge;
break;
}
}
break;
case "clearpads":
if (this.#phase !== ChallengePhase.setup) {
Log.debug("Cannot run clearpads outside of setup phase.");
return;
}
if (!messageSender.isAdmin) {
this.sendMessageToAdminsPlus("Cannot run clearpads, " + messageSender.name + " is not an admin.", player);
return;
}
this.sendMessageToAdminsPlus("Clearing pads!", player);
this.clearPads();
break;
}
}
}
getPlayer(name: string) {
name = this.canonicalizePlayerName(name);
for (let playerName in this.challengePlayers) {
let playerNameCand = this.canonicalizePlayerName(playerName);
if (name === playerNameCand) {
return this.challengePlayers[playerName];
}
}
return undefined;
}
ensurePlayerByName(name: string) {
const nameCanon = this.canonicalizePlayerName(name);
for (let playerName in this.challengePlayers) {
let playerNameCand = this.canonicalizePlayerName(playerName);
if (nameCanon === playerNameCand) {
return this.challengePlayers[playerName];
}
}
const newChallPlayer = new ChallengePlayer(this, name);
this.challengePlayers[nameCanon] = newChallPlayer;
return newChallPlayer;
}
setStart(x: number, y: number, z: number) {
Log.debug("Setting new start: " + x + " " + y + " " + z + " for " + this.teams.length + " teams");
this.nwbLocation = { x: x, y: y, z: z };
let ow = world.getDimension(MinecraftDimensionTypes.Overworld);
const centerX = x + Math.floor((PAD_SIZE_X + PAD_SURROUND_X) * 2.5);
const centerY = y + 1;
const centerZ = z + Math.floor((PAD_SIZE_Z + PAD_SURROUND_Z) * 2.5);
const airBlock = BlockPermutation.resolve(MinecraftBlockTypes.Air);
if (airBlock) {
Utilities.fillBlock(airBlock, centerX - 2, centerY - 2, centerZ - 2, centerX + 2, centerY + 2, centerZ + 2);
}
ow.runCommand(`tickingarea remove_all`);
for (let i = 0; i < 9; i++) {
const tickingAreaCommand = `tickingarea add ${Math.floor(x - 10 + (TOTAL_X / 8) * i)} ${y - 4} ${
z - 2
} ${Math.ceil(x - 10 + (TOTAL_X / 8) * (i + 1))} ${y + TOTAL_Y} ${z + TOTAL_Z + 2} bc${i} true`;
console.warn("Running: " + tickingAreaCommand);
try {
ow.runCommand(tickingAreaCommand);
} catch (e) {
console.warn("Failed to run: " + tickingAreaCommand);
}
}
world.setDefaultSpawnLocation({
x: centerX,
y: centerY,
z: centerZ,
});
this.save();
this.setupTracks();
system.run(this.clearPads); // add a delay so that tickingareas have a moment to get instated
}
clearPads() {
Log.debug("Clearing pads at " + this.nwbLocation.x + " " + this.nwbLocation.y + " " + this.nwbLocation.z);
this.clearTeamIter = 0;
this.clearTeamArea();
}
refreshTeam() {
let effectiveTeam = this.getTeamIndexFromSlot(this.refreshTeamIter);
if (effectiveTeam < this.teams.length) {
this.teams[effectiveTeam].updateLocation();
this.teams[effectiveTeam].ensurePad();
}
this.refreshTeamIter++;
if (this.refreshTeamIter < MAX_SLOTS) {
system.run(this.refreshTeam);
}
}
clearTeamArea() {
let effectiveTeam = this.getTeamIndexFromSlot(this.clearTeamIter % MAX_SLOTS);
if (effectiveTeam < this.teams.length) {
this.teams[effectiveTeam].updateLocation();
this.teams[effectiveTeam].clearPad(Math.floor(this.clearTeamIter / MAX_SLOTS)); // <-- NOTE THIS CLEAR OUT THE AIR ABOVE A PAD
}
this.clearTeamIter++;
if (this.clearTeamIter < MAX_SLOTS * 12) {
system.run(this.clearTeamArea);
} else {
this.refreshTeamIter = 0;
system.run(this.refreshTeam);
}
}
ensureAllPlayers() {
let players = world.getPlayers();
for (let player of players) {
this.ensurePlayer(player);
}
}
ensureAllPlayersHaveEquipment() {
let players = world.getPlayers();
for (let player of players) {
this.ensurePlayerHasProperEquipment(player);
}
}
updatePlayers() {
let players = world.getPlayers();
for (let player of players) {
let challPlayer = this.ensurePlayer(player);
if (challPlayer) {
if (challPlayer.isSpectator) {
let trackSequence = this.tickIndex % this.totalTrackTime;
let trackIndex = Math.floor(trackSequence / STANDARD_TRACK_TIME);
let track = this.tracks[trackIndex];
let vec = Utilities.lerp(track.from, track.to, (trackSequence % STANDARD_TRACK_TIME) / STANDARD_TRACK_TIME);
player.teleport(vec, {
dimension: world.getDimension(MinecraftDimensionTypes.Overworld),
facingLocation: {
x: vec.x + track.facingAdjust.x,
y: vec.y + track.facingAdjust.y,
z: vec.z + track.facingAdjust.z,
},
});
if (this.tickIndex % 1200 === 500 && this.#motdSubtitle && this.motdTitle) {
player.onScreenDisplay.setTitle(this.motdTitle, {
fadeInDuration: 1 * TicksPerSecond,
fadeOutDuration: 1 * TicksPerSecond,
stayDuration: 6 * TicksPerSecond,
subtitle: this.#motdSubtitle,
});
}
} else if (this.phase === ChallengePhase.build) {
let loc = player.location;
if (
loc.x >= this.nwbLocation.x &&
loc.x < this.nwbLocation.x + TOTAL_X && // is this player in the village area?
loc.y >= this.nwbLocation.y &&
loc.y < this.nwbLocation.y + TOTAL_Y &&
loc.z >= this.nwbLocation.z &&
loc.z < this.nwbLocation.z + TOTAL_Z
) {
for (let team of this.teams) {
if (team.index != challPlayer.teamId) {
if (
loc.x >= team.padNwbX - AIRSPACE_GAP &&
loc.y >= team.padNwbY - AIRSPACE_GAP &&
loc.z >= team.padNwbZ - AIRSPACE_GAP &&
loc.x <= team.padNwbX + PAD_SIZE_X + AIRSPACE_GAP &&
loc.y <= team.padNwbY + PAD_SIZE_Y + AIRSPACE_GAP &&
loc.z <= team.padNwbZ + PAD_SIZE_Z + AIRSPACE_GAP
) {
let westGap = loc.x - (team.padNwbX + AIRSPACE_GAP);
let eastGap = team.padNwbX + PAD_SIZE_X + AIRSPACE_GAP - loc.x;
let northGap = loc.z - (team.padNwbZ + +AIRSPACE_GAP);
let southGap = team.padNwbZ + PAD_SIZE_Z + AIRSPACE_GAP - loc.z;
let topGap = team.padNwbY + PAD_SIZE_Y + AIRSPACE_GAP - loc.y;
let newX = loc.x,
newY = loc.y,
newZ = loc.z;
if (westGap < eastGap && westGap < northGap && westGap < southGap && westGap < topGap) {
newX = team.padNwbX - AIRSPACE_GAP - 2;
} else if (eastGap < westGap && eastGap < northGap && eastGap < southGap && eastGap < topGap) {
newX = team.padNwbX + AIRSPACE_GAP + PAD_SIZE_X + 2;
} else if (topGap < westGap && topGap < southGap && topGap < eastGap && topGap < northGap) {
newY = team.padNwbY + AIRSPACE_GAP + PAD_SIZE_Y + 2;
} else if (northGap < westGap && northGap < southGap && northGap < eastGap && northGap < topGap) {
newZ = team.padNwbZ - AIRSPACE_GAP - 2;
} else {
newZ = team.padNwbZ + AIRSPACE_GAP + PAD_SIZE_Z + 2;
}
player.onScreenDisplay.setTitle(" ", {
fadeInDuration: 1 * TicksPerSecond,
fadeOutDuration: 1 * TicksPerSecond,
stayDuration: 3 * TicksPerSecond,
subtitle: "You cannot enter " + team.name + "'s area",
});
player.teleport({
x: newX,
y: newY,
z: newZ,
});
}
}
}
}
}
}
}
}
postInit() {
let ow = world.getDimension(MinecraftDimensionTypes.Overworld);
if (this.#motdTitle) {
world.sendMessage({ rawtext: [{ text: "§l" + this.#motdTitle }] });
}
if (this.#motdSubtitle) {
world.sendMessage({ rawtext: [{ text: this.#motdSubtitle }] });
}
ow.runCommand("gamerule sendcommandfeedback false");
ow.runCommand("gamerule mobgriefing false");
ow.runCommand("gamerule commandblocksenabled false");
ow.runCommand("gamerule commandblockoutput false");
ow.runCommand("gamerule tntexplodes false");
ow.runCommand("gamerule doinsomnia false");
ow.runCommand("gamerule pvp false");
system.runTimeout(this.updateMetaBonuses, 3);
system.runTimeout(this.refreshTeamScores, 6);
}
addScores() {
// adding main team score
let mainObj = world.scoreboard.addObjective("main", "Team Score");
world.scoreboard.setObjectiveAtDisplaySlot(DisplaySlotId.Sidebar, { objective: mainObj });
for (let team of this.teams) {
if (team.active || team.score > 0) {
team.applyScore();
}
}
}
applyFlavor() {}
applyPhase() {
let ow = world.getDimension(MinecraftDimensionTypes.Overworld);
switch (this.phase) {
case ChallengePhase.setup:
this.sendToAllPlayers("Setup Phase", {
fadeInDuration: 1 * TicksPerSecond,
fadeOutDuration: 1 * TicksPerSecond,
stayDuration: 7 * TicksPerSecond,
subtitle: "OPs are getting the game ready, hold on",
});
ow.runCommand("gamemode c");
this.applyRoleToAllPlayers();
break;
case ChallengePhase.pre:
this.sendToAllPlayers("Preliminary Phase", {
fadeInDuration: 1 * TicksPerSecond,
fadeOutDuration: 1 * TicksPerSecond,
stayDuration: 7 * TicksPerSecond,
subtitle: "Pull lever near a pad to join a team",
});
ow.runCommand("gamemode a");
this.applyRoleToAllPlayers();
break;
case ChallengePhase.build:
this.sendToAllPlayers("Build Phase", {
fadeInDuration: 1 * TicksPerSecond,
fadeOutDuration: 1 * TicksPerSecond,
stayDuration: 7 * TicksPerSecond,
subtitle: "Build things on your team pad",
});
ow.runCommand("gamemode s");
this.applyRoleToAllPlayers();
break;
case ChallengePhase.vote:
if (this.#flavor === ChallengeFlavor.goodVibes) {
this.sendToAllPlayers("Heart Phase", {
fadeInDuration: 1 * TicksPerSecond,
fadeOutDuration: 1 * TicksPerSecond,
stayDuration: 7 * TicksPerSecond,
subtitle: "Give your heart to\r\nyour favorite team builds",
});
this.ensureAllPlayersHaveEquipment();
} else {
this.sendToAllPlayers("Vote Phase", {
fadeInDuration: 1 * TicksPerSecond,
fadeOutDuration: 1 * TicksPerSecond,
stayDuration: 7 * TicksPerSecond,
subtitle: "Pull lever to vote for a team (2 votes)",
});
}
ow.runCommand("gamemode a");
this.applyRoleToAllPlayers();
break;
case ChallengePhase.post:
this.sendToAllPlayers("Post Phase", {
fadeInDuration: 1 * TicksPerSecond,
fadeOutDuration: 1 * TicksPerSecond,
stayDuration: 7 * TicksPerSecond,
subtitle: "Congratulate the winners",
});
ow.runCommand("gamemode a");
this.applyRoleToAllPlayers();
break;
}
}
sendMessageToAdminsPlus(message: string, additionalPlayer?: Player) {
Log.debug(message);
for (let player of world.getPlayers()) {
let challPlayer = this.ensurePlayer(player);
if (
challPlayer &&
(challPlayer.isAdmin ||
player === additionalPlayer ||
(additionalPlayer && player.name === additionalPlayer.name))
) {
player.sendMessage(message);
}
}
}
applyRoleToAllPlayers() {
for (let player of world.getPlayers()) {
let challPlayer = this.ensurePlayer(player);
if (challPlayer) {
challPlayer.applyRole();
}
}
}
sendToAllPlayers(title: string, options?: TitleDisplayOptions) {
for (let player of world.getPlayers()) {
player.onScreenDisplay.setTitle(title, options);
}
}
tick() {
// try {
this.tickIndex++;
if (this.tickIndex === POST_INIT_TICK) {
this.postInit();
}
if (this.tickIndex >= TEAM_INIT_TICK && this.tickIndex < TEAM_INIT_TICK + this.teams.length) {
this.teams[this.tickIndex - TEAM_INIT_TICK].initPad();
}
this.updatePlayers();
this.updateCount(this.tickIndex);
if (this.tickIndex % 200 === 0) {
this.updateTocks();
}
if (this.tickIndex % 600 === 0) {
this.updateMetaBonuses();
}
/* } catch (e) {
Log.debug("Challenge script error: " + e);
}*/
system.runTimeout(this.tick, 2);
}
updateMetaBonuses() {
let teamsByVote = [];
let teamsByTocks = [];
let hasChanged = false;
for (let i = 0; i < this.teams.length; i++) {
this.teams[i].votes = 0;
this.teams[i].rankByVote = -1;
teamsByVote.push(this.teams[i]);
if (this.teams[i].active || (this.teams[i].score > 0 && this.teams[i].playerTocks > 0)) {
teamsByTocks.push(this.teams[i]);
}
}
if (this.phase === ChallengePhase.post || this.phase === ChallengePhase.vote) {
for (let challengePlayerName in this.challengePlayers) {
let challPlayer = this.challengePlayers[challengePlayerName];
if (challPlayer) {
if (challPlayer.voteA >= 0 && challPlayer.voteA < this.teams.length) {
this.teams[challPlayer.voteA].votes++;
}
if (challPlayer.voteB >= 0 && challPlayer.voteB < this.teams.length) {
this.teams[challPlayer.voteB].votes++;
}
}
}
teamsByVote.sort(function (teamA: Team, teamB: Team) {
return teamB.votes - teamA.votes;
});
for (let i = 0; i < teamsByVote.length; i++) {
if (teamsByVote[i].rankByVote !== i && teamsByVote[i].votes > 0) {
teamsByVote[i].rankByVote = i;
hasChanged = true;
}
}
// in case of tie, adjust rank by vote
if (teamsByVote[1].votes === teamsByVote[0].votes) {
teamsByVote[1].rankByVote = teamsByVote[0].rankByVote;
}
if (teamsByVote[2].votes === teamsByVote[1].votes) {
teamsByVote[2].rankByVote = teamsByVote[1].rankByVote;
}
}
teamsByTocks.sort(function (teamA: Team, teamB: Team) {
return teamB.playerTocks - teamA.playerTocks;
});
for (let i = 0; i < teamsByTocks.length; i++) {
let newQuartile = Math.floor((i * 4) / teamsByTocks.length);
if (teamsByTocks[i].teamUsageQuartile !== newQuartile) {
teamsByTocks[i].teamUsageQuartile = newQuartile;
hasChanged = true;
}
}
if (hasChanged) {
this.refreshTeamScores();
}
}
updateTocks() {
let isActive = false;
for (let player of world.getPlayers()) {
let collPlayer = this.ensurePlayer(player);
if (collPlayer && collPlayer.teamId >= 0 && collPlayer.teamId < this.teams.length) {
let team = this.teams[collPlayer.teamId];
let loc = player.location;
if (loc.x !== collPlayer.tockX || loc.y !== collPlayer.tockY || loc.z !== collPlayer.tockZ) {
collPlayer.tockX = loc.x;
collPlayer.tockY = loc.y;
collPlayer.tockZ = loc.z;
isActive = true;
team.playerTocks++;
}
}
}
if (isActive) {
this.save();
}
}
updateCount(tick: number) {
const teamIndex = Math.floor(tick / 16) % this.teams.length;
let team = this.teams[teamIndex];
if (team && !team.active) {
return;
}
const area = tick % 16;
if (area === 0) {
this.activeTeamScore = 0;
}
let ow = world.getDimension(MinecraftDimensionTypes.Overworld);
if (this.activeTeamScore >= 0) {
let canaryLoc = { x: team.padNwbX, y: team.padNwbY, z: team.padNwbZ + (PAD_SIZE_Z / 16) * area };
let canaryBlock = ow.getBlock(canaryLoc);
// if we don't find blackstone at our canary location, assume the chunk is loaded and bail on calculating score.
if (canaryBlock && canaryBlock.typeId !== "minecraft:sandstone") {
Log.debug(
"Did not find sandstone at " +
team.padNwbX +
" " +
team.padNwbY +
" " +
String(team.padNwbZ + (PAD_SIZE_Z / 16) * area)
);
this.activeTeamScore = -1;
return;
}
for (let i = 0; i < PAD_SIZE_X; i++) {
for (let j = 0; j < PAD_SIZE_Y; j++) {
for (let k = (PAD_SIZE_Z / 16) * area; k < (PAD_SIZE_Z / 16) * (area + 1); k++) {
let loc = { x: team.padNwbX + i, y: team.padNwbY + j + 1, z: team.padNwbZ + k };
let block = ow.getBlock(loc);
if (block) {
let typeId = block.typeId.toLowerCase().trim();
if (typeId.startsWith("minecraft:")) {
typeId = typeId.substring(10);
}
if (typeId !== "air") {
if (BLOCK_SCORESHEET[typeId] > 0) {
this.activeTeamScore += BLOCK_SCORESHEET[typeId];
}
if (typeId === "chest") {
let leftLoc = { x: loc.x - 1, y: loc.y, z: loc.z };
let northLoc = { x: loc.x, y: loc.y, z: loc.z - 1 };
let leftBlock = ow.getBlock(leftLoc);
let northBlock = ow.getBlock(northLoc);
// if this is a double chest, only calc inventory from the west or northern side of the chest
// todo: improve to accommodate double chests placed right next to each other
if (
leftBlock &&
leftBlock.typeId.indexOf("chest") < 0 &&
northBlock &&
northBlock.typeId.indexOf("chest") < 0
) {
let invComp = block.getComponent(BlockComponentTypes.Inventory) as BlockInventoryComponent;
if (invComp) {
let cont = invComp.container;
if (cont) {
for (let ci = 0; ci < cont.size; ci++) {
let item = cont.getItem(ci);
if (item) {
let itemTypeId = item.typeId.toLowerCase().trim();
if (itemTypeId.startsWith("minecraft:")) {
itemTypeId = itemTypeId.substring(10);
}
if (ITEM_SCORESHEET[itemTypeId] > 0) {
this.activeTeamScore += ITEM_SCORESHEET[itemTypeId] * item.amount;
} else if (BLOCK_SCORESHEET[itemTypeId] > 0) {
this.activeTeamScore += Math.floor(BLOCK_SCORESHEET[itemTypeId] / 2) * item.amount;
}
}
}
}
}
}
}
}
}
}
}
}
}
if (area === 15) {
if (this.activeTeamScore !== team.score && this.activeTeamScore >= 0) {
if (this.activeTeamScore > team.score && team.score > 0) {
for (let challPlayer of team.players) {
try {
if (challPlayer.player && challPlayer.player.isValid) {
challPlayer.player.playSound("random.orb");
}
} catch (e) {}
}
}
team.score = this.activeTeamScore;
team.applyScore();
this.save();
}
}
}
showMotd(player: Player) {
if (this.#motdTitle) {
player.sendMessage([`§l${this.#motdTitle}`]);
}
if (this.#motdSubtitle) {
player.sendMessage([this.#motdSubtitle]);
}
}
playerLeft(event: PlayerLeaveAfterEvent) {
if (!event.playerName) {
return;
}
let cp = this.getPlayerFromName(event.playerName);
if (cp) {
cp.player = undefined;
}
}
playerSpawned(event: PlayerSpawnAfterEvent) {
if (!event.player) {
return;
}
this.ensurePlayerHasProperEquipment(event.player);
this.ensurePlayer(event.player);
if (event.initialSpawn) {
this.showMotd(event.player);
}
switch (this.phase) {
case ChallengePhase.setup:
event.player.onScreenDisplay.setTitle("Setup Phase", {
fadeInDuration: 1 * TicksPerSecond,
fadeOutDuration: 1 * TicksPerSecond,
stayDuration: 7 * TicksPerSecond,
subtitle: "OPs are getting the game ready, hold on",
});
break;
case ChallengePhase.pre:
event.player.onScreenDisplay.setTitle("Preliminary Phase", {
fadeInDuration: 1 * TicksPerSecond,
fadeOutDuration: 1 * TicksPerSecond,
stayDuration: 7 * TicksPerSecond,
subtitle: "Pull lever near a pad to join a team",
});
break;
case ChallengePhase.build:
event.player.onScreenDisplay.setTitle("Build Phase", {
fadeInDuration: 1 * TicksPerSecond,
fadeOutDuration: 1 * TicksPerSecond,
stayDuration: 7 * TicksPerSecond,
subtitle: "Build things on your team pad",
});
break;
case ChallengePhase.vote:
if (this.#flavor === ChallengeFlavor.goodVibes) {
event.player.onScreenDisplay.setTitle("Heart Phase", {
fadeInDuration: 1 * TicksPerSecond,
fadeOutDuration: 1 * TicksPerSecond,
stayDuration: 7 * TicksPerSecond,
subtitle: "Give a heart to your two favorite builds",
});
} else {
event.player.onScreenDisplay.setTitle("Vote Phase", {
fadeInDuration: 1 * TicksPerSecond,
fadeOutDuration: 1 * TicksPerSecond,
stayDuration: 7 * TicksPerSecond,
subtitle: "Pull lever to vote for a team (2 votes)",
});
}
break;
case ChallengePhase.post:
event.player.onScreenDisplay.setTitle("Post Phase", {
fadeInDuration: 1 * TicksPerSecond,
fadeOutDuration: 1 * TicksPerSecond,
stayDuration: 7 * TicksPerSecond,
subtitle: "Congratulate the winners",
});
}
}
itemUseOn(event: ItemStopUseOnAfterEvent) {
if (!event.source) {
return;
}
if (event.itemStack && event.itemStack.typeId === "bc:heart" && this.#phase === ChallengePhase.vote) {
this.doAction(event.source, event.block);
}
}
leverAction(event: LeverActionAfterEvent) {
if (!event.player) {
return;
}
this.doAction(event.player, event.block);
}
doAction(player: Player, block: Block) {
let x = block.x;
let y = block.y;
let z = block.z;
for (let team of this.teams) {
if (x === team.nwbX + JOIN_TEAM_X && y === team.nwbY + JOIN_TEAM_Y && z === team.nwbZ + JOIN_TEAM_Z) {
let challPlayer = this.ensurePlayer(player);
if (challPlayer) {
if (this.#phase === ChallengePhase.vote) {
if (challPlayer.teamId === team.index) {
if (this.#flavor === ChallengeFlavor.goodVibes) {
player.onScreenDisplay.setTitle(`Share the love`, {
fadeInDuration: 1 * TicksPerSecond,
fadeOutDuration: 1 * TicksPerSecond,
stayDuration: 5 * TicksPerSecond,
subtitle: "Give hearts to teams other than your own.",
});
} else {
player.onScreenDisplay.setTitle(`Nope`, {
fadeInDuration: 1 * TicksPerSecond,
fadeOutDuration: 1 * TicksPerSecond,
stayDuration: 5 * TicksPerSecond,
subtitle: "You can't vote for your own team.",
});
}
} else if (challPlayer.allowTeamChangeAlways) {
player.onScreenDisplay.setTitle(`Nope`, {
fadeInDuration: 1 * TicksPerSecond,
fadeOutDuration: 1 * TicksPerSecond,
stayDuration: 5 * TicksPerSecond,
subtitle: "A temporary account can't vote for teams.",
});
} else {
if (challPlayer.voteA !== team.index && challPlayer.teamId !== team.index) {
challPlayer.voteB = challPlayer.voteA;
challPlayer.voteA = team.index;
this.updateMetaBonuses();
}
let noun = "vote";
if (this.#flavor === ChallengeFlavor.goodVibes) {
noun = "heart";
}
if (challPlayer.voteB >= 0) {
player.onScreenDisplay.setTitle(`Your ` + noun + `s:`, {
fadeInDuration: 1 * TicksPerSecond,
fadeOutDuration: 1 * TicksPerSecond,
stayDuration: 5 * TicksPerSecond,
subtitle: this.teams[challPlayer.voteA].name + ", " + this.teams[challPlayer.voteB].name,
});
} else {
player.onScreenDisplay.setTitle(`Your ` + noun + `:`, {
fadeInDuration: 1 * TicksPerSecond,
fadeOutDuration: 1 * TicksPerSecond,
stayDuration: 5 * TicksPerSecond,
subtitle: this.teams[challPlayer.voteA].name,
});
}
}
} else if (challPlayer.teamId === team.index) {
player.onScreenDisplay.setTitle(`Already joined ` + team.name);
} else if (team.getEffectiveTeamPlayerCount() >= MAX_PLAYERS_TEAM) {
player.onScreenDisplay.setTitle(`Team ${team.name} is full at ${MAX_PLAYERS_TEAM}`);
} else if (
challPlayer.teamId >= 0 &&
!challPlayer.isAdmin &&
!challPlayer.allowTeamChangeAlways &&
this.tickIndex > challPlayer.lastTeamSwitchTick + MAX_TIME_TO_SWITCH_TEAMS_TICK
) {
player.onScreenDisplay.setTitle(`You can no longer switch teams.`);
} else {
if (challPlayer.teamId >= 0 && challPlayer.teamId < this.teams.length) {
this.teams[challPlayer.teamId].removePlayer(challPlayer);
}
challPlayer.lastTeamSwitchTick = this.tickIndex;
challPlayer.teamId = team.index;
team.ensurePlayerIsOnTeam(challPlayer);
player.onScreenDisplay.setTitle(`Joined`, {
fadeInDuration: 1 * TicksPerSecond,
fadeOutDuration: 1 * TicksPerSecond,
stayDuration: 7 * TicksPerSecond,
subtitle: team.name,
});
}
}
} else if (
x === team.nwbX + TEAM_OPTIONS_X &&
y === team.nwbY + TEAM_OPTIONS_Y &&
z ==