372 lines
9.2 KiB
Go
372 lines
9.2 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
_ "embed"
|
|
"fmt"
|
|
"image"
|
|
"image/color"
|
|
"log"
|
|
"math"
|
|
"os"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/hajimehoshi/ebiten/v2"
|
|
"github.com/hajimehoshi/ebiten/v2/inpututil"
|
|
"github.com/hajimehoshi/ebiten/v2/text"
|
|
"golang.org/x/term"
|
|
)
|
|
|
|
const (
|
|
DIMWIDTH = 1920
|
|
DIMHEIGHT = 1080
|
|
|
|
COLOR_GRAD_TOP = 0xff887c
|
|
COLOR_GRAD_BOTTOM = 0xb6325f
|
|
COLOR_GRAD_ETOP = 0xffcc33
|
|
COLOR_GRAD_EBOTTOM = 0xcc9933
|
|
|
|
SCORE_UPDATE_INTERVAL = 60 * 60 * 5 //in seconds, 60 updates per second, 60 seconds per minute, 5 minutes
|
|
SCORE_VERTICAL_OFFSET = 40 //pixels
|
|
REWARD_SCALE = 5
|
|
TIMER_UPDATE_RATE = 3 //updates per frame
|
|
WINNER_CHARACTER_SPACING = 150
|
|
TIMER_XOFFSET_DIST = 500
|
|
POOMOJI_SIN_SCALER = 10
|
|
DASH_SCALE_FACTOR = 3 //pixels per team-points on results bar
|
|
DASH_ANIMATIONS_PER_CYCLE = 12 //our effective animation rate
|
|
|
|
//various textual offset
|
|
DASH_TIMESTR_XOFFSET = 50
|
|
DASH_TIMESTR_YOFFSET = 40
|
|
DASH_DEBUG_OFFSET = 50
|
|
DASH_WINSTR_XOFFSET = 80
|
|
DASH_WINSTR_YOFFSET = 55
|
|
|
|
MAX_DEBUG_TARGET = 200
|
|
)
|
|
|
|
type TeamData struct {
|
|
TeamName string
|
|
Points int
|
|
}
|
|
|
|
type AllTeams struct {
|
|
Teams []TeamData
|
|
}
|
|
|
|
type Dashboard struct {
|
|
WindowTitle string
|
|
Width int
|
|
Height int
|
|
|
|
//timer and animation related
|
|
tick int
|
|
deadline time.Time
|
|
timeleft time.Duration
|
|
expired bool
|
|
aps int //animation-cycles per second
|
|
endcondition bool
|
|
|
|
//dashboard elements and backgrounds
|
|
gradientImage *image.RGBA
|
|
gradColorBottom color.RGBA
|
|
gradColorTop color.RGBA
|
|
reward *Reward
|
|
|
|
//functional/logical elements
|
|
leaderboard *Leaderboard
|
|
issuereader *IssueReader
|
|
leaders []*AnimatedScoreBar
|
|
winnerCharacters []*Character
|
|
}
|
|
|
|
func (d *Dashboard) Update() error {
|
|
|
|
//exit condition
|
|
if ebiten.IsKeyPressed(ebiten.KeyQ) {
|
|
os.Exit(0)
|
|
}
|
|
|
|
//perform animation rate update check
|
|
d.UpdateAnimationRate()
|
|
|
|
/********** instantaneous score updates, bypassing timer bypass timer *****************/
|
|
//END IMMEDIATELY
|
|
if inpututil.IsKeyJustPressed(ebiten.KeyE) {
|
|
d.deadline = time.Now()
|
|
}
|
|
|
|
//GENERATE RANDOM SCORES
|
|
if inpututil.IsKeyJustPressed(ebiten.KeyP) {
|
|
d.leaderboard.RandomizeTeamTargets()
|
|
d.leaders = d.leaderboard.GetLeaders()
|
|
}
|
|
|
|
//SET ALL TEAMS TO PREDETERMINED VALUE
|
|
if inpututil.IsKeyJustPressed(ebiten.KeyL) {
|
|
d.leaderboard.SetAllTargets(MAX_DEBUG_TARGET)
|
|
d.leaders = d.leaderboard.GetLeaders()
|
|
}
|
|
/**************************************************************************************/
|
|
|
|
//perform timer calculation
|
|
if d.tick%TIMER_UPDATE_RATE == 0 {
|
|
d.timeleft = time.Until(d.deadline)
|
|
}
|
|
|
|
//reload scores in new thread
|
|
if d.tick%SCORE_UPDATE_INTERVAL == 0 {
|
|
go d.UpdateScoreboard()
|
|
}
|
|
|
|
//have we expired?
|
|
d.expired = d.timeleft.Seconds() <= 0
|
|
d.tick++
|
|
|
|
return nil
|
|
}
|
|
|
|
// update score from jira actuals
|
|
func (d *Dashboard) UpdateScoreboard() {
|
|
|
|
if d.issuereader == nil {
|
|
return
|
|
}
|
|
|
|
if !d.expired {
|
|
scores := d.issuereader.RefreshScores()
|
|
for k, v := range scores {
|
|
d.leaderboard.UpdateScoreForTeam(k, v)
|
|
}
|
|
|
|
d.leaders = d.leaderboard.GetLeaders()
|
|
}
|
|
}
|
|
|
|
func (d *Dashboard) Draw(screen *ebiten.Image) {
|
|
screen.WritePixels(d.gradientImage.Pix)
|
|
d.DrawUpdate(screen)
|
|
|
|
//d.DrawDebug(screen)
|
|
}
|
|
|
|
func (d *Dashboard) DrawUpdate(screen *ebiten.Image) {
|
|
if d.expired {
|
|
d.DrawRewardScreen(screen)
|
|
d.DrawExpiration(screen)
|
|
} else {
|
|
d.RenderTimer(screen)
|
|
d.DrawLeaderboard(screen)
|
|
}
|
|
}
|
|
|
|
func (d *Dashboard) DrawLeaderboard(screen *ebiten.Image) {
|
|
op := &ebiten.DrawImageOptions{}
|
|
op.GeoM.Translate(200, float64(d.Height/2))
|
|
screen.DrawImage(d.leaderboard.Board, op)
|
|
if d.tick%(ebiten.TPS()/d.aps) == 0 {
|
|
d.leaderboard.Animate()
|
|
}
|
|
}
|
|
|
|
func (d *Dashboard) DrawRewardScreen(screen *ebiten.Image) {
|
|
|
|
d.IniitializeEndCondition()
|
|
d.DrawReward(screen)
|
|
d.DrawWinners(screen)
|
|
d.AnimateRewards()
|
|
}
|
|
|
|
func (d *Dashboard) DrawReward(screen *ebiten.Image) {
|
|
var ypos float64
|
|
//compute relative position and offset for reward
|
|
rpos := CharacterPosition{X: float64(d.Width) / 2, Y: float64(d.Height) / 2}
|
|
xpos := rpos.X - float64(d.reward.Width())/2*REWARD_SCALE
|
|
|
|
//animate the y position
|
|
incr := d.tick * 2
|
|
if incr < int(rpos.Y) {
|
|
ypos = float64(incr)
|
|
} else {
|
|
ypos = rpos.Y + math.Sin((float64(d.tick)-rpos.Y)/(math.Pi*4))*POOMOJI_SIN_SCALER
|
|
}
|
|
|
|
//adjust y offset to center
|
|
ypos = ypos - float64(d.reward.Width())/2*REWARD_SCALE
|
|
|
|
//apply transformation
|
|
op := &ebiten.DrawImageOptions{}
|
|
op.GeoM.Scale(REWARD_SCALE, REWARD_SCALE)
|
|
op.GeoM.Translate(xpos, ypos)
|
|
screen.DrawImage(d.reward.Sprite, op)
|
|
}
|
|
|
|
// draw winning characteres + scores
|
|
func (d *Dashboard) DrawWinners(screen *ebiten.Image) {
|
|
winnertext := GetWinnerText(len(d.leaders))
|
|
text.Draw(screen, winnertext, DashFont.Debug, d.Width/2-DASH_WINSTR_XOFFSET, d.Height*2/3-DASH_WINSTR_YOFFSET, color.White)
|
|
var i int = 0
|
|
for _, team := range d.leaders {
|
|
name := team.Name()
|
|
|
|
mascot := d.winnerCharacters[i]
|
|
op := &ebiten.DrawImageOptions{}
|
|
|
|
totallen := len(d.leaders) * WINNER_CHARACTER_SPACING
|
|
|
|
xpos := d.Width/2 - totallen/2 + i*WINNER_CHARACTER_SPACING
|
|
ypos := d.Height * 2 / 3
|
|
|
|
op.GeoM.Scale(2, 2)
|
|
op.GeoM.Translate(float64(xpos), float64(ypos))
|
|
screen.DrawImage(mascot.Sprite, op)
|
|
text.Draw(screen, name, DashFont.Normal, xpos, ypos+150, color.Black)
|
|
|
|
str := fmt.Sprintf("%0.fPTS", team.GetTarget())
|
|
text.Draw(screen, str, DashFont.Points, xpos, ypos+150+20, color.Black)
|
|
i++
|
|
}
|
|
}
|
|
|
|
// update reward animation cycles
|
|
func (d *Dashboard) AnimateRewards() {
|
|
if d.tick%(ebiten.TPS()/d.aps) == 0 {
|
|
d.reward.Animate()
|
|
for i := range d.leaders {
|
|
d.winnerCharacters[i].AnimateWinner()
|
|
}
|
|
}
|
|
}
|
|
|
|
// performs end condition settings exactly once per dashboard reset
|
|
func (d *Dashboard) IniitializeEndCondition() {
|
|
if !d.endcondition {
|
|
FillGradient(d.gradientImage, d.Width, d.Height, HexToRGBA(COLOR_GRAD_ETOP), HexToRGBA(COLOR_GRAD_EBOTTOM))
|
|
d.endcondition = true
|
|
d.tick = 0
|
|
|
|
d.winnerCharacters = d.winnerCharacters[:0]
|
|
var pidx int = 0
|
|
for _, team := range d.leaders {
|
|
d.winnerCharacters = append(d.winnerCharacters, NewCharacter(team.GetCharacterType()))
|
|
d.winnerCharacters[pidx].SetTargetCycles(CHARACTER_WINIMATION_CYCLES)
|
|
d.winnerCharacters[pidx].RandomizeCycleStart()
|
|
pidx++
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
func (d *Dashboard) DrawDebug(screen *ebiten.Image) {
|
|
x, y := ebiten.CursorPosition()
|
|
msg := fmt.Sprintf("(%d, %d )", x, y)
|
|
text.Draw(screen, msg, DashFont.Debug, DASH_DEBUG_OFFSET, d.Height-DASH_DEBUG_OFFSET, color.White)
|
|
}
|
|
|
|
func (d *Dashboard) RenderTimer(screen *ebiten.Image) {
|
|
|
|
hoursRaw := d.timeleft.Hours()
|
|
days := math.Floor(float64(hoursRaw) / 24)
|
|
hours := int(hoursRaw) % 24
|
|
minutes := int(d.timeleft.Minutes()) % 60 //minutes per hour
|
|
seconds := int(d.timeleft.Seconds()) % 60 //seconds per minute
|
|
|
|
formattedTime := fmt.Sprintf("%3.f:%02d:%02d:%02d", days, hours, minutes, seconds)
|
|
ypos := d.Height / 3
|
|
xpos := d.Width/2 - TIMER_XOFFSET_DIST
|
|
|
|
text.Draw(screen, formattedTime, DashFont.PrincipleTimer, xpos, ypos, color.Black)
|
|
text.Draw(screen, GetTimerFormatString(), DashFont.Normal, d.Width/2-DASH_TIMESTR_XOFFSET, ypos+DASH_TIMESTR_YOFFSET, color.Black)
|
|
}
|
|
|
|
func (d *Dashboard) DrawExpiration(screen *ebiten.Image) {
|
|
if d.tick%90 < 45 {
|
|
text.Draw(screen, GetCompletionText(), DashFont.Celebration, d.Width/2-600, d.Height/3, color.Black)
|
|
}
|
|
}
|
|
|
|
func (d *Dashboard) Layout(width, height int) (int, int) {
|
|
return d.Width, d.Height
|
|
}
|
|
|
|
func (d *Dashboard) SetDimensions(w, h int) {
|
|
if w > 0 {
|
|
d.Width = w
|
|
}
|
|
if h > 0 {
|
|
d.Height = h
|
|
}
|
|
|
|
d.gradientImage = image.NewRGBA(image.Rect(0, 0, w, h))
|
|
FillGradient(d.gradientImage, d.Width, d.Height, d.gradColorTop, d.gradColorBottom)
|
|
}
|
|
|
|
func (d *Dashboard) SetTime(t time.Time) {
|
|
d.deadline = t
|
|
}
|
|
|
|
// adjust animation rate based on keyboard input (up/down arrow keys)
|
|
func (d *Dashboard) UpdateAnimationRate() {
|
|
var keys []ebiten.Key
|
|
keys = inpututil.AppendJustPressedKeys(keys[0:])
|
|
for _, k := range keys {
|
|
switch k {
|
|
case ebiten.KeyArrowUp:
|
|
if d.aps < ebiten.TPS()-1 {
|
|
d.aps = d.aps + 1
|
|
}
|
|
case ebiten.KeyArrowDown:
|
|
if d.aps > 1 {
|
|
d.aps = d.aps - 1
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// adds a jira issue reader to the dashboard - prompts for credentials
|
|
func (d *Dashboard) AddIssueReader() {
|
|
r := bufio.NewReader(os.Stdin)
|
|
fmt.Print("Jira Username: ")
|
|
username, _ := r.ReadString('\n')
|
|
fmt.Print("Jira Password: ")
|
|
pass, _ := term.ReadPassword(int(syscall.Stdin))
|
|
password := string(pass)
|
|
d.issuereader = NewIssueReader(username, password)
|
|
}
|
|
|
|
func NewDashboard() *Dashboard {
|
|
d := &Dashboard{
|
|
tick: 0,
|
|
WindowTitle: GetDashboardTitle(),
|
|
gradColorTop: HexToRGBA(COLOR_GRAD_TOP),
|
|
gradColorBottom: HexToRGBA(COLOR_GRAD_BOTTOM),
|
|
aps: DASH_ANIMATIONS_PER_CYCLE,
|
|
reward: NewReward(),
|
|
endcondition: false,
|
|
winnerCharacters: []*Character{},
|
|
}
|
|
|
|
//d.AddIssueReader()
|
|
|
|
teamnames := GetTeamNames()
|
|
d.leaderboard = NewLeaderboard(ASB_WIDTH, (len(teamnames)+1)*ASB_HEIGHT+ASB_BUFFER, teamnames)
|
|
d.leaderboard.SetScaleFactor(DASH_SCALE_FACTOR)
|
|
|
|
return d
|
|
}
|
|
|
|
func RunDashboard(t time.Time) {
|
|
dash := NewDashboard()
|
|
ebiten.SetWindowSize(ebiten.ScreenSizeInFullscreen())
|
|
|
|
ebiten.SetWindowTitle(dash.WindowTitle)
|
|
dash.SetDimensions(DIMWIDTH, DIMHEIGHT)
|
|
ebiten.SetFullscreen(true)
|
|
dash.SetTime(t)
|
|
if err := ebiten.RunGame(dash); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|