mirror of
https://github.com/OpenDiablo2/OpenDiablo2
synced 2025-02-20 23:47:16 -05: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:
parent
e5dae4e5d8
commit
7661b81576
73
d2common/d2enum/skill_class.go
Normal file
73
d2common/d2enum/skill_class.go
Normal 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 ""
|
||||
}
|
@ -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
|
||||
|
@ -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]
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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())
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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"),
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 := ""
|
||||
|
||||
|
35
d2game/d2player/skill_row.go
Normal file
35
d2game/d2player/skill_row.go
Normal 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)
|
||||
}
|
106
d2game/d2player/skill_select_menu.go
Normal file
106
d2game/d2player/skill_select_menu.go
Normal 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()
|
||||
}
|
||||
}
|
372
d2game/d2player/skill_select_panel.go
Normal file
372
d2game/d2player/skill_select_panel.go
Normal 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
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user