diff --git a/balance/numbers.go b/balance/numbers.go index 0bac452..38ddf0c 100644 --- a/balance/numbers.go +++ b/balance/numbers.go @@ -7,4 +7,7 @@ var ( // Default chunk size for canvases. ChunkSize = 1000 + + // Default size for a new Doodad. + DoodadSize = 100 ) diff --git a/cmd/doodle/main.go b/cmd/doodle/main.go index 10926f6..e86e791 100644 --- a/cmd/doodle/main.go +++ b/cmd/doodle/main.go @@ -43,7 +43,7 @@ func main() { app.SetupEngine() if filename != "" { if edit { - app.EditLevel(filename) + app.EditDrawing(filename) } else { app.PlayLevel(filename) } diff --git a/commands.go b/commands.go index 1bd78bd..4274823 100644 --- a/commands.go +++ b/commands.go @@ -132,7 +132,7 @@ func (c Command) Edit(d *Doodle) error { filename := c.Args[0] d.shell.Write("Editing level: " + filename) - d.EditLevel(filename) + d.EditDrawing(filename) return nil } diff --git a/doodads/actor.go b/doodads/actor.go new file mode 100644 index 0000000..89707d1 --- /dev/null +++ b/doodads/actor.go @@ -0,0 +1,78 @@ +package doodads + +import ( + "git.kirsle.net/apps/doodle/level" + "git.kirsle.net/apps/doodle/render" +) + +// Actor is a reusable run-time drawing component used in Doodle. Actors are an +// active instance of a Doodad which have a position, velocity, etc. +type Actor interface { + ID() string + + // Position and velocity, not saved to disk. + Position() render.Point + Velocity() render.Point + Size() render.Rect + Grounded() bool + SetGrounded(bool) + + // Movement commands. + MoveBy(render.Point) // Add {X,Y} to current Position. + MoveTo(render.Point) // Set current Position to {X,Y}. + + // Implement the Draw function. + Draw(render.Engine) +} + +// GetBoundingRect computes the full pairs of points for the collision box +// around a doodad actor. +func GetBoundingRect(d Actor) render.Rect { + var ( + P = d.Position() + S = d.Size() + ) + return render.Rect{ + X: P.X, + Y: P.Y, + W: S.W, + H: S.H, + } +} + +// ScanBoundingBox scans all of the pixels in a bounding box on the grid and +// returns if any of them intersect with level geometry. +func (c *Collide) ScanBoundingBox(box render.Rect, grid *level.Chunker) bool { + col := GetCollisionBox(box) + + c.ScanGridLine(col.Top[0], col.Top[1], grid, Top) + c.ScanGridLine(col.Bottom[0], col.Bottom[1], grid, Bottom) + c.ScanGridLine(col.Left[0], col.Left[1], grid, Left) + c.ScanGridLine(col.Right[0], col.Right[1], grid, Right) + return c.IsColliding() +} + +// ScanGridLine scans all of the pixels between p1 and p2 on the grid and tests +// for any pixels to be set, implying a collision between level geometry and the +// bounding boxes of the doodad. +func (c *Collide) ScanGridLine(p1, p2 render.Point, grid *level.Chunker, side Side) { + for point := range render.IterLine2(p1, p2) { + if _, err := grid.Get(point); err == nil { + // A hit! + switch side { + case Top: + c.Top = true + c.TopPoint = point + case Bottom: + c.Bottom = true + c.BottomPoint = point + case Left: + c.Left = true + c.LeftPoint = point + case Right: + c.Right = true + c.RightPoint = point + } + } + } +} diff --git a/doodads/doodads.go b/doodads/collision.go similarity index 71% rename from doodads/doodads.go rename to doodads/collision.go index c906034..739a7ad 100644 --- a/doodads/doodads.go +++ b/doodads/collision.go @@ -5,27 +5,6 @@ import ( "git.kirsle.net/apps/doodle/render" ) -// Doodad is a reusable drawing component used in Doodle. Doodads are buttons, -// doors, switches, the player characters themselves, anything that isn't a part -// of the level geometry. -type Doodad interface { - ID() string - - // Position and velocity, not saved to disk. - Position() render.Point - Velocity() render.Point - Size() render.Rect - Grounded() bool - SetGrounded(bool) - - // Movement commands. - MoveBy(render.Point) // Add {X,Y} to current Position. - MoveTo(render.Point) // Set current Position to {X,Y}. - - // Implement the Draw function. - Draw(render.Engine) -} - // Collide describes how a collision occurred. type Collide struct { Top bool @@ -60,6 +39,52 @@ type CollisionBox struct { Right []render.Point } +// GetCollisionBox returns a CollisionBox with the four coordinates. +func GetCollisionBox(box render.Rect) CollisionBox { + return CollisionBox{ + Top: []render.Point{ + { + X: box.X, + Y: box.Y, + }, + { + X: box.X + box.W, + Y: box.Y, + }, + }, + Bottom: []render.Point{ + { + X: box.X, + Y: box.Y + box.H, + }, + { + X: box.X + box.W, + Y: box.Y + box.H, + }, + }, + Left: []render.Point{ + { + X: box.X, + Y: box.Y + box.H - 1, + }, + { + X: box.X, + Y: box.Y + 1, + }, + }, + Right: []render.Point{ + { + X: box.X + box.W, + Y: box.Y + box.H - 1, + }, + { + X: box.X + box.W, + Y: box.Y + 1, + }, + }, + } +} + // Side of the collision box (top, bottom, left, right) type Side uint8 @@ -72,7 +97,7 @@ const ( ) // CollidesWithGrid checks if a Doodad collides with level geometry. -func CollidesWithGrid(d Doodad, grid *level.Chunker, target render.Point) (*Collide, bool) { +func CollidesWithGrid(d Actor, grid *level.Chunker, target render.Point) (*Collide, bool) { var ( P = d.Position() S = d.Size() @@ -221,100 +246,3 @@ func CollidesWithGrid(d Doodad, grid *level.Chunker, target render.Point) (*Coll func (c *Collide) IsColliding() bool { return c.Top || c.Bottom || c.Left || c.Right } - -// GetBoundingRect computes the full pairs of points for the collision box -// around a doodad. -func GetBoundingRect(d Doodad) render.Rect { - var ( - P = d.Position() - S = d.Size() - ) - return render.Rect{ - X: P.X, - Y: P.Y, - W: S.W, - H: S.H, - } -} - -func GetCollisionBox(box render.Rect) CollisionBox { - return CollisionBox{ - Top: []render.Point{ - { - X: box.X, - Y: box.Y, - }, - { - X: box.X + box.W, - Y: box.Y, - }, - }, - Bottom: []render.Point{ - { - X: box.X, - Y: box.Y + box.H, - }, - { - X: box.X + box.W, - Y: box.Y + box.H, - }, - }, - Left: []render.Point{ - { - X: box.X, - Y: box.Y + box.H - 1, - }, - { - X: box.X, - Y: box.Y + 1, - }, - }, - Right: []render.Point{ - { - X: box.X + box.W, - Y: box.Y + box.H - 1, - }, - { - X: box.X + box.W, - Y: box.Y + 1, - }, - }, - } -} - -// ScanBoundingBox scans all of the pixels in a bounding box on the grid and -// returns if any of them intersect with level geometry. -func (c *Collide) ScanBoundingBox(box render.Rect, grid *level.Chunker) bool { - col := GetCollisionBox(box) - - c.ScanGridLine(col.Top[0], col.Top[1], grid, Top) - c.ScanGridLine(col.Bottom[0], col.Bottom[1], grid, Bottom) - c.ScanGridLine(col.Left[0], col.Left[1], grid, Left) - c.ScanGridLine(col.Right[0], col.Right[1], grid, Right) - return c.IsColliding() -} - -// ScanGridLine scans all of the pixels between p1 and p2 on the grid and tests -// for any pixels to be set, implying a collision between level geometry and the -// bounding boxes of the doodad. -func (c *Collide) ScanGridLine(p1, p2 render.Point, grid *level.Chunker, side Side) { - for point := range render.IterLine2(p1, p2) { - if _, err := grid.Get(point); err == nil { - // A hit! - switch side { - case Top: - c.Top = true - c.TopPoint = point - case Bottom: - c.Bottom = true - c.BottomPoint = point - case Left: - c.Left = true - c.LeftPoint = point - case Right: - c.Right = true - c.RightPoint = point - } - } - } -} diff --git a/doodads/doodad.go b/doodads/doodad.go new file mode 100644 index 0000000..ca5fc7d --- /dev/null +++ b/doodads/doodad.go @@ -0,0 +1,48 @@ +package doodads + +import ( + "git.kirsle.net/apps/doodle/balance" + "git.kirsle.net/apps/doodle/level" +) + +// Doodad is a reusable component for Levels that have scripts and graphics. +type Doodad struct { + level.Base + Palette *level.Palette `json:"palette"` + Script string `json:"script"` + Layers []Layer `json:"layers"` +} + +// Layer holds a layer of drawing data for a Doodad. +type Layer struct { + Name string `json:"name"` + Chunker *level.Chunker `json:"chunks"` +} + +// New creates a new Doodad. +func New(size int) *Doodad { + if size == 0 { + size = balance.DoodadSize + } + + return &Doodad{ + Base: level.Base{ + Version: 1, + }, + Palette: level.DefaultPalette(), + Layers: []Layer{ + { + Name: "main", + Chunker: level.NewChunker(size), + }, + }, + } +} + +// Inflate attaches the pixels to their swatches after loading from disk. +func (d *Doodad) Inflate() { + d.Palette.Inflate() + for _, layer := range d.Layers { + layer.Chunker.Inflate(d.Palette) + } +} diff --git a/doodads/json.go b/doodads/json.go new file mode 100644 index 0000000..9ce0547 --- /dev/null +++ b/doodads/json.go @@ -0,0 +1,54 @@ +package doodads + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "os" +) + +// ToJSON serializes the doodad as JSON. +func (d *Doodad) ToJSON() ([]byte, error) { + out := bytes.NewBuffer([]byte{}) + encoder := json.NewEncoder(out) + encoder.SetIndent("", "\t") + err := encoder.Encode(d) + return out.Bytes(), err +} + +// WriteJSON writes a Doodad to JSON on disk. +func (d *Doodad) WriteJSON(filename string) error { + json, err := d.ToJSON() + if err != nil { + return fmt.Errorf("Doodad.WriteJSON: JSON encode error: %s", err) + } + + err = ioutil.WriteFile(filename, json, 0755) + if err != nil { + return fmt.Errorf("Doodad.WriteJSON: WriteFile error: %s", err) + } + + return nil +} + +// LoadJSON loads a map from JSON file. +func LoadJSON(filename string) (*Doodad, error) { + fh, err := os.Open(filename) + if err != nil { + return nil, err + } + defer fh.Close() + + // Decode the JSON file from disk. + d := New(0) + decoder := json.NewDecoder(fh) + err = decoder.Decode(&d) + if err != nil { + return d, fmt.Errorf("doodad.LoadJSON: JSON decode error: %s", err) + } + + // Inflate the chunk metadata to map the pixels to their palette indexes. + d.Inflate() + return d, err +} diff --git a/doodle.go b/doodle.go index e7a3431..de1a96e 100644 --- a/doodle.go +++ b/doodle.go @@ -1,8 +1,11 @@ package doodle import ( + "fmt" + "strings" "time" + "git.kirsle.net/apps/doodle/enum" "git.kirsle.net/apps/doodle/render" "github.com/kirsle/golog" ) @@ -163,13 +166,41 @@ func (d *Doodle) NewMap() { d.Goto(scene) } -// EditLevel loads a map from JSON into the EditorScene. -func (d *Doodle) EditLevel(filename string) error { - log.Info("Loading level from file: %s", filename) +// NewDoodad loads a new Doodad in Edit Mode. +func (d *Doodle) NewDoodad(size int) { + log.Info("Starting a new doodad") + scene := &EditorScene{ + DrawingType: enum.DoodadDrawing, + DoodadSize: size, + } + d.Goto(scene) +} + +// EditDrawing loads a drawing (Level or Doodad) in Edit Mode. +func (d *Doodle) EditDrawing(filename string) error { + log.Info("Loading drawing from file: %s", filename) + parts := strings.Split(filename, ".") + if len(parts) < 2 { + return fmt.Errorf("filename `%s` has no file extension", filename) + } + ext := strings.ToLower(parts[len(parts)-1]) + scene := &EditorScene{ Filename: filename, OpenFile: true, } + + switch ext { + case "level": + case "map": + log.Info("is a LEvel type") + scene.DrawingType = enum.LevelDrawing + case "doodad": + scene.DrawingType = enum.DoodadDrawing + default: + return fmt.Errorf("file extension '%s' doesn't indicate its drawing type", ext) + } + d.Goto(scene) return nil } diff --git a/editor_scene.go b/editor_scene.go index 8d70f02..279e875 100644 --- a/editor_scene.go +++ b/editor_scene.go @@ -1,32 +1,38 @@ package doodle import ( + "errors" "fmt" "io/ioutil" "os" "git.kirsle.net/apps/doodle/balance" + "git.kirsle.net/apps/doodle/doodads" "git.kirsle.net/apps/doodle/enum" "git.kirsle.net/apps/doodle/events" "git.kirsle.net/apps/doodle/level" "git.kirsle.net/apps/doodle/render" + "git.kirsle.net/apps/doodle/uix" ) // EditorScene manages the "Edit Level" game mode. type EditorScene struct { // Configuration for the scene initializer. - OpenFile bool - Filename string + DrawingType enum.DrawingType + OpenFile bool + Filename string + DoodadSize int UI *EditorUI - // The current level being edited. - DrawingType enum.DrawingType - Level *level.Level + // The current level or doodad object being edited, based on the + // DrawingType. + Level *level.Level + Doodad *doodads.Doodad // The canvas widget that contains the map we're working on. // XXX: in dev builds this is available at $ d.Scene.GetDrawing() - drawing *level.Canvas + drawing *uix.Canvas // Last saved filename by the user. filename string @@ -39,7 +45,7 @@ func (s *EditorScene) Name() string { // Setup the editor scene. func (s *EditorScene) Setup(d *Doodle) error { - s.drawing = level.NewCanvas(balance.ChunkSize, true) + s.drawing = uix.NewCanvas(balance.ChunkSize, true) if len(s.drawing.Palette.Swatches) > 0 { s.drawing.SetSwatch(s.drawing.Palette.Swatches[0]) } @@ -49,28 +55,57 @@ func (s *EditorScene) Setup(d *Doodle) error { s.drawing.Resize(render.NewRect(d.width-150, d.height-44)) s.drawing.Compute(d.Engine) - // // Were we given configuration data? + // Were we given configuration data? if s.Filename != "" { log.Debug("EditorScene.Setup: Set filename to %s", s.Filename) s.filename = s.Filename s.Filename = "" } - if s.Level != nil { - log.Debug("EditorScene.Setup: received level from scene caller") - s.drawing.LoadLevel(s.Level) - } else if s.filename != "" && s.OpenFile { - log.Debug("EditorScene.Setup: Loading map from filename at %s", s.filename) - if err := s.LoadLevel(s.filename); err != nil { - d.Flash("LoadLevel error: %s", err) - } - } - // No level? - if s.Level == nil { - log.Debug("EditorScene.Setup: initializing a new Level") - s.Level = level.New() - s.Level.Palette = level.DefaultPalette() - s.drawing.LoadLevel(s.Level) + // Loading a Level or a Doodad? + switch s.DrawingType { + case enum.LevelDrawing: + if s.Level != nil { + log.Debug("EditorScene.Setup: received level from scene caller") + s.drawing.LoadLevel(s.Level) + } else if s.filename != "" && s.OpenFile { + log.Debug("EditorScene.Setup: Loading map from filename at %s", s.filename) + if err := s.LoadLevel(s.filename); err != nil { + d.Flash("LoadLevel error: %s", err) + } + } + + // No level? + if s.Level == nil { + log.Debug("EditorScene.Setup: initializing a new Level") + s.Level = level.New() + s.Level.Palette = level.DefaultPalette() + s.drawing.LoadLevel(s.Level) + s.drawing.ScrollTo(render.Origin) + s.drawing.Scrollable = true + } + case enum.DoodadDrawing: + // No Doodad? + if s.filename != "" && s.OpenFile { + log.Debug("EditorScene.Setup: Loading doodad from filename at %s", s.filename) + if err := s.LoadDoodad(s.filename); err != nil { + d.Flash("LoadDoodad error: %s", err) + } + } + + // No Doodad? + if s.Doodad == nil { + log.Debug("EditorScene.Setup: initializing a new Doodad") + s.Doodad = doodads.New(s.DoodadSize) + s.drawing.LoadDoodad(s.Doodad) + } + + // TODO: move inside the UI. Just an approximate position for now. + s.drawing.MoveTo(render.NewPoint(200, 200)) + s.drawing.Resize(render.NewRect(int32(s.DoodadSize), int32(s.DoodadSize))) + s.drawing.ScrollTo(render.Origin) + s.drawing.Scrollable = false + s.drawing.Compute(d.Engine) } // Initialize the user interface. It references the palette and such so it @@ -127,7 +162,11 @@ func (s *EditorScene) LoadLevel(filename string) error { // SaveLevel saves the level to disk. // TODO: move this into the Canvas? -func (s *EditorScene) SaveLevel(filename string) { +func (s *EditorScene) SaveLevel(filename string) error { + if s.DrawingType != enum.LevelDrawing { + return errors.New("SaveLevel: current drawing is not a Level type") + } + s.filename = filename m := s.Level @@ -143,15 +182,54 @@ func (s *EditorScene) SaveLevel(filename string) { json, err := m.ToJSON() if err != nil { - log.Error("SaveLevel error: %s", err) - return + return fmt.Errorf("SaveLevel error: %s", err) } err = ioutil.WriteFile(filename, json, 0644) if err != nil { - log.Error("Create map file error: %s", err) - return + return fmt.Errorf("Create map file error: %s", err) } + + return nil +} + +// LoadDoodad loads a doodad from disk. +func (s *EditorScene) LoadDoodad(filename string) error { + s.filename = filename + + doodad, err := doodads.LoadJSON(filename) + if err != nil { + return fmt.Errorf("EditorScene.LoadDoodad(%s): %s", filename, err) + } + + s.DrawingType = enum.DoodadDrawing + s.Doodad = doodad + s.DoodadSize = doodad.Layers[0].Chunker.Size + s.drawing.LoadDoodad(s.Doodad) + return nil +} + +// SaveDoodad saves the doodad to disk. +func (s *EditorScene) SaveDoodad(filename string) error { + if s.DrawingType != enum.DoodadDrawing { + return errors.New("SaveDoodad: current drawing is not a Doodad type") + } + + s.filename = filename + d := s.Doodad + if d.Title == "" { + d.Title = "Untitled Doodad" + } + if d.Author == "" { + d.Author = os.Getenv("USER") + } + + // TODO: is this copying necessary? + d.Palette = s.drawing.Palette + d.Layers[0].Chunker = s.drawing.Chunker() + + err := d.WriteJSON(s.filename) + return err } // Destroy the scene. diff --git a/editor_scene_debug.go b/editor_scene_debug.go index 8320a7e..7e74866 100644 --- a/editor_scene_debug.go +++ b/editor_scene_debug.go @@ -1,11 +1,11 @@ package doodle -import "git.kirsle.net/apps/doodle/level" +import "git.kirsle.net/apps/doodle/uix" // TODO: build flags to not include this in production builds. // This adds accessors for private variables from the dev console. -// GetDrawing returns the level.Canvas -func (w *EditorScene) GetDrawing() *level.Canvas { +// GetDrawing returns the uix.Canvas +func (w *EditorScene) GetDrawing() *uix.Canvas { return w.drawing } diff --git a/editor_ui.go b/editor_ui.go index 01a0619..6714dc5 100644 --- a/editor_ui.go +++ b/editor_ui.go @@ -2,8 +2,10 @@ package doodle import ( "fmt" + "strconv" "git.kirsle.net/apps/doodle/balance" + "git.kirsle.net/apps/doodle/enum" "git.kirsle.net/apps/doodle/events" "git.kirsle.net/apps/doodle/render" "git.kirsle.net/apps/doodle/ui" @@ -63,11 +65,16 @@ func (u *EditorUI) Loop(ev *events.State) { // Statusbar filename label. filename := "untitled.map" + fileType := "Level" if u.Scene.filename != "" { filename = u.Scene.filename } - u.StatusFilenameText = fmt.Sprintf("Filename: %s", + if u.Scene.DrawingType == enum.DoodadDrawing { + fileType = "Doodad" + } + u.StatusFilenameText = fmt.Sprintf("Filename: %s (%s)", filename, + fileType, ) u.MenuBar.Compute(u.d.Engine) @@ -110,20 +117,52 @@ func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.Frame { menuButton{ Text: "New Doodad", Click: func(render.Point) { - d.NewMap() + d.Prompt("Doodad size [100]>", func(answer string) { + size := balance.DoodadSize + if answer != "" { + i, err := strconv.Atoi(answer) + if err != nil { + d.Flash("Error: Doodad size must be a number.") + return + } + size = i + } + d.NewDoodad(size) + }) }, }, menuButton{ Text: "Save", Click: func(render.Point) { + var saveFunc func(filename string) + + switch u.Scene.DrawingType { + case enum.LevelDrawing: + saveFunc = func(filename string) { + if err := u.Scene.SaveLevel(filename); err != nil { + d.Flash("Error: %s", err) + } else { + d.Flash("Saved level: %s", filename) + } + } + case enum.DoodadDrawing: + saveFunc = func(filename string) { + if err := u.Scene.SaveDoodad(filename); err != nil { + d.Flash("Error: %s", err) + } else { + d.Flash("Saved doodad: %s", filename) + } + } + default: + d.Flash("Error: Scene.DrawingType is not a valid type") + } + if u.Scene.filename != "" { - u.Scene.SaveLevel(u.Scene.filename) - d.Flash("Saved: %s", u.Scene.filename) + saveFunc(u.Scene.filename) } else { d.Prompt("Save filename>", func(answer string) { if answer != "" { - u.Scene.SaveLevel("./maps/" + answer) // TODO: maps path - d.Flash("Saved: %s", answer) + saveFunc(answer) } }) } @@ -145,7 +184,7 @@ func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.Frame { Click: func(render.Point) { d.Prompt("Open filename>", func(answer string) { if answer != "" { - u.d.EditLevel("./maps/" + answer) // TODO: maps path + u.d.EditDrawing("./maps/" + answer) // TODO: maps path } }) }, diff --git a/fps.go b/fps.go index 809272a..6d2295e 100644 --- a/fps.go +++ b/fps.go @@ -58,7 +58,7 @@ func (d *Doodle) DrawDebugOverlay() { } // DrawCollisionBox draws the collision box around a Doodad. -func (d *Doodle) DrawCollisionBox(actor doodads.Doodad) { +func (d *Doodle) DrawCollisionBox(actor doodads.Actor) { if !d.Debug || !DebugCollision { return } diff --git a/guitest_scene.go b/guitest_scene.go index 8e67a03..63610d8 100644 --- a/guitest_scene.go +++ b/guitest_scene.go @@ -221,7 +221,7 @@ func (s *GUITestScene) Setup(d *Doodle) error { })) button2.Handle(ui.Click, func(p render.Point) { d.Prompt("Map name>", func(name string) { - d.EditLevel(name) + d.EditDrawing(name) }) }) diff --git a/level/json.go b/level/json.go index 3ea821b..ff14de7 100644 --- a/level/json.go +++ b/level/json.go @@ -50,14 +50,5 @@ func LoadJSON(filename string) (*Level, error) { // Inflate the private instance values. m.Palette.Inflate() - for _, px := range m.Pixels { - if int(px.PaletteIndex) > len(m.Palette.Swatches) { - return nil, fmt.Errorf( - "pixel %s references palette index %d but there are only %d swatches in the palette", - px, px.PaletteIndex, len(m.Palette.Swatches), - ) - } - px.Swatch = m.Palette.Swatches[px.PaletteIndex] - } return m, err } diff --git a/level/types.go b/level/types.go index 794abb0..e318d10 100644 --- a/level/types.go +++ b/level/types.go @@ -8,37 +8,36 @@ import ( "git.kirsle.net/apps/doodle/render" ) -// Level is the container format for Doodle map drawings. -type Level struct { +// Base provides the common struct keys that are shared between Levels and +// Doodads. +type Base struct { Version int `json:"version"` // File format version spec. GameVersion string `json:"gameVersion"` // Game version that created the level. Title string `json:"title"` Author string `json:"author"` - Password string `json:"passwd"` - Locked bool `json:"locked"` +} + +// Level is the container format for Doodle map drawings. +type Level struct { + Base + Password string `json:"passwd"` + Locked bool `json:"locked"` // Chunked pixel data. Chunker *Chunker `json:"chunks"` - // XXX: deprecated? - Width int32 `json:"w"` - Height int32 `json:"h"` - // The Palette holds the unique "colors" used in this map file, and their // properties (solid, fire, slippery, etc.) Palette *Palette `json:"palette"` - - // Pixels is a 2D array indexed by [X][Y]. The cell values are indexes into - // the Palette. - Pixels []*Pixel `json:"pixels"` } // New creates a blank level object with all its members initialized. func New() *Level { return &Level{ - Version: 1, + Base: Base{ + Version: 1, + }, Chunker: NewChunker(balance.ChunkSize), - Pixels: []*Pixel{}, Palette: &Palette{}, } } diff --git a/play_scene.go b/play_scene.go index b5b8bee..dc2d227 100644 --- a/play_scene.go +++ b/play_scene.go @@ -8,6 +8,7 @@ import ( "git.kirsle.net/apps/doodle/events" "git.kirsle.net/apps/doodle/level" "git.kirsle.net/apps/doodle/render" + "git.kirsle.net/apps/doodle/uix" ) // PlayScene manages the "Edit Level" game mode. @@ -17,10 +18,10 @@ type PlayScene struct { Level *level.Level // Private variables. - drawing *level.Canvas + drawing *uix.Canvas // Player character - Player doodads.Doodad + Player doodads.Actor } // Name of the scene. @@ -30,7 +31,7 @@ func (s *PlayScene) Name() string { // Setup the play scene. func (s *PlayScene) Setup(d *Doodle) error { - s.drawing = level.NewCanvas(balance.ChunkSize, false) + s.drawing = uix.NewCanvas(balance.ChunkSize, false) s.drawing.MoveTo(render.Origin) s.drawing.Resize(render.NewRect(d.width, d.height)) s.drawing.Compute(d.Engine) diff --git a/level/canvas.go b/uix/canvas.go similarity index 73% rename from level/canvas.go rename to uix/canvas.go index 7b936ae..0539909 100644 --- a/level/canvas.go +++ b/uix/canvas.go @@ -1,8 +1,10 @@ -package level +package uix import ( "git.kirsle.net/apps/doodle/balance" + "git.kirsle.net/apps/doodle/doodads" "git.kirsle.net/apps/doodle/events" + "git.kirsle.net/apps/doodle/level" "git.kirsle.net/apps/doodle/render" "git.kirsle.net/apps/doodle/ui" ) @@ -10,32 +12,37 @@ import ( // Canvas is a custom ui.Widget that manages a single drawing. type Canvas struct { ui.Frame - Palette *Palette + Palette *level.Palette // Set to true to allow clicking to edit this canvas. - Editable bool + Editable bool + Scrollable bool - chunks *Chunker - pixelHistory []*Pixel - lastPixel *Pixel + chunks *level.Chunker + pixelHistory []*level.Pixel + lastPixel *level.Pixel // We inherit the ui.Widget which manages the width and height. Scroll render.Point // Scroll offset for which parts of canvas are visible. } // NewCanvas initializes a Canvas widget. +// +// 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 { w := &Canvas{ - Editable: editable, - Palette: NewPalette(), - chunks: NewChunker(size), + Editable: editable, + Scrollable: editable, + Palette: level.NewPalette(), + chunks: level.NewChunker(size), } w.setup() return w } // Load initializes the Canvas using an existing Palette and Grid. -func (w *Canvas) Load(p *Palette, g *Chunker) { +func (w *Canvas) Load(p *level.Palette, g *level.Chunker) { w.Palette = p w.chunks = g @@ -45,12 +52,18 @@ func (w *Canvas) Load(p *Palette, g *Chunker) { } // LoadLevel initializes a Canvas from a Level object. -func (w *Canvas) LoadLevel(level *Level) { +func (w *Canvas) LoadLevel(level *level.Level) { w.Load(level.Palette, level.Chunker) } +// LoadDoodad initializes a Canvas from a Doodad object. +func (w *Canvas) LoadDoodad(d *doodads.Doodad) { + // TODO more safe + w.Load(d.Palette, d.Layers[0].Chunker) +} + // SetSwatch changes the currently selected swatch for editing. -func (w *Canvas) SetSwatch(s *Swatch) { +func (w *Canvas) SetSwatch(s *level.Swatch) { w.Palette.ActiveSwatch = s } @@ -73,20 +86,22 @@ func (w *Canvas) Loop(ev *events.State) error { _ = P ) - // Arrow keys to scroll the view. - scrollBy := render.Point{} - if ev.Right.Now { - scrollBy.X += balance.CanvasScrollSpeed - } else if ev.Left.Now { - scrollBy.X -= balance.CanvasScrollSpeed - } - if ev.Down.Now { - scrollBy.Y += balance.CanvasScrollSpeed - } else if ev.Up.Now { - scrollBy.Y -= balance.CanvasScrollSpeed - } - if !scrollBy.IsZero() { - w.ScrollBy(scrollBy) + if w.Scrollable { + // Arrow keys to scroll the view. + scrollBy := render.Point{} + if ev.Right.Now { + scrollBy.X += balance.CanvasScrollSpeed + } else if ev.Left.Now { + scrollBy.X -= balance.CanvasScrollSpeed + } + if ev.Down.Now { + scrollBy.Y += balance.CanvasScrollSpeed + } else if ev.Up.Now { + scrollBy.Y -= balance.CanvasScrollSpeed + } + if !scrollBy.IsZero() { + w.ScrollBy(scrollBy) + } } // Only care if the cursor is over our space. @@ -108,7 +123,7 @@ func (w *Canvas) Loop(ev *events.State) error { X: ev.CursorX.Now - P.X + w.Scroll.X, Y: ev.CursorY.Now - P.Y + w.Scroll.Y, } - pixel := &Pixel{ + pixel := &level.Pixel{ X: cursor.X, Y: cursor.Y, Swatch: w.Palette.ActiveSwatch, @@ -152,10 +167,16 @@ func (w *Canvas) Viewport() render.Rect { } // Chunker returns the underlying Chunker object. -func (w *Canvas) Chunker() *Chunker { +func (w *Canvas) Chunker() *level.Chunker { return w.chunks } +// ScrollTo sets the viewport scroll position. +func (w *Canvas) ScrollTo(to render.Point) { + w.Scroll.X = to.X + w.Scroll.Y = to.Y +} + // ScrollBy adjusts the viewport scroll position. func (w *Canvas) ScrollBy(by render.Point) { w.Scroll.Add(by)