262 lines
8.9 KiB
Go
262 lines
8.9 KiB
Go
|
|
package world
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"time"
|
|||
|
|
|
|||
|
|
"github.com/df-mc/dragonfly/server/block/cube"
|
|||
|
|
"github.com/go-gl/mathgl/mgl64"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// weather implements weather related methods for World. World embeds this
|
|||
|
|
// struct, so any exported methods on weather are exported methods on World.
|
|||
|
|
type weather struct{ w *World }
|
|||
|
|
|
|||
|
|
// StopWeatherCycle disables weather cycle of the World.
|
|||
|
|
func (w weather) StopWeatherCycle() {
|
|||
|
|
w.enableWeatherCycle(false)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// StartWeatherCycle enables weather cycle of the World.
|
|||
|
|
func (w weather) StartWeatherCycle() {
|
|||
|
|
w.enableWeatherCycle(true)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// snowingAt checks if it is snowing at a specific cube.Pos in the World. True
|
|||
|
|
// is returned if the temperature in the Biome at that position is sufficiently
|
|||
|
|
// low, if it is raining and if it's above the top-most obstructing block.
|
|||
|
|
func (w weather) snowingAt(pos cube.Pos) bool {
|
|||
|
|
if w.w == nil || !w.w.Dimension().WeatherCycle() {
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
if b := w.w.biome(pos); b.Rainfall() == 0 || w.w.temperature(pos) > 0.15 {
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
w.w.set.Lock()
|
|||
|
|
raining := w.w.set.Raining
|
|||
|
|
w.w.set.Unlock()
|
|||
|
|
return raining && w.w.highestObstructingBlock(pos[0], pos[2]) < pos[1]
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// rainingAt checks if it is raining at a specific cube.Pos in the World. True
|
|||
|
|
// is returned if it is raining, if the temperature is high enough in the biome
|
|||
|
|
// for it not to be snow and if the block is above the top-most obstructing
|
|||
|
|
// block.
|
|||
|
|
func (w weather) rainingAt(pos cube.Pos) bool {
|
|||
|
|
if w.w == nil || !w.w.Dimension().WeatherCycle() {
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
if b := w.w.biome(pos); b.Rainfall() == 0 || w.w.temperature(pos) <= 0.15 {
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
w.w.set.Lock()
|
|||
|
|
a := w.w.set.Raining
|
|||
|
|
w.w.set.Unlock()
|
|||
|
|
return a && w.w.highestObstructingBlock(pos[0], pos[2]) < pos[1]
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// thunderingAt checks if it is thundering at a specific cube.Pos in the World.
|
|||
|
|
// True is returned if rainingAt returns true and if it is thundering in the
|
|||
|
|
// world.
|
|||
|
|
func (w weather) thunderingAt(pos cube.Pos) bool {
|
|||
|
|
raining := w.rainingAt(pos)
|
|||
|
|
w.w.set.Lock()
|
|||
|
|
a := w.w.set.Thundering && raining
|
|||
|
|
w.w.set.Unlock()
|
|||
|
|
return a && w.w.highestObstructingBlock(pos[0], pos[2]) < pos[1]
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// raining checks if it is raining anywhere in the World.
|
|||
|
|
func (w weather) raining() bool {
|
|||
|
|
if w.w == nil || !w.w.Dimension().WeatherCycle() {
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
w.w.set.Lock()
|
|||
|
|
defer w.w.set.Unlock()
|
|||
|
|
return w.w.set.Raining
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// thundering checks if it is thundering anywhere in the World.
|
|||
|
|
func (w weather) thundering() bool {
|
|||
|
|
if w.w == nil || !w.w.Dimension().WeatherCycle() {
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
w.w.set.Lock()
|
|||
|
|
defer w.w.set.Unlock()
|
|||
|
|
return w.w.set.Thundering
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// StartRaining makes it rain in the World. The time.Duration passed will
|
|||
|
|
// determine how long it will rain.
|
|||
|
|
func (w weather) StartRaining(dur time.Duration) {
|
|||
|
|
w.w.set.Lock()
|
|||
|
|
defer w.w.set.Unlock()
|
|||
|
|
w.setRaining(true, dur)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// StopRaining makes it stop raining in the World.
|
|||
|
|
func (w weather) StopRaining() {
|
|||
|
|
w.w.set.Lock()
|
|||
|
|
defer w.w.set.Unlock()
|
|||
|
|
|
|||
|
|
if w.w.set.Raining {
|
|||
|
|
w.setRaining(false, time.Second*(time.Duration(w.w.r.IntN(8400)+600)))
|
|||
|
|
if w.w.set.Thundering {
|
|||
|
|
// Also reset thunder if it was previously thundering.
|
|||
|
|
w.setThunder(false, time.Second*(time.Duration(w.w.r.IntN(8400)+600)))
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// StartThundering makes it thunder in the World. The time.Duration passed will
|
|||
|
|
// determine how long it will thunder. StartThundering will also make it rain
|
|||
|
|
// if it wasn't already raining. In this case the rain will, like the thunder,
|
|||
|
|
// last for the time.Duration passed.
|
|||
|
|
func (w weather) StartThundering(dur time.Duration) {
|
|||
|
|
w.w.set.Lock()
|
|||
|
|
defer w.w.set.Unlock()
|
|||
|
|
|
|||
|
|
w.setThunder(true, dur)
|
|||
|
|
w.setRaining(true, dur)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// StopThundering makes it stop thundering in the current world.
|
|||
|
|
func (w weather) StopThundering() {
|
|||
|
|
w.w.set.Lock()
|
|||
|
|
defer w.w.set.Unlock()
|
|||
|
|
if w.w.set.Thundering && w.w.set.Raining {
|
|||
|
|
w.setThunder(false, time.Second*(time.Duration(w.w.r.IntN(8400)+600)))
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// advanceWeather advances the weather counters of the World. Rain and thunder
|
|||
|
|
// are stopped/started when the rain and thunder times reach 0.
|
|||
|
|
func (w weather) advanceWeather() {
|
|||
|
|
w.w.set.RainTime--
|
|||
|
|
w.w.set.ThunderTime--
|
|||
|
|
|
|||
|
|
if w.w.set.RainTime <= 0 {
|
|||
|
|
// Wiki: The rain counter counts down to zero, and each time it reaches
|
|||
|
|
// zero, the rain is toggled on or off. When the rain is turned on, the
|
|||
|
|
// counter is reset to a value between 12,000-23,999 ticks (0.5-1 game
|
|||
|
|
// days) and when the rain is turned off it is reset to a value of
|
|||
|
|
// 12,000-179,999 ticks (0.5-7.5 game days).
|
|||
|
|
if w.w.set.Raining {
|
|||
|
|
w.w.setRaining(false, time.Second*(time.Duration(w.w.r.IntN(8400)+600)))
|
|||
|
|
} else {
|
|||
|
|
w.w.setRaining(true, time.Second*time.Duration(w.w.r.IntN(600)+600))
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if w.w.set.ThunderTime <= 0 {
|
|||
|
|
// Wiki: the thunder counter toggles thunder on/off when it reaches
|
|||
|
|
// zero, but clear weather overrides the "on" state. When thunder is
|
|||
|
|
// turned on, the thunder counter is reset to 3,600-15,999 ticks (3-13
|
|||
|
|
// minutes), and when thunder is turned off the counter rests to
|
|||
|
|
// 12,000-179,999 ticks (0.5-7.5 days).
|
|||
|
|
if w.w.set.Thundering {
|
|||
|
|
w.w.setThunder(false, time.Second*(time.Duration(w.w.r.IntN(8400)+600)))
|
|||
|
|
} else {
|
|||
|
|
w.w.setThunder(true, time.Second*time.Duration(w.w.r.IntN(620)+180))
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// setRaining toggles raining depending on the raining argument. This does not
|
|||
|
|
// lock the world mutex as opposed to StartRaining and StopRaining.
|
|||
|
|
func (w weather) setRaining(raining bool, x time.Duration) {
|
|||
|
|
w.w.set.Raining = raining
|
|||
|
|
w.w.set.RainTime = int64(x.Seconds() * 20)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// setThunder toggles thundering depending on the thundering argument. This
|
|||
|
|
// does not lock the world mutex as opposed to StartThundering and
|
|||
|
|
// StopThundering.
|
|||
|
|
func (w weather) setThunder(thundering bool, x time.Duration) {
|
|||
|
|
w.w.set.Thundering = thundering
|
|||
|
|
w.w.set.ThunderTime = int64(x.Seconds() * 20)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// enableWeatherCycle either enables or disables the weather cycle of the World.
|
|||
|
|
func (w weather) enableWeatherCycle(v bool) {
|
|||
|
|
if w.w == nil {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
w.w.set.Lock()
|
|||
|
|
defer w.w.set.Unlock()
|
|||
|
|
w.w.set.WeatherCycle = v
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// tickLightning iterates over all loaded chunks in the World, striking
|
|||
|
|
// lightning in each one with a 1/100,000 chance.
|
|||
|
|
func (w weather) tickLightning(tx *Tx) {
|
|||
|
|
positions := make([]ChunkPos, 0, len(w.w.chunks)/100000)
|
|||
|
|
for pos := range w.w.chunks {
|
|||
|
|
// Wiki: For each loaded chunk, every tick there is a 1⁄100,000 chance
|
|||
|
|
// of an attempted lightning strike during a thunderstorm
|
|||
|
|
if w.w.r.IntN(100000) == 0 {
|
|||
|
|
positions = append(positions, pos)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for _, pos := range positions {
|
|||
|
|
w.w.strikeLightning(tx, pos)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// strikeLightning attempts to strike lightning in the world at a specific
|
|||
|
|
// ChunkPos. The final position is influenced by living entities that might be
|
|||
|
|
// near the lightning strike. If there is no rain at the final position
|
|||
|
|
// selected, the lightning strike will fail.
|
|||
|
|
func (w weather) strikeLightning(tx *Tx, c ChunkPos) {
|
|||
|
|
if w.w.conf.Entities.conf.Lightning == nil {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
if pos := w.lightningPosition(tx, c); tx.ThunderingAt(cube.PosFromVec3(pos)) {
|
|||
|
|
tx.AddEntity(w.w.conf.Entities.conf.Lightning(EntitySpawnOpts{Position: pos}))
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// lightningPosition finds a random position in the ChunkPos to strike
|
|||
|
|
// lightning and adjusts the position to any of the living entities found in or
|
|||
|
|
// above the position if any are found.
|
|||
|
|
func (w weather) lightningPosition(tx *Tx, c ChunkPos) mgl64.Vec3 {
|
|||
|
|
v := w.w.r.Int32()
|
|||
|
|
x, z := float64(c[0]<<4+(v&0xf)), float64(c[1]<<4+((v>>8)&0xf))
|
|||
|
|
|
|||
|
|
vec := w.adjustPositionToEntities(tx, mgl64.Vec3{x, float64(tx.HighestBlock(int(x), int(z)) + 1), z})
|
|||
|
|
if pos := cube.PosFromVec3(vec); len(tx.Block(pos).Model().BBox(pos, tx)) != 0 {
|
|||
|
|
// If lightning is about to strike inside a block that is not fully
|
|||
|
|
// transparent. In this case, move the lightning up by one block so that
|
|||
|
|
// it strikes above the block.
|
|||
|
|
return vec.Add(mgl64.Vec3{0, 1})
|
|||
|
|
}
|
|||
|
|
return vec
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// adjustPositionToEntities adjusts the mgl64.Vec3 passed to the position of
|
|||
|
|
// any Entity found in the 3x3 column upwards from the mgl64.Vec3. If multiple
|
|||
|
|
// entities are found, the position of one of the entities is selected
|
|||
|
|
// randomly.
|
|||
|
|
func (w weather) adjustPositionToEntities(tx *Tx, vec mgl64.Vec3) mgl64.Vec3 {
|
|||
|
|
max := vec.Add(mgl64.Vec3{0, float64(w.w.Range().Max())})
|
|||
|
|
|
|||
|
|
list := make([]mgl64.Vec3, 0, 16)
|
|||
|
|
for e := range tx.EntitiesWithin(cube.Box(vec[0], vec[1], vec[2], max[0], max[1], max[2]).GrowVec3(mgl64.Vec3{3, 3, 3})) {
|
|||
|
|
if h, ok := e.(interface{ Health() float64 }); ok && h.Health() > 0 {
|
|||
|
|
// Any (living) entity that is positioned higher than the highest
|
|||
|
|
// block at its position is eligible to be struck by lightning. We
|
|||
|
|
// first save all entity positions where this is the case.
|
|||
|
|
pos := cube.PosFromVec3(e.Position())
|
|||
|
|
if tx.HighestBlock(pos[0], pos[1]) < pos[2] {
|
|||
|
|
list = append(list, e.Position())
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
// We then select one of the positions of entities higher than the highest
|
|||
|
|
// block and adjust the position of the lightning to it, so that the entity
|
|||
|
|
// is struck directly.
|
|||
|
|
if len(list) > 0 {
|
|||
|
|
vec = list[w.w.r.IntN(len(list))]
|
|||
|
|
}
|
|||
|
|
return vec
|
|||
|
|
}
|