From 9356502a50132298f29046d696f99b32014ee547 Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Sat, 21 Jul 2018 20:43:01 -0700 Subject: [PATCH] Implement Developer Console with Initial Commands Implements the dev console in-game with various commands to start out with. Press the Enter key to show or hide the console. Commands supported: new Start a new map in Edit Mode. save [filename.json] Save the current map to disk. Filename is required unless you have saved recently. edit filename.json Open a map from disk in Edit Mode. play filename.json Play a map from disk in Play Mode. --- balance/shell.go | 17 +++ commands.go | 102 ++++++++++++++++++ doodle.go | 58 +++++++--- editor_scene.go | 14 ++- events/events.go | 48 +++++++++ events/types.go | 26 +++++ fps.go | 21 ++-- play_scene.go | 5 +- render/interface.go | 7 +- render/sdl/canvas.go | 13 +++ render/sdl/log.go | 1 + render/sdl/sdl.go | 40 +++++-- render/sdl/text.go | 48 ++++++++- scene.go | 6 ++ scene/editor.go | 86 --------------- scene/log.go | 9 -- scene/scene.go | 67 ------------ shell.go | 252 +++++++++++++++++++++++++++++++++++++++++++ 18 files changed, 610 insertions(+), 210 deletions(-) create mode 100644 balance/shell.go create mode 100644 commands.go delete mode 100644 scene/editor.go delete mode 100644 scene/log.go delete mode 100644 scene/scene.go create mode 100644 shell.go diff --git a/balance/shell.go b/balance/shell.go new file mode 100644 index 0000000..2be7193 --- /dev/null +++ b/balance/shell.go @@ -0,0 +1,17 @@ +package balance + +import "git.kirsle.net/apps/doodle/render" + +// Shell related variables. +var ( + // TODO: why not renders transparent + ShellBackgroundColor = render.Color{0, 10, 20, 128} + ShellForegroundColor = render.White + ShellPadding int32 = 8 + ShellFontSize = 14 + ShellCursorBlinkRate uint64 = 20 + ShellHistoryLineCount = 8 + + // Ticks that a flashed message persists for. + FlashTTL uint64 = 200 +) diff --git a/commands.go b/commands.go new file mode 100644 index 0000000..9f96387 --- /dev/null +++ b/commands.go @@ -0,0 +1,102 @@ +package doodle + +import ( + "errors" + "fmt" +) + +// Command is a parsed shell command. +type Command struct { + Raw string // The complete raw command the user typed. + Command string // The first word of their command. + Args []string // The shell-args array of parameters. + ArgsLiteral string // The args portion of the command literally. +} + +// Run the command. +func (c Command) Run(d *Doodle) error { + if len(c.Raw) == 0 { + return nil + } + + switch c.Command { + case "new": + return c.New(d) + case "save": + return c.Save(d) + case "edit": + return c.Edit(d) + case "play": + return c.Play(d) + case "exit": + case "quit": + return c.Quit() + default: + return c.Default() + } + return nil +} + +// New opens a new map in the editor mode. +func (c Command) New(d *Doodle) error { + d.shell.Write("Starting a new map") + d.NewMap() + return nil +} + +// Save the current map to disk. +func (c Command) Save(d *Doodle) error { + if scene, ok := d.scene.(*EditorScene); ok { + filename := "" + if len(c.Args) > 0 { + filename = c.Args[0] + } else if scene.filename != "" { + filename = scene.filename + } else { + return errors.New("usage: save ") + } + + d.shell.Write("Saving to file: " + filename) + scene.SaveLevel(filename) + } else { + return errors.New("save: only available in Edit Mode") + } + + return nil +} + +// Edit a map from disk. +func (c Command) Edit(d *Doodle) error { + if len(c.Args) == 0 { + return errors.New("Usage: edit ") + } + + filename := c.Args[0] + d.shell.Write("Editing level: " + filename) + d.EditLevel(filename) + return nil +} + +// Play a map. +func (c Command) Play(d *Doodle) error { + if len(c.Args) == 0 { + return errors.New("Usage: play ") + } + + filename := c.Args[0] + d.shell.Write("Playing level: " + filename) + d.PlayLevel(filename) + return nil +} + +// Quit the command line shell. +func (c Command) Quit() error { + return nil +} + +// Default command. +func (c Command) Default() error { + return fmt.Errorf("%s: command not found. Try `help` for help", + c.Command, + ) +} diff --git a/doodle.go b/doodle.go index 67f9de6..ac86522 100644 --- a/doodle.go +++ b/doodle.go @@ -4,7 +4,6 @@ import ( "fmt" "time" - "git.kirsle.net/apps/doodle/events" "git.kirsle.net/apps/doodle/render" "github.com/kirsle/golog" ) @@ -28,10 +27,12 @@ type Doodle struct { startTime time.Time running bool ticks uint64 - events *events.State width int32 height int32 + // Command line shell options. + shell Shell + scene Scene } @@ -41,11 +42,11 @@ func New(debug bool, engine render.Engine) *Doodle { Debug: debug, Engine: engine, startTime: time.Now(), - events: events.New(), running: true, width: 800, height: 600, } + d.shell = NewShell(d) if !debug { log.Config.Level = golog.InfoLevel @@ -68,6 +69,8 @@ func (d *Doodle) Run() error { log.Info("Enter Main Loop") for d.running { + d.Engine.Clear(render.White) + start := time.Now() // Record how long this frame took. d.ticks++ @@ -79,24 +82,43 @@ func (d *Doodle) Run() error { break } - // Global event handlers. - if ev.EscapeKey.Pressed() { - log.Error("Escape key pressed, shutting down") - d.running = false - break + // Command line shell. + if d.shell.Open { + + } else if ev.EnterKey.Read() { + log.Debug("Shell: opening shell") + d.shell.Open = true + } else { + // Global event handlers. + if ev.EscapeKey.Read() { + log.Error("Escape key pressed, shutting down") + d.running = false + break + } + + // Run the scene's logic. + err = d.scene.Loop(d, ev) + if err != nil { + return err + } } - // Run the scene's logic. - err = d.scene.Loop(d, ev) + // Draw the scene. + d.scene.Draw(d) + + // Draw the shell. + err = d.shell.Draw(d, ev) if err != nil { - return err + log.Error("shell error: %s", err) + d.running = false + break } // Draw the debug overlay over all scenes. d.DrawDebugOverlay() // Render the pixels to the screen. - err = d.Engine.Draw() + err = d.Engine.Present() if err != nil { log.Error("draw error: %s", err) d.running = false @@ -114,12 +136,22 @@ func (d *Doodle) Run() error { // Track how long this frame took to measure FPS over time. d.TrackFPS(delay) + + // Consume any lingering key sym. + ev.KeyName.Read() } log.Warn("Main Loop Exited! Shutting down...") return nil } +// NewMap loads a new map in Edit Mode. +func (d *Doodle) NewMap() { + log.Info("Starting a new map") + scene := &EditorScene{} + d.Goto(scene) +} + // EditLevel loads a map from JSON into the EditorScene. func (d *Doodle) EditLevel(filename string) error { log.Info("Loading level from file: %s", filename) @@ -144,7 +176,7 @@ func (d *Doodle) PlayLevel(filename string) error { return nil } -// TODO: not a global +// Pixel TODO: not a global type Pixel struct { start bool x int32 diff --git a/editor_scene.go b/editor_scene.go index 84667e9..dbed66a 100644 --- a/editor_scene.go +++ b/editor_scene.go @@ -19,6 +19,7 @@ type EditorScene struct { // History of all the pixels placed by the user. pixelHistory []Pixel canvas Grid + filename string // Last saved filename. // Canvas size width int32 @@ -49,7 +50,6 @@ func (s *EditorScene) Loop(d *Doodle, ev *events.State) error { if ev.ScreenshotKey.Pressed() { log.Info("Taking a screenshot") s.Screenshot() - s.SaveLevel() } // Clear the canvas and fill it with white. @@ -81,6 +81,11 @@ func (s *EditorScene) Loop(d *Doodle, ev *events.State) error { } } + return nil +} + +// Draw the current frame. +func (s *EditorScene) Draw(d *Doodle) error { for i, pixel := range s.pixelHistory { if !pixel.start && i > 0 { prev := s.pixelHistory[i-1] @@ -105,6 +110,7 @@ func (s *EditorScene) Loop(d *Doodle, ev *events.State) error { // LoadLevel loads a level from disk. func (s *EditorScene) LoadLevel(filename string) error { + s.filename = filename s.pixelHistory = []Pixel{} s.canvas = Grid{} @@ -129,7 +135,8 @@ func (s *EditorScene) LoadLevel(filename string) error { } // SaveLevel saves the level to disk. -func (s *EditorScene) SaveLevel() { +func (s *EditorScene) SaveLevel(filename string) { + s.filename = filename m := level.Level{ Version: 1, Title: "Alpha", @@ -161,9 +168,6 @@ func (s *EditorScene) SaveLevel() { return } - filename := fmt.Sprintf("./map-%s.json", - time.Now().Format("2006-01-02T15-04-05"), - ) err = ioutil.WriteFile(filename, json, 0644) if err != nil { log.Error("Create map file error: %s", err) diff --git a/events/events.go b/events/events.go index b0d3647..a4dffbb 100644 --- a/events/events.go +++ b/events/events.go @@ -1,6 +1,8 @@ // Package events manages mouse and keyboard SDL events for Doodle. package events +import "strings" + // State keeps track of event states. type State struct { // Mouse buttons. @@ -10,6 +12,9 @@ type State struct { // Screenshot key. ScreenshotKey *BoolTick EscapeKey *BoolTick + EnterKey *BoolTick + ShiftActive *BoolTick + KeyName *StringTick Up *BoolTick Left *BoolTick Right *BoolTick @@ -27,6 +32,9 @@ func New() *State { Button2: &BoolTick{}, ScreenshotKey: &BoolTick{}, EscapeKey: &BoolTick{}, + EnterKey: &BoolTick{}, + ShiftActive: &BoolTick{}, + KeyName: &StringTick{}, Up: &BoolTick{}, Left: &BoolTick{}, Right: &BoolTick{}, @@ -35,3 +43,43 @@ func New() *State { CursorY: &Int32Tick{}, } } + +// ReadKey returns the normalized key symbol being pressed, +// taking the Shift key into account. QWERTY keyboard only, probably. +func (ev *State) ReadKey() string { + if key := ev.KeyName.Read(); key != "" { + if ev.ShiftActive.Pressed() { + if symbol, ok := shiftMap[key]; ok { + return symbol + } + return strings.ToUpper(key) + } + return key + } + return "" +} + +// shiftMap maps keys to their Shift versions. +var shiftMap = map[string]string{ + "`": "~", + "1": "!", + "2": "@", + "3": "#", + "4": "$", + "5": "%", + "6": "^", + "7": "&", + "8": "*", + "9": "(", + "0": ")", + "-": "_", + "=": "+", + "[": "{", + "]": "}", + `\`: "|", + ";": ":", + `'`: `"`, + ",": "<", + ".": ">", + "/": "?", +} diff --git a/events/types.go b/events/types.go index 4a413b6..811ce56 100644 --- a/events/types.go +++ b/events/types.go @@ -12,6 +12,12 @@ type Int32Tick struct { Last int32 } +// StringTick manages strings between this frame and the previous. +type StringTick struct { + Now string + Last string +} + // Push a bool state, copying the current Now value to Last. func (bs *BoolTick) Push(v bool) { bs.Last = bs.Now @@ -23,8 +29,28 @@ func (bs *BoolTick) Pressed() bool { return bs.Now && !bs.Last } +// Read a bool state, resetting its value to false. +func (bs *BoolTick) Read() bool { + now := bs.Now + bs.Push(false) + return now +} + // Push an int32 state, copying the current Now value to Last. func (is *Int32Tick) Push(v int32) { is.Last = is.Now is.Now = v } + +// Push a string state. +func (s *StringTick) Push(v string) { + s.Last = s.Now + s.Now = v +} + +// Read a string state, resetting its value. +func (s *StringTick) Read() string { + now := s.Now + s.Push("") + return now +} diff --git a/fps.go b/fps.go index 5ce5e73..b46db71 100644 --- a/fps.go +++ b/fps.go @@ -2,7 +2,6 @@ package doodle import ( "fmt" - "time" "git.kirsle.net/apps/doodle/render" ) @@ -26,11 +25,9 @@ func (d *Doodle) DrawDebugOverlay() { } label := fmt.Sprintf( - "FPS: %d (%dms) (%d,%d) S:%s F12=screenshot", + "FPS: %d (%dms) S:%s F12=screenshot", fpsCurrent, fpsSkipped, - d.events.CursorX.Now, - d.events.CursorY.Now, d.scene.Name(), ) @@ -42,11 +39,9 @@ func (d *Doodle) DrawDebugOverlay() { Stroke: DebugTextStroke, Shadow: DebugTextShadow, }, - render.Rect{ + render.Point{ X: DebugTextPadding, Y: DebugTextPadding, - W: d.width, - H: d.height, }, ) if err != nil { @@ -65,12 +60,12 @@ func (d *Doodle) TrackFPS(skipped uint32) { } if fpsLastTime < fpsCurrentTicks-fpsInterval { - log.Debug("Uptime: %s FPS: %d deltaTicks: %d skipped: %dms", - time.Now().Sub(d.startTime), - fpsCurrent, - fpsCurrentTicks-fpsLastTime, - skipped, - ) + // log.Debug("Uptime: %s FPS: %d deltaTicks: %d skipped: %dms", + // time.Now().Sub(d.startTime), + // fpsCurrent, + // fpsCurrentTicks-fpsLastTime, + // skipped, + // ) fpsLastTime = fpsCurrentTicks fpsCurrent = fpsFrames diff --git a/play_scene.go b/play_scene.go index 157ab96..37aeee4 100644 --- a/play_scene.go +++ b/play_scene.go @@ -42,9 +42,7 @@ func (s *PlayScene) Setup(d *Doodle) error { // Loop the editor scene. func (s *PlayScene) Loop(d *Doodle, ev *events.State) error { s.movePlayer(ev) - - // Apply gravity. - return s.Draw(d) + return nil } // Draw the pixels on this frame. @@ -57,7 +55,6 @@ func (s *PlayScene) Draw(d *Doodle) error { } // Draw our hero. - log.Info("hero %s %+v", render.Magenta, render.Magenta) d.Engine.DrawRect(render.Magenta, render.Rect{s.x, s.y, 16, 16}) return nil diff --git a/render/interface.go b/render/interface.go index fe37109..8a0e9aa 100644 --- a/render/interface.go +++ b/render/interface.go @@ -15,15 +15,16 @@ type Engine interface { Poll() (*events.State, error) GetTicks() uint32 - // Draw presents the current state to the screen. - Draw() error + // Present presents the current state to the screen. + Present() error // Clear the full canvas and set this color. Clear(Color) DrawPoint(Color, Point) DrawLine(Color, Point, Point) DrawRect(Color, Rect) - DrawText(Text, Rect) error + DrawBox(Color, Rect) + DrawText(Text, Point) error // Delay for a moment using the render engine's delay method, // implemented by sdl.Delay(uint32) diff --git a/render/sdl/canvas.go b/render/sdl/canvas.go index f6f45b4..28899e5 100644 --- a/render/sdl/canvas.go +++ b/render/sdl/canvas.go @@ -42,3 +42,16 @@ func (r *Renderer) DrawRect(color render.Color, rect render.Rect) { H: rect.H, }) } + +// DrawBox draws a filled rectangle. +func (r *Renderer) DrawBox(color render.Color, rect render.Rect) { + if color != r.lastColor { + r.renderer.SetDrawColor(color.Red, color.Green, color.Blue, color.Alpha) + } + r.renderer.FillRect(&sdl.Rect{ + X: rect.X, + Y: rect.Y, + W: rect.W, + H: rect.H, + }) +} diff --git a/render/sdl/log.go b/render/sdl/log.go index 30aa217..1564f5d 100644 --- a/render/sdl/log.go +++ b/render/sdl/log.go @@ -8,6 +8,7 @@ var log *golog.Logger var ( DebugMouseEvents = false DebugClickEvents = false + DebugKeyEvents = false ) func init() { diff --git a/render/sdl/sdl.go b/render/sdl/sdl.go index ae386fd..24369cf 100644 --- a/render/sdl/sdl.go +++ b/render/sdl/sdl.go @@ -159,16 +159,23 @@ func (r *Renderer) Poll() (*events.State, error) { ) } case *sdl.KeyboardEvent: - log.Debug("[%d ms] tick:%d Keyboard type:%d sym:%c modifiers:%d state:%d repeat:%d\n", - t.Timestamp, r.ticks, t.Type, t.Keysym.Sym, t.Keysym.Mod, t.State, t.Repeat, - ) - if t.Repeat == 1 { - continue + if DebugKeyEvents { + log.Debug("[%d ms] tick:%d Keyboard type:%d sym:%c modifiers:%d state:%d repeat:%d\n", + t.Timestamp, r.ticks, t.Type, t.Keysym.Sym, t.Keysym.Mod, t.State, t.Repeat, + ) } switch t.Keysym.Scancode { case sdl.SCANCODE_ESCAPE: + if t.Repeat == 1 { + continue + } s.EscapeKey.Push(t.State == 1) + case sdl.SCANCODE_RETURN: + if t.Repeat == 1 { + continue + } + s.EnterKey.Push(t.State == 1) case sdl.SCANCODE_F12: s.ScreenshotKey.Push(t.State == 1) case sdl.SCANCODE_UP: @@ -179,6 +186,25 @@ func (r *Renderer) Poll() (*events.State, error) { s.Right.Push(t.State == 1) case sdl.SCANCODE_DOWN: s.Down.Push(t.State == 1) + case sdl.SCANCODE_LSHIFT: + case sdl.SCANCODE_RSHIFT: + s.ShiftActive.Push(t.State == 1) + continue + case sdl.SCANCODE_LALT: + case sdl.SCANCODE_RALT: + case sdl.SCANCODE_LCTRL: + case sdl.SCANCODE_RCTRL: + continue + case sdl.SCANCODE_BACKSPACE: + // Make it a key event with "\b" as the sequence. + if t.State == 1 || t.Repeat == 1 { + s.KeyName.Push(`\b`) + } + default: + // Push the string value of the key. + if t.State == 1 { + s.KeyName.Push(string(t.Keysym.Sym)) + } } } } @@ -186,8 +212,8 @@ func (r *Renderer) Poll() (*events.State, error) { return s, nil } -// Draw a single frame. -func (r *Renderer) Draw() error { +// Present the current frame. +func (r *Renderer) Present() error { r.renderer.Present() return nil } diff --git a/render/sdl/text.go b/render/sdl/text.go index 9740298..b841037 100644 --- a/render/sdl/text.go +++ b/render/sdl/text.go @@ -1,6 +1,9 @@ package sdl import ( + "strings" + + "git.kirsle.net/apps/doodle/events" "git.kirsle.net/apps/doodle/render" "github.com/veandco/go-sdl2/sdl" "github.com/veandco/go-sdl2/ttf" @@ -23,8 +26,22 @@ func LoadFont(size int) (*ttf.Font, error) { return font, nil } +// Keysym returns the current key pressed, taking into account the Shift +// key modifier. +func (r *Renderer) Keysym(ev *events.State) string { + if key := ev.KeyName.Read(); key != "" { + if ev.ShiftActive.Pressed() { + if symbol, ok := shiftMap[key]; ok { + return symbol + } + return strings.ToUpper(key) + } + } + return "" +} + // DrawText draws text on the canvas. -func (r *Renderer) DrawText(text render.Text, rect render.Rect) error { +func (r *Renderer) DrawText(text render.Text, point render.Point) error { var ( font *ttf.Font surface *sdl.Surface @@ -48,8 +65,8 @@ func (r *Renderer) DrawText(text render.Text, rect render.Rect) error { defer tex.Destroy() tmp := &sdl.Rect{ - X: rect.X + dx, - Y: rect.Y + dy, + X: point.X + dx, + Y: point.Y + dy, W: surface.W, H: surface.H, } @@ -79,3 +96,28 @@ func (r *Renderer) DrawText(text render.Text, rect render.Rect) error { return err } + +// shiftMap maps keys to their Shift versions. +var shiftMap = map[string]string{ + "`": "~", + "1": "!", + "2": "@", + "3": "#", + "4": "$", + "5": "%", + "6": "^", + "7": "&", + "8": "*", + "9": "(", + "0": ")", + "-": "_", + "=": "+", + "[": "{", + "]": "}", + `\`: "|", + ";": ":", + `'`: `"`, + ",": "<", + ".": ">", + "/": "?", +} diff --git a/scene.go b/scene.go index bc7bac6..26e6865 100644 --- a/scene.go +++ b/scene.go @@ -8,7 +8,13 @@ import "git.kirsle.net/apps/doodle/events" type Scene interface { Name() string Setup(*Doodle) error + + // Loop should update the scene's state but not draw anything. Loop(*Doodle, *events.State) error + + // Draw should use the scene's state to figure out what pixels need + // to draw to the screen. + Draw(*Doodle) error } // Goto a scene. First it unloads the current scene. diff --git a/scene/editor.go b/scene/editor.go deleted file mode 100644 index 11b025a..0000000 --- a/scene/editor.go +++ /dev/null @@ -1,86 +0,0 @@ -package scene - -import "git.kirsle.net/apps/doodle/events" - -// Editor is the drawing mode of the game where the user is clicking and -// dragging to draw pixels. -type Editor struct{} - -func (s *Editor) String() string { - return "Editor" -} - -// Setup the scene. -func (s *Editor) Setup() error { - return nil -} - -// Loop the scene. -func (s *Editor) Loop(ev *events.State) error { - // Taking a screenshot? - if ev.ScreenshotKey.Pressed() { - log.Info("Taking a screenshot") - d.Screenshot() - d.SaveLevel() - } - - // Clear the canvas and fill it with white. - d.renderer.SetDrawColor(255, 255, 255, 255) - d.renderer.Clear() - - // Clicking? Log all the pixels while doing so. - if ev.Button1.Now { - pixel := Pixel{ - start: ev.Button1.Pressed(), - x: ev.CursorX.Now, - y: ev.CursorY.Now, - dx: ev.CursorX.Now, - dy: ev.CursorY.Now, - } - - // Append unique new pixels. - if len(pixelHistory) == 0 || pixelHistory[len(pixelHistory)-1] != pixel { - // If not a start pixel, make the delta coord the previous one. - if !pixel.start && len(pixelHistory) > 0 { - prev := pixelHistory[len(pixelHistory)-1] - pixel.dx = prev.x - pixel.dy = prev.y - } - - pixelHistory = append(pixelHistory, pixel) - - // Save in the pixel canvas map. - d.canvas[pixel] = nil - } - } - - d.renderer.SetDrawColor(0, 0, 0, 255) - for i, pixel := range pixelHistory { - if !pixel.start && i > 0 { - prev := pixelHistory[i-1] - if prev.x == pixel.x && prev.y == pixel.y { - d.renderer.DrawPoint(pixel.x, pixel.y) - } else { - d.renderer.DrawLine( - pixel.x, - pixel.y, - prev.x, - prev.y, - ) - } - } - d.renderer.DrawPoint(pixel.x, pixel.y) - } - - // Draw the FPS. - d.DrawDebugOverlay() - - d.renderer.Present() - - return nil -} - -// Destroy the scene. -func (s *Editor) Destroy() error { - return nil -} diff --git a/scene/log.go b/scene/log.go deleted file mode 100644 index fadeb4b..0000000 --- a/scene/log.go +++ /dev/null @@ -1,9 +0,0 @@ -package scene - -import "github.com/kirsle/golog" - -var log *golog.Logger - -func init() { - log = golog.GetLogger("doodle") -} diff --git a/scene/scene.go b/scene/scene.go deleted file mode 100644 index f9bf712..0000000 --- a/scene/scene.go +++ /dev/null @@ -1,67 +0,0 @@ -package scene - -import ( - "errors" - "fmt" - - "git.kirsle.net/apps/doodle/events" -) - -// Scene is an interface for the top level of a game mode. The game points to -// one Scene at a time, and that Scene has majority control of the main loop, -// and maintains its own state local to that scene. -type Scene interface { - String() string // the scene's name - Setup() error - Loop() error - Destroy() error -} - -// Manager is a type that provides context switching features to manage scenes. -type Manager struct { - events *events.State - scene Scene - ticks uint64 -} - -// NewManager creates the new manager. -func NewManager(events *events.State) Manager { - return Manager{ - events: events, - scene: nil, - } -} - -// Go to a new scene. This tears down the existing scene, sets up the new one, -// and switches control to the new scene. -func (m *Manager) Go(scene Scene) error { - // Already running a scene? - if m.scene != nil { - if err := m.scene.Destroy(); err != nil { - return fmt.Errorf("couldn't destroy scene %s: %s", m.scene, err) - } - m.scene = nil - } - - // Initialize the new scene. - m.scene = scene - return m.scene.Setup() -} - -// Loop the scene manager. This is the game's main loop which runs all the tasks -// that fall in the realm of the scene manager. -func (m *Manager) Loop() error { - if m.scene == nil { - return errors.New("no scene loaded") - } - - // Poll for events. - ev, err := m.events.Poll(m.ticks) - if err != nil { - log.Error("event poll error: %s", err) - return err - } - _ = ev - - return m.scene.Loop() -} diff --git a/shell.go b/shell.go new file mode 100644 index 0000000..0d5d74b --- /dev/null +++ b/shell.go @@ -0,0 +1,252 @@ +package doodle + +import ( + "bytes" + "strings" + + "git.kirsle.net/apps/doodle/balance" + "git.kirsle.net/apps/doodle/events" + "git.kirsle.net/apps/doodle/render" +) + +// Shell implements the developer console in-game. +type Shell struct { + parent *Doodle + Open bool + Prompt string + Text string + History []string + Output []string + Flashes []Flash + Cursor string + cursorFlip uint64 // ticks until cursor flip + cursorRate uint64 +} + +// Flash holds a message to flash on screen. +type Flash struct { + Text string + Expires uint64 // tick that it expires +} + +// NewShell initializes the shell helper (the "Shellper"). +func NewShell(d *Doodle) Shell { + return Shell{ + parent: d, + History: []string{}, + Output: []string{}, + Flashes: []Flash{}, + Prompt: ">", + Cursor: "_", + cursorRate: balance.ShellCursorBlinkRate, + } +} + +// Close the shell, resetting its internal state. +func (s *Shell) Close() { + log.Debug("Shell: closing shell") + s.Open = false + s.Prompt = ">" + s.Text = "" +} + +// Execute a command in the shell. +func (s *Shell) Execute(input string) { + command := s.Parse(input) + err := command.Run(s.parent) + if err != nil { + s.Write(err.Error()) + } + + if command.Raw != "" { + s.History = append(s.History, command.Raw) + } + + // Reset the text buffer in the shell. + s.Text = "" +} + +// Write a line of output text to the console. +func (s *Shell) Write(line string) { + s.Output = append(s.Output, line) + s.Flashes = append(s.Flashes, Flash{ + Text: line, + Expires: s.parent.ticks + balance.FlashTTL, + }) +} + +// Parse the command line. +func (s *Shell) Parse(input string) Command { + input = strings.TrimSpace(input) + if len(input) == 0 { + return Command{} + } + + var ( + inQuote bool + buffer = bytes.NewBuffer([]byte{}) + words = []string{} + ) + for i := 0; i < len(input); i++ { + char := input[i] + switch char { + case ' ': + if inQuote { + buffer.WriteByte(char) + continue + } + + if word := buffer.String(); word != "" { + words = append(words, word) + buffer.Reset() + } + case '"': + if !inQuote { + // An opening quote character. + inQuote = true + } else { + // The closing quote. + inQuote = false + + if word := buffer.String(); word != "" { + words = append(words, word) + buffer.Reset() + } + } + default: + buffer.WriteByte(char) + } + } + + if remainder := buffer.String(); remainder != "" { + words = append(words, remainder) + } + + return Command{ + Raw: input, + Command: words[0], + Args: words[1:], + ArgsLiteral: strings.TrimSpace(input[len(words[0]):]), + } +} + +// Draw the shell. +func (s *Shell) Draw(d *Doodle, ev *events.State) error { + if ev.EscapeKey.Read() { + s.Close() + return nil + } else if ev.EnterKey.Read() || ev.EscapeKey.Read() { + s.Execute(s.Text) + s.Close() + return nil + } + + // Compute the line height we can draw. + lineHeight := balance.ShellFontSize + int(balance.ShellPadding) + + // If the console is open, draw the console. + if s.Open { + // Cursor flip? + if d.ticks > s.cursorFlip { + s.cursorFlip = d.ticks + s.cursorRate + if s.Cursor == "" { + s.Cursor = "_" + } else { + s.Cursor = "" + } + } + + // Read a character from the keyboard. + if key := ev.ReadKey(); key != "" { + // Backspace? + if key == `\b` { + if len(s.Text) > 0 { + s.Text = s.Text[:len(s.Text)-1] + } + } else { + s.Text += key + } + } + + // How tall is the box? + boxHeight := int32(lineHeight*(balance.ShellHistoryLineCount+1)) + balance.ShellPadding + + // Draw the background color. + d.Engine.DrawBox( + balance.ShellBackgroundColor, + render.Rect{ + X: 0, + Y: d.height - boxHeight, + W: d.width, + H: boxHeight, + }, + ) + + // Draw the recent commands. + outputY := d.height - int32(lineHeight*2) + for i := 0; i < balance.ShellHistoryLineCount; i++ { + if len(s.Output) > i { + line := s.Output[len(s.Output)-1-i] + d.Engine.DrawText( + render.Text{ + Text: line, + Size: balance.ShellFontSize, + Color: render.Grey, + }, + render.Point{ + X: balance.ShellPadding, + Y: outputY, + }, + ) + } + outputY -= int32(lineHeight) + } + + // Draw the command prompt. + d.Engine.DrawText( + render.Text{ + Text: s.Prompt + s.Text + s.Cursor, + Size: balance.ShellFontSize, + Color: balance.ShellForegroundColor, + }, + render.Point{ + X: balance.ShellPadding, + Y: d.height - int32(balance.ShellFontSize) - balance.ShellPadding, + }, + ) + } else if len(s.Flashes) > 0 { + // Otherwise, just draw flashed messages. + valid := false // Did we actually draw any? + + outputY := d.height - int32(lineHeight*2) + for i := len(s.Flashes); i > 0; i-- { + flash := s.Flashes[i-1] + if d.ticks >= flash.Expires { + continue + } + + d.Engine.DrawText( + render.Text{ + Text: flash.Text, + Size: balance.ShellFontSize, + Color: render.SkyBlue, + Stroke: render.Grey, + Shadow: render.Black, + }, + render.Point{ + X: balance.ShellPadding, + Y: outputY, + }, + ) + outputY -= int32(lineHeight) + valid = true + } + + // If we've exhausted all flashes, free up the memory. + if !valid { + s.Flashes = []Flash{} + } + } + + return nil +}