diff --git a/balance/numbers.go b/balance/numbers.go index 085d3b3..0bac452 100644 --- a/balance/numbers.go +++ b/balance/numbers.go @@ -4,4 +4,7 @@ package balance var ( // Speed to scroll a canvas with arrow keys in Edit Mode. CanvasScrollSpeed int32 = 8 + + // Default chunk size for canvases. + ChunkSize = 1000 ) diff --git a/editor_scene.go b/editor_scene.go index 60ebdcd..5997a63 100644 --- a/editor_scene.go +++ b/editor_scene.go @@ -4,6 +4,7 @@ import ( "io/ioutil" "os" + "git.kirsle.net/apps/doodle/balance" "git.kirsle.net/apps/doodle/events" "git.kirsle.net/apps/doodle/level" "git.kirsle.net/apps/doodle/render" @@ -14,7 +15,7 @@ type EditorScene struct { // Configuration for the scene initializer. OpenFile bool Filename string - Canvas *level.Grid + Canvas *level.Chunker UI *EditorUI @@ -37,7 +38,7 @@ func (s *EditorScene) Name() string { // Setup the editor scene. func (s *EditorScene) Setup(d *Doodle) error { - s.drawing = level.NewCanvas(true) + s.drawing = level.NewCanvas(balance.ChunkSize, true) s.drawing.Palette = level.DefaultPalette() if len(s.drawing.Palette.Swatches) > 0 { s.drawing.SetSwatch(s.drawing.Palette.Swatches[0]) @@ -80,7 +81,7 @@ func (s *EditorScene) Loop(d *Doodle, ev *events.State) error { if ev.KeyName.Read() == "p" { log.Info("Play Mode, Go!") d.Goto(&PlayScene{ - Canvas: s.drawing.Grid(), + // Canvas: s.drawing.Grid(), XXX }) return nil } @@ -122,14 +123,7 @@ func (s *EditorScene) SaveLevel(filename string) { m.Width = s.width m.Height = s.height m.Palette = s.drawing.Palette - - for pixel := range *s.drawing.Grid() { - m.Pixels = append(m.Pixels, &level.Pixel{ - X: pixel.X, - Y: pixel.Y, - PaletteIndex: int32(pixel.Swatch.Index()), - }) - } + m.Chunker = s.drawing.Chunker() json, err := m.ToJSON() if err != nil { diff --git a/fps.go b/fps.go index 9898d60..809272a 100644 --- a/fps.go +++ b/fps.go @@ -13,7 +13,7 @@ const maxSamples = 100 // Debug mode options, these can be enabled in the dev console // like: boolProp DebugOverlay true var ( - DebugOverlay = false + DebugOverlay = true DebugCollision = true ) diff --git a/level/canvas.go b/level/canvas.go index 00ce716..3bc592d 100644 --- a/level/canvas.go +++ b/level/canvas.go @@ -15,7 +15,7 @@ type Canvas struct { // Set to true to allow clicking to edit this canvas. Editable bool - grid Grid + chunks *Chunker pixelHistory []*Pixel lastPixel *Pixel @@ -24,35 +24,31 @@ type Canvas struct { } // NewCanvas initializes a Canvas widget. -func NewCanvas(editable bool) *Canvas { +func NewCanvas(size int, editable bool) *Canvas { w := &Canvas{ Editable: editable, Palette: NewPalette(), - grid: Grid{}, + chunks: NewChunker(size), } w.setup() return w } // Load initializes the Canvas using an existing Palette and Grid. -func (w *Canvas) Load(p *Palette, g *Grid) { +func (w *Canvas) Load(p *Palette, g *Chunker) { w.Palette = p - w.grid = *g + w.chunks = g } // LoadFilename initializes the Canvas using a file on disk. func (w *Canvas) LoadFilename(filename string) error { - w.grid = Grid{} - m, err := LoadJSON(filename) if err != nil { return err } - for _, pixel := range m.Pixels { - w.grid[pixel] = nil - } w.Palette = m.Palette + w.chunks = m.Chunker if len(w.Palette.Swatches) > 0 { w.SetSwatch(w.Palette.Swatches[0]) @@ -80,7 +76,6 @@ func (w *Canvas) setup() { // Loop is called on the scene's event loop to handle mouse interaction with // the canvas, i.e. to edit it. func (w *Canvas) Loop(ev *events.State) error { - log.Info("my territory") var ( P = w.Point() _ = P @@ -117,9 +112,13 @@ func (w *Canvas) Loop(ev *events.State) error { if ev.Button1.Now { // log.Warn("Button1: %+v", ev.Button1) lastPixel := w.lastPixel + cursor := render.Point{ + X: ev.CursorX.Now - P.X + w.Scroll.X, + Y: ev.CursorY.Now - P.Y + w.Scroll.Y, + } pixel := &Pixel{ - X: ev.CursorX.Now - P.X + w.Scroll.X, - Y: ev.CursorY.Now - P.Y + w.Scroll.Y, + X: cursor.X, + Y: cursor.Y, Palette: w.Palette, Swatch: w.Palette.ActiveSwatch, } @@ -130,13 +129,7 @@ func (w *Canvas) Loop(ev *events.State) error { // Draw the pixels in between. if lastPixel != pixel { for point := range render.IterLine(lastPixel.X, lastPixel.Y, pixel.X, pixel.Y) { - dot := &Pixel{ - X: point.X, - Y: point.Y, - Palette: lastPixel.Palette, - Swatch: lastPixel.Swatch, - } - w.grid[dot] = nil + w.chunks.Set(point, lastPixel.Swatch) } } } @@ -145,7 +138,7 @@ func (w *Canvas) Loop(ev *events.State) error { w.pixelHistory = append(w.pixelHistory, pixel) // Save in the pixel canvas map. - w.grid[pixel] = nil + w.chunks.Set(cursor, pixel.Swatch) } } else { w.lastPixel = nil @@ -167,9 +160,9 @@ func (w *Canvas) Viewport() render.Rect { } } -// Grid returns the underlying grid object. -func (w *Canvas) Grid() *Grid { - return &w.grid +// Chunker returns the underlying Chunker object. +func (w *Canvas) Chunker() *Chunker { + return w.chunks } // ScrollBy adjusts the viewport scroll position. @@ -197,20 +190,15 @@ func (w *Canvas) Present(e render.Engine, p render.Point) { H: S.H - w.BoxThickness(2), }) - for pixel := range w.grid { - point := render.NewPoint(pixel.X, pixel.Y) - if point.Inside(Viewport) { - // This pixel is visible in the canvas, but offset it by the - // scroll height. - point.Add(render.Point{ - X: -Viewport.X, - Y: -Viewport.Y, - }) - color := pixel.Swatch.Color - e.DrawPoint(color, render.Point{ - X: p.X + w.BoxThickness(1) + point.X, - Y: p.Y + w.BoxThickness(1) + point.Y, - }) - } + for px := range w.chunks.IterViewport(Viewport) { + // This pixel is visible in the canvas, but offset it by the + // scroll height. + px.X -= Viewport.X + px.Y -= Viewport.Y + color := px.Swatch.Color + e.DrawPoint(color, render.Point{ + X: p.X + w.BoxThickness(1) + px.X, + Y: p.Y + w.BoxThickness(1) + px.Y, + }) } } diff --git a/level/chunk.go b/level/chunk.go new file mode 100644 index 0000000..fa37431 --- /dev/null +++ b/level/chunk.go @@ -0,0 +1,89 @@ +package level + +import ( + "encoding/json" + "fmt" + + "git.kirsle.net/apps/doodle/render" +) + +// Types of chunks. +const ( + MapType int = iota + GridType +) + +// Chunk holds a single portion of the pixel canvas. +type Chunk struct { + Type int // map vs. 2D array. + Accessor +} + +// JSONChunk holds a lightweight (interface-free) copy of the Chunk for +// unmarshalling JSON files from disk. +type JSONChunk struct { + Type int `json:"type"` + Data json.RawMessage `json:"data"` +} + +// Accessor provides a high-level API to interact with absolute pixel coordinates +// while abstracting away the details of how they're stored. +type Accessor interface { + Inflate(*Palette) error + Iter() <-chan Pixel + IterViewport(viewport render.Rect) <-chan Pixel + Get(render.Point) (*Swatch, error) + Set(render.Point, *Swatch) error + Delete(render.Point) error + Len() int + MarshalJSON() ([]byte, error) + UnmarshalJSON([]byte) error +} + +// NewChunk creates a new chunk. +func NewChunk() *Chunk { + return &Chunk{ + Type: MapType, + Accessor: NewMapAccessor(), + } +} + +// Usage returns the percent of free space vs. allocated pixels in the chunk. +func (c *Chunk) Usage(size int) float64 { + return float64(c.Len()) / float64(size) +} + +// MarshalJSON writes the chunk to JSON. +func (c *Chunk) MarshalJSON() ([]byte, error) { + data, err := c.Accessor.MarshalJSON() + if err != nil { + return []byte{}, err + } + + generic := &JSONChunk{ + Type: c.Type, + Data: data, + } + b, err := json.Marshal(generic) + return b, err +} + +// UnmarshalJSON loads the chunk from JSON and uses the correct accessor to +// parse the inner details. +func (c *Chunk) UnmarshalJSON(b []byte) error { + // Parse it generically so we can hand off the inner "data" object to the + // right accessor for unmarshalling. + generic := &JSONChunk{} + err := json.Unmarshal(b, generic) + if err != nil { + return fmt.Errorf("Chunk.UnmarshalJSON: failed to unmarshal into generic JSONChunk type: %s", err) + } + + switch c.Type { + case MapType: + c.Accessor = NewMapAccessor() + return c.Accessor.UnmarshalJSON(generic.Data) + default: + return fmt.Errorf("Chunk.UnmarshalJSON: unsupported chunk type '%d'", c.Type) + } +} diff --git a/level/chunk_map.go b/level/chunk_map.go new file mode 100644 index 0000000..45a1075 --- /dev/null +++ b/level/chunk_map.go @@ -0,0 +1,129 @@ +package level + +import ( + "encoding/json" + "errors" + "fmt" + + "git.kirsle.net/apps/doodle/render" +) + +// MapAccessor implements a chunk accessor by using a map of points to their +// palette indexes. This is the simplest accessor and is best for sparse chunks. +type MapAccessor map[render.Point]*Swatch + +// NewMapAccessor initializes a MapAccessor. +func NewMapAccessor() MapAccessor { + return MapAccessor{} +} + +// Inflate the sparse swatches from their palette indexes. +func (a MapAccessor) Inflate(pal *Palette) error { + for point, swatch := range a { + if swatch.IsSparse() { + // Replace this with the correct swatch from the palette. + if len(pal.Swatches) < swatch.paletteIndex { + return fmt.Errorf("MapAccessor.Inflate: swatch for point %s has paletteIndex %d but palette has only %d colors", + point, + swatch.paletteIndex, + len(pal.Swatches), + ) + } + a[point] = pal.Swatches[swatch.paletteIndex] + } + } + return nil +} + +// Len returns the current size of the map, or number of pixels registered. +func (a MapAccessor) Len() int { + return len(a) +} + +// IterViewport returns a channel to loop over pixels in the viewport. +func (a MapAccessor) IterViewport(viewport render.Rect) <-chan Pixel { + pipe := make(chan Pixel) + go func() { + for px := range a.Iter() { + if px.Point().Inside(viewport) { + pipe <- px + } + } + close(pipe) + }() + return pipe +} + +// Iter returns a channel to loop over all points in this chunk. +func (a MapAccessor) Iter() <-chan Pixel { + pipe := make(chan Pixel) + go func() { + for point, swatch := range a { + pipe <- Pixel{ + X: point.X, + Y: point.Y, + Swatch: swatch, + } + } + close(pipe) + }() + return pipe +} + +// Get a pixel from the map. +func (a MapAccessor) Get(p render.Point) (*Swatch, error) { + pixel, ok := a[p] + if !ok { + return nil, errors.New("no pixel") + } + return pixel, nil +} + +// Set a pixel on the map. +func (a MapAccessor) Set(p render.Point, sw *Swatch) error { + a[p] = sw + return nil +} + +// Delete a pixel from the map. +func (a MapAccessor) Delete(p render.Point) error { + if _, ok := a[p]; ok { + delete(a, p) + return nil + } + return errors.New("pixel was not there") +} + +// MarshalJSON to convert the chunk map to JSON. +// +// When serialized, the key is the "X,Y" coordinate and the value is the +// swatch index of the Palette, rather than redundantly serializing out the +// Swatch object for every pixel. +func (a MapAccessor) MarshalJSON() ([]byte, error) { + dict := map[string]int{} + for point, sw := range a { + dict[point.String()] = sw.Index() + } + + out, err := json.Marshal(dict) + return out, err +} + +// UnmarshalJSON to convert the chunk map back from JSON. +func (a MapAccessor) UnmarshalJSON(b []byte) error { + var dict map[string]int + err := json.Unmarshal(b, &dict) + if err != nil { + return err + } + + for coord, index := range dict { + point, err := render.ParsePoint(coord) + if err != nil { + return fmt.Errorf("MapAccessor.UnmarshalJSON: %s", err) + } + a[point] = NewSparseSwatch(index) + } + + return nil +} diff --git a/level/chunk_test.go b/level/chunk_test.go new file mode 100644 index 0000000..7ebf3ff --- /dev/null +++ b/level/chunk_test.go @@ -0,0 +1,306 @@ +package level_test + +import ( + "fmt" + "testing" + + "git.kirsle.net/apps/doodle/level" + "git.kirsle.net/apps/doodle/render" +) + +// Test the high level Chunker. +func TestChunker(t *testing.T) { + c := level.NewChunker(1000) + + // Test swatches. + var ( + grey = &level.Swatch{ + Name: "solid", + Color: render.Grey, + } + ) + + type testCase struct { + name string + run func() error + } + tests := []testCase{ + testCase{ + name: "Access a pixel on the blank map and expect an error", + run: func() error { + p := render.NewPoint(65535, -214564545) + _, err := c.Get(p) + if err == nil { + return fmt.Errorf("unexpected success getting point %s", p) + } + return nil + }, + }, + + testCase{ + name: "Set a pixel", + run: func() error { + // Set a point. + p := render.NewPoint(100, 200) + err := c.Set(p, grey) + if err != nil { + return fmt.Errorf("unexpected error getting point %s: %s", p, err) + } + return nil + }, + }, + + testCase{ + name: "Verify the set pixel", + run: func() error { + p := render.NewPoint(100, 200) + px, err := c.Get(p) + if err != nil { + return err + } + if px != grey { + return fmt.Errorf("pixel at %s not the expected color:\n"+ + "Expected: %s\n"+ + " Got: %s", + p, + grey, + px, + ) + } + return nil + }, + }, + + testCase{ + name: "Verify the neighboring pixel is unset", + run: func() error { + p := render.NewPoint(101, 200) + _, err := c.Get(p) + if err == nil { + return fmt.Errorf("unexpected success getting point %s", p) + } + return nil + }, + }, + + testCase{ + name: "Delete the set pixel", + run: func() error { + p := render.NewPoint(100, 200) + err := c.Delete(p) + if err != nil { + return err + } + return nil + }, + }, + + testCase{ + name: "Verify the deleted pixel is unset", + run: func() error { + p := render.NewPoint(100, 200) + _, err := c.Get(p) + if err == nil { + return fmt.Errorf("unexpected success getting point %s", p) + } + return nil + }, + }, + + testCase{ + name: "Delete a pixel that didn't exist", + run: func() error { + p := render.NewPoint(-100, -100) + err := c.Delete(p) + if err == nil { + return fmt.Errorf("unexpected success deleting point %s", p) + } + return nil + }, + }, + } + + for _, test := range tests { + if err := test.run(); err != nil { + t.Errorf("Failed: %s\n%s", test.name, err) + } + } +} + +// Test the map chunk accessor. +func TestMapAccessor(t *testing.T) { + a := level.NewMapAccessor() + _ = a + + // Test action types + var ( + Get = "Get" + Set = "Set" + Delete = "Delete" + ) + + // Test swatches. + var ( + red = &level.Swatch{ + Name: "fire", + Color: render.Red, + } + ) + + type testCase struct { + Action string + P render.Point + S *level.Swatch + Expect *level.Swatch + Err bool // expect error + } + tests := []testCase{ + // Get a random point and expect to fail. + testCase{ + Action: Get, + P: render.NewPoint(128, 128), + Err: true, + }, + + // Set a point. + testCase{ + Action: Set, + S: red, + P: render.NewPoint(1024, 2048), + }, + + // Verify it exists. + testCase{ + Action: Get, + P: render.NewPoint(1024, 2048), + Expect: red, + }, + + // A neighboring point does not exist. + testCase{ + Action: Get, + P: render.NewPoint(1025, 2050), + Err: true, + }, + + // Delete a pixel that doesn't exist. + testCase{ + Action: Delete, + P: render.NewPoint(1987, 2006), + Err: true, + }, + + // Delete one that does. + testCase{ + Action: Delete, + P: render.NewPoint(1024, 2048), + }, + + // Verify gone + testCase{ + Action: Get, + P: render.NewPoint(1024, 2048), + Err: true, + }, + } + + for _, test := range tests { + var px *level.Swatch + var err error + switch test.Action { + case Get: + px, err = a.Get(test.P) + case Set: + err = a.Set(test.P, test.S) + case Delete: + err = a.Delete(test.P) + } + + if err != nil && !test.Err { + t.Errorf("unexpected error from %s %s: %s", test.Action, test.P, err) + continue + } else if err == nil && test.Err { + t.Errorf("didn't get error when we expected from %s %s", test.Action, test.P) + continue + } + + if test.Action == Get { + if px != test.Expect { + t.Errorf("didn't get expected result\n"+ + "Expected: %s\n"+ + " Got: %s\n", + test.Expect, + px, + ) + } + } + } +} + +// Test the ChunkCoordinate function. +func TestChunkCoordinates(t *testing.T) { + c := level.NewChunker(1000) + + type testCase struct { + In render.Point + Expect render.Point + } + tests := []testCase{ + testCase{ + In: render.NewPoint(0, 0), + Expect: render.NewPoint(0, 0), + }, + testCase{ + In: render.NewPoint(128, 128), + Expect: render.NewPoint(0, 0), + }, + testCase{ + In: render.NewPoint(1024, 128), + Expect: render.NewPoint(1, 0), + }, + testCase{ + In: render.NewPoint(3600, 1228), + Expect: render.NewPoint(3, 1), + }, + testCase{ + In: render.NewPoint(-100, -1), + Expect: render.NewPoint(-1, -1), + }, + testCase{ + In: render.NewPoint(-950, 100), + Expect: render.NewPoint(-1, 0), + }, + testCase{ + In: render.NewPoint(-1001, -856), + Expect: render.NewPoint(-2, -1), + }, + testCase{ + In: render.NewPoint(-3600, -4800), + Expect: render.NewPoint(-4, -5), + }, + } + + for _, test := range tests { + actual := c.ChunkCoordinate(test.In) + if actual != test.Expect { + t.Errorf( + "Failed ChunkCoordinate conversion:\n"+ + " Input: %s\n"+ + "Expected: %s\n"+ + " Got: %s", + test.In, + test.Expect, + actual, + ) + } + } +} + +func TestZeroChunkSize(t *testing.T) { + c := &level.Chunker{} + + coord := c.ChunkCoordinate(render.NewPoint(1200, 3600)) + if !coord.IsZero() { + t.Errorf("ChunkCoordinate didn't fail with a zero chunk size!") + } +} diff --git a/level/chunker.go b/level/chunker.go new file mode 100644 index 0000000..c2b0afb --- /dev/null +++ b/level/chunker.go @@ -0,0 +1,145 @@ +package level + +import ( + "encoding/json" + "fmt" + "math" + + "git.kirsle.net/apps/doodle/render" +) + +// Chunker is the data structure that manages the chunks of a level, and +// provides the API to interact with the pixels using their absolute coordinates +// while abstracting away the underlying details. +type Chunker struct { + Size int `json:"size"` + Chunks ChunkMap `json:"chunks"` +} + +// NewChunker creates a new chunk manager with a given chunk size. +func NewChunker(size int) *Chunker { + return &Chunker{ + Size: size, + Chunks: ChunkMap{}, + } +} + +// Inflate iterates over the pixels in the (loaded) chunks and expands any +// Sparse Swatches (which have only their palette index, from the file format +// on disk) to connect references to the swatches in the palette. +func (c *Chunker) Inflate(pal *Palette) error { + for coord, chunk := range c.Chunks { + log.Debug("Chunker.Inflate: expanding chunk %s %+v", coord, chunk) + chunk.Inflate(pal) + } + return nil +} + +// IterViewport returns a channel to iterate every point that exists within +// the viewport rect. +func (c *Chunker) IterViewport(viewport render.Rect) <-chan Pixel { + pipe := make(chan Pixel) + go func() { + // Get the chunk box coordinates. + var ( + topLeft = c.ChunkCoordinate(render.NewPoint(viewport.X, viewport.Y)) + bottomRight = c.ChunkCoordinate(render.Point{ + X: viewport.X + viewport.W, + Y: viewport.Y + viewport.H, + }) + ) + for cx := topLeft.X; cx <= bottomRight.X; cx++ { + for cy := topLeft.Y; cy <= bottomRight.Y; cy++ { + if chunk, ok := c.GetChunk(render.NewPoint(cx, cy)); ok { + for px := range chunk.Iter() { + pipe <- px + } + } + } + } + close(pipe) + }() + return pipe +} + +// IterPixels returns a channel to iterate over every pixel in the entire +// chunker. +func (c *Chunker) IterPixels() <-chan Pixel { + pipe := make(chan Pixel) + go func() { + for _, chunk := range c.Chunks { + for px := range chunk.Iter() { + pipe <- px + } + } + close(pipe) + }() + return pipe +} + +// GetChunk gets a chunk at a certain position. Returns false if not found. +func (c *Chunker) GetChunk(p render.Point) (*Chunk, bool) { + chunk, ok := c.Chunks[p] + return chunk, ok +} + +// Get a pixel at the given coordinate. Returns the Palette entry for that +// pixel or else returns an error if not found. +func (c *Chunker) Get(p render.Point) (*Swatch, error) { + // Compute the chunk coordinate. + coord := c.ChunkCoordinate(p) + if chunk, ok := c.Chunks[coord]; ok { + return chunk.Get(p) + } + return nil, fmt.Errorf("no chunk %s exists for point %s", coord, p) +} + +// Set a pixel at the given coordinate. +func (c *Chunker) Set(p render.Point, sw *Swatch) error { + coord := c.ChunkCoordinate(p) + chunk, ok := c.Chunks[coord] + if !ok { + chunk = NewChunk() + c.Chunks[coord] = chunk + } + + return chunk.Set(p, sw) +} + +// Delete a pixel at the given coordinate. +func (c *Chunker) Delete(p render.Point) error { + coord := c.ChunkCoordinate(p) + if chunk, ok := c.Chunks[coord]; ok { + return chunk.Delete(p) + } + return fmt.Errorf("no chunk %s exists for point %s", coord, p) +} + +// ChunkCoordinate computes a chunk coordinate from an absolute coordinate. +func (c *Chunker) ChunkCoordinate(abs render.Point) render.Point { + if c.Size == 0 { + return render.Point{} + } + + size := float64(c.Size) + return render.NewPoint( + int32(math.Floor(float64(abs.X)/size)), + int32(math.Floor(float64(abs.Y)/size)), + ) +} + +// ChunkMap maps a chunk coordinate to its chunk data. +type ChunkMap map[render.Point]*Chunk + +// MarshalJSON to convert the chunk map to JSON. This is needed for writing so +// the JSON encoder knows how to serializes a `map[Point]*Chunk` but the inverse +// is not necessary to implement. +func (c ChunkMap) MarshalJSON() ([]byte, error) { + dict := map[string]*Chunk{} + for point, chunk := range c { + dict[point.String()] = chunk + } + + out, err := json.Marshal(dict) + return out, err +} diff --git a/level/json.go b/level/json.go index f5a8385..324bbf1 100644 --- a/level/json.go +++ b/level/json.go @@ -16,6 +16,19 @@ func (m *Level) ToJSON() ([]byte, error) { return out.Bytes(), err } +// WriteJSON writes a level to JSON on disk. +func (m *Level) WriteJSON(filename string) error { + fh, err := os.Create(filename) + if err != nil { + return fmt.Errorf("Level.WriteJSON(%s): failed to create file: %s", filename, err) + } + defer fh.Close() + + _ = fh + + return nil +} + // LoadJSON loads a map from JSON file. func LoadJSON(filename string) (*Level, error) { fh, err := os.Open(filename) @@ -24,13 +37,17 @@ func LoadJSON(filename string) (*Level, error) { } defer fh.Close() + // Decode the JSON file from disk. m := New() decoder := json.NewDecoder(fh) err = decoder.Decode(&m) if err != nil { - return m, err + return m, fmt.Errorf("level.LoadJSON: JSON decode error: %s", err) } + // Inflate the chunk metadata to map the pixels to their palette indexes. + m.Chunker.Inflate(m.Palette) + // Inflate the private instance values. m.Palette.Inflate() for _, px := range m.Pixels { diff --git a/level/palette.go b/level/palette.go index c74d9f5..3061760 100644 --- a/level/palette.go +++ b/level/palette.go @@ -1,8 +1,6 @@ package level import ( - "fmt" - "git.kirsle.net/apps/doodle/render" ) @@ -50,30 +48,6 @@ type Palette struct { byName map[string]int // Cache map of swatches by name } -// Swatch holds details about a single value in the palette. -type Swatch struct { - Name string `json:"name"` - Color render.Color `json:"color"` - - // Optional attributes. - Solid bool `json:"solid,omitempty"` - Fire bool `json:"fire,omitempty"` - Water bool `json:"water,omitempty"` - - // Private runtime attributes. - index int // position in the Palette, for reverse of `Palette.byName` -} - -func (s Swatch) String() string { - return s.Name -} - -// Index returns the Swatch's position in the palette. -func (s Swatch) Index() int { - fmt.Printf("%+v index: %d", s, s.index) - return s.index -} - // Inflate the palette swatch caches. Always call this method after you have // initialized the palette (i.e. loaded it from JSON); this will update the // "color by name" cache and assign the index numbers to each swatch. diff --git a/level/swatch.go b/level/swatch.go new file mode 100644 index 0000000..2dc1c46 --- /dev/null +++ b/level/swatch.go @@ -0,0 +1,56 @@ +package level + +import ( + "fmt" + + "git.kirsle.net/apps/doodle/render" +) + +// Swatch holds details about a single value in the palette. +type Swatch struct { + Name string `json:"name"` + Color render.Color `json:"color"` + + // Optional attributes. + Solid bool `json:"solid,omitempty"` + Fire bool `json:"fire,omitempty"` + Water bool `json:"water,omitempty"` + + // Private runtime attributes. + index int // position in the Palette, for reverse of `Palette.byName` + + // When the swatch is loaded from JSON we only get the index number, and + // need to expand out the swatch later when the palette is loaded. + paletteIndex int + isSparse bool +} + +// NewSparseSwatch creates a sparse Swatch from a palette index that will need +// later expansion, when loading drawings from disk. +func NewSparseSwatch(paletteIndex int) *Swatch { + return &Swatch{ + isSparse: true, + paletteIndex: paletteIndex, + } +} + +func (s Swatch) String() string { + if s.isSparse { + return fmt.Sprintf("Swatch", s.paletteIndex) + } + if s.Name == "" { + return s.Color.String() + } + return s.Name +} + +// IsSparse returns whether this Swatch is sparse (has only a palette index) and +// requires inflation. +func (s *Swatch) IsSparse() bool { + return s.isSparse +} + +// Index returns the Swatch's position in the palette. +func (s *Swatch) Index() int { + return s.index +} diff --git a/level/types.go b/level/types.go index 9c9f72c..40e8f96 100644 --- a/level/types.go +++ b/level/types.go @@ -3,18 +3,24 @@ package level import ( "encoding/json" "fmt" + + "git.kirsle.net/apps/doodle/balance" + "git.kirsle.net/apps/doodle/render" ) // Level is the container format for Doodle map drawings. type Level struct { - Version int32 `json:"version"` // File format version spec. + 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 size. + // Chunked pixel data. + Chunker *Chunker `json:"chunks"` + + // XXX: deprecated? Width int32 `json:"w"` Height int32 `json:"h"` @@ -31,6 +37,7 @@ type Level struct { func New() *Level { return &Level{ Version: 1, + Chunker: NewChunker(balance.ChunkSize), Pixels: []*Pixel{}, Palette: &Palette{}, } @@ -51,6 +58,14 @@ func (p Pixel) String() string { return fmt.Sprintf("Pixel<%s '%s' (%d,%d)>", p.Swatch.Color, p.Swatch.Name, p.X, p.Y) } +// Point returns the pixel's point. +func (p Pixel) Point() render.Point { + return render.Point{ + X: p.X, + Y: p.Y, + } +} + // MarshalJSON serializes a Pixel compactly as a simple list. func (p Pixel) MarshalJSON() ([]byte, error) { return []byte(fmt.Sprintf( diff --git a/log.go b/log.go index 7eddc71..c0c87bf 100644 --- a/log.go +++ b/log.go @@ -7,8 +7,9 @@ var log *golog.Logger func init() { log = golog.GetLogger("doodle") log.Configure(&golog.Config{ - Level: golog.DebugLevel, - Theme: golog.DarkTheme, - Colors: golog.ExtendedColor, + Level: golog.DebugLevel, + Theme: golog.DarkTheme, + Colors: golog.ExtendedColor, + TimeFormat: "2006-01-02 15:04:05.000000", }) } diff --git a/play_scene.go b/play_scene.go index b89adf4..c4082d3 100644 --- a/play_scene.go +++ b/play_scene.go @@ -63,7 +63,7 @@ func (s *PlayScene) Loop(d *Doodle, ev *events.State) error { if ev.KeyName.Read() == "e" { log.Info("Edit Mode, Go!") d.Goto(&EditorScene{ - Canvas: s.canvas, + // Canvas: s.canvas, }) return nil } diff --git a/render/interface.go b/render/interface.go index dbef992..6e0698a 100644 --- a/render/interface.go +++ b/render/interface.go @@ -38,46 +38,6 @@ type Engine interface { Loop() error // maybe? } -// Point holds an X,Y coordinate value. -type Point struct { - X int32 - Y int32 -} - -// NewPoint makes a new Point at an X,Y coordinate. -func NewPoint(x, y int32) Point { - return Point{ - X: x, - Y: y, - } -} - -func (p Point) String() string { - return fmt.Sprintf("Point<%d,%d>", p.X, p.Y) -} - -// IsZero returns if the point is the zero value. -func (p Point) IsZero() bool { - return p.X == 0 && p.Y == 0 -} - -// Inside returns whether the Point falls inside the rect. -func (p Point) Inside(r Rect) bool { - var ( - x1 = r.X - y1 = r.Y - x2 = r.X + r.W - y2 = r.Y + r.H - ) - return p.X >= x1 && p.X <= x2 && p.Y >= y1 && p.Y <= y2 -} - -// Add (or subtract) the other point to your current point. -func (p *Point) Add(other Point) { - p.X += other.X - p.Y += other.Y -} - // Rect has a coordinate and a width and height. type Rect struct { X int32 diff --git a/render/point.go b/render/point.go new file mode 100644 index 0000000..dcb16cf --- /dev/null +++ b/render/point.go @@ -0,0 +1,94 @@ +package render + +import ( + "fmt" + "strconv" + "strings" +) + +// Point holds an X,Y coordinate value. +type Point struct { + X int32 + Y int32 +} + +// NewPoint makes a new Point at an X,Y coordinate. +func NewPoint(x, y int32) Point { + return Point{ + X: x, + Y: y, + } +} + +func (p Point) String() string { + return fmt.Sprintf("%d,%d", p.X, p.Y) +} + +// ParsePoint to parse a point from its string representation. +func ParsePoint(v string) (Point, error) { + halves := strings.Split(v, ",") + if len(halves) != 2 { + return Point{}, fmt.Errorf("'%s': not a valid coordinate string", v) + } + x, errX := strconv.Atoi(halves[0]) + y, errY := strconv.Atoi(halves[1]) + if errX != nil || errY != nil { + return Point{}, fmt.Errorf("invalid coordinate string (X: %v; Y: %v)", + errX, + errY, + ) + } + return Point{ + X: int32(x), + Y: int32(y), + }, nil +} + +// IsZero returns if the point is the zero value. +func (p Point) IsZero() bool { + return p.X == 0 && p.Y == 0 +} + +// Inside returns whether the Point falls inside the rect. +func (p Point) Inside(r Rect) bool { + var ( + x1 = r.X + y1 = r.Y + x2 = r.X + r.W + y2 = r.Y + r.H + ) + return p.X >= x1 && p.X <= x2 && p.Y >= y1 && p.Y <= y2 +} + +// Add (or subtract) the other point to your current point. +func (p *Point) Add(other Point) { + p.X += other.X + p.Y += other.Y +} + +// MarshalText to convert the point into text so that a render.Point may be used +// as a map key and serialized to JSON. +func (p *Point) MarshalText() ([]byte, error) { + return []byte(fmt.Sprintf("%d,%d", p.X, p.Y)), nil +} + +// UnmarshalText to restore it from text. +func (p *Point) UnmarshalText(b []byte) error { + halves := strings.Split(strings.Trim(string(b), `"`), ",") + if len(halves) != 2 { + return fmt.Errorf("'%s': not a valid coordinate string", b) + } + + x, errX := strconv.Atoi(halves[0]) + y, errY := strconv.Atoi(halves[1]) + if errX != nil || errY != nil { + return fmt.Errorf("Point.UnmarshalJSON: Atoi errors (X=%s Y=%s)", + errX, + errY, + ) + } + + p.X = int32(x) + p.Y = int32(y) + return nil +} diff --git a/shell.go b/shell.go index c5f44f2..36e7f1c 100644 --- a/shell.go +++ b/shell.go @@ -13,6 +13,7 @@ import ( // Flash a message to the user. func (d *Doodle) Flash(template string, v ...interface{}) { + log.Warn(template, v...) d.shell.Write(fmt.Sprintf(template, v...)) }