Merge pull request 'Chunker size to uint8 and Rectangular Doodads' (#84) from file-format-optimization into master

Reviewed-on: #84
This commit is contained in:
Noah 2023-02-18 05:49:48 +00:00
commit 0d8933513e
31 changed files with 475 additions and 282 deletions

View File

@ -2,6 +2,22 @@
## v0.13.2 (TBD) ## 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 * 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 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 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! on your level as might happen with the Actor Tool!
* Start distributing AppImage releases for GNU/Linux (64-bit and 32-bit) * 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) ## v0.13.1 (Oct 10 2022)
This release brings a handful of minor new features to the game. This release brings a handful of minor new features to the game.

View File

@ -70,23 +70,22 @@ def main(fast=False):
def install_deps(fast): def install_deps(fast):
"""Install system dependencies.""" """Install system dependencies."""
if fast: fast = " -y" if fast else ""
fast = " -y"
if shell("which rpm") == 0 and shell("which dnf") == 0: if shell("which rpm") == 0 and shell("which dnf") == 0:
# Fedora-like. # Fedora-like.
if shell("rpm -q {}".format(' '.join(dep_fedora))) != 0: 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: elif shell("which brew") == 0:
# MacOS, as Catalina has an apt command now?? # 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: elif shell("which apt") == 0:
# Debian-like. # Debian-like.
if shell("dpkg-query -l {}".format(' '.join(dep_debian))) != 0: 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: elif shell("which pacman") == 0:
# Arch-like. # Arch-like.
must_shell("sudo pacman -S{} {}{}".format(fast, ' '.join(dep_arch))) must_shell("sudo pacman -S{} {}".format(fast, ' '.join(dep_arch)))
else: else:
print("Warning: didn't detect your package manager to install SDL2 and other dependencies") 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__": if __name__ == "__main__":
parser = argparse.ArgumentParser("doodle bootstrap") parser = argparse.ArgumentParser("doodle bootstrap")
parser.add_argument("fast", "f", parser.add_argument("--fast", "-f",
action="store_true", action="store_true",
help="Run the script non-interactively (yes to your package manager, git clone over https)", help="Run the script non-interactively (yes to your package manager, git clone over https)",
) )

View File

@ -15,6 +15,7 @@ import (
"git.kirsle.net/SketchyMaze/doodle/pkg/doodads" "git.kirsle.net/SketchyMaze/doodle/pkg/doodads"
"git.kirsle.net/SketchyMaze/doodle/pkg/level" "git.kirsle.net/SketchyMaze/doodle/pkg/level"
"git.kirsle.net/SketchyMaze/doodle/pkg/log" "git.kirsle.net/SketchyMaze/doodle/pkg/log"
"git.kirsle.net/SketchyMaze/doodle/pkg/native"
"git.kirsle.net/go/render" "git.kirsle.net/go/render"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"golang.org/x/image/bmp" "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. // Read the source images. Ensure they all have the same boundaries.
var ( var (
imageBounds image.Point 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 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. // Validate all images are the same size.
if i == 0 { if i == 0 {
imageBounds = imageSize imageBounds = imageSize
if imageSize.X > imageSize.Y { width = imageSize.X
chunkSize = imageSize.X height = imageSize.Y
} else {
chunkSize = imageSize.Y
}
} else if imageSize != imageBounds { } else if imageSize != imageBounds {
return cli.Exit("your source images are not all the same dimensions", 1) 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. // Generate the output drawing file.
switch strings.ToLower(filepath.Ext(outputFile)) { switch strings.ToLower(filepath.Ext(outputFile)) {
case extDoodad: case extDoodad:
log.Info("Output is a Doodad file (chunk size %d): %s", chunkSize, outputFile) doodad := doodads.New(width, height)
doodad := doodads.New(chunkSize)
doodad.GameVersion = branding.Version doodad.GameVersion = branding.Version
doodad.Title = c.String("title") doodad.Title = c.String("title")
if doodad.Title == "" { if doodad.Title == "" {
doodad.Title = "Converted Doodad" doodad.Title = "Converted Doodad"
} }
doodad.Author = os.Getenv("USER") doodad.Author = native.DefaultAuthor()
// Write the first layer and gather its palette. // Write the first layer and gather its palette.
log.Info("Converting first layer to drawing and getting the 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) palette, layer0 := imageToChunker(images[0], chroma, nil, chunkSize)
doodad.Palette = palette doodad.Palette = palette
doodad.Layers[0].Chunker = layer0 doodad.Layers[0].Chunker = layer0
@ -188,11 +188,14 @@ func imageToDrawing(c *cli.Context, chroma render.Color, inputFiles []string, ou
lvl := level.New() lvl := level.New()
lvl.GameVersion = branding.Version lvl.GameVersion = branding.Version
lvl.MaxWidth = int64(width)
lvl.MaxHeight = int64(height)
lvl.PageType = level.Bounded
lvl.Title = c.String("title") lvl.Title = c.String("title")
if lvl.Title == "" { if lvl.Title == "" {
lvl.Title = "Converted Level" lvl.Title = "Converted Level"
} }
lvl.Author = os.Getenv("USER") lvl.Author = native.DefaultAuthor()
palette, chunker := imageToChunker(images[0], chroma, nil, lvl.Chunker.Size) palette, chunker := imageToChunker(images[0], chroma, nil, lvl.Chunker.Size)
lvl.Palette = palette lvl.Palette = palette
lvl.Chunker = chunker lvl.Chunker = chunker
@ -288,7 +291,7 @@ func drawingToImage(c *cli.Context, chroma render.Color, inputFiles []string, ou
// //
// img: input image like a PNG // img: input image like a PNG
// chroma: transparent color // 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 ( var (
chunker = level.NewChunker(chunkSize) chunker = level.NewChunker(chunkSize)
bounds = img.Bounds() bounds = img.Bounds()

View File

@ -4,6 +4,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"git.kirsle.net/SketchyMaze/doodle/pkg/balance"
"git.kirsle.net/SketchyMaze/doodle/pkg/level" "git.kirsle.net/SketchyMaze/doodle/pkg/level"
"git.kirsle.net/SketchyMaze/doodle/pkg/log" "git.kirsle.net/SketchyMaze/doodle/pkg/log"
"git.kirsle.net/go/render" "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 // Handles the deep operation of re-copying the old level into a new level
// at the new chunk size. // at the new chunk size.
func rechunkLevel(c *cli.Context, filename string, lvl *level.Level) error { 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("Resizing the level's chunk size.")
log.Info("Current chunk size: %d", lvl.Chunker.Size) log.Info("Current chunk size: %d", lvl.Chunker.Size)
log.Info("Target chunk size: %d", chunkSize) log.Info("Target chunk size: %d", chunkSize)

View File

@ -205,6 +205,7 @@ func showDoodad(c *cli.Context, filename string) error {
fmt.Printf(" Game version: %s\n", dd.GameVersion) fmt.Printf(" Game version: %s\n", dd.GameVersion)
fmt.Printf(" Doodad title: %s\n", dd.Title) fmt.Printf(" Doodad title: %s\n", dd.Title)
fmt.Printf(" Author: %s\n", dd.Author) fmt.Printf(" Author: %s\n", dd.Author)
fmt.Printf(" Dimensions: %s\n", dd.Size)
fmt.Printf(" Hitbox: %s\n", dd.Hitbox) fmt.Printf(" Hitbox: %s\n", dd.Hitbox)
fmt.Printf(" Locked: %+v\n", dd.Locked) fmt.Printf(" Locked: %+v\n", dd.Locked)
fmt.Printf(" Hidden: %+v\n", dd.Hidden) fmt.Printf(" Hidden: %+v\n", dd.Hidden)
@ -256,9 +257,12 @@ func showPalette(pal *level.Palette) {
} }
func showChunker(c *cli.Context, ch *level.Chunker) { func showChunker(c *cli.Context, ch *level.Chunker) {
var worldSize = ch.WorldSize() var (
var width = worldSize.W - worldSize.X worldSize = ch.WorldSize()
var height = worldSize.H - worldSize.Y chunkSize = int(ch.Size)
width = worldSize.W - worldSize.X
height = worldSize.H - worldSize.Y
)
fmt.Println("Chunks:") fmt.Println("Chunks:")
fmt.Printf(" Pixels Per Chunk: %d^2\n", ch.Size) fmt.Printf(" Pixels Per Chunk: %d^2\n", ch.Size)
fmt.Printf(" Number Generated: %d\n", len(ch.Chunks)) 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(" - Coord: %s\n", point)
fmt.Printf(" Type: %s\n", chunkTypeToName(chunk.Type)) fmt.Printf(" Type: %s\n", chunkTypeToName(chunk.Type))
fmt.Printf(" Range: (%d,%d) ... (%d,%d)\n", fmt.Printf(" Range: (%d,%d) ... (%d,%d)\n",
int(point.X)*ch.Size, int(point.X)*chunkSize,
int(point.Y)*ch.Size, int(point.Y)*chunkSize,
(int(point.X)*ch.Size)+ch.Size, (int(point.X)*chunkSize)+chunkSize,
(int(point.Y)*ch.Size)+ch.Size, (int(point.Y)*chunkSize)+chunkSize,
) )
} }
} else { } else {

View File

@ -43,7 +43,7 @@ var (
// Actor replacement cheats // Actor replacement cheats
var CheatActors = map[string]string{ var CheatActors = map[string]string{
"pinocchio": "boy", "pinocchio": PlayerCharacterDoodad,
"the cell": "azu-blu", "the cell": "azu-blu",
"super azulian": "azu-red", "super azulian": "azu-red",
"hyper azulian": "azu-white", "hyper azulian": "azu-white",

View File

@ -55,7 +55,7 @@ var (
FollowPlayerFirstTicks uint64 = 60 FollowPlayerFirstTicks uint64 = 60
// Default chunk size for canvases. // Default chunk size for canvases.
ChunkSize = 128 ChunkSize uint8 = 128
// Default size for a new Doodad. // Default size for a new Doodad.
DoodadSize = 100 DoodadSize = 100

View File

@ -36,10 +36,15 @@ func (d *Doodle) MakeCheatsWindow(supervisor *ui.Supervisor) *ui.Window {
return d.Scene.Name() return d.Scene.Name()
}, },
RunCommand: func(command string) { 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) d.shell.Execute(command)
}, },
OnSetPlayerCharacter: func(doodad string) { OnSetPlayerCharacter: func(doodad string) {
if scene, ok := d.Scene.(*PlayScene); ok { if scene, ok := d.Scene.(*PlayScene); ok {
scene.SetCheated()
scene.SetPlayerCharacter(doodad) scene.SetPlayerCharacter(doodad)
} else { } else {
shmem.FlashError("This only works during Play Mode.") shmem.FlashError("This only works during Play Mode.")

View File

@ -14,6 +14,7 @@ type Doodad struct {
Filename string `json:"-"` // used internally, not saved in json Filename string `json:"-"` // used internally, not saved in json
Hidden bool `json:"hidden,omitempty"` Hidden bool `json:"hidden,omitempty"`
Palette *level.Palette `json:"palette"` Palette *level.Palette `json:"palette"`
Size render.Rect `json:"size"` // doodad dimensions
Script string `json:"script"` Script string `json:"script"`
Hitbox render.Rect `json:"hitbox"` Hitbox render.Rect `json:"hitbox"`
Layers []Layer `json:"layers"` Layers []Layer `json:"layers"`
@ -30,10 +31,47 @@ type Layer struct {
Chunker *level.Chunker `json:"chunks"` 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 { 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{ return &Doodad{
@ -41,11 +79,12 @@ func New(size int) *Doodad {
Version: 1, Version: 1,
}, },
Palette: level.DefaultPalette(), Palette: level.DefaultPalette(),
Hitbox: render.NewRect(size, size), Hitbox: render.NewRect(width, height),
Size: render.NewRect(width, height),
Layers: []Layer{ Layers: []Layer{
{ {
Name: "main", Name: "main",
Chunker: level.NewChunker(size), Chunker: level.NewChunker(chunkSize),
}, },
}, },
Tags: map[string]string{}, Tags: map[string]string{},
@ -59,7 +98,7 @@ func New(size int) *Doodad {
// is optional - pass nil and a new blank chunker is created. // is optional - pass nil and a new blank chunker is created.
func (d *Doodad) AddLayer(name string, chunker *level.Chunker) Layer { func (d *Doodad) AddLayer(name string, chunker *level.Chunker) Layer {
if chunker == nil { if chunker == nil {
chunker = level.NewChunker(d.ChunkSize()) chunker = level.NewChunker(d.ChunkSize8())
} }
layer := Layer{ layer := Layer{
@ -107,12 +146,17 @@ func (d *Doodad) Tag(name string) string {
// ChunkSize returns the chunk size of the Doodad's first layer. // ChunkSize returns the chunk size of the Doodad's first layer.
func (d *Doodad) ChunkSize() int { 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 return d.Layers[0].Chunker.Size
} }
// Rect returns a rect of the ChunkSize for scaling a Canvas widget. // Rect returns a rect of the ChunkSize for scaling a Canvas widget.
func (d *Doodad) Rect() render.Rect { func (d *Doodad) Rect() render.Rect {
var size = d.ChunkSize() var size = int(d.ChunkSize())
return render.Rect{ return render.Rect{
W: size, W: size,
H: size, H: size,

View File

@ -6,8 +6,8 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"git.kirsle.net/SketchyMaze/doodle/pkg/balance"
"git.kirsle.net/SketchyMaze/doodle/pkg/log" "git.kirsle.net/SketchyMaze/doodle/pkg/log"
"git.kirsle.net/go/render"
) )
// ToZipfile serializes the doodad into zipfile format. // ToZipfile serializes the doodad into zipfile format.
@ -63,7 +63,7 @@ func (d *Doodad) ToZipfile() ([]byte, error) {
// FromZipfile reads a doodad from zipfile format. // FromZipfile reads a doodad from zipfile format.
func FromZipfile(data []byte) (*Doodad, error) { func FromZipfile(data []byte) (*Doodad, error) {
var ( var (
doodad = New(balance.DoodadSize) doodad = New(0)
err = doodad.populateFromZipfile(data) err = doodad.populateFromZipfile(data)
) )
return doodad, err return doodad, err
@ -109,6 +109,13 @@ func (d *Doodad) populateFromZipfile(data []byte) error {
// Re-inflate data after saving a new zipfile. // Re-inflate data after saving a new zipfile.
d.Inflate() 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 return err
} }

View File

@ -3,7 +3,6 @@ package doodle
import ( import (
"fmt" "fmt"
"path/filepath" "path/filepath"
"strconv"
"strings" "strings"
"time" "time"
@ -291,32 +290,16 @@ func (d *Doodle) NewMap() {
// NewDoodad loads a new Doodad in Edit Mode. // 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. // If size is zero, it prompts the user to select a size or accept the default size.
func (d *Doodle) NewDoodad(size int) { func (d *Doodle) NewDoodad(width, height int) {
if size == 0 { if width+height == 0 {
d.Prompt(fmt.Sprintf("Doodad size or %d>", balance.DoodadSize), func(answer string) { width = balance.DoodadSize
size := balance.DoodadSize height = width
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
} }
log.Info("Starting a new doodad") log.Info("Starting a new doodad")
scene := &EditorScene{ scene := &EditorScene{
DrawingType: enum.DoodadDrawing, DrawingType: enum.DoodadDrawing,
DoodadSize: size, DoodadSize: render.NewRect(width, height),
} }
d.Goto(scene) d.Goto(scene)
} }

View File

@ -3,7 +3,6 @@ package doodle
import ( import (
"errors" "errors"
"fmt" "fmt"
"os"
"strings" "strings"
"time" "time"
@ -19,6 +18,7 @@ import (
"git.kirsle.net/SketchyMaze/doodle/pkg/log" "git.kirsle.net/SketchyMaze/doodle/pkg/log"
"git.kirsle.net/SketchyMaze/doodle/pkg/modal" "git.kirsle.net/SketchyMaze/doodle/pkg/modal"
"git.kirsle.net/SketchyMaze/doodle/pkg/modal/loadscreen" "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/usercfg"
"git.kirsle.net/SketchyMaze/doodle/pkg/userdir" "git.kirsle.net/SketchyMaze/doodle/pkg/userdir"
"git.kirsle.net/SketchyMaze/doodle/pkg/windows" "git.kirsle.net/SketchyMaze/doodle/pkg/windows"
@ -32,7 +32,7 @@ type EditorScene struct {
DrawingType enum.DrawingType DrawingType enum.DrawingType
OpenFile bool OpenFile bool
Filename string Filename string
DoodadSize int DoodadSize render.Rect
RememberScrollPosition render.Point // Play mode remembers it for us RememberScrollPosition render.Point // Play mode remembers it for us
UI *EditorUI UI *EditorUI
@ -195,7 +195,7 @@ func (s *EditorScene) setupAsync(d *Doodle) error {
// No Doodad? // No Doodad?
if s.Doodad == nil { if s.Doodad == nil {
log.Debug("EditorScene.Setup: initializing a new Doodad") 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) 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. // 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.ScrollTo(render.Origin)
s.UI.Canvas.Scrollable = false s.UI.Canvas.Scrollable = false
s.UI.Workspace.Compute(d.Engine) s.UI.Workspace.Compute(d.Engine)
@ -512,7 +512,7 @@ func (s *EditorScene) SaveLevel(filename string) error {
m.Title = "Alpha" m.Title = "Alpha"
} }
if m.Author == "" { if m.Author == "" {
m.Author = os.Getenv("USER") m.Author = native.DefaultAuthor()
} }
m.Palette = s.UI.Canvas.Palette m.Palette = s.UI.Canvas.Palette
@ -574,7 +574,7 @@ func (s *EditorScene) LoadDoodad(filename string) error {
s.DrawingType = enum.DoodadDrawing s.DrawingType = enum.DoodadDrawing
s.Doodad = doodad s.Doodad = doodad
s.DoodadSize = doodad.Layers[0].Chunker.Size s.DoodadSize = doodad.Size
s.UI.Canvas.LoadDoodad(s.Doodad) s.UI.Canvas.LoadDoodad(s.Doodad)
return nil return nil
} }
@ -595,7 +595,7 @@ func (s *EditorScene) SaveDoodad(filename string) error {
d.Title = "Untitled Doodad" d.Title = "Untitled Doodad"
} }
if d.Author == "" { if d.Author == "" {
d.Author = os.Getenv("USER") d.Author = native.DefaultAuthor()
} }
// TODO: is this copying necessary? // TODO: is this copying necessary?

View File

@ -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 and scale this doodad according to the zoom level.
size := doodad.Rect() size := doodad.Size
size.W = u.Canvas.ZoomMultiply(size.W) size.W = u.Canvas.ZoomMultiply(size.W)
size.H = u.Canvas.ZoomMultiply(size.H) size.H = u.Canvas.ZoomMultiply(size.H)

View File

@ -52,12 +52,7 @@ func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.MenuBar {
// File menu // File menu
fileMenu := menu.AddMenu("File") fileMenu := menu.AddMenu("File")
fileMenu.AddItemAccel("New level", "Ctrl-N", u.Scene.MenuNewLevel) fileMenu.AddItemAccel("New level", "Ctrl-N", u.Scene.MenuNewLevel)
fileMenu.AddItem("New doodad", func() { fileMenu.AddItem("New doodad", u.Scene.MenuNewDoodad)
u.Scene.ConfirmUnload(func() {
// New doodad size with prompt.
d.NewDoodad(0)
})
})
fileMenu.AddItemAccel("Save", "Ctrl-S", u.Scene.MenuSave(false)) fileMenu.AddItemAccel("Save", "Ctrl-S", u.Scene.MenuSave(false))
fileMenu.AddItemAccel("Save as...", "Shift-Ctrl-S", func() { fileMenu.AddItemAccel("Save as...", "Shift-Ctrl-S", func() {
d.Prompt("Save as filename>", func(answer string) { 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 // File->Open, or Ctrl-O
func (s *EditorScene) MenuOpen() { func (s *EditorScene) MenuOpen() {
s.ConfirmUnload(func() { s.ConfirmUnload(func() {

View File

@ -83,11 +83,11 @@ func (u *EditorUI) setupPaletteFrame(window *ui.Window) *ui.Frame {
if u.Canvas != nil && u.Canvas.Palette != nil { if u.Canvas != nil && u.Canvas.Palette != nil {
for i, swatch := range u.Canvas.Palette.Swatches { for i, swatch := range u.Canvas.Palette.Swatches {
swatch := swatch 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) // Drawing buttons in two-column mode? (default right-side palette layout)
if twoColumn { if twoColumn {
width = buttonSize / 2 width /= 2
if row == nil || i%2 == 0 { if row == nil || i%2 == 0 {
row = ui.NewFrame(fmt.Sprintf("Swatch(%s) Button Frame", swatch.Name)) row = ui.NewFrame(fmt.Sprintf("Swatch(%s) Button Frame", swatch.Name))
frame.Pack(row, packConfig) frame.Pack(row, packConfig)
@ -101,7 +101,8 @@ func (u *EditorUI) setupPaletteFrame(window *ui.Window) *ui.Frame {
var ( var (
colorbox = uix.NewCanvas(width, false) colorbox = uix.NewCanvas(width, false)
chunker = level.NewChunker(width) chunker = level.NewChunker(width)
size = render.NewRect(width, width) iw = int(width)
size = render.NewRect(iw, iw)
) )
chunker.SetRect(size, swatch) chunker.SetRect(size, swatch)
colorbox.Resize(size) colorbox.Resize(size)

View File

@ -27,7 +27,7 @@ type Chunk struct {
// Values told to it from higher up, not stored in JSON. // Values told to it from higher up, not stored in JSON.
Point render.Point Point render.Point
Size int Size uint8
// Texture cache properties so we don't redraw pixel-by-pixel every frame. // Texture cache properties so we don't redraw pixel-by-pixel every frame.
uuid uuid.UUID 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 // want a cached bitmap image that only generates itself once, and
// again when marked dirty. // again when marked dirty.
func (c *Chunk) ToBitmap(mask render.Color) image.Image { func (c *Chunk) ToBitmap(mask render.Color) image.Image {
canvas := c.SizePositive() var (
imgSize := image.Rectangle{ size = int(c.Size)
Min: image.Point{}, canvas = c.SizePositive()
Max: image.Point{ imgSize = image.Rectangle{
X: c.Size, Min: image.Point{},
Y: c.Size, Max: image.Point{
}, X: size,
} Y: size,
},
}
)
if imgSize.Max.X == 0 { if imgSize.Max.X == 0 {
imgSize.Max.X = int(canvas.W) 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 // Pixel coordinate offset to map the Chunk World Position to the
// smaller image boundaries. // smaller image boundaries.
pointOffset := render.Point{ pointOffset := render.Point{
X: c.Point.X * c.Size, X: c.Point.X * size,
Y: c.Point.Y * c.Size, Y: c.Point.Y * size,
} }
// Blot all the pixels onto it. // Blot all the pixels onto it.

View File

@ -20,8 +20,8 @@ type Chunker struct {
// Layer is optional for the caller, levels use only 0 and // Layer is optional for the caller, levels use only 0 and
// doodads use them for frames. When chunks are exported to // doodads use them for frames. When chunks are exported to
// zipfile the Layer keeps them from overlapping. // zipfile the Layer keeps them from overlapping.
Layer int `json:"-"` // internal use only Layer int `json:"-"` // internal use only
Size int `json:"size"` Size uint8 `json:"size"`
// A Zipfile reference for new-style levels and doodads which // A Zipfile reference for new-style levels and doodads which
// keep their chunks in external parts of a zip file. // 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. // NewChunker creates a new chunk manager with a given chunk size.
func NewChunker(size int) *Chunker { func NewChunker(size uint8) *Chunker {
return &Chunker{ return &Chunker{
Size: size, Size: size,
Chunks: ChunkMap{}, Chunks: ChunkMap{},
@ -186,10 +186,13 @@ func (c *Chunker) IterCachedChunks() <-chan *Chunk {
func (c *Chunker) IterViewportChunks(viewport render.Rect) <-chan render.Point { func (c *Chunker) IterViewportChunks(viewport render.Rect) <-chan render.Point {
pipe := make(chan render.Point) pipe := make(chan render.Point)
go func() { 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 x := viewport.X; x < viewport.W; x += (size / 4) {
for y := viewport.Y; y < viewport.H; y += (c.Size / 4) { for y := viewport.Y; y < viewport.H; y += (size / 4) {
// Constrain this chunksize step to a point within the bounds // Constrain this chunksize step to a point within the bounds
// of the viewport. This can yield partial chunks on the edges // 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 // manage: the lowest pixels from the lowest chunks to the highest pixels of
// the highest chunks. // the highest chunks.
func (c *Chunker) WorldSize() render.Rect { func (c *Chunker) WorldSize() render.Rect {
chunkLowest, chunkHighest := c.Bounds() var (
size = int(c.Size)
chunkLowest, chunkHighest = c.Bounds()
)
return render.Rect{ return render.Rect{
X: chunkLowest.X * c.Size, X: chunkLowest.X * size,
Y: chunkLowest.Y * c.Size, Y: chunkLowest.Y * size,
W: (chunkHighest.X * c.Size) + (c.Size - 1), W: (chunkHighest.X * size) + (size - 1),
H: (chunkHighest.Y * c.Size) + (c.Size - 1), H: (chunkHighest.Y * size) + (size - 1),
} }
} }

View File

@ -44,7 +44,7 @@ func GiantScreenshot(lvl *level.Level) (image.Image, error) {
// How big will our image be? // How big will our image be?
var ( var (
size = lvl.Chunker.WorldSizePositive() size = lvl.Chunker.WorldSizePositive()
chunkSize = lvl.Chunker.Size chunkSize = int(lvl.Chunker.Size)
chunkLow, chunkHigh = lvl.Chunker.Bounds() chunkLow, chunkHigh = lvl.Chunker.Bounds()
worldSize = render.Rect{ worldSize = render.Rect{
X: chunkLow.X, X: chunkLow.X,

View File

@ -4,12 +4,12 @@ import (
"archive/zip" "archive/zip"
"encoding/json" "encoding/json"
"fmt" "fmt"
"os"
"git.kirsle.net/SketchyMaze/doodle/pkg/balance" "git.kirsle.net/SketchyMaze/doodle/pkg/balance"
"git.kirsle.net/SketchyMaze/doodle/pkg/drawtool" "git.kirsle.net/SketchyMaze/doodle/pkg/drawtool"
"git.kirsle.net/SketchyMaze/doodle/pkg/enum" "git.kirsle.net/SketchyMaze/doodle/pkg/enum"
"git.kirsle.net/SketchyMaze/doodle/pkg/log" "git.kirsle.net/SketchyMaze/doodle/pkg/log"
"git.kirsle.net/SketchyMaze/doodle/pkg/native"
"git.kirsle.net/go/render" "git.kirsle.net/go/render"
) )
@ -83,7 +83,7 @@ func New() *Level {
Base: Base{ Base: Base{
Version: 1, Version: 1,
Title: "Untitled", Title: "Untitled",
Author: os.Getenv("USER"), Author: native.DefaultAuthor(),
Files: NewFileSystem(), Files: NewFileSystem(),
}, },
Chunker: NewChunker(balance.ChunkSize), Chunker: NewChunker(balance.ChunkSize),

View File

@ -24,6 +24,7 @@ on the MainScene or elsewhere as wanted.
type MenuScene struct { type MenuScene struct {
// Configuration. // Configuration.
StartupMenu string StartupMenu string
NewDoodad bool
Supervisor *ui.Supervisor Supervisor *ui.Supervisor
@ -60,6 +61,17 @@ func (d *Doodle) GotoNewMenu() {
d.Goto(scene) 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. // GotoLoadMenu loads the MenuScene and shows the "Load" window.
func (d *Doodle) GotoLoadMenu() { func (d *Doodle) GotoLoadMenu() {
log.Info("Loading the MenuScene to the Load window for Edit Mode") 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{ window := windows.NewAddEditLevel(windows.AddEditLevel{
Supervisor: s.Supervisor, Supervisor: s.Supervisor,
Engine: d.Engine, Engine: d.Engine,
NewDoodad: s.NewDoodad,
OnChangePageTypeAndWallpaper: func(pageType level.PageType, wallpaper string) { OnChangePageTypeAndWallpaper: func(pageType level.PageType, wallpaper string) {
log.Info("OnChangePageTypeAndWallpaper called: %+v, %+v", pageType, wallpaper) log.Info("OnChangePageTypeAndWallpaper called: %+v, %+v", pageType, wallpaper)
s.canvas.Destroy() // clean up old textures s.canvas.Destroy() // clean up old textures
@ -163,8 +176,8 @@ func (s *MenuScene) setupNewWindow(d *Doodle) error {
Level: lvl, Level: lvl,
}) })
}, },
OnCreateNewDoodad: func(size int) { OnCreateNewDoodad: func(width, height int) {
d.NewDoodad(size) d.NewDoodad(width, height)
}, },
OnCancel: func() { OnCancel: func() {
d.Goto(&MainScene{}) d.Goto(&MainScene{})

29
pkg/native/username.go Normal file
View File

@ -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")
}

View File

@ -69,7 +69,7 @@ func (s *PlayScene) computeInventory() {
continue continue
} }
canvas := uix.NewCanvas(doodad.ChunkSize(), false) canvas := uix.NewCanvas(doodad.ChunkSize8(), false)
canvas.SetBackground(render.RGBA(1, 0, 0, 0)) canvas.SetBackground(render.RGBA(1, 0, 0, 0))
canvas.LoadDoodad(doodad) canvas.LoadDoodad(doodad)
canvas.Resize(render.NewRect( canvas.Resize(render.NewRect(

View File

@ -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 // SetPlayerCharacter changes the doodad used for the player, by destroying the
// current player character and making it from scratch. // current player character and making it from scratch.
func (s *PlayScene) SetPlayerCharacter(filename string) { 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. // Center the player within the box of the doodad, for the Start Flag especially.
if !centerIn.IsZero() { if !centerIn.IsZero() {
spawn = render.NewPoint( 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. // 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() { } else if spawn.IsZero() && !s.SpawnPoint.IsZero() {
spawn = s.SpawnPoint spawn = s.SpawnPoint
@ -638,6 +643,11 @@ func (s *PlayScene) GetCheated() bool {
return s.cheated 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. // ShowEndLevelModal centralizes the EndLevel modal config.
// This is the common handler function between easy methods such as // This is the common handler function between easy methods such as
// BeatLevel, FailLevel, and DieByFire. // 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. // Draw the UI screen and any widgets that attached to it.
s.screen.Compute(d.Engine) s.screen.Compute(d.Engine)
s.screen.Present(d.Engine, render.Origin) s.screen.Present(d.Engine, render.Origin)

View File

@ -19,11 +19,11 @@ import (
// Actor is an object that marries together the three things that make a // Actor is an object that marries together the three things that make a
// Doodad instance "tick" while inside a Canvas: // Doodad instance "tick" while inside a Canvas:
// //
// - uix.Actor is a doodads.Drawing so it fulfills doodads.Actor to be a // - uix.Actor is a doodads.Drawing so it fulfills doodads.Actor to be a
// dynamic object during gameplay. // dynamic object during gameplay.
// - It has a pointer to the level.Actor indicating its static level data // - It has a pointer to the level.Actor indicating its static level data
// as defined in the map: its spawn coordinate and configuration. // as defined in the map: its spawn coordinate and configuration.
// - A uix.Canvas that can present the actor's graphics to the screen. // - A uix.Canvas that can present the actor's graphics to the screen.
type Actor struct { type Actor struct {
Drawing *doodads.Drawing Drawing *doodads.Drawing
Actor *level.Actor Actor *level.Actor
@ -67,8 +67,8 @@ func NewActor(id string, levelActor *level.Actor, doodad *doodads.Doodad) *Actor
id = uuid.Must(uuid.NewUUID()).String() id = uuid.Must(uuid.NewUUID()).String()
} }
size := doodad.Layers[0].Chunker.Size size := doodad.ChunkSize()
can := NewCanvas(int(size), false) can := NewCanvas(uint8(size), false)
can.Name = id can.Name = id
// TODO: if the Background is render.Invisible it gets defaulted to // 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.SetBackground(render.RGBA(0, 0, 1, 0))
can.LoadDoodad(doodad) can.LoadDoodad(doodad)
can.Resize(render.NewRect(size, size)) can.Resize(doodad.Size)
actor := &Actor{ actor := &Actor{
Drawing: doodads.NewDrawing(id, doodad), 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. // Size returns the size of the actor, from the underlying doodads.Drawing.
func (a *Actor) Size() render.Rect { func (a *Actor) Size() render.Rect {
return a.Drawing.Size() return a.Drawing.Doodad.Size
} }
// Velocity returns the actor's current velocity vector. // Velocity returns the actor's current velocity vector.

View File

@ -36,6 +36,11 @@ type Canvas struct {
Scrollable bool // Cursor keys will scroll the viewport of this canvas. Scrollable bool // Cursor keys will scroll the viewport of this canvas.
Zoom int // Zoom level on the 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. // Toogle for doodad canvases in the Level Editor to show their buttons.
ShowDoodadButtons bool ShowDoodadButtons bool
doodadButtonFrame ui.Widget // lazy init doodadButtonFrame ui.Widget // lazy init
@ -132,15 +137,17 @@ type Canvas struct {
// NewCanvas initializes a Canvas widget. // 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 // 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. // 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{ w := &Canvas{
Editable: editable, Editable: editable,
Scrollable: editable, Scrollable: editable,
Palette: level.NewPalette(), Palette: level.NewPalette(),
BrushSize: 1, BrushSize: 1,
chunks: level.NewChunker(size), chunks: level.NewChunker(uint8(size)),
actors: make([]*Actor, 0), actors: make([]*Actor, 0),
wallpaper: &Wallpaper{}, wallpaper: &Wallpaper{},
@ -372,7 +379,7 @@ func (w *Canvas) ViewportRelative() render.Rect {
// levels under control. // levels under control.
func (w *Canvas) LoadingViewport() render.Rect { func (w *Canvas) LoadingViewport() render.Rect {
var ( var (
chunkSize int chunkSize uint8
vp = w.Viewport() vp = w.Viewport()
margin = balance.LoadingViewportMarginChunks margin = balance.LoadingViewportMarginChunks
) )
@ -381,17 +388,18 @@ func (w *Canvas) LoadingViewport() render.Rect {
if w.level != nil { if w.level != nil {
chunkSize = w.level.Chunker.Size chunkSize = w.level.Chunker.Size
} else if w.doodad != nil { } else if w.doodad != nil {
chunkSize = w.doodad.ChunkSize() chunkSize = w.doodad.ChunkSize8()
} else { } else {
chunkSize = balance.ChunkSize chunkSize = balance.ChunkSize
log.Error("Canvas.LoadingViewport: no drawing to get chunk size from, default to %d", chunkSize) log.Error("Canvas.LoadingViewport: no drawing to get chunk size from, default to %d", chunkSize)
} }
var size = int(chunkSize)
return render.Rect{ return render.Rect{
X: vp.X - chunkSize*margin.X, X: vp.X - size*margin.X,
Y: vp.Y - chunkSize*margin.Y, Y: vp.Y - size*margin.Y,
W: vp.W + chunkSize*margin.X, W: vp.W + size*margin.X,
H: vp.H + chunkSize*margin.Y, H: vp.H + size*margin.Y,
} }
} }

View File

@ -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. // Hitting the left edge. Cap the X coord and shrink the width.
delta := p.X - drawAt.X // positive number delta := p.X - drawAt.X // positive number
drawAt.X = p.X drawAt.X = p.X
// scrollTo.X -= delta / 2 // TODO scrollTo.X -= delta
resizeTo.W -= 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. // Hitting the top edge. Cap the Y coord and shrink the height.
delta := p.Y - drawAt.Y delta := p.Y - drawAt.Y
drawAt.Y = p.Y drawAt.Y = p.Y
scrollTo.Y -= delta
resizeTo.H -= delta resizeTo.H -= delta
} }

26
pkg/uix/canvas_debug.go Normal file
View File

@ -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...)
}

View File

@ -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. // into which it will render, cap the source width and height.
// This is especially useful for Doodad buttons because the drawing // This is especially useful for Doodad buttons because the drawing
// is bigger than the button. // is bigger than the button.
if src.W > S.W { if w.CroppedSize {
src.W = S.W // 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.H > S.H { if src.W > S.W {
src.H = S.H src.W = S.W
}
if src.H > S.H {
src.H = S.H
}
} }
var size = int(chunk.Size)
dst := render.Rect{ dst := render.Rect{
X: p.X + w.Scroll.X + w.BoxThickness(1) + w.ZoomMultiply(coord.X*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*chunk.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 // 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 // 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 // TODO: all this shit is in TrimBox(), make it DRY
// If the destination width will cause it to overflow the widget // wtf? don't need all this code anymore??
// box, trim off the right edge of the destination rect. _ = ParentPosition
// /*
// 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
}
// The same for the top left edge, so the drawings don't overlap // If the destination width will cause it to overflow the widget
// menu bars or left side toolbars. // box, trim off the right edge of the destination rect.
// - Canvas was placed 80px from the left of the screen. //
// Canvas.MoveTo(80, 0) // Keep in mind we're dealing with chunks here, and a chunk is
// - A texture wants to draw at 60, 0 which would cause it to // a small part of the image. Example:
// overlap 20 pixels into the left toolbar. It needs to be cropped. // - Canvas is 800x600 (S.W=800 S.H=600)
// - The delta is: p.X=80 - dst.X=60 == 20 // - Chunk wants to render at 790,0 width 100,100 or whatever
// - Set destination X to p.X to constrain it there: 20 // dst={790, 0, 100, 100}
// - Subtract the delta from destination W so we don't scale it. // - Chunk box would exceed 800px width (X=790 + W=100 == 890)
// - Add 20 to X of the source: the left edge of source is not visible // - Find the delta how much it exceeds as negative (800 - 890 == -90)
// // - Lower the Source and Dest rects by that delta size so they
// Note: the +w.BoxThickness works around a bug if the Actor Canvas has // stay proportional and don't scale or anything dumb.
// a border on it (e.g. in the Actor/Link Tool mouse-over or debug setting) if dst.X+src.W > p.X+S.W+w.BoxThickness(1) {
if dst.X == ParentPosition.X+w.BoxThickness(1) { // NOTE: delta is a negative number,
// NOTE: delta is a positive number, // so it will subtract from the width.
// so it will add to the destination coordinates. delta := (p.X + S.W - w.BoxThickness(1)) - (dst.W + dst.X)
delta := texSizeOrig.W - src.W src.W += delta
dst.X = p.X + w.BoxThickness(1) dst.W += delta
src.X += delta }
} if dst.Y+src.H > p.Y+S.H+w.BoxThickness(1) {
if dst.Y == ParentPosition.Y+w.BoxThickness(1) { // NOTE: delta is a negative number
delta := texSizeOrig.H - src.H delta := (p.Y + S.H - w.BoxThickness(1)) - (dst.H + dst.Y)
dst.Y = p.Y + w.BoxThickness(1) src.H += delta
src.Y += delta dst.H += delta
} }
// Trim the destination width so it doesn't overlap the Canvas border. // The same for the top left edge, so the drawings don't overlap
if dst.W >= S.W-w.BoxThickness(1) { // menu bars or left side toolbars.
dst.W = S.W - w.BoxThickness(1) // - 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 // When zooming OUT, make sure the source rect is at least the
// full size of the chunk texture; otherwise the ZoomMultiplies // full size of the chunk texture; otherwise the ZoomMultiplies

View File

@ -398,6 +398,14 @@ func (form Form) Create(into *ui.Frame, fields []Field) {
if row.OnSelect != nil { if row.OnSelect != nil {
row.OnSelect(selection.Value) 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 return nil
}) })

View File

@ -26,10 +26,13 @@ type AddEditLevel struct {
// Editing settings for an existing level? // Editing settings for an existing level?
EditLevel *level.Level EditLevel *level.Level
// Show the "New Doodad" tab by default?
NewDoodad bool
// Callback functions. // Callback functions.
OnChangePageTypeAndWallpaper func(pageType level.PageType, wallpaper string) OnChangePageTypeAndWallpaper func(pageType level.PageType, wallpaper string)
OnCreateNewLevel func(*level.Level) OnCreateNewLevel func(*level.Level)
OnCreateNewDoodad func(size int) OnCreateNewDoodad func(width, height int)
OnReload func() OnReload func()
OnCancel func() OnCancel func()
} }
@ -74,6 +77,11 @@ func NewAddEditLevel(config AddEditLevel) *ui.Window {
tabframe.Supervise(config.Supervisor) tabframe.Supervise(config.Supervisor)
// Show the doodad tab?
if config.NewDoodad {
tabframe.SetTab("doodad")
}
window.Hide() window.Hide()
return window return window
} }
@ -389,7 +397,8 @@ func (config AddEditLevel) setupLevelFrame(tf *ui.TabFrame) {
func (config AddEditLevel) setupDoodadFrame(tf *ui.TabFrame) { func (config AddEditLevel) setupDoodadFrame(tf *ui.TabFrame) {
// Default options. // Default options.
var ( var (
doodadSize = 64 doodadWidth = 64
doodadHeight = doodadWidth
) )
frame := tf.AddTab("doodad", ui.NewLabel(ui.Label{ frame := tf.AddTab("doodad", ui.NewLabel(ui.Label{
@ -401,110 +410,89 @@ func (config AddEditLevel) setupDoodadFrame(tf *ui.TabFrame) {
* Frame for selecting Page Type * Frame for selecting Page Type
******************/ ******************/
typeFrame := ui.NewFrame("Doodad Options Frame") var sizeOptions = []magicform.Option{
frame.Pack(typeFrame, ui.Pack{ {Label: "32", Value: 32},
Side: ui.N, {Label: "64", Value: 64},
FillX: true, {Label: "96", Value: 96},
}) {Label: "128", Value: 128},
{Label: "200", Value: 200},
label1 := ui.NewLabel(ui.Label{ {Label: "256", Value: 256},
Text: "Doodad sprite size (square):", {Label: "Custom...", Value: 0},
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() {})
} }
sizeBtn.SetValue(doodadSize) form := magicform.Form{
sizeBtn.Handle(ui.Change, func(ed ui.EventData) error { Supervisor: config.Supervisor,
if selection, ok := sizeBtn.GetValue(); ok { Engine: config.Engine,
if size, ok := selection.Value.(int); ok { Vertical: true,
if size == 0 { LabelWidth: 90,
shmem.Prompt("Enter a custom size for the doodad width and height: ", func(answer string) { }
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 { if a, err := strconv.Atoi(answer); err == nil && a > 0 {
doodadSize = a doodadWidth = a
} else { } else {
shmem.FlashError("Doodad size should be a number greater than zero.") 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.) // Creates the Game Rules frame for existing level (set difficulty, etc.)

View File

@ -253,7 +253,7 @@ func makeDoodadTab(config DoodadDropper, frame *ui.Frame, size render.Rect, cate
lastColumn = 0 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.Name = doodad.Title
can.SetBackground(balance.DoodadButtonBackground) can.SetBackground(balance.DoodadButtonBackground)
can.LoadDoodad(doodad) can.LoadDoodad(doodad)
@ -262,6 +262,7 @@ func makeDoodadTab(config DoodadDropper, frame *ui.Frame, size render.Rect, cate
canvases = append(canvases, can) canvases = append(canvases, can)
btn := ui.NewButton(doodad.Title, can) btn := ui.NewButton(doodad.Title, can)
can.CroppedSize = true
btn.Resize(render.NewRect( btn.Resize(render.NewRect(
buttonSize-2, // TODO: without the -2 the button border buttonSize-2, // TODO: without the -2 the button border
buttonSize-2, // rests on top of the window border buttonSize-2, // rests on top of the window border