OpenDiablo2/d2game/d2player/escape_menu.go

630 lines
16 KiB
Go

package d2player
import (
"time"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2util"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2gui"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2ui"
)
type (
layoutID int
optionID int
)
const (
// UI
labelGutter = 10
sidePanelsSize = 80
pentSize = 54
menuSize = 500
spacerWidth = 10
// layouts
noLayoutID layoutID = iota - 2
saveLayoutID
mainLayoutID
optionsLayoutID
soundOptionsLayoutID
videoOptionsLayoutID
automapOptionsLayoutID
configureControlsLayoutID
// audio
optAudioSoundVolume optionID = iota
optAudioMusicVolume
optAudio3dSound
optAudioHardwareAcceleration
optAudioEnvEffects
optAudioNpcSpeech
// video
optVideoResolution
optVideoLightingQuality
optVideoBlendedShadows
optVideoPerspective
optVideoGamma
optVideoContrast
// automap
optAutomapSize
optAutomapFade
optAutomapCenterWhenCleared
optAutomapShowParty
optAutomapShowNames
)
const (
singleFrame = time.Millisecond * 16
)
// NewEscapeMenu creates a new escape menu
func NewEscapeMenu(navigator d2interface.Navigator,
renderer d2interface.Renderer,
audioProvider d2interface.AudioProvider,
uiManager *d2ui.UIManager,
guiManager *d2gui.GuiManager,
assetManager *d2asset.AssetManager,
l d2util.LogLevel,
keyMap *KeyMap,
) *EscapeMenu {
m := &EscapeMenu{
audioProvider: audioProvider,
renderer: renderer,
navigator: navigator,
guiManager: guiManager,
assetManager: assetManager,
keyMap: keyMap,
}
keyBindingMenu := NewKeyBindingMenu(assetManager, renderer, uiManager, guiManager, keyMap, l, m)
m.keyBindingMenu = keyBindingMenu
m.layouts = make(map[layoutID]*layout)
m.layouts[mainLayoutID] = m.newMainLayout()
m.layouts[optionsLayoutID] = m.newOptionsLayout()
m.layouts[soundOptionsLayoutID] = m.newSoundOptionsLayout()
m.layouts[videoOptionsLayoutID] = m.newVideoOptionsLayout()
m.layouts[automapOptionsLayoutID] = m.newAutomapOptionsLayout()
m.Logger = d2util.NewLogger()
m.Logger.SetLevel(l)
m.Logger.SetPrefix(logPrefix)
return m
}
// EscapeMenu represents the in-game menu that shows up when the esc key is pressed
type EscapeMenu struct {
isOpen bool
selectSound d2interface.SoundEffect
currentLayout layoutID
// leftPent and rightPent are generated once and shared between the layouts
leftPent *d2gui.AnimatedSprite
rightPent *d2gui.AnimatedSprite
layouts map[layoutID]*layout
renderer d2interface.Renderer
audioProvider d2interface.AudioProvider
navigator d2interface.Navigator
guiManager *d2gui.GuiManager
assetManager *d2asset.AssetManager
keyMap *KeyMap
keyBindingMenu *KeyBindingMenu
onCloseCb func()
*d2util.Logger
}
type layout struct {
*d2gui.Layout
leftPent *d2gui.AnimatedSprite
rightPent *d2gui.AnimatedSprite
actionableElements []actionableElement
currentEl int
rendered bool
isRaw bool
}
func (l *layout) Trigger() {
// noop
}
type showLayoutLabel struct {
*d2gui.Label
target layoutID
showLayout func(id layoutID)
}
func (l *showLayoutLabel) Trigger() {
l.showLayout(l.target)
}
type enumLabel struct {
*d2gui.Layout
textChangingLabel *d2gui.Label
optionID optionID
values []string
current int
playSound func()
updateValue func(optID optionID, value string)
*EscapeMenu
}
func (l *enumLabel) Trigger() {
l.playSound()
next := (l.current + 1) % len(l.values)
l.current = next
currentValue := l.values[l.current]
if err := l.textChangingLabel.SetText(currentValue); err != nil {
l.EscapeMenu.Errorf("could not change the label text to: %s", currentValue)
}
l.updateValue(l.optionID, currentValue)
}
type actionableElement interface {
GetOffset() (int, int)
Trigger()
}
func (m *EscapeMenu) newMainLayout() *layout {
return m.wrapLayout(func(l *layout) {
m.addBigSelectionLabel(l, "OPTIONS", optionsLayoutID)
m.addBigSelectionLabel(l, "SAVE AND EXIT GAME", saveLayoutID)
m.addBigSelectionLabel(l, "RETURN TO GAME", noLayoutID)
})
}
func (m *EscapeMenu) newOptionsLayout() *layout {
return m.wrapLayout(func(l *layout) {
m.addBigSelectionLabel(l, "SOUND OPTIONS", soundOptionsLayoutID)
m.addBigSelectionLabel(l, "VIDEO OPTIONS", videoOptionsLayoutID)
m.addBigSelectionLabel(l, "AUTOMAP OPTIONS", automapOptionsLayoutID)
m.addBigSelectionLabel(l, "CONFIGURE CONTROLS", configureControlsLayoutID)
m.addBigSelectionLabel(l, "PREVIOUS MENU", mainLayoutID)
})
}
func (m *EscapeMenu) newSoundOptionsLayout() *layout {
return m.wrapLayout(func(l *layout) {
m.addTitle(l, "SOUND OPTIONS")
m.addEnumLabel(l, optAudioSoundVolume, "SOUND", []string{"TODO"})
m.addEnumLabel(l, optAudioMusicVolume, "MUSIC", []string{"TODO"})
m.addEnumLabel(l, optAudio3dSound, "3D BIAS", []string{"TODO"})
m.addEnumLabel(l, optAudioHardwareAcceleration, "HARDWARE ACCELERATION", []string{"ON", "OFF"})
m.addEnumLabel(l, optAudioEnvEffects, "ENVIRONMENTAL EFFECTS", []string{"ON", "OFF"})
m.addEnumLabel(l, optAudioNpcSpeech, "NPC SPEECH", []string{"AUDIO AND TEXT", "AUDIO ONLY", "TEXT ONLY"})
m.addPreviousMenuLabel(l)
})
}
func (m *EscapeMenu) newVideoOptionsLayout() *layout {
return m.wrapLayout(func(l *layout) {
m.addTitle(l, "VIDEO OPTIONS")
m.addEnumLabel(l, optVideoResolution, "VIDEO RESOLUTION", []string{"800X600", "1024X768"})
m.addEnumLabel(l, optVideoLightingQuality, "LIGHTING QUALITY", []string{"LOW", "HIGH"})
m.addEnumLabel(l, optVideoBlendedShadows, "BLENDED SHADOWS", []string{"ON", "OFF"})
m.addEnumLabel(l, optVideoPerspective, "PERSPECTIVE", []string{"ON", "OFF"})
m.addEnumLabel(l, optVideoGamma, "GAMMA", []string{"TODO"})
m.addEnumLabel(l, optVideoContrast, "CONTRAST", []string{"TODO"})
m.addPreviousMenuLabel(l)
})
}
func (m *EscapeMenu) newAutomapOptionsLayout() *layout {
return m.wrapLayout(func(l *layout) {
m.addTitle(l, "AUTOMAP OPTIONS")
m.addEnumLabel(l, optAutomapSize, "AUTOMAP SIZE", []string{"FULL SCREEN"})
m.addEnumLabel(l, optAutomapFade, "FADE", []string{"YES", "NO"})
m.addEnumLabel(l, optAutomapCenterWhenCleared, "CENTER WHEN CLEARED", []string{"YES", "NO"})
m.addEnumLabel(l, optAutomapShowParty, "SHOW PARTY", []string{"YES", "NO"})
m.addEnumLabel(l, optAutomapShowNames, "SHOW NAMES", []string{"YES", "NO"})
m.addPreviousMenuLabel(l)
})
}
func (m *EscapeMenu) newConfigureControlsLayout(keyBindingMenu *KeyBindingMenu) *layout {
return &layout{
Layout: keyBindingMenu.GetLayout(),
isRaw: true,
}
}
func (m *EscapeMenu) wrapLayout(fn func(*layout)) *layout {
wrapper := d2gui.CreateLayout(m.renderer, d2gui.PositionTypeHorizontal, m.assetManager)
wrapper.SetVerticalAlign(d2gui.VerticalAlignMiddle)
wrapper.AddSpacerDynamic()
center := wrapper.AddLayout(d2gui.PositionTypeHorizontal)
center.SetSize(menuSize, 0)
left := center.AddLayout(d2gui.PositionTypeVertical)
left.SetSize(sidePanelsSize, pentSize)
leftPent, err := left.AddAnimatedSprite(d2resource.PentSpin, d2resource.PaletteUnits, d2gui.DirectionBackward)
if err != nil {
m.Error(err.Error())
return nil
}
m.leftPent = leftPent
// wrap the base layout so we can pass values around more easily
base := &layout{}
baseLayout := center.AddLayout(d2gui.PositionTypeVertical)
baseLayout.SetHorizontalAlign(d2gui.HorizontalAlignCenter)
base.Layout = baseLayout
fn(base)
right := center.AddLayout(d2gui.PositionTypeHorizontal)
// For some reason, aligning the panel to the right won't align the pentagram, so we need to add a static spacer.
right.AddSpacerStatic(sidePanelsSize-pentSize, 0)
right.SetSize(sidePanelsSize, pentSize)
rightPent, err := right.AddAnimatedSprite(d2resource.PentSpin, d2resource.PaletteUnits, d2gui.DirectionForward)
if err != nil {
m.Error(err.Error())
return nil
}
m.rightPent = rightPent
wrapper.AddSpacerDynamic()
return &layout{
Layout: wrapper,
leftPent: leftPent,
rightPent: rightPent,
actionableElements: base.actionableElements,
}
}
func (m *EscapeMenu) addTitle(l *layout, text string) {
_, err := l.AddLabel(text, d2gui.FontStyle42Units)
if err != nil {
m.Errorf("could not add label: %s to the escape menu", text)
}
l.AddSpacerStatic(spacerWidth, labelGutter)
}
func (m *EscapeMenu) addBigSelectionLabel(l *layout, text string, targetLayout layoutID) {
guiLabel, err := l.AddLabel(text, d2gui.FontStyle42Units)
if err != nil {
m.Error(err.Error())
}
label := &showLayoutLabel{Label: guiLabel, target: targetLayout, showLayout: m.showLayout}
label.SetMouseClickHandler(func(_ d2interface.MouseEvent) {
label.Trigger()
})
elID := len(l.actionableElements)
label.SetMouseEnterHandler(func(_ d2interface.MouseMoveEvent) {
m.onHoverElement(elID)
})
l.AddSpacerStatic(spacerWidth, labelGutter)
l.actionableElements = append(l.actionableElements, label)
}
func (m *EscapeMenu) addPreviousMenuLabel(l *layout) {
l.AddSpacerStatic(spacerWidth, labelGutter)
guiLabel, err := l.AddLabel("PREVIOUS MENU", d2gui.FontStyle30Units)
if err != nil {
m.Error(err.Error())
}
label := &showLayoutLabel{Label: guiLabel, target: optionsLayoutID, showLayout: m.showLayout}
label.SetMouseClickHandler(func(_ d2interface.MouseEvent) {
label.Trigger()
})
elID := len(l.actionableElements)
label.SetMouseEnterHandler(func(_ d2interface.MouseMoveEvent) {
m.onHoverElement(elID)
})
l.actionableElements = append(l.actionableElements, label)
}
func (m *EscapeMenu) addEnumLabel(l *layout, optID optionID, text string, values []string) {
guiLayout := l.AddLayout(d2gui.PositionTypeHorizontal)
layout := &layout{Layout: guiLayout}
layout.SetSize(menuSize, 0)
_, err := layout.AddLabel(text, d2gui.FontStyle30Units)
if err != nil {
m.Errorf("could not add label: %s to the escape menu", text)
}
elID := len(l.actionableElements)
layout.SetMouseEnterHandler(func(_ d2interface.MouseMoveEvent) {
m.onHoverElement(elID)
})
layout.AddSpacerDynamic()
guiLabel, err := layout.AddLabel(values[0], d2gui.FontStyle30Units)
if err != nil {
m.Error(err.Error())
}
label := &enumLabel{
Layout: guiLayout,
textChangingLabel: guiLabel,
optionID: optID,
values: values,
current: 0,
playSound: m.playSound,
updateValue: m.onUpdateValue,
}
layout.SetMouseClickHandler(func(_ d2interface.MouseEvent) {
label.Trigger()
})
l.AddSpacerStatic(spacerWidth, labelGutter)
l.actionableElements = append(l.actionableElements, label)
}
// OnLoad loads the necessary files for the escape menu
func (m *EscapeMenu) OnLoad() {
var err error
err = m.keyBindingMenu.Load()
if err != nil {
m.Errorf("unable to load the configure controls window: %v", err.Error())
}
m.layouts[configureControlsLayoutID] = m.newConfigureControlsLayout(m.keyBindingMenu)
m.selectSound, err = m.audioProvider.LoadSound(d2resource.SFXCursorSelect, false, false)
if err != nil {
m.Error(err.Error())
}
}
// OnEscKey is called when the escape key is pressed
func (m *EscapeMenu) OnEscKey() {
if m.currentLayout == configureControlsLayoutID {
m.setLayout(optionsLayoutID)
if err := m.keyBindingMenu.Close(); err != nil {
m.Errorf("unable to close the configure controls menu: %v", err.Error())
}
} else {
m.close()
}
}
// SetOnCloseCb sets the callback that is run when close() is called
func (m *EscapeMenu) SetOnCloseCb(cb func()) {
m.onCloseCb = cb
}
func (m *EscapeMenu) close() {
m.isOpen = false
m.guiManager.SetLayout(nil)
m.onCloseCb()
}
func (m *EscapeMenu) open() {
m.isOpen = true
m.setLayout(mainLayoutID)
}
func (m *EscapeMenu) playSound() {
m.selectSound.Play()
}
func (m *EscapeMenu) showLayout(id layoutID) {
m.playSound()
if id == noLayoutID {
m.close()
return
}
if id == saveLayoutID {
m.navigator.ToMainMenu()
return
}
m.setLayout(id)
if id == configureControlsLayoutID {
m.keyBindingMenu.Open()
} else if err := m.keyBindingMenu.Close(); err != nil {
m.Errorf("unable to close the configure controls menu: %v", err.Error())
}
}
func (m *EscapeMenu) onHoverElement(id int) {
_, y := m.layouts[m.currentLayout].actionableElements[id].GetOffset()
m.layouts[m.currentLayout].currentEl = id
x, _ := m.leftPent.GetPosition()
m.leftPent.SetPosition(x, y+spacerWidth)
x, _ = m.rightPent.GetPosition()
m.rightPent.SetPosition(x, y+spacerWidth)
}
func (m *EscapeMenu) onUpdateValue(optID optionID, value string) {
m.Infof("updating value %d with %s", optID, value)
}
func (m *EscapeMenu) setLayout(id layoutID) {
layout := m.layouts[id]
if layout.isRaw {
m.guiManager.SetLayout(layout.Layout)
m.layouts[id].rendered = true
m.currentLayout = id
return
}
m.leftPent = m.layouts[id].leftPent
m.rightPent = m.layouts[id].rightPent
m.currentLayout = id
m.layouts[id].currentEl = len(m.layouts[id].actionableElements) - 1 // default to Previous Menu
m.guiManager.SetLayout(m.layouts[id].Layout)
// when first rendering a layout, widgets don't have offsets so we hide pentagrams for a frame
if !m.layouts[id].rendered {
m.layouts[id].rendered = true
m.leftPent.SetVisible(false)
m.rightPent.SetVisible(false)
go func() {
time.Sleep(singleFrame)
m.onHoverElement(m.layouts[id].currentEl)
m.leftPent.SetVisible(true)
m.rightPent.SetVisible(true)
}()
} else {
m.onHoverElement(m.layouts[id].currentEl)
}
}
func (m *EscapeMenu) onUpKey() {
if !m.isOpen {
return
}
if m.layouts[m.currentLayout].currentEl == 0 {
return
}
m.layouts[m.currentLayout].currentEl--
m.onHoverElement(m.layouts[m.currentLayout].currentEl)
}
func (m *EscapeMenu) onDownKey() {
if !m.isOpen {
return
}
if m.layouts[m.currentLayout].currentEl == len(m.layouts[m.currentLayout].actionableElements)-1 {
return
}
m.layouts[m.currentLayout].currentEl++
m.onHoverElement(m.layouts[m.currentLayout].currentEl)
}
func (m *EscapeMenu) onEnterKey() {
if !m.isOpen {
return
}
m.layouts[m.currentLayout].actionableElements[m.layouts[m.currentLayout].currentEl].Trigger()
}
// IsOpen returns whether the escape menu is open (visible) or not
func (m *EscapeMenu) IsOpen() bool {
return m.isOpen
}
// Advance computes the state of the elements of the menu overtime
func (m *EscapeMenu) Advance(elapsed float64) error {
if m.keyBindingMenu != nil {
if err := m.keyBindingMenu.Advance(elapsed); err != nil {
return err
}
}
return nil
}
// Render will render the escape menu on the target surface
func (m *EscapeMenu) Render(target d2interface.Surface) error {
if m.isOpen {
if err := m.keyBindingMenu.Render(target); err != nil {
return err
}
}
return nil
}
// OnMouseButtonDown triggers whnever a mous button is pressed
func (m *EscapeMenu) OnMouseButtonDown(event d2interface.MouseEvent) bool {
if !m.isOpen {
return false
}
if m.currentLayout == configureControlsLayoutID {
if err := m.keyBindingMenu.onMouseButtonDown(event); err != nil {
m.Errorf("unable to handle mouse down on configure controls menu: %v", err.Error())
}
}
return false
}
// OnMouseButtonUp triggers whenever a mouse button is released
func (m *EscapeMenu) OnMouseButtonUp(event d2interface.MouseEvent) bool {
if !m.isOpen {
return false
}
if m.currentLayout == configureControlsLayoutID {
m.keyBindingMenu.onMouseButtonUp()
}
return false
}
// OnMouseMove triggers whenever the mouse moves within the renderer
func (m *EscapeMenu) OnMouseMove(event d2interface.MouseMoveEvent) bool {
if !m.isOpen {
return false
}
if m.currentLayout == configureControlsLayoutID {
m.keyBindingMenu.onMouseMove(event)
}
return false
}
// OnKeyDown defines the actions of the Escape Menu when a key is pressed
func (m *EscapeMenu) OnKeyDown(event d2interface.KeyEvent) bool {
if m.keyBindingMenu.IsOpen() {
if err := m.keyBindingMenu.OnKeyDown(event); err != nil {
m.Errorf("unable to handle key down on configure controls menu: %v", err.Error())
}
return false
}
switch event.Key() {
case d2enum.KeyUp:
m.onUpKey()
case d2enum.KeyDown:
m.onDownKey()
case d2enum.KeyEnter:
m.onEnterKey()
default:
return false
}
return true
}