319 lines
		
	
	
		
			7.6 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			319 lines
		
	
	
		
			7.6 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package assets
 | |
| 
 | |
| import (
 | |
| 	"bufio"
 | |
| 	_ "image/png"
 | |
| 	"io/fs"
 | |
| 	"log"
 | |
| 	"log/slog"
 | |
| 	"openquell/config"
 | |
| 	"openquell/util"
 | |
| 	"os"
 | |
| 	"path/filepath"
 | |
| 	"sort"
 | |
| 	"strconv"
 | |
| 	"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 {
 | |
| 	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
 | |
| }
 | |
| 
 | |
| const (
 | |
| 	Primary   bool = true
 | |
| 	Secondary bool = false
 | |
| )
 | |
| 
 | |
| func NewTilePlayer(isprimary bool) *Tile {
 | |
| 	tile := &Tile{
 | |
| 		Id:         'S',
 | |
| 		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"]
 | |
| 		tile.Id = 's'
 | |
| 	}
 | |
| 
 | |
| 	// 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{
 | |
| 		Id:         '#',
 | |
| 		Sprite:     Assets[class],
 | |
| 		Class:      class,
 | |
| 		Solid:      true,
 | |
| 		Renderable: true,
 | |
| 	}
 | |
| }
 | |
| 
 | |
| 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,
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // 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
 | |
| 	MinMoves    int
 | |
| }
 | |
| 
 | |
| 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),
 | |
| 		'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"}),
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // 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 _, levelfile := range entries {
 | |
| 		if levelfile.Type().IsRegular() && strings.Contains(levelfile.Name(), ".lvl") {
 | |
| 			path := filepath.Join("assets", dir)
 | |
| 			level := ParseRawLevel(path, levelfile)
 | |
| 
 | |
| 			levels = append(levels, level)
 | |
| 
 | |
| 			slog.Debug("loaded level", "path", path, "file", levelfile)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	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{}
 | |
| 	minmoves := 0
 | |
| 
 | |
| 	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)
 | |
| 			}
 | |
| 		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,
 | |
| 	}
 | |
| }
 |