2024-02-06 15:26:20 +01:00
|
|
|
package game
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"fmt"
|
|
|
|
|
"image"
|
2024-02-07 18:01:58 +01:00
|
|
|
"image/draw"
|
2024-02-06 15:26:20 +01:00
|
|
|
"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
|
2024-02-07 18:01:58 +01:00
|
|
|
UseCache bool
|
|
|
|
|
Cache *ebiten.Image
|
|
|
|
|
Selector map[string]ecs.Mask
|
|
|
|
|
Component map[string]ecs.ID
|
2024-02-06 15:26:20 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func NewLevel(game *Game, cellsize int, plan *assets.RawLevel) *Level {
|
2024-02-07 18:01:58 +01:00
|
|
|
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)
|
2024-02-08 18:33:59 +01:00
|
|
|
ptid := ecs.ComponentID[components.Particle](game.World)
|
|
|
|
|
sid := ecs.ComponentID[components.Solid](game.World)
|
|
|
|
|
renderid := ecs.ComponentID[components.Renderable](game.World)
|
2024-02-07 18:01:58 +01:00
|
|
|
|
|
|
|
|
selectors := map[string]ecs.Mask{}
|
|
|
|
|
selectors["player"] = filter.All(positionid, velocityid, playerid)
|
2024-02-08 18:33:59 +01:00
|
|
|
selectors["tile"] = filter.All(renderid, positionid, sid)
|
|
|
|
|
selectors["movable"] = filter.All(renderid, positionid)
|
2024-02-07 18:01:58 +01:00
|
|
|
selectors["collectible"] = filter.All(positionid, colid)
|
2024-02-08 18:33:59 +01:00
|
|
|
selectors["particle"] = filter.All(positionid, ptid)
|
2024-02-07 18:01:58 +01:00
|
|
|
|
|
|
|
|
components := map[string]ecs.ID{}
|
|
|
|
|
components["position"] = positionid
|
|
|
|
|
components["velocity"] = velocityid
|
|
|
|
|
components["player"] = playerid
|
|
|
|
|
components["collectible"] = colid
|
2024-02-08 18:33:59 +01:00
|
|
|
components["particle"] = ptid
|
|
|
|
|
components["solid"] = sid
|
|
|
|
|
components["renderable"] = renderid
|
2024-02-07 18:01:58 +01:00
|
|
|
|
2024-02-06 15:26:20 +01:00
|
|
|
return &Level{
|
|
|
|
|
Mapslice: LevelToSlice(game, plan, cellsize),
|
|
|
|
|
Cellsize: cellsize,
|
|
|
|
|
World: game.World,
|
|
|
|
|
Width: game.ScreenWidth,
|
|
|
|
|
Height: game.ScreenHeight,
|
|
|
|
|
Description: plan.Description,
|
|
|
|
|
Background: plan.Background,
|
2024-02-07 18:01:58 +01:00
|
|
|
UseCache: false,
|
|
|
|
|
Cache: cache,
|
|
|
|
|
Selector: selectors,
|
|
|
|
|
Component: components,
|
2024-02-06 15:26:20 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (level *Level) Update() {
|
2024-02-07 18:01:58 +01:00
|
|
|
query := level.World.Query(level.Selector["player"])
|
2024-02-06 15:26:20 +01:00
|
|
|
|
2024-02-08 12:43:37 +01:00
|
|
|
toRemove := []ecs.Entity{}
|
2024-02-08 18:33:59 +01:00
|
|
|
particle_pos := image.Point{}
|
2024-02-08 12:43:37 +01:00
|
|
|
|
2024-02-06 15:26:20 +01:00
|
|
|
for query.Next() {
|
2024-02-07 18:01:58 +01:00
|
|
|
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
|
|
|
|
|
}
|
2024-02-06 15:26:20 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if velocity.Moving() {
|
2024-02-06 19:02:25 +01:00
|
|
|
ok, newpos := level.Grid.BumpEdge(playerposition, velocity)
|
2024-02-06 15:26:20 +01:00
|
|
|
if ok {
|
2024-02-06 19:02:25 +01:00
|
|
|
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)
|
|
|
|
|
}
|
2024-02-06 15:26:20 +01:00
|
|
|
}
|
|
|
|
|
}
|
2024-02-07 18:01:58 +01:00
|
|
|
|
2024-02-07 23:17:51 +01:00
|
|
|
colquery := level.World.Query(level.Selector["collectible"])
|
2024-02-07 18:01:58 +01:00
|
|
|
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)
|
2024-02-08 12:43:37 +01:00
|
|
|
toRemove = append(toRemove, colquery.Entity())
|
2024-02-08 18:33:59 +01:00
|
|
|
particle_pos.X = colposition.X
|
|
|
|
|
particle_pos.Y = colposition.Y
|
2024-02-07 18:01:58 +01:00
|
|
|
}
|
|
|
|
|
}
|
2024-02-06 15:26:20 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
playerposition.Move(velocity)
|
|
|
|
|
}
|
2024-02-08 12:43:37 +01:00
|
|
|
|
2024-02-08 18:33:59 +01:00
|
|
|
// remove collectible if collected
|
2024-02-08 12:43:37 +01:00
|
|
|
for _, entity := range toRemove {
|
|
|
|
|
// FIXME: or keep them and prepare an animated death
|
|
|
|
|
level.World.RemoveEntity(entity)
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-08 18:33:59 +01:00
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-06 15:26:20 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
2024-02-08 18:33:59 +01:00
|
|
|
func (level *Level) GetTile(
|
|
|
|
|
position *components.Position,
|
|
|
|
|
velocity *components.Velocity) *assets.Tile {
|
|
|
|
|
|
2024-02-06 15:26:20 +01:00
|
|
|
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) {
|
2024-02-08 18:33:59 +01:00
|
|
|
level.DrawTiles(screen)
|
|
|
|
|
level.DrawMovables(screen)
|
|
|
|
|
level.DrawParticles(screen)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (level *Level) DrawTiles(screen *ebiten.Image) {
|
|
|
|
|
op := &ebiten.DrawImageOptions{}
|
2024-02-07 18:01:58 +01:00
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
2024-02-08 18:33:59 +01:00
|
|
|
query := level.World.Query(level.Selector["tile"])
|
2024-02-07 18:01:58 +01:00
|
|
|
for query.Next() {
|
2024-02-08 18:33:59 +01:00
|
|
|
pos := (*components.Position)(query.Get(level.Component["position"]))
|
|
|
|
|
sprite := (*components.Renderable)(query.Get(level.Component["renderable"]))
|
2024-02-07 18:01:58 +01:00
|
|
|
|
|
|
|
|
draw.Draw(
|
|
|
|
|
level.Cache,
|
|
|
|
|
image.Rect(pos.X, pos.Y, pos.X+pos.Cellsize, pos.Y+pos.Cellsize),
|
|
|
|
|
sprite.Image, image.ZP, draw.Over)
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-08 18:33:59 +01:00
|
|
|
op.GeoM.Reset()
|
2024-02-07 18:01:58 +01:00
|
|
|
screen.DrawImage(level.Cache, op)
|
|
|
|
|
|
|
|
|
|
level.UseCache = true
|
|
|
|
|
} else {
|
|
|
|
|
// use the cached map
|
2024-02-08 18:33:59 +01:00
|
|
|
op.GeoM.Reset()
|
2024-02-07 18:01:58 +01:00
|
|
|
screen.DrawImage(level.Cache, op)
|
|
|
|
|
}
|
2024-02-08 18:33:59 +01:00
|
|
|
}
|
2024-02-06 15:26:20 +01:00
|
|
|
|
2024-02-08 18:33:59 +01:00
|
|
|
func (level *Level) DrawMovables(screen *ebiten.Image) {
|
2024-02-07 18:01:58 +01:00
|
|
|
// write the movable tiles
|
2024-02-08 18:33:59 +01:00
|
|
|
op := &ebiten.DrawImageOptions{}
|
|
|
|
|
selector := level.Selector["movable"].Without(level.Component["solid"])
|
2024-02-07 18:01:58 +01:00
|
|
|
query := level.World.Query(&selector)
|
2024-02-08 18:33:59 +01:00
|
|
|
|
2024-02-06 15:26:20 +01:00
|
|
|
for query.Next() {
|
2024-02-08 18:33:59 +01:00
|
|
|
pos := (*components.Position)(query.Get(level.Component["position"]))
|
|
|
|
|
sprite := (*components.Renderable)(query.Get(level.Component["renderable"]))
|
2024-02-06 15:26:20 +01:00
|
|
|
|
2024-02-08 18:33:59 +01:00
|
|
|
op.GeoM.Reset()
|
2024-02-06 15:26:20 +01:00
|
|
|
op.GeoM.Translate(float64(pos.X), float64(pos.Y))
|
|
|
|
|
|
|
|
|
|
screen.DrawImage(sprite.Image, op)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-08 18:33:59 +01:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-06 15:26:20 +01:00
|
|
|
func (level *Level) SetupGrid(game *Game) {
|
2024-02-09 20:20:13 +01:00
|
|
|
// 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)
|
|
|
|
|
|
|
|
|
|
// setup world
|
2024-02-06 15:26:20 +01:00
|
|
|
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
|
|
|
|
|
}
|