From 0ee937f01b90b985de43742df2a2e849a6c33375 Mon Sep 17 00:00:00 2001 From: Alex Yatskov Date: Mon, 23 Dec 2019 22:48:45 -0800 Subject: [PATCH] Rename Paperdoll to Composite (#265) Make AnimatedEntity use Composite --- README.md | 5 + d2asset/animation.go | 94 +++++++++- d2asset/animation_manager.go | 54 ++++-- d2asset/asset_manager.go | 32 +--- d2asset/composite.go | 259 ++++++++++++++++++++++++++ d2asset/paperdoll.go | 302 ------------------------------ d2asset/paperdoll_manager.go | 22 --- d2audio/audio_provider.go | 5 +- d2audio/sound_effect.go | 7 +- d2core/d2scene/blizzard_intro.go | 5 +- d2core/d2scene/credits.go | 8 +- d2core/hero.go | 47 ++--- d2core/npc.go | 14 +- d2render/animated_entity.go | 310 ++++--------------------------- d2render/animated_entity_test.go | 4 +- d2render/d2mapengine/region.go | 21 ++- d2render/d2ui/font.go | 5 +- 17 files changed, 513 insertions(+), 681 deletions(-) create mode 100644 d2asset/composite.go delete mode 100644 d2asset/paperdoll.go delete mode 100644 d2asset/paperdoll_manager.go diff --git a/README.md b/README.md index 75c4a427..1ae9ba4d 100644 --- a/README.md +++ b/README.md @@ -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 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 ![Main Menu](docs/MainMenuSS.png) diff --git a/d2asset/animation.go b/d2asset/animation.go index 0379135f..b4fe0345 100644 --- a/d2asset/animation.go +++ b/d2asset/animation.go @@ -3,8 +3,11 @@ package d2asset import ( "errors" "image/color" + "math" + "github.com/OpenDiablo2/D2Shared/d2data/d2datadict" "github.com/OpenDiablo2/D2Shared/d2data/d2dc6" + "github.com/OpenDiablo2/D2Shared/d2data/d2dcc" "github.com/OpenDiablo2/D2Shared/d2helper" "github.com/OpenDiablo2/OpenDiablo2/d2corehelper" @@ -37,6 +40,7 @@ type Animation struct { frameIndex int directionIndex int lastFrameTime float64 + playedCount int compositeMode ebiten.CompositeMode colorMod color.Color @@ -46,19 +50,81 @@ type Animation struct { 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) { animation := &Animation{ playLength: 1.0, playLoop: true, } - for frameIndex, frame := range dc6.Frames { - image, err := ebiten.NewImage(int(frame.Width), int(frame.Height), ebiten.FilterNearest) + for frameIndex, dc6Frame := range dc6.Frames { + image, err := ebiten.NewImage(int(dc6Frame.Width), int(dc6Frame.Height), ebiten.FilterNearest) if err != nil { return nil, err } - if err := image.ReplacePixels(frame.ColorData()); err != nil { + if err := image.ReplacePixels(dc6Frame.ColorData()); err != nil { return nil, err } @@ -69,10 +135,10 @@ func createAnimationFromDC6(dc6 *d2dc6.DC6File) (*Animation, error) { direction := animation.directions[directionIndex] direction.frames = append(direction.frames, &animationFrame{ - width: int(frame.Width), - height: int(frame.Height), - offsetX: int(frame.OffsetX), - offsetY: int(frame.OffsetY), + width: int(dc6Frame.Width), + height: int(dc6Frame.Height), + offsetX: int(dc6Frame.OffsetX), + offsetY: int(dc6Frame.OffsetY), image: image, }) } @@ -101,6 +167,7 @@ func (a *Animation) Advance(elapsed float64) error { case playModeForward: a.frameIndex++ if a.frameIndex >= frameCount { + a.playedCount++ if a.playLoop { a.frameIndex = 0 } else { @@ -111,6 +178,7 @@ func (a *Animation) Advance(elapsed float64) error { case playModeBackward: a.frameIndex-- if a.frameIndex < 0 { + a.playedCount++ if a.playLoop { a.frameIndex = frameCount - 1 } else { @@ -233,6 +301,10 @@ func (a *Animation) SetPlayLoop(loop bool) { a.playLoop = true } +func (a *Animation) SetPlaySpeed(playSpeed float64) { + a.SetPlayLength(playSpeed * float64(a.GetFrameCount())) +} + func (a *Animation) SetPlayLength(playLength float64) { a.playLength = playLength a.lastFrameTime = 0 @@ -246,6 +318,14 @@ func (a *Animation) SetColorMod(color color.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) { if blend { a.compositeMode = ebiten.CompositeModeLighter diff --git a/d2asset/animation_manager.go b/d2asset/animation_manager.go index 115e2914..5f2e2511 100644 --- a/d2asset/animation_manager.go +++ b/d2asset/animation_manager.go @@ -1,5 +1,12 @@ package d2asset +import ( + "errors" + "fmt" + "path/filepath" + "strings" +) + type animationManager struct { cache *cache } @@ -8,22 +15,47 @@ func createAnimationManager() *animationManager { return &animationManager{cache: createCache(AnimationBudget)} } -func (sm *animationManager) loadAnimation(animationPath, palettePath string) (*Animation, error) { - cachePath := animationPath + palettePath +func (sm *animationManager) loadAnimation(animationPath, palettePath string, transparency int) (*Animation, error) { + cachePath := fmt.Sprintf("%s;%s;%d", animationPath, palettePath, transparency) if animation, found := sm.cache.retrieve(cachePath); found { return animation.(*Animation).clone(), nil } - dc6, err := loadDC6(animationPath, palettePath) - if err != nil { + var animation *Animation + 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 } - animation, err := createAnimationFromDC6(dc6) - if err != nil { - return nil, err - } - - sm.cache.insert(cachePath, animation.clone(), 1) - return animation, err + return animation, nil } diff --git a/d2asset/asset_manager.go b/d2asset/asset_manager.go index bd93fe3f..dbd59855 100644 --- a/d2asset/asset_manager.go +++ b/d2asset/asset_manager.go @@ -18,7 +18,6 @@ const ( // In counts PaletteBudget = 64 - PaperdollBudget = 64 AnimationBudget = 64 ) @@ -31,7 +30,6 @@ type assetManager struct { archiveManager *archiveManager fileManager *fileManager paletteManager *paletteManager - paperdollManager *paperdollManager animationManager *animationManager } @@ -46,7 +44,6 @@ func Initialize(config *d2corecommon.Configuration) error { archiveManager = createArchiveManager(config) fileManager = createFileManager(config, archiveManager) paletteManager = createPaletteManager() - paperdollManager = createPaperdollManager() animationManager = createAnimationManager() ) @@ -54,7 +51,6 @@ func Initialize(config *d2corecommon.Configuration) error { archiveManager, fileManager, paletteManager, - paperdollManager, animationManager, } @@ -78,29 +74,19 @@ func LoadFile(filePath string) ([]byte, 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 { 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) { - if singleton == 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 LoadComposite(object *d2datadict.ObjectLookupRecord, palettePath string) (*Composite, error) { + return createComposite(object, palettePath), nil } func loadPalette(palettePath string) (*d2datadict.PaletteRec, error) { @@ -130,7 +116,7 @@ func loadDC6(dc6Path, palettePath string) (*d2dc6.DC6File, error) { return &dc6, nil } -func LoadDCC(dccPath string) (*d2dcc.DCC, error) { +func loadDCC(dccPath string) (*d2dcc.DCC, error) { dccData, err := LoadFile(dccPath) if err != nil { return nil, err @@ -139,7 +125,7 @@ func LoadDCC(dccPath string) (*d2dcc.DCC, error) { return d2dcc.LoadDCC(dccData) } -func LoadCOF(cofPath string) (*d2cof.COF, error) { +func loadCOF(cofPath string) (*d2cof.COF, error) { cofData, err := LoadFile(cofPath) if err != nil { return nil, err diff --git a/d2asset/composite.go b/d2asset/composite.go new file mode 100644 index 00000000..48e72add --- /dev/null +++ b/d2asset/composite.go @@ -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") +} diff --git a/d2asset/paperdoll.go b/d2asset/paperdoll.go deleted file mode 100644 index b4a8c04f..00000000 --- a/d2asset/paperdoll.go +++ /dev/null @@ -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 -} diff --git a/d2asset/paperdoll_manager.go b/d2asset/paperdoll_manager.go deleted file mode 100644 index 78f6a029..00000000 --- a/d2asset/paperdoll_manager.go +++ /dev/null @@ -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 -} diff --git a/d2audio/audio_provider.go b/d2audio/audio_provider.go index 71be4a0f..0a077b39 100644 --- a/d2audio/audio_provider.go +++ b/d2audio/audio_provider.go @@ -45,7 +45,10 @@ func (v *Manager) PlayBGM(song string) { 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)) if err != nil { log.Fatal(err) diff --git a/d2audio/sound_effect.go b/d2audio/sound_effect.go index fd4d042e..eeb7ba49 100644 --- a/d2audio/sound_effect.go +++ b/d2audio/sound_effect.go @@ -24,7 +24,12 @@ func CreateSoundEffect(sfx string, context *audio.Context, volume float64) *Soun } else { soundFile = sfx } - audioData := d2asset.MustLoadFile(soundFile) + + audioData, err := d2asset.LoadFile(soundFile) + if err != nil { + panic(err) + } + d, err := wav.Decode(context, audio.BytesReadSeekCloser(audioData)) if err != nil { log.Fatal(err) diff --git a/d2core/d2scene/blizzard_intro.go b/d2core/d2scene/blizzard_intro.go index fc885d4e..e0b0932d 100644 --- a/d2core/d2scene/blizzard_intro.go +++ b/d2core/d2scene/blizzard_intro.go @@ -21,7 +21,10 @@ func CreateBlizzardIntro(sceneProvider d2coreinterface.SceneProvider) *BlizzardI func (v *BlizzardIntro) Load() []func() { return []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) }, } diff --git a/d2core/d2scene/credits.go b/d2core/d2scene/credits.go index d1186d7c..506c6c98 100644 --- a/d2core/d2scene/credits.go +++ b/d2core/d2scene/credits.go @@ -87,8 +87,12 @@ func (v *Credits) Load() []func() { v.uiManager.AddWidget(&v.exitButton) }, func() { - fileData, _ := dh.Utf16BytesToString(d2asset.MustLoadFile(d2resource.CreditsText)[2:]) - v.creditsText = strings.Split(fileData, "\r\n") + fileData, err := d2asset.LoadFile(d2resource.CreditsText) + if err != nil { + panic(err) + } + creditData, _ := dh.Utf16BytesToString(fileData[2:]) + v.creditsText = strings.Split(creditData, "\r\n") for i := range v.creditsText { v.creditsText[i] = strings.Trim(v.creditsText[i], " ") } diff --git a/d2core/hero.go b/d2core/hero.go index b2159c97..3c9e45f1 100644 --- a/d2core/hero.go +++ b/d2core/hero.go @@ -2,41 +2,42 @@ package d2core import ( "github.com/OpenDiablo2/D2Shared/d2common/d2enum" + "github.com/OpenDiablo2/D2Shared/d2common/d2resource" "github.com/OpenDiablo2/D2Shared/d2data/d2datadict" "github.com/OpenDiablo2/OpenDiablo2/d2render" "github.com/hajimehoshi/ebiten" ) type Hero struct { - AnimatedEntity d2render.AnimatedEntity + AnimatedEntity *d2render.AnimatedEntity Equipment CharacterEquipment mode d2enum.AnimationMode direction int } func CreateHero(x, y int32, direction int, heroType d2enum.Hero, equipment CharacterEquipment) *Hero { - result := &Hero{ - AnimatedEntity: d2render.CreateAnimatedEntity(x, y, &d2datadict.ObjectLookupRecord{ - Mode: d2enum.AnimationModePlayerNeutral.String(), - Base: "/data/global/chars", - Token: heroType.GetToken(), - Class: equipment.RightHand.GetWeaponClass(), - SH: equipment.Shield.GetItemCode(), - // TODO: Offhand class? - HD: equipment.Head.GetArmorClass(), - TR: equipment.Torso.GetArmorClass(), - LG: equipment.Legs.GetArmorClass(), - RA: equipment.RightArm.GetArmorClass(), - LA: equipment.LeftArm.GetArmorClass(), - RH: equipment.RightHand.GetItemCode(), - LH: equipment.LeftHand.GetItemCode(), - }, - d2enum.Units, - ), - Equipment: equipment, - mode: d2enum.AnimationModePlayerTownNeutral, - direction: direction, + object := &d2datadict.ObjectLookupRecord{ + Mode: d2enum.AnimationModePlayerNeutral.String(), + Base: "/data/global/chars", + Token: heroType.GetToken(), + Class: equipment.RightHand.GetWeaponClass(), + SH: equipment.Shield.GetItemCode(), + // TODO: Offhand class? + HD: equipment.Head.GetArmorClass(), + TR: equipment.Torso.GetArmorClass(), + LG: equipment.Legs.GetArmorClass(), + RA: equipment.RightArm.GetArmorClass(), + LA: equipment.LeftArm.GetArmorClass(), + RH: equipment.RightHand.GetItemCode(), + LH: equipment.LeftHand.GetItemCode(), } + + 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) return result } @@ -47,6 +48,8 @@ func (v *Hero) Advance(tickTime float64) { v.AnimatedEntity.LocationY != v.AnimatedEntity.TargetY { v.AnimatedEntity.Step(tickTime) } + + v.AnimatedEntity.Advance(tickTime) } func (v *Hero) Render(target *ebiten.Image, offsetX, offsetY int) { diff --git a/d2core/npc.go b/d2core/npc.go index 2a1946a9..c077d57c 100644 --- a/d2core/npc.go +++ b/d2core/npc.go @@ -2,24 +2,26 @@ package d2core import ( "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/OpenDiablo2/d2render" "github.com/hajimehoshi/ebiten" ) type NPC struct { - AnimatedEntity d2render.AnimatedEntity + AnimatedEntity *d2render.AnimatedEntity HasPaths bool Paths []d2common.Path path int } func CreateNPC(x, y int32, object *d2datadict.ObjectLookupRecord, direction int) *NPC { - result := &NPC{ - AnimatedEntity: d2render.CreateAnimatedEntity(x, y, object, d2enum.Units), - HasPaths: false, + entity, err := d2render.CreateAnimatedEntity(x, y, object, d2resource.PaletteUnits) + if err != nil { + panic(err) } + + result := &NPC{AnimatedEntity: entity, HasPaths: false} result.AnimatedEntity.SetMode(object.Mode, object.Class, direction) return result } @@ -68,4 +70,6 @@ func (v *NPC) Advance(tickTime float64) { v.AnimatedEntity.LocationY != v.AnimatedEntity.TargetY { v.AnimatedEntity.Step(tickTime) } + + v.AnimatedEntity.Advance(tickTime) } diff --git a/d2render/animated_entity.go b/d2render/animated_entity.go index 209f4462..dcc05ef2 100644 --- a/d2render/animated_entity.go +++ b/d2render/animated_entity.go @@ -1,326 +1,83 @@ package d2render import ( - "errors" - "fmt" - "image" - "log" "math" "math/rand" - "strings" "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/d2dcc" "github.com/OpenDiablo2/D2Shared/d2helper" "github.com/OpenDiablo2/OpenDiablo2/d2asset" "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 type AnimatedEntity struct { LocationX float64 LocationY float64 TileX, TileY int // Coordinates of the tile the unit is within 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 weaponClass string - lastFrameTime float64 - framesToAnimate int - animationSpeed float64 direction int - currentFrame int offsetX, offsetY int32 - object *d2datadict.ObjectLookupRecord - layerCache []LayerCacheEntry - drawOrder [][]d2enum.CompositeType TargetX float64 TargetY float64 action int32 - repetitions int32 + repetitions int + + composite *d2asset.Composite } // CreateAnimatedEntity creates an instance of AnimatedEntity -func CreateAnimatedEntity(x, y int32, object *d2datadict.ObjectLookupRecord, palette d2enum.PaletteType) AnimatedEntity { - result := AnimatedEntity{ - base: object.Base, - token: object.Token, - object: object, - palette: palette, - layerCache: make([]LayerCacheEntry, d2enum.CompositeTypeMax), +func CreateAnimatedEntity(x, y int32, object *d2datadict.ObjectLookupRecord, palettePath string) (*AnimatedEntity, error) { + composite, err := d2asset.LoadComposite(object, palettePath) + if err != nil { + return nil, err } - 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) - result.TileY = int(result.LocationY / 5) - result.subcellX = 1 + math.Mod(result.LocationX, 5) - result.subcellY = 1 + math.Mod(result.LocationY, 5) + entity := &AnimatedEntity{composite: composite} + entity.LocationX = float64(x) + entity.LocationY = float64(y) + 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 -func (v *AnimatedEntity) SetMode(animationMode, weaponClass string, direction int) { - 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 +func (v *AnimatedEntity) SetMode(animationMode, weaponClass string, direction int) error { v.animationMode = animationMode - v.weaponClass = weaponClass 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) -} - -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) + err := v.composite.SetMode(animationMode, weaponClass, direction) 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") - result, err = d2asset.LoadDCC(dccPath) - if err != nil { - return nil, err - } + err = v.composite.SetMode(animationMode, "HTH", direction) } - return result, nil + return err } // If an npc has a path to pause at each location. // Waits for animation to end and all repetitions to be exhausted. func (v AnimatedEntity) Wait() bool { - // currentFrame might skip the final frame if framesToAdd doesn't match up, - // bail immediately after the last repetition if that happens. - return v.repetitions < 0 || (v.repetitions == 0 && v.currentFrame >= v.framesToAnimate-1) + return v.composite.GetPlayedCount() > v.repetitions } // Render draws this animated entity onto the target 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 localY := ((v.subcellX + v.subcellY) * 8) - 5 - - if v.drawOrder == nil { - return - } - 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) - } + v.composite.Render( + target, + int(v.offsetX)+offsetX+int(localX), + int(v.offsetY)+offsetY+int(localY), + ) } func (v AnimatedEntity) GetDirection() int { @@ -365,8 +122,7 @@ func (v *AnimatedEntity) Step(tickTime float64) { v.TileY = int(v.LocationY / 5) if v.LocationX == v.TargetX && v.LocationY == v.TargetY { - - v.repetitions = 3 + rand.Int31n(5) + v.repetitions = 3 + rand.Intn(5) newAnimationMode := d2enum.AnimationModeObjectNeutral // TODO: Figure out what 1-3 are for, 4 is correct. switch v.action { @@ -381,10 +137,10 @@ func (v *AnimatedEntity) Step(tickTime float64) { v.repetitions = 0 } + v.composite.ResetPlayedCount() if v.animationMode != newAnimationMode.String() { 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() } - newDirection := angleToDirection(float64(angle), v.Cof.NumberOfDirections) + newDirection := angleToDirection(float64(angle), v.composite.GetDirectionCount()) if newDirection != v.GetDirection() || newAnimationMode != v.animationMode { v.SetMode(newAnimationMode, v.weaponClass, newDirection) } @@ -428,8 +184,8 @@ func angleToDirection(angle float64, numberOfDirections int) int { return newDirection } -func (v *AnimatedEntity) Advance(tickTime float64) { - +func (v *AnimatedEntity) Advance(elapsed float64) { + v.composite.Advance(elapsed) } func (v *AnimatedEntity) GetPosition() (float64, float64) { diff --git a/d2render/animated_entity_test.go b/d2render/animated_entity_test.go index d7c0f28e..3396a079 100644 --- a/d2render/animated_entity_test.go +++ b/d2render/animated_entity_test.go @@ -1,8 +1,9 @@ package d2render import ( - "github.com/stretchr/testify/assert" "testing" + + "github.com/stretchr/testify/assert" ) func TestAngleToDirection_16Directions(t *testing.T) { @@ -61,7 +62,6 @@ func TestAngleToDirection_1Direction(t *testing.T) { } } - func TestAngleToDirection_0Directions(t *testing.T) { angle := 0.0 for i := 0; i < 120; i++ { diff --git a/d2render/d2mapengine/region.go b/d2render/d2mapengine/region.go index f096c484..ddbbb57a 100644 --- a/d2render/d2mapengine/region.go +++ b/d2render/d2mapengine/region.go @@ -13,6 +13,7 @@ import ( "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/d2ds1" "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 { 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...) } } @@ -72,7 +78,11 @@ func loadRegion(seed int64, tileOffsetX, tileOffsetY int, levelType d2enum.Regio } 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{ Left: tileOffsetX, Top: tileOffsetY, @@ -133,9 +143,12 @@ func (mr *MapRegion) loadEntities() []MapEntity { } case d2datadict.ObjectTypeItem: 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) - entities = append(entities, &entity) + entities = append(entities, entity) } } } diff --git a/d2render/d2ui/font.go b/d2render/d2ui/font.go index b3cc16ee..cc4c640b 100644 --- a/d2render/d2ui/font.go +++ b/d2render/d2ui/font.go @@ -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 result.fontSprite, _ = d2render.LoadSprite(font+".dc6", palettePath) 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 { panic("No woo :(") }