diff --git a/pkg/balance/numbers.go b/pkg/balance/numbers.go index ad03a07..b63abbe 100644 --- a/pkg/balance/numbers.go +++ b/pkg/balance/numbers.go @@ -23,10 +23,11 @@ var ( } // Player speeds - PlayerMaxVelocity float64 = 6 - PlayerAcceleration float64 = 0.9 + PlayerMaxVelocity float64 = 7 + PlayerJumpVelocity float64 = -20 + PlayerAcceleration float64 = 0.12 Gravity float64 = 6 - GravityAcceleration float64 = 0.2 + GravityAcceleration float64 = 0.1 SlopeMaxHeight = 8 // max pixel height for player to walk up a slope // Default chunk size for canvases. diff --git a/pkg/balance/theme.go b/pkg/balance/theme.go index 3e3cb85..e741005 100644 --- a/pkg/balance/theme.go +++ b/pkg/balance/theme.go @@ -71,6 +71,20 @@ var ( WindowBackground = render.MustHexColor("#cdb689") 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. MenuBackground = render.Black MenuFont = render.Text{ diff --git a/pkg/cheats.go b/pkg/cheats.go index 5b3cf44..1d58d8c 100644 --- a/pkg/cheats.go +++ b/pkg/cheats.go @@ -28,7 +28,7 @@ func (c Command) cheatCommand(d *Doodle) bool { playScene.drawing.Editable = true d.Flash("Level canvas is now editable. Don't edit and drive!") } 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": @@ -36,7 +36,7 @@ func (c Command) cheatCommand(d *Doodle) bool { playScene.drawing.Scrollable = true d.Flash("Level canvas is now scrollable with the arrow keys.") } 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": @@ -50,7 +50,7 @@ func (c Command) cheatCommand(d *Doodle) bool { d.Flash("Gravity restored for player character.") } } 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": @@ -67,7 +67,7 @@ func (c Command) cheatCommand(d *Doodle) bool { d.Flash("Clipping and gravity restored for player character.") } } 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": @@ -77,7 +77,7 @@ func (c Command) cheatCommand(d *Doodle) bool { } d.Flash("All invisible actors made visible.") } 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": @@ -89,7 +89,7 @@ func (c Command) cheatCommand(d *Doodle) bool { playScene.Player.AddItem("small-key.doodad", 99) d.Flash("Given all keys to the player character.") } 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": @@ -97,7 +97,7 @@ func (c Command) cheatCommand(d *Doodle) bool { playScene.Player.ClearInventory() d.Flash("Cleared inventory of player character.") } 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": diff --git a/pkg/commands.go b/pkg/commands.go index 451d5d4..5b02de9 100644 --- a/pkg/commands.go +++ b/pkg/commands.go @@ -39,6 +39,9 @@ func (c Command) Run(d *Doodle) error { case "echo": d.Flash(c.ArgsLiteral) return nil + case "error": + d.FlashError(c.ArgsLiteral) + return nil case "alert": modal.Alert(c.ArgsLiteral) return nil @@ -104,13 +107,13 @@ func (c Command) Close(d *Doodle) error { // ExtractBindata dumps the app's embedded bindata to the filesystem. func (c Command) ExtractBindata(d *Doodle, path string) error { 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 } err := os.MkdirAll(path, 0755) if err != nil { - d.Flash("MkdirAll: %s", err) + d.FlashError("MkdirAll: %s", err) return err } @@ -120,7 +123,7 @@ func (c Command) ExtractBindata(d *Doodle, path string) error { data, err := assets.Asset(filename) if err != nil { - d.Flash("error on file %s: %s", filename, err) + d.FlashError("error on file %s: %s", filename, err) continue } @@ -131,7 +134,7 @@ func (c Command) ExtractBindata(d *Doodle, path string) error { fh, err := os.Create(outfile) if err != nil { - d.Flash("error writing file %s: %s", outfile, err) + d.FlashError("error writing file %s: %s", outfile, err) continue } fh.Write(data) @@ -145,7 +148,7 @@ func (c Command) ExtractBindata(d *Doodle, path string) error { // Help prints the help info. func (c Command) Help(d *Doodle) error { 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("Type `help` and then the command, like: `help edit`") return nil @@ -155,6 +158,9 @@ func (c Command) Help(d *Doodle) error { case "echo": d.Flash("Usage: echo ") d.Flash("Flash a message back to the console") + case "error": + d.Flash("Usage: error ") + d.Flash("Flash an error message back to the console") case "alert": d.Flash("Usage: alert ") d.Flash("Pop up an Alert box with a custom message") @@ -294,7 +300,7 @@ func (c Command) BoolProp(d *Doodle) error { } else { // Try the global boolProps in balance package. if err := balance.BoolProp(name, truthy); err != nil { - d.Flash("%s", err) + d.FlashError("%s", err) } else { 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) { defer func() { if err := recover(); err != nil { - d.Flash("Panic: %s", err) + d.FlashError("Command.RunScript: Panic: %s", err) } }() out, err := d.shell.js.Run(code) diff --git a/pkg/doodle.go b/pkg/doodle.go index 3219bd1..63bbb4a 100644 --- a/pkg/doodle.go +++ b/pkg/doodle.go @@ -269,7 +269,7 @@ func (d *Doodle) NewDoodad(size int) { if answer != "" { i, err := strconv.Atoi(answer) if err != nil { - d.Flash("Error: Doodad size must be a number.") + d.FlashError("Error: Doodad size must be a number.") return } size = i @@ -277,7 +277,7 @@ func (d *Doodle) NewDoodad(size int) { // Recurse with the proper answer. 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) }) diff --git a/pkg/editor_scene.go b/pkg/editor_scene.go index 88a7e50..b6b1235 100644 --- a/pkg/editor_scene.go +++ b/pkg/editor_scene.go @@ -127,7 +127,7 @@ func (s *EditorScene) setupAsync(d *Doodle) error { "Opening: " + s.filename, ) if err := s.LoadLevel(s.filename); err != nil { - d.Flash("LoadLevel error: %s", err) + d.FlashError("LoadLevel error: %s", err) } else { s.UI.Canvas.InstallActors(s.Level.Actors) } @@ -138,7 +138,7 @@ func (s *EditorScene) setupAsync(d *Doodle) error { if usercfg.Current.WriteLockOverride { d.Flash("Note: write lock has been overridden") } 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.UI.Canvas.ClearActors() s.filename = "" @@ -165,7 +165,7 @@ func (s *EditorScene) setupAsync(d *Doodle) error { if s.filename != "" && s.OpenFile { log.Debug("EditorScene.Setup: Loading doodad from filename at %s", s.filename) 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 { d.Flash("Note: write lock has been overridden") } 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.filename = "" } diff --git a/pkg/editor_ui_menubar.go b/pkg/editor_ui_menubar.go index 20a7cab..15d69ad 100644 --- a/pkg/editor_ui_menubar.go +++ b/pkg/editor_ui_menubar.go @@ -33,7 +33,7 @@ func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.MenuBar { drawingType = "level" saveFunc = func(filename string) { if err := u.Scene.SaveLevel(filename); err != nil { - d.Flash("Error: %s", err) + d.FlashError("Error: %s", err) } else { d.Flash("Saved level: %s", filename) } @@ -42,13 +42,13 @@ func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.MenuBar { drawingType = "doodad" saveFunc = func(filename string) { if err := u.Scene.SaveDoodad(filename); err != nil { - d.Flash("Error: %s", err) + d.FlashError("Error: %s", err) } else { d.Flash("Saved doodad: %s", filename) } } 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.AddItem("Giant Screenshot", func() { - filename, err := giant_screenshot.SaveGiantScreenshot(u.Scene.Level) - if err != nil { - d.Flash(err.Error()) - return - } + // It takes a LONG TIME to render for medium+ maps. + // Do so on a background thread. + go func() { + 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() { native.OpenLocalURL(userdir.ScreenshotDirectory) @@ -326,7 +330,7 @@ func (s *EditorScene) MenuSave(as bool) func() { // drawingType = "level" saveFunc = func(filename string) { if err := s.SaveLevel(filename); err != nil { - s.d.Flash("Error: %s", err) + s.d.FlashError("Error: %s", err) } else { s.d.Flash("Saved level: %s", filename) } @@ -335,13 +339,13 @@ func (s *EditorScene) MenuSave(as bool) func() { // drawingType = "doodad" saveFunc = func(filename string) { if err := s.SaveDoodad(filename); err != nil { - s.d.Flash("Error: %s", err) + s.d.FlashError("Error: %s", err) } else { s.d.Flash("Saved doodad: %s", filename) } } 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"? diff --git a/pkg/editor_ui_popups.go b/pkg/editor_ui_popups.go index 82cf78e..db8c4cf 100644 --- a/pkg/editor_ui_popups.go +++ b/pkg/editor_ui_popups.go @@ -182,7 +182,7 @@ func (u *EditorUI) SetupPopups(d *Doodle) { u.licenseWindow.Show() 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( // "This feature is only available in the full version of the game.", // ).WithTitle("Please register") @@ -193,7 +193,7 @@ func (u *EditorUI) SetupPopups(d *Doodle) { cwd, _ := os.Getwd() d.Prompt(fmt.Sprintf("File name (relative to %s)> ", cwd), func(answer string) { 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 } @@ -364,7 +364,7 @@ func (u *EditorUI) SetupPopups(d *Doodle) { }, OnChangeLayer: func(index int) { 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 } diff --git a/pkg/level/giant_screenshot/giant_screenshot.go b/pkg/level/giant_screenshot/giant_screenshot.go index f950573..51483d2 100644 --- a/pkg/level/giant_screenshot/giant_screenshot.go +++ b/pkg/level/giant_screenshot/giant_screenshot.go @@ -1,6 +1,7 @@ package giant_screenshot import ( + "errors" "image" "image/draw" "image/png" @@ -11,6 +12,7 @@ import ( "git.kirsle.net/apps/doodle/pkg/doodads" "git.kirsle.net/apps/doodle/pkg/level" "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/wallpaper" "git.kirsle.net/go/render" @@ -20,8 +22,25 @@ import ( Giant Screenshot functionality for the Level Editor. */ +var locked bool + // 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? var ( 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, @@ -118,7 +137,10 @@ func GiantScreenshot(lvl *level.Level) image.Image { func SaveGiantScreenshot(level *level.Level) (string, error) { 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)) if err != nil { diff --git a/pkg/menu_scene.go b/pkg/menu_scene.go index b2d8516..cfba6b2 100644 --- a/pkg/menu_scene.go +++ b/pkg/menu_scene.go @@ -117,7 +117,7 @@ func (s *MenuScene) Setup(d *Doodle) error { return err } 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. diff --git a/pkg/play_inventory.go b/pkg/play_inventory.go index 8d69245..77243ee 100644 --- a/pkg/play_inventory.go +++ b/pkg/play_inventory.go @@ -65,7 +65,7 @@ func (s *PlayScene) computeInventory() { // Cache miss. Load the doodad here. doodad, err := doodads.LoadFile(filename) if err != nil { - s.d.Flash("Inventory item '%s' error: %s", filename, err) + s.d.FlashError("Inventory item '%s' error: %s", filename, err) continue } diff --git a/pkg/play_scene.go b/pkg/play_scene.go index 3c14b24..6f469c2 100644 --- a/pkg/play_scene.go +++ b/pkg/play_scene.go @@ -49,12 +49,13 @@ type PlayScene struct { debWorldIndex *string // 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 + Player *uix.Actor + playerPhysics *physics.Mover + lastCheckpoint render.Point + playerLastDirection float64 // player's heading last tick + antigravity bool // Cheat: disable player gravity + noclip bool // Cheat: disable player clipping + playerJumpCounter int // limit jump length // Inventory HUD. Impl. in play_inventory.go invenFrame *ui.Frame @@ -200,7 +201,7 @@ func (s *PlayScene) setupAsync(d *Doodle) error { if s.CanEdit { d.Flash("Entered Play Mode. Press 'E' to edit this map.") } else { - d.Flash("%s", s.Level.Title) + d.FlashError("%s", s.Level.Title) } // Pre-cache all bitmap images from the level chunks. @@ -272,9 +273,9 @@ func (s *PlayScene) setupPlayer() { // Surface warnings around the spawn flag. 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 { - 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) @@ -340,7 +341,7 @@ func (s *PlayScene) BeatLevel() { // FailLevel handles a level failure triggered by a doodad. func (s *PlayScene) FailLevel(message string) { - s.d.Flash(message) + s.d.FlashError(message) s.ShowEndLevelModal( false, "You've died!", @@ -521,17 +522,29 @@ func (s *PlayScene) movePlayer(ev *event.State) { } // Up button to signal they want to jump. - if keybind.Up(ev) && (s.Player.Grounded() || s.playerJumpCounter >= 0) { - jumping = true - + if keybind.Up(ev) { if s.Player.Grounded() { - // Allow them to sustain the jump this many ticks. - s.playerJumpCounter = 32 + velocity.Y = balance.PlayerJumpVelocity } + } 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. 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 // slip and slide while their velocity updates. velocity.X = physics.Lerp( @@ -560,6 +573,8 @@ func (s *PlayScene) movePlayer(ev *event.State) { } } + s.playerLastDirection = direction + // Move the player unless frozen. // 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 diff --git a/pkg/shell.go b/pkg/shell.go index e17a2e5..6ea0dbc 100644 --- a/pkg/shell.go +++ b/pkg/shell.go @@ -9,6 +9,7 @@ import ( "git.kirsle.net/apps/doodle/pkg/keybind" "git.kirsle.net/apps/doodle/pkg/log" "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/go/render" "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...)) } +// 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. func (d *Doodle) Prompt(question string, callback func(string)) { d.shell.Prompt = question @@ -59,6 +66,7 @@ type Shell struct { type Flash struct { Text string Expires uint64 // tick that it expires + Color render.Color } // NewShell initializes the shell helper (the "Shellper"). @@ -80,6 +88,7 @@ func NewShell(d *Doodle) Shell { "Execute": s.Execute, "RGBA": render.RGBA, "Point": render.NewPoint, + "Vector": physics.NewVector, "Rect": render.NewRect, "Tree": func(w ui.Widget) string { 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. func (s *Shell) Parse(input string) Command { input = strings.TrimSpace(input) @@ -352,14 +371,15 @@ func (s *Shell) Draw(d *Doodle, ev *event.State) error { 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( - render.Text{ - Text: flash.Text, - Size: balance.ShellFontSize, - Color: render.SkyBlue, - Stroke: render.Grey, - Shadow: render.Black, - }, + text, render.Point{ X: balance.ShellPadding + toolbarWidth, Y: outputY,