2018-09-26 17:04:46 +00:00
|
|
|
package uix
|
2018-08-17 03:37:19 +00:00
|
|
|
|
|
|
|
import (
|
2018-10-08 20:06:42 +00:00
|
|
|
"fmt"
|
2018-10-28 05:22:13 +00:00
|
|
|
"os"
|
2019-06-27 01:36:54 +00:00
|
|
|
"runtime"
|
2018-10-08 20:06:42 +00:00
|
|
|
"strings"
|
|
|
|
|
2019-04-10 00:35:44 +00:00
|
|
|
"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"
|
2019-06-28 05:54:46 +00:00
|
|
|
"git.kirsle.net/apps/doodle/pkg/bindata"
|
Add Switches, Fire/Water Collision and Play Menu
* New doodads: Switches.
* They come in four varieties: wall switch (background element, with
"ON/OFF" text) and three side-profile switches for the floor, left
or right walls.
* On collision with the player, they flip their state from "OFF" to
"ON" or vice versa. If the player walks away and then collides
again, the switch flips again.
* Can be used to open/close Electric Doors when turned on/off. Their
default state is "off"
* If a switch receives a power signal from another linked switch, it
sets its own state to match. So, two "on/off" switches that are
connected to a door AND to each other will both flip on/off when one
of them flips.
* Update the Level Collision logic to support Decoration, Fire and Water
pixel collisions.
* Previously, ALL pixels in the level were acting as though solid.
* Non-solid pixels don't count for collision detection, but their
attributes (fire and water) are collected and returned.
* Updated the MenuScene to support loading a map file in Play Mode
instead of Edit Mode. Updated the title screen menu to add a button
for playing levels instead of editing them.
* Wrote some documentation.
2019-07-07 01:30:03 +00:00
|
|
|
"git.kirsle.net/apps/doodle/pkg/collision"
|
2019-04-10 00:35:44 +00:00
|
|
|
"git.kirsle.net/apps/doodle/pkg/doodads"
|
2019-07-03 23:22:30 +00:00
|
|
|
"git.kirsle.net/apps/doodle/pkg/drawtool"
|
2019-04-10 00:35:44 +00:00
|
|
|
"git.kirsle.net/apps/doodle/pkg/level"
|
|
|
|
"git.kirsle.net/apps/doodle/pkg/log"
|
2019-04-16 06:07:15 +00:00
|
|
|
"git.kirsle.net/apps/doodle/pkg/scripting"
|
2018-10-28 05:22:13 +00:00
|
|
|
"git.kirsle.net/apps/doodle/pkg/wallpaper"
|
2018-08-17 03:37:19 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
// Canvas is a custom ui.Widget that manages a single drawing.
|
|
|
|
type Canvas struct {
|
|
|
|
ui.Frame
|
2018-09-26 17:04:46 +00:00
|
|
|
Palette *level.Palette
|
2018-08-17 03:37:19 +00:00
|
|
|
|
2018-10-20 22:42:49 +00:00
|
|
|
// 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.
|
|
|
|
|
2018-10-21 00:08:20 +00:00
|
|
|
// Selected draw tool/mode, default Pencil, for editable canvases.
|
Eraser Tool, Brush Sizes
* Implement Brush Sizes for drawtool.Stroke and add a UI to the tools panel
to control the brush size.
* Brush sizes: 1, 2, 4, 8, 16, 24, 32, 48, 64
* Add the Eraser Tool to editor mode. It uses a default brush size of 16
and a max size of 32 due to some performance issues.
* The Undo/Redo system now remembers the original color of pixels when
you change them, so that Undo will set them back how they were instead
of deleting the pixel entirely. Due to performance issues, this only
happens when your Brush Size is 0 (drawing single-pixel shapes).
* UI: Add an IntVariable option to ui.Label to bind showing the value of
an int reference.
Aforementioned performance issues:
* When we try to remember whole rects of pixels for drawing thick
shapes, it requires a ton of scanning for each step of the shape. Even
de-duplicating pixel checks, tons of extra reads are constantly
checked.
* The Eraser is the only tool that absolutely needs to be able to
remember wiped pixels AND have large brush sizes. The performance
sucks and lags a bit if you erase a lot all at once, but it's a
trade-off for now.
* So pixels aren't remembered when drawing lines in your level with
thick brushes, so the Undo action will simply delete your pixels and not
reset them. Only the Eraser can bring back pixels.
2019-07-12 02:07:46 +00:00
|
|
|
Tool drawtool.Tool
|
|
|
|
BrushSize int // thickness of selected brush
|
2018-10-21 00:08:20 +00:00
|
|
|
|
2018-10-20 22:42:49 +00:00
|
|
|
// 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
|
2018-08-17 03:37:19 +00:00
|
|
|
|
2019-04-10 02:17:56 +00:00
|
|
|
// Actor ID to follow the camera on automatically, i.e. the main player.
|
|
|
|
FollowActor string
|
|
|
|
|
2018-10-28 05:22:13 +00:00
|
|
|
// Debug tools
|
|
|
|
// NoLimitScroll suppresses the scroll limit for bounded levels.
|
|
|
|
NoLimitScroll bool
|
|
|
|
|
2018-10-19 20:31:58 +00:00
|
|
|
// Underlying chunk data for the drawing.
|
2019-07-03 23:22:30 +00:00
|
|
|
level *level.Level
|
2018-10-19 20:31:58 +00:00
|
|
|
chunks *level.Chunker
|
|
|
|
|
|
|
|
// Actors to superimpose on top of the drawing.
|
2019-04-14 22:25:03 +00:00
|
|
|
actor *Actor // if this canvas IS an actor
|
|
|
|
actors []*Actor // if this canvas CONTAINS actors (i.e., is a level)
|
2018-10-19 20:31:58 +00:00
|
|
|
|
2019-05-07 05:57:32 +00:00
|
|
|
// Collision memory for the actors.
|
|
|
|
collidingActors map[string]string // mapping their IDs to each other
|
|
|
|
|
2019-04-16 06:07:15 +00:00
|
|
|
// Doodad scripting engine supervisor.
|
|
|
|
// NOTE: initialized and managed by the play_scene.
|
|
|
|
scripting *scripting.Supervisor
|
|
|
|
|
2018-10-28 05:22:13 +00:00
|
|
|
// Wallpaper settings.
|
|
|
|
wallpaper *Wallpaper
|
|
|
|
|
2018-10-21 00:08:20 +00:00
|
|
|
// 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)
|
2019-07-05 23:04:36 +00:00
|
|
|
OnDragStart func(*level.Actor)
|
2018-10-21 00:08:20 +00:00
|
|
|
|
2019-06-23 23:15:09 +00:00
|
|
|
// -- WHEN Canvas.Tool is "Link" --
|
2019-06-09 00:02:28 +00:00
|
|
|
// When the Canvas wants to link two actors together. Arguments are the IDs
|
|
|
|
// of the two actors.
|
2019-06-28 05:54:46 +00:00
|
|
|
OnLinkActors func(a, b *Actor)
|
2019-06-23 23:15:09 +00:00
|
|
|
linkFirst *Actor
|
2019-06-09 00:02:28 +00:00
|
|
|
|
Add Switches, Fire/Water Collision and Play Menu
* New doodads: Switches.
* They come in four varieties: wall switch (background element, with
"ON/OFF" text) and three side-profile switches for the floor, left
or right walls.
* On collision with the player, they flip their state from "OFF" to
"ON" or vice versa. If the player walks away and then collides
again, the switch flips again.
* Can be used to open/close Electric Doors when turned on/off. Their
default state is "off"
* If a switch receives a power signal from another linked switch, it
sets its own state to match. So, two "on/off" switches that are
connected to a door AND to each other will both flip on/off when one
of them flips.
* Update the Level Collision logic to support Decoration, Fire and Water
pixel collisions.
* Previously, ALL pixels in the level were acting as though solid.
* Non-solid pixels don't count for collision detection, but their
attributes (fire and water) are collected and returned.
* Updated the MenuScene to support loading a map file in Play Mode
instead of Edit Mode. Updated the title screen menu to add a button
for playing levels instead of editing them.
* Wrote some documentation.
2019-07-07 01:30:03 +00:00
|
|
|
// Collision handlers for level geometry.
|
|
|
|
OnLevelCollision func(*Actor, *collision.Collide)
|
|
|
|
|
2019-07-03 23:22:30 +00:00
|
|
|
/********
|
|
|
|
* Editable canvas private variables.
|
|
|
|
********/
|
|
|
|
// The current stroke actively being drawn by the user, during a
|
|
|
|
// mousedown-and-dragging event.
|
|
|
|
currentStroke *drawtool.Stroke
|
|
|
|
strokes map[int]*drawtool.Stroke // active stroke mapped by ID
|
Eraser Tool, Brush Sizes
* Implement Brush Sizes for drawtool.Stroke and add a UI to the tools panel
to control the brush size.
* Brush sizes: 1, 2, 4, 8, 16, 24, 32, 48, 64
* Add the Eraser Tool to editor mode. It uses a default brush size of 16
and a max size of 32 due to some performance issues.
* The Undo/Redo system now remembers the original color of pixels when
you change them, so that Undo will set them back how they were instead
of deleting the pixel entirely. Due to performance issues, this only
happens when your Brush Size is 0 (drawing single-pixel shapes).
* UI: Add an IntVariable option to ui.Label to bind showing the value of
an int reference.
Aforementioned performance issues:
* When we try to remember whole rects of pixels for drawing thick
shapes, it requires a ton of scanning for each step of the shape. Even
de-duplicating pixel checks, tons of extra reads are constantly
checked.
* The Eraser is the only tool that absolutely needs to be able to
remember wiped pixels AND have large brush sizes. The performance
sucks and lags a bit if you erase a lot all at once, but it's a
trade-off for now.
* So pixels aren't remembered when drawing lines in your level with
thick brushes, so the Undo action will simply delete your pixels and not
reset them. Only the Eraser can bring back pixels.
2019-07-12 02:07:46 +00:00
|
|
|
lastPixel *level.Pixel
|
2018-08-17 03:37:19 +00:00
|
|
|
|
|
|
|
// 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.
|
2018-09-26 17:04:46 +00:00
|
|
|
//
|
|
|
|
// 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.
|
2018-09-23 22:20:45 +00:00
|
|
|
func NewCanvas(size int, editable bool) *Canvas {
|
2018-08-17 03:37:19 +00:00
|
|
|
w := &Canvas{
|
2018-09-26 17:04:46 +00:00
|
|
|
Editable: editable,
|
|
|
|
Scrollable: editable,
|
|
|
|
Palette: level.NewPalette(),
|
|
|
|
chunks: level.NewChunker(size),
|
2018-10-19 20:31:58 +00:00
|
|
|
actors: make([]*Actor, 0),
|
2018-10-28 05:22:13 +00:00
|
|
|
wallpaper: &Wallpaper{},
|
2019-07-03 23:22:30 +00:00
|
|
|
|
|
|
|
strokes: map[int]*drawtool.Stroke{},
|
2018-08-17 03:37:19 +00:00
|
|
|
}
|
|
|
|
w.setup()
|
2018-10-08 17:38:49 +00:00
|
|
|
w.IDFunc(func() string {
|
2018-10-08 20:06:42 +00:00
|
|
|
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, "; "))
|
2018-10-08 17:38:49 +00:00
|
|
|
})
|
2018-08-17 03:37:19 +00:00
|
|
|
return w
|
|
|
|
}
|
|
|
|
|
|
|
|
// Load initializes the Canvas using an existing Palette and Grid.
|
2018-09-26 17:04:46 +00:00
|
|
|
func (w *Canvas) Load(p *level.Palette, g *level.Chunker) {
|
2018-08-17 03:37:19 +00:00
|
|
|
w.Palette = p
|
2018-09-23 22:20:45 +00:00
|
|
|
w.chunks = g
|
2018-08-17 03:37:19 +00:00
|
|
|
|
|
|
|
if len(w.Palette.Swatches) > 0 {
|
|
|
|
w.SetSwatch(w.Palette.Swatches[0])
|
|
|
|
}
|
2018-09-25 16:40:34 +00:00
|
|
|
}
|
2018-08-17 03:37:19 +00:00
|
|
|
|
2018-09-25 16:40:34 +00:00
|
|
|
// LoadLevel initializes a Canvas from a Level object.
|
2018-10-28 05:22:13 +00:00
|
|
|
func (w *Canvas) LoadLevel(e render.Engine, level *level.Level) {
|
2019-07-03 23:22:30 +00:00
|
|
|
w.level = level
|
2018-09-25 16:40:34 +00:00
|
|
|
w.Load(level.Palette, level.Chunker)
|
2018-10-28 05:22:13 +00:00
|
|
|
|
|
|
|
// TODO: wallpaper paths
|
|
|
|
filename := "assets/wallpapers/" + level.Wallpaper
|
2019-06-27 01:36:54 +00:00
|
|
|
if runtime.GOOS != "js" {
|
2019-06-28 05:54:46 +00:00
|
|
|
// Check if the wallpaper wasn't found. Check bindata and file system.
|
|
|
|
if _, err := bindata.Asset(filename); err != nil {
|
|
|
|
if _, err := os.Stat(filename); os.IsNotExist(err) {
|
|
|
|
log.Error("LoadLevel: wallpaper %s did not appear to exist, default to notebook.png", filename)
|
|
|
|
filename = "assets/wallpapers/notebook.png"
|
|
|
|
}
|
2019-06-27 01:36:54 +00:00
|
|
|
}
|
2018-10-28 05:22:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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)
|
|
|
|
}
|
2018-08-17 03:37:19 +00:00
|
|
|
}
|
|
|
|
|
2018-09-26 17:04:46 +00:00
|
|
|
// 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)
|
|
|
|
}
|
|
|
|
|
2018-08-17 03:37:19 +00:00
|
|
|
// SetSwatch changes the currently selected swatch for editing.
|
2018-09-26 17:04:46 +00:00
|
|
|
func (w *Canvas) SetSwatch(s *level.Swatch) {
|
2018-08-17 03:37:19 +00:00
|
|
|
w.Palette.ActiveSwatch = s
|
|
|
|
}
|
|
|
|
|
|
|
|
// setup common configs between both initializers of the canvas.
|
|
|
|
func (w *Canvas) setup() {
|
2018-10-19 20:31:58 +00:00
|
|
|
// XXX: Debug code.
|
|
|
|
if balance.DebugCanvasBorder != render.Invisible {
|
|
|
|
w.Configure(ui.Config{
|
|
|
|
BorderColor: balance.DebugCanvasBorder,
|
|
|
|
BorderSize: 2,
|
|
|
|
BorderStyle: ui.BorderSolid,
|
|
|
|
})
|
|
|
|
}
|
2018-08-17 03:37:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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 {
|
2019-04-10 02:17:56 +00:00
|
|
|
// 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
|
|
|
|
}
|
2019-04-19 23:21:04 +00:00
|
|
|
_ = w.loopConstrainScroll()
|
2019-04-10 02:17:56 +00:00
|
|
|
|
2019-05-05 23:32:30 +00:00
|
|
|
// Current time of this loop so we can advance animations.
|
2019-05-29 04:43:30 +00:00
|
|
|
// now := time.Now()
|
2019-05-05 23:32:30 +00:00
|
|
|
|
2019-04-19 01:15:05 +00:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2019-04-16 02:12:25 +00:00
|
|
|
// Check collisions between actors.
|
2019-07-05 22:02:22 +00:00
|
|
|
if w.scripting != nil {
|
|
|
|
if err := w.loopActorCollision(); err != nil {
|
|
|
|
log.Error("loopActorCollision: %s", err)
|
|
|
|
}
|
2018-08-17 03:37:19 +00:00
|
|
|
}
|
|
|
|
|
2018-10-21 00:08:20 +00:00
|
|
|
// 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)
|
2018-08-17 03:37:19 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
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.
|
2018-10-19 20:31:58 +00:00
|
|
|
//
|
|
|
|
// 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.
|
2018-08-17 03:37:19 +00:00
|
|
|
func (w *Canvas) Viewport() render.Rect {
|
|
|
|
var S = w.Size()
|
|
|
|
return render.Rect{
|
2018-10-18 06:01:21 +00:00
|
|
|
X: -w.Scroll.X,
|
|
|
|
Y: -w.Scroll.Y,
|
|
|
|
W: S.W - w.Scroll.X,
|
|
|
|
H: S.H - w.Scroll.Y,
|
2018-08-17 03:37:19 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-10-19 20:31:58 +00:00
|
|
|
// 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,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-10-20 22:42:49 +00:00
|
|
|
// 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,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-09-23 22:20:45 +00:00
|
|
|
// Chunker returns the underlying Chunker object.
|
2018-09-26 17:04:46 +00:00
|
|
|
func (w *Canvas) Chunker() *level.Chunker {
|
2018-09-23 22:20:45 +00:00
|
|
|
return w.chunks
|
2018-08-17 03:37:19 +00:00
|
|
|
}
|
|
|
|
|
2018-09-26 17:04:46 +00:00
|
|
|
// ScrollTo sets the viewport scroll position.
|
|
|
|
func (w *Canvas) ScrollTo(to render.Point) {
|
|
|
|
w.Scroll.X = to.X
|
|
|
|
w.Scroll.Y = to.Y
|
|
|
|
}
|
|
|
|
|
2018-08-17 03:37:19 +00:00
|
|
|
// 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) {
|
|
|
|
|
|
|
|
}
|