diff --git a/Ideas.md b/Ideas.md index f100945..a087fce 100644 --- a/Ideas.md +++ b/Ideas.md @@ -76,10 +76,10 @@ The major milestones of the game are roughly: * Colors are not tied to behaviors. Each "Swatch" on the palette has its own color and a set of boolean flags for `solid`, `fire` and `water` behaviors. * [ ] User interface to edit (add/remove) swatches from the palette. -* [ ] A Toolbox window with radio buttons to select between various drawing tools. +* [x] A Toolbox window with radio buttons to select between various drawing tools. * [x] Pencil (the default) draws single pixels on the level. - * [ ] Rectangle would draw a rectangular outline. - * [ ] Line would draw a line from point to point. + * [x] Rectangle would draw a rectangular outline. + * [x] Line would draw a line from point to point. * [ ] A way to adjust brush properties: * [ ] Brush size, shape (round or square). * [ ] Tools to toggle "layers" of visibility into your level: @@ -98,11 +98,11 @@ The major milestones of the game are roughly: For creating Doodads in particular: -* [ ] Make a way to enter Edit Mode in either "Level Mode" or "Doodad Mode", +* [x] Make a way to enter Edit Mode in either "Level Mode" or "Doodad Mode", i.e. by a "New Level" or "New Doodad" button. * [ ] Create a "frame manager" window to see and page between the frames of the drawing. -* [ ] Ability to work on canvases with constrained size (including smaller than +* [x] Ability to work on canvases with constrained size (including smaller than your window). This will use a Canvas widget in the UI toolkit as an abstraction layer. Small canvases will be useful for drawing doodads of a fixed size. diff --git a/TODO.md b/TODO.md index b682471..29f5f66 100644 --- a/TODO.md +++ b/TODO.md @@ -47,8 +47,8 @@ - [x] Buttons - [x] Press Button - [x] Sticky Button -- [ ] Switches -- [ ] Doors +- [x] Switches (4 varieties) +- [x] Doors - [x] Locked Doors and Keys - [x] Electric Doors - [x] Trapdoors (all 4 directions) @@ -66,6 +66,7 @@ In addition to those listed above: as a level goal and ends the level. - Doodads "Warp Door A" through "Warp Door D" - The campaign.json would link levels together. +- [ ] Conveyor Belt ## New Ideas @@ -75,3 +76,13 @@ In addition to those listed above: keys only get picked up by player characters and not "any doodad that touches them" - [ ] `` + +## Path to Multiplayer + +* Add a Player abstraction between events and player characters. + * Keyboard keys would update PlayerOne's state with actions (move left, right, jump, etc) + * Possible to have multiple local players (i.e. bound to different keyboard keys, bound to joypads, etc.) +* A NetworkPlayer provides a Player's inputs from over a network. +* Client/server negotiation, protocol + * Client can request chunks from server for local rendering. + * Players send inputs over network sockets. diff --git a/dev-assets/doodads/build.sh b/dev-assets/doodads/build.sh index e06763e..97ce263 100755 --- a/dev-assets/doodads/build.sh +++ b/dev-assets/doodads/build.sh @@ -24,6 +24,23 @@ buttons() { cd .. } +switches() { + cd switches/ + + doodad convert -t "Switch" switch-off.png switch-on.png switch.doodad + doodad convert -t "Floor Switch" down-off.png down-on.png switch-down.doodad + doodad convert -t "Left Switch" left-off.png left-on.png switch-left.doodad + doodad convert -t "Right Switch" right-off.png right-on.png switch-right.doodad + + doodad install-script switch.js switch.doodad + doodad install-script switch.js switch-down.doodad + doodad install-script switch.js switch-left.doodad + doodad install-script switch.js switch-right.doodad + + cp *.doodad ../../../assets/doodads/ + cd .. +} + doors() { cd doors/ @@ -104,6 +121,7 @@ objects() { } buttons +switches doors trapdoors azulians diff --git a/dev-assets/doodads/doors/electric-door.js b/dev-assets/doodads/doors/electric-door.js index cd028b7..7ecdbdb 100644 --- a/dev-assets/doodads/doors/electric-door.js +++ b/dev-assets/doodads/doors/electric-door.js @@ -23,8 +23,8 @@ function main() { }); } else { animating = true; + opened = false; Self.PlayAnimation("close", function() { - opened = false; animating = false; }) } diff --git a/dev-assets/doodads/switches/down-off.png b/dev-assets/doodads/switches/down-off.png new file mode 100644 index 0000000..2b08ca0 Binary files /dev/null and b/dev-assets/doodads/switches/down-off.png differ diff --git a/dev-assets/doodads/switches/down-on.png b/dev-assets/doodads/switches/down-on.png new file mode 100644 index 0000000..527b2a0 Binary files /dev/null and b/dev-assets/doodads/switches/down-on.png differ diff --git a/dev-assets/doodads/switches/left-off.png b/dev-assets/doodads/switches/left-off.png new file mode 100644 index 0000000..33acceb Binary files /dev/null and b/dev-assets/doodads/switches/left-off.png differ diff --git a/dev-assets/doodads/switches/left-on.png b/dev-assets/doodads/switches/left-on.png new file mode 100644 index 0000000..36d5c45 Binary files /dev/null and b/dev-assets/doodads/switches/left-on.png differ diff --git a/dev-assets/doodads/switches/right-off.png b/dev-assets/doodads/switches/right-off.png new file mode 100644 index 0000000..500e65c Binary files /dev/null and b/dev-assets/doodads/switches/right-off.png differ diff --git a/dev-assets/doodads/switches/right-on.png b/dev-assets/doodads/switches/right-on.png new file mode 100644 index 0000000..c5a196e Binary files /dev/null and b/dev-assets/doodads/switches/right-on.png differ diff --git a/dev-assets/doodads/switches/switch-off.png b/dev-assets/doodads/switches/switch-off.png new file mode 100644 index 0000000..4e7c006 Binary files /dev/null and b/dev-assets/doodads/switches/switch-off.png differ diff --git a/dev-assets/doodads/switches/switch-on.png b/dev-assets/doodads/switches/switch-on.png new file mode 100644 index 0000000..bb191ce Binary files /dev/null and b/dev-assets/doodads/switches/switch-on.png differ diff --git a/dev-assets/doodads/switches/switch.js b/dev-assets/doodads/switches/switch.js new file mode 100644 index 0000000..6a273ee --- /dev/null +++ b/dev-assets/doodads/switches/switch.js @@ -0,0 +1,38 @@ +function main() { + console.log("%s initialized!", Self.Doodad.Title); + + // Switch has two frames: + // 0: Off + // 1: On + + var state = false; + var collide = false; + + Message.Subscribe("power", function(powered) { + state = powered; + showState(state); + }); + + Events.OnCollide(function(e) { + if (collide === false) { + state = !state; + Message.Publish("power", state); + showState(state); + + collide = true; + } + }); + + Events.OnLeave(function(e) { + collide = false; + }); +} + +// showState shows the on/off frame based on the boolean powered state. +function showState(state) { + if (state) { + Self.ShowLayer(1); + } else { + Self.ShowLayer(0); + } +} diff --git a/docs/Doodad Ideas.md b/docs/Doodad Ideas.md new file mode 100644 index 0000000..b78d517 --- /dev/null +++ b/docs/Doodad Ideas.md @@ -0,0 +1,22 @@ +# Doodad Ideas and Implementation Notes + +## Crumbly Floor + +A rectangular floor piece with lines indicating cracks. Most similar to: +the break-away floors in Tomb Raider. + +Animation frames/states: + +* Default: a rectangle with jagged lines through it indicating cracks. +* Rumble: draw little rumble mark lines and maybe shake the segments around. +* Break: the floor breaks apart and pieces fall/shrink into nothing over a few + frames of animation. + +Behavior: + +* If touched, act as a solid object. +* If touched along its top edge, start the Rumble animation. If touched from + the bottom, don't do anything, just act solid. +* After a moment of rumbling, stop acting solid and play the break animation. + A player standing on top of the floor falls through it now. +* When the broken floor scrolls out of view it resets. diff --git a/docs/Shell.md b/docs/Shell.md index 9868442..1e92b2f 100644 --- a/docs/Shell.md +++ b/docs/Shell.md @@ -2,6 +2,43 @@ ## Cheats -| Cheat Code | Effect | -|-------------------|--------------------------------| -| unleash the beast | Disable frame rate throttling. | +* `unleash the beast` - disable frame rate throttling. +* `don't edit and drive` - enable editing while playing a level. +* `scroll scroll scroll your boat` - enable scrolling the level with arrow keys + while playing a level. + +## Bool Props + +Some boolean switches can be toggled in the command shell. + +Usage: `boolProp ` + +The value is truthy if its first character is the letter T or the number 1. +All other values are false. Examples: True, true, T, t, 1. + +* `Debug` or `D`: toggle debug mode within the app. +* `DebugOverlay` or `DO`: toggle the debug text overlay. +* `DebugCollision` or `DC`: toggle collision hitbox lines. + +## Interesting Tricks + +### Editable Map While Playing + +In Play Mode run the command: + +| Command | Effect | +|--------------------------------------------|----------------------------------------------------------------| +| `$ d.Scene.Drawing().Editable = true` | Can click and drag new pixels onto the level while playing it. | +| `$ d.Scene.Drawing().Scrollable = true` | Arrow keys scroll the map, like in editor mode. | +| `$ d.Scene.Drawing().NoLimitScroll = true` | Allow map to scroll beyond bounded limits. | + +The equivalent Canvas in the Edit Mode is at `d.Scene.UI.Canvas` + +### Edit Out-of-Bounds in Editor Mode + +In Edit Mode run the command: + +`$ d.Scene.UI.Canvas.NoLimitScroll = true` + +and you can scroll the map freely outside of the normal scroll boundaries. For +example, to see/edit pixels outside the top-left edges of bounded levels. diff --git a/lib/render/color.go b/lib/render/color.go index 3ec3ac5..e6b045d 100644 --- a/lib/render/color.go +++ b/lib/render/color.go @@ -211,6 +211,11 @@ func (c Color) DecodeMsgpack(dec *msgpack.Decoder) error { // return nil // } +// IsZero returns if the color is all zeroes (invisible). +func (c Color) IsZero() bool { + return c.Red+c.Green+c.Blue+c.Alpha == 0 +} + // Add a relative color value to the color. func (c Color) Add(r, g, b, a int) Color { var ( @@ -237,6 +242,16 @@ func (c Color) Add(r, g, b, a int) Color { } } +// AddColor adds another Color to your Color. +func (c Color) AddColor(other Color) Color { + return c.Add( + int(other.Red), + int(other.Green), + int(other.Blue), + int(other.Alpha), + ) +} + // Lighten a color value. func (c Color) Lighten(v int) Color { return c.Add(v, v, v, 0) diff --git a/pkg/collision/collide_level.go b/pkg/collision/collide_level.go index 11f54df..1f5acf5 100644 --- a/pkg/collision/collide_level.go +++ b/pkg/collision/collide_level.go @@ -23,6 +23,10 @@ type Collide struct { BottomPoint render.Point BottomPixel *level.Swatch MoveTo render.Point + + // Swatch attributes affecting the collision at this time. + InFire bool + InWater bool } // Reset a Collide struct flipping all the bools off, but keeping MoveTo. @@ -196,7 +200,8 @@ func CollidesWithGrid(d doodads.Actor, grid *level.Chunker, target render.Point) // IsColliding returns whether any sort of collision has occurred. func (c *Collide) IsColliding() bool { - return c.Top || c.Bottom || c.Left || c.Right + return c.Top || c.Bottom || c.Left || c.Right || + c.InFire || c.InWater } // ScanBoundingBox scans all of the pixels in a bounding box on the grid and @@ -235,21 +240,39 @@ func (c *Collide) ScanBoundingBox(box render.Rect, grid *level.Chunker) bool { // 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! + if swatch, err := grid.Get(point); err == nil { + // We're intersecting a pixel! If it's a solid one we'll return it + // in our result. If non-solid, we'll collect attributes from it + // and return them in the final result for gameplay behavior. + if swatch.Fire { + c.InFire = true + } + if swatch.Water { + c.InWater = true + } + + // Non-solid swatches don't collide so don't pay them attention. + if !swatch.Solid { + continue + } + switch side { case Top: c.Top = true c.TopPoint = point + c.TopPixel = swatch case Bottom: c.Bottom = true c.BottomPoint = point + c.BottomPixel = swatch case Left: c.Left = true c.LeftPoint = point + c.LeftPixel = swatch case Right: c.Right = true c.RightPoint = point + c.RightPixel = swatch } } } diff --git a/pkg/level/chunk.go b/pkg/level/chunk.go index b8dda84..5dac9ae 100644 --- a/pkg/level/chunk.go +++ b/pkg/level/chunk.go @@ -161,7 +161,12 @@ func (c *Chunk) ToBitmap(filename string, mask render.Color) (render.Texturer, e for px := range c.Iter() { var color = px.Swatch.Color if mask != render.Invisible { - color = mask + // A semi-transparent mask will overlay on top of the actual color. + if mask.Alpha < 255 { + color = color.AddColor(mask) + } else { + color = mask + } } img.Set( int(px.X-pointOffset.X), diff --git a/pkg/main_scene.go b/pkg/main_scene.go index 84465f3..72819f8 100644 --- a/pkg/main_scene.go +++ b/pkg/main_scene.go @@ -40,34 +40,40 @@ func (s *MainScene) Setup(d *Doodle) error { frame := ui.NewFrame("frame") s.frame = frame - button1 := ui.NewButton("Button1", ui.NewLabel(ui.Label{ - Text: "New Map", - Font: balance.StatusFont, - })) - button1.Handle(ui.Click, func(p render.Point) { - d.GotoNewMenu() - }) - - button2 := ui.NewButton("Button2", ui.NewLabel(ui.Label{ - Text: "Load Map", - Font: balance.StatusFont, - })) - button2.Handle(ui.Click, func(p render.Point) { - d.GotoLoadMenu() - }) - - frame.Pack(button1, ui.Pack{ - Anchor: ui.N, - Fill: true, - }) - frame.Pack(button2, ui.Pack{ - Anchor: ui.N, - PadY: 12, - Fill: true, - }) - - s.Supervisor.Add(button1) - s.Supervisor.Add(button2) + var buttons = []struct { + Name string + Func func() + }{ + { + Name: "Play a Level", + Func: d.GotoPlayMenu, + }, + { + Name: "Create a New Level", + Func: d.GotoNewMenu, + }, + { + Name: "Edit a Level", + Func: d.GotoLoadMenu, + }, + } + for _, button := range buttons { + button := button + btn := ui.NewButton(button.Name, ui.NewLabel(ui.Label{ + Text: button.Name, + Font: balance.StatusFont, + })) + btn.Handle(ui.Click, func(p render.Point) { + button.Func() + }) + s.Supervisor.Add(btn) + frame.Pack(btn, ui.Pack{ + Anchor: ui.N, + PadY: 8, + // Fill: true, + FillX: true, + }) + } return nil } diff --git a/pkg/menu_scene.go b/pkg/menu_scene.go index 5662599..91587f5 100644 --- a/pkg/menu_scene.go +++ b/pkg/menu_scene.go @@ -36,6 +36,9 @@ type MenuScene struct { // Values for the New menu newPageType string newWallpaper string + + // Values for the Load/Play menu. + loadForPlay bool // false = load for edit } // Name of the scene. @@ -54,13 +57,24 @@ func (d *Doodle) GotoNewMenu() { // GotoLoadMenu loads the MenuScene and shows the "Load" window. func (d *Doodle) GotoLoadMenu() { - log.Info("Loading the MenuScene to the Load window") + log.Info("Loading the MenuScene to the Load window for Edit Mode") scene := &MenuScene{ StartupMenu: "load", } d.Goto(scene) } +// GotoPlayMenu loads the MenuScene and shows the "Load" window for playing a +// level, not editing it. +func (d *Doodle) GotoPlayMenu() { + log.Info("Loading the MenuScene to the Load window for Play Mode") + scene := &MenuScene{ + StartupMenu: "load", + loadForPlay: true, + } + d.Goto(scene) +} + // Setup the scene. func (s *MenuScene) Setup(d *Doodle) error { s.Supervisor = ui.NewSupervisor() @@ -358,7 +372,11 @@ func (s *MenuScene) setupLoadWindow(d *Doodle) error { Font: balance.MenuFont, })) btn.Handle(ui.Click, func(p render.Point) { - d.EditFile(lvl) + if s.loadForPlay { + d.PlayLevel(lvl) + } else { + d.EditFile(lvl) + } }) s.Supervisor.Add(btn) lvlRow.Pack(btn, ui.Pack{ @@ -383,7 +401,9 @@ func (s *MenuScene) setupLoadWindow(d *Doodle) error { * Frame for selecting User Doodads ******************/ - if !balance.FreeVersion { + // Doodads not shown if we're loading a map to play, nor are they + // available to the free version. + if !s.loadForPlay && !balance.FreeVersion { label2 := ui.NewLabel(ui.Label{ Text: "Doodads", Font: balance.LabelFont, diff --git a/pkg/play_scene.go b/pkg/play_scene.go index 0633f52..549c66e 100644 --- a/pkg/play_scene.go +++ b/pkg/play_scene.go @@ -7,6 +7,7 @@ import ( "git.kirsle.net/apps/doodle/lib/render" "git.kirsle.net/apps/doodle/lib/ui" "git.kirsle.net/apps/doodle/pkg/balance" + "git.kirsle.net/apps/doodle/pkg/collision" "git.kirsle.net/apps/doodle/pkg/doodads" "git.kirsle.net/apps/doodle/pkg/level" "git.kirsle.net/apps/doodle/pkg/log" @@ -115,6 +116,17 @@ func (s *PlayScene) Setup(d *Doodle) error { s.drawing.Resize(render.NewRect(int32(d.width), int32(d.height))) s.drawing.Compute(d.Engine) + // Handler when an actor touches water or fire. + s.drawing.OnLevelCollision = func(a *uix.Actor, col *collision.Collide) { + if col.InFire { + a.Canvas.MaskColor = render.Black + } else if col.InWater { + a.Canvas.MaskColor = render.DarkBlue + } else { + a.Canvas.MaskColor = render.Invisible + } + } + // Given a filename or map data to play? if s.Level != nil { log.Debug("PlayScene.Setup: received level from scene caller") diff --git a/pkg/uix/actor_collision.go b/pkg/uix/actor_collision.go index 7befaa0..7664865 100644 --- a/pkg/uix/actor_collision.go +++ b/pkg/uix/actor_collision.go @@ -77,6 +77,9 @@ func (w *Canvas) loopActorCollision() error { info, ok := collision.CollidesWithGrid(a, w.chunks, delta) if ok { // Collision happened with world. + if w.OnLevelCollision != nil { + w.OnLevelCollision(a, info) + } } delta = info.MoveTo // Move us back where the collision check put us diff --git a/pkg/uix/canvas.go b/pkg/uix/canvas.go index e0e90c7..f2b6695 100644 --- a/pkg/uix/canvas.go +++ b/pkg/uix/canvas.go @@ -11,6 +11,7 @@ import ( "git.kirsle.net/apps/doodle/lib/ui" "git.kirsle.net/apps/doodle/pkg/balance" "git.kirsle.net/apps/doodle/pkg/bindata" + "git.kirsle.net/apps/doodle/pkg/collision" "git.kirsle.net/apps/doodle/pkg/doodads" "git.kirsle.net/apps/doodle/pkg/drawtool" "git.kirsle.net/apps/doodle/pkg/level" @@ -75,6 +76,9 @@ type Canvas struct { OnLinkActors func(a, b *Actor) linkFirst *Actor + // Collision handlers for level geometry. + OnLevelCollision func(*Actor, *collision.Collide) + /******** * Editable canvas private variables. ********/ diff --git a/pkg/uix/canvas_wallpaper.go b/pkg/uix/canvas_wallpaper.go index 86bc1ac..897043b 100644 --- a/pkg/uix/canvas_wallpaper.go +++ b/pkg/uix/canvas_wallpaper.go @@ -114,6 +114,9 @@ func (w *Canvas) PresentWallpaper(e render.Engine, p render.Point) error { limit.Y = S.H } + limit.X += size.W + limit.Y += size.H + // Tile the repeat texture. for x := origin.X - size.W; x < limit.X; x += size.W { for y := origin.Y - size.H; y < limit.Y; y += size.H {