Add JS + History to Shell, Add Main Scene

* The shell now supports an "eval" command, or "$" for short.
  * Runs it in an Otto JavaScript VM.
  * Some global variables are available, like `d` is the Doodle object
    itself, `log`, `RGBA()` and `Point()`
* The shell supports paging through input history using the arrow keys.
* Added an initial Main Scene
This commit is contained in:
Noah 2018-07-25 19:38:54 -07:00
parent 94c1df050b
commit 41e1838549
12 changed files with 327 additions and 112 deletions

46
Debug Notes.md Normal file
View File

@ -0,0 +1,46 @@
# Debug Notes
## Entering Debug Mode
Command line argument:
```bash
% doodle -debug
# running the dev build uses debug mode by default
% make run
```
In the developer console:
```dos
> boolProp Debug true
> boolProp D true
```
## Debug Options
The `boolProp` command can also be used to toggle on and off different
debug options while the game is running.
```
DebugOverlay
DO
Toggles the main debug text overlay with FPS counter.
DebugCollision
DC
Toggles the collision detection bounding box lines.
```
## JavaScript Shell
The developer console can parse JavaScript commands for more access to the
game's internal objects.
The following global variables are available to the shell:
* `d` is the master Doodle struct.
* `log` is the master logger object for logging messages to the terminal.
* `RGBA()` is the `render.RGBA()` function for creating a Color value.
* `Point(x, y)` to create a `render.Point`

View File

@ -147,9 +147,46 @@ As a rough idea of the milestones needed for this game to work:
* [ ] UI Manager that will keep track of buttons to know when the mouse * [ ] UI Manager that will keep track of buttons to know when the mouse
is interacting with them. is interacting with them.
* [ ] Frames * [ ] Frames
* Like Buttons, can have border (raised, sunken or solid), padding and
background color.
* [ ] Should be able to size themselves dynamically based on child widgets.
* [ ] Windows (fixed, non-draggable is OK) * [ ] Windows (fixed, non-draggable is OK)
* [ ] Title bar with label
* [ ] Window body implements a Frame that contains child widgets.
* [ ] Window can resize itself dynamically based on the Frame.
* [ ] Create a "Main Menu" scene with buttons to enter a new Edit Mode,
play an existing map from disk, etc.
* [ ] Add user interface Frames or Windows to the Edit Mode.
* [ ] A toolbar of buttons (New, Save, Open, Play) can be drawn at the top
before the UI toolkit gains a proper MenuBar widget.
* [ ] Expand the Palette support in levels for solid vs. transparent, fire, * [ ] Expand the Palette support in levels for solid vs. transparent, fire,
etc. with UI toolbar to choose palettes. etc. with UI toolbar to choose palettes.
Lesser important UI features that can come at any later time:
* [ ] MenuBar widget with drop-down menu support.
* [ ] Checkbox and Radiobox widgets.
* [ ] Text Entry widgets (in the meantime use the Developer Shell to prompt for
text input questions)
## Doodad Editor
* [ ] The Edit Mode should support creating drawings for Doodads.
* [ ] It should know whether you're drawing a Map or a Doodad as some
behaviors may need to be different between the two.
* [ ] Compress the coordinates down toward `(0,0)` when saving a Doodad,
by finding the toppest, leftest point and making that `(0,0)` and adjusting
the rest accordingly. This will help trim down Doodads into the smallest
possible space for easy collision detection.
* [ ] Add a UX to edit multiple frames for a Doodad.
* [ ] Edit Mode should be able to fully save the drawings and frames, and an
external CLI tool can install the JavaScript into them.
* [ ] Possible UX to toggle Doodad options, like its collision rules and
whether the Doodad is continued to be "mobile" (i.e. doors and buttons won't
move, but items and enemies may be able to; and non-mobile Doodads don't
need to collision check against level geometry).
* [ ] Edit Mode should have a Doodad Palette (Frame or Window) to drag
Doodads into the map.
* [ ] ??? * [ ] ???
# Building # Building

View File

@ -3,6 +3,7 @@ package doodle
import ( import (
"errors" "errors"
"fmt" "fmt"
"strconv"
) )
// Command is a parsed shell command. // Command is a parsed shell command.
@ -36,6 +37,13 @@ func (c Command) Run(d *Doodle) error {
return c.Quit() return c.Quit()
case "help": case "help":
return c.Help(d) return c.Help(d)
case "eval":
case "$":
out, err := d.shell.js.Run(c.ArgsLiteral)
d.Flash("%+v", out)
return err
case "boolProp":
return c.BoolProp(d)
default: default:
return c.Default() return c.Default()
} }
@ -91,7 +99,7 @@ func (c Command) Help(d *Doodle) error {
// Save the current map to disk. // Save the current map to disk.
func (c Command) Save(d *Doodle) error { func (c Command) Save(d *Doodle) error {
if scene, ok := d.scene.(*EditorScene); ok { if scene, ok := d.Scene.(*EditorScene); ok {
filename := "" filename := ""
if len(c.Args) > 0 { if len(c.Args) > 0 {
filename = c.Args[0] filename = c.Args[0]
@ -139,6 +147,42 @@ func (c Command) Quit() error {
return nil return nil
} }
// BoolProp command sets available boolean variables.
func (c Command) BoolProp(d *Doodle) error {
if len(c.Args) != 2 {
return errors.New("Usage: boolProp <name> <true or false>")
}
var (
name = c.Args[0]
value = c.Args[1]
truthy = value[0] == 't' || value[0] == 'T' || value[0] == '1'
ok = true
)
switch name {
case "Debug":
case "D":
d.Debug = truthy
case "DebugOverlay":
case "DO":
DebugOverlay = truthy
case "DebugCollision":
case "DC":
DebugCollision = truthy
default:
ok = false
}
if ok {
d.Flash("Set boolProp %s=%s", name, strconv.FormatBool(truthy))
} else {
d.Flash("Unknown boolProp name %s", name)
}
return nil
}
// Default command. // Default command.
func (c Command) Default() error { func (c Command) Default() error {
return fmt.Errorf("%s: command not found. Try `help` for help", return fmt.Errorf("%s: command not found. Try `help` for help",

View File

@ -32,7 +32,7 @@ type Doodle struct {
// Command line shell options. // Command line shell options.
shell Shell shell Shell
scene Scene Scene Scene
} }
// New initializes the game object. // New initializes the game object.
@ -62,8 +62,8 @@ func (d *Doodle) Run() error {
} }
// Set up the default scene. // Set up the default scene.
if d.scene == nil { if d.Scene == nil {
d.Goto(&EditorScene{}) d.Goto(&MainScene{})
} }
log.Info("Enter Main Loop") log.Info("Enter Main Loop")
@ -96,14 +96,14 @@ func (d *Doodle) Run() error {
} }
// Run the scene's logic. // Run the scene's logic.
err = d.scene.Loop(d, ev) err = d.Scene.Loop(d, ev)
if err != nil { if err != nil {
return err return err
} }
} }
// Draw the scene. // Draw the scene.
d.scene.Draw(d) d.Scene.Draw(d)
// Draw the shell. // Draw the shell.
err = d.shell.Draw(d, ev) err = d.shell.Draw(d, ev)
@ -114,7 +114,7 @@ func (d *Doodle) Run() error {
} }
// 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.Present() err = d.Engine.Present()

View File

@ -11,7 +11,6 @@ import (
"git.kirsle.net/apps/doodle/events" "git.kirsle.net/apps/doodle/events"
"git.kirsle.net/apps/doodle/level" "git.kirsle.net/apps/doodle/level"
"git.kirsle.net/apps/doodle/render" "git.kirsle.net/apps/doodle/render"
"git.kirsle.net/apps/doodle/ui"
) )
// EditorScene manages the "Edit Level" game mode. // EditorScene manages the "Edit Level" game mode.
@ -133,59 +132,6 @@ func (s *EditorScene) Loop(d *Doodle, ev *events.State) error {
func (s *EditorScene) Draw(d *Doodle) error { func (s *EditorScene) Draw(d *Doodle) error {
s.canvas.Draw(d.Engine) s.canvas.Draw(d.Engine)
label := ui.NewLabel(render.Text{
Text: "Hello UI toolkit!",
Size: 26,
Color: render.Pink,
Stroke: render.SkyBlue,
Shadow: render.Black,
})
label.SetPoint(render.NewPoint(128, 128))
label.Compute(d.Engine)
log.Info("Label rect: %+v", label.Size())
log.Info("Label at: %s", label.Point())
label.Present(d.Engine)
button := ui.NewButton(*ui.NewLabel(render.Text{
Text: "Hello",
Size: 14,
Color: render.Black,
}))
button.SetPoint(render.NewPoint(200, 200))
button.Present(d.Engine)
// Point and size of that button
point := button.Point()
size := button.Size()
button2 := ui.NewButton(*ui.NewLabel(render.Text{
Text: "World!",
Size: 14,
Color: render.Blue,
}))
button2.SetPoint(render.Point{
X: point.X + size.W,
Y: point.Y,
})
button2.Present(d.Engine)
button.SetText("Buttons that don't click yet")
button.SetPoint(render.NewPoint(250, 300))
button.Label.Text.Size = 24
button.Border = 8
button.Outline = 4
button.Present(d.Engine)
button2.SetText("Multiple colors, too")
button2.Label.Text.Color = render.White
button2.Background = render.RGBA(0, 153, 255, 255)
button2.HighlightColor = render.RGBA(100, 200, 255, 255)
button2.ShadowColor = render.RGBA(0, 100, 153, 255)
button2.SetPoint(render.NewPoint(10, 300))
button2.Present(d.Engine)
_ = label
return nil return nil
} }

22
fps.go
View File

@ -10,6 +10,13 @@ import (
// Frames to cache for FPS calculation. // Frames to cache for FPS calculation.
const maxSamples = 100 const maxSamples = 100
// Debug mode options, these can be enabled in the dev console
// like: boolProp DebugOverlay true
var (
DebugOverlay = true
DebugCollision = true
)
var ( var (
fpsCurrentTicks uint32 // current time we get sdl.GetTicks() fpsCurrentTicks uint32 // current time we get sdl.GetTicks()
fpsLastTime uint32 // last time we printed the fpsCurrentTicks fpsLastTime uint32 // last time we printed the fpsCurrentTicks
@ -21,7 +28,7 @@ var (
// DrawDebugOverlay draws the debug FPS text on the SDL canvas. // DrawDebugOverlay draws the debug FPS text on the SDL canvas.
func (d *Doodle) DrawDebugOverlay() { func (d *Doodle) DrawDebugOverlay() {
if !d.Debug { if !d.Debug || !DebugOverlay {
return return
} }
@ -29,7 +36,7 @@ func (d *Doodle) DrawDebugOverlay() {
"FPS: %d (%dms) S:%s F12=screenshot", "FPS: %d (%dms) S:%s F12=screenshot",
fpsCurrent, fpsCurrent,
fpsSkipped, fpsSkipped,
d.scene.Name(), d.Scene.Name(),
) )
err := d.Engine.DrawText( err := d.Engine.DrawText(
@ -52,6 +59,10 @@ func (d *Doodle) DrawDebugOverlay() {
// DrawCollisionBox draws the collision box around a Doodad. // DrawCollisionBox draws the collision box around a Doodad.
func (d *Doodle) DrawCollisionBox(actor doodads.Doodad) { func (d *Doodle) DrawCollisionBox(actor doodads.Doodad) {
if !d.Debug || !DebugCollision {
return
}
var ( var (
rect = doodads.GetBoundingRect(actor) rect = doodads.GetBoundingRect(actor)
box = doodads.GetCollisionBox(rect) box = doodads.GetCollisionBox(rect)
@ -74,13 +85,6 @@ func (d *Doodle) TrackFPS(skipped uint32) {
} }
if fpsLastTime < fpsCurrentTicks-fpsInterval { if fpsLastTime < fpsCurrentTicks-fpsInterval {
// log.Debug("Uptime: %s FPS: %d deltaTicks: %d skipped: %dms",
// time.Now().Sub(d.startTime),
// fpsCurrent,
// fpsCurrentTicks-fpsLastTime,
// skipped,
// )
fpsLastTime = fpsCurrentTicks fpsLastTime = fpsCurrentTicks
fpsCurrent = fpsFrames fpsCurrent = fpsFrames
fpsFrames = 0 fpsFrames = 0

74
main_scene.go Normal file
View File

@ -0,0 +1,74 @@
package doodle
import (
"git.kirsle.net/apps/doodle/events"
"git.kirsle.net/apps/doodle/render"
"git.kirsle.net/apps/doodle/ui"
)
// MainScene implements the main menu of Doodle.
type MainScene struct {
}
// Name of the scene.
func (s *MainScene) Name() string {
return "Main"
}
// Setup the scene.
func (s *MainScene) Setup(d *Doodle) error {
return nil
}
// Loop the editor scene.
func (s *MainScene) Loop(d *Doodle, ev *events.State) error {
return nil
}
// Draw the pixels on this frame.
func (s *MainScene) Draw(d *Doodle) error {
// Clear the canvas and fill it with white.
d.Engine.Clear(render.White)
label := ui.NewLabel(render.Text{
Text: "Doodle v" + Version,
Size: 26,
Color: render.Pink,
Stroke: render.SkyBlue,
Shadow: render.Black,
})
label.Compute(d.Engine)
label.MoveTo(render.Point{
X: (d.width / 2) - (label.Size().W / 2),
Y: 120,
})
label.Present(d.Engine)
button := ui.NewButton(*ui.NewLabel(render.Text{
Text: "New Map",
Size: 14,
Color: render.Black,
}))
button.Compute(d.Engine)
button.MoveTo(render.Point{
X: (d.width / 2) - (button.Size().W / 2),
Y: 200,
})
button.Present(d.Engine)
button.SetText("Load Map")
button.Compute(d.Engine)
button.MoveTo(render.Point{
X: (d.width / 2) - (button.Size().W / 2),
Y: 260,
})
button.Present(d.Engine)
return nil
}
// Destroy the scene.
func (s *MainScene) Destroy() error {
return nil
}

View File

@ -21,7 +21,7 @@ type PlayScene struct {
height int32 height int32
// Player character // Player character
player doodads.Doodad Player doodads.Doodad
} }
// Name of the scene. // Name of the scene.
@ -42,7 +42,7 @@ func (s *PlayScene) Setup(d *Doodle) error {
s.Filename = "" s.Filename = ""
} }
s.player = doodads.NewPlayer() s.Player = doodads.NewPlayer()
if s.canvas == nil { if s.canvas == nil {
log.Debug("PlayScene.Setup: no grid given, initializing empty grid") log.Debug("PlayScene.Setup: no grid given, initializing empty grid")
@ -80,17 +80,17 @@ func (s *PlayScene) Draw(d *Doodle) error {
s.canvas.Draw(d.Engine) s.canvas.Draw(d.Engine)
// Draw our hero. // Draw our hero.
s.player.Draw(d.Engine) s.Player.Draw(d.Engine)
// Draw out bounding boxes. // Draw out bounding boxes.
d.DrawCollisionBox(s.player) d.DrawCollisionBox(s.Player)
return nil return nil
} }
// movePlayer updates the player's X,Y coordinate based on key pressed. // movePlayer updates the player's X,Y coordinate based on key pressed.
func (s *PlayScene) movePlayer(ev *events.State) { func (s *PlayScene) movePlayer(ev *events.State) {
delta := s.player.Position() delta := s.Player.Position()
var playerSpeed int32 = 8 var playerSpeed int32 = 8
var gravity int32 = 2 var gravity int32 = 2
@ -110,20 +110,20 @@ func (s *PlayScene) movePlayer(ev *events.State) {
// Apply gravity. // Apply gravity.
// var onFloor bool // var onFloor bool
info, ok := doodads.CollidesWithGrid(s.player, &s.canvas, delta) info, ok := doodads.CollidesWithGrid(s.Player, &s.canvas, delta)
if ok { if ok {
// Collision happened with world. // Collision happened with world.
} }
delta = info.MoveTo delta = info.MoveTo
// Apply gravity if not grounded. // Apply gravity if not grounded.
if !s.player.Grounded() { if !s.Player.Grounded() {
// Gravity has to pipe through the collision checker, too, so it // Gravity has to pipe through the collision checker, too, so it
// can't give us a cheated downward boost. // can't give us a cheated downward boost.
delta.Y += gravity delta.Y += gravity
} }
s.player.MoveTo(delta) s.Player.MoveTo(delta)
} }
// LoadLevel loads a level from disk. // LoadLevel loads a level from disk.

View File

@ -21,11 +21,11 @@ type Scene interface {
// Goto a scene. First it unloads the current scene. // Goto a scene. First it unloads the current scene.
func (d *Doodle) Goto(scene Scene) error { func (d *Doodle) Goto(scene Scene) error {
// Teardown existing scene. // Teardown existing scene.
if d.scene != nil { if d.Scene != nil {
d.scene.Destroy() d.Scene.Destroy()
} }
log.Info("Goto Scene") log.Info("Goto Scene: %s", scene.Name())
d.scene = scene d.Scene = scene
return d.scene.Setup(d) return d.Scene.Setup(d)
} }

View File

@ -8,6 +8,7 @@ import (
"git.kirsle.net/apps/doodle/balance" "git.kirsle.net/apps/doodle/balance"
"git.kirsle.net/apps/doodle/events" "git.kirsle.net/apps/doodle/events"
"git.kirsle.net/apps/doodle/render" "git.kirsle.net/apps/doodle/render"
"github.com/robertkrimen/otto"
) )
// Flash a message to the user. // Flash a message to the user.
@ -18,15 +19,25 @@ func (d *Doodle) Flash(template string, v ...interface{}) {
// Shell implements the developer console in-game. // Shell implements the developer console in-game.
type Shell struct { type Shell struct {
parent *Doodle parent *Doodle
Open bool Open bool
Prompt string Prompt string
Text string Text string
History []string History []string
Output []string Output []string
Flashes []Flash Flashes []Flash
Cursor string
// Blinky cursor variables.
cursor byte // cursor symbol
cursorFlip uint64 // ticks until cursor flip cursorFlip uint64 // ticks until cursor flip
cursorRate uint64 cursorRate uint64
// Paging through history variables.
historyPaging bool
historyIndex int
// JavaScript shell interpreter.
js *otto.Otto
} }
// Flash holds a message to flash on screen. // Flash holds a message to flash on screen.
@ -37,15 +48,32 @@ type Flash struct {
// NewShell initializes the shell helper (the "Shellper"). // NewShell initializes the shell helper (the "Shellper").
func NewShell(d *Doodle) Shell { func NewShell(d *Doodle) Shell {
return Shell{ s := Shell{
parent: d, parent: d,
History: []string{}, History: []string{},
Output: []string{}, Output: []string{},
Flashes: []Flash{}, Flashes: []Flash{},
Prompt: ">", Prompt: ">",
Cursor: "_", cursor: '_',
cursorRate: balance.ShellCursorBlinkRate, cursorRate: balance.ShellCursorBlinkRate,
js: otto.New(),
} }
// Make the Doodle instance available to the shell.
bindings := map[string]interface{}{
"d": d,
"log": log,
"RGBA": render.RGBA,
"Point": render.NewPoint,
}
for name, v := range bindings {
err := s.js.Set(name, v)
if err != nil {
log.Error("Failed to make `%s` available to JS shell: %s", name, err)
}
}
return s
} }
// Close the shell, resetting its internal state. // Close the shell, resetting its internal state.
@ -54,11 +82,18 @@ func (s *Shell) Close() {
s.Open = false s.Open = false
s.Prompt = ">" s.Prompt = ">"
s.Text = "" s.Text = ""
s.historyPaging = false
s.historyIndex = 0
} }
// Execute a command in the shell. // Execute a command in the shell.
func (s *Shell) Execute(input string) { func (s *Shell) Execute(input string) {
command := s.Parse(input) command := s.Parse(input)
if command.Raw != "" {
s.Output = append(s.Output, s.Prompt+command.Raw)
s.History = append(s.History, command.Raw)
}
if command.Command == "clear" { if command.Command == "clear" {
s.Output = []string{} s.Output = []string{}
} else { } else {
@ -68,10 +103,6 @@ func (s *Shell) Execute(input string) {
} }
} }
if command.Raw != "" {
s.History = append(s.History, command.Raw)
}
// Reset the text buffer in the shell. // Reset the text buffer in the shell.
s.Text = "" s.Text = ""
} }
@ -149,6 +180,32 @@ func (s *Shell) Draw(d *Doodle, ev *events.State) error {
s.Execute(s.Text) s.Execute(s.Text)
s.Close() s.Close()
return nil return nil
} else if (ev.Up.Now || ev.Down.Now) && len(s.History) > 0 {
// Paging through history.
if !s.historyPaging {
s.historyPaging = true
s.historyIndex = len(s.History)
}
// Consume the inputs and make convenient variables.
ev.Down.Read()
isUp := ev.Up.Read()
// Scroll through the input history.
if isUp {
s.historyIndex--
if s.historyIndex < 0 {
s.historyIndex = 0
}
} else {
s.historyIndex++
if s.historyIndex >= len(s.History) {
s.historyIndex = len(s.History) - 1
}
}
s.Text = s.History[s.historyIndex]
} }
// Compute the line height we can draw. // Compute the line height we can draw.
@ -159,10 +216,10 @@ func (s *Shell) Draw(d *Doodle, ev *events.State) error {
// Cursor flip? // Cursor flip?
if d.ticks > s.cursorFlip { if d.ticks > s.cursorFlip {
s.cursorFlip = d.ticks + s.cursorRate s.cursorFlip = d.ticks + s.cursorRate
if s.Cursor == "" { if s.cursor == ' ' {
s.Cursor = "_" s.cursor = '_'
} else { } else {
s.Cursor = "" s.cursor = ' '
} }
} }
@ -215,7 +272,7 @@ func (s *Shell) Draw(d *Doodle, ev *events.State) error {
// Draw the command prompt. // Draw the command prompt.
d.Engine.DrawText( d.Engine.DrawText(
render.Text{ render.Text{
Text: s.Prompt + s.Text + s.Cursor, Text: s.Prompt + s.Text + string(s.cursor),
Size: balance.ShellFontSize, Size: balance.ShellFontSize,
Color: balance.ShellForegroundColor, Color: balance.ShellForegroundColor,
}, },

View File

@ -90,7 +90,7 @@ func (w *Button) Present(e render.Engine) {
e.DrawBox(w.Background, box) e.DrawBox(w.Background, box)
// Draw the text label inside. // Draw the text label inside.
w.Label.SetPoint(render.Point{ w.Label.MoveTo(render.Point{
X: P.X + w.Padding + w.Border + w.Outline, X: P.X + w.Padding + w.Border + w.Outline,
Y: P.Y + w.Padding + w.Border + w.Outline, Y: P.Y + w.Padding + w.Border + w.Outline,
}) })

View File

@ -9,7 +9,8 @@ type Widget interface {
SetWidth(int32) // Set SetWidth(int32) // Set
SetHeight(int32) // Set SetHeight(int32) // Set
Point() render.Point Point() render.Point
SetPoint(render.Point) MoveTo(render.Point)
MoveBy(render.Point)
Size() render.Rect // Return the Width and Height of the widget. Size() render.Rect // Return the Width and Height of the widget.
Resize(render.Rect) Resize(render.Rect)
@ -35,11 +36,17 @@ func (w *BaseWidget) Point() render.Point {
return w.point return w.point
} }
// SetPoint updates the X,Y position of the widget relative to the window. // MoveTo updates the X,Y position to the new point.
func (w *BaseWidget) SetPoint(v render.Point) { func (w *BaseWidget) MoveTo(v render.Point) {
w.point = v w.point = v
} }
// MoveBy adds the X,Y values to the widget's current position.
func (w *BaseWidget) MoveBy(v render.Point) {
w.point.X += v.X
w.point.Y += v.Y
}
// Size returns the box with W and H attributes containing the size of the // Size returns the box with W and H attributes containing the size of the
// widget. The X,Y attributes of the box are ignored and zero. // widget. The X,Y attributes of the box are ignored and zero.
func (w *BaseWidget) Size() render.Rect { func (w *BaseWidget) Size() render.Rect {