Bullet hell video game
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

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
}