diff --git a/Changes.md b/Changes.md index 096c401..cfc1bdd 100644 --- a/Changes.md +++ b/Changes.md @@ -1,5 +1,48 @@ # Changes +## v0.7.0 (TBD) + +This is the first release of the game where the "free version" drifts meaningfully +away from the "full version". Free versions of the game will show the label +"(shareware)" next to the game version numbers and will not support embedding +doodads inside of level files -- for creating them or playing them. Check the +website for how you can register the full version of the game. + +This release brings several improvements to the game: + +* **Brush Patterns** for your level palette. Instead of your colors drawing on as + plain, solid pixels, a color swatch can _sample_ with a Pattern to create a + textured appearance when plotted on your level. Several patterns are built in + including Noise, Marker, Ink, and others. The idea is that your brush strokes can + look as though they were drawn in pencil graphite or similar. +* **Title Screen:** the demo level shown on the title screen will leisurely scroll + around the page. The arrow keys may still manually scroll the level any direction. +* **Attach Doodads to Level Files:** this is the first release that supports _truly_ + portable custom levels! By attaching your custom doodads _with_ your custom level + file, it will "just play" on someone else's computer, and they don't need to copy + all your custom doodads for it to work! But, free versions of the game will not + get to enjoy this feature. + +Some small bits of polish in the game's user interface: + +* Some buttons are more colorful! The "Ok" button in alert boxes is blue and pressing + Enter will select the "Ok" button. +* When opening a Level or Doodad to play or edit, a blue **Browse...** button is + added so you can more easily find downloaded custom levels and play them. +* In the Level Editor, the "Level -> **Attached Files**" menu will let you see + and manage files attached to your level, such as its custom wallpaper image or + any custom doodads that were published with the level. +* The keyboard shortcut to open the developer console is now the tilde/grave key + instead of Enter. + +This release also makes the game a little bit more functional on smartphone-sized +devices like the Pine64 Pinephone: + +* **Horizontal Toolbars** option for the Level Editor. If the game is started with + the `-w mobile` command line option, the game window takes on a mobile form factor + and the Horizontal Toolbars are enabled by default. +* Alternatively, press the tilde/grave key and type: `boolProp horizontalToolbars true` + ## v0.6.0-alpha (June 6 2021) This release brings less jank and some new features. diff --git a/pkg/editor_scene.go b/pkg/editor_scene.go index de691ca..ecd4340 100644 --- a/pkg/editor_scene.go +++ b/pkg/editor_scene.go @@ -196,6 +196,21 @@ func (s *EditorScene) Loop(d *Doodle, ev *event.State) error { } } + // Menu key bindings. + if keybind.NewLevel(ev) { + // Ctrl-N, New Level + s.MenuNewLevel() + } else if keybind.SaveAs(ev) { + // Shift-Ctrl-S, Save As + s.MenuSave(true)() + } else if keybind.Save(ev) { + // Ctrl-S, Save + s.MenuSave(false)() + } else if keybind.Open(ev) { + // Ctrl-O, Open + s.MenuOpen() + } + // Undo/Redo key bindings. if keybind.Undo(ev) { s.UI.Canvas.UndoStroke() @@ -223,7 +238,7 @@ func (s *EditorScene) Loop(d *Doodle, ev *event.State) error { s.UI.Canvas.ScrollTo(render.Origin) } - s.UI.Loop(ev) + // s.UI.Loop(ev) // Switching to Play Mode? if keybind.GotoPlay(ev) { @@ -252,6 +267,8 @@ func (s *EditorScene) Loop(d *Doodle, ev *event.State) error { s.UI.doodadWindow.Show() } + s.UI.Loop(ev) + return nil } diff --git a/pkg/editor_ui.go b/pkg/editor_ui.go index cf028be..9f45ac6 100644 --- a/pkg/editor_ui.go +++ b/pkg/editor_ui.go @@ -3,7 +3,6 @@ package doodle import ( "fmt" "path/filepath" - "strconv" "git.kirsle.net/apps/doodle/pkg/balance" "git.kirsle.net/apps/doodle/pkg/branding" @@ -12,9 +11,7 @@ import ( "git.kirsle.net/apps/doodle/pkg/level" "git.kirsle.net/apps/doodle/pkg/license" "git.kirsle.net/apps/doodle/pkg/log" - "git.kirsle.net/apps/doodle/pkg/native" "git.kirsle.net/apps/doodle/pkg/uix" - "git.kirsle.net/apps/doodle/pkg/windows" "git.kirsle.net/go/render" "git.kirsle.net/go/render/event" "git.kirsle.net/go/ui" @@ -469,252 +466,6 @@ func (u *EditorUI) ExpandCanvas(e render.Engine) { u.Workspace.Compute(e) } -// SetupMenuBar sets up the menu bar. -func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.MenuBar { - menu := ui.NewMenuBar("Main Menu") - - // Save and Save As common menu handler - var ( - drawingType string - saveFunc func(filename string) - ) - - switch u.Scene.DrawingType { - case enum.LevelDrawing: - drawingType = "level" - saveFunc = func(filename string) { - if err := u.Scene.SaveLevel(filename); err != nil { - d.Flash("Error: %s", err) - } else { - d.Flash("Saved level: %s", filename) - } - } - case enum.DoodadDrawing: - drawingType = "doodad" - saveFunc = func(filename string) { - if err := u.Scene.SaveDoodad(filename); err != nil { - d.Flash("Error: %s", err) - } else { - d.Flash("Saved doodad: %s", filename) - } - } - default: - d.Flash("Error: Scene.DrawingType is not a valid type") - } - - //////// - // File menu - fileMenu := menu.AddMenu("File") - fileMenu.AddItemAccel("New level", "Ctrl-N*", func() { - u.Scene.ConfirmUnload(func() { - d.GotoNewMenu() - }) - }) - fileMenu.AddItem("New doodad", func() { - u.Scene.ConfirmUnload(func() { - d.Prompt("Doodad size [100]>", func(answer string) { - size := balance.DoodadSize - if answer != "" { - i, err := strconv.Atoi(answer) - if err != nil { - d.Flash("Error: Doodad size must be a number.") - return - } - size = i - } - d.NewDoodad(size) - }) - }) - }) - fileMenu.AddItemAccel("Save", "Ctrl-S*", func() { - if u.Scene.filename != "" { - saveFunc(u.Scene.filename) - } else { - d.Prompt("Save filename>", func(answer string) { - if answer != "" { - saveFunc(answer) - } - }) - } - }) - fileMenu.AddItem("Save as...", func() { - d.Prompt("Save as filename>", func(answer string) { - if answer != "" { - saveFunc(answer) - } - }) - }) - - if balance.Feature.EmbeddableDoodads && drawingType == "level" { - fileMenu.AddItem("Publish level", func() { - u.OpenPublishWindow() - }) - } - - fileMenu.AddItemAccel("Open...", "Ctrl-O*", func() { - u.Scene.ConfirmUnload(func() { - d.GotoLoadMenu() - }) - }) - fileMenu.AddSeparator() - fileMenu.AddItem("Close "+drawingType, func() { - u.Scene.ConfirmUnload(func() { - d.Goto(&MainScene{}) - }) - }) - fileMenu.AddItemAccel("Quit", "Escape", func() { - d.ConfirmExit() - }) - - //////// - // Edit menu - editMenu := menu.AddMenu("Edit") - editMenu.AddItemAccel("Undo", "Ctrl-Z", func() { - u.Canvas.UndoStroke() - }) - editMenu.AddItemAccel("Redo", "Ctrl-Y", func() { - u.Canvas.RedoStroke() - }) - - //////// - // 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.AddItem("Attached files", func() { - log.Info("Opening the FileSystem window") - u.OpenFileSystemWindow() - }) - 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") - toolMenu.AddItemAccel("Debug overlay", "F3", func() { - DebugOverlay = !DebugOverlay - if DebugOverlay { - d.Flash("Debug overlay enabled. Press F3 to turn it off.") - } - }) - toolMenu.AddItemAccel("Command shell", "Enter", func() { - d.shell.Open = true - }) - toolMenu.AddSeparator() - toolMenu.AddItem("Edit Palette", func() { - u.OpenPaletteWindow() - }) - if u.Scene.DrawingType == enum.LevelDrawing { - toolMenu.AddItemAccel("Doodads", "d", func() { - log.Info("Open the DoodadDropper") - u.doodadWindow.Show() - }) - } else if u.Scene.DrawingType == enum.DoodadDrawing { - toolMenu.AddItem("Layers", func() { - u.OpenLayersWindow() - }) - } - - // Draw Tools - toolMenu.AddItemAccel("Pencil Tool", "F", func() { - u.Canvas.Tool = drawtool.PencilTool - u.activeTool = u.Canvas.Tool.String() - d.Flash("Pencil Tool selected.") - }) - toolMenu.AddItemAccel("Line Tool", "L", func() { - u.Canvas.Tool = drawtool.LineTool - u.activeTool = u.Canvas.Tool.String() - d.Flash("Line Tool selected.") - }) - toolMenu.AddItemAccel("Rectangle Tool", "R", func() { - u.Canvas.Tool = drawtool.RectTool - u.activeTool = u.Canvas.Tool.String() - d.Flash("Rectangle Tool selected.") - }) - toolMenu.AddItemAccel("Ellipse Tool", "C", func() { - u.Canvas.Tool = drawtool.EllipseTool - u.activeTool = u.Canvas.Tool.String() - d.Flash("Ellipse Tool selected.") - }) - toolMenu.AddItemAccel("Eraser Tool", "x", func() { - u.Canvas.Tool = drawtool.EraserTool - u.activeTool = u.Canvas.Tool.String() - d.Flash("Eraser Tool selected.") - }) - - if u.Scene.DrawingType == enum.LevelDrawing { - toolMenu.AddItemAccel("Doodads", "d", func() { - log.Info("Open the DoodadDropper") - u.doodadWindow.Show() - }) - toolMenu.AddItem("Link Tool", func() { - u.Canvas.Tool = drawtool.LinkTool - u.activeTool = u.Canvas.Tool.String() - d.Flash("Link Tool selected. Click a doodad in your level to link it to another.") - }) - } - - //////// - // Help menu - helpMenu := menu.AddMenu("Help") - helpMenu.AddItemAccel("User Manual", "F1", func() { - native.OpenLocalURL(balance.GuidebookPath) - }) - helpMenu.AddItem("Register", func() { - u.licenseWindow.Show() - }) - helpMenu.AddItem("About", func() { - if u.aboutWindow == nil { - u.aboutWindow = windows.NewAboutWindow(windows.About{ - Supervisor: u.Supervisor, - Engine: d.Engine, - }) - u.aboutWindow.Compute(d.Engine) - u.aboutWindow.Supervise(u.Supervisor) - - // Center the window. - u.aboutWindow.MoveTo(render.Point{ - X: (d.width / 2) - (u.aboutWindow.Size().W / 2), - Y: 60, - }) - } - u.aboutWindow.Show() - }) - - menu.Supervise(u.Supervisor) - menu.Compute(d.Engine) - - return menu -} - // SetupStatusBar sets up the status bar widget along the bottom of the window. func (u *EditorUI) SetupStatusBar(d *Doodle) *ui.Frame { frame := ui.NewFrame("Status Bar") diff --git a/pkg/editor_ui_menubar.go b/pkg/editor_ui_menubar.go new file mode 100644 index 0000000..b6ea919 --- /dev/null +++ b/pkg/editor_ui_menubar.go @@ -0,0 +1,318 @@ +package doodle + +// Menu Bar features for Edit Mode. +// In here is the SetupMenuBar() and menu item functions. +// The rest of it is controlled in editor_ui.go + +import ( + "strconv" + + "git.kirsle.net/apps/doodle/pkg/balance" + "git.kirsle.net/apps/doodle/pkg/drawtool" + "git.kirsle.net/apps/doodle/pkg/enum" + "git.kirsle.net/apps/doodle/pkg/log" + "git.kirsle.net/apps/doodle/pkg/native" + "git.kirsle.net/apps/doodle/pkg/windows" + "git.kirsle.net/go/render" + "git.kirsle.net/go/ui" +) + +// SetupMenuBar sets up the menu bar. +func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.MenuBar { + menu := ui.NewMenuBar("Main Menu") + + // Save and Save As common menu handler + var ( + drawingType string + saveFunc func(filename string) + ) + + switch u.Scene.DrawingType { + case enum.LevelDrawing: + drawingType = "level" + saveFunc = func(filename string) { + if err := u.Scene.SaveLevel(filename); err != nil { + d.Flash("Error: %s", err) + } else { + d.Flash("Saved level: %s", filename) + } + } + case enum.DoodadDrawing: + drawingType = "doodad" + saveFunc = func(filename string) { + if err := u.Scene.SaveDoodad(filename); err != nil { + d.Flash("Error: %s", err) + } else { + d.Flash("Saved doodad: %s", filename) + } + } + default: + d.Flash("Error: Scene.DrawingType is not a valid type") + } + + //////// + // File menu + fileMenu := menu.AddMenu("File") + fileMenu.AddItemAccel("New level", "Ctrl-N", u.Scene.MenuNewLevel) + fileMenu.AddItem("New doodad", func() { + u.Scene.ConfirmUnload(func() { + d.Prompt("Doodad size [100]>", func(answer string) { + size := balance.DoodadSize + if answer != "" { + i, err := strconv.Atoi(answer) + if err != nil { + d.Flash("Error: Doodad size must be a number.") + return + } + size = i + } + d.NewDoodad(size) + }) + }) + }) + fileMenu.AddItemAccel("Save", "Ctrl-S", u.Scene.MenuSave(false)) + fileMenu.AddItemAccel("Save as...", "Shift-Ctrl-S", func() { + d.Prompt("Save as filename>", func(answer string) { + if answer != "" { + saveFunc(answer) + } + }) + }) + + if balance.Feature.EmbeddableDoodads && drawingType == "level" { + fileMenu.AddItem("Publish level", func() { + u.OpenPublishWindow() + }) + } + + fileMenu.AddItemAccel("Open...", "Ctrl-O", u.Scene.MenuOpen) + fileMenu.AddSeparator() + fileMenu.AddItem("Close "+drawingType, func() { + u.Scene.ConfirmUnload(func() { + d.Goto(&MainScene{}) + }) + }) + fileMenu.AddItemAccel("Quit", "Escape", func() { + d.ConfirmExit() + }) + + //////// + // Edit menu + editMenu := menu.AddMenu("Edit") + editMenu.AddItemAccel("Undo", "Ctrl-Z", func() { + u.Canvas.UndoStroke() + }) + editMenu.AddItemAccel("Redo", "Ctrl-Y", func() { + u.Canvas.RedoStroke() + }) + + //////// + // 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.AddItem("Attached files", func() { + log.Info("Opening the FileSystem window") + u.OpenFileSystemWindow() + }) + 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") + toolMenu.AddItemAccel("Debug overlay", "F3", func() { + DebugOverlay = !DebugOverlay + if DebugOverlay { + d.Flash("Debug overlay enabled. Press F3 to turn it off.") + } + }) + toolMenu.AddItemAccel("Command shell", "`", func() { + d.shell.Open = true + }) + toolMenu.AddSeparator() + toolMenu.AddItem("Edit Palette", func() { + u.OpenPaletteWindow() + }) + if u.Scene.DrawingType == enum.LevelDrawing { + toolMenu.AddItemAccel("Doodads", "d", func() { + log.Info("Open the DoodadDropper") + u.doodadWindow.Show() + }) + } else if u.Scene.DrawingType == enum.DoodadDrawing { + toolMenu.AddItem("Layers", func() { + u.OpenLayersWindow() + }) + } + + // Draw Tools + toolMenu.AddItemAccel("Pencil Tool", "F", func() { + u.Canvas.Tool = drawtool.PencilTool + u.activeTool = u.Canvas.Tool.String() + d.Flash("Pencil Tool selected.") + }) + toolMenu.AddItemAccel("Line Tool", "L", func() { + u.Canvas.Tool = drawtool.LineTool + u.activeTool = u.Canvas.Tool.String() + d.Flash("Line Tool selected.") + }) + toolMenu.AddItemAccel("Rectangle Tool", "R", func() { + u.Canvas.Tool = drawtool.RectTool + u.activeTool = u.Canvas.Tool.String() + d.Flash("Rectangle Tool selected.") + }) + toolMenu.AddItemAccel("Ellipse Tool", "C", func() { + u.Canvas.Tool = drawtool.EllipseTool + u.activeTool = u.Canvas.Tool.String() + d.Flash("Ellipse Tool selected.") + }) + toolMenu.AddItemAccel("Eraser Tool", "x", func() { + u.Canvas.Tool = drawtool.EraserTool + u.activeTool = u.Canvas.Tool.String() + d.Flash("Eraser Tool selected.") + }) + + if u.Scene.DrawingType == enum.LevelDrawing { + toolMenu.AddItemAccel("Doodads", "d", func() { + log.Info("Open the DoodadDropper") + u.doodadWindow.Show() + }) + toolMenu.AddItem("Link Tool", func() { + u.Canvas.Tool = drawtool.LinkTool + u.activeTool = u.Canvas.Tool.String() + d.Flash("Link Tool selected. Click a doodad in your level to link it to another.") + }) + } + + //////// + // Help menu + helpMenu := menu.AddMenu("Help") + helpMenu.AddItemAccel("User Manual", "F1", func() { + native.OpenLocalURL(balance.GuidebookPath) + }) + helpMenu.AddItem("Register", func() { + u.licenseWindow.Show() + }) + helpMenu.AddItem("About", func() { + if u.aboutWindow == nil { + u.aboutWindow = windows.NewAboutWindow(windows.About{ + Supervisor: u.Supervisor, + Engine: d.Engine, + }) + u.aboutWindow.Compute(d.Engine) + u.aboutWindow.Supervise(u.Supervisor) + + // Center the window. + u.aboutWindow.MoveTo(render.Point{ + X: (d.width / 2) - (u.aboutWindow.Size().W / 2), + Y: 60, + }) + } + u.aboutWindow.Show() + }) + + menu.Supervise(u.Supervisor) + menu.Compute(d.Engine) + + return menu +} + +// Menu functions that have keybind callbacks below. + +// File->New level, or Ctrl-N +func (s *EditorScene) MenuNewLevel() { + s.ConfirmUnload(func() { + s.d.GotoNewMenu() + }) +} + +// File->Open, or Ctrl-O +func (s *EditorScene) MenuOpen() { + s.ConfirmUnload(func() { + s.d.GotoLoadMenu() + }) +} + +// File->Save, or Ctrl-S +// File->Save As, or Shift-Ctrl-S +// NOTICE: this one returns a func() so you need to call that one! +func (s *EditorScene) MenuSave(as bool) func() { + return func() { + var ( + // drawingType string + saveFunc func(filename string) + ) + + switch s.DrawingType { + case enum.LevelDrawing: + // drawingType = "level" + saveFunc = func(filename string) { + if err := s.SaveLevel(filename); err != nil { + s.d.Flash("Error: %s", err) + } else { + s.d.Flash("Saved level: %s", filename) + } + } + case enum.DoodadDrawing: + // drawingType = "doodad" + saveFunc = func(filename string) { + if err := s.SaveDoodad(filename); err != nil { + s.d.Flash("Error: %s", err) + } else { + s.d.Flash("Saved doodad: %s", filename) + } + } + default: + s.d.Flash("Error: Scene.DrawingType is not a valid type") + } + + // "Save As"? + if as { + s.d.Prompt("Save as filename>", func(answer string) { + if answer != "" { + saveFunc(answer) + } + }) + return + } + + // "Save", write to existing filename or prompt for it. + if s.filename != "" { + saveFunc(s.filename) + } else { + s.d.Prompt("Save filename>", func(answer string) { + if answer != "" { + saveFunc(answer) + } + }) + } + } +} diff --git a/pkg/keybind/keybind.go b/pkg/keybind/keybind.go index 3623f40..682a463 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") } +// New Level (Ctrl-N) +func NewLevel(ev *event.State) bool { + return ev.Ctrl && ev.KeyDown("n") +} + +// Save (Ctrl-S) +func Save(ev *event.State) bool { + return ev.Ctrl && ev.KeyDown("s") +} + +// SaveAs (Shift-Ctrl-S) +func SaveAs(ev *event.State) bool { + return ev.Ctrl && ev.Shift && ev.KeyDown("s") +} + +// Open (Ctrl-O) +func Open(ev *event.State) bool { + return ev.Ctrl && ev.KeyDown("o") +} + // ZoomIn (+) func ZoomIn(ev *event.State) bool { return ev.KeyDown("=") || ev.KeyDown("+")