Skip to content

Commit

Permalink
dragonfly/server: Implement hoppers (df-mc#914)
Browse files Browse the repository at this point in the history
  • Loading branch information
xNatsuri authored Sep 1, 2024
1 parent 06e5131 commit ce3b980
Show file tree
Hide file tree
Showing 13 changed files with 456 additions and 36 deletions.
283 changes: 283 additions & 0 deletions server/block/campfire.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,283 @@
package block

import (
"github.com/df-mc/dragonfly/server/block/cube"
"github.com/df-mc/dragonfly/server/block/model"
"github.com/df-mc/dragonfly/server/internal/nbtconv"
"github.com/df-mc/dragonfly/server/item"
"github.com/df-mc/dragonfly/server/world"
"github.com/df-mc/dragonfly/server/world/sound"
"github.com/go-gl/mathgl/mgl64"
"math/rand"
"strconv"
"time"
)

// Campfire is a block that can be used to cook food, pacify bees, act as a spread-proof light source, smoke signal or
// damaging trap block.
type Campfire struct {
transparent
bass
sourceWaterDisplacer

// Items represents the items in the campfire that are being cooked.
Items [4]CampfireItem
// Facing represents the direction that the campfire is facing.
Facing cube.Direction
// Extinguished is true if the campfire was extinguished by a water source.
Extinguished bool
// Type represents the type of Campfire, currently there are Normal and Soul campfires.
Type FireType
}

// CampfireItem holds data about the items in the campfire.
type CampfireItem struct {
// Item is a specific item being cooked on top of the campfire.
Item item.Stack
// Time is the countdown of ticks until the food item is cooked (when 0).
Time time.Duration
}

// Model ...
func (Campfire) Model() world.BlockModel {
return model.Campfire{}
}

// SideClosed ...
func (Campfire) SideClosed(cube.Pos, cube.Pos, *world.World) bool {
return false
}

// BreakInfo ...
func (c Campfire) BreakInfo() BreakInfo {
return newBreakInfo(2, alwaysHarvestable, axeEffective, func(t item.Tool, enchantments []item.Enchantment) []item.Stack {
var drops []item.Stack
if hasSilkTouch(enchantments) {
drops = append(drops, item.NewStack(c, 1))
} else {
switch c.Type {
case NormalFire():
drops = append(drops, item.NewStack(item.Charcoal{}, 2))
case SoulFire():
drops = append(drops, item.NewStack(SoulSoil{}, 1))
}
}
for _, v := range c.Items {
if !v.Item.Empty() {
drops = append(drops, v.Item)
}
}
return drops
})
}

// LightEmissionLevel ...
func (c Campfire) LightEmissionLevel() uint8 {
if c.Extinguished {
return 0
}
return c.Type.LightLevel()
}

// Ignite ...
func (c Campfire) Ignite(pos cube.Pos, w *world.World, _ world.Entity) bool {
w.PlaySound(pos.Vec3(), sound.Ignite{})
if !c.Extinguished {
return false
}
if _, ok := w.Liquid(pos); ok {
return false
}

c.Extinguished = false
w.SetBlock(pos, c, nil)
return true
}

// Splash ...
func (c Campfire) Splash(w *world.World, pos cube.Pos) {
if c.Extinguished {
return
}

c.extinguish(pos, w)
}

// extinguish extinguishes the campfire.
func (c Campfire) extinguish(pos cube.Pos, w *world.World) {
w.PlaySound(pos.Vec3Centre(), sound.FireExtinguish{})
c.Extinguished = true

for i := range c.Items {
c.Items[i].Time = time.Second * 30
}

w.SetBlock(pos, c, nil)
}

// Activate ...
func (c Campfire) Activate(pos cube.Pos, _ cube.Face, w *world.World, u item.User, ctx *item.UseContext) bool {
held, _ := u.HeldItems()
if held.Empty() {
return false
}

if _, ok := held.Item().(item.Shovel); ok && !c.Extinguished {
c.extinguish(pos, w)
ctx.DamageItem(1)
return true
}

rawFood, ok := held.Item().(item.Smeltable)
if !ok || !rawFood.SmeltInfo().Food {
return false
}

for i, it := range c.Items {
if it.Item.Empty() {
c.Items[i] = CampfireItem{
Item: held.Grow(-held.Count() + 1),
Time: time.Second * 30,
}

ctx.SubtractFromCount(1)

w.PlaySound(pos.Vec3Centre(), sound.ItemAdd{})
w.SetBlock(pos, c, nil)
return true
}
}
return false
}

// UseOnBlock ...
func (c Campfire) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, w *world.World, user item.User, ctx *item.UseContext) (used bool) {
pos, _, used = firstReplaceable(w, pos, face, c)
if !used {
return
}
if _, ok := w.Block(pos.Side(cube.FaceDown)).(Campfire); ok {
return false
}
c.Facing = user.Rotation().Direction().Opposite()
place(w, pos, c, user, ctx)
return placed(ctx)
}

// Tick is called to cook the items within the campfire.
func (c Campfire) Tick(_ int64, pos cube.Pos, w *world.World) {
if c.Extinguished {
// Extinguished, do nothing.
return
}
if rand.Float64() <= 0.016 { // Every three or so seconds.
w.PlaySound(pos.Vec3Centre(), sound.CampfireCrackle{})
}

updated := false
for i, it := range c.Items {
if it.Item.Empty() {
continue
}

updated = true
if it.Time > 0 {
c.Items[i].Time = it.Time - time.Millisecond*50
continue
}

if food, ok := it.Item.Item().(item.Smeltable); ok {
dropItem(w, food.SmeltInfo().Product, pos.Vec3Middle())
}
c.Items[i].Item = item.Stack{}
}
if updated {
w.SetBlock(pos, c, nil)
}
}

// NeighbourUpdateTick ...
func (c Campfire) NeighbourUpdateTick(pos, _ cube.Pos, w *world.World) {
_, ok := w.Liquid(pos)
liquid, okTwo := w.Liquid(pos.Side(cube.FaceUp))
if (ok || (okTwo && liquid.LiquidType() == "water")) && !c.Extinguished {
c.extinguish(pos, w)
}
}

// EntityInside ...
func (c Campfire) EntityInside(pos cube.Pos, w *world.World, e world.Entity) {
if flammable, ok := e.(flammableEntity); ok {
if flammable.OnFireDuration() > 0 && c.Extinguished {
c.Extinguished = false
w.PlaySound(pos.Vec3(), sound.Ignite{})
w.SetBlock(pos, c, nil)
}
if !c.Extinguished {
if l, ok := e.(livingEntity); ok && !l.AttackImmune() {
l.Hurt(c.Type.Damage(), FireDamageSource{})
}
}
}
}

// EncodeNBT ...
func (c Campfire) EncodeNBT() map[string]any {
m := map[string]any{"id": "Campfire"}
for i, v := range c.Items {
id := strconv.Itoa(i + 1)
if !v.Item.Empty() {
m["Item"+id] = nbtconv.WriteItem(v.Item, true)
m["ItemTime"+id] = uint8(v.Time.Milliseconds() / 50)
}
}
return m
}

// DecodeNBT ...
func (c Campfire) DecodeNBT(data map[string]any) any {
for i := 0; i < 4; i++ {
id := strconv.Itoa(i + 1)
c.Items[i] = CampfireItem{
Item: nbtconv.MapItem(data, "Item"+id),
Time: time.Duration(nbtconv.Int16(data, "ItemTime"+id)) * time.Millisecond * 50,
}
}
return c
}

// EncodeItem ...
func (c Campfire) EncodeItem() (name string, meta int16) {
switch c.Type {
case NormalFire():
return "minecraft:campfire", 0
case SoulFire():
return "minecraft:soul_campfire", 0
}
panic("invalid fire type")
}

// EncodeBlock ...
func (c Campfire) EncodeBlock() (name string, properties map[string]any) {
switch c.Type {
case NormalFire():
name = "minecraft:campfire"
case SoulFire():
name = "minecraft:soul_campfire"
}
return name, map[string]any{
"minecraft:cardinal_direction": c.Facing.String(),
"extinguished": c.Extinguished,
}
}

// allCampfires ...
func allCampfires() (campfires []world.Block) {
for _, d := range cube.Directions() {
for _, f := range FireTypes() {
campfires = append(campfires, Campfire{Facing: d, Type: f, Extinguished: true})
campfires = append(campfires, Campfire{Facing: d, Type: f})
}
}
return campfires
}
6 changes: 6 additions & 0 deletions server/block/hash.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion server/block/item_frame.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func (i ItemFrame) Activate(pos cube.Pos, _ cube.Face, w *world.World, u item.Us
i.Item = held.Grow(-held.Count() + 1)
// TODO: When maps are implemented, check the item is a map, and if so, display the large version of the frame.
ctx.SubtractFromCount(1)
w.PlaySound(pos.Vec3Centre(), sound.ItemFrameAdd{})
w.PlaySound(pos.Vec3Centre(), sound.ItemAdd{})
} else {
return true
}
Expand Down
19 changes: 19 additions & 0 deletions server/block/model/campfire.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package model

import (
"github.com/df-mc/dragonfly/server/block/cube"
"github.com/df-mc/dragonfly/server/world"
)

// Campfire is the model used by campfires.
type Campfire struct{}

// BBox returns a flat BBox with a height of 0.4375.
func (Campfire) BBox(cube.Pos, *world.World) []cube.BBox {
return []cube.BBox{cube.Box(0, 0, 0, 1, 0.4375, 1)}
}

// FaceSolid returns true if the face is down.
func (Campfire) FaceSolid(_ cube.Pos, face cube.Face, _ *world.World) bool {
return face == cube.FaceDown
}
2 changes: 2 additions & 0 deletions server/block/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ func init() {
registerAll(allBoneBlock())
registerAll(allCactus())
registerAll(allCake())
registerAll(allCampfires())
registerAll(allCarpet())
registerAll(allCarrots())
registerAll(allChains())
Expand Down Expand Up @@ -394,6 +395,7 @@ func init() {
for _, f := range FireTypes() {
world.RegisterItem(Lantern{Type: f})
world.RegisterItem(Torch{Type: f})
world.RegisterItem(Campfire{Type: f})
}
for _, f := range FlowerTypes() {
world.RegisterItem(Flower{Type: f})
Expand Down
29 changes: 29 additions & 0 deletions server/entity/splashable.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,20 @@ import (
"time"
)

// SplashableBlock is a block that can be splashed with a splash bottle.
type SplashableBlock interface {
world.Block
// Splash is called when a water bottle splashes onto a block.
Splash(w *world.World, pos cube.Pos)
}

// SplashableEntity is an entity that can be splashed with a splash bottle.
type SplashableEntity interface {
world.Entity
// Splash is called when a water bottle splashes onto an entity.
Splash(w *world.World, pos mgl64.Vec3)
}

// potionSplash returns a function that creates a potion splash with a specific
// duration multiplier and potion type.
func potionSplash(durMul float64, pot potion.Potion, linger bool) func(e *Ent, res trace.Result) {
Expand Down Expand Up @@ -66,10 +80,25 @@ func potionSplash(durMul float64, pot potion.Potion, linger bool) func(e *Ent, r
if h := blockPos.Side(f); w.Block(h) == fire() {
w.SetBlock(h, nil, nil)
}

if b, ok := w.Block(blockPos.Side(f)).(SplashableBlock); ok {
b.Splash(w, blockPos.Side(f))
}
}

resultPos := result.BlockPosition()
if b, ok := w.Block(resultPos).(SplashableBlock); ok {
b.Splash(w, resultPos)
}
case trace.EntityResult:
// TODO: Damage endermen, blazes, striders and snow golems when implemented and rehydrate axolotls.
}

for _, otherE := range w.EntitiesWithin(box.GrowVec3(mgl64.Vec3{8.25, 4.25, 8.25}), ignores) {
if splashE, ok := otherE.(SplashableEntity); ok {
splashE.Splash(w, otherE.Position())
}
}
}
if linger {
w.AddEntity(NewAreaEffectCloud(pos, pot))
Expand Down
Binary file added server/item/recipe/furnace_data.nbt
Binary file not shown.
Loading

0 comments on commit ce3b980

Please sign in to comment.