1
1
mirror of https://github.com/OpenDiablo2/OpenDiablo2 synced 2024-12-29 05:27:20 -05:00
OpenDiablo2/d2game/d2player/game_controls.go

1045 lines
24 KiB
Go
Raw Normal View History

package d2player
import (
"fmt"
"strings"
"time"
2020-09-12 16:25:09 -04:00
"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"
2020-06-28 21:40:52 -04:00
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset"
2020-06-21 18:40:37 -04:00
"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"
)
const (
logPrefix = "Player"
)
2020-07-26 14:52:54 -04:00
// Panel represents the panel at the bottom of the game screen
type Panel interface {
IsOpen() bool
Toggle()
Open()
Close()
}
const mouseBtnActionsThreshold = 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
newSkills
rightSkill
hpGlobe
manaGlobe
)
const (
leftSkillX,
leftSkillY,
leftSkillWidth,
2020-11-13 15:08:43 -05:00
leftSkillHeight = 117, 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
newSkillsX,
newSkillsY,
newSkillsWidth,
newSkillsHeight = 562, 563, 30, 30
rightSkillX,
rightSkillY,
rightSkillWidth,
2020-11-13 15:08:43 -05:00
rightSkillHeight = 635, 550, 50, 50
hpGlobeX,
hpGlobeY,
hpGlobeWidth,
hpGlobeHeight = 30, 525, 80, 60
manaGlobeX,
manaGlobeY,
manaGlobeWidth,
manaGlobeHeight = 695, 525, 80, 60
)
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
)
2020-08-11 18:01:33 -04:00
// NewGameControls creates a GameControls instance and returns a pointer to it
// nolint:funlen // doesn't make sense to split this up
func NewGameControls(
remove d2asset singleton (#726) * export d2asset singleton * add *d2asset.AssetManager to d2app - d2app now has a reference to an asset manager which it will use for loading - added asset loader methods to the asset manager - functions in d2asset are now wrappers for asset manager methods * add asset manager reference to audio provider - d2app asset manager reference is now passed to audio provider - asset manager is created in main.go for now to pass into audio provider - CreateSoundEffect is now a method, no longer exported, uses the asset manager reference * d2app passes asset manager refence to map engine test * in d2asset, all calls to LoadFile replaced with call to Singleton.Loadfile * blizzard intro and credits screen - d2app passes reference to the asset manager to these screens * asset manager for d2map - adding MapStampFactory, takes an asset manager reference - embedded MapStampFactory into the MapEngine - LoadStamp is now a method of the MapStampFactory * d2asset: removed LoadFileStream, LoadFile, and FileExists * d2gui changes - singleton now has an asset manager reference - calls to d2asset loader functions removed - createButton is now a method of LayoutManager - moved LayoutEntry to its own file * map entity factory - Map engine has an embedded map entity factory - Map stamp factory gets a reference to the map engine's entity factory - Stamps are given a reference to the map engine entity factory when created - Character select gets a map entity factory - Embedded the stamp factory into the MapEngine * asset manager for d2ui - d2ui is passed an asset manager reference when created - all calls to d2asset loader functions in d2ui now refer to the asset manager - d2gamescreen gets a ui manager when created - help overlay is now passed a ui manager when created * d2gamescreen + d2player: asset manager references added an asset manager reference to - inventory panel + inventory grid - mini panel - game controls - help overlay - character select - main menu - select hero class - hero stats panel * Removed d2asset.LoadAnimation all references to this function have been replaced with calls to the asset manager method * adding asset to help overlay, bugfix for 4d59c91 * Removed d2asset.LoadFont and d2asset.LoadAnimationWithEffect all references to these have been replaced with calls to the asset manager methods * MapRenderer now gets an asset manager reference * removed d2asset.LoadPalette all references have been replaced with calls to an asset manager instance * merged d2object with d2mapentity d2object was only being used to create objects in the map, so the provider function is now a method of the map entity factory. calls to d2asset have been removed. * removed d2asset.LoadComposite all calls are now made to the asset manager method * removed d2asset singleton all singleton references have been removed, a single instance of the asset manager is passed around the entire app * rename Initialize to NewAssetManager
2020-09-12 16:51:30 -04:00
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,
keyMap *KeyMap,
l d2util.LogLevel,
isSinglePlayer bool,
) (*GameControls, error) {
var inventoryRecordKey string
2020-07-26 14:52:54 -04:00
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:
2020-09-12 16:25:09 -04:00
return nil, fmt.Errorf("unknown hero class: %d", hero.Class)
}
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,
}},
{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,
}},
}
inventoryRecord := asset.Records.Layout.Inventory[inventoryRecordKey]
heroStatsPanel := NewHeroStatsPanel(asset, ui, hero.Name(), hero.Class, l, hero.Stats)
inventory := NewInventory(asset, ui, l, inventoryRecord)
skilltree := newSkillTree(hero.Skills, hero.Class, asset, l, ui)
miniPanel := newMiniPanel(asset, ui, l, isSinglePlayer)
Removing d2datadict singletons (#738) * Remove weapons, armor, misc, itemCommon, itemTyps datadict singletons - removed loader calls from d2app - removed the HeroObjects singleton from `d2core/d2inventory` - added an InventoryItemFactory in d2inventory - package-level functions that use data records are now methods of the InventoryItemFactory - renamed ItemGenerator in d2item to ItemFactory - package-level functions that use records are now methods of ItemFactory - d2map.MapEntityFactory now has an item factory instance for creating items - fixed a bug in unique item record loader where it loaded an empty record - added a PlayerStateFactory for creating a player state (uses the asset manager) - updated the test inventory/equipment code in d2player to handle errors from the ItemFactory - character select and character creation screens have a player state and inventory item factory - updated item tests to use the item factory * minor edit * Removed d2datadict.Experience singleton added a HeroStatsFactory, much like the other factories. The factory gets an asset manager reference in order to use data records. * removed d2datadict.AutoMagic singleton * removed d2datadict.AutoMap singleton * removed d2datadict.BodyLocations singleton * removed d2datadict.Books singleton * Removed singletons for level records - removed loader calls in d2app - changed type references from d2datadict to d2records - added a `MapGenerator` in d2mapgen which uses thew asset manager and map engine - package-level map generation functions are now MapGenerator methods - `d2datadict.GetLevelDetails(id int)` is now a method of the RecordManager * remove SkillCalc and MissileCalc singletons * Removed CharStats and ItemStatCost singletons - added an ItemStatFactory which uses the asset manager to create stats - package-level functions for stats in d2item are now StatFactory methods - changed type references from d2datadict to d2records - `d2player.GetAllPlayerStates` is now a method of the `PlayerStateFactory` * Removed DkillDesc and Skills singletons from d2datadict - removed loader calls from d2app - diablo2stats.Stat instances are given a reference to the factory for doing record lookups * update the stats test to use mock a asset manager and stat factory * fixed diablo2stats tests and diablo2item tests * removed CompCodes singleton from d2datadict * remove cubemain singleton from d2datadict * removed DifficultyLevels singleton from d2datadict * removed ElemTypes singleton from d2datadict * removed events.go loader from d2datadict (was unused) * removed Gems singleton from d2datadict * removed Hireling and Inventory singletons from d2datadict * removed MagicPrefix and MagicSuffix singletons from d2datadict * removed ItemRatios singleton from d2datadict * removed Missiles singleton from d2datadict * removed MonModes singleton * Removed all monster and npc singletons from d2datadict - MapStamp instances now get a reference to their factory for doing record lookups * removed SoundEntry and SoundEnviron singletons from d2datadict
2020-09-20 17:52:01 -04:00
heroState, err := d2hero.NewHeroStateFactory(asset)
if err != nil {
return nil, err
}
helpOverlay := NewHelpOverlay(asset, renderer, ui, guiManager, l, keyMap)
hud := NewHUD(asset, ui, hero, helpOverlay, miniPanel, actionableRegions, mapEngine, l, mapRenderer)
const blackAlpha50percent = 0x0000007f
hoverLabel := hud.nameLabel
hoverLabel.SetBackgroundColor(d2util.Color(blackAlpha50percent))
gc := &GameControls{
asset: asset,
ui: ui,
renderer: renderer,
hero: hero,
heroState: heroState,
escapeMenu: escapeMenu,
inputListener: inputListener,
mapRenderer: mapRenderer,
inventory: inventory,
skilltree: skilltree,
heroStatsPanel: heroStatsPanel,
HelpOverlay: helpOverlay,
keyMap: keyMap,
hud: hud,
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: actionableRegions,
2020-08-11 18:01:33 -04:00
lastLeftBtnActionTime: 0,
lastRightBtnActionTime: 0,
isSinglePlayer: isSinglePlayer,
}
gc.heroStatsPanel.SetOnCloseCb(gc.onCloseHeroStatsPanel)
gc.inventory.SetOnCloseCb(gc.onCloseInventory)
gc.skilltree.SetOnCloseCb(gc.onCloseSkilltree)
gc.escapeMenu.SetOnCloseCb(gc.hud.restoreMinipanelFromTempClose)
err = gc.bindTerminalCommands(term)
if err != nil {
return nil, err
}
gc.logger = d2util.NewLogger()
gc.logger.SetLevel(l)
gc.logger.SetPrefix(logPrefix)
2020-07-26 14:52:54 -04:00
return gc, nil
}
// 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
mapRenderer *d2maprenderer.MapRenderer
escapeMenu *EscapeMenu
ui *d2ui.UIManager
inventory *Inventory
hud *HUD
skilltree *skillTree
heroStatsPanel *HeroStatsPanel
HelpOverlay *HelpOverlay
bottomMenuRect *d2geom.Rectangle
leftMenuRect *d2geom.Rectangle
rightMenuRect *d2geom.Rectangle
lastMouseX int
lastMouseY int
lastLeftBtnActionTime float64
lastRightBtnActionTime float64
FreeCam bool
isSinglePlayer bool
logger *d2util.Logger
}
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
}
2020-08-11 18:01:33 -04:00
// 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
}
2020-08-11 18:01:33 -04:00
// 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.toggleInventoryPanel()
case d2enum.ToggleSkillTreePanel:
g.toggleInventoryPanel()
case d2enum.ToggleCharacterPanel:
g.toggleHeroStatsPanel()
case d2enum.ToggleRunWalk:
g.hud.onToggleRunButton(false)
case d2enum.HoldRun:
g.hud.onToggleRunButton(true)
case d2enum.ToggleHelpScreen:
g.HelpOverlay.Toggle()
g.updateLayout()
default:
return false
}
2020-08-11 18:01:33 -04:00
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.hud.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.hud.skillSelectMenu.IsOpen() {
g.hud.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.openEscMenu()
}
}
}
func truncateFloat64(n float64) float64 {
const ten = 10.0
return float64(int(n*ten)) / ten
}
2020-08-11 18:01:33 -04:00
// 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
2020-08-11 18:01:33 -04:00
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() {
2020-08-11 18:01:33 -04:00
g.lastLeftBtnActionTime = now
2020-07-26 14:52:54 -04:00
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() {
2020-08-11 18:01:33 -04:00
g.lastRightBtnActionTime = now
2020-07-26 14:52:54 -04:00
g.inputListener.OnPlayerCast(g.hero.RightSkill.ID, px, py)
2020-07-26 14:52:54 -04:00
return true
}
return true
}
2020-08-11 18:01:33 -04:00
// 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.hud.OnMouseMove(event)
return false
}
// OnMouseButtonUp handles mouse button presses
func (g *GameControls) OnMouseButtonUp(event d2interface.MouseEvent) bool {
return false
}
2020-08-11 18:01:33 -04:00
// OnMouseButtonDown handles mouse button presses
func (g *GameControls) OnMouseButtonDown(event d2interface.MouseEvent) bool {
mx, my := event.X(), event.Y()
2020-07-26 14:52:54 -04:00
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.hud.skillSelectMenu.IsOpen() && event.Button() == d2enum.MouseButtonLeft {
g.lastLeftBtnActionTime = d2util.Now()
g.hud.skillSelectMenu.HandleClick(mx, my)
g.hud.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()
2020-07-26 14:52:54 -04:00
if event.KeyMod() == d2enum.KeyModShift {
g.inputListener.OnPlayerCast(g.hero.LeftSkill.ID, px, py)
} else {
g.inputListener.OnPlayerMove(px, py)
}
2020-07-26 14:52:54 -04:00
return true
}
if event.Button() == d2enum.MouseButtonRight && !g.isInActiveMenusRect(mx, my) && !g.hero.IsCasting() {
g.lastRightBtnActionTime = d2util.Now()
2020-07-26 14:52:54 -04:00
g.inputListener.OnPlayerCast(g.hero.RightSkill.ID, px, py)
2020-07-26 14:52:54 -04:00
return true
}
return false
}
func (g *GameControls) toggleHeroStatsPanel() {
g.heroStatsPanel.Toggle()
g.hud.miniPanel.SetMovedRight(g.heroStatsPanel.IsOpen())
g.updateLayout()
}
func (g *GameControls) onCloseHeroStatsPanel() {
g.hud.miniPanel.SetMovedRight(g.heroStatsPanel.IsOpen())
g.updateLayout()
}
func (g *GameControls) toggleInventoryPanel() {
g.skilltree.Close()
g.inventory.Toggle()
g.hud.miniPanel.SetMovedLeft(g.inventory.IsOpen())
g.updateLayout()
}
func (g *GameControls) onCloseInventory() {
g.hud.miniPanel.SetMovedLeft(g.inventory.IsOpen())
g.updateLayout()
}
func (g *GameControls) toggleSkilltreePanel() {
g.inventory.Close()
g.skilltree.Toggle()
g.hud.miniPanel.SetMovedLeft(g.skilltree.IsOpen())
g.updateLayout()
}
func (g *GameControls) onCloseSkilltree() {
g.hud.miniPanel.SetMovedLeft(g.skilltree.IsOpen())
g.updateLayout()
}
func (g *GameControls) openEscMenu() {
g.inventory.Close()
g.skilltree.Close()
g.heroStatsPanel.Close()
g.hud.closeMinipanelTemporary()
g.escapeMenu.open()
g.updateLayout()
}
// Load the resources required for the GameControls
func (g *GameControls) Load() {
g.hud.Load()
g.inventory.Load()
g.skilltree.load()
g.heroStatsPanel.Load()
g.HelpOverlay.Load()
miniPanelActions := &miniPanelActions{
characterToggle: g.toggleHeroStatsPanel,
inventoryToggle: g.toggleInventoryPanel,
skilltreeToggle: g.toggleSkilltreePanel,
menuToggle: g.openEscMenu,
}
g.hud.miniPanel.load(miniPanelActions)
}
2020-08-11 18:01:33 -04:00
// Advance advances the state of the GameControls
func (g *GameControls) Advance(elapsed float64) error {
g.mapRenderer.Advance(elapsed)
if err := g.escapeMenu.Advance(elapsed); err != nil {
return err
}
return nil
}
func (g *GameControls) updateLayout() {
isRightPanelOpen := g.isLeftPanelOpen()
isLeftPanelOpen := g.isRightPanelOpen()
2020-08-11 18:01:33 -04:00
switch {
case isRightPanelOpen == isLeftPanelOpen:
g.mapRenderer.ViewportDefault()
2020-08-11 18:01:33 -04:00
case isRightPanelOpen:
g.mapRenderer.ViewportToLeft()
2020-08-11 18:01:33 -04:00
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()
}
2020-08-11 18:01:33 -04:00
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.hud.miniPanel.IsOpen() && g.hud.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.hud.skillSelectMenu.IsOpen() {
return true
}
return false
}
2020-08-11 18:01:33 -04:00
// Render draws the GameControls onto the target
2020-07-26 14:52:54 -04:00
func (g *GameControls) Render(target d2interface.Surface) error {
if err := g.renderPanels(target); err != nil {
return err
}
if err := g.hud.Render(target); err != nil {
return err
}
if err := g.escapeMenu.Render(target); err != nil {
return err
}
return nil
}
func (g *GameControls) renderPanels(target d2interface.Surface) error {
g.inventory.Render(target)
return nil
}
2020-08-11 18:01:33 -04:00
// SetZoneChangeText sets the zoneChangeText
func (g *GameControls) SetZoneChangeText(text string) {
g.hud.zoneChangeText.SetText(text)
}
2020-08-11 18:01:33 -04:00
// ShowZoneChangeText shows the zoneChangeText
func (g *GameControls) ShowZoneChangeText() {
g.hud.isZoneTextShown = true
}
2020-08-11 18:01:33 -04:00
// 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.hud.isZoneTextShown = false
})
}
// HpStatsIsVisible returns true if the hp and mana stats are visible to the player
func (g *GameControls) HpStatsIsVisible() bool {
return g.hud.hpStatsIsVisible
}
// ManaStatsIsVisible returns true if the hp and mana stats are visible to the player
func (g *GameControls) ManaStatsIsVisible() bool {
return g.hud.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.hud.hpStatsIsVisible = !g.hud.hpStatsIsVisible
}
// ToggleManaStats toggles the visibility of the hp and mana stats placed above their respective globe
func (g *GameControls) ToggleManaStats() {
g.hud.manaStatsIsVisible = !g.hud.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() {},
newSkills: func() {},
rightSkill: func() {},
hpGlobe: func() {},
manaGlobe: func() {},
}
onHover, found := hoverMap[item]
if !found {
g.logger.Error(fmt.Sprintf("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.hud.skillSelectMenu.ToggleLeftPanel()
},
newStats: func() {
g.logger.Info("New Stats Selector Action Pressed")
},
xp: func() {
g.logger.Info("XP Action Pressed")
},
walkRun: func() {
g.logger.Info("Walk/Run Action Pressed")
},
stamina: func() {
g.logger.Info("Stamina Action Pressed")
},
newSkills: func() {
g.logger.Info("New Skills Selector Action Pressed")
},
rightSkill: func() {
g.hud.skillSelectMenu.ToggleRightPanel()
},
hpGlobe: func() {
g.ToggleHpStats()
g.logger.Info("HP Globe Pressed")
},
manaGlobe: func() {
g.ToggleManaStats()
g.logger.Info("Mana Globe Pressed")
},
}
action, found := actionMap[item]
if !found {
// Warning, because some action types are still todo, and could return this error
g.logger.Warning(fmt.Sprintf("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
}
if skill, ok := g.hero.Skills[skillDetailRecord.ID]; ok {
skill.SkillPoints++
learnedSkillsCount++
} else {
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.hud.skillSelectMenu.RegenerateImageCache()
g.logger.Info(fmt.Sprintf("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.hud.skillSelectMenu.RegenerateImageCache()
g.logger.Info("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
}