From 4d1f10502ac44a23cb1d57fd2dcf77ca687513d9 Mon Sep 17 00:00:00 2001 From: Knut Ahlers Date: Wed, 28 Aug 2019 14:07:37 +0200 Subject: [PATCH] Initial version --- go.mod | 11 +++ go.sum | 33 +++++++ levels.go | 285 +++++++++++++++++++++++++++++++++++++++++++++++++++++ main.go | 215 ++++++++++++++++++++++++++++++++++++++++ metrics.go | 172 ++++++++++++++++++++++++++++++++ skill.go | 167 +++++++++++++++++++++++++++++++ 6 files changed, 883 insertions(+) create mode 100644 go.mod create mode 100644 go.sum create mode 100644 levels.go create mode 100644 main.go create mode 100644 metrics.go create mode 100644 skill.go diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..ebb9427 --- /dev/null +++ b/go.mod @@ -0,0 +1,11 @@ +module github.com/Luzifer/runemetrics + +go 1.12 + +require ( + github.com/Luzifer/rconfig/v2 v2.2.1 + github.com/gizak/termui/v3 v3.1.0 + github.com/gorhill/cronexpr v0.0.0-20180427100037-88b0669f7d75 + github.com/pkg/errors v0.8.1 + github.com/sirupsen/logrus v1.4.2 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..4651d74 --- /dev/null +++ b/go.sum @@ -0,0 +1,33 @@ +github.com/Luzifer/rconfig/v2 v2.2.1 h1:zcDdLQlnlzwcBJ8E0WFzOkQE1pCMn3EbX0dFYkeTczg= +github.com/Luzifer/rconfig/v2 v2.2.1/go.mod h1:OKIX0/JRZrPJ/ZXXWklQEFXA6tBfWaljZbW37w+sqBw= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gizak/termui/v3 v3.1.0 h1:ZZmVDgwHl7gR7elfKf1xc4IudXZ5qqfDh4wExk4Iajc= +github.com/gizak/termui/v3 v3.1.0/go.mod h1:bXQEBkJpzxUAKf0+xq9MSWAvWZlE7c+aidmyFlkYTrY= +github.com/gorhill/cronexpr v0.0.0-20180427100037-88b0669f7d75 h1:f0n1xnMSmBLzVfsMMvriDyA75NB/oBgILX2GcHXIQzY= +github.com/gorhill/cronexpr v0.0.0-20180427100037-88b0669f7d75/go.mod h1:g2644b03hfBX9Ov0ZBDgXXens4rxSxmqFBbhvKv2yVA= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/mattn/go-runewidth v0.0.2 h1:UnlwIPBGaTZfPQ6T1IGzPI0EkYAQmT9fAEJ/poFC63o= +github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM= +github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= +github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d h1:x3S6kxmy49zXVVyhcnrFqxvNVCBPb2KZ9hV2RBdS840= +github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/validator.v2 v2.0.0-20180514200540-135c24b11c19 h1:WB265cn5OpO+hK3pikC9hpP1zI/KTwmyMFKloW9eOVc= +gopkg.in/validator.v2 v2.0.0-20180514200540-135c24b11c19/go.mod h1:o4V0GXN9/CAmCsvJ0oXYZvrZOe7syiDZSN1GWGZTGzc= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/levels.go b/levels.go new file mode 100644 index 0000000..0b66ffb --- /dev/null +++ b/levels.go @@ -0,0 +1,285 @@ +package main + +var levels = map[int]int64{ + 1: 0, + 2: 83, + 3: 174, + 4: 276, + 5: 388, + 6: 512, + 7: 650, + 8: 801, + 9: 969, + 10: 1154, + 11: 1358, + 12: 1584, + 13: 1833, + 14: 2107, + 15: 2411, + 16: 2746, + 17: 3115, + 18: 3523, + 19: 3973, + 20: 4470, + 21: 5018, + 22: 5624, + 23: 6291, + 24: 7028, + 25: 7842, + 26: 8740, + 27: 9730, + 28: 10824, + 29: 12031, + 30: 13363, + 31: 14833, + 32: 16456, + 33: 18247, + 34: 20224, + 35: 22406, + 36: 24815, + 37: 27473, + 38: 30408, + 39: 33648, + 40: 37224, + 41: 41171, + 42: 45529, + 43: 50339, + 44: 55649, + 45: 61512, + 46: 67983, + 47: 75127, + 48: 83014, + 49: 91721, + 50: 101333, + 51: 111945, + 52: 123660, + 53: 136594, + 54: 150872, + 55: 166636, + 56: 184040, + 57: 203254, + 58: 224466, + 59: 247886, + 60: 273742, + 61: 302288, + 62: 333804, + 63: 368599, + 64: 407015, + 65: 449428, + 66: 496254, + 67: 547953, + 68: 605032, + 69: 668051, + 70: 737627, + 71: 814445, + 72: 899257, + 73: 992895, + 74: 1096278, + 75: 1210421, + 76: 1336443, + 77: 1475581, + 78: 1629200, + 79: 1798808, + 80: 1986068, + 81: 2192818, + 82: 2421087, + 83: 2673114, + 84: 2951373, + 85: 3258594, + 86: 3597792, + 87: 3972294, + 88: 4385776, + 89: 4842295, + 90: 5346332, + 91: 5902831, + 92: 6517253, + 93: 7195629, + 94: 7944614, + 95: 8771558, + 96: 9684577, + 97: 10692629, + 98: 11805606, + 99: 13034431, + 100: 14391160, + 101: 15889109, + 102: 17542976, + 103: 19368992, + 104: 21385073, + 105: 23611006, + 106: 26068632, + 107: 28782069, + 108: 31777943, + 109: 35085654, + 110: 38737661, + 111: 42769801, + 112: 47221641, + 113: 52136869, + 114: 57563718, + 115: 63555443, + 116: 70170840, + 117: 77474828, + 118: 85539082, + 119: 94442737, + 120: 104273167, + 121: 115126838, + 122: 127110260, + 123: 140341028, + 124: 154948977, + 125: 171077457, + 126: 188884740, + 127: 2e8, +} + +var masterLevels = map[int]int64{ + 1: 0, + 2: 830, + 3: 1861, + 4: 2902, + 5: 3980, + 6: 5126, + 7: 6380, + 8: 7787, + 9: 9400, + 10: 11275, + 11: 13605, + 12: 16372, + 13: 19656, + 14: 23546, + 15: 28134, + 16: 33520, + 17: 39809, + 18: 47109, + 19: 55535, + 20: 65209, + 21: 77190, + 22: 90811, + 23: 106221, + 24: 123573, + 25: 143025, + 26: 164742, + 27: 188893, + 28: 215651, + 29: 245196, + 30: 277713, + 31: 316311, + 32: 358547, + 33: 404634, + 34: 454796, + 35: 509259, + 36: 568254, + 37: 632019, + 38: 700797, + 39: 774834, + 40: 854383, + 41: 946227, + 42: 1044569, + 43: 1149696, + 44: 1261903, + 45: 1381488, + 46: 1508756, + 47: 1644015, + 48: 1787581, + 49: 1939773, + 50: 2100917, + 51: 2283490, + 52: 2476369, + 53: 2679917, + 54: 2894505, + 55: 3120508, + 56: 3358307, + 57: 3608290, + 58: 3870846, + 59: 4146374, + 60: 4435275, + 61: 4758122, + 62: 5096111, + 63: 5449685, + 64: 5819299, + 65: 6205407, + 66: 6608473, + 67: 7028964, + 68: 7467354, + 69: 7924122, + 70: 8399751, + 71: 8925664, + 72: 9472665, + 73: 10041285, + 74: 10632061, + 75: 11245538, + 76: 11882262, + 77: 12542789, + 78: 13227679, + 79: 13937496, + 80: 14672812, + 81: 15478994, + 82: 16313404, + 83: 17176661, + 84: 18069395, + 85: 18992239, + 86: 19945833, + 87: 20930821, + 88: 21947856, + 89: 22997593, + 90: 24080695, + 91: 25259906, + 92: 26475754, + 93: 27728955, + 94: 29020233, + 95: 30350318, + 96: 31719944, + 97: 33129852, + 98: 34580790, + 99: 36073511, + 100: 37608773, + 101: 39270442, + 102: 40978509, + 103: 42733789, + 104: 44537107, + 105: 46389292, + 106: 48291180, + 107: 50243611, + 108: 52247435, + 109: 54303504, + 110: 56412678, + 111: 58575824, + 112: 60793812, + 113: 63067521, + 114: 65397835, + 115: 67785643, + 116: 70231841, + 117: 72737330, + 118: 75303019, + 119: 77929820, + 120: 80618654, + 121: 83370445, + 122: 86186124, + 123: 89066630, + 124: 92012904, + 125: 95025896, + 126: 98106559, + 127: 101255855, + 128: 104474750, + 129: 107764216, + 130: 111125230, + 131: 114558777, + 132: 118065845, + 133: 121647430, + 134: 125304532, + 135: 129038159, + 136: 132849323, + 137: 136739041, + 138: 140708338, + 139: 144758242, + 140: 148889790, + 141: 153104021, + 142: 157401983, + 143: 161784728, + 144: 166253312, + 145: 170808801, + 146: 175452262, + 147: 180184770, + 148: 185007406, + 149: 189921255, + 150: 194927409, + 151: 2e8, +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..d565b7d --- /dev/null +++ b/main.go @@ -0,0 +1,215 @@ +package main + +import ( + "fmt" + "os" + "strconv" + "strings" + "time" + + ui "github.com/gizak/termui/v3" + "github.com/gizak/termui/v3/widgets" + "github.com/gorhill/cronexpr" + log "github.com/sirupsen/logrus" + + "github.com/Luzifer/rconfig/v2" +) + +const ( + updateKeyTotalXP = "total_xp" + updateKeyGeneral = "general" + updateKeyFeed = "feed" +) + +var ( + cfg = struct { + MarkerTime time.Duration `flag:"marker-time" default:"30m" description:"How long to highlight new entries"` + Update string `flag:"update" default:"* * * * *" description:"When to fetch metrics (cron syntax)"` + LogLevel string `flag:"log-level" default:"info" description:"Log level (debug, info, warn, error, fatal)"` + VersionAndExit bool `flag:"version" default:"false" description:"Prints current version and exits"` + }{} + + lastUpdate = map[string]time.Time{} + + version = "dev" +) + +func init() { + if err := rconfig.ParseAndValidate(&cfg); err != nil { + log.Fatalf("Unable to parse commandline options: %s", err) + } + + if cfg.VersionAndExit { + fmt.Printf("git-changerelease %s\n", version) + os.Exit(0) + } + + if l, err := log.ParseLevel(cfg.LogLevel); err != nil { + log.WithError(err).Fatal("Unable to parse log level") + } else { + log.SetLevel(l) + } +} + +func main() { + var err error + + if len(rconfig.Args()) != 2 { + log.Fatal("Usage: runemetrics ") + } + + if playerInfoCache, err = loadPlayerInfoCache(); err != nil { + log.WithError(err).Fatal("Unable to load cache") + } + + if err = ui.Init(); err != nil { + log.WithError(err).Fatal("Unable to initialize termui") + } + defer ui.Close() + + var ( + cron = cronexpr.MustParse(cfg.Update) + player = rconfig.Args()[1] + updateTicker = time.NewTimer(0) + ) + + updateUI(player) + + for { + select { + + case evt := <-ui.PollEvents(): + switch evt.ID { + + case "q", "": + return + + case "": + updateTicker.Reset(0) + + case "": + ui.Clear() + updateUI(player) + + } + + case <-updateTicker.C: + if err := updateUI(player); err != nil { + log.WithError(err).Error("Unable to update metrics") + return + } + updateTicker.Reset(time.Until(cron.Next(time.Now()))) + + if err := playerInfoCache.storeCache(); err != nil { + log.WithError(err).Error("Unable to write cache") + } + + } + } +} + +func updateUI(player string) error { + termWidth, termHeight := ui.TerminalDimensions() + + playerData, err := getPlayerInfo(player, 20) + + // Status-bar + status := widgets.NewParagraph() + status.Title = "Status" + status.Text = fmt.Sprintf("Last Refresh: %s | XP Change: %s | Feed Change: %s", + lastUpdate[updateKeyGeneral].Format("15:04:05"), + lastUpdate[updateKeyTotalXP].Format("15:04:05"), + lastUpdate[updateKeyFeed].Format("15:04:05"), + ) + status.SetRect(0, termHeight-3, termWidth, termHeight) + defer ui.Render(status) + + if err != nil { + status.Text = fmt.Sprintf("Unable to get player info: %s", err.Error()) + status.BorderStyle.Fg = ui.ColorRed + return nil + } + + // Header + hdrText := widgets.NewParagraph() + hdrText.Title = "Player" + hdrText.Text = playerData.Name + hdrText.SetRect(0, 0, termWidth, 3) + ui.Render(hdrText) + + // General stats + combatLevel := widgets.NewParagraph() + combatLevel.Title = "Combat Level" + combatLevel.Text = strconv.Itoa(playerData.CombatLevel) + + totalXP := widgets.NewParagraph() + totalXP.Title = "Total XP" + totalXP.Text = strconv.FormatInt(playerData.TotalXP, 10) + + totalLevel := widgets.NewParagraph() + totalLevel.Title = "Total Level" + totalLevel.Text = strconv.FormatInt(playerData.TotalSkill, 10) + + rank := widgets.NewParagraph() + rank.Title = "Rank" + rank.Text = strconv.FormatInt(playerData.NumericRank(), 10) + + statsGrid := ui.NewGrid() + statsGrid.SetRect(0, 3, termWidth, 6) + statsGrid.Set( + ui.NewRow(1.0, + ui.NewCol(1.0/4, combatLevel), + ui.NewCol(1.0/4, totalXP), + ui.NewCol(1.0/4, totalLevel), + ui.NewCol(1.0/4, rank), + ), + ) + ui.Render(statsGrid) + + // Levels + levelTable := widgets.NewTable() + levelTable.Title = "Levels" + levelTable.TextAlignment = ui.AlignRight + levelTable.RowStyles[0] = ui.Style{Fg: ui.ColorWhite, Modifier: ui.ModifierBold} + levelTable.SetRect(0, 6, termWidth, 6+2+len(playerData.SkillValues)+1) + levelTable.RowSeparator = false + levelTable.Rows = [][]string{{"Skill", "Level", "Level %", "XP", "XP to next Level"}} + for i, s := range playerData.SkillValues { + levelTable.Rows = append(levelTable.Rows, []string{ + s.ID.String(), + strconv.Itoa(s.Level), + strconv.FormatFloat(s.ID.Info().LevelPercentage(s.Level, s.XP/10), 'f', 1, 64), + strconv.FormatInt(s.XP/10, 10), + strconv.FormatInt(s.ID.Info().XPToNextLevel(s.Level, s.XP/10), 10), + }) + + if time.Since(s.Updated) < cfg.MarkerTime { + levelTable.RowStyles[i+1] = ui.Style{Fg: ui.ColorGreen} + } + } + ui.Render(levelTable) + + // Latest events + events := widgets.NewTable() + events.Title = "Event Log" + events.RowSeparator = false + events.ColumnWidths = []int{12, termWidth - 3 - 12} + events.SetRect(0, 6+2+len(playerData.SkillValues)+1, termWidth, termHeight-3) + for i, logEntry := range playerData.Activities { + date, _ := logEntry.GetParsedDate() + events.Rows = append( + events.Rows, + []string{ + date.Local().Format("01/02 15:04"), + strings.Replace(logEntry.Details, " ", " ", -1), + }, + ) + + if time.Since(date) < cfg.MarkerTime { + events.RowStyles[i] = ui.Style{Fg: ui.ColorGreen} + } + } + ui.Render(events) + + return nil +} diff --git a/metrics.go b/metrics.go new file mode 100644 index 0000000..1154e31 --- /dev/null +++ b/metrics.go @@ -0,0 +1,172 @@ +package main + +import ( + "encoding/json" + "net/http" + "net/url" + "os" + "path" + "strconv" + "strings" + "time" + + "github.com/pkg/errors" +) + +var ( + knownTotalXP int64 + knownFeed time.Time + + playerInfoCache *playerInfo +) + +type activity struct { + Date string `json:"date"` + Details string `json:"details"` + Text string `json:"text"` +} + +func (a activity) GetParsedDate() (time.Time, error) { + loc, err := time.LoadLocation("Europe/London") + if err != nil { + return time.Time{}, errors.Wrap(err, "Unable to load London time information") + } + + return time.ParseInLocation("02-Jan-2006 15:04", a.Date, loc) +} + +type skill struct { + ID skillID `json:"id"` + Level int `json:"level"` + Rank int64 `json:"rank"` + XP int64 `json:"xp"` + + Updated time.Time +} + +type playerInfo struct { + Activities []activity `json:"activities"` + CombatLevel int `json:"combatlevel"` + LoggedIn bool `json:"loggedIn,string"` + Magic int64 `json:"magic"` + Melee int64 `json:"melee"` + Name string `json:"name"` + QuestsComplete int `json:"questscomplete"` + QuestsNotStarted int `json:"questsnotstarted"` + QuestsStarted int `json:"questsstarted"` + Ranged int64 `json:"ranged"` + Rank string `json:"rank"` + SkillValues []skill `json:"skillvalues"` + TotalSkill int64 `json:"totalskill"` + TotalXP int64 `json:"totalxp"` +} + +func (p playerInfo) NumericRank() int64 { + v, _ := strconv.ParseInt(strings.Replace(p.Rank, ",", "", -1), 10, 64) + return v +} + +func (p playerInfo) GetSkill(s skillID) skill { + for _, sk := range p.SkillValues { + if sk.ID == s { + return sk + } + } + return skill{} +} + +func (p playerInfo) storeCache() error { + cacheDir, err := os.UserCacheDir() + if err != nil { + return errors.Wrap(err, "Unable to retrieve user cache dir") + } + + cacheDir = path.Join(cacheDir, "luzifer", "runemetrics") + if err = os.MkdirAll(cacheDir, 0755); err != nil { + return errors.Wrap(err, "Unable to create cache dir") + } + + cacheFile := path.Join(cacheDir, "metrics.json") + f, err := os.Create(cacheFile) + if err != nil { + return errors.Wrap(err, "Unable to create cache file") + } + defer f.Close() + + return errors.Wrap(json.NewEncoder(f).Encode(p), "Unable to marshal into cache file") +} + +func getPlayerInfo(name string, activities int) (*playerInfo, error) { + if name == "" { + return nil, errors.New("Player name must not be empty") + } + + params := url.Values{ + "user": []string{name}, + "activities": []string{strconv.Itoa(activities)}, + } + uri := "https://apps.runescape.com/runemetrics/profile/profile?" + params.Encode() + + resp, err := http.Get(uri) + if err != nil { + return nil, errors.Wrap(err, "Unable to query profile data") + } + defer resp.Body.Close() + + out := &playerInfo{} + if err = json.NewDecoder(resp.Body).Decode(out); err != nil { + return nil, errors.Wrap(err, "Unable to decode profile data") + } + + if playerInfoCache != nil { + for i, nSk := range out.SkillValues { + if oSk := playerInfoCache.GetSkill(nSk.ID); oSk.XP == nSk.XP { + out.SkillValues[i].Updated = oSk.Updated + continue + } + + out.SkillValues[i].Updated = time.Now() + } + } + + if knownTotalXP != out.TotalXP { + knownTotalXP = out.TotalXP + lastUpdate[updateKeyTotalXP] = time.Now() + } + + if d, _ := out.Activities[0].GetParsedDate(); !d.Equal(knownFeed) { + knownFeed = d + lastUpdate[updateKeyFeed] = time.Now() + } + + lastUpdate[updateKeyGeneral] = time.Now() + + playerInfoCache = out + + return out, nil +} + +func loadPlayerInfoCache() (*playerInfo, error) { + cacheDir, err := os.UserCacheDir() + if err != nil { + return nil, errors.Wrap(err, "Unable to retrieve user cache dir") + } + + cacheFile := path.Join(cacheDir, "luzifer", "runemetrics", "metrics.json") + if _, err := os.Stat(cacheFile); err != nil { + if os.IsNotExist(err) { + // Empty cache + return nil, nil + } + return nil, errors.Wrap(err, "Unable to stat cache file") + } + + f, err := os.Open(cacheFile) + if err != nil { + return nil, errors.Wrap(err, "Unable to open cache file") + } + defer f.Close() + + p := &playerInfo{} + return p, errors.Wrap(json.NewDecoder(f).Decode(p), "Unable to unmarshal cache file") +} diff --git a/skill.go b/skill.go new file mode 100644 index 0000000..cfafb78 --- /dev/null +++ b/skill.go @@ -0,0 +1,167 @@ +package main + +type skillInfo struct { + id uint + name string + color string + maxLevel int + skillCurve string // ??? +} + +func (s skillInfo) NextLevelXP(level int) int64 { + levelTree := levels + if s.skillCurve != "" { + levelTree = masterLevels + } + + return levelTree[level+1] +} + +func (s skillInfo) LevelPercentage(level int, xp int64) float64 { + var ( + xpCurr = float64(s.NextLevelXP(level - 1)) + xpNext = float64(s.NextLevelXP(level)) + ) + + return (float64(xp) - xpCurr) / (xpNext - xpCurr) * 100 +} + +func (s skillInfo) XPToNextLevel(level int, xp int64) int64 { + return s.NextLevelXP(level) - xp +} + +var skillList = []skillInfo{ + { + id: 0, + name: "Attack", + color: "#981414", + }, { + id: 1, + name: "Defence", + color: "#147e98", + }, { + id: 2, + name: "Strength", + color: "#13b787", + }, { + id: 3, + name: "Constitution", + color: "#AACEDA", + }, { + id: 4, + name: "Ranged", + color: "#13b751", + }, { + id: 5, + name: "Prayer", + color: "#6dbff2", + }, { + id: 6, + name: "Magic", + color: "#c3e3dc", + }, { + id: 7, + name: "Cooking", + color: "#553285", + }, { + id: 8, + name: "Woodcutting", + color: "#7e4f35", + }, { + id: 9, + name: "Fletching", + color: "#149893", + }, { + id: 10, + name: "Fishing", + color: "#3e70b9", + }, { + id: 11, + name: "Firemaking", + color: "#f75f28", + }, { + id: 12, + name: "Crafting", + color: "#b6952c", + }, { + id: 13, + name: "Smithing", + color: "#65887e", + }, { + id: 14, + name: "Mining", + color: "#56495e", + }, { + id: 15, + name: "Herblore", + color: "#12453a", + }, { + id: 16, + name: "Agility", + color: "#284A95", + }, { + id: 17, + name: "Thieving", + color: "#36175e", + }, { + id: 18, + name: "Slayer", + color: "#48412f", + }, { + id: 19, + name: "Farming", + color: "#1f7d54", + }, { + id: 20, + name: "Runecrafting", + color: "#d7eba3", + }, { + id: 21, + name: "Hunter", + color: "#c38b4e", + }, { + id: 22, + name: "Construction", + color: "#a8babc", + }, { + id: 23, + name: "Summoning", + color: "#DEA1B0", + }, { + id: 24, + name: "Dungeoneering", + color: "#723920", + maxLevel: 120, + }, { + id: 25, + name: "Divination", + color: "#943fba", + }, { + id: 26, + name: "Invention", + color: "#f7b528", + skillCurve: "master", + }, +} + +type skillID uint + +func (s skillID) String() string { + for _, se := range skillList { + if se.id == uint(s) { + return se.name + } + } + + return "" +} + +func (s skillID) Info() skillInfo { + for _, se := range skillList { + if se.id == uint(s) { + return se + } + } + + return skillInfo{} +}