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
|
||||
}
|
||||