2019-11-10 03:36:53 -05:00
|
|
|
package d2render
|
2019-11-06 18:25:19 -05:00
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
2019-11-08 11:05:51 -05:00
|
|
|
"image"
|
2019-11-10 12:28:41 -05:00
|
|
|
"log"
|
2019-11-13 20:31:47 -05:00
|
|
|
"math"
|
2019-11-06 18:25:19 -05:00
|
|
|
"strings"
|
|
|
|
|
2019-11-17 00:16:33 -05:00
|
|
|
"github.com/OpenDiablo2/D2Shared/d2data/d2cof"
|
2019-11-10 10:44:13 -05:00
|
|
|
|
2019-11-17 00:16:33 -05:00
|
|
|
"github.com/OpenDiablo2/D2Shared/d2data/d2dcc"
|
2019-11-10 10:44:13 -05:00
|
|
|
|
2019-11-17 00:16:33 -05:00
|
|
|
"github.com/OpenDiablo2/D2Shared/d2helper"
|
2019-11-10 03:36:53 -05:00
|
|
|
|
2019-11-17 00:16:33 -05:00
|
|
|
"github.com/OpenDiablo2/D2Shared/d2common/d2interface"
|
2019-11-10 03:36:53 -05:00
|
|
|
|
2019-11-17 00:16:33 -05:00
|
|
|
"github.com/OpenDiablo2/D2Shared/d2common/d2enum"
|
2019-11-10 03:36:53 -05:00
|
|
|
|
2019-11-17 00:16:33 -05:00
|
|
|
"github.com/OpenDiablo2/D2Shared/d2common"
|
2019-11-10 03:36:53 -05:00
|
|
|
|
2019-11-17 00:16:33 -05:00
|
|
|
"github.com/OpenDiablo2/D2Shared/d2data"
|
2019-11-10 03:36:53 -05:00
|
|
|
|
2019-11-17 00:16:33 -05:00
|
|
|
"github.com/OpenDiablo2/D2Shared/d2data/d2datadict"
|
2019-11-06 18:25:19 -05:00
|
|
|
|
|
|
|
"github.com/hajimehoshi/ebiten"
|
|
|
|
)
|
|
|
|
|
2019-11-09 23:37:02 -05:00
|
|
|
var DccLayerNames = []string{"HD", "TR", "LG", "RA", "LA", "RH", "LH", "SH", "S1", "S2", "S3", "S4", "S5", "S6", "S7", "S8"}
|
|
|
|
|
2019-11-06 22:12:15 -05:00
|
|
|
// AnimatedEntity represents an entity on the map that can be animated
|
2019-11-06 18:25:19 -05:00
|
|
|
type AnimatedEntity struct {
|
2019-11-14 22:20:01 -05:00
|
|
|
fileProvider d2interface.FileProvider
|
2019-11-06 22:12:15 -05:00
|
|
|
// LocationX represents the tile X position of the entity
|
|
|
|
LocationX float64
|
|
|
|
// LocationY represents the tile Y position of the entity
|
2019-11-13 20:31:47 -05:00
|
|
|
subcellX, subcellY float64 // Subcell coordinates within the current tile
|
|
|
|
LocationY float64
|
|
|
|
dccLayers map[string]d2dcc.DCC
|
|
|
|
Cof *d2cof.COF
|
|
|
|
palette d2enum.PaletteType
|
|
|
|
base string
|
|
|
|
token string
|
|
|
|
animationMode string
|
|
|
|
weaponClass string
|
2019-11-15 13:40:26 -05:00
|
|
|
lastFrameTime float64
|
2019-11-13 20:31:47 -05:00
|
|
|
framesToAnimate int
|
2019-11-15 13:40:26 -05:00
|
|
|
animationSpeed float64
|
2019-11-13 20:31:47 -05:00
|
|
|
direction int
|
|
|
|
currentFrame int
|
|
|
|
frames map[string][]*ebiten.Image
|
|
|
|
frameLocations map[string][]d2common.Rectangle
|
|
|
|
object *d2datadict.ObjectLookupRecord
|
2019-11-06 18:25:19 -05:00
|
|
|
}
|
|
|
|
|
2019-11-06 22:12:15 -05:00
|
|
|
// CreateAnimatedEntity creates an instance of AnimatedEntity
|
2019-11-11 23:48:55 -05:00
|
|
|
func CreateAnimatedEntity(x, y int32, object *d2datadict.ObjectLookupRecord, fileProvider d2interface.FileProvider, palette d2enum.PaletteType) AnimatedEntity {
|
2019-11-10 10:44:13 -05:00
|
|
|
result := AnimatedEntity{
|
2019-11-14 22:20:01 -05:00
|
|
|
fileProvider: fileProvider,
|
|
|
|
base: object.Base,
|
|
|
|
token: object.Token,
|
|
|
|
object: object,
|
|
|
|
palette: palette,
|
2019-11-06 18:25:19 -05:00
|
|
|
}
|
2019-11-10 10:44:13 -05:00
|
|
|
result.dccLayers = make(map[string]d2dcc.DCC)
|
2019-11-11 23:48:55 -05:00
|
|
|
result.LocationX = float64(x) / 5
|
|
|
|
result.LocationY = float64(y) / 5
|
2019-11-13 20:31:47 -05:00
|
|
|
|
|
|
|
result.subcellX = 1 + math.Mod(float64(x), 5)
|
|
|
|
result.subcellY = 1 + math.Mod(float64(y), 5)
|
|
|
|
|
2019-11-06 18:25:19 -05:00
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
2019-11-06 22:12:15 -05:00
|
|
|
// DirectionLookup is used to decode the direction offset indexes
|
2019-11-17 02:54:22 -05:00
|
|
|
var DirectionLookup = []int{9, 15, 5, 6, 4, 12, 10, 2, 8, 13, 1, 7, 0, 14, 11, 3}
|
2019-11-17 01:14:58 -05:00
|
|
|
|
|
|
|
func (v AnimatedEntity) GetDirection() int {
|
|
|
|
return v.direction
|
|
|
|
}
|
2019-11-06 18:25:19 -05:00
|
|
|
|
2019-11-06 22:12:15 -05:00
|
|
|
// SetMode changes the graphical mode of this animated entity
|
2019-11-14 22:20:01 -05:00
|
|
|
func (v *AnimatedEntity) SetMode(animationMode, weaponClass string, direction int) {
|
2019-11-10 10:44:13 -05:00
|
|
|
cofPath := fmt.Sprintf("%s/%s/COF/%s%s%s.COF", v.base, v.token, v.token, animationMode, weaponClass)
|
2019-11-14 22:20:01 -05:00
|
|
|
v.Cof = d2cof.LoadCOF(cofPath, v.fileProvider)
|
2019-11-06 18:25:19 -05:00
|
|
|
v.animationMode = animationMode
|
|
|
|
v.weaponClass = weaponClass
|
|
|
|
v.direction = direction
|
2019-11-09 23:37:02 -05:00
|
|
|
if v.direction >= v.Cof.NumberOfDirections {
|
|
|
|
v.direction = v.Cof.NumberOfDirections - 1
|
|
|
|
}
|
|
|
|
v.frames = make(map[string][]*ebiten.Image)
|
2019-11-10 03:36:53 -05:00
|
|
|
v.frameLocations = make(map[string][]d2common.Rectangle)
|
2019-11-10 10:44:13 -05:00
|
|
|
v.dccLayers = make(map[string]d2dcc.DCC)
|
2019-11-09 23:37:02 -05:00
|
|
|
for _, cofLayer := range v.Cof.CofLayers {
|
|
|
|
layerName := DccLayerNames[cofLayer.Type]
|
2019-11-14 22:20:01 -05:00
|
|
|
v.dccLayers[layerName] = v.LoadLayer(layerName, v.fileProvider)
|
2019-11-10 10:44:13 -05:00
|
|
|
if !v.dccLayers[layerName].IsValid() {
|
2019-11-09 23:37:02 -05:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
v.cacheFrames(layerName)
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2019-11-10 10:44:13 -05:00
|
|
|
func (v *AnimatedEntity) LoadLayer(layer string, fileProvider d2interface.FileProvider) d2dcc.DCC {
|
2019-11-14 22:20:01 -05:00
|
|
|
layerName := "TR"
|
2019-11-09 23:37:02 -05:00
|
|
|
switch strings.ToUpper(layer) {
|
|
|
|
case "HD": // Head
|
2019-11-11 23:48:55 -05:00
|
|
|
layerName = v.object.HD
|
2019-11-09 23:37:02 -05:00
|
|
|
case "TR": // Torso
|
2019-11-11 23:48:55 -05:00
|
|
|
layerName = v.object.TR
|
2019-11-09 23:37:02 -05:00
|
|
|
case "LG": // Legs
|
2019-11-11 23:48:55 -05:00
|
|
|
layerName = v.object.LG
|
2019-11-09 23:37:02 -05:00
|
|
|
case "RA": // RightArm
|
2019-11-11 23:48:55 -05:00
|
|
|
layerName = v.object.RA
|
2019-11-09 23:37:02 -05:00
|
|
|
case "LA": // LeftArm
|
2019-11-11 23:48:55 -05:00
|
|
|
layerName = v.object.LA
|
2019-11-09 23:37:02 -05:00
|
|
|
case "RH": // RightHand
|
2019-11-11 23:48:55 -05:00
|
|
|
layerName = v.object.RH
|
2019-11-09 23:37:02 -05:00
|
|
|
case "LH": // LeftHand
|
2019-11-11 23:48:55 -05:00
|
|
|
layerName = v.object.LH
|
2019-11-09 23:37:02 -05:00
|
|
|
case "SH": // Shield
|
2019-11-11 23:48:55 -05:00
|
|
|
layerName = v.object.SH
|
2019-11-09 23:37:02 -05:00
|
|
|
case "S1": // Special1
|
2019-11-11 23:48:55 -05:00
|
|
|
layerName = v.object.S1
|
2019-11-09 23:37:02 -05:00
|
|
|
case "S2": // Special2
|
2019-11-11 23:48:55 -05:00
|
|
|
layerName = v.object.S2
|
2019-11-09 23:37:02 -05:00
|
|
|
case "S3": // Special3
|
2019-11-11 23:48:55 -05:00
|
|
|
layerName = v.object.S3
|
2019-11-09 23:37:02 -05:00
|
|
|
case "S4": // Special4
|
2019-11-11 23:48:55 -05:00
|
|
|
layerName = v.object.S4
|
2019-11-09 23:37:02 -05:00
|
|
|
case "S5": // Special5
|
2019-11-11 23:48:55 -05:00
|
|
|
layerName = v.object.S5
|
2019-11-09 23:37:02 -05:00
|
|
|
case "S6": // Special6
|
2019-11-11 23:48:55 -05:00
|
|
|
layerName = v.object.S6
|
2019-11-09 23:37:02 -05:00
|
|
|
case "S7": // Special7
|
2019-11-11 23:48:55 -05:00
|
|
|
layerName = v.object.S7
|
2019-11-09 23:37:02 -05:00
|
|
|
case "S8": // Special8
|
2019-11-11 23:48:55 -05:00
|
|
|
layerName = v.object.S8
|
2019-11-09 23:37:02 -05:00
|
|
|
}
|
|
|
|
if len(layerName) == 0 {
|
2019-11-10 10:44:13 -05:00
|
|
|
return d2dcc.DCC{}
|
2019-11-09 23:37:02 -05:00
|
|
|
}
|
|
|
|
dccPath := fmt.Sprintf("%s/%s/%s/%s%s%s%s%s.dcc", v.base, v.token, layer, v.token, layer, layerName, v.animationMode, v.weaponClass)
|
2019-11-14 22:20:01 -05:00
|
|
|
result := d2dcc.LoadDCC(dccPath, fileProvider)
|
|
|
|
if !result.IsValid() {
|
|
|
|
dccPath = fmt.Sprintf("%s/%s/%s/%s%s%s%s%s.dcc", v.base, v.token, layer, v.token, layer, layerName, v.animationMode, "HTH")
|
|
|
|
result = d2dcc.LoadDCC(dccPath, fileProvider)
|
|
|
|
}
|
|
|
|
return result
|
2019-11-06 18:25:19 -05:00
|
|
|
}
|
|
|
|
|
2019-11-06 22:12:15 -05:00
|
|
|
// Render draws this animated entity onto the target
|
2019-11-06 18:25:19 -05:00
|
|
|
func (v *AnimatedEntity) Render(target *ebiten.Image, offsetX, offsetY int) {
|
2019-11-13 15:08:09 -05:00
|
|
|
if v.animationSpeed > 0 {
|
2019-11-15 13:40:26 -05:00
|
|
|
now := d2helper.Now()
|
|
|
|
framesToAdd := math.Floor((now - v.lastFrameTime) / v.animationSpeed)
|
|
|
|
if framesToAdd > 0 {
|
|
|
|
v.lastFrameTime += v.animationSpeed * framesToAdd
|
|
|
|
v.currentFrame += int(math.Floor(framesToAdd))
|
|
|
|
for v.currentFrame >= v.framesToAnimate {
|
|
|
|
v.currentFrame -= v.framesToAnimate
|
2019-11-13 15:08:09 -05:00
|
|
|
}
|
2019-11-06 18:25:19 -05:00
|
|
|
}
|
|
|
|
}
|
2019-11-09 23:37:02 -05:00
|
|
|
for idx := 0; idx < v.Cof.NumberOfLayers; idx++ {
|
|
|
|
priority := v.Cof.Priority[v.direction][v.currentFrame][idx]
|
|
|
|
if int(priority) >= len(DccLayerNames) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
frameName := DccLayerNames[priority]
|
|
|
|
if v.frames[frameName] == nil {
|
|
|
|
continue
|
|
|
|
}
|
2019-11-13 20:31:47 -05:00
|
|
|
|
|
|
|
// Location within the current tile
|
2019-11-15 13:40:26 -05:00
|
|
|
localX := (v.subcellX - v.subcellY) * 16
|
2019-11-15 19:48:49 -05:00
|
|
|
localY := ((v.subcellX + v.subcellY) * 8) - 5
|
2019-11-13 20:31:47 -05:00
|
|
|
|
2019-11-09 23:37:02 -05:00
|
|
|
// TODO: Transparency op maybe, but it'l murder batch calls
|
|
|
|
opts := &ebiten.DrawImageOptions{}
|
2019-11-13 20:31:47 -05:00
|
|
|
opts.GeoM.Translate(float64(v.frameLocations[frameName][v.currentFrame].Left+offsetX)+localX,
|
|
|
|
float64(v.frameLocations[frameName][v.currentFrame].Top+offsetY)+localY)
|
2019-11-10 12:28:41 -05:00
|
|
|
if err := target.DrawImage(v.frames[frameName][v.currentFrame], opts); err != nil {
|
|
|
|
log.Panic(err.Error())
|
|
|
|
}
|
2019-11-09 23:37:02 -05:00
|
|
|
}
|
2019-11-06 18:25:19 -05:00
|
|
|
}
|
|
|
|
|
2019-11-09 23:37:02 -05:00
|
|
|
func (v *AnimatedEntity) cacheFrames(layerName string) {
|
|
|
|
dcc := v.dccLayers[layerName]
|
2019-11-07 23:44:03 -05:00
|
|
|
v.currentFrame = 0
|
2019-11-10 03:36:53 -05:00
|
|
|
animationData := d2data.AnimationData[strings.ToLower(v.token+v.animationMode+v.weaponClass)][0]
|
2019-11-15 13:40:26 -05:00
|
|
|
v.animationSpeed = 1.0 / ((float64(animationData.AnimationSpeed) * 25.0) / 256.0)
|
2019-11-06 18:25:19 -05:00
|
|
|
v.framesToAnimate = animationData.FramesPerDirection
|
2019-11-15 13:40:26 -05:00
|
|
|
v.lastFrameTime = d2helper.Now()
|
2019-11-09 23:37:02 -05:00
|
|
|
minX := int32(10000)
|
|
|
|
minY := int32(10000)
|
|
|
|
maxX := int32(-10000)
|
|
|
|
maxY := int32(-10000)
|
|
|
|
for _, layer := range dcc.Directions {
|
2019-11-10 03:36:53 -05:00
|
|
|
minX = d2helper.MinInt32(minX, int32(layer.Box.Left))
|
|
|
|
minY = d2helper.MinInt32(minY, int32(layer.Box.Top))
|
|
|
|
maxX = d2helper.MaxInt32(maxX, int32(layer.Box.Right()))
|
|
|
|
maxY = d2helper.MaxInt32(maxY, int32(layer.Box.Bottom()))
|
2019-11-06 18:25:19 -05:00
|
|
|
}
|
|
|
|
frameW := maxX - minX
|
|
|
|
frameH := maxY - minY
|
2019-11-09 23:37:02 -05:00
|
|
|
v.frames[layerName] = make([]*ebiten.Image, v.framesToAnimate)
|
2019-11-10 03:36:53 -05:00
|
|
|
v.frameLocations[layerName] = make([]d2common.Rectangle, v.framesToAnimate)
|
2019-11-09 23:37:02 -05:00
|
|
|
for frameIndex := range v.frames[layerName] {
|
|
|
|
v.frames[layerName][frameIndex], _ = ebiten.NewImage(int(frameW), int(frameH), ebiten.FilterNearest)
|
|
|
|
for layerIdx := 0; layerIdx < v.Cof.NumberOfLayers; layerIdx++ {
|
|
|
|
transparency := byte(255)
|
|
|
|
if v.Cof.CofLayers[layerIdx].Transparent {
|
|
|
|
transparency = byte(128)
|
2019-11-06 18:25:19 -05:00
|
|
|
}
|
2019-11-09 23:37:02 -05:00
|
|
|
|
|
|
|
direction := dcc.Directions[v.direction]
|
2019-11-17 00:52:13 -05:00
|
|
|
if frameIndex >= len(direction.Frames) {
|
|
|
|
continue
|
|
|
|
}
|
2019-11-06 18:25:19 -05:00
|
|
|
frame := direction.Frames[frameIndex]
|
2019-11-08 11:05:51 -05:00
|
|
|
img := image.NewRGBA(image.Rect(0, 0, int(frameW), int(frameH)))
|
2019-11-06 18:25:19 -05:00
|
|
|
for y := 0; y < direction.Box.Height; y++ {
|
|
|
|
for x := 0; x < direction.Box.Width; x++ {
|
|
|
|
paletteIndex := frame.PixelData[x+(y*direction.Box.Width)]
|
|
|
|
|
|
|
|
if paletteIndex == 0 {
|
|
|
|
continue
|
|
|
|
}
|
2019-11-10 08:51:02 -05:00
|
|
|
color := d2datadict.Palettes[v.palette].Colors[paletteIndex]
|
2019-11-06 18:25:19 -05:00
|
|
|
actualX := x + direction.Box.Left - int(minX)
|
|
|
|
actualY := y + direction.Box.Top - int(minY)
|
2019-11-08 11:05:51 -05:00
|
|
|
img.Pix[(actualX*4)+(actualY*int(frameW)*4)] = color.R
|
|
|
|
img.Pix[(actualX*4)+(actualY*int(frameW)*4)+1] = color.G
|
|
|
|
img.Pix[(actualX*4)+(actualY*int(frameW)*4)+2] = color.B
|
2019-11-09 23:37:02 -05:00
|
|
|
img.Pix[(actualX*4)+(actualY*int(frameW)*4)+3] = transparency
|
2019-11-06 18:25:19 -05:00
|
|
|
}
|
|
|
|
}
|
2019-11-08 11:05:51 -05:00
|
|
|
newImage, _ := ebiten.NewImageFromImage(img, ebiten.FilterNearest)
|
|
|
|
img = nil
|
2019-11-09 23:37:02 -05:00
|
|
|
v.frames[layerName][frameIndex] = newImage
|
2019-11-10 03:36:53 -05:00
|
|
|
v.frameLocations[layerName][frameIndex] = d2common.Rectangle{
|
2019-11-06 18:25:19 -05:00
|
|
|
Left: int(minX),
|
|
|
|
Top: int(minY),
|
|
|
|
Width: int(frameW),
|
|
|
|
Height: int(frameH),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|