diff --git a/Common/Sprite.go b/Common/Sprite.go index 5b434530..011065b8 100644 --- a/Common/Sprite.go +++ b/Common/Sprite.go @@ -61,7 +61,7 @@ func CreateSprite(data []byte, palette Palette) *Sprite { result.Frames = make([]*SpriteFrame, totalFrames) for i := uint32(0); i < totalFrames; i++ { dataPointer = framePointers[i] - result.Frames[i] = &SpriteFrame{} + result.Frames[i] = &SpriteFrame{Loaded: false} result.Frames[i].Flip = binary.LittleEndian.Uint32(data[dataPointer : dataPointer+4]) dataPointer += 4 result.Frames[i].Width = binary.LittleEndian.Uint32(data[dataPointer : dataPointer+4]) @@ -130,6 +130,9 @@ func CreateSprite(data []byte, palette Palette) *Sprite { // GetSize returns the size of the sprite func (v *Sprite) GetSize() (uint32, uint32) { frame := v.Frames[uint32(v.Frame)+(uint32(v.Direction)*v.FramesPerDirection)] + for frame.Loaded == false { + time.Sleep(time.Millisecond) + } return frame.Width, frame.Height } @@ -148,11 +151,29 @@ func (v *Sprite) updateAnimation() { } } +// GetFrameSize returns the size of the specific frame +func (v *Sprite) GetFrameSize(frame int) (width, height uint32) { + for v.Frames[frame].Loaded == false { + time.Sleep(time.Millisecond) + } + width = v.Frames[frame].Width + height = v.Frames[frame].Height + return +} + +// GetTotalFrames returns the number of frames in this sprite (for all directions) +func (v *Sprite) GetTotalFrames() int { + return len(v.Frames) +} + // Draw draws the sprite onto the target func (v *Sprite) Draw(target *ebiten.Image) { v.updateAnimation() opts := &ebiten.DrawImageOptions{} frame := v.Frames[uint32(v.Frame)+(uint32(v.Direction)*v.FramesPerDirection)] + for frame.Loaded == false { + time.Sleep(time.Millisecond) + } opts.GeoM.Translate( float64(int32(v.X)+frame.OffsetX), float64((int32(v.Y) - int32(frame.Height) + frame.OffsetY)), @@ -165,9 +186,6 @@ func (v *Sprite) Draw(target *ebiten.Image) { if v.ColorMod != nil { opts.ColorM = ColorToColorM(v.ColorMod) } - for frame.Image == nil { - time.Sleep(time.Millisecond) - } target.DrawImage(frame.Image, opts) } @@ -193,7 +211,7 @@ func (v *Sprite) DrawSegments(target *ebiten.Image, xSegments, ySegments, offset if v.ColorMod != nil { opts.ColorM = ColorToColorM(v.ColorMod) } - for frame.Image == nil { + for frame.Loaded == false { time.Sleep(time.Millisecond) } target.DrawImage(frame.Image, opts) diff --git a/Scenes/SceneMainMenu.go b/Scenes/SceneMainMenu.go index 1331232f..0223a00d 100644 --- a/Scenes/SceneMainMenu.go +++ b/Scenes/SceneMainMenu.go @@ -2,6 +2,7 @@ package Scenes import ( "image/color" + "os" "github.com/essial/OpenDiablo2/Common" "github.com/essial/OpenDiablo2/Palettes" @@ -88,14 +89,19 @@ func (v *MainMenu) Load() []func() { v.diabloLogoRightBack.MoveTo(400, 120) }, func() { - v.exitDiabloButton = UI.CreateButton(v.fileProvider, "EXIT DIABLO II") + v.exitDiabloButton = UI.CreateButton(UI.ButtonTypeWide, v.fileProvider, "EXIT DIABLO II") v.exitDiabloButton.MoveTo(264, 535) v.exitDiabloButton.SetVisible(false) + v.exitDiabloButton.OnActivated(func() { v.onExitButtonClicked() }) v.uiManager.AddWidget(v.exitDiabloButton) }, } } +func (v *MainMenu) onExitButtonClicked() { + os.Exit(0) +} + // Unload unloads the data for the main menu func (v *MainMenu) Unload() { diff --git a/UI/Button.go b/UI/Button.go index 5740cfc7..2e058ba5 100644 --- a/UI/Button.go +++ b/UI/Button.go @@ -1,6 +1,7 @@ package UI import ( + "image" "image/color" "github.com/essial/OpenDiablo2/Common" @@ -9,44 +10,160 @@ import ( "github.com/hajimehoshi/ebiten" ) +// ButtonType defines the type of button +type ButtonType int + +const ( + ButtonTypeWide ButtonType = 1 + ButtonTypeMedium ButtonType = 2 + ButtonTypeNarrow ButtonType = 3 + ButtonTypeCancel ButtonType = 4 + ButtonTypeTall ButtonType = 5 + ButtonTypeShort ButtonType = 6 + + // Game UI + + ButtonTypeSkill ButtonType = 7 + ButtonTypeRun ButtonType = 8 + ButtonTypeMenu ButtonType = 9 + ButtonTypeGoldCoin ButtonType = 10 + ButtonTypeClose ButtonType = 11 + ButtonTypeSecondaryInvHand ButtonType = 12 + ButtonTypeMinipanelCharacter ButtonType = 13 + ButtonTypeMinipanelInventory ButtonType = 14 + ButtonTypeMinipanelSkill ButtonType = 15 + ButtonTypeMinipanelAutomap ButtonType = 16 + ButtonTypeMinipanelMessage ButtonType = 17 + ButtonTypeMinipanelQuest ButtonType = 18 + ButtonTypeMinipanelMen ButtonType = 19 +) + +// ButtonLayout defines the type of buttons +type ButtonLayout struct { + XSegments int //1 + YSegments int // 1 + ResourceName string + PaletteName Palettes.Palette + Toggleable bool // false + BaseFrame int // 0 + DisabledFrame int // -1 + FontPath string // ResourcePaths.FontExocet10 + ClickableRect *image.Rectangle // nil + AllowFrameChange bool // true +} + +// ButtonLayouts define the type of buttons you can have +var ButtonLayouts = map[ButtonType]ButtonLayout{ + ButtonTypeWide: {2, 1, ResourcePaths.WideButtonBlank, Palettes.Units, false, 0, -1, ResourcePaths.FontExocet10, nil, true}, + ButtonTypeShort: {1, 1, ResourcePaths.ShortButtonBlank, Palettes.Units, false, 0, -1, ResourcePaths.FontExocet8, nil, true}, + /* + {eButtonType.Wide, new ButtonLayout { XSegments = 2, ResourceName = ResourcePaths.WideButtonBlank, PaletteName = Palettes.Units } }, + {eButtonType.Medium, new ButtonLayout{ ResourceName = ResourcePaths.MediumButtonBlank, PaletteName = Palettes.Units } }, + {eButtonType.Narrow, new ButtonLayout { ResourceName = ResourcePaths.NarrowButtonBlank, PaletteName = Palettes.Units } }, + {eButtonType.Tall, new ButtonLayout { ResourceName = ResourcePaths.TallButtonBlank, PaletteName = Palettes.Units } }, + + {eButtonType.Cancel, new ButtonLayout { ResourceName = ResourcePaths.CancelButton, PaletteName = Palettes.Units } }, + // Minipanel + {eButtonType.MinipanelCharacter, new ButtonLayout { ResourceName = ResourcePaths.MinipanelButton, PaletteName = Palettes.Units, BaseFrame = 0 } }, + {eButtonType.MinipanelInventory, new ButtonLayout { ResourceName = ResourcePaths.MinipanelButton, PaletteName = Palettes.Units, BaseFrame = 2 } }, + {eButtonType.MinipanelSkill, new ButtonLayout { ResourceName = ResourcePaths.MinipanelButton, PaletteName = Palettes.Units, BaseFrame = 4 } }, + {eButtonType.MinipanelAutomap, new ButtonLayout { ResourceName = ResourcePaths.MinipanelButton, PaletteName = Palettes.Units, BaseFrame = 8 } }, + {eButtonType.MinipanelMessage, new ButtonLayout { ResourceName = ResourcePaths.MinipanelButton, PaletteName = Palettes.Units, BaseFrame = 10 } }, + {eButtonType.MinipanelQuest, new ButtonLayout { ResourceName = ResourcePaths.MinipanelButton, PaletteName = Palettes.Units, BaseFrame = 12 } }, + {eButtonType.MinipanelMenu, new ButtonLayout { ResourceName = ResourcePaths.MinipanelButton, PaletteName = Palettes.Units, BaseFrame = 14 } }, + + {eButtonType.SecondaryInvHand, new ButtonLayout { ResourceName = ResourcePaths.InventoryWeaponsTab, PaletteName = Palettes.Units, ClickableRect = new Rectangle(0, 0, 0, 20), AllowFrameChange = false } }, + {eButtonType.Run, new ButtonLayout { ResourceName = ResourcePaths.RunButton, PaletteName = Palettes.Units, Toggleable = true } }, + {eButtonType.Menu, new ButtonLayout { ResourceName = ResourcePaths.MenuButton, PaletteName = Palettes.Units, Toggleable = true } }, + {eButtonType.GoldCoin, new ButtonLayout { ResourceName = ResourcePaths.GoldCoinButton, PaletteName = Palettes.Units } }, + {eButtonType.Close, new ButtonLayout { ResourceName = ResourcePaths.SquareButton, PaletteName = Palettes.Units, BaseFrame = 10 } }, + {eButtonType.Skill, new ButtonLayout { ResourceName = ResourcePaths.AddSkillButton, PaletteName = Palettes.Units, DisabledFrame = 2 + */ +} + // 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 + enabled bool + x, y int + width, height uint32 + visible bool + pressed bool + toggled bool + fileProvider Common.FileProvider + normalImage *ebiten.Image + pressedImage *ebiten.Image + toggledImage *ebiten.Image + pressedToggledImage *ebiten.Image + disabledImage *ebiten.Image + onClick func() } // CreateButton creates an instance of Button -func CreateButton(fileProvider Common.FileProvider, text string) *Button { +func CreateButton(buttonType ButtonType, fileProvider Common.FileProvider, text string) *Button { result := &Button{ fileProvider: fileProvider, - width: 272, - height: 35, + width: 0, + height: 0, 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 + buttonLayout := ButtonLayouts[buttonType] + font := GetFont(buttonLayout.FontPath, buttonLayout.PaletteName, fileProvider) buttonSprite := fileProvider.LoadSprite(ResourcePaths.WideButtonBlank, Palettes.Units) + totalButtonTypes := buttonSprite.GetTotalFrames() / (buttonLayout.XSegments * buttonLayout.YSegments) + for i := 0; i < buttonLayout.XSegments; i++ { + w, _ := buttonSprite.GetFrameSize(i) + result.width += w + } + for i := 0; i < buttonLayout.YSegments; i++ { + _, h := buttonSprite.GetFrameSize(i * buttonLayout.YSegments) + result.height += h + } + + result.normalImage, _ = ebiten.NewImage(int(result.width), int(result.height), ebiten.FilterNearest) + result.pressedImage, _ = ebiten.NewImage(int(result.width), int(result.height), ebiten.FilterNearest) + textWidth, textHeight := font.GetTextMetrics(text) + textX := (result.width / 2) - (textWidth / 2) + textY := (result.height / 2) - (textHeight / 2) + 5 buttonSprite.MoveTo(0, 0) buttonSprite.Blend = true - buttonSprite.DrawSegments(result.normalImage, 2, 1, 0) + buttonSprite.DrawSegments(result.normalImage, 2, 1, buttonLayout.BaseFrame) 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) + if buttonLayout.AllowFrameChange { + if totalButtonTypes > 1 { + buttonSprite.DrawSegments(result.pressedImage, 2, 1, buttonLayout.BaseFrame+1) + font.Draw(int(textX-2), int(textY+2), text, color.RGBA{100, 100, 100, 255}, result.pressedImage) + } + if totalButtonTypes > 2 { + buttonSprite.DrawSegments(result.toggledImage, 2, 1, buttonLayout.BaseFrame+2) + font.Draw(int(textX), int(textY), text, color.RGBA{100, 100, 100, 255}, result.toggledImage) + } + if totalButtonTypes > 3 { + buttonSprite.DrawSegments(result.pressedToggledImage, 2, 1, buttonLayout.BaseFrame+3) + font.Draw(int(textX), int(textY), text, color.RGBA{100, 100, 100, 255}, result.pressedToggledImage) + } + if buttonLayout.DisabledFrame != -1 { + buttonSprite.DrawSegments(result.disabledImage, 2, 1, buttonLayout.DisabledFrame) + font.Draw(int(textX), int(textY), text, color.RGBA{100, 100, 100, 255}, result.disabledImage) + } + } return result } +// OnActivated defines the callback handler for the activate event +func (v *Button) OnActivated(callback func()) { + v.onClick = callback +} + +// Activate calls the on activated callback handler, if any +func (v *Button) Activate() { + if v.onClick == nil { + return + } + v.onClick() +} + // Draw renders the button func (v *Button) Draw(target *ebiten.Image) { opts := &ebiten.DrawImageOptions{ @@ -54,11 +171,18 @@ func (v *Button) Draw(target *ebiten.Image) { Filter: ebiten.FilterNearest, } opts.GeoM.Translate(float64(v.x), float64(v.y)) - if v.pressed { + + if !v.enabled { + target.DrawImage(v.disabledImage, opts) + } else if v.toggled && v.pressed { + target.DrawImage(v.pressedToggledImage, opts) + } else if v.pressed { target.DrawImage(v.pressedImage, opts) - return + } else if v.toggled { + target.DrawImage(v.toggledImage, opts) + } else { + target.DrawImage(v.normalImage, opts) } - target.DrawImage(v.normalImage, opts) } // GetEnabled returns the enabled state @@ -96,3 +220,13 @@ func (v *Button) GetVisible() bool { func (v *Button) SetVisible(visible bool) { v.visible = visible } + +// GetPressed returns the pressed state of the button +func (v *Button) GetPressed() bool { + return v.pressed +} + +// SetPressed sets the pressed state of the button +func (v *Button) SetPressed(pressed bool) { + v.pressed = pressed +} diff --git a/UI/Manager.go b/UI/Manager.go index 82d22ed3..5fbee60d 100644 --- a/UI/Manager.go +++ b/UI/Manager.go @@ -22,6 +22,7 @@ type Manager struct { widgets []Widget cursorSprite *Common.Sprite cursorButtons CursorButton + pressedIndex int CursorX int CursorY int } @@ -29,6 +30,7 @@ type Manager struct { // CreateManager creates a new instance of a UI manager func CreateManager(provider Common.FileProvider) *Manager { result := &Manager{ + pressedIndex: -1, widgets: make([]Widget, 0), cursorSprite: provider.LoadSprite(ResourcePaths.CursorDefault, Palettes.Units), } @@ -38,6 +40,7 @@ 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.pressedIndex = -1 } // AddWidget adds a widget to the UI manager @@ -69,6 +72,55 @@ func (v *Manager) Update() { v.cursorButtons |= CursorButtonRight } v.CursorX, v.CursorY = ebiten.CursorPosition() + if v.CursorButtonPressed(CursorButtonLeft) { + found := false + for i, widget := range v.widgets { + if !widget.GetVisible() || !widget.GetEnabled() { + continue + } + wx, wy := widget.GetLocation() + ww, wh := widget.GetSize() + if v.CursorX >= wx && v.CursorX <= wx+int(ww) && v.CursorY >= wy && v.CursorY <= wy+int(wh) { + widget.SetPressed(true) + if v.pressedIndex == -1 { + found = true + v.pressedIndex = i + } else if v.pressedIndex > -1 && v.pressedIndex != i { + v.widgets[i].SetPressed(false) + } else { + v.widgets[i].SetPressed(true) + found = true + } + break + } else { + widget.SetPressed(false) + } + } + if !found { + if v.pressedIndex > -1 { + v.widgets[v.pressedIndex].SetPressed(false) + } else { + v.pressedIndex = -2 + } + } + } else { + if v.pressedIndex > -1 { + widget := v.widgets[v.pressedIndex] + wx, wy := widget.GetLocation() + ww, wh := widget.GetSize() + if v.CursorX >= wx && v.CursorX <= wx+int(ww) && v.CursorY >= wy && v.CursorY <= wy+int(wh) { + widget.Activate() + } + } else { + for _, widget := range v.widgets { + if !widget.GetVisible() || !widget.GetEnabled() { + continue + } + widget.SetPressed(false) + } + } + v.pressedIndex = -1 + } } // CursorButtonPressed determines if the specified button has been pressed diff --git a/UI/Widget.go b/UI/Widget.go index 7db196b1..ec008d55 100644 --- a/UI/Widget.go +++ b/UI/Widget.go @@ -9,4 +9,8 @@ type Widget interface { Common.Drawable GetEnabled() bool SetEnabled(enabled bool) + SetPressed(pressed bool) + GetPressed() bool + OnActivated(callback func()) + Activate() }