Doodad Edit Mode: Saving and Loading From Disk

Adds the first features to Edit Mode to support creation of Doodad
files! The "New Doodad" button pops up a prompt for a Doodad size
(default 100px) and configures the Canvas widget and makes a Doodad
struct instead of a Level to manage.

* Move the custom Canvas widget from `level.Canvas` to `uix.Canvas`
  (the uix package is for our custom UI widgets now)
* Rename the `doodads.Doodad` interface (for runtime instances of
  Doodads) to `doodads.Actor` and make `doodads.Doodad` describe the
  file format and JSON schema instead.
* Rename the `EditLevel()` method to `EditDrawing()` and it inspects the
  file extension to know whether to launch the Edit Mode for a Level or
  for a Doodad drawing.
* Doodads can be edited by using the `-edit` CLI flag or using the
  in-game file open features (including `edit` command of dev console).
* Add a `Scrollable` boolean to uix.Canvas to restrict the keyboard
  being able to scroll the level, for editing Doodads which have a fixed
  size.
This commit is contained in:
Noah 2018-09-26 10:04:46 -07:00
parent e25869644c
commit a7fd3aa1ca
17 changed files with 489 additions and 218 deletions

View File

@ -7,4 +7,7 @@ var (
// Default chunk size for canvases. // Default chunk size for canvases.
ChunkSize = 1000 ChunkSize = 1000
// Default size for a new Doodad.
DoodadSize = 100
) )

View File

@ -43,7 +43,7 @@ func main() {
app.SetupEngine() app.SetupEngine()
if filename != "" { if filename != "" {
if edit { if edit {
app.EditLevel(filename) app.EditDrawing(filename)
} else { } else {
app.PlayLevel(filename) app.PlayLevel(filename)
} }

View File

@ -132,7 +132,7 @@ func (c Command) Edit(d *Doodle) error {
filename := c.Args[0] filename := c.Args[0]
d.shell.Write("Editing level: " + filename) d.shell.Write("Editing level: " + filename)
d.EditLevel(filename) d.EditDrawing(filename)
return nil return nil
} }

78
doodads/actor.go Normal file
View File

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

View File

@ -5,27 +5,6 @@ import (
"git.kirsle.net/apps/doodle/render" "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. // Collide describes how a collision occurred.
type Collide struct { type Collide struct {
Top bool Top bool
@ -60,6 +39,52 @@ type CollisionBox struct {
Right []render.Point 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) // Side of the collision box (top, bottom, left, right)
type Side uint8 type Side uint8
@ -72,7 +97,7 @@ const (
) )
// CollidesWithGrid checks if a Doodad collides with level geometry. // 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 ( var (
P = d.Position() P = d.Position()
S = d.Size() S = d.Size()
@ -221,100 +246,3 @@ func CollidesWithGrid(d Doodad, grid *level.Chunker, target render.Point) (*Coll
func (c *Collide) IsColliding() bool { func (c *Collide) IsColliding() bool {
return c.Top || c.Bottom || c.Left || c.Right 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
}
}
}
}

48
doodads/doodad.go Normal file
View File

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

54
doodads/json.go Normal file
View File

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

View File

@ -1,8 +1,11 @@
package doodle package doodle
import ( import (
"fmt"
"strings"
"time" "time"
"git.kirsle.net/apps/doodle/enum"
"git.kirsle.net/apps/doodle/render" "git.kirsle.net/apps/doodle/render"
"github.com/kirsle/golog" "github.com/kirsle/golog"
) )
@ -163,13 +166,41 @@ func (d *Doodle) NewMap() {
d.Goto(scene) d.Goto(scene)
} }
// EditLevel loads a map from JSON into the EditorScene. // NewDoodad loads a new Doodad in Edit Mode.
func (d *Doodle) EditLevel(filename string) error { func (d *Doodle) NewDoodad(size int) {
log.Info("Loading level from file: %s", filename) 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{ scene := &EditorScene{
Filename: filename, Filename: filename,
OpenFile: true, 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) d.Goto(scene)
return nil return nil
} }

View File

@ -1,32 +1,38 @@
package doodle package doodle
import ( import (
"errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os" "os"
"git.kirsle.net/apps/doodle/balance" "git.kirsle.net/apps/doodle/balance"
"git.kirsle.net/apps/doodle/doodads"
"git.kirsle.net/apps/doodle/enum" "git.kirsle.net/apps/doodle/enum"
"git.kirsle.net/apps/doodle/events" "git.kirsle.net/apps/doodle/events"
"git.kirsle.net/apps/doodle/level" "git.kirsle.net/apps/doodle/level"
"git.kirsle.net/apps/doodle/render" "git.kirsle.net/apps/doodle/render"
"git.kirsle.net/apps/doodle/uix"
) )
// EditorScene manages the "Edit Level" game mode. // EditorScene manages the "Edit Level" game mode.
type EditorScene struct { type EditorScene struct {
// Configuration for the scene initializer. // Configuration for the scene initializer.
OpenFile bool DrawingType enum.DrawingType
Filename string OpenFile bool
Filename string
DoodadSize int
UI *EditorUI UI *EditorUI
// The current level being edited. // The current level or doodad object being edited, based on the
DrawingType enum.DrawingType // DrawingType.
Level *level.Level Level *level.Level
Doodad *doodads.Doodad
// The canvas widget that contains the map we're working on. // The canvas widget that contains the map we're working on.
// XXX: in dev builds this is available at $ d.Scene.GetDrawing() // XXX: in dev builds this is available at $ d.Scene.GetDrawing()
drawing *level.Canvas drawing *uix.Canvas
// Last saved filename by the user. // Last saved filename by the user.
filename string filename string
@ -39,7 +45,7 @@ func (s *EditorScene) Name() string {
// Setup the editor scene. // Setup the editor scene.
func (s *EditorScene) Setup(d *Doodle) error { 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 { if len(s.drawing.Palette.Swatches) > 0 {
s.drawing.SetSwatch(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.Resize(render.NewRect(d.width-150, d.height-44))
s.drawing.Compute(d.Engine) s.drawing.Compute(d.Engine)
// // Were we given configuration data? // Were we given configuration data?
if s.Filename != "" { if s.Filename != "" {
log.Debug("EditorScene.Setup: Set filename to %s", s.Filename) log.Debug("EditorScene.Setup: Set filename to %s", s.Filename)
s.filename = s.Filename 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? // Loading a Level or a Doodad?
if s.Level == nil { switch s.DrawingType {
log.Debug("EditorScene.Setup: initializing a new Level") case enum.LevelDrawing:
s.Level = level.New() if s.Level != nil {
s.Level.Palette = level.DefaultPalette() log.Debug("EditorScene.Setup: received level from scene caller")
s.drawing.LoadLevel(s.Level) 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 // 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. // SaveLevel saves the level to disk.
// TODO: move this into the Canvas? // 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 s.filename = filename
m := s.Level m := s.Level
@ -143,15 +182,54 @@ func (s *EditorScene) SaveLevel(filename string) {
json, err := m.ToJSON() json, err := m.ToJSON()
if err != nil { if err != nil {
log.Error("SaveLevel error: %s", err) return fmt.Errorf("SaveLevel error: %s", err)
return
} }
err = ioutil.WriteFile(filename, json, 0644) err = ioutil.WriteFile(filename, json, 0644)
if err != nil { if err != nil {
log.Error("Create map file error: %s", err) return fmt.Errorf("Create map file error: %s", err)
return
} }
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. // Destroy the scene.

View File

@ -1,11 +1,11 @@
package doodle 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. // TODO: build flags to not include this in production builds.
// This adds accessors for private variables from the dev console. // This adds accessors for private variables from the dev console.
// GetDrawing returns the level.Canvas // GetDrawing returns the uix.Canvas
func (w *EditorScene) GetDrawing() *level.Canvas { func (w *EditorScene) GetDrawing() *uix.Canvas {
return w.drawing return w.drawing
} }

View File

@ -2,8 +2,10 @@ package doodle
import ( import (
"fmt" "fmt"
"strconv"
"git.kirsle.net/apps/doodle/balance" "git.kirsle.net/apps/doodle/balance"
"git.kirsle.net/apps/doodle/enum"
"git.kirsle.net/apps/doodle/events" "git.kirsle.net/apps/doodle/events"
"git.kirsle.net/apps/doodle/render" "git.kirsle.net/apps/doodle/render"
"git.kirsle.net/apps/doodle/ui" "git.kirsle.net/apps/doodle/ui"
@ -63,11 +65,16 @@ func (u *EditorUI) Loop(ev *events.State) {
// Statusbar filename label. // Statusbar filename label.
filename := "untitled.map" filename := "untitled.map"
fileType := "Level"
if u.Scene.filename != "" { if u.Scene.filename != "" {
filename = 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, filename,
fileType,
) )
u.MenuBar.Compute(u.d.Engine) u.MenuBar.Compute(u.d.Engine)
@ -110,20 +117,52 @@ func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.Frame {
menuButton{ menuButton{
Text: "New Doodad", Text: "New Doodad",
Click: func(render.Point) { 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{ menuButton{
Text: "Save", Text: "Save",
Click: func(render.Point) { 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 != "" { if u.Scene.filename != "" {
u.Scene.SaveLevel(u.Scene.filename) saveFunc(u.Scene.filename)
d.Flash("Saved: %s", u.Scene.filename)
} else { } else {
d.Prompt("Save filename>", func(answer string) { d.Prompt("Save filename>", func(answer string) {
if answer != "" { if answer != "" {
u.Scene.SaveLevel("./maps/" + answer) // TODO: maps path saveFunc(answer)
d.Flash("Saved: %s", answer)
} }
}) })
} }
@ -145,7 +184,7 @@ func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.Frame {
Click: func(render.Point) { Click: func(render.Point) {
d.Prompt("Open filename>", func(answer string) { d.Prompt("Open filename>", func(answer string) {
if answer != "" { if answer != "" {
u.d.EditLevel("./maps/" + answer) // TODO: maps path u.d.EditDrawing("./maps/" + answer) // TODO: maps path
} }
}) })
}, },

2
fps.go
View File

@ -58,7 +58,7 @@ func (d *Doodle) DrawDebugOverlay() {
} }
// DrawCollisionBox draws the collision box around a Doodad. // 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 { if !d.Debug || !DebugCollision {
return return
} }

View File

@ -221,7 +221,7 @@ func (s *GUITestScene) Setup(d *Doodle) error {
})) }))
button2.Handle(ui.Click, func(p render.Point) { button2.Handle(ui.Click, func(p render.Point) {
d.Prompt("Map name>", func(name string) { d.Prompt("Map name>", func(name string) {
d.EditLevel(name) d.EditDrawing(name)
}) })
}) })

View File

@ -50,14 +50,5 @@ func LoadJSON(filename string) (*Level, error) {
// Inflate the private instance values. // Inflate the private instance values.
m.Palette.Inflate() 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 return m, err
} }

View File

@ -8,37 +8,36 @@ import (
"git.kirsle.net/apps/doodle/render" "git.kirsle.net/apps/doodle/render"
) )
// Level is the container format for Doodle map drawings. // Base provides the common struct keys that are shared between Levels and
type Level struct { // Doodads.
type Base struct {
Version int `json:"version"` // File format version spec. Version int `json:"version"` // File format version spec.
GameVersion string `json:"gameVersion"` // Game version that created the level. GameVersion string `json:"gameVersion"` // Game version that created the level.
Title string `json:"title"` Title string `json:"title"`
Author string `json:"author"` 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. // Chunked pixel data.
Chunker *Chunker `json:"chunks"` 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 // The Palette holds the unique "colors" used in this map file, and their
// properties (solid, fire, slippery, etc.) // properties (solid, fire, slippery, etc.)
Palette *Palette `json:"palette"` 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. // New creates a blank level object with all its members initialized.
func New() *Level { func New() *Level {
return &Level{ return &Level{
Version: 1, Base: Base{
Version: 1,
},
Chunker: NewChunker(balance.ChunkSize), Chunker: NewChunker(balance.ChunkSize),
Pixels: []*Pixel{},
Palette: &Palette{}, Palette: &Palette{},
} }
} }

View File

@ -8,6 +8,7 @@ import (
"git.kirsle.net/apps/doodle/events" "git.kirsle.net/apps/doodle/events"
"git.kirsle.net/apps/doodle/level" "git.kirsle.net/apps/doodle/level"
"git.kirsle.net/apps/doodle/render" "git.kirsle.net/apps/doodle/render"
"git.kirsle.net/apps/doodle/uix"
) )
// PlayScene manages the "Edit Level" game mode. // PlayScene manages the "Edit Level" game mode.
@ -17,10 +18,10 @@ type PlayScene struct {
Level *level.Level Level *level.Level
// Private variables. // Private variables.
drawing *level.Canvas drawing *uix.Canvas
// Player character // Player character
Player doodads.Doodad Player doodads.Actor
} }
// Name of the scene. // Name of the scene.
@ -30,7 +31,7 @@ func (s *PlayScene) Name() string {
// Setup the play scene. // Setup the play scene.
func (s *PlayScene) Setup(d *Doodle) error { 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.MoveTo(render.Origin)
s.drawing.Resize(render.NewRect(d.width, d.height)) s.drawing.Resize(render.NewRect(d.width, d.height))
s.drawing.Compute(d.Engine) s.drawing.Compute(d.Engine)

View File

@ -1,8 +1,10 @@
package level package uix
import ( import (
"git.kirsle.net/apps/doodle/balance" "git.kirsle.net/apps/doodle/balance"
"git.kirsle.net/apps/doodle/doodads"
"git.kirsle.net/apps/doodle/events" "git.kirsle.net/apps/doodle/events"
"git.kirsle.net/apps/doodle/level"
"git.kirsle.net/apps/doodle/render" "git.kirsle.net/apps/doodle/render"
"git.kirsle.net/apps/doodle/ui" "git.kirsle.net/apps/doodle/ui"
) )
@ -10,32 +12,37 @@ import (
// Canvas is a custom ui.Widget that manages a single drawing. // Canvas is a custom ui.Widget that manages a single drawing.
type Canvas struct { type Canvas struct {
ui.Frame ui.Frame
Palette *Palette Palette *level.Palette
// Set to true to allow clicking to edit this canvas. // Set to true to allow clicking to edit this canvas.
Editable bool Editable bool
Scrollable bool
chunks *Chunker chunks *level.Chunker
pixelHistory []*Pixel pixelHistory []*level.Pixel
lastPixel *Pixel lastPixel *level.Pixel
// We inherit the ui.Widget which manages the width and height. // We inherit the ui.Widget which manages the width and height.
Scroll render.Point // Scroll offset for which parts of canvas are visible. Scroll render.Point // Scroll offset for which parts of canvas are visible.
} }
// NewCanvas initializes a Canvas widget. // 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 { func NewCanvas(size int, editable bool) *Canvas {
w := &Canvas{ w := &Canvas{
Editable: editable, Editable: editable,
Palette: NewPalette(), Scrollable: editable,
chunks: NewChunker(size), Palette: level.NewPalette(),
chunks: level.NewChunker(size),
} }
w.setup() w.setup()
return w return w
} }
// Load initializes the Canvas using an existing Palette and Grid. // 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.Palette = p
w.chunks = g w.chunks = g
@ -45,12 +52,18 @@ func (w *Canvas) Load(p *Palette, g *Chunker) {
} }
// LoadLevel initializes a Canvas from a Level object. // 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) 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. // 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 w.Palette.ActiveSwatch = s
} }
@ -73,20 +86,22 @@ func (w *Canvas) Loop(ev *events.State) error {
_ = P _ = P
) )
// Arrow keys to scroll the view. if w.Scrollable {
scrollBy := render.Point{} // Arrow keys to scroll the view.
if ev.Right.Now { scrollBy := render.Point{}
scrollBy.X += balance.CanvasScrollSpeed if ev.Right.Now {
} else if ev.Left.Now { scrollBy.X += balance.CanvasScrollSpeed
scrollBy.X -= balance.CanvasScrollSpeed } else if ev.Left.Now {
} scrollBy.X -= balance.CanvasScrollSpeed
if ev.Down.Now { }
scrollBy.Y += balance.CanvasScrollSpeed if ev.Down.Now {
} else if ev.Up.Now { scrollBy.Y += balance.CanvasScrollSpeed
scrollBy.Y -= balance.CanvasScrollSpeed } else if ev.Up.Now {
} scrollBy.Y -= balance.CanvasScrollSpeed
if !scrollBy.IsZero() { }
w.ScrollBy(scrollBy) if !scrollBy.IsZero() {
w.ScrollBy(scrollBy)
}
} }
// Only care if the cursor is over our space. // 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, X: ev.CursorX.Now - P.X + w.Scroll.X,
Y: ev.CursorY.Now - P.Y + w.Scroll.Y, Y: ev.CursorY.Now - P.Y + w.Scroll.Y,
} }
pixel := &Pixel{ pixel := &level.Pixel{
X: cursor.X, X: cursor.X,
Y: cursor.Y, Y: cursor.Y,
Swatch: w.Palette.ActiveSwatch, Swatch: w.Palette.ActiveSwatch,
@ -152,10 +167,16 @@ func (w *Canvas) Viewport() render.Rect {
} }
// Chunker returns the underlying Chunker object. // Chunker returns the underlying Chunker object.
func (w *Canvas) Chunker() *Chunker { func (w *Canvas) Chunker() *level.Chunker {
return w.chunks 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. // ScrollBy adjusts the viewport scroll position.
func (w *Canvas) ScrollBy(by render.Point) { func (w *Canvas) ScrollBy(by render.Point) {
w.Scroll.Add(by) w.Scroll.Add(by)