diff --git a/Common/Drawable.go b/Common/Drawable.go index bd301b81..4559ee2f 100644 --- a/Common/Drawable.go +++ b/Common/Drawable.go @@ -5,9 +5,9 @@ import "github.com/hajimehoshi/ebiten" // Drawable represents an instance that can be drawn type Drawable interface { Draw(target *ebiten.Image) - GetSize() (uint32, uint32) + GetSize() (width, height uint32) MoveTo(x, y int) - GetLocation() (int, int) + GetLocation() (x, y int) GetVisible() bool - SetVisible(bool) + SetVisible(visible bool) } diff --git a/Common/Sprite.go b/Common/Sprite.go index 4560bbbb..5b434530 100644 --- a/Common/Sprite.go +++ b/Common/Sprite.go @@ -12,7 +12,7 @@ import ( type Sprite struct { Directions uint32 FramesPerDirection uint32 - Frames []SpriteFrame + Frames []*SpriteFrame X, Y int Frame, Direction uint8 Blend bool @@ -34,6 +34,7 @@ type SpriteFrame struct { Length uint32 ImageData []int16 Image *ebiten.Image + Loaded bool } // CreateSprite creates an instance of a sprite @@ -57,10 +58,10 @@ func CreateSprite(data []byte, palette Palette) *Sprite { framePointers[i] = binary.LittleEndian.Uint32(data[dataPointer : dataPointer+4]) dataPointer += 4 } - result.Frames = make([]SpriteFrame, totalFrames) + result.Frames = make([]*SpriteFrame, totalFrames) for i := uint32(0); i < totalFrames; i++ { dataPointer = framePointers[i] - + result.Frames[i] = &SpriteFrame{} result.Frames[i].Flip = binary.LittleEndian.Uint32(data[dataPointer : dataPointer+4]) dataPointer += 4 result.Frames[i].Width = binary.LittleEndian.Uint32(data[dataPointer : dataPointer+4]) @@ -120,6 +121,7 @@ func CreateSprite(data []byte, palette Palette) *Sprite { newData[(ii*4)+3] = 0xFF } result.Frames[ix].Image.ReplacePixels(newData) + result.Frames[ix].Loaded = true }(i, dataPointer) } return result @@ -156,11 +158,16 @@ func (v *Sprite) Draw(target *ebiten.Image) { float64((int32(v.Y) - int32(frame.Height) + frame.OffsetY)), ) if v.Blend { - opts.CompositeMode = ebiten.CompositeModeLighter + opts.CompositeMode = ebiten.CompositeModeSourceOver + } else { + opts.CompositeMode = ebiten.CompositeModeSourceOver } if v.ColorMod != nil { opts.ColorM = ColorToColorM(v.ColorMod) } + for frame.Image == nil { + time.Sleep(time.Millisecond) + } target.DrawImage(frame.Image, opts) } @@ -180,10 +187,15 @@ func (v *Sprite) DrawSegments(target *ebiten.Image, xSegments, ySegments, offset ) if v.Blend { opts.CompositeMode = ebiten.CompositeModeLighter + } else { + opts.CompositeMode = ebiten.CompositeModeSourceOver } if v.ColorMod != nil { opts.ColorM = ColorToColorM(v.ColorMod) } + for frame.Image == nil { + time.Sleep(time.Millisecond) + } target.DrawImage(frame.Image, opts) xOffset += int32(frame.Width) biggestYOffset = MaxInt32(biggestYOffset, int32(frame.Height)) diff --git a/Engine.go b/Engine.go index 37e352d7..2b388b9d 100644 --- a/Engine.go +++ b/Engine.go @@ -104,8 +104,11 @@ func (v *Engine) mapMpqFiles() { } } +var mutex sync.Mutex + // LoadFile loads a file from the specified mpq and returns the data as a byte array func (v *Engine) LoadFile(fileName string) []byte { + mutex.Lock() // TODO: May want to cache some things if performance becomes an issue mpqFile := v.Files[strings.ToLower(fileName)] mpq, err := MPQ.Load(mpqFile) @@ -120,7 +123,7 @@ func (v *Engine) LoadFile(fileName string) []byte { mpqStream := MPQ.CreateStream(mpq, blockTableEntry, fileName) result := make([]byte, blockTableEntry.UncompressedFileSize) mpqStream.Read(result, 0, blockTableEntry.UncompressedFileSize) - + mutex.Unlock() return result } diff --git a/Scenes/SceneMainMenu.go b/Scenes/SceneMainMenu.go index 6656aaee..1331232f 100644 --- a/Scenes/SceneMainMenu.go +++ b/Scenes/SceneMainMenu.go @@ -23,6 +23,7 @@ type MainMenu struct { diabloLogoRight *Common.Sprite diabloLogoLeftBack *Common.Sprite diabloLogoRightBack *Common.Sprite + exitDiabloButton *UI.Button copyrightLabel *UI.Label copyrightLabel2 *UI.Label showTrademarkScreen bool @@ -48,14 +49,14 @@ func (v *MainMenu) Load() []func() { v.copyrightLabel = UI.CreateLabel(v.fileProvider, ResourcePaths.FontFormal12, Palettes.Static) v.copyrightLabel.Alignment = UI.LabelAlignCenter v.copyrightLabel.SetText("Diablo 2 is © Copyright 2000-2016 Blizzard Entertainment") - v.copyrightLabel.ColorMod = color.RGBA{188, 168, 140, 255} + v.copyrightLabel.Color = color.RGBA{188, 168, 140, 255} v.copyrightLabel.MoveTo(400, 500) }, func() { v.copyrightLabel2 = UI.CreateLabel(v.fileProvider, ResourcePaths.FontFormal12, Palettes.Static) v.copyrightLabel2.Alignment = UI.LabelAlignCenter v.copyrightLabel2.SetText("All Rights Reserved.") - v.copyrightLabel2.ColorMod = color.RGBA{188, 168, 140, 255} + v.copyrightLabel2.Color = color.RGBA{188, 168, 140, 255} v.copyrightLabel2.MoveTo(400, 525) }, func() { @@ -86,6 +87,12 @@ func (v *MainMenu) Load() []func() { v.diabloLogoRightBack = v.fileProvider.LoadSprite(ResourcePaths.Diablo2LogoBlackRight, Palettes.Units) v.diabloLogoRightBack.MoveTo(400, 120) }, + func() { + v.exitDiabloButton = UI.CreateButton(v.fileProvider, "EXIT DIABLO II") + v.exitDiabloButton.MoveTo(264, 535) + v.exitDiabloButton.SetVisible(false) + v.uiManager.AddWidget(v.exitDiabloButton) + }, } } @@ -120,6 +127,7 @@ func (v *MainMenu) Update() { if v.uiManager.CursorButtonPressed(UI.CursorButtonLeft) { v.leftButtonHeld = true v.showTrademarkScreen = false + v.exitDiabloButton.SetVisible(true) } return } diff --git a/UI/Button.go b/UI/Button.go index 40a60f41..5740cfc7 100644 --- a/UI/Button.go +++ b/UI/Button.go @@ -1,8 +1,98 @@ package UI -// Button defines an object that acts like a button -type Button interface { - Widget - isPressed() bool - setPressed(bool) +import ( + "image/color" + + "github.com/essial/OpenDiablo2/Common" + "github.com/essial/OpenDiablo2/Palettes" + "github.com/essial/OpenDiablo2/ResourcePaths" + "github.com/hajimehoshi/ebiten" +) + +// Button defines a standard wide UI button +type Button struct { + enabled bool + x, y int + width, height uint32 + visible bool + pressed bool + fileProvider Common.FileProvider + normalImage *ebiten.Image + pressedImage *ebiten.Image +} + +// CreateButton creates an instance of Button +func CreateButton(fileProvider Common.FileProvider, text string) *Button { + result := &Button{ + fileProvider: fileProvider, + width: 272, + height: 35, + visible: true, + enabled: true, + pressed: false, + } + font := GetFont(ResourcePaths.FontExocet10, Palettes.Units, fileProvider) + result.normalImage, _ = ebiten.NewImage(272, 35, ebiten.FilterNearest) + result.pressedImage, _ = ebiten.NewImage(272, 35, ebiten.FilterNearest) + textWidth, textHeight := font.GetTextMetrics(text) + textX := (272 / 2) - (textWidth / 2) + textY := (35 / 2) - (textHeight / 2) + 5 + buttonSprite := fileProvider.LoadSprite(ResourcePaths.WideButtonBlank, Palettes.Units) + buttonSprite.MoveTo(0, 0) + buttonSprite.Blend = true + buttonSprite.DrawSegments(result.normalImage, 2, 1, 0) + font.Draw(int(textX), int(textY), text, color.RGBA{100, 100, 100, 255}, result.normalImage) + buttonSprite.DrawSegments(result.pressedImage, 2, 1, 1) + font.Draw(int(textX-2), int(textY+2), text, color.Black, result.pressedImage) + return result +} + +// Draw renders the button +func (v *Button) Draw(target *ebiten.Image) { + opts := &ebiten.DrawImageOptions{ + CompositeMode: ebiten.CompositeModeSourceAtop, + Filter: ebiten.FilterNearest, + } + opts.GeoM.Translate(float64(v.x), float64(v.y)) + if v.pressed { + target.DrawImage(v.pressedImage, opts) + return + } + target.DrawImage(v.normalImage, opts) +} + +// GetEnabled returns the enabled state +func (v *Button) GetEnabled() bool { + return v.enabled +} + +// SetEnabled sets the enabled state +func (v *Button) SetEnabled(enabled bool) { + v.enabled = enabled +} + +// GetSize returns the size of the button +func (v *Button) GetSize() (uint32, uint32) { + return v.width, v.height +} + +// MoveTo moves the button +func (v *Button) MoveTo(x, y int) { + v.x = x + v.y = y +} + +// GetLocation returns the location of the button +func (v *Button) GetLocation() (x, y int) { + return v.x, v.y +} + +// GetVisible returns the visibility of the button +func (v *Button) GetVisible() bool { + return v.visible +} + +// SetVisible sets the visibility of the button +func (v *Button) SetVisible(visible bool) { + v.visible = visible } diff --git a/UI/Font.go b/UI/Font.go index 5625ba43..0f94dadc 100644 --- a/UI/Font.go +++ b/UI/Font.go @@ -1,8 +1,11 @@ package UI import ( + "image/color" + "github.com/essial/OpenDiablo2/Common" "github.com/essial/OpenDiablo2/Palettes" + "github.com/hajimehoshi/ebiten" ) var fontCache = map[string]*Font{} @@ -15,8 +18,8 @@ type FontSize struct { // Font represents a font type Font struct { - FontSprite *Common.Sprite - Metrics map[uint8]FontSize + fontSprite *Common.Sprite + metrics map[uint8]FontSize } // GetFont creates or loads an existing font @@ -33,9 +36,9 @@ func GetFont(font string, palette Palettes.Palette, fileProvider Common.FileProv // CreateFont creates an instance of a MPQ Font func CreateFont(font string, palette Palettes.Palette, fileProvider Common.FileProvider) *Font { result := &Font{ - Metrics: make(map[uint8]FontSize), + metrics: make(map[uint8]FontSize), } - result.FontSprite = fileProvider.LoadSprite(font+".dc6", palette) + result.fontSprite = fileProvider.LoadSprite(font+".dc6", palette) woo := "Woo!\x01" fontData := fileProvider.LoadFile(font + ".tbl") if string(fontData[0:5]) != woo { @@ -46,7 +49,34 @@ func CreateFont(font string, palette Palettes.Palette, fileProvider Common.FileP Width: fontData[i+3], Height: fontData[i+4], } - result.Metrics[fontData[i+8]] = fontSize + result.metrics[fontData[i+8]] = fontSize } return result } + +// GetTextMetrics returns the size of the specified text +func (v *Font) GetTextMetrics(text string) (width, height uint32) { + width = uint32(0) + height = uint32(0) + for _, ch := range text { + metric := v.metrics[uint8(ch)] + width += uint32(metric.Width) + height = Common.Max(height, uint32(metric.Height)) + } + return +} + +// Draw draws the font on the target surface +func (v *Font) Draw(x, y int, text string, color color.Color, target *ebiten.Image) { + v.fontSprite.ColorMod = color + v.fontSprite.Blend = true + _, height := v.GetTextMetrics(text) + for _, ch := range text { + char := uint8(ch) + metric := v.metrics[char] + v.fontSprite.Frame = char + v.fontSprite.MoveTo(x, y+int(height)) + v.fontSprite.Draw(target) + x += int(metric.Width) + } +} diff --git a/UI/Manager.go b/UI/Manager.go index fe0ebdf9..82d22ed3 100644 --- a/UI/Manager.go +++ b/UI/Manager.go @@ -19,7 +19,7 @@ const ( // Manager represents the UI manager type Manager struct { - widgets []*Widget + widgets []Widget cursorSprite *Common.Sprite cursorButtons CursorButton CursorX int @@ -29,7 +29,7 @@ type Manager struct { // CreateManager creates a new instance of a UI manager func CreateManager(provider Common.FileProvider) *Manager { result := &Manager{ - widgets: make([]*Widget, 0), + widgets: make([]Widget, 0), cursorSprite: provider.LoadSprite(ResourcePaths.CursorDefault, Palettes.Units), } return result @@ -37,16 +37,23 @@ func CreateManager(provider Common.FileProvider) *Manager { // Reset resets the state of the UI manager. Typically called for new scenes func (v *Manager) Reset() { - v.widgets = make([]*Widget, 0) + v.widgets = make([]Widget, 0) } // AddWidget adds a widget to the UI manager -func (v *Manager) AddWidget(widget *Widget) { +func (v *Manager) AddWidget(widget Widget) { v.widgets = append(v.widgets, widget) } // Draw renders all of the UI elements func (v *Manager) Draw(screen *ebiten.Image) { + for _, widget := range v.widgets { + if !widget.GetVisible() { + continue + } + widget.Draw(screen) + } + cx, cy := ebiten.CursorPosition() v.cursorSprite.MoveTo(cx, cy) v.cursorSprite.Draw(screen) diff --git a/UI/UILabel.go b/UI/UILabel.go index 880b8ae0..47da3684 100644 --- a/UI/UILabel.go +++ b/UI/UILabel.go @@ -30,14 +30,14 @@ type Label struct { Alignment LabelAlignment font *Font imageData *ebiten.Image - ColorMod color.Color + Color color.Color } // CreateLabel creates a new instance of a UI label func CreateLabel(provider Common.FileProvider, font string, palette Palettes.Palette) *Label { result := &Label{ Alignment: LabelAlignLeft, - ColorMod: nil, + Color: color.White, font: GetFont(font, palette, provider), } @@ -64,17 +64,6 @@ func (v *Label) Draw(target *ebiten.Image) { target.DrawImage(v.imageData, opts) } -func (v *Label) calculateSize() (uint32, uint32) { - width := uint32(0) - height := uint32(0) - for _, ch := range v.text { - metric := v.font.Metrics[uint8(ch)] - width += uint32(metric.Width) - height = Common.Max(height, uint32(metric.Height)) - } - return width, height -} - // MoveTo moves the label to the specified location func (v *Label) MoveTo(x, y int) { v.X = x @@ -85,20 +74,11 @@ func (v *Label) cacheImage() { if v.imageData != nil { return } - width, height := v.calculateSize() + width, height := v.font.GetTextMetrics(v.text) v.Width = width v.Height = height v.imageData, _ = ebiten.NewImage(int(width), int(height), ebiten.FilterNearest) - x := uint32(0) - v.font.FontSprite.ColorMod = v.ColorMod - for _, ch := range v.text { - char := uint8(ch) - metric := v.font.Metrics[char] - v.font.FontSprite.Frame = char - v.font.FontSprite.MoveTo(int(x), int(height)) - v.font.FontSprite.Draw(v.imageData) - x += uint32(metric.Width) - } + v.font.Draw(0, 0, v.text, v.Color, v.imageData) } // SetText sets the label's text diff --git a/UI/Widget.go b/UI/Widget.go index 0f032b54..7db196b1 100644 --- a/UI/Widget.go +++ b/UI/Widget.go @@ -7,6 +7,6 @@ import ( // Widget defines an object that is a UI widget type Widget interface { Common.Drawable - getEnabled() bool - setEnabled(bool) + GetEnabled() bool + SetEnabled(enabled bool) }