diff --git a/src/main/kotlin/com/coderjoe/atlas/Atlas.kt b/src/main/kotlin/com/coderjoe/atlas/Atlas.kt index 7e4efa3..56429cd 100644 --- a/src/main/kotlin/com/coderjoe/atlas/Atlas.kt +++ b/src/main/kotlin/com/coderjoe/atlas/Atlas.kt @@ -10,6 +10,10 @@ import com.coderjoe.atlas.power.PowerBlockDialog import com.coderjoe.atlas.power.PowerBlockFactory import com.coderjoe.atlas.power.PowerBlockPersistence import com.coderjoe.atlas.power.PowerBlockRegistry +import com.coderjoe.atlas.transport.TransportBlockDialog +import com.coderjoe.atlas.transport.TransportBlockFactory +import com.coderjoe.atlas.transport.TransportBlockPersistence +import com.coderjoe.atlas.transport.TransportBlockRegistry import org.bukkit.plugin.java.JavaPlugin import org.bukkit.scheduler.BukkitTask @@ -20,6 +24,8 @@ class Atlas : JavaPlugin() { private lateinit var powerBlockPersistence: PowerBlockPersistence private lateinit var fluidBlockRegistry: FluidBlockRegistry private lateinit var fluidBlockPersistence: FluidBlockPersistence + private lateinit var transportBlockRegistry: TransportBlockRegistry + private lateinit var transportBlockPersistence: TransportBlockPersistence private var autoSaveTask: BukkitTask? = null override fun onEnable() { @@ -42,9 +48,11 @@ class Atlas : JavaPlugin() { PowerBlockDialog.init(this) FluidBlockDialog.init(this) + TransportBlockDialog.init(this) initPowerSystem() initFluidSystem() + initTransportSystem() // Register unified listener val powerSystem = BlockSystem( @@ -67,8 +75,18 @@ class Atlas : JavaPlugin() { } ) + val transportSystem = BlockSystem( + name = "transport", + registry = transportBlockRegistry, + factory = TransportBlockFactory, + descriptors = transportDescriptors(), + showDialog = { player, block -> + TransportBlockDialog.showTransportDialog(player, block as com.coderjoe.atlas.transport.TransportBlock, transportBlockRegistry) + } + ) + server.pluginManager.registerEvents( - AtlasBlockListener(this, listOf(powerSystem, fluidSystem)), + AtlasBlockListener(this, listOf(powerSystem, fluidSystem, transportSystem)), this ) @@ -76,6 +94,7 @@ class Atlas : JavaPlugin() { autoSaveTask = server.scheduler.runTaskTimer(this, Runnable { powerBlockPersistence.save(powerBlockRegistry) fluidBlockPersistence.save(fluidBlockRegistry) + transportBlockPersistence.save(transportBlockRegistry) }, 6000L, 6000L) logger.atlasInfo("Atlas plugin enabled!") @@ -92,8 +111,13 @@ class Atlas : JavaPlugin() { fluidBlockPersistence.save(fluidBlockRegistry) } + if (::transportBlockPersistence.isInitialized && ::transportBlockRegistry.isInitialized) { + transportBlockPersistence.save(transportBlockRegistry) + } + PowerBlockDialog.cleanup() FluidBlockDialog.cleanup() + TransportBlockDialog.cleanup() if (::powerBlockRegistry.isInitialized) { powerBlockRegistry.stopAll() @@ -103,6 +127,10 @@ class Atlas : JavaPlugin() { fluidBlockRegistry.stopAll() } + if (::transportBlockRegistry.isInitialized) { + transportBlockRegistry.stopAll() + } + logger.atlasInfo("Atlas plugin has been disabled!") } @@ -124,6 +152,21 @@ class Atlas : JavaPlugin() { logger.atlasInfo("Fluid system initialized with ${FluidBlockFactory.getRegisteredBlockIds().size} block types") } + fun initTransportSystem() { + TransportBlockFactory.registerFromDescriptors(transportDescriptors().values) + transportBlockRegistry = TransportBlockRegistry(this) + transportBlockPersistence = TransportBlockPersistence(this) + transportBlockPersistence.load(transportBlockRegistry) + + logger.atlasInfo("Transport system initialized with ${TransportBlockFactory.getRegisteredBlockIds().size} block types") + } + + private fun transportDescriptors(): Map { + return listOf( + com.coderjoe.atlas.transport.block.ConveyorBelt.descriptor + ).associateBy { it.baseBlockId } + } + private fun powerDescriptors(): Map { return listOf( com.coderjoe.atlas.power.block.SmallSolarPanel.descriptor, diff --git a/src/main/kotlin/com/coderjoe/atlas/NexoIntegration.kt b/src/main/kotlin/com/coderjoe/atlas/NexoIntegration.kt index 751da9d..84bd986 100644 --- a/src/main/kotlin/com/coderjoe/atlas/NexoIntegration.kt +++ b/src/main/kotlin/com/coderjoe/atlas/NexoIntegration.kt @@ -12,6 +12,7 @@ class NexoIntegration(private val plugin: JavaPlugin) { fun initialize() { copyItemConfigurations() copyTextures() + copyModels() copyRecipes() plugin.logger.atlasInfo("Atlas Nexo integration initialized") } @@ -78,6 +79,28 @@ class NexoIntegration(private val plugin: JavaPlugin) { } } + private fun copyModels() { + val modelsFolder = File(nexoFolder, "pack/assets/atlas/models/block") + if (!modelsFolder.exists()) { + modelsFolder.mkdirs() + } + + val prefix = "nexo/pack/assets/atlas/models/block/" + val modelPaths = discoverResources(prefix, ".json") + + for (resourcePath in modelPaths) { + val fileName = resourcePath.substringAfterLast("/") + val modelFile = File(modelsFolder, fileName) + plugin.saveResource(resourcePath, true) + val sourceFile = File(plugin.dataFolder, resourcePath) + if (sourceFile.exists()) { + sourceFile.copyTo(modelFile, overwrite = true) + sourceFile.delete() + plugin.logger.atlasInfo("Copied ${fileName.removeSuffix(".json")} model to Nexo") + } + } + } + private fun copyRecipes() { val recipesFolder = File(nexoFolder, "recipes/shapeless") if (!recipesFolder.exists()) { diff --git a/src/main/kotlin/com/coderjoe/atlas/core/AtlasBlockListener.kt b/src/main/kotlin/com/coderjoe/atlas/core/AtlasBlockListener.kt index 9eabb5f..356e488 100644 --- a/src/main/kotlin/com/coderjoe/atlas/core/AtlasBlockListener.kt +++ b/src/main/kotlin/com/coderjoe/atlas/core/AtlasBlockListener.kt @@ -60,7 +60,7 @@ class AtlasBlockListener( createAndRegister(system, blockId, location, facing) } PlacementType.DIRECTIONAL -> { - val facing = getPlayerFacing(event) + val facing = getDirectionalFacing(event, descriptor) val variantId = descriptor.directionalVariants[facing] ?: return val location = event.block.location.clone() plugin.server.scheduler.runTask(plugin, Runnable { @@ -70,7 +70,7 @@ class AtlasBlockListener( }) } PlacementType.DIRECTIONAL_OPPOSITE -> { - val facing = getPlayerFacing(event).oppositeFace + val facing = getDirectionalFacing(event, descriptor, opposite = true) val variantId = descriptor.directionalVariants[facing] ?: blockId val location = event.block.location.clone() plugin.server.scheduler.runTask(plugin, Runnable { @@ -146,6 +146,15 @@ class AtlasBlockListener( } companion object { + fun getDirectionalFacing(event: BlockPlaceEvent, descriptor: BlockDescriptor, opposite: Boolean = false): BlockFace { + val raw = getPlayerFacing(event) + val facing = if (opposite) raw.oppositeFace else raw + if (descriptor.directionalVariants.containsKey(facing)) return facing + // Fall back to the player's horizontal look direction + val fallback = event.player.facing + return if (opposite) fallback.oppositeFace else fallback + } + fun getPlayerFacing(event: BlockPlaceEvent): BlockFace { val against = event.blockAgainst.location val placed = event.block.location diff --git a/src/main/kotlin/com/coderjoe/atlas/transport/TransportBlock.kt b/src/main/kotlin/com/coderjoe/atlas/transport/TransportBlock.kt new file mode 100644 index 0000000..0b9bfd8 --- /dev/null +++ b/src/main/kotlin/com/coderjoe/atlas/transport/TransportBlock.kt @@ -0,0 +1,20 @@ +package com.coderjoe.atlas.transport + +import com.coderjoe.atlas.core.AtlasBlock +import com.coderjoe.atlas.core.BlockRegistry +import org.bukkit.Location + +abstract class TransportBlock( + location: Location +) : AtlasBlock(location) { + + protected abstract fun transportUpdate() + + override fun blockUpdate() { + transportUpdate() + } + + override fun getRegistry(): BlockRegistry<*> { + return TransportBlockRegistry.instance ?: throw IllegalStateException("TransportBlockRegistry not initialized") + } +} diff --git a/src/main/kotlin/com/coderjoe/atlas/transport/TransportBlockDialog.kt b/src/main/kotlin/com/coderjoe/atlas/transport/TransportBlockDialog.kt new file mode 100644 index 0000000..0fa5d60 --- /dev/null +++ b/src/main/kotlin/com/coderjoe/atlas/transport/TransportBlockDialog.kt @@ -0,0 +1,78 @@ +package com.coderjoe.atlas.transport + +import com.coderjoe.atlas.core.AtlasBlockDialog +import com.coderjoe.atlas.core.BlockRegistry +import com.coderjoe.atlas.transport.block.ConveyorBelt +import io.papermc.paper.dialog.Dialog +import io.papermc.paper.registry.data.dialog.ActionButton +import io.papermc.paper.registry.data.dialog.DialogBase +import io.papermc.paper.registry.data.dialog.action.DialogAction +import io.papermc.paper.registry.data.dialog.action.DialogActionCallback +import io.papermc.paper.registry.data.dialog.body.DialogBody +import io.papermc.paper.registry.data.dialog.type.DialogType +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.event.ClickCallback +import net.kyori.adventure.text.format.NamedTextColor +import org.bukkit.entity.Player +import org.bukkit.plugin.java.JavaPlugin + +object TransportBlockDialog { + + fun init(plugin: JavaPlugin) { + AtlasBlockDialog.init(plugin) + } + + fun showTransportDialog(player: Player, block: TransportBlock, registry: BlockRegistry<*>) { + AtlasBlockDialog.showDialog(player, block, registry) { p, b, onClose -> + sendDialog(p, b as TransportBlock, onClose) + } + } + + fun cleanup() { + AtlasBlockDialog.cleanup() + } + + private fun sendDialog(player: Player, block: TransportBlock, onClose: (Player) -> Unit) { + val title = Component.text(getBlockDisplayName(block)) + val bodyText = getBlockDescription(block) + val body = DialogBody.plainMessage(bodyText) + + val closeAction = DialogAction.customClick( + DialogActionCallback { _, audience -> + val p = audience as? Player ?: return@DialogActionCallback + onClose(p) + }, + ClickCallback.Options.builder().build() + ) + + val closeButton = ActionButton.builder(Component.text("Close")) + .action(closeAction) + .build() + + val dialog = Dialog.create { factory -> + factory.empty() + .base( + DialogBase.builder(title) + .body(listOf(body)) + .canCloseWithEscape(false) + .afterAction(DialogBase.DialogAfterAction.CLOSE) + .build() + ) + .type(DialogType.notice(closeButton)) + } + + player.showDialog(dialog) + } + + private fun getBlockDisplayName(block: TransportBlock): String = when (block) { + is ConveyorBelt -> "Conveyor Belt (${block.facing.name.lowercase().replaceFirstChar { it.uppercase() }})" + else -> "Transport Block" + } + + private fun getBlockDescription(block: TransportBlock): Component = when (block) { + is ConveyorBelt -> Component.text("Moves items forward 1 block every second") + .color(NamedTextColor.GRAY) + else -> Component.text("Transport block") + .color(NamedTextColor.GRAY) + } +} diff --git a/src/main/kotlin/com/coderjoe/atlas/transport/TransportBlockFactory.kt b/src/main/kotlin/com/coderjoe/atlas/transport/TransportBlockFactory.kt new file mode 100644 index 0000000..ad47df5 --- /dev/null +++ b/src/main/kotlin/com/coderjoe/atlas/transport/TransportBlockFactory.kt @@ -0,0 +1,12 @@ +package com.coderjoe.atlas.transport + +import com.coderjoe.atlas.core.BlockFactory +import org.bukkit.Location +import org.bukkit.block.BlockFace + +object TransportBlockFactory : BlockFactory() { + + fun createTransportBlock(blockId: String, location: Location, facing: BlockFace = BlockFace.SELF): TransportBlock? { + return create(blockId, location, facing) + } +} diff --git a/src/main/kotlin/com/coderjoe/atlas/transport/TransportBlockPersistence.kt b/src/main/kotlin/com/coderjoe/atlas/transport/TransportBlockPersistence.kt new file mode 100644 index 0000000..06af196 --- /dev/null +++ b/src/main/kotlin/com/coderjoe/atlas/transport/TransportBlockPersistence.kt @@ -0,0 +1,18 @@ +package com.coderjoe.atlas.transport + +import com.coderjoe.atlas.core.BlockPersistence +import org.bukkit.plugin.java.JavaPlugin + +class TransportBlockPersistence(plugin: JavaPlugin) { + private val persistence = BlockPersistence( + plugin = plugin, + fileName = "transport_blocks.yml", + yamlKey = "transport_blocks", + factory = TransportBlockFactory, + serialize = { _, _ -> emptyMap() }, + restore = { _, _ -> } + ) + + fun save(registry: TransportBlockRegistry) = persistence.save(registry) + fun load(registry: TransportBlockRegistry) = persistence.load(registry) +} diff --git a/src/main/kotlin/com/coderjoe/atlas/transport/TransportBlockRegistry.kt b/src/main/kotlin/com/coderjoe/atlas/transport/TransportBlockRegistry.kt new file mode 100644 index 0000000..36bfbe0 --- /dev/null +++ b/src/main/kotlin/com/coderjoe/atlas/transport/TransportBlockRegistry.kt @@ -0,0 +1,30 @@ +package com.coderjoe.atlas.transport + +import com.coderjoe.atlas.core.BlockRegistry +import org.bukkit.Location +import org.bukkit.block.BlockFace +import org.bukkit.plugin.java.JavaPlugin + +class TransportBlockRegistry(plugin: JavaPlugin) : BlockRegistry(plugin) { + + companion object { + var instance: TransportBlockRegistry? = null + private set + + fun locationKey(location: Location): String = BlockRegistry.locationKey(location) + } + + init { + instance = this + } + + fun registerTransportBlock(block: TransportBlock, blockId: String) = register(block, blockId) + + fun unregisterTransportBlock(location: Location): TransportBlock? = unregister(location) + + fun getTransportBlock(location: Location): TransportBlock? = getBlock(location) + + fun getAdjacentTransportBlock(location: Location, face: BlockFace): TransportBlock? = getAdjacentBlock(location, face) + + fun getAllTransportBlocksWithIds(): List> = getAllBlocksWithIds() +} diff --git a/src/main/kotlin/com/coderjoe/atlas/transport/block/ConveyorBelt.kt b/src/main/kotlin/com/coderjoe/atlas/transport/block/ConveyorBelt.kt new file mode 100644 index 0000000..5f8fd08 --- /dev/null +++ b/src/main/kotlin/com/coderjoe/atlas/transport/block/ConveyorBelt.kt @@ -0,0 +1,61 @@ +package com.coderjoe.atlas.transport.block + +import com.coderjoe.atlas.core.BlockDescriptor +import com.coderjoe.atlas.core.PlacementType +import com.coderjoe.atlas.transport.TransportBlock +import org.bukkit.Location +import org.bukkit.block.BlockFace +import org.bukkit.entity.Item + +class ConveyorBelt(location: Location, override val facing: BlockFace) : TransportBlock(location) { + + companion object { + const val BLOCK_ID = "conveyor_belt" + private const val MOVE_DISTANCE = 1.0 + + val DIRECTIONAL_IDS = mapOf( + BlockFace.NORTH to "conveyor_belt_north", + BlockFace.SOUTH to "conveyor_belt_south", + BlockFace.EAST to "conveyor_belt_east", + BlockFace.WEST to "conveyor_belt_west" + ) + + val ID_TO_FACING = DIRECTIONAL_IDS.entries.associate { (face, id) -> id to face } + + fun facingFromBlockId(blockId: String): BlockFace? = ID_TO_FACING[blockId] + + val descriptor = BlockDescriptor( + baseBlockId = BLOCK_ID, + displayName = "Conveyor Belt", + description = "Moves items forward in the facing direction", + placementType = PlacementType.DIRECTIONAL, + directionalVariants = DIRECTIONAL_IDS, + allRegistrableIds = DIRECTIONAL_IDS.values.toList(), + constructor = { loc, face -> ConveyorBelt(loc, face) } + ) + } + + override val baseBlockId: String = BLOCK_ID + + override val updateIntervalTicks: Long = 20L + + override fun getVisualStateBlockId(): String = DIRECTIONAL_IDS[facing]!! + + override fun transportUpdate() { + val world = location.world ?: return + + // Belt surface is at y + 6/16 (0.375), scan items resting on top + val scanCenter = location.clone().add(0.5, 0.75, 0.5) + val nearbyItems = world.getNearbyEntities(scanCenter, 0.5, 0.75, 0.5) + .filterIsInstance() + + if (nearbyItems.isEmpty()) return + + val dx = facing.direction.x * MOVE_DISTANCE + val dz = facing.direction.z * MOVE_DISTANCE + + for (item in nearbyItems) { + item.teleportAsync(item.location.add(dx, 0.0, dz)) + } + } +} diff --git a/src/main/resources/nexo/items/atlas_blocks.yml b/src/main/resources/nexo/items/atlas_blocks.yml index a3365e9..a1f0759 100644 --- a/src/main/resources/nexo/items/atlas_blocks.yml +++ b/src/main/resources/nexo/items/atlas_blocks.yml @@ -2709,3 +2709,154 @@ lava_generator_active: loots: - nexo_item: lava_generator probability: 1.0 + +# ─── Conveyor Belt ──────────────────────────────────────────────── +conveyor_belt: + itemname: "Conveyor Belt" + material: paper + Pack: + generate_model: true + parent_model: atlas:block/conveyor_belt + textures: + north: atlas:block/conveyor_belt_front + south: atlas:block/conveyor_belt_back + east: atlas:block/conveyor_belt_side + west: atlas:block/conveyor_belt_side + up: atlas:block/conveyor_belt_top_north + down: atlas:block/conveyor_belt_bottom + Mechanics: + custom_block: + type: CHORUSBLOCK + custom_variation: 44 + hardness: 3 + block_sounds: + break_sound: block.metal.break + place_sound: block.metal.place + hit_sound: block.metal.hit + step_sound: block.metal.step + fall_sound: block.metal.fall + drop: + silktouch: false + loots: + - nexo_item: conveyor_belt + probability: 1.0 + +conveyor_belt_north: + itemname: "Conveyor Belt" + material: paper + Pack: + generate_model: true + parent_model: atlas:block/conveyor_belt + textures: + north: atlas:block/conveyor_belt_front + south: atlas:block/conveyor_belt_back + east: atlas:block/conveyor_belt_side + west: atlas:block/conveyor_belt_side + up: atlas:block/conveyor_belt_top_north + down: atlas:block/conveyor_belt_bottom + Mechanics: + custom_block: + type: CHORUSBLOCK + custom_variation: 45 + hardness: 3 + block_sounds: + break_sound: block.metal.break + place_sound: block.metal.place + hit_sound: block.metal.hit + step_sound: block.metal.step + fall_sound: block.metal.fall + drop: + silktouch: false + loots: + - nexo_item: conveyor_belt + probability: 1.0 + +conveyor_belt_south: + itemname: "Conveyor Belt" + material: paper + Pack: + generate_model: true + parent_model: atlas:block/conveyor_belt + textures: + north: atlas:block/conveyor_belt_back + south: atlas:block/conveyor_belt_front + east: atlas:block/conveyor_belt_side + west: atlas:block/conveyor_belt_side + up: atlas:block/conveyor_belt_top_south + down: atlas:block/conveyor_belt_bottom + Mechanics: + custom_block: + type: CHORUSBLOCK + custom_variation: 46 + hardness: 3 + block_sounds: + break_sound: block.metal.break + place_sound: block.metal.place + hit_sound: block.metal.hit + step_sound: block.metal.step + fall_sound: block.metal.fall + drop: + silktouch: false + loots: + - nexo_item: conveyor_belt + probability: 1.0 + +conveyor_belt_east: + itemname: "Conveyor Belt" + material: paper + Pack: + generate_model: true + parent_model: atlas:block/conveyor_belt + textures: + north: atlas:block/conveyor_belt_side + south: atlas:block/conveyor_belt_side + east: atlas:block/conveyor_belt_front + west: atlas:block/conveyor_belt_back + up: atlas:block/conveyor_belt_top_east + down: atlas:block/conveyor_belt_bottom + Mechanics: + custom_block: + type: CHORUSBLOCK + custom_variation: 47 + hardness: 3 + block_sounds: + break_sound: block.metal.break + place_sound: block.metal.place + hit_sound: block.metal.hit + step_sound: block.metal.step + fall_sound: block.metal.fall + drop: + silktouch: false + loots: + - nexo_item: conveyor_belt + probability: 1.0 + +conveyor_belt_west: + itemname: "Conveyor Belt" + material: paper + Pack: + generate_model: true + parent_model: atlas:block/conveyor_belt + textures: + north: atlas:block/conveyor_belt_side + south: atlas:block/conveyor_belt_side + east: atlas:block/conveyor_belt_back + west: atlas:block/conveyor_belt_front + up: atlas:block/conveyor_belt_top_west + down: atlas:block/conveyor_belt_bottom + Mechanics: + custom_block: + type: CHORUSBLOCK + custom_variation: 48 + hardness: 3 + block_sounds: + break_sound: block.metal.break + place_sound: block.metal.place + hit_sound: block.metal.hit + step_sound: block.metal.step + fall_sound: block.metal.fall + drop: + silktouch: false + loots: + - nexo_item: conveyor_belt + probability: 1.0 diff --git a/src/main/resources/nexo/pack/assets/atlas/models/block/conveyor_belt.json b/src/main/resources/nexo/pack/assets/atlas/models/block/conveyor_belt.json new file mode 100644 index 0000000..ea82db7 --- /dev/null +++ b/src/main/resources/nexo/pack/assets/atlas/models/block/conveyor_belt.json @@ -0,0 +1,19 @@ +{ + "textures": { + "particle": "#up" + }, + "elements": [ + { + "from": [0, 0, 0], + "to": [16, 6, 16], + "faces": { + "north": { "texture": "#north", "uv": [0, 10, 16, 16] }, + "south": { "texture": "#south", "uv": [0, 10, 16, 16] }, + "east": { "texture": "#east", "uv": [0, 10, 16, 16] }, + "west": { "texture": "#west", "uv": [0, 10, 16, 16] }, + "up": { "texture": "#up" }, + "down": { "texture": "#down", "cullface": "down" } + } + } + ] +} diff --git a/src/main/resources/nexo/pack/assets/atlas/textures/block/conveyor_belt_back.png b/src/main/resources/nexo/pack/assets/atlas/textures/block/conveyor_belt_back.png new file mode 100644 index 0000000..3afc14f Binary files /dev/null and b/src/main/resources/nexo/pack/assets/atlas/textures/block/conveyor_belt_back.png differ diff --git a/src/main/resources/nexo/pack/assets/atlas/textures/block/conveyor_belt_bottom.png b/src/main/resources/nexo/pack/assets/atlas/textures/block/conveyor_belt_bottom.png new file mode 100644 index 0000000..5d67cb3 Binary files /dev/null and b/src/main/resources/nexo/pack/assets/atlas/textures/block/conveyor_belt_bottom.png differ diff --git a/src/main/resources/nexo/pack/assets/atlas/textures/block/conveyor_belt_front.png b/src/main/resources/nexo/pack/assets/atlas/textures/block/conveyor_belt_front.png new file mode 100644 index 0000000..01037cc Binary files /dev/null and b/src/main/resources/nexo/pack/assets/atlas/textures/block/conveyor_belt_front.png differ diff --git a/src/main/resources/nexo/pack/assets/atlas/textures/block/conveyor_belt_side.png b/src/main/resources/nexo/pack/assets/atlas/textures/block/conveyor_belt_side.png new file mode 100644 index 0000000..c445ab9 Binary files /dev/null and b/src/main/resources/nexo/pack/assets/atlas/textures/block/conveyor_belt_side.png differ diff --git a/src/main/resources/nexo/pack/assets/atlas/textures/block/conveyor_belt_top_east.png b/src/main/resources/nexo/pack/assets/atlas/textures/block/conveyor_belt_top_east.png new file mode 100644 index 0000000..38a528c Binary files /dev/null and b/src/main/resources/nexo/pack/assets/atlas/textures/block/conveyor_belt_top_east.png differ diff --git a/src/main/resources/nexo/pack/assets/atlas/textures/block/conveyor_belt_top_north.png b/src/main/resources/nexo/pack/assets/atlas/textures/block/conveyor_belt_top_north.png new file mode 100644 index 0000000..57df28d Binary files /dev/null and b/src/main/resources/nexo/pack/assets/atlas/textures/block/conveyor_belt_top_north.png differ diff --git a/src/main/resources/nexo/pack/assets/atlas/textures/block/conveyor_belt_top_south.png b/src/main/resources/nexo/pack/assets/atlas/textures/block/conveyor_belt_top_south.png new file mode 100644 index 0000000..b319caa Binary files /dev/null and b/src/main/resources/nexo/pack/assets/atlas/textures/block/conveyor_belt_top_south.png differ diff --git a/src/main/resources/nexo/pack/assets/atlas/textures/block/conveyor_belt_top_west.png b/src/main/resources/nexo/pack/assets/atlas/textures/block/conveyor_belt_top_west.png new file mode 100644 index 0000000..4b9ca78 Binary files /dev/null and b/src/main/resources/nexo/pack/assets/atlas/textures/block/conveyor_belt_top_west.png differ diff --git a/src/main/resources/nexo/recipes/shapeless/atlas_recipes.yml b/src/main/resources/nexo/recipes/shapeless/atlas_recipes.yml index 703928b..587ff75 100644 --- a/src/main/resources/nexo/recipes/shapeless/atlas_recipes.yml +++ b/src/main/resources/nexo/recipes/shapeless/atlas_recipes.yml @@ -107,3 +107,15 @@ lava_generator_recipe: C: amount: 1 minecraft_type: MAGMA_BLOCK + +conveyor_belt_recipe: + result: + nexo_item: conveyor_belt + amount: 4 + ingredients: + A: + amount: 3 + minecraft_type: IRON_INGOT + B: + amount: 3 + minecraft_type: REDSTONE diff --git a/src/test/kotlin/com/coderjoe/atlas/AtlasPluginTest.kt b/src/test/kotlin/com/coderjoe/atlas/AtlasPluginTest.kt index 52f6155..058ae3e 100644 --- a/src/test/kotlin/com/coderjoe/atlas/AtlasPluginTest.kt +++ b/src/test/kotlin/com/coderjoe/atlas/AtlasPluginTest.kt @@ -6,6 +6,9 @@ import com.coderjoe.atlas.fluid.FluidBlockRegistry import com.coderjoe.atlas.power.PowerBlockDialog import com.coderjoe.atlas.power.PowerBlockFactory import com.coderjoe.atlas.power.PowerBlockRegistry +import com.coderjoe.atlas.transport.TransportBlockDialog +import com.coderjoe.atlas.transport.TransportBlockFactory +import com.coderjoe.atlas.transport.TransportBlockRegistry import org.junit.jupiter.api.* import org.junit.jupiter.api.Assertions.* @@ -16,6 +19,7 @@ class AtlasPluginTest { TestHelper.setup() PowerBlockDialog.init(TestHelper.mockPlugin) FluidBlockDialog.init(TestHelper.mockPlugin) + TransportBlockDialog.init(TestHelper.mockPlugin) } @AfterEach @@ -35,6 +39,12 @@ class AtlasPluginTest { assertEquals(63, FluidBlockFactory.getRegisteredBlockIds().size) } + @Test + fun `transport system initializes with 4 block types`() { + TestHelper.initTransportFactory() + assertEquals(4, TransportBlockFactory.getRegisteredBlockIds().size) + } + @Test fun `power registry is set after creation`() { val registry = PowerBlockRegistry(TestHelper.mockPlugin) @@ -49,11 +59,19 @@ class AtlasPluginTest { assertSame(registry, FluidBlockRegistry.instance) } + @Test + fun `transport registry is set after creation`() { + val registry = TransportBlockRegistry(TestHelper.mockPlugin) + assertNotNull(TransportBlockRegistry.instance) + assertSame(registry, TransportBlockRegistry.instance) + } + @Test fun `dialog cleanup does not throw`() { assertDoesNotThrow { PowerBlockDialog.cleanup() FluidBlockDialog.cleanup() + TransportBlockDialog.cleanup() } } @@ -73,6 +91,14 @@ class AtlasPluginTest { assertEquals(0, registry.getAllFluidBlocksWithIds().size) } + @Test + fun `stopAll clears transport blocks`() { + TestHelper.initTransportFactory() + val registry = TransportBlockRegistry(TestHelper.mockPlugin) + registry.stopAll() + assertEquals(0, registry.getAllTransportBlocksWithIds().size) + } + @Test fun `auto-save interval is 6000 ticks`() { // The Atlas plugin schedules auto-save at 6000L ticks diff --git a/src/test/kotlin/com/coderjoe/atlas/TestHelper.kt b/src/test/kotlin/com/coderjoe/atlas/TestHelper.kt index 16413dc..68f2e96 100644 --- a/src/test/kotlin/com/coderjoe/atlas/TestHelper.kt +++ b/src/test/kotlin/com/coderjoe/atlas/TestHelper.kt @@ -11,6 +11,10 @@ import com.coderjoe.atlas.fluid.block.FluidPump import com.coderjoe.atlas.power.PowerBlock import com.coderjoe.atlas.power.PowerBlockFactory import com.coderjoe.atlas.power.PowerBlockRegistry +import com.coderjoe.atlas.transport.TransportBlock +import com.coderjoe.atlas.transport.TransportBlockFactory +import com.coderjoe.atlas.transport.TransportBlockRegistry +import com.coderjoe.atlas.transport.block.ConveyorBelt import com.coderjoe.atlas.power.block.LavaGenerator import com.coderjoe.atlas.power.block.PowerCable import com.coderjoe.atlas.power.block.SmallBattery @@ -88,6 +92,12 @@ object TestHelper { method.invoke(this) } + fun TransportBlock.callTransportUpdate() { + val method = TransportBlock::class.java.getDeclaredMethod("transportUpdate") + method.isAccessible = true + method.invoke(this) + } + fun addToRegistry(registry: PowerBlockRegistry, block: PowerBlock, blockId: String) { val blocksField = BlockRegistry::class.java.getDeclaredField("blocks") blocksField.isAccessible = true @@ -104,6 +114,22 @@ object TestHelper { blockIds[key] = blockId } + fun addToRegistry(registry: TransportBlockRegistry, block: TransportBlock, blockId: String) { + val blocksField = BlockRegistry::class.java.getDeclaredField("blocks") + blocksField.isAccessible = true + @Suppress("UNCHECKED_CAST") + val blocks = blocksField.get(registry) as java.util.concurrent.ConcurrentHashMap + + val blockIdsField = BlockRegistry::class.java.getDeclaredField("blockIds") + blockIdsField.isAccessible = true + @Suppress("UNCHECKED_CAST") + val blockIds = blockIdsField.get(registry) as java.util.concurrent.ConcurrentHashMap + + val key = TransportBlockRegistry.locationKey(block.location) + blocks[key] = block + blockIds[key] = blockId + } + fun addToRegistry(registry: FluidBlockRegistry, block: FluidBlock, blockId: String) { val blocksField = BlockRegistry::class.java.getDeclaredField("blocks") blocksField.isAccessible = true @@ -132,6 +158,12 @@ object TestHelper { instanceField.isAccessible = true instanceField.set(FluidBlockRegistry.Companion, null) } catch (_: Exception) {} + + try { + val instanceField = TransportBlockRegistry.Companion::class.java.getDeclaredField("instance") + instanceField.isAccessible = true + instanceField.set(TransportBlockRegistry.Companion, null) + } catch (_: Exception) {} } fun initPowerFactory() { @@ -149,8 +181,15 @@ object TestHelper { )) } + fun initTransportFactory() { + TransportBlockFactory.registerFromDescriptors(listOf( + ConveyorBelt.descriptor + )) + } + private fun clearFactories() { PowerBlockFactory.clear() FluidBlockFactory.clear() + TransportBlockFactory.clear() } } diff --git a/src/test/kotlin/com/coderjoe/atlas/transport/ConveyorBeltTest.kt b/src/test/kotlin/com/coderjoe/atlas/transport/ConveyorBeltTest.kt new file mode 100644 index 0000000..9890063 --- /dev/null +++ b/src/test/kotlin/com/coderjoe/atlas/transport/ConveyorBeltTest.kt @@ -0,0 +1,186 @@ +package com.coderjoe.atlas.transport + +import com.coderjoe.atlas.TestHelper +import com.coderjoe.atlas.TestHelper.callTransportUpdate +import com.coderjoe.atlas.transport.block.ConveyorBelt +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.bukkit.Location +import org.bukkit.World +import org.bukkit.block.BlockFace +import org.bukkit.entity.Item +import org.bukkit.util.BoundingBox +import org.junit.jupiter.api.* +import org.junit.jupiter.api.Assertions.* +import java.util.concurrent.CompletableFuture + +class ConveyorBeltTest { + + @BeforeEach + fun setup() { + TestHelper.setup() + } + + @AfterEach + fun teardown() { + TestHelper.teardown() + } + + @Test + fun `conveyor belt has correct facing`() { + val belt = ConveyorBelt(TestHelper.createLocation(), BlockFace.NORTH) + assertEquals(BlockFace.NORTH, belt.facing) + } + + @Test + fun `conveyor belt visual state matches facing`() { + for ((face, id) in ConveyorBelt.DIRECTIONAL_IDS) { + val belt = ConveyorBelt(TestHelper.createLocation(), face) + assertEquals(id, belt.getVisualStateBlockId()) + } + } + + @Test + fun `conveyor belt base block ID is conveyor_belt`() { + val belt = ConveyorBelt(TestHelper.createLocation(), BlockFace.SOUTH) + assertEquals("conveyor_belt", belt.baseBlockId) + } + + @Test + fun `conveyor belt descriptor has correct properties`() { + val desc = ConveyorBelt.descriptor + assertEquals("conveyor_belt", desc.baseBlockId) + assertEquals("Conveyor Belt", desc.displayName) + assertEquals(4, desc.allRegistrableIds.size) + assertTrue(desc.allRegistrableIds.contains("conveyor_belt_north")) + assertTrue(desc.allRegistrableIds.contains("conveyor_belt_south")) + assertTrue(desc.allRegistrableIds.contains("conveyor_belt_east")) + assertTrue(desc.allRegistrableIds.contains("conveyor_belt_west")) + } + + @Test + fun `conveyor belt descriptor has directional placement`() { + val desc = ConveyorBelt.descriptor + assertEquals(com.coderjoe.atlas.core.PlacementType.DIRECTIONAL, desc.placementType) + assertEquals(4, desc.directionalVariants.size) + } + + @Test + fun `facingFromBlockId returns correct facing`() { + assertEquals(BlockFace.NORTH, ConveyorBelt.facingFromBlockId("conveyor_belt_north")) + assertEquals(BlockFace.SOUTH, ConveyorBelt.facingFromBlockId("conveyor_belt_south")) + assertEquals(BlockFace.EAST, ConveyorBelt.facingFromBlockId("conveyor_belt_east")) + assertEquals(BlockFace.WEST, ConveyorBelt.facingFromBlockId("conveyor_belt_west")) + } + + @Test + fun `facingFromBlockId returns null for unknown ID`() { + assertNull(ConveyorBelt.facingFromBlockId("conveyor_belt_up")) + assertNull(ConveyorBelt.facingFromBlockId("unknown")) + } + + @Test + fun `all directional IDs are registered`() { + TestHelper.initTransportFactory() + for (id in ConveyorBelt.DIRECTIONAL_IDS.values) { + assertTrue(TransportBlockFactory.isRegistered(id), "Missing conveyor belt ID: $id") + } + } + + @Test + fun `factory creates ConveyorBelt from directional ID`() { + TestHelper.initTransportFactory() + val block = TransportBlockFactory.createTransportBlock("conveyor_belt_north", TestHelper.createLocation(), BlockFace.NORTH) + assertTrue(block is ConveyorBelt) + assertEquals(BlockFace.NORTH, block!!.facing) + } + + @Test + fun `transport update does not throw with no nearby entities`() { + TransportBlockRegistry(TestHelper.mockPlugin) + val belt = ConveyorBelt(TestHelper.createLocation(), BlockFace.NORTH) + + every { TestHelper.mockWorld.getNearbyEntities(any(), any(), any(), any()) } returns emptyList() + + assertDoesNotThrow { + belt.callTransportUpdate() + } + } + + @Test + fun `transport update moves item north`() { + TransportBlockRegistry(TestHelper.mockPlugin) + val belt = ConveyorBelt(TestHelper.createLocation(0.0, 64.0, 0.0), BlockFace.NORTH) + + val itemLoc = Location(TestHelper.mockWorld, 0.5, 64.375, 0.5) + val mockItem = mockk(relaxed = true) + every { mockItem.location } returns itemLoc + every { mockItem.teleportAsync(any()) } returns CompletableFuture.completedFuture(true) + + every { TestHelper.mockWorld.getNearbyEntities(any(), any(), any(), any()) } returns listOf(mockItem) + + belt.callTransportUpdate() + + verify { mockItem.teleportAsync(match { loc -> + loc.z < 0.5 && loc.x == 0.5 + }) } + } + + @Test + fun `transport update moves item east`() { + TransportBlockRegistry(TestHelper.mockPlugin) + val belt = ConveyorBelt(TestHelper.createLocation(0.0, 64.0, 0.0), BlockFace.EAST) + + val itemLoc = Location(TestHelper.mockWorld, 0.5, 64.375, 0.5) + val mockItem = mockk(relaxed = true) + every { mockItem.location } returns itemLoc + every { mockItem.teleportAsync(any()) } returns CompletableFuture.completedFuture(true) + + every { TestHelper.mockWorld.getNearbyEntities(any(), any(), any(), any()) } returns listOf(mockItem) + + belt.callTransportUpdate() + + verify { mockItem.teleportAsync(match { loc -> + loc.x > 0.5 && loc.z == 0.5 + }) } + } + + @Test + fun `transport update moves multiple items`() { + TransportBlockRegistry(TestHelper.mockPlugin) + val belt = ConveyorBelt(TestHelper.createLocation(0.0, 64.0, 0.0), BlockFace.SOUTH) + + val item1 = mockk(relaxed = true) + val item2 = mockk(relaxed = true) + every { item1.location } returns Location(TestHelper.mockWorld, 0.3, 64.4, 0.3) + every { item2.location } returns Location(TestHelper.mockWorld, 0.7, 64.4, 0.7) + every { item1.teleportAsync(any()) } returns CompletableFuture.completedFuture(true) + every { item2.teleportAsync(any()) } returns CompletableFuture.completedFuture(true) + + every { TestHelper.mockWorld.getNearbyEntities(any(), any(), any(), any()) } returns listOf(item1, item2) + + belt.callTransportUpdate() + + verify { item1.teleportAsync(any()) } + verify { item2.teleportAsync(any()) } + } + + @Test + fun `transport update ignores non-item entities`() { + TransportBlockRegistry(TestHelper.mockPlugin) + val belt = ConveyorBelt(TestHelper.createLocation(), BlockFace.NORTH) + + val mockPlayer = mockk(relaxed = true) + every { TestHelper.mockWorld.getNearbyEntities(any(), any(), any(), any()) } returns listOf(mockPlayer) + + assertDoesNotThrow { + belt.callTransportUpdate() + } + } + + @Test + fun `descriptor description mentions direction`() { + assertTrue(ConveyorBelt.descriptor.description.contains("forward")) + } +}