diff --git a/d2common/d2math/d2vector/position.go b/d2common/d2math/d2vector/position.go index 3606436b..d22a24e7 100644 --- a/d2common/d2math/d2vector/position.go +++ b/d2common/d2math/d2vector/position.go @@ -3,16 +3,24 @@ package d2vector import ( "fmt" "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 { 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 { p := Position{NewVector(x, y)} p.checkValues() @@ -20,15 +28,7 @@ func NewPosition(x, y float64) Position { return p } -// EntityPosition returns a Position struct based on the given entity spawn point. -// 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. +// Set sets this position to the given sub tile coordinates where 1 = 1 sub tile, with a fractional offset. func (p *Position) Set(x, y float64) { p.x, p.y = x, y p.checkValues() @@ -44,48 +44,45 @@ func (p *Position) checkValues() { } } -// World is the position, where 1 = one map tile. -// unused +// World is the exact position where 1 = one map tile and 0.2 = one sub tile. 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 { - c := p.World().Clone() - return c.Floor() + return p.World().Floor() } -// TileOffset is the offset from the tile position, always < 1. -// unused -func (p *Position) TileOffset() *Vector { - c := p.World().Clone() - return c.Subtract(p.Tile()) +// RenderOffset is the offset in sub tiles from the curren tile, + 1. This places the vector at the bottom vertex of an +// isometric diamond visually representing one sub tile. Sub tile indices increase to the lower right diagonal ('down') +// and to the lower left diagonal ('left') of the isometric grid. This renders the target one index above which visually +// is one tile below. +func (p *Position) RenderOffset() *Vector { + return p.subTileOffset().AddScalar(1) } -// SubWorld is the position, where 5 = one map tile. (locationX, locationY) -func (p *Position) SubWorld() *Vector { - c := p.World().Clone() - return c.Scale(subTilesPerTile) +// SubTileOffset is the offset from the current map tile in sub tiles. +func (p *Position) subTileOffset() *Vector { + t := p.Tile().Scale(subTilesPerTile) + c := p.Clone() + + return c.Subtract(t) } -// SubTile is the tile position in sub tiles, always a multiple of 5. -// unused -func (p *Position) SubTile() *Vector { - return p.Tile().Scale(subTilesPerTile) -} +// DirectionTo returns the entity direction from this vector to the given vector. +func (v *Vector) DirectionTo(target Vector) int { + direction := target.Clone() + direction.Subtract(v) -// SubTileOffset is the offset from the sub tile position in sub tiles, always < 1. -// unused -func (p *Position) SubTileOffset() *Vector { - return p.SubWorld().Subtract(p.SubTile()) -} + angle := direction.SignedAngle(VectorRight()) + radiansPerDirection := d2math.RadFull / entityDirectionCount -// TODO: understand this and maybe improve/remove it -// SubTileOffset() + 1. It's used for rendering. It seems to always do this: -// v.offsetX+int((v.subcellX-v.subcellY)*16), -// v.offsetY+int(((v.subcellX+v.subcellY)*8)-5), -// ^ Maybe similar to Viewport.OrthoToWorld? (subCellX, subCellY) -func (p *Position) SubCell() *Vector { - return p.SubTileOffset().AddScalar(1) + // Note: The direction is always one increment out so we must subtract one increment. + // This might not work when we implement all 16 directions (as of this writing entities can only face one of 8 + // directions). See entityDirectionIncrement. + newDirection := int((angle / radiansPerDirection) - entityDirectionIncrement) + + return d2math.WrapInt(newDirection, int(entityDirectionCount)) } diff --git a/d2common/d2math/d2vector/position_test.go b/d2common/d2math/d2vector/position_test.go index ed44d6ea..6a79386b 100644 --- a/d2common/d2math/d2vector/position_test.go +++ b/d2common/d2math/d2vector/position_test.go @@ -6,18 +6,18 @@ import ( "testing" ) -func TestEntityPosition(t *testing.T) { +func TestNewPosition(t *testing.T) { x, y := rand.Intn(1000), rand.Intn(1000) - pos := EntityPosition(x, y) locX, locY := float64(x), float64(y) + pos := NewPosition(locX, locY) // old coordinate values Position equivalent locationX := locX // .SubWord().X() locationY := locY // .SubWord().Y() tileX := float64(x / 5) // .Tile().X() tileY := float64(y / 5) // .Tile().Y() - subcellX := 1 + math.Mod(locX, 5) // .SubCell().X() - subcellY := 1 + math.Mod(locY, 5) // .SubCell().Y() + subcellX := 1 + math.Mod(locX, 5) // .RenderOffset().X() + subcellY := 1 + math.Mod(locY, 5) // .RenderOffset().Y() want := NewVector(tileX, tileY) got := pos.Tile() @@ -27,14 +27,14 @@ func TestEntityPosition(t *testing.T) { } want = NewVector(subcellX, subcellY) - got = pos.SubCell() + got = pos.RenderOffset() 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) - got = pos.SubWorld() + got = &pos.Vector if !got.Equals(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) { - p := NewPosition(1.6, 1.6) +func TestPosition_World(t *testing.T) { + 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() - want := NewVector(1, 1) - unchanged := NewVector(1.6, 1.6) + want := NewVector(4, 4) validate("tile position", t, p.Vector, *got, want, unchanged) } -func TestTileOffset(t *testing.T) { - p := NewPosition(1.6, 1.6) - got := p.TileOffset() - want := NewVector(0.6, 0.6) - unchanged := NewVector(1.6, 1.6) - - 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) +func TestPosition_RenderOffset(t *testing.T) { + p := NewPosition(12.1, 14.2) + unchanged := p.Clone() + got := p.RenderOffset() + want := NewVector(3.1, 5.2) validate("offset from sub tile", t, p.Vector, *got, want, unchanged) } diff --git a/d2common/d2math/d2vector/vector.go b/d2common/d2math/d2vector/vector.go index e5524697..c729a4ea 100644 --- a/d2common/d2math/d2vector/vector.go +++ b/d2common/d2math/d2vector/vector.go @@ -49,6 +49,11 @@ func (v *Vector) CompareApprox(o Vector) (x, y int) { 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. func (v *Vector) Set(x, y float64) *Vector { v.x = x @@ -95,7 +100,7 @@ func (v *Vector) Add(o *Vector) *Vector { 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 { v.x += s v.y += s @@ -135,7 +140,7 @@ func (v *Vector) Divide(o *Vector) *Vector { 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 { v.x /= 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 // float64 return value to restore it's original length. func (v *Vector) Normalize() float64 { + if v.IsZero() { + return 0 + } + multiplier := 1 / v.Length() 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 // a full circle. For angles describing a full circumference use SignedAngle. func (v *Vector) Angle(o Vector) float64 { + if v.IsZero() || o.IsZero() { + return 0 + } + from := v.Clone() from.Normalize() diff --git a/d2common/d2math/d2vector/vector_test.go b/d2common/d2math/d2vector/vector_test.go index fecc8b4a..3ce5eb73 100644 --- a/d2common/d2math/d2vector/vector_test.go +++ b/d2common/d2math/d2vector/vector_test.go @@ -77,7 +77,7 @@ func TestEqualsF(t *testing.T) { } } -func TestCompareF(t *testing.T) { +func TestCompareApprox(t *testing.T) { subEpsilon := d2math.Epsilon / 3 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) { v := NewVector(1, 1) want := NewVector(2, 3) @@ -337,6 +351,14 @@ func TestNormalize(t *testing.T) { v.Scale(reverse) 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) { diff --git a/d2common/d2math/doc.go b/d2common/d2math/doc.go index 528283e1..6a758ac1 100644 --- a/d2common/d2math/doc.go +++ b/d2common/d2math/doc.go @@ -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 diff --git a/d2common/d2math/math.go b/d2common/d2math/math.go index ef788502..b98856aa 100644 --- a/d2common/d2math/math.go +++ b/d2common/d2math/math.go @@ -14,6 +14,11 @@ const ( 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 // the comparison of floats a and b. 0 will be returned if the // 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 { 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 +} diff --git a/d2common/d2math/math_test.go b/d2common/d2math/math_test.go index f16e67be..e0d0b772 100644 --- a/d2common/d2math/math_test.go +++ b/d2common/d2math/math_test.go @@ -4,6 +4,24 @@ import ( "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) { subEpsilon := Epsilon / 3 @@ -111,3 +129,39 @@ func TestUnlerp(t *testing.T) { 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) + } +} diff --git a/d2common/math.go b/d2common/math.go index 04e8476e..6d0e2f9f 100644 --- a/d2common/math.go +++ b/d2common/math.go @@ -73,25 +73,6 @@ func MinInt32(a, b int32) int32 { // 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. func GetRadiansBetween(p1X, p1Y, p2X, p2Y float64) float64 { deltaY := p2Y - p1Y @@ -104,34 +85,3 @@ func GetRadiansBetween(p1X, p1Y, p2X, p2Y float64) float64 { func AlmostEqual(a, b, threshold float64) bool { 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 -} diff --git a/d2core/d2map/d2mapentity/animated_entity.go b/d2core/d2map/d2mapentity/animated_entity.go index c6d7c02c..cead298e 100644 --- a/d2core/d2map/d2mapentity/animated_entity.go +++ b/d2core/d2map/d2mapentity/animated_entity.go @@ -27,10 +27,12 @@ func CreateAnimatedEntity(x, y int, animation d2interface.Animation) *AnimatedEn // Render draws this animated entity onto the target func (ae *AnimatedEntity) Render(target d2interface.Surface) { + renderOffset := ae.Position.RenderOffset() target.PushTranslation( - ae.offsetX+int((ae.subcellX-ae.subcellY)*16), - ae.offsetY+int(((ae.subcellX+ae.subcellY)*8)-5), + int((renderOffset.X()-renderOffset.Y())*16), + int(((renderOffset.X()+renderOffset.Y())*8)-5), ) + defer target.Pop() ae.animation.Render(target) } diff --git a/d2core/d2map/d2mapentity/map_entity.go b/d2core/d2map/d2mapentity/map_entity.go index 1fc370e9..719ca4d9 100644 --- a/d2core/d2map/d2mapentity/map_entity.go +++ b/d2core/d2map/d2mapentity/map_entity.go @@ -1,25 +1,20 @@ package d2mapentity import ( - "math" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2math/d2vector" "github.com/OpenDiablo2/OpenDiablo2/d2common" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2astar" ) // mapEntity represents an entity on the map that can be animated -// TODO: Has a coordinate (issue #456) type mapEntity struct { - LocationX float64 - LocationY float64 - TileX, TileY int // Coordinates of the tile the unit is within - subcellX, subcellY float64 // Subcell coordinates within the current tile - offsetX, offsetY int - TargetX float64 - TargetY float64 - Speed float64 - path []d2astar.Pather - drawLayer int + Position d2vector.Position + Target d2vector.Position + + Speed float64 + path []d2astar.Pather + drawLayer int done func() directioner func(direction int) @@ -30,14 +25,8 @@ func createMapEntity(x, y int) mapEntity { locX, locY := float64(x), float64(y) return mapEntity{ - LocationX: locX, - LocationY: locY, - TargetX: locX, - TargetY: locY, - TileX: x / 5, - TileY: y / 5, - subcellX: 1 + math.Mod(locX, 5), - subcellY: 1 + math.Mod(locY, 5), + Position: d2vector.NewPosition(locX, locY), + Target: d2vector.NewPosition(locX, locY), Speed: 6, drawLayer: 0, path: []d2astar.Pather{}, @@ -71,25 +60,9 @@ func (m *mapEntity) GetSpeed() float64 { return m.Speed } -func (m *mapEntity) getStepLength(tickTime float64) (float64, float64) { - 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. +// IsAtTarget returns true if the distance between entity and target is almost zero. 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. @@ -103,27 +76,16 @@ func (m *mapEntity) Step(tickTime float64) { return } - stepX, stepY := m.getStepLength(tickTime) + velocity := m.velocity(tickTime) for { - if d2common.AlmostEqual(m.LocationX-m.TargetX, 0, 0.0001) { - stepX = 0 - } + // Add the velocity to the position and set new velocity to remainder + applyVelocity(&m.Position.Vector, &velocity, &m.Target.Vector) - if d2common.AlmostEqual(m.LocationY-m.TargetY, 0, 0.0001) { - stepY = 0 - } - - 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) { + // New position is at target + if m.Position.EqualsApprox(m.Target.Vector) { 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) if len(m.path) > 1 { @@ -132,21 +94,61 @@ func (m *mapEntity) Step(tickTime float64) { m.path = []d2astar.Pather{} } } else { - m.LocationX = m.TargetX - m.LocationY = 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) + // End of path. + m.Position.Copy(&m.Target.Vector) } } - if stepX == 0 && stepY == 0 { + if velocity.IsZero() { 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. func (m *mapEntity) HasPathFinding() bool { 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. func (m *mapEntity) SetTarget(tx, ty float64, done func()) { - m.TargetX, m.TargetY = tx, ty + m.Target.Set(tx, ty) m.done = done if m.directioner != nil { - angle := 359 - d2common.GetAngleBetween( - m.LocationX, - m.LocationY, - tx, - ty, - ) - m.directioner(angleToDirection(float64(angle))) + d := m.Position.DirectionTo(m.Target.Vector) + + m.directioner(d) } } -func angleToDirection(angle float64) int { - degreesPerDirection := 360.0 / 64.0 - offset := 45.0 - (degreesPerDirection / 2) - - 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, always a whole number. +func (m *mapEntity) GetPosition() (x, y float64) { + t := m.Position.Tile() + return t.X(), t.Y() } -// GetPosition returns the entity's current tile position. -func (m *mapEntity) GetPosition() (float64, float64) { - return float64(m.TileX), float64(m.TileY) -} - -// 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) +// GetPositionF returns the entity's current tile position where 0.2 is one sub tile. +func (m *mapEntity) GetPositionF() (x, y float64) { + w := m.Position.World() + return w.X(), w.Y() } // Name returns the NPC's in-game name (e.g. "Deckard Cain") or an empty string if it does not have a name diff --git a/d2core/d2map/d2mapentity/missile.go b/d2core/d2map/d2mapentity/missile.go index dbaeb384..f1e221f6 100644 --- a/d2core/d2map/d2mapentity/missile.go +++ b/d2core/d2map/d2mapentity/missile.go @@ -32,7 +32,7 @@ func CreateMissile(x, y int, record *d2datadict.MissileRecord) (*Missile, error) } animation.SetEffect(d2enum.DrawEffectModulate) - //animation.SetPlaySpeed(float64(record.Animation.AnimationSpeed)) + // animation.SetPlaySpeed(float64(record.Animation.AnimationSpeed)) animation.SetPlayLoop(record.Animation.LoopAnimation) animation.PlayForward() 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()) { r := float64(m.record.Range) - x := m.LocationX + (r * math.Cos(angle)) - y := m.LocationY + (r * math.Sin(angle)) + x := m.Position.X() + (r * math.Cos(angle)) + y := m.Position.Y() + (r * math.Sin(angle)) m.SetTarget(x, y, done) } diff --git a/d2core/d2map/d2mapentity/npc.go b/d2core/d2map/d2mapentity/npc.go index 17f0e25e..aea49d95 100644 --- a/d2core/d2map/d2mapentity/npc.go +++ b/d2core/d2map/d2mapentity/npc.go @@ -72,10 +72,12 @@ func selectEquip(slice []string) string { // Render renders this entity's animated composite. func (v *NPC) Render(target d2interface.Surface) { + renderOffset := v.Position.RenderOffset() target.PushTranslation( - v.offsetX+int((v.subcellX-v.subcellY)*16), - v.offsetY+int(((v.subcellX+v.subcellY)*8)-5), + int((renderOffset.X()-renderOffset.Y())*16), + int(((renderOffset.X()+renderOffset.Y())*8)-5), ) + defer target.Pop() v.composite.Render(target) } diff --git a/d2core/d2map/d2mapentity/player.go b/d2core/d2map/d2mapentity/player.go index e2c71e72..20b5c143 100644 --- a/d2core/d2map/d2mapentity/player.go +++ b/d2core/d2map/d2mapentity/player.go @@ -144,10 +144,12 @@ func (v *Player) Advance(tickTime float64) { // Render renders the animated composite for this entity. func (v *Player) Render(target d2interface.Surface) { + renderOffset := v.Position.RenderOffset() target.PushTranslation( - v.offsetX+int((v.subcellX-v.subcellY)*16), - v.offsetY+int(((v.subcellX+v.subcellY)*8)-5), + int((renderOffset.X()-renderOffset.Y())*16), + int(((renderOffset.X()+renderOffset.Y())*8)-5), ) + defer target.Pop() v.composite.Render(target) // v.nameLabel.X = v.offsetX @@ -180,6 +182,7 @@ func (v *Player) GetAnimationMode() d2enum.PlayerAnimationMode { return d2enum.PlayerAnimationModeNeutral } +// SetAnimationMode sets the Composite's animation mode weapon class and direction. func (v *Player) SetAnimationMode(animationMode d2enum.PlayerAnimationMode) error { return v.composite.SetMode(animationMode, v.composite.GetWeaponClass()) } diff --git a/d2game/d2gamescreen/game.go b/d2game/d2gamescreen/game.go index 297ff168..792ec853 100644 --- a/d2game/d2gamescreen/game.go +++ b/d2game/d2gamescreen/game.go @@ -117,7 +117,9 @@ func (v *Game) Advance(tickTime float64) error { if v.ticksSinceLevelCheck > 1.0 { v.ticksSinceLevelCheck = 0 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 { musicInfo := d2common.GetMusicDef(tile.RegionType) v.audioProvider.PlayBGM(musicInfo.MusicFile) @@ -142,7 +144,8 @@ func (v *Game) Advance(tickTime float64) error { // Update the camera to focus on the player 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) } @@ -169,10 +172,9 @@ func (v *Game) bindGameControls() { // OnPlayerMove sends the player move action to the server func (v *Game) OnPlayerMove(x, y float64) { - heroPosX := v.localPlayer.LocationX / 5.0 - heroPosY := v.localPlayer.LocationY / 5.0 + worldPosition := v.localPlayer.Position.World() - 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 { fmt.Printf("failed to send MovePlayer packet to the server, playerId: %s, x: %g, x: %g\n", v.gameClient.PlayerId, x, y) } diff --git a/d2networking/d2client/game_client.go b/d2networking/d2client/game_client.go index 0330d3db..ac987acd 100644 --- a/d2networking/d2client/game_client.go +++ b/d2networking/d2client/game_client.go @@ -111,7 +111,8 @@ func (g *GameClient) OnPacketReceived(packet d2netpacket.NetPacket) error { path, _, _ := g.MapEngine.PathFind(movePlayer.StartX, movePlayer.StartY, movePlayer.DestX, movePlayer.DestY) if len(path) > 0 { 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 { return } @@ -135,8 +136,8 @@ func (g *GameClient) OnPacketReceived(packet d2netpacket.NetPacket) error { player.ClearPath() // currently hardcoded to missile skill missile, err := d2mapentity.CreateMissile( - int(player.LocationX), - int(player.LocationY), + int(player.Position.X()), + int(player.Position.Y()), d2datadict.Missiles[playerCast.SkillID], ) if err != nil { @@ -144,8 +145,8 @@ func (g *GameClient) OnPacketReceived(packet d2netpacket.NetPacket) error { } rads := d2common.GetRadiansBetween( - player.LocationX, - player.LocationY, + player.Position.X(), + player.Position.Y(), playerCast.TargetX*5, playerCast.TargetY*5, )