Add gravity and player-seeking AI to zombies
This commit is contained in:
parent
7af0acd3a3
commit
09a694daf5
5 changed files with 89 additions and 7 deletions
|
|
@ -44,6 +44,8 @@ func (h *handler) serve() {
|
||||||
defer cancel() // stops the streamer when the read loop ends
|
defer cancel() // stops the streamer when the read loop ends
|
||||||
h.ctx = ctx
|
h.ctx = ctx
|
||||||
|
|
||||||
|
defer h.srv.RemovePlayerPosition(h.conn.Profile.Name)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
pkt, err := h.conn.ReadPacket()
|
pkt, err := h.conn.ReadPacket()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,8 @@ func (h *handler) beginPlay() error {
|
||||||
|
|
||||||
// onPlayerMove recenters the streamer when the player crosses into a new chunk.
|
// 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.
|
// 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)
|
cx := int32(int64(math.Floor(x)) >> 4)
|
||||||
cz := int32(int64(math.Floor(z)) >> 4)
|
cz := int32(int64(math.Floor(z)) >> 4)
|
||||||
if h.streamer != nil {
|
if h.streamer != nil {
|
||||||
|
|
@ -230,14 +231,15 @@ func (h *handler) handlePlay(pkt protocol.Packet) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if _, err := r.Float64(); err != nil { // feet Y, unused for streaming
|
y, err := r.Float64() // feet Y
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
z, err := r.Float64()
|
z, err := r.Float64()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return h.onPlayerMove(x, z)
|
return h.onPlayerMove(x, y, z)
|
||||||
|
|
||||||
case protocol.PlayPlayerAction:
|
case protocol.PlayPlayerAction:
|
||||||
return h.handlePlayerAction(pkt)
|
return h.handlePlayerAction(pkt)
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"regionio/internal/protocol"
|
"regionio/internal/protocol"
|
||||||
"regionio/internal/world"
|
"regionio/internal/world"
|
||||||
|
|
@ -55,6 +56,9 @@ type Server struct {
|
||||||
chunks *world.Cache
|
chunks *world.Cache
|
||||||
store *world.Store // nil when persistence is disabled
|
store *world.Store // nil when persistence is disabled
|
||||||
entities *world.EntityManager
|
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
|
// 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.
|
// Entities returns the shared entity manager.
|
||||||
func (s *Server) Entities() *world.EntityManager { return s.entities }
|
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.
|
// Store returns the on-disk world store, or nil if persistence is disabled.
|
||||||
func (s *Server) Store() *world.Store { return s.store }
|
func (s *Server) Store() *world.Store { return s.store }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"math"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -20,10 +21,44 @@ func (s *Server) entityTickLoop() {
|
||||||
for range ticker.C {
|
for range ticker.C {
|
||||||
all := s.entities.All()
|
all := s.entities.All()
|
||||||
for _, e := range all {
|
for _, e := range all {
|
||||||
// Basic random wandering
|
// Apply gravity
|
||||||
e.X += (rand.Float64() - 0.5) * 0.2
|
yBelow := int(e.Y - 0.1) // slightly below the entity
|
||||||
e.Z += (rand.Float64() - 0.5) * 0.2
|
blockBelow := s.chunks.GetBlock(int(e.X), yBelow, int(e.Z))
|
||||||
e.Yaw += float32((rand.Float64() - 0.5) * 10.0)
|
|
||||||
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -187,6 +187,18 @@ func (c *Cache) Frame(cx, cz int32) []byte {
|
||||||
return frame
|
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
|
// 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
|
// 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).
|
// whether a chunk was actually touched (false if y is out of range).
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue