From 12821147ceb2719968c43ee7e9b2f8c71b58789f Mon Sep 17 00:00:00 2001 From: juander-ux <73304484+juander-ux@users.noreply.github.com> Date: Fri, 13 Nov 2020 21:08:43 +0100 Subject: [PATCH] Ui panel refactor part 2 (#921) * d2ui/skilltree: Don't render availSPLabel this is handled by the ui_manager now. * d2ui/custom_widget: Allow them to be cached into static images * d2player/hero_stats_panel: Remove render() function from game_controls all ui elements are now grouped into a WidgetGroup, thus rendering is done by the ui manager. * d2player/hero_stats_panel: Remove unnecessary widgets from struct we don't need to store them in the HeroStatsPanel struct anymore as they are completly handled by the uiManager. * d2ui/widget_group: Remove priority member this is already defined by the BaseWidget. * d2ui/widget: Move uiManager.contains() to the baseWidgets this method makes more sense on a widget anyways. * d2ui/widget: Add methods to handle widget hovering * d2ui/custom_widget: Require define width/height since the custom render() method can do whatever, we need the user to specify the width/height such that GetSize() calls are meaningful. * d2ui/widget: Allow widgets to return the uiManager * d2player/HUD: Refactor health/mana globe into its own widget * d2player/hud: Refactor load() seperate each type of loading into its own method. * d2player/HUD: Move stamina/exp bar into widgets * d2player/HUD: Refactor left/right skills into widget * d2ui/custom_widget: cached custom widgets should use widget.x/y since we render to an image, we use widget.x/y to position the cached image. * d2player/HUD: User cached custom widget for all static images --- d2core/d2ui/custom_widget.go | 28 ++- d2core/d2ui/ui_manager.go | 26 +-- d2core/d2ui/widget.go | 56 ++++++ d2core/d2ui/widget_group.go | 16 +- d2game/d2player/game_controls.go | 5 +- d2game/d2player/globeWidget.go | 145 ++++++++++++++ d2game/d2player/hero_stats_panel.go | 120 +++++------- d2game/d2player/hud.go | 288 ++++++++++++---------------- d2game/d2player/skilltree.go | 5 +- 9 files changed, 433 insertions(+), 256 deletions(-) create mode 100644 d2game/d2player/globeWidget.go diff --git a/d2core/d2ui/custom_widget.go b/d2core/d2ui/custom_widget.go index c5b9fa70..f0b19b7b 100644 --- a/d2core/d2ui/custom_widget.go +++ b/d2core/d2ui/custom_widget.go @@ -9,11 +9,29 @@ var _ Widget = &CustomWidget{} type CustomWidget struct { *BaseWidget renderFunc func(target d2interface.Surface) + cached bool + cachedImg *d2interface.Surface +} + +// NewCustomWidgetCached creates a new widget and caches anything rendered via the +// renderFunc into a static image to be displayed +func (ui *UIManager) NewCustomWidgetCached(renderFunc func(target d2interface.Surface), width, height int) *CustomWidget { + c := ui.NewCustomWidget(renderFunc, width, height) + c.cached = true + + // render using the renderFunc to a cache + surface := ui.Renderer().NewSurface(width, height) + c.cachedImg = &surface + renderFunc(*c.cachedImg) + + return c } // NewCustomWidget creates a new widget with custom render function -func (ui *UIManager) NewCustomWidget(renderFunc func(target d2interface.Surface)) *CustomWidget { +func (ui *UIManager) NewCustomWidget(renderFunc func(target d2interface.Surface), width, height int) *CustomWidget { base := NewBaseWidget(ui) + base.width = width + base.height = height return &CustomWidget{ BaseWidget: base, @@ -23,7 +41,13 @@ func (ui *UIManager) NewCustomWidget(renderFunc func(target d2interface.Surface) // Render draws the custom widget func (c *CustomWidget) Render(target d2interface.Surface) { - c.renderFunc(target) + if c.cached { + target.PushTranslation(c.GetPosition()) + target.Render(*c.cachedImg) + target.Pop() + } else { + c.renderFunc(target) + } } // Advance is a no-op diff --git a/d2core/d2ui/ui_manager.go b/d2core/d2ui/ui_manager.go index f6f95edb..e2aa4ed8 100644 --- a/d2core/d2ui/ui_manager.go +++ b/d2core/d2ui/ui_manager.go @@ -54,8 +54,9 @@ func (ui *UIManager) Reset() { // addWidgetGroup adds a widgetGroup to the UI manager and sorts by priority func (ui *UIManager) addWidgetGroup(group *WidgetGroup) { ui.widgetsGroups = append(ui.widgetsGroups, group) + sort.SliceStable(ui.widgetsGroups, func(i, j int) bool { - return ui.widgetsGroups[i].priority < ui.widgetsGroups[j].priority + return ui.widgetsGroups[i].renderPriority < ui.widgetsGroups[j].renderPriority }) } @@ -83,7 +84,7 @@ func (ui *UIManager) OnMouseButtonUp(event d2interface.MouseEvent) bool { // 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() { + if w != nil && w.Contains(ui.CursorX, ui.CursorY) && w.GetVisible() && w.GetEnabled() { w.Activate() } @@ -96,6 +97,17 @@ func (ui *UIManager) OnMouseButtonUp(event d2interface.MouseEvent) bool { return false } +// OnMouseMove is the mouse move event handler +func (ui *UIManager) OnMouseMove(event d2interface.MouseMoveEvent) bool { + for _, w := range ui.widgetsGroups { + if w.GetVisible() { + w.OnMouseMove(event.X(), event.Y()) + } + } + + 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() @@ -103,7 +115,7 @@ func (ui *UIManager) OnMouseButtonDown(event d2interface.MouseEvent) bool { // find and press a widget on screen ui.pressedWidget = nil for _, w := range ui.clickableWidgets { - if ui.contains(w, ui.CursorX, ui.CursorY) && w.GetVisible() && w.GetEnabled() { + if w.Contains(ui.CursorX, ui.CursorY) && w.GetVisible() && w.GetEnabled() { w.SetPressed(true) ui.pressedWidget = w ui.clickSfx.Play() @@ -135,14 +147,6 @@ func (ui *UIManager) Render(target d2interface.Surface) { } } -// 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 { diff --git a/d2core/d2ui/widget.go b/d2core/d2ui/widget.go index d53fa359..27aaf05f 100644 --- a/d2core/d2ui/widget.go +++ b/d2core/d2ui/widget.go @@ -11,6 +11,8 @@ const ( RenderPrioritySkilltree // RenderPrioritySkilltreeIcon is the priority for the skilltree icons RenderPrioritySkilltreeIcon + // RenderPriorityHeroStatsPanel is the priority for the hero_stats_panel + RenderPriorityHeroStatsPanel // RenderPriorityForeground is the last element drawn RenderPriorityForeground ) @@ -19,6 +21,13 @@ const ( type Widget interface { Drawable bindManager(ui *UIManager) + GetManager() (ui *UIManager) + OnHoverStart(callback func()) + OnHoverEnd(callback func()) + isHovered() bool + hoverStart() + hoverEnd() + Contains(x, y int) (contained bool) } // ClickableWidget defines an object that can be clicked @@ -41,6 +50,10 @@ type BaseWidget struct { height int renderPriority RenderPriority visible bool + + hovered bool + onHoverStartCb func() + onHoverEndCb func() } // NewBaseWidget creates a new BaseWidget with defaults @@ -100,3 +113,46 @@ func (b *BaseWidget) GetRenderPriority() (prio RenderPriority) { func (b *BaseWidget) SetRenderPriority(prio RenderPriority) { b.renderPriority = prio } + +// OnHoverStart sets a function that is called if the hovering of the widget starts +func (b *BaseWidget) OnHoverStart(callback func()) { + b.onHoverStartCb = callback +} + +// HoverStart is called when the hovering of the widget starts +func (b *BaseWidget) hoverStart() { + b.hovered = true + if b.onHoverStartCb != nil { + b.onHoverStartCb() + } +} + +// OnHoverEnd sets a function that is called if the hovering of the widget ends +func (b *BaseWidget) OnHoverEnd(callback func()) { + b.onHoverEndCb = callback +} + +// hoverEnd is called when the widget hovering ends +func (b *BaseWidget) hoverEnd() { + b.hovered = false + if b.onHoverEndCb != nil { + b.onHoverEndCb() + } +} + +func (b *BaseWidget) isHovered() bool { + return b.hovered +} + +// Contains determines whether a given x,y coordinate lands within a Widget +func (b *BaseWidget) Contains(x, y int) bool { + wx, wy := b.GetPosition() + ww, wh := b.GetSize() + + return x >= wx && x <= wx+ww && y >= wy && y <= wy+wh +} + +// GetManager returns the uiManager +func (b *BaseWidget) GetManager() (ui *UIManager) { + return b.manager +} diff --git a/d2core/d2ui/widget_group.go b/d2core/d2ui/widget_group.go index 88deb8a7..70424607 100644 --- a/d2core/d2ui/widget_group.go +++ b/d2core/d2ui/widget_group.go @@ -13,8 +13,7 @@ var _ Widget = &WidgetGroup{} // widgets at once. type WidgetGroup struct { *BaseWidget - entries []Widget - priority RenderPriority + entries []Widget } // NewWidgetGroup creates a new widget group @@ -85,3 +84,16 @@ func (wg *WidgetGroup) SetVisible(visible bool) { entry.SetVisible(visible) } } + +// OnMouseMove handles mouse move events +func (wg *WidgetGroup) OnMouseMove(x, y int) { + for _, entry := range wg.entries { + if entry.Contains(x, y) && entry.GetVisible() { + if !entry.isHovered() { + entry.hoverStart() + } + } else if entry.isHovered() { + entry.hoverEnd() + } + } +} diff --git a/d2game/d2player/game_controls.go b/d2game/d2player/game_controls.go index 8b423ba6..c8bc8d21 100644 --- a/d2game/d2player/game_controls.go +++ b/d2game/d2player/game_controls.go @@ -57,7 +57,7 @@ const ( leftSkillX, leftSkillY, leftSkillWidth, - leftSkillHeight = 115, 550, 50, 50 + leftSkillHeight = 117, 550, 50, 50 newStatsX, newStatsY, @@ -92,7 +92,7 @@ const ( rightSkillX, rightSkillY, rightSkillWidth, - rightSkillHeight = 634, 550, 50, 50 + rightSkillHeight = 635, 550, 50, 50 hpGlobeX, hpGlobeY, @@ -783,7 +783,6 @@ func (g *GameControls) Render(target d2interface.Surface) error { } func (g *GameControls) renderPanels(target d2interface.Surface) error { - g.heroStatsPanel.Render(target) g.inventory.Render(target) return nil diff --git a/d2game/d2player/globeWidget.go b/d2game/d2player/globeWidget.go new file mode 100644 index 00000000..16d31293 --- /dev/null +++ b/d2game/d2player/globeWidget.go @@ -0,0 +1,145 @@ +package d2player + +import ( + "image" + "log" + + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource" + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2ui" +) + +type globeType = int + +const ( + typeHealthGlobe globeType = iota + typeManaGlobe +) + +const ( + globeHeight = 80 + globeWidth = 80 + + globeSpriteOffsetX = 28 + globeSpriteOffsetY = -5 + + healthStatusOffsetX = 30 + healthStatusOffsetY = -13 + + manaStatusOffsetX = 7 + manaStatusOffsetY = -12 + + manaGlobeScreenOffsetX = 117 +) + +// static check that globeWidget implements Widget +var _ d2ui.Widget = &globeWidget{} + +type globeFrame struct { + sprite *d2ui.Sprite + offsetX int + offsetY int + idx int +} + +func (gf *globeFrame) setFrameIndex() { + if err := gf.sprite.SetCurrentFrame(gf.idx); err != nil { + log.Print(err) + } +} + +func (gf *globeFrame) setPosition(x, y int) { + gf.sprite.SetPosition(x+gf.offsetX, y+gf.offsetY) +} + +func (gf *globeFrame) getSize() (x, y int) { + w, h := gf.sprite.GetSize() + return w + gf.offsetX, h + gf.offsetY +} + +type globeWidget struct { + *d2ui.BaseWidget + value *int + valueMax *int + globe *globeFrame + overlap *globeFrame +} + +func newGlobeWidget(ui *d2ui.UIManager, x, y int, gtype globeType, value, valueMax *int) *globeWidget { + var globe, overlap *globeFrame + + base := d2ui.NewBaseWidget(ui) + base.SetPosition(x, y) + + if gtype == typeHealthGlobe { + globe = &globeFrame{ + offsetX: healthStatusOffsetX, + offsetY: healthStatusOffsetY, + idx: frameHealthStatus, + } + overlap = &globeFrame{ + offsetX: globeSpriteOffsetX, + offsetY: globeSpriteOffsetY, + idx: frameHealthStatus, + } + } else if gtype == typeManaGlobe { + globe = &globeFrame{ + offsetX: manaStatusOffsetX, + offsetY: manaStatusOffsetY, + idx: frameManaStatus, + } + overlap = &globeFrame{ + offsetX: rightGlobeOffsetX, + offsetY: rightGlobeOffsetY, + idx: frameRightGlobe, + } + } + + return &globeWidget{ + BaseWidget: base, + value: value, + valueMax: valueMax, + globe: globe, + overlap: overlap, + } +} + +func (g *globeWidget) load() { + var err error + + g.globe.sprite, err = g.GetManager().NewSprite(d2resource.HealthManaIndicator, d2resource.PaletteSky) + if err != nil { + log.Print(err) + } + + g.globe.setFrameIndex() + + g.overlap.sprite, err = g.GetManager().NewSprite(d2resource.GameGlobeOverlap, d2resource.PaletteSky) + if err != nil { + log.Print(err) + } + + g.overlap.setFrameIndex() +} + +// Render draws the widget to the screen +func (g *globeWidget) Render(target d2interface.Surface) { + valuePercent := float64(*g.value) / float64(*g.valueMax) + barHeight := int(valuePercent * float64(globeHeight)) + + maskRect := image.Rect(0, globeHeight-barHeight, globeWidth, globeHeight) + + g.globe.setPosition(g.GetPosition()) + g.globe.sprite.RenderSection(target, maskRect) + + g.overlap.setPosition(g.GetPosition()) + g.overlap.sprite.Render(target) +} + +func (g *globeWidget) GetSize() (x, y int) { + return g.overlap.getSize() +} + +func (g *globeWidget) Advance(elapsed float64) error { + return nil +} diff --git a/d2game/d2player/hero_stats_panel.go b/d2game/d2player/hero_stats_panel.go index 3790fafa..303aeccd 100644 --- a/d2game/d2player/hero_stats_panel.go +++ b/d2game/d2player/hero_stats_panel.go @@ -84,18 +84,15 @@ type StatsPanelLabels struct { // HeroStatsPanel represents the hero status panel type HeroStatsPanel struct { - asset *d2asset.AssetManager - uiManager *d2ui.UIManager - frame *d2ui.UIFrame - panel *d2ui.Sprite - heroState *d2hero.HeroStatsState - heroName string - heroClass d2enum.Hero - renderer d2interface.Renderer - staticMenuImageCache *d2interface.Surface - labels *StatsPanelLabels - closeButton *d2ui.Button - onCloseCb func() + asset *d2asset.AssetManager + uiManager *d2ui.UIManager + panel *d2ui.Sprite + heroState *d2hero.HeroStatsState + heroName string + heroClass d2enum.Hero + labels *StatsPanelLabels + onCloseCb func() + panelGroup *d2ui.WidgetGroup originX int originY int @@ -111,7 +108,6 @@ func NewHeroStatsPanel(asset *d2asset.AssetManager, ui *d2ui.UIManager, heroName return &HeroStatsPanel{ asset: asset, uiManager: ui, - renderer: ui.Renderer(), originX: originX, originY: originY, heroState: heroState, @@ -125,19 +121,30 @@ func NewHeroStatsPanel(asset *d2asset.AssetManager, ui *d2ui.UIManager, heroName func (s *HeroStatsPanel) Load() { var err error - s.frame = d2ui.NewUIFrame(s.asset, s.uiManager, d2ui.FrameLeft) + s.panelGroup = s.uiManager.NewWidgetGroup(d2ui.RenderPriorityHeroStatsPanel) - s.closeButton = s.uiManager.NewButton(d2ui.ButtonTypeSquareClose, "") - s.closeButton.SetVisible(false) - s.closeButton.SetPosition(heroStatsCloseButtonX, heroStatsCloseButtonY) - s.closeButton.OnActivated(func() { s.Close() }) + frame := d2ui.NewUIFrame(s.asset, s.uiManager, d2ui.FrameLeft) + s.panelGroup.AddWidget(frame) s.panel, err = s.uiManager.NewSprite(d2resource.InventoryCharacterPanel, d2resource.PaletteSky) if err != nil { log.Print(err) } + fw, fh := frame.GetFrameBounds() + fc := frame.GetFrameCount() + w, h := fw*fc, fh*fc + staticPanel := s.uiManager.NewCustomWidgetCached(s.renderStaticMenu, w, h) + s.panelGroup.AddWidget(staticPanel) + + closeButton := s.uiManager.NewButton(d2ui.ButtonTypeSquareClose, "") + closeButton.SetVisible(false) + closeButton.SetPosition(heroStatsCloseButtonX, heroStatsCloseButtonY) + closeButton.OnActivated(func() { s.Close() }) + s.panelGroup.AddWidget(closeButton) + s.initStatValueLabels() + s.panelGroup.SetVisible(false) } // IsOpen returns true if the hero status panel is open @@ -157,13 +164,13 @@ func (s *HeroStatsPanel) Toggle() { // Open opens the hero status panel func (s *HeroStatsPanel) Open() { s.isOpen = true - s.closeButton.SetVisible(true) + s.panelGroup.SetVisible(true) } // Close closed the hero status panel func (s *HeroStatsPanel) Close() { s.isOpen = false - s.closeButton.SetVisible(false) + s.panelGroup.SetVisible(false) s.onCloseCb() } @@ -172,43 +179,21 @@ func (s *HeroStatsPanel) SetOnCloseCb(cb func()) { s.onCloseCb = cb } -// Render renders the hero status panel -func (s *HeroStatsPanel) Render(target d2interface.Surface) { +// Advance updates labels on the panel +func (s *HeroStatsPanel) Advance(elapsed float64) { if !s.isOpen { return } - if s.staticMenuImageCache == nil { - frameWidth, frameHeight := s.frame.GetFrameBounds() - framesCount := s.frame.GetFrameCount() - surface := s.renderer.NewSurface(frameWidth*framesCount, frameHeight*framesCount) - - s.staticMenuImageCache = &surface - - err := s.renderStaticMenu(*s.staticMenuImageCache) - if err != nil { - log.Println(err) - } - } - - target.Render(*s.staticMenuImageCache) - - s.renderStatValues(target) + s.setStatValues() } -func (s *HeroStatsPanel) renderStaticMenu(target d2interface.Surface) error { - if err := s.renderStaticPanelFrames(target); err != nil { - return err - } - +func (s *HeroStatsPanel) renderStaticMenu(target d2interface.Surface) { + s.renderStaticPanelFrames(target) s.renderStaticLabels(target) - - return nil } -func (s *HeroStatsPanel) renderStaticPanelFrames(target d2interface.Surface) error { - s.frame.Render(target) - +func (s *HeroStatsPanel) renderStaticPanelFrames(target d2interface.Surface) { frames := []int{ statsPanelTopLeft, statsPanelTopRight, @@ -221,7 +206,7 @@ func (s *HeroStatsPanel) renderStaticPanelFrames(target d2interface.Surface) err for _, frameIndex := range frames { if err := s.panel.SetCurrentFrame(frameIndex); err != nil { - return err + log.Printf("%e", err) } w, h := s.panel.GetCurrentFrameSize() @@ -241,8 +226,6 @@ func (s *HeroStatsPanel) renderStaticPanelFrames(target d2interface.Surface) err s.panel.Render(target) } - - return nil } func (s *HeroStatsPanel) renderStaticLabels(target d2interface.Surface) { @@ -322,30 +305,24 @@ func (s *HeroStatsPanel) initStatValueLabels() { } } -func (s *HeroStatsPanel) renderStatValues(target d2interface.Surface) { - s.renderStatValueNum(s.labels.Level, s.heroState.Level, target) - s.renderStatValueNum(s.labels.Experience, s.heroState.Experience, target) - s.renderStatValueNum(s.labels.NextLevelExp, s.heroState.NextLevelExp, target) +func (s *HeroStatsPanel) setStatValues() { + s.labels.Level.SetText(strconv.Itoa(s.heroState.Level)) + s.labels.Experience.SetText(strconv.Itoa(s.heroState.Experience)) + s.labels.NextLevelExp.SetText(strconv.Itoa(s.heroState.NextLevelExp)) - s.renderStatValueNum(s.labels.Strength, s.heroState.Strength, target) - s.renderStatValueNum(s.labels.Dexterity, s.heroState.Dexterity, target) - s.renderStatValueNum(s.labels.Vitality, s.heroState.Vitality, target) - s.renderStatValueNum(s.labels.Energy, s.heroState.Energy, target) + s.labels.Strength.SetText(strconv.Itoa(s.heroState.Strength)) + s.labels.Dexterity.SetText(strconv.Itoa(s.heroState.Dexterity)) + s.labels.Vitality.SetText(strconv.Itoa(s.heroState.Vitality)) + s.labels.Energy.SetText(strconv.Itoa(s.heroState.Energy)) - s.renderStatValueNum(s.labels.MaxHealth, s.heroState.MaxHealth, target) - s.renderStatValueNum(s.labels.Health, s.heroState.Health, target) + s.labels.MaxHealth.SetText(strconv.Itoa(s.heroState.MaxHealth)) + s.labels.Health.SetText(strconv.Itoa(s.heroState.Health)) - s.renderStatValueNum(s.labels.MaxStamina, s.heroState.MaxStamina, target) - s.renderStatValueNum(s.labels.Stamina, int(s.heroState.Stamina), target) + s.labels.MaxStamina.SetText(strconv.Itoa(s.heroState.MaxStamina)) + s.labels.Stamina.SetText(strconv.Itoa(int(s.heroState.Stamina))) - s.renderStatValueNum(s.labels.MaxMana, s.heroState.MaxMana, target) - s.renderStatValueNum(s.labels.Mana, s.heroState.Mana, target) -} - -func (s *HeroStatsPanel) renderStatValueNum(label *d2ui.Label, value int, - target d2interface.Surface) { - label.SetText(strconv.Itoa(value)) - label.Render(target) + s.labels.MaxMana.SetText(strconv.Itoa(s.heroState.MaxMana)) + s.labels.Mana.SetText(strconv.Itoa(s.heroState.Mana)) } func (s *HeroStatsPanel) createStatValueLabel(stat, x, y int) *d2ui.Label { @@ -361,6 +338,7 @@ func (s *HeroStatsPanel) createTextLabel(element PanelText) *d2ui.Label { label.SetText(element.Text) label.SetPosition(element.X, element.Y) + s.panelGroup.AddWidget(label) return label } diff --git a/d2game/d2player/hud.go b/d2game/d2player/hud.go index d50344b9..5ceaf0c4 100644 --- a/d2game/d2player/hud.go +++ b/d2game/d2player/hud.go @@ -2,7 +2,6 @@ package d2player import ( "fmt" - "image" "log" "math" "strings" @@ -30,10 +29,9 @@ const ( const ( expBarWidth = 120.0 + expBarHeight = 4 staminaBarWidth = 102.0 staminaBarHeight = 19.0 - globeHeight = 80 - globeWidth = 80 hoverLabelOuterPad = 5 percentStaminaBarLow = 0.25 ) @@ -61,15 +59,6 @@ const ( ) const ( - manaStatusOffsetX = 7 - manaStatusOffsetY = -12 - - healthStatusOffsetX = 30 - healthStatusOffsetY = -13 - - globeSpriteOffsetX = 28 - globeSpriteOffsetY = -5 - staminaBarOffsetX = 273 staminaBarOffsetY = 572 @@ -120,6 +109,13 @@ type HUD struct { manaTooltip *d2ui.Tooltip miniPanelTooltip *d2ui.Tooltip nameLabel *d2ui.Label + healthGlobe *globeWidget + manaGlobe *globeWidget + widgetStamina *d2ui.CustomWidget + widgetExperience *d2ui.CustomWidget + widgetLeftSkill *d2ui.CustomWidget + widgetRightSkill *d2ui.CustomWidget + panelBackground *d2ui.CustomWidget } // NewHUD creates a HUD object @@ -140,6 +136,9 @@ func NewHUD( zoneLabel := ui.NewLabel(d2resource.Font30, d2resource.PaletteUnits) zoneLabel.Alignment = d2ui.HorizontalAlignCenter + healthGlobe := newGlobeWidget(ui, 0, screenHeight, typeHealthGlobe, &hero.Stats.Health, &hero.Stats.MaxHealth) + manaGlobe := newGlobeWidget(ui, screenWidth-manaGlobeScreenOffsetX, screenHeight, typeManaGlobe, &hero.Stats.Mana, &hero.Stats.MaxMana) + return &HUD{ asset: asset, uiManager: ui, @@ -152,11 +151,84 @@ func NewHUD( nameLabel: nameLabel, skillSelectMenu: NewSkillSelectMenu(asset, ui, hero), zoneChangeText: zoneLabel, + healthGlobe: healthGlobe, + manaGlobe: manaGlobe, } } // Load creates the ui elemets func (h *HUD) Load() { + h.loadSprites() + + h.healthGlobe.load() + h.manaGlobe.load() + + h.loadSkillResources() + h.loadCustomWidgets() + h.loadUIButtons() + h.loadTooltips() +} + +func (h *HUD) loadCustomWidgets() { + // static background + _, height, err := h.mainPanel.GetFrameSize(0) // health globe is the frame with max height + if err != nil { + log.Print(err) + return + } + + h.panelBackground = h.uiManager.NewCustomWidgetCached(h.renderPanelStatic, screenWidth, height) + h.panelBackground.SetPosition(0, screenHeight-height) + + // stamina bar + h.widgetStamina = h.uiManager.NewCustomWidget(h.renderStaminaBar, staminaBarWidth, staminaBarHeight) + h.widgetStamina.SetPosition(staminaBarOffsetX, staminaBarOffsetY) + + // experience bar + h.widgetExperience = h.uiManager.NewCustomWidget(h.renderExperienceBar, expBarWidth, expBarHeight) + + // Left skill widget + leftRenderFunc := func(target d2interface.Surface) { + x, y := h.widgetLeftSkill.GetPosition() + h.renderLeftSkill(x, y, target) + } + + h.widgetLeftSkill = h.uiManager.NewCustomWidget(leftRenderFunc, skillIconWidth, skillIconHeight) + h.widgetLeftSkill.SetPosition(leftSkillX, screenHeight) + + // Right skill widget + rightRenderFunc := func(target d2interface.Surface) { + x, y := h.widgetRightSkill.GetPosition() + h.renderRightSkill(x, y, target) + } + + h.widgetRightSkill = h.uiManager.NewCustomWidget(rightRenderFunc, skillIconWidth, skillIconHeight) + h.widgetRightSkill.SetPosition(rightSkillX, screenHeight) +} + +func (h *HUD) loadSkillResources() { + // https://github.com/OpenDiablo2/OpenDiablo2/issues/799 + genericSkillsSprite, err := h.uiManager.NewSprite(d2resource.GenericSkills, d2resource.PaletteSky) + if err != nil { + log.Print(err) + } + + attackIconID := 2 + + h.leftSkillResource = &SkillResource{ + SkillIcon: genericSkillsSprite, + IconNumber: attackIconID, + SkillResourcePath: d2resource.GenericSkills, + } + + h.rightSkillResource = &SkillResource{ + SkillIcon: genericSkillsSprite, + IconNumber: attackIconID, + SkillResourcePath: d2resource.GenericSkills, + } +} + +func (h *HUD) loadSprites() { var err error h.globeSprite, err = h.uiManager.NewSprite(d2resource.GameGlobeOverlap, d2resource.PaletteSky) @@ -183,29 +255,6 @@ func (h *HUD) Load() { if err != nil { log.Print(err) } - - // https://github.com/OpenDiablo2/OpenDiablo2/issues/799 - genericSkillsSprite, err := h.uiManager.NewSprite(d2resource.GenericSkills, d2resource.PaletteSky) - if err != nil { - log.Print(err) - } - - attackIconID := 2 - - h.leftSkillResource = &SkillResource{ - SkillIcon: genericSkillsSprite, - IconNumber: attackIconID, - SkillResourcePath: d2resource.GenericSkills, - } - - h.rightSkillResource = &SkillResource{ - SkillIcon: genericSkillsSprite, - IconNumber: attackIconID, - SkillResourcePath: d2resource.GenericSkills, - } - - h.loadUIButtons() - h.loadTooltips() } func (h *HUD) loadTooltips() { @@ -292,35 +341,23 @@ func (h *HUD) onToggleRunButton(noButton bool) { // NOTE: the positioning of all of the panel elements is coupled to the rendering order :( // don't change the order in which the render methods are called, as there is an x,y offset // that is updated between render calls -func (h *HUD) renderGameControlPanelElements(target d2interface.Surface) error { +func (h *HUD) renderPanelStatic(target d2interface.Surface) { _, height := target.GetSize() - offsetX, offsetY := 0, 0 + offsetX, offsetY := 0, height // Main panel background - offsetY = height if err := h.renderPanel(offsetX, offsetY, target); err != nil { - return err - } - - // Health globe - w, _ := h.mainPanel.GetCurrentFrameSize() - - if err := h.renderHealthGlobe(offsetX, offsetY, target); err != nil { - return err - } - - // Left Skill - offsetX += w - if err := h.renderLeftSkill(offsetX, offsetY, target); err != nil { - return err + log.Print(err) + return } // New Stats Button - w, _ = h.leftSkillResource.SkillIcon.GetCurrentFrameSize() - offsetX += w + w, _ := h.mainPanel.GetCurrentFrameSize() + offsetX += w + skillIconWidth if err := h.renderNewStatsButton(offsetX, offsetY, target); err != nil { - return err + log.Print(err) + return } // Stamina @@ -328,30 +365,17 @@ func (h *HUD) renderGameControlPanelElements(target d2interface.Surface) error { offsetX += w if err := h.renderStamina(offsetX, offsetY, target); err != nil { - return err - } - - // Stamina status bar - w, _ = h.mainPanel.GetCurrentFrameSize() - offsetX += w - - if err := h.renderStaminaBar(target); err != nil { - return err - } - - // Experience status bar - if err := h.renderExperienceBar(target); err != nil { - return err - } - - // Mini Panel and button - if err := h.renderMiniPanel(target); err != nil { - return err + log.Print(err) + return } // Potions + w, _ = h.mainPanel.GetCurrentFrameSize() + offsetX += w + if err := h.renderPotions(offsetX, offsetY, target); err != nil { - return err + log.Print(err) + return } // New Skills Button @@ -359,87 +383,21 @@ func (h *HUD) renderGameControlPanelElements(target d2interface.Surface) error { offsetX += w if err := h.renderNewSkillsButton(offsetX, offsetY, target); err != nil { - return err + log.Print(err) + return } - // Right skill + // Empty Mana Globe w, _ = h.mainPanel.GetCurrentFrameSize() - offsetX += w - - if err := h.renderRightSkill(offsetX, offsetY, target); err != nil { - return err - } - - // Mana Globe - w, _ = h.rightSkillResource.SkillIcon.GetCurrentFrameSize() - offsetX += w - - if err := h.renderManaGlobe(offsetX, offsetY, target); err != nil { - return err - } - - return nil -} - -func (h *HUD) renderManaGlobe(x, _ int, target d2interface.Surface) error { - _, height := target.GetSize() + offsetX += w + skillIconWidth if err := h.mainPanel.SetCurrentFrame(frameRightGlobeHolder); err != nil { - return err + log.Print(err) + return } - h.mainPanel.SetPosition(x, height) - + h.mainPanel.SetPosition(offsetX, height) h.mainPanel.Render(target) - - // Mana status bar - manaPercent := float64(h.hero.Stats.Mana) / float64(h.hero.Stats.MaxMana) - manaBarHeight := int(manaPercent * float64(globeHeight)) - - if err := h.hpManaStatusSprite.SetCurrentFrame(frameManaStatus); err != nil { - return err - } - - h.hpManaStatusSprite.SetPosition(x+manaStatusOffsetX, height+manaStatusOffsetY) - - manaMaskRect := image.Rect(0, globeHeight-manaBarHeight, globeWidth, globeHeight) - h.hpManaStatusSprite.RenderSection(target, manaMaskRect) - - // Right globe - if err := h.globeSprite.SetCurrentFrame(frameRightGlobe); err != nil { - return err - } - - h.globeSprite.SetPosition(x+rightGlobeOffsetX, height+rightGlobeOffsetY) - - h.globeSprite.Render(target) - h.globeSprite.Render(target) - - return nil -} - -func (h *HUD) renderHealthGlobe(x, y int, target d2interface.Surface) error { - healthPercent := float64(h.hero.Stats.Health) / float64(h.hero.Stats.MaxHealth) - hpBarHeight := int(healthPercent * float64(globeHeight)) - - if err := h.hpManaStatusSprite.SetCurrentFrame(0); err != nil { - return err - } - - h.hpManaStatusSprite.SetPosition(x+healthStatusOffsetX, y+healthStatusOffsetY) - - healthMaskRect := image.Rect(0, globeHeight-hpBarHeight, globeWidth, globeHeight) - h.hpManaStatusSprite.RenderSection(target, healthMaskRect) - - // Left globe - if err := h.globeSprite.SetCurrentFrame(frameHealthStatus); err != nil { - return err - } - - h.globeSprite.SetPosition(x+globeSpriteOffsetX, y+globeSpriteOffsetY) - h.globeSprite.Render(target) - - return nil } func (h *HUD) renderPanel(x, y int, target d2interface.Surface) error { @@ -453,7 +411,7 @@ func (h *HUD) renderPanel(x, y int, target d2interface.Surface) error { return nil } -func (h *HUD) renderLeftSkill(x, y int, target d2interface.Surface) error { +func (h *HUD) renderLeftSkill(x, y int, target d2interface.Surface) { newSkillResourcePath := h.getSkillResourceByClass(h.hero.LeftSkill.Charclass) if newSkillResourcePath != h.leftSkillResource.SkillResourcePath { h.leftSkillResource.SkillResourcePath = newSkillResourcePath @@ -461,16 +419,15 @@ func (h *HUD) renderLeftSkill(x, y int, target d2interface.Surface) error { } if err := h.leftSkillResource.SkillIcon.SetCurrentFrame(h.hero.LeftSkill.IconCel); err != nil { - return err + log.Print(err) + return } h.leftSkillResource.SkillIcon.SetPosition(x, y) h.leftSkillResource.SkillIcon.Render(target) - - return nil } -func (h *HUD) renderRightSkill(x, _ int, target d2interface.Surface) error { +func (h *HUD) renderRightSkill(x, _ int, target d2interface.Surface) { _, height := target.GetSize() newSkillResourcePath := h.getSkillResourceByClass(h.hero.RightSkill.Charclass) @@ -480,13 +437,12 @@ func (h *HUD) renderRightSkill(x, _ int, target d2interface.Surface) error { } if err := h.rightSkillResource.SkillIcon.SetCurrentFrame(h.hero.RightSkill.IconCel); err != nil { - return err + log.Print(err) + return } h.rightSkillResource.SkillIcon.SetPosition(x, height) h.rightSkillResource.SkillIcon.Render(target) - - return nil } func (h *HUD) renderNewStatsButton(x, y int, target d2interface.Surface) error { @@ -511,7 +467,7 @@ func (h *HUD) renderStamina(x, y int, target d2interface.Surface) error { return nil } -func (h *HUD) renderStaminaBar(target d2interface.Surface) error { +func (h *HUD) renderStaminaBar(target d2interface.Surface) { target.PushTranslation(staminaBarOffsetX, staminaBarOffsetY) defer target.Pop() @@ -526,19 +482,15 @@ func (h *HUD) renderStaminaBar(target d2interface.Surface) error { } target.DrawRect(int(staminaPercent*staminaBarWidth), staminaBarHeight, staminaBarColor) - - return nil } -func (h *HUD) renderExperienceBar(target d2interface.Surface) error { +func (h *HUD) renderExperienceBar(target d2interface.Surface) { target.PushTranslation(experienceBarOffsetX, experienceBarOffsetY) defer target.Pop() expPercent := float64(h.hero.Stats.Experience) / float64(h.hero.Stats.NextLevelExp) target.DrawRect(int(expPercent*expBarWidth), 2, d2util.Color(whiteAlpha100)) - - return nil } func (h *HUD) renderMiniPanel(target d2interface.Surface) error { @@ -765,7 +717,17 @@ func (h *HUD) renderForSelectableEntitiesHovered(target d2interface.Surface) { func (h *HUD) Render(target d2interface.Surface) error { h.renderForSelectableEntitiesHovered(target) - if err := h.renderGameControlPanelElements(target); err != nil { + h.panelBackground.Render(target) + + h.healthGlobe.Render(target) + h.widgetLeftSkill.Render(target) + h.widgetRightSkill.Render(target) + h.manaGlobe.Render(target) + h.widgetStamina.Render(target) + h.widgetExperience.Render(target) + + // Mini Panel and button + if err := h.renderMiniPanel(target); err != nil { return err } diff --git a/d2game/d2player/skilltree.go b/d2game/d2player/skilltree.go index 53e3b219..246f4995 100644 --- a/d2game/d2player/skilltree.go +++ b/d2game/d2player/skilltree.go @@ -131,7 +131,7 @@ func (s *skillTree) load() { s.panelGroup = s.uiManager.NewWidgetGroup(d2ui.RenderPrioritySkilltree) s.iconGroup = s.uiManager.NewWidgetGroup(d2ui.RenderPrioritySkilltreeIcon) - s.panel = s.uiManager.NewCustomWidget(s.Render) + s.panel = s.uiManager.NewCustomWidget(s.Render, 400, 600) s.panelGroup.AddWidget(s.panel) s.frame = d2ui.NewUIFrame(s.asset, s.uiManager, d2ui.FrameRight) @@ -424,9 +424,6 @@ func (s *skillTree) renderTabCommon(target d2interface.Surface) { skillPanel.SetPosition(x+w, y) s.renderPanelSegment(target, frameCommonTabBottomRight) - - // available skill points label - s.availSPLabel.Render(target) } func (s *skillTree) renderTab(target d2interface.Surface, tab int) {