openquell/assets/loader-levels.go
Thomas von Dein dcf31e46ed fixes+changes:
- fixed windows build, using path, instead of path/filepath
- added switch + door, implemented relation (pair_system missing yet)
- fixed ldtk map loading, now using correct entitymap.png tileset
2024-03-28 19:41:20 +01:00

296 lines
7.8 KiB
Go

package assets
import (
_ "image/png"
"log"
"openquell/config"
"openquell/util"
"strings"
"github.com/hajimehoshi/ebiten/v2"
"github.com/solarlune/ldtkgo"
)
var Project = LoadLDTK("levels")
var Tiles = InitTiles()
// Tile: contains image, identifier (as used in level data) and
// additional properties
type Tile struct {
Id, Ref string
Sprite *ebiten.Image
Class string
Solid bool // wall brick
Player bool // player sphere
IsPrimary bool // primary player sphere
Renderable bool // visible, has sprite
Velocity bool // movable
Collectible bool // collectible, vanishes once collected
Transient bool // turns into brick wall when traversed
Destroyable bool // turns into empty floor when bumped into twice
Particle int // -1=unused, 0-3 = show image of slice
Tiles []*ebiten.Image // has N sprites
TileNames []string // same thing, only the names
Obstacle bool // is an obstacle/enemy
Direction int // obstacle business end shows into this direction
Shader *ebiten.Shader
Alpha *ebiten.Image
Bond bool // denotes an entity which can have a relation to another
Door bool // a door, can be manipulated by a switch
Switch bool // opens|closes a door
}
func (tile *Tile) Clone() *Tile {
newtile := &Tile{
Id: tile.Id,
Ref: tile.Ref,
Sprite: tile.Sprite,
Class: tile.Class,
Solid: tile.Solid,
Player: tile.Player,
IsPrimary: tile.IsPrimary,
Renderable: tile.Renderable,
Velocity: tile.Velocity,
Collectible: tile.Collectible,
Transient: tile.Transient,
Destroyable: tile.Destroyable,
Particle: tile.Particle,
Tiles: tile.Tiles,
TileNames: tile.TileNames,
Obstacle: tile.Obstacle,
Direction: tile.Direction,
Alpha: tile.Alpha,
Shader: tile.Shader,
Bond: tile.Bond,
Door: tile.Door,
Switch: tile.Switch,
}
return newtile
}
const (
Primary bool = true
Secondary bool = false
)
func GetSprites(class []string) []*ebiten.Image {
sprites := []*ebiten.Image{}
for _, sprite := range class {
sprites = append(sprites, Assets[sprite])
}
return sprites
}
func NewTilePlayer(isprimary bool) *Tile {
tile := &Tile{
Class: "sphere",
Renderable: true,
Player: true,
Velocity: true,
IsPrimary: isprimary,
}
switch isprimary {
case Primary:
tile.Sprite = Assets["sphere-blue"]
case Secondary:
tile.Sprite = Assets["sphere-blue-secondary"]
}
// primary sprite is always the first one
tile.Tiles = []*ebiten.Image{Assets["sphere-blue"], Assets["sphere-blue-secondary"]}
return tile
}
func NewTileBlock(class string) *Tile {
return &Tile{
Sprite: Assets[class],
Class: class,
Solid: true,
Renderable: true,
}
}
func NewTileCollectible(class string) *Tile {
return &Tile{
Sprite: Assets[class],
Class: class,
Solid: false,
Renderable: true,
Collectible: true,
}
}
func NewTileObstacle(class string, direction int) *Tile {
return &Tile{
Sprite: Assets[class],
Class: class,
Solid: false,
Renderable: true,
Obstacle: true,
Direction: direction,
Velocity: true,
}
}
func NewTileParticle(class []string) *Tile {
sprites := GetSprites(class)
return &Tile{
Class: "particle",
Solid: false,
Renderable: false,
Particle: 0,
Tiles: sprites,
}
}
func NewTileTranswall(class []string) *Tile {
sprites := GetSprites(class)
return &Tile{
Class: "transwall",
Solid: false,
Renderable: true,
Transient: true,
Tiles: sprites,
Sprite: sprites[0], // initially use the first
TileNames: class,
}
}
func NewTileHiddenDoor(class, alpha string) *Tile {
return &Tile{
Class: "hiddendoor",
Solid: false,
Renderable: true,
Destroyable: true,
Sprite: Assets[class],
Alpha: Assets[alpha],
Shader: Shaders["destruct"],
}
}
func NewTileSwitch(class []string) *Tile {
sprites := GetSprites(class)
return &Tile{
Class: "switch",
Solid: false,
Renderable: true,
Bond: true,
Switch: true,
Tiles: sprites,
Sprite: sprites[0], // initially use the first
}
}
func NewTileDoor(class []string) *Tile {
sprites := GetSprites(class)
return &Tile{
Class: "door",
Solid: false,
Renderable: true,
Bond: true,
Door: true,
Tiles: sprites,
Sprite: sprites[0], // initially use the first
}
}
// used to map level data bytes to actual tiles
type TileRegistry map[string]*Tile
func InitTiles() TileRegistry {
return TileRegistry{
"floor": {Class: "floor", Renderable: false},
"default": NewTileBlock("block-greycolored"),
"solidorange": NewTileBlock("block-orange-32"),
"PlayerPrimary": NewTilePlayer(Primary),
"PlayerSecondary": NewTilePlayer(Secondary),
"Collectible": NewTileCollectible("collectible-orange"),
"ObstacleStar": NewTileObstacle("obstacle-star", config.All),
"ObstacleNorth": NewTileObstacle("obstacle-north", config.North),
"ObstacleSouth": NewTileObstacle("obstacle-south", config.South),
"ObstacleWest": NewTileObstacle("obstacle-west", config.West),
"ObstacleEast": NewTileObstacle("obstacle-east", config.East),
"Particle": NewTileParticle([]string{
"particle-ring-1",
"particle-ring-2",
"particle-ring-3",
"particle-ring-4",
"particle-ring-5",
"particle-ring-6",
}),
"Transient": NewTileTranswall([]string{"transwall", "block-orange-32"}),
"HiddenDoor": NewTileHiddenDoor("block-greycolored", "damage"),
"HiddenDoor2": NewTileHiddenDoor("block-greycolored", "damage"),
"HiddenDoor3": NewTileHiddenDoor("block-greycolored", "damage"),
"HiddenDoor4": NewTileHiddenDoor("block-greycolored", "damage"),
"HiddenDoor5": NewTileHiddenDoor("block-greycolored", "damage"),
"HiddenDoor6": NewTileHiddenDoor("block-greycolored", "damage"),
"HiddenDoor7": NewTileHiddenDoor("block-greycolored", "damage"),
"HiddenDoor8": NewTileHiddenDoor("block-greycolored", "damage"),
"HiddenDoor9": NewTileHiddenDoor("block-greycolored", "damage"),
"HiddenDoor10": NewTileHiddenDoor("block-greycolored", "damage"),
"HiddenDoor11": NewTileHiddenDoor("block-greycolored", "damage"),
"HiddenDoor12": NewTileHiddenDoor("block-greycolored", "damage"),
"HiddenDoor13": NewTileHiddenDoor("block-greycolored", "damage"),
"Switch": NewTileSwitch([]string{"switch-open.png", "switch-closed.png"}),
"Door": NewTileDoor([]string{"door-open.png", "door-closed.png"}),
}
}
// load LDTK project at compile time into ram
func LoadLDTK(dir string) *ldtkgo.Project {
fd, err := assetfs.Open("levels/openquell.ldtk")
if err != nil {
log.Fatalf("failed to open LDTK file levels/openquell.ldtk: %s", err)
}
defer fd.Close()
fileinfo, err := fd.Stat()
if err != nil {
log.Fatalf("failed to stat() LDTK file levels/openquell.ldtk: %s", err)
}
filesize := fileinfo.Size()
buffer := make([]byte, filesize)
_, err = fd.Read(buffer)
if err != nil {
log.Fatalf("failed to read bytes from LDTK file levels/openquell.ldtk: %s", err)
}
project, err := ldtkgo.Read(buffer)
if err != nil {
panic(err)
}
// do some sanity checks
properties := []string{"minmoves", "background", "level", "description"}
need := len(properties)
for idx, level := range project.Levels {
have := 0
for _, property := range level.Properties {
if util.Contains(properties, property.Identifier) {
have++
}
}
if have != need {
log.Fatalf("level definition for level %d (%s) invalid: %d missing properties\n required: %s",
idx, level.Identifier, need-have, strings.Join(properties, ", "),
)
}
}
return project
}