v0.11.0 last minute tweaks

* When playing as the Bird, the dive attack is able to destroy other
  mobile doodads such as Azulians and Thieves.
* The Box has been made invulnerable so it can't be destroyed by Anvils
  or player-controlled Birds.
* Bugfixes with pop-up modals:
  * The quit game confirm modal doesn't appear if another modal is
    already active on screen.
  * The Escape key can dismiss Alert and Confirm modals.
* Add "Level" menu items to Play Mode to restart the level or retry from
  the last checkpoint (in case of softlocks, etc.)
This commit is contained in:
Noah 2022-02-21 13:09:51 -08:00
parent 40cb9f15cb
commit 962098d4e7
10 changed files with 100 additions and 35 deletions

View File

@ -1,6 +1,6 @@
# Changes # Changes
## v0.11.0 (Feb 20 2022) ## v0.11.0 (Feb 21 2022)
New features: New features:
@ -34,8 +34,9 @@ are becoming more dangerous:
* The **Bird** now searches for the player diagonally in front of * The **Bird** now searches for the player diagonally in front of
it for about 240px or so. If spotted it will dive toward you and it for about 240px or so. If spotted it will dive toward you and
it is dangerous when diving! When playing as the bird, the dive sprite it is dangerous when diving! When _playing_ as the bird, the dive sprite
is used when flying diagonally downwards. is used when flying diagonally downwards. The player-controlled bird
can kill mobile doodads by diving into them.
* The **Azulians** will start to follow the player when you get * The **Azulians** will start to follow the player when you get
close and they are dangerous when they touch you -- but not if close and they are dangerous when they touch you -- but not if
you're the **Thief.** The red Azulian has a wider search radius, you're the **Thief.** The red Azulian has a wider search radius,
@ -49,6 +50,8 @@ are becoming more dangerous:
* The **Anvil** is invulnerable -- if the player character is the Anvil * The **Anvil** is invulnerable -- if the player character is the Anvil
it can not die by fire or hostile enemies, and Anvils can not destroy it can not die by fire or hostile enemies, and Anvils can not destroy
other Anvils. other Anvils.
* The **Box** is also made invulnerable so it can't be destroyed by a
player-controlled Anvil or Bird.
New functions are available on the JavaScript API for doodads: New functions are available on the JavaScript API for doodads:
@ -82,6 +85,8 @@ Other changes:
caught and presented nicely in an in-game popup window. caught and presented nicely in an in-game popup window.
* When playing as the Bird, the flying animation now loops while the * When playing as the Bird, the flying animation now loops while the
player is staying still rather than pausing. player is staying still rather than pausing.
* The "Level" menu in Play Mode has options to restart the level or
retry from last checkpoint, in case a player got softlocked.
* When the game checks if there's an update available via * When the game checks if there's an update available via
<https://download.sketchymaze.com/version.json> it will send a user <https://download.sketchymaze.com/version.json> it will send a user
agent header like: "Sketchy Maze v0.10.2 on linux/amd64" sending only agent header like: "Sketchy Maze v0.10.2 on linux/amd64" sending only

View File

@ -144,9 +144,24 @@ function AI_ScanForPlayer() {
// If under control of the player character. // If under control of the player character.
function player() { function player() {
var playerSpeed = 12; let playerSpeed = 12,
diving = false,
falling = false;
// The player can dive by moving downwards and laterally, but
// de-cheese their ability to just sweep across the ground; if
// they aren't seen to be moving downwards, cancel the dive.
let lastPoint = Self.Position();
setInterval(() => {
let nowAt = Self.Position();
if (nowAt.Y > lastPoint.Y) {
falling = true;
} else {
falling = false;
}
lastPoint = nowAt;
}, 100);
Self.SetInventory(true);
Events.OnKeypress((ev) => { Events.OnKeypress((ev) => {
Vx = 0; Vx = 0;
Vy = 0; Vy = 0;
@ -158,31 +173,45 @@ function player() {
} }
// Dive! // Dive!
if (ev.Down && ev.Right) { if (ev.Down && ev.Right && falling) {
Self.StopAnimation(); Self.StopAnimation();
Self.ShowLayerNamed("dive-right"); Self.ShowLayerNamed("dive-right");
} else if (ev.Down && ev.Left) { diving = falling;
} else if (ev.Down && ev.Left && falling) {
Self.StopAnimation(); Self.StopAnimation();
Self.ShowLayerNamed("dive-left"); Self.ShowLayerNamed("dive-left");
diving = falling;
} else if (ev.Right) { } else if (ev.Right) {
// Fly right. // Fly right.
if (!Self.IsAnimating()) { if (!Self.IsAnimating()) {
Self.PlayAnimation("fly-right", null); Self.PlayAnimation("fly-right", null);
} }
Vx = playerSpeed; Vx = playerSpeed;
diving = false;
} else if (ev.Left) { } else if (ev.Left) {
// Fly left. // Fly left.
if (!Self.IsAnimating()) { if (!Self.IsAnimating()) {
Self.PlayAnimation("fly-left", null); Self.PlayAnimation("fly-left", null);
} }
Vx = -playerSpeed; Vx = -playerSpeed;
diving = false;
} else { } else {
// Hover in place. // Hover in place.
if (!Self.IsAnimating()) { if (!Self.IsAnimating()) {
Self.PlayAnimation("fly-"+direction); Self.PlayAnimation("fly-"+direction);
} }
diving = false;
} }
// Self.SetVelocity(Vector(Vx, Vy)); // Player is invulnerable while diving.
}) Self.SetInvulnerable(diving);
});
Events.OnCollide((e) => {
// If the player is diving at an enemy mob, destroy it.
if (diving && e.Settled && e.Actor.IsMobile() && !e.Actor.Invulnerable()) {
Sound.Play("crumbly-break.wav");
e.Actor.Destroy();
}
});
} }

View File

@ -5,6 +5,7 @@ const speed = 4,
function main() { function main() {
Self.SetMobile(true); Self.SetMobile(true);
Self.SetInvulnerable(true);
Self.SetGravity(true); Self.SetGravity(true);
Self.SetHitbox(0, 0, size, size); Self.SetHitbox(0, 0, size, size);

View File

@ -152,16 +152,6 @@ func (d *Doodle) Run() error {
log.Debug("Shell: opening shell") log.Debug("Shell: opening shell")
d.shell.Open = true d.shell.Open = true
} else { } else {
// Global event handlers.
if keybind.Shutdown(ev) {
if d.Debug { // fast exit in -debug mode.
d.running = false
} else {
d.ConfirmExit()
}
continue
}
if keybind.Help(ev) { if keybind.Help(ev) {
// Launch the local guidebook // Launch the local guidebook
native.OpenLocalURL(balance.GuidebookPath) native.OpenLocalURL(balance.GuidebookPath)
@ -174,6 +164,16 @@ func (d *Doodle) Run() error {
// Make sure no UI modals (alerts, confirms) // Make sure no UI modals (alerts, confirms)
// or loadscreen are currently visible. // or loadscreen are currently visible.
if !modal.Handled(ev) { if !modal.Handled(ev) {
// Global event handlers.
if keybind.Shutdown(ev) {
if d.Debug { // fast exit in -debug mode.
d.running = false
} else {
d.ConfirmExit()
}
continue
}
// Run the scene's logic. // Run the scene's logic.
err = d.Scene.Loop(d, ev) err = d.Scene.Loop(d, ev)
if err != nil { if err != nil {

View File

@ -85,7 +85,9 @@ func FromEvent(ev *event.State) State {
// Shutdown (Escape) signals the game to start closing down. // Shutdown (Escape) signals the game to start closing down.
func Shutdown(ev *event.State) bool { func Shutdown(ev *event.State) bool {
return ev.Escape result := ev.Escape
ev.Escape = false
return result
} }
// Help (F1) can be checked one time. // Help (F1) can be checked one time.

View File

@ -13,7 +13,7 @@ func Alert(message string, args ...interface{}) *Modal {
if !ready { if !ready {
panic("modal.Alert(): not ready") panic("modal.Alert(): not ready")
} else if current != nil { } else if current != nil {
return current current.Dismiss(false)
} }
// Reset the supervisor. // Reset the supervisor.
@ -22,6 +22,7 @@ func Alert(message string, args ...interface{}) *Modal {
m := &Modal{ m := &Modal{
title: "Alert", title: "Alert",
message: fmt.Sprintf(message, args...), message: fmt.Sprintf(message, args...),
cancelable: true,
} }
m.window = makeAlert(m) m.window = makeAlert(m)

View File

@ -12,7 +12,7 @@ func Confirm(message string, args ...interface{}) *Modal {
if !ready { if !ready {
panic("modal.Confirm(): not ready") panic("modal.Confirm(): not ready")
} else if current != nil { } else if current != nil {
return current current.Dismiss(false)
} }
// Reset the supervisor. // Reset the supervisor.
@ -21,6 +21,7 @@ func Confirm(message string, args ...interface{}) *Modal {
m := &Modal{ m := &Modal{
title: "Confirm", title: "Confirm",
message: fmt.Sprintf(message, args...), message: fmt.Sprintf(message, args...),
cancelable: true,
} }
m.window = makeConfirm(m) m.window = makeConfirm(m)

View File

@ -66,6 +66,12 @@ func Handled(ev *event.State) bool {
return true return true
} }
// Escape key cancels the modal.
if keybind.Shutdown(ev) && current.cancelable {
current.Dismiss(false)
return true
}
supervisor.Loop(ev) supervisor.Loop(ev)
// Has the window changed size? // Has the window changed size?
@ -111,6 +117,7 @@ type Modal struct {
message string message string
window *ui.Window window *ui.Window
callback func() callback func()
cancelable bool // Escape key can cancel the modal
} }
// WithTitle sets the title of the modal. // WithTitle sets the title of the modal.

View File

@ -435,6 +435,19 @@ func (s *PlayScene) installPlayerDoodad(filename string, spawn render.Point, cen
// 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!")
// If they didn't come from the Level Editor originally, e.g. they are in Story Mode,
// confirm they want the editor in case they accidentally hit the "E" key due to
// its proximity to the WASD keys.
if !s.CanEdit {
modal.Confirm("Open this level in the editor?").Then(s.doEditLevel)
} else {
s.doEditLevel()
}
}
// Common logic to transition into the Editor.
func (s *PlayScene) doEditLevel() {
gamepad.SetMode(gamepad.MouseMode) gamepad.SetMode(gamepad.MouseMode)
s.d.Goto(&EditorScene{ s.d.Goto(&EditorScene{
Filename: s.Filename, Filename: s.Filename,
@ -447,6 +460,7 @@ func (s *PlayScene) EditLevel() {
func (s *PlayScene) RestartLevel() { func (s *PlayScene) RestartLevel() {
log.Info("Restart Level") log.Info("Restart Level")
s.d.Goto(&PlayScene{ s.d.Goto(&PlayScene{
LevelPack: s.LevelPack,
Filename: s.Filename, Filename: s.Filename,
Level: s.Level, Level: s.Level,
CanEdit: s.CanEdit, CanEdit: s.CanEdit,
@ -637,8 +651,7 @@ func (s *PlayScene) Loop(d *Doodle, ev *event.State) error {
} }
// Switching to Edit Mode? // Switching to Edit Mode?
if s.CanEdit && keybind.GotoEdit(ev) { if keybind.GotoEdit(ev) {
gamepad.SetMode(gamepad.MouseMode)
s.EditLevel() s.EditLevel()
return nil return nil
} }

View File

@ -53,6 +53,12 @@ func (u *PlayScene) setupMenuBar(d *Doodle) *ui.MenuBar {
//////// ////////
// Level menu // Level menu
levelMenu := menu.AddMenu("Level") levelMenu := menu.AddMenu("Level")
levelMenu.AddItem("Restart level", u.RestartLevel)
levelMenu.AddItem("Retry from checkpoint", func() {
u.SetImperfect()
u.RetryCheckpoint()
})
levelMenu.AddSeparator()
levelMenu.AddItemAccel("Edit level", "E", u.EditLevel) levelMenu.AddItemAccel("Edit level", "E", u.EditLevel)
// Hilariously broken, someday! // Hilariously broken, someday!