26ed99fda6
Build and deploy / Build (push) Has been cancelled
Build and deploy / Update Contributors (push) Has been cancelled
Build and deploy / Deploy (push) Has been cancelled
385 lines
14 KiB
Go
385 lines
14 KiB
Go
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})
|
|
}
|