Files
mc/server/block/campfire.go
T

300 lines
7.2 KiB
Go
Raw Normal View History

2026-07-09 08:33:57 +08:00
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/v2"
"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.Tx) bool {
return false
}
// BreakInfo ...
func (c Campfire) BreakInfo() BreakInfo {
return newBreakInfo(2, alwaysHarvestable, axeEffective, func(t item.Tool, enchantments []item.Enchantment) []item.Stack {
if hasSilkTouch(enchantments) {
return []item.Stack{item.NewStack(Campfire{Type: c.Type}, 1)}
}
switch c.Type {
case NormalFire():
return []item.Stack{item.NewStack(item.Charcoal{}, 2)}
case SoulFire():
return []item.Stack{item.NewStack(SoulSoil{}, 1)}
}
panic("should never happen")
}).withBreakHandler(func(pos cube.Pos, tx *world.Tx, u item.User) {
for _, v := range c.Items {
if !v.Item.Empty() {
dropItem(tx, v.Item, pos.Vec3Centre())
}
}
})
}
// LightEmissionLevel ...
func (c Campfire) LightEmissionLevel() uint8 {
if c.Extinguished {
return 0
}
return c.Type.LightLevel()
}
// Ignite ...
func (c Campfire) Ignite(pos cube.Pos, tx *world.Tx, _ world.Entity) bool {
tx.PlaySound(pos.Vec3(), sound.Ignite{})
if !c.Extinguished {
return false
}
if _, ok := tx.Liquid(pos); ok {
return false
}
c.Extinguished = false
tx.SetBlock(pos, c, nil)
return true
}
// Splash ...
func (c Campfire) Splash(tx *world.Tx, pos cube.Pos) {
if c.Extinguished {
return
}
c.extinguish(pos, tx)
}
// extinguish extinguishes the campfire.
func (c Campfire) extinguish(pos cube.Pos, tx *world.Tx) {
tx.PlaySound(pos.Vec3Centre(), sound.FireExtinguish{})
c.Extinguished = true
for i := range c.Items {
c.Items[i].Time = time.Second * 30
}
tx.SetBlock(pos, c, nil)
}
// Activate ...
func (c Campfire) Activate(pos cube.Pos, _ cube.Face, tx *world.Tx, 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, tx)
ctx.DamageItem(1)
return true
}
rawFood, ok := held.Item().(item.Smeltable)
if !ok || !rawFood.SmeltInfo().Food {
return false
}
if _, ok = tx.Liquid(pos); ok {
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)
tx.PlaySound(pos.Vec3Centre(), sound.ItemAdd{})
tx.SetBlock(pos, c, nil)
return true
}
}
return false
}
// UseOnBlock ...
func (c Campfire) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, tx *world.Tx, user item.User, ctx *item.UseContext) (used bool) {
pos, _, used = firstReplaceable(tx, pos, face, c)
if !used {
return
}
if _, ok := tx.Block(pos.Side(cube.FaceDown)).(Campfire); ok {
return false
}
c.Facing = user.Rotation().Direction().Opposite()
place(tx, 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, tx *world.Tx) {
if c.Extinguished {
// Extinguished, do nothing.
return
}
if rand.Float64() <= 0.016 { // Every three or so seconds.
tx.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(tx, food.SmeltInfo().Product, pos.Vec3Middle())
}
c.Items[i].Item = item.Stack{}
}
if updated {
tx.SetBlock(pos, c, nil)
}
}
// NeighbourUpdateTick ...
func (c Campfire) NeighbourUpdateTick(pos, _ cube.Pos, tx *world.Tx) {
if _, ok := tx.Liquid(pos); ok {
var updated bool
for i, it := range c.Items {
if !it.Item.Empty() {
dropItem(tx, it.Item, pos.Vec3Centre())
c.Items[i].Item, updated = item.Stack{}, true
}
}
if !c.Extinguished {
c.extinguish(pos, tx)
} else if updated {
tx.SetBlock(pos, c, nil)
}
return
}
if liquid, ok := tx.Liquid(pos.Side(cube.FaceUp)); ok && liquid.LiquidType() == "water" && !c.Extinguished {
c.extinguish(pos, tx)
}
}
// EntityInside ...
func (c Campfire) EntityInside(pos cube.Pos, tx *world.Tx, e world.Entity) {
if flammable, ok := e.(flammableEntity); ok {
if flammable.OnFireDuration() > 0 && c.Extinguished {
c.Extinguished = false
tx.PlaySound(pos.Vec3(), sound.Ignite{})
tx.SetBlock(pos, c, nil)
}
if !c.Extinguished {
if l, ok := e.(livingEntity); ok {
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
}