diff --git a/server/block/hash.go b/server/block/hash.go index 684d9f010..fc8c999df 100644 --- a/server/block/hash.go +++ b/server/block/hash.go @@ -180,6 +180,7 @@ const ( hashSmoker hashSmoothBasalt hashSnow + hashSnowLayer hashSoulSand hashSoulSoil hashSponge @@ -920,6 +921,10 @@ func (Snow) Hash() (uint64, uint64) { return hashSnow, 0 } +func (s SnowLayer) Hash() (uint64, uint64) { + return hashSnowLayer, uint64(s.Height) +} + func (SoulSand) Hash() (uint64, uint64) { return hashSoulSand, 0 } diff --git a/server/block/model/snow.go b/server/block/model/snow.go new file mode 100644 index 000000000..46340b704 --- /dev/null +++ b/server/block/model/snow.go @@ -0,0 +1,28 @@ +package model + +import ( + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/world" +) + +// Snow is the model of a snow layer. The height of its bounding box scales with the number of layers the block +// has, ranging from a single passable layer to a near-full block. +type Snow struct { + // Layers is the number of snow layers the block has, ranging from 1 to 8. + Layers int +} + +// BBox returns a flat box whose height scales with the number of layers. A single layer returns no box at all, so +// that entities walk over it freely as they do in vanilla. +func (s Snow) BBox(cube.Pos, world.BlockSource) []cube.BBox { + if s.Layers <= 1 { + return nil + } + return []cube.BBox{cube.Box(0, 0, 0, 1, float64(s.Layers-1)*0.125, 1)} +} + +// FaceSolid returns true only for the upward face of a full eight-layer block, the only state to which other +// blocks may be attached. +func (s Snow) FaceSolid(_ cube.Pos, face cube.Face, _ world.BlockSource) bool { + return s.Layers >= 8 && face == cube.FaceUp +} diff --git a/server/block/register.go b/server/block/register.go index 116c454e6..c6fff44fa 100644 --- a/server/block/register.go +++ b/server/block/register.go @@ -108,6 +108,7 @@ func init() { world.RegisterBlock(SmithingTable{}) world.RegisterBlock(SmoothBasalt{}) world.RegisterBlock(Snow{}) + registerAll(allSnowLayers()) world.RegisterBlock(SoulSand{}) world.RegisterBlock(SoulSoil{}) world.RegisterBlock(Sponge{Wet: true}) @@ -382,6 +383,7 @@ func init() { world.RegisterItem(Smoker{}) world.RegisterItem(SmoothBasalt{}) world.RegisterItem(Snow{}) + world.RegisterItem(SnowLayer{}) world.RegisterItem(SoulSand{}) world.RegisterItem(SoulSoil{}) world.RegisterItem(Sponge{Wet: true}) diff --git a/server/block/snow_layer.go b/server/block/snow_layer.go new file mode 100644 index 000000000..12eafdcd0 --- /dev/null +++ b/server/block/snow_layer.go @@ -0,0 +1,121 @@ +package block + +import ( + "math/rand/v2" + + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/block/model" + "github.com/df-mc/dragonfly/server/item" + "github.com/df-mc/dragonfly/server/world" + "github.com/go-gl/mathgl/mgl64" +) + +// SnowLayer is a thin, partial covering of snow that may be stacked up to eight layers high. +type SnowLayer struct { + transparent + + // Height is the number of snow layers in addition to the bottom one, ranging from 0 (a single layer) to 7 + // (eight layers, the height of a full block). + Height int +} + +// Model ... +func (s SnowLayer) Model() world.BlockModel { + return model.Snow{Layers: s.Height + 1} +} + +// ReplaceableBy returns true if more snow may still be stacked onto the layer, or, for any other block, only when +// this is a single layer thin enough to be replaced. +func (s SnowLayer) ReplaceableBy(b world.Block) bool { + if _, ok := b.(SnowLayer); ok { + return s.Height < 7 + } + return s.Height == 0 +} + +// UseOnBlock places a new snow layer or, when used on an existing layer, stacks another layer onto it. +func (s SnowLayer) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, tx *world.Tx, user item.User, ctx *item.UseContext) bool { + if existing, ok := tx.Block(pos).(SnowLayer); ok && existing.Height < 7 { + existing.Height++ + place(tx, pos, existing, user, ctx) + return placed(ctx) + } + + pos, _, used := firstReplaceable(tx, pos, face, s) + if !used { + return false + } + below := pos.Side(cube.FaceDown) + if !tx.Block(below).Model().FaceSolid(below, cube.FaceUp, tx) { + // Snow can only rest on top of a block with a solid upward face. + return false + } + + place(tx, pos, s, user, ctx) + return placed(ctx) +} + +// NeighbourUpdateTick breaks the snow layer if the block below it no longer supports it. +func (s SnowLayer) NeighbourUpdateTick(pos, _ cube.Pos, tx *world.Tx) { + below := pos.Side(cube.FaceDown) + if !tx.Block(below).Model().FaceSolid(below, cube.FaceUp, tx) { + breakBlock(s, pos, tx) + } +} + +// RandomTick melts the snow in biomes too warm to sustain it, removing a single layer at a time until it is gone. +func (s SnowLayer) RandomTick(pos cube.Pos, tx *world.Tx, _ *rand.Rand) { + // TODO: also melt when the block light level is 12 or higher, regardless of the biome temperature. The world + // does not currently expose the block light level separately from the sky light, which is required to do this + // without melting snow in daylight. + if tx.Temperature(pos) <= 0.15 { + return + } + if s.Height == 0 { + tx.SetBlock(pos, nil, nil) + return + } + s.Height-- + tx.SetBlock(pos, s, nil) +} + +// BreakInfo ... +func (s SnowLayer) BreakInfo() BreakInfo { + return newBreakInfo(0.1, alwaysHarvestable, shovelEffective, func(t item.Tool, enchantments []item.Enchantment) []item.Stack { + layers := s.Height + 1 + if hasSilkTouch(enchantments) { + if layers >= 8 { + return []item.Stack{item.NewStack(Snow{}, 1)} + } + return []item.Stack{item.NewStack(SnowLayer{}, layers)} + } + switch { + case layers <= 3: + return []item.Stack{item.NewStack(item.Snowball{}, 1)} + case layers <= 5: + return []item.Stack{item.NewStack(item.Snowball{}, 2)} + case layers <= 7: + return []item.Stack{item.NewStack(item.Snowball{}, 3)} + default: + return []item.Stack{item.NewStack(item.Snowball{}, 4)} + } + }) +} + +// EncodeItem ... +func (SnowLayer) EncodeItem() (name string, meta int16) { + return "minecraft:snow_layer", 0 +} + +// EncodeBlock ... +func (s SnowLayer) EncodeBlock() (string, map[string]any) { + return "minecraft:snow_layer", map[string]any{"height": int32(s.Height), "covered_bit": boolByte(false)} +} + +// allSnowLayers ... +func allSnowLayers() (s []world.Block) { + for h := 0; h < 8; h++ { + s = append(s, SnowLayer{Height: h}) + } + return +}