doodle/pkg/level/types.go
Noah Petherbridge cf1bc81f25 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-07 21:55:10 -08:00

169 lines
4.5 KiB
Go

package level
import (
"archive/zip"
"encoding/json"
"fmt"
"git.kirsle.net/SketchyMaze/doodle/pkg/balance"
"git.kirsle.net/SketchyMaze/doodle/pkg/drawtool"
"git.kirsle.net/SketchyMaze/doodle/pkg/enum"
"git.kirsle.net/SketchyMaze/doodle/pkg/log"
"git.kirsle.net/SketchyMaze/doodle/pkg/native"
"git.kirsle.net/go/render"
)
// Useful variables.
var (
DefaultWallpaper = "notebook.png"
)
// Base provides the common struct keys that are shared between Levels and
// Doodads.
type Base struct {
Version int `json:"version"` // File format version spec.
GameVersion string `json:"gameVersion"` // Game version that created the level.
Title string `json:"title"`
Author string `json:"author"`
Locked bool `json:"locked"`
// v2 level format: zip files with external chunks.
// (v0 was json text, v1 was gzip compressed json text).
// The game must load levels created using the previous
// formats, they will not have a Zipfile and will have
// Chunkers in memory from their (gz) json.
Zipfile *zip.Reader `json:"-"`
// Every drawing type is able to embed other files inside of itself.
Files *FileSystem `json:"files,omitempty"`
}
// Level is the container format for Doodle map drawings.
type Level struct {
Base
Password string `json:"passwd"`
UUID string `json:"uuid"` // unique level IDs, especially for the savegame.json
GameRule GameRule `json:"rules"`
// Chunked pixel data.
Chunker *Chunker `json:"chunks"`
// The Palette holds the unique "colors" used in this map file, and their
// properties (solid, fire, slippery, etc.)
Palette *Palette `json:"palette"`
// Page boundaries and wallpaper settings.
PageType PageType `json:"pageType"`
MaxWidth int64 `json:"boundedWidth"` // only if bounded or bordered
MaxHeight int64 `json:"boundedHeight"`
Wallpaper string `json:"wallpaper"`
// The last scrolled position in the editor.
ScrollPosition render.Point `json:"scroll"`
// Actors keep a list of the doodad instances in this map.
Actors ActorMap `json:"actors"`
// Publishing: attach any custom doodads the map uses on save.
SaveDoodads bool `json:"saveDoodads"`
SaveBuiltins bool `json:"saveBuiltins"`
// Signature for a level with embedded doodads to still play in free mode.
Signature []byte `json:"signature,omitempty"`
// Undo history, temporary live data not persisted to the level file.
UndoHistory *drawtool.History `json:"-"`
}
// GameRule
type GameRule struct {
Difficulty enum.Difficulty `json:"difficulty"`
Survival bool `json:"survival,omitempty"`
}
// New creates a blank level object with all its members initialized.
func New() *Level {
return &Level{
Base: Base{
Version: 1,
Title: "Untitled",
Author: native.DefaultAuthor(),
Files: NewFileSystem(),
},
Chunker: NewChunker(balance.ChunkSize),
Palette: &Palette{},
Actors: ActorMap{},
PageType: NoNegativeSpace,
Wallpaper: DefaultWallpaper,
MaxWidth: 2550,
MaxHeight: 3300,
UndoHistory: drawtool.NewHistory(balance.UndoHistory),
}
}
// Teardown the level when the game is done with it. This frees up SDL2 cached
// texture chunks and reclaims memory in ways the Go garbage collector can not.
func (m *Level) Teardown() {
var (
chunks int
textures int
)
// Free any CACHED chunks' memory.
for chunk := range m.Chunker.IterCachedChunks() {
freed := chunk.Teardown()
chunks++
textures += freed
}
log.Debug("Teardown level (%s): Freed %d textures across %d level chunks", m.Title, textures, chunks)
}
// Pixel associates a coordinate with a palette index.
type Pixel struct {
X int `json:"x"`
Y int `json:"y"`
PaletteIndex int `json:"p"`
// Private runtime values.
Swatch *Swatch `json:"-"` // pointer to its swatch, for when rendered.
}
func (p Pixel) String() string {
return fmt.Sprintf("Pixel<%s '%s' (%d,%d)>", p.Swatch.Color, p.Swatch.Name, p.X, p.Y)
}
// Point returns the pixel's point.
func (p Pixel) Point() render.Point {
return render.Point{
X: p.X,
Y: p.Y,
}
}
// MarshalJSON serializes a Pixel compactly as a simple list.
func (p Pixel) MarshalJSON() ([]byte, error) {
return []byte(fmt.Sprintf(
`[%d, %d, %d]`,
p.X, p.Y, p.PaletteIndex,
)), nil
}
// UnmarshalJSON loads a Pixel from JSON again.
func (p *Pixel) UnmarshalJSON(text []byte) error {
var triplet []int
err := json.Unmarshal(text, &triplet)
if err != nil {
return err
}
p.X = triplet[0]
p.Y = triplet[1]
if len(triplet) > 2 {
p.PaletteIndex = triplet[2]
}
return nil
}