1
1
mirror of https://github.com/OpenDiablo2/OpenDiablo2 synced 2024-07-01 11:25:26 +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 (
"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))
}

View File

@ -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)
}

View File

@ -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()

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
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) {

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

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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())
}

View File

@ -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)
}

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)
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,
)