Add Switches, Fire/Water Collision and Play Menu

* New doodads: Switches.
  * They come in four varieties: wall switch (background element, with
    "ON/OFF" text) and three side-profile switches for the floor, left
    or right walls.
  * On collision with the player, they flip their state from "OFF" to
    "ON" or vice versa. If the player walks away and then collides
    again, the switch flips again.
  * Can be used to open/close Electric Doors when turned on/off. Their
    default state is "off"
  * If a switch receives a power signal from another linked switch, it
    sets its own state to match. So, two "on/off" switches that are
    connected to a door AND to each other will both flip on/off when one
    of them flips.
* Update the Level Collision logic to support Decoration, Fire and Water
  pixel collisions.
  * Previously, ALL pixels in the level were acting as though solid.
  * Non-solid pixels don't count for collision detection, but their
    attributes (fire and water) are collected and returned.
* Updated the MenuScene to support loading a map file in Play Mode
  instead of Edit Mode. Updated the title screen menu to add a button
  for playing levels instead of editing them.
* Wrote some documentation.
This commit is contained in:
Noah 2019-07-06 18:30:03 -07:00
parent a504658055
commit cb02feff1d
24 changed files with 263 additions and 46 deletions

View File

@ -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 * 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. color and a set of boolean flags for `solid`, `fire` and `water` behaviors.
* [ ] User interface to edit (add/remove) swatches from the palette. * [ ] 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. * [x] Pencil (the default) draws single pixels on the level.
* [ ] Rectangle would draw a rectangular outline. * [x] Rectangle would draw a rectangular outline.
* [ ] Line would draw a line from point to point. * [x] Line would draw a line from point to point.
* [ ] A way to adjust brush properties: * [ ] A way to adjust brush properties:
* [ ] Brush size, shape (round or square). * [ ] Brush size, shape (round or square).
* [ ] Tools to toggle "layers" of visibility into your level: * [ ] 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: 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. i.e. by a "New Level" or "New Doodad" button.
* [ ] Create a "frame manager" window to see and page between the frames of the * [ ] Create a "frame manager" window to see and page between the frames of the
drawing. 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 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. layer. Small canvases will be useful for drawing doodads of a fixed size.

15
TODO.md
View File

@ -47,8 +47,8 @@
- [x] Buttons - [x] Buttons
- [x] Press Button - [x] Press Button
- [x] Sticky Button - [x] Sticky Button
- [ ] Switches - [x] Switches (4 varieties)
- [ ] Doors - [x] Doors
- [x] Locked Doors and Keys - [x] Locked Doors and Keys
- [x] Electric Doors - [x] Electric Doors
- [x] Trapdoors (all 4 directions) - [x] Trapdoors (all 4 directions)
@ -66,6 +66,7 @@ In addition to those listed above:
as a level goal and ends the level. as a level goal and ends the level.
- Doodads "Warp Door A" through "Warp Door D" - Doodads "Warp Door A" through "Warp Door D"
- The campaign.json would link levels together. - The campaign.json would link levels together.
- [ ] Conveyor Belt
## New Ideas ## 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 keys only get picked up by player characters and not "any doodad that
touches them" 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.

View File

@ -24,6 +24,23 @@ buttons() {
cd .. 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() { doors() {
cd doors/ cd doors/
@ -104,6 +121,7 @@ objects() {
} }
buttons buttons
switches
doors doors
trapdoors trapdoors
azulians azulians

View File

@ -23,8 +23,8 @@ function main() {
}); });
} else { } else {
animating = true; animating = true;
Self.PlayAnimation("close", function() {
opened = false; opened = false;
Self.PlayAnimation("close", function() {
animating = false; animating = false;
}) })
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 678 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 674 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 702 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 696 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 695 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 702 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 687 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 699 B

View File

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

22
docs/Doodad Ideas.md Normal file
View File

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

View File

@ -2,6 +2,43 @@
## Cheats ## Cheats
| Cheat Code | Effect | * `unleash the beast` - disable frame rate throttling.
|-------------------|--------------------------------| * `don't edit and drive` - enable editing while playing a level.
| unleash the beast | Disable frame rate throttling. | * `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 <name> <value>`
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.

View File

@ -211,6 +211,11 @@ func (c Color) DecodeMsgpack(dec *msgpack.Decoder) error {
// return nil // 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. // Add a relative color value to the color.
func (c Color) Add(r, g, b, a int) Color { func (c Color) Add(r, g, b, a int) Color {
var ( 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. // Lighten a color value.
func (c Color) Lighten(v int) Color { func (c Color) Lighten(v int) Color {
return c.Add(v, v, v, 0) return c.Add(v, v, v, 0)

View File

@ -23,6 +23,10 @@ type Collide struct {
BottomPoint render.Point BottomPoint render.Point
BottomPixel *level.Swatch BottomPixel *level.Swatch
MoveTo render.Point 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. // 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. // IsColliding returns whether any sort of collision has occurred.
func (c *Collide) IsColliding() bool { func (c *Collide) IsColliding() bool {
return c.Top || c.Bottom || c.Left || c.Right return c.Top || c.Bottom || c.Left || c.Right ||
c.InFire || c.InWater
} }
// ScanBoundingBox scans all of the pixels in a bounding box on the grid and // 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. // bounding boxes of the doodad.
func (c *Collide) ScanGridLine(p1, p2 render.Point, grid *level.Chunker, side Side) { func (c *Collide) ScanGridLine(p1, p2 render.Point, grid *level.Chunker, side Side) {
for point := range render.IterLine2(p1, p2) { for point := range render.IterLine2(p1, p2) {
if _, err := grid.Get(point); err == nil { if swatch, err := grid.Get(point); err == nil {
// A hit! // 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 { switch side {
case Top: case Top:
c.Top = true c.Top = true
c.TopPoint = point c.TopPoint = point
c.TopPixel = swatch
case Bottom: case Bottom:
c.Bottom = true c.Bottom = true
c.BottomPoint = point c.BottomPoint = point
c.BottomPixel = swatch
case Left: case Left:
c.Left = true c.Left = true
c.LeftPoint = point c.LeftPoint = point
c.LeftPixel = swatch
case Right: case Right:
c.Right = true c.Right = true
c.RightPoint = point c.RightPoint = point
c.RightPixel = swatch
} }
} }
} }

View File

@ -161,8 +161,13 @@ func (c *Chunk) ToBitmap(filename string, mask render.Color) (render.Texturer, e
for px := range c.Iter() { for px := range c.Iter() {
var color = px.Swatch.Color var color = px.Swatch.Color
if mask != render.Invisible { if mask != render.Invisible {
// A semi-transparent mask will overlay on top of the actual color.
if mask.Alpha < 255 {
color = color.AddColor(mask)
} else {
color = mask color = mask
} }
}
img.Set( img.Set(
int(px.X-pointOffset.X), int(px.X-pointOffset.X),
int(px.Y-pointOffset.Y), int(px.Y-pointOffset.Y),

View File

@ -40,34 +40,40 @@ func (s *MainScene) Setup(d *Doodle) error {
frame := ui.NewFrame("frame") frame := ui.NewFrame("frame")
s.frame = frame s.frame = frame
button1 := ui.NewButton("Button1", ui.NewLabel(ui.Label{ var buttons = []struct {
Text: "New Map", 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, Font: balance.StatusFont,
})) }))
button1.Handle(ui.Click, func(p render.Point) { btn.Handle(ui.Click, func(p render.Point) {
d.GotoNewMenu() button.Func()
}) })
s.Supervisor.Add(btn)
button2 := ui.NewButton("Button2", ui.NewLabel(ui.Label{ frame.Pack(btn, ui.Pack{
Text: "Load Map",
Font: balance.StatusFont,
}))
button2.Handle(ui.Click, func(p render.Point) {
d.GotoLoadMenu()
})
frame.Pack(button1, ui.Pack{
Anchor: ui.N, Anchor: ui.N,
Fill: true, PadY: 8,
// Fill: true,
FillX: true,
}) })
frame.Pack(button2, ui.Pack{ }
Anchor: ui.N,
PadY: 12,
Fill: true,
})
s.Supervisor.Add(button1)
s.Supervisor.Add(button2)
return nil return nil
} }

View File

@ -36,6 +36,9 @@ type MenuScene struct {
// Values for the New menu // Values for the New menu
newPageType string newPageType string
newWallpaper string newWallpaper string
// Values for the Load/Play menu.
loadForPlay bool // false = load for edit
} }
// Name of the scene. // Name of the scene.
@ -54,13 +57,24 @@ func (d *Doodle) GotoNewMenu() {
// GotoLoadMenu loads the MenuScene and shows the "Load" window. // GotoLoadMenu loads the MenuScene and shows the "Load" window.
func (d *Doodle) GotoLoadMenu() { 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{ scene := &MenuScene{
StartupMenu: "load", StartupMenu: "load",
} }
d.Goto(scene) 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. // Setup the scene.
func (s *MenuScene) Setup(d *Doodle) error { func (s *MenuScene) Setup(d *Doodle) error {
s.Supervisor = ui.NewSupervisor() s.Supervisor = ui.NewSupervisor()
@ -358,7 +372,11 @@ func (s *MenuScene) setupLoadWindow(d *Doodle) error {
Font: balance.MenuFont, Font: balance.MenuFont,
})) }))
btn.Handle(ui.Click, func(p render.Point) { btn.Handle(ui.Click, func(p render.Point) {
if s.loadForPlay {
d.PlayLevel(lvl)
} else {
d.EditFile(lvl) d.EditFile(lvl)
}
}) })
s.Supervisor.Add(btn) s.Supervisor.Add(btn)
lvlRow.Pack(btn, ui.Pack{ lvlRow.Pack(btn, ui.Pack{
@ -383,7 +401,9 @@ func (s *MenuScene) setupLoadWindow(d *Doodle) error {
* Frame for selecting User Doodads * 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{ label2 := ui.NewLabel(ui.Label{
Text: "Doodads", Text: "Doodads",
Font: balance.LabelFont, Font: balance.LabelFont,

View File

@ -7,6 +7,7 @@ import (
"git.kirsle.net/apps/doodle/lib/render" "git.kirsle.net/apps/doodle/lib/render"
"git.kirsle.net/apps/doodle/lib/ui" "git.kirsle.net/apps/doodle/lib/ui"
"git.kirsle.net/apps/doodle/pkg/balance" "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/doodads"
"git.kirsle.net/apps/doodle/pkg/level" "git.kirsle.net/apps/doodle/pkg/level"
"git.kirsle.net/apps/doodle/pkg/log" "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.Resize(render.NewRect(int32(d.width), int32(d.height)))
s.drawing.Compute(d.Engine) 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? // Given a filename or map data to play?
if s.Level != nil { if s.Level != nil {
log.Debug("PlayScene.Setup: received level from scene caller") log.Debug("PlayScene.Setup: received level from scene caller")

View File

@ -77,6 +77,9 @@ func (w *Canvas) loopActorCollision() error {
info, ok := collision.CollidesWithGrid(a, w.chunks, delta) info, ok := collision.CollidesWithGrid(a, w.chunks, delta)
if ok { if ok {
// Collision happened with world. // Collision happened with world.
if w.OnLevelCollision != nil {
w.OnLevelCollision(a, info)
}
} }
delta = info.MoveTo // Move us back where the collision check put us delta = info.MoveTo // Move us back where the collision check put us

View File

@ -11,6 +11,7 @@ import (
"git.kirsle.net/apps/doodle/lib/ui" "git.kirsle.net/apps/doodle/lib/ui"
"git.kirsle.net/apps/doodle/pkg/balance" "git.kirsle.net/apps/doodle/pkg/balance"
"git.kirsle.net/apps/doodle/pkg/bindata" "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/doodads"
"git.kirsle.net/apps/doodle/pkg/drawtool" "git.kirsle.net/apps/doodle/pkg/drawtool"
"git.kirsle.net/apps/doodle/pkg/level" "git.kirsle.net/apps/doodle/pkg/level"
@ -75,6 +76,9 @@ type Canvas struct {
OnLinkActors func(a, b *Actor) OnLinkActors func(a, b *Actor)
linkFirst *Actor linkFirst *Actor
// Collision handlers for level geometry.
OnLevelCollision func(*Actor, *collision.Collide)
/******** /********
* Editable canvas private variables. * Editable canvas private variables.
********/ ********/

View File

@ -114,6 +114,9 @@ func (w *Canvas) PresentWallpaper(e render.Engine, p render.Point) error {
limit.Y = S.H limit.Y = S.H
} }
limit.X += size.W
limit.Y += size.H
// Tile the repeat texture. // Tile the repeat texture.
for x := origin.X - size.W; x < limit.X; x += size.W { for x := origin.X - size.W; x < limit.X; x += size.W {
for y := origin.Y - size.H; y < limit.Y; y += size.H { for y := origin.Y - size.H; y < limit.Y; y += size.H {