From ddcad274857de1970c1c2e93f59f8fe24df6726e Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Thu, 16 Feb 2023 21:47:18 -0800 Subject: [PATCH 1/3] WIP: Chunker size to uint8 and Rectangular Doodads Convert the Chunker size to a uint8 so chunk sizes are limited to 255px. This means that inside of a chunk, uint8's can track the relative pixel coordinates and result in a great memory savings since all of these uint8's are currently 64-bits wide apiece. WIP on rectangular shaped doodads: * You can create such a doodad in the editor and draw it normally. * It doesn't draw the right size when dragged into your level however: - In uix.Actor.Size() it gets a rect of the doodad's square Chunker size, instead of getting the proper doodad.Size rect. - If you give it the doodad.Size rect, it draws the Canvas size correctly instead of a square - the full drawing appears and in gameplay its hitbox (assuming the same large rectangle size) works correctly in-game. - But, the doodad has scrolling issues when it gets to the top or left edge of the screen! This old gnarly bug has come back. For some reason square canvas doodads draw correctly but rectangular ones have the drawing scroll just a bit - how far it scrolls is proportional to how big the doodad is, with the Start Flag only scrolling a few pixels before it stops. --- bootstrap.py | 13 +- cmd/doodad/commands/convert.go | 15 +- cmd/doodad/commands/edit_level.go | 9 +- cmd/doodad/commands/show.go | 18 +- pkg/balance/numbers.go | 2 +- pkg/doodads/doodad.go | 54 +++++- pkg/doodads/drawing.go | 2 + pkg/doodle.go | 27 +-- pkg/editor_scene.go | 8 +- pkg/editor_ui_doodad.go | 2 +- pkg/editor_ui_menubar.go | 14 +- pkg/editor_ui_palette.go | 7 +- pkg/level/chunk.go | 25 +-- pkg/level/chunker.go | 29 +-- .../giant_screenshot/giant_screenshot.go | 2 +- pkg/menu_scene.go | 17 +- pkg/play_inventory.go | 2 +- pkg/play_scene.go | 4 +- pkg/uix/actor.go | 16 +- pkg/uix/canvas.go | 19 +- pkg/uix/canvas_actors.go | 2 +- pkg/uix/canvas_present.go | 5 +- pkg/uix/magic-form/magic_form.go | 8 + pkg/windows/add_edit_level.go | 180 ++++++++---------- pkg/windows/doodad_dropper.go | 2 +- 25 files changed, 273 insertions(+), 209 deletions(-) diff --git a/bootstrap.py b/bootstrap.py index 90d9c62..927c414 100755 --- a/bootstrap.py +++ b/bootstrap.py @@ -70,23 +70,22 @@ def main(fast=False): def install_deps(fast): """Install system dependencies.""" - if fast: - fast = " -y" + fast = " -y" if fast else "" if shell("which rpm") == 0 and shell("which dnf") == 0: # Fedora-like. if shell("rpm -q {}".format(' '.join(dep_fedora))) != 0: - must_shell("sudo dnf install {}{}".format(' '.join(dep_fedora)), fast) + must_shell("sudo dnf install {}{}".format(' '.join(dep_fedora), fast)) elif shell("which brew") == 0: # MacOS, as Catalina has an apt command now?? - must_shell("brew install {} {}".format(' '.join(dep_macos)), fast) + must_shell("brew install {} {}".format(' '.join(dep_macos), fast)) elif shell("which apt") == 0: # Debian-like. if shell("dpkg-query -l {}".format(' '.join(dep_debian))) != 0: - must_shell("sudo apt update && sudo apt install {}{}".format(' '.join(dep_debian)), fast) + must_shell("sudo apt update && sudo apt install {}{}".format(' '.join(dep_debian), fast)) elif shell("which pacman") == 0: # Arch-like. - must_shell("sudo pacman -S{} {}{}".format(fast, ' '.join(dep_arch))) + must_shell("sudo pacman -S{} {}".format(fast, ' '.join(dep_arch))) else: print("Warning: didn't detect your package manager to install SDL2 and other dependencies") @@ -152,7 +151,7 @@ def must_shell(cmd): if __name__ == "__main__": parser = argparse.ArgumentParser("doodle bootstrap") - parser.add_argument("fast", "f", + parser.add_argument("--fast", "-f", action="store_true", help="Run the script non-interactively (yes to your package manager, git clone over https)", ) diff --git a/cmd/doodad/commands/convert.go b/cmd/doodad/commands/convert.go index 6c855dc..c6bcc29 100644 --- a/cmd/doodad/commands/convert.go +++ b/cmd/doodad/commands/convert.go @@ -104,7 +104,9 @@ func imageToDrawing(c *cli.Context, chroma render.Color, inputFiles []string, ou // Read the source images. Ensure they all have the same boundaries. var ( imageBounds image.Point - chunkSize int // the square shape for the Doodad chunk size + chunkSize uint8 // the square shape for the Doodad chunk size + width int // dimensions of the incoming image + height int images []image.Image ) @@ -130,9 +132,9 @@ func imageToDrawing(c *cli.Context, chroma render.Color, inputFiles []string, ou if i == 0 { imageBounds = imageSize if imageSize.X > imageSize.Y { - chunkSize = imageSize.X + width = imageSize.X } else { - chunkSize = imageSize.Y + height = imageSize.Y } } else if imageSize != imageBounds { return cli.Exit("your source images are not all the same dimensions", 1) @@ -151,7 +153,7 @@ func imageToDrawing(c *cli.Context, chroma render.Color, inputFiles []string, ou switch strings.ToLower(filepath.Ext(outputFile)) { case extDoodad: log.Info("Output is a Doodad file (chunk size %d): %s", chunkSize, outputFile) - doodad := doodads.New(chunkSize) + doodad := doodads.New(width, height) doodad.GameVersion = branding.Version doodad.Title = c.String("title") if doodad.Title == "" { @@ -188,6 +190,9 @@ func imageToDrawing(c *cli.Context, chroma render.Color, inputFiles []string, ou lvl := level.New() lvl.GameVersion = branding.Version + lvl.MaxWidth = int64(width) + lvl.MaxHeight = int64(height) + lvl.PageType = level.Bounded lvl.Title = c.String("title") if lvl.Title == "" { lvl.Title = "Converted Level" @@ -288,7 +293,7 @@ func drawingToImage(c *cli.Context, chroma render.Color, inputFiles []string, ou // // img: input image like a PNG // chroma: transparent color -func imageToChunker(img image.Image, chroma render.Color, palette *level.Palette, chunkSize int) (*level.Palette, *level.Chunker) { +func imageToChunker(img image.Image, chroma render.Color, palette *level.Palette, chunkSize uint8) (*level.Palette, *level.Chunker) { var ( chunker = level.NewChunker(chunkSize) bounds = img.Bounds() diff --git a/cmd/doodad/commands/edit_level.go b/cmd/doodad/commands/edit_level.go index a97e805..5e8c580 100644 --- a/cmd/doodad/commands/edit_level.go +++ b/cmd/doodad/commands/edit_level.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" + "git.kirsle.net/SketchyMaze/doodle/pkg/balance" "git.kirsle.net/SketchyMaze/doodle/pkg/level" "git.kirsle.net/SketchyMaze/doodle/pkg/log" "git.kirsle.net/go/render" @@ -220,7 +221,13 @@ func editLevel(c *cli.Context, filename string) error { // Handles the deep operation of re-copying the old level into a new level // at the new chunk size. func rechunkLevel(c *cli.Context, filename string, lvl *level.Level) error { - var chunkSize = c.Int("resize") + var chunkSize = balance.ChunkSize + if v := c.Int("resize"); v != 0 { + if v > 255 { + return errors.New("chunk size must be a uint8 <= 255") + } + chunkSize = uint8(v) + } log.Info("Resizing the level's chunk size.") log.Info("Current chunk size: %d", lvl.Chunker.Size) log.Info("Target chunk size: %d", chunkSize) diff --git a/cmd/doodad/commands/show.go b/cmd/doodad/commands/show.go index 113e2bf..2144d17 100644 --- a/cmd/doodad/commands/show.go +++ b/cmd/doodad/commands/show.go @@ -205,6 +205,7 @@ func showDoodad(c *cli.Context, filename string) error { fmt.Printf(" Game version: %s\n", dd.GameVersion) fmt.Printf(" Doodad title: %s\n", dd.Title) fmt.Printf(" Author: %s\n", dd.Author) + fmt.Printf(" Dimensions: %s\n", dd.Size) fmt.Printf(" Hitbox: %s\n", dd.Hitbox) fmt.Printf(" Locked: %+v\n", dd.Locked) fmt.Printf(" Hidden: %+v\n", dd.Hidden) @@ -256,9 +257,12 @@ func showPalette(pal *level.Palette) { } func showChunker(c *cli.Context, ch *level.Chunker) { - var worldSize = ch.WorldSize() - var width = worldSize.W - worldSize.X - var height = worldSize.H - worldSize.Y + var ( + worldSize = ch.WorldSize() + chunkSize = int(ch.Size) + width = worldSize.W - worldSize.X + height = worldSize.H - worldSize.Y + ) fmt.Println("Chunks:") fmt.Printf(" Pixels Per Chunk: %d^2\n", ch.Size) fmt.Printf(" Number Generated: %d\n", len(ch.Chunks)) @@ -277,10 +281,10 @@ func showChunker(c *cli.Context, ch *level.Chunker) { fmt.Printf(" - Coord: %s\n", point) fmt.Printf(" Type: %s\n", chunkTypeToName(chunk.Type)) fmt.Printf(" Range: (%d,%d) ... (%d,%d)\n", - int(point.X)*ch.Size, - int(point.Y)*ch.Size, - (int(point.X)*ch.Size)+ch.Size, - (int(point.Y)*ch.Size)+ch.Size, + int(point.X)*chunkSize, + int(point.Y)*chunkSize, + (int(point.X)*chunkSize)+chunkSize, + (int(point.Y)*chunkSize)+chunkSize, ) } } else { diff --git a/pkg/balance/numbers.go b/pkg/balance/numbers.go index 630d06a..5f89a1c 100644 --- a/pkg/balance/numbers.go +++ b/pkg/balance/numbers.go @@ -55,7 +55,7 @@ var ( FollowPlayerFirstTicks uint64 = 60 // Default chunk size for canvases. - ChunkSize = 128 + ChunkSize uint8 = 128 // Default size for a new Doodad. DoodadSize = 100 diff --git a/pkg/doodads/doodad.go b/pkg/doodads/doodad.go index 0a39dc5..3ea8301 100644 --- a/pkg/doodads/doodad.go +++ b/pkg/doodads/doodad.go @@ -14,6 +14,7 @@ type Doodad struct { Filename string `json:"-"` // used internally, not saved in json Hidden bool `json:"hidden,omitempty"` Palette *level.Palette `json:"palette"` + Size render.Rect `json:"size"` // doodad dimensions Script string `json:"script"` Hitbox render.Rect `json:"hitbox"` Layers []Layer `json:"layers"` @@ -30,10 +31,41 @@ type Layer struct { Chunker *level.Chunker `json:"chunks"` } -// New creates a new Doodad. -func New(size int) *Doodad { - if size == 0 { - size = balance.DoodadSize +/* +New creates a new Doodad. + +You can give it one or two values for dimensions: + +- New(size int) creates a square doodad (classic) +- New(width, height int) lets you have a different width x height. +*/ +func New(dimensions ...int) *Doodad { + var ( + // Defaults + size = balance.DoodadSize + chunkSize = balance.ChunkSize + width int + height int + ) + + switch len(dimensions) { + case 1: + size = dimensions[0] + case 2: + width, height = dimensions[0], dimensions[1] + } + + // If no width/height, make it a classic square doodad. + if width+height == 0 { + width = size + height = size + } + + // Pick an optimal chunk size - if our doodad size + // is under 256 use only one chunk per layer by matching + // that size. + if size <= 255 { + chunkSize = uint8(size) } return &Doodad{ @@ -41,11 +73,12 @@ func New(size int) *Doodad { Version: 1, }, Palette: level.DefaultPalette(), - Hitbox: render.NewRect(size, size), + Hitbox: render.NewRect(width, height), + Size: render.NewRect(width, height), Layers: []Layer{ { Name: "main", - Chunker: level.NewChunker(size), + Chunker: level.NewChunker(chunkSize), }, }, Tags: map[string]string{}, @@ -59,7 +92,7 @@ func New(size int) *Doodad { // is optional - pass nil and a new blank chunker is created. func (d *Doodad) AddLayer(name string, chunker *level.Chunker) Layer { if chunker == nil { - chunker = level.NewChunker(d.ChunkSize()) + chunker = level.NewChunker(d.ChunkSize8()) } layer := Layer{ @@ -107,12 +140,17 @@ func (d *Doodad) Tag(name string) string { // ChunkSize returns the chunk size of the Doodad's first layer. func (d *Doodad) ChunkSize() int { + return int(d.Layers[0].Chunker.Size) +} + +// ChunkSize8 returns the chunk size of the Doodad's first layer as its actual uint8 value. +func (d *Doodad) ChunkSize8() uint8 { return d.Layers[0].Chunker.Size } // Rect returns a rect of the ChunkSize for scaling a Canvas widget. func (d *Doodad) Rect() render.Rect { - var size = d.ChunkSize() + var size = int(d.ChunkSize()) return render.Rect{ W: size, H: size, diff --git a/pkg/doodads/drawing.go b/pkg/doodads/drawing.go index f57a282..e8a6312 100644 --- a/pkg/doodads/drawing.go +++ b/pkg/doodads/drawing.go @@ -1,6 +1,7 @@ package doodads import ( + "git.kirsle.net/SketchyMaze/doodle/pkg/log" "git.kirsle.net/go/render" "github.com/google/uuid" ) @@ -24,6 +25,7 @@ func NewDrawing(id string, doodad *Doodad) *Drawing { if id == "" { id = uuid.Must(uuid.NewUUID()).String() } + log.Warn("NewDraging(%s): doodad.Rect=%s doodad.Size=%s", doodad.Title, doodad.Rect(), doodad.Size) return &Drawing{ id: id, Doodad: doodad, diff --git a/pkg/doodle.go b/pkg/doodle.go index 5c7fd0a..d2fc31f 100644 --- a/pkg/doodle.go +++ b/pkg/doodle.go @@ -3,7 +3,6 @@ package doodle import ( "fmt" "path/filepath" - "strconv" "strings" "time" @@ -291,32 +290,16 @@ func (d *Doodle) NewMap() { // NewDoodad loads a new Doodad in Edit Mode. // If size is zero, it prompts the user to select a size or accept the default size. -func (d *Doodle) NewDoodad(size int) { - if size == 0 { - d.Prompt(fmt.Sprintf("Doodad size or %d>", balance.DoodadSize), func(answer string) { - size := balance.DoodadSize - if answer != "" { - i, err := strconv.Atoi(answer) - if err != nil { - d.FlashError("Error: Doodad size must be a number.") - return - } - size = i - } - - // Recurse with the proper answer. - if size <= 0 { - d.FlashError("Error: Doodad size must be a positive number.") - } - d.NewDoodad(size) - }) - return +func (d *Doodle) NewDoodad(width, height int) { + if width+height == 0 { + width = balance.DoodadSize + height = width } log.Info("Starting a new doodad") scene := &EditorScene{ DrawingType: enum.DoodadDrawing, - DoodadSize: size, + DoodadSize: render.NewRect(width, height), } d.Goto(scene) } diff --git a/pkg/editor_scene.go b/pkg/editor_scene.go index 127bbe2..4a5f145 100644 --- a/pkg/editor_scene.go +++ b/pkg/editor_scene.go @@ -32,7 +32,7 @@ type EditorScene struct { DrawingType enum.DrawingType OpenFile bool Filename string - DoodadSize int + DoodadSize render.Rect RememberScrollPosition render.Point // Play mode remembers it for us UI *EditorUI @@ -195,7 +195,7 @@ func (s *EditorScene) setupAsync(d *Doodle) error { // No Doodad? if s.Doodad == nil { log.Debug("EditorScene.Setup: initializing a new Doodad") - s.Doodad = doodads.New(s.DoodadSize) + s.Doodad = doodads.New(s.DoodadSize.W, s.DoodadSize.H) s.UI.Canvas.LoadDoodad(s.Doodad) } @@ -206,7 +206,7 @@ func (s *EditorScene) setupAsync(d *Doodle) error { ) // TODO: move inside the UI. Just an approximate position for now. - s.UI.Canvas.Resize(render.NewRect(s.DoodadSize, s.DoodadSize)) + s.UI.Canvas.Resize(s.DoodadSize) s.UI.Canvas.ScrollTo(render.Origin) s.UI.Canvas.Scrollable = false s.UI.Workspace.Compute(d.Engine) @@ -574,7 +574,7 @@ func (s *EditorScene) LoadDoodad(filename string) error { s.DrawingType = enum.DoodadDrawing s.Doodad = doodad - s.DoodadSize = doodad.Layers[0].Chunker.Size + s.DoodadSize = doodad.Size s.UI.Canvas.LoadDoodad(s.Doodad) return nil } diff --git a/pkg/editor_ui_doodad.go b/pkg/editor_ui_doodad.go index 2b945e9..a847bd0 100644 --- a/pkg/editor_ui_doodad.go +++ b/pkg/editor_ui_doodad.go @@ -46,7 +46,7 @@ func (u *EditorUI) startDragActor(doodad *doodads.Doodad, actor *level.Actor) { } // Size and scale this doodad according to the zoom level. - size := doodad.Rect() + size := doodad.Size size.W = u.Canvas.ZoomMultiply(size.W) size.H = u.Canvas.ZoomMultiply(size.H) diff --git a/pkg/editor_ui_menubar.go b/pkg/editor_ui_menubar.go index c909786..b0a6ff4 100644 --- a/pkg/editor_ui_menubar.go +++ b/pkg/editor_ui_menubar.go @@ -52,12 +52,7 @@ func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.MenuBar { // File menu fileMenu := menu.AddMenu("File") fileMenu.AddItemAccel("New level", "Ctrl-N", u.Scene.MenuNewLevel) - fileMenu.AddItem("New doodad", func() { - u.Scene.ConfirmUnload(func() { - // New doodad size with prompt. - d.NewDoodad(0) - }) - }) + fileMenu.AddItem("New doodad", u.Scene.MenuNewDoodad) 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) { @@ -285,6 +280,13 @@ func (s *EditorScene) MenuNewLevel() { }) } +func (s *EditorScene) MenuNewDoodad() { + s.ConfirmUnload(func() { + // New doodad size with prompt. + s.d.GotoNewDoodadMenu() + }) +} + // File->Open, or Ctrl-O func (s *EditorScene) MenuOpen() { s.ConfirmUnload(func() { diff --git a/pkg/editor_ui_palette.go b/pkg/editor_ui_palette.go index 99c438f..39d70c3 100644 --- a/pkg/editor_ui_palette.go +++ b/pkg/editor_ui_palette.go @@ -83,11 +83,11 @@ func (u *EditorUI) setupPaletteFrame(window *ui.Window) *ui.Frame { if u.Canvas != nil && u.Canvas.Palette != nil { for i, swatch := range u.Canvas.Palette.Swatches { swatch := swatch - var width = buttonSize + var width = uint8(buttonSize) // TODO: dangerous - buttonSize must be small // Drawing buttons in two-column mode? (default right-side palette layout) if twoColumn { - width = buttonSize / 2 + width /= 2 if row == nil || i%2 == 0 { row = ui.NewFrame(fmt.Sprintf("Swatch(%s) Button Frame", swatch.Name)) frame.Pack(row, packConfig) @@ -101,7 +101,8 @@ func (u *EditorUI) setupPaletteFrame(window *ui.Window) *ui.Frame { var ( colorbox = uix.NewCanvas(width, false) chunker = level.NewChunker(width) - size = render.NewRect(width, width) + iw = int(width) + size = render.NewRect(iw, iw) ) chunker.SetRect(size, swatch) colorbox.Resize(size) diff --git a/pkg/level/chunk.go b/pkg/level/chunk.go index d074044..46a461c 100644 --- a/pkg/level/chunk.go +++ b/pkg/level/chunk.go @@ -27,7 +27,7 @@ type Chunk struct { // Values told to it from higher up, not stored in JSON. Point render.Point - Size int + Size uint8 // Texture cache properties so we don't redraw pixel-by-pixel every frame. uuid uuid.UUID @@ -157,14 +157,17 @@ func (c *Chunk) generateTexture(mask render.Color) (render.Texturer, error) { // want a cached bitmap image that only generates itself once, and // again when marked dirty. func (c *Chunk) ToBitmap(mask render.Color) image.Image { - canvas := c.SizePositive() - imgSize := image.Rectangle{ - Min: image.Point{}, - Max: image.Point{ - X: c.Size, - Y: c.Size, - }, - } + var ( + size = int(c.Size) + canvas = c.SizePositive() + imgSize = image.Rectangle{ + Min: image.Point{}, + Max: image.Point{ + X: size, + Y: size, + }, + } + ) if imgSize.Max.X == 0 { imgSize.Max.X = int(canvas.W) @@ -186,8 +189,8 @@ func (c *Chunk) ToBitmap(mask render.Color) image.Image { // Pixel coordinate offset to map the Chunk World Position to the // smaller image boundaries. pointOffset := render.Point{ - X: c.Point.X * c.Size, - Y: c.Point.Y * c.Size, + X: c.Point.X * size, + Y: c.Point.Y * size, } // Blot all the pixels onto it. diff --git a/pkg/level/chunker.go b/pkg/level/chunker.go index 4574560..ee97442 100644 --- a/pkg/level/chunker.go +++ b/pkg/level/chunker.go @@ -20,8 +20,8 @@ type Chunker struct { // Layer is optional for the caller, levels use only 0 and // doodads use them for frames. When chunks are exported to // zipfile the Layer keeps them from overlapping. - Layer int `json:"-"` // internal use only - Size int `json:"size"` + Layer int `json:"-"` // internal use only + Size uint8 `json:"size"` // A Zipfile reference for new-style levels and doodads which // keep their chunks in external parts of a zip file. @@ -51,7 +51,7 @@ type Chunker struct { } // NewChunker creates a new chunk manager with a given chunk size. -func NewChunker(size int) *Chunker { +func NewChunker(size uint8) *Chunker { return &Chunker{ Size: size, Chunks: ChunkMap{}, @@ -186,10 +186,13 @@ func (c *Chunker) IterCachedChunks() <-chan *Chunk { func (c *Chunker) IterViewportChunks(viewport render.Rect) <-chan render.Point { pipe := make(chan render.Point) go func() { - sent := make(map[render.Point]interface{}) + var ( + sent = make(map[render.Point]interface{}) + size = int(c.Size) + ) - for x := viewport.X; x < viewport.W; x += (c.Size / 4) { - for y := viewport.Y; y < viewport.H; y += (c.Size / 4) { + for x := viewport.X; x < viewport.W; x += (size / 4) { + for y := viewport.Y; y < viewport.H; y += (size / 4) { // Constrain this chunksize step to a point within the bounds // of the viewport. This can yield partial chunks on the edges @@ -243,12 +246,16 @@ func (c *Chunker) IterPixels() <-chan Pixel { // manage: the lowest pixels from the lowest chunks to the highest pixels of // the highest chunks. func (c *Chunker) WorldSize() render.Rect { - chunkLowest, chunkHighest := c.Bounds() + var ( + size = int(c.Size) + chunkLowest, chunkHighest = c.Bounds() + ) + return render.Rect{ - X: chunkLowest.X * c.Size, - Y: chunkLowest.Y * c.Size, - W: (chunkHighest.X * c.Size) + (c.Size - 1), - H: (chunkHighest.Y * c.Size) + (c.Size - 1), + X: chunkLowest.X * size, + Y: chunkLowest.Y * size, + W: (chunkHighest.X * size) + (size - 1), + H: (chunkHighest.Y * size) + (size - 1), } } diff --git a/pkg/level/giant_screenshot/giant_screenshot.go b/pkg/level/giant_screenshot/giant_screenshot.go index e8ac644..27f03a7 100644 --- a/pkg/level/giant_screenshot/giant_screenshot.go +++ b/pkg/level/giant_screenshot/giant_screenshot.go @@ -44,7 +44,7 @@ func GiantScreenshot(lvl *level.Level) (image.Image, error) { // How big will our image be? var ( size = lvl.Chunker.WorldSizePositive() - chunkSize = lvl.Chunker.Size + chunkSize = int(lvl.Chunker.Size) chunkLow, chunkHigh = lvl.Chunker.Bounds() worldSize = render.Rect{ X: chunkLow.X, diff --git a/pkg/menu_scene.go b/pkg/menu_scene.go index 1342356..42ba387 100644 --- a/pkg/menu_scene.go +++ b/pkg/menu_scene.go @@ -24,6 +24,7 @@ on the MainScene or elsewhere as wanted. type MenuScene struct { // Configuration. StartupMenu string + NewDoodad bool Supervisor *ui.Supervisor @@ -60,6 +61,17 @@ func (d *Doodle) GotoNewMenu() { d.Goto(scene) } +// GotoNewDoodadMenu loads the MenuScene and shows the "New" window, +// but selected on the Doodad tab by default. +func (d *Doodle) GotoNewDoodadMenu() { + log.Info("Loading the MenuScene to the New window") + scene := &MenuScene{ + StartupMenu: "new", + NewDoodad: true, + } + d.Goto(scene) +} + // GotoLoadMenu loads the MenuScene and shows the "Load" window. func (d *Doodle) GotoLoadMenu() { log.Info("Loading the MenuScene to the Load window for Edit Mode") @@ -152,6 +164,7 @@ func (s *MenuScene) setupNewWindow(d *Doodle) error { window := windows.NewAddEditLevel(windows.AddEditLevel{ Supervisor: s.Supervisor, Engine: d.Engine, + NewDoodad: s.NewDoodad, OnChangePageTypeAndWallpaper: func(pageType level.PageType, wallpaper string) { log.Info("OnChangePageTypeAndWallpaper called: %+v, %+v", pageType, wallpaper) s.canvas.Destroy() // clean up old textures @@ -163,8 +176,8 @@ func (s *MenuScene) setupNewWindow(d *Doodle) error { Level: lvl, }) }, - OnCreateNewDoodad: func(size int) { - d.NewDoodad(size) + OnCreateNewDoodad: func(width, height int) { + d.NewDoodad(width, height) }, OnCancel: func() { d.Goto(&MainScene{}) diff --git a/pkg/play_inventory.go b/pkg/play_inventory.go index 4437b74..adf957e 100644 --- a/pkg/play_inventory.go +++ b/pkg/play_inventory.go @@ -69,7 +69,7 @@ func (s *PlayScene) computeInventory() { continue } - canvas := uix.NewCanvas(doodad.ChunkSize(), false) + canvas := uix.NewCanvas(doodad.ChunkSize8(), false) canvas.SetBackground(render.RGBA(1, 0, 0, 0)) canvas.LoadDoodad(doodad) canvas.Resize(render.NewRect( diff --git a/pkg/play_scene.go b/pkg/play_scene.go index e26429b..9dd8acd 100644 --- a/pkg/play_scene.go +++ b/pkg/play_scene.go @@ -471,10 +471,10 @@ func (s *PlayScene) installPlayerDoodad(filename string, spawn render.Point, cen // Center the player within the box of the doodad, for the Start Flag especially. if !centerIn.IsZero() { spawn = render.NewPoint( - spawn.X+(centerIn.W/2)-(player.Layers[0].Chunker.Size/2), + spawn.X+(centerIn.W/2)-(player.ChunkSize()/2), // Y: the bottom of the flag, 4 pixels from the floor. - spawn.Y+centerIn.H-4-(player.Layers[0].Chunker.Size), + spawn.Y+centerIn.H-4-(player.ChunkSize()), ) } else if spawn.IsZero() && !s.SpawnPoint.IsZero() { spawn = s.SpawnPoint diff --git a/pkg/uix/actor.go b/pkg/uix/actor.go index c6c0f85..19c9801 100644 --- a/pkg/uix/actor.go +++ b/pkg/uix/actor.go @@ -19,11 +19,11 @@ import ( // Actor is an object that marries together the three things that make a // Doodad instance "tick" while inside a Canvas: // -// - uix.Actor is a doodads.Drawing so it fulfills doodads.Actor to be a -// dynamic object during gameplay. -// - It has a pointer to the level.Actor indicating its static level data -// as defined in the map: its spawn coordinate and configuration. -// - A uix.Canvas that can present the actor's graphics to the screen. +// - uix.Actor is a doodads.Drawing so it fulfills doodads.Actor to be a +// dynamic object during gameplay. +// - It has a pointer to the level.Actor indicating its static level data +// as defined in the map: its spawn coordinate and configuration. +// - A uix.Canvas that can present the actor's graphics to the screen. type Actor struct { Drawing *doodads.Drawing Actor *level.Actor @@ -67,8 +67,8 @@ func NewActor(id string, levelActor *level.Actor, doodad *doodads.Doodad) *Actor id = uuid.Must(uuid.NewUUID()).String() } - size := doodad.Layers[0].Chunker.Size - can := NewCanvas(int(size), false) + size := doodad.ChunkSize() + can := NewCanvas(uint8(size), false) can.Name = id // TODO: if the Background is render.Invisible it gets defaulted to @@ -76,7 +76,7 @@ func NewActor(id string, levelActor *level.Actor, doodad *doodads.Doodad) *Actor can.SetBackground(render.RGBA(0, 0, 1, 0)) can.LoadDoodad(doodad) - can.Resize(render.NewRect(size, size)) + can.Resize(doodad.Size) actor := &Actor{ Drawing: doodads.NewDrawing(id, doodad), diff --git a/pkg/uix/canvas.go b/pkg/uix/canvas.go index d162847..32dd80e 100644 --- a/pkg/uix/canvas.go +++ b/pkg/uix/canvas.go @@ -132,15 +132,17 @@ type Canvas struct { // NewCanvas initializes a Canvas widget. // +// size is the Chunker size (uint8) +// // If editable is true, Scrollable is also set to true, which means the arrow // keys will scroll the canvas viewport which is desirable in Edit Mode. -func NewCanvas(size int, editable bool) *Canvas { +func NewCanvas(size uint8, editable bool) *Canvas { w := &Canvas{ Editable: editable, Scrollable: editable, Palette: level.NewPalette(), BrushSize: 1, - chunks: level.NewChunker(size), + chunks: level.NewChunker(uint8(size)), actors: make([]*Actor, 0), wallpaper: &Wallpaper{}, @@ -372,7 +374,7 @@ func (w *Canvas) ViewportRelative() render.Rect { // levels under control. func (w *Canvas) LoadingViewport() render.Rect { var ( - chunkSize int + chunkSize uint8 vp = w.Viewport() margin = balance.LoadingViewportMarginChunks ) @@ -381,17 +383,18 @@ func (w *Canvas) LoadingViewport() render.Rect { if w.level != nil { chunkSize = w.level.Chunker.Size } else if w.doodad != nil { - chunkSize = w.doodad.ChunkSize() + chunkSize = w.doodad.ChunkSize8() } else { chunkSize = balance.ChunkSize log.Error("Canvas.LoadingViewport: no drawing to get chunk size from, default to %d", chunkSize) } + var size = int(chunkSize) return render.Rect{ - X: vp.X - chunkSize*margin.X, - Y: vp.Y - chunkSize*margin.Y, - W: vp.W + chunkSize*margin.X, - H: vp.H + chunkSize*margin.Y, + X: vp.X - size*margin.X, + Y: vp.Y - size*margin.Y, + W: vp.W + size*margin.X, + H: vp.H + size*margin.Y, } } diff --git a/pkg/uix/canvas_actors.go b/pkg/uix/canvas_actors.go index 6e949ef..3250403 100644 --- a/pkg/uix/canvas_actors.go +++ b/pkg/uix/canvas_actors.go @@ -220,7 +220,7 @@ func (w *Canvas) drawActors(e render.Engine, p render.Point) { // Hitting the left edge. Cap the X coord and shrink the width. delta := p.X - drawAt.X // positive number drawAt.X = p.X - // scrollTo.X -= delta / 2 // TODO + // scrollTo.X -= delta // TODO resizeTo.W -= delta } diff --git a/pkg/uix/canvas_present.go b/pkg/uix/canvas_present.go index 4ab260e..8f6ad5d 100644 --- a/pkg/uix/canvas_present.go +++ b/pkg/uix/canvas_present.go @@ -121,9 +121,10 @@ func (w *Canvas) Present(e render.Engine, p render.Point) { src.H = S.H } + var size = int(chunk.Size) dst := render.Rect{ - X: p.X + w.Scroll.X + w.BoxThickness(1) + w.ZoomMultiply(coord.X*chunk.Size), - Y: p.Y + w.Scroll.Y + w.BoxThickness(1) + w.ZoomMultiply(coord.Y*chunk.Size), + X: p.X + w.Scroll.X + w.BoxThickness(1) + w.ZoomMultiply(coord.X*size), + Y: p.Y + w.Scroll.Y + w.BoxThickness(1) + w.ZoomMultiply(coord.Y*size), // src.W and src.H will be AT MOST the full width and height of // a Canvas widget. Subtract the scroll offset to keep it bounded diff --git a/pkg/uix/magic-form/magic_form.go b/pkg/uix/magic-form/magic_form.go index a10ebc7..7e2f902 100644 --- a/pkg/uix/magic-form/magic_form.go +++ b/pkg/uix/magic-form/magic_form.go @@ -398,6 +398,14 @@ func (form Form) Create(into *ui.Frame, fields []Field) { if row.OnSelect != nil { row.OnSelect(selection.Value) } + + // Update bound variables. + if v, ok := selection.Value.(int); ok && row.IntVariable != nil { + *row.IntVariable = v + } + if v, ok := selection.Value.(string); ok && row.TextVariable != nil { + *row.TextVariable = v + } } return nil }) diff --git a/pkg/windows/add_edit_level.go b/pkg/windows/add_edit_level.go index 0ce1ce3..8ddeb94 100644 --- a/pkg/windows/add_edit_level.go +++ b/pkg/windows/add_edit_level.go @@ -26,10 +26,13 @@ type AddEditLevel struct { // Editing settings for an existing level? EditLevel *level.Level + // Show the "New Doodad" tab by default? + NewDoodad bool + // Callback functions. OnChangePageTypeAndWallpaper func(pageType level.PageType, wallpaper string) OnCreateNewLevel func(*level.Level) - OnCreateNewDoodad func(size int) + OnCreateNewDoodad func(width, height int) OnReload func() OnCancel func() } @@ -74,6 +77,11 @@ func NewAddEditLevel(config AddEditLevel) *ui.Window { tabframe.Supervise(config.Supervisor) + // Show the doodad tab? + if config.NewDoodad { + tabframe.SetTab("doodad") + } + window.Hide() return window } @@ -389,7 +397,8 @@ func (config AddEditLevel) setupLevelFrame(tf *ui.TabFrame) { func (config AddEditLevel) setupDoodadFrame(tf *ui.TabFrame) { // Default options. var ( - doodadSize = 64 + doodadWidth = 64 + doodadHeight = doodadWidth ) frame := tf.AddTab("doodad", ui.NewLabel(ui.Label{ @@ -401,110 +410,89 @@ func (config AddEditLevel) setupDoodadFrame(tf *ui.TabFrame) { * Frame for selecting Page Type ******************/ - typeFrame := ui.NewFrame("Doodad Options Frame") - frame.Pack(typeFrame, ui.Pack{ - Side: ui.N, - FillX: true, - }) - - label1 := ui.NewLabel(ui.Label{ - Text: "Doodad sprite size (square):", - Font: balance.LabelFont, - }) - typeFrame.Pack(label1, ui.Pack{ - Side: ui.W, - }) - - // A selectbox to suggest some sizes or let the user enter a custom. - sizeBtn := ui.NewSelectBox("Size Select", ui.Label{ - Font: ui.MenuFont, - }) - typeFrame.Pack(sizeBtn, ui.Pack{ - Side: ui.W, - Expand: true, - }) - - for _, row := range []struct { - Name string - Value int - }{ - {"32", 32}, - {"64", 64}, - {"96", 96}, - {"128", 128}, - {"200", 200}, - {"256", 256}, - {"Custom...", 0}, - } { - row := row - sizeBtn.AddItem(row.Name, row.Value, func() {}) + var sizeOptions = []magicform.Option{ + {Label: "32", Value: 32}, + {Label: "64", Value: 64}, + {Label: "96", Value: 96}, + {Label: "128", Value: 128}, + {Label: "200", Value: 200}, + {Label: "256", Value: 256}, + {Label: "Custom...", Value: 0}, } - sizeBtn.SetValue(doodadSize) - sizeBtn.Handle(ui.Change, func(ed ui.EventData) error { - if selection, ok := sizeBtn.GetValue(); ok { - if size, ok := selection.Value.(int); ok { - if size == 0 { - shmem.Prompt("Enter a custom size for the doodad width and height: ", func(answer string) { + form := magicform.Form{ + Supervisor: config.Supervisor, + Engine: config.Engine, + Vertical: true, + LabelWidth: 90, + } + form.Create(frame, []magicform.Field{ + { + Label: "Width:", + Font: balance.LabelFont, + Type: magicform.Selectbox, + IntVariable: &doodadWidth, + Options: sizeOptions, + OnSelect: func(v interface{}) { + if v.(int) == 0 { + shmem.Prompt("Enter a custom size for the doodad width: ", func(answer string) { if a, err := strconv.Atoi(answer); err == nil && a > 0 { - doodadSize = a + doodadWidth = a } else { shmem.FlashError("Doodad size should be a number greater than zero.") } }) - } else { - doodadSize = size } - } - } - return nil + }, + }, + { + Label: "Height:", + Font: balance.LabelFont, + Type: magicform.Selectbox, + IntVariable: &doodadHeight, + Options: sizeOptions, + OnSelect: func(v interface{}) { + if v.(int) == 0 { + shmem.Prompt("Enter a custom size for the doodad height: ", func(answer string) { + if a, err := strconv.Atoi(answer); err == nil && a > 0 { + doodadHeight = a + } else { + shmem.FlashError("Doodad size should be a number greater than zero.") + } + }) + } + }, + }, + { + Buttons: []magicform.Field{ + { + Label: "Continue", + Font: balance.UIFont, + ButtonStyle: &balance.ButtonPrimary, + OnClick: func() { + if config.OnCreateNewDoodad != nil { + config.OnCreateNewDoodad(doodadWidth, doodadHeight) + } else { + shmem.FlashError("OnCreateNewDoodad not attached") + } + }, + }, + { + Label: "Cancel", + Font: balance.UIFont, + ButtonStyle: &balance.ButtonPrimary, + OnClick: func() { + if config.OnCancel != nil { + config.OnCancel() + } else { + shmem.FlashError("OnCancel not attached") + } + }, + }, + }, + }, }) - sizeBtn.Supervise(config.Supervisor) - config.Supervisor.Add(sizeBtn) - - /****************** - * Confirm/cancel buttons. - ******************/ - - bottomFrame := ui.NewFrame("Button Frame") - frame.Pack(bottomFrame, ui.Pack{ - Side: ui.N, - FillX: true, - PadY: 8, - }) - - var buttons = []struct { - Label string - F func(ui.EventData) error - }{ - {"Continue", func(ed ui.EventData) error { - if config.OnCreateNewDoodad != nil { - config.OnCreateNewDoodad(doodadSize) - } else { - shmem.FlashError("OnCreateNewDoodad not attached") - } - return nil - }}, - - {"Cancel", func(ed ui.EventData) error { - config.OnCancel() - return nil - }}, - } - for _, t := range buttons { - btn := ui.NewButton(t.Label, ui.NewLabel(ui.Label{ - Text: t.Label, - Font: balance.MenuFont, - })) - btn.Handle(ui.Click, t.F) - config.Supervisor.Add(btn) - bottomFrame.Pack(btn, ui.Pack{ - Side: ui.W, - PadX: 4, - PadY: 8, - }) - } } // Creates the Game Rules frame for existing level (set difficulty, etc.) diff --git a/pkg/windows/doodad_dropper.go b/pkg/windows/doodad_dropper.go index a9f2590..5c3baea 100644 --- a/pkg/windows/doodad_dropper.go +++ b/pkg/windows/doodad_dropper.go @@ -253,7 +253,7 @@ func makeDoodadTab(config DoodadDropper, frame *ui.Frame, size render.Rect, cate lastColumn = 0 } - can := uix.NewCanvas(int(buttonSize), true) + can := uix.NewCanvas(uint8(buttonSize), true) // TODO: dangerous - buttonSize must be small can.Name = doodad.Title can.SetBackground(balance.DoodadButtonBackground) can.LoadDoodad(doodad) -- 2.30.2 From 31097881ff2590dfaf48fb0413c5f2bd9eb9fe23 Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Fri, 17 Feb 2023 21:09:11 -0800 Subject: [PATCH 2/3] Finalize Non-square Doodads * Fix display bug with rectangular doodads scrolling off screen. * The default Author of new files will be your registration name, if available before using your $USER name. --- cmd/doodad/commands/convert.go | 18 +++-- pkg/doodads/doodad.go | 20 ++++-- pkg/doodads/drawing.go | 2 - pkg/doodads/fmt_zipfile.go | 11 ++- pkg/editor_scene.go | 6 +- pkg/level/types.go | 4 +- pkg/native/username.go | 29 ++++++++ pkg/play_scene.go | 5 ++ pkg/uix/actor.go | 2 +- pkg/uix/canvas.go | 5 ++ pkg/uix/canvas_actors.go | 3 +- pkg/uix/canvas_debug.go | 26 +++++++ pkg/uix/canvas_present.go | 128 ++++++++++++++++++--------------- pkg/windows/doodad_dropper.go | 1 + 14 files changed, 173 insertions(+), 87 deletions(-) create mode 100644 pkg/native/username.go create mode 100644 pkg/uix/canvas_debug.go diff --git a/cmd/doodad/commands/convert.go b/cmd/doodad/commands/convert.go index c6bcc29..d73b655 100644 --- a/cmd/doodad/commands/convert.go +++ b/cmd/doodad/commands/convert.go @@ -15,6 +15,7 @@ import ( "git.kirsle.net/SketchyMaze/doodle/pkg/doodads" "git.kirsle.net/SketchyMaze/doodle/pkg/level" "git.kirsle.net/SketchyMaze/doodle/pkg/log" + "git.kirsle.net/SketchyMaze/doodle/pkg/native" "git.kirsle.net/go/render" "github.com/urfave/cli/v2" "golang.org/x/image/bmp" @@ -104,8 +105,7 @@ func imageToDrawing(c *cli.Context, chroma render.Color, inputFiles []string, ou // Read the source images. Ensure they all have the same boundaries. var ( imageBounds image.Point - chunkSize uint8 // the square shape for the Doodad chunk size - width int // dimensions of the incoming image + width int // dimensions of the incoming image height int images []image.Image ) @@ -131,11 +131,8 @@ func imageToDrawing(c *cli.Context, chroma render.Color, inputFiles []string, ou // Validate all images are the same size. if i == 0 { imageBounds = imageSize - if imageSize.X > imageSize.Y { - width = imageSize.X - } else { - height = imageSize.Y - } + width = imageSize.X + height = imageSize.Y } else if imageSize != imageBounds { return cli.Exit("your source images are not all the same dimensions", 1) } @@ -152,17 +149,18 @@ func imageToDrawing(c *cli.Context, chroma render.Color, inputFiles []string, ou // Generate the output drawing file. switch strings.ToLower(filepath.Ext(outputFile)) { case extDoodad: - log.Info("Output is a Doodad file (chunk size %d): %s", chunkSize, outputFile) doodad := doodads.New(width, height) doodad.GameVersion = branding.Version doodad.Title = c.String("title") if doodad.Title == "" { doodad.Title = "Converted Doodad" } - doodad.Author = os.Getenv("USER") + doodad.Author = native.DefaultAuthor() // Write the first layer and gather its palette. log.Info("Converting first layer to drawing and getting the palette") + var chunkSize = doodad.ChunkSize8() + log.Info("Output is a Doodad file (%dx%d): %s", width, height, outputFile) palette, layer0 := imageToChunker(images[0], chroma, nil, chunkSize) doodad.Palette = palette doodad.Layers[0].Chunker = layer0 @@ -197,7 +195,7 @@ func imageToDrawing(c *cli.Context, chroma render.Color, inputFiles []string, ou if lvl.Title == "" { lvl.Title = "Converted Level" } - lvl.Author = os.Getenv("USER") + lvl.Author = native.DefaultAuthor() palette, chunker := imageToChunker(images[0], chroma, nil, lvl.Chunker.Size) lvl.Palette = palette lvl.Chunker = chunker diff --git a/pkg/doodads/doodad.go b/pkg/doodads/doodad.go index 3ea8301..4803432 100644 --- a/pkg/doodads/doodad.go +++ b/pkg/doodads/doodad.go @@ -42,23 +42,29 @@ You can give it one or two values for dimensions: func New(dimensions ...int) *Doodad { var ( // Defaults - size = balance.DoodadSize - chunkSize = balance.ChunkSize + size int + chunkSize uint8 width int height int ) switch len(dimensions) { case 1: - size = dimensions[0] + width, height = dimensions[0], dimensions[0] case 2: width, height = dimensions[0], dimensions[1] } - // If no width/height, make it a classic square doodad. - if width+height == 0 { - width = size - height = size + // Set the desired chunkSize to be the largest dimension. + if width > height { + size = width + } else { + size = height + } + + // If no size at all, fall back on the default. + if size == 0 { + size = int(balance.ChunkSize) } // Pick an optimal chunk size - if our doodad size diff --git a/pkg/doodads/drawing.go b/pkg/doodads/drawing.go index e8a6312..f57a282 100644 --- a/pkg/doodads/drawing.go +++ b/pkg/doodads/drawing.go @@ -1,7 +1,6 @@ package doodads import ( - "git.kirsle.net/SketchyMaze/doodle/pkg/log" "git.kirsle.net/go/render" "github.com/google/uuid" ) @@ -25,7 +24,6 @@ func NewDrawing(id string, doodad *Doodad) *Drawing { if id == "" { id = uuid.Must(uuid.NewUUID()).String() } - log.Warn("NewDraging(%s): doodad.Rect=%s doodad.Size=%s", doodad.Title, doodad.Rect(), doodad.Size) return &Drawing{ id: id, Doodad: doodad, diff --git a/pkg/doodads/fmt_zipfile.go b/pkg/doodads/fmt_zipfile.go index bc3b39b..c8f5142 100644 --- a/pkg/doodads/fmt_zipfile.go +++ b/pkg/doodads/fmt_zipfile.go @@ -6,8 +6,8 @@ import ( "encoding/json" "fmt" - "git.kirsle.net/SketchyMaze/doodle/pkg/balance" "git.kirsle.net/SketchyMaze/doodle/pkg/log" + "git.kirsle.net/go/render" ) // ToZipfile serializes the doodad into zipfile format. @@ -63,7 +63,7 @@ func (d *Doodad) ToZipfile() ([]byte, error) { // FromZipfile reads a doodad from zipfile format. func FromZipfile(data []byte) (*Doodad, error) { var ( - doodad = New(balance.DoodadSize) + doodad = New(0) err = doodad.populateFromZipfile(data) ) return doodad, err @@ -109,6 +109,13 @@ func (d *Doodad) populateFromZipfile(data []byte) error { // Re-inflate data after saving a new zipfile. d.Inflate() + // If we are a legacy doodad and don't have a Size (width x height), + // set it from the chunk size. + if d.Size.IsZero() { + var size = d.ChunkSize() + d.Size = render.NewRect(size, size) + } + return err } diff --git a/pkg/editor_scene.go b/pkg/editor_scene.go index 4a5f145..e5c4510 100644 --- a/pkg/editor_scene.go +++ b/pkg/editor_scene.go @@ -3,7 +3,6 @@ package doodle import ( "errors" "fmt" - "os" "strings" "time" @@ -19,6 +18,7 @@ import ( "git.kirsle.net/SketchyMaze/doodle/pkg/log" "git.kirsle.net/SketchyMaze/doodle/pkg/modal" "git.kirsle.net/SketchyMaze/doodle/pkg/modal/loadscreen" + "git.kirsle.net/SketchyMaze/doodle/pkg/native" "git.kirsle.net/SketchyMaze/doodle/pkg/usercfg" "git.kirsle.net/SketchyMaze/doodle/pkg/userdir" "git.kirsle.net/SketchyMaze/doodle/pkg/windows" @@ -512,7 +512,7 @@ func (s *EditorScene) SaveLevel(filename string) error { m.Title = "Alpha" } if m.Author == "" { - m.Author = os.Getenv("USER") + m.Author = native.DefaultAuthor() } m.Palette = s.UI.Canvas.Palette @@ -595,7 +595,7 @@ func (s *EditorScene) SaveDoodad(filename string) error { d.Title = "Untitled Doodad" } if d.Author == "" { - d.Author = os.Getenv("USER") + d.Author = native.DefaultAuthor() } // TODO: is this copying necessary? diff --git a/pkg/level/types.go b/pkg/level/types.go index cf23529..6e40690 100644 --- a/pkg/level/types.go +++ b/pkg/level/types.go @@ -4,12 +4,12 @@ import ( "archive/zip" "encoding/json" "fmt" - "os" "git.kirsle.net/SketchyMaze/doodle/pkg/balance" "git.kirsle.net/SketchyMaze/doodle/pkg/drawtool" "git.kirsle.net/SketchyMaze/doodle/pkg/enum" "git.kirsle.net/SketchyMaze/doodle/pkg/log" + "git.kirsle.net/SketchyMaze/doodle/pkg/native" "git.kirsle.net/go/render" ) @@ -83,7 +83,7 @@ func New() *Level { Base: Base{ Version: 1, Title: "Untitled", - Author: os.Getenv("USER"), + Author: native.DefaultAuthor(), Files: NewFileSystem(), }, Chunker: NewChunker(balance.ChunkSize), diff --git a/pkg/native/username.go b/pkg/native/username.go new file mode 100644 index 0000000..85d9298 --- /dev/null +++ b/pkg/native/username.go @@ -0,0 +1,29 @@ +package native + +import ( + "os" + + "git.kirsle.net/SketchyMaze/doodle/pkg/license" +) + +var USER string = os.Getenv("USER") + +/* +DefaultAuthor will return the local user's name to be the default Author +for levels and doodads they create. + +If they have registered the game, use the name from their license JWT token. + +Otherwise fall back to their native operating system user. +*/ +func DefaultAuthor() string { + // Are we registered? + if license.IsRegistered() { + if reg, err := license.GetRegistration(); err == nil { + return reg.Name + } + } + + // Return OS username + return os.Getenv("USER") +} diff --git a/pkg/play_scene.go b/pkg/play_scene.go index 9dd8acd..7827fea 100644 --- a/pkg/play_scene.go +++ b/pkg/play_scene.go @@ -348,6 +348,11 @@ func (s *PlayScene) PlaceResizeCanvas() { }) } +// Canvas returns the main level canvas - useful to call from the debug console as `d.Scene.Canvas()` +func (s *PlayScene) Canvas() *uix.Canvas { + return s.drawing +} + // SetPlayerCharacter changes the doodad used for the player, by destroying the // current player character and making it from scratch. func (s *PlayScene) SetPlayerCharacter(filename string) { diff --git a/pkg/uix/actor.go b/pkg/uix/actor.go index 19c9801..e9bfa3c 100644 --- a/pkg/uix/actor.go +++ b/pkg/uix/actor.go @@ -164,7 +164,7 @@ func (a *Actor) SetWet(v bool) { // Size returns the size of the actor, from the underlying doodads.Drawing. func (a *Actor) Size() render.Rect { - return a.Drawing.Size() + return a.Drawing.Doodad.Size } // Velocity returns the actor's current velocity vector. diff --git a/pkg/uix/canvas.go b/pkg/uix/canvas.go index 32dd80e..cee6321 100644 --- a/pkg/uix/canvas.go +++ b/pkg/uix/canvas.go @@ -36,6 +36,11 @@ type Canvas struct { Scrollable bool // Cursor keys will scroll the viewport of this canvas. Zoom int // Zoom level on the canvas. + // Set this if your Canvas is a small fixed size (e.g. in doodad dropper), + // so that doodads will crop their texture (if chunk size larger than your + // Canvas) as to not overflow the canvas bounds. Not needed for Level canvases. + CroppedSize bool + // Toogle for doodad canvases in the Level Editor to show their buttons. ShowDoodadButtons bool doodadButtonFrame ui.Widget // lazy init diff --git a/pkg/uix/canvas_actors.go b/pkg/uix/canvas_actors.go index 3250403..bae6f97 100644 --- a/pkg/uix/canvas_actors.go +++ b/pkg/uix/canvas_actors.go @@ -220,7 +220,7 @@ func (w *Canvas) drawActors(e render.Engine, p render.Point) { // Hitting the left edge. Cap the X coord and shrink the width. delta := p.X - drawAt.X // positive number drawAt.X = p.X - // scrollTo.X -= delta // TODO + scrollTo.X -= delta resizeTo.W -= delta } @@ -232,6 +232,7 @@ func (w *Canvas) drawActors(e render.Engine, p render.Point) { // Hitting the top edge. Cap the Y coord and shrink the height. delta := p.Y - drawAt.Y drawAt.Y = p.Y + scrollTo.Y -= delta resizeTo.H -= delta } diff --git a/pkg/uix/canvas_debug.go b/pkg/uix/canvas_debug.go new file mode 100644 index 0000000..1268164 --- /dev/null +++ b/pkg/uix/canvas_debug.go @@ -0,0 +1,26 @@ +package uix + +import "strings" + +// Some debugging functions for the Canvas reachable via dev console in-game. + +// GetCanvasesByActorName searches a (level) canvas's installed actors and returns any of +// them having this Title or Filename, with filename being more precise. +func (c *Canvas) GetCanvasesByActorName(filename string) []*Canvas { + var ( + byFilename = []*Canvas{} + byTitle = []*Canvas{} + lower = strings.ToLower(filename) + ) + + for _, a := range c.actors { + var doodad = a.Doodad() + if doodad.Filename == filename { + byFilename = append(byFilename, a.Canvas) + } else if strings.ToLower(doodad.Title) == lower { + byTitle = append(byTitle, a.Canvas) + } + } + + return append(byFilename, byTitle...) +} diff --git a/pkg/uix/canvas_present.go b/pkg/uix/canvas_present.go index 8f6ad5d..cc742fa 100644 --- a/pkg/uix/canvas_present.go +++ b/pkg/uix/canvas_present.go @@ -114,11 +114,15 @@ func (w *Canvas) Present(e render.Engine, p render.Point) { // into which it will render, cap the source width and height. // This is especially useful for Doodad buttons because the drawing // is bigger than the button. - if src.W > S.W { - src.W = S.W - } - if src.H > S.H { - src.H = S.H + if w.CroppedSize { + // NOTE: this is a concern mainly for the Doodad Dropper so that + // the doodads won't overflow the button size they appear in. + if src.W > S.W { + src.W = S.W + } + if src.H > S.H { + src.H = S.H + } } var size = int(chunk.Size) @@ -135,62 +139,68 @@ func (w *Canvas) Present(e render.Engine, p render.Point) { // TODO: all this shit is in TrimBox(), make it DRY - // If the destination width will cause it to overflow the widget - // box, trim off the right edge of the destination rect. - // - // Keep in mind we're dealing with chunks here, and a chunk is - // a small part of the image. Example: - // - Canvas is 800x600 (S.W=800 S.H=600) - // - Chunk wants to render at 790,0 width 100,100 or whatever - // dst={790, 0, 100, 100} - // - Chunk box would exceed 800px width (X=790 + W=100 == 890) - // - Find the delta how much it exceeds as negative (800 - 890 == -90) - // - Lower the Source and Dest rects by that delta size so they - // stay proportional and don't scale or anything dumb. - if dst.X+src.W > p.X+S.W+w.BoxThickness(1) { - // NOTE: delta is a negative number, - // so it will subtract from the width. - delta := (p.X + S.W - w.BoxThickness(1)) - (dst.W + dst.X) - src.W += delta - dst.W += delta - } - if dst.Y+src.H > p.Y+S.H+w.BoxThickness(1) { - // NOTE: delta is a negative number - delta := (p.Y + S.H - w.BoxThickness(1)) - (dst.H + dst.Y) - src.H += delta - dst.H += delta - } + // wtf? don't need all this code anymore?? + _ = ParentPosition + /* - // The same for the top left edge, so the drawings don't overlap - // menu bars or left side toolbars. - // - Canvas was placed 80px from the left of the screen. - // Canvas.MoveTo(80, 0) - // - A texture wants to draw at 60, 0 which would cause it to - // overlap 20 pixels into the left toolbar. It needs to be cropped. - // - The delta is: p.X=80 - dst.X=60 == 20 - // - Set destination X to p.X to constrain it there: 20 - // - Subtract the delta from destination W so we don't scale it. - // - Add 20 to X of the source: the left edge of source is not visible - // - // Note: the +w.BoxThickness works around a bug if the Actor Canvas has - // a border on it (e.g. in the Actor/Link Tool mouse-over or debug setting) - if dst.X == ParentPosition.X+w.BoxThickness(1) { - // NOTE: delta is a positive number, - // so it will add to the destination coordinates. - delta := texSizeOrig.W - src.W - dst.X = p.X + w.BoxThickness(1) - src.X += delta - } - if dst.Y == ParentPosition.Y+w.BoxThickness(1) { - delta := texSizeOrig.H - src.H - dst.Y = p.Y + w.BoxThickness(1) - src.Y += delta - } + // If the destination width will cause it to overflow the widget + // box, trim off the right edge of the destination rect. + // + // Keep in mind we're dealing with chunks here, and a chunk is + // a small part of the image. Example: + // - Canvas is 800x600 (S.W=800 S.H=600) + // - Chunk wants to render at 790,0 width 100,100 or whatever + // dst={790, 0, 100, 100} + // - Chunk box would exceed 800px width (X=790 + W=100 == 890) + // - Find the delta how much it exceeds as negative (800 - 890 == -90) + // - Lower the Source and Dest rects by that delta size so they + // stay proportional and don't scale or anything dumb. + if dst.X+src.W > p.X+S.W+w.BoxThickness(1) { + // NOTE: delta is a negative number, + // so it will subtract from the width. + delta := (p.X + S.W - w.BoxThickness(1)) - (dst.W + dst.X) + src.W += delta + dst.W += delta + } + if dst.Y+src.H > p.Y+S.H+w.BoxThickness(1) { + // NOTE: delta is a negative number + delta := (p.Y + S.H - w.BoxThickness(1)) - (dst.H + dst.Y) + src.H += delta + dst.H += delta + } - // Trim the destination width so it doesn't overlap the Canvas border. - if dst.W >= S.W-w.BoxThickness(1) { - dst.W = S.W - w.BoxThickness(1) - } + // The same for the top left edge, so the drawings don't overlap + // menu bars or left side toolbars. + // - Canvas was placed 80px from the left of the screen. + // Canvas.MoveTo(80, 0) + // - A texture wants to draw at 60, 0 which would cause it to + // overlap 20 pixels into the left toolbar. It needs to be cropped. + // - The delta is: p.X=80 - dst.X=60 == 20 + // - Set destination X to p.X to constrain it there: 20 + // - Subtract the delta from destination W so we don't scale it. + // - Add 20 to X of the source: the left edge of source is not visible + // + // Note: the +w.BoxThickness works around a bug if the Actor Canvas has + // a border on it (e.g. in the Actor/Link Tool mouse-over or debug setting) + if dst.X == ParentPosition.X+w.BoxThickness(1) { + // NOTE: delta is a positive number, + // so it will add to the destination coordinates. + delta := texSizeOrig.W - src.W + dst.X = p.X + w.BoxThickness(1) + src.X += delta + } + if dst.Y == ParentPosition.Y+w.BoxThickness(1) { + delta := texSizeOrig.H - src.H + dst.Y = p.Y + w.BoxThickness(1) + src.Y += delta + } + + // Trim the destination width so it doesn't overlap the Canvas border. + if dst.W >= S.W-w.BoxThickness(1) { + dst.W = S.W - w.BoxThickness(1) + } + + */ // When zooming OUT, make sure the source rect is at least the // full size of the chunk texture; otherwise the ZoomMultiplies diff --git a/pkg/windows/doodad_dropper.go b/pkg/windows/doodad_dropper.go index 5c3baea..74049ed 100644 --- a/pkg/windows/doodad_dropper.go +++ b/pkg/windows/doodad_dropper.go @@ -262,6 +262,7 @@ func makeDoodadTab(config DoodadDropper, frame *ui.Frame, size render.Rect, cate canvases = append(canvases, can) btn := ui.NewButton(doodad.Title, can) + can.CroppedSize = true btn.Resize(render.NewRect( buttonSize-2, // TODO: without the -2 the button border buttonSize-2, // rests on top of the window border -- 2.30.2 From 1e37509421ef88c8f76d46b28400606c66d7221e Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Fri, 17 Feb 2023 21:49:19 -0800 Subject: [PATCH 3/3] Update README and fix perfect run icon display --- Changes.md | 26 ++++++++++++++++++++++++++ pkg/balance/cheats.go | 2 +- pkg/cheats.go | 5 +++++ pkg/play_scene.go | 12 ++++++++++++ 4 files changed, 44 insertions(+), 1 deletion(-) diff --git a/Changes.md b/Changes.md index f8f9f05..7cb3617 100644 --- a/Changes.md +++ b/Changes.md @@ -2,6 +2,22 @@ ## v0.13.2 (TBD) +Some new features: + +* **Doodads can be non-square!** You can now set a rectangular canvas size + for your doodads. Many of the game's built-in doodads that used to be + off-center before (doors, creatures) because their sprites were not squares + now have correct rectangular shapes. +* A **Cheats Menu** has been added which enables you to enter many of the + game's cheat codes by clicking on buttons instead. Enable it through the + "Experimental" tab of the Settings, and the cheats menu can be opened from + the Help menu bar during gameplay. + +Other miscellaneous changes: + +* The default Author name on your new drawings will prefer to use your + license registration name (if the game is registered) before falling back + on your operating system's $USER name like before. * In the level editor, you can now use the Pan Tool to access the actor properties of doodads you've dropped into your level. Similar to the Actor Tool, when you mouse-over an actor on your level it will highlight @@ -12,6 +28,16 @@ on your level as might happen with the Actor Tool! * Start distributing AppImage releases for GNU/Linux (64-bit and 32-bit) +Some technical changes: + +* Chunk sizes in levels/doodads is now a uint8 type, meaning the maximum + chunk size is 255x255 pixels. The game's default has always been 128x128 + but now there is a limit. This takes a step towards optimizing the game's + file formats: large world coordinates (64-bit) are mapped to a chunk + coordinate, and if each chunk only needs to worry about the 255 pixels + in its territory, space can be saved in memory without chunks needing to + theoretically support 64-bit sizes of pixels! + ## v0.13.1 (Oct 10 2022) This release brings a handful of minor new features to the game. diff --git a/pkg/balance/cheats.go b/pkg/balance/cheats.go index fb12527..d7b3344 100644 --- a/pkg/balance/cheats.go +++ b/pkg/balance/cheats.go @@ -43,7 +43,7 @@ var ( // Actor replacement cheats var CheatActors = map[string]string{ - "pinocchio": "boy", + "pinocchio": PlayerCharacterDoodad, "the cell": "azu-blu", "super azulian": "azu-red", "hyper azulian": "azu-white", diff --git a/pkg/cheats.go b/pkg/cheats.go index 4e0bd69..1e4699b 100644 --- a/pkg/cheats.go +++ b/pkg/cheats.go @@ -36,10 +36,15 @@ func (d *Doodle) MakeCheatsWindow(supervisor *ui.Supervisor) *ui.Window { return d.Scene.Name() }, RunCommand: func(command string) { + // If we are in Play Mode, every command out of here is cheating. + if playScene, ok := d.Scene.(*PlayScene); ok { + playScene.SetCheated() + } d.shell.Execute(command) }, OnSetPlayerCharacter: func(doodad string) { if scene, ok := d.Scene.(*PlayScene); ok { + scene.SetCheated() scene.SetPlayerCharacter(doodad) } else { shmem.FlashError("This only works during Play Mode.") diff --git a/pkg/play_scene.go b/pkg/play_scene.go index 7827fea..03fe151 100644 --- a/pkg/play_scene.go +++ b/pkg/play_scene.go @@ -643,6 +643,11 @@ func (s *PlayScene) GetCheated() bool { return s.cheated } +// GetPerfect gives read-only access to the perfectRun flag. +func (s *PlayScene) GetPerfect() bool { + return s.perfectRun +} + // ShowEndLevelModal centralizes the EndLevel modal config. // This is the common handler function between easy methods such as // BeatLevel, FailLevel, and DieByFire. @@ -833,6 +838,13 @@ func (s *PlayScene) Draw(d *Doodle) error { } } + // Bug: sometimes (especially after cheating) if you restart a level + // properly, cheated=false perfectRun=true but the perfectRunIcon + // would not be showing. + if !s.cheated && s.perfectRun && s.timerPerfectImage.Hidden() { + s.timerPerfectImage.Show() + } + // Draw the UI screen and any widgets that attached to it. s.screen.Compute(d.Engine) s.screen.Present(d.Engine, render.Origin) -- 2.30.2