2018-06-21 01:43:14 +00:00
|
|
|
package doodle
|
|
|
|
|
|
|
|
import (
|
2018-09-26 17:04:46 +00:00
|
|
|
"errors"
|
2018-09-25 16:40:34 +00:00
|
|
|
"fmt"
|
2018-06-21 01:43:14 +00:00
|
|
|
"io/ioutil"
|
|
|
|
"os"
|
2018-10-02 17:11:38 +00:00
|
|
|
"strings"
|
2018-06-21 01:43:14 +00:00
|
|
|
|
2018-09-26 17:04:46 +00:00
|
|
|
"git.kirsle.net/apps/doodle/doodads"
|
2018-09-25 16:40:34 +00:00
|
|
|
"git.kirsle.net/apps/doodle/enum"
|
2018-07-22 00:12:22 +00:00
|
|
|
"git.kirsle.net/apps/doodle/events"
|
2018-06-21 01:43:14 +00:00
|
|
|
"git.kirsle.net/apps/doodle/level"
|
2018-10-19 20:31:58 +00:00
|
|
|
"git.kirsle.net/apps/doodle/pkg/userdir"
|
2018-07-22 00:12:22 +00:00
|
|
|
"git.kirsle.net/apps/doodle/render"
|
2018-06-21 01:43:14 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
// EditorScene manages the "Edit Level" game mode.
|
|
|
|
type EditorScene struct {
|
2018-07-24 03:10:53 +00:00
|
|
|
// Configuration for the scene initializer.
|
2018-09-26 17:04:46 +00:00
|
|
|
DrawingType enum.DrawingType
|
|
|
|
OpenFile bool
|
|
|
|
Filename string
|
|
|
|
DoodadSize int
|
2018-07-24 03:10:53 +00:00
|
|
|
|
2018-08-05 19:54:57 +00:00
|
|
|
UI *EditorUI
|
2018-10-28 05:22:13 +00:00
|
|
|
d *Doodle
|
2018-08-05 19:54:57 +00:00
|
|
|
|
2018-09-26 17:04:46 +00:00
|
|
|
// The current level or doodad object being edited, based on the
|
|
|
|
// DrawingType.
|
|
|
|
Level *level.Level
|
|
|
|
Doodad *doodads.Doodad
|
2018-09-25 16:40:34 +00:00
|
|
|
|
Play: Autoscrolling and Bounded Level Support
Implement scrolling behavior in Play Mode by allowing the Canvas to
follow a specific actor and keep it in view. The Canvas has a
FollowActor property which holds an ID of the actor to follow (if blank,
no actor is being followed).
In Play Mode the Player is followed and when they get too close to the
left or right edges of the screen, the level will scroll to try and
catch them. If the player is moving very fast they can outrun the
camera.
The bounded levels are enforced in Play Mode and the camera won't scroll
to view pixels out-of-bounds and the Doodad actors inside the level
aren't allowed to exit its boundaries. This is global, not only for the
Player doodad but any Doodad that came with the level as well.
Other changes:
- Restructured Canvas widget code into many new files. The Canvas widget
is shaping up to be where most of the magic happens, which is okay
because it's close to the action and pulling the strings from outside
would be harder, even tho as a UI element you think it should be
lightweight.
- Debug Overlay: added room for Scenes to insert their own custom Debug
Overlay key/value pairs (the values are string pointers so the Scene
can update them freely):
- The core labels are FPS, Scene and Mouse. The Pixel (world
coordinate under cursor) is removed from the core labels.
- Edit Scene provides Pixel, Tool and Swatch
- Play Scene provides Pixel, Player, Viewport, Scroll
2018-10-29 00:33:24 +00:00
|
|
|
// Custom debug overlay labels.
|
|
|
|
debTool *string
|
|
|
|
debSwatch *string
|
|
|
|
debWorldIndex *string
|
|
|
|
|
2018-09-25 16:40:34 +00:00
|
|
|
// Last saved filename by the user.
|
|
|
|
filename string
|
2018-06-21 01:43:14 +00:00
|
|
|
}
|
|
|
|
|
2018-06-21 02:00:46 +00:00
|
|
|
// Name of the scene.
|
|
|
|
func (s *EditorScene) Name() string {
|
|
|
|
return "Edit"
|
|
|
|
}
|
|
|
|
|
2018-06-21 01:43:14 +00:00
|
|
|
// Setup the editor scene.
|
2018-07-21 22:11:00 +00:00
|
|
|
func (s *EditorScene) Setup(d *Doodle) error {
|
Play: Autoscrolling and Bounded Level Support
Implement scrolling behavior in Play Mode by allowing the Canvas to
follow a specific actor and keep it in view. The Canvas has a
FollowActor property which holds an ID of the actor to follow (if blank,
no actor is being followed).
In Play Mode the Player is followed and when they get too close to the
left or right edges of the screen, the level will scroll to try and
catch them. If the player is moving very fast they can outrun the
camera.
The bounded levels are enforced in Play Mode and the camera won't scroll
to view pixels out-of-bounds and the Doodad actors inside the level
aren't allowed to exit its boundaries. This is global, not only for the
Player doodad but any Doodad that came with the level as well.
Other changes:
- Restructured Canvas widget code into many new files. The Canvas widget
is shaping up to be where most of the magic happens, which is okay
because it's close to the action and pulling the strings from outside
would be harder, even tho as a UI element you think it should be
lightweight.
- Debug Overlay: added room for Scenes to insert their own custom Debug
Overlay key/value pairs (the values are string pointers so the Scene
can update them freely):
- The core labels are FPS, Scene and Mouse. The Pixel (world
coordinate under cursor) is removed from the core labels.
- Edit Scene provides Pixel, Tool and Swatch
- Play Scene provides Pixel, Player, Viewport, Scroll
2018-10-29 00:33:24 +00:00
|
|
|
// Debug overlay labels.
|
|
|
|
s.debTool = new(string)
|
|
|
|
s.debSwatch = new(string)
|
|
|
|
s.debWorldIndex = new(string)
|
|
|
|
customDebugLabels = []debugLabel{
|
|
|
|
{"Pixel:", s.debWorldIndex},
|
|
|
|
{"Tool:", s.debTool},
|
|
|
|
{"Swatch:", s.debSwatch},
|
|
|
|
}
|
|
|
|
|
2018-10-08 17:38:49 +00:00
|
|
|
// Initialize the user interface. It references the palette and such so it
|
|
|
|
// must be initialized after those things.
|
2018-10-28 05:22:13 +00:00
|
|
|
s.d = d
|
2018-10-08 17:38:49 +00:00
|
|
|
s.UI = NewEditorUI(d, s)
|
2018-09-25 16:40:34 +00:00
|
|
|
|
2018-09-26 17:04:46 +00:00
|
|
|
// Were we given configuration data?
|
2018-07-24 03:10:53 +00:00
|
|
|
if s.Filename != "" {
|
2018-09-25 16:40:34 +00:00
|
|
|
log.Debug("EditorScene.Setup: Set filename to %s", s.Filename)
|
2018-07-24 03:10:53 +00:00
|
|
|
s.filename = s.Filename
|
|
|
|
s.Filename = ""
|
2018-09-25 16:40:34 +00:00
|
|
|
}
|
2018-09-26 17:04:46 +00:00
|
|
|
|
|
|
|
// Loading a Level or a Doodad?
|
|
|
|
switch s.DrawingType {
|
|
|
|
case enum.LevelDrawing:
|
|
|
|
if s.Level != nil {
|
|
|
|
log.Debug("EditorScene.Setup: received level from scene caller")
|
2018-10-28 05:22:13 +00:00
|
|
|
s.UI.Canvas.LoadLevel(d.Engine, s.Level)
|
2018-10-21 02:49:59 +00:00
|
|
|
s.UI.Canvas.InstallActors(s.Level.Actors)
|
2018-09-26 17:04:46 +00:00
|
|
|
} else if s.filename != "" && s.OpenFile {
|
|
|
|
log.Debug("EditorScene.Setup: Loading map from filename at %s", s.filename)
|
|
|
|
if err := s.LoadLevel(s.filename); err != nil {
|
|
|
|
d.Flash("LoadLevel error: %s", err)
|
|
|
|
}
|
2018-07-24 03:10:53 +00:00
|
|
|
}
|
2018-09-25 16:40:34 +00:00
|
|
|
|
2018-09-26 17:04:46 +00:00
|
|
|
// No level?
|
|
|
|
if s.Level == nil {
|
|
|
|
log.Debug("EditorScene.Setup: initializing a new Level")
|
|
|
|
s.Level = level.New()
|
|
|
|
s.Level.Palette = level.DefaultPalette()
|
2018-10-28 05:22:13 +00:00
|
|
|
s.UI.Canvas.LoadLevel(d.Engine, s.Level)
|
2018-10-08 17:38:49 +00:00
|
|
|
s.UI.Canvas.ScrollTo(render.Origin)
|
|
|
|
s.UI.Canvas.Scrollable = true
|
2018-09-26 17:04:46 +00:00
|
|
|
}
|
|
|
|
case enum.DoodadDrawing:
|
|
|
|
// No Doodad?
|
|
|
|
if s.filename != "" && s.OpenFile {
|
|
|
|
log.Debug("EditorScene.Setup: Loading doodad from filename at %s", s.filename)
|
|
|
|
if err := s.LoadDoodad(s.filename); err != nil {
|
|
|
|
d.Flash("LoadDoodad error: %s", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// No Doodad?
|
|
|
|
if s.Doodad == nil {
|
|
|
|
log.Debug("EditorScene.Setup: initializing a new Doodad")
|
|
|
|
s.Doodad = doodads.New(s.DoodadSize)
|
2018-10-08 17:38:49 +00:00
|
|
|
s.UI.Canvas.LoadDoodad(s.Doodad)
|
2018-09-26 17:04:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: move inside the UI. Just an approximate position for now.
|
2018-10-08 17:38:49 +00:00
|
|
|
s.UI.Canvas.Resize(render.NewRect(int32(s.DoodadSize), int32(s.DoodadSize)))
|
|
|
|
s.UI.Canvas.ScrollTo(render.Origin)
|
|
|
|
s.UI.Canvas.Scrollable = false
|
|
|
|
s.UI.Workspace.Compute(d.Engine)
|
2018-07-24 03:10:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
d.Flash("Editor Mode. Press 'P' to play this map.")
|
|
|
|
|
2018-06-21 01:43:14 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Loop the editor scene.
|
2018-07-22 00:12:22 +00:00
|
|
|
func (s *EditorScene) Loop(d *Doodle, ev *events.State) error {
|
Play: Autoscrolling and Bounded Level Support
Implement scrolling behavior in Play Mode by allowing the Canvas to
follow a specific actor and keep it in view. The Canvas has a
FollowActor property which holds an ID of the actor to follow (if blank,
no actor is being followed).
In Play Mode the Player is followed and when they get too close to the
left or right edges of the screen, the level will scroll to try and
catch them. If the player is moving very fast they can outrun the
camera.
The bounded levels are enforced in Play Mode and the camera won't scroll
to view pixels out-of-bounds and the Doodad actors inside the level
aren't allowed to exit its boundaries. This is global, not only for the
Player doodad but any Doodad that came with the level as well.
Other changes:
- Restructured Canvas widget code into many new files. The Canvas widget
is shaping up to be where most of the magic happens, which is okay
because it's close to the action and pulling the strings from outside
would be harder, even tho as a UI element you think it should be
lightweight.
- Debug Overlay: added room for Scenes to insert their own custom Debug
Overlay key/value pairs (the values are string pointers so the Scene
can update them freely):
- The core labels are FPS, Scene and Mouse. The Pixel (world
coordinate under cursor) is removed from the core labels.
- Edit Scene provides Pixel, Tool and Swatch
- Play Scene provides Pixel, Player, Viewport, Scroll
2018-10-29 00:33:24 +00:00
|
|
|
// Update debug overlay labels.
|
|
|
|
*s.debTool = s.UI.Canvas.Tool.String()
|
|
|
|
*s.debSwatch = s.UI.Canvas.Palette.ActiveSwatch.Name
|
|
|
|
*s.debWorldIndex = s.UI.Canvas.WorldIndexAt(s.UI.cursor).String()
|
|
|
|
|
2018-10-19 20:31:58 +00:00
|
|
|
// Has the window been resized?
|
|
|
|
if resized := ev.Resized.Read(); resized {
|
|
|
|
w, h := d.Engine.WindowSize()
|
|
|
|
if w != d.width || h != d.height {
|
|
|
|
// Not a false alarm.
|
|
|
|
d.width = w
|
|
|
|
d.height = h
|
|
|
|
s.UI.Resized(d)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-08-05 19:54:57 +00:00
|
|
|
s.UI.Loop(ev)
|
2018-06-21 01:43:14 +00:00
|
|
|
|
2018-07-24 03:10:53 +00:00
|
|
|
// Switching to Play Mode?
|
|
|
|
if ev.KeyName.Read() == "p" {
|
|
|
|
log.Info("Play Mode, Go!")
|
|
|
|
d.Goto(&PlayScene{
|
2018-09-25 16:40:34 +00:00
|
|
|
Filename: s.filename,
|
|
|
|
Level: s.Level,
|
2018-07-24 03:10:53 +00:00
|
|
|
})
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-07-22 03:43:01 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Draw the current frame.
|
|
|
|
func (s *EditorScene) Draw(d *Doodle) error {
|
2018-08-17 03:37:19 +00:00
|
|
|
// Clear the canvas and fill it with magenta so it's clear if any spots are missed.
|
|
|
|
d.Engine.Clear(render.Magenta)
|
Menu Toolbar for Editor + Shell Prompts + Theme
* Added a "menu toolbar" to the top of the Edit Mode with useful buttons
that work: New Level, New Doodad (same thing), Save, Save as, Open.
* Added ability for the dev console to prompt the user for a question,
which opens the console automatically. "Save", "Save as" and "Load"
ask for their filenames this way.
* Started groundwork for theming the app. The palette window is a light
brown with an orange title bar, the Menu Toolbar has a black
background, etc.
* Added support for multiple fonts instead of just monospace. DejaVu
Sans (normal and bold) are used now for most labels and window titles,
respectively. The dev console uses DejaVu Sans Mono as before.
* Update ui.Label to accept PadX and PadY separately instead of only
having the Padding option which did both.
* Improvements to Frame packing algorithm.
* Set the SDL draw mode to BLEND so we can use alpha colors properly,
so now the dev console is semi-translucent.
2018-08-12 00:30:00 +00:00
|
|
|
|
2018-08-05 19:54:57 +00:00
|
|
|
s.UI.Present(d.Engine)
|
2018-08-17 03:37:19 +00:00
|
|
|
|
2018-06-21 01:43:14 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// LoadLevel loads a level from disk.
|
2018-07-21 22:11:00 +00:00
|
|
|
func (s *EditorScene) LoadLevel(filename string) error {
|
2018-07-22 03:43:01 +00:00
|
|
|
s.filename = filename
|
2018-08-11 00:19:47 +00:00
|
|
|
|
2018-09-25 16:40:34 +00:00
|
|
|
level, err := level.LoadJSON(filename)
|
2018-10-19 20:31:58 +00:00
|
|
|
fmt.Printf("%+v\n", level)
|
2018-09-25 16:40:34 +00:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("EditorScene.LoadLevel(%s): %s", filename, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
s.DrawingType = enum.LevelDrawing
|
|
|
|
s.Level = level
|
2018-10-28 05:22:13 +00:00
|
|
|
s.UI.Canvas.LoadLevel(s.d.Engine, s.Level)
|
2018-10-19 20:31:58 +00:00
|
|
|
|
|
|
|
// TODO: debug
|
|
|
|
for i, actor := range level.Actors {
|
|
|
|
log.Info("Actor %s is a %s", i, actor.ID())
|
|
|
|
}
|
|
|
|
for name, file := range level.Files {
|
|
|
|
log.Info("File %s has: %s", name, file.Data)
|
|
|
|
}
|
|
|
|
|
|
|
|
log.Info("Installing %d actors into the drawing", len(level.Actors))
|
|
|
|
if err := s.UI.Canvas.InstallActors(level.Actors); err != nil {
|
|
|
|
return fmt.Errorf("EditorScene.LoadLevel: InstallActors: %s", err)
|
|
|
|
}
|
|
|
|
|
2018-09-25 16:40:34 +00:00
|
|
|
return nil
|
2018-06-21 01:43:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// SaveLevel saves the level to disk.
|
2018-08-17 03:37:19 +00:00
|
|
|
// TODO: move this into the Canvas?
|
2018-09-26 17:04:46 +00:00
|
|
|
func (s *EditorScene) SaveLevel(filename string) error {
|
|
|
|
if s.DrawingType != enum.LevelDrawing {
|
|
|
|
return errors.New("SaveLevel: current drawing is not a Level type")
|
|
|
|
}
|
|
|
|
|
2018-10-19 20:31:58 +00:00
|
|
|
if !strings.HasSuffix(filename, enum.LevelExt) {
|
|
|
|
filename += enum.LevelExt
|
2018-10-02 17:11:38 +00:00
|
|
|
}
|
|
|
|
|
2018-07-22 03:43:01 +00:00
|
|
|
s.filename = filename
|
2018-08-11 00:19:47 +00:00
|
|
|
|
2018-09-25 16:40:34 +00:00
|
|
|
m := s.Level
|
|
|
|
if m.Title == "" {
|
|
|
|
m.Title = "Alpha"
|
|
|
|
}
|
|
|
|
if m.Author == "" {
|
|
|
|
m.Author = os.Getenv("USER")
|
|
|
|
}
|
|
|
|
|
2018-10-08 17:38:49 +00:00
|
|
|
m.Palette = s.UI.Canvas.Palette
|
|
|
|
m.Chunker = s.UI.Canvas.Chunker()
|
2018-06-21 01:43:14 +00:00
|
|
|
|
|
|
|
json, err := m.ToJSON()
|
|
|
|
if err != nil {
|
2018-09-26 17:04:46 +00:00
|
|
|
return fmt.Errorf("SaveLevel error: %s", err)
|
2018-06-21 01:43:14 +00:00
|
|
|
}
|
|
|
|
|
2018-10-02 17:11:38 +00:00
|
|
|
// Save it to their profile directory.
|
2018-10-19 20:31:58 +00:00
|
|
|
filename = userdir.LevelPath(filename)
|
2018-10-02 17:11:38 +00:00
|
|
|
log.Info("Write Level: %s", filename)
|
2018-06-21 01:43:14 +00:00
|
|
|
err = ioutil.WriteFile(filename, json, 0644)
|
|
|
|
if err != nil {
|
2018-09-26 17:04:46 +00:00
|
|
|
return fmt.Errorf("Create map file error: %s", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// LoadDoodad loads a doodad from disk.
|
|
|
|
func (s *EditorScene) LoadDoodad(filename string) error {
|
|
|
|
s.filename = filename
|
|
|
|
|
|
|
|
doodad, err := doodads.LoadJSON(filename)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("EditorScene.LoadDoodad(%s): %s", filename, err)
|
2018-06-21 01:43:14 +00:00
|
|
|
}
|
2018-09-26 17:04:46 +00:00
|
|
|
|
|
|
|
s.DrawingType = enum.DoodadDrawing
|
|
|
|
s.Doodad = doodad
|
|
|
|
s.DoodadSize = doodad.Layers[0].Chunker.Size
|
2018-10-08 17:38:49 +00:00
|
|
|
s.UI.Canvas.LoadDoodad(s.Doodad)
|
2018-09-26 17:04:46 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// SaveDoodad saves the doodad to disk.
|
|
|
|
func (s *EditorScene) SaveDoodad(filename string) error {
|
|
|
|
if s.DrawingType != enum.DoodadDrawing {
|
|
|
|
return errors.New("SaveDoodad: current drawing is not a Doodad type")
|
|
|
|
}
|
|
|
|
|
2018-10-19 20:31:58 +00:00
|
|
|
if !strings.HasSuffix(filename, enum.DoodadExt) {
|
|
|
|
filename += enum.DoodadExt
|
2018-10-02 17:11:38 +00:00
|
|
|
}
|
|
|
|
|
2018-09-26 17:04:46 +00:00
|
|
|
s.filename = filename
|
|
|
|
d := s.Doodad
|
|
|
|
if d.Title == "" {
|
|
|
|
d.Title = "Untitled Doodad"
|
|
|
|
}
|
|
|
|
if d.Author == "" {
|
|
|
|
d.Author = os.Getenv("USER")
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: is this copying necessary?
|
2018-10-08 17:38:49 +00:00
|
|
|
d.Palette = s.UI.Canvas.Palette
|
|
|
|
d.Layers[0].Chunker = s.UI.Canvas.Chunker()
|
2018-09-26 17:04:46 +00:00
|
|
|
|
2018-10-02 17:11:38 +00:00
|
|
|
// Save it to their profile directory.
|
2018-10-19 20:31:58 +00:00
|
|
|
filename = userdir.DoodadPath(filename)
|
2018-10-02 17:11:38 +00:00
|
|
|
log.Info("Write Doodad: %s", filename)
|
|
|
|
err := d.WriteJSON(filename)
|
2018-09-26 17:04:46 +00:00
|
|
|
return err
|
2018-06-21 01:43:14 +00:00
|
|
|
}
|
|
|
|
|
2018-07-24 03:10:53 +00:00
|
|
|
// Destroy the scene.
|
|
|
|
func (s *EditorScene) Destroy() error {
|
|
|
|
return nil
|
|
|
|
}
|