Files
pobounty/dashboard.go

468 lines
12 KiB
Go
Raw Normal View History

2023-09-17 13:34:03 -04:00
package main
import (
"bufio"
_ "embed"
"fmt"
"image/color"
"math"
2023-10-02 14:38:49 -04:00
"math/rand"
2023-09-17 13:34:03 -04:00
"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 (
2023-10-02 14:38:49 -04:00
DASH_WIDTH = 1920
DASH_HEIGHT = 1080
//color codes
DASH_COLOR_GRAD_TOP = 0x9601ce //0xff887c
DASH_COLOR_GRAD_BOTTOM = 0x4b007a //0xb6325f
DASH_COLOR_GRAD_ETOP = 0xffcc33
DASH_COLOR_GRAD_EBOTTOM = 0xcc9933
DASH_COLOR_GRAD_24TOP = 0xd42518
DASH_COLOR_GRAD_24BOT = 0x911416
//intervals and rates
DASH_SCORE_UPDATE_INTERVAL = 60 * 60 * 5 //in seconds, 60 updates per second, 60 seconds per minute, 5 minutes
DASH_TIMER_UPDATE_RATE = 3 //updates per frame
DASH_ANIMATIONS_PER_CYCLE = 12 //our effective animation rate
//offsets and scalers
DASH_TIMESTR_XOFFSET = 50
DASH_TIMESTR_YOFFSET = 40
DASH_WINSTR_XOFFSET = 80
DASH_WINSTR_YOFFSET = 55
DASH_SCORE_VERTICAL_OFFSET = 40 //pixels
DASH_REWARD_SCALE = 5
DASH_WINNER_CHARACTER_SPACING = 150
DASH_TIMER_XOFFSET_DIST = 500
DASH_POOMOJI_SIN_SCALER = 10
DASH_SCALE_FACTOR = 3 //pixels per team-points on results bar
DASH_DEBUG_OFFSET = 50
DASH_MAX_DEBUG_TARGET = 200
2023-09-17 13:34:03 -04:00
)
type TeamData struct {
TeamName string
Points int
}
type AllTeams struct {
Teams []TeamData
}
type Dashboard struct {
WindowTitle string
Width int
Height int
2023-10-02 14:38:49 -04:00
settings *DashSettings
2023-09-17 13:34:03 -04:00
//timer and animation related
tick int
deadline time.Time
timeleft time.Duration
expired bool
aps int //animation-cycles per second
endcondition bool
2023-10-02 14:38:49 -04:00
final24 bool
finalready bool
2023-09-17 13:34:03 -04:00
//dashboard elements and backgrounds
2023-10-02 14:38:49 -04:00
gradient *Gradient
//gradientImage *image.RGBA
2023-09-17 13:34:03 -04:00
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()
2023-10-02 14:38:49 -04:00
d.UpdateTimer()
d.UpdateScoreboardAsync()
d.HandleInputs()
d.tick++
return nil
}
// instantaneous inputs, bypassing timers
func (d *Dashboard) HandleInputs() {
if d.expired {
return
}
2023-09-17 13:34:03 -04:00
//END IMMEDIATELY
if inpututil.IsKeyJustPressed(ebiten.KeyE) {
d.deadline = time.Now()
}
2023-10-02 14:38:49 -04:00
//24 HOURS REMAINING MODE
if inpututil.IsKeyJustPressed(ebiten.KeyR) {
//find the time.Time just under 24 hours from now, then use this to compute delta
//between original deadline and new pre-24hour time, finally subtracting this duration
//from the original deadline to arrive at our new, just shy of 24hour deadline
timePlus24 := time.Now().Add(24*time.Hour + 5*time.Second)
newoffset := d.deadline.Sub(timePlus24)
d.deadline = d.deadline.Add(-newoffset)
}
2023-09-17 13:34:03 -04:00
//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) {
2023-10-02 14:38:49 -04:00
d.leaderboard.SetAllTargets(DASH_MAX_DEBUG_TARGET)
2023-09-17 13:34:03 -04:00
d.leaders = d.leaderboard.GetLeaders()
}
2023-10-02 14:38:49 -04:00
//CHOOSE AND SET NEW RANDOM GRADIENT COLORS FOR BG
if inpututil.IsKeyJustPressed(ebiten.KeyPeriod) {
d.gradient.SetColors(HexToRGBA(int(rand.Uint32())), HexToRGBA(int(rand.Uint32())))
2023-09-17 13:34:03 -04:00
}
2023-10-02 14:38:49 -04:00
//TRY AND RECONNECT TO JIRA
if inpututil.IsKeyJustPressed(ebiten.KeyJ) {
d.issuereader.Reconnect()
if d.issuereader.IsAvailable() {
go d.UpdateScoreboardAsync()
}
2023-09-17 13:34:03 -04:00
}
2023-10-02 14:38:49 -04:00
//FORCE DROP JIRA CONNECTION
if inpututil.IsKeyJustPressed(ebiten.KeyD) {
d.issuereader.DropClient()
}
2023-09-17 13:34:03 -04:00
2023-10-02 14:38:49 -04:00
}
func (d *Dashboard) UpdateTimer() {
//perform timer calculation if we're at the correct update interval
if d.tick%DASH_TIMER_UPDATE_RATE == 0 {
//except don't do it on weekends (since we're using working days weekends don't count)
isweekend := (time.Now().Weekday() == time.Sunday || time.Now().Weekday() == time.Saturday)
if !isweekend {
d.timeleft = time.Until(d.deadline)
d.expired = d.timeleft.Seconds() <= 0 //set expiry
}
}
}
// use thread to reload scores
func (d *Dashboard) UpdateScoreboardAsync() {
if d.tick%DASH_SCORE_UPDATE_INTERVAL == 0 && !d.endcondition {
go d.UpdateScoreboard()
}
2023-09-17 13:34:03 -04:00
}
// update score from jira actuals
func (d *Dashboard) UpdateScoreboard() {
2023-10-02 14:38:49 -04:00
if !d.issuereader.IsAvailable() {
2023-09-17 13:34:03 -04:00
return
}
2023-10-02 14:38:49 -04:00
if !d.endcondition {
2023-09-17 13:34:03 -04:00
scores := d.issuereader.RefreshScores()
2023-10-02 14:38:49 -04:00
//refresh could take a while, we may have actually ended in between
if !d.endcondition {
for k, v := range scores {
d.leaderboard.UpdateScoreForTeam(k, v)
}
d.leaders = d.leaderboard.GetLeaders()
2023-09-17 13:34:03 -04:00
}
}
}
func (d *Dashboard) Draw(screen *ebiten.Image) {
2023-10-02 14:38:49 -04:00
screen.DrawImage(d.gradient.Scene, nil)
2023-09-17 13:34:03 -04:00
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)
}
2023-10-02 14:38:49 -04:00
d.DrawWarnings(screen)
}
func (d *Dashboard) DrawWarnings(screen *ebiten.Image) {
if d.tick%90 < 45 {
var str string
if !d.issuereader.IsAvailable() {
str = "OFFLINE MODE"
}
if str != "" {
text.Draw(screen, str, DashFont.Debug, DASH_TIMESTR_YOFFSET, DASH_TIMESTR_YOFFSET, color.White)
}
}
2023-09-17 13:34:03 -04:00
}
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}
2023-10-02 14:38:49 -04:00
xpos := rpos.X - float64(d.reward.Width())/2*DASH_REWARD_SCALE
2023-09-17 13:34:03 -04:00
//animate the y position
incr := d.tick * 2
if incr < int(rpos.Y) {
ypos = float64(incr)
} else {
2023-10-02 14:38:49 -04:00
ypos = rpos.Y + math.Sin((float64(d.tick)-rpos.Y)/(math.Pi*4))*DASH_POOMOJI_SIN_SCALER
2023-09-17 13:34:03 -04:00
}
//adjust y offset to center
2023-10-02 14:38:49 -04:00
ypos = ypos - float64(d.reward.Width())/2*DASH_REWARD_SCALE
2023-09-17 13:34:03 -04:00
//apply transformation
op := &ebiten.DrawImageOptions{}
2023-10-02 14:38:49 -04:00
op.GeoM.Scale(DASH_REWARD_SCALE, DASH_REWARD_SCALE)
2023-09-17 13:34:03 -04:00
op.GeoM.Translate(xpos, ypos)
screen.DrawImage(d.reward.Sprite, op)
}
// draw winning characteres + scores
func (d *Dashboard) DrawWinners(screen *ebiten.Image) {
2023-10-02 14:38:49 -04:00
numleaders := len(d.leaders)
2023-09-17 13:34:03 -04:00
2023-10-02 14:38:49 -04:00
if numleaders > 0 {
text.Draw(screen, GetWinnerText(numleaders), 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()
2023-09-17 13:34:03 -04:00
2023-10-02 14:38:49 -04:00
mascot := d.winnerCharacters[i]
op := &ebiten.DrawImageOptions{}
2023-09-17 13:34:03 -04:00
2023-10-02 14:38:49 -04:00
totallen := numleaders * DASH_WINNER_CHARACTER_SPACING
2023-09-17 13:34:03 -04:00
2023-10-02 14:38:49 -04:00
xpos := d.Width/2 - totallen/2 + i*DASH_WINNER_CHARACTER_SPACING
ypos := d.Height * 2 / 3
2023-09-17 13:34:03 -04:00
2023-10-02 14:38:49 -04:00
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++
}
} else {
text.Draw(screen, "EVERYONE LOSES", DashFont.Debug, d.Width/2-DASH_WINSTR_XOFFSET*3/2, d.Height*2/3-DASH_WINSTR_YOFFSET, color.White)
2023-09-17 13:34:03 -04:00
}
}
// 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 {
2023-10-02 14:38:49 -04:00
d.gradient.SetColors(HexToRGBA(DASH_COLOR_GRAD_ETOP), HexToRGBA(DASH_COLOR_GRAD_EBOTTOM))
2023-09-17 13:34:03 -04:00
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()
2023-10-02 14:38:49 -04:00
//ok this bit is messy due to our lame workweek tracking, if we've got less than 5 days left, use the raw day count
//otherwise take into account business day tracking
days := math.Floor(float64(hoursRaw) / 24) //total number of days, including weekends
if days > 5 {
days = float64(GetWeekDaysUntil(d.deadline)) //weekdays only (not including holidays)
}
2023-09-17 13:34:03 -04:00
hours := int(hoursRaw) % 24
minutes := int(d.timeleft.Minutes()) % 60 //minutes per hour
seconds := int(d.timeleft.Seconds()) % 60 //seconds per minute
2023-10-02 14:38:49 -04:00
d.final24 = hoursRaw < 24
if d.final24 && !d.finalready {
d.leaderboard.Pinkify()
d.gradient.SetColors(HexToRGBA(DASH_COLOR_GRAD_24TOP), HexToRGBA(DASH_COLOR_GRAD_24BOT))
d.finalready = true
}
formattedTime := fmt.Sprintf("%.f:%02d:%02d:%02d", days, hours, minutes, seconds)
2023-09-17 13:34:03 -04:00
ypos := d.Height / 3
2023-10-02 14:38:49 -04:00
xpos := d.Width/2 - DASH_TIMER_XOFFSET_DIST
2023-09-17 13:34:03 -04:00
2023-10-02 14:38:49 -04:00
text.Draw(screen, formattedTime, DashFont.PrincipleTimer, xpos, ypos, color.White)
text.Draw(screen, GetTimerFormatString(), DashFont.Normal, d.Width/2-DASH_TIMESTR_XOFFSET, ypos+DASH_TIMESTR_YOFFSET, color.White)
2023-09-17 13:34:03 -04:00
}
func (d *Dashboard) DrawExpiration(screen *ebiten.Image) {
if d.tick%90 < 45 {
2023-10-02 14:38:49 -04:00
text.Draw(screen, d.settings.Labels.Completion, DashFont.Celebration, d.Width/2-600, d.Height/3, color.Black)
2023-09-17 13:34:03 -04:00
}
}
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
}
2023-10-02 14:38:49 -04:00
d.gradient.SetColors(d.gradColorTop, d.gradColorBottom)
2023-09-17 13:34:03 -04:00
}
func (d *Dashboard) SetTime(t time.Time) {
d.deadline = t
2023-10-02 14:38:49 -04:00
d.timeleft = time.Until(d.deadline)
2023-09-17 13:34:03 -04:00
}
// 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() {
2023-10-02 14:38:49 -04:00
2023-09-17 13:34:03 -04:00
r := bufio.NewReader(os.Stdin)
fmt.Print("Jira Username: ")
username, _ := r.ReadString('\n')
2023-10-02 14:38:49 -04:00
2023-09-17 13:34:03 -04:00
fmt.Print("Jira Password: ")
pass, _ := term.ReadPassword(int(syscall.Stdin))
2023-10-02 14:38:49 -04:00
2023-09-17 13:34:03 -04:00
password := string(pass)
2023-10-02 14:38:49 -04:00
d.issuereader = NewIssueReader(username, password, d.settings)
2023-09-17 13:34:03 -04:00
}
2023-10-02 14:38:49 -04:00
func NewDashboard(s *DashSettings) *Dashboard {
2023-09-17 13:34:03 -04:00
d := &Dashboard{
tick: 0,
2023-10-02 14:38:49 -04:00
WindowTitle: s.Labels.Title,
gradColorTop: HexToRGBA(DASH_COLOR_GRAD_TOP),
gradColorBottom: HexToRGBA(DASH_COLOR_GRAD_BOTTOM),
2023-09-17 13:34:03 -04:00
aps: DASH_ANIMATIONS_PER_CYCLE,
reward: NewReward(),
endcondition: false,
winnerCharacters: []*Character{},
2023-10-02 14:38:49 -04:00
gradient: NewGradient(DASH_WIDTH, DASH_HEIGHT),
final24: false,
finalready: false,
settings: s,
2023-09-17 13:34:03 -04:00
}
2023-10-02 14:38:49 -04:00
d.AddIssueReader()
2023-09-17 13:34:03 -04:00
2023-10-02 14:38:49 -04:00
d.SetTime(d.settings.Expiry)
d.leaderboard = NewLeaderboard(ASB_WIDTH, (len(d.settings.Teams)+1)*ASB_HEIGHT+ASB_BUFFER, d.settings.Teams)
2023-09-17 13:34:03 -04:00
d.leaderboard.SetScaleFactor(DASH_SCALE_FACTOR)
return d
}
2023-10-02 14:38:49 -04:00
func RunDashboard(s *DashSettings) {
if s == nil {
return
}
dash := NewDashboard(s)
dash.SetDimensions(DASH_WIDTH, DASH_HEIGHT)
2023-09-17 13:34:03 -04:00
ebiten.SetWindowSize(ebiten.ScreenSizeInFullscreen())
2023-10-02 14:38:49 -04:00
ebiten.SetWindowTitle(s.Labels.Title)
2023-09-17 13:34:03 -04:00
ebiten.SetFullscreen(true)
if err := ebiten.RunGame(dash); err != nil {
2023-10-02 14:38:49 -04:00
DashLogger.Fatal(err)
2023-09-17 13:34:03 -04:00
}
}