From 0d691dbffa2a81ec7152c2e033ef3cc8cda673f0 Mon Sep 17 00:00:00 2001 From: Julien Ganichot <2302338+Ganitzsh@users.noreply.github.com> Date: Fri, 13 Nov 2020 21:03:30 +0100 Subject: [PATCH] Key binding menu (#918) * Feat(KeyBindingMenu): Adds dynamic box system with scrollbar * Feat(Hotkeys): WIP Adds a lot of things * Feat(KeyBindingMenu): WIP Adds logic to binding * Feat(KeyBindingMenu): Fixes assignment logic * Feat(KeyBindingMenu): Adds buttons logic * Feat(KeyBindingMenu): Fixes sprites positions+add padding to Box * Feat(KeyBindingMenu): Adds label blinking cap * Feat(KeyBindingMenu): Removes commented func * Feat(KeyBindingMenu): Fixes lint errors and refactors a bit * Feat(KeyBindingMenu): Corrects few minor things from Grave * Feat(KeyBindingMenu): removes forgotten key to string mapping --- d2common/d2enum/game_event.go | 13 + d2common/d2enum/input_key.go | 29 +- d2common/d2interface/animation.go | 1 + d2common/d2resource/resource_paths.go | 16 + d2core/d2asset/animation.go | 6 + d2core/d2gui/box.go | 513 ++++++++++++++++ d2core/d2gui/label.go | 74 ++- d2core/d2gui/label_button.go | 99 +++ d2core/d2gui/layout.go | 27 +- d2core/d2gui/layout_scrollbar.go | 385 ++++++++++++ d2core/d2render/ebiten/ebiten_surface.go | 17 + d2core/d2ui/button.go | 3 +- d2core/d2ui/d2ui.go | 10 + d2core/d2ui/label.go | 11 +- d2core/d2ui/sprite.go | 5 + d2core/d2ui/tooltip.go | 3 +- d2game/d2gamescreen/character_select.go | 5 +- d2game/d2gamescreen/cinematics.go | 3 +- d2game/d2gamescreen/game.go | 8 +- d2game/d2gamescreen/main_menu.go | 19 +- d2game/d2gamescreen/select_hero_class.go | 17 +- d2game/d2player/binding_layout.go | 91 +++ d2game/d2player/escape_menu.go | 147 ++++- d2game/d2player/game_controls.go | 19 +- d2game/d2player/help_overlay.go | 14 +- d2game/d2player/hero_stats_panel.go | 3 +- d2game/d2player/hud.go | 5 +- d2game/d2player/key_binding_menu.go | 733 +++++++++++++++++++++++ d2game/d2player/key_map.go | 249 ++++++-- d2game/d2player/skilltree.go | 3 +- go.mod | 1 + 31 files changed, 2391 insertions(+), 138 deletions(-) create mode 100644 d2core/d2gui/box.go create mode 100644 d2core/d2gui/label_button.go create mode 100644 d2core/d2gui/layout_scrollbar.go create mode 100644 d2game/d2player/binding_layout.go create mode 100644 d2game/d2player/key_binding_menu.go diff --git a/d2common/d2enum/game_event.go b/d2common/d2enum/game_event.go index 9b197ec5..c535fb68 100644 --- a/d2common/d2enum/game_event.go +++ b/d2common/d2enum/game_event.go @@ -26,6 +26,7 @@ const ( FadeAutomap // reduces the brightness of the map (not the players/npcs) TogglePartyOnAutomap // toggles the display of the party members on the automap ToggleNamesOnAutomap // toggles the display of party members names and npcs on the automap + ToggleMiniMap // there can be 16 hotkeys, each hotkey can have a skill assigned UseSkill1 @@ -58,13 +59,25 @@ const ( UseBeltSlot4 SwapWeapons + ToggleChatBox ToggleRunWalk + SayHelp + SayFollowMe + SayThisIsForYou + SayThanks + SaySorry + SayBye + SayNowYouDie + SayRetreat + // these events are fired while a player holds the corresponding key HoldRun HoldStandStill HoldShowGroundItems HoldShowPortraits + TakeScreenShot ClearScreen // closes all active menus/panels + ClearMessages ) diff --git a/d2common/d2enum/input_key.go b/d2common/d2enum/input_key.go index 92f1e2d9..4bd8122e 100644 --- a/d2common/d2enum/input_key.go +++ b/d2common/d2enum/input_key.go @@ -3,28 +3,6 @@ package d2enum // Key represents button on a traditional keyboard. type Key int -// GetString returns a string representing the key -func (k Key) GetString() string { - switch k { - case -1: - return "None" - case KeyControl: - return "Ctrl" - case KeyShift: - return "Shift" - case KeySpace: - return "Space" - case KeyAlt: - return "Alt" - case KeyTab: - return "Tab" - case KeyH: - return "H" - default: - return "Unknown" - } -} - // Input keys const ( Key0 Key = iota @@ -128,9 +106,14 @@ const ( KeyControl KeyShift KeyTilde + KeyMouse3 + KeyMouse4 + KeyMouse5 + KeyMouseWheelUp + KeyMouseWheelDown KeyMin = Key0 - KeyMax = KeyShift + KeyMax = KeyMouseWheelDown ) // KeyMod represents a "modified" key action. This could mean, for example, ctrl-S diff --git a/d2common/d2interface/animation.go b/d2common/d2interface/animation.go index 31aa1375..0971ac5a 100644 --- a/d2common/d2interface/animation.go +++ b/d2common/d2interface/animation.go @@ -13,6 +13,7 @@ type Animation interface { Clone() Animation SetSubLoop(startFrame, EndFrame int) Advance(elapsed float64) error + GetCurrentFrameSurface() Surface Render(target Surface) RenderFromOrigin(target Surface, shadow bool) RenderSection(sfc Surface, bound image.Rectangle) diff --git a/d2common/d2resource/resource_paths.go b/d2common/d2resource/resource_paths.go index 6159c938..f355b04b 100644 --- a/d2common/d2resource/resource_paths.go +++ b/d2common/d2resource/resource_paths.go @@ -111,6 +111,14 @@ const ( HelpYellowBullet = "/data/global/ui/MENU/helpyellowbullet.DC6" HelpWhiteBullet = "/data/global/ui/MENU/helpwhitebullet.DC6" + // Box pieces, used in all in game boxes like npc interaction menu on click, + // the chat window and the key binding menu + BoxPieces = "/data/global/ui/MENU/boxpieces.DC6" + + // TextSlider contains the pieces to build a scrollbar in the + // menus, such as the one in the configure keys menu + TextSlider = "/data/global/ui/MENU/textslid.DC6" + // Issue #685 - used in the mini-panel GameSmallMenuButton = "/data/global/ui/PANEL/menubutton.DC6" SkillIcon = "/data/global/ui/PANEL/Skillicon.DC6" @@ -152,6 +160,14 @@ const ( Checkbox = "/data/global/ui/FrontEnd/clickbox.dc6" Scrollbar = "/data/global/ui/PANEL/scrollbar.dc6" + PopUpLarge = "/data/global/ui/FrontEnd/PopUpLarge.dc6" + PopUpLargest = "/data/global/ui/FrontEnd/PopUpLargest.dc6" + PopUpWide = "/data/global/ui/FrontEnd/PopUpWide.dc6" + PopUpOk = "/data/global/ui/FrontEnd/PopUpOk.dc6" + PopUpOk2 = "/data/global/ui/FrontEnd/PopUpOk.dc6" + PopUpOkCancel2 = "/data/global/ui/FrontEnd/PopUpOkCancel2.dc6" + PopUp340x224 = "/data/global/ui/FrontEnd/PopUp_340x224.dc6" + // --- GAME UI --- PentSpin = "/data/global/ui/CURSOR/pentspin.DC6" diff --git a/d2core/d2asset/animation.go b/d2core/d2asset/animation.go index f6128fff..94fe94af 100644 --- a/d2core/d2asset/animation.go +++ b/d2core/d2asset/animation.go @@ -151,6 +151,12 @@ func (a *Animation) renderShadow(target d2interface.Surface) { target.Render(frame.image) } +// GetCurrentFrameSurface returns the surface for the current frame of the +// animation +func (a *Animation) GetCurrentFrameSurface() d2interface.Surface { + return a.directions[a.directionIndex].frames[a.frameIndex].image +} + // Render renders the animation to the given surface func (a *Animation) Render(target d2interface.Surface) { if a.renderer == nil { diff --git a/d2core/d2gui/box.go b/d2core/d2gui/box.go new file mode 100644 index 00000000..9836d4f3 --- /dev/null +++ b/d2core/d2gui/box.go @@ -0,0 +1,513 @@ +package d2gui + +import ( + "log" + + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2util" + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset" + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2ui" +) + +const ( + boxSpriteHeight = 15 - 5 + boxSpriteWidth = 14 - 2 + + boxBorderSpriteLeftBorderOffset = 4 + boxBorderSpriteRightBorderOffset = 7 + boxBorderSpriteTopBorderSectionOffset = 5 + + minimumAllowedSectionSize = 14 + sectionHeightPercentageOfBox = 0.12 + boxBackgroundColor = 0x000000d0 +) + +const ( + boxCornerTopLeft = iota + boxCornerTopRight + boxTopHorizontalEdge1 + boxTopHorizontalEdge2 + boxTopHorizontalEdge3 + boxTopHorizontalEdge4 + boxTopHorizontalEdge5 + boxTopHorizontalEdge6 + boxCornerBottomLeft + boxCornerBottomRight + boxSideEdge1 + boxSideEdge2 + boxSideEdge3 + boxSideEdge4 + boxSideEdge5 + boxSideEdge6 + boxBottomHorizontalEdge1 + boxBottomHorizontalEdge2 + boxBottomHorizontalEdge3 + boxBottomHorizontalEdge4 + boxBottomHorizontalEdge5 + boxBottomHorizontalEdge6 +) + +// Box takes a content layout and wraps in +// a box +type Box struct { + renderer d2interface.Renderer + asset *d2asset.AssetManager + sprites []*d2ui.Sprite + uiManager *d2ui.UIManager + layout *Layout + contentLayout *Layout + Options []*LabelButton + sfc d2interface.Surface + + x, y int + paddingX, paddingY int + width, height int + disableBorder bool + isOpen bool + title string +} + +// NewBox return a new Box instance +func NewBox( + asset *d2asset.AssetManager, + renderer d2interface.Renderer, + ui *d2ui.UIManager, + contentLayout *Layout, + width, height int, + x, y int, + title string, +) *Box { + return &Box{ + asset: asset, + renderer: renderer, + uiManager: ui, + width: width, + height: height, + contentLayout: contentLayout, + sfc: renderer.NewSurface(width, height), + title: title, + x: x, + y: y, + } +} + +// GetLayout returns the box layout +func (box *Box) GetLayout() *Layout { + return box.layout +} + +// Toggle the visibility state of the menu +func (box *Box) Toggle() { + if box.isOpen { + box.Close() + } else { + box.Open() + } +} + +// SetPadding sets the padding of the box content +func (box *Box) SetPadding(paddingX, paddingY int) { + box.paddingX = paddingX + box.paddingY = paddingY +} + +// Open will set the isOpen value to true +func (box *Box) Open() { + box.isOpen = true +} + +// Close will hide the help overlay +func (box *Box) Close() { + box.isOpen = false +} + +// IsOpen returns whether or not the box is opened +func (box *Box) IsOpen() bool { + return box.isOpen +} + +func (box *Box) setupTopBorder(offsetY int) { + topEdgePiece := []int{ + boxTopHorizontalEdge1, + boxTopHorizontalEdge2, + boxTopHorizontalEdge3, + boxTopHorizontalEdge4, + boxTopHorizontalEdge5, + boxTopHorizontalEdge6, + } + + i := 0 + maxPieces := box.width / boxSpriteWidth + currentX, currentY := box.x, box.y+offsetY + + for { + for _, frameIndex := range topEdgePiece { + f, err := box.uiManager.NewSprite(d2resource.BoxPieces, d2resource.PaletteSky) + if err != nil { + log.Print(err) + } + + err = f.SetCurrentFrame(frameIndex) + if err != nil { + log.Print(err) + } + + f.SetPosition(currentX, currentY) + currentX += boxSpriteWidth + + box.sprites = append(box.sprites, f) + + i++ + if i >= maxPieces { + break + } + } + + if i >= maxPieces { + break + } + } +} +func (box *Box) setupBottomBorder(offsetY int) { + bottomEdgePiece := []int{ + boxBottomHorizontalEdge1, + boxBottomHorizontalEdge2, + boxBottomHorizontalEdge3, + boxBottomHorizontalEdge4, + boxBottomHorizontalEdge5, + boxBottomHorizontalEdge6, + } + + i := 0 + currentX, currentY := box.x, offsetY + maxPieces := box.width / boxSpriteWidth + + for { + for _, frameIndex := range bottomEdgePiece { + f, err := box.uiManager.NewSprite(d2resource.BoxPieces, d2resource.PaletteSky) + if err != nil { + log.Print(err) + } + + err = f.SetCurrentFrame(frameIndex) + if err != nil { + log.Print(err) + } + + f.SetPosition(currentX, currentY) + currentX += boxSpriteWidth + + box.sprites = append(box.sprites, f) + + i++ + if i >= maxPieces { + break + } + } + + if i >= maxPieces { + break + } + } +} + +func (box *Box) setupLeftBorder() { + leftBorderPiece := []int{ + boxSideEdge1, + boxSideEdge2, + boxSideEdge3, + } + + currentX, currentY := box.x-boxBorderSpriteLeftBorderOffset, box.y+boxSpriteHeight + maxPieces := box.height / boxSpriteHeight + i := 0 + + for { + for _, frameIndex := range leftBorderPiece { + f, err := box.uiManager.NewSprite(d2resource.BoxPieces, d2resource.PaletteSky) + if err != nil { + log.Print(err) + } + + err = f.SetCurrentFrame(frameIndex) + if err != nil { + log.Print(err) + } + + f.SetPosition(currentX, currentY) + currentY += boxSpriteHeight + + box.sprites = append(box.sprites, f) + + i++ + if i >= maxPieces { + break + } + } + + if i >= maxPieces { + break + } + } +} +func (box *Box) setupRightBorder() { + rightBorderPiece := []int{ + boxSideEdge4, + boxSideEdge5, + boxSideEdge6, + } + + i := 0 + currentX, currentY := box.width+box.x-boxBorderSpriteRightBorderOffset, box.y+boxSpriteHeight + maxPieces := box.height / boxSpriteHeight + + for { + for _, frameIndex := range rightBorderPiece { + f, err := box.uiManager.NewSprite(d2resource.BoxPieces, d2resource.PaletteSky) + if err != nil { + log.Print(err) + } + + err = f.SetCurrentFrame(frameIndex) + if err != nil { + log.Print(err) + } + + f.SetPosition(currentX, currentY) + currentY += boxSpriteHeight + + box.sprites = append(box.sprites, f) + + i++ + if i >= maxPieces { + break + } + } + + if i >= maxPieces { + break + } + } +} + +func (box *Box) setupCorners() { + cornersFrames := []int{ + boxCornerTopLeft, + boxCornerTopRight, + boxCornerBottomLeft, + boxCornerBottomRight, + } + + for _, frameIndex := range cornersFrames { + f, err := box.uiManager.NewSprite(d2resource.BoxPieces, d2resource.PaletteSky) + if err != nil { + log.Print(err) + } + + err = f.SetCurrentFrame(frameIndex) + if err != nil { + log.Print(err) + } + + switch frameIndex { + case boxCornerTopLeft: + f.SetPosition(box.x, box.y+boxSpriteHeight) + case boxCornerTopRight: + f.SetPosition(box.x+box.width-boxSpriteWidth, box.y+boxSpriteHeight) + case boxCornerBottomLeft: + f.SetPosition(box.x, box.y+box.height) + case boxCornerBottomRight: + f.SetPosition(box.x+box.width-boxSpriteWidth, box.y+box.height) + } + + box.sprites = append(box.sprites, f) + } +} + +// SetOptions sets the box options that will show up at the bottom +func (box *Box) SetOptions(options []*LabelButton) { + box.Options = options +} + +func (box *Box) setupTitle(sectionHeight int) error { + if !box.disableBorder { + cornerLeft, err := box.uiManager.NewSprite(d2resource.BoxPieces, d2resource.PaletteSky) + if err != nil { + return err + } + + cornerRight, err := box.uiManager.NewSprite(d2resource.BoxPieces, d2resource.PaletteSky) + if err != nil { + return err + } + + offsetY := box.y + sectionHeight + + if err := cornerLeft.SetCurrentFrame(boxCornerBottomLeft); err != nil { + return err + } + + cornerLeft.SetPosition(box.x, offsetY) + + if err := cornerRight.SetCurrentFrame(boxCornerBottomRight); err != nil { + return err + } + + cornerRight.SetPosition(box.x+box.width-boxSpriteWidth, offsetY) + + box.sprites = append(box.sprites, cornerLeft, cornerRight) + box.setupBottomBorder(offsetY) + } + + contentLayoutW, contentLayoutH := box.contentLayout.GetSize() + contentLayoutX, contentLayoutY := box.contentLayout.GetPosition() + box.contentLayout.SetSize(contentLayoutW, contentLayoutH-sectionHeight) + box.contentLayout.SetPosition(contentLayoutX, contentLayoutY+sectionHeight) + + titleLayout := box.layout.AddLayout(PositionTypeHorizontal) + titleLayout.SetHorizontalAlign(HorizontalAlignCenter) + titleLayout.SetVerticalAlign(VerticalAlignMiddle) + titleLayout.SetPosition(box.x, box.y) + titleLayout.SetSize(contentLayoutW, sectionHeight) + titleLayout.AddSpacerDynamic() + + if _, err := titleLayout.AddLabel(box.title, FontStyle30Units); err != nil { + return err + } + + titleLayout.AddSpacerDynamic() + + return nil +} + +func (box *Box) setupOptions(sectionHeight int) error { + box.contentLayout.SetSize(box.width, (box.height - sectionHeight)) + + if !box.disableBorder { + cornerLeft, err := box.uiManager.NewSprite(d2resource.BoxPieces, d2resource.PaletteSky) + if err != nil { + return err + } + + cornerRight, err := box.uiManager.NewSprite(d2resource.BoxPieces, d2resource.PaletteSky) + if err != nil { + return err + } + + offsetY := box.y + box.height - sectionHeight + boxSpriteHeight + + if err := cornerLeft.SetCurrentFrame(boxCornerTopLeft); err != nil { + return err + } + + cornerLeft.SetPosition(box.x, offsetY) + + if err := cornerRight.SetCurrentFrame(boxCornerTopRight); err != nil { + return err + } + + cornerRight.SetPosition(box.x+box.width-boxSpriteWidth, offsetY) + box.setupTopBorder(box.height - (4 * boxSpriteHeight) + boxSpriteHeight - boxBorderSpriteTopBorderSectionOffset) + box.sprites = append(box.sprites, cornerLeft, cornerRight) + } + + buttonsLayoutWrapper := box.layout.AddLayout(PositionTypeAbsolute) + buttonsLayoutWrapper.SetSize(box.width, sectionHeight) + buttonsLayoutWrapper.SetPosition(box.x, box.y+box.height-sectionHeight) + buttonsLayout := buttonsLayoutWrapper.AddLayout(PositionTypeHorizontal) + buttonsLayout.SetSize(buttonsLayoutWrapper.GetSize()) + buttonsLayout.SetVerticalAlign(VerticalAlignMiddle) + buttonsLayout.AddSpacerDynamic() + + for _, option := range box.Options { + option.Load(box.renderer, box.asset) + buttonsLayout.AddLayoutFromSource(option.GetLayout()) + buttonsLayout.AddSpacerDynamic() + } + + return nil +} + +// Load will setup the layouts and sprites for the box deptending on the parameters +func (box *Box) Load() error { + box.layout = CreateLayout(box.renderer, PositionTypeAbsolute, box.asset) + box.layout.SetPosition(box.x, box.y) + box.layout.SetSize(box.width, box.height) + box.contentLayout.SetPosition(box.x, box.y) + + if !box.disableBorder { + box.setupTopBorder(boxSpriteHeight) + box.setupBottomBorder(box.y + box.height + boxSpriteHeight) + box.setupLeftBorder() + box.setupRightBorder() + box.setupCorners() + } + + sectionHeight := int(float32(box.height) * sectionHeightPercentageOfBox) + + optionsEnabled := len(box.Options) > 0 && sectionHeight >= minimumAllowedSectionSize + if optionsEnabled { + if err := box.setupOptions(sectionHeight); err != nil { + return err + } + } else { + box.contentLayout.SetSize(box.width, box.height) + } + + if box.title != "" { + if err := box.setupTitle(sectionHeight); err != nil { + return err + } + } + + contentLayoutW, contentLayoutH := box.contentLayout.GetSize() + contentLayoutX, contentLayoutY := box.contentLayout.GetPosition() + box.contentLayout.SetPosition(contentLayoutX+box.paddingX, contentLayoutY+box.paddingY) + box.contentLayout.SetSize(contentLayoutW-(2*box.paddingX), contentLayoutH-(2*box.paddingY)) + + box.layout.AddLayoutFromSource(box.contentLayout) + + return nil +} + +// OnMouseButtonDown will be called whenever a mouse button is triggered +func (box *Box) OnMouseButtonDown(event d2interface.MouseEvent) bool { + for _, option := range box.Options { + if option.IsInRect(event.X(), event.Y()) { + option.callback() + return true + } + } + + return false +} + +// Render the box to the given surface +func (box *Box) Render(target d2interface.Surface) error { + if !box.isOpen { + return nil + } + + target.PushTranslation(box.x, box.y) + target.DrawRect(box.width, box.height, d2util.Color(boxBackgroundColor)) + target.Pop() + + for _, s := range box.sprites { + s.Render(target) + } + + return nil +} + +// IsInRect checks if the given point is within the box main layout rectangle +func (box *Box) IsInRect(px, py int) bool { + ww, hh := box.layout.GetSize() + x, y := box.layout.GetPosition() + + if px >= x && px <= x+ww && py >= y && py <= y+hh { + return true + } + + return false +} diff --git a/d2core/d2gui/label.go b/d2core/d2gui/label.go index f916aab4..7d144ff6 100644 --- a/d2core/d2gui/label.go +++ b/d2core/d2gui/label.go @@ -1,26 +1,47 @@ package d2gui import ( + "image/color" "log" + "time" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset" ) +// Constants defining the main shades of basic colors +// found in the game +const ( + ColorWhite = 0xffffffff + ColorRed = 0xdb3f3dff + ColorGreen = 0x00d000ff + ColorBlue = 0x5450d1ff + ColorBrown = 0xa1925dff + ColorGrey = 0x555555ff +) + // Label is renderable text type Label struct { widgetBase - renderer d2interface.Renderer - text string - font *d2asset.Font - surface d2interface.Surface + renderer d2interface.Renderer + text string + font *d2asset.Font + surface d2interface.Surface + color color.RGBA + hoverColor color.RGBA + isHovered bool + isBlinking bool + isDisplayed bool + blinkTimer time.Time } -func createLabel(renderer d2interface.Renderer, text string, font *d2asset.Font) *Label { +func createLabel(renderer d2interface.Renderer, text string, font *d2asset.Font, col color.RGBA) *Label { label := &Label{ - font: font, - renderer: renderer, + font: font, + renderer: renderer, + color: col, + hoverColor: col, } err := label.setText(text) @@ -34,7 +55,33 @@ func createLabel(renderer d2interface.Renderer, text string, font *d2asset.Font) return label } +// SetHoverColor will set the value of hoverColor +func (l *Label) SetHoverColor(col color.RGBA) { + l.hoverColor = col +} + +// SetIsBlinking will set the isBlinking value +func (l *Label) SetIsBlinking(isBlinking bool) { + l.isBlinking = isBlinking +} + +// SetIsHovered will set the isHovered value +func (l *Label) SetIsHovered(isHovered bool) error { + l.isHovered = isHovered + + return l.setText(l.text) +} + func (l *Label) render(target d2interface.Surface) { + if l.isBlinking && time.Since(l.blinkTimer) >= 200*time.Millisecond { + l.isDisplayed = !l.isDisplayed + l.blinkTimer = time.Now() + } + + if l.isBlinking && !l.isDisplayed { + return + } + target.Render(l.surface) } @@ -47,6 +94,12 @@ func (l *Label) GetText() string { return l.text } +// SetColor sets the label text +func (l *Label) SetColor(col color.RGBA) error { + l.color = col + return l.setText(l.text) +} + // SetText sets the label text func (l *Label) SetText(text string) error { if text == l.text { @@ -61,6 +114,13 @@ func (l *Label) setText(text string) error { surface := l.renderer.NewSurface(width, height) + col := l.color + if l.isHovered { + col = l.hoverColor + } + + l.font.SetColor(col) + if err := l.font.RenderText(text, surface); err != nil { return err } diff --git a/d2core/d2gui/label_button.go b/d2core/d2gui/label_button.go new file mode 100644 index 00000000..be338a4f --- /dev/null +++ b/d2core/d2gui/label_button.go @@ -0,0 +1,99 @@ +package d2gui + +import ( + "image/color" + "log" + + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2util" + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset" +) + +// LabelButton is a label that can change when hovered and has +// a callback function that can be called when clicked +type LabelButton struct { + label string + callback func() + hoverColor color.RGBA + canHover bool + isHovered bool + layout *Layout + x, y int +} + +// NewLabelButton generates a new instance of LabelButton +func NewLabelButton(x, y int, text string, col color.RGBA, callback func()) *LabelButton { + return &LabelButton{ + x: x, + y: y, + hoverColor: col, + label: text, + callback: callback, + canHover: true, + } +} + +// IsInRect checks if the given point is within the overlay layout rectangle +func (lb *LabelButton) IsInRect(px, py int) bool { + if lb.layout == nil { + return false + } + + ww, hh := lb.layout.GetSize() + x, y := lb.layout.Sx, lb.layout.Sy + + if px >= x && px <= x+ww && py >= y && py <= y+hh { + return true + } + + return false +} + +// Load sets the button handlers and sets the layouts +func (lb *LabelButton) Load(renderer d2interface.Renderer, asset *d2asset.AssetManager) { + mainLayout := CreateLayout(renderer, PositionTypeAbsolute, asset) + l, _ := mainLayout.AddLabelWithColor(lb.label, FontStyleFormal11Units, d2util.Color(ColorBrown)) + + if lb.canHover { + l.SetHoverColor(lb.hoverColor) + } + + mainLayout.SetMouseEnterHandler(func(event d2interface.MouseMoveEvent) { + if err := l.SetIsHovered(true); err != nil { + log.Printf("could not change label to hover state: %v", err) + } + }) + + mainLayout.SetMouseLeaveHandler(func(event d2interface.MouseMoveEvent) { + if err := l.SetIsHovered(false); err != nil { + log.Printf("could not change label to hover state: %v", err) + } + }) + + lb.layout = mainLayout +} + +// SetLabel sets the text of label label +func (lb *LabelButton) SetLabel(val string) { + lb.label = val +} + +// SetHoverColor sets the hover color of the Label +func (lb *LabelButton) SetHoverColor(col color.RGBA) { + lb.hoverColor = col +} + +// SetCanHover sets the value of canHover +func (lb *LabelButton) SetCanHover(val bool) { + lb.canHover = val +} + +// IsHovered returns the value of isHovered +func (lb *LabelButton) IsHovered() bool { + return lb.isHovered +} + +// GetLayout returns the laout of the label +func (lb *LabelButton) GetLayout() *Layout { + return lb.layout +} diff --git a/d2core/d2gui/layout.go b/d2core/d2gui/layout.go index 7edbfcb7..03376bf7 100644 --- a/d2core/d2gui/layout.go +++ b/d2core/d2gui/layout.go @@ -2,9 +2,11 @@ package d2gui import ( "errors" + "image/color" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2math" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2util" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset" ) @@ -105,6 +107,14 @@ func (l *Layout) AddLayout(positionType PositionType) *Layout { return layout } +// AddLayoutFromSource adds a nested layout to this layout, given a position type. +// Returns a pointer to the nested layout +func (l *Layout) AddLayoutFromSource(source *Layout) *Layout { + l.entries = append(l.entries, &layoutEntry{widget: source}) + + return source +} + // AddSpacerStatic adds a spacer with explicitly defined height and width func (l *Layout) AddSpacerStatic(width, height int) *SpacerStatic { spacer := createSpacerStatic(width, height) @@ -157,7 +167,21 @@ func (l *Layout) AddLabel(text string, fontStyle FontStyle) (*Label, error) { return nil, err } - label := createLabel(l.renderer, text, font) + label := createLabel(l.renderer, text, font, d2util.Color(ColorWhite)) + + l.entries = append(l.entries, &layoutEntry{widget: label}) + + return label, nil +} + +// AddLabelWithColor given a string and a FontStyle and a Color, adds a text label as a layout entry +func (l *Layout) AddLabelWithColor(text string, fontStyle FontStyle, col color.RGBA) (*Label, error) { + font, err := l.loadFont(fontStyle) + if err != nil { + return nil, err + } + + label := createLabel(l.renderer, text, font, col) l.entries = append(l.entries, &layoutEntry{widget: label}) @@ -276,6 +300,7 @@ func (l *Layout) getSize() (width, height int) { func (l *Layout) onMouseButtonDown(event d2interface.MouseEvent) bool { for _, entry := range l.entries { if entry.IsIn(event) { + entry.widget.onMouseButtonClick(event) entry.widget.onMouseButtonDown(event) entry.mouseDown[event.Button()] = true } diff --git a/d2core/d2gui/layout_scrollbar.go b/d2core/d2gui/layout_scrollbar.go new file mode 100644 index 00000000..151a2a34 --- /dev/null +++ b/d2core/d2gui/layout_scrollbar.go @@ -0,0 +1,385 @@ +package d2gui + +import ( + "log" + "math" + + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource" + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2ui" +) + +// LayoutScrollbar is a scrollbar that can be used with any layout +// and attaches to a main layout. You need to use a wrapper for your content +// as main layout in order for the scrollbar to work properly +type LayoutScrollbar struct { + sliderSprites []*d2ui.Sprite + gutterSprites []*d2ui.Sprite + + parentLayout *Layout + targetLayout *Layout + sliderLayout *Layout + + arrowUpLayout *Layout + arrowDownLayout *Layout + + arrowUpSprite *d2ui.Sprite + arrowDownSprite *d2ui.Sprite + + maxY int + minY int + arrowClickSliderOffset int + viewportSize int + contentSize int + + clickedAtY int + mouseYOnSlider int + lastY int + gutterHeight int + sliderHeight int + contentToViewRatio float32 + + // isVisible bool + arrowUpClicked bool + arrowDownClicked bool + sliderClicked bool +} + +const ( + textSliderPartWidth = 12 + textSliderPartHeight = 13 + + arrrowClickContentOffsetPercentage = 0.02 + oneHundredPercent = 1.0 +) + +const ( + textSliderPartArrowDownHollow int = iota + 8 + textSliderPartArrowUpHollow + textSliderPartArrowDownFilled int = 10 + textSliderPartArrowUpFilled int = 11 + // textSliderPartSquare + textSliderPartInnerGutter int = 13 + textSliderPartFillingVariation1 int = 14 +) + +// NewLayoutScrollbar attaches a scrollbar to the parentLayout to control the targetLayout +func NewLayoutScrollbar( + parentLayout *Layout, + targetLayout *Layout, +) *LayoutScrollbar { + parentW, parentH := parentLayout.GetSize() + _, targetH := targetLayout.GetSize() + gutterHeight := parentH - (2 * textSliderPartHeight) + viewportPercentage := oneHundredPercent - (float32(targetH-parentH) / float32(targetH)) + sliderHeight := int(float32(gutterHeight) * viewportPercentage) + x, y := parentW-textSliderPartWidth, 0 + + ret := &LayoutScrollbar{ + sliderSprites: []*d2ui.Sprite{}, + gutterSprites: []*d2ui.Sprite{}, + } + + ret.contentToViewRatio = viewportPercentage + ret.contentToViewRatio = float32(targetH) / float32(gutterHeight) + ret.gutterHeight = gutterHeight + ret.sliderHeight = sliderHeight + ret.minY = y + textSliderPartHeight + ret.maxY = (y + parentH) - (textSliderPartHeight + sliderHeight) + ret.contentSize = targetH + ret.arrowClickSliderOffset = int(float32(sliderHeight) * arrrowClickContentOffsetPercentage) + ret.viewportSize = parentH + + arrowUpLayout := parentLayout.AddLayout(PositionTypeAbsolute) + arrowUpLayout.SetSize(textSliderPartWidth, textSliderPartHeight) + arrowUpLayout.SetPosition(x, 0) + ret.arrowUpLayout = arrowUpLayout + + gutterLayout := parentLayout.AddLayout(PositionTypeAbsolute) + gutterLayout.SetSize(textSliderPartWidth, gutterHeight) + gutterLayout.SetPosition(x, textSliderPartHeight) + + sliderLayout := parentLayout.AddLayout(PositionTypeAbsolute) + sliderLayout.SetPosition(x, textSliderPartHeight) + sliderLayout.SetSize(textSliderPartWidth, sliderHeight) + sliderLayout.SetMouseClickHandler(ret.OnSliderMouseClick) + + arrowDownLayout := parentLayout.AddLayout(PositionTypeAbsolute) + arrowDownLayout.SetSize(textSliderPartWidth, textSliderPartHeight) + arrowDownLayout.SetPosition(x, textSliderPartHeight+gutterHeight) + ret.arrowDownLayout = arrowDownLayout + + ret.sliderLayout = sliderLayout + ret.parentLayout = parentLayout + ret.targetLayout = targetLayout + + ret.parentLayout.AdjustEntryPlacement() + ret.targetLayout.AdjustEntryPlacement() + ret.sliderLayout.AdjustEntryPlacement() + + return ret +} + +// Load sets the scrollbar layouts and loads the sprites +func (scrollbar *LayoutScrollbar) Load(ui *d2ui.UIManager) error { + arrowUpX, arrowUpY := scrollbar.arrowUpLayout.ScreenPos() + arrowUpSprite, _ := ui.NewSprite(d2resource.TextSlider, d2resource.PaletteSky) + + if err := arrowUpSprite.SetCurrentFrame(textSliderPartArrowUpFilled); err != nil { + return err + } + + arrowUpSprite.SetPosition(arrowUpX, arrowUpY+textSliderPartHeight) + scrollbar.arrowUpSprite = arrowUpSprite + + arrowDownX, arrowDownY := scrollbar.arrowDownLayout.ScreenPos() + arrowDownSprite, _ := ui.NewSprite(d2resource.TextSlider, d2resource.PaletteSky) + + if err := arrowDownSprite.SetCurrentFrame(textSliderPartArrowDownFilled); err != nil { + return err + } + + arrowDownSprite.SetPosition(arrowDownX, arrowDownY+textSliderPartHeight) + scrollbar.arrowDownSprite = arrowDownSprite + + gutterParts := int(math.Ceil(float64(scrollbar.gutterHeight+(2*textSliderPartHeight)) / float64(textSliderPartHeight))) + sliderParts := int(math.Ceil(float64(scrollbar.sliderHeight) / float64(textSliderPartHeight))) + gutterX, gutterY := arrowUpX, arrowUpY+(2*textSliderPartHeight)-1 + i := 0 + + for { + if i >= gutterParts { + break + } + + f, _ := ui.NewSprite(d2resource.TextSlider, d2resource.PaletteSky) + + if err := f.SetCurrentFrame(textSliderPartInnerGutter); err != nil { + return err + } + + newY := gutterY + (i * (textSliderPartHeight - 1)) + f.SetPosition(gutterX, newY) + + scrollbar.gutterSprites = append(scrollbar.gutterSprites, f) + + i++ + } + + i = 0 + + for { + if i >= sliderParts { + break + } + + f, _ := ui.NewSprite(d2resource.TextSlider, d2resource.PaletteSky) + + if err := f.SetCurrentFrame(textSliderPartFillingVariation1); err != nil { + return err + } + + scrollbar.sliderSprites = append(scrollbar.sliderSprites, f) + + i++ + } + + scrollbar.updateSliderSpritesPosition() + + return nil +} + +func (scrollbar *LayoutScrollbar) updateSliderSpritesPosition() { + scrollbar.sliderLayout.AdjustEntryPlacement() + sliderLayoutX, sliderLayoutY := scrollbar.sliderLayout.ScreenPos() + + for i, s := range scrollbar.sliderSprites { + newY := sliderLayoutY + (i * (textSliderPartHeight - 1)) + textSliderPartHeight + s.SetPosition(sliderLayoutX-1, newY) + } +} + +// OnSliderMouseClick affects the state of the slider +func (scrollbar *LayoutScrollbar) OnSliderMouseClick(event d2interface.MouseEvent) { + scrollbar.clickedAtY = event.Y() + scrollbar.lastY = scrollbar.clickedAtY + scrollbar.mouseYOnSlider = event.Y() - scrollbar.sliderLayout.Sy +} + +func (scrollbar *LayoutScrollbar) moveScaledContentBy(offset int) int { + _, y := scrollbar.sliderLayout.GetPosition() + newY := y + offset + + outOfBoundsUp := false + outOfBoundsDown := false + + if newY > scrollbar.maxY { + newY = scrollbar.maxY + outOfBoundsDown = true + } + + if newY < scrollbar.minY { + newY = scrollbar.minY + outOfBoundsUp = true + } + + if !outOfBoundsUp && !outOfBoundsDown { + scrollbar.clickedAtY += offset + + if scrollbar.targetLayout != nil { + contentX, contentY := scrollbar.targetLayout.GetPosition() + scaledOffset := int(math.Round(float64(float32(offset) * scrollbar.contentToViewRatio))) + newContentY := contentY - scaledOffset + scrollbar.targetLayout.SetPosition(contentX, newContentY) + } + } + + if outOfBoundsDown && scrollbar.targetLayout != nil { + newContentY := -scrollbar.contentSize + scrollbar.viewportSize + scrollbar.targetLayout.SetPosition(0, newContentY) + } + + if outOfBoundsUp && scrollbar.targetLayout != nil { + scrollbar.targetLayout.SetPosition(0, 0) + } + + return newY +} + +// OnMouseMove will affect the slider and the content depending on the state fof it +func (scrollbar *LayoutScrollbar) OnMouseMove(event d2interface.MouseMoveEvent) { + if !scrollbar.sliderClicked { + return + } + + sliderX, _ := scrollbar.sliderLayout.GetPosition() + newY := scrollbar.moveScaledContentBy(event.Y() - scrollbar.clickedAtY) + + scrollbar.sliderLayout.SetPosition(sliderX, newY) + scrollbar.updateSliderSpritesPosition() +} + +// OnArrowUpClick will move the slider and the content up +func (scrollbar *LayoutScrollbar) OnArrowUpClick() { + sliderX, _ := scrollbar.sliderLayout.GetPosition() + newY := scrollbar.moveScaledContentBy(-scrollbar.arrowClickSliderOffset) + + scrollbar.sliderLayout.SetPosition(sliderX, newY) + scrollbar.updateSliderSpritesPosition() +} + +// OnArrowDownClick will move the slider and the content down +func (scrollbar *LayoutScrollbar) OnArrowDownClick() { + sliderX, _ := scrollbar.sliderLayout.GetPosition() + newY := scrollbar.moveScaledContentBy(scrollbar.arrowClickSliderOffset) + + scrollbar.sliderLayout.SetPosition(sliderX, newY) + scrollbar.updateSliderSpritesPosition() +} + +// SetSliderClicked sets the value of sliderClicked +func (scrollbar *LayoutScrollbar) SetSliderClicked(value bool) { + scrollbar.sliderClicked = value +} + +// SetArrowUpClicked sets the value of sliderClicked +func (scrollbar *LayoutScrollbar) SetArrowUpClicked(value bool) { + var arrowSpriteFrame int + + scrollbar.arrowUpClicked = value + + if scrollbar.arrowUpClicked { + arrowSpriteFrame = textSliderPartArrowUpHollow + } else { + arrowSpriteFrame = textSliderPartArrowUpFilled + } + + if err := scrollbar.arrowUpSprite.SetCurrentFrame(arrowSpriteFrame); err != nil { + log.Printf("unable to set arrow up sprite frame: %v", err) + } +} + +// SetArrowDownClicked sets the value of sliderClicked +func (scrollbar *LayoutScrollbar) SetArrowDownClicked(value bool) { + var arrowSpriteFrame int + + scrollbar.arrowDownClicked = value + + if scrollbar.arrowDownClicked { + arrowSpriteFrame = textSliderPartArrowDownHollow + } else { + arrowSpriteFrame = textSliderPartArrowDownFilled + } + + if err := scrollbar.arrowDownSprite.SetCurrentFrame(arrowSpriteFrame); err != nil { + log.Printf("unable to set arrow down sprite frame: %v", err) + } +} + +// Advance updates the layouts according to the state of the arrown +func (scrollbar *LayoutScrollbar) Advance(elapsed float64) error { + if scrollbar.arrowDownClicked { + scrollbar.OnArrowDownClick() + } + + if scrollbar.arrowUpClicked { + scrollbar.OnArrowUpClick() + } + + return nil +} + +// Render draws the scrollbar sprites on the given surface +func (scrollbar *LayoutScrollbar) Render(target d2interface.Surface) { + for _, s := range scrollbar.gutterSprites { + s.Render(target) + } + + for _, s := range scrollbar.sliderSprites { + s.Render(target) + } + + scrollbar.arrowUpSprite.Render(target) + scrollbar.arrowDownSprite.Render(target) +} + +func (scrollbar *LayoutScrollbar) isInLayoutRect(layout *Layout, px, py int) bool { + ww, hh := layout.GetSize() + x, y := layout.Sx, layout.Sy + + if px >= x && px <= x+ww && py >= y && py <= y+hh { + return true + } + + return false +} + +// IsSliderClicked returns the state of the slider +func (scrollbar *LayoutScrollbar) IsSliderClicked() bool { + return scrollbar.sliderClicked +} + +// IsArrowUpClicked returns the state of arrow up clicked +func (scrollbar *LayoutScrollbar) IsArrowUpClicked() bool { + return scrollbar.arrowUpClicked +} + +// IsArrowDownClicked returns the state of arrow down clicked +func (scrollbar *LayoutScrollbar) IsArrowDownClicked() bool { + return scrollbar.arrowDownClicked +} + +// IsInArrowUpRect checks if the given point is within the overlay layout rectangle +func (scrollbar *LayoutScrollbar) IsInArrowUpRect(px, py int) bool { + return scrollbar.isInLayoutRect(scrollbar.arrowUpLayout, px, py) +} + +// IsInArrowDownRect checks if the given point is within the overlay layout rectangle +func (scrollbar *LayoutScrollbar) IsInArrowDownRect(px, py int) bool { + return scrollbar.isInLayoutRect(scrollbar.arrowDownLayout, px, py) +} + +// IsInSliderRect checks if the given point is within the overlay layout rectangle +func (scrollbar *LayoutScrollbar) IsInSliderRect(px, py int) bool { + return scrollbar.isInLayoutRect(scrollbar.sliderLayout, px, py) +} diff --git a/d2core/d2render/ebiten/ebiten_surface.go b/d2core/d2render/ebiten/ebiten_surface.go index cf4ceca7..059aa53e 100644 --- a/d2core/d2render/ebiten/ebiten_surface.go +++ b/d2core/d2render/ebiten/ebiten_surface.go @@ -11,6 +11,7 @@ import ( "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2ui" ) // static check that we implement our interface @@ -136,6 +137,20 @@ func (s *ebitenSurface) PopN(n int) { } } +func (s *ebitenSurface) RenderSprite(sprite *d2ui.Sprite) { + opts := s.createDrawImageOptions() + + if s.stateCurrent.brightness != 1 || s.stateCurrent.saturation != 1 { + opts.ColorM.ChangeHSV(0, s.stateCurrent.saturation, s.stateCurrent.brightness) + } + + s.handleStateEffect(opts) + + opts.CompositeMode = ebiten.CompositeModeSourceOver + + sprite.Render(s) +} + // Render renders the given surface func (s *ebitenSurface) Render(sfc d2interface.Surface) { opts := s.createDrawImageOptions() @@ -146,6 +161,8 @@ func (s *ebitenSurface) Render(sfc d2interface.Surface) { s.handleStateEffect(opts) + opts.CompositeMode = ebiten.CompositeModeSourceOver + s.image.DrawImage(sfc.(*ebitenSurface).image, opts) } diff --git a/d2core/d2ui/button.go b/d2core/d2ui/button.go index 01cacd65..71c1aa92 100644 --- a/d2core/d2ui/button.go +++ b/d2core/d2ui/button.go @@ -8,7 +8,6 @@ import ( "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2util" - "github.com/OpenDiablo2/OpenDiablo2/d2core/d2gui" ) // ButtonType defines the type of button @@ -271,7 +270,7 @@ func (ui *UIManager) NewButton(buttonType ButtonType, text string) *Button { lbl.SetText(text) lbl.Color[0] = d2util.Color(buttonLayout.LabelColor) - lbl.Alignment = d2gui.HorizontalAlignCenter + lbl.Alignment = HorizontalAlignCenter buttonSprite, err := ui.NewSprite(buttonLayout.ResourceName, buttonLayout.PaletteName) if err != nil { diff --git a/d2core/d2ui/d2ui.go b/d2core/d2ui/d2ui.go index 4ae6f4b9..91a9030f 100644 --- a/d2core/d2ui/d2ui.go +++ b/d2core/d2ui/d2ui.go @@ -15,6 +15,16 @@ const ( CursorButtonRight CursorButton = 2 ) +// HorizontalAlign type, determines alignment along x-axis within a layout +type HorizontalAlign int + +// Horizontal alignment types +const ( + HorizontalAlignLeft HorizontalAlign = iota + HorizontalAlignCenter + HorizontalAlignRight +) + // NewUIManager creates a UIManager instance with the given input and audio provider func NewUIManager( asset *d2asset.AssetManager, diff --git a/d2core/d2ui/label.go b/d2core/d2ui/label.go index aa768ee1..4ce6fe60 100644 --- a/d2core/d2ui/label.go +++ b/d2core/d2ui/label.go @@ -10,14 +10,13 @@ import ( "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2util" - "github.com/OpenDiablo2/OpenDiablo2/d2core/d2gui" ) // Label represents a user interface label type Label struct { *BaseWidget text string - Alignment d2gui.HorizontalAlign + Alignment HorizontalAlign font *d2asset.Font Color map[int]color.Color backgroundColor color.Color @@ -35,7 +34,7 @@ func (ui *UIManager) NewLabel(fontPath, palettePath string) *Label { result := &Label{ BaseWidget: base, - Alignment: d2gui.HorizontalAlignLeft, + Alignment: HorizontalAlignLeft, Color: map[int]color.Color{0: color.White}, font: font, } @@ -150,11 +149,11 @@ func (v *Label) processColorTokens(str string) string { func (v *Label) getAlignOffset(textWidth int) int { switch v.Alignment { - case d2gui.HorizontalAlignLeft: + case HorizontalAlignLeft: return 0 - case d2gui.HorizontalAlignCenter: + case HorizontalAlignCenter: return -textWidth / 2 - case d2gui.HorizontalAlignRight: + case HorizontalAlignRight: return -textWidth default: log.Fatal("Invalid Alignment") diff --git a/d2core/d2ui/sprite.go b/d2core/d2ui/sprite.go index dc956e3d..cc864c21 100644 --- a/d2core/d2ui/sprite.go +++ b/d2core/d2ui/sprite.go @@ -47,6 +47,11 @@ func (s *Sprite) Render(target d2interface.Surface) { s.animation.Render(target) } +// GetSurface returns the surface of the sprite at the given frame +func (s *Sprite) GetSurface() d2interface.Surface { + return s.animation.GetCurrentFrameSurface() +} + // RenderSection renders the section of the sprite enclosed by bounds func (s *Sprite) RenderSection(sfc d2interface.Surface, bound image.Rectangle) { sfc.PushTranslation(s.x, s.y-bound.Dy()) diff --git a/d2core/d2ui/tooltip.go b/d2core/d2ui/tooltip.go index 1f1e1216..c0136bc7 100644 --- a/d2core/d2ui/tooltip.go +++ b/d2core/d2ui/tooltip.go @@ -5,7 +5,6 @@ import ( "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2util" - "github.com/OpenDiablo2/OpenDiablo2/d2core/d2gui" ) const ( @@ -56,7 +55,7 @@ func (ui *UIManager) NewTooltip(font, originX tooltipXOrigin, originY tooltipYOrigin) *Tooltip { label := ui.NewLabel(font, palette) - label.Alignment = d2gui.HorizontalAlignCenter + label.Alignment = HorizontalAlignCenter base := NewBaseWidget(ui) diff --git a/d2game/d2gamescreen/character_select.go b/d2game/d2gamescreen/character_select.go index d64a0f08..8360d157 100644 --- a/d2game/d2gamescreen/character_select.go +++ b/d2game/d2gamescreen/character_select.go @@ -13,7 +13,6 @@ import ( "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset" - "github.com/OpenDiablo2/OpenDiablo2/d2core/d2gui" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2mapentity" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2screen" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2ui" @@ -216,14 +215,14 @@ func (v *CharacterSelect) loadHeroTitle() { heroTitleX, heroTitleY := 320, 23 v.d2HeroTitle = v.uiManager.NewLabel(d2resource.Font42, d2resource.PaletteUnits) v.d2HeroTitle.SetPosition(heroTitleX, heroTitleY) - v.d2HeroTitle.Alignment = d2gui.HorizontalAlignCenter + v.d2HeroTitle.Alignment = d2ui.HorizontalAlignCenter } func (v *CharacterSelect) loadDeleteCharConfirm() { v.deleteCharConfirmLabel = v.uiManager.NewLabel(d2resource.Font16, d2resource.PaletteUnits) lines := "Are you sure that you want\nto delete this character?\nTake note: this will delete all\nversions of this Character." v.deleteCharConfirmLabel.SetText(lines) - v.deleteCharConfirmLabel.Alignment = d2gui.HorizontalAlignCenter + v.deleteCharConfirmLabel.Alignment = d2ui.HorizontalAlignCenter deleteConfirmX, deleteConfirmY := 400, 185 v.deleteCharConfirmLabel.SetPosition(deleteConfirmX, deleteConfirmY) } diff --git a/d2game/d2gamescreen/cinematics.go b/d2game/d2gamescreen/cinematics.go index 39192f89..ae5d1cb2 100644 --- a/d2game/d2gamescreen/cinematics.go +++ b/d2game/d2gamescreen/cinematics.go @@ -7,7 +7,6 @@ import ( "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset" - "github.com/OpenDiablo2/OpenDiablo2/d2core/d2gui" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2screen" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2ui" ) @@ -90,7 +89,7 @@ func (v *Cinematics) OnLoad(_ d2screen.LoadingState) { v.createButtons() v.cinematicsLabel = v.uiManager.NewLabel(d2resource.Font30, d2resource.PaletteStatic) - v.cinematicsLabel.Alignment = d2gui.HorizontalAlignCenter + v.cinematicsLabel.Alignment = d2ui.HorizontalAlignCenter v.cinematicsLabel.SetText("SELECT CINEMATIC") v.cinematicsLabel.Color[0] = rgbaColor(lightBrown) v.cinematicsLabel.SetPosition(cinematicsLabelX, cinematicsLabelY) diff --git a/d2game/d2gamescreen/game.go b/d2game/d2gamescreen/game.go index aabbc2a5..e2e6926e 100644 --- a/d2game/d2gamescreen/game.go +++ b/d2game/d2gamescreen/game.go @@ -50,6 +50,7 @@ type Game struct { soundEngine *d2audio.SoundEngine soundEnv d2audio.SoundEnvironment guiManager *d2gui.GuiManager + keyMap *d2player.KeyMap renderer d2interface.Renderer inputManager d2interface.InputManager @@ -83,6 +84,8 @@ func CreateGame( break } + keyMap := d2player.GetDefaultKeyMap(asset) + result := &Game{ asset: asset, gameClient: gameClient, @@ -92,7 +95,7 @@ func CreateGame( ticksSinceLevelCheck: 0, mapRenderer: d2maprenderer.CreateMapRenderer(asset, renderer, gameClient.MapEngine, term, startX, startY), - escapeMenu: d2player.NewEscapeMenu(navigator, renderer, audioProvider, guiManager, asset), + escapeMenu: d2player.NewEscapeMenu(navigator, renderer, audioProvider, ui, guiManager, asset, keyMap), inputManager: inputManager, audioProvider: audioProvider, renderer: renderer, @@ -100,6 +103,7 @@ func CreateGame( soundEngine: d2audio.NewSoundEngine(audioProvider, asset, term), uiManager: ui, guiManager: guiManager, + keyMap: keyMap, } result.soundEnv = d2audio.NewSoundEnvironment(result.soundEngine) @@ -290,7 +294,7 @@ func (v *Game) bindGameControls() error { var err error v.gameControls, err = d2player.NewGameControls(v.asset, v.renderer, player, v.gameClient.MapEngine, - v.escapeMenu, v.mapRenderer, v, v.terminal, v.uiManager, v.guiManager, v.gameClient.IsSinglePlayer()) + v.escapeMenu, v.mapRenderer, v, v.terminal, v.uiManager, v.guiManager, v.keyMap, v.gameClient.IsSinglePlayer()) if err != nil { return err diff --git a/d2game/d2gamescreen/main_menu.go b/d2game/d2gamescreen/main_menu.go index 87ae5853..c99150ee 100644 --- a/d2game/d2gamescreen/main_menu.go +++ b/d2game/d2gamescreen/main_menu.go @@ -15,7 +15,6 @@ import ( "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset" - "github.com/OpenDiablo2/OpenDiablo2/d2core/d2gui" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2screen" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2ui" "github.com/OpenDiablo2/OpenDiablo2/d2networking/d2client/d2clientconnectiontype" @@ -226,32 +225,32 @@ func (v *MainMenu) loadBackgroundSprites() { func (v *MainMenu) createLabels(loading d2screen.LoadingState) { v.versionLabel = v.uiManager.NewLabel(d2resource.FontFormal12, d2resource.PaletteStatic) - v.versionLabel.Alignment = d2gui.HorizontalAlignRight + v.versionLabel.Alignment = d2ui.HorizontalAlignRight v.versionLabel.SetText("OpenDiablo2 - " + v.buildInfo.Branch) v.versionLabel.Color[0] = rgbaColor(white) v.versionLabel.SetPosition(versionLabelX, versionLabelY) v.commitLabel = v.uiManager.NewLabel(d2resource.FontFormal10, d2resource.PaletteStatic) - v.commitLabel.Alignment = d2gui.HorizontalAlignLeft + v.commitLabel.Alignment = d2ui.HorizontalAlignLeft v.commitLabel.SetText(v.buildInfo.Commit) v.commitLabel.Color[0] = rgbaColor(white) v.commitLabel.SetPosition(commitLabelX, commitLabelY) v.copyrightLabel = v.uiManager.NewLabel(d2resource.FontFormal12, d2resource.PaletteStatic) - v.copyrightLabel.Alignment = d2gui.HorizontalAlignCenter + v.copyrightLabel.Alignment = d2ui.HorizontalAlignCenter v.copyrightLabel.SetText("Diablo 2 is © Copyright 2000-2016 Blizzard Entertainment") v.copyrightLabel.Color[0] = rgbaColor(lightBrown) v.copyrightLabel.SetPosition(copyrightX, copyrightY) loading.Progress(thirtyPercent) v.copyrightLabel2 = v.uiManager.NewLabel(d2resource.FontFormal12, d2resource.PaletteStatic) - v.copyrightLabel2.Alignment = d2gui.HorizontalAlignCenter + v.copyrightLabel2.Alignment = d2ui.HorizontalAlignCenter v.copyrightLabel2.SetText("All Rights Reserved.") v.copyrightLabel2.Color[0] = rgbaColor(lightBrown) v.copyrightLabel2.SetPosition(copyright2X, copyright2Y) v.openDiabloLabel = v.uiManager.NewLabel(d2resource.FontFormal10, d2resource.PaletteStatic) - v.openDiabloLabel.Alignment = d2gui.HorizontalAlignCenter + v.openDiabloLabel.Alignment = d2ui.HorizontalAlignCenter v.openDiabloLabel.SetText("OpenDiablo2 is neither developed by, nor endorsed by Blizzard or its parent company Activision") v.openDiabloLabel.Color[0] = rgbaColor(lightYellow) v.openDiabloLabel.SetPosition(od2LabelX, od2LabelY) @@ -259,24 +258,24 @@ func (v *MainMenu) createLabels(loading d2screen.LoadingState) { v.tcpIPOptionsLabel = v.uiManager.NewLabel(d2resource.Font42, d2resource.PaletteUnits) v.tcpIPOptionsLabel.SetPosition(tcpOptionsX, tcpOptionsY) - v.tcpIPOptionsLabel.Alignment = d2gui.HorizontalAlignCenter + v.tcpIPOptionsLabel.Alignment = d2ui.HorizontalAlignCenter v.tcpIPOptionsLabel.SetText("TCP/IP Options") v.tcpJoinGameLabel = v.uiManager.NewLabel(d2resource.Font16, d2resource.PaletteUnits) - v.tcpJoinGameLabel.Alignment = d2gui.HorizontalAlignCenter + v.tcpJoinGameLabel.Alignment = d2ui.HorizontalAlignCenter v.tcpJoinGameLabel.SetText("Enter Host IP Address\nto Join Game") v.tcpJoinGameLabel.Color[0] = rgbaColor(gold) v.tcpJoinGameLabel.SetPosition(joinGameX, joinGameY) v.machineIP = v.uiManager.NewLabel(d2resource.Font24, d2resource.PaletteUnits) - v.machineIP.Alignment = d2gui.HorizontalAlignCenter + v.machineIP.Alignment = d2ui.HorizontalAlignCenter v.machineIP.SetText("Your IP address is:\n" + v.getLocalIP()) v.machineIP.Color[0] = rgbaColor(lightYellow) v.machineIP.SetPosition(machineIPX, machineIPY) if v.errorLabel != nil { v.errorLabel.SetPosition(errorLabelX, errorLabelY) - v.errorLabel.Alignment = d2gui.HorizontalAlignCenter + v.errorLabel.Alignment = d2ui.HorizontalAlignCenter v.errorLabel.Color[0] = rgbaColor(red) } } diff --git a/d2game/d2gamescreen/select_hero_class.go b/d2game/d2gamescreen/select_hero_class.go index 571e3000..5d07e73a 100644 --- a/d2game/d2gamescreen/select_hero_class.go +++ b/d2game/d2gamescreen/select_hero_class.go @@ -14,7 +14,6 @@ import ( "github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2util" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset" - "github.com/OpenDiablo2/OpenDiablo2/d2core/d2gui" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2screen" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2ui" "github.com/OpenDiablo2/OpenDiablo2/d2networking/d2client/d2clientconnectiontype" @@ -412,36 +411,36 @@ func (v *SelectHeroClass) createLabels() { v.headingLabel.SetPosition(headingX-halfFontWidth, headingY) v.headingLabel.SetText("Select Hero Class") - v.headingLabel.Alignment = d2gui.HorizontalAlignCenter + v.headingLabel.Alignment = d2ui.HorizontalAlignCenter v.heroClassLabel = v.uiManager.NewLabel(d2resource.Font30, d2resource.PaletteUnits) - v.heroClassLabel.Alignment = d2gui.HorizontalAlignCenter + v.heroClassLabel.Alignment = d2ui.HorizontalAlignCenter v.heroClassLabel.SetPosition(heroClassLabelX, heroClassLabelY) v.heroDesc1Label = v.uiManager.NewLabel(d2resource.Font16, d2resource.PaletteUnits) - v.heroDesc1Label.Alignment = d2gui.HorizontalAlignCenter + v.heroDesc1Label.Alignment = d2ui.HorizontalAlignCenter v.heroDesc1Label.SetPosition(heroDescLine1X, heroDescLine1Y) v.heroDesc2Label = v.uiManager.NewLabel(d2resource.Font16, d2resource.PaletteUnits) - v.heroDesc2Label.Alignment = d2gui.HorizontalAlignCenter + v.heroDesc2Label.Alignment = d2ui.HorizontalAlignCenter v.heroDesc2Label.SetPosition(heroDescLine2X, heroDescLine2Y) v.heroDesc3Label = v.uiManager.NewLabel(d2resource.Font16, d2resource.PaletteUnits) - v.heroDesc3Label.Alignment = d2gui.HorizontalAlignCenter + v.heroDesc3Label.Alignment = d2ui.HorizontalAlignCenter v.heroDesc3Label.SetPosition(heroDescLine3X, heroDescLine3Y) v.heroNameLabel = v.uiManager.NewLabel(d2resource.Font16, d2resource.PaletteUnits) - v.heroNameLabel.Alignment = d2gui.HorizontalAlignLeft + v.heroNameLabel.Alignment = d2ui.HorizontalAlignLeft v.heroNameLabel.SetText(d2ui.ColorTokenize("Character Name", d2ui.ColorTokenGold)) v.heroNameLabel.SetPosition(heroNameLabelX, heroNameLabelY) v.expansionCharLabel = v.uiManager.NewLabel(d2resource.Font16, d2resource.PaletteUnits) - v.expansionCharLabel.Alignment = d2gui.HorizontalAlignLeft + v.expansionCharLabel.Alignment = d2ui.HorizontalAlignLeft v.expansionCharLabel.SetText(d2ui.ColorTokenize("EXPANSION CHARACTER", d2ui.ColorTokenGold)) v.expansionCharLabel.SetPosition(expansionLabelX, expansionLabelY) v.hardcoreCharLabel = v.uiManager.NewLabel(d2resource.Font16, d2resource.PaletteUnits) - v.hardcoreCharLabel.Alignment = d2gui.HorizontalAlignLeft + v.hardcoreCharLabel.Alignment = d2ui.HorizontalAlignLeft v.hardcoreCharLabel.SetText(d2ui.ColorTokenize("Hardcore", d2ui.ColorTokenGold)) v.hardcoreCharLabel.SetPosition(hardcoreLabelX, hardcoreLabelY) } diff --git a/d2game/d2player/binding_layout.go b/d2game/d2player/binding_layout.go new file mode 100644 index 00000000..9b12bf6d --- /dev/null +++ b/d2game/d2player/binding_layout.go @@ -0,0 +1,91 @@ +package d2player + +import ( + "image/color" + + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2gui" +) + +type bindingLayout struct { + wrapperLayout *d2gui.Layout + descLayout *d2gui.Layout + descLabel *d2gui.Label + primaryLayout *d2gui.Layout + primaryLabel *d2gui.Label + secondaryLayout *d2gui.Layout + secondaryLabel *d2gui.Label + + binding *KeyBinding + gameEvent d2enum.GameEvent +} + +func (l *bindingLayout) setTextAndColor(layout *d2gui.Label, text string, col color.RGBA) error { + if err := layout.SetText(text); err != nil { + return err + } + + if err := layout.SetColor(col); err != nil { + return err + } + + return nil +} + +func (l *bindingLayout) SetPrimaryBindingTextAndColor(text string, col color.RGBA) error { + return l.setTextAndColor(l.primaryLabel, text, col) +} + +func (l *bindingLayout) SetSecondaryBindingTextAndColor(text string, col color.RGBA) error { + return l.setTextAndColor(l.secondaryLabel, text, col) +} + +func (l *bindingLayout) Reset() error { + if err := l.descLabel.SetIsHovered(false); err != nil { + return err + } + + if err := l.primaryLabel.SetIsHovered(false); err != nil { + return err + } + + if err := l.secondaryLabel.SetIsHovered(false); err != nil { + return err + } + + l.primaryLabel.SetIsBlinking(false) + l.secondaryLabel.SetIsBlinking(false) + + return nil +} + +func (l *bindingLayout) isInLayoutRect(x, y int, targetLayout *d2gui.Layout) bool { + targetW, targetH := targetLayout.GetSize() + targetX, targetY := targetLayout.Sx, targetLayout.Sy + + if x >= targetX && x <= targetX+targetW && y >= targetY && y <= targetY+targetH { + return true + } + + return false +} + +func (l *bindingLayout) GetPointedLayoutAndLabel(x, y int) (d2enum.GameEvent, KeyBindingType) { + if l.isInLayoutRect(x, y, l.descLayout) { + return l.gameEvent, KeyBindingTypePrimary + } + + if l.primaryLayout != nil { + if l.isInLayoutRect(x, y, l.primaryLayout) { + return l.gameEvent, KeyBindingTypePrimary + } + } + + if l.secondaryLayout != nil { + if l.isInLayoutRect(x, y, l.secondaryLayout) { + return l.gameEvent, KeyBindingTypeSecondary + } + } + + return defaultGameEvent, KeyBindingTypeNone +} diff --git a/d2game/d2player/escape_menu.go b/d2game/d2player/escape_menu.go index 3a4f17a1..d2154991 100644 --- a/d2game/d2player/escape_menu.go +++ b/d2game/d2player/escape_menu.go @@ -10,6 +10,7 @@ import ( "github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2gui" + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2ui" ) type ( @@ -70,22 +71,25 @@ type EscapeMenu struct { // leftPent and rightPent are generated once and shared between the layouts leftPent *d2gui.AnimatedSprite rightPent *d2gui.AnimatedSprite - layouts []*layout + layouts map[layoutID]*layout - renderer d2interface.Renderer - audioProvider d2interface.AudioProvider - navigator d2interface.Navigator - guiManager *d2gui.GuiManager - assetManager *d2asset.AssetManager + renderer d2interface.Renderer + audioProvider d2interface.AudioProvider + navigator d2interface.Navigator + guiManager *d2gui.GuiManager + assetManager *d2asset.AssetManager + keyMap *KeyMap + keyBindingMenu *KeyBindingMenu } type layout struct { *d2gui.Layout leftPent *d2gui.AnimatedSprite rightPent *d2gui.AnimatedSprite + actionableElements []actionableElement currentEl int rendered bool - actionableElements []actionableElement + isRaw bool } func (l *layout) Trigger() { @@ -134,8 +138,10 @@ type actionableElement interface { func NewEscapeMenu(navigator d2interface.Navigator, renderer d2interface.Renderer, audioProvider d2interface.AudioProvider, + uiManager *d2ui.UIManager, guiManager *d2gui.GuiManager, assetManager *d2asset.AssetManager, + keyMap *KeyMap, ) *EscapeMenu { m := &EscapeMenu{ audioProvider: audioProvider, @@ -143,16 +149,18 @@ func NewEscapeMenu(navigator d2interface.Navigator, navigator: navigator, guiManager: guiManager, assetManager: assetManager, + keyMap: keyMap, } - m.layouts = []*layout{ - mainLayoutID: m.newMainLayout(), - optionsLayoutID: m.newOptionsLayout(), - soundOptionsLayoutID: m.newSoundOptionsLayout(), - videoOptionsLayoutID: m.newVideoOptionsLayout(), - automapOptionsLayoutID: m.newAutomapOptionsLayout(), - configureControlsLayoutID: m.newConfigureControlsLayout(), - } + keyBindingMenu := NewKeyBindingMenu(assetManager, renderer, uiManager, guiManager, keyMap, m) + m.keyBindingMenu = keyBindingMenu + + m.layouts = make(map[layoutID]*layout) + m.layouts[mainLayoutID] = m.newMainLayout() + m.layouts[optionsLayoutID] = m.newOptionsLayout() + m.layouts[soundOptionsLayoutID] = m.newSoundOptionsLayout() + m.layouts[videoOptionsLayoutID] = m.newVideoOptionsLayout() + m.layouts[automapOptionsLayoutID] = m.newAutomapOptionsLayout() return m } @@ -213,11 +221,11 @@ func (m *EscapeMenu) newAutomapOptionsLayout() *layout { }) } -func (m *EscapeMenu) newConfigureControlsLayout() *layout { - return m.wrapLayout(func(l *layout) { - m.addTitle(l, "CONFIGURE CONTROLS") - m.addPreviousMenuLabel(l) - }) +func (m *EscapeMenu) newConfigureControlsLayout(keyBindingMenu *KeyBindingMenu) *layout { + return &layout{ + Layout: keyBindingMenu.GetLayout(), + isRaw: true, + } } func (m *EscapeMenu) wrapLayout(fn func(*layout)) *layout { @@ -366,6 +374,13 @@ func (m *EscapeMenu) addEnumLabel(l *layout, optID optionID, text string, values func (m *EscapeMenu) OnLoad() { var err error + err = m.keyBindingMenu.Load() + if err != nil { + log.Printf("unable to load the configure controls window: %v", err) + } + + m.layouts[configureControlsLayoutID] = m.newConfigureControlsLayout(m.keyBindingMenu) + m.selectSound, err = m.audioProvider.LoadSound(d2resource.SFXCursorSelect, false, false) if err != nil { log.Print(err) @@ -384,6 +399,11 @@ func (m *EscapeMenu) OnEscKey() { automapOptionsLayoutID, configureControlsLayoutID: m.setLayout(optionsLayoutID) + + if err := m.keyBindingMenu.Close(); err != nil { + log.Printf("unable to close the configure controls menu: %v", err) + } + return } @@ -419,6 +439,12 @@ func (m *EscapeMenu) showLayout(id layoutID) { } m.setLayout(id) + + if id == configureControlsLayoutID { + m.keyBindingMenu.Open() + } else if err := m.keyBindingMenu.Close(); err != nil { + fmt.Printf("unable to close the configure controls menu: %v", err) + } } func (m *EscapeMenu) onHoverElement(id int) { @@ -437,6 +463,16 @@ func (m *EscapeMenu) onUpdateValue(optID optionID, value string) { } func (m *EscapeMenu) setLayout(id layoutID) { + layout := m.layouts[id] + + if layout.isRaw { + m.guiManager.SetLayout(layout.Layout) + m.layouts[id].rendered = true + m.currentLayout = id + + return + } + m.leftPent = m.layouts[id].leftPent m.rightPent = m.layouts[id].rightPent m.currentLayout = id @@ -500,8 +536,79 @@ func (m *EscapeMenu) IsOpen() bool { return m.isOpen } +// Advance computes the state of the elements of the menu overtime +func (m *EscapeMenu) Advance(elapsed float64) error { + if m.keyBindingMenu != nil { + if err := m.keyBindingMenu.Advance(elapsed); err != nil { + return err + } + } + + return nil +} + +// Render will render the escape menu on the target surface +func (m *EscapeMenu) Render(target d2interface.Surface) error { + if m.isOpen { + if err := m.keyBindingMenu.Render(target); err != nil { + return err + } + } + + return nil +} + +// OnMouseButtonDown triggers whnever a mous button is pressed +func (m *EscapeMenu) OnMouseButtonDown(event d2interface.MouseEvent) bool { + if !m.isOpen { + return false + } + + if m.currentLayout == configureControlsLayoutID { + if err := m.keyBindingMenu.onMouseButtonDown(event); err != nil { + log.Printf("unable to handle mouse down on configure controls menu: %v", err) + } + } + + return false +} + +// OnMouseButtonUp triggers whenever a mouse button is released +func (m *EscapeMenu) OnMouseButtonUp(event d2interface.MouseEvent) bool { + if !m.isOpen { + return false + } + + if m.currentLayout == configureControlsLayoutID { + m.keyBindingMenu.onMouseButtonUp() + } + + return false +} + +// OnMouseMove triggers whenever the mouse moves within the renderer +func (m *EscapeMenu) OnMouseMove(event d2interface.MouseMoveEvent) bool { + if !m.isOpen { + return false + } + + if m.currentLayout == configureControlsLayoutID { + m.keyBindingMenu.onMouseMove(event) + } + + return false +} + // OnKeyDown defines the actions of the Escape Menu when a key is pressed func (m *EscapeMenu) OnKeyDown(event d2interface.KeyEvent) bool { + if m.keyBindingMenu.IsOpen() { + if err := m.keyBindingMenu.OnKeyDown(event); err != nil { + log.Printf("unable to handle key down on configure controls menu: %v", err) + } + + return false + } + switch event.Key() { case d2enum.KeyUp: m.onUpKey() diff --git a/d2game/d2player/game_controls.go b/d2game/d2player/game_controls.go index 37c0c8f0..8b423ba6 100644 --- a/d2game/d2player/game_controls.go +++ b/d2game/d2player/game_controls.go @@ -214,7 +214,7 @@ func NewGameControls( term d2interface.Terminal, ui *d2ui.UIManager, guiManager *d2gui.GuiManager, - + keyMap *KeyMap, isSinglePlayer bool, ) (*GameControls, error) { var inventoryRecordKey string @@ -350,7 +350,6 @@ func NewGameControls( return nil, err } - keyMap := getDefaultKeyMap() helpOverlay := NewHelpOverlay(asset, renderer, ui, guiManager, keyMap) hud := NewHUD(asset, ui, hero, helpOverlay, newMiniPanel(asset, ui, isSinglePlayer), actionableRegions, mapEngine, mapRenderer) @@ -360,7 +359,6 @@ func NewGameControls( hoverLabel.SetBackgroundColor(d2util.Color(blackAlpha50percent)) gc := &GameControls{ - keyMap: keyMap, asset: asset, ui: ui, renderer: renderer, @@ -373,6 +371,7 @@ func NewGameControls( skilltree: newSkillTree(hero.Skills, hero.Class, asset, ui), heroStatsPanel: NewHeroStatsPanel(asset, ui, hero.Name(), hero.Class, hero.Stats), HelpOverlay: helpOverlay, + keyMap: keyMap, hud: hud, bottomMenuRect: &d2geom.Rectangle{ Left: menuBottomRectX, @@ -639,6 +638,11 @@ func (g *GameControls) OnMouseMove(event d2interface.MouseMoveEvent) bool { return false } +// OnMouseButtonUp handles mouse button presses +func (g *GameControls) OnMouseButtonUp(event d2interface.MouseEvent) bool { + return false +} + // OnMouseButtonDown handles mouse button presses func (g *GameControls) OnMouseButtonDown(event d2interface.MouseEvent) bool { mx, my := event.X(), event.Y() @@ -698,6 +702,11 @@ func (g *GameControls) Load() { // Advance advances the state of the GameControls func (g *GameControls) Advance(elapsed float64) error { g.mapRenderer.Advance(elapsed) + + if err := g.escapeMenu.Advance(elapsed); err != nil { + return err + } + return nil } @@ -766,6 +775,10 @@ func (g *GameControls) Render(target d2interface.Surface) error { return err } + if err := g.escapeMenu.Render(target); err != nil { + return err + } + return nil } diff --git a/d2game/d2player/help_overlay.go b/d2game/d2player/help_overlay.go index 8414d3c8..58f64cf0 100644 --- a/d2game/d2player/help_overlay.go +++ b/d2game/d2player/help_overlay.go @@ -339,25 +339,25 @@ func (h *HelpOverlay) setupBulletedList() { // "Ctrl" should be hotkey // "Hold Down <%s> to Run" {text: fmt.Sprintf( h.asset.TranslateString("StrHelp2"), - h.keyMap.GetKeysForGameEvent(d2enum.HoldRun).Primary.GetString(), + h.keyMap.KeyToString(h.keyMap.GetKeysForGameEvent(d2enum.HoldRun).Primary), )}, // "Alt" should be hotkey // "Hold down <%s> to highlight items on the ground" {text: fmt.Sprintf( h.asset.TranslateString("StrHelp3"), - h.keyMap.GetKeysForGameEvent(d2enum.HoldShowGroundItems).Primary.GetString(), + h.keyMap.KeyToString(h.keyMap.GetKeysForGameEvent(d2enum.HoldShowGroundItems).Primary), )}, // "Shift" should be hotkey // "Hold down <%s> to attack while standing still" {text: fmt.Sprintf( h.asset.TranslateString("StrHelp4"), - h.keyMap.GetKeysForGameEvent(d2enum.HoldStandStill).Primary.GetString(), + h.keyMap.KeyToString(h.keyMap.GetKeysForGameEvent(d2enum.HoldStandStill).Primary), )}, // "Tab" should be hotkey // "Hit <%s> to toggle the automap on and off" {text: fmt.Sprintf( h.asset.TranslateString("StrHelp5"), - h.keyMap.GetKeysForGameEvent(d2enum.ToggleAutomap).Primary.GetString(), + h.keyMap.KeyToString(h.keyMap.GetKeysForGameEvent(d2enum.ToggleAutomap).Primary), )}, // "Hit to bring up the Game Menu" @@ -372,7 +372,7 @@ func (h *HelpOverlay) setupBulletedList() { // "H" should be hotkey, {text: fmt.Sprintf( h.asset.TranslateString("StrHelp8a"), - h.keyMap.GetKeysForGameEvent(d2enum.ToggleHelpScreen).Primary.GetString(), + h.keyMap.KeyToString(h.keyMap.GetKeysForGameEvent(d2enum.ToggleHelpScreen).Primary), )}, } @@ -576,7 +576,7 @@ func (h *HelpOverlay) createLabel(c callout) { newLabel.SetText(c.LabelText) newLabel.SetPosition(c.LabelX, c.LabelY) h.text = append(h.text, newLabel) - newLabel.Alignment = d2gui.HorizontalAlignCenter + newLabel.Alignment = d2ui.HorizontalAlignCenter } func (h *HelpOverlay) createCallout(c callout) { @@ -584,7 +584,7 @@ func (h *HelpOverlay) createCallout(c callout) { newLabel.Color[0] = color.White newLabel.SetText(c.LabelText) newLabel.SetPosition(c.LabelX, c.LabelY) - newLabel.Alignment = d2gui.HorizontalAlignCenter + newLabel.Alignment = d2ui.HorizontalAlignCenter ww, hh := newLabel.GetTextMetrics(c.LabelText) h.text = append(h.text, newLabel) _ = ww diff --git a/d2game/d2player/hero_stats_panel.go b/d2game/d2player/hero_stats_panel.go index 8d3355fd..3790fafa 100644 --- a/d2game/d2player/hero_stats_panel.go +++ b/d2game/d2player/hero_stats_panel.go @@ -8,7 +8,6 @@ import ( "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset" - "github.com/OpenDiablo2/OpenDiablo2/d2core/d2gui" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2hero" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2ui" ) @@ -357,7 +356,7 @@ func (s *HeroStatsPanel) createStatValueLabel(stat, x, y int) *d2ui.Label { func (s *HeroStatsPanel) createTextLabel(element PanelText) *d2ui.Label { label := s.uiManager.NewLabel(element.Font, d2resource.PaletteStatic) if element.AlignCenter { - label.Alignment = d2gui.HorizontalAlignCenter + label.Alignment = d2ui.HorizontalAlignCenter } label.SetText(element.Text) diff --git a/d2game/d2player/hud.go b/d2game/d2player/hud.go index bbeff94d..d50344b9 100644 --- a/d2game/d2player/hud.go +++ b/d2game/d2player/hud.go @@ -12,7 +12,6 @@ import ( "github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2util" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset" - "github.com/OpenDiablo2/OpenDiablo2/d2core/d2gui" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2mapengine" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2mapentity" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2maprenderer" @@ -135,11 +134,11 @@ func NewHUD( mapRenderer *d2maprenderer.MapRenderer, ) *HUD { nameLabel := ui.NewLabel(d2resource.Font16, d2resource.PaletteStatic) - nameLabel.Alignment = d2gui.HorizontalAlignCenter + nameLabel.Alignment = d2ui.HorizontalAlignCenter nameLabel.SetText(d2ui.ColorTokenize("", d2ui.ColorTokenServer)) zoneLabel := ui.NewLabel(d2resource.Font30, d2resource.PaletteUnits) - zoneLabel.Alignment = d2gui.HorizontalAlignCenter + zoneLabel.Alignment = d2ui.HorizontalAlignCenter return &HUD{ asset: asset, diff --git a/d2game/d2player/key_binding_menu.go b/d2game/d2player/key_binding_menu.go new file mode 100644 index 00000000..1dce1d42 --- /dev/null +++ b/d2game/d2player/key_binding_menu.go @@ -0,0 +1,733 @@ +package d2player + +import ( + "image/color" + "log" + + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2util" + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset" + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2gui" + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2ui" +) + +const ( + selectionBackgroundColor = 0x000000d0 + defaultGameEvent = -1 + + keyBindingMenuWidth = 620 + keyBindingMenuHeight = 375 + keyBindingMenuX = 90 + keyBindingMenuY = 75 + + keyBindingMenuPaddingX = 17 + keyBindingSettingPaddingY = 19 + + keyBindingMenuHeaderHeight = 24 + keyBindingMenuHeaderSpacer1 = 131 + keyBindingMenuHeaderSpacer2 = 86 + + keyBindingMenuBindingSpacerBetween = 25 + keyBindingMenuBindingSpacerLeft = 17 + keyBindingMenuBindingDescWidth = 190 + keyBindingMenuBindingDescHeight = 0 + keyBindingMenuBindingPrimaryWidth = 190 + keyBindingMenuBindingPrimaryHeight = 0 + keyBindingMenuBindingSecondaryWidth = 90 + keyBindingMenuBindingSecondaryHeight = 0 +) + +type bindingChange struct { + target *KeyBinding + primary d2enum.Key + secondary d2enum.Key +} + +// KeyBindingMenu represents the menu to view/edit the +// key bindings +type KeyBindingMenu struct { + *d2gui.Box + + asset *d2asset.AssetManager + renderer d2interface.Renderer + ui *d2ui.UIManager + guiManager *d2gui.GuiManager + keyMap *KeyMap + escapeMenu *EscapeMenu + + mainLayout *d2gui.Layout + contentLayout *d2gui.Layout + scrollbar *d2gui.LayoutScrollbar + bindingLayouts []*bindingLayout + changesToBeSaved map[d2enum.GameEvent]*bindingChange + + isAwaitingKeyDown bool + currentBindingModifierType KeyBindingType + currentBindingModifier d2enum.GameEvent + currentBindingLayout *bindingLayout + lastBindingLayout *bindingLayout +} + +// NewKeyBindingMenu generates a new instance of the "Configure Keys" +// menu found in the options +func NewKeyBindingMenu( + asset *d2asset.AssetManager, + renderer d2interface.Renderer, + ui *d2ui.UIManager, + guiManager *d2gui.GuiManager, + keyMap *KeyMap, + escapeMenu *EscapeMenu, +) *KeyBindingMenu { + mainLayout := d2gui.CreateLayout(renderer, d2gui.PositionTypeAbsolute, asset) + contentLayout := mainLayout.AddLayout(d2gui.PositionTypeAbsolute) + + ret := &KeyBindingMenu{ + keyMap: keyMap, + asset: asset, + ui: ui, + guiManager: guiManager, + renderer: renderer, + mainLayout: mainLayout, + contentLayout: contentLayout, + bindingLayouts: []*bindingLayout{}, + changesToBeSaved: make(map[d2enum.GameEvent]*bindingChange), + escapeMenu: escapeMenu, + } + + ret.Box = d2gui.NewBox( + asset, renderer, ui, ret.mainLayout, + keyBindingMenuWidth, keyBindingMenuHeight, + keyBindingMenuX, keyBindingMenuY, "", + ) + + ret.Box.SetPadding(keyBindingMenuPaddingX, keyBindingSettingPaddingY) + + ret.Box.SetOptions([]*d2gui.LabelButton{ + d2gui.NewLabelButton(0, 0, "Cancel", d2util.Color(d2gui.ColorRed), func() { + if err := ret.onCancelClicked(); err != nil { + log.Printf("error while clicking option Cancel: %v", err) + } + }), + d2gui.NewLabelButton(0, 0, "Default", d2util.Color(d2gui.ColorBlue), func() { + if err := ret.onDefaultClicked(); err != nil { + log.Printf("error while clicking option Default: %v", err) + } + }), + d2gui.NewLabelButton(0, 0, "Accept", d2util.Color(d2gui.ColorGreen), func() { + if err := ret.onAcceptClicked(); err != nil { + log.Printf("error while clicking option Accept: %v", err) + } + }), + }) + + return ret +} + +// Close will disable the render of the menu and clear +// the current selection +func (menu *KeyBindingMenu) Close() error { + menu.Box.Close() + + if err := menu.clearSelection(); err != nil { + return err + } + + return nil +} + +// Load will setup the layouts of the menu +func (menu *KeyBindingMenu) Load() error { + if err := menu.Box.Load(); err != nil { + return err + } + + mainLayoutW, mainLayoutH := menu.mainLayout.GetSize() + + headerLayout := menu.contentLayout.AddLayout(d2gui.PositionTypeHorizontal) + headerLayout.SetSize(mainLayoutW, keyBindingMenuHeaderHeight) + + if _, err := headerLayout.AddLabelWithColor( + menu.asset.TranslateString("CfgFunction"), + d2gui.FontStyleFormal11Units, + d2util.Color(d2gui.ColorBrown), + ); err != nil { + return err + } + + headerLayout.AddSpacerStatic(keyBindingMenuHeaderSpacer1, keyBindingMenuHeaderHeight) + + if _, err := headerLayout.AddLabelWithColor( + menu.asset.TranslateString("CfgPrimaryKey"), + d2gui.FontStyleFormal11Units, + d2util.Color(d2gui.ColorBrown), + ); err != nil { + return err + } + + headerLayout.AddSpacerStatic(keyBindingMenuHeaderSpacer2, 1) + + if _, err := headerLayout.AddLabelWithColor( + menu.asset.TranslateString("CfgSecondaryKey"), + d2gui.FontStyleFormal11Units, + d2util.Color(d2gui.ColorBrown), + ); err != nil { + return err + } + + headerLayout.SetVerticalAlign(d2gui.VerticalAlignMiddle) + + bindingWrapper := menu.contentLayout.AddLayout(d2gui.PositionTypeAbsolute) + bindingWrapper.SetPosition(0, keyBindingMenuHeaderHeight) + bindingWrapper.SetSize(mainLayoutW, mainLayoutH-keyBindingMenuHeaderHeight) + + bindingLayout := menu.generateLayout() + + menu.Box.GetLayout().AdjustEntryPlacement() + menu.mainLayout.AdjustEntryPlacement() + menu.contentLayout.AdjustEntryPlacement() + + menu.scrollbar = d2gui.NewLayoutScrollbar(bindingWrapper, bindingLayout) + + if err := menu.scrollbar.Load(menu.ui); err != nil { + return err + } + + bindingWrapper.AddLayoutFromSource(bindingLayout) + bindingWrapper.AdjustEntryPlacement() + + return nil +} + +type keyBindingSetting struct { + label string + gameEvent d2enum.GameEvent +} + +func (menu *KeyBindingMenu) getBindingGroups() [][]keyBindingSetting { + return [][]keyBindingSetting{ + { + {menu.asset.TranslateString("CfgCharacter"), d2enum.ToggleCharacterPanel}, + {menu.asset.TranslateString("CfgInventory"), d2enum.ToggleInventoryPanel}, + {menu.asset.TranslateString("CfgParty"), d2enum.TogglePartyPanel}, + {menu.asset.TranslateString("Cfghireling"), d2enum.ToggleHirelingPanel}, + {menu.asset.TranslateString("CfgMessageLog"), d2enum.ToggleMessageLog}, + {menu.asset.TranslateString("CfgQuestLog"), d2enum.ToggleQuestLog}, + {menu.asset.TranslateString("CfgHelp"), d2enum.ToggleHelpScreen}, + }, + { + {menu.asset.TranslateString("CfgSkillTree"), d2enum.ToggleSkillTreePanel}, + {menu.asset.TranslateString("CfgSkillPick"), d2enum.ToggleRightSkillSelector}, + {menu.asset.TranslateString("CfgSkill1"), d2enum.UseSkill1}, + {menu.asset.TranslateString("CfgSkill2"), d2enum.UseSkill2}, + {menu.asset.TranslateString("CfgSkill3"), d2enum.UseSkill3}, + {menu.asset.TranslateString("CfgSkill4"), d2enum.UseSkill4}, + {menu.asset.TranslateString("CfgSkill5"), d2enum.UseSkill5}, + {menu.asset.TranslateString("CfgSkill6"), d2enum.UseSkill6}, + {menu.asset.TranslateString("CfgSkill7"), d2enum.UseSkill7}, + {menu.asset.TranslateString("CfgSkill8"), d2enum.UseSkill8}, + {menu.asset.TranslateString("CfgSkill9"), d2enum.UseSkill9}, + {menu.asset.TranslateString("CfgSkill10"), d2enum.UseSkill10}, + {menu.asset.TranslateString("CfgSkill11"), d2enum.UseSkill11}, + {menu.asset.TranslateString("CfgSkill12"), d2enum.UseSkill12}, + {menu.asset.TranslateString("CfgSkill13"), d2enum.UseSkill13}, + {menu.asset.TranslateString("CfgSkill14"), d2enum.UseSkill14}, + {menu.asset.TranslateString("CfgSkill15"), d2enum.UseSkill15}, + {menu.asset.TranslateString("CfgSkill16"), d2enum.UseSkill16}, + {menu.asset.TranslateString("Cfgskillup"), d2enum.SelectPreviousSkill}, + {menu.asset.TranslateString("Cfgskilldown"), d2enum.SelectNextSkill}, + }, + { + {menu.asset.TranslateString("CfgBeltShow"), d2enum.ToggleBelts}, + {menu.asset.TranslateString("CfgBelt1"), d2enum.UseBeltSlot1}, + {menu.asset.TranslateString("CfgBelt2"), d2enum.UseBeltSlot2}, + {menu.asset.TranslateString("CfgBelt3"), d2enum.UseBeltSlot3}, + {menu.asset.TranslateString("CfgBelt4"), d2enum.UseBeltSlot4}, + {menu.asset.TranslateString("Cfgswapweapons"), d2enum.SwapWeapons}, + }, + { + {menu.asset.TranslateString("CfgChat"), d2enum.ToggleChatBox}, + {menu.asset.TranslateString("CfgRun"), d2enum.HoldRun}, + {menu.asset.TranslateString("CfgRunLock"), d2enum.ToggleRunWalk}, + {menu.asset.TranslateString("CfgStandStill"), d2enum.HoldStandStill}, + {menu.asset.TranslateString("CfgShowItems"), d2enum.HoldShowGroundItems}, + {menu.asset.TranslateString("CfgTogglePortraits"), d2enum.HoldShowPortraits}, + }, + { + {menu.asset.TranslateString("CfgAutoMap"), d2enum.ToggleAutomap}, + {menu.asset.TranslateString("CfgAutoMapCenter"), d2enum.CenterAutomap}, + {menu.asset.TranslateString("CfgAutoMapParty"), d2enum.TogglePartyOnAutomap}, + {menu.asset.TranslateString("CfgAutoMapNames"), d2enum.ToggleNamesOnAutomap}, + {menu.asset.TranslateString("CfgToggleminimap"), d2enum.ToggleMiniMap}, + }, + { + {menu.asset.TranslateString("CfgSay0"), d2enum.SayHelp}, + {menu.asset.TranslateString("CfgSay1"), d2enum.SayFollowMe}, + {menu.asset.TranslateString("CfgSay2"), d2enum.SayThisIsForYou}, + {menu.asset.TranslateString("CfgSay3"), d2enum.SayThanks}, + {menu.asset.TranslateString("CfgSay4"), d2enum.SaySorry}, + {menu.asset.TranslateString("CfgSay5"), d2enum.SayBye}, + {menu.asset.TranslateString("CfgSay6"), d2enum.SayNowYouDie}, + {menu.asset.TranslateString("CfgSay7"), d2enum.SayNowYouDie}, + }, + { + {menu.asset.TranslateString("CfgSnapshot"), d2enum.TakeScreenShot}, + {menu.asset.TranslateString("CfgClearScreen"), d2enum.ClearScreen}, + {menu.asset.TranslateString("Cfgcleartextmsg"), d2enum.ClearMessages}, + }, + } +} + +func (menu *KeyBindingMenu) generateLayout() *d2gui.Layout { + groups := menu.getBindingGroups() + + wrapper := d2gui.CreateLayout(menu.renderer, d2gui.PositionTypeAbsolute, menu.asset) + layout := wrapper.AddLayout(d2gui.PositionTypeVertical) + + for i, settingsGroup := range groups { + groupLayout := layout.AddLayout(d2gui.PositionTypeVertical) + + for _, setting := range settingsGroup { + bl := bindingLayout{} + + settingLayout := groupLayout.AddLayout(d2gui.PositionTypeHorizontal) + settingLayout.AddSpacerStatic(keyBindingMenuBindingSpacerLeft, 0) + descLabelWrapper := settingLayout.AddLayout(d2gui.PositionTypeAbsolute) + descLabelWrapper.SetSize(keyBindingMenuBindingDescWidth, keyBindingMenuBindingDescHeight) + + descLabel, _ := descLabelWrapper.AddLabel(setting.label, d2gui.FontStyleFormal11Units) + descLabel.SetHoverColor(d2util.Color(d2gui.ColorBlue)) + + bl.wrapperLayout = settingLayout + bl.descLabel = descLabel + bl.descLayout = descLabelWrapper + + if binding := menu.keyMap.GetKeysForGameEvent(setting.gameEvent); binding != nil { + primaryStr := menu.keyMap.KeyToString(binding.Primary) + secondaryStr := menu.keyMap.KeyToString(binding.Secondary) + primaryCol := menu.getKeyColor(binding.Primary) + secondaryCol := menu.getKeyColor(binding.Secondary) + + if binding.IsEmpty() { + primaryCol = d2util.Color(d2gui.ColorRed) + secondaryCol = d2util.Color(d2gui.ColorRed) + } + + primaryKeyLabelWrapper := settingLayout.AddLayout(d2gui.PositionTypeAbsolute) + primaryKeyLabelWrapper.SetSize(keyBindingMenuBindingPrimaryWidth, keyBindingMenuBindingPrimaryHeight) + primaryLabel, _ := primaryKeyLabelWrapper.AddLabelWithColor(primaryStr, d2gui.FontStyleFormal11Units, primaryCol) + primaryLabel.SetHoverColor(d2util.Color(d2gui.ColorBlue)) + + bl.primaryLabel = primaryLabel + bl.primaryLayout = primaryKeyLabelWrapper + bl.gameEvent = setting.gameEvent + + secondaryKeyLabelWrapper := settingLayout.AddLayout(d2gui.PositionTypeAbsolute) + secondaryKeyLabelWrapper.SetSize(keyBindingMenuBindingSecondaryWidth, keyBindingMenuBindingSecondaryHeight) + secondaryLabel, _ := secondaryKeyLabelWrapper.AddLabelWithColor(secondaryStr, d2gui.FontStyleFormal11Units, secondaryCol) + secondaryLabel.SetHoverColor(d2util.Color(d2gui.ColorBlue)) + + bl.secondaryLabel = secondaryLabel + bl.secondaryLayout = secondaryKeyLabelWrapper + bl.binding = binding + } + + menu.bindingLayouts = append(menu.bindingLayouts, &bl) + } + + if i < len(groups)-1 { + layout.AddSpacerStatic(0, keyBindingMenuBindingSpacerBetween) + } + } + + return wrapper +} + +func (menu *KeyBindingMenu) getKeyColor(key d2enum.Key) color.RGBA { + switch key { + case -1: + return d2util.Color(d2gui.ColorGrey) + default: + return d2util.Color(d2gui.ColorBrown) + } +} + +func (menu *KeyBindingMenu) setSelection(bl *bindingLayout, bindingType KeyBindingType, gameEvent d2enum.GameEvent) error { + if menu.currentBindingLayout != nil { + menu.lastBindingLayout = menu.currentBindingLayout + if err := menu.currentBindingLayout.Reset(); err != nil { + return err + } + } + + menu.currentBindingModifier = gameEvent + menu.currentBindingLayout = bl + + if bindingType == KeyBindingTypePrimary { + menu.currentBindingLayout.primaryLabel.SetIsBlinking(true) + } else if bindingType == KeyBindingTypeSecondary { + menu.currentBindingLayout.secondaryLabel.SetIsBlinking(true) + } + + menu.currentBindingModifierType = bindingType + menu.isAwaitingKeyDown = true + + if err := bl.descLabel.SetIsHovered(true); err != nil { + return err + } + + if err := bl.primaryLabel.SetIsHovered(true); err != nil { + return err + } + + if err := bl.secondaryLabel.SetIsHovered(true); err != nil { + return err + } + + return nil +} + +func (menu *KeyBindingMenu) onMouseButtonDown(event d2interface.MouseEvent) error { + if !menu.IsOpen() { + return nil + } + + menu.Box.OnMouseButtonDown(event) + + if menu.scrollbar != nil { + if menu.scrollbar.IsInSliderRect(event.X(), event.Y()) { + menu.scrollbar.SetSliderClicked(true) + menu.scrollbar.OnSliderMouseClick(event) + + return nil + } + + if menu.scrollbar.IsInArrowUpRect(event.X(), event.Y()) { + if !menu.scrollbar.IsArrowUpClicked() { + menu.scrollbar.SetArrowUpClicked(true) + } + + menu.scrollbar.OnArrowUpClick() + + return nil + } + + if menu.scrollbar.IsInArrowDownRect(event.X(), event.Y()) { + if !menu.scrollbar.IsArrowDownClicked() { + menu.scrollbar.SetArrowDownClicked(true) + } + + menu.scrollbar.OnArrowDownClick() + + return nil + } + } + + for _, bl := range menu.bindingLayouts { + gameEvent, typ := bl.GetPointedLayoutAndLabel(event.X(), event.Y()) + + if gameEvent != -1 { + if err := menu.setSelection(bl, typ, gameEvent); err != nil { + return err + } + + break + } else if menu.currentBindingLayout != nil { + if err := menu.clearSelection(); err != nil { + return err + } + } + } + + return nil +} + +func (menu *KeyBindingMenu) onMouseMove(event d2interface.MouseMoveEvent) { + if !menu.IsOpen() { + return + } + + if menu.scrollbar != nil && menu.scrollbar.IsSliderClicked() { + menu.scrollbar.OnMouseMove(event) + } +} + +func (menu *KeyBindingMenu) onMouseButtonUp() { + if !menu.IsOpen() { + return + } + + if menu.scrollbar != nil { + menu.scrollbar.SetSliderClicked(false) + menu.scrollbar.SetArrowDownClicked(false) + menu.scrollbar.SetArrowUpClicked(false) + } +} + +func (menu *KeyBindingMenu) getPendingChangeByKey(key d2enum.Key) (*bindingChange, *KeyBinding, d2enum.GameEvent, KeyBindingType) { + var ( + existingBinding *KeyBinding + gameEvent d2enum.GameEvent + bindingType KeyBindingType + ) + + for ge, existingChange := range menu.changesToBeSaved { + if existingChange.primary == key { + bindingType = KeyBindingTypePrimary + } else if existingChange.secondary == key { + bindingType = KeyBindingTypeSecondary + } + + if bindingType != -1 { + existingBinding = existingChange.target + gameEvent = ge + + return existingChange, existingBinding, gameEvent, bindingType + } + } + + return nil, nil, -1, KeyBindingTypeNone +} + +func (menu *KeyBindingMenu) saveKeyChange(key d2enum.Key) error { + changeExisting, existingBinding, gameEvent, bindingType := menu.getPendingChangeByKey(key) + + if changeExisting == nil { + existingBinding, gameEvent, bindingType = menu.keyMap.GetBindingByKey(key) + } + + if existingBinding != nil && changeExisting == nil { + changeExisting = &bindingChange{ + target: existingBinding, + primary: existingBinding.Primary, + secondary: existingBinding.Secondary, + } + + menu.changesToBeSaved[gameEvent] = changeExisting + } + + changeCurrent := menu.changesToBeSaved[menu.currentBindingLayout.gameEvent] + if changeCurrent == nil { + changeCurrent = &bindingChange{ + target: menu.currentBindingLayout.binding, + primary: menu.currentBindingLayout.binding.Primary, + secondary: menu.currentBindingLayout.binding.Secondary, + } + + menu.changesToBeSaved[menu.currentBindingLayout.gameEvent] = changeCurrent + } + + switch menu.currentBindingModifierType { + case KeyBindingTypePrimary: + changeCurrent.primary = key + case KeyBindingTypeSecondary: + changeCurrent.secondary = key + } + + if changeExisting != nil { + if bindingType == KeyBindingTypePrimary { + changeExisting.primary = -1 + } + + if bindingType == KeyBindingTypeSecondary { + changeExisting.secondary = -1 + } + } + + if err := menu.setBindingLabels( + changeCurrent.primary, + changeCurrent.secondary, + menu.currentBindingLayout, + ); err != nil { + return err + } + + if changeExisting != nil { + for _, bindingLayout := range menu.bindingLayouts { + if bindingLayout.binding == changeExisting.target { + if err := menu.setBindingLabels(changeExisting.primary, changeExisting.secondary, bindingLayout); err != nil { + return err + } + } + } + } + + return nil +} + +func (menu *KeyBindingMenu) setBindingLabels(primary, secondary d2enum.Key, bl *bindingLayout) error { + noneStr := menu.keyMap.KeyToString(-1) + + if primary != -1 { + if err := bl.SetPrimaryBindingTextAndColor(menu.keyMap.KeyToString(primary), d2util.Color(d2gui.ColorBrown)); err != nil { + return err + } + } else { + if err := bl.SetPrimaryBindingTextAndColor(noneStr, d2util.Color(d2gui.ColorGrey)); err != nil { + return err + } + } + + if secondary != -1 { + if err := bl.SetSecondaryBindingTextAndColor(menu.keyMap.KeyToString(secondary), d2util.Color(d2gui.ColorBrown)); err != nil { + return err + } + } else { + if err := bl.SetSecondaryBindingTextAndColor(noneStr, d2util.Color(d2gui.ColorGrey)); err != nil { + return err + } + } + + if primary == -1 && secondary == -1 { + if err := bl.primaryLabel.SetColor(d2util.Color(d2gui.ColorRed)); err != nil { + return err + } + + if err := bl.secondaryLabel.SetColor(d2util.Color(d2gui.ColorRed)); err != nil { + return err + } + } + + return nil +} + +func (menu *KeyBindingMenu) onCancelClicked() error { + for gameEvent := range menu.changesToBeSaved { + for _, bindingLayout := range menu.bindingLayouts { + if bindingLayout.gameEvent == gameEvent { + if err := menu.setBindingLabels(bindingLayout.binding.Primary, bindingLayout.binding.Secondary, bindingLayout); err != nil { + return err + } + } + } + } + + menu.changesToBeSaved = make(map[d2enum.GameEvent]*bindingChange) + if menu.currentBindingLayout != nil { + if err := menu.clearSelection(); err != nil { + return err + } + } + + if err := menu.Close(); err != nil { + return err + } + + menu.escapeMenu.showLayout(optionsLayoutID) + + return nil +} + +func (menu *KeyBindingMenu) reload() error { + for _, bl := range menu.bindingLayouts { + if bl.binding != nil { + if err := menu.setBindingLabels(bl.binding.Primary, bl.binding.Secondary, bl); err != nil { + return err + } + } + } + + return nil +} + +func (menu *KeyBindingMenu) clearSelection() error { + if menu.currentBindingLayout != nil { + if err := menu.currentBindingLayout.Reset(); err != nil { + return err + } + + menu.lastBindingLayout = menu.currentBindingLayout + menu.currentBindingLayout = nil + menu.currentBindingModifier = -1 + menu.currentBindingModifierType = -1 + } + + return nil +} + +func (menu *KeyBindingMenu) onDefaultClicked() error { + menu.keyMap.ResetToDefault() + + if err := menu.reload(); err != nil { + return err + } + + menu.changesToBeSaved = make(map[d2enum.GameEvent]*bindingChange) + + return menu.clearSelection() +} + +func (menu *KeyBindingMenu) onAcceptClicked() error { + for gameEvent, change := range menu.changesToBeSaved { + menu.keyMap.SetPrimaryBinding(gameEvent, change.primary) + menu.keyMap.SetSecondaryBinding(gameEvent, change.primary) + } + + menu.changesToBeSaved = make(map[d2enum.GameEvent]*bindingChange) + + return menu.clearSelection() +} + +// OnKeyDown will assign the new key to the selected binding if any +func (menu *KeyBindingMenu) OnKeyDown(event d2interface.KeyEvent) error { + if menu.isAwaitingKeyDown { + key := event.Key() + + if key == d2enum.KeyEscape { + if menu.currentBindingLayout != nil { + menu.lastBindingLayout = menu.currentBindingLayout + + if err := menu.currentBindingLayout.Reset(); err != nil { + return err + } + + if err := menu.clearSelection(); err != nil { + return err + } + } + } else { + if err := menu.saveKeyChange(key); err != nil { + return err + } + } + + menu.isAwaitingKeyDown = false + } + + return nil +} + +// Advance computes the state of the elements of the menu overtime +func (menu *KeyBindingMenu) Advance(elapsed float64) error { + if menu.scrollbar != nil { + if err := menu.scrollbar.Advance(elapsed); err != nil { + return err + } + } + + return nil +} + +// Render draws the different element of the menu on the target surface +func (menu *KeyBindingMenu) Render(target d2interface.Surface) error { + if menu.IsOpen() { + if err := menu.Box.Render(target); err != nil { + return err + } + + if menu.scrollbar != nil { + menu.scrollbar.Render(target) + } + + if menu.currentBindingLayout != nil { + x, y := menu.currentBindingLayout.wrapperLayout.Sx, menu.currentBindingLayout.wrapperLayout.Sy + w, h := menu.currentBindingLayout.wrapperLayout.GetSize() + + target.PushTranslation(x, y) + target.DrawRect(w, h, d2util.Color(selectionBackgroundColor)) + target.Pop() + } + } + + return nil +} diff --git a/d2game/d2player/key_map.go b/d2game/d2player/key_map.go index ec29e590..a131a5ac 100644 --- a/d2game/d2player/key_map.go +++ b/d2game/d2player/key_map.go @@ -4,28 +4,143 @@ import ( "sync" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset" ) // KeyMap represents the key mappings of the game. Each game event // can be associated to 2 different keys. A key of -1 means none type KeyMap struct { - mutex sync.RWMutex - mapping map[d2enum.Key]d2enum.GameEvent - controls map[d2enum.GameEvent]*KeyBinding + mutex sync.RWMutex + mapping map[d2enum.Key]d2enum.GameEvent + controls map[d2enum.GameEvent]*KeyBinding + keyToStringMapping map[d2enum.Key]string } +// KeyBindingType defines whether it's a primary or +// secondary binding +type KeyBindingType int + +// Values defining the type of key binding +const ( + KeyBindingTypeNone KeyBindingType = iota + KeyBindingTypePrimary + KeyBindingTypeSecondary +) + // NewKeyMap returns a new instance of a KeyMap -func NewKeyMap() *KeyMap { +func NewKeyMap(asset *d2asset.AssetManager) *KeyMap { return &KeyMap{ - mapping: make(map[d2enum.Key]d2enum.GameEvent), - controls: make(map[d2enum.GameEvent]*KeyBinding), + mapping: make(map[d2enum.Key]d2enum.GameEvent), + controls: make(map[d2enum.GameEvent]*KeyBinding), + keyToStringMapping: getKeyStringMapping(asset), } } +func getKeyStringMapping(assetManager *d2asset.AssetManager) map[d2enum.Key]string { + return map[d2enum.Key]string{ + -1: assetManager.TranslateString("KeyNone"), + d2enum.KeyTilde: "~", + d2enum.KeyHome: assetManager.TranslateString("KeyHome"), + d2enum.KeyControl: assetManager.TranslateString("KeyControl"), + d2enum.KeyShift: assetManager.TranslateString("KeyShift"), + d2enum.KeySpace: assetManager.TranslateString("KeySpace"), + d2enum.KeyAlt: assetManager.TranslateString("KeyAlt"), + d2enum.KeyTab: assetManager.TranslateString("KeyTab"), + d2enum.Key0: "0", + d2enum.Key1: "1", + d2enum.Key2: "2", + d2enum.Key3: "3", + d2enum.Key4: "4", + d2enum.Key5: "5", + d2enum.Key6: "6", + d2enum.Key7: "7", + d2enum.Key8: "8", + d2enum.Key9: "9", + d2enum.KeyA: "A", + d2enum.KeyB: "B", + d2enum.KeyC: "C", + d2enum.KeyD: "D", + d2enum.KeyE: "E", + d2enum.KeyF: "F", + d2enum.KeyG: "G", + d2enum.KeyH: "H", + d2enum.KeyI: "I", + d2enum.KeyJ: "J", + d2enum.KeyK: "K", + d2enum.KeyL: "L", + d2enum.KeyM: "M", + d2enum.KeyN: "N", + d2enum.KeyO: "O", + d2enum.KeyP: "P", + d2enum.KeyQ: "Q", + d2enum.KeyR: "R", + d2enum.KeyS: "S", + d2enum.KeyT: "T", + d2enum.KeyU: "U", + d2enum.KeyV: "V", + d2enum.KeyW: "W", + d2enum.KeyX: "X", + d2enum.KeyY: "Y", + d2enum.KeyZ: "Z", + d2enum.KeyF1: "F1", + d2enum.KeyF2: "F2", + d2enum.KeyF3: "F3", + d2enum.KeyF4: "F4", + d2enum.KeyF5: "F5", + d2enum.KeyF6: "F6", + d2enum.KeyF7: "F7", + d2enum.KeyF8: "F8", + d2enum.KeyF9: "F9", + d2enum.KeyF10: "F10", + d2enum.KeyF11: "F11", + d2enum.KeyF12: "F12", + d2enum.KeyKP0: assetManager.TranslateString("KeyNumPad0"), + d2enum.KeyKP1: assetManager.TranslateString("KeyNumPad1"), + d2enum.KeyKP2: assetManager.TranslateString("KeyNumPad2"), + d2enum.KeyKP3: assetManager.TranslateString("KeyNumPad3"), + d2enum.KeyKP4: assetManager.TranslateString("KeyNumPad4"), + d2enum.KeyKP5: assetManager.TranslateString("KeyNumPad5"), + d2enum.KeyKP6: assetManager.TranslateString("KeyNumPad6"), + d2enum.KeyKP7: assetManager.TranslateString("KeyNumPad7"), + d2enum.KeyKP8: assetManager.TranslateString("KeyNumPad8"), + d2enum.KeyKP9: assetManager.TranslateString("KeyNumPad9"), + d2enum.KeyPrintScreen: assetManager.TranslateString("KeySnapshot"), + d2enum.KeyRightBracket: assetManager.TranslateString("KeyRBracket"), + d2enum.KeyLeftBracket: assetManager.TranslateString("KeyLBracket"), + d2enum.KeyMouse3: assetManager.TranslateString("KeyMButton"), + d2enum.KeyMouse4: assetManager.TranslateString("Key4Button"), + d2enum.KeyMouse5: assetManager.TranslateString("Key5Button"), + d2enum.KeyMouseWheelUp: assetManager.TranslateString("KeyWheelUp"), + d2enum.KeyMouseWheelDown: assetManager.TranslateString("KeyWheelDown"), + } +} +func (km *KeyMap) checkOverwrite(key d2enum.Key) (*KeyBinding, KeyBindingType) { + var ( + overwrittenBinding *KeyBinding + overwrittenBindingType KeyBindingType + ) + + for _, binding := range km.controls { + if binding.Primary == key { + binding.Primary = -1 + overwrittenBinding = binding + overwrittenBindingType = KeyBindingTypePrimary + } + + if binding.Secondary == key { + binding.Secondary = -1 + overwrittenBinding = binding + overwrittenBindingType = KeyBindingTypeSecondary + } + } + + return overwrittenBinding, overwrittenBindingType +} + // SetPrimaryBinding binds the first key for gameEvent -func (km *KeyMap) SetPrimaryBinding(gameEvent d2enum.GameEvent, key d2enum.Key) { +func (km *KeyMap) SetPrimaryBinding(gameEvent d2enum.GameEvent, key d2enum.Key) (*KeyBinding, KeyBindingType) { if key == d2enum.KeyEscape { - return + return nil, -1 } km.mutex.Lock() @@ -35,17 +150,21 @@ func (km *KeyMap) SetPrimaryBinding(gameEvent d2enum.GameEvent, key d2enum.Key) km.controls[gameEvent] = &KeyBinding{} } + overwrittenBinding, overwrittenBindingType := km.checkOverwrite(key) + currentKey := km.controls[gameEvent].Primary delete(km.mapping, currentKey) km.mapping[key] = gameEvent km.controls[gameEvent].Primary = key + + return overwrittenBinding, overwrittenBindingType } // SetSecondaryBinding binds the second key for gameEvent -func (km *KeyMap) SetSecondaryBinding(gameEvent d2enum.GameEvent, key d2enum.Key) { +func (km *KeyMap) SetSecondaryBinding(gameEvent d2enum.GameEvent, key d2enum.Key) (*KeyBinding, KeyBindingType) { if key == d2enum.KeyEscape { - return + return nil, -1 } km.mutex.Lock() @@ -55,6 +174,8 @@ func (km *KeyMap) SetSecondaryBinding(gameEvent d2enum.GameEvent, key d2enum.Key km.controls[gameEvent] = &KeyBinding{} } + overwrittenBinding, overwrittenBindingType := km.checkOverwrite(key) + currentKey := km.controls[gameEvent].Secondary delete(km.mapping, currentKey) km.mapping[key] = gameEvent @@ -64,6 +185,8 @@ func (km *KeyMap) SetSecondaryBinding(gameEvent d2enum.GameEvent, key d2enum.Key } km.controls[gameEvent].Secondary = key + + return overwrittenBinding, overwrittenBindingType } func (km *KeyMap) getGameEvent(key d2enum.Key) d2enum.GameEvent { @@ -81,25 +204,46 @@ func (km *KeyMap) GetKeysForGameEvent(gameEvent d2enum.GameEvent) *KeyBinding { return km.controls[gameEvent] } +// GetBindingByKey returns the bindings for a givent game event +func (km *KeyMap) GetBindingByKey(key d2enum.Key) (*KeyBinding, d2enum.GameEvent, KeyBindingType) { + km.mutex.RLock() + defer km.mutex.RUnlock() + + for gameEvent, binding := range km.controls { + if binding.Primary == key { + return binding, gameEvent, KeyBindingTypePrimary + } + + if binding.Secondary == key { + return binding, gameEvent, KeyBindingTypeSecondary + } + } + + return nil, -1, -1 +} + // KeyBinding holds the primary and secondary keys assigned to a GameEvent type KeyBinding struct { Primary d2enum.Key Secondary d2enum.Key } -func getDefaultKeyMap() *KeyMap { - keyMap := NewKeyMap() +// IsEmpty checks if no keys are associated to the binding +func (b KeyBinding) IsEmpty() bool { + return b.Primary == -1 && b.Secondary == -1 +} +// ResetToDefault will reset the KeyMap to the default values +func (km *KeyMap) ResetToDefault() { defaultControls := map[d2enum.GameEvent]KeyBinding{ - d2enum.ToggleCharacterPanel: {d2enum.KeyA, d2enum.KeyC}, - d2enum.ToggleInventoryPanel: {d2enum.KeyB, d2enum.KeyI}, - d2enum.ToggleHelpScreen: {d2enum.KeyH, -1}, - d2enum.TogglePartyPanel: {d2enum.KeyP, -1}, - d2enum.ToggleMessageLog: {d2enum.KeyM, -1}, - d2enum.ToggleQuestLog: {d2enum.KeyQ, -1}, - d2enum.ToggleChatOverlay: {d2enum.KeyEnter, -1}, - d2enum.ToggleAutomap: {d2enum.KeyTab, -1}, - d2enum.CenterAutomap: {d2enum.KeyHome, -1}, + d2enum.ToggleCharacterPanel: {d2enum.KeyA, d2enum.KeyC}, + d2enum.ToggleInventoryPanel: {d2enum.KeyB, d2enum.KeyI}, + d2enum.TogglePartyPanel: {d2enum.KeyP, -1}, + d2enum.ToggleHirelingPanel: {d2enum.KeyO, -1}, + d2enum.ToggleMessageLog: {d2enum.KeyM, -1}, + d2enum.ToggleQuestLog: {d2enum.KeyQ, -1}, + d2enum.ToggleHelpScreen: {d2enum.KeyH, -1}, + d2enum.ToggleSkillTreePanel: {d2enum.KeyT, -1}, d2enum.ToggleRightSkillSelector: {d2enum.KeyS, -1}, d2enum.UseSkill1: {d2enum.KeyF1, -1}, @@ -118,22 +262,59 @@ func getDefaultKeyMap() *KeyMap { d2enum.UseSkill14: {-1, -1}, d2enum.UseSkill15: {-1, -1}, d2enum.UseSkill16: {-1, -1}, - d2enum.ToggleBelts: {d2enum.KeyTilde, -1}, - d2enum.UseBeltSlot1: {d2enum.Key1, -1}, - d2enum.UseBeltSlot2: {d2enum.Key2, -1}, - d2enum.UseBeltSlot3: {d2enum.Key3, -1}, - d2enum.UseBeltSlot4: {d2enum.Key4, -1}, - d2enum.ToggleRunWalk: {d2enum.KeyR, -1}, - d2enum.HoldRun: {d2enum.KeyControl, -1}, - d2enum.HoldShowGroundItems: {d2enum.KeyAlt, -1}, - d2enum.HoldShowPortraits: {d2enum.KeyZ, -1}, - d2enum.HoldStandStill: {d2enum.KeyShift, -1}, - d2enum.ClearScreen: {d2enum.KeySpace, -1}, + d2enum.SelectPreviousSkill: {d2enum.KeyMouseWheelUp, -1}, + d2enum.SelectNextSkill: {d2enum.KeyMouseWheelDown, -1}, + + d2enum.ToggleBelts: {d2enum.KeyTilde, -1}, + d2enum.UseBeltSlot1: {d2enum.Key1, -1}, + d2enum.UseBeltSlot2: {d2enum.Key2, -1}, + d2enum.UseBeltSlot3: {d2enum.Key3, -1}, + d2enum.UseBeltSlot4: {d2enum.Key4, -1}, + d2enum.SwapWeapons: {d2enum.KeyW, -1}, + + d2enum.ToggleChatBox: {d2enum.KeyEnter, -1}, + d2enum.HoldRun: {d2enum.KeyControl, -1}, + d2enum.ToggleRunWalk: {d2enum.KeyR, -1}, + d2enum.HoldStandStill: {d2enum.KeyShift, -1}, + d2enum.HoldShowGroundItems: {d2enum.KeyAlt, -1}, + d2enum.HoldShowPortraits: {d2enum.KeyZ, -1}, + + d2enum.ToggleAutomap: {d2enum.KeyTab, -1}, + d2enum.CenterAutomap: {d2enum.KeyHome, -1}, + d2enum.TogglePartyOnAutomap: {d2enum.KeyF11, -1}, + d2enum.ToggleNamesOnAutomap: {d2enum.KeyF12, -1}, + d2enum.ToggleMiniMap: {d2enum.KeyV, -1}, + + d2enum.SayHelp: {d2enum.KeyKP0, -1}, + d2enum.SayFollowMe: {d2enum.KeyKP1, -1}, + d2enum.SayThisIsForYou: {d2enum.KeyKP2, -1}, + d2enum.SayThanks: {d2enum.KeyKP3, -1}, + d2enum.SaySorry: {d2enum.KeyKP4, -1}, + d2enum.SayBye: {d2enum.KeyKP5, -1}, + d2enum.SayNowYouDie: {d2enum.KeyKP6, -1}, + d2enum.SayRetreat: {d2enum.KeyKP7, -1}, + + d2enum.TakeScreenShot: {d2enum.KeyPrintScreen, -1}, + d2enum.ClearScreen: {d2enum.KeySpace, -1}, + d2enum.ClearMessages: {d2enum.KeyN, -1}, } + for gameEvent, keys := range defaultControls { - keyMap.SetPrimaryBinding(gameEvent, keys.Primary) - keyMap.SetSecondaryBinding(gameEvent, keys.Secondary) + km.SetPrimaryBinding(gameEvent, keys.Primary) + km.SetSecondaryBinding(gameEvent, keys.Secondary) } +} + +// KeyToString returns a string representing the key +func (km *KeyMap) KeyToString(k d2enum.Key) string { + return km.keyToStringMapping[k] +} + +// GetDefaultKeyMap generates a KeyMap instance with the +// default values +func GetDefaultKeyMap(asset *d2asset.AssetManager) *KeyMap { + keyMap := NewKeyMap(asset) + keyMap.ResetToDefault() return keyMap } diff --git a/d2game/d2player/skilltree.go b/d2game/d2player/skilltree.go index 73d01fdb..53e3b219 100644 --- a/d2game/d2player/skilltree.go +++ b/d2game/d2player/skilltree.go @@ -8,7 +8,6 @@ import ( "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset" - "github.com/OpenDiablo2/OpenDiablo2/d2core/d2gui" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2hero" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2ui" ) @@ -186,7 +185,7 @@ func (s *skillTree) loadForHeroType() { s.availSPLabel = s.uiManager.NewLabel(d2resource.Font16, d2resource.PaletteSky) s.availSPLabel.SetPosition(availSPLabelX, availSPLabelY) - s.availSPLabel.Alignment = d2gui.HorizontalAlignCenter + s.availSPLabel.Alignment = d2ui.HorizontalAlignCenter s.availSPLabel.SetText(s.makeTabString("StrSklTree1", "StrSklTree2", "StrSklTree3")) s.panelGroup.AddWidget(s.availSPLabel) } diff --git a/go.mod b/go.mod index 5ac964d7..85c8fc93 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/JoshVarga/blast v0.0.0-20180421040937-681c804fb9f0 github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d // indirect + github.com/davecgh/go-spew v1.1.0 github.com/go-restruct/restruct v1.2.0-alpha github.com/google/uuid v1.1.2 github.com/gravestench/akara v0.0.0-20201014060234-a64208a7fd3c