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:
parent
e25869644c
commit
a7fd3aa1ca
|
@ -7,4 +7,7 @@ var (
|
|||
|
||||
// Default chunk size for canvases.
|
||||
ChunkSize = 1000
|
||||
|
||||
// Default size for a new Doodad.
|
||||
DoodadSize = 100
|
||||
)
|
||||
|
|
|
@ -43,7 +43,7 @@ func main() {
|
|||
app.SetupEngine()
|
||||
if filename != "" {
|
||||
if edit {
|
||||
app.EditLevel(filename)
|
||||
app.EditDrawing(filename)
|
||||
} else {
|
||||
app.PlayLevel(filename)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
78
doodads/actor.go
Normal file
78
doodads/actor.go
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
48
doodads/doodad.go
Normal file
48
doodads/doodad.go
Normal 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
54
doodads/json.go
Normal 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
|
||||
}
|
37
doodle.go
37
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
|
||||
}
|
||||
|
|
134
editor_scene.go
134
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.
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
53
editor_ui.go
53
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
|
||||
}
|
||||
})
|
||||
},
|
||||
|
|
2
fps.go
2
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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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{},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
Loading…
Reference in New Issue
Block a user