Spit and polish

* New doodad: Invisible Warp Door
* All warp doors require the player to be grounded (if affected by
  gravity) to open them. No jumping or falling thru and opening
  a warp door mid-air!
* Title Screen now randomly selects from a couple of levels.
* Title Screen: if it fails to load a level it sets up a basic
  blank level with a wallpaper instead.
* New developer shell command: titlescreen <level>
  Opens the MainScene with a custom user level as the background.
* Add Auto-save to the Editor to save your drawing every 5 minutes
* Add a MenuBar to the Play Scene for easier navigation to other
  features of the game.
* Doodad JS API: time.Since() now available.
This commit is contained in:
Noah 2022-01-02 22:36:32 -08:00
parent 672ee9641a
commit 9a51ac39f9
20 changed files with 408 additions and 87 deletions

View File

@ -1,5 +1,64 @@
# Changes # Changes
## v0.11.0 (TBD)
New features:
* **High scores and level progression:** when playing levels out of
a Level Pack, the game will save your progress and high scores on
each level you play. See details on how scoring works so far, below.
* **Auto-save** for the Editor. Automatically saves your drawing every
5 minutes. Look for e.g. the _autosave.level to recover your drawing
if the game crashed or exited wrongly!
* **Color picker UI:** when asked to choose a color (e.g. for your level
palette) a UI window makes picking a color easy! You can still manually
enter a hexadecimal color value but this is no longer required!
Scoring system:
* The high score on a level is based on how quickly you complete it.
A timer is shown in-game and there are two possible high scores
for each level in a pack:
* Perfect Time (gold): if you complete the level without dying and
restarting at a checkpoint.
* Best Time (silver): if you had used a checkpoint.
* The gold/silver icon is displayed next to the timer in-game; it
starts gold and drops to silver if you die and restart from checkpoint.
It gives you a preview of which high score you'll be competing with.
* If cheat codes are used, the user is not eligible for a high score
but may still mark the level "completed."
* Level Packs may have some of their later levels locked by default,
with only one or a few available immediately. Completing a level will
unlock the next level until they have all been unlocked.
New and changed doodads:
* **Invisible Warp Door:** a technical doodad to create an invisible
Warp Door (press Space/'Use' key to activate).
* All **Warp Doors** now require the player to be grounded before they
can be opened, or else under the effects of antigravity. You shouldn't
be able to open Warp Doors while falling or jumping off the ground
at them.
Revised levels:
* Desert-2of2.level uses a new work-around for the unfortunate glitch
of sometimes getting stuck on two boxes, instead of a cheat code
being necessary to resolve.
* Revised difficulty on Tutorial 2 and Tutorial 3.
Miscellaneous changes:
* Title Screen: picks a random level from a few options, in the future
it will pick random user levels too.
* Play Mode gets a menu bar like the Editor for easier navigation to
other game features.
* New dev shell command: `titlescreen <level name>` to load the Title
Screen with the named level as its background, can be used to load
user levels _now_.
* For the doodads JavaScript API: `time.Since()` is now available (from
the Go standard library)
## v0.10.0 (Dec 30 2021) ## v0.10.0 (Dec 30 2021)
New features and changes: New features and changes:

View File

@ -23,6 +23,11 @@ build:
doodad convert -t "Power Source" power-64.png power-source.doodad doodad convert -t "Power Source" power-64.png power-source.doodad
doodad install-script power.js power-source.doodad doodad install-script power.js power-source.doodad
# Warp Door
doodad convert -t "Invisible Warp Door" warp-door-64.png reg-warp-door.doodad
doodad edit-doodad --tag "color=invisible" reg-warp-door.doodad
doodad install-script ../warp-door/warp-door.js reg-warp-door.doodad
for i in *.doodad; do\ for i in *.doodad; do\
doodad edit-doodad --tag "category=technical" $${i};\ doodad edit-doodad --tag "category=technical" $${i};\
done done

Binary file not shown.

After

Width:  |  Height:  |  Size: 821 B

View File

@ -17,7 +17,7 @@ build:
doodad install-script warp-door.js warp-door-orange.doodad doodad install-script warp-door.js warp-door-orange.doodad
for i in *.doodad; do\ for i in *.doodad; do\
doodad edit-doodad --tag "category=doors" $${i};\ doodad edit-doodad --tag "category=doors" --hitbox=34,76 $${i};\
done done
for i in warp-door-*.doodad; do\ for i in warp-door-*.doodad; do\
doodad edit-doodad --tag "category=doors,gizmos" $${i};\ doodad edit-doodad --tag "category=doors,gizmos" $${i};\

View File

@ -1,7 +1,5 @@
// Warp Doors // Warp Doors
function main() { function main() {
Self.SetHitbox(0, 0, 34, 76);
// Are we a blue or orange door? Regular warp door will be 'none' // Are we a blue or orange door? Regular warp door will be 'none'
var color = Self.GetTag("color"); var color = Self.GetTag("color");
var isStateDoor = color === 'blue' || color === 'orange'; var isStateDoor = color === 'blue' || color === 'orange';
@ -23,6 +21,11 @@ function main() {
Self.AddAnimation("close", animSpeed, ["orange-4", "orange-3", "orange-2", "orange-1"]); Self.AddAnimation("close", animSpeed, ["orange-4", "orange-3", "orange-2", "orange-1"]);
spriteDefault = "orange-1"; spriteDefault = "orange-1";
spriteDisabled = "orange-off"; spriteDisabled = "orange-off";
} else if (color === 'invisible') {
// Invisible Warp Door region.
Self.Hide();
Self.AddAnimation("open", animSpeed, [0, 0]);
Self.AddAnimation("close", animSpeed, [0, 0]);
} else { } else {
Self.AddAnimation("open", animSpeed, ["door-2", "door-3", "door-4"]); Self.AddAnimation("open", animSpeed, ["door-2", "door-3", "door-4"]);
Self.AddAnimation("close", animSpeed, ["door-4", "door-3", "door-2", "door-1"]); Self.AddAnimation("close", animSpeed, ["door-4", "door-3", "door-2", "door-1"]);
@ -48,6 +51,10 @@ function main() {
}); });
} }
// For player groundedness work-around
var playerLastY = []; // last sampling of Y values
var lastUsed = time.Now();
// The player Uses the door. // The player Uses the door.
var flashedCooldown = false; // "Locked Door" flashed message. var flashedCooldown = false; // "Locked Door" flashed message.
Events.OnUse(function(e) { Events.OnUse(function(e) {
@ -74,6 +81,40 @@ function main() {
return; return;
} }
// The player must be grounded or have no gravity to open the door.
if (!e.Actor.Grounded() && e.Actor.HasGravity()) {
// Work-around: if two Boxes are stacked atop each other the player can
// get stuck if he jumps on top. He may not be Grounded but isn't changing
// effective Y position and a warp door may work as a good way out.
var yValue = e.Actor.Position().Y;
// Collect a sampling of last few Y values. If the player Y position
// is constant the last handful of frames, treat them as if they're
// grounded (or else they can't activate the warp door).
playerLastY.unshift(yValue);
if (playerLastY.length < 6) {
return;
}
// We have enough history.
playerLastY.pop();
// Hasn't moved?
var isGrounded = true;
for (var i = 0; i < playerLastY.length; i++) {
if (yValue !== playerLastY[i]) {
isGrounded = false;
break;
}
}
if (!isGrounded) {
return;
}
// Player was effectively grounded! No change in Y position.
}
// Freeze the player. // Freeze the player.
e.Actor.Freeze() e.Actor.Freeze()

View File

@ -59,11 +59,18 @@ var (
DefaultEraserBrushSize = 8 DefaultEraserBrushSize = 8
MaxEraserBrushSize = 32 // the bigger, the slower MaxEraserBrushSize = 32 // the bigger, the slower
// Interval for auto-save in the editor
AutoSaveInterval = 5 * time.Minute
// Default player character doodad in Play Mode. // Default player character doodad in Play Mode.
PlayerCharacterDoodad = "boy.doodad" PlayerCharacterDoodad = "boy.doodad"
// Level name for the title screen. // Level names for the title screen.
DemoLevelName = "Tutorial 3.level" DemoLevelName = []string{
"Tutorial 1.level",
"Tutorial 2.level",
"Tutorial 3.level",
}
// Level attachment filename for the custom wallpaper. // Level attachment filename for the custom wallpaper.
// NOTE: due to hard-coded "assets/wallpapers/" prefix in uix/canvas.go#LoadLevel. // NOTE: due to hard-coded "assets/wallpapers/" prefix in uix/canvas.go#LoadLevel.

View File

@ -60,6 +60,8 @@ func (c Command) Run(d *Doodle) error {
return c.Play(d) return c.Play(d)
case "close": case "close":
return c.Close(d) return c.Close(d)
case "titlescreen":
return c.TitleScreen(d)
case "exit": case "exit":
case "quit": case "quit":
return c.Quit() return c.Quit()
@ -192,6 +194,9 @@ func (c Command) Help(d *Doodle) error {
d.Flash("Enter a JavaScript shell on the in-game interpreter") d.Flash("Enter a JavaScript shell on the in-game interpreter")
case "boolprop": case "boolprop":
d.Flash("Toggle boolean values. `boolProp list` lists available") d.Flash("Toggle boolean values. `boolProp list` lists available")
case "titlescreen":
d.Flash("Usage: titlescreen <filename.level>")
d.Flash("Open the title screen with a level")
case "help": case "help":
d.Flash("Usage: help <command>") d.Flash("Usage: help <command>")
d.Flash("Gets further help on a command") d.Flash("Gets further help on a command")
@ -252,6 +257,20 @@ func (c Command) Play(d *Doodle) error {
return nil return nil
} }
// TitleScreen loads the title with a custom user level.
func (c Command) TitleScreen(d *Doodle) error {
if len(c.Args) == 0 {
return errors.New("Usage: titlescreen <level name.level>")
}
filename := c.Args[0]
d.shell.Write("Playing level: " + filename)
d.Goto(&MainScene{
LevelFilename: filename,
})
return nil
}
// Quit the command line shell. // Quit the command line shell.
func (c Command) Quit() error { func (c Command) Quit() error {
return nil return nil

44
pkg/common_menubar.go Normal file
View File

@ -0,0 +1,44 @@
package doodle
import (
"git.kirsle.net/apps/doodle/pkg/balance"
"git.kirsle.net/apps/doodle/pkg/branding"
"git.kirsle.net/apps/doodle/pkg/native"
"git.kirsle.net/apps/doodle/pkg/windows"
"git.kirsle.net/go/render"
"git.kirsle.net/go/ui"
)
// Common menubars between Play and Edit.
// MakeHelpMenu creates the "Help" menu with its common items
// across any scene.
func (d *Doodle) MakeHelpMenu(menu *ui.MenuBar, supervisor *ui.Supervisor) *ui.MenuButton {
helpMenu := menu.AddMenu("Help")
helpMenu.AddItemAccel("User Manual", "F1", func() {
native.OpenLocalURL(balance.GuidebookPath)
})
helpMenu.AddItem("About", func() {
aboutWindow := windows.NewAboutWindow(windows.About{
Supervisor: supervisor,
Engine: d.Engine,
})
aboutWindow.Compute(d.Engine)
aboutWindow.Supervise(supervisor)
// Center the window.
aboutWindow.MoveTo(render.Point{
X: (d.width / 2) - (aboutWindow.Size().W / 2),
Y: 60,
})
aboutWindow.Show()
})
helpMenu.AddSeparator()
helpMenu.AddItem("Go to Website", func() {
native.OpenURL(branding.Website)
})
helpMenu.AddItem("Guidebook Online", func() {
native.OpenURL(branding.GuidebookURL)
})
return helpMenu
}

View File

@ -245,6 +245,7 @@ func (d *Doodle) MakeSettingsWindow(supervisor *ui.Supervisor) *ui.Window {
CrosshairSize: &usercfg.Current.CrosshairSize, CrosshairSize: &usercfg.Current.CrosshairSize,
CrosshairColor: &usercfg.Current.CrosshairColor, CrosshairColor: &usercfg.Current.CrosshairColor,
HideTouchHints: &usercfg.Current.HideTouchHints, HideTouchHints: &usercfg.Current.HideTouchHints,
DisableAutosave: &usercfg.Current.DisableAutosave,
} }
return windows.MakeSettingsWindow(d.width, d.height, cfg) return windows.MakeSettingsWindow(d.width, d.height, cfg)
} }

View File

@ -5,7 +5,9 @@ import (
"fmt" "fmt"
"os" "os"
"strings" "strings"
"time"
"git.kirsle.net/apps/doodle/pkg/balance"
"git.kirsle.net/apps/doodle/pkg/doodads" "git.kirsle.net/apps/doodle/pkg/doodads"
"git.kirsle.net/apps/doodle/pkg/drawtool" "git.kirsle.net/apps/doodle/pkg/drawtool"
"git.kirsle.net/apps/doodle/pkg/enum" "git.kirsle.net/apps/doodle/pkg/enum"
@ -47,6 +49,8 @@ type EditorScene struct {
// Last saved filename by the user. // Last saved filename by the user.
filename string filename string
lastAutosaveAt time.Time
} }
// Name of the scene. // Name of the scene.
@ -66,6 +70,9 @@ func (s *EditorScene) Setup(d *Doodle) error {
{"Swatch:", s.debSwatch}, {"Swatch:", s.debSwatch},
} }
// Initialize autosave time.
s.lastAutosaveAt = time.Now()
// Show the loading screen. // Show the loading screen.
loadscreen.ShowWithProgress() loadscreen.ShowWithProgress()
go func() { go func() {
@ -424,6 +431,16 @@ func (s *EditorScene) Loop(d *Doodle, ev *event.State) error {
s.UI.Loop(ev) s.UI.Loop(ev)
// Trigger auto-save of the level in case of crash or accidental closure.
if time.Since(s.lastAutosaveAt) > balance.AutoSaveInterval {
s.lastAutosaveAt = time.Now()
if !usercfg.Current.DisableAutosave {
if err := s.AutoSave(); err != nil {
d.FlashError("Autosave error: %s", err)
}
}
}
return nil return nil
} }
@ -471,7 +488,6 @@ func (s *EditorScene) LoadLevel(filename string) error {
} }
// SaveLevel saves the level to disk. // SaveLevel saves the level to disk.
// TODO: move this into the Canvas?
func (s *EditorScene) SaveLevel(filename string) error { func (s *EditorScene) SaveLevel(filename string) error {
if s.DrawingType != enum.LevelDrawing { if s.DrawingType != enum.LevelDrawing {
return errors.New("SaveLevel: current drawing is not a Level type") return errors.New("SaveLevel: current drawing is not a Level type")
@ -500,6 +516,24 @@ func (s *EditorScene) SaveLevel(filename string) error {
return m.WriteFile(filename) return m.WriteFile(filename)
} }
// AutoSave takes an autosave snapshot of the level or drawing.
func (s *EditorScene) AutoSave() error {
var filename = "_autosave.level"
switch s.DrawingType {
case enum.LevelDrawing:
s.d.Flash("Automatically saved level to %s", filename)
return s.Level.WriteFile(filename)
case enum.DoodadDrawing:
filename = "_autosave.doodad"
s.d.Flash("Automatically saved doodad to %s", filename)
return s.Doodad.WriteFile(filename)
}
return nil
}
// LoadDoodad loads a doodad from disk. // LoadDoodad loads a doodad from disk.
func (s *EditorScene) LoadDoodad(filename string) error { func (s *EditorScene) LoadDoodad(filename string) error {
s.filename = filename s.filename = filename

View File

@ -6,12 +6,13 @@ package doodle
import ( import (
"git.kirsle.net/apps/doodle/pkg/balance" "git.kirsle.net/apps/doodle/pkg/balance"
"git.kirsle.net/apps/doodle/pkg/branding"
"git.kirsle.net/apps/doodle/pkg/drawtool" "git.kirsle.net/apps/doodle/pkg/drawtool"
"git.kirsle.net/apps/doodle/pkg/enum" "git.kirsle.net/apps/doodle/pkg/enum"
"git.kirsle.net/apps/doodle/pkg/level/giant_screenshot" "git.kirsle.net/apps/doodle/pkg/level/giant_screenshot"
"git.kirsle.net/apps/doodle/pkg/license"
"git.kirsle.net/apps/doodle/pkg/log" "git.kirsle.net/apps/doodle/pkg/log"
"git.kirsle.net/apps/doodle/pkg/native" "git.kirsle.net/apps/doodle/pkg/native"
"git.kirsle.net/apps/doodle/pkg/usercfg"
"git.kirsle.net/apps/doodle/pkg/userdir" "git.kirsle.net/apps/doodle/pkg/userdir"
"git.kirsle.net/apps/doodle/pkg/windows" "git.kirsle.net/apps/doodle/pkg/windows"
"git.kirsle.net/go/render" "git.kirsle.net/go/render"
@ -143,20 +144,22 @@ func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.MenuBar {
native.OpenLocalURL(userdir.ScreenshotDirectory) native.OpenLocalURL(userdir.ScreenshotDirectory)
}) })
levelMenu.AddSeparator() if usercfg.Current.EnableFeatures {
levelMenu.AddItemAccel("New viewport", "v", func() { levelMenu.AddSeparator()
pip := windows.MakePiPWindow(d.width, d.height, windows.PiP{ levelMenu.AddItemAccel("New viewport", "v", func() {
Supervisor: u.Supervisor, pip := windows.MakePiPWindow(d.width, d.height, windows.PiP{
Engine: u.d.Engine, Supervisor: u.Supervisor,
Level: u.Scene.Level, Engine: u.d.Engine,
Event: u.d.event, Level: u.Scene.Level,
Event: u.d.event,
Tool: &u.Scene.UI.Canvas.Tool, Tool: &u.Scene.UI.Canvas.Tool,
BrushSize: &u.Scene.UI.Canvas.BrushSize, BrushSize: &u.Scene.UI.Canvas.BrushSize,
})
pip.Show()
}) })
}
pip.Show()
})
} }
//////// ////////
@ -261,36 +264,16 @@ func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.MenuBar {
//////// ////////
// Help menu // Help menu
helpMenu := menu.AddMenu("Help") var (
helpMenu.AddItemAccel("User Manual", "F1", func() { helpMenu = u.d.MakeHelpMenu(menu, u.Supervisor)
native.OpenLocalURL(balance.GuidebookPath) registerText = "Register"
}) )
helpMenu.AddItem("Register", func() {
u.licenseWindow.Show()
})
helpMenu.AddItem("About", func() {
if u.aboutWindow == nil {
u.aboutWindow = windows.NewAboutWindow(windows.About{
Supervisor: u.Supervisor,
Engine: d.Engine,
})
u.aboutWindow.Compute(d.Engine)
u.aboutWindow.Supervise(u.Supervisor)
// Center the window.
u.aboutWindow.MoveTo(render.Point{
X: (d.width / 2) - (u.aboutWindow.Size().W / 2),
Y: 60,
})
}
u.aboutWindow.Show()
})
helpMenu.AddSeparator() helpMenu.AddSeparator()
helpMenu.AddItem("Go to Website", func() { if license.IsRegistered() {
native.OpenURL(branding.Website) registerText = "Registration"
}) }
helpMenu.AddItem("Guidebook Online", func() { helpMenu.AddItem(registerText, func() {
native.OpenURL(branding.GuidebookURL) u.licenseWindow.Show()
}) })
menu.Supervise(u.Supervisor) menu.Supervise(u.Supervisor)

View File

@ -2,6 +2,7 @@ package doodle
import ( import (
"fmt" "fmt"
"math/rand"
"git.kirsle.net/apps/doodle/pkg/balance" "git.kirsle.net/apps/doodle/pkg/balance"
"git.kirsle.net/apps/doodle/pkg/branding" "git.kirsle.net/apps/doodle/pkg/branding"
@ -24,7 +25,8 @@ import (
// MainScene implements the main menu of Doodle. // MainScene implements the main menu of Doodle.
type MainScene struct { type MainScene struct {
Supervisor *ui.Supervisor Supervisor *ui.Supervisor
LevelFilename string // custom level filename to load in background
// Background wallpaper canvas. // Background wallpaper canvas.
scripting *scripting.Supervisor scripting *scripting.Supervisor
@ -296,8 +298,16 @@ func (s *MainScene) SetupDemoLevel(d *Doodle) error {
s.scripting = scripting.NewSupervisor() s.scripting = scripting.NewSupervisor()
s.canvas.SetScriptSupervisor(s.scripting) s.canvas.SetScriptSupervisor(s.scripting)
// Title screen level to load. // Title screen level to load. Pick a random level.
if lvl, err := level.LoadFile(balance.DemoLevelName); err == nil { levelName := balance.DemoLevelName[0]
if s.LevelFilename != "" {
levelName = s.LevelFilename
} else if len(balance.DemoLevelName) > 1 {
randIndex := rand.Intn(len(balance.DemoLevelName))
levelName = balance.DemoLevelName[randIndex]
}
if lvl, err := level.LoadFile(levelName); err == nil {
s.canvas.LoadLevel(lvl) s.canvas.LoadLevel(lvl)
s.canvas.InstallActors(lvl.Actors) s.canvas.InstallActors(lvl.Actors)
@ -312,6 +322,16 @@ func (s *MainScene) SetupDemoLevel(d *Doodle) error {
} }
} else { } else {
log.Error("Error loading demo level %s: %s", balance.DemoLevelName, err) log.Error("Error loading demo level %s: %s", balance.DemoLevelName, err)
// Create a basic notebook level.
s.canvas.LoadLevel(&level.Level{
Chunker: level.NewChunker(100),
Palette: level.NewPalette(),
PageType: level.Bounded,
MaxWidth: 42,
MaxHeight: 42,
Wallpaper: "notebook.png",
})
} }
return nil return nil

View File

@ -50,9 +50,11 @@ type PlayScene struct {
cheated bool // user has entered a cheat code while playing cheated bool // user has entered a cheat code while playing
// UI widgets. // UI widgets.
supervisor *ui.Supervisor supervisor *ui.Supervisor
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 menubar *ui.MenuBar
editButton *ui.Button
winLevelPacks *ui.Window
// Custom debug labels. // Custom debug labels.
debPosition *string debPosition *string
@ -119,6 +121,13 @@ func (s *PlayScene) setupAsync(d *Doodle) error {
s.screen = ui.NewFrame("Screen") s.screen = ui.NewFrame("Screen")
s.screen.Resize(render.NewRect(d.width, d.height)) s.screen.Resize(render.NewRect(d.width, d.height))
// Menu Bar
s.menubar = s.setupMenuBar(d)
s.screen.Pack(s.menubar, ui.Pack{
Side: ui.N,
FillX: true,
})
// Level Exit handler. // Level Exit handler.
s.scripting.OnLevelExit(s.BeatLevel) s.scripting.OnLevelExit(s.BeatLevel)
s.scripting.OnLevelFail(s.FailLevel) s.scripting.OnLevelFail(s.FailLevel)
@ -620,6 +629,9 @@ func (s *PlayScene) Draw(d *Doodle) error {
// Visualize the touch regions? // Visualize the touch regions?
s.DrawTouchable() s.DrawTouchable()
// Let Supervisor draw menus
s.supervisor.Present(d.Engine)
return nil return nil
} }

79
pkg/play_scene_menubar.go Normal file
View File

@ -0,0 +1,79 @@
package doodle
import (
"git.kirsle.net/apps/doodle/pkg/levelpack"
"git.kirsle.net/apps/doodle/pkg/shmem"
"git.kirsle.net/apps/doodle/pkg/usercfg"
"git.kirsle.net/apps/doodle/pkg/windows"
"git.kirsle.net/go/render"
"git.kirsle.net/go/ui"
)
// Set up the menu bar for Play Scene.
func (u *PlayScene) setupMenuBar(d *Doodle) *ui.MenuBar {
menu := ui.NewMenuBar("Main Menu")
////////
// Game menu
gameMenu := menu.AddMenu("Game")
gameMenu.AddItem("Story Mode", func() {
// TODO: de-duplicate code from MainScene
if u.winLevelPacks == nil {
u.winLevelPacks = windows.NewLevelPackWindow(windows.LevelPack{
Supervisor: u.supervisor,
Engine: d.Engine,
OnPlayLevel: func(lp levelpack.LevelPack, which levelpack.Level) {
if err := d.PlayFromLevelpack(lp, which); err != nil {
shmem.FlashError(err.Error())
}
},
OnCloseWindow: func() {
u.winLevelPacks.Hide()
},
})
}
u.winLevelPacks.MoveTo(render.Point{
X: (d.width / 2) - (u.winLevelPacks.Size().W / 2),
Y: (d.height / 2) - (u.winLevelPacks.Size().H / 2),
})
u.winLevelPacks.Show()
})
gameMenu.AddItemAccel("New drawing", "Ctrl-N", d.GotoNewMenu)
gameMenu.AddItemAccel("Open drawing", "Ctrl-O", d.GotoLoadMenu)
gameMenu.AddSeparator()
gameMenu.AddItem("Quit to menu", func() {
d.Goto(&MainScene{})
})
gameMenu.AddItemAccel("Quit", "Escape", func() {
d.ConfirmExit()
})
////////
// Level menu
levelMenu := menu.AddMenu("Level")
levelMenu.AddItemAccel("Edit level", "E", u.EditLevel)
// Hilariously broken, someday!
if usercfg.Current.EnableFeatures {
levelMenu.AddSeparator()
levelMenu.AddItemAccel("New viewport", "v", func() {
pip := windows.MakePiPWindow(d.width, d.height, windows.PiP{
Supervisor: u.supervisor,
Engine: u.d.Engine,
Level: u.Level,
Event: u.d.event,
})
pip.Show()
})
}
d.MakeHelpMenu(menu, u.supervisor)
menu.Supervise(u.supervisor)
menu.Compute(d.Engine)
return menu
}

View File

@ -28,6 +28,13 @@ func (s *PlayScene) LoopTouchable(ev *event.State) {
cursor = render.NewPoint(ev.CursorX, ev.CursorY) cursor = render.NewPoint(ev.CursorX, ev.CursorY)
) )
// Don't do any of this if the mouse is over the menu bar, so
// clicking on the menus doesn't make the character move or jump.
if cursor.Inside(s.menubar.Rect()) || s.supervisor.GetModal() != nil ||
s.supervisor.IsPointInWindow(cursor) {
return
}
// Detect if the player is idle. // Detect if the player is idle.
// Idle means that they are not holding any directional or otherwise input key. // Idle means that they are not holding any directional or otherwise input key.
// Keyboard inputs and touch events from this function will set these keys. // Keyboard inputs and touch events from this function will set these keys.

View File

@ -42,7 +42,8 @@ func NewJSProxy(vm *VM) JSProxy {
return shmem.Tick return shmem.Tick
}, },
"time": map[string]interface{}{ "time": map[string]interface{}{
"Now": time.Now, "Now": time.Now,
"Since": time.Since,
"Add": func(t time.Time, ms int64) time.Time { "Add": func(t time.Time, ms int64) time.Time {
return t.Add(time.Duration(ms) * time.Millisecond) return t.Add(time.Duration(ms) * time.Millisecond)
}, },

View File

@ -36,6 +36,7 @@ type Settings struct {
CrosshairSize int `json:",omitempty"` CrosshairSize int `json:",omitempty"`
CrosshairColor render.Color CrosshairColor render.Color
HideTouchHints bool `json:",omitempty"` HideTouchHints bool `json:",omitempty"`
DisableAutosave bool `json:",omitempty"`
// Secret boolprops from balance/boolprops.go // Secret boolprops from balance/boolprops.go
ShowHiddenDoodads bool `json:",omitempty"` ShowHiddenDoodads bool `json:",omitempty"`

View File

@ -39,7 +39,7 @@ func NewLevelPackWindow(config LevelPack) *ui.Window {
// size of the popup window // size of the popup window
width = 320 width = 320
height = 340 height = 360
) )
// Get the available .levelpack files. // Get the available .levelpack files.
@ -275,7 +275,7 @@ func (config LevelPack) makeDetailScreen(frame *ui.Frame, width, height int, lp
buttonWidth = width - 40 buttonWidth = width - 40
page = 1 page = 1
perPage = 3 perPage = 4
pages = int( pages = int(
math.Ceil( math.Ceil(
float64(len(lp.Levels)) / float64(perPage), float64(len(lp.Levels)) / float64(perPage),

View File

@ -77,25 +77,37 @@ func NewPiPWindow(cfg PiP) *ui.Window {
canvas.Editable = true canvas.Editable = true
canvas.Resize(render.NewRect(canvasWidth, canvasHeight)) canvas.Resize(render.NewRect(canvasWidth, canvasHeight))
// If we have tool bindings to edit in PiP window
var (
editable bool
curTool drawtool.Tool
curThicc int
)
if cfg.Tool != nil && cfg.BrushSize != nil {
editable = true
curTool = *cfg.Tool
curThicc = *cfg.BrushSize
canvas.Tool = curTool
}
// NOTE: my UI toolkit calls this every tick, if this is "fixed" // NOTE: my UI toolkit calls this every tick, if this is "fixed"
// in the future make one that does. // in the future make one that does.
var (
curTool = *cfg.Tool
curThicc = *cfg.BrushSize
)
canvas.Tool = curTool
window.Handle(ui.MouseMove, func(ed ui.EventData) error { window.Handle(ui.MouseMove, func(ed ui.EventData) error {
canvas.Loop(cfg.Event) canvas.Loop(cfg.Event)
// Check if bound values have modified. // Did we have tool bindings for an editable PiP?
if *cfg.Tool != curTool { if editable {
curTool = *cfg.Tool // Check if bound values have modified.
canvas.Tool = curTool if *cfg.Tool != curTool {
} curTool = *cfg.Tool
if *cfg.BrushSize != curThicc { canvas.Tool = curTool
curThicc = *cfg.BrushSize }
canvas.BrushSize = curThicc if *cfg.BrushSize != curThicc {
curThicc = *cfg.BrushSize
canvas.BrushSize = curThicc
}
} }
return nil return nil
}) })

View File

@ -29,6 +29,7 @@ type Settings struct {
CrosshairSize *int CrosshairSize *int
CrosshairColor *render.Color CrosshairColor *render.Color
HideTouchHints *bool HideTouchHints *bool
DisableAutosave *bool
// Configuration options. // Configuration options.
SceneName string // name of scene which called this window SceneName string // name of scene which called this window
@ -142,6 +143,12 @@ func (c Settings) makeOptionsTab(tabFrame *ui.TabFrame, Width, Height int) *ui.F
PadX: 4, PadX: 4,
name: "toolbars", name: "toolbars",
}, },
{
Boolean: c.DisableAutosave,
Text: "Disable auto-save in the Editor",
PadX: 4,
name: "autosave",
},
{ {
Integer: c.CrosshairSize, Integer: c.CrosshairSize,
Text: "Editor: Crosshair size (0 to disable):", Text: "Editor: Crosshair size (0 to disable):",
@ -170,9 +177,7 @@ func (c Settings) makeOptionsTab(tabFrame *ui.TabFrame, Width, Height int) *ui.F
}, },
{ {
Text: "Levels and doodads you create in-game are placed in your\n" + Text: "Levels and doodads you create in-game are placed in your\n" +
"Profile Directory. This is also where you can place content made\n" + "Profile Directory, which you can access below:",
"by others to use them in your game. Click on the button below\n" +
"to (hopefully) be taken to your Profile Directory:",
}, },
} }
for _, row := range rows { for _, row := range rows {
@ -634,20 +639,11 @@ func (c Settings) makeExperimentalTab(tabFrame *ui.TabFrame, Width, Height int)
PadY: 2, PadY: 2,
}, },
{ {
Header: "Zoom In/Out", Header: "Viewport window",
}, },
{ {
Text: "This adds Zoom options to the level editor. It has a few\n" + Text: "This option in the Level menu opens another view into\n" +
"bugs around scrolling but may be useful already.", "the level. Has glitchy wallpaper problems.",
PadY: 2,
},
{
Header: "Replace Level Palette",
},
{
Text: "This adds an option to the Level Properties dialog to\n" +
"replace your level palette with one of the defaults,\n" +
"like on the New Level screen. It might not actually work.",
PadY: 2, PadY: 2,
}, },
{ {