feat: 回転と破壊・設置

This commit is contained in:
Keisuke Hirata 2025-12-12 05:21:52 +09:00
parent ad4245d764
commit af4008ee45
5 changed files with 311 additions and 132 deletions

View File

@ -64,6 +64,9 @@ class LandSectorPlugin : JavaPlugin() {
service.flushChanges()
}, 6000L, 6000L)
// Schedule rotation task
net.hareworks.hcu.landsector.task.SectorRotationTask(this).runTaskTimer(this, 1L, 1L)
logger.info("LandSector initialized with services.")
}

View File

@ -103,18 +103,17 @@ class LandSectorCommand(private val landSectorPlugin: LandSectorPlugin) {
val blockBase = world.getBlockAt(x, y - 1, z)
val blockTop = world.getBlockAt(x, y + 1, z)
if (blockBase.type == Material.BEDROCK) blockBase.type = Material.AIR
if (blockTop.type == Material.BEDROCK) blockTop.type = Material.AIR
if (blockBase.type == Material.DEEPSLATE_TILE_SLAB) blockBase.type = Material.AIR
if (blockTop.type == Material.DEEPSLATE_TILE_SLAB) blockTop.type = Material.AIR
// Remove Entity
// Remove All Entities with this sector ID
val center = Location(world, x + 0.5, y.toDouble(), z + 0.5)
if (center.chunk.isLoaded) {
val entities = world.getNearbyEntities(center, 0.5, 0.5, 0.5)
entities.forEach { entity ->
if (entity is Shulker) {
val key = NamespacedKey(landSectorPlugin, "sector_id")
val sId = entity.persistentDataContainer.get(key, PersistentDataType.INTEGER)
if (sId == id || sId == null) {
val sectorKey = NamespacedKey(landSectorPlugin, "sector_id")
center.chunk.entities.forEach { entity ->
if (entity.persistentDataContainer.has(sectorKey, PersistentDataType.INTEGER)) {
val sId = entity.persistentDataContainer.get(sectorKey, PersistentDataType.INTEGER)
if (sId == id) {
entity.remove()
}
}

View File

@ -14,6 +14,7 @@ import org.bukkit.entity.Shulker
import org.bukkit.attribute.Attribute
import org.bukkit.event.EventHandler
import org.bukkit.event.Listener
import org.bukkit.event.block.BlockBreakEvent
import org.bukkit.event.block.BlockPlaceEvent
import org.bukkit.event.entity.EntityDamageEvent
import org.bukkit.event.entity.EntityDamageByEntityEvent
@ -32,8 +33,31 @@ class SectorListener(
private val playerIdService: PlayerIdService
) : Listener {
@EventHandler
fun onBreak(event: BlockBreakEvent) {
val block = event.block
val loc = block.location
val player = event.player
if (sectorService.isSectorBlock(loc.world.name, loc.blockX, loc.blockY, loc.blockZ)) {
player.sendMessage(Component.text("You cannot break Sector Core blocks!", NamedTextColor.RED))
event.isCancelled = true
}
}
@EventHandler
fun onPlace(event: BlockPlaceEvent) {
val block = event.block
val loc = block.location
val player = event.player
// Global protection check
if (sectorService.isSectorArea(loc.world.name, loc.blockX, loc.blockY, loc.blockZ)) {
player.sendMessage(Component.text("This area is protected by a Sector Core.", NamedTextColor.RED))
event.isCancelled = true
return
}
val item = event.itemInHand
val meta = item.itemMeta ?: return
val key = NamespacedKey(plugin, "component")
@ -42,7 +66,6 @@ class SectorListener(
return
}
val player = event.player
val playerEntry = playerIdService.find(player.uniqueId)
if (playerEntry == null) {
player.sendMessage(Component.text("Identity not found.", NamedTextColor.RED))
@ -50,40 +73,47 @@ class SectorListener(
return
}
val block = event.blockPlaced
val loc = block.location
// ... (rest of the code)
// Space check
val above1 = block.getRelative(0, 1, 0)
val above2 = block.getRelative(0, 2, 0)
if (!above1.type.isAir || !above2.type.isAir) {
player.sendMessage(Component.text("Not enough space for Sector Core.", NamedTextColor.RED))
// Check 3x3x3 space availability (relative to placed block base)
val baseLoc = block.location
for (dx in -1..1) {
for (dy in 0..2) {
for (dz in -1..1) {
if (dx == 0 && dy == 0 && dz == 0) continue // Skip the placed block itself
val checkLoc = baseLoc.clone().add(dx.toDouble(), dy.toDouble(), dz.toDouble())
if (!checkLoc.block.type.isAir) {
player.sendMessage(Component.text("Not enough space! Need 3x3x3 free area.", NamedTextColor.RED))
event.isCancelled = true
return
}
}
}
}
// Define Center Location (This will be the DB coordinates)
val centerLoc = baseLoc.clone().add(0.0, 1.0, 0.0) // y+1 from base
// Record to DB first to get ID
val sector = sectorService.createSector(
playerEntry.actorId,
player.world.name,
above1.x,
above1.y,
above1.z
centerLoc.blockX,
centerLoc.blockY,
centerLoc.blockZ
)
if (sector == null) {
player.sendMessage(Component.text("Failed to create sector record.", NamedTextColor.RED))
player.sendMessage(Component.text("Failed to create sector.", NamedTextColor.RED))
event.isCancelled = true
return
}
// Create Visuals
val sectorKey = NamespacedKey(plugin, "sector_id")
val locCenter = loc.clone().add(0.5, 0.0, 0.5)
val visualCenter = centerLoc.clone().add(0.5, 0.0, 0.5) // Center of the block space
// 1. Command Block
val cbLoc = locCenter.clone().add(0.0, 1.5, 0.0)
// 1. Command Block (Center + 0.5y) -> Matches old base+1.5
val cbLoc = visualCenter.clone().add(0.0, 0.5, 0.0)
val cb = player.world.spawnEntity(cbLoc, EntityType.BLOCK_DISPLAY) as BlockDisplay
cb.block = Material.COMMAND_BLOCK.createBlockData {
(it as org.bukkit.block.data.Directional).facing = org.bukkit.block.BlockFace.UP
@ -96,7 +126,7 @@ class SectorListener(
)
cb.persistentDataContainer.set(sectorKey, PersistentDataType.INTEGER, sector.id)
// 2. Tinted Glass
// 2. Tinted Glass (Center + 0.5y) -> Matches old base+1.5
val glass = player.world.spawnEntity(cbLoc, EntityType.BLOCK_DISPLAY) as BlockDisplay
glass.block = Material.TINTED_GLASS.createBlockData()
glass.transformation = Transformation(
@ -107,8 +137,8 @@ class SectorListener(
)
glass.persistentDataContainer.set(sectorKey, PersistentDataType.INTEGER, sector.id)
// 3. Top Cauldron
val topCauldronLoc = locCenter.clone().add(0.0, 2.3125, 0.0)
// 3. Top Cauldron (Center + 1.3125y) -> Matches old base+2.3125
val topCauldronLoc = visualCenter.clone().add(0.0, 1.3125, 0.0)
val topCauldron = player.world.spawnEntity(topCauldronLoc, EntityType.BLOCK_DISPLAY) as BlockDisplay
topCauldron.block = Material.CAULDRON.createBlockData()
topCauldron.transformation = Transformation(
@ -119,8 +149,8 @@ class SectorListener(
)
topCauldron.persistentDataContainer.set(sectorKey, PersistentDataType.INTEGER, sector.id)
// 4. Bottom Cauldron
val botCauldronLoc = locCenter.clone().add(0.0, 0.6875, 0.0)
// 4. Bottom Cauldron (Center - 0.3125y) -> Matches old base+0.6875
val botCauldronLoc = visualCenter.clone().add(0.0, -0.3125, 0.0)
val botCauldron = player.world.spawnEntity(botCauldronLoc, EntityType.BLOCK_DISPLAY) as BlockDisplay
botCauldron.block = Material.CAULDRON.createBlockData()
botCauldron.transformation = Transformation(
@ -131,8 +161,8 @@ class SectorListener(
)
botCauldron.persistentDataContainer.set(sectorKey, PersistentDataType.INTEGER, sector.id)
// Spawn Shulker (Hitbox)
val shulkerLoc = loc.clone().add(0.5, 1.0, 0.5) // Y+1, sits in space 1-2.
// Spawn Shulker (Hitbox) at Center
val shulkerLoc = visualCenter.clone() // Center is exactly y+1 from base
val shulker = player.world.spawnEntity(shulkerLoc, EntityType.SHULKER) as Shulker
shulker.setAI(false)
shulker.isInvisible = true
@ -146,16 +176,20 @@ class SectorListener(
// Tag Shulker with Sector ID
shulker.persistentDataContainer.set(sectorKey, PersistentDataType.INTEGER, sector.id)
// Place Blocks
// Place Blocks (Base and Top)
// Base is at center.y - 1 (The placed block)
// Top is at center.y + 1
block.type = Material.DEEPSLATE_TILE_SLAB
val bottomSlab = block.blockData as Slab
bottomSlab.type = Slab.Type.BOTTOM
block.blockData = bottomSlab
above2.type = Material.DEEPSLATE_TILE_SLAB
val topSlab = above2.blockData as Slab
val topBlock = centerLoc.clone().add(0.0, 1.0, 0.0).block
topBlock.type = Material.DEEPSLATE_TILE_SLAB
val topSlab = topBlock.blockData as Slab
topSlab.type = Slab.Type.TOP
above2.blockData = topSlab
topBlock.blockData = topSlab
player.sendMessage(Component.text("Sector Core placed!", NamedTextColor.GREEN))
}
@ -175,28 +209,36 @@ class SectorListener(
val damage = event.finalDamage
val maxHealth = entity.getAttribute(Attribute.MAX_HEALTH)?.value ?: 1000.0
val newHp = sectorService.reduceHealth(sectorId, damage.toInt())
// Cancel the event so it doesn't actually die in vanilla terms,
// OR let it happen but force health reset.
// Cancelling prevents the red flash in some versions/cases, so setting damage to 0 is often better
// if we want the effect. But resetting health is robust.
// Let's reset health.
val oldHp = sectorService.getHealth(sectorId) ?: return // Get current health from service
val newHp = sectorService.reduceHealth(sectorId, damage.toInt())
entity.health = maxHealth
entity.noDamageTicks = 0
entity.noDamageTicks = 0 // Disable invulnerability
if (newHp != null) {
val loc = entity.location
val world = entity.world
// Effects
world.playSound(loc, Sound.BLOCK_ANVIL_PLACE, 1.0f, 0.5f) // Heavy metallic sound
// Sound Logic
val threshold = 100 // 10% of 1000
val crossedThreshold = (oldHp / threshold) > (newHp / threshold)
if (crossedThreshold) {
world.playSound(loc, Sound.BLOCK_ANVIL_PLACE, 1.0f, 0.5f)
} else {
world.playSound(loc, Sound.BLOCK_STONE_BREAK, 1.0f, 1.0f)
}
world.spawnParticle(Particle.BLOCK, loc.add(0.0, 0.5, 0.0), 20, 0.3, 0.3, 0.3, Material.BEDROCK.createBlockData())
// Action Bar Display
if (event is EntityDamageByEntityEvent && event.damager is Player) {
val player = event.damager as Player
val percent = newHp.toDouble() / maxHealth.toDouble()
val percent = newHp.toDouble() / maxHealth
val color = when {
percent > 0.5 -> NamedTextColor.GREEN
percent > 0.2 -> NamedTextColor.YELLOW
@ -229,10 +271,6 @@ class SectorListener(
}
// Remove blocks.
// We know Shulker is at x, y, z.
// Base is y-1, Top is y+1
val loc = entity.location
val world = entity.world
val x = loc.blockX
val y = loc.blockY
val z = loc.blockZ
@ -244,9 +282,6 @@ class SectorListener(
if (top.type == Material.DEEPSLATE_TILE_SLAB) top.type = Material.AIR
world.dropItemNaturally(loc, org.bukkit.inventory.ItemStack(Material.DEEPSLATE_TILE_SLAB, 2))
// Maybe delete from DB or mark as destroyed?
// For now, valid destruction.
}
}
}

View File

@ -18,10 +18,24 @@ class SectorService(private val database: Database) {
private val hpCache = ConcurrentHashMap<Int, Int>()
private val dirtySet = ConcurrentHashMap.newKeySet<Int>()
private val sectorsCache = ConcurrentHashMap<Int, Sector>()
fun init() {
transaction(database) {
SchemaUtils.createMissingTablesAndColumns(SectorsTable)
SectorsTable.selectAll().forEach {
val id = it[SectorsTable.id]
val sector = Sector(
id,
it[SectorsTable.ownerActorId],
it[SectorsTable.world],
it[SectorsTable.x],
it[SectorsTable.y],
it[SectorsTable.z],
it[SectorsTable.hp]
)
sectorsCache[id] = sector
}
}
}
@ -37,42 +51,41 @@ class SectorService(private val database: Database) {
}[SectorsTable.id]
hpCache[id] = 1000
Sector(id, ownerActorId, world, x, y, z, 1000)
val sector = Sector(id, ownerActorId, world, x, y, z, 1000)
sectorsCache[id] = sector
sector
}
}
fun getSectorAt(world: String, x: Int, y: Int, z: Int): Sector? {
return transaction(database) {
SectorsTable.selectAll()
.andWhere { SectorsTable.world eq world }
.andWhere { SectorsTable.x eq x }
.andWhere { SectorsTable.y eq y }
.andWhere { SectorsTable.z eq z }
.map {
Sector(
it[SectorsTable.id],
it[SectorsTable.ownerActorId],
it[SectorsTable.world],
it[SectorsTable.x],
it[SectorsTable.y],
it[SectorsTable.z],
it[SectorsTable.hp]
)
}.singleOrNull()
// Optimized to use cache
return sectorsCache.values.firstOrNull {
it.world == world && it.x == x && it.y == y && it.z == z
}
}
fun getHealth(id: Int): Int? {
// Try get from cache
var currentHp = hpCache[id]
// If not in cache, load from DB
if (currentHp == null) {
val sector = sectorsCache[id] ?: return null
currentHp = sector.hp
hpCache[id] = currentHp
}
return currentHp
}
fun reduceHealth(id: Int, amount: Int): Int? {
// Try get from cache
var currentHp = hpCache[id]
// If not in cache, load from DB
// If not in cache, try to init from sectorsCache
if (currentHp == null) {
val sector = transaction(database) {
SectorsTable.selectAll().andWhere { SectorsTable.id eq id }.singleOrNull()
} ?: return null // Not found
currentHp = sector[SectorsTable.hp]
val sector = sectorsCache[id] ?: return null
currentHp = sector.hp
hpCache[id] = currentHp
}
@ -122,11 +135,33 @@ class SectorService(private val database: Database) {
if (sector != null) {
hpCache.remove(id)
dirtySet.remove(id)
sectorsCache.remove(id)
}
return sector
}
fun isSectorBlock(world: String, x: Int, y: Int, z: Int): Boolean {
// Sector blocks are at center.y - 1 (Base) and center.y + 1 (Top)
// Center is at sector.y
return sectorsCache.values.any {
it.world == world &&
it.x == x &&
it.z == z &&
(it.y - 1 == y || it.y + 1 == y)
}
}
fun isSectorArea(world: String, x: Int, y: Int, z: Int): Boolean {
// Check 3x3x3 around center (sector.x, sector.y, sector.z)
return sectorsCache.values.any {
it.world == world &&
x >= it.x - 1 && x <= it.x + 1 &&
y >= it.y - 1 && y <= it.y + 1 &&
z >= it.z - 1 && z <= it.z + 1
}
}
fun exists(id: Int): Boolean {
if (hpCache.containsKey(id)) return true
@ -135,6 +170,25 @@ class SectorService(private val database: Database) {
}
}
fun getAllSectorsList(): List<Sector> {
return transaction(database) {
SectorsTable.selectAll().map {
val id = it[SectorsTable.id]
val cachedHp = hpCache[id]
val hp = cachedHp ?: it[SectorsTable.hp]
Sector(
id,
it[SectorsTable.ownerActorId],
it[SectorsTable.world],
it[SectorsTable.x],
it[SectorsTable.y],
it[SectorsTable.z],
hp
)
}
}
}
fun getAllSectors(world: String): List<Sector> {
return transaction(database) {
SectorsTable.selectAll()

View File

@ -0,0 +1,88 @@
package net.hareworks.hcu.landsector.task
import net.hareworks.hcu.landsector.LandSectorPlugin
import org.bukkit.Bukkit
import org.bukkit.Material
import org.bukkit.NamespacedKey
import org.bukkit.entity.BlockDisplay
import org.bukkit.entity.EntityType
import org.bukkit.persistence.PersistentDataType
import org.bukkit.scheduler.BukkitRunnable
import org.bukkit.util.Transformation
import org.joml.Quaternionf
import org.joml.Vector3f
import kotlin.math.PI
class SectorRotationTask(private val plugin: LandSectorPlugin) : BukkitRunnable() {
private val sectorKey = NamespacedKey(plugin, "sector_id")
private val scale = Vector3f(0.5f, 0.5f, 0.5f)
private val centerOffset = Vector3f(0.25f, 0.25f, 0.25f) // Scale 0.5 * Model Center (0.5, 0.5, 0.5)
override fun run() {
val sectors = plugin.sectorService?.getAllSectorsList() ?: return
// Bukkit.getLogger().info("Task running. Sectors count: ${sectors.size}")
sectors.forEach { sector ->
val world = Bukkit.getWorld(sector.world) ?: return@forEach
// Simple check if chunk is loaded to avoid loading chunks
val chunkX = sector.x shr 4
val chunkZ = sector.z shr 4
if (!world.isChunkLoaded(chunkX, chunkZ)) {
// Bukkit.getLogger().info("Chunk not loaded for sector ${sector.id}")
return@forEach
}
// Check entities at the core location
val centerLoc = org.bukkit.Location(world, sector.x + 0.5, sector.y + 0.5, sector.z + 0.5)
// Search radius expanded to 2.0 to ensure hit
val entities = world.getNearbyEntities(centerLoc, 2.0, 2.0, 2.0)
// Bukkit.getLogger().info("Checking sector ${sector.id} at $centerLoc. Found entities: ${entities.size}")
entities.forEach { entity ->
if (entity.type == EntityType.BLOCK_DISPLAY && entity.persistentDataContainer.has(sectorKey, PersistentDataType.INTEGER)) {
val bd = entity as BlockDisplay
// Check if it's the command block
if (bd.block.material == Material.COMMAND_BLOCK) {
// Bukkit.getLogger().info("Rotater found target: ${bd.uniqueId}")
updateRotation(bd)
}
}
}
}
}
private fun updateRotation(bd: BlockDisplay) {
// Continuous rotation based on time: 1 full rotation every 4 seconds
val periodMs = 4000L
val time = System.currentTimeMillis()
val phase = (time % periodMs).toDouble() / periodMs.toDouble()
val angle = (phase * 2 * Math.PI).toFloat() // 0 to 2PI
// Log for debug (once per 100 ticks approx to avoid spam? No, just once to verify)
// Bukkit.getLogger().info("Rotating BD: angle=$angle")
// Rotation around Y axis
val rot = Quaternionf().rotateY(angle)
val centerOffset = Vector3f(0.25f, 0.25f, 0.25f) // The center in Scaled space
// T = - (R * C)
val rotatedOffset = Vector3f(centerOffset).rotate(rot)
val newTranslation = Vector3f(rotatedOffset).negate()
val newTrans = Transformation(
newTranslation,
rot,
scale,
Quaternionf(0f, 0f, 0f, 1f)
)
// Interpolation settings
bd.interpolationDuration = 3 // Short duration to match high frequency update
bd.interpolationDelay = -1 // Start immediately
bd.transformation = newTrans
}
}