horsegen: generate skills

This commit is contained in:
2026-01-10 02:30:38 -05:00
parent 5bcdd45b10
commit 05688a08e2
9 changed files with 3382 additions and 529 deletions

View File

@@ -61,25 +61,37 @@ func ExecCharacterKK(t *template.Template, w io.Writer, c []NamedID[Character],
return t.ExecuteTemplate(w, "koka-character", &data)
}
func ExecSkillKK(t *template.Template, w io.Writer, g []NamedID[SkillGroup]) error {
func ExecSkillKK(t *template.Template, w io.Writer, g []NamedID[SkillGroup], s []Skill) error {
data := struct {
Groups []NamedID[SkillGroup]
}{g}
Skills []Skill
}{g, s}
return t.ExecuteTemplate(w, "koka-skill", &data)
}
const replaceDash = " ,!?/+();#○◎☆♡'&=♪∀゚∴"
func ExecSkillGroupKK(t *template.Template, w io.Writer, g []NamedID[SkillGroup], s []Skill) error {
data := struct {
Groups []NamedID[SkillGroup]
Skills []Skill
}{g, s}
return t.ExecuteTemplate(w, "koka-skill-group", &data)
}
const replaceDash = " ,!?/+();#○☆♡'&=♪∀゚∴"
var (
kkReplace = func() *strings.Replacer {
r := []string{
"Triple 7s", "Triple-Sevens", // hard to replace with the right thing automatically
"1,500,000 CC", "Million-CC",
"1,500,000 CC", "One-Million-CC",
"15,000,000 CC", "Fifteen-Million-CC",
"1st", "First",
".", "",
"'s", "s",
"ó", "o",
"∞", "Infinity",
"×", "x",
"◎", "Lv2",
}
for _, c := range replaceDash {
r = append(r, string(c), "-")

View File

@@ -20,6 +20,9 @@ var characterAffinity3SQL string
//go:embed skill-group.sql
var skillGroupSQL string
//go:embed skill.sql
var skillSQL string
type (
Character struct{}
SkillGroup struct{}
@@ -174,3 +177,133 @@ func SkillGroups(ctx context.Context, db *sqlitex.Pool) ([]NamedID[SkillGroup],
}
return r, nil
}
type Skill struct {
ID int
Name string
Description string
GroupID int
GroupName string
Rarity int
GroupRate int
GradeValue int
WitCheck bool
Activations [2]SkillActivation
IconID int
Index int
}
type SkillActivation struct {
Precondition string
Condition string
Duration float64
Cooldown float64
Abilities [3]SkillAbility
}
type SkillAbility struct {
Type int
ValueUsage int
Value float64
Target int
TargetValue int
}
func Skills(ctx context.Context, db *sqlitex.Pool) ([]Skill, error) {
conn, err := db.Take(ctx)
defer db.Put(conn)
if err != nil {
return nil, fmt.Errorf("couldn't get connection for skills: %w", err)
}
stmt, _, err := conn.PrepareTransient(skillSQL)
if err != nil {
return nil, fmt.Errorf("couldn't prepare statement for skills: %w", err)
}
defer stmt.Finalize()
var r []Skill
for {
ok, err := stmt.Step()
if err != nil {
return nil, fmt.Errorf("error stepping skills: %w", err)
}
if !ok {
break
}
s := Skill{
ID: stmt.ColumnInt(0),
Name: stmt.ColumnText(1),
Description: stmt.ColumnText(2),
GroupID: stmt.ColumnInt(3),
GroupName: stmt.ColumnText(4),
Rarity: stmt.ColumnInt(5),
GroupRate: stmt.ColumnInt(6),
GradeValue: stmt.ColumnInt(7),
WitCheck: stmt.ColumnInt(8) != 0,
Activations: [2]SkillActivation{
{
Precondition: stmt.ColumnText(9),
Condition: stmt.ColumnText(10),
Duration: stmt.ColumnFloat(11),
Cooldown: stmt.ColumnFloat(12),
Abilities: [3]SkillAbility{
{
Type: stmt.ColumnInt(13),
ValueUsage: stmt.ColumnInt(14),
Value: stmt.ColumnFloat(15),
Target: stmt.ColumnInt(16),
TargetValue: stmt.ColumnInt(17),
},
{
Type: stmt.ColumnInt(18),
ValueUsage: stmt.ColumnInt(19),
Value: stmt.ColumnFloat(20),
Target: stmt.ColumnInt(21),
TargetValue: stmt.ColumnInt(22),
},
{
Type: stmt.ColumnInt(23),
ValueUsage: stmt.ColumnInt(24),
Value: stmt.ColumnFloat(25),
Target: stmt.ColumnInt(26),
TargetValue: stmt.ColumnInt(27),
},
},
},
{
Precondition: stmt.ColumnText(28),
Condition: stmt.ColumnText(29),
Duration: stmt.ColumnFloat(30),
Cooldown: stmt.ColumnFloat(31),
Abilities: [3]SkillAbility{
{
Type: stmt.ColumnInt(32),
ValueUsage: stmt.ColumnInt(33),
Value: stmt.ColumnFloat(34),
Target: stmt.ColumnInt(35),
TargetValue: stmt.ColumnInt(36),
},
{
Type: stmt.ColumnInt(37),
ValueUsage: stmt.ColumnInt(38),
Value: stmt.ColumnFloat(39),
Target: stmt.ColumnInt(40),
TargetValue: stmt.ColumnInt(41),
},
{
Type: stmt.ColumnInt(42),
ValueUsage: stmt.ColumnInt(43),
Value: stmt.ColumnFloat(44),
Target: stmt.ColumnInt(45),
TargetValue: stmt.ColumnInt(46),
},
},
},
},
IconID: stmt.ColumnInt(47),
Index: stmt.ColumnInt(48),
}
r = append(r, s)
}
return r, nil
}

View File

@@ -47,6 +47,7 @@ func main() {
pairs []AffinityRelation
trios []AffinityRelation
sg []NamedID[SkillGroup]
skills []Skill
)
eg.Go(func() error {
slog.Info("get characters")
@@ -72,6 +73,12 @@ func main() {
sg = r
return err
})
eg.Go(func() error {
slog.Info("get skills")
r, err := Skills(ctx, db)
skills = r
return err
})
if err := eg.Wait(); err != nil {
slog.Error("load", slog.Any("err", err))
os.Exit(1)
@@ -92,7 +99,15 @@ func main() {
return err
}
slog.Info("write skills")
return ExecSkillKK(t, sf, sg)
return ExecSkillKK(t, sf, sg, skills)
})
eg.Go(func() error {
sf, err := os.Create(filepath.Join(out, "skill-group.kk"))
if err != nil {
return err
}
slog.Info("write skill groups")
return ExecSkillGroupKK(t, sf, sg, skills)
})
if err := eg.Wait(); err != nil {
slog.Error("generate", slog.Any("err", err))

View File

@@ -1,7 +1,7 @@
{{ define "koka-skill" -}}
module horse/skill
{{- define "koka-skill-group" -}}
module horse/skill-group
// Automatically generated with the horsegen tool; DO NOT EDIT
// Automatically generated with horsegen; DO NOT EDIT
// Skill groups.
// A skill group may contain white, circle, double-circle, gold, and purple skills
@@ -27,7 +27,7 @@ pub fip(1) fun skill-group/from-id(^id: int): maybe<skill-group>
{{- end }}
_ -> Nothing
// Get names for skill groups.
// Get the name for a skill group.
// Skill group names are the name of the base skill in the group.
pub fun skill-group/show(sg: skill-group): string
match sg
@@ -45,4 +45,291 @@ pub fip fun skill-group/order2(a: skill-group, b: skill-group): order2<skill-gro
pub fun skill-group/(==)(a: skill-group, b: skill-group): bool
a.group-id == b.group-id
{{- end }}
{{- end -}}
{{- define "koka-skill" -}}
module horse/skill
import std/num/float64
pub import horse/skill-group
// Skills instances.
pub type skill
{{- range $s := $.Skills }}
{{ kkenum $s.Name }}
{{- end }}
// Map a skill to its ID.
pub fip fun skill/skill-id(^s: skill): int
match s
{{- range $s := $.Skills }}
{{ kkenum $s.Name }} -> {{ $s.ID }}
{{- end }}
// Get the skill for an ID.
pub fip(1) fun skill/from-id(^id: int): maybe<skill>
match id
{{- range $s := $.Skills }}
{{ $s.ID }} -> Just( {{- kkenum $s.Name -}} )
{{- end }}
_ -> Nothing
// Get the name of a skill.
pub fun skill/show(s: skill): string
match s
{{- range $s := $.Skills }}
{{ kkenum $s.Name }} -> {{ printf "%q" $s.Name }}
{{- end }}
// Compare two skills by ID order.
pub fip fun skill/order2(a: skill, b: skill): order2<skill>
match cmp(a.skill-id, b.skill-id)
Lt -> Lt2(a, b)
Eq -> Eq2(a)
Gt -> Gt2(a, b)
pub fun skill/(==)(a: skill, b: skill): bool
a.skill-id == b.skill-id
// Get complete skill info.
pub fun skill/detail(^s: skill): skill-detail
match s
{{- range $s := $.Skills }}
{{ kkenum $s.Name }} -> {{ template "kk-render-skill-detail" $s }}
{{- end }}
// Details about a skill.
pub struct skill-detail
skill-id: int
name: string
description: string
group: skill-group
rarity: rarity
group-rate: int
grade-value: int
wit-check: bool
activations: list<activation>
icon-id: int
// Automatically generated.
// Shows a string representation of the `skill-detail` type.
pub fun skill-detail/show(this : skill-detail) : e string
match this
Skill-detail(skill-id, name, description, group, rarity, group-rate, grade-value, wit-check, activations, icon-id) -> "Skill-detail(skill-id: " ++ skill-id.show ++ ", name: " ++ name.show ++ ", description: " ++ description.show ++ ", group: " ++ group.show ++ ", rarity: " ++ rarity.show ++ ", group-rate: " ++ group-rate.show ++ ", grade-value: " ++ grade-value.show ++ ", wit-check: " ++ wit-check.show ++ ", activations: " ++ activations.show ++ ", icon-id: " ++ icon-id.show ++ ")"
// Skill rarity.
pub type rarity
Common // white
Rare // gold
Unique-Low // 1*/2* unique
Unique-Upgraded // 3*+ unique on a trainee upgraded from 1*/2*
Unique // base 3* unique
pub fun rarity/show(r: rarity): string
match r
Common -> "Common"
Rare -> "Rare"
Unique-Low -> "Unique (1☆/2☆)"
Unique-Upgraded -> "Unique (3☆+ from 1☆/2☆ upgraded)"
Unique -> "Unique (3☆+)"
// Condition and precondition logic.
pub alias condition = string
// Activation conditions and effects.
// A skill has one or two activations.
pub struct activation
precondition: condition
condition: condition
duration: float64 // seconds
cooldown: float64 // seconds
abilities: list<ability> // one to three elements
pub fun activation/show(a: activation): string
match a
Activation("", condition, -1.0, _, abilities) -> condition ++ " -> " ++ abilities.show
Activation("", condition, duration, cooldown, abilities) | cooldown >= 500.0 -> condition ++ " -> " ++ abilities.show ++ " for " ++ duration.show ++ "s"
Activation("", condition, duration, cooldown, abilities) -> condition ++ " -> " ++ abilities.show ++ " for " ++ duration.show ++ "s on " ++ cooldown.show ++ "s cooldown"
Activation(precondition, condition, -1.0, _, abilities)-> precondition ++ " -> " ++ condition ++ " -> " ++ abilities.show
Activation(precondition, condition, duration, cooldown, abilities) | cooldown >= 500.0 -> precondition ++ " -> " ++ condition ++ " -> " ++ abilities.show ++ " for " ++ duration.show ++ "s"
Activation(precondition, condition, duration, cooldown, abilities) -> precondition ++ "-> " ++ condition ++ " -> " ++ abilities.show ++ " for " ++ duration.show ++ "s on " ++ cooldown.show ++ "s cooldown"
// Effects of activating a skill.
pub struct ability
ability-type: ability-type
value-usage: value-usage
target: target
pub fun ability/show(a: ability): string
match a
Ability(t, Direct, Self) -> t.show
Ability(t, Direct, target) -> t.show ++ " " ++ target.show
Ability(t, v, Self) -> t.show ++ " scaling by " ++ v.show
Ability(t, v, target) -> t.show ++ " " ++ target.show ++ " scaling by " ++ v.show
// Target of a skill activation effect.
pub type ability-type
Passive-Speed(bonus: float64)
Passive-Stamina(bonus: float64)
Passive-Power(bonus: float64)
Passive-Guts(bonus: float64)
Passive-Wit(bonus: float64)
Runaway
Vision(bonus: float64)
HP(rate: float64)
Gate-Delay(rate: float64)
Frenzy(add: float64)
Current-Speed(rate: float64)
Target-Speed(rate: float64)
Lane-Speed(rate: float64)
Accel(rate: float64)
Lane-Change(rate: float64)
pub fun ability-type/show(a: ability-type): string
match a
Passive-Speed(bonus) -> "passive " ++ bonus.show ++ " Speed"
Passive-Stamina(bonus) -> "passive " ++ bonus.show ++ " Stamina"
Passive-Power(bonus) -> "passive " ++ bonus.show ++ " Power"
Passive-Guts(bonus) -> "passive " ++ bonus.show ++ " Guts"
Passive-Wit(bonus) -> "passive " ++ bonus.show ++ " Wit"
Runaway -> "enable Great Escape style"
Vision(bonus) -> bonus.show ++ " vision"
HP(rate) | rate >= 0.0 -> show(rate * 100.0) ++ "% HP recovery"
HP(rate) -> show(rate * 100.0) ++ "% HP loss"
Gate-Delay(rate) -> rate.show ++ "× gate delay"
Frenzy(add) -> add.show ++ "s longer Rushed"
Current-Speed(rate) -> show(rate * 100.0) ++ "% current speed"
Target-Speed(rate) -> show(rate * 100.0) ++ "% target speed"
Lane-Speed(rate) -> show(rate * 100.0) ++ "% lane speed"
Accel(rate) -> show(rate * 100.0) ++ "% acceleration"
Lane-Change(rate) -> rate.show ++ " course width movement"
// Special scaling for skill activation effects.
pub type value-usage
Direct
Team-Speed
Team-Stamina
Team-Power
Team-Guts
Team-Wit
Multiply-Random
pub fun value-usage/show(v: value-usage): string
match v
Direct -> "no scaling"
Team-Speed -> "team's Speed"
Team-Stamina -> "team's Stamina"
Team-Power -> "team's Power"
Team-Guts -> "team's Guts"
Team-Wit -> "team's Wit"
Multiply-Random -> "random multiplier (0× to 0.04×)"
// Who a skill activation targets.
pub type target
Self
In-View
Ahead(limit: int)
Behind(limit: int)
Style(style: style)
Rushing-Ahead(limit: int)
Rushing-Behind(limit: int)
Rushing-Style(style: style)
pub fun target/show(t: target): string
match t
Self -> "self"
In-View -> "others in field of view"
Ahead(limit) | limit >= 18 -> "others ahead"
Ahead(limit) -> "next " ++ limit.show ++ " others ahead"
Behind(limit) | limit >= 18 -> "others behind"
Behind(limit) -> "next " ++ limit.show ++ " others behind"
Style(Front-Runner) -> "other Front Runners"
Style(Pace-Chaser) -> "other Pace Chasers"
Style(Late-Surger) -> "other Late Surgers"
Style(End-Closer) -> "other End Closers"
Rushing-Ahead(limit) | limit >= 18 -> "others rushing ahead"
Rushing-Ahead(limit) -> "next " ++ limit.show ++ " others rushing ahead"
Rushing-Behind(limit) | limit >= 18 -> "others rushing behind"
Rushing-Behind(limit) -> "next " ++ limit.show ++ " others rushing behind"
Rushing-Style(Front-Runner) -> "rushing Front Runners"
Rushing-Style(Pace-Chaser) -> "rushing Pace Chasers"
Rushing-Style(Late-Surger) -> "rushing Late Surgers"
Rushing-Style(End-Closer) -> "rushing End Closers"
// Running style for skill targets.
// TODO(zeph): there is definitely a better place for this to live
pub type style
Front-Runner
Pace-Chaser
Late-Surger
End-Closer
{{- end -}}
{{ define "kk-render-skill-detail" }}
{{- /* Call with Skill structure as argument. */ -}}
Skill-detail(skill-id = {{ $.ID -}}
, name = {{ printf "%q" $.Name -}}
, description = {{ printf "%q" $.Description -}}
, group = {{ kkenum $.GroupName -}}
, rarity = {{ if eq $.Rarity 1 }}Common{{ else if eq $.Rarity 2 }}Rare{{ else if eq $.Rarity 3 }}Unique-Low{{ else if eq $.Rarity 4 }}Unique-Upgraded{{ else if eq $.Rarity 5 }}Unique{{ else }}??? $.Rarity={{ $.Rarity }}{{ end -}}
, group-rate = {{ $.GroupRate -}}
, grade-value = {{ $.GradeValue -}}
, wit-check = {{ if $.WitCheck }}True{{ else }}False{{ end -}}
, activations = [
{{- range $a := $.Activations -}}
{{- if ne $a.Condition "" -}}
Activation(precondition = {{ printf "%q" $a.Precondition -}}
, condition = {{ printf "%q" $a.Condition -}}
, duration = {{ printf "%f" $a.Duration -}}
, cooldown = {{ printf "%f" $a.Cooldown -}}
, abilities = [
{{- range $abil := $a.Abilities -}}
{{- if ne $abil.Type 0 -}}
Ability(ability-type =
{{- if eq $abil.Type 1 -}}Passive-Speed({{ printf "%f" $abil.Value }})
{{- else if eq $abil.Type 2 -}}Passive-Stamina({{ printf "%f" $abil.Value }})
{{- else if eq $abil.Type 3 -}}Passive-Power({{ printf "%f" $abil.Value }})
{{- else if eq $abil.Type 4 -}}Passive-Guts({{ printf "%f" $abil.Value }})
{{- else if eq $abil.Type 5 -}}Passive-Wit({{ printf "%f" $abil.Value }})
{{- else if eq $abil.Type 6 -}}Runaway
{{- else if eq $abil.Type 8 -}}Vision({{ printf "%f" $abil.Value }})
{{- else if eq $abil.Type 9 -}}HP({{ printf "%f" $abil.Value }})
{{- else if eq $abil.Type 10 -}}Gate-Delay({{ printf "%f" $abil.Value }})
{{- else if eq $abil.Type 13 -}}Frenzy({{ printf "%f" $abil.Value }})
{{- else if eq $abil.Type 21 -}}Current-Speed({{ printf "%f" $abil.Value }})
{{- else if eq $abil.Type 27 -}}Target-Speed({{ printf "%f" $abil.Value }})
{{- else if eq $abil.Type 28 -}}Lane-Speed({{ printf "%f" $abil.Value }})
{{- else if eq $abil.Type 31 -}}Accel({{ printf "%f" $abil.Value }})
{{- else if eq $abil.Type 35 -}}Lane-Change({{ printf "%f" $abil.Value }})
{{- else -}}??? $abil.Type={{$abil.Type}}
{{- end -}}
, value-usage =
{{- if eq $abil.ValueUsage 1 -}}Direct
{{- else if eq $abil.ValueUsage 3 -}}Team-Speed
{{- else if eq $abil.ValueUsage 4 -}}Team-Stamina
{{- else if eq $abil.ValueUsage 5 -}}Team-Power
{{- else if eq $abil.ValueUsage 6 -}}Team-Guts
{{- else if eq $abil.ValueUsage 7 -}}Team-Wit
{{- else if eq $abil.ValueUsage 8 -}}Multiply-Random
{{- else -}}??? $abil.ValueUsage={{ $abil.ValueUsage }}
{{- end -}}
, target =
{{- if eq $abil.Target 1 -}}Self
{{- else if eq $abil.Target 4 -}}In-View
{{- else if eq $abil.Target 9 -}}Ahead({{ $abil.TargetValue }})
{{- else if eq $abil.Target 10 -}}Behind({{ $abil.TargetValue }})
{{- else if eq $abil.Target 18 -}}Style({{ if eq $abil.TargetValue 1 }}Front-Runner{{ else if eq $abil.TargetValue 2 }}Pace-Chaser{{ else if eq $abil.TargetValue 3 }}Late-Surger{{ else if eq $abil.TargetValue 4 }}End-Closer{{ else }}??? $abil.TargetValue={{ $abil.TargetValue }}{{ end }})
{{- else if eq $abil.Target 19 -}}Rushing-Ahead({{ $abil.TargetValue }})
{{- else if eq $abil.Target 20 -}}Rushing-Behind({{ $abil.TargetValue }})
{{- else if eq $abil.Target 21 -}}Rushing-Style({{ if eq $abil.TargetValue 1 }}Front-Runner{{ else if eq $abil.TargetValue 2 }}Pace-Chaser{{ else if eq $abil.TargetValue 3 }}Late-Surger{{ else if eq $abil.TargetValue 4 }}End-Closer{{ else }}??? $abil.TargetValue={{ $abil.TargetValue }}{{ end }})
{{- end -}}
),
{{- end -}}
{{- end -}}
]),
{{- end -}}
{{- end -}}
], icon-id = {{ $.IconID -}}
)
{{- end -}}

69
horsegen/skill.sql Normal file
View File

@@ -0,0 +1,69 @@
WITH skill_names AS (
SELECT
n."index" AS "id",
n."text" AS "name",
d."text" AS "description"
FROM text_data n
JOIN text_data d ON n."index" = d."index" AND n."category" = 47 AND d."category" = 48
), skill_groups AS (
SELECT
group_id,
name
FROM skill_data d
JOIN skill_names n ON d.id = n.id
WHERE group_rate = 1
)
SELECT
d.id,
n.name,
n.description,
d.group_id,
g.name,
d.rarity,
d.group_rate,
d.grade_value,
d.activate_lot,
d.precondition_1,
d.condition_1,
IIF(d.float_ability_time_1 <= 0, CAST(d.float_ability_time_1 AS REAL), d.float_ability_time_1 / 1e4) AS float_ability_time_1,
IIF(d.float_cooldown_time_1 <= 0, CAST(d.float_cooldown_time_1 AS REAL), d.float_cooldown_time_1 / 1e4) AS float_cooldown_time_1,
d.ability_type_1_1,
d.ability_value_usage_1_1,
d.float_ability_value_1_1 / 1e4 AS float_ability_value_1_1,
d.target_type_1_1,
d.target_value_1_1,
d.ability_type_1_2,
d.ability_value_usage_1_2,
d.float_ability_value_1_2 / 1e4 AS float_ability_value_1_2,
d.target_type_1_2,
d.target_value_1_2,
d.ability_type_1_3,
d.ability_value_usage_1_3,
d.float_ability_value_1_3 / 1e4 AS float_ability_value_1_3,
d.target_type_1_3,
d.target_value_1_3,
d.precondition_2,
d.condition_2,
IIF(d.float_ability_time_2 <= 0, CAST(d.float_ability_time_2 AS REAL), d.float_ability_time_2 / 1e4) AS float_ability_time_2,
IIF(d.float_cooldown_time_2 <= 0, CAST(d.float_cooldown_time_2 AS REAL), d.float_cooldown_time_2 / 1e4) AS float_cooldown_time_2,
d.ability_type_2_1,
d.ability_value_usage_2_1,
d.float_ability_value_2_1 / 1e4 AS float_ability_value_2_1,
d.target_type_2_1,
d.target_value_2_1,
d.ability_type_2_2,
d.ability_value_usage_2_2,
d.float_ability_value_2_2 / 1e4 AS float_ability_value_2_2,
d.target_type_2_2,
d.target_value_2_2,
d.ability_type_2_3,
d.ability_value_usage_2_3,
d.float_ability_value_2_3 / 1e4 AS float_ability_value_2_3,
d.target_type_2_3,
d.target_value_2_3,
d.icon_id,
ROW_NUMBER() OVER (ORDER BY d.id) - 1 AS "index"
FROM skill_data d
JOIN skill_names n ON d.id = n.id
JOIN skill_groups g ON d.group_id = g.group_id
ORDER BY d.id