mirror of
https://github.com/OpenDiablo2/OpenDiablo2
synced 2025-02-05 16:17:45 -05:00
parent
1b03e691b9
commit
0ee937f01b
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
259
d2asset/composite.go
Normal file
259
d2asset/composite.go
Normal 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")
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
},
|
||||
}
|
||||
|
@ -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], " ")
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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++ {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 :(")
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user