package game import ( "fmt" "image" "image/draw" "log" "openquell/assets" "openquell/components" . "openquell/config" "strings" "github.com/hajimehoshi/ebiten/v2" "github.com/mlange-42/arche/ecs" "github.com/mlange-42/arche/filter" ) type Level struct { Grid *Grid Cellsize, Width, Height int World *ecs.World Name string Description string Background *ebiten.Image Mapslice map[image.Point]*assets.Tile UseCache bool Cache *ebiten.Image Selector map[string]ecs.Mask Component map[string]ecs.ID } func NewLevel(game *Game, cellsize int, plan *assets.RawLevel) *Level { cache := ebiten.NewImage(game.ScreenWidth, game.ScreenHeight) positionid := ecs.ComponentID[components.Position](game.World) velocityid := ecs.ComponentID[components.Velocity](game.World) playerid := ecs.ComponentID[components.Player](game.World) colid := ecs.ComponentID[components.Collectible](game.World) ptid := ecs.ComponentID[components.Particle](game.World) sid := ecs.ComponentID[components.Solid](game.World) renderid := ecs.ComponentID[components.Renderable](game.World) selectors := map[string]ecs.Mask{} selectors["player"] = filter.All(positionid, velocityid, playerid) selectors["tile"] = filter.All(renderid, positionid, sid) selectors["movable"] = filter.All(renderid, positionid) selectors["collectible"] = filter.All(positionid, colid) selectors["particle"] = filter.All(positionid, ptid) components := map[string]ecs.ID{} components["position"] = positionid components["velocity"] = velocityid components["player"] = playerid components["collectible"] = colid components["particle"] = ptid components["solid"] = sid components["renderable"] = renderid return &Level{ Mapslice: LevelToSlice(game, plan, cellsize), Cellsize: cellsize, World: game.World, Width: game.ScreenWidth, Height: game.ScreenHeight, Description: plan.Description, Background: plan.Background, UseCache: false, Cache: cache, Selector: selectors, Component: components, } } func (level *Level) Update() { query := level.World.Query(level.Selector["player"]) toRemove := []ecs.Entity{} particle_pos := image.Point{} for query.Next() { playerposition := (*components.Position)(query.Get(level.Component["position"])) velocity := (*components.Velocity)(query.Get(level.Component["velocity"])) if !velocity.Moving() { switch { case ebiten.IsKeyPressed(ebiten.KeyRight): velocity.Change(East) case ebiten.IsKeyPressed(ebiten.KeyLeft): velocity.Change(West) case ebiten.IsKeyPressed(ebiten.KeyDown): velocity.Change(South) case ebiten.IsKeyPressed(ebiten.KeyUp): velocity.Change(North) // other keys: : switch player, etc } } if velocity.Moving() { ok, newpos := level.Grid.BumpEdge(playerposition, velocity) if ok { fmt.Printf("falling off the edge, new pos: %v\n", newpos) playerposition.Set(newpos) } else { ok, tilepos := level.Grid.GetSolidNeighborPosition(playerposition, velocity, true) if ok { intersects, newpos := tilepos.Intersects(playerposition, velocity) if intersects { fmt.Printf("collision detected. tile: %s\n", tilepos) fmt.Printf(" player: %s\n", playerposition) fmt.Printf(" new: %s\n", newpos) playerposition.Set(newpos) fmt.Printf(" player new: %s\n", playerposition) velocity.Change(Stop) } } } colquery := level.World.Query(level.Selector["collectible"]) for colquery.Next() { collectible := (*components.Collectible)(colquery.Get(level.Component["collectible"])) colposition := (*components.Position)(colquery.Get(level.Component["position"])) ok, _ := playerposition.Intersects(colposition, velocity) if ok { fmt.Printf("bumped into collectible %v\n", collectible) toRemove = append(toRemove, colquery.Entity()) particle_pos.X = colposition.X particle_pos.Y = colposition.Y } } } playerposition.Move(velocity) } // remove collectible if collected for _, entity := range toRemove { // FIXME: or keep them and prepare an animated death level.World.RemoveEntity(entity) } // display debris after collecting ptquery := level.World.Query(level.Selector["particle"]) for ptquery.Next() { // we loop, but it's only one anyway particle := (*components.Particle)(ptquery.Get(level.Component["particle"])) colposition := (*components.Position)(ptquery.Get(level.Component["position"])) if len(toRemove) > 0 { // particle appears colposition.Update( particle_pos.X-(level.Cellsize/2), particle_pos.Y-(level.Cellsize/2), 64, ) particle.Index = 0 // start displaying the particle } else { switch { case particle.Index > -1 && particle.Index < len(particle.Particles)-1: particle.Index++ default: // last sprite reached, remove it particle.Index = -1 } } } } func (level *Level) Position2Point(position *components.Position) image.Point { return image.Point{ int(position.X) / level.Cellsize, int(position.Y) / level.Cellsize, } } // return the tile the moving object would end up on if indeed moving func (level *Level) GetTile( position *components.Position, velocity *components.Velocity) *assets.Tile { newpoint := image.Point{ int(position.X+velocity.Data.X) / level.Cellsize, int(position.Y+velocity.Data.Y) / level.Cellsize, } tile := level.Mapslice[newpoint] return tile } func (level *Level) Draw(screen *ebiten.Image) { level.DrawTiles(screen) level.DrawMovables(screen) level.DrawParticles(screen) } func (level *Level) DrawTiles(screen *ebiten.Image) { op := &ebiten.DrawImageOptions{} if !level.UseCache { // map not cached or cacheable, write it to the cache draw.Draw(level.Cache, level.Background.Bounds(), level.Background, image.ZP, draw.Src) query := level.World.Query(level.Selector["tile"]) for query.Next() { pos := (*components.Position)(query.Get(level.Component["position"])) sprite := (*components.Renderable)(query.Get(level.Component["renderable"])) draw.Draw( level.Cache, image.Rect(pos.X, pos.Y, pos.X+pos.Cellsize, pos.Y+pos.Cellsize), sprite.Image, image.ZP, draw.Over) } op.GeoM.Reset() screen.DrawImage(level.Cache, op) level.UseCache = true } else { // use the cached map op.GeoM.Reset() screen.DrawImage(level.Cache, op) } } func (level *Level) DrawMovables(screen *ebiten.Image) { // write the movable tiles op := &ebiten.DrawImageOptions{} selector := level.Selector["movable"].Without(level.Component["solid"]) query := level.World.Query(&selector) for query.Next() { pos := (*components.Position)(query.Get(level.Component["position"])) sprite := (*components.Renderable)(query.Get(level.Component["renderable"])) op.GeoM.Reset() op.GeoM.Translate(float64(pos.X), float64(pos.Y)) screen.DrawImage(sprite.Image, op) } } func (level *Level) DrawParticles(screen *ebiten.Image) { // write particles (these are no tiles!) op := &ebiten.DrawImageOptions{} query := level.World.Query(level.Selector["particle"]) for query.Next() { pos := (*components.Position)(query.Get(level.Component["position"])) particle := (*components.Particle)(query.Get(level.Component["particle"])) if particle.Index > -1 { op.GeoM.Reset() op.GeoM.Translate(float64(pos.X), float64(pos.Y)) screen.DrawImage(particle.Particles[particle.Index], op) } } } func (level *Level) SetupGrid(game *Game) { level.Grid = NewGrid(game, level.Cellsize, level.Mapslice) } func LevelToSlice(game *Game, level *assets.RawLevel, tilesize int) map[image.Point]*assets.Tile { size := game.ScreenWidth * game.ScreenHeight mapslice := make(map[image.Point]*assets.Tile, size) for y, line := range strings.Split(string(level.Data), "\n") { if len(line) != game.ScreenWidth/tilesize && y < game.ScreenHeight/tilesize { log.Fatalf("line %d doesn't contain %d tiles, but %d", y, game.ScreenWidth/tilesize, len(line)) } for x, char := range line { mapslice[image.Point{x, y}] = assets.Tiles[byte(char)] } } return mapslice }