diff --git a/d2common/d2data/object.go b/d2common/d2data/object.go index e4b5a62da..b9530ae65 100644 --- a/d2common/d2data/object.go +++ b/d2common/d2data/object.go @@ -6,11 +6,11 @@ import ( ) type Object struct { - Type int32 - Id int32 - X int32 - Y int32 - Flags int32 + Type int + Id int + X int + Y int + Flags int Paths []d2common.Path Lookup *d2datadict.ObjectLookupRecord ObjectInfo *d2datadict.ObjectRecord diff --git a/d2common/d2fileformats/d2ds1/ds1.go b/d2common/d2fileformats/d2ds1/ds1.go index 151ec9c5f..b47b22391 100644 --- a/d2common/d2fileformats/d2ds1/ds1.go +++ b/d2common/d2fileformats/d2ds1/ds1.go @@ -174,11 +174,11 @@ func LoadDS1(fileData []byte) DS1 { ds1.Objects = make([]d2data.Object, numberOfObjects) for objIdx := 0; objIdx < int(numberOfObjects); objIdx++ { newObject := d2data.Object{} - newObject.Type = br.GetInt32() - newObject.Id = br.GetInt32() - newObject.X = br.GetInt32() - newObject.Y = br.GetInt32() - newObject.Flags = br.GetInt32() + newObject.Type = int(br.GetInt32()) + newObject.Id = int(br.GetInt32()) + newObject.X = int(br.GetInt32()) + newObject.Y = int(br.GetInt32()) + newObject.Flags = int(br.GetInt32()) //TODO: There's a crash here, we aren't loading this data right.... newObject.Lookup = d2datadict.LookupObject(int(ds1.Act), int(newObject.Type), int(newObject.Id)) if newObject.Lookup != nil && newObject.Lookup.ObjectsTxtId != -1 { @@ -212,8 +212,8 @@ func LoadDS1(fileData []byte) DS1 { numberOfNpcs := br.GetInt32() for npcIdx := 0; npcIdx < int(numberOfNpcs); npcIdx++ { numPaths := br.GetInt32() - npcX := br.GetInt32() - npcY := br.GetInt32() + npcX := int(br.GetInt32()) + npcY := int(br.GetInt32()) objIdx := -1 for idx, ds1Obj := range ds1.Objects { if ds1Obj.X == npcX && ds1Obj.Y == npcY { @@ -227,10 +227,10 @@ func LoadDS1(fileData []byte) DS1 { } for pathIdx := 0; pathIdx < int(numPaths); pathIdx++ { newPath := d2common.Path{} - newPath.X = br.GetInt32() - newPath.Y = br.GetInt32() + newPath.X = int(br.GetInt32()) + newPath.Y = int(br.GetInt32()) if ds1.Version >= 15 { - newPath.Action = br.GetInt32() + newPath.Action = int(br.GetInt32()) } ds1.Objects[objIdx].Paths[pathIdx] = newPath } diff --git a/d2common/d2resource/resource_paths.go b/d2common/d2resource/resource_paths.go index ccd7a3172..5608ef8aa 100644 --- a/d2common/d2resource/resource_paths.go +++ b/d2common/d2resource/resource_paths.go @@ -180,6 +180,7 @@ const ( ObjectData = "/data/global/objects" AnimationData = "/data/global/animdata.d2" PlayerAnimationBase = "/data/global/CHARS" + MissileData = "/data/global/missiles" // --- Inventory Data --- diff --git a/d2common/path.go b/d2common/path.go index 7e3a706ec..cbb26a02b 100644 --- a/d2common/path.go +++ b/d2common/path.go @@ -1,7 +1,7 @@ package d2common type Path struct { - X int32 - Y int32 - Action int32 + X int + Y int + Action int } diff --git a/d2core/d2asset/animation_manager.go b/d2core/d2asset/animation_manager.go index 64d37d760..3195f4441 100644 --- a/d2core/d2asset/animation_manager.go +++ b/d2core/d2asset/animation_manager.go @@ -1,7 +1,6 @@ package d2asset import ( - "errors" "fmt" "path/filepath" "strings" @@ -28,7 +27,8 @@ func (am *animationManager) loadAnimation(animationPath, palettePath string, tra } var animation *Animation - switch strings.ToLower(filepath.Ext(animationPath)) { + ext := strings.ToLower(filepath.Ext(animationPath)) + switch ext { case ".dc6": dc6, err := loadDC6(animationPath, palettePath) if err != nil { @@ -54,9 +54,8 @@ func (am *animationManager) loadAnimation(animationPath, palettePath string, tra if err != nil { return nil, err } - default: - return nil, errors.New("unknown animation format") + return nil, fmt.Errorf("unknown animation format: %s", ext) } if err := am.cache.Insert(cachePath, animation.Clone(), 1); err != nil { diff --git a/d2core/d2asset/composite.go b/d2core/d2asset/composite.go index dee4173b9..86b2997a0 100644 --- a/d2core/d2asset/composite.go +++ b/d2core/d2asset/composite.go @@ -72,6 +72,7 @@ func (c *Composite) SetMode(animationMode, weaponClass string, direction int) er return err } + c.ResetPlayedCount() c.mode = mode return nil } diff --git a/d2core/d2map/animated_composite.go b/d2core/d2map/animated_composite.go new file mode 100644 index 000000000..f1d671481 --- /dev/null +++ b/d2core/d2map/animated_composite.go @@ -0,0 +1,81 @@ +package d2map + +import ( + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset" + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2render" +) + +// AnimatedComposite represents a composite of animations that can be projected onto the map. +type AnimatedComposite struct { + mapEntity + animationMode string + composite *d2asset.Composite + direction int +} + +// CreateAnimatedComposite creates an instance of AnimatedComposite +func CreateAnimatedComposite(x, y int, object *d2datadict.ObjectLookupRecord, palettePath string) (*AnimatedComposite, error) { + composite, err := d2asset.LoadComposite(object, palettePath) + if err != nil { + return nil, err + } + + entity := &AnimatedComposite{ + mapEntity: createMapEntity(x, y), + composite: composite, + } + entity.mapEntity.directioner = entity.rotate + return entity, nil +} + +func (ac *AnimatedComposite) SetAnimationMode(animationMode string) error { + return ac.composite.SetMode(animationMode, ac.weaponClass, ac.direction) +} + +// SetMode changes the graphical mode of this animated entity +func (ac *AnimatedComposite) SetMode(animationMode, weaponClass string, direction int) error { + ac.animationMode = animationMode + ac.direction = direction + + err := ac.composite.SetMode(animationMode, weaponClass, direction) + if err != nil { + err = ac.composite.SetMode(animationMode, "HTH", direction) + ac.weaponClass = "HTH" + } + + return err +} + +// Render draws this animated entity onto the target +func (ac *AnimatedComposite) Render(target d2render.Surface) { + target.PushTranslation( + ac.offsetX+int((ac.subcellX-ac.subcellY)*16), + ac.offsetY+int(((ac.subcellX+ac.subcellY)*8)-5), + ) + defer target.Pop() + ac.composite.Render(target) +} + +// rotate sets direction and changes animation +func (ac *AnimatedComposite) rotate(angle float64) { + // TODO: Check if is in town and if is player. + newAnimationMode := ac.animationMode + if !ac.IsAtTarget() { + newAnimationMode = d2enum.AnimationModeMonsterWalk.String() + } + + if newAnimationMode != ac.animationMode { + ac.SetMode(newAnimationMode, ac.weaponClass, ac.direction) + } + + newDirection := angleToDirection(angle, ac.composite.GetDirectionCount()) + if newDirection != ac.direction { + ac.SetMode(ac.animationMode, ac.weaponClass, newDirection) + } +} + +func (ac *AnimatedComposite) Advance(elapsed float64) { + ac.composite.Advance(elapsed) +} diff --git a/d2core/d2map/animated_entity.go b/d2core/d2map/animated_entity.go index e1e9bbc52..a78261129 100644 --- a/d2core/d2map/animated_entity.go +++ b/d2core/d2map/animated_entity.go @@ -1,220 +1,64 @@ package d2map import ( - "github.com/beefsack/go-astar" - "math" - "math/rand" - - "github.com/OpenDiablo2/OpenDiablo2/d2common" - "github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict" - "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2dcc" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2render" ) -// AnimatedEntity represents an entity on the map that can be animated +// AnimatedEntity represents an animation that can be projected onto the map. type AnimatedEntity 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 - animationMode string - weaponClass string - direction int - offsetX, offsetY int32 - TargetX float64 - TargetY float64 - action int32 - repetitions int - path []astar.Pather + mapEntity + direction int + action int + repetitions int - composite *d2asset.Composite + animation *d2asset.Animation } // CreateAnimatedEntity creates an instance of AnimatedEntity -func CreateAnimatedEntity(x, y int32, object *d2datadict.ObjectLookupRecord, palettePath string) (*AnimatedEntity, error) { - composite, err := d2asset.LoadComposite(object, palettePath) - if err != nil { - return nil, err - } - - entity := &AnimatedEntity{composite: composite} - entity.LocationX = float64(x) - entity.LocationY = float64(y) - entity.TargetX = entity.LocationX - entity.TargetY = entity.LocationY - entity.path = []astar.Pather{} - - entity.TileX = int(entity.LocationX / 5) - entity.TileY = int(entity.LocationY / 5) - entity.subcellX = 1 + math.Mod(entity.LocationX, 5) - entity.subcellY = 1 + math.Mod(entity.LocationY, 5) - - return entity, nil -} - -// SetMode changes the graphical mode of this animated entity -func (v *AnimatedEntity) SetMode(animationMode, weaponClass string, direction int) error { - v.animationMode = animationMode - v.direction = direction - - err := v.composite.SetMode(animationMode, weaponClass, direction) - if err != nil { - err = v.composite.SetMode(animationMode, "HTH", direction) +func CreateAnimatedEntity(x, y int, animation *d2asset.Animation) *AnimatedEntity { + entity := &AnimatedEntity{ + mapEntity: createMapEntity(x, y), + animation: animation, } - - return err + entity.mapEntity.directioner = entity.rotate + return entity } -// If an npc has a path to pause at each location. -// Waits for animation to end and all repetitions to be exhausted. -func (v AnimatedEntity) Wait() bool { - return v.composite.GetPlayedCount() > v.repetitions -} - -func (v *AnimatedEntity) SetPath(path []astar.Pather) { - v.path = path -} // Render draws this animated entity onto the target -func (v *AnimatedEntity) Render(target d2render.Surface) { +func (ae *AnimatedEntity) Render(target d2render.Surface) { target.PushTranslation( - int(v.offsetX)+int((v.subcellX-v.subcellY)*16), - int(v.offsetY)+int(((v.subcellX+v.subcellY)*8)-5), + ae.offsetX+int((ae.subcellX-ae.subcellY)*16), + ae.offsetY+int(((ae.subcellX+ae.subcellY)*8)-5), ) defer target.Pop() - v.composite.Render(target) + ae.animation.Render(target) } -func (v AnimatedEntity) GetDirection() int { - return v.direction +func (ae AnimatedEntity) GetDirection() int { + return ae.direction } -func (v *AnimatedEntity) getStepLength(tickTime float64) (float64, float64) { - speed := 6.0 - length := tickTime * speed - - angle := 359 - d2common.GetAngleBetween( - v.LocationX, - v.LocationY, - v.TargetX, - v.TargetY, - ) - radians := (math.Pi / 180.0) * float64(angle) - oneStepX := length * math.Cos(radians) - oneStepY := length * math.Sin(radians) - return oneStepX, oneStepY -} - -func (v *AnimatedEntity) Step(tickTime float64) { - stepX, stepY := v.getStepLength(tickTime) - - if d2common.AlmostEqual(v.LocationX, v.TargetX, stepX) { - v.LocationX = v.TargetX - } - if d2common.AlmostEqual(v.LocationY, v.TargetY, stepY) { - v.LocationY = v.TargetY - } - if v.LocationX != v.TargetX { - v.LocationX += stepX - } - if v.LocationY != v.TargetY { - v.LocationY += stepY - } - - v.subcellX = 1 + math.Mod(v.LocationX, 5) - v.subcellY = 1 + math.Mod(v.LocationY, 5) - v.TileX = int(v.LocationX / 5) - v.TileY = int(v.LocationY / 5) - - if (v.LocationX != v.TargetX) || (v.LocationY != v.TargetY) { - return - } - - if len(v.path) > 0 { - v.SetTarget(v.path[0].(*PathTile).X * 5, v.path[0].(*PathTile).Y * 5, 1) - - if len(v.path) > 1 { - v.path = v.path[1:] - } else { - v.path = []astar.Pather{} - } - return - } +// rotate sets direction and changes animation +func (ae *AnimatedEntity) rotate(angle float64) { + ae.direction = angleToDirection(angle, ae.animation.GetDirectionCount()) - v.repetitions = 3 + rand.Intn(5) - newAnimationMode := d2enum.AnimationModeObjectNeutral - // TODO: Figure out what 1-3 are for, 4 is correct. - switch v.action { - case 1: - newAnimationMode = d2enum.AnimationModeMonsterNeutral - case 2: - newAnimationMode = d2enum.AnimationModeMonsterNeutral - case 3: - newAnimationMode = d2enum.AnimationModeMonsterNeutral + var layerDirection int + switch ae.animation.GetDirectionCount() { case 4: - newAnimationMode = d2enum.AnimationModeMonsterSkill1 - v.repetitions = 0 - } - - v.composite.ResetPlayedCount() - if v.animationMode != newAnimationMode.String() { - v.SetMode(newAnimationMode.String(), v.weaponClass, v.direction) - } -} - -func (v *AnimatedEntity) HasPathFinding() bool { - return len(v.path) > 0 -} - -// SetTarget sets target coordinates and changes animation based on proximity and direction -func (v *AnimatedEntity) SetTarget(tx, ty float64, action int32) { - angle := 359 - d2common.GetAngleBetween( - v.LocationX, - v.LocationY, - tx, - ty, - ) - - v.action = action - // TODO: Check if is in town and if is player. - newAnimationMode := d2enum.AnimationModeMonsterWalk.String() - if tx != v.LocationX || ty != v.LocationY { - v.TargetX, v.TargetY = tx, ty - newAnimationMode = d2enum.AnimationModeMonsterWalk.String() + layerDirection = d2dcc.CofToDir4[ae.direction] + case 8: + layerDirection = d2dcc.CofToDir8[ae.direction] + case 16: + layerDirection = d2dcc.CofToDir16[ae.direction] + case 32: + layerDirection = d2dcc.CofToDir32[ae.direction] } - if newAnimationMode != v.animationMode { - v.SetMode(newAnimationMode, v.weaponClass, v.direction) - } - - newDirection := angleToDirection(float64(angle), v.composite.GetDirectionCount()) - - if newDirection != v.GetDirection() { - v.SetMode(v.animationMode, v.weaponClass, newDirection) - } -} - -func angleToDirection(angle float64, numberOfDirections int) int { - if numberOfDirections == 0 { - return 0 - } - - degreesPerDirection := 360.0 / float64(numberOfDirections) - offset := 45.0 - (degreesPerDirection / 2) - newDirection := int((angle - offset) / degreesPerDirection) - if newDirection >= numberOfDirections { - newDirection = newDirection - numberOfDirections - } else if newDirection < 0 { - newDirection = numberOfDirections + newDirection - } - - return newDirection -} - -func (v *AnimatedEntity) Advance(elapsed float64) { - v.composite.Advance(elapsed) + ae.animation.SetDirection(layerDirection) } -func (v *AnimatedEntity) GetPosition() (float64, float64) { - return float64(v.TileX), float64(v.TileY) +func (ae *AnimatedEntity) Advance(elapsed float64) { + ae.animation.Advance(elapsed) } diff --git a/d2core/d2map/engine.go b/d2core/d2map/engine.go index b8435c291..f0f0b6d9e 100644 --- a/d2core/d2map/engine.go +++ b/d2core/d2map/engine.go @@ -128,6 +128,22 @@ func (m *MapEngine) AddEntity(entity MapEntity) { m.entities = append(m.entities, entity) } +func (m *MapEngine) RemoveEntity(entity MapEntity) { + if entity == nil { + return + } + + // In-place filter to remove the given entity. + n := 0 + for _, check := range m.entities { + if check != entity { + m.entities[n] = check + n++ + } + } + m.entities = m.entities[:n] +} + func (m *MapEngine) Advance(tickTime float64) { for _, region := range m.regions { if region.isVisbile(m.viewport) { @@ -151,20 +167,20 @@ func (m *MapEngine) Render(target d2render.Surface) { } } -func (m *MapEngine) PathFind(startX, startY, endX, endY float64) (path []astar.Pather, distance float64, found bool){ +func (m *MapEngine) PathFind(startX, startY, endX, endY float64) (path []astar.Pather, distance float64, found bool) { startTileX := int(math.Floor(startX)) startTileY := int(math.Floor(startY)) startSubtileX := int((startX - float64(int(startX))) * 5) startSubtileY := int((startY - float64(int(startY))) * 5) startRegion := m.GetRegionAtTile(startTileX, startTileY) - startNode := &startRegion.walkableArea[startSubtileY + ((startTileY - startRegion.tileRect.Top) * 5)][startSubtileX + ((startTileX - startRegion.tileRect.Left) * 5)] + startNode := &startRegion.walkableArea[startSubtileY+((startTileY-startRegion.tileRect.Top)*5)][startSubtileX+((startTileX-startRegion.tileRect.Left)*5)] endTileX := int(math.Floor(endX)) endTileY := int(math.Floor(endY)) endSubtileX := int((endX - float64(int(endX))) * 5) endSubtileY := int((endY - float64(int(endY))) * 5) endRegion := m.GetRegionAtTile(endTileX, endTileY) - endNode := &endRegion.walkableArea[endSubtileY + ((endTileY - endRegion.tileRect.Top) * 5)][endSubtileX + ((endTileX - endRegion.tileRect.Left) * 5)] + endNode := &endRegion.walkableArea[endSubtileY+((endTileY-endRegion.tileRect.Top)*5)][endSubtileX+((endTileX-endRegion.tileRect.Left)*5)] path, distance, found = astar.Path(endNode, startNode) if path != nil { diff --git a/d2core/d2map/hero.go b/d2core/d2map/hero.go index 6c10071d3..1e3d4ee7b 100644 --- a/d2core/d2map/hero.go +++ b/d2core/d2map/hero.go @@ -9,13 +9,13 @@ import ( ) type Hero struct { - AnimatedEntity *AnimatedEntity - Equipment d2inventory.CharacterEquipment - mode d2enum.AnimationMode - direction int + *AnimatedComposite + Equipment d2inventory.CharacterEquipment + mode d2enum.AnimationMode + direction int } -func CreateHero(x, y int32, direction int, heroType d2enum.Hero, equipment d2inventory.CharacterEquipment) *Hero { +func CreateHero(x, y int, direction int, heroType d2enum.Hero, equipment d2inventory.CharacterEquipment) *Hero { object := &d2datadict.ObjectLookupRecord{ Mode: d2enum.AnimationModePlayerNeutral.String(), Base: "/data/global/chars", @@ -32,30 +32,30 @@ func CreateHero(x, y int32, direction int, heroType d2enum.Hero, equipment d2inv LH: equipment.LeftHand.ItemCode(), } - entity, err := CreateAnimatedEntity(x, y, object, d2resource.PaletteUnits) + entity, err := CreateAnimatedComposite(x, y, object, d2resource.PaletteUnits) if err != nil { panic(err) } - result := &Hero{AnimatedEntity: entity, Equipment: equipment, mode: d2enum.AnimationModePlayerTownNeutral, direction: direction} - result.AnimatedEntity.SetMode(result.mode.String(), equipment.RightHand.WeaponClass(), direction) + result := &Hero{ + AnimatedComposite: entity, + Equipment: equipment, + mode: d2enum.AnimationModePlayerTownNeutral, + direction: direction, + } + result.SetMode(result.mode.String(), equipment.RightHand.WeaponClass(), direction) return result } func (v *Hero) Advance(tickTime float64) { - if v.AnimatedEntity.LocationX != v.AnimatedEntity.TargetX || - v.AnimatedEntity.LocationY != v.AnimatedEntity.TargetY || - v.AnimatedEntity.HasPathFinding(){ - v.AnimatedEntity.Step(tickTime) - } - - v.AnimatedEntity.Advance(tickTime) + v.Step(tickTime) + v.AnimatedComposite.Advance(tickTime) } func (v *Hero) Render(target d2render.Surface) { - v.AnimatedEntity.Render(target) + v.AnimatedComposite.Render(target) } func (v *Hero) GetPosition() (float64, float64) { - return v.AnimatedEntity.GetPosition() + return v.AnimatedComposite.GetPosition() } diff --git a/d2core/d2map/map_entity.go b/d2core/d2map/map_entity.go new file mode 100644 index 000000000..49a43c654 --- /dev/null +++ b/d2core/d2map/map_entity.go @@ -0,0 +1,152 @@ +package d2map + +import ( + "github.com/OpenDiablo2/OpenDiablo2/d2common" + "github.com/beefsack/go-astar" + "math" +) + +// mapEntity represents an entity on the map that can be animated +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 + weaponClass string + offsetX, offsetY int + TargetX float64 + TargetY float64 + Speed float64 + path []astar.Pather + + done func() + directioner func(angle float64) +} + +// createMapEntity creates an instance of mapEntity +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), + Speed: 6, + path: []astar.Pather{}, + } +} + +func (m *mapEntity) SetPath(path []astar.Pather, done func()) { + m.path = path + m.done = done +} + +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 +} + +func (m *mapEntity) IsAtTarget() bool { + return m.LocationX == m.TargetX && m.LocationY == m.TargetY && !m.HasPathFinding() +} + +func (m *mapEntity) Step(tickTime float64) { + if m.IsAtTarget() { + if m.done != nil { + m.done() + m.done = nil + } + return + } + + stepX, stepY := m.getStepLength(tickTime) + + if d2common.AlmostEqual(m.LocationX, m.TargetX, stepX) { + m.LocationX = m.TargetX + } + if d2common.AlmostEqual(m.LocationY, m.TargetY, stepY) { + m.LocationY = m.TargetY + } + if m.LocationX != m.TargetX { + m.LocationX += stepX + } + if m.LocationY != m.TargetY { + m.LocationY += stepY + } + + 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 (m.LocationX != m.TargetX) || (m.LocationY != m.TargetY) { + return + } + + if len(m.path) > 0 { + m.SetTarget(m.path[0].(*PathTile).X*5, m.path[0].(*PathTile).Y*5, m.done) + + if len(m.path) > 1 { + m.path = m.path[1:] + } else { + m.path = []astar.Pather{} + } + return + } + +} + +func (m *mapEntity) HasPathFinding() bool { + return len(m.path) > 0 +} + +// 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.done = done + + if m.directioner != nil { + angle := 359 - d2common.GetAngleBetween( + m.LocationX, + m.LocationY, + tx, + ty, + ) + m.directioner(float64(angle)) + } +} + +func angleToDirection(angle float64, numberOfDirections int) int { + if numberOfDirections == 0 { + return 0 + } + + degreesPerDirection := 360.0 / float64(numberOfDirections) + offset := 45.0 - (degreesPerDirection / 2) + newDirection := int((angle - offset) / degreesPerDirection) + if newDirection >= numberOfDirections { + newDirection = newDirection - numberOfDirections + } else if newDirection < 0 { + newDirection = numberOfDirections + newDirection + } + + return newDirection +} + +func (m *mapEntity) GetPosition() (float64, float64) { + return float64(m.TileX), float64(m.TileY) +} diff --git a/d2core/d2map/missile.go b/d2core/d2map/missile.go new file mode 100644 index 000000000..17933f547 --- /dev/null +++ b/d2core/d2map/missile.go @@ -0,0 +1,39 @@ +package d2map + +import ( + "fmt" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource" + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset" +) + +type Missile struct { + *AnimatedEntity + record *d2datadict.MissileRecord +} + +func CreateMissile(x, y int, record *d2datadict.MissileRecord) (*Missile, error) { + animation, err := d2asset.LoadAnimation( + fmt.Sprintf("%s/%s.dcc", d2resource.MissileData, record.Animation.CelFileName), + d2resource.PaletteUnits, + ) + if err != nil { + return nil, err + } + + animation.PlayForward() + entity := CreateAnimatedEntity(x, y, animation) + + result := &Missile{ + AnimatedEntity: entity, + record: record, + } + result.Speed = float64(record.Velocity) + return result, nil +} + +func (m *Missile) Advance(tickTime float64) { + // TODO: collision detection + m.Step(tickTime) + m.AnimatedEntity.Advance(tickTime) +} diff --git a/d2core/d2map/npc.go b/d2core/d2map/npc.go index 713b7e831..0e6852715 100644 --- a/d2core/d2map/npc.go +++ b/d2core/d2map/npc.go @@ -3,25 +3,29 @@ package d2map import ( "github.com/OpenDiablo2/OpenDiablo2/d2common" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource" - "github.com/OpenDiablo2/OpenDiablo2/d2core/d2render" + "math/rand" ) type NPC struct { - AnimatedEntity *AnimatedEntity - HasPaths bool - Paths []d2common.Path - path int + *AnimatedComposite + action int + HasPaths bool + Paths []d2common.Path + path int + isDone bool + repetitions int } -func CreateNPC(x, y int32, object *d2datadict.ObjectLookupRecord, direction int) *NPC { - entity, err := CreateAnimatedEntity(x, y, object, d2resource.PaletteUnits) +func CreateNPC(x, y int, object *d2datadict.ObjectLookupRecord, direction int) *NPC { + entity, err := CreateAnimatedComposite(x, y, object, d2resource.PaletteUnits) if err != nil { panic(err) } - result := &NPC{AnimatedEntity: entity, HasPaths: false} - result.AnimatedEntity.SetMode(object.Mode, object.Class, direction) + result := &NPC{AnimatedComposite: entity, HasPaths: false} + result.SetMode(object.Mode, object.Class, direction) return result } @@ -41,34 +45,52 @@ func (v *NPC) NextPath() d2common.Path { func (v *NPC) SetPaths(paths []d2common.Path) { v.Paths = paths v.HasPaths = len(paths) > 0 -} - -func (v *NPC) Render(target d2render.Surface) { - v.AnimatedEntity.Render(target) -} - -func (v *NPC) GetPosition() (float64, float64) { - return v.AnimatedEntity.GetPosition() + v.isDone = true } func (v *NPC) Advance(tickTime float64) { - if v.HasPaths && - v.AnimatedEntity.LocationX == v.AnimatedEntity.TargetX && - v.AnimatedEntity.LocationY == v.AnimatedEntity.TargetY && - v.AnimatedEntity.Wait() { + v.Step(tickTime) + v.AnimatedComposite.Advance(tickTime) + + if v.HasPaths && v.wait() { // If at the target, set target to the next path. + v.isDone = false path := v.NextPath() - v.AnimatedEntity.SetTarget( + v.SetTarget( float64(path.X), float64(path.Y), - path.Action, + v.next, ) + v.action = path.Action } +} + +// If an npc has a path to pause at each location. +// Waits for animation to end and all repetitions to be exhausted. +func (v *NPC) wait() bool { + return v.isDone && v.composite.GetPlayedCount() > v.repetitions +} - if v.AnimatedEntity.LocationX != v.AnimatedEntity.TargetX || - v.AnimatedEntity.LocationY != v.AnimatedEntity.TargetY { - v.AnimatedEntity.Step(tickTime) +func (v *NPC) next() { + v.isDone = true + v.repetitions = 3 + rand.Intn(5) + newAnimationMode := d2enum.AnimationModeObjectNeutral + // TODO: Figure out what 1-3 are for, 4 is correct. + switch v.action { + case 1: + newAnimationMode = d2enum.AnimationModeMonsterNeutral + case 2: + newAnimationMode = d2enum.AnimationModeMonsterNeutral + case 3: + newAnimationMode = d2enum.AnimationModeMonsterNeutral + case 4: + newAnimationMode = d2enum.AnimationModeMonsterSkill1 + v.repetitions = 0 + default: + v.repetitions = 0 } - v.AnimatedEntity.Advance(tickTime) + if v.animationMode != newAnimationMode.String() { + v.SetMode(newAnimationMode.String(), v.weaponClass, v.direction) + } } diff --git a/d2core/d2map/region.go b/d2core/d2map/region.go index b3af6e5af..efd95dc96 100644 --- a/d2core/d2map/region.go +++ b/d2core/d2map/region.go @@ -262,18 +262,18 @@ func (mr *MapRegion) loadEntities() []MapEntity { var entities []MapEntity for _, object := range mr.ds1.Objects { - worldX, worldY := mr.getTileWorldPosition(int(object.X), int(object.Y)) + worldX, worldY := mr.getTileWorldPosition(object.X, object.Y) switch object.Lookup.Type { case d2datadict.ObjectTypeCharacter: if object.Lookup.Base != "" && object.Lookup.Token != "" && object.Lookup.TR != "" { - npc := CreateNPC(int32(worldX), int32(worldY), object.Lookup, 0) + npc := CreateNPC(int(worldX), int(worldY), object.Lookup, 0) npc.SetPaths(object.Paths) entities = append(entities, npc) } case d2datadict.ObjectTypeItem: if object.ObjectInfo != nil && object.ObjectInfo.Draw && object.Lookup.Base != "" && object.Lookup.Token != "" { - entity, err := CreateAnimatedEntity(int32(worldX), int32(worldY), object.Lookup, d2resource.PaletteUnits) + entity, err := CreateAnimatedComposite(int(worldX), int(worldY), object.Lookup, d2resource.PaletteUnits) if err != nil { panic(err) } diff --git a/d2game/d2gamescene/character_select.go b/d2game/d2gamescene/character_select.go index 56c1a1574..02affb038 100644 --- a/d2game/d2gamescene/character_select.go +++ b/d2game/d2gamescene/character_select.go @@ -243,7 +243,7 @@ func (v *CharacterSelect) Advance(tickTime float64) error { } for _, hero := range v.characterImage { if hero != nil { - hero.AnimatedEntity.Advance(tickTime) + hero.AnimatedComposite.Advance(tickTime) } } diff --git a/d2game/d2gamescene/game.go b/d2game/d2gamescene/game.go index a0bc6c29f..a8a1b9ca8 100644 --- a/d2game/d2gamescene/game.go +++ b/d2game/d2gamescene/game.go @@ -52,8 +52,8 @@ func (v *Game) OnLoad() error { startX, startY := v.mapEngine.GetStartPosition() v.hero = d2map.CreateHero( - int32(startX*5)+3, - int32(startY*5)+3, + int(startX*5)+3, + int(startY*5)+3, 0, v.gameState.HeroType, v.gameState.Equipment, @@ -82,7 +82,7 @@ func (v *Game) Render(screen d2render.Surface) error { func (v *Game) Advance(tickTime float64) error { v.mapEngine.Advance(tickTime) - rx, ry := v.mapEngine.WorldToOrtho(v.hero.AnimatedEntity.LocationX/5, v.hero.AnimatedEntity.LocationY/5) + rx, ry := v.mapEngine.WorldToOrtho(v.hero.AnimatedComposite.LocationX/5, v.hero.AnimatedComposite.LocationY/5) v.mapEngine.MoveCameraTo(rx, ry) return nil } diff --git a/d2game/d2player/game_controls.go b/d2game/d2player/game_controls.go index 4c5cc6509..4da5a8e4d 100644 --- a/d2game/d2player/game_controls.go +++ b/d2game/d2player/game_controls.go @@ -1,11 +1,14 @@ package d2player import ( + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2input" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2map" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2render" + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2term" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2ui" ) @@ -16,6 +19,9 @@ type Panel interface { Close() } +// ID of missile to create when user right clicks. +var missileID = 59 + type GameControls struct { hero *d2map.Hero mapEngine *d2map.MapEngine @@ -30,6 +36,10 @@ type GameControls struct { } func NewGameControls(hero *d2map.Hero, mapEngine *d2map.MapEngine) *GameControls { + d2term.BindAction("setmissile", "set missile id to summon on right click", func(id int) { + missileID = id + }) + return &GameControls{ hero: hero, mapEngine: mapEngine, @@ -52,18 +62,42 @@ func (g *GameControls) OnKeyDown(event d2input.KeyEvent) bool { } func (g *GameControls) OnMouseButtonDown(event d2input.MouseEvent) bool { + px, py := g.mapEngine.ScreenToWorld(event.X, event.Y) + px = float64(int(px*10)) / 10.0 + py = float64(int(py*10)) / 10.0 + heroPosX := g.hero.AnimatedComposite.LocationX / 5.0 + heroPosY := g.hero.AnimatedComposite.LocationY / 5.0 + if event.Button == d2input.MouseButtonLeft { - px, py := g.mapEngine.ScreenToWorld(event.X, event.Y) - px = float64(int(px*10)) / 10.0 - py = float64(int(py*10)) / 10.0 - heroPosX := g.hero.AnimatedEntity.LocationX / 5.0 - heroPosY := g.hero.AnimatedEntity.LocationY / 5.0 path, _, found := g.mapEngine.PathFind(heroPosX, heroPosY, px, py) if found { - g.hero.AnimatedEntity.SetPath(path) + g.hero.AnimatedComposite.SetPath(path, func() { + g.hero.AnimatedComposite.SetAnimationMode( + d2enum.AnimationModeObjectNeutral.String(), + ) + }) + } + return true + } + + if event.Button == d2input.MouseButtonRight { + missile, err := d2map.CreateMissile( + int(g.hero.AnimatedComposite.LocationX), + int(g.hero.AnimatedComposite.LocationY), + d2datadict.Missiles[missileID], + ) + if err != nil { + return false } + + missile.SetTarget(px*5, py*5, func() { + g.mapEngine.RemoveEntity(missile) + }) + + g.mapEngine.AddEntity(missile) return true } + return false }