Skip to content

Commit 66eb2ac

Browse files
committed
feat: reimplement enchanting table behaviour
1 parent 4736fa1 commit 66eb2ac

File tree

11 files changed

+346
-76
lines changed

11 files changed

+346
-76
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
package net.azisaba.vanilife.islands
22

3+
import net.azisaba.vanilife.islands.enchantment.EnchantingTableBehaviour
4+
import net.azisaba.vanilife.islands.listener.EnchantingTableListener
35
import net.azisaba.vanilife.islands.listener.IslandPlayerListener
46
import org.koin.core.Koin
57

68
internal fun Main.setupEventListeners(koin: Koin) {
9+
server.pluginManager.registerEvents(EnchantingTableListener(EnchantingTableBehaviour.Default), this)
710
server.pluginManager.registerEvents(IslandPlayerListener(koin.get(), koin.get()), this)
811
}

plugins/plugin-islands/src/main/kotlin/net/azisaba/vanilife/islands/IslandsFonts.kt

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,39 @@ import net.azisaba.packed.font.CharCodeFactory
66
import net.azisaba.packed.font.PackFont
77
import net.azisaba.packed.font.provider.PackBitmapFontProvider
88
import net.azisaba.vanilife.Vanilife
9+
import net.kyori.adventure.inventory.Book
910
import net.kyori.adventure.key.Key
1011

1112
object IslandsFonts {
1213
private const val WAVE_ASCENT: Int = 511
1314
private const val WAVE_HEIGHT: Int = 512
1415

16+
val ENCHANTS: PackedKey<PackFont> = PackedKey.font(Vanilife.NAMESPACE, "enchants")
1517
val WAVES: PackedKey<PackFont> = PackedKey.font(Vanilife.NAMESPACE, "waves")
1618

19+
fun enchants(): PackFont = PackFont(
20+
listOf(
21+
PackBitmapFontProvider(
22+
file = Key.key(Vanilife.NAMESPACE, "enchanting_table/enchanting_table.png"),
23+
chars = listOf(Enchants.ENCHANTING_TABLE.toString()),
24+
ascent = 8,
25+
height = 9,
26+
),
27+
PackBitmapFontProvider(
28+
file = Key.key("item/enchanted_book.png"),
29+
chars = listOf(Enchants.ENCHANTED_BOOK.toString()),
30+
ascent = 12,
31+
height = 16,
32+
),
33+
PackBitmapFontProvider(
34+
file = Key.key(Vanilife.NAMESPACE, "enchanting_table/book.png"),
35+
chars = listOf(Enchants.BOOK.toString()),
36+
ascent = 0,
37+
height = 52,
38+
),
39+
)
40+
)
41+
1742
fun waves(): PackFont = PackFont(
1843
listOf(
1944
PackBitmapFontProvider(
@@ -85,6 +110,12 @@ object IslandsFonts {
85110
)
86111
)
87112

113+
object Enchants : CharCodeFactory() {
114+
val ENCHANTING_TABLE: Char = nextChar()
115+
val ENCHANTED_BOOK: Char = nextChar()
116+
val BOOK: Char = nextChar()
117+
}
118+
88119
object Waves : CharCodeFactory() {
89120
val FRAME0: Char = nextChar()
90121
val FRAME1: Char = nextChar()
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package net.azisaba.vanilife.islands.enchantment
2+
3+
import io.github.retrooper.packetevents.util.SpigotConversionUtil
4+
import io.papermc.paper.registry.keys.SoundEventKeys
5+
import net.kyori.adventure.sound.Sound
6+
import org.bukkit.Material
7+
import org.bukkit.Particle
8+
import org.bukkit.block.Block
9+
import org.bukkit.entity.Player
10+
import org.bukkit.inventory.ItemStack
11+
import java.util.concurrent.ConcurrentHashMap
12+
13+
internal open class EnchantingTableBehaviour {
14+
private val instanceMap: MutableMap<Block, Instance> = ConcurrentHashMap()
15+
16+
fun use(player: Player, enchantingTable: Block, itemStack: ItemStack) {
17+
require(enchantingTable.type == Material.ENCHANTING_TABLE) { "Block must be an enchanting table" }
18+
19+
if (enchantingTable in instanceMap) return
20+
21+
val placedItemStack = itemStack.clone().apply { amount = 1 }
22+
if (!player.gameMode.isInvulnerable) {
23+
itemStack.subtract()
24+
}
25+
26+
val wrapperItemDisplay = WrapperEnchantingTableItemDisplay(enchantingTable, placedItemStack).apply {
27+
spawn(
28+
SpigotConversionUtil.fromBukkitLocation(
29+
enchantingTable.location.toCenterLocation().add(0.0, 0.95, 0.0)
30+
)
31+
)
32+
}
33+
val wrapperTextDisplay = WrapperEnchantingTableTextDisplay().apply {
34+
spawn(
35+
SpigotConversionUtil.fromBukkitLocation(enchantingTable.location.toCenterLocation())
36+
)
37+
}
38+
enchantingTable.chunk.playersSeeingChunk.map(Player::getUniqueId).forEach { viewer ->
39+
wrapperItemDisplay.addViewer(viewer)
40+
wrapperTextDisplay.addViewer(viewer)
41+
}
42+
43+
instanceMap[enchantingTable] = Instance(placedItemStack, wrapperItemDisplay, wrapperTextDisplay)
44+
45+
playPlacementEffects(enchantingTable)
46+
}
47+
48+
fun pickup(player: Player, enchantingTable: Block) {
49+
require(enchantingTable.type == Material.ENCHANTING_TABLE) { "Block must be an enchanting table" }
50+
51+
val instance = instanceMap.remove(enchantingTable) ?: return
52+
instance.dropItemStack(enchantingTable)
53+
instance.dispose()
54+
55+
player.playSound(Sound.sound(SoundEventKeys.ENTITY_ITEM_PICKUP, Sound.Source.PLAYER, 0.5f, 0.1f))
56+
}
57+
58+
fun tick(time: Long) {
59+
val iterator = instanceMap.entries.iterator()
60+
while (iterator.hasNext()) {
61+
val (enchantingTable, instance) = iterator.next()
62+
val nearestPlayer = findNearestPlayer(enchantingTable)
63+
if (nearestPlayer == null) {
64+
instance.dropItemStack(enchantingTable)
65+
instance.dispose()
66+
iterator.remove()
67+
continue
68+
}
69+
instance.tick(time, nearestPlayer)
70+
}
71+
}
72+
73+
fun hasItem(enchantingTable: Block): Boolean = enchantingTable in instanceMap
74+
75+
private fun playPlacementEffects(enchantingTable: Block) {
76+
enchantingTable.world.spawnParticle(
77+
Particle.CLOUD,
78+
enchantingTable.location.add(0.5, 0.5, 0.5),
79+
6, 0.12, 0.08, 0.12, 0.01,
80+
)
81+
enchantingTable.world.playSound(
82+
Sound.sound(SoundEventKeys.ENTITY_ITEM_FRAME_ADD_ITEM, Sound.Source.BLOCK, 0.7f, 0.85f),
83+
enchantingTable.x + 0.5, enchantingTable.y + 0.5, enchantingTable.z + 0.5,
84+
)
85+
}
86+
87+
private fun findNearestPlayer(enchantingTable: Block): Player? {
88+
val tableCenter = enchantingTable.location.toCenterLocation()
89+
return tableCenter.world.getNearbyPlayers(tableCenter, 3.0)
90+
.minByOrNull { player -> player.location.distanceSquared(tableCenter) }
91+
}
92+
93+
companion object Default : EnchantingTableBehaviour()
94+
95+
private data class Instance(
96+
val itemStack: ItemStack,
97+
val wrapperItemDisplay: WrapperEnchantingTableItemDisplay,
98+
val wrapperTextDisplay: WrapperEnchantingTableTextDisplay,
99+
) {
100+
fun tick(time: Long, nearestPlayer: Player) {
101+
wrapperItemDisplay.tick(time, nearestPlayer)
102+
}
103+
104+
fun dispose() {
105+
wrapperItemDisplay.remove()
106+
wrapperTextDisplay.remove()
107+
}
108+
109+
fun dropItemStack(enchantingTable: Block) {
110+
val dropLocation = enchantingTable.location.toCenterLocation().add(0.0, 1.0, 0.0)
111+
dropLocation.world.dropItemNaturally(dropLocation, itemStack.clone())
112+
}
113+
}
114+
}
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
package net.azisaba.vanilife.islands.enchantment
2+
3+
import com.github.retrooper.packetevents.protocol.entity.type.EntityTypes
4+
import com.github.retrooper.packetevents.protocol.world.Location
5+
import com.github.retrooper.packetevents.util.Quaternion4f
6+
import com.github.retrooper.packetevents.util.Vector3f
7+
import io.github.retrooper.packetevents.util.SpigotConversionUtil
8+
import me.tofaa.entitylib.container.EntityContainer
9+
import me.tofaa.entitylib.meta.display.AbstractDisplayMeta
10+
import me.tofaa.entitylib.meta.display.ItemDisplayMeta
11+
import me.tofaa.entitylib.wrapper.WrapperEntity
12+
import org.bukkit.Particle
13+
import org.bukkit.block.Block
14+
import org.bukkit.entity.Player
15+
import org.bukkit.inventory.ItemStack
16+
import kotlin.math.atan2
17+
import kotlin.math.cos
18+
import kotlin.math.sin
19+
import kotlin.math.sqrt
20+
21+
internal class WrapperEnchantingTableItemDisplay(
22+
private val enchantingTable: Block, private val itemStack: ItemStack,
23+
) : WrapperEntity(EntityTypes.ITEM_DISPLAY) {
24+
override fun spawn(location: Location, parent: EntityContainer): Boolean {
25+
if (!super.spawn(location, parent)) return false
26+
consumeEntityMeta(ItemDisplayMeta::class.java) { meta ->
27+
meta.item = SpigotConversionUtil.fromBukkitItemStack(itemStack)
28+
meta.displayType = ItemDisplayMeta.DisplayType.GROUND
29+
meta.billboardConstraints = AbstractDisplayMeta.BillboardConstraints.FIXED
30+
meta.interpolationDelay = 0
31+
meta.transformationInterpolationDuration = 0
32+
meta.isGlowing = true
33+
applyAnimation(meta, 0L, 0f)
34+
}
35+
refresh()
36+
return true
37+
}
38+
39+
fun tick(time: Long, player: Player) {
40+
val rotation = rotationFor(player)
41+
consumeEntityMeta(ItemDisplayMeta::class.java) { meta ->
42+
meta.transformationInterpolationDuration = 2
43+
applyAnimation(meta, time, rotation)
44+
}
45+
refresh()
46+
47+
if (time % 4L == 0L) {
48+
spawnEnchantParticles(rotation)
49+
}
50+
}
51+
52+
private fun applyAnimation(meta: ItemDisplayMeta, time: Long, rotation: Float) {
53+
meta.scale = animatedScale(time)
54+
meta.translation = translationFor(time, rotation)
55+
meta.leftRotation = yawRotation(-(rotation + (Math.PI / 2.0).toFloat()))
56+
meta.rightRotation = tiltRotation()
57+
meta.glowColorOverride = enchantAuraColor(time)
58+
}
59+
60+
private fun spawnEnchantParticles(rotation: Float) {
61+
val tableCenter = enchantingTable.location.toCenterLocation()
62+
val x = tableCenter.x + cos(rotation) * 0.18
63+
val z = tableCenter.z + sin(rotation) * 0.18
64+
tableCenter.world.spawnParticle(
65+
Particle.ENCHANT,
66+
x,
67+
tableCenter.y + 1.2,
68+
z,
69+
3,
70+
0.18,
71+
0.12,
72+
0.18,
73+
0.02,
74+
)
75+
}
76+
77+
private fun translationFor(time: Long, rotation: Float): Vector3f {
78+
val offsetX = cos(rotation) * 0.18
79+
val offsetZ = sin(rotation) * 0.18
80+
return Vector3f(offsetX.toFloat(), verticalOffset(time), offsetZ.toFloat())
81+
}
82+
83+
private fun animatedScale(time: Long): Vector3f {
84+
val scale = 0.85f + sin(time.toDouble() * 0.08).toFloat() * 0.04f
85+
return Vector3f(scale, scale, scale)
86+
}
87+
88+
private fun verticalOffset(time: Long): Float =
89+
-0.27f + sin(time.toDouble() * 0.1).toFloat() * 0.03f
90+
91+
private fun enchantAuraColor(time: Long): Int {
92+
val blend = ((sin(time.toDouble() * 0.08) + 1.0) * 0.5).toFloat()
93+
return lerpRgb(0x2B174F, 0x7A5BC2, blend)
94+
}
95+
96+
private fun tiltRotation(): Quaternion4f = axisAngle(1f, 0f, 0f, 45f)
97+
98+
private fun yawRotation(radians: Float): Quaternion4f {
99+
val half = radians / 2f
100+
return Quaternion4f(0f, sin(half), 0f, cos(half))
101+
}
102+
103+
private fun axisAngle(ax: Float, ay: Float, az: Float, degrees: Float): Quaternion4f {
104+
val len = sqrt((ax * ax + ay * ay + az * az).toDouble()).toFloat()
105+
val nx = ax / len
106+
val ny = ay / len
107+
val nz = az / len
108+
val half = Math.toRadians((degrees / 2f).toDouble())
109+
val s = sin(half).toFloat()
110+
val c = cos(half).toFloat()
111+
return Quaternion4f(nx * s, ny * s, nz * s, c)
112+
}
113+
114+
private fun lerpRgb(from: Int, to: Int, progress: Float): Int {
115+
val clamped = progress.coerceIn(0f, 1f)
116+
val fromR = (from shr 16) and 0xFF
117+
val fromG = (from shr 8) and 0xFF
118+
val fromB = from and 0xFF
119+
val toR = (to shr 16) and 0xFF
120+
val toG = (to shr 8) and 0xFF
121+
val toB = to and 0xFF
122+
123+
val r = (fromR + ((toR - fromR) * clamped)).toInt().coerceIn(0, 255)
124+
val g = (fromG + ((toG - fromG) * clamped)).toInt().coerceIn(0, 255)
125+
val b = (fromB + ((toB - fromB) * clamped)).toInt().coerceIn(0, 255)
126+
return (r shl 16) or (g shl 8) or b
127+
}
128+
129+
private fun rotationFor(player: Player): Float {
130+
val tableCenter = enchantingTable.location.toCenterLocation()
131+
return atan2(player.z - tableCenter.z, player.x - tableCenter.x).toFloat()
132+
}
133+
134+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package net.azisaba.vanilife.islands.enchantment
2+
3+
import com.github.retrooper.packetevents.protocol.entity.type.EntityTypes
4+
import com.github.retrooper.packetevents.protocol.world.Location
5+
import com.github.retrooper.packetevents.util.Vector3f
6+
import me.tofaa.entitylib.container.EntityContainer
7+
import me.tofaa.entitylib.meta.display.AbstractDisplayMeta
8+
import me.tofaa.entitylib.meta.display.TextDisplayMeta
9+
import me.tofaa.entitylib.wrapper.WrapperEntity
10+
import net.azisaba.vanilife.islands.IslandsFonts
11+
import net.kyori.adventure.text.Component
12+
13+
internal class WrapperEnchantingTableTextDisplay : WrapperEntity(EntityTypes.TEXT_DISPLAY) {
14+
override fun spawn(location: Location, parent: EntityContainer): Boolean {
15+
if (!super.spawn(location, parent)) return false
16+
consumeEntityMeta(TextDisplayMeta::class.java) { meta ->
17+
meta.text = Component.text()
18+
.append(Component.text(IslandsFonts.Enchants.ENCHANTING_TABLE).font(IslandsFonts.ENCHANTS))
19+
.appendSpace()
20+
.append(Component.text("Right Click to Open"))
21+
.build()
22+
meta.billboardConstraints = AbstractDisplayMeta.BillboardConstraints.CENTER
23+
meta.scale = Vector3f(0.75f, 0.75f, 0.75f)
24+
meta.translation = Vector3f(0f, 0.55f, 0f)
25+
}
26+
refresh()
27+
return true
28+
}
29+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package net.azisaba.vanilife.islands.listener
2+
3+
import com.destroystokyo.paper.event.server.ServerTickStartEvent
4+
import net.azisaba.vanilife.islands.enchantment.EnchantingTableBehaviour
5+
import org.bukkit.Material
6+
import org.bukkit.event.Event
7+
import org.bukkit.event.EventHandler
8+
import org.bukkit.event.Listener
9+
import org.bukkit.event.player.PlayerInteractEvent
10+
11+
internal class EnchantingTableListener(private val behaviour: EnchantingTableBehaviour) : Listener {
12+
@EventHandler
13+
fun onPlayerInteract(event: PlayerInteractEvent) {
14+
val enchantingTable = event.clickedBlock
15+
?.takeIf { it.type == Material.ENCHANTING_TABLE }
16+
?: return
17+
18+
event.isCancelled = true
19+
event.setUseInteractedBlock(Event.Result.DENY)
20+
event.setUseItemInHand(Event.Result.DENY)
21+
22+
if (event.action.isRightClick) {
23+
val itemStack = event.item ?: return
24+
behaviour.use(event.player, enchantingTable, itemStack)
25+
} else if (event.action.isRightClick) {
26+
behaviour.pickup(event.player, enchantingTable)
27+
}
28+
}
29+
30+
@EventHandler
31+
fun onServerTickStart(event: ServerTickStartEvent) {
32+
behaviour.tick(event.tickNumber.toLong())
33+
}
34+
}
2.17 KB
Loading
760 Bytes
Loading

plugins/plugin-npc/src/main/kotlin/net/azisaba/vanilife/npc/Bootstrap.kt

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,5 @@ class Bootstrap : PluginBootstrap {
1010
context.lifecycleManager.registerEventHandler(LifecycleEvents.COMMANDS.newHandler { event ->
1111
event.registrar().register(NpcCommand.create(), listOf("vanilife:npc"))
1212
})
13-
14-
context.lifecycleManager.registerEventHandler(
15-
RegistryEvents.SERVER_ITEM.compose().newHandler(NpcItems::bootstrap)
16-
)
1713
}
1814
}

0 commit comments

Comments
 (0)