diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ad13d12 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +danmaku +danmaku.exe +shaders/* diff --git a/ease.go b/ease.go new file mode 100644 index 0000000..b531fbc --- /dev/null +++ b/ease.go @@ -0,0 +1,21 @@ +package main + +import ( + "math" +) + +func easeInOutCubic(x float32) float32 { + if x < 0.5 { + return 4 * x * x * x + } else { + return 1 - float32(math.Pow(float64(-2*x+2), 3))/2 + } +} + +func lerp(v0, v1, t float32) float32 { + return (1-t)*v0 + t*v1 +} + +func easeInOutSine(x float32) float32 { + return -float32(math.Cos(math.Pi*float64(x))-1) / 2 +} diff --git a/go.mod b/go.mod index 4a29b8c..7fe4ecd 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,5 @@ module danmaku -go 1.20 +go 1.23 require github.com/gen2brain/raylib-go/raylib v0.0.0-20230719211022-1083eace2049 diff --git a/main.go b/main.go index 0d1faa3..40e722d 100644 --- a/main.go +++ b/main.go @@ -3,292 +3,666 @@ package main import ( "fmt" "math" + "math/rand" + "path" + "strconv" rl "github.com/gen2brain/raylib-go/raylib" ) -type movementPattern int -type shootingPattern int +// diz para um inimigo em que condições atirar. acionado pelo movementPattern +type shootingPattern func(body) -const ( - horizontal movementPattern = iota - circular movementPattern = iota -) +// 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 { - screenWidth int32 - screenHeight int32 - time int - enemies []*enemy - bullets []*bullet + 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 - enemy bool -} - -type player struct { - pos rl.Vector2 - moveSpeed float32 - bulletMoveSpeed float32 - hitBoxRadius float32 + pos rl.Vector2 + speed rl.Vector2 + size float32 + dmg int + owner body + onHit func(body) + onDestroy func() } type enemy struct { - pos rl.Vector2 - health int - movePattern func(*enemy)rl.Vector2 - shootPattern func() - hitBoxRadius float32 + 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.screenHeight) && v.X <= float32(g.screenWidth) + 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] + 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, b.speed) + b.pos = rl.Vector2Add(b.pos, rl.Vector2Scale(b.speed, rl.GetFrameTime()*g.gameSpeed)) - if !g.insideArena(b.pos) { - g.removeBullet(index) - return - } + if !g.insideArena(b.pos) { + if b.onDestroy != nil { + b.onDestroy() + } + g.removeBullet(index) + return + } - rl.DrawCircleV(b.pos, b.size, rl.Yellow) + rl.DrawCircleV(b.pos, b.size, rl.Yellow) } -func (e *enemy) shoot(g *game) { - if g.time % 15 != 0 { - return - } +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() - g.bullets = append(g.bullets, &bullet{ - speed: rl.Vector2{X: 0, Y: 10}, - size: 4, - dmg: 1, - enemy: true, - pos: e.pos, - }) + 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 horizonalPattern(g *game) (func(*enemy)rl.Vector2) { - direction := rl.Vector2{X: 4, Y: 0} +// 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(), +// }) +// } +// } - return func(e *enemy) rl.Vector2 { - result := rl.Vector2Add(direction, 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) { - if !g.insideArena(result) { - direction = rl.Vector2Negate(direction) - } - return result - } + 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 circlePattern(g *game, center rl.Vector2) (func(*enemy)rl.Vector2) { - - t := float64(0.5) - - return func(e *enemy) rl.Vector2 { - // x' = (x - x₀) * cos(θ) - (y - y₀) * sin(θ) + x₀ - // y' = (x - x₀) * sin(θ) + (y - y₀) * cos(θ) + y₀ - - // x' = x * cos(θ) - y * sin(θ) - // y' = x * sin(θ) + y * cos(θ) - - // result := center - // - // result.X = result.X + (e.pos.X - result.X) * float32(math.Cos(t)) - - // (e.pos.Y - result.Y) * float32(math.Sin(t)) - // - // result.Y = result.Y + (e.pos.Y - result.Y) * float32(math.Sin(t)) + - // (e.pos.Y - result.Y) * float32(math.Cos(t)) - - result := center - result.X = result.X * float32(math.Cos(t)) - result.Y * float32(math.Sin(t)) - result.Y = result.X * float32(math.Sin(t)) - result.Y * float32(math.Cos(t)) - - t += .08 - return result - } +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 { + for index, bullet := range g.bullets { - playerBullet := !bullet.enemy - if !playerBullet { - continue - } - distance := rl.Vector2Distance(e.pos, bullet.pos) - bullet.size - // fmt.Println(distance) + _, 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 + 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] + g.enemies[index] = g.enemies[len(g.enemies)-1] + g.enemies = g.enemies[:len(g.enemies)-1] } - -func (e *enemy) update(g *game, index int) { - - if e.movePattern != nil { - e.pos = e.movePattern(e) - } - - if hit, bullet, idx := e.checkHit(g); hit { - e.health -= bullet.dmg - g.removeBullet(idx) - } - - if e.health <= 0 { - g.killEnemy(index) - return - } - - e.shoot(g) - - rl.DrawCircleV(e.pos, 5, rl.Blue) +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 (p *player) move() { - if rl.IsKeyDown(rl.KeyW) { p.pos.Y -= p.moveSpeed } - if rl.IsKeyDown(rl.KeyS) { p.pos.Y += p.moveSpeed } - if rl.IsKeyDown(rl.KeyA) { p.pos.X -= p.moveSpeed } - if rl.IsKeyDown(rl.KeyD) { p.pos.X += p.moveSpeed } +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 (p *player) shoot(g *game){ - if g.time % 10 != 0 { - return - } - - if rl.IsMouseButtonDown(rl.MouseLeftButton) { - // não leva em consideração a velocidade do jogador - mouse := rl.GetMousePosition() - direction := rl.Vector2Subtract(mouse, p.pos) - direction = rl.Vector2Normalize(direction) - g.bullets = append(g.bullets, &bullet{ - pos: p.pos, - size: 2, - speed: rl.Vector2Scale(direction, p.bulletMoveSpeed), - dmg: 1, - enemy: false, - }) - } +func (g *game) fullClear() bool { + for _, e := range g.enemies { + if e.health > 0 { + return false + } + } + return true } -func (p *player) checkHit(g *game) { - for _, bullet := range g.bullets { - if !bullet.enemy { - continue - } - distance := rl.Vector2Distance(p.pos, bullet.pos) - bullet.size - // fmt.Println(distance) - - if distance < p.hitBoxRadius { - fmt.Println("hit!") - } - } +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 (p *player) update(g *game) { +func (g *game) wavesOver() bool { + return g.currentWave == len(g.waves) +} - p.move() - - p.shoot(g) - - p.checkHit(g) - - // hitbox - rl.DrawCircleV(p.pos, p.hitBoxRadius, rl.Red) - - // mira - mouse := rl.GetMousePosition() - inverted := rl.Vector2Subtract(p.pos, mouse) - inverted = rl.Vector2Negate(inverted) - inverted = rl.Vector2Normalize(inverted) - inverted = rl.Vector2Scale(inverted, 2000) - rl.DrawLineV(p.pos, rl.Vector2Add(mouse, inverted), rl.NewColor(255, 0, 0, 100)) +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), + } - state := &game{screenWidth: 450, screenHeight: 700} + // 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 - player := player{ - pos: rl.Vector2{X: 100, Y: 100}, - moveSpeed: 4, - bulletMoveSpeed: 8, - hitBoxRadius: 5, - } + rl.SetTraceLog(rl.LogWarning | rl.LogDebug) - rl.SetTraceLog(rl.LogWarning | rl.LogDebug) - rl.InitWindow(state.screenWidth, state.screenHeight, "danmaku") - rl.SetTargetFPS(60) + rl.SetConfigFlags(rl.FlagMsaa4xHint) + rl.InitWindow(state.arenaWidth+state.interfaceWidth, state.arenaHeight, "danmaku") + targetFPS := 120 + rl.SetTargetFPS(int32(targetFPS)) - state.enemies = []*enemy{ - { - pos: rl.Vector2{X: 200, Y: 200}, - health: 1, - hitBoxRadius: 5, - movePattern: horizonalPattern(state), - }, - { - pos: rl.Vector2{X: 169, Y: 222}, - health: 1, - hitBoxRadius:5, - movePattern: horizonalPattern(state), - }, - { - pos: rl.Vector2{X: 400, Y: 400}, - health: 1, - hitBoxRadius:5, - // movePattern: circlePattern(state, rl.Vector2{X: 200, Y: 200}), - }, - { - pos: rl.Vector2{X: 200, Y: 200}, - health: 1, - hitBoxRadius:5, - // movePattern: circlePattern(state, rl.Vector2{X: 200, Y: 200}), - }} + // 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, + // ) - for ; !rl.WindowShouldClose(); state.time += 1 { + 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.ClearBackground(rl.Black) + rl.BeginShaderMode(shader) - rl.DrawText("danmaku", 20, 20, 20, rl.DarkGray) - rl.DrawLine(18, 42, state.screenWidth-18, 42, rl.Black) - rl.DrawFPS(0, 0) + // rl.BeginTextureMode(target) + rl.ClearBackground(state.backgroundColor) + state.backgroundColor = rl.Black - player.update(state) + player.update(state) - for i := range state.enemies { - state.enemies[i].update(state, i) - } + if state.fullClear() || state.waveTimeout() { + if !state.nextWave() { + break + } + } - for i := 0; i < len(state.bullets); i++ { - state.bullets[i].update(state, i) - } + 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() diff --git a/movementPatterns.go b/movementPatterns.go new file mode 100644 index 0000000..adfc584 --- /dev/null +++ b/movementPatterns.go @@ -0,0 +1,138 @@ +package main + +import ( + "math" + + rl "github.com/gen2brain/raylib-go/raylib" +) + +type movementPattern func(*enemy) *enemy + +type patternDuration struct { + duration duration + move movementPattern +} + +type statePipeline []patternDuration + +func shootStill() movementPattern { + return func(e *enemy) *enemy { + if e.shoot != nil { + e.shoot(e) + } + return e + } +} + +func (sp statePipeline) MovementPattern(g *game) movementPattern { + state := -1 + t := newTimer(0) + return func(e *enemy) *enemy { + if !t.isTimeout() { + t.tick(g) + } else if state < len(sp)-1 { + state++ + t = newTimer(sp[state].duration) + } + sp[state].move(e) + return e + } +} + +func horizonalPattern(g *game) movementPattern { + direction := 1 + + return func(e *enemy) *enemy { + if e.shoot != nil { + e.shoot(e) + } + + delta := rl.Vector2{ + X: 400, + Y: 0, + } + + if e.pos.X < e.hitBoxRadius || e.pos.X > float32(g.arenaWidth-int32(e.hitBoxRadius)) { + direction = -direction + } + + e.pos = rl.Vector2Add( + e.pos, + rl.Vector2Scale( + rl.Vector2{X: float32(direction) * delta.X, Y: delta.Y}, + rl.GetFrameTime()*g.gameSpeed, + ), + ) + return e + } +} + +func descentMove(g *game, y0, y1 float32, d duration) movementPattern { + t := newTimer(d) + return func(e *enemy) *enemy { + + if e.shoot != nil { + e.shoot(e) + } + + if !t.isTimeout() { + t.tick(g) + } else { + return e + } + + e.pos.Y = lerp(y0, y1, easeInOutSine(t.unit())) + return e + } +} + +func sineDescentMove(g *game, y0, y1 float32, d duration) movementPattern { + t := newTimer(d) + return func(e *enemy) *enemy { + + if e.shoot != nil { + e.shoot(e) + } + + if !t.isTimeout() { + t.tick(g) + } else { + return e + } + + e.pos.Y = lerp(y0, y1, easeInOutSine(t.unit())) + e.pos.X += float32(math.Sin(rl.GetTime() * 5 * float64(g.gameSpeed))) + + return e + } +} + +func sineHorizonalPattern(g *game, offset float64) movementPattern { + direction := 1 + + return func(e *enemy) *enemy { + if e.shoot != nil { + e.shoot(e) + } + + sine := math.Sin(rl.GetTime()*5*float64(g.gameSpeed)+offset) * 150 + + delta := rl.Vector2{ + X: 400, + Y: float32(sine), + } + + if e.pos.X < e.hitBoxRadius || e.pos.X > float32(g.arenaWidth-int32(e.hitBoxRadius)) { + direction = -direction + } + + e.pos = rl.Vector2Add( + e.pos, + rl.Vector2Scale( + rl.Vector2{X: float32(direction) * delta.X, Y: delta.Y}, + rl.GetFrameTime()*g.gameSpeed, + ), + ) + return e + } +} diff --git a/player.go b/player.go new file mode 100644 index 0000000..672ef9f --- /dev/null +++ b/player.go @@ -0,0 +1,243 @@ +package main + +import ( + "fmt" + + rl "github.com/gen2brain/raylib-go/raylib" +) + +type player struct { + pos rl.Vector2 + speed rl.Vector2 + direction rl.Vector2 + moveSpeed float32 + bulletMoveSpeed float32 + hitBoxRadius float32 + shoot shootingPattern + bulletSize float32 + focusMode bool + focusSpeedDecrease float32 +} + +func (p *player) Pos() rl.Vector2 { + return p.pos +} + +func (p *player) SetPos(x rl.Vector2) { + p.pos = x +} + +func (p *player) Direction() rl.Vector2 { + return p.direction +} + +func (p *player) move(g *game) { + + var moveSpeed float32 + if p.focusMode { + moveSpeed = p.moveSpeed * p.focusSpeedDecrease + } else { + moveSpeed = p.moveSpeed + } + + p.speed = rl.Vector2{X: 0, Y: 0} + + if rl.IsKeyDown(rl.KeyW) { + p.speed.Y -= 1 + } + if rl.IsKeyDown(rl.KeyS) { + p.speed.Y += 1 + } + if rl.IsKeyDown(rl.KeyA) { + p.speed.X -= 1 + } + if rl.IsKeyDown(rl.KeyD) { + p.speed.X += 1 + } + + if !(p.speed.X == 0 && p.speed.Y == 0) { + // jogador se move mais rapido na diagonal caso o contrario + p.speed = rl.Vector2Normalize(p.speed) + p.speed = rl.Vector2Scale(p.speed, moveSpeed) + } + + result := rl.Vector2Add(p.pos, rl.Vector2Scale(p.speed, rl.GetFrameTime())) + + if result.Y-p.hitBoxRadius < 0 || result.Y+p.hitBoxRadius > float32(g.arenaHeight) { + p.speed.Y = 0 + } + if result.X-p.hitBoxRadius < 0 || result.X+p.hitBoxRadius > float32(g.arenaWidth) { + p.speed.X = 0 + } + + p.pos = rl.Vector2Add(p.pos, rl.Vector2Scale(p.speed, rl.GetFrameTime())) +} + +// func (p *player) shoot(g *game) { +// if p.focusMode { +// return +// } +// +// if int(g.frame)%3 != 0 { +// return +// } +// +// if rl.IsMouseButtonDown(rl.MouseLeftButton) { +// mouse := rl.GetMousePosition() +// direction := rl.Vector2Subtract(mouse, p.pos) +// direction = rl.Vector2Add(direction, p.speed) +// direction = rl.Vector2Normalize(direction) +// direction = rl.Vector2Scale(direction, p.bulletMoveSpeed) +// +// g.bullets = append(g.bullets, &bullet{ +// pos: p.pos, +// size: p.bulletSize, +// speed: direction, +// dmg: 1, +// enemy: false, +// }) +// } +// } + +func (p *player) checkHit(g *game) { + for _, bullet := range g.bullets { + _, playerBullet := bullet.owner.(*player) + if playerBullet { + continue + } + distance := rl.Vector2Distance(p.pos, bullet.pos) - bullet.size + + if distance < p.hitBoxRadius { + // fmt.Println("hit!") + } + } +} + +func (p *player) checkFocus() { + // raylib não entende keybindings customizadas através do xmodmap? + p.focusMode = rl.IsKeyDown(rl.KeyLeftShift) || rl.IsKeyDown(rl.KeyRightShift) +} + +func (p *player) update(g *game) { + + p.checkFocus() + + p.move(g) + + if !p.focusMode && p.shoot != nil { + p.shoot(p) + } + + p.checkHit(g) + + // hitbox + rl.DrawCircleV(p.pos, p.hitBoxRadius, rl.Red) + + if p.focusMode { + return + } + // mira + mouse := rl.GetMousePosition() + inverted := rl.Vector2Subtract(p.pos, mouse) + inverted = rl.Vector2Negate(inverted) + inverted = rl.Vector2Normalize(inverted) + inverted = rl.Vector2Scale(inverted, 2000) + rl.DrawLineEx(p.pos, rl.Vector2Add(mouse, inverted), 3, rl.NewColor(255, 0, 0, 100)) + +} + +func tripleFire(g *game) shootingPattern { + t := newTimer(second * 0.05) + return func(b body) { + + if !t.isTimeout() { + t.tick(g) + return + } + t.reset() + + if rl.IsMouseButtonDown(rl.MouseLeftButton) { + + g.bullets = append(g.bullets, &bullet{ + pos: b.Pos(), + size: 9, + speed: rl.Vector2Scale( + rl.Mat2MultiplyVector2(rl.Mat2Radians(0.3), b.Direction()), + 600, + ), + dmg: 1, + owner: b, + }) + g.bullets = append(g.bullets, &bullet{ + pos: b.Pos(), + size: 9, + speed: rl.Vector2Scale( + rl.Mat2MultiplyVector2(rl.Mat2Radians(0), b.Direction()), + 600, + ), + dmg: 1, + owner: b, + }) + g.bullets = append(g.bullets, &bullet{ + pos: b.Pos(), + size: 9, + speed: rl.Vector2Scale( + rl.Mat2MultiplyVector2(rl.Mat2Radians(-0.3), b.Direction()), + 600, + ), + dmg: 1, + owner: b, + }) + } + + // rl.matrix + } +} + +func snipe(g *game) shootingPattern { + t := newTimer(1) + hits := 0 + return func(b body) { + + p, ok := b.(*player) + if !ok { + panic(b) + } + + if !t.isTimeout() { + t.tick(g) + return + } + + if !rl.IsMouseButtonPressed(rl.MouseLeftButton) { + return + } + + t.reset() + + mouse := rl.GetMousePosition() + direction := rl.Vector2Subtract(mouse, p.pos) + direction = rl.Vector2Add(direction, rl.Vector2Scale(p.speed, rl.GetFrameTime()*g.gameSpeed)) + direction = rl.Vector2Normalize(direction) + direction = rl.Vector2Scale(direction, p.bulletMoveSpeed*700) + + g.bullets = append(g.bullets, &bullet{ + pos: b.Pos(), + size: p.bulletSize, + speed: direction, + dmg: 10 * hits, + owner: b, + onHit: func(b body) { + hits += 2 + fmt.Println("hit!", hits) + }, + onDestroy: func() { + hits-- + hits = max(hits, 0) + fmt.Println("hit!", hits) + }, + }) + + } + +} diff --git a/windows.sh b/windows.sh new file mode 100644 index 0000000..f3ece73 --- /dev/null +++ b/windows.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc GOOS=windows GOARCH=amd64 CGO_LDFLAGS="-static-libgcc -static -lpthread" go build