From 1ac85c92977fbcb379bcf8cd8ee6eadd72ad0dcf Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Sun, 15 Aug 2021 20:17:53 -0700 Subject: [PATCH] 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. --- dev-assets/doodads/buttons/sticky.js | 4 +- dev-assets/doodads/objects/Makefile | 5 + .../doodads/objects/checkpoint-active.png | Bin 0 -> 8630 bytes dev-assets/doodads/objects/checkpoint-flag.js | 38 +++ .../doodads/objects/checkpoint-inactive.png | Bin 0 -> 7309 bytes pkg/balance/numbers.go | 3 +- pkg/level/palette_defaults.go | 21 ++ pkg/modal/end_level.go | 147 ++++++++++++ pkg/play_scene.go | 217 ++++++------------ pkg/scripting/scripting.go | 6 +- pkg/scripting/supervisor_events.go | 13 ++ pkg/uix/canvas_scrolling.go | 12 + 12 files changed, 308 insertions(+), 158 deletions(-) create mode 100644 dev-assets/doodads/objects/checkpoint-active.png create mode 100644 dev-assets/doodads/objects/checkpoint-flag.js create mode 100644 dev-assets/doodads/objects/checkpoint-inactive.png create mode 100644 pkg/modal/end_level.go diff --git a/dev-assets/doodads/buttons/sticky.js b/dev-assets/doodads/buttons/sticky.js index a715a59..1702f8c 100644 --- a/dev-assets/doodads/buttons/sticky.js +++ b/dev-assets/doodads/buttons/sticky.js @@ -2,7 +2,7 @@ function main() { var pressed = false; // When a sticky button receives power, it pops back up. - Message.Subscribe("power", function(powered) { + Message.Subscribe("power", function (powered) { if (powered && pressed) { Self.ShowLayer(0); pressed = false; @@ -12,7 +12,7 @@ function main() { } }) - Events.OnCollide(function(e) { + Events.OnCollide(function (e) { if (!e.Settled) { return; } diff --git a/dev-assets/doodads/objects/Makefile b/dev-assets/doodads/objects/Makefile index 86e33ce..e79ee47 100644 --- a/dev-assets/doodads/objects/Makefile +++ b/dev-assets/doodads/objects/Makefile @@ -10,6 +10,11 @@ build: doodad convert -t "Exit Flag" exit-flag.png 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 doodad convert -t "Anvil" anvil.png anvil.doodad doodad install-script anvil.js anvil.doodad diff --git a/dev-assets/doodads/objects/checkpoint-active.png b/dev-assets/doodads/objects/checkpoint-active.png new file mode 100644 index 0000000000000000000000000000000000000000..d8d9503211d8bd2f8c9a60eb8aee58536f8f945d GIT binary patch literal 8630 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 literal 0 HcmV?d00001 diff --git a/dev-assets/doodads/objects/checkpoint-flag.js b/dev-assets/doodads/objects/checkpoint-flag.js new file mode 100644 index 0000000..a50608a --- /dev/null +++ b/dev-assets/doodads/objects/checkpoint-flag.js @@ -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"); +} \ No newline at end of file diff --git a/dev-assets/doodads/objects/checkpoint-inactive.png b/dev-assets/doodads/objects/checkpoint-inactive.png new file mode 100644 index 0000000000000000000000000000000000000000..6781ff9abfdae14001a0b37147d0d8228a660731 GIT binary patch literal 7309 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 N2WMh!Tx94G@gMm|3!?x4 literal 0 HcmV?d00001 diff --git a/pkg/balance/numbers.go b/pkg/balance/numbers.go index 154ad5a..a507220 100644 --- a/pkg/balance/numbers.go +++ b/pkg/balance/numbers.go @@ -9,7 +9,8 @@ var ( Height = 768 // Speed to scroll a canvas with arrow keys in Edit Mode. - CanvasScrollSpeed = 8 + CanvasScrollSpeed = 8 + FollowActorMaxScrollSpeed = 64 // Window scrolling behavior in Play Mode. ScrollboxOffset = render.Point{ // from center of screen diff --git a/pkg/level/palette_defaults.go b/pkg/level/palette_defaults.go index d88fc02..fba575e 100644 --- a/pkg/level/palette_defaults.go +++ b/pkg/level/palette_defaults.go @@ -38,11 +38,22 @@ var ( Water: true, Pattern: "ink.png", }, + { + Name: "hint", + Color: render.MustHexColor("#F0F"), + Pattern: "marker.png", + }, }, }, "Colored Pencil": { Swatches: []*Swatch{ + { + Name: "darkstone", + Color: render.MustHexColor("#777"), + Pattern: "noise.png", + Solid: true, + }, { Name: "grass", Color: render.DarkGreen, @@ -79,6 +90,11 @@ var ( Water: true, Pattern: "ink.png", }, + { + Name: "hint", + Color: render.MustHexColor("#F0F"), + Pattern: "marker.png", + }, }, }, @@ -113,6 +129,11 @@ var ( Solid: true, Pattern: "marker.png", }, + { + Name: "hint", + Color: render.MustHexColor("#F0F"), + Pattern: "marker.png", + }, }, }, } diff --git a/pkg/modal/end_level.go b/pkg/modal/end_level.go new file mode 100644 index 0000000..6fe9aca --- /dev/null +++ b/pkg/modal/end_level.go @@ -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 +} diff --git a/pkg/play_scene.go b/pkg/play_scene.go index b1d9385..52ddccb 100644 --- a/pkg/play_scene.go +++ b/pkg/play_scene.go @@ -9,6 +9,7 @@ import ( "git.kirsle.net/apps/doodle/pkg/keybind" "git.kirsle.net/apps/doodle/pkg/level" "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/physics" "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. 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. debPosition *string debViewport *string @@ -57,6 +48,7 @@ type PlayScene struct { // Player character Player *uix.Actor playerPhysics *physics.Mover + lastCheckpoint render.Point antigravity bool // Cheat: disable player gravity noclip bool // Cheat: disable player clipping 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)) // Level Exit handler. - s.SetupAlertbox() - s.scripting.OnLevelExit(func() { - d.Flash("Hurray!") - - // 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() - }) + s.scripting.OnLevelExit(s.BeatLevel) + s.scripting.OnLevelFail(s.FailLevel) + s.scripting.OnSetCheckpoint(s.SetCheckpoint) // Initialize debug overlay values. 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. player, err := doodads.LoadFile(playerCharacterFilename) 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. func (s *PlayScene) EditLevel() { 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. func (s *PlayScene) DieByFire(name string) { - log.Info("Watch out for %s!", name) - s.alertBox.Title = "You've died!" - s.alertBoxValue = fmt.Sprintf("Watch out for %s!", name) + s.FailLevel(fmt.Sprintf("Watch out for %s!", name)) +} - s.alertReplayButton.Show() - if s.CanEdit { - s.alertEditButton.Show() +// ShowEndLevelModal centralizes the EndLevel modal config. +// This is the common handler function between easy methods such as +// 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. s.running = false @@ -538,16 +459,6 @@ func (s *PlayScene) Draw(d *Doodle) error { }) 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 } diff --git a/pkg/scripting/scripting.go b/pkg/scripting/scripting.go index ab98a6d..c769aaf 100644 --- a/pkg/scripting/scripting.go +++ b/pkg/scripting/scripting.go @@ -8,6 +8,7 @@ import ( "git.kirsle.net/apps/doodle/pkg/level" "git.kirsle.net/apps/doodle/pkg/log" + "git.kirsle.net/go/render" ) // Supervisor manages the JavaScript VMs for each doodad by its @@ -16,8 +17,9 @@ type Supervisor struct { scripts map[string]*VM // Global event handlers. - onLevelExit func() - onLevelFail func(message string) + onLevelExit func() + onLevelFail func(message string) + onSetCheckpoint func(where render.Point) } // NewSupervisor creates a new JavaScript Supervior. diff --git a/pkg/scripting/supervisor_events.go b/pkg/scripting/supervisor_events.go index 12eacf9..cf40dcf 100644 --- a/pkg/scripting/supervisor_events.go +++ b/pkg/scripting/supervisor_events.go @@ -1,5 +1,7 @@ package scripting +import "git.kirsle.net/go/render" + /* RegisterEventHooks attaches the supervisor level event hooks into a JS VM. @@ -21,6 +23,12 @@ func RegisterEventHooks(s *Supervisor, vm *VM) { } 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. @@ -32,3 +40,8 @@ func (s *Supervisor) OnLevelExit(handler func()) { func (s *Supervisor) OnLevelFail(handler func(string)) { s.onLevelFail = handler } + +// OnSetCheckpoint registers an event hook for setting player checkpoints. +func (s *Supervisor) OnSetCheckpoint(handler func(render.Point)) { + s.onSetCheckpoint = handler +} diff --git a/pkg/uix/canvas_scrolling.go b/pkg/uix/canvas_scrolling.go index ac45354..ee388c8 100644 --- a/pkg/uix/canvas_scrolling.go +++ b/pkg/uix/canvas_scrolling.go @@ -179,6 +179,18 @@ func (w *Canvas) loopFollowActor(ev *event.State) error { 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 { w.ScrollBy(scrollBy) }