From 09a694daf543d71aa8ef481b5f90abb593dbd340 Mon Sep 17 00:00:00 2001 From: Master290 Date: Sat, 27 Jun 2026 21:16:39 +0300 Subject: [PATCH] Add gravity and player-seeking AI to zombies --- internal/network/handler.go | 2 ++ internal/network/play.go | 8 ++++--- internal/server/server.go | 31 ++++++++++++++++++++++++++ internal/server/spawner.go | 43 +++++++++++++++++++++++++++++++++---- internal/world/cache.go | 12 +++++++++++ 5 files changed, 89 insertions(+), 7 deletions(-) diff --git a/internal/network/handler.go b/internal/network/handler.go index 35409ba..28a45d6 100644 --- a/internal/network/handler.go +++ b/internal/network/handler.go @@ -44,6 +44,8 @@ func (h *handler) serve() { defer cancel() // stops the streamer when the read loop ends h.ctx = ctx + defer h.srv.RemovePlayerPosition(h.conn.Profile.Name) + for { pkt, err := h.conn.ReadPacket() if err != nil { diff --git a/internal/network/play.go b/internal/network/play.go index 9dc72cc..b82660b 100644 --- a/internal/network/play.go +++ b/internal/network/play.go @@ -48,7 +48,8 @@ func (h *handler) beginPlay() error { // onPlayerMove recenters the streamer when the player crosses into a new chunk. // It is a non-blocking push; the read loop never waits on generation. -func (h *handler) onPlayerMove(x, z float64) error { +func (h *handler) onPlayerMove(x, y, z float64) error { + h.srv.SetPlayerPosition(h.conn.Profile.Name, x, y, z) cx := int32(int64(math.Floor(x)) >> 4) cz := int32(int64(math.Floor(z)) >> 4) if h.streamer != nil { @@ -230,14 +231,15 @@ func (h *handler) handlePlay(pkt protocol.Packet) error { if err != nil { return err } - if _, err := r.Float64(); err != nil { // feet Y, unused for streaming + y, err := r.Float64() // feet Y + if err != nil { return err } z, err := r.Float64() if err != nil { return err } - return h.onPlayerMove(x, z) + return h.onPlayerMove(x, y, z) case protocol.PlayPlayerAction: return h.handlePlayerAction(pkt) diff --git a/internal/server/server.go b/internal/server/server.go index a7e1c6b..2efad02 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -4,6 +4,7 @@ package server import ( "encoding/json" + "sync" "regionio/internal/protocol" "regionio/internal/world" @@ -55,6 +56,9 @@ type Server struct { chunks *world.Cache store *world.Store // nil when persistence is disabled entities *world.EntityManager + + // playerPos tracks the last known position of each player by name. + playerPos sync.Map // map[string][3]float64 } // New constructs a Server from cfg. When cfg.WorldDir is set, the world is @@ -89,6 +93,33 @@ func (s *Server) Chunks() *world.Cache { return s.chunks } // Entities returns the shared entity manager. func (s *Server) Entities() *world.EntityManager { return s.entities } +// SetPlayerPosition updates the tracked position of a player. +func (s *Server) SetPlayerPosition(name string, x, y, z float64) { + s.playerPos.Store(name, [3]float64{x, y, z}) +} + +// RemovePlayerPosition removes a player from tracking. +func (s *Server) RemovePlayerPosition(name string) { + s.playerPos.Delete(name) +} + +// NearestPlayer returns the position of the nearest player to (x, y, z). +// Returns false if no players are online. +func (s *Server) NearestPlayer(x, y, z float64) (pos [3]float64, ok bool) { + minDist := float64(-1) + s.playerPos.Range(func(key, value any) bool { + p := value.([3]float64) + dist := (p[0]-x)*(p[0]-x) + (p[1]-y)*(p[1]-y) + (p[2]-z)*(p[2]-z) + if minDist < 0 || dist < minDist { + minDist = dist + pos = p + ok = true + } + return true + }) + return +} + // Store returns the on-disk world store, or nil if persistence is disabled. func (s *Server) Store() *world.Store { return s.store } diff --git a/internal/server/spawner.go b/internal/server/spawner.go index f9b4e8c..bd9434f 100644 --- a/internal/server/spawner.go +++ b/internal/server/spawner.go @@ -1,6 +1,7 @@ package server import ( + "math" "math/rand" "time" @@ -20,10 +21,44 @@ func (s *Server) entityTickLoop() { for range ticker.C { all := s.entities.All() for _, e := range all { - // Basic random wandering - e.X += (rand.Float64() - 0.5) * 0.2 - e.Z += (rand.Float64() - 0.5) * 0.2 - e.Yaw += float32((rand.Float64() - 0.5) * 10.0) + // Apply gravity + yBelow := int(e.Y - 0.1) // slightly below the entity + blockBelow := s.chunks.GetBlock(int(e.X), yBelow, int(e.Z)) + + if blockBelow == world.StateAir || blockBelow == world.StateWater { // Air or Water + e.VelocityY -= 80 // gravity acceleration + if e.VelocityY < -3000 { + e.VelocityY = -3000 // terminal velocity + } + } else { + e.VelocityY = 0 + e.Y = float64(yBelow + 1) + + // Basic random wandering or player tracking when on ground + pos, ok := s.NearestPlayer(e.X, e.Y, e.Z) + + if ok && e.TypeName == "minecraft:zombie" { + // Zombies move towards the player + dx := pos[0] - e.X + dz := pos[2] - e.Z + dist := math.Sqrt(dx*dx + dz*dz) + if dist > 1.0 && dist < 32.0 { + e.X += (dx / dist) * 0.15 + e.Z += (dz / dist) * 0.15 + // Simple yaw calculation + e.Yaw = float32(math.Atan2(-dx, dz) * (180 / math.Pi)) + } + } else { + // Random wander + e.X += (rand.Float64() - 0.5) * 0.2 + e.Z += (rand.Float64() - 0.5) * 0.2 + e.Yaw += float32((rand.Float64() - 0.5) * 10.0) + } + } + + if e.VelocityY != 0 { + e.Y += float64(e.VelocityY) / 8000.0 + } } } } diff --git a/internal/world/cache.go b/internal/world/cache.go index b46910b..761205a 100644 --- a/internal/world/cache.go +++ b/internal/world/cache.go @@ -187,6 +187,18 @@ func (c *Cache) Frame(cx, cz int32) []byte { return frame } +// GetBlock returns the block state at world coordinates (x, y, z). +// It loads or generates the chunk if necessary. +func (c *Cache) GetBlock(x, y, z int) uint16 { + if y < MinY || y >= MinY+WorldHeight { + return 0 // StateAir + } + cx := int32(x >> 4) + cz := int32(z >> 4) + ch := c.chunkAt(cx, cz) + return ch.GetBlock(x, y, z) +} + // SetBlock changes the block at world coordinates (x, y, z), invalidating the // affected chunk's cached frame and marking it dirty for autosave. It reports // whether a chunk was actually touched (false if y is out of range).