OpenDiablo2/d2game/d2player/skill_select_panel.go

373 lines
9.4 KiB
Go

package d2player
import (
"fmt"
"sort"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2geom"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2util"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset"
"github.com/OpenDiablo2/OpenDiablo2/d2core/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
)
// NewHeroSkillsPanel creates a new hero status panel
func NewHeroSkillsPanel(asset *d2asset.AssetManager,
ui *d2ui.UIManager,
hero *d2mapentity.Player,
l d2util.LogLevel,
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)
skillPanel := &SkillPanel{
asset: asset,
activeSkill: activeSkill,
ui: ui,
isOpen: false,
ListRows: make([]*SkillListRow, skillListsLength),
renderer: ui.Renderer(),
isLeftPanel: isLeftPanel,
hero: hero,
hoverTooltip: hoverTooltip,
}
skillPanel.Logger = d2util.NewLogger()
skillPanel.Logger.SetLevel(l)
skillPanel.Logger.SetPrefix(logPrefix)
return skillPanel
}
// 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
*d2util.Logger
}
// 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 {
s.hoverTooltip.Render(target)
}
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 {
s.Error(err.Error())
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
s.Errorf("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:
s.Errorf("Unknown class token: '%s'", class)
}
return resource
}