package game import ( "image" "log" "log/slog" "openquell/assets" "openquell/components" "openquell/grid" "openquell/observers" "openquell/systems" "openquell/util" "strings" "github.com/hajimehoshi/ebiten/v2" "github.com/mlange-42/arche/ecs" "github.com/solarlune/ldtkgo" ) type Map map[image.Point]*assets.Tile type BackupMap map[image.Point]assets.Tile type Level struct { Cellsize, Width, Height int World *ecs.World Name string Description string Number int Mapslice Map BackupMapslice Map GridContainer *grid.GridContainer Systems []systems.System Grid *grid.Grid } func NewLevel(game *Game, cellsize int, plan *ldtkgo.Level) *Level { systemlist := []systems.System{} gridcontainer := &grid.GridContainer{} bgimage := util.GetBGImage(plan) systemlist = append(systemlist, systems.NewGridSystem(game.World, game.ScreenWidth, game.ScreenHeight, cellsize, bgimage)) systemlist = append(systemlist, systems.NewCollectibleSystem(game.World, cellsize)) systemlist = append(systemlist, systems.NewObstacleSystem(game.World, gridcontainer)) systemlist = append(systemlist, systems.NewPairSystem(game.World, gridcontainer)) systemlist = append(systemlist, systems.NewPlayerSystem(game.World, gridcontainer, game.ScreenWidth, game.ScreenHeight)) systemlist = append(systemlist, systems.NewAnimationSystem(game.World, game.Cellsize)) systemlist = append(systemlist, systems.NewTransientSystem(game.World, gridcontainer)) systemlist = append(systemlist, systems.NewDestroyableSystem(game.World, gridcontainer, game.Cellsize)) systemlist = append(systemlist, systems.NewHudSystem(game.World, plan)) mapslice, backupmap := LevelToSlice(game, plan, cellsize) return &Level{ Mapslice: mapslice, BackupMapslice: backupmap, Cellsize: cellsize, World: game.World, Width: game.ScreenWidth, Height: game.ScreenHeight, Description: plan.PropertyByIdentifier("description").AsString(), Number: plan.PropertyByIdentifier("level").AsInt(), Name: strings.ReplaceAll(plan.Identifier, "_", " "), GridContainer: gridcontainer, Systems: systemlist, } } func (level *Level) Update() { for _, sys := range level.Systems { sys.Update() } } func (level *Level) Draw(screen *ebiten.Image) { for _, sys := range level.Systems { sys.Draw(screen) } } func (level *Level) RestoreMap() { for point, tile := range level.BackupMapslice { level.Mapslice[point] = tile.Clone() } } func (level *Level) SetupGrid(game *Game) { // generic variant does not work here: // selector := generic.NewFilter1[components.Position]() // level.World.Batch().RemoveEntities(selector) // missing argument in conversion to generic.Filter1[components.Position] // erase all entities of previous level, if any posID := ecs.ComponentID[components.Position](level.World) selector := ecs.All(posID) level.World.Batch().RemoveEntities(selector) // get rid of any players on PlayerObserver. FIXME: remove them in grid.NewGrid()? observer := observers.GetGameObserver(level.World) observer.RemoveEntities() // get rid of possibly manipulated map level.RestoreMap() // setup world slog.Debug("new grid?") level.GridContainer.SetGrid( grid.NewGrid(game.World, level.Cellsize, level.Width, level.Height, level.Mapslice)) } func NewMapSlice(game *Game, tilesize int) Map { size := game.ScreenWidth * game.ScreenHeight mapslice := make(Map, size) for y := 0; y < game.ScreenHeight; y += 32 { for x := 0; x < game.ScreenWidth; x += 32 { mapslice[image.Point{x / tilesize, y / tilesize}] = assets.Tiles["floor"] } } return mapslice } // parses a RawLevel and generates a mapslice from it, which is being used as grid func LevelToSlice(game *Game, level *ldtkgo.Level, tilesize int) (Map, Map) { mapslice := NewMapSlice(game, tilesize) backupmap := NewMapSlice(game, tilesize) for _, layer := range level.Layers { switch layer.Type { case ldtkgo.LayerTypeTile: // load tile from LDTK tile layer, use sprites from associated map. if tiles := layer.AllTiles(); len(tiles) > 0 { for _, tileData := range tiles { // Subimage the Tile from the already loaded map, // but referenced from LDTK file, that way we // could use multiple tileset images tile := assets.Tiles["default"] // FIXME: load from LDTK file tile.Sprite = assets.Assets["primarymap"].SubImage( image.Rect(tileData.Src[0], tileData.Src[1], tileData.Src[0]+layer.GridSize, tileData.Src[1]+layer.GridSize)).(*ebiten.Image) point := image.Point{ tileData.Position[0] / tilesize, tileData.Position[1] / tilesize} mapslice[point] = tile backupmap[point] = tile.Clone() } } case ldtkgo.LayerTypeEntity: // load mobile tiles (they call them entities) using static map map.png. tileset := assets.Assets["entitymap"] for _, entity := range layer.Entities { if entity.TileRect != nil { tile := assets.Tiles[entity.Identifier] tile.Id = entity.IID toggleRect := util.GetPropertyToggleTile(entity) if toggleRect != nil { tile.ToggleSprite = tileset.SubImage( image.Rect(toggleRect.X, toggleRect.Y, toggleRect.X+toggleRect.W, toggleRect.Y+toggleRect.H)).(*ebiten.Image) } tile.Ref = util.GetPropertyRef(entity) if tile.Transient { slog.Debug("LOAD TILE", "tileref", tile.Ref, "tileid", tile.Id, "name", entity.Identifier, "isswitch", tile.Switch, "isdoor", tile.Door, "togglerect", toggleRect, "tilerect", entity.TileRect, ) } tileRect := entity.TileRect animationtrigger := util.GetPropertyString(entity, "AnimationTrigger") slog.Debug("got trigger", "trigger", animationtrigger) //animateondestruct := util.GetPropertyBool(entity, "AnimateOnDestruct") // FIXME: also check for AnimationLoop and other animation reasons // if animateondestruct { if animationtrigger != "" { tile.AnimateOnDestruct = true animation := util.GetPropertyString(entity, "AnimateSpriteSheet") if animation != "" { if !util.Exists(assets.Animations, animation) { log.Fatalf("entity %s refers to non existent animation set %s", entity.Identifier, animation) } } tile.AnimationSpriteSheet = assets.Animations[animation] tile.AnimationTrigger = animationtrigger } tile.Sprite = tileset.SubImage( image.Rect(tileRect.X, tileRect.Y, tileRect.X+tileRect.W, tileRect.Y+tileRect.H)).(*ebiten.Image) point := image.Point{ entity.Position[0] / tilesize, entity.Position[1] / tilesize} mapslice[point] = tile backupmap[point] = tile.Clone() } } } } return mapslice, backupmap }