diff --git a/d2common/d2enum/region_id.go b/d2common/d2enum/region_id.go index 2bd4f23b..983b14ca 100644 --- a/d2common/d2enum/region_id.go +++ b/d2common/d2enum/region_id.go @@ -3,6 +3,7 @@ package d2enum type RegionIdType int const ( + RegionNone RegionIdType = 0 RegionAct1Town RegionIdType = 1 RegionAct1Wilderness RegionIdType = 2 RegionAct1Cave RegionIdType = 3 diff --git a/d2common/d2fileformats/d2ds1/tilerecord.go b/d2common/d2fileformats/d2ds1/tilerecord.go index ec7798b0..fb5b8ed5 100644 --- a/d2common/d2fileformats/d2ds1/tilerecord.go +++ b/d2common/d2fileformats/d2ds1/tilerecord.go @@ -1,8 +1,15 @@ package d2ds1 +import ( + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" +) + type TileRecord struct { Floors []FloorShadowRecord Walls []WallRecord Shadows []FloorShadowRecord Substitutions []SubstitutionRecord + + // This is set and used internally by the engine to determine what region this map is from + RegionType d2enum.RegionIdType } diff --git a/d2common/path_tile.go b/d2common/path_tile.go new file mode 100644 index 00000000..cabad35a --- /dev/null +++ b/d2common/path_tile.go @@ -0,0 +1,58 @@ +package d2common + +import "github.com/beefsack/go-astar" + +type PathTile struct { + Walkable bool + Up, Down, Left, Right, UpLeft, UpRight, DownLeft, DownRight *PathTile + X, Y float64 +} + +func (t *PathTile) PathNeighbors() []astar.Pather { + result := make([]astar.Pather, 0) + if t.Up != nil { + result = append(result, t.Up) + } + if t.Right != nil { + result = append(result, t.Right) + } + if t.Down != nil { + result = append(result, t.Down) + } + if t.Left != nil { + result = append(result, t.Left) + } + if t.UpLeft != nil { + result = append(result, t.UpLeft) + } + if t.UpRight != nil { + result = append(result, t.UpRight) + } + if t.DownLeft != nil { + result = append(result, t.DownLeft) + } + if t.DownRight != nil { + result = append(result, t.DownRight) + } + + return result +} + +func (t *PathTile) PathNeighborCost(to astar.Pather) float64 { + return 1 // No cost specifics currently... +} + +func (t *PathTile) PathEstimatedCost(to astar.Pather) float64 { + toT := to.(*PathTile) + absX := toT.X - t.X + if absX < 0 { + absX = -absX + } + absY := toT.Y - t.Y + if absY < 0 { + absY = -absY + } + r := absX + absY + + return r +} diff --git a/d2common/size.go b/d2common/size.go new file mode 100644 index 00000000..fa8e432f --- /dev/null +++ b/d2common/size.go @@ -0,0 +1,5 @@ +package d2common + +type Size struct { + Width, Height int +} diff --git a/d2core/d2map/act1_overworld.go b/d2core/d2map/act1_overworld.go deleted file mode 100644 index 444b2900..00000000 --- a/d2core/d2map/act1_overworld.go +++ /dev/null @@ -1,66 +0,0 @@ -package d2map - -import ( - "math/rand" - "strings" - - "github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2wilderness" - - "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" -) - -func (m *MapEngine) GenerateAct1Overworld(cacheTiles bool) { - rand.Seed(m.seed) - region, entities := loadRegion(m.seed, 0, 0, d2enum.RegionAct1Town, 1, -1, cacheTiles) - m.regions = append(m.regions, region) - m.entities.Add(entities...) - if strings.Contains(region.regionPath, "E1") { - region, entities := loadRegion(m.seed, region.tileRect.Width, 0, d2enum.RegionAct1Town, 2, -1, cacheTiles) - m.AppendRegion(region) - m.entities.Add(entities...) - } else if strings.Contains(region.regionPath, "S1") { - region.tileRect.Height -= 1 // For some reason, this has a duplciate wall tile strip... - mapWidthTiles := ((region.tileRect.Width - 18) / 9) - yOffset := region.tileRect.Height - waterXOffset := region.tileRect.Width - 17 - region, entities := loadRegion(m.seed, 0, yOffset, d2enum.RegionAct1Town, 3, -1, cacheTiles) - m.AppendRegion(region) - m.entities.Add(entities...) - yOffset += region.tileRect.Height - - var choices = [...]int{ - d2wilderness.StoneFill1, - d2wilderness.StoneFill2, - d2wilderness.SwampFill1, - d2wilderness.Cottages1, - d2wilderness.Cottages2, - d2wilderness.Cottages3, - d2wilderness.CorralFill, - d2wilderness.FallenCamp1, - d2wilderness.FallenCamp2, - d2wilderness.Pond, - } - - for i := 0; i < 6; i++ { - // West Border - region, entities = loadRegion(m.seed, 0, yOffset, d2enum.RegionAct1Wilderness, d2wilderness.TreeBorderWest, 0, cacheTiles) - m.AppendRegion(region) - m.entities.Add(entities...) - - // East Border - region, entities = loadRegion(m.seed, waterXOffset, yOffset, d2enum.RegionAct1Wilderness, d2wilderness.WaterBorderEast, 0, cacheTiles) - m.AppendRegion(region) - m.entities.Add(entities...) - - // Grass - for ix := 0; ix < mapWidthTiles; ix++ { - region, entities = loadRegion(m.seed, ((ix)*9)+7, yOffset, d2enum.RegionAct1Wilderness, choices[rand.Intn(len(choices))], 0, cacheTiles) - m.AppendRegion(region) - m.entities.Add(entities...) - } - - yOffset += 9 - } - - } -} diff --git a/d2core/d2map/d2mapengine/engine.go b/d2core/d2map/d2mapengine/engine.go new file mode 100644 index 00000000..380e4c41 --- /dev/null +++ b/d2core/d2map/d2mapengine/engine.go @@ -0,0 +1,207 @@ +package d2mapengine + +import ( + "log" + + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset" + + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2dt1" + + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" + + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2mapentity" + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2mapstamp" + + "github.com/OpenDiablo2/OpenDiablo2/d2common" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2ds1" +) + +// Represents the map data for a specific location +type MapEngine struct { + seed int64 // The map seed + entities []d2mapentity.MapEntity // Entities on the map + tiles []d2ds1.TileRecord // The map tiles + size d2common.Size // The size of the map, in tiles + levelType d2datadict.LevelTypeRecord // The level type of this map + dt1TileData []d2dt1.Tile // The DT1 tile data + walkMesh []d2common.PathTile // The walk mesh + startSubTileX int // The starting X position + startSubTileY int // The starting Y position +} + +// Creates a new instance of the map engine +func CreateMapEngine() *MapEngine { + engine := &MapEngine{} + return engine +} + +func (m *MapEngine) WalkMesh() *[]d2common.PathTile { + return &m.walkMesh +} + +// Returns the starting position on the map in sub-tiles +func (m *MapEngine) GetStartingPosition() (int, int) { + return m.startSubTileX, m.startSubTileY +} + +func (m *MapEngine) ResetMap(seed int64, levelType d2enum.RegionIdType, width, height int) { + m.seed = seed + m.entities = make([]d2mapentity.MapEntity, 0) + m.levelType = d2datadict.LevelTypes[levelType] + m.size = d2common.Size{Width: width, Height: height} + m.tiles = make([]d2ds1.TileRecord, width*height) + m.dt1TileData = make([]d2dt1.Tile, 0) + m.walkMesh = make([]d2common.PathTile, width*height*25) + + for _, dtFileName := range m.levelType.Files { + if len(dtFileName) == 0 || dtFileName == "0" { + continue + } + fileData, err := d2asset.LoadFile("/data/global/tiles/" + dtFileName) + if err != nil { + panic(err) + } + dt1, _ := d2dt1.LoadDT1(fileData) + m.dt1TileData = append(m.dt1TileData, dt1.Tiles...) + } +} + +// Returns the level type of this map +func (m *MapEngine) LevelType() d2datadict.LevelTypeRecord { + return m.levelType +} + +// Sets the seed of the map for generation +func (m *MapEngine) SetSeed(seed int64) { + log.Printf("Setting map engine seed to %d", seed) + m.seed = seed +} + +// Returns the size of the map (in sub-tiles) +func (m *MapEngine) Size() d2common.Size { + return m.size +} + +// Returns the map's tiles +func (m *MapEngine) Tiles() *[]d2ds1.TileRecord { + return &m.tiles +} + +// Places a stamp at the specified location. Also adds any entities from the stamp to the map engine +func (m *MapEngine) PlaceStamp(stamp *d2mapstamp.Stamp, tileOffsetX, tileOffsetY int) { + stampSize := stamp.Size() + if (tileOffsetX < 0) || (tileOffsetY < 0) || ((tileOffsetX + stampSize.Width) > m.size.Width) || ((tileOffsetY + stampSize.Height) > m.size.Height) { + panic("Tried placing a stamp outside the bounds of the map") + } + + // Copy over the map tile data + for y := 0; y < stampSize.Height; y++ { + for x := 0; x < stampSize.Width; x++ { + mapTileIdx := x + tileOffsetX + ((y + tileOffsetY) * stampSize.Width) + m.tiles[mapTileIdx] = *stamp.Tile(x, y) + } + } + + // Copy over the entities + m.entities = append(m.entities, stamp.Entities()...) +} + +// Returns a reference to a map tile based on the specified tile X and Y coordinate +func (m *MapEngine) TileAt(tileX, tileY int) *d2ds1.TileRecord { + idx := tileX + (tileY * m.size.Width) + if idx < 0 || idx >= len(m.tiles) { + return nil + } + return &m.tiles[idx] +} + +// Returns a reference to the map entities +func (m *MapEngine) Entities() *[]d2mapentity.MapEntity { + return &m.entities +} + +// Returns the map engine's seed +func (m *MapEngine) Seed() int64 { + return m.seed +} + +// Adds an entity to the map engine +func (m *MapEngine) AddEntity(entity d2mapentity.MapEntity) { + m.entities = append(m.entities, entity) +} + +// Removes an entity from the map engine +func (m *MapEngine) RemoveEntity(entity d2mapentity.MapEntity) { + if entity == nil { + return + } + panic("Removing entities is not currently implemented") + //m.entities.Remove(entity) +} + +func (m *MapEngine) GetTiles(style, sequence, tileType int32) []d2dt1.Tile { + var tiles []d2dt1.Tile + for _, tile := range m.dt1TileData { + if tile.Style != style || tile.Sequence != sequence || tile.Type != tileType { + continue + } + tiles = append(tiles, tile) + } + if len(tiles) == 0 { + log.Printf("Unknown tile ID [%d %d %d]\n", style, sequence, tileType) + return nil + } + return tiles +} + +func (m *MapEngine) GetStartPosition() (float64, float64) { + for tileY := 0; tileY < m.size.Height; tileY++ { + for tileX := 0; tileX < m.size.Width; tileX++ { + tile := m.tiles[tileX+(tileY*m.size.Width)] + for _, wall := range tile.Walls { + if wall.Type == 10 && (wall.Style == 10 || wall.Style == 31) { + return float64(tileX) + 0.5, float64(tileY) + 0.5 + } + } + } + } + + return m.GetCenterPosition() +} + +// Returns the center of the map +func (m *MapEngine) GetCenterPosition() (float64, float64) { + return float64(m.size.Width) / 2.0, float64(m.size.Height) / 2.0 +} + +// Advances time on the map engine +func (m *MapEngine) Advance(tickTime float64) { + for _, entity := range m.entities { + entity.Advance(tickTime) + } +} + +func (m *MapEngine) TileExists(tileX, tileY int) bool { + if tileX < 0 || tileX >= m.size.Width || tileY < 0 || tileY >= m.size.Height { + return false + } + tile := m.tiles[tileX+(tileY*m.size.Width)] + return len(tile.Floors) > 0 || len(tile.Shadows) > 0 || len(tile.Walls) > 0 || len(tile.Substitutions) > 0 +} + +func (m *MapEngine) GenerateMap(regionType d2enum.RegionIdType, levelPreset int, fileIndex int, cacheTiles bool) { + region := d2mapstamp.LoadStamp(m.seed, regionType, levelPreset, fileIndex) + regionSize := region.Size() + m.ResetMap(0, regionType, regionSize.Width, regionSize.Height) + m.PlaceStamp(region, 0, 0) +} + +func (m *MapEngine) GetTileData(style int32, sequence int32, tileType d2enum.TileType) *d2dt1.Tile { + for _, tile := range m.dt1TileData { + if tile.Style == style && tile.Sequence == sequence && tile.Type == int32(tileType) { + return &tile + } + } + return nil +} diff --git a/d2core/d2map/d2mapengine/walk_mesh.go b/d2core/d2map/d2mapengine/walk_mesh.go new file mode 100644 index 00000000..3482e9b1 --- /dev/null +++ b/d2core/d2map/d2mapengine/walk_mesh.go @@ -0,0 +1,105 @@ +package d2mapengine + +import ( + "math" + + "github.com/OpenDiablo2/OpenDiablo2/d2common" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" + "github.com/beefsack/go-astar" +) + +func (m *MapEngine) RegenerateWalkPaths() { + for subTileY := 0; subTileY < m.size.Height*5; subTileY++ { + tileY := int(float64(subTileY) / 5.0) + for subTileX := 0; subTileX < m.size.Width*5; subTileX++ { + tileX := int(float64(subTileX) / 5.0) + tile := m.TileAt(tileX, tileY) + isBlocked := false + for _, floor := range tile.Floors { + tileData := m.GetTileData(int32(floor.Style), int32(floor.Sequence), d2enum.Floor) + if tileData == nil { + continue + } + tileSubAttributes := tileData.GetSubTileFlags(subTileX%5, subTileY%5) + isBlocked = isBlocked || tileSubAttributes.BlockWalk + if isBlocked { + break + } + } + if !isBlocked { + for _, wall := range tile.Walls { + tileData := m.GetTileData(int32(wall.Style), int32(wall.Sequence), wall.Type) + if tileData == nil { + continue + } + tileSubAttributes := tileData.GetSubTileFlags(subTileX%5, subTileY%5) + isBlocked = isBlocked || tileSubAttributes.BlockWalk + if isBlocked { + break + } + } + } + + index := subTileX + (subTileY * m.size.Width * 5) + m.walkMesh[index] = d2common.PathTile{ + Walkable: !isBlocked, + X: float64(subTileX) / 5.0, + Y: float64(subTileY) / 5.0, + } + + ySkew := m.size.Width * 5 + if !isBlocked && subTileY > 0 && m.walkMesh[index-ySkew].Walkable { + m.walkMesh[index].Up = &m.walkMesh[index-ySkew] + m.walkMesh[index-ySkew].Down = &m.walkMesh[index] + } + if !isBlocked && subTileX > 0 && m.walkMesh[index-1].Walkable { + m.walkMesh[index].Left = &m.walkMesh[index-1] + m.walkMesh[index-1].Right = &m.walkMesh[index] + } + if !isBlocked && subTileX > 0 && subTileY > 0 && m.walkMesh[(index-ySkew)-1].Walkable { + m.walkMesh[index].UpLeft = &m.walkMesh[(index-ySkew)-1] + m.walkMesh[(index-ySkew)-1].DownRight = &m.walkMesh[index] + } + if !isBlocked && subTileY > 0 && subTileX < (m.size.Width*5)-1 && m.walkMesh[(index-ySkew)+1].Walkable { + m.walkMesh[index].UpRight = &m.walkMesh[(index-ySkew)+1] + m.walkMesh[(index-ySkew)+1].DownLeft = &m.walkMesh[index] + } + } + } +} + +// Finds a walkable path between two points +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)) + if !m.TileExists(startTileX, startTileY) { + return + } + startSubtileX := int((startX - float64(int(startX))) * 5) + startSubtileY := int((startY - float64(int(startY))) * 5) + startNodeIndex := ((startSubtileY + (startTileY * 5)) * m.size.Width * 5) + startSubtileX + ((startTileX) * 5) + if startNodeIndex < 0 || startNodeIndex >= len(m.walkMesh) { + return + } + startNode := &m.walkMesh[startNodeIndex] + + endTileX := int(math.Floor(endX)) + endTileY := int(math.Floor(endY)) + if !m.TileExists(endTileX, endTileY) { + return + } + endSubtileX := int((endX - float64(int(endX))) * 5) + endSubtileY := int((endY - float64(int(endY))) * 5) + + endNodeIndex := ((endSubtileY + (endTileY * 5)) * m.size.Width * 5) + endSubtileX + ((endTileX) * 5) + if endNodeIndex < 0 || endNodeIndex >= len(m.walkMesh) { + return + } + endNode := &m.walkMesh[endNodeIndex] + + path, distance, found = astar.Path(endNode, startNode) + if path != nil { + path = path[1:] + } + return +} diff --git a/d2core/d2map/animated_composite.go b/d2core/d2map/d2mapentity/animated_composite.go similarity index 99% rename from d2core/d2map/animated_composite.go rename to d2core/d2map/d2mapentity/animated_composite.go index 5ca9237b..41ce7175 100644 --- a/d2core/d2map/animated_composite.go +++ b/d2core/d2map/d2mapentity/animated_composite.go @@ -1,4 +1,4 @@ -package d2map +package d2mapentity import ( "github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict" diff --git a/d2core/d2map/animated_entity.go b/d2core/d2map/d2mapentity/animated_entity.go similarity index 98% rename from d2core/d2map/animated_entity.go rename to d2core/d2map/d2mapentity/animated_entity.go index 87e06e50..e9bd8ef3 100644 --- a/d2core/d2map/animated_entity.go +++ b/d2core/d2map/d2mapentity/animated_entity.go @@ -1,4 +1,4 @@ -package d2map +package d2mapentity import ( "github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset" diff --git a/d2core/d2map/map_entity.go b/d2core/d2map/d2mapentity/map_entity.go similarity index 92% rename from d2core/d2map/map_entity.go rename to d2core/d2map/d2mapentity/map_entity.go index 4a518756..5b367c31 100644 --- a/d2core/d2map/map_entity.go +++ b/d2core/d2map/d2mapentity/map_entity.go @@ -1,12 +1,19 @@ -package d2map +package d2mapentity import ( "math" "github.com/OpenDiablo2/OpenDiablo2/d2common" + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2render" "github.com/beefsack/go-astar" ) +type MapEntity interface { + Render(target d2render.Surface) + Advance(tickTime float64) + GetPosition() (float64, float64) +} + // mapEntity represents an entity on the map that can be animated type mapEntity struct { LocationX float64 @@ -92,7 +99,7 @@ func (m *mapEntity) Step(tickTime float64) { if d2common.AlmostEqual(m.LocationX, m.TargetX, 0.01) && d2common.AlmostEqual(m.LocationY, m.TargetY, 0.01) { if len(m.path) > 0 { - m.SetTarget(m.path[0].(*PathTile).X*5, m.path[0].(*PathTile).Y*5, m.done) + m.SetTarget(m.path[0].(*d2common.PathTile).X*5, m.path[0].(*d2common.PathTile).Y*5, m.done) if len(m.path) > 1 { m.path = m.path[1:] diff --git a/d2core/d2map/missile.go b/d2core/d2map/d2mapentity/missile.go similarity index 98% rename from d2core/d2map/missile.go rename to d2core/d2map/d2mapentity/missile.go index 8077fa86..f0e7b9ed 100644 --- a/d2core/d2map/missile.go +++ b/d2core/d2map/d2mapentity/missile.go @@ -1,4 +1,4 @@ -package d2map +package d2mapentity import ( "fmt" diff --git a/d2core/d2map/npc.go b/d2core/d2map/d2mapentity/npc.go similarity index 99% rename from d2core/d2map/npc.go rename to d2core/d2map/d2mapentity/npc.go index c50b95d0..a64502ca 100644 --- a/d2core/d2map/npc.go +++ b/d2core/d2map/d2mapentity/npc.go @@ -1,4 +1,4 @@ -package d2map +package d2mapentity import ( "math/rand" diff --git a/d2core/d2map/player.go b/d2core/d2map/d2mapentity/player.go similarity index 99% rename from d2core/d2map/player.go rename to d2core/d2map/d2mapentity/player.go index b44ead6a..fb58387f 100644 --- a/d2core/d2map/player.go +++ b/d2core/d2map/d2mapentity/player.go @@ -1,4 +1,4 @@ -package d2map +package d2mapentity import ( "image/color" diff --git a/d2core/d2map/d2mapgen/act1_overworld.go b/d2core/d2map/d2mapgen/act1_overworld.go new file mode 100644 index 00000000..27c23dea --- /dev/null +++ b/d2core/d2map/d2mapgen/act1_overworld.go @@ -0,0 +1,72 @@ +package d2mapgen + +import ( + "math/rand" + + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2mapengine" + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2mapstamp" +) + +func GenerateAct1Overworld(mapEngine *d2mapengine.MapEngine) { + rand.Seed(mapEngine.Seed()) + townStamp := d2mapstamp.LoadStamp(mapEngine.Seed(), d2enum.RegionAct1Town, 1, -1) + townSize := townStamp.Size() + mapEngine.ResetMap(0, d2enum.RegionAct1Town, townSize.Width, townSize.Height) // TODO: Mapgen - Needs levels.txt stuff + mapEngine.PlaceStamp(townStamp, 0, 0) + + mapEngine.RegenerateWalkPaths() + + //region, entities := LoadStamp(m.seed, 0, 0, d2enum.RegionAct1Town, 1, -1, cacheTiles) + //m.regions = append(m.regions, region) + //m.entities.Add(entities...) + //if strings.Contains(region.regionPath, "E1") { + // region, entities := LoadStamp(m.seed, region.tileRect.Width, 0, d2enum.RegionAct1Town, 2, -1, cacheTiles) + // m.AppendRegion(region) + // m.entities.Add(entities...) + //} else if strings.Contains(region.regionPath, "S1") { + // region.tileRect.Height -= 1 // For some reason, this has a duplciate wall tile strip... + // mapWidthTiles := ((region.tileRect.Width - 18) / 9) + // yOffset := region.tileRect.Height + // waterXOffset := region.tileRect.Width - 17 + // region, entities := LoadStamp(m.seed, 0, yOffset, d2enum.RegionAct1Town, 3, -1, cacheTiles) + // m.AppendRegion(region) + // m.entities.Add(entities...) + // yOffset += region.tileRect.Height + // + // var choices = [...]int{ + // d2wilderness.StoneFill1, + // d2wilderness.StoneFill2, + // d2wilderness.SwampFill1, + // d2wilderness.Cottages1, + // d2wilderness.Cottages2, + // d2wilderness.Cottages3, + // d2wilderness.CorralFill, + // d2wilderness.FallenCamp1, + // d2wilderness.FallenCamp2, + // d2wilderness.Pond, + // } + // + // for i := 0; i < 6; i++ { + // // West Border + // region, entities = LoadStamp(m.seed, 0, yOffset, d2enum.RegionAct1Wilderness, d2wilderness.TreeBorderWest, 0, cacheTiles) + // m.AppendRegion(region) + // m.entities.Add(entities...) + // + // // East Border + // region, entities = LoadStamp(m.seed, waterXOffset, yOffset, d2enum.RegionAct1Wilderness, d2wilderness.WaterBorderEast, 0, cacheTiles) + // m.AppendRegion(region) + // m.entities.Add(entities...) + // + // // Grass + // for ix := 0; ix < mapWidthTiles; ix++ { + // region, entities = LoadStamp(m.seed, ((ix)*9)+7, yOffset, d2enum.RegionAct1Wilderness, choices[rand.Intn(len(choices))], 0, cacheTiles) + // m.AppendRegion(region) + // m.entities.Add(entities...) + // } + // + // yOffset += 9 + // } + // + //} +} diff --git a/d2core/d2map/d2wilderness/wilderness_tile_types.go b/d2core/d2map/d2mapgen/d2wilderness/wilderness_tile_types.go similarity index 100% rename from d2core/d2map/d2wilderness/wilderness_tile_types.go rename to d2core/d2map/d2mapgen/d2wilderness/wilderness_tile_types.go diff --git a/d2core/d2map/camera.go b/d2core/d2map/d2maprenderer/camera.go similarity index 91% rename from d2core/d2map/camera.go rename to d2core/d2map/d2maprenderer/camera.go index 9c27bf4f..88dae6c7 100644 --- a/d2core/d2map/camera.go +++ b/d2core/d2map/d2maprenderer/camera.go @@ -1,4 +1,4 @@ -package d2map +package d2maprenderer type Camera struct { x float64 diff --git a/d2core/d2map/d2maprenderer/gfx_decode.go b/d2core/d2map/d2maprenderer/gfx_decode.go new file mode 100644 index 00000000..7b5bf62f --- /dev/null +++ b/d2core/d2map/d2maprenderer/gfx_decode.go @@ -0,0 +1,76 @@ +package d2maprenderer + +import "github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2dt1" + +func (mr *MapRenderer) decodeTileGfxData(blocks []d2dt1.Block, pixels *[]byte, tileYOffset int32, tileWidth int32) { + for _, block := range blocks { + if block.Format == d2dt1.BlockFormatIsometric { + // 3D isometric decoding + xjump := []int32{14, 12, 10, 8, 6, 4, 2, 0, 2, 4, 6, 8, 10, 12, 14} + nbpix := []int32{4, 8, 12, 16, 20, 24, 28, 32, 28, 24, 20, 16, 12, 8, 4} + blockX := int32(block.X) + blockY := int32(block.Y) + length := int32(256) + x := int32(0) + y := int32(0) + idx := 0 + for length > 0 { + x = xjump[y] + n := nbpix[y] + length -= n + for n > 0 { + colorIndex := block.EncodedData[idx] + if colorIndex != 0 { + pixelColor := mr.palette.Colors[colorIndex] + offset := 4 * (((blockY + y + tileYOffset) * tileWidth) + (blockX + x)) + (*pixels)[offset] = pixelColor.R + (*pixels)[offset+1] = pixelColor.G + (*pixels)[offset+2] = pixelColor.B + (*pixels)[offset+3] = 255 + } + x++ + n-- + idx++ + } + y++ + } + continue + } + // RLE Encoding + blockX := int32(block.X) + blockY := int32(block.Y) + x := int32(0) + y := int32(0) + idx := 0 + length := block.Length + for length > 0 { + b1 := block.EncodedData[idx] + b2 := block.EncodedData[idx+1] + idx += 2 + length -= 2 + if (b1 | b2) == 0 { + x = 0 + y++ + continue + } + x += int32(b1) + length -= int32(b2) + for b2 > 0 { + colorIndex := block.EncodedData[idx] + if colorIndex != 0 { + pixelColor := mr.palette.Colors[colorIndex] + + offset := 4 * (((blockY + y + tileYOffset) * tileWidth) + (blockX + x)) + (*pixels)[offset] = pixelColor.R + (*pixels)[offset+1] = pixelColor.G + (*pixels)[offset+2] = pixelColor.B + (*pixels)[offset+3] = 255 + + } + idx++ + x++ + b2-- + } + } + } +} diff --git a/d2core/d2map/d2maprenderer/image_cache.go b/d2core/d2map/d2maprenderer/image_cache.go new file mode 100644 index 00000000..fa1a7c1c --- /dev/null +++ b/d2core/d2map/d2maprenderer/image_cache.go @@ -0,0 +1,26 @@ +package d2maprenderer + +import ( + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2render" +) + +var imageCacheRecords map[uint32]d2render.Surface + +// Invalidates the global region image cache. Call this when you are changing regions +func InvalidateImageCache() { + imageCacheRecords = nil +} + +func (mr *MapRenderer) getImageCacheRecord(style, sequence byte, tileType d2enum.TileType, randomIndex byte) d2render.Surface { + lookupIndex := uint32(style)<<24 | uint32(sequence)<<16 | uint32(tileType)<<8 | uint32(randomIndex) + return imageCacheRecords[lookupIndex] +} + +func (mr *MapRenderer) setImageCacheRecord(style, sequence byte, tileType d2enum.TileType, randomIndex byte, image d2render.Surface) { + lookupIndex := uint32(style)<<24 | uint32(sequence)<<16 | uint32(tileType)<<8 | uint32(randomIndex) + if imageCacheRecords == nil { + imageCacheRecords = make(map[uint32]d2render.Surface) + } + imageCacheRecords[lookupIndex] = image +} diff --git a/d2core/d2map/d2maprenderer/renderer.go b/d2core/d2map/d2maprenderer/renderer.go new file mode 100644 index 00000000..0df398f5 --- /dev/null +++ b/d2core/d2map/d2maprenderer/renderer.go @@ -0,0 +1,346 @@ +package d2maprenderer + +import ( + "errors" + "image/color" + "log" + + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2mapengine" + + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2dat" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource" + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset" + + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2ds1" + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2render" + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2term" +) + +// The map renderer, used to render the map +type MapRenderer struct { + mapEngine *d2mapengine.MapEngine // The map engine that is being rendered + palette *d2dat.DATPalette // The palette used for this map + viewport *Viewport // The viewport for the map renderer (used for rendering offsets) + camera Camera // The camera for this map renderer (used to determine where on the map we are rendering) + debugVisLevel int // Debug visibility index (0=none, 1=tiles, 2=sub-tiles) + lastFrameTime float64 // The last time the map was rendered + currentFrame int // The current render frame (for animations) +} + +// Creates an instance of the map renderer +func CreateMapRenderer(mapEngine *d2mapengine.MapEngine) *MapRenderer { + result := &MapRenderer{ + mapEngine: mapEngine, + viewport: NewViewport(0, 0, 800, 600), + } + + result.viewport.SetCamera(&result.camera) + + d2term.BindAction("mapdebugvis", "set map debug visualization level", func(level int) { + result.debugVisLevel = level + }) + + if mapEngine.LevelType().Id != 0 { + result.generateTileCache() + } + + return result +} + +func (mr *MapRenderer) RegenerateTileCache() { + mr.generateTileCache() +} + +func (mr *MapRenderer) SetMapEngine(mapEngine *d2mapengine.MapEngine) { + mr.mapEngine = mapEngine + mr.generateTileCache() +} + +func (mr *MapRenderer) Render(target d2render.Surface) { + mr.renderPass1(mr.viewport, target) + if mr.debugVisLevel > 0 { + mr.renderDebug(mr.debugVisLevel, mr.viewport, target) + } + mr.renderPass2(mr.viewport, target) + mr.renderPass3(mr.viewport, target) +} + +func (mr *MapRenderer) MoveCameraTo(x, y float64) { + mr.camera.MoveTo(x, y) +} + +func (mr *MapRenderer) MoveCameraBy(x, y float64) { + mr.camera.MoveBy(x, y) +} + +func (mr *MapRenderer) ScreenToWorld(x, y int) (float64, float64) { + return mr.viewport.ScreenToWorld(x, y) +} + +func (mr *MapRenderer) ScreenToOrtho(x, y int) (float64, float64) { + return mr.viewport.ScreenToOrtho(x, y) +} + +func (mr *MapRenderer) WorldToOrtho(x, y float64) (float64, float64) { + return mr.viewport.WorldToOrtho(x, y) +} + +func (mr *MapRenderer) renderPass1(viewport *Viewport, target d2render.Surface) { + mapSize := mr.mapEngine.Size() + // TODO: Render based on visible area + for tileY := 0; tileY < mapSize.Height; tileY++ { + for tileX := 0; tileX < mapSize.Width; tileX++ { + tile := mr.mapEngine.TileAt(tileX, tileY) + if viewport.IsTileVisible(float64(tileX), float64(tileY)) { + viewport.PushTranslationWorld(float64(tileX), float64(tileY)) + mr.renderTilePass1(tile, target) + viewport.PopTranslation() + } + } + } +} + +func (mr *MapRenderer) renderPass2(viewport *Viewport, target d2render.Surface) { + mapSize := mr.mapEngine.Size() + + // TODO: Render based on visible area + for tileY := 0; tileY < mapSize.Height; tileY++ { + for tileX := 0; tileX < mapSize.Width; tileX++ { + tile := mr.mapEngine.TileAt(tileX, tileY) + if viewport.IsTileVisible(float64(tileX), float64(tileY)) { + viewport.PushTranslationWorld(float64(tileX), float64(tileY)) + mr.renderTilePass2(tile, target) + + // TODO: Do not loop over every entity every frame + for _, mapEntity := range *mr.mapEngine.Entities() { + entityX, entityY := mapEntity.GetPosition() + if (int(entityX) != tileX) || (int(entityY) != tileY) { + continue + } + target.PushTranslation(viewport.GetTranslationScreen()) + mapEntity.Render(target) + target.Pop() + } + viewport.PopTranslation() + } + } + } +} + +func (mr *MapRenderer) renderPass3(viewport *Viewport, target d2render.Surface) { + mapSize := mr.mapEngine.Size() + // TODO: Render based on visible area + for tileY := 0; tileY < mapSize.Height; tileY++ { + for tileX := 0; tileX < mapSize.Width; tileX++ { + tile := mr.mapEngine.TileAt(tileX, tileY) + if viewport.IsTileVisible(float64(tileX), float64(tileY)) { + viewport.PushTranslationWorld(float64(tileX), float64(tileY)) + mr.renderTilePass3(tile, target) + viewport.PopTranslation() + } + } + } + +} + +func (mr *MapRenderer) renderTilePass1(tile *d2ds1.TileRecord, target d2render.Surface) { + for _, wall := range tile.Walls { + if !wall.Hidden && wall.Prop1 != 0 && wall.Type.LowerWall() { + mr.renderWall(wall, mr.viewport, target) + } + } + + for _, floor := range tile.Floors { + if !floor.Hidden && floor.Prop1 != 0 { + mr.renderFloor(floor, target) + } + } + + for _, shadow := range tile.Shadows { + if !shadow.Hidden && shadow.Prop1 != 0 { + mr.renderShadow(shadow, target) + } + } +} + +func (mr *MapRenderer) renderTilePass2(tile *d2ds1.TileRecord, target d2render.Surface) { + for _, wall := range tile.Walls { + if !wall.Hidden && wall.Type.UpperWall() { + mr.renderWall(wall, mr.viewport, target) + } + } +} + +func (mr *MapRenderer) renderTilePass3(tile *d2ds1.TileRecord, target d2render.Surface) { + for _, wall := range tile.Walls { + if wall.Type == d2enum.Roof { + mr.renderWall(wall, mr.viewport, target) + } + } +} + +func (mr *MapRenderer) renderFloor(tile d2ds1.FloorShadowRecord, target d2render.Surface) { + var img d2render.Surface + if !tile.Animated { + img = mr.getImageCacheRecord(tile.Style, tile.Sequence, 0, tile.RandomIndex) + } else { + img = mr.getImageCacheRecord(tile.Style, tile.Sequence, 0, byte(mr.currentFrame)) + } + if img == nil { + log.Printf("Render called on uncached floor {%v,%v}", tile.Style, tile.Sequence) + return + } + + mr.viewport.PushTranslationOrtho(-80, float64(tile.YAdjust)) + defer mr.viewport.PopTranslation() + + target.PushTranslation(mr.viewport.GetTranslationScreen()) + defer target.Pop() + + target.Render(img) +} + +func (mr *MapRenderer) renderWall(tile d2ds1.WallRecord, viewport *Viewport, target d2render.Surface) { + img := mr.getImageCacheRecord(tile.Style, tile.Sequence, tile.Type, tile.RandomIndex) + if img == nil { + log.Printf("Render called on uncached wall {%v,%v,%v}", tile.Style, tile.Sequence, tile.Type) + return + } + + viewport.PushTranslationOrtho(-80, float64(tile.YAdjust)-8) + defer viewport.PopTranslation() + + target.PushTranslation(viewport.GetTranslationScreen()) + defer target.Pop() + + target.Render(img) +} + +func (mr *MapRenderer) renderShadow(tile d2ds1.FloorShadowRecord, target d2render.Surface) { + img := mr.getImageCacheRecord(tile.Style, tile.Sequence, 13, tile.RandomIndex) + if img == nil { + log.Printf("Render called on uncached shadow {%v,%v}", tile.Style, tile.Sequence) + return + } + + 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}) + defer target.PopN(2) + + target.Render(img) +} + +func (mr *MapRenderer) renderDebug(debugVisLevel int, viewport *Viewport, target d2render.Surface) { + mapSize := mr.mapEngine.Size() + // TODO: Render based on visible area + for tileY := 0; tileY < mapSize.Height; tileY++ { + for tileX := 0; tileX < mapSize.Width; tileX++ { + if viewport.IsTileVisible(float64(tileX), float64(tileY)) { + viewport.PushTranslationWorld(float64(tileX), float64(tileY)) + mr.renderTileDebug(tileX, tileY, debugVisLevel, target) + viewport.PopTranslation() + } + } + } +} + +func (mr *MapRenderer) renderTileDebug(ax, ay int, debugVisLevel int, target d2render.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} + + screenX1, screenY1 := mr.viewport.WorldToScreen(float64(ax), float64(ay)) + screenX2, screenY2 := mr.viewport.WorldToScreen(float64(ax+1), float64(ay)) + screenX3, screenY3 := mr.viewport.WorldToScreen(float64(ax), float64(ay+1)) + + target.PushTranslation(screenX1, screenY1) + defer target.Pop() + + target.DrawLine(screenX2-screenX1, screenY2-screenY1, tileColor) + target.DrawLine(screenX3-screenX1, screenY3-screenY1, tileColor) + target.PushTranslation(-10, 10) + target.DrawText("%v, %v", ax, ay) + target.Pop() + + if debugVisLevel > 1 { + for i := 1; i <= 4; i++ { + x2 := i * 16 + y2 := i * 8 + + target.PushTranslation(-x2, y2) + target.DrawLine(80, 40, subTileColor) + target.Pop() + + target.PushTranslation(x2, y2) + target.DrawLine(-80, 40, subTileColor) + target.Pop() + } + + tile := mr.mapEngine.TileAt(ax, ay) + + for i, floor := range tile.Floors { + target.PushTranslation(-20, 10+(i+1)*14) + target.DrawText("f: %v-%v", floor.Style, floor.Sequence) + target.Pop() + } + + for yy := 0; yy < 5; yy++ { + for xx := 0; xx < 5; xx++ { + isoX := (xx - yy) * 16 + isoY := (xx + yy) * 8 + var walkableArea = (*mr.mapEngine.WalkMesh())[((yy+(ay*5))*mr.mapEngine.Size().Width*5)+xx+(ax*5)] + if !walkableArea.Walkable { + target.PushTranslation(isoX-3, isoY+4) + target.DrawRect(5, 5, tileCollisionColor) + target.Pop() + } + } + } + } +} + +func (mr *MapRenderer) Advance(elapsed float64) { + frameLength := 0.1 + + mr.lastFrameTime += elapsed + framesAdvanced := int(mr.lastFrameTime / frameLength) + mr.lastFrameTime -= float64(framesAdvanced) * frameLength + + mr.currentFrame += framesAdvanced + if mr.currentFrame > 9 { + mr.currentFrame = 0 + } +} + +func loadPaletteForAct(levelType d2enum.RegionIdType) (*d2dat.DATPalette, error) { + var palettePath string + switch levelType { + case d2enum.RegionAct1Town, d2enum.RegionAct1Wilderness, d2enum.RegionAct1Cave, d2enum.RegionAct1Crypt, + d2enum.RegionAct1Monestary, d2enum.RegionAct1Courtyard, d2enum.RegionAct1Barracks, + d2enum.RegionAct1Jail, d2enum.RegionAct1Cathedral, d2enum.RegionAct1Catacombs, d2enum.RegionAct1Tristram: + palettePath = d2resource.PaletteAct1 + break + case d2enum.RegionAct2Town, d2enum.RegionAct2Sewer, d2enum.RegionAct2Harem, d2enum.RegionAct2Basement, + d2enum.RegionAct2Desert, d2enum.RegionAct2Tomb, d2enum.RegionAct2Lair, d2enum.RegionAct2Arcane: + palettePath = d2resource.PaletteAct2 + break + case d2enum.RegionAct3Town, d2enum.RegionAct3Jungle, d2enum.RegionAct3Kurast, d2enum.RegionAct3Spider, + d2enum.RegionAct3Dungeon, d2enum.RegionAct3Sewer: + palettePath = d2resource.PaletteAct3 + break + case d2enum.RegionAct4Town, d2enum.RegionAct4Mesa, d2enum.RegionAct4Lava, d2enum.RegionAct5Lava: + palettePath = d2resource.PaletteAct4 + break + case d2enum.RegonAct5Town, d2enum.RegionAct5Siege, d2enum.RegionAct5Barricade, d2enum.RegionAct5Temple, + d2enum.RegionAct5IceCaves, d2enum.RegionAct5Baal: + palettePath = d2resource.PaletteAct5 + break + default: + return nil, errors.New("failed to find palette for region") + } + + return d2asset.LoadPalette(palettePath) +} diff --git a/d2core/d2map/d2maprenderer/tile_cache.go b/d2core/d2map/d2maprenderer/tile_cache.go new file mode 100644 index 00000000..aabf4cc2 --- /dev/null +++ b/d2core/d2map/d2maprenderer/tile_cache.go @@ -0,0 +1,225 @@ +package d2maprenderer + +import ( + "log" + + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" + + "github.com/OpenDiablo2/OpenDiablo2/d2common" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2ds1" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2dt1" + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2render" +) + +func (mr *MapRenderer) generateTileCache() { + mr.palette, _ = loadPaletteForAct(d2enum.RegionIdType(mr.mapEngine.LevelType().Id)) + mapEngineSize := mr.mapEngine.Size() + + for idx, tile := range *mr.mapEngine.Tiles() { + tileX := idx % mapEngineSize.Width + tileY := (idx - tileX) / mapEngineSize.Width + for i := range tile.Floors { + if !tile.Floors[i].Hidden && tile.Floors[i].Prop1 != 0 { + mr.generateFloorCache(&tile.Floors[i], tileX, tileY) + } + } + for i := range tile.Shadows { + if !tile.Shadows[i].Hidden && tile.Shadows[i].Prop1 != 0 { + mr.generateShadowCache(&tile.Shadows[i], tileX, tileY) + } + } + for i := range tile.Walls { + if !tile.Walls[i].Hidden && tile.Walls[i].Prop1 != 0 { + mr.generateWallCache(&tile.Walls[i], tileX, tileY) + } + } + } +} + +func (mr *MapRenderer) generateFloorCache(tile *d2ds1.FloorShadowRecord, tileX, tileY int) { + tileOptions := mr.mapEngine.GetTiles(int32(tile.Style), int32(tile.Sequence), 0) + var tileData []*d2dt1.Tile + var tileIndex byte + + if tileOptions == nil { + log.Printf("Could not locate tile Style:%d, Seq: %d, Type: %d\n", tile.Style, tile.Sequence, 0) + tileData = append(tileData, &d2dt1.Tile{}) + tileData[0].Width = 10 + tileData[0].Height = 10 + } else { + if !tileOptions[0].MaterialFlags.Lava { + tileIndex = mr.getRandomTile(tileOptions, tileX, tileY, mr.mapEngine.Seed()) + tileData = append(tileData, &tileOptions[tileIndex]) + } else { + tile.Animated = true + for i := range tileOptions { + tileData = append(tileData, &tileOptions[i]) + } + } + } + + for i := range tileData { + if !tileData[i].MaterialFlags.Lava { + tile.RandomIndex = tileIndex + } else { + tileIndex = byte(tileData[i].RarityFrameIndex) + } + cachedImage := mr.getImageCacheRecord(tile.Style, tile.Sequence, 0, tileIndex) + if cachedImage != nil { + return + } + tileYMinimum := int32(0) + for _, block := range tileData[i].Blocks { + tileYMinimum = d2common.MinInt32(tileYMinimum, int32(block.Y)) + } + tileYOffset := d2common.AbsInt32(tileYMinimum) + tileHeight := d2common.AbsInt32(tileData[i].Height) + image, _ := d2render.NewSurface(int(tileData[i].Width), int(tileHeight), d2render.FilterNearest) + pixels := make([]byte, 4*tileData[i].Width*tileHeight) + mr.decodeTileGfxData(tileData[i].Blocks, &pixels, tileYOffset, tileData[i].Width) + image.ReplacePixels(pixels) + mr.setImageCacheRecord(tile.Style, tile.Sequence, 0, tileIndex, image) + } +} + +func (mr *MapRenderer) generateShadowCache(tile *d2ds1.FloorShadowRecord, tileX, tileY int) { + tileOptions := mr.mapEngine.GetTiles(int32(tile.Style), int32(tile.Sequence), 13) + var tileIndex byte + var tileData *d2dt1.Tile + if tileOptions == nil { + return + } else { + tileIndex = mr.getRandomTile(tileOptions, tileX, tileY, mr.mapEngine.Seed()) + tileData = &tileOptions[tileIndex] + } + + if tileData.Width == 0 || tileData.Height == 0 { + return + } + + tile.RandomIndex = tileIndex + tileMinY := int32(0) + tileMaxY := int32(0) + for _, block := range tileData.Blocks { + tileMinY = d2common.MinInt32(tileMinY, int32(block.Y)) + tileMaxY = d2common.MaxInt32(tileMaxY, int32(block.Y+32)) + } + tileYOffset := -tileMinY + tileHeight := int(tileMaxY - tileMinY) + tile.YAdjust = int(tileMinY + 80) + + cachedImage := mr.getImageCacheRecord(tile.Style, tile.Sequence, 13, tileIndex) + if cachedImage != nil { + return + } + + image, _ := d2render.NewSurface(int(tileData.Width), tileHeight, d2render.FilterNearest) + pixels := make([]byte, 4*tileData.Width*int32(tileHeight)) + mr.decodeTileGfxData(tileData.Blocks, &pixels, tileYOffset, tileData.Width) + image.ReplacePixels(pixels) + mr.setImageCacheRecord(tile.Style, tile.Sequence, 13, tileIndex, image) +} + +func (mr *MapRenderer) generateWallCache(tile *d2ds1.WallRecord, tileX, tileY int) { + tileOptions := mr.mapEngine.GetTiles(int32(tile.Style), int32(tile.Sequence), int32(tile.Type)) + var tileIndex byte + var tileData *d2dt1.Tile + if tileOptions == nil { + return + } else { + tileIndex = mr.getRandomTile(tileOptions, tileX, tileY, mr.mapEngine.Seed()) + tileData = &tileOptions[tileIndex] + } + + tile.RandomIndex = tileIndex + var newTileData *d2dt1.Tile = nil + + if tile.Type == 3 { + newTileOptions := mr.mapEngine.GetTiles(int32(tile.Style), int32(tile.Sequence), int32(4)) + newTileIndex := mr.getRandomTile(newTileOptions, tileX, tileY, mr.mapEngine.Seed()) + newTileData = &newTileOptions[newTileIndex] + } + + tileMinY := int32(0) + tileMaxY := int32(0) + + target := tileData + + if newTileData != nil && newTileData.Height < tileData.Height { + target = newTileData + } + + for _, block := range target.Blocks { + tileMinY = d2common.MinInt32(tileMinY, int32(block.Y)) + tileMaxY = d2common.MaxInt32(tileMaxY, int32(block.Y+32)) + } + + realHeight := d2common.MaxInt32(d2common.AbsInt32(tileData.Height), tileMaxY-tileMinY) + tileYOffset := -tileMinY + //tileHeight := int(tileMaxY - tileMinY) + + if tile.Type == 15 { + tile.YAdjust = -int(tileData.RoofHeight) + } else { + tile.YAdjust = int(tileMinY) + 80 + } + + cachedImage := mr.getImageCacheRecord(tile.Style, tile.Sequence, tile.Type, tileIndex) + if cachedImage != nil { + return + } + + if realHeight == 0 { + log.Printf("Invalid 0 height for wall tile") + return + } + + image, _ := d2render.NewSurface(160, int(realHeight), d2render.FilterNearest) + pixels := make([]byte, 4*160*realHeight) + mr.decodeTileGfxData(tileData.Blocks, &pixels, tileYOffset, 160) + + if newTileData != nil { + mr.decodeTileGfxData(newTileData.Blocks, &pixels, tileYOffset, 160) + } + + if err := image.ReplacePixels(pixels); err != nil { + log.Panicf(err.Error()) + } + + mr.setImageCacheRecord(tile.Style, tile.Sequence, tile.Type, tileIndex, image) +} + +func (mr *MapRenderer) getRandomTile(tiles []d2dt1.Tile, x, y int, seed int64) byte { + /* Walker's Alias Method for weighted random selection + * with xorshifting for random numbers */ + + var tileSeed uint64 + tileSeed = uint64(seed) + uint64(x) + tileSeed *= uint64(y) + uint64(mr.mapEngine.LevelType().Id) + + tileSeed ^= tileSeed << 13 + tileSeed ^= tileSeed >> 17 + tileSeed ^= tileSeed << 5 + + weightSum := 0 + for _, tile := range tiles { + weightSum += int(tile.RarityFrameIndex) + } + + if weightSum == 0 { + return 0 + } + + random := tileSeed % uint64(weightSum) + + sum := 0 + for i, tile := range tiles { + sum += int(tile.RarityFrameIndex) + if sum >= int(random) { + return byte(i) + } + } + + // This return shouldn't be hit + return 0 +} diff --git a/d2core/d2map/viewport.go b/d2core/d2map/d2maprenderer/viewport.go similarity index 96% rename from d2core/d2map/viewport.go rename to d2core/d2map/d2maprenderer/viewport.go index b156822a..94a2cba3 100644 --- a/d2core/d2map/viewport.go +++ b/d2core/d2map/d2maprenderer/viewport.go @@ -1,4 +1,4 @@ -package d2map +package d2maprenderer import ( "math" @@ -32,7 +32,7 @@ func NewViewport(x, y, width, height int) *Viewport { Left: x, Top: y, Width: width, - Height: height, + Height: height + 200, // TODO: Temporary hack to prevent clipping }, defaultScreenRect: d2common.Rectangle{ Left: x, @@ -109,10 +109,11 @@ func (v *Viewport) GetTranslationScreen() (int, int) { return v.OrthoToScreen(v.transCurrent.x, v.transCurrent.y) } -func (v *Viewport) PushTranslationOrtho(x, y float64) { +func (v *Viewport) PushTranslationOrtho(x, y float64) *Viewport { v.transStack = append(v.transStack, v.transCurrent) v.transCurrent.x += x v.transCurrent.y += y + return v } func (v *Viewport) PushTranslationWorld(x, y float64) { diff --git a/d2core/d2map/d2mapstamp/stamp.go b/d2core/d2map/d2mapstamp/stamp.go new file mode 100644 index 00000000..8817c883 --- /dev/null +++ b/d2core/d2map/d2mapstamp/stamp.go @@ -0,0 +1,163 @@ +package d2mapstamp + +import ( + "math" + "math/rand" + + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2mapentity" + + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource" + + "github.com/OpenDiablo2/OpenDiablo2/d2common" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2ds1" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2dt1" + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset" +) + +// Represents a pre-fabricated map stamp that can be placed on a map +type Stamp struct { + regionPath string // The file path of the region + levelType d2datadict.LevelTypeRecord // The level type id for this stamp + levelPreset d2datadict.LevelPresetRecord // The level preset id for this stamp + tiles []d2dt1.Tile // The tiles contained on this stamp + ds1 *d2ds1.DS1 // The backing DS1 file for this stamp +} + +// Loads a stamp based on the supplied parameters +func LoadStamp(seed int64, levelType d2enum.RegionIdType, levelPreset int, fileIndex int) *Stamp { + stamp := &Stamp{ + levelType: d2datadict.LevelTypes[levelType], + levelPreset: d2datadict.LevelPresets[levelPreset], + } + + //stamp.palette, _ = loadPaletteForAct(levelType) + + for _, levelTypeDt1 := range stamp.levelType.Files { + if len(levelTypeDt1) != 0 && levelTypeDt1 != "" && levelTypeDt1 != "0" { + fileData, err := d2asset.LoadFile("/data/global/tiles/" + levelTypeDt1) + if err != nil { + panic(err) + } + + dt1, _ := d2dt1.LoadDT1(fileData) + + stamp.tiles = append(stamp.tiles, dt1.Tiles...) + } + } + + var levelFilesToPick []string + for _, fileRecord := range stamp.levelPreset.Files { + if len(fileRecord) != 0 && fileRecord != "" && fileRecord != "0" { + levelFilesToPick = append(levelFilesToPick, fileRecord) + } + } + + levelIndex := int(math.Round(float64(len(levelFilesToPick)-1) * rand.Float64())) + if fileIndex >= 0 && fileIndex < len(levelFilesToPick) { + levelIndex = fileIndex + } + + if levelFilesToPick == nil { + panic("no level files to pick from") + } + + stamp.regionPath = levelFilesToPick[levelIndex] + fileData, err := d2asset.LoadFile("/data/global/tiles/" + stamp.regionPath) + if err != nil { + panic(err) + } + stamp.ds1, _ = d2ds1.LoadDS1(fileData) + + // Update the region info for the tiles + for rx := 0; rx < len(stamp.ds1.Tiles); rx++ { + for x := 0; x < len(stamp.ds1.Tiles[rx]); x++ { + stamp.ds1.Tiles[rx][x].RegionType = levelType + } + } + + //entities := stamp.loadEntities() + //stamp.loadSpecials() + + return stamp +} + +// Returns the size of the stamp, in tiles +func (mr *Stamp) Size() d2common.Size { + return d2common.Size{int(mr.ds1.Width), int(mr.ds1.Height)} +} + +// Gets the level preset id +func (mr *Stamp) LevelPreset() d2datadict.LevelPresetRecord { + return mr.levelPreset +} + +// Returns the level type id +func (mr *Stamp) LevelType() d2datadict.LevelTypeRecord { + return mr.levelType +} + +// Gets the file path of the region +func (mr *Stamp) RegionPath() string { + return mr.regionPath +} + +// Returns the specified tile +func (mr *Stamp) Tile(x, y int) *d2ds1.TileRecord { + return &mr.ds1.Tiles[y][x] +} + +// Returns tile data based on the supplied paramters +func (mr *Stamp) TileData(style int32, sequence int32, tileType d2enum.TileType) *d2dt1.Tile { + for _, tile := range mr.tiles { + if tile.Style == style && tile.Sequence == sequence && tile.Type == int32(tileType) { + return &tile + } + } + return nil +} + +func (mr *Stamp) Entities() []d2mapentity.MapEntity { + entities := make([]d2mapentity.MapEntity, 0) + + for _, object := range mr.ds1.Objects { + + switch object.Lookup.Type { + case d2datadict.ObjectTypeCharacter: + if object.Lookup.Base != "" && object.Lookup.Token != "" && object.Lookup.TR != "" { + npc := d2mapentity.CreateNPC(object.X, object.Y, 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 := d2mapentity.CreateAnimatedComposite(object.X, object.Y, object.Lookup, d2resource.PaletteUnits) + if err != nil { + panic(err) + } + entity.SetMode(object.Lookup.Mode, object.Lookup.Class, 0) + entities = append(entities, entity) + } + } + } + + return entities +} + +// +//func (mr *Stamp) loadSpecials() { +// for tileY := range mr.ds1.Tiles { +// for tileX := range mr.ds1.Tiles[tileY] { +// for _, wall := range mr.ds1.Tiles[tileY][tileX].Walls { +// if wall.Type == 10 && wall.Style == 30 && wall.Sequence == 0 && mr.startX == 0 && mr.startY == 0 { +// mr.startX, mr.startY = mr.getTileWorldPosition(tileX, tileY) +// mr.startX += 0.5 +// mr.startY += 0.5 +// return +// } +// } +// } +// } +//} +// diff --git a/d2core/d2map/engine.go b/d2core/d2map/engine.go deleted file mode 100644 index 6599ed35..00000000 --- a/d2core/d2map/engine.go +++ /dev/null @@ -1,234 +0,0 @@ -package d2map - -import ( - "log" - "math" - - "github.com/beefsack/go-astar" - - "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" - - "github.com/OpenDiablo2/OpenDiablo2/d2core/d2render" -) - -type MapEntity interface { - Render(target d2render.Surface) - Advance(tickTime float64) - GetPosition() (float64, float64) -} - -// Represents the map data for a specific location -type MapEngine struct { - seed int64 - regions []*MapRegion - entities MapEntitiesSearcher -} - -// Creates a new instance of the map engine -func CreateMapEngine(seed int64) *MapEngine { - engine := &MapEngine{ - seed: seed, - entities: NewRangeSearcher(), - } - - return engine -} - -// Sets the seed of the map for generation -func (m *MapEngine) SetSeed(seed int64) { - log.Printf("Setting map engine seed to %d", seed) - m.seed = seed -} - -func (m *MapEngine) GetStartPosition() (float64, float64) { - var startX, startY float64 - - // TODO: Temporary code, only works for starting map - if len(m.regions) > 0 { - region := m.regions[0] - startX, startY = region.getStartTilePosition() - } - - return startX, startY -} - -// Returns the center of the map -func (m *MapEngine) GetCenterPosition() (float64, float64) { - var centerX, centerY float64 - if len(m.regions) > 0 { - region := m.regions[0] - centerX = float64(region.tileRect.Left) + float64(region.tileRect.Width)/2 - centerY = float64(region.tileRect.Top) + float64(region.tileRect.Height)/2 - } - - return centerX, centerY -} - -func (m *MapEngine) GenerateMap(regionType d2enum.RegionIdType, levelPreset int, fileIndex int, cacheTiles bool) { - region, entities := loadRegion(m.seed, 0, 0, regionType, levelPreset, fileIndex, cacheTiles) - m.regions = append(m.regions, region) - m.entities.Add(entities...) -} - -// Appends a region to the map -func (m *MapEngine) AppendRegion(region *MapRegion) { - m.regions = append(m.regions, region) - // Stitch together the walk map - - // Top/Bottom - for x := 0; x < region.tileRect.Width*5; x++ { - otherRegion := m.GetRegionAtTile(region.tileRect.Left+(x/5), region.tileRect.Top-1) - if otherRegion == nil { - continue - } - xDiff := (region.tileRect.Left - otherRegion.tileRect.Left) * 5 - - sourceSubtile := ®ion.walkableArea[0][x] - if !sourceSubtile.Walkable { - continue - } - - // North West - otherX := x + xDiff - 1 - otherY := (otherRegion.tileRect.Height * 5) - 1 - if otherX < 0 || otherX >= len(otherRegion.walkableArea[otherY]) { - continue - } - otherRegion.walkableArea[otherY][x+xDiff].DownRight = sourceSubtile - sourceSubtile.UpLeft = &otherRegion.walkableArea[otherY][x+xDiff] - - // North - otherX++ - if otherX < 0 || otherX >= len(otherRegion.walkableArea[otherY]) { - continue - } - otherRegion.walkableArea[otherY][x+xDiff].Down = sourceSubtile - sourceSubtile.Up = &otherRegion.walkableArea[otherY][x+xDiff] - - // NorthEast - otherX++ - if otherX < 0 || otherX >= len(otherRegion.walkableArea[otherY]) { - continue - } - otherRegion.walkableArea[otherY][x+xDiff].DownLeft = sourceSubtile - sourceSubtile.UpRight = &otherRegion.walkableArea[otherY][x+xDiff] - } - - // West/East - for y := 0; y < region.tileRect.Height*5; y++ { - otherRegion := m.GetRegionAtTile(region.tileRect.Left-1, region.tileRect.Top+(y/5)) - if otherRegion == nil { - continue - } - yDiff := (region.tileRect.Top - otherRegion.tileRect.Top) * 5 - - sourceSubtile := ®ion.walkableArea[y][0] - if !sourceSubtile.Walkable { - continue - } - - // North West - otherX := (otherRegion.tileRect.Width * 5) - 1 - otherY := y + yDiff - 1 - if otherY < 0 || otherY >= len(otherRegion.walkableArea) { - continue - } - otherRegion.walkableArea[y+yDiff][otherX].DownRight = sourceSubtile - sourceSubtile.UpLeft = &otherRegion.walkableArea[y+yDiff][otherX] - - // West - otherY++ - if otherY < 0 || otherY >= len(otherRegion.walkableArea) { - continue - } - otherRegion.walkableArea[y+yDiff][otherX].Right = sourceSubtile - sourceSubtile.Left = &otherRegion.walkableArea[y+yDiff][otherX] - - // South East - otherY++ - if otherY < 0 || otherY >= len(otherRegion.walkableArea) { - continue - } - otherRegion.walkableArea[y+yDiff][otherX].UpRight = sourceSubtile - sourceSubtile.DownLeft = &otherRegion.walkableArea[y+yDiff][otherX] - } - -} - -// Returns the region located at the specified tile location -func (m *MapEngine) GetRegionAtTile(x, y int) *MapRegion { - // Read in reverse order as tiles can be placed over other tiles, and we prioritize the top level tiles - for i := len(m.regions) - 1; i >= 0; i-- { - region := m.regions[i] - if region.tileRect.IsInRect(x, y) { - return region - } - } - return nil -} - -// Adds an entity to the map engine -func (m *MapEngine) AddEntity(entity MapEntity) { - m.entities.Add(entity) -} - -// Removes an entity from the map engine -func (m *MapEngine) RemoveEntity(entity MapEntity) { - if entity == nil { - return - } - - m.entities.Remove(entity) -} - -// Advances time on the map engine -func (m *MapEngine) Advance(tickTime float64) { - for _, region := range m.regions { - //if region.isVisbile(m.viewport) { - region.advance(tickTime) - //} - } - - for _, entity := range m.entities.All() { - entity.Advance(tickTime) - } - - m.entities.Update() -} - -// Finds a walkable path between two points -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) - if startRegion == nil { - return - } - 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) - if endRegion == nil { - return - } - endNodeY := endSubtileY + ((endTileY - endRegion.tileRect.Top) * 5) - endNodeX := endSubtileX + ((endTileX - endRegion.tileRect.Left) * 5) - if endNodeY < 0 || endNodeY >= len(endRegion.walkableArea) { - return - } - if endNodeX < 0 || endNodeX >= len(endRegion.walkableArea[endNodeY]) { - return - } - endNode := &endRegion.walkableArea[endNodeY][endNodeX] - - path, distance, found = astar.Path(endNode, startNode) - if path != nil { - path = path[1:] - } - return -} diff --git a/d2core/d2map/range_searcher.go b/d2core/d2map/range_searcher.go deleted file mode 100644 index ac6f32a9..00000000 --- a/d2core/d2map/range_searcher.go +++ /dev/null @@ -1,149 +0,0 @@ -package d2map - -import ( - "math" - "sort" - - "github.com/OpenDiablo2/OpenDiablo2/d2common" -) - -type MapEntitiesSearcher interface { - // Returns all map entities. - All() []MapEntity - // Add adds an entity to the index and re-sorts. - Add(entities ...MapEntity) - // Remove finds and removes the entity from the index. - Remove(entity MapEntity) - // SearchByRect get entities in a rectangle, results will be sorted top left to bottom right. - // Elements with equal Y will be sorted by X - SearchByRect(rect d2common.Rectangle) []MapEntity - // SearchByRadius get entities in a circle, results will be sorted top left to bottom right. - // Elements with equal Y will be sorted by X - SearchByRadius(originX, originY, radius float64) []MapEntity - // Update re-sorts the index, must be ran after each update. - Update() -} - -// rangeSearcher a basic index of entity locations using a slice ordered by Y then X coordinates. -// Eventually this should be probably replaced with a proper spatial index. -type rangeSearcher struct { - entities []MapEntity -} - -func NewRangeSearcher() MapEntitiesSearcher { - return &rangeSearcher{ - entities: make([]MapEntity, 0, 64), - } -} - -func (r *rangeSearcher) All() []MapEntity { - return r.entities -} - -func (r *rangeSearcher) Add(entities ...MapEntity) { - r.entities = append(r.entities, entities...) - - r.Update() -} - -func (r *rangeSearcher) Remove(entity MapEntity) { - if entity == nil { - return - } - - // In-place filter to remove the given entity. - n := 0 - for _, check := range r.entities { - if check != entity { - r.entities[n] = check - n++ - } - } - r.entities = r.entities[:n] -} - -func (r *rangeSearcher) SearchByRect(rect d2common.Rectangle) []MapEntity { - left, top, right, bottom := float64(rect.Left), float64(rect.Top), float64(rect.Right()), float64(rect.Bottom()) - topIndex := sort.Search(len(r.entities), func(i int) bool { - x, y := r.entities[i].GetPosition() - if y == top { - return x >= left - } - return y >= top - }) - - matches := make([]MapEntity, 0, 16) - - for i := topIndex; i < len(r.entities); i++ { - x, y := r.entities[i].GetPosition() - if y > bottom { - break - } - - if x >= left && x <= right { - matches = append(matches, r.entities[i]) - } - - } - - return matches -} - -func (r *rangeSearcher) SearchByRadius(originX, originY, radius float64) []MapEntity { - left, right := originX-radius, originX+radius - top, bottom := originY-radius, originY+radius - inRect := r.SearchByRect(d2common.Rectangle{ - Left: int(left), - Top: int(top), - Width: int(right - left), - Height: int(bottom - top), - }) - - // In-place filter to remove entities outside the radius. - n := 0 - for _, check := range inRect { - x, y := check.GetPosition() - if distance(originX, originY, x, y) <= radius { - inRect[n] = check - n++ - } - } - return inRect[:n] -} - -func distance(x1, y1, x2, y2 float64) float64 { - return math.Abs(math.Sqrt(math.Pow(x2-x1, 2) + math.Pow(y2-y1, 2))) -} - -// Re-sorts the index after entities have moved. -// Uses bubble sort to target O(n) sort time, in most cases no entities will be swapped. -func (r *rangeSearcher) Update() { - bubbleSort(r.entities, func(i, j int) bool { - ix, iy := r.entities[i].GetPosition() - jx, jy := r.entities[j].GetPosition() - if iy == jy { - return ix < jx - } - return iy < jy - }) -} - -func bubbleSort(items []MapEntity, less func(i, j int) bool) { - var ( - n = len(items) - sorted = false - ) - for !sorted { - swapped := false - for i := 0; i < n-1; i++ { - if less(i+1, i) { - items[i+1], items[i] = items[i], items[i+1] - swapped = true - } - } - if !swapped { - sorted = true - } - n = n - 1 - } -} diff --git a/d2core/d2map/range_searcher_test.go b/d2core/d2map/range_searcher_test.go deleted file mode 100644 index 15868ebe..00000000 --- a/d2core/d2map/range_searcher_test.go +++ /dev/null @@ -1,125 +0,0 @@ -package d2map - -import ( - "testing" - - "github.com/OpenDiablo2/OpenDiablo2/d2common" - "github.com/OpenDiablo2/OpenDiablo2/d2core/d2render" - "github.com/stretchr/testify/assert" -) - -type mockEntity struct { - x float64 - y float64 -} - -func (m *mockEntity) Render(target d2render.Surface) { - panic("implement me") -} - -func (m *mockEntity) Advance(tickTime float64) { - panic("implement me") -} - -func (m *mockEntity) GetPosition() (float64, float64) { - return m.x, m.y -} - -func newMockEntity(x, y float64) MapEntity { - return &mockEntity{ - x: x, - y: y, - } -} - -func TestRangeSearcher_Add(t *testing.T) { - searcher := &rangeSearcher{ - entities: make([]MapEntity, 0, 64), - } - - searcher.Add( - newMockEntity(0, 9), - newMockEntity(8, 1), - newMockEntity(1, 8), - newMockEntity(3, 6), - newMockEntity(5, 4), - newMockEntity(6, 3), - newMockEntity(9, 0), - newMockEntity(4, 5), - newMockEntity(2, 7), - newMockEntity(7, 2), - ) - - for i := 0; i <= 9; i++ { - _, pos := searcher.entities[i].GetPosition() - assert.Equal(t, float64(i), pos) - } - -} - -func TestRangeSearcher_SearchByRect(t *testing.T) { - searcher := &rangeSearcher{ - entities: make([]MapEntity, 0, 64), - } - - searcher.Add( - newMockEntity(0, 9), - newMockEntity(8, 1), - newMockEntity(1, 8), - newMockEntity(3, 6), - newMockEntity(5, 4), - newMockEntity(6, 3), - newMockEntity(9, 0), - newMockEntity(4, 5), - newMockEntity(2, 7), - newMockEntity(7, 2), - ) - - matches := searcher.SearchByRect(d2common.Rectangle{ - Left: 3, - Top: 0, - Width: 4, - Height: 9, - }) - - valsX := make([]float64, 0) - for _, match := range matches { - x, _ := match.GetPosition() - valsX = append(valsX, x) - } - - assert.ElementsMatch(t, []float64{3, 4, 5, 6, 7}, valsX) - - matches = searcher.SearchByRect(d2common.Rectangle{ - Left: 0, - Top: 1, - Width: 9, - Height: 4, - }) - - valsY := make([]float64, 0) - for _, match := range matches { - _, y := match.GetPosition() - valsY = append(valsY, y) - } - - assert.ElementsMatch(t, []float64{1, 2, 3, 4, 5}, valsY) - - matches = searcher.SearchByRect(d2common.Rectangle{ - Left: 3, - Top: 3, - Width: 2, - Height: 2, - }) - - valsY = make([]float64, 0) - valsX = make([]float64, 0) - for _, match := range matches { - x, y := match.GetPosition() - valsX = append(valsX, x) - valsY = append(valsY, y) - } - - assert.ElementsMatch(t, []float64{4, 5}, valsY) - assert.ElementsMatch(t, []float64{4, 5}, valsX) -} diff --git a/d2core/d2map/region.go b/d2core/d2map/region.go deleted file mode 100644 index 2dfd2bae..00000000 --- a/d2core/d2map/region.go +++ /dev/null @@ -1,890 +0,0 @@ -package d2map - -import ( - "errors" - "image/color" - "log" - "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/d2dat" - "github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2ds1" - "github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2dt1" - "github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource" - - "github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset" - "github.com/OpenDiablo2/OpenDiablo2/d2core/d2render" - "github.com/beefsack/go-astar" -) - -var imageCacheRecords map[uint32]d2render.Surface - -type PathTile struct { - Walkable bool - Up, Down, Left, Right, UpLeft, UpRight, DownLeft, DownRight *PathTile - X, Y float64 -} - -func (t *PathTile) PathNeighbors() []astar.Pather { - result := make([]astar.Pather, 0) - if t.Up != nil { - result = append(result, t.Up) - } - if t.Right != nil { - result = append(result, t.Right) - } - if t.Down != nil { - result = append(result, t.Down) - } - if t.Left != nil { - result = append(result, t.Left) - } - if t.UpLeft != nil { - result = append(result, t.UpLeft) - } - if t.UpRight != nil { - result = append(result, t.UpRight) - } - if t.DownLeft != nil { - result = append(result, t.DownLeft) - } - if t.DownRight != nil { - result = append(result, t.DownRight) - } - - return result -} - -func (t *PathTile) PathNeighborCost(to astar.Pather) float64 { - return 1 // No cost specifics currently... -} - -func (t *PathTile) PathEstimatedCost(to astar.Pather) float64 { - toT := to.(*PathTile) - absX := toT.X - t.X - if absX < 0 { - absX = -absX - } - absY := toT.Y - t.Y - if absY < 0 { - absY = -absY - } - r := absX + absY - - return r -} - -type MapRegion struct { - tileRect d2common.Rectangle - regionPath string - levelType d2datadict.LevelTypeRecord - levelPreset d2datadict.LevelPresetRecord - tiles []d2dt1.Tile - ds1 *d2ds1.DS1 - palette *d2dat.DATPalette - startX float64 - startY float64 - seed int64 - currentFrame int - lastFrameTime float64 - walkableArea [][]PathTile -} - -// Invalidates the global region image cache. Call this when you are changing regions -func InvalidateImageCache() { - imageCacheRecords = nil -} - -func loadRegion(seed int64, tileOffsetX, tileOffsetY int, levelType d2enum.RegionIdType, levelPreset int, fileIndex int, cacheTiles bool) (*MapRegion, []MapEntity) { - region := &MapRegion{ - levelType: d2datadict.LevelTypes[levelType], - levelPreset: d2datadict.LevelPresets[levelPreset], - seed: seed, - } - - region.palette, _ = loadPaletteForAct(levelType) - - for _, levelTypeDt1 := range region.levelType.Files { - if len(levelTypeDt1) != 0 && levelTypeDt1 != "" && levelTypeDt1 != "0" { - fileData, err := d2asset.LoadFile("/data/global/tiles/" + levelTypeDt1) - if err != nil { - panic(err) - } - - dt1, _ := d2dt1.LoadDT1(fileData) - region.tiles = append(region.tiles, dt1.Tiles...) - } - } - - var levelFilesToPick []string - for _, fileRecord := range region.levelPreset.Files { - if len(fileRecord) != 0 && fileRecord != "" && fileRecord != "0" { - levelFilesToPick = append(levelFilesToPick, fileRecord) - } - } - - levelIndex := int(math.Round(float64(len(levelFilesToPick)-1) * rand.Float64())) - if fileIndex >= 0 && fileIndex < len(levelFilesToPick) { - levelIndex = fileIndex - } - - if levelFilesToPick == nil { - panic("no level files to pick from") - } - - region.regionPath = levelFilesToPick[levelIndex] - fileData, err := d2asset.LoadFile("/data/global/tiles/" + region.regionPath) - if err != nil { - panic(err) - } - region.ds1, _ = d2ds1.LoadDS1(fileData) - region.tileRect = d2common.Rectangle{ - Left: tileOffsetX, - Top: tileOffsetY, - Width: int(region.ds1.Width), - Height: int(region.ds1.Height), - } - - entities := region.loadEntities() - region.loadSpecials() - region.generateWalkableMatrix() - - if cacheTiles { - region.generateTileCache() - } - return region, entities -} - -func (mr *MapRegion) generateWalkableMatrix() { - mr.walkableArea = make([][]PathTile, mr.tileRect.Height*5) - for y := 0; y < mr.tileRect.Height*5; y++ { - mr.walkableArea[y] = make([]PathTile, mr.tileRect.Width*5) - ty := int(float64(y) / 5.0) - for x := 0; x < mr.tileRect.Width*5; x++ { - tx := int(float64(x) / 5.0) - tile := mr.GetTile(tx, ty) - isBlocked := false - for _, floor := range tile.Floors { - tileData := mr.GetTileData(int32(floor.Style), int32(floor.Sequence), d2enum.Floor) - tileSubAttrs := &d2dt1.SubTileFlags{} - if tileData != nil { - tileSubAttrs = tileData.GetSubTileFlags(x%5, y%5) - } - isBlocked = isBlocked || tileSubAttrs.BlockWalk - if isBlocked { - break - } - } - if !isBlocked { - for _, wall := range tile.Walls { - tileData := mr.GetTileData(int32(wall.Style), int32(wall.Sequence), d2enum.TileType(wall.Type)) - tileSubAttrs := &d2dt1.SubTileFlags{} - if tileData != nil { - tileSubAttrs = tileData.GetSubTileFlags(x%5, y%5) - } - isBlocked = isBlocked || tileSubAttrs.BlockWalk - if isBlocked { - break - } - } - } - mr.walkableArea[y][x] = PathTile{ - Walkable: !isBlocked, - X: (float64(x) / 5.0) + float64(mr.tileRect.Left), - Y: (float64(y) / 5.0) + float64(mr.tileRect.Top), - } - if !isBlocked && y > 0 && mr.walkableArea[y-1][x].Walkable { - mr.walkableArea[y][x].Up = &mr.walkableArea[y-1][x] - mr.walkableArea[y-1][x].Down = &mr.walkableArea[y][x] - } - if !isBlocked && x > 0 && mr.walkableArea[y][x-1].Walkable { - mr.walkableArea[y][x].Left = &mr.walkableArea[y][x-1] - mr.walkableArea[y][x-1].Right = &mr.walkableArea[y][x] - } - if !isBlocked && x > 0 && y > 0 && mr.walkableArea[y-1][x-1].Walkable { - mr.walkableArea[y][x].UpLeft = &mr.walkableArea[y-1][x-1] - mr.walkableArea[y-1][x-1].DownRight = &mr.walkableArea[y][x] - } - if !isBlocked && y > 0 && x < (mr.tileRect.Width*5)-1 && mr.walkableArea[y-1][x+1].Walkable { - mr.walkableArea[y][x].UpRight = &mr.walkableArea[y-1][x+1] - mr.walkableArea[y-1][x+1].DownLeft = &mr.walkableArea[y][x] - } - } - } -} - -func (mr *MapRegion) GetTileRect() d2common.Rectangle { - return mr.tileRect -} - -func (mr *MapRegion) GetLevelPreset() d2datadict.LevelPresetRecord { - return mr.levelPreset -} - -func (mr *MapRegion) GetLevelType() d2datadict.LevelTypeRecord { - return mr.levelType -} - -func (mr *MapRegion) GetPath() string { - return mr.regionPath -} - -func (mr *MapRegion) loadSpecials() { - for tileY := range mr.ds1.Tiles { - for tileX := range mr.ds1.Tiles[tileY] { - for _, wall := range mr.ds1.Tiles[tileY][tileX].Walls { - if wall.Type == 10 && wall.Style == 30 && wall.Sequence == 0 && mr.startX == 0 && mr.startY == 0 { - mr.startX, mr.startY = mr.getTileWorldPosition(tileX, tileY) - mr.startX += 0.5 - mr.startY += 0.5 - return - } - } - } - } -} - -func (mr *MapRegion) GetTile(x, y int) *d2ds1.TileRecord { - return &mr.ds1.Tiles[y][x] -} - -func (mr *MapRegion) GetTileData(style int32, sequence int32, tileType d2enum.TileType) *d2dt1.Tile { - for _, tile := range mr.tiles { - if tile.Style == style && tile.Sequence == sequence && tile.Type == int32(tileType) { - return &tile - } - } - return nil -} - -func (mr *MapRegion) GetTileSize() (int, int) { - return mr.tileRect.Width, mr.tileRect.Height -} - -func (mr *MapRegion) loadEntities() []MapEntity { - var entities []MapEntity - - for _, object := range mr.ds1.Objects { - worldX, worldY := mr.getSubTileWorldPosition(object.X, object.Y) - - switch object.Lookup.Type { - case d2datadict.ObjectTypeCharacter: - if object.Lookup.Base != "" && object.Lookup.Token != "" && object.Lookup.TR != "" { - 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 := CreateAnimatedComposite(int(worldX), int(worldY), object.Lookup, d2resource.PaletteUnits) - if err != nil { - panic(err) - } - entity.SetMode(object.Lookup.Mode, object.Lookup.Class, 0) - entities = append(entities, entity) - } - } - } - - return entities -} - -func (mr *MapRegion) getStartTilePosition() (float64, float64) { - return float64(mr.tileRect.Left) + mr.startX, float64(mr.tileRect.Top) + mr.startY -} - -func (mr *MapRegion) getRandomTile(tiles []d2dt1.Tile, x, y int, seed int64) byte { - /* Walker's Alias Method for weighted random selection - * with xorshifting for random numbers */ - - var tileSeed uint64 - tileSeed = uint64(seed) + uint64(x) - tileSeed *= uint64(y) + uint64(mr.levelType.Id) - - tileSeed ^= tileSeed << 13 - tileSeed ^= tileSeed >> 17 - tileSeed ^= tileSeed << 5 - - weightSum := 0 - for _, tile := range tiles { - weightSum += int(tile.RarityFrameIndex) - } - - if weightSum == 0 { - return 0 - } - - random := tileSeed % uint64(weightSum) - - sum := 0 - for i, tile := range tiles { - sum += int(tile.RarityFrameIndex) - if sum >= int(random) { - return byte(i) - } - } - - // This return shouldn't be hit - return 0 -} - -func (mr *MapRegion) getTiles(style, sequence, tileType int32) []d2dt1.Tile { - var tiles []d2dt1.Tile - for _, tile := range mr.tiles { - if tile.Style != style || tile.Sequence != sequence || tile.Type != tileType { - continue - } - tiles = append(tiles, tile) - } - if len(tiles) == 0 { - log.Printf("Unknown tile ID [%d %d %d]\n", style, sequence, tileType) - return nil - } - return tiles -} - -func (mr *MapRegion) isVisbile(viewport *Viewport) bool { - return viewport.IsTileRectVisible(mr.tileRect) -} - -func (mr *MapRegion) advance(elapsed float64) { - frameLength := 0.1 - - mr.lastFrameTime += elapsed - framesAdvanced := int(mr.lastFrameTime / frameLength) - mr.lastFrameTime -= float64(framesAdvanced) * frameLength - - mr.currentFrame += framesAdvanced - if mr.currentFrame > 9 { - mr.currentFrame = 0 - } -} - -func (mr *MapRegion) getSubTileWorldPosition(subTileX, subTileY int) (float64, float64) { - return float64(subTileX + (mr.tileRect.Left * 5)), float64(subTileY + (mr.tileRect.Top * 5)) -} - -func (mr *MapRegion) getTileWorldPosition(tileX, tileY int) (float64, float64) { - return float64(tileX + mr.tileRect.Left), float64(tileY + mr.tileRect.Top) -} - -func (mr *MapRegion) renderPass1(viewport *Viewport, target d2render.Surface) { - for tileY := range mr.ds1.Tiles { - for tileX, tile := range mr.ds1.Tiles[tileY] { - worldX, worldY := mr.getTileWorldPosition(tileX, tileY) - if viewport.IsTileVisible(worldX, worldY) { - viewport.PushTranslationWorld(worldX, worldY) - mr.renderTilePass1(tile, viewport, target) - viewport.PopTranslation() - } - } - } -} - -func (mr *MapRegion) renderPass2(entities MapEntitiesSearcher, viewport *Viewport, target d2render.Surface) { - tileEntities := entities.SearchByRect(mr.tileRect) - nextIndex := 0 - - for tileY := range mr.ds1.Tiles { - for tileX, tile := range mr.ds1.Tiles[tileY] { - worldX, worldY := mr.getTileWorldPosition(tileX, tileY) - if viewport.IsTileVisible(worldX, worldY) { - viewport.PushTranslationWorld(worldX, worldY) - mr.renderTilePass2(tile, viewport, target) - - for nextIndex < len(tileEntities) { - nextX, nextY := tileEntities[nextIndex].GetPosition() - if nextX == worldX && nextY == worldY { - target.PushTranslation(viewport.GetTranslationScreen()) - tileEntities[nextIndex].Render(target) - target.Pop() - nextIndex++ - } else if (nextY == worldY && nextX < worldX) || nextY < worldY { - nextIndex++ - } else { - break - } - } - - viewport.PopTranslation() - } - } - } -} - -func (mr *MapRegion) renderPass3(viewport *Viewport, target d2render.Surface) { - for tileY := range mr.ds1.Tiles { - for tileX, tile := range mr.ds1.Tiles[tileY] { - worldX, worldY := mr.getTileWorldPosition(tileX, tileY) - if viewport.IsTileVisible(worldX, worldY) { - viewport.PushTranslationWorld(worldX, worldY) - mr.renderTilePass3(tile, viewport, target) - viewport.PopTranslation() - } - } - } -} - -func (mr *MapRegion) renderTilePass1(tile d2ds1.TileRecord, viewport *Viewport, target d2render.Surface) { - for _, wall := range tile.Walls { - if !wall.Hidden && wall.Prop1 != 0 && wall.Type.LowerWall() { - mr.renderWall(wall, viewport, target) - } - } - - for _, floor := range tile.Floors { - if !floor.Hidden && floor.Prop1 != 0 { - mr.renderFloor(floor, viewport, target) - } - } - - for _, shadow := range tile.Shadows { - if !shadow.Hidden && shadow.Prop1 != 0 { - mr.renderShadow(shadow, viewport, target) - } - } -} - -func (mr *MapRegion) renderTilePass2(tile d2ds1.TileRecord, viewport *Viewport, target d2render.Surface) { - for _, wall := range tile.Walls { - if !wall.Hidden && wall.Type.UpperWall() { - mr.renderWall(wall, viewport, target) - } - } -} - -func (mr *MapRegion) renderTilePass3(tile d2ds1.TileRecord, viewport *Viewport, target d2render.Surface) { - for _, wall := range tile.Walls { - if wall.Type == d2enum.Roof { - mr.renderWall(wall, viewport, target) - } - } -} - -func (mr *MapRegion) renderFloor(tile d2ds1.FloorShadowRecord, viewport *Viewport, target d2render.Surface) { - var img d2render.Surface - if !tile.Animated { - img = mr.getImageCacheRecord(tile.Style, tile.Sequence, 0, tile.RandomIndex) - } else { - img = mr.getImageCacheRecord(tile.Style, tile.Sequence, 0, byte(mr.currentFrame)) - } - if img == nil { - log.Printf("Render called on uncached floor {%v,%v}", tile.Style, tile.Sequence) - return - } - - viewport.PushTranslationOrtho(-80, float64(tile.YAdjust)) - defer viewport.PopTranslation() - - target.PushTranslation(viewport.GetTranslationScreen()) - defer target.Pop() - - target.Render(img) -} - -func (mr *MapRegion) renderWall(tile d2ds1.WallRecord, viewport *Viewport, target d2render.Surface) { - img := mr.getImageCacheRecord(tile.Style, tile.Sequence, tile.Type, tile.RandomIndex) - if img == nil { - log.Printf("Render called on uncached wall {%v,%v,%v}", tile.Style, tile.Sequence, tile.Type) - return - } - - viewport.PushTranslationOrtho(-80, float64(tile.YAdjust)-8) - defer viewport.PopTranslation() - - target.PushTranslation(viewport.GetTranslationScreen()) - defer target.Pop() - - target.Render(img) -} - -func (mr *MapRegion) renderShadow(tile d2ds1.FloorShadowRecord, viewport *Viewport, target d2render.Surface) { - img := mr.getImageCacheRecord(tile.Style, tile.Sequence, 13, tile.RandomIndex) - if img == nil { - log.Printf("Render called on uncached shadow {%v,%v}", tile.Style, tile.Sequence) - return - } - - viewport.PushTranslationOrtho(-80, float64(tile.YAdjust)) - defer viewport.PopTranslation() - - target.PushTranslation(viewport.GetTranslationScreen()) - target.PushColor(color.RGBA{R: 255, G: 255, B: 255, A: 160}) - defer target.PopN(2) - - target.Render(img) -} - -func (mr *MapRegion) renderDebug(debugVisLevel int, viewport *Viewport, target d2render.Surface) { - for tileY := range mr.ds1.Tiles { - for tileX := range mr.ds1.Tiles[tileY] { - worldX, worldY := mr.getTileWorldPosition(tileX, tileY) - if viewport.IsTileVisible(worldX, worldY) { - mr.renderTileDebug(int(worldX), int(worldY), debugVisLevel, viewport, target) - } - } - } -} - -func (mr *MapRegion) renderTileDebug(x, y int, debugVisLevel int, viewport *Viewport, target d2render.Surface) { - ax := x - mr.tileRect.Left - ay := y - mr.tileRect.Top - - if debugVisLevel > 0 { - if ay < 0 || ax < 0 || ay >= len(mr.ds1.Tiles) || ax >= len(mr.ds1.Tiles[ay]) { - return - } - 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} - - screenX1, screenY1 := viewport.WorldToScreen(float64(x), float64(y)) - screenX2, screenY2 := viewport.WorldToScreen(float64(x+1), float64(y)) - screenX3, screenY3 := viewport.WorldToScreen(float64(x), float64(y+1)) - - target.PushTranslation(screenX1, screenY1) - defer target.Pop() - - target.DrawLine(screenX2-screenX1, screenY2-screenY1, tileColor) - target.DrawLine(screenX3-screenX1, screenY3-screenY1, tileColor) - target.PushTranslation(-10, 10) - target.DrawText("%v, %v", x, y) - target.Pop() - - if debugVisLevel > 1 { - for i := 1; i <= 4; i++ { - x2 := i * 16 - y2 := i * 8 - - target.PushTranslation(-x2, y2) - target.DrawLine(80, 40, subTileColor) - target.Pop() - - target.PushTranslation(x2, y2) - target.DrawLine(-80, 40, subTileColor) - target.Pop() - } - - tile := mr.ds1.Tiles[ay][ax] - - for i, floor := range tile.Floors { - target.PushTranslation(-20, 10+(i+1)*14) - target.DrawText("f: %v-%v", floor.Style, floor.Sequence) - target.Pop() - } - - for yy := 0; yy < 5; yy++ { - for xx := 0; xx < 5; xx++ { - isoX := (xx - yy) * 16 - isoY := (xx + yy) * 8 - if !((len(mr.walkableArea) <= yy+(ay*5)) || (len(mr.walkableArea[yy+(ay*5)]) <= xx+(ax*5))) { - var walkableArea = mr.walkableArea[yy+(ay*5)][xx+(ax*5)] - if !walkableArea.Walkable { - target.PushTranslation(isoX-3, isoY+4) - target.DrawRect(5, 5, tileCollisionColor) - target.Pop() - } - } - } - } - } - } -} - -func (mr *MapRegion) generateTileCache() { - for tileY := range mr.ds1.Tiles { - for tileX := range mr.ds1.Tiles[tileY] { - tile := mr.ds1.Tiles[tileY][tileX] - - for i := range tile.Floors { - if !tile.Floors[i].Hidden && tile.Floors[i].Prop1 != 0 { - mr.generateFloorCache(&tile.Floors[i], tileX, tileY) - } - } - - for i := range tile.Shadows { - if !tile.Shadows[i].Hidden && tile.Shadows[i].Prop1 != 0 { - mr.generateShadowCache(&tile.Shadows[i], tileX, tileY) - } - } - - for i := range tile.Walls { - if !tile.Walls[i].Hidden && tile.Walls[i].Prop1 != 0 { - mr.generateWallCache(&tile.Walls[i], tileX, tileY) - } - } - } - } -} - -func (mr *MapRegion) getImageCacheRecord(style, sequence byte, tileType d2enum.TileType, randomIndex byte) d2render.Surface { - lookupIndex := uint32(style)<<24 | uint32(sequence)<<16 | uint32(tileType)<<8 | uint32(randomIndex) - return imageCacheRecords[lookupIndex] -} - -func (mr *MapRegion) setImageCacheRecord(style, sequence byte, tileType d2enum.TileType, randomIndex byte, image d2render.Surface) { - lookupIndex := uint32(style)<<24 | uint32(sequence)<<16 | uint32(tileType)<<8 | uint32(randomIndex) - if imageCacheRecords == nil { - imageCacheRecords = make(map[uint32]d2render.Surface) - } - imageCacheRecords[lookupIndex] = image -} - -func (mr *MapRegion) generateFloorCache(tile *d2ds1.FloorShadowRecord, tileX, tileY int) { - tileOptions := mr.getTiles(int32(tile.Style), int32(tile.Sequence), 0) - var tileData []*d2dt1.Tile - var tileIndex byte - - if tileOptions == nil { - log.Printf("Could not locate tile Style:%d, Seq: %d, Type: %d\n", tile.Style, tile.Sequence, 0) - tileData = append(tileData, &d2dt1.Tile{}) - tileData[0].Width = 10 - tileData[0].Height = 10 - } else { - if !tileOptions[0].MaterialFlags.Lava { - tileIndex = mr.getRandomTile(tileOptions, tileX, tileY, mr.seed) - tileData = append(tileData, &tileOptions[tileIndex]) - } else { - tile.Animated = true - for i := range tileOptions { - tileData = append(tileData, &tileOptions[i]) - } - } - } - - for i := range tileData { - if !tileData[i].MaterialFlags.Lava { - tile.RandomIndex = tileIndex - } else { - tileIndex = byte(tileData[i].RarityFrameIndex) - } - cachedImage := mr.getImageCacheRecord(tile.Style, tile.Sequence, 0, tileIndex) - if cachedImage != nil { - return - } - tileYMinimum := int32(0) - for _, block := range tileData[i].Blocks { - tileYMinimum = d2common.MinInt32(tileYMinimum, int32(block.Y)) - } - tileYOffset := d2common.AbsInt32(tileYMinimum) - tileHeight := d2common.AbsInt32(tileData[i].Height) - image, _ := d2render.NewSurface(int(tileData[i].Width), int(tileHeight), d2render.FilterNearest) - pixels := make([]byte, 4*tileData[i].Width*tileHeight) - mr.decodeTileGfxData(tileData[i].Blocks, &pixels, tileYOffset, tileData[i].Width) - image.ReplacePixels(pixels) - mr.setImageCacheRecord(tile.Style, tile.Sequence, 0, tileIndex, image) - } -} - -func (mr *MapRegion) generateShadowCache(tile *d2ds1.FloorShadowRecord, tileX, tileY int) { - tileOptions := mr.getTiles(int32(tile.Style), int32(tile.Sequence), 13) - var tileIndex byte - var tileData *d2dt1.Tile - if tileOptions == nil { - return - } else { - tileIndex = mr.getRandomTile(tileOptions, tileX, tileY, mr.seed) - tileData = &tileOptions[tileIndex] - } - - if tileData.Width == 0 || tileData.Height == 0 { - return - } - - tile.RandomIndex = tileIndex - tileMinY := int32(0) - tileMaxY := int32(0) - for _, block := range tileData.Blocks { - tileMinY = d2common.MinInt32(tileMinY, int32(block.Y)) - tileMaxY = d2common.MaxInt32(tileMaxY, int32(block.Y+32)) - } - tileYOffset := -tileMinY - tileHeight := int(tileMaxY - tileMinY) - tile.YAdjust = int(tileMinY + 80) - - cachedImage := mr.getImageCacheRecord(tile.Style, tile.Sequence, 13, tileIndex) - if cachedImage != nil { - return - } - - image, _ := d2render.NewSurface(int(tileData.Width), tileHeight, d2render.FilterNearest) - pixels := make([]byte, 4*tileData.Width*int32(tileHeight)) - mr.decodeTileGfxData(tileData.Blocks, &pixels, tileYOffset, tileData.Width) - image.ReplacePixels(pixels) - mr.setImageCacheRecord(tile.Style, tile.Sequence, 13, tileIndex, image) -} - -func (mr *MapRegion) generateWallCache(tile *d2ds1.WallRecord, tileX, tileY int) { - tileOptions := mr.getTiles(int32(tile.Style), int32(tile.Sequence), int32(tile.Type)) - var tileIndex byte - var tileData *d2dt1.Tile - if tileOptions == nil { - return - } else { - tileIndex = mr.getRandomTile(tileOptions, tileX, tileY, mr.seed) - tileData = &tileOptions[tileIndex] - } - - tile.RandomIndex = tileIndex - var newTileData *d2dt1.Tile = nil - - if tile.Type == 3 { - newTileOptions := mr.getTiles(int32(tile.Style), int32(tile.Sequence), int32(4)) - newTileIndex := mr.getRandomTile(newTileOptions, tileX, tileY, mr.seed) - newTileData = &newTileOptions[newTileIndex] - } - - tileMinY := int32(0) - tileMaxY := int32(0) - - target := tileData - - if newTileData != nil && newTileData.Height < tileData.Height { - target = newTileData - } - - for _, block := range target.Blocks { - tileMinY = d2common.MinInt32(tileMinY, int32(block.Y)) - tileMaxY = d2common.MaxInt32(tileMaxY, int32(block.Y+32)) - } - - realHeight := d2common.MaxInt32(d2common.AbsInt32(tileData.Height), tileMaxY-tileMinY) - tileYOffset := -tileMinY - //tileHeight := int(tileMaxY - tileMinY) - - if tile.Type == 15 { - tile.YAdjust = -int(tileData.RoofHeight) - } else { - tile.YAdjust = int(tileMinY) + 80 - } - - cachedImage := mr.getImageCacheRecord(tile.Style, tile.Sequence, tile.Type, tileIndex) - if cachedImage != nil { - return - } - - if realHeight == 0 { - log.Printf("Invalid 0 height for wall tile") - return - } - - image, _ := d2render.NewSurface(160, int(realHeight), d2render.FilterNearest) - pixels := make([]byte, 4*160*realHeight) - mr.decodeTileGfxData(tileData.Blocks, &pixels, tileYOffset, 160) - - if newTileData != nil { - mr.decodeTileGfxData(newTileData.Blocks, &pixels, tileYOffset, 160) - } - - if err := image.ReplacePixels(pixels); err != nil { - log.Panicf(err.Error()) - } - - mr.setImageCacheRecord(tile.Style, tile.Sequence, tile.Type, tileIndex, image) -} - -func (mr *MapRegion) decodeTileGfxData(blocks []d2dt1.Block, pixels *[]byte, tileYOffset int32, tileWidth int32) { - for _, block := range blocks { - if block.Format == d2dt1.BlockFormatIsometric { - // 3D isometric decoding - xjump := []int32{14, 12, 10, 8, 6, 4, 2, 0, 2, 4, 6, 8, 10, 12, 14} - nbpix := []int32{4, 8, 12, 16, 20, 24, 28, 32, 28, 24, 20, 16, 12, 8, 4} - blockX := int32(block.X) - blockY := int32(block.Y) - length := int32(256) - x := int32(0) - y := int32(0) - idx := 0 - for length > 0 { - x = xjump[y] - n := nbpix[y] - length -= n - for n > 0 { - colorIndex := block.EncodedData[idx] - if colorIndex != 0 { - pixelColor := mr.palette.Colors[colorIndex] - offset := 4 * (((blockY + y + tileYOffset) * tileWidth) + (blockX + x)) - (*pixels)[offset] = pixelColor.R - (*pixels)[offset+1] = pixelColor.G - (*pixels)[offset+2] = pixelColor.B - (*pixels)[offset+3] = 255 - } - x++ - n-- - idx++ - } - y++ - } - } else { - // RLE Encoding - blockX := int32(block.X) - blockY := int32(block.Y) - x := int32(0) - y := int32(0) - idx := 0 - length := block.Length - for length > 0 { - b1 := block.EncodedData[idx] - b2 := block.EncodedData[idx+1] - idx += 2 - length -= 2 - if (b1 | b2) == 0 { - x = 0 - y++ - continue - } - x += int32(b1) - length -= int32(b2) - for b2 > 0 { - colorIndex := block.EncodedData[idx] - if colorIndex != 0 { - pixelColor := mr.palette.Colors[colorIndex] - - offset := 4 * (((blockY + y + tileYOffset) * tileWidth) + (blockX + x)) - (*pixels)[offset] = pixelColor.R - (*pixels)[offset+1] = pixelColor.G - (*pixels)[offset+2] = pixelColor.B - (*pixels)[offset+3] = 255 - - } - idx++ - x++ - b2-- - } - } - } - } -} - -func loadPaletteForAct(levelType d2enum.RegionIdType) (*d2dat.DATPalette, error) { - var palettePath string - switch levelType { - case d2enum.RegionAct1Town, d2enum.RegionAct1Wilderness, d2enum.RegionAct1Cave, d2enum.RegionAct1Crypt, - d2enum.RegionAct1Monestary, d2enum.RegionAct1Courtyard, d2enum.RegionAct1Barracks, - d2enum.RegionAct1Jail, d2enum.RegionAct1Cathedral, d2enum.RegionAct1Catacombs, d2enum.RegionAct1Tristram: - palettePath = d2resource.PaletteAct1 - break - case d2enum.RegionAct2Town, d2enum.RegionAct2Sewer, d2enum.RegionAct2Harem, d2enum.RegionAct2Basement, - d2enum.RegionAct2Desert, d2enum.RegionAct2Tomb, d2enum.RegionAct2Lair, d2enum.RegionAct2Arcane: - palettePath = d2resource.PaletteAct2 - break - case d2enum.RegionAct3Town, d2enum.RegionAct3Jungle, d2enum.RegionAct3Kurast, d2enum.RegionAct3Spider, - d2enum.RegionAct3Dungeon, d2enum.RegionAct3Sewer: - palettePath = d2resource.PaletteAct3 - break - case d2enum.RegionAct4Town, d2enum.RegionAct4Mesa, d2enum.RegionAct4Lava, d2enum.RegionAct5Lava: - palettePath = d2resource.PaletteAct4 - break - case d2enum.RegonAct5Town, d2enum.RegionAct5Siege, d2enum.RegionAct5Barricade, d2enum.RegionAct5Temple, - d2enum.RegionAct5IceCaves, d2enum.RegionAct5Baal: - palettePath = d2resource.PaletteAct5 - break - default: - return nil, errors.New("failed to find palette for region") - } - - return d2asset.LoadPalette(palettePath) -} diff --git a/d2core/d2map/renderer.go b/d2core/d2map/renderer.go deleted file mode 100644 index e10cd5b4..00000000 --- a/d2core/d2map/renderer.go +++ /dev/null @@ -1,72 +0,0 @@ -package d2map - -import ( - "github.com/OpenDiablo2/OpenDiablo2/d2core/d2render" - "github.com/OpenDiablo2/OpenDiablo2/d2core/d2term" -) - -type MapRenderer struct { - mapEngine *MapEngine - viewport *Viewport - camera Camera - debugVisLevel int -} - -func CreateMapRenderer(mapEngine *MapEngine) *MapRenderer { - result := &MapRenderer{ - mapEngine: mapEngine, - viewport: NewViewport(0, 0, 800, 600), - } - result.viewport.SetCamera(&result.camera) - d2term.BindAction("mapdebugvis", "set map debug visualization level", func(level int) { - result.debugVisLevel = level - }) - return result -} - -func (m *MapRenderer) SetMapEngine(mapEngine *MapEngine) { - m.mapEngine = mapEngine -} - -func (m *MapRenderer) Render(target d2render.Surface) { - for _, region := range m.mapEngine.regions { - if region.isVisbile(m.viewport) { - region.renderPass1(m.viewport, target) - region.renderDebug(m.debugVisLevel, m.viewport, target) - region.renderPass2(m.mapEngine.entities, m.viewport, target) - region.renderPass3(m.viewport, target) - } - } -} - -func (m *MapRenderer) MoveCameraTo(x, y float64) { - m.camera.MoveTo(x, y) -} - -func (m *MapRenderer) MoveCameraBy(x, y float64) { - m.camera.MoveBy(x, y) -} - -func (m *MapRenderer) ScreenToWorld(x, y int) (float64, float64) { - return m.viewport.ScreenToWorld(x, y) -} - -func (m *MapRenderer) ScreenToOrtho(x, y int) (float64, float64) { - return m.viewport.ScreenToOrtho(x, y) -} - -func (m *MapRenderer) WorldToOrtho(x, y float64) (float64, float64) { - return m.viewport.WorldToOrtho(x, y) -} - -func (m *MapRenderer) ViewportToLeft() { - m.viewport.toLeft() -} - -func (m *MapRenderer) ViewportToRight() { - m.viewport.toRight() -} - -func (m *MapRenderer) ViewportDefault() { - m.viewport.resetAlign() -} diff --git a/d2core/d2ui/button.go b/d2core/d2ui/button.go index b6a9e0ae..e1ec5158 100644 --- a/d2core/d2ui/button.go +++ b/d2core/d2ui/button.go @@ -208,7 +208,7 @@ func (v *Button) SetEnabled(enabled bool) { v.enabled = enabled } -// GetSize returns the size of the button +// Size returns the size of the button func (v *Button) GetSize() (int, int) { return v.width, v.height } diff --git a/d2core/d2ui/label.go b/d2core/d2ui/label.go index 219c5c17..cba5657c 100644 --- a/d2core/d2ui/label.go +++ b/d2core/d2ui/label.go @@ -94,7 +94,7 @@ func (v *Label) SetText(newText string) { v.imageData = nil } -// GetSize returns the size of the label +// Size returns the size of the label func (v Label) GetSize() (width, height int) { v.cacheImage() width = v.Width diff --git a/d2game/d2gamescreen/character_select.go b/d2game/d2gamescreen/character_select.go index e20ebece..71090028 100644 --- a/d2game/d2gamescreen/character_select.go +++ b/d2game/d2gamescreen/character_select.go @@ -6,6 +6,8 @@ import ( "os" "strings" + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2mapentity" + "github.com/OpenDiablo2/OpenDiablo2/d2game/d2player" "github.com/OpenDiablo2/OpenDiablo2/d2networking/d2client" @@ -20,7 +22,6 @@ import ( "github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2audio" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2inventory" - "github.com/OpenDiablo2/OpenDiablo2/d2core/d2map" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2render" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2screen" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2ui" @@ -43,7 +44,7 @@ type CharacterSelect struct { characterNameLabel [8]d2ui.Label characterStatsLabel [8]d2ui.Label characterExpLabel [8]d2ui.Label - characterImage [8]*d2map.Player + characterImage [8]*d2mapentity.Player gameStates []*d2player.PlayerState selectedCharacter int mouseButtonPressed bool @@ -165,7 +166,7 @@ func (v *CharacterSelect) updateCharacterBoxes() { v.characterStatsLabel[i].SetText("Level 1 " + v.gameStates[idx].HeroType.String()) v.characterExpLabel[i].SetText(expText) // TODO: Generate or load the object from the actual player data... - v.characterImage[i] = d2map.CreatePlayer("", "", 0, 0, 0, + v.characterImage[i] = d2mapentity.CreatePlayer("", "", 0, 0, 0, v.gameStates[idx].HeroType, d2inventory.HeroObjects[v.gameStates[idx].HeroType], ) diff --git a/d2game/d2gamescreen/game.go b/d2game/d2gamescreen/game.go index 0bcf4f78..23b906be 100644 --- a/d2game/d2gamescreen/game.go +++ b/d2game/d2gamescreen/game.go @@ -3,17 +3,15 @@ package d2gamescreen import ( "image/color" - "github.com/OpenDiablo2/OpenDiablo2/d2networking/d2netpacket" - - "github.com/OpenDiablo2/OpenDiablo2/d2game/d2player" + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2mapentity" + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2maprenderer" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2audio" - "github.com/OpenDiablo2/OpenDiablo2/d2core/d2map" - - "github.com/OpenDiablo2/OpenDiablo2/d2networking/d2client" - "github.com/OpenDiablo2/OpenDiablo2/d2core/d2input" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2render" + "github.com/OpenDiablo2/OpenDiablo2/d2game/d2player" + "github.com/OpenDiablo2/OpenDiablo2/d2networking/d2client" + "github.com/OpenDiablo2/OpenDiablo2/d2networking/d2netpacket" ) type Game struct { @@ -21,22 +19,23 @@ type Game struct { //pentSpinRight *d2ui.Sprite //testLabel d2ui.Label gameClient *d2client.GameClient - mapRenderer *d2map.MapRenderer + mapRenderer *d2maprenderer.MapRenderer gameControls *d2player.GameControls // TODO: Hack - localPlayer *d2map.Player + localPlayer *d2mapentity.Player lastLevelType int ticksSinceLevelCheck float64 } func CreateGame(gameClient *d2client.GameClient) *Game { - return &Game{ + result := &Game{ gameClient: gameClient, gameControls: nil, localPlayer: nil, lastLevelType: -1, ticksSinceLevelCheck: 0, - mapRenderer: d2map.CreateMapRenderer(gameClient.MapEngine), + mapRenderer: d2maprenderer.CreateMapRenderer(gameClient.MapEngine), } + return result } func (v *Game) OnLoad() error { @@ -50,6 +49,10 @@ func (v *Game) OnUnload() error { } func (v *Game) Render(screen d2render.Surface) error { + if v.gameClient.RegenMap { + v.gameClient.RegenMap = false + v.mapRenderer.RegenerateTileCache() + } screen.Clear(color.Black) v.mapRenderer.Render(screen) if v.gameControls != nil { @@ -71,19 +74,15 @@ func (v *Game) Advance(tickTime float64) error { if v.ticksSinceLevelCheck > 1.0 { v.ticksSinceLevelCheck = 0 if v.localPlayer != nil { - region := v.gameClient.MapEngine.GetRegionAtTile(v.localPlayer.TileX, v.localPlayer.TileY) - if region != nil { - levelType := region.GetLevelType().Id - if levelType != v.lastLevelType { - v.lastLevelType = levelType - switch levelType { - case 1: // Rogue encampent - v.localPlayer.SetIsInTown(true) - d2audio.PlayBGM("/data/global/music/Act1/town1.wav") - case 2: // Blood Moore - v.localPlayer.SetIsInTown(false) - d2audio.PlayBGM("/data/global/music/Act1/wild.wav") - } + tile := v.gameClient.MapEngine.TileAt(v.localPlayer.TileX, v.localPlayer.TileY) + if tile != nil { + switch tile.RegionType { + case 1: // Rogue encampent + v.localPlayer.SetIsInTown(true) + d2audio.PlayBGM("/data/global/music/Act1/town1.wav") + case 2: // Blood Moore + v.localPlayer.SetIsInTown(false) + d2audio.PlayBGM("/data/global/music/Act1/wild.wav") } } } diff --git a/d2game/d2gamescreen/map_engine_testing.go b/d2game/d2gamescreen/map_engine_testing.go index 77a82b5c..0512b492 100644 --- a/d2game/d2gamescreen/map_engine_testing.go +++ b/d2game/d2gamescreen/map_engine_testing.go @@ -1,20 +1,20 @@ package d2gamescreen import ( - "math" "math/rand" "os" "time" - "github.com/OpenDiablo2/OpenDiablo2/d2game/d2player" + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2mapgen" + + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2mapengine" + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2maprenderer" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" - "github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2dt1" - "github.com/OpenDiablo2/OpenDiablo2/d2core/d2input" - "github.com/OpenDiablo2/OpenDiablo2/d2core/d2map" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2render" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2screen" + "github.com/OpenDiablo2/OpenDiablo2/d2game/d2player" ) type RegionSpec struct { @@ -80,8 +80,8 @@ var regions = []RegionSpec{ type MapEngineTest struct { gameState *d2player.PlayerState - mapEngine *d2map.MapEngine - mapRenderer *d2map.MapRenderer + mapEngine *d2mapengine.MapEngine + mapRenderer *d2maprenderer.MapRenderer //TODO: this is region specific properties, should be refactored for multi-region rendering currentRegion int @@ -105,7 +105,7 @@ func CreateMapEngineTest(currentRegion int, levelPreset int) *MapEngineTest { } func (met *MapEngineTest) LoadRegionByIndex(n int, levelPreset, fileIndex int) { - d2map.InvalidateImageCache() + d2maprenderer.InvalidateImageCache() for _, spec := range regions { if spec.regionType == d2enum.RegionIdType(n) { met.regionSpec = spec @@ -131,20 +131,20 @@ func (met *MapEngineTest) LoadRegionByIndex(n int, levelPreset, fileIndex int) { if n == 0 { rand.Seed(time.Now().UnixNano()) - met.mapEngine.GenerateAct1Overworld(true) + d2mapgen.GenerateAct1Overworld(met.mapEngine) } else { - met.mapEngine = d2map.CreateMapEngine(0) // necessary for map name update + met.mapEngine = d2mapengine.CreateMapEngine() // necessary for map name update met.mapEngine.GenerateMap(d2enum.RegionIdType(n), levelPreset, fileIndex, true) - met.mapRenderer.SetMapEngine(met.mapEngine) + met.mapEngine.RegenerateWalkPaths() } - + met.mapRenderer.SetMapEngine(met.mapEngine) met.mapRenderer.MoveCameraTo(met.mapRenderer.WorldToOrtho(met.mapEngine.GetCenterPosition())) } func (met *MapEngineTest) OnLoad() error { d2input.BindHandler(met) - met.mapEngine = d2map.CreateMapEngine(0) - met.mapRenderer = d2map.CreateMapRenderer(met.mapEngine) + met.mapEngine = d2mapengine.CreateMapEngine() + met.mapRenderer = d2maprenderer.CreateMapRenderer(met.mapEngine) met.LoadRegionByIndex(met.currentRegion, met.levelPreset, met.fileIndex) return nil @@ -158,121 +158,113 @@ func (met *MapEngineTest) OnUnload() error { func (met *MapEngineTest) Render(screen d2render.Surface) error { met.mapRenderer.Render(screen) - screenX, screenY := d2render.GetCursorPos() - worldX, worldY := met.mapRenderer.ScreenToWorld(screenX, screenY) - //subtileX := int(math.Ceil(math.Mod(worldX*10, 10))) / 2 - //subtileY := int(math.Ceil(math.Mod(worldY*10, 10))) / 2 - - curRegion := met.mapEngine.GetRegionAtTile(int(worldX), int(worldY)) - if curRegion == nil { - return nil - } - - tileRect := curRegion.GetTileRect() - - levelFilesToPick := make([]string, 0) - fileIndex := met.fileIndex - levelPreset := curRegion.GetLevelPreset() - regionPath := curRegion.GetPath() - for n, fileRecord := range levelPreset.Files { - if len(fileRecord) == 0 || fileRecord == "" || fileRecord == "0" { - continue - } - levelFilesToPick = append(levelFilesToPick, fileRecord) - if fileRecord == regionPath { - fileIndex = n - } - } - if met.fileIndex == -1 { - met.fileIndex = fileIndex - } - met.filesCount = len(levelFilesToPick) - - tileX := int(math.Floor(worldX)) - tileRect.Left - tileY := int(math.Floor(worldY)) - tileRect.Top - - subtileX := int((worldX - float64(int(worldX))) * 5) - subtileY := int((worldY - float64(int(worldY))) * 5) - - regionWidth, regionHeight := curRegion.GetTileSize() - if tileX >= 0 && tileY >= 0 && tileX < regionWidth && tileY < regionHeight { - tile := curRegion.GetTile(tileX, tileY) - screen.PushTranslation(5, 5) - screen.DrawText("%d, %d (Tile %d.%d, %d.%d)", screenX, screenY, tileX, subtileX, tileY, subtileY) - screen.PushTranslation(0, 16) - screen.DrawText("Map: " + curRegion.GetLevelType().Name) - screen.PushTranslation(0, 16) - screen.DrawText("%v: %v/%v [%v, %v]", regionPath, fileIndex+1, met.filesCount, met.currentRegion, met.levelPreset) - screen.PushTranslation(0, 16) - screen.DrawText("N - next region, P - previous region") - screen.PushTranslation(0, 16) - screen.DrawText("Shift+N - next preset, Shift+P - previous preset") - screen.PushTranslation(0, 16) - screen.DrawText("Ctrl+N - next file, Ctrl+P - previous file") - screen.PushTranslation(0, 16) - popN := 7 - if len(tile.Floors) > 0 { - screen.PushTranslation(0, 16) - screen.DrawText("Floors:") - screen.PushTranslation(16, 0) - for idx, floor := range tile.Floors { - popN++ - screen.PushTranslation(0, 16) - tileData := curRegion.GetTileData(int32(floor.Style), int32(floor.Sequence), d2enum.Floor) - tileSubAttrs := d2dt1.SubTileFlags{} - if tileData != nil { - tileSubAttrs = *tileData.GetSubTileFlags(subtileX, subtileY) - } - screen.DrawText("Floor %v: [ANI:%t] %s", idx, floor.Animated, tileSubAttrs.DebugString()) - - } - screen.PushTranslation(-16, 0) - popN += 3 - } - if len(tile.Walls) > 0 { - screen.PushTranslation(0, 16) - screen.DrawText("Walls:") - screen.PushTranslation(16, 0) - for idx, wall := range tile.Walls { - popN++ - screen.PushTranslation(0, 16) - tileData := curRegion.GetTileData(int32(wall.Style), int32(wall.Sequence), d2enum.Floor) - tileSubAttrs := d2dt1.SubTileFlags{} - if tileData != nil { - tileSubAttrs = *tileData.GetSubTileFlags(subtileX, subtileY) - } - screen.DrawText("Wall %v: [HID:%t] %s", idx, wall.Hidden, tileSubAttrs.DebugString()) - - } - screen.PushTranslation(-16, 0) - popN += 3 - } - if len(tile.Walls) > 0 { - screen.PushTranslation(0, 16) - screen.DrawText("Shadows:") - screen.PushTranslation(16, 0) - for idx, shadow := range tile.Shadows { - popN++ - screen.PushTranslation(0, 16) - tileData := curRegion.GetTileData(int32(shadow.Style), int32(shadow.Sequence), d2enum.Floor) - tileSubAttrs := d2dt1.SubTileFlags{} - if tileData != nil { - tileSubAttrs = *tileData.GetSubTileFlags(subtileX, subtileY) - } - screen.DrawText("Wall %v: [HID:%t] %s", idx, shadow.Hidden, tileSubAttrs.DebugString()) - - } - screen.PushTranslation(-16, 0) - popN += 3 - } - screen.PopN(popN) - } + //screenX, screenY := d2render.GetCursorPos() + //worldX, worldY := met.mapRenderer.ScreenToWorld(screenX, screenY) + // + //levelFilesToPick := make([]string, 0) + //fileIndex := met.fileIndex + //levelPreset := curRegion.LevelPreset() + //regionPath := curRegion.RegionPath() + //for n, fileRecord := range levelPreset.Files { + // if len(fileRecord) == 0 || fileRecord == "" || fileRecord == "0" { + // continue + // } + // levelFilesToPick = append(levelFilesToPick, fileRecord) + // if fileRecord == regionPath { + // fileIndex = n + // } + //} + //if met.fileIndex == -1 { + // met.fileIndex = fileIndex + //} + //met.filesCount = len(levelFilesToPick) + // + //tileX := int(math.Floor(worldX)) + //tileY := int(math.Floor(worldY)) + // + //subtileX := int((worldX - float64(int(worldX))) * 5) + //subtileY := int((worldY - float64(int(worldY))) * 5) + // + //regionWidth, regionHeight := curRegion.GetTileSize() + //if tileX >= 0 && tileY >= 0 && tileX < regionWidth && tileY < regionHeight { + // tile := curRegion.Tile(tileX, tileY) + // screen.PushTranslation(5, 5) + // screen.DrawText("%d, %d (Tile %d.%d, %d.%d)", screenX, screenY, tileX, subtileX, tileY, subtileY) + // screen.PushTranslation(0, 16) + // screen.DrawText("Map: " + curRegion.LevelType().Name) + // screen.PushTranslation(0, 16) + // screen.DrawText("%v: %v/%v [%v, %v]", regionPath, fileIndex+1, met.filesCount, met.currentRegion, met.levelPreset) + // screen.PushTranslation(0, 16) + // screen.DrawText("N - next region, P - previous region") + // screen.PushTranslation(0, 16) + // screen.DrawText("Shift+N - next preset, Shift+P - previous preset") + // screen.PushTranslation(0, 16) + // screen.DrawText("Ctrl+N - next file, Ctrl+P - previous file") + // screen.PushTranslation(0, 16) + // popN := 7 + // if len(tile.Floors) > 0 { + // screen.PushTranslation(0, 16) + // screen.DrawText("Floors:") + // screen.PushTranslation(16, 0) + // for idx, floor := range tile.Floors { + // popN++ + // screen.PushTranslation(0, 16) + // tileData := curRegion.TileData(int32(floor.Style), int32(floor.Sequence), d2enum.Floor) + // tileSubAttrs := d2dt1.SubTileFlags{} + // if tileData != nil { + // tileSubAttrs = *tileData.GetSubTileFlags(subtileX, subtileY) + // } + // screen.DrawText("Floor %v: [ANI:%t] %s", idx, floor.Animated, tileSubAttrs.DebugString()) + // + // } + // screen.PushTranslation(-16, 0) + // popN += 3 + // } + // if len(tile.Walls) > 0 { + // screen.PushTranslation(0, 16) + // screen.DrawText("Walls:") + // screen.PushTranslation(16, 0) + // for idx, wall := range tile.Walls { + // popN++ + // screen.PushTranslation(0, 16) + // tileData := curRegion.TileData(int32(wall.Style), int32(wall.Sequence), d2enum.Floor) + // tileSubAttrs := d2dt1.SubTileFlags{} + // if tileData != nil { + // tileSubAttrs = *tileData.GetSubTileFlags(subtileX, subtileY) + // } + // screen.DrawText("Wall %v: [HID:%t] %s", idx, wall.Hidden, tileSubAttrs.DebugString()) + // + // } + // screen.PushTranslation(-16, 0) + // popN += 3 + // } + // if len(tile.Walls) > 0 { + // screen.PushTranslation(0, 16) + // screen.DrawText("Shadows:") + // screen.PushTranslation(16, 0) + // for idx, shadow := range tile.Shadows { + // popN++ + // screen.PushTranslation(0, 16) + // tileData := curRegion.TileData(int32(shadow.Style), int32(shadow.Sequence), d2enum.Floor) + // tileSubAttrs := d2dt1.SubTileFlags{} + // if tileData != nil { + // tileSubAttrs = *tileData.GetSubTileFlags(subtileX, subtileY) + // } + // screen.DrawText("Wall %v: [HID:%t] %s", idx, shadow.Hidden, tileSubAttrs.DebugString()) + // + // } + // screen.PushTranslation(-16, 0) + // popN += 3 + // } + // screen.PopN(popN) + //} return nil } func (met *MapEngineTest) Advance(tickTime float64) error { met.mapEngine.Advance(tickTime) + met.mapRenderer.Advance(tickTime) return nil } diff --git a/d2game/d2player/game_controls.go b/d2game/d2player/game_controls.go index 0ebd5fac..4ecb27df 100644 --- a/d2game/d2player/game_controls.go +++ b/d2game/d2player/game_controls.go @@ -6,7 +6,9 @@ import ( "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/d2map/d2mapengine" + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2mapentity" + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2maprenderer" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2render" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2term" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2ui" @@ -23,9 +25,9 @@ type Panel interface { var missileID = 59 type GameControls struct { - hero *d2map.Player - mapEngine *d2map.MapEngine - mapRenderer *d2map.MapRenderer + hero *d2mapentity.Player + mapEngine *d2mapengine.MapEngine + mapRenderer *d2maprenderer.MapRenderer inventory *Inventory heroStats *HeroStats escapeMenu *EscapeMenu @@ -38,7 +40,7 @@ type GameControls struct { skillIcon *d2ui.Sprite } -func NewGameControls(hero *d2map.Player, mapEngine *d2map.MapEngine, mapRenderer *d2map.MapRenderer, inputListener InputCallbackListener) *GameControls { +func NewGameControls(hero *d2mapentity.Player, mapEngine *d2mapengine.MapEngine, mapRenderer *d2maprenderer.MapRenderer, inputListener InputCallbackListener) *GameControls { d2term.BindAction("setmissile", "set missile id to summon on right click", func(id int) { missileID = id }) @@ -91,7 +93,7 @@ func (g *GameControls) OnMouseButtonDown(event d2input.MouseEvent) bool { } if event.Button == d2input.MouseButtonRight { - missile, err := d2map.CreateMissile( + missile, err := d2mapentity.CreateMissile( int(g.hero.AnimatedComposite.LocationX), int(g.hero.AnimatedComposite.LocationY), d2datadict.Missiles[missileID], diff --git a/d2networking/d2client/game_client.go b/d2networking/d2client/game_client.go index 6627795f..09665fe0 100644 --- a/d2networking/d2client/game_client.go +++ b/d2networking/d2client/game_client.go @@ -4,10 +4,13 @@ import ( "fmt" "log" - "github.com/OpenDiablo2/OpenDiablo2/d2game/d2player" + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2mapgen" + + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2mapengine" + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2mapentity" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" - "github.com/OpenDiablo2/OpenDiablo2/d2core/d2map" + "github.com/OpenDiablo2/OpenDiablo2/d2game/d2player" "github.com/OpenDiablo2/OpenDiablo2/d2networking/d2client/d2clientconnectiontype" "github.com/OpenDiablo2/OpenDiablo2/d2networking/d2client/d2localclient" "github.com/OpenDiablo2/OpenDiablo2/d2networking/d2client/d2remoteclient" @@ -19,16 +22,17 @@ type GameClient struct { clientConnection ClientConnection connectionType d2clientconnectiontype.ClientConnectionType GameState *d2player.PlayerState - MapEngine *d2map.MapEngine + MapEngine *d2mapengine.MapEngine PlayerId string - Players map[string]*d2map.Player + Players map[string]*d2mapentity.Player Seed int64 + RegenMap bool } func Create(connectionType d2clientconnectiontype.ClientConnectionType) (*GameClient, error) { result := &GameClient{ - MapEngine: d2map.CreateMapEngine(0), - Players: make(map[string]*d2map.Player, 0), + MapEngine: d2mapengine.CreateMapEngine(), // TODO: Mapgen - Needs levels.txt stuff + Players: make(map[string]*d2mapentity.Player, 0), connectionType: connectionType, } @@ -64,8 +68,9 @@ func (g *GameClient) OnPacketReceived(packet d2netpacket.NetPacket) error { mapData := packet.PacketData.(d2netpacket.GenerateMapPacket) switch mapData.RegionType { case d2enum.RegionAct1Town: - g.MapEngine.GenerateAct1Overworld(true) + d2mapgen.GenerateAct1Overworld(g.MapEngine) } + g.RegenMap = true break case d2netpackettype.UpdateServerInfo: serverInfo := packet.PacketData.(d2netpacket.UpdateServerInfoPacket) @@ -76,7 +81,7 @@ func (g *GameClient) OnPacketReceived(packet d2netpacket.NetPacket) error { break case d2netpackettype.AddPlayer: player := packet.PacketData.(d2netpacket.AddPlayerPacket) - newPlayer := d2map.CreatePlayer(player.Id, player.Name, player.X, player.Y, 0, player.HeroType, player.Equipment) + newPlayer := d2mapentity.CreatePlayer(player.Id, player.Name, player.X, player.Y, 0, player.HeroType, player.Equipment) g.Players[newPlayer.Id] = newPlayer g.MapEngine.AddEntity(newPlayer) break @@ -86,7 +91,13 @@ func (g *GameClient) OnPacketReceived(packet d2netpacket.NetPacket) error { path, _, found := g.MapEngine.PathFind(movePlayer.StartX, movePlayer.StartY, movePlayer.DestX, movePlayer.DestY) if found { player.AnimatedComposite.SetPath(path, func() { - if g.MapEngine.GetRegionAtTile(player.TileX, player.TileY).GetLevelType().Id == int(d2enum.RegionAct1Town) { + tile := g.MapEngine.TileAt(player.TileX, player.TileY) + if tile == nil { + return + } + + regionType := tile.RegionType + if regionType == d2enum.RegionAct1Town { player.AnimatedComposite.SetAnimationMode(d2enum.AnimationModePlayerTownNeutral.String()) } else { player.AnimatedComposite.SetAnimationMode(d2enum.AnimationModePlayerNeutral.String()) diff --git a/d2networking/d2server/game_server.go b/d2networking/d2server/game_server.go index 72095677..3de46b7f 100644 --- a/d2networking/d2server/game_server.go +++ b/d2networking/d2server/game_server.go @@ -9,9 +9,13 @@ import ( "log" "net" "strings" + "time" + + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2mapgen" + + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2mapengine" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" - "github.com/OpenDiablo2/OpenDiablo2/d2core/d2map" "github.com/OpenDiablo2/OpenDiablo2/d2networking/d2netpacket" "github.com/OpenDiablo2/OpenDiablo2/d2networking/d2netpacket/d2netpackettype" "github.com/OpenDiablo2/OpenDiablo2/d2networking/d2server/d2udpclientconnection" @@ -21,7 +25,7 @@ import ( type GameServer struct { clientConnections map[string]ClientConnection - mapEngines []*d2map.MapEngine + mapEngines []*d2mapengine.MapEngine scriptEngine *d2script.ScriptEngine udpConnection *net.UDPConn seed int64 @@ -38,13 +42,14 @@ func Create(openNetworkServer bool) { singletonServer = &GameServer{ clientConnections: make(map[string]ClientConnection), - mapEngines: make([]*d2map.MapEngine, 0), + mapEngines: make([]*d2mapengine.MapEngine, 0), scriptEngine: d2script.CreateScriptEngine(), - seed: 1592539977884044000, //time.Now().UnixNano(), + seed: time.Now().UnixNano(), } - mapEngine := d2map.CreateMapEngine(singletonServer.seed) - mapEngine.GenerateAct1Overworld(false) + mapEngine := d2mapengine.CreateMapEngine() + mapEngine.ResetMap(0, d2enum.RegionAct1Town, 100, 100) // TODO: Mapgen - Needs levels.txt stuff + d2mapgen.GenerateAct1Overworld(mapEngine) singletonServer.mapEngines = append(singletonServer.mapEngines, mapEngine) singletonServer.scriptEngine.AddFunction("getMapEngines", func(call otto.FunctionCall) otto.Value {