Add in-world zone visualizer, note particles, scenario shuffle, and step editor

This commit is contained in:
rardog
2026-09-24 13:09:53 +03:00
parent fba6ffacd4
commit f72b01b68b
6 changed files with 295 additions and 13 deletions
@@ -55,6 +55,7 @@ public final class ClientHooks {
@SubscribeEvent
public static void onRenderWorld(RenderWorldLastEvent event) {
DebugOverlayRenderer.render(event.getMatrixStack(), event.getPartialTicks());
com.example.soundmod.client.render.ZoneWorldRenderer.render(event.getMatrixStack(), event.getPartialTicks());
}
@SubscribeEvent
@@ -0,0 +1,138 @@
package com.example.soundmod.client.render;
import com.example.soundmod.client.screen.SoundPlayerScreen;
import com.example.soundmod.common.ZoneShape;
import com.mojang.blaze3d.matrix.MatrixStack;
import com.mojang.blaze3d.vertex.IVertexBuilder;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.IRenderTypeBuffer;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.client.renderer.WorldRenderer;
import net.minecraft.util.math.AxisAlignedBB;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.MathHelper;
import net.minecraft.util.math.vector.Matrix3f;
import net.minecraft.util.math.vector.Matrix4f;
import net.minecraft.util.math.vector.Vector3d;
public final class ZoneWorldRenderer {
private ZoneWorldRenderer() {}
public static void render(MatrixStack stack, float partialTicks) {
SoundPlayerScreen screen = SoundPlayerScreen.activeScreen;
if (screen == null || !SoundPlayerScreen.showZoneVisualizer) {
return;
}
Minecraft mc = Minecraft.getInstance();
if (mc.player == null || mc.level == null) {
return;
}
BlockPos blockPos = screen.getBlockPos();
if (blockPos == null) {
return;
}
Vector3d camera = mc.gameRenderer.getMainCamera().getPosition();
IRenderTypeBuffer.Impl bufferSource = mc.renderBuffers().bufferSource();
IVertexBuilder lineBuilder = bufferSource.getBuffer(RenderType.lines());
// 1. Highlight the edited block itself (soft gold)
double bx = blockPos.getX() - camera.x;
double by = blockPos.getY() - camera.y;
double bz = blockPos.getZ() - camera.z;
AxisAlignedBB blockBox = new AxisAlignedBB(bx, by, bz, bx + 1.0, by + 1.0, bz + 1.0);
WorldRenderer.renderLineBox(stack, lineBuilder, blockBox, 1.0f, 0.85f, 0.2f, 0.9f);
// 2. Zone center
double cx;
double cy;
double cz;
if (screen.isZoneCustomCoords()) {
cx = (double) screen.getZoneSourceX() + 0.5 - camera.x;
cy = (double) screen.getZoneSourceY() + 0.5 - camera.y;
cz = (double) screen.getZoneSourceZ() + 0.5 - camera.z;
// Draw connecting line from block center to custom center
drawLine(stack, lineBuilder, bx + 0.5, by + 0.5, bz + 0.5, cx, cy, cz, 1.0f, 0.6f, 0.1f, 0.85f);
} else {
cx = bx + 0.5;
cy = by + 0.5;
cz = bz + 0.5;
}
// 3. Render Zone depending on shape
if (screen.getZoneShape() == ZoneShape.SPHERE) {
float radius = Math.max(0.5f, screen.getZoneRadius());
// Equatorial and vertical rings in cyan
drawCircleRing(stack, lineBuilder, cx, cy, cz, radius, 0, 0.2f, 0.85f, 1.0f, 0.95f);
drawCircleRing(stack, lineBuilder, cx, cy, cz, radius, 1, 0.2f, 0.85f, 1.0f, 0.75f);
drawCircleRing(stack, lineBuilder, cx, cy, cz, radius, 2, 0.2f, 0.85f, 1.0f, 0.75f);
// Bounding box around the sphere (light translucent)
AxisAlignedBB sphereBox = new AxisAlignedBB(cx - radius, cy - radius, cz - radius, cx + radius, cy + radius, cz + radius);
WorldRenderer.renderLineBox(stack, lineBuilder, sphereBox, 0.15f, 0.6f, 0.9f, 0.4f);
} else {
// BOX shape
float hx = Math.max(0.5f, screen.getZoneBoxX());
float hy = Math.max(0.5f, screen.getZoneBoxY());
float hz = Math.max(0.5f, screen.getZoneBoxZ());
AxisAlignedBB zoneBox = new AxisAlignedBB(cx - hx, cy - hy, cz - hz, cx + hx, cy + hy, cz + hz);
WorldRenderer.renderLineBox(stack, lineBuilder, zoneBox, 0.25f, 1.0f, 0.5f, 0.95f);
}
bufferSource.endBatch(RenderType.lines());
}
private static void drawCircleRing(MatrixStack stack, IVertexBuilder builder, double cx, double cy, double cz, double r, int plane, float red, float green, float blue, float alpha) {
int segments = 36;
for (int i = 0; i < segments; i++) {
double a1 = (i * 2.0 * Math.PI) / segments;
double a2 = ((i + 1) * 2.0 * Math.PI) / segments;
double x1;
double y1;
double z1;
double x2;
double y2;
double z2;
if (plane == 0) {
x1 = cx + Math.cos(a1) * r;
y1 = cy;
z1 = cz + Math.sin(a1) * r;
x2 = cx + Math.cos(a2) * r;
y2 = cy;
z2 = cz + Math.sin(a2) * r;
} else if (plane == 1) {
x1 = cx + Math.cos(a1) * r;
y1 = cy + Math.sin(a1) * r;
z1 = cz;
x2 = cx + Math.cos(a2) * r;
y2 = cy + Math.sin(a2) * r;
z2 = cz;
} else {
x1 = cx;
y1 = cy + Math.sin(a1) * r;
z1 = cz + Math.cos(a1) * r;
x2 = cx;
y2 = cy + Math.sin(a2) * r;
z2 = cz + Math.cos(a2) * r;
}
drawLine(stack, builder, x1, y1, z1, x2, y2, z2, red, green, blue, alpha);
}
}
private static void drawLine(MatrixStack stack, IVertexBuilder builder, double x1, double y1, double z1, double x2, double y2, double z2, float r, float g, float b, float a) {
Matrix4f m = stack.last().pose();
Matrix3f n = stack.last().normal();
float dx = (float)(x2 - x1);
float dy = (float)(y2 - y1);
float dz = (float)(z2 - z1);
float len = MathHelper.sqrt(dx * dx + dy * dy + dz * dz);
if (len > 1.0e-5f) {
dx /= len;
dy /= len;
dz /= len;
}
builder.vertex(m, (float)x1, (float)y1, (float)z1).color(r, g, b, a).normal(n, dx, dy, dz).endVertex();
builder.vertex(m, (float)x2, (float)y2, (float)z2).color(r, g, b, a).normal(n, dx, dy, dz).endVertex();
}
}
@@ -61,8 +61,13 @@ public class SoundPlayerScreen extends ContainerScreen<SoundPlayerContainer> {
private SoundSettings localSettings = new SoundSettings();
private final List<ScenarioQueue.Step> sourceScenarioSteps = new ArrayList<>();
private final List<ScenarioQueue.Step> zoneScenarioSteps = new ArrayList<>();
public static SoundPlayerScreen activeScreen;
public static boolean showZoneVisualizer = true;
private boolean sourceScenarioLoop;
private boolean zoneScenarioLoop;
private boolean sourceScenarioShuffle;
private boolean zoneScenarioShuffle;
private boolean editingZoneScenario;
private boolean sourceEnabled = true;
private boolean zoneEnabled = true;
@@ -123,6 +128,7 @@ public class SoundPlayerScreen extends ContainerScreen<SoundPlayerContainer> {
private Button zoneStopOnExitButton;
private Button zoneUseCustomCoordsButton;
private Button myPosButton;
private Button zoneVisualizerButton;
private TextFieldWidget zoneRadiusField;
private TextFieldWidget zoneBoxXField;
private TextFieldWidget zoneBoxYField;
@@ -134,9 +140,11 @@ public class SoundPlayerScreen extends ContainerScreen<SoundPlayerContainer> {
// Controls - Scenario
private Button scenarioTargetButton;
private Button scenarioLoopButton;
private Button scenarioShuffleButton;
private TextFieldWidget scenarioSoundField;
private TextFieldWidget scenarioDelayField;
private Button scenarioUseCurrentBtn;
private Button scenarioSetButton;
private Button scenarioAddButton;
private Button scenarioRemoveButton;
private Button scenarioUpButton;
@@ -195,9 +203,11 @@ public class SoundPlayerScreen extends ContainerScreen<SoundPlayerContainer> {
this.sourceScenarioSteps.clear();
this.sourceScenarioSteps.addAll(tile.getScenario().steps);
this.sourceScenarioLoop = tile.getScenario().loop;
this.sourceScenarioShuffle = tile.getScenario().shuffle;
this.zoneScenarioSteps.clear();
this.zoneScenarioSteps.addAll(tile.getZoneScenario().steps);
this.zoneScenarioLoop = tile.getZoneScenario().loop;
this.zoneScenarioShuffle = tile.getZoneScenario().shuffle;
this.sourceEnabled = tile.isSourceEnabled();
this.zoneEnabled = tile.isZoneEnabled();
this.wizardCompleted = tile.isWizardCompleted();
@@ -214,6 +224,7 @@ public class SoundPlayerScreen extends ContainerScreen<SoundPlayerContainer> {
this.zoneSourceY = tile.getZoneSourceY();
this.zoneSourceZ = tile.getZoneSourceZ();
}
activeScreen = this;
this.initFieldsAndButtons();
this.addTabButtons();
@@ -363,6 +374,10 @@ public class SoundPlayerScreen extends ContainerScreen<SoundPlayerContainer> {
this.zoneBoxXField = this.createField(x + 18, y + 68, 42, this.floatText(this.zoneBoxX, 1));
this.zoneBoxYField = this.createField(x + 78, y + 68, 42, this.floatText(this.zoneBoxY, 1));
this.zoneBoxZField = this.createField(x + 138, y + 68, 42, this.floatText(this.zoneBoxZ, 1));
this.zoneVisualizerButton = this.addButtonTracked(new Button(x + 150, y + 68, 100, 20, this.boolLabel("Вид в мире", showZoneVisualizer), b -> {
showZoneVisualizer = !showZoneVisualizer;
b.setMessage(this.boolLabel("Вид в мире", showZoneVisualizer));
}), this.zoneButtons);
this.zoneAutoStartButton = this.addButtonTracked(new Button(x, y + 94, 122, 20, this.boolLabel("Авто-вход", this.zoneAutoStartOnEnter), b -> {
this.zoneAutoStartOnEnter = !this.zoneAutoStartOnEnter;
@@ -389,23 +404,30 @@ public class SoundPlayerScreen extends ContainerScreen<SoundPlayerContainer> {
this.zoneSourceZField = this.createField(x + 178, y + 146, 58, Integer.toString(this.zoneSourceZ));
// --- TAB 5: SCENARIO ---
this.scenarioTargetButton = this.addButtonTracked(new Button(x, y + 42, 122, 20, new StringTextComponent(this.editingZoneScenario ? "Шаги: Зона" : "Шаги: Источник"), b -> {
this.scenarioTargetButton = this.addButtonTracked(new Button(x, y + 42, 78, 20, new StringTextComponent(this.editingZoneScenario ? "Шаги: Зона" : "Шаги: Блок"), b -> {
this.editingZoneScenario = !this.editingZoneScenario;
b.setMessage(new StringTextComponent(this.editingZoneScenario ? "Шаги: Зона" : "Шаги: Источник"));
b.setMessage(new StringTextComponent(this.editingZoneScenario ? "Шаги: Зона" : "Шаги: Блок"));
this.selectedScenarioIndex = -1;
this.scenarioLoopButton.setMessage(this.boolLabel("Цикл очереди", this.activeScenarioLoop()));
this.scenarioLoopButton.setMessage(this.boolLabel("Цикл", this.activeScenarioLoop()));
this.scenarioShuffleButton.setMessage(this.boolLabel("Случайно", this.activeScenarioShuffle()));
}), this.scenarioButtons);
this.scenarioLoopButton = this.addButtonTracked(new Button(x + 126, y + 42, 124, 20, this.boolLabel("Цикл очереди", this.activeScenarioLoop()), b -> {
this.scenarioLoopButton = this.addButtonTracked(new Button(x + 80, y + 42, 84, 20, this.boolLabel("Цикл", this.activeScenarioLoop()), b -> {
this.setActiveScenarioLoop(!this.activeScenarioLoop());
b.setMessage(this.boolLabel("Цикл очереди", this.activeScenarioLoop()));
b.setMessage(this.boolLabel("Цикл", this.activeScenarioLoop()));
}), this.scenarioButtons);
this.scenarioSoundField = this.createField(x, y + 70, 126, "");
this.scenarioUseCurrentBtn = this.addButtonTracked(new Button(x + 130, y + 69, 54, 20, new StringTextComponent("Текущ."), b -> {
this.scenarioShuffleButton = this.addButtonTracked(new Button(x + 166, y + 42, 84, 20, this.boolLabel("Случайно", this.activeScenarioShuffle()), b -> {
this.setActiveScenarioShuffle(!this.activeScenarioShuffle());
b.setMessage(this.boolLabel("Случайно", this.activeScenarioShuffle()));
}), this.scenarioButtons);
this.scenarioSoundField = this.createField(x, y + 70, 116, "");
this.scenarioUseCurrentBtn = this.addButtonTracked(new Button(x + 118, y + 69, 44, 20, new StringTextComponent("Тек."), b -> {
this.scenarioSoundField.setValue(this.soundIdField.getValue().trim());
}), this.scenarioButtons);
this.scenarioDelayField = this.createField(x + 192, y + 70, 58, "20");
this.scenarioDelayField = this.createField(x + 164, y + 70, 38, "20");
this.scenarioSetButton = this.addButtonTracked(new Button(x + 204, y + 69, 46, 20, new StringTextComponent("Задать"), b -> this.updateSelectedScenarioStep()), this.scenarioButtons);
this.scenarioAddButton = this.addButtonTracked(new Button(x, y + 96, 60, 20, new StringTextComponent("+ Шаг"), b -> this.addScenarioStep()), this.scenarioButtons);
this.scenarioRemoveButton = this.addButtonTracked(new Button(x + 64, y + 96, 60, 20, new StringTextComponent("- Шаг"), b -> this.removeScenarioStep()), this.scenarioButtons);
@@ -597,10 +619,12 @@ public class SoundPlayerScreen extends ContainerScreen<SoundPlayerContainer> {
payload.put("settings", (INBT) this.localSettings.toNbt());
ScenarioQueue sourceQ = new ScenarioQueue();
sourceQ.loop = this.sourceScenarioLoop;
sourceQ.shuffle = this.sourceScenarioShuffle;
sourceQ.steps.addAll(this.sourceScenarioSteps);
payload.put("scenario", (INBT) sourceQ.toNbt());
ScenarioQueue zoneQ = new ScenarioQueue();
zoneQ.loop = this.zoneScenarioLoop;
zoneQ.shuffle = this.zoneScenarioShuffle;
zoneQ.steps.addAll(this.zoneScenarioSteps);
payload.put("zoneScenario", (INBT) zoneQ.toNbt());
payload.putBoolean("sourceEnabled", this.sourceEnabled);
@@ -646,10 +670,12 @@ public class SoundPlayerScreen extends ContainerScreen<SoundPlayerContainer> {
this.sourceScenarioSteps.clear();
this.sourceScenarioSteps.addAll(sourceQ.steps);
this.sourceScenarioLoop = sourceQ.loop;
this.sourceScenarioShuffle = sourceQ.shuffle;
ScenarioQueue zoneQ = ScenarioQueue.fromNbt(payload.getCompound("zoneScenario"));
this.zoneScenarioSteps.clear();
this.zoneScenarioSteps.addAll(zoneQ.steps);
this.zoneScenarioLoop = zoneQ.loop;
this.zoneScenarioShuffle = zoneQ.shuffle;
this.sourceEnabled = payload.contains("sourceEnabled") && payload.getBoolean("sourceEnabled");
this.zoneEnabled = payload.contains("zoneEnabled") && payload.getBoolean("zoneEnabled");
this.wizardCompleted = payload.getBoolean("wizardCompleted");
@@ -769,6 +795,70 @@ public class SoundPlayerScreen extends ContainerScreen<SoundPlayerContainer> {
}
}
private boolean activeScenarioShuffle() {
return this.editingZoneScenario ? this.zoneScenarioShuffle : this.sourceScenarioShuffle;
}
private void setActiveScenarioShuffle(boolean shuffle) {
if (this.editingZoneScenario) {
this.zoneScenarioShuffle = shuffle;
} else {
this.sourceScenarioShuffle = shuffle;
}
}
private void updateSelectedScenarioStep() {
List<ScenarioQueue.Step> list = this.activeScenarioList();
if (this.selectedScenarioIndex >= 0 && this.selectedScenarioIndex < list.size()) {
String sid = this.scenarioSoundField.getValue().trim();
if (!sid.isEmpty()) {
int delay = this.clampInt(this.parseInt(this.scenarioDelayField.getValue(), 20), 0, 36000);
list.set(this.selectedScenarioIndex, new ScenarioQueue.Step(sid, delay));
this.saveAndSend(false, false);
}
}
}
public BlockPos getBlockPos() {
return this.menu != null ? this.menu.getPos() : null;
}
public ZoneShape getZoneShape() {
return this.zoneShape;
}
public float getZoneRadius() {
return this.zoneRadius;
}
public float getZoneBoxX() {
return this.zoneBoxX;
}
public float getZoneBoxY() {
return this.zoneBoxY;
}
public float getZoneBoxZ() {
return this.zoneBoxZ;
}
public boolean isZoneCustomCoords() {
return this.zoneUseCustomCoords;
}
public int getZoneSourceX() {
return this.zoneSourceX;
}
public int getZoneSourceY() {
return this.zoneSourceY;
}
public int getZoneSourceZ() {
return this.zoneSourceZ;
}
public boolean keyPressed(int keyCode, int scanCode, int modifiers) {
boolean enter = keyCode == 257 || keyCode == 335;
List<SoundManifestEntry> suggestions = this.filteredSoundEntries();
@@ -838,6 +928,9 @@ public class SoundPlayerScreen extends ContainerScreen<SoundPlayerContainer> {
int ry = listY + i * 14;
if (mouseX >= listX && mouseX <= (listX + 242) && mouseY >= ry && mouseY < (ry + 14)) {
this.selectedScenarioIndex = idx;
ScenarioQueue.Step step = list.get(idx);
this.scenarioSoundField.setValue(step.soundId);
this.scenarioDelayField.setValue(Integer.toString(step.delayTicks));
return true;
}
}
@@ -872,12 +965,24 @@ public class SoundPlayerScreen extends ContainerScreen<SoundPlayerContainer> {
return super.mouseScrolled(mouseX, mouseY, delta);
}
@Override
public void onClose() {
if (activeScreen == this) {
activeScreen = null;
}
AudioPlaybackService.get().stop("gui_preview");
this.saveAndSend(false, false);
super.onClose();
}
@Override
public void removed() {
if (activeScreen == this) {
activeScreen = null;
}
super.removed();
}
protected void renderBg(MatrixStack matrixStack, float partialTicks, int mouseX, int mouseY) {
int x1 = this.leftPos;
int y1 = this.topPos;
@@ -17,6 +17,7 @@ import net.minecraft.nbt.ListNBT;
public class ScenarioQueue {
public final List<Step> steps = new ArrayList<Step>();
public boolean loop;
public boolean shuffle;
public CompoundNBT toNbt() {
CompoundNBT tag = new CompoundNBT();
@@ -26,6 +27,7 @@ public class ScenarioQueue {
}
tag.put("steps", list);
tag.putBoolean("loop", this.loop);
tag.putBoolean("shuffle", this.shuffle);
return tag;
}
@@ -36,6 +38,7 @@ public class ScenarioQueue {
queue.steps.add(Step.fromNbt(list.getCompound(i)));
}
queue.loop = tag.getBoolean("loop");
queue.shuffle = tag.getBoolean("shuffle");
return queue;
}
@@ -20,14 +20,17 @@
*/
package com.example.soundmod.gameplay.block;
import com.example.soundmod.client.audio.AudioPlaybackService;
import com.example.soundmod.gameplay.tile.SoundPlayerTileEntity;
import com.example.soundmod.registry.ModTileEntities;
import java.util.Random;
import net.minecraft.block.AbstractBlock;
import net.minecraft.block.Block;
import net.minecraft.block.BlockState;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.entity.player.ServerPlayerEntity;
import net.minecraft.inventory.container.INamedContainerProvider;
import net.minecraft.particles.ParticleTypes;
import net.minecraft.tileentity.TileEntity;
import net.minecraft.tileentity.TileEntityType;
import net.minecraft.util.ActionResultType;
@@ -36,23 +39,27 @@ import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.BlockRayTraceResult;
import net.minecraft.world.IBlockReader;
import net.minecraft.world.World;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import net.minecraftforge.fml.network.NetworkHooks;
public class SoundPlayerBlock
extends Block {
public class SoundPlayerBlock extends Block {
public SoundPlayerBlock(AbstractBlock.Properties properties) {
super(properties);
}
@Override
public boolean hasTileEntity(BlockState state) {
return true;
}
@Override
public TileEntity createTileEntity(BlockState state, IBlockReader world) {
return ((TileEntityType)ModTileEntities.SOUND_PLAYER_TILE.get()).create();
}
public ActionResultType func_225533_a_(BlockState state, World world, BlockPos pos, PlayerEntity player, Hand hand, BlockRayTraceResult hit) {
@Override
public ActionResultType use(BlockState state, World world, BlockPos pos, PlayerEntity player, Hand hand, BlockRayTraceResult hit) {
TileEntity te = world.getBlockEntity(pos);
if (!world.isClientSide && te instanceof SoundPlayerTileEntity && player instanceof ServerPlayerEntity) {
SoundPlayerTileEntity soundTe = (SoundPlayerTileEntity)te;
@@ -62,14 +69,27 @@ extends Block {
NetworkHooks.openGui((ServerPlayerEntity)((ServerPlayerEntity)player), (INamedContainerProvider)soundTe, (BlockPos)pos);
}
}
return ActionResultType.sidedSuccess((boolean)world.isClientSide);
return ActionResultType.sidedSuccess(world.isClientSide);
}
public void func_220069_a(BlockState state, World world, BlockPos pos, Block block, BlockPos fromPos, boolean moving) {
@Override
public void neighborChanged(BlockState state, World world, BlockPos pos, Block block, BlockPos fromPos, boolean moving) {
TileEntity te = world.getBlockEntity(pos);
if (te instanceof SoundPlayerTileEntity) {
((SoundPlayerTileEntity)te).onRedstoneChanged(world.hasNeighborSignal(pos));
}
}
@Override
@OnlyIn(Dist.CLIENT)
public void animateTick(BlockState state, World world, BlockPos pos, Random rand) {
if (world.isClientSide && AudioPlaybackService.get().isPlaying("sound_player:" + pos.asLong())) {
double px = (double) pos.getX() + 0.5 + (rand.nextDouble() - 0.5) * 0.5;
double py = (double) pos.getY() + 1.05 + rand.nextDouble() * 0.25;
double pz = (double) pos.getZ() + 0.5 + (rand.nextDouble() - 0.5) * 0.5;
double noteColor = (double) rand.nextInt(25) / 24.0;
world.addParticle(ParticleTypes.NOTE, px, py, pz, noteColor, 0.0, 0.0);
}
}
}
@@ -509,6 +509,21 @@ ITickableTileEntity {
--this.scenarioWaitTicks;
return;
}
if (this.scenario.shuffle) {
int size = this.scenario.steps.size();
int nextIdx = 0;
if (size > 1) {
do {
nextIdx = this.level.random.nextInt(size);
} while (nextIdx == this.scenarioStepIndex && this.level.random.nextFloat() > 0.15f);
}
this.scenarioStepIndex = nextIdx;
ScenarioQueue.Step step = this.scenario.steps.get(nextIdx);
this.scenarioWaitTicks = Math.max(0, step.delayTicks);
this.emitScenarioStepPlay(step.soundId);
this.setChanged();
return;
}
if (this.scenarioStepIndex >= this.scenario.steps.size()) {
if (this.scenario.loop) {
this.scenarioStepIndex = 0;