D2mapengine remove entity, minor edits (#694)

* implement entity removal

* add rgba color func, fix some lint errors in d2map

* bugfix for map entity tests
This commit is contained in:
lord 2020-08-05 18:27:45 -07:00 committed by GitHub
parent 0a6915a040
commit 8b2b991b12
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 159 additions and 54 deletions

View File

@ -4,6 +4,7 @@ import "github.com/OpenDiablo2/OpenDiablo2/d2common/d2math/d2vector"
// MapEntity is something that can be positioned on and rendered on the game map
type MapEntity interface {
ID() string
Render(target Surface)
Advance(tickTime float64)
GetPosition() d2vector.Position

28
d2common/rgba_color.go Normal file
View File

@ -0,0 +1,28 @@
package d2common
import "image/color"
func Color(rgba uint32) color.RGBA {
result := color.RGBA{}
a, b, g, r := 0, 1, 2, 3
byteWidth := 8
byteMask := 0xff
for idx := 0; idx < 4; idx++ {
shift := idx * byteWidth
component := uint8(rgba>>shift) & uint8(byteMask)
switch idx {
case a:
result.A = component
case b:
result.B = component
case g:
result.G = component
case r:
result.R = component
}
}
return result
}

View File

@ -20,8 +20,8 @@ import (
// MapEngine loads the tiles which make up the isometric map and the entities
type MapEngine struct {
seed int64 // The map seed
entities []d2interface.MapEntity // Entities on the map
seed int64 // The map seed
entities map[string]d2interface.MapEntity // Entities on the map
tiles []MapTile
size d2common.Size // Size of the map, in tiles
levelType d2datadict.LevelTypeRecord // Level type of this map
@ -44,7 +44,7 @@ func (m *MapEngine) GetStartingPosition() (x, y int) {
// ResetMap clears all map and entity data and reloads it from the cached files.
func (m *MapEngine) ResetMap(levelType d2enum.RegionIdType, width, height int) {
m.entities = make([]d2interface.MapEntity, 0)
m.entities = make(map[string]d2interface.MapEntity)
m.levelType = d2datadict.LevelTypes[levelType]
m.size = d2common.Size{Width: width, Height: height}
m.tiles = make([]MapTile, width*height)
@ -155,7 +155,7 @@ func (m *MapEngine) PlaceStamp(stamp *d2mapstamp.Stamp, tileOffsetX, tileOffsetY
// Copy over the map tile data
for y := 0; y < stampH; y++ {
for x := 0; x < stampW; x++ {
targetTileIndex := m.tileCoordinateToIndex((x + xMin), (y + yMin))
targetTileIndex := m.tileCoordinateToIndex(x+xMin, y+yMin)
stampTile := *stamp.Tile(x, y)
m.tiles[targetTileIndex].RegionType = stamp.RegionID()
m.tiles[targetTileIndex].Components = stampTile
@ -164,7 +164,11 @@ func (m *MapEngine) PlaceStamp(stamp *d2mapstamp.Stamp, tileOffsetX, tileOffsetY
}
// Copy over the entities
m.entities = append(m.entities, stamp.Entities(tileOffsetX, tileOffsetY)...)
stampEntities := stamp.Entities(tileOffsetX, tileOffsetY)
for idx := range stampEntities {
e := stampEntities[idx]
m.entities[e.ID()] = e
}
}
// converts x,y tile coordinate into index in MapEngine.tiles
@ -172,7 +176,7 @@ func (m *MapEngine) tileCoordinateToIndex(x, y int) int {
return x + (y * m.size.Width)
}
// converts tile index from MapEngine.tiles to x,y coordinate
// tileIndexToCoordinate converts tile index from MapEngine.tiles to x,y coordinate
func (m *MapEngine) tileIndexToCoordinate(index int) (x, y int) {
return index % m.size.Width, index / m.size.Width
}
@ -196,8 +200,8 @@ func (m *MapEngine) TileAt(tileX, tileY int) *MapTile {
}
// Entities returns a pointer a slice of all map entities.
func (m *MapEngine) Entities() *[]d2interface.MapEntity {
return &m.entities
func (m *MapEngine) Entities() map[string]d2interface.MapEntity {
return m.entities
}
// Seed returns the map generation seed.
@ -207,15 +211,16 @@ func (m *MapEngine) Seed() int64 {
// AddEntity adds an entity to a slice containing all entities.
func (m *MapEngine) AddEntity(entity d2interface.MapEntity) {
m.entities = append(m.entities, entity)
m.entities[entity.ID()] = entity
}
// RemoveEntity is not currently implemented.
// RemoveEntity removes an entity from the map engine
func (m *MapEngine) RemoveEntity(entity d2interface.MapEntity) {
if entity == nil {
return
}
// m.entities.Remove(entity)
delete(m.entities, entity.ID())
}
// GetTiles returns a slice of all tiles matching the given style,
@ -264,8 +269,8 @@ func (m *MapEngine) GetCenterPosition() (x, y float64) {
// Advance calls the Advance() method for all entities,
// processing a single tick.
func (m *MapEngine) Advance(tickTime float64) {
for idx := range m.entities {
m.entities[idx].Advance(tickTime)
for ID := range m.entities {
m.entities[ID].Advance(tickTime)
}
}

View File

@ -3,7 +3,6 @@ package d2mapentity
import (
"errors"
"fmt"
"github.com/OpenDiablo2/OpenDiablo2/d2common"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
@ -50,7 +49,6 @@ func NewPlayer(id, name string, x, y, direction int, heroType d2enum.Hero,
stats.Stamina = stats.MaxStamina
result := &Player{
ID: id,
mapEntity: newMapEntity(x, y),
composite: composite,
Equipment: equipment,
@ -62,6 +60,8 @@ func NewPlayer(id, name string, x, y, direction int, heroType d2enum.Hero,
isInTown: true,
isRunning: true,
}
result.mapEntity.uuid = id
result.SetSpeed(baseRunSpeed)
result.mapEntity.directioner = result.rotate
err = composite.SetMode(d2enum.PlayerAnimationModeTownNeutral, equipment.RightHand.GetWeaponClass())

View File

@ -19,6 +19,11 @@ type Item struct {
Item *diablo2item.Item
}
// ID returns the item uuid
func (i *Item) ID() string {
return i.AnimatedEntity.uuid
}
// GetPosition returns the item position vector
func (i *Item) GetPosition() d2vector.Position {
return i.AnimatedEntity.Position

View File

@ -1,6 +1,8 @@
package d2mapentity
import (
uuid "github.com/satori/go.uuid"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2math/d2vector"
)
@ -10,6 +12,7 @@ const (
// mapEntity represents an entity on the map that can be animated
type mapEntity struct {
uuid string
Position d2vector.Position
Target d2vector.Position
velocity d2vector.Vector
@ -29,6 +32,7 @@ func newMapEntity(x, y int) mapEntity {
pos := d2vector.NewPosition(float64(x), float64(y))
return mapEntity{
uuid: uuid.NewV4().String(),
Position: pos,
Target: pos,
velocity: *d2vector.VectorZero(),

View File

@ -14,6 +14,11 @@ type Missile struct {
record *d2datadict.MissileRecord
}
// ID returns the missile uuid
func (m *Missile) ID() string {
return m.AnimatedEntity.uuid
}
// GetPosition returns the position of the missile
func (m *Missile) GetPosition() d2vector.Position {
return m.AnimatedEntity.Position

View File

@ -43,6 +43,11 @@ func selectEquip(slice []string) string {
return ""
}
// ID returns the NPC uuid
func (v *NPC) ID() string {
return v.mapEntity.uuid
}
// Render renders this entity's animated composite.
func (v *NPC) Render(target d2interface.Surface) {
renderOffset := v.Position.RenderOffset()

View File

@ -14,7 +14,6 @@ import (
// Player is the player character entity.
type Player struct {
mapEntity
ID string
name string
animationMode string
composite *d2asset.Composite
@ -35,6 +34,11 @@ type Player struct {
const baseWalkSpeed = 6.0
const baseRunSpeed = 9.0
// ID returns the Player uuid
func (p *Player) ID() string {
return p.mapEntity.uuid
}
// SetIsInTown sets a flag indicating that the player is in town.
func (p *Player) SetIsInTown(isInTown bool) {
p.isInTown = isInTown
@ -86,7 +90,7 @@ func (p *Player) Advance(tickTime float64) {
}
if err := p.composite.Advance(tickTime); err != nil {
fmt.Printf("failed to advance composite animation of player: %s, err: %v\n", p.ID, err)
fmt.Printf("failed to advance composite animation of player: %s, err: %v\n", p.ID(), err)
}
if p.lastPathSize != len(p.path) {
@ -109,7 +113,7 @@ func (p *Player) Render(target d2interface.Surface) {
defer target.Pop()
if err := p.composite.Render(target); err != nil {
fmt.Printf("failed to render the composite of player: %s, err: %v\n", p.ID, err)
fmt.Printf("failed to render the composite of player: %s, err: %v\n", p.ID(), err)
}
}
@ -173,7 +177,8 @@ func (p *Player) IsCasting() bool {
func (p *Player) SetCasting() {
p.isCasting = true
if err := p.SetAnimationMode(d2enum.PlayerAnimationModeCast); err != nil {
fmt.Printf("failed to set animationMode of player: %s to: %d, err: %v\n", p.ID, d2enum.PlayerAnimationModeCast, err)
fmtStr := "failed to set animationMode of player: %s to: %d, err: %v\n"
fmt.Printf(fmtStr, p.ID(), d2enum.PlayerAnimationModeCast, err)
}
}

View File

@ -7,6 +7,7 @@ import (
"log"
"math"
"github.com/OpenDiablo2/OpenDiablo2/d2common"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2math/d2vector"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
@ -19,6 +20,31 @@ import (
const (
screenMiddleX = 400
two = 2
dbgOffsetXY = 40
dbgBoxWidth = 220
dbgBoxHeight = 60
dbgBoxPadding = 10
dbgCollisionSize = 5
dbgCollisionOffsetX = -3
dbgCollisionOffsetY = 4
whiteHalfOpacity = 0xffffff7f
blackQuarterOpacity = 0x00000040
lightGreenFullOpacity = 0x40ff00ff
magentaFullOpacity = 0xff00ffff
yellowFullOpacity = 0xffff00ff
lightBlueQuarterOpacity = 0x5050ff32
whiteQuarterOpacity = 0xffffff64
redQuarterOpacity = 0x74000064
subtilesPerTile = 5
orthoSubTileWidth = 16
orthoSubTileHeight = 8
orthoTileWidth = subtilesPerTile * orthoSubTileWidth
orthoTileHeight = subtilesPerTile * orthoSubTileHeight
)
// MapRenderer manages the game viewport and Camera. It requests tile and entity data from MapEngine and renders it.
@ -172,7 +198,7 @@ func (mr *MapRenderer) renderPass2(target d2interface.Surface, startX, startY, e
mr.viewport.PushTranslationWorld(float64(tileX), float64(tileY))
// TODO: Do not loop over every entity every frame
for _, mapEntity := range *mr.mapEngine.Entities() {
for _, mapEntity := range mr.mapEngine.Entities() {
pos := mapEntity.GetPosition()
vec := pos.World()
entityX, entityY := vec.X(), vec.Y()
@ -204,7 +230,7 @@ func (mr *MapRenderer) renderPass3(target d2interface.Surface, startX, startY, e
mr.renderTilePass2(tile, target)
// TODO: Do not loop over every entity every frame
for _, mapEntity := range *mr.mapEngine.Entities() {
for _, mapEntity := range mr.mapEngine.Entities() {
pos := mapEntity.GetPosition()
vec := pos.World()
entityX, entityY := vec.X(), vec.Y()
@ -327,9 +353,10 @@ func (mr *MapRenderer) renderShadow(tile d2ds1.FloorShadowRecord, target d2inter
defer mr.viewport.PushTranslationOrtho(-80, float64(tile.YAdjust)).PopTranslation()
target.PushTranslation(mr.viewport.GetTranslationScreen())
target.PushColor(color.RGBA{R: 255, G: 255, B: 255, A: 160}) //nolint:gomnd // Not a magic number...
defer target.Pop()
defer target.PopN(2)
target.PushColor(color.RGBA{R: 255, G: 255, B: 255, A: 160}) //nolint:gomnd // Not a magic number...
defer target.Pop()
if err := target.Render(img); err != nil {
fmt.Printf("failed to render the shadow, err: %v\n", err)
@ -346,26 +373,27 @@ func (mr *MapRenderer) renderMapDebug(mapDebugVisLevel int, target d2interface.S
}
}
//nolint:funlen // doesn't make sense to split this function
func (mr *MapRenderer) renderEntityDebug(target d2interface.Surface) {
entities := *mr.mapEngine.Entities()
entities := mr.mapEngine.Entities()
for idx := range entities {
e := entities[idx]
pos := e.GetPosition()
world := pos
x, y := world.X()/5, world.Y()/5
x, y := world.X()/subtilesPerTile, world.Y()/subtilesPerTile
velocity := e.GetVelocity()
velocity = *velocity.Clone()
vx, vy := mr.viewport.WorldToOrtho(velocity.X(), velocity.Y())
screenX, screenY := mr.viewport.WorldToScreen(x, y)
offX, offY := 40, -40
offX, offY := dbgOffsetXY, -dbgOffsetXY
entScreenXf, entScreenYf := mr.WorldToScreenF(e.GetPositionF())
entScreenX := int(math.Floor(entScreenXf))
entScreenY := int(math.Floor(entScreenYf))
entityWidth, entityHeight := e.GetSize()
halfWidth, halfHeight := entityWidth/2, entityHeight/2
halfWidth, halfHeight := entityWidth/two, entityHeight/two
l, r := entScreenX-halfWidth, entScreenX+halfWidth
t, b := entScreenY-halfHeight, entScreenY+halfHeight
mx, my := mr.renderer.GetCursorPos()
@ -373,8 +401,8 @@ func (mr *MapRenderer) renderEntityDebug(target d2interface.Surface) {
yWithin := (t <= my) && (b >= my)
within := xWithin && yWithin
boxLineColor := color.RGBA{255, 0, 255, 128}
boxHoverColor := color.RGBA{255, 255, 0, 128}
boxLineColor := d2common.Color(magentaFullOpacity)
boxHoverColor := d2common.Color(yellowFullOpacity)
boxColor := boxLineColor
@ -382,30 +410,39 @@ func (mr *MapRenderer) renderEntityDebug(target d2interface.Surface) {
boxColor = boxHoverColor
}
stack := 0
// box
mr.viewport.PushTranslationWorld(x, y)
target.PushTranslation(screenX, screenY)
stack++
target.PushTranslation(-halfWidth, -halfHeight)
stack++
target.DrawLine(0, entityHeight, boxColor)
target.DrawLine(entityWidth, 0, boxColor)
target.PushTranslation(entityWidth, entityHeight)
stack++
target.DrawLine(-entityWidth, 0, boxColor)
target.DrawLine(0, -entityHeight, boxColor)
target.PopN(3)
target.PopN(stack)
mr.viewport.PopTranslation()
// hover
if within {
mr.viewport.PushTranslationWorld(x, y)
target.PushTranslation(screenX, screenY)
target.DrawLine(offX, offY, color.RGBA{255, 255, 255, 128})
target.PushTranslation(offX+10, offY-20)
target.PushTranslation(-10, -10)
target.DrawRect(200, 50, color.RGBA{0, 0, 0, 64})
target.DrawLine(offX, offY, d2common.Color(whiteHalfOpacity))
target.PushTranslation(offX+dbgBoxPadding, offY-dbgBoxPadding*two)
target.PushTranslation(-dbgOffsetXY, -dbgOffsetXY)
target.DrawRect(dbgBoxWidth, dbgBoxHeight, d2common.Color(blackQuarterOpacity))
target.Pop()
target.DrawTextf("World (%.2f, %.2f)\nVelocity (%.2f, %.2f)", x, y, vx, vy)
target.Pop()
target.DrawLine(int(vx), int(vy), color.RGBA{64, 255, 0, 255})
target.DrawLine(int(vx), int(vy), d2common.Color(lightGreenFullOpacity))
target.Pop()
mr.viewport.PopTranslation()
}
@ -423,9 +460,9 @@ func (mr *MapRenderer) WorldToScreenF(x, y float64) (screenX, screenY float64) {
}
func (mr *MapRenderer) renderTileDebug(ax, ay, debugVisLevel int, target d2interface.Surface) {
subTileColor := color.RGBA{R: 80, G: 80, B: 255, A: 50}
tileColor := color.RGBA{R: 255, G: 255, B: 255, A: 100}
tileCollisionColor := color.RGBA{R: 128, G: 0, B: 0, A: 100}
subTileColor := d2common.Color(lightBlueQuarterOpacity)
tileColor := d2common.Color(whiteQuarterOpacity)
tileCollisionColor := d2common.Color(redQuarterOpacity)
screenX1, screenY1 := mr.viewport.WorldToScreen(float64(ax), float64(ay))
screenX2, screenY2 := mr.viewport.WorldToScreen(float64(ax+1), float64(ay))
@ -442,15 +479,15 @@ func (mr *MapRenderer) renderTileDebug(ax, ay, debugVisLevel int, target d2inter
if debugVisLevel > 1 {
for i := 1; i <= 4; i++ {
x2 := i * 16
y2 := i * 8
x2 := i * orthoSubTileWidth
y2 := i * orthoSubTileHeight
target.PushTranslation(-x2, y2)
target.DrawLine(80, 40, subTileColor)
target.DrawLine(orthoTileWidth, orthoTileHeight, subTileColor)
target.Pop()
target.PushTranslation(x2, y2)
target.DrawLine(-80, 40, subTileColor)
target.DrawLine(-orthoTileWidth, orthoTileHeight, subTileColor)
target.Pop()
}
@ -458,7 +495,7 @@ func (mr *MapRenderer) renderTileDebug(ax, ay, debugVisLevel int, target d2inter
for i, wall := range tile.Components.Walls {
if wall.Type.Special() {
target.PushTranslation(-20, 10+(i+1)*14)
target.PushTranslation(-20, 10+(i+1)*14) // what are these magic numbers??
target.DrawTextf("s: %v-%v", wall.Style, wall.Sequence)
target.Pop()
}
@ -466,14 +503,14 @@ func (mr *MapRenderer) renderTileDebug(ax, ay, debugVisLevel int, target d2inter
for yy := 0; yy < 5; yy++ {
for xx := 0; xx < 5; xx++ {
isoX := (xx - yy) * 16
isoY := (xx + yy) * 8
isoX := (xx - yy) * orthoSubTileWidth
isoY := (xx + yy) * orthoSubTileHeight
blocked := tile.GetSubTileFlags(xx, yy).BlockWalk
if blocked {
target.PushTranslation(isoX-3, isoY+4)
target.DrawRect(5, 5, tileCollisionColor)
target.PushTranslation(isoX+dbgCollisionOffsetX, isoY+dbgCollisionOffsetY)
target.DrawRect(dbgCollisionSize, dbgCollisionSize, tileCollisionColor)
target.Pop()
}
}

View File

@ -4,6 +4,8 @@ package d2object
import (
"math/rand"
uuid "github.com/satori/go.uuid"
"github.com/OpenDiablo2/OpenDiablo2/d2common"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
@ -15,6 +17,7 @@ import (
// Object represents a composite of animations that can be projected onto the map.
type Object struct {
uuid string
Position d2vector.Position
composite *d2asset.Composite
highlight bool
@ -28,6 +31,7 @@ type Object struct {
func CreateObject(x, y int, objectRec *d2datadict.ObjectRecord, palettePath string) (*Object, error) {
locX, locY := float64(x), float64(y)
entity := &Object{
uuid: uuid.NewV4().String(),
objectRecord: objectRec,
Position: d2vector.NewPosition(locX, locY),
name: d2common.TranslateString(objectRec.Name),
@ -84,6 +88,11 @@ func (ob *Object) setMode(animationMode d2enum.ObjectAnimationMode, direction in
return err
}
// ID returns the object uuid
func (ob *Object) ID() string {
return ob.uuid
}
// Highlight sets the entity highlighted flag to true.
func (ob *Object) Highlight() {
ob.highlight = true

View File

@ -56,7 +56,7 @@ func CreateGame(
// find the local player and its initial location
var startX, startY float64
for _, player := range gameClient.Players {
if player.ID != gameClient.PlayerID {
if player.ID() != gameClient.PlayerID {
continue
}
worldPosition := player.Position.World()
@ -237,7 +237,7 @@ func (v *Game) Advance(elapsed float64) error {
func (v *Game) bindGameControls() error {
for _, player := range v.gameClient.Players {
if player.ID != v.gameClient.PlayerID {
if player.ID() != v.gameClient.PlayerID {
continue
}
@ -253,7 +253,7 @@ func (v *Game) bindGameControls() error {
v.gameControls.Load()
if err := v.inputManager.BindHandler(v.gameControls); err != nil {
fmt.Printf(bindControlsErrStr, player.ID)
fmt.Printf(bindControlsErrStr, player.ID())
}
break

View File

@ -421,8 +421,8 @@ func (g *GameControls) isInActiveMenusRect(px int, py int) bool {
// TODO: consider caching the panels to single image that is reused.
func (g *GameControls) Render(target d2interface.Surface) error {
for entityIdx := range *g.mapEngine.Entities() {
entity := (*g.mapEngine.Entities())[entityIdx]
for entityIdx := range g.mapEngine.Entities() {
entity := (g.mapEngine.Entities())[entityIdx]
if !entity.Selectable() {
continue
}

View File

@ -171,7 +171,7 @@ func (g *GameClient) handleAddPlayerPacket(packet d2netpacket.NetPacket) error {
newPlayer := d2mapentity.NewPlayer(player.ID, player.Name, player.X, player.Y, 0,
player.HeroType, player.Stats, &player.Equipment)
g.Players[newPlayer.ID] = newPlayer
g.Players[newPlayer.ID()] = newPlayer
g.MapEngine.AddEntity(newPlayer)
return nil
@ -214,7 +214,8 @@ func (g *GameClient) handleMovePlayerPacket(packet d2netpacket.NetPacket) error
err := player.SetAnimationMode(player.GetAnimationMode())
if err != nil {
log.Printf("GameClient: error setting animation mode for player %s: %s", player.ID, err)
fmtStr := "GameClient: error setting animation mode for player %s: %s"
log.Printf(fmtStr, player.ID(), err)
}
})
}