UNPKG

@minecraft/creator-tools

Version:

Minecraft Creator Tools command line and libraries.

1,636 lines (1,347 loc) 52.7 kB
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 ==