Async Giant Screenshot, Player Physics and UI Polish

* The "Giant Screenshot" feature takes a very long time, so it is made
  asynchronous. If you try and run a second one while the first is busy,
  you get an error flash. You can continue editing the level, even
  playtest it, or load a different level, and it will continue crunching
  on the Giant Screenshot and flash when it's finished.
* Updated the player physics to use proper Velocity to jump off the
  ground rather than the hacky timer-based fixed speed approach.
* FlashError() function to flash "error level" messages to the screen.
  They appear in orange text instead of the usual blue, and most error
  messages in the game use this now. The dev console "error <msg>"
  command can simulate an error message.
* Flashed message fonts are updated. The blue font now uses softer
  stroke and shadow colors and the same algorithm applies to the orange
  error flashes.

Some other changes to player physics:

* Max velocity, acceleration speed, and gravity have been tweaked.
* Fast turn-around if you are moving right and then need to go left.
  Your velocity resets to zero at the transition so you quickly get
  going the way you want to go.

Some levels that need a bit of love for the new platforming physics:

* Tutorial 3.level
This commit is contained in:
Noah 2021-10-07 18:24:18 -07:00
parent 0b0af70a62
commit fb5a8a1ae8
13 changed files with 147 additions and 65 deletions

View File

@ -23,10 +23,11 @@ var (
} }
// Player speeds // Player speeds
PlayerMaxVelocity float64 = 6 PlayerMaxVelocity float64 = 7
PlayerAcceleration float64 = 0.9 PlayerJumpVelocity float64 = -20
PlayerAcceleration float64 = 0.12
Gravity float64 = 6 Gravity float64 = 6
GravityAcceleration float64 = 0.2 GravityAcceleration float64 = 0.1
SlopeMaxHeight = 8 // max pixel height for player to walk up a slope SlopeMaxHeight = 8 // max pixel height for player to walk up a slope
// Default chunk size for canvases. // Default chunk size for canvases.

View File

@ -71,6 +71,20 @@ var (
WindowBackground = render.MustHexColor("#cdb689") WindowBackground = render.MustHexColor("#cdb689")
WindowBorder = render.Grey WindowBorder = render.Grey
// Developer Shell and Flashed Messages styles.
FlashStrokeDarken = 60
FlashShadowDarken = 120
FlashFont = func(text string) render.Text {
return render.Text{
Text: text,
Size: 18,
Color: render.SkyBlue,
Stroke: render.SkyBlue.Darken(FlashStrokeDarken),
Shadow: render.SkyBlue.Darken(FlashShadowDarken),
}
}
FlashErrorColor = render.MustHexColor("#FF9900")
// Menu bar styles. // Menu bar styles.
MenuBackground = render.Black MenuBackground = render.Black
MenuFont = render.Text{ MenuFont = render.Text{

View File

@ -28,7 +28,7 @@ func (c Command) cheatCommand(d *Doodle) bool {
playScene.drawing.Editable = true playScene.drawing.Editable = true
d.Flash("Level canvas is now editable. Don't edit and drive!") d.Flash("Level canvas is now editable. Don't edit and drive!")
} else { } else {
d.Flash("Use this cheat in Play Mode to make the level canvas editable.") d.FlashError("Use this cheat in Play Mode to make the level canvas editable.")
} }
case "scroll scroll scroll your boat": case "scroll scroll scroll your boat":
@ -36,7 +36,7 @@ func (c Command) cheatCommand(d *Doodle) bool {
playScene.drawing.Scrollable = true playScene.drawing.Scrollable = true
d.Flash("Level canvas is now scrollable with the arrow keys.") d.Flash("Level canvas is now scrollable with the arrow keys.")
} else { } else {
d.Flash("Use this cheat in Play Mode to make the level scrollable.") d.FlashError("Use this cheat in Play Mode to make the level scrollable.")
} }
case "import antigravity": case "import antigravity":
@ -50,7 +50,7 @@ func (c Command) cheatCommand(d *Doodle) bool {
d.Flash("Gravity restored for player character.") d.Flash("Gravity restored for player character.")
} }
} else { } else {
d.Flash("Use this cheat in Play Mode to disable gravity for the player character.") d.FlashError("Use this cheat in Play Mode to disable gravity for the player character.")
} }
case "ghost mode": case "ghost mode":
@ -67,7 +67,7 @@ func (c Command) cheatCommand(d *Doodle) bool {
d.Flash("Clipping and gravity restored for player character.") d.Flash("Clipping and gravity restored for player character.")
} }
} else { } else {
d.Flash("Use this cheat in Play Mode to disable clipping for the player character.") d.FlashError("Use this cheat in Play Mode to disable clipping for the player character.")
} }
case "show all actors": case "show all actors":
@ -77,7 +77,7 @@ func (c Command) cheatCommand(d *Doodle) bool {
} }
d.Flash("All invisible actors made visible.") d.Flash("All invisible actors made visible.")
} else { } else {
d.Flash("Use this cheat in Play Mode to show hidden actors, such as technical doodads.") d.FlashError("Use this cheat in Play Mode to show hidden actors, such as technical doodads.")
} }
case "give all keys": case "give all keys":
@ -89,7 +89,7 @@ func (c Command) cheatCommand(d *Doodle) bool {
playScene.Player.AddItem("small-key.doodad", 99) playScene.Player.AddItem("small-key.doodad", 99)
d.Flash("Given all keys to the player character.") d.Flash("Given all keys to the player character.")
} else { } else {
d.Flash("Use this cheat in Play Mode to get all colored keys.") d.FlashError("Use this cheat in Play Mode to get all colored keys.")
} }
case "drop all items": case "drop all items":
@ -97,7 +97,7 @@ func (c Command) cheatCommand(d *Doodle) bool {
playScene.Player.ClearInventory() playScene.Player.ClearInventory()
d.Flash("Cleared inventory of player character.") d.Flash("Cleared inventory of player character.")
} else { } else {
d.Flash("Use this cheat in Play Mode to clear your inventory.") d.FlashError("Use this cheat in Play Mode to clear your inventory.")
} }
case "fly like a bird": case "fly like a bird":

View File

@ -39,6 +39,9 @@ func (c Command) Run(d *Doodle) error {
case "echo": case "echo":
d.Flash(c.ArgsLiteral) d.Flash(c.ArgsLiteral)
return nil return nil
case "error":
d.FlashError(c.ArgsLiteral)
return nil
case "alert": case "alert":
modal.Alert(c.ArgsLiteral) modal.Alert(c.ArgsLiteral)
return nil return nil
@ -104,13 +107,13 @@ func (c Command) Close(d *Doodle) error {
// ExtractBindata dumps the app's embedded bindata to the filesystem. // ExtractBindata dumps the app's embedded bindata to the filesystem.
func (c Command) ExtractBindata(d *Doodle, path string) error { func (c Command) ExtractBindata(d *Doodle, path string) error {
if len(path) == 0 || path[0] != '/' { if len(path) == 0 || path[0] != '/' {
d.Flash("Required: an absolute path to a directory to extract to.") d.FlashError("Required: an absolute path to a directory to extract to.")
return nil return nil
} }
err := os.MkdirAll(path, 0755) err := os.MkdirAll(path, 0755)
if err != nil { if err != nil {
d.Flash("MkdirAll: %s", err) d.FlashError("MkdirAll: %s", err)
return err return err
} }
@ -120,7 +123,7 @@ func (c Command) ExtractBindata(d *Doodle, path string) error {
data, err := assets.Asset(filename) data, err := assets.Asset(filename)
if err != nil { if err != nil {
d.Flash("error on file %s: %s", filename, err) d.FlashError("error on file %s: %s", filename, err)
continue continue
} }
@ -131,7 +134,7 @@ func (c Command) ExtractBindata(d *Doodle, path string) error {
fh, err := os.Create(outfile) fh, err := os.Create(outfile)
if err != nil { if err != nil {
d.Flash("error writing file %s: %s", outfile, err) d.FlashError("error writing file %s: %s", outfile, err)
continue continue
} }
fh.Write(data) fh.Write(data)
@ -145,7 +148,7 @@ func (c Command) ExtractBindata(d *Doodle, path string) error {
// Help prints the help info. // Help prints the help info.
func (c Command) Help(d *Doodle) error { func (c Command) Help(d *Doodle) error {
if len(c.Args) == 0 { if len(c.Args) == 0 {
d.Flash("Available commands: new save edit play quit echo") d.Flash("Available commands: new save edit play quit echo error")
d.Flash(" alert clear help boolProp eval repl") d.Flash(" alert clear help boolProp eval repl")
d.Flash("Type `help` and then the command, like: `help edit`") d.Flash("Type `help` and then the command, like: `help edit`")
return nil return nil
@ -155,6 +158,9 @@ func (c Command) Help(d *Doodle) error {
case "echo": case "echo":
d.Flash("Usage: echo <message>") d.Flash("Usage: echo <message>")
d.Flash("Flash a message back to the console") d.Flash("Flash a message back to the console")
case "error":
d.Flash("Usage: error <message>")
d.Flash("Flash an error message back to the console")
case "alert": case "alert":
d.Flash("Usage: alert <message>") d.Flash("Usage: alert <message>")
d.Flash("Pop up an Alert box with a custom message") d.Flash("Pop up an Alert box with a custom message")
@ -294,7 +300,7 @@ func (c Command) BoolProp(d *Doodle) error {
} else { } else {
// Try the global boolProps in balance package. // Try the global boolProps in balance package.
if err := balance.BoolProp(name, truthy); err != nil { if err := balance.BoolProp(name, truthy); err != nil {
d.Flash("%s", err) d.FlashError("%s", err)
} else { } else {
d.Flash("%s: %+v", name, truthy) d.Flash("%s: %+v", name, truthy)
} }
@ -307,7 +313,7 @@ func (c Command) BoolProp(d *Doodle) error {
func (c Command) RunScript(d *Doodle, code interface{}) (otto.Value, error) { func (c Command) RunScript(d *Doodle, code interface{}) (otto.Value, error) {
defer func() { defer func() {
if err := recover(); err != nil { if err := recover(); err != nil {
d.Flash("Panic: %s", err) d.FlashError("Command.RunScript: Panic: %s", err)
} }
}() }()
out, err := d.shell.js.Run(code) out, err := d.shell.js.Run(code)

View File

@ -269,7 +269,7 @@ func (d *Doodle) NewDoodad(size int) {
if answer != "" { if answer != "" {
i, err := strconv.Atoi(answer) i, err := strconv.Atoi(answer)
if err != nil { if err != nil {
d.Flash("Error: Doodad size must be a number.") d.FlashError("Error: Doodad size must be a number.")
return return
} }
size = i size = i
@ -277,7 +277,7 @@ func (d *Doodle) NewDoodad(size int) {
// Recurse with the proper answer. // Recurse with the proper answer.
if size <= 0 { if size <= 0 {
d.Flash("Error: Doodad size must be a positive number.") d.FlashError("Error: Doodad size must be a positive number.")
} }
d.NewDoodad(size) d.NewDoodad(size)
}) })

View File

@ -127,7 +127,7 @@ func (s *EditorScene) setupAsync(d *Doodle) error {
"Opening: " + s.filename, "Opening: " + s.filename,
) )
if err := s.LoadLevel(s.filename); err != nil { if err := s.LoadLevel(s.filename); err != nil {
d.Flash("LoadLevel error: %s", err) d.FlashError("LoadLevel error: %s", err)
} else { } else {
s.UI.Canvas.InstallActors(s.Level.Actors) s.UI.Canvas.InstallActors(s.Level.Actors)
} }
@ -138,7 +138,7 @@ func (s *EditorScene) setupAsync(d *Doodle) error {
if usercfg.Current.WriteLockOverride { if usercfg.Current.WriteLockOverride {
d.Flash("Note: write lock has been overridden") d.Flash("Note: write lock has been overridden")
} else { } else {
d.Flash("That level is write-protected and cannot be viewed in the editor.") d.FlashError("That level is write-protected and cannot be viewed in the editor.")
s.Level = nil s.Level = nil
s.UI.Canvas.ClearActors() s.UI.Canvas.ClearActors()
s.filename = "" s.filename = ""
@ -165,7 +165,7 @@ func (s *EditorScene) setupAsync(d *Doodle) error {
if s.filename != "" && s.OpenFile { if s.filename != "" && s.OpenFile {
log.Debug("EditorScene.Setup: Loading doodad from filename at %s", s.filename) log.Debug("EditorScene.Setup: Loading doodad from filename at %s", s.filename)
if err := s.LoadDoodad(s.filename); err != nil { if err := s.LoadDoodad(s.filename); err != nil {
d.Flash("LoadDoodad error: %s", err) d.FlashError("LoadDoodad error: %s", err)
} }
} }
@ -174,7 +174,7 @@ func (s *EditorScene) setupAsync(d *Doodle) error {
if usercfg.Current.WriteLockOverride { if usercfg.Current.WriteLockOverride {
d.Flash("Note: write lock has been overridden") d.Flash("Note: write lock has been overridden")
} else { } else {
d.Flash("That doodad is write-protected and cannot be viewed in the editor.") d.FlashError("That doodad is write-protected and cannot be viewed in the editor.")
s.Doodad = nil s.Doodad = nil
s.filename = "" s.filename = ""
} }

View File

@ -33,7 +33,7 @@ func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.MenuBar {
drawingType = "level" drawingType = "level"
saveFunc = func(filename string) { saveFunc = func(filename string) {
if err := u.Scene.SaveLevel(filename); err != nil { if err := u.Scene.SaveLevel(filename); err != nil {
d.Flash("Error: %s", err) d.FlashError("Error: %s", err)
} else { } else {
d.Flash("Saved level: %s", filename) d.Flash("Saved level: %s", filename)
} }
@ -42,13 +42,13 @@ func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.MenuBar {
drawingType = "doodad" drawingType = "doodad"
saveFunc = func(filename string) { saveFunc = func(filename string) {
if err := u.Scene.SaveDoodad(filename); err != nil { if err := u.Scene.SaveDoodad(filename); err != nil {
d.Flash("Error: %s", err) d.FlashError("Error: %s", err)
} else { } else {
d.Flash("Saved doodad: %s", filename) d.Flash("Saved doodad: %s", filename)
} }
} }
default: default:
d.Flash("Error: Scene.DrawingType is not a valid type") d.FlashError("Error: Scene.DrawingType is not a valid type")
} }
//////// ////////
@ -127,13 +127,17 @@ func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.MenuBar {
levelMenu.AddSeparator() levelMenu.AddSeparator()
levelMenu.AddItem("Giant Screenshot", func() { levelMenu.AddItem("Giant Screenshot", func() {
filename, err := giant_screenshot.SaveGiantScreenshot(u.Scene.Level) // It takes a LONG TIME to render for medium+ maps.
if err != nil { // Do so on a background thread.
d.Flash(err.Error()) go func() {
return filename, err := giant_screenshot.SaveGiantScreenshot(u.Scene.Level)
} if err != nil {
d.FlashError("Error: %s", err.Error())
return
}
d.Flash("Saved screenshot to: %s", filename) d.FlashError("Giant screenshot saved as: %s", filename)
}()
}) })
levelMenu.AddItem("Open screenshot folder", func() { levelMenu.AddItem("Open screenshot folder", func() {
native.OpenLocalURL(userdir.ScreenshotDirectory) native.OpenLocalURL(userdir.ScreenshotDirectory)
@ -326,7 +330,7 @@ func (s *EditorScene) MenuSave(as bool) func() {
// drawingType = "level" // drawingType = "level"
saveFunc = func(filename string) { saveFunc = func(filename string) {
if err := s.SaveLevel(filename); err != nil { if err := s.SaveLevel(filename); err != nil {
s.d.Flash("Error: %s", err) s.d.FlashError("Error: %s", err)
} else { } else {
s.d.Flash("Saved level: %s", filename) s.d.Flash("Saved level: %s", filename)
} }
@ -335,13 +339,13 @@ func (s *EditorScene) MenuSave(as bool) func() {
// drawingType = "doodad" // drawingType = "doodad"
saveFunc = func(filename string) { saveFunc = func(filename string) {
if err := s.SaveDoodad(filename); err != nil { if err := s.SaveDoodad(filename); err != nil {
s.d.Flash("Error: %s", err) s.d.FlashError("Error: %s", err)
} else { } else {
s.d.Flash("Saved doodad: %s", filename) s.d.Flash("Saved doodad: %s", filename)
} }
} }
default: default:
s.d.Flash("Error: Scene.DrawingType is not a valid type") s.d.FlashError("Error: Scene.DrawingType is not a valid type")
} }
// "Save As"? // "Save As"?

View File

@ -182,7 +182,7 @@ func (u *EditorUI) SetupPopups(d *Doodle) {
u.licenseWindow.Show() u.licenseWindow.Show()
u.Supervisor.FocusWindow(u.licenseWindow) u.Supervisor.FocusWindow(u.licenseWindow)
} }
d.Flash("Level Publishing is only available in the full version of the game.") d.FlashError("Level Publishing is only available in the full version of the game.")
// modal.Alert( // modal.Alert(
// "This feature is only available in the full version of the game.", // "This feature is only available in the full version of the game.",
// ).WithTitle("Please register") // ).WithTitle("Please register")
@ -193,7 +193,7 @@ func (u *EditorUI) SetupPopups(d *Doodle) {
cwd, _ := os.Getwd() cwd, _ := os.Getwd()
d.Prompt(fmt.Sprintf("File name (relative to %s)> ", cwd), func(answer string) { d.Prompt(fmt.Sprintf("File name (relative to %s)> ", cwd), func(answer string) {
if answer == "" { if answer == "" {
d.Flash("A file name is required to publish this level.") d.FlashError("A file name is required to publish this level.")
return return
} }
@ -364,7 +364,7 @@ func (u *EditorUI) SetupPopups(d *Doodle) {
}, },
OnChangeLayer: func(index int) { OnChangeLayer: func(index int) {
if index < 0 || index >= len(scene.Doodad.Layers) { if index < 0 || index >= len(scene.Doodad.Layers) {
d.Flash("OnChangeLayer: layer %d out of range", index) d.FlashError("OnChangeLayer: layer %d out of range", index)
return return
} }

View File

@ -1,6 +1,7 @@
package giant_screenshot package giant_screenshot
import ( import (
"errors"
"image" "image"
"image/draw" "image/draw"
"image/png" "image/png"
@ -11,6 +12,7 @@ import (
"git.kirsle.net/apps/doodle/pkg/doodads" "git.kirsle.net/apps/doodle/pkg/doodads"
"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/shmem"
"git.kirsle.net/apps/doodle/pkg/userdir" "git.kirsle.net/apps/doodle/pkg/userdir"
"git.kirsle.net/apps/doodle/pkg/wallpaper" "git.kirsle.net/apps/doodle/pkg/wallpaper"
"git.kirsle.net/go/render" "git.kirsle.net/go/render"
@ -20,8 +22,25 @@ import (
Giant Screenshot functionality for the Level Editor. Giant Screenshot functionality for the Level Editor.
*/ */
var locked bool
// GiantScreenshot returns a rendered RGBA image of the entire level. // GiantScreenshot returns a rendered RGBA image of the entire level.
func GiantScreenshot(lvl *level.Level) image.Image { //
// Only one thread should be doing this at a time. A sync.Mutex will cause
// an error to return if another goroutine is already in the process of
// generating a screenshot, and you'll have to wait and try later.
func GiantScreenshot(lvl *level.Level) (image.Image, error) {
// Lock this to one user at a time.
if locked {
return nil, errors.New("a giant screenshot is still being processed; try later...")
}
locked = true
defer func() {
locked = false
}()
shmem.Flash("Saving a giant screenshot (this takes a moment)...")
// How big will our image be? // How big will our image be?
var ( var (
size = lvl.Chunker.WorldSizePositive() size = lvl.Chunker.WorldSizePositive()
@ -110,7 +129,7 @@ func GiantScreenshot(lvl *level.Level) image.Image {
} }
return img return img, nil
} }
// SaveGiantScreenshot will take a screenshot and write it to a file on disk, // SaveGiantScreenshot will take a screenshot and write it to a file on disk,
@ -118,7 +137,10 @@ func GiantScreenshot(lvl *level.Level) image.Image {
func SaveGiantScreenshot(level *level.Level) (string, error) { func SaveGiantScreenshot(level *level.Level) (string, error) {
var filename = time.Now().Format("2006-01-02_15-04-05.png") var filename = time.Now().Format("2006-01-02_15-04-05.png")
img := GiantScreenshot(level) img, err := GiantScreenshot(level)
if err != nil {
return "", err
}
fh, err := os.Create(filepath.Join(userdir.ScreenshotDirectory, filename)) fh, err := os.Create(filepath.Join(userdir.ScreenshotDirectory, filename))
if err != nil { if err != nil {

View File

@ -117,7 +117,7 @@ func (s *MenuScene) Setup(d *Doodle) error {
return err return err
} }
default: default:
d.Flash("No Valid StartupMenu Given to MenuScene") d.FlashError("No Valid StartupMenu Given to MenuScene")
} }
// Whatever window we got, give it window manager controls under Supervisor. // Whatever window we got, give it window manager controls under Supervisor.

View File

@ -65,7 +65,7 @@ func (s *PlayScene) computeInventory() {
// Cache miss. Load the doodad here. // Cache miss. Load the doodad here.
doodad, err := doodads.LoadFile(filename) doodad, err := doodads.LoadFile(filename)
if err != nil { if err != nil {
s.d.Flash("Inventory item '%s' error: %s", filename, err) s.d.FlashError("Inventory item '%s' error: %s", filename, err)
continue continue
} }

View File

@ -49,12 +49,13 @@ type PlayScene struct {
debWorldIndex *string debWorldIndex *string
// Player character // Player character
Player *uix.Actor Player *uix.Actor
playerPhysics *physics.Mover playerPhysics *physics.Mover
lastCheckpoint render.Point lastCheckpoint render.Point
antigravity bool // Cheat: disable player gravity playerLastDirection float64 // player's heading last tick
noclip bool // Cheat: disable player clipping antigravity bool // Cheat: disable player gravity
playerJumpCounter int // limit jump length noclip bool // Cheat: disable player clipping
playerJumpCounter int // limit jump length
// Inventory HUD. Impl. in play_inventory.go // Inventory HUD. Impl. in play_inventory.go
invenFrame *ui.Frame invenFrame *ui.Frame
@ -200,7 +201,7 @@ func (s *PlayScene) setupAsync(d *Doodle) error {
if s.CanEdit { if s.CanEdit {
d.Flash("Entered Play Mode. Press 'E' to edit this map.") d.Flash("Entered Play Mode. Press 'E' to edit this map.")
} else { } else {
d.Flash("%s", s.Level.Title) d.FlashError("%s", s.Level.Title)
} }
// Pre-cache all bitmap images from the level chunks. // Pre-cache all bitmap images from the level chunks.
@ -272,9 +273,9 @@ func (s *PlayScene) setupPlayer() {
// Surface warnings around the spawn flag. // Surface warnings around the spawn flag.
if flagCount == 0 { if flagCount == 0 {
s.d.Flash("Warning: this level contained no Start Flag.") s.d.FlashError("Warning: this level contained no Start Flag.")
} else if flagCount > 1 { } else if flagCount > 1 {
s.d.Flash("Warning: this level contains multiple Start Flags. Player spawn point is ambiguous.") s.d.FlashError("Warning: this level contains multiple Start Flags. Player spawn point is ambiguous.")
} }
s.Player = uix.NewActor("PLAYER", &level.Actor{}, player) s.Player = uix.NewActor("PLAYER", &level.Actor{}, player)
@ -340,7 +341,7 @@ func (s *PlayScene) BeatLevel() {
// FailLevel handles a level failure triggered by a doodad. // FailLevel handles a level failure triggered by a doodad.
func (s *PlayScene) FailLevel(message string) { func (s *PlayScene) FailLevel(message string) {
s.d.Flash(message) s.d.FlashError(message)
s.ShowEndLevelModal( s.ShowEndLevelModal(
false, false,
"You've died!", "You've died!",
@ -521,17 +522,29 @@ func (s *PlayScene) movePlayer(ev *event.State) {
} }
// Up button to signal they want to jump. // Up button to signal they want to jump.
if keybind.Up(ev) && (s.Player.Grounded() || s.playerJumpCounter >= 0) { if keybind.Up(ev) {
jumping = true
if s.Player.Grounded() { if s.Player.Grounded() {
// Allow them to sustain the jump this many ticks. velocity.Y = balance.PlayerJumpVelocity
s.playerJumpCounter = 32
} }
} else if velocity.Y < 0 {
velocity.Y = 0
} }
// if keybind.Up(ev) && (s.Player.Grounded() || s.playerJumpCounter >= 0) {
// jumping = true
// if s.Player.Grounded() {
// // Allow them to sustain the jump this many ticks.
// s.playerJumpCounter = 32
// }
// }
// Moving left or right? Interpolate their velocity by acceleration. // Moving left or right? Interpolate their velocity by acceleration.
if direction != 0 { if direction != 0 {
if s.playerLastDirection != direction {
log.Error("Changed directions!")
velocity.X = 0
}
// TODO: fast turn-around if they change directions so they don't // TODO: fast turn-around if they change directions so they don't
// slip and slide while their velocity updates. // slip and slide while their velocity updates.
velocity.X = physics.Lerp( velocity.X = physics.Lerp(
@ -560,6 +573,8 @@ func (s *PlayScene) movePlayer(ev *event.State) {
} }
} }
s.playerLastDirection = direction
// Move the player unless frozen. // Move the player unless frozen.
// TODO: if Y=0 then gravity fails, but not doing this allows the // TODO: if Y=0 then gravity fails, but not doing this allows the
// player to jump while frozen. Not a HUGE deal right now as only Warp Doors // player to jump while frozen. Not a HUGE deal right now as only Warp Doors

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/log" "git.kirsle.net/apps/doodle/pkg/log"
"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/shmem" "git.kirsle.net/apps/doodle/pkg/shmem"
"git.kirsle.net/go/render" "git.kirsle.net/go/render"
"git.kirsle.net/go/render/event" "git.kirsle.net/go/render/event"
@ -22,6 +23,12 @@ func (d *Doodle) Flash(template string, v ...interface{}) {
d.shell.Write(fmt.Sprintf(template, v...)) d.shell.Write(fmt.Sprintf(template, v...))
} }
// FlashError flashes an error-colored message to the user.
func (d *Doodle) FlashError(template string, v ...interface{}) {
log.Error(template, v...)
d.shell.WriteColorful(fmt.Sprintf(template, v...), balance.FlashErrorColor)
}
// Prompt the user for a question in the dev console. // Prompt the user for a question in the dev console.
func (d *Doodle) Prompt(question string, callback func(string)) { func (d *Doodle) Prompt(question string, callback func(string)) {
d.shell.Prompt = question d.shell.Prompt = question
@ -59,6 +66,7 @@ type Shell struct {
type Flash struct { type Flash struct {
Text string Text string
Expires uint64 // tick that it expires Expires uint64 // tick that it expires
Color render.Color
} }
// NewShell initializes the shell helper (the "Shellper"). // NewShell initializes the shell helper (the "Shellper").
@ -80,6 +88,7 @@ func NewShell(d *Doodle) Shell {
"Execute": s.Execute, "Execute": s.Execute,
"RGBA": render.RGBA, "RGBA": render.RGBA,
"Point": render.NewPoint, "Point": render.NewPoint,
"Vector": physics.NewVector,
"Rect": render.NewRect, "Rect": render.NewRect,
"Tree": func(w ui.Widget) string { "Tree": func(w ui.Widget) string {
for _, row := range ui.WidgetTree(w) { for _, row := range ui.WidgetTree(w) {
@ -160,6 +169,16 @@ func (s *Shell) Write(line string) {
}) })
} }
// WriteError writes a line of error (red) text to the console.
func (s *Shell) WriteColorful(line string, color render.Color) {
s.Output = append(s.Output, line)
s.Flashes = append(s.Flashes, Flash{
Text: line,
Color: color,
Expires: shmem.Tick + balance.FlashTTL,
})
}
// Parse the command line. // Parse the command line.
func (s *Shell) Parse(input string) Command { func (s *Shell) Parse(input string) Command {
input = strings.TrimSpace(input) input = strings.TrimSpace(input)
@ -352,14 +371,15 @@ func (s *Shell) Draw(d *Doodle, ev *event.State) error {
continue continue
} }
var text = balance.FlashFont(flash.Text)
if !flash.Color.IsZero() {
text.Color = flash.Color
text.Stroke = text.Color.Darken(balance.FlashStrokeDarken)
text.Shadow = text.Color.Darken(balance.FlashShadowDarken)
}
d.Engine.DrawText( d.Engine.DrawText(
render.Text{ text,
Text: flash.Text,
Size: balance.ShellFontSize,
Color: render.SkyBlue,
Stroke: render.Grey,
Shadow: render.Black,
},
render.Point{ render.Point{
X: balance.ShellPadding + toolbarWidth, X: balance.ShellPadding + toolbarWidth,
Y: outputY, Y: outputY,