1
1
mirror of https://github.com/OpenDiablo2/OpenDiablo2 synced 2024-09-27 13:46:00 -04:00

Rename Paperdoll to Composite (#265)

Make AnimatedEntity use Composite
This commit is contained in:
Alex Yatskov 2019-12-23 22:48:45 -08:00 committed by Tim Sarbin
parent 1b03e691b9
commit 0ee937f01b
17 changed files with 513 additions and 681 deletions

View File

@ -68,6 +68,11 @@ The engine is configured via the `config.json` file. By default, the configurati
expansion via the official Blizzard Diablo2 installers using the default file paths. If you are not on Windows, or have installed expansion via the official Blizzard Diablo2 installers using the default file paths. If you are not on Windows, or have installed
the game in a different location, the base path may have to be adjusted. the game in a different location, the base path may have to be adjusted.
## Roadmap
There is an in-progress [project roadmap](https://docs.google.com/document/d/156sWiuk-XBfomVxZ3MD-ijxnwM1X66KTHo2AcWIy8bE/edit?usp=sharing),
which will be updated over time with new requirements.
## Screenshots ## Screenshots
![Main Menu](docs/MainMenuSS.png) ![Main Menu](docs/MainMenuSS.png)

View File

@ -3,8 +3,11 @@ package d2asset
import ( import (
"errors" "errors"
"image/color" "image/color"
"math"
"github.com/OpenDiablo2/D2Shared/d2data/d2datadict"
"github.com/OpenDiablo2/D2Shared/d2data/d2dc6" "github.com/OpenDiablo2/D2Shared/d2data/d2dc6"
"github.com/OpenDiablo2/D2Shared/d2data/d2dcc"
"github.com/OpenDiablo2/D2Shared/d2helper" "github.com/OpenDiablo2/D2Shared/d2helper"
"github.com/OpenDiablo2/OpenDiablo2/d2corehelper" "github.com/OpenDiablo2/OpenDiablo2/d2corehelper"
@ -37,6 +40,7 @@ type Animation struct {
frameIndex int frameIndex int
directionIndex int directionIndex int
lastFrameTime float64 lastFrameTime float64
playedCount int
compositeMode ebiten.CompositeMode compositeMode ebiten.CompositeMode
colorMod color.Color colorMod color.Color
@ -46,19 +50,81 @@ type Animation struct {
playLoop bool playLoop bool
} }
func createAnimationFromDCC(dcc *d2dcc.DCC, palette *d2datadict.PaletteRec, transparency int) (*Animation, error) {
animation := &Animation{
playLength: 1.0,
playLoop: true,
}
for directionIndex, dccDirection := range dcc.Directions {
for _, dccFrame := range dccDirection.Frames {
minX, minY := math.MaxInt32, math.MaxInt32
maxX, maxY := math.MinInt32, math.MinInt32
for _, dccFrame := range dccDirection.Frames {
minX = d2helper.MinInt(minX, dccFrame.Box.Left)
minY = d2helper.MinInt(minY, dccFrame.Box.Top)
maxX = d2helper.MaxInt(maxX, dccFrame.Box.Right())
maxY = d2helper.MaxInt(maxY, dccFrame.Box.Bottom())
}
frameWidth := maxX - minX
frameHeight := maxY - minY
pixels := make([]byte, frameWidth*frameHeight*4)
for y := 0; y < frameHeight; y++ {
for x := 0; x < frameWidth; x++ {
if paletteIndex := dccFrame.PixelData[y*frameWidth+x]; paletteIndex != 0 {
color := palette.Colors[paletteIndex]
offset := (x + y*frameWidth) * 4
pixels[offset] = color.R
pixels[offset+1] = color.G
pixels[offset+2] = color.B
pixels[offset+3] = byte(transparency)
}
}
}
image, err := ebiten.NewImage(frameWidth, frameHeight, ebiten.FilterNearest)
if err != nil {
return nil, err
}
if err := image.ReplacePixels(pixels); err != nil {
return nil, err
}
if directionIndex >= len(animation.directions) {
animation.directions = append(animation.directions, new(animationDirection))
}
direction := animation.directions[directionIndex]
direction.frames = append(direction.frames, &animationFrame{
width: int(dccFrame.Width),
height: int(dccFrame.Height),
offsetX: minX,
offsetY: minY,
image: image,
})
}
}
return animation, nil
}
func createAnimationFromDC6(dc6 *d2dc6.DC6File) (*Animation, error) { func createAnimationFromDC6(dc6 *d2dc6.DC6File) (*Animation, error) {
animation := &Animation{ animation := &Animation{
playLength: 1.0, playLength: 1.0,
playLoop: true, playLoop: true,
} }
for frameIndex, frame := range dc6.Frames { for frameIndex, dc6Frame := range dc6.Frames {
image, err := ebiten.NewImage(int(frame.Width), int(frame.Height), ebiten.FilterNearest) image, err := ebiten.NewImage(int(dc6Frame.Width), int(dc6Frame.Height), ebiten.FilterNearest)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if err := image.ReplacePixels(frame.ColorData()); err != nil { if err := image.ReplacePixels(dc6Frame.ColorData()); err != nil {
return nil, err return nil, err
} }
@ -69,10 +135,10 @@ func createAnimationFromDC6(dc6 *d2dc6.DC6File) (*Animation, error) {
direction := animation.directions[directionIndex] direction := animation.directions[directionIndex]
direction.frames = append(direction.frames, &animationFrame{ direction.frames = append(direction.frames, &animationFrame{
width: int(frame.Width), width: int(dc6Frame.Width),
height: int(frame.Height), height: int(dc6Frame.Height),
offsetX: int(frame.OffsetX), offsetX: int(dc6Frame.OffsetX),
offsetY: int(frame.OffsetY), offsetY: int(dc6Frame.OffsetY),
image: image, image: image,
}) })
} }
@ -101,6 +167,7 @@ func (a *Animation) Advance(elapsed float64) error {
case playModeForward: case playModeForward:
a.frameIndex++ a.frameIndex++
if a.frameIndex >= frameCount { if a.frameIndex >= frameCount {
a.playedCount++
if a.playLoop { if a.playLoop {
a.frameIndex = 0 a.frameIndex = 0
} else { } else {
@ -111,6 +178,7 @@ func (a *Animation) Advance(elapsed float64) error {
case playModeBackward: case playModeBackward:
a.frameIndex-- a.frameIndex--
if a.frameIndex < 0 { if a.frameIndex < 0 {
a.playedCount++
if a.playLoop { if a.playLoop {
a.frameIndex = frameCount - 1 a.frameIndex = frameCount - 1
} else { } else {
@ -233,6 +301,10 @@ func (a *Animation) SetPlayLoop(loop bool) {
a.playLoop = true a.playLoop = true
} }
func (a *Animation) SetPlaySpeed(playSpeed float64) {
a.SetPlayLength(playSpeed * float64(a.GetFrameCount()))
}
func (a *Animation) SetPlayLength(playLength float64) { func (a *Animation) SetPlayLength(playLength float64) {
a.playLength = playLength a.playLength = playLength
a.lastFrameTime = 0 a.lastFrameTime = 0
@ -246,6 +318,14 @@ func (a *Animation) SetColorMod(color color.Color) {
a.colorMod = color a.colorMod = color
} }
func (a *Animation) GetPlayedCount() int {
return a.playedCount
}
func (a *Animation) ResetPlayedCount() {
a.playedCount = 0
}
func (a *Animation) SetBlend(blend bool) { func (a *Animation) SetBlend(blend bool) {
if blend { if blend {
a.compositeMode = ebiten.CompositeModeLighter a.compositeMode = ebiten.CompositeModeLighter

View File

@ -1,5 +1,12 @@
package d2asset package d2asset
import (
"errors"
"fmt"
"path/filepath"
"strings"
)
type animationManager struct { type animationManager struct {
cache *cache cache *cache
} }
@ -8,22 +15,47 @@ func createAnimationManager() *animationManager {
return &animationManager{cache: createCache(AnimationBudget)} return &animationManager{cache: createCache(AnimationBudget)}
} }
func (sm *animationManager) loadAnimation(animationPath, palettePath string) (*Animation, error) { func (sm *animationManager) loadAnimation(animationPath, palettePath string, transparency int) (*Animation, error) {
cachePath := animationPath + palettePath cachePath := fmt.Sprintf("%s;%s;%d", animationPath, palettePath, transparency)
if animation, found := sm.cache.retrieve(cachePath); found { if animation, found := sm.cache.retrieve(cachePath); found {
return animation.(*Animation).clone(), nil return animation.(*Animation).clone(), nil
} }
dc6, err := loadDC6(animationPath, palettePath) var animation *Animation
if err != nil { switch strings.ToLower(filepath.Ext(animationPath)) {
case ".dc6":
dc6, err := loadDC6(animationPath, palettePath)
if err != nil {
return nil, err
}
animation, err = createAnimationFromDC6(dc6)
if err != nil {
return nil, err
}
case ".dcc":
dcc, err := loadDCC(animationPath)
if err != nil {
return nil, err
}
palette, err := loadPalette(palettePath)
if err != nil {
return nil, err
}
animation, err = createAnimationFromDCC(dcc, palette, transparency)
if err != nil {
return nil, err
}
default:
return nil, errors.New("unknown animation format")
}
if err := sm.cache.insert(cachePath, animation.clone(), 1); err != nil {
return nil, err return nil, err
} }
animation, err := createAnimationFromDC6(dc6) return animation, nil
if err != nil {
return nil, err
}
sm.cache.insert(cachePath, animation.clone(), 1)
return animation, err
} }

View File

@ -18,7 +18,6 @@ const (
// In counts // In counts
PaletteBudget = 64 PaletteBudget = 64
PaperdollBudget = 64
AnimationBudget = 64 AnimationBudget = 64
) )
@ -31,7 +30,6 @@ type assetManager struct {
archiveManager *archiveManager archiveManager *archiveManager
fileManager *fileManager fileManager *fileManager
paletteManager *paletteManager paletteManager *paletteManager
paperdollManager *paperdollManager
animationManager *animationManager animationManager *animationManager
} }
@ -46,7 +44,6 @@ func Initialize(config *d2corecommon.Configuration) error {
archiveManager = createArchiveManager(config) archiveManager = createArchiveManager(config)
fileManager = createFileManager(config, archiveManager) fileManager = createFileManager(config, archiveManager)
paletteManager = createPaletteManager() paletteManager = createPaletteManager()
paperdollManager = createPaperdollManager()
animationManager = createAnimationManager() animationManager = createAnimationManager()
) )
@ -54,7 +51,6 @@ func Initialize(config *d2corecommon.Configuration) error {
archiveManager, archiveManager,
fileManager, fileManager,
paletteManager, paletteManager,
paperdollManager,
animationManager, animationManager,
} }
@ -78,29 +74,19 @@ func LoadFile(filePath string) ([]byte, error) {
} }
func LoadAnimation(animationPath, palettePath string) (*Animation, error) { func LoadAnimation(animationPath, palettePath string) (*Animation, error) {
return LoadAnimationWithTransparency(animationPath, palettePath, 255)
}
func LoadAnimationWithTransparency(animationPath, palettePath string, transparency int) (*Animation, error) {
if singleton == nil { if singleton == nil {
return nil, ErrNoInit return nil, ErrNoInit
} }
return singleton.animationManager.loadAnimation(animationPath, palettePath) return singleton.animationManager.loadAnimation(animationPath, palettePath, transparency)
} }
func LoadPaperdoll(object *d2datadict.ObjectLookupRecord, palettePath string) (*Paperdoll, error) { func LoadComposite(object *d2datadict.ObjectLookupRecord, palettePath string) (*Composite, error) {
if singleton == nil { return createComposite(object, palettePath), nil
return nil, ErrNoInit
}
return singleton.paperdollManager.loadPaperdoll(object, palettePath)
}
// TODO: remove transitional usage pattern
func MustLoadFile(filePath string) []byte {
data, err := LoadFile(filePath)
if err != nil {
return []byte{}
}
return data
} }
func loadPalette(palettePath string) (*d2datadict.PaletteRec, error) { func loadPalette(palettePath string) (*d2datadict.PaletteRec, error) {
@ -130,7 +116,7 @@ func loadDC6(dc6Path, palettePath string) (*d2dc6.DC6File, error) {
return &dc6, nil return &dc6, nil
} }
func LoadDCC(dccPath string) (*d2dcc.DCC, error) { func loadDCC(dccPath string) (*d2dcc.DCC, error) {
dccData, err := LoadFile(dccPath) dccData, err := LoadFile(dccPath)
if err != nil { if err != nil {
return nil, err return nil, err
@ -139,7 +125,7 @@ func LoadDCC(dccPath string) (*d2dcc.DCC, error) {
return d2dcc.LoadDCC(dccData) return d2dcc.LoadDCC(dccData)
} }
func LoadCOF(cofPath string) (*d2cof.COF, error) { func loadCOF(cofPath string) (*d2cof.COF, error) {
cofData, err := LoadFile(cofPath) cofData, err := LoadFile(cofPath)
if err != nil { if err != nil {
return nil, err return nil, err

259
d2asset/composite.go Normal file
View File

@ -0,0 +1,259 @@
package d2asset
import (
"errors"
"fmt"
"strings"
"github.com/OpenDiablo2/D2Shared/d2common/d2enum"
"github.com/OpenDiablo2/D2Shared/d2data"
"github.com/OpenDiablo2/D2Shared/d2data/d2datadict"
"github.com/OpenDiablo2/D2Shared/d2data/d2dcc"
"github.com/hajimehoshi/ebiten"
)
type Composite struct {
object *d2datadict.ObjectLookupRecord
palettePath string
mode *compositeMode
}
func createComposite(object *d2datadict.ObjectLookupRecord, palettePath string) *Composite {
return &Composite{object: object, palettePath: palettePath}
}
func (c *Composite) Advance(elapsed float64) error {
if c.mode == nil {
return nil
}
c.mode.lastFrameTime += elapsed
framesToAdd := int(c.mode.lastFrameTime / c.mode.animationSpeed)
c.mode.lastFrameTime -= float64(framesToAdd) * c.mode.animationSpeed
c.mode.frameIndex += framesToAdd
c.mode.playedCount += c.mode.frameIndex / c.mode.frameCount
c.mode.frameIndex %= c.mode.frameCount
for _, layer := range c.mode.layers {
if layer != nil {
if err := layer.Advance(elapsed); err != nil {
return err
}
}
}
return nil
}
func (c *Composite) Render(target *ebiten.Image, offsetX, offsetY int) error {
if c.mode == nil {
return nil
}
for _, layerIndex := range c.mode.drawOrder[c.mode.frameIndex] {
layer := c.mode.layers[layerIndex]
if layer != nil {
if err := layer.Render(target, offsetX, offsetY); err != nil {
return err
}
}
}
return nil
}
func (c *Composite) SetMode(animationMode, weaponClass string, direction int) error {
if c.mode != nil && c.mode.animationMode == animationMode && c.mode.weaponClass == weaponClass && c.mode.direction == direction {
return nil
}
mode, err := c.createMode(animationMode, weaponClass, direction)
if err != nil {
return err
}
c.mode = mode
return nil
}
func (c *Composite) GetDirectionCount() int {
if c.mode == nil {
return 0
}
return c.mode.directionCount
}
func (c *Composite) GetPlayedCount() int {
if c.mode == nil {
return 0
}
return c.mode.playedCount
}
func (c *Composite) ResetPlayedCount() {
if c.mode != nil {
c.mode.playedCount = 0
}
}
type compositeMode struct {
animationMode string
weaponClass string
direction int
directionCount int
playedCount int
layers []*Animation
drawOrder [][]d2enum.CompositeType
frameCount int
frameIndex int
animationSpeed float64
lastFrameTime float64
}
func (c *Composite) createMode(animationMode, weaponClass string, direction int) (*compositeMode, error) {
cof, err := loadCOF(fmt.Sprintf("%s/%s/COF/%s%s%s.COF", c.object.Base, c.object.Token, c.object.Token, animationMode, weaponClass))
if err != nil {
return nil, err
}
if direction >= cof.NumberOfDirections {
return nil, errors.New("invalid direction")
}
animationKey := strings.ToLower(c.object.Token + animationMode + weaponClass)
animationData := d2data.AnimationData[animationKey]
if len(animationData) == 0 {
return nil, errors.New("could not find animation data")
}
mode := &compositeMode{
animationMode: animationMode,
weaponClass: weaponClass,
direction: direction,
directionCount: cof.NumberOfDirections,
layers: make([]*Animation, d2enum.CompositeTypeMax),
frameCount: animationData[0].FramesPerDirection,
animationSpeed: 1.0 / ((float64(animationData[0].AnimationSpeed) * 25.0) / 256.0),
}
mode.drawOrder = make([][]d2enum.CompositeType, mode.frameCount)
for frame := 0; frame < mode.frameCount; frame++ {
mode.drawOrder[frame] = cof.Priority[direction][frame]
}
var layerDirection int
switch cof.NumberOfDirections {
case 4:
layerDirection = d2dcc.CofToDir4[mode.direction]
case 8:
layerDirection = d2dcc.CofToDir8[mode.direction]
case 16:
layerDirection = d2dcc.CofToDir16[mode.direction]
case 32:
layerDirection = d2dcc.CofToDir32[mode.direction]
}
for _, cofLayer := range cof.CofLayers {
var layerKey, layerValue string
switch cofLayer.Type {
case d2enum.CompositeTypeHead:
layerKey = "HD"
layerValue = c.object.HD
case d2enum.CompositeTypeTorso:
layerKey = "TR"
layerValue = c.object.TR
case d2enum.CompositeTypeLegs:
layerKey = "LG"
layerValue = c.object.LG
case d2enum.CompositeTypeRightArm:
layerKey = "RA"
layerValue = c.object.RA
case d2enum.CompositeTypeLeftArm:
layerKey = "LA"
layerValue = c.object.LA
case d2enum.CompositeTypeRightHand:
layerKey = "RH"
layerValue = c.object.RH
case d2enum.CompositeTypeLeftHand:
layerKey = "LH"
layerValue = c.object.LH
case d2enum.CompositeTypeShield:
layerKey = "SH"
layerValue = c.object.SH
case d2enum.CompositeTypeSpecial1:
layerKey = "S1"
layerValue = c.object.S1
case d2enum.CompositeTypeSpecial2:
layerKey = "S2"
layerValue = c.object.S2
case d2enum.CompositeTypeSpecial3:
layerKey = "S3"
layerValue = c.object.S3
case d2enum.CompositeTypeSpecial4:
layerKey = "S4"
layerValue = c.object.S4
case d2enum.CompositeTypeSpecial5:
layerKey = "S5"
layerValue = c.object.S5
case d2enum.CompositeTypeSpecial6:
layerKey = "S6"
layerValue = c.object.S6
case d2enum.CompositeTypeSpecial7:
layerKey = "S7"
layerValue = c.object.S7
case d2enum.CompositeTypeSpecial8:
layerKey = "S8"
layerValue = c.object.S8
default:
return nil, errors.New("unknown layer type")
}
blend := false
transparency := 255
if cofLayer.Transparent {
switch cofLayer.DrawEffect {
case d2enum.DrawEffectPctTransparency25:
transparency = 64
case d2enum.DrawEffectPctTransparency50:
transparency = 128
case d2enum.DrawEffectPctTransparency75:
transparency = 192
case d2enum.DrawEffectModulate:
blend = true
}
}
layer, err := loadCompositeLayer(c.object, layerKey, layerValue, animationMode, weaponClass, c.palettePath, transparency)
if err == nil {
layer.SetPlaySpeed(mode.animationSpeed)
layer.PlayForward()
layer.SetBlend(blend)
layer.SetDirection(layerDirection)
mode.layers[cofLayer.Type] = layer
}
}
return mode, nil
}
func loadCompositeLayer(object *d2datadict.ObjectLookupRecord, layerKey, layerValue, animationMode, weaponClass, palettePath string, transparency int) (*Animation, error) {
animationPaths := []string{
fmt.Sprintf("%s/%s/%s/%s%s%s%s%s.dcc", object.Base, object.Token, layerKey, object.Token, layerKey, layerValue, animationMode, weaponClass),
fmt.Sprintf("%s/%s/%s/%s%s%s%s%s.dcc", object.Base, object.Token, layerKey, object.Token, layerKey, layerValue, animationMode, "HTH"),
fmt.Sprintf("%s/%s/%s/%s%s%s%s%s.dc6", object.Base, object.Token, layerKey, object.Token, layerKey, layerValue, animationMode, weaponClass),
fmt.Sprintf("%s/%s/%s/%s%s%s%s%s.dc6", object.Base, object.Token, layerKey, object.Token, layerKey, layerValue, animationMode, "HTH"),
}
for _, animationPath := range animationPaths {
animation, err := LoadAnimationWithTransparency(animationPath, palettePath, transparency)
if err == nil {
return animation, nil
}
}
return nil, errors.New("animation not found")
}

View File

@ -1,302 +0,0 @@
package d2asset
import (
"errors"
"fmt"
"image"
"math"
"strings"
"github.com/OpenDiablo2/D2Shared/d2common/d2enum"
"github.com/OpenDiablo2/D2Shared/d2data"
"github.com/OpenDiablo2/D2Shared/d2data/d2datadict"
"github.com/OpenDiablo2/D2Shared/d2data/d2dcc"
"github.com/OpenDiablo2/D2Shared/d2helper"
"github.com/hajimehoshi/ebiten"
)
type paperdollCacheEntry struct {
sheetImage *ebiten.Image
compositeMode ebiten.CompositeMode
width int
height int
offsetX int
offsetY int
}
type Paperdoll struct {
object *d2datadict.ObjectLookupRecord
palette *d2datadict.PaletteRec
mode *paperdollMode
}
func createPaperdoll(object *d2datadict.ObjectLookupRecord, palette *d2datadict.PaletteRec) *Paperdoll {
return &Paperdoll{object: object, palette: palette}
}
func (p *Paperdoll) Render(target *ebiten.Image, offsetX, offsetY int) {
if p.mode == nil {
return
}
if p.mode.animationSpeed > 0 {
frameTime := d2helper.Now()
framesToAdd := int(math.Floor((frameTime - p.mode.lastFrameTime) / p.mode.animationSpeed))
if framesToAdd > 0 {
p.mode.lastFrameTime += p.mode.animationSpeed * float64(framesToAdd)
p.mode.currentFrame = (p.mode.currentFrame + framesToAdd) % p.mode.frameCount
}
}
for _, layerIndex := range p.mode.drawOrder[p.mode.currentFrame] {
cacheEntry := p.mode.layerCache[layerIndex]
x := float64(offsetX) + float64(p.mode.layerCache[layerIndex].offsetX)
y := float64(offsetY) + float64(p.mode.layerCache[layerIndex].offsetY)
sheetOffset := cacheEntry.width * p.mode.currentFrame
sheetRect := image.Rect(sheetOffset, 0, sheetOffset+cacheEntry.width, cacheEntry.height)
opts := &ebiten.DrawImageOptions{}
opts.GeoM.Translate(x, y)
opts.CompositeMode = cacheEntry.compositeMode
target.DrawImage(cacheEntry.sheetImage.SubImage(sheetRect).(*ebiten.Image), opts)
}
}
func (p *Paperdoll) SetMode(animationMode, weaponClass string, direction int) error {
mode, err := p.createMode(animationMode, weaponClass, direction)
if err != nil {
return err
}
p.mode = mode
return nil
}
type paperdollMode struct {
animationMode string
weaponClass string
direction int
layers []*d2dcc.DCC
layerCache []*paperdollCacheEntry
drawOrder [][]d2enum.CompositeType
frameCount int
animationSpeed float64
currentFrame int
lastFrameTime float64
}
func (p *Paperdoll) createMode(animationMode, weaponClass string, direction int) (*paperdollMode, error) {
mode := &paperdollMode{
animationMode: animationMode,
weaponClass: weaponClass,
direction: direction,
}
cofPath := fmt.Sprintf(
"%s/%s/COF/%s%s%s.COF",
p.object.Base,
p.object.Token,
p.object.Token,
mode.animationMode,
mode.weaponClass,
)
cof, err := LoadCOF(cofPath)
if err != nil {
return nil, err
}
if mode.direction >= cof.NumberOfDirections {
return nil, errors.New("invalid direction")
}
mode.layers = make([]*d2dcc.DCC, d2enum.CompositeTypeMax)
for _, cofLayer := range cof.CofLayers {
var layerKey, layerValue string
switch cofLayer.Type {
case d2enum.CompositeTypeHead:
layerKey = "HD"
layerValue = p.object.HD
case d2enum.CompositeTypeTorso:
layerKey = "TR"
layerValue = p.object.TR
case d2enum.CompositeTypeLegs:
layerKey = "LG"
layerValue = p.object.LG
case d2enum.CompositeTypeRightArm:
layerKey = "RA"
layerValue = p.object.RA
case d2enum.CompositeTypeLeftArm:
layerKey = "LA"
layerValue = p.object.LA
case d2enum.CompositeTypeRightHand:
layerKey = "RH"
layerValue = p.object.RH
case d2enum.CompositeTypeLeftHand:
layerKey = "LH"
layerValue = p.object.LH
case d2enum.CompositeTypeShield:
layerKey = "SH"
layerValue = p.object.SH
case d2enum.CompositeTypeSpecial1:
layerKey = "S1"
layerValue = p.object.S1
case d2enum.CompositeTypeSpecial2:
layerKey = "S2"
layerValue = p.object.S2
case d2enum.CompositeTypeSpecial3:
layerKey = "S3"
layerValue = p.object.S3
case d2enum.CompositeTypeSpecial4:
layerKey = "S4"
layerValue = p.object.S4
case d2enum.CompositeTypeSpecial5:
layerKey = "S5"
layerValue = p.object.S5
case d2enum.CompositeTypeSpecial6:
layerKey = "S6"
layerValue = p.object.S6
case d2enum.CompositeTypeSpecial7:
layerKey = "S7"
layerValue = p.object.S7
case d2enum.CompositeTypeSpecial8:
layerKey = "S8"
layerValue = p.object.S8
default:
return nil, errors.New("unknown layer type")
}
layerPath := fmt.Sprintf(
"%s/%s/%s/%s%s%s%s%s.dcc",
p.object.Base,
p.object.Token,
layerKey,
p.object.Token,
layerKey,
layerValue,
mode.animationMode,
mode.weaponClass,
)
dcc, err := LoadDCC(layerPath)
if err != nil {
return nil, err
}
mode.layers[cofLayer.Type] = dcc
}
animationKey := strings.ToLower(p.object.Token + mode.animationMode + mode.weaponClass)
animationData := d2data.AnimationData[animationKey]
if len(animationData) == 0 {
return nil, errors.New("could not find animation data")
}
mode.animationSpeed = 1.0 / ((float64(animationData[0].AnimationSpeed) * 25.0) / 256.0)
mode.lastFrameTime = d2helper.Now()
mode.frameCount = animationData[0].FramesPerDirection
var dccDirection int
switch cof.NumberOfDirections {
case 4:
dccDirection = d2dcc.CofToDir4[mode.direction]
case 8:
dccDirection = d2dcc.CofToDir8[mode.direction]
case 16:
dccDirection = d2dcc.CofToDir16[mode.direction]
case 32:
dccDirection = d2dcc.CofToDir32[mode.direction]
}
mode.drawOrder = make([][]d2enum.CompositeType, mode.frameCount)
for frame := 0; frame < mode.frameCount; frame++ {
mode.drawOrder[frame] = cof.Priority[direction][frame]
}
mode.layerCache = make([]*paperdollCacheEntry, d2enum.CompositeTypeMax)
for _, cofLayer := range cof.CofLayers {
layer := mode.layers[cofLayer.Type]
minX, minY := math.MaxInt32, math.MaxInt32
maxX, maxY := math.MinInt32, math.MinInt32
for _, frame := range layer.Directions[dccDirection].Frames {
minX = d2helper.MinInt(minX, frame.Box.Left)
minY = d2helper.MinInt(minY, frame.Box.Top)
maxX = d2helper.MaxInt(maxX, frame.Box.Right())
maxY = d2helper.MaxInt(maxY, frame.Box.Bottom())
}
cacheEntry := &paperdollCacheEntry{
offsetX: minX,
offsetY: minY,
width: maxX - minX,
height: maxY - minY,
}
if cacheEntry.width <= 0 || cacheEntry.height <= 0 {
return nil, errors.New("invalid animation size")
}
var transparency int
if cofLayer.Transparent {
switch cofLayer.DrawEffect {
case d2enum.DrawEffectPctTransparency25:
transparency = 64
case d2enum.DrawEffectPctTransparency50:
transparency = 128
case d2enum.DrawEffectPctTransparency75:
transparency = 192
case d2enum.DrawEffectModulate:
cacheEntry.compositeMode = ebiten.CompositeModeLighter
default:
transparency = 255
}
}
pixels := make([]byte, mode.frameCount*cacheEntry.width*cacheEntry.height*4)
for i := 0; i < mode.frameCount; i++ {
direction := layer.Directions[dccDirection]
if i >= len(direction.Frames) {
return nil, errors.New("invalid animation index")
}
sheetOffset := cacheEntry.width * i
sheetWidth := cacheEntry.height * mode.frameCount
frame := direction.Frames[i]
for y := 0; y < direction.Box.Height; y++ {
for x := 0; x < direction.Box.Width; x++ {
if paletteIndex := frame.PixelData[x+(y*direction.Box.Width)]; paletteIndex != 0 {
color := p.palette.Colors[paletteIndex]
frameX := (x + direction.Box.Left) - minX
frameY := (y + direction.Box.Top) - minY
offset := (sheetOffset + frameX + (frameY * sheetWidth)) * 4
pixels[offset] = color.R
pixels[offset+1] = color.G
pixels[offset+2] = color.B
pixels[offset+3] = byte(transparency)
}
}
}
}
cacheEntry.sheetImage, err = ebiten.NewImage(cacheEntry.width*mode.frameCount, cacheEntry.height, ebiten.FilterNearest)
if err != nil {
return nil, err
}
if err := cacheEntry.sheetImage.ReplacePixels(pixels); err != nil {
return nil, err
}
mode.layerCache[cofLayer.Type] = cacheEntry
}
return mode, nil
}

View File

@ -1,22 +0,0 @@
package d2asset
import (
"github.com/OpenDiablo2/D2Shared/d2data/d2datadict"
)
type paperdollManager struct {
cache *cache
}
func createPaperdollManager() *paperdollManager {
return &paperdollManager{cache: createCache(PaperdollBudget)}
}
func (pm *paperdollManager) loadPaperdoll(object *d2datadict.ObjectLookupRecord, palettePath string) (*Paperdoll, error) {
palette, err := loadPalette(palettePath)
if err != nil {
return nil, err
}
return createPaperdoll(object, palette), nil
}

View File

@ -45,7 +45,10 @@ func (v *Manager) PlayBGM(song string) {
log.Panic(err) log.Panic(err)
} }
} }
audioData := d2asset.MustLoadFile(song) audioData, err := d2asset.LoadFile(song)
if err != nil {
panic(err)
}
d, err := wav.Decode(v.audioContext, audio.BytesReadSeekCloser(audioData)) d, err := wav.Decode(v.audioContext, audio.BytesReadSeekCloser(audioData))
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)

View File

@ -24,7 +24,12 @@ func CreateSoundEffect(sfx string, context *audio.Context, volume float64) *Soun
} else { } else {
soundFile = sfx soundFile = sfx
} }
audioData := d2asset.MustLoadFile(soundFile)
audioData, err := d2asset.LoadFile(soundFile)
if err != nil {
panic(err)
}
d, err := wav.Decode(context, audio.BytesReadSeekCloser(audioData)) d, err := wav.Decode(context, audio.BytesReadSeekCloser(audioData))
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)

View File

@ -21,7 +21,10 @@ func CreateBlizzardIntro(sceneProvider d2coreinterface.SceneProvider) *BlizzardI
func (v *BlizzardIntro) Load() []func() { func (v *BlizzardIntro) Load() []func() {
return []func(){ return []func(){
func() { func() {
videoBytes := d2asset.MustLoadFile("/data/local/video/BlizNorth640x480.bik") videoBytes, err := d2asset.LoadFile("/data/local/video/BlizNorth640x480.bik")
if err != nil {
panic(err)
}
v.videoDecoder = d2video.CreateBinkDecoder(videoBytes) v.videoDecoder = d2video.CreateBinkDecoder(videoBytes)
}, },
} }

View File

@ -87,8 +87,12 @@ func (v *Credits) Load() []func() {
v.uiManager.AddWidget(&v.exitButton) v.uiManager.AddWidget(&v.exitButton)
}, },
func() { func() {
fileData, _ := dh.Utf16BytesToString(d2asset.MustLoadFile(d2resource.CreditsText)[2:]) fileData, err := d2asset.LoadFile(d2resource.CreditsText)
v.creditsText = strings.Split(fileData, "\r\n") if err != nil {
panic(err)
}
creditData, _ := dh.Utf16BytesToString(fileData[2:])
v.creditsText = strings.Split(creditData, "\r\n")
for i := range v.creditsText { for i := range v.creditsText {
v.creditsText[i] = strings.Trim(v.creditsText[i], " ") v.creditsText[i] = strings.Trim(v.creditsText[i], " ")
} }

View File

@ -2,41 +2,42 @@ package d2core
import ( import (
"github.com/OpenDiablo2/D2Shared/d2common/d2enum" "github.com/OpenDiablo2/D2Shared/d2common/d2enum"
"github.com/OpenDiablo2/D2Shared/d2common/d2resource"
"github.com/OpenDiablo2/D2Shared/d2data/d2datadict" "github.com/OpenDiablo2/D2Shared/d2data/d2datadict"
"github.com/OpenDiablo2/OpenDiablo2/d2render" "github.com/OpenDiablo2/OpenDiablo2/d2render"
"github.com/hajimehoshi/ebiten" "github.com/hajimehoshi/ebiten"
) )
type Hero struct { type Hero struct {
AnimatedEntity d2render.AnimatedEntity AnimatedEntity *d2render.AnimatedEntity
Equipment CharacterEquipment Equipment CharacterEquipment
mode d2enum.AnimationMode mode d2enum.AnimationMode
direction int direction int
} }
func CreateHero(x, y int32, direction int, heroType d2enum.Hero, equipment CharacterEquipment) *Hero { func CreateHero(x, y int32, direction int, heroType d2enum.Hero, equipment CharacterEquipment) *Hero {
result := &Hero{ object := &d2datadict.ObjectLookupRecord{
AnimatedEntity: d2render.CreateAnimatedEntity(x, y, &d2datadict.ObjectLookupRecord{ Mode: d2enum.AnimationModePlayerNeutral.String(),
Mode: d2enum.AnimationModePlayerNeutral.String(), Base: "/data/global/chars",
Base: "/data/global/chars", Token: heroType.GetToken(),
Token: heroType.GetToken(), Class: equipment.RightHand.GetWeaponClass(),
Class: equipment.RightHand.GetWeaponClass(), SH: equipment.Shield.GetItemCode(),
SH: equipment.Shield.GetItemCode(), // TODO: Offhand class?
// TODO: Offhand class? HD: equipment.Head.GetArmorClass(),
HD: equipment.Head.GetArmorClass(), TR: equipment.Torso.GetArmorClass(),
TR: equipment.Torso.GetArmorClass(), LG: equipment.Legs.GetArmorClass(),
LG: equipment.Legs.GetArmorClass(), RA: equipment.RightArm.GetArmorClass(),
RA: equipment.RightArm.GetArmorClass(), LA: equipment.LeftArm.GetArmorClass(),
LA: equipment.LeftArm.GetArmorClass(), RH: equipment.RightHand.GetItemCode(),
RH: equipment.RightHand.GetItemCode(), LH: equipment.LeftHand.GetItemCode(),
LH: equipment.LeftHand.GetItemCode(),
},
d2enum.Units,
),
Equipment: equipment,
mode: d2enum.AnimationModePlayerTownNeutral,
direction: direction,
} }
entity, err := d2render.CreateAnimatedEntity(x, y, object, d2resource.PaletteUnits)
if err != nil {
panic(err)
}
result := &Hero{AnimatedEntity: entity, Equipment: equipment, mode: d2enum.AnimationModePlayerTownNeutral, direction: direction}
result.AnimatedEntity.SetMode(result.mode.String(), equipment.RightHand.GetWeaponClass(), direction) result.AnimatedEntity.SetMode(result.mode.String(), equipment.RightHand.GetWeaponClass(), direction)
return result return result
} }
@ -47,6 +48,8 @@ func (v *Hero) Advance(tickTime float64) {
v.AnimatedEntity.LocationY != v.AnimatedEntity.TargetY { v.AnimatedEntity.LocationY != v.AnimatedEntity.TargetY {
v.AnimatedEntity.Step(tickTime) v.AnimatedEntity.Step(tickTime)
} }
v.AnimatedEntity.Advance(tickTime)
} }
func (v *Hero) Render(target *ebiten.Image, offsetX, offsetY int) { func (v *Hero) Render(target *ebiten.Image, offsetX, offsetY int) {

View File

@ -2,24 +2,26 @@ package d2core
import ( import (
"github.com/OpenDiablo2/D2Shared/d2common" "github.com/OpenDiablo2/D2Shared/d2common"
"github.com/OpenDiablo2/D2Shared/d2common/d2enum" "github.com/OpenDiablo2/D2Shared/d2common/d2resource"
"github.com/OpenDiablo2/D2Shared/d2data/d2datadict" "github.com/OpenDiablo2/D2Shared/d2data/d2datadict"
"github.com/OpenDiablo2/OpenDiablo2/d2render" "github.com/OpenDiablo2/OpenDiablo2/d2render"
"github.com/hajimehoshi/ebiten" "github.com/hajimehoshi/ebiten"
) )
type NPC struct { type NPC struct {
AnimatedEntity d2render.AnimatedEntity AnimatedEntity *d2render.AnimatedEntity
HasPaths bool HasPaths bool
Paths []d2common.Path Paths []d2common.Path
path int path int
} }
func CreateNPC(x, y int32, object *d2datadict.ObjectLookupRecord, direction int) *NPC { func CreateNPC(x, y int32, object *d2datadict.ObjectLookupRecord, direction int) *NPC {
result := &NPC{ entity, err := d2render.CreateAnimatedEntity(x, y, object, d2resource.PaletteUnits)
AnimatedEntity: d2render.CreateAnimatedEntity(x, y, object, d2enum.Units), if err != nil {
HasPaths: false, panic(err)
} }
result := &NPC{AnimatedEntity: entity, HasPaths: false}
result.AnimatedEntity.SetMode(object.Mode, object.Class, direction) result.AnimatedEntity.SetMode(object.Mode, object.Class, direction)
return result return result
} }
@ -68,4 +70,6 @@ func (v *NPC) Advance(tickTime float64) {
v.AnimatedEntity.LocationY != v.AnimatedEntity.TargetY { v.AnimatedEntity.LocationY != v.AnimatedEntity.TargetY {
v.AnimatedEntity.Step(tickTime) v.AnimatedEntity.Step(tickTime)
} }
v.AnimatedEntity.Advance(tickTime)
} }

View File

@ -1,326 +1,83 @@
package d2render package d2render
import ( import (
"errors"
"fmt"
"image"
"log"
"math" "math"
"math/rand" "math/rand"
"strings"
"github.com/OpenDiablo2/D2Shared/d2common/d2enum" "github.com/OpenDiablo2/D2Shared/d2common/d2enum"
"github.com/OpenDiablo2/D2Shared/d2data"
"github.com/OpenDiablo2/D2Shared/d2data/d2cof"
"github.com/OpenDiablo2/D2Shared/d2data/d2datadict" "github.com/OpenDiablo2/D2Shared/d2data/d2datadict"
"github.com/OpenDiablo2/D2Shared/d2data/d2dcc"
"github.com/OpenDiablo2/D2Shared/d2helper" "github.com/OpenDiablo2/D2Shared/d2helper"
"github.com/OpenDiablo2/OpenDiablo2/d2asset" "github.com/OpenDiablo2/OpenDiablo2/d2asset"
"github.com/hajimehoshi/ebiten" "github.com/hajimehoshi/ebiten"
) )
var DccLayerNames = []string{"HD", "TR", "LG", "RA", "LA", "RH", "LH", "SH", "S1", "S2", "S3", "S4", "S5", "S6", "S7", "S8"}
type LayerCacheEntry struct {
frameSheet *ebiten.Image
frameWidth int
frameHeight int
compositeMode ebiten.CompositeMode
offsetX, offsetY int32
}
// AnimatedEntity represents an entity on the map that can be animated // AnimatedEntity represents an entity on the map that can be animated
type AnimatedEntity struct { type AnimatedEntity struct {
LocationX float64 LocationX float64
LocationY float64 LocationY float64
TileX, TileY int // Coordinates of the tile the unit is within TileX, TileY int // Coordinates of the tile the unit is within
subcellX, subcellY float64 // Subcell coordinates within the current tile subcellX, subcellY float64 // Subcell coordinates within the current tile
dccLayers map[string]*d2dcc.DCC
Cof *d2cof.COF
palette d2enum.PaletteType
base string
token string
animationMode string animationMode string
weaponClass string weaponClass string
lastFrameTime float64
framesToAnimate int
animationSpeed float64
direction int direction int
currentFrame int
offsetX, offsetY int32 offsetX, offsetY int32
object *d2datadict.ObjectLookupRecord
layerCache []LayerCacheEntry
drawOrder [][]d2enum.CompositeType
TargetX float64 TargetX float64
TargetY float64 TargetY float64
action int32 action int32
repetitions int32 repetitions int
composite *d2asset.Composite
} }
// CreateAnimatedEntity creates an instance of AnimatedEntity // CreateAnimatedEntity creates an instance of AnimatedEntity
func CreateAnimatedEntity(x, y int32, object *d2datadict.ObjectLookupRecord, palette d2enum.PaletteType) AnimatedEntity { func CreateAnimatedEntity(x, y int32, object *d2datadict.ObjectLookupRecord, palettePath string) (*AnimatedEntity, error) {
result := AnimatedEntity{ composite, err := d2asset.LoadComposite(object, palettePath)
base: object.Base, if err != nil {
token: object.Token, return nil, err
object: object,
palette: palette,
layerCache: make([]LayerCacheEntry, d2enum.CompositeTypeMax),
} }
result.dccLayers = make(map[string]*d2dcc.DCC)
result.LocationX = float64(x)
result.LocationY = float64(y)
result.TargetX = result.LocationX
result.TargetY = result.LocationY
result.TileX = int(result.LocationX / 5) entity := &AnimatedEntity{composite: composite}
result.TileY = int(result.LocationY / 5) entity.LocationX = float64(x)
result.subcellX = 1 + math.Mod(result.LocationX, 5) entity.LocationY = float64(y)
result.subcellY = 1 + math.Mod(result.LocationY, 5) entity.TargetX = entity.LocationX
entity.TargetY = entity.LocationY
return result entity.TileX = int(entity.LocationX / 5)
entity.TileY = int(entity.LocationY / 5)
entity.subcellX = 1 + math.Mod(entity.LocationX, 5)
entity.subcellY = 1 + math.Mod(entity.LocationY, 5)
return entity, nil
} }
// SetMode changes the graphical mode of this animated entity // SetMode changes the graphical mode of this animated entity
func (v *AnimatedEntity) SetMode(animationMode, weaponClass string, direction int) { func (v *AnimatedEntity) SetMode(animationMode, weaponClass string, direction int) error {
cofPath := fmt.Sprintf("%s/%s/COF/%s%s%s.COF", v.base, v.token, v.token, animationMode, weaponClass)
var err error
if v.Cof, err = d2asset.LoadCOF(cofPath); err != nil {
return
}
if v.Cof.NumberOfDirections == 0 || v.Cof.NumberOfLayers == 0 || v.Cof.FramesPerDirection == 0 {
return
}
resetAnimation := v.animationMode != animationMode || v.weaponClass != weaponClass
v.animationMode = animationMode v.animationMode = animationMode
v.weaponClass = weaponClass
v.direction = direction v.direction = direction
if v.direction >= v.Cof.NumberOfDirections {
v.direction = v.Cof.NumberOfDirections - 1
}
v.dccLayers = make(map[string]*d2dcc.DCC)
for _, cofLayer := range v.Cof.CofLayers {
layerName := DccLayerNames[cofLayer.Type]
if v.dccLayers[layerName], err = v.LoadLayer(layerName); err != nil {
continue
}
}
v.updateFrameCache(resetAnimation) err := v.composite.SetMode(animationMode, weaponClass, direction)
}
func (v *AnimatedEntity) LoadLayer(layer string) (*d2dcc.DCC, error) {
layerName := "TR"
switch strings.ToUpper(layer) {
case "HD": // Head
layerName = v.object.HD
case "TR": // Torso
layerName = v.object.TR
case "LG": // Legs
layerName = v.object.LG
case "RA": // RightArm
layerName = v.object.RA
case "LA": // LeftArm
layerName = v.object.LA
case "RH": // RightHand
layerName = v.object.RH
case "LH": // LeftHand
layerName = v.object.LH
case "SH": // Shield
layerName = v.object.SH
case "S1": // Special1
layerName = v.object.S1
case "S2": // Special2
layerName = v.object.S2
case "S3": // Special3
layerName = v.object.S3
case "S4": // Special4
layerName = v.object.S4
case "S5": // Special5
layerName = v.object.S5
case "S6": // Special6
layerName = v.object.S6
case "S7": // Special7
layerName = v.object.S7
case "S8": // Special8
layerName = v.object.S8
}
if len(layerName) == 0 {
return nil, errors.New("invalid layer")
}
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)
result, err := d2asset.LoadDCC(dccPath)
if err != nil { if err != nil {
dccPath = fmt.Sprintf("%s/%s/%s/%s%s%s%s%s.dcc", v.base, v.token, layer, v.token, layer, layerName, v.animationMode, "HTH") err = v.composite.SetMode(animationMode, "HTH", direction)
result, err = d2asset.LoadDCC(dccPath)
if err != nil {
return nil, err
}
} }
return result, nil return err
} }
// If an npc has a path to pause at each location. // If an npc has a path to pause at each location.
// Waits for animation to end and all repetitions to be exhausted. // Waits for animation to end and all repetitions to be exhausted.
func (v AnimatedEntity) Wait() bool { func (v AnimatedEntity) Wait() bool {
// currentFrame might skip the final frame if framesToAdd doesn't match up, return v.composite.GetPlayedCount() > v.repetitions
// bail immediately after the last repetition if that happens.
return v.repetitions < 0 || (v.repetitions == 0 && v.currentFrame >= v.framesToAnimate-1)
} }
// Render draws this animated entity onto the target // Render draws this animated entity onto the target
func (v *AnimatedEntity) Render(target *ebiten.Image, offsetX, offsetY int) { func (v *AnimatedEntity) Render(target *ebiten.Image, offsetX, offsetY int) {
if v.animationSpeed > 0 {
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
v.repetitions = d2helper.MinInt32(-1, v.repetitions-1)
}
}
}
localX := (v.subcellX - v.subcellY) * 16 localX := (v.subcellX - v.subcellY) * 16
localY := ((v.subcellX + v.subcellY) * 8) - 5 localY := ((v.subcellX + v.subcellY) * 8) - 5
v.composite.Render(
if v.drawOrder == nil { target,
return int(v.offsetX)+offsetX+int(localX),
} int(v.offsetY)+offsetY+int(localY),
for _, layerIdx := range v.drawOrder[v.currentFrame] { )
if v.currentFrame < 0 || v.layerCache[layerIdx].frameSheet == nil || v.currentFrame >= v.framesToAnimate {
continue
}
opts := &ebiten.DrawImageOptions{}
layer := v.layerCache[layerIdx]
x := float64(v.offsetX) + float64(offsetX) + localX + float64(v.layerCache[layerIdx].offsetX)
y := float64(v.offsetY) + float64(offsetY) + localY + float64(v.layerCache[layerIdx].offsetY)
opts.GeoM.Translate(x, y)
opts.CompositeMode = v.layerCache[layerIdx].compositeMode
xOffset := layer.frameWidth * v.currentFrame
sheetIndex := image.Rect(xOffset, 0, xOffset+layer.frameWidth, layer.frameHeight)
if err := target.DrawImage(layer.frameSheet.SubImage(sheetIndex).(*ebiten.Image), opts); err != nil {
log.Panic(err.Error())
}
}
}
func (v *AnimatedEntity) updateFrameCache(resetAnimation bool) {
if resetAnimation {
v.currentFrame = 0
}
// TODO: This animation data madness is incorrect, yet tasty
animDataTemp := d2data.AnimationData[strings.ToLower(v.token+v.animationMode+v.weaponClass)]
if animDataTemp == nil {
return
}
animationData := animDataTemp[0]
v.animationSpeed = 1.0 / ((float64(animationData.AnimationSpeed) * 25.0) / 256.0)
v.framesToAnimate = animationData.FramesPerDirection
v.lastFrameTime = d2helper.Now()
v.drawOrder = make([][]d2enum.CompositeType, v.framesToAnimate)
var dccDirection int
switch v.Cof.NumberOfDirections {
case 4:
dccDirection = d2dcc.CofToDir4[v.direction]
case 8:
dccDirection = d2dcc.CofToDir8[v.direction]
case 16:
dccDirection = d2dcc.CofToDir16[v.direction]
case 32:
dccDirection = d2dcc.CofToDir32[v.direction]
default:
dccDirection = 0
}
for frame := 0; frame < v.framesToAnimate; frame++ {
v.drawOrder[frame] = v.Cof.Priority[v.direction][frame]
}
for cofLayerIdx := range v.Cof.CofLayers {
layerType := v.Cof.CofLayers[cofLayerIdx].Type
layerName := DccLayerNames[layerType]
dccLayer := v.dccLayers[layerName]
if dccLayer == nil {
continue
}
minX := int32(10000)
minY := int32(10000)
maxX := int32(-10000)
maxY := int32(-10000)
for frameIdx := range dccLayer.Directions[dccDirection].Frames {
minX = d2helper.MinInt32(minX, int32(dccLayer.Directions[dccDirection].Frames[frameIdx].Box.Left))
minY = d2helper.MinInt32(minY, int32(dccLayer.Directions[dccDirection].Frames[frameIdx].Box.Top))
maxX = d2helper.MaxInt32(maxX, int32(dccLayer.Directions[dccDirection].Frames[frameIdx].Box.Right()))
maxY = d2helper.MaxInt32(maxY, int32(dccLayer.Directions[dccDirection].Frames[frameIdx].Box.Bottom()))
}
v.layerCache[layerType].offsetX = minX
v.layerCache[layerType].offsetY = minY
actualWidth := maxX - minX
actualHeight := maxY - minY
if (actualWidth <= 0) || (actualHeight < 0) {
log.Printf("Animated entity created with an invalid size of (%d, %d)", actualWidth, actualHeight)
return
}
transparency := byte(255)
if v.Cof.CofLayers[cofLayerIdx].Transparent {
switch v.Cof.CofLayers[cofLayerIdx].DrawEffect {
//Lets pick whatever we have that's closest.
case d2enum.DrawEffectPctTransparency25:
transparency = byte(64)
case d2enum.DrawEffectPctTransparency50:
transparency = byte(128)
case d2enum.DrawEffectPctTransparency75:
transparency = byte(192)
case d2enum.DrawEffectModulate:
v.layerCache[layerType].compositeMode = ebiten.CompositeModeLighter
case d2enum.DrawEffectBurn:
// Flies in tal rasha's tomb use this
case d2enum.DrawEffectNormal:
}
}
pixels := make([]byte, int32(v.framesToAnimate)*(actualWidth*actualHeight*4))
for animationIdx := 0; animationIdx < v.framesToAnimate; animationIdx++ {
if animationIdx >= len(dccLayer.Directions[dccDirection].Frames) {
log.Printf("Invalid animation index of %d for animated entity", animationIdx)
continue
}
sheetOffset := int(actualWidth) * animationIdx
combinedWidth := int(actualWidth) * v.framesToAnimate
frame := dccLayer.Directions[dccDirection].Frames[animationIdx]
for y := 0; y < dccLayer.Directions[dccDirection].Box.Height; y++ {
for x := 0; x < dccLayer.Directions[dccDirection].Box.Width; x++ {
paletteIndex := frame.PixelData[x+(y*dccLayer.Directions[dccDirection].Box.Width)]
if paletteIndex == 0 {
continue
}
color := d2datadict.Palettes[v.palette].Colors[paletteIndex]
actualX := (x + dccLayer.Directions[dccDirection].Box.Left) - int(minX)
actualY := (y + dccLayer.Directions[dccDirection].Box.Top) - int(minY)
idx := (sheetOffset + actualX + ((actualY) * combinedWidth)) * 4
pixels[idx] = color.R
pixels[idx+1] = color.G
pixels[idx+2] = color.B
pixels[idx+3] = transparency
}
}
}
v.layerCache[layerType].frameSheet, _ = ebiten.NewImage(int(actualWidth)*v.framesToAnimate, int(actualHeight), ebiten.FilterNearest)
_ = v.layerCache[layerType].frameSheet.ReplacePixels(pixels)
v.layerCache[layerType].frameWidth = int(actualWidth)
v.layerCache[layerType].frameHeight = int(actualHeight)
}
} }
func (v AnimatedEntity) GetDirection() int { func (v AnimatedEntity) GetDirection() int {
@ -365,8 +122,7 @@ func (v *AnimatedEntity) Step(tickTime float64) {
v.TileY = int(v.LocationY / 5) v.TileY = int(v.LocationY / 5)
if v.LocationX == v.TargetX && v.LocationY == v.TargetY { if v.LocationX == v.TargetX && v.LocationY == v.TargetY {
v.repetitions = 3 + rand.Intn(5)
v.repetitions = 3 + rand.Int31n(5)
newAnimationMode := d2enum.AnimationModeObjectNeutral newAnimationMode := d2enum.AnimationModeObjectNeutral
// TODO: Figure out what 1-3 are for, 4 is correct. // TODO: Figure out what 1-3 are for, 4 is correct.
switch v.action { switch v.action {
@ -381,10 +137,10 @@ func (v *AnimatedEntity) Step(tickTime float64) {
v.repetitions = 0 v.repetitions = 0
} }
v.composite.ResetPlayedCount()
if v.animationMode != newAnimationMode.String() { if v.animationMode != newAnimationMode.String() {
v.SetMode(newAnimationMode.String(), v.weaponClass, v.direction) v.SetMode(newAnimationMode.String(), v.weaponClass, v.direction)
} }
} }
} }
@ -405,7 +161,7 @@ func (v *AnimatedEntity) SetTarget(tx, ty float64, action int32) {
newAnimationMode = d2enum.AnimationModeMonsterWalk.String() newAnimationMode = d2enum.AnimationModeMonsterWalk.String()
} }
newDirection := angleToDirection(float64(angle), v.Cof.NumberOfDirections) newDirection := angleToDirection(float64(angle), v.composite.GetDirectionCount())
if newDirection != v.GetDirection() || newAnimationMode != v.animationMode { if newDirection != v.GetDirection() || newAnimationMode != v.animationMode {
v.SetMode(newAnimationMode, v.weaponClass, newDirection) v.SetMode(newAnimationMode, v.weaponClass, newDirection)
} }
@ -428,8 +184,8 @@ func angleToDirection(angle float64, numberOfDirections int) int {
return newDirection return newDirection
} }
func (v *AnimatedEntity) Advance(tickTime float64) { func (v *AnimatedEntity) Advance(elapsed float64) {
v.composite.Advance(elapsed)
} }
func (v *AnimatedEntity) GetPosition() (float64, float64) { func (v *AnimatedEntity) GetPosition() (float64, float64) {

View File

@ -1,8 +1,9 @@
package d2render package d2render
import ( import (
"github.com/stretchr/testify/assert"
"testing" "testing"
"github.com/stretchr/testify/assert"
) )
func TestAngleToDirection_16Directions(t *testing.T) { func TestAngleToDirection_16Directions(t *testing.T) {
@ -61,7 +62,6 @@ func TestAngleToDirection_1Direction(t *testing.T) {
} }
} }
func TestAngleToDirection_0Directions(t *testing.T) { func TestAngleToDirection_0Directions(t *testing.T) {
angle := 0.0 angle := 0.0
for i := 0; i < 120; i++ { for i := 0; i < 120; i++ {

View File

@ -13,6 +13,7 @@ import (
"github.com/OpenDiablo2/D2Shared/d2common" "github.com/OpenDiablo2/D2Shared/d2common"
"github.com/OpenDiablo2/D2Shared/d2common/d2enum" "github.com/OpenDiablo2/D2Shared/d2common/d2enum"
"github.com/OpenDiablo2/D2Shared/d2common/d2resource"
"github.com/OpenDiablo2/D2Shared/d2data/d2datadict" "github.com/OpenDiablo2/D2Shared/d2data/d2datadict"
"github.com/OpenDiablo2/D2Shared/d2data/d2ds1" "github.com/OpenDiablo2/D2Shared/d2data/d2ds1"
"github.com/OpenDiablo2/D2Shared/d2data/d2dt1" "github.com/OpenDiablo2/D2Shared/d2data/d2dt1"
@ -54,7 +55,12 @@ func loadRegion(seed int64, tileOffsetX, tileOffsetY int, levelType d2enum.Regio
for _, levelTypeDt1 := range region.levelType.Files { for _, levelTypeDt1 := range region.levelType.Files {
if len(levelTypeDt1) != 0 && levelTypeDt1 != "" && levelTypeDt1 != "0" { if len(levelTypeDt1) != 0 && levelTypeDt1 != "" && levelTypeDt1 != "0" {
dt1 := d2dt1.LoadDT1(d2asset.MustLoadFile("/data/global/tiles/" + levelTypeDt1)) fileData, err := d2asset.LoadFile("/data/global/tiles/" + levelTypeDt1)
if err != nil {
panic(err)
}
dt1 := d2dt1.LoadDT1(fileData)
region.tiles = append(region.tiles, dt1.Tiles...) region.tiles = append(region.tiles, dt1.Tiles...)
} }
} }
@ -72,7 +78,11 @@ func loadRegion(seed int64, tileOffsetX, tileOffsetY int, levelType d2enum.Regio
} }
region.regionPath = levelFilesToPick[levelIndex] region.regionPath = levelFilesToPick[levelIndex]
region.ds1 = d2ds1.LoadDS1(d2asset.MustLoadFile("/data/global/tiles/" + region.regionPath)) fileData, err := d2asset.LoadFile("/data/global/tiles/" + region.regionPath)
if err != nil {
panic(err)
}
region.ds1 = d2ds1.LoadDS1(fileData)
region.tileRect = d2common.Rectangle{ region.tileRect = d2common.Rectangle{
Left: tileOffsetX, Left: tileOffsetX,
Top: tileOffsetY, Top: tileOffsetY,
@ -133,9 +143,12 @@ func (mr *MapRegion) loadEntities() []MapEntity {
} }
case d2datadict.ObjectTypeItem: case d2datadict.ObjectTypeItem:
if object.ObjectInfo != nil && object.ObjectInfo.Draw && object.Lookup.Base != "" && object.Lookup.Token != "" { if object.ObjectInfo != nil && object.ObjectInfo.Draw && object.Lookup.Base != "" && object.Lookup.Token != "" {
entity := d2render.CreateAnimatedEntity(int32(worldX), int32(worldY), object.Lookup, d2enum.Units) entity, err := d2render.CreateAnimatedEntity(int32(worldX), int32(worldY), object.Lookup, d2resource.PaletteUnits)
if err != nil {
panic(err)
}
entity.SetMode(object.Lookup.Mode, object.Lookup.Class, 0) entity.SetMode(object.Lookup.Mode, object.Lookup.Class, 0)
entities = append(entities, &entity) entities = append(entities, entity)
} }
} }
} }

View File

@ -51,7 +51,10 @@ func CreateFont(font string, palettePath string) *Font {
// bug: performance issue when using CJK fonts, because ten thousand frames will be rendered PER font // bug: performance issue when using CJK fonts, because ten thousand frames will be rendered PER font
result.fontSprite, _ = d2render.LoadSprite(font+".dc6", palettePath) result.fontSprite, _ = d2render.LoadSprite(font+".dc6", palettePath)
woo := "Woo!\x01" woo := "Woo!\x01"
fontData := d2asset.MustLoadFile(font + ".tbl") fontData, err := d2asset.LoadFile(font + ".tbl")
if err != nil {
panic(err)
}
if string(fontData[0:5]) != woo { if string(fontData[0:5]) != woo {
panic("No woo :(") panic("No woo :(")
} }