2019-04-10 02:17:56 +00:00
|
|
|
package uix
|
|
|
|
|
|
|
|
import (
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
|
2022-09-24 22:17:25 +00:00
|
|
|
"git.kirsle.net/SketchyMaze/doodle/pkg/balance"
|
|
|
|
"git.kirsle.net/SketchyMaze/doodle/pkg/drawtool"
|
|
|
|
"git.kirsle.net/SketchyMaze/doodle/pkg/keybind"
|
|
|
|
"git.kirsle.net/SketchyMaze/doodle/pkg/level"
|
|
|
|
"git.kirsle.net/SketchyMaze/doodle/pkg/shmem"
|
2019-12-28 03:16:34 +00:00
|
|
|
"git.kirsle.net/go/render"
|
|
|
|
"git.kirsle.net/go/render/event"
|
2019-04-10 02:17:56 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
/*
|
|
|
|
Loop() subroutine to scroll the canvas using arrow keys (for edit mode).
|
|
|
|
|
|
|
|
If w.Scrollable is false this function won't do anything.
|
|
|
|
|
|
|
|
Cursor keys will scroll the drawing by balance.CanvasScrollSpeed per tick.
|
|
|
|
If the level pageType is constrained, the scrollable viewport will be
|
|
|
|
constrained to fit the bounds of the level.
|
|
|
|
|
|
|
|
The debug boolean `NoLimitScroll=true` will override the bounded level scroll
|
|
|
|
restriction and allow scrolling into out-of-bounds areas of the level.
|
|
|
|
*/
|
2019-12-22 22:11:01 +00:00
|
|
|
func (w *Canvas) loopEditorScroll(ev *event.State) error {
|
2019-04-10 02:17:56 +00:00
|
|
|
if !w.Scrollable {
|
|
|
|
return errors.New("canvas not scrollable")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Arrow keys to scroll the view.
|
2021-07-13 05:19:36 +00:00
|
|
|
// Shift key to scroll very slowly.
|
|
|
|
var (
|
|
|
|
scrollBy = render.Point{}
|
|
|
|
scrollSpeed = balance.CanvasScrollSpeed
|
|
|
|
)
|
|
|
|
if keybind.Shift(ev) {
|
|
|
|
scrollSpeed = 1
|
2019-04-10 02:17:56 +00:00
|
|
|
}
|
2021-07-13 05:19:36 +00:00
|
|
|
|
|
|
|
// Arrow key handlers.
|
|
|
|
if keybind.Right(ev) {
|
|
|
|
scrollBy.X -= scrollSpeed
|
|
|
|
} else if keybind.Left(ev) {
|
|
|
|
scrollBy.X += scrollSpeed
|
|
|
|
}
|
|
|
|
if keybind.Down(ev) {
|
|
|
|
scrollBy.Y -= scrollSpeed
|
|
|
|
} else if keybind.Up(ev) {
|
|
|
|
scrollBy.Y += scrollSpeed
|
2019-04-10 02:17:56 +00:00
|
|
|
}
|
|
|
|
if !scrollBy.IsZero() {
|
|
|
|
w.ScrollBy(scrollBy)
|
|
|
|
}
|
|
|
|
|
2021-10-07 03:02:09 +00:00
|
|
|
// Multitouch events to pan the level, like middle click on desktop.
|
|
|
|
if ev.Touching {
|
|
|
|
// Intention: user drags with 2 fingers to scroll the canvas.
|
|
|
|
// SDL2 will register one finger also as a Button1 mouse click.
|
|
|
|
// We need to record the "mouse cursor" as start point but then
|
|
|
|
// fake that no click occurs so we don't nick the drawing.
|
|
|
|
if !w.scrollDragging {
|
|
|
|
w.scrollDragging = true
|
|
|
|
w.scrollStartAt = shmem.Cursor
|
|
|
|
w.scrollWasAt = w.Scroll
|
|
|
|
w.scrollLastDelta = render.Point{}
|
|
|
|
} else {
|
|
|
|
delta := shmem.Cursor.Compare(w.scrollStartAt)
|
|
|
|
w.Scroll = w.scrollWasAt
|
|
|
|
w.Scroll.Subtract(delta)
|
|
|
|
|
|
|
|
// So, SDL2 spams us with events for every subtle movement of 2+ fingers
|
|
|
|
// on the screen, but we don't know when that STOPS. As a heuristic, it
|
|
|
|
// seems we can tell by if the delta stops updating.
|
|
|
|
if !w.scrollLastDelta.IsZero() {
|
|
|
|
if w.scrollLastDelta == delta {
|
|
|
|
ev.Touching = false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
w.scrollLastDelta = delta
|
|
|
|
}
|
|
|
|
|
|
|
|
// Lift the mouse button.
|
|
|
|
ev.Button1 = false
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2021-10-05 03:49:11 +00:00
|
|
|
// Middle click of the mouse to pan the level.
|
2022-03-05 23:31:09 +00:00
|
|
|
// NOTE: PanTool intercepts both Left and MiddleClick.
|
|
|
|
if w.Tool != drawtool.PanTool {
|
|
|
|
if keybind.MiddleClick(ev) {
|
|
|
|
if !w.scrollDragging {
|
|
|
|
w.scrollDragging = true
|
|
|
|
w.scrollStartAt = shmem.Cursor
|
|
|
|
w.scrollWasAt = w.Scroll
|
|
|
|
} else {
|
|
|
|
delta := shmem.Cursor.Compare(w.scrollStartAt)
|
|
|
|
w.Scroll = w.scrollWasAt
|
|
|
|
w.Scroll.Subtract(delta)
|
|
|
|
}
|
2021-10-05 03:49:11 +00:00
|
|
|
} else {
|
2022-03-05 23:31:09 +00:00
|
|
|
if w.scrollDragging {
|
|
|
|
w.scrollDragging = false
|
|
|
|
}
|
2021-10-05 03:49:11 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-04-10 02:17:56 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
Loop() subroutine to constrain the scrolled view to within a bounded level.
|
|
|
|
*/
|
|
|
|
func (w *Canvas) loopConstrainScroll() error {
|
Update savegame format, Allow out-of-bounds camera
Updates the savegame.json file format:
* Levels now have a UUID value assigned at first save.
* The savegame.json will now track level completion/score based on UUID,
making it robust to filename changes in either levels or levelpacks.
* The savegame file is auto-migrated on startup - for any levels not
found or have no UUID, no change is made, it's backwards compatible.
* Level Properties window adds an "Advanced" tab to show/re-roll UUID.
New JavaScript API for doodad scripts:
* `Actors.CameraFollowPlayer()` tells the camera to return focus to the
player character. Useful for "cutscene" doodads that freeze the player,
call `Self.CameraFollowMe()` and do a thing before unfreezing and sending the
camera back to the player. (Or it will follow them at their next directional
input control).
* `Self.MoveBy(Point(x, y int))` to move the current actor a bit.
New option for the `doodad` command-line tool:
* `doodad resave <.level or .doodad>` will load and re-save a drawing, to
migrate it to the newest file format versions.
Small tweaks:
* On bounded levels, allow the camera to still follow the player if the player
finds themselves WELL far out of bounds (40 pixels margin). So on bounded
levels you can create "interior rooms" out-of-bounds to Warp Door into.
* New wallpaper: "Atmosphere" has a black starscape pattern that fades into a
solid blue atmosphere.
* Camera strictly follows the player the first 20 ticks, not 60 of level start
* If player is frozen, directional inputs do not take the camera focus back.
2023-03-08 05:55:10 +00:00
|
|
|
if w.NoLimitScroll || w.scrollOutOfBounds {
|
2019-04-10 02:17:56 +00:00
|
|
|
return errors.New("NoLimitScroll enabled")
|
|
|
|
}
|
|
|
|
|
2021-10-11 22:57:33 +00:00
|
|
|
// Levels only.
|
|
|
|
if w.level == nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2021-10-10 03:45:38 +00:00
|
|
|
var (
|
|
|
|
capped bool
|
|
|
|
maxWidth = w.level.MaxWidth
|
|
|
|
maxHeight = w.level.MaxHeight
|
|
|
|
)
|
2019-04-10 02:17:56 +00:00
|
|
|
|
|
|
|
// Constrain the bottom and right for limited world sizes.
|
2019-06-25 21:57:11 +00:00
|
|
|
if w.wallpaper.pageType >= level.Bounded &&
|
2021-10-10 03:45:38 +00:00
|
|
|
maxWidth+maxHeight > 0 {
|
2019-04-10 02:17:56 +00:00
|
|
|
var (
|
|
|
|
// TODO: downcast from int64!
|
2021-10-10 03:45:38 +00:00
|
|
|
mw = int(maxWidth)
|
|
|
|
mh = int(maxHeight)
|
2019-04-10 02:17:56 +00:00
|
|
|
Viewport = w.Viewport()
|
2021-09-12 05:21:47 +00:00
|
|
|
vw = w.ZoomDivide(Viewport.W)
|
|
|
|
vh = w.ZoomDivide(Viewport.H)
|
2019-04-10 02:17:56 +00:00
|
|
|
)
|
2021-09-12 05:21:47 +00:00
|
|
|
|
|
|
|
if vw > mw {
|
|
|
|
delta := vw - mw
|
2019-04-10 02:17:56 +00:00
|
|
|
w.Scroll.X += delta
|
|
|
|
capped = true
|
|
|
|
}
|
2021-09-12 05:21:47 +00:00
|
|
|
if vh > mh {
|
|
|
|
delta := vh - mh
|
2019-04-10 02:17:56 +00:00
|
|
|
w.Scroll.Y += delta
|
|
|
|
capped = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-09-12 22:59:40 +00:00
|
|
|
// Constrain the top and left edges.
|
|
|
|
if w.wallpaper.pageType > level.Unbounded {
|
|
|
|
if w.Scroll.X > 0 {
|
|
|
|
w.Scroll.X = 0
|
|
|
|
capped = true
|
|
|
|
}
|
|
|
|
if w.Scroll.Y > 0 {
|
|
|
|
w.Scroll.Y = 0
|
|
|
|
capped = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-04-10 02:17:56 +00:00
|
|
|
if capped {
|
|
|
|
return errors.New("scroll limited by level constraint")
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
Loop() subroutine for Play Mode to follow an actor in the camera's view.
|
|
|
|
|
|
|
|
Does nothing if w.FollowActor is an empty string. Set it to the ID of an Actor
|
|
|
|
to follow. If the actor exists, the Canvas will scroll to keep it on the
|
|
|
|
screen.
|
|
|
|
*/
|
2019-12-22 22:11:01 +00:00
|
|
|
func (w *Canvas) loopFollowActor(ev *event.State) error {
|
2019-04-10 02:17:56 +00:00
|
|
|
// Are we following an actor?
|
|
|
|
if w.FollowActor == "" {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
var (
|
2021-06-14 03:25:42 +00:00
|
|
|
VP = w.Viewport()
|
|
|
|
engine = shmem.CurrentRenderEngine
|
|
|
|
Width, Height = engine.WindowSize()
|
|
|
|
midpoint = render.NewPoint(Width/2, Height/2)
|
|
|
|
scrollboxHoz = midpoint.X - balance.ScrollboxOffset.X
|
|
|
|
scrollboxVert = midpoint.Y - balance.ScrollboxOffset.Y
|
2019-04-10 02:17:56 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
// Find the actor.
|
|
|
|
for _, actor := range w.actors {
|
|
|
|
if actor.ID() != w.FollowActor {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
var (
|
2019-04-14 22:25:03 +00:00
|
|
|
APosition = actor.Position() // absolute world position
|
2019-04-10 02:17:56 +00:00
|
|
|
ASize = actor.Drawing.Size()
|
|
|
|
scrollBy render.Point
|
|
|
|
)
|
|
|
|
|
|
|
|
// Scroll left
|
2021-06-14 03:25:42 +00:00
|
|
|
if APosition.X <= VP.X+scrollboxHoz {
|
|
|
|
var delta = VP.X + scrollboxHoz - APosition.X
|
2019-04-10 02:17:56 +00:00
|
|
|
|
2020-01-03 04:23:27 +00:00
|
|
|
// constrain in case they're FAR OFF SCREEN so we don't flip back around
|
2019-04-10 02:17:56 +00:00
|
|
|
if delta < 0 {
|
|
|
|
delta = -delta
|
|
|
|
}
|
|
|
|
scrollBy.X = delta
|
|
|
|
}
|
|
|
|
|
|
|
|
// Scroll right
|
2021-06-14 03:25:42 +00:00
|
|
|
if APosition.X >= VP.W-ASize.W-scrollboxHoz {
|
|
|
|
var delta = VP.W - ASize.W - APosition.X - scrollboxHoz
|
2020-01-03 04:23:27 +00:00
|
|
|
scrollBy.X = delta
|
2019-04-10 02:17:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Scroll up
|
2021-06-14 03:25:42 +00:00
|
|
|
if APosition.Y <= VP.Y+scrollboxVert {
|
|
|
|
var delta = VP.Y + scrollboxVert - APosition.Y
|
2019-04-19 05:02:59 +00:00
|
|
|
|
2019-04-10 02:17:56 +00:00
|
|
|
if delta < 0 {
|
|
|
|
delta = -delta
|
|
|
|
}
|
|
|
|
scrollBy.Y = delta
|
|
|
|
}
|
|
|
|
|
|
|
|
// Scroll down
|
2021-06-14 03:25:42 +00:00
|
|
|
if APosition.Y >= VP.H-ASize.H-scrollboxVert {
|
|
|
|
var delta = VP.H - ASize.H - APosition.Y - scrollboxVert
|
2020-01-03 04:23:27 +00:00
|
|
|
if delta > 300 {
|
|
|
|
delta = 300
|
|
|
|
} else if delta < -300 {
|
|
|
|
delta = -300
|
2019-04-10 02:17:56 +00:00
|
|
|
}
|
2020-01-03 04:23:27 +00:00
|
|
|
scrollBy.Y = delta
|
2019-04-10 02:17:56 +00:00
|
|
|
}
|
|
|
|
|
2022-09-25 06:54:51 +00:00
|
|
|
// If we are VERY FAR away, allow greater leaps.
|
|
|
|
if scrollBy.X > balance.FollowActorMaxScrollSpeed*4 {
|
|
|
|
scrollBy.X = balance.FollowActorMaxScrollSpeed * 4
|
|
|
|
} else if scrollBy.X < -balance.FollowActorMaxScrollSpeed*4 {
|
|
|
|
scrollBy.X = -balance.FollowActorMaxScrollSpeed * 4
|
|
|
|
}
|
|
|
|
if scrollBy.Y > balance.FollowActorMaxScrollSpeed*4 {
|
|
|
|
scrollBy.Y = balance.FollowActorMaxScrollSpeed * 4
|
|
|
|
} else if scrollBy.Y < -balance.FollowActorMaxScrollSpeed*4 {
|
|
|
|
scrollBy.Y = -balance.FollowActorMaxScrollSpeed * 4
|
|
|
|
}
|
|
|
|
|
2021-08-16 03:17:53 +00:00
|
|
|
// Constrain the maximum scroll speed.
|
|
|
|
if scrollBy.X > balance.FollowActorMaxScrollSpeed {
|
|
|
|
scrollBy.X = balance.FollowActorMaxScrollSpeed
|
|
|
|
} else if scrollBy.X < -balance.FollowActorMaxScrollSpeed {
|
|
|
|
scrollBy.X = -balance.FollowActorMaxScrollSpeed
|
|
|
|
}
|
|
|
|
if scrollBy.Y > balance.FollowActorMaxScrollSpeed {
|
|
|
|
scrollBy.Y = balance.FollowActorMaxScrollSpeed
|
|
|
|
} else if scrollBy.Y < -balance.FollowActorMaxScrollSpeed {
|
|
|
|
scrollBy.Y = -balance.FollowActorMaxScrollSpeed
|
|
|
|
}
|
|
|
|
|
2019-04-10 02:17:56 +00:00
|
|
|
if scrollBy != render.Origin {
|
|
|
|
w.ScrollBy(scrollBy)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return fmt.Errorf("actor ID '%s' not found in level", w.FollowActor)
|
|
|
|
}
|