diff --git a/d2common/d2interface/surface.go b/d2common/d2interface/surface.go index 5d9ae8c8..eeb6db9e 100644 --- a/d2common/d2interface/surface.go +++ b/d2common/d2interface/surface.go @@ -23,6 +23,8 @@ type Surface interface { PushTranslation(x, y int) PushBrightness(brightness float64) Render(surface Surface) error + // Renders a section of the surface enclosed by bounds + RenderSection(surface Surface, bound image.Rectangle) error ReplacePixels(pixels []byte) error Screenshot() *image.RGBA } diff --git a/d2core/d2asset/animation.go b/d2core/d2asset/animation.go index f240fd07..8af714c3 100644 --- a/d2core/d2asset/animation.go +++ b/d2core/d2asset/animation.go @@ -2,6 +2,7 @@ package d2asset import ( "errors" + "image" "image/color" "math" @@ -38,6 +39,7 @@ type animationDirection struct { frames []*animationFrame } +// Animation has directionality, play modes, and frame counting type Animation struct { directions []*animationDirection frameIndex int @@ -67,6 +69,7 @@ func CreateAnimationFromDCC(dcc *d2dcc.DCC, palette *d2dat.DATPalette, transpare for _, dccFrame := range dccDirection.Frames { minX, minY := math.MaxInt32, math.MaxInt32 maxX, maxY := math.MinInt32, math.MinInt32 + for _, dccFrame := range dccDirection.Frames { minX = d2common.MinInt(minX, dccFrame.Box.Left) minY = d2common.MinInt(minY, dccFrame.Box.Top) @@ -78,8 +81,10 @@ func CreateAnimationFromDCC(dcc *d2dcc.DCC, palette *d2dat.DATPalette, transpare 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 { palColor := palette.Colors[paletteIndex] offset := (x + y*frameWidth) * 4 @@ -91,12 +96,12 @@ func CreateAnimationFromDCC(dcc *d2dcc.DCC, palette *d2dat.DATPalette, transpare } } - image, err := d2render.NewSurface(frameWidth, frameHeight, d2interface.FilterNearest) + sfc, err := d2render.NewSurface(frameWidth, frameHeight, d2interface.FilterNearest) if err != nil { return nil, err } - if err := image.ReplacePixels(pixels); err != nil { + if err := sfc.ReplacePixels(pixels); err != nil { return nil, err } @@ -110,7 +115,7 @@ func CreateAnimationFromDCC(dcc *d2dcc.DCC, palette *d2dat.DATPalette, transpare height: dccFrame.Height, offsetX: minX, offsetY: minY, - image: image, + image: sfc, }) } } @@ -126,7 +131,7 @@ func CreateAnimationFromDC6(dc6 *d2dc6.DC6, palette *d2dat.DATPalette) (*Animati } for frameIndex, dc6Frame := range dc6.Frames { - image, err := d2render.NewSurface(int(dc6Frame.Width), int(dc6Frame.Height), d2interface.FilterNearest) + sfc, err := d2render.NewSurface(int(dc6Frame.Width), int(dc6Frame.Height), d2interface.FilterNearest) if err != nil { return nil, err } @@ -166,17 +171,19 @@ func CreateAnimationFromDC6(dc6 *d2dc6.DC6, palette *d2dat.DATPalette) (*Animati } colorData := make([]byte, dc6Frame.Width*dc6Frame.Height*4) + for i := 0; i < int(dc6Frame.Width*dc6Frame.Height); i++ { if indexData[i] < 1 { // TODO: Is this == -1 or < 1? continue } + colorData[i*4] = palette.Colors[indexData[i]].R colorData[i*4+1] = palette.Colors[indexData[i]].G colorData[i*4+2] = palette.Colors[indexData[i]].B colorData[i*4+3] = 0xff } - if err := image.ReplacePixels(colorData); err != nil { + if err := sfc.ReplacePixels(colorData); err != nil { return nil, err } @@ -191,7 +198,7 @@ func CreateAnimationFromDC6(dc6 *d2dc6.DC6, palette *d2dat.DATPalette) (*Animati height: int(dc6Frame.Height), offsetX: int(dc6Frame.OffsetX), offsetY: int(dc6Frame.OffsetY), - image: image, + image: sfc, }) } @@ -279,6 +286,19 @@ func (a *Animation) RenderFromOrigin(target d2interface.Surface) error { return a.Render(target) } +// RenderSection renders the section of the animation frame enclosed by bounds +func (a *Animation) RenderSection(sfc d2interface.Surface, bound image.Rectangle) error { + direction := a.directions[a.directionIndex] + frame := direction.frames[a.frameIndex] + + sfc.PushTranslation(frame.offsetX, frame.offsetY) + sfc.PushCompositeMode(a.compositeMode) + sfc.PushColor(a.colorMod) + defer sfc.PopN(3) + return sfc.RenderSection(frame.image, bound) +} + +// GetFrameSize gets the Size(width, height) of a indexed frame. func (a *Animation) GetFrameSize(frameIndex int) (int, int, error) { direction := a.directions[a.directionIndex] if frameIndex >= len(direction.frames) { @@ -289,11 +309,13 @@ func (a *Animation) GetFrameSize(frameIndex int) (int, int, error) { return frame.width, frame.height, nil } +// GetCurrentFrameSize gets the Size(width, height) of the current frame. func (a *Animation) GetCurrentFrameSize() (int, int) { width, height, _ := a.GetFrameSize(a.frameIndex) return width, height } +// GetFrameBounds gets maximum Size(width, height) of all frame. func (a *Animation) GetFrameBounds() (int, int) { maxWidth, maxHeight := 0, 0 @@ -306,27 +328,33 @@ func (a *Animation) GetFrameBounds() (int, int) { return maxWidth, maxHeight } +// GetCurrentFrame gets index of current frame in animation func (a *Animation) GetCurrentFrame() int { return a.frameIndex } +// GetFrameCount gets number of frames in animation func (a *Animation) GetFrameCount() int { direction := a.directions[a.directionIndex] return len(direction.frames) } +// IsOnFirstFrame gets if the animation on its first frame func (a *Animation) IsOnFirstFrame() bool { return a.frameIndex == 0 } +// IsOnLastFrame gets if the animation on its last frame func (a *Animation) IsOnLastFrame() bool { return a.frameIndex == a.GetFrameCount()-1 } +// GetDirectionCount gets the number of animation direction func (a *Animation) GetDirectionCount() int { return len(a.directions) } +// SetDirection places the animation in the direction of an animation func (a *Animation) SetDirection(directionIndex int) error { if directionIndex >= 64 { return errors.New("invalid direction index") @@ -337,10 +365,12 @@ func (a *Animation) SetDirection(directionIndex int) error { return nil } +// GetDirection get the current animation direction func (a *Animation) GetDirection() int { return a.directionIndex } +// SetCurrentFrame sets animation at a specific frame func (a *Animation) SetCurrentFrame(frameIndex int) error { if frameIndex >= a.GetFrameCount() { return errors.New("invalid frame index") @@ -351,29 +381,35 @@ func (a *Animation) SetCurrentFrame(frameIndex int) error { return nil } +// Rewind animation to beginning func (a *Animation) Rewind() { a.SetCurrentFrame(0) } +// PlayForward plays animation forward func (a *Animation) PlayForward() { a.playMode = playModeForward a.lastFrameTime = 0 } +// PlayBackward plays animation backward func (a *Animation) PlayBackward() { a.playMode = playModeBackward a.lastFrameTime = 0 } +// Pause animation func (a *Animation) Pause() { a.playMode = playModePause a.lastFrameTime = 0 } +// SetPlayLoop sets whether to loop the animation func (a *Animation) SetPlayLoop(loop bool) { a.playLoop = loop } +// SetPlaySpeed sets play speed of the animation func (a *Animation) SetPlaySpeed(playSpeed float64) { a.SetPlayLength(playSpeed * float64(a.GetFrameCount())) } @@ -391,14 +427,17 @@ func (a *Animation) SetColorMod(color color.Color) { a.colorMod = color } +// GetPlayedCount gets the number of times the application played func (a *Animation) GetPlayedCount() int { return a.playedCount } +// ResetPlayedCount resets the play count func (a *Animation) ResetPlayedCount() { a.playedCount = 0 } +// SetBlend sets the animation alpha blending status func (a *Animation) SetBlend(blend bool) { if blend { a.compositeMode = d2enum.CompositeModeLighter diff --git a/d2core/d2render/ebiten/ebiten_surface.go b/d2core/d2render/ebiten/ebiten_surface.go index 58627657..6aeb0b3c 100644 --- a/d2core/d2render/ebiten/ebiten_surface.go +++ b/d2core/d2render/ebiten/ebiten_surface.go @@ -76,6 +76,22 @@ func (s *ebitenSurface) Render(sfc d2interface.Surface) error { return s.image.DrawImage(img, opts) } +// Renders the section of the animation frame enclosed by bounds +func (s *ebitenSurface) RenderSection(sfc d2interface.Surface, bound image.Rectangle) error { + opts := &ebiten.DrawImageOptions{CompositeMode: s.stateCurrent.mode} + opts.GeoM.Translate(float64(s.stateCurrent.x), float64(s.stateCurrent.y)) + opts.Filter = s.stateCurrent.filter + if s.stateCurrent.color != nil { + opts.ColorM = ColorToColorM(s.stateCurrent.color) + } + if s.stateCurrent.brightness != 0 { + opts.ColorM.ChangeHSV(0, 1, s.stateCurrent.brightness) + } + + var img = sfc.(*ebitenSurface).image + return s.image.DrawImage(img.SubImage(bound).(*ebiten.Image), opts) +} + func (s *ebitenSurface) DrawText(format string, params ...interface{}) { ebitenutil.DebugPrintAt(s.image, fmt.Sprintf(format, params...), s.stateCurrent.x, s.stateCurrent.y) } diff --git a/d2core/d2ui/sprite.go b/d2core/d2ui/sprite.go index 8c2fc4f7..ad71d3c0 100644 --- a/d2core/d2ui/sprite.go +++ b/d2core/d2ui/sprite.go @@ -2,6 +2,7 @@ package d2ui import ( "errors" + "image" "image/color" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" @@ -10,6 +11,7 @@ import ( "github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset" ) +// Sprite is a positioned visual object. type Sprite struct { x int y int @@ -32,14 +34,25 @@ func (s *Sprite) Render(target d2interface.Surface) error { target.PushTranslation(s.x, s.y-frameHeight) defer target.Pop() + return s.animation.Render(target) } +// RenderSection renders the section of the sprite enclosed by bounds +func (s *Sprite) RenderSection(sfc d2interface.Surface, bound image.Rectangle) error { + sfc.PushTranslation(s.x, s.y-bound.Dy()) + defer sfc.Pop() + + return s.animation.RenderSection(sfc, bound) +} + func (s *Sprite) RenderSegmented(target d2interface.Surface, segmentsX, segmentsY, frameOffset int) error { var currentY int + for y := 0; y < segmentsY; y++ { var currentX int var maxFrameHeight int + for x := 0; x < segmentsX; x++ { if err := s.animation.SetCurrentFrame(x + y*segmentsX + frameOffset*segmentsX*segmentsY); err != nil { return err @@ -48,6 +61,7 @@ func (s *Sprite) RenderSegmented(target d2interface.Surface, segmentsX, segments target.PushTranslation(s.x+currentX, s.y+currentY) err := s.animation.Render(target) target.Pop() + if err != nil { return err } @@ -63,75 +77,93 @@ func (s *Sprite) RenderSegmented(target d2interface.Surface, segmentsX, segments return nil } +// SetPosition places the sprite in 2D func (s *Sprite) SetPosition(x, y int) { s.x = x s.y = y } +// GetPosition retrieves the 2D position of the sprite func (s *Sprite) GetPosition() (int, int) { return s.x, s.y } +// GetFrameSize gets the Size(width, height) of a indexed frame. func (s *Sprite) GetFrameSize(frameIndex int) (int, int, error) { return s.animation.GetFrameSize(frameIndex) } +// GetCurrentFrameSize gets the Size(width, height) of the current frame. func (s *Sprite) GetCurrentFrameSize() (int, int) { return s.animation.GetCurrentFrameSize() } +// GetFrameBounds gets maximum Size(width, height) of all frame. func (s *Sprite) GetFrameBounds() (int, int) { return s.animation.GetFrameBounds() } +// GetCurrentFrame gets index of current frame in animation func (s *Sprite) GetCurrentFrame() int { return s.animation.GetCurrentFrame() } +// GetFrameCount gets number of frames in animation func (s *Sprite) GetFrameCount() int { return s.animation.GetFrameCount() } +// IsOnFirstFrame gets if the animation on its first frame func (s *Sprite) IsOnFirstFrame() bool { return s.animation.IsOnFirstFrame() } +// IsOnLastFrame gets if the animation on its last frame func (s *Sprite) IsOnLastFrame() bool { return s.animation.IsOnLastFrame() } +// GetDirectionCount gets the number of animation direction func (s *Sprite) GetDirectionCount() int { return s.animation.GetDirectionCount() } +// SetDirection places the animation in the direction of an animation func (s *Sprite) SetDirection(directionIndex int) error { return s.animation.SetDirection(directionIndex) } +// GetDirection get the current animation direction func (s *Sprite) GetDirection() int { return s.animation.GetDirection() } +// SetCurrentFrame sets animation at a specific frame func (s *Sprite) SetCurrentFrame(frameIndex int) error { return s.animation.SetCurrentFrame(frameIndex) } +// Rewind sprite to beginning func (s *Sprite) Rewind() { s.animation.SetCurrentFrame(0) } +// PlayForward plays sprite forward func (s *Sprite) PlayForward() { s.animation.PlayForward() } +// PlayBackward play sprites backward func (s *Sprite) PlayBackward() { s.animation.PlayBackward() } +// Pause animation func (s *Sprite) Pause() { s.animation.Pause() } +// SetPlayLoop sets whether to loop the animation func (s *Sprite) SetPlayLoop(loop bool) { s.animation.SetPlayLoop(loop) } @@ -148,6 +180,7 @@ func (s *Sprite) SetColorMod(color color.Color) { s.animation.SetColorMod(color) } +// SetBlend sets the animation alpha blending status func (s *Sprite) SetBlend(blend bool) { s.animation.SetBlend(blend) } diff --git a/d2game/d2player/game_controls.go b/d2game/d2player/game_controls.go index aee315b7..2b23524d 100644 --- a/d2game/d2player/game_controls.go +++ b/d2game/d2player/game_controls.go @@ -1,6 +1,7 @@ package d2player import ( + "image" "image/color" "log" "math" @@ -16,7 +17,6 @@ import ( "github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2mapengine" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2mapentity" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2maprenderer" - "github.com/OpenDiablo2/OpenDiablo2/d2core/d2render" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2ui" ) @@ -48,14 +48,10 @@ type GameControls struct { FreeCam bool lastMouseX int lastMouseY int - lastHealthPercent float64 - lastManaPercent float64 // UI globeSprite *d2ui.Sprite hpManaStatusSprite *d2ui.Sprite - hpStatusBar d2interface.Surface - manaStatusBar d2interface.Surface mainPanel *d2ui.Sprite menuButton *d2ui.Sprite skillIcon *d2ui.Sprite @@ -256,8 +252,6 @@ func (g *GameControls) Load() { animation, _ = d2asset.LoadAnimation(d2resource.HealthManaIndicator, d2resource.PaletteSky) g.hpManaStatusSprite, _ = d2ui.LoadSprite(animation) - g.hpStatusBar, _ = d2render.NewSurface(globeWidth, globeHeight, d2interface.FilterNearest) - animation, _ = d2asset.LoadAnimation(d2resource.GamePanels, d2resource.PaletteSky) g.mainPanel, _ = d2ui.LoadSprite(animation) @@ -369,28 +363,18 @@ func (g *GameControls) Render(target d2interface.Surface) { g.mainPanel.SetPosition(offset, height) g.mainPanel.Render(target) + // Health status bar + healthPercent := float64(g.hero.Stats.Health) / float64(g.hero.Stats.MaxHealth) + hpBarHeight := int(healthPercent * float64(globeHeight)) + g.hpManaStatusSprite.SetCurrentFrame(0) + g.hpManaStatusSprite.SetPosition(offset+30, height-13) + g.hpManaStatusSprite.RenderSection(target, image.Rect(0, globeHeight-hpBarHeight, globeWidth, globeHeight)) + // Left globe g.globeSprite.SetCurrentFrame(0) g.globeSprite.SetPosition(offset+28, height-5) g.globeSprite.Render(target) - // Health status bar - healthPercent := float64(g.hero.Stats.Health) / float64(g.hero.Stats.MaxHealth) - hpBarHeight := int(healthPercent * float64(globeHeight)) - if g.lastHealthPercent != healthPercent { - g.hpStatusBar, _ = d2render.NewSurface(globeWidth, hpBarHeight, d2interface.FilterNearest) - g.hpManaStatusSprite.SetCurrentFrame(0) - g.hpStatusBar.PushTranslation(0, hpBarHeight) - - g.hpManaStatusSprite.Render(g.hpStatusBar) - g.hpStatusBar.Pop() - g.lastHealthPercent = healthPercent - } - - target.PushTranslation(30, 508+(globeHeight-hpBarHeight)) - target.Render(g.hpStatusBar) - target.Pop() - offset += w // Left skill @@ -460,29 +444,19 @@ func (g *GameControls) Render(target d2interface.Surface) { g.mainPanel.SetPosition(offset, height) g.mainPanel.Render(target) + // Mana status bar + manaPercent := float64(g.hero.Stats.Mana) / float64(g.hero.Stats.MaxMana) + manaBarHeight := int(manaPercent * float64(globeHeight)) + g.hpManaStatusSprite.SetCurrentFrame(1) + g.hpManaStatusSprite.SetPosition(offset+7, height-12) + g.hpManaStatusSprite.RenderSection(target, image.Rect(0, globeHeight-manaBarHeight, globeWidth, globeHeight)) + // Right globe g.globeSprite.SetCurrentFrame(1) g.globeSprite.SetPosition(offset+8, height-8) g.globeSprite.Render(target) g.globeSprite.Render(target) - // Mana status bar - manaPercent := float64(g.hero.Stats.Mana) / float64(g.hero.Stats.MaxMana) - manaBarHeight := int(manaPercent * float64(globeHeight)) - if manaPercent != g.lastManaPercent { - g.manaStatusBar, _ = d2render.NewSurface(globeWidth, manaBarHeight, d2interface.FilterNearest) - g.hpManaStatusSprite.SetCurrentFrame(1) - - g.manaStatusBar.PushTranslation(0, manaBarHeight) - g.hpManaStatusSprite.Render(g.manaStatusBar) - g.manaStatusBar.Pop() - - g.lastManaPercent = manaPercent - } - target.PushTranslation(offset+8, 508+(globeHeight-manaBarHeight)) - target.Render(g.manaStatusBar) - target.Pop() - if g.isZoneTextShown { g.zoneChangeText.SetPosition(width/2, height/4) g.zoneChangeText.Render(target)