228 lines
5.4 KiB
Go
228 lines
5.4 KiB
Go
package client
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
|
|
"hackerbots.us/server"
|
|
|
|
"github.com/nsf/termbox-go"
|
|
)
|
|
|
|
const d = 'v'
|
|
const u = '^'
|
|
const r = '>'
|
|
const l = '<'
|
|
|
|
// NewSpectator initializes and returns a *Spectator.
|
|
func NewSpectator(w, h float64) *Spectator {
|
|
return &Spectator{
|
|
StateStream: make(chan *server.Boardstate),
|
|
Die: make(chan struct{}),
|
|
width: w,
|
|
height: h,
|
|
}
|
|
}
|
|
|
|
// Spectator encodes a termbox ui.
|
|
type Spectator struct {
|
|
// max dimensions of field
|
|
width, height float64
|
|
|
|
// dimensions of the terminal window
|
|
viewX, viewY int
|
|
|
|
StateStream chan *server.Boardstate
|
|
|
|
// when closed will cause the Spectator to exit the render loop.
|
|
Die chan struct{}
|
|
}
|
|
|
|
// SetIDs is implemented so Spectator can be used as a client.Player.
|
|
func (s Spectator) SetIDs(map[string]string) {}
|
|
|
|
// GetStats is implemented so Spectator can be used as a client.Player.
|
|
func (s Spectator) GetStats() map[string]server.StatsRequest { return nil }
|
|
|
|
// Update is implemented so Spectator can be used as a client.Player.
|
|
func (s Spectator) Update(bs *server.Boardstate) map[string]server.Instruction {
|
|
s.StateStream <- bs
|
|
return nil
|
|
}
|
|
|
|
// Spectate runs the termbox render loop.
|
|
func (s *Spectator) Spectate() error {
|
|
err := termbox.Init()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
s.viewX, s.viewY = termbox.Size()
|
|
|
|
events := make(chan termbox.Event, 1024)
|
|
go func() {
|
|
for {
|
|
events <- termbox.PollEvent()
|
|
}
|
|
}()
|
|
|
|
termbox.HideCursor()
|
|
termbox.Clear(termbox.ColorBlack, termbox.ColorBlack)
|
|
|
|
func() {
|
|
for {
|
|
select {
|
|
case event := <-events:
|
|
switch event.Type {
|
|
case termbox.EventKey:
|
|
switch event.Key {
|
|
case termbox.KeyCtrlZ, termbox.KeyCtrlC:
|
|
return
|
|
}
|
|
|
|
switch event.Ch {
|
|
case 'q':
|
|
return
|
|
case 'f':
|
|
termbox.SetCell(
|
|
20,
|
|
20,
|
|
'*',
|
|
termbox.ColorRed, termbox.ColorBlack,
|
|
)
|
|
case 'c':
|
|
termbox.Clear(termbox.ColorBlack, termbox.ColorBlack)
|
|
}
|
|
|
|
case termbox.EventResize:
|
|
s.viewX, s.viewY = event.Width, event.Height
|
|
case termbox.EventError:
|
|
err = fmt.Errorf("Quitting because of termbox error:\n%v\n", event.Err)
|
|
return
|
|
}
|
|
case update := <-s.StateStream:
|
|
termbox.Clear(termbox.ColorBlack, termbox.ColorBlack)
|
|
|
|
for _, obstacle := range update.Obstacles {
|
|
startX := int((obstacle.Bounds.A.X / s.width) * float64(s.viewX))
|
|
stopX := int((obstacle.Bounds.B.X / s.width) * float64(s.viewX))
|
|
startY := int((obstacle.Bounds.A.Y / s.height) * float64(s.viewY))
|
|
stopY := int((obstacle.Bounds.B.Y / s.height) * float64(s.viewY))
|
|
for x := startX; x < stopX; x++ {
|
|
for y := startY; y < stopY; y++ {
|
|
termbox.SetCell(
|
|
x,
|
|
s.viewY-y,
|
|
' ',
|
|
termbox.ColorBlack, termbox.ColorBlue,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, bot := range update.OtherRobots {
|
|
x := int((bot.Position.X / s.width) * float64(s.viewX))
|
|
y := int((bot.Position.Y / s.height) * float64(s.viewY))
|
|
var b rune
|
|
if math.Abs(bot.Heading.X) > math.Abs(bot.Heading.Y) {
|
|
if bot.Heading.X > 0 {
|
|
b = r
|
|
} else {
|
|
b = l
|
|
}
|
|
} else {
|
|
if bot.Heading.Y > 0 {
|
|
b = u
|
|
} else {
|
|
b = d
|
|
}
|
|
}
|
|
c := termbox.ColorRed
|
|
if bot.Health <= 0 {
|
|
c = termbox.ColorBlack
|
|
}
|
|
termbox.SetCell(
|
|
x,
|
|
s.viewY-y,
|
|
b,
|
|
c, termbox.ColorBlack,
|
|
)
|
|
}
|
|
|
|
for _, p := range update.Projectiles {
|
|
x := int((p.Position.X / s.width) * float64(s.viewX))
|
|
y := int((p.Position.Y / s.height) * float64(s.viewY))
|
|
termbox.SetCell(
|
|
x,
|
|
s.viewY-y,
|
|
'·',
|
|
termbox.ColorWhite|termbox.AttrBold, termbox.ColorBlack,
|
|
)
|
|
}
|
|
|
|
for _, splosion := range update.Splosions {
|
|
startX := int(((splosion.Position.X - float64(splosion.Radius)) / s.width) * float64(s.viewX))
|
|
startY := int(((splosion.Position.Y - float64(splosion.Radius)) / s.height) * float64(s.viewY))
|
|
stopX := int(((splosion.Position.X+float64(splosion.Radius))/s.width)*float64(s.viewX)) + 1
|
|
stopY := int(((splosion.Position.Y+float64(splosion.Radius))/s.height)*float64(s.viewY)) + 1
|
|
for x := startX; x < stopX; x++ {
|
|
for y := startY; y < stopY; y++ {
|
|
realX := float64(x) * s.width / float64(s.viewX)
|
|
realY := float64(y) * s.height / float64(s.viewY)
|
|
dX := realX - splosion.Position.X
|
|
dY := realY - splosion.Position.Y
|
|
curRad := math.Sqrt(dX*dX + dY*dY)
|
|
if curRad < float64(splosion.Radius) {
|
|
termbox.SetCell(
|
|
x,
|
|
s.viewY-y,
|
|
'·',
|
|
termbox.ColorYellow|termbox.AttrBold, termbox.ColorRed,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, bot := range update.MyRobots {
|
|
x := int((bot.Position.X / s.width) * float64(s.viewX))
|
|
y := int((bot.Position.Y / s.height) * float64(s.viewY))
|
|
var b rune
|
|
if math.Abs(bot.Heading.X) > math.Abs(bot.Heading.Y) {
|
|
if bot.Heading.X > 0 {
|
|
b = r
|
|
} else {
|
|
b = l
|
|
}
|
|
} else {
|
|
if bot.Heading.Y > 0 {
|
|
b = u
|
|
} else {
|
|
b = d
|
|
}
|
|
}
|
|
c := termbox.ColorWhite
|
|
if bot.Health <= 0 {
|
|
c = termbox.ColorBlack
|
|
}
|
|
termbox.SetCell(
|
|
x,
|
|
s.viewY-y,
|
|
b,
|
|
c|termbox.AttrBold, termbox.ColorBlack,
|
|
)
|
|
}
|
|
|
|
err = termbox.Flush()
|
|
case <-s.Die:
|
|
err = errors.New("was told to die")
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
termbox.Close()
|
|
return err
|
|
}
|