Noah Petherbridge
6def8f7625
* Fix collision detection to allow actors to walk up slopes smoothly, without losing any horizontal velocity. * Fix scrolling a level canvas so that chunks near the right or bottom edge of the viewpoint were getting culled prematurely. * Centralize JavaScript exception catching logic to attach Go and JS stack traces where possible to be more useful for debugging. * Performance: flush all SDL2 textures from memory between scene transitions in the app. Also add a `flush-textures` dev console command to flush the textures at any time - they all should regenerate if still needed based on underlying go.Images which can be garbage collected.
271 lines
7.4 KiB
Go
271 lines
7.4 KiB
Go
package uix
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
|
|
"git.kirsle.net/SketchyMaze/doodle/pkg/doodads"
|
|
"git.kirsle.net/SketchyMaze/doodle/pkg/level"
|
|
"git.kirsle.net/SketchyMaze/doodle/pkg/license/levelsigning"
|
|
"git.kirsle.net/SketchyMaze/doodle/pkg/log"
|
|
"git.kirsle.net/SketchyMaze/doodle/pkg/scripting"
|
|
"git.kirsle.net/SketchyMaze/doodle/pkg/scripting/exceptions"
|
|
"git.kirsle.net/go/render"
|
|
)
|
|
|
|
// InstallActors adds external Actors to the canvas to be superimposed on top
|
|
// of the drawing.
|
|
func (w *Canvas) InstallActors(actors level.ActorMap) error {
|
|
var errs []string
|
|
|
|
// Order the actors deterministically, by their ID string. Actors get
|
|
// a time-ordered UUID ID by default so the most recently added actor
|
|
// should render on top of the others.
|
|
var actorIDs []string
|
|
for id := range actors {
|
|
actorIDs = append(actorIDs, id)
|
|
}
|
|
sort.Strings(actorIDs)
|
|
|
|
// In case we are replacing the actors, free up all their textures first!
|
|
for _, actor := range w.actors {
|
|
actor.Canvas.Destroy()
|
|
}
|
|
|
|
// Signed Levels: the free version normally won't load embedded assets from
|
|
// a level and the call to LoadFromEmbeddable below returns the error. If the
|
|
// level is signed it is allowed to use its embedded assets.
|
|
isSigned := w.IsSignedLevelPack != nil || levelsigning.IsLevelSigned(w.level)
|
|
|
|
w.actors = make([]*Actor, 0)
|
|
for _, id := range actorIDs {
|
|
var actor = actors[id]
|
|
|
|
// Try loading the doodad from the level's own attached files.
|
|
doodad, err := doodads.LoadFromEmbeddable(actor.Filename, w.level, isSigned)
|
|
if err != nil {
|
|
// If we have a signed levelpack, try loading from the levelpack.
|
|
if w.IsSignedLevelPack != nil {
|
|
if found, err := doodads.LoadFromEmbeddable(actor.Filename, w.IsSignedLevelPack, true); err == nil {
|
|
doodad = found
|
|
}
|
|
}
|
|
|
|
// If not found, append the error and continue.
|
|
if doodad == nil {
|
|
errs = append(errs, fmt.Sprintf("%s: %s", actor.Filename, err.Error()))
|
|
continue
|
|
}
|
|
}
|
|
|
|
// Create the "live" Actor to exist in the world, and set its world
|
|
// position to the Point defined in the level data.
|
|
liveActor := NewActor(id, actor, doodad)
|
|
liveActor.Canvas.parent = w
|
|
liveActor.LevelCanvas = w
|
|
liveActor.MoveTo(actor.Point)
|
|
|
|
w.actors = append(w.actors, liveActor)
|
|
}
|
|
|
|
if len(errs) > 0 {
|
|
return errors.New(strings.Join(errs, "\n"))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Actors returns the list of actors currently in the Canvas.
|
|
func (w *Canvas) Actors() []*Actor {
|
|
return w.actors
|
|
}
|
|
|
|
// ClearActors removes all the actors from the Canvas.
|
|
func (w *Canvas) ClearActors() {
|
|
w.actors = []*Actor{}
|
|
}
|
|
|
|
// SetScriptSupervisor assigns the Canvas scripting supervisor to enable
|
|
// interaction with actor scripts.
|
|
func (w *Canvas) SetScriptSupervisor(s *scripting.Supervisor) {
|
|
w.scripting = s
|
|
}
|
|
|
|
// InstallScripts loads all the current actors' scripts into the scripting
|
|
// engine supervisor.
|
|
func (w *Canvas) InstallScripts() error {
|
|
if w.scripting == nil {
|
|
return errors.New("no script supervisor is configured for this canvas")
|
|
}
|
|
|
|
if len(w.actors) == 0 {
|
|
return errors.New("no actors exist in this canvas to install scripts for")
|
|
}
|
|
|
|
for _, actor := range w.actors {
|
|
vm := w.scripting.To(actor.ID())
|
|
|
|
if vm.Self != nil {
|
|
// Already initialized!
|
|
continue
|
|
}
|
|
|
|
// Security: expose a selective API to the actor to the JS engine.
|
|
vm.Self = w.MakeSelfAPI(actor)
|
|
w.MakeScriptAPI(vm)
|
|
vm.Set("Self", vm.Self)
|
|
|
|
// If there is no script attached, do not try and load or call the main() function.
|
|
if actor.Doodad().Script == "" {
|
|
continue
|
|
}
|
|
|
|
if _, err := vm.Run(actor.Doodad().Script); err != nil {
|
|
log.Error("Run script for actor %s failed: %s", actor.ID(), err)
|
|
}
|
|
|
|
// Call the main() function.
|
|
if err := vm.Main(); err != nil {
|
|
exceptions.FormatAndCatch(
|
|
nil,
|
|
"Error in main() for actor %s:\n\n%s\n\nActor ID: %s\nFilename: %s\nPosition: %s",
|
|
actor.Actor.Filename,
|
|
err,
|
|
actor.ID(),
|
|
actor.Actor.Filename,
|
|
actor.Position(),
|
|
)
|
|
}
|
|
}
|
|
|
|
// Broadcast the "ready" signal to any actors that want to publish
|
|
// messages ASAP on level start.
|
|
for _, actor := range w.actors {
|
|
w.scripting.To(actor.ID()).Inbound <- scripting.Message{
|
|
Name: "broadcast:ready",
|
|
Args: nil,
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// AddActor injects additional actors into the canvas, such as a Player doodad.
|
|
func (w *Canvas) AddActor(actor *Actor) error {
|
|
actor.LevelCanvas = w
|
|
w.actors = append(w.actors, actor)
|
|
return nil
|
|
}
|
|
|
|
// RemoveActor removes the actor from the canvas.
|
|
func (w *Canvas) RemoveActor(actor *Actor) {
|
|
var actors = []*Actor{}
|
|
for _, exist := range w.actors {
|
|
if actor == exist {
|
|
w.scripting.RemoveVM(actor.ID())
|
|
continue
|
|
}
|
|
actors = append(actors, exist)
|
|
}
|
|
w.actors = actors
|
|
}
|
|
|
|
// drawActors is a subroutine of Present() that superimposes the actors on top
|
|
// of the level drawing.
|
|
func (w *Canvas) drawActors(e render.Engine, p render.Point) {
|
|
var (
|
|
Viewport = w.ViewportRelative()
|
|
S = w.Size()
|
|
)
|
|
|
|
// See if each Actor is in range of the Viewport.
|
|
for i, a := range w.actors {
|
|
if a == nil {
|
|
log.Error("Canvas.drawActors: null actor at index %d (of %d actors)", i, len(w.actors))
|
|
continue
|
|
}
|
|
|
|
// Skip hidden actors.
|
|
if a.hidden {
|
|
continue
|
|
}
|
|
|
|
var (
|
|
can = a.Canvas // Canvas widget that draws the actor
|
|
actorPoint = a.Position()
|
|
actorSize = a.Size()
|
|
resizeTo = actorSize
|
|
)
|
|
|
|
// Adjust actor position and size by the zoom level.
|
|
actorPoint.X = w.ZoomMultiply(actorPoint.X)
|
|
actorPoint.Y = w.ZoomMultiply(actorPoint.Y)
|
|
resizeTo.W = w.ZoomMultiply(resizeTo.W)
|
|
resizeTo.H = w.ZoomMultiply(resizeTo.H)
|
|
|
|
// Tell the actor's canvas to copy our zoom level so it resizes its image too.
|
|
can.Zoom = w.Zoom
|
|
|
|
// Create a box of World Coordinates that this actor occupies. The
|
|
// Actor X,Y from level data is already a World Coordinate;
|
|
// accomodate for the size of the Actor.
|
|
actorBox := render.Rect{
|
|
X: actorPoint.X,
|
|
Y: actorPoint.Y,
|
|
W: actorSize.W,
|
|
H: actorSize.H,
|
|
}
|
|
|
|
// Is any part of the actor visible?
|
|
if !Viewport.Intersects(actorBox) {
|
|
continue // not visible on screen
|
|
}
|
|
|
|
drawAt := render.Point{
|
|
X: p.X + w.Scroll.X + actorPoint.X + w.BoxThickness(1),
|
|
Y: p.Y + w.Scroll.Y + actorPoint.Y + w.BoxThickness(1),
|
|
}
|
|
|
|
// XXX TODO: when an Actor hits the left or top edge and shrinks,
|
|
// scrolling to offset that shrink is currently hard to solve.
|
|
scrollTo := render.Origin
|
|
|
|
// Handle cropping and scaling if this Actor's canvas can't be
|
|
// completely visible within the parent.
|
|
if drawAt.X+resizeTo.W > p.X+S.W {
|
|
// Hitting the right edge, shrunk the width now.
|
|
delta := (drawAt.X + resizeTo.W) - (p.X + S.W)
|
|
resizeTo.W -= delta
|
|
} else if drawAt.X < p.X {
|
|
// Hitting the left edge. Cap the X coord and shrink the width.
|
|
delta := p.X - drawAt.X // positive number
|
|
drawAt.X = p.X
|
|
scrollTo.X -= delta
|
|
resizeTo.W -= delta
|
|
}
|
|
|
|
if drawAt.Y+resizeTo.H > p.Y+S.H {
|
|
// Hitting the bottom edge, shrink the height.
|
|
delta := (drawAt.Y + resizeTo.H) - (p.Y + S.H)
|
|
resizeTo.H -= delta
|
|
} else if drawAt.Y < p.Y {
|
|
// Hitting the top edge. Cap the Y coord and shrink the height.
|
|
delta := p.Y - drawAt.Y
|
|
drawAt.Y = p.Y
|
|
scrollTo.Y -= delta
|
|
resizeTo.H -= delta
|
|
}
|
|
|
|
if resizeTo != actorSize {
|
|
can.Resize(resizeTo)
|
|
can.ScrollTo(scrollTo)
|
|
}
|
|
can.Present(e, drawAt)
|
|
|
|
// Clean up the canvas size and offset.
|
|
can.Resize(actorSize) // restore original size in case cropped
|
|
can.ScrollTo(render.Origin)
|
|
}
|
|
}
|