From acc4c7a13e791c31f3f17b42b24d6cdc03767209 Mon Sep 17 00:00:00 2001 From: lord Date: Thu, 6 Aug 2020 07:30:23 -0700 Subject: [PATCH] d2ui refactor (#699) * fixed lint errors in button.go * fixed lint errors in checkbox.go * Removed d2ui singleton, fixed nearly all lint errors - Changed `UI` struct to `UIManager`, removed singleton - UI element provider functions are now methods of the UI Manager - Screens now use the UI manager to create UI elements - game panels in d2player now use the UI Manager to create UI elements - Only the UI manager knows about "widgets"; calls to `d2ui.AddWidget` in Screen instances have been removed * changed ui element provider methods from `Create` to `New` --- d2app/app.go | 27 +-- d2core/d2map/d2mapentity/d2mapentity.go | 2 +- d2core/d2screen/d2screen.go | 9 +- d2core/d2screen/screen_manager.go | 3 +- d2core/d2ui/button.go | 203 +++++++++++++++++------ d2core/d2ui/checkbox.go | 38 +++-- d2core/d2ui/color_tokens.go | 58 +++++++ d2core/d2ui/d2ui.go | 121 ++------------ d2core/d2ui/drawable.go | 2 +- d2core/d2ui/label.go | 93 +++-------- d2core/d2ui/scrollbar.go | 76 +++++++-- d2core/d2ui/sprite.go | 38 +++-- d2core/d2ui/textbox.go | 32 ++-- d2core/d2ui/ui_manager.go | 140 ++++++++++++++++ d2core/d2ui/widget.go | 1 + d2game/d2gamescreen/character_select.go | 81 ++++----- d2game/d2gamescreen/credits.go | 35 ++-- d2game/d2gamescreen/game.go | 6 +- d2game/d2gamescreen/main_menu.go | 126 +++++++------- d2game/d2gamescreen/select_hero_class.go | 110 ++++++------ d2game/d2player/game_controls.go | 43 +++-- d2game/d2player/hero_stats_panel.go | 48 +++--- d2game/d2player/inventory.go | 23 +-- d2game/d2player/inventory_grid.go | 6 +- 24 files changed, 782 insertions(+), 539 deletions(-) create mode 100644 d2core/d2ui/color_tokens.go create mode 100644 d2core/d2ui/ui_manager.go diff --git a/d2app/app.go b/d2app/app.go index aa6b8d3d..057122f5 100644 --- a/d2app/app.go +++ b/d2app/app.go @@ -64,6 +64,7 @@ type App struct { audio d2interface.AudioProvider renderer d2interface.Renderer screen *d2screen.ScreenManager + ui *d2ui.UIManager tAllocSamples *ring.Ring } @@ -85,7 +86,11 @@ func Create(gitBranch, gitCommit string, terminal d2interface.Terminal, scriptEngine *d2script.ScriptEngine, audio d2interface.AudioProvider, - renderer d2interface.Renderer) *App { + renderer d2interface.Renderer, +) *App { + uiManager := d2ui.NewUIManager(renderer, inputManager, audio) + screenManager := d2screen.NewScreenManager(uiManager) + result := &App{ gitBranch: gitBranch, gitCommit: gitCommit, @@ -94,7 +99,8 @@ func Create(gitBranch, gitCommit string, scriptEngine: scriptEngine, audio: audio, renderer: renderer, - screen: d2screen.NewScreenManager(), + ui: uiManager, + screen: screenManager, tAllocSamples: createZeroedRing(nSamplesTAlloc), } @@ -187,7 +193,7 @@ func (a *App) initialize() error { d2inventory.LoadHeroObjects() - d2ui.Initialize(a.inputManager, a.audio) + a.ui.Initialize() return nil } @@ -368,7 +374,7 @@ func (a *App) render(target d2interface.Surface) error { return err } - d2ui.Render(target) + a.ui.Render(target) if err := d2gui.Render(target); err != nil { return err @@ -398,7 +404,7 @@ func (a *App) advance(elapsed, elapsedUnscaled, current float64) error { return err } - d2ui.Advance(elapsed) + a.ui.Advance(elapsed) if err := a.inputManager.Advance(elapsed, current); err != nil { return err @@ -668,14 +674,14 @@ func updateInitError(target d2interface.Surface) error { // ToMainMenu forces the game to transition to the Main Menu func (a *App) ToMainMenu() { buildInfo := d2gamescreen.BuildInfo{Branch: a.gitBranch, Commit: a.gitCommit} - mainMenu := d2gamescreen.CreateMainMenu(a, a.renderer, a.inputManager, a.audio, buildInfo) - mainMenu.SetScreenMode(d2gamescreen.ScreenModeMainMenu) + mainMenu := d2gamescreen.CreateMainMenu(a, a.renderer, a.inputManager, a.audio, a.ui, buildInfo) + // mainMenu.SetScreenMode(d2gamescreen.ScreenModeMainMenu) a.screen.SetNextScreen(mainMenu) } // ToSelectHero forces the game to transition to the Select Hero (create character) screen func (a *App) ToSelectHero(connType d2clientconnectiontype.ClientConnectionType, host string) { - selectHero := d2gamescreen.CreateSelectHeroClass(a, a.renderer, a.audio, connType, host) + selectHero := d2gamescreen.CreateSelectHeroClass(a, a.renderer, a.audio, a.ui, connType, host) a.screen.SetNextScreen(selectHero) } @@ -693,7 +699,8 @@ func (a *App) ToCreateGame(filePath string, connType d2clientconnectiontype.Clie // ToCharacterSelect forces the game to transition to the Character Select (load character) screen func (a *App) ToCharacterSelect(connType d2clientconnectiontype.ClientConnectionType, connHost string) { - characterSelect := d2gamescreen.CreateCharacterSelect(a, a.renderer, a.inputManager, a.audio, connType, connHost) + characterSelect := d2gamescreen.CreateCharacterSelect(a, a.renderer, a.inputManager, a.audio, + a.ui, connType, connHost) a.screen.SetNextScreen(characterSelect) } @@ -706,5 +713,5 @@ func (a *App) ToMapEngineTest(region, level int) { // ToCredits forces the game to transition to the credits screen func (a *App) ToCredits() { - a.screen.SetNextScreen(d2gamescreen.CreateCredits(a, a.renderer)) + a.screen.SetNextScreen(d2gamescreen.CreateCredits(a, a.renderer, a.ui)) } diff --git a/d2core/d2map/d2mapentity/d2mapentity.go b/d2core/d2map/d2mapentity/d2mapentity.go index c922a546..7addd79a 100644 --- a/d2core/d2map/d2mapentity/d2mapentity.go +++ b/d2core/d2map/d2mapentity/d2mapentity.go @@ -55,7 +55,7 @@ func NewPlayer(id, name string, x, y, direction int, heroType d2enum.Hero, Stats: stats, name: name, Class: heroType, - //nameLabel: d2ui.CreateLabel(d2resource.FontFormal11, d2resource.PaletteStatic), + //nameLabel: d2ui.NewLabel(d2resource.FontFormal11, d2resource.PaletteStatic), isRunToggled: true, isInTown: true, isRunning: true, diff --git a/d2core/d2screen/d2screen.go b/d2core/d2screen/d2screen.go index b3012fcc..cb7eb485 100644 --- a/d2core/d2screen/d2screen.go +++ b/d2core/d2screen/d2screen.go @@ -2,11 +2,16 @@ package d2screen import ( "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2ui" ) // NewScreenManager creates a screen manager -func NewScreenManager() *ScreenManager { - return &ScreenManager{} +func NewScreenManager(ui *d2ui.UIManager) *ScreenManager { + sm := &ScreenManager{ + uiManager: ui, + } + + return sm } // Screen is an exported interface diff --git a/d2core/d2screen/screen_manager.go b/d2core/d2screen/screen_manager.go index 763c153b..7f63c906 100644 --- a/d2core/d2screen/screen_manager.go +++ b/d2core/d2screen/screen_manager.go @@ -10,6 +10,7 @@ import ( // ScreenManager manages game screens (main menu, credits, character select, game, etc) type ScreenManager struct { + uiManager *d2ui.UIManager nextScreen Screen loadingScreen Screen loadingState LoadingState @@ -52,7 +53,7 @@ func (sm *ScreenManager) Advance(elapsed float64) error { } } - d2ui.Reset() + sm.uiManager.Reset() d2gui.SetLayout(nil) if handler, ok := sm.nextScreen.(ScreenLoadHandler); ok { diff --git a/d2core/d2ui/button.go b/d2core/d2ui/button.go index d957dd8a..3c283a8c 100644 --- a/d2core/d2ui/button.go +++ b/d2core/d2ui/button.go @@ -3,8 +3,8 @@ package d2ui import ( "fmt" "image" - "image/color" + "github.com/OpenDiablo2/OpenDiablo2/d2common" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2gui" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" @@ -43,6 +43,11 @@ const ( ButtonTypeMinipanelMen ButtonType = 19 ) +const ( + greyAlpha100 = 0x646464_ff + lightGreyAlpha75 = 0x808080_c3 +) + // ButtonLayout defines the type of buttons type ButtonLayout struct { ResourceName string @@ -58,31 +63,101 @@ type ButtonLayout struct { AllowFrameChange bool } +const ( + buttonWideSegmentsX = 2 + buttonWideSegmentsY = 1 + buttonWideDisabledFrame = -1 + buttonWideTextOffset = 1 + + buttonShortSegmentsX = 1 + buttonShortSegmentsY = 1 + buttonShortDisabledFrame = -1 + buttonShortTextOffset = -1 + + buttonMediumSegmentsX = 1 + buttonMediumSegmentsY = 1 + + buttonTallSegmentsX = 1 + buttonTallSegmentsY = 1 + buttonTallTextOffset = 5 + + buttonOkCancelSegmentsX = 1 + buttonOkCancelSegmentsY = 1 + buttonOkCancelDisabledFrame = -1 + + buttonRunSegmentsX = 1 + buttonRunSegmentsY = 1 + buttonRunDisabledFrame = -1 + + pressedButtonOffset = 2 +) + func getButtonLayouts() map[ButtonType]ButtonLayout { return map[ButtonType]ButtonLayout{ ButtonTypeWide: { - XSegments: 2, YSegments: 1, ResourceName: d2resource.WideButtonBlank, PaletteName: d2resource.PaletteUnits, - DisabledFrame: -1, FontPath: d2resource.FontExocet10, AllowFrameChange: true, TextOffset: 1}, + XSegments: buttonWideSegmentsX, + YSegments: buttonWideSegmentsY, + DisabledFrame: buttonWideDisabledFrame, + TextOffset: buttonWideTextOffset, + ResourceName: d2resource.WideButtonBlank, + PaletteName: d2resource.PaletteUnits, + FontPath: d2resource.FontExocet10, + AllowFrameChange: true, + }, ButtonTypeShort: { - XSegments: 1, YSegments: 1, ResourceName: d2resource.ShortButtonBlank, PaletteName: d2resource.PaletteUnits, - DisabledFrame: -1, FontPath: d2resource.FontRediculous, AllowFrameChange: true, TextOffset: -1}, + XSegments: buttonShortSegmentsX, + YSegments: buttonShortSegmentsY, + DisabledFrame: buttonShortDisabledFrame, + TextOffset: buttonShortTextOffset, + ResourceName: d2resource.ShortButtonBlank, + PaletteName: d2resource.PaletteUnits, + FontPath: d2resource.FontRediculous, + AllowFrameChange: true, + }, ButtonTypeMedium: { - XSegments: 1, YSegments: 1, ResourceName: d2resource.MediumButtonBlank, PaletteName: d2resource.PaletteUnits, - FontPath: d2resource.FontExocet10, AllowFrameChange: true}, + XSegments: buttonMediumSegmentsX, + YSegments: buttonMediumSegmentsY, + ResourceName: d2resource.MediumButtonBlank, + PaletteName: d2resource.PaletteUnits, + FontPath: d2resource.FontExocet10, + AllowFrameChange: true, + }, ButtonTypeTall: { - XSegments: 1, YSegments: 1, ResourceName: d2resource.TallButtonBlank, PaletteName: d2resource.PaletteUnits, - FontPath: d2resource.FontExocet10, AllowFrameChange: true, TextOffset: 5}, + XSegments: buttonTallSegmentsX, + YSegments: buttonTallSegmentsY, + TextOffset: buttonTallTextOffset, + ResourceName: d2resource.TallButtonBlank, + PaletteName: d2resource.PaletteUnits, + FontPath: d2resource.FontExocet10, + AllowFrameChange: true, + }, ButtonTypeOkCancel: { - XSegments: 1, YSegments: 1, ResourceName: d2resource.CancelButton, PaletteName: d2resource.PaletteUnits, - DisabledFrame: -1, FontPath: d2resource.FontRediculous, AllowFrameChange: true}, + XSegments: buttonOkCancelSegmentsX, + YSegments: buttonOkCancelSegmentsY, + DisabledFrame: buttonOkCancelDisabledFrame, + ResourceName: d2resource.CancelButton, + PaletteName: d2resource.PaletteUnits, + FontPath: d2resource.FontRediculous, + AllowFrameChange: true, + }, ButtonTypeRun: { - XSegments: 1, YSegments: 1, ResourceName: d2resource.RunButton, PaletteName: d2resource.PaletteSky, - Toggleable: true, DisabledFrame: -1, FontPath: d2resource.FontRediculous, AllowFrameChange: true}, + XSegments: buttonRunSegmentsX, + YSegments: buttonRunSegmentsY, + DisabledFrame: buttonRunDisabledFrame, + ResourceName: d2resource.RunButton, + PaletteName: d2resource.PaletteSky, + Toggleable: true, + FontPath: d2resource.FontRediculous, + AllowFrameChange: true, + }, } } +var _ Widget = &Button{} // static check to ensure button implements widget + // Button defines a standard wide UI button type Button struct { + manager *UIManager buttonLayout ButtonLayout normalSurface d2interface.Surface pressedSurface d2interface.Surface @@ -100,80 +175,93 @@ type Button struct { toggled bool } -// CreateButton creates an instance of Button -func CreateButton(renderer d2interface.Renderer, buttonType ButtonType, text string) Button { - result := Button{ +// NewButton creates an instance of Button +func (ui *UIManager) NewButton(buttonType ButtonType, text string) *Button { + btn := &Button{ width: 0, height: 0, visible: true, enabled: true, pressed: false, } - buttonLayout := getButtonLayouts()[buttonType] - result.buttonLayout = buttonLayout - lbl := CreateLabel(buttonLayout.FontPath, d2resource.PaletteUnits) + buttonLayout := getButtonLayouts()[buttonType] + btn.buttonLayout = buttonLayout + lbl := ui.NewLabel(buttonLayout.FontPath, d2resource.PaletteUnits) lbl.SetText(text) - lbl.Color[0] = color.RGBA{R: 100, G: 100, B: 100, A: 255} + lbl.Color[0] = d2common.Color(greyAlpha100) lbl.Alignment = d2gui.HorizontalAlignCenter animation, _ := d2asset.LoadAnimation(buttonLayout.ResourceName, buttonLayout.PaletteName) - buttonSprite, _ := LoadSprite(animation) + buttonSprite, _ := ui.NewSprite(animation) for i := 0; i < buttonLayout.XSegments; i++ { w, _, _ := buttonSprite.GetFrameSize(i) - result.width += w + btn.width += w } for i := 0; i < buttonLayout.YSegments; i++ { _, h, _ := buttonSprite.GetFrameSize(i * buttonLayout.YSegments) - result.height += h + btn.height += h } - result.normalSurface, _ = renderer.NewSurface(result.width, result.height, d2enum.FilterNearest) + btn.normalSurface, _ = ui.renderer.NewSurface(btn.width, btn.height, d2enum.FilterNearest) buttonSprite.SetPosition(0, 0) buttonSprite.SetEffect(d2enum.DrawEffectModulate) - result.renderFrames(renderer, buttonSprite, &buttonLayout, &lbl) + ui.addWidget(btn) // important that this comes before renderFrames! - return result + btn.renderFrames(buttonSprite, &buttonLayout, lbl) + + return btn } -func (v *Button) renderFrames(renderer d2interface.Renderer, buttonSprite *Sprite, buttonLayout *ButtonLayout, label *Label) { - totalButtonTypes := buttonSprite.GetFrameCount() / (buttonLayout.XSegments * buttonLayout.YSegments) +func (v *Button) renderFrames(btnSprite *Sprite, btnLayout *ButtonLayout, label *Label) { + totalButtonTypes := btnSprite.GetFrameCount() / (btnLayout.XSegments * btnLayout.YSegments) var err error - err = buttonSprite.RenderSegmented(v.normalSurface, buttonLayout.XSegments, buttonLayout.YSegments, buttonLayout.BaseFrame) + err = btnSprite.RenderSegmented(v.normalSurface, btnLayout.XSegments, btnLayout.YSegments, btnLayout.BaseFrame) if err != nil { fmt.Printf("failed to render button normalSurface, err: %v\n", err) } _, labelHeight := label.GetSize() - textY := v.height/2 - labelHeight/2 - xOffset := v.width / 2 + textY := half(v.height - labelHeight) + xOffset := half(v.width) label.SetPosition(xOffset, textY) label.Render(v.normalSurface) - if buttonLayout.AllowFrameChange { - if totalButtonTypes > 1 { - v.pressedSurface, _ = renderer.NewSurface(v.width, v.height, d2enum.FilterNearest) - err = buttonSprite.RenderSegmented(v.pressedSurface, buttonLayout.XSegments, buttonLayout.YSegments, buttonLayout.BaseFrame+1) + if btnLayout.AllowFrameChange { + frameOffset := 0 + xSeg, ySeg, baseFrame := btnLayout.XSegments, btnLayout.YSegments, btnLayout.BaseFrame + + totalButtonTypes-- + if totalButtonTypes > 0 { // button has more than one type + frameOffset++ + + v.pressedSurface, _ = v.manager.renderer.NewSurface(v.width, v.height, + d2enum.FilterNearest) + err = btnSprite.RenderSegmented(v.pressedSurface, xSeg, ySeg, baseFrame+frameOffset) if err != nil { fmt.Printf("failed to render button pressedSurface, err: %v\n", err) } - label.SetPosition(xOffset-2, textY+2) + label.SetPosition(xOffset-pressedButtonOffset, textY+pressedButtonOffset) label.Render(v.pressedSurface) } - if totalButtonTypes > 2 { - v.toggledSurface, _ = renderer.NewSurface(v.width, v.height, d2enum.FilterNearest) - err = buttonSprite.RenderSegmented(v.toggledSurface, buttonLayout.XSegments, buttonLayout.YSegments, buttonLayout.BaseFrame+2) + totalButtonTypes-- + if totalButtonTypes > 0 { // button has more than two types + frameOffset++ + + v.toggledSurface, _ = v.manager.renderer.NewSurface(v.width, v.height, + d2enum.FilterNearest) + err = btnSprite.RenderSegmented(v.pressedSurface, xSeg, ySeg, baseFrame+frameOffset) if err != nil { fmt.Printf("failed to render button toggledSurface, err: %v\n", err) @@ -183,9 +271,13 @@ func (v *Button) renderFrames(renderer d2interface.Renderer, buttonSprite *Sprit label.Render(v.toggledSurface) } - if totalButtonTypes > 3 { - v.pressedToggledSurface, _ = renderer.NewSurface(v.width, v.height, d2enum.FilterNearest) - err = buttonSprite.RenderSegmented(v.pressedToggledSurface, buttonLayout.XSegments, buttonLayout.YSegments, buttonLayout.BaseFrame+3) + totalButtonTypes-- + if totalButtonTypes > 0 { // button has more than three types + frameOffset++ + + v.pressedToggledSurface, _ = v.manager.renderer.NewSurface(v.width, v.height, + d2enum.FilterNearest) + err = btnSprite.RenderSegmented(v.pressedSurface, xSeg, ySeg, baseFrame+frameOffset) if err != nil { fmt.Printf("failed to render button pressedToggledSurface, err: %v\n", err) @@ -195,9 +287,10 @@ func (v *Button) renderFrames(renderer d2interface.Renderer, buttonSprite *Sprit label.Render(v.pressedToggledSurface) } - if buttonLayout.DisabledFrame != -1 { - v.disabledSurface, _ = renderer.NewSurface(v.width, v.height, d2enum.FilterNearest) - err = buttonSprite.RenderSegmented(v.disabledSurface, buttonLayout.XSegments, buttonLayout.YSegments, buttonLayout.DisabledFrame) + if btnLayout.DisabledFrame != -1 { + v.disabledSurface, _ = v.manager.renderer.NewSurface(v.width, v.height, + d2enum.FilterNearest) + err = btnSprite.RenderSegmented(v.disabledSurface, xSeg, ySeg, btnLayout.DisabledFrame) if err != nil { fmt.Printf("failed to render button disabledSurface, err: %v\n", err) @@ -209,6 +302,11 @@ func (v *Button) renderFrames(renderer d2interface.Renderer, buttonSprite *Sprit } } +// bindManager binds the button to the UI manager +func (v *Button) bindManager(manager *UIManager) { + v.manager = manager +} + // OnActivated defines the callback handler for the activate event func (v *Button) OnActivated(callback func()) { v.onClick = callback @@ -226,15 +324,16 @@ func (v *Button) Activate() { // Render renders the button func (v *Button) Render(target d2interface.Surface) error { target.PushFilter(d2enum.FilterNearest) - target.PushTranslation(v.x, v.y) + defer target.Pop() - defer target.PopN(2) + target.PushTranslation(v.x, v.y) + defer target.Pop() var err error switch { case !v.enabled: - target.PushColor(color.RGBA{R: 128, G: 128, B: 128, A: 195}) + target.PushColor(d2common.Color(lightGreyAlpha75)) defer target.Pop() err = target.Render(v.disabledSurface) case v.toggled && v.pressed: @@ -260,8 +359,8 @@ func (v *Button) Toggle() { } // Advance advances the button state -func (v *Button) Advance(elapsed float64) { - +func (v *Button) Advance(_ float64) error { + return nil } // GetEnabled returns the enabled state @@ -309,3 +408,7 @@ func (v *Button) GetPressed() bool { func (v *Button) SetPressed(pressed bool) { v.pressed = pressed } + +func half(n int) int { + return n / 2 +} diff --git a/d2core/d2ui/checkbox.go b/d2core/d2ui/checkbox.go index 6be58a9e..0c2d460a 100644 --- a/d2core/d2ui/checkbox.go +++ b/d2core/d2ui/checkbox.go @@ -9,6 +9,7 @@ import ( // Checkbox represents a checkbox UI element type Checkbox struct { + manager *UIManager Image d2interface.Surface checkedImage d2interface.Surface x int @@ -21,9 +22,9 @@ type Checkbox struct { enabled bool } -// CreateCheckbox creates a new instance of a checkbox -func CreateCheckbox(renderer d2interface.Renderer, checkState bool) Checkbox { - result := Checkbox{ +// NewCheckbox creates a new instance of a checkbox +func (ui *UIManager) NewCheckbox(checkState bool) *Checkbox { + result := &Checkbox{ checkState: checkState, visible: true, width: 0, @@ -32,27 +33,35 @@ func CreateCheckbox(renderer d2interface.Renderer, checkState bool) Checkbox { } animation, _ := d2asset.LoadAnimation(d2resource.Checkbox, d2resource.PaletteFechar) - checkboxSprite, _ := LoadSprite(animation) + checkboxSprite, _ := ui.NewSprite(animation) result.width, result.height, _ = checkboxSprite.GetFrameSize(0) checkboxSprite.SetPosition(0, 0) - result.Image, _ = renderer.NewSurface(result.width, result.height, d2enum.FilterNearest) + result.Image, _ = ui.renderer.NewSurface(result.width, result.height, d2enum.FilterNearest) _ = checkboxSprite.RenderSegmented(result.Image, 1, 1, 0) - result.checkedImage, _ = renderer.NewSurface(result.width, result.height, d2enum.FilterNearest) + result.checkedImage, _ = ui.renderer.NewSurface(result.width, result.height, d2enum.FilterNearest) _ = checkboxSprite.RenderSegmented(result.checkedImage, 1, 1, 1) + ui.addWidget(result) + return result } +// bindManager binds the checkbox to the UI manager +func (v *Checkbox) bindManager(manager *UIManager) { + v.manager = manager +} + // Render renders the checkbox func (v *Checkbox) Render(target d2interface.Surface) error { target.PushTranslation(v.x, v.y) - target.PushFilter(d2enum.FilterNearest) + defer target.Pop() - defer target.PopN(2) + target.PushFilter(d2enum.FilterNearest) + defer target.Pop() if v.checkState { _ = target.Render(v.checkedImage) @@ -64,8 +73,8 @@ func (v *Checkbox) Render(target d2interface.Surface) error { } // Advance does nothing for checkboxes -func (v *Checkbox) Advance(elapsed float64) { - +func (v *Checkbox) Advance(_ float64) error { + return nil } // GetEnabled returns the enabled state of the checkbox @@ -97,7 +106,7 @@ func (v *Checkbox) GetPressed() bool { return v.checkState } -// OnACtivated sets the callback function of the click event for the checkbox +// OnActivated sets the callback function of the click event for the checkbox func (v *Checkbox) OnActivated(callback func()) { v.onClick = callback } @@ -108,16 +117,17 @@ func (v *Checkbox) Activate() { if v.onClick == nil { return } + v.onClick() } // GetPosition returns the position of the checkbox -func (v *Checkbox) GetPosition() (int, int) { +func (v *Checkbox) GetPosition() (x, y int) { return v.x, v.y } // GetSize returns the size of the checkbox -func (v *Checkbox) GetSize() (int, int) { +func (v *Checkbox) GetSize() (width, height int) { return v.width, v.height } @@ -127,7 +137,7 @@ func (v *Checkbox) GetVisible() bool { } // SetPosition sets the position of the checkbox -func (v *Checkbox) SetPosition(x int, y int) { +func (v *Checkbox) SetPosition(x, y int) { v.x = x v.y = y } diff --git a/d2core/d2ui/color_tokens.go b/d2core/d2ui/color_tokens.go new file mode 100644 index 00000000..dd91180c --- /dev/null +++ b/d2core/d2ui/color_tokens.go @@ -0,0 +1,58 @@ +package d2ui + +import "fmt" + +// ColorToken is a string which is used inside of label strings to set font color. +type ColorToken string + +const ( + colorTokenFmt = `%s%s` + colorTokenMatch = `\[[^\]]+\]` // nolint:gosec // has nothing to to with credentials + colorStrMatch = colorTokenMatch + `[^\[]+` +) + +// Color tokens for colored labels +const ( + ColorTokenGrey ColorToken = "[grey]" + ColorTokenRed ColorToken = "[red]" + ColorTokenWhite ColorToken = "[white]" + ColorTokenBlue ColorToken = "[blue]" + ColorTokenYellow ColorToken = "[yellow]" + ColorTokenGreen ColorToken = "[green]" + ColorTokenGold ColorToken = "[gold]" + ColorTokenOrange ColorToken = "[orange]" + ColorTokenBlack ColorToken = "[black]" +) + +// Color tokens for specific use-cases +const ( + ColorTokenSocketedItem = ColorTokenGrey + ColorTokenNormalItem = ColorTokenWhite + ColorTokenMagicItem = ColorTokenBlue + ColorTokenRareItem = ColorTokenYellow + ColorTokenSetItem = ColorTokenGreen + ColorTokenUniqueItem = ColorTokenGold + ColorTokenCraftedItem = ColorTokenOrange + ColorTokenServer = ColorTokenRed + ColorTokenButton = ColorTokenBlack + ColorTokenCharacterName = ColorTokenGold + ColorTokenCharacterDesc = ColorTokenWhite + ColorTokenCharacterType = ColorTokenGreen +) + +const ( + colorGrey100Alpha = 0x69_69_69_ff + colorWhite100Alpha = 0xff_ff_ff_ff + colorBlue100Alpha = 0x69_69_ff_ff + colorYellow100Alpha = 0xff_ff_64_ff + colorGreen100Alpha = 0x00_ff_00_ff + colorGold100Alpha = 0xc7_b3_77_ff + colorOrange100Alpha = 0xff_a8_00_ff + colorRed100Alpha = 0xff_77_77_ff + colorBlack100Alpha = 0x00_00_00_ff +) + +// ColorTokenize formats the string with the given color token +func ColorTokenize(s string, t ColorToken) string { + return fmt.Sprintf(colorTokenFmt, t, s) +} diff --git a/d2core/d2ui/d2ui.go b/d2core/d2ui/d2ui.go index 066c5e7c..a42b75d4 100644 --- a/d2core/d2ui/d2ui.go +++ b/d2core/d2ui/d2ui.go @@ -1,13 +1,7 @@ package d2ui import ( - "log" - - "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" - "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" - - "github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource" ) // CursorButton represents a mouse button @@ -20,110 +14,17 @@ const ( CursorButtonRight CursorButton = 2 ) -type UI struct { - inputManager d2interface.InputManager - widgets []Widget - cursorButtons CursorButton // TODO (carrelld) convert dependent code and remove - CursorX int // TODO (carrelld) convert dependent code and remove - CursorY int // TODO (carrelld) convert dependent code and remove - pressedWidget Widget - clickSfx d2interface.SoundEffect -} - -var singleton UI - -func Initialize(inputManager d2interface.InputManager, audioProvider d2interface.AudioProvider) { - sfx, err := audioProvider.LoadSound(d2resource.SFXButtonClick, false, false) - if err != nil { - log.Fatalf("failed to initialize ui: %v", err) +// NewUIManager creates a UIManager instance with the given input and audio provider +func NewUIManager( + renderer d2interface.Renderer, + input d2interface.InputManager, + audio d2interface.AudioProvider, +) *UIManager { + ui := &UIManager{ + renderer: renderer, + inputManager: input, + audio: audio, } - singleton.clickSfx = sfx - singleton.inputManager = inputManager - if err := singleton.inputManager.BindHandler(&singleton); err != nil { - log.Fatalf("failed to initialize ui: %v", err) - } -} - -// Reset resets the state of the UI manager. Typically called for new screens -func Reset() { - singleton.widgets = nil - singleton.pressedWidget = nil -} - -// AddWidget adds a widget to the UI manager -func AddWidget(widget Widget) { - singleton.inputManager.BindHandler(widget) - singleton.widgets = append(singleton.widgets, widget) -} - -func (u *UI) OnMouseButtonUp(event d2interface.MouseEvent) bool { - singleton.CursorX, singleton.CursorY = event.X(), event.Y() - if event.Button() == d2enum.MouseButtonLeft { - singleton.cursorButtons |= CursorButtonLeft - // activate previously pressed widget if cursor is still hovering - w := singleton.pressedWidget - if w != nil && contains(w, singleton.CursorX, singleton.CursorY) && w.GetVisible() && w.GetEnabled() { - w.Activate() - } - // unpress all widgets that are pressed - for _, w := range singleton.widgets { - w.SetPressed(false) - } - } - return false -} - -func (u *UI) OnMouseButtonDown(event d2interface.MouseEvent) bool { - singleton.CursorX, singleton.CursorY = event.X(), event.Y() - if event.Button() == d2enum.MouseButtonLeft { - // find and press a widget on screen - singleton.pressedWidget = nil - for _, w := range singleton.widgets { - if contains(w, singleton.CursorX, singleton.CursorY) && w.GetVisible() && w.GetEnabled() { - w.SetPressed(true) - singleton.pressedWidget = w - u.clickSfx.Play() - break - } - } - } - if event.Button() == d2enum.MouseButtonRight { - singleton.cursorButtons |= CursorButtonRight - } - return false -} - -// Render renders all of the UI elements -func Render(target d2interface.Surface) { - for _, widget := range singleton.widgets { - if widget.GetVisible() { - widget.Render(target) - } - } -} - -// contains determines whether a given x,y coordinate lands within a Widget -func contains(w Widget, x, y int) bool { - wx, wy := w.GetPosition() - ww, wh := w.GetSize() - return x >= wx && x <= wx+ww && y >= wy && y <= wy+wh -} - -// Update updates all of the UI elements -func Advance(elapsed float64) { - for _, widget := range singleton.widgets { - if widget.GetVisible() { - widget.Advance(elapsed) - } - } -} - -// CursorButtonPressed determines if the specified button has been pressed -func CursorButtonPressed(button CursorButton) bool { - return singleton.cursorButtons&button > 0 -} - -func CursorPosition() (x, y int) { - return singleton.CursorX, singleton.CursorY + return ui } diff --git a/d2core/d2ui/drawable.go b/d2core/d2ui/drawable.go index 31b5f78b..09e0852d 100644 --- a/d2core/d2ui/drawable.go +++ b/d2core/d2ui/drawable.go @@ -7,7 +7,7 @@ import ( // Drawable represents an instance that can be drawn type Drawable interface { Render(target d2interface.Surface) error - Advance(elapsed float64) + Advance(elapsed float64) error GetSize() (width, height int) SetPosition(x, y int) GetPosition() (x, y int) diff --git a/d2core/d2ui/label.go b/d2core/d2ui/label.go index 9f5409d0..32d57051 100644 --- a/d2core/d2ui/label.go +++ b/d2core/d2ui/label.go @@ -1,66 +1,20 @@ package d2ui import ( - "fmt" - "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" - "github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset" - "github.com/OpenDiablo2/OpenDiablo2/d2core/d2gui" "image/color" "log" "regexp" "strings" + + "github.com/OpenDiablo2/OpenDiablo2/d2common" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset" + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2gui" ) -// ColorToken is a string which is used inside of label strings to set font color. -type ColorToken string - -const ( - colorTokenFmt = `%s%s` - colorTokenMatch = `\[[^\]]+\]` - colorStrMatch = colorTokenMatch + `[^\[]+` -) - -const ( - ColorTokenGrey ColorToken = "[grey]" - ColorTokenRed ColorToken = "[red]" - ColorTokenWhite ColorToken = "[white]" - ColorTokenBlue ColorToken = "[blue]" - ColorTokenYellow ColorToken = "[yellow]" - ColorTokenGreen ColorToken = "[green]" - ColorTokenGold ColorToken = "[gold]" - ColorTokenOrange ColorToken = "[orange]" - ColorTokenBlack ColorToken = "[black]" -) - -// Color tokens for item labels -const ( - ColorTokenSocketedItem = ColorTokenGrey - ColorTokenNormalItem = ColorTokenWhite - ColorTokenMagicItem = ColorTokenBlue - ColorTokenRareItem = ColorTokenYellow - ColorTokenSetItem = ColorTokenGreen - ColorTokenUniqueItem = ColorTokenGold - ColorTokenCraftedItem = ColorTokenOrange -) - -const ( - ColorTokenServer = ColorTokenRed - ColorTokenButton = ColorTokenBlack -) - -const ( - ColorTokenCharacterName = ColorTokenGold - ColorTokenCharacterDesc = ColorTokenWhite - ColorTokenCharacterType = ColorTokenGreen -) - -// ColorTokenize formats the string with the given color token -func ColorTokenize(s string, t ColorToken) string { - return fmt.Sprintf(colorTokenFmt, t, s) -} - // Label represents a user interface label type Label struct { + manager *UIManager text string X int Y int @@ -70,15 +24,17 @@ type Label struct { backgroundColor color.Color } -// CreateLabel creates a new instance of a UI label -func CreateLabel(fontPath, palettePath string) Label { +// NewLabel creates a new instance of a UI label +func (ui *UIManager) NewLabel(fontPath, palettePath string) *Label { font, _ := d2asset.LoadFont(fontPath+".tbl", fontPath+".dc6", palettePath) - result := Label{ + result := &Label{ Alignment: d2gui.HorizontalAlignLeft, Color: map[int]color.Color{0: color.White}, font: font, } + result.bindManager(ui) + return result } @@ -126,6 +82,11 @@ func (v *Label) Render(target d2interface.Surface) { target.Pop() } +// bindManager binds the label to the UI manager +func (v *Label) bindManager(manager *UIManager) { + v.manager = manager +} + // SetPosition moves the label to the specified location func (v *Label) SetPosition(x, y int) { v.X = x @@ -148,7 +109,7 @@ func (v *Label) SetText(newText string) { } // SetBackgroundColor sets the background highlight color -func (v *Label) SetBackgroundColor(c color.RGBA) { +func (v *Label) SetBackgroundColor(c color.Color) { v.backgroundColor = c } @@ -203,19 +164,17 @@ func (v *Label) getAlignOffset(textWidth int) int { } func getColor(token ColorToken) color.Color { - alpha := uint8(255) - // todo this should really come from the PL2 files colors := map[ColorToken]color.Color{ - ColorTokenGrey: color.RGBA{105, 105, 105, alpha}, - ColorTokenWhite: color.RGBA{255, 255, 255, alpha}, - ColorTokenBlue: color.RGBA{105, 105, 255, alpha}, - ColorTokenYellow: color.RGBA{255, 255, 100, alpha}, - ColorTokenGreen: color.RGBA{0, 255, 0, alpha}, - ColorTokenGold: color.RGBA{199, 179, 119, alpha}, - ColorTokenOrange: color.RGBA{255, 168, 0, alpha}, - ColorTokenRed: color.RGBA{255, 77, 77, alpha}, - ColorTokenBlack: color.RGBA{0, 0, 0, alpha}, + ColorTokenGrey: d2common.Color(colorGrey100Alpha), + ColorTokenWhite: d2common.Color(colorWhite100Alpha), + ColorTokenBlue: d2common.Color(colorBlue100Alpha), + ColorTokenYellow: d2common.Color(colorYellow100Alpha), + ColorTokenGreen: d2common.Color(colorGreen100Alpha), + ColorTokenGold: d2common.Color(colorGold100Alpha), + ColorTokenOrange: d2common.Color(colorOrange100Alpha), + ColorTokenRed: d2common.Color(colorRed100Alpha), + ColorTokenBlack: d2common.Color(colorBlack100Alpha), } chosen := colors[token] diff --git a/d2core/d2ui/scrollbar.go b/d2core/d2ui/scrollbar.go index 3cfe8cd4..7a99e5ad 100644 --- a/d2core/d2ui/scrollbar.go +++ b/d2core/d2ui/scrollbar.go @@ -6,7 +6,17 @@ import ( "github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset" ) +const ( + scrollbarOffsetY = 30 + halfScrollbarOffsetY = scrollbarOffsetY / 2 + scrollbarSpriteOffsetY = 10 + scrollbarFrameOffset = 4 + scrollbarWidth = 10 +) + +// Scrollbar is a vertical slider ui element type Scrollbar struct { + manager *UIManager x, y, height int visible bool enabled bool @@ -17,10 +27,11 @@ type Scrollbar struct { scrollbarSprite *Sprite } -func CreateScrollbar(x, y, height int) Scrollbar { +// NewScrollbar creates a scrollbar instance +func (ui *UIManager) NewScrollbar(x, y, height int) *Scrollbar { animation, _ := d2asset.LoadAnimation(d2resource.Scrollbar, d2resource.PaletteSky) - scrollbarSprite, _ := LoadSprite(animation) - result := Scrollbar{ + scrollbarSprite, _ := ui.NewSprite(animation) + result := &Scrollbar{ visible: true, enabled: true, x: x, @@ -28,32 +39,44 @@ func CreateScrollbar(x, y, height int) Scrollbar { height: height, scrollbarSprite: scrollbarSprite, } + + ui.addWidget(result) + return result } -func (v Scrollbar) GetEnabled() bool { +// GetEnabled returns whether or not the scrollbar is enabled +func (v *Scrollbar) GetEnabled() bool { return v.enabled } +// SetEnabled sets the enabled state func (v *Scrollbar) SetEnabled(enabled bool) { v.enabled = enabled } -func (v *Scrollbar) SetPressed(pressed bool) {} -func (v *Scrollbar) GetPressed() bool { return false } +// SetPressed is not used by the scrollbar, but is present to satisfy the ui widget interface +func (v *Scrollbar) SetPressed(_ bool) {} +// GetPressed is not used by the scrollbar, but is present to satisfy the ui widget interface +func (v *Scrollbar) GetPressed() bool { return false } + +// OnActivated sets the onActivate callback function for the scrollbar func (v *Scrollbar) OnActivated(callback func()) { v.onActivate = callback } -func (v Scrollbar) getBarPosition() int { - return int((float32(v.currentOffset) / float32(v.maxOffset)) * float32(v.height-30)) +func (v *Scrollbar) getBarPosition() int { + maxOffset := float32(v.maxOffset) * float32(v.height-scrollbarOffsetY) + return int(float32(v.currentOffset) / maxOffset) } +// Activate will call the onActivate callback (if set) func (v *Scrollbar) Activate() { - _, my := CursorPosition() + _, my := v.manager.CursorPosition() barPosition := v.getBarPosition() - if my <= v.y+barPosition+15 { + + if my <= v.y+barPosition+halfScrollbarOffsetY { if v.currentOffset > 0 { v.currentOffset-- v.lastDirChange = -1 @@ -70,10 +93,12 @@ func (v *Scrollbar) Activate() { } } +// GetLastDirChange get the last direction change func (v *Scrollbar) GetLastDirChange() int { return v.lastDirChange } +// Render renders the scrollbar to the given surface func (v *Scrollbar) Render(target d2interface.Surface) error { if !v.visible || v.maxOffset == 0 { return nil @@ -91,7 +116,7 @@ func (v *Scrollbar) Render(target d2interface.Surface) error { return err } - v.scrollbarSprite.SetPosition(v.x, v.y+v.height-10) + v.scrollbarSprite.SetPosition(v.x, v.y+v.height-scrollbarSpriteOffsetY) // what is the magic? if err := v.scrollbarSprite.RenderSegmented(target, 1, 1, 1+offset); err != nil { return err @@ -103,65 +128,82 @@ func (v *Scrollbar) Render(target d2interface.Surface) error { v.scrollbarSprite.SetPosition(v.x, v.y+10+v.getBarPosition()) - offset = 0 + offset = scrollbarFrameOffset if !v.enabled { - offset = 1 + offset++ } - if err := v.scrollbarSprite.RenderSegmented(target, 1, 1, 4+offset); err != nil { + if err := v.scrollbarSprite.RenderSegmented(target, 1, 1, offset); err != nil { return err } return nil } -func (v *Scrollbar) Advance(elapsed float64) { - v.scrollbarSprite.Advance(elapsed) +// bindManager binds the scrollbar to the UI manager +func (v *Scrollbar) bindManager(manager *UIManager) { + v.manager = manager } +// Advance advances the scrollbar sprite +func (v *Scrollbar) Advance(elapsed float64) error { + return v.scrollbarSprite.Advance(elapsed) +} + +// GetSize returns the scrollbar width and height func (v *Scrollbar) GetSize() (width, height int) { - return 10, v.height + return scrollbarWidth, v.height } +// SetPosition sets the scrollbar x,y position func (v *Scrollbar) SetPosition(x, y int) { v.x = x v.y = y } +// GetPosition returns the scrollbar x,y position func (v *Scrollbar) GetPosition() (x, y int) { return v.x, v.y } +// GetVisible returns whether or not the scrollbar is visible func (v *Scrollbar) GetVisible() bool { return v.visible } +// SetVisible sets the scrollbar visibility state func (v *Scrollbar) SetVisible(visible bool) { v.visible = visible } +// SetMaxOffset sets the maximum offset of the scrollbar func (v *Scrollbar) SetMaxOffset(maxOffset int) { v.maxOffset = maxOffset if v.maxOffset < 0 { v.maxOffset = 0 } + if v.currentOffset > v.maxOffset { v.currentOffset = v.maxOffset } + if v.maxOffset == 0 { v.currentOffset = 0 } } +// SetCurrentOffset sets the scrollbar's current offset func (v *Scrollbar) SetCurrentOffset(currentOffset int) { v.currentOffset = currentOffset } +// GetMaxOffset returns the max offset func (v *Scrollbar) GetMaxOffset() int { return v.maxOffset } +// GetCurrentOffset gets the current max offset of the scrollbar func (v *Scrollbar) GetCurrentOffset() int { return v.currentOffset } diff --git a/d2core/d2ui/sprite.go b/d2core/d2ui/sprite.go index 4a597558..b6aab812 100644 --- a/d2core/d2ui/sprite.go +++ b/d2core/d2ui/sprite.go @@ -1,13 +1,13 @@ package d2ui import ( - "errors" - "github.com/OpenDiablo2/OpenDiablo2/d2common/d2math" + "fmt" "image" "image/color" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2math" ) // Sprite is a positioned visual object. @@ -17,17 +17,20 @@ type Sprite struct { animation d2interface.Animation } -var ( - ErrNoAnimation = errors.New("no animation was specified") +const ( + errNoAnimation = "no animation was specified" ) -func LoadSprite(animation d2interface.Animation) (*Sprite, error) { +// NewSprite creates a new Sprite +func (ui *UIManager) NewSprite(animation d2interface.Animation) (*Sprite, error) { if animation == nil { - return nil, ErrNoAnimation + return nil, fmt.Errorf(errNoAnimation) } + return &Sprite{animation: animation}, nil } +// Render renders the sprite on the given surface func (s *Sprite) Render(target d2interface.Surface) error { _, frameHeight := s.animation.GetCurrentFrameSize() @@ -45,12 +48,12 @@ func (s *Sprite) RenderSection(sfc d2interface.Surface, bound image.Rectangle) e return s.animation.RenderSection(sfc, bound) } +// RenderSegmented renders a sprite that is internally segmented as frames func (s *Sprite) RenderSegmented(target d2interface.Surface, segmentsX, segmentsY, frameOffset int) error { var currentY int for y := 0; y < segmentsY; y++ { - var currentX int - var maxFrameHeight int + var currentX, maxFrameHeight int for x := 0; x < segmentsX; x++ { if err := s.animation.SetCurrentFrame(x + y*segmentsX + frameOffset*segmentsX*segmentsY); err != nil { @@ -83,22 +86,22 @@ func (s *Sprite) SetPosition(x, y int) { } // GetPosition retrieves the 2D position of the sprite -func (s *Sprite) GetPosition() (int, int) { +func (s *Sprite) GetPosition() (x, y int) { return s.x, s.y } // GetFrameSize gets the Size(width, height) of a indexed frame. -func (s *Sprite) GetFrameSize(frameIndex int) (int, int, error) { +func (s *Sprite) GetFrameSize(frameIndex int) (x, y int, err error) { return s.animation.GetFrameSize(frameIndex) } // GetCurrentFrameSize gets the Size(width, height) of the current frame. -func (s *Sprite) GetCurrentFrameSize() (int, int) { +func (s *Sprite) GetCurrentFrameSize() (width, height int) { return s.animation.GetCurrentFrameSize() } // GetFrameBounds gets maximum Size(width, height) of all frame. -func (s *Sprite) GetFrameBounds() (int, int) { +func (s *Sprite) GetFrameBounds() (width, height int) { return s.animation.GetFrameBounds() } @@ -144,7 +147,7 @@ func (s *Sprite) SetCurrentFrame(frameIndex int) error { // Rewind sprite to beginning func (s *Sprite) Rewind() { - s.animation.SetCurrentFrame(0) + _ = s.animation.SetCurrentFrame(0) } // PlayForward plays sprite forward @@ -167,22 +170,27 @@ func (s *Sprite) SetPlayLoop(loop bool) { s.animation.SetPlayLoop(loop) } +// SetPlayLength sets the play length of the sprite animation func (s *Sprite) SetPlayLength(playLength float64) { s.animation.SetPlayLength(playLength) } +// SetPlayLengthMs sets the play length of the sprite animation in milliseconds func (s *Sprite) SetPlayLengthMs(playLengthMs int) { s.animation.SetPlayLengthMs(playLengthMs) } -func (s *Sprite) SetColorMod(color color.Color) { - s.animation.SetColorMod(color) +// SetColorMod sets the color modifier +func (s *Sprite) SetColorMod(c color.Color) { + s.animation.SetColorMod(c) } +// Advance advances the animation func (s *Sprite) Advance(elapsed float64) error { return s.animation.Advance(elapsed) } +// SetEffect sets the draw effect type func (s *Sprite) SetEffect(e d2enum.DrawEffect) { s.animation.SetEffect(e) } diff --git a/d2core/d2ui/textbox.go b/d2core/d2ui/textbox.go index af64fd18..68fc3aec 100644 --- a/d2core/d2ui/textbox.go +++ b/d2core/d2ui/textbox.go @@ -12,8 +12,9 @@ import ( // TextBox represents a text input box type TextBox struct { - textLabel Label - lineBar Label + manager *UIManager + textLabel *Label + lineBar *Label text string filter string x int @@ -24,20 +25,22 @@ type TextBox struct { isFocused bool } -// CreateTextbox creates a new instance of a text box -func CreateTextbox() TextBox { +// NewTextbox creates a new instance of a text box +func (ui *UIManager) NewTextbox() *TextBox { animation, _ := d2asset.LoadAnimation(d2resource.TextBox2, d2resource.PaletteUnits) - bgSprite, _ := LoadSprite(animation) - tb := TextBox{ + bgSprite, _ := ui.NewSprite(animation) + tb := &TextBox{ filter: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", bgSprite: bgSprite, - textLabel: CreateLabel(d2resource.FontFormal11, d2resource.PaletteUnits), - lineBar: CreateLabel(d2resource.FontFormal11, d2resource.PaletteUnits), + textLabel: ui.NewLabel(d2resource.FontFormal11, d2resource.PaletteUnits), + lineBar: ui.NewLabel(d2resource.FontFormal11, d2resource.PaletteUnits), enabled: true, visible: true, } tb.lineBar.SetText("_") + ui.addWidget(tb) + return tb } @@ -65,6 +68,11 @@ func (v *TextBox) Render(target d2interface.Surface) error { return nil } +// bindManager binds the textbox to the UI manager +func (v *TextBox) bindManager(manager *UIManager) { + v.manager = manager +} + // OnKeyChars handles key character events func (v *TextBox) OnKeyChars(event d2interface.KeyCharsEvent) bool { if !v.isFocused || !v.visible || !v.enabled { @@ -115,13 +123,11 @@ func debounceEvents(numFrames int) bool { } // Advance updates the text box -func (v *TextBox) Advance(_ float64) { - if !v.visible || !v.enabled { - return - } +func (v *TextBox) Advance(_ float64) error { + return nil } -// Update updates the textbox +// Update updates the textbox (not currently implemented) func (v *TextBox) Update() { } diff --git a/d2core/d2ui/ui_manager.go b/d2core/d2ui/ui_manager.go new file mode 100644 index 00000000..410d24a2 --- /dev/null +++ b/d2core/d2ui/ui_manager.go @@ -0,0 +1,140 @@ +package d2ui + +import ( + "log" + + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource" +) + +// UIManager manages a collection of UI elements (buttons, textboxes, labels) +type UIManager struct { + renderer d2interface.Renderer + inputManager d2interface.InputManager + audio d2interface.AudioProvider + widgets []Widget + cursorButtons CursorButton // TODO (carrelld) convert dependent code and remove + CursorX int // TODO (carrelld) convert dependent code and remove + CursorY int // TODO (carrelld) convert dependent code and remove + pressedWidget Widget + clickSfx d2interface.SoundEffect +} + +// Note: methods for creating buttons and stuff are in their respective files + +// Initialize is meant to be called after the game loads all of the necessary files +// for sprites and audio +func (ui *UIManager) Initialize() { + sfx, err := ui.audio.LoadSound(d2resource.SFXButtonClick, false, false) + if err != nil { + log.Fatalf("failed to initialize ui: %v", err) + } + + ui.clickSfx = sfx + + if err := ui.inputManager.BindHandler(ui); err != nil { + log.Fatalf("failed to initialize ui: %v", err) + } +} + +// Reset resets the state of the UI manager. Typically called for new screens +func (ui *UIManager) Reset() { + ui.widgets = nil + ui.pressedWidget = nil +} + +// addWidget adds a widget to the UI manager +func (ui *UIManager) addWidget(widget Widget) { + _ = ui.inputManager.BindHandler(widget) + ui.widgets = append(ui.widgets, widget) + + widget.bindManager(ui) +} + +// OnMouseButtonUp is an event handler for input +func (ui *UIManager) OnMouseButtonUp(event d2interface.MouseEvent) bool { + ui.CursorX, ui.CursorY = event.X(), event.Y() + if event.Button() == d2enum.MouseButtonLeft { + ui.cursorButtons |= CursorButtonLeft + // activate previously pressed widget if cursor is still hovering + w := ui.pressedWidget + + if w != nil && ui.contains(w, ui.CursorX, ui.CursorY) && w.GetVisible() && w. + GetEnabled() { + w.Activate() + } + + // unpress all widgets that are pressed + for _, w := range ui.widgets { + w.SetPressed(false) + } + } + + return false +} + +// OnMouseButtonDown is the mouse button down event handler +func (ui *UIManager) OnMouseButtonDown(event d2interface.MouseEvent) bool { + ui.CursorX, ui.CursorY = event.X(), event.Y() + if event.Button() == d2enum.MouseButtonLeft { + // find and press a widget on screen + ui.pressedWidget = nil + for _, w := range ui.widgets { + if ui.contains(w, ui.CursorX, ui.CursorY) && w.GetVisible() && w.GetEnabled() { + w.SetPressed(true) + ui.pressedWidget = w + ui.clickSfx.Play() + + break + } + } + } + + if event.Button() == d2enum.MouseButtonRight { + ui.cursorButtons |= CursorButtonRight + } + + return false +} + +// Render renders all of the UI elements +func (ui *UIManager) Render(target d2interface.Surface) { + for _, widget := range ui.widgets { + if widget.GetVisible() { + _ = widget.Render(target) + } + } +} + +// contains determines whether a given x,y coordinate lands within a Widget +func (ui *UIManager) contains(w Widget, x, y int) bool { + wx, wy := w.GetPosition() + ww, wh := w.GetSize() + + return x >= wx && x <= wx+ww && y >= wy && y <= wy+wh +} + +// Advance updates all of the UI elements +func (ui *UIManager) Advance(elapsed float64) { + for _, widget := range ui.widgets { + if widget.GetVisible() { + _ = widget.Advance(elapsed) + } + } +} + +// CursorButtonPressed determines if the specified button has been pressed +func (ui *UIManager) CursorButtonPressed(button CursorButton) bool { + return ui.cursorButtons&button > 0 +} + +// CursorPosition returns the current cursor position +func (ui *UIManager) CursorPosition() (x, y int) { + return ui.CursorX, ui.CursorY +} + +// Renderer returns the renderer for this ui manager +func (ui *UIManager) Renderer() d2interface.Renderer { + return ui.renderer +} diff --git a/d2core/d2ui/widget.go b/d2core/d2ui/widget.go index 3b53c380..7e3307b2 100644 --- a/d2core/d2ui/widget.go +++ b/d2core/d2ui/widget.go @@ -3,6 +3,7 @@ package d2ui // Widget defines an object that is a UI widget type Widget interface { Drawable + bindManager(ui *UIManager) GetEnabled() bool SetEnabled(enabled bool) SetPressed(pressed bool) diff --git a/d2game/d2gamescreen/character_select.go b/d2game/d2gamescreen/character_select.go index e2a431c1..f51c6c0d 100644 --- a/d2game/d2gamescreen/character_select.go +++ b/d2game/d2gamescreen/character_select.go @@ -22,21 +22,21 @@ import ( // CharacterSelect represents the character select screen type CharacterSelect struct { background *d2ui.Sprite - newCharButton d2ui.Button - convertCharButton d2ui.Button - deleteCharButton d2ui.Button - exitButton d2ui.Button - okButton d2ui.Button - deleteCharCancelButton d2ui.Button - deleteCharOkButton d2ui.Button + newCharButton *d2ui.Button + convertCharButton *d2ui.Button + deleteCharButton *d2ui.Button + exitButton *d2ui.Button + okButton *d2ui.Button + deleteCharCancelButton *d2ui.Button + deleteCharOkButton *d2ui.Button selectionBox *d2ui.Sprite okCancelBox *d2ui.Sprite - d2HeroTitle d2ui.Label - deleteCharConfirmLabel d2ui.Label - charScrollbar d2ui.Scrollbar - characterNameLabel [8]d2ui.Label - characterStatsLabel [8]d2ui.Label - characterExpLabel [8]d2ui.Label + d2HeroTitle *d2ui.Label + deleteCharConfirmLabel *d2ui.Label + charScrollbar *d2ui.Scrollbar + characterNameLabel [8]*d2ui.Label + characterStatsLabel [8]*d2ui.Label + characterExpLabel [8]*d2ui.Label characterImage [8]*d2mapentity.Player gameStates []*d2player.PlayerState selectedCharacter int @@ -44,6 +44,7 @@ type CharacterSelect struct { connectionType d2clientconnectiontype.ClientConnectionType connectionHost string + uiManager *d2ui.UIManager inputManager d2interface.InputManager audioProvider d2interface.AudioProvider renderer d2interface.Renderer @@ -56,6 +57,7 @@ func CreateCharacterSelect( renderer d2interface.Renderer, inputManager d2interface.InputManager, audioProvider d2interface.AudioProvider, + ui *d2ui.UIManager, connectionType d2clientconnectiontype.ClientConnectionType, connectionHost string, ) *CharacterSelect { @@ -67,6 +69,7 @@ func CreateCharacterSelect( inputManager: inputManager, audioProvider: audioProvider, navigator: navigator, + uiManager: ui, } } @@ -132,19 +135,19 @@ func (v *CharacterSelect) OnLoad(loading d2screen.LoadingState) { animation, _ := d2asset.LoadAnimation(d2resource.CharacterSelectionBackground, d2resource.PaletteSky) bgX, bgY := 0, 0 - v.background, _ = d2ui.LoadSprite(animation) + v.background, _ = v.uiManager.NewSprite(animation) v.background.SetPosition(bgX, bgY) v.createButtons(loading) heroTitleX, heroTitleY := 320, 23 - v.d2HeroTitle = d2ui.CreateLabel(d2resource.Font42, d2resource.PaletteUnits) + v.d2HeroTitle = v.uiManager.NewLabel(d2resource.Font42, d2resource.PaletteUnits) v.d2HeroTitle.SetPosition(heroTitleX, heroTitleY) v.d2HeroTitle.Alignment = d2gui.HorizontalAlignCenter loading.Progress(thirtyPercent) - v.deleteCharConfirmLabel = d2ui.CreateLabel(d2resource.Font16, d2resource.PaletteUnits) + 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 @@ -152,19 +155,18 @@ func (v *CharacterSelect) OnLoad(loading d2screen.LoadingState) { v.deleteCharConfirmLabel.SetPosition(deleteConfirmX, deleteConfirmY) animation, _ = d2asset.LoadAnimation(d2resource.CharacterSelectionSelectBox, d2resource.PaletteSky) - v.selectionBox, _ = d2ui.LoadSprite(animation) + v.selectionBox, _ = v.uiManager.NewSprite(animation) selBoxX, selBoxY := 37, 86 v.selectionBox.SetPosition(selBoxX, selBoxY) animation, _ = d2asset.LoadAnimation(d2resource.PopUpOkCancel, d2resource.PaletteFechar) - v.okCancelBox, _ = d2ui.LoadSprite(animation) + v.okCancelBox, _ = v.uiManager.NewSprite(animation) okCancelX, okCancelY := 270, 175 v.okCancelBox.SetPosition(okCancelX, okCancelY) scrollBarX, scrollBarY, scrollBarHeight := 586, 87, 369 - v.charScrollbar = d2ui.CreateScrollbar(scrollBarX, scrollBarY, scrollBarHeight) + v.charScrollbar = v.uiManager.NewScrollbar(scrollBarX, scrollBarY, scrollBarHeight) v.charScrollbar.OnActivated(func() { v.onScrollUpdate() }) - d2ui.AddWidget(&v.charScrollbar) loading.Progress(fiftyPercent) @@ -174,16 +176,16 @@ func (v *CharacterSelect) OnLoad(loading d2screen.LoadingState) { offsetX = 385 } - v.characterNameLabel[i] = d2ui.CreateLabel(d2resource.Font16, d2resource.PaletteUnits) + v.characterNameLabel[i] = v.uiManager.NewLabel(d2resource.Font16, d2resource.PaletteUnits) v.characterNameLabel[i].SetPosition(offsetX, offsetY) v.characterNameLabel[i].Color[0] = rgbaColor(lightBrown) offsetY += labelHeight - v.characterStatsLabel[i] = d2ui.CreateLabel(d2resource.Font16, d2resource.PaletteUnits) + v.characterStatsLabel[i] = v.uiManager.NewLabel(d2resource.Font16, d2resource.PaletteUnits) v.characterStatsLabel[i].SetPosition(offsetX, offsetY) offsetY += labelHeight - v.characterExpLabel[i] = d2ui.CreateLabel(d2resource.Font16, d2resource.PaletteStatic) + v.characterExpLabel[i] = v.uiManager.NewLabel(d2resource.Font16, d2resource.PaletteStatic) v.characterExpLabel[i].SetPosition(offsetX, offsetY) v.characterExpLabel[i].Color[0] = rgbaColor(lightGreen) } @@ -216,58 +218,39 @@ func rgbaColor(rgba uint32) color.RGBA { } func (v *CharacterSelect) createButtons(loading d2screen.LoadingState) { - v.newCharButton = d2ui.CreateButton( - v.renderer, - d2ui.ButtonTypeTall, - "CREATE NEW\nCHARACTER", - ) + v.newCharButton = v.uiManager.NewButton(d2ui.ButtonTypeTall, "CREATE NEW\nCHARACTER") v.newCharButton.SetPosition(newCharBtnX, newCharBtnY) v.newCharButton.OnActivated(func() { v.onNewCharButtonClicked() }) - d2ui.AddWidget(&v.newCharButton) - v.convertCharButton = d2ui.CreateButton( - v.renderer, - d2ui.ButtonTypeTall, - "CONVERT TO\nEXPANSION", - ) + v.convertCharButton = v.uiManager.NewButton(d2ui.ButtonTypeTall, "CONVERT TO\nEXPANSION") v.convertCharButton.SetPosition(convertCharBtnX, convertCharBtnY) v.convertCharButton.SetEnabled(false) - d2ui.AddWidget(&v.convertCharButton) - v.deleteCharButton = d2ui.CreateButton( - v.renderer, - d2ui.ButtonTypeTall, - "DELETE\nCHARACTER", - ) + v.deleteCharButton = v.uiManager.NewButton(d2ui.ButtonTypeTall, "DELETE\nCHARACTER") v.deleteCharButton.OnActivated(func() { v.onDeleteCharButtonClicked() }) v.deleteCharButton.SetPosition(deleteCharBtnX, deleteCharBtnY) - d2ui.AddWidget(&v.deleteCharButton) - v.exitButton = d2ui.CreateButton(v.renderer, d2ui.ButtonTypeMedium, "EXIT") + v.exitButton = v.uiManager.NewButton(d2ui.ButtonTypeMedium, "EXIT") v.exitButton.SetPosition(exitBtnX, exitBtnY) v.exitButton.OnActivated(func() { v.onExitButtonClicked() }) - d2ui.AddWidget(&v.exitButton) loading.Progress(twentyPercent) - v.deleteCharCancelButton = d2ui.CreateButton(v.renderer, d2ui.ButtonTypeOkCancel, "NO") + v.deleteCharCancelButton = v.uiManager.NewButton(d2ui.ButtonTypeOkCancel, "NO") v.deleteCharCancelButton.SetPosition(deleteCancelX, deleteCancelY) v.deleteCharCancelButton.SetVisible(false) v.deleteCharCancelButton.OnActivated(func() { v.onDeleteCharacterCancelClicked() }) - d2ui.AddWidget(&v.deleteCharCancelButton) - v.deleteCharOkButton = d2ui.CreateButton(v.renderer, d2ui.ButtonTypeOkCancel, "YES") + v.deleteCharOkButton = v.uiManager.NewButton(d2ui.ButtonTypeOkCancel, "YES") v.deleteCharOkButton.SetPosition(deleteOkX, deleteOkY) v.deleteCharOkButton.SetVisible(false) v.deleteCharOkButton.OnActivated(func() { v.onDeleteCharacterConfirmClicked() }) - d2ui.AddWidget(&v.deleteCharOkButton) - v.okButton = d2ui.CreateButton(v.renderer, d2ui.ButtonTypeMedium, "OK") + v.okButton = v.uiManager.NewButton(d2ui.ButtonTypeMedium, "OK") v.okButton.SetPosition(okBtnX, okBtnY) v.okButton.OnActivated(func() { v.onOkButtonClicked() }) - d2ui.AddWidget(&v.okButton) } func (v *CharacterSelect) onScrollUpdate() { diff --git a/d2game/d2gamescreen/credits.go b/d2game/d2gamescreen/credits.go index 8ce6b20b..3c04fec3 100644 --- a/d2game/d2gamescreen/credits.go +++ b/d2game/d2gamescreen/credits.go @@ -17,14 +17,14 @@ import ( ) const ( - creditsX, creditsY = 0, 0 + creditsX, creditsY = 0, 0 charSelExitBtnX, charSelExitBtnY = 33, 543 ) const secondsPerCycle float64 = 0.02 type labelItem struct { - Label d2ui.Label + Label *d2ui.Label IsHeading bool Available bool } @@ -32,7 +32,7 @@ type labelItem struct { // Credits represents the credits screen type Credits struct { creditsBackground *d2ui.Sprite - exitButton d2ui.Button + exitButton *d2ui.Button creditsText []string labels []*labelItem cycleTime float64 @@ -41,10 +41,11 @@ type Credits struct { renderer d2interface.Renderer navigator Navigator + uiManager *d2ui.UIManager } // CreateCredits creates an instance of the credits screen -func CreateCredits(navigator Navigator, renderer d2interface.Renderer) *Credits { +func CreateCredits(navigator Navigator, renderer d2interface.Renderer, ui *d2ui.UIManager) *Credits { result := &Credits{ labels: make([]*labelItem, 0), cycleTime: 0, @@ -52,6 +53,7 @@ func CreateCredits(navigator Navigator, renderer d2interface.Renderer) *Credits cyclesTillNextLine: 0, renderer: renderer, navigator: navigator, + uiManager: ui, } return result @@ -85,14 +87,13 @@ func (v *Credits) LoadContributors() []string { // OnLoad is called to load the resources for the credits screen func (v *Credits) OnLoad(loading d2screen.LoadingState) { animation, _ := d2asset.LoadAnimation(d2resource.CreditsBackground, d2resource.PaletteSky) - v.creditsBackground, _ = d2ui.LoadSprite(animation) + v.creditsBackground, _ = v.uiManager.NewSprite(animation) v.creditsBackground.SetPosition(creditsX, creditsY) loading.Progress(twentyPercent) - v.exitButton = d2ui.CreateButton(v.renderer, d2ui.ButtonTypeMedium, "EXIT") + v.exitButton = v.uiManager.NewButton(d2ui.ButtonTypeMedium, "EXIT") v.exitButton.SetPosition(charSelExitBtnX, charSelExitBtnY) v.exitButton.OnActivated(func() { v.onExitButtonClicked() }) - d2ui.AddWidget(&v.exitButton) loading.Progress(fourtyPercent) fileData, err := d2asset.LoadFile(d2resource.CreditsText) @@ -214,17 +215,17 @@ func (v *Credits) addNextItem() { } } -const( - itemLabelY = 605 - itemLabelX = 400 - itemLabel2offsetX = 10 - halfItemLabel2offsetX = itemLabel2offsetX/2 +const ( + itemLabelY = 605 + itemLabelX = 400 + itemLabel2offsetX = 10 + halfItemLabel2offsetX = itemLabel2offsetX / 2 ) func (v *Credits) setItemLabelPosition(label *d2ui.Label, isHeading, isNextHeading, isNextSpace bool) (isDoubled, nextHeading bool) { width, _ := label.GetSize() half := 2 - halfWidth := width/half + halfWidth := width / half if !isHeading && !isNextHeading && !isNextSpace { isDoubled = true @@ -250,7 +251,7 @@ func (v *Credits) setItemLabelPosition(label *d2ui.Label, isHeading, isNextHeadi const ( lightRed = 0xff5852ff - beige = 0xc6b296ff + beige = 0xc6b296ff ) func (v *Credits) getNewFontLabel(isHeading bool) *d2ui.Label { @@ -263,14 +264,14 @@ func (v *Credits) getNewFontLabel(isHeading bool) *d2ui.Label { label.Label.Color[0] = rgbaColor(beige) } - return &label.Label + return label.Label } } newLabelItem := &labelItem{ Available: false, IsHeading: isHeading, - Label: d2ui.CreateLabel(d2resource.FontFormal10, d2resource.PaletteSky), + Label: v.uiManager.NewLabel(d2resource.FontFormal10, d2resource.PaletteSky), } if isHeading { @@ -281,5 +282,5 @@ func (v *Credits) getNewFontLabel(isHeading bool) *d2ui.Label { v.labels = append(v.labels, newLabelItem) - return &newLabelItem.Label + return newLabelItem.Label } diff --git a/d2game/d2gamescreen/game.go b/d2game/d2gamescreen/game.go index 64020d55..c4193941 100644 --- a/d2game/d2gamescreen/game.go +++ b/d2game/d2gamescreen/game.go @@ -3,6 +3,7 @@ package d2gamescreen import ( "fmt" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2math/d2vector" + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2ui" "image/color" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict" @@ -30,6 +31,7 @@ const ( type Game struct { gameClient *d2client.GameClient mapRenderer *d2maprenderer.MapRenderer + uiManager *d2ui.UIManager gameControls *d2player.GameControls // TODO: Hack localPlayer *d2mapentity.Player lastRegionType d2enum.RegionIdType @@ -77,6 +79,7 @@ func CreateGame( renderer: renderer, terminal: term, soundEngine: d2audio.NewSoundEngine(audioProvider, term), + uiManager: d2ui.NewUIManager(renderer, inputManager, audioProvider), } result.soundEnv = d2audio.NewSoundEnvironment(result.soundEngine) @@ -244,7 +247,8 @@ func (v *Game) bindGameControls() error { v.localPlayer = player var err error - v.gameControls, err = d2player.NewGameControls(v.renderer, player, v.gameClient.MapEngine, v.mapRenderer, v, v.terminal) + v.gameControls, err = d2player.NewGameControls(v.renderer, player, + v.gameClient.MapEngine, v.mapRenderer, v, v.terminal, v.uiManager) if err != nil { return err diff --git a/d2game/d2gamescreen/main_menu.go b/d2game/d2gamescreen/main_menu.go index 2dfed1bd..fa4ddaf8 100644 --- a/d2game/d2gamescreen/main_menu.go +++ b/d2game/d2gamescreen/main_menu.go @@ -86,28 +86,28 @@ type MainMenu struct { diabloLogoLeftBack *d2ui.Sprite diabloLogoRightBack *d2ui.Sprite serverIPBackground *d2ui.Sprite - singlePlayerButton d2ui.Button - multiplayerButton d2ui.Button - githubButton d2ui.Button - exitDiabloButton d2ui.Button - creditsButton d2ui.Button - cinematicsButton d2ui.Button - mapTestButton d2ui.Button - networkTCPIPButton d2ui.Button - networkCancelButton d2ui.Button - btnTCPIPCancel d2ui.Button - btnTCPIPHostGame d2ui.Button - btnTCPIPJoinGame d2ui.Button - btnServerIPCancel d2ui.Button - btnServerIPOk d2ui.Button - copyrightLabel d2ui.Label - copyrightLabel2 d2ui.Label - openDiabloLabel d2ui.Label - versionLabel d2ui.Label - commitLabel d2ui.Label - tcpIPOptionsLabel d2ui.Label - tcpJoinGameLabel d2ui.Label - tcpJoinGameEntry d2ui.TextBox + singlePlayerButton *d2ui.Button + multiplayerButton *d2ui.Button + githubButton *d2ui.Button + exitDiabloButton *d2ui.Button + creditsButton *d2ui.Button + cinematicsButton *d2ui.Button + mapTestButton *d2ui.Button + networkTCPIPButton *d2ui.Button + networkCancelButton *d2ui.Button + btnTCPIPCancel *d2ui.Button + btnTCPIPHostGame *d2ui.Button + btnTCPIPJoinGame *d2ui.Button + btnServerIPCancel *d2ui.Button + btnServerIPOk *d2ui.Button + copyrightLabel *d2ui.Label + copyrightLabel2 *d2ui.Label + openDiabloLabel *d2ui.Label + versionLabel *d2ui.Label + commitLabel *d2ui.Label + tcpIPOptionsLabel *d2ui.Label + tcpJoinGameLabel *d2ui.Label + tcpJoinGameEntry *d2ui.TextBox screenMode mainMenuScreenMode leftButtonHeld bool @@ -116,6 +116,7 @@ type MainMenu struct { audioProvider d2interface.AudioProvider scriptEngine *d2script.ScriptEngine navigator Navigator + uiManager *d2ui.UIManager buildInfo BuildInfo } @@ -126,6 +127,7 @@ func CreateMainMenu( renderer d2interface.Renderer, inputManager d2interface.InputManager, audioProvider d2interface.AudioProvider, + ui *d2ui.UIManager, buildInfo BuildInfo, ) *MainMenu { return &MainMenu{ @@ -135,7 +137,8 @@ func CreateMainMenu( inputManager: inputManager, audioProvider: audioProvider, navigator: navigator, - buildInfo: buildInfo, + buildInfo: buildInfo, + uiManager: ui, } } @@ -149,10 +152,9 @@ func (v *MainMenu) OnLoad(loading d2screen.LoadingState) { v.createLogos(loading) v.createButtons(loading) - v.tcpJoinGameEntry = d2ui.CreateTextbox() + v.tcpJoinGameEntry = v.uiManager.NewTextbox() v.tcpJoinGameEntry.SetPosition(joinGameDialogX, joinGameDialogY) v.tcpJoinGameEntry.SetFilter(joinGameCharacterFilter) - d2ui.AddWidget(&v.tcpJoinGameEntry) loading.Progress(ninetyPercent) if v.screenMode == ScreenModeUnknown { @@ -168,61 +170,61 @@ func (v *MainMenu) OnLoad(loading d2screen.LoadingState) { func (v *MainMenu) loadBackgroundSprites() { animation, _ := d2asset.LoadAnimation(d2resource.GameSelectScreen, d2resource.PaletteSky) - v.background, _ = d2ui.LoadSprite(animation) + v.background, _ = v.uiManager.NewSprite(animation) v.background.SetPosition(backgroundX, backgroundY) animation, _ = d2asset.LoadAnimation(d2resource.TrademarkScreen, d2resource.PaletteSky) - v.trademarkBackground, _ = d2ui.LoadSprite(animation) + v.trademarkBackground, _ = v.uiManager.NewSprite(animation) v.trademarkBackground.SetPosition(backgroundX, backgroundY) animation, _ = d2asset.LoadAnimation(d2resource.TCPIPBackground, d2resource.PaletteSky) - v.tcpIPBackground, _ = d2ui.LoadSprite(animation) + v.tcpIPBackground, _ = v.uiManager.NewSprite(animation) v.tcpIPBackground.SetPosition(backgroundX, backgroundY) animation, _ = d2asset.LoadAnimation(d2resource.PopUpOkCancel, d2resource.PaletteFechar) - v.serverIPBackground, _ = d2ui.LoadSprite(animation) + v.serverIPBackground, _ = v.uiManager.NewSprite(animation) v.serverIPBackground.SetPosition(serverIPbackgroundX, serverIPbackgroundY) } func (v *MainMenu) createLabels(loading d2screen.LoadingState) { - v.versionLabel = d2ui.CreateLabel(d2resource.FontFormal12, d2resource.PaletteStatic) + v.versionLabel = v.uiManager.NewLabel(d2resource.FontFormal12, d2resource.PaletteStatic) v.versionLabel.Alignment = d2gui.HorizontalAlignRight v.versionLabel.SetText("OpenDiablo2 - " + v.buildInfo.Branch) v.versionLabel.Color[0] = rgbaColor(white) v.versionLabel.SetPosition(versionLabelX, versionLabelY) - v.commitLabel = d2ui.CreateLabel(d2resource.FontFormal10, d2resource.PaletteStatic) + v.commitLabel = v.uiManager.NewLabel(d2resource.FontFormal10, d2resource.PaletteStatic) v.commitLabel.Alignment = d2gui.HorizontalAlignLeft v.commitLabel.SetText(v.buildInfo.Commit) v.commitLabel.Color[0] = rgbaColor(white) v.commitLabel.SetPosition(commitLabelX, commitLabelY) - v.copyrightLabel = d2ui.CreateLabel(d2resource.FontFormal12, d2resource.PaletteStatic) + v.copyrightLabel = v.uiManager.NewLabel(d2resource.FontFormal12, d2resource.PaletteStatic) v.copyrightLabel.Alignment = d2gui.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 = d2ui.CreateLabel(d2resource.FontFormal12, d2resource.PaletteStatic) + v.copyrightLabel2 = v.uiManager.NewLabel(d2resource.FontFormal12, d2resource.PaletteStatic) v.copyrightLabel2.Alignment = d2gui.HorizontalAlignCenter v.copyrightLabel2.SetText("All Rights Reserved.") v.copyrightLabel2.Color[0] = rgbaColor(lightBrown) v.copyrightLabel2.SetPosition(copyright2X, copyright2Y) - v.openDiabloLabel = d2ui.CreateLabel(d2resource.FontFormal10, d2resource.PaletteStatic) + v.openDiabloLabel = v.uiManager.NewLabel(d2resource.FontFormal10, d2resource.PaletteStatic) v.openDiabloLabel.Alignment = d2gui.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) loading.Progress(fiftyPercent) - v.tcpIPOptionsLabel = d2ui.CreateLabel(d2resource.Font42, d2resource.PaletteUnits) + v.tcpIPOptionsLabel = v.uiManager.NewLabel(d2resource.Font42, d2resource.PaletteUnits) v.tcpIPOptionsLabel.SetPosition(tcpOptionsX, tcpOptionsY) v.tcpIPOptionsLabel.Alignment = d2gui.HorizontalAlignCenter v.tcpIPOptionsLabel.SetText("TCP/IP Options") - v.tcpJoinGameLabel = d2ui.CreateLabel(d2resource.Font16, d2resource.PaletteUnits) + v.tcpJoinGameLabel = v.uiManager.NewLabel(d2resource.Font16, d2resource.PaletteUnits) v.tcpJoinGameLabel.Alignment = d2gui.HorizontalAlignCenter v.tcpJoinGameLabel.SetText("Enter Host IP Address\nto Join Game") @@ -232,102 +234,90 @@ func (v *MainMenu) createLabels(loading d2screen.LoadingState) { func (v *MainMenu) createLogos(loading d2screen.LoadingState) { animation, _ := d2asset.LoadAnimation(d2resource.Diablo2LogoFireLeft, d2resource.PaletteUnits) - v.diabloLogoLeft, _ = d2ui.LoadSprite(animation) + v.diabloLogoLeft, _ = v.uiManager.NewSprite(animation) v.diabloLogoLeft.SetEffect(d2enum.DrawEffectModulate) v.diabloLogoLeft.PlayForward() v.diabloLogoLeft.SetPosition(diabloLogoX, diabloLogoY) loading.Progress(sixtyPercent) animation, _ = d2asset.LoadAnimation(d2resource.Diablo2LogoFireRight, d2resource.PaletteUnits) - v.diabloLogoRight, _ = d2ui.LoadSprite(animation) + v.diabloLogoRight, _ = v.uiManager.NewSprite(animation) v.diabloLogoRight.SetEffect(d2enum.DrawEffectModulate) v.diabloLogoRight.PlayForward() v.diabloLogoRight.SetPosition(diabloLogoX, diabloLogoY) animation, _ = d2asset.LoadAnimation(d2resource.Diablo2LogoBlackLeft, d2resource.PaletteUnits) - v.diabloLogoLeftBack, _ = d2ui.LoadSprite(animation) + v.diabloLogoLeftBack, _ = v.uiManager.NewSprite(animation) v.diabloLogoLeftBack.SetPosition(diabloLogoX, diabloLogoY) animation, _ = d2asset.LoadAnimation(d2resource.Diablo2LogoBlackRight, d2resource.PaletteUnits) - v.diabloLogoRightBack, _ = d2ui.LoadSprite(animation) + v.diabloLogoRightBack, _ = v.uiManager.NewSprite(animation) v.diabloLogoRightBack.SetPosition(diabloLogoX, diabloLogoY) } func (v *MainMenu) createButtons(loading d2screen.LoadingState) { - v.exitDiabloButton = d2ui.CreateButton(v.renderer, d2ui.ButtonTypeWide, "EXIT DIABLO II") + v.exitDiabloButton = v.uiManager.NewButton(d2ui.ButtonTypeWide, "EXIT DIABLO II") v.exitDiabloButton.SetPosition(exitDiabloBtnX, exitDiabloBtnY) v.exitDiabloButton.OnActivated(func() { v.onExitButtonClicked() }) - d2ui.AddWidget(&v.exitDiabloButton) - v.creditsButton = d2ui.CreateButton(v.renderer, d2ui.ButtonTypeShort, "CREDITS") + v.creditsButton = v.uiManager.NewButton(d2ui.ButtonTypeShort, "CREDITS") v.creditsButton.SetPosition(creditBtnX, creditBtnY) v.creditsButton.OnActivated(func() { v.onCreditsButtonClicked() }) - d2ui.AddWidget(&v.creditsButton) - v.cinematicsButton = d2ui.CreateButton(v.renderer, d2ui.ButtonTypeShort, "CINEMATICS") + v.cinematicsButton = v.uiManager.NewButton(d2ui.ButtonTypeShort, "CINEMATICS") v.cinematicsButton.SetPosition(cineBtnX, cineBtnY) - d2ui.AddWidget(&v.cinematicsButton) loading.Progress(seventyPercent) - v.singlePlayerButton = d2ui.CreateButton(v.renderer, d2ui.ButtonTypeWide, "SINGLE PLAYER") + v.singlePlayerButton = v.uiManager.NewButton(d2ui.ButtonTypeWide, "SINGLE PLAYER") v.singlePlayerButton.SetPosition(singlePlayerBtnX, singlePlayerBtnY) v.singlePlayerButton.OnActivated(func() { v.onSinglePlayerClicked() }) - d2ui.AddWidget(&v.singlePlayerButton) - v.githubButton = d2ui.CreateButton(v.renderer, d2ui.ButtonTypeWide, "PROJECT WEBSITE") + v.githubButton = v.uiManager.NewButton(d2ui.ButtonTypeWide, "PROJECT WEBSITE") v.githubButton.SetPosition(githubBtnX, githubBtnY) v.githubButton.OnActivated(func() { v.onGithubButtonClicked() }) - d2ui.AddWidget(&v.githubButton) - v.mapTestButton = d2ui.CreateButton(v.renderer, d2ui.ButtonTypeWide, "MAP ENGINE TEST") + v.mapTestButton = v.uiManager.NewButton(d2ui.ButtonTypeWide, "MAP ENGINE TEST") v.mapTestButton.SetPosition(mapTestBtnX, mapTestBtnY) v.mapTestButton.OnActivated(func() { v.onMapTestClicked() }) - d2ui.AddWidget(&v.mapTestButton) - v.btnTCPIPCancel = d2ui.CreateButton(v.renderer, d2ui.ButtonTypeMedium, d2common.TranslateString("cancel")) + v.btnTCPIPCancel = v.uiManager.NewButton(d2ui.ButtonTypeMedium, + d2common.TranslateString("cancel")) v.btnTCPIPCancel.SetPosition(tcpBtnX, tcpBtnY) v.btnTCPIPCancel.OnActivated(func() { v.onTCPIPCancelClicked() }) - d2ui.AddWidget(&v.btnTCPIPCancel) - v.btnServerIPCancel = d2ui.CreateButton(v.renderer, d2ui.ButtonTypeOkCancel, "CANCEL") + v.btnServerIPCancel = v.uiManager.NewButton(d2ui.ButtonTypeOkCancel, "CANCEL") v.btnServerIPCancel.SetPosition(srvCancelBtnX, srvCancelBtnY) v.btnServerIPCancel.OnActivated(func() { v.onBtnTCPIPCancelClicked() }) - d2ui.AddWidget(&v.btnServerIPCancel) - v.btnServerIPOk = d2ui.CreateButton(v.renderer, d2ui.ButtonTypeOkCancel, "OK") + v.btnServerIPOk = v.uiManager.NewButton(d2ui.ButtonTypeOkCancel, "OK") v.btnServerIPOk.SetPosition(srvOkBtnX, srvOkBtnY) v.btnServerIPOk.OnActivated(func() { v.onBtnTCPIPOkClicked() }) - d2ui.AddWidget(&v.btnServerIPOk) v.createMultiplayerMenuButtons() loading.Progress(eightyPercent) } func (v *MainMenu) createMultiplayerMenuButtons() { - v.multiplayerButton = d2ui.CreateButton(v.renderer, d2ui.ButtonTypeWide, "MULTIPLAYER") + v.multiplayerButton = v.uiManager.NewButton(d2ui.ButtonTypeWide, "MULTIPLAYER") v.multiplayerButton.SetPosition(multiplayerBtnX, multiplayerBtnY) v.multiplayerButton.OnActivated(func() { v.onMultiplayerClicked() }) - d2ui.AddWidget(&v.multiplayerButton) - v.networkTCPIPButton = d2ui.CreateButton(v.renderer, d2ui.ButtonTypeWide, "TCP/IP GAME") + v.networkTCPIPButton = v.uiManager.NewButton(d2ui.ButtonTypeWide, "TCP/IP GAME") v.networkTCPIPButton.SetPosition(tcpNetBtnX, tcpNetBtnY) v.networkTCPIPButton.OnActivated(func() { v.onNetworkTCPIPClicked() }) - d2ui.AddWidget(&v.networkTCPIPButton) - v.networkCancelButton = d2ui.CreateButton(v.renderer, d2ui.ButtonTypeWide, d2common.TranslateString("cancel")) + v.networkCancelButton = v.uiManager.NewButton(d2ui.ButtonTypeWide, + d2common.TranslateString("cancel")) v.networkCancelButton.SetPosition(networkCancelBtnX, networkCancelBtnY) v.networkCancelButton.OnActivated(func() { v.onNetworkCancelClicked() }) - d2ui.AddWidget(&v.networkCancelButton) - v.btnTCPIPHostGame = d2ui.CreateButton(v.renderer, d2ui.ButtonTypeWide, "HOST GAME") + v.btnTCPIPHostGame = v.uiManager.NewButton(d2ui.ButtonTypeWide, "HOST GAME") v.btnTCPIPHostGame.SetPosition(tcpHostBtnX, tcpHostBtnY) v.btnTCPIPHostGame.OnActivated(func() { v.onTCPIPHostGameClicked() }) - d2ui.AddWidget(&v.btnTCPIPHostGame) - v.btnTCPIPJoinGame = d2ui.CreateButton(v.renderer, d2ui.ButtonTypeWide, "JOIN GAME") + v.btnTCPIPJoinGame = v.uiManager.NewButton(d2ui.ButtonTypeWide, "JOIN GAME") v.btnTCPIPJoinGame.SetPosition(tcpJoinBtnX, tcpJoinBtnY) v.btnTCPIPJoinGame.OnActivated(func() { v.onTCPIPJoinGameClicked() }) - d2ui.AddWidget(&v.btnTCPIPJoinGame) } func (v *MainMenu) onMapTestClicked() { diff --git a/d2game/d2gamescreen/select_hero_class.go b/d2game/d2gamescreen/select_hero_class.go index 160a8f16..3f1f24b4 100644 --- a/d2game/d2gamescreen/select_hero_class.go +++ b/d2game/d2gamescreen/select_hero_class.go @@ -267,23 +267,24 @@ func (hri *HeroRenderInfo) advance(elapsed float64) { // SelectHeroClass represents the Select Hero Class screen type SelectHeroClass struct { + uiManager *d2ui.UIManager bgImage *d2ui.Sprite campfire *d2ui.Sprite - headingLabel d2ui.Label - heroClassLabel d2ui.Label - heroDesc1Label d2ui.Label - heroDesc2Label d2ui.Label - heroDesc3Label d2ui.Label - heroNameTextbox d2ui.TextBox - heroNameLabel d2ui.Label + headingLabel *d2ui.Label + heroClassLabel *d2ui.Label + heroDesc1Label *d2ui.Label + heroDesc2Label *d2ui.Label + heroDesc3Label *d2ui.Label + heroNameTextbox *d2ui.TextBox + heroNameLabel *d2ui.Label heroRenderInfo map[d2enum.Hero]*HeroRenderInfo selectedHero d2enum.Hero - exitButton d2ui.Button - okButton d2ui.Button - expansionCheckbox d2ui.Checkbox - expansionCharLabel d2ui.Label - hardcoreCheckbox d2ui.Checkbox - hardcoreCharLabel d2ui.Label + exitButton *d2ui.Button + okButton *d2ui.Button + expansionCheckbox *d2ui.Checkbox + expansionCharLabel *d2ui.Label + hardcoreCheckbox *d2ui.Checkbox + hardcoreCharLabel *d2ui.Label connectionType d2clientconnectiontype.ClientConnectionType connectionHost string @@ -297,6 +298,7 @@ func CreateSelectHeroClass( navigator Navigator, renderer d2interface.Renderer, audioProvider d2interface.AudioProvider, + ui *d2ui.UIManager, connectionType d2clientconnectiontype.ClientConnectionType, connectionHost string, ) *SelectHeroClass { @@ -308,6 +310,7 @@ func CreateSelectHeroClass( audioProvider: audioProvider, renderer: renderer, navigator: navigator, + uiManager: ui, } return result @@ -318,7 +321,7 @@ func (v *SelectHeroClass) OnLoad(loading d2screen.LoadingState) { v.audioProvider.PlayBGM(d2resource.BGMTitle) loading.Progress(tenPercent) - v.bgImage = loadSprite( + v.bgImage = v.loadSprite( d2resource.CharacterSelectBackground, point(0, 0), 0, @@ -332,7 +335,7 @@ func (v *SelectHeroClass) OnLoad(loading d2screen.LoadingState) { loading.Progress(fourtyPercent) v.createButtons() - v.campfire = loadSprite( + v.campfire = v.loadSprite( d2resource.CharacterSelectCampfire, point(campfirePosX, campfirePosY), 0, @@ -340,12 +343,12 @@ func (v *SelectHeroClass) OnLoad(loading d2screen.LoadingState) { true, ) - v.createCheckboxes(v.renderer) + v.createCheckboxes() loading.Progress(fiftyPercent) for hero, config := range getHeroRenderConfiguration() { position := config.position - forwardWalkOverlaySprite := loadSprite( + forwardWalkOverlaySprite := v.loadSprite( config.forwardWalkOverlayAnimationPath, position, config.forwardWalkPlayLengthMs, @@ -353,24 +356,32 @@ func (v *SelectHeroClass) OnLoad(loading d2screen.LoadingState) { config.forwardWalkOverlayBlend, ) v.heroRenderInfo[hero] = &HeroRenderInfo{ - Stance: d2enum.HeroStanceIdle, - IdleSprite: loadSprite(config.idleAnimationPath, position, config.idlePlayLengthMs, true, false), - IdleSelectedSprite: loadSprite(config.idleSelectedAnimationPath, position, config.idlePlayLengthMs, true, false), - ForwardWalkSprite: loadSprite(config.forwardWalkAnimationPath, position, config.forwardWalkPlayLengthMs, false, false), + Stance: d2enum.HeroStanceIdle, + IdleSprite: v.loadSprite(config.idleAnimationPath, position, + config.idlePlayLengthMs, true, false), + IdleSelectedSprite: v.loadSprite(config.idleSelectedAnimationPath, + position, + config.idlePlayLengthMs, true, false), + ForwardWalkSprite: v.loadSprite(config.forwardWalkAnimationPath, position, + config.forwardWalkPlayLengthMs, false, false), ForwardWalkSpriteOverlay: forwardWalkOverlaySprite, - SelectedSprite: loadSprite(config.selectedAnimationPath, position, config.idlePlayLengthMs, true, false), - SelectedSpriteOverlay: loadSprite(config.selectedOverlayAnimationPath, position, config.idlePlayLengthMs, true, true), - BackWalkSprite: loadSprite(config.backWalkAnimationPath, position, config.backWalkPlayLengthMs, false, false), - BackWalkSpriteOverlay: loadSprite(config.backWalkOverlayAnimationPath, position, config.backWalkPlayLengthMs, false, true), - SelectionBounds: config.selectionBounds, - SelectSfx: v.loadSoundEffect(config.selectSfx), - DeselectSfx: v.loadSoundEffect(config.deselectSfx), + SelectedSprite: v.loadSprite(config.selectedAnimationPath, position, + config.idlePlayLengthMs, true, false), + SelectedSpriteOverlay: v.loadSprite(config.selectedOverlayAnimationPath, position, + config.idlePlayLengthMs, true, true), + BackWalkSprite: v.loadSprite(config.backWalkAnimationPath, position, + config.backWalkPlayLengthMs, false, false), + BackWalkSpriteOverlay: v.loadSprite(config.backWalkOverlayAnimationPath, position, + config.backWalkPlayLengthMs, false, true), + SelectionBounds: config.selectionBounds, + SelectSfx: v.loadSoundEffect(config.selectSfx), + DeselectSfx: v.loadSoundEffect(config.deselectSfx), } } } func (v *SelectHeroClass) createLabels() { - v.headingLabel = d2ui.CreateLabel(d2resource.Font30, d2resource.PaletteUnits) + v.headingLabel = v.uiManager.NewLabel(d2resource.Font30, d2resource.PaletteUnits) fontWidth, _ := v.headingLabel.GetSize() half := 2 halfFontWidth := fontWidth / half @@ -379,67 +390,62 @@ func (v *SelectHeroClass) createLabels() { v.headingLabel.SetText("Select Hero Class") v.headingLabel.Alignment = d2gui.HorizontalAlignCenter - v.heroClassLabel = d2ui.CreateLabel(d2resource.Font30, d2resource.PaletteUnits) + v.heroClassLabel = v.uiManager.NewLabel(d2resource.Font30, d2resource.PaletteUnits) v.heroClassLabel.Alignment = d2gui.HorizontalAlignCenter v.heroClassLabel.SetPosition(heroClassLabelX, heroClassLabelY) - v.heroDesc1Label = d2ui.CreateLabel(d2resource.Font16, d2resource.PaletteUnits) + v.heroDesc1Label = v.uiManager.NewLabel(d2resource.Font16, d2resource.PaletteUnits) v.heroDesc1Label.Alignment = d2gui.HorizontalAlignCenter v.heroDesc1Label.SetPosition(heroDescLine1X, heroDescLine1Y) - v.heroDesc2Label = d2ui.CreateLabel(d2resource.Font16, d2resource.PaletteUnits) + v.heroDesc2Label = v.uiManager.NewLabel(d2resource.Font16, d2resource.PaletteUnits) v.heroDesc2Label.Alignment = d2gui.HorizontalAlignCenter v.heroDesc2Label.SetPosition(heroDescLine2X, heroDescLine2Y) - v.heroDesc3Label = d2ui.CreateLabel(d2resource.Font16, d2resource.PaletteUnits) + v.heroDesc3Label = v.uiManager.NewLabel(d2resource.Font16, d2resource.PaletteUnits) v.heroDesc3Label.Alignment = d2gui.HorizontalAlignCenter v.heroDesc3Label.SetPosition(heroDescLine3X, heroDescLine3Y) - v.heroNameLabel = d2ui.CreateLabel(d2resource.Font16, d2resource.PaletteUnits) + v.heroNameLabel = v.uiManager.NewLabel(d2resource.Font16, d2resource.PaletteUnits) v.heroNameLabel.Alignment = d2gui.HorizontalAlignLeft v.heroNameLabel.SetText(d2ui.ColorTokenize("Character Name", d2ui.ColorTokenGold)) v.heroNameLabel.SetPosition(heroNameLabelX, heroNameLabelY) - v.expansionCharLabel = d2ui.CreateLabel(d2resource.Font16, d2resource.PaletteUnits) + v.expansionCharLabel = v.uiManager.NewLabel(d2resource.Font16, d2resource.PaletteUnits) v.expansionCharLabel.Alignment = d2gui.HorizontalAlignLeft v.expansionCharLabel.SetText(d2ui.ColorTokenize("EXPANSION CHARACTER", d2ui.ColorTokenGold)) v.expansionCharLabel.SetPosition(expansionLabelX, expansionLabelY) - v.hardcoreCharLabel = d2ui.CreateLabel(d2resource.Font16, d2resource.PaletteUnits) + v.hardcoreCharLabel = v.uiManager.NewLabel(d2resource.Font16, d2resource.PaletteUnits) v.hardcoreCharLabel.Alignment = d2gui.HorizontalAlignLeft v.hardcoreCharLabel.SetText(d2ui.ColorTokenize("Hardcore", d2ui.ColorTokenGold)) v.hardcoreCharLabel.SetPosition(hardcoreLabelX, hardcoreLabelY) } func (v *SelectHeroClass) createButtons() { - v.exitButton = d2ui.CreateButton(v.renderer, d2ui.ButtonTypeMedium, "EXIT") + v.exitButton = v.uiManager.NewButton(d2ui.ButtonTypeMedium, "EXIT") v.exitButton.SetPosition(selHeroExitBtnX, selHeroExitBtnY) v.exitButton.OnActivated(func() { v.onExitButtonClicked() }) - d2ui.AddWidget(&v.exitButton) - v.okButton = d2ui.CreateButton(v.renderer, d2ui.ButtonTypeMedium, "OK") + v.okButton = v.uiManager.NewButton(d2ui.ButtonTypeMedium, "OK") v.okButton.SetPosition(selHeroOkBtnX, selHeroOkBtnY) v.okButton.OnActivated(func() { v.onOkButtonClicked() }) v.okButton.SetVisible(false) v.okButton.SetEnabled(false) - d2ui.AddWidget(&v.okButton) } -func (v *SelectHeroClass) createCheckboxes(renderer d2interface.Renderer) { - v.heroNameTextbox = d2ui.CreateTextbox() +func (v *SelectHeroClass) createCheckboxes() { + v.heroNameTextbox = v.uiManager.NewTextbox() v.heroNameTextbox.SetPosition(heroNameTextBoxX, heoNameTextBoxY) v.heroNameTextbox.SetVisible(false) - d2ui.AddWidget(&v.heroNameTextbox) - v.expansionCheckbox = d2ui.CreateCheckbox(v.renderer, true) + v.expansionCheckbox = v.uiManager.NewCheckbox(true) v.expansionCheckbox.SetPosition(expandsionCheckboxX, expansionCheckboxY) v.expansionCheckbox.SetVisible(false) - d2ui.AddWidget(&v.expansionCheckbox) - v.hardcoreCheckbox = d2ui.CreateCheckbox(renderer, false) + v.hardcoreCheckbox = v.uiManager.NewCheckbox(false) v.hardcoreCheckbox.SetPosition(hardcoreCheckoxX, hardcoreCheckboxY) v.hardcoreCheckbox.SetVisible(false) - d2ui.AddWidget(&v.hardcoreCheckbox) } // OnUnload releases the resources of the Select Hero Class screen @@ -559,11 +565,11 @@ func (v *SelectHeroClass) updateHeroSelectionHover(hero d2enum.Hero, canSelect b return } - mouseX, mouseY := d2ui.CursorPosition() + mouseX, mouseY := v.uiManager.CursorPosition() b := renderInfo.SelectionBounds mouseHover := (mouseX >= b.Min.X) && (mouseX <= b.Min.X+b.Max.X) && (mouseY >= b.Min.Y) && (mouseY <= b.Min.Y+b.Max.Y) - if mouseHover && d2ui.CursorButtonPressed(d2ui.CursorButtonLeft) { + if mouseHover && v.uiManager.CursorButtonPressed(d2ui.CursorButtonLeft) { v.handleCursorButtonPress(hero, renderInfo) return } @@ -717,7 +723,9 @@ func advanceSprite(sprite *d2ui.Sprite, elapsed float64) { } } -func loadSprite(animationPath string, position image.Point, playLength int, playLoop, +func (v *SelectHeroClass) loadSprite(animationPath string, position image.Point, + playLength int, + playLoop, blend bool) *d2ui.Sprite { if animationPath == "" { return nil @@ -740,7 +748,7 @@ func loadSprite(animationPath string, position image.Point, playLength int, play animation.SetPlayLengthMs(playLength) } - sprite, err := d2ui.LoadSprite(animation) + sprite, err := v.uiManager.NewSprite(animation) if err != nil { fmt.Printf("could not load sprite for the animation: %s\n", animationPath) return nil diff --git a/d2game/d2player/game_controls.go b/d2game/d2player/game_controls.go index ca3f803d..4d149fdb 100644 --- a/d2game/d2player/game_controls.go +++ b/d2game/d2player/game_controls.go @@ -46,6 +46,7 @@ type GameControls struct { hero *d2mapentity.Player mapEngine *d2mapengine.MapEngine mapRenderer *d2maprenderer.MapRenderer + uiManager *d2ui.UIManager inventory *Inventory heroStatsPanel *HeroStatsPanel inputListener InputCallbackListener @@ -62,7 +63,7 @@ type GameControls struct { skillIcon *d2ui.Sprite zoneChangeText *d2ui.Label nameLabel *d2ui.Label - runButton d2ui.Button + runButton *d2ui.Button isZoneTextShown bool actionableRegions []ActionableRegion } @@ -86,17 +87,24 @@ const ( rightSkill = ActionableType(iota) ) -func NewGameControls(renderer d2interface.Renderer, hero *d2mapentity.Player, mapEngine *d2mapengine.MapEngine, - mapRenderer *d2maprenderer.MapRenderer, inputListener InputCallbackListener, term d2interface.Terminal) (*GameControls, error) { +func NewGameControls( + renderer d2interface.Renderer, + hero *d2mapentity.Player, + mapEngine *d2mapengine.MapEngine, + mapRenderer *d2maprenderer.MapRenderer, + inputListener InputCallbackListener, + term d2interface.Terminal, + ui *d2ui.UIManager, +) (*GameControls, error) { missileID := initialMissileID term.BindAction("setmissile", "set missile id to summon on right click", func(id int) { missileID = id }) - zoneLabel := d2ui.CreateLabel(d2resource.Font30, d2resource.PaletteUnits) + zoneLabel := ui.NewLabel(d2resource.Font30, d2resource.PaletteUnits) zoneLabel.Alignment = d2gui.HorizontalAlignCenter - nameLabel := d2ui.CreateLabel(d2resource.FontFormal11, d2resource.PaletteStatic) + nameLabel := ui.NewLabel(d2resource.FontFormal11, d2resource.PaletteStatic) nameLabel.Alignment = d2gui.HorizontalAlignCenter nameLabel.SetText(d2ui.ColorTokenize("", d2ui.ColorTokenServer)) @@ -124,20 +132,21 @@ func NewGameControls(renderer d2interface.Renderer, hero *d2mapentity.Player, ma inventoryRecord := d2datadict.Inventory[inventoryRecordKey] - hoverLabel := &nameLabel - hoverLabel.SetBackgroundColor(color.RGBA{0,0,0, uint8(128)}) + hoverLabel := nameLabel + hoverLabel.SetBackgroundColor(color.RGBA{0, 0, 0, uint8(128)}) gc := &GameControls{ + uiManager: ui, renderer: renderer, hero: hero, mapEngine: mapEngine, inputListener: inputListener, mapRenderer: mapRenderer, - inventory: NewInventory(inventoryRecord), - heroStatsPanel: NewHeroStatsPanel(renderer, hero.Name(), hero.Class, hero.Stats), + inventory: NewInventory(ui, inventoryRecord), + heroStatsPanel: NewHeroStatsPanel(ui, hero.Name(), hero.Class, hero.Stats), missileID: missileID, nameLabel: hoverLabel, - zoneChangeText: &zoneLabel, + zoneChangeText: zoneLabel, actionableRegions: []ActionableRegion{ {leftSkill, d2common.Rectangle{Left: 115, Top: 550, Width: 50, Height: 50}}, {leftSelec, d2common.Rectangle{Left: 206, Top: 563, Width: 30, Height: 30}}, @@ -328,19 +337,19 @@ func (g *GameControls) OnMouseButtonDown(event d2interface.MouseEvent) bool { func (g *GameControls) Load() { animation, _ := d2asset.LoadAnimation(d2resource.GameGlobeOverlap, d2resource.PaletteSky) - g.globeSprite, _ = d2ui.LoadSprite(animation) + g.globeSprite, _ = g.uiManager.NewSprite(animation) animation, _ = d2asset.LoadAnimation(d2resource.HealthManaIndicator, d2resource.PaletteSky) - g.hpManaStatusSprite, _ = d2ui.LoadSprite(animation) + g.hpManaStatusSprite, _ = g.uiManager.NewSprite(animation) animation, _ = d2asset.LoadAnimation(d2resource.GamePanels, d2resource.PaletteSky) - g.mainPanel, _ = d2ui.LoadSprite(animation) + g.mainPanel, _ = g.uiManager.NewSprite(animation) animation, _ = d2asset.LoadAnimation(d2resource.MenuButton, d2resource.PaletteSky) - g.menuButton, _ = d2ui.LoadSprite(animation) + g.menuButton, _ = g.uiManager.NewSprite(animation) animation, _ = d2asset.LoadAnimation(d2resource.GenericSkills, d2resource.PaletteSky) - g.skillIcon, _ = d2ui.LoadSprite(animation) + g.skillIcon, _ = g.uiManager.NewSprite(animation) g.loadUIButtons() @@ -350,7 +359,7 @@ func (g *GameControls) Load() { func (g *GameControls) loadUIButtons() { // Run button - g.runButton = d2ui.CreateButton(g.renderer, d2ui.ButtonTypeRun, "") + g.runButton = g.uiManager.NewButton(d2ui.ButtonTypeRun, "") g.runButton.SetPosition(255, 570) g.runButton.OnActivated(func() { g.onToggleRunButton() }) @@ -358,8 +367,6 @@ func (g *GameControls) loadUIButtons() { if g.hero.IsRunToggled() { g.runButton.Toggle() } - - d2ui.AddWidget(&g.runButton) } func (g *GameControls) onToggleRunButton() { diff --git a/d2game/d2player/hero_stats_panel.go b/d2game/d2player/hero_stats_panel.go index e2d0d221..83a1f2d7 100644 --- a/d2game/d2player/hero_stats_panel.go +++ b/d2game/d2player/hero_stats_panel.go @@ -24,19 +24,19 @@ type PanelText struct { // StatsPanelLabels represents the labels in the status panel type StatsPanelLabels struct { - Level d2ui.Label - Experience d2ui.Label - NextLevelExp d2ui.Label - Strength d2ui.Label - Dexterity d2ui.Label - Vitality d2ui.Label - Energy d2ui.Label - Health d2ui.Label - MaxHealth d2ui.Label - Mana d2ui.Label - MaxMana d2ui.Label - MaxStamina d2ui.Label - Stamina d2ui.Label + Level *d2ui.Label + Experience *d2ui.Label + NextLevelExp *d2ui.Label + Strength *d2ui.Label + Dexterity *d2ui.Label + Vitality *d2ui.Label + Energy *d2ui.Label + Health *d2ui.Label + MaxHealth *d2ui.Label + Mana *d2ui.Label + MaxMana *d2ui.Label + MaxStamina *d2ui.Label + Stamina *d2ui.Label } // stores all the labels that can change during gameplay(e.g. current level, current hp, mana, etc.) @@ -44,6 +44,7 @@ var StatValueLabels = make([]d2ui.Label, 13) // HeroStatsPanel represents the hero status panel type HeroStatsPanel struct { + uiManager *d2ui.UIManager frame *d2ui.Sprite panel *d2ui.Sprite heroState *d2hero.HeroStatsState @@ -59,13 +60,14 @@ type HeroStatsPanel struct { } // NewHeroStatsPanel creates a new hero status panel -func NewHeroStatsPanel(renderer d2interface.Renderer, heroName string, heroClass d2enum.Hero, +func NewHeroStatsPanel(ui *d2ui.UIManager, heroName string, heroClass d2enum.Hero, heroState *d2hero.HeroStatsState) *HeroStatsPanel { originX := 0 originY := 0 return &HeroStatsPanel{ - renderer: renderer, + uiManager: ui, + renderer: ui.Renderer(), originX: originX, originY: originY, heroState: heroState, @@ -78,9 +80,9 @@ func NewHeroStatsPanel(renderer d2interface.Renderer, heroName string, heroClass // Load loads the data for the hero status panel func (s *HeroStatsPanel) Load() { animation, _ := d2asset.LoadAnimation(d2resource.Frame, d2resource.PaletteSky) - s.frame, _ = d2ui.LoadSprite(animation) + s.frame, _ = s.uiManager.NewSprite(animation) animation, _ = d2asset.LoadAnimation(d2resource.InventoryCharacterPanel, d2resource.PaletteSky) - s.panel, _ = d2ui.LoadSprite(animation) + s.panel, _ = s.uiManager.NewSprite(animation) s.initStatValueLabels() } @@ -273,7 +275,7 @@ func (s *HeroStatsPanel) renderStaticMenu(target d2interface.Surface) error { return err } - var label d2ui.Label + var label *d2ui.Label // all static labels are not stored since we use them only once to generate the image cache @@ -360,23 +362,25 @@ func (s *HeroStatsPanel) renderStatValues(target d2interface.Surface) { s.renderStatValueNum(s.labels.Mana, s.heroState.Mana, target) } -func (s *HeroStatsPanel) renderStatValueNum(label d2ui.Label, value int, target d2interface.Surface) { +func (s *HeroStatsPanel) renderStatValueNum(label *d2ui.Label, value int, + target d2interface.Surface) { label.SetText(strconv.Itoa(value)) label.Render(target) } -func (s *HeroStatsPanel) createStatValueLabel(stat int, x int, y int) d2ui.Label { +func (s *HeroStatsPanel) createStatValueLabel(stat int, x int, y int) *d2ui.Label { text := strconv.Itoa(stat) return s.createTextLabel(PanelText{X: x, Y: y, Text: text, Font: d2resource.Font16, AlignCenter: true}) } -func (s *HeroStatsPanel) createTextLabel(element PanelText) d2ui.Label { - label := d2ui.CreateLabel(element.Font, d2resource.PaletteStatic) +func (s *HeroStatsPanel) createTextLabel(element PanelText) *d2ui.Label { + label := s.uiManager.NewLabel(element.Font, d2resource.PaletteStatic) if element.AlignCenter { label.Alignment = d2gui.HorizontalAlignCenter } label.SetText(element.Text) label.SetPosition(element.X, element.Y) + return label } diff --git a/d2game/d2player/inventory.go b/d2game/d2player/inventory.go index a929394e..74fd244e 100644 --- a/d2game/d2player/inventory.go +++ b/d2game/d2player/inventory.go @@ -14,9 +14,10 @@ import ( ) type Inventory struct { - frame *d2ui.Sprite - panel *d2ui.Sprite - grid *ItemGrid + uiManager *d2ui.UIManager + frame *d2ui.Sprite + panel *d2ui.Sprite + grid *ItemGrid hoverLabel *d2ui.Label hoverX, hoverY int @@ -28,15 +29,16 @@ type Inventory struct { isOpen bool } -func NewInventory(record *d2datadict.InventoryRecord) *Inventory { +func NewInventory(ui *d2ui.UIManager, record *d2datadict.InventoryRecord) *Inventory { - hoverLabel := d2ui.CreateLabel(d2resource.FontFormal11, d2resource.PaletteStatic) + hoverLabel := ui.NewLabel(d2resource.FontFormal11, d2resource.PaletteStatic) hoverLabel.Alignment = d2gui.HorizontalAlignCenter return &Inventory{ - grid: NewItemGrid(record), + uiManager: ui, + grid: NewItemGrid(ui, record), originX: record.Panel.Left, - hoverLabel: &hoverLabel, + hoverLabel: hoverLabel, // originY: record.Panel.Top, originY: 0, // expansion data has these all offset by +60 ... } @@ -60,10 +62,10 @@ func (g *Inventory) Close() { func (g *Inventory) Load() { animation, _ := d2asset.LoadAnimation(d2resource.Frame, d2resource.PaletteSky) - g.frame, _ = d2ui.LoadSprite(animation) + g.frame, _ = g.uiManager.NewSprite(animation) animation, _ = d2asset.LoadAnimation(d2resource.InventoryCharacterPanel, d2resource.PaletteSky) - g.panel, _ = d2ui.LoadSprite(animation) + g.panel, _ = g.uiManager.NewSprite(animation) items := []InventoryItem{ diablo2item.NewItem("kit", "Crimson", "of the Bat", "of Frost").Identify(), diablo2item.NewItem("rin", "Steel", "of Shock").Identify(), @@ -246,6 +248,7 @@ func (g *Inventory) Render(target d2interface.Surface) error { } g.renderItemDescription(target, item) + break } } @@ -272,7 +275,7 @@ func (g *Inventory) renderItemDescription(target d2interface.Surface, i Inventor } halfW, halfH := maxW/2, maxH/2 - centerX, centerY := g.hoverX, iy - halfH + centerX, centerY := g.hoverX, iy-halfH if (centerX + halfW) > 800 { centerX = 800 - halfW diff --git a/d2game/d2player/inventory_grid.go b/d2game/d2player/inventory_grid.go index fff92c63..1d4c2f10 100644 --- a/d2game/d2player/inventory_grid.go +++ b/d2game/d2player/inventory_grid.go @@ -33,6 +33,7 @@ var ErrorInventoryFull = errors.New("inventory full") // Reusable grid for use with player and merchant inventory. // Handles layout and rendering item icons based on code. type ItemGrid struct { + uiManager *d2ui.UIManager items []InventoryItem equipmentSlots map[d2enum.EquippedSlot]EquipmentSlot width int @@ -43,10 +44,11 @@ type ItemGrid struct { slotSize int } -func NewItemGrid(record *d2datadict.InventoryRecord) *ItemGrid { +func NewItemGrid(ui *d2ui.UIManager, record *d2datadict.InventoryRecord) *ItemGrid { grid := record.Grid return &ItemGrid{ + uiManager: ui, width: grid.Box.Width, height: grid.Box.Height, originX: grid.Box.Left, @@ -126,7 +128,7 @@ func (g *ItemGrid) loadItem(item InventoryItem) { return } - itemSprite, err = d2ui.LoadSprite(animation) + itemSprite, err = g.uiManager.NewSprite(animation) if err != nil { log.Printf("Failed to load sprite, error: " + err.Error()) }