Files

385 lines
14 KiB
Go
Raw Permalink Normal View History

2026-07-09 08:33:57 +08:00
package block
import (
"math"
"math/rand/v2"
"slices"
"time"
"github.com/df-mc/dragonfly/server/block/cube"
"github.com/df-mc/dragonfly/server/item"
"github.com/df-mc/dragonfly/server/item/enchantment"
"github.com/df-mc/dragonfly/server/world"
"github.com/df-mc/dragonfly/server/world/particle"
)
// Breakable represents a block that may be broken by a player in survival mode. Blocks not include are blocks
// such as bedrock.
type Breakable interface {
// BreakInfo returns information of the block related to the breaking of it. Callers that execute the BreakHandler
// must call BreakInfo on the concrete block value being broken, as handlers may need block state after the world
// position has been cleared.
BreakInfo() BreakInfo
}
// BreakDuration returns the base duration that breaking the block passed takes when being broken using the
// item passed.
func BreakDuration(b world.Block, i item.Stack) time.Duration {
breakable, ok := b.(Breakable)
if !ok {
return math.MaxInt64
}
t, ok := i.Item().(item.Tool)
if !ok {
t = item.ToolNone{}
}
info := breakable.BreakInfo()
breakTime := info.Hardness * 5
if info.Harvestable(t) {
breakTime = info.Hardness * 1.5
}
if info.Effective(t) {
eff := t.BaseMiningEfficiency(b)
if e, ok := i.Enchantment(enchantment.Efficiency); ok {
eff += enchantment.Efficiency.Addend(e.Level())
}
breakTime /= eff
}
// TODO: Account for haste etc here.
timeInTicksAccurate := math.Round(breakTime/0.05) * 0.05
return (time.Duration(math.Round(timeInTicksAccurate*20)) * time.Second) / 20
}
// BreaksInstantly checks if the block passed can be broken instantly using the item stack passed to break
// it.
func BreaksInstantly(b world.Block, i item.Stack) bool {
breakable, ok := b.(Breakable)
if !ok {
return false
}
hardness := breakable.BreakInfo().Hardness
if hardness == 0 {
return true
}
t, ok := i.Item().(item.Tool)
if !ok || !breakable.BreakInfo().Effective(t) {
return false
}
// TODO: Account for haste etc here.
efficiencyVal := 0.0
if e, ok := i.Enchantment(enchantment.Efficiency); ok {
efficiencyVal += enchantment.Efficiency.Addend(e.Level())
}
hasteVal := 0.0
return (t.BaseMiningEfficiency(b)+efficiencyVal)*hasteVal >= hardness*30
}
// BreakInfo is a struct returned by every block. It holds information on block breaking related data, such as
// the tool type and tier required to break it.
type BreakInfo struct {
// Hardness is the hardness of the block, which influences the speed with which the block may be mined.
Hardness float64
// Harvestable is a function called to check if the block is harvestable using the tool passed. If the
// item used to break the block is not a tool, a tool.ToolNone is passed.
Harvestable func(t item.Tool) bool
// Effective is a function called to check if the block can be mined more effectively with the tool passed
// than with an empty hand.
Effective func(t item.Tool) bool
// Drops is a function called to get the drops of the block if it is broken using the item passed.
Drops func(t item.Tool, enchantments []item.Enchantment) []item.Stack
// BreakHandler is called after the block has broken.
BreakHandler func(pos cube.Pos, w *world.Tx, u item.User)
// XPDrops is the range of XP a block can drop when broken.
XPDrops XPDropRange
// BlastResistance is the blast resistance of the block, which influences the block's ability to withstand an
// explosive blast.
BlastResistance float64
}
// newBreakInfo creates a BreakInfo struct with the properties passed. The XPDrops field is 0 by default. The blast
// resistance is set to the block's hardness*5 by default.
func newBreakInfo(hardness float64, harvestable func(item.Tool) bool, effective func(item.Tool) bool, drops func(item.Tool, []item.Enchantment) []item.Stack) BreakInfo {
return BreakInfo{
Hardness: hardness,
BlastResistance: hardness * 5,
Harvestable: harvestable,
Effective: effective,
Drops: drops,
}
}
// withXPDropRange sets the XPDropRange field of the BreakInfo struct to the passed value.
func (b BreakInfo) withXPDropRange(min, max int) BreakInfo {
b.XPDrops = XPDropRange{min, max}
return b
}
// withBlastResistance sets the BlastResistance field of the BreakInfo struct to the passed value.
func (b BreakInfo) withBlastResistance(res float64) BreakInfo {
b.BlastResistance = res
return b
}
// withBreakHandler sets the BreakHandler field of the BreakInfo struct to the passed value.
func (b BreakInfo) withBreakHandler(handler func(pos cube.Pos, w *world.Tx, u item.User)) BreakInfo {
b.BreakHandler = handler
return b
}
// XPDropRange holds the min & max XP drop amounts of blocks.
type XPDropRange [2]int
// RandomValue returns a random XP value that falls within the drop range.
func (r XPDropRange) RandomValue() int {
diff := r[1] - r[0]
// Add one because it's a [r[0], r[1]] interval.
return rand.IntN(diff+1) + r[0]
}
// pickaxeEffective is a convenience function for blocks that are effectively mined with a pickaxe.
var pickaxeEffective = func(t item.Tool) bool {
return t.ToolType() == item.TypePickaxe
}
// axeEffective is a convenience function for blocks that are effectively mined with an axe.
var axeEffective = func(t item.Tool) bool {
return t.ToolType() == item.TypeAxe
}
// shearsEffective is a convenience function for blocks that are effectively mined with shears.
var shearsEffective = func(t item.Tool) bool {
return t.ToolType() == item.TypeShears
}
// swordEffective is a convenience function for blocks that are effectively mined with a sword.
var swordEffective = func(t item.Tool) bool {
return t.ToolType() == item.TypeSword
}
// shovelEffective is a convenience function for blocks that are effectively mined with a shovel.
var shovelEffective = func(t item.Tool) bool {
return t.ToolType() == item.TypeShovel
}
// hoeEffective is a convenience function for blocks that are effectively mined with a hoe.
var hoeEffective = func(t item.Tool) bool {
return t.ToolType() == item.TypeHoe
}
// nothingEffective is a convenience function for blocks that cannot be mined efficiently with any tool.
var nothingEffective = func(item.Tool) bool {
return false
}
// alwaysHarvestable is a convenience function for blocks that are harvestable using any item.
var alwaysHarvestable = func(t item.Tool) bool {
return true
}
// neverHarvestable is a convenience function for blocks that are not harvestable by any item.
var neverHarvestable = func(t item.Tool) bool {
return false
}
// pickaxeHarvestable is a convenience function for blocks that are harvestable using any kind of pickaxe.
var pickaxeHarvestable = pickaxeEffective
// simpleDrops returns a drops function that returns the items passed.
func simpleDrops(s ...item.Stack) func(item.Tool, []item.Enchantment) []item.Stack {
return func(item.Tool, []item.Enchantment) []item.Stack {
return s
}
}
// oneOf returns a drops function that returns one of each of the item types passed.
func oneOf(i ...world.Item) func(item.Tool, []item.Enchantment) []item.Stack {
return func(item.Tool, []item.Enchantment) []item.Stack {
var s []item.Stack
for _, it := range i {
s = append(s, item.NewStack(it, 1))
}
return s
}
}
// hasSilkTouch checks if an item has the silk touch enchantment.
func hasSilkTouch(enchantments []item.Enchantment) bool {
return slices.IndexFunc(enchantments, func(i item.Enchantment) bool {
return i.Type() == enchantment.SilkTouch
}) != -1
}
// silkTouchOneOf returns a drop function that returns 1x of the silk touch drop when silk touch exists, or 1x of the
// normal drop when it does not.
func silkTouchOneOf(normal, silkTouch world.Item) func(item.Tool, []item.Enchantment) []item.Stack {
return func(t item.Tool, enchantments []item.Enchantment) []item.Stack {
if hasSilkTouch(enchantments) {
return []item.Stack{item.NewStack(silkTouch, 1)}
}
return []item.Stack{item.NewStack(normal, 1)}
}
}
// silkTouchDrop returns a drop function that returns the silk touch drop when silk touch exists, or the
// normal drop when it does not.
func silkTouchDrop(normal, silkTouch item.Stack) func(item.Tool, []item.Enchantment) []item.Stack {
return func(t item.Tool, enchantments []item.Enchantment) []item.Stack {
if hasSilkTouch(enchantments) {
return []item.Stack{silkTouch}
}
return []item.Stack{normal}
}
}
// silkTouchOnlyDrop returns a drop function that returns the drop when silk touch exists.
func silkTouchOnlyDrop(it world.Item) func(t item.Tool, enchantments []item.Enchantment) []item.Stack {
return func(t item.Tool, enchantments []item.Enchantment) []item.Stack {
if hasSilkTouch(enchantments) {
return []item.Stack{item.NewStack(it, 1)}
}
return nil
}
}
// fortuneLevel returns the level of the fortune enchantment in enchantments, or 0 if it isn't present.
func fortuneLevel(enchantments []item.Enchantment) int {
index := slices.IndexFunc(enchantments, func(i item.Enchantment) bool {
return i.Type() == enchantment.Fortune
})
if index == -1 {
return 0
}
return enchantments[index].Level()
}
// fortuneOreCount computes the drop count for an ore after applying the Fortune ore multiplier to a given base
// drop count. The Fortune enchantment has a 2/(level + 2) chance of applying an integer bonus multiplier between
// 2x up to (level + 1)x to the drop count.
func fortuneOreCount(base int, enchantments []item.Enchantment) int {
fortune := fortuneLevel(enchantments)
if fortune == 0 || rand.IntN(fortune+2) < 2 {
return base
}
multiplier := rand.IntN(fortune) + 2
return base * multiplier
}
// fortuneDiscreteCount computes the drop count for a block with a discrete uniform distribution. A drop count is
// chosen with equal likelihood between min and max. Every level of Fortune will increase the max by one. The final
// drop count is then limited by the cap count.
func fortuneDiscreteCount(minCount, maxCount, capCount int, enchantments []item.Enchantment) int {
fortune := fortuneLevel(enchantments)
maxWithFortune := maxCount + fortune
return min(capCount, rand.IntN(maxWithFortune-minCount+1)+minCount)
}
// fortuneBinomial computes the binomial distribution B(n=attempts, p=8/15) for crop seed drops.
func fortuneBinomial(attempts int) int {
count := 0
for range attempts {
if rand.IntN(15) < 8 {
count++
}
}
return count
}
// oreDrops returns a drop function for ores that drop a single item, such as diamond. Silk touch tools will
// cause the ore block itself to always drop. Otherwise, a single item is dropped. The Fortune enchantment has a
// 2/(level + 2) chance of applying an integer bonus multiplier between 2x up to (level + 1)x to the drop count.
func oreDrops(drop, block world.Item) func(item.Tool, []item.Enchantment) []item.Stack {
return func(t item.Tool, enchantments []item.Enchantment) []item.Stack {
if hasSilkTouch(enchantments) {
return []item.Stack{item.NewStack(block, 1)}
}
return []item.Stack{item.NewStack(drop, fortuneOreCount(1, enchantments))}
}
}
// multiOreDrops returns a drop function for ores that drop multiple items, such as copper. Silk touch tools will
// cause the ore block itself to always drop. Otherwise, a drop count is chosen with equal likelihood between min
// and max. The Fortune enchantment has a 2/(level + 2) chance of applying an integer bonus multiplier between 2x
// up to (level + 1)x to the drop count.
func multiOreDrops(drop, block world.Item, minCount, maxCount int) func(item.Tool, []item.Enchantment) []item.Stack {
return func(t item.Tool, enchantments []item.Enchantment) []item.Stack {
if hasSilkTouch(enchantments) {
return []item.Stack{item.NewStack(block, 1)}
}
baseCount := rand.IntN(maxCount-minCount+1) + minCount
return []item.Stack{item.NewStack(drop, fortuneOreCount(baseCount, enchantments))}
}
}
// discreteDrops returns a drop function for blocks with discrete uniform random drops, such as glowstone or melon
// blocks. Silk touch tools will cause the block itself to always drop. Otherwise, a drop count is chosen with equal
// likelihood between min and max. Every level of Fortune will increase the max by one. The final drop count is then
// limited by the cap count.
func discreteDrops(drop, block world.Item, minCount, maxCount, capCount int) func(item.Tool, []item.Enchantment) []item.Stack {
return func(t item.Tool, enchantments []item.Enchantment) []item.Stack {
if hasSilkTouch(enchantments) {
return []item.Stack{item.NewStack(block, 1)}
}
return []item.Stack{item.NewStack(drop, fortuneDiscreteCount(minCount, maxCount, capCount, enchantments))}
}
}
// grassDrops returns a drop function for grass/fern blocks. Shears or silk touch tools will cause the grass block
// itself to always drop. Otherwise, there is a 12.5% chance of dropping a wheat seed. Every level of Fortune will
// increase the max drop count by 2, with each possible drop count being equally likely.
func grassDrops(grass world.Item) func(item.Tool, []item.Enchantment) []item.Stack {
return func(t item.Tool, enchantments []item.Enchantment) []item.Stack {
if t.ToolType() == item.TypeShears || hasSilkTouch(enchantments) {
return []item.Stack{item.NewStack(grass, 1)}
}
if rand.Float32() < 0.125 {
count := 1
if fortune := fortuneLevel(enchantments); fortune > 0 {
count += rand.IntN(fortune*2 + 1)
}
return []item.Stack{item.NewStack(WheatSeeds{}, count)}
}
return nil
}
}
// cropSeedDrops returns a drop function for wheat/beetroot seeds.
// Uses binomial distribution B(3+fortune, 8/15), seeds may not drop.
func cropSeedDrops(seed, crop world.Item, growth int) func(item.Tool, []item.Enchantment) []item.Stack {
return func(t item.Tool, enchantments []item.Enchantment) []item.Stack {
if growth < 7 {
return []item.Stack{item.NewStack(seed, 1)}
}
seedCount := fortuneBinomial(3 + fortuneLevel(enchantments))
if seedCount == 0 {
return []item.Stack{item.NewStack(crop, 1)}
}
return []item.Stack{item.NewStack(crop, 1), item.NewStack(seed, seedCount)}
}
}
// breakBlock removes a block, shows breaking particles and drops the drops of
// the block as items.
func breakBlock(b world.Block, pos cube.Pos, tx *world.Tx) {
breakBlockNoDrops(b, pos, tx)
if breakable, ok := b.(Breakable); ok {
for _, drop := range breakable.BreakInfo().Drops(item.ToolNone{}, nil) {
dropItem(tx, drop, pos.Vec3Centre())
}
}
}
func breakBlockNoDrops(b world.Block, pos cube.Pos, tx *world.Tx) {
// Clear the block first so neighbour-sensitive break handlers observe the post-break world state.
tx.SetBlock(pos, nil, nil)
if breakable, ok := b.(Breakable); ok {
breakHandler := breakable.BreakInfo().BreakHandler
if breakHandler != nil {
breakHandler(pos, tx, nil)
}
}
tx.AddParticle(pos.Vec3Centre(), particle.BlockBreak{Block: b})
}