Noah Petherbridge
a2e1bd1ccb
* Events.OnCollide now receives a CollideEvent object, which makes available the .Actor who collided and the .Overlap rect which is zero-relative to the target actor. Doodad scripts can use the .Overlap to see WHERE in their own box the other actor has intruded. * Update the LockedDoor and ElectricDoor doodads to detect when the player has entered their inner rect (since their doors are narrower than their doodad size) * Update the Button doodads to only press in when the player actually touches them (because their sizes are shorter than their doodad height) * Update the Trapdoor to only trigger its animation when the board along its top has been touched, not when the empty space below was touched from the bottom. * Events.OnLeave now implemented and fires when an actor who was previously intersecting your doodad has left. * The engine detects when an event JS callback returns false. Eventually, the OnCollide can return false to signify the collision is not accepted and the actor should be bumped away as if they hit solid geometry.
362 lines
10 KiB
Go
362 lines
10 KiB
Go
package uix
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"git.kirsle.net/apps/doodle/lib/events"
|
|
"git.kirsle.net/apps/doodle/lib/render"
|
|
"git.kirsle.net/apps/doodle/lib/ui"
|
|
"git.kirsle.net/apps/doodle/pkg/balance"
|
|
"git.kirsle.net/apps/doodle/pkg/collision"
|
|
"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/scripting"
|
|
"git.kirsle.net/apps/doodle/pkg/wallpaper"
|
|
"github.com/robertkrimen/otto"
|
|
)
|
|
|
|
// Canvas is a custom ui.Widget that manages a single drawing.
|
|
type Canvas struct {
|
|
ui.Frame
|
|
Palette *level.Palette
|
|
|
|
// Editable and Scrollable go hand in hand and, if you initialize a
|
|
// NewCanvas() with editable=true, they are both enabled.
|
|
Editable bool // Clicking will edit pixels of this canvas.
|
|
Scrollable bool // Cursor keys will scroll the viewport of this canvas.
|
|
|
|
// Selected draw tool/mode, default Pencil, for editable canvases.
|
|
Tool Tool
|
|
|
|
// MaskColor will force every pixel to render as this color regardless of
|
|
// the palette index of that pixel. Otherwise pixels behave the same and
|
|
// the palette does work as normal. Set to render.Invisible (zero value)
|
|
// to remove the mask.
|
|
MaskColor render.Color
|
|
|
|
// Actor ID to follow the camera on automatically, i.e. the main player.
|
|
FollowActor string
|
|
|
|
// Debug tools
|
|
// NoLimitScroll suppresses the scroll limit for bounded levels.
|
|
NoLimitScroll bool
|
|
|
|
// Underlying chunk data for the drawing.
|
|
chunks *level.Chunker
|
|
|
|
// Actors to superimpose on top of the drawing.
|
|
actor *Actor // if this canvas IS an actor
|
|
actors []*Actor // if this canvas CONTAINS actors (i.e., is a level)
|
|
|
|
// Collision memory for the actors.
|
|
collidingActors map[string]string // mapping their IDs to each other
|
|
|
|
// Doodad scripting engine supervisor.
|
|
// NOTE: initialized and managed by the play_scene.
|
|
scripting *scripting.Supervisor
|
|
|
|
// Wallpaper settings.
|
|
wallpaper *Wallpaper
|
|
|
|
// When the Canvas wants to delete Actors, but ultimately it is upstream
|
|
// that controls the actors. Upstream should delete them and then reinstall
|
|
// the actor list from scratch.
|
|
OnDeleteActors func([]*level.Actor)
|
|
OnDragStart func(filename string)
|
|
|
|
// Tracking pixels while editing. TODO: get rid of pixelHistory?
|
|
pixelHistory []*level.Pixel
|
|
lastPixel *level.Pixel
|
|
|
|
// We inherit the ui.Widget which manages the width and height.
|
|
Scroll render.Point // Scroll offset for which parts of canvas are visible.
|
|
}
|
|
|
|
// NewCanvas initializes a Canvas widget.
|
|
//
|
|
// If editable is true, Scrollable is also set to true, which means the arrow
|
|
// keys will scroll the canvas viewport which is desirable in Edit Mode.
|
|
func NewCanvas(size int, editable bool) *Canvas {
|
|
w := &Canvas{
|
|
Editable: editable,
|
|
Scrollable: editable,
|
|
Palette: level.NewPalette(),
|
|
chunks: level.NewChunker(size),
|
|
actors: make([]*Actor, 0),
|
|
wallpaper: &Wallpaper{},
|
|
}
|
|
w.setup()
|
|
w.IDFunc(func() string {
|
|
var attrs []string
|
|
|
|
if w.Editable {
|
|
attrs = append(attrs, "editable")
|
|
} else {
|
|
attrs = append(attrs, "read-only")
|
|
}
|
|
|
|
if w.Scrollable {
|
|
attrs = append(attrs, "scrollable")
|
|
}
|
|
|
|
return fmt.Sprintf("Canvas<%d; %s>", size, strings.Join(attrs, "; "))
|
|
})
|
|
return w
|
|
}
|
|
|
|
// Load initializes the Canvas using an existing Palette and Grid.
|
|
func (w *Canvas) Load(p *level.Palette, g *level.Chunker) {
|
|
w.Palette = p
|
|
w.chunks = g
|
|
|
|
if len(w.Palette.Swatches) > 0 {
|
|
w.SetSwatch(w.Palette.Swatches[0])
|
|
}
|
|
}
|
|
|
|
// LoadLevel initializes a Canvas from a Level object.
|
|
func (w *Canvas) LoadLevel(e render.Engine, level *level.Level) {
|
|
w.Load(level.Palette, level.Chunker)
|
|
|
|
// TODO: wallpaper paths
|
|
filename := "assets/wallpapers/" + level.Wallpaper
|
|
if _, err := os.Stat(filename); os.IsNotExist(err) {
|
|
log.Error("LoadLevel: %s", err)
|
|
filename = "assets/wallpapers/notebook.png" // XXX TODO
|
|
}
|
|
|
|
wp, err := wallpaper.FromFile(e, filename)
|
|
if err != nil {
|
|
log.Error("wallpaper FromFile(%s): %s", filename, err)
|
|
}
|
|
|
|
w.wallpaper.maxWidth = level.MaxWidth
|
|
w.wallpaper.maxHeight = level.MaxHeight
|
|
err = w.wallpaper.Load(e, level.PageType, wp)
|
|
if err != nil {
|
|
log.Error("wallpaper Load: %s", err)
|
|
}
|
|
}
|
|
|
|
// LoadDoodad initializes a Canvas from a Doodad object.
|
|
func (w *Canvas) LoadDoodad(d *doodads.Doodad) {
|
|
// TODO more safe
|
|
w.Load(d.Palette, d.Layers[0].Chunker)
|
|
}
|
|
|
|
// SetSwatch changes the currently selected swatch for editing.
|
|
func (w *Canvas) SetSwatch(s *level.Swatch) {
|
|
w.Palette.ActiveSwatch = s
|
|
}
|
|
|
|
// setup common configs between both initializers of the canvas.
|
|
func (w *Canvas) setup() {
|
|
// XXX: Debug code.
|
|
if balance.DebugCanvasBorder != render.Invisible {
|
|
w.Configure(ui.Config{
|
|
BorderColor: balance.DebugCanvasBorder,
|
|
BorderSize: 2,
|
|
BorderStyle: ui.BorderSolid,
|
|
})
|
|
}
|
|
}
|
|
|
|
// Loop is called on the scene's event loop to handle mouse interaction with
|
|
// the canvas, i.e. to edit it.
|
|
func (w *Canvas) Loop(ev *events.State) error {
|
|
// Process the arrow keys scrolling the level in Edit Mode.
|
|
// canvas_scrolling.go
|
|
w.loopEditorScroll(ev)
|
|
if err := w.loopFollowActor(ev); err != nil {
|
|
log.Error("Follow actor: %s", err) // not fatal but nice to know
|
|
}
|
|
_ = w.loopConstrainScroll()
|
|
|
|
// Current time of this loop so we can advance animations.
|
|
now := time.Now()
|
|
|
|
// Remove any actors that were destroyed the previous tick.
|
|
var newActors []*Actor
|
|
for _, a := range w.actors {
|
|
if a.flagDestroy {
|
|
continue
|
|
}
|
|
newActors = append(newActors, a)
|
|
}
|
|
if len(newActors) < len(w.actors) {
|
|
w.actors = newActors
|
|
}
|
|
|
|
// Move any actors. As we iterate over all actors, track their bounding
|
|
// rectangles so we can later see if any pair of actors intersect each other.
|
|
boxes := make([]render.Rect, len(w.actors))
|
|
var wg sync.WaitGroup
|
|
for i, a := range w.actors {
|
|
wg.Add(1)
|
|
go func(i int, a *Actor) {
|
|
defer wg.Done()
|
|
|
|
// Advance any animations for this actor.
|
|
if a.activeAnimation != nil && a.activeAnimation.nextFrameAt.Before(now) {
|
|
if done := a.TickAnimation(a.activeAnimation); done {
|
|
// Animation has finished, run the callback script.
|
|
if a.animationCallback.IsFunction() {
|
|
a.animationCallback.Call(otto.NullValue())
|
|
}
|
|
|
|
// Clean up the animation state.
|
|
a.StopAnimation()
|
|
}
|
|
}
|
|
|
|
// Get the actor's velocity to see if it's moving this tick.
|
|
v := a.Velocity()
|
|
if a.hasGravity {
|
|
v.Y += int32(balance.Gravity)
|
|
}
|
|
|
|
// If not moving, grab the bounding box right now.
|
|
if v == render.Origin {
|
|
boxes[i] = doodads.GetBoundingRect(a)
|
|
return
|
|
}
|
|
|
|
// Create a delta point from their current location to where they
|
|
// want to move to this tick.
|
|
delta := a.Position()
|
|
delta.Add(v)
|
|
|
|
// Check collision with level geometry.
|
|
info, ok := collision.CollidesWithGrid(a, w.chunks, delta)
|
|
if ok {
|
|
// Collision happened with world.
|
|
}
|
|
delta = info.MoveTo // Move us back where the collision check put us
|
|
|
|
// Move the actor's World Position to the new location.
|
|
a.MoveTo(delta)
|
|
|
|
// Keep the actor from leaving the world borders of bounded maps.
|
|
w.loopContainActorsInsideLevel(a)
|
|
|
|
// Store this actor's bounding box after they've moved.
|
|
boxes[i] = doodads.GetBoundingRect(a)
|
|
}(i, a)
|
|
wg.Wait()
|
|
}
|
|
|
|
// Check collisions between actors.
|
|
var collidingActors = map[string]string{}
|
|
for tuple := range collision.BetweenBoxes(boxes) {
|
|
a, b := w.actors[tuple.A], w.actors[tuple.B]
|
|
|
|
collidingActors[a.ID()] = b.ID()
|
|
|
|
// Call the OnCollide handler.
|
|
if w.scripting != nil {
|
|
// Tell actor A about the collision with B.
|
|
if err := w.scripting.To(a.ID()).Events.RunCollide(&CollideEvent{
|
|
Actor: b,
|
|
Overlap: tuple.Overlap,
|
|
}); err != nil {
|
|
log.Error(err.Error())
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check for lacks of collisions since last frame.
|
|
for sourceID, targetID := range w.collidingActors {
|
|
if _, ok := collidingActors[sourceID]; !ok {
|
|
w.scripting.To(sourceID).Events.RunLeave(targetID)
|
|
}
|
|
}
|
|
|
|
// Store this frame's colliding actors for next frame.
|
|
w.collidingActors = collidingActors
|
|
|
|
// If the canvas is editable, only care if it's over our space.
|
|
if w.Editable {
|
|
cursor := render.NewPoint(ev.CursorX.Now, ev.CursorY.Now)
|
|
if cursor.Inside(ui.AbsoluteRect(w)) {
|
|
return w.loopEditable(ev)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Viewport returns a rect containing the viewable drawing coordinates in this
|
|
// canvas. The X,Y values are the scroll offset (top left) and the W,H values
|
|
// are the scroll offset plus the width/height of the Canvas widget.
|
|
//
|
|
// The Viewport rect are the Absolute World Coordinates of the drawing that are
|
|
// visible inside the Canvas. The X,Y is the top left World Coordinate and the
|
|
// W,H are the bottom right World Coordinate, making this rect an absolute
|
|
// slice of the world. For a normal rect with a relative width and height,
|
|
// use ViewportRelative().
|
|
//
|
|
// The rect X,Y are the negative Scroll Value.
|
|
// The rect W,H are the Canvas widget size minus the Scroll Value.
|
|
func (w *Canvas) Viewport() render.Rect {
|
|
var S = w.Size()
|
|
return render.Rect{
|
|
X: -w.Scroll.X,
|
|
Y: -w.Scroll.Y,
|
|
W: S.W - w.Scroll.X,
|
|
H: S.H - w.Scroll.Y,
|
|
}
|
|
}
|
|
|
|
// ViewportRelative returns a relative viewport where the Width and Height
|
|
// values are zero-relative: so you can use it with point.Inside(viewport)
|
|
// to see if a World Index point should be visible on screen.
|
|
//
|
|
// The rect X,Y are the negative Scroll Value
|
|
// The rect W,H are the Canvas widget size.
|
|
func (w *Canvas) ViewportRelative() render.Rect {
|
|
var S = w.Size()
|
|
return render.Rect{
|
|
X: -w.Scroll.X,
|
|
Y: -w.Scroll.Y,
|
|
W: S.W,
|
|
H: S.H,
|
|
}
|
|
}
|
|
|
|
// WorldIndexAt returns the World Index that corresponds to a Screen Pixel
|
|
// on the screen. If the screen pixel is the mouse coordinate (relative to
|
|
// the application window) this will return the World Index of the pixel below
|
|
// the mouse cursor.
|
|
func (w *Canvas) WorldIndexAt(screenPixel render.Point) render.Point {
|
|
var P = ui.AbsolutePosition(w)
|
|
return render.Point{
|
|
X: screenPixel.X - P.X - w.Scroll.X,
|
|
Y: screenPixel.Y - P.Y - w.Scroll.Y,
|
|
}
|
|
}
|
|
|
|
// Chunker returns the underlying Chunker object.
|
|
func (w *Canvas) Chunker() *level.Chunker {
|
|
return w.chunks
|
|
}
|
|
|
|
// ScrollTo sets the viewport scroll position.
|
|
func (w *Canvas) ScrollTo(to render.Point) {
|
|
w.Scroll.X = to.X
|
|
w.Scroll.Y = to.Y
|
|
}
|
|
|
|
// ScrollBy adjusts the viewport scroll position.
|
|
func (w *Canvas) ScrollBy(by render.Point) {
|
|
w.Scroll.Add(by)
|
|
}
|
|
|
|
// Compute the canvas.
|
|
func (w *Canvas) Compute(e render.Engine) {
|
|
|
|
}
|