From b5db51800c98f752dea3108e60c9821b8e056a08 Mon Sep 17 00:00:00 2001 From: nicholas-eden Date: Fri, 6 Dec 2019 06:44:52 -0800 Subject: [PATCH] Setup NPCs to follow paths (#243) Change location to contain canonical location, add field to get rounded location for tile rendering. If NPC has path, loop through path. --- d2core/d2scene/game.go | 30 +++++++++++++++--- d2core/npc.go | 17 ++++++++++ d2render/animated_entity.go | 54 ++++++++++++++++++++++---------- d2render/animated_entity_test.go | 54 ++++++++++++++++++++++++++++++++ d2render/d2mapengine/engine.go | 18 ++++++++--- 5 files changed, 148 insertions(+), 25 deletions(-) create mode 100644 d2render/animated_entity_test.go diff --git a/d2core/d2scene/game.go b/d2core/d2scene/game.go index 3ac91fd3..077c3297 100644 --- a/d2core/d2scene/game.go +++ b/d2core/d2scene/game.go @@ -77,7 +77,8 @@ func (v *Game) Load() []func() { v.mapEngine = d2mapengine.CreateMapEngine(v.gameState, v.soundManager, v.fileProvider) // TODO: This needs to be different depending on the act of the player v.mapEngine.GenerateMap(d2enum.RegionAct1Town, 1, 0) - region := v.mapEngine.GetRegion(0) + v.mapEngine.SetRegion(0) + region := v.mapEngine.Region() rx, ry := d2helper.IsoToScreen(region.Region.StartX, region.Region.StartY, 0, 0) v.mapEngine.CenterCameraOn(rx, ry) v.mapEngine.Hero = d2core.CreateHero( @@ -108,13 +109,34 @@ func (v *Game) Update(tickTime float64) { v.mapEngine.Hero.AnimatedEntity.Step(tickTime) } + for _, npc := range v.mapEngine.Region().Region.NPCs { + + if npc.HasPaths && + npc.AnimatedEntity.LocationX == npc.AnimatedEntity.TargetX && + npc.AnimatedEntity.LocationY == npc.AnimatedEntity.TargetY { + // If at the target, set target to the next path. + // TODO: pause at target, figure out how to use Path.Action + path := npc.NextPath() + npc.AnimatedEntity.SetTarget( + float64(path.X), + float64(path.Y), + ) + } + + if npc.AnimatedEntity.LocationX != npc.AnimatedEntity.TargetX || + npc.AnimatedEntity.LocationY != npc.AnimatedEntity.TargetY { + npc.AnimatedEntity.Step(tickTime) + } + + } + if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) { mx, my := ebiten.CursorPosition() px, py := d2helper.ScreenToIso(float64(mx)-v.mapEngine.OffsetX, float64(my)-v.mapEngine.OffsetY) - v.mapEngine.Hero.AnimatedEntity.SetTarget(px, py) + v.mapEngine.Hero.AnimatedEntity.SetTarget(px*5, py*5) } - rx, ry := d2helper.IsoToScreen(v.mapEngine.Hero.AnimatedEntity.LocationX, v.mapEngine.Hero.AnimatedEntity.LocationY, 0, 0) - v.mapEngine.CenterCameraOn(float64(rx), float64(ry)) + rx, ry := d2helper.IsoToScreen(v.mapEngine.Hero.AnimatedEntity.LocationX/5, v.mapEngine.Hero.AnimatedEntity.LocationY/5, 0, 0) + v.mapEngine.CenterCameraOn(rx, ry) } diff --git a/d2core/npc.go b/d2core/npc.go index b5be954f..adaa9a4a 100644 --- a/d2core/npc.go +++ b/d2core/npc.go @@ -11,19 +11,36 @@ import ( type NPC struct { AnimatedEntity d2render.AnimatedEntity + HasPaths bool Paths []d2common.Path + path int } func CreateNPC(x, y int32, object *d2datadict.ObjectLookupRecord, fileProvider d2interface.FileProvider, direction int) *NPC { result := &NPC{ AnimatedEntity: d2render.CreateAnimatedEntity(x, y, object, fileProvider, d2enum.Units), + HasPaths: false, } result.AnimatedEntity.SetMode(object.Mode, object.Class, direction) return result } +func (v *NPC) Path() d2common.Path { + return v.Paths[v.path] +} + +func (v *NPC) NextPath() d2common.Path { + v.path++ + if v.path == len(v.Paths) { + v.path = 0 + } + + return v.Paths[v.path] +} + func (v *NPC) SetPaths(paths []d2common.Path) { v.Paths = paths + v.HasPaths = len(paths) > 0 } func (v *NPC) Render(target *ebiten.Image, offsetX, offsetY int) { diff --git a/d2render/animated_entity.go b/d2render/animated_entity.go index 6355e95d..949a3a1a 100644 --- a/d2render/animated_entity.go +++ b/d2render/animated_entity.go @@ -33,12 +33,11 @@ type LayerCacheEntry struct { // AnimatedEntity represents an entity on the map that can be animated type AnimatedEntity struct { - fileProvider d2interface.FileProvider - // LocationX represents the tile X position of the entity - LocationX float64 - // LocationY represents the tile Y position of the entity - subcellX, subcellY float64 // Subcell coordinates within the current tile + fileProvider d2interface.FileProvider + LocationX float64 LocationY float64 + TileX, TileY int // Coordinates of the tile the unit is within + subcellX, subcellY float64 // Subcell coordinates within the current tile dccLayers map[string]d2dcc.DCC Cof *d2cof.COF palette d2enum.PaletteType @@ -72,13 +71,15 @@ func CreateAnimatedEntity(x, y int32, object *d2datadict.ObjectLookupRecord, fil //frameLocations: []d2common.Rectangle{}, } result.dccLayers = make(map[string]d2dcc.DCC) - result.LocationX = float64(x) / 5 - result.LocationY = float64(y) / 5 + result.LocationX = float64(x) + result.LocationY = float64(y) result.TargetX = result.LocationX result.TargetY = result.LocationY - result.subcellX = 1 + math.Mod(float64(x), 5) - result.subcellY = 1 + math.Mod(float64(y), 5) + result.TileX = int(result.LocationX / 5) + result.TileY = int(result.LocationY / 5) + result.subcellX = 1 + math.Mod(result.LocationX, 5) + result.subcellY = 1 + math.Mod(result.LocationY, 5) return result } @@ -309,7 +310,7 @@ func (v AnimatedEntity) GetDirection() int { } func (v *AnimatedEntity) getStepLength(tickTime float64) (float64, float64) { - speed := 2.5 + speed := 6.0 length := tickTime * speed angle := 359 - d2helper.GetAngleBetween( @@ -318,7 +319,7 @@ func (v *AnimatedEntity) getStepLength(tickTime float64) (float64, float64) { v.TargetX, v.TargetY, ) - radians := (math.Pi / 180.0) * float64(angle) + radians := (math.Pi / 180.0) * angle oneStepX := length * math.Cos(radians) oneStepY := length * math.Sin(radians) return oneStepX, oneStepY @@ -339,9 +340,15 @@ func (v *AnimatedEntity) Step(tickTime float64) { if v.LocationY != v.TargetY { v.LocationY += stepY } + + v.subcellX = 1 + math.Mod(v.LocationX, 5) + v.subcellY = 1 + math.Mod(v.LocationY, 5) + v.TileX = int(v.LocationX / 5) + v.TileY = int(v.LocationY / 5) + if v.LocationX == v.TargetX && v.LocationY == v.TargetY { - if v.animationMode != d2enum.AnimationModePlayerTownNeutral.String() { - v.SetMode(d2enum.AnimationModePlayerTownNeutral.String(), v.weaponClass, v.direction) + if v.animationMode != d2enum.AnimationModeObjectNeutral.String() { + v.SetMode(d2enum.AnimationModeObjectNeutral.String(), v.weaponClass, v.direction) } } } @@ -354,13 +361,28 @@ func (v *AnimatedEntity) SetTarget(tx, ty float64) { tx, ty, ) - newAnimationMode := d2enum.AnimationModePlayerTownNeutral.String() + // TODO: Check if is in town and if is player. + newAnimationMode := d2enum.AnimationModeMonsterWalk.String() if tx != v.LocationX || ty != v.LocationY { v.TargetX, v.TargetY = tx, ty - newAnimationMode = d2enum.AnimationModePlayerTownWalk.String() + newAnimationMode = d2enum.AnimationModeMonsterWalk.String() } - newDirection := int((float64(angle) / 360.0) * 16.0) + + newDirection := angleToDirection(angle, v.Cof.NumberOfDirections) if newDirection != v.GetDirection() || newAnimationMode != v.animationMode { v.SetMode(newAnimationMode, v.weaponClass, newDirection) } } + +func angleToDirection(angle float64, numberOfDirections int) int { + degreesPerDirection := 360.0 / float64(numberOfDirections) + offset := 45.0 - (degreesPerDirection / 2) + newDirection := int((angle - offset) / degreesPerDirection) + if newDirection >= numberOfDirections { + newDirection = newDirection - numberOfDirections + } else if newDirection < 0 { + newDirection = numberOfDirections + newDirection + } + + return newDirection +} diff --git a/d2render/animated_entity_test.go b/d2render/animated_entity_test.go new file mode 100644 index 00000000..a7ea724a --- /dev/null +++ b/d2render/animated_entity_test.go @@ -0,0 +1,54 @@ +package d2render + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestAngleToDirection_16Directions(t *testing.T) { + + numberOfDirections := 16 + + angle := 45.0 + for i := 0; i < numberOfDirections; i++ { + assert.Equal(t, i, angleToDirection(angle, numberOfDirections)) + angle += 22.5 + } + + angle = 50.0 + for i := 0; i < numberOfDirections; i++ { + assert.Equal(t, i, angleToDirection(angle, numberOfDirections)) + angle += 22.5 + } + + angle = 40.0 + for i := 0; i < numberOfDirections; i++ { + assert.Equal(t, i, angleToDirection(angle, numberOfDirections)) + angle += 22.5 + } + +} + +func TestAngleToDirection_8Directions(t *testing.T) { + + numberOfDirections := 8 + + angle := 45.0 + for i := 0; i < numberOfDirections; i++ { + assert.Equal(t, i, angleToDirection(angle, numberOfDirections)) + angle += 45 + } + + angle = 50.0 + for i := 0; i < numberOfDirections; i++ { + assert.Equal(t, i, angleToDirection(angle, numberOfDirections)) + angle += 45 + } + + angle = 40.0 + for i := 0; i < numberOfDirections; i++ { + assert.Equal(t, i, angleToDirection(angle, numberOfDirections)) + angle += 45 + } + +} diff --git a/d2render/d2mapengine/engine.go b/d2render/d2mapengine/engine.go index ad6a7af8..0fa2144b 100644 --- a/d2render/d2mapengine/engine.go +++ b/d2render/d2mapengine/engine.go @@ -3,7 +3,6 @@ package d2mapengine import ( "fmt" "image/color" - "math" "strings" "github.com/OpenDiablo2/D2Shared/d2common/d2enum" @@ -35,6 +34,7 @@ type Engine struct { soundManager *d2audio.Manager gameState *d2core.GameState fileProvider d2interface.FileProvider + region int regions []EngineRegion OffsetX float64 OffsetY float64 @@ -52,6 +52,14 @@ func CreateMapEngine(gameState *d2core.GameState, soundManager *d2audio.Manager, return result } +func (v *Engine) Region() *EngineRegion { + return &v.regions[v.region] +} + +func (v *Engine) SetRegion(region int) { + v.region = region +} + func (v *Engine) GetRegion(regionIndex int) *EngineRegion { return &v.regions[regionIndex] } @@ -244,17 +252,17 @@ func (v *Engine) RenderPass2(region *Region, offX, offY, x, y int, target *ebite } for _, obj := range region.AnimationEntities { - if int(math.Floor(obj.LocationX)) == x && int(math.Floor(obj.LocationY)) == y { + if obj.TileX == x && obj.TileY == y { obj.Render(target, offX+int(v.OffsetX), offY+int(v.OffsetY)) } } for _, npc := range region.NPCs { - if int(math.Floor(npc.AnimatedEntity.LocationX)) == x && int(math.Floor(npc.AnimatedEntity.LocationY)) == y { + if npc.AnimatedEntity.TileX == x && npc.AnimatedEntity.TileY == y { npc.Render(target, offX+int(v.OffsetX), offY+int(v.OffsetY)) } } - if v.Hero != nil && int(v.Hero.AnimatedEntity.LocationX) == x && int(v.Hero.AnimatedEntity.LocationY) == y { - v.Hero.Render(target, 400, 300) + if v.Hero != nil && v.Hero.AnimatedEntity.TileX == x && v.Hero.AnimatedEntity.TileY == y { + v.Hero.Render(target, offX+int(v.OffsetX), offY+int(v.OffsetY)) } }