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.
404 lines
8.9 KiB
404 lines
8.9 KiB
package world |
|
|
|
import ( |
|
"image" |
|
"log" |
|
"math" |
|
"math/rand" |
|
"path/filepath" |
|
|
|
"code.rocketnine.space/tslocum/brownboxbatman/asset" |
|
"code.rocketnine.space/tslocum/brownboxbatman/component" |
|
"code.rocketnine.space/tslocum/brownboxbatman/entity" |
|
"code.rocketnine.space/tslocum/gohan" |
|
"github.com/hajimehoshi/ebiten/v2" |
|
"github.com/lafriks/go-tiled" |
|
) |
|
|
|
var World = &GameWorld{ |
|
CamScale: 1, |
|
CamMoving: true, |
|
PlayerWidth: 8, |
|
PlayerHeight: 32, |
|
TileImages: make(map[uint32]*ebiten.Image), |
|
ResetGame: true, |
|
} |
|
|
|
type GameWorld struct { |
|
Player gohan.Entity |
|
|
|
ScreenW, ScreenH int |
|
|
|
DisableEsc bool |
|
|
|
Debug int |
|
NoClip bool |
|
|
|
GameStarted bool |
|
GameStartedTicks int |
|
GameOver bool |
|
|
|
MessageVisible bool |
|
MessageTicks int |
|
MessageDuration int |
|
MessageUpdated bool |
|
MessageText string |
|
|
|
PlayerX, PlayerY float64 |
|
|
|
CamX, CamY float64 |
|
CamScale float64 |
|
CamMoving bool |
|
|
|
PlayerWidth float64 |
|
PlayerHeight float64 |
|
|
|
Map *tiled.Map |
|
ObjectGroups []*tiled.ObjectGroup |
|
HazardRects []image.Rectangle |
|
CreepRects []image.Rectangle |
|
CreepEntities []gohan.Entity |
|
TriggerEntities []gohan.Entity |
|
TriggerRects []image.Rectangle |
|
TriggerNames []string |
|
|
|
NativeResolution bool |
|
|
|
BrokenPieceA, BrokenPieceB gohan.Entity |
|
|
|
TileImages map[uint32]*ebiten.Image |
|
|
|
ResetGame bool |
|
|
|
resetTipShown bool |
|
} |
|
|
|
func TileToGameCoords(x, y int) (float64, float64) { |
|
//return float64(x) * 32, float64(g.currentMap.Height*32) - float64(y)*32 - 32 |
|
return float64(x) * 32, float64(y) * 32 |
|
} |
|
|
|
func Reset() { |
|
for _, e := range gohan.AllEntities() { |
|
e.Remove() |
|
} |
|
World.Player = 0 |
|
|
|
World.ObjectGroups = nil |
|
World.HazardRects = nil |
|
World.CreepRects = nil |
|
World.CreepEntities = nil |
|
World.TriggerEntities = nil |
|
World.TriggerRects = nil |
|
World.TriggerNames = nil |
|
|
|
World.MessageVisible = false |
|
} |
|
|
|
func LoadMap(filePath string) { |
|
// Parse .tmx file. |
|
m, err := tiled.LoadFile(filePath, tiled.WithFileSystem(asset.FS)) |
|
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(filepath.FromSlash(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. |
|
|
|
for i := uint32(0); i < uint32(tileset.TileCount); i++ { |
|
rect := tileset.GetTileRect(i) |
|
World.TileImages[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 := gohan.NewEntity() |
|
mapTile.AddComponent(&component.Position{ |
|
X: tileX, |
|
Y: tileY, |
|
}) |
|
|
|
sprite := &component.Sprite{ |
|
Image: World.TileImages[t.Tileset.FirstGID+t.ID], |
|
HorizontalFlip: t.HorizontalFlip, |
|
VerticalFlip: t.VerticalFlip, |
|
DiagonalFlip: t.DiagonalFlip, |
|
} |
|
mapTile.AddComponent(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. |
|
} |
|
|
|
tileImg := World.TileImages[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 |
|
|
|
for _, grp := range World.ObjectGroups { |
|
if grp.Name == "TRIGGERS" { |
|
for _, obj := range grp.Objects { |
|
mapTile := gohan.NewEntity() |
|
mapTile.AddComponent(&component.Position{ |
|
X: obj.X, |
|
Y: obj.Y - 32, |
|
}) |
|
mapTile.AddComponent(&component.Sprite{ |
|
Image: World.TileImages[obj.GID], |
|
}) |
|
|
|
World.TriggerNames = append(World.TriggerNames, obj.Name) |
|
World.TriggerEntities = append(World.TriggerEntities, mapTile) |
|
World.TriggerRects = append(World.TriggerRects, ObjectToRect(obj)) |
|
} |
|
} else if grp.Name == "HAZARDS" { |
|
for _, obj := range grp.Objects { |
|
r := ObjectToRect(obj) |
|
r.Min.Y += 32 |
|
r.Max.Y += 32 |
|
World.HazardRects = append(World.HazardRects, r) |
|
} |
|
} else if grp.Name == "CREEPS" { |
|
for _, obj := range grp.Objects { |
|
creepType := component.CreepSnowblower |
|
switch obj.GID { |
|
case 9: |
|
creepType = component.CreepSmallRock |
|
case 18: |
|
creepType = component.CreepMediumRock |
|
case 23: |
|
creepType = component.CreepLargeRock |
|
} |
|
r := ObjectToRect(obj) |
|
c := NewCreep(creepType, int64(obj.ID), float64(r.Min.X), float64(r.Min.Y)) |
|
World.CreepRects = append(World.CreepRects, r) |
|
World.CreepEntities = append(World.CreepEntities, c) |
|
} |
|
} |
|
} |
|
} |
|
|
|
func ObjectToRect(o *tiled.Object) image.Rectangle { |
|
x, y, w, h := int(o.X), int(o.Y), int(o.Width), int(o.Height) |
|
y -= 32 |
|
return image.Rect(x, y, x+w, y+h) |
|
} |
|
|
|
func LevelCoordinatesToScreen(x, y float64) (float64, float64) { |
|
return (x - World.CamX) * World.CamScale, (y - World.CamY) * World.CamScale |
|
} |
|
|
|
func (w *GameWorld) SetGameOver(vx, vy float64) { |
|
if w.GameOver { |
|
return |
|
} |
|
|
|
w.GameOver = true |
|
|
|
if rand.Intn(100) == 7 { |
|
asset.SoundBatHit4.Rewind() |
|
asset.SoundBatHit4.Play() |
|
} else { |
|
deathSound := rand.Intn(3) |
|
switch deathSound { |
|
case 0: |
|
asset.SoundBatHit1.Rewind() |
|
asset.SoundBatHit1.Play() |
|
case 1: |
|
asset.SoundBatHit2.Rewind() |
|
asset.SoundBatHit2.Play() |
|
case 2: |
|
asset.SoundBatHit3.Rewind() |
|
asset.SoundBatHit3.Play() |
|
} |
|
} |
|
|
|
w.Player.With(func(position *component.Position, velocity *component.Velocity, sprite *component.Sprite) { |
|
sprite.Image = ebiten.NewImage(1, 1) |
|
|
|
if vx == 0 && vy == 0 { |
|
vx, vy = velocity.X, velocity.Y |
|
} |
|
|
|
xSpeedA := 1.5 |
|
xSpeedB := -1.5 |
|
ySpeedA := -1.5 |
|
ySpeedB := -1.5 |
|
if vy > 0 { |
|
ySpeedA = 1.5 |
|
ySpeedB = 1.5 |
|
} else if vx < 0 { |
|
xSpeedA = -1.5 |
|
xSpeedB = -1.5 |
|
ySpeedA = -1.5 |
|
ySpeedB = 1.5 |
|
} else if vx > 0 { |
|
xSpeedA = 1.5 |
|
xSpeedB = 1.5 |
|
ySpeedA = -1.5 |
|
ySpeedB = 1.5 |
|
} |
|
|
|
w.BrokenPieceA = entity.NewCreepBullet(position.X, position.Y, xSpeedA, ySpeedA) |
|
pieceASprite := &component.Sprite{ |
|
Image: asset.ImgBatBroken1, |
|
} |
|
w.BrokenPieceA.AddComponent(pieceASprite) |
|
w.BrokenPieceA.AddComponent(&component.CreepBullet{ |
|
Invulnerable: true, |
|
}) |
|
|
|
w.BrokenPieceB = entity.NewCreepBullet(position.X, position.Y, xSpeedB, ySpeedB) |
|
pieceBSprite := &component.Sprite{ |
|
Image: asset.ImgBatBroken2, |
|
} |
|
w.BrokenPieceB.AddComponent(pieceBSprite) |
|
w.BrokenPieceB.AddComponent(&component.CreepBullet{ |
|
Invulnerable: true, |
|
}) |
|
}) |
|
|
|
if !World.resetTipShown { |
|
SetMessage(" GAME OVER\n\nRESET: <ENTER>", math.MaxInt) |
|
World.resetTipShown = true |
|
} else { |
|
SetMessage("GAME OVER", math.MaxInt) |
|
} |
|
} |
|
|
|
// TODO move |
|
func NewCreep(creepType int, creepID int64, x float64, y float64) gohan.Entity { |
|
creep := gohan.NewEntity() |
|
|
|
creep.AddComponent(&component.Position{ |
|
X: x, |
|
Y: y, |
|
}) |
|
|
|
if creepType == component.CreepSmallRock { |
|
creep.AddComponent(&component.Velocity{}) |
|
|
|
creep.AddComponent(&component.Creep{ |
|
Type: creepType, |
|
Health: 32, |
|
FireAmount: 2, |
|
FireRate: 144 * 1, |
|
Rand: rand.New(rand.NewSource(creepID)), |
|
}) |
|
} else if creepType == component.CreepMediumRock { |
|
creep.AddComponent(&component.Velocity{}) |
|
|
|
creep.AddComponent(&component.Creep{ |
|
Type: creepType, |
|
Health: 64, |
|
FireAmount: 4, |
|
FireRate: 144 * 1, |
|
Rand: rand.New(rand.NewSource(creepID)), |
|
}) |
|
} else if creepType == component.CreepLargeRock { |
|
creep.AddComponent(&component.Velocity{}) |
|
|
|
creep.AddComponent(&component.Creep{ |
|
Type: creepType, |
|
Health: 96, |
|
FireAmount: 8, |
|
FireRate: 144, |
|
Rand: rand.New(rand.NewSource(creepID)), |
|
}) |
|
} else { // CreepSnowblower |
|
creep.AddComponent(&component.Creep{ |
|
Type: creepType, |
|
Health: 64, |
|
FireAmount: 8, |
|
FireRate: 144 / 4, |
|
Rand: rand.New(rand.NewSource(creepID)), |
|
}) |
|
} |
|
|
|
// TODO handle flipped creep |
|
var img *ebiten.Image |
|
if creepType == component.CreepSmallRock { |
|
img = World.TileImages[9] |
|
} else if creepType == component.CreepMediumRock { |
|
img = World.TileImages[18] |
|
} else if creepType == component.CreepLargeRock { |
|
img = World.TileImages[23] |
|
} else { // CreepSnowblower |
|
img = World.TileImages[50] |
|
} |
|
creep.AddComponent(&component.Sprite{ |
|
Image: img, |
|
}) |
|
|
|
return creep |
|
} |
|
|
|
func StartGame() { |
|
if World.GameStarted { |
|
return |
|
} |
|
World.GameStarted = true |
|
|
|
asset.SoundTitleMusic.Pause() |
|
|
|
asset.SoundLevelMusic.Play() |
|
} |
|
|
|
func SetMessage(message string, duration int) { |
|
World.MessageText = message |
|
World.MessageVisible = true |
|
World.MessageUpdated = true |
|
World.MessageDuration = duration |
|
World.MessageTicks = 0 |
|
}
|
|
|