diff --git a/Changes.md b/Changes.md index f487a43..479195b 100644 --- a/Changes.md +++ b/Changes.md @@ -32,8 +32,15 @@ Some minor changes: the ground in order to overcome the constant (fast) gravity. * Coyote time where the player can still jump a few frames late when they walk off a cliff and jump late. + * Improved character speed when walking up slopes, they will now travel + horizontally at their regular speed instead of being slowed down by level + collision every step of the way. * Sound effects are now preferred to be in OGG format over MP3 as it is more reliable to compile the game cross-platform without the dependency on mpg123. +* Fix a bug where level chunks on the far right and bottom edge of the screen + would flicker out of existence while the level scrolls. +* When JavaScript exceptions are caught in doodad scripts, the error message + will now include the Go and JavaScript stack traces to help with debugging. * The game window maximizes on startup to fill the screen. ## v0.13.2 (Dec 2 2023) diff --git a/pkg/collision/collide_level.go b/pkg/collision/collide_level.go index a7146c8..ed04f74 100644 --- a/pkg/collision/collide_level.go +++ b/pkg/collision/collide_level.go @@ -78,6 +78,7 @@ func CollidesWithGrid(d Actor, grid *level.Chunker, target render.Point) (*Colli // e.g.: Boy's Canvas size is 56x56 but he is a narrower character with a // hitbox width smaller than its Canvas size. S = SizePlusHitbox(GetBoundingRect(d), hitbox) + actorHeight := P.Y + S.H // Test if we are ALREADY colliding with level geometry and try and wiggle // free. ScanBoundingBox scans level pixels along the four edges of the @@ -120,20 +121,16 @@ func CollidesWithGrid(d Actor, grid *level.Chunker, target render.Point) (*Colli // Cap our horizontal movement if we're touching walls. if (result.Left && target.X < P.X) || (result.Right && target.X > P.X) { - // If the step is short enough, try and jump up. - height := P.Y + S.H - if result.Left { // && target.X < P.X { - height -= result.LeftPoint.Y - } else { - height -= result.RightPoint.Y + // Handle walking up slopes, if the step is short enough. + var slopeHeight int + if result.Left { + slopeHeight = result.LeftPoint.Y + } else if result.Right { + slopeHeight = result.RightPoint.Y } - if height <= balance.SlopeMaxHeight { - target.Y -= height - if target.X < P.X { - target.X-- // push along to the left - } else if target.X > P.X { - target.X++ // push along to the right - } + + if offset, ok := CanStepUp(actorHeight, slopeHeight, target.X > P.X); ok { + target.Add(offset) } else { // Not a slope.. may be a solid wall. If the wall is a SemiSolid though, // do not cap our direction just yet. @@ -198,13 +195,20 @@ func CollidesWithGrid(d Actor, grid *level.Chunker, target render.Point) (*Colli // for regular solid slopes too. But if this block of code is dummied out for // solid walls, the player is able to clip thru thin walls (couple px thick); the // capLeft/capRight behavior is good at stopping the player here. + + // See if they have hit a solid wall on their left or right edge. If the wall + // is short enough to step up, allow them to pass through. if result.Left && !hitLeft && !result.LeftPixel.SemiSolid { - hitLeft = true - capLeft = result.LeftPoint.X + if _, ok := CanStepUp(actorHeight, result.LeftPoint.Y, false); !ok { + hitLeft = true + capLeft = result.LeftPoint.X + } } if result.Right && !hitRight && !result.RightPixel.SemiSolid { - hitRight = true - capRight = result.RightPoint.X - S.W + if _, ok := CanStepUp(actorHeight, result.RightPoint.Y, false); !ok { + hitRight = true + capRight = result.RightPoint.X - S.W + } } } @@ -234,6 +238,37 @@ func CollidesWithGrid(d Actor, grid *level.Chunker, target render.Point) (*Colli return result, result.IsColliding() } +/* +CanStepUp checks whether the actor is moving left or right onto a gentle slope which +they can step on top of instead of being blocked by the solid wall. + +* actorHeight is the actor's Y position + their hitbox height. +* slopeHeight is the Y position of the left or right edge of the level they collide with. +* moveRight is true if moving right, false if moving left. + +If the actor can step up the slope, the return value is the Point of how to offset their +X,Y position to move up the slope and the boolean is whether they can step up. +*/ +func CanStepUp(actorHeight, slopeHeight int, moveRight bool) (render.Point, bool) { + var ( + height = actorHeight - slopeHeight + target render.Point + ) + + if height <= balance.SlopeMaxHeight { + target.Y -= height + if moveRight { + target.X++ + } else { + target.X-- + } + + return target, true + } + + return target, false +} + // IsColliding returns whether any sort of collision has occurred. func (c *Collide) IsColliding() bool { return c.Top || c.Bottom || (c.Left && !c.LeftPixel.SemiSolid) || (c.Right && !c.RightPixel.SemiSolid) || diff --git a/pkg/commands.go b/pkg/commands.go index 44356fd..39a7deb 100644 --- a/pkg/commands.go +++ b/pkg/commands.go @@ -14,6 +14,7 @@ import ( "git.kirsle.net/SketchyMaze/doodle/pkg/enum" "git.kirsle.net/SketchyMaze/doodle/pkg/log" "git.kirsle.net/SketchyMaze/doodle/pkg/modal" + "git.kirsle.net/SketchyMaze/doodle/pkg/native" "git.kirsle.net/SketchyMaze/doodle/pkg/scripting/exceptions" "github.com/dop251/goja" ) @@ -112,6 +113,10 @@ func (c Command) Run(d *Doodle) error { "Filename: trapdoor-down.doodad\n" + "Position: 643,266", ) + case "flush-textures": + // Flush all textures. + native.FreeTextures(d.Engine) + d.Flash("All textures freed.") default: return c.Default(d) } diff --git a/pkg/level/chunker.go b/pkg/level/chunker.go index 6a305d4..e1c00c8 100644 --- a/pkg/level/chunker.go +++ b/pkg/level/chunker.go @@ -191,8 +191,8 @@ func (c *Chunker) IterViewportChunks(viewport render.Rect) <-chan render.Point { size = int(c.Size) ) - for x := viewport.X; x < viewport.W; x += (size / 4) { - for y := viewport.Y; y < viewport.H; y += (size / 4) { + for x := viewport.X; x < viewport.W+size; x += (size / 4) { + for y := viewport.Y; y < viewport.H+size; y += (size / 4) { // Constrain this chunksize step to a point within the bounds // of the viewport. This can yield partial chunks on the edges diff --git a/pkg/native/engine_sdl.go b/pkg/native/engine_sdl.go index ee810ab..c7998d8 100644 --- a/pkg/native/engine_sdl.go +++ b/pkg/native/engine_sdl.go @@ -8,6 +8,7 @@ import ( "fmt" "image" + "git.kirsle.net/SketchyMaze/doodle/pkg/log" "git.kirsle.net/SketchyMaze/doodle/pkg/shmem" "git.kirsle.net/go/render" "git.kirsle.net/go/render/sdl" @@ -43,6 +44,16 @@ func CountTextures(e render.Engine) string { return texCount } +// FreeTextures will free all SDL2 textures currently in memory. +func FreeTextures(e render.Engine) { + if sdl, ok := e.(*sdl.Renderer); ok { + texCount := sdl.FreeTextures() + if texCount > 0 { + log.Info("FreeTextures: %d SDL2 textures freed", texCount) + } + } +} + /* TextToImage takes an SDL2_TTF texture and makes it into a Go image. diff --git a/pkg/native/engine_wasm.go b/pkg/native/engine_wasm.go index f93c8f9..2a5099e 100644 --- a/pkg/native/engine_wasm.go +++ b/pkg/native/engine_wasm.go @@ -26,4 +26,6 @@ func CountTextures(e render.Engine) string { return "n/a" } +func FreeTextures() {} + func MaximizeWindow(e render.Engine) {} diff --git a/pkg/scene.go b/pkg/scene.go index e6dc150..b088d1b 100644 --- a/pkg/scene.go +++ b/pkg/scene.go @@ -3,6 +3,7 @@ package doodle import ( "git.kirsle.net/SketchyMaze/doodle/pkg/gamepad" "git.kirsle.net/SketchyMaze/doodle/pkg/log" + "git.kirsle.net/SketchyMaze/doodle/pkg/native" "git.kirsle.net/SketchyMaze/doodle/pkg/scripting/exceptions" "git.kirsle.net/go/render/event" ) @@ -39,6 +40,9 @@ func (d *Doodle) Goto(scene Scene) error { // Teardown exceptions modal (singleton windows so it can clean up). exceptions.Teardown() + // Flush all SDL2 textures between scenes. + native.FreeTextures(d.Engine) + log.Info("Goto Scene: %s", scene.Name()) d.Scene = scene return d.Scene.Setup(d) diff --git a/pkg/scripting/events.go b/pkg/scripting/events.go index b7a1361..1ba434e 100644 --- a/pkg/scripting/events.go +++ b/pkg/scripting/events.go @@ -5,7 +5,6 @@ import ( "sync" "git.kirsle.net/SketchyMaze/doodle/pkg/keybind" - "git.kirsle.net/SketchyMaze/doodle/pkg/log" "git.kirsle.net/SketchyMaze/doodle/pkg/scripting/exceptions" "github.com/dop251/goja" ) @@ -107,7 +106,7 @@ func (e *Events) run(name string, args ...interface{}) error { // TODO EXCEPTIONS: I once saw a "runtime error: index out of range [-1]" // from an OnCollide handler between azu-white and thief that was crashing // the app, report this upstream nicely to the user. - exceptions.Catch("PANIC: JS %s handler: %s", name, err) + exceptions.FormatAndCatch(e.vm.vm, "PANIC: JS %s handler (%s): %s", name, e.vm.Name, err) } }() @@ -126,13 +125,13 @@ func (e *Events) run(name string, args ...interface{}) error { // TODO EXCEPTIONS: this err is useful like // `ReferenceError: playerSpeed is not defined at :173:9(93)` // but wherever we're returning the err to isn't handling it! - exceptions.Catch( + exceptions.FormatAndCatch( + e.vm.vm, "Scripting error in %s for %s:\n\n%s", name, e.vm.Name, err, ) - log.Error("Scripting error on %s: %s", name, err) return err } diff --git a/pkg/scripting/exceptions/exceptions.go b/pkg/scripting/exceptions/exceptions.go index b90e83d..e94eb29 100644 --- a/pkg/scripting/exceptions/exceptions.go +++ b/pkg/scripting/exceptions/exceptions.go @@ -6,6 +6,7 @@ import ( "strings" "sync" + "git.kirsle.net/SketchyMaze/doodle/lib/debugging" "git.kirsle.net/SketchyMaze/doodle/pkg/balance" "git.kirsle.net/SketchyMaze/doodle/pkg/log" "git.kirsle.net/SketchyMaze/doodle/pkg/native" @@ -13,6 +14,7 @@ import ( "git.kirsle.net/go/render" "git.kirsle.net/go/render/event" "git.kirsle.net/go/ui" + "github.com/dop251/goja" ) // The exception catching window is a singleton and appears on top with @@ -61,6 +63,38 @@ func Catch(exc string, args ...interface{}) { *excLabel = trim(exc) } +// FormatAndCatch an exception from a JavaScript VM. This is the common function called +// immediately when a scripting-related panic is recovered, and appends stack trace frames +// and formats the message for final Catch display. +func FormatAndCatch(vm *goja.Runtime, exc string, args ...interface{}) { + // Collect the JavaScript stack frame for debugging. + var ( + buf [1000]goja.StackFrame + jsCallers = []string{} + sections = []string{ + fmt.Sprintf(exc, args...), + } + ) + + if vm != nil { + frames := vm.CaptureCallStack(1000, buf[:0]) + for i, frame := range frames { + var position = frame.Position() + jsCallers = append(jsCallers, fmt.Sprintf("%d. %s at %s line %d column %d", i+1, frame.FuncName(), position.Filename, position.Line, position.Column)) + } + + if len(jsCallers) > 0 { + sections = append(sections, fmt.Sprintf("JS stack:\n%s", strings.Join(jsCallers, "\n"))) + } + } + + sections = append(sections, fmt.Sprintf("Go stack:\n%s", debugging.StringifyCallers())) + + Catch( + strings.Join(sections, "\n\n"), + ) +} + // Setup the global supervisor and window the first time - after the render engine has initialized, // e.g., when you want the window to show up the first time. func Setup() { diff --git a/pkg/scripting/pubsub.go b/pkg/scripting/pubsub.go index 05526be..0bc96fe 100644 --- a/pkg/scripting/pubsub.go +++ b/pkg/scripting/pubsub.go @@ -1,7 +1,6 @@ package scripting import ( - "git.kirsle.net/SketchyMaze/doodle/lib/debugging" "git.kirsle.net/SketchyMaze/doodle/pkg/log" "git.kirsle.net/SketchyMaze/doodle/pkg/scripting/exceptions" "github.com/dop251/goja" @@ -28,9 +27,7 @@ func RegisterPublishHooks(s *Supervisor, vm *VM) { // Catch any exceptions raised by the JavaScript VM. defer func() { if err := recover(); err != nil { - // TODO EXCEPTIONS - exceptions.Catch("RegisterPublishHooks(%s): %s", vm.Name, err) - debugging.PrintCallers() + exceptions.FormatAndCatch(vm.vm, "RegisterPublishHooks(%s): %s: %s", vm.Name, err) } }() diff --git a/pkg/uix/canvas_actors.go b/pkg/uix/canvas_actors.go index c6a6fa8..949ceab 100644 --- a/pkg/uix/canvas_actors.go +++ b/pkg/uix/canvas_actors.go @@ -127,7 +127,8 @@ func (w *Canvas) InstallScripts() error { // Call the main() function. if err := vm.Main(); err != nil { - exceptions.Catch( + exceptions.FormatAndCatch( + nil, "Error in main() for actor %s:\n\n%s\n\nActor ID: %s\nFilename: %s\nPosition: %s", actor.Actor.Filename, err,