1
1
mirror of https://github.com/OpenDiablo2/OpenDiablo2 synced 2024-06-29 10:35:23 +00:00

Initial left & right skill select panel implementation. HeroSkill serialization cleanup. (#783)

- Clicking the active left/right skill now opens a skill select panel.
Only the available skills for the hero, which are valid for the panel type are shown.
Clicking on a skill from the skill select panel makes it the new active skill for the hero.

- Hovering a skill in the skill select panel shows the skill name +
skill description.

- New command which learns all skills for a specific
class(not persisted to a save file yet) - e.g. `learnskills ama` will learn
skills for the Amazon class.

- Initialize HeroSkill.shallowHeroSkill struct in the hero state factory, so we can use it
when we serialize the HeroSkill to packets/game save files.

- The parsed Skill.ListRow is now a number instead  of string.

Co-authored-by: Presiyan Ivanov <presiyan-ivanov@users.noreply.github.com>
This commit is contained in:
presiyan-ivanov 2020-10-22 23:53:18 +03:00 committed by GitHub
parent e5dae4e5d8
commit 7661b81576
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 752 additions and 60 deletions

View File

@ -0,0 +1,73 @@
package d2enum
import "log"
type SkillClass int
const (
// SkillClassGeneric is ""
SkillClassGeneric SkillClass = iota
SkillClassBarbarian
SkillClassNecromancer
SkillClassPaladin
SkillClassAssassin
SkillClassSorceress
SkillClassAmazon
SkillClassDruid
)
// FromToken returns the enum which corresponds to the given class token
func (sc *SkillClass) FromToken(classToken string) SkillClass {
resource := SkillClassGeneric
switch classToken {
case "":
return SkillClassGeneric
case "bar":
return SkillClassBarbarian
case "nec":
return SkillClassNecromancer
case "pal":
return SkillClassPaladin
case "ass":
return SkillClassAssassin
case "sor":
return SkillClassSorceress
case "ama":
return SkillClassAmazon
case "dru":
return SkillClassDruid
default:
log.Fatalf("Unknown skill class token: '%s'", classToken)
}
// should not be reached
return resource
}
// GetToken returns a string token for the enum
func (sc SkillClass) GetToken() string {
switch sc {
case SkillClassGeneric:
return ""
case SkillClassBarbarian:
return "bar"
case SkillClassNecromancer:
return "nec"
case SkillClassPaladin:
return "pal"
case SkillClassAssassin:
return "ass"
case SkillClassSorceress:
return "sor"
case SkillClassAmazon:
return "ama"
case SkillClassDruid:
return "dru"
default:
log.Fatalf("Unknown skill class token: %v", sc)
}
// should not be reached
return ""
}

View File

@ -98,6 +98,16 @@ func (c *Composite) GetAnimationMode() string {
return c.mode.animationMode.String()
}
// GetCurrentFrame returns the frame index in the current animation mode.
func (c *Composite) GetCurrentFrame() int {
return c.mode.frameIndex
}
// GetFrameCount returns the number of frames in the current animation mode.
func (c *Composite) GetFrameCount() int {
return c.mode.frameCount
}
// GetWeaponClass returns the currently loaded weapon class
func (c *Composite) GetWeaponClass() string {
return c.mode.weaponClass

View File

@ -6,16 +6,8 @@ import "github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset"
// This is done to avoid serializing the whole record data of HeroSkill to a game save or network packets.
// We cant do this while unmarshalling because there is no reference to the asset manager.
func HydrateSkills(skills map[int]*HeroSkill, asset *d2asset.AssetManager) {
for skillID := range skills {
heroSkill := skills[skillID]
// TODO: figure out why these are nil sometimes
if heroSkill == nil {
continue
}
heroSkill.SkillRecord = asset.Records.Skill.Details[skillID]
heroSkill.SkillDescriptionRecord = asset.Records.Skill.Descriptions[heroSkill.SkillRecord.Skilldesc]
heroSkill.SkillPoints = skills[skillID].SkillPoints
for skillID, skill := range skills {
skill.SkillRecord = asset.Records.Skill.Details[skillID]
skill.SkillDescriptionRecord = asset.Records.Skill.Descriptions[skill.SkillRecord.Skilldesc]
}
}

View File

@ -158,6 +158,7 @@ func (f *HeroStateFactory) CreateHeroSkill(points int, name string) (*HeroSkill,
SkillPoints: points,
SkillRecord: skillRecord,
SkillDescriptionRecord: skillDescRecord,
shallow: &shallowHeroSkill{SkillID: skillRecord.ID, SkillPoints: points},
}
return result, nil

View File

@ -102,7 +102,8 @@ func (f *MapEntityFactory) NewPlayer(id, name string, x, y, direction int, heroT
}
result.mapEntity.uuid = id
result.SetSpeed(baseRunSpeed)
//TODO: should be based on Player.isRunning after we store isRunning in the save file
result.SetSpeed(baseWalkSpeed)
result.mapEntity.directioner = result.rotate
err = composite.SetMode(d2enum.PlayerAnimationModeTownNeutral, equipment.RightHand.GetWeaponClass())

View File

@ -83,15 +83,19 @@ func (p *Player) IsInTown() bool {
func (p *Player) Advance(tickTime float64) {
p.Step(tickTime)
if p.IsCasting() && p.composite.GetPlayedCount() >= 1 {
p.isCasting = false
if p.onFinishedCasting != nil {
p.onFinishedCasting()
p.onFinishedCasting = nil
if p.IsCasting() {
if p.composite.GetPlayedCount() >= 1 {
p.isCasting = false
if err := p.SetAnimationMode(p.GetAnimationMode()); err != nil {
fmt.Printf("failed to set animationMode to: %d, err: %v\n", p.GetAnimationMode(), err)
}
}
if err := p.SetAnimationMode(p.GetAnimationMode()); err != nil {
fmt.Printf("failed to set animationMode to: %d, err: %v\n", p.GetAnimationMode(), err)
// skills are casted after the first half of the casting animation is played
isHalfDoneCasting := float64(p.composite.GetCurrentFrame()) / float64(p.composite.GetFrameCount()) >= 0.5
if isHalfDoneCasting && p.onFinishedCasting != nil {
p.onFinishedCasting()
p.onFinishedCasting = nil
}
}

View File

@ -21,7 +21,7 @@ func skillDescriptionLoader(r *RecordManager, d *d2txt.DataDictionary) error {
d.Number("SkillPage"),
d.Number("SkillRow"),
d.Number("SkillColumn"),
d.String("ListRow"),
d.Number("ListRow"),
d.String("ListPool"),
d.Number("IconCel"),
d.String("str name"),

View File

@ -12,7 +12,7 @@ type SkillDescriptionRecord struct {
SkillPage int // SkillPage
SkillRow int // SkillRow
SkillColumn int // SkillColumn
ListRow string // ListRow
ListRow int // ListRow
ListPool string // ListPool
IconCel int // IconCel
NameKey string // str name

View File

@ -295,7 +295,7 @@ func animToEnum(anim string) d2enum.PlayerAnimationMode {
return d2enum.PlayerAnimationModeSkill1
case "S2":
return d2enum.PlayerAnimationModeSkill1
return d2enum.PlayerAnimationModeSkill2
case "S3":
return d2enum.PlayerAnimationModeSkill3

View File

@ -69,6 +69,7 @@ type GameControls struct {
hpManaStatusSprite *d2ui.Sprite
mainPanel *d2ui.Sprite
menuButton *d2ui.Sprite
skillSelectMenu *SkillSelectMenu
leftSkillResource *SkillResource
rightSkillResource *SkillResource
zoneChangeText *d2ui.Label
@ -192,6 +193,7 @@ func NewGameControls(
inputListener: inputListener,
mapRenderer: mapRenderer,
inventory: NewInventory(asset, ui, inventoryRecord),
skillSelectMenu: NewSkillSelectMenu(asset, ui, hero),
skilltree: NewSkillTree(hero.Skills, hero.Class, asset, renderer, ui, guiManager),
heroStatsPanel: NewHeroStatsPanel(asset, ui, hero.Name(), hero.Class, hero.Stats),
HelpOverlay: help.NewHelpOverlay(asset, renderer, ui, guiManager),
@ -223,35 +225,7 @@ func NewGameControls(
isSinglePlayer: isSinglePlayer,
}
err = term.BindAction("freecam", "toggle free camera movement", func() {
gc.FreeCam = !gc.FreeCam
})
if err != nil {
return nil, err
}
err = term.BindAction("setleftskill", "set skill to fire on left click", func(id int) {
skillRecord := gc.asset.Records.Skill.Details[id]
skill, err := heroState.CreateHeroSkill(0, skillRecord.Skill)
if err != nil {
term.OutputErrorf("cannot create skill with ID of %d, error: %s", id, err)
return
}
gc.hero.LeftSkill = skill
})
err = term.BindAction("setrightskill", "set skill to fire on right click", func(id int) {
skillRecord := gc.asset.Records.Skill.Details[id]
skill, err := heroState.CreateHeroSkill(0, skillRecord.Skill)
if err != nil {
term.OutputErrorf("cannot create skill with ID of %d, error: %s", id, err)
return
}
gc.hero.RightSkill = skill
})
gc.bindTerminalCommands(term)
if err != nil {
return nil, err
@ -346,6 +320,10 @@ func (g *GameControls) OnKeyUp(event d2interface.KeyEvent) bool {
func (g *GameControls) onEscKey() {
escHandled := false
if g.skillSelectMenu.IsOpen() {
g.skillSelectMenu.ClosePanels()
escHandled = true
}
if g.inventory.IsOpen() {
g.inventory.Close()
@ -447,6 +425,9 @@ func (g *GameControls) OnMouseMove(event d2interface.MouseMoveEvent) bool {
}
}
g.skillSelectMenu.LeftPanel.HandleMouseMove(mx, my)
g.skillSelectMenu.RightPanel.HandleMouseMove(mx, my)
return false
}
@ -462,6 +443,13 @@ func (g *GameControls) OnMouseButtonDown(event d2interface.MouseEvent) bool {
}
}
if g.skillSelectMenu.IsOpen() && event.Button() == d2enum.MouseButtonLeft {
g.lastLeftBtnActionTime = d2util.Now()
g.skillSelectMenu.HandleClick(mx, my)
g.skillSelectMenu.ClosePanels()
return false
}
px, py := g.mapRenderer.ScreenToWorld(mx, my)
px = float64(int(px*10)) / 10.0
py = float64(int(py*10)) / 10.0
@ -616,11 +604,15 @@ func (g *GameControls) isInActiveMenusRect(px, py int) bool {
return true
}
if g.skillSelectMenu.IsOpen() {
return true
}
return false
}
// TODO: consider caching the panels to single image that is reused.
// Render draws the GameControls onto the target
// TODO: consider caching the panels to single image that is reused.
func (g *GameControls) Render(target d2interface.Surface) error {
mx, my := g.lastMouseX, g.lastMouseY
@ -714,9 +706,10 @@ func (g *GameControls) Render(target d2interface.Surface) error {
offset += w
// Left skill
skillResourcePath := g.getSkillResourceByClass(g.hero.LeftSkill.Charclass)
if skillResourcePath != g.leftSkillResource.SkillResourcePath {
g.leftSkillResource.SkillIcon, _ = g.ui.NewSprite(skillResourcePath, d2resource.PaletteSky)
newSkillResourcePath := g.getSkillResourceByClass(g.hero.LeftSkill.Charclass)
if newSkillResourcePath != g.leftSkillResource.SkillResourcePath {
g.leftSkillResource.SkillResourcePath = newSkillResourcePath
g.leftSkillResource.SkillIcon, _ = g.ui.NewSprite(newSkillResourcePath, d2resource.PaletteSky)
}
if err := g.leftSkillResource.SkillIcon.SetCurrentFrame(g.hero.LeftSkill.IconCel); err != nil {
@ -833,9 +826,10 @@ func (g *GameControls) Render(target d2interface.Surface) error {
offset += w
// Right skill
skillResourcePath = g.getSkillResourceByClass(g.hero.RightSkill.Charclass)
if skillResourcePath != g.rightSkillResource.SkillResourcePath {
g.rightSkillResource.SkillIcon, _ = g.ui.NewSprite(skillResourcePath, d2resource.PaletteSky)
newSkillResourcePath = g.getSkillResourceByClass(g.hero.RightSkill.Charclass)
if newSkillResourcePath != g.rightSkillResource.SkillResourcePath {
g.rightSkillResource.SkillIcon, _ = g.ui.NewSprite(newSkillResourcePath, d2resource.PaletteSky)
g.rightSkillResource.SkillResourcePath = newSkillResourcePath
}
if err := g.rightSkillResource.SkillIcon.SetCurrentFrame(g.hero.RightSkill.IconCel); err != nil {
@ -1033,6 +1027,10 @@ func (g *GameControls) Render(target d2interface.Surface) error {
g.nameLabel.Render(target)
}
if g.skillSelectMenu.IsOpen() {
g.skillSelectMenu.Render(target)
}
return nil
}
@ -1119,7 +1117,7 @@ func (g *GameControls) onHoverActionable(item actionableType) {
func (g *GameControls) onClickActionable(item actionableType) {
switch item {
case leftSkill:
log.Println("Left Skill Action Pressed")
g.skillSelectMenu.ToggleLeftPanel()
case newStats:
log.Println("New Stats Selector Action Pressed")
case xp:
@ -1135,7 +1133,7 @@ func (g *GameControls) onClickActionable(item actionableType) {
case newSkills:
log.Println("New Skills Selector Action Pressed")
case rightSkill:
log.Println("Right Skill Action Pressed")
g.skillSelectMenu.ToggleRightPanel()
case hpGlobe:
g.ToggleHpStats()
log.Println("HP Globe Pressed")
@ -1165,6 +1163,106 @@ func (g *GameControls) onClickActionable(item actionableType) {
}
}
func (g *GameControls) bindTerminalCommands(term d2interface.Terminal) error {
err := term.BindAction("freecam", "toggle free camera movement", func() {
g.FreeCam = !g.FreeCam
})
if err != nil {
return err
}
err = term.BindAction("setleftskill", "set skill to fire on left click", func(id int) {
skillRecord := g.asset.Records.Skill.Details[id]
skill, err := g.heroState.CreateHeroSkill(1, skillRecord.Skill)
if err != nil {
term.OutputErrorf("cannot create skill with ID of %d, error: %s", id, err)
return
}
g.hero.LeftSkill = skill
})
err = term.BindAction("learnskills", "learn all skills for the a given class", func(token string) {
if len(token) < 3 {
term.OutputErrorf("The given class token should be at least 3 characters")
return
}
validPrefixes := []string{"ama", "ass", "nec", "bar", "sor", "dru", "pal"}
classToken := strings.ToLower(token)
tokenPrefix := classToken[0:3]
isValidToken := false
for idx := range validPrefixes {
if strings.Compare(tokenPrefix, validPrefixes[idx]) == 0 {
isValidToken = true
}
}
if !isValidToken {
term.OutputErrorf("Invalid class, must be a value starting with(case insensitive): %s", strings.Join(validPrefixes, ", "))
return
}
learnedSkillsCount := 0
for _, skillDetailRecord := range g.asset.Records.Skill.Details {
if skillDetailRecord.Charclass == classToken || skillDetailRecord.Charclass == "" {
skill, err := g.heroState.CreateHeroSkill(1, skillDetailRecord.Skill)
if skill == nil {
continue
}
learnedSkillsCount++
g.hero.Skills[skill.ID] = skill
if err != nil {
break
}
}
}
g.skillSelectMenu.RegenerateImageCache()
log.Printf("Learned %d skills", learnedSkillsCount)
if err != nil {
term.OutputErrorf("cannot learn skill for class, error: %s", err)
return
}
})
err = term.BindAction("setrightskill", "set skill to fire on right click", func(id int) {
skillRecord := g.asset.Records.Skill.Details[id]
skill, err := g.heroState.CreateHeroSkill(0, skillRecord.Skill)
if err != nil {
term.OutputErrorf("cannot create skill with ID of %d, error: %s", id, err)
return
}
g.hero.RightSkill = skill
})
err = term.BindAction("learnskillid", "learn a skill by a given ID", func(id int) {
skillRecord := g.asset.Records.Skill.Details[id]
if skillRecord == nil {
term.OutputErrorf("cannot find a skill record for ID: %d", id)
return
}
skill, err := g.heroState.CreateHeroSkill(1, skillRecord.Skill)
g.hero.Skills[skill.ID] = skill
if err != nil {
term.OutputErrorf("cannot learn skill for class, error: %s", err)
return
}
g.skillSelectMenu.RegenerateImageCache()
log.Println("Learned skill: ", skill.Skill)
})
return nil
}
func (g *GameControls) getSkillResourceByClass(class string) string {
resource := ""

View File

@ -0,0 +1,35 @@
package d2player
import (
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2geom"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2hero"
)
// SkillListRow represents a row of skills that is shown when the skill select menu is rendered.
type SkillListRow struct {
Rectangle d2geom.Rectangle
Skills []*d2hero.HeroSkill
cachedImage d2interface.Surface
}
// AddSkill appends to the skills of the row.
func (s *SkillListRow) AddSkill(skill *d2hero.HeroSkill) {
s.Skills = append(s.Skills, skill)
}
// GetWidth returns the width based on the size of the skills.
func (s *SkillListRow) GetWidth() int {
return skillIconWidth * len(s.Skills)
}
// GetRectangle returns the rectangle of the list.
func (s *SkillListRow) GetRectangle() d2geom.Rectangle {
return s.Rectangle
}
// IsInRect returns true when the list has any skills and coordinates are in the rectangle of the list.
func (s *SkillListRow) IsInRect(X int, Y int) bool {
// if there are no skills, row won't be rendered and it shouldn't be considered visible
return len(s.Skills) > 0 && s.Rectangle.IsInRect(X, Y)
}

View File

@ -0,0 +1,106 @@
package d2player
import (
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2mapentity"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2ui"
)
// SkillSelectMenu is a wrapper for the left + right menu that pop up when a player clicks the left/right skill select.
type SkillSelectMenu struct {
LeftPanel *SkillPanel
RightPanel *SkillPanel
}
// NewSkillSelectMenu creates a skill select menu.
func NewSkillSelectMenu(asset *d2asset.AssetManager, ui *d2ui.UIManager, hero *d2mapentity.Player) *SkillSelectMenu {
skillSelectMenu := &SkillSelectMenu{
LeftPanel: NewHeroSkillsPanel(asset, ui, hero, true),
RightPanel: NewHeroSkillsPanel(asset, ui, hero, false),
}
return skillSelectMenu
}
// HandleClick will propagate the click to the panels.
func (sm *SkillSelectMenu) HandleClick(X int, Y int) bool {
if sm.LeftPanel.HandleClick(X, Y) {
return true
}
if sm.RightPanel.HandleClick(X, Y) {
return true
}
return true
}
// HandleMouseMove will propagate the mouse move event to the panels.
func (sm *SkillSelectMenu) HandleMouseMove(X int, Y int) {
if sm.LeftPanel.IsOpen() {
sm.LeftPanel.HandleMouseMove(X, Y)
} else if sm.RightPanel.IsOpen() {
sm.RightPanel.HandleMouseMove(X, Y)
}
}
// RegenerateImageCache will force both panels to re-create the image shown at skill popup menus.
// Somewhat expensive operation, should not be called often.
func (sm *SkillSelectMenu) RegenerateImageCache() {
sm.LeftPanel.RegenerateImageCache()
sm.RightPanel.RegenerateImageCache()
}
// Render gets called on every frame
func (sm *SkillSelectMenu) Render(target d2interface.Surface) {
sm.LeftPanel.Render(target)
sm.RightPanel.Render(target)
}
// IsOpen returns whether one of the panels(left or right) is open
func (sm *SkillSelectMenu) IsOpen() bool {
return sm.LeftPanel.IsOpen() || sm.RightPanel.IsOpen()
}
// IsInRect returns whether the coordinates are in one of the panels(left or right)
func (sm *SkillSelectMenu) IsInRect(X int, Y int) bool {
return sm.LeftPanel.IsInRect(X, Y) || sm.RightPanel.IsInRect(X, Y)
}
// ClosePanels will close both panels
func (sm *SkillSelectMenu) ClosePanels() {
sm.RightPanel.Close()
sm.LeftPanel.Close()
}
// OpenLeftPanel will close the right panel and open the left panel.
func (sm *SkillSelectMenu) OpenLeftPanel() {
sm.RightPanel.Close()
sm.LeftPanel.Open()
}
// ToggleLeftPanel will close or open the left panel, depending on the current state
func (sm *SkillSelectMenu) ToggleLeftPanel() {
if sm.LeftPanel.IsOpen() {
sm.LeftPanel.Close()
} else {
sm.OpenLeftPanel()
}
}
// OpenRightPanel will close the left panel and open the right panel.
func (sm *SkillSelectMenu) OpenRightPanel() {
sm.LeftPanel.Close()
sm.RightPanel.Open()
}
// ToggleRightPanel will close or open the right panel, depending on the current state
func (sm *SkillSelectMenu) ToggleRightPanel() {
if sm.RightPanel.IsOpen() {
sm.RightPanel.Close()
} else {
sm.OpenRightPanel()
}
}

View File

@ -0,0 +1,372 @@
package d2player
import (
"fmt"
"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
)
// 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
hoverTooltipPos d2geom.Point
//TODO: should be a cached image which contains the skill text + the tooltip background
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 {
target.PushTranslation(s.hoverTooltipPos.X, s.hoverTooltipPos.Y)
s.hoverTooltipText.Render(target)
target.Pop()
}
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)
//TODO: should generate a cached image for the tooltip instead
s.hoverTooltipText.SetText(fmt.Sprintf("%s\n%s", s.hoveredSkill.Skill, skillDescription))
listRow := s.GetListRowByPos(X, Y)
tooltipWidth, _ := s.hoverTooltipText.GetSize()
tooltipX := (s.getSkillIdxAtPos(X, Y) * skillIconWidth) + s.getRowStartX(listRow) + (tooltipWidth / 2)
if tooltipX+tooltipWidth >= screenWidth {
tooltipX = screenWidth - (tooltipWidth / 2)
}
tooltipY := listRow.Rectangle.Top + listRow.Rectangle.Height
s.hoverTooltipPos = d2geom.Point{X: tooltipX, Y: 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
}