openquell/assets/loader-levels.go

322 lines
7.6 KiB
Go
Raw Normal View History

2024-02-06 15:26:20 +01:00
package assets
import (
"bufio"
_ "image/png"
"io/fs"
"log"
2024-02-13 18:42:13 +01:00
"log/slog"
"openquell/config"
2024-02-06 15:26:20 +01:00
"openquell/util"
"os"
"path/filepath"
"sort"
"strconv"
2024-02-06 15:26:20 +01:00
"strings"
"github.com/hajimehoshi/ebiten/v2"
)
var Levels = LoadLevels("levels")
var Tiles = InitTiles()
// Tile: contains image, identifier (as used in level data) and
// additional properties
type Tile struct {
2024-02-07 18:01:58 +01:00
Id byte
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
}
func (tile *Tile) Clone() *Tile {
newtile := &Tile{
Id: tile.Id,
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,
}
return newtile
2024-02-06 15:26:20 +01:00
}
const (
Primary bool = true
Secondary bool = false
)
func NewTilePlayer(isprimary bool) *Tile {
tile := &Tile{
2024-02-06 15:26:20 +01:00
Id: 'S',
Class: "sphere",
Renderable: true,
Player: true,
Velocity: true,
IsPrimary: isprimary,
2024-02-06 15:26:20 +01:00
}
switch isprimary {
case Primary:
tile.Sprite = Assets["sphere-blue"]
case Secondary:
tile.Sprite = Assets["sphere-blue-secondary"]
tile.Id = 's'
}
// primary sprite is always the first one
tile.Tiles = []*ebiten.Image{Assets["sphere-blue"], Assets["sphere-blue-secondary"]}
return tile
2024-02-06 15:26:20 +01:00
}
func NewTileBlock(class string) *Tile {
return &Tile{
Id: '#',
Sprite: Assets[class],
Class: class,
Solid: true,
Renderable: true,
}
}
2024-02-07 18:01:58 +01:00
func NewTileCollectible(class string) *Tile {
return &Tile{
Id: 'o',
Sprite: Assets[class],
Class: class,
Solid: false,
Renderable: true,
Collectible: true,
}
}
func NewTileObstacle(class string, direction int) *Tile {
return &Tile{
Id: '+',
Sprite: Assets[class],
Class: class,
Solid: false,
Renderable: true,
Obstacle: true,
Direction: direction,
Velocity: true,
}
}
func NewTileParticle(class []string) *Tile {
sprites := []*ebiten.Image{}
for _, sprite := range class {
sprites = append(sprites, Assets[sprite])
}
return &Tile{
Id: '*',
Class: "particle",
Solid: false,
Renderable: false,
Particle: 0,
Tiles: sprites,
}
}
func NewTileTranswall(class []string) *Tile {
sprites := []*ebiten.Image{}
names := []string{}
for _, sprite := range class {
sprites = append(sprites, Assets[sprite])
names = append(names, sprite)
}
return &Tile{
Id: 't',
Class: "transwall",
Solid: false,
Renderable: true,
Transient: true,
Tiles: sprites,
Sprite: sprites[0], // initially use the first
TileNames: names,
}
}
func NewTileHiddenDoor(class []string) *Tile {
sprites := []*ebiten.Image{}
names := []string{}
for _, sprite := range class {
sprites = append(sprites, Assets[sprite])
names = append(names, sprite)
}
return &Tile{
Id: 'W',
Class: "hiddendoor",
Solid: false,
Renderable: true,
Destroyable: true,
Tiles: sprites,
Sprite: sprites[0], // initially use the first
TileNames: names,
}
}
2024-02-06 15:26:20 +01:00
// used to map level data bytes to actual tiles
type TileRegistry map[byte]*Tile
// holds a raw level spec:
//
// Name: the name of the level file w/o the .lvl extension
// Background: an image name used as game background for this level
// Description: text to display on top
// Data: a level spec consisting of chars of the above mapping and spaces, e.g.:
// ####
// # #
// ####
//
// Each level data must be 20 chars wide (= 640 px width) and 15 chars
// high (=480 px height).
type RawLevel struct {
Number int
2024-02-06 15:26:20 +01:00
Name string
Description string
Background *ebiten.Image
Data []byte
MinMoves int
2024-02-06 15:26:20 +01:00
}
func InitTiles() TileRegistry {
return TileRegistry{
' ': {Id: ' ', Class: "floor", Renderable: false},
//'#': NewTileBlock("block-grey32"),
'#': NewTileBlock("block-greycolored"),
'B': NewTileBlock("block-orange-32"),
'S': NewTilePlayer(Primary),
's': NewTilePlayer(Secondary),
2024-02-07 18:01:58 +01:00
'o': NewTileCollectible("collectible-orange"),
'+': NewTileObstacle("obstacle-star", config.All),
'^': NewTileObstacle("obstacle-north", config.North),
'v': NewTileObstacle("obstacle-south", config.South),
'<': NewTileObstacle("obstacle-west", config.West),
'>': NewTileObstacle("obstacle-east", config.East),
'*': NewTileParticle([]string{
//"particle-ring-1",
"particle-ring-2",
"particle-ring-3",
"particle-ring-4",
"particle-ring-5",
"particle-ring-6",
}),
't': NewTileTranswall([]string{"transwall", "block-orange-32"}),
'W': NewTileHiddenDoor([]string{"block-greycolored", "block-greycolored-damaged"}),
2024-02-06 15:26:20 +01:00
}
}
// load levels at compile time into ram, creates a slice of raw levels
func LoadLevels(dir string) []RawLevel {
levels := []RawLevel{}
// we use embed.FS to iterate over all files in ./levels/
entries, err := assetfs.ReadDir(dir)
if err != nil {
log.Fatalf("failed to read level dir %s: %s", dir, err)
}
sort.Slice(entries, func(i, j int) bool {
return entries[i].Name() < entries[j].Name()
})
for idx, levelfile := range entries {
2024-02-06 15:26:20 +01:00
if levelfile.Type().IsRegular() && strings.Contains(levelfile.Name(), ".lvl") {
path := filepath.Join("assets", dir)
level := ParseRawLevel(path, levelfile)
level.Number = idx
2024-02-13 18:42:13 +01:00
2024-02-06 15:26:20 +01:00
levels = append(levels, level)
2024-02-13 18:42:13 +01:00
slog.Debug("loaded level", "path", path, "file", levelfile)
2024-02-06 15:26:20 +01:00
}
}
return levels
}
func ParseRawLevel(dir string, levelfile fs.DirEntry) RawLevel {
fd, err := os.Open(filepath.Join(dir, levelfile.Name()))
if err != nil {
log.Fatalf("failed to read level file %s: %s", levelfile.Name(), err)
}
defer fd.Close()
name := strings.TrimSuffix(levelfile.Name(), ".lvl")
name = name[3:]
2024-02-06 15:26:20 +01:00
des := ""
background := &ebiten.Image{}
data := []byte{}
minmoves := 0
2024-02-06 15:26:20 +01:00
scanner := bufio.NewScanner(fd)
for scanner.Scan() {
// ignore any whitespace
line := scanner.Text()
// ignore empty lines
if len(line) == 0 {
continue
}
switch {
case strings.Contains(line, "Background:"):
haveit := strings.Split(line, ": ")
if util.Exists(Assets, haveit[1]) {
background = Assets[haveit[1]]
}
case strings.Contains(line, "Description:"):
haveit := strings.Split(line, ": ")
des = haveit[1]
case strings.Contains(line, "MinMoves:"):
haveit := strings.Split(line, ": ")
minmoves, err = strconv.Atoi(haveit[1])
if err != nil {
log.Fatal("Failed to convert MinMoves to int: %w", err)
}
2024-02-06 15:26:20 +01:00
default:
// all other non-empty and non-equalsign lines are
// level definition matrix data, merge thes into data
data = append(data, line+"\n"...)
}
}
return RawLevel{
Name: name,
Data: data,
Background: background,
Description: des,
MinMoves: minmoves,
2024-02-06 15:26:20 +01:00
}
}