1
1
mirror of https://github.com/OpenDiablo2/OpenDiablo2 synced 2024-11-18 02:16:23 -05:00

Cleaned up d2term

This commit is contained in:
Intyre 2020-12-21 21:46:58 +01:00
parent 8a55e1bd4b
commit 04ec879035
12 changed files with 673 additions and 506 deletions

View File

@ -91,12 +91,6 @@ type Options struct {
LogLevel *d2util.LogLevel LogLevel *d2util.LogLevel
} }
type bindTerminalEntry struct {
name string
description string
action interface{}
}
const ( const (
bytesToMegabyte = 1024 * 1024 bytesToMegabyte = 1024 * 1024
nSamplesTAlloc = 100 nSamplesTAlloc = 100
@ -184,11 +178,6 @@ func (a *App) loadEngine() error {
return err return err
} }
err = a.asset.BindTerminalCommands(term)
if err != nil {
return err
}
scriptEngine := d2script.CreateScriptEngine() scriptEngine := d2script.CreateScriptEngine()
uiManager := d2ui.NewUIManager(a.asset, renderer, inputManager, *a.Options.LogLevel, audio) uiManager := d2ui.NewUIManager(a.asset, renderer, inputManager, *a.Options.LogLevel, audio)
@ -351,25 +340,28 @@ func (a *App) initialize() error {
a.renderer.SetWindowIcon("d2logo.png") a.renderer.SetWindowIcon("d2logo.png")
a.terminal.BindLogger() a.terminal.BindLogger()
terminalActions := [...]bindTerminalEntry{ terminalCommands := []struct {
{"dumpheap", "dumps the heap to pprof/heap.pprof", a.dumpHeap}, name string
{"fullscreen", "toggles fullscreen", a.toggleFullScreen}, desc string
{"capframe", "captures a still frame", a.setupCaptureFrame}, args []string
{"capgifstart", "captures an animation (start)", a.startAnimationCapture}, fn func(args []string) error
{"capgifstop", "captures an animation (stop)", a.stopAnimationCapture}, }{
{"vsync", "toggles vsync", a.toggleVsync}, {"dumpheap", "dumps the heap to pprof/heap.pprof", nil, a.dumpHeap},
{"fps", "toggle fps counter", a.toggleFpsCounter}, {"fullscreen", "toggles fullscreen", nil, a.toggleFullScreen},
{"timescale", "set scalar for elapsed time", a.setTimeScale}, {"capframe", "captures a still frame", []string{"filename"}, a.setupCaptureFrame},
{"quit", "exits the game", a.quitGame}, {"capgifstart", "captures an animation (start)", []string{"filename"}, a.startAnimationCapture},
{"screen-gui", "enters the gui playground screen", a.enterGuiPlayground}, {"capgifstop", "captures an animation (stop)", nil, a.stopAnimationCapture},
{"js", "eval JS scripts", a.evalJS}, {"vsync", "toggles vsync", nil, a.toggleVsync},
{"fps", "toggle fps counter", nil, a.toggleFpsCounter},
{"timescale", "set scalar for elapsed time", []string{"float"}, a.setTimeScale},
{"quit", "exits the game", nil, a.quitGame},
{"screen-gui", "enters the gui playground screen", nil, a.enterGuiPlayground},
{"js", "eval JS scripts", []string{"code"}, a.evalJS},
} }
for idx := range terminalActions { for _, cmd := range terminalCommands {
action := &terminalActions[idx] if err := a.terminal.Bind(cmd.name, cmd.desc, cmd.args, cmd.fn); err != nil {
a.Fatalf("failed to bind action %q: %v", cmd.name, err.Error())
if err := a.terminal.BindAction(action.name, action.description, action.action); err != nil {
a.Fatal(err.Error())
} }
} }
@ -644,7 +636,7 @@ func (a *App) allocRate(totalAlloc uint64, fps float64) float64 {
return deltaAllocPerFrame * fps / bytesToMegabyte return deltaAllocPerFrame * fps / bytesToMegabyte
} }
func (a *App) dumpHeap() { func (a *App) dumpHeap([]string) error {
if _, err := os.Stat("./pprof/"); os.IsNotExist(err) { if _, err := os.Stat("./pprof/"); os.IsNotExist(err) {
if err := os.Mkdir("./pprof/", 0750); err != nil { if err := os.Mkdir("./pprof/", 0750); err != nil {
a.Fatal(err.Error()) a.Fatal(err.Error())
@ -663,48 +655,56 @@ func (a *App) dumpHeap() {
if err := fileOut.Close(); err != nil { if err := fileOut.Close(); err != nil {
a.Fatal(err.Error()) a.Fatal(err.Error())
} }
return nil
} }
func (a *App) evalJS(code string) { func (a *App) evalJS(args []string) error {
val, err := a.scriptEngine.Eval(code) val, err := a.scriptEngine.Eval(args[0])
if err != nil { if err != nil {
a.terminal.OutputErrorf("%s", err) a.terminal.Errorf(err.Error())
return return nil
} }
a.Info("%s" + val) a.Info("%s" + val)
return nil
} }
func (a *App) toggleFullScreen() { func (a *App) toggleFullScreen([]string) error {
fullscreen := !a.renderer.IsFullScreen() fullscreen := !a.renderer.IsFullScreen()
a.renderer.SetFullScreen(fullscreen) a.renderer.SetFullScreen(fullscreen)
a.terminal.OutputInfof("fullscreen is now: %v", fullscreen) a.terminal.Infof("fullscreen is now: %v", fullscreen)
return nil
} }
func (a *App) setupCaptureFrame(path string) { func (a *App) setupCaptureFrame(args []string) error {
a.captureState = captureStateFrame a.captureState = captureStateFrame
a.capturePath = path a.capturePath = args[0]
a.captureFrames = nil a.captureFrames = nil
return nil
} }
func (a *App) doCaptureFrame(target d2interface.Surface) error { func (a *App) doCaptureFrame(target d2interface.Surface) error {
fp, err := os.Create(a.capturePath) fp, err := os.Create(a.capturePath)
if err != nil { if err != nil {
a.terminal.Errorf("failed to create %q", a.capturePath)
return err return err
} }
defer func() {
if err := fp.Close(); err != nil {
a.Fatal(err.Error())
}
}()
screenshot := target.Screenshot() screenshot := target.Screenshot()
if err := png.Encode(fp, screenshot); err != nil { if err := png.Encode(fp, screenshot); err != nil {
return err return err
} }
a.Infof("saved frame to %s", a.capturePath) if err := fp.Close(); err != nil {
a.terminal.Errorf("failed to create %q", a.capturePath)
return nil
}
a.terminal.Infof("saved frame to %s", a.capturePath)
return nil return nil
} }
@ -769,42 +769,56 @@ func (a *App) convertFramesToGif() error {
return nil return nil
} }
func (a *App) startAnimationCapture(path string) { func (a *App) startAnimationCapture(args []string) error {
a.captureState = captureStateGif a.captureState = captureStateGif
a.capturePath = path a.capturePath = args[0]
a.captureFrames = nil a.captureFrames = nil
return nil
} }
func (a *App) stopAnimationCapture() { func (a *App) stopAnimationCapture([]string) error {
a.captureState = captureStateNone a.captureState = captureStateNone
return nil
} }
func (a *App) toggleVsync() { func (a *App) toggleVsync([]string) error {
vsync := !a.renderer.GetVSyncEnabled() vsync := !a.renderer.GetVSyncEnabled()
a.renderer.SetVSyncEnabled(vsync) a.renderer.SetVSyncEnabled(vsync)
a.terminal.OutputInfof("vsync is now: %v", vsync) a.terminal.Infof("vsync is now: %v", vsync)
return nil
} }
func (a *App) toggleFpsCounter() { func (a *App) toggleFpsCounter([]string) error {
a.showFPS = !a.showFPS a.showFPS = !a.showFPS
a.terminal.OutputInfof("fps counter is now: %v", a.showFPS) a.terminal.Infof("fps counter is now: %v", a.showFPS)
return nil
} }
func (a *App) setTimeScale(timeScale float64) { func (a *App) setTimeScale(args []string) error {
if timeScale <= 0 { timeScale, err := strconv.ParseFloat(args[0], 64)
a.terminal.OutputErrorf("invalid time scale value") if err != nil || timeScale <= 0 {
} else { a.terminal.Errorf("invalid time scale value")
a.terminal.OutputInfof("timescale changed from %f to %f", a.timeScale, timeScale) return nil
a.timeScale = timeScale
} }
a.terminal.Infof("timescale changed from %f to %f", a.timeScale, timeScale)
a.timeScale = timeScale
return nil
} }
func (a *App) quitGame() { func (a *App) quitGame([]string) error {
os.Exit(0) os.Exit(0)
return nil
} }
func (a *App) enterGuiPlayground() { func (a *App) enterGuiPlayground([]string) error {
a.screen.SetNextScreen(d2gamescreen.CreateGuiTestMain(a.renderer, a.guiManager, *a.Options.LogLevel, a.asset)) a.screen.SetNextScreen(d2gamescreen.CreateGuiTestMain(a.renderer, a.guiManager, *a.Options.LogLevel, a.asset))
return nil
} }
func createZeroedRing(n int) *ring.Ring { func createZeroedRing(n int) *ring.Ring {

View File

@ -13,17 +13,17 @@ type Terminal interface {
OnKeyChars(event KeyCharsEvent) bool OnKeyChars(event KeyCharsEvent) bool
Render(surface Surface) error Render(surface Surface) error
Execute(command string) error Execute(command string) error
OutputRaw(text string, category d2enum.TermCategory) Rawf(category d2enum.TermCategory, format string, params ...interface{})
Outputf(format string, params ...interface{}) Printf(format string, params ...interface{})
OutputInfof(format string, params ...interface{}) Infof(format string, params ...interface{})
OutputWarningf(format string, params ...interface{}) Warningf(format string, params ...interface{})
OutputErrorf(format string, params ...interface{}) Errorf(format string, params ...interface{})
OutputClear() Clear()
IsVisible() bool Visible() bool
Hide() Hide()
Show() Show()
BindAction(name, description string, action interface{}) error Bind(name, description string, arguments []string, fn func(args []string) error) error
UnbindAction(name string) error Unbind(name ...string) error
} }
// TerminalLogger is used tomake the Terminal write out // TerminalLogger is used tomake the Terminal write out

View File

@ -3,6 +3,7 @@ package d2asset
import ( import (
"fmt" "fmt"
"image/color" "image/color"
"strconv"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2util" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2util"
@ -409,43 +410,70 @@ func (am *AssetManager) loadDCC(path string,
// BindTerminalCommands binds the in-game terminal comands for the asset manager. // BindTerminalCommands binds the in-game terminal comands for the asset manager.
func (am *AssetManager) BindTerminalCommands(term d2interface.Terminal) error { func (am *AssetManager) BindTerminalCommands(term d2interface.Terminal) error {
if err := term.BindAction("assetspam", "display verbose asset manager logs", func(verbose bool) { if err := term.Bind("assetspam", "display verbose asset manager logs", nil, am.commandAssetSpam(term)); err != nil {
return err
}
if err := term.Bind("assetstat", "display asset manager cache statistics", nil, am.commandAssetStat(term)); err != nil {
return err
}
if err := term.Bind("assetclear", "clear asset manager cache", nil, am.commandAssetClear); err != nil {
return err
}
return nil
}
// UnbindTerminalCommands unbinds commands from the terminal
func (am *AssetManager) UnbindTerminalCommands(term d2interface.Terminal) error {
return term.Unbind("assetspam", "assetstat", "assetclear")
}
func (am *AssetManager) commandAssetSpam(term d2interface.Terminal) func([]string) error {
return func(args []string) error {
verbose, err := strconv.ParseBool(args[0])
if err != nil {
term.Errorf("asset manager verbose invalid argument")
return nil
}
if verbose { if verbose {
term.OutputInfof("asset manager verbose logging enabled") term.Infof("asset manager verbose logging enabled")
} else { } else {
term.OutputInfof("asset manager verbose logging disabled") term.Infof("asset manager verbose logging disabled")
} }
am.palettes.SetVerbose(verbose) am.palettes.SetVerbose(verbose)
am.fonts.SetVerbose(verbose) am.fonts.SetVerbose(verbose)
am.transforms.SetVerbose(verbose) am.transforms.SetVerbose(verbose)
am.animations.SetVerbose(verbose) am.animations.SetVerbose(verbose)
}); err != nil {
return err
}
if err := term.BindAction("assetstat", "display asset manager cache statistics", func() { return nil
}
}
func (am *AssetManager) commandAssetStat(term d2interface.Terminal) func([]string) error {
return func([]string) error {
var cacheStatistics = func(c d2interface.Cache) float64 { var cacheStatistics = func(c d2interface.Cache) float64 {
const percent = 100.0 const percent = 100.0
return float64(c.GetWeight()) / float64(c.GetBudget()) * percent return float64(c.GetWeight()) / float64(c.GetBudget()) * percent
} }
term.OutputInfof("palette cache: %f", cacheStatistics(am.palettes)) term.Infof("palette cache: %f", cacheStatistics(am.palettes))
term.OutputInfof("palette transform cache: %f", cacheStatistics(am.transforms)) term.Infof("palette transform cache: %f", cacheStatistics(am.transforms))
term.OutputInfof("Animation cache: %f", cacheStatistics(am.animations)) term.Infof("Animation cache: %f", cacheStatistics(am.animations))
term.OutputInfof("font cache: %f", cacheStatistics(am.fonts)) term.Infof("font cache: %f", cacheStatistics(am.fonts))
}); err != nil {
return err
}
if err := term.BindAction("assetclear", "clear asset manager cache", func() { return nil
am.palettes.Clear()
am.transforms.Clear()
am.animations.Clear()
am.fonts.Clear()
}); err != nil {
return err
} }
}
func (am *AssetManager) commandAssetClear([]string) error {
am.palettes.Clear()
am.transforms.Clear()
am.animations.Clear()
am.fonts.Clear()
return nil return nil
} }

View File

@ -3,6 +3,7 @@ package d2audio
import ( import (
"fmt" "fmt"
"math/rand" "math/rand"
"strconv"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset"
@ -73,7 +74,7 @@ func (s *Sound) SetPan(pan float64) {
// Play the sound // Play the sound
func (s *Sound) Play() { func (s *Sound) Play() {
s.Info("starting sound" + s.entry.Handle) s.Info("starting sound " + s.entry.Handle)
s.effect.Play() s.effect.Play()
if s.entry.FadeIn != 0 { if s.entry.FadeIn != 0 {
@ -103,6 +104,11 @@ func (s *Sound) Stop() {
} }
} }
// String returns the sound filename
func (s *Sound) String() string {
return s.entry.Handle
}
// SoundEngine provides functions for playing sounds // SoundEngine provides functions for playing sounds
type SoundEngine struct { type SoundEngine struct {
asset *d2asset.AssetManager asset *d2asset.AssetManager
@ -128,43 +134,25 @@ func NewSoundEngine(provider d2interface.AudioProvider,
r.Logger.SetPrefix(logPrefix) r.Logger.SetPrefix(logPrefix)
r.Logger.SetLevel(l) r.Logger.SetLevel(l)
err := term.BindAction("playsoundid", "plays the sound for a given id", func(id int) { if err := term.Bind("playsoundid", "plays the sound for a given id", []string{"id"}, r.commandPlaySoundID); err != nil {
r.PlaySoundID(id)
})
if err != nil {
r.Error(err.Error()) r.Error(err.Error())
return nil return nil
} }
err = term.BindAction("playsound", "plays the sound for a given handle string", func(handle string) { if err := term.Bind("playsound", "plays the sound for a given handle string", []string{"name"}, r.commandPlaySound); err != nil {
r.PlaySoundHandle(handle)
})
if err != nil {
r.Error(err.Error()) r.Error(err.Error())
return nil return nil
} }
err = term.BindAction("activesounds", "list currently active sounds", func() { if err := term.Bind("activesounds", "list currently active sounds", nil, r.commandActiveSounds); err != nil {
for s := range r.sounds { r.Error(err.Error())
if err != nil { return nil
r.Error(err.Error()) }
return
}
r.Info(fmt.Sprint(s)) if err := term.Bind("killsounds", "kill active sounds", nil, r.commandKillSounds); err != nil {
} r.Error(err.Error())
}) return nil
}
err = term.BindAction("killsounds", "kill active sounds", func() {
for s := range r.sounds {
if err != nil {
r.Error(err.Error())
return
}
s.Stop()
}
})
return &r return &r
} }
@ -194,6 +182,11 @@ func (s *SoundEngine) Advance(elapsed float64) {
} }
} }
// UnbindTerminalCommands unbinds commands from the terminal
func (s *SoundEngine) UnbindTerminalCommands(term d2interface.Terminal) error {
return term.Unbind("playsoundid", "playsound", "activesounds", "killsounds")
}
// Reset stop all sounds and reset state // Reset stop all sounds and reset state
func (s *SoundEngine) Reset() { func (s *SoundEngine) Reset() {
for snd := range s.sounds { for snd := range s.sounds {
@ -242,3 +235,35 @@ func (s *SoundEngine) PlaySoundHandle(handle string) *Sound {
sound := s.asset.Records.Sound.Details[handle].Index sound := s.asset.Records.Sound.Details[handle].Index
return s.PlaySoundID(sound) return s.PlaySoundID(sound)
} }
func (s *SoundEngine) commandPlaySoundID(args []string) error {
id, err := strconv.Atoi(args[0])
if err != nil {
return fmt.Errorf("invalid argument")
}
s.PlaySoundID(id)
return nil
}
func (s *SoundEngine) commandPlaySound(args []string) error {
s.PlaySoundHandle(args[0])
return nil
}
func (s *SoundEngine) commandActiveSounds([]string) error {
for sound := range s.sounds {
s.Info(sound.String())
}
return nil
}
func (s *SoundEngine) commandKillSounds([]string) error {
for sound := range s.sounds {
sound.Stop()
}
return nil
}

View File

@ -2,8 +2,10 @@ package d2maprenderer
import ( import (
"errors" "errors"
"fmt"
"image/color" "image/color"
"math" "math"
"strconv"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2ds1" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2ds1"
@ -86,20 +88,11 @@ func CreateMapRenderer(asset *d2asset.AssetManager, renderer d2interface.Rendere
result.Camera.position = &startPosition result.Camera.position = &startPosition
result.viewport.SetCamera(&result.Camera) result.viewport.SetCamera(&result.Camera)
var err error if err := term.Bind("mapdebugvis", "set map debug visualization level", nil, result.commandMapDebugVis); err != nil {
err = term.BindAction("mapdebugvis", "set map debug visualization level", func(level int) {
result.mapDebugVisLevel = level
})
if err != nil {
result.Errorf("could not bind the mapdebugvis action, err: %v", err) result.Errorf("could not bind the mapdebugvis action, err: %v", err)
} }
err = term.BindAction("entitydebugvis", "set entity debug visualization level", func(level int) { if err := term.Bind("entitydebugvis", "set entity debug visualization level", nil, result.commandEntityDebugVis); err != nil {
result.entityDebugVisLevel = level
})
if err != nil {
result.Errorf("could not bind the entitydebugvis action, err: %v", err) result.Errorf("could not bind the entitydebugvis action, err: %v", err)
} }
@ -110,6 +103,33 @@ func CreateMapRenderer(asset *d2asset.AssetManager, renderer d2interface.Rendere
return result return result
} }
// UnbindTerminalCommands unbinds commands from the terminal
func (mr *MapRenderer) UnbindTerminalCommands(term d2interface.Terminal) error {
return term.Unbind("mapdebugvis", "entitydebugvis")
}
func (mr *MapRenderer) commandMapDebugVis(args []string) error {
level, err := strconv.Atoi(args[0])
if err != nil {
return fmt.Errorf("invalid argument supplied")
}
mr.mapDebugVisLevel = level
return nil
}
func (mr *MapRenderer) commandEntityDebugVis(args []string) error {
level, err := strconv.Atoi(args[0])
if err != nil {
return fmt.Errorf("invalid argument supplied")
}
mr.entityDebugVisLevel = level
return nil
}
// RegenerateTileCache calls MapRenderer.generateTileCache(). // RegenerateTileCache calls MapRenderer.generateTileCache().
func (mr *MapRenderer) RegenerateTileCache() { func (mr *MapRenderer) RegenerateTileCache() {
mr.generateTileCache() mr.generateTileCache()

33
d2core/d2term/commmand.go Normal file
View File

@ -0,0 +1,33 @@
package d2term
import (
"sort"
)
func (t *Terminal) commandList([]string) error {
names := make([]string, 0, len(t.commands))
for name := range t.commands {
names = append(names, name)
}
sort.Strings(names)
t.Infof("available actions (%d):", len(names))
for _, name := range names {
entry := t.commands[name]
if entry.arguments != nil {
t.Infof("%s: %s; %v", name, entry.description, entry.arguments)
continue
}
t.Infof("%s: %s", name, entry.description)
}
return nil
}
func (t *Terminal) commandClear([]string) error {
t.Clear()
return nil
}

View File

@ -6,8 +6,8 @@ import (
) )
// New creates and initializes the terminal // New creates and initializes the terminal
func New(inputManager d2interface.InputManager) (d2interface.Terminal, error) { func New(inputManager d2interface.InputManager) (*Terminal, error) {
term, err := createTerminal() term, err := NewTerminal()
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -6,9 +6,6 @@ import (
"image/color" "image/color"
"log" "log"
"math" "math"
"reflect"
"sort"
"strconv"
"strings" "strings"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
@ -18,13 +15,13 @@ import (
) )
const ( const (
termCharWidth = 6 charWidth = 6
termCharHeight = 16 charHeight = 16
termCharDoubleWidth = termCharWidth * 2 charDoubleWidth = charWidth * 2
termRowCount = 24 rowCount = 24
termRowCountMax = 32 rowCountMax = 32
termColCountMax = 128 colCountMax = 128
termAnimLength = 0.5 animLength = 0.5
) )
const ( const (
@ -35,13 +32,13 @@ const (
red = 0xcc0000b0 red = 0xcc0000b0
) )
type termVis int type visibility int
const ( const (
termVisHidden termVis = iota visHidden visibility = iota
termVisShowing visShowing
termVisShown visShown
termVisHiding visHiding
) )
const ( const (
@ -49,18 +46,22 @@ const (
minVisAnim = 0.0 minVisAnim = 0.0
) )
type termHistoryEntry struct { type historyEntry struct {
text string text string
category d2enum.TermCategory category d2enum.TermCategory
} }
type termActionEntry struct { type commandEntry struct {
action interface{}
description string description string
arguments []string
fn func([]string) error
} }
type terminal struct { }
outputHistory []termHistoryEntry
// Terminal handles the in-game terminal
type Terminal struct {
outputHistory []historyEntry
outputIndex int outputIndex int
command string command string
@ -68,7 +69,7 @@ type terminal struct {
commandIndex int commandIndex int
lineCount int lineCount int
visState termVis visState visibility
visAnim float64 visAnim float64
bgColor color.RGBA bgColor color.RGBA
@ -77,36 +78,88 @@ type terminal struct {
warningColor color.RGBA warningColor color.RGBA
errorColor color.RGBA errorColor color.RGBA
actions map[string]termActionEntry commands map[string]commandEntry
} }
func (t *terminal) Advance(elapsed float64) error { // NewTerminal creates and returns a terminal
func NewTerminal() (*Terminal, error) {
term := &Terminal{
lineCount: rowCount,
bgColor: d2util.Color(darkGrey),
fgColor: d2util.Color(lightGrey),
infoColor: d2util.Color(lightBlue),
warningColor: d2util.Color(yellow),
errorColor: d2util.Color(red),
commands: make(map[string]commandEntry),
}
term.Infof("::: OpenDiablo2 Terminal :::")
term.Infof("type \"ls\" for a list of commands")
if err := term.Bind("ls", "list available commands", nil, term.commandList); err != nil {
return nil, err
}
if err := term.Bind("clear", "clear terminal", nil, term.commandClear); err != nil {
return nil, err
}
return term, nil
}
// Bind binds commands to the terminal
func (t *Terminal) Bind(name, description string, arguments []string, fn func(args []string) error) error {
if name == "" || description == "" {
return fmt.Errorf("missing name or description")
}
if _, ok := t.commands[name]; ok {
t.Warningf("rebinding command with name: %s", name)
}
t.commands[name] = commandEntry{description, arguments, fn}
return nil
}
// Unbind unbinds commands from the terminal
func (t *Terminal) Unbind(names ...string) error {
for _, name := range names {
delete(t.commands, name)
}
return nil
}
// Advance advances the terminal animation
func (t *Terminal) Advance(elapsed float64) error {
switch t.visState { switch t.visState {
case termVisShowing: case visShowing:
t.visAnim = math.Min(maxVisAnim, t.visAnim+elapsed/termAnimLength) t.visAnim = math.Min(maxVisAnim, t.visAnim+elapsed/animLength)
if t.visAnim == maxVisAnim { if t.visAnim == maxVisAnim {
t.visState = termVisShown t.visState = visShown
} }
case termVisHiding: case visHiding:
t.visAnim = math.Max(minVisAnim, t.visAnim-elapsed/termAnimLength) t.visAnim = math.Max(minVisAnim, t.visAnim-elapsed/animLength)
if t.visAnim == minVisAnim { if t.visAnim == minVisAnim {
t.visState = termVisHidden t.visState = visHidden
} }
} }
if !t.IsVisible() { if !t.Visible() {
return nil return nil
} }
return nil return nil
} }
func (t *terminal) OnKeyDown(event d2interface.KeyEvent) bool { // OnKeyDown handles key down in the terminal
func (t *Terminal) OnKeyDown(event d2interface.KeyEvent) bool {
if event.Key() == d2enum.KeyGraveAccent { if event.Key() == d2enum.KeyGraveAccent {
t.toggleTerminal() t.toggle()
} }
if !t.IsVisible() { if !t.Visible() {
return false return false
} }
@ -139,7 +192,7 @@ func (t *terminal) OnKeyDown(event d2interface.KeyEvent) bool {
return true return true
} }
func (t *terminal) processCommand() { func (t *Terminal) processCommand() {
if t.command == "" { if t.command == "" {
return return
} }
@ -156,17 +209,17 @@ func (t *terminal) processCommand() {
t.commandHistory = t.commandHistory[:n] t.commandHistory = t.commandHistory[:n]
t.commandHistory = append(t.commandHistory, t.command) t.commandHistory = append(t.commandHistory, t.command)
t.Outputf(t.command) t.Printf(t.command)
if err := t.Execute(t.command); err != nil { if err := t.Execute(t.command); err != nil {
t.OutputErrorf(err.Error()) t.Errorf(err.Error())
} }
t.commandIndex = len(t.commandHistory) - 1 t.commandIndex = len(t.commandHistory) - 1
t.command = "" t.command = ""
} }
func (t *terminal) handleControlKey(eventKey d2enum.Key, keyMod d2enum.KeyMod) { func (t *Terminal) handleControlKey(eventKey d2enum.Key, keyMod d2enum.KeyMod) {
switch eventKey { switch eventKey {
case d2enum.KeyUp: case d2enum.KeyUp:
if keyMod == d2enum.KeyModControl { if keyMod == d2enum.KeyModControl {
@ -181,21 +234,14 @@ func (t *terminal) handleControlKey(eventKey d2enum.Key, keyMod d2enum.KeyMod) {
} }
case d2enum.KeyDown: case d2enum.KeyDown:
if keyMod == d2enum.KeyModControl { if keyMod == d2enum.KeyModControl {
t.lineCount = d2math.MinInt(t.lineCount+1, termRowCountMax) t.lineCount = d2math.MinInt(t.lineCount+1, rowCountMax)
} }
} }
} }
func (t *terminal) toggleTerminal() { // OnKeyChars handles char key in terminal
if t.visState == termVisHiding || t.visState == termVisHidden { func (t *Terminal) OnKeyChars(event d2interface.KeyCharsEvent) bool {
t.Show() if !t.Visible() {
} else {
t.Hide()
}
}
func (t *terminal) OnKeyChars(event d2interface.KeyCharsEvent) bool {
if !t.IsVisible() {
return false return false
} }
@ -211,14 +257,15 @@ func (t *terminal) OnKeyChars(event d2interface.KeyCharsEvent) bool {
return handled return handled
} }
func (t *terminal) Render(surface d2interface.Surface) error { // Render renders the terminal
if !t.IsVisible() { func (t *Terminal) Render(surface d2interface.Surface) error {
if !t.Visible() {
return nil return nil
} }
totalWidth, _ := surface.GetSize() totalWidth, _ := surface.GetSize()
outputHeight := t.lineCount * termCharHeight outputHeight := t.lineCount * charHeight
totalHeight := outputHeight + termCharHeight totalHeight := outputHeight + charHeight
offset := -int((1.0 - easeInOut(t.visAnim)) * float64(totalHeight)) offset := -int((1.0 - easeInOut(t.visAnim)) * float64(totalHeight))
surface.PushTranslation(0, offset) surface.PushTranslation(0, offset)
@ -231,19 +278,19 @@ func (t *terminal) Render(surface d2interface.Surface) error {
break break
} }
historyEntry := t.outputHistory[historyIndex] entry := t.outputHistory[historyIndex]
surface.PushTranslation(termCharDoubleWidth, outputHeight-(i+1)*termCharHeight) surface.PushTranslation(charDoubleWidth, outputHeight-(i+1)*charHeight)
surface.DrawTextf(historyEntry.text) surface.DrawTextf(entry.text)
surface.PushTranslation(-termCharDoubleWidth, 0) surface.PushTranslation(-charDoubleWidth, 0)
switch historyEntry.category { switch entry.category {
case d2enum.TermCategoryInfo: case d2enum.TermCategoryInfo:
surface.DrawRect(termCharWidth, termCharHeight, t.infoColor) surface.DrawRect(charWidth, charHeight, t.infoColor)
case d2enum.TermCategoryWarning: case d2enum.TermCategoryWarning:
surface.DrawRect(termCharWidth, termCharHeight, t.warningColor) surface.DrawRect(charWidth, charHeight, t.warningColor)
case d2enum.TermCategoryError: case d2enum.TermCategoryError:
surface.DrawRect(termCharWidth, termCharHeight, t.errorColor) surface.DrawRect(charWidth, charHeight, t.errorColor)
} }
surface.Pop() surface.Pop()
@ -251,7 +298,7 @@ func (t *terminal) Render(surface d2interface.Surface) error {
} }
surface.PushTranslation(0, outputHeight) surface.PushTranslation(0, outputHeight)
surface.DrawRect(totalWidth, termCharHeight, t.fgColor) surface.DrawRect(totalWidth, charHeight, t.fgColor)
surface.DrawTextf("> " + t.command) surface.DrawTextf("> " + t.command)
surface.Pop() surface.Pop()
@ -260,174 +307,105 @@ func (t *terminal) Render(surface d2interface.Surface) error {
return nil return nil
} }
func (t *terminal) Execute(command string) error { // Execute executes a command with arguments
func (t *Terminal) Execute(command string) error {
params := parseCommand(command) params := parseCommand(command)
if len(params) == 0 { if len(params) == 0 {
return errors.New("invalid command") return errors.New("invalid command")
} }
actionName := params[0] name := params[0]
actionParams := params[1:] args := params[1:]
actionEntry, ok := t.actions[actionName] entry, ok := t.commands[name]
if !ok { if !ok {
return errors.New("action not found") return errors.New("command not found")
} }
actionType := reflect.TypeOf(actionEntry.action) if len(args) != len(entry.arguments) {
if actionType.Kind() != reflect.Func { return errors.New("command requires different argument count")
return errors.New("action is not a function")
} }
if len(actionParams) != actionType.NumIn() { if err := entry.fn(args); err != nil {
return errors.New("action requires different argument count")
}
paramValues, err := parseActionParams(actionType, actionParams)
if err != nil {
return err return err
} }
actionValue := reflect.ValueOf(actionEntry.action)
actionReturnValues := actionValue.Call(paramValues)
if actionReturnValueCount := len(actionReturnValues); actionReturnValueCount > 0 {
t.OutputInfof("function returned %d values:", actionReturnValueCount)
for _, actionReturnValue := range actionReturnValues {
t.OutputInfof("%v: %s", actionReturnValue.Interface(), actionReturnValue.String())
}
}
return nil return nil
} }
func parseActionParams(actionType reflect.Type, actionParams []string) ([]reflect.Value, error) { // Rawf writes a raw message to the terminal
var paramValues []reflect.Value func (t *Terminal) Rawf(category d2enum.TermCategory, format string, params ...interface{}) {
text := fmt.Sprintf(format, params...)
for i := 0; i < actionType.NumIn(); i++ { lines := d2util.SplitIntoLinesWithMaxWidth(text, colCountMax)
actionParam := actionParams[i]
switch actionType.In(i).Kind() {
case reflect.String:
paramValues = append(paramValues, reflect.ValueOf(actionParam))
case reflect.Int:
value, err := strconv.ParseInt(actionParam, 10, 64)
if err != nil {
return nil, err
}
paramValues = append(paramValues, reflect.ValueOf(int(value)))
case reflect.Uint:
value, err := strconv.ParseUint(actionParam, 10, 64)
if err != nil {
return nil, err
}
paramValues = append(paramValues, reflect.ValueOf(uint(value)))
case reflect.Float64:
value, err := strconv.ParseFloat(actionParam, 64)
if err != nil {
return nil, err
}
paramValues = append(paramValues, reflect.ValueOf(value))
case reflect.Bool:
value, err := strconv.ParseBool(actionParam)
if err != nil {
return nil, err
}
paramValues = append(paramValues, reflect.ValueOf(value))
default:
return nil, errors.New("action has unsupported arguments")
}
}
return paramValues, nil
}
func (t *terminal) OutputRaw(text string, category d2enum.TermCategory) {
lines := d2util.SplitIntoLinesWithMaxWidth(text, termColCountMax)
for _, line := range lines { for _, line := range lines {
// removes color token (this token ends with [0m ) // removes color token (this token ends with [0m )
l := strings.Split(line, "\033[0m") l := strings.Split(line, "\033[0m")
line = l[len(l)-1] line = l[len(l)-1]
t.outputHistory = append(t.outputHistory, termHistoryEntry{line, category}) t.outputHistory = append(t.outputHistory, historyEntry{line, category})
} }
} }
func (t *terminal) Outputf(format string, params ...interface{}) { // Printf writes a message to the terminal
t.OutputRaw(fmt.Sprintf(format, params...), d2enum.TermCategoryNone) func (t *Terminal) Printf(format string, params ...interface{}) {
t.Rawf(d2enum.TermCategoryNone, format, params...)
} }
func (t *terminal) OutputInfof(format string, params ...interface{}) { // Infof writes a warning message to the terminal
t.OutputRaw(fmt.Sprintf(format, params...), d2enum.TermCategoryInfo) func (t *Terminal) Infof(format string, params ...interface{}) {
t.Rawf(d2enum.TermCategoryInfo, format, params...)
} }
func (t *terminal) OutputWarningf(format string, params ...interface{}) { // Warningf writes a warning message to the terminal
t.OutputRaw(fmt.Sprintf(format, params...), d2enum.TermCategoryWarning) func (t *Terminal) Warningf(format string, params ...interface{}) {
t.Rawf(d2enum.TermCategoryWarning, format, params...)
} }
func (t *terminal) OutputErrorf(format string, params ...interface{}) { // Errorf writes a error message to the terminal
t.OutputRaw(fmt.Sprintf(format, params...), d2enum.TermCategoryError) func (t *Terminal) Errorf(format string, params ...interface{}) {
t.Rawf(d2enum.TermCategoryError, format, params...)
} }
func (t *terminal) OutputClear() { // Clear clears the terminal
func (t *Terminal) Clear() {
t.outputHistory = nil t.outputHistory = nil
t.outputIndex = 0 t.outputIndex = 0
} }
func (t *terminal) IsVisible() bool { // Visible returns visible state
return t.visState != termVisHidden func (t *Terminal) Visible() bool {
return t.visState != visHidden
} }
func (t *terminal) Hide() { // Hide hides the terminal
if t.visState != termVisHidden { func (t *Terminal) Hide() {
t.visState = termVisHiding if t.visState != visHidden {
t.visState = visHiding
} }
} }
func (t *terminal) Show() { // Show shows the terminal
if t.visState != termVisShown { func (t *Terminal) Show() {
t.visState = termVisShowing if t.visState != visShown {
t.visState = visShowing
} }
} }
func (t *terminal) BindAction(name, description string, action interface{}) error { func (t *Terminal) toggle() {
actionType := reflect.TypeOf(action) if t.visState == visHiding || t.visState == visHidden {
if actionType.Kind() != reflect.Func { t.Show()
return errors.New("action is not a function") return
} }
for i := 0; i < actionType.NumIn(); i++ { t.Hide()
switch actionType.In(i).Kind() {
case reflect.String:
case reflect.Int:
case reflect.Uint:
case reflect.Float64:
case reflect.Bool:
default:
return errors.New("action has unsupported arguments")
}
}
t.actions[name] = termActionEntry{action, description}
return nil
} }
func (t *terminal) BindLogger() { // BindLogger binds a log.Writer to the output
func (t *Terminal) BindLogger() {
log.SetOutput(&terminalLogger{writer: log.Writer(), terminal: t}) log.SetOutput(&terminalLogger{writer: log.Writer(), terminal: t})
} }
func (t *terminal) UnbindAction(name string) error {
delete(t.actions, name)
return nil
}
func easeInOut(t float64) float64 { func easeInOut(t float64) float64 {
t *= 2 t *= 2
if t < 1 { if t < 1 {
@ -481,45 +459,3 @@ func parseCommand(command string) []string {
return params return params
} }
func createTerminal() (*terminal, error) {
terminal := &terminal{
lineCount: termRowCount,
bgColor: d2util.Color(darkGrey),
fgColor: d2util.Color(lightGrey),
infoColor: d2util.Color(lightBlue),
warningColor: d2util.Color(yellow),
errorColor: d2util.Color(red),
actions: make(map[string]termActionEntry),
}
terminal.OutputInfof("::: OpenDiablo2 Terminal :::")
terminal.OutputInfof("type \"ls\" for a list of actions")
err := terminal.BindAction("ls", "list available actions", func() {
var names []string
for name := range terminal.actions {
names = append(names, name)
}
sort.Strings(names)
terminal.OutputInfof("available actions (%d):", len(names))
for _, name := range names {
entry := terminal.actions[name]
terminal.OutputInfof("%s: %s; %s", name, entry.description, reflect.TypeOf(entry.action).String())
}
})
if err != nil {
return nil, fmt.Errorf("failed to bind the '%s' action, err: %w", "ls", err)
}
err = terminal.BindAction("clear", "clear terminal", func() {
terminal.OutputClear()
})
if err != nil {
return nil, fmt.Errorf("failed to bind the '%s' action, err: %w", "clear", err)
}
return terminal, nil
}

View File

@ -8,7 +8,7 @@ import (
) )
type terminalLogger struct { type terminalLogger struct {
terminal *terminal terminal *Terminal
buffer bytes.Buffer buffer bytes.Buffer
writer io.Writer writer io.Writer
} }
@ -31,16 +31,16 @@ func (tl *terminalLogger) Write(p []byte) (int, error) {
switch { switch {
case strings.Index(lineLower, "error") > 0: case strings.Index(lineLower, "error") > 0:
tl.terminal.OutputErrorf(line) tl.terminal.Errorf(line)
case strings.Index(lineLower, "warning") > 0: case strings.Index(lineLower, "warning") > 0:
tl.terminal.OutputWarningf(line) tl.terminal.Errorf(line)
default: default:
tl.terminal.Outputf(line) tl.terminal.Printf(line)
} }
return tl.writer.Write(p) return tl.writer.Write(p)
} }
func (tl *terminalLogger) BindToTerminal(t *terminal) { func (tl *terminalLogger) BindToTerminal(t *Terminal) {
tl.terminal = t tl.terminal = t
} }

View File

@ -0,0 +1,71 @@
package d2term
import (
"fmt"
"testing"
)
func TestTerminal(t *testing.T) {
term, err := NewTerminal()
if err != nil {
t.Fatal(err)
}
lenOutput := len(term.outputHistory)
const expected1 = 2
if lenOutput != expected1 {
t.Fatalf("got %d expected %d", lenOutput, expected1)
}
term.Execute("clear")
term.Execute("ls")
lenOutput = len(term.outputHistory)
const expected2 = 3
if lenOutput != expected2 {
t.Fatalf("got %d expected %d", lenOutput, expected2)
}
}
func TestBind(t *testing.T) {
term, err := NewTerminal()
if err != nil {
t.Fatal(err)
}
term.Clear()
if err := term.Bind("hello", "world", []string{"world"}, func(args []string) error {
const expected = "world"
if args[0] != expected {
return fmt.Errorf("got %s expected %s", args[0], expected)
}
return nil
}); err != nil {
t.Fatal(err)
}
if err := term.Execute("hello world"); err != nil {
t.Fatal(err)
}
}
func TestUnbind(t *testing.T) {
term, err := NewTerminal()
if err != nil {
t.Fatal(err)
}
term.Unbind("clear")
term.Clear()
term.Execute("ls")
lenOutput := len(term.outputHistory)
const expected = 2
if lenOutput != expected {
t.Fatalf("got %d expected %d", lenOutput, expected)
}
}

View File

@ -4,6 +4,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"image/color" "image/color"
"strconv"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2gui" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2gui"
@ -130,58 +131,36 @@ type Game struct {
func (v *Game) OnLoad(_ d2screen.LoadingState) { func (v *Game) OnLoad(_ d2screen.LoadingState) {
v.audioProvider.PlayBGM("") v.audioProvider.PlayBGM("")
err := v.terminal.BindAction( commands := []struct {
"spawnitem", name string
"spawns an item at the local player position", desc string
func(code1, code2, code3, code4, code5 string) { args []string
codes := []string{code1, code2, code3, code4, code5} fn func([]string) error
v.debugSpawnItemAtPlayer(codes...) }{
}, {"spawnitem", "spawns an item at the local player position",
) []string{"code1", "code2", "code3", "code4", "code5"}, v.commandSpawnItem},
if err != nil { {"spawnitemat", "spawns an item at the x,y coordinates",
v.Errorf("failed to bind the '%s' action, err: %v\n", "spawnitem", err) []string{"x", "y", "code1", "code2", "code3", "code4", "code5"}, v.commandSpawnItemAt},
{"spawnmon", "spawn monster at the local player position", []string{"name"}, v.commandSpawnMon},
} }
err = v.terminal.BindAction( for _, cmd := range commands {
"spawnitemat", if err := v.terminal.Bind(cmd.name, cmd.desc, cmd.args, cmd.fn); err != nil {
"spawns an item at the x,y coordinates", v.Errorf(err.Error())
func(x, y int, code1, code2, code3, code4, code5 string) { }
codes := []string{code1, code2, code3, code4, code5}
v.debugSpawnItemAtLocation(x, y, codes...)
},
)
if err != nil {
v.Errorf("failed to bind the '%s' action, err: %v\n", "spawnitemat", err)
} }
err = v.terminal.BindAction( if err := v.asset.BindTerminalCommands(v.terminal); err != nil {
"spawnmon", v.Errorf(err.Error())
"spawn monster at the local player position",
func(name string) {
x := int(v.localPlayer.Position.X())
y := int(v.localPlayer.Position.Y())
monstat := v.asset.Records.Monster.Stats[name]
if monstat == nil {
v.terminal.OutputErrorf("no monstat entry for \"%s\"", name)
return
}
monster, npcErr := v.gameClient.MapEngine.NewNPC(x, y, monstat, 0)
if npcErr != nil {
v.terminal.OutputErrorf("error generating monster \"%s\": %v", name, npcErr)
return
}
v.gameClient.MapEngine.AddEntity(monster)
},
)
if err != nil {
v.Errorf("failed to bind the '%s' action, err: %v\n", "spawnmon", err)
} }
} }
// OnUnload releases the resources of Gameplay screen // OnUnload releases the resources of Gameplay screen
func (v *Game) OnUnload() error { func (v *Game) OnUnload() error {
if err := v.gameControls.UnbindTerminalCommands(v.terminal); err != nil {
return err
}
// https://github.com/OpenDiablo2/OpenDiablo2/issues/792 // https://github.com/OpenDiablo2/OpenDiablo2/issues/792
if err := v.inputManager.UnbindHandler(v.gameControls); err != nil { if err := v.inputManager.UnbindHandler(v.gameControls); err != nil {
return err return err
@ -192,11 +171,7 @@ func (v *Game) OnUnload() error {
return err return err
} }
if err := v.terminal.UnbindAction("spawnItemAt"); err != nil { if err := v.terminal.Unbind("spawnitemat", "spawnitem", "spawnmon"); err != nil {
return err
}
if err := v.terminal.UnbindAction("spawnItem"); err != nil {
return err return err
} }
@ -208,6 +183,18 @@ func (v *Game) OnUnload() error {
return err return err
} }
if err := v.asset.UnbindTerminalCommands(v.terminal); err != nil {
return err
}
if err := v.mapRenderer.UnbindTerminalCommands(v.terminal); err != nil {
return err
}
if err := v.soundEngine.UnbindTerminalCommands(v.terminal); err != nil {
return err
}
v.soundEngine.Reset() v.soundEngine.Reset()
return nil return nil
@ -395,3 +382,47 @@ func (v *Game) debugSpawnItemAtLocation(x, y int, codes ...string) {
v.Errorf(spawnItemErrStr, x, y, codes) v.Errorf(spawnItemErrStr, x, y, codes)
} }
} }
func (v *Game) commandSpawnItem(args []string) error {
v.debugSpawnItemAtPlayer(args...)
return nil
}
func (v *Game) commandSpawnItemAt(args []string) error {
x, err := strconv.Atoi(args[0])
if err != nil {
return fmt.Errorf("invalid argument")
}
y, err := strconv.Atoi(args[0])
if err != nil {
return fmt.Errorf("invalid argument")
}
v.debugSpawnItemAtLocation(x, y, args[2:]...)
return nil
}
func (v *Game) commandSpawnMon(args []string) error {
name := args[0]
x := int(v.localPlayer.Position.X())
y := int(v.localPlayer.Position.Y())
monstat := v.asset.Records.Monster.Stats[name]
if monstat == nil {
v.terminal.Errorf("no monstat entry for \"%s\"", name)
return nil
}
monster, npcErr := v.gameClient.MapEngine.NewNPC(x, y, monstat, 0)
if npcErr != nil {
v.terminal.Errorf("error generating monster \"%s\": %v", name, npcErr)
return nil
}
v.gameClient.MapEngine.AddEntity(monster)
return nil
}

View File

@ -2,6 +2,7 @@ package d2player
import ( import (
"fmt" "fmt"
"strconv"
"strings" "strings"
"time" "time"
@ -936,59 +937,137 @@ func (g *GameControls) onClickActionable(item actionableType) {
action() action()
} }
func (g *GameControls) bindFreeCamCommand(term d2interface.Terminal) error { func (g *GameControls) bindTerminalCommands(term d2interface.Terminal) error {
return term.BindAction("freecam", "toggle free camera movement", func() { if err := term.Bind("freecam", "toggle free camera movement", nil, g.commandFreeCam); err != nil {
g.FreeCam = !g.FreeCam 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
} }
func (g *GameControls) bindSetLeftSkillCommand(term d2interface.Terminal) error { // UnbindTerminalCommands unbinds commands from the terminal
setLeftSkill := func(id int) { func (g *GameControls) UnbindTerminalCommands(term d2interface.Terminal) error {
return term.Unbind("freecam", "setleftskill", "setrightskill", "learnskills", "learnskillid")
}
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] skillRecord := g.asset.Records.Skill.Details[id]
skill, err := g.heroState.CreateHeroSkill(1, skillRecord.Skill) skill, err := g.heroState.CreateHeroSkill(1, skillRecord.Skill)
if err != nil { if err != nil {
term.OutputErrorf("cannot create skill with ID of %d, error: %s", id, err) term.Errorf("cannot create skill with ID of %d, error: %s", id, err)
return return nil
} }
g.hero.LeftSkill = skill g.hero.LeftSkill = skill
}
return term.BindAction( return nil
"setleftskill", }
"set skill to fire on left click",
setLeftSkill,
)
} }
func (g *GameControls) bindSetRightSkillCommand(term d2interface.Terminal) error { func (g *GameControls) commandSetRightSkill(term d2interface.Terminal) func(args []string) error {
setRightSkill := func(id int) { 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] skillRecord := g.asset.Records.Skill.Details[id]
skill, err := g.heroState.CreateHeroSkill(0, skillRecord.Skill) skill, err := g.heroState.CreateHeroSkill(0, skillRecord.Skill)
if err != nil { if err != nil {
term.OutputErrorf("cannot create skill with ID of %d, error: %s", id, err) term.Errorf("cannot create skill with ID of %d, error: %s", id, err)
return return nil
} }
g.hero.RightSkill = skill g.hero.RightSkill = skill
}
return term.BindAction( return nil
"setrightskill", }
"set skill to fire on right click",
setRightSkill,
)
} }
const classTokenLength = 3 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
}
func (g *GameControls) bindLearnSkillsCommand(term d2interface.Terminal) error { skillRecord := g.asset.Records.Skill.Details[id]
learnSkills := func(token string) { 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
}
}
func (g *GameControls) commandLearnSkills(term d2interface.Terminal) func(args []string) error {
const classTokenLength = 3
return func(args []string) error {
token := args[0]
if len(token) < classTokenLength { if len(token) < classTokenLength {
term.OutputErrorf("The given class token should be at least 3 characters") term.Errorf("The given class token should be at least 3 characters")
return return nil
} }
validPrefixes := []string{"ama", "ass", "nec", "bar", "sor", "dru", "pal"} validPrefixes := []string{"ama", "ass", "nec", "bar", "sor", "dru", "pal"}
@ -1004,9 +1083,9 @@ func (g *GameControls) bindLearnSkillsCommand(term d2interface.Terminal) error {
if !isValidToken { if !isValidToken {
fmtInvalid := "Invalid class, must be a value starting with(case insensitive): %s" fmtInvalid := "Invalid class, must be a value starting with(case insensitive): %s"
term.OutputErrorf(fmtInvalid, strings.Join(validPrefixes, ", ")) term.Errorf(fmtInvalid, strings.Join(validPrefixes, ", "))
return return nil
} }
var err error var err error
@ -1042,80 +1121,10 @@ func (g *GameControls) bindLearnSkillsCommand(term d2interface.Terminal) error {
g.Infof("Learned %d skills", learnedSkillsCount) g.Infof("Learned %d skills", learnedSkillsCount)
if err != nil { if err != nil {
term.OutputErrorf("cannot learn skill for class, error: %s", err) term.Errorf("cannot learn skill for class, error: %s", err)
return return nil
}
}
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) return nil
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.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
}
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() })
} }