1
1
mirror of https://github.com/OpenDiablo2/OpenDiablo2 synced 2024-06-19 21:55:24 +00:00
OpenDiablo2/d2game/d2player/game_controls.go

1131 lines
27 KiB
Go
Raw Normal View History

package d2player
import (
"fmt"
2020-12-21 20:46:58 +00:00
"strconv"
"strings"
"time"
2020-09-12 20:25:09 +00: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/d2hero"
2020-06-29 01:40:52 +00:00
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset"
2020-06-21 22:40:37 +00: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 18:52:54 +00: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 20:08:43 +00: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 20:08:43 +00: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 22:01:33 +00: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 20:51:30 +00: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,
keyMap *KeyMap,
2020-12-16 14:08:39 +00:00
audioProvider d2interface.AudioProvider,
l d2util.LogLevel,
isSinglePlayer bool,
) (*GameControls, error) {
var inventoryRecordKey string
2020-07-26 18:52:54 +00: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 20:25:09 +00: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)
2020-12-16 14:08:39 +00:00
questLog := NewQuestLog(asset, ui, l, audioProvider, hero.Act)
inventory, err := NewInventory(asset, ui, l, hero.Gold, inventoryRecord)
if err != nil {
return nil, err
}
2020-12-15 19:03:24 +00:00
skilltree := newSkillTree(hero.Skills, hero.Class, hero.Stats, 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 21:52:01 +00:00
heroState, err := d2hero.NewHeroStateFactory(asset)
if err != nil {
return nil, err
}
Ui hud polishing (#938) * d2ui/tooltip: Make it invisible by default * d2ui/button: Add GetToggled() method * d2player/HUD: Add tooltip for minipanel button * d2ui/button: Add disabled frame to minipanel buttons * d2ui/widget_group: Add SetEnable method for clickable widgets * d2player/mini_panel: move menu button here from HUD * d2ui/button: toggled buttons take preference over disabled buttons * d2player/help_overlay: Make panel only use widgets * d2player/hud: Group most widgets into widget group * d2ui/custom_widget: Allow tooltip to be attached * d2player/hud: Attach staminaBar tooltip to staminaBar * d2player/hud: Attach experienceBar tooltip to experienceBar widget * d2ui/ui_manager: Always draw tooltips last * d2player/help_overlay: It should be drawn over the HUD * d2player/globeWidget: Move tooltip here from HUD * d2core/tooltip: Automatically add tooltips to the uiManager * d2core/ui_manager: Remove special handling of widgetGroups for rendering * d2player/help_overlay: Add button to widget group * d2player/hud: Attack runwalk tooltip to button * d2player/mini_panel: Add panelButton to its own widget group * d2core/widget_group: When a clickable is added, it's also added to uiManager * d2player/globeWidget: make tooltip un/lock on click * d2player/hud: Add runbutton to widget group * d2player/mini_panel: Add group for tooltips this allows us to move the tooltip with the panelbuttons. They can't be in the general panelGroup as they would all become visible when the panel is opened. * d2core/button: Remove debug log when a button with tooltip is hovered
2020-11-21 10:35:32 +00:00
helpOverlay := NewHelpOverlay(asset, ui, l, keyMap)
const blackAlpha50percent = 0x0000007f
gc := &GameControls{
asset: asset,
ui: ui,
renderer: renderer,
hero: hero,
heroState: heroState,
escapeMenu: escapeMenu,
inputListener: inputListener,
mapRenderer: mapRenderer,
inventory: inventory,
skilltree: skilltree,
heroStatsPanel: heroStatsPanel,
questLog: questLog,
HelpOverlay: helpOverlay,
keyMap: keyMap,
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 22:01:33 +00:00
lastLeftBtnActionTime: 0,
lastRightBtnActionTime: 0,
isSinglePlayer: isSinglePlayer,
}
2020-12-15 11:37:35 +00:00
hud := NewHUD(asset, ui, hero, miniPanel, actionableRegions, mapEngine, l, gc, mapRenderer)
gc.hud = hud
hoverLabel := hud.nameLabel
hoverLabel.SetBackgroundColor(d2util.Color(blackAlpha50percent))
gc.heroStatsPanel.SetOnCloseCb(gc.onCloseHeroStatsPanel)
gc.questLog.SetOnCloseCb(gc.onCloseQuestLog)
gc.inventory.SetOnCloseCb(gc.onCloseInventory)
gc.skilltree.SetOnCloseCb(gc.onCloseSkilltree)
Ui hud polishing (#938) * d2ui/tooltip: Make it invisible by default * d2ui/button: Add GetToggled() method * d2player/HUD: Add tooltip for minipanel button * d2ui/button: Add disabled frame to minipanel buttons * d2ui/widget_group: Add SetEnable method for clickable widgets * d2player/mini_panel: move menu button here from HUD * d2ui/button: toggled buttons take preference over disabled buttons * d2player/help_overlay: Make panel only use widgets * d2player/hud: Group most widgets into widget group * d2ui/custom_widget: Allow tooltip to be attached * d2player/hud: Attach staminaBar tooltip to staminaBar * d2player/hud: Attach experienceBar tooltip to experienceBar widget * d2ui/ui_manager: Always draw tooltips last * d2player/help_overlay: It should be drawn over the HUD * d2player/globeWidget: Move tooltip here from HUD * d2core/tooltip: Automatically add tooltips to the uiManager * d2core/ui_manager: Remove special handling of widgetGroups for rendering * d2player/help_overlay: Add button to widget group * d2player/hud: Attack runwalk tooltip to button * d2player/mini_panel: Add panelButton to its own widget group * d2core/widget_group: When a clickable is added, it's also added to uiManager * d2player/globeWidget: make tooltip un/lock on click * d2player/hud: Add runbutton to widget group * d2player/mini_panel: Add group for tooltips this allows us to move the tooltip with the panelbuttons. They can't be in the general panelGroup as they would all become visible when the panel is opened. * d2core/button: Remove debug log when a button with tooltip is hovered
2020-11-21 10:35:32 +00:00
gc.escapeMenu.SetOnCloseCb(gc.hud.miniPanel.restoreDisabled)
gc.HelpOverlay.SetOnCloseCb(gc.hud.miniPanel.restoreDisabled)
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 18:52:54 +00: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
questLog *QuestLog
HelpOverlay *HelpOverlay
bottomMenuRect *d2geom.Rectangle
leftMenuRect *d2geom.Rectangle
rightMenuRect *d2geom.Rectangle
lastMouseX int
lastMouseY int
lastLeftBtnActionTime float64
lastRightBtnActionTime float64
FreeCam bool
isSinglePlayer bool
*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 22:01:33 +00: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 22:01:33 +00: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.questLog.Close()
g.HelpOverlay.Close()
g.updateLayout()
case d2enum.ToggleInventoryPanel:
g.toggleInventoryPanel()
case d2enum.ToggleSkillTreePanel:
g.toggleSkilltreePanel()
case d2enum.ToggleCharacterPanel:
g.toggleHeroStatsPanel()
case d2enum.ToggleQuestLog:
g.toggleQuestLog()
case d2enum.ToggleRunWalk:
g.hud.onToggleRunButton(false)
case d2enum.HoldRun:
g.hud.onToggleRunButton(true)
case d2enum.ToggleHelpScreen:
2020-12-08 08:18:27 +00:00
g.toggleHelpOverlay()
default:
return false
}
2020-08-11 22:01:33 +00: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() {
if g.inventory.moveGoldPanel.IsOpen() {
g.inventory.moveGoldPanel.Close()
return
}
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.questLog.IsOpen() {
g.questLog.Close()
escHandled = true
}
if g.HelpOverlay.IsOpen() {
2020-12-08 08:18:27 +00:00
g.HelpOverlay.Close()
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 22:01:33 +00: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 22:01:33 +00: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 22:01:33 +00:00
g.lastLeftBtnActionTime = now
2020-07-26 18:52:54 +00: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 22:01:33 +00:00
g.lastRightBtnActionTime = now
2020-07-26 18:52:54 +00:00
g.inputListener.OnPlayerCast(g.hero.RightSkill.ID, px, py)
2020-07-26 18:52:54 +00:00
return true
}
return true
}
2020-08-11 22:01:33 +00: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 22:01:33 +00:00
// OnMouseButtonDown handles mouse button presses
func (g *GameControls) OnMouseButtonDown(event d2interface.MouseEvent) bool {
mx, my := event.X(), event.Y()
2020-07-26 18:52:54 +00: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 18:52:54 +00:00
if event.KeyMod() == d2enum.KeyModShift {
g.inputListener.OnPlayerCast(g.hero.LeftSkill.ID, px, py)
} else {
g.inputListener.OnPlayerMove(px, py)
}
2020-07-26 18:52:54 +00:00
return true
}
if event.Button() == d2enum.MouseButtonRight && !g.isInActiveMenusRect(mx, my) && !g.hero.IsCasting() {
g.lastRightBtnActionTime = d2util.Now()
2020-07-26 18:52:54 +00:00
g.inputListener.OnPlayerCast(g.hero.RightSkill.ID, px, py)
2020-07-26 18:52:54 +00:00
return true
}
return false
}
func (g *GameControls) toggleHeroStatsPanel() {
2020-12-08 08:18:27 +00:00
if !g.HelpOverlay.IsOpen() {
g.questLog.Close()
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) toggleQuestLog() {
2020-12-08 08:18:27 +00:00
if !g.HelpOverlay.IsOpen() {
g.heroStatsPanel.Close()
g.questLog.Toggle()
g.hud.miniPanel.SetMovedRight(g.questLog.IsOpen())
g.updateLayout()
}
}
func (g *GameControls) onCloseQuestLog() {
g.hud.miniPanel.SetMovedRight(g.questLog.IsOpen())
g.updateLayout()
}
2020-12-08 08:18:27 +00:00
func (g *GameControls) toggleHelpOverlay() {
if !g.inventory.IsOpen() && !g.skilltree.IsOpen() && !g.heroStatsPanel.IsOpen() && !g.questLog.IsOpen() {
2020-12-14 17:29:54 +00:00
g.HelpOverlay.updateKeyMap(g.keyMap)
2020-12-08 08:18:27 +00:00
g.hud.miniPanel.openDisabled()
g.HelpOverlay.Toggle()
g.updateLayout()
}
}
func (g *GameControls) toggleInventoryPanel() {
2020-12-08 08:18:27 +00:00
if !g.HelpOverlay.IsOpen() {
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() {
2020-12-08 08:18:27 +00:00
if !g.HelpOverlay.IsOpen() {
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.questLog.Close()
Ui hud polishing (#938) * d2ui/tooltip: Make it invisible by default * d2ui/button: Add GetToggled() method * d2player/HUD: Add tooltip for minipanel button * d2ui/button: Add disabled frame to minipanel buttons * d2ui/widget_group: Add SetEnable method for clickable widgets * d2player/mini_panel: move menu button here from HUD * d2ui/button: toggled buttons take preference over disabled buttons * d2player/help_overlay: Make panel only use widgets * d2player/hud: Group most widgets into widget group * d2ui/custom_widget: Allow tooltip to be attached * d2player/hud: Attach staminaBar tooltip to staminaBar * d2player/hud: Attach experienceBar tooltip to experienceBar widget * d2ui/ui_manager: Always draw tooltips last * d2player/help_overlay: It should be drawn over the HUD * d2player/globeWidget: Move tooltip here from HUD * d2core/tooltip: Automatically add tooltips to the uiManager * d2core/ui_manager: Remove special handling of widgetGroups for rendering * d2player/help_overlay: Add button to widget group * d2player/hud: Attack runwalk tooltip to button * d2player/mini_panel: Add panelButton to its own widget group * d2core/widget_group: When a clickable is added, it's also added to uiManager * d2player/globeWidget: make tooltip un/lock on click * d2player/hud: Add runbutton to widget group * d2player/mini_panel: Add group for tooltips this allows us to move the tooltip with the panelbuttons. They can't be in the general panelGroup as they would all become visible when the panel is opened. * d2core/button: Remove debug log when a button with tooltip is hovered
2020-11-21 10:35:32 +00:00
g.hud.miniPanel.closeDisabled()
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.questLog.Load()
g.HelpOverlay.Load()
2020-12-15 11:37:35 +00:00
g.loadAddButtons()
2020-12-15 17:02:52 +00:00
g.setAddButtons()
2020-12-15 11:37:35 +00:00
miniPanelActions := &miniPanelActions{
characterToggle: g.toggleHeroStatsPanel,
inventoryToggle: g.toggleInventoryPanel,
skilltreeToggle: g.toggleSkilltreePanel,
menuToggle: g.openEscMenu,
questToggle: g.toggleQuestLog,
}
g.hud.miniPanel.load(miniPanelActions)
}
2020-08-11 22:01:33 +00:00
// Advance advances the state of the GameControls
func (g *GameControls) Advance(elapsed float64) error {
g.mapRenderer.Advance(elapsed)
Ui hud polishing (#938) * d2ui/tooltip: Make it invisible by default * d2ui/button: Add GetToggled() method * d2player/HUD: Add tooltip for minipanel button * d2ui/button: Add disabled frame to minipanel buttons * d2ui/widget_group: Add SetEnable method for clickable widgets * d2player/mini_panel: move menu button here from HUD * d2ui/button: toggled buttons take preference over disabled buttons * d2player/help_overlay: Make panel only use widgets * d2player/hud: Group most widgets into widget group * d2ui/custom_widget: Allow tooltip to be attached * d2player/hud: Attach staminaBar tooltip to staminaBar * d2player/hud: Attach experienceBar tooltip to experienceBar widget * d2ui/ui_manager: Always draw tooltips last * d2player/help_overlay: It should be drawn over the HUD * d2player/globeWidget: Move tooltip here from HUD * d2core/tooltip: Automatically add tooltips to the uiManager * d2core/ui_manager: Remove special handling of widgetGroups for rendering * d2player/help_overlay: Add button to widget group * d2player/hud: Attack runwalk tooltip to button * d2player/mini_panel: Add panelButton to its own widget group * d2core/widget_group: When a clickable is added, it's also added to uiManager * d2player/globeWidget: make tooltip un/lock on click * d2player/hud: Add runbutton to widget group * d2player/mini_panel: Add group for tooltips this allows us to move the tooltip with the panelbuttons. They can't be in the general panelGroup as they would all become visible when the panel is opened. * d2core/button: Remove debug log when a button with tooltip is hovered
2020-11-21 10:35:32 +00:00
g.hud.Advance(elapsed)
g.inventory.Advance(elapsed)
2020-12-16 14:08:39 +00:00
g.questLog.Advance(elapsed)
if err := g.escapeMenu.Advance(elapsed); err != nil {
return err
}
2020-12-15 17:02:52 +00:00
if g.heroStatsPanel.IsOpen() || g.skilltree.IsOpen() {
g.setAddButtons()
}
return nil
}
func (g *GameControls) updateLayout() {
isRightPanelOpen := g.isLeftPanelOpen()
isLeftPanelOpen := g.isRightPanelOpen()
2020-08-11 22:01:33 +00:00
switch {
case isRightPanelOpen == isLeftPanelOpen:
g.mapRenderer.ViewportDefault()
2020-08-11 22:01:33 +00:00
case isRightPanelOpen:
g.mapRenderer.ViewportToLeft()
2020-08-11 22:01:33 +00:00
default:
g.mapRenderer.ViewportToRight()
}
}
func (g *GameControls) isLeftPanelOpen() bool {
// https://github.com/OpenDiablo2/OpenDiablo2/issues/801
return g.heroStatsPanel.IsOpen() || g.questLog.IsOpen() || g.inventory.moveGoldPanel.IsOpen()
}
func (g *GameControls) isRightPanelOpen() bool {
return g.inventory.IsOpen() || g.skilltree.IsOpen()
}
2020-08-11 22:01:33 +00: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 22:01:33 +00:00
// Render draws the GameControls onto the target
2020-07-26 18:52:54 +00: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 22:01:33 +00:00
// SetZoneChangeText sets the zoneChangeText
func (g *GameControls) SetZoneChangeText(text string) {
g.hud.zoneChangeText.SetText(text)
}
2020-08-11 22:01:33 +00:00
// ShowZoneChangeText shows the zoneChangeText
func (g *GameControls) ShowZoneChangeText() {
g.hud.isZoneTextShown = true
}
2020-08-11 22:01:33 +00: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.Errorf("Unrecognized actionableType(%d) being hovered", 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.Info("New Stats Selector Action Pressed")
},
xp: func() {
g.Info("XP Action Pressed")
},
walkRun: func() {
g.Info("Walk/Run Action Pressed")
},
stamina: func() {
g.Info("Stamina Action Pressed")
},
newSkills: func() {
g.Info("New Skills Selector Action Pressed")
},
rightSkill: func() {
g.hud.skillSelectMenu.ToggleRightPanel()
},
hpGlobe: func() {
g.ToggleHpStats()
g.Info("HP Globe Pressed")
},
manaGlobe: func() {
g.ToggleManaStats()
g.Info("Mana Globe Pressed")
},
}
action, found := actionMap[item]
if !found {
// Warning, because some action types are still todo, and could return this error
g.Warningf("Unrecognized actionableType(%d) being clicked", item)
return
}
action()
}
2020-12-21 20:46:58 +00:00
func (g *GameControls) bindTerminalCommands(term d2interface.Terminal) error {
if err := term.Bind("freecam", "toggle free camera movement", nil, g.commandFreeCam); err != nil {
return err
}
if err := term.Bind("setleftskill", "set skill to fire on left click", []string{"id"}, g.commandSetLeftSkill(term)); err != nil {
return err
}
if err := term.Bind("setrightskill", "set skill to fire on right click", []string{"id"}, g.commandSetRightSkill(term)); err != nil {
return err
}
if err := term.Bind("learnskills", "learn all skills for the a given class", []string{"token"}, g.commandLearnSkills(term)); err != nil {
return err
}
if err := term.Bind("learnskillid", "learn a skill by a given ID", []string{"id"}, g.commandLearnSkillID(term)); err != nil {
return err
}
return nil
}
// UnbindTerminalCommands unbinds commands from the terminal
func (g *GameControls) UnbindTerminalCommands(term d2interface.Terminal) error {
return term.Unbind("freecam", "setleftskill", "setrightskill", "learnskills", "learnskillid")
}
2020-12-21 20:46:58 +00:00
func (g *GameControls) setAddButtons() {
g.hud.addStatsButton.SetEnabled(g.hero.Stats.StatsPoints > 0)
g.hud.addSkillButton.SetEnabled(g.hero.Stats.SkillPoints > 0)
}
func (g *GameControls) loadAddButtons() {
g.hud.addStatsButton.OnActivated(func() { g.toggleHeroStatsPanel() })
g.hud.addSkillButton.OnActivated(func() { g.toggleSkilltreePanel() })
}
func (g *GameControls) commandFreeCam([]string) error {
g.FreeCam = !g.FreeCam
return nil
}
func (g *GameControls) commandSetLeftSkill(term d2interface.Terminal) func(args []string) error {
return func(args []string) error {
id, err := strconv.Atoi(args[0])
if err != nil {
term.Errorf("invalid argument")
return nil
}
skillRecord := g.asset.Records.Skill.Details[id]
skill, err := g.heroState.CreateHeroSkill(1, skillRecord.Skill)
if err != nil {
2020-12-21 20:46:58 +00:00
term.Errorf("cannot create skill with ID of %d, error: %s", id, err)
return nil
}
g.hero.LeftSkill = skill
2020-12-21 20:46:58 +00:00
return nil
}
}
2020-12-21 20:46:58 +00:00
func (g *GameControls) commandSetRightSkill(term d2interface.Terminal) func(args []string) error {
return func(args []string) error {
id, err := strconv.Atoi(args[0])
if err != nil {
term.Errorf("invalid argument")
return nil
}
skillRecord := g.asset.Records.Skill.Details[id]
skill, err := g.heroState.CreateHeroSkill(0, skillRecord.Skill)
if err != nil {
2020-12-21 20:46:58 +00:00
term.Errorf("cannot create skill with ID of %d, error: %s", id, err)
return nil
}
g.hero.RightSkill = skill
2020-12-21 20:46:58 +00:00
return nil
}
2020-12-21 20:46:58 +00:00
}
2020-12-21 20:46:58 +00:00
func (g *GameControls) commandLearnSkillID(term d2interface.Terminal) func(args []string) error {
return func(args []string) error {
id, err := strconv.Atoi(args[0])
if err != nil {
term.Errorf("invalid argument")
return nil
}
skillRecord := g.asset.Records.Skill.Details[id]
if skillRecord == nil {
term.Errorf("cannot find a skill record for ID: %d", id)
return nil
}
skill, err := g.heroState.CreateHeroSkill(1, skillRecord.Skill)
if skill == nil {
term.Errorf("cannot create skill: %s", skillRecord.Skill)
return nil
}
g.hero.Skills[skill.ID] = skill
if err != nil {
term.Errorf("cannot learn skill for class, error: %s", err)
return nil
}
g.hud.skillSelectMenu.RegenerateImageCache()
g.Infof("Learned skill: " + skill.Skill)
return nil
}
}
2020-12-21 20:46:58 +00:00
func (g *GameControls) commandLearnSkills(term d2interface.Terminal) func(args []string) error {
const classTokenLength = 3
2020-12-21 20:46:58 +00:00
return func(args []string) error {
token := args[0]
if len(token) < classTokenLength {
2020-12-21 20:46:58 +00:00
term.Errorf("The given class token should be at least 3 characters")
return nil
}
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"
2020-12-21 20:46:58 +00:00
term.Errorf(fmtInvalid, strings.Join(validPrefixes, ", "))
2020-12-21 20:46:58 +00:00
return nil
}
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.Infof("Learned %d skills", learnedSkillsCount)
if err != nil {
2020-12-21 20:46:58 +00:00
term.Errorf("cannot learn skill for class, error: %s", err)
return nil
}
2020-12-21 20:46:58 +00:00
return nil
}
2020-12-15 11:37:35 +00:00
}