mirror of
https://github.com/OpenDiablo2/OpenDiablo2
synced 2024-09-18 09:15:59 -04:00
9a8e16c411
Loop through neutral animation a random number of repetitions before moving on. Only run the skill animation once, it is not designed for looping.
416 lines
13 KiB
Go
416 lines
13 KiB
Go
package d2render
|
|
|
|
import (
|
|
"fmt"
|
|
"github.com/OpenDiablo2/D2Shared/d2common/d2enum"
|
|
"github.com/OpenDiablo2/D2Shared/d2common/d2interface"
|
|
"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/hajimehoshi/ebiten"
|
|
"log"
|
|
"math"
|
|
"math/rand"
|
|
"strings"
|
|
)
|
|
|
|
var DccLayerNames = []string{"HD", "TR", "LG", "RA", "LA", "RH", "LH", "SH", "S1", "S2", "S3", "S4", "S5", "S6", "S7", "S8"}
|
|
|
|
type LayerCacheEntry struct {
|
|
frames []*ebiten.Image
|
|
compositeMode ebiten.CompositeMode
|
|
offsetX, offsetY int32
|
|
}
|
|
|
|
// AnimatedEntity represents an entity on the map that can be animated
|
|
type AnimatedEntity struct {
|
|
fileProvider d2interface.FileProvider
|
|
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
|
|
//frameLocations []d2common.Rectangle
|
|
object *d2datadict.ObjectLookupRecord
|
|
layerCache []LayerCacheEntry
|
|
drawOrder [][]d2enum.CompositeType
|
|
TargetX float64
|
|
TargetY float64
|
|
action int32
|
|
repetitions int32
|
|
}
|
|
|
|
// CreateAnimatedEntity creates an instance of AnimatedEntity
|
|
func CreateAnimatedEntity(x, y int32, object *d2datadict.ObjectLookupRecord, fileProvider d2interface.FileProvider, palette d2enum.PaletteType) AnimatedEntity {
|
|
result := AnimatedEntity{
|
|
fileProvider: fileProvider,
|
|
base: object.Base,
|
|
token: object.Token,
|
|
object: object,
|
|
palette: palette,
|
|
layerCache: make([]LayerCacheEntry, d2enum.CompositeTypeMax),
|
|
//frameLocations: []d2common.Rectangle{},
|
|
}
|
|
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)
|
|
|
|
return result
|
|
}
|
|
|
|
// 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)
|
|
v.Cof = d2cof.LoadCOF(cofPath, v.fileProvider)
|
|
if v.Cof.NumberOfDirections == 0 || v.Cof.NumberOfLayers == 0 || v.Cof.FramesPerDirection == 0 {
|
|
return
|
|
}
|
|
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]
|
|
v.dccLayers[layerName] = v.LoadLayer(layerName, v.fileProvider)
|
|
if !v.dccLayers[layerName].IsValid() {
|
|
continue
|
|
}
|
|
}
|
|
|
|
v.updateFrameCache()
|
|
}
|
|
|
|
func (v *AnimatedEntity) LoadLayer(layer string, fileProvider d2interface.FileProvider) d2dcc.DCC {
|
|
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 d2dcc.DCC{}
|
|
}
|
|
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 := d2dcc.LoadDCC(dccPath, fileProvider)
|
|
if !result.IsValid() {
|
|
dccPath = fmt.Sprintf("%s/%s/%s/%s%s%s%s%s.dcc", v.base, v.token, layer, v.token, layer, layerName, v.animationMode, "HTH")
|
|
result = d2dcc.LoadDCC(dccPath, fileProvider)
|
|
}
|
|
return result
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// 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].frames == nil || v.currentFrame >= len(v.layerCache[layerIdx].frames) || v.layerCache[layerIdx].frames[v.currentFrame] == nil {
|
|
continue
|
|
}
|
|
opts := &ebiten.DrawImageOptions{}
|
|
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
|
|
if err := target.DrawImage(v.layerCache[layerIdx].frames[v.currentFrame], opts); err != nil {
|
|
log.Panic(err.Error())
|
|
}
|
|
}
|
|
}
|
|
|
|
func (v *AnimatedEntity) updateFrameCache() {
|
|
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.IsValid() {
|
|
continue
|
|
}
|
|
v.layerCache[layerType].frames = make([]*ebiten.Image, v.framesToAnimate)
|
|
|
|
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, actualWidth*actualHeight*4)
|
|
|
|
for animationIdx := 0; animationIdx < v.framesToAnimate; animationIdx++ {
|
|
for i := 0; i < int(actualWidth*actualHeight); i++ {
|
|
pixels[(i*4)+3] = 0
|
|
}
|
|
if animationIdx >= len(dccLayer.Directions[dccDirection].Frames) {
|
|
log.Printf("Invalid animation index of %d for animated entity", animationIdx)
|
|
continue
|
|
}
|
|
|
|
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)
|
|
pixels[(actualX*4)+(actualY*int(actualWidth)*4)] = color.R
|
|
pixels[(actualX*4)+(actualY*int(actualWidth)*4)+1] = color.G
|
|
pixels[(actualX*4)+(actualY*int(actualWidth)*4)+2] = color.B
|
|
pixels[(actualX*4)+(actualY*int(actualWidth)*4)+3] = transparency
|
|
}
|
|
}
|
|
v.layerCache[layerType].frames[animationIdx], _ = ebiten.NewImage(int(actualWidth), int(actualHeight), ebiten.FilterNearest)
|
|
_ = v.layerCache[layerType].frames[animationIdx].ReplacePixels(pixels)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (v AnimatedEntity) GetDirection() int {
|
|
return v.direction
|
|
}
|
|
|
|
func (v *AnimatedEntity) getStepLength(tickTime float64) (float64, float64) {
|
|
speed := 6.0
|
|
length := tickTime * speed
|
|
|
|
angle := 359 - d2helper.GetAngleBetween(
|
|
v.LocationX,
|
|
v.LocationY,
|
|
v.TargetX,
|
|
v.TargetY,
|
|
)
|
|
radians := (math.Pi / 180.0) * float64(angle)
|
|
oneStepX := length * math.Cos(radians)
|
|
oneStepY := length * math.Sin(radians)
|
|
return oneStepX, oneStepY
|
|
}
|
|
|
|
func (v *AnimatedEntity) Step(tickTime float64) {
|
|
stepX, stepY := v.getStepLength(tickTime)
|
|
|
|
if d2helper.AlmostEqual(v.LocationX, v.TargetX, stepX) {
|
|
v.LocationX = v.TargetX
|
|
}
|
|
if d2helper.AlmostEqual(v.LocationY, v.TargetY, stepY) {
|
|
v.LocationY = v.TargetY
|
|
}
|
|
if v.LocationX != v.TargetX {
|
|
v.LocationX += stepX
|
|
}
|
|
if v.LocationY != v.TargetY {
|
|
v.LocationY += stepY
|
|
}
|
|
|
|
v.subcellX = 1 + math.Mod(v.LocationX, 5)
|
|
v.subcellY = 1 + math.Mod(v.LocationY, 5)
|
|
v.TileX = int(v.LocationX / 5)
|
|
v.TileY = int(v.LocationY / 5)
|
|
|
|
if v.LocationX == v.TargetX && v.LocationY == v.TargetY {
|
|
|
|
v.repetitions = 3 + rand.Int31n(5)
|
|
newAnimationMode := d2enum.AnimationModeObjectNeutral
|
|
// TODO: Figure out what 1-3 are for, 4 is correct.
|
|
switch v.action {
|
|
case 1:
|
|
newAnimationMode = d2enum.AnimationModeMonsterNeutral
|
|
case 2:
|
|
newAnimationMode = d2enum.AnimationModeMonsterNeutral
|
|
case 3:
|
|
newAnimationMode = d2enum.AnimationModeMonsterNeutral
|
|
case 4:
|
|
newAnimationMode = d2enum.AnimationModeMonsterSkill1
|
|
v.repetitions = 0
|
|
}
|
|
|
|
if v.animationMode != newAnimationMode.String() {
|
|
v.SetMode(newAnimationMode.String(), v.weaponClass, v.direction)
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
// SetTarget sets target coordinates and changes animation based on proximity and direction
|
|
func (v *AnimatedEntity) SetTarget(tx, ty float64, action int32) {
|
|
angle := 359 - d2helper.GetAngleBetween(
|
|
v.LocationX,
|
|
v.LocationY,
|
|
tx,
|
|
ty,
|
|
)
|
|
|
|
v.action = action
|
|
// TODO: Check if is in town and if is player.
|
|
newAnimationMode := d2enum.AnimationModeMonsterWalk.String()
|
|
if tx != v.LocationX || ty != v.LocationY {
|
|
v.TargetX, v.TargetY = tx, ty
|
|
newAnimationMode = d2enum.AnimationModeMonsterWalk.String()
|
|
}
|
|
|
|
newDirection := angleToDirection(float64(angle), v.Cof.NumberOfDirections)
|
|
if newDirection != v.GetDirection() || newAnimationMode != v.animationMode {
|
|
v.SetMode(newAnimationMode, v.weaponClass, newDirection)
|
|
}
|
|
}
|
|
|
|
func angleToDirection(angle float64, numberOfDirections int) int {
|
|
if numberOfDirections == 0 {
|
|
return 0
|
|
}
|
|
|
|
degreesPerDirection := 360.0 / float64(numberOfDirections)
|
|
offset := 45.0 - (degreesPerDirection / 2)
|
|
newDirection := int((angle - offset) / degreesPerDirection)
|
|
if newDirection >= numberOfDirections {
|
|
newDirection = newDirection - numberOfDirections
|
|
} else if newDirection < 0 {
|
|
newDirection = numberOfDirections + newDirection
|
|
}
|
|
|
|
return newDirection
|
|
}
|