diff --git a/src/main/java/ru/windcorp/progressia/client/graphics/model/Shapes.java b/src/main/java/ru/windcorp/progressia/client/graphics/model/Shapes.java index 92d9872..29d8629 100644 --- a/src/main/java/ru/windcorp/progressia/client/graphics/model/Shapes.java +++ b/src/main/java/ru/windcorp/progressia/client/graphics/model/Shapes.java @@ -259,6 +259,18 @@ public class Shapes { public PppBuilder setSize(float size) { return this.setSize(size, size, size); } + + public PppBuilder centerAt(float x, float y, float z) { + origin.set(x, y, z); + + origin.mul(2); + origin.sub(width); + origin.sub(height); + origin.sub(depth); + origin.div(2); + + return this; + } public PppBuilder flip() { this.flip = true; diff --git a/src/main/java/ru/windcorp/progressia/server/comms/ClientManager.java b/src/main/java/ru/windcorp/progressia/server/comms/ClientManager.java index 935e846..6b1595e 100644 --- a/src/main/java/ru/windcorp/progressia/server/comms/ClientManager.java +++ b/src/main/java/ru/windcorp/progressia/server/comms/ClientManager.java @@ -145,7 +145,7 @@ public class ClientManager { return; if (!(c instanceof ClientPlayer)) return; - if (!((ClientPlayer) c).isChunkVisible(entityId)) + if (!((ClientPlayer) c).isEntityVisible(entityId)) return; c.sendPacket(packet); }); diff --git a/src/main/java/ru/windcorp/progressia/server/comms/ClientPlayer.java b/src/main/java/ru/windcorp/progressia/server/comms/ClientPlayer.java index c05d952..7f4f26a 100644 --- a/src/main/java/ru/windcorp/progressia/server/comms/ClientPlayer.java +++ b/src/main/java/ru/windcorp/progressia/server/comms/ClientPlayer.java @@ -19,6 +19,9 @@ package ru.windcorp.progressia.server.comms; import glm.vec._3.i.Vec3i; +import gnu.trove.TCollections; +import gnu.trove.set.TLongSet; +import gnu.trove.set.hash.TLongHashSet; import ru.windcorp.progressia.common.world.generic.ChunkSet; import ru.windcorp.progressia.common.world.generic.ChunkSets; import ru.windcorp.progressia.server.Player; @@ -53,8 +56,19 @@ public abstract class ClientPlayer extends ClientChat { return player.getServer().getLoadManager().getVisionManager().getVisibleChunks(player); } - public boolean isChunkVisible(long entityId) { - return true; + public boolean isEntityVisible(long entityId) { + if (player == null) + return false; + return player.getServer().getLoadManager().getVisionManager().isEntityVisible(entityId, player); + } + + private static final TLongSet EMPTY_TLONGSET = TCollections.unmodifiableSet(new TLongHashSet(0)); + + public TLongSet getVisibleEntities(Player player) { + if (player == null) { + return EMPTY_TLONGSET; + } + return player.getServer().getLoadManager().getVisionManager().getVisibleEntities(player); } } diff --git a/src/main/java/ru/windcorp/progressia/server/management/load/ChunkManager.java b/src/main/java/ru/windcorp/progressia/server/management/load/ChunkManager.java index 3071530..9513aa2 100644 --- a/src/main/java/ru/windcorp/progressia/server/management/load/ChunkManager.java +++ b/src/main/java/ru/windcorp/progressia/server/management/load/ChunkManager.java @@ -17,10 +17,14 @@ */ package ru.windcorp.progressia.server.management.load; +import java.util.HashSet; +import java.util.Set; + import glm.vec._3.i.Vec3i; import ru.windcorp.progressia.common.world.DefaultChunkData; import ru.windcorp.progressia.common.world.PacketRevokeChunk; import ru.windcorp.progressia.common.world.PacketSendChunk; +import ru.windcorp.progressia.common.world.entity.EntityData; import ru.windcorp.progressia.common.world.DefaultWorldData; import ru.windcorp.progressia.server.Player; import ru.windcorp.progressia.server.Server; @@ -140,6 +144,11 @@ public class ChunkManager { return false; } + // TODO allow entities to be saved first + Set entitiesToRemove = new HashSet<>(); + world.forEachEntityInChunk(chunkPos, entitiesToRemove::add); + entitiesToRemove.forEach(world::removeEntity); + world.removeChunk(chunk); TestWorldDiskIO.saveChunk(chunk, getServer()); diff --git a/src/main/java/ru/windcorp/progressia/server/management/load/VisionManager.java b/src/main/java/ru/windcorp/progressia/server/management/load/VisionManager.java index b4d2a93..b166fd0 100644 --- a/src/main/java/ru/windcorp/progressia/server/management/load/VisionManager.java +++ b/src/main/java/ru/windcorp/progressia/server/management/load/VisionManager.java @@ -25,6 +25,9 @@ import java.util.function.Consumer; import com.google.common.eventbus.Subscribe; import glm.vec._3.i.Vec3i; +import gnu.trove.TCollections; +import gnu.trove.set.TLongSet; +import gnu.trove.set.hash.TLongHashSet; import ru.windcorp.progressia.common.world.generic.ChunkSet; import ru.windcorp.progressia.common.world.generic.ChunkSets; import ru.windcorp.progressia.server.Player; @@ -85,6 +88,28 @@ public class VisionManager { return vision.getVisibleChunks(); } + public boolean isEntityVisible(long entityId, Player player) { + PlayerVision vision = getVision(player, false); + + if (vision == null) { + return false; + } + + return vision.isEntityVisible(entityId); + } + + private static final TLongSet EMPTY_TLONGSET = TCollections.unmodifiableSet(new TLongHashSet(0)); + + public TLongSet getVisibleEntities(Player player) { + PlayerVision vision = getVision(player, false); + + if (vision == null) { + return EMPTY_TLONGSET; + } + + return vision.getVisibleEntities(); + } + /** * @return the loadManager */ diff --git a/src/main/java/ru/windcorp/progressia/server/world/DefaultWorldLogic.java b/src/main/java/ru/windcorp/progressia/server/world/DefaultWorldLogic.java index 3e654b6..bbaa4d3 100644 --- a/src/main/java/ru/windcorp/progressia/server/world/DefaultWorldLogic.java +++ b/src/main/java/ru/windcorp/progressia/server/world/DefaultWorldLogic.java @@ -15,12 +15,13 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - + package ru.windcorp.progressia.server.world; import java.util.Collection; import java.util.HashMap; import java.util.Map; +import java.util.Objects; import glm.Glm; import glm.vec._3.i.Vec3i; @@ -50,7 +51,7 @@ public class DefaultWorldLogic implements WorldLogic { public DefaultWorldLogic(DefaultWorldData data, Server server, WorldGenerator generator, WorldAccessor accessor) { this.data = data; this.server = server; - + this.generator = generator; data.setGravityModel(getGenerator().getGravityModel()); @@ -83,12 +84,33 @@ public class DefaultWorldLogic implements WorldLogic { public Collection getEntities() { return getData().getEntities(); } - + @Override public EntityData getEntity(long entityId) { return getData().getEntity(entityId); } + @Override + public void spawnEntity(EntityData entity) { + Objects.requireNonNull(entity, "entity"); + if (entity.getEntityId() != EntityData.NULL_ENTITY_ID) { + throw new IllegalArgumentException( + "Entity ID of entity " + entity + + " is not unassigned; use WorldData.addEntity to add entity with assigned entity ID" + ); + } + + long entityId; + + // TODO this should be synchronized on the entity set + do { + entityId = server.getAdHocRandom().nextLong(); + } while (entityId == EntityData.NULL_ENTITY_ID || getEntity(entityId) != null); + + entity.setEntityId(entityId); + getData().addEntity(entity); + } + public Evaluation getTickEntitiesTask() { return tickEntitiesTask; } @@ -108,36 +130,54 @@ public class DefaultWorldLogic implements WorldLogic { public DefaultChunkData generate(Vec3i chunkPos) { DefaultChunkData chunk = getGenerator().generate(chunkPos); - + if (!Glm.equals(chunkPos, chunk.getPosition())) { - throw CrashReports.report(null, "Generator %s has generated a chunk at (%d; %d; %d) when requested to generate a chunk at (%d; %d; %d)", + throw CrashReports.report( + null, + "Generator %s has generated a chunk at (%d; %d; %d) when requested to generate a chunk at (%d; %d; %d)", getGenerator(), - chunk.getX(), chunk.getY(), chunk.getZ(), - chunkPos.x, chunkPos.y, chunkPos.z + chunk.getX(), + chunk.getY(), + chunk.getZ(), + chunkPos.x, + chunkPos.y, + chunkPos.z ); } - + if (getData().getChunk(chunk.getPosition()) != chunk) { if (isChunkLoaded(chunkPos)) { - throw CrashReports.report(null, "Generator %s has returned a chunk different to the chunk that is located at (%d; %d; %d)", + throw CrashReports.report( + null, + "Generator %s has returned a chunk different to the chunk that is located at (%d; %d; %d)", getGenerator(), - chunkPos.x, chunkPos.y, chunkPos.z + chunkPos.x, + chunkPos.y, + chunkPos.z ); } else { - throw CrashReports.report(null, "Generator %s has returned a chunk that is not loaded when requested to generate a chunk at (%d; %d; %d)", + throw CrashReports.report( + null, + "Generator %s has returned a chunk that is not loaded when requested to generate a chunk at (%d; %d; %d)", getGenerator(), - chunkPos.x, chunkPos.y, chunkPos.z + chunkPos.x, + chunkPos.y, + chunkPos.z ); } } - + if (!getChunk(chunk).isReady()) { - throw CrashReports.report(null, "Generator %s has returned a chunk that is not ready when requested to generate a chunk at (%d; %d; %d)", + throw CrashReports.report( + null, + "Generator %s has returned a chunk that is not ready when requested to generate a chunk at (%d; %d; %d)", getGenerator(), - chunkPos.x, chunkPos.y, chunkPos.z + chunkPos.x, + chunkPos.y, + chunkPos.z ); } - + return chunk; } diff --git a/src/main/java/ru/windcorp/progressia/server/world/WorldLogic.java b/src/main/java/ru/windcorp/progressia/server/world/WorldLogic.java index f0cdb0a..4e94afe 100644 --- a/src/main/java/ru/windcorp/progressia/server/world/WorldLogic.java +++ b/src/main/java/ru/windcorp/progressia/server/world/WorldLogic.java @@ -21,9 +21,12 @@ import java.util.Collection; import glm.vec._3.i.Vec3i; import ru.windcorp.progressia.common.world.WorldData; +import ru.windcorp.progressia.common.world.entity.EntityData; import ru.windcorp.progressia.common.world.rels.BlockFace; public interface WorldLogic extends WorldLogicRO { + + void spawnEntity(EntityData entity); /* * Override return types diff --git a/src/main/java/ru/windcorp/progressia/server/world/tasks/AddEntity.java b/src/main/java/ru/windcorp/progressia/server/world/tasks/AddEntity.java new file mode 100644 index 0000000..7bbdaa6 --- /dev/null +++ b/src/main/java/ru/windcorp/progressia/server/world/tasks/AddEntity.java @@ -0,0 +1,75 @@ +/* + * Progressia + * Copyright (C) 2020-2021 Wind Corporation and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package ru.windcorp.progressia.server.world.tasks; + +import java.util.function.Consumer; + +import glm.vec._3.i.Vec3i; +import ru.windcorp.progressia.common.world.entity.EntityData; +import ru.windcorp.progressia.server.Server; + +class AddEntity extends CachedChange { + + private EntityData entity; + + public AddEntity(Consumer disposer) { + super(disposer); + } + + public void set(EntityData entity) { + if (this.entity != null) + throw new IllegalStateException("Entity is not null. Current: " + this.entity + "; requested: " + entity); + + this.entity = entity; + } + + @Override + public void affect(Server server) { + server.getWorld().spawnEntity(entity); + } + + @Override + public void getRelevantChunk(Vec3i output) { + // Do nothing + } + + @Override + public boolean isThreadSensitive() { + return false; + } + + @Override + public void dispose() { + super.dispose(); + this.entity = null; + } + + @Override + public int hashCode() { + return System.identityHashCode(entity); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof AddEntity)) + return false; + return ((AddEntity) obj).entity == entity; + } + +} diff --git a/src/main/java/ru/windcorp/progressia/server/world/tasks/RemoveEntity.java b/src/main/java/ru/windcorp/progressia/server/world/tasks/RemoveEntity.java new file mode 100644 index 0000000..11fd898 --- /dev/null +++ b/src/main/java/ru/windcorp/progressia/server/world/tasks/RemoveEntity.java @@ -0,0 +1,75 @@ +/* + * Progressia + * Copyright (C) 2020-2021 Wind Corporation and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package ru.windcorp.progressia.server.world.tasks; + +import java.util.function.Consumer; + +import glm.vec._3.i.Vec3i; +import ru.windcorp.progressia.common.world.entity.EntityData; +import ru.windcorp.progressia.server.Server; + +class RemoveEntity extends CachedChange { + + private long entityId = EntityData.NULL_ENTITY_ID; + + public RemoveEntity(Consumer disposer) { + super(disposer); + } + + public void set(long entityId) { + if (this.entityId != EntityData.NULL_ENTITY_ID) + throw new IllegalStateException("Entity ID is not null. Current: " + this.entityId + "; requested: " + entityId); + + this.entityId = entityId; + } + + @Override + public void affect(Server server) { + server.getWorld().getData().removeEntity(entityId); + } + + @Override + public void getRelevantChunk(Vec3i output) { + // Do nothing + } + + @Override + public boolean isThreadSensitive() { + return false; + } + + @Override + public void dispose() { + super.dispose(); + this.entityId = EntityData.NULL_ENTITY_ID; + } + + @Override + public int hashCode() { + return Long.hashCode(entityId); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof RemoveEntity)) + return false; + return ((RemoveEntity) obj).entityId == entityId; + } + +} diff --git a/src/main/java/ru/windcorp/progressia/server/world/tasks/WorldAccessor.java b/src/main/java/ru/windcorp/progressia/server/world/tasks/WorldAccessor.java index 76ab835..0a639c8 100644 --- a/src/main/java/ru/windcorp/progressia/server/world/tasks/WorldAccessor.java +++ b/src/main/java/ru/windcorp/progressia/server/world/tasks/WorldAccessor.java @@ -46,6 +46,9 @@ public class WorldAccessor implements ReportingServerContext.ChangeListener { .addClass(SetBlock.class, () -> new SetBlock(disposer)) .addClass(AddTile.class, () -> new AddTile(disposer)) .addClass(RemoveTile.class, () -> new RemoveTile(disposer)) + + .addClass(AddEntity.class, () -> new AddEntity(disposer)) + .addClass(RemoveEntity.class, () -> new RemoveEntity(disposer)) .addClass(ChangeEntity.class, () -> new ChangeEntity(disposer)) .addClass(BlockTriggeredUpdate.class, () -> new BlockTriggeredUpdate(disposer)) @@ -81,14 +84,16 @@ public class WorldAccessor implements ReportingServerContext.ChangeListener { @Override public void onEntityAdded(EntityData entity) { - // TODO Auto-generated method stub - + AddEntity change = cache.grab(AddEntity.class); + change.set(entity); + server.requestChange(change); } @Override public void onEntityRemoved(long entityId) { - // TODO Auto-generated method stub - + RemoveEntity change = cache.grab(RemoveEntity.class); + change.set(entityId); + server.requestChange(change); } @Override diff --git a/src/main/java/ru/windcorp/progressia/test/LayerTestGUI.java b/src/main/java/ru/windcorp/progressia/test/LayerTestGUI.java index 65913d2..0c082fa 100755 --- a/src/main/java/ru/windcorp/progressia/test/LayerTestGUI.java +++ b/src/main/java/ru/windcorp/progressia/test/LayerTestGUI.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - + package ru.windcorp.progressia.test; import glm.vec._3.Vec3; @@ -129,7 +129,7 @@ public class LayerTestGUI extends GUILayer { 128 ) ); - + group.addChild( new DynamicLabel( "ChunkStatsDisplay", @@ -266,12 +266,21 @@ public class LayerTestGUI extends GUILayer { } + private static final String[] CLOCK_CHARS = "\u2591\u2598\u259d\u2580\u2596\u258c\u259e\u259b\u2597\u259a\u2590\u259c\u2584\u2599\u259f\u2588" + .chars().mapToObj(c -> ((char) c) + "").toArray(String[]::new); + + private static String getTPSClockChar() { + return CLOCK_CHARS[(int) (ServerState.getInstance().getUptimeTicks() % CLOCK_CHARS.length)]; + } + private static final Averager FPS_RECORD = new Averager(); private static final Averager TPS_RECORD = new Averager(); private static final Supplier TPS_STRING = DynamicStrings.builder() .addDyn(new MutableStringLocalized("LayerTestGUI.TPSDisplay")) .addDyn(() -> TPS_RECORD.update(ServerState.getInstance().getTPS()), 5, 1) + .add(' ') + .addDyn(LayerTestGUI::getTPSClockChar) .buildSupplier(); private static final Supplier POS_STRING = DynamicStrings.builder() diff --git a/src/main/java/ru/windcorp/progressia/test/TestBlockLogicStatieSpawner.java b/src/main/java/ru/windcorp/progressia/test/TestBlockLogicStatieSpawner.java new file mode 100644 index 0000000..99335ae --- /dev/null +++ b/src/main/java/ru/windcorp/progressia/test/TestBlockLogicStatieSpawner.java @@ -0,0 +1,51 @@ +/* + * Progressia + * Copyright (C) 2020-2021 Wind Corporation and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package ru.windcorp.progressia.test; + +import glm.vec._3.Vec3; +import glm.vec._3.i.Vec3i; +import ru.windcorp.progressia.common.world.block.BlockDataRegistry; +import ru.windcorp.progressia.common.world.entity.EntityData; +import ru.windcorp.progressia.server.world.block.BlockLogic; +import ru.windcorp.progressia.server.world.block.TickableBlock; +import ru.windcorp.progressia.server.world.context.ServerBlockContext; +import ru.windcorp.progressia.server.world.context.ServerBlockContextRO; +import ru.windcorp.progressia.server.world.ticking.TickingPolicy; + +public class TestBlockLogicStatieSpawner extends BlockLogic implements TickableBlock { + + public TestBlockLogicStatieSpawner(String id) { + super(id); + } + + @Override + public void tick(ServerBlockContext context) { + Vec3i loc = context.toAbsolute(context.getLocation(), null); + EntityData entity = new TestEntityDataStatie(); + entity.setPosition(new Vec3(loc.x, loc.y, loc.z)); + + context.addEntity(entity); + context.setBlock(BlockDataRegistry.getInstance().get("Test:Air")); + } + + @Override + public TickingPolicy getTickingPolicy(ServerBlockContextRO context) { + return TickingPolicy.RANDOM; + } + +} diff --git a/src/main/java/ru/windcorp/progressia/test/TestContent.java b/src/main/java/ru/windcorp/progressia/test/TestContent.java index d927613..9ad6518 100644 --- a/src/main/java/ru/windcorp/progressia/test/TestContent.java +++ b/src/main/java/ru/windcorp/progressia/test/TestContent.java @@ -131,6 +131,10 @@ public class TestContent { register(new BlockRenderTransparentCube("Test:TemporaryLeaves", getBlockTexture("TemporaryLeaves"))); // Sic, using Glass logic for leaves because Test register(new TestBlockLogicGlass("Test:TemporaryLeaves")); + + register(new BlockData("Test:StatieSpawner")); + register(new BlockRenderOpaqueCube("Test:StatieSpawner", getBlockTexture("StatieSpawner"))); + register(new TestBlockLogicStatieSpawner("Test:StatieSpawner")); BlockDataRegistry.getInstance().values().forEach(PLACEABLE_BLOCKS::add); PLACEABLE_BLOCKS.removeIf(b -> placeableBlacklist.contains(b.getId())); diff --git a/src/main/java/ru/windcorp/progressia/test/TestEntityRenderStatie.java b/src/main/java/ru/windcorp/progressia/test/TestEntityRenderStatie.java index fa595e7..89f7ca9 100644 --- a/src/main/java/ru/windcorp/progressia/test/TestEntityRenderStatie.java +++ b/src/main/java/ru/windcorp/progressia/test/TestEntityRenderStatie.java @@ -15,29 +15,41 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - + package ru.windcorp.progressia.test; +import ru.windcorp.progressia.client.graphics.backend.GraphicsInterface; import ru.windcorp.progressia.client.graphics.model.Renderable; import ru.windcorp.progressia.client.graphics.model.ShapeRenderHelper; -import ru.windcorp.progressia.client.graphics.model.Shapes; -import ru.windcorp.progressia.client.graphics.texture.Texture; +import ru.windcorp.progressia.client.graphics.model.Shapes.PppBuilder; +import ru.windcorp.progressia.client.graphics.texture.ComplexTexture; +import ru.windcorp.progressia.client.graphics.texture.TexturePrimitive; import ru.windcorp.progressia.client.graphics.world.WorldRenderProgram; import ru.windcorp.progressia.client.world.entity.EntityRender; +import ru.windcorp.progressia.client.world.entity.EntityRenderRegistry; import ru.windcorp.progressia.client.world.entity.EntityRenderable; import ru.windcorp.progressia.common.world.entity.EntityData; public class TestEntityRenderStatie extends EntityRender { - private final Renderable cube = new Shapes.PppBuilder( - WorldRenderProgram.getDefault(), - (Texture) null - ) - .setColorMultiplier(1, 1, 0) - .create(); + private final static int PARTICLE_COUNT = 16; + private final Renderable core; + private final Renderable particle; public TestEntityRenderStatie(String id) { super(id); + + TexturePrimitive texturePrimitive = EntityRenderRegistry.getEntityTexture("Statie"); + ComplexTexture texture = new ComplexTexture(texturePrimitive, 4, 4); + WorldRenderProgram program = WorldRenderProgram.getDefault(); + + final float coreSize = 1f / 4; + final float particleSize = 1f / 16; + + core = new PppBuilder(program, texture.getCuboidTextures(0, 2, 1)).setSize(coreSize).centerAt(0, 0, 0).create(); + particle = new PppBuilder(program, texture.getCuboidTextures(0, 0, 1)).setSize(particleSize) + .centerAt(2.5f * coreSize, 0, 0).create(); + } @Override @@ -45,13 +57,29 @@ public class TestEntityRenderStatie extends EntityRender { return new EntityRenderable(entity) { @Override public void doRender(ShapeRenderHelper renderer) { + double phase = GraphicsInterface.getTime(); + renderer.pushTransform().translate(0, 0, (float) Math.sin(phase) * 0.1f); + renderer.pushTransform().scale( ((TestEntityDataStatie) entity).getSize() / 24.0f - ); + ).rotateY((float) -Math.sin(phase - Math.PI / 3) * Math.PI / 12); - cube.render(renderer); + core.render(renderer); renderer.popTransform(); + renderer.popTransform(); + renderer.pushTransform().translate(0, 0, (float) Math.sin(phase + Math.PI / 2) * 0.05f); + + for (int i = 0; i < PARTICLE_COUNT; ++i) { + double phaseOffset = 2 * Math.PI / PARTICLE_COUNT * i; + renderer.pushTransform() + .translate((float) Math.sin(phase + phaseOffset) * 0.1f, 0, 0) + .rotateX(Math.sin(phase / 2 + phaseOffset) * Math.PI / 6) + .rotateZ(phase + phaseOffset * 2); + particle.render(renderer); + renderer.popTransform(); + } + } }; } diff --git a/src/main/resources/assets/textures/blocks/StatieSpawner.png b/src/main/resources/assets/textures/blocks/StatieSpawner.png new file mode 100644 index 0000000..815e0ab Binary files /dev/null and b/src/main/resources/assets/textures/blocks/StatieSpawner.png differ diff --git a/src/main/resources/assets/textures/entities/Statie.png b/src/main/resources/assets/textures/entities/Statie.png new file mode 100644 index 0000000..ed8a351 Binary files /dev/null and b/src/main/resources/assets/textures/entities/Statie.png differ diff --git a/src/main/resources/assets/textures/tiles/Fern.png b/src/main/resources/assets/textures/tiles/Fern.png index 00e1ec1..bf5e48c 100644 Binary files a/src/main/resources/assets/textures/tiles/Fern.png and b/src/main/resources/assets/textures/tiles/Fern.png differ