235 lines
6.7 KiB
Go
235 lines
6.7 KiB
Go
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{}
|
|
|
|
// FIXME: use plan.BGImage.Path here?
|
|
systemlist = append(systemlist,
|
|
systems.NewGridSystem(game.World, game.ScreenWidth, game.ScreenHeight, cellsize,
|
|
assets.Assets[plan.PropertyByIdentifier("background").AsString()]))
|
|
|
|
systemlist = append(systemlist, systems.NewCollectibleSystem(game.World))
|
|
|
|
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
|
|
|
|
animateondestruct := util.GetPropertyBool(entity, "AnimateOnDestruct")
|
|
// FIXME: also check for AnimationLoop and other animation reasons
|
|
if animateondestruct {
|
|
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.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
|
|
}
|