2019-11-10 03:36:53 -05:00
|
|
|
package d2render
|
2019-10-24 09:31:59 -04:00
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/binary"
|
2019-11-08 11:05:51 -05:00
|
|
|
"image"
|
2019-10-24 19:13:30 -04:00
|
|
|
"image/color"
|
2019-11-08 11:05:51 -05:00
|
|
|
"sync"
|
2019-10-24 09:31:59 -04:00
|
|
|
"time"
|
|
|
|
|
2019-11-10 03:36:53 -05:00
|
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2helper"
|
|
|
|
|
2019-11-10 08:51:02 -05:00
|
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2data/d2datadict"
|
2019-11-10 03:36:53 -05:00
|
|
|
|
2019-10-24 09:31:59 -04:00
|
|
|
"github.com/hajimehoshi/ebiten"
|
|
|
|
)
|
|
|
|
|
2019-10-25 18:40:27 -04:00
|
|
|
// Sprite represents a type of object in D2 that is comprised of one or more frames and directions
|
2019-10-24 09:31:59 -04:00
|
|
|
type Sprite struct {
|
|
|
|
Directions uint32
|
|
|
|
FramesPerDirection uint32
|
2019-11-09 01:13:49 -05:00
|
|
|
atlas *ebiten.Image
|
|
|
|
atlasBytes []byte
|
2019-11-10 10:44:13 -05:00
|
|
|
Frames []SpriteFrame
|
2019-10-27 02:58:37 -04:00
|
|
|
SpecialFrameTime int
|
|
|
|
StopOnLastFrame bool
|
2019-10-24 09:31:59 -04:00
|
|
|
X, Y int
|
|
|
|
Frame, Direction uint8
|
|
|
|
Blend bool
|
|
|
|
LastFrameTime time.Time
|
|
|
|
Animate bool
|
2019-10-24 19:13:30 -04:00
|
|
|
ColorMod color.Color
|
2019-10-25 18:40:27 -04:00
|
|
|
visible bool
|
2019-11-10 10:44:13 -05:00
|
|
|
valid bool
|
2019-10-24 09:31:59 -04:00
|
|
|
}
|
|
|
|
|
2019-10-25 18:40:27 -04:00
|
|
|
// SpriteFrame represents a single frame of a sprite
|
2019-10-24 09:31:59 -04:00
|
|
|
type SpriteFrame struct {
|
|
|
|
Flip uint32
|
|
|
|
Width uint32
|
|
|
|
Height uint32
|
|
|
|
OffsetX int32
|
|
|
|
OffsetY int32
|
|
|
|
Unknown uint32
|
|
|
|
NextBlock uint32
|
|
|
|
Length uint32
|
|
|
|
ImageData []int16
|
2019-11-08 16:50:33 -05:00
|
|
|
FrameData []byte
|
2019-10-24 09:31:59 -04:00
|
|
|
Image *ebiten.Image
|
2019-11-09 01:13:49 -05:00
|
|
|
cached bool
|
2019-10-24 09:31:59 -04:00
|
|
|
}
|
|
|
|
|
2019-10-25 18:40:27 -04:00
|
|
|
// CreateSprite creates an instance of a sprite
|
2019-11-10 10:44:13 -05:00
|
|
|
func CreateSprite(data []byte, palette d2datadict.PaletteRec) Sprite {
|
|
|
|
result := Sprite{
|
2019-10-24 09:31:59 -04:00
|
|
|
X: 50,
|
|
|
|
Y: 50,
|
|
|
|
Frame: 0,
|
|
|
|
Direction: 0,
|
|
|
|
Blend: false,
|
2019-10-24 19:13:30 -04:00
|
|
|
ColorMod: nil,
|
2019-10-24 09:31:59 -04:00
|
|
|
Directions: binary.LittleEndian.Uint32(data[16:20]),
|
|
|
|
FramesPerDirection: binary.LittleEndian.Uint32(data[20:24]),
|
|
|
|
Animate: false,
|
|
|
|
LastFrameTime: time.Now(),
|
2019-10-27 02:58:37 -04:00
|
|
|
SpecialFrameTime: -1,
|
|
|
|
StopOnLastFrame: false,
|
2019-11-10 10:44:13 -05:00
|
|
|
valid: false,
|
2019-10-24 09:31:59 -04:00
|
|
|
}
|
|
|
|
dataPointer := uint32(24)
|
|
|
|
totalFrames := result.Directions * result.FramesPerDirection
|
|
|
|
framePointers := make([]uint32, totalFrames)
|
|
|
|
for i := uint32(0); i < totalFrames; i++ {
|
|
|
|
framePointers[i] = binary.LittleEndian.Uint32(data[dataPointer : dataPointer+4])
|
|
|
|
dataPointer += 4
|
|
|
|
}
|
2019-11-10 10:44:13 -05:00
|
|
|
result.Frames = make([]SpriteFrame, totalFrames)
|
2019-11-09 01:13:49 -05:00
|
|
|
wg := sync.WaitGroup{}
|
2019-11-08 11:05:51 -05:00
|
|
|
wg.Add(int(totalFrames))
|
2019-10-24 09:31:59 -04:00
|
|
|
for i := uint32(0); i < totalFrames; i++ {
|
2019-11-08 11:05:51 -05:00
|
|
|
go func(i uint32) {
|
|
|
|
defer wg.Done()
|
|
|
|
dataPointer := framePointers[i]
|
2019-11-10 10:44:13 -05:00
|
|
|
result.Frames[i] = SpriteFrame{}
|
2019-11-08 11:05:51 -05:00
|
|
|
result.Frames[i].Flip = binary.LittleEndian.Uint32(data[dataPointer : dataPointer+4])
|
|
|
|
dataPointer += 4
|
|
|
|
result.Frames[i].Width = binary.LittleEndian.Uint32(data[dataPointer : dataPointer+4])
|
|
|
|
dataPointer += 4
|
|
|
|
result.Frames[i].Height = binary.LittleEndian.Uint32(data[dataPointer : dataPointer+4])
|
|
|
|
dataPointer += 4
|
2019-11-10 03:36:53 -05:00
|
|
|
result.Frames[i].OffsetX = d2helper.BytesToInt32(data[dataPointer : dataPointer+4])
|
2019-11-08 11:05:51 -05:00
|
|
|
dataPointer += 4
|
2019-11-10 03:36:53 -05:00
|
|
|
result.Frames[i].OffsetY = d2helper.BytesToInt32(data[dataPointer : dataPointer+4])
|
2019-11-08 11:05:51 -05:00
|
|
|
dataPointer += 4
|
|
|
|
result.Frames[i].Unknown = binary.LittleEndian.Uint32(data[dataPointer : dataPointer+4])
|
|
|
|
dataPointer += 4
|
|
|
|
result.Frames[i].NextBlock = binary.LittleEndian.Uint32(data[dataPointer : dataPointer+4])
|
|
|
|
dataPointer += 4
|
|
|
|
result.Frames[i].Length = binary.LittleEndian.Uint32(data[dataPointer : dataPointer+4])
|
|
|
|
dataPointer += 4
|
|
|
|
result.Frames[i].ImageData = make([]int16, result.Frames[i].Width*result.Frames[i].Height)
|
|
|
|
for fi := range result.Frames[i].ImageData {
|
|
|
|
result.Frames[i].ImageData[fi] = -1
|
|
|
|
}
|
2019-10-24 09:31:59 -04:00
|
|
|
|
2019-11-08 11:05:51 -05:00
|
|
|
x := uint32(0)
|
2019-11-09 01:13:49 -05:00
|
|
|
y := result.Frames[i].Height - 1
|
2019-10-24 19:33:01 -04:00
|
|
|
for true {
|
|
|
|
b := data[dataPointer]
|
|
|
|
dataPointer++
|
|
|
|
if b == 0x80 {
|
|
|
|
if y == 0 {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
y--
|
|
|
|
x = 0
|
|
|
|
} else if (b & 0x80) > 0 {
|
|
|
|
transparentPixels := b & 0x7F
|
|
|
|
for ti := byte(0); ti < transparentPixels; ti++ {
|
2019-11-08 11:05:51 -05:00
|
|
|
result.Frames[i].ImageData[x+(y*result.Frames[i].Width)+uint32(ti)] = -1
|
2019-10-24 19:33:01 -04:00
|
|
|
}
|
|
|
|
x += uint32(transparentPixels)
|
|
|
|
} else {
|
|
|
|
for bi := 0; bi < int(b); bi++ {
|
2019-11-08 11:05:51 -05:00
|
|
|
result.Frames[i].ImageData[x+(y*result.Frames[i].Width)+uint32(bi)] = int16(data[dataPointer])
|
2019-10-24 19:33:01 -04:00
|
|
|
dataPointer++
|
|
|
|
}
|
|
|
|
x += uint32(b)
|
2019-10-24 09:31:59 -04:00
|
|
|
}
|
|
|
|
}
|
2019-11-08 16:50:33 -05:00
|
|
|
result.Frames[i].FrameData = make([]byte, result.Frames[i].Width*result.Frames[i].Height*4)
|
2019-11-08 11:05:51 -05:00
|
|
|
for ii := uint32(0); ii < result.Frames[i].Width*result.Frames[i].Height; ii++ {
|
|
|
|
if result.Frames[i].ImageData[ii] < 1 { // TODO: Is this == -1 or < 1?
|
2019-10-24 19:33:01 -04:00
|
|
|
continue
|
|
|
|
}
|
2019-11-08 16:50:33 -05:00
|
|
|
result.Frames[i].FrameData[ii*4] = palette.Colors[result.Frames[i].ImageData[ii]].R
|
|
|
|
result.Frames[i].FrameData[(ii*4)+1] = palette.Colors[result.Frames[i].ImageData[ii]].G
|
|
|
|
result.Frames[i].FrameData[(ii*4)+2] = palette.Colors[result.Frames[i].ImageData[ii]].B
|
|
|
|
result.Frames[i].FrameData[(ii*4)+3] = 0xFF
|
2019-10-24 09:31:59 -04:00
|
|
|
}
|
2019-11-08 16:50:33 -05:00
|
|
|
//newImage, _ := ebiten.NewImageFromImage(img, ebiten.FilterNearest)
|
|
|
|
//result.Frames[i].Image = newImage
|
|
|
|
//img = nil
|
2019-11-08 11:05:51 -05:00
|
|
|
}(i)
|
2019-10-24 09:31:59 -04:00
|
|
|
}
|
2019-11-08 11:05:51 -05:00
|
|
|
wg.Wait()
|
2019-11-08 16:50:33 -05:00
|
|
|
totalWidth := 0
|
|
|
|
totalHeight := 0
|
|
|
|
frame := 0
|
|
|
|
for d := 0; d < int(result.Directions); d++ {
|
2019-11-09 23:33:09 -05:00
|
|
|
curMaxWidth := 0
|
2019-11-08 16:50:33 -05:00
|
|
|
for f := 0; f < int(result.FramesPerDirection); f++ {
|
2019-11-10 03:36:53 -05:00
|
|
|
curMaxWidth = int(d2helper.Max(uint32(curMaxWidth), result.Frames[frame].Width))
|
2019-11-09 23:33:09 -05:00
|
|
|
totalHeight += int(result.Frames[frame].Height)
|
2019-11-08 16:50:33 -05:00
|
|
|
frame++
|
|
|
|
}
|
2019-11-09 23:33:09 -05:00
|
|
|
totalWidth += curMaxWidth
|
2019-11-08 16:50:33 -05:00
|
|
|
}
|
2019-11-09 01:13:49 -05:00
|
|
|
result.atlas, _ = ebiten.NewImage(totalWidth, totalHeight, ebiten.FilterNearest)
|
|
|
|
result.atlasBytes = make([]byte, totalWidth*totalHeight*4)
|
2019-11-08 16:50:33 -05:00
|
|
|
frame = 0
|
|
|
|
curX := 0
|
|
|
|
curY := 0
|
|
|
|
for d := 0; d < int(result.Directions); d++ {
|
2019-11-09 23:33:09 -05:00
|
|
|
curMaxWidth := 0
|
2019-11-08 16:50:33 -05:00
|
|
|
for f := 0; f < int(result.FramesPerDirection); f++ {
|
2019-11-10 03:36:53 -05:00
|
|
|
curMaxWidth = int(d2helper.Max(uint32(curMaxWidth), result.Frames[frame].Width))
|
2019-11-09 01:13:49 -05:00
|
|
|
result.Frames[frame].Image = result.atlas.SubImage(image.Rect(curX, curY, curX+int(result.Frames[frame].Width), curY+int(result.Frames[frame].Height))).(*ebiten.Image)
|
2019-11-09 23:33:09 -05:00
|
|
|
curY += int(result.Frames[frame].Height)
|
2019-11-08 16:50:33 -05:00
|
|
|
frame++
|
|
|
|
}
|
2019-11-09 23:33:09 -05:00
|
|
|
curX += curMaxWidth
|
|
|
|
curY = 0
|
2019-11-08 16:50:33 -05:00
|
|
|
}
|
2019-11-10 10:44:13 -05:00
|
|
|
result.valid = true
|
2019-10-24 09:31:59 -04:00
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
2019-11-10 10:44:13 -05:00
|
|
|
func (v Sprite) IsValid() bool {
|
|
|
|
return v.valid
|
|
|
|
}
|
|
|
|
|
2019-11-09 01:13:49 -05:00
|
|
|
func (v *Sprite) cacheFrame(frame int) {
|
|
|
|
if v.Frames[frame].cached {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
r := v.Frames[frame].Image.Bounds().Min
|
|
|
|
curX := r.X
|
|
|
|
curY := r.Y
|
|
|
|
totalWidth := v.atlas.Bounds().Max.X
|
|
|
|
for y := 0; y < int(v.Frames[frame].Height); y++ {
|
|
|
|
for x := 0; x < int(v.Frames[frame].Width); x++ {
|
|
|
|
pix := (x + (y * int(v.Frames[frame].Width))) * 4
|
|
|
|
idx := (curX + x + ((curY + y) * totalWidth)) * 4
|
|
|
|
v.atlasBytes[idx] = v.Frames[frame].FrameData[pix]
|
|
|
|
v.atlasBytes[idx+1] = v.Frames[frame].FrameData[pix+1]
|
|
|
|
v.atlasBytes[idx+2] = v.Frames[frame].FrameData[pix+2]
|
|
|
|
v.atlasBytes[idx+3] = v.Frames[frame].FrameData[pix+3]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
v.atlas.ReplacePixels(v.atlasBytes)
|
|
|
|
v.Frames[frame].cached = true
|
|
|
|
}
|
|
|
|
|
2019-10-25 18:40:27 -04:00
|
|
|
// GetSize returns the size of the sprite
|
2019-11-10 10:44:13 -05:00
|
|
|
func (v Sprite) GetSize() (uint32, uint32) {
|
2019-10-24 09:31:59 -04:00
|
|
|
frame := v.Frames[uint32(v.Frame)+(uint32(v.Direction)*v.FramesPerDirection)]
|
|
|
|
return frame.Width, frame.Height
|
|
|
|
}
|
|
|
|
|
|
|
|
func (v *Sprite) updateAnimation() {
|
|
|
|
if !v.Animate {
|
|
|
|
return
|
|
|
|
}
|
2019-10-27 02:58:37 -04:00
|
|
|
var timePerFrame time.Duration
|
|
|
|
|
|
|
|
if v.SpecialFrameTime >= 0 {
|
|
|
|
timePerFrame = time.Duration(float64(time.Millisecond) * (float64(v.SpecialFrameTime) / float64(len(v.Frames))))
|
|
|
|
} else {
|
|
|
|
timePerFrame = time.Duration(float64(time.Second) * (1.0 / float64(len(v.Frames))))
|
2019-10-24 09:31:59 -04:00
|
|
|
}
|
2019-10-27 02:58:37 -04:00
|
|
|
for time.Now().Sub(v.LastFrameTime) >= timePerFrame {
|
|
|
|
v.LastFrameTime = v.LastFrameTime.Add(timePerFrame)
|
|
|
|
v.Frame++
|
|
|
|
if v.Frame >= uint8(v.FramesPerDirection) {
|
|
|
|
if v.StopOnLastFrame {
|
|
|
|
v.Frame = uint8(v.FramesPerDirection) - 1
|
|
|
|
} else {
|
|
|
|
v.Frame = 0
|
|
|
|
}
|
|
|
|
}
|
2019-10-24 09:31:59 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-10-27 02:58:37 -04:00
|
|
|
func (v *Sprite) ResetAnimation() {
|
|
|
|
v.LastFrameTime = time.Now()
|
|
|
|
v.Frame = 0
|
|
|
|
}
|
|
|
|
|
2019-11-10 10:44:13 -05:00
|
|
|
func (v Sprite) OnLastFrame() bool {
|
2019-10-27 02:58:37 -04:00
|
|
|
return v.Frame == uint8(v.FramesPerDirection-1)
|
|
|
|
}
|
|
|
|
|
2019-10-25 23:41:54 -04:00
|
|
|
// GetFrameSize returns the size of the specific frame
|
2019-11-10 10:44:13 -05:00
|
|
|
func (v Sprite) GetFrameSize(frame int) (width, height uint32) {
|
2019-10-25 23:41:54 -04:00
|
|
|
width = v.Frames[frame].Width
|
|
|
|
height = v.Frames[frame].Height
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetTotalFrames returns the number of frames in this sprite (for all directions)
|
2019-11-10 10:44:13 -05:00
|
|
|
func (v Sprite) GetTotalFrames() int {
|
2019-10-25 23:41:54 -04:00
|
|
|
return len(v.Frames)
|
|
|
|
}
|
|
|
|
|
2019-10-24 09:31:59 -04:00
|
|
|
// Draw draws the sprite onto the target
|
|
|
|
func (v *Sprite) Draw(target *ebiten.Image) {
|
|
|
|
v.updateAnimation()
|
|
|
|
opts := &ebiten.DrawImageOptions{}
|
|
|
|
frame := v.Frames[uint32(v.Frame)+(uint32(v.Direction)*v.FramesPerDirection)]
|
2019-11-09 01:13:49 -05:00
|
|
|
if !frame.cached {
|
|
|
|
v.cacheFrame(int(v.Frame) + (int(v.Direction) * int(v.FramesPerDirection)))
|
|
|
|
}
|
2019-10-24 09:31:59 -04:00
|
|
|
opts.GeoM.Translate(
|
|
|
|
float64(int32(v.X)+frame.OffsetX),
|
|
|
|
float64((int32(v.Y) - int32(frame.Height) + frame.OffsetY)),
|
|
|
|
)
|
|
|
|
if v.Blend {
|
2019-10-26 00:06:54 -04:00
|
|
|
opts.CompositeMode = ebiten.CompositeModeLighter
|
2019-10-25 22:20:36 -04:00
|
|
|
} else {
|
|
|
|
opts.CompositeMode = ebiten.CompositeModeSourceOver
|
2019-10-24 09:31:59 -04:00
|
|
|
}
|
2019-10-24 19:13:30 -04:00
|
|
|
if v.ColorMod != nil {
|
2019-11-10 03:36:53 -05:00
|
|
|
opts.ColorM = d2helper.ColorToColorM(v.ColorMod)
|
2019-10-24 19:13:30 -04:00
|
|
|
}
|
2019-10-24 09:31:59 -04:00
|
|
|
target.DrawImage(frame.Image, opts)
|
|
|
|
}
|
|
|
|
|
2019-10-25 18:40:27 -04:00
|
|
|
// DrawSegments draws the sprite via a grid of segments
|
2019-10-24 09:31:59 -04:00
|
|
|
func (v *Sprite) DrawSegments(target *ebiten.Image, xSegments, ySegments, offset int) {
|
|
|
|
v.updateAnimation()
|
|
|
|
yOffset := int32(0)
|
|
|
|
for y := 0; y < ySegments; y++ {
|
|
|
|
xOffset := int32(0)
|
|
|
|
biggestYOffset := int32(0)
|
|
|
|
for x := 0; x < xSegments; x++ {
|
|
|
|
frame := v.Frames[uint32(x+(y*xSegments)+(offset*xSegments*ySegments))]
|
2019-11-09 01:13:49 -05:00
|
|
|
if !frame.cached {
|
|
|
|
v.cacheFrame(x + (y * xSegments) + (offset * xSegments * ySegments))
|
|
|
|
}
|
2019-10-24 09:31:59 -04:00
|
|
|
opts := &ebiten.DrawImageOptions{}
|
|
|
|
opts.GeoM.Translate(
|
|
|
|
float64(int32(v.X)+frame.OffsetX+xOffset),
|
|
|
|
float64(int32(v.Y)+frame.OffsetY+yOffset),
|
|
|
|
)
|
2019-10-24 19:13:30 -04:00
|
|
|
if v.Blend {
|
|
|
|
opts.CompositeMode = ebiten.CompositeModeLighter
|
2019-10-25 22:20:36 -04:00
|
|
|
} else {
|
|
|
|
opts.CompositeMode = ebiten.CompositeModeSourceOver
|
2019-10-24 19:13:30 -04:00
|
|
|
}
|
|
|
|
if v.ColorMod != nil {
|
2019-11-10 03:36:53 -05:00
|
|
|
opts.ColorM = d2helper.ColorToColorM(v.ColorMod)
|
2019-10-24 19:13:30 -04:00
|
|
|
}
|
2019-10-24 09:31:59 -04:00
|
|
|
target.DrawImage(frame.Image, opts)
|
|
|
|
xOffset += int32(frame.Width)
|
2019-11-10 03:36:53 -05:00
|
|
|
biggestYOffset = d2helper.MaxInt32(biggestYOffset, int32(frame.Height))
|
2019-10-24 09:31:59 -04:00
|
|
|
}
|
|
|
|
yOffset += biggestYOffset
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// MoveTo moves the sprite to the specified coordinates
|
|
|
|
func (v *Sprite) MoveTo(x, y int) {
|
|
|
|
v.X = x
|
|
|
|
v.Y = y
|
|
|
|
}
|
2019-10-25 18:40:27 -04:00
|
|
|
|
|
|
|
// GetLocation returns the location of the sprite
|
2019-11-10 10:44:13 -05:00
|
|
|
func (v Sprite) GetLocation() (int, int) {
|
2019-10-25 18:40:27 -04:00
|
|
|
return v.X, v.Y
|
|
|
|
}
|