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 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[string]*Tile func InitTiles() TileRegistry { return TileRegistry{ "floor": {Id: ' ', 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([]string{"block-greycolored", "block-greycolored-damaged"}), } } // 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 }