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 ToggleSprite *ebiten.Image 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, ToggleSprite: tile.ToggleSprite, 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{ 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() *Tile { return &Tile{ Solid: true, Renderable: true, } } func NewTileCollectible() *Tile { return &Tile{ Solid: false, Renderable: true, Collectible: true, } } func NewTileObstacle(direction int) *Tile { return &Tile{ Solid: false, Renderable: true, Obstacle: true, Direction: direction, Velocity: true, } } func NewTileParticle(class []string) *Tile { sprites := GetSprites(class) return &Tile{ Solid: false, Renderable: false, Particle: 0, Tiles: sprites, } } func NewTileTranswall() *Tile { return &Tile{ Solid: false, Renderable: true, Transient: true, } } func NewTileHiddenDoor(alpha string) *Tile { return &Tile{ Solid: false, Renderable: true, Destroyable: true, Alpha: Assets[alpha], Shader: Shaders["destruct"], } } func NewTileSwitch() *Tile { return &Tile{ Solid: false, Renderable: true, Bond: true, Switch: true, } } func NewTileDoor() *Tile { return &Tile{ Solid: false, Renderable: true, Bond: true, Door: true, } } // used to map level data bytes to actual tiles type TileRegistry map[string]*Tile func InitTiles() TileRegistry { return TileRegistry{ "floor": {Renderable: false}, "default": NewTileBlock(), "solidorange": NewTileBlock(), "PlayerPrimary": NewTilePlayer(Primary), "PlayerSecondary": NewTilePlayer(Secondary), "Collectible": NewTileCollectible(), "ObstacleStar": NewTileObstacle(config.All), "ObstacleNorth": NewTileObstacle(config.North), "ObstacleSouth": NewTileObstacle(config.South), "ObstacleWest": NewTileObstacle(config.West), "ObstacleEast": NewTileObstacle(config.East), "Particle": NewTileParticle([]string{ "collectible-detonating1", "collectible-detonating2", "collectible-detonating3", "collectible-detonating4", "collectible-detonating5", "collectible-detonating6", "collectible-detonating7", "collectible-detonating8", "collectible-detonating9", "collectible-detonating10", "collectible-detonating11", "collectible-detonating12", "collectible-detonating13", "collectible-detonating14", "collectible-detonating15", }), "Transient": NewTileTranswall(), "HiddenDoor": NewTileHiddenDoor("damage"), "HiddenDoor2": NewTileHiddenDoor("damage"), "HiddenDoor3": NewTileHiddenDoor("damage"), "HiddenDoor4": NewTileHiddenDoor("damage"), "HiddenDoor5": NewTileHiddenDoor("damage"), "HiddenDoor6": NewTileHiddenDoor("damage"), "HiddenDoor7": NewTileHiddenDoor("damage"), "HiddenDoor8": NewTileHiddenDoor("damage"), "HiddenDoor9": NewTileHiddenDoor("damage"), "HiddenDoor10": NewTileHiddenDoor("damage"), "HiddenDoor11": NewTileHiddenDoor("damage"), "HiddenDoor12": NewTileHiddenDoor("damage"), "HiddenDoor13": NewTileHiddenDoor("damage"), "Switch": NewTileSwitch(), "Door": NewTileDoor(), } } // 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 }