You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
265 lines
6.2 KiB
265 lines
6.2 KiB
package world |
|
|
|
import ( |
|
"bytes" |
|
"image" |
|
"log" |
|
"math" |
|
"net/http" |
|
"path/filepath" |
|
"time" |
|
|
|
"code.rocketnine.space/tslocum/monovania/engine" |
|
|
|
"code.rocketnine.space/tslocum/gohan" |
|
"code.rocketnine.space/tslocum/monovania/asset" |
|
"code.rocketnine.space/tslocum/monovania/component" |
|
"github.com/hajimehoshi/ebiten/v2" |
|
"github.com/lafriks/go-tiled" |
|
) |
|
|
|
// Fire tile IDs. |
|
const ( |
|
FireTileA = 13 |
|
FireTileB = 14 |
|
FireTileC = 15 |
|
) |
|
|
|
var World = &GameWorld{ |
|
StartedAt: time.Now(), |
|
DuckStart: -1, |
|
DuckEnd: -1, |
|
} |
|
|
|
type GameWorld struct { |
|
Map *tiled.Map |
|
SpawnX, SpawnY float64 |
|
ObjectGroups []*tiled.ObjectGroup |
|
StartedAt time.Time |
|
GameOver bool |
|
Player gohan.Entity |
|
ScreenW, ScreenH int |
|
NoClip bool |
|
Debug int |
|
|
|
OffsetX, OffsetY float64 |
|
|
|
DuckStart float64 |
|
DuckEnd float64 |
|
|
|
// Abilities |
|
CanDoubleJump bool |
|
CanLevitate bool |
|
|
|
Jumps int |
|
Levitating bool |
|
|
|
TriggerRects []image.Rectangle |
|
TriggerEntities []gohan.Entity |
|
TriggerNames []string |
|
|
|
DisableEsc bool // TODO |
|
} |
|
|
|
func TileToGameCoords(x, y int) (float64, float64) { |
|
//return float64(x) * 16, float64(g.currentMap.Height*16) - float64(y)*16 - 16 |
|
return float64(x) * 16, float64(y) * 16 |
|
} |
|
|
|
func LoadMap(filePath string) { |
|
loader := tiled.Loader{ |
|
FileSystem: http.FS(asset.FS), |
|
} |
|
|
|
b, err := asset.FS.ReadFile(filePath) |
|
if err != nil { |
|
panic(err) |
|
} |
|
|
|
// Parse .tmx file. |
|
m, err := loader.LoadFromReader("/", bytes.NewReader(b)) |
|
if err != nil { |
|
log.Fatalf("error parsing world: %+v", err) |
|
} |
|
|
|
// Load tileset. |
|
|
|
tileset := m.Tilesets[0] |
|
|
|
imgPath := filepath.Join("./map/", tileset.Image.Source) |
|
f, err := asset.FS.Open(imgPath) |
|
if err != nil { |
|
panic(err) |
|
} |
|
defer f.Close() |
|
|
|
img, _, err := image.Decode(f) |
|
if err != nil { |
|
panic(err) |
|
} |
|
tilesetImg := ebiten.NewImageFromImage(img) |
|
|
|
// Load tiles. |
|
|
|
tileCache := make(map[uint32]*ebiten.Image) |
|
for i := uint32(0); i < uint32(tileset.TileCount); i++ { |
|
rect := tileset.GetTileRect(i) |
|
tileCache[i+tileset.FirstGID] = tilesetImg.SubImage(rect).(*ebiten.Image) |
|
} |
|
|
|
createTileEntity := func(t *tiled.LayerTile, x int, y int) gohan.Entity { |
|
tileX, tileY := TileToGameCoords(x, y) |
|
|
|
mapTile := engine.Engine.NewEntity() |
|
engine.Engine.AddComponent(mapTile, &component.PositionComponent{ |
|
X: tileX, |
|
Y: tileY, |
|
}) |
|
|
|
sprite := &component.SpriteComponent{ |
|
Image: tileCache[t.Tileset.FirstGID+t.ID], |
|
HorizontalFlip: t.HorizontalFlip, |
|
VerticalFlip: t.VerticalFlip, |
|
DiagonalFlip: t.DiagonalFlip, |
|
} |
|
|
|
// Animate fire tiles. |
|
if t.ID == FireTileA || t.ID == FireTileB || t.ID == FireTileC { |
|
switch t.ID { |
|
case FireTileA: |
|
sprite.Frames = []*ebiten.Image{ |
|
tileCache[t.Tileset.FirstGID+FireTileA], |
|
tileCache[t.Tileset.FirstGID+FireTileB], |
|
tileCache[t.Tileset.FirstGID+FireTileC], |
|
tileCache[t.Tileset.FirstGID+FireTileA], |
|
tileCache[t.Tileset.FirstGID+FireTileC], |
|
tileCache[t.Tileset.FirstGID+FireTileA], |
|
tileCache[t.Tileset.FirstGID+FireTileB], |
|
} |
|
case FireTileB: |
|
sprite.Frames = []*ebiten.Image{ |
|
tileCache[t.Tileset.FirstGID+FireTileB], |
|
tileCache[t.Tileset.FirstGID+FireTileA], |
|
tileCache[t.Tileset.FirstGID+FireTileB], |
|
tileCache[t.Tileset.FirstGID+FireTileC], |
|
tileCache[t.Tileset.FirstGID+FireTileB], |
|
tileCache[t.Tileset.FirstGID+FireTileA], |
|
tileCache[t.Tileset.FirstGID+FireTileC], |
|
} |
|
case FireTileC: |
|
sprite.Frames = []*ebiten.Image{ |
|
tileCache[t.Tileset.FirstGID+FireTileC], |
|
tileCache[t.Tileset.FirstGID+FireTileA], |
|
tileCache[t.Tileset.FirstGID+FireTileC], |
|
tileCache[t.Tileset.FirstGID+FireTileB], |
|
tileCache[t.Tileset.FirstGID+FireTileA], |
|
tileCache[t.Tileset.FirstGID+FireTileC], |
|
tileCache[t.Tileset.FirstGID+FireTileB], |
|
} |
|
} |
|
sprite.NumFrames = len(sprite.Frames) |
|
sprite.FrameTime = 150 * time.Millisecond |
|
} |
|
|
|
engine.Engine.AddComponent(mapTile, sprite) |
|
|
|
return mapTile |
|
} |
|
|
|
var t *tiled.LayerTile |
|
for _, layer := range m.Layers { |
|
for y := 0; y < m.Height; y++ { |
|
for x := 0; x < m.Width; x++ { |
|
t = layer.Tiles[y*m.Width+x] |
|
if t == nil || t.Nil { |
|
continue // No tile at this position. |
|
} |
|
|
|
// TODO use Tileset.Animation |
|
// use current time in millis (cached) % total animation time |
|
|
|
tileImg := tileCache[t.Tileset.FirstGID+t.ID] |
|
if tileImg == nil { |
|
continue |
|
} |
|
|
|
_ = createTileEntity(t, x, y) |
|
} |
|
} |
|
} |
|
|
|
// Load ObjectGroups. |
|
|
|
var objects []*tiled.ObjectGroup |
|
var loadObjects func(grp *tiled.Group) |
|
loadObjects = func(grp *tiled.Group) { |
|
for _, subGrp := range grp.Groups { |
|
loadObjects(subGrp) |
|
} |
|
for _, objGrp := range grp.ObjectGroups { |
|
objects = append(objects, objGrp) |
|
} |
|
} |
|
for _, grp := range m.Groups { |
|
loadObjects(grp) |
|
} |
|
for _, objGrp := range m.ObjectGroups { |
|
objects = append(objects, objGrp) |
|
} |
|
|
|
World.Map = m |
|
World.ObjectGroups = objects |
|
|
|
World.SpawnX, World.SpawnY = -math.MaxFloat64, -math.MaxFloat64 |
|
for _, grp := range World.ObjectGroups { |
|
if grp.Name == "PLAYERSPAWN" { |
|
for _, obj := range grp.Objects { |
|
World.SpawnX, World.SpawnY = obj.X, obj.Y-1 |
|
} |
|
break |
|
} |
|
} |
|
for _, grp := range World.ObjectGroups { |
|
if !grp.Visible { |
|
continue |
|
} |
|
if grp.Name == "TEMPSPAWN" { |
|
for _, obj := range grp.Objects { |
|
World.SpawnX, World.SpawnY = obj.X, obj.Y-1 |
|
} |
|
break |
|
} |
|
} |
|
if World.SpawnX == -math.MaxFloat64 || World.SpawnY == -math.MaxFloat64 { |
|
panic("world does not contain a player spawn object") |
|
} |
|
|
|
for _, grp := range World.ObjectGroups { |
|
if grp.Name == "TRIGGERS" { |
|
for _, obj := range grp.Objects { |
|
if obj.Name == "" { |
|
continue |
|
} |
|
|
|
mapTile := engine.Engine.NewEntity() |
|
engine.Engine.AddComponent(mapTile, &component.PositionComponent{ |
|
X: obj.X, |
|
Y: obj.Y - 16, |
|
}) |
|
engine.Engine.AddComponent(mapTile, &component.SpriteComponent{ |
|
Image: tileCache[obj.GID], |
|
}) |
|
|
|
World.TriggerNames = append(World.TriggerNames, obj.Name) |
|
World.TriggerEntities = append(World.TriggerEntities, mapTile) |
|
World.TriggerRects = append(World.TriggerRects, ObjectToRect(obj)) |
|
} |
|
break |
|
} |
|
} |
|
} |
|
|
|
func ObjectToRect(o *tiled.Object) image.Rectangle { |
|
x, y, w, h := int(o.X), int(o.Y), int(o.Width), int(o.Height) |
|
return image.Rect(x, y, x+w, y+h) |
|
}
|
|
|