bullet-hell/main.go
2025-08-23 19:55:34 -03:00

669 lines
14 KiB
Go

package main
import (
"fmt"
"math"
"math/rand"
"path"
"strconv"
rl "github.com/gen2brain/raylib-go/raylib"
)
// diz para um inimigo em que condições atirar. acionado pelo movementPattern
type shootingPattern func(body)
// diz para uma bala como ela deve se mover.
type bulletMovementPattern func(*bullet) rl.Vector2
type duration float32
const second duration = 1
type body interface { // implementado por player e enemy
Pos() rl.Vector2
SetPos(rl.Vector2)
Direction() rl.Vector2
}
// type hazard interface { // inimigos e projéteis
// body
// }
type game struct {
arenaWidth int32
arenaHeight int32
interfaceWidth int32
frame float32
enemies []*enemy
waves []*wave
walls []plane
currentWave int
bullets []*bullet
score int
gameSpeed float32 // 1 = velocidade normal
backgroundColor rl.Color
}
type plane struct {
normal rl.Vector2
pos rl.Vector2
}
type bullet struct {
pos rl.Vector2
speed rl.Vector2
size float32
dmg int
owner body
onHit func(body)
onDestroy func()
}
type enemy struct {
pos rl.Vector2
direction rl.Vector2
health int
move movementPattern
shoot shootingPattern
hitBoxRadius float32
}
func (e *enemy) Pos() rl.Vector2 {
return e.pos
}
func (e *enemy) Direction() rl.Vector2 {
return e.direction
}
func (e *enemy) SetPos(x rl.Vector2) {
e.pos = x
}
type wave struct {
duration *timer
enemies []*enemy
entrance movementPattern
}
type timer struct {
ttl duration
time float32
start float64
}
func (t *timer) tick(g *game) {
t.time += rl.GetFrameTime() * g.gameSpeed
}
func (t *timer) isTimeout() bool {
return t.time >= float32(t.ttl)
}
func (t *timer) reset() {
t.start = float64(t.time)
t.time = 0
}
func (t *timer) unit() float32 {
return t.time / float32(t.ttl)
}
func newTimer(duration duration) *timer {
return &timer{
time: 0,
ttl: duration,
start: rl.GetTime(),
}
}
func (g game) insideArena(v rl.Vector2) bool {
return v.X >= 0 && v.Y >= 0 &&
v.Y <= float32(g.arenaHeight) && v.X <= float32(g.arenaWidth)
}
func (g *game) removeBullet(index int) {
g.bullets[index] = g.bullets[len(g.bullets)-1]
g.bullets = g.bullets[:len(g.bullets)-1]
}
func (b *bullet) update(g *game, index int) {
b.pos = rl.Vector2Add(b.pos, rl.Vector2Scale(b.speed, rl.GetFrameTime()*g.gameSpeed))
if !g.insideArena(b.pos) {
if b.onDestroy != nil {
b.onDestroy()
}
g.removeBullet(index)
return
}
rl.DrawCircleV(b.pos, b.size, rl.Yellow)
}
func bulletExplosion(g *game, rate float32, amount int, bulletSpeed, size float32) shootingPattern {
t := newTimer(second * duration(rate+rand.Float32()))
return func(e body) {
t.tick(g)
if !t.isTimeout() {
return
}
t.reset()
var bullets []*bullet
for i := 0; i < amount; i++ {
angle := 2.0 * math.Pi * float64(i) / float64(amount)
cos := math.Cos(angle)
sin := math.Sin(angle)
direction := rl.Vector2{X: float32(cos), Y: float32(sin)}
direction = rl.Vector2Scale(direction, bulletSpeed)
bullets = append(bullets, &bullet{
speed: direction,
size: size,
dmg: 1,
owner: e,
pos: e.Pos(),
})
}
g.bullets = append(g.bullets, bullets...)
}
}
// func ShootAtPlayer(g *game, p *player, rate int,
// bulletMoveSpeed float32) shootingPattern {
// return func(e body) {
// if int(g.frame)%rate != 0 {
// return
// }
// direction := rl.Vector2Subtract(p.pos, e.Pos())
// direction = rl.Vector2Normalize(direction)
// direction = rl.Vector2Scale(direction, bulletMoveSpeed)
//
// g.bullets = append(g.bullets, &bullet{
// speed: direction,
// size: 12,
// dmg: 1,
// enemy: true,
// pos: e.Pos(),
// })
// }
// }
func burstShootAtPlayer(g *game, p *player, rate float32, bulletMoveSpeed float32) shootingPattern {
flag := true
off := newTimer(second)
on := newTimer(duration(float32(second) * rate))
return func(e body) {
off.tick(g)
if off.isTimeout() {
flag = !flag
off.reset()
}
if !flag {
return
}
on.tick(g)
if !on.isTimeout() {
return
}
on.reset()
direction := rl.Vector2Subtract(p.pos, e.Pos())
direction = rl.Vector2Normalize(direction)
direction = rl.Vector2Scale(direction, bulletMoveSpeed)
g.bullets = append(g.bullets, &bullet{
speed: direction,
size: 12,
dmg: 1,
owner: e,
pos: e.Pos(),
})
}
}
func shootStraightDown(g *game) shootingPattern {
return func(e body) {
if int(g.frame)%10 != 0 {
return
}
g.bullets = append(g.bullets, &bullet{
speed: rl.Vector2{X: 0, Y: 5},
size: 12,
dmg: 1,
owner: e,
pos: e.Pos(),
})
}
}
// func detectWallCollision(planes []plane, initialPos, direction rl.Vector2) float32 {
// p := initialPos
// w := direction
// var closest = math.Inf(1)
// for _, plane := range planes {
// c := plane.pos
// n := plane.normal
// denominator := rl.Vector2DotProduct(w, n)
// if denominator == 0 {
// continue
// }
// t := rl.Vector2DotProduct(rl.Vector2Subtract(p, c), n) / denominator
// closest = min(float64(t), closest)
// }
// return float32(-closest)
// }
// func foobarPattern(g *game) movementPattern {
//
// pos := rl.Vector2{}
// state := 0
// wait := 0
//
// return func(e *enemy) rl.Vector2 {
//
// switch state {
// case 0: // init
// pos.X = e.pos.X
// state = 1
// return pos
// case 1: // descer
// pos.Y += 1
// if pos.Y >= 100 { state = 2; wait = 50 }
// return pos
// case 2: // atirar por um tempo
// e.shoot(e)
// wait -= 1
// if wait <= 0 { state = 3; wait = 60 }
// return pos
// case 3: // wait
// wait -= 1
// if wait <= 0 { state = 4 }
// return pos
// case 4: // retornar
// pos.Y -= 3
// if pos.Y - e.hitBoxRadius < 0 {
// e.health = 0
// }
// return pos
// }
// panic(state)
// }
// }
func (e *enemy) checkHit(g *game) (bool, *bullet, int) {
for index, bullet := range g.bullets {
_, playerBullet := bullet.owner.(*player)
if !playerBullet {
continue
}
distance := rl.Vector2Distance(e.pos, bullet.pos) - bullet.size
if distance < e.hitBoxRadius {
return true, bullet, index
}
}
return false, nil, 0
}
func (g *game) killEnemy(index int) {
g.enemies[index] = g.enemies[len(g.enemies)-1]
g.enemies = g.enemies[:len(g.enemies)-1]
}
func (e *enemy) deleteBullets(g *game) {
for i := 0; i < len(g.bullets); i++ {
enemy, isEnemyBullet := g.bullets[i].owner.(*enemy)
if isEnemyBullet && enemy == e {
g.removeBullet(i)
i--
}
}
}
func (e *enemy) update(g *game) {
if e.health <= 0 {
return
}
enemyColor := rl.Blue
if e.move != nil {
// e.pos = rl.Vector2Add(rl.Vector2Scale(e.move(e), g.gameSpeed), e.pos)
e.move(e)
// e.pos = e.move(e)
}
if hit, bullet, idx := e.checkHit(g); hit {
g.score += 273
e.health -= bullet.dmg
if bullet.onHit != nil {
bullet.onHit(e)
}
g.removeBullet(idx)
enemyColor = rl.White
g.backgroundColor = rl.NewColor(20, 20, 20, 255)
if e.health <= 0 {
e.deleteBullets(g)
return
}
}
rl.DrawCircleV(e.pos, e.hitBoxRadius, enemyColor)
}
func (g *game) fullClear() bool {
for _, e := range g.enemies {
if e.health > 0 {
return false
}
}
return true
}
func (g *game) waveTimeout() bool {
currentWave := g.waves[g.currentWave]
if currentWave.duration == nil {
return false
}
if currentWave.duration.isTimeout() {
return true
}
currentWave.duration.tick(g)
return false
}
func (g *game) wavesOver() bool {
return g.currentWave == len(g.waves)
}
func (g *game) nextWave() bool {
g.currentWave++
if g.wavesOver() {
return false
}
g.addEnemy(g.waves[g.currentWave].enemies...)
return true
}
func (g *game) addEnemy(e ...*enemy) {
g.enemies = append(g.enemies, e...)
}
func main() {
state := &game{
arenaWidth: 450,
arenaHeight: 900,
interfaceWidth: 400,
gameSpeed: 1,
backgroundColor: rl.NewColor(0, 0, 0, 100),
}
player := player{
pos: rl.Vector2{
X: float32(state.arenaWidth) / 2,
Y: float32(state.arenaHeight) * 0.8,
},
direction: rl.Vector2{X: 0, Y: -1},
moveSpeed: 400,
focusSpeedDecrease: 0.5,
bulletMoveSpeed: 6,
bulletSize: 9,
hitBoxRadius: 5,
shoot: snipe(state),
}
// var arena = []plane{
// {
// normal: rl.Vector2{X: 0, Y: -1},
// pos: rl.Vector2{X: 1, Y: 0},
// },
// {
// normal: rl.Vector2{X: -1, Y: 0},
// pos: rl.Vector2{X: float32(state.arenaWidth), Y: 0},
// },
// {
// normal: rl.Vector2{X: 0, Y: 1},
// pos: rl.Vector2{X: 0, Y: float32(state.arenaHeight)},
// },
// {
// normal: rl.Vector2{X: 1, Y: 0},
// pos: rl.Vector2{X: 0, Y: 1},
// },
// }
// state.walls = arena
rl.SetTraceLog(rl.LogWarning | rl.LogDebug)
rl.SetConfigFlags(rl.FlagMsaa4xHint)
rl.InitWindow(state.arenaWidth+state.interfaceWidth, state.arenaHeight, "danmaku")
targetFPS := 120
rl.SetTargetFPS(int32(targetFPS))
// shader_path := path.Join("shaders", "trails.glsl")
// shader := rl.LoadShader("", shader_path)
// timeShaderLocation := rl.GetShaderLocation(shader, "time")
// target := rl.LoadRenderTexture(
// state.arenaWidth+state.interfaceWidth,
// state.arenaHeight,
// )
shader_path := path.Join("shaders", "pixelized.glsl")
shader := rl.LoadShader("", shader_path)
// timeShaderLocation := rl.GetShaderLocation(shader, "time")
// target := rl.LoadRenderTexture(
// state.arenaWidth+state.interfaceWidth,
// state.arenaHeight,
// )
state.waves = []*wave{
{
duration: newTimer(second * 10),
enemies: []*enemy{
{
pos: rl.Vector2{X: 100, Y: -20},
health: 100,
hitBoxRadius: 20,
move: statePipeline{
{
duration: 1.1,
move: sineDescentMove(state, -20, 200, 1),
},
{
move: sineHorizonalPattern(state, 0),
},
}.MovementPattern(state),
shoot: burstShootAtPlayer(state, &player, 0.1, 400),
},
{
pos: rl.Vector2{X: 200, Y: -20},
health: 100,
hitBoxRadius: 20,
move: statePipeline{
{
duration: 5,
move: shootStill(),
},
{
duration: 1.1,
move: descentMove(state, -20, 200, 1),
},
{
move: sineHorizonalPattern(state, 1),
},
}.MovementPattern(state),
shoot: burstShootAtPlayer(state, &player, 0.1, 400),
},
},
},
{
duration: newTimer(second * 20),
enemies: []*enemy{
{
pos: rl.Vector2{X: 100, Y: 100},
health: 100,
hitBoxRadius: 20,
move: statePipeline{
{
duration: 1.1,
move: sineDescentMove(state, -20, 200, 1),
},
{
move: sineHorizonalPattern(state, 0),
},
}.MovementPattern(state),
shoot: bulletExplosion(state, 1, 40, 300, 11),
},
},
},
{
enemies: []*enemy{
{
pos: rl.Vector2{X: 200, Y: 200},
health: 100,
hitBoxRadius: 20,
move: statePipeline{
{
duration: 1.1,
move: sineDescentMove(state, -20, 200, 1),
},
{
move: sineHorizonalPattern(state, 0),
},
}.MovementPattern(state),
shoot: bulletExplosion(state, 1, 20, 300, 11),
},
{
pos: rl.Vector2{X: 100, Y: 100}, health: 100,
hitBoxRadius: 20,
move: statePipeline{
{
duration: 1.1,
move: sineDescentMove(state, -20, 200, 1),
},
{
move: sineHorizonalPattern(state, 0),
},
}.MovementPattern(state),
shoot: burstShootAtPlayer(state, &player, 0.2, 400),
},
{
pos: rl.Vector2{X: 50, Y: 250},
health: 100,
hitBoxRadius: 20,
move: statePipeline{
{
duration: 1.1,
move: sineDescentMove(state, -20, 200, 1),
},
{
move: sineHorizonalPattern(state, 0),
},
}.MovementPattern(state),
shoot: bulletExplosion(state, 1, 20, 300, 11),
// shoot: shootStraightDown(state),
},
},
},
}
currectScore := 0
state.addEnemy(state.waves[0].enemies...)
spawner := starsBackground(state)
for ; !rl.WindowShouldClose(); state.frame += state.gameSpeed {
rl.BeginDrawing()
rl.BeginShaderMode(shader)
// rl.BeginTextureMode(target)
rl.ClearBackground(state.backgroundColor)
state.backgroundColor = rl.Black
player.update(state)
if state.fullClear() || state.waveTimeout() {
if !state.nextWave() {
break
}
}
enemiesTotalLifeRemaining := 0
for i := 0; i < len(state.enemies); i++ {
state.enemies[i].update(state)
enemiesTotalLifeRemaining += max(state.enemies[i].health, 0)
}
for i := 0; i < len(state.bullets); i++ {
state.bullets[i].update(state, i)
}
rl.DrawRectangle(
state.arenaWidth, 0, state.interfaceWidth, state.arenaHeight,
rl.NewColor(0, 33, 59, 255),
)
spawner.update(state)
if player.focusMode {
state.gameSpeed = 0.3
} else {
state.gameSpeed = 1
}
{ // UI
currectScore += (state.score - currectScore) / 11
rl.DrawText(
fmt.Sprint("score: ", strconv.Itoa(currectScore)),
state.arenaWidth, 0, 50, rl.White,
)
rl.DrawText(
fmt.Sprint("bullets: ", strconv.Itoa(len(state.bullets))),
state.arenaWidth, 50, 50, rl.White,
)
rl.DrawText(
fmt.Sprintf("wave: %d/%d", state.currentWave, len(state.waves)),
state.arenaWidth, 100, 50, rl.White,
)
rl.DrawText(
fmt.Sprintf("t: %f", rl.GetTime()), state.arenaWidth, 150, 50, rl.White,
)
rl.DrawText(
fmt.Sprintf("sec/f: %.5f", rl.GetFrameTime()), state.arenaWidth, 200, 50, rl.White,
)
rl.DrawText(
fmt.Sprintf("total life: %d", enemiesTotalLifeRemaining),
state.arenaWidth, 250, 50, rl.White,
)
rl.DrawText(
fmt.Sprintf("speed: %f", state.gameSpeed),
state.arenaWidth, 300, 50, rl.White,
)
rl.DrawText("danmaku babe bullet shoot shoot", 20, 20, 20, rl.DarkGray)
rl.DrawLine(18, 42, state.arenaWidth-18, 42, rl.Black)
rl.DrawFPS(0, 0)
}
rl.EndShaderMode()
rl.EndDrawing()
}
rl.CloseWindow()
}