openquell/game/levels.go

217 lines
6.2 KiB
Go

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)
selectors := map[string]ecs.Mask{}
selectors["player"] = filter.All(positionid, velocityid, playerid)
selectors["collectible"] = filter.All(positionid, colid)
components := map[string]ecs.ID{}
components["position"] = positionid
components["velocity"] = velocityid
components["player"] = playerid
components["collectible"] = colid
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{}
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: <tab>: 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())
}
}
}
playerposition.Move(velocity)
}
for _, entity := range toRemove {
// FIXME: or keep them and prepare an animated death
level.World.RemoveEntity(entity)
}
}
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) {
rid := ecs.ComponentID[components.Renderable](level.World)
pid := ecs.ComponentID[components.Position](level.World)
sid := ecs.ComponentID[components.Solid](level.World)
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)
selector := filter.All(rid, pid, sid)
query := level.World.Query(selector)
for query.Next() {
pos := (*components.Position)(query.Get(pid))
sprite := (*components.Renderable)(query.Get(rid))
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 := &ebiten.DrawImageOptions{}
screen.DrawImage(level.Cache, op)
level.UseCache = true
} else {
// use the cached map
op := &ebiten.DrawImageOptions{}
screen.DrawImage(level.Cache, op)
}
// write the movable tiles
selector := filter.All(rid, pid).Without(sid)
query := level.World.Query(&selector)
for query.Next() {
pos := (*components.Position)(query.Get(pid))
sprite := (*components.Renderable)(query.Get(rid))
op := &ebiten.DrawImageOptions{}
op.GeoM.Translate(float64(pos.X), float64(pos.Y))
screen.DrawImage(sprite.Image, 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
}