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.
This commit is contained in:
Noah 2018-07-21 20:43:01 -07:00
parent 30be42c343
commit 9356502a50
18 changed files with 610 additions and 210 deletions

17
balance/shell.go Normal file
View File

@ -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
)

102
commands.go Normal file
View File

@ -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 <filename.json>")
}
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 <file name>")
}
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 <file name>")
}
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,
)
}

View File

@ -4,7 +4,6 @@ import (
"fmt" "fmt"
"time" "time"
"git.kirsle.net/apps/doodle/events"
"git.kirsle.net/apps/doodle/render" "git.kirsle.net/apps/doodle/render"
"github.com/kirsle/golog" "github.com/kirsle/golog"
) )
@ -28,10 +27,12 @@ type Doodle struct {
startTime time.Time startTime time.Time
running bool running bool
ticks uint64 ticks uint64
events *events.State
width int32 width int32
height int32 height int32
// Command line shell options.
shell Shell
scene Scene scene Scene
} }
@ -41,11 +42,11 @@ func New(debug bool, engine render.Engine) *Doodle {
Debug: debug, Debug: debug,
Engine: engine, Engine: engine,
startTime: time.Now(), startTime: time.Now(),
events: events.New(),
running: true, running: true,
width: 800, width: 800,
height: 600, height: 600,
} }
d.shell = NewShell(d)
if !debug { if !debug {
log.Config.Level = golog.InfoLevel log.Config.Level = golog.InfoLevel
@ -68,6 +69,8 @@ func (d *Doodle) Run() error {
log.Info("Enter Main Loop") log.Info("Enter Main Loop")
for d.running { for d.running {
d.Engine.Clear(render.White)
start := time.Now() // Record how long this frame took. start := time.Now() // Record how long this frame took.
d.ticks++ d.ticks++
@ -79,24 +82,43 @@ func (d *Doodle) Run() error {
break break
} }
// Global event handlers. // Command line shell.
if ev.EscapeKey.Pressed() { if d.shell.Open {
log.Error("Escape key pressed, shutting down")
d.running = false } else if ev.EnterKey.Read() {
break 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. // Draw the scene.
err = d.scene.Loop(d, ev) d.scene.Draw(d)
// Draw the shell.
err = d.shell.Draw(d, ev)
if err != nil { if err != nil {
return err log.Error("shell error: %s", err)
d.running = false
break
} }
// Draw the debug overlay over all scenes. // Draw the debug overlay over all scenes.
d.DrawDebugOverlay() d.DrawDebugOverlay()
// Render the pixels to the screen. // Render the pixels to the screen.
err = d.Engine.Draw() err = d.Engine.Present()
if err != nil { if err != nil {
log.Error("draw error: %s", err) log.Error("draw error: %s", err)
d.running = false d.running = false
@ -114,12 +136,22 @@ func (d *Doodle) Run() error {
// Track how long this frame took to measure FPS over time. // Track how long this frame took to measure FPS over time.
d.TrackFPS(delay) d.TrackFPS(delay)
// Consume any lingering key sym.
ev.KeyName.Read()
} }
log.Warn("Main Loop Exited! Shutting down...") log.Warn("Main Loop Exited! Shutting down...")
return nil 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. // EditLevel loads a map from JSON into the EditorScene.
func (d *Doodle) EditLevel(filename string) error { func (d *Doodle) EditLevel(filename string) error {
log.Info("Loading level from file: %s", filename) log.Info("Loading level from file: %s", filename)
@ -144,7 +176,7 @@ func (d *Doodle) PlayLevel(filename string) error {
return nil return nil
} }
// TODO: not a global // Pixel TODO: not a global
type Pixel struct { type Pixel struct {
start bool start bool
x int32 x int32

View File

@ -19,6 +19,7 @@ type EditorScene struct {
// History of all the pixels placed by the user. // History of all the pixels placed by the user.
pixelHistory []Pixel pixelHistory []Pixel
canvas Grid canvas Grid
filename string // Last saved filename.
// Canvas size // Canvas size
width int32 width int32
@ -49,7 +50,6 @@ func (s *EditorScene) Loop(d *Doodle, ev *events.State) error {
if ev.ScreenshotKey.Pressed() { if ev.ScreenshotKey.Pressed() {
log.Info("Taking a screenshot") log.Info("Taking a screenshot")
s.Screenshot() s.Screenshot()
s.SaveLevel()
} }
// Clear the canvas and fill it with white. // 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 { for i, pixel := range s.pixelHistory {
if !pixel.start && i > 0 { if !pixel.start && i > 0 {
prev := s.pixelHistory[i-1] 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. // LoadLevel loads a level from disk.
func (s *EditorScene) LoadLevel(filename string) error { func (s *EditorScene) LoadLevel(filename string) error {
s.filename = filename
s.pixelHistory = []Pixel{} s.pixelHistory = []Pixel{}
s.canvas = Grid{} s.canvas = Grid{}
@ -129,7 +135,8 @@ func (s *EditorScene) LoadLevel(filename string) error {
} }
// SaveLevel saves the level to disk. // SaveLevel saves the level to disk.
func (s *EditorScene) SaveLevel() { func (s *EditorScene) SaveLevel(filename string) {
s.filename = filename
m := level.Level{ m := level.Level{
Version: 1, Version: 1,
Title: "Alpha", Title: "Alpha",
@ -161,9 +168,6 @@ func (s *EditorScene) SaveLevel() {
return return
} }
filename := fmt.Sprintf("./map-%s.json",
time.Now().Format("2006-01-02T15-04-05"),
)
err = ioutil.WriteFile(filename, json, 0644) err = ioutil.WriteFile(filename, json, 0644)
if err != nil { if err != nil {
log.Error("Create map file error: %s", err) log.Error("Create map file error: %s", err)

View File

@ -1,6 +1,8 @@
// Package events manages mouse and keyboard SDL events for Doodle. // Package events manages mouse and keyboard SDL events for Doodle.
package events package events
import "strings"
// State keeps track of event states. // State keeps track of event states.
type State struct { type State struct {
// Mouse buttons. // Mouse buttons.
@ -10,6 +12,9 @@ type State struct {
// Screenshot key. // Screenshot key.
ScreenshotKey *BoolTick ScreenshotKey *BoolTick
EscapeKey *BoolTick EscapeKey *BoolTick
EnterKey *BoolTick
ShiftActive *BoolTick
KeyName *StringTick
Up *BoolTick Up *BoolTick
Left *BoolTick Left *BoolTick
Right *BoolTick Right *BoolTick
@ -27,6 +32,9 @@ func New() *State {
Button2: &BoolTick{}, Button2: &BoolTick{},
ScreenshotKey: &BoolTick{}, ScreenshotKey: &BoolTick{},
EscapeKey: &BoolTick{}, EscapeKey: &BoolTick{},
EnterKey: &BoolTick{},
ShiftActive: &BoolTick{},
KeyName: &StringTick{},
Up: &BoolTick{}, Up: &BoolTick{},
Left: &BoolTick{}, Left: &BoolTick{},
Right: &BoolTick{}, Right: &BoolTick{},
@ -35,3 +43,43 @@ func New() *State {
CursorY: &Int32Tick{}, 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": ")",
"-": "_",
"=": "+",
"[": "{",
"]": "}",
`\`: "|",
";": ":",
`'`: `"`,
",": "<",
".": ">",
"/": "?",
}

View File

@ -12,6 +12,12 @@ type Int32Tick struct {
Last int32 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. // Push a bool state, copying the current Now value to Last.
func (bs *BoolTick) Push(v bool) { func (bs *BoolTick) Push(v bool) {
bs.Last = bs.Now bs.Last = bs.Now
@ -23,8 +29,28 @@ func (bs *BoolTick) Pressed() bool {
return bs.Now && !bs.Last 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. // Push an int32 state, copying the current Now value to Last.
func (is *Int32Tick) Push(v int32) { func (is *Int32Tick) Push(v int32) {
is.Last = is.Now is.Last = is.Now
is.Now = v 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
}

21
fps.go
View File

@ -2,7 +2,6 @@ package doodle
import ( import (
"fmt" "fmt"
"time"
"git.kirsle.net/apps/doodle/render" "git.kirsle.net/apps/doodle/render"
) )
@ -26,11 +25,9 @@ func (d *Doodle) DrawDebugOverlay() {
} }
label := fmt.Sprintf( label := fmt.Sprintf(
"FPS: %d (%dms) (%d,%d) S:%s F12=screenshot", "FPS: %d (%dms) S:%s F12=screenshot",
fpsCurrent, fpsCurrent,
fpsSkipped, fpsSkipped,
d.events.CursorX.Now,
d.events.CursorY.Now,
d.scene.Name(), d.scene.Name(),
) )
@ -42,11 +39,9 @@ func (d *Doodle) DrawDebugOverlay() {
Stroke: DebugTextStroke, Stroke: DebugTextStroke,
Shadow: DebugTextShadow, Shadow: DebugTextShadow,
}, },
render.Rect{ render.Point{
X: DebugTextPadding, X: DebugTextPadding,
Y: DebugTextPadding, Y: DebugTextPadding,
W: d.width,
H: d.height,
}, },
) )
if err != nil { if err != nil {
@ -65,12 +60,12 @@ func (d *Doodle) TrackFPS(skipped uint32) {
} }
if fpsLastTime < fpsCurrentTicks-fpsInterval { if fpsLastTime < fpsCurrentTicks-fpsInterval {
log.Debug("Uptime: %s FPS: %d deltaTicks: %d skipped: %dms", // log.Debug("Uptime: %s FPS: %d deltaTicks: %d skipped: %dms",
time.Now().Sub(d.startTime), // time.Now().Sub(d.startTime),
fpsCurrent, // fpsCurrent,
fpsCurrentTicks-fpsLastTime, // fpsCurrentTicks-fpsLastTime,
skipped, // skipped,
) // )
fpsLastTime = fpsCurrentTicks fpsLastTime = fpsCurrentTicks
fpsCurrent = fpsFrames fpsCurrent = fpsFrames

View File

@ -42,9 +42,7 @@ func (s *PlayScene) Setup(d *Doodle) error {
// Loop the editor scene. // Loop the editor scene.
func (s *PlayScene) Loop(d *Doodle, ev *events.State) error { func (s *PlayScene) Loop(d *Doodle, ev *events.State) error {
s.movePlayer(ev) s.movePlayer(ev)
return nil
// Apply gravity.
return s.Draw(d)
} }
// Draw the pixels on this frame. // Draw the pixels on this frame.
@ -57,7 +55,6 @@ func (s *PlayScene) Draw(d *Doodle) error {
} }
// Draw our hero. // 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}) d.Engine.DrawRect(render.Magenta, render.Rect{s.x, s.y, 16, 16})
return nil return nil

View File

@ -15,15 +15,16 @@ type Engine interface {
Poll() (*events.State, error) Poll() (*events.State, error)
GetTicks() uint32 GetTicks() uint32
// Draw presents the current state to the screen. // Present presents the current state to the screen.
Draw() error Present() error
// Clear the full canvas and set this color. // Clear the full canvas and set this color.
Clear(Color) Clear(Color)
DrawPoint(Color, Point) DrawPoint(Color, Point)
DrawLine(Color, Point, Point) DrawLine(Color, Point, Point)
DrawRect(Color, Rect) 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, // Delay for a moment using the render engine's delay method,
// implemented by sdl.Delay(uint32) // implemented by sdl.Delay(uint32)

View File

@ -42,3 +42,16 @@ func (r *Renderer) DrawRect(color render.Color, rect render.Rect) {
H: rect.H, 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,
})
}

View File

@ -8,6 +8,7 @@ var log *golog.Logger
var ( var (
DebugMouseEvents = false DebugMouseEvents = false
DebugClickEvents = false DebugClickEvents = false
DebugKeyEvents = false
) )
func init() { func init() {

View File

@ -159,16 +159,23 @@ func (r *Renderer) Poll() (*events.State, error) {
) )
} }
case *sdl.KeyboardEvent: case *sdl.KeyboardEvent:
log.Debug("[%d ms] tick:%d Keyboard type:%d sym:%c modifiers:%d state:%d repeat:%d\n", if DebugKeyEvents {
t.Timestamp, r.ticks, t.Type, t.Keysym.Sym, t.Keysym.Mod, t.State, t.Repeat, 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
} }
switch t.Keysym.Scancode { switch t.Keysym.Scancode {
case sdl.SCANCODE_ESCAPE: case sdl.SCANCODE_ESCAPE:
if t.Repeat == 1 {
continue
}
s.EscapeKey.Push(t.State == 1) 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: case sdl.SCANCODE_F12:
s.ScreenshotKey.Push(t.State == 1) s.ScreenshotKey.Push(t.State == 1)
case sdl.SCANCODE_UP: case sdl.SCANCODE_UP:
@ -179,6 +186,25 @@ func (r *Renderer) Poll() (*events.State, error) {
s.Right.Push(t.State == 1) s.Right.Push(t.State == 1)
case sdl.SCANCODE_DOWN: case sdl.SCANCODE_DOWN:
s.Down.Push(t.State == 1) 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 return s, nil
} }
// Draw a single frame. // Present the current frame.
func (r *Renderer) Draw() error { func (r *Renderer) Present() error {
r.renderer.Present() r.renderer.Present()
return nil return nil
} }

View File

@ -1,6 +1,9 @@
package sdl package sdl
import ( import (
"strings"
"git.kirsle.net/apps/doodle/events"
"git.kirsle.net/apps/doodle/render" "git.kirsle.net/apps/doodle/render"
"github.com/veandco/go-sdl2/sdl" "github.com/veandco/go-sdl2/sdl"
"github.com/veandco/go-sdl2/ttf" "github.com/veandco/go-sdl2/ttf"
@ -23,8 +26,22 @@ func LoadFont(size int) (*ttf.Font, error) {
return font, nil 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. // 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 ( var (
font *ttf.Font font *ttf.Font
surface *sdl.Surface surface *sdl.Surface
@ -48,8 +65,8 @@ func (r *Renderer) DrawText(text render.Text, rect render.Rect) error {
defer tex.Destroy() defer tex.Destroy()
tmp := &sdl.Rect{ tmp := &sdl.Rect{
X: rect.X + dx, X: point.X + dx,
Y: rect.Y + dy, Y: point.Y + dy,
W: surface.W, W: surface.W,
H: surface.H, H: surface.H,
} }
@ -79,3 +96,28 @@ func (r *Renderer) DrawText(text render.Text, rect render.Rect) error {
return err return err
} }
// shiftMap maps keys to their Shift versions.
var shiftMap = map[string]string{
"`": "~",
"1": "!",
"2": "@",
"3": "#",
"4": "$",
"5": "%",
"6": "^",
"7": "&",
"8": "*",
"9": "(",
"0": ")",
"-": "_",
"=": "+",
"[": "{",
"]": "}",
`\`: "|",
";": ":",
`'`: `"`,
",": "<",
".": ">",
"/": "?",
}

View File

@ -8,7 +8,13 @@ import "git.kirsle.net/apps/doodle/events"
type Scene interface { type Scene interface {
Name() string Name() string
Setup(*Doodle) error Setup(*Doodle) error
// Loop should update the scene's state but not draw anything.
Loop(*Doodle, *events.State) error 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. // Goto a scene. First it unloads the current scene.

View File

@ -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
}

View File

@ -1,9 +0,0 @@
package scene
import "github.com/kirsle/golog"
var log *golog.Logger
func init() {
log = golog.GetLogger("doodle")
}

View File

@ -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()
}

252
shell.go Normal file
View File

@ -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
}