diff --git a/Debug Notes.md b/Debug Notes.md new file mode 100644 index 0000000..42b8d72 --- /dev/null +++ b/Debug Notes.md @@ -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` diff --git a/README.md b/README.md index 4551455..df1a3b4 100644 --- a/README.md +++ b/README.md @@ -139,17 +139,54 @@ As a rough idea of the milestones needed for this game to work: for the rest of this program. * [x] Labels * [ ] Buttons (text only is OK) - * [x] Buttons wrap their Label and dynamically compute their size based - on how wide the label will render, plus padding and border. - * [x] Border colors and widths and paddings are all configurable. - * [ ] Buttons should interact with the cursor and be hoverable and - clickable. + * [x] Buttons wrap their Label and dynamically compute their size based + on how wide the label will render, plus padding and border. + * [x] Border colors and widths and paddings are all configurable. + * [ ] Buttons should interact with the cursor and be hoverable and + clickable. * [ ] UI Manager that will keep track of buttons to know when the mouse is interacting with them. * [ ] 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) -* [ ] Expand the Palette support in levels for solid vs. transparent, fire, - etc. with UI toolbar to choose palettes. + * [ ] 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, + 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 diff --git a/commands.go b/commands.go index 31e9649..5ba1952 100644 --- a/commands.go +++ b/commands.go @@ -3,6 +3,7 @@ package doodle import ( "errors" "fmt" + "strconv" ) // Command is a parsed shell command. @@ -36,6 +37,13 @@ func (c Command) Run(d *Doodle) error { return c.Quit() case "help": 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: return c.Default() } @@ -91,7 +99,7 @@ func (c Command) Help(d *Doodle) error { // Save the current map to disk. func (c Command) Save(d *Doodle) error { - if scene, ok := d.scene.(*EditorScene); ok { + if scene, ok := d.Scene.(*EditorScene); ok { filename := "" if len(c.Args) > 0 { filename = c.Args[0] @@ -139,6 +147,42 @@ func (c Command) Quit() error { 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 ") + } + + 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. func (c Command) Default() error { return fmt.Errorf("%s: command not found. Try `help` for help", diff --git a/doodle.go b/doodle.go index 404f7e1..d487100 100644 --- a/doodle.go +++ b/doodle.go @@ -32,7 +32,7 @@ type Doodle struct { // Command line shell options. shell Shell - scene Scene + Scene Scene } // New initializes the game object. @@ -62,8 +62,8 @@ func (d *Doodle) Run() error { } // Set up the default scene. - if d.scene == nil { - d.Goto(&EditorScene{}) + if d.Scene == nil { + d.Goto(&MainScene{}) } log.Info("Enter Main Loop") @@ -96,14 +96,14 @@ func (d *Doodle) Run() error { } // Run the scene's logic. - err = d.scene.Loop(d, ev) + err = d.Scene.Loop(d, ev) if err != nil { return err } } // Draw the scene. - d.scene.Draw(d) + d.Scene.Draw(d) // Draw the shell. err = d.shell.Draw(d, ev) @@ -114,7 +114,7 @@ func (d *Doodle) Run() error { } // Draw the debug overlay over all scenes. - // d.DrawDebugOverlay() + d.DrawDebugOverlay() // Render the pixels to the screen. err = d.Engine.Present() diff --git a/editor_scene.go b/editor_scene.go index c1d1fd7..6491415 100644 --- a/editor_scene.go +++ b/editor_scene.go @@ -11,7 +11,6 @@ import ( "git.kirsle.net/apps/doodle/events" "git.kirsle.net/apps/doodle/level" "git.kirsle.net/apps/doodle/render" - "git.kirsle.net/apps/doodle/ui" ) // 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 { 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 } diff --git a/fps.go b/fps.go index d3c9ec3..c8cb70a 100644 --- a/fps.go +++ b/fps.go @@ -10,6 +10,13 @@ import ( // Frames to cache for FPS calculation. const maxSamples = 100 +// Debug mode options, these can be enabled in the dev console +// like: boolProp DebugOverlay true +var ( + DebugOverlay = true + DebugCollision = true +) + var ( fpsCurrentTicks uint32 // current time we get sdl.GetTicks() fpsLastTime uint32 // last time we printed the fpsCurrentTicks @@ -21,7 +28,7 @@ var ( // DrawDebugOverlay draws the debug FPS text on the SDL canvas. func (d *Doodle) DrawDebugOverlay() { - if !d.Debug { + if !d.Debug || !DebugOverlay { return } @@ -29,7 +36,7 @@ func (d *Doodle) DrawDebugOverlay() { "FPS: %d (%dms) S:%s F12=screenshot", fpsCurrent, fpsSkipped, - d.scene.Name(), + d.Scene.Name(), ) err := d.Engine.DrawText( @@ -52,6 +59,10 @@ func (d *Doodle) DrawDebugOverlay() { // DrawCollisionBox draws the collision box around a Doodad. func (d *Doodle) DrawCollisionBox(actor doodads.Doodad) { + if !d.Debug || !DebugCollision { + return + } + var ( rect = doodads.GetBoundingRect(actor) box = doodads.GetCollisionBox(rect) @@ -74,13 +85,6 @@ 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, - // ) - fpsLastTime = fpsCurrentTicks fpsCurrent = fpsFrames fpsFrames = 0 diff --git a/main_scene.go b/main_scene.go new file mode 100644 index 0000000..fe40071 --- /dev/null +++ b/main_scene.go @@ -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 +} diff --git a/play_scene.go b/play_scene.go index 7b825ee..9ba3fe7 100644 --- a/play_scene.go +++ b/play_scene.go @@ -21,7 +21,7 @@ type PlayScene struct { height int32 // Player character - player doodads.Doodad + Player doodads.Doodad } // Name of the scene. @@ -42,7 +42,7 @@ func (s *PlayScene) Setup(d *Doodle) error { s.Filename = "" } - s.player = doodads.NewPlayer() + s.Player = doodads.NewPlayer() if s.canvas == nil { 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) // Draw our hero. - s.player.Draw(d.Engine) + s.Player.Draw(d.Engine) // Draw out bounding boxes. - d.DrawCollisionBox(s.player) + d.DrawCollisionBox(s.Player) return nil } // movePlayer updates the player's X,Y coordinate based on key pressed. func (s *PlayScene) movePlayer(ev *events.State) { - delta := s.player.Position() + delta := s.Player.Position() var playerSpeed int32 = 8 var gravity int32 = 2 @@ -110,20 +110,20 @@ func (s *PlayScene) movePlayer(ev *events.State) { // Apply gravity. // var onFloor bool - info, ok := doodads.CollidesWithGrid(s.player, &s.canvas, delta) + info, ok := doodads.CollidesWithGrid(s.Player, &s.canvas, delta) if ok { // Collision happened with world. } delta = info.MoveTo // Apply gravity if not grounded. - if !s.player.Grounded() { + if !s.Player.Grounded() { // Gravity has to pipe through the collision checker, too, so it // can't give us a cheated downward boost. delta.Y += gravity } - s.player.MoveTo(delta) + s.Player.MoveTo(delta) } // LoadLevel loads a level from disk. diff --git a/scene.go b/scene.go index f7d4e78..1c04c43 100644 --- a/scene.go +++ b/scene.go @@ -21,11 +21,11 @@ type Scene interface { // Goto a scene. First it unloads the current scene. func (d *Doodle) Goto(scene Scene) error { // Teardown existing scene. - if d.scene != nil { - d.scene.Destroy() + if d.Scene != nil { + d.Scene.Destroy() } - log.Info("Goto Scene") - d.scene = scene - return d.scene.Setup(d) + log.Info("Goto Scene: %s", scene.Name()) + d.Scene = scene + return d.Scene.Setup(d) } diff --git a/shell.go b/shell.go index e2abcc9..2827506 100644 --- a/shell.go +++ b/shell.go @@ -8,6 +8,7 @@ import ( "git.kirsle.net/apps/doodle/balance" "git.kirsle.net/apps/doodle/events" "git.kirsle.net/apps/doodle/render" + "github.com/robertkrimen/otto" ) // Flash a message to the user. @@ -17,16 +18,26 @@ func (d *Doodle) Flash(template string, v ...interface{}) { // 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 + parent *Doodle + + Open bool + Prompt string + Text string + History []string + Output []string + Flashes []Flash + + // Blinky cursor variables. + cursor byte // cursor symbol cursorFlip uint64 // ticks until cursor flip cursorRate uint64 + + // Paging through history variables. + historyPaging bool + historyIndex int + + // JavaScript shell interpreter. + js *otto.Otto } // Flash holds a message to flash on screen. @@ -37,15 +48,32 @@ type Flash struct { // NewShell initializes the shell helper (the "Shellper"). func NewShell(d *Doodle) Shell { - return Shell{ + s := Shell{ parent: d, History: []string{}, Output: []string{}, Flashes: []Flash{}, Prompt: ">", - Cursor: "_", + cursor: '_', 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. @@ -54,11 +82,18 @@ func (s *Shell) Close() { s.Open = false s.Prompt = ">" s.Text = "" + s.historyPaging = false + s.historyIndex = 0 } // Execute a command in the shell. func (s *Shell) Execute(input string) { 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" { s.Output = []string{} } 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. s.Text = "" } @@ -149,6 +180,32 @@ func (s *Shell) Draw(d *Doodle, ev *events.State) error { s.Execute(s.Text) s.Close() 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. @@ -159,10 +216,10 @@ func (s *Shell) Draw(d *Doodle, ev *events.State) error { // Cursor flip? if d.ticks > s.cursorFlip { s.cursorFlip = d.ticks + s.cursorRate - if s.Cursor == "" { - s.Cursor = "_" + if s.cursor == ' ' { + s.cursor = '_' } else { - s.Cursor = "" + s.cursor = ' ' } } @@ -215,7 +272,7 @@ func (s *Shell) Draw(d *Doodle, ev *events.State) error { // Draw the command prompt. d.Engine.DrawText( render.Text{ - Text: s.Prompt + s.Text + s.Cursor, + Text: s.Prompt + s.Text + string(s.cursor), Size: balance.ShellFontSize, Color: balance.ShellForegroundColor, }, diff --git a/ui/button.go b/ui/button.go index 801e6d8..abf5313 100644 --- a/ui/button.go +++ b/ui/button.go @@ -90,7 +90,7 @@ func (w *Button) Present(e render.Engine) { e.DrawBox(w.Background, box) // Draw the text label inside. - w.Label.SetPoint(render.Point{ + w.Label.MoveTo(render.Point{ X: P.X + w.Padding + w.Border + w.Outline, Y: P.Y + w.Padding + w.Border + w.Outline, }) diff --git a/ui/widget.go b/ui/widget.go index 2e6b2a9..d4cbaaa 100644 --- a/ui/widget.go +++ b/ui/widget.go @@ -9,7 +9,8 @@ type Widget interface { SetWidth(int32) // Set SetHeight(int32) // Set Point() render.Point - SetPoint(render.Point) + MoveTo(render.Point) + MoveBy(render.Point) Size() render.Rect // Return the Width and Height of the widget. Resize(render.Rect) @@ -35,11 +36,17 @@ func (w *BaseWidget) Point() render.Point { return w.point } -// SetPoint updates the X,Y position of the widget relative to the window. -func (w *BaseWidget) SetPoint(v render.Point) { +// MoveTo updates the X,Y position to the new point. +func (w *BaseWidget) MoveTo(v render.Point) { 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 // widget. The X,Y attributes of the box are ignored and zero. func (w *BaseWidget) Size() render.Rect {