diff --git a/balance/shell.go b/balance/shell.go index 2be7193..c0d6494 100644 --- a/balance/shell.go +++ b/balance/shell.go @@ -8,10 +8,10 @@ var ( ShellBackgroundColor = render.Color{0, 10, 20, 128} ShellForegroundColor = render.White ShellPadding int32 = 8 - ShellFontSize = 14 + ShellFontSize = 16 ShellCursorBlinkRate uint64 = 20 ShellHistoryLineCount = 8 // Ticks that a flashed message persists for. - FlashTTL uint64 = 200 + FlashTTL uint64 = 400 ) diff --git a/doodads/doodads.go b/doodads/doodads.go new file mode 100644 index 0000000..cf673ef --- /dev/null +++ b/doodads/doodads.go @@ -0,0 +1,113 @@ +package doodads + +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 + + // 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 { + X int32 + Y int32 + W int32 + H int32 + Top bool + Left bool + Right bool + Bottom bool +} + +// CollidesWithGrid checks if a Doodad collides with level geometry. +func CollidesWithGrid(d Doodad, grid *render.Grid) (Collide, bool) { + var ( + P = d.Position() + S = d.Size() + topLeft = P + topRight = render.Point{ + X: P.X + S.W, + Y: P.Y, + } + bottomLeft = render.Point{ + X: P.X, + Y: P.Y + S.H, + } + bottomRight = render.Point{ + X: bottomLeft.X + S.W, + Y: P.Y + S.H, + } + ) + + // Bottom edge. + for point := range render.IterLine2(bottomLeft, bottomRight) { + if grid.Exists(render.Pixel{ + X: point.X, + Y: point.Y, + }) { + return Collide{ + Bottom: true, + X: point.X, + Y: point.Y, + }, true + } + } + + // Top edge. + for point := range render.IterLine2(topLeft, topRight) { + if grid.Exists(render.Pixel{ + X: point.X, + Y: point.Y, + }) { + return Collide{ + Top: true, + X: point.X, + Y: point.Y, + }, true + } + } + + for point := range render.IterLine2(topLeft, bottomLeft) { + if grid.Exists(render.Pixel{ + X: point.X, + Y: point.Y, + }) { + return Collide{ + Left: true, + X: point.X, + Y: point.Y, + }, true + } + } + + for point := range render.IterLine2(topRight, bottomRight) { + if grid.Exists(render.Pixel{ + X: point.X, + Y: point.Y, + }) { + return Collide{ + Right: true, + X: point.X, + Y: point.Y, + }, true + } + } + + return Collide{}, false +} diff --git a/doodads/player.go b/doodads/player.go new file mode 100644 index 0000000..3f3372f --- /dev/null +++ b/doodads/player.go @@ -0,0 +1,70 @@ +package doodads + +import ( + "git.kirsle.net/apps/doodle/render" +) + +// PlayerID is the Doodad ID for the player character. +const PlayerID = "PLAYER" + +// Player is a special doodad for the player character. +type Player struct { + point render.Point + velocity render.Point + size render.Rect +} + +// NewPlayer creates the special Player Character doodad. +func NewPlayer() *Player { + return &Player{ + point: render.Point{ + X: 100, + Y: 100, + }, + size: render.Rect{ + W: 16, + H: 16, + }, + } +} + +// ID of the Player singleton. +func (p *Player) ID() string { + return PlayerID +} + +// Position of the player. +func (p *Player) Position() render.Point { + return p.point +} + +// MoveBy a relative delta position. +func (p *Player) MoveBy(by render.Point) { + p.point.X += by.X + p.point.Y += by.Y +} + +// MoveTo an absolute position. +func (p *Player) MoveTo(to render.Point) { + p.point = to +} + +// Velocity returns the player's current velocity. +func (p *Player) Velocity() render.Point { + return p.velocity +} + +// Size returns the player's size. +func (p *Player) Size() render.Rect { + return p.size +} + +// Draw the player sprite. +func (p *Player) Draw(e render.Engine) { + e.DrawRect(render.Magenta, render.Rect{ + X: p.point.X, + Y: p.point.Y, + W: p.size.W, + H: p.size.H, + }) +} diff --git a/doodle.go b/doodle.go index ac86522..79c8dda 100644 --- a/doodle.go +++ b/doodle.go @@ -1,7 +1,6 @@ package doodle import ( - "fmt" "time" "git.kirsle.net/apps/doodle/render" @@ -167,30 +166,9 @@ func (d *Doodle) EditLevel(filename string) error { // PlayLevel loads a map from JSON into the PlayScene. func (d *Doodle) PlayLevel(filename string) error { log.Info("Loading level from file: %s", filename) - scene := &PlayScene{} - err := scene.LoadLevel(filename) - if err != nil { - return err + scene := &PlayScene{ + Filename: filename, } d.Goto(scene) return nil } - -// Pixel TODO: not a global -type Pixel struct { - start bool - x int32 - y int32 - dx int32 - dy int32 -} - -func (p Pixel) String() string { - return fmt.Sprintf("(%d,%d) delta (%d,%d)", - p.x, p.y, - p.dx, p.dy, - ) -} - -// Grid is a 2D grid of pixels in X,Y notation. -type Grid map[Pixel]interface{} diff --git a/draw/line.go b/draw/line.go index 45dfab5..a174164 100644 --- a/draw/line.go +++ b/draw/line.go @@ -3,13 +3,13 @@ package draw import ( "math" - "git.kirsle.net/apps/doodle/types" + "git.kirsle.net/apps/doodle/render" ) // Line is a generator that returns the X,Y coordinates to draw a line. // https://en.wikipedia.org/wiki/Digital_differential_analyzer_(graphics_algorithm) -func Line(x1, y1, x2, y2 int32) chan types.Point { - generator := make(chan types.Point) +func Line(x1, y1, x2, y2 int32) chan render.Point { + generator := make(chan render.Point) go func() { var ( @@ -28,7 +28,7 @@ func Line(x1, y1, x2, y2 int32) chan types.Point { x := float64(x1) y := float64(y1) for i := 0; i <= int(step); i++ { - generator <- types.Point{ + generator <- render.Point{ X: int32(x), Y: int32(y), } diff --git a/draw/line_test.go b/draw/line_test.go index d796ae3..eb1cabb 100644 --- a/draw/line_test.go +++ b/draw/line_test.go @@ -5,7 +5,7 @@ import ( "testing" "git.kirsle.net/apps/doodle/draw" - "git.kirsle.net/apps/doodle/types" + "git.kirsle.net/apps/doodle/render" ) func TestLine(t *testing.T) { @@ -14,7 +14,7 @@ func TestLine(t *testing.T) { X2 int32 Y1 int32 Y2 int32 - Expect []types.Point + Expect []render.Point } toString := func(t task) string { return fmt.Sprintf("Line<%d,%d -> %d,%d>", @@ -29,7 +29,7 @@ func TestLine(t *testing.T) { Y1: 0, X2: 0, Y2: 10, - Expect: []types.Point{ + Expect: []render.Point{ {X: 0, Y: 0}, {X: 0, Y: 1}, {X: 0, Y: 2}, @@ -48,7 +48,7 @@ func TestLine(t *testing.T) { Y1: 10, X2: 15, Y2: 15, - Expect: []types.Point{ + Expect: []render.Point{ {X: 10, Y: 10}, {X: 11, Y: 11}, {X: 12, Y: 12}, diff --git a/editor_scene.go b/editor_scene.go index dbed66a..b71e21c 100644 --- a/editor_scene.go +++ b/editor_scene.go @@ -16,9 +16,14 @@ import ( // EditorScene manages the "Edit Level" game mode. type EditorScene struct { + // Configuration for the scene initializer. + OpenFile bool + Filename string + Canvas render.Grid + // History of all the pixels placed by the user. - pixelHistory []Pixel - canvas Grid + pixelHistory []render.Pixel + canvas render.Grid filename string // Last saved filename. // Canvas size @@ -33,11 +38,32 @@ func (s *EditorScene) Name() string { // Setup the editor scene. func (s *EditorScene) Setup(d *Doodle) error { + // Were we given configuration data? + if s.Filename != "" { + log.Debug("EditorScene: Set filename to %s", s.Filename) + s.filename = s.Filename + s.Filename = "" + if s.OpenFile { + log.Debug("EditorScene: Loading map from filename at %s", s.filename) + if err := s.LoadLevel(s.filename); err != nil { + return err + } + } + } + if s.Canvas != nil { + log.Debug("EditorScene: Received Canvas from caller") + s.canvas = s.Canvas + s.Canvas = nil + } + + d.Flash("Editor Mode. Press 'P' to play this map.") + if s.pixelHistory == nil { - s.pixelHistory = []Pixel{} + s.pixelHistory = []render.Pixel{} } if s.canvas == nil { - s.canvas = Grid{} + log.Debug("EditorScene: Setting default canvas to an empty grid") + s.canvas = render.Grid{} } s.width = d.width // TODO: canvas width = copy the window size s.height = d.height @@ -52,31 +78,45 @@ func (s *EditorScene) Loop(d *Doodle, ev *events.State) error { s.Screenshot() } + // Switching to Play Mode? + if ev.KeyName.Read() == "p" { + log.Info("Play Mode, Go!") + d.Goto(&PlayScene{ + Canvas: s.canvas, + }) + return nil + } + // Clear the canvas and fill it with white. d.Engine.Clear(render.White) // Clicking? Log all the pixels while doing so. if ev.Button1.Now { - pixel := Pixel{ - start: ev.Button1.Pressed(), - x: ev.CursorX.Now, - y: ev.CursorY.Now, - dx: ev.CursorX.Now, - dy: ev.CursorY.Now, + log.Warn("Button1: %+v", ev.Button1) + pixel := render.Pixel{ + Start: ev.Button1.Pressed(), + X: ev.CursorX.Now, + Y: ev.CursorY.Now, + DX: ev.CursorX.Last, + DY: ev.CursorY.Last, + } + if pixel.Start { + log.Warn("START PIXEL %+v", pixel) } // Append unique new pixels. if len(s.pixelHistory) == 0 || s.pixelHistory[len(s.pixelHistory)-1] != pixel { // If not a start pixel, make the delta coord the previous one. - if !pixel.start && len(s.pixelHistory) > 0 { + if !pixel.Start && len(s.pixelHistory) > 0 { prev := s.pixelHistory[len(s.pixelHistory)-1] - pixel.dx = prev.x - pixel.dy = prev.y + pixel.DY = prev.Y + pixel.DX = prev.X } s.pixelHistory = append(s.pixelHistory, pixel) // Save in the pixel canvas map. + fmt.Printf("%+v", pixel) s.canvas[pixel] = nil } } @@ -86,24 +126,26 @@ func (s *EditorScene) Loop(d *Doodle, ev *events.State) error { // Draw the current frame. func (s *EditorScene) Draw(d *Doodle) error { - for i, pixel := range s.pixelHistory { - if !pixel.start && i > 0 { - prev := s.pixelHistory[i-1] - if prev.x == pixel.x && prev.y == pixel.y { - d.Engine.DrawPoint( - render.Black, - render.Point{pixel.x, pixel.y}, - ) - } else { - d.Engine.DrawLine( - render.Black, - render.Point{pixel.x, pixel.y}, - render.Point{prev.x, prev.y}, - ) - } - } - d.Engine.DrawPoint(render.Black, render.Point{pixel.x, pixel.y}) - } + // for i, pixel := range s.pixelHistory { + // if !pixel.Start && i > 0 { + // prev := s.pixelHistory[i-1] + // if prev.X == pixel.X && prev.Y == pixel.Y { + // d.Engine.DrawPoint( + // render.Black, + // render.Point{pixel.X, pixel.Y}, + // ) + // } else { + // d.Engine.DrawLine( + // render.Black, + // render.Point{pixel.X, pixel.Y}, + // render.Point{prev.X, prev.Y}, + // ) + // } + // } + // d.Engine.DrawPoint(render.Black, render.Point{pixel.X, pixel.Y}) + // } + + s.canvas.Draw(d.Engine) return nil } @@ -111,8 +153,8 @@ func (s *EditorScene) Draw(d *Doodle) error { // LoadLevel loads a level from disk. func (s *EditorScene) LoadLevel(filename string) error { s.filename = filename - s.pixelHistory = []Pixel{} - s.canvas = Grid{} + s.pixelHistory = []render.Pixel{} + s.canvas = render.Grid{} m, err := level.LoadJSON(filename) if err != nil { @@ -120,12 +162,12 @@ func (s *EditorScene) LoadLevel(filename string) error { } for _, point := range m.Pixels { - pixel := Pixel{ - start: true, - x: point.X, - y: point.Y, - dx: point.X, - dy: point.Y, + pixel := render.Pixel{ + Start: true, + X: point.X, + Y: point.Y, + DX: point.X, + DY: point.Y, } s.pixelHistory = append(s.pixelHistory, pixel) s.canvas[pixel] = nil @@ -153,12 +195,20 @@ func (s *EditorScene) SaveLevel(filename string) { } for pixel := range s.canvas { - for point := range draw.Line(pixel.x, pixel.y, pixel.dx, pixel.dy) { + if pixel.DX == 0 && pixel.DY == 0 { m.Pixels = append(m.Pixels, level.Pixel{ - X: point.X, - Y: point.Y, + X: pixel.X, + Y: pixel.Y, Palette: 0, }) + } else { + for point := range render.IterLine(pixel.X, pixel.Y, pixel.DX, pixel.DY) { + m.Pixels = append(m.Pixels, level.Pixel{ + X: point.X, + Y: point.Y, + Palette: 0, + }) + } } } @@ -189,10 +239,10 @@ func (s *EditorScene) Screenshot() { // Fill in the dots we drew. for pixel := range s.canvas { // A line or a dot? - if pixel.x == pixel.dx && pixel.y == pixel.dy { - screenshot.Set(int(pixel.x), int(pixel.y), image.Black) + if pixel.DX == 0 && pixel.DY == 0 { + screenshot.Set(int(pixel.X), int(pixel.Y), image.Black) } else { - for point := range draw.Line(pixel.x, pixel.y, pixel.dx, pixel.dy) { + for point := range draw.Line(pixel.X, pixel.Y, pixel.DX, pixel.DY) { screenshot.Set(int(point.X), int(point.Y), image.Black) } } @@ -223,3 +273,8 @@ func (s *EditorScene) Screenshot() { return } } + +// Destroy the scene. +func (s *EditorScene) Destroy() error { + return nil +} diff --git a/level/types.go b/level/types.go index 3709e79..9bfc1e8 100644 --- a/level/types.go +++ b/level/types.go @@ -7,11 +7,12 @@ import ( // Level is the container format for Doodle map drawings. type Level struct { - Version int32 `json:"version"` // File format version spec. - Title string `json:"title"` - Author string `json:"author"` - Password string `json:"passwd"` - Locked bool `json:"locked"` + Version int32 `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. Width int32 `json:"w"` diff --git a/play_scene.go b/play_scene.go index 37aeee4..ce1ac55 100644 --- a/play_scene.go +++ b/play_scene.go @@ -1,6 +1,7 @@ package doodle import ( + "git.kirsle.net/apps/doodle/doodads" "git.kirsle.net/apps/doodle/events" "git.kirsle.net/apps/doodle/level" "git.kirsle.net/apps/doodle/render" @@ -8,17 +9,19 @@ import ( // PlayScene manages the "Edit Level" game mode. type PlayScene struct { - canvas Grid + // Configuration attributes. + Filename string + Canvas render.Grid + + // Private variables. + canvas render.Grid // Canvas size width int32 height int32 - // Player position and velocity. - x int32 - y int32 - vx int32 - vy int32 + // Player character + player doodads.Doodad } // Name of the scene. @@ -28,19 +31,43 @@ func (s *PlayScene) Name() string { // Setup the play scene. func (s *PlayScene) Setup(d *Doodle) error { - s.x = 10 - s.y = 10 + // Given a filename or map data to play? + if s.Canvas != nil { + log.Debug("PlayScene.Setup: loading map from given canvas") + s.canvas = s.Canvas + + } else if s.Filename != "" { + log.Debug("PlayScene.Setup: loading map from file %s", s.Filename) + s.LoadLevel(s.Filename) + s.Filename = "" + } + + s.player = doodads.NewPlayer() if s.canvas == nil { - s.canvas = Grid{} + log.Debug("PlayScene.Setup: no grid given, initializing empty grid") + s.canvas = render.Grid{} } + s.width = d.width // TODO: canvas width = copy the window size s.height = d.height + + d.Flash("Entered Play Mode. Press 'E' to edit this map.") + return nil } // Loop the editor scene. func (s *PlayScene) Loop(d *Doodle, ev *events.State) error { + // Switching to Edit Mode? + if ev.KeyName.Read() == "e" { + log.Info("Edit Mode, Go!") + d.Goto(&EditorScene{ + Canvas: s.canvas, + }) + return nil + } + s.movePlayer(ev) return nil } @@ -50,35 +77,53 @@ func (s *PlayScene) Draw(d *Doodle) error { // Clear the canvas and fill it with white. d.Engine.Clear(render.White) - for pixel := range s.canvas { - d.Engine.DrawPoint(render.Black, render.Point{pixel.x, pixel.y}) - } + s.canvas.Draw(d.Engine) // Draw our hero. - d.Engine.DrawRect(render.Magenta, render.Rect{s.x, s.y, 16, 16}) + s.player.Draw(d.Engine) return nil } // movePlayer updates the player's X,Y coordinate based on key pressed. func (s *PlayScene) movePlayer(ev *events.State) { + delta := s.player.Position() + var playerSpeed int32 = 8 + var gravity int32 = 2 + if ev.Down.Now { - s.y += 4 + delta.Y += playerSpeed } if ev.Left.Now { - s.x -= 4 + delta.X -= playerSpeed } if ev.Right.Now { - s.x += 4 + delta.X += playerSpeed } if ev.Up.Now { - s.y -= 4 + delta.Y -= playerSpeed } + + // Apply gravity. + delta.Y += gravity + + // Draw a ray and check for collision. + var lastOk = s.player.Position() + for point := range render.IterLine2(s.player.Position(), delta) { + s.player.MoveTo(point) + if _, ok := doodads.CollidesWithGrid(s.player, &s.canvas); ok { + s.player.MoveTo(lastOk) + } else { + lastOk = s.player.Position() + } + } + + s.player.MoveTo(lastOk) } // LoadLevel loads a level from disk. func (s *PlayScene) LoadLevel(filename string) error { - s.canvas = Grid{} + s.canvas = render.Grid{} m, err := level.LoadJSON(filename) if err != nil { @@ -86,12 +131,17 @@ func (s *PlayScene) LoadLevel(filename string) error { } for _, point := range m.Pixels { - pixel := Pixel{ - x: point.X, - y: point.Y, + pixel := render.Pixel{ + X: point.X, + Y: point.Y, } s.canvas[pixel] = nil } return nil } + +// Destroy the scene. +func (s *PlayScene) Destroy() error { + return nil +} diff --git a/render/grid.go b/render/grid.go new file mode 100644 index 0000000..ad80a35 --- /dev/null +++ b/render/grid.go @@ -0,0 +1,49 @@ +package render + +import ( + "fmt" +) + +// Pixel TODO: not a global +// TODO get rid of this ugly thing. +type Pixel struct { + Start bool + X int32 + Y int32 + DX int32 + DY int32 +} + +func (p Pixel) String() string { + return fmt.Sprintf("(%d,%d) delta (%d,%d)", + p.X, p.Y, + p.DX, p.DY, + ) +} + +// Grid is a 2D grid of pixels in X,Y notation. +type Grid map[Pixel]interface{} + +// Exists returns true if the point exists on the grid. +func (g *Grid) Exists(p Pixel) bool { + if _, ok := (*g)[p]; ok { + return true + } + return false +} + +// Draw the grid efficiently. +func (g *Grid) Draw(e Engine) { + for pixel := range *g { + if pixel.DX == 0 && pixel.DY == 0 { + e.DrawPoint(Black, Point{ + X: pixel.X, + Y: pixel.Y, + }) + } else { + for point := range IterLine(pixel.X, pixel.Y, pixel.DX, pixel.DY) { + e.DrawPoint(Black, point) + } + } + } +} diff --git a/render/interface.go b/render/interface.go index 8a0e9aa..b4713d1 100644 --- a/render/interface.go +++ b/render/interface.go @@ -2,6 +2,7 @@ package render import ( "fmt" + "math" "git.kirsle.net/apps/doodle/events" ) @@ -103,3 +104,43 @@ var ( Magenta = Color{255, 0, 255, 255} Pink = Color{255, 153, 255, 255} ) + +// IterLine is a generator that returns the X,Y coordinates to draw a line. +// https://en.wikipedia.org/wiki/Digital_differential_analyzer_(graphics_algorithm) +func IterLine(x1, y1, x2, y2 int32) chan Point { + generator := make(chan Point) + + go func() { + var ( + dx = float64(x2 - x1) + dy = float64(y2 - y1) + ) + var step float64 + if math.Abs(dx) >= math.Abs(dy) { + step = math.Abs(dx) + } else { + step = math.Abs(dy) + } + + dx = dx / step + dy = dy / step + x := float64(x1) + y := float64(y1) + for i := 0; i <= int(step); i++ { + generator <- Point{ + X: int32(x), + Y: int32(y), + } + x += dx + y += dy + } + + close(generator) + }() + + return generator +} + +func IterLine2(p1 Point, p2 Point) chan Point { + return IterLine(p1.X, p1.Y, p2.X, p2.Y) +} diff --git a/scene.go b/scene.go index 26e6865..f7d4e78 100644 --- a/scene.go +++ b/scene.go @@ -8,6 +8,7 @@ import "git.kirsle.net/apps/doodle/events" type Scene interface { Name() string Setup(*Doodle) error + Destroy() error // Loop should update the scene's state but not draw anything. Loop(*Doodle, *events.State) error @@ -19,7 +20,11 @@ type Scene interface { // Goto a scene. First it unloads the current scene. func (d *Doodle) Goto(scene Scene) error { - // d.scene.Destroy() + // Teardown existing scene. + if d.scene != nil { + d.scene.Destroy() + } + log.Info("Goto Scene") d.scene = scene return d.scene.Setup(d) diff --git a/shell.go b/shell.go index 0d5d74b..10418de 100644 --- a/shell.go +++ b/shell.go @@ -2,6 +2,7 @@ package doodle import ( "bytes" + "fmt" "strings" "git.kirsle.net/apps/doodle/balance" @@ -9,6 +10,11 @@ import ( "git.kirsle.net/apps/doodle/render" ) +// Flash a message to the user. +func (d *Doodle) Flash(template string, v ...interface{}) { + d.shell.Write(fmt.Sprintf(template, v...)) +} + // Shell implements the developer console in-game. type Shell struct { parent *Doodle