Compare commits

5 Commits
Author SHA1 Message Date
zephyr 2a34b5aef4 cmd/factorfactor: initial work 2026-09-20 11:42:56 -04:00
zephyr 0ffd2c099a horse, mdb: include id in lobby conversations
ci/woodpecker/push/horsebot Pipeline was successful
ci/woodpecker/push/zenno Pipeline was successful
2026-09-17 12:59:28 -04:00
zephyr 6828b1bd88 horse, mdb: put skill groups in skills
ci/woodpecker/push/horsebot Pipeline was successful
ci/woodpecker/push/zenno Pipeline was successful
Fixes #13.
2026-09-15 11:00:10 -04:00
zephyr 78aee43e5f zenno: format
ci/woodpecker/push/horsebot Pipeline was successful
ci/woodpecker/push/zenno Pipeline was successful
why the h*ck does vscode not format my .ts files on save even though i
have it configured to always format on save!!!!!!
2026-09-14 17:21:51 -04:00
zephyr 745c9d01dd zenno/doc/frbm: delete jbc sprint
Fixes #46.
2026-09-14 17:20:40 -04:00
13 changed files with 620 additions and 200 deletions
+2
View File
@@ -28,6 +28,8 @@ type AffinityDetail struct {
// Conversation describes a lobby conversation. // Conversation describes a lobby conversation.
type Conversation struct { type Conversation struct {
// ID is the conversation ID.
ID int32 `json:"id"`
// CharacterID is the ID of the character who has the conversation as // CharacterID is the ID of the character who has the conversation as
// a gallery entry. // a gallery entry.
CharacterID CharacterID `json:"chara_id"` CharacterID CharacterID `json:"chara_id"`
+77
View File
@@ -0,0 +1,77 @@
package main
import (
"log/slog"
"git.sunturtle.xyz/zephyr/horse"
"git.sunturtle.xyz/zephyr/horse/umadump/ism"
)
// For each gain type from inspiration (stats, caps, aptitudes, skills):
// find all activated sparks that can give that gain type.
// Then, find all subsets of effect groups among those sparks
// that have effects summing to those observed
// (or more if the .end_info result is maxed),
// with the restriction that one comes from each spark.
// Deduplicate permutations.
// Lastly, find the intersection of possible effects from sparks that appear
// multiple times.
//
// To some extent, we rely on there being no sparks with the random stats effect type,
// although we also don't know the distribution of stats from overcapped skill hints.
type target struct {
spark *horse.Spark
group int
effect horse.SparkEffect
}
func skilltargets(sparks []horse.Spark) map[horse.SkillID]map[horse.SparkID][]target {
r := make(map[horse.SkillID]map[horse.SparkID][]target)
for i, sp := range sparks {
for j, g := range sp.Effects {
for _, e := range g {
if e.Target != horse.SparkSkillHint {
continue
}
if r[horse.SkillID(e.Value1)] == nil {
r[horse.SkillID(e.Value1)] = make(map[horse.SparkID][]target)
}
r[horse.SkillID(e.Value1)][sp.ID] = append(r[horse.SkillID(e.Value1)][sp.ID], target{&sparks[i], j, e})
}
}
}
return r
}
func resolveSkills(tab tables, career *ism.WorkIdleSingleMode)
// skillsparks appends all activated sparks that can yield a given skill to r.
// It assumes that no spark can give multiple skills.
func skillsparks(tab tables, sparks []ism.Factor, id horse.SkillID, r []*horse.Spark) []*horse.Spark {
for _, f := range sparks {
sp := tab.sparks[f.FactorID]
if sp == nil {
// ATM we do generally exclude Carnival Bonus from spark info.
// Better to check.
slog.Warn("skipped spark", slog.Int("id", int(f.FactorID)))
continue
}
if sparkHasSkill(sp, id) {
r = append(r, sp)
}
}
return r
}
func sparkHasSkill(sp *horse.Spark, id horse.SkillID) bool {
for _, g := range sp.Effects {
for _, e := range g {
if e.Target != horse.SparkSkillHint {
continue
}
return id == horse.SkillID(e.Value1)
}
}
return false
}
+111
View File
@@ -0,0 +1,111 @@
package main
import (
"context"
"encoding/json"
"flag"
"fmt"
"log/slog"
"os"
"os/signal"
"path/filepath"
"slices"
"git.sunturtle.xyz/zephyr/horse/mdb"
"git.sunturtle.xyz/zephyr/horse/umadump/ism"
"golang.org/x/sync/errgroup"
"zombiezen.com/go/sqlite"
"zombiezen.com/go/sqlite/sqlitex"
)
func main() {
var (
mdbf string
dirs []string
files []string
)
flag.StringVar(&mdbf, "-mdb", "", "`path` to master.mdb")
flag.Func("d", "idle_single_mode `dir`ectory (may be specified multiple times)", func(s string) error {
dirs = append(dirs, s)
return nil
})
flag.Parse()
files = slices.Clone(flag.Args())
for _, dir := range dirs {
ds, err := os.ReadDir(dir)
if err != nil {
slog.Error("getting files in directory", slog.String("dir", dir), slog.Any("err", err))
os.Exit(1)
}
for _, d := range ds {
if d.IsDir() {
continue
}
files = append(files, filepath.Join(dir, d.Name()))
}
}
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
go func() {
<-ctx.Done()
stop()
}()
db, err := sqlitex.NewPool(mdbf, sqlitex.PoolOptions{Flags: sqlite.OpenReadOnly})
if err != nil {
slog.Error("mdb", slog.Any("err", err))
os.Exit(1)
}
slog.Info("load skills")
skills, err := mdb.Skills(ctx, db)
if err != nil {
slog.Error("skills", slog.Any("err", err))
os.Exit(1)
}
slog.Info("load sparks")
sparks, err := mdb.Sparks(ctx, db)
if err != nil {
slog.Error("sparks", slog.Any("err", err))
os.Exit(1)
}
slog.Info("load umas")
umas, err := mdb.Umas(ctx, db)
if err != nil {
slog.Error("umas", slog.Any("err", err))
os.Exit(1)
}
slog.Info("mdb",
slog.Int("sparks", len(sparks)),
slog.Int("skills", len(skills)),
slog.Int("umas", len(umas)),
)
tab := makeTables(skills, sparks, umas)
g, ctx := errgroup.WithContext(ctx)
g.SetLimit(64)
for _, file := range files {
g.Go(func() error {
return dofile(ctx, file)
})
}
if err := g.Wait(); err != nil {
slog.Error("process", slog.Any("err", err))
os.Exit(1)
}
}
func dofile(ctx context.Context, file string) error {
f, err := os.Open(file)
if err != nil {
return err
}
var career ism.WorkIdleSingleMode
d := json.NewDecoder(f)
d.DisallowUnknownFields()
if err := d.Decode(&career); err != nil {
return fmt.Errorf("%s: %w", file, err)
}
}
+37
View File
@@ -0,0 +1,37 @@
package main
import "git.sunturtle.xyz/zephyr/horse"
type tables struct {
skills map[horse.SkillID]*horse.Skill
groups map[horse.SkillGroupID]map[int8]*horse.Skill
sparks map[horse.SparkID]*horse.Spark
umas map[horse.UmaID]*horse.Uma
}
func makeTables(skills []horse.Skill, sparks []horse.Spark, umas []horse.Uma) tables {
tab := tables{
skills: make(map[horse.SkillID]*horse.Skill, len(skills)),
groups: make(map[horse.SkillGroupID]map[int8]*horse.Skill, len(skills)/2),
sparks: make(map[horse.SparkID]*horse.Spark, len(sparks)),
umas: make(map[horse.UmaID]*horse.Uma, len(umas)),
}
for i, sk := range skills {
tab.skills[sk.ID] = &skills[i]
if tab.groups[sk.Group] == nil {
tab.groups[sk.Group] = make(map[int8]*horse.Skill, 4)
}
// Only add the skill to the groups table if they're rare, the base
// version of the common skill, or there isn't anything else there.
if sk.Rarity == 2 || sk.GroupRate == 1 || tab.groups[sk.Group][sk.Rarity] == nil {
tab.groups[sk.Group][sk.Rarity] = &skills[i]
}
}
for i, sp := range sparks {
tab.sparks[sp.ID] = &sparks[i]
}
for i, u := range umas {
tab.umas[u.ID] = &umas[i]
}
return tab
}
+8 -7
View File
@@ -89,13 +89,14 @@ func Umas(ctx context.Context, db *sqlitex.Pool) ([]horse.Uma, error) {
func Conversations(ctx context.Context, db *sqlitex.Pool) ([]horse.Conversation, error) { func Conversations(ctx context.Context, db *sqlitex.Pool) ([]horse.Conversation, error) {
return load(ctx, db, make([]horse.Conversation, 0, 1024), conversationSQL, func(s *sqlite.Stmt) horse.Conversation { return load(ctx, db, make([]horse.Conversation, 0, 1024), conversationSQL, func(s *sqlite.Stmt) horse.Conversation {
return horse.Conversation{ return horse.Conversation{
CharacterID: horse.CharacterID(s.ColumnInt(0)), ID: s.ColumnInt32(0),
Number: s.ColumnInt(1), CharacterID: horse.CharacterID(s.ColumnInt(1)),
Location: horse.LobbyConversationLocationID(s.ColumnInt(2)), Number: s.ColumnInt(2),
Chara1: horse.CharacterID(s.ColumnInt(3)), Location: horse.LobbyConversationLocationID(s.ColumnInt(3)),
Chara2: horse.CharacterID(s.ColumnInt(4)), Chara1: horse.CharacterID(s.ColumnInt(4)),
Chara3: horse.CharacterID(s.ColumnInt(5)), Chara2: horse.CharacterID(s.ColumnInt(5)),
ConditionType: s.ColumnInt(6), Chara3: horse.CharacterID(s.ColumnInt(6)),
ConditionType: s.ColumnInt(7),
} }
}) })
} }
+11
View File
@@ -238,12 +238,14 @@ func TestConversations(t *testing.T) {
} }
want := []horse.Conversation{ want := []horse.Conversation{
{ {
ID: 1,
CharacterID: 1001, CharacterID: 1001,
Number: 1, Number: 1,
Location: 410, Location: 410,
Chara1: 1001, Chara1: 1001,
}, },
{ {
ID: 2,
CharacterID: 1001, CharacterID: 1001,
Number: 2, Number: 2,
Location: 510, Location: 510,
@@ -251,6 +253,7 @@ func TestConversations(t *testing.T) {
ConditionType: 1, ConditionType: 1,
}, },
{ {
ID: 3,
CharacterID: 1001, CharacterID: 1001,
Number: 3, Number: 3,
Location: 310, Location: 310,
@@ -258,6 +261,7 @@ func TestConversations(t *testing.T) {
ConditionType: 1, ConditionType: 1,
}, },
{ {
ID: 76,
CharacterID: 1001, CharacterID: 1001,
Number: 4, Number: 4,
Location: 120, Location: 120,
@@ -266,6 +270,7 @@ func TestConversations(t *testing.T) {
ConditionType: 2, ConditionType: 2,
}, },
{ {
ID: 77,
CharacterID: 1001, CharacterID: 1001,
Number: 5, Number: 5,
Location: 520, Location: 520,
@@ -274,6 +279,7 @@ func TestConversations(t *testing.T) {
ConditionType: 3, ConditionType: 3,
}, },
{ {
ID: 126,
CharacterID: 1001, CharacterID: 1001,
Number: 6, Number: 6,
Location: 430, Location: 430,
@@ -283,12 +289,14 @@ func TestConversations(t *testing.T) {
ConditionType: 1, ConditionType: 1,
}, },
{ {
ID: 4,
CharacterID: 1002, CharacterID: 1002,
Number: 1, Number: 1,
Location: 310, Location: 310,
Chara1: 1002, Chara1: 1002,
}, },
{ {
ID: 5,
CharacterID: 1002, CharacterID: 1002,
Number: 2, Number: 2,
Location: 210, Location: 210,
@@ -296,6 +304,7 @@ func TestConversations(t *testing.T) {
ConditionType: 1, ConditionType: 1,
}, },
{ {
ID: 6,
CharacterID: 1002, CharacterID: 1002,
Number: 3, Number: 3,
Location: 110, Location: 110,
@@ -303,6 +312,7 @@ func TestConversations(t *testing.T) {
ConditionType: 1, ConditionType: 1,
}, },
{ {
ID: 78,
CharacterID: 1002, CharacterID: 1002,
Number: 4, Number: 4,
Location: 520, Location: 520,
@@ -311,6 +321,7 @@ func TestConversations(t *testing.T) {
ConditionType: 3, ConditionType: 3,
}, },
{ {
ID: 79,
CharacterID: 1002, CharacterID: 1002,
Number: 5, Number: 5,
Location: 220, Location: 220,
+123 -77
View File
@@ -18,6 +18,8 @@ var (
skillGroupSQL string skillGroupSQL string
//go:embed sql/skill.sql //go:embed sql/skill.sql
skillSQL string skillSQL string
//go:embed sql/skill-group-skills.sql
skillGroupSkillsSQL string
) )
// SkillGroups retrieves all skill groups. // SkillGroups retrieves all skill groups.
@@ -35,84 +37,128 @@ func SkillGroups(ctx context.Context, db *sqlitex.Pool) ([]horse.SkillGroup, err
// Skills retrieves all skills. // Skills retrieves all skills.
func Skills(ctx context.Context, db *sqlitex.Pool) ([]horse.Skill, error) { func Skills(ctx context.Context, db *sqlitex.Pool) ([]horse.Skill, error) {
return load(ctx, db, nil, skillSQL, func(s *sqlite.Stmt) horse.Skill { // We don't use func load here because we have multiple queries to run.
return horse.Skill{ conn, err := db.Take(ctx)
ID: horse.SkillID(s.ColumnInt(0)), defer db.Put(conn)
Name: s.ColumnText(1), if err != nil {
Description: s.ColumnText(2), return nil, err
Group: horse.SkillGroupID(s.ColumnInt32(3)), }
Rarity: int8(s.ColumnInt(5)), var sk []horse.Skill
GroupRate: int8(s.ColumnInt(6)), {
GradeValue: s.ColumnInt32(7), stmt := conn.Prep(skillSQL)
WitCheck: s.ColumnBool(8), defer stmt.Reset()
Activations: trimActivations([]horse.Activation{ for {
{ ok, err := stmt.Step()
Precondition: s.ColumnText(9), if err != nil {
Condition: s.ColumnText(10), return nil, err
Duration: horse.TenThousandths(s.ColumnInt(11)), }
DurScale: horse.DurScale(s.ColumnInt(12)), if !ok {
Cooldown: horse.TenThousandths(s.ColumnInt(13)), break
Abilities: trimAbilities([]horse.Ability{ }
{ s := horse.Skill{
Type: horse.AbilityType(s.ColumnInt(14)), ID: horse.SkillID(stmt.ColumnInt(0)),
ValueUsage: horse.AbilityValueUsage(s.ColumnInt(15)), Name: stmt.ColumnText(1),
Value: horse.TenThousandths(s.ColumnInt(16)), Description: stmt.ColumnText(2),
Target: horse.AbilityTarget(s.ColumnInt(17)), GroupSkills: make([]horse.GroupSkill, 0, 3),
TargetValue: s.ColumnInt32(18), Group: horse.SkillGroupID(stmt.ColumnInt32(3)),
}, Rarity: int8(stmt.ColumnInt(5)),
{ GroupRate: int8(stmt.ColumnInt(6)),
Type: horse.AbilityType(s.ColumnInt(19)), GradeValue: stmt.ColumnInt32(7),
ValueUsage: horse.AbilityValueUsage(s.ColumnInt(20)), WitCheck: stmt.ColumnBool(8),
Value: horse.TenThousandths(s.ColumnInt(21)), Activations: trimActivations([]horse.Activation{
Target: horse.AbilityTarget(s.ColumnInt(22)), {
TargetValue: s.ColumnInt32(23), Precondition: stmt.ColumnText(9),
}, Condition: stmt.ColumnText(10),
{ Duration: horse.TenThousandths(stmt.ColumnInt(11)),
Type: horse.AbilityType(s.ColumnInt(24)), DurScale: horse.DurScale(stmt.ColumnInt(12)),
ValueUsage: horse.AbilityValueUsage(s.ColumnInt(25)), Cooldown: horse.TenThousandths(stmt.ColumnInt(13)),
Value: horse.TenThousandths(s.ColumnInt(26)), Abilities: trimAbilities([]horse.Ability{
Target: horse.AbilityTarget(s.ColumnInt(27)), {
TargetValue: s.ColumnInt32(28), Type: horse.AbilityType(stmt.ColumnInt(14)),
}, ValueUsage: horse.AbilityValueUsage(stmt.ColumnInt(15)),
}), Value: horse.TenThousandths(stmt.ColumnInt(16)),
}, Target: horse.AbilityTarget(stmt.ColumnInt(17)),
{ TargetValue: stmt.ColumnInt32(18),
Precondition: s.ColumnText(29), },
Condition: s.ColumnText(30), {
Duration: horse.TenThousandths(s.ColumnInt(31)), Type: horse.AbilityType(stmt.ColumnInt(19)),
DurScale: horse.DurScale(s.ColumnInt(32)), ValueUsage: horse.AbilityValueUsage(stmt.ColumnInt(20)),
Cooldown: horse.TenThousandths(s.ColumnInt(33)), Value: horse.TenThousandths(stmt.ColumnInt(21)),
Abilities: trimAbilities([]horse.Ability{ Target: horse.AbilityTarget(stmt.ColumnInt(22)),
{ TargetValue: stmt.ColumnInt32(23),
Type: horse.AbilityType(s.ColumnInt(34)), },
ValueUsage: horse.AbilityValueUsage(s.ColumnInt(35)), {
Value: horse.TenThousandths(s.ColumnInt(36)), Type: horse.AbilityType(stmt.ColumnInt(24)),
Target: horse.AbilityTarget(s.ColumnInt(37)), ValueUsage: horse.AbilityValueUsage(stmt.ColumnInt(25)),
TargetValue: s.ColumnInt32(38), Value: horse.TenThousandths(stmt.ColumnInt(26)),
}, Target: horse.AbilityTarget(stmt.ColumnInt(27)),
{ TargetValue: stmt.ColumnInt32(28),
Type: horse.AbilityType(s.ColumnInt(39)), },
ValueUsage: horse.AbilityValueUsage(s.ColumnInt(40)), }),
Value: horse.TenThousandths(s.ColumnInt(41)), },
Target: horse.AbilityTarget(s.ColumnInt(42)), {
TargetValue: s.ColumnInt32(43), Precondition: stmt.ColumnText(29),
}, Condition: stmt.ColumnText(30),
{ Duration: horse.TenThousandths(stmt.ColumnInt(31)),
Type: horse.AbilityType(s.ColumnInt(44)), DurScale: horse.DurScale(stmt.ColumnInt(32)),
ValueUsage: horse.AbilityValueUsage(s.ColumnInt(45)), Cooldown: horse.TenThousandths(stmt.ColumnInt(33)),
Value: horse.TenThousandths(s.ColumnInt(46)), Abilities: trimAbilities([]horse.Ability{
Target: horse.AbilityTarget(s.ColumnInt(47)), {
TargetValue: s.ColumnInt32(48), Type: horse.AbilityType(stmt.ColumnInt(34)),
}, ValueUsage: horse.AbilityValueUsage(stmt.ColumnInt(35)),
}), Value: horse.TenThousandths(stmt.ColumnInt(36)),
}, Target: horse.AbilityTarget(stmt.ColumnInt(37)),
}), TargetValue: stmt.ColumnInt32(38),
UniqueOwner: s.ColumnText(52), // TODO(zeph): should be id, not name },
Tags: parseTags(s.ColumnText(54)), {
SPCost: s.ColumnInt(49), Type: horse.AbilityType(stmt.ColumnInt(39)),
IconID: s.ColumnInt(53), ValueUsage: horse.AbilityValueUsage(stmt.ColumnInt(40)),
Value: horse.TenThousandths(stmt.ColumnInt(41)),
Target: horse.AbilityTarget(stmt.ColumnInt(42)),
TargetValue: stmt.ColumnInt32(43),
},
{
Type: horse.AbilityType(stmt.ColumnInt(44)),
ValueUsage: horse.AbilityValueUsage(stmt.ColumnInt(45)),
Value: horse.TenThousandths(stmt.ColumnInt(46)),
Target: horse.AbilityTarget(stmt.ColumnInt(47)),
TargetValue: stmt.ColumnInt32(48),
},
}),
},
}),
UniqueOwner: stmt.ColumnText(52), // TODO(zeph): should be id, not name
Tags: parseTags(stmt.ColumnText(54)),
SPCost: stmt.ColumnInt(49),
IconID: stmt.ColumnInt(53),
}
sk = append(sk, s)
} }
}) }
stmt := conn.Prep(skillGroupSkillsSQL)
defer stmt.Reset()
cur := sk
for {
ok, err := stmt.Step()
if err != nil {
return nil, err
}
if !ok {
break
}
// We sort skills by ID first, so we can add group skills in a single pass.
id := stmt.ColumnInt(0)
for len(cur) != 0 && cur[0].ID != horse.SkillID(id) {
cur = cur[1:]
}
g := horse.GroupSkill{
ID: horse.SkillID(stmt.ColumnInt(1)),
GroupRate: int8(stmt.ColumnInt(2)),
Rarity: int8(stmt.ColumnInt(3)),
}
cur[0].GroupSkills = append(cur[0].GroupSkills, g)
}
return sk, nil
} }
func parseTags(s string) []uint16 { func parseTags(s string) []uint16 {
+182 -95
View File
@@ -49,11 +49,16 @@ func TestSkills(t *testing.T) {
ID: 10351, ID: 10351,
Name: "V Is for Victory!", Name: "V Is for Victory!",
Description: "Moderately increase velocity with winning ambition when positioned toward the front on the final straight after engaging in a challenge on the final corner or later.", Description: "Moderately increase velocity with winning ambition when positioned toward the front on the final straight after engaging in a challenge on the final corner or later.",
Group: 1035, GroupSkills: []horse.GroupSkill{
Rarity: 3, {ID: 100351, GroupRate: 1, Rarity: 4},
GroupRate: 1, {ID: 10351, GroupRate: 1, Rarity: 3},
GradeValue: 240, {ID: 900351, GroupRate: 2, Rarity: 1},
WitCheck: false, },
Group: 1035,
Rarity: 3,
GroupRate: 1,
GradeValue: 240,
WitCheck: false,
Activations: []horse.Activation{ Activations: []horse.Activation{
{ {
Precondition: "is_finalcorner==1&blocked_side_continuetime>=2", Precondition: "is_finalcorner==1&blocked_side_continuetime>=2",
@@ -81,11 +86,15 @@ func TestSkills(t *testing.T) {
ID: 100011, ID: 100011,
Name: "Shooting Star", Name: "Shooting Star",
Description: "Ride the momentum to increase velocity and very slightly increase acceleration after passing another runner toward the front late-race.", Description: "Ride the momentum to increase velocity and very slightly increase acceleration after passing another runner toward the front late-race.",
Group: 10001, GroupSkills: []horse.GroupSkill{
Rarity: 5, {ID: 100011, GroupRate: 1, Rarity: 5},
GroupRate: 1, {ID: 900011, GroupRate: 2, Rarity: 1},
GradeValue: 340, },
WitCheck: false, Group: 10001,
Rarity: 5,
GroupRate: 1,
GradeValue: 340,
WitCheck: false,
Activations: []horse.Activation{ Activations: []horse.Activation{
{ {
Precondition: "", Precondition: "",
@@ -120,11 +129,16 @@ func TestSkills(t *testing.T) {
ID: 100351, ID: 100351,
Name: "Our Ticket to Win!", Name: "Our Ticket to Win!",
Description: "Increase velocity with winning ambition when positioned toward the front on the final straight after engaging in a challenge on the final corner or later.", Description: "Increase velocity with winning ambition when positioned toward the front on the final straight after engaging in a challenge on the final corner or later.",
Group: 10035, GroupSkills: []horse.GroupSkill{
Rarity: 4, {ID: 100351, GroupRate: 1, Rarity: 4},
GroupRate: 1, {ID: 10351, GroupRate: 1, Rarity: 3},
GradeValue: 340, {ID: 900351, GroupRate: 2, Rarity: 1},
WitCheck: false, },
Group: 10035,
Rarity: 4,
GroupRate: 1,
GradeValue: 340,
WitCheck: false,
Activations: []horse.Activation{ Activations: []horse.Activation{
{ {
Precondition: "is_finalcorner==1&blocked_side_continuetime>=2", Precondition: "is_finalcorner==1&blocked_side_continuetime>=2",
@@ -152,11 +166,15 @@ func TestSkills(t *testing.T) {
ID: 110241, ID: 110241,
Name: "Flowery☆Maneuver", Name: "Flowery☆Maneuver",
Description: "Increase velocity when passing another runner toward the front on the final corner. If passing toward the back, increase acceleration instead.", Description: "Increase velocity when passing another runner toward the front on the final corner. If passing toward the back, increase acceleration instead.",
Group: 11024, GroupSkills: []horse.GroupSkill{
Rarity: 5, {ID: 110241, GroupRate: 1, Rarity: 5},
GroupRate: 1, {ID: 910241, GroupRate: 2, Rarity: 1},
GradeValue: 340, },
WitCheck: false, Group: 11024,
Rarity: 5,
GroupRate: 1,
GradeValue: 340,
WitCheck: false,
Activations: []horse.Activation{ Activations: []horse.Activation{
{ {
Precondition: "", Precondition: "",
@@ -200,11 +218,17 @@ func TestSkills(t *testing.T) {
ID: 200011, ID: 200011,
Name: "Right-Handed ◎", Name: "Right-Handed ◎",
Description: "Increase performance on right-handed tracks.", Description: "Increase performance on right-handed tracks.",
Group: 20001, GroupSkills: []horse.GroupSkill{
Rarity: 1, {ID: 200013, GroupRate: -1, Rarity: 1},
GroupRate: 2, {ID: 200012, GroupRate: 1, Rarity: 1},
GradeValue: 174, {ID: 200011, GroupRate: 2, Rarity: 1},
WitCheck: false, {ID: 200014, GroupRate: 3, Rarity: 2},
},
Group: 20001,
Rarity: 1,
GroupRate: 2,
GradeValue: 174,
WitCheck: false,
Activations: []horse.Activation{ Activations: []horse.Activation{
{ {
Precondition: "", Precondition: "",
@@ -231,11 +255,17 @@ func TestSkills(t *testing.T) {
ID: 200012, ID: 200012,
Name: "Right-Handed ○", Name: "Right-Handed ○",
Description: "Moderately increase performance on right-handed tracks.", Description: "Moderately increase performance on right-handed tracks.",
Group: 20001, GroupSkills: []horse.GroupSkill{
Rarity: 1, {ID: 200013, GroupRate: -1, Rarity: 1},
GroupRate: 1, {ID: 200012, GroupRate: 1, Rarity: 1},
GradeValue: 129, {ID: 200011, GroupRate: 2, Rarity: 1},
WitCheck: false, {ID: 200014, GroupRate: 3, Rarity: 2},
},
Group: 20001,
Rarity: 1,
GroupRate: 1,
GradeValue: 129,
WitCheck: false,
Activations: []horse.Activation{ Activations: []horse.Activation{
{ {
Precondition: "", Precondition: "",
@@ -262,11 +292,17 @@ func TestSkills(t *testing.T) {
ID: 200013, ID: 200013,
Name: "Right-Handed ×", Name: "Right-Handed ×",
Description: "Moderately decrease performance on right-handed tracks.", Description: "Moderately decrease performance on right-handed tracks.",
Group: 20001, GroupSkills: []horse.GroupSkill{
Rarity: 1, {ID: 200013, GroupRate: -1, Rarity: 1},
GroupRate: -1, {ID: 200012, GroupRate: 1, Rarity: 1},
GradeValue: -129, {ID: 200011, GroupRate: 2, Rarity: 1},
WitCheck: false, {ID: 200014, GroupRate: 3, Rarity: 2},
},
Group: 20001,
Rarity: 1,
GroupRate: -1,
GradeValue: -129,
WitCheck: false,
Activations: []horse.Activation{ Activations: []horse.Activation{
{ {
Precondition: "", Precondition: "",
@@ -293,11 +329,17 @@ func TestSkills(t *testing.T) {
ID: 200014, ID: 200014,
Name: "Right-Handed Demon", Name: "Right-Handed Demon",
Description: "Increase proficiency in right-handed tracks, increasing Speed and Power.", Description: "Increase proficiency in right-handed tracks, increasing Speed and Power.",
Group: 20001, GroupSkills: []horse.GroupSkill{
Rarity: 2, {ID: 200013, GroupRate: -1, Rarity: 1},
GroupRate: 3, {ID: 200012, GroupRate: 1, Rarity: 1},
GradeValue: 461, {ID: 200011, GroupRate: 2, Rarity: 1},
WitCheck: false, {ID: 200014, GroupRate: 3, Rarity: 2},
},
Group: 20001,
Rarity: 2,
GroupRate: 3,
GradeValue: 461,
WitCheck: false,
Activations: []horse.Activation{ Activations: []horse.Activation{
{ {
Precondition: "", Precondition: "",
@@ -331,11 +373,16 @@ func TestSkills(t *testing.T) {
ID: 200021, ID: 200021,
Name: "Left-Handed ◎", Name: "Left-Handed ◎",
Description: "Increase performance on left-handed tracks.", Description: "Increase performance on left-handed tracks.",
Group: 20002, GroupSkills: []horse.GroupSkill{
Rarity: 1, {ID: 200023, GroupRate: -1, Rarity: 1},
GroupRate: 2, {ID: 200022, GroupRate: 1, Rarity: 1},
GradeValue: 174, {ID: 200021, GroupRate: 2, Rarity: 1},
WitCheck: false, },
Group: 20002,
Rarity: 1,
GroupRate: 2,
GradeValue: 174,
WitCheck: false,
Activations: []horse.Activation{ Activations: []horse.Activation{
{ {
Precondition: "", Precondition: "",
@@ -362,11 +409,16 @@ func TestSkills(t *testing.T) {
ID: 200022, ID: 200022,
Name: "Left-Handed ○", Name: "Left-Handed ○",
Description: "Moderately increase performance on left-handed tracks.", Description: "Moderately increase performance on left-handed tracks.",
Group: 20002, GroupSkills: []horse.GroupSkill{
Rarity: 1, {ID: 200023, GroupRate: -1, Rarity: 1},
GroupRate: 1, {ID: 200022, GroupRate: 1, Rarity: 1},
GradeValue: 129, {ID: 200021, GroupRate: 2, Rarity: 1},
WitCheck: false, },
Group: 20002,
Rarity: 1,
GroupRate: 1,
GradeValue: 129,
WitCheck: false,
Activations: []horse.Activation{ Activations: []horse.Activation{
{ {
Precondition: "", Precondition: "",
@@ -393,11 +445,16 @@ func TestSkills(t *testing.T) {
ID: 200023, ID: 200023,
Name: "Left-Handed ×", Name: "Left-Handed ×",
Description: "Moderately decrease performance on left-handed tracks.", Description: "Moderately decrease performance on left-handed tracks.",
Group: 20002, GroupSkills: []horse.GroupSkill{
Rarity: 1, {ID: 200023, GroupRate: -1, Rarity: 1},
GroupRate: -1, {ID: 200022, GroupRate: 1, Rarity: 1},
GradeValue: -129, {ID: 200021, GroupRate: 2, Rarity: 1},
WitCheck: false, },
Group: 20002,
Rarity: 1,
GroupRate: -1,
GradeValue: -129,
WitCheck: false,
Activations: []horse.Activation{ Activations: []horse.Activation{
{ {
Precondition: "", Precondition: "",
@@ -424,11 +481,15 @@ func TestSkills(t *testing.T) {
ID: 200361, ID: 200361,
Name: "Beeline Burst", Name: "Beeline Burst",
Description: "Increase velocity on a straight.", Description: "Increase velocity on a straight.",
Group: 20036, GroupSkills: []horse.GroupSkill{
Rarity: 2, {ID: 200362, GroupRate: 1, Rarity: 1},
GroupRate: 2, {ID: 200361, GroupRate: 2, Rarity: 2},
GradeValue: 508, },
WitCheck: true, Group: 20036,
Rarity: 2,
GroupRate: 2,
GradeValue: 508,
WitCheck: true,
Activations: []horse.Activation{ Activations: []horse.Activation{
{ {
Precondition: "", Precondition: "",
@@ -455,11 +516,15 @@ func TestSkills(t *testing.T) {
ID: 200362, ID: 200362,
Name: "Straightaway Adept", Name: "Straightaway Adept",
Description: "Slightly increase velocity on a straight.", Description: "Slightly increase velocity on a straight.",
Group: 20036, GroupSkills: []horse.GroupSkill{
Rarity: 1, {ID: 200362, GroupRate: 1, Rarity: 1},
GroupRate: 1, {ID: 200361, GroupRate: 2, Rarity: 2},
GradeValue: 217, },
WitCheck: true, Group: 20036,
Rarity: 1,
GroupRate: 1,
GradeValue: 217,
WitCheck: true,
Activations: []horse.Activation{ Activations: []horse.Activation{
{ {
Precondition: "", Precondition: "",
@@ -486,11 +551,14 @@ func TestSkills(t *testing.T) {
ID: 200831, ID: 200831,
Name: "Subdued Front Runners", Name: "Subdued Front Runners",
Description: "Slightly increase fatigue for front runners early-race.", Description: "Slightly increase fatigue for front runners early-race.",
Group: 20083, GroupSkills: []horse.GroupSkill{
Rarity: 1, {ID: 200831, GroupRate: 1, Rarity: 1},
GroupRate: 1, },
GradeValue: 217, Group: 20083,
WitCheck: true, Rarity: 1,
GroupRate: 1,
GradeValue: 217,
WitCheck: true,
Activations: []horse.Activation{ Activations: []horse.Activation{
{ {
Precondition: "", Precondition: "",
@@ -517,11 +585,14 @@ func TestSkills(t *testing.T) {
ID: 201801, ID: 201801,
Name: "♡ 3D Nail Art", Name: "♡ 3D Nail Art",
Description: "Moderately decrease performance on firm ground.", Description: "Moderately decrease performance on firm ground.",
Group: 20180, GroupSkills: []horse.GroupSkill{
Rarity: 1, {ID: 201801, GroupRate: -1, Rarity: 1},
GroupRate: -1, },
GradeValue: -129, Group: 20180,
WitCheck: false, Rarity: 1,
GroupRate: -1,
GradeValue: -129,
WitCheck: false,
Activations: []horse.Activation{ Activations: []horse.Activation{
{ {
Precondition: "", Precondition: "",
@@ -548,11 +619,14 @@ func TestSkills(t *testing.T) {
ID: 300011, ID: 300011,
Name: "Unquenched Thirst", Name: "Unquenched Thirst",
Description: "Moderately increase performance with the desire to race.", Description: "Moderately increase performance with the desire to race.",
Group: 30001, GroupSkills: []horse.GroupSkill{
Rarity: 1, {ID: 300011, GroupRate: 1, Rarity: 1},
GroupRate: 1, },
GradeValue: 0, Group: 30001,
WitCheck: false, Rarity: 1,
GroupRate: 1,
GradeValue: 0,
WitCheck: false,
Activations: []horse.Activation{ Activations: []horse.Activation{
{ {
Precondition: "", Precondition: "",
@@ -579,11 +653,15 @@ func TestSkills(t *testing.T) {
ID: 900011, ID: 900011,
Name: "Shooting Star", Name: "Shooting Star",
Description: "Slightly increase velocity and very minimally increase acceleration after passing another runner toward the front late-race.", Description: "Slightly increase velocity and very minimally increase acceleration after passing another runner toward the front late-race.",
Group: 10001, GroupSkills: []horse.GroupSkill{
Rarity: 1, {ID: 100011, GroupRate: 1, Rarity: 5},
GroupRate: 2, {ID: 900011, GroupRate: 2, Rarity: 1},
GradeValue: 180, },
WitCheck: true, Group: 10001,
Rarity: 1,
GroupRate: 2,
GradeValue: 180,
WitCheck: true,
Activations: []horse.Activation{ Activations: []horse.Activation{
{ {
Precondition: "", Precondition: "",
@@ -618,11 +696,16 @@ func TestSkills(t *testing.T) {
ID: 900351, ID: 900351,
Name: "Our Ticket to Win!", Name: "Our Ticket to Win!",
Description: "Slightly increase velocity when positioned toward the front on the final straight after engaging in a challenge on the final corner or later.", Description: "Slightly increase velocity when positioned toward the front on the final straight after engaging in a challenge on the final corner or later.",
Group: 10035, GroupSkills: []horse.GroupSkill{
Rarity: 1, {ID: 100351, GroupRate: 1, Rarity: 4},
GroupRate: 2, {ID: 10351, GroupRate: 1, Rarity: 3},
GradeValue: 180, {ID: 900351, GroupRate: 2, Rarity: 1},
WitCheck: true, },
Group: 10035,
Rarity: 1,
GroupRate: 2,
GradeValue: 180,
WitCheck: true,
Activations: []horse.Activation{ Activations: []horse.Activation{
{ {
Precondition: "is_finalcorner==1&blocked_side_continuetime>=2", Precondition: "is_finalcorner==1&blocked_side_continuetime>=2",
@@ -650,11 +733,15 @@ func TestSkills(t *testing.T) {
ID: 910241, ID: 910241,
Name: "Flowery☆Maneuver", Name: "Flowery☆Maneuver",
Description: "Slightly increase velocity when passing another runner toward the front on the final corner. If passing toward the back, slightly increase acceleration instead.", Description: "Slightly increase velocity when passing another runner toward the front on the final corner. If passing toward the back, slightly increase acceleration instead.",
Group: 11024, GroupSkills: []horse.GroupSkill{
Rarity: 1, {ID: 110241, GroupRate: 1, Rarity: 5},
GroupRate: 2, {ID: 910241, GroupRate: 2, Rarity: 1},
GradeValue: 180, },
WitCheck: true, Group: 11024,
Rarity: 1,
GroupRate: 2,
GradeValue: 180,
WitCheck: true,
Activations: []horse.Activation{ Activations: []horse.Activation{
{ {
Precondition: "", Precondition: "",
+1
View File
@@ -1,4 +1,5 @@
SELECT SELECT
id,
gallery_chara_id, gallery_chara_id,
disp_order, disp_order,
pos_id, pos_id,
+43
View File
@@ -0,0 +1,43 @@
WITH g AS (
SELECT
l.id AS l,
r.id AS r,
r.group_rate,
r.rarity
FROM skill_data l
JOIN skill_data r ON l.group_id = r.group_id
UNION ALL
SELECT
s.id AS l,
i.id AS r,
i.group_rate,
i.rarity
FROM skill_data s
JOIN skill_data i ON s.id IN (i.unique_skill_id_1, i.unique_skill_id_2)
UNION ALL
SELECT
s.id AS l,
i.id AS r,
i.group_rate,
i.rarity
FROM skill_data s
JOIN skill_data i ON i.id IN (s.unique_skill_id_1, s.unique_skill_id_2)
UNION ALL
SELECT
l.id AS l,
r.id AS r,
r.group_rate,
r.rarity
FROM skill_data i
JOIN skill_data l ON l.id IN (i.unique_skill_id_1, i.unique_skill_id_2)
JOIN skill_data r ON r.id IN (i.unique_skill_id_1, i.unique_skill_id_2)
WHERE l.id != r.id
)
SELECT * FROM g
ORDER BY l, group_rate, rarity DESC
+7
View File
@@ -30,6 +30,7 @@ type Skill struct {
ID SkillID `json:"skill_id"` ID SkillID `json:"skill_id"`
Name string `json:"name"` Name string `json:"name"`
Description string `json:"description"` Description string `json:"description"`
GroupSkills []GroupSkill `json:"group_skills"`
Group SkillGroupID `json:"group"` Group SkillGroupID `json:"group"`
Rarity int8 `json:"rarity"` Rarity int8 `json:"rarity"`
GroupRate int8 `json:"group_rate"` GroupRate int8 `json:"group_rate"`
@@ -42,6 +43,12 @@ type Skill struct {
IconID int `json:"icon_id"` IconID int `json:"icon_id"`
} }
type GroupSkill struct {
ID SkillID `json:"skill_id"`
GroupRate int8 `json:"group_rate"`
Rarity int8 `json:"rarity"`
}
// Activation is the parameters controlling when a skill activates. // Activation is the parameters controlling when a skill activates.
type Activation struct { type Activation struct {
Precondition string `json:"precondition,omitzero"` Precondition string `json:"precondition,omitzero"`
+10 -5
View File
@@ -49,11 +49,16 @@ export function convoCharas(c: Conversation): number[] {
export function requiredCharas(c: Conversation): number[] { export function requiredCharas(c: Conversation): number[] {
switch (c.condition_type) { switch (c.condition_type) {
case Required.None: return [] case Required.None:
case Required.All: return convoCharas(c); return [];
case Required.Chara1: return [c.chara_1]; case Required.All:
case Required.Chara2: return [c.chara_2!]; return convoCharas(c);
case Required.Chara3: return [c.chara_3!]; case Required.Chara1:
return [c.chara_1];
case Required.Chara2:
return [c.chara_2!];
case Required.Chara3:
return [c.chara_3!];
} }
} }
+8 -16
View File
@@ -1143,22 +1143,14 @@
>. (This race is why I made it.) >. (This race is why I made it.)
</p> </p>
<Sec h={3} id="jbc-sprint">JBC Sprint (Ooi ver.)</Sec> <div id="jbc-sprint" class="hidden target:block">
<p> <h3>JBC Sprint</h3>
An even more anti-front track is Ooi 1200 Dirt. This one is actively malicious. Visually, it looks like late race starts on a <p>
corner, but the portion before the stretch is a special <i>neither corner nor straight</i> property. That means Angling won't activate, It used to be the case that Oi 1200 Dirt had a strange neither-straight-nor-corner segment at the start of late race so that
and VPP is delayed (though still within the accel period). Angling wouldn't activate. As of the 1 Jul update, the corner is now connected to the final straight, so there is no longer
</p> anything remarkable about this track.
<p> </p>
Fortunately, JBC Sprint is after summer, which means you should be able to stat diff your opponents. If you are planning to </div>
win this race, you may want to prioritize a bit of extra speed training, or take Front Straights/Corners. If MANT, don't be
too surprised if you lose a clock or five to a Taiki Shuttle rival.
</p>
<p>
Since the 1 Jul update, the JBC races take place at a randomly selected one of the NAR courses (Ooi, Kawasaki, Funabashi,
Morioka) each year. If you're hunting for a completionist title on a front runner, you have the option of just not picking an
Ooi JBC Sprint; the others are all Angling tracks.
</p>
<Sec h={3} id="3k">Kikuka Sho &amp; Tenno Sho (Spring)</Sec> <Sec h={3} id="3k">Kikuka Sho &amp; Tenno Sho (Spring)</Sec>
<p> <p>