1
1
mirror of https://github.com/OpenDiablo2/OpenDiablo2 synced 2024-07-03 11:55:22 +00:00

Map entity rework - vectors in mapEntity (#579)

* Fixed NaN on normalize 0 vector.

* Tentative implementation in getStepLength.

* mapEntity.TileX/Y removed.

* Fixed Position and Target not being initialised in createMapEntity.

* mapEntity.Position is no longer embedded.

* mapEntity.LocationX/Y no longer used outside map_entity.go.

* mapEntity.subCellX/Y removed.

* mapEntity.LocationX/Y removed.

* mapEntity.OffsetX/Y and TargetX/Y removed.

* Direction method added to Vector, returns 0-64 direction.

* Moved Vector.Direction() to Position.DirectionTo and refactored.

* Renamed RenderOffset from SubCell and tested IsZero.

* d2math.WrapInt added for use with directions.

* Tidied up position and tests.

* Refactored d2common.AdjustWithRemainder into d2mapEntity.

* Tidying up d2mapEntity.

* Final cleanup.

* Lint warnings.

* Spelling correction.
This commit is contained in:
danhale-git 2020-07-13 14:06:50 +01:00 committed by GitHub
parent 41e7a5b28b
commit 894c60f77a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 280 additions and 251 deletions

View File

@ -3,16 +3,24 @@ package d2vector
import ( import (
"fmt" "fmt"
"math" "math"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2math"
) )
const subTilesPerTile float64 = 5 const (
subTilesPerTile float64 = 5 // The number of sub tiles that make up one map tile.
entityDirectionCount float64 = 64 // The Diablo equivalent of 360 degrees when dealing with entity rotation.
entityDirectionIncrement float64 = 8 // One 8th of 64. There 8 possible facing directions for entities.
// Note: there should be 16 facing directions for entities. See Position.DirectionTo()
)
// Position is a vector in world space. The stored value is the one returned by Position.World() // Position is a vector in world space. The stored value is the sub tile position.
type Position struct { type Position struct {
Vector Vector
} }
// NewPosition creates a new Position at the given float64 world position. // NewPosition returns a Position struct with the given sub tile coordinates where 1 = 1 sub tile, with a fractional
// offset.
func NewPosition(x, y float64) Position { func NewPosition(x, y float64) Position {
p := Position{NewVector(x, y)} p := Position{NewVector(x, y)}
p.checkValues() p.checkValues()
@ -20,15 +28,7 @@ func NewPosition(x, y float64) Position {
return p return p
} }
// EntityPosition returns a Position struct based on the given entity spawn point. // Set sets this position to the given sub tile coordinates where 1 = 1 sub tile, with a fractional offset.
// The value given should be the one set in d2mapstamp.Stamp.Entities:
// (tileOffsetX*5)+object.X, (tileOffsetY*5)+object.Y
// TODO: This probably doesn't support positions in between sub tiles so will only be suitable for spawning entities from map generation, not for multiplayer syncing.
func EntityPosition(x, y int) Position {
return NewPosition(float64(x)/5, float64(y)/5)
}
// Set sets this position to the given x and y values.
func (p *Position) Set(x, y float64) { func (p *Position) Set(x, y float64) {
p.x, p.y = x, y p.x, p.y = x, y
p.checkValues() p.checkValues()
@ -44,48 +44,45 @@ func (p *Position) checkValues() {
} }
} }
// World is the position, where 1 = one map tile. // World is the exact position where 1 = one map tile and 0.2 = one sub tile.
// unused
func (p *Position) World() *Vector { func (p *Position) World() *Vector {
return &p.Vector c := p.Clone()
return c.DivideScalar(subTilesPerTile)
} }
// Tile is the tile position, always a whole number. (tileX, tileY) // Tile is the position of the current map tile. It is the floor of World(), always a whole number.
func (p *Position) Tile() *Vector { func (p *Position) Tile() *Vector {
c := p.World().Clone() return p.World().Floor()
return c.Floor()
} }
// TileOffset is the offset from the tile position, always < 1. // RenderOffset is the offset in sub tiles from the curren tile, + 1. This places the vector at the bottom vertex of an
// unused // isometric diamond visually representing one sub tile. Sub tile indices increase to the lower right diagonal ('down')
func (p *Position) TileOffset() *Vector { // and to the lower left diagonal ('left') of the isometric grid. This renders the target one index above which visually
c := p.World().Clone() // is one tile below.
return c.Subtract(p.Tile()) func (p *Position) RenderOffset() *Vector {
return p.subTileOffset().AddScalar(1)
} }
// SubWorld is the position, where 5 = one map tile. (locationX, locationY) // SubTileOffset is the offset from the current map tile in sub tiles.
func (p *Position) SubWorld() *Vector { func (p *Position) subTileOffset() *Vector {
c := p.World().Clone() t := p.Tile().Scale(subTilesPerTile)
return c.Scale(subTilesPerTile) c := p.Clone()
return c.Subtract(t)
} }
// SubTile is the tile position in sub tiles, always a multiple of 5. // DirectionTo returns the entity direction from this vector to the given vector.
// unused func (v *Vector) DirectionTo(target Vector) int {
func (p *Position) SubTile() *Vector { direction := target.Clone()
return p.Tile().Scale(subTilesPerTile) direction.Subtract(v)
}
// SubTileOffset is the offset from the sub tile position in sub tiles, always < 1. angle := direction.SignedAngle(VectorRight())
// unused radiansPerDirection := d2math.RadFull / entityDirectionCount
func (p *Position) SubTileOffset() *Vector {
return p.SubWorld().Subtract(p.SubTile())
}
// TODO: understand this and maybe improve/remove it // Note: The direction is always one increment out so we must subtract one increment.
// SubTileOffset() + 1. It's used for rendering. It seems to always do this: // This might not work when we implement all 16 directions (as of this writing entities can only face one of 8
// v.offsetX+int((v.subcellX-v.subcellY)*16), // directions). See entityDirectionIncrement.
// v.offsetY+int(((v.subcellX+v.subcellY)*8)-5), newDirection := int((angle / radiansPerDirection) - entityDirectionIncrement)
// ^ Maybe similar to Viewport.OrthoToWorld? (subCellX, subCellY)
func (p *Position) SubCell() *Vector { return d2math.WrapInt(newDirection, int(entityDirectionCount))
return p.SubTileOffset().AddScalar(1)
} }

View File

@ -6,18 +6,18 @@ import (
"testing" "testing"
) )
func TestEntityPosition(t *testing.T) { func TestNewPosition(t *testing.T) {
x, y := rand.Intn(1000), rand.Intn(1000) x, y := rand.Intn(1000), rand.Intn(1000)
pos := EntityPosition(x, y)
locX, locY := float64(x), float64(y) locX, locY := float64(x), float64(y)
pos := NewPosition(locX, locY)
// old coordinate values Position equivalent // old coordinate values Position equivalent
locationX := locX // .SubWord().X() locationX := locX // .SubWord().X()
locationY := locY // .SubWord().Y() locationY := locY // .SubWord().Y()
tileX := float64(x / 5) // .Tile().X() tileX := float64(x / 5) // .Tile().X()
tileY := float64(y / 5) // .Tile().Y() tileY := float64(y / 5) // .Tile().Y()
subcellX := 1 + math.Mod(locX, 5) // .SubCell().X() subcellX := 1 + math.Mod(locX, 5) // .RenderOffset().X()
subcellY := 1 + math.Mod(locY, 5) // .SubCell().Y() subcellY := 1 + math.Mod(locY, 5) // .RenderOffset().Y()
want := NewVector(tileX, tileY) want := NewVector(tileX, tileY)
got := pos.Tile() got := pos.Tile()
@ -27,14 +27,14 @@ func TestEntityPosition(t *testing.T) {
} }
want = NewVector(subcellX, subcellY) want = NewVector(subcellX, subcellY)
got = pos.SubCell() got = pos.RenderOffset()
if !got.Equals(want) { if !got.Equals(want) {
t.Errorf("sub cell position should match old value: got %s: want %s", got, want) t.Errorf("render offset position should match old value: got %s: want %s", got, want)
} }
want = NewVector(locationX, locationY) want = NewVector(locationX, locationY)
got = pos.SubWorld() got = &pos.Vector
if !got.Equals(want) { if !got.Equals(want) {
t.Errorf("sub tile position should match old value: got %s: want %s", got, want) t.Errorf("sub tile position should match old value: got %s: want %s", got, want)
@ -51,47 +51,29 @@ func validate(description string, t *testing.T, original, got, want, unchanged V
} }
} }
func TestTile(t *testing.T) { func TestPosition_World(t *testing.T) {
p := NewPosition(1.6, 1.6) p := NewPosition(5, 10)
unchanged := p.Clone()
got := p.World()
want := NewVector(1, 2)
validate("world position", t, p.Vector, *got, want, unchanged)
}
func TestPosition_Tile(t *testing.T) {
p := NewPosition(23, 24)
unchanged := p.Clone()
got := p.Tile() got := p.Tile()
want := NewVector(1, 1) want := NewVector(4, 4)
unchanged := NewVector(1.6, 1.6)
validate("tile position", t, p.Vector, *got, want, unchanged) validate("tile position", t, p.Vector, *got, want, unchanged)
} }
func TestTileOffset(t *testing.T) { func TestPosition_RenderOffset(t *testing.T) {
p := NewPosition(1.6, 1.6) p := NewPosition(12.1, 14.2)
got := p.TileOffset() unchanged := p.Clone()
want := NewVector(0.6, 0.6) got := p.RenderOffset()
unchanged := NewVector(1.6, 1.6) want := NewVector(3.1, 5.2)
validate("tile offset", t, p.Vector, *got, want, unchanged)
}
func TestSubWorld(t *testing.T) {
p := NewPosition(1, 1)
got := p.SubWorld()
want := NewVector(5, 5)
unchanged := NewVector(1, 1)
validate("sub tile world position", t, p.Vector, *got, want, unchanged)
}
func TestSubTile(t *testing.T) {
p := NewPosition(1, 1)
got := p.SubTile()
want := NewVector(5, 5)
unchanged := NewVector(1, 1)
validate("sub tile with offset", t, p.Vector, *got, want, unchanged)
}
func TestSubTileOffset(t *testing.T) {
p := NewPosition(1.1, 1.1)
got := p.SubTileOffset()
want := NewVector(0.5, 0.5)
unchanged := NewVector(1.1, 1.1)
validate("offset from sub tile", t, p.Vector, *got, want, unchanged) validate("offset from sub tile", t, p.Vector, *got, want, unchanged)
} }

View File

@ -49,6 +49,11 @@ func (v *Vector) CompareApprox(o Vector) (x, y int) {
d2math.CompareFloat64Fuzzy(v.y, o.y) d2math.CompareFloat64Fuzzy(v.y, o.y)
} }
// IsZero returns true if this vector's values are both exactly zero.
func (v *Vector) IsZero() bool {
return v.x == 0 && v.y == 0
}
// Set the vector values to the given float64 values. // Set the vector values to the given float64 values.
func (v *Vector) Set(x, y float64) *Vector { func (v *Vector) Set(x, y float64) *Vector {
v.x = x v.x = x
@ -95,7 +100,7 @@ func (v *Vector) Add(o *Vector) *Vector {
return v return v
} }
// AddScalar the given vector to this vector. // AddScalar the given value to both values of this vector.
func (v *Vector) AddScalar(s float64) *Vector { func (v *Vector) AddScalar(s float64) *Vector {
v.x += s v.x += s
v.y += s v.y += s
@ -135,7 +140,7 @@ func (v *Vector) Divide(o *Vector) *Vector {
return v return v
} }
// DivideScalar divides this vector by the given float64 value. // DivideScalar divides both values of this vector by the given value.
func (v *Vector) DivideScalar(s float64) *Vector { func (v *Vector) DivideScalar(s float64) *Vector {
v.x /= s v.x /= s
v.y /= s v.y /= s
@ -219,6 +224,10 @@ func (v *Vector) Cross(o Vector) float64 {
// Normalize sets the vector length to 1 without changing the direction. The normalized vector may be scaled by the // Normalize sets the vector length to 1 without changing the direction. The normalized vector may be scaled by the
// float64 return value to restore it's original length. // float64 return value to restore it's original length.
func (v *Vector) Normalize() float64 { func (v *Vector) Normalize() float64 {
if v.IsZero() {
return 0
}
multiplier := 1 / v.Length() multiplier := 1 / v.Length()
v.Scale(multiplier) v.Scale(multiplier)
@ -228,6 +237,10 @@ func (v *Vector) Normalize() float64 {
// Angle computes the unsigned angle in radians from this vector to the given vector. This angle will never exceed half // Angle computes the unsigned angle in radians from this vector to the given vector. This angle will never exceed half
// a full circle. For angles describing a full circumference use SignedAngle. // a full circle. For angles describing a full circumference use SignedAngle.
func (v *Vector) Angle(o Vector) float64 { func (v *Vector) Angle(o Vector) float64 {
if v.IsZero() || o.IsZero() {
return 0
}
from := v.Clone() from := v.Clone()
from.Normalize() from.Normalize()

View File

@ -77,7 +77,7 @@ func TestEqualsF(t *testing.T) {
} }
} }
func TestCompareF(t *testing.T) { func TestCompareApprox(t *testing.T) {
subEpsilon := d2math.Epsilon / 3 subEpsilon := d2math.Epsilon / 3
f := NewVector(1+subEpsilon, 1+subEpsilon) f := NewVector(1+subEpsilon, 1+subEpsilon)
@ -108,6 +108,20 @@ func TestCompareF(t *testing.T) {
} }
} }
func TestIsZero(t *testing.T) {
testIsZero(NewVector(0, 0), true, t)
testIsZero(NewVector(1, 0), false, t)
testIsZero(NewVector(0, 1), false, t)
testIsZero(NewVector(1, 1), false, t)
}
func testIsZero(v Vector, want bool, t *testing.T) {
got := v.IsZero()
if got != want {
t.Errorf("%s is zero: want %t: got %t", v, want, got)
}
}
func TestSet(t *testing.T) { func TestSet(t *testing.T) {
v := NewVector(1, 1) v := NewVector(1, 1)
want := NewVector(2, 3) want := NewVector(2, 3)
@ -337,6 +351,14 @@ func TestNormalize(t *testing.T) {
v.Scale(reverse) v.Scale(reverse)
evaluateVector(fmt.Sprintf("reverse normalizing of %s", c), want, v, t) evaluateVector(fmt.Sprintf("reverse normalizing of %s", c), want, v, t)
v = NewVector(0, 0)
c = v.Clone()
want = NewVector(0, 0)
v.Normalize()
evaluateVector(fmt.Sprintf("normalize zero vector should do nothing %s", c), want, v, t)
} }
func TestAngle(t *testing.T) { func TestAngle(t *testing.T) {

View File

@ -1,2 +1,2 @@
// Package d2math provides mathmatical functions not included in Golang's standard math library. // Package d2math provides mathematical functions not included in Golang's standard math library.
package d2math package d2math

View File

@ -14,6 +14,11 @@ const (
RadFull float64 = 6.283185253783088 RadFull float64 = 6.283185253783088
) )
// EqualsApprox returns true if the difference between a and b is less than Epsilon.
func EqualsApprox(a, b float64) bool {
return math.Abs(a-b) < Epsilon
}
// CompareFloat64Fuzzy returns an integer between -1 and 1 describing // CompareFloat64Fuzzy returns an integer between -1 and 1 describing
// the comparison of floats a and b. 0 will be returned if the // the comparison of floats a and b. 0 will be returned if the
// absolute difference between a and b is less than Epsilon. // absolute difference between a and b is less than Epsilon.
@ -65,3 +70,14 @@ func Lerp(a, b, x float64) float64 {
func Unlerp(a, b, x float64) float64 { func Unlerp(a, b, x float64) float64 {
return (x - a) / (b - a) return (x - a) / (b - a)
} }
// WrapInt wraps x to between 0 and max. For example WrapInt(450, 360) would return 90.
func WrapInt(x, max int) int {
wrapped := x % max
if wrapped < 0 {
return max + wrapped
}
return wrapped
}

View File

@ -4,6 +4,24 @@ import (
"testing" "testing"
) )
func TestEqualsApprox(t *testing.T) {
subEpsilon := Epsilon / 3
a, b := 1+subEpsilon, 1.0
got := EqualsApprox(a, b)
if !got {
t.Errorf("compare %.2f and %.2f: wanted %t: got %t", a, b, true, got)
}
a, b = 1+Epsilon, 1.0
got = EqualsApprox(a, b)
if !got {
t.Errorf("compare %.2f and %.2f: wanted %t: got %t", a, b, false, got)
}
}
func TestCompareFloat64Fuzzy(t *testing.T) { func TestCompareFloat64Fuzzy(t *testing.T) {
subEpsilon := Epsilon / 3 subEpsilon := Epsilon / 3
@ -111,3 +129,39 @@ func TestUnlerp(t *testing.T) {
t.Errorf(d, x, a, b, want, got) t.Errorf(d, x, a, b, want, got)
} }
} }
func TestWrapInt(t *testing.T) {
want := 50
a, b := 1050, 100
got := WrapInt(a, b)
d := "wrap %d between 0 and %d: want %d: got %d"
if got != want {
t.Errorf(d, a, b, want, got)
}
want = 270
a, b = -1170, 360
got = WrapInt(a, b)
if got != want {
t.Errorf(d, a, b, want, got)
}
want = 270
a, b = 270, 360
got = WrapInt(a, b)
if got != want {
t.Errorf(d, a, b, want, got)
}
want = 90
a, b = -270, 360
got = WrapInt(a, b)
if got != want {
t.Errorf(d, a, b, want, got)
}
}

View File

@ -73,25 +73,6 @@ func MinInt32(a, b int32) int32 {
// ScreenToIso converts screenspace coordinates to isometric coordinates // ScreenToIso converts screenspace coordinates to isometric coordinates
// GetAngleBetween returns the angle between two points. 0deg is facing to the right.
func GetAngleBetween(p1X, p1Y, p2X, p2Y float64) int {
deltaY := p1Y - p2Y
deltaX := p2X - p1X
result := math.Atan2(deltaY, deltaX) * (180 / math.Pi)
iResult := int(result)
for iResult < 0 {
iResult += 360
}
for iResult >= 360 {
iResult -= 360
}
return iResult
}
// GetRadiansBetween returns the radians between two points. 0rad is facing to the right. // GetRadiansBetween returns the radians between two points. 0rad is facing to the right.
func GetRadiansBetween(p1X, p1Y, p2X, p2Y float64) float64 { func GetRadiansBetween(p1X, p1Y, p2X, p2Y float64) float64 {
deltaY := p2Y - p1Y deltaY := p2Y - p1Y
@ -104,34 +85,3 @@ func GetRadiansBetween(p1X, p1Y, p2X, p2Y float64) float64 {
func AlmostEqual(a, b, threshold float64) bool { func AlmostEqual(a, b, threshold float64) bool {
return math.Abs(a-b) <= threshold return math.Abs(a-b) <= threshold
} }
// AdjustWithRemainder returns the new adjusted value, as well as any remaining amount after the max
func AdjustWithRemainder(sourceValue, adjustment, targetvalue float64) (newValue, remainder float64) {
if adjustment == 0 || math.Abs(adjustment) < 0.000001 {
return sourceValue, 0
}
adjustNegative := adjustment < 0.0
maxNegative := targetvalue-sourceValue < 0.0
if adjustNegative != maxNegative {
// FIXME: This shouldn't happen but it happens all the time..
return sourceValue, 0
}
finalValue := sourceValue + adjustment
if !adjustNegative {
if finalValue > targetvalue {
diff := finalValue - targetvalue
return targetvalue, diff
}
return finalValue, 0
}
if finalValue < targetvalue {
return targetvalue, finalValue - targetvalue
}
return finalValue, 0
}

View File

@ -27,10 +27,12 @@ func CreateAnimatedEntity(x, y int, animation d2interface.Animation) *AnimatedEn
// Render draws this animated entity onto the target // Render draws this animated entity onto the target
func (ae *AnimatedEntity) Render(target d2interface.Surface) { func (ae *AnimatedEntity) Render(target d2interface.Surface) {
renderOffset := ae.Position.RenderOffset()
target.PushTranslation( target.PushTranslation(
ae.offsetX+int((ae.subcellX-ae.subcellY)*16), int((renderOffset.X()-renderOffset.Y())*16),
ae.offsetY+int(((ae.subcellX+ae.subcellY)*8)-5), int(((renderOffset.X()+renderOffset.Y())*8)-5),
) )
defer target.Pop() defer target.Pop()
ae.animation.Render(target) ae.animation.Render(target)
} }

View File

@ -1,25 +1,20 @@
package d2mapentity package d2mapentity
import ( import (
"math" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2math/d2vector"
"github.com/OpenDiablo2/OpenDiablo2/d2common" "github.com/OpenDiablo2/OpenDiablo2/d2common"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2astar" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2astar"
) )
// mapEntity represents an entity on the map that can be animated // mapEntity represents an entity on the map that can be animated
// TODO: Has a coordinate (issue #456)
type mapEntity struct { type mapEntity struct {
LocationX float64 Position d2vector.Position
LocationY float64 Target d2vector.Position
TileX, TileY int // Coordinates of the tile the unit is within
subcellX, subcellY float64 // Subcell coordinates within the current tile Speed float64
offsetX, offsetY int path []d2astar.Pather
TargetX float64 drawLayer int
TargetY float64
Speed float64
path []d2astar.Pather
drawLayer int
done func() done func()
directioner func(direction int) directioner func(direction int)
@ -30,14 +25,8 @@ func createMapEntity(x, y int) mapEntity {
locX, locY := float64(x), float64(y) locX, locY := float64(x), float64(y)
return mapEntity{ return mapEntity{
LocationX: locX, Position: d2vector.NewPosition(locX, locY),
LocationY: locY, Target: d2vector.NewPosition(locX, locY),
TargetX: locX,
TargetY: locY,
TileX: x / 5,
TileY: y / 5,
subcellX: 1 + math.Mod(locX, 5),
subcellY: 1 + math.Mod(locY, 5),
Speed: 6, Speed: 6,
drawLayer: 0, drawLayer: 0,
path: []d2astar.Pather{}, path: []d2astar.Pather{},
@ -71,25 +60,9 @@ func (m *mapEntity) GetSpeed() float64 {
return m.Speed return m.Speed
} }
func (m *mapEntity) getStepLength(tickTime float64) (float64, float64) { // IsAtTarget returns true if the distance between entity and target is almost zero.
length := tickTime * m.Speed
angle := 359 - d2common.GetAngleBetween(
m.LocationX,
m.LocationY,
m.TargetX,
m.TargetY,
)
radians := (math.Pi / 180.0) * float64(angle)
oneStepX := length * math.Cos(radians)
oneStepY := length * math.Sin(radians)
return oneStepX, oneStepY
}
// IsAtTarget returns true if the entity is within a 0.0002 square of it's target and has a path.
func (m *mapEntity) IsAtTarget() bool { func (m *mapEntity) IsAtTarget() bool {
return math.Abs(m.LocationX-m.TargetX) < 0.0001 && math.Abs(m.LocationY-m.TargetY) < 0.0001 && !m.HasPathFinding() return m.Position.EqualsApprox(m.Target.Vector) && !m.HasPathFinding()
} }
// Step moves the entity along it's path by one tick. If the path is complete it calls entity.done() then returns. // Step moves the entity along it's path by one tick. If the path is complete it calls entity.done() then returns.
@ -103,27 +76,16 @@ func (m *mapEntity) Step(tickTime float64) {
return return
} }
stepX, stepY := m.getStepLength(tickTime) velocity := m.velocity(tickTime)
for { for {
if d2common.AlmostEqual(m.LocationX-m.TargetX, 0, 0.0001) { // Add the velocity to the position and set new velocity to remainder
stepX = 0 applyVelocity(&m.Position.Vector, &velocity, &m.Target.Vector)
}
if d2common.AlmostEqual(m.LocationY-m.TargetY, 0, 0.0001) { // New position is at target
stepY = 0 if m.Position.EqualsApprox(m.Target.Vector) {
}
m.LocationX, stepX = d2common.AdjustWithRemainder(m.LocationX, stepX, m.TargetX)
m.LocationY, stepY = d2common.AdjustWithRemainder(m.LocationY, stepY, m.TargetY)
m.subcellX = 1 + math.Mod(m.LocationX, 5)
m.subcellY = 1 + math.Mod(m.LocationY, 5)
m.TileX = int(m.LocationX / 5)
m.TileY = int(m.LocationY / 5)
if d2common.AlmostEqual(m.LocationX, m.TargetX, 0.01) && d2common.AlmostEqual(m.LocationY, m.TargetY, 0.01) {
if len(m.path) > 0 { if len(m.path) > 0 {
// Set next path node
m.SetTarget(m.path[0].(*d2common.PathTile).X*5, m.path[0].(*d2common.PathTile).Y*5, m.done) m.SetTarget(m.path[0].(*d2common.PathTile).X*5, m.path[0].(*d2common.PathTile).Y*5, m.done)
if len(m.path) > 1 { if len(m.path) > 1 {
@ -132,21 +94,61 @@ func (m *mapEntity) Step(tickTime float64) {
m.path = []d2astar.Pather{} m.path = []d2astar.Pather{}
} }
} else { } else {
m.LocationX = m.TargetX // End of path.
m.LocationY = m.TargetY m.Position.Copy(&m.Target.Vector)
m.subcellX = 1 + math.Mod(m.LocationX, 5)
m.subcellY = 1 + math.Mod(m.LocationY, 5)
m.TileX = int(m.LocationX / 5)
m.TileY = int(m.LocationY / 5)
} }
} }
if stepX == 0 && stepY == 0 { if velocity.IsZero() {
break break
} }
} }
} }
// velocity returns a vector describing the change in position this tick.
func (m *mapEntity) velocity(tickTime float64) d2vector.Vector {
length := tickTime * m.Speed
v := m.Target.Vector.Clone()
v.Subtract(&m.Position.Vector)
v.SetLength(length)
return v
}
// applyVelocity adds velocity to position. If the new position extends beyond target from the original position, the
// position is set to the target and velocity is set to the overshot amount.
func applyVelocity(position, velocity, target *d2vector.Vector) {
// Set velocity values to zero if almost zero
x, y := position.CompareApprox(*target)
vx, vy := velocity.X(), velocity.Y()
if x == 0 {
vx = 0
}
if y == 0 {
vy = 0
}
velocity.Set(vx, vy)
dest := position.Clone()
dest.Add(velocity)
destDistance := position.Distance(dest)
targetDistance := position.Distance(*target)
if destDistance > targetDistance {
// Destination overshot target. Set position to target and velocity to overshot amount.
position.Copy(target)
velocity.Copy(dest.Subtract(target))
} else {
// At or before target, set position to destination and velocity to zero.
position.Copy(&dest)
velocity.Set(0, 0)
}
}
// HasPathFinding returns false if the length of the entity movement path is 0. // HasPathFinding returns false if the length of the entity movement path is 0.
func (m *mapEntity) HasPathFinding() bool { func (m *mapEntity) HasPathFinding() bool {
return len(m.path) > 0 return len(m.path) > 0
@ -154,43 +156,26 @@ func (m *mapEntity) HasPathFinding() bool {
// SetTarget sets target coordinates and changes animation based on proximity and direction. // SetTarget sets target coordinates and changes animation based on proximity and direction.
func (m *mapEntity) SetTarget(tx, ty float64, done func()) { func (m *mapEntity) SetTarget(tx, ty float64, done func()) {
m.TargetX, m.TargetY = tx, ty m.Target.Set(tx, ty)
m.done = done m.done = done
if m.directioner != nil { if m.directioner != nil {
angle := 359 - d2common.GetAngleBetween( d := m.Position.DirectionTo(m.Target.Vector)
m.LocationX,
m.LocationY, m.directioner(d)
tx,
ty,
)
m.directioner(angleToDirection(float64(angle)))
} }
} }
func angleToDirection(angle float64) int { // GetPosition returns the entity's current tile position, always a whole number.
degreesPerDirection := 360.0 / 64.0 func (m *mapEntity) GetPosition() (x, y float64) {
offset := 45.0 - (degreesPerDirection / 2) t := m.Position.Tile()
return t.X(), t.Y()
newDirection := int((angle - offset) / degreesPerDirection)
if newDirection >= 64 {
newDirection = newDirection - 64
} else if newDirection < 0 {
newDirection = 64 + newDirection
}
return newDirection
} }
// GetPosition returns the entity's current tile position. // GetPositionF returns the entity's current tile position where 0.2 is one sub tile.
func (m *mapEntity) GetPosition() (float64, float64) { func (m *mapEntity) GetPositionF() (x, y float64) {
return float64(m.TileX), float64(m.TileY) w := m.Position.World()
} return w.X(), w.Y()
// GetPositionF returns the entity's current sub tile position.
func (m *mapEntity) GetPositionF() (float64, float64) {
return float64(m.TileX) + (float64(m.subcellX) / 5.0), float64(m.TileY) + (float64(m.subcellY) / 5.0)
} }
// Name returns the NPC's in-game name (e.g. "Deckard Cain") or an empty string if it does not have a name // Name returns the NPC's in-game name (e.g. "Deckard Cain") or an empty string if it does not have a name

View File

@ -32,7 +32,7 @@ func CreateMissile(x, y int, record *d2datadict.MissileRecord) (*Missile, error)
} }
animation.SetEffect(d2enum.DrawEffectModulate) animation.SetEffect(d2enum.DrawEffectModulate)
//animation.SetPlaySpeed(float64(record.Animation.AnimationSpeed)) // animation.SetPlaySpeed(float64(record.Animation.AnimationSpeed))
animation.SetPlayLoop(record.Animation.LoopAnimation) animation.SetPlayLoop(record.Animation.LoopAnimation)
animation.PlayForward() animation.PlayForward()
entity := CreateAnimatedEntity(x, y, animation) entity := CreateAnimatedEntity(x, y, animation)
@ -51,8 +51,8 @@ func CreateMissile(x, y int, record *d2datadict.MissileRecord) (*Missile, error)
func (m *Missile) SetRadians(angle float64, done func()) { func (m *Missile) SetRadians(angle float64, done func()) {
r := float64(m.record.Range) r := float64(m.record.Range)
x := m.LocationX + (r * math.Cos(angle)) x := m.Position.X() + (r * math.Cos(angle))
y := m.LocationY + (r * math.Sin(angle)) y := m.Position.Y() + (r * math.Sin(angle))
m.SetTarget(x, y, done) m.SetTarget(x, y, done)
} }

View File

@ -72,10 +72,12 @@ func selectEquip(slice []string) string {
// Render renders this entity's animated composite. // Render renders this entity's animated composite.
func (v *NPC) Render(target d2interface.Surface) { func (v *NPC) Render(target d2interface.Surface) {
renderOffset := v.Position.RenderOffset()
target.PushTranslation( target.PushTranslation(
v.offsetX+int((v.subcellX-v.subcellY)*16), int((renderOffset.X()-renderOffset.Y())*16),
v.offsetY+int(((v.subcellX+v.subcellY)*8)-5), int(((renderOffset.X()+renderOffset.Y())*8)-5),
) )
defer target.Pop() defer target.Pop()
v.composite.Render(target) v.composite.Render(target)
} }

View File

@ -144,10 +144,12 @@ func (v *Player) Advance(tickTime float64) {
// Render renders the animated composite for this entity. // Render renders the animated composite for this entity.
func (v *Player) Render(target d2interface.Surface) { func (v *Player) Render(target d2interface.Surface) {
renderOffset := v.Position.RenderOffset()
target.PushTranslation( target.PushTranslation(
v.offsetX+int((v.subcellX-v.subcellY)*16), int((renderOffset.X()-renderOffset.Y())*16),
v.offsetY+int(((v.subcellX+v.subcellY)*8)-5), int(((renderOffset.X()+renderOffset.Y())*8)-5),
) )
defer target.Pop() defer target.Pop()
v.composite.Render(target) v.composite.Render(target)
// v.nameLabel.X = v.offsetX // v.nameLabel.X = v.offsetX
@ -180,6 +182,7 @@ func (v *Player) GetAnimationMode() d2enum.PlayerAnimationMode {
return d2enum.PlayerAnimationModeNeutral return d2enum.PlayerAnimationModeNeutral
} }
// SetAnimationMode sets the Composite's animation mode weapon class and direction.
func (v *Player) SetAnimationMode(animationMode d2enum.PlayerAnimationMode) error { func (v *Player) SetAnimationMode(animationMode d2enum.PlayerAnimationMode) error {
return v.composite.SetMode(animationMode, v.composite.GetWeaponClass()) return v.composite.SetMode(animationMode, v.composite.GetWeaponClass())
} }

View File

@ -117,7 +117,9 @@ func (v *Game) Advance(tickTime float64) error {
if v.ticksSinceLevelCheck > 1.0 { if v.ticksSinceLevelCheck > 1.0 {
v.ticksSinceLevelCheck = 0 v.ticksSinceLevelCheck = 0
if v.localPlayer != nil { if v.localPlayer != nil {
tile := v.gameClient.MapEngine.TileAt(v.localPlayer.TileX, v.localPlayer.TileY) tilePosition := v.localPlayer.Position.Tile()
tile := v.gameClient.MapEngine.TileAt(int(tilePosition.X()), int(tilePosition.Y()))
if tile != nil { if tile != nil {
musicInfo := d2common.GetMusicDef(tile.RegionType) musicInfo := d2common.GetMusicDef(tile.RegionType)
v.audioProvider.PlayBGM(musicInfo.MusicFile) v.audioProvider.PlayBGM(musicInfo.MusicFile)
@ -142,7 +144,8 @@ func (v *Game) Advance(tickTime float64) error {
// Update the camera to focus on the player // Update the camera to focus on the player
if v.localPlayer != nil && !v.gameControls.FreeCam { if v.localPlayer != nil && !v.gameControls.FreeCam {
rx, ry := v.mapRenderer.WorldToOrtho(v.localPlayer.LocationX/5, v.localPlayer.LocationY/5) worldPosition := v.localPlayer.Position.World()
rx, ry := v.mapRenderer.WorldToOrtho(worldPosition.X(), worldPosition.Y())
v.mapRenderer.MoveCameraTo(rx, ry) v.mapRenderer.MoveCameraTo(rx, ry)
} }
@ -169,10 +172,9 @@ func (v *Game) bindGameControls() {
// OnPlayerMove sends the player move action to the server // OnPlayerMove sends the player move action to the server
func (v *Game) OnPlayerMove(x, y float64) { func (v *Game) OnPlayerMove(x, y float64) {
heroPosX := v.localPlayer.LocationX / 5.0 worldPosition := v.localPlayer.Position.World()
heroPosY := v.localPlayer.LocationY / 5.0
err := v.gameClient.SendPacketToServer(d2netpacket.CreateMovePlayerPacket(v.gameClient.PlayerId, heroPosX, heroPosY, x, y)) err := v.gameClient.SendPacketToServer(d2netpacket.CreateMovePlayerPacket(v.gameClient.PlayerId, worldPosition.X(), worldPosition.Y(), x, y))
if err != nil { if err != nil {
fmt.Printf("failed to send MovePlayer packet to the server, playerId: %s, x: %g, x: %g\n", v.gameClient.PlayerId, x, y) fmt.Printf("failed to send MovePlayer packet to the server, playerId: %s, x: %g, x: %g\n", v.gameClient.PlayerId, x, y)
} }

View File

@ -111,7 +111,8 @@ func (g *GameClient) OnPacketReceived(packet d2netpacket.NetPacket) error {
path, _, _ := g.MapEngine.PathFind(movePlayer.StartX, movePlayer.StartY, movePlayer.DestX, movePlayer.DestY) path, _, _ := g.MapEngine.PathFind(movePlayer.StartX, movePlayer.StartY, movePlayer.DestX, movePlayer.DestY)
if len(path) > 0 { if len(path) > 0 {
player.SetPath(path, func() { player.SetPath(path, func() {
tile := g.MapEngine.TileAt(player.TileX, player.TileY) tilePosition := player.Position.Tile()
tile := g.MapEngine.TileAt(int(tilePosition.X()), int(tilePosition.Y()))
if tile == nil { if tile == nil {
return return
} }
@ -135,8 +136,8 @@ func (g *GameClient) OnPacketReceived(packet d2netpacket.NetPacket) error {
player.ClearPath() player.ClearPath()
// currently hardcoded to missile skill // currently hardcoded to missile skill
missile, err := d2mapentity.CreateMissile( missile, err := d2mapentity.CreateMissile(
int(player.LocationX), int(player.Position.X()),
int(player.LocationY), int(player.Position.Y()),
d2datadict.Missiles[playerCast.SkillID], d2datadict.Missiles[playerCast.SkillID],
) )
if err != nil { if err != nil {
@ -144,8 +145,8 @@ func (g *GameClient) OnPacketReceived(packet d2netpacket.NetPacket) error {
} }
rads := d2common.GetRadiansBetween( rads := d2common.GetRadiansBetween(
player.LocationX, player.Position.X(),
player.LocationY, player.Position.Y(),
playerCast.TargetX*5, playerCast.TargetX*5,
playerCast.TargetY*5, playerCast.TargetY*5,
) )