Checkpoint Flag & Retry from Checkpoint

* New Doodad: Checkpoint Flag. They update the player's spawn point
  whenever the player passes one. The most recently activated
  checkpoint is rendered brighter than the others.
* End Level Modal: the fake alert box window drawn by the Play Mode
  is replaced with a fancy modal widget (similar to Alert and Confirm).
  It handles level victory or failure conditions and can show or hide
  all the buttons as needed.
* Gameplay: There is a "Retry from Checkpoint" option added, which
  appears in the level failure modal. It will teleport you back to
  the Start Flag or the last Checkpoint Flag you had touched, without
  resetting the level -- your keys, unlocked doors, etc. will be
  preserved so you can retry.
* Set a maximum speed on the "Camera Follows Actor" logic of 64
  pixels per tick. This results in a smoother scrolling transition
  when the player jumps to a new location on the map, such as by
  a Warp Door.
* Update the default color palettes:
    * All: Add a "hint" magenta color.
    * Colored Pencil: Add a "darkstone" solid color.

Updates to the Doodads JavaScript API:

* SetCheckpoint(Point(x, y)): set the player character's spawn
  position. Giving it Self.Position() is an easy way to set the
  player spawn to your doodad's location.
This commit is contained in:
Noah 2021-08-15 20:17:53 -07:00
parent 43f8e3d9b2
commit 1ac85c9297
12 changed files with 308 additions and 158 deletions

View File

@ -2,7 +2,7 @@ function main() {
var pressed = false; var pressed = false;
// When a sticky button receives power, it pops back up. // When a sticky button receives power, it pops back up.
Message.Subscribe("power", function(powered) { Message.Subscribe("power", function (powered) {
if (powered && pressed) { if (powered && pressed) {
Self.ShowLayer(0); Self.ShowLayer(0);
pressed = false; pressed = false;
@ -12,7 +12,7 @@ function main() {
} }
}) })
Events.OnCollide(function(e) { Events.OnCollide(function (e) {
if (!e.Settled) { if (!e.Settled) {
return; return;
} }

View File

@ -10,6 +10,11 @@ build:
doodad convert -t "Exit Flag" exit-flag.png exit-flag.doodad doodad convert -t "Exit Flag" exit-flag.png exit-flag.doodad
doodad install-script exit-flag.js exit-flag.doodad doodad install-script exit-flag.js exit-flag.doodad
# Checkpoint Flag
doodad convert -t "Checkpoint Flag" checkpoint-active.png \
checkpoint-inactive.png checkpoint-flag.doodad
doodad install-script checkpoint-flag.js checkpoint-flag.doodad
# Anvil # Anvil
doodad convert -t "Anvil" anvil.png anvil.doodad doodad convert -t "Anvil" anvil.png anvil.doodad
doodad install-script anvil.js anvil.doodad doodad install-script anvil.js anvil.doodad

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

View File

@ -0,0 +1,38 @@
// Checkpoint Flag.
var isCurrentCheckpoint = false;
function main() {
Self.SetHitbox(22 + 16, 16, 75 - 16, 86);
setActive(false);
// Checkpoints broadcast to all of their peers so they all
// know which one is the most recently activated.
Message.Subscribe("broadcast:checkpoint", function (currentID) {
setActive(false);
});
Events.OnCollide(function (e) {
if (!e.Settled) {
return;
}
// Only care about the player character.
if (!e.Actor.IsPlayer()) {
return;
}
// Set the player checkpoint.
SetCheckpoint(Self.Position());
setActive(true);
Message.Broadcast("broadcast:checkpoint", Self.ID())
});
}
function setActive(v) {
if (v && !isCurrentCheckpoint) {
Flash("Checkpoint!");
}
isCurrentCheckpoint = v;
Self.ShowLayerNamed(v ? "checkpoint-active" : "checkpoint-inactive");
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

@ -10,6 +10,7 @@ var (
// Speed to scroll a canvas with arrow keys in Edit Mode. // Speed to scroll a canvas with arrow keys in Edit Mode.
CanvasScrollSpeed = 8 CanvasScrollSpeed = 8
FollowActorMaxScrollSpeed = 64
// Window scrolling behavior in Play Mode. // Window scrolling behavior in Play Mode.
ScrollboxOffset = render.Point{ // from center of screen ScrollboxOffset = render.Point{ // from center of screen

View File

@ -38,11 +38,22 @@ var (
Water: true, Water: true,
Pattern: "ink.png", Pattern: "ink.png",
}, },
{
Name: "hint",
Color: render.MustHexColor("#F0F"),
Pattern: "marker.png",
},
}, },
}, },
"Colored Pencil": { "Colored Pencil": {
Swatches: []*Swatch{ Swatches: []*Swatch{
{
Name: "darkstone",
Color: render.MustHexColor("#777"),
Pattern: "noise.png",
Solid: true,
},
{ {
Name: "grass", Name: "grass",
Color: render.DarkGreen, Color: render.DarkGreen,
@ -79,6 +90,11 @@ var (
Water: true, Water: true,
Pattern: "ink.png", Pattern: "ink.png",
}, },
{
Name: "hint",
Color: render.MustHexColor("#F0F"),
Pattern: "marker.png",
},
}, },
}, },
@ -113,6 +129,11 @@ var (
Solid: true, Solid: true,
Pattern: "marker.png", Pattern: "marker.png",
}, },
{
Name: "hint",
Color: render.MustHexColor("#F0F"),
Pattern: "marker.png",
},
}, },
}, },
} }

147
pkg/modal/end_level.go Normal file
View File

@ -0,0 +1,147 @@
package modal
import (
"fmt"
"git.kirsle.net/apps/doodle/pkg/balance"
"git.kirsle.net/go/ui"
)
// ConfigEndLevel sets options for the EndLevel modal.
type ConfigEndLevel struct {
Success bool // false = failure condition
// Handler functions - what you don't define will not
// show as buttons in the modal.
OnRestartLevel func() // Restart Level
OnRetryCheckpoint func() // Continue from checkpoint
OnEditLevel func()
OnNextLevel func() // Next Level
OnExitToMenu func() // Exit to Menu
}
// EndLevel shows the End Level modal.
func EndLevel(cfg ConfigEndLevel, title, message string, args ...interface{}) *Modal {
if !ready {
panic("modal.EndLevel(): not ready")
} else if current != nil {
return current
}
// Reset the supervisor.
supervisor = ui.NewSupervisor()
m := &Modal{
title: title,
message: fmt.Sprintf(message, args...),
}
m.window = makeEndLevel(m, cfg)
center(m.window)
current = m
return m
}
// makeEndLevel creates the ui.Window for the Confirm modal.
func makeEndLevel(m *Modal, cfg ConfigEndLevel) *ui.Window {
win := ui.NewWindow("EndLevel")
_, title := win.TitleBar()
title.TextVariable = &m.title
msgFrame := ui.NewFrame("Confirm Message")
win.Pack(msgFrame, ui.Pack{
Side: ui.N,
})
msg := ui.NewLabel(ui.Label{
TextVariable: &m.message,
Font: balance.UIFont,
})
msgFrame.Pack(msg, ui.Pack{
Side: ui.N,
})
// Ok/Cancel button bar.
btnBar := ui.NewFrame("Button Bar")
msgFrame.Pack(btnBar, ui.Pack{
Side: ui.N,
PadY: 4,
})
var buttons []*ui.Button
var primaryFunc func()
for _, btn := range []struct {
Label string
F func()
}{
{
Label: "Next Level",
F: cfg.OnNextLevel,
},
{
Label: "Retry from Checkpoint",
F: cfg.OnRetryCheckpoint,
},
{
Label: "Restart Level",
F: cfg.OnRestartLevel,
},
{
Label: "Edit Level",
F: cfg.OnEditLevel,
},
{
Label: "Exit to Menu",
F: cfg.OnExitToMenu,
},
} {
btn := btn
if btn.F == nil {
continue
}
if primaryFunc == nil {
primaryFunc = btn.F
}
button := ui.NewButton(btn.Label+"Button", ui.NewLabel(ui.Label{
Text: btn.Label,
Font: balance.MenuFont,
}))
button.Handle(ui.Click, func(ed ui.EventData) error {
btn.F()
m.Dismiss(false)
return nil
})
button.Compute(engine)
buttons = append(buttons, button)
supervisor.Add(button)
btnBar.Pack(button, ui.Pack{
Side: ui.N,
PadY: 2,
FillX: true,
})
// // Make a new row of buttons?
// if i > 0 && i%3 == 0 {
// btnBar = ui.NewFrame("Button Bar")
// msgFrame.Pack(btnBar, ui.Pack{
// Side: ui.N,
// PadY: 0,
// })
// }
}
// Mark the first button the primary button.
if primaryFunc != nil {
m.Then(primaryFunc)
}
buttons[0].SetStyle(&balance.ButtonPrimary)
win.Compute(engine)
win.Supervise(supervisor)
return win
}

View File

@ -9,6 +9,7 @@ import (
"git.kirsle.net/apps/doodle/pkg/keybind" "git.kirsle.net/apps/doodle/pkg/keybind"
"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"
"git.kirsle.net/apps/doodle/pkg/modal"
"git.kirsle.net/apps/doodle/pkg/modal/loadscreen" "git.kirsle.net/apps/doodle/pkg/modal/loadscreen"
"git.kirsle.net/apps/doodle/pkg/physics" "git.kirsle.net/apps/doodle/pkg/physics"
"git.kirsle.net/apps/doodle/pkg/scripting" "git.kirsle.net/apps/doodle/pkg/scripting"
@ -38,16 +39,6 @@ type PlayScene struct {
screen *ui.Frame // A window sized invisible frame to position UI elements. screen *ui.Frame // A window sized invisible frame to position UI elements.
editButton *ui.Button editButton *ui.Button
// The alert box shows up when the level goal is reached and includes
// buttons what to do next.
alertBox *ui.Window
alertBoxLabel *ui.Label
alertBoxValue string
alertReplayButton *ui.Button // Replay level
alertEditButton *ui.Button // Edit Level
alertNextButton *ui.Button // Next Level
alertExitButton *ui.Button // Exit to menu
// Custom debug labels. // Custom debug labels.
debPosition *string debPosition *string
debViewport *string debViewport *string
@ -57,6 +48,7 @@ type PlayScene struct {
// Player character // Player character
Player *uix.Actor Player *uix.Actor
playerPhysics *physics.Mover playerPhysics *physics.Mover
lastCheckpoint render.Point
antigravity bool // Cheat: disable player gravity antigravity bool // Cheat: disable player gravity
noclip bool // Cheat: disable player clipping noclip bool // Cheat: disable player clipping
playerJumpCounter int // limit jump length playerJumpCounter int // limit jump length
@ -100,51 +92,9 @@ func (s *PlayScene) setupAsync(d *Doodle) error {
s.screen.Resize(render.NewRect(d.width, d.height)) s.screen.Resize(render.NewRect(d.width, d.height))
// Level Exit handler. // Level Exit handler.
s.SetupAlertbox() s.scripting.OnLevelExit(s.BeatLevel)
s.scripting.OnLevelExit(func() { s.scripting.OnLevelFail(s.FailLevel)
d.Flash("Hurray!") s.scripting.OnSetCheckpoint(s.SetCheckpoint)
// Pause the simulation.
s.running = false
// Toggle the relevant buttons on.
if s.CanEdit {
s.alertEditButton.Show()
}
if s.HasNext {
s.alertNextButton.Show()
}
// Always-visible buttons.
s.alertReplayButton.Show()
s.alertExitButton.Show()
// Show the alert box.
s.alertBox.Title = "Level Completed"
s.alertBoxValue = "Congratulations on clearing the level!"
s.alertBox.Show()
})
s.scripting.OnLevelFail(func(message string) {
d.Flash(message)
// Pause the simulation.
s.running = false
// Toggle the relevant buttons on.
if s.CanEdit {
s.alertEditButton.Show()
}
s.alertNextButton.Hide()
// Always-visible buttons.
s.alertReplayButton.Show()
s.alertExitButton.Show()
// Show the alert box.
s.alertBox.Title = "You've died!"
s.alertBoxValue = message
s.alertBox.Show()
})
// Initialize debug overlay values. // Initialize debug overlay values.
s.debPosition = new(string) s.debPosition = new(string)
@ -288,6 +238,9 @@ func (s *PlayScene) setupPlayer() {
} }
} }
// The Start Flag becomes the player's initial checkpoint.
s.lastCheckpoint = flag.Point
// Load in the player character. // Load in the player character.
player, err := doodads.LoadFile(playerCharacterFilename) player, err := doodads.LoadFile(playerCharacterFilename)
if err != nil { if err != nil {
@ -329,86 +282,6 @@ func (s *PlayScene) setupPlayer() {
} }
} }
// SetupAlertbox configures the alert box UI.
func (s *PlayScene) SetupAlertbox() {
window := ui.NewWindow("Level Completed")
window.Configure(ui.Config{
Width: 320,
Height: 160,
Background: render.Grey,
})
window.Compute(s.d.Engine)
{
frame := ui.NewFrame("Open Drawing Frame")
window.Pack(frame, ui.Pack{
Side: ui.N,
Fill: true,
Expand: true,
})
/******************
* Frame for selecting User Levels
******************/
s.alertBoxLabel = ui.NewLabel(ui.Label{
TextVariable: &s.alertBoxValue,
Font: balance.LabelFont,
})
frame.Pack(s.alertBoxLabel, ui.Pack{
Side: ui.N,
FillX: true,
PadY: 16,
})
/******************
* Confirm/cancel buttons.
******************/
bottomFrame := ui.NewFrame("Button Frame")
frame.Pack(bottomFrame, ui.Pack{
Side: ui.N,
FillX: true,
PadY: 8,
})
// Button factory for the various options.
makeButton := func(text string, handler func()) *ui.Button {
btn := ui.NewButton(text, ui.NewLabel(ui.Label{
Font: balance.LabelFont,
Text: text,
}))
btn.Handle(ui.Click, func(ed ui.EventData) error {
handler()
return nil
})
bottomFrame.Pack(btn, ui.Pack{
Side: ui.W,
PadX: 2,
})
s.supervisor.Add(btn)
btn.Hide() // all buttons hidden by default
return btn
}
s.alertReplayButton = makeButton("Play Again", func() {
s.RestartLevel()
})
s.alertEditButton = makeButton("Edit Level", func() {
s.EditLevel()
})
s.alertNextButton = makeButton("Next Level", func() {
s.d.Flash("Not Implemented")
})
s.alertExitButton = makeButton("Exit to Menu", func() {
s.d.Goto(&MainScene{})
})
}
s.alertBox = window
s.alertBox.Hide()
}
// EditLevel toggles out of Play Mode to edit the level. // EditLevel toggles out of Play Mode to edit the level.
func (s *PlayScene) EditLevel() { func (s *PlayScene) EditLevel() {
log.Info("Edit Mode, Go!") log.Info("Edit Mode, Go!")
@ -428,19 +301,67 @@ func (s *PlayScene) RestartLevel() {
}) })
} }
// SetCheckpoint sets the player's checkpoint.
func (s *PlayScene) SetCheckpoint(where render.Point) {
s.lastCheckpoint = where
}
// RetryCheckpoint moves the player back to their last checkpoint.
func (s *PlayScene) RetryCheckpoint() {
log.Info("Move player back to last checkpoint")
s.Player.MoveTo(s.lastCheckpoint)
s.running = true
}
// BeatLevel handles the level success condition.
func (s *PlayScene) BeatLevel() {
s.d.Flash("Hurray!")
s.ShowEndLevelModal(
true,
"Level Completed",
"Congratulations on clearing the level!",
)
}
// FailLevel handles a level failure triggered by a doodad.
func (s *PlayScene) FailLevel(message string) {
s.d.Flash(message)
s.ShowEndLevelModal(
false,
"You've died!",
message,
)
}
// DieByFire ends the level by "fire", or w/e the swatch is named. // DieByFire ends the level by "fire", or w/e the swatch is named.
func (s *PlayScene) DieByFire(name string) { func (s *PlayScene) DieByFire(name string) {
log.Info("Watch out for %s!", name) s.FailLevel(fmt.Sprintf("Watch out for %s!", name))
s.alertBox.Title = "You've died!" }
s.alertBoxValue = fmt.Sprintf("Watch out for %s!", name)
s.alertReplayButton.Show() // ShowEndLevelModal centralizes the EndLevel modal config.
if s.CanEdit { // This is the common handler function between easy methods such as
s.alertEditButton.Show() // BeatLevel, FailLevel, and DieByFire.
func (s *PlayScene) ShowEndLevelModal(success bool, title, message string) {
config := modal.ConfigEndLevel{
Success: success,
OnRestartLevel: s.RestartLevel,
OnRetryCheckpoint: s.RetryCheckpoint,
OnExitToMenu: func() {
s.d.Goto(&MainScene{})
},
} }
s.alertExitButton.Show()
s.alertBox.Show() if s.CanEdit {
config.OnEditLevel = s.EditLevel
}
// Beaten the level?
if success {
config.OnRetryCheckpoint = nil
}
// Show the modal.
modal.EndLevel(config, title, message)
// Stop the simulation. // Stop the simulation.
s.running = false s.running = false
@ -538,16 +459,6 @@ func (s *PlayScene) Draw(d *Doodle) error {
}) })
s.editButton.Present(d.Engine, s.editButton.Point()) s.editButton.Present(d.Engine, s.editButton.Point())
// Draw the alert box window.
if !s.alertBox.Hidden() {
s.alertBox.Compute(d.Engine)
s.alertBox.MoveTo(render.Point{
X: (d.width / 2) - (s.alertBox.Size().W / 2),
Y: (d.height / 2) - (s.alertBox.Size().H / 2),
})
s.alertBox.Present(d.Engine, s.alertBox.Point())
}
return nil return nil
} }

View File

@ -8,6 +8,7 @@ import (
"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"
"git.kirsle.net/go/render"
) )
// Supervisor manages the JavaScript VMs for each doodad by its // Supervisor manages the JavaScript VMs for each doodad by its
@ -18,6 +19,7 @@ type Supervisor struct {
// Global event handlers. // Global event handlers.
onLevelExit func() onLevelExit func()
onLevelFail func(message string) onLevelFail func(message string)
onSetCheckpoint func(where render.Point)
} }
// NewSupervisor creates a new JavaScript Supervior. // NewSupervisor creates a new JavaScript Supervior.

View File

@ -1,5 +1,7 @@
package scripting package scripting
import "git.kirsle.net/go/render"
/* /*
RegisterEventHooks attaches the supervisor level event hooks into a JS VM. RegisterEventHooks attaches the supervisor level event hooks into a JS VM.
@ -21,6 +23,12 @@ func RegisterEventHooks(s *Supervisor, vm *VM) {
} }
s.onLevelFail(message) s.onLevelFail(message)
}) })
vm.Set("SetCheckpoint", func(p render.Point) {
if s.onSetCheckpoint == nil {
panic("JS SetCheckpoint(): No OnSetCheckpoint handler attached to script supervisor")
}
s.onSetCheckpoint(p)
})
} }
// OnLevelExit registers an event hook for when a Level Exit doodad is reached. // OnLevelExit registers an event hook for when a Level Exit doodad is reached.
@ -32,3 +40,8 @@ func (s *Supervisor) OnLevelExit(handler func()) {
func (s *Supervisor) OnLevelFail(handler func(string)) { func (s *Supervisor) OnLevelFail(handler func(string)) {
s.onLevelFail = handler s.onLevelFail = handler
} }
// OnSetCheckpoint registers an event hook for setting player checkpoints.
func (s *Supervisor) OnSetCheckpoint(handler func(render.Point)) {
s.onSetCheckpoint = handler
}

View File

@ -179,6 +179,18 @@ func (w *Canvas) loopFollowActor(ev *event.State) error {
scrollBy.Y = delta scrollBy.Y = delta
} }
// Constrain the maximum scroll speed.
if scrollBy.X > balance.FollowActorMaxScrollSpeed {
scrollBy.X = balance.FollowActorMaxScrollSpeed
} else if scrollBy.X < -balance.FollowActorMaxScrollSpeed {
scrollBy.X = -balance.FollowActorMaxScrollSpeed
}
if scrollBy.Y > balance.FollowActorMaxScrollSpeed {
scrollBy.Y = balance.FollowActorMaxScrollSpeed
} else if scrollBy.Y < -balance.FollowActorMaxScrollSpeed {
scrollBy.Y = -balance.FollowActorMaxScrollSpeed
}
if scrollBy != render.Origin { if scrollBy != render.Origin {
w.ScrollBy(scrollBy) w.ScrollBy(scrollBy)
} }