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/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..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,7 +105,8 @@ 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 + width int // dimensions of the incoming image + height int images []image.Image ) @@ -129,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 { - chunkSize = imageSize.X - } else { - chunkSize = 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) } @@ -150,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(chunkSize) + 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 @@ -188,11 +188,14 @@ 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" } - 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 @@ -288,7 +291,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/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/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/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/doodads/doodad.go b/pkg/doodads/doodad.go index 0a39dc5..4803432 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,47 @@ type Layer struct { Chunker *level.Chunker `json:"chunks"` } -// New creates a new Doodad. -func New(size int) *Doodad { +/* +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 int + chunkSize uint8 + width int + height int + ) + + switch len(dimensions) { + case 1: + width, height = dimensions[0], dimensions[0] + case 2: + width, height = dimensions[0], dimensions[1] + } + + // 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 = balance.DoodadSize + size = int(balance.ChunkSize) + } + + // 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 +79,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 +98,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 +146,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/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/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..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" @@ -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) @@ -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 @@ -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 } @@ -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/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/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/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/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_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..03fe151 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) { @@ -471,10 +476,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 @@ -638,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. @@ -828,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) diff --git a/pkg/uix/actor.go b/pkg/uix/actor.go index c6c0f85..e9bfa3c 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), @@ -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 d162847..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 @@ -132,15 +137,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 +379,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 +388,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..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 / 2 // 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 4ab260e..cc742fa 100644 --- a/pkg/uix/canvas_present.go +++ b/pkg/uix/canvas_present.go @@ -114,16 +114,21 @@ 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) 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 @@ -134,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/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..74049ed 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) @@ -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