2020-10-22 16:53:18 -04:00
|
|
|
package d2player
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"sort"
|
|
|
|
|
|
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2geom"
|
|
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
|
|
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource"
|
2020-11-18 16:02:49 -05:00
|
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2util"
|
2020-10-22 16:53:18 -04:00
|
|
|
"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
|
2020-10-24 22:52:26 -04:00
|
|
|
screenHeight = 600
|
2020-10-22 16:53:18 -04:00
|
|
|
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
|
2020-11-18 16:02:49 -05:00
|
|
|
func NewHeroSkillsPanel(asset *d2asset.AssetManager,
|
|
|
|
ui *d2ui.UIManager,
|
|
|
|
hero *d2mapentity.Player,
|
|
|
|
l d2util.LogLevel,
|
|
|
|
isLeftPanel bool) *SkillPanel {
|
2020-10-22 16:53:18 -04:00
|
|
|
var activeSkill *d2hero.HeroSkill
|
|
|
|
if isLeftPanel {
|
|
|
|
activeSkill = hero.LeftSkill
|
|
|
|
} else {
|
|
|
|
activeSkill = hero.RightSkill
|
|
|
|
}
|
|
|
|
|
2020-10-28 13:54:55 -04:00
|
|
|
hoverTooltip := ui.NewTooltip(d2resource.Font16, d2resource.PaletteStatic, d2ui.TooltipXLeft, d2ui.TooltipYTop)
|
2020-10-22 16:53:18 -04:00
|
|
|
|
2020-11-18 16:02:49 -05:00
|
|
|
skillPanel := &SkillPanel{
|
2020-10-28 13:54:55 -04:00
|
|
|
asset: asset,
|
|
|
|
activeSkill: activeSkill,
|
|
|
|
ui: ui,
|
|
|
|
isOpen: false,
|
|
|
|
ListRows: make([]*SkillListRow, skillListsLength),
|
|
|
|
renderer: ui.Renderer(),
|
|
|
|
isLeftPanel: isLeftPanel,
|
|
|
|
hero: hero,
|
|
|
|
hoverTooltip: hoverTooltip,
|
2020-10-22 16:53:18 -04:00
|
|
|
}
|
2020-11-18 16:02:49 -05:00
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
logger *d2util.Logger
|
2020-10-22 16:53:18 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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.
|
2020-10-25 03:42:31 -04:00
|
|
|
func (s *SkillPanel) IsInRect(x, y int) bool {
|
2020-10-22 16:53:18 -04:00
|
|
|
for _, listRow := range s.ListRows {
|
2020-10-25 03:42:31 -04:00
|
|
|
if listRow != nil && listRow.IsInRect(x, y) {
|
2020-10-22 16:53:18 -04:00
|
|
|
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.
|
2020-10-25 03:42:31 -04:00
|
|
|
func (s *SkillPanel) GetListRowByPos(x, y int) *SkillListRow {
|
2020-10-22 16:53:18 -04:00
|
|
|
for _, listRow := range s.ListRows {
|
2020-10-25 03:42:31 -04:00
|
|
|
if listRow.IsInRect(x, y) {
|
2020-10-22 16:53:18 -04:00
|
|
|
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 {
|
2020-10-25 03:42:31 -04:00
|
|
|
if err := s.generateSkillRowImageCache(); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-10-22 16:53:18 -04:00
|
|
|
s.regenerateImageCache = false
|
|
|
|
}
|
|
|
|
|
|
|
|
renderedRows := 0
|
2020-10-25 03:42:31 -04:00
|
|
|
|
2020-10-22 16:53:18 -04:00
|
|
|
for _, skillListRow := range s.ListRows {
|
|
|
|
if len(skillListRow.Skills) == 0 {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
startX := s.getRowStartX(skillListRow)
|
|
|
|
rowOffsetY := skillPanelOffsetY - (renderedRows * skillIconHeight)
|
|
|
|
|
|
|
|
target.PushTranslation(startX, rowOffsetY)
|
2020-10-28 14:17:42 -04:00
|
|
|
target.Render(skillListRow.cachedImage)
|
2020-10-22 16:53:18 -04:00
|
|
|
target.Pop()
|
|
|
|
|
|
|
|
renderedRows++
|
|
|
|
}
|
|
|
|
|
|
|
|
if s.hoveredSkill != nil {
|
2020-11-11 08:55:59 -05:00
|
|
|
s.hoverTooltip.Render(target)
|
2020-10-22 16:53:18 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// RegenerateImageCache will force re-generating the cached menu image on next Render.
|
2020-10-25 03:42:31 -04:00
|
|
|
// Somewhat expensive operation, should not be called often.
|
|
|
|
// Currently called every time the panel is opened or when the player learns a new skill.
|
2020-10-22 16:53:18 -04:00
|
|
|
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()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-25 03:42:31 -04:00
|
|
|
func (s *SkillPanel) generateSkillRowImageCache() error {
|
2020-10-22 16:53:18 -04:00
|
|
|
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
|
2020-10-25 03:42:31 -04:00
|
|
|
|
2020-10-22 16:53:18 -04:00
|
|
|
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),
|
|
|
|
}
|
|
|
|
|
2020-10-25 03:42:31 -04:00
|
|
|
skillRow := skillListRow
|
|
|
|
|
2020-10-22 16:53:18 -04:00
|
|
|
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 {
|
2020-10-25 03:42:31 -04:00
|
|
|
return skillRow.Skills[a].ID < skillRow.Skills[b].ID
|
2020-10-22 16:53:18 -04:00
|
|
|
}
|
|
|
|
|
2020-10-25 03:42:31 -04:00
|
|
|
return skillRow.Skills[a].ID > skillRow.Skills[b].ID
|
2020-10-22 16:53:18 -04:00
|
|
|
})
|
|
|
|
|
|
|
|
cachedImage, err := s.createSkillListImage(skillListRow)
|
|
|
|
|
|
|
|
if err != nil {
|
2020-11-18 16:02:49 -05:00
|
|
|
s.logger.Error(err.Error())
|
2020-10-22 16:53:18 -04:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
s.ListRows[idx].cachedImage = cachedImage
|
|
|
|
visibleRows++
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *SkillPanel) createSkillListImage(skillsListRow *SkillListRow) (d2interface.Surface, error) {
|
2020-10-28 14:17:42 -04:00
|
|
|
surface := s.renderer.NewSurface(len(skillsListRow.Skills)*skillIconWidth, skillIconHeight)
|
2020-10-22 16:53:18 -04:00
|
|
|
|
|
|
|
lastSkillResourcePath := d2resource.GenericSkills
|
|
|
|
skillSprite, _ := s.ui.NewSprite(s.getSkillResourceByClass(""), d2resource.PaletteSky)
|
2020-10-25 03:42:31 -04:00
|
|
|
|
2020-10-22 16:53:18 -04:00
|
|
|
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
|
2020-11-18 16:02:49 -05:00
|
|
|
s.logger.Error(fmt.Sprintf("Invalid IconCel(sprite frame index) [%d] - Skill name: %s, skipping.", skill.IconCel, skill.Name))
|
2020-10-22 16:53:18 -04:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := skillSprite.SetCurrentFrame(skill.IconCel); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
surface.PushTranslation(idx*skillIconWidth, 50)
|
2020-11-11 09:05:04 -05:00
|
|
|
skillSprite.Render(surface)
|
2020-10-22 16:53:18 -04:00
|
|
|
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()
|
|
|
|
}
|
|
|
|
|
2020-10-25 03:42:31 -04:00
|
|
|
func (s *SkillPanel) getSkillAtPos(x, y int) *d2hero.HeroSkill {
|
|
|
|
listRow := s.GetListRowByPos(x, y)
|
2020-10-22 16:53:18 -04:00
|
|
|
|
|
|
|
if listRow == nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-10-25 03:42:31 -04:00
|
|
|
skillIndex := (x - s.getRowStartX(listRow)) / skillIconWidth
|
2020-10-22 16:53:18 -04:00
|
|
|
skill := listRow.Skills[skillIndex]
|
|
|
|
|
|
|
|
return skill
|
|
|
|
}
|
|
|
|
|
2020-10-25 03:42:31 -04:00
|
|
|
func (s *SkillPanel) getSkillIdxAtPos(x, y int) int {
|
|
|
|
listRow := s.GetListRowByPos(x, y)
|
2020-10-22 16:53:18 -04:00
|
|
|
|
|
|
|
if listRow == nil {
|
|
|
|
return -1
|
|
|
|
}
|
|
|
|
|
2020-10-25 03:42:31 -04:00
|
|
|
skillIndex := (x - s.getRowStartX(listRow)) / skillIconWidth
|
2020-10-22 16:53:18 -04:00
|
|
|
|
|
|
|
return skillIndex
|
|
|
|
}
|
|
|
|
|
2020-10-25 03:42:31 -04:00
|
|
|
// 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) {
|
2020-10-22 16:53:18 -04:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2020-10-25 03:42:31 -04:00
|
|
|
clickedSkill := s.getSkillAtPos(x, y)
|
2020-10-22 16:53:18 -04:00
|
|
|
|
|
|
|
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.
|
2020-10-25 03:42:31 -04:00
|
|
|
func (s *SkillPanel) HandleMouseMove(x, y int) bool {
|
2020-10-22 16:53:18 -04:00
|
|
|
if !s.isOpen {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2020-10-25 03:42:31 -04:00
|
|
|
if !s.IsInRect(x, y) {
|
2020-10-22 16:53:18 -04:00
|
|
|
// panel still open but player hovered outside panel - hide the previously hovered skill(if any)
|
|
|
|
s.hoveredSkill = nil
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
previousHovered := s.hoveredSkill
|
2020-10-25 03:42:31 -04:00
|
|
|
s.hoveredSkill = s.getSkillAtPos(x, y)
|
2020-10-22 16:53:18 -04:00
|
|
|
|
|
|
|
if previousHovered != s.hoveredSkill && s.hoveredSkill != nil {
|
2020-11-03 14:10:11 -05:00
|
|
|
skillDescription := s.asset.TranslateString(s.hoveredSkill.ShortKey)
|
2020-10-28 13:54:55 -04:00
|
|
|
s.hoverTooltip.SetText(fmt.Sprintf("%s\n%s", s.hoveredSkill.Skill, skillDescription))
|
2020-10-22 16:53:18 -04:00
|
|
|
|
2020-10-25 03:42:31 -04:00
|
|
|
listRow := s.GetListRowByPos(x, y)
|
2020-10-24 10:08:45 -04:00
|
|
|
|
2020-10-25 03:42:31 -04:00
|
|
|
tooltipX := (s.getSkillIdxAtPos(x, y) * skillIconWidth) + s.getRowStartX(listRow)
|
2020-10-22 16:53:18 -04:00
|
|
|
tooltipY := listRow.Rectangle.Top + listRow.Rectangle.Height
|
2020-10-28 13:54:55 -04:00
|
|
|
s.hoverTooltip.SetPosition(tooltipX, tooltipY)
|
2020-10-22 16:53:18 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
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:
|
2020-11-18 16:02:49 -05:00
|
|
|
s.logger.Error(fmt.Sprintf("Unknown class token: '%s'", class))
|
2020-10-22 16:53:18 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
return resource
|
|
|
|
}
|