doodle/pkg/editor_scene.go
Noah Petherbridge 258b2eb285 Script Timers, Multiple Doodad Frames
* CLI: fix the `doodad convert` command to share the same Palette when
  converting each frame (layer) of a doodad so subsequent layers find
  the correct color swatches for serialization.
* Scripting: add timers and intervals to Doodad scripts to allow them to
  animate themselves or add delayed callbacks. The timers have the same
  API as a web browser: setTimeout(), setInterval(), clearTimeout(),
  clearInterval().
* Add support for uix.Actor to change its currently rendered layer in
  the level. For example a Button Doodad can set its image to Layer 1
  (pressed) when touched by the player, and Trapdoors can cycle through
  their layers to animate opening and closing.
  * Usage from a Doodad script: Self.ShowLayer(1)
* Default Doodads: added scripts for all Buttons, Doors, Keys and the
  Trapdoor to run their various animations when touched (in the case of
  Keys, destroy themselves when touched, because there is no player
  inventory yet)
2019-04-18 18:15:05 -07:00

282 lines
6.7 KiB
Go

package doodle
import (
"errors"
"fmt"
"io/ioutil"
"os"
"strings"
"git.kirsle.net/apps/doodle/lib/events"
"git.kirsle.net/apps/doodle/lib/render"
"git.kirsle.net/apps/doodle/pkg/doodads"
"git.kirsle.net/apps/doodle/pkg/enum"
"git.kirsle.net/apps/doodle/pkg/level"
"git.kirsle.net/apps/doodle/pkg/log"
"git.kirsle.net/apps/doodle/pkg/userdir"
)
// EditorScene manages the "Edit Level" game mode.
type EditorScene struct {
// Configuration for the scene initializer.
DrawingType enum.DrawingType
OpenFile bool
Filename string
DoodadSize int
UI *EditorUI
d *Doodle
// The current level or doodad object being edited, based on the
// DrawingType.
Level *level.Level
Doodad *doodads.Doodad
// Custom debug overlay values.
debTool *string
debSwatch *string
debWorldIndex *string
// Last saved filename by the user.
filename string
}
// Name of the scene.
func (s *EditorScene) Name() string {
return "Edit"
}
// Setup the editor scene.
func (s *EditorScene) Setup(d *Doodle) error {
// Debug overlay values.
s.debTool = new(string)
s.debSwatch = new(string)
s.debWorldIndex = new(string)
customDebugLabels = []debugLabel{
{"Pixel", s.debWorldIndex},
{"Tool", s.debTool},
{"Swatch", s.debSwatch},
}
// Initialize the user interface. It references the palette and such so it
// must be initialized after those things.
s.d = d
s.UI = NewEditorUI(d, s)
// Were we given configuration data?
if s.Filename != "" {
log.Debug("EditorScene.Setup: Set filename to %s", s.Filename)
s.filename = s.Filename
s.Filename = ""
}
// 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")
s.UI.Canvas.LoadLevel(d.Engine, s.Level)
s.UI.Canvas.InstallActors(s.Level.Actors)
} 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)
} else {
s.UI.Canvas.InstallActors(s.Level.Actors)
}
}
// No level?
if s.Level == nil {
log.Debug("EditorScene.Setup: initializing a new Level")
s.Level = level.New()
s.Level.Palette = level.DefaultPalette()
s.UI.Canvas.LoadLevel(d.Engine, s.Level)
s.UI.Canvas.ScrollTo(render.Origin)
s.UI.Canvas.Scrollable = true
}
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)
s.UI.Canvas.LoadDoodad(s.Doodad)
}
// TODO: move inside the UI. Just an approximate position for now.
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)
}
d.Flash("Editor Mode. Press 'P' to play this map.")
return nil
}
// Loop the editor scene.
func (s *EditorScene) Loop(d *Doodle, ev *events.State) error {
// Update debug overlay values.
*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()
// 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
}
}
s.UI.Loop(ev)
// Switching to Play Mode?
if ev.KeyName.Read() == "p" {
log.Info("Play Mode, Go!")
d.Goto(&PlayScene{
Filename: s.filename,
Level: s.Level,
})
return nil
}
return nil
}
// Draw the current frame.
func (s *EditorScene) Draw(d *Doodle) error {
// Clear the canvas and fill it with magenta so it's clear if any spots are missed.
d.Engine.Clear(render.Magenta)
s.UI.Present(d.Engine)
return nil
}
// LoadLevel loads a level from disk.
func (s *EditorScene) LoadLevel(filename string) error {
s.filename = filename
level, err := level.LoadJSON(filename)
fmt.Printf("%+v\n", level)
if err != nil {
return fmt.Errorf("EditorScene.LoadLevel(%s): %s", filename, err)
}
s.DrawingType = enum.LevelDrawing
s.Level = level
s.UI.Canvas.LoadLevel(s.d.Engine, s.Level)
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)
}
return nil
}
// SaveLevel saves the level to disk.
// TODO: move this into the Canvas?
func (s *EditorScene) SaveLevel(filename string) error {
if s.DrawingType != enum.LevelDrawing {
return errors.New("SaveLevel: current drawing is not a Level type")
}
if !strings.HasSuffix(filename, enum.LevelExt) {
filename += enum.LevelExt
}
s.filename = filename
m := s.Level
if m.Title == "" {
m.Title = "Alpha"
}
if m.Author == "" {
m.Author = os.Getenv("USER")
}
m.Palette = s.UI.Canvas.Palette
m.Chunker = s.UI.Canvas.Chunker()
json, err := m.ToJSON()
if err != nil {
return fmt.Errorf("SaveLevel error: %s", err)
}
// Save it to their profile directory.
filename = userdir.LevelPath(filename)
log.Info("Write Level: %s", filename)
err = ioutil.WriteFile(filename, json, 0644)
if err != nil {
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)
}
s.DrawingType = enum.DoodadDrawing
s.Doodad = doodad
s.DoodadSize = doodad.Layers[0].Chunker.Size
s.UI.Canvas.LoadDoodad(s.Doodad)
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")
}
if !strings.HasSuffix(filename, enum.DoodadExt) {
filename += enum.DoodadExt
}
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?
d.Palette = s.UI.Canvas.Palette
d.Layers[0].Chunker = s.UI.Canvas.Chunker()
// Save it to their profile directory.
filename = userdir.DoodadPath(filename)
log.Info("Write Doodad: %s", filename)
err := d.WriteJSON(filename)
return err
}
// Destroy the scene.
func (s *EditorScene) Destroy() error {
return nil
}