1
1
mirror of https://github.com/OpenDiablo2/OpenDiablo2 synced 2024-09-27 13:46:00 -04:00
OpenDiablo2/d2game/d2player/skill_select_panel.go
juander 01927d0f3b d2core/d2ui: Add checks to all widgets if they implement Widget
this also adds missing methods to elements not implementing widget. Note
here that we do not enable sprite and label, as this would produce a
crazy amount of linter warnings due to render() requiering error
handling then, which non of the callers handle. Since we remove the
render calls later anyways, we can postpone this static check for now.
2020-11-09 18:13:17 +01:00

363 lines
9.3 KiB
Go

package d2player
import (
"fmt"
"log"
"sort"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2geom"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2hero"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2mapentity"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2ui"
)
const (
skillIconWidth = 48
screenWidth = 800
screenHeight = 600
skillIconHeight = 48
rightPanelEndX = 720
leftPanelStartX = 90
skillPanelOffsetY = 465
skillListsLength = 5 // 0 to 4. 0 - General Skills, 1 to 3 - Class-specific skills(based on the 3 different skill trees), 4 - Other skills
)
// SkillPanel represents a skill select menu popup that is displayed when the player left clicks on his active left/right skill.
type SkillPanel struct {
asset *d2asset.AssetManager
activeSkill *d2hero.HeroSkill
hero *d2mapentity.Player
ListRows []*SkillListRow
renderer d2interface.Renderer
ui *d2ui.UIManager
hoveredSkill *d2hero.HeroSkill
hoverTooltip *d2ui.Tooltip
isOpen bool
regenerateImageCache bool
isLeftPanel bool
}
// NewHeroSkillsPanel creates a new hero status panel
func NewHeroSkillsPanel(asset *d2asset.AssetManager, ui *d2ui.UIManager, hero *d2mapentity.Player, isLeftPanel bool) *SkillPanel {
var activeSkill *d2hero.HeroSkill
if isLeftPanel {
activeSkill = hero.LeftSkill
} else {
activeSkill = hero.RightSkill
}
hoverTooltip := ui.NewTooltip(d2resource.Font16, d2resource.PaletteStatic, d2ui.TooltipXLeft, d2ui.TooltipYTop)
return &SkillPanel{
asset: asset,
activeSkill: activeSkill,
ui: ui,
isOpen: false,
ListRows: make([]*SkillListRow, skillListsLength),
renderer: ui.Renderer(),
isLeftPanel: isLeftPanel,
hero: hero,
hoverTooltip: hoverTooltip,
}
}
// Open opens the hero skills panel
func (s *SkillPanel) Open() {
s.isOpen = true
s.regenerateImageCache = true
}
// Close the hero skills panel
func (s *SkillPanel) Close() {
s.isOpen = false
}
// IsInRect returns whether the X Y coordinates are in some of the list rows of the panel.
func (s *SkillPanel) IsInRect(x, y int) bool {
for _, listRow := range s.ListRows {
if listRow != nil && listRow.IsInRect(x, y) {
return true
}
}
return false
}
// GetListRowByPos returns the skill list row for a given X and Y, based on the width and height of the skills list.
func (s *SkillPanel) GetListRowByPos(x, y int) *SkillListRow {
for _, listRow := range s.ListRows {
if listRow.IsInRect(x, y) {
return listRow
}
}
return nil
}
// Render gets called on every tick
func (s *SkillPanel) Render(target d2interface.Surface) error {
if !s.isOpen {
return nil
}
if s.regenerateImageCache {
if err := s.generateSkillRowImageCache(); err != nil {
return err
}
s.regenerateImageCache = false
}
renderedRows := 0
for _, skillListRow := range s.ListRows {
if len(skillListRow.Skills) == 0 {
continue
}
startX := s.getRowStartX(skillListRow)
rowOffsetY := skillPanelOffsetY - (renderedRows * skillIconHeight)
target.PushTranslation(startX, rowOffsetY)
target.Render(skillListRow.cachedImage)
target.Pop()
renderedRows++
}
if s.hoveredSkill != nil {
if err := s.hoverTooltip.Render(target); err != nil {
log.Printf("Cannot render tooltip, %e", err)
}
}
return nil
}
// RegenerateImageCache will force re-generating the cached menu image on next Render.
// Somewhat expensive operation, should not be called often.
// Currently called every time the panel is opened or when the player learns a new skill.
func (s *SkillPanel) RegenerateImageCache() {
s.regenerateImageCache = true
}
// IsOpen returns true if the hero skills panel is open
func (s *SkillPanel) IsOpen() bool {
return s.isOpen
}
// Toggle toggles the visibility of the hero status panel
func (s *SkillPanel) Toggle() {
if s.isOpen {
s.Close()
} else {
s.Open()
}
}
func (s *SkillPanel) generateSkillRowImageCache() error {
for idx := range s.ListRows {
s.ListRows[idx] = &SkillListRow{Skills: make([]*d2hero.HeroSkill, 0), Rectangle: d2geom.Rectangle{Height: 0, Width: 0}}
}
for _, skill := range s.hero.Skills {
// left panel with an incompatible skill(e.g. Paladin auras cant be used as a left skill)
if s.isLeftPanel && !skill.Leftskill {
continue
}
// ListRow is -1 for other skills that should not be shown in the panel(e.g. Kick)
if skill.ListRow == -1 || skill.Passive {
continue
}
s.ListRows[skill.ListRow].AddSkill(skill)
}
visibleRows := 0
for idx, skillListRow := range s.ListRows {
// row won't be considered as visible
if len(skillListRow.Skills) == 0 {
continue
}
skillListRow.Rectangle = d2geom.Rectangle{
Height: skillIconHeight,
Width: skillListRow.GetWidth(),
Left: s.getRowStartX(skillListRow),
Top: skillPanelOffsetY - (visibleRows * skillIconHeight),
}
skillRow := skillListRow
sort.SliceStable(skillListRow.Skills, func(a, b int) bool {
// left panel skills are aligned by ID (low to high), right panel is the opposite
if s.isLeftPanel {
return skillRow.Skills[a].ID < skillRow.Skills[b].ID
}
return skillRow.Skills[a].ID > skillRow.Skills[b].ID
})
cachedImage, err := s.createSkillListImage(skillListRow)
if err != nil {
log.Println(err)
return err
}
s.ListRows[idx].cachedImage = cachedImage
visibleRows++
}
return nil
}
func (s *SkillPanel) createSkillListImage(skillsListRow *SkillListRow) (d2interface.Surface, error) {
surface := s.renderer.NewSurface(len(skillsListRow.Skills)*skillIconWidth, skillIconHeight)
lastSkillResourcePath := d2resource.GenericSkills
skillSprite, _ := s.ui.NewSprite(s.getSkillResourceByClass(""), d2resource.PaletteSky)
for idx, skill := range skillsListRow.Skills {
currentResourcePath := s.getSkillResourceByClass(skill.Charclass)
// only load a new sprite if the DCC file path changed
if currentResourcePath != lastSkillResourcePath {
lastSkillResourcePath = currentResourcePath
skillSprite, _ = s.ui.NewSprite(currentResourcePath, d2resource.PaletteSky)
}
if skillSprite.GetFrameCount() <= skill.IconCel {
// happens for non-player skills, since they do not have an icon
log.Printf("Invalid IconCel(sprite frame index) [%d] - Skill name: %s, skipping.", skill.IconCel, skill.Name)
continue
}
if err := skillSprite.SetCurrentFrame(skill.IconCel); err != nil {
return nil, err
}
surface.PushTranslation(idx*skillIconWidth, 50)
skillSprite.Render(surface)
surface.Pop()
}
return surface, nil
}
func (s *SkillPanel) getRowStartX(skillRow *SkillListRow) int {
if s.isLeftPanel {
return leftPanelStartX
}
// for the right panel, we only know where it should end, so we calculate the start based on the width of the list row
return rightPanelEndX - skillRow.GetWidth()
}
func (s *SkillPanel) getSkillAtPos(x, y int) *d2hero.HeroSkill {
listRow := s.GetListRowByPos(x, y)
if listRow == nil {
return nil
}
skillIndex := (x - s.getRowStartX(listRow)) / skillIconWidth
skill := listRow.Skills[skillIndex]
return skill
}
func (s *SkillPanel) getSkillIdxAtPos(x, y int) int {
listRow := s.GetListRowByPos(x, y)
if listRow == nil {
return -1
}
skillIndex := (x - s.getRowStartX(listRow)) / skillIconWidth
return skillIndex
}
// HandleClick will change the hero's active(left or right) skill and return true.
// Returns false if the given X, Y is out of panel boundaries.
func (s *SkillPanel) HandleClick(x, y int) bool {
if !s.isOpen || !s.IsInRect(x, y) {
return false
}
clickedSkill := s.getSkillAtPos(x, y)
if clickedSkill == nil {
return false
}
if s.isLeftPanel {
s.hero.LeftSkill = clickedSkill
} else {
s.hero.RightSkill = clickedSkill
}
return true
}
// HandleMouseMove will process a mouse move event, if inside the panel.
func (s *SkillPanel) HandleMouseMove(x, y int) bool {
if !s.isOpen {
return false
}
if !s.IsInRect(x, y) {
// panel still open but player hovered outside panel - hide the previously hovered skill(if any)
s.hoveredSkill = nil
return false
}
previousHovered := s.hoveredSkill
s.hoveredSkill = s.getSkillAtPos(x, y)
if previousHovered != s.hoveredSkill && s.hoveredSkill != nil {
skillDescription := s.asset.TranslateString(s.hoveredSkill.ShortKey)
s.hoverTooltip.SetText(fmt.Sprintf("%s\n%s", s.hoveredSkill.Skill, skillDescription))
listRow := s.GetListRowByPos(x, y)
tooltipX := (s.getSkillIdxAtPos(x, y) * skillIconWidth) + s.getRowStartX(listRow)
tooltipY := listRow.Rectangle.Top + listRow.Rectangle.Height
s.hoverTooltip.SetPosition(tooltipX, tooltipY)
}
return true
}
func (s *SkillPanel) getSkillResourceByClass(class string) string {
resource := ""
switch class {
case "":
resource = d2resource.GenericSkills
case "bar":
resource = d2resource.BarbarianSkills
case "nec":
resource = d2resource.NecromancerSkills
case "pal":
resource = d2resource.PaladinSkills
case "ass":
resource = d2resource.AssassinSkills
case "sor":
resource = d2resource.SorcererSkills
case "ama":
resource = d2resource.AmazonSkills
case "dru":
resource = d2resource.DruidSkills
default:
log.Fatalf("Unknown class token: '%s'", class)
}
return resource
}