github.com/df-mc/dragonfly@v0.9.13/server/block/fire.go (about)

     1  package block
     2  
     3  //lint:file-ignore ST1022 Exported variables in this package have compiler directives. These variables are not otherwise exposed to users.
     4  
     5  import (
     6  	"github.com/df-mc/dragonfly/server/block/cube"
     7  	"github.com/df-mc/dragonfly/server/event"
     8  	"github.com/df-mc/dragonfly/server/item"
     9  	"github.com/df-mc/dragonfly/server/item/enchantment"
    10  	"github.com/df-mc/dragonfly/server/world"
    11  	"math/rand"
    12  	"time"
    13  )
    14  
    15  // Fire is a non-solid block that can spread to nearby flammable blocks.
    16  type Fire struct {
    17  	replaceable
    18  	transparent
    19  	empty
    20  
    21  	// Type is the type of fire.
    22  	Type FireType
    23  	// Age affects how fire extinguishes. Newly placed fire starts at 0 and the value has a 1/3 chance of incrementing
    24  	// each block tick.
    25  	Age int
    26  }
    27  
    28  // flammableBlock returns true if a block is flammable.
    29  func flammableBlock(block world.Block) bool {
    30  	flammable, ok := block.(Flammable)
    31  	return ok && flammable.FlammabilityInfo().Encouragement > 0
    32  }
    33  
    34  // neighboursFlammable returns true if one a block adjacent to the passed position is flammable.
    35  func neighboursFlammable(pos cube.Pos, w *world.World) bool {
    36  	for _, i := range cube.Faces() {
    37  		if flammableBlock(w.Block(pos.Side(i))) {
    38  			return true
    39  		}
    40  	}
    41  	return false
    42  }
    43  
    44  // max ...
    45  func max(a, b int) int {
    46  	if a > b {
    47  		return a
    48  	}
    49  	return b
    50  }
    51  
    52  // infinitelyBurning returns true if fire can infinitely burn at the specified position.
    53  func infinitelyBurning(pos cube.Pos, w *world.World) bool {
    54  	switch block := w.Block(pos.Side(cube.FaceDown)).(type) {
    55  	// TODO: Magma Block
    56  	case Netherrack:
    57  		return true
    58  	case Bedrock:
    59  		return block.InfiniteBurning
    60  	}
    61  	return false
    62  }
    63  
    64  // burn attempts to burn a block.
    65  func (f Fire) burn(from, to cube.Pos, w *world.World, r *rand.Rand, chanceBound int) {
    66  	if flammable, ok := w.Block(to).(Flammable); ok && r.Intn(chanceBound) < flammable.FlammabilityInfo().Flammability {
    67  		if r.Intn(f.Age+10) < 5 && !rainingAround(to, w) {
    68  			f.spread(from, to, w, r)
    69  			return
    70  		}
    71  		if t, ok := flammable.(TNT); ok {
    72  			t.Ignite(to, w)
    73  			return
    74  		}
    75  		w.SetBlock(to, nil, nil)
    76  	}
    77  }
    78  
    79  // rainingAround checks if it is raining either at the cube.Pos passed or at any of its horizontal neighbours.
    80  func rainingAround(pos cube.Pos, w *world.World) bool {
    81  	raining := w.RainingAt(pos)
    82  	for _, face := range cube.HorizontalFaces() {
    83  		if raining {
    84  			break
    85  		}
    86  		raining = w.RainingAt(pos.Side(face))
    87  	}
    88  	return raining
    89  }
    90  
    91  // tick ...
    92  func (f Fire) tick(pos cube.Pos, w *world.World, r *rand.Rand) {
    93  	if f.Type == SoulFire() {
    94  		return
    95  	}
    96  	infinitelyBurns := infinitelyBurning(pos, w)
    97  	if !infinitelyBurns && (20+f.Age*3) > r.Intn(100) && rainingAround(pos, w) {
    98  		// Fire is extinguished by the rain.
    99  		w.SetBlock(pos, nil, nil)
   100  		return
   101  	}
   102  
   103  	if f.Age < 15 && r.Intn(3) == 0 {
   104  		f.Age++
   105  		w.SetBlock(pos, f, nil)
   106  	}
   107  
   108  	w.ScheduleBlockUpdate(pos, time.Duration(30+r.Intn(10))*time.Second/20)
   109  
   110  	if !infinitelyBurns {
   111  		_, waterBelow := w.Block(pos.Side(cube.FaceDown)).(Water)
   112  		if waterBelow {
   113  			w.SetBlock(pos, nil, nil)
   114  			return
   115  		}
   116  		if !neighboursFlammable(pos, w) {
   117  			if !w.Block(pos.Side(cube.FaceDown)).Model().FaceSolid(pos, cube.FaceUp, w) || f.Age > 3 {
   118  				w.SetBlock(pos, nil, nil)
   119  			}
   120  			return
   121  		}
   122  		if !flammableBlock(w.Block(pos.Side(cube.FaceDown))) && f.Age == 15 && r.Intn(4) == 0 {
   123  			w.SetBlock(pos, nil, nil)
   124  			return
   125  		}
   126  	}
   127  
   128  	humid := w.Biome(pos).Rainfall() > 0.85
   129  
   130  	s := 0
   131  	if humid {
   132  		s = 50
   133  	}
   134  	for _, face := range cube.Faces() {
   135  		if face == cube.FaceUp || face == cube.FaceDown {
   136  			f.burn(pos, pos.Side(face), w, r, 300-s)
   137  		} else {
   138  			f.burn(pos, pos.Side(face), w, r, 250-s)
   139  		}
   140  	}
   141  
   142  	for y := -1; y <= 4; y++ {
   143  		randomBound := 100
   144  		if y > 1 {
   145  			randomBound += (y - 1) * 100
   146  		}
   147  
   148  		for x := -1; x <= 1; x++ {
   149  			for z := -1; z <= 1; z++ {
   150  				if x == 0 && y == 0 && z == 0 {
   151  					continue
   152  				}
   153  				blockPos := pos.Add(cube.Pos{x, y, z})
   154  				block := w.Block(blockPos)
   155  				if _, ok := block.(Air); !ok {
   156  					continue
   157  				}
   158  
   159  				encouragement := 0
   160  				blockPos.Neighbours(func(neighbour cube.Pos) {
   161  					if flammable, ok := w.Block(neighbour).(Flammable); ok {
   162  						encouragement = max(encouragement, flammable.FlammabilityInfo().Encouragement)
   163  					}
   164  				}, w.Range())
   165  				if encouragement <= 0 {
   166  					continue
   167  				}
   168  
   169  				maxChance := (encouragement + 40 + w.Difficulty().FireSpreadIncrease()) / (f.Age + 30)
   170  				if humid {
   171  					maxChance /= 2
   172  				}
   173  
   174  				if maxChance > 0 && r.Intn(randomBound) <= maxChance && !rainingAround(blockPos, w) {
   175  					f.spread(pos, blockPos, w, r)
   176  				}
   177  			}
   178  		}
   179  	}
   180  }
   181  
   182  // spread attempts to spread fire from a cube.Pos to another. If the block burn or fire spreading events are cancelled,
   183  // this might end up not happening.
   184  func (f Fire) spread(from, to cube.Pos, w *world.World, r *rand.Rand) {
   185  	if _, air := w.Block(to).(Air); !air {
   186  		ctx := event.C()
   187  		if w.Handler().HandleBlockBurn(ctx, to); ctx.Cancelled() {
   188  			return
   189  		}
   190  	}
   191  	ctx := event.C()
   192  	if w.Handler().HandleFireSpread(ctx, from, to); ctx.Cancelled() {
   193  		return
   194  	}
   195  	w.SetBlock(to, Fire{Type: f.Type, Age: min(15, f.Age+r.Intn(5)/4)}, nil)
   196  	w.ScheduleBlockUpdate(to, time.Duration(30+r.Intn(10))*time.Second/20)
   197  }
   198  
   199  // EntityInside ...
   200  func (f Fire) EntityInside(_ cube.Pos, _ *world.World, e world.Entity) {
   201  	if flammable, ok := e.(flammableEntity); ok {
   202  		if l, ok := e.(livingEntity); ok && !l.AttackImmune() {
   203  			l.Hurt(f.Type.Damage(), FireDamageSource{})
   204  		}
   205  		if flammable.OnFireDuration() < time.Second*8 {
   206  			flammable.SetOnFire(8 * time.Second)
   207  		}
   208  	}
   209  }
   210  
   211  // ScheduledTick ...
   212  func (f Fire) ScheduledTick(pos cube.Pos, w *world.World, r *rand.Rand) {
   213  	f.tick(pos, w, r)
   214  }
   215  
   216  // RandomTick ...
   217  func (f Fire) RandomTick(pos cube.Pos, w *world.World, r *rand.Rand) {
   218  	f.tick(pos, w, r)
   219  }
   220  
   221  // NeighbourUpdateTick ...
   222  func (f Fire) NeighbourUpdateTick(pos, neighbour cube.Pos, w *world.World) {
   223  	below := w.Block(pos.Side(cube.FaceDown))
   224  	if diffuser, ok := below.(LightDiffuser); (ok && diffuser.LightDiffusionLevel() != 15) && (!neighboursFlammable(pos, w) || f.Type == SoulFire()) {
   225  		w.SetBlock(pos, nil, nil)
   226  		return
   227  	}
   228  	switch below.(type) {
   229  	case SoulSand, SoulSoil:
   230  		f.Type = SoulFire()
   231  		w.SetBlock(pos, f, nil)
   232  	case Water:
   233  		if neighbour == pos {
   234  			w.SetBlock(pos, nil, nil)
   235  		}
   236  	default:
   237  		if f.Type == SoulFire() {
   238  			w.SetBlock(pos, nil, nil)
   239  			return
   240  		}
   241  	}
   242  }
   243  
   244  // HasLiquidDrops ...
   245  func (f Fire) HasLiquidDrops() bool {
   246  	return false
   247  }
   248  
   249  // LightEmissionLevel ...
   250  func (f Fire) LightEmissionLevel() uint8 {
   251  	return f.Type.LightLevel()
   252  }
   253  
   254  // EncodeBlock ...
   255  func (f Fire) EncodeBlock() (name string, properties map[string]any) {
   256  	switch f.Type {
   257  	case NormalFire():
   258  		return "minecraft:fire", map[string]any{"age": int32(f.Age)}
   259  	case SoulFire():
   260  		return "minecraft:soul_fire", map[string]any{"age": int32(f.Age)}
   261  	}
   262  	panic("unknown fire type")
   263  }
   264  
   265  // Start starts a fire at a position in the world. The position passed must be either air or tall grass and conditions
   266  // for a fire to be present must be present.
   267  func (f Fire) Start(w *world.World, pos cube.Pos) {
   268  	b := w.Block(pos)
   269  	_, air := b.(Air)
   270  	_, tallGrass := b.(TallGrass)
   271  	if air || tallGrass {
   272  		below := w.Block(pos.Side(cube.FaceDown))
   273  		if below.Model().FaceSolid(pos, cube.FaceUp, w) || neighboursFlammable(pos, w) {
   274  			w.SetBlock(pos, Fire{}, nil)
   275  		}
   276  	}
   277  }
   278  
   279  // allFire ...
   280  func allFire() (b []world.Block) {
   281  	for i := 0; i < 16; i++ {
   282  		b = append(b, Fire{Age: i, Type: NormalFire()})
   283  		b = append(b, Fire{Age: i, Type: SoulFire()})
   284  	}
   285  	return
   286  }
   287  
   288  // FireDamageSource is used for damage caused by being in fire.
   289  type FireDamageSource struct{}
   290  
   291  func (FireDamageSource) ReducedByResistance() bool { return true }
   292  func (FireDamageSource) ReducedByArmour() bool     { return true }
   293  func (FireDamageSource) Fire() bool                { return true }
   294  func (FireDamageSource) AffectedByEnchantment(e item.EnchantmentType) bool {
   295  	_, prot := e.(enchantment.FireProtection)
   296  	return prot
   297  }