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, Ref string 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 Shader *ebiten.Shader Alpha *ebiten.Image Bond bool // denotes an entity which can have a relation to another Door bool // a door, can be manipulated by a switch Switch bool // opens|closes a door } func (tile *Tile) Clone() *Tile { newtile := &Tile{ Id: tile.Id, Ref: tile.Ref, 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, Alpha: tile.Alpha, Shader: tile.Shader, Bond: tile.Bond, Door: tile.Door, Switch: tile.Switch, } return newtile } const ( Primary bool = true Secondary bool = false ) func GetSprites(class []string) []*ebiten.Image { sprites := []*ebiten.Image{} for _, sprite := range class { sprites = append(sprites, Assets[sprite]) } return sprites } func NewTilePlayer(isprimary bool) *Tile { tile := &Tile{ 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"] } // 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{ Sprite: Assets[class], Class: class, Solid: true, Renderable: true, } } func NewTileCollectible(class string) *Tile { return &Tile{ Sprite: Assets[class], Class: class, Solid: false, Renderable: true, Collectible: true, } } func NewTileObstacle(class string, direction int) *Tile { return &Tile{ Sprite: Assets[class], Class: class, Solid: false, Renderable: true, Obstacle: true, Direction: direction, Velocity: true, } } func NewTileParticle(class []string) *Tile { sprites := GetSprites(class) return &Tile{ Class: "particle", Solid: false, Renderable: false, Particle: 0, Tiles: sprites, } } func NewTileTranswall(class []string) *Tile { sprites := GetSprites(class) return &Tile{ Class: "transwall", Solid: false, Renderable: true, Transient: true, Tiles: sprites, Sprite: sprites[0], // initially use the first TileNames: class, } } func NewTileHiddenDoor(class, alpha string) *Tile { return &Tile{ Class: "hiddendoor", Solid: false, Renderable: true, Destroyable: true, Sprite: Assets[class], Alpha: Assets[alpha], Shader: Shaders["destruct"], } } func NewTileSwitch(class []string) *Tile { sprites := GetSprites(class) return &Tile{ Class: "switch", Solid: false, Renderable: true, Bond: true, Switch: true, Tiles: sprites, Sprite: sprites[0], // initially use the first } } func NewTileDoor(class []string) *Tile { sprites := GetSprites(class) return &Tile{ Class: "door", Solid: false, Renderable: true, Bond: true, Door: true, Tiles: sprites, Sprite: sprites[0], // initially use the first } } // used to map level data bytes to actual tiles type TileRegistry map[string]*Tile func InitTiles() TileRegistry { return TileRegistry{ "floor": {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("block-greycolored", "damage"), "HiddenDoor2": NewTileHiddenDoor("block-greycolored", "damage"), "HiddenDoor3": NewTileHiddenDoor("block-greycolored", "damage"), "HiddenDoor4": NewTileHiddenDoor("block-greycolored", "damage"), "HiddenDoor5": NewTileHiddenDoor("block-greycolored", "damage"), "HiddenDoor6": NewTileHiddenDoor("block-greycolored", "damage"), "HiddenDoor7": NewTileHiddenDoor("block-greycolored", "damage"), "HiddenDoor8": NewTileHiddenDoor("block-greycolored", "damage"), "HiddenDoor9": NewTileHiddenDoor("block-greycolored", "damage"), "HiddenDoor10": NewTileHiddenDoor("block-greycolored", "damage"), "HiddenDoor11": NewTileHiddenDoor("block-greycolored", "damage"), "HiddenDoor12": NewTileHiddenDoor("block-greycolored", "damage"), "HiddenDoor13": NewTileHiddenDoor("block-greycolored", "damage"), "Switch": NewTileSwitch([]string{"switch-open.png", "switch-closed.png"}), "Door": NewTileDoor([]string{"door-open.png", "door-closed.png"}), } } // 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 }