1
0
Fork 0
mirror of https://github.com/Luzifer/runemetrics.git synced 2024-10-18 05:54:25 +00:00

Initial version

This commit is contained in:
Knut Ahlers 2019-08-28 14:07:37 +02:00
commit 4d1f10502a
Signed by: luzifer
GPG key ID: DC2729FDD34BE99E
6 changed files with 883 additions and 0 deletions

11
go.mod Normal file
View file

@ -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
)

33
go.sum Normal file
View file

@ -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=

285
levels.go Normal file
View file

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

215
main.go Normal file
View file

@ -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 <player>")
}
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", "<C-c>":
return
case "<C-r>":
updateTicker.Reset(0)
case "<Resize>":
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
}

172
metrics.go Normal file
View file

@ -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")
}

167
skill.go Normal file
View file

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