mirror of
https://github.com/OpenDiablo2/OpenDiablo2
synced 2025-07-26 11:24:38 -04:00
This reverts commit 77cd538c2f8995845280fcbcc606624eeff74dbc. Since we removed the return of errors from the Render() method, we no longer require the RenderNoError() method.
361 lines
9.2 KiB
Go
361 lines
9.2 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 {
|
|
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 {
|
|
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
|
|
}
|