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 0000000..d8d9503 Binary files /dev/null and b/dev-assets/doodads/objects/checkpoint-active.png differ 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 0000000..6781ff9 Binary files /dev/null and b/dev-assets/doodads/objects/checkpoint-inactive.png differ 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) }