package assets import ( "bufio" _ "image/png" "io/fs" "log" "log/slog" "openquell/config" "openquell/util" "os" "path/filepath" "sort" "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 Player bool Renderable bool Velocity bool Collectible bool Particle int // -1=unused, 0-3 = show image of slice Particles []*ebiten.Image Obstacle bool Direction int // obstacles } 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, } } 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, } } 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, Particles: sprites, } } // 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(), '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", }), } } // 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{} 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, } }