2024-02-06 15:26:20 +01:00
|
|
|
package assets
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bufio"
|
|
|
|
|
"fmt"
|
|
|
|
|
_ "image/png"
|
|
|
|
|
"io/fs"
|
|
|
|
|
"log"
|
|
|
|
|
"openquell/util"
|
|
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"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
|
|
|
|
|
Player bool
|
|
|
|
|
Renderable bool
|
|
|
|
|
Velocity bool
|
|
|
|
|
Collectible bool
|
2024-02-08 18:33:59 +01:00
|
|
|
Particle int // -1=unused, 0-3 = show image of slice
|
|
|
|
|
Particles []*ebiten.Image
|
2024-02-06 15:26:20 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func NewTilePlayer() *Tile {
|
|
|
|
|
return &Tile{
|
|
|
|
|
Id: 'S',
|
|
|
|
|
Sprite: Assets["sphere-blue"],
|
|
|
|
|
Class: "sphere",
|
|
|
|
|
Renderable: true,
|
|
|
|
|
Player: true,
|
|
|
|
|
Velocity: true,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-08 18:33:59 +01:00
|
|
|
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,
|
2024-02-11 13:00:56 +01:00
|
|
|
Particle: 0,
|
2024-02-08 18:33:59 +01:00
|
|
|
Particles: sprites,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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 {
|
|
|
|
|
Name string
|
|
|
|
|
Description string
|
|
|
|
|
Background *ebiten.Image
|
|
|
|
|
Data []byte
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func InitTiles() TileRegistry {
|
|
|
|
|
return TileRegistry{
|
|
|
|
|
' ': {Id: ' ', Class: "floor", Renderable: false},
|
|
|
|
|
'#': NewTileBlock("block-grey32"),
|
|
|
|
|
'S': NewTilePlayer(),
|
2024-02-07 18:01:58 +01:00
|
|
|
'o': NewTileCollectible("collectible-orange"),
|
2024-02-08 18:33:59 +01:00
|
|
|
'*': NewTileParticle([]string{
|
2024-02-12 17:27:52 +01:00
|
|
|
"particle-ring-1", "particle-ring-2", "particle-ring-3", "particle-ring-4", "particle-ring-5", "particle-ring-6",
|
2024-02-08 18:33:59 +01:00
|
|
|
}),
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, levelfile := range entries {
|
|
|
|
|
if levelfile.Type().IsRegular() && strings.Contains(levelfile.Name(), ".lvl") {
|
|
|
|
|
path := filepath.Join("assets", dir)
|
|
|
|
|
fmt.Printf("LOADING level %s/%s ... ", path, levelfile)
|
|
|
|
|
level := ParseRawLevel(path, levelfile)
|
|
|
|
|
fmt.Printf("done\n")
|
|
|
|
|
levels = append(levels, level)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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")
|
|
|
|
|
des := ""
|
|
|
|
|
background := &ebiten.Image{}
|
|
|
|
|
data := []byte{}
|
|
|
|
|
|
|
|
|
|
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]
|
|
|
|
|
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,
|
|
|
|
|
}
|
|
|
|
|
}
|