473 lines
12 KiB
Go
473 lines
12 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"math/rand"
|
|
"strconv"
|
|
"time"
|
|
|
|
"gogs.bullercodeworks.com/brian/termbox-util"
|
|
|
|
"github.com/nsf/termbox-go"
|
|
)
|
|
|
|
const (
|
|
modeInit = iota
|
|
modeRun
|
|
modeRunInput
|
|
modePause
|
|
modeGameOver
|
|
)
|
|
|
|
type mainScreen struct {
|
|
GameMode int
|
|
PlayerTurn int
|
|
Player1 *player
|
|
Player2 *player
|
|
Bullet *projectile
|
|
Buildings []building
|
|
PauseMenu *termboxUtil.Menu
|
|
r *rand.Rand
|
|
animating bool
|
|
gravity float64
|
|
}
|
|
|
|
func (screen *mainScreen) handleKeyPress(event termbox.Event) int {
|
|
if event.Key == termbox.KeyEsc {
|
|
if screen.GameMode != modePause {
|
|
screen.GameMode = modePause
|
|
} else {
|
|
screen.GameMode = modeRun
|
|
}
|
|
}
|
|
if screen.GameMode == modePause || screen.GameMode == modeGameOver {
|
|
screen.PauseMenu.HandleEvent(event)
|
|
if screen.PauseMenu.IsDone() {
|
|
selOpt := screen.PauseMenu.GetSelectedOption()
|
|
if selOpt.GetText() == "Exit" {
|
|
return exitScreenIndex
|
|
} else if selOpt.GetText() == "Resume" {
|
|
screen.GameMode = modeRun
|
|
} else if selOpt.GetText() == "Restart" {
|
|
screen.GameMode = modeInit
|
|
return titleScreenIndex
|
|
}
|
|
}
|
|
}
|
|
if screen.GameMode == modeRunInput {
|
|
p := screen.Player1
|
|
if screen.PlayerTurn == 2 {
|
|
p = screen.Player2
|
|
}
|
|
if event.Key == termbox.KeySpace {
|
|
screen.Bullet.init(p)
|
|
if p.projSpeed <= 5 {
|
|
//screen.Bullet.angle = 270
|
|
//screen.Bullet.speed = 8
|
|
}
|
|
screen.animating = true
|
|
screen.GameMode = modeRun
|
|
} else if event.Key == termbox.KeyArrowLeft || event.Ch == 'a' {
|
|
p.projAngle--
|
|
} else if event.Key == termbox.KeyArrowRight || event.Ch == 'd' {
|
|
p.projAngle++
|
|
} else if event.Key == termbox.KeyArrowUp || event.Ch == 'w' {
|
|
p.projSpeed++
|
|
} else if event.Key == termbox.KeyArrowDown || event.Ch == 's' {
|
|
p.projSpeed--
|
|
}
|
|
}
|
|
return mainScreenIndex
|
|
}
|
|
|
|
func (screen *mainScreen) performLayout(style style) {
|
|
var bldCity int
|
|
if screen.GameMode == modeInit {
|
|
// Default screen gravity:
|
|
screen.gravity = float64(-9.81)
|
|
b := projectile{gravity: screen.gravity}
|
|
screen.Bullet = &b
|
|
screen.Buildings = []building{}
|
|
// Create a random seed (from time)
|
|
seed := fmt.Sprintf("%d", time.Now().UnixNano())
|
|
screen.r = rand.New(rand.NewSource(getSeedFromString(seed + "-world")))
|
|
screen.PlayerTurn = (screen.r.Int() % 2) + 1
|
|
for bldCity < ScreenWidth {
|
|
w := screen.r.Intn(8) + 6
|
|
h := screen.r.Intn(ScreenHeight-10) + 2
|
|
var c termbox.Attribute
|
|
switch screen.r.Intn(3) {
|
|
case 0:
|
|
c = termbox.ColorRed
|
|
case 1:
|
|
c = termbox.ColorGreen
|
|
case 2:
|
|
c = termbox.ColorBlue
|
|
}
|
|
|
|
if ScreenWidth-(bldCity+w) < 5 {
|
|
w += ScreenWidth - (bldCity + w)
|
|
}
|
|
b := building{startX: bldCity, width: w, height: h, color: c, windowsAt: screen.r.Intn(3)}
|
|
screen.Buildings = append(screen.Buildings, b)
|
|
bldCity += w
|
|
}
|
|
// Player 1 should be on the first 1/3 of the buildings
|
|
bld := screen.r.Intn(len(screen.Buildings) / 3)
|
|
p1x, p1y := screen.getRndPosForBuilding(bld)
|
|
screen.Player1 = createPlayer(p1x, p1y)
|
|
screen.Player1.gravity = screen.gravity
|
|
|
|
// Player 2 should be on the third 1/3 of the buildings
|
|
bld = screen.r.Intn(len(screen.Buildings)/3) + ((len(screen.Buildings) * 2) / 3)
|
|
p2x, p2y := screen.getRndPosForBuilding(bld)
|
|
if ScreenWidth-p2x < 5 {
|
|
p2x, p2y = screen.getRndPosForBuilding(bld - 1)
|
|
}
|
|
screen.Player2 = createPlayer(p2x, p2y)
|
|
screen.Player2.projAngle = 225
|
|
screen.Player2.gravity = screen.gravity
|
|
|
|
screen.PauseMenu = termboxUtil.CreateMenu("** GOPHER BATTLE **",
|
|
[]string{"Resume", "Restart", "Exit"},
|
|
(ScreenWidth/2)-11, (ScreenHeight/2)-3, 22, 6,
|
|
style.defaultFg, style.defaultBg)
|
|
screen.PauseMenu.SetBordered(true)
|
|
screen.PauseMenu.EnableVimMode()
|
|
screen.GameMode = modeRun
|
|
}
|
|
}
|
|
|
|
func createPlayer(px, py int) *player {
|
|
p := player{
|
|
x: px,
|
|
y: py,
|
|
projSpeed: 10,
|
|
projAngle: 315,
|
|
baseColor: termbox.ColorCyan,
|
|
}
|
|
return &p
|
|
}
|
|
|
|
// Trigger all updates
|
|
func (screen *mainScreen) update() {
|
|
if screen.GameMode == modeRun {
|
|
if !screen.animating {
|
|
if screen.PlayerTurn == 1 {
|
|
screen.PlayerTurn = 2
|
|
screen.Player1.turn = false
|
|
screen.Player2.turn = true
|
|
} else {
|
|
screen.PlayerTurn = 1
|
|
screen.Player1.turn = true
|
|
screen.Player2.turn = false
|
|
}
|
|
screen.GameMode = modeRunInput
|
|
} else {
|
|
if screen.Bullet.x > 0 && screen.Bullet.x < ScreenWidth && screen.Bullet.y < ScreenHeight {
|
|
screen.Bullet.update()
|
|
// Check for collisions
|
|
if screen.Player1.didCollide(screen.Bullet) {
|
|
screen.Player1.dead = true
|
|
screen.PauseMenu.SetOptionDisabled(0)
|
|
if screen.PlayerTurn == 1 {
|
|
// Self kill
|
|
screen.PauseMenu.SetTitle("Player 1 LOSES.")
|
|
} else {
|
|
// Victory!
|
|
screen.PauseMenu.SetTitle("Player 2 Wins!")
|
|
}
|
|
screen.GameMode = modePause
|
|
}
|
|
if screen.Player2.didCollide(screen.Bullet) {
|
|
screen.Player2.dead = true
|
|
screen.PauseMenu.SetOptionDisabled(0)
|
|
if screen.PlayerTurn == 2 {
|
|
// Self kill
|
|
screen.PauseMenu.SetTitle("Player 2 LOSES.")
|
|
} else {
|
|
// Victory!
|
|
screen.PauseMenu.SetTitle("Player 1 Wins!")
|
|
}
|
|
screen.GameMode = modePause
|
|
}
|
|
for i := range screen.Buildings {
|
|
if screen.Buildings[i].didCollide(screen.Bullet) {
|
|
// Collision
|
|
screen.animating = false
|
|
break
|
|
}
|
|
}
|
|
} else {
|
|
// Bullet went off the sides of the screen
|
|
screen.animating = false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (screen *mainScreen) drawScreen(style style) {
|
|
// Draw Landscape
|
|
var x int
|
|
for i := range screen.Buildings {
|
|
screen.Buildings[i].draw(x)
|
|
x += screen.Buildings[i].width
|
|
}
|
|
if screen.GameMode == modeRun {
|
|
// Draw Projectile(s)
|
|
if screen.Bullet != nil {
|
|
screen.Bullet.draw()
|
|
}
|
|
} else if screen.GameMode == modeRunInput {
|
|
// Draw inputs
|
|
if screen.PlayerTurn == 1 {
|
|
termboxUtil.DrawStringAtPoint(
|
|
"Angle: "+strconv.Itoa(screen.Player1.projAngle),
|
|
0, 0, style.defaultFg, style.defaultBg)
|
|
termboxUtil.DrawStringAtPoint(
|
|
"Speed: "+strconv.Itoa(screen.Player1.projSpeed),
|
|
0, 1, style.defaultFg, style.defaultBg)
|
|
termboxUtil.DrawStringAtPoint("'space' to fire", 0, 2, style.defaultFg, style.defaultBg)
|
|
} else {
|
|
termboxUtil.DrawStringAtPoint(
|
|
"Angle: "+strconv.Itoa(screen.Player2.projAngle),
|
|
ScreenWidth-11, 0, style.defaultFg, style.defaultBg)
|
|
termboxUtil.DrawStringAtPoint(
|
|
"Speed: "+strconv.Itoa(screen.Player2.projSpeed),
|
|
ScreenWidth-11, 1, style.defaultFg, style.defaultBg)
|
|
termboxUtil.DrawStringAtPoint("'space' to fire", ScreenWidth-15, 2, style.defaultFg, style.defaultBg)
|
|
}
|
|
}
|
|
|
|
// Draw Player 1
|
|
screen.Player1.draw()
|
|
// Draw Player 2
|
|
screen.Player2.draw()
|
|
|
|
if screen.GameMode == modePause {
|
|
// Draw the pause menu
|
|
screen.PauseMenu.Draw()
|
|
}
|
|
}
|
|
|
|
func (screen *mainScreen) getRndPosForBuilding(i int) (int, int) {
|
|
retY := ScreenHeight - screen.Buildings[i].height
|
|
bldStX := 0
|
|
for idx := 0; idx < i; idx++ {
|
|
bldStX += screen.Buildings[idx].width
|
|
}
|
|
retX := screen.r.Intn(screen.Buildings[i].width-5) + bldStX
|
|
return retX, retY
|
|
}
|
|
|
|
func getSeedFromString(seed string) int64 {
|
|
// Does this algorithm work for string->seed generation?
|
|
// ~\_(o.o)_/~ - seems to.
|
|
var ret int64
|
|
for i, k := range seed {
|
|
ret += int64(k) * int64(i)
|
|
}
|
|
return ret
|
|
}
|
|
|
|
func degToRad(d int) float64 {
|
|
return (float64(d) * (math.Pi / float64(180)))
|
|
}
|
|
|
|
type projectile struct {
|
|
x, y int
|
|
startX, startY int
|
|
speed, angle int
|
|
gravity float64
|
|
launchTime time.Time
|
|
movingUp bool
|
|
}
|
|
|
|
func (p *projectile) init(pl *player) {
|
|
p.launchTime = time.Now()
|
|
p.angle = pl.projAngle
|
|
p.speed = pl.projSpeed
|
|
if p.speed <= 5 {
|
|
p.angle = 270
|
|
p.speed = 10
|
|
p.startX = pl.x + 2
|
|
p.startY = pl.y - 5
|
|
} else {
|
|
p.startX, p.startY = pl.GetClosestTrajectory()
|
|
}
|
|
p.x = p.startX
|
|
p.y = p.startY
|
|
}
|
|
|
|
func (p *projectile) update() {
|
|
tm := time.Since(p.launchTime).Seconds()
|
|
useSpeed := float64(p.speed)
|
|
useX, useY := float64(p.startX), float64(p.startY)
|
|
useA := degToRad(p.angle)
|
|
lastY := p.y
|
|
p.x = int(useX + (useSpeed * math.Cos(useA) * tm))
|
|
p.y = int(useY + (useSpeed*math.Sin(useA)*tm - p.gravity*tm*tm/2))
|
|
p.movingUp = p.y < lastY
|
|
}
|
|
|
|
func (p *projectile) draw() {
|
|
if p.y < 0 {
|
|
if p.movingUp {
|
|
termbox.SetCell(p.x, 0, '^', termbox.ColorYellow, termbox.ColorBlack)
|
|
} else {
|
|
termbox.SetCell(p.x, 0, 'v', termbox.ColorYellow, termbox.ColorBlack)
|
|
}
|
|
} else {
|
|
termbox.SetCell(p.x, p.y, '0', termbox.ColorYellow, termbox.ColorBlack)
|
|
}
|
|
}
|
|
|
|
type player struct {
|
|
x, y int
|
|
projSpeed int
|
|
projAngle int
|
|
gravity float64
|
|
baseColor termbox.Attribute
|
|
turn bool
|
|
dead bool
|
|
}
|
|
|
|
func (p *player) draw() {
|
|
termbox.SetCell(p.x, p.y-4, ' ', p.baseColor, p.baseColor)
|
|
termbox.SetCell(p.x+1, p.y-4, ' ', p.baseColor, p.baseColor)
|
|
termbox.SetCell(p.x+2, p.y-4, ' ', p.baseColor, p.baseColor)
|
|
termbox.SetCell(p.x+3, p.y-4, ' ', p.baseColor, p.baseColor)
|
|
termbox.SetCell(p.x+4, p.y-4, ' ', p.baseColor, p.baseColor)
|
|
|
|
termbox.SetCell(p.x, p.y-3, ' ', p.baseColor, p.baseColor)
|
|
termbox.SetCell(p.x+2, p.y-3, ' ', p.baseColor, p.baseColor)
|
|
termbox.SetCell(p.x+4, p.y-3, ' ', p.baseColor, p.baseColor)
|
|
if p.dead {
|
|
termbox.SetCell(p.x+1, p.y-3, 'X', termbox.ColorRed, termbox.ColorBlack)
|
|
termbox.SetCell(p.x+3, p.y-3, 'X', termbox.ColorRed, termbox.ColorBlack)
|
|
} else {
|
|
termbox.SetCell(p.x+1, p.y-3, '@', termbox.ColorBlack, termbox.ColorWhite)
|
|
termbox.SetCell(p.x+3, p.y-3, '@', termbox.ColorBlack, termbox.ColorWhite)
|
|
}
|
|
|
|
termbox.SetCell(p.x, p.y-2, ' ', p.baseColor, p.baseColor)
|
|
termbox.SetCell(p.x+1, p.y-2, ' ', p.baseColor, p.baseColor)
|
|
termbox.SetCell(p.x+2, p.y-2, 'w', termbox.ColorWhite, p.baseColor)
|
|
termbox.SetCell(p.x+3, p.y-2, ' ', p.baseColor, p.baseColor)
|
|
termbox.SetCell(p.x+4, p.y-2, ' ', p.baseColor, p.baseColor)
|
|
|
|
termbox.SetCell(p.x, p.y-1, ' ', p.baseColor, p.baseColor)
|
|
termbox.SetCell(p.x+1, p.y-1, ' ', p.baseColor, p.baseColor)
|
|
termbox.SetCell(p.x+2, p.y-1, ' ', p.baseColor, p.baseColor)
|
|
termbox.SetCell(p.x+3, p.y-1, ' ', p.baseColor, p.baseColor)
|
|
termbox.SetCell(p.x+4, p.y-1, ' ', p.baseColor, p.baseColor)
|
|
|
|
termbox.SetCell(p.x, p.y, ' ', p.baseColor, p.baseColor)
|
|
termbox.SetCell(p.x+1, p.y, ' ', p.baseColor, p.baseColor)
|
|
termbox.SetCell(p.x+2, p.y, ' ', p.baseColor, p.baseColor)
|
|
termbox.SetCell(p.x+3, p.y, ' ', p.baseColor, p.baseColor)
|
|
termbox.SetCell(p.x+4, p.y, ' ', p.baseColor, p.baseColor)
|
|
|
|
// Now draw the target
|
|
if p.turn {
|
|
bulletHandX, bulletHandY := p.GetBulletHandPos()
|
|
if p.coordCollide(bulletHandX, bulletHandY) {
|
|
termbox.SetCell(bulletHandX, bulletHandY,
|
|
'!', termbox.ColorRed|termbox.AttrBold, termbox.ColorBlack)
|
|
} else {
|
|
termbox.SetCell(bulletHandX, bulletHandY,
|
|
'X', termbox.ColorRed|termbox.AttrBold, termbox.ColorBlack)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (p *player) coordCollide(x, y int) bool {
|
|
if x >= p.x && x <= p.x+4 {
|
|
return y <= p.y && y >= p.y-4
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (p *player) didCollide(pr *projectile) bool {
|
|
return p.coordCollide(pr.x, pr.y)
|
|
}
|
|
|
|
func (p *player) GetClosestTrajectory() (int, int) {
|
|
// Find the spot closest to the player in a direct line with the target
|
|
bHx, bHy := p.GetBulletHandPos()
|
|
gx, gy := p.x+2, p.y-2
|
|
for p.coordCollide(gx, gy) {
|
|
if math.Abs(float64(bHx-gx)) > math.Abs(float64(bHy-gy)) {
|
|
if bHx > gx {
|
|
gx++
|
|
} else {
|
|
gx--
|
|
}
|
|
} else {
|
|
if bHy > gy {
|
|
gy++
|
|
} else {
|
|
gy--
|
|
}
|
|
}
|
|
}
|
|
return gx, gy
|
|
}
|
|
|
|
func (p *player) GetBulletHandPos() (int, int) {
|
|
tm := 0.5
|
|
useSpeed := float64(p.projSpeed)
|
|
useX, useY := float64(p.x+2), float64(p.y-2)
|
|
useA := degToRad(p.projAngle)
|
|
retX := int(useX + (useSpeed * math.Cos(useA) * tm))
|
|
retY := int(useY + (useSpeed*math.Sin(useA)*tm - p.gravity*tm*tm/2))
|
|
return retX, retY
|
|
}
|
|
|
|
type building struct {
|
|
startX int
|
|
width, height int
|
|
color termbox.Attribute
|
|
windowsAt int
|
|
}
|
|
|
|
func (b *building) draw(x int) {
|
|
for i := 0; i < b.height; i++ {
|
|
for j := 0; j < b.width; j++ {
|
|
c := b.color
|
|
if b.width%2 == 1 {
|
|
// Individual windows
|
|
if i > 0 && i < b.height-1 && j > 0 && j < b.width-1 {
|
|
if i%3 == b.windowsAt && j%2 == 1 {
|
|
if i%5 == b.windowsAt || j%5 == b.windowsAt || (i+j)%5 == b.windowsAt {
|
|
c = termbox.ColorYellow
|
|
} else {
|
|
c = termbox.ColorBlack
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// Floor windows
|
|
if i > 0 && i < b.height-1 && j > 0 && j < b.width-1 {
|
|
if i%3 == b.windowsAt {
|
|
if i%4 == b.windowsAt {
|
|
c = termbox.ColorYellow
|
|
} else {
|
|
c = termbox.ColorBlack
|
|
}
|
|
}
|
|
}
|
|
}
|
|
termbox.SetCell(x+j, ScreenHeight-i, ' ', c, c)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (b *building) didCollide(p *projectile) bool {
|
|
if p.x >= b.startX && p.x <= b.startX+b.width {
|
|
return p.y >= ScreenHeight-b.height
|
|
}
|
|
return false
|
|
}
|