OpenDiablo2/d2game/d2player/game_controls.go

1842 lines
45 KiB
Go

package d2player
import (
"fmt"
"image"
"log"
"math"
"strings"
"time"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2tbl"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2geom"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2util"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2math/d2vector"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2gui"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2hero"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2mapengine"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2mapentity"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2maprenderer"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2ui"
)
// Panel represents the panel at the bottom of the game screen
type Panel interface {
IsOpen() bool
Toggle()
Open()
Close()
}
const (
expBarWidth = 120.0
staminaBarWidth = 102.0
staminaBarHeight = 19.0
globeHeight = 80
globeWidth = 80
hoverLabelOuterPad = 5
mouseBtnActionsThreshold = 0.25
percentStaminaBarLow = 0.25
)
const (
// Since they require special handling, not considering (1) globes, (2) content of the mini panel, (3) belt
leftSkill actionableType = iota
newStats
xp
walkRun
stamina
miniPnl
newSkills
rightSkill
hpGlobe
manaGlobe
miniPanelCharacter
miniPanelInventory
miniPanelSkillTree
miniPanelAutomap
miniPanelMessageLog
miniPanelQuestLog
miniPanelGameMenu
)
const (
leftSkillX,
leftSkillY,
leftSkillWidth,
leftSkillHeight = 115, 550, 50, 50
newStatsX,
newStatsY,
newStatsWidth,
newStatsHeight = 206, 563, 30, 30
xpX,
xpY,
xpWidth,
xpHeight = 253, 560, 125, 5
walkRunX,
walkRunY,
walkRunWidth,
walkRunHeight = 255, 573, 17, 20
staminaX,
staminaY,
staminaWidth,
staminaHeight = 273, 573, 105, 20
miniPnlX,
miniPnlY,
miniPnlWidth,
miniPnlHeight = 393, 563, 12, 23
newSkillsX,
newSkillsY,
newSkillsWidth,
newSkillsHeight = 562, 563, 30, 30
rightSkillX,
rightSkillY,
rightSkillWidth,
rightSkillHeight = 634, 550, 50, 50
hpGlobeX,
hpGlobeY,
hpGlobeWidth,
hpGlobeHeight = 30, 525, 80, 60
manaGlobeX,
manaGlobeY,
manaGlobeWidth,
manaGlobeHeight = 695, 525, 80, 60
miniPanelCharacterX,
miniPanelCharacterY,
miniPanelCharacterWidth,
miniPanelCharacterHeight = 324, 528, 22, 26
miniPanelInventoryX,
miniPanelInventoryY,
miniPanelInventoryWidth,
miniPanelInventoryHeight = 346, 528, 22, 26
miniPanelSkillTreeX,
miniPanelSkillTreeY,
miniPanelSkillTreeWidth,
miniPanelSkillTreeHeight = 368, 528, 22, 26
miniPanelAutomapX,
miniPanelAutomapY,
miniPanelAutomapWidth,
miniPanelAutomapHeight = 390, 528, 22, 26
miniPanelMessageLogX,
miniPanelMessageLogY,
miniPanelMessageLogWidth,
miniPanelMessageLogHeight = 412, 528, 22, 26
miniPanelQuestLogX,
miniPanelQuestLogY,
miniPanelQuestLogWidth,
miniPanelQuestLogHeight = 434, 528, 22, 26
miniPanelGameMenuX,
miniPanelGameMenuY,
miniPanelGameMenuWidth,
miniPanelGameMenuHeight = 456, 528, 22, 26
)
const (
hpLabelX = 15
hpLabelY = 487
manaLabelX = 785
manaLabelY = 487
staminaExperienceY = 535
)
const (
runButtonX = 255
runButtonY = 570
)
const (
frameMenuButton = 2
frameHealthStatus = 0
frameManaStatus = 1
frameNewStatsSelector = 1
frameStamina = 2
framePotions = 3
frameNewSkillsSelector = 4
frameRightGlobeHolder = 5
frameRightGlobe = 1
)
const (
manaStatusOffsetX = 7
manaStatusOffsetY = -12
healthStatusOffsetX = 30
healthStatusOffsetY = -13
globeSpriteOffsetX = 28
globeSpriteOffsetY = -5
staminaBarOffsetX = 273
staminaBarOffsetY = 572
experienceBarOffsetX = 256
experienceBarOffsetY = 561
rightGlobeOffsetX = 8
rightGlobeOffsetY = -8
miniPanelButtonOffsetX = -8
miniPanelButtonOffsetY = -16
)
const (
lightBrownAlpha72 = 0xaf8848c8
redAlpha72 = 0xff0000c8
whiteAlpha100 = 0xffffffff
)
const (
zoneChangeTextX = screenWidth / 2
zoneChangeTextY = screenHeight / 4
)
const (
menuBottomRectX,
menuBottomRectY,
menuBottomRectW,
menuBottomRectH = 0, 550, 800, 50
menuLeftRectX,
menuLeftRectY,
menuLeftRectW,
menuLeftRectH = 0, 0, 400, 600
menuRightRectX,
menuRightRectY,
menuRightRectW,
menuRightRectH = 400, 0, 400, 600
)
// GameControls represents the game's controls on the screen
type GameControls struct {
keyMap *KeyMap
actionableRegions []actionableRegion
asset *d2asset.AssetManager
renderer d2interface.Renderer // https://github.com/OpenDiablo2/OpenDiablo2/issues/798
inputListener inputCallbackListener
hero *d2mapentity.Player
heroState *d2hero.HeroStateFactory
mapEngine *d2mapengine.MapEngine
mapRenderer *d2maprenderer.MapRenderer
escapeMenu *EscapeMenu
ui *d2ui.UIManager
inventory *Inventory
skilltree *skillTree
heroStatsPanel *HeroStatsPanel
HelpOverlay *HelpOverlay
miniPanel *miniPanel
bottomMenuRect *d2geom.Rectangle
leftMenuRect *d2geom.Rectangle
rightMenuRect *d2geom.Rectangle
lastMouseX int
lastMouseY int
globeSprite *d2ui.Sprite
hpManaStatusSprite *d2ui.Sprite
mainPanel *d2ui.Sprite
menuButton *d2ui.Sprite
skillSelectMenu *SkillSelectMenu
leftSkillResource *SkillResource
rightSkillResource *SkillResource
zoneChangeText *d2ui.Label
nameLabel *d2ui.Label
hpManaStatsLabel *d2ui.Label
runButton *d2ui.Button
lastLeftBtnActionTime float64
lastRightBtnActionTime float64
FreeCam bool
isZoneTextShown bool
hpStatsIsVisible bool
manaStatsIsVisible bool
isSinglePlayer bool
}
type actionableType int
type actionableRegion struct {
actionableTypeID actionableType
rect d2geom.Rectangle
}
// SkillResource represents a Skill with its corresponding icon sprite, path to DC6 file and icon number.
// SkillResourcePath points to a DC6 resource which contains the icons of multiple skills as frames.
// The IconNumber is the frame at which we can find our skill sprite in the DC6 file.
type SkillResource struct {
SkillResourcePath string // path to a skills DC6 file(see getSkillResourceByClass)
IconNumber int // the index of the frame in the DC6 file
SkillIcon *d2ui.Sprite
}
// NewGameControls creates a GameControls instance and returns a pointer to it
// nolint:funlen // doesn't make sense to split this up
func NewGameControls(
asset *d2asset.AssetManager,
renderer d2interface.Renderer,
hero *d2mapentity.Player,
mapEngine *d2mapengine.MapEngine,
escapeMenu *EscapeMenu,
mapRenderer *d2maprenderer.MapRenderer,
inputListener inputCallbackListener,
term d2interface.Terminal,
ui *d2ui.UIManager,
guiManager *d2gui.GuiManager,
isSinglePlayer bool,
) (*GameControls, error) {
zoneLabel := ui.NewLabel(d2resource.Font30, d2resource.PaletteUnits)
zoneLabel.Alignment = d2gui.HorizontalAlignCenter
nameLabel := ui.NewLabel(d2resource.Font16, d2resource.PaletteStatic)
nameLabel.Alignment = d2gui.HorizontalAlignCenter
nameLabel.SetText(d2ui.ColorTokenize("", d2ui.ColorTokenServer))
hpManaStatsLabel := ui.NewLabel(d2resource.Font16, d2resource.PaletteUnits)
hpManaStatsLabel.Alignment = d2gui.HorizontalAlignLeft
var inventoryRecordKey string
switch hero.Class {
case d2enum.HeroAssassin:
inventoryRecordKey = "Assassin2"
case d2enum.HeroAmazon:
inventoryRecordKey = "Amazon2"
case d2enum.HeroBarbarian:
inventoryRecordKey = "Barbarian2"
case d2enum.HeroDruid:
inventoryRecordKey = "Druid2"
case d2enum.HeroNecromancer:
inventoryRecordKey = "Necromancer2"
case d2enum.HeroPaladin:
inventoryRecordKey = "Paladin2"
case d2enum.HeroSorceress:
inventoryRecordKey = "Sorceress2"
default:
return nil, fmt.Errorf("unknown hero class: %d", hero.Class)
}
inventoryRecord := asset.Records.Layout.Inventory[inventoryRecordKey]
const blackAlpha50percent = 0x0000007f
hoverLabel := nameLabel
hoverLabel.SetBackgroundColor(d2util.Color(blackAlpha50percent))
globeStatsLabel := hpManaStatsLabel
heroState, err := d2hero.NewHeroStateFactory(asset)
if err != nil {
return nil, err
}
keyMap := getDefaultKeyMap()
gc := &GameControls{
keyMap: keyMap,
asset: asset,
ui: ui,
renderer: renderer,
hero: hero,
heroState: heroState,
mapEngine: mapEngine,
escapeMenu: escapeMenu,
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: NewHelpOverlay(asset, renderer, ui, guiManager, keyMap),
miniPanel: newMiniPanel(asset, ui, isSinglePlayer),
nameLabel: hoverLabel,
zoneChangeText: zoneLabel,
hpManaStatsLabel: globeStatsLabel,
bottomMenuRect: &d2geom.Rectangle{
Left: menuBottomRectX,
Top: menuBottomRectY,
Width: menuBottomRectW,
Height: menuBottomRectH,
},
leftMenuRect: &d2geom.Rectangle{
Left: menuLeftRectX,
Top: menuLeftRectY,
Width: menuLeftRectW,
Height: menuLeftRectH,
},
rightMenuRect: &d2geom.Rectangle{
Left: menuRightRectX,
Top: menuRightRectY,
Width: menuRightRectW,
Height: menuRightRectH,
},
actionableRegions: []actionableRegion{
{leftSkill, d2geom.Rectangle{
Left: leftSkillX,
Top: leftSkillY,
Width: leftSkillWidth,
Height: leftSkillHeight,
}},
{newStats, d2geom.Rectangle{
Left: newStatsX,
Top: newStatsY,
Width: newStatsWidth,
Height: newStatsHeight,
}},
{xp, d2geom.Rectangle{
Left: xpX,
Top: xpY,
Width: xpWidth,
Height: xpHeight,
}},
{walkRun, d2geom.Rectangle{
Left: walkRunX,
Top: walkRunY,
Width: walkRunWidth,
Height: walkRunHeight,
}},
{stamina, d2geom.Rectangle{
Left: staminaX,
Top: staminaY,
Width: staminaWidth,
Height: staminaHeight,
}},
{miniPnl, d2geom.Rectangle{
Left: miniPnlX,
Top: miniPnlY,
Width: miniPnlWidth,
Height: miniPnlHeight,
}},
{newSkills, d2geom.Rectangle{
Left: newSkillsX,
Top: newSkillsY,
Width: newSkillsWidth,
Height: newSkillsHeight,
}},
{rightSkill, d2geom.Rectangle{
Left: rightSkillX,
Top: rightSkillY,
Width: rightSkillWidth,
Height: rightSkillHeight,
}},
{hpGlobe, d2geom.Rectangle{
Left: hpGlobeX,
Top: hpGlobeY,
Width: hpGlobeWidth,
Height: hpGlobeHeight,
}},
{manaGlobe, d2geom.Rectangle{
Left: manaGlobeX,
Top: manaGlobeY,
Width: manaGlobeWidth,
Height: manaGlobeHeight,
}},
{miniPanelCharacter, d2geom.Rectangle{
Left: miniPanelCharacterX,
Top: miniPanelCharacterY,
Width: miniPanelCharacterWidth,
Height: miniPanelCharacterHeight,
}},
{miniPanelInventory, d2geom.Rectangle{
Left: miniPanelInventoryX,
Top: miniPanelInventoryY,
Width: miniPanelInventoryWidth,
Height: miniPanelInventoryHeight,
}},
{miniPanelSkillTree, d2geom.Rectangle{
Left: miniPanelSkillTreeX,
Top: miniPanelSkillTreeY,
Width: miniPanelSkillTreeWidth,
Height: miniPanelSkillTreeHeight,
}},
{miniPanelAutomap, d2geom.Rectangle{
Left: miniPanelAutomapX,
Top: miniPanelAutomapY,
Width: miniPanelAutomapWidth,
Height: miniPanelAutomapHeight,
}},
{miniPanelMessageLog, d2geom.Rectangle{
Left: miniPanelMessageLogX,
Top: miniPanelMessageLogY,
Width: miniPanelMessageLogWidth,
Height: miniPanelMessageLogHeight,
}},
{miniPanelQuestLog, d2geom.Rectangle{
Left: miniPanelQuestLogX,
Top: miniPanelQuestLogY,
Width: miniPanelQuestLogWidth,
Height: miniPanelQuestLogHeight,
}},
{miniPanelGameMenu, d2geom.Rectangle{
Left: miniPanelGameMenuX,
Top: miniPanelGameMenuY,
Width: miniPanelGameMenuWidth,
Height: miniPanelGameMenuHeight,
}},
},
lastLeftBtnActionTime: 0,
lastRightBtnActionTime: 0,
isSinglePlayer: isSinglePlayer,
}
closeCb := func() { gc.updateLayout() }
gc.heroStatsPanel.SetOnCloseCb(closeCb)
gc.inventory.SetOnCloseCb(closeCb)
gc.skilltree.SetOnCloseCb(closeCb)
err = gc.bindTerminalCommands(term)
if err != nil {
return nil, err
}
return gc, nil
}
// OnKeyRepeat is called to handle repeated key presses
func (g *GameControls) OnKeyRepeat(event d2interface.KeyEvent) bool {
if g.FreeCam {
var moveSpeed float64 = 8
if event.KeyMod() == d2enum.KeyModShift {
moveSpeed *= 2
}
if event.Key() == d2enum.KeyDown {
v := d2vector.NewVector(0, moveSpeed)
g.mapRenderer.MoveCameraTargetBy(v)
return true
}
if event.Key() == d2enum.KeyUp {
v := d2vector.NewVector(0, -moveSpeed)
g.mapRenderer.MoveCameraTargetBy(v)
return true
}
if event.Key() == d2enum.KeyRight {
v := d2vector.NewVector(moveSpeed, 0)
g.mapRenderer.MoveCameraTargetBy(v)
return true
}
if event.Key() == d2enum.KeyLeft {
v := d2vector.NewVector(-moveSpeed, 0)
g.mapRenderer.MoveCameraTargetBy(v)
return true
}
}
return false
}
// OnKeyDown handles key presses
func (g *GameControls) OnKeyDown(event d2interface.KeyEvent) bool {
if event.Key() == d2enum.KeyEscape {
g.onEscKey()
return true
}
gameEvent := g.keyMap.getGameEvent(event.Key())
switch gameEvent {
case d2enum.ClearScreen:
g.inventory.Close()
g.skilltree.Close()
g.heroStatsPanel.Close()
g.HelpOverlay.Close()
g.updateLayout()
case d2enum.ToggleInventoryPanel:
g.inventory.Toggle()
g.updateLayout()
case d2enum.ToggleSkillTreePanel:
g.skilltree.Toggle()
g.updateLayout()
case d2enum.ToggleCharacterPanel:
g.heroStatsPanel.Toggle()
g.updateLayout()
case d2enum.ToggleRunWalk:
g.onToggleRunButton(false)
case d2enum.HoldRun:
g.onToggleRunButton(true)
case d2enum.ToggleHelpScreen:
g.HelpOverlay.Toggle()
g.updateLayout()
default:
return false
}
return false
}
// OnKeyUp handles key release
func (g *GameControls) OnKeyUp(event d2interface.KeyEvent) bool {
gameEvent := g.keyMap.getGameEvent(event.Key())
switch gameEvent {
case d2enum.HoldRun:
g.onToggleRunButton(true)
default:
return false
}
return false
}
// When escape is pressed:
// 1. If there was some overlay or panel open, close it
// 2. Otherwise, if the Escape Menu was open, let the Escape Menu handle it
// 3. If nothing was open, open the Escape Menu
func (g *GameControls) onEscKey() {
escHandled := false
if g.skillSelectMenu.IsOpen() {
g.skillSelectMenu.ClosePanels()
escHandled = true
}
if g.inventory.IsOpen() {
g.inventory.Close()
escHandled = true
}
if g.skilltree.IsOpen() {
g.skilltree.Close()
escHandled = true
}
if g.heroStatsPanel.IsOpen() {
g.heroStatsPanel.Close()
escHandled = true
}
if g.HelpOverlay.IsOpen() {
g.HelpOverlay.Toggle()
escHandled = true
}
switch escHandled {
case true:
g.updateLayout()
case false:
if g.escapeMenu.IsOpen() {
g.escapeMenu.OnEscKey()
} else {
g.escapeMenu.open()
}
}
}
func truncateFloat64(n float64) float64 {
const ten = 10.0
return float64(int(n*ten)) / ten
}
// OnMouseButtonRepeat handles repeated mouse clicks
func (g *GameControls) OnMouseButtonRepeat(event d2interface.MouseEvent) bool {
const (
screenWidth, screenHeight = 800, 600
halfScreenWidth, halfScreenHeight = screenWidth / 2, screenHeight / 2
subtilesPerTile = 5
)
px, py := g.mapRenderer.ScreenToWorld(event.X(), event.Y())
px = truncateFloat64(px)
py = truncateFloat64(py)
now := d2util.Now()
button := event.Button()
isLeft := button == d2enum.MouseButtonLeft
isRight := button == d2enum.MouseButtonRight
lastLeft := now - g.lastLeftBtnActionTime
lastRight := now - g.lastRightBtnActionTime
inRect := !g.isInActiveMenusRect(event.X(), event.Y())
shouldDoLeft := lastLeft >= mouseBtnActionsThreshold
shouldDoRight := lastRight >= mouseBtnActionsThreshold
if isLeft && shouldDoLeft && inRect && !g.hero.IsCasting() {
g.lastLeftBtnActionTime = now
if event.KeyMod() == d2enum.KeyModShift {
g.inputListener.OnPlayerCast(g.hero.LeftSkill.ID, px, py)
} else {
g.inputListener.OnPlayerMove(px, py)
}
if g.FreeCam {
if event.Button() == d2enum.MouseButtonLeft {
camVect := g.mapRenderer.Camera.GetPosition().Vector
x := float64(halfScreenWidth) / subtilesPerTile
y := float64(halfScreenHeight) / subtilesPerTile
targetPosition := d2vector.NewPositionTile(x, y)
targetPosition.Add(&camVect)
g.mapRenderer.SetCameraTarget(&targetPosition)
return true
}
}
return true
}
if isRight && shouldDoRight && inRect && !g.hero.IsCasting() {
g.lastRightBtnActionTime = now
g.inputListener.OnPlayerCast(g.hero.RightSkill.ID, px, py)
return true
}
return true
}
// OnMouseMove handles mouse movement events
func (g *GameControls) OnMouseMove(event d2interface.MouseMoveEvent) bool {
mx, my := event.X(), event.Y()
g.lastMouseX = mx
g.lastMouseY = my
g.inventory.lastMouseX = mx
g.inventory.lastMouseY = my
for i := range g.actionableRegions {
// Mouse over a game control element
if g.actionableRegions[i].rect.IsInRect(mx, my) {
g.onHoverActionable(g.actionableRegions[i].actionableTypeID)
}
}
g.skillSelectMenu.LeftPanel.HandleMouseMove(mx, my)
g.skillSelectMenu.RightPanel.HandleMouseMove(mx, my)
return false
}
// OnMouseButtonDown handles mouse button presses
func (g *GameControls) OnMouseButtonDown(event d2interface.MouseEvent) bool {
mx, my := event.X(), event.Y()
for i := range g.actionableRegions {
// If click is on a game control element
if g.actionableRegions[i].rect.IsInRect(mx, my) {
g.onClickActionable(g.actionableRegions[i].actionableTypeID)
return false
}
}
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 = truncateFloat64(px)
py = truncateFloat64(py)
if event.Button() == d2enum.MouseButtonLeft && !g.isInActiveMenusRect(mx, my) && !g.hero.IsCasting() {
g.lastLeftBtnActionTime = d2util.Now()
if event.KeyMod() == d2enum.KeyModShift {
g.inputListener.OnPlayerCast(g.hero.LeftSkill.ID, px, py)
} else {
g.inputListener.OnPlayerMove(px, py)
}
return true
}
if event.Button() == d2enum.MouseButtonRight && !g.isInActiveMenusRect(mx, my) && !g.hero.IsCasting() {
g.lastRightBtnActionTime = d2util.Now()
g.inputListener.OnPlayerCast(g.hero.RightSkill.ID, px, py)
return true
}
return false
}
// Load the resources required for the GameControls
func (g *GameControls) Load() {
var err error
g.globeSprite, err = g.ui.NewSprite(d2resource.GameGlobeOverlap, d2resource.PaletteSky)
if err != nil {
log.Print(err)
}
g.hpManaStatusSprite, err = g.ui.NewSprite(d2resource.HealthManaIndicator, d2resource.PaletteSky)
if err != nil {
log.Print(err)
}
g.mainPanel, err = g.ui.NewSprite(d2resource.GamePanels, d2resource.PaletteSky)
if err != nil {
log.Print(err)
}
g.menuButton, err = g.ui.NewSprite(d2resource.MenuButton, d2resource.PaletteSky)
if err != nil {
log.Print(err)
}
err = g.menuButton.SetCurrentFrame(frameMenuButton)
if err != nil {
log.Print(err)
}
genericSkillsSprite, err := g.ui.NewSprite(d2resource.GenericSkills, d2resource.PaletteSky)
if err != nil {
log.Print(err)
}
attackIconID := 2
g.leftSkillResource = &SkillResource{
SkillIcon: genericSkillsSprite,
IconNumber: attackIconID,
SkillResourcePath: d2resource.GenericSkills,
}
g.rightSkillResource = &SkillResource{
SkillIcon: genericSkillsSprite,
IconNumber: attackIconID,
SkillResourcePath: d2resource.GenericSkills,
}
g.loadUIButtons()
g.inventory.Load()
g.skilltree.load()
g.heroStatsPanel.Load()
g.HelpOverlay.Load()
}
func (g *GameControls) loadUIButtons() {
// Run button
g.runButton = g.ui.NewButton(d2ui.ButtonTypeRun, "")
g.runButton.SetPosition(runButtonX, runButtonY)
g.runButton.OnActivated(func() { g.onToggleRunButton(false) })
if g.hero.IsRunToggled() {
g.runButton.Toggle()
}
}
func (g *GameControls) onToggleRunButton(noButton bool) {
if !noButton {
g.runButton.Toggle()
}
g.hero.ToggleRunWalk()
// https://github.com/OpenDiablo2/OpenDiablo2/issues/800
g.hero.SetIsRunning(g.hero.IsRunToggled())
}
// Advance advances the state of the GameControls
func (g *GameControls) Advance(elapsed float64) error {
g.mapRenderer.Advance(elapsed)
return nil
}
func (g *GameControls) updateLayout() {
isRightPanelOpen := g.isLeftPanelOpen()
isLeftPanelOpen := g.isRightPanelOpen()
switch {
case isRightPanelOpen == isLeftPanelOpen:
g.mapRenderer.ViewportDefault()
case isRightPanelOpen:
g.mapRenderer.ViewportToLeft()
default:
g.mapRenderer.ViewportToRight()
}
}
func (g *GameControls) isLeftPanelOpen() bool {
// https://github.com/OpenDiablo2/OpenDiablo2/issues/801
return g.heroStatsPanel.IsOpen()
}
func (g *GameControls) isRightPanelOpen() bool {
return g.inventory.IsOpen() || g.skilltree.IsOpen()
}
func (g *GameControls) isInActiveMenusRect(px, py int) bool {
if g.bottomMenuRect.IsInRect(px, py) {
return true
}
if g.isLeftPanelOpen() && g.leftMenuRect.IsInRect(px, py) {
return true
}
if g.isRightPanelOpen() && g.rightMenuRect.IsInRect(px, py) {
return true
}
if g.miniPanel.IsOpen() && g.miniPanel.isInRect(px, py) {
return true
}
if g.escapeMenu.IsOpen() {
return true
}
if g.HelpOverlay.IsOpen() && g.HelpOverlay.IsInRect(px, py) {
return true
}
if g.skillSelectMenu.IsOpen() {
return true
}
return false
}
// Render draws the GameControls onto the target
func (g *GameControls) Render(target d2interface.Surface) error {
g.renderForSelectableEntitiesHovered(target)
if err := g.renderPanels(target); err != nil {
return err
}
if err := g.renderHUD(target); err != nil {
return err
}
return nil
}
func (g *GameControls) renderForSelectableEntitiesHovered(target d2interface.Surface) {
mx, my := g.lastMouseX, g.lastMouseY
for entityIdx := range g.mapEngine.Entities() {
entity := (g.mapEngine.Entities())[entityIdx]
if !entity.Selectable() {
continue
}
entPos := entity.GetPosition()
entOffset := entPos.RenderOffset()
entScreenXf, entScreenYf := g.mapRenderer.WorldToScreenF(entity.GetPositionF())
entScreenX := int(math.Floor(entScreenXf))
entScreenY := int(math.Floor(entScreenYf))
entityWidth, entityHeight := entity.GetSize()
halfWidth, halfHeight := entityWidth>>1, entityHeight>>1
l, r := entScreenX-halfWidth-hoverLabelOuterPad, entScreenX+halfWidth+hoverLabelOuterPad
t, b := entScreenY-halfHeight-hoverLabelOuterPad, entScreenY+halfHeight-hoverLabelOuterPad
xWithin := (l <= mx) && (r >= mx)
yWithin := (t <= my) && (b >= my)
within := xWithin && yWithin
if within {
xOff, yOff := int(entOffset.X()), int(entOffset.Y())
g.nameLabel.SetText(entity.Label())
xLabel, yLabel := entScreenX-xOff, entScreenY-yOff-entityHeight-hoverLabelOuterPad
g.nameLabel.SetPosition(xLabel, yLabel)
g.nameLabel.Render(target)
entity.Highlight()
break
}
}
}
func (g *GameControls) renderPanels(target d2interface.Surface) error {
g.heroStatsPanel.Render(target)
g.inventory.Render(target)
err := g.skilltree.Render(target)
if err != nil {
log.Println(err)
}
return nil
}
func (g *GameControls) renderHUD(target d2interface.Surface) error {
if err := g.renderGameControlPanelElements(target); err != nil {
return err
}
if err := g.HelpOverlay.Render(target); err != nil {
return err
}
if g.isZoneTextShown {
g.zoneChangeText.SetPosition(zoneChangeTextX, zoneChangeTextY)
g.zoneChangeText.Render(target)
}
g.renderHealthTooltip(target)
g.renderManaTooltip(target)
g.renderRunWalkTooltip(target)
g.renderStaminaTooltip(target)
g.renderExperienceTooltip(target)
if g.skillSelectMenu.IsOpen() {
g.skillSelectMenu.Render(target)
}
return nil
}
// NOTE: the positioning of all of the panel elements is coupled to the rendering order :(
// don't change the order in which the render methods are called, as there is an x,y offset
// that is updated between render calls
func (g *GameControls) renderGameControlPanelElements(target d2interface.Surface) error {
_, height := target.GetSize()
offsetX, offsetY := 0, 0
// Main panel background
offsetY = height
if err := g.renderPanel(offsetX, offsetY, target); err != nil {
return err
}
// Health globe
w, _ := g.mainPanel.GetCurrentFrameSize()
if err := g.renderHealthGlobe(offsetX, offsetY, target); err != nil {
return err
}
// Left Skill
offsetX += w
if err := g.renderLeftSkill(offsetX, offsetY, target); err != nil {
return err
}
// New Stats Button
w, _ = g.leftSkillResource.SkillIcon.GetCurrentFrameSize()
offsetX += w
if err := g.renderNewStatsButton(offsetX, offsetY, target); err != nil {
return err
}
// Stamina
w, _ = g.mainPanel.GetCurrentFrameSize()
offsetX += w
if err := g.renderStamina(offsetX, offsetY, target); err != nil {
return err
}
// Stamina status bar
w, _ = g.mainPanel.GetCurrentFrameSize()
offsetX += w
if err := g.renderStaminaBar(target); err != nil {
return err
}
// Experience status bar
if err := g.renderExperienceBar(target); err != nil {
return err
}
// Mini Panel and button
if err := g.renderMiniPanel(target); err != nil {
return err
}
// Potions
if err := g.renderPotions(offsetX, offsetY, target); err != nil {
return err
}
// New Skills Button
w, _ = g.mainPanel.GetCurrentFrameSize()
offsetX += w
if err := g.renderNewSkillsButton(offsetX, offsetY, target); err != nil {
return err
}
// Right skill
w, _ = g.mainPanel.GetCurrentFrameSize()
offsetX += w
if err := g.renderRightSkill(offsetX, offsetY, target); err != nil {
return err
}
// Mana Globe
w, _ = g.rightSkillResource.SkillIcon.GetCurrentFrameSize()
offsetX += w
if err := g.renderManaGlobe(offsetX, offsetY, target); err != nil {
return err
}
return nil
}
func (g *GameControls) renderPanel(x, y int, target d2interface.Surface) error {
if err := g.mainPanel.SetCurrentFrame(0); err != nil {
return err
}
g.mainPanel.SetPosition(x, y)
g.mainPanel.Render(target)
return nil
}
func (g *GameControls) renderHealthGlobe(x, y int, target d2interface.Surface) error {
healthPercent := float64(g.hero.Stats.Health) / float64(g.hero.Stats.MaxHealth)
hpBarHeight := int(healthPercent * float64(globeHeight))
if err := g.hpManaStatusSprite.SetCurrentFrame(0); err != nil {
return err
}
g.hpManaStatusSprite.SetPosition(x+healthStatusOffsetX, y+healthStatusOffsetY)
healthMaskRect := image.Rect(0, globeHeight-hpBarHeight, globeWidth, globeHeight)
g.hpManaStatusSprite.RenderSection(target, healthMaskRect)
// Left globe
if err := g.globeSprite.SetCurrentFrame(frameHealthStatus); err != nil {
return err
}
g.globeSprite.SetPosition(x+globeSpriteOffsetX, y+globeSpriteOffsetY)
g.globeSprite.Render(target)
return nil
}
func (g *GameControls) renderLeftSkill(x, y int, target d2interface.Surface) error {
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 {
return err
}
g.leftSkillResource.SkillIcon.SetPosition(x, y)
g.leftSkillResource.SkillIcon.Render(target)
return nil
}
func (g *GameControls) renderNewStatsButton(x, y int, target d2interface.Surface) error {
if err := g.mainPanel.SetCurrentFrame(frameNewStatsSelector); err != nil {
return err
}
g.mainPanel.SetPosition(x, y)
g.mainPanel.Render(target)
return nil
}
func (g *GameControls) renderStamina(x, y int, target d2interface.Surface) error {
if err := g.mainPanel.SetCurrentFrame(frameStamina); err != nil {
return err
}
g.mainPanel.SetPosition(x, y)
g.mainPanel.Render(target)
return nil
}
func (g *GameControls) renderStaminaBar(target d2interface.Surface) error {
target.PushTranslation(staminaBarOffsetX, staminaBarOffsetY)
defer target.Pop()
target.PushEffect(d2enum.DrawEffectModulate)
defer target.Pop()
staminaPercent := g.hero.Stats.Stamina / float64(g.hero.Stats.MaxStamina)
staminaBarColor := d2util.Color(lightBrownAlpha72)
if staminaPercent < percentStaminaBarLow {
staminaBarColor = d2util.Color(redAlpha72)
}
target.DrawRect(int(staminaPercent*staminaBarWidth), staminaBarHeight, staminaBarColor)
return nil
}
func (g *GameControls) renderExperienceBar(target d2interface.Surface) error {
target.PushTranslation(experienceBarOffsetX, experienceBarOffsetY)
defer target.Pop()
expPercent := float64(g.hero.Stats.Experience) / float64(g.hero.Stats.NextLevelExp)
target.DrawRect(int(expPercent*expBarWidth), 2, d2util.Color(whiteAlpha100))
return nil
}
func (g *GameControls) renderMiniPanel(target d2interface.Surface) error {
width, height := target.GetSize()
mx, my := g.lastMouseX, g.lastMouseY
menuButtonFrameIndex := 0
if g.miniPanel.isOpen {
menuButtonFrameIndex = 2
}
if err := g.menuButton.SetCurrentFrame(menuButtonFrameIndex); err != nil {
return err
}
buttonX, buttonY := (width>>1)+miniPanelButtonOffsetX, height+miniPanelButtonOffsetY
g.menuButton.SetPosition(buttonX, buttonY)
g.menuButton.Render(target)
g.miniPanel.Render(target)
miniPanelButtons := map[actionableType]string{
miniPanelCharacter: "minipanelchar",
miniPanelInventory: "minipanelinv",
miniPanelSkillTree: "minipaneltree",
miniPanelAutomap: "minipanelautomap",
miniPanelMessageLog: "minipanelmessage",
miniPanelQuestLog: "minipanelquest",
miniPanelGameMenu: "minipanelmenubtn",
}
if !g.miniPanel.IsOpen() {
return nil
}
for miniPanelButton, stringTableKey := range miniPanelButtons {
if !g.actionableRegions[miniPanelButton].rect.IsInRect(mx, my) {
continue
}
rect := &g.actionableRegions[miniPanelButton].rect
g.nameLabel.SetText(d2tbl.TranslateString(stringTableKey))
halfButtonWidth := rect.Width >> 1
halfButtonHeight := rect.Height >> 1
centerX := rect.Left + halfButtonWidth
centerY := rect.Top + halfButtonHeight
_, labelHeight := g.nameLabel.GetSize()
labelX := centerX
labelY := centerY - halfButtonHeight - labelHeight
g.nameLabel.SetPosition(labelX, labelY)
g.nameLabel.Render(target)
}
return nil
}
func (g *GameControls) renderPotions(x, _ int, target d2interface.Surface) error {
_, height := target.GetSize()
if err := g.mainPanel.SetCurrentFrame(framePotions); err != nil {
return err
}
g.mainPanel.SetPosition(x, height)
g.mainPanel.Render(target)
return nil
}
func (g *GameControls) renderNewSkillsButton(x, _ int, target d2interface.Surface) error {
_, height := target.GetSize()
if err := g.mainPanel.SetCurrentFrame(frameNewSkillsSelector); err != nil {
return err
}
g.mainPanel.SetPosition(x, height)
g.mainPanel.Render(target)
return nil
}
func (g *GameControls) renderRightSkill(x, _ int, target d2interface.Surface) error {
_, height := target.GetSize()
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 {
return err
}
g.rightSkillResource.SkillIcon.SetPosition(x, height)
g.rightSkillResource.SkillIcon.Render(target)
return nil
}
func (g *GameControls) renderManaGlobe(x, _ int, target d2interface.Surface) error {
_, height := target.GetSize()
if err := g.mainPanel.SetCurrentFrame(frameRightGlobeHolder); err != nil {
return err
}
g.mainPanel.SetPosition(x, height)
g.mainPanel.Render(target)
// Mana status bar
manaPercent := float64(g.hero.Stats.Mana) / float64(g.hero.Stats.MaxMana)
manaBarHeight := int(manaPercent * float64(globeHeight))
if err := g.hpManaStatusSprite.SetCurrentFrame(frameManaStatus); err != nil {
return err
}
g.hpManaStatusSprite.SetPosition(x+manaStatusOffsetX, height+manaStatusOffsetY)
manaMaskRect := image.Rect(0, globeHeight-manaBarHeight, globeWidth, globeHeight)
g.hpManaStatusSprite.RenderSection(target, manaMaskRect)
// Right globe
if err := g.globeSprite.SetCurrentFrame(frameRightGlobe); err != nil {
return err
}
g.globeSprite.SetPosition(x+rightGlobeOffsetX, height+rightGlobeOffsetY)
g.globeSprite.Render(target)
g.globeSprite.Render(target)
return nil
}
func (g *GameControls) renderHealthTooltip(target d2interface.Surface) {
mx, my := g.lastMouseX, g.lastMouseY
// Create and format Health string from string lookup table.
fmtHealth := d2tbl.TranslateString("panelhealth")
healthCurr, healthMax := g.hero.Stats.Health, g.hero.Stats.MaxHealth
strPanelHealth := fmt.Sprintf(fmtHealth, healthCurr, healthMax)
// Display current hp and mana stats hpGlobe or manaGlobe region is clicked
if !(g.actionableRegions[hpGlobe].rect.IsInRect(mx, my) || g.hpStatsIsVisible) {
return
}
g.hpManaStatsLabel.SetText(strPanelHealth)
g.hpManaStatsLabel.SetPosition(hpLabelX, hpLabelY)
g.hpManaStatsLabel.Render(target)
}
func (g *GameControls) renderManaTooltip(target d2interface.Surface) {
mx, my := g.lastMouseX, g.lastMouseY
// Create and format Mana string from string lookup table.
fmtMana := d2tbl.TranslateString("panelmana")
manaCurr, manaMax := g.hero.Stats.Mana, g.hero.Stats.MaxMana
strPanelMana := fmt.Sprintf(fmtMana, manaCurr, manaMax)
if !(g.actionableRegions[manaGlobe].rect.IsInRect(mx, my) || g.manaStatsIsVisible) {
return
}
g.hpManaStatsLabel.SetText(strPanelMana)
// In case if the mana value gets higher, we need to shift the
// label to the left a little, hence widthManaLabel.
widthManaLabel, _ := g.hpManaStatsLabel.GetSize()
xManaLabel := manaLabelX - widthManaLabel
g.hpManaStatsLabel.SetPosition(xManaLabel, manaLabelY)
g.hpManaStatsLabel.Render(target)
}
func (g *GameControls) renderRunWalkTooltip(target d2interface.Surface) {
mx, my := g.lastMouseX, g.lastMouseY
// Display run/walk tooltip when hovered.
// Note that whether the player is walking or running, the tooltip is the same in Diablo 2.
if !g.actionableRegions[walkRun].rect.IsInRect(mx, my) {
return
}
var stringTableKey string
if g.hero.IsRunToggled() {
stringTableKey = "RunOff"
} else {
stringTableKey = "RunOn"
}
g.nameLabel.SetText(d2tbl.TranslateString(stringTableKey))
rect := &g.actionableRegions[walkRun].rect
halfButtonWidth := rect.Width >> 1
halfButtonHeight := rect.Height >> 1
centerX := rect.Left + halfButtonWidth
centerY := rect.Top + halfButtonHeight
_, labelHeight := g.nameLabel.GetSize()
labelX := centerX
labelY := centerY - halfButtonHeight - labelHeight
g.nameLabel.SetPosition(labelX, labelY)
g.nameLabel.Render(target)
}
func (g *GameControls) renderStaminaTooltip(target d2interface.Surface) {
mx, my := g.lastMouseX, g.lastMouseY
// Display stamina tooltip when hovered.
if !g.actionableRegions[stamina].rect.IsInRect(mx, my) {
return
}
// Create and format Stamina string from string lookup table.
fmtStamina := d2tbl.TranslateString("panelstamina")
staminaCurr, staminaMax := int(g.hero.Stats.Stamina), g.hero.Stats.MaxStamina
strPanelStamina := fmt.Sprintf(fmtStamina, staminaCurr, staminaMax)
g.nameLabel.SetText(strPanelStamina)
rect := &g.actionableRegions[stamina].rect
halfButtonWidth := rect.Width >> 1
centerX := rect.Left + halfButtonWidth
_, labelHeight := g.nameLabel.GetSize()
halfLabelHeight := labelHeight >> 1
labelX := centerX
labelY := staminaExperienceY - halfLabelHeight
g.nameLabel.SetPosition(labelX, labelY)
g.nameLabel.Render(target)
}
func (g *GameControls) renderExperienceTooltip(target d2interface.Surface) {
mx, my := g.lastMouseX, g.lastMouseY
// Display experience tooltip when hovered.
if !g.actionableRegions[xp].rect.IsInRect(mx, my) {
return
}
// Create and format Experience string from string lookup table.
fmtExp := d2tbl.TranslateString("panelexp")
// The English string for "panelexp" is "Experience: %u / %u", however %u doesn't
// translate well. So we need to rewrite %u into a formatable Go verb. %d is used in other
// strings, so we go with that, keeping in mind that %u likely referred to
// an unsigned integer.
fmtExp = strings.ReplaceAll(fmtExp, "%u", "%d")
expCurr, expMax := uint(g.hero.Stats.Experience), uint(g.hero.Stats.NextLevelExp)
strPanelExp := fmt.Sprintf(fmtExp, expCurr, expMax)
g.nameLabel.SetText(strPanelExp)
rect := &g.actionableRegions[stamina].rect
halfButtonWidth := rect.Width >> 1
centerX := rect.Left + halfButtonWidth
_, labelHeight := g.nameLabel.GetSize()
halfLabelHeight := labelHeight >> 1
labelX := centerX
labelY := staminaExperienceY - halfLabelHeight
g.nameLabel.SetPosition(labelX, labelY)
g.nameLabel.Render(target)
}
// SetZoneChangeText sets the zoneChangeText
func (g *GameControls) SetZoneChangeText(text string) {
g.zoneChangeText.SetText(text)
}
// ShowZoneChangeText shows the zoneChangeText
func (g *GameControls) ShowZoneChangeText() {
g.isZoneTextShown = true
}
// HideZoneChangeTextAfter hides the zoneChangeText after the given amount of seconds
func (g *GameControls) HideZoneChangeTextAfter(delay float64) {
time.AfterFunc(time.Duration(delay)*time.Second, func() {
g.isZoneTextShown = false
})
}
// HpStatsIsVisible returns true if the hp and mana stats are visible to the player
func (g *GameControls) HpStatsIsVisible() bool {
return g.hpStatsIsVisible
}
// ManaStatsIsVisible returns true if the hp and mana stats are visible to the player
func (g *GameControls) ManaStatsIsVisible() bool {
return g.manaStatsIsVisible
}
// ToggleHpStats toggles the visibility of the hp and mana stats placed above their respective globe and load only if they do not match
func (g *GameControls) ToggleHpStats() {
g.hpStatsIsVisible = !g.hpStatsIsVisible
}
// ToggleManaStats toggles the visibility of the hp and mana stats placed above their respective globe
func (g *GameControls) ToggleManaStats() {
g.manaStatsIsVisible = !g.manaStatsIsVisible
}
// Handles what to do when an actionable is hovered
func (g *GameControls) onHoverActionable(item actionableType) {
hoverMap := map[actionableType]func(){
leftSkill: func() {},
newStats: func() {},
xp: func() {},
walkRun: func() {},
stamina: func() {},
miniPnl: func() {},
newSkills: func() {},
rightSkill: func() {},
hpGlobe: func() {},
manaGlobe: func() {},
miniPanelCharacter: func() {},
miniPanelInventory: func() {},
miniPanelSkillTree: func() {},
miniPanelAutomap: func() {},
miniPanelMessageLog: func() {},
miniPanelQuestLog: func() {},
miniPanelGameMenu: func() {},
}
onHover, found := hoverMap[item]
if !found {
log.Printf("Unrecognized actionableType(%d) being hovered\n", item)
return
}
onHover()
}
// Handles what to do when an actionable is clicked
func (g *GameControls) onClickActionable(item actionableType) {
actionMap := map[actionableType]func(){
leftSkill: func() {
g.skillSelectMenu.ToggleLeftPanel()
},
newStats: func() {
log.Println("New Stats Selector Action Pressed")
},
xp: func() {
log.Println("XP Action Pressed")
},
walkRun: func() {
log.Println("Walk/Run Action Pressed")
},
stamina: func() {
log.Println("Stamina Action Pressed")
},
miniPnl: func() {
log.Println("Mini Panel Action Pressed")
g.miniPanel.Toggle()
},
newSkills: func() {
log.Println("New Skills Selector Action Pressed")
},
rightSkill: func() {
g.skillSelectMenu.ToggleRightPanel()
},
hpGlobe: func() {
g.ToggleHpStats()
log.Println("HP Globe Pressed")
},
manaGlobe: func() {
g.ToggleManaStats()
log.Println("Mana Globe Pressed")
},
miniPanelCharacter: func() {
log.Println("Character button on mini panel is pressed")
g.heroStatsPanel.Toggle()
g.updateLayout()
},
miniPanelInventory: func() {
log.Println("Inventory button on mini panel is pressed")
g.inventory.Toggle()
g.updateLayout()
},
miniPanelSkillTree: func() {
log.Println("Skilltree button on mini panel is pressed")
g.skilltree.Toggle()
g.updateLayout()
},
miniPanelGameMenu: func() {
g.miniPanel.Close()
g.escapeMenu.open()
},
}
action, found := actionMap[item]
if !found {
log.Printf("Unrecognized actionableType(%d) being clicked\n", item)
return
}
action()
}
func (g *GameControls) bindFreeCamCommand(term d2interface.Terminal) error {
return term.BindAction("freecam", "toggle free camera movement", func() {
g.FreeCam = !g.FreeCam
})
}
func (g *GameControls) bindSetLeftSkillCommand(term d2interface.Terminal) error {
setLeftSkill := 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
}
return term.BindAction(
"setleftskill",
"set skill to fire on left click",
setLeftSkill,
)
}
func (g *GameControls) bindSetRightSkillCommand(term d2interface.Terminal) error {
setRightSkill := 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
}
return term.BindAction(
"setrightskill",
"set skill to fire on right click",
setRightSkill,
)
}
const classTokenLength = 3
func (g *GameControls) bindLearnSkillsCommand(term d2interface.Terminal) error {
learnSkills := func(token string) {
if len(token) < classTokenLength {
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 {
fmtInvalid := "Invalid class, must be a value starting with(case insensitive): %s"
term.OutputErrorf(fmtInvalid, strings.Join(validPrefixes, ", "))
return
}
var err error
learnedSkillsCount := 0
for _, skillDetailRecord := range g.asset.Records.Skill.Details {
if skillDetailRecord.Charclass != classToken {
continue
}
skill, skillErr := g.heroState.CreateHeroSkill(1, skillDetailRecord.Skill)
if skill == nil {
continue
}
learnedSkillsCount++
g.hero.Skills[skill.ID] = skill
if skillErr != nil {
err = skillErr
break
}
}
g.skillSelectMenu.RegenerateImageCache()
log.Printf("Learned %d skills", learnedSkillsCount)
if err != nil {
term.OutputErrorf("cannot learn skill for class, error: %s", err)
return
}
}
return term.BindAction(
"learnskills",
"learn all skills for the a given class",
learnSkills,
)
}
func (g *GameControls) bindLearnSkillByIDCommand(term d2interface.Terminal) error {
learnByID := 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)
if skill == nil {
term.OutputErrorf("cannot create skill: %s", skillRecord.Skill)
return
}
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 term.BindAction(
"learnskillid",
"learn a skill by a given ID",
learnByID,
)
}
func (g *GameControls) bindTerminalCommands(term d2interface.Terminal) error {
if err := g.bindFreeCamCommand(term); err != nil {
return err
}
if err := g.bindSetLeftSkillCommand(term); err != nil {
return err
}
if err := g.bindSetRightSkillCommand(term); err != nil {
return err
}
if err := g.bindLearnSkillsCommand(term); err != nil {
return err
}
if err := g.bindLearnSkillByIDCommand(term); err != nil {
return err
}
return nil
}
func (g *GameControls) getSkillResourceByClass(class string) string {
resourceMap := map[string]string{
"": d2resource.GenericSkills,
"bar": d2resource.BarbarianSkills,
"nec": d2resource.NecromancerSkills,
"pal": d2resource.PaladinSkills,
"ass": d2resource.AssassinSkills,
"sor": d2resource.SorcererSkills,
"ama": d2resource.AmazonSkills,
"dru": d2resource.DruidSkills,
}
entry, found := resourceMap[class]
if !found {
log.Fatalf("Unknown class token: '%s'", class)
}
return entry
}