2020-10-22 16:53:18 -04:00
|
|
|
package d2player
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
2020-10-24 10:08:45 -04:00
|
|
|
"image/color"
|
2020-10-22 16:53:18 -04:00
|
|
|
"log"
|
|
|
|
"sort"
|
|
|
|
|
|
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
|
|
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2tbl"
|
|
|
|
"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/d2gui"
|
|
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2hero"
|
|
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2mapentity"
|
|
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2ui"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
skillIconWidth = 48
|
|
|
|
screenWidth = 800
|
|
|
|
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
|
2020-10-24 10:08:45 -04:00
|
|
|
tooltipPadLeft = 3
|
|
|
|
tooltipPadRight = 3
|
|
|
|
tooltipPadBottom = 5
|
2020-10-22 16:53:18 -04:00
|
|
|
)
|
|
|
|
|
|
|
|
// 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
|
|
|
|
isOpen bool
|
|
|
|
regenerateImageCache bool
|
|
|
|
hero *d2mapentity.Player
|
|
|
|
ListRows []*SkillListRow
|
|
|
|
isLeftPanel bool
|
|
|
|
renderer d2interface.Renderer
|
|
|
|
ui *d2ui.UIManager
|
|
|
|
hoveredSkill *d2hero.HeroSkill
|
2020-10-24 10:08:45 -04:00
|
|
|
hoverTooltipRect *d2geom.Rectangle
|
2020-10-22 16:53:18 -04:00
|
|
|
hoverTooltipText *d2ui.Label
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
|
|
|
hoverTooltipText := ui.NewLabel(d2resource.Font16, d2resource.PaletteStatic)
|
|
|
|
hoverTooltipText.Alignment = d2gui.HorizontalAlignCenter
|
|
|
|
|
|
|
|
return &SkillPanel{
|
|
|
|
asset: asset,
|
|
|
|
activeSkill: activeSkill,
|
|
|
|
ui: ui,
|
|
|
|
isOpen: false,
|
|
|
|
ListRows: make([]*SkillListRow, skillListsLength),
|
|
|
|
renderer: ui.Renderer(),
|
|
|
|
isLeftPanel: isLeftPanel,
|
|
|
|
hero: hero,
|
|
|
|
hoverTooltipText: hoverTooltipText,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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 int, Y int) bool {
|
|
|
|
for _, listRow := range s.ListRows {
|
|
|
|
|
|
|
|
// TODO: investigate why listRow can be nil
|
|
|
|
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 int, 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 {
|
|
|
|
s.generateSkillRowImageCache(target)
|
|
|
|
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 {
|
2020-10-24 10:08:45 -04:00
|
|
|
target.PushTranslation(s.hoverTooltipRect.Left, s.hoverTooltipRect.Top)
|
|
|
|
target.DrawRect(s.hoverTooltipRect.Width, s.hoverTooltipRect.Height, color.RGBA{0, 0, 0, uint8(200)})
|
|
|
|
|
|
|
|
// the text should be centered horizontally in the tooltip rect
|
|
|
|
target.PushTranslation(s.hoverTooltipRect.Width/2, 0)
|
2020-10-22 16:53:18 -04:00
|
|
|
s.hoverTooltipText.Render(target)
|
2020-10-24 10:08:45 -04:00
|
|
|
|
|
|
|
target.PopN(2)
|
2020-10-22 16:53:18 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
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(target d2interface.Surface) 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),
|
|
|
|
}
|
|
|
|
|
|
|
|
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 skillListRow.Skills[a].ID < skillListRow.Skills[b].ID
|
|
|
|
}
|
|
|
|
|
|
|
|
return skillListRow.Skills[a].ID > skillListRow.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, err := s.renderer.NewSurface(len(skillsListRow.Skills)*skillIconWidth, skillIconHeight, d2enum.FilterNearest)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
if err := skillSprite.Render(surface); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
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 int, 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 int, 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 int, 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 int, 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 := d2tbl.TranslateString(s.hoveredSkill.ShortKey)
|
|
|
|
s.hoverTooltipText.SetText(fmt.Sprintf("%s\n%s", s.hoveredSkill.Skill, skillDescription))
|
|
|
|
|
|
|
|
listRow := s.GetListRowByPos(X, Y)
|
2020-10-24 10:08:45 -04:00
|
|
|
textWidth, textHeight := s.hoverTooltipText.GetSize()
|
|
|
|
|
|
|
|
tooltipX := (s.getSkillIdxAtPos(X, Y) * skillIconWidth) + s.getRowStartX(listRow)
|
|
|
|
tooltipWidth := textWidth + tooltipPadLeft + tooltipPadRight
|
|
|
|
|
2020-10-22 16:53:18 -04:00
|
|
|
if tooltipX+tooltipWidth >= screenWidth {
|
2020-10-24 10:08:45 -04:00
|
|
|
tooltipX = screenWidth - tooltipWidth
|
2020-10-22 16:53:18 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
tooltipY := listRow.Rectangle.Top + listRow.Rectangle.Height
|
2020-10-24 10:08:45 -04:00
|
|
|
tooltipHeight := textHeight + tooltipPadBottom
|
2020-10-22 16:53:18 -04:00
|
|
|
|
2020-10-24 10:08:45 -04:00
|
|
|
s.hoverTooltipRect = &d2geom.Rectangle{
|
|
|
|
Left: tooltipX,
|
|
|
|
Top: tooltipY,
|
|
|
|
Width: tooltipWidth,
|
|
|
|
Height: tooltipHeight}
|
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:
|
|
|
|
log.Fatalf("Unknown class token: '%s'", class)
|
|
|
|
}
|
|
|
|
|
|
|
|
return resource
|
|
|
|
}
|