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.
10
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.
|
||||
|
||||
|
|
15
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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -23,8 +23,8 @@ function main() {
|
|||
});
|
||||
} else {
|
||||
animating = true;
|
||||
Self.PlayAnimation("close", function() {
|
||||
opened = false;
|
||||
Self.PlayAnimation("close", function() {
|
||||
animating = false;
|
||||
})
|
||||
}
|
||||
|
|
BIN
dev-assets/doodads/switches/down-off.png
Normal file
After Width: | Height: | Size: 678 B |
BIN
dev-assets/doodads/switches/down-on.png
Normal file
After Width: | Height: | Size: 674 B |
BIN
dev-assets/doodads/switches/left-off.png
Normal file
After Width: | Height: | Size: 702 B |
BIN
dev-assets/doodads/switches/left-on.png
Normal file
After Width: | Height: | Size: 696 B |
BIN
dev-assets/doodads/switches/right-off.png
Normal file
After Width: | Height: | Size: 695 B |
BIN
dev-assets/doodads/switches/right-on.png
Normal file
After Width: | Height: | Size: 702 B |
BIN
dev-assets/doodads/switches/switch-off.png
Normal file
After Width: | Height: | Size: 687 B |
BIN
dev-assets/doodads/switches/switch-on.png
Normal file
After Width: | Height: | Size: 699 B |
38
dev-assets/doodads/switches/switch.js
Normal 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
|
@ -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.
|
|
@ -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 <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.
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -161,8 +161,13 @@ 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 {
|
||||
// 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),
|
||||
int(px.Y-pointOffset.Y),
|
||||
|
|
|
@ -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",
|
||||
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,
|
||||
}))
|
||||
button1.Handle(ui.Click, func(p render.Point) {
|
||||
d.GotoNewMenu()
|
||||
btn.Handle(ui.Click, func(p render.Point) {
|
||||
button.Func()
|
||||
})
|
||||
|
||||
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{
|
||||
s.Supervisor.Add(btn)
|
||||
frame.Pack(btn, ui.Pack{
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
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,
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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.
|
||||
********/
|
||||
|
|
|
@ -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 {
|
||||
|
|