diff --git a/README.md b/README.md index c021fac..a7f1ea8 100644 --- a/README.md +++ b/README.md @@ -142,7 +142,9 @@ A brief introduction to the built-in doodads available so far: # Developer Console -Press `Enter` at any time to open the developer console. +Press `Enter` at any time to open the developer console. The console +provides commands and advanced functionality, and is also where cheat +codes can be entered. Commands supported: @@ -154,8 +156,8 @@ new Show the "New Level" screen to start editing a new map. save [filename] - Save the current map in Edit Mode. The filename is required if the map has - not been saved yet. + Save the current map in Edit Mode. The filename is required + if the map has not been saved yet. edit [filename] Open a map or doodad in Edit Mode. @@ -166,14 +168,39 @@ play [filename] echo Flash a message to the console. +alert + Test an alert box modal with a custom message. + clear Clear the console output history. exit quit Close the developer console. + +boolProp + Toggle certain boolean settings in the game. Most of these + are debugging related. `boolProp list` shows the available + props. + +eval +$ + Execute a line of JavaScript code in the console. Several + of the game's core data types are available here; `d` is + the master game struct; d.Scene is the pointer to the + current scene. d.Scene.UI.Canvas may point to the level edit + canvas in Editor Mode. Object.keys() can enumerate public + functions and variables. + +repl + Enters an interactive JavaScript shell, where the console + stays open and pre-fills a $ prompt for subsequent commands. ``` +The JavaScript console is a feature for advanced users and was +used while developing the game. Cool things you can do with it +may be documented elsewhere. + ## Cheat Codes The following cheats can be entered into the developer console. @@ -205,6 +232,64 @@ Experimental: The player character must always remain on screen though so you can't scroll too far away. +Unsupported shell commands (here be dragons): + +* `reload`: reloads the current 'scene' within the game engine, using the + existing scene's data. If playing a level this will start the level over. + If editing a level this will reload the editor, but your recent unsaved + changes _should_ be left intact. +* `guitest`: loads the GUI Test scene within the game. This was where I + was testing UI widgets early on; not well maintained; the `close` + command can get you out of it. + +## Environment Variables + +To enable certain debug features or customize some aspects of the game, +run it with environment variables like the following: + +```bash +# Draw a semi-transparent yellow background over all level chunks +$ DEBUG_CHUNK_COLOR=FFFF0066 ./doodle + +# Set a window size for the application +# (equivalent to: doodle --window 1024x768) +$ DOODLE_W=1024 DOODLE_H=768 ./doodle + +# Turn on lots of fun debug features. +$ DEBUG_CANVAS_LABEL=1 DEBUG_CHUNK_COLOR=FFFF00AA \ + DEBUG_CANVAS_BORDER=FF0 ./doodle +``` + +Supported variables include: + +* `DOODLE_W` and `DOODLE_H` set the width and height of the application + window. Equivalent to the `--window` command-line option. +* `D_SCROLL_SPEED` (int): tune the canvas scrolling speed. Default might + be around 8 or so. +* `D_DOODAD_SIZE` (int): default size for newly created doodads +* `D_SHELL_BG` (color): set the background color of the developer console +* `D_SHELL_FG` (color): text color for the developer console +* `D_SHELL_PC` (color): color for the shell prompt text +* `D_SHELL_LN` (int): set the number of lines of output history the + console will show. This dictates how 'tall' it rises from the bottom + of the screen. Large values will cover the entire screen with console + whenever the shell is open. +* `D_SHELL_FS` (int): set the font size for the developer shell. Default + is about 16. This also affects the size of "flashed" text that appears + at the bottom of the screen. +* `DEBUG_CHUNK_COLOR` (color): set a background color over each chunk + of drawing (level or doodad). A solid color will completely block out + the wallpaper; semitransparent is best. +* `DEBUG_CANVAS_BORDER` (color): the game will draw an insert colored + border around every "Canvas" widget (drawing) on the screen. The level + itself is a Canvas and every individual Doodad or actor in the level is + its own Canvas. +* `DEBUG_CANVAS_LABEL` (bool): draws a text label over every Canvas + widget on the screen, showing its name or Actor ID and some properties, + such as Level Position (LP) and World Position (WP) of actors within + a level. LP is their placement in the level file and WP is their + actual position now (in case it moves). + # Author Copyright (C) 2020 Noah Petherbridge. All rights reserved. diff --git a/cmd/doodle/main.go b/cmd/doodle/main.go index cee197f..c2b8f9d 100644 --- a/cmd/doodle/main.go +++ b/cmd/doodle/main.go @@ -77,6 +77,10 @@ func main() { Name: "guitest", Usage: "enter the GUI Test scene on startup", }, + &cli.BoolFlag{ + Name: "experimental", + Usage: "enable experimental Feature Flags", + }, } app.Action = func(c *cli.Context) error { @@ -92,6 +96,11 @@ func main() { } } + // Enable feature flags? + if c.Bool("experimental") { + balance.FeaturesOn() + } + // SDL engine. engine := sdl.New( fmt.Sprintf("%s v%s", branding.AppName, branding.Version), diff --git a/pkg/balance/feature_flags.go b/pkg/balance/feature_flags.go new file mode 100644 index 0000000..d57af0f --- /dev/null +++ b/pkg/balance/feature_flags.go @@ -0,0 +1,15 @@ +package balance + +// Feature Flags to turn on/off experimental content. +var Feature = feature{ + Zoom: false, +} + +// FeaturesOn turns on all feature flags, from CLI --experimental option. +func FeaturesOn() { + Feature.Zoom = true +} + +type feature struct { + Zoom bool +} diff --git a/pkg/doodle.go b/pkg/doodle.go index 431f082..17d7ae1 100644 --- a/pkg/doodle.go +++ b/pkg/doodle.go @@ -137,7 +137,11 @@ func (d *Doodle) Run() error { } else { // Global event handlers. if keybind.Shutdown(ev) { - d.ConfirmExit() + if d.Debug { // fast exit in -debug mode. + d.running = false + } else { + d.ConfirmExit() + } continue } diff --git a/pkg/editor_scene.go b/pkg/editor_scene.go index 43de340..52f5eab 100644 --- a/pkg/editor_scene.go +++ b/pkg/editor_scene.go @@ -202,6 +202,23 @@ func (s *EditorScene) Loop(d *Doodle, ev *event.State) error { s.UI.Canvas.RedoStroke() } + // Zoom in/out. + if balance.Feature.Zoom { + if keybind.ZoomIn(ev) { + d.Flash("Zoom in") + s.UI.Canvas.Zoom++ + } else if keybind.ZoomOut(ev) { + d.Flash("Zoom out") + s.UI.Canvas.Zoom-- + } else if keybind.ZoomReset(ev) { + d.Flash("Reset zoom") + s.UI.Canvas.Zoom = 0 + } else if keybind.Origin(ev) { + d.Flash("Scrolled back to level origin (0,0)") + s.UI.Canvas.ScrollTo(render.Origin) + } + } + s.UI.Loop(ev) // Switching to Play Mode? diff --git a/pkg/editor_ui.go b/pkg/editor_ui.go index 832d392..d02cc3d 100644 --- a/pkg/editor_ui.go +++ b/pkg/editor_ui.go @@ -547,26 +547,43 @@ func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.MenuBar { editMenu.AddItemAccel("Redo", "Ctrl-Y", func() { u.Canvas.RedoStroke() }) - editMenu.AddSeparator() - editMenu.AddItem("Level options", func() { - log.Info("Opening the window") - - // Open the New Level window in edit-settings mode. - u.levelSettingsWindow.Hide() - u.levelSettingsWindow = nil - u.SetupPopups(u.d) - u.levelSettingsWindow.Show() - }) //////// // Level menu if u.Scene.DrawingType == enum.LevelDrawing { levelMenu := menu.AddMenu("Level") + levelMenu.AddItem("Page settings", func() { + log.Info("Opening the window") + + // Open the New Level window in edit-settings mode. + u.levelSettingsWindow.Hide() + u.levelSettingsWindow = nil + u.SetupPopups(u.d) + u.levelSettingsWindow.Show() + }) levelMenu.AddItemAccel("Playtest", "P", func() { u.Scene.Playtest() }) } + //////// + // View menu + if balance.Feature.Zoom { + viewMenu := menu.AddMenu("View") + viewMenu.AddItemAccel("Zoom in", "+", func() { + u.Canvas.Zoom++ + }) + viewMenu.AddItemAccel("Zoom out", "-", func() { + u.Canvas.Zoom-- + }) + viewMenu.AddItemAccel("Reset zoom", "1", func() { + u.Canvas.Zoom = 0 + }) + viewMenu.AddItemAccel("Scroll drawing to origin", "0", func() { + u.Canvas.ScrollTo(render.Origin) + }) + } + //////// // Tools menu toolMenu := menu.AddMenu("Tools") diff --git a/pkg/keybind/keybind.go b/pkg/keybind/keybind.go index 8b277ba..2feda7c 100644 --- a/pkg/keybind/keybind.go +++ b/pkg/keybind/keybind.go @@ -45,6 +45,26 @@ func Redo(ev *event.State) bool { return ev.Ctrl && ev.KeyDown("y") } +// ZoomIn (+) +func ZoomIn(ev *event.State) bool { + return ev.KeyDown("=") || ev.KeyDown("+") +} + +// ZoomOut (-) +func ZoomOut(ev *event.State) bool { + return ev.KeyDown("-") +} + +// ZoomReset (1) +func ZoomReset(ev *event.State) bool { + return ev.KeyDown("1") +} + +// Origin (0) -- scrolls the canvas back to 0,0 in Editor Mode. +func Origin(ev *event.State) bool { + return ev.KeyDown("0") +} + // GotoPlay (P) play tests the current level in the editor. func GotoPlay(ev *event.State) bool { return ev.KeyDown("p") diff --git a/pkg/uix/canvas.go b/pkg/uix/canvas.go index a4cde0b..dbdc214 100644 --- a/pkg/uix/canvas.go +++ b/pkg/uix/canvas.go @@ -29,6 +29,7 @@ type Canvas struct { // NewCanvas() with editable=true, they are both enabled. Editable bool // Clicking will edit pixels of this canvas. Scrollable bool // Cursor keys will scroll the viewport of this canvas. + Zoom int // Zoom level on the canvas. // Selected draw tool/mode, default Pencil, for editable canvases. Tool drawtool.Tool @@ -289,10 +290,17 @@ func (w *Canvas) ViewportRelative() render.Rect { // the mouse cursor. func (w *Canvas) WorldIndexAt(screenPixel render.Point) render.Point { var P = ui.AbsolutePosition(w) - return render.Point{ + world := render.Point{ X: screenPixel.X - P.X - w.Scroll.X, Y: screenPixel.Y - P.Y - w.Scroll.Y, } + + // Handle Zoomies + if w.Zoom != 0 { + world.X = w.ZoomMultiply(world.X) + world.Y = w.ZoomMultiply(world.Y) + } + return world } // Chunker returns the underlying Chunker object. diff --git a/pkg/uix/canvas_editable.go b/pkg/uix/canvas_editable.go index e9245a7..3527dda 100644 --- a/pkg/uix/canvas_editable.go +++ b/pkg/uix/canvas_editable.go @@ -28,6 +28,10 @@ func (w *Canvas) commitStroke(tool drawtool.Tool, addHistory bool) { return } + // Zoom the stroke coordinates (this modifies the pointer) + zStroke := w.ZoomStroke(w.currentStroke) + _ = zStroke + // Mark the canvas as modified. w.modified = true diff --git a/pkg/uix/canvas_present.go b/pkg/uix/canvas_present.go index 5b5fc37..aadef58 100644 --- a/pkg/uix/canvas_present.go +++ b/pkg/uix/canvas_present.go @@ -42,9 +42,16 @@ func (w *Canvas) Present(e render.Engine, p render.Point) { } else { tex = chunk.Texture(e) } + + // Zoom in the texture. + texSize := tex.Size() + if w.Zoom != 0 { + texSize.W = w.ZoomMultiply(texSize.W) + texSize.H = w.ZoomMultiply(texSize.H) + } src := render.Rect{ - W: tex.Size().W, - H: tex.Size().H, + W: texSize.W, + H: texSize.H, } // If the source bitmap is already bigger than the Canvas widget @@ -69,6 +76,16 @@ func (w *Canvas) Present(e render.Engine, p render.Point) { H: src.H, } + // Zoom the destination rect. + if w.Zoom != 0 { + // dst.X += int(w.GetZoomMultiplier()) + // dst.Y += int(w.GetZoomMultiplier()) + // dst.X = w.ZoomMultiply(dst.X) + // dst.Y = w.ZoomMultiply(dst.Y) + // dst.W = w.ZoomMultiply(dst.W) + // dst.H = w.ZoomMultiply(dst.H) + } + // TODO: all this shit is in TrimBox(), make it DRY // If the destination width will cause it to overflow the widget diff --git a/pkg/uix/canvas_wallpaper.go b/pkg/uix/canvas_wallpaper.go index 29c84d8..0e2f092 100644 --- a/pkg/uix/canvas_wallpaper.go +++ b/pkg/uix/canvas_wallpaper.go @@ -2,6 +2,7 @@ package uix import ( "git.kirsle.net/apps/doodle/pkg/level" + "git.kirsle.net/apps/doodle/pkg/log" "git.kirsle.net/apps/doodle/pkg/wallpaper" "git.kirsle.net/go/render" ) @@ -70,45 +71,96 @@ func (w *Canvas) loopContainActorsInsideLevel(a *Actor) { } // PresentWallpaper draws the wallpaper. +// Point p is the one given to Canvas.Present(), i.e., the position of the +// top-left corner of the Canvas widget relative to the application window. func (w *Canvas) PresentWallpaper(e render.Engine, p render.Point) error { var ( - wp = w.wallpaper - S = w.Size() - size = wp.corner.Size() + wp = w.wallpaper + S = w.Size() + size = wp.corner.Size() + + // Get the relative viewport of world coordinates looked at by the canvas. + // The X,Y values are the negative Scroll value + // The W,H values are the Canvas size same as var S above. Viewport = w.ViewportRelative() - origin = render.Point{ - X: p.X + w.Scroll.X + w.BoxThickness(1), - Y: p.Y + w.Scroll.Y + w.BoxThickness(1), - } - limit = render.Point{ - // NOTE: we add + the texture size so we would actually draw one - // full extra texture out-of-bounds for the repeating backgrounds. - // This is cuz for scrolling we offset the draw spot on a loop. - X: origin.X + S.W - w.BoxThickness(1) + size.W, - Y: origin.Y + S.H - w.BoxThickness(1) + size.H, + + // origin and limit seem to be the boundaries of where on screen + // we are rendering inside. + origin = render.Point{ + X: p.X + w.Scroll.X, // + w.BoxThickness(1), + Y: p.Y + w.Scroll.Y, // + w.BoxThickness(1), } + limit render.Point // TBD later ) + // Grow or shrink the render limit if we're zoomed. + if w.Zoom != 0 { + // I was surprised to discover that just zooming the texture + // quadrant size handled most of the problem! For reference, the + // Blueprint wallpaper has a size of 120x120 for the tiling pattern. + size.H = w.ZoomMultiply(size.H) + size.W = w.ZoomMultiply(size.W) + } + + // SCRATCH + // at bootup, scroll position 0,0: + // origin=44,20 p=44,20 p=relative to application window + // scroll right and down to -60,-60: + // origin=-16,-40 p=44,20 and looks good in that direction + // scroll left and up to 60,60: + // origin=104,80 p=44,20 + // becomes origin=44,20 p=44,20 d=-16,-40 + // the latter case is handled below. walking thru: + // if o(104) > p(44): + // while o(104) > p(44): + // o -= size(120) of texture block + // o is now -16,-40 + // while o(-16) > p(44): it's not; break + // dx = o(-16) + // origin.X = p.X + // (becomes origin=44,20 p=44,20 d=-16,-40) + // + // The visual bug is: if you scroll left or up on an Unbounded level from + // the origin (0, 0), the tiling of the wallpaper jumps to the right and + // down by an offset of 44x20 pixels. + // + // what is meant to happen: + // - + // For tiled textures, compute the offset amount. If we are scrolled away // from the Origin (0,0) we find out by how far (subtract full tile sizes) // and use the remainder as an offset for drawing the tiles. + // p = position on screen of the Canvas widget + // origin = p.X + Scroll.X, p.Y + scroll.Y + // note: negative Scroll values means to the right and down var dx, dy int if origin.X > p.X { - for origin.X > p.X && origin.X > size.W { - origin.X -= size.W - } + // View is scrolled leftward (into negative world coordinates) dx = origin.X - origin.X = p.X + for dx > p.X { + dx -= size.W + } + origin.X = 0 // note: origin 0,0 will be the corner of the app window } if origin.Y > p.Y { - for origin.Y > p.Y && origin.Y > size.H { - origin.Y -= size.H - } + // View is scrolled upward (into negative world coordinates) dy = origin.Y - origin.Y = p.Y + for dy > p.Y { + dy -= size.H + } + origin.Y = 0 } - // And capping the scroll delta in the other direction. + limit = render.Point{ + // NOTE: we add + the texture size so we would actually draw one + // full extra texture out-of-bounds for the repeating backgrounds. + // This is cuz for scrolling we offset the draw spot on a loop. + X: origin.X + S.W + size.W, + Y: origin.Y + S.H + size.H, + } + + // And capping the scroll delta in the other direction. Always draw + // pixels until the Canvas size is covered. if limit.X < S.W { limit.X = S.W } @@ -117,10 +169,12 @@ func (w *Canvas) PresentWallpaper(e render.Engine, p render.Point) error { limit.Y = S.H } + // TODO: was still getting some slight flicker on the right and bottom + // when scrolling.. add a bit extra margin. limit.X += size.W limit.Y += size.H - // Tile the repeat texture. + // Tile the repeat texture. Start from 1 full wallpaper tile out of bounds for x := origin.X - size.W; x < limit.X; x += size.W { for y := origin.Y - size.H; y < limit.Y; y += size.H { src := render.Rect{ @@ -134,8 +188,20 @@ func (w *Canvas) PresentWallpaper(e render.Engine, p render.Point) error { H: src.H, } + // Zoom the output texture. + if w.Zoom != 0 { + // dst.X = w.ZoomMultiply(dst.X - p.X) + // dst.Y = w.ZoomMultiply(dst.Y - p.Y) + // dst.W = w.ZoomMultiply(dst.W) + // dst.H = w.ZoomMultiply(dst.H) + } + // Trim the edges of the destination box, like in canvas.go#Present + odst := dst render.TrimBox(&src, &dst, p, S, w.BoxThickness(1)) + if dst.W == 0 { + log.Error("TrimBoxed! %s => %s", odst, dst) + } e.Copy(wp.repeat, src, dst) } @@ -154,6 +220,15 @@ func (w *Canvas) PresentWallpaper(e render.Engine, p render.Point) error { W: src.W, H: src.H, } + + // Zoom the output texture. + if w.Zoom != 0 { + // dst.X = w.ZoomMultiply(dst.X - origin.X) + // dst.Y = w.ZoomMultiply(dst.Y - origin.Y) + // dst.W = w.ZoomMultiply(dst.W) + // dst.H = w.ZoomMultiply(dst.H) + } + render.TrimBox(&src, &dst, p, S, w.BoxThickness(1)) e.Copy(wp.left, src, dst) } @@ -170,6 +245,15 @@ func (w *Canvas) PresentWallpaper(e render.Engine, p render.Point) error { W: src.W, H: src.H, } + + // Zoom the output texture. + if w.Zoom != 0 { + // dst.X = w.ZoomMultiply(dst.X - origin.X) + // dst.Y = w.ZoomMultiply(dst.Y - origin.Y) + // dst.W = w.ZoomMultiply(dst.W) + // dst.H = w.ZoomMultiply(dst.H) + } + render.TrimBox(&src, &dst, p, S, w.BoxThickness(1)) e.Copy(wp.top, src, dst) } @@ -186,6 +270,15 @@ func (w *Canvas) PresentWallpaper(e render.Engine, p render.Point) error { W: src.W, H: src.H, } + + // Zoom the output texture. + if w.Zoom != 0 { + // dst.X = w.ZoomMultiply(dst.X - origin.X) + // dst.Y = w.ZoomMultiply(dst.Y - origin.Y) + // dst.W = w.ZoomMultiply(dst.W) + // dst.H = w.ZoomMultiply(dst.H) + } + render.TrimBox(&src, &dst, p, S, w.BoxThickness(1)) e.Copy(wp.corner, src, dst) } diff --git a/pkg/uix/canvas_zoom.go b/pkg/uix/canvas_zoom.go new file mode 100644 index 0000000..ab2d731 --- /dev/null +++ b/pkg/uix/canvas_zoom.go @@ -0,0 +1,97 @@ +package uix + +import ( + "git.kirsle.net/apps/doodle/pkg/drawtool" + "git.kirsle.net/go/render" +) + +// Functions related to the Zoom Tool to magnify the size of the canvas. + +/* +GetZoomMultiplier parses the .Zoom integer and returns a multiplier. + +Examples: + + Zoom = 0: neutral (100% scale, 1x) + Zoom = 1: 2x zoom + Zoom = 2: 4x zoom + Zoom = 3: 8x zoom + Zoom = -1: 0.5x zoom + Zoom = -2: 0.25x zoom +*/ +func (w *Canvas) GetZoomMultiplier() float64 { + // Get and bounds cap the zoom setting. + if w.Zoom < -2 { + w.Zoom = -2 + } else if w.Zoom > 3 { + w.Zoom = 3 + } + + // Return the multipliers. + switch w.Zoom { + case -2: + return 0.25 + case -1: + return 0.5 + case 0: + return 1 + case 1: + return 1.5 + case 2: + return 2 + case 3: + return 2.5 + default: + return 1 + } +} + +/* +ZoomMultiply multiplies a width or height value by the Zoom Multiplier and +returns the modified integer. + +Usage is like: + + // when building a render.Rect destination box. + dest.W *= ZoomMultiply(dest.W) + dest.H *= ZoomMultiply(dest.H) +*/ +func (w *Canvas) ZoomMultiply(value int) int { + return int(float64(value) * w.GetZoomMultiplier()) +} + +/* +ZoomStroke adjusts a drawn stroke on the canvas to account for the zoom level. + +Returns a copy Stroke value without changing the original. +*/ +func (w *Canvas) ZoomStroke(stroke *drawtool.Stroke) drawtool.Stroke { + copy := drawtool.Stroke{ + ID: stroke.ID, + Shape: stroke.Shape, + Color: stroke.Color, + Thickness: stroke.Thickness, + ExtraData: stroke.ExtraData, + PointA: stroke.PointA, + PointB: stroke.PointB, + Points: stroke.Points, + OriginalPoints: stroke.OriginalPoints, + } + return copy + + // Multiply all coordinates in this stroke, which should be World + // Coordinates in the level data, by the zoom multiplier. + adjust := func(p render.Point) render.Point { + p.X = w.ZoomMultiply(p.X) + p.Y = w.ZoomMultiply(p.Y) + return p + } + + copy.PointA = adjust(copy.PointA) + copy.PointB = adjust(copy.PointB) + for i := range copy.Points { + copy.Points[i] = adjust(copy.Points[i]) + } + + return copy +}