package main import ( "embed" "errors" "fmt" "io" "regexp" "strings" "text/template" "unicode" ) //go:embed character.kk.template skill.kk.template character.go.template skill.go.template var templates embed.FS // LoadTemplates sets up templates to render game data to source code. func LoadTemplates() (*template.Template, error) { t := template.New("root") t.Funcs(template.FuncMap{ "kkenum": kkenum, "goenum": goenum, }) return t.ParseFS(templates, "*") } // ExecCharacter renders the Koka character module to kk and the Go character file to g. // If either is nil, it is skipped. func ExecCharacter(t *template.Template, kk, g io.Writer, c []NamedID[Character], pairs, trios []AffinityRelation) error { if len(pairs) != len(c)*len(c) { return fmt.Errorf("there are %d pairs but there must be %d for %d characters", len(pairs), len(c)*len(c), len(c)) } if len(trios) != len(c)*len(c)*len(c) { return fmt.Errorf("there are %d trios but there must be %d for %d characters", len(trios), len(c)*len(c)*len(c), len(c)) } maxid := 0 pm := make(map[int]map[int]int, len(c)) tm := make(map[int]map[int]map[int]int, len(c)) for _, u := range c { maxid = max(maxid, u.ID) pm[u.ID] = make(map[int]int, len(c)) tm[u.ID] = make(map[int]map[int]int, len(c)) for _, v := range c { tm[u.ID][v.ID] = make(map[int]int, len(c)) } } for _, p := range pairs { pm[p.IDA][p.IDB] = p.Affinity } for _, t := range trios { tm[t.IDA][t.IDB][t.IDC] = t.Affinity } data := struct { Characters []NamedID[Character] Pairs []AffinityRelation Trios []AffinityRelation PairMaps map[int]map[int]int TrioMaps map[int]map[int]map[int]int Count int MaxID int }{c, pairs, trios, pm, tm, len(c), maxid} var err error if kk != nil { err = errors.Join(t.ExecuteTemplate(kk, "koka-character", &data)) } if g != nil { err = errors.Join(t.ExecuteTemplate(g, "go-character", &data)) } return err } func ExecSkill(t *template.Template, kk, g io.Writer, groups []NamedID[SkillGroup], skills []Skill) error { m := make(map[int][]Skill, len(groups)) u := make(map[int]int, len(groups)) for _, t := range skills { m[t.GroupID] = append(m[t.GroupID], t) if t.Rarity >= 4 { // Add inheritable uniques to u so we can add inherited versions to groups. u[t.ID] = t.GroupID } } // Now that u is set up, iterate through again and add in inherited skills. for _, t := range skills { if t.InheritID != 0 { m[u[t.InheritID]] = append(m[u[t.InheritID]], t) } } data := struct { Groups []NamedID[SkillGroup] Skills []Skill Related map[int][]Skill }{groups, skills, m} var err error if kk != nil { err = errors.Join(t.ExecuteTemplate(kk, "koka-skill", &data)) } if g != nil { err = errors.Join(t.ExecuteTemplate(g, "go-skill-data", &data)) } return err } 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 wordSeps = " ,!?/-+();#○☆♡'=♪∀゚∴" var ( kkReplace = func() *strings.Replacer { r := []string{ "Triple 7s", "Triple-Sevens", // hard to replace with the right thing automatically "1,500,000 CC", "One-Million-CC", "15,000,000 CC", "Fifteen-Million-CC", "1st", "First", "♡ 3D Nail Art", "Nail-Art", ".", "", "&", "-and-", "'s", "s", "ó", "o", "∞", "Infinity", "×", "x", "◎", "Lv2", } for _, c := range wordSeps { r = append(r, string(c), "-") } return strings.NewReplacer(r...) }() kkMultidash = regexp.MustCompile(`-+`) kkDashNonletter = regexp.MustCompile(`-[^A-Za-z]`) goReplace = func() *strings.Replacer { r := []string{ "Triple 7s", "TripleSevens", "1,500,000 CC", "OneMillionCC", "15,000,000 CC", "FifteenMillionCC", "1st", "First", "♡ 3D Nail Art", "NailArt", ".", "", "&", "And", "'s", "s", "∞", "Infinity", "×", "X", "◎", "Lv2", } for _, c := range wordSeps { r = append(r, string(c), "") } return strings.NewReplacer(r...) }() ) func kkenum(name string) string { orig := name name = kkReplace.Replace(name) name = kkMultidash.ReplaceAllLiteralString(name, "-") name = strings.Trim(name, "-") if len(name) == 0 { panic(fmt.Errorf("%q became empty as Koka enum variant", orig)) } name = strings.ToUpper(name[:1]) + name[1:] if !unicode.IsLetter(rune(name[0])) { panic(fmt.Errorf("Koka enum variant %q (from %q) starts with a non-letter", name, orig)) } for _, c := range name { if c > 127 { // Koka does not allow non-ASCII characters in source code. // Don't proceed if we've missed one. panic(fmt.Errorf("non-ASCII character %q (%[1]U) in Koka enum variant %q (from %q)", c, name, orig)) } } if kkDashNonletter.MatchString(name) { panic(fmt.Errorf("non-letter character after a dash in Koka enum variant %q (from %q)", name, orig)) } return name } func goenum(name string) string { // go names are a bit more lax, so we need fewer checks orig := name name = goReplace.Replace(name) if len(name) == 0 { panic(fmt.Errorf("%q became empty as Go enum variant", orig)) } name = strings.ToUpper(name[:1]) + name[1:] return name }