Last active
May 30, 2025 07:17
-
-
Save Teekeks/0d2820ac1a17ae723a5a0e0733ed17fb to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Generates a minecraft litematic file which defines a circle of beacons that surround the center with a dome spanning over all. | |
# | |
# The beacons are placed in such a way that the beams are visually as uniformly spaced as possible from the center. | |
# The beams are colored in a smooth color transition from one beacon to the next. | |
# Each beam is surrounded by a random scattering of "particles" (glass panes) of appropriate colors. | |
# | |
# Inspired by this video: https://www.youtube.com/watch?v=VcsEm7FnheU by Squibble | |
import math | |
from itertools import accumulate | |
from litemapy import Region, BlockState | |
import random | |
###################################################################################################################################################### | |
# SETTINGS | |
###################################################################################################################################################### | |
MIN_DISTANCE = 53 # minimum distance the beacon could be placed from the center | |
MAX_DISTANCE = 91 # maximum distance the beacon could be placed from the center | |
STEP_RESOLUTION = 5000 # the nr of places the program should check per 1 block distance from the center | |
COLOR_OFFSET = 1 # nr of blocks the color stack is offset from the beacon | |
BLOCK_BEACON_BASE = BlockState("minecraft:iron_block") # the block to use for the beacon base | |
BEACON_BASE_SIZE = 4 # the height of the beacon base, between 1 and 4 | |
BEAM_PARTICLE_HEIGHT_OFFSET = 10 # the height offset of the particles measured from the top of the color stack | |
BEAM_PARTICLE_HEIGHT = 180 # the height of the particle beam surrounding the beacon | |
BEAM_PARTICLE_MIN_DISTANCE = 1 # the minimum distance beam particles should have from the beacon beam | |
BEAM_PARTICLE_MAX_DISTANCE = 3 # the maximum distance beam particles should have from the beacon beam | |
BEAM_PARTICLE_WEIGHT = 2 # the weight set on the middle of the particles | |
BEAM_PARTICLE_COUNT = 100 # the number of particles arround the beacon beam | |
DOME_OFFSET = 22 # Y offset of the dome | |
DOME_SIZE = 180 | |
DOME_CUTTOF = 96 | |
DOME_LIGHT_BLOCK = BlockState("minecraft:end_rod", facing="down") | |
DOME_LIGHT_COUNT = 1500 | |
DOME_PARTICLE_COUNT = 10000 | |
COLOR_TAPE = [ # the colors to use for the rainbow colors | |
[BlockState("minecraft:blue_stained_glass"), BlockState("minecraft:blue_stained_glass_pane")], | |
[BlockState("minecraft:light_blue_stained_glass"), BlockState("minecraft:light_blue_stained_glass_pane")], | |
[BlockState("minecraft:green_stained_glass"), BlockState("minecraft:green_stained_glass_pane")], | |
[BlockState("minecraft:yellow_stained_glass"), BlockState("minecraft:yellow_stained_glass_pane")], | |
[BlockState("minecraft:orange_stained_glass"), BlockState("minecraft:orange_stained_glass_pane")], | |
[BlockState("minecraft:red_stained_glass"), BlockState("minecraft:red_stained_glass_pane")], | |
] | |
TAPE_SEQUENCE = [ # the sequence to use for color mixing, 0 is the previous color, 1 is the current one | |
[0, 0, 0, 0, 0], | |
[0, 1, 0, 0, 0], | |
[0, 0, 1, 0, 0], | |
[0, 1, 1, 0, 0], | |
[0, 0, 0, 1, 0], | |
[0, 1, 0, 1, 0], | |
[0, 0, 1, 1, 0], | |
[0, 1, 1, 1, 0], | |
[0, 0, 0, 0, 1], | |
[0, 1, 0, 0, 1], | |
[0, 0, 1, 0, 1], | |
[0, 1, 1, 0, 1], | |
[0, 0, 0, 1, 1], | |
[0, 1, 0, 1, 1], | |
[0, 0, 1, 1, 1], | |
[0, 1, 1, 1, 1], | |
] | |
###################################################################################################################################################### | |
###################################################################################################################################################### | |
SCHEM_OFFSET = MAX_DISTANCE + 2 | |
BLOCK_BEACON = BlockState("minecraft:beacon") | |
base_height = 1 + BEACON_BASE_SIZE + COLOR_OFFSET + len(TAPE_SEQUENCE[0]) + BEAM_PARTICLE_HEIGHT_OFFSET + BEAM_PARTICLE_HEIGHT | |
if base_height < DOME_SIZE + DOME_OFFSET + 1: | |
base_height = DOME_SIZE + DOME_OFFSET + 1 | |
BLOCK_CLEAR = BlockState("minecraft:air") | |
if SCHEM_OFFSET < DOME_SIZE: | |
SCHEM_OFFSET = DOME_SIZE | |
reg = Region(-SCHEM_OFFSET, | |
0, | |
-SCHEM_OFFSET, | |
SCHEM_OFFSET * 2 + 1, | |
base_height, | |
SCHEM_OFFSET * 2 + 1) | |
def gen_color_sequence(): | |
res = [] | |
for idx, _c in enumerate(COLOR_TAPE): | |
_color, _beam_color = _c | |
prev, prev_beam = COLOR_TAPE[idx - 1] | |
for seq in TAPE_SEQUENCE: | |
res.append([[_color if _a == 1 else prev for _a in seq], [_beam_color if _a == 1 else prev_beam for _a in seq]]) | |
return res | |
class Vec2d: | |
def __init__(self, x, y): | |
self.x = x | |
self.y = y | |
@classmethod | |
def from_angle(cls, angle): | |
return cls(math.sin(angle), math.cos(angle)) | |
def length(self) -> float: | |
return abs(math.sqrt(self.x ** 2 + self.y ** 2)) | |
def normalize(self) -> "Vec2d": | |
length = self.length() | |
self.x, self.y = self.x / length, self.y / length | |
return self | |
def __mul__(self, other): | |
return Vec2d(self.x * other, self.y * other) | |
def int_dist(self): | |
return math.dist([self.x, self.y], [round(self.x), round(self.y)]) | |
def __repr__(self): | |
return f"Vec2d({self.x}, {self.y})" | |
def rand_sphere_coords(sphere_size, center): | |
theta = random.random() * 2 * math.pi | |
phi = random.random() * math.pi | |
return [ | |
round(sphere_size * math.cos(theta) * math.sin(phi) + center[0]), | |
round(sphere_size * math.sin(theta) * math.sin(phi) + center[1]), | |
round(sphere_size * math.cos(phi) + center[2]), | |
] | |
beacon_spots = [] | |
color_stacks = gen_color_sequence() | |
print(f'generating beacon circle with {len(color_stacks)} beacons') | |
step = 360 / len(color_stacks) | |
for i, cs in enumerate(color_stacks): | |
color_stack, beams = cs | |
vec = Vec2d.from_angle(math.radians(step * i)).normalize() | |
vecs = sorted([vec * (dist / STEP_RESOLUTION) | |
for dist in range(MIN_DISTANCE * STEP_RESOLUTION, MAX_DISTANCE * STEP_RESOLUTION)], | |
key=lambda d: d.int_dist())[0] | |
best = [round(vecs.x + SCHEM_OFFSET - 0.5), round(vecs.y + SCHEM_OFFSET - 0.5)] | |
print(f'found position for beacon {i + 1} at {best}') | |
beacon_spots.append((best[0], best[1])) | |
reg[best[0], BEACON_BASE_SIZE, best[1]] = BLOCK_BEACON | |
# beacon base | |
for base_size in range(BEACON_BASE_SIZE): | |
for x in range(-1 - base_size, 2 + base_size): | |
for y in range(-1 - base_size, 2 + base_size): | |
reg[best[0] + x, BEACON_BASE_SIZE - base_size - 1, best[1] + y] = BLOCK_BEACON_BASE | |
# color stack | |
for _i, c in enumerate(color_stack, start=1+COLOR_OFFSET+BEACON_BASE_SIZE): | |
reg[best[0], _i, best[1]] = c | |
# beam particles | |
particle_height_start = 1+COLOR_OFFSET+BEACON_BASE_SIZE+BEAM_PARTICLE_HEIGHT_OFFSET | |
part_mid = BEAM_PARTICLE_HEIGHT // 2 | |
cum_weight = list(accumulate([(part_mid - abs(part_mid - _x)) * BEAM_PARTICLE_WEIGHT for _x in range(BEAM_PARTICLE_HEIGHT)])) | |
height_vals = list(range(BEAM_PARTICLE_HEIGHT)) | |
for _ in range(BEAM_PARTICLE_COUNT): | |
rand_vec = (Vec2d.from_angle(math.radians(random.uniform(0, 360))).normalize() * | |
random.uniform(BEAM_PARTICLE_MIN_DISTANCE, BEAM_PARTICLE_MAX_DISTANCE)) | |
reg[round(best[0] + rand_vec.x), | |
particle_height_start + random.choices(height_vals, cum_weights=cum_weight, k=1)[0], | |
round(best[1] + rand_vec.y)] = random.choice(beams) | |
dome_mid = [SCHEM_OFFSET, DOME_OFFSET, SCHEM_OFFSET] | |
light_count = 0 | |
while light_count < DOME_LIGHT_COUNT: | |
rand_spot = rand_sphere_coords(DOME_SIZE, dome_mid) | |
if rand_spot[1] < DOME_CUTTOF: | |
continue | |
light_count += 1 | |
if light_count % 1000 == 0: | |
print(f'generated {light_count} lights in dome...') | |
reg[rand_spot[0], rand_spot[1], rand_spot[2]] = DOME_LIGHT_BLOCK | |
print(f'done generating {light_count} lights in dome') | |
dome_count = 0 | |
beams = [_x[1] for _x in COLOR_TAPE] | |
while dome_count < DOME_PARTICLE_COUNT: | |
rand_spot = rand_sphere_coords(DOME_SIZE, dome_mid) | |
if rand_spot[1] < DOME_CUTTOF: | |
continue | |
dist_vec = Vec2d(rand_spot[0] - SCHEM_OFFSET, rand_spot[2] - SCHEM_OFFSET) | |
if dist_vec.length() > random.random() * DOME_SIZE: | |
continue | |
# do not overlap with a beacon beam | |
dome_count += 1 | |
reg[rand_spot[0], rand_spot[1], rand_spot[2]] = random.choice(beams) | |
if dome_count % 1000 == 0: | |
print(f'generated {dome_count} dome particles...') | |
print(f'done generating {dome_count} dome particles...') | |
print('cleaning path above beacon beams...') | |
clean_start_height = 1+COLOR_OFFSET+BEACON_BASE_SIZE + len(TAPE_SEQUENCE[0]) | |
for beam in beacon_spots: | |
for y in range(clean_start_height, reg.height): | |
reg[beam[0], y, beam[1]] = BLOCK_CLEAR | |
print(f'writing file...') | |
schem = reg.as_schematic(name="beacon_circle") | |
schem.save("beacon_circle.litematic") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment