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}) }