First commit >:]
200
asb.go
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
_ "embed"
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
"log"
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
|
"github.com/hajimehoshi/ebiten/v2/text"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ASB_HEIGHT = 50
|
||||||
|
ASB_BUFFER = 10
|
||||||
|
ASB_WIDTH = 1400
|
||||||
|
ASB_COLOR = 0x5d2f6a
|
||||||
|
ASB_MARGIN = 10
|
||||||
|
|
||||||
|
ASB_GRAD_TOP = 0x9933cc
|
||||||
|
ASB_GRAD_BOTTOM = 0x5d2f6a
|
||||||
|
|
||||||
|
ASB_FLAME_SCALE = 1.56
|
||||||
|
ASB_FLAME_OFFSET = 15
|
||||||
|
|
||||||
|
ASB_MASK_WIDTH = 10
|
||||||
|
ASB_MASK_HEIGHT = ASB_HEIGHT
|
||||||
|
|
||||||
|
ASB_STRFADE_XOFFSET = 80
|
||||||
|
ASB_STRFADE_YOFFSET = 50
|
||||||
|
ASB_STRFADE_VALUE = 0x50
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
//go:embed resources/images/barmask.png
|
||||||
|
barmaskAsset_png []byte
|
||||||
|
assetsBarmask *ebiten.Image
|
||||||
|
)
|
||||||
|
|
||||||
|
type AnimatedScoreBar struct {
|
||||||
|
ScoreImage *ebiten.Image
|
||||||
|
name string
|
||||||
|
target float64
|
||||||
|
value float64
|
||||||
|
gradient *image.RGBA
|
||||||
|
character *Character
|
||||||
|
flames *Flame
|
||||||
|
barbuffer *ebiten.Image
|
||||||
|
order int //ranking
|
||||||
|
sf float64 //scaling factor
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAnimatedScoreBar(t CharacterType, s string) *AnimatedScoreBar {
|
||||||
|
a := &AnimatedScoreBar{
|
||||||
|
name: s,
|
||||||
|
target: 0,
|
||||||
|
value: 0,
|
||||||
|
sf: 1,
|
||||||
|
ScoreImage: ebiten.NewImage(ASB_WIDTH, ASB_HEIGHT),
|
||||||
|
barbuffer: ebiten.NewImage(ASB_WIDTH, ASB_HEIGHT),
|
||||||
|
character: NewCharacter(t),
|
||||||
|
flames: NewFlame(),
|
||||||
|
order: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
a.character.RandomizeCycleStart()
|
||||||
|
|
||||||
|
//fill bar gradient
|
||||||
|
a.gradient = image.NewRGBA(image.Rect(0, 0, ASB_WIDTH, ASB_HEIGHT))
|
||||||
|
FillGradient(a.gradient, a.barbuffer.Bounds().Dx(), a.barbuffer.Bounds().Dy(), HexToRGBA(ASB_GRAD_TOP), HexToRGBA(ASB_GRAD_BOTTOM))
|
||||||
|
a.barbuffer.WritePixels(a.gradient.Pix)
|
||||||
|
|
||||||
|
//mask start of the bar for smooth edges
|
||||||
|
op := &ebiten.DrawImageOptions{}
|
||||||
|
op.Blend = ebiten.BlendDestinationOut
|
||||||
|
a.barbuffer.DrawImage(assetsBarmask.SubImage(image.Rect(0, 0, ASB_MASK_WIDTH, ASB_MASK_HEIGHT)).(*ebiten.Image), op)
|
||||||
|
|
||||||
|
//add name to the fill bar, which will gradually display as the value increases
|
||||||
|
text.Draw(a.barbuffer, a.name, DashFont.TeamBarName, ASB_MARGIN, ASB_HEIGHT-ASB_MARGIN, color.White)
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AnimatedScoreBar) GetCharacterType() CharacterType {
|
||||||
|
return a.character.GetType()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AnimatedScoreBar) Name() string {
|
||||||
|
return a.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AnimatedScoreBar) SetTarget(t float64) {
|
||||||
|
a.target = t
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AnimatedScoreBar) GetTarget() float64 {
|
||||||
|
return a.target
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AnimatedScoreBar) GetValue() float64 {
|
||||||
|
return a.value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AnimatedScoreBar) Animate() {
|
||||||
|
|
||||||
|
a.ScoreImage.Clear()
|
||||||
|
|
||||||
|
a.AddFadedTeamName()
|
||||||
|
a.AddScoreBar()
|
||||||
|
a.AddCharacter()
|
||||||
|
a.AddTextScore()
|
||||||
|
a.AdjustValue()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AnimatedScoreBar) AddFadedTeamName() {
|
||||||
|
//alpha blended black for the team name
|
||||||
|
c := HexToRGBA(0x000000)
|
||||||
|
c.A = ASB_STRFADE_VALUE
|
||||||
|
text.Draw(a.ScoreImage, a.name, DashFont.TeamBackgroundName, ASB_STRFADE_XOFFSET, ASB_STRFADE_YOFFSET, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AnimatedScoreBar) SetOrder(order int) {
|
||||||
|
a.order = order
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AnimatedScoreBar) GetOrder() int {
|
||||||
|
return a.order
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw our score bar by using subset of full bar
|
||||||
|
func (a *AnimatedScoreBar) AddScoreBar() {
|
||||||
|
//ss = subset start, se = subset end
|
||||||
|
ssx := 0
|
||||||
|
ssy := 0
|
||||||
|
sex := int(a.value * a.sf)
|
||||||
|
sey := ASB_HEIGHT
|
||||||
|
barSubImage := a.barbuffer.SubImage(image.Rect(ssx, ssy, sex, sey)).(*ebiten.Image)
|
||||||
|
a.ScoreImage.DrawImage(barSubImage, nil)
|
||||||
|
|
||||||
|
//add trailing image mask to round out the edges
|
||||||
|
op := &ebiten.DrawImageOptions{}
|
||||||
|
op.Blend = ebiten.BlendDestinationOut
|
||||||
|
op.GeoM.Translate(float64(sex)-ASB_MASK_WIDTH, float64(sey)-ASB_MASK_HEIGHT)
|
||||||
|
|
||||||
|
msx := ASB_MASK_WIDTH
|
||||||
|
msy := 0
|
||||||
|
mex := ASB_MASK_WIDTH + ASB_MASK_WIDTH
|
||||||
|
mey := ASB_MASK_HEIGHT
|
||||||
|
a.ScoreImage.DrawImage(assetsBarmask.SubImage(image.Rect(msx, msy, mex, mey)).(*ebiten.Image), op)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AnimatedScoreBar) AddCharacter() {
|
||||||
|
|
||||||
|
op := &ebiten.DrawImageOptions{}
|
||||||
|
|
||||||
|
//if we're on the move, let's add some flair to the character
|
||||||
|
if a.target != a.value {
|
||||||
|
op.GeoM.Scale(ASB_FLAME_SCALE, ASB_FLAME_SCALE)
|
||||||
|
op.GeoM.Rotate(-math.Pi / 4)
|
||||||
|
op.GeoM.Translate(a.value*a.sf+ASB_BUFFER-ASB_FLAME_OFFSET*5/2, ASB_FLAME_OFFSET*3/2)
|
||||||
|
a.ScoreImage.DrawImage(a.flames.Sprite, op)
|
||||||
|
a.flames.Animate()
|
||||||
|
}
|
||||||
|
|
||||||
|
op = &ebiten.DrawImageOptions{}
|
||||||
|
op.GeoM.Translate(a.value*a.sf+ASB_BUFFER, 0)
|
||||||
|
a.ScoreImage.DrawImage(a.character.Sprite, op)
|
||||||
|
a.character.Animate()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AnimatedScoreBar) AddTextScore() {
|
||||||
|
tx := int(a.value*a.sf) + ASB_BUFFER + a.character.GetWidth()
|
||||||
|
ty := ASB_HEIGHT
|
||||||
|
text.Draw(a.ScoreImage, fmt.Sprintf("%0.f", a.value), DashFont.Title, tx, ty, color.White)
|
||||||
|
text.Draw(a.ScoreImage, "POINTS", DashFont.Title, tx+ASB_BUFFER*7, ty, color.White)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AnimatedScoreBar) AdjustValue() {
|
||||||
|
if a.value < a.target {
|
||||||
|
a.value++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AnimatedScoreBar) SetScaleFactor(sf float64) {
|
||||||
|
a.sf = sf
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AnimatedScoreBar) Reset() {
|
||||||
|
a.value = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
img, _, err := image.Decode(bytes.NewReader(barmaskAsset_png))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
assetsBarmask = ebiten.NewImageFromImage(img)
|
||||||
|
}
|
||||||
148
characters.go
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
_ "embed"
|
||||||
|
"image"
|
||||||
|
_ "image/png"
|
||||||
|
"log"
|
||||||
|
"math/rand"
|
||||||
|
|
||||||
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CharacterType uint
|
||||||
|
|
||||||
|
const (
|
||||||
|
WhiteCharacter CharacterType = 0
|
||||||
|
PinkCharacter CharacterType = 1
|
||||||
|
BlueCharacter CharacterType = 2
|
||||||
|
GreenCharacter CharacterType = 3
|
||||||
|
BlackCharacter CharacterType = 4
|
||||||
|
OrangeCharacter CharacterType = 5
|
||||||
|
RedCharacter CharacterType = 6
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c CharacterType) IsValid() bool {
|
||||||
|
result := false
|
||||||
|
switch c {
|
||||||
|
case WhiteCharacter, PinkCharacter, BlueCharacter, GreenCharacter, BlackCharacter, OrangeCharacter, RedCharacter:
|
||||||
|
result = true
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
CHARACTER_NUMTYPES = 7
|
||||||
|
CHARACTER_WIDTH = 50
|
||||||
|
CHARACTER_HEIGHT = 50
|
||||||
|
|
||||||
|
CHARACTER_ANIMATION_CYCLES = 6
|
||||||
|
CHARACTER_WINIMATION_CYCLES = 8
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
//go:embed resources/images/idle.png
|
||||||
|
assets_png []byte
|
||||||
|
assetsImage *ebiten.Image
|
||||||
|
|
||||||
|
//go:embed resources/images/win.png
|
||||||
|
winnerAssets_png []byte
|
||||||
|
assetsWinner *ebiten.Image
|
||||||
|
)
|
||||||
|
|
||||||
|
type CharacterPosition struct {
|
||||||
|
X float64
|
||||||
|
Y float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type Character struct {
|
||||||
|
Sprite *ebiten.Image //our principle sprite representing the character
|
||||||
|
chartype CharacterType //character type, determines offset position to use from asset image
|
||||||
|
cycle int //current animation cycle for the character
|
||||||
|
targetcycles int //specified animation cycle
|
||||||
|
pos CharacterPosition //character coordinates
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Character) RandomizeCycleStart() {
|
||||||
|
c.cycle = rand.Int() % c.targetcycles
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCharacter(t CharacterType) *Character {
|
||||||
|
c := &Character{
|
||||||
|
Sprite: ebiten.NewImage(CHARACTER_WIDTH, CHARACTER_HEIGHT),
|
||||||
|
pos: CharacterPosition{X: 0, Y: 0},
|
||||||
|
targetcycles: CHARACTER_ANIMATION_CYCLES,
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.IsValid() {
|
||||||
|
c.chartype = t
|
||||||
|
} else {
|
||||||
|
c.chartype = WhiteCharacter
|
||||||
|
}
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Character) SetTargetCycles(cycles int) {
|
||||||
|
c.targetcycles = cycles
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Character) CycleUpdate() {
|
||||||
|
c.cycle = (c.cycle + 1) % c.targetcycles
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Character) Animate() {
|
||||||
|
c.AnimateFromTarget(assetsImage)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Character) AnimateWinner() {
|
||||||
|
c.AnimateFromTarget(assetsWinner)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Character) AnimateFromTarget(source *ebiten.Image) {
|
||||||
|
//compute start and end location of asset to use for animation frame cycle
|
||||||
|
sx := CHARACTER_WIDTH * c.cycle
|
||||||
|
sy := CHARACTER_HEIGHT * int(c.chartype)
|
||||||
|
ex := CHARACTER_WIDTH * (c.cycle + 1)
|
||||||
|
ey := CHARACTER_HEIGHT * (int(c.chartype) + 1)
|
||||||
|
|
||||||
|
c.Sprite.Clear()
|
||||||
|
c.Sprite.DrawImage(source.SubImage(image.Rect(sx, sy, ex, ey)).(*ebiten.Image), nil)
|
||||||
|
|
||||||
|
c.CycleUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Character) SetPosition(p CharacterPosition) {
|
||||||
|
c.pos = p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Character) Position() CharacterPosition {
|
||||||
|
return c.pos
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Character) GetWidth() int {
|
||||||
|
return c.Sprite.Bounds().Dx()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Character) GetHeight() int {
|
||||||
|
return c.Sprite.Bounds().Dy()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Character) GetType() CharacterType {
|
||||||
|
return c.chartype
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
img, _, err := image.Decode(bytes.NewReader(assets_png))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
assetsImage = ebiten.NewImageFromImage(img)
|
||||||
|
|
||||||
|
img, _, err = image.Decode(bytes.NewReader(winnerAssets_png))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
assetsWinner = ebiten.NewImageFromImage(img)
|
||||||
|
}
|
||||||
41
constants.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
const (
|
||||||
|
CUSTOM_FIELD_TEAM = "customfield_14580"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetDeadlineResourcePath() string {
|
||||||
|
return "resources/settings/deadline.txt"
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetDashboardTitle() string {
|
||||||
|
return "PI-8 DASHBOARD"
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetTeamNames() []string {
|
||||||
|
return []string{"devops", "gaia", "igaluk", "orion", "pulsar", "solaris", "supernova"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetPOList() []string {
|
||||||
|
return []string{"dbenavid", "el076320", "em031838", "ktsui", "ma031420"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetBountyJQL() string {
|
||||||
|
return `project = GERSSW AND ("Epic Link" =GERSSW-4971 OR "Epic Link" =GERSSW-3759) AND Status = Accepted`
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetCompletionText() string {
|
||||||
|
return "PHASE B COMPLETE"
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetTimerFormatString() string {
|
||||||
|
return "D:H:M:S"
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetWinnerText(qty int) string {
|
||||||
|
str := "OUR WINNER"
|
||||||
|
if qty > 1 {
|
||||||
|
str = str + "S"
|
||||||
|
}
|
||||||
|
return str
|
||||||
|
}
|
||||||
371
dashboard.go
Normal file
@@ -0,0 +1,371 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
69
flame.go
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
_ "embed"
|
||||||
|
"image"
|
||||||
|
_ "image/png"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
FLAME_ANIMATION_CYCLES = 8
|
||||||
|
FLAME_WIDTH = 24
|
||||||
|
FLAME_HEIGHT = 32
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
//go:embed resources/images/hot.png
|
||||||
|
flameAssets_png []byte
|
||||||
|
assetsFlame *ebiten.Image
|
||||||
|
)
|
||||||
|
|
||||||
|
type Flame struct {
|
||||||
|
Sprite *ebiten.Image //our principle sprite representing the flame
|
||||||
|
cycle int //current animation cycle for the character
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFlame() *Flame {
|
||||||
|
return &Flame{
|
||||||
|
cycle: 0,
|
||||||
|
Sprite: ebiten.NewImage(FLAME_WIDTH, FLAME_HEIGHT),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Flame) CycleUpdate() {
|
||||||
|
f.cycle = (f.cycle + 1) % CHARACTER_ANIMATION_CYCLES
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Flame) Animate() {
|
||||||
|
|
||||||
|
//compute start and end location of asset to use for animation frame cycle
|
||||||
|
sx := FLAME_WIDTH * f.cycle
|
||||||
|
sy := 0
|
||||||
|
ex := FLAME_WIDTH * (f.cycle + 1)
|
||||||
|
ey := FLAME_HEIGHT
|
||||||
|
|
||||||
|
f.Sprite.Clear()
|
||||||
|
f.Sprite.DrawImage(assetsFlame.SubImage(image.Rect(sx, sy, ex, ey)).(*ebiten.Image), nil)
|
||||||
|
|
||||||
|
f.CycleUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Flame) GetWidth() int {
|
||||||
|
return f.Sprite.Bounds().Dx()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Flame) GetHeight() int {
|
||||||
|
return f.Sprite.Bounds().Dy()
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
img, _, err := image.Decode(bytes.NewReader(flameAssets_png))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
assetsFlame = ebiten.NewImageFromImage(img)
|
||||||
|
}
|
||||||
129
fonts.go
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/hajimehoshi/ebiten/v2/examples/resources/fonts"
|
||||||
|
"golang.org/x/image/font"
|
||||||
|
"golang.org/x/image/font/opentype"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
DashFont *FontBase
|
||||||
|
|
||||||
|
//go:embed resources/fonts/bahnschrift.ttf
|
||||||
|
banschrift_ttf []byte
|
||||||
|
)
|
||||||
|
|
||||||
|
type FontBase struct {
|
||||||
|
PrincipleTimer font.Face
|
||||||
|
Title font.Face
|
||||||
|
Normal font.Face
|
||||||
|
Expiry font.Face
|
||||||
|
Debug font.Face
|
||||||
|
Celebration font.Face
|
||||||
|
Points font.Face
|
||||||
|
TeamBackgroundName font.Face
|
||||||
|
TeamBarName font.Face
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
|
||||||
|
const dpi = 72
|
||||||
|
DashFont = &FontBase{}
|
||||||
|
tt, err := opentype.ParseCollection(banschrift_ttf)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
//fmt.Printf("%d\n", tt.NumFonts())
|
||||||
|
|
||||||
|
fnt, _ := tt.Font(0)
|
||||||
|
DashFont.PrincipleTimer, err = opentype.NewFace(fnt, &opentype.FaceOptions{
|
||||||
|
Size: 200,
|
||||||
|
DPI: dpi,
|
||||||
|
Hinting: font.HintingVertical,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
DashFont.Expiry, err = opentype.NewFace(fnt, &opentype.FaceOptions{
|
||||||
|
Size: 120,
|
||||||
|
DPI: dpi,
|
||||||
|
Hinting: font.HintingVertical,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
DashFont.Title, err = opentype.NewFace(fnt, &opentype.FaceOptions{
|
||||||
|
Size: 40,
|
||||||
|
DPI: dpi,
|
||||||
|
Hinting: font.HintingVertical,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
DashFont.Normal, err = opentype.NewFace(fnt, &opentype.FaceOptions{
|
||||||
|
Size: 20,
|
||||||
|
DPI: dpi,
|
||||||
|
Hinting: font.HintingVertical,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
DashFont.Points, err = opentype.NewFace(fnt, &opentype.FaceOptions{
|
||||||
|
Size: 15,
|
||||||
|
DPI: dpi,
|
||||||
|
Hinting: font.HintingVertical,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
p2tt, err := opentype.Parse(fonts.PressStart2P_ttf)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
DashFont.Celebration, err = opentype.NewFace(p2tt, &opentype.FaceOptions{
|
||||||
|
Size: 72,
|
||||||
|
DPI: dpi,
|
||||||
|
Hinting: font.HintingVertical,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
DashFont.TeamBackgroundName, err = opentype.NewFace(p2tt, &opentype.FaceOptions{
|
||||||
|
Size: 40,
|
||||||
|
DPI: dpi,
|
||||||
|
Hinting: font.HintingVertical,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
DashFont.TeamBarName, err = opentype.NewFace(p2tt, &opentype.FaceOptions{
|
||||||
|
Size: 25,
|
||||||
|
DPI: dpi,
|
||||||
|
Hinting: font.HintingVertical,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
DashFont.Debug, err = opentype.NewFace(p2tt, &opentype.FaceOptions{
|
||||||
|
Size: 15,
|
||||||
|
DPI: dpi,
|
||||||
|
Hinting: font.HintingVertical,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
26
go.mod
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
module pobounty
|
||||||
|
|
||||||
|
go 1.21.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/andygrunwald/go-jira v1.16.0
|
||||||
|
github.com/hajimehoshi/ebiten/v2 v2.5.6
|
||||||
|
golang.org/x/image v0.6.0
|
||||||
|
golang.org/x/term v0.5.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/ebitengine/purego v0.4.0 // indirect
|
||||||
|
github.com/fatih/structs v1.1.0 // indirect
|
||||||
|
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b // indirect
|
||||||
|
github.com/golang-jwt/jwt/v4 v4.4.2 // indirect
|
||||||
|
github.com/google/go-querystring v1.1.0 // indirect
|
||||||
|
github.com/jezek/xgb v1.1.0 // indirect
|
||||||
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
|
github.com/trivago/tgo v1.0.7 // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 // indirect
|
||||||
|
golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c // indirect
|
||||||
|
golang.org/x/sync v0.1.0 // indirect
|
||||||
|
golang.org/x/sys v0.7.0 // indirect
|
||||||
|
golang.org/x/text v0.8.0 // indirect
|
||||||
|
)
|
||||||
43
gradient.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
)
|
||||||
|
|
||||||
|
func HexToRGBA(hexcolor int) color.RGBA {
|
||||||
|
return color.RGBA{
|
||||||
|
R: uint8(hexcolor >> 0x10 & 0xff),
|
||||||
|
G: uint8(hexcolor >> 0x08 & 0xff),
|
||||||
|
B: uint8(hexcolor >> 0x00 & 0xff),
|
||||||
|
A: 0xff,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// performs linear gradient fill on target image for the background from top to bottom colors
|
||||||
|
func FillGradient(target *image.RGBA, width int, height int, top, bottom color.RGBA) {
|
||||||
|
|
||||||
|
for y := 0; y < height; y++ {
|
||||||
|
|
||||||
|
//the percent of our transition informs our blending
|
||||||
|
percentIn := float64(y) / float64(height)
|
||||||
|
|
||||||
|
//compute top and bottom blend values as factor of percentage
|
||||||
|
pTopR := float64(top.R) * (1 - percentIn)
|
||||||
|
pTopG := float64(top.G) * (1 - percentIn)
|
||||||
|
pTopB := float64(top.B) * (1 - percentIn)
|
||||||
|
|
||||||
|
pBottomR := float64(bottom.R) * (percentIn)
|
||||||
|
pBottomG := float64(bottom.G) * (percentIn)
|
||||||
|
pBottomB := float64(bottom.B) * (percentIn)
|
||||||
|
|
||||||
|
//perform blending on per pixel row basis
|
||||||
|
for x := 0; x < width; x++ {
|
||||||
|
pixelIndex := 4 * (width*y + x)
|
||||||
|
target.Pix[pixelIndex] = uint8(pTopR + pBottomR)
|
||||||
|
target.Pix[pixelIndex+1] = uint8(pTopG + pBottomG)
|
||||||
|
target.Pix[pixelIndex+2] = uint8(pTopB + pBottomB)
|
||||||
|
target.Pix[pixelIndex+3] = 0xff
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
163
issuereader.go
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/andygrunwald/go-jira"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
JIRA_URL = "https://jirawms.mda.ca"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IssueReader struct {
|
||||||
|
auth jira.BasicAuthTransport //auth data
|
||||||
|
client *jira.Client //jira client
|
||||||
|
score map[string]int //score information
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIssueReader(user, pass string) *IssueReader {
|
||||||
|
ir := &IssueReader{
|
||||||
|
auth: jira.BasicAuthTransport{
|
||||||
|
Username: strings.TrimSpace(user),
|
||||||
|
Password: strings.TrimSpace(pass),
|
||||||
|
},
|
||||||
|
score: make(map[string]int),
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
ir.client, err = jira.NewClient(ir.auth.Client(), strings.TrimSpace(JIRA_URL))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ir
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *IssueReader) GetScores() map[string]int {
|
||||||
|
return i.score
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *IssueReader) ZeroScores() {
|
||||||
|
for k := range i.score {
|
||||||
|
i.score[k] = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *IssueReader) RefreshScores() map[string]int {
|
||||||
|
|
||||||
|
i.ZeroScores()
|
||||||
|
|
||||||
|
poIssues, er := i.GetAllIssues(i.client, GetBountyJQL())
|
||||||
|
if er != nil {
|
||||||
|
log.Fatal(er)
|
||||||
|
}
|
||||||
|
|
||||||
|
//run through each to check if we've got bounty points to award
|
||||||
|
for _, issue := range poIssues {
|
||||||
|
teamId := i.ParseCustomTeamField(issue.Fields.Unknowns[CUSTOM_FIELD_TEAM])
|
||||||
|
i.ExtractScoreFromComments(teamId, issue.Key)
|
||||||
|
}
|
||||||
|
|
||||||
|
return i.GetScores()
|
||||||
|
}
|
||||||
|
|
||||||
|
// the team field is one of type string after a non-intuitive map decomposition, assert on interface
|
||||||
|
func (i *IssueReader) ParseCustomTeamField(teamField interface{}) string {
|
||||||
|
var str string = ""
|
||||||
|
if teamField != nil {
|
||||||
|
str = teamField.([]interface{})[0].(map[string]interface{})["value"].(string)
|
||||||
|
}
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
// given a teamid and jira issue, validate potential PO bounty scores
|
||||||
|
func (i *IssueReader) ExtractScoreFromComments(teamId string, key string) {
|
||||||
|
if i.IsValidTeam(teamId) {
|
||||||
|
is, _, err := i.client.Issue.Get(key, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
comments := is.Fields.Comments.Comments
|
||||||
|
for _, c := range comments {
|
||||||
|
if i.CommentAuthorIsPO(c) {
|
||||||
|
bp := i.ExtractBountyFromComment(c)
|
||||||
|
i.score[teamId] = i.score[teamId] + bp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if s exists in Scrum Teams list
|
||||||
|
func (i *IssueReader) IsValidTeam(s string) bool {
|
||||||
|
for _, t := range GetTeamNames() {
|
||||||
|
if strings.Compare(strings.ToLower(s), strings.ToLower(t)) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// given a jira.Comment, find and extract the associated PO Bounty points
|
||||||
|
func (i *IssueReader) ExtractBountyFromComment(c *jira.Comment) int {
|
||||||
|
result := 0
|
||||||
|
re := regexp.MustCompile(`POB: (\d+)`)
|
||||||
|
if c != nil {
|
||||||
|
matches := re.FindStringSubmatch(c.Body)
|
||||||
|
|
||||||
|
//we're expecting exactly one principle match and one submatch (2 entries total)
|
||||||
|
if len(matches) == 2 {
|
||||||
|
//submatch must be an integer, or else we simply don't update the result
|
||||||
|
a, err := strconv.Atoi(matches[1])
|
||||||
|
if err == nil {
|
||||||
|
result = a
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// given a specific jira.Comment, find if it was authored by someone int he POList
|
||||||
|
func (i *IssueReader) CommentAuthorIsPO(c *jira.Comment) bool {
|
||||||
|
if c != nil {
|
||||||
|
for _, p := range GetPOList() {
|
||||||
|
if c.Author.Name == p {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// retrieve all issues as per searchString JQL
|
||||||
|
func (i *IssueReader) GetAllIssues(client *jira.Client, searchString string) ([]jira.Issue, error) {
|
||||||
|
last := 0
|
||||||
|
var issues []jira.Issue
|
||||||
|
for {
|
||||||
|
opt := &jira.SearchOptions{
|
||||||
|
MaxResults: 1000, // Max results can go up to 1000
|
||||||
|
StartAt: last,
|
||||||
|
}
|
||||||
|
|
||||||
|
chunk, resp, err := client.Issue.Search(searchString, opt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
total := resp.Total
|
||||||
|
if issues == nil {
|
||||||
|
issues = make([]jira.Issue, 0, total)
|
||||||
|
}
|
||||||
|
issues = append(issues, chunk...)
|
||||||
|
last = resp.StartAt + len(chunk)
|
||||||
|
if last >= total {
|
||||||
|
return issues, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
87
leaderboard.go
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
MAX_RANDOM_INCREMENT = 50
|
||||||
|
)
|
||||||
|
|
||||||
|
type Leaderboard struct {
|
||||||
|
Board *ebiten.Image //the team board target image
|
||||||
|
teams map[string]*AnimatedScoreBar //collection of teams + score bars
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLeaderboard(w, h int, teamnames []string) *Leaderboard {
|
||||||
|
t := &Leaderboard{}
|
||||||
|
t.Board = ebiten.NewImage(w, h)
|
||||||
|
t.teams = make(map[string]*AnimatedScoreBar)
|
||||||
|
|
||||||
|
for i, name := range teamnames {
|
||||||
|
cleanname := strings.ToUpper(name)
|
||||||
|
t.teams[cleanname] = NewAnimatedScoreBar(CharacterType(i%CHARACTER_NUMTYPES), strings.ToUpper(name))
|
||||||
|
t.teams[cleanname].SetOrder(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Leaderboard) SetScaleFactor(sf float64) {
|
||||||
|
for _, team := range t.teams {
|
||||||
|
team.SetScaleFactor(sf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Leaderboard) UpdateScoreForTeam(name string, score int) {
|
||||||
|
t := l.teams[strings.ToUpper(name)]
|
||||||
|
if t != nil {
|
||||||
|
t.SetTarget(float64(score))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Leaderboard) GetLeaders() []*AnimatedScoreBar {
|
||||||
|
leaders := []*AnimatedScoreBar{}
|
||||||
|
|
||||||
|
highScore := 0
|
||||||
|
|
||||||
|
for _, team := range l.teams {
|
||||||
|
teamscore := int(team.GetTarget())
|
||||||
|
if teamscore > highScore {
|
||||||
|
leaders = leaders[:0]
|
||||||
|
leaders = append(leaders, team)
|
||||||
|
highScore = teamscore
|
||||||
|
} else if teamscore == highScore {
|
||||||
|
leaders = append(leaders, team)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return leaders
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Leaderboard) Animate() {
|
||||||
|
|
||||||
|
t.Board.Clear()
|
||||||
|
for _, v := range t.teams {
|
||||||
|
op := &ebiten.DrawImageOptions{}
|
||||||
|
op.GeoM.Translate(0, float64(v.GetOrder())*(ASB_HEIGHT+ASB_BUFFER))
|
||||||
|
t.Board.DrawImage(v.ScoreImage, op)
|
||||||
|
v.Animate()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Leaderboard) SetAllTargets(t float64) {
|
||||||
|
for _, board := range l.teams {
|
||||||
|
board.SetTarget(t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Leaderboard) RandomizeTeamTargets() {
|
||||||
|
for _, board := range l.teams {
|
||||||
|
board.SetTarget(board.GetTarget() + float64(rand.Int()%MAX_RANDOM_INCREMENT))
|
||||||
|
}
|
||||||
|
}
|
||||||
5
main.go
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
RunDashboard(LoadTime())
|
||||||
|
}
|
||||||
BIN
resources/fonts/arcade_n.ttf
Normal file
BIN
resources/fonts/bahnschrift.ttf
Normal file
BIN
resources/images/barmask.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
resources/images/char2.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
resources/images/gold.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
resources/images/hot.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
resources/images/idle.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
resources/images/shiny.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
resources/images/win.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
1
resources/settings/deadline.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
2023-11-24T04:00:00.000Z
|
||||||
104
reward.go
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
_ "embed"
|
||||||
|
"image"
|
||||||
|
_ "image/png"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
REWARD_HEIGHT = 32
|
||||||
|
REWARD_WIDTH = 32
|
||||||
|
|
||||||
|
SHINY_ANIMATION_CYCLES = 9
|
||||||
|
SHINY_ROW_INDEX = 11
|
||||||
|
SPARKLE_ROW_INDEX = 7
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
//go:embed resources/images/gold.png
|
||||||
|
assetsReward_png []byte
|
||||||
|
rewardsImage *ebiten.Image
|
||||||
|
|
||||||
|
//go:embed resources/images/shiny.png
|
||||||
|
assetsShiny_png []byte
|
||||||
|
shinyImage *ebiten.Image
|
||||||
|
)
|
||||||
|
|
||||||
|
type Reward struct {
|
||||||
|
Sprite *ebiten.Image
|
||||||
|
scratch *ebiten.Image
|
||||||
|
cycle int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewReward() *Reward {
|
||||||
|
r := &Reward{
|
||||||
|
cycle: 0,
|
||||||
|
}
|
||||||
|
r.Sprite = ebiten.NewImageFromImage(rewardsImage)
|
||||||
|
r.scratch = ebiten.NewImageFromImage(rewardsImage)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Reward) Animate() {
|
||||||
|
|
||||||
|
r.scratch.Clear()
|
||||||
|
r.Sprite.Clear()
|
||||||
|
|
||||||
|
r.scratch.DrawImage(rewardsImage.SubImage(image.Rect(0, 0, REWARD_WIDTH, REWARD_HEIGHT)).(*ebiten.Image), nil)
|
||||||
|
|
||||||
|
if r.cycle < SHINY_ANIMATION_CYCLES {
|
||||||
|
//add sheen
|
||||||
|
op := &ebiten.DrawImageOptions{}
|
||||||
|
op.Blend = ebiten.BlendSourceAtop
|
||||||
|
|
||||||
|
sx := r.cycle * REWARD_WIDTH
|
||||||
|
sy := REWARD_HEIGHT * SHINY_ROW_INDEX
|
||||||
|
|
||||||
|
ex := sx + REWARD_WIDTH
|
||||||
|
ey := sy + REWARD_HEIGHT
|
||||||
|
|
||||||
|
r.scratch.DrawImage(shinyImage.SubImage(image.Rect(sx, sy, ex, ey)).(*ebiten.Image), op)
|
||||||
|
|
||||||
|
//add sparkle
|
||||||
|
spsy := REWARD_HEIGHT * SPARKLE_ROW_INDEX
|
||||||
|
spey := spsy + REWARD_HEIGHT
|
||||||
|
op.GeoM.Translate(REWARD_WIDTH/2, REWARD_HEIGHT/2)
|
||||||
|
r.scratch.DrawImage(shinyImage.SubImage(image.Rect(sx, spsy, ex, spey)).(*ebiten.Image), nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Sprite.DrawImage(r.scratch, nil)
|
||||||
|
r.CycleUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Reward) CycleUpdate() {
|
||||||
|
r.cycle = (r.cycle + 1) % (SHINY_ANIMATION_CYCLES * 8)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Reward) Width() int {
|
||||||
|
return REWARD_WIDTH
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Reward) Height() int {
|
||||||
|
return REWARD_HEIGHT
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
|
||||||
|
img, _, err := image.Decode(bytes.NewReader(assetsReward_png))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
rewardsImage = ebiten.NewImageFromImage(img)
|
||||||
|
|
||||||
|
img, _, err = image.Decode(bytes.NewReader(assetsShiny_png))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
shinyImage = ebiten.NewImageFromImage(img)
|
||||||
|
|
||||||
|
}
|
||||||
23
timer.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func LoadTime() time.Time {
|
||||||
|
|
||||||
|
data, err := os.ReadFile(GetDeadlineResourcePath())
|
||||||
|
if err != nil {
|
||||||
|
data = make([]byte, 1)
|
||||||
|
data[0] = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
deadline, err := time.Parse(time.RFC3339, string(data))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return deadline
|
||||||
|
}
|
||||||