mirror of
https://github.com/OpenDiablo2/OpenDiablo2
synced 2024-11-04 17:27:16 -05:00
698 lines
22 KiB
Go
698 lines
22 KiB
Go
package d2maprenderer
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"image/color"
|
|
"math"
|
|
"strconv"
|
|
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2ds1"
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2math/d2vector"
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource"
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2util"
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset"
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2mapengine"
|
|
)
|
|
|
|
const (
|
|
logPrefix = "Map Renderer"
|
|
)
|
|
|
|
const (
|
|
screenMiddleX = 400
|
|
two = 2
|
|
|
|
dbgOffsetXY = 40
|
|
dbgBoxWidth = 220
|
|
dbgBoxHeight = 60
|
|
dbgBoxPadding = 10
|
|
|
|
dbgCollisionSize = 5
|
|
dbgCollisionOffsetX = -3
|
|
dbgCollisionOffsetY = 4
|
|
|
|
whiteHalfOpacity = 0xffffff7f
|
|
blackQuarterOpacity = 0x00000040
|
|
lightGreenFullOpacity = 0x40ff00ff
|
|
magentaFullOpacity = 0xff00ffff
|
|
yellowFullOpacity = 0xffff00ff
|
|
lightBlueQuarterOpacity = 0x5050ff32
|
|
whiteQuarterOpacity = 0xffffff64
|
|
redQuarterOpacity = 0x74000064
|
|
|
|
subtilesPerTile = 5
|
|
orthoSubTileWidth = 16
|
|
orthoSubTileHeight = 8
|
|
orthoTileWidth = subtilesPerTile * orthoSubTileWidth
|
|
orthoTileHeight = subtilesPerTile * orthoSubTileHeight
|
|
)
|
|
|
|
// MapRenderer manages the game viewport and Camera. It requests tile and entity data from MapEngine and renders it.
|
|
type MapRenderer struct {
|
|
asset *d2asset.AssetManager
|
|
renderer d2interface.Renderer // Used for drawing operations
|
|
mapEngine *d2mapengine.MapEngine // The map engine that is being rendered
|
|
palette d2interface.Palette // The palette used for this map
|
|
viewport *Viewport // Used for rendering offsets
|
|
Camera Camera // Used to determine where on the map we are rendering
|
|
imageCacheRecords map[uint32]d2interface.Surface
|
|
mapDebugVisLevel int // Map debug visibility index (0=none, 1=tiles, 2=sub-tiles)
|
|
entityDebugVisLevel int // Entity Debug visibility index (0=none, 1=vectors)
|
|
lastFrameTime float64 // The last time the map was rendered
|
|
currentFrame int // Current render frame (for animations)
|
|
|
|
*d2util.Logger
|
|
}
|
|
|
|
// CreateMapRenderer creates a new MapRenderer, sets the required fields and returns a pointer to it.
|
|
func CreateMapRenderer(asset *d2asset.AssetManager, renderer d2interface.Renderer,
|
|
mapEngine *d2mapengine.MapEngine,
|
|
term d2interface.Terminal, l d2util.LogLevel, startX, startY float64) *MapRenderer {
|
|
result := &MapRenderer{
|
|
asset: asset,
|
|
renderer: renderer,
|
|
mapEngine: mapEngine,
|
|
viewport: NewViewport(0, 0, 800, 600),
|
|
}
|
|
|
|
result.Logger = d2util.NewLogger()
|
|
result.Logger.SetPrefix(logPrefix)
|
|
result.Logger.SetLevel(l)
|
|
|
|
result.Camera = Camera{}
|
|
rx, ry := result.WorldToOrtho(startX, startY)
|
|
startPosition := d2vector.NewPosition(rx, ry)
|
|
result.Camera.position = &startPosition
|
|
result.viewport.SetCamera(&result.Camera)
|
|
|
|
var name, desc, level string
|
|
|
|
name, desc = "mapdebugvis", "set map debug visualization level"
|
|
level = "level"
|
|
|
|
if err := term.Bind(name, desc, []string{level}, result.commandMapDebugVis); err != nil {
|
|
result.Errorf("could not bind the mapdebugvis action, err: %v", err)
|
|
}
|
|
|
|
name, desc = "entitydebugvis", "set entity debug visualization level"
|
|
|
|
if err := term.Bind(name, desc, []string{level}, result.commandEntityDebugVis); err != nil {
|
|
result.Errorf("could not bind the entitydebugvis action, err: %v", err)
|
|
}
|
|
|
|
if mapEngine.LevelType().ID != 0 {
|
|
result.generateTileCache()
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// UnbindTerminalCommands unbinds commands from the terminal
|
|
func (mr *MapRenderer) UnbindTerminalCommands(term d2interface.Terminal) error {
|
|
return term.Unbind("mapdebugvis", "entitydebugvis")
|
|
}
|
|
|
|
func (mr *MapRenderer) commandMapDebugVis(args []string) error {
|
|
if len(args) < 1 {
|
|
return fmt.Errorf("invalid argument supplied")
|
|
}
|
|
|
|
level, err := strconv.Atoi(args[0])
|
|
if err != nil {
|
|
return fmt.Errorf("invalid argument supplied")
|
|
}
|
|
|
|
mr.mapDebugVisLevel = level
|
|
|
|
return nil
|
|
}
|
|
|
|
func (mr *MapRenderer) commandEntityDebugVis(args []string) error {
|
|
level, err := strconv.Atoi(args[0])
|
|
if err != nil {
|
|
return fmt.Errorf("invalid argument supplied")
|
|
}
|
|
|
|
mr.entityDebugVisLevel = level
|
|
|
|
return nil
|
|
}
|
|
|
|
// RegenerateTileCache calls MapRenderer.generateTileCache().
|
|
func (mr *MapRenderer) RegenerateTileCache() {
|
|
mr.generateTileCache()
|
|
}
|
|
|
|
// SetMapEngine sets the MapEngine this renderer is rendering.
|
|
func (mr *MapRenderer) SetMapEngine(mapEngine *d2mapengine.MapEngine) {
|
|
mr.mapEngine = mapEngine
|
|
mr.generateTileCache()
|
|
}
|
|
|
|
// Render determines the width and height of map tiles that should be rendered. The following four render passes are
|
|
// made in succession:
|
|
//
|
|
// Pass 1: Lower wall tiles, tile shadows and floor tiles.
|
|
//
|
|
// Pass 2: Entities below walls.
|
|
//
|
|
// Pass 3: Upper wall tiles and entities above walls.
|
|
//
|
|
// Pass 4: Roof tiles.
|
|
func (mr *MapRenderer) Render(target d2interface.Surface) {
|
|
// https://github.com/OpenDiablo2/OpenDiablo2/issues/789
|
|
// Prevents concurrent map read & write exceptions that otherwise occur when we join a TCP game
|
|
// as a remote client, due to rendering before we have handled the GenerateMapPacket.
|
|
if mr.mapEngine.IsLoading {
|
|
return
|
|
}
|
|
|
|
mapSize := mr.mapEngine.Size()
|
|
|
|
stxf, styf := mr.viewport.ScreenToWorld(screenMiddleX, -200)
|
|
etxf, etyf := mr.viewport.ScreenToWorld(screenMiddleX, 1050)
|
|
|
|
startX := int(math.Max(0, math.Floor(stxf)))
|
|
startY := int(math.Max(0, math.Floor(styf)))
|
|
|
|
endX := int(math.Min(float64(mapSize.Width), math.Ceil(etxf)))
|
|
endY := int(math.Min(float64(mapSize.Height), math.Ceil(etyf)))
|
|
|
|
mr.renderPass1(target, startX, startY, endX, endY)
|
|
mr.renderPass2(target, startX, startY, endX, endY)
|
|
|
|
if mr.mapDebugVisLevel > 0 {
|
|
mr.renderMapDebug(mr.mapDebugVisLevel, target, startX, startY, endX, endY)
|
|
}
|
|
|
|
mr.renderPass3(target, startX, startY, endX, endY)
|
|
mr.renderPass4(target, startX, startY, endX, endY)
|
|
|
|
if mr.entityDebugVisLevel > 0 {
|
|
mr.renderEntityDebug(target)
|
|
}
|
|
}
|
|
|
|
// MoveCameraTo sets the position of the Camera to the given x and y coordinates.
|
|
func (mr *MapRenderer) MoveCameraTo(position *d2vector.Position) {
|
|
mr.Camera.MoveTo(position)
|
|
}
|
|
|
|
// MoveCameraBy adds the given vector to the current position of the Camera.
|
|
func (mr *MapRenderer) MoveCameraBy(vector *d2vector.Vector) {
|
|
mr.Camera.MoveBy(vector)
|
|
}
|
|
|
|
// MoveCameraTargetBy adds the given vector to the current position of the Camera.
|
|
func (mr *MapRenderer) MoveCameraTargetBy(vector *d2vector.Vector) {
|
|
mr.Camera.MoveTargetBy(vector)
|
|
}
|
|
|
|
// ScreenToWorld returns the world position for the given screen (pixel) position.
|
|
func (mr *MapRenderer) ScreenToWorld(x, y int) (worldX, worldY float64) {
|
|
return mr.viewport.ScreenToWorld(x, y)
|
|
}
|
|
|
|
// ScreenToOrtho returns the orthogonal position, without accounting for the isometric angle, for the given screen
|
|
// (pixel) position.
|
|
func (mr *MapRenderer) ScreenToOrtho(x, y int) (orthoX, orthoY float64) {
|
|
return mr.viewport.ScreenToOrtho(x, y)
|
|
}
|
|
|
|
// WorldToOrtho returns the orthogonal position for the given isometric world position.
|
|
func (mr *MapRenderer) WorldToOrtho(x, y float64) (orthoX, orthoY float64) {
|
|
return mr.viewport.WorldToOrtho(x, y)
|
|
}
|
|
|
|
// Lower wall tiles, tile shadows and floor tiles.
|
|
func (mr *MapRenderer) renderPass1(target d2interface.Surface, startX, startY, endX, endY int) {
|
|
for tileY := startY; tileY < endY; tileY++ {
|
|
for tileX := startX; tileX < endX; tileX++ {
|
|
tile := mr.mapEngine.TileAt(tileX, tileY)
|
|
mr.viewport.PushTranslationWorld(float64(tileX), float64(tileY))
|
|
mr.renderTilePass1(tile, target)
|
|
mr.viewport.PopTranslation()
|
|
}
|
|
}
|
|
}
|
|
|
|
// Entities below walls.
|
|
func (mr *MapRenderer) renderPass2(target d2interface.Surface, startX, startY, endX, endY int) {
|
|
for tileY := startY; tileY < endY; tileY++ {
|
|
for tileX := startX; tileX < endX; tileX++ {
|
|
mr.viewport.PushTranslationWorld(float64(tileX), float64(tileY))
|
|
|
|
tileEnt := mr.getEntitiesBelowWalls(tileX, tileY)
|
|
|
|
for subY := 0; subY < 5; subY++ {
|
|
for subX := 0; subX < 5; subX++ {
|
|
for _, mapEntity := range tileEnt {
|
|
pos := mapEntity.GetPosition()
|
|
if (int(pos.SubTileOffset().X()) != subX) || (int(pos.SubTileOffset().Y()) != subY) {
|
|
continue
|
|
}
|
|
|
|
target.PushTranslation(mr.viewport.GetTranslationScreen())
|
|
mapEntity.Render(target)
|
|
target.Pop()
|
|
}
|
|
}
|
|
}
|
|
|
|
mr.viewport.PopTranslation()
|
|
}
|
|
}
|
|
}
|
|
|
|
func (mr *MapRenderer) getEntitiesBelowWalls(tileX, tileY int) []d2interface.MapEntity {
|
|
entities := make([]d2interface.MapEntity, 0)
|
|
|
|
// need to add render culling
|
|
// https://github.com/OpenDiablo2/OpenDiablo2/issues/821
|
|
for _, mapEntity := range mr.mapEngine.Entities() {
|
|
pos := mapEntity.GetPosition()
|
|
vec := pos.World()
|
|
entityX, entityY := vec.X(), vec.Y()
|
|
|
|
if mapEntity.GetLayer() != 1 {
|
|
continue
|
|
}
|
|
|
|
if (int(entityX) != tileX) || (int(entityY) != tileY) {
|
|
continue
|
|
}
|
|
|
|
entities = append(entities, mapEntity)
|
|
}
|
|
|
|
return entities
|
|
}
|
|
|
|
// Upper wall tiles and entities above walls.
|
|
func (mr *MapRenderer) renderPass3(target d2interface.Surface, startX, startY, endX, endY int) {
|
|
for tileY := startY; tileY < endY; tileY++ {
|
|
for tileX := startX; tileX < endX; tileX++ {
|
|
tile := mr.mapEngine.TileAt(tileX, tileY)
|
|
mr.viewport.PushTranslationWorld(float64(tileX), float64(tileY))
|
|
mr.renderTilePass2(tile, target)
|
|
|
|
entities := mr.getEntitiesAboveWalls(tileX, tileY)
|
|
|
|
for subY := 0; subY < 5; subY++ {
|
|
for subX := 0; subX < 5; subX++ {
|
|
for _, entity := range entities {
|
|
pos := entity.GetPosition()
|
|
if (int(pos.SubTileOffset().X()) != subX) || (int(pos.SubTileOffset().Y()) != subY) {
|
|
continue
|
|
}
|
|
|
|
target.PushTranslation(mr.viewport.GetTranslationScreen())
|
|
entity.Render(target)
|
|
target.Pop()
|
|
}
|
|
}
|
|
}
|
|
|
|
mr.viewport.PopTranslation()
|
|
}
|
|
}
|
|
}
|
|
|
|
func (mr *MapRenderer) getEntitiesAboveWalls(tileX, tileY int) []d2interface.MapEntity {
|
|
entities := make([]d2interface.MapEntity, 0)
|
|
|
|
// need to add render culling
|
|
// https://github.com/OpenDiablo2/OpenDiablo2/issues/821
|
|
for _, mapEntity := range mr.mapEngine.Entities() {
|
|
pos := mapEntity.GetPosition()
|
|
vec := pos.World()
|
|
entityX, entityY := vec.X(), vec.Y()
|
|
|
|
if mapEntity.GetLayer() == 1 {
|
|
continue
|
|
}
|
|
|
|
if (int(entityX) != tileX) || (int(entityY) != tileY) {
|
|
continue
|
|
}
|
|
|
|
entities = append(entities, mapEntity)
|
|
}
|
|
|
|
return entities
|
|
}
|
|
|
|
// Roof tiles.
|
|
func (mr *MapRenderer) renderPass4(target d2interface.Surface, startX, startY, endX, endY int) {
|
|
for tileY := startY; tileY < endY; tileY++ {
|
|
for tileX := startX; tileX < endX; tileX++ {
|
|
tile := mr.mapEngine.TileAt(tileX, tileY)
|
|
mr.viewport.PushTranslationWorld(float64(tileX), float64(tileY))
|
|
mr.renderTilePass3(tile, target)
|
|
mr.viewport.PopTranslation()
|
|
}
|
|
}
|
|
}
|
|
|
|
func (mr *MapRenderer) renderTilePass1(tile *d2mapengine.MapTile, target d2interface.Surface) {
|
|
for _, wall := range tile.Components.Walls {
|
|
if !wall.Hidden() && wall.Prop1 != 0 && wall.Type.LowerWall() {
|
|
mr.renderWall(wall, mr.viewport, target)
|
|
}
|
|
}
|
|
|
|
for _, floor := range tile.Components.Floors {
|
|
if !floor.Hidden() && floor.Prop1 != 0 {
|
|
mr.renderFloor(floor, target)
|
|
}
|
|
}
|
|
|
|
for _, shadow := range tile.Components.Shadows {
|
|
if !shadow.Hidden() && shadow.Prop1 != 0 {
|
|
mr.renderShadow(shadow, target)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (mr *MapRenderer) renderTilePass2(tile *d2mapengine.MapTile, target d2interface.Surface) {
|
|
for _, wall := range tile.Components.Walls {
|
|
if !wall.Hidden() && wall.Type.UpperWall() {
|
|
mr.renderWall(wall, mr.viewport, target)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (mr *MapRenderer) renderTilePass3(tile *d2mapengine.MapTile, target d2interface.Surface) {
|
|
for _, wall := range tile.Components.Walls {
|
|
if wall.Type == d2enum.TileRoof {
|
|
mr.renderWall(wall, mr.viewport, target)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (mr *MapRenderer) renderFloor(tile d2ds1.FloorShadowRecord, target d2interface.Surface) {
|
|
var img d2interface.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 {
|
|
mr.Warningf("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 d2interface.Surface) {
|
|
img := mr.getImageCacheRecord(tile.Style, tile.Sequence, tile.Type, tile.RandomIndex)
|
|
if img == nil {
|
|
mr.Warningf("Render called on uncached wall {%v,%v,%v}", tile.Style, tile.Sequence, tile.Type)
|
|
return
|
|
}
|
|
|
|
viewport.PushTranslationOrtho(-80, float64(tile.YAdjust))
|
|
defer viewport.PopTranslation()
|
|
|
|
target.PushTranslation(viewport.GetTranslationScreen())
|
|
defer target.Pop()
|
|
|
|
target.Render(img)
|
|
}
|
|
|
|
func (mr *MapRenderer) renderShadow(tile d2ds1.FloorShadowRecord, target d2interface.Surface) {
|
|
img := mr.getImageCacheRecord(tile.Style, tile.Sequence, 13, tile.RandomIndex)
|
|
if img == nil {
|
|
mr.Warningf("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())
|
|
defer target.Pop()
|
|
|
|
target.PushColor(color.RGBA{R: 255, G: 255, B: 255, A: 160}) //nolint:gomnd // Not a magic number...
|
|
defer target.Pop()
|
|
|
|
target.Render(img)
|
|
}
|
|
|
|
func (mr *MapRenderer) renderMapDebug(mapDebugVisLevel int, target d2interface.Surface, startX, startY, endX, endY int) {
|
|
for tileY := startY; tileY < endY; tileY++ {
|
|
for tileX := startX; tileX < endX; tileX++ {
|
|
mr.viewport.PushTranslationWorld(float64(tileX), float64(tileY))
|
|
mr.renderTileDebug(tileX, tileY, mapDebugVisLevel, target)
|
|
mr.viewport.PopTranslation()
|
|
}
|
|
}
|
|
}
|
|
|
|
//nolint:funlen // doesn't make sense to split this function
|
|
func (mr *MapRenderer) renderEntityDebug(target d2interface.Surface) {
|
|
entities := mr.mapEngine.Entities()
|
|
|
|
for idx := range entities {
|
|
e := entities[idx]
|
|
pos := e.GetPosition()
|
|
world := pos
|
|
x, y := world.X()/subtilesPerTile, world.Y()/subtilesPerTile
|
|
velocity := e.GetVelocity()
|
|
velocity = *velocity.Clone()
|
|
vx, vy := mr.viewport.WorldToOrtho(velocity.X(), velocity.Y())
|
|
screenX, screenY := mr.viewport.WorldToScreen(x, y)
|
|
|
|
offX, offY := dbgOffsetXY, -dbgOffsetXY
|
|
|
|
entScreenXf, entScreenYf := mr.WorldToScreenF(e.GetPositionF())
|
|
entScreenX := int(math.Floor(entScreenXf))
|
|
entScreenY := int(math.Floor(entScreenYf))
|
|
entityWidth, entityHeight := e.GetSize()
|
|
halfWidth, halfHeight := entityWidth/two, entityHeight/two
|
|
l, r := entScreenX-halfWidth, entScreenX+halfWidth
|
|
t, b := entScreenY-halfHeight, entScreenY+halfHeight
|
|
mx, my := mr.renderer.GetCursorPos()
|
|
xWithin := (l <= mx) && (r >= mx)
|
|
yWithin := (t <= my) && (b >= my)
|
|
within := xWithin && yWithin
|
|
|
|
boxLineColor := d2util.Color(magentaFullOpacity)
|
|
boxHoverColor := d2util.Color(yellowFullOpacity)
|
|
|
|
boxColor := boxLineColor
|
|
|
|
if within {
|
|
boxColor = boxHoverColor
|
|
}
|
|
|
|
stack := 0
|
|
// box
|
|
mr.viewport.PushTranslationWorld(x, y)
|
|
|
|
target.PushTranslation(screenX, screenY)
|
|
stack++
|
|
|
|
target.PushTranslation(-halfWidth, -halfHeight)
|
|
stack++
|
|
|
|
target.DrawLine(0, entityHeight, boxColor)
|
|
target.DrawLine(entityWidth, 0, boxColor)
|
|
|
|
target.PushTranslation(entityWidth, entityHeight)
|
|
stack++
|
|
|
|
target.DrawLine(-entityWidth, 0, boxColor)
|
|
target.DrawLine(0, -entityHeight, boxColor)
|
|
target.PopN(stack)
|
|
mr.viewport.PopTranslation()
|
|
|
|
// hover
|
|
if within {
|
|
mr.viewport.PushTranslationWorld(x, y)
|
|
target.PushTranslation(screenX, screenY)
|
|
target.DrawLine(offX, offY, d2util.Color(whiteHalfOpacity))
|
|
target.PushTranslation(offX+dbgBoxPadding, offY-dbgBoxPadding*two)
|
|
target.PushTranslation(-dbgOffsetXY, -dbgOffsetXY)
|
|
target.DrawRect(dbgBoxWidth, dbgBoxHeight, d2util.Color(blackQuarterOpacity))
|
|
target.Pop()
|
|
target.DrawTextf("World (%.2f, %.2f)\nVelocity (%.2f, %.2f)", x, y, vx, vy)
|
|
target.Pop()
|
|
target.DrawLine(int(vx), int(vy), d2util.Color(lightGreenFullOpacity))
|
|
target.Pop()
|
|
mr.viewport.PopTranslation()
|
|
}
|
|
}
|
|
}
|
|
|
|
// WorldToScreen returns the screen (pixel) position for the given isometric world position as two ints.
|
|
func (mr *MapRenderer) WorldToScreen(x, y float64) (screenX, screenY int) {
|
|
return mr.viewport.WorldToScreen(x, y)
|
|
}
|
|
|
|
// WorldToScreenF returns the screen (pixel) position for the given isometric world position as two float64s.
|
|
func (mr *MapRenderer) WorldToScreenF(x, y float64) (screenX, screenY float64) {
|
|
return mr.viewport.WorldToScreenF(x, y)
|
|
}
|
|
|
|
func (mr *MapRenderer) renderTileDebug(ax, ay, debugVisLevel int, target d2interface.Surface) {
|
|
subTileColor := d2util.Color(lightBlueQuarterOpacity)
|
|
tileColor := d2util.Color(whiteQuarterOpacity)
|
|
tileCollisionColor := d2util.Color(redQuarterOpacity)
|
|
|
|
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.DrawTextf("%v, %v", ax, ay)
|
|
target.Pop()
|
|
|
|
if debugVisLevel > 1 {
|
|
for i := 1; i <= 4; i++ {
|
|
x2 := i * orthoSubTileWidth
|
|
y2 := i * orthoSubTileHeight
|
|
|
|
target.PushTranslation(-x2, y2)
|
|
target.DrawLine(orthoTileWidth, orthoTileHeight, subTileColor)
|
|
target.Pop()
|
|
|
|
target.PushTranslation(x2, y2)
|
|
target.DrawLine(-orthoTileWidth, orthoTileHeight, subTileColor)
|
|
target.Pop()
|
|
}
|
|
|
|
tile := mr.mapEngine.TileAt(ax, ay)
|
|
|
|
for i, wall := range tile.Components.Walls {
|
|
if wall.Type.Special() {
|
|
target.PushTranslation(-20, 10+(i+1)*14) // nolint:gomnd // just for debug
|
|
target.DrawTextf("s: %v-%v", wall.Style, wall.Sequence)
|
|
target.Pop()
|
|
}
|
|
}
|
|
|
|
for yy := 0; yy < 5; yy++ {
|
|
for xx := 0; xx < 5; xx++ {
|
|
isoX := (xx - yy) * orthoSubTileWidth
|
|
isoY := (xx + yy) * orthoSubTileHeight
|
|
|
|
blocked := tile.GetSubTileFlags(xx, yy).BlockWalk
|
|
|
|
if blocked {
|
|
target.PushTranslation(isoX+dbgCollisionOffsetX, isoY+dbgCollisionOffsetY)
|
|
target.DrawRect(dbgCollisionSize, dbgCollisionSize, tileCollisionColor)
|
|
target.Pop()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const (
|
|
frameOverflow = 10
|
|
frameLength = 1.0 / frameOverflow
|
|
)
|
|
|
|
// Advance is called once per frame and maintains the MapRenderer's previous
|
|
// render timestamp and current frame.
|
|
func (mr *MapRenderer) Advance(elapsed float64) {
|
|
mr.lastFrameTime += elapsed
|
|
framesAdvanced := int(mr.lastFrameTime / frameLength)
|
|
mr.lastFrameTime -= float64(framesAdvanced) * frameLength
|
|
|
|
mr.currentFrame += framesAdvanced
|
|
if mr.currentFrame >= frameOverflow {
|
|
mr.currentFrame = 0
|
|
}
|
|
|
|
mr.Camera.Advance(elapsed)
|
|
}
|
|
|
|
func (mr *MapRenderer) loadPaletteForAct(levelType d2enum.RegionIdType) (d2interface.Palette,
|
|
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
|
|
case d2enum.RegionAct2Town, d2enum.RegionAct2Sewer, d2enum.RegionAct2Harem, d2enum.RegionAct2Basement,
|
|
d2enum.RegionAct2Desert, d2enum.RegionAct2Tomb, d2enum.RegionAct2Lair, d2enum.RegionAct2Arcane:
|
|
palettePath = d2resource.PaletteAct2
|
|
case d2enum.RegionAct3Town, d2enum.RegionAct3Jungle, d2enum.RegionAct3Kurast, d2enum.RegionAct3Spider,
|
|
d2enum.RegionAct3Dungeon, d2enum.RegionAct3Sewer:
|
|
palettePath = d2resource.PaletteAct3
|
|
case d2enum.RegionAct4Town, d2enum.RegionAct4Mesa, d2enum.RegionAct4Lava, d2enum.RegionAct5Lava:
|
|
palettePath = d2resource.PaletteAct4
|
|
case d2enum.RegonAct5Town, d2enum.RegionAct5Siege, d2enum.RegionAct5Barricade, d2enum.RegionAct5Temple,
|
|
d2enum.RegionAct5IceCaves, d2enum.RegionAct5Baal:
|
|
palettePath = d2resource.PaletteAct5
|
|
default:
|
|
return nil, errors.New("failed to find palette for region")
|
|
}
|
|
|
|
return mr.asset.LoadPalette(palettePath)
|
|
}
|
|
|
|
// ViewportToLeft moves the viewport to the left.
|
|
func (mr *MapRenderer) ViewportToLeft() {
|
|
mr.viewport.toLeft()
|
|
}
|
|
|
|
// ViewportToRight moves the viewport to the right.
|
|
func (mr *MapRenderer) ViewportToRight() {
|
|
mr.viewport.toRight()
|
|
}
|
|
|
|
// ViewportDefault resets the viewport to it's default position.
|
|
func (mr *MapRenderer) ViewportDefault() {
|
|
mr.viewport.resetAlign()
|
|
}
|
|
|
|
// SetCameraTarget sets the Camera target
|
|
func (mr *MapRenderer) SetCameraTarget(position *d2vector.Position) {
|
|
mr.Camera.SetTarget(position)
|
|
}
|
|
|
|
// SetCameraPosition sets the Camera position
|
|
func (mr *MapRenderer) SetCameraPosition(position *d2vector.Position) {
|
|
mr.Camera.MoveTo(position)
|
|
}
|
|
|
|
// InvalidateImageCache the global region image cache. Call this when you are changing regions.
|
|
func (mr *MapRenderer) InvalidateImageCache() {
|
|
mr.imageCacheRecords = nil
|
|
}
|
|
|
|
func (mr *MapRenderer) getImageCacheRecord(style, sequence byte, tileType d2enum.TileType, randomIndex byte) d2interface.Surface {
|
|
lookupIndex := uint32(style)<<24 | uint32(sequence)<<16 | uint32(tileType)<<8 | uint32(randomIndex)
|
|
return mr.imageCacheRecords[lookupIndex]
|
|
}
|
|
|
|
func (mr *MapRenderer) setImageCacheRecord(style, sequence byte, tileType d2enum.TileType, randomIndex byte, image d2interface.Surface) {
|
|
lookupIndex := uint32(style)<<24 | uint32(sequence)<<16 | uint32(tileType)<<8 | uint32(randomIndex)
|
|
|
|
if mr.imageCacheRecords == nil {
|
|
mr.imageCacheRecords = make(map[uint32]d2interface.Surface)
|
|
}
|
|
|
|
mr.imageCacheRecords[lookupIndex] = image
|
|
}
|