mirror of
https://github.com/OpenDiablo2/OpenDiablo2
synced 2025-02-04 15:46:51 -05: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:
parent
41e7a5b28b
commit
894c60f77a
@ -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))
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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())
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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,
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user